package service import ( "errors" "fmt" "time" "gorm.io/gorm" "github.com/wangjia/jiu/backend/internal/model" ) var ErrInsufficientStock = errors.New("insufficient stock") type StockService struct { db *gorm.DB } func NewStockService(db *gorm.DB) *StockService { return &StockService{db: db} } // ApproveStockIn 审核入库单,审核通过后更新库存(事务) func (s *StockService) ApproveStockIn(shopID, orderID, reviewerID uint64) error { return s.db.Transaction(func(tx *gorm.DB) error { var order model.StockInOrder if err := tx.Preload("Items"). Where("id = ? AND shop_id = ?", orderID, shopID). First(&order).Error; err != nil { return err } if order.Status != "pending" { return errors.New("order is not in pending status") } now := time.Now() for _, item := range order.Items { if err := s.updateInventory(tx, shopID, order.WarehouseID, item.ProductID, "in", item.Quantity, orderID, "stock_in", reviewerID); err != nil { return err } } return tx.Model(&order).Updates(map[string]interface{}{ "status": "approved", "reviewer_id": reviewerID, "reviewed_at": now, }).Error }) } // ApproveStockOut 审核出库单 func (s *StockService) ApproveStockOut(shopID, orderID, reviewerID uint64) error { return s.db.Transaction(func(tx *gorm.DB) error { var order model.StockOutOrder if err := tx.Preload("Items"). Where("id = ? AND shop_id = ?", orderID, shopID). First(&order).Error; err != nil { return err } if order.Status != "pending" { return errors.New("order is not in pending status") } // 预检库存(FOR UPDATE 加锁,防止并发审核超卖) for _, item := range order.Items { var inv model.Inventory if err := tx.Set("gorm:query_option", "FOR UPDATE"). Where("shop_id = ? AND warehouse_id = ? AND product_id = ?", shopID, order.WarehouseID, item.ProductID).First(&inv).Error; err != nil { return fmt.Errorf("product %d not in inventory", item.ProductID) } if inv.Quantity < item.Quantity { return fmt.Errorf("%w: product_id=%d, available=%.3f, required=%.3f", ErrInsufficientStock, item.ProductID, inv.Quantity, item.Quantity) } } now := time.Now() for _, item := range order.Items { if err := s.updateInventory(tx, shopID, order.WarehouseID, item.ProductID, "out", item.Quantity, orderID, "stock_out", reviewerID); err != nil { return err } } return tx.Model(&order).Updates(map[string]interface{}{ "status": "approved", "reviewer_id": reviewerID, "reviewed_at": now, }).Error }) } // updateInventory 更新库存并写流水(在事务中调用) func (s *StockService) updateInventory(tx *gorm.DB, shopID, warehouseID, productID uint64, direction string, qty float64, refID uint64, refType string, operatorID uint64) error { var inv model.Inventory result := tx.Set("gorm:query_option", "FOR UPDATE"). Where("shop_id = ? AND warehouse_id = ? AND product_id = ?", shopID, warehouseID, productID).First(&inv) qtyBefore := inv.Quantity var qtyAfter float64 if direction == "in" { qtyAfter = qtyBefore + qty if result.Error != nil { // 不存在则创建 inv = model.Inventory{ShopID: shopID, WarehouseID: warehouseID, ProductID: productID, Quantity: qtyAfter} if err := tx.Create(&inv).Error; err != nil { return err } } else { if err := tx.Model(&inv).Update("quantity", qtyAfter).Error; err != nil { return err } } } else { qtyAfter = qtyBefore - qty if err := tx.Model(&inv).Update("quantity", qtyAfter).Error; err != nil { return err } } log := model.InventoryLog{ ShopID: shopID, WarehouseID: warehouseID, ProductID: productID, Direction: direction, Quantity: qty, QtyBefore: qtyBefore, QtyAfter: qtyAfter, RefType: refType, RefID: refID, OperatorID: &operatorID, } return tx.Create(&log).Error } // GenerateOrderNo 生成单号(事务安全,FOR UPDATE 防止并发重复单号) func (s *StockService) GenerateOrderNo(shopID uint64, orderType string) (string, error) { var no string err := s.db.Transaction(func(tx *gorm.DB) error { var rule model.NumberRule result := tx.Set("gorm:query_option", "FOR UPDATE"). Where("shop_id = ? AND type = ?", shopID, orderType).First(&rule) if result.Error != nil { // 初始化规则 rule = model.NumberRule{ShopID: shopID, Type: orderType, Prefix: orderType[:2], CurrentNo: 0} tx.Create(&rule) } rule.CurrentNo++ tx.Model(&rule).Update("current_no", rule.CurrentNo) dateStr := time.Now().Format("20060102") no = fmt.Sprintf("%s%s%06d", rule.Prefix, dateStr, rule.CurrentNo) return nil }) return no, err }