package handler import ( "bytes" "encoding/json" "io" "net/http" "regexp" "strconv" "strings" "silk-server-go/internal/middleware" "silk-server-go/internal/model" "silk-server-go/internal/service" "github.com/gin-gonic/gin" "gorm.io/gorm" ) var uuidPattern = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`) // isUUID 判断字符串是否为标准 UUID 格式(roomId 等外键校验用) func isUUID(s string) bool { return uuidPattern.MatchString(s) } // RegisterInspectionRoutes 注册 AI 巡检路由 func RegisterInspectionRoutes(rg *gin.RouterGroup, db *gorm.DB, s3 *service.S3Service, ai *service.AIClient, imageBucket string) { rg.POST("/inspections", middleware.RequirePermission(db, "inspection:create"), createInspection(db, s3, ai, imageBucket)) rg.GET("/inspections", middleware.RequirePermission(db, "inspection:read"), listInspections(db)) } // currentUserID 从 JWT 上下文取用户 ID func currentUserID(c *gin.Context) *string { if user, ok := c.Get("user"); ok { if m, ok := user.(map[string]interface{}); ok { if s, ok := m["sub"].(string); ok && s != "" { return &s } } } return nil } // createInspection 拍照巡检:图片存 S3 → 调 AI /detect → 写记录。 // 幂等:客户端传 Idempotency-Key 头时,重复请求返回已有记录。 func createInspection(db *gorm.DB, s3 *service.S3Service, ai *service.AIClient, bucket string) gin.HandlerFunc { return func(c *gin.Context) { idemKey := strings.TrimSpace(c.GetHeader("Idempotency-Key")) roomID := strings.TrimSpace(c.PostForm("roomId")) if idemKey != "" { var exist model.InspectionRecord if db.Where("idempotency_key = ?", idemKey).First(&exist).Error == nil { c.JSON(http.StatusOK, exist) 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("inspections", 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 } imageURL := s3.Endpoint() + "/" + bucket + "/" + key rec := model.InspectionRecord{ UserID: currentUserID(c), ImageURL: &imageURL, AIStatus: "done", } if roomID != "" { if !isUUID(roomID) { c.JSON(http.StatusBadRequest, gin.H{"error": "roomId 不是合法的 UUID"}) return } rec.RoomID = &roomID } if idemKey != "" { rec.IdempotencyKey = &idemKey } detRes, aiErr := ai.Detect(c.Request.Context(), imageBytes, header.Filename) if aiErr != nil { rec.AIStatus = "failed" } else { raw, _ := json.Marshal(detRes.Detections) rec.Detections = raw } if err := db.Create(&rec).Error; err != nil { // 并发幂等:唯一索引冲突时返回已有记录 if idemKey != "" { var exist model.InspectionRecord if db.Where("idempotency_key = ?", idemKey).First(&exist).Error == nil { c.JSON(http.StatusOK, exist) return } } c.JSON(http.StatusInternalServerError, gin.H{"error": "创建巡检记录失败"}) return } c.JSON(http.StatusCreated, rec) } } // listInspections 巡检记录列表(roomId/limit 过滤) func listInspections(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { q := db.Model(&model.InspectionRecord{}) if room := c.Query("roomId"); room != "" { q = q.Where("room_id = ?", room) } limit := 50 if l, err := strconv.Atoi(c.Query("limit")); err == nil && l > 0 && l <= 200 { limit = l } var list []model.InspectionRecord q.Order("created_at DESC").Limit(limit).Find(&list) c.JSON(http.StatusOK, list) } }