Files
assets/service/procurement/purchase_inbound_service.go

202 lines
7.0 KiB
Go
Raw Normal View History

2026-03-18 10:18:03 +08:00
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)
}