Files
assets/dao/stock/private_stock_dao.go
2026-03-18 10:18:03 +08:00

269 lines
8.2 KiB
Go
Raw 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.
// 实物库存批次DAO层
// 职责CRUD、IncrementAvailableQty原子操作(防并发超卖)、SumAvailableQtyByLocation聚合汇总
// 紧密耦合service.PrivateStock、service.Capacity(容量计算入口)
// 注意IncrementAvailableQty使用$inc+$gte条件防止并发导致库存变负
package dao
import (
"assets/consts/public"
"assets/consts/stock"
dto "assets/model/dto/stock"
entity "assets/model/entity/stock"
"context"
"gitea.com/red-future/common/beans"
"gitea.com/red-future/common/db/mongo"
"gitea.com/red-future/common/utils"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"go.mongodb.org/mongo-driver/v2/bson"
)
var PrivateStock = new(privateStock)
type privateStock struct {
}
// Insert 插入私域库存
func (d *privateStock) Insert(ctx context.Context, req *dto.CreatePrivateStockReq) (ids []interface{}, err error) {
var result *entity.PrivateStock
if err = utils.Struct(req, &result); err != nil {
return
}
// 设置StockType如果未指定默认为PrivateStock类型
if !g.IsEmpty(req.StockType) {
result.StockType = req.StockType
} else {
result.StockType = stock.StockLocationTypePrivateStock
}
// 获取仓库信息(非必填)
if req.WarehouseId != nil && !req.WarehouseId.IsZero() {
warehouse, err := Warehouse.GetOne(ctx, &dto.GetWarehouseReq{Id: req.WarehouseId})
if err != nil {
return nil, err
}
result.WarehouseID = req.WarehouseId
result.WarehouseCode = warehouse.WarehouseCode
result.WarehouseName = warehouse.WarehouseName
}
// 如果有库区信息
if req.ZoneId != nil && !req.ZoneId.IsZero() {
zone, err := Zone.GetOne(ctx, &dto.GetZoneReq{Id: req.ZoneId})
if err != nil {
return nil, err
}
result.ZoneID = req.ZoneId
result.ZoneCode = zone.ZoneCode
result.ZoneName = zone.ZoneName
result.ZoneType = zone.ZoneType
}
// 如果有库位信息
if req.LocationId != nil && !req.LocationId.IsZero() {
location, err := Location.GetOne(ctx, &dto.GetLocationReq{Id: req.LocationId})
if err != nil {
return nil, err
}
result.LocationID = req.LocationId
result.LocationCode = location.LocationCode
result.LocationName = location.LocationName
result.LocationType = location.LocationType
}
ids, err = mongo.DB().Insert(ctx, []interface{}{&result}, public.PrivateStockCollection)
return
}
// Update 更新私域库存
func (d *privateStock) Update(ctx context.Context, req *dto.UpdatePrivateStockReq) (err error) {
filter := bson.M{"_id": req.Id}
update := bson.M{"$set": bson.M{}}
if req.WarehouseId != nil && !req.WarehouseId.IsZero() {
warehouse, err := Warehouse.GetOne(ctx, &dto.GetWarehouseReq{Id: req.WarehouseId})
if err != nil {
return err
}
update["$set"].(bson.M)["warehouseId"] = req.WarehouseId
update["$set"].(bson.M)["warehouseCode"] = warehouse.WarehouseCode
update["$set"].(bson.M)["warehouseName"] = warehouse.WarehouseName
}
if req.ZoneId != nil && !req.ZoneId.IsZero() {
zone, err := Zone.GetOne(ctx, &dto.GetZoneReq{Id: req.ZoneId})
if err != nil {
return err
}
update["$set"].(bson.M)["zoneId"] = req.ZoneId
update["$set"].(bson.M)["zoneCode"] = zone.ZoneCode
update["$set"].(bson.M)["zoneName"] = zone.ZoneName
update["$set"].(bson.M)["zoneType"] = zone.ZoneType
}
if req.LocationId != nil && !req.LocationId.IsZero() {
location, err := Location.GetOne(ctx, &dto.GetLocationReq{Id: req.LocationId})
if err != nil {
return err
}
update["$set"].(bson.M)["locationId"] = req.LocationId
update["$set"].(bson.M)["locationCode"] = location.LocationCode
update["$set"].(bson.M)["locationName"] = location.LocationName
update["$set"].(bson.M)["locationType"] = location.LocationType
}
if req.PrivateSkuID != nil {
update["$set"].(bson.M)["privateSkuId"] = req.PrivateSkuID
}
if !g.IsEmpty(req.BatchNo) {
update["$set"].(bson.M)["batchNo"] = req.BatchNo
}
if req.BatchQty > 0 {
update["$set"].(bson.M)["batchQty"] = req.BatchQty
}
// AvailableQty已移除全量覆盖请使用IncrementAvailableQty方法进行增量更新
if req.BatchStatus != nil {
update["$set"].(bson.M)["batchStatus"] = req.BatchStatus
}
if req.StockStatus != nil {
update["$set"].(bson.M)["stockStatus"] = req.StockStatus
}
if req.SupplierID != nil {
update["$set"].(bson.M)["supplierId"] = req.SupplierID
}
if req.SupportsRecycle != nil {
update["$set"].(bson.M)["supportsRecycle"] = *req.SupportsRecycle
}
if req.ProductionDate != nil {
update["$set"].(bson.M)["productionDate"] = req.ProductionDate
}
if req.ExpiryDate != nil {
update["$set"].(bson.M)["expiryDate"] = req.ExpiryDate
}
if req.ExpiryWarningDate != nil {
update["$set"].(bson.M)["expiryWarningDate"] = req.ExpiryWarningDate
}
if !g.IsEmpty(req.PrivateCategoryPath) {
update["$set"].(bson.M)["privateCategoryPath"] = req.PrivateCategoryPath
}
if !g.IsEmpty(req.StockType) {
update["$set"].(bson.M)["stockType"] = req.StockType
}
_, err = mongo.DB().Update(ctx, filter, update, public.PrivateStockCollection)
return
}
// DeleteFake 软删除私域库存
func (d *privateStock) DeleteFake(ctx context.Context, req *dto.DeletePrivateStockReq) error {
filter := bson.M{"_id": req.Id}
_, err := mongo.DB().DeleteSoft(ctx, filter, public.PrivateStockCollection)
return err
}
// GetOne 根据ID查询私域库存
func (d *privateStock) GetOne(ctx context.Context, req *dto.GetPrivateStockReq) (res *entity.PrivateStock, err error) {
filter := bson.M{"_id": req.Id}
err = mongo.DB().FindOne(ctx, filter, &res, public.PrivateStockCollection)
return
}
// IncrementAvailableQty 增量更新可用数量(并发安全)
// deltaQty正数表示增加入库负数表示减少出库
// 减少时自动添加 availableQty >= |deltaQty| 条件,防止并发导致库存变负
func (d *privateStock) IncrementAvailableQty(ctx context.Context, id *bson.ObjectID, deltaQty int) (err error) {
filter := bson.M{"_id": id}
if deltaQty < 0 {
filter["availableQty"] = bson.M{"$gte": -deltaQty}
}
update := bson.M{
"$inc": bson.M{
"availableQty": deltaQty,
},
}
modifiedCount, err := mongo.DB().Update(ctx, filter, update, public.PrivateStockCollection)
if err != nil {
return
}
if deltaQty < 0 && modifiedCount == 0 {
err = gerror.Newf("库存不足,无法减少%d", -deltaQty)
}
return
}
// List 查询私域库存列表
func (d *privateStock) List(ctx context.Context, req *dto.ListPrivateStockReq) (res []entity.PrivateStock, total int64, err error) {
filter := bson.M{}
if req.WarehouseId != nil && !req.WarehouseId.IsZero() {
filter["warehouseId"] = req.WarehouseId
}
if req.ZoneId != nil && !req.ZoneId.IsZero() {
filter["zoneId"] = req.ZoneId
}
if req.LocationId != nil && !req.LocationId.IsZero() {
filter["locationId"] = req.LocationId
}
if req.PrivateSkuID != nil && !req.PrivateSkuID.IsZero() {
filter["privateSkuId"] = req.PrivateSkuID
}
if req.BatchStatus != nil {
filter["batchStatus"] = req.BatchStatus
}
if req.StockStatus != nil {
filter["stockStatus"] = req.StockStatus
}
if req.SupplierID != nil && !req.SupplierID.IsZero() {
filter["supplierId"] = req.SupplierID
}
if !g.IsEmpty(req.PrivateCategoryPath) {
filter["privateCategoryPath"] = req.PrivateCategoryPath
}
if !g.IsEmpty(req.StockType) {
filter["stockType"] = req.StockType
}
// 默认排序
if len(req.OrderBy) == 0 {
req.OrderBy = []beans.OrderBy{
{Field: "createdAt", Order: beans.Desc},
}
}
total, err = mongo.DB().Find(ctx, filter, &res, public.PrivateStockCollection, req.Page, req.OrderBy)
return
}
// SumAvailableQtyByLocation 按库位ID汇总所有库存的可用数量使用MongoDB聚合管道
func (d *privateStock) SumAvailableQtyByLocation(ctx context.Context, locationId *bson.ObjectID) (totalQty int, err error) {
pipeline := bson.A{
bson.M{"$match": bson.M{
"locationId": locationId,
"isDeleted": false,
}},
bson.M{"$group": bson.M{
"_id": nil,
"totalQty": bson.M{"$sum": "$availableQty"},
}},
}
coll := mongo.GetDB().Collection(public.PrivateStockCollection)
cursor, err := coll.Aggregate(ctx, pipeline)
if err != nil {
return
}
defer cursor.Close(ctx)
var results []struct {
TotalQty int `bson:"totalQty"`
}
if err = cursor.All(ctx, &results); err != nil {
return
}
if len(results) > 0 {
totalQty = results[0].TotalQty
}
return
}