From d0e9a0fa82fe78bc5f8a8e646d34759477f02ce3 Mon Sep 17 00:00:00 2001 From: xiaodongzhu825 <97694732@qq.com> Date: Wed, 17 Jun 2026 15:58:44 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E5=BA=93=E5=AD=98=E6=8E=A5=E5=8F=A3=E6=9C=AA=E8=AE=A1=E7=AE=97?= =?UTF-8?q?=E9=94=81=E5=AE=9A=E5=BA=93=E5=AD=98=E9=97=AE=E9=A2=98=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=E5=90=8C=E6=AD=A5=E5=8F=8D=E5=B0=84?= =?UTF-8?q?=E5=95=86=E5=93=81=E8=A1=A8=E6=9C=AA=E5=90=8C=E6=AD=A5=E5=88=B0?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controllers/process.go | 202 ++++++++++++++---- models/product_book.go | 3 +- models/request/logistics.go | 38 ++-- models/request/split_account_deduction_log.go | 15 +- models/split_account_deduction_log.go | 1 + service/process.go | 26 +-- service/product.go | 6 +- 7 files changed, 206 insertions(+), 85 deletions(-) diff --git a/controllers/process.go b/controllers/process.go index b7185e1..d243b58 100644 --- a/controllers/process.go +++ b/controllers/process.go @@ -4,10 +4,12 @@ import ( "bytes" "errors" "fmt" + "gorm.io/gorm" "io" "mime/multipart" "net/http" "strings" + "time" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" @@ -196,16 +198,6 @@ func (r *ProcessApi) SubmitReceiving(c *gin.Context) { } // 同步给主库对应的product_book_xx中 - /*var warehouse models.Warehouse - if err := databaseConn.Where("id = ? AND is_del = 0", receivingOrder.WarehouseID).First(&warehouse).Error; err != nil { - utils.ErrorLog(constant.LoggerChannelWork, logrus.Fields{ - "source": "查询仓库失败", - "warehouse_id": receivingOrder.WarehouseID, - "err_msg": err.Error(), - }) - return - }*/ - // 同步给主库对应的product_book_xx中 // 获取租户数据库连接 tenantDB, err := database.GetTenantDB(aboutID) @@ -230,52 +222,188 @@ func (r *ProcessApi) SubmitReceiving(c *gin.Context) { return } - // 查询库位信息(用于获取库位名称) - locationMap := make(map[int64]string) + // 查询库位信息(获取库位ID和Code) + type locationInfo struct { + LocationID int64 + Code string + } + locationMap := make(map[int64]locationInfo) for _, product := range products { - // 通过库存明细表查询商品所在的库位 var inventoryDetail models.InventoryDetail if err := tenantDB.Where("product_id = ? AND warehouse_id = ? AND is_del = 0", product.ID, receivingOrder.WarehouseID).First(&inventoryDetail).Error; err == nil { var location models.Location if err := tenantDB.Where("id = ? AND is_del = 0", inventoryDetail.LocationID).First(&location).Error; err == nil { - locationMap[product.ID] = location.Code + locationMap[product.ID] = locationInfo{ + LocationID: location.ID, + Code: location.Code, + } } } } - // 逐个同步商品到主库 + // 查询BookInfo(按StandardProductID关联) + bookInfoMap := make(map[int64]models.BookInfo) for _, product := range products { - locationCode := "" - if code, exists := locationMap[product.ID]; exists { - locationCode = code + if product.StandardProductID > 0 { + if _, exists := bookInfoMap[product.StandardProductID]; !exists { + var bookInfo models.BookInfo + if err := database.DB.Where("id = ?", product.StandardProductID).First(&bookInfo).Error; err == nil { + bookInfoMap[product.StandardProductID] = bookInfo + } + } + } + } + + // 逐个写入主库 product_book_xx 分表 + for _, product := range products { + isbn := product.Barcode + if isbn == "" { + continue } - if syncErr := database.SyncProductToMainDB(aboutID, &product, warehouse.ID, warehouse.Name, 0, locationCode); syncErr != nil { - utils.ErrorLog(constant.LoggerChannelWork, logrus.Fields{ - "source": "同步商品到主库失败", - "user_id": aboutID, - "product_id": product.ID, - "warehouse_id": warehouse.ID, - "location_code": locationCode, - "err_msg": syncErr.Error(), - }) + tableName := models.ProductBookTableName(isbn) + + var locInfo locationInfo + if li, exists := locationMap[product.ID]; exists { + locInfo = li + } + + var bookInfo models.BookInfo + if bi, exists := bookInfoMap[product.StandardProductID]; exists { + bookInfo = bi + } + + now := time.Now().Unix() + + // 按 self_id + about_id 查重,避免不同租户商品ID冲突 + var existingBook models.ProductBook + err := database.DB.Table(tableName).Where("self_id = ? AND about_id = ? AND is_del = 0", product.ID, aboutID).First(&existingBook).Error + + if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { + bookRecord := models.ProductBook{ + SelfID: product.ID, + AboutId: aboutID, + WarehouseID: warehouse.ID, + WarehouseName: warehouse.Name, + LocationID: locInfo.LocationID, + LocationName: locInfo.Code, + CategoryID: product.CategoryID, + StandardProductID: product.StandardProductID, + Fid: bookInfo.Fid, + Type: bookInfo.Type, + ISBN: isbn, + FISBN: bookInfo.FISBN, + BookName: bookInfo.BookName, + FBookName: bookInfo.FBookName, + Author: bookInfo.Author, + Publishing: bookInfo.Publishing, + PublicationTime: bookInfo.PublicationTime, + Binding: bookInfo.Binding, + PagesCount: bookInfo.PagesCount, + WordsCount: bookInfo.WordsCount, + Format: bookInfo.Format, + CatID: bookInfo.CatID, + Name: product.Name, + Appearance: product.Appearance, + Barcode: product.Barcode, + Price: product.Price, + SalePrice: product.SalePrice, + Cost: product.Cost, + LiveImage: product.LiveImage, + IsBatchManaged: product.IsBatchManaged, + IsShelfLifeManaged: product.IsShelfLifeManaged, + Status: product.Status, + CreatedAt: now, + UpdatedAt: now, + IsDel: 0, + } + if createErr := database.DB.Table(tableName).Create(&bookRecord).Error; createErr != nil { + utils.ErrorLog(constant.LoggerChannelWork, logrus.Fields{ + "source": "写入product_book分表失败", + "user_id": aboutID, + "self_id": product.ID, + "isbn": isbn, + "table_name": tableName, + "warehouse_id": warehouse.ID, + "err_msg": createErr.Error(), + }) + } else { + utils.InfoLog(constant.LoggerChannelWork, logrus.Fields{ + "source": "写入product_book分表成功", + "user_id": aboutID, + "self_id": product.ID, + "isbn": isbn, + "table_name": tableName, + "warehouse_id": warehouse.ID, + "location_id": locInfo.LocationID, + }) + } + } else if err == nil { + updateData := map[string]interface{}{ + "warehouse_id": warehouse.ID, + "warehouse_name": warehouse.Name, + "location_id": locInfo.LocationID, + "location_name": locInfo.Code, + "category_id": product.CategoryID, + "standard_product_id": product.StandardProductID, + "name": product.Name, + "book_name": bookInfo.BookName, + "f_isbn": bookInfo.FISBN, + "f_book_name": bookInfo.FBookName, + "author": bookInfo.Author, + "publishing": bookInfo.Publishing, + "publication_time": bookInfo.PublicationTime, + "binding": bookInfo.Binding, + "pages_count": bookInfo.PagesCount, + "words_count": bookInfo.WordsCount, + "format": bookInfo.Format, + "cat_id": bookInfo.CatID, + "appearance": product.Appearance, + "barcode": product.Barcode, + "price": product.Price, + "sale_price": product.SalePrice, + "cost": product.Cost, + "live_image": product.LiveImage, + "is_batch_managed": product.IsBatchManaged, + "is_shelf_life_managed": product.IsShelfLifeManaged, + "status": product.Status, + "updated_at": now, + } + if updateErr := database.DB.Table(tableName).Model(&existingBook).Updates(updateData).Error; updateErr != nil { + utils.ErrorLog(constant.LoggerChannelWork, logrus.Fields{ + "source": "更新product_book分表失败", + "user_id": aboutID, + "self_id": product.ID, + "isbn": isbn, + "table_name": tableName, + "warehouse_id": warehouse.ID, + "err_msg": updateErr.Error(), + }) + } else { + utils.InfoLog(constant.LoggerChannelWork, logrus.Fields{ + "source": "更新product_book分表成功", + "user_id": aboutID, + "self_id": product.ID, + "isbn": isbn, + "table_name": tableName, + "warehouse_id": warehouse.ID, + "location_id": locInfo.LocationID, + }) + } } else { - utils.InfoLog(constant.LoggerChannelWork, logrus.Fields{ - "source": "同步商品到主库成功", - "user_id": aboutID, - "product_id": product.ID, - "product_name": product.Name, - "barcode": product.Barcode, - "warehouse_id": warehouse.ID, - "location_code": locationCode, + utils.ErrorLog(constant.LoggerChannelWork, logrus.Fields{ + "source": "查询product_book分表失败", + "user_id": aboutID, + "self_id": product.ID, + "isbn": isbn, + "table_name": tableName, + "err_msg": err.Error(), }) } } warehouseId := warehouse.ID - //warehouseId := warehouse.ID - productIds := make([]string, len(itemProductIDs)) for i, pid := range itemProductIDs { productIds[i] = fmt.Sprintf("%d", pid) diff --git a/models/product_book.go b/models/product_book.go index 8111cb9..e3862c4 100644 --- a/models/product_book.go +++ b/models/product_book.go @@ -7,7 +7,8 @@ import ( // ProductBook 商品书籍分表(按ISBN后两位分表 product_book_00 ~ product_book_99) type ProductBook struct { - ID int64 `json:"id" gorm:"primarykey;comment:商品ID"` + ID int64 `json:"id" gorm:"primarykey;comment:自增ID"` + SelfID int64 `json:"self_id" gorm:"not null;default:0;comment:再分库商品表中的商品ID"` CategoryID int64 `json:"category_id" gorm:"not null;default:0;comment:分类ID"` AboutId int64 `json:"about_id" gorm:"not null;default:0;index;comment:关联ID"` WarehouseID int64 `json:"warehouse_id" gorm:"not null;default:0;index;comment:仓库ID"` diff --git a/models/request/logistics.go b/models/request/logistics.go index ebcddea..a4ed5c7 100644 --- a/models/request/logistics.go +++ b/models/request/logistics.go @@ -29,25 +29,25 @@ type CreateLogisticsRequest struct { } type UpdateLogisticsRequest struct { - Id uint64 `form:"id" binding:"required"` - TemplateName string `form:"template_name" binding:"required,max=255"` - DeliveryProvince string `form:"delivery_province" binding:"required,max=255"` - DeliveryCity string `form:"delivery_city" binding:"required,max=255"` - DeliveryArea string `form:"delivery_area" binding:"required,max=255"` - DeliveryAddress string `form:"delivery_address" binding:"required,max=255"` - PricingMethod string `form:"pricing_method" binding:"required,oneof=0 1 2 3"` - Shipping string `form:"shipping" binding:"required,oneof=0 1 2"` - FirWbv float64 `form:"fir_wbv"` - FirPrice float64 `form:"fir_price"` - ContinueWbv float64 `form:"continue_wbv"` - ContinuePrice float64 `form:"continue_price"` - Contact string `form:"contact" binding:"required,max=14"` - PhoneNumber uint64 `form:"phone_number" binding:"required"` - FullAddress string `form:"full_address" binding:"required,max=255"` - ShippingRange string `form:"shipping_range" binding:"required"` - WarehouseId uint64 `form:"warehouse_id"` - Remark string `form:"remark" binding:"max=255"` - Status string `form:"status" binding:"oneof=0 1"` + Id uint64 `form:"id" binding:"required"` // 物流模板ID + TemplateName string `form:"template_name" binding:"required,max=255"` // 物流模板名称 + DeliveryProvince string `form:"delivery_province" binding:"required,max=255"` // 发货省 + DeliveryCity string `form:"delivery_city" binding:"required,max=255"` // 发货市 + DeliveryArea string `form:"delivery_area" binding:"required,max=255"` // 发货区 + DeliveryAddress string `form:"delivery_address" binding:"required,max=255"` // 发货地址 + PricingMethod string `form:"pricing_method" binding:"required,oneof=0 1 2 3"` // 计费方式 + Shipping string `form:"shipping" binding:"required,oneof=0 1 2"` // 配送方式 + FirWbv float64 `form:"fir_wbv"` // 首件 + FirPrice float64 `form:"fir_price"` // 首费 + ContinueWbv float64 `form:"continue_wbv"` // 续件 + ContinuePrice float64 `form:"continue_price"` // 续费 + Contact string `form:"contact" binding:"required,max=14"` // 联系人 + PhoneNumber uint64 `form:"phone_number" binding:"required"` // 联系电话 + FullAddress string `form:"full_address" binding:"required,max=255"` // 收货地址 + ShippingRange string `form:"shipping_range" binding:"required"` // 配送范围 + WarehouseId uint64 `form:"warehouse_id"` // 仓库ID + Remark string `form:"remark" binding:"max=255"` // 备注 + Status string `form:"status" binding:"oneof=0 1"` // 状态 } type DeleteLogisticsRequest struct { diff --git a/models/request/split_account_deduction_log.go b/models/request/split_account_deduction_log.go index 2e695a3..ec7bf43 100644 --- a/models/request/split_account_deduction_log.go +++ b/models/request/split_account_deduction_log.go @@ -12,13 +12,13 @@ type GetSplitAccountDeductionLogListRequest struct { // AddSplitAccountDeductionLogRequest 添加分账扣钱日志请求 type AddSplitAccountDeductionLogRequest struct { - BusinessNo string `form:"business_no" binding:"required"` - ConfigID int64 `form:"config_id" binding:"required"` - ConfigName string `form:"config_name" binding:"required"` - DeductionDetails string `form:"deduction_details" binding:"required"` - TotalAmount float64 `form:"total_amount" binding:"required"` - DeductionAmount float64 `form:"deduction_amount" binding:"required"` - RemainingAmount float64 `form:"remaining_amount" binding:"required"` + BusinessNo string `form:"business_no" binding:"required"` // 业务单号 + ConfigID int64 `form:"config_id" binding:"required"` // 分账配置ID + ConfigName string `form:"config_name" binding:"required"` // 分账配置名称 + DeductionDetails string `form:"deduction_details" binding:"required"` // 扣钱详情 + TotalAmount float64 `form:"total_amount" binding:"required"` // 总金额 + DeductionAmount float64 `form:"deduction_amount" binding:"required"` // 扣钱金额 + RemainingAmount float64 `form:"remaining_amount" binding:"required"` // 剩余金额 } // UpdateSplitAccountDeductionLogRequest 更新分账扣钱日志请求 @@ -29,6 +29,7 @@ type UpdateSplitAccountDeductionLogRequest struct { ConfigName string `form:"config_name"` DeductionDetails string `form:"deduction_details"` TotalAmount float64 `form:"total_amount"` + Status int8 `form:"status"` DeductionAmount float64 `form:"deduction_amount"` RemainingAmount float64 `form:"remaining_amount"` } diff --git a/models/split_account_deduction_log.go b/models/split_account_deduction_log.go index 6326eaa..475d3ae 100644 --- a/models/split_account_deduction_log.go +++ b/models/split_account_deduction_log.go @@ -12,6 +12,7 @@ type SplitAccountDeductionLog struct { TotalAmount float64 `json:"total_amount" gorm:"type:decimal(15,2);not null;default:0.00;comment:总金额(分账前)"` DeductionAmount float64 `json:"deduction_amount" gorm:"type:decimal(15,2);not null;default:0.00;comment:扣款总金额"` RemainingAmount float64 `json:"remaining_amount" gorm:"type:decimal(15,2);not null;default:0.00;comment:剩余金额(分账后)"` + Status int8 `json:"status" gorm:"type:tinyint;not null;default:0;comment:状态 0:正常 1:退款"` CreatedBy string `json:"created_by" gorm:"size:100;not null;default:'';comment:创建人/系统"` UpdatedBy string `json:"updated_by" gorm:"size:100;comment:更新人"` CreatedAt int64 `json:"created_at" gorm:"type:bigint;default:0;comment:创建时间戳(秒)"` diff --git a/service/process.go b/service/process.go index c851312..6a95e36 100644 --- a/service/process.go +++ b/service/process.go @@ -797,10 +797,7 @@ func (s *ProcessService) loadOrderAndWaveDetails(tx *gorm.DB, orderID, waveTaskI } // processOrderItems 处理订单明细 -func (s *ProcessService) processOrderItems(tx *gorm.DB, items []orderItemInfo, orderInfo *orderInfo, - productMap map[int64]models.Product, locationMap map[int64]models.Location, - orderItemMap map[int64]interface{}, waveTaskDetailMap map[int64]*models.WaveTaskDetail, - operator string, operatorID int64, now int64, changeType int8) (map[inventoryKey]*inventoryOperation, []models.InventoryLog, error) { +func (s *ProcessService) processOrderItems(tx *gorm.DB, items []orderItemInfo, orderInfo *orderInfo, productMap map[int64]models.Product, locationMap map[int64]models.Location, orderItemMap map[int64]interface{}, waveTaskDetailMap map[int64]*models.WaveTaskDetail, operator string, operatorID int64, now int64, changeType int8) (map[inventoryKey]*inventoryOperation, []models.InventoryLog, error) { inventoryOpMap := make(map[inventoryKey]*inventoryOperation) @@ -999,8 +996,7 @@ func (s *ProcessService) processOrderItems(tx *gorm.DB, items []orderItemInfo, o } // executeInventoryOperations 执行库存操作 -func (s *ProcessService) executeInventoryOperations(tx *gorm.DB, inventoryOpMap map[inventoryKey]*inventoryOperation, - inventoryLogs []models.InventoryLog, orderNo string, operator string, operatorID int64, now int64, changeType int8) error { +func (s *ProcessService) executeInventoryOperations(tx *gorm.DB, inventoryOpMap map[inventoryKey]*inventoryOperation, inventoryLogs []models.InventoryLog, orderNo string, operator string, operatorID int64, now int64, changeType int8) error { for _, op := range inventoryOpMap { log, err := s.processInventoryOperation(tx, op.key, op.locationID, op.quantity, changeType, orderNo, operator, operatorID, now) @@ -2667,8 +2663,7 @@ func (s *ProcessService) ChangeLocation(req systemReq.ChangeLocationRequest, ope } // 创建波次 -func (s *ProcessService) createWaveHeader(tx *gorm.DB, waveNo string, direction int8, - warehouseID, relatedOrderID int64, creator string, creatorID int64) (*models.WaveHeader, error) { +func (s *ProcessService) createWaveHeader(tx *gorm.DB, waveNo string, direction int8, warehouseID, relatedOrderID int64, creator string, creatorID int64) (*models.WaveHeader, error) { now := time.Now().Unix() waveHeader := models.WaveHeader{ WaveNo: waveNo, @@ -2718,8 +2713,7 @@ func (s *ProcessService) createWaveTaskDetails(tx *gorm.DB, waveTaskID int64, it } // 创建波次任务和明细 -func (s *ProcessService) createWaveTaskAndDetails(tx *gorm.DB, waveID int64, taskType int8, - items []WaveItemData, assignee string, assigneeID, carId, carCode, carCapacity int64) (*models.WaveTask, error) { +func (s *ProcessService) createWaveTaskAndDetails(tx *gorm.DB, waveID int64, taskType int8, items []WaveItemData, assignee string, assigneeID, carId, carCode, carCapacity int64) (*models.WaveTask, error) { now := time.Now().Unix() taskNo := utils.GenerateTaskNo() taskTd := utils.GenerateTaskDetailNo() @@ -2767,8 +2761,7 @@ func (s *ProcessService) createWaveTaskAndDetails(tx *gorm.DB, waveID int64, tas } // createWaveTaskAndDetailsForOutbound 创建出库波次任务和明细(使用入库批次号) -func (s *ProcessService) createWaveTaskAndDetailsForOutbound(tx *gorm.DB, waveID int64, taskType int8, - items []WaveItemData, outboundItems []models.OutboundOrderItem, assignee string, assigneeID, carId, carCode, carCapacity int64) (*models.WaveTask, error) { +func (s *ProcessService) createWaveTaskAndDetailsForOutbound(tx *gorm.DB, waveID int64, taskType int8, items []WaveItemData, outboundItems []models.OutboundOrderItem, assignee string, assigneeID, carId, carCode, carCapacity int64) (*models.WaveTask, error) { now := time.Now().Unix() taskNo := utils.GenerateTaskNo() @@ -2864,8 +2857,7 @@ func (s *ProcessService) updateWaveTaskToPicking(tx *gorm.DB, waveTaskID int64) } // 处理库存操作(使用原子操作和行级锁保证并发安全) -func (s *ProcessService) processInventoryOperation(tx *gorm.DB, opKey inventoryKey, locationID int64, - quantity int64, changeType int8, orderNo string, operator string, operatorID int64, now int64) (*models.InventoryLog, error) { +func (s *ProcessService) processInventoryOperation(tx *gorm.DB, opKey inventoryKey, locationID int64, quantity int64, changeType int8, orderNo string, operator string, operatorID int64, now int64) (*models.InventoryLog, error) { if changeType == constant.InventoryChangeInbound { var inventory models.Inventory @@ -3499,8 +3491,7 @@ func (s *ProcessService) CancelSalesOrder(orderID int64, operator string, operat } // processInventoryOperationForAdjustment 处理盘库调整的库存汇总操作 -func (s *ProcessService) processInventoryOperationForAdjustment(tx *gorm.DB, opKey inventoryKey, locationID int64, - quantity int64, orderNo string, operator string, operatorID int64, now int64, remark string) (*models.InventoryLog, error) { +func (s *ProcessService) processInventoryOperationForAdjustment(tx *gorm.DB, opKey inventoryKey, locationID int64, quantity int64, orderNo string, operator string, operatorID int64, now int64, remark string) (*models.InventoryLog, error) { var inventory models.Inventory err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). @@ -3598,8 +3589,7 @@ func (s *ProcessService) processInventoryOperationForAdjustment(tx *gorm.DB, opK } // processInventoryDetailOperationForAdjustment 处理盘库调整的库存明细操作 -func (s *ProcessService) processInventoryDetailOperationForAdjustment(tx *gorm.DB, opKey inventoryKey, locationID int64, - quantity int64, now int64) error { +func (s *ProcessService) processInventoryDetailOperationForAdjustment(tx *gorm.DB, opKey inventoryKey, locationID int64, quantity int64, now int64) error { var inventoryDetail models.InventoryDetail err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). diff --git a/service/product.go b/service/product.go index b32f4af..8e07bfc 100644 --- a/service/product.go +++ b/service/product.go @@ -675,10 +675,10 @@ func (s *ProductService) GetProductInventory(req systemReq.GetProductInventoryRe } var groupList []GroupStock - // 先根据商品的 ISBN 和品相,查询所有匹配的库存记录,再按仓库分组统计 + // 先根据商品的 ISBN 和品相,查询所有匹配的库存记录,再按仓库分组统计(可用量 = 总量 - 锁定量) databaseConn.Table("inventory"). Select(` - COALESCE(SUM(inventory.quantity), 0) as total_quantity + COALESCE(SUM(inventory.quantity - inventory.locked_quantity), 0) as total_quantity `). Joins("LEFT JOIN product p ON inventory.product_id = p.id AND p.is_del = ?", 0). Where("p.barcode = ? AND p.appearance = ? AND inventory.warehouse_id IS NOT NULL AND inventory.is_del = ?", @@ -686,7 +686,7 @@ func (s *ProductService) GetProductInventory(req systemReq.GetProductInventoryRe Group("inventory.warehouse_id"). Scan(&groupList) - // 累加所有分组的数量 + // 累加所有分组的可用数量 for _, group := range groupList { totalQuantity += group.TotalQuantity }