package handler import ( "net/http" "strconv" "time" "tcm-agent/internal/agent" "tcm-agent/internal/model/entity" "github.com/gin-gonic/gin" ) // ======================================================================== // 处方 HTTP 接口处理器 // ======================================================================== // 职责链: // HTTP 请求 → 参数校验 → 调用 Agent → 规则校验 → 持久化 → 响应 // ======================================================================== // PrescriptionHandler 处方接口处理器 type PrescriptionHandler struct { agentRunner *agent.Runner rxAgent *agent.PrescriptionGenerator } // NewPrescriptionHandler 创建处方处理器 // // 参数: // // runner - Agent 引擎 // scene - 场景名(为空则使用默认值 "prescription") func NewPrescriptionHandler(runner *agent.Runner, scene string) *PrescriptionHandler { return &PrescriptionHandler{ agentRunner: runner, rxAgent: agent.NewPrescriptionGenerator(runner, scene), } } // PrescriptionGenerateRequest 处方生成请求体(避免与 emr_handler.GenerateRequest 重名) type PrescriptionGenerateRequest struct { PatientID string `json:"patient_id" binding:"required"` EMRText string `json:"emr_text" binding:"required"` Diagnosis string `json:"diagnosis" binding:"required"` Age int `json:"age"` IsPregnant bool `json:"is_pregnant"` Allergies []string `json:"allergies"` } // Generate 根据病历生成处方 // // POST /api/v1/prescription/generate func (h *PrescriptionHandler) Generate(c *gin.Context) { // ===== ① 参数校验 ===== var req PrescriptionGenerateRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "参数错误", "detail": err.Error(), "example": `{"patient_id":"P001","emr_text":"...","diagnosis":"痰湿中阻证","age":45,"is_pregnant":false,"allergies":[]}`, }) return } doctorID := c.GetString("user_id") // ===== ②~⑤ 调用 Agent ===== agentReq := &agent.PrescriptionRequest{ PatientID: req.PatientID, EMRText: req.EMRText, Diagnosis: req.Diagnosis, Age: req.Age, IsPregnant: req.IsPregnant, Allergies: req.Allergies, } resp, err := h.rxAgent.Generate(c.Request.Context(), agentReq) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "处方生成失败", "detail": err.Error(), }) return } // ===== ⑥ 持久化 ===== rx := &entity.Prescription{ PatientID: req.PatientID, DoctorID: doctorID, SessionID: resp.SessionID, Draft: resp.Draft, Status: resp.Status, Blocked: resp.Blocked, Warnings: sliceToJSON(resp.Warnings), CreatedAt: time.Now(), UpdatedAt: time.Now(), } // dao.Prescription.Create(rx) // ===== ⑦ 返回响应 ===== httpStatus := http.StatusOK if resp.Blocked { httpStatus = http.StatusConflict // 409 } c.JSON(httpStatus, gin.H{ "code": httpStatus, "message": getPrescriptionMessage(resp.Status), "data": gin.H{ "session_id": resp.SessionID, "prescription": resp.Prescription, "draft": resp.Draft, "warnings": resp.Warnings, "blocked": resp.Blocked, "status": resp.Status, "rx_id": rx.ID, "next_action": getPrescriptionNextAction(resp.Status), }, }) } // Validate 仅校验处方(不生成) // // POST /api/v1/prescription/validate func (h *PrescriptionHandler) Validate(c *gin.Context) { var req struct { PrescriptionText string `json:"prescription_text" binding:"required"` PatientID string `json:"patient_id"` IsPregnant bool `json:"is_pregnant"` Allergies []string `json:"allergies"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{"error": "处方文本不能为空"}) return } agentReq := &agent.PrescriptionRequest{ PatientID: req.PatientID, IsPregnant: req.IsPregnant, Allergies: req.Allergies, } warnings, blocked := h.rxAgent.Validate(c.Request.Context(), req.PrescriptionText, agentReq) c.JSON(200, gin.H{ "code": 200, "data": gin.H{ "warnings": warnings, "blocked": blocked, "safe": !blocked && len(warnings) == 0, }, }) } // GetByID 查询处方详情 // // GET /api/v1/prescription/:id func (h *PrescriptionHandler) GetByID(c *gin.Context) { idStr := c.Param("id") id, _ := strconv.ParseInt(idStr, 10, 64) c.JSON(200, gin.H{ "code": 200, "data": gin.H{ "id": id, "note": "实际项目从数据库查询处方详情", }, }) } // Approve 医生审核确认处方 // // POST /api/v1/prescription/:id/approve // // 这是 Human-in-the-Loop 的关键环节: // AI 生成 → 规则校验 → 医生最终审核 → 生效 func (h *PrescriptionHandler) Approve(c *gin.Context) { idStr := c.Param("id") id, _ := strconv.ParseInt(idStr, 10, 64) var req struct { Approved bool `json:"approved"` DoctorNote string `json:"doctor_note"` ModifiedRx string `json:"modified_rx"` } c.ShouldBindJSON(&req) doctorID := c.GetString("user_id") logAudit("prescription_approve", doctorID, idStr, req.DoctorNote) status := "approved" if !req.Approved { status = "rejected" } c.JSON(200, gin.H{ "code": 200, "message": "审核完成", "data": gin.H{ "id": id, "status": status, "approved_by": doctorID, "doctor_note": req.DoctorNote, "modified_rx": req.ModifiedRx, }, }) } // getPrescriptionMessage 根据状态返回提示信息 func getPrescriptionMessage(status string) string { switch status { case "success": return "处方生成成功,请医生审核" case "blocked": return "处方被安全规则拦截,已自动修正,请查看" case "need_review": return "处方存在警告,需医生重点关注" default: return "处方生成完成" } } // getPrescriptionNextAction 下一步操作建议 func getPrescriptionNextAction(status string) string { switch status { case "success": return "请医生审核处方并确认" case "blocked": return "处方已被拦截修正,请医生重新审阅" case "need_review": return "存在安全警告,请医生评估后决定" default: return "请检查输入信息" } } // sliceToJSON 将字符串切片转为 JSON 字符串(用于数据库存储) func sliceToJSON(s []string) string { if len(s) == 0 { return "[]" } // 简单拼接,生产环境用 json.Marshal result := "[" for i, v := range s { if i > 0 { result += "," } result += `"` + v + `"` } result += "]" return result }