feat: add product trace APIs
This commit is contained in:
@@ -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