api.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. package deviceimage
  2. import (
  3. "crypto/sha256"
  4. "encoding/hex"
  5. "errors"
  6. "fmt"
  7. "io"
  8. "mime/multipart"
  9. "net/http"
  10. "strings"
  11. "time"
  12. "github.com/gin-gonic/gin"
  13. "go.uber.org/zap"
  14. "gorm.io/gorm"
  15. "wails-app/internal/dao"
  16. "wails-app/internal/global"
  17. "wails-app/internal/model/common/response"
  18. "wails-app/internal/utils/upload"
  19. )
  20. const maxImageBytes int64 = 8 * 1024 * 1024
  21. // Upload accepts images pushed by an enabled, provisioned edge device on the
  22. // trusted parking LAN. It deliberately does not require a management JWT.
  23. func Upload(c *gin.Context) {
  24. startedAt := time.Now()
  25. remoteAddr := c.Request.RemoteAddr
  26. deviceCode := strings.TrimSpace(c.PostForm("device_code"))
  27. imageID := strings.TrimSpace(c.PostForm("image_id"))
  28. if deviceCode == "" || imageID == "" || len(deviceCode) > 50 || len(imageID) > 128 {
  29. logUploadFailure("参数无效", remoteAddr, deviceCode, imageID, "", startedAt, nil)
  30. response.FailWithMessage("device_code 和 image_id 必填且长度无效", c)
  31. return
  32. }
  33. if global.GVA_DB == nil {
  34. logUploadFailure("数据库未初始化", remoteAddr, deviceCode, imageID, "", startedAt, nil)
  35. response.FailWithMessage("数据库未初始化", c)
  36. return
  37. }
  38. var reader dao.UHFReader
  39. if err := global.GVA_DB.Where("device_code = ? AND is_active = ? AND connect_type = ?", deviceCode, true, dao.ConnectTypeMQTT).First(&reader).Error; err != nil {
  40. logUploadFailure("设备未配置或已停用", remoteAddr, deviceCode, imageID, "", startedAt, err)
  41. response.FailWithMessage("设备未配置或已停用", c)
  42. return
  43. }
  44. var existing dao.DeviceImage
  45. if err := global.GVA_DB.Where("device_code = ? AND image_id = ?", deviceCode, imageID).First(&existing).Error; err == nil {
  46. logUploadSuccess("重复上传幂等返回", remoteAddr, deviceCode, imageID, existing.EventID, existing.ContentType, existing.Size, existing.URL, existing.Key, startedAt, true)
  47. response.OkWithData(gin.H{"image_id": existing.ImageID, "url": existing.URL, "key": existing.Key}, c)
  48. return
  49. } else if !errors.Is(err, gorm.ErrRecordNotFound) {
  50. logUploadFailure("查询图片记录失败", remoteAddr, deviceCode, imageID, "", startedAt, err)
  51. response.FailWithMessage("查询图片记录失败", c)
  52. return
  53. }
  54. c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxImageBytes+1024*1024)
  55. file, header, err := c.Request.FormFile("image")
  56. if err != nil {
  57. logUploadFailure("缺少图片文件", remoteAddr, deviceCode, imageID, "", startedAt, err)
  58. response.FailWithMessage("缺少 image 图片文件", c)
  59. return
  60. }
  61. _ = file.Close()
  62. if header.Size <= 0 || header.Size > maxImageBytes {
  63. logUploadFailure("图片大小超限", remoteAddr, deviceCode, imageID, "", startedAt, fmt.Errorf("size=%d", header.Size))
  64. response.FailWithMessage("图片大小必须在 1B 到 8MB 之间", c)
  65. return
  66. }
  67. contentType, digest, err := inspectImage(header)
  68. if err != nil {
  69. logUploadFailure("图片格式或内容校验失败", remoteAddr, deviceCode, imageID, "", startedAt, err)
  70. response.FailWithMessage(err.Error(), c)
  71. return
  72. }
  73. url, key, err := upload.NewOss().UploadFile(header)
  74. if err != nil {
  75. logUploadFailure("对象存储保存图片失败", remoteAddr, deviceCode, imageID, contentType, startedAt, err)
  76. response.FailWithMessage("保存图片失败", c)
  77. return
  78. }
  79. 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}
  80. if err := global.GVA_DB.Create(&record).Error; err != nil {
  81. logUploadFailure("保存图片记录失败", remoteAddr, deviceCode, imageID, contentType, startedAt, err)
  82. response.FailWithMessage("保存图片记录失败", c)
  83. return
  84. }
  85. if global.GVA_LOG != nil {
  86. logUploadSuccess("图片上传成功", remoteAddr, deviceCode, imageID, record.EventID, contentType, header.Size, record.URL, record.Key, startedAt, false)
  87. }
  88. response.OkWithData(gin.H{"image_id": imageID, "url": url, "key": key, "sha256": digest}, c)
  89. }
  90. func logUploadFailure(reason, remoteAddr, deviceCode, imageID, contentType string, startedAt time.Time, err error) {
  91. if global.GVA_LOG == nil {
  92. return
  93. }
  94. 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)}
  95. if err != nil {
  96. fields = append(fields, zap.Error(err))
  97. }
  98. global.GVA_LOG.Warn("边缘设备图片上传失败", fields...)
  99. }
  100. func logUploadSuccess(message, remoteAddr, deviceCode, imageID, eventID, contentType string, size int64, storageURL, storageKey string, startedAt time.Time, idempotent bool) {
  101. if global.GVA_LOG == nil {
  102. return
  103. }
  104. 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)))
  105. }
  106. func inspectImage(header *multipart.FileHeader) (string, string, error) {
  107. f, err := header.Open()
  108. if err != nil {
  109. return "", "", errors.New("读取图片失败")
  110. }
  111. defer f.Close()
  112. buf := make([]byte, 512)
  113. n, err := io.ReadFull(f, buf)
  114. if err != nil && err != io.ErrUnexpectedEOF {
  115. return "", "", errors.New("读取图片失败")
  116. }
  117. contentType := http.DetectContentType(buf[:n])
  118. if !strings.HasPrefix(contentType, "image/") {
  119. return "", "", fmt.Errorf("不支持的图片格式: %s", contentType)
  120. }
  121. if _, err := f.Seek(0, io.SeekStart); err != nil {
  122. return "", "", errors.New("读取图片失败")
  123. }
  124. h := sha256.New()
  125. if _, err := io.Copy(h, f); err != nil {
  126. return "", "", errors.New("计算图片摘要失败")
  127. }
  128. return contentType, hex.EncodeToString(h.Sum(nil)), nil
  129. }