202 lines
7.0 KiB
Go
202 lines
7.0 KiB
Go
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)
|
||
}
|