daShangDao_psiServer/service/product.go

3950 lines
126 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"bytes"
"crypto/md5"
"encoding/json"
"errors"
"fmt"
"github.com/xuri/excelize/v2"
"gorm.io/datatypes"
"gorm.io/gorm"
"io"
"net/http"
"psi/config"
"psi/constant"
"psi/database"
"psi/models"
systemReq "psi/models/request"
systemRes "psi/models/response"
"psi/utils"
"strconv"
"strings"
"time"
)
type ProductService struct{}
type OutTaskInfo struct {
ShopList []systemRes.ShopInfo
}
// ParseExportedExcel 解析系统导出的Excel文件
func (s *ProductService) ParseExportedExcel(fileBytes []byte) ([]map[string]string, error) {
f, err := excelize.OpenReader(bytes.NewReader(fileBytes))
if err != nil {
return nil, fmt.Errorf("读取Excel失败: %v", err)
}
defer f.Close()
sheet := f.GetSheetName(0)
rows, err := f.GetRows(sheet)
if err != nil {
return nil, fmt.Errorf("获取工作表数据失败: %v", err)
}
if len(rows) < 2 {
return nil, fmt.Errorf("表格至少需要包含表头和1行数据")
}
var result []map[string]string
for _, row := range rows[1:] {
if len(row) == 0 {
continue
}
getCol := func(idx int) string {
if idx < len(row) {
return strings.TrimSpace(row[idx])
}
return ""
}
isbn := getCol(0)
name := getCol(3)
if isbn == "" && name == "" {
continue
}
result = append(result, map[string]string{
"isbn": isbn,
"name": name,
"quantity": getCol(4),
"price": getCol(6),
"location_code": getCol(7),
"image": getCol(9),
})
}
return result, nil
}
// ReimportProducts 将导出的Excel修改后导回覆盖已有商品或新增
func (s *ProductService) ReimportProducts(fileBytes []byte, warehouseID int64, db ...*gorm.DB) (*systemReq.ProductReimportResult, error) {
databaseConn := database.OptionalDB(db...)
rows, err := s.ParseExportedExcel(fileBytes)
if err != nil {
return nil, err
}
if len(rows) == 0 {
return nil, fmt.Errorf("Excel中没有有效数据行")
}
result := &systemReq.ProductReimportResult{
FailDetails: make([]string, 0),
}
now := time.Now().Unix()
isbns := make([]string, 0, len(rows))
locationCodes := make([]string, 0)
for _, row := range rows {
if row["isbn"] != "" {
isbns = append(isbns, row["isbn"])
}
if code := row["location_code"]; code != "" {
locationCodes = append(locationCodes, code)
}
}
existingProducts := make(map[string]models.Product)
if len(isbns) > 0 {
var products []models.Product
if err := databaseConn.Where("barcode IN ? AND is_del = ?", isbns, 0).Find(&products).Error; err != nil {
return nil, fmt.Errorf("查询已有商品失败: %v", err)
}
for _, p := range products {
existingProducts[p.Barcode] = p
}
}
locationMap := make(map[string]models.Location)
if len(locationCodes) > 0 {
var locations []models.Location
if err := databaseConn.Where("code IN ? AND warehouse_id = ? AND is_del = ?", locationCodes, warehouseID, 0).Find(&locations).Error; err != nil {
return nil, fmt.Errorf("查询库位失败: %v", err)
}
for _, loc := range locations {
locationMap[loc.Code] = loc
}
}
tx := databaseConn.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
for _, row := range rows {
isbn := row["isbn"]
name := row["name"]
var locationID int64
if code := row["location_code"]; code != "" {
if loc, exists := locationMap[code]; exists {
locationID = loc.ID
} else {
newLoc := models.Location{
WarehouseID: warehouseID,
Code: code,
Type: 1,
Capacity: 255,
Status: 1,
Sort: 0,
CreatedAt: now,
UpdatedAt: now,
IsDel: 0,
}
if err := tx.Create(&newLoc).Error; err != nil {
tx.Rollback()
return nil, fmt.Errorf("创建库位失败(code=%s): %v", code, err)
}
locationMap[code] = newLoc
locationID = newLoc.ID
}
}
salePrice := int64(0)
if row["price"] != "" {
if f, err := strconv.ParseFloat(row["price"], 64); err == nil {
salePrice = int64(f * 100)
}
}
quantity := int64(0)
if row["quantity"] != "" {
if v, err := strconv.ParseInt(row["quantity"], 10, 64); err == nil {
quantity = v
}
}
var liveImage datatypes.JSON
if row["image"] != "" {
b, _ := json.Marshal([]string{row["image"]})
liveImage = datatypes.JSON(b)
} else {
liveImage = datatypes.JSON("[]")
}
if existing, found := existingProducts[isbn]; found {
updates := map[string]interface{}{
"updated_at": now,
}
if name != "" && name != existing.Name {
updates["name"] = name
}
if salePrice > 0 && salePrice != existing.SalePrice {
updates["sale_price"] = salePrice
}
if liveImage != nil && string(liveImage) != string(existing.LiveImage) {
updates["live_image"] = liveImage
}
if locationID > 0 && locationID != existing.LocationID {
updates["location_id"] = locationID
}
if err := tx.Model(&existing).Updates(updates).Error; err != nil {
result.FailDetails = append(result.FailDetails, fmt.Sprintf("ISBN=%s 更新商品失败: %v", isbn, err))
result.FailCount++
continue
}
if quantity > 0 {
var invDetail models.InventoryDetail
err := tx.Where("product_id = ? AND warehouse_id = ? AND is_del = ?", existing.ID, warehouseID, 0).First(&invDetail).Error
if err == nil {
oldQty := invDetail.Quantity
tx.Model(&invDetail).Updates(map[string]interface{}{
"quantity": quantity,
"available_quantity": quantity - invDetail.LockedQuantity,
"updated_at": now,
})
if quantity != oldQty {
tx.Create(&models.InventoryLog{
WarehouseID: warehouseID,
LocationID: invDetail.LocationID,
ProductID: existing.ID,
BatchNo: invDetail.BatchNo,
ChangeType: constant.InventoryChangeAdjustment,
ChangeQuantity: quantity - oldQty,
BeforeQuantity: oldQty,
AfterQuantity: quantity,
RelatedOrderType: "",
RelatedOrderNo: "",
Operator: "",
OperatorID: 0,
Remark: "Excel回传覆盖",
CreatedAt: now,
IsDel: 0,
})
}
} else if err == gorm.ErrRecordNotFound && quantity > 0 {
locID := locationID
if locID == 0 {
locID = existing.LocationID
}
tx.Create(&models.InventoryDetail{
WarehouseID: warehouseID,
LocationID: locID,
ProductID: existing.ID,
BatchNo: utils.GenerateExcelImportBatchNo(),
Quantity: quantity,
LockedQuantity: 0,
AvailableQuantity: quantity,
CreatedAt: now,
UpdatedAt: now,
IsDel: 0,
})
var inv models.Inventory
invErr := tx.Where("product_id = ? AND warehouse_id = ? AND is_del = ?", existing.ID, warehouseID, 0).First(&inv).Error
if invErr == nil {
tx.Model(&inv).Updates(map[string]interface{}{
"quantity": quantity,
"available_quantity": quantity - inv.LockedQuantity,
"updated_at": now,
})
} else if invErr == gorm.ErrRecordNotFound {
tx.Create(&models.Inventory{
WarehouseID: warehouseID,
ProductID: existing.ID,
BatchNo: utils.GenerateExcelImportBatchNo(),
Quantity: quantity,
LockedQuantity: 0,
AvailableQuantity: quantity,
CreatedAt: now,
UpdatedAt: now,
IsDel: 0,
})
}
}
}
result.UpdatedCount++
} else {
if isbn == "" {
result.FailDetails = append(result.FailDetails, fmt.Sprintf("商品名称=%s ISBN为空跳过新增", name))
result.FailCount++
continue
}
product := models.Product{
CategoryID: 1,
StandardProductID: 1,
Name: name,
Barcode: isbn,
Price: 0,
SalePrice: salePrice,
Cost: 0,
LiveImage: liveImage,
Status: 1,
WarehouseID: warehouseID,
LocationID: locationID,
CreatedAt: now,
UpdatedAt: now,
IsDel: 0,
}
if err := tx.Create(&product).Error; err != nil {
result.FailDetails = append(result.FailDetails, fmt.Sprintf("ISBN=%s 新增商品失败: %v", isbn, err))
result.FailCount++
continue
}
if quantity > 0 {
batchNo := utils.GenerateExcelImportBatchNo()
tx.Create(&models.Inventory{
WarehouseID: warehouseID,
ProductID: product.ID,
BatchNo: batchNo,
Quantity: quantity,
LockedQuantity: 0,
AvailableQuantity: quantity,
CreatedAt: now,
UpdatedAt: now,
IsDel: 0,
})
tx.Create(&models.InventoryDetail{
WarehouseID: warehouseID,
LocationID: locationID,
ProductID: product.ID,
BatchNo: batchNo,
Quantity: quantity,
LockedQuantity: 0,
AvailableQuantity: quantity,
CreatedAt: now,
UpdatedAt: now,
IsDel: 0,
})
}
result.CreatedCount++
}
}
if err := tx.Commit().Error; err != nil {
return nil, fmt.Errorf("提交事务失败: %v", err)
}
result.Message = fmt.Sprintf("导入完成:更新%d个新增%d个失败%d个", result.UpdatedCount, result.CreatedCount, result.FailCount)
if result.FailCount > 0 && len(result.FailDetails) <= 10 {
result.Message += ",失败详情:" + strings.Join(result.FailDetails, "; ")
}
return result, nil
}
// 获取商品列表
func (s *ProductService) GetProductList(req systemReq.GetProductListRequest, db ...*gorm.DB) (*systemRes.ProductListResponse, error) {
databaseConn := database.OptionalDB(db...)
if req.Page < 1 {
req.Page = 1
}
if req.PageSize < 1 || req.PageSize > 100 {
req.PageSize = 20
}
query := databaseConn.Model(&models.Product{}).Where("product.is_del = ?", 0)
if len(req.IDs) > 0 {
query = query.Where("product.id IN ?", req.IDs)
}
if req.Status != "" {
query = query.Where("product.status = ?", req.Status)
}
if req.Keyword != "" {
query = query.Where("product.name LIKE ? OR product.barcode LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
}
if req.StartCreatedAt > 0 {
query = query.Where("product.created_at >= ?", req.StartCreatedAt)
}
if req.EndCreatedAt > 0 {
query = query.Where("product.created_at <= ?", req.EndCreatedAt)
}
if req.MinSalePrice > 0 {
query = query.Where("product.sale_price >= ?", req.MinSalePrice)
}
if req.MaxSalePrice > 0 {
query = query.Where("product.sale_price <= ?", req.MaxSalePrice)
}
if req.WarehouseID > 0 || req.MinStock > 0 || req.MaxStock > 0 || req.LocationID > 0 {
query = query.Joins("LEFT JOIN inventory_detail inv_filter ON inv_filter.product_id = product.id AND inv_filter.is_del = ?", 0)
if req.WarehouseID > 0 {
query = query.Where("inv_filter.warehouse_id = ?", req.WarehouseID)
}
if req.MinStock > 0 {
query = query.Where("COALESCE(inv_filter.quantity, 0) > ?", req.MinStock)
}
if req.MaxStock > 0 {
query = query.Where("COALESCE(inv_filter.quantity, 0) < ?", req.MaxStock)
}
if req.LocationID > 0 {
query = query.Where("inv_filter.location_id = ?", req.LocationID)
}
}
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, utils.NewError("查询总数失败")
}
var products []systemRes.ProductWithInfo
offset := (req.Page - 1) * req.PageSize
if err := query.Select("product.*,c.name as category_name,w.id as warehouse_id,w.code as warehouse_code,w.name as warehouse_name,l.id as location_id,l.code as location_code,t.quantity").
Joins("LEFT JOIN product_category c ON c.id = product.category_id").
Joins("LEFT JOIN inventory t ON t.product_id = product.id").
Joins("LEFT JOIN inventory_detail i ON i.product_id = product.id").
Joins("LEFT JOIN warehouse w ON w.id = i.warehouse_id").
Joins("LEFT JOIN location l ON l.id = i.location_id").
Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&products).Error; err != nil {
return nil, utils.NewError("查询商品列表失败")
}
productIDs := make([]int64, 0, len(products))
for _, product := range products {
productIDs = append(productIDs, product.ID)
}
outTaskInfoMap, err := s.getProductOutTaskInfo(databaseConn, productIDs)
if err != nil {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "查询商品任务信息",
"error": fmt.Sprintf("查询失败: %v", err),
})
}
var productItems []systemRes.ProductItem
for _, product := range products {
item := systemRes.ConvertProductWithInfoToItem(product)
if outTaskInfo, exists := outTaskInfoMap[product.ID]; exists {
item.ShopList = outTaskInfo.ShopList
}
productItems = append(productItems, item)
}
locatedCount, unlocatedCount, enabledCount, disabledCount := s.getProductStatistics(databaseConn, req, total)
return &systemRes.ProductListResponse{
List: productItems,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
LocatedCount: locatedCount,
UnlocatedCount: unlocatedCount,
EnabledCount: enabledCount,
DisabledCount: disabledCount,
}, nil
}
// getProductStatistics 获取商品统计数据
func (s *ProductService) getProductStatistics(db *gorm.DB, req systemReq.GetProductListRequest, total int64) (int64, int64, int64, int64) {
var enabledCount, disabledCount, locatedCount int64
buildBaseQuery := func() *gorm.DB {
query := db.Model(&models.Product{}).Where("product.is_del = ?", 0)
if len(req.IDs) > 0 {
query = query.Where("product.id IN ?", req.IDs)
}
if req.Keyword != "" {
query = query.Where("product.name LIKE ? OR product.barcode LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
}
if req.StartCreatedAt > 0 {
query = query.Where("product.created_at >= ?", req.StartCreatedAt)
}
if req.EndCreatedAt > 0 {
query = query.Where("product.created_at <= ?", req.EndCreatedAt)
}
if req.MinSalePrice > 0 {
query = query.Where("product.sale_price >= ?", req.MinSalePrice)
}
if req.MaxSalePrice > 0 {
query = query.Where("product.sale_price <= ?", req.MaxSalePrice)
}
hasInventoryFilter := req.WarehouseID > 0 || req.MinStock > 0 || req.MaxStock > 0 || req.LocationID > 0
if hasInventoryFilter {
query = query.Joins("LEFT JOIN inventory_detail inv_filter ON inv_filter.product_id = product.id AND inv_filter.is_del = ?", 0)
if req.WarehouseID > 0 {
query = query.Where("inv_filter.warehouse_id = ?", req.WarehouseID)
}
if req.MinStock > 0 {
query = query.Where("COALESCE(inv_filter.quantity, 0) > ?", req.MinStock)
}
if req.MaxStock > 0 {
query = query.Where("COALESCE(inv_filter.quantity, 0) < ?", req.MaxStock)
}
if req.LocationID > 0 {
query = query.Where("inv_filter.location_id = ?", req.LocationID)
}
}
return query
}
buildBaseQuery().Where("product.status = ?", 1).Count(&enabledCount)
buildBaseQuery().Where("product.status = ?", 0).Count(&disabledCount)
locatedQuery := db.Table("product").
Joins("INNER JOIN inventory_detail inv ON inv.product_id = product.id AND inv.is_del = ?", 0).
Where("product.is_del = ?", 0)
if len(req.IDs) > 0 {
locatedQuery = locatedQuery.Where("product.id IN ?", req.IDs)
}
if req.Status != "" {
locatedQuery = locatedQuery.Where("product.status = ?", req.Status)
}
if req.Keyword != "" {
locatedQuery = locatedQuery.Where("product.name LIKE ? OR product.barcode LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
}
if req.StartCreatedAt > 0 {
locatedQuery = locatedQuery.Where("product.created_at >= ?", req.StartCreatedAt)
}
if req.EndCreatedAt > 0 {
locatedQuery = locatedQuery.Where("product.created_at <= ?", req.EndCreatedAt)
}
if req.MinSalePrice > 0 {
locatedQuery = locatedQuery.Where("product.sale_price >= ?", req.MinSalePrice)
}
if req.MaxSalePrice > 0 {
locatedQuery = locatedQuery.Where("product.sale_price <= ?", req.MaxSalePrice)
}
if req.WarehouseID > 0 {
locatedQuery = locatedQuery.Where("inv.warehouse_id = ?", req.WarehouseID)
}
if req.MinStock > 0 {
locatedQuery = locatedQuery.Where("inv.quantity > ?", req.MinStock)
}
if req.MaxStock > 0 {
locatedQuery = locatedQuery.Where("inv.quantity < ?", req.MaxStock)
}
if req.LocationID > 0 {
locatedQuery = locatedQuery.Where("inv.location_id = ?", req.LocationID)
}
locatedQuery.Distinct("product.id").Count(&locatedCount)
unlocatedCount := total - locatedCount
return locatedCount, unlocatedCount, enabledCount, disabledCount
}
// 获取商品任务信息
func (s *ProductService) GetDistributionProductList(req systemReq.GetDistributionProductListRequest) (*systemRes.ProductListResponse, error) {
databaseConn, err := database.GetTenantDB(req.UserID)
if err != nil {
return nil, utils.NewError("获取数据库连接失败")
}
if req.Page < 1 {
req.Page = 1
}
if req.PageSize < 1 || req.PageSize > 100 {
req.PageSize = 20
}
query := databaseConn.Model(&models.Product{}).Where("product.is_del = ?", 0)
if req.Keyword != "" {
query = query.Where("(product.name LIKE ? OR product.barcode LIKE ?)", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
}
if req.Name != "" {
query = query.Where("product.name LIKE ?", "%"+req.Name+"%")
}
if req.Barcode != "" {
query = query.Where("product.barcode LIKE ?", "%"+req.Barcode+"%")
}
if req.Appearance > 0 {
query = query.Where("product.appearance = ?", req.Appearance)
}
if req.StartCreatedAt > 0 {
query = query.Where("product.created_at >= ?", req.StartCreatedAt)
}
if req.EndCreatedAt > 0 {
query = query.Where("product.created_at <= ?", req.EndCreatedAt)
}
if req.MinSalePrice > 0 {
query = query.Where("product.sale_price >= ?", req.MinSalePrice)
}
if req.MaxSalePrice > 0 {
query = query.Where("product.sale_price <= ?", req.MaxSalePrice)
}
subQuery := databaseConn.Table("inventory").
Select("product_id, SUM(quantity) as total_quantity").
Where("is_del = ?", 0)
if req.StartStockAt > 0 {
subQuery = subQuery.Where("created_at >= ?", req.StartStockAt)
}
if req.EndStockAt > 0 {
subQuery = subQuery.Where("created_at <= ?", req.EndStockAt)
}
if req.WarehouseID > 0 {
subQuery = subQuery.Where("warehouse_id = ?", req.WarehouseID)
}
subQuery = subQuery.Group("product_id").Having("SUM(quantity) > 0")
query = query.Joins("INNER JOIN (?) as inv_sum ON inv_sum.product_id = product.id", subQuery)
if req.LocationID > 0 {
query = query.Joins("INNER JOIN inventory_detail inv_detail ON inv_detail.product_id = product.id AND inv_detail.location_id = ? AND inv_detail.is_del = ?", req.LocationID, 0)
}
if req.MinStock > 0 {
query = query.Where("inv_sum.total_quantity > ?", req.MinStock)
}
if req.MaxStock > 0 {
query = query.Where("inv_sum.total_quantity < ?", req.MaxStock)
}
if req.StockValue > 0 && req.StockOperator != "" {
query = query.Where("inv_sum.total_quantity "+req.StockOperator+" ?", req.StockValue)
}
if req.SalePriceValue > 0 && req.SalePriceOperator != "" {
query = query.Where("product.sale_price "+req.SalePriceOperator+" ?", req.SalePriceValue)
}
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, utils.NewError("查询总数失败")
}
var products []systemRes.ProductWithInfo
offset := (req.Page - 1) * req.PageSize
if err := query.Select("product.*, c.name as category_name, w.id as warehouse_id, w.code as warehouse_code, " +
"w.name as warehouse_name, l.id as location_id, l.code as location_code, inv_sum.total_quantity as quantity, " +
"IF(ls.fir_price IS NOT NULL, ls.fir_price * 100, NULL) as fir_price, " +
"FROM_UNIXTIME(i.created_at, '%Y-%m-%d %H:%i:%s') as inbound_time ").
Joins("LEFT JOIN product_category c ON c.id = product.category_id").
Joins("LEFT JOIN inventory_detail i ON i.product_id = product.id AND i.is_del = 0").
Joins("LEFT JOIN warehouse w ON w.id = i.warehouse_id").
Joins("LEFT JOIN location l ON l.id = i.location_id").
Joins("left join logistics ls ON ls.warehouse_id = w.id and ls.del_flag='0'").
Order("product.created_at DESC").Offset(offset).Limit(req.PageSize).Find(&products).Error; err != nil {
return nil, utils.NewError("查询商品列表失败")
}
var productItems []systemRes.ProductItem
for _, product := range products {
item := systemRes.ConvertProductWithInfoToItem(product)
productItems = append(productItems, item)
}
return &systemRes.ProductListResponse{
List: productItems,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
}, nil
}
// GetProductDetail 获取商品详情
func (s *ProductService) GetProductDetail(req systemReq.GetProductDetailRequest, db ...*gorm.DB) (*systemRes.ProductItem, error) {
databaseConn := database.OptionalDB(db...)
if req.ID <= 0 {
return nil, utils.NewError("商品ID不能为空")
}
var product models.Product
if err := databaseConn.Where("id = ? AND is_del = ?", req.ID, 0).First(&product).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, utils.NewError("商品不存在")
}
return nil, utils.NewError("查询商品详情失败")
}
// 打印SQL
var liveImage []string
if len(product.LiveImage) > 0 {
err := json.Unmarshal(product.LiveImage, &liveImage)
if err != nil {
return nil, err
}
}
item := &systemRes.ProductItem{
ID: product.ID,
CategoryID: product.CategoryID,
StandardProductID: product.StandardProductID,
Name: product.Name,
Appearance: product.Appearance,
Barcode: product.Barcode,
Price: product.Price,
SalePrice: product.SalePrice,
LiveImage: liveImage,
IsBatchManaged: product.IsBatchManaged,
IsShelfLifeManaged: product.IsShelfLifeManaged,
Status: product.Status,
CreatedAt: product.CreatedAt,
UpdatedAt: product.UpdatedAt,
ShopList: []systemRes.ShopInfo{},
}
return item, nil
}
/*// GetProductFullInfo 获取商品完整信息(包含库存和店铺信息)
func (s *ProductService) GetProductFullInfo(req systemReq.GetProductFullInfoRequest, db ...*gorm.DB) (*systemRes.ProductFullInfoResponse, error) {
databaseConn := database.OptionalDB(db...)
utils.InfoLog(constant.LoggerChannelWork, map[string]interface{}{
"action": "开始查询商品完整信息",
"product_id": req.ProductID,
"user_id": req.UserID,
})
if req.ProductID <= 0 {
return nil, utils.NewError("商品ID不能为空")
}
if req.UserID <= 0 {
return nil, utils.NewError("用户ID不能为空")
}
var product models.Product
if err := databaseConn.Where("id = ? AND is_del = ?", req.ProductID, 0).First(&product).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"action": "商品不存在",
"product_id": req.ProductID,
})
return nil, utils.NewError("商品不存在")
}
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"action": "查询商品失败",
"product_id": req.ProductID,
"error": err.Error(),
})
return nil, utils.NewError("查询商品失败")
}
utils.InfoLog(constant.LoggerChannelWork, map[string]interface{}{
"action": "商品基本信息查询成功",
"product_name": product.Name,
"barcode": product.Barcode,
})
var liveImage []string
if len(product.LiveImage) > 0 {
if err := json.Unmarshal(product.LiveImage, &liveImage); err != nil {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"action": "解析商品图片失败",
"error": err.Error(),
})
liveImage = []string{}
}
}
var categoryName string
if product.CategoryID > 0 {
var category models.ProductCategory
if err := databaseConn.Where("id = ?", product.CategoryID).First(&category).Error; err == nil {
categoryName = category.Name
}
}
var inventories []systemRes.ProductInventoryDetail
if err := databaseConn.Table("inventory_detail inv").
Select("inv.warehouse_id, w.name as warehouse_name, w.code as warehouse_code, inv.location_id, l.code as location_code, l.name as location_name, inv.quantity, inv.inbound_time").
Joins("LEFT JOIN warehouse w ON w.id = inv.warehouse_id").
Joins("LEFT JOIN location l ON l.id = inv.location_id").
Where("inv.product_id = ? AND inv.is_del = ?", req.ProductID, 0).
Scan(&inventories).Error; err != nil {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"action": "查询库存信息失败",
"error": err.Error(),
})
inventories = []systemRes.ProductInventoryDetail{}
}
utils.InfoLog(constant.LoggerChannelWork, map[string]interface{}{
"action": "库存信息查询完成",
"inventory_count": len(inventories),
})
outTaskInfoMap, err := s.getProductOutTaskInfo(databaseConn, []int64{req.ProductID})
if err != nil {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"action": "查询任务信息失败",
"error": err.Error(),
})
}
var shopList []systemRes.ShopInfo
if outTaskInfo, exists := outTaskInfoMap[req.ProductID]; exists {
shopList = outTaskInfo.ShopList
}
response := &systemRes.ProductFullInfoResponse{
ID: product.ID,
CategoryID: product.CategoryID,
CategoryName: categoryName,
StandardProductID: product.StandardProductID,
Name: product.Name,
Appearance: product.Appearance,
Barcode: product.Barcode,
Price: product.Price,
SalePrice: product.SalePrice,
LiveImage: liveImage,
IsBatchManaged: product.IsBatchManaged,
IsShelfLifeManaged: product.IsShelfLifeManaged,
Status: product.Status,
CreatedAt: product.CreatedAt,
UpdatedAt: product.UpdatedAt,
Inventories: inventories,
ShopList: shopList,
}
utils.InfoLog(constant.LoggerChannelWork, map[string]interface{}{
"action": "商品完整信息查询成功",
"success": true,
"shop_count": len(shopList),
})
return response, nil
}*/
// GetProductFullInfo 获取商品完整信息(包含库存和店铺信息) - 从租户分库查询
func (s *ProductService) GetProductFullInfo(req systemReq.GetProductFullInfoRequest, db ...*gorm.DB) (*systemRes.ProductFullInfoResponse, error) {
fmt.Printf("【断点1】开始查询商品完整信息 - ProductID: %d, UserID: %d\n", req.ProductID, req.UserID)
if req.ProductID <= 0 {
return nil, utils.NewError("商品ID不能为空")
}
if req.UserID <= 0 {
return nil, utils.NewError("用户ID不能为空")
}
databaseConn, err := database.GetTenantDB(req.UserID)
if err != nil {
fmt.Printf("【断点2】获取租户数据库连接失败: %v\n", err)
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"action": "获取租户数据库连接失败",
"user_id": req.UserID,
"error": err.Error(),
})
return nil, utils.NewError("获取数据库连接失败")
}
fmt.Printf("【断点3】租户数据库连接成功, 开始查询商品信息\n")
var product models.Product
if err := databaseConn.Where("id = ? AND is_del = ?", req.ProductID, 0).First(&product).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
fmt.Printf("【断点4】商品不存在 - ProductID: %d\n", req.ProductID)
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"action": "商品不存在",
"product_id": req.ProductID,
"user_id": req.UserID,
})
return nil, utils.NewError("商品不存在")
}
fmt.Printf("【断点5】查询商品失败: %v\n", err)
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"action": "查询商品失败",
"product_id": req.ProductID,
"user_id": req.UserID,
"error": err.Error(),
})
return nil, utils.NewError("查询商品失败")
}
fmt.Printf("【断点6】商品基本信息查询成功 - Name: %s, Barcode: %s, Cost: %d, Stock: %d\n",
product.Name, product.Barcode, product.Cost, product.Stock)
var liveImage []string
if len(product.LiveImage) > 0 {
if err := json.Unmarshal(product.LiveImage, &liveImage); err != nil {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"action": "解析商品图片失败",
"error": err.Error(),
})
liveImage = []string{}
}
}
var categoryName string
if product.CategoryID > 0 {
var category models.ProductCategory
if err := databaseConn.Where("id = ?", product.CategoryID).First(&category).Error; err == nil {
categoryName = category.Name
}
}
fmt.Printf("【断点7】开始查询库存明细信息\n")
var inventories []systemRes.ProductInventoryDetail
if err := databaseConn.Table("inventory_detail inv").
Select("inv.warehouse_id, w.name as warehouse_name, w.code as warehouse_code, inv.location_id, l.code as location_code, l.name as location_name, inv.quantity, FROM_UNIXTIME(inv.created_at) as inbound_time").
Joins("LEFT JOIN warehouse w ON w.id = inv.warehouse_id AND w.is_del = 0").
Joins("LEFT JOIN location l ON l.id = inv.location_id AND l.is_del = 0").
Where("inv.product_id = ? AND inv.is_del = ?", req.ProductID, 0).
Scan(&inventories).Error; err != nil {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"action": "查询库存信息失败",
"error": err.Error(),
})
inventories = []systemRes.ProductInventoryDetail{}
}
fmt.Printf("【断点8】库存明细查询完成 - 数量: %d\n", len(inventories))
totalStock := int64(0)
for _, inv := range inventories {
totalStock += inv.Quantity
}
fmt.Printf("【断点9】库存统计 - 模型层Stock: %d, 计算总库存(total_stock): %d, 运费(Cost): %d\n",
product.Stock, totalStock, product.Cost)
outTaskInfoMap, err := s.getProductOutTaskInfo(databaseConn, []int64{req.ProductID})
if err != nil {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"action": "查询任务信息失败",
"error": err.Error(),
})
}
var shopList []systemRes.ShopInfo
if outTaskInfo, exists := outTaskInfoMap[req.ProductID]; exists {
shopList = outTaskInfo.ShopList
}
fmt.Printf("【断点10】店铺信息查询完成 - 数量: %d\n", len(shopList))
response := &systemRes.ProductFullInfoResponse{
ID: product.ID,
CategoryID: product.CategoryID,
CategoryName: categoryName,
StandardProductID: product.StandardProductID,
Name: product.Name,
Appearance: product.Appearance,
Barcode: product.Barcode,
Price: product.Price,
SalePrice: product.SalePrice,
Cost: product.Cost,
Stock: product.Stock,
LiveImage: liveImage,
IsBatchManaged: product.IsBatchManaged,
IsShelfLifeManaged: product.IsShelfLifeManaged,
Status: product.Status,
CreatedAt: product.CreatedAt,
UpdatedAt: product.UpdatedAt,
TotalStock: totalStock,
Inventories: inventories,
ShopList: shopList,
}
fmt.Printf("【断点11】商品完整信息查询成功,准备返回\n")
utils.InfoLog(constant.LoggerChannelWork, map[string]interface{}{
"action": "商品完整信息查询成功",
"product_id": req.ProductID,
"user_id": req.UserID,
"product_name": product.Name,
"barcode": product.Barcode,
"sale_price": product.SalePrice,
"cost": product.Cost,
"stock": product.Stock,
"total_stock": totalStock,
"inventory_count": len(inventories),
"shop_count": len(shopList),
"success": true,
})
return response, nil
}
// UpdateProductNameAndImages 修改商品名称和实拍图
func (s *ProductService) UpdateProductNameAndImages(req systemReq.UpdateProductNameAndImagesRequest, db ...*gorm.DB) error {
databaseConn := database.OptionalDB(db...)
if req.ProductID <= 0 {
return fmt.Errorf("商品ID不能为空")
}
var product models.Product
if err := databaseConn.Where("id = ? AND is_del = ?", req.ProductID, 0).First(&product).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("商品不存在")
}
return fmt.Errorf("查询商品失败: %v", err)
}
fmt.Printf("【UpdateProductNameAndImages Service】原商品图片: %s\n", string(product.LiveImage))
updates := make(map[string]interface{})
hasUpdate := false
if req.Name != "" {
updates["name"] = req.Name
hasUpdate = true
fmt.Printf("【UpdateProductNameAndImages Service】更新名称: %s\n", req.Name)
}
// 只要LiveImage不为nil,就覆盖(包括空数组)
// 注意: Gin的ShouldBind对于未传的数组字段会设为nil,对于传的空数组会设为[]string{}
if req.LiveImage != nil {
jsonBytes, _ := json.Marshal(req.LiveImage)
updates["live_image"] = datatypes.JSON(jsonBytes)
hasUpdate = true
fmt.Printf("【UpdateProductNameAndImages Service】覆盖图片为: %s (数量: %d)\n", string(jsonBytes), len(req.LiveImage))
}
if !hasUpdate {
return fmt.Errorf("至少需要提供一个要修改的字段")
}
updates["updated_at"] = time.Now().Unix()
if err := databaseConn.Model(&product).Updates(updates).Error; err != nil {
return fmt.Errorf("更新商品失败: %v", err)
}
fmt.Printf("【UpdateProductNameAndImages Service】更新成功\n")
return nil
}
// getProductOutTaskInfo
func (s *ProductService) getProductOutTaskInfo(db *gorm.DB, productIDs []int64) (map[int64]*OutTaskInfo, error) {
resultMap := make(map[int64]*OutTaskInfo)
if len(productIDs) == 0 {
return resultMap, nil
}
type OutTaskResult struct {
ProductID int64 `gorm:"column:product_id"`
ShopID int64 `gorm:"column:shop_id"`
ShopAliasName string `gorm:"column:shop_alias_name"`
ShopType int8 `gorm:"column:shop_type"`
IsSent bool `gorm:"column:is_sent"`
OutTaskLogID int64 `gorm:"column:out_task_log_id"`
OutTaskID int64 `gorm:"column:out_task_id"`
LogStatus int8 `gorm:"column:log_status"`
LogMsg string `gorm:"column:log_msg"`
LogCreatedAt int64 `gorm:"column:log_created_at"`
}
var outTaskResults []OutTaskResult
err := db.Table("wave_task_detail wtd").
Select(`wtd.product_id,
s.id as shop_id,
s.shop_alias_name,
s.shop_type,
CASE WHEN ot.id IS NOT NULL THEN 1 ELSE 0 END as is_sent,
otl.id as out_task_log_id,
ot.out_task_id as out_task_id,
otl.status as log_status,
otl.msg as log_msg,
otl.created_at as log_created_at`).
Joins("INNER JOIN wave_task wt ON wt.id = wtd.wave_task_id AND wt.is_del = 0").
Joins("LEFT JOIN out_task ot ON ot.wave_task_id = wt.id AND ot.is_del = 0").
Joins("LEFT JOIN shop s ON s.id = ot.shop_id AND s.del_flag = 0").
Joins("LEFT JOIN out_task_log otl ON otl.out_task_id = ot.out_task_id AND otl.product_id = wtd.product_id AND otl.is_del = 0 AND otl.id = (SELECT MAX(otl2.id) FROM out_task_log otl2 WHERE otl2.out_task_id = ot.out_task_id AND otl2.product_id = wtd.product_id AND otl2.is_del = 0)").
Where("wtd.product_id IN ? AND wtd.is_del = 0", productIDs).
Order("otl.created_at DESC").
Scan(&outTaskResults).Error
if err != nil {
return nil, err
}
for _, item := range outTaskResults {
if _, exists := resultMap[item.ProductID]; !exists {
resultMap[item.ProductID] = &OutTaskInfo{
ShopList: []systemRes.ShopInfo{},
}
}
outTaskInfo := resultMap[item.ProductID]
if item.ShopAliasName != "" {
shopInfo := systemRes.ShopInfo{
ShopID: strconv.FormatInt(item.ShopID, 10),
ShopAliasName: item.ShopAliasName,
ShopType: item.ShopType,
ShopTypeName: s.getShopTypeName(item.ShopType),
IsSent: item.IsSent,
OutTaskLogID: item.OutTaskLogID,
OutTaskID: item.OutTaskID,
LogStatus: item.LogStatus,
LogMsg: item.LogMsg,
LogCreatedAt: item.LogCreatedAt,
}
exists := false
for _, shop := range outTaskInfo.ShopList {
if shop.ShopAliasName == item.ShopAliasName && shop.ShopType == item.ShopType {
exists = true
break
}
}
if !exists {
outTaskInfo.ShopList = append(outTaskInfo.ShopList, shopInfo)
}
}
}
return resultMap, nil
}
// SaveProduct 保存商品
func (s *ProductService) SaveProduct(req systemReq.ProductRequest, db ...*gorm.DB) (int64, error) {
databaseConn := database.OptionalDB(db...)
now := time.Now().Unix()
if req.ID > 0 {
return s.updateProduct(req, now, databaseConn)
}
return s.createProduct(req, now, databaseConn)
}
// createProduct 创建商品
func (s *ProductService) createProduct(req systemReq.ProductRequest, now int64, db *gorm.DB) (int64, error) {
var liveImage datatypes.JSON
if len(req.LiveImage) > 0 {
jsonBytes, _ := json.Marshal(req.LiveImage)
liveImage = jsonBytes
} else {
liveImage = datatypes.JSON("[]")
}
product := models.Product{
CategoryID: req.CategoryID,
StandardProductID: req.StandardProductID,
Name: req.Name,
Appearance: req.Appearance,
Barcode: req.Barcode,
Price: req.Price,
SalePrice: req.SalePrice,
Cost: req.Cost,
LiveImage: liveImage,
IsBatchManaged: req.IsBatchManaged,
IsShelfLifeManaged: req.IsShelfLifeManaged,
Status: req.Status,
CreatedAt: now,
UpdatedAt: now,
IsDel: 0,
}
if err := db.Create(&product).Error; err != nil {
return 0, fmt.Errorf("创建商品失败:%w", err)
}
return product.ID, nil
}
// updateProduct 修改商品
func (s *ProductService) updateProduct(req systemReq.ProductRequest, now int64, db *gorm.DB) (int64, error) {
var product models.Product
if err := db.Where("id = ? AND is_del = 0", req.ID).First(&product).Error; err != nil {
return 0, fmt.Errorf("商品不存在:%w", err)
}
var liveImage datatypes.JSON
if len(req.LiveImage) > 0 {
jsonBytes, _ := json.Marshal(req.LiveImage)
liveImage = jsonBytes
} else {
liveImage = product.LiveImage
}
updates := map[string]interface{}{
"category_id": req.CategoryID,
"standard_product_id": req.StandardProductID,
"name": req.Name,
"appearance": req.Appearance,
"barcode": req.Barcode,
"price": req.Price,
"sale_price": req.SalePrice,
"cost": req.Cost,
"live_image": liveImage,
"is_batch_managed": req.IsBatchManaged,
"is_shelf_life_managed": req.IsShelfLifeManaged,
"status": req.Status,
"updated_at": now,
}
if err := db.Model(&product).Updates(updates).Error; err != nil {
return 0, fmt.Errorf("更新商品失败:%w", err)
}
return product.ID, nil
}
// DeleteProduct 删除商品
func (s *ProductService) DeleteProduct(req systemReq.DeleteProductRequest, db ...*gorm.DB) error {
databaseConn := database.OptionalDB(db...)
var product models.Product
if err := databaseConn.Where("id = ? AND is_del = ?", req.ID, 0).First(&product).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("商品不存在")
}
return fmt.Errorf("查询商品失败: %v", err)
}
now := time.Now().Unix()
err := executeInTransactionWithDB(databaseConn, func(tx *gorm.DB) error {
var purchaseOrderItems []models.PurchaseOrderItem
if err := tx.Where("product_id = ? AND is_del = ?", req.ID, 0).Find(&purchaseOrderItems).Error; err != nil {
return fmt.Errorf("查询采购订单明细失败: %v", err)
}
if len(purchaseOrderItems) > 0 {
purchaseOrderIDs := make([]int64, 0)
for _, item := range purchaseOrderItems {
purchaseOrderIDs = append(purchaseOrderIDs, item.PurchaseOrderID)
}
if err := tx.Model(&models.PurchaseOrderItem{}).
Where("product_id = ? AND is_del = ?", req.ID, 0).
Updates(map[string]interface{}{
"is_del": 1,
"updated_at": now,
}).Error; err != nil {
return fmt.Errorf("删除采购订单明细失败: %v", err)
}
for _, poID := range purchaseOrderIDs {
var remainingItems int64
if err := tx.Model(&models.PurchaseOrderItem{}).
Where("purchase_order_id = ? AND is_del = ?", poID, 0).
Count(&remainingItems).Error; err != nil {
return fmt.Errorf("检查采购订单剩余明细失败: %v", err)
}
if remainingItems == 0 {
if err := tx.Model(&models.PurchaseOrder{}).
Where("id = ? AND is_del = ?", poID, 0).
Updates(map[string]interface{}{
"is_del": 1,
"updated_at": now,
}).Error; err != nil {
return fmt.Errorf("删除采购订单失败: %v", err)
}
}
}
}
var waveHeaders []models.WaveHeader
if err := tx.Where("direction = ? AND status <= ? AND is_del = ?", 1, 2, 0).
Find(&waveHeaders).Error; err != nil {
return fmt.Errorf("查询波次主表失败: %v", err)
}
for _, waveHeader := range waveHeaders {
var waveTasks []models.WaveTask
if err := tx.Where("wave_id = ? AND is_del = ?", waveHeader.ID, 0).
Find(&waveTasks).Error; err != nil {
return fmt.Errorf("查询波次任务失败: %v", err)
}
for _, waveTask := range waveTasks {
var taskDetails []models.WaveTaskDetail
if err := tx.Where("wave_task_id = ? AND product_id = ? AND is_del = ?", waveTask.ID, req.ID, 0).
Find(&taskDetails).Error; err != nil {
return fmt.Errorf("查询波次任务明细失败: %v", err)
}
if len(taskDetails) > 0 {
if err := tx.Model(&models.WaveTaskDetail{}).
Where("wave_task_id = ? AND product_id = ? AND is_del = ?", waveTask.ID, req.ID, 0).
Updates(map[string]interface{}{
"is_del": 1,
"updated_at": now,
}).Error; err != nil {
return fmt.Errorf("删除波次任务明细失败: %v", err)
}
var remainingDetails int64
if err := tx.Model(&models.WaveTaskDetail{}).
Where("wave_task_id = ? AND is_del = ?", waveTask.ID, 0).
Count(&remainingDetails).Error; err != nil {
return fmt.Errorf("检查波次任务剩余明细失败: %v", err)
}
if remainingDetails == 0 {
if err := tx.Model(&models.WaveTask{}).
Where("id = ? AND is_del = ?", waveTask.ID, 0).
Updates(map[string]interface{}{
"is_del": 1,
"updated_at": now,
}).Error; err != nil {
return fmt.Errorf("删除波次任务失败: %v", err)
}
var remainingTasks int64
if err := tx.Model(&models.WaveTask{}).
Where("wave_id = ? AND is_del = ?", waveHeader.ID, 0).
Count(&remainingTasks).Error; err != nil {
return fmt.Errorf("检查波次剩余任务失败: %v", err)
}
if remainingTasks == 0 {
if err := tx.Model(&models.WaveHeader{}).
Where("id = ? AND is_del = ?", waveHeader.ID, 0).
Updates(map[string]interface{}{
"is_del": 1,
"updated_at": now,
}).Error; err != nil {
return fmt.Errorf("删除波次主表失败: %v", err)
}
}
}
}
}
}
if err := tx.Model(&models.Product{}).
Where("id = ? AND is_del = ?", req.ID, 0).
Updates(map[string]interface{}{
"is_del": 1,
"updated_at": now,
}).Error; err != nil {
return fmt.Errorf("删除商品失败: %v", err)
}
return nil
})
if err != nil {
return err
}
return nil
}
// UpdatePrice 修改商品售价
func (s *ProductService) UpdatePrice(req systemReq.UpdatePriceRequest) error {
databaseConn, err := database.GetTenantDB(req.UserID)
if err != nil {
return fmt.Errorf("获取数据库连接失败: %v", err)
}
now := time.Now().Unix()
var product models.Product
if err := databaseConn.Where("id = ? AND is_del = 0", req.ProductID).First(&product).Error; err != nil {
return fmt.Errorf("商品不存在: %v", err)
}
if req.SalePrice < 0 {
return fmt.Errorf("售价不能为负数")
}
if err := databaseConn.Model(&models.Product{}).Where("id = ?", req.ProductID).Updates(map[string]interface{}{
"sale_price": req.SalePrice,
"cost": req.Cost,
"updated_at": now,
}).Error; err != nil {
return fmt.Errorf("更新售价失败: %v", err)
}
shouldSync := true
var waveTaskDetail models.WaveTaskDetail
if err := databaseConn.Where("product_id = ? AND is_del = 0", req.ProductID).First(&waveTaskDetail).Error; err == nil && waveTaskDetail.WaveTaskID > 0 {
var waveTask models.WaveTask
if err := databaseConn.Where("id = ? AND is_del = 0", waveTaskDetail.WaveTaskID).First(&waveTask).Error; err == nil && waveTask.CarId > 0 {
var car models.Car
if err := databaseConn.Where("id = ? AND is_del = 0", waveTask.CarId).First(&car).Error; err == nil {
if car.PushType == 2 {
shouldSync = false
}
}
}
}
if shouldSync {
if err := s.syncPriceToExternal(waveTaskDetail, req.ProductID, req.SalePrice, req.UserID, databaseConn); err != nil {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "同步售价到外部接口",
"product_id": req.ProductID,
"sale_price": req.SalePrice,
"cost": req.Cost,
"error": fmt.Sprintf("同步失败: %v", err),
})
}
}
return nil
}
// GetProductInventory 获取商品库存数量
func (s *ProductService) GetProductInventory(req systemReq.GetProductInventoryRequest) (*systemRes.ProductInventoryResponse, error) {
databaseConn, err := database.GetTenantDB(req.UserID)
if err != nil {
return nil, fmt.Errorf("获取数据库连接失败: %v", err)
}
// 验证商品是否存在,并获取商品信息
var product models.Product
if err := databaseConn.Where("id = ? AND is_del = ?", req.ProductID, 0).First(&product).Error; err != nil {
return nil, fmt.Errorf("商品不存在")
}
var totalQuantity int64
// type=1: 按品相+ISBN+仓库分组后统计总数量
if req.Type == 1 {
type GroupStock struct {
TotalQuantity int64 `gorm:"column:total_quantity"`
}
var groupList []GroupStock
// 先根据商品的 ISBN 和品相,查询所有匹配的库存记录,再按仓库分组统计(可用量 = 总量 - 锁定量)
databaseConn.Table("inventory").
Select(`
COALESCE(SUM(inventory.quantity - inventory.locked_quantity), 0) as total_quantity
`).
Joins("LEFT JOIN product p ON inventory.product_id = p.id AND p.is_del = ?", 0).
Where("p.barcode = ? AND p.appearance = ? AND inventory.warehouse_id IS NOT NULL AND inventory.is_del = ?",
product.Barcode, product.Appearance, 0).
Group("inventory.warehouse_id").
Scan(&groupList)
// 累加所有分组的可用数量
for _, group := range groupList {
totalQuantity += group.TotalQuantity
}
} else {
databaseConn.Table("inventory").
Select("COALESCE(SUM(quantity), 0)").
Where("product_id = ? AND is_del = ?", req.ProductID, 0).
Scan(&totalQuantity)
}
return &systemRes.ProductInventoryResponse{
Quantity: totalQuantity,
}, nil
}
// RetryOutTask 重试失败的外部任务(单条商品)
func (s *ProductService) RetryOutTask(req systemReq.RetryOutTaskRequest, db ...*gorm.DB) error {
databaseConn := database.OptionalDB(db...)
// 验证店铺是否存在
var shop models.Shop
if err := databaseConn.Where("id = ? AND del_flag = ?", req.ShopID, 0).First(&shop).Error; err != nil {
return fmt.Errorf("店铺不存在: %v", err)
}
// 验证店铺类型是否匹配
if shop.ShopType != req.ShopType {
return fmt.Errorf("店铺类型不匹配")
}
// 获取产品信息
var product models.Product
if err := databaseConn.Where("id = ? AND is_del = ?", req.ProductID, 0).First(&product).Error; err != nil {
return fmt.Errorf("商品不存在: %v", err)
}
// 查找该商品的波次任务明细(可能不存在,比如从外部同步的商品)
var waveTaskDetail models.WaveTaskDetail
var hasWaveTask bool
if err := databaseConn.Where("product_id = ? AND is_del = ?", req.ProductID, 0).First(&waveTaskDetail).Error; err == nil {
hasWaveTask = true
} else {
hasWaveTask = false
}
params := map[string]string{
"shop_id": strconv.FormatInt(req.ShopID, 10),
"shop_type": strconv.Itoa(int(req.ShopType)),
"task_count": "1",
"task_type": "7",
"img_type": "2",
}
sign := utils.SignParams(params)
params["sign"] = sign
// 创建任务
url := config.AppConfig.ExternalAPI.SyncTaskURL
res, err := utils.SubmitFormData(url, params)
if err != nil {
// 创建任务失败,记录日志
s.saveRetryLog(req.ShopID, 0, req.ProductID, product, waveTaskDetail, hasWaveTask, fmt.Sprintf("创建任务接口请求失败: %v", err), databaseConn)
return fmt.Errorf("创建任务接口请求失败: %v", err)
}
var resData systemRes.ExternalAPIResponse
if err := json.Unmarshal([]byte(res), &resData); err != nil {
// 解析响应失败,记录日志
s.saveRetryLog(req.ShopID, 0, req.ProductID, product, waveTaskDetail, hasWaveTask, fmt.Sprintf("解析外部接口响应失败: %v", err), databaseConn)
return fmt.Errorf("解析外部接口响应失败: %v", err)
}
if resData.Code != "200" {
// 创建任务失败,记录日志
s.saveRetryLog(req.ShopID, 0, req.ProductID, product, waveTaskDetail, hasWaveTask, fmt.Sprintf("创建任务接口返回错误: %s", resData.Msg), databaseConn)
return fmt.Errorf("创建任务接口返回错误: %s", resData.Msg)
}
// 解析任务ID
var taskIDInt int64
switch v := resData.Data.(type) {
case float64:
taskIDInt = int64(v)
case string:
if parsed, err := strconv.ParseInt(v, 10, 64); err == nil {
taskIDInt = parsed
}
default:
taskIDInt = 0
}
if taskIDInt == 0 {
s.saveRetryLog(req.ShopID, 0, req.ProductID, product, waveTaskDetail, hasWaveTask, "创建任务接口返回的任务ID无效", databaseConn)
return fmt.Errorf("创建任务接口返回的任务ID无效")
}
// 创建外部任务记录
now := time.Now().Unix()
waveTaskID := func() int64 {
if hasWaveTask {
return waveTaskDetail.WaveTaskID
}
return 0
}()
outTask := models.OutTask{
ShopID: req.ShopID,
WaveTaskID: waveTaskID,
OutTaskID: taskIDInt,
ShopType: req.ShopType,
TaskType: 7,
ImgType: 2,
TaskCount: 1,
CreatedAt: now,
UpdatedAt: now,
IsDel: 0,
}
if createErr := databaseConn.Create(&outTask).Error; createErr != nil {
s.saveRetryLog(req.ShopID, taskIDInt, req.ProductID, product, waveTaskDetail, hasWaveTask, fmt.Sprintf("创建外部任务记录失败: %v", createErr), databaseConn)
return fmt.Errorf("创建外部任务记录失败: %v", createErr)
}
// 调用外部接口推送任务数据
salePrice := product.SalePrice
err = s.syncPriceToExternalTaskWithOptionalWave(outTask, product, waveTaskDetail, hasWaveTask, salePrice, databaseConn)
if err != nil {
// 推送数据失败,记录日志
s.saveRetryLog(req.ShopID, taskIDInt, req.ProductID, product, waveTaskDetail, hasWaveTask, fmt.Sprintf("推送任务数据失败: %v", err), databaseConn)
return fmt.Errorf("推送任务数据失败: %v", err)
}
// 推送成功,更新之前相同 shop_id 和 product_id 的所有日志状态为成功
if updateErr := databaseConn.Model(&models.OutTaskLog{}).
Where("shop_id = ? AND product_id = ? AND is_del = ?", req.ShopID, req.ProductID, 0).
Updates(map[string]interface{}{
"status": 1,
"msg": "成功",
"updated_at": now,
}).Error; updateErr != nil {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "更新历史日志状态失败",
"shop_id": req.ShopID,
"product_id": req.ProductID,
"error": fmt.Sprintf("更新失败: %v", updateErr),
})
}
return nil
}
// BatchPushProducts 批量推送商品到多个店铺
func (s *ProductService) BatchPushProducts(req systemReq.BatchPushProductRequest) error {
fmt.Printf("[DEBUG BatchPushProducts] 入口 user_id=%d shop_ids=%v shop_types=%v product_ids=%v\n", req.UserID, req.ShopIDs, req.ShopTypes, req.ProductIDs)
utils.InfoLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "批量推送-入口",
"user_id": req.UserID,
"shop_ids": req.ShopIDs,
"shop_types": req.ShopTypes,
"product_ids": req.ProductIDs,
})
db, err := database.GetTenantDB(req.UserID)
if err != nil {
fmt.Printf("[DEBUG BatchPushProducts] 获取数据库连接失败 user_id=%d err=%v\n", req.UserID, err)
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "批量推送-获取数据库连接失败",
"user_id": req.UserID,
"error": err.Error(),
})
return fmt.Errorf("获取数据库连接失败: %v", err)
}
if len(req.ShopIDs) != len(req.ShopTypes) {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "批量推送-参数校验失败",
"shop_ids_count": len(req.ShopIDs),
"shop_types_count": len(req.ShopTypes),
})
return fmt.Errorf("商家ID数组与商家类型数组长度不一致")
}
now := time.Now().Unix()
taskCount := int64(len(req.ProductIDs))
type shopTask struct {
shopID int64
shopType int8
taskID int64
outTask models.OutTask
}
var tasks []shopTask
fmt.Printf("[DEBUG BatchPushProducts] 开始遍历店铺 shop_ids=%v task_count=%d\n", req.ShopIDs, taskCount)
for i, shopID := range req.ShopIDs {
shopType := req.ShopTypes[i]
params := map[string]string{
"shop_id": strconv.FormatInt(shopID, 10),
"shop_type": strconv.Itoa(int(shopType)),
"task_count": strconv.FormatInt(taskCount, 10),
"task_type": "7",
"img_type": "2",
}
sign := utils.SignParams(params)
params["sign"] = sign
url := config.AppConfig.ExternalAPI.SyncTaskURL
fmt.Printf("[DEBUG BatchPushProducts] 请求API shop_id=%d url=%s params=%v\n", shopID, url, params)
utils.InfoLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "批量推送-发起创建任务请求",
"shop_id": shopID,
"url": url,
"params": params,
})
res, err := utils.SubmitFormData(url, params)
if err != nil {
fmt.Printf("[DEBUG BatchPushProducts] API请求失败 shop_id=%d err=%v\n", shopID, err)
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "批量推送-创建外部任务失败",
"shop_id": shopID,
"error": fmt.Sprintf("请求失败: %v", err),
})
continue
}
var resData systemRes.ExternalAPIResponse
if err := json.Unmarshal([]byte(res), &resData); err != nil {
fmt.Printf("[DEBUG BatchPushProducts] 解析响应失败 shop_id=%d raw=%s err=%v\n", shopID, res, err)
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "批量推送-解析创建任务响应失败",
"shop_id": shopID,
"raw_response": res,
"error": fmt.Sprintf("解析失败: %v", err),
})
continue
}
fmt.Printf("[DEBUG BatchPushProducts] API响应 shop_id=%d code=%s msg=%s data=%v data_type=%T\n",
shopID, resData.Code, resData.Msg, resData.Data, resData.Data)
utils.InfoLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "批量推送-创建任务响应",
"shop_id": shopID,
"code": resData.Code,
"msg": resData.Msg,
"data": resData.Data,
"data_type": fmt.Sprintf("%T", resData.Data),
})
if resData.Code != "200" {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "批量推送-创建任务返回错误",
"shop_id": shopID,
"code": resData.Code,
"msg": resData.Msg,
})
continue
}
var taskIDInt int64
switch v := resData.Data.(type) {
case float64:
taskIDInt = int64(v)
case string:
if parsed, err := strconv.ParseInt(v, 10, 64); err == nil {
taskIDInt = parsed
}
}
if taskIDInt == 0 {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "批量推送-任务ID无效",
"shop_id": shopID,
})
continue
}
outTask := models.OutTask{
ShopID: shopID,
OutTaskID: taskIDInt,
ShopType: shopType,
TaskType: 7,
ImgType: 2,
TaskCount: taskCount,
CreatedAt: now,
UpdatedAt: now,
IsDel: 0,
}
if err := db.Create(&outTask).Error; err != nil {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "批量推送-保存OutTask记录失败",
"shop_id": shopID,
"out_task_id": taskIDInt,
"error": fmt.Sprintf("保存失败: %v", err),
})
continue
}
tasks = append(tasks, shopTask{
shopID: shopID,
shopType: shopType,
taskID: taskIDInt,
outTask: outTask,
})
}
if len(tasks) == 0 {
fmt.Printf("[DEBUG BatchPushProducts] tasks为空! 没有成功创建任何任务 user_id=%d shop_ids=%v shop_types=%v product_ids=%v\n",
req.UserID, req.ShopIDs, req.ShopTypes, req.ProductIDs)
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "批量推送-没有成功创建任何任务",
"user_id": req.UserID,
"shop_ids": req.ShopIDs,
"shop_types": req.ShopTypes,
"product_ids": req.ProductIDs,
"task_count": taskCount,
})
return fmt.Errorf("没有成功创建任何任务")
}
// 批量查询商品
var products []models.Product
if err := db.Where("id IN ? AND is_del = ?", req.ProductIDs, 0).Find(&products).Error; err != nil {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "批量推送-批量查询商品失败",
"product_ids": req.ProductIDs,
"error": err.Error(),
})
return fmt.Errorf("批量查询商品失败: %v", err)
}
productMap := make(map[int64]models.Product, len(products))
for _, p := range products {
productMap[p.ID] = p
}
// 批量查询库存明细(每个product取第一条)
var inventoryDetails []models.InventoryDetail
if err := db.Where("product_id IN ? AND is_del = ?", req.ProductIDs, 0).Find(&inventoryDetails).Error; err != nil {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "批量推送-批量查询库存明细失败",
"product_ids": req.ProductIDs,
"error": err.Error(),
})
return fmt.Errorf("批量查询库存明细失败: %v", err)
}
inventoryMap := make(map[int64]models.InventoryDetail)
warehouseIDSet := make(map[int64]bool)
for _, inv := range inventoryDetails {
if _, exists := inventoryMap[inv.ProductID]; !exists {
inventoryMap[inv.ProductID] = inv
}
warehouseIDSet[inv.WarehouseID] = true
}
warehouseIDs := make([]int64, 0, len(warehouseIDSet))
for wid := range warehouseIDSet {
warehouseIDs = append(warehouseIDs, wid)
}
// 批量查询仓库
warehouseMap := make(map[int64]models.Warehouse)
if len(warehouseIDs) > 0 {
var warehouses []models.Warehouse
if err := db.Where("id IN ? AND is_del = ?", warehouseIDs, 0).Find(&warehouses).Error; err != nil {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "批量推送-批量查询仓库失败",
"warehouse_ids": warehouseIDs,
"error": err.Error(),
})
return fmt.Errorf("批量查询仓库失败: %v", err)
}
for _, w := range warehouses {
warehouseMap[w.ID] = w
}
}
// 收集所有 logistics_id 并批量查询
logisticsIDSet := make(map[int64]bool)
for _, w := range warehouseMap {
if w.LogisticsID > 0 {
logisticsIDSet[w.LogisticsID] = true
}
}
logisticsIDs := make([]int64, 0, len(logisticsIDSet))
for lid := range logisticsIDSet {
logisticsIDs = append(logisticsIDs, lid)
}
logisticsMap := make(map[int64]models.Logistics)
if len(logisticsIDs) > 0 {
var logisticsList []models.Logistics
if err := db.Where("id IN ? AND del_flag = ?", logisticsIDs, "0").Find(&logisticsList).Error; err != nil {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "批量推送-批量查询物流失败",
"logistics_ids": logisticsIDs,
"error": err.Error(),
})
return fmt.Errorf("批量查询物流失败: %v", err)
}
for _, l := range logisticsList {
logisticsMap[int64(l.Id)] = l
}
}
// 批量查询库位
locationIDSet := make(map[int64]bool)
for _, inv := range inventoryDetails {
if inv.LocationID > 0 {
locationIDSet[inv.LocationID] = true
}
}
locationIDs := make([]int64, 0, len(locationIDSet))
for lid := range locationIDSet {
locationIDs = append(locationIDs, lid)
}
locationMap := make(map[int64]models.Location)
if len(locationIDs) > 0 {
var locations []models.Location
if err := db.Where("id IN ? AND is_del = ?", locationIDs, 0).Find(&locations).Error; err != nil {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "批量推送-批量查询库位失败",
"location_ids": locationIDs,
"error": err.Error(),
})
return fmt.Errorf("批量查询库位失败: %v", err)
}
for _, loc := range locations {
locationMap[loc.ID] = loc
}
}
// 遍历商品推送
for _, productID := range req.ProductIDs {
product, ok := productMap[productID]
if !ok {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "批量推送-商品不存在",
"product_id": productID,
})
continue
}
inventoryDetail, ok := inventoryMap[productID]
if !ok {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "批量推送-库存明细不存在",
"product_id": productID,
})
continue
}
warehouse, ok := warehouseMap[inventoryDetail.WarehouseID]
if !ok {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "批量推送-仓库不存在",
"product_id": productID,
"warehouse_id": inventoryDetail.WarehouseID,
})
continue
}
if warehouse.LogisticsID == 0 {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "批量推送-仓库未绑定运费模板",
"product_id": productID,
"warehouse": warehouse.Name,
})
continue
}
logistics, ok := logisticsMap[warehouse.LogisticsID]
if !ok {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "批量推送-物流模板不存在",
"product_id": productID,
"logistics_id": warehouse.LogisticsID,
})
continue
}
cost := int64(logistics.FirPrice * 100)
stock := inventoryDetail.Quantity
salePrice := product.SalePrice
warehouseCode := warehouse.Code
location, ok := locationMap[inventoryDetail.LocationID]
if !ok {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "批量推送-库位不存在",
"product_id": productID,
"location_id": inventoryDetail.LocationID,
"warehouse_id": inventoryDetail.WarehouseID,
})
continue
}
locationCode := location.Code
condition := product.Appearance
for _, st := range tasks {
s.batchPushProductBody(db, st.outTask, product, stock, salePrice, cost, now, req.UserID, warehouseCode, locationCode, condition)
}
}
return nil
}
// PushProductToShop 上架商品到商铺:创建商品、库存记录,并推送到商铺
func (s *ProductService) PushProductToShop(req systemReq.PushProductToShopRequest) (*systemRes.PushProductToShopResponse, error) {
utils.InfoLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "上架到商铺保存数据库-入口",
"user_id": req.UserID,
"location_id": req.LocationID,
"warehouse_id": req.WarehouseID,
"isbn": req.ISBN,
"price": req.Price,
"stock": req.Stock,
"appearance": req.Appearance,
})
db, err := database.GetTenantDB(req.UserID)
if err != nil {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "上架到商铺保存数据库-获取数据库连接失败",
"user_id": req.UserID,
"error": err.Error(),
})
return nil, fmt.Errorf("获取数据库连接失败: %v", err)
}
// 1. 根据货位ID查找库位
var location models.Location
if err := db.Where("id = ? AND is_del = ?", req.LocationID, 0).First(&location).Error; err != nil {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "上架到商铺保存数据库-货位不存在",
"location_id": req.LocationID,
"error": err.Error(),
})
return nil, fmt.Errorf("货位ID【%d】不存在", req.LocationID)
}
// 2. 根据仓库ID查找仓库
var warehouse models.Warehouse
if err := db.Where("id = ? AND is_del = ?", req.WarehouseID, 0).First(&warehouse).Error; err != nil {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "上架到商铺保存数据库-仓库不存在",
"warehouse_id": req.WarehouseID,
"error": err.Error(),
})
return nil, fmt.Errorf("仓库ID【%d】不存在", req.WarehouseID)
}
now := time.Now().Unix()
// 3. 查找或创建商品
var product models.Product
err = db.Where("barcode = ? AND is_del = ?", req.ISBN, 0).First(&product).Error
// 序列化照片为JSON
var liveImageJSON datatypes.JSON
if len(req.Photos) > 0 {
jsonBytes, _ := json.Marshal(req.Photos)
liveImageJSON = jsonBytes
} else {
liveImageJSON = datatypes.JSON("[]")
}
/////////////////////////////////////////////////////////////////////////
// 创建新商品
productName := req.ISBN // 默认用ISBN作为名称
// 优先使用传入的图书名称
if req.ProductName != "" {
productName = req.ProductName
} else {
// 尝试从ES API获取书名
bookSvc := BookService{}
bookInfo, bookErr := bookSvc.GetBookInfo(systemReq.BookRequest{Isbn: req.ISBN})
if bookErr == nil && bookInfo != nil && bookInfo.BookName.Value != "" {
productName = bookInfo.BookName.Value
utils.InfoLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "上架到商铺-获取书名成功",
"isbn": req.ISBN,
"product_name": productName,
})
}
}
product = models.Product{
Name: productName,
Barcode: req.ISBN,
Appearance: req.Appearance,
Price: req.Price,
SalePrice: req.Price,
LiveImage: liveImageJSON,
Status: 1,
CreatedAt: now,
UpdatedAt: now,
IsDel: 0,
}
if err := db.Create(&product).Error; err != nil {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "上架到商铺-创建商品失败",
"isbn": req.ISBN,
"error": err.Error(),
})
return nil, fmt.Errorf("创建商品失败: %v", err)
}
utils.InfoLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "上架到商铺-创建商品成功",
"product_id": product.ID,
"isbn": req.ISBN,
})
////////////////////////////////////////////////////////////////////////
//if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
// // 创建新商品
// productName := req.ISBN // 默认用ISBN作为名称
// // 优先使用传入的图书名称
// if req.ProductName != "" {
// productName = req.ProductName
// } else {
// // 尝试从ES API获取书名
// bookSvc := BookService{}
// bookInfo, bookErr := bookSvc.GetBookInfo(systemReq.BookRequest{Isbn: req.ISBN})
// if bookErr == nil && bookInfo != nil && bookInfo.BookName.Value != "" {
// productName = bookInfo.BookName.Value
// utils.InfoLog(constant.LoggerChannelWork, map[string]interface{}{
// "source": "上架到商铺-获取书名成功",
// "isbn": req.ISBN,
// "product_name": productName,
// })
// }
// }
//
// product = models.Product{
// Name: productName,
// Barcode: req.ISBN,
// Appearance: req.Appearance,
// Price: req.Price,
// SalePrice: req.Price,
// LiveImage: liveImageJSON,
// Status: 1,
// CreatedAt: now,
// UpdatedAt: now,
// IsDel: 0,
// }
//
// if err := db.Create(&product).Error; err != nil {
// utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
// "source": "上架到商铺-创建商品失败",
// "isbn": req.ISBN,
// "error": err.Error(),
// })
// return nil, fmt.Errorf("创建商品失败: %v", err)
// }
// utils.InfoLog(constant.LoggerChannelWork, map[string]interface{}{
// "source": "上架到商铺-创建商品成功",
// "product_id": product.ID,
// "isbn": req.ISBN,
// })
//} else if err != nil {
// utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
// "source": "上架到商铺-查询商品失败",
// "isbn": req.ISBN,
// "error": err.Error(),
// })
// return nil, fmt.Errorf("查询商品失败: %v", err)
//} else {
// // 更新已有商品
// updates := map[string]interface{}{
// "appearance": req.Appearance,
// "price": req.Price,
// "sale_price": req.Price,
// "updated_at": now,
// }
// if req.ProductName != "" {
// updates["name"] = req.ProductName
// }
// if len(req.Photos) > 0 {
// updates["live_image"] = liveImageJSON
// }
//
// if err := db.Model(&product).Updates(updates).Error; err != nil {
// utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
// "source": "上架到商铺-更新商品失败",
// "product_id": product.ID,
// "error": err.Error(),
// })
// return nil, fmt.Errorf("更新商品失败: %v", err)
// }
// utils.InfoLog(constant.LoggerChannelWork, map[string]interface{}{
// "source": "上架到商铺-更新商品成功",
// "product_id": product.ID,
// "isbn": req.ISBN,
// })
//}
// 4. 创建或更新 InventoryDetail (库位级库存)
var invDetail models.InventoryDetail
err = db.Where("product_id = ? AND location_id = ? AND is_del = ?", product.ID, location.ID, 0).First(&invDetail).Error
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
invDetail = models.InventoryDetail{
WarehouseID: warehouse.ID,
LocationID: location.ID,
ProductID: product.ID,
Quantity: req.Stock,
CreatedAt: now,
UpdatedAt: now,
IsDel: 0,
}
if err := db.Create(&invDetail).Error; err != nil {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "上架到商铺-创建库存明细失败",
"product_id": product.ID,
"location_id": location.ID,
"error": err.Error(),
})
return nil, fmt.Errorf("创建库存明细失败: %v", err)
}
} else if err == nil {
if err := db.Model(&invDetail).Updates(map[string]interface{}{
"quantity": req.Stock,
"updated_at": now,
}).Error; err != nil {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "上架到商铺-更新库存明细失败",
"product_id": product.ID,
"error": err.Error(),
})
return nil, fmt.Errorf("更新库存明细失败: %v", err)
}
}
// 5. 创建或更新 Inventory (仓库级库存)
var inv models.Inventory
err = db.Where("warehouse_id = ? AND product_id = ? AND is_del = ?", warehouse.ID, product.ID, 0).First(&inv).Error
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
inv = models.Inventory{
WarehouseID: warehouse.ID,
ProductID: product.ID,
Quantity: req.Stock,
CreatedAt: now,
UpdatedAt: now,
IsDel: 0,
}
if err := db.Create(&inv).Error; err != nil {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "上架到商铺-创建库存失败",
"product_id": product.ID,
"warehouse_id": warehouse.ID,
"error": err.Error(),
})
return nil, fmt.Errorf("创建库存失败: %v", err)
}
} else if err == nil {
if err := db.Model(&inv).Updates(map[string]interface{}{
"quantity": req.Stock,
"updated_at": now,
}).Error; err != nil {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "上架到商铺-更新库存失败",
"product_id": product.ID,
"error": err.Error(),
})
return nil, fmt.Errorf("更新库存失败: %v", err)
}
}
utils.InfoLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "上架到商铺-数据保存完成",
"product_id": product.ID,
"warehouse_id": warehouse.ID,
"location_id": location.ID,
"quantity": req.Stock,
})
return &systemRes.PushProductToShopResponse{
UserID: req.UserID,
WarehouseID: warehouse.ID,
ProductID: product.ID,
ISBN: req.ISBN,
}, nil
}
// batchPushProductBody 批量上架
func (s *ProductService) batchPushProductBody(db *gorm.DB, outTask models.OutTask, product models.Product, stock, salePrice, cost, now, userID int64, warehouseCode string, locationCode string, condition int64) {
isbn := product.Barcode
var imgList []string
if product.LiveImage != nil && len(product.LiveImage) > 0 {
var rawImgList []string
if err := json.Unmarshal(product.LiveImage, &rawImgList); err != nil {
s.saveOutTaskLogWithStock(outTask, product.ID, isbn, product.LiveImage, stock, salePrice, cost, fmt.Sprintf("解析图片失败: %v", err), db)
return
}
for _, imgStr := range rawImgList {
imgStr = strings.TrimSpace(imgStr)
if imgStr != "" {
imgList = append(imgList, imgStr)
}
}
}
msgData := map[string]interface{}{
"product_id": product.ID,
"user_id": strconv.FormatInt(userID, 10),
"is_distribution": "1",
}
msgJSON, _ := json.Marshal(msgData)
bodyData := map[string]interface{}{
"book_info": map[string]interface{}{
"isbn": isbn,
"book_name": product.Name,
"image_object": map[string]interface{}{
"carousel_url_array": imgList,
"white_background_url": "",
"detail_url_object": map[string]interface{}{},
"introduction_url": []string{},
"catalogue_url": []string{},
"live_shooting_url": []string{},
"other_url": []string{},
"default_image_url": func() string {
if len(imgList) > 0 {
return imgList[0]
}
return ""
}(),
},
},
"detail": map[string]interface{}{
"stock": stock,
"price": salePrice,
"shipping_cost": cost,
"msg": string(msgJSON),
"sku_code": warehouseCode + "##" + locationCode,
"condition": condition,
},
}
bodyDataJSON, err := json.Marshal(bodyData)
if err != nil {
s.saveOutTaskLogWithStock(outTask, product.ID, isbn, product.LiveImage, stock, salePrice, cost, fmt.Sprintf("序列化请求体失败: %v", err), db)
return
}
bodyList := []string{string(bodyDataJSON)}
taskID := fmt.Sprintf("%d", outTask.OutTaskID)
allBody := strings.Join(bodyList, "")
signParams := map[string]string{
"task_id": taskID,
"body": allBody,
}
sign := utils.SignParams(signParams)
url := config.AppConfig.ExternalAPI.SyncTaskBodyURL
resp, err := utils.SubmitMultiBody(url, taskID, bodyList, sign)
if err != nil {
s.saveOutTaskLogWithStock(outTask, product.ID, isbn, product.LiveImage, stock, salePrice, cost, fmt.Sprintf("请求外部接口失败: %v", err), db)
return
}
var resData systemRes.ExternalAPIResponse
if err := json.Unmarshal([]byte(resp), &resData); err != nil {
s.saveOutTaskLogWithStock(outTask, product.ID, isbn, product.LiveImage, stock, salePrice, cost, fmt.Sprintf("解析响应失败: %v", err), db)
return
}
if resData.Code != "200" {
s.saveOutTaskLogWithStock(outTask, product.ID, isbn, product.LiveImage, stock, salePrice, cost, fmt.Sprintf("外部接口返回错误: code=%s, msg=%s", resData.Code, resData.Msg), db)
return
}
if updateErr := db.Model(&models.OutTaskLog{}).
Where("shop_id = ? AND product_id = ? AND is_del = ?", outTask.ShopID, product.ID, 0).
Updates(map[string]interface{}{
"status": 1,
"msg": "成功",
"updated_at": now,
}).Error; updateErr != nil {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "批量推送-更新历史日志状态失败",
"shop_id": outTask.ShopID,
"product_id": product.ID,
"error": fmt.Sprintf("更新失败: %v", updateErr),
})
}
}
// saveRetryLog 保存重试任务日志
func (s *ProductService) saveRetryLog(shopID, outTaskID, productID int64, product models.Product, waveTaskDetail models.WaveTaskDetail, hasWaveTask bool, msg string, db *gorm.DB) {
now := time.Now().Unix()
stock := func() int64 {
if hasWaveTask {
return waveTaskDetail.PlannedQuantity
}
return 0
}()
logRecord := models.OutTaskLog{
ShopID: shopID,
OutTaskID: outTaskID,
ProductID: productID,
ISBN: product.Barcode,
LiveImage: product.LiveImage,
Stock: stock,
SalePrice: product.SalePrice,
Cost: product.Cost,
Status: func() int8 {
if msg == "成功" {
return 1 // 成功
}
return 0 // 失败
}(),
Msg: msg,
CreatedAt: now,
UpdatedAt: now,
IsDel: 0,
}
if err := db.Create(&logRecord).Error; err != nil {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "保存重试任务日志",
"shop_id": shopID,
"product_id": productID,
"out_task_id": outTaskID,
"error": fmt.Sprintf("保存日志失败: %v", err),
})
}
}
// syncPriceToExternalTaskWithOptionalWave 同步售价到单个外部任务(支持无波次明细)
func (s *ProductService) syncPriceToExternalTaskWithOptionalWave(outTask models.OutTask, product models.Product, waveTaskDetail models.WaveTaskDetail, hasWaveTask bool, salePrice int64, db *gorm.DB) error {
isbn := product.Barcode
var imgList []string
if product.LiveImage != nil && len(product.LiveImage) > 0 {
var rawImgList []string
if err := json.Unmarshal(product.LiveImage, &rawImgList); err != nil {
return fmt.Errorf("解析json失败: %v", err)
}
for _, imgStr := range rawImgList {
imgStr = strings.TrimSpace(imgStr)
if imgStr != "" {
imgList = append(imgList, imgStr)
}
}
}
msgData := map[string]interface{}{
"product_id": product.ID,
}
msgJSON, _ := json.Marshal(msgData)
var stock int64
var cost int64
if hasWaveTask {
var logistics models.Logistics
err := db.Table("logistics").
Select("logistics.fir_price").
Joins("INNER JOIN warehouse ON warehouse.logistics_id = logistics.id AND warehouse.is_del = ?", "0").
Joins("INNER JOIN wave_header ON wave_header.warehouse_id = warehouse.id AND wave_header.is_del = 0").
Joins("INNER JOIN wave_task ON wave_task.wave_id = wave_header.id AND wave_task.is_del = 0").
Where("wave_task.id = ?", waveTaskDetail.WaveTaskID).
First(&logistics).Error
if err != nil {
return fmt.Errorf("查询物流运费失败: %v", err)
}
cost = int64(logistics.FirPrice * 100)
stock = waveTaskDetail.PlannedQuantity
} else {
var inventoryDetail models.InventoryDetail
if err := db.Where("product_id = ? AND is_del = ?", product.ID, 0).First(&inventoryDetail).Error; err != nil {
return fmt.Errorf("查询库存明细失败: %v", err)
}
var warehouse models.Warehouse
if err := db.Where("id = ? AND is_del = ?", inventoryDetail.WarehouseID, 0).First(&warehouse).Error; err != nil {
return fmt.Errorf("查询仓库信息失败: %v", err)
}
if warehouse.LogisticsID == 0 {
return fmt.Errorf("仓库未绑定运费模板")
}
var logistics models.Logistics
if err := db.Where("id = ? AND del_flag = ?", warehouse.LogisticsID, "0").First(&logistics).Error; err != nil {
return fmt.Errorf("查询物流运费失败: %v", err)
}
cost = int64(logistics.FirPrice * 100)
stock = inventoryDetail.Quantity
}
bodyData := map[string]interface{}{
"book_info": map[string]interface{}{
"isbn": isbn,
"book_name": product.Name,
"image_object": map[string]interface{}{
"carousel_url_array": imgList,
"white_background_url": "",
"detail_url_object": map[string]interface{}{},
"introduction_url": []string{},
"catalogue_url": []string{},
"live_shooting_url": []string{},
"other_url": []string{},
"default_image_url": func() string {
if len(imgList) > 0 {
return imgList[0]
}
return ""
}(),
},
},
"detail": map[string]interface{}{
"stock": stock,
"price": salePrice,
"shipping_cost": cost,
"msg": string(msgJSON),
},
}
bodyDataJSON, err := json.Marshal(bodyData)
if err != nil {
return fmt.Errorf("序列化请求体失败: %v", err)
}
bodyList := []string{
string(bodyDataJSON),
}
taskID := fmt.Sprintf("%d", outTask.OutTaskID)
allBody := strings.Join(bodyList, "")
signParams := map[string]string{
"task_id": taskID,
"body": allBody,
}
sign := utils.SignParams(signParams)
url := config.AppConfig.ExternalAPI.SyncTaskBodyURL
resp, err := utils.SubmitMultiBody(url, taskID, bodyList, sign)
if err != nil {
s.saveOutTaskLogWithStock(outTask, product.ID, isbn, product.LiveImage, stock, salePrice, cost, fmt.Sprintf("请求外部接口失败: %v", err), db)
return fmt.Errorf("请求外部接口失败: %v", err)
}
var resData systemRes.ExternalAPIResponse
if err := json.Unmarshal([]byte(resp), &resData); err != nil {
return fmt.Errorf("解析响应失败: %v", err)
}
if resData.Code != "200" {
s.saveOutTaskLogWithStock(outTask, product.ID, isbn, product.LiveImage, stock, salePrice, cost, fmt.Sprintf("外部接口返回错误: code=%s, msg=%s", resData.Code, resData.Msg), db)
return fmt.Errorf("外部接口返回错误: code=%s, msg=%s", resData.Code, resData.Msg)
}
return nil
}
// saveOutTaskLogWithStock 保存外部任务日志(支持自定义库存)
func (s *ProductService) saveOutTaskLogWithStock(outTask models.OutTask, productID int64, isbn string, liveImage datatypes.JSON, stock, salePrice, cost int64, msg string, db *gorm.DB) {
now := time.Now().Unix()
logRecord := models.OutTaskLog{
ShopID: outTask.ShopID,
OutTaskID: outTask.OutTaskID,
ProductID: productID,
ISBN: isbn,
LiveImage: liveImage,
Stock: stock,
SalePrice: salePrice,
Cost: cost,
Status: 0,
Msg: msg,
CreatedAt: now,
UpdatedAt: now,
IsDel: 0,
}
if err := db.Create(&logRecord).Error; err != nil {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "保存外部任务日志",
"out_task_id": outTask.OutTaskID,
"product_id": productID,
"error": fmt.Sprintf("保存日志失败: %v", err),
})
}
}
// syncPriceToExternal 同步售价到外部接口
func (s *ProductService) syncPriceToExternal(waveTaskDetail models.WaveTaskDetail, productID, salePrice, userId int64, db *gorm.DB) error {
var logistics models.Logistics
err := db.Table("logistics").
Select("logistics.fir_price").
Joins("INNER JOIN warehouse ON warehouse.logistics_id = logistics.id AND warehouse.is_del = ?", "0").
Joins("INNER JOIN wave_header ON wave_header.warehouse_id = warehouse.id AND wave_header.is_del = 0").
Joins("INNER JOIN wave_task ON wave_task.wave_id = wave_header.id AND wave_task.is_del = 0").
Where("wave_task.id = ?", waveTaskDetail.WaveTaskID).
First(&logistics).Error
if err != nil {
return fmt.Errorf("查询物流运费失败: %v", err)
}
cost := int64(logistics.FirPrice * 100)
var product models.Product
if err := db.Where("id = ? AND is_del = 0", productID).First(&product).Error; err != nil {
return fmt.Errorf("查询商品信息失败: %v", err)
}
var outTasks []models.OutTask
if err := db.Where("wave_task_id = ? AND is_del = 0", waveTaskDetail.WaveTaskID).Find(&outTasks).Error; err != nil {
return fmt.Errorf("查询外部任务失败: %v", err)
}
if len(outTasks) == 0 {
return fmt.Errorf("未找到关联的外部任务")
}
for _, outTask := range outTasks {
if outTask.OutTaskID > 0 {
if err := s.syncPriceToExternalTask(outTask, product, waveTaskDetail, salePrice, cost, userId, db); err != nil {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "同步售价到外部接口",
"product_id": productID,
"task_id": outTask.OutTaskID,
"error": fmt.Sprintf("请求失败: %v", err),
})
}
}
}
return nil
}
// syncPriceToExternalTask 同步售价到单个外部任务
func (s *ProductService) syncPriceToExternalTask(outTask models.OutTask, product models.Product, waveTaskDetail models.WaveTaskDetail, salePrice, cost, userId int64, db *gorm.DB) error {
isbn := product.Barcode
var imgList []string
if product.LiveImage != nil && len(product.LiveImage) > 0 {
var rawImgList []string
if err := json.Unmarshal(product.LiveImage, &rawImgList); err != nil {
return fmt.Errorf("解析json失败: %v", err)
}
for _, imgStr := range rawImgList {
imgStr = strings.TrimSpace(imgStr)
if imgStr != "" {
imgList = append(imgList, imgStr)
}
}
}
msgData := map[string]interface{}{
"product_id": product.ID,
"user_id": fmt.Sprintf("%d", userId),
}
msgJSON, _ := json.Marshal(msgData)
bodyData := map[string]interface{}{
"book_info": map[string]interface{}{
"isbn": isbn,
"book_name": product.Name,
"image_object": map[string]interface{}{
"carousel_url_array": imgList,
"white_background_url": "",
"detail_url_object": map[string]interface{}{},
"introduction_url": []string{},
"catalogue_url": []string{},
"live_shooting_url": []string{},
"other_url": []string{},
"default_image_url": func() string {
if len(imgList) > 0 {
return imgList[0]
}
return ""
}(),
},
},
"detail": map[string]interface{}{
"stock": waveTaskDetail.PlannedQuantity,
"price": salePrice,
"shipping_cost": cost,
"msg": string(msgJSON),
},
}
bodyDataJSON, err := json.Marshal(bodyData)
if err != nil {
return fmt.Errorf("序列化请求体失败: %v", err)
}
bodyList := []string{
string(bodyDataJSON),
}
taskID := fmt.Sprintf("%d", outTask.OutTaskID)
allBody := strings.Join(bodyList, "")
signParams := map[string]string{
"task_id": taskID,
"body": allBody,
}
sign := utils.SignParams(signParams)
url := config.AppConfig.ExternalAPI.SyncTaskBodyURL
resp, err := utils.SubmitMultiBody(url, taskID, bodyList, sign)
if err != nil {
s.saveOutTaskLog(outTask, product.ID, isbn, product.LiveImage, waveTaskDetail.PlannedQuantity, salePrice, cost, fmt.Sprintf("请求外部接口失败: %v", err), db)
return fmt.Errorf("请求外部接口失败: %v", err)
}
var resData systemRes.ExternalAPIResponse
if err := json.Unmarshal([]byte(resp), &resData); err != nil {
return fmt.Errorf("解析响应失败: %v", err)
}
if resData.Code != "200" {
s.saveOutTaskLog(outTask, product.ID, isbn, product.LiveImage, waveTaskDetail.PlannedQuantity, salePrice, cost, fmt.Sprintf("外部接口返回错误: code=%s, msg=%s", resData.Code, resData.Msg), db)
return fmt.Errorf("外部接口返回错误: code=%s, msg=%s", resData.Code, resData.Msg)
}
s.saveOutTaskLog(outTask, product.ID, isbn, product.LiveImage, waveTaskDetail.PlannedQuantity, salePrice, cost, "成功", db)
return nil
}
// saveOutTaskLog 保存外部任务日志
func (s *ProductService) saveOutTaskLog(outTask models.OutTask, productID int64, isbn string, liveImage []byte, stock, salePrice, cost int64, msg string, db *gorm.DB) {
now := time.Now().Unix()
logRecord := models.OutTaskLog{
ShopID: outTask.ShopID,
OutTaskID: outTask.OutTaskID,
ProductID: productID,
ISBN: isbn,
LiveImage: liveImage,
Stock: stock,
SalePrice: salePrice,
Cost: cost,
Status: func() int8 {
if msg == "成功" {
return 1
} else {
return 0
}
}(),
Msg: msg,
CreatedAt: now,
UpdatedAt: now,
IsDel: 0,
}
if err := db.Create(&logRecord).Error; err != nil {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "保存外部任务日志",
"out_task_id": outTask.OutTaskID,
"product_id": productID,
"error": fmt.Sprintf("保存日志失败: %v", err),
})
}
}
// ExportProducts 导出商品到Excel
func (s *ProductService) ExportProducts(req systemReq.ExportProductRequest, db ...*gorm.DB) (*systemRes.ExportProductResponse, error) {
databaseConn := database.OptionalDB(db...)
query := databaseConn.Table("product").
Select(`product.barcode,
product.name,
product.sale_price,
product.live_image,
product.created_at,
inventory_detail.location_id,
COALESCE(inventory_detail.quantity, 0) as quantity`).
Joins("LEFT JOIN inventory_detail ON inventory_detail.product_id = product.id AND inventory_detail.is_del = ?", 0).
Where("product.is_del = ?", 0)
if req.StartCreatedAt > 0 {
query = query.Where("product.created_at >= ?", req.StartCreatedAt)
}
if req.EndCreatedAt > 0 {
query = query.Where("product.created_at <= ?", req.EndCreatedAt)
}
if req.MinSalePrice > 0 {
query = query.Where("product.sale_price >= ?", req.MinSalePrice)
}
if req.MaxSalePrice > 0 {
query = query.Where("product.sale_price <= ?", req.MaxSalePrice)
}
if req.WarehouseID > 0 {
query = query.Where("inventory_detail.warehouse_id = ? OR inventory_detail.warehouse_id IS NULL", req.WarehouseID)
}
if req.MinStock >= 0 {
query = query.Where("COALESCE(inventory_detail.quantity, 0) >= ?", req.MinStock)
}
if req.MaxStock >= 0 {
query = query.Where("COALESCE(inventory_detail.quantity, 0) <= ?", req.MaxStock)
}
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, utils.NewError("查询总数失败")
}
if req.Type == 0 {
return &systemRes.ExportProductResponse{
Total: total,
}, nil
}
if total == 0 {
return nil, fmt.Errorf("没有符合条件的商品数据")
}
type ProductExportData struct {
Barcode string `gorm:"column:barcode"`
Name string `gorm:"column:name"`
SalePrice int64 `gorm:"column:sale_price"`
LiveImage datatypes.JSON `gorm:"column:live_image"`
CreatedAt int64 `gorm:"column:created_at"`
LocationID int64 `gorm:"column:location_id"`
Quantity int64 `gorm:"column:quantity"`
}
var products []ProductExportData
if err := query.Order("product.created_at DESC").Find(&products).Error; err != nil {
return nil, utils.NewError("查询商品数据失败")
}
locationIDs := make([]int64, 0)
for _, p := range products {
if p.LocationID > 0 {
locationIDs = append(locationIDs, p.LocationID)
}
}
locationMap := make(map[int64]string)
if len(locationIDs) > 0 {
var locations []models.Location
if err := databaseConn.Where("id IN ? AND is_del = ?", locationIDs, 0).Find(&locations).Error; err == nil {
for _, loc := range locations {
locationMap[loc.ID] = loc.Code
}
}
}
f := excelize.NewFile()
defer func() {
if err := f.Close(); err != nil {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "关闭Excel文件",
"error": fmt.Sprintf("关闭失败: %v", err),
})
}
}()
sheetName := "Sheet1"
f.SetSheetName("Sheet1", sheetName)
headers := []string{"ISBN", "", "", "商品名称", "库存数量", "创建时间", "价格", "规格编码/货号", "", "商品图片"}
for i, header := range headers {
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
f.SetCellValue(sheetName, cell, header)
}
headerStyle, _ := f.NewStyle(&excelize.Style{
Font: &excelize.Font{
Bold: true,
Size: 12,
},
Alignment: &excelize.Alignment{
Horizontal: "center",
Vertical: "center",
},
Fill: excelize.Fill{
Type: "pattern",
Color: []string{"#E0E0E0"},
Pattern: 1,
},
})
f.SetCellStyle(sheetName, "A1", "J1", headerStyle)
for idx, product := range products {
row := idx + 2
f.SetCellValue(sheetName, fmt.Sprintf("A%d", row), product.Barcode)
f.SetCellValue(sheetName, fmt.Sprintf("D%d", row), product.Name)
f.SetCellValue(sheetName, fmt.Sprintf("E%d", row), product.Quantity)
createdAtStr := time.Unix(product.CreatedAt, 0).Format("2006-01-02 15:04:05")
f.SetCellValue(sheetName, fmt.Sprintf("F%d", row), createdAtStr)
f.SetCellValue(sheetName, fmt.Sprintf("G%d", row), float64(product.SalePrice)/100.0)
locationCode := ""
if product.LocationID > 0 {
if code, exists := locationMap[product.LocationID]; exists {
locationCode = code
}
}
f.SetCellValue(sheetName, fmt.Sprintf("H%d", row), locationCode)
var imgList []string
if product.LiveImage != nil && len(product.LiveImage) > 0 {
if err := json.Unmarshal(product.LiveImage, &imgList); err == nil && len(imgList) > 0 {
f.SetCellValue(sheetName, fmt.Sprintf("J%d", row), imgList[0])
}
}
}
colWidths := map[string]float64{
"A": 20,
"D": 30,
"E": 12,
"F": 20,
"G": 12,
"H": 15,
"J": 40,
}
for col, width := range colWidths {
f.SetColWidth(sheetName, col, col, width)
}
now := time.Now()
fileName := fmt.Sprintf("task_template_product_%s.xlsx", now.Format("20060102150405"))
filePath := fmt.Sprintf("excel/%s", fileName)
if err := f.SaveAs(filePath); err != nil {
return nil, fmt.Errorf("保存Excel文件失败: %v", err)
}
return &systemRes.ExportProductResponse{
Total: total,
FileName: fileName,
FilePath: config.AppConfig.Server.Host + filePath,
}, nil
}
// GetProductLogList 导出商品
func (s *ProductService) GetProductLogList(req systemReq.GetProductLogListRequest, aboutID int64) (*systemRes.ProductLogListResponse, error) {
// TODO
//if aboutID != 0 {
// return nil, utils.NewError("只有系统管理员才可以访问")
//}
if req.Page < 1 {
req.Page = 1
}
if req.PageSize < 1 || req.PageSize > 100 {
req.PageSize = 20
}
query := database.DB.Model(&models.ProductLog{}).Where("is_del = ?", 0)
if req.Barcode != "" {
query = query.Where("barcode = ?", req.Barcode)
}
if req.Status != nil {
query = query.Where("status = ?", *req.Status)
}
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, utils.NewError("查询总数失败")
}
var logs []models.ProductLog
offset := (req.Page - 1) * req.PageSize
if err := query.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&logs).Error; err != nil {
return nil, utils.NewError("查询商品日志列表失败")
}
var logItems []systemRes.ProductLogItem
for _, log := range logs {
item := s.convertProductLogToItem(log)
logItems = append(logItems, item)
}
return &systemRes.ProductLogListResponse{
List: logItems,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
}, nil
}
func (s *ProductService) SaveProductLog(req systemReq.ProductLogRequest, id int64) (int64, error) {
now := time.Now().Unix()
if req.ID > 0 {
return s.updateProductLog(req, now)
}
return s.createProductLog(req, now, id)
}
func (s *ProductService) createProductLog(req systemReq.ProductLogRequest, now, userId int64) (int64, error) {
var OldliveImage datatypes.JSON
if len(req.OldLiveImage) > 0 {
jsonBytes, _ := json.Marshal(req.OldLiveImage)
OldliveImage = jsonBytes
} else {
OldliveImage = datatypes.JSON("[]")
}
var NewliveImage datatypes.JSON
if len(req.NewLiveImage) > 0 {
jsonBytes, _ := json.Marshal(req.NewLiveImage)
NewliveImage = jsonBytes
} else {
NewliveImage = datatypes.JSON("[]")
}
if req.OldName == req.NewName &&
req.OldPublisher == req.NewPublisher &&
req.OldAuthor == req.NewAuthor &&
req.OldPublicationTime == req.NewPublicationTime &&
req.OldPrice == req.NewPrice &&
req.OldBindingLayout == req.NewBindingLayout &&
req.OldPageCount == req.NewPageCount &&
req.OldWordCount == req.NewWordCount &&
string(OldliveImage) == string(NewliveImage) &&
req.OldIsSuit == req.NewIsSuit {
return 0, fmt.Errorf("新旧数据完全一致,无需提交变更")
}
md5Str := fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%s|%s|%s|%d|%d|%d|%s|%d|%d|%d",
req.Barcode,
req.NewName,
req.NewPublisher,
req.NewAuthor,
req.NewPublicationTime,
req.NewPrice,
req.NewBindingLayout,
req.NewPageCount,
req.NewWordCount,
string(NewliveImage),
))))
var existingLog models.ProductLog
if err := database.DB.Where("md5_data = ? AND is_del = 0", md5Str).First(&existingLog).Error; err == nil {
return 0, fmt.Errorf("该商品变更日志已存在,无需重复提交")
} else if err != gorm.ErrRecordNotFound {
return 0, fmt.Errorf("查询商品日志失败:%w", err)
}
product := models.ProductLog{
Barcode: req.Barcode,
Md5Data: md5Str,
OldName: req.OldName,
NewName: req.NewName,
OldPublisher: req.OldPublisher,
NewPublisher: req.NewPublisher,
OldAuthor: req.OldAuthor,
NewAuthor: req.NewAuthor,
OldPublicationTime: req.OldPublicationTime,
NewPublicationTime: req.NewPublicationTime,
OldPrice: req.OldPrice,
NewPrice: req.NewPrice,
OldBindingLayout: req.OldBindingLayout,
NewBindingLayout: req.NewBindingLayout,
OldPageCount: req.OldPageCount,
NewPageCount: req.NewPageCount,
OldWordCount: req.OldWordCount,
NewWordCount: req.NewWordCount,
OldLiveImage: OldliveImage,
NewLiveImage: NewliveImage,
OldIsSuit: req.OldIsSuit,
NewIsSuit: req.NewIsSuit,
Status: 0,
CreateBy: userId,
CreatedAt: now,
UpdatedAt: now,
}
if err := database.DB.Create(&product).Error; err != nil {
return 0, fmt.Errorf("创建商品日志失败:%w", err)
}
return product.ID, nil
}
// 更新商品日志
func (s *ProductService) updateProductLog(req systemReq.ProductLogRequest, now int64) (int64, error) {
var productLog models.ProductLog
if err := database.DB.Where("id = ? AND is_del = 0", req.ID).First(&productLog).Error; err != nil {
return 0, fmt.Errorf("商品日志不存在:%w", err)
}
if productLog.Status != 0 {
return 0, fmt.Errorf("商品日志已审核,无法修改")
}
var OldliveImage datatypes.JSON
if len(req.OldLiveImage) > 0 {
jsonBytes, _ := json.Marshal(req.OldLiveImage)
OldliveImage = jsonBytes
} else {
OldliveImage = datatypes.JSON("[]")
}
var NewliveImage datatypes.JSON
if len(req.NewLiveImage) > 0 {
jsonBytes, _ := json.Marshal(req.NewLiveImage)
NewliveImage = jsonBytes
} else {
NewliveImage = datatypes.JSON("[]")
}
md5Str := fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%s|%s|%s|%d|%d|%d|%s|%d|%d|%d",
req.Barcode,
req.NewName,
req.NewPublisher,
req.NewAuthor,
req.NewPublicationTime,
req.NewPrice,
req.NewBindingLayout,
req.NewPageCount,
req.NewWordCount,
string(NewliveImage),
))))
var existingLog models.ProductLog
if err := database.DB.Where("md5_data = ? AND is_del = 0 AND id != ?", md5Str, req.ID).First(&existingLog).Error; err == nil {
return 0, fmt.Errorf("该商品变更日志已存在,无需重复提交")
} else if err != gorm.ErrRecordNotFound {
return 0, fmt.Errorf("查询商品日志失败:%w", err)
}
if req.OldName == req.NewName &&
req.OldPublisher == req.NewPublisher &&
req.OldAuthor == req.NewAuthor &&
req.OldPublicationTime == req.NewPublicationTime &&
req.OldPrice == req.NewPrice &&
req.OldBindingLayout == req.NewBindingLayout &&
req.OldPageCount == req.NewPageCount &&
req.OldWordCount == req.NewWordCount &&
string(OldliveImage) == string(NewliveImage) &&
req.OldIsSuit == req.NewIsSuit {
return 0, fmt.Errorf("新旧数据完全一致,无需提交变更")
}
updates := map[string]interface{}{
"barcode": req.Barcode,
"md5_data": md5Str,
"old_name": req.OldName,
"new_name": req.NewName,
"old_publisher": req.OldPublisher,
"new_publisher": req.NewPublisher,
"old_author": req.OldAuthor,
"new_author": req.NewAuthor,
"old_publication_time": req.OldPublicationTime,
"new_publication_time": req.NewPublicationTime,
"old_price": req.OldPrice,
"new_price": req.NewPrice,
"old_binding_layout": req.OldBindingLayout,
"new_binding_layout": req.NewBindingLayout,
"old_page_count": req.OldPageCount,
"new_page_count": req.NewPageCount,
"old_word_count": req.OldWordCount,
"new_word_count": req.NewWordCount,
"old_live_image": OldliveImage,
"new_live_image": NewliveImage,
"old_is_suit": req.OldIsSuit,
"new_is_suit": req.NewIsSuit,
"updated_at": now,
}
if err := database.DB.Model(&productLog).Updates(updates).Error; err != nil {
return 0, fmt.Errorf("更新商品日志失败:%w", err)
}
return productLog.ID, nil
}
// 审核商品日志
func (s *ProductService) AuditProductLog(req systemReq.AuditProductLogRequest, userID int64) error {
var log models.ProductLog
if err := database.DB.Where("id = ? AND is_del = 0", req.ID).First(&log).Error; err != nil {
return fmt.Errorf("商品日志不存在:%w", err)
}
if log.Status != 0 {
return fmt.Errorf("该日志已审核,无法重复审核")
}
var NewliveImage datatypes.JSON
if len(req.NewLiveImage) > 0 {
jsonBytes, _ := json.Marshal(req.NewLiveImage)
NewliveImage = jsonBytes
} else {
NewliveImage = datatypes.JSON("[]")
}
now := time.Now().Unix()
updates := map[string]interface{}{
"new_name": req.NewName,
"new_publisher": req.NewPublisher,
"new_author": req.NewAuthor,
"new_publication_time": req.NewPublicationTime,
"new_price": req.NewPrice,
"new_binding_layout": req.NewBindingLayout,
"new_page_count": req.NewPageCount,
"new_word_count": req.NewWordCount,
"new_live_image": NewliveImage,
"new_is_suit": req.NewIsSuit,
"status": req.Status,
"audit_by": userID,
"updated_at": now,
}
if err := database.DB.Model(&log).Updates(updates).Error; err != nil {
return fmt.Errorf("审核商品日志失败:%w", err)
}
// 审核通过时,同步更新 ES 中的商品字段
if req.Status == 1 {
if err := s.syncApprovedLogToES(log.Barcode, req); err != nil {
// ES 同步失败只记日志,不阻断审核流程
fmt.Printf("[WARN] 审核后同步 ES 失败 (product_log_id=%d, barcode=%s): %v\r\n", req.ID, log.Barcode, err)
}
}
return nil
}
// syncApprovedLogToES 审核通过后将 new_* 字段同步到 ES 商品数据
func (s *ProductService) syncApprovedLogToES(barcode string, req systemReq.AuditProductLogRequest) error {
esURL := config.AppConfig.ExternalAPI.ESUpdateBookURL
if esURL == "" {
return fmt.Errorf("ES 更新接口地址未配置")
}
// 构建 data 字段(只传有值的字段)
data := make(map[string]interface{})
if req.NewName != "" {
data["book_name"] = req.NewName
}
if req.NewAuthor != "" {
data["author"] = req.NewAuthor
}
if req.NewPublisher != "" {
data["publisher"] = req.NewPublisher
}
if req.NewPrice > 0 {
data["fix_price"] = req.NewPrice
}
if req.NewBindingLayout != "" {
data["binding_layout"] = req.NewBindingLayout
}
if req.NewPublicationTime > 0 {
data["publication_time"] = req.NewPublicationTime
}
data["is_suit"] = req.NewIsSuit
payload := map[string]interface{}{
"isbn": barcode,
"data": data,
}
jsonBytes, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("序列化请求参数失败:%w", err)
}
resp, err := http.Post(esURL, "application/json", bytes.NewReader(jsonBytes))
if err != nil {
return fmt.Errorf("调用 ES 更新接口失败:%w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("ES 更新接口返回非200状态码[%d]: %s", resp.StatusCode, string(body))
}
return nil
}
// 删除商品日志
func (s *ProductService) DeleteProductLog(req systemReq.DeleteProductLogRequest) error {
var log models.ProductLog
if err := database.DB.Where("id = ? AND is_del = 0", req.ID).First(&log).Error; err != nil {
return fmt.Errorf("商品日志不存在:%w", err)
}
now := time.Now().Unix()
if err := database.DB.Model(&log).Updates(map[string]interface{}{
"is_del": 1,
"updated_at": now,
}).Error; err != nil {
return fmt.Errorf("删除商品日志失败:%w", err)
}
return nil
}
// GetShopProductDetail 获取店铺商品详情(包含商品数据和发送记录)
func (s *ProductService) GetShopProductDetail(req systemReq.GetShopProductDetailRequest, db ...*gorm.DB) (*systemRes.ShopProductDetailResponse, error) {
databaseConn := database.OptionalDB(db...)
if req.Page < 1 {
req.Page = 1
}
if req.PageSize < 1 || req.PageSize > 100 {
req.PageSize = 20
}
// 查询店铺信息
var shop models.Shop
if err := databaseConn.Where("id = ? AND del_flag = ?", req.ShopID, 0).First(&shop).Error; err != nil {
return nil, utils.NewError("店铺不存在")
}
// 构建响应
response := &systemRes.ShopProductDetailResponse{
ID: shop.ID,
ShopName: shop.ShopAliasName, // 使用别名作为店铺名称
ShopAliasName: shop.ShopAliasName,
ShopType: shop.ShopType,
ShopTypeName: s.getShopTypeName(shop.ShopType),
CreatedAt: shop.CreateTime,
UpdatedAt: shop.UpdateTime,
Products: []systemRes.ProductInShop{},
Page: req.Page,
PageSize: req.PageSize,
}
// 查询该店铺下的商品信息及发送记录
type ProductShopInfo struct {
ProductID int64 `gorm:"column:product_id"`
ProductName string `gorm:"column:product_name"`
ProductBarcode string `gorm:"column:product_barcode"`
LiveImage datatypes.JSON `gorm:"column:live_image"`
Price int64 `gorm:"column:price"`
SalePrice int64 `gorm:"column:sale_price"`
Quantity int64 `gorm:"column:quantity"`
WarehouseName string `gorm:"column:warehouse_name"`
LocationCode string `gorm:"column:location_code"`
IsBatchManaged int8 `gorm:"column:is_batch_managed"`
IsShelfLifeManaged int8 `gorm:"column:is_shelf_life_managed"`
OutTaskLogID int64 `gorm:"column:out_task_log_id"`
LogStatus int8 `gorm:"column:log_status"`
LogMsg string `gorm:"column:log_msg"`
WaveNo string `gorm:"column:wave_no"`
WaveTaskNo string `gorm:"column:wave_task_no"`
OutTaskNo int64 `gorm:"column:out_task_no"`
SalesOrderNo string `gorm:"column:sales_order_no"`
ShippingNo string `gorm:"column:shipping_no"`
ProductCreatedAt int64 `gorm:"column:product_created_at"`
ProductUpdatedAt int64 `gorm:"column:product_updated_at"`
}
var productInfos []ProductShopInfo
// 通过 out_task 和 wave_task_detail 关联查询商品
baseQuery := databaseConn.Table("out_task ot").
Select(`p.id as product_id,
p.name as product_name,
p.barcode as product_barcode,
p.live_image,
p.price,
p.sale_price,
COALESCE(inv.quantity, 0) as quantity,
w.name as warehouse_name,
l.code as location_code,
p.is_batch_managed,
p.is_shelf_life_managed,
COALESCE(otl.id, 0) as out_task_log_id,
COALESCE(otl.status, 0) as log_status,
COALESCE(otl.msg, '') as log_msg,
COALESCE(wh.wave_no, '') as wave_no,
COALESCE(wt.task_no, '') as wave_task_no,
ot.out_task_id as out_task_no,
COALESCE(so.so_no, '') as sales_order_no,
COALESCE(sh.shipping_no, '') as shipping_no,
p.created_at as product_created_at,
p.updated_at as product_updated_at`).
Joins("INNER JOIN wave_task wt ON wt.id = ot.wave_task_id AND wt.is_del = 0").
Joins("INNER JOIN wave_task_detail wtd ON wtd.wave_task_id = wt.id AND wtd.is_del = 0").
Joins("INNER JOIN product p ON p.id = wtd.product_id AND p.is_del = 0").
Joins("LEFT JOIN wave_header wh ON wh.id = wt.wave_id AND wh.is_del = 0").
Joins("LEFT JOIN sales_order so ON so.id = wh.related_order_id AND so.is_del = 0").
Joins("LEFT JOIN outbound_order obo ON obo.wave_task_id = wt.id AND obo.is_del = 0").
Joins("LEFT JOIN outbound_order_item oboi ON oboi.out_order_id = obo.id AND oboi.product_id = p.id AND oboi.is_del = 0").
Joins("LEFT JOIN shipping_order_item soi ON soi.outbound_order_item_id = oboi.id AND soi.is_del = 0").
Joins("LEFT JOIN shipping_order sh ON sh.id = soi.shipping_order_id AND sh.is_del = 0").
Joins("LEFT JOIN inventory inv ON inv.product_id = p.id AND inv.is_del = 0").
Joins("LEFT JOIN inventory_detail idetail ON idetail.product_id = p.id AND idetail.is_del = 0").
Joins("LEFT JOIN warehouse w ON w.id = inv.warehouse_id").
Joins("LEFT JOIN location l ON l.id = idetail.location_id").
Joins("LEFT JOIN out_task_log otl ON otl.out_task_id = ot.out_task_id AND otl.product_id = p.id AND otl.is_del = 0 AND otl.id = (SELECT MAX(otl2.id) FROM out_task_log otl2 WHERE otl2.out_task_id = ot.out_task_id AND otl2.product_id = p.id AND otl2.is_del = 0)").
Where("ot.shop_id = ? AND ot.is_del = 0", req.ShopID).
Group("p.id, p.name, p.barcode, p.live_image, p.price, p.sale_price, inv.quantity, w.name, l.code, p.is_batch_managed, p.is_shelf_life_managed, out_task_log_id, log_status, log_msg, wh.wave_no, wt.task_no, ot.out_task_id, so.so_no, sh.shipping_no, p.created_at, p.updated_at")
// 查询总数
var total int64
if err := databaseConn.Raw("SELECT COUNT(*) FROM (?) AS sub", baseQuery).Scan(&total).Error; err != nil {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "查询店铺商品总数",
"error": fmt.Sprintf("查询失败: %v", err),
})
return nil, utils.NewError("查询店铺商品总数失败")
}
response.Total = total
// 查询各状态数量(基于全量商品,不受分页影响)
type StatusCount struct {
StatusType string `gorm:"column:status_type"`
Cnt int64 `gorm:"column:cnt"`
}
var statusCounts []StatusCount
if err := databaseConn.Table("(?) AS sub", baseQuery).
Select(`CASE
WHEN COALESCE(sub.out_task_log_id, 0) > 0 AND COALESCE(sub.log_status, 0) = 2 THEN 'success'
WHEN COALESCE(sub.out_task_log_id, 0) = 0 THEN 'not_sent'
ELSE 'failed'
END as status_type, COUNT(*) as cnt`).
Group("status_type").
Scan(&statusCounts).Error; err != nil {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "查询店铺商品状态统计",
"error": fmt.Sprintf("查询失败: %v", err),
})
} else {
for _, sc := range statusCounts {
switch sc.StatusType {
case "success":
response.SuccessCount = sc.Cnt
case "not_sent":
response.NotSentCount = sc.Cnt
case "failed":
response.FailedCount = sc.Cnt
}
}
}
// 按状态筛选(不影响上述的全局统计计数)
if req.Status > 0 {
switch req.Status {
case 1: // 成功
baseQuery = baseQuery.Having("out_task_log_id > 0 AND log_status = 2")
case 2: // 任务已创建未发送到店铺
baseQuery = baseQuery.Having("out_task_log_id = 0")
case 3: // 发送失败
baseQuery = baseQuery.Having("out_task_log_id > 0 AND log_status != 2")
}
// 按筛选条件重新计算 total
var filteredTotal int64
if err := databaseConn.Raw("SELECT COUNT(*) FROM (?) AS sub", baseQuery).Scan(&filteredTotal).Error; err == nil {
response.Total = filteredTotal
}
}
// 分页查询
offset := (req.Page - 1) * req.PageSize
err := baseQuery.
Order("p.created_at DESC").
Offset(offset).
Limit(req.PageSize).
Scan(&productInfos).Error
if err != nil {
utils.ErrorLog(constant.LoggerChannelWork, map[string]interface{}{
"source": "查询店铺商品详情",
"error": fmt.Sprintf("查询失败: %v", err),
})
return nil, utils.NewError("查询店铺商品详情失败")
}
// 转换为响应格式
for _, info := range productInfos {
var liveImage []string
if len(info.LiveImage) > 0 {
json.Unmarshal(info.LiveImage, &liveImage)
}
// 根据日志状态判断在店铺中的状态和消息
statusInShop := int8(0)
msg := "任务已创建未发送到店铺"
if info.OutTaskLogID > 0 {
if info.LogStatus == 2 {
statusInShop = 1
msg = "发布成功"
} else if info.LogStatus == 0 {
statusInShop = 0
msg = info.LogMsg
if msg == "" {
msg = "发送到店铺失败"
}
} else if info.LogStatus == 1 {
statusInShop = 0
msg = "推送成功,待发布"
}
}
product := systemRes.ProductInShop{
ID: info.ProductID,
Name: info.ProductName,
Barcode: info.ProductBarcode,
LiveImage: liveImage,
Price: info.Price,
SalePrice: info.SalePrice,
Quantity: info.Quantity,
WarehouseName: info.WarehouseName,
LocationCode: info.LocationCode,
IsBatchManaged: info.IsBatchManaged,
IsShelfLifeManaged: info.IsShelfLifeManaged,
OutTaskLogID: info.OutTaskLogID,
StatusInShop: statusInShop,
Msg: msg,
WaveNo: info.WaveNo,
WaveTaskNo: info.WaveTaskNo,
OutTaskNo: info.OutTaskNo,
SalesOrderNo: info.SalesOrderNo,
ShippingNo: info.ShippingNo,
CreatedAt: info.ProductCreatedAt,
UpdatedAt: info.ProductUpdatedAt,
}
response.Products = append(response.Products, product)
}
return response, nil
}
func (s *ProductService) convertProductLogToItem(log models.ProductLog) systemRes.ProductLogItem {
var oldLiveImage []string
var newLiveImage []string
if len(log.OldLiveImage) > 0 {
err := json.Unmarshal(log.OldLiveImage, &oldLiveImage)
if err != nil {
return systemRes.ProductLogItem{}
}
}
if len(log.NewLiveImage) > 0 {
err := json.Unmarshal(log.NewLiveImage, &newLiveImage)
if err != nil {
return systemRes.ProductLogItem{}
}
}
return systemRes.ProductLogItem{
ID: log.ID,
Barcode: log.Barcode,
Md5Data: log.Md5Data,
OldName: log.OldName,
NewName: log.NewName,
OldPublisher: log.OldPublisher,
NewPublisher: log.NewPublisher,
OldAuthor: log.OldAuthor,
NewAuthor: log.NewAuthor,
OldPublicationTime: log.OldPublicationTime,
NewPublicationTime: log.NewPublicationTime,
OldPrice: log.OldPrice,
NewPrice: log.NewPrice,
OldBindingLayout: log.OldBindingLayout,
NewBindingLayout: log.NewBindingLayout,
OldPageCount: log.OldPageCount,
NewPageCount: log.NewPageCount,
OldWordCount: log.OldWordCount,
NewWordCount: log.NewWordCount,
OldLiveImage: oldLiveImage,
NewLiveImage: newLiveImage,
OldIsSuit: log.OldIsSuit,
NewIsSuit: log.NewIsSuit,
Status: log.Status,
CreateBy: log.CreateBy,
AuditBy: log.AuditBy,
CreatedAt: log.CreatedAt,
UpdatedAt: log.UpdatedAt,
}
}
func (s *ProductService) getShopTypeName(shopType int8) string {
switch shopType {
case 1:
return "拼多多"
case 2:
return "孔夫子"
case 5:
return "闲鱼"
default:
return "未知"
}
}
// 在 service 中使用
/*func (s *ProcessService) asyncWriteToMainDB(aboutID int64, data interface{}) {
go func() {
mainDB := database.DB
// 写入主库逻辑
mainDB.Create(data)
}()
}*/
// 追加到 service/product.go 末尾
// DestroyProduct 销毁商品(写入快照日志 + 逻辑删除商品)
func (s *ProductService) DestroyProduct(req systemReq.DestroyProductRequest, operator string, operatorID int64, db ...*gorm.DB) (int64, error) {
databaseConn := database.OptionalDB(db...)
var product models.Product
if err := databaseConn.Where("id = ? AND is_del = ?", req.ProductID, 0).First(&product).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, fmt.Errorf("商品不存在或已被销毁")
}
return 0, fmt.Errorf("查询商品失败: %v", err)
}
now := time.Now().Unix()
productSnapshot, _ := json.Marshal(product)
var productBookSnapshot datatypes.JSON
var book models.ProductBook
if product.Barcode != "" {
tableName := models.ProductBookTableName(product.Barcode)
err := databaseConn.Table(tableName).Where("barcode = ? AND is_del = ?", product.Barcode, 0).First(&book).Error
if err == nil {
productBookSnapshot, _ = json.Marshal(book)
}
}
var inventorySnapshot datatypes.JSON
var inventory models.Inventory
inventoryIDs := make([]int64, 0)
if err := databaseConn.Model(&models.Inventory{}).
Where("product_id = ? AND is_del = ?", req.ProductID, 0).Find(&inventory).Error; err == nil && inventory.ID > 0 {
inventoryIDs = append(inventoryIDs, inventory.ID)
inventorySnapshot, _ = json.Marshal(inventory)
}
var inventoryDetailSnapshot datatypes.JSON
var inventoryDetails []models.InventoryDetail
databaseConn.Where("product_id = ? AND is_del = ?", req.ProductID, 0).Find(&inventoryDetails)
if len(inventoryDetails) > 0 {
inventoryDetailSnapshot, _ = json.Marshal(inventoryDetails)
}
destroyLog := models.ProductDestroyLog{
ProductID: req.ProductID,
Barcode: product.Barcode,
ProductSnapshot: productSnapshot,
ProductBookSnapshot: productBookSnapshot,
InventorySnapshot: inventorySnapshot,
InventoryDetailSnapshot: inventoryDetailSnapshot,
DestroyedBy: operator,
DestroyedByID: operatorID,
DestroyedAt: now,
Status: 0,
CreatedAt: now,
UpdatedAt: now,
IsDel: 0,
}
err := executeInTransactionWithDB(databaseConn, func(tx *gorm.DB) error {
if err := tx.Create(&destroyLog).Error; err != nil {
return fmt.Errorf("写入销毁日志失败: %v", err)
}
if err := tx.Model(&models.Product{}).
Where("id = ? AND is_del = ?", req.ProductID, 0).
Updates(map[string]interface{}{
"is_del": 1,
"updated_at": now,
}).Error; err != nil {
return fmt.Errorf("删除商品失败: %v", err)
}
if err := tx.Model(&models.Inventory{}).
Where("product_id = ? AND is_del = ?", req.ProductID, 0).
Updates(map[string]interface{}{
"is_del": 1,
"updated_at": now,
}).Error; err != nil {
return fmt.Errorf("删除库存汇总失败: %v", err)
}
if err := tx.Model(&models.InventoryDetail{}).
Where("product_id = ? AND is_del = ?", req.ProductID, 0).
Updates(map[string]interface{}{
"is_del": 1,
"updated_at": now,
}).Error; err != nil {
return fmt.Errorf("删除库存明细失败: %v", err)
}
if product.Barcode != "" && len(productBookSnapshot) > 0 {
tableName := models.ProductBookTableName(product.Barcode)
if err := tx.Table(tableName).
Where("barcode = ? AND is_del = ?", product.Barcode, 0).
Updates(map[string]interface{}{
"is_del": 1,
"updated_at": now,
}).Error; err != nil {
return fmt.Errorf("删除书籍反射失败: %v", err)
}
}
for _, _ = range inventoryIDs {
tx.Create(&models.InventoryLog{
WarehouseID: inventory.WarehouseID,
LocationID: 0,
ProductID: req.ProductID,
BatchNo: inventory.BatchNo,
ChangeType: constant.InventoryChangeAdjustment,
ChangeQuantity: -inventory.Quantity,
BeforeQuantity: inventory.Quantity,
AfterQuantity: 0,
RelatedOrderType: "",
RelatedOrderNo: "",
Operator: operator,
OperatorID: operatorID,
Remark: "商品销毁",
CreatedAt: now,
IsDel: 0,
})
}
return nil
})
if err != nil {
return 0, err
}
return destroyLog.ID, nil
}
// RestoreProduct 还原商品(从销毁日志中恢复)
func (s *ProductService) RestoreProduct(req systemReq.RestoreProductRequest, operator string, operatorID int64, db ...*gorm.DB) error {
databaseConn := database.OptionalDB(db...)
var destroyLog models.ProductDestroyLog
if err := databaseConn.Where("id = ? AND is_del = ?", req.DestroyLogID, 0).First(&destroyLog).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("销毁日志不存在")
}
return fmt.Errorf("查询销毁日志失败: %v", err)
}
if destroyLog.Status == 1 {
return fmt.Errorf("该商品已还原,请勿重复操作")
}
var product models.Product
if err := json.Unmarshal(destroyLog.ProductSnapshot, &product); err != nil {
return fmt.Errorf("解析商品快照失败: %v", err)
}
product.IsDel = 0
product.UpdatedAt = time.Now().Unix()
err := executeInTransactionWithDB(databaseConn, func(tx *gorm.DB) error {
var existing models.Product
checkErr := tx.Where("barcode = ? AND is_del = ?", product.Barcode, 0).First(&existing).Error
if checkErr == nil && existing.ID != product.ID {
return fmt.Errorf("条码[%s]已存在其他商品(ID=%d),无法还原", product.Barcode, existing.ID)
}
var oldProduct models.Product
if err := tx.Where("id = ?", product.ID).First(&oldProduct).Error; err == nil {
if err := tx.Model(&models.Product{}).Where("id = ?", product.ID).Updates(map[string]interface{}{
"name": product.Name,
"appearance": product.Appearance,
"barcode": product.Barcode,
"price": product.Price,
"sale_price": product.SalePrice,
"cost": product.Cost,
"live_image": product.LiveImage,
"warehouse_id": product.WarehouseID,
"warehouse_name": product.WarehouseName,
"location_id": product.LocationID,
"location_name": product.LocationName,
"category_id": product.CategoryID,
"standard_product_id": product.StandardProductID,
"status": product.Status,
"is_del": 0,
"updated_at": product.UpdatedAt,
}).Error; err != nil {
return fmt.Errorf("恢复商品失败: %v", err)
}
} else if errors.Is(err, gorm.ErrRecordNotFound) {
product.CreatedAt = time.Now().Unix()
product.UpdatedAt = product.CreatedAt
if err := tx.Create(&product).Error; err != nil {
return fmt.Errorf("创建商品失败: %v", err)
}
} else {
return fmt.Errorf("查询商品失败: %v", err)
}
if len(destroyLog.ProductBookSnapshot) > 0 {
var book models.ProductBook
if err := json.Unmarshal(destroyLog.ProductBookSnapshot, &book); err == nil {
book.IsDel = 0
book.UpdatedAt = time.Now().Unix()
tableName := models.ProductBookTableName(book.Barcode)
var oldBook models.ProductBook
if err := tx.Table(tableName).Where("barcode = ?", book.Barcode).First(&oldBook).Error; err == nil {
tx.Table(tableName).Where("barcode = ?", book.Barcode).Updates(map[string]interface{}{
"is_del": 0,
"updated_at": book.UpdatedAt,
})
} else if errors.Is(err, gorm.ErrRecordNotFound) {
book.CreatedAt = time.Now().Unix()
book.UpdatedAt = book.CreatedAt
tx.Table(tableName).Create(&book)
}
}
}
if len(destroyLog.InventorySnapshot) > 0 {
var inventory models.Inventory
if err := json.Unmarshal(destroyLog.InventorySnapshot, &inventory); err == nil {
inventory.IsDel = 0
inventory.UpdatedAt = time.Now().Unix()
var oldInv models.Inventory
if err := tx.Where("product_id = ? AND warehouse_id = ?", inventory.ProductID, inventory.WarehouseID).First(&oldInv).Error; err == nil {
tx.Model(&oldInv).Updates(map[string]interface{}{
"is_del": 0,
"quantity": inventory.Quantity,
"updated_at": inventory.UpdatedAt,
})
} else if errors.Is(err, gorm.ErrRecordNotFound) {
inventory.CreatedAt = time.Now().Unix()
inventory.UpdatedAt = inventory.CreatedAt
tx.Create(&inventory)
}
tx.Create(&models.InventoryLog{
WarehouseID: inventory.WarehouseID,
ProductID: inventory.ProductID,
BatchNo: inventory.BatchNo,
ChangeType: constant.InventoryChangeAdjustment,
ChangeQuantity: inventory.Quantity,
BeforeQuantity: 0,
AfterQuantity: inventory.Quantity,
RelatedOrderType: "",
RelatedOrderNo: "",
Operator: operator,
OperatorID: operatorID,
Remark: "商品还原",
CreatedAt: time.Now().Unix(),
IsDel: 0,
})
}
}
if len(destroyLog.InventoryDetailSnapshot) > 0 {
var details []models.InventoryDetail
if err := json.Unmarshal(destroyLog.InventoryDetailSnapshot, &details); err == nil {
for _, d := range details {
d.IsDel = 0
d.UpdatedAt = time.Now().Unix()
var oldDetail models.InventoryDetail
if err := tx.Where("product_id = ? AND location_id = ? AND warehouse_id = ?", d.ProductID, d.LocationID, d.WarehouseID).First(&oldDetail).Error; err == nil {
tx.Model(&oldDetail).Updates(map[string]interface{}{
"is_del": 0,
"quantity": d.Quantity,
"updated_at": d.UpdatedAt,
})
} else if errors.Is(err, gorm.ErrRecordNotFound) {
d.CreatedAt = time.Now().Unix()
d.UpdatedAt = d.CreatedAt
tx.Create(&d)
}
}
}
}
now := time.Now().Unix()
tx.Model(&destroyLog).Updates(map[string]interface{}{
"status": 1,
"restored_by": operator,
"restored_by_id": operatorID,
"restored_at": now,
"updated_at": now,
})
return nil
})
return err
}
// GetDestroyLogList 获取商品销毁日志列表
func (s *ProductService) GetDestroyLogList(req systemReq.GetDestroyLogListRequest, db ...*gorm.DB) (*systemRes.DestroyLogListResponse, error) {
databaseConn := database.OptionalDB(db...)
if req.Page < 1 {
req.Page = 1
}
if req.PageSize < 1 || req.PageSize > 100 {
req.PageSize = 20
}
query := databaseConn.Model(&models.ProductDestroyLog{}).Where("is_del = ?", 0)
if req.Keyword != "" {
query = query.Where("barcode LIKE ?", "%"+req.Keyword+"%")
}
if req.Status != nil {
query = query.Where("status = ?", *req.Status)
}
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, utils.NewError("查询总数失败")
}
if total == 0 {
return &systemRes.DestroyLogListResponse{
List: []systemRes.DestroyLogItem{},
Total: 0,
Page: req.Page,
PageSize: req.PageSize,
}, nil
}
var logs []models.ProductDestroyLog
offset := (req.Page - 1) * req.PageSize
if err := query.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&logs).Error; err != nil {
return nil, utils.NewError("查询销毁日志列表失败")
}
items := make([]systemRes.DestroyLogItem, 0, len(logs))
for _, log := range logs {
productName := ""
if len(log.ProductSnapshot) > 0 {
var p models.Product
if err := json.Unmarshal(log.ProductSnapshot, &p); err == nil {
productName = p.Name
}
}
items = append(items, systemRes.DestroyLogItem{
ID: log.ID,
ProductID: log.ProductID,
Barcode: log.Barcode,
ProductName: productName,
DestroyedBy: log.DestroyedBy,
DestroyedAt: log.DestroyedAt,
RestoredBy: log.RestoredBy,
RestoredAt: log.RestoredAt,
Status: log.Status,
StatusText: systemRes.GetDestroyLogStatusText(log.Status),
CreatedAt: log.CreatedAt,
})
}
return &systemRes.DestroyLogListResponse{
List: items,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
}, nil
}
// GetDestroyLogDetail 获取商品销毁日志详情
func (s *ProductService) GetDestroyLogDetail(req systemReq.GetDestroyLogDetailRequest, db ...*gorm.DB) (*systemRes.DestroyLogDetailResponse, error) {
databaseConn := database.OptionalDB(db...)
var log models.ProductDestroyLog
if err := databaseConn.Where("id = ? AND is_del = ?", req.ID, 0).First(&log).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, utils.NewError("销毁日志不存在")
}
return nil, utils.NewError("查询销毁日志失败")
}
return &systemRes.DestroyLogDetailResponse{
ID: log.ID,
ProductID: log.ProductID,
Barcode: log.Barcode,
ProductSnapshot: string(log.ProductSnapshot),
ProductBookSnapshot: string(log.ProductBookSnapshot),
InventorySnapshot: string(log.InventorySnapshot),
InventoryDetailSnapshot: string(log.InventoryDetailSnapshot),
DestroyedBy: log.DestroyedBy,
DestroyedByID: log.DestroyedByID,
DestroyedAt: log.DestroyedAt,
RestoredBy: log.RestoredBy,
RestoredByID: log.RestoredByID,
RestoredAt: log.RestoredAt,
Status: log.Status,
StatusText: systemRes.GetDestroyLogStatusText(log.Status),
CreatedAt: log.CreatedAt,
UpdatedAt: log.UpdatedAt,
}, nil
}