package deviceimage import ( "crypto/sha256" "encoding/hex" "errors" "fmt" "io" "mime/multipart" "net/http" "strings" "time" "github.com/gin-gonic/gin" "go.uber.org/zap" "gorm.io/gorm" "wails-app/internal/dao" "wails-app/internal/global" "wails-app/internal/model/common/response" "wails-app/internal/utils/upload" ) const maxImageBytes int64 = 8 * 1024 * 1024 // Upload accepts images pushed by an enabled, provisioned edge device on the // trusted parking LAN. It deliberately does not require a management JWT. func Upload(c *gin.Context) { startedAt := time.Now() remoteAddr := c.Request.RemoteAddr deviceCode := strings.TrimSpace(c.PostForm("device_code")) imageID := strings.TrimSpace(c.PostForm("image_id")) if deviceCode == "" || imageID == "" || len(deviceCode) > 50 || len(imageID) > 128 { logUploadFailure("参数无效", remoteAddr, deviceCode, imageID, "", startedAt, nil) response.FailWithMessage("device_code 和 image_id 必填且长度无效", c) return } if global.GVA_DB == nil { logUploadFailure("数据库未初始化", remoteAddr, deviceCode, imageID, "", startedAt, nil) response.FailWithMessage("数据库未初始化", c) return } var reader dao.UHFReader if err := global.GVA_DB.Where("device_code = ? AND is_active = ? AND connect_type = ?", deviceCode, true, dao.ConnectTypeMQTT).First(&reader).Error; err != nil { logUploadFailure("设备未配置或已停用", remoteAddr, deviceCode, imageID, "", startedAt, err) response.FailWithMessage("设备未配置或已停用", c) return } var existing dao.DeviceImage if err := global.GVA_DB.Where("device_code = ? AND image_id = ?", deviceCode, imageID).First(&existing).Error; err == nil { logUploadSuccess("重复上传幂等返回", remoteAddr, deviceCode, imageID, existing.EventID, existing.ContentType, existing.Size, existing.URL, existing.Key, startedAt, true) response.OkWithData(gin.H{"image_id": existing.ImageID, "url": existing.URL, "key": existing.Key}, c) return } else if !errors.Is(err, gorm.ErrRecordNotFound) { logUploadFailure("查询图片记录失败", remoteAddr, deviceCode, imageID, "", startedAt, err) response.FailWithMessage("查询图片记录失败", c) return } c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxImageBytes+1024*1024) file, header, err := c.Request.FormFile("image") if err != nil { logUploadFailure("缺少图片文件", remoteAddr, deviceCode, imageID, "", startedAt, err) response.FailWithMessage("缺少 image 图片文件", c) return } _ = file.Close() if header.Size <= 0 || header.Size > maxImageBytes { logUploadFailure("图片大小超限", remoteAddr, deviceCode, imageID, "", startedAt, fmt.Errorf("size=%d", header.Size)) response.FailWithMessage("图片大小必须在 1B 到 8MB 之间", c) return } contentType, digest, err := inspectImage(header) if err != nil { logUploadFailure("图片格式或内容校验失败", remoteAddr, deviceCode, imageID, "", startedAt, err) response.FailWithMessage(err.Error(), c) return } url, key, err := upload.NewOss().UploadFile(header) if err != nil { logUploadFailure("对象存储保存图片失败", remoteAddr, deviceCode, imageID, contentType, startedAt, err) response.FailWithMessage("保存图片失败", c) return } record := dao.DeviceImage{DeviceCode: deviceCode, ImageID: imageID, EventID: strings.TrimSpace(c.PostForm("event_id")), Direction: strings.TrimSpace(c.PostForm("direction")), URL: url, Key: key, ContentType: contentType, Size: header.Size, SHA256: digest} if err := global.GVA_DB.Create(&record).Error; err != nil { logUploadFailure("保存图片记录失败", remoteAddr, deviceCode, imageID, contentType, startedAt, err) response.FailWithMessage("保存图片记录失败", c) return } if global.GVA_LOG != nil { logUploadSuccess("图片上传成功", remoteAddr, deviceCode, imageID, record.EventID, contentType, header.Size, record.URL, record.Key, startedAt, false) } response.OkWithData(gin.H{"image_id": imageID, "url": url, "key": key, "sha256": digest}, c) } func logUploadFailure(reason, remoteAddr, deviceCode, imageID, contentType string, startedAt time.Time, err error) { if global.GVA_LOG == nil { return } fields := []zap.Field{zap.String("remote_addr", remoteAddr), zap.String("device_code", deviceCode), zap.String("image_id", imageID), zap.String("content_type", contentType), zap.Duration("duration", time.Since(startedAt)), zap.String("reason", reason)} if err != nil { fields = append(fields, zap.Error(err)) } global.GVA_LOG.Warn("边缘设备图片上传失败", fields...) } func logUploadSuccess(message, remoteAddr, deviceCode, imageID, eventID, contentType string, size int64, storageURL, storageKey string, startedAt time.Time, idempotent bool) { if global.GVA_LOG == nil { return } global.GVA_LOG.Info("边缘设备图片上传结果", zap.String("result", message), zap.String("remote_addr", remoteAddr), zap.String("device_code", deviceCode), zap.String("image_id", imageID), zap.String("event_id", eventID), zap.String("content_type", contentType), zap.Int64("size", size), zap.String("storage_url", storageURL), zap.String("storage_key", storageKey), zap.Bool("idempotent", idempotent), zap.Duration("duration", time.Since(startedAt))) } func inspectImage(header *multipart.FileHeader) (string, string, error) { f, err := header.Open() if err != nil { return "", "", errors.New("读取图片失败") } defer f.Close() buf := make([]byte, 512) n, err := io.ReadFull(f, buf) if err != nil && err != io.ErrUnexpectedEOF { return "", "", errors.New("读取图片失败") } contentType := http.DetectContentType(buf[:n]) if !strings.HasPrefix(contentType, "image/") { return "", "", fmt.Errorf("不支持的图片格式: %s", contentType) } if _, err := f.Seek(0, io.SeekStart); err != nil { return "", "", errors.New("读取图片失败") } h := sha256.New() if _, err := io.Copy(h, f); err != nil { return "", "", errors.New("计算图片摘要失败") } return contentType, hex.EncodeToString(h.Sum(nil)), nil }