306 lines
9.9 KiB
Go
306 lines
9.9 KiB
Go
// 实物库存批次服务
|
||
// 职责: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
|
||
}
|