Dockerfile

This commit is contained in:
2026-03-18 10:18:03 +08:00
parent 5c5dbc7420
commit b65f3439f3
189 changed files with 19027 additions and 0 deletions

View 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
}