package handler import ( "encoding/json" "fmt" "net/http" "strings" "time" "silk-server-go/internal/middleware" "silk-server-go/internal/model" "github.com/gin-gonic/gin" "gorm.io/gorm" "gorm.io/gorm/clause" ) // RegisterDetectionTaskRoutes 注册统一检测任务、样本和发病事件路由。 func RegisterDetectionTaskRoutes(rg *gin.RouterGroup, db *gorm.DB) { lampRead := middleware.RequirePermission(db, "lamp:read") lampWrite := middleware.RequirePermission(db, "lamp:write") traceRead := middleware.RequirePermission(db, "trace:read") traceWrite := middleware.RequirePermission(db, "trace:write") rg.GET("/detection-tasks", lampRead, listDetectionTasks(db)) rg.POST("/detection-tasks", lampWrite, createDetectionTask(db)) rg.GET("/detection-tasks/:id", lampRead, getDetectionTask(db)) rg.PATCH("/detection-tasks/:id", lampWrite, updateDetectionTask(db)) rg.GET("/detection-tasks/:id/samples", lampRead, listSamples(db)) rg.POST("/detection-tasks/:id/samples", lampWrite, createSample(db)) rg.PATCH("/samples/:id", lampWrite, updateSample(db)) rg.GET("/disease-events", traceRead, listDiseaseEvents(db)) rg.POST("/disease-events", traceWrite, createDiseaseEvent(db)) rg.PATCH("/disease-events/:id", traceWrite, updateDiseaseEvent(db)) } func listDetectionTasks(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { q := db.Model(&model.DetectionTask{}) if status := c.Query("status"); status != "" { q = q.Where("status = ?", status) } if room := c.Query("roomId"); room != "" { q = q.Where("room_id = ?", room) } if sourceType := c.Query("sourceType"); sourceType != "" { q = q.Where("source_type = ?", sourceType) } var list []model.DetectionTask q.Order("created_at DESC").Limit(200).Find(&list) fillDetectionTaskRoomNames(db, list) c.JSON(http.StatusOK, list) } } func getDetectionTask(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { var task model.DetectionTask if db.Where("id = ?", c.Param("id")).First(&task).Error != nil { c.JSON(http.StatusNotFound, gin.H{"error": "detection task not found"}) return } fillDetectionTaskRoomNames(db, []model.DetectionTask{task}) c.JSON(http.StatusOK, task) } } func createDetectionTask(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { var body struct { SourceKey string `json:"sourceKey"` SourceType string `json:"sourceType"` SourceID string `json:"sourceId"` RoomID *string `json:"roomId"` BatchID *string `json:"batchId"` InspectionID *string `json:"inspectionId"` Disease string `json:"disease"` RecommendedMethod string `json:"recommendedMethod"` Method *string `json:"method"` Priority string `json:"priority"` Note *string `json:"note"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if body.Disease == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "病种不能为空"}) return } if body.Method != nil && !model.ValidDetectionMethod(*body.Method) { c.JSON(http.StatusBadRequest, gin.H{"error": "method 仅支持 lamp/qpcr/sers/hyperspectral"}) return } for _, id := range []*string{body.RoomID, body.BatchID, body.InspectionID} { if id != nil && *id != "" && !isUUID(*id) { c.JSON(http.StatusBadRequest, gin.H{"error": "关联 ID 不是合法的 UUID"}) return } } sourceKey := body.SourceKey if sourceKey == "" { sourceKey = fmt.Sprintf("manual-%d", time.Now().UnixNano()) } sourceType := body.SourceType if sourceType == "" { sourceType = "manual" } priority := body.Priority if priority == "" { priority = "routine" } task := model.DetectionTask{ SourceKey: sourceKey, SourceType: sourceType, SourceID: body.SourceID, RoomID: body.RoomID, BatchID: body.BatchID, InspectionID: body.InspectionID, Disease: body.Disease, RecommendedMethod: body.RecommendedMethod, Method: body.Method, Priority: priority, Status: "pending", CreatedBy: currentUserID(c), Note: body.Note, } if err := db.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "source_key"}}, DoNothing: true, }).Create(&task).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "创建检测任务失败"}) return } if task.ID == "" { _ = db.Where("source_key = ?", sourceKey).First(&task).Error } c.JSON(http.StatusCreated, task) } } func updateDetectionTask(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { var task model.DetectionTask if db.Where("id = ?", c.Param("id")).First(&task).Error != nil { c.JSON(http.StatusNotFound, gin.H{"error": "detection task not found"}) return } var body struct { AssigneeID *string `json:"assigneeId"` Method *string `json:"method"` Status *string `json:"status"` Result *string `json:"result"` CancelledReason *string `json:"cancelledReason"` Note *string `json:"note"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if body.Method != nil && !model.ValidDetectionMethod(*body.Method) { c.JSON(http.StatusBadRequest, gin.H{"error": "method 仅支持 lamp/qpcr/sers/hyperspectral"}) return } if body.Status != nil && !model.ValidDetectionTaskTransition(task.Status, *body.Status) { c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("检测任务状态不能从 %s 转到 %s", task.Status, *body.Status)}) return } if body.Status != nil && *body.Status == "assigned" && body.AssigneeID == nil && task.AssigneeID == nil { c.JSON(http.StatusBadRequest, gin.H{"error": "指派状态必须先分配负责人"}) return } if body.Status != nil && *body.Status == "completed" && body.Result == nil && task.Result == nil { c.JSON(http.StatusBadRequest, gin.H{"error": "完成检测任务前必须录入结果"}) return } if body.Status != nil && *body.Status == "cancelled" && (body.CancelledReason == nil || strings.TrimSpace(*body.CancelledReason) == "") { c.JSON(http.StatusBadRequest, gin.H{"error": "取消任务必须填写原因"}) return } updates := map[string]interface{}{} if body.AssigneeID != nil { updates["assignee_id"] = *body.AssigneeID updates["assigned_at"] = time.Now() } if body.Method != nil { updates["method"] = *body.Method } if body.Note != nil { updates["note"] = *body.Note } if body.Result != nil { if task.Status != "review" { c.JSON(http.StatusBadRequest, gin.H{"error": "结果需在复核阶段录入"}) return } updates["result"] = *body.Result updates["resulted_at"] = time.Now() } if body.Status != nil { updates["status"] = *body.Status if *body.Status == "cancelled" { updates["cancelled_reason"] = *body.CancelledReason } } if len(updates) > 0 { if err := db.Model(&model.DetectionTask{}).Where("id = ?", task.ID).Updates(updates).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "更新检测任务失败"}) return } } db.Where("id = ?", task.ID).First(&task) if task.Result != nil && *task.Result == "positive" { _ = ensureDiseaseEventFromDetectionTask(db, task) } fillDetectionTaskRoomNames(db, []model.DetectionTask{task}) c.JSON(http.StatusOK, task) } } func listSamples(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { var samples []model.Sample db.Where("detection_task_id = ?", c.Param("id")).Order("created_at ASC").Find(&samples) c.JSON(http.StatusOK, samples) } } func createSample(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { var task model.DetectionTask if db.Where("id = ?", c.Param("id")).First(&task).Error != nil { c.JSON(http.StatusNotFound, gin.H{"error": "detection task not found"}) return } var existing model.Sample if db.Where("detection_task_id = ?", task.ID).First(&existing).Error == nil { c.JSON(http.StatusOK, existing) return } var body struct { SampleNo string `json:"sampleNo"` RoomID *string `json:"roomId"` BatchID *string `json:"batchId"` TrayID *string `json:"trayId"` Note *string `json:"note"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } sampleNo := body.SampleNo if sampleNo == "" { sampleNo = fmt.Sprintf("S-%s", strings.ToUpper(task.ID[:8])) } now := time.Now() sample := model.Sample{ DetectionTaskID: task.ID, SampleNo: sampleNo, RoomID: body.RoomID, BatchID: body.BatchID, TrayID: body.TrayID, SampledBy: currentUserID(c), SampledAt: &now, State: "created", Note: body.Note, } if sample.RoomID == nil { sample.RoomID = task.RoomID } if sample.BatchID == nil { sample.BatchID = task.BatchID } if err := db.Create(&sample).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "创建样本失败"}) return } c.JSON(http.StatusCreated, sample) } } func updateSample(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { var sample model.Sample if db.Where("id = ?", c.Param("id")).First(&sample).Error != nil { c.JSON(http.StatusNotFound, gin.H{"error": "sample not found"}) return } var body struct { State *string `json:"state"` Note *string `json:"note"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } updates := map[string]interface{}{} if body.Note != nil { updates["note"] = *body.Note } if body.State != nil { if !model.ValidSampleTransition(sample.State, *body.State) { c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("样本状态不能从 %s 转到 %s", sample.State, *body.State)}) return } updates["state"] = *body.State now := time.Now() switch *body.State { case "collected": updates["collected_at"] = now case "handed_over": updates["handed_over_at"] = now updates["handed_over_by"] = currentUserID(c) case "received": updates["received_at"] = now updates["received_by"] = currentUserID(c) case "testing": updates["testing_started_at"] = now case "consumed": updates["consumed_at"] = now case "disposed": updates["disposed_at"] = now } } if len(updates) > 0 { if err := db.Model(&model.Sample{}).Where("id = ?", sample.ID).Updates(updates).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "更新样本失败"}) return } } db.Where("id = ?", sample.ID).First(&sample) c.JSON(http.StatusOK, sample) } } func listDiseaseEvents(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { q := db.Model(&model.DiseaseEvent{}) if status := c.Query("status"); status != "" { q = q.Where("status = ?", status) } if room := c.Query("roomId"); room != "" { q = q.Where("room_id = ?", room) } var list []model.DiseaseEvent q.Order("created_at DESC").Limit(200).Find(&list) fillDiseaseEventRoomNames(db, list) c.JSON(http.StatusOK, list) } } func createDiseaseEvent(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { var body struct { SourceKey string `json:"sourceKey"` RoomID *string `json:"roomId"` BatchID *string `json:"batchId"` DetectionTaskID *string `json:"detectionTaskId"` LampTestID *string `json:"lampTestId"` ConsultationID *string `json:"consultationId"` InspectionID *string `json:"inspectionId"` Disease string `json:"disease"` Status string `json:"status"` Evidence json.RawMessage `json:"evidence"` LossSummary *string `json:"lossSummary"` Measure *string `json:"measure"` Note *string `json:"note"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if body.Disease == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "病种不能为空"}) return } for _, id := range []*string{body.RoomID, body.BatchID, body.DetectionTaskID, body.LampTestID, body.ConsultationID, body.InspectionID} { if id != nil && *id != "" && !isUUID(*id) { c.JSON(http.StatusBadRequest, gin.H{"error": "关联 ID 不是合法的 UUID"}) return } } if body.Status == "" { body.Status = "suspected" } event := model.DiseaseEvent{ SourceKey: body.SourceKey, RoomID: body.RoomID, BatchID: body.BatchID, DetectionTaskID: body.DetectionTaskID, LampTestID: body.LampTestID, ConsultationID: body.ConsultationID, InspectionID: body.InspectionID, Disease: body.Disease, Status: body.Status, Evidence: body.Evidence, LossSummary: body.LossSummary, Measure: body.Measure, Note: body.Note, } if event.SourceKey == "" { event.SourceKey = fmt.Sprintf("manual-%d", time.Now().UnixNano()) } if err := model.ValidateDiseaseEventEvidence(event); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if event.Status == "confirmed" { now := time.Now() event.ConfirmedAt = &now event.ConfirmedBy = currentUserID(c) } if err := db.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "source_key"}}, DoNothing: true, }).Create(&event).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "创建发病事件失败"}) return } if event.ID == "" { _ = db.Where("source_key = ?", event.SourceKey).First(&event).Error } if event.Status == "confirmed" { _ = ensureTraceForDiseaseEvent(db, event) } c.JSON(http.StatusCreated, event) } } func updateDiseaseEvent(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { var event model.DiseaseEvent if db.Where("id = ?", c.Param("id")).First(&event).Error != nil { c.JSON(http.StatusNotFound, gin.H{"error": "disease event not found"}) return } var body struct { Status *string `json:"status"` Evidence json.RawMessage `json:"evidence"` LossSummary *string `json:"lossSummary"` Measure *string `json:"measure"` Note *string `json:"note"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } updates := map[string]interface{}{} if body.Status != nil { if !model.ValidDiseaseEventTransition(event.Status, *body.Status) { c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("发病事件状态不能从 %s 转到 %s", event.Status, *body.Status)}) return } updates["status"] = *body.Status if *body.Status == "confirmed" { if len(body.Evidence) == 0 && len(event.Evidence) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "确诊发病事件必须提供证据"}) return } now := time.Now() updates["confirmed_at"] = now updates["confirmed_by"] = currentUserID(c) } } if len(body.Evidence) > 0 { updates["evidence"] = body.Evidence } if body.LossSummary != nil { updates["loss_summary"] = *body.LossSummary } if body.Measure != nil { updates["measure"] = *body.Measure } if body.Note != nil { updates["note"] = *body.Note } if len(updates) > 0 { if err := db.Model(&model.DiseaseEvent{}).Where("id = ?", event.ID).Updates(updates).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "更新发病事件失败"}) return } } db.Where("id = ?", event.ID).First(&event) if event.Status == "confirmed" { _ = ensureTraceForDiseaseEvent(db, event) } c.JSON(http.StatusOK, event) } } func fillDetectionTaskRoomNames(db *gorm.DB, list []model.DetectionTask) { var rooms []model.Room db.Select("id", "name").Find(&rooms) names := make(map[string]string, len(rooms)) for _, r := range rooms { names[r.ID] = r.Name } for i := range list { if list[i].RoomID != nil { if name, ok := names[*list[i].RoomID]; ok { list[i].RoomName = &name } } } } func fillDiseaseEventRoomNames(db *gorm.DB, list []model.DiseaseEvent) { var rooms []model.Room db.Select("id", "name").Find(&rooms) names := make(map[string]string, len(rooms)) for _, r := range rooms { names[r.ID] = r.Name } for i := range list { if list[i].RoomID != nil { if name, ok := names[*list[i].RoomID]; ok { list[i].RoomName = &name } } } } // ensureTraceForDiseaseEvent 确诊后自动创建关联溯源记录,重复调用不重复创建。 func ensureTraceForDiseaseEvent(db *gorm.DB, event model.DiseaseEvent) error { if event.Status != "confirmed" { return nil } var count int64 if err := db.Model(&model.TraceRecord{}).Where("disease_event_id = ?", event.ID).Count(&count).Error; err != nil { return err } if count > 0 { return nil } trace := model.TraceRecord{ DiseaseEventID: &event.ID, RoomID: event.RoomID, LampTestID: event.LampTestID, Disease: event.Disease, Status: "pending", } return db.Create(&trace).Error } func ensureDiseaseEventFromDetectionTask(db *gorm.DB, task model.DetectionTask) error { if task.Result == nil || *task.Result != "positive" { return nil } now := time.Now() evidence, _ := json.Marshal(map[string]interface{}{ "result": *task.Result, "method": task.Method, "detectionTaskId": task.ID, "resultedAt": task.ResultedAt, }) sourceKey := "detection-task-" + task.ID event := model.DiseaseEvent{ SourceKey: sourceKey, RoomID: task.RoomID, BatchID: task.BatchID, DetectionTaskID: &task.ID, InspectionID: task.InspectionID, Disease: task.Disease, Status: "confirmed", Evidence: evidence, ConfirmedAt: &now, ConfirmedBy: task.AssigneeID, } if err := db.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "source_key"}}, DoNothing: true, }).Create(&event).Error; err != nil { return err } if event.ID == "" { _ = db.Where("source_key = ?", sourceKey).First(&event).Error } return ensureTraceForDiseaseEvent(db, event) }