package service import ( "assets/consts/public" daoAsset "assets/dao/asset" daoProcurement "assets/dao/procurement" daoStock "assets/dao/stock" dtoProcurement "assets/model/dto/procurement" dtoStock "assets/model/dto/stock" serviceStock "assets/service/stock" "context" "errors" "fmt" "gitea.com/red-future/common/utils" "github.com/gogf/gf/v2/frame/g" "go.mongodb.org/mongo-driver/v2/bson" ) type purchaseInbound struct{} var PurchaseInbound = new(purchaseInbound) // Create 创建采购入库 func (s *purchaseInbound) Create(ctx context.Context, req *dtoProcurement.CreatePurchaseInboundReq) (res *dtoProcurement.CreatePurchaseInboundRes, err error) { // 1. 查询采购订单明细 orderItem, err := daoProcurement.PurchaseOrderItem.GetOne(ctx, req.OrderItemId) if err != nil { return nil, errors.New("采购订单明细不存在") } if g.IsEmpty(orderItem) { return nil, errors.New("采购订单明细不存在") } // 2. 校验+更新入库数量(原子操作,防止并发竞态) // 使用$inc + $expr条件:inboundQty + delta <= passQuantity if err = daoProcurement.PurchaseOrderItem.IncrementInboundQty(ctx, req.OrderItemId, req.InboundQty); err != nil { return nil, fmt.Errorf("入库数量校验失败: %v", err) } // 3. 生成入库单号和批次号 inboundNo, err := s.generateIncrSequence(ctx, public.StockInboundNoKeyPrefix) if err != nil { return nil, fmt.Errorf("生成入库单号失败: %v", err) } batchNo, err := s.generateIncrSequence(ctx, public.StockBatchNoKeyPrefix) if err != nil { return nil, fmt.Errorf("生成批次号失败: %v", err) } // 4. 查询关联信息 warehouseName, zoneName, locationName := s.getStorageNames(ctx, req.WarehouseId, req.ZoneId, req.LocationId) privateSkuName, privateCategoryPath := s.getPrivateSkuInfo(ctx, req.PrivateSkuId, req.PrivateCategoryId) // 5. 创建入库记录 inboundReq := &dtoProcurement.CreatePurchaseInboundReq{ OrderItemId: req.OrderItemId, InboundQty: req.InboundQty, WarehouseId: req.WarehouseId, ZoneId: req.ZoneId, LocationId: req.LocationId, PrivateSkuId: req.PrivateSkuId, PrivateCategoryId: req.PrivateCategoryId, Remark: req.Remark, } ids, err := daoProcurement.PurchaseInbound.Insert(ctx, inboundReq) if err != nil { return nil, fmt.Errorf("创建入库记录失败: %v", err) } inboundId := ids[0].(bson.ObjectID) // 6. 更新入库记录的关联信息 err = s.updateInboundDetails(ctx, &inboundId, orderItem.OrderId, inboundNo, batchNo, warehouseName, zoneName, locationName, privateSkuName, privateCategoryPath) if err != nil { return nil, fmt.Errorf("更新入库记录失败: %v", err) } // 7. 创建PrivateStock批次记录 privateStockReq := &dtoStock.CreatePrivateStockReq{ PrivateSkuID: req.PrivateSkuId, BatchNo: batchNo, BatchQty: req.InboundQty, AvailableQty: req.InboundQty, WarehouseId: req.WarehouseId, ZoneId: req.ZoneId, LocationId: req.LocationId, PrivateCategoryPath: privateCategoryPath, } privateStockIds, err := daoStock.PrivateStock.Insert(ctx, privateStockReq) if err != nil { return nil, fmt.Errorf("创建库存批次失败: %v", err) } privateStockId := privateStockIds[0].(bson.ObjectID) // 8. 触发库位容量更新(异步,失败不影响入库) if req.LocationId != nil && !req.LocationId.IsZero() { if capErr := serviceStock.Capacity.UpdateLocationCapacity(ctx, req.LocationId); capErr != nil { g.Log().Warningf(ctx, "更新库位容量失败(不影响入库): %v", capErr) } } // 9. 更新入库记录关联的库存ID err = s.updateInboundPrivateStockId(ctx, &inboundId, &privateStockId) if err != nil { return nil, fmt.Errorf("更新入库记录库存ID失败: %v", err) } // 10. 入库数量已在步骤2原子更新,无需重复操作 return &dtoProcurement.CreatePurchaseInboundRes{ Id: &inboundId, InboundNo: inboundNo, BatchNo: batchNo, }, nil } // GetOne 获取入库详情 func (s *purchaseInbound) GetOne(ctx context.Context, req *dtoProcurement.GetPurchaseInboundReq) (res *dtoProcurement.GetPurchaseInboundRes, err error) { one, err := daoProcurement.PurchaseInbound.GetOne(ctx, req) if err != nil { return } err = utils.Struct(one, &res) return } // List 获取入库列表 func (s *purchaseInbound) List(ctx context.Context, req *dtoProcurement.ListPurchaseInboundReq) (res *dtoProcurement.ListPurchaseInboundRes, err error) { list, total, err := daoProcurement.PurchaseInbound.List(ctx, req) if err != nil { return } res = &dtoProcurement.ListPurchaseInboundRes{ Total: total, } err = utils.Struct(list, &res.List) return } // generateInboundNo 生成入库单号(Redis自增,按天重置) func (s *purchaseInbound) generateIncrSequence(ctx context.Context, keyPrefix string) (string, error) { seq, err := utils.IncrSequence(ctx, keyPrefix, 6, "-") if err != nil { return "", err } return seq, nil } // getStorageNames 获取仓储名称 func (s *purchaseInbound) getStorageNames(ctx context.Context, warehouseId, zoneId, locationId *bson.ObjectID) (warehouseName, zoneName, locationName string) { if !g.IsEmpty(warehouseId) { warehouse, _ := daoStock.Warehouse.GetOne(ctx, &dtoStock.GetWarehouseReq{Id: warehouseId}) if !g.IsEmpty(warehouse) { warehouseName = warehouse.WarehouseName } } if !g.IsEmpty(zoneId) { zone, _ := daoStock.Zone.GetOne(ctx, &dtoStock.GetZoneReq{Id: zoneId}) if !g.IsEmpty(zone) { zoneName = zone.ZoneName } } if !g.IsEmpty(locationId) { location, _ := daoStock.Location.GetOne(ctx, &dtoStock.GetLocationReq{Id: locationId}) if !g.IsEmpty(location) { locationName = location.LocationName } } return } // getPrivateSkuInfo 获取私域SKU信息 func (s *purchaseInbound) getPrivateSkuInfo(ctx context.Context, privateSkuId, privateCategoryId *bson.ObjectID) (privateSkuName, privateCategoryPath string) { if !g.IsEmpty(privateSkuId) { privateSku, err := daoAsset.PrivateSku.GetOne(ctx, privateSkuId) if err == nil && !g.IsEmpty(privateSku) { privateSkuName = privateSku.SkuName } } if !g.IsEmpty(privateCategoryId) { privateCategory, err := daoAsset.PrivateCategory.GetOne(ctx, privateCategoryId) if err == nil && !g.IsEmpty(privateCategory) { privateCategoryPath = privateCategory.Path } } return } // updateInboundDetails 更新入库记录详细信息 func (s *purchaseInbound) updateInboundDetails(ctx context.Context, inboundId, orderId *bson.ObjectID, inboundNo, batchNo, warehouseName, zoneName, locationName, privateSkuName, privateCategoryPath string) (err error) { return daoProcurement.PurchaseInbound.UpdateDetails(ctx, inboundId, orderId, inboundNo, batchNo, warehouseName, zoneName, locationName, privateSkuName, privateCategoryPath) } // updateInboundPrivateStockId 更新入库记录关联的库存ID func (s *purchaseInbound) updateInboundPrivateStockId(ctx context.Context, inboundId, privateStockId *bson.ObjectID) (err error) { return daoProcurement.PurchaseInbound.UpdatePrivateStockId(ctx, inboundId, privateStockId) }