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 var warehouses []systemRes.ProductInventoryWarehouse // type=1: 按品相+ISBN+仓库分组后统计总数量 if req.Type == 1 { type GroupStock struct { WarehouseID int64 `gorm:"column:warehouse_id"` WarehouseName string `gorm:"column:warehouse_name"` WarehouseCode string `gorm:"column:warehouse_code"` LocationID int64 `gorm:"column:location_id"` LocationCode string `gorm:"column:location_code"` TotalQuantity int64 `gorm:"column:total_quantity"` LgID uint64 `gorm:"column:lg_id"` LgTemplateName string `gorm:"column:lg_template_name"` LgDeliveryProvince string `gorm:"column:lg_delivery_province"` LgDeliveryCity string `gorm:"column:lg_delivery_city"` LgDeliveryArea string `gorm:"column:lg_delivery_area"` LgDeliveryAddress string `gorm:"column:lg_delivery_address"` LgPricingMethod string `gorm:"column:lg_pricing_method"` LgShipping string `gorm:"column:lg_shipping"` LgFirWbv float64 `gorm:"column:lg_fir_wbv"` LgFirPrice float64 `gorm:"column:lg_fir_price"` LgContinueWbv float64 `gorm:"column:lg_continue_wbv"` LgContinuePrice float64 `gorm:"column:lg_continue_price"` LgContact string `gorm:"column:lg_contact"` LgPhoneNumber uint64 `gorm:"column:lg_phone_number"` LgFullAddress string `gorm:"column:lg_full_address"` LgShippingRange string `gorm:"column:lg_shipping_range"` LgWarehouseID uint64 `gorm:"column:lg_warehouse_id"` LgRemark string `gorm:"column:lg_remark"` LgStatus string `gorm:"column:lg_status"` LgCreateTime *time.Time `gorm:"column:lg_create_time"` LgUpdateTime *time.Time `gorm:"column:lg_update_time"` } var groupList []GroupStock // 先根据商品的 ISBN 和品相,查询所有匹配的库存记录,再按仓库分组统计(可用量 = 总量 - 锁定量) // 仓库和库位信息从 inventory_detail 表关联获取 databaseConn.Table("inventory"). Select(` inventory.warehouse_id, COALESCE(w.name, '') as warehouse_name, COALESCE(w.code, '') as warehouse_code, COALESCE(MIN(l.id), 0) as location_id, COALESCE(MIN(l.code), '') as location_code, COALESCE(SUM(inventory.quantity - inventory.locked_quantity), 0) as total_quantity, COALESCE(logistics.id, 0) as lg_id, COALESCE(logistics.template_name, '') as lg_template_name, COALESCE(logistics.delivery_province, '') as lg_delivery_province, COALESCE(logistics.delivery_city, '') as lg_delivery_city, COALESCE(logistics.delivery_area, '') as lg_delivery_area, COALESCE(logistics.delivery_address, '') as lg_delivery_address, COALESCE(logistics.pricing_method, '') as lg_pricing_method, COALESCE(logistics.shipping, '') as lg_shipping, COALESCE(logistics.fir_wbv, 0) as lg_fir_wbv, COALESCE(logistics.fir_price, 0) as lg_fir_price, COALESCE(logistics.continue_wbv, 0) as lg_continue_wbv, COALESCE(logistics.continue_price, 0) as lg_continue_price, COALESCE(logistics.contact, '') as lg_contact, COALESCE(logistics.phone_number, 0) as lg_phone_number, COALESCE(logistics.full_address, '') as lg_full_address, COALESCE(logistics.shipping_range, '') as lg_shipping_range, COALESCE(logistics.warehouse_id, 0) as lg_warehouse_id, COALESCE(logistics.remark, '') as lg_remark, COALESCE(logistics.status, '') as lg_status, logistics.create_time as lg_create_time, logistics.update_time as lg_update_time `). Joins("LEFT JOIN product p ON inventory.product_id = p.id AND p.is_del = ?", 0). Joins("LEFT JOIN warehouse w ON inventory.warehouse_id = w.id AND w.is_del = 0"). Joins("LEFT JOIN logistics ON w.logistics_id = logistics.id AND logistics.del_flag = '0'"). Joins("LEFT JOIN inventory_detail d ON d.product_id = inventory.product_id AND d.warehouse_id = inventory.warehouse_id AND d.is_del = 0"). Joins("LEFT JOIN location l ON d.location_id = l.id AND l.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, w.name, w.code"). Scan(&groupList) // 累加所有分组的可用数量 for _, group := range groupList { totalQuantity += group.TotalQuantity var lg *systemRes.LogisticsResponse if group.LgID > 0 { lg = &systemRes.LogisticsResponse{ Id: group.LgID, TemplateName: group.LgTemplateName, DeliveryProvince: group.LgDeliveryProvince, DeliveryCity: group.LgDeliveryCity, DeliveryArea: group.LgDeliveryArea, DeliveryAddress: group.LgDeliveryAddress, PricingMethod: group.LgPricingMethod, Shipping: group.LgShipping, FirWbv: group.LgFirWbv, FirPrice: group.LgFirPrice, ContinueWbv: group.LgContinueWbv, ContinuePrice: group.LgContinuePrice, Contact: group.LgContact, PhoneNumber: group.LgPhoneNumber, FullAddress: group.LgFullAddress, ShippingRange: group.LgShippingRange, WarehouseId: group.LgWarehouseID, Remark: group.LgRemark, Status: group.LgStatus, CreateTime: group.LgCreateTime, UpdateTime: group.LgUpdateTime, } } warehouses = append(warehouses, systemRes.ProductInventoryWarehouse{ WarehouseID: group.WarehouseID, WarehouseName: group.WarehouseName, WarehouseCode: group.WarehouseCode, LocationID: group.LocationID, LocationName: group.LocationCode, Logistics: lg, 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, Warehouses: warehouses, }, 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 { fmt.Printf("[DEBUG] 审核通过, NewLiveImage=%v, NewPageCount=%d, NewWordCount=%d\n", req.NewLiveImage, req.NewPageCount, req.NewWordCount) 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) } // 同步更新 product 表的 live_image,解决其他平台从 product 表读不到新图片的问题 if len(req.NewLiveImage) > 0 { liveImageJSON, _ := json.Marshal(req.NewLiveImage) if err := database.DB.Model(&models.Product{}).Where("barcode = ?", log.Barcode).Update("live_image", datatypes.JSON(liveImageJSON)).Error; err != nil { fmt.Printf("[WARN] 审核后更新 product 表 live_image 失败 (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 if len(req.NewLiveImage) > 0 { data["book_pic"] = map[string]string{ "localPath": "", "pddPath": req.NewLiveImage[0], } data["book_pic_b"] = req.NewLiveImage[0] } if req.NewPageCount > 0 { data["page_count"] = strconv.FormatInt(req.NewPageCount, 10) data["pages"] = strconv.FormatInt(req.NewPageCount, 10) } if req.NewWordCount > 0 { data["word_count"] = strconv.FormatInt(req.NewWordCount, 10) data["words"] = strconv.FormatInt(req.NewWordCount, 10) } payload := map[string]interface{}{ "isbn": barcode, "data": data, } jsonBytes, err := json.Marshal(payload) if err != nil { return fmt.Errorf("序列化请求参数失败:%w", err) } fmt.Printf("[DEBUG] ES更新请求 URL: %s\n", esURL) fmt.Printf("[DEBUG] ES更新请求 payload: %s\n", string(jsonBytes)) 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) fmt.Printf("[DEBUG] ES更新响应 status=%d body: %s\n", resp.StatusCode, string(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 }