253 lines
6.4 KiB
Go
253 lines
6.4 KiB
Go
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
|
||
}
|