// 盘点明细服务 // 职责:盘点明细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 }