| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138 |
- 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
- }
|