Dockerfile

This commit is contained in:
2026-03-18 10:18:03 +08:00
parent 5c5dbc7420
commit b65f3439f3
189 changed files with 19027 additions and 0 deletions

View File

@@ -0,0 +1,305 @@
// 实物库存批次服务
// 职责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
}