Files
assets/service/stock/private_stock_service.go
2026-03-18 10:18:03 +08:00

306 lines
9.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 实物库存批次服务
// 职责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
}