Dockerfile
This commit is contained in:
399
service/stock/inventory_count_detail_service.go
Normal file
399
service/stock/inventory_count_detail_service.go
Normal file
@@ -0,0 +1,399 @@
|
||||
// 盘点明细服务
|
||||
// 职责:盘点明细CRUD、库存调整(adjustStock使用$inc原子操作)、统计更新、相似商品查询
|
||||
// 调用链:InventoryCount.Import → adjustStock → validateStockAfterAdjust → autoCompleteIfNoDifference
|
||||
// 紧密耦合:dao.InventoryCountDetail、PrivateStock(调整库存)、InventoryCount(更新统计)
|
||||
// 注意:AssetSkuID字段实际存储的是privateSkuId,列表查询使用批量填充避免N+1
|
||||
package service
|
||||
|
||||
import (
|
||||
"assets/consts/public"
|
||||
"assets/consts/stock"
|
||||
dao "assets/dao/stock"
|
||||
dto "assets/model/dto/stock"
|
||||
entity "assets/model/entity/stock"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"gitea.com/red-future/common/db/mongo"
|
||||
"gitea.com/red-future/common/utils"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type inventoryCountDetail struct{}
|
||||
|
||||
var InventoryCountDetail = new(inventoryCountDetail)
|
||||
|
||||
func (s *inventoryCountDetail) Create(ctx context.Context, req *dto.CreateInventoryCountDetailReq) (res *dto.CreateInventoryCountDetailRes, err error) {
|
||||
ids, err := dao.InventoryCountDetail.Insert(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
id := ids[0].(bson.ObjectID)
|
||||
res = &dto.CreateInventoryCountDetailRes{
|
||||
Id: &id,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *inventoryCountDetail) Update(ctx context.Context, req *dto.UpdateInventoryCountDetailReq) error {
|
||||
return dao.InventoryCountDetail.Update(ctx, req)
|
||||
}
|
||||
|
||||
func (s *inventoryCountDetail) Delete(ctx context.Context, req *dto.DeleteInventoryCountDetailReq) error {
|
||||
return dao.InventoryCountDetail.DeleteFake(ctx, req)
|
||||
}
|
||||
|
||||
func (s *inventoryCountDetail) GetOne(ctx context.Context, req *dto.GetInventoryCountDetailReq) (res *dto.GetInventoryCountDetailRes, err error) {
|
||||
one, err := dao.InventoryCountDetail.GetOne(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = utils.Struct(one, &res)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *inventoryCountDetail) List(ctx context.Context, req *dto.ListInventoryCountDetailReq) (res *dto.ListInventoryCountDetailRes, err error) {
|
||||
list, total, err := dao.InventoryCountDetail.List(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = &dto.ListInventoryCountDetailRes{
|
||||
Total: total,
|
||||
}
|
||||
err = utils.Struct(list, &res.List)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 批量查询关联名称(避免N+1查询)
|
||||
s.fillListItemNames(ctx, list, res.List)
|
||||
return
|
||||
}
|
||||
|
||||
// fillListItemNames 批量填充列表项的关联名称
|
||||
func (s *inventoryCountDetail) fillListItemNames(ctx context.Context, details []entity.InventoryCountDetail, items []dto.InventoryCountDetailListItem) {
|
||||
if len(details) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// 1. 收集所有需要查询的ID(去重)
|
||||
assetIdSet := make(map[string]*bson.ObjectID)
|
||||
skuIdSet := make(map[string]*bson.ObjectID)
|
||||
warehouseIdSet := make(map[string]*bson.ObjectID)
|
||||
zoneIdSet := make(map[string]*bson.ObjectID)
|
||||
locationIdSet := make(map[string]*bson.ObjectID)
|
||||
|
||||
for _, d := range details {
|
||||
if d.AssetID != nil {
|
||||
assetIdSet[d.AssetID.Hex()] = d.AssetID
|
||||
}
|
||||
if d.AssetSkuID != nil {
|
||||
skuIdSet[d.AssetSkuID.Hex()] = d.AssetSkuID
|
||||
}
|
||||
if d.WarehouseID != nil {
|
||||
warehouseIdSet[d.WarehouseID.Hex()] = d.WarehouseID
|
||||
}
|
||||
if d.ZoneID != nil {
|
||||
zoneIdSet[d.ZoneID.Hex()] = d.ZoneID
|
||||
}
|
||||
if d.LocationID != nil {
|
||||
locationIdSet[d.LocationID.Hex()] = d.LocationID
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 批量查询asset名称
|
||||
assetNameMap := make(map[string]string)
|
||||
if len(assetIdSet) > 0 {
|
||||
ids := make([]*bson.ObjectID, 0, len(assetIdSet))
|
||||
for _, id := range assetIdSet {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
var assets []struct {
|
||||
Id *bson.ObjectID `bson:"_id"`
|
||||
Name string `bson:"assetName"`
|
||||
}
|
||||
if _, e := mongo.DB().Find(ctx, bson.M{"_id": bson.M{"$in": ids}}, &assets, public.AssetCollection, nil, nil); e == nil {
|
||||
for _, a := range assets {
|
||||
assetNameMap[a.Id.Hex()] = a.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 批量查询private_sku名称(AssetSkuID实际存的是privateSkuId)
|
||||
skuNameMap := make(map[string]string)
|
||||
if len(skuIdSet) > 0 {
|
||||
ids := make([]*bson.ObjectID, 0, len(skuIdSet))
|
||||
for _, id := range skuIdSet {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
var skus []struct {
|
||||
Id *bson.ObjectID `bson:"_id"`
|
||||
Name string `bson:"skuName"`
|
||||
}
|
||||
if _, e := mongo.DB().Find(ctx, bson.M{"_id": bson.M{"$in": ids}}, &skus, public.PrivateSkuCollection, nil, nil); e == nil {
|
||||
for _, s := range skus {
|
||||
skuNameMap[s.Id.Hex()] = s.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 批量查询warehouse名称
|
||||
warehouseNameMap := make(map[string]string)
|
||||
if len(warehouseIdSet) > 0 {
|
||||
ids := make([]*bson.ObjectID, 0, len(warehouseIdSet))
|
||||
for _, id := range warehouseIdSet {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
var warehouses []struct {
|
||||
Id *bson.ObjectID `bson:"_id"`
|
||||
Name string `bson:"warehouseName"`
|
||||
}
|
||||
if _, e := mongo.DB().Find(ctx, bson.M{"_id": bson.M{"$in": ids}}, &warehouses, public.WarehouseCollection, nil, nil); e == nil {
|
||||
for _, w := range warehouses {
|
||||
warehouseNameMap[w.Id.Hex()] = w.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 批量查询zone名称
|
||||
zoneNameMap := make(map[string]string)
|
||||
if len(zoneIdSet) > 0 {
|
||||
ids := make([]*bson.ObjectID, 0, len(zoneIdSet))
|
||||
for _, id := range zoneIdSet {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
var zones []struct {
|
||||
Id *bson.ObjectID `bson:"_id"`
|
||||
Name string `bson:"zoneName"`
|
||||
}
|
||||
if _, e := mongo.DB().Find(ctx, bson.M{"_id": bson.M{"$in": ids}}, &zones, public.ZoneCollection, nil, nil); e == nil {
|
||||
for _, z := range zones {
|
||||
zoneNameMap[z.Id.Hex()] = z.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 批量查询location名称
|
||||
locationNameMap := make(map[string]string)
|
||||
if len(locationIdSet) > 0 {
|
||||
ids := make([]*bson.ObjectID, 0, len(locationIdSet))
|
||||
for _, id := range locationIdSet {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
var locations []struct {
|
||||
Id *bson.ObjectID `bson:"_id"`
|
||||
Name string `bson:"locationName"`
|
||||
}
|
||||
if _, e := mongo.DB().Find(ctx, bson.M{"_id": bson.M{"$in": ids}}, &locations, public.LocationCollection, nil, nil); e == nil {
|
||||
for _, l := range locations {
|
||||
locationNameMap[l.Id.Hex()] = l.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 填充名称到列表项
|
||||
for i := range items {
|
||||
if details[i].AssetID != nil {
|
||||
items[i].AssetName = assetNameMap[details[i].AssetID.Hex()]
|
||||
}
|
||||
if details[i].AssetSkuID != nil {
|
||||
items[i].AssetSkuName = skuNameMap[details[i].AssetSkuID.Hex()]
|
||||
}
|
||||
if details[i].WarehouseID != nil {
|
||||
items[i].WarehouseName = warehouseNameMap[details[i].WarehouseID.Hex()]
|
||||
}
|
||||
if details[i].ZoneID != nil {
|
||||
items[i].ZoneName = zoneNameMap[details[i].ZoneID.Hex()]
|
||||
}
|
||||
if details[i].LocationID != nil {
|
||||
items[i].LocationName = locationNameMap[details[i].LocationID.Hex()]
|
||||
}
|
||||
// 填充差异类型文本
|
||||
if details[i].DiscrepancyType != 0 {
|
||||
items[i].DiscrepancyTypeText = details[i].DiscrepancyType.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// adjustStock 原子操作调整库存(更新private_stock表,使用$inc原子加减)
|
||||
func (s *inventoryCountDetail) adjustStock(ctx context.Context, detail *entity.InventoryCountDetail) (err error) {
|
||||
// 使用MongoDB的$inc原子操作(无锁并发安全)
|
||||
// 注意:盘点明细的AssetSkuID字段实际存储的是privateSkuId
|
||||
filter := bson.M{
|
||||
"privateSkuId": detail.AssetSkuID,
|
||||
"warehouseId": detail.WarehouseID,
|
||||
}
|
||||
if !g.IsEmpty(detail.ZoneID) {
|
||||
filter["zoneId"] = detail.ZoneID
|
||||
}
|
||||
if !g.IsEmpty(detail.LocationID) {
|
||||
filter["locationId"] = detail.LocationID
|
||||
}
|
||||
|
||||
update := bson.M{
|
||||
"$inc": bson.M{
|
||||
"availableQty": detail.Difference,
|
||||
},
|
||||
}
|
||||
|
||||
_, err = mongo.DB().Update(ctx, filter, update, public.PrivateStockCollection)
|
||||
return
|
||||
}
|
||||
|
||||
// updateCountStats 更新盘点任务统计信息
|
||||
func (s *inventoryCountDetail) updateCountStats(ctx context.Context, countId *bson.ObjectID) (err error) {
|
||||
details, err := dao.InventoryCountDetail.ListByCountId(ctx, countId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
totalItems := len(details)
|
||||
completedItems := 0
|
||||
discrepancyItems := 0
|
||||
|
||||
for _, detail := range details {
|
||||
if detail.IsAdjusted {
|
||||
completedItems++
|
||||
}
|
||||
if detail.Difference != 0 {
|
||||
discrepancyItems++
|
||||
}
|
||||
}
|
||||
|
||||
err = dao.InventoryCount.UpdateStats(ctx, countId, totalItems, completedItems, discrepancyItems)
|
||||
return
|
||||
}
|
||||
|
||||
// validateStockAfterAdjust 验证调整后库存不能为负数(查询private_stock表)
|
||||
func (s *inventoryCountDetail) validateStockAfterAdjust(ctx context.Context, detail *entity.InventoryCountDetail) (err error) {
|
||||
// 查询当前库存(注意:盘点明细的AssetSkuID字段实际存储的是privateSkuId)
|
||||
filter := bson.M{
|
||||
"privateSkuId": detail.AssetSkuID,
|
||||
"warehouseId": detail.WarehouseID,
|
||||
}
|
||||
if !g.IsEmpty(detail.ZoneID) {
|
||||
filter["zoneId"] = detail.ZoneID
|
||||
}
|
||||
if !g.IsEmpty(detail.LocationID) {
|
||||
filter["locationId"] = detail.LocationID
|
||||
}
|
||||
|
||||
// 聚合同SKU同位置的所有批次总可用数量
|
||||
var stocks []struct {
|
||||
AvailableQty int `bson:"availableQty" json:"availableQty"`
|
||||
}
|
||||
_, err = mongo.DB().Find(ctx, filter, &stocks, public.PrivateStockCollection, nil, nil)
|
||||
if err != nil {
|
||||
if detail.Difference < 0 {
|
||||
err = errors.New("当前库存不存在,无法执行减少操作")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
totalQty := 0
|
||||
for _, s := range stocks {
|
||||
totalQty += s.AvailableQty
|
||||
}
|
||||
|
||||
afterQty := totalQty + detail.Difference
|
||||
if afterQty < 0 {
|
||||
err = fmt.Errorf("调整后库存为负数(当前库存%d,差异%d,结果%d),不允许调整", totalQty, detail.Difference, afterQty)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// autoCompleteIfNoDifference 所有明细都已调整(含无差异自动调整)时,自动完成盘点
|
||||
func (s *inventoryCountDetail) autoCompleteIfNoDifference(ctx context.Context, countId *bson.ObjectID) (err error) {
|
||||
details, err := dao.InventoryCountDetail.ListByCountId(ctx, countId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, detail := range details {
|
||||
// 只要有一条未调整(含未盘点),就不自动完成
|
||||
if !detail.IsAdjusted {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = dao.InventoryCount.UpdateStatus(ctx, countId, stock.InventoryCountStatusCompleted)
|
||||
return
|
||||
}
|
||||
|
||||
// SearchSimilarAssets 查询相似商品(单字模糊匹配)
|
||||
// 用于库存不存在时提示用户可能的相似商品
|
||||
// 流程:先查private_sku模糊匹配skuName → $in查private_stock获取库存 → 关联填充名称
|
||||
func (s *inventoryCountDetail) SearchSimilarAssets(ctx context.Context, req *dto.SearchSimilarAssetsReq) (res *dto.SearchSimilarAssetsRes, err error) {
|
||||
// 1. 单字分词:将关键词拆分为单个字符
|
||||
keywords := []string{}
|
||||
for _, char := range req.Keyword {
|
||||
keywords = append(keywords, string(char))
|
||||
}
|
||||
|
||||
// 2. 查private_sku表模糊匹配skuName
|
||||
orConditions := []bson.M{}
|
||||
for _, keyword := range keywords {
|
||||
orConditions = append(orConditions, bson.M{"skuName": bson.M{"$regex": keyword, "$options": "i"}})
|
||||
}
|
||||
var matchedSkus []struct {
|
||||
ID *bson.ObjectID `bson:"_id"`
|
||||
SkuName string `bson:"skuName"`
|
||||
}
|
||||
_, err = mongo.DB().Find(ctx, bson.M{"$or": orConditions}, &matchedSkus, public.PrivateSkuCollection, nil, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(matchedSkus) == 0 {
|
||||
res = &dto.SearchSimilarAssetsRes{List: []dto.SimilarAssetItem{}}
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 构建skuId列表和名称Map
|
||||
skuIds := make([]*bson.ObjectID, 0, len(matchedSkus))
|
||||
skuNameMap := make(map[string]string, len(matchedSkus))
|
||||
for _, sku := range matchedSkus {
|
||||
skuIds = append(skuIds, sku.ID)
|
||||
skuNameMap[sku.ID.Hex()] = sku.SkuName
|
||||
}
|
||||
|
||||
// 4. $in查private_stock表获取库存
|
||||
stockFilter := bson.M{
|
||||
"privateSkuId": bson.M{"$in": skuIds},
|
||||
"availableQty": bson.M{"$gt": 0},
|
||||
}
|
||||
if !g.IsEmpty(req.WarehouseID) {
|
||||
stockFilter["warehouseId"] = req.WarehouseID
|
||||
}
|
||||
var stocks []struct {
|
||||
PrivateSkuID *bson.ObjectID `bson:"privateSkuId"`
|
||||
AvailableQty int `bson:"availableQty"`
|
||||
WarehouseID *bson.ObjectID `bson:"warehouseId"`
|
||||
WarehouseName string `bson:"warehouseName"`
|
||||
}
|
||||
_, err = mongo.DB().Find(ctx, stockFilter, &stocks, public.PrivateStockCollection, nil, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 5. 转换为响应结构
|
||||
var list []dto.SimilarAssetItem
|
||||
for _, s := range stocks {
|
||||
skuName := ""
|
||||
if s.PrivateSkuID != nil {
|
||||
skuName = skuNameMap[s.PrivateSkuID.Hex()]
|
||||
}
|
||||
list = append(list, dto.SimilarAssetItem{
|
||||
AssetSkuID: s.PrivateSkuID,
|
||||
AssetSkuName: skuName,
|
||||
AvailableQty: s.AvailableQty,
|
||||
WarehouseID: s.WarehouseID,
|
||||
WarehouseName: s.WarehouseName,
|
||||
})
|
||||
}
|
||||
|
||||
res = &dto.SearchSimilarAssetsRes{List: list}
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user