Files
assets/service/procurement/purchase_inbound_service.go
2026-03-18 10:18:03 +08:00

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