package handler import ( "bytes" "encoding/json" "io" "net/http" "strconv" "time" "silk-server-go/internal/middleware" "silk-server-go/internal/model" "silk-server-go/internal/service" "github.com/gin-gonic/gin" "gorm.io/gorm" "gorm.io/gorm/clause" ) // RegisterLampRoutes 注册 LAMP 检测路由 func RegisterLampRoutes(rg *gin.RouterGroup, db *gorm.DB, s3 *service.S3Service, imageBucket string) { read := middleware.RequirePermission(db, "lamp:read") write := middleware.RequirePermission(db, "lamp:write") rg.GET("/lamp-tests", read, listLampTests(db)) rg.POST("/lamp-tests", write, createLampTest(db)) rg.PATCH("/lamp-tests/:id", write, updateLampTest(db)) rg.DELETE("/lamp-tests/:id", write, deleteLampTest(db)) rg.GET("/lamp-tests/:id/steps", read, listLampTestSteps(db)) rg.PATCH("/lamp-tests/:id/steps/:stepNo", write, updateLampTestStep(db)) rg.POST("/lamp-tests/:id/result-image", write, uploadLampResultImage(db, s3, imageBucket)) rg.GET("/lamp-tests/:id/cross-validation", read, getLampCrossValidation(db)) rg.POST("/lamp-tests/:id/judge-qpcr", write, judgeQPCR(db)) rg.POST("/lamp-tests/:id/spectrum", write, uploadLampSpectrum(db, s3, imageBucket)) rg.GET("/spectrum-entries", read, listSpectrumEntries(db)) rg.POST("/spectrum-entries", write, createSpectrumEntry(db)) rg.POST("/spectrum-entries/upload", write, uploadSpectrumEntryFile(s3, imageBucket)) rg.DELETE("/spectrum-entries/:id", write, deleteSpectrumEntry(db)) } // listLampTests 检测任务单列表(roomId/batchId/status 过滤) func listLampTests(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { q := db.Model(&model.LampTest{}) if room := c.Query("roomId"); room != "" { q = q.Where("room_id = ?", room) } if batch := c.Query("batchId"); batch != "" { q = q.Where("batch_id = ?", batch) } if status := c.Query("status"); status != "" { q = q.Where("status = ?", status) } var list []model.LampTest q.Order("created_at DESC").Find(&list) c.JSON(http.StatusOK, list) } } // createLampTest 新建任务单并自动生成标准 5 步 func createLampTest(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { var t model.LampTest if err := c.ShouldBindJSON(&t); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } t.ID = "" if t.Method == "" { t.Method = "lamp" } if !model.ValidDetectionMethod(t.Method) { c.JSON(http.StatusBadRequest, gin.H{"error": "method 仅支持 lamp/qpcr/sers/hyperspectral"}) return } if t.RoomID != nil && !isUUID(*t.RoomID) { c.JSON(http.StatusBadRequest, gin.H{"error": "roomId 不是合法的 UUID"}) return } if t.BatchID != nil && !isUUID(*t.BatchID) { c.JSON(http.StatusBadRequest, gin.H{"error": "batchId 不是合法的 UUID"}) return } if t.DetectionTaskID != nil && !isUUID(*t.DetectionTaskID) { c.JSON(http.StatusBadRequest, gin.H{"error": "detectionTaskId 不是合法的 UUID"}) return } if t.Status == "" { t.Status = "pending" } t.OperatorID = currentUserID(c) if err := db.Create(&t).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "创建失败"}) return } for i, name := range model.DefaultLampSteps() { step := model.LampTestStep{LampTestID: t.ID, StepNo: i + 1, Name: name} if err := db.Create(&step).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "创建步骤失败"}) return } } c.JSON(http.StatusCreated, t) } } // judgeQPCR qPCR Ct 值录入并自动判读(#19) func judgeQPCR(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { id := c.Param("id") var t model.LampTest if db.Where("id = ?", id).First(&t).Error != nil { c.JSON(http.StatusNotFound, gin.H{"error": "lamp test not found"}) return } var body struct { CtValues []float64 `json:"ctValues"` Threshold float64 `json:"threshold"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } result, reason := model.JudgeQPCR(body.CtValues, body.Threshold) extra := map[string]interface{}{} if len(t.ExtraData) > 0 { _ = json.Unmarshal(t.ExtraData, &extra) } extra["ctValues"] = body.CtValues extra["judgeReason"] = reason raw, _ := json.Marshal(extra) now := time.Now() db.Model(&model.LampTest{}).Where("id = ?", id).Updates(map[string]interface{}{ "extra_data": raw, "result": result, "status": "resulted", "resulted_at": now, }) _ = syncDetectionTaskFromLamp(db, id) _ = ensureDiseaseEventForLamp(db, id) _ = runCrossValidation(db, id, result) db.Where("id = ?", id).First(&t) c.JSON(http.StatusOK, t) } } // uploadLampSpectrum SERS 光谱数据上传(S3 spectrum/ 前缀) func uploadLampSpectrum(db *gorm.DB, s3 *service.S3Service, bucket string) gin.HandlerFunc { return func(c *gin.Context) { id := c.Param("id") var t model.LampTest if db.Where("id = ?", id).First(&t).Error != nil { c.JSON(http.StatusNotFound, gin.H{"error": "lamp test not found"}) return } file, header, err := c.Request.FormFile("file") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请选择光谱文件(字段名 file)"}) return } defer file.Close() if err := model.ValidateSpectrumFile(header.Filename, header.Size); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } data, err := io.ReadAll(file) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "读取文件失败"}) return } key, ext, err := buildDataKey("spectrum", header.Filename) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := s3.EnsureBucket(bucket); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "存储不可用: " + err.Error()}) return } if err := s3.UploadImage(bucket, key, bytes.NewReader(data), spectrumContentType(ext)); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "上传失败: " + err.Error()}) return } url := s3.Endpoint() + "/" + bucket + "/" + key extra := map[string]interface{}{} if len(t.ExtraData) > 0 { _ = json.Unmarshal(t.ExtraData, &extra) } extra["spectrumUrl"] = url raw, _ := json.Marshal(extra) db.Model(&model.LampTest{}).Where("id = ?", id).Update("extra_data", raw) c.JSON(http.StatusOK, gin.H{"url": url}) } } // spectrumContentType 光谱文件 Content-Type func spectrumContentType(ext string) string { switch ext { case ".json": return "application/json" case ".csv": return "text/csv" default: return "text/plain" } } // ---------- 光谱库 ---------- func listSpectrumEntries(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { q := db.Model(&model.SpectrumEntry{}) if disease := c.Query("disease"); disease != "" { q = q.Where("disease = ?", disease) } var list []model.SpectrumEntry q.Order("created_at DESC").Find(&list) c.JSON(http.StatusOK, list) } } func createSpectrumEntry(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { var e model.SpectrumEntry if err := c.ShouldBindJSON(&e); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } e.ID = "" if e.Disease == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "病种不能为空"}) return } if err := db.Create(&e).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "创建失败"}) return } c.JSON(http.StatusCreated, e) } } func uploadSpectrumEntryFile(s3 *service.S3Service, bucket string) gin.HandlerFunc { return func(c *gin.Context) { file, header, err := c.Request.FormFile("file") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请选择光谱文件(字段名 file)"}) return } defer file.Close() if err := model.ValidateSpectrumFile(header.Filename, header.Size); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } data, err := io.ReadAll(file) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "读取文件失败"}) return } key, ext, err := buildDataKey("spectrum", header.Filename) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := s3.EnsureBucket(bucket); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "存储不可用: " + err.Error()}) return } if err := s3.UploadImage(bucket, key, bytes.NewReader(data), spectrumContentType(ext)); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "上传失败: " + err.Error()}) return } c.JSON(http.StatusOK, gin.H{"url": s3.Endpoint() + "/" + bucket + "/" + key}) } } func deleteSpectrumEntry(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { id := c.Param("id") var e model.SpectrumEntry if db.Where("id = ?", id).First(&e).Error != nil { c.JSON(http.StatusNotFound, gin.H{"error": "spectrum entry not found"}) return } db.Where("id = ?", id).Delete(&model.SpectrumEntry{}) c.JSON(http.StatusOK, gin.H{"id": id}) } } // updateLampTest 更新任务单;result 合法时自动置状态 resulted func updateLampTest(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { id := c.Param("id") var t model.LampTest if db.Where("id = ?", id).First(&t).Error != nil { c.JSON(http.StatusNotFound, gin.H{"error": "lamp test not found"}) return } updates, err := bindUpdates(c) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } resultSet := false resultValue := "" if r, ok := updates["result"]; ok { result, _ := r.(string) if result != "" && !model.ValidLampResult(result) { c.JSON(http.StatusBadRequest, gin.H{"error": "result 仅支持 positive/negative/invalid"}) return } if result != "" { updates["status"] = "resulted" updates["resulted_at"] = time.Now() } else { updates["status"] = "testing" } resultSet = true resultValue = result } if len(updates) > 0 { db.Model(&model.LampTest{}).Where("id = ?", id).Updates(updates) } // 交叉验证:结果落库后与同房间最近巡检比对 if resultSet && resultValue != "" { _ = runCrossValidation(db, id, resultValue) _ = syncDetectionTaskFromLamp(db, id) _ = ensureDiseaseEventForLamp(db, id) } db.Where("id = ?", id).First(&t) c.JSON(http.StatusOK, t) } } // syncDetectionTaskFromLamp LAMP 结果同步到统一检测任务。 func syncDetectionTaskFromLamp(db *gorm.DB, lampID string) error { var t model.LampTest if err := db.Where("id = ?", lampID).First(&t).Error; err != nil { return err } if t.DetectionTaskID == nil || t.Result == nil { return nil } return db.Model(&model.DetectionTask{}).Where("id = ?", *t.DetectionTaskID). Updates(map[string]interface{}{ "result": *t.Result, "status": "completed", "resulted_at": t.ResultedAt, }).Error } // ensureDiseaseEventForLamp 有效阳性结果创建发病事件,重复结果不重复建单。 func ensureDiseaseEventForLamp(db *gorm.DB, lampID string) error { var t model.LampTest if err := db.Where("id = ?", lampID).First(&t).Error; err != nil { return err } if t.Result == nil || *t.Result != "positive" { return nil } disease := "待确认" var diseases []string if len(t.Diseases) > 0 { _ = json.Unmarshal(t.Diseases, &diseases) if len(diseases) > 0 { disease = diseases[0] } } now := time.Now() evidence, _ := json.Marshal(map[string]interface{}{ "result": *t.Result, "method": t.Method, "resultedAt": t.ResultedAt, "lampTestId": t.ID, "operatorId": t.OperatorID, }) sourceKey := "lamp-" + t.ID event := model.DiseaseEvent{ SourceKey: sourceKey, RoomID: t.RoomID, BatchID: t.BatchID, DetectionTaskID: t.DetectionTaskID, LampTestID: &t.ID, Disease: disease, Status: "confirmed", Evidence: evidence, ConfirmedAt: &now, ConfirmedBy: t.OperatorID, } 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) } // runCrossValidation 用同房间最近的巡检记录做交叉验证,并回写 cross_status/cross_reason func runCrossValidation(db *gorm.DB, lampTestID, lampResult string) error { var t model.LampTest if err := db.Where("id = ?", lampTestID).First(&t).Error; err != nil { return err } if t.RoomID == nil { return nil } var inspection model.InspectionRecord if err := db.Where("room_id = ?", *t.RoomID).Order("created_at DESC").First(&inspection).Error; err != nil { return nil // 无关联巡检,保持 pending } var detections []service.AIDetection if len(inspection.Detections) > 0 { _ = json.Unmarshal(inspection.Detections, &detections) } aiClass := service.AIClassFromDetections(detections) var diseases []string if len(t.Diseases) > 0 { _ = json.Unmarshal(t.Diseases, &diseases) } ok, reason := service.CrossValidate(aiClass, lampResult, diseases) status := "inconsistent" if ok { status = "consistent" } return db.Model(&model.LampTest{}).Where("id = ?", lampTestID). Updates(map[string]interface{}{"cross_status": status, "cross_reason": reason}).Error } // getLampCrossValidation 交叉验证详情(含关联巡检摘要) func getLampCrossValidation(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { id := c.Param("id") var t model.LampTest if db.Where("id = ?", id).First(&t).Error != nil { c.JSON(http.StatusNotFound, gin.H{"error": "lamp test not found"}) return } var related map[string]interface{} if t.RoomID != nil { var inspection model.InspectionRecord if db.Where("room_id = ?", *t.RoomID).Order("created_at DESC").First(&inspection).Error == nil { var detections []service.AIDetection if len(inspection.Detections) > 0 { _ = json.Unmarshal(inspection.Detections, &detections) } related = map[string]interface{}{ "id": inspection.ID, "imageUrl": inspection.ImageURL, "aiClass": service.AIClassFromDetections(detections), "riskLevel": inspection.RiskLevel, "createdAt": inspection.CreatedAt, } } } c.JSON(http.StatusOK, gin.H{ "crossStatus": t.CrossStatus, "crossReason": t.CrossReason, "relatedInspection": related, }) } } // deleteLampTest 删除任务单(级联删除步骤) func deleteLampTest(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { id := c.Param("id") var t model.LampTest if db.Where("id = ?", id).First(&t).Error != nil { c.JSON(http.StatusNotFound, gin.H{"error": "lamp test not found"}) return } db.Where("lamp_test_id = ?", id).Delete(&model.LampTestStep{}) db.Where("id = ?", id).Delete(&model.LampTest{}) c.JSON(http.StatusOK, gin.H{"id": id}) } } // listLampTestSteps 任务单步骤 func listLampTestSteps(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { id := c.Param("id") var t model.LampTest if db.Where("id = ?", id).First(&t).Error != nil { c.JSON(http.StatusNotFound, gin.H{"error": "lamp test not found"}) return } var steps []model.LampTestStep db.Where("lamp_test_id = ?", id).Order("step_no ASC").Find(&steps) c.JSON(http.StatusOK, steps) } } // updateLampTestStep 更新步骤完成状态 func updateLampTestStep(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { id := c.Param("id") stepNo, err := strconv.Atoi(c.Param("stepNo")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "stepNo 不合法"}) return } var step model.LampTestStep if db.Where("lamp_test_id = ? AND step_no = ?", id, stepNo).First(&step).Error != nil { c.JSON(http.StatusNotFound, gin.H{"error": "step not found"}) return } var body struct { Done *bool `json:"done"` 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.Done != nil { updates["done"] = *body.Done if *body.Done { updates["done_at"] = time.Now() } else { updates["done_at"] = nil } if stepNo == 5 && *body.Done { db.Model(&model.LampTest{}).Where("id = ?", id).Update("status", "testing") } } if body.Note != nil { updates["note"] = *body.Note } if len(updates) > 0 { db.Model(&model.LampTestStep{}).Where("id = ?", step.ID).Updates(updates) } db.Where("id = ?", step.ID).First(&step) c.JSON(http.StatusOK, step) } } // uploadLampResultImage 结果照片上传(S3 lamp/ 前缀) func uploadLampResultImage(db *gorm.DB, s3 *service.S3Service, bucket string) gin.HandlerFunc { return func(c *gin.Context) { id := c.Param("id") var t model.LampTest if db.Where("id = ?", id).First(&t).Error != nil { c.JSON(http.StatusNotFound, gin.H{"error": "lamp test not found"}) return } file, header, err := c.Request.FormFile("file") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请选择图片文件(字段名 file)"}) return } defer file.Close() if err := validateImageFile(header.Filename, header.Size); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } imageBytes, err := io.ReadAll(file) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "读取图片失败"}) return } key, ext, err := buildObjectKey("lamp", header.Filename) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } contentType, _ := imageContentType(ext) if err := s3.EnsureBucket(bucket); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "图片存储不可用: " + err.Error()}) return } if err := s3.UploadImage(bucket, key, bytes.NewReader(imageBytes), contentType); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "图片上传失败: " + err.Error()}) return } url := s3.Endpoint() + "/" + bucket + "/" + key db.Model(&model.LampTest{}).Where("id = ?", id).Update("result_image_url", url) c.JSON(http.StatusOK, gin.H{"url": url}) } }