// 实物库存批次服务 // 职责:CRUD、移库(MoveStock库位间)、调拨(TransferStock仓库间)、出库(Outbound) // 调用链:Controller → Create/Update/Delete → Capacity.UpdateLocationCapacity(容量重算) // 紧密耦合:dao.PrivateStock、dao.Warehouse/Zone/Location(获取名称)、Capacity(容量更新) // 注意:区别于StockDetails/StockBatch的逻辑库存,PrivateStock记录实际存放位置 package service import ( "assets/consts/public" "assets/consts/stock" dao "assets/dao/stock" dto "assets/model/dto/stock" "context" "github.com/gogf/gf/v2/os/gtime" "gitea.com/red-future/common/db/mongo" "gitea.com/red-future/common/utils" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "go.mongodb.org/mongo-driver/v2/bson" ) type privateStock struct{} // PrivateStock 实物库存批次服务 // 职责: // 1. CRUD:创建、更新、删除、查询实物库存批次 // 2. 移库(MoveStock):库位间移动 // 3. 调拨(TransferStock):仓库间调拨 // 4. 出库(Outbound):减少库存数量 // 特点:记录SKU批次的实际存放位置(仓库/库区/库位)和数量,区别于StockDetails/StockBatch的逻辑库存 var PrivateStock = new(privateStock) func (s *privateStock) Create(ctx context.Context, req *dto.CreatePrivateStockReq) (res *dto.CreatePrivateStockRes, err error) { ids, err := dao.PrivateStock.Insert(ctx, req) if err != nil { return } id := ids[0].(bson.ObjectID) res = &dto.CreatePrivateStockRes{ Id: &id, } // 触发库位容量更新 if req.LocationId != nil && !req.LocationId.IsZero() { if err := Capacity.UpdateLocationCapacity(ctx, req.LocationId); err != nil { // 容量更新失败不影响创建 g.Log().Warningf(ctx, "更新库位容量失败: %v", err) } } return } func (s *privateStock) Update(ctx context.Context, req *dto.UpdatePrivateStockReq) error { // 查询库存信息获取LocationId stock, err := dao.PrivateStock.GetOne(ctx, &dto.GetPrivateStockReq{Id: req.Id}) if err != nil { return err } err = dao.PrivateStock.Update(ctx, req) if err != nil { return err } // 触发库位容量更新 if stock.LocationID != nil && !stock.LocationID.IsZero() { if err := Capacity.UpdateLocationCapacity(ctx, stock.LocationID); err != nil { g.Log().Warningf(ctx, "更新库位容量失败: %v", err) } } return nil } func (s *privateStock) Delete(ctx context.Context, req *dto.DeletePrivateStockReq) error { // 查询库存信息获取LocationId(用于删除后更新容量) stockInfo, err := dao.PrivateStock.GetOne(ctx, &dto.GetPrivateStockReq{Id: req.Id}) if err != nil { return err } if err := dao.PrivateStock.DeleteFake(ctx, req); err != nil { return err } // 触发库位容量更新 if stockInfo.LocationID != nil && !stockInfo.LocationID.IsZero() { if capErr := Capacity.UpdateLocationCapacity(ctx, stockInfo.LocationID); capErr != nil { g.Log().Warningf(ctx, "删除库存后更新库位容量失败: %v", capErr) } } return nil } func (s *privateStock) GetOne(ctx context.Context, req *dto.GetPrivateStockReq) (res *dto.GetPrivateStockRes, err error) { one, err := dao.PrivateStock.GetOne(ctx, req) if err != nil { return } err = utils.Struct(one, &res) return } func (s *privateStock) List(ctx context.Context, req *dto.ListPrivateStockReq) (res *dto.ListPrivateStockRes, err error) { list, total, err := dao.PrivateStock.List(ctx, req) if err != nil { return } res = &dto.ListPrivateStockRes{ Total: total, } err = utils.Struct(list, &res.List) return } // MoveStock 移库(库位间移动) func (s *privateStock) MoveStock(ctx context.Context, req *dto.MoveStockReq) error { // 只支持PrivateStock移库,StockDetails/StockBatch是逻辑库存无位置信息 if req.StockType != stock.StockLocationTypePrivateStock { return gerror.New("移库操作仅支持实物库存批次(PrivateStock),明细和批次库存为逻辑库存无位置信息") } // 验证源库位和目标库位不能相同 if req.FromLocationId.Hex() == req.ToLocationId.Hex() { return gerror.New("源库位和目标库位不能相同") } // 验证库存是否存在且位置匹配 privateStock, err := dao.PrivateStock.GetOne(ctx, &dto.GetPrivateStockReq{Id: req.StockId}) if err != nil { return err } if privateStock.LocationID == nil || privateStock.LocationID.Hex() != req.FromLocationId.Hex() { return gerror.New("库存当前位置与源库位不匹配") } // 获取目标库位信息 toLocation, err := dao.Location.GetOne(ctx, &dto.GetLocationReq{Id: req.ToLocationId}) if err != nil { return err } // 更新库存的库位信息 filter := bson.M{"_id": req.StockId} update := bson.M{ "$set": bson.M{ "locationId": req.ToLocationId, "locationCode": toLocation.LocationCode, "locationName": toLocation.LocationName, "locationType": toLocation.LocationType, "lastMovedAt": gtime.Now(), }, } _, err = mongo.DB().Update(ctx, filter, update, public.PrivateStockCollection) if err != nil { return err } // 触发源库位和目标库位容量更新 if req.FromLocationId != nil && !req.FromLocationId.IsZero() { if err := Capacity.UpdateLocationCapacity(ctx, req.FromLocationId); err != nil { g.Log().Warningf(ctx, "更新源库位容量失败: %v", err) } } if req.ToLocationId != nil && !req.ToLocationId.IsZero() { if err := Capacity.UpdateLocationCapacity(ctx, req.ToLocationId); err != nil { g.Log().Warningf(ctx, "更新目标库位容量失败: %v", err) } } return nil } // TransferStock 调拨(仓库间调拨) func (s *privateStock) TransferStock(ctx context.Context, req *dto.TransferStockReq) error { // 只支持PrivateStock调拨,StockDetails/StockBatch是逻辑库存无位置信息 if req.StockType != stock.StockLocationTypePrivateStock { return gerror.New("调拨操作仅支持实物库存批次(PrivateStock),明细和批次库存为逻辑库存无位置信息") } // 验证源仓库和目标仓库不能相同 if req.FromWarehouseId.Hex() == req.ToWarehouseId.Hex() { return gerror.New("源仓库和目标仓库不能相同") } // 验证库存是否存在且仓库匹配 privateStock, err := dao.PrivateStock.GetOne(ctx, &dto.GetPrivateStockReq{Id: req.StockId}) if err != nil { return err } if privateStock.WarehouseID == nil || privateStock.WarehouseID.Hex() != req.FromWarehouseId.Hex() { return gerror.New("库存当前仓库与源仓库不匹配") } // 获取目标仓库信息 toWarehouse, err := dao.Warehouse.GetOne(ctx, &dto.GetWarehouseReq{Id: req.ToWarehouseId}) if err != nil { return err } // 构建更新字段 updateFields := bson.M{ "warehouseId": req.ToWarehouseId, "warehouseCode": toWarehouse.WarehouseCode, "warehouseName": toWarehouse.WarehouseName, "lastMovedAt": gtime.Now(), } // 未指定目标库区/库位时清空旧值,避免残留指向源仓库 unsetFields := bson.M{} // 如果指定了目标库区 if req.ToZoneId != nil && !req.ToZoneId.IsZero() { toZone, err := dao.Zone.GetOne(ctx, &dto.GetZoneReq{Id: req.ToZoneId}) if err != nil { return err } updateFields["zoneId"] = req.ToZoneId updateFields["zoneCode"] = toZone.ZoneCode updateFields["zoneName"] = toZone.ZoneName updateFields["zoneType"] = toZone.ZoneType } else { unsetFields["zoneId"] = "" unsetFields["zoneCode"] = "" unsetFields["zoneName"] = "" unsetFields["zoneType"] = "" } // 如果指定了目标库位 if req.ToLocationId != nil && !req.ToLocationId.IsZero() { toLocation, err := dao.Location.GetOne(ctx, &dto.GetLocationReq{Id: req.ToLocationId}) if err != nil { return err } updateFields["locationId"] = req.ToLocationId updateFields["locationCode"] = toLocation.LocationCode updateFields["locationName"] = toLocation.LocationName updateFields["locationType"] = toLocation.LocationType } else { unsetFields["locationId"] = "" unsetFields["locationCode"] = "" unsetFields["locationName"] = "" unsetFields["locationType"] = "" } filter := bson.M{"_id": req.StockId} update := bson.M{"$set": updateFields} if len(unsetFields) > 0 { update["$unset"] = unsetFields } _, err = mongo.DB().Update(ctx, filter, update, public.PrivateStockCollection) if err != nil { return err } // 触发库位容量更新(调拨后涉及库位变化时) // 更新源库位 if privateStock.LocationID != nil && !privateStock.LocationID.IsZero() { if err := Capacity.UpdateLocationCapacity(ctx, privateStock.LocationID); err != nil { g.Log().Warningf(ctx, "更新源库位容量失败: %v", err) } } // 更新目标库位 if req.ToLocationId != nil && !req.ToLocationId.IsZero() { if err := Capacity.UpdateLocationCapacity(ctx, req.ToLocationId); err != nil { g.Log().Warningf(ctx, "更新目标库位容量失败: %v", err) } } return nil } // Outbound 实物库存批次出库 func (s *privateStock) Outbound(ctx context.Context, req *dto.OutboundPrivateStockReq) error { // 只支持PrivateStock出库,StockDetails/StockBatch是逻辑库存无位置信息 if req.StockType != stock.StockLocationTypePrivateStock { return gerror.New("出库操作仅支持实物库存批次(PrivateStock),明细和批次库存为逻辑库存无位置信息") } // 验证库存是否存在 privateStock, err := dao.PrivateStock.GetOne(ctx, &dto.GetPrivateStockReq{Id: req.StockId}) if err != nil { return err } // 验证可用数量是否足够 if privateStock.AvailableQty < req.OutboundQty { return gerror.Newf("可用库存不足:当前可用%d,需要出库%d", privateStock.AvailableQty, req.OutboundQty) } // 使用IncrementAvailableQty原子更新(传负数表示减少) err = dao.PrivateStock.IncrementAvailableQty(ctx, req.StockId, -req.OutboundQty) if err != nil { return err } // 触发库位容量更新 if privateStock.LocationID != nil && !privateStock.LocationID.IsZero() { if err := Capacity.UpdateLocationCapacity(ctx, privateStock.LocationID); err != nil { g.Log().Warningf(ctx, "更新库位容量失败: %v", err) } } return nil }