Files
xk-ai-agent/internal/handler/prescription_handler.go
2026-08-14 21:50:48 +08:00

253 lines
6.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}