feat: add product trace APIs
This commit is contained in:
@@ -0,0 +1,197 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"git.beifan.cn/trace-system/backend-go/models"
|
||||||
|
"git.beifan.cn/trace-system/backend-go/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProductTracesController 产品溯源控制器
|
||||||
|
type ProductTracesController struct {
|
||||||
|
productTracesService services.ProductTracesService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProductTracesController 创建产品溯源控制器实例
|
||||||
|
func NewProductTracesController() *ProductTracesController {
|
||||||
|
return &ProductTracesController{
|
||||||
|
productTracesService: services.ProductTracesService{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 创建产品溯源
|
||||||
|
func (c *ProductTracesController) Create(ctx *gin.Context) {
|
||||||
|
userModel, ok := GetCurrentUser(ctx)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var dto models.CreateProductTraceDTO
|
||||||
|
if !BindJSON(ctx, &dto) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
trace, err := c.productTracesService.Create(dto, userModel.ID)
|
||||||
|
if err != nil {
|
||||||
|
ErrorResponse(ctx, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
SuccessResponse(ctx, "产品溯源创建成功", gin.H{
|
||||||
|
"trace": trace,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindAll 获取产品溯源列表
|
||||||
|
func (c *ProductTracesController) FindAll(ctx *gin.Context) {
|
||||||
|
page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1"))
|
||||||
|
limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "20"))
|
||||||
|
search := ctx.DefaultQuery("search", "")
|
||||||
|
|
||||||
|
traces, total, totalPages, err := c.productTracesService.FindAll(page, limit, search)
|
||||||
|
if err != nil {
|
||||||
|
ErrorResponse(ctx, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
SuccessResponse(ctx, "获取产品溯源列表成功", gin.H{
|
||||||
|
"data": traces,
|
||||||
|
"pagination": gin.H{
|
||||||
|
"page": page,
|
||||||
|
"limit": limit,
|
||||||
|
"total": total,
|
||||||
|
"totalPages": totalPages,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindOne 获取产品溯源详情
|
||||||
|
func (c *ProductTracesController) FindOne(ctx *gin.Context) {
|
||||||
|
serialNumber := ctx.Param("serialNumber")
|
||||||
|
|
||||||
|
trace, err := c.productTracesService.FindOne(serialNumber)
|
||||||
|
if err != nil {
|
||||||
|
ErrorResponse(ctx, http.StatusNotFound, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
SuccessResponse(ctx, "查询成功", gin.H{
|
||||||
|
"trace": trace,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 更新产品溯源
|
||||||
|
func (c *ProductTracesController) Update(ctx *gin.Context) {
|
||||||
|
serialNumber := ctx.Param("serialNumber")
|
||||||
|
var dto models.UpdateProductTraceDTO
|
||||||
|
if !BindJSON(ctx, &dto) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
trace, err := c.productTracesService.Update(serialNumber, dto)
|
||||||
|
if err != nil {
|
||||||
|
ErrorResponse(ctx, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
SuccessResponse(ctx, "产品溯源更新成功", gin.H{
|
||||||
|
"trace": trace,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicQuery 公开查询产品溯源
|
||||||
|
func (c *ProductTracesController) PublicQuery(ctx *gin.Context) {
|
||||||
|
serialNumber := ctx.Param("serialNumber")
|
||||||
|
|
||||||
|
trace, err := c.productTracesService.PublicQuery(serialNumber)
|
||||||
|
if err != nil {
|
||||||
|
ErrorResponse(ctx, http.StatusNotFound, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
SuccessResponse(ctx, "查询成功", gin.H{
|
||||||
|
"trace": trace,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateQRCode 生成产品溯源二维码
|
||||||
|
func (c *ProductTracesController) GenerateQRCode(ctx *gin.Context) {
|
||||||
|
serialNumber := ctx.Param("serialNumber")
|
||||||
|
|
||||||
|
var qrCodeData models.QRCodeDTO
|
||||||
|
if !BindJSON(ctx, &qrCodeData) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol := "http"
|
||||||
|
if ctx.Request.TLS != nil {
|
||||||
|
protocol = "https"
|
||||||
|
}
|
||||||
|
|
||||||
|
qrCodeBase64, queryURL, err := c.productTracesService.GenerateQRCode(
|
||||||
|
serialNumber,
|
||||||
|
qrCodeData.BaseUrl,
|
||||||
|
ctx.Request.Host,
|
||||||
|
protocol,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
ErrorResponse(ctx, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
SuccessResponse(ctx, "二维码生成成功", gin.H{
|
||||||
|
"qrCodeData": qrCodeBase64,
|
||||||
|
"queryUrl": queryURL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadWechatQRCode 上传公众号二维码
|
||||||
|
func (c *ProductTracesController) UploadWechatQRCode(ctx *gin.Context) {
|
||||||
|
serialNumber := ctx.Param("serialNumber")
|
||||||
|
|
||||||
|
fileHeader, err := ctx.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
ErrorResponse(ctx, http.StatusBadRequest, "请选择公众号二维码图片")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
trace, err := c.productTracesService.UploadWechatQRCode(serialNumber, fileHeader)
|
||||||
|
if err != nil {
|
||||||
|
ErrorResponse(ctx, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
SuccessResponse(ctx, "公众号二维码上传成功", gin.H{
|
||||||
|
"trace": trace,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revoke 停用产品溯源
|
||||||
|
func (c *ProductTracesController) Revoke(ctx *gin.Context) {
|
||||||
|
serialNumber := ctx.Param("serialNumber")
|
||||||
|
|
||||||
|
trace, err := c.productTracesService.Revoke(serialNumber)
|
||||||
|
if err != nil {
|
||||||
|
ErrorResponse(ctx, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
SuccessResponse(ctx, "产品溯源已停用", gin.H{
|
||||||
|
"trace": trace,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除产品溯源
|
||||||
|
func (c *ProductTracesController) Delete(ctx *gin.Context) {
|
||||||
|
serialNumber := ctx.Param("serialNumber")
|
||||||
|
|
||||||
|
if err := c.productTracesService.Delete(serialNumber); err != nil {
|
||||||
|
ErrorResponse(ctx, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
SuccessResponse(ctx, "产品溯源删除成功")
|
||||||
|
}
|
||||||
@@ -113,6 +113,7 @@ func AutoMigrate() {
|
|||||||
&models.User{},
|
&models.User{},
|
||||||
&models.Company{},
|
&models.Company{},
|
||||||
&models.Serial{},
|
&models.Serial{},
|
||||||
|
&models.ProductTrace{},
|
||||||
&models.EmployeeSerial{},
|
&models.EmployeeSerial{},
|
||||||
&models.AftersalesOrder{},
|
&models.AftersalesOrder{},
|
||||||
&models.ProjectOrder{},
|
&models.ProjectOrder{},
|
||||||
|
|||||||
@@ -50,6 +50,26 @@ type Serial struct {
|
|||||||
Company *Company `gorm:"foreignKey:CompanyName;references:CompanyName" json:"company,omitempty"`
|
Company *Company `gorm:"foreignKey:CompanyName;references:CompanyName" json:"company,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProductTrace 产品溯源模型
|
||||||
|
type ProductTrace struct {
|
||||||
|
ID uint `gorm:"primaryKey" json:"id"`
|
||||||
|
CompanyName string `gorm:"index;size:255" json:"companyName"`
|
||||||
|
CompanyAddress string `gorm:"size:500" json:"companyAddress"`
|
||||||
|
CompanyPhone string `gorm:"size:32" json:"companyPhone"`
|
||||||
|
DeviceInfo string `gorm:"type:text" json:"deviceInfo"`
|
||||||
|
WarrantyPeriod string `gorm:"size:100" json:"warrantyPeriod"`
|
||||||
|
ManufactureDate time.Time `json:"manufactureDate"`
|
||||||
|
SerialNumber string `gorm:"uniqueIndex;size:255" json:"serialNumber"`
|
||||||
|
OfficialWebsite string `gorm:"size:500" json:"officialWebsite,omitempty"`
|
||||||
|
WechatQRCode string `gorm:"type:text" json:"wechatQrCode,omitempty"`
|
||||||
|
IsActive bool `gorm:"default:true" json:"isActive"`
|
||||||
|
CreatedBy *uint `json:"createdBy"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
Creator *User `gorm:"foreignKey:CreatedBy" json:"creator,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// UserDTO 数据传输对象
|
// UserDTO 数据传输对象
|
||||||
type UserDTO struct {
|
type UserDTO struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
@@ -131,6 +151,32 @@ type UpdateSerialDTO struct {
|
|||||||
IsActive *bool `json:"isActive,omitempty"`
|
IsActive *bool `json:"isActive,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateProductTraceDTO 创建产品溯源请求数据
|
||||||
|
type CreateProductTraceDTO struct {
|
||||||
|
CompanyName string `json:"companyName" validate:"required"`
|
||||||
|
CompanyAddress string `json:"companyAddress" validate:"required"`
|
||||||
|
CompanyPhone string `json:"companyPhone" validate:"required"`
|
||||||
|
DeviceInfo string `json:"deviceInfo" validate:"required"`
|
||||||
|
WarrantyPeriod string `json:"warrantyPeriod" validate:"required"`
|
||||||
|
ManufactureDate time.Time `json:"manufactureDate" validate:"required"`
|
||||||
|
SerialNumber string `json:"serialNumber" validate:"required"`
|
||||||
|
OfficialWebsite string `json:"officialWebsite,omitempty" validate:"omitempty,url"`
|
||||||
|
WechatQRCode string `json:"wechatQrCode,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateProductTraceDTO 更新产品溯源请求数据
|
||||||
|
type UpdateProductTraceDTO struct {
|
||||||
|
CompanyName string `json:"companyName,omitempty"`
|
||||||
|
CompanyAddress string `json:"companyAddress,omitempty"`
|
||||||
|
CompanyPhone string `json:"companyPhone,omitempty"`
|
||||||
|
DeviceInfo string `json:"deviceInfo,omitempty"`
|
||||||
|
WarrantyPeriod string `json:"warrantyPeriod,omitempty"`
|
||||||
|
ManufactureDate *time.Time `json:"manufactureDate,omitempty"`
|
||||||
|
OfficialWebsite string `json:"officialWebsite,omitempty" validate:"omitempty,url"`
|
||||||
|
WechatQRCode string `json:"wechatQrCode,omitempty"`
|
||||||
|
IsActive *bool `json:"isActive,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// QRCodeDTO 二维码生成请求数据
|
// QRCodeDTO 二维码生成请求数据
|
||||||
type QRCodeDTO struct {
|
type QRCodeDTO struct {
|
||||||
BaseUrl string `json:"baseUrl,omitempty"`
|
BaseUrl string `json:"baseUrl,omitempty"`
|
||||||
|
|||||||
@@ -60,6 +60,21 @@ func SetupAPIRoutes(r *gin.RouterGroup) {
|
|||||||
companiesRoutes.DELETE("/:companyName", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), companiesController.Delete)
|
companiesRoutes.DELETE("/:companyName", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), companiesController.Delete)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 产品溯源路由
|
||||||
|
productTracesController := controllers.NewProductTracesController()
|
||||||
|
productTracesRoutes := r.Group("/product-traces")
|
||||||
|
{
|
||||||
|
productTracesRoutes.GET("/:serialNumber/query", productTracesController.PublicQuery)
|
||||||
|
productTracesRoutes.POST("", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), productTracesController.Create)
|
||||||
|
productTracesRoutes.GET("", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), productTracesController.FindAll)
|
||||||
|
productTracesRoutes.GET("/:serialNumber", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), productTracesController.FindOne)
|
||||||
|
productTracesRoutes.PATCH("/:serialNumber", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), productTracesController.Update)
|
||||||
|
productTracesRoutes.POST("/:serialNumber/qrcode", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), productTracesController.GenerateQRCode)
|
||||||
|
productTracesRoutes.POST("/:serialNumber/wechat-qrcode", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), productTracesController.UploadWechatQRCode)
|
||||||
|
productTracesRoutes.POST("/:serialNumber/revoke", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), productTracesController.Revoke)
|
||||||
|
productTracesRoutes.DELETE("/:serialNumber", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), productTracesController.Delete)
|
||||||
|
}
|
||||||
|
|
||||||
// 员工赋码路由
|
// 员工赋码路由
|
||||||
employeeSerialsController := controllers.NewEmployeeSerialsController()
|
employeeSerialsController := controllers.NewEmployeeSerialsController()
|
||||||
employeeSerialsRoutes := r.Group("/employee-serials")
|
employeeSerialsRoutes := r.Group("/employee-serials")
|
||||||
|
|||||||
@@ -0,0 +1,273 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
qr "github.com/yeqown/go-qrcode/v2"
|
||||||
|
"github.com/yeqown/go-qrcode/writer/standard"
|
||||||
|
|
||||||
|
"git.beifan.cn/trace-system/backend-go/config"
|
||||||
|
"git.beifan.cn/trace-system/backend-go/database"
|
||||||
|
"git.beifan.cn/trace-system/backend-go/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProductTracesService 产品溯源服务
|
||||||
|
type ProductTracesService struct{}
|
||||||
|
|
||||||
|
func normalizeProductSerial(serialNumber string) string {
|
||||||
|
return strings.TrimSpace(serialNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hydrateProductTrace(trace *models.ProductTrace) {
|
||||||
|
if trace == nil || strings.TrimSpace(trace.WechatQRCode) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(trace.WechatQRCode, "http://") || strings.HasPrefix(trace.WechatQRCode, "https://") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
trace.WechatQRCode = NewOSSService().AccessURL(trace.WechatQRCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProductTracesService) findRaw(serialNumber string) (*models.ProductTrace, error) {
|
||||||
|
var trace models.ProductTrace
|
||||||
|
if err := database.DB.Preload("Creator").Where("serial_number = ?", normalizeProductSerial(serialNumber)).First(&trace).Error; err != nil {
|
||||||
|
return nil, fmt.Errorf("查询产品溯源失败: %w", errors.New("产品序列号不存在"))
|
||||||
|
}
|
||||||
|
return &trace, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProductTracesService) Create(dto models.CreateProductTraceDTO, userID uint) (*models.ProductTrace, error) {
|
||||||
|
serialNumber := normalizeProductSerial(dto.SerialNumber)
|
||||||
|
if serialNumber == "" {
|
||||||
|
return nil, errors.New("请填写产品序列号")
|
||||||
|
}
|
||||||
|
|
||||||
|
var existing models.ProductTrace
|
||||||
|
if err := database.DB.Unscoped().Where("serial_number = ?", serialNumber).First(&existing).Error; err == nil {
|
||||||
|
return nil, errors.New("产品序列号已存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
trace := models.ProductTrace{
|
||||||
|
CompanyName: strings.TrimSpace(dto.CompanyName),
|
||||||
|
CompanyAddress: strings.TrimSpace(dto.CompanyAddress),
|
||||||
|
CompanyPhone: strings.TrimSpace(dto.CompanyPhone),
|
||||||
|
DeviceInfo: strings.TrimSpace(dto.DeviceInfo),
|
||||||
|
WarrantyPeriod: strings.TrimSpace(dto.WarrantyPeriod),
|
||||||
|
ManufactureDate: dto.ManufactureDate,
|
||||||
|
SerialNumber: serialNumber,
|
||||||
|
OfficialWebsite: strings.TrimSpace(dto.OfficialWebsite),
|
||||||
|
IsActive: true,
|
||||||
|
CreatedBy: &userID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := database.DB.Create(&trace).Error; err != nil {
|
||||||
|
return nil, fmt.Errorf("创建产品溯源失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = database.DB.Preload("Creator").Where("serial_number = ?", trace.SerialNumber).First(&trace)
|
||||||
|
hydrateProductTrace(&trace)
|
||||||
|
return &trace, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProductTracesService) FindAll(page int, limit int, search string) ([]models.ProductTrace, int, int, error) {
|
||||||
|
var traces []models.ProductTrace
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
offset := (page - 1) * limit
|
||||||
|
db := database.DB.Model(&models.ProductTrace{}).Preload("Creator")
|
||||||
|
if search != "" {
|
||||||
|
pattern := "%" + search + "%"
|
||||||
|
db = db.Where("serial_number LIKE ? OR company_name LIKE ? OR device_info LIKE ?", pattern, pattern, pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, 0, fmt.Errorf("查询产品溯源总数失败: %w", err)
|
||||||
|
}
|
||||||
|
if err := db.Order("created_at DESC").Offset(offset).Limit(limit).Find(&traces).Error; err != nil {
|
||||||
|
return nil, 0, 0, fmt.Errorf("查询产品溯源列表失败: %w", err)
|
||||||
|
}
|
||||||
|
for i := range traces {
|
||||||
|
hydrateProductTrace(&traces[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
totalPages := (int(total) + limit - 1) / limit
|
||||||
|
return traces, int(total), totalPages, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProductTracesService) FindOne(serialNumber string) (*models.ProductTrace, error) {
|
||||||
|
trace, err := s.findRaw(serialNumber)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
hydrateProductTrace(trace)
|
||||||
|
return trace, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProductTracesService) PublicQuery(serialNumber string) (*models.ProductTrace, error) {
|
||||||
|
return s.FindOne(serialNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProductTracesService) Update(serialNumber string, dto models.UpdateProductTraceDTO) (*models.ProductTrace, error) {
|
||||||
|
trace, err := s.findRaw(serialNumber)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if dto.CompanyName != "" {
|
||||||
|
trace.CompanyName = strings.TrimSpace(dto.CompanyName)
|
||||||
|
}
|
||||||
|
if dto.CompanyAddress != "" {
|
||||||
|
trace.CompanyAddress = strings.TrimSpace(dto.CompanyAddress)
|
||||||
|
}
|
||||||
|
if dto.CompanyPhone != "" {
|
||||||
|
trace.CompanyPhone = strings.TrimSpace(dto.CompanyPhone)
|
||||||
|
}
|
||||||
|
if dto.DeviceInfo != "" {
|
||||||
|
trace.DeviceInfo = strings.TrimSpace(dto.DeviceInfo)
|
||||||
|
}
|
||||||
|
if dto.WarrantyPeriod != "" {
|
||||||
|
trace.WarrantyPeriod = strings.TrimSpace(dto.WarrantyPeriod)
|
||||||
|
}
|
||||||
|
if dto.ManufactureDate != nil {
|
||||||
|
trace.ManufactureDate = *dto.ManufactureDate
|
||||||
|
}
|
||||||
|
trace.OfficialWebsite = strings.TrimSpace(dto.OfficialWebsite)
|
||||||
|
if dto.WechatQRCode != "" {
|
||||||
|
trace.WechatQRCode = strings.TrimSpace(dto.WechatQRCode)
|
||||||
|
}
|
||||||
|
if dto.IsActive != nil {
|
||||||
|
trace.IsActive = *dto.IsActive
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := database.DB.Save(trace).Error; err != nil {
|
||||||
|
return nil, fmt.Errorf("更新产品溯源失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = database.DB.Preload("Creator").Where("serial_number = ?", trace.SerialNumber).First(trace)
|
||||||
|
hydrateProductTrace(trace)
|
||||||
|
return trace, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProductTracesService) UploadWechatQRCode(serialNumber string, fileHeader *multipart.FileHeader) (*models.ProductTrace, error) {
|
||||||
|
if fileHeader == nil {
|
||||||
|
return nil, errors.New("请选择公众号二维码图片")
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized := normalizeProductSerial(serialNumber)
|
||||||
|
var trace models.ProductTrace
|
||||||
|
if err := database.DB.Where("serial_number = ?", normalized).First(&trace).Error; err != nil {
|
||||||
|
return nil, fmt.Errorf("查询产品溯源失败: %w", errors.New("产品序列号不存在"))
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := config.GetAppConfig().OSS
|
||||||
|
maxFileSize := int64(cfg.MaxFileSizeMB)
|
||||||
|
if maxFileSize <= 0 {
|
||||||
|
maxFileSize = 5
|
||||||
|
}
|
||||||
|
maxFileSize *= 1024 * 1024
|
||||||
|
|
||||||
|
if fileHeader.Size <= 0 {
|
||||||
|
return nil, errors.New("二维码图片内容为空")
|
||||||
|
}
|
||||||
|
if fileHeader.Size > maxFileSize {
|
||||||
|
return nil, fmt.Errorf("二维码图片超过 %dMB 限制", maxFileSize/(1024*1024))
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType := strings.ToLower(strings.TrimSpace(fileHeader.Header.Get("Content-Type")))
|
||||||
|
if !allowedSiteImageContentTypes[contentType] {
|
||||||
|
return nil, errors.New("二维码图片格式不支持")
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := fileHeader.Open()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("读取二维码图片失败: %w", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
objectKey := buildSiteImageObjectKey(cfg.Prefix, "product-traces/"+normalized, fileHeader.Filename)
|
||||||
|
if err := NewOSSService().UploadObject(objectKey, file, contentType); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
trace.WechatQRCode = objectKey
|
||||||
|
if err := database.DB.Save(&trace).Error; err != nil {
|
||||||
|
return nil, fmt.Errorf("保存公众号二维码失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = database.DB.Preload("Creator").Where("serial_number = ?", trace.SerialNumber).First(&trace)
|
||||||
|
hydrateProductTrace(&trace)
|
||||||
|
return &trace, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProductTracesService) Revoke(serialNumber string) (*models.ProductTrace, error) {
|
||||||
|
trace, err := s.findRaw(serialNumber)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
trace.IsActive = false
|
||||||
|
if err := database.DB.Save(trace).Error; err != nil {
|
||||||
|
return nil, fmt.Errorf("停用产品溯源失败: %w", err)
|
||||||
|
}
|
||||||
|
hydrateProductTrace(trace)
|
||||||
|
return trace, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProductTracesService) Delete(serialNumber string) error {
|
||||||
|
trace, err := s.findRaw(serialNumber)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := database.DB.Delete(trace).Error; err != nil {
|
||||||
|
return fmt.Errorf("删除产品溯源失败: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProductTracesService) GenerateQRCode(
|
||||||
|
serialNumber string,
|
||||||
|
baseURL string,
|
||||||
|
requestHost string,
|
||||||
|
protocol string,
|
||||||
|
) (string, string, error) {
|
||||||
|
trace, err := s.FindOne(serialNumber)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if baseURL == "" {
|
||||||
|
baseURL = fmt.Sprintf("%s://%s/product-traces/%s", protocol, requestHost, url.PathEscape(trace.SerialNumber))
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := fmt.Sprintf("temp_qr_%s.png", uuid.New().String())
|
||||||
|
writer, err := standard.New(filePath, standard.WithQRWidth(6))
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("二维码写入器创建失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
qrc, errCode := qr.New(baseURL)
|
||||||
|
if errCode != nil {
|
||||||
|
os.Remove(filePath)
|
||||||
|
return "", "", fmt.Errorf("二维码创建失败: %w", errCode)
|
||||||
|
}
|
||||||
|
if errSave := qrc.Save(writer); errSave != nil {
|
||||||
|
os.Remove(filePath)
|
||||||
|
return "", "", fmt.Errorf("二维码保存失败: %w", errSave)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileContent, errRead := os.ReadFile(filePath)
|
||||||
|
if errRead != nil {
|
||||||
|
os.Remove(filePath)
|
||||||
|
return "", "", fmt.Errorf("二维码文件读取失败: %w", errRead)
|
||||||
|
}
|
||||||
|
os.Remove(filePath)
|
||||||
|
|
||||||
|
qrCodeBase64 := fmt.Sprintf("data:image/png;base64,%s", base64.StdEncoding.EncodeToString(fileContent))
|
||||||
|
return qrCodeBase64, baseURL, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user