Files
silk/server-go/internal/handler/inspection.go
T

204 lines
6.0 KiB
Go
Raw 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 (
"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
// 风险评分(#9):AI 置信度取检测结果最大值;环境/阶段系数在有 roomId 时按房间数据计算
aiConf := 0.0
for _, d := range detRes.Detections {
if d.Confidence > aiConf {
aiConf = d.Confidence
}
}
stageCoef, envCoef := loadRoomRisk(db, roomID)
score := service.ComputeRiskScore(service.RiskInput{
AI: aiConf,
Env: envCoef,
Stage: stageCoef,
})
rec.RiskScore = &score
level := service.RiskLevel(score)
rec.RiskLevel = &level
}
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)
}
}
// loadRoomRisk 加载房间阶段系数与环境系数(无房间/无数据时返回 0)
func loadRoomRisk(db *gorm.DB, roomID string) (stageCoef, envCoef float64) {
if roomID == "" {
return 0, 0
}
var room model.Room
if db.Where("id = ?", roomID).First(&room).Error != nil {
return 0, 0
}
if room.Stage != nil {
stageCoef = service.StageCoefficient(*room.Stage)
}
var humidity, temperature *float64
var h model.Telemetry
if err := db.Table("telemetry").
Select("telemetry.value").
Joins("JOIN devices ON devices.device_key = telemetry.device_key AND devices.room_id = ?", roomID).
Where("telemetry.metric = ?", "humidity").
Order("telemetry.timestamp DESC").
Limit(1).
First(&h).Error; err == nil {
humidity = &h.Value
}
var t model.Telemetry
if err := db.Table("telemetry").
Select("telemetry.value").
Joins("JOIN devices ON devices.device_key = telemetry.device_key AND devices.room_id = ?", roomID).
Where("telemetry.metric = ?", "temperature").
Order("telemetry.timestamp DESC").
Limit(1).
First(&t).Error; err == nil {
temperature = &t.Value
}
envCoef = service.EnvCoefficient(temperature, humidity)
return stageCoef, envCoef
}
// 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)
}
}