| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393 |
- from typing import Optional
- import json
- import csv
- from io import StringIO
- from datetime import datetime, timezone
- from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File
- from pydantic import BaseModel, Field
- from app.core.security import get_current_user
- from app.core.config import get_settings
- settings = get_settings()
- router = APIRouter(prefix="/admin/knowledge", tags=["管理-知识库"])
- # ============================================
- # Schemas
- # ============================================
- class DrugCreate(BaseModel):
- drug_id: str
- name: str
- name_en: Optional[str] = None
- pinyin: Optional[str] = None
- category: Optional[str] = None
- subcategory: Optional[str] = None
- approval_number: Optional[str] = None
- sections: Optional[dict] = None
- source_version: Optional[str] = None
- source_volume: Optional[str] = None
- source_page: Optional[str] = None
- class DrugUpdate(BaseModel):
- name: Optional[str] = None
- name_en: Optional[str] = None
- pinyin: Optional[str] = None
- category: Optional[str] = None
- subcategory: Optional[str] = None
- approval_number: Optional[str] = None
- sections: Optional[dict] = None
- source_version: Optional[str] = None
- source_volume: Optional[str] = None
- source_page: Optional[str] = None
- is_active: Optional[bool] = None
- class KnowledgePointCreate(BaseModel):
- point_id: str
- subject: str
- chapter_id: str
- chapter_name: str
- title: str
- content: str
- difficulty: int = Field(default=1, ge=1, le=5)
- frequency: Optional[str] = None
- related_drugs: Optional[list] = None
- source: Optional[str] = None
- class KnowledgePointUpdate(BaseModel):
- chapter_name: Optional[str] = None
- title: Optional[str] = None
- content: Optional[str] = None
- difficulty: Optional[int] = Field(default=None, ge=1, le=5)
- frequency: Optional[str] = None
- related_drugs: Optional[list] = None
- source: Optional[str] = None
- class QuestionCreate(BaseModel):
- question_id: str
- question_type: str
- subject: str
- chapter_id: str
- difficulty: int = Field(default=1, ge=1, le=5)
- content: str
- options: list[str]
- answer: str
- explanation: str
- knowledge_point_ids: Optional[list[str]] = None
- source: Optional[str] = None
- frequency: Optional[str] = None
- class QuestionUpdate(BaseModel):
- question_type: Optional[str] = None
- subject: Optional[str] = None
- chapter_id: Optional[str] = None
- difficulty: Optional[int] = Field(default=None, ge=1, le=5)
- content: Optional[str] = None
- options: Optional[list[str]] = None
- answer: Optional[str] = None
- explanation: Optional[str] = None
- knowledge_point_ids: Optional[list[str]] = None
- source: Optional[str] = None
- frequency: Optional[str] = None
- audited: Optional[bool] = None
- class ReindexRequest(BaseModel):
- collection: str = Field(default="drug_entries", description="drug_entries / exam_knowledge")
- drug_ids: Optional[list[str]] = Field(default=None, description="指定药品ID,不填则全量重建")
- class ImportRequest(BaseModel):
- type: str = Field(..., description="drug / knowledge_point / question")
- format: str = Field(default="json", description="json / csv")
- data: str = Field(..., description="JSON/CSV 内容字符串")
- class BatchAuditRequest(BaseModel):
- question_ids: list[str]
- audited: bool = True
- # ============================================
- # 药品管理 CRUD
- # ============================================
- @router.get("/drugs")
- async def list_drugs(
- keyword: Optional[str] = Query(None, description="药品名称搜索"),
- category: Optional[str] = Query(None, description="药品分类"),
- page: int = Query(1, ge=1),
- page_size: int = Query(20, ge=1, le=100),
- user: dict = Depends(get_current_user),
- ):
- return {
- "items": [],
- "total": 0,
- "page": page,
- "page_size": page_size,
- "message": "数据入库后可用",
- }
- @router.get("/drugs/{drug_id}")
- async def get_drug(drug_id: str, user: dict = Depends(get_current_user)):
- return {"message": "数据入库后可用", "drug_id": drug_id}
- @router.post("/drugs")
- async def create_drug(
- drug: DrugCreate,
- user: dict = Depends(get_current_user),
- ):
- return {"message": "已创建", "drug_id": drug.drug_id}
- @router.put("/drugs/{drug_id}")
- async def update_drug(
- drug_id: str,
- drug: DrugUpdate,
- user: dict = Depends(get_current_user),
- ):
- return {"message": "已更新", "drug_id": drug_id}
- @router.delete("/drugs/{drug_id}")
- async def delete_drug(drug_id: str, user: dict = Depends(get_current_user)):
- return {"message": "已标记删除", "drug_id": drug_id}
- @router.post("/drugs/import")
- async def import_drugs(
- req: ImportRequest,
- user: dict = Depends(get_current_user),
- ):
- count = 0
- if req.format == "json":
- try:
- items = json.loads(req.data)
- count = len(items) if isinstance(items, list) else 0
- except json.JSONDecodeError as e:
- raise HTTPException(status_code=400, detail=f"JSON 解析失败: {str(e)}")
- elif req.format == "csv":
- try:
- reader = csv.DictReader(StringIO(req.data))
- count = sum(1 for _ in reader)
- except Exception as e:
- raise HTTPException(status_code=400, detail=f"CSV 解析失败: {str(e)}")
- return {
- "message": "导入任务已提交",
- "total": count,
- "success": 0,
- "failed": 0,
- "note": "Phase 2 实现异步批量写入 + 向量化",
- }
- @router.post("/drugs/reindex")
- async def reindex_drugs(
- req: ReindexRequest,
- user: dict = Depends(get_current_user),
- ):
- return {
- "message": "重建索引任务已提交",
- "collection": req.collection,
- "drug_ids": req.drug_ids,
- "status": "pending",
- "note": "Phase 2 实现:重新 chunk → 向量化 → 写入 Milvus",
- }
- # ============================================
- # 知识点管理 CRUD
- # ============================================
- @router.get("/knowledge-points")
- async def list_knowledge_points(
- subject: Optional[str] = Query(None),
- chapter_id: Optional[str] = Query(None),
- difficulty: Optional[int] = Query(None, ge=1, le=5),
- frequency: Optional[str] = Query(None),
- page: int = Query(1, ge=1),
- page_size: int = Query(20, ge=1, le=100),
- user: dict = Depends(get_current_user),
- ):
- return {
- "items": [],
- "total": 0,
- "page": page,
- "page_size": page_size,
- }
- @router.get("/knowledge-points/{point_id}")
- async def get_knowledge_point(point_id: str, user: dict = Depends(get_current_user)):
- return {"message": "数据入库后可用", "point_id": point_id}
- @router.post("/knowledge-points")
- async def create_knowledge_point(
- kp: KnowledgePointCreate,
- user: dict = Depends(get_current_user),
- ):
- return {"message": "已创建", "point_id": kp.point_id}
- @router.put("/knowledge-points/{point_id}")
- async def update_knowledge_point(
- point_id: str,
- kp: KnowledgePointUpdate,
- user: dict = Depends(get_current_user),
- ):
- return {"message": "已更新", "point_id": point_id}
- @router.delete("/knowledge-points/{point_id}")
- async def delete_knowledge_point(point_id: str, user: dict = Depends(get_current_user)):
- return {"message": "已删除", "point_id": point_id}
- @router.post("/knowledge-points/import")
- async def import_knowledge_points(
- req: ImportRequest,
- user: dict = Depends(get_current_user),
- ):
- count = 0
- try:
- items = json.loads(req.data)
- count = len(items) if isinstance(items, list) else 0
- except json.JSONDecodeError as e:
- raise HTTPException(status_code=400, detail=f"JSON 解析失败: {str(e)}")
- return {
- "message": "导入任务已提交",
- "total": count,
- "success": 0,
- "failed": 0,
- }
- # ============================================
- # 题库管理 CRUD
- # ============================================
- @router.get("/questions")
- async def list_questions(
- subject: Optional[str] = Query(None),
- chapter_id: Optional[str] = Query(None),
- question_type: Optional[str] = Query(None),
- difficulty: Optional[int] = Query(None, ge=1, le=5),
- audited: Optional[bool] = Query(None),
- page: int = Query(1, ge=1),
- page_size: int = Query(20, ge=1, le=100),
- user: dict = Depends(get_current_user),
- ):
- return {
- "items": [],
- "total": 0,
- "page": page,
- "page_size": page_size,
- }
- @router.get("/questions/{question_id}")
- async def get_question(question_id: str, user: dict = Depends(get_current_user)):
- return {"message": "数据入库后可用", "question_id": question_id}
- @router.post("/questions")
- async def create_question(
- q: QuestionCreate,
- user: dict = Depends(get_current_user),
- ):
- return {"message": "已创建", "question_id": q.question_id}
- @router.put("/questions/{question_id}")
- async def update_question(
- question_id: str,
- q: QuestionUpdate,
- user: dict = Depends(get_current_user),
- ):
- return {"message": "已更新", "question_id": question_id}
- @router.delete("/questions/{question_id}")
- async def delete_question(question_id: str, user: dict = Depends(get_current_user)):
- return {"message": "已删除", "question_id": question_id}
- @router.post("/questions/batch-audit")
- async def batch_audit_questions(
- req: BatchAuditRequest,
- user: dict = Depends(get_current_user),
- ):
- return {
- "message": "批量审核完成",
- "question_ids": req.question_ids,
- "audited": req.audited,
- }
- @router.post("/questions/import")
- async def import_questions(
- req: ImportRequest,
- user: dict = Depends(get_current_user),
- ):
- count = 0
- try:
- items = json.loads(req.data)
- count = len(items) if isinstance(items, list) else 0
- except json.JSONDecodeError as e:
- raise HTTPException(status_code=400, detail=f"JSON 解析失败: {str(e)}")
- return {
- "message": "导入任务已提交",
- "total": count,
- "success": 0,
- "failed": 0,
- }
- @router.post("/questions/generate")
- async def ai_generate_questions(
- point_id: str = Query(..., description="知识点ID"),
- count: int = Query(10, ge=1, le=50),
- question_type: str = Query("A", description="A/B/X"),
- user: dict = Depends(get_current_user),
- ):
- return {
- "message": "AI 出题任务已提交",
- "point_id": point_id,
- "count": count,
- "question_type": question_type,
- "status": "pending",
- "note": "Phase 2 实现:LLM 根据知识点生成题目",
- }
- # ============================================
- # 知识库状态与统计
- # ============================================
- @router.get("/stats")
- async def knowledge_stats(user: dict = Depends(get_current_user)):
- return {
- "drugs": {"total": 0, "active": 0, "with_chunks": 0},
- "knowledge_points": {"total": 0, "by_subject": {}},
- "questions": {"total": 0, "audited": 0, "pending": 0},
- "vector_index": {
- "status": "empty",
- "collection": settings.milvus_collection,
- "last_reindex": None,
- },
- "last_data_update": None,
- }
|