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, }