3950 lines
126 KiB
Go
3950 lines
126 KiB
Go
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
|
||
}
|