Dockerfile
This commit is contained in:
176
service/asset/asset_service.go
Normal file
176
service/asset/asset_service.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"assets/consts/stock"
|
||||
dao "assets/dao/asset"
|
||||
"assets/dao/base"
|
||||
dto "assets/model/dto/asset"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"gitea.com/red-future/common/http"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
|
||||
"gitea.com/red-future/common/beans"
|
||||
"gitea.com/red-future/common/minio"
|
||||
"gitea.com/red-future/common/utils"
|
||||
)
|
||||
|
||||
type asset struct{}
|
||||
|
||||
// Asset 资产服务
|
||||
var Asset = new(asset)
|
||||
|
||||
// Create 创建资产
|
||||
func (s *asset) Create(ctx context.Context, req *dto.CreateAssetReq) (res *dto.CreateAssetRes, err error) {
|
||||
count, err := dao.Asset.Count(ctx, &dto.ListAssetReq{Name: req.Name})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if count > 0 {
|
||||
return nil, errors.New("资产名称已存在")
|
||||
}
|
||||
// 检查是否是超级管理员
|
||||
isSuperAdmin := false
|
||||
// 获取当前请求的 headers 并传递到下游
|
||||
headers := make(map[string]string)
|
||||
if r := g.RequestFromCtx(ctx); r != nil {
|
||||
for k, v := range r.Request.Header {
|
||||
if len(v) > 0 {
|
||||
headers[k] = v[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
if err = http.Get(ctx, "admin-go/api/v1/system/user/checkIsSuperAdmin", headers, &isSuperAdmin); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !isSuperAdmin {
|
||||
req.StockMode = stock.StockModeDetail
|
||||
|
||||
//var getUserInfo beans.User
|
||||
//getUserInfo, err = utils.GetUserInfo(ctx)
|
||||
//if err != nil {
|
||||
// return
|
||||
//}
|
||||
//var get *gvar.Var
|
||||
//get, err = message.GetRedisClientTest("test").Get(ctx, fmt.Sprintf("module_tenant:tenantId-%v", getUserInfo.TenantId))
|
||||
//if err != nil {
|
||||
// return
|
||||
//}
|
||||
//if !g.IsEmpty(get.String()) {
|
||||
// list := new(beans.ModuleTenant)
|
||||
// if err = json.Unmarshal(get.Bytes(), &list); err != nil {
|
||||
// return
|
||||
// }
|
||||
// req.TenantModuleType = list.TenantModuleType
|
||||
//} else {
|
||||
// moduleTenantRes := new(beans.ModuleTenant)
|
||||
// err = message.CallRPC(ctx, "moduleService.AddRedisByTenantId", map[string]interface{}{"tenantId": getUserInfo.TenantId}, moduleTenantRes)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// if !g.IsEmpty(moduleTenantRes.TenantModuleType) {
|
||||
// req.TenantModuleType = moduleTenantRes.TenantModuleType
|
||||
// } else {
|
||||
// return nil, errors.New("您未开通此模块,请开通后再使用")
|
||||
// }
|
||||
//}
|
||||
} else {
|
||||
req.TenantModuleType = beans.TenantModuleTypePlatform
|
||||
}
|
||||
|
||||
// 插入数据库
|
||||
id, err := dao.Asset.Insert(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = &dto.CreateAssetRes{
|
||||
Id: gconv.Uint64(id),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// List 获取资产列表
|
||||
func (s *asset) List(ctx context.Context, req *dto.ListAssetReq) (res *dto.ListAssetRes, err error) {
|
||||
assetList, total, err := dao.Asset.List(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
user, err := utils.GetUserInfo(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fmt.Println(user)
|
||||
res = &dto.ListAssetRes{
|
||||
Total: total,
|
||||
}
|
||||
err = utils.Struct(assetList, &res.List)
|
||||
return
|
||||
}
|
||||
|
||||
// GetOne 获取单个资产
|
||||
func (s *asset) GetOne(ctx context.Context, req *dto.GetAssetReq) (res *dto.GetAssetRes, err error) {
|
||||
assetOne, err := dao.Asset.GetOne(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// TODO: CategoryId类型不匹配,需要同步修改category为uint64
|
||||
// getCategoryRes, err := dao.Category.GetOne(ctx, &dto.GetCategoryReq{
|
||||
// Id: assetOne.CategoryId,
|
||||
// })
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
return &dto.GetAssetRes{
|
||||
Asset: assetOne,
|
||||
CategoryName: "", // getCategoryRes.Name,
|
||||
ImgAddressPrefix: minio.GetFileAddressPrefix(ctx),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetAssetAndSku 获取资产和Sku详情
|
||||
func (s *asset) GetAssetAndSku(ctx context.Context, req *dto.GetAssetAndSkuReq) (res *dto.GetAssetAndSkuRes, err error) {
|
||||
// 跳过租户ID过滤获取资产
|
||||
// TODO: AssetId 类型不匹配,bson.ObjectID 需要转换为 uint64
|
||||
// 使用 SkipTenantId 跳过租户ID过滤
|
||||
assetOne, err := dao.Asset.GetOneById(base.SkipTenantId(ctx), 0)
|
||||
_ = req.AssetId
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// TODO: AssetId类型不匹配,需要适配
|
||||
// moduleType, err := service.Enum.GetTenantModuleType(ctx, &enumDto.GetTenantModuleTypeReq{AssetId: req.AssetId.Hex()})
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// TODO: AssetId类型不匹配,需要同步修改AssetSku为uint64
|
||||
// skus, _, err := dao.AssetSku.List(ctx, &dto.ListAssetSkuReq{AssetId: req.AssetId}, true)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
return &dto.GetAssetAndSkuRes{
|
||||
Asset: assetOne,
|
||||
Skus: nil, // skus,
|
||||
TenantModuleType: nil, // moduleType.Options,
|
||||
ImgAddressPrefix: minio.GetFileAddressPrefix(ctx),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Update 更新资产
|
||||
func (s *asset) Update(ctx context.Context, req *dto.UpdateAssetReq) error {
|
||||
return dao.Asset.Update(ctx, req)
|
||||
}
|
||||
|
||||
// UpdateStatus 更新资产状态
|
||||
func (s *asset) UpdateStatus(ctx context.Context, req *dto.UpdateAssetStatusReq) (err error) {
|
||||
var updateReq *dto.UpdateAssetReq
|
||||
err = utils.Struct(req, &updateReq)
|
||||
return dao.Asset.Update(ctx, updateReq)
|
||||
}
|
||||
|
||||
// Delete 删除资产
|
||||
func (s *asset) Delete(ctx context.Context, req *dto.DeleteAssetReq) error {
|
||||
return dao.Asset.DeleteFake(ctx, req)
|
||||
}
|
||||
191
service/asset/asset_sku_service.go
Normal file
191
service/asset/asset_sku_service.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"assets/consts/public"
|
||||
dao "assets/dao/asset"
|
||||
dto "assets/model/dto/asset"
|
||||
entity "assets/model/entity/asset"
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
|
||||
"gitea.com/red-future/common/beans"
|
||||
"gitea.com/red-future/common/utils"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type assetSku struct{}
|
||||
|
||||
var AssetSku = new(assetSku)
|
||||
|
||||
// CreateAssetSku 创建SKU
|
||||
func (s *assetSku) CreateAssetSku(ctx context.Context, req *dto.CreateAssetSkuReq) (res *dto.CreateAssetSkuRes, err error) {
|
||||
// 验证资产是否存在, 如果存在,则获取资产信息(用于下面验证,自定义属性传过来的全不全)
|
||||
assetEntity, err := dao.Asset.GetOne(ctx, &dto.GetAssetReq{Id: req.AssetId})
|
||||
if err != nil {
|
||||
return nil, errors.New("关联资产不存在")
|
||||
}
|
||||
// 根据资产ID查询SKU列表(用于下面验证,自定义属性和sku名称重不重复)
|
||||
skusList, _, err := dao.AssetSku.List(ctx, &dto.ListAssetSkuReq{AssetId: req.AssetId}, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// 验证参数
|
||||
err = s.parameterValidation(ctx, assetEntity, skusList, req.SkuName, req.SpecValues)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
req.UnlimitedStock = assetEntity.UnlimitedStock
|
||||
req.StockMode = assetEntity.StockMode
|
||||
// TODO: 类型不匹配 uint64 vs *bson.ObjectID
|
||||
// req.CategoryId = assetEntity.CategoryId
|
||||
req.CategoryPath = assetEntity.CategoryPath
|
||||
// TODO: 类型不匹配 string vs beans.TenantModuleType
|
||||
// req.TenantModuleType = assetEntity.TenantModuleType
|
||||
// 插入数据库
|
||||
ids, err := dao.AssetSku.Insert(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
id := ids[0].(bson.ObjectID)
|
||||
res = &dto.CreateAssetSkuRes{
|
||||
Id: &id,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateAssetSku 更新SKU
|
||||
func (s *assetSku) UpdateAssetSku(ctx context.Context, req *dto.UpdateAssetSkuReq) error {
|
||||
getOne, err := dao.AssetSku.GetOne(ctx, &dto.GetAssetSkuReq{Id: req.Id}, false)
|
||||
if err != nil {
|
||||
return errors.New("SUK不存在")
|
||||
}
|
||||
// 根据资产ID查询SKU列表(用于下面验证,自定义属性和sku名称重不重复)
|
||||
skusList, _, err := dao.AssetSku.GetListByAssetIdExcludeCurrentSku(ctx, getOne.AssetId, &dto.ListAssetSkuReq{Id: req.Id, Page: &beans.Page{PageSize: -1}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 验证资产是否存在, 如果存在,则获取资产信息(用于下面验证,自定义属性传过来的全不全)
|
||||
assetEntity, err := dao.Asset.GetOne(ctx, &dto.GetAssetReq{Id: getOne.AssetId})
|
||||
if err != nil {
|
||||
return errors.New("关联资产不存在")
|
||||
}
|
||||
// 验证参数
|
||||
err = s.parameterValidation(ctx, assetEntity, skusList, req.SkuName, req.SpecValues)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 更新数据库
|
||||
return dao.AssetSku.Update(ctx, req)
|
||||
}
|
||||
|
||||
func (s *assetSku) parameterValidation(ctx context.Context, assetEntity *entity.Asset, list []entity.AssetSku, skuName string, specValues []map[string]interface{}) (err error) {
|
||||
_ = ctx
|
||||
|
||||
specNoExist := true
|
||||
metadataList := make([]string, 0)
|
||||
// 验证,自定义属性传过来的全不全
|
||||
// TODO: Metadata类型从[]map[string]interface{}变为*gjson.Json,需要适配
|
||||
// if assetEntity.Metadata != nil {
|
||||
// for _, metadata := range assetEntity.Metadata {
|
||||
// attributeType := gconv.String(gconv.Map(metadata)["type"])
|
||||
// if attributeType == string(consts.AttributeTypeMultiSelect) {
|
||||
// metadataList = append(metadataList, attributeType)
|
||||
// }
|
||||
// }
|
||||
// if len(metadataList) != len(specValues) {
|
||||
// // 如果请求参数中不存在该键,则跳过
|
||||
// return errors.New("规格参数填写不完整")
|
||||
// }
|
||||
// }
|
||||
// 验证,自定义属性和sku名称重不重复
|
||||
for _, list := range list {
|
||||
if list.SkuName == skuName {
|
||||
return errors.New("SKU名称已存在")
|
||||
}
|
||||
if !g.IsEmpty(metadataList) && len(metadataList) > 0 {
|
||||
if !g.IsEmpty(specValues) {
|
||||
exist := true
|
||||
// 遍历请求参数 map
|
||||
for key, subValue := range specValues {
|
||||
// 1. 检查请求参数 map 中是否存在该键
|
||||
for _, subValues := range list.SpecValues {
|
||||
parentValue, ok := subValues[gconv.String(key)]
|
||||
if ok {
|
||||
// 2. 检查对应的值是否相等
|
||||
if reflect.DeepEqual(parentValue, subValue) {
|
||||
exist = false
|
||||
} else {
|
||||
exist = true
|
||||
break
|
||||
}
|
||||
} else {
|
||||
exist = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if exist {
|
||||
specNoExist = true
|
||||
} else {
|
||||
specNoExist = false
|
||||
break
|
||||
}
|
||||
} else {
|
||||
return errors.New("规格参数不能为空")
|
||||
}
|
||||
}
|
||||
}
|
||||
if !specNoExist {
|
||||
return errors.New("规格参数已存在")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteAssetSku 删除SKU(软删除)
|
||||
func (s *assetSku) DeleteAssetSku(ctx context.Context, req *dto.DeleteAssetSkuReq) error {
|
||||
return dao.AssetSku.DeleteFake(ctx, req)
|
||||
}
|
||||
|
||||
// GetAssetSku 获取SKU详情
|
||||
func (s *assetSku) GetAssetSku(ctx context.Context, req *dto.GetAssetSkuReq) (res *dto.GetAssetSkuRes, err error) {
|
||||
one, err := dao.AssetSku.GetOne(ctx, req, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = utils.Struct(one, &res)
|
||||
return
|
||||
}
|
||||
|
||||
// ListAssetSkus 获取SKU列表
|
||||
func (s *assetSku) ListAssetSkus(ctx context.Context, req *dto.ListAssetSkuReq) (res *dto.ListAssetSkuRes, err error) {
|
||||
// 查询数据库
|
||||
list, total, err := dao.AssetSku.List(ctx, req, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = &dto.ListAssetSkuRes{
|
||||
Total: total,
|
||||
}
|
||||
err = utils.Struct(list, &res.List)
|
||||
return
|
||||
}
|
||||
|
||||
// GetAssetSkuModule 获取SKU详情
|
||||
func (s *assetSku) GetAssetSkuModule(ctx context.Context, req *dto.GetAssetSkuModuleReq) (res *dto.GetAssetSkuModuleRes, err error) {
|
||||
one, err := dao.AssetSku.GetOne(ctx, &dto.GetAssetSkuReq{Id: req.Id}, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = &dto.GetAssetSkuModuleRes{}
|
||||
// 计算到期时间
|
||||
if one.SpecsUnit != nil && one.SpecsCount > 0 {
|
||||
durationType := public.DurationType(one.SpecsUnit.Key)
|
||||
res.ExpireAt = durationType.AddTime(one.SpecsCount)
|
||||
res.AssetId = one.AssetId
|
||||
}
|
||||
return
|
||||
}
|
||||
265
service/asset/category_service.go
Normal file
265
service/asset/category_service.go
Normal file
@@ -0,0 +1,265 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
dao "assets/dao/asset"
|
||||
dto "assets/model/dto/asset"
|
||||
entity "assets/model/entity/asset"
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"gitea.com/red-future/common/db/gfdb"
|
||||
"gitea.com/red-future/common/utils"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultPathSeparator 路径分隔符
|
||||
DefaultPathSeparator = "/"
|
||||
)
|
||||
|
||||
// CategoryService 分类服务
|
||||
type CategoryService struct{}
|
||||
|
||||
var Category = new(CategoryService)
|
||||
|
||||
// Create 创建分类
|
||||
func (s *CategoryService) Create(ctx context.Context, req *dto.CreateCategoryReq) (res *dto.CreateCategoryRes, err error) {
|
||||
|
||||
// 构建分类路径和层级
|
||||
parent, err := dao.Category.GetOne(ctx, &dto.GetCategoryReq{Bid: req.ParentId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Path = parent.Path + DefaultPathSeparator + req.ParentId
|
||||
req.Level = parent.Level + 1
|
||||
|
||||
err = gfdb.DB(ctx).Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
var r *entity.Category
|
||||
|
||||
if r, err = dao.Category.Insert(ctx, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新新创建分类的 IsLeafNode 为 true
|
||||
if err = s.updateLeafNode(ctx, r.Bid, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新父节点的 IsLeafNode 为 false
|
||||
if err = s.updateLeafNode(ctx, req.ParentId, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res = &dto.CreateCategoryRes{
|
||||
Bid: r.Bid,
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// updateLeafNode 更新分类的 IsLeafNode 字段
|
||||
func (s *CategoryService) updateLeafNode(ctx context.Context, categoryId string, isLeaf bool) error {
|
||||
return dao.Category.Update(ctx, &dto.UpdateCategoryReq{
|
||||
Bid: categoryId,
|
||||
IsLeafNode: isLeaf,
|
||||
})
|
||||
}
|
||||
|
||||
// GetOne 获取单个分类
|
||||
func (s *CategoryService) GetOne(ctx context.Context, req *dto.GetCategoryReq) (*dto.GetCategoryRes, error) {
|
||||
one, err := dao.Category.GetOne(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := new(dto.GetCategoryRes)
|
||||
err = utils.Struct(one, &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// List 获取分类列表
|
||||
func (s *CategoryService) List(ctx context.Context, req *dto.ListCategoryReq) (*dto.ListCategoryRes, error) {
|
||||
list, total, err := dao.Category.List(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := &dto.ListCategoryRes{Total: total}
|
||||
err = gconv.Struct(list, &res.List)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// GetTree 获取分类树
|
||||
func (s *CategoryService) GetTree(ctx context.Context, req *dto.GetCategoryTreeReq) (*dto.GetCategoryTreeRes, error) {
|
||||
list, _, err := dao.Category.List(ctx, &dto.ListCategoryReq{
|
||||
Keyword: req.Name,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tree, err := s.buildTree(ctx, list, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.GetCategoryTreeRes{Tree: tree}, nil
|
||||
}
|
||||
|
||||
// buildTree 构建树形结构
|
||||
func (s *CategoryService) buildTree(ctx context.Context, categories []entity.Category, parentId string) ([]*dto.CategoryTreeNode, error) {
|
||||
tree := make([]*dto.CategoryTreeNode, 0)
|
||||
for _, cat := range categories {
|
||||
if !s.isChildOf(cat.ParentId, parentId) {
|
||||
continue
|
||||
}
|
||||
node, err := s.convertToTreeNode(ctx, &cat, categories)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tree = append(tree, node)
|
||||
}
|
||||
return tree, nil
|
||||
}
|
||||
|
||||
// isChildOf 判断分类是否为指定父节点的子节点
|
||||
func (s *CategoryService) isChildOf(childParentId, parentId string) bool {
|
||||
if g.IsEmpty(childParentId) && g.IsEmpty(parentId) {
|
||||
return true
|
||||
}
|
||||
if g.IsEmpty(childParentId) || g.IsEmpty(parentId) {
|
||||
return false
|
||||
}
|
||||
return childParentId == parentId
|
||||
}
|
||||
|
||||
// convertToTreeNode 将分类实体转换为树节点
|
||||
func (s *CategoryService) convertToTreeNode(ctx context.Context, category *entity.Category, allCategories []entity.Category) (*dto.CategoryTreeNode, error) {
|
||||
res := new(dto.CategoryTreeNode)
|
||||
if err := gconv.Struct(&category, &res); err != nil {
|
||||
return res, err
|
||||
}
|
||||
children, err := s.buildTree(ctx, allCategories, category.Bid)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
res.Children = children
|
||||
res.IsLeafNode = len(children) == 0
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// hasChildren 检查分类是否有子分类
|
||||
func (s *CategoryService) hasChildren(ctx context.Context, parentId string) (bool, error) {
|
||||
count, err := dao.Category.Count(ctx, &dto.ListCategoryReq{
|
||||
ParentId: parentId,
|
||||
})
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
// updateLeafNodeIfNoChildren 如果父分类没有子分类了,更新为叶子节点
|
||||
func (s *CategoryService) updateLeafNodeIfNoChildren(ctx context.Context, parentId string) error {
|
||||
hasChildren, err := s.hasChildren(ctx, parentId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.updateLeafNode(ctx, parentId, !hasChildren)
|
||||
}
|
||||
|
||||
// UpdateStatus 更新分类状态
|
||||
func (s *CategoryService) UpdateStatus(ctx context.Context, req *dto.UpdateCategoryStatusReq) error {
|
||||
var updateReq *dto.UpdateCategoryReq
|
||||
if err := gconv.Struct(req, &updateReq); err != nil {
|
||||
return err
|
||||
}
|
||||
return dao.Category.Update(ctx, updateReq)
|
||||
}
|
||||
|
||||
// Update 更新分类
|
||||
func (s *CategoryService) Update(ctx context.Context, req *dto.UpdateCategoryReq) error {
|
||||
oldCategory, err := dao.Category.GetOne(ctx, &dto.GetCategoryReq{Id: req.Id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = gfdb.DB(ctx).Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
if err = dao.Category.Update(ctx, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果修改了父分类,需要更新相关节点的 IsLeafNode
|
||||
if s.parentIdChanged(req.ParentId, oldCategory.ParentId) {
|
||||
if !g.IsEmpty(req.ParentId) {
|
||||
if err = s.updateLeafNode(ctx, req.ParentId, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !g.IsEmpty(oldCategory.ParentId) {
|
||||
if err = s.updateLeafNodeIfNoChildren(ctx, oldCategory.ParentId); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// parentIdChanged 判断父分类是否发生变化
|
||||
func (s *CategoryService) parentIdChanged(newParentId, oldParentId string) bool {
|
||||
if g.IsEmpty(newParentId) && g.IsEmpty(oldParentId) {
|
||||
return false
|
||||
}
|
||||
if g.IsEmpty(newParentId) || g.IsEmpty(oldParentId) {
|
||||
return true
|
||||
}
|
||||
return newParentId != oldParentId
|
||||
}
|
||||
|
||||
// Delete 删除分类
|
||||
func (s *CategoryService) Delete(ctx context.Context, req *dto.DeleteCategoryReq) error {
|
||||
// 检查是否有子分类
|
||||
hasChildren, err := s.hasChildren(ctx, req.Bid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hasChildren {
|
||||
return errors.New("该分类下有子分类,无法删除")
|
||||
}
|
||||
|
||||
// 检查是否有资产
|
||||
if count, err := dao.Asset.Count(ctx, &dto.ListAssetReq{
|
||||
CategoryId: req.Bid,
|
||||
}); err != nil {
|
||||
return err
|
||||
} else if count > 0 {
|
||||
return errors.New("该分类下有资产信息,无法删除")
|
||||
}
|
||||
|
||||
// 获取分类信息用于后续操作
|
||||
category, err := dao.Category.GetOne(ctx, &dto.GetCategoryReq{Bid: req.Bid})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = gfdb.DB(ctx).Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
// 删除分类
|
||||
if err := dao.Category.DeleteFake(ctx, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果删除的是叶子节点,更新父节点为叶子节点
|
||||
if !g.IsEmpty(category.ParentId) {
|
||||
return s.updateLeafNodeIfNoChildren(ctx, category.ParentId)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
176
service/asset/private_category_service.go
Normal file
176
service/asset/private_category_service.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
dao "assets/dao/asset"
|
||||
dto "assets/model/dto/asset"
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type privateCategory struct{}
|
||||
|
||||
// PrivateCategory 私域分类服务
|
||||
var PrivateCategory = new(privateCategory)
|
||||
|
||||
// CreatePrivateCategory 创建私域分类
|
||||
func (s *privateCategory) CreatePrivateCategory(ctx context.Context, req *dto.CreatePrivateCategoryReq) (*dto.CreatePrivateCategoryRes, error) {
|
||||
// 自动设置level
|
||||
if !req.ParentID.IsZero() {
|
||||
parentCategory, err := dao.PrivateCategory.GetOne(ctx, req.ParentID)
|
||||
if err == nil && parentCategory != nil {
|
||||
req.Level = parentCategory.Level + 1
|
||||
req.Path = parentCategory.Path + "/" + parentCategory.Id.Hex()
|
||||
}
|
||||
} else {
|
||||
req.Level = 0
|
||||
req.Path = "/root"
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
ids, err := dao.PrivateCategory.Insert(ctx, req)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "创建私域分类失败")
|
||||
}
|
||||
|
||||
var id *bson.ObjectID
|
||||
if len(ids) > 0 {
|
||||
if objectID, ok := ids[0].(bson.ObjectID); ok {
|
||||
id = &objectID
|
||||
}
|
||||
}
|
||||
|
||||
return &dto.CreatePrivateCategoryRes{ID: id}, nil
|
||||
}
|
||||
|
||||
// BatchCreatePrivateCategory 批量创建私域分类
|
||||
func (s *privateCategory) BatchCreatePrivateCategory(ctx context.Context, req *dto.BatchCreatePrivateCategoryReq) (*dto.BatchCreatePrivateCategoryRes, error) {
|
||||
// 保存到数据库
|
||||
ids, err := dao.PrivateCategory.BatchInsert(ctx, req)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "批量创建私域分类失败")
|
||||
}
|
||||
|
||||
// 转换ID列表
|
||||
idList := make([]*bson.ObjectID, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if objectID, ok := id.(bson.ObjectID); ok {
|
||||
idList = append(idList, &objectID)
|
||||
}
|
||||
}
|
||||
|
||||
return &dto.BatchCreatePrivateCategoryRes{IDs: idList}, nil
|
||||
}
|
||||
|
||||
// UpdatePrivateCategory 更新私域分类
|
||||
func (s *privateCategory) UpdatePrivateCategory(ctx context.Context, req *dto.UpdatePrivateCategoryReq) error {
|
||||
// 更新到数据库
|
||||
err := dao.PrivateCategory.Update(ctx, req)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "更新私域分类失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeletePrivateCategory 删除私域分类
|
||||
func (s *privateCategory) DeletePrivateCategory(ctx context.Context, id *bson.ObjectID) error {
|
||||
return dao.PrivateCategory.DeleteFake(ctx, id)
|
||||
}
|
||||
|
||||
// GetPrivateCategory 获取私域分类详情
|
||||
func (s *privateCategory) GetPrivateCategory(ctx context.Context, id *bson.ObjectID) (*dto.GetPrivateCategoryRes, error) {
|
||||
category, err := dao.PrivateCategory.GetOne(ctx, id)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取私域分类失败")
|
||||
}
|
||||
|
||||
// 转换为响应
|
||||
res := &dto.GetPrivateCategoryRes{
|
||||
ID: category.Id,
|
||||
Name: category.Name,
|
||||
ParentID: category.ParentID,
|
||||
Path: category.Path,
|
||||
Level: category.Level,
|
||||
IsLeafNode: category.IsLeafNode,
|
||||
Sort: category.Sort,
|
||||
Image: category.Image,
|
||||
CreatedAt: category.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
UpdatedAt: category.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// ListPrivateCategory 获取私域分类列表
|
||||
func (s *privateCategory) ListPrivateCategory(ctx context.Context, req *dto.ListPrivateCategoryReq) (*dto.ListPrivateCategoryRes, error) {
|
||||
// 获取数据
|
||||
categories, total, err := dao.PrivateCategory.List(ctx, req)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取私域分类列表失败")
|
||||
}
|
||||
|
||||
// 转换为响应
|
||||
listItems := make([]*dto.PrivateCategoryListItem, 0, len(categories))
|
||||
for _, category := range categories {
|
||||
listItems = append(listItems, &dto.PrivateCategoryListItem{
|
||||
ID: category.Id,
|
||||
Name: category.Name,
|
||||
ParentID: category.ParentID,
|
||||
Path: category.Path,
|
||||
Level: category.Level,
|
||||
IsLeafNode: category.IsLeafNode,
|
||||
Sort: category.Sort,
|
||||
Image: category.Image,
|
||||
CreatedAt: category.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
UpdatedAt: category.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
}
|
||||
|
||||
return &dto.ListPrivateCategoryRes{
|
||||
List: listItems,
|
||||
Total: total,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetPrivateCategoryTree 获取私域分类树
|
||||
func (s *privateCategory) GetPrivateCategoryTree(ctx context.Context) (*dto.GetPrivateCategoryTreeRes, error) {
|
||||
categories, err := dao.PrivateCategory.GetTree(ctx)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取私域分类树失败")
|
||||
}
|
||||
|
||||
// 转换为响应
|
||||
treeItems := make([]*dto.PrivateCategoryTreeItem, 0, len(categories))
|
||||
for _, category := range categories {
|
||||
treeItems = append(treeItems, &dto.PrivateCategoryTreeItem{
|
||||
ID: category.Id,
|
||||
Name: category.Name,
|
||||
ParentID: category.ParentID,
|
||||
Path: category.Path,
|
||||
Level: category.Level,
|
||||
IsLeafNode: category.IsLeafNode,
|
||||
Sort: category.Sort,
|
||||
Image: category.Image,
|
||||
})
|
||||
}
|
||||
|
||||
return &dto.GetPrivateCategoryTreeRes{
|
||||
Tree: treeItems,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GenerateTestData 生成测试数据
|
||||
func (s *privateCategory) GenerateTestData(ctx context.Context) error {
|
||||
testData := &dto.BatchCreatePrivateCategoryReq{
|
||||
Categories: []dto.CreatePrivateCategoryReq{},
|
||||
}
|
||||
|
||||
_, err := s.BatchCreatePrivateCategory(ctx, testData)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "生成测试数据失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
189
service/asset/private_sku_service.go
Normal file
189
service/asset/private_sku_service.go
Normal file
@@ -0,0 +1,189 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
dao "assets/dao/asset"
|
||||
dto "assets/model/dto/asset"
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type privateSku struct{}
|
||||
|
||||
// PrivateSku 私域SKU服务
|
||||
var PrivateSku = new(privateSku)
|
||||
|
||||
// CreatePrivateSku 创建私域SKU
|
||||
func (s *privateSku) CreatePrivateSku(ctx context.Context, req *dto.CreatePrivateSkuReq) (*dto.CreatePrivateSkuRes, error) {
|
||||
// 保存到数据库
|
||||
ids, err := dao.PrivateSku.Insert(ctx, req)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "创建私域SKU失败")
|
||||
}
|
||||
|
||||
var id *bson.ObjectID
|
||||
if len(ids) > 0 {
|
||||
if objectID, ok := ids[0].(bson.ObjectID); ok {
|
||||
id = &objectID
|
||||
}
|
||||
}
|
||||
|
||||
return &dto.CreatePrivateSkuRes{ID: id}, nil
|
||||
}
|
||||
|
||||
// BatchCreatePrivateSku 批量创建私域SKU
|
||||
func (s *privateSku) BatchCreatePrivateSku(ctx context.Context, req *dto.BatchCreatePrivateSkuReq) (*dto.BatchCreatePrivateSkuRes, error) {
|
||||
// 保存到数据库
|
||||
ids, err := dao.PrivateSku.BatchInsert(ctx, req)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "批量创建私域SKU失败")
|
||||
}
|
||||
|
||||
// 转换ID列表
|
||||
idList := make([]*bson.ObjectID, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if objectID, ok := id.(bson.ObjectID); ok {
|
||||
idList = append(idList, &objectID)
|
||||
}
|
||||
}
|
||||
|
||||
return &dto.BatchCreatePrivateSkuRes{IDs: idList}, nil
|
||||
}
|
||||
|
||||
// UpdatePrivateSku 更新私域SKU
|
||||
func (s *privateSku) UpdatePrivateSku(ctx context.Context, req *dto.UpdatePrivateSkuReq) error {
|
||||
// 更新到数据库
|
||||
err := dao.PrivateSku.Update(ctx, req)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "更新私域SKU失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeletePrivateSku 删除私域SKU
|
||||
func (s *privateSku) DeletePrivateSku(ctx context.Context, id *bson.ObjectID) error {
|
||||
return dao.PrivateSku.DeleteFake(ctx, id)
|
||||
}
|
||||
|
||||
// GetPrivateSku 获取私域SKU详情
|
||||
func (s *privateSku) GetPrivateSku(ctx context.Context, id *bson.ObjectID) (*dto.GetPrivateSkuRes, error) {
|
||||
sku, err := dao.PrivateSku.GetOne(ctx, id)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取私域SKU失败")
|
||||
}
|
||||
|
||||
// 转换为响应
|
||||
res := &dto.GetPrivateSkuRes{
|
||||
ID: sku.Id,
|
||||
SkuName: sku.SkuName,
|
||||
ImageURL: sku.ImageURL,
|
||||
Price: sku.Price,
|
||||
Stock: sku.Stock,
|
||||
Sort: sku.Sort,
|
||||
PrivateCategoryPath: sku.PrivateCategoryPath,
|
||||
CreatedAt: sku.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
UpdatedAt: sku.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// ListPrivateSku 获取私域SKU列表
|
||||
func (s *privateSku) ListPrivateSku(ctx context.Context, req *dto.ListPrivateSkuReq) (*dto.ListPrivateSkuRes, error) {
|
||||
// 获取数据
|
||||
skus, total, err := dao.PrivateSku.List(ctx, req)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取私域SKU列表失败")
|
||||
}
|
||||
|
||||
// 转换为响应
|
||||
listItems := make([]*dto.PrivateSkuListItem, 0, len(skus))
|
||||
for _, sku := range skus {
|
||||
listItems = append(listItems, &dto.PrivateSkuListItem{
|
||||
ID: sku.Id,
|
||||
SkuName: sku.SkuName,
|
||||
ImageURL: sku.ImageURL,
|
||||
Price: sku.Price,
|
||||
Stock: sku.Stock,
|
||||
Sort: sku.Sort,
|
||||
PrivateCategoryPath: sku.PrivateCategoryPath,
|
||||
CreatedAt: sku.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
UpdatedAt: sku.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
}
|
||||
|
||||
return &dto.ListPrivateSkuRes{
|
||||
List: listItems,
|
||||
Total: total,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdatePrivateSkuStock 更新私域SKU库存
|
||||
func (s *privateSku) UpdatePrivateSkuStock(ctx context.Context, id *bson.ObjectID, stockChange int) error {
|
||||
return dao.PrivateSku.UpdateStock(ctx, id, stockChange)
|
||||
}
|
||||
|
||||
// GenerateTestData 生成测试数据
|
||||
func (s *privateSku) GenerateTestData(ctx context.Context) error {
|
||||
testData := &dto.BatchCreatePrivateSkuReq{
|
||||
Skus: []dto.CreatePrivateSkuReq{
|
||||
{
|
||||
SkuName: "联想ThinkPad X1 Carbon 14英寸 i7 16G 512G",
|
||||
ImageURL: "https://example.com/images/lenovo_x1_carbon.jpg",
|
||||
Price: 899900, // 8999.00元
|
||||
Stock: 100,
|
||||
Sort: 1,
|
||||
PrivateCategoryPath: "/笔记本电脑/ThinkPad",
|
||||
},
|
||||
{
|
||||
SkuName: "联想ThinkPad X1 Carbon 14英寸 i7 32G 1T",
|
||||
ImageURL: "https://example.com/images/lenovo_x1_carbon_32g.jpg",
|
||||
Price: 1199900, // 11999.00元
|
||||
Stock: 50,
|
||||
Sort: 2,
|
||||
PrivateCategoryPath: "/笔记本电脑/ThinkPad",
|
||||
},
|
||||
{
|
||||
SkuName: "戴尔Latitude 7440 14英寸 i7 16G 512G",
|
||||
ImageURL: "https://example.com/images/dell_latitude_7440.jpg",
|
||||
Price: 750000, // 7500.00元
|
||||
Stock: 80,
|
||||
Sort: 1,
|
||||
PrivateCategoryPath: "/笔记本电脑/Latitude",
|
||||
},
|
||||
{
|
||||
SkuName: "戴尔Latitude 7440 14英寸 i7 32G 1T",
|
||||
ImageURL: "https://example.com/images/dell_latitude_7440_32g.jpg",
|
||||
Price: 999900, // 9999.00元
|
||||
Stock: 60,
|
||||
Sort: 2,
|
||||
PrivateCategoryPath: "/笔记本电脑/Latitude",
|
||||
},
|
||||
{
|
||||
SkuName: "惠普暗影精灵9 16.1英寸 i7 32G 1T RTX4060",
|
||||
ImageURL: "https://example.com/images/hp_omen_9.jpg",
|
||||
Price: 849900, // 8499.00元
|
||||
Stock: 70,
|
||||
Sort: 1,
|
||||
PrivateCategoryPath: "/游戏笔记本/暗影精灵",
|
||||
},
|
||||
{
|
||||
SkuName: "惠普暗影精灵9 16.1英寸 i9 32G 2T RTX4070",
|
||||
ImageURL: "https://example.com/images/hp_omen_9_i9.jpg",
|
||||
Price: 1249900, // 12499.00元
|
||||
Stock: 40,
|
||||
Sort: 2,
|
||||
PrivateCategoryPath: "/游戏笔记本/暗影精灵",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := s.BatchCreatePrivateSku(ctx, testData)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "生成测试数据失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
62
service/enum/enum_service.go
Normal file
62
service/enum/enum_service.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
consts "assets/consts/asset"
|
||||
"assets/consts/public"
|
||||
dto "assets/model/dto/enum"
|
||||
"context"
|
||||
|
||||
"gitea.com/red-future/common/beans"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type enum struct{}
|
||||
|
||||
// Enum 枚举服务
|
||||
var Enum = new(enum)
|
||||
|
||||
// GetAssetType 获取资产类型
|
||||
func (s *enum) GetAssetType(ctx context.Context, req *dto.GetAssetTypeReq) (res *dto.GetAssetTypeRes, err error) {
|
||||
_, _ = ctx, req
|
||||
res = new(dto.GetAssetTypeRes)
|
||||
err = gconv.Structs(consts.GetAllAssetTypeKeyValue(), &res.Options)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *enum) GetCategoryAttrType(ctx context.Context, req *dto.GetCategoryAttrTypeReq) (res *dto.GetCategoryAttrTypeRes, err error) {
|
||||
_, _ = ctx, req
|
||||
res = new(dto.GetCategoryAttrTypeRes)
|
||||
err = gconv.Structs(consts.GetAllAttrTypeKeyValue(), &res.Options)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *enum) GetSpecsUnit(ctx context.Context, req *dto.GetSpecsUnitReq) (res *dto.GetSpecsUnitRes, err error) {
|
||||
res = new(dto.GetSpecsUnitRes)
|
||||
if *req.AssetType == consts.AssetTypeVirtual {
|
||||
keyValue := public.GetAllDurationTypeKeyValue()
|
||||
err = gconv.Structs(keyValue, &res.Options)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 使用简化的 RPC 调用方式 - 直接传 map 参数
|
||||
//dictData := &dto.GetDictRes{}
|
||||
//if err = message.CallRPC(ctx, "dictService.GetDictWithDataByType", map[string]interface{}{"dictType": gconv.String(req.AssetType)}, dictData); err != nil {
|
||||
// return
|
||||
//}
|
||||
//for _, v := range dictData.Values {
|
||||
// res.Options = append(res.Options, dto.KeyValue{
|
||||
// Key: v.DictValue,
|
||||
// Value: v.DictLabel,
|
||||
// })
|
||||
//}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *enum) GetTenantModuleType(ctx context.Context, req *dto.GetTenantModuleTypeReq) (res *dto.GetTenantModuleTypeRes, err error) {
|
||||
_ = ctx
|
||||
res = new(dto.GetTenantModuleTypeRes)
|
||||
err = gconv.Structs(beans.GetTenantModuleTypes(req.AssetId), &res.Options)
|
||||
return
|
||||
}
|
||||
201
service/procurement/purchase_inbound_service.go
Normal file
201
service/procurement/purchase_inbound_service.go
Normal file
@@ -0,0 +1,201 @@
|
||||
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)
|
||||
}
|
||||
182
service/procurement/purchase_order_item_service.go
Normal file
182
service/procurement/purchase_order_item_service.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"assets/dao/procurement"
|
||||
"assets/model/dto/procurement"
|
||||
"assets/model/entity/procurement"
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type purchaseOrderItem struct{}
|
||||
|
||||
// PurchaseOrderItem 采购订单明细服务
|
||||
var PurchaseOrderItem = new(purchaseOrderItem)
|
||||
|
||||
// CreatePurchaseOrderItem 创建采购订单明细
|
||||
func (s *purchaseOrderItem) CreatePurchaseOrderItem(ctx context.Context, req *dto.CreatePurchaseOrderItemReq) (*dto.CreatePurchaseOrderItemRes, error) {
|
||||
// 自动计算总价(如果未提供)
|
||||
if req.TotalPrice == 0 {
|
||||
req.TotalPrice = req.UnitPrice * req.Quantity
|
||||
}
|
||||
// 默认折扣价为单价(如果未提供)
|
||||
if req.DiscountPrice == 0 {
|
||||
req.DiscountPrice = req.UnitPrice
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
ids, err := dao.PurchaseOrderItem.Insert(ctx, req)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "创建采购订单明细失败")
|
||||
}
|
||||
|
||||
var id *bson.ObjectID
|
||||
if len(ids) > 0 {
|
||||
if objectID, ok := ids[0].(bson.ObjectID); ok {
|
||||
id = &objectID
|
||||
}
|
||||
}
|
||||
|
||||
return &dto.CreatePurchaseOrderItemRes{ID: id}, nil
|
||||
}
|
||||
|
||||
// BatchCreatePurchaseOrderItems 批量创建采购订单明细
|
||||
func (s *purchaseOrderItem) BatchCreatePurchaseOrderItems(ctx context.Context, req *dto.BatchCreatePurchaseOrderItemsReq) (*dto.BatchCreatePurchaseOrderItemsRes, error) {
|
||||
// 自动计算总价和设置默认折扣价
|
||||
for i := range req.Items {
|
||||
if req.Items[i].TotalPrice == 0 {
|
||||
req.Items[i].TotalPrice = req.Items[i].UnitPrice * req.Items[i].Quantity
|
||||
}
|
||||
if req.Items[i].DiscountPrice == 0 {
|
||||
req.Items[i].DiscountPrice = req.Items[i].UnitPrice
|
||||
}
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
ids, err := dao.PurchaseOrderItem.BatchInsert(ctx, req)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "批量创建采购订单明细失败")
|
||||
}
|
||||
|
||||
// 转换ID列表
|
||||
idList := make([]*bson.ObjectID, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if objectID, ok := id.(bson.ObjectID); ok {
|
||||
idList = append(idList, &objectID)
|
||||
}
|
||||
}
|
||||
|
||||
return &dto.BatchCreatePurchaseOrderItemsRes{IDs: idList}, nil
|
||||
}
|
||||
|
||||
// UpdatePurchaseOrderItem 更新采购订单明细
|
||||
func (s *purchaseOrderItem) UpdatePurchaseOrderItem(ctx context.Context, req *dto.UpdatePurchaseOrderItemReq) error {
|
||||
// 自动计算总价(如果提供了数量和单价)
|
||||
if req.Quantity > 0 && req.UnitPrice > 0 && req.TotalPrice == 0 {
|
||||
req.TotalPrice = req.UnitPrice * req.Quantity
|
||||
}
|
||||
|
||||
// 更新到数据库
|
||||
err := dao.PurchaseOrderItem.Update(ctx, req)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "更新采购订单明细失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeletePurchaseOrderItem 删除采购订单明细
|
||||
func (s *purchaseOrderItem) DeletePurchaseOrderItem(ctx context.Context, id *bson.ObjectID) error {
|
||||
return dao.PurchaseOrderItem.DeleteFake(ctx, id)
|
||||
}
|
||||
|
||||
// GetPurchaseOrderItem 获取采购订单明细详情
|
||||
func (s *purchaseOrderItem) GetPurchaseOrderItem(ctx context.Context, id *bson.ObjectID) (*dto.GetPurchaseOrderItemRes, error) {
|
||||
item, err := dao.PurchaseOrderItem.GetOne(ctx, id)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取采购订单明细失败")
|
||||
}
|
||||
|
||||
// 转换为响应
|
||||
res := &dto.GetPurchaseOrderItemRes{
|
||||
ID: item.Id,
|
||||
OrderId: item.OrderId,
|
||||
AssetId: item.AssetId,
|
||||
AssetSkuId: item.AssetSkuId,
|
||||
ProductName: item.ProductName,
|
||||
Specification: item.Specification,
|
||||
Brand: item.Brand,
|
||||
Quantity: item.Quantity,
|
||||
Unit: item.Unit,
|
||||
UnitPrice: item.UnitPrice,
|
||||
TotalPrice: item.TotalPrice,
|
||||
DiscountPrice: item.DiscountPrice,
|
||||
RequirementDesc: item.RequirementDesc,
|
||||
DeliveryAddress: item.DeliveryAddress,
|
||||
CreatedAt: item.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
UpdatedAt: item.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// ListPurchaseOrderItems 获取采购订单明细列表
|
||||
func (s *purchaseOrderItem) ListPurchaseOrderItems(ctx context.Context, req *dto.ListPurchaseOrderItemsReq) (*dto.ListPurchaseOrderItemsRes, error) {
|
||||
// 获取数据
|
||||
items, total, err := dao.PurchaseOrderItem.List(ctx, req)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取采购订单明细列表失败")
|
||||
}
|
||||
|
||||
// 转换为响应
|
||||
listItems := make([]*dto.PurchaseOrderItemListItem, 0, len(items))
|
||||
for _, item := range items {
|
||||
listItems = append(listItems, &dto.PurchaseOrderItemListItem{
|
||||
ID: item.Id,
|
||||
OrderId: item.OrderId,
|
||||
AssetId: item.AssetId,
|
||||
AssetSkuId: item.AssetSkuId,
|
||||
ProductName: item.ProductName,
|
||||
Specification: item.Specification,
|
||||
Brand: item.Brand,
|
||||
Quantity: item.Quantity,
|
||||
Unit: item.Unit,
|
||||
UnitPrice: item.UnitPrice,
|
||||
TotalPrice: item.TotalPrice,
|
||||
DiscountPrice: item.DiscountPrice,
|
||||
RequirementDesc: item.RequirementDesc,
|
||||
DeliveryAddress: item.DeliveryAddress,
|
||||
CreatedAt: item.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
UpdatedAt: item.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
}
|
||||
|
||||
return &dto.ListPurchaseOrderItemsRes{
|
||||
List: listItems,
|
||||
Total: total,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListByOrderId 根据订单ID获取采购订单明细列表
|
||||
func (s *purchaseOrderItem) ListByOrderId(ctx context.Context, orderId *bson.ObjectID) ([]*entity.PurchaseOrderItem, error) {
|
||||
items, err := dao.PurchaseOrderItem.ListByOrderId(ctx, orderId)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取订单明细列表失败")
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// GenerateTestData 生成测试数据
|
||||
func (s *purchaseOrderItem) GenerateTestData(ctx context.Context) error {
|
||||
|
||||
testData := &dto.BatchCreatePurchaseOrderItemsReq{}
|
||||
|
||||
_, err := s.BatchCreatePurchaseOrderItems(ctx, testData)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "生成测试数据失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
199
service/procurement/purchase_order_service.go
Normal file
199
service/procurement/purchase_order_service.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"assets/consts/procurement"
|
||||
"assets/dao/procurement"
|
||||
"assets/model/dto/procurement"
|
||||
"assets/model/entity/procurement"
|
||||
"context"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type purchaseOrder struct{}
|
||||
|
||||
// PurchaseOrder 采购订单服务
|
||||
var PurchaseOrder = new(purchaseOrder)
|
||||
|
||||
// CreatePurchaseOrder 创建采购订单
|
||||
func (s *purchaseOrder) CreatePurchaseOrder(ctx context.Context, req *dto.CreatePurchaseOrderReq) (*dto.CreatePurchaseOrderRes, error) {
|
||||
|
||||
// 保存到数据库
|
||||
ids, err := dao.PurchaseOrder.Insert(ctx, req)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "创建采购订单失败")
|
||||
}
|
||||
|
||||
var id *bson.ObjectID
|
||||
if len(ids) > 0 {
|
||||
if objectID, ok := ids[0].(bson.ObjectID); ok {
|
||||
id = &objectID
|
||||
}
|
||||
}
|
||||
|
||||
return &dto.CreatePurchaseOrderRes{ID: id}, nil
|
||||
}
|
||||
|
||||
// BatchCreatePurchaseOrders 批量创建采购订单
|
||||
func (s *purchaseOrder) BatchCreatePurchaseOrders(ctx context.Context, req *dto.BatchCreatePurchaseOrdersReq) (*dto.BatchCreatePurchaseOrdersRes, error) {
|
||||
// 保存到数据库
|
||||
ids, err := dao.PurchaseOrder.BatchInsert(ctx, req)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "批量创建采购订单失败")
|
||||
}
|
||||
|
||||
// 转换ID列表
|
||||
idList := make([]*bson.ObjectID, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if objectID, ok := id.(bson.ObjectID); ok {
|
||||
idList = append(idList, &objectID)
|
||||
}
|
||||
}
|
||||
|
||||
return &dto.BatchCreatePurchaseOrdersRes{IDs: idList}, nil
|
||||
}
|
||||
|
||||
// UpdatePurchaseOrder 更新采购订单
|
||||
func (s *purchaseOrder) UpdatePurchaseOrder(ctx context.Context, req *dto.UpdatePurchaseOrderReq) error {
|
||||
// 更新到数据库
|
||||
err := dao.PurchaseOrder.Update(ctx, req)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "更新采购订单失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeletePurchaseOrder 删除采购订单
|
||||
func (s *purchaseOrder) DeletePurchaseOrder(ctx context.Context, id *bson.ObjectID) error {
|
||||
return dao.PurchaseOrder.DeleteFake(ctx, id)
|
||||
}
|
||||
|
||||
// GetPurchaseOrder 获取采购订单详情
|
||||
func (s *purchaseOrder) GetPurchaseOrder(ctx context.Context, id *bson.ObjectID) (*dto.GetPurchaseOrderRes, error) {
|
||||
order, err := dao.PurchaseOrder.GetOne(ctx, id)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取采购订单失败")
|
||||
}
|
||||
|
||||
// 转换为响应
|
||||
res := &dto.GetPurchaseOrderRes{
|
||||
ID: order.Id,
|
||||
OrderNo: order.OrderNo,
|
||||
Title: order.Title,
|
||||
Description: order.Description,
|
||||
OrderType: order.OrderType,
|
||||
BuyerId: order.BuyerId,
|
||||
BuyerName: order.BuyerName,
|
||||
BuyerType: order.BuyerType,
|
||||
Status: order.Status,
|
||||
StatusText: consts.GetPurchaseOrderStatusText(order.Status),
|
||||
Priority: order.Priority,
|
||||
ExpectedDelivery: formatGTime(order.ExpectedDelivery),
|
||||
ExpiryTime: formatGTime(order.ExpiryTime),
|
||||
CreatedAt: order.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
UpdatedAt: order.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
|
||||
// 转换指定供应商模式信息
|
||||
if order.DirectPurchase != nil {
|
||||
res.DirectPurchase = &dto.DirectPurchaseInfoRes{
|
||||
SupplierId: order.DirectPurchase.SupplierId,
|
||||
SupplierName: order.DirectPurchase.SupplierName,
|
||||
SupplierCode: order.DirectPurchase.SupplierCode,
|
||||
AssignReason: order.DirectPurchase.AssignReason,
|
||||
DeliveryAddress: order.DirectPurchase.DeliveryAddress,
|
||||
ContactPerson: order.DirectPurchase.ContactPerson,
|
||||
ContactPhone: order.DirectPurchase.ContactPhone,
|
||||
ResponseStatus: order.DirectPurchase.ResponseStatus,
|
||||
AssignedAt: formatGTime(order.DirectPurchase.AssignedAt),
|
||||
AcceptedAt: formatGTime(order.DirectPurchase.AcceptedAt),
|
||||
RejectedAt: formatGTime(order.DirectPurchase.RejectedAt),
|
||||
DeliveredAt: formatGTime(order.DirectPurchase.DeliveredAt),
|
||||
}
|
||||
}
|
||||
|
||||
// 转换竞价模式信息
|
||||
if order.BiddingInfo != nil {
|
||||
res.BiddingInfo = &dto.BiddingInfoRes{
|
||||
BidMode: order.BiddingInfo.BidMode,
|
||||
BidModeText: consts.GetBidModeText(order.BiddingInfo.BidMode),
|
||||
MinSuppliers: order.BiddingInfo.MinSuppliers,
|
||||
MaxSuppliers: order.BiddingInfo.MaxSuppliers,
|
||||
BidDuration: order.BiddingInfo.BidDuration,
|
||||
BidSupplierCount: order.BiddingInfo.BidSupplierCount,
|
||||
BidStartAt: formatGTime(order.BiddingInfo.BidStartAt),
|
||||
BidEndAt: formatGTime(order.BiddingInfo.BidEndAt),
|
||||
ResultPublishedAt: formatGTime(order.BiddingInfo.ResultPublishedAt),
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// ListPurchaseOrders 获取采购订单列表
|
||||
func (s *purchaseOrder) ListPurchaseOrders(ctx context.Context, req *dto.ListPurchaseOrdersReq) (*dto.ListPurchaseOrdersRes, error) {
|
||||
// 获取数据
|
||||
orders, total, err := dao.PurchaseOrder.List(ctx, req)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取采购订单列表失败")
|
||||
}
|
||||
|
||||
// 转换为响应
|
||||
listItems := make([]*dto.PurchaseOrderListItem, 0, len(orders))
|
||||
for _, order := range orders {
|
||||
listItems = append(listItems, &dto.PurchaseOrderListItem{
|
||||
ID: order.Id,
|
||||
OrderNo: order.OrderNo,
|
||||
Title: order.Title,
|
||||
OrderType: order.OrderType,
|
||||
OrderTypeText: consts.GetPurchaseOrderTypeText(order.OrderType),
|
||||
BuyerName: order.BuyerName,
|
||||
BuyerType: order.BuyerType,
|
||||
Status: order.Status,
|
||||
StatusText: consts.GetPurchaseOrderStatusText(order.Status),
|
||||
Priority: order.Priority,
|
||||
ExpectedDelivery: formatGTime(order.ExpectedDelivery),
|
||||
ExpiryTime: formatGTime(order.ExpiryTime),
|
||||
CreatedAt: order.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
UpdatedAt: order.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
}
|
||||
|
||||
return &dto.ListPurchaseOrdersRes{
|
||||
List: listItems,
|
||||
Total: total,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListByBuyerId 根据采购方ID获取采购订单列表
|
||||
func (s *purchaseOrder) ListByBuyerId(ctx context.Context, buyerId *bson.ObjectID) ([]*entity.PurchaseOrder, error) {
|
||||
orders, err := dao.PurchaseOrder.ListByBuyerId(ctx, buyerId)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取订单列表失败")
|
||||
}
|
||||
return orders, nil
|
||||
}
|
||||
|
||||
// GenerateTestData 生成测试数据
|
||||
func (s *purchaseOrder) GenerateTestData(ctx context.Context) error {
|
||||
|
||||
testData := &dto.BatchCreatePurchaseOrdersReq{}
|
||||
|
||||
_, err := s.BatchCreatePurchaseOrders(ctx, testData)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "生成测试数据失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// formatGTime 格式化时间
|
||||
func formatGTime(t *gtime.Time) string {
|
||||
if t == nil {
|
||||
return ""
|
||||
}
|
||||
return t.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
172
service/procurement/supplier_service.go
Normal file
172
service/procurement/supplier_service.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"assets/consts/procurement"
|
||||
"assets/dao/procurement"
|
||||
"assets/model/dto/procurement"
|
||||
"assets/model/entity/procurement"
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type supplier struct{}
|
||||
|
||||
// Supplier 供应商服务
|
||||
var Supplier = new(supplier)
|
||||
|
||||
// CreateSupplier 创建供应商
|
||||
func (s *supplier) CreateSupplier(ctx context.Context, req *dto.CreateSupplierReq) (*dto.CreateSupplierRes, error) {
|
||||
// 转换为实体
|
||||
_ = &entity.Supplier{
|
||||
Name: req.Name,
|
||||
Code: req.Code,
|
||||
Phone: req.Phone,
|
||||
Mobile: req.Mobile,
|
||||
Email: req.Email,
|
||||
Address: req.Address,
|
||||
Status: consts.SupplierStatus(1), // 默认活跃状态
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
ids, err := dao.Supplier.Insert(ctx, req)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "创建供应商失败")
|
||||
}
|
||||
|
||||
var id *bson.ObjectID
|
||||
if len(ids) > 0 {
|
||||
if objectID, ok := ids[0].(bson.ObjectID); ok {
|
||||
id = &objectID
|
||||
}
|
||||
}
|
||||
|
||||
return &dto.CreateSupplierRes{ID: id}, nil
|
||||
}
|
||||
|
||||
// BatchCreateSuppliers 批量创建供应商
|
||||
func (s *supplier) BatchCreateSuppliers(ctx context.Context, req *dto.BatchCreateSuppliersReq) (*dto.BatchCreateSuppliersRes, error) {
|
||||
// 保存到数据库
|
||||
ids, err := dao.Supplier.BatchInsert(ctx, req)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "批量创建供应商失败")
|
||||
}
|
||||
|
||||
// 转换ID列表
|
||||
idList := make([]*bson.ObjectID, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if objectID, ok := id.(bson.ObjectID); ok {
|
||||
idList = append(idList, &objectID)
|
||||
}
|
||||
}
|
||||
|
||||
return &dto.BatchCreateSuppliersRes{IDs: idList}, nil
|
||||
}
|
||||
|
||||
// UpdateSupplier 更新供应商
|
||||
func (s *supplier) UpdateSupplier(ctx context.Context, req *dto.UpdateSupplierReq) error {
|
||||
// 更新到数据库
|
||||
err := dao.Supplier.Update(ctx, req)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "更新供应商失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteSupplier 删除供应商
|
||||
func (s *supplier) DeleteSupplier(ctx context.Context, id *bson.ObjectID) error {
|
||||
return dao.Supplier.DeleteFake(ctx, id)
|
||||
}
|
||||
|
||||
// GetSupplier 获取供应商详情
|
||||
func (s *supplier) GetSupplier(ctx context.Context, id *bson.ObjectID) (*dto.GetSupplierRes, error) {
|
||||
supplier, err := dao.Supplier.GetOne(ctx, id)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取供应商失败")
|
||||
}
|
||||
|
||||
// 转换为响应
|
||||
res := &dto.GetSupplierRes{
|
||||
ID: supplier.Id,
|
||||
Name: supplier.Name,
|
||||
Code: supplier.Code,
|
||||
Phone: supplier.Phone,
|
||||
Mobile: supplier.Mobile,
|
||||
Email: supplier.Email,
|
||||
Website: supplier.Website,
|
||||
Address: supplier.Address,
|
||||
Status: supplier.Status,
|
||||
StatusText: consts.GetSupplierStatusText(supplier.Status),
|
||||
Rating: supplier.Rating,
|
||||
CreatedAt: supplier.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
UpdatedAt: supplier.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// ListSuppliers 获取供应商列表
|
||||
func (s *supplier) ListSuppliers(ctx context.Context, req *dto.ListSuppliersReq) (*dto.ListSuppliersRes, error) {
|
||||
// 获取数据
|
||||
suppliers, total, err := dao.Supplier.List(ctx, req)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取供应商列表失败")
|
||||
}
|
||||
|
||||
// 转换为响应
|
||||
listItems := make([]*dto.SupplierListItem, 0, len(suppliers))
|
||||
for _, supplier := range suppliers {
|
||||
listItems = append(listItems, &dto.SupplierListItem{
|
||||
ID: supplier.Id,
|
||||
Name: supplier.Name,
|
||||
Code: supplier.Code,
|
||||
Phone: supplier.Phone,
|
||||
Mobile: supplier.Mobile,
|
||||
Email: supplier.Email,
|
||||
Address: supplier.Address,
|
||||
Status: supplier.Status,
|
||||
StatusText: consts.GetSupplierStatusText(supplier.Status),
|
||||
Rating: supplier.Rating,
|
||||
CreatedAt: supplier.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
UpdatedAt: supplier.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
}
|
||||
|
||||
return &dto.ListSuppliersRes{
|
||||
List: listItems,
|
||||
Total: total,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetSupplierOptions 获取供应商选项(用于下拉选择)
|
||||
func (s *supplier) GetSupplierOptions(ctx context.Context) ([]*dto.SupplierListItem, error) {
|
||||
suppliers, err := dao.Supplier.FindActiveSuppliers(ctx)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取供应商选项失败")
|
||||
}
|
||||
|
||||
options := make([]*dto.SupplierListItem, 0, len(suppliers))
|
||||
for _, supplier := range suppliers {
|
||||
options = append(options, &dto.SupplierListItem{
|
||||
ID: supplier.Id,
|
||||
Name: supplier.Name,
|
||||
Code: supplier.Code,
|
||||
})
|
||||
}
|
||||
|
||||
return options, nil
|
||||
}
|
||||
|
||||
// GenerateTestData 生成测试数据
|
||||
func (s *supplier) GenerateTestData(ctx context.Context) error {
|
||||
testData := &dto.BatchCreateSuppliersReq{}
|
||||
|
||||
_, err := s.BatchCreateSuppliers(ctx, testData)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "生成测试数据失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
289
service/stock/capacity_service.go
Normal file
289
service/stock/capacity_service.go
Normal file
@@ -0,0 +1,289 @@
|
||||
// 库位容量管理服务
|
||||
// 职责:库位/库区/仓库三级容量计算与同步,支持整入整出换算
|
||||
// 调用链:PrivateStock.Create/Update/Delete → UpdateLocationCapacity → SyncCapacityToZone → SyncCapacityToWarehouse
|
||||
// 紧密耦合:dao.Location(更新容量)、dao.Zone(汇总)、dao.Warehouse(汇总)、dao.UnitConversion(单位换算)
|
||||
// 注意:使用Redis分布式锁防止并发重算覆盖,锁key格式 lock:location:{id}:capacity
|
||||
package service
|
||||
|
||||
import (
|
||||
"assets/consts/public"
|
||||
"assets/consts/stock"
|
||||
dao "assets/dao/stock"
|
||||
dto "assets/model/dto/stock"
|
||||
entityAsset "assets/model/entity/asset"
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"gitea.com/red-future/common/db/mongo"
|
||||
"gitea.com/red-future/common/jaeger"
|
||||
"gitea.com/red-future/common/redis"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
var Capacity = new(capacity)
|
||||
|
||||
type capacity struct{}
|
||||
|
||||
// UpdateLocationCapacity 更新库位容量(入口方法,带Redis分布式锁)
|
||||
func (s *capacity) UpdateLocationCapacity(ctx context.Context, locationId *bson.ObjectID) (err error) {
|
||||
// Redis分布式锁(防止并发入库/出库同一库位时重算覆盖)
|
||||
lockKey := fmt.Sprintf("lock:location:%s:capacity", locationId.Hex())
|
||||
expireSeconds := int64(30)
|
||||
|
||||
var zoneId *bson.ObjectID
|
||||
var capacityUnitType stock.CapacityUnitType
|
||||
|
||||
success, err := redis.Lock(ctx, lockKey, expireSeconds, func(ctx context.Context) error {
|
||||
// 1. 查询库位信息
|
||||
location, err := dao.Location.GetOne(ctx, &dto.GetLocationReq{Id: locationId})
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "查询库位失败: %v", err)
|
||||
return err
|
||||
}
|
||||
zoneId = location.ZoneId
|
||||
capacityUnitType = location.CapacityUnitType
|
||||
|
||||
// 2. 查询库位下所有库存记录
|
||||
privateStocks, _, err := dao.PrivateStock.List(ctx, &dto.ListPrivateStockReq{
|
||||
LocationId: locationId,
|
||||
})
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "查询库位库存失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 3. 批量查询PrivateSku(避免N+1查询问题)
|
||||
skuIds := make([]*bson.ObjectID, 0, len(privateStocks))
|
||||
for _, ps := range privateStocks {
|
||||
if ps.PrivateSkuID != nil && ps.AvailableQty > 0 {
|
||||
skuIds = append(skuIds, ps.PrivateSkuID)
|
||||
}
|
||||
}
|
||||
|
||||
// 批量查询PrivateSku并构建Map缓存
|
||||
skuMap := make(map[string]*entityAsset.PrivateSku)
|
||||
if len(skuIds) > 0 {
|
||||
var skuList []*entityAsset.PrivateSku
|
||||
filter := bson.M{"_id": bson.M{"$in": skuIds}}
|
||||
_, err = mongo.DB().Find(ctx, filter, &skuList, public.PrivateSkuCollection, nil, nil)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "批量查询PrivateSku失败: %v", err)
|
||||
return err
|
||||
}
|
||||
// 构建Map缓存
|
||||
for i := range skuList {
|
||||
skuMap[skuList[i].Id.Hex()] = skuList[i]
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 整入整出计算:先按SKU聚合同库位的总数量(同SKU可合箱),再按库位单位换算
|
||||
// 聚合同一SKU的总数量,避免逐条取整导致容量虚高
|
||||
// 例:2批次各1瓶,逐条取整=2箱(错误),聚合后取整=ceil(2/20)=1箱(正确)
|
||||
skuQtyMap := make(map[string]int) // key: privateSkuId.Hex(), value: 总可用数量
|
||||
for _, ps := range privateStocks {
|
||||
if ps.PrivateSkuID == nil || ps.AvailableQty <= 0 {
|
||||
continue
|
||||
}
|
||||
skuQtyMap[ps.PrivateSkuID.Hex()] += ps.AvailableQty
|
||||
}
|
||||
|
||||
totalCapacity := 0
|
||||
for skuIdHex, totalQty := range skuQtyMap {
|
||||
// 从Map缓存中获取PrivateSku
|
||||
privateSku, exists := skuMap[skuIdHex]
|
||||
if !exists || privateSku == nil {
|
||||
g.Log().Warningf(ctx, "PrivateSku不存在,跳过: %s", skuIdHex)
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查location和privateSku的Capacity是否为nil
|
||||
if location.Capacity == nil || privateSku.Capacity.CapacityUnit == "" {
|
||||
g.Log().Warningf(ctx, "库位或SKU容量信息不完整,跳过")
|
||||
continue
|
||||
}
|
||||
|
||||
// 如果库存单位与库位单位相同,直接累加
|
||||
if privateSku.Capacity.CapacityUnit == location.Capacity.CapacityUnit {
|
||||
totalCapacity += totalQty
|
||||
continue
|
||||
}
|
||||
|
||||
// 不同单位需要换算
|
||||
conversion, err := dao.UnitConversion.GetByUnits(ctx,
|
||||
location.CapacityUnitType,
|
||||
privateSku.Capacity.CapacityUnit,
|
||||
location.Capacity.CapacityUnit,
|
||||
)
|
||||
if err != nil {
|
||||
err = gerror.Newf("未找到单位换算规则 %s→%s,请在系统中添加该换算规则",
|
||||
privateSku.Capacity.CapacityUnit, location.Capacity.CapacityUnit)
|
||||
jaeger.RecordError(ctx, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查换算系数是否为0,防止除零错误
|
||||
if conversion.ConversionFactor == 0 {
|
||||
err = gerror.Newf("换算系数为0:%s→%s,请检查换算规则配置",
|
||||
privateSku.Capacity.CapacityUnit, location.Capacity.CapacityUnit)
|
||||
jaeger.RecordError(ctx, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 向上取整计算(整入整出:同SKU合箱后取整,不足一箱按一箱计)
|
||||
convertedQty := int(math.Ceil(float64(totalQty) / conversion.ConversionFactor))
|
||||
totalCapacity += convertedQty
|
||||
|
||||
g.Log().Debugf(ctx, "单位换算: %d%s ÷ %.2f = %d%s",
|
||||
totalQty, privateSku.Capacity.CapacityUnit,
|
||||
conversion.ConversionFactor, convertedQty, location.Capacity.CapacityUnit)
|
||||
}
|
||||
|
||||
currentCapacity := totalCapacity
|
||||
|
||||
// 5. 更新库位容量
|
||||
err = dao.Location.UpdateCapacity(ctx, locationId, currentCapacity)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "更新库位容量失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "库位容量更新成功: locationId=%s, 当前容量=%d",
|
||||
locationId.Hex(), currentCapacity)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if !success {
|
||||
return fmt.Errorf("获取库位容量锁失败: %v", err)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 6. 触发向上汇总到库区(在锁外执行,避免嵌套锁时间过长)
|
||||
if zoneId != nil && !zoneId.IsZero() {
|
||||
if syncErr := s.SyncCapacityToZone(ctx, zoneId, capacityUnitType); syncErr != nil {
|
||||
g.Log().Errorf(ctx, "同步库区容量失败: %v", syncErr)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SyncCapacityToZone 同步容量到库区(带Redis分布式锁)
|
||||
func (s *capacity) SyncCapacityToZone(ctx context.Context, zoneId *bson.ObjectID, unitType stock.CapacityUnitType) (err error) {
|
||||
// 1. Redis分布式锁
|
||||
lockKey := fmt.Sprintf("lock:zone:%s:capacity:%s", zoneId.Hex(), unitType)
|
||||
expireSeconds := int64(30) // 30秒超时
|
||||
|
||||
success, err := redis.Lock(ctx, lockKey, expireSeconds, func(ctx context.Context) error {
|
||||
// 2. 查询该库区下所有使用该单位类型的库位
|
||||
locations, err := dao.Location.ListByZoneAndUnitType(ctx, zoneId, unitType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查询库位列表失败: %v", err)
|
||||
}
|
||||
|
||||
// 3. 汇总所有库位的当前容量
|
||||
totalCapacity := 0
|
||||
maxCapacity := 0
|
||||
var capacityUnit string
|
||||
for _, loc := range locations {
|
||||
if loc.Capacity != nil {
|
||||
totalCapacity += loc.Capacity.CurrentCapacity
|
||||
maxCapacity += loc.Capacity.MaxCapacity
|
||||
if capacityUnit == "" {
|
||||
capacityUnit = loc.Capacity.CapacityUnit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 查询库区信息(获取warehouseId)
|
||||
zone, err := dao.Zone.GetOne(ctx, &dto.GetZoneReq{Id: zoneId})
|
||||
if err != nil {
|
||||
return fmt.Errorf("查询库区失败: %v", err)
|
||||
}
|
||||
|
||||
// 5. 更新库区该单位类型的容量
|
||||
err = dao.Zone.UpdateCapacityByUnitType(ctx, zoneId, unitType, totalCapacity, maxCapacity, capacityUnit)
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新库区容量失败: %v", err)
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "库区容量同步成功: zoneId=%s, unitType=%s, 当前容量=%d",
|
||||
zoneId.Hex(), unitType, totalCapacity)
|
||||
|
||||
// 6. 触发向上汇总到仓库
|
||||
if zone.WarehouseId != "" {
|
||||
warehouseObjId, hexErr := bson.ObjectIDFromHex(zone.WarehouseId)
|
||||
if hexErr != nil {
|
||||
g.Log().Errorf(ctx, "库区WarehouseId格式错误: %s, %v", zone.WarehouseId, hexErr)
|
||||
} else {
|
||||
if syncErr := s.SyncCapacityToWarehouse(ctx, &warehouseObjId, unitType); syncErr != nil {
|
||||
g.Log().Errorf(ctx, "同步仓库容量失败: %v", syncErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if !success {
|
||||
return fmt.Errorf("获取Redis锁失败: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SyncCapacityToWarehouse 同步容量到仓库(带Redis分布式锁)
|
||||
func (s *capacity) SyncCapacityToWarehouse(ctx context.Context, warehouseId *bson.ObjectID, unitType stock.CapacityUnitType) (err error) {
|
||||
// 1. Redis分布式锁
|
||||
lockKey := fmt.Sprintf("lock:warehouse:%s:capacity:%s", warehouseId.Hex(), unitType)
|
||||
expireSeconds := int64(30) // 30秒超时
|
||||
|
||||
success, err := redis.Lock(ctx, lockKey, expireSeconds, func(ctx context.Context) error {
|
||||
// 2. 查询该仓库下所有库区
|
||||
zones, err := dao.Zone.ListByWarehouseAndUnitType(ctx, warehouseId.Hex())
|
||||
if err != nil {
|
||||
return fmt.Errorf("查询库区列表失败: %v", err)
|
||||
}
|
||||
|
||||
// 3. 汇总所有库区该单位类型的容量
|
||||
totalCapacity := 0
|
||||
maxCapacity := 0
|
||||
var capacityUnit string
|
||||
for _, zone := range zones {
|
||||
if zone.Capacity != nil {
|
||||
if cap, exists := (*zone.Capacity)[unitType]; exists {
|
||||
totalCapacity += cap.CurrentCapacity
|
||||
maxCapacity += cap.MaxCapacity
|
||||
if capacityUnit == "" {
|
||||
capacityUnit = cap.CapacityUnit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 更新仓库该单位类型的容量
|
||||
err = dao.Warehouse.UpdateCapacityByUnitType(ctx, warehouseId, unitType, totalCapacity, maxCapacity, capacityUnit)
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新仓库容量失败: %v", err)
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "仓库容量同步成功: warehouseId=%s, unitType=%s, 当前容量=%d",
|
||||
warehouseId.Hex(), unitType, totalCapacity)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if !success {
|
||||
return fmt.Errorf("获取Redis锁失败: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ConvertWithCeil 向上取整换算(用于容量计算)
|
||||
func (s *capacity) ConvertWithCeil(fromQty int, conversionFactor float64) int {
|
||||
return int(math.Ceil(float64(fromQty) / conversionFactor))
|
||||
}
|
||||
55
service/stock/inventory_count_adjust_history_service.go
Normal file
55
service/stock/inventory_count_adjust_history_service.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
dao "assets/dao/stock"
|
||||
dto "assets/model/dto/stock"
|
||||
"context"
|
||||
|
||||
"gitea.com/red-future/common/utils"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type inventoryCountAdjustHistory struct{}
|
||||
|
||||
var InventoryCountAdjustHistory = new(inventoryCountAdjustHistory)
|
||||
|
||||
// Create 创建盘点调整历史记录
|
||||
func (s *inventoryCountAdjustHistory) Create(ctx context.Context, req *dto.CreateInventoryCountAdjustHistoryReq) (res *dto.CreateInventoryCountAdjustHistoryRes, err error) {
|
||||
ids, err := dao.InventoryCountAdjustHistory.Insert(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
id := ids[0].(bson.ObjectID)
|
||||
res = &dto.CreateInventoryCountAdjustHistoryRes{
|
||||
Id: &id,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Delete 删除盘点调整历史记录
|
||||
func (s *inventoryCountAdjustHistory) Delete(ctx context.Context, req *dto.DeleteInventoryCountAdjustHistoryReq) error {
|
||||
return dao.InventoryCountAdjustHistory.DeleteFake(ctx, req)
|
||||
}
|
||||
|
||||
// GetOne 查询单条盘点调整历史记录详情
|
||||
func (s *inventoryCountAdjustHistory) GetOne(ctx context.Context, req *dto.GetInventoryCountAdjustHistoryReq) (res *dto.GetInventoryCountAdjustHistoryRes, err error) {
|
||||
one, err := dao.InventoryCountAdjustHistory.GetOne(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = utils.Struct(one, &res)
|
||||
return
|
||||
}
|
||||
|
||||
// List 分页查询盘点调整历史列表
|
||||
func (s *inventoryCountAdjustHistory) List(ctx context.Context, req *dto.ListInventoryCountAdjustHistoryReq) (res *dto.ListInventoryCountAdjustHistoryRes, err error) {
|
||||
list, total, err := dao.InventoryCountAdjustHistory.List(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = &dto.ListInventoryCountAdjustHistoryRes{
|
||||
Total: total,
|
||||
}
|
||||
err = utils.Struct(list, &res.List)
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
1025
service/stock/inventory_count_service.go
Normal file
1025
service/stock/inventory_count_service.go
Normal file
File diff suppressed because it is too large
Load Diff
42
service/stock/inventory_warning_history_service.go
Normal file
42
service/stock/inventory_warning_history_service.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// 库存预警历史服务
|
||||
// 职责:预警历史查询、删除(无Create/Update,由预警状态变更时自动归档)
|
||||
// 紧密耦合:dao.InventoryWarningHistory
|
||||
// 注意:历史记录由系统自动归档,非用户手动创建
|
||||
package service
|
||||
|
||||
import (
|
||||
dao "assets/dao/stock"
|
||||
dto "assets/model/dto/stock"
|
||||
"context"
|
||||
|
||||
"gitea.com/red-future/common/utils"
|
||||
)
|
||||
|
||||
type inventoryWarningHistory struct{}
|
||||
|
||||
var InventoryWarningHistory = new(inventoryWarningHistory)
|
||||
|
||||
func (s *inventoryWarningHistory) GetOne(ctx context.Context, req *dto.GetInventoryWarningHistoryReq) (res *dto.GetInventoryWarningHistoryRes, err error) {
|
||||
one, err := dao.InventoryWarningHistory.GetOne(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = utils.Struct(one, &res)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *inventoryWarningHistory) List(ctx context.Context, req *dto.ListInventoryWarningHistoryReq) (res *dto.ListInventoryWarningHistoryRes, err error) {
|
||||
list, total, err := dao.InventoryWarningHistory.List(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = &dto.ListInventoryWarningHistoryRes{
|
||||
Total: total,
|
||||
}
|
||||
err = utils.Struct(list, &res.List)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *inventoryWarningHistory) Delete(ctx context.Context, req *dto.DeleteInventoryWarningHistoryReq) error {
|
||||
return dao.InventoryWarningHistory.DeleteFake(ctx, req)
|
||||
}
|
||||
38
service/stock/inventory_warning_service.go
Normal file
38
service/stock/inventory_warning_service.go
Normal file
@@ -0,0 +1,38 @@
|
||||
// 库存预警服务
|
||||
// 职责:预警查询(无Create/Update/Delete,由定时任务或库存变动触发)
|
||||
// 紧密耦合:dao.InventoryWarning
|
||||
// 注意:预警记录由系统自动生成,非用户手动创建
|
||||
package service
|
||||
|
||||
import (
|
||||
dao "assets/dao/stock"
|
||||
dto "assets/model/dto/stock"
|
||||
"context"
|
||||
|
||||
"gitea.com/red-future/common/utils"
|
||||
)
|
||||
|
||||
type inventoryWarning struct{}
|
||||
|
||||
var InventoryWarning = new(inventoryWarning)
|
||||
|
||||
func (s *inventoryWarning) GetOne(ctx context.Context, req *dto.GetInventoryWarningReq) (res *dto.GetInventoryWarningRes, err error) {
|
||||
one, err := dao.InventoryWarning.GetOne(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = utils.Struct(one, &res)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *inventoryWarning) List(ctx context.Context, req *dto.ListInventoryWarningReq) (res *dto.ListInventoryWarningRes, err error) {
|
||||
list, total, err := dao.InventoryWarning.List(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = &dto.ListInventoryWarningRes{
|
||||
Total: total,
|
||||
}
|
||||
err = utils.Struct(list, &res.List)
|
||||
return
|
||||
}
|
||||
92
service/stock/location_service.go
Normal file
92
service/stock/location_service.go
Normal file
@@ -0,0 +1,92 @@
|
||||
// 库位服务
|
||||
// 职责:库位CRUD、状态更新、删除前检查(3个库存集合)
|
||||
// 调用链:Delete → CountStockDetailsByLocationId/CountStockBatchByLocationId/CountPrivateStockByLocationId
|
||||
// 紧密耦合:dao.Location、Capacity(容量更新入口)
|
||||
// 注意:删除前检查StockDetails/StockBatch/PrivateStock三个集合是否有库存
|
||||
package service
|
||||
|
||||
import (
|
||||
dao "assets/dao/stock"
|
||||
dto "assets/model/dto/stock"
|
||||
"context"
|
||||
|
||||
"gitea.com/red-future/common/utils"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type location struct{}
|
||||
|
||||
var Location = new(location)
|
||||
|
||||
func (s *location) Create(ctx context.Context, req *dto.CreateLocationReq) (res *dto.CreateLocationRes, err error) {
|
||||
ids, err := dao.Location.Insert(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
id := ids[0].(bson.ObjectID)
|
||||
res = &dto.CreateLocationRes{
|
||||
Id: &id,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *location) Update(ctx context.Context, req *dto.UpdateLocationReq) error {
|
||||
return dao.Location.Update(ctx, req)
|
||||
}
|
||||
|
||||
func (s *location) Delete(ctx context.Context, req *dto.DeleteLocationReq) error {
|
||||
locationId := req.Id.Hex()
|
||||
// 检查库位上是否有库存(3个集合)
|
||||
stockDetailsCount, err := dao.Location.CountStockDetailsByLocationId(ctx, locationId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if stockDetailsCount > 0 {
|
||||
return gerror.Newf("库位上存在%d件库存明细,无法删除", stockDetailsCount)
|
||||
}
|
||||
|
||||
stockBatchCount, err := dao.Location.CountStockBatchByLocationId(ctx, locationId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if stockBatchCount > 0 {
|
||||
return gerror.Newf("库位上存在%d个批次库存,无法删除", stockBatchCount)
|
||||
}
|
||||
|
||||
privateStockCount, err := dao.Location.CountPrivateStockByLocationId(ctx, locationId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if privateStockCount > 0 {
|
||||
return gerror.Newf("库位上存在%d件实物库存批次,无法删除", privateStockCount)
|
||||
}
|
||||
|
||||
return dao.Location.DeleteFake(ctx, req)
|
||||
}
|
||||
|
||||
// UpdateStatus 更新库位状态(单独的状态修改接口)
|
||||
func (s *location) UpdateStatus(ctx context.Context, req *dto.UpdateLocationStatusReq) error {
|
||||
return dao.Location.UpdateStatus(ctx, req)
|
||||
}
|
||||
|
||||
func (s *location) GetOne(ctx context.Context, req *dto.GetLocationReq) (res *dto.GetLocationRes, err error) {
|
||||
one, err := dao.Location.GetOne(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = utils.Struct(one, &res)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *location) List(ctx context.Context, req *dto.ListLocationReq) (res *dto.ListLocationRes, err error) {
|
||||
list, total, err := dao.Location.List(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = &dto.ListLocationRes{
|
||||
Total: total,
|
||||
}
|
||||
err = utils.Struct(list, &res.List)
|
||||
return
|
||||
}
|
||||
305
service/stock/private_stock_service.go
Normal file
305
service/stock/private_stock_service.go
Normal file
@@ -0,0 +1,305 @@
|
||||
// 实物库存批次服务
|
||||
// 职责:CRUD、移库(MoveStock库位间)、调拨(TransferStock仓库间)、出库(Outbound)
|
||||
// 调用链:Controller → Create/Update/Delete → Capacity.UpdateLocationCapacity(容量重算)
|
||||
// 紧密耦合:dao.PrivateStock、dao.Warehouse/Zone/Location(获取名称)、Capacity(容量更新)
|
||||
// 注意:区别于StockDetails/StockBatch的逻辑库存,PrivateStock记录实际存放位置
|
||||
package service
|
||||
|
||||
import (
|
||||
"assets/consts/public"
|
||||
"assets/consts/stock"
|
||||
dao "assets/dao/stock"
|
||||
dto "assets/model/dto/stock"
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type privateStock struct{}
|
||||
|
||||
// PrivateStock 实物库存批次服务
|
||||
// 职责:
|
||||
// 1. CRUD:创建、更新、删除、查询实物库存批次
|
||||
// 2. 移库(MoveStock):库位间移动
|
||||
// 3. 调拨(TransferStock):仓库间调拨
|
||||
// 4. 出库(Outbound):减少库存数量
|
||||
// 特点:记录SKU批次的实际存放位置(仓库/库区/库位)和数量,区别于StockDetails/StockBatch的逻辑库存
|
||||
var PrivateStock = new(privateStock)
|
||||
|
||||
func (s *privateStock) Create(ctx context.Context, req *dto.CreatePrivateStockReq) (res *dto.CreatePrivateStockRes, err error) {
|
||||
ids, err := dao.PrivateStock.Insert(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
id := ids[0].(bson.ObjectID)
|
||||
res = &dto.CreatePrivateStockRes{
|
||||
Id: &id,
|
||||
}
|
||||
|
||||
// 触发库位容量更新
|
||||
if req.LocationId != nil && !req.LocationId.IsZero() {
|
||||
if err := Capacity.UpdateLocationCapacity(ctx, req.LocationId); err != nil {
|
||||
// 容量更新失败不影响创建
|
||||
g.Log().Warningf(ctx, "更新库位容量失败: %v", err)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *privateStock) Update(ctx context.Context, req *dto.UpdatePrivateStockReq) error {
|
||||
// 查询库存信息获取LocationId
|
||||
stock, err := dao.PrivateStock.GetOne(ctx, &dto.GetPrivateStockReq{Id: req.Id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = dao.PrivateStock.Update(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 触发库位容量更新
|
||||
if stock.LocationID != nil && !stock.LocationID.IsZero() {
|
||||
if err := Capacity.UpdateLocationCapacity(ctx, stock.LocationID); err != nil {
|
||||
g.Log().Warningf(ctx, "更新库位容量失败: %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *privateStock) Delete(ctx context.Context, req *dto.DeletePrivateStockReq) error {
|
||||
// 查询库存信息获取LocationId(用于删除后更新容量)
|
||||
stockInfo, err := dao.PrivateStock.GetOne(ctx, &dto.GetPrivateStockReq{Id: req.Id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := dao.PrivateStock.DeleteFake(ctx, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 触发库位容量更新
|
||||
if stockInfo.LocationID != nil && !stockInfo.LocationID.IsZero() {
|
||||
if capErr := Capacity.UpdateLocationCapacity(ctx, stockInfo.LocationID); capErr != nil {
|
||||
g.Log().Warningf(ctx, "删除库存后更新库位容量失败: %v", capErr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *privateStock) GetOne(ctx context.Context, req *dto.GetPrivateStockReq) (res *dto.GetPrivateStockRes, err error) {
|
||||
one, err := dao.PrivateStock.GetOne(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = utils.Struct(one, &res)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *privateStock) List(ctx context.Context, req *dto.ListPrivateStockReq) (res *dto.ListPrivateStockRes, err error) {
|
||||
list, total, err := dao.PrivateStock.List(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = &dto.ListPrivateStockRes{
|
||||
Total: total,
|
||||
}
|
||||
err = utils.Struct(list, &res.List)
|
||||
return
|
||||
}
|
||||
|
||||
// MoveStock 移库(库位间移动)
|
||||
func (s *privateStock) MoveStock(ctx context.Context, req *dto.MoveStockReq) error {
|
||||
// 只支持PrivateStock移库,StockDetails/StockBatch是逻辑库存无位置信息
|
||||
if req.StockType != stock.StockLocationTypePrivateStock {
|
||||
return gerror.New("移库操作仅支持实物库存批次(PrivateStock),明细和批次库存为逻辑库存无位置信息")
|
||||
}
|
||||
|
||||
// 验证源库位和目标库位不能相同
|
||||
if req.FromLocationId.Hex() == req.ToLocationId.Hex() {
|
||||
return gerror.New("源库位和目标库位不能相同")
|
||||
}
|
||||
|
||||
// 验证库存是否存在且位置匹配
|
||||
privateStock, err := dao.PrivateStock.GetOne(ctx, &dto.GetPrivateStockReq{Id: req.StockId})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if privateStock.LocationID == nil || privateStock.LocationID.Hex() != req.FromLocationId.Hex() {
|
||||
return gerror.New("库存当前位置与源库位不匹配")
|
||||
}
|
||||
|
||||
// 获取目标库位信息
|
||||
toLocation, err := dao.Location.GetOne(ctx, &dto.GetLocationReq{Id: req.ToLocationId})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新库存的库位信息
|
||||
filter := bson.M{"_id": req.StockId}
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
"locationId": req.ToLocationId,
|
||||
"locationCode": toLocation.LocationCode,
|
||||
"locationName": toLocation.LocationName,
|
||||
"locationType": toLocation.LocationType,
|
||||
"lastMovedAt": gtime.Now(),
|
||||
},
|
||||
}
|
||||
|
||||
_, err = mongo.DB().Update(ctx, filter, update, public.PrivateStockCollection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 触发源库位和目标库位容量更新
|
||||
if req.FromLocationId != nil && !req.FromLocationId.IsZero() {
|
||||
if err := Capacity.UpdateLocationCapacity(ctx, req.FromLocationId); err != nil {
|
||||
g.Log().Warningf(ctx, "更新源库位容量失败: %v", err)
|
||||
}
|
||||
}
|
||||
if req.ToLocationId != nil && !req.ToLocationId.IsZero() {
|
||||
if err := Capacity.UpdateLocationCapacity(ctx, req.ToLocationId); err != nil {
|
||||
g.Log().Warningf(ctx, "更新目标库位容量失败: %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TransferStock 调拨(仓库间调拨)
|
||||
func (s *privateStock) TransferStock(ctx context.Context, req *dto.TransferStockReq) error {
|
||||
// 只支持PrivateStock调拨,StockDetails/StockBatch是逻辑库存无位置信息
|
||||
if req.StockType != stock.StockLocationTypePrivateStock {
|
||||
return gerror.New("调拨操作仅支持实物库存批次(PrivateStock),明细和批次库存为逻辑库存无位置信息")
|
||||
}
|
||||
|
||||
// 验证源仓库和目标仓库不能相同
|
||||
if req.FromWarehouseId.Hex() == req.ToWarehouseId.Hex() {
|
||||
return gerror.New("源仓库和目标仓库不能相同")
|
||||
}
|
||||
|
||||
// 验证库存是否存在且仓库匹配
|
||||
privateStock, err := dao.PrivateStock.GetOne(ctx, &dto.GetPrivateStockReq{Id: req.StockId})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if privateStock.WarehouseID == nil || privateStock.WarehouseID.Hex() != req.FromWarehouseId.Hex() {
|
||||
return gerror.New("库存当前仓库与源仓库不匹配")
|
||||
}
|
||||
|
||||
// 获取目标仓库信息
|
||||
toWarehouse, err := dao.Warehouse.GetOne(ctx, &dto.GetWarehouseReq{Id: req.ToWarehouseId})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 构建更新字段
|
||||
updateFields := bson.M{
|
||||
"warehouseId": req.ToWarehouseId,
|
||||
"warehouseCode": toWarehouse.WarehouseCode,
|
||||
"warehouseName": toWarehouse.WarehouseName,
|
||||
"lastMovedAt": gtime.Now(),
|
||||
}
|
||||
|
||||
// 未指定目标库区/库位时清空旧值,避免残留指向源仓库
|
||||
unsetFields := bson.M{}
|
||||
|
||||
// 如果指定了目标库区
|
||||
if req.ToZoneId != nil && !req.ToZoneId.IsZero() {
|
||||
toZone, err := dao.Zone.GetOne(ctx, &dto.GetZoneReq{Id: req.ToZoneId})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updateFields["zoneId"] = req.ToZoneId
|
||||
updateFields["zoneCode"] = toZone.ZoneCode
|
||||
updateFields["zoneName"] = toZone.ZoneName
|
||||
updateFields["zoneType"] = toZone.ZoneType
|
||||
} else {
|
||||
unsetFields["zoneId"] = ""
|
||||
unsetFields["zoneCode"] = ""
|
||||
unsetFields["zoneName"] = ""
|
||||
unsetFields["zoneType"] = ""
|
||||
}
|
||||
|
||||
// 如果指定了目标库位
|
||||
if req.ToLocationId != nil && !req.ToLocationId.IsZero() {
|
||||
toLocation, err := dao.Location.GetOne(ctx, &dto.GetLocationReq{Id: req.ToLocationId})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updateFields["locationId"] = req.ToLocationId
|
||||
updateFields["locationCode"] = toLocation.LocationCode
|
||||
updateFields["locationName"] = toLocation.LocationName
|
||||
updateFields["locationType"] = toLocation.LocationType
|
||||
} else {
|
||||
unsetFields["locationId"] = ""
|
||||
unsetFields["locationCode"] = ""
|
||||
unsetFields["locationName"] = ""
|
||||
unsetFields["locationType"] = ""
|
||||
}
|
||||
|
||||
filter := bson.M{"_id": req.StockId}
|
||||
update := bson.M{"$set": updateFields}
|
||||
if len(unsetFields) > 0 {
|
||||
update["$unset"] = unsetFields
|
||||
}
|
||||
|
||||
_, err = mongo.DB().Update(ctx, filter, update, public.PrivateStockCollection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 触发库位容量更新(调拨后涉及库位变化时)
|
||||
// 更新源库位
|
||||
if privateStock.LocationID != nil && !privateStock.LocationID.IsZero() {
|
||||
if err := Capacity.UpdateLocationCapacity(ctx, privateStock.LocationID); err != nil {
|
||||
g.Log().Warningf(ctx, "更新源库位容量失败: %v", err)
|
||||
}
|
||||
}
|
||||
// 更新目标库位
|
||||
if req.ToLocationId != nil && !req.ToLocationId.IsZero() {
|
||||
if err := Capacity.UpdateLocationCapacity(ctx, req.ToLocationId); err != nil {
|
||||
g.Log().Warningf(ctx, "更新目标库位容量失败: %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Outbound 实物库存批次出库
|
||||
func (s *privateStock) Outbound(ctx context.Context, req *dto.OutboundPrivateStockReq) error {
|
||||
// 只支持PrivateStock出库,StockDetails/StockBatch是逻辑库存无位置信息
|
||||
if req.StockType != stock.StockLocationTypePrivateStock {
|
||||
return gerror.New("出库操作仅支持实物库存批次(PrivateStock),明细和批次库存为逻辑库存无位置信息")
|
||||
}
|
||||
|
||||
// 验证库存是否存在
|
||||
privateStock, err := dao.PrivateStock.GetOne(ctx, &dto.GetPrivateStockReq{Id: req.StockId})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 验证可用数量是否足够
|
||||
if privateStock.AvailableQty < req.OutboundQty {
|
||||
return gerror.Newf("可用库存不足:当前可用%d,需要出库%d", privateStock.AvailableQty, req.OutboundQty)
|
||||
}
|
||||
|
||||
// 使用IncrementAvailableQty原子更新(传负数表示减少)
|
||||
err = dao.PrivateStock.IncrementAvailableQty(ctx, req.StockId, -req.OutboundQty)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 触发库位容量更新
|
||||
if privateStock.LocationID != nil && !privateStock.LocationID.IsZero() {
|
||||
if err := Capacity.UpdateLocationCapacity(ctx, privateStock.LocationID); err != nil {
|
||||
g.Log().Warningf(ctx, "更新库位容量失败: %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
60
service/stock/stock_batch_service.go
Normal file
60
service/stock/stock_batch_service.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// 批次库存服务(逻辑库存)
|
||||
// 职责:批次CRUD、列表查询
|
||||
// 紧密耦合:dao.StockBatch
|
||||
// 注意:区别于PrivateStock的实物库存,批次库存是逻辑概念,不记录物理位置
|
||||
package service
|
||||
|
||||
import (
|
||||
dao "assets/dao/stock"
|
||||
dto "assets/model/dto/stock"
|
||||
"context"
|
||||
|
||||
"gitea.com/red-future/common/utils"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type stockBatch struct{}
|
||||
|
||||
// StockBatch 批次服务
|
||||
var StockBatch = new(stockBatch)
|
||||
|
||||
func (s *stockBatch) Create(ctx context.Context, req *dto.CreateBatchReq) (res *dto.CreateBatchRes, err error) {
|
||||
ids, err := dao.StockBatch.Insert(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
id := ids[0].(bson.ObjectID)
|
||||
res = &dto.CreateBatchRes{
|
||||
Id: &id,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *stockBatch) Update(ctx context.Context, req *dto.UpdateBatchReq) error {
|
||||
return dao.StockBatch.Update(ctx, req)
|
||||
}
|
||||
|
||||
func (s *stockBatch) Delete(ctx context.Context, req *dto.DeleteBatchReq) error {
|
||||
return dao.StockBatch.DeleteFake(ctx, req)
|
||||
}
|
||||
|
||||
func (s *stockBatch) GetOne(ctx context.Context, req *dto.GetBatchReq) (res *dto.GetBatchRes, err error) {
|
||||
one, err := dao.StockBatch.GetOneById(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = utils.Struct(one, &res)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *stockBatch) List(ctx context.Context, req *dto.ListBatchReq) (res *dto.ListBatchRes, err error) {
|
||||
list, total, err := dao.StockBatch.List(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = &dto.ListBatchRes{
|
||||
Total: total,
|
||||
}
|
||||
err = utils.Struct(list, &res.List)
|
||||
return
|
||||
}
|
||||
39
service/stock/stock_details_service.go
Normal file
39
service/stock/stock_details_service.go
Normal file
@@ -0,0 +1,39 @@
|
||||
// 库存明细服务(逻辑库存)
|
||||
// 职责:库存明细查询(无Create/Update/Delete,由StockManage.StockOperation管理)
|
||||
// 紧密耦合:dao.StockDetails
|
||||
// 注意:区别于PrivateStock的实物库存,明细库存是逻辑概念,不记录物理位置
|
||||
package service
|
||||
|
||||
import (
|
||||
dao "assets/dao/stock"
|
||||
dto "assets/model/dto/stock"
|
||||
"context"
|
||||
|
||||
"gitea.com/red-future/common/utils"
|
||||
)
|
||||
|
||||
type stockDetails struct{}
|
||||
|
||||
// StockDetails 库存服务
|
||||
var StockDetails = new(stockDetails)
|
||||
|
||||
func (s *stockDetails) GetOne(ctx context.Context, req *dto.GetStockDetailsReq) (res *dto.GetStockDetailsRes, err error) {
|
||||
one, err := dao.StockDetails.GetOneById(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = utils.Struct(one, &res)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *stockDetails) List(ctx context.Context, req *dto.ListStockDetailsReq) (res *dto.ListStockDetailsRes, err error) {
|
||||
list, total, err := dao.StockDetails.List(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = &dto.ListStockDetailsRes{
|
||||
Total: total,
|
||||
}
|
||||
err = utils.Struct(list, &res.List)
|
||||
return
|
||||
}
|
||||
329
service/stock/stock_manage_service.go
Normal file
329
service/stock/stock_manage_service.go
Normal file
@@ -0,0 +1,329 @@
|
||||
// 库存管理服务(Stock公共库存)
|
||||
// 职责:入库/出库操作,支持明细模式(StockDetails)和批次模式(StockBatch)
|
||||
// 调用链:Controller → StockOperation → stockPublishMessage → NATS → AddStock(消费者)
|
||||
// 紧密耦合:dao.StockDetails、dao.StockBatch、dao.AssetSku(更新库存数)、common/message(NATS发布)
|
||||
// 注意:移库/调拨是PrivateStock专属操作,不在此实现
|
||||
package service
|
||||
|
||||
import (
|
||||
"assets/consts/public"
|
||||
"assets/consts/stock"
|
||||
assetDao "assets/dao/asset"
|
||||
dao "assets/dao/stock"
|
||||
assetDto "assets/model/dto/asset"
|
||||
stockDto "assets/model/dto/stock"
|
||||
assetEntity "assets/model/entity/asset"
|
||||
entity "assets/model/entity/stock"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.com/red-future/common/beans"
|
||||
"gitea.com/red-future/common/redis"
|
||||
"gitea.com/red-future/common/utils"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type stockManage struct{}
|
||||
|
||||
// StockManage 库存管理服务(Stock公共库存)
|
||||
// 职责:
|
||||
// 1. 入库/出库:StockDetails(明细模式)和 StockBatch(批次模式)的库存操作
|
||||
// 2. 库存查询表单字段生成
|
||||
// 注意:移库(MoveStock)和调拨(TransferStock)是PrivateStock专属操作,不要在这里实现
|
||||
var StockManage = new(stockManage)
|
||||
|
||||
// GetStockFormFields 获取库存操作表单字段
|
||||
func (s *stockManage) GetStockFormFields(ctx context.Context, req *stockDto.GetStockFormFieldsReq) (*stockDto.GetStockFormFieldsRes, error) {
|
||||
// 获取资产SKU信息
|
||||
assetSku, err := assetDao.AssetSku.GetOne(ctx, &assetDto.GetAssetSkuReq{Id: req.AssetSkuId}, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fields := make([]map[string]interface{}, 0)
|
||||
// Stock 字段在两种模式下都显示
|
||||
fields = append(fields, map[string]interface{}{
|
||||
"name": "stock",
|
||||
"label": "库存数量",
|
||||
"type": "number",
|
||||
"required": true,
|
||||
"min": 1,
|
||||
})
|
||||
// 如果是批次模式(2),添加批次相关字段
|
||||
if !g.IsEmpty(assetSku.StockMode) && assetSku.StockMode == stock.StockModeBatch {
|
||||
fields = append(fields, []map[string]interface{}{
|
||||
{
|
||||
"name": "batchNo",
|
||||
"label": "批次号",
|
||||
"type": "number",
|
||||
"required": true,
|
||||
"default": gconv.Int(gtime.Now().Format("20060102") + "0001"),
|
||||
"maxLength": 12,
|
||||
},
|
||||
{
|
||||
"name": "productionDate",
|
||||
"label": "生产日期",
|
||||
"type": "date",
|
||||
},
|
||||
{
|
||||
"name": "expiryDate",
|
||||
"label": "过期日期",
|
||||
"type": "date",
|
||||
},
|
||||
{
|
||||
"name": "expiryWarningDate",
|
||||
"label": "临期预警时间",
|
||||
"type": "date",
|
||||
},
|
||||
}...)
|
||||
}
|
||||
return &stockDto.GetStockFormFieldsRes{
|
||||
StockMode: assetSku.StockMode,
|
||||
Fields: fields,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// StockOperation 库存操作入口(入库/出库)
|
||||
// 根据SKU的StockMode区分明细模式和批次模式,计算差值后发布消息到NATS
|
||||
func (s *stockManage) StockOperation(ctx context.Context, req *stockDto.StockOperationReq) (err error) {
|
||||
assetSku, err := assetDao.AssetSku.GetOne(ctx, &assetDto.GetAssetSkuReq{Id: req.AssetSkuId}, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !assetSku.UnlimitedStock && req.Stock >= 0 {
|
||||
var stockId *bson.ObjectID
|
||||
count := 0
|
||||
if assetSku.StockMode == stock.StockModeDetail {
|
||||
_count, err := dao.StockDetails.GetStockCountBySkuId(ctx, assetSku.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
count = gconv.Int(_count)
|
||||
}
|
||||
if assetSku.StockMode == stock.StockModeBatch {
|
||||
if g.IsEmpty(req.BatchNo) {
|
||||
return gerror.New("批次号不能为空")
|
||||
}
|
||||
getOne, err := dao.StockBatch.GetOne(ctx, req.BatchNo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !g.IsEmpty(getOne) {
|
||||
stockId = getOne.Id
|
||||
count = getOne.BatchQty
|
||||
}
|
||||
}
|
||||
stockCount := 0
|
||||
operationType := ""
|
||||
if count != req.Stock {
|
||||
if count > req.Stock {
|
||||
stockCount = count - req.Stock
|
||||
operationType = "del"
|
||||
} else {
|
||||
stockCount = req.Stock - count
|
||||
operationType = "add"
|
||||
}
|
||||
}
|
||||
if !g.IsEmpty(operationType) && stockCount > 0 {
|
||||
if err = s.stockPublishMessage(ctx, assetSku, stockId, stockCount, operationType, req); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// stockPublishMessage 发布库存变更消息到NATS
|
||||
// 消费者接收后执行实际的入库/出库操作(异步解耦)
|
||||
func (s *stockManage) stockPublishMessage(ctx context.Context, assetSku *assetEntity.AssetSku, stockId *bson.ObjectID, stockCount int, operationType string, req *stockDto.StockOperationReq) (err error) {
|
||||
// 用户信息
|
||||
user, err := utils.GetUserInfo(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
publishMessage := stockDto.StockPublishMessage{
|
||||
AssetId: assetSku.AssetId.Hex(),
|
||||
AssetSkuId: assetSku.Id.Hex(),
|
||||
TenantId: user.TenantId,
|
||||
UserName: user.UserName,
|
||||
StockCount: stockCount,
|
||||
OperationType: operationType,
|
||||
Metadata: assetSku.SpecValues,
|
||||
StockMode: int(assetSku.StockMode),
|
||||
BatchNo: req.BatchNo,
|
||||
ProductionDate: req.ProductionDate,
|
||||
ExpiryDate: req.ExpiryDate,
|
||||
ExpiryWarningDate: req.ExpiryWarningDate,
|
||||
}
|
||||
if !g.IsEmpty(stockId) && !stockId.IsZero() {
|
||||
publishMessage.StockId = stockId.Hex()
|
||||
}
|
||||
// 发布到 NATS
|
||||
//plugin, err := message.GetMsgPlugin(ctx, message.MessageNATS)
|
||||
//if err != nil {
|
||||
// return gerror.Newf("NATS插件未就绪: %v", err)
|
||||
//}
|
||||
//err = plugin.Publish(ctx, &message.NatsPublishMsgConfig{
|
||||
// QueueName: public.StockDetailGroupName,
|
||||
// Durable: true,
|
||||
// Data: publishMessage,
|
||||
//})
|
||||
//_, err = message.PublishMessage(ctx, &message.RedisMessageConfig{StreamKey: public.StockDetailStreamKey}, publishMessage)
|
||||
//plugin, err := message.GetMsgPlugin(message.MessageRedis)
|
||||
//if err != nil {
|
||||
// return err
|
||||
//}
|
||||
//err = plugin.Publish(ctx, &message.RedisPublishMsgConfig{
|
||||
// QueueName: public.StockDetailQueueName,
|
||||
// Data: publishMessage,
|
||||
//})
|
||||
return
|
||||
}
|
||||
|
||||
// AddStock NATS消费者调用,执行实际的入库/出库操作
|
||||
// 使用Redis分布式锁防止并发冲突,支持明细模式和批次模式
|
||||
func (s *stockManage) AddStock(ctx context.Context, msg map[string]interface{}) error {
|
||||
assetId, err := bson.ObjectIDFromHex(gconv.String(msg["assetId"]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
assetSkuId, err := bson.ObjectIDFromHex(gconv.String(msg["assetSkuId"]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stockId := bson.ObjectID{}
|
||||
if !g.IsEmpty(msg["stockId"]) {
|
||||
stockId, err = bson.ObjectIDFromHex(gconv.String(msg["stockId"]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
userName := gconv.String(msg["userName"])
|
||||
tenantId := gconv.Float64(msg["tenantId"])
|
||||
stockCount := gconv.Int(msg["stockCount"])
|
||||
operationType := gconv.String(msg["operationType"])
|
||||
metadata := gconv.Maps(msg["metadata"])
|
||||
stockMode := stock.StockMode(gconv.Int(msg["stockMode"]))
|
||||
batchNo := gconv.String(msg["batchNo"])
|
||||
productionDate := gtime.New(msg["productionDate"])
|
||||
expiryDate := gtime.New(msg["expiryDate"])
|
||||
expiryWarningDate := gtime.New(msg["expiryWarningDate"])
|
||||
// 设置 userId 和 tenantId 到 ctx
|
||||
ctx = context.WithValue(ctx, "userName", userName)
|
||||
ctx = context.WithValue(ctx, "tenantId", tenantId)
|
||||
// 获取redis-租户存储-锁key
|
||||
fileLockKey := fmt.Sprintf(public.StockDetailLockKey, assetSkuId)
|
||||
success, err := redis.Lock(ctx, fileLockKey, int64(60), func(ctx context.Context) error {
|
||||
if operationType == "add" {
|
||||
if stockMode == stock.StockModeBatch {
|
||||
if !stockId.IsZero() {
|
||||
batch := stockDto.UpdateBatchReq{
|
||||
Id: &stockId,
|
||||
BatchQty: stockCount,
|
||||
AvailableQty: stockCount,
|
||||
}
|
||||
if err := dao.StockBatch.Update(ctx, &batch); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
batch := stockDto.CreateBatchReq{
|
||||
AssetId: &assetId,
|
||||
AssetSkuId: &assetSkuId,
|
||||
Status: stock.BatchStatusActive,
|
||||
Metadata: metadata,
|
||||
BatchNo: batchNo,
|
||||
BatchQty: stockCount,
|
||||
AvailableQty: stockCount,
|
||||
ProductionDate: productionDate,
|
||||
ExpiryDate: expiryDate,
|
||||
ExpiryWarningDate: expiryWarningDate,
|
||||
}
|
||||
if _, err := dao.StockBatch.Insert(ctx, &batch); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if stockMode == stock.StockModeDetail {
|
||||
// 创建指定数量的库存
|
||||
var stockInterfaces []interface{}
|
||||
for i := 0; i < stockCount; i++ {
|
||||
stockInterfaces = append(stockInterfaces, entity.StockDetails{
|
||||
AssetId: &assetId,
|
||||
AssetSkuId: &assetSkuId,
|
||||
Status: stock.StockStatusAvailable,
|
||||
Metadata: metadata,
|
||||
})
|
||||
}
|
||||
// 批量插入数据库
|
||||
if _, err = dao.StockDetails.BatchInsert(ctx, stockInterfaces); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if operationType == "del" {
|
||||
if stockMode == stock.StockModeBatch {
|
||||
stockCount = 0 - stockCount
|
||||
// 更新批次
|
||||
batch := stockDto.UpdateBatchReq{
|
||||
Id: &stockId,
|
||||
BatchQty: stockCount,
|
||||
AvailableQty: stockCount,
|
||||
}
|
||||
if err := dao.StockBatch.Update(ctx, &batch); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if stockMode == stock.StockModeDetail {
|
||||
// 分页查询所有库存明细,收集所有ID
|
||||
var allStockIds []*bson.ObjectID
|
||||
pageSize := int64(50)
|
||||
for pageNum := int64(1); ; pageNum++ {
|
||||
details, total, err := dao.StockDetails.List(ctx,
|
||||
&stockDto.ListStockDetailsReq{
|
||||
AssetSkuId: &assetSkuId,
|
||||
Status: stock.StockStatusAvailable,
|
||||
Page: &beans.Page{PageNum: pageNum, PageSize: pageSize},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if pageNum == 1 && int(total) < stockCount {
|
||||
return gerror.New("可操作库存数量不足")
|
||||
}
|
||||
// 收集当前页的ID
|
||||
for _, detail := range details {
|
||||
if detail.Id != nil && !detail.Id.IsZero() {
|
||||
allStockIds = append(allStockIds, detail.Id)
|
||||
if len(allStockIds) >= stockCount {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(allStockIds) >= stockCount {
|
||||
break
|
||||
}
|
||||
}
|
||||
// 根据ID批量删除库存
|
||||
delCount, err := dao.StockDetails.DeleteManyByIds(ctx, allStockIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if delCount != int64(stockCount) {
|
||||
return gerror.New("删除库存数量不匹配")
|
||||
}
|
||||
stockCount = 0 - stockCount
|
||||
}
|
||||
}
|
||||
return assetDao.AssetSku.Update(ctx, &assetDto.UpdateAssetSkuReq{Id: &assetSkuId, Stock: stockCount})
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !success {
|
||||
return fmt.Errorf("获取库存操作锁失败: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
103
service/stock/unit_conversion_service.go
Normal file
103
service/stock/unit_conversion_service.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package service
|
||||
|
||||
// unit_conversion_service.go
|
||||
// 单位换算服务层
|
||||
//
|
||||
// 职责:
|
||||
// 1. GetConversionFactor - 获取单位换算系数
|
||||
// 查询UnitConversion表,返回fromUnit→toUnit的换算系数
|
||||
//
|
||||
// 2. ConvertWithCeil - 向上取整换算(用于容量计算)
|
||||
// 公式:ceil(fromQty / factor)
|
||||
// 示例:50瓶 ÷ 20(瓶/箱)= 2.5 → ceil = 3箱
|
||||
//
|
||||
// 3. ConvertExact - 精确换算(用于出入库数量计算)
|
||||
// 公式:fromQty / factor
|
||||
// 示例:50瓶 ÷ 20(瓶/箱)= 2.5箱
|
||||
//
|
||||
// 核心逻辑:
|
||||
// - 从UnitConversion表查询换算系数
|
||||
// - 向上取整确保不满一个单位也占用空间
|
||||
// - 只能在同一CapacityUnitType(枚举类型)内换算
|
||||
|
||||
import (
|
||||
dao "assets/dao/stock"
|
||||
dto "assets/model/dto/stock"
|
||||
entity "assets/model/entity/stock"
|
||||
"context"
|
||||
|
||||
"gitea.com/red-future/common/utils"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
var UnitConversion = new(unitConversion)
|
||||
|
||||
type unitConversion struct{}
|
||||
|
||||
// Create 创建单位换算规则
|
||||
func (s *unitConversion) Create(ctx context.Context, req *dto.CreateUnitConversionReq) (res *dto.CreateUnitConversionRes, err error) {
|
||||
var conversion *entity.UnitConversion
|
||||
if err = utils.Struct(req, &conversion); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ids, err := dao.UnitConversion.Insert(ctx, conversion)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
id := ids[0].(bson.ObjectID)
|
||||
res = &dto.CreateUnitConversionRes{
|
||||
Id: &id,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Update 更新单位换算规则
|
||||
func (s *unitConversion) Update(ctx context.Context, req *dto.UpdateUnitConversionReq) (err error) {
|
||||
update := bson.M{}
|
||||
|
||||
if req.ConversionCode != "" {
|
||||
update["conversionCode"] = req.ConversionCode
|
||||
}
|
||||
if req.ConversionName != "" {
|
||||
update["conversionName"] = req.ConversionName
|
||||
}
|
||||
if req.UnitType != "" {
|
||||
update["unitType"] = req.UnitType
|
||||
}
|
||||
if req.FromUnit != "" {
|
||||
update["fromUnit"] = req.FromUnit
|
||||
}
|
||||
if req.ToUnit != "" {
|
||||
update["toUnit"] = req.ToUnit
|
||||
}
|
||||
if req.ConversionFactor > 0 {
|
||||
update["conversionFactor"] = req.ConversionFactor
|
||||
}
|
||||
if req.Remark != "" {
|
||||
update["remark"] = req.Remark
|
||||
}
|
||||
|
||||
err = dao.UnitConversion.Update(ctx, req.Id, update)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete 删除单位换算规则
|
||||
func (s *unitConversion) Delete(ctx context.Context, req *dto.DeleteUnitConversionReq) (err error) {
|
||||
err = dao.UnitConversion.DeleteFake(ctx, req.Id)
|
||||
return
|
||||
}
|
||||
|
||||
// List 查询单位换算列表
|
||||
func (s *unitConversion) List(ctx context.Context, req *dto.ListUnitConversionReq) (res *dto.ListUnitConversionRes, err error) {
|
||||
list, err := dao.UnitConversion.List(ctx, req.UnitType, req.FromUnit, req.ToUnit)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res = &dto.ListUnitConversionRes{
|
||||
List: list,
|
||||
}
|
||||
return
|
||||
}
|
||||
96
service/stock/warehouse_service.go
Normal file
96
service/stock/warehouse_service.go
Normal file
@@ -0,0 +1,96 @@
|
||||
// 仓库服务
|
||||
// 职责:仓库CRUD、状态更新(联动库区/库位状态)
|
||||
// 调用链:UpdateStatus → BatchUpdateZoneStatus/BatchUpdateLocationStatus(状态联动)
|
||||
// 紧密耦合:dao.Warehouse、dao.Zone/dao.Location(级联状态更新、删除前检查)
|
||||
// 注意:删除前检查是否存在库区,启用/禁用联动子表状态
|
||||
package service
|
||||
|
||||
import (
|
||||
stockConst "assets/consts/stock"
|
||||
dao "assets/dao/stock"
|
||||
dto "assets/model/dto/stock"
|
||||
"context"
|
||||
|
||||
"gitea.com/red-future/common/utils"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type warehouse struct{}
|
||||
|
||||
var Warehouse = new(warehouse)
|
||||
|
||||
func (s *warehouse) Create(ctx context.Context, req *dto.CreateWarehouseReq) (res *dto.CreateWarehouseRes, err error) {
|
||||
ids, err := dao.Warehouse.Insert(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
id := ids[0].(bson.ObjectID)
|
||||
res = &dto.CreateWarehouseRes{
|
||||
Id: &id,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *warehouse) Update(ctx context.Context, req *dto.UpdateWarehouseReq) error {
|
||||
return dao.Warehouse.Update(ctx, req)
|
||||
}
|
||||
|
||||
func (s *warehouse) Delete(ctx context.Context, req *dto.DeleteWarehouseReq) error {
|
||||
warehouseId := req.Id.Hex()
|
||||
count, err := dao.Warehouse.CountZonesByWarehouseId(ctx, warehouseId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return gerror.Newf("仓库下存在%d个库区,无法删除", count)
|
||||
}
|
||||
return dao.Warehouse.DeleteFake(ctx, req)
|
||||
}
|
||||
|
||||
// UpdateStatus 更新仓库状态(单独的状态修改接口,联动子表)
|
||||
func (s *warehouse) UpdateStatus(ctx context.Context, req *dto.UpdateWarehouseStatusReq) (err error) {
|
||||
warehouseId := req.Id.Hex()
|
||||
if err = dao.Warehouse.UpdateStatus(ctx, req); err != nil {
|
||||
return
|
||||
}
|
||||
if req.Status == stockConst.WarehouseStatusEnabled {
|
||||
// 启用:只恢复Disabled状态的库区/库位,保留InUse等其他状态
|
||||
if _, err = dao.Warehouse.BatchUpdateZoneStatus(ctx, warehouseId, stockConst.ZoneStatusEnabled, stockConst.ZoneStatusDisabled); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err = dao.Warehouse.BatchUpdateLocationStatus(ctx, warehouseId, stockConst.LocationStatusIdle, stockConst.LocationStatusDisabled); err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 禁用:所有库区/库位统一设为Disabled
|
||||
if _, err = dao.Warehouse.BatchUpdateZoneStatus(ctx, warehouseId, stockConst.ZoneStatusDisabled); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err = dao.Warehouse.BatchUpdateLocationStatus(ctx, warehouseId, stockConst.LocationStatusDisabled); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *warehouse) GetOne(ctx context.Context, req *dto.GetWarehouseReq) (res *dto.GetWarehouseRes, err error) {
|
||||
one, err := dao.Warehouse.GetOne(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = utils.Struct(one, &res)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *warehouse) List(ctx context.Context, req *dto.ListWarehouseReq) (res *dto.ListWarehouseRes, err error) {
|
||||
list, total, err := dao.Warehouse.List(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = &dto.ListWarehouseRes{
|
||||
Total: total,
|
||||
}
|
||||
err = utils.Struct(list, &res.List)
|
||||
return
|
||||
}
|
||||
93
service/stock/zone_service.go
Normal file
93
service/stock/zone_service.go
Normal file
@@ -0,0 +1,93 @@
|
||||
// 库区服务
|
||||
// 职责:库区CRUD、状态更新(联动库位状态)
|
||||
// 调用链:UpdateStatus → BatchUpdateLocationStatus(状态联动)
|
||||
// 紧密耦合:dao.Zone、dao.Location(级联状态更新、删除前检查)
|
||||
// 注意:删除前检查是否存在库位,启用/禁用联动库位状态
|
||||
package service
|
||||
|
||||
import (
|
||||
"assets/consts/stock"
|
||||
dao "assets/dao/stock"
|
||||
dto "assets/model/dto/stock"
|
||||
"context"
|
||||
|
||||
"gitea.com/red-future/common/utils"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type zone struct{}
|
||||
|
||||
var Zone = new(zone)
|
||||
|
||||
func (s *zone) Create(ctx context.Context, req *dto.CreateZoneReq) (res *dto.CreateZoneRes, err error) {
|
||||
ids, err := dao.Zone.Insert(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
id := ids[0].(bson.ObjectID)
|
||||
res = &dto.CreateZoneRes{
|
||||
Id: &id,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *zone) Update(ctx context.Context, req *dto.UpdateZoneReq) error {
|
||||
return dao.Zone.Update(ctx, req)
|
||||
}
|
||||
|
||||
func (s *zone) Delete(ctx context.Context, req *dto.DeleteZoneReq) error {
|
||||
zoneId := req.Id.Hex()
|
||||
// 删除前检查:是否存在关联的库位
|
||||
count, err := dao.Zone.CountLocationsByZoneId(ctx, zoneId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return gerror.Newf("库区下存在%d个库位,无法删除", count)
|
||||
}
|
||||
return dao.Zone.DeleteFake(ctx, req)
|
||||
}
|
||||
|
||||
// UpdateStatus 更新库区状态(单独的状态修改接口,联动子表)
|
||||
func (s *zone) UpdateStatus(ctx context.Context, req *dto.UpdateZoneStatusReq) (err error) {
|
||||
zoneId := req.Id.Hex()
|
||||
// 1. 更新库区状态
|
||||
if err = dao.Zone.UpdateStatus(ctx, req); err != nil {
|
||||
return
|
||||
}
|
||||
// 2. 联动更新库位状态
|
||||
if req.Status == stock.ZoneStatusEnabled {
|
||||
// 启用:只恢复Disabled状态的库位,保留InUse等其他状态
|
||||
if _, err = dao.Zone.BatchUpdateLocationStatus(ctx, zoneId, stock.LocationStatusIdle, stock.LocationStatusDisabled); err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 禁用:所有库位统一设为Disabled
|
||||
if _, err = dao.Zone.BatchUpdateLocationStatus(ctx, zoneId, stock.LocationStatusDisabled); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *zone) GetOne(ctx context.Context, req *dto.GetZoneReq) (res *dto.GetZoneRes, err error) {
|
||||
one, err := dao.Zone.GetOne(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = utils.Struct(one, &res)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *zone) List(ctx context.Context, req *dto.ListZoneReq) (res *dto.ListZoneRes, err error) {
|
||||
list, total, err := dao.Zone.List(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = &dto.ListZoneRes{
|
||||
Total: total,
|
||||
}
|
||||
err = utils.Struct(list, &res.List)
|
||||
return
|
||||
}
|
||||
108
service/sync/default.go
Normal file
108
service/sync/default.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
consts "assets/consts/public"
|
||||
"assets/model/entity/sync"
|
||||
"context"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
// BasePlatformService 平台服务基类
|
||||
type BasePlatformService struct {
|
||||
Platform consts.SyncPlatform
|
||||
Config *entity.ChannelConfig
|
||||
}
|
||||
|
||||
// NewBasePlatformService 创建基础平台服务
|
||||
func NewBasePlatformService(platform consts.SyncPlatform, config *entity.ChannelConfig) *BasePlatformService {
|
||||
return &BasePlatformService{
|
||||
Platform: platform,
|
||||
Config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultAssetService 默认资产平台服务(用于未明确定义的平台)
|
||||
type DefaultAssetService struct {
|
||||
*BasePlatformService
|
||||
}
|
||||
|
||||
// NewDefaultAssetService 创建默认资产服务
|
||||
func NewDefaultAssetService(platform consts.SyncPlatform, config *entity.ChannelConfig) *DefaultAssetService {
|
||||
return &DefaultAssetService{
|
||||
BasePlatformService: NewBasePlatformService(platform, config),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DefaultAssetService) SyncAsset(ctx context.Context, assetID *bson.ObjectID) error {
|
||||
// 默认实现,不执行实际同步
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DefaultAssetService) GetAsset(ctx context.Context, assetID *bson.ObjectID) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *DefaultAssetService) UpdateAsset(ctx context.Context, assetID *bson.ObjectID, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DefaultAssetService) DeleteAsset(ctx context.Context, assetID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DefaultAssetSkuService 默认资产SKU平台服务
|
||||
type DefaultAssetSkuService struct {
|
||||
*BasePlatformService
|
||||
}
|
||||
|
||||
// NewDefaultAssetSkuService 创建默认资产SKU服务
|
||||
func NewDefaultAssetSkuService(platform consts.SyncPlatform, config *entity.ChannelConfig) *DefaultAssetSkuService {
|
||||
return &DefaultAssetSkuService{
|
||||
BasePlatformService: NewBasePlatformService(platform, config),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DefaultAssetSkuService) SyncAssetSku(ctx context.Context, assetSkuID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DefaultAssetSkuService) GetAssetSku(ctx context.Context, assetSkuID *bson.ObjectID) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *DefaultAssetSkuService) UpdateAssetSku(ctx context.Context, assetSkuID *bson.ObjectID, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DefaultAssetSkuService) DeleteAssetSku(ctx context.Context, assetSkuID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DefaultStockService 默认库存平台服务
|
||||
type DefaultStockService struct {
|
||||
*BasePlatformService
|
||||
}
|
||||
|
||||
// NewDefaultStockService 创建默认库存服务
|
||||
func NewDefaultStockService(platform consts.SyncPlatform, config *entity.ChannelConfig) *DefaultStockService {
|
||||
return &DefaultStockService{
|
||||
BasePlatformService: NewBasePlatformService(platform, config),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DefaultStockService) SyncStock(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DefaultStockService) GetStock(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *DefaultStockService) UpdateStock(ctx context.Context, stockID *bson.ObjectID, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DefaultStockService) DeleteStock(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
156
service/sync/platform_factory.go
Normal file
156
service/sync/platform_factory.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
consts "assets/consts/public"
|
||||
"assets/dao/sync"
|
||||
"context"
|
||||
)
|
||||
|
||||
// DefaultPlatformFactory 默认平台服务工厂实现
|
||||
type DefaultPlatformFactory struct{}
|
||||
|
||||
// NewPlatformServiceFactory 创建平台服务工厂
|
||||
func NewPlatformServiceFactory() PlatformFactory {
|
||||
return &DefaultPlatformFactory{}
|
||||
}
|
||||
|
||||
// CreateAssetService 创建资产平台服务
|
||||
func (f *DefaultPlatformFactory) CreateAssetService(platform consts.SyncPlatform) AssetPlatformService {
|
||||
config, err := dao.SyncConfig.GetByPlatform(context.Background(), platform)
|
||||
if err != nil || config == nil {
|
||||
// 如果没有配置,创建默认配置
|
||||
//config = &entity.SyncConfig{
|
||||
// Platform: platform,
|
||||
// IsEnabled: true,
|
||||
// APIEndpoint: f.getAPIEndpoint(platform),
|
||||
// SyncInterval: 300,
|
||||
// BatchSize: 100,
|
||||
// MaxRetries: 3,
|
||||
//}
|
||||
}
|
||||
|
||||
switch platform {
|
||||
case consts.SyncPlatformTaobao:
|
||||
return NewTaobaoAssetService(config)
|
||||
case consts.SyncPlatformJD:
|
||||
return NewJDAssetService(config)
|
||||
case consts.SyncPlatformDouyin:
|
||||
return NewDouyinAssetService(config)
|
||||
case consts.SyncPlatformKuaishou:
|
||||
return NewKuaishouAssetService(config)
|
||||
case consts.SyncPlatformXiaohongshu:
|
||||
return NewXiaohongshuAssetService(config)
|
||||
case consts.SyncPlatformPinduoduo:
|
||||
return NewPinduoduoAssetService(config)
|
||||
case consts.SyncPlatformXianyu:
|
||||
return NewXianyuAssetService(config)
|
||||
case consts.SyncPlatformBlockchain:
|
||||
return NewBlockchainAssetService(config)
|
||||
case consts.SyncPlatformInternal:
|
||||
return NewInternalAssetService(config)
|
||||
default:
|
||||
return NewDefaultAssetService(platform, config)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateAssetSkuService 创建资产SKU平台服务
|
||||
func (f *DefaultPlatformFactory) CreateAssetSkuService(platform consts.SyncPlatform) AssetSkuPlatformService {
|
||||
config, err := dao.SyncConfig.GetByPlatform(context.Background(), platform)
|
||||
if err != nil || config == nil {
|
||||
//config = &entity.ChannelConfig{
|
||||
// Platform: platform,
|
||||
// IsEnabled: true,
|
||||
// APIEndpoint: f.getAPIEndpoint(platform),
|
||||
// SyncInterval: 300,
|
||||
// BatchSize: 100,
|
||||
// MaxRetries: 3,
|
||||
//}
|
||||
}
|
||||
|
||||
switch platform {
|
||||
case consts.SyncPlatformTaobao:
|
||||
return NewTaobaoAssetSkuService(config)
|
||||
case consts.SyncPlatformJD:
|
||||
return NewJDAssetSkuService(config)
|
||||
case consts.SyncPlatformDouyin:
|
||||
return NewDouyinAssetSkuService(config)
|
||||
case consts.SyncPlatformKuaishou:
|
||||
return NewKuaishouAssetSkuService(config)
|
||||
case consts.SyncPlatformXiaohongshu:
|
||||
return NewXiaohongshuAssetSkuService(config)
|
||||
case consts.SyncPlatformPinduoduo:
|
||||
return NewPinduoduoAssetSkuService(config)
|
||||
case consts.SyncPlatformXianyu:
|
||||
return NewXianyuAssetSkuService(config)
|
||||
case consts.SyncPlatformBlockchain:
|
||||
return NewBlockchainAssetSkuService(config)
|
||||
case consts.SyncPlatformInternal:
|
||||
return NewInternalAssetSkuService(config)
|
||||
default:
|
||||
return NewDefaultAssetSkuService(platform, config)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateStockService 创建库存平台服务
|
||||
func (f *DefaultPlatformFactory) CreateStockService(platform consts.SyncPlatform) StockPlatformService {
|
||||
config, err := dao.SyncConfig.GetByPlatform(context.Background(), platform)
|
||||
if err != nil || config == nil {
|
||||
//config = &entity.SyncConfig{
|
||||
// Platform: platform,
|
||||
// IsEnabled: true,
|
||||
// APIEndpoint: f.getAPIEndpoint(platform),
|
||||
// SyncInterval: 300,
|
||||
// BatchSize: 100,
|
||||
// MaxRetries: 3,
|
||||
//}
|
||||
}
|
||||
|
||||
switch platform {
|
||||
case consts.SyncPlatformTaobao:
|
||||
return NewTaobaoStockService(config)
|
||||
case consts.SyncPlatformJD:
|
||||
return NewJDStockService(config)
|
||||
case consts.SyncPlatformDouyin:
|
||||
return NewDouyinStockService(config)
|
||||
case consts.SyncPlatformKuaishou:
|
||||
return NewKuaishouStockService(config)
|
||||
case consts.SyncPlatformXiaohongshu:
|
||||
return NewXiaohongshuStockService(config)
|
||||
case consts.SyncPlatformPinduoduo:
|
||||
return NewPinduoduoStockService(config)
|
||||
case consts.SyncPlatformXianyu:
|
||||
return NewXianyuStockService(config)
|
||||
case consts.SyncPlatformBlockchain:
|
||||
return NewBlockchainStockService(config)
|
||||
case consts.SyncPlatformInternal:
|
||||
return NewInternalStockService(config)
|
||||
default:
|
||||
return NewDefaultStockService(platform, config)
|
||||
}
|
||||
}
|
||||
|
||||
// getAPIEndpoint 根据平台获取API端点
|
||||
func (f *DefaultPlatformFactory) getAPIEndpoint(platform consts.SyncPlatform) string {
|
||||
switch platform {
|
||||
case consts.SyncPlatformTaobao:
|
||||
return "https://eco.taobao.com/router/rest"
|
||||
case consts.SyncPlatformJD:
|
||||
return "https://api.jd.com/routerjson"
|
||||
case consts.SyncPlatformDouyin:
|
||||
return "https://open.douyin.com/api"
|
||||
case consts.SyncPlatformKuaishou:
|
||||
return "https://open.kuaishou.com/api"
|
||||
case consts.SyncPlatformXiaohongshu:
|
||||
return "https://open.xiaohongshu.com/api"
|
||||
case consts.SyncPlatformPinduoduo:
|
||||
return "https://open.pinduoduo.com/api"
|
||||
case consts.SyncPlatformXianyu:
|
||||
return "https://api.xianyu.com/api"
|
||||
case consts.SyncPlatformBlockchain:
|
||||
return "https://api.blockchain.com/api"
|
||||
case consts.SyncPlatformInternal:
|
||||
return "http://localhost:3004/api"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
54
service/sync/platform_interface.go
Normal file
54
service/sync/platform_interface.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
consts "assets/consts/public"
|
||||
"context"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
// AssetPlatformService 资产平台服务接口
|
||||
type AssetPlatformService interface {
|
||||
// SyncAsset 同步资产到平台
|
||||
SyncAsset(ctx context.Context, assetID *bson.ObjectID) error
|
||||
// GetAsset 从平台获取资产
|
||||
GetAsset(ctx context.Context, assetID *bson.ObjectID) (interface{}, error)
|
||||
// UpdateAsset 更新平台资产
|
||||
UpdateAsset(ctx context.Context, assetID *bson.ObjectID, data interface{}) error
|
||||
// DeleteAsset 删除平台资产
|
||||
DeleteAsset(ctx context.Context, assetID *bson.ObjectID) error
|
||||
}
|
||||
|
||||
// AssetSkuPlatformService 资产SKU平台服务接口
|
||||
type AssetSkuPlatformService interface {
|
||||
// SyncAssetSku 同步资产SKU到平台
|
||||
SyncAssetSku(ctx context.Context, assetSkuID *bson.ObjectID) error
|
||||
// GetAssetSku 从平台获取资产SKU
|
||||
GetAssetSku(ctx context.Context, assetSkuID *bson.ObjectID) (interface{}, error)
|
||||
// UpdateAssetSku 更新平台资产SKU
|
||||
UpdateAssetSku(ctx context.Context, assetSkuID *bson.ObjectID, data interface{}) error
|
||||
// DeleteAssetSku 删除平台资产SKU
|
||||
DeleteAssetSku(ctx context.Context, assetSkuID *bson.ObjectID) error
|
||||
}
|
||||
|
||||
// StockPlatformService 库存平台服务接口
|
||||
type StockPlatformService interface {
|
||||
// SyncStock 同步库存到平台
|
||||
SyncStock(ctx context.Context, stockID *bson.ObjectID) error
|
||||
// GetStock 从平台获取库存
|
||||
GetStock(ctx context.Context, stockID *bson.ObjectID) (interface{}, error)
|
||||
// UpdateStock 更新平台库存
|
||||
UpdateStock(ctx context.Context, stockID *bson.ObjectID, data interface{}) error
|
||||
// DeleteStock 删除平台库存
|
||||
DeleteStock(ctx context.Context, stockID *bson.ObjectID) error
|
||||
}
|
||||
|
||||
// PlatformFactory 平台服务工厂
|
||||
type PlatformFactory interface {
|
||||
// CreateAssetService 创建资产平台服务
|
||||
CreateAssetService(platform consts.SyncPlatform) AssetPlatformService
|
||||
// CreateAssetSkuService 创建资产SKU平台服务
|
||||
CreateAssetSkuService(platform consts.SyncPlatform) AssetSkuPlatformService
|
||||
// CreateStockService 创建库存平台服务
|
||||
CreateStockService(platform consts.SyncPlatform) StockPlatformService
|
||||
}
|
||||
755
service/sync/platforms.go
Normal file
755
service/sync/platforms.go
Normal file
@@ -0,0 +1,755 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
consts "assets/consts/public"
|
||||
"assets/model/entity/sync"
|
||||
"context"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
// 为各平台创建占位服务
|
||||
// 实际的API调用逻辑将在后续实现
|
||||
|
||||
// TaobaoAssetService 淘宝资产服务
|
||||
type TaobaoAssetService struct {
|
||||
*BasePlatformService
|
||||
}
|
||||
|
||||
func NewTaobaoAssetService(config *entity.ChannelConfig) *TaobaoAssetService {
|
||||
return &TaobaoAssetService{
|
||||
BasePlatformService: NewBasePlatformService(consts.SyncPlatformTaobao, config),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TaobaoAssetService) SyncAsset(ctx context.Context, assetID *bson.ObjectID) error {
|
||||
// TODO: 实现淘宝资产同步逻辑
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *TaobaoAssetService) GetAsset(ctx context.Context, assetID *bson.ObjectID) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *TaobaoAssetService) UpdateAsset(ctx context.Context, assetID *bson.ObjectID, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *TaobaoAssetService) DeleteAsset(ctx context.Context, assetID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TaobaoAssetSkuService 淘宝资产SKU服务
|
||||
type TaobaoAssetSkuService struct {
|
||||
*BasePlatformService
|
||||
}
|
||||
|
||||
func NewTaobaoAssetSkuService(config *entity.ChannelConfig) *TaobaoAssetSkuService {
|
||||
return &TaobaoAssetSkuService{
|
||||
BasePlatformService: NewBasePlatformService(consts.SyncPlatformTaobao, config),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TaobaoAssetSkuService) SyncAssetSku(ctx context.Context, assetSkuID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *TaobaoAssetSkuService) GetAssetSku(ctx context.Context, assetSkuID *bson.ObjectID) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *TaobaoAssetSkuService) UpdateAssetSku(ctx context.Context, assetSkuID *bson.ObjectID, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *TaobaoAssetSkuService) DeleteAssetSku(ctx context.Context, assetSkuID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TaobaoStockService 淘宝库存服务
|
||||
type TaobaoStockService struct {
|
||||
*BasePlatformService
|
||||
}
|
||||
|
||||
func NewTaobaoStockService(config *entity.ChannelConfig) *TaobaoStockService {
|
||||
return &TaobaoStockService{
|
||||
BasePlatformService: NewBasePlatformService(consts.SyncPlatformTaobao, config),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TaobaoStockService) SyncStock(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *TaobaoStockService) GetStock(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *TaobaoStockService) UpdateStock(ctx context.Context, stockID *bson.ObjectID, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *TaobaoStockService) DeleteStock(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 其他平台的占位服务结构体...
|
||||
// 京东、抖音、快手、小红书、拼多多、闲鱼、区块链等
|
||||
// 由于代码结构相似,这里只展示模板
|
||||
|
||||
// JDAssetService 京东资产服务
|
||||
type JDAssetService struct {
|
||||
*BasePlatformService
|
||||
}
|
||||
|
||||
func NewJDAssetService(config *entity.ChannelConfig) *JDAssetService {
|
||||
return &JDAssetService{
|
||||
BasePlatformService: NewBasePlatformService(consts.SyncPlatformJD, config),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *JDAssetService) SyncAsset(ctx context.Context, assetID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *JDAssetService) GetAsset(ctx context.Context, assetID *bson.ObjectID) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *JDAssetService) UpdateAsset(ctx context.Context, assetID *bson.ObjectID, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *JDAssetService) DeleteAsset(ctx context.Context, assetID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// JDAssetSkuService 京东资产SKU服务
|
||||
type JDAssetSkuService struct {
|
||||
*BasePlatformService
|
||||
}
|
||||
|
||||
func NewJDAssetSkuService(config *entity.ChannelConfig) *JDAssetSkuService {
|
||||
return &JDAssetSkuService{
|
||||
BasePlatformService: NewBasePlatformService(consts.SyncPlatformJD, config),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *JDAssetSkuService) SyncAssetSku(ctx context.Context, assetSkuID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *JDAssetSkuService) GetAssetSku(ctx context.Context, assetSkuID *bson.ObjectID) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *JDAssetSkuService) UpdateAssetSku(ctx context.Context, assetSkuID *bson.ObjectID, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *JDAssetSkuService) DeleteAssetSku(ctx context.Context, assetSkuID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// JDStockService 京东库存服务
|
||||
type JDStockService struct {
|
||||
*BasePlatformService
|
||||
}
|
||||
|
||||
func NewJDStockService(config *entity.ChannelConfig) *JDStockService {
|
||||
return &JDStockService{
|
||||
BasePlatformService: NewBasePlatformService(consts.SyncPlatformJD, config),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *JDStockService) SyncStock(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *JDStockService) GetStock(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *JDStockService) UpdateStock(ctx context.Context, stockID *bson.ObjectID, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *JDStockService) DeleteStock(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DouyinAssetService 抖音资产服务
|
||||
type DouyinAssetService struct {
|
||||
*BasePlatformService
|
||||
}
|
||||
|
||||
func NewDouyinAssetService(config *entity.ChannelConfig) *DouyinAssetService {
|
||||
return &DouyinAssetService{
|
||||
BasePlatformService: NewBasePlatformService(consts.SyncPlatformDouyin, config),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DouyinAssetService) SyncAsset(ctx context.Context, assetID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DouyinAssetService) GetAsset(ctx context.Context, assetID *bson.ObjectID) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *DouyinAssetService) UpdateAsset(ctx context.Context, assetID *bson.ObjectID, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DouyinAssetService) DeleteAsset(ctx context.Context, assetID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DouyinAssetSkuService 抖音资产SKU服务
|
||||
type DouyinAssetSkuService struct {
|
||||
*BasePlatformService
|
||||
}
|
||||
|
||||
func NewDouyinAssetSkuService(config *entity.ChannelConfig) *DouyinAssetSkuService {
|
||||
return &DouyinAssetSkuService{
|
||||
BasePlatformService: NewBasePlatformService(consts.SyncPlatformDouyin, config),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DouyinAssetSkuService) SyncAssetSku(ctx context.Context, assetSkuID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DouyinAssetSkuService) GetAssetSku(ctx context.Context, assetSkuID *bson.ObjectID) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *DouyinAssetSkuService) UpdateAssetSku(ctx context.Context, assetSkuID *bson.ObjectID, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DouyinAssetSkuService) DeleteAssetSku(ctx context.Context, assetSkuID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DouyinStockService 抖音库存服务
|
||||
type DouyinStockService struct {
|
||||
*BasePlatformService
|
||||
}
|
||||
|
||||
func NewDouyinStockService(config *entity.ChannelConfig) *DouyinStockService {
|
||||
return &DouyinStockService{
|
||||
BasePlatformService: NewBasePlatformService(consts.SyncPlatformDouyin, config),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DouyinStockService) SyncStock(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DouyinStockService) GetStock(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *DouyinStockService) UpdateStock(ctx context.Context, stockID *bson.ObjectID, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DouyinStockService) DeleteStock(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 其他平台服务的占位函数声明
|
||||
// KuaishouAssetService, KuaishouAssetSkuService, KuaishouStockService
|
||||
// XiaohongshuAssetService, XiaohongshuAssetSkuService, XiaohongshuStockService
|
||||
// PinduoduoAssetService, PinduoduoAssetSkuService, PinduoduoStockService
|
||||
// XianyuAssetService, XianyuAssetSkuService, XianyuStockService
|
||||
// BlockchainAssetService, BlockchainAssetSkuService, BlockchainStockService
|
||||
// InternalAssetService, InternalAssetSkuService, InternalStockService
|
||||
|
||||
// KuaishouAssetService 快手资产服务
|
||||
type KuaishouAssetService struct {
|
||||
*BasePlatformService
|
||||
}
|
||||
|
||||
func NewKuaishouAssetService(config *entity.ChannelConfig) *KuaishouAssetService {
|
||||
return &KuaishouAssetService{
|
||||
BasePlatformService: NewBasePlatformService(consts.SyncPlatformKuaishou, config),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *KuaishouAssetService) SyncAsset(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *KuaishouAssetService) GetAsset(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *KuaishouAssetService) UpdateAsset(ctx context.Context, stockID *bson.ObjectID, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *KuaishouAssetService) DeleteAsset(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// KuaishouAssetSkuService 快手资产SKU服务
|
||||
type KuaishouAssetSkuService struct {
|
||||
*BasePlatformService
|
||||
}
|
||||
|
||||
func NewKuaishouAssetSkuService(config *entity.ChannelConfig) *KuaishouAssetSkuService {
|
||||
return &KuaishouAssetSkuService{
|
||||
BasePlatformService: NewBasePlatformService(consts.SyncPlatformKuaishou, config),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *KuaishouAssetSkuService) SyncAssetSku(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *KuaishouAssetSkuService) GetAssetSku(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *KuaishouAssetSkuService) UpdateAssetSku(ctx context.Context, stockID *bson.ObjectID, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *KuaishouAssetSkuService) DeleteAssetSku(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// KuaishouStockService 快手库存服务
|
||||
type KuaishouStockService struct {
|
||||
*BasePlatformService
|
||||
}
|
||||
|
||||
func NewKuaishouStockService(config *entity.ChannelConfig) *KuaishouStockService {
|
||||
return &KuaishouStockService{
|
||||
BasePlatformService: NewBasePlatformService(consts.SyncPlatformKuaishou, config),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *KuaishouStockService) SyncStock(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *KuaishouStockService) GetStock(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *KuaishouStockService) UpdateStock(ctx context.Context, stockID *bson.ObjectID, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *KuaishouStockService) DeleteStock(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// XiaohongshuAssetService 小红书资产服务
|
||||
type XiaohongshuAssetService struct {
|
||||
*BasePlatformService
|
||||
}
|
||||
|
||||
func NewXiaohongshuAssetService(config *entity.ChannelConfig) *XiaohongshuAssetService {
|
||||
return &XiaohongshuAssetService{
|
||||
BasePlatformService: NewBasePlatformService(consts.SyncPlatformXiaohongshu, config),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *XiaohongshuAssetService) SyncAsset(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *XiaohongshuAssetService) GetAsset(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *XiaohongshuAssetService) UpdateAsset(ctx context.Context, stockID *bson.ObjectID, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *XiaohongshuAssetService) DeleteAsset(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// XiaohongshuAssetSkuService 小红书资产SKU服务
|
||||
type XiaohongshuAssetSkuService struct {
|
||||
*BasePlatformService
|
||||
}
|
||||
|
||||
func NewXiaohongshuAssetSkuService(config *entity.ChannelConfig) *XiaohongshuAssetSkuService {
|
||||
return &XiaohongshuAssetSkuService{
|
||||
BasePlatformService: NewBasePlatformService(consts.SyncPlatformXiaohongshu, config),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *XiaohongshuAssetSkuService) SyncAssetSku(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *XiaohongshuAssetSkuService) GetAssetSku(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *XiaohongshuAssetSkuService) UpdateAssetSku(ctx context.Context, stockID *bson.ObjectID, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *XiaohongshuAssetSkuService) DeleteAssetSku(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// XiaohongshuStockService 小红书库存服务
|
||||
type XiaohongshuStockService struct {
|
||||
*BasePlatformService
|
||||
}
|
||||
|
||||
func NewXiaohongshuStockService(config *entity.ChannelConfig) *XiaohongshuStockService {
|
||||
return &XiaohongshuStockService{
|
||||
BasePlatformService: NewBasePlatformService(consts.SyncPlatformXiaohongshu, config),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *XiaohongshuStockService) SyncStock(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *XiaohongshuStockService) GetStock(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *XiaohongshuStockService) UpdateStock(ctx context.Context, stockID *bson.ObjectID, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *XiaohongshuStockService) DeleteStock(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// PinduoduoAssetService 拼多多资产服务
|
||||
type PinduoduoAssetService struct {
|
||||
*BasePlatformService
|
||||
}
|
||||
|
||||
func NewPinduoduoAssetService(config *entity.ChannelConfig) *PinduoduoAssetService {
|
||||
return &PinduoduoAssetService{
|
||||
BasePlatformService: NewBasePlatformService(consts.SyncPlatformPinduoduo, config),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PinduoduoAssetService) SyncAsset(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PinduoduoAssetService) GetAsset(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *PinduoduoAssetService) UpdateAsset(ctx context.Context, stockID *bson.ObjectID, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PinduoduoAssetService) DeleteAsset(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// PinduoduoAssetSkuService 拼多多资产SKU服务
|
||||
type PinduoduoAssetSkuService struct {
|
||||
*BasePlatformService
|
||||
}
|
||||
|
||||
func NewPinduoduoAssetSkuService(config *entity.ChannelConfig) *PinduoduoAssetSkuService {
|
||||
return &PinduoduoAssetSkuService{
|
||||
BasePlatformService: NewBasePlatformService(consts.SyncPlatformPinduoduo, config),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PinduoduoAssetSkuService) SyncAssetSku(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PinduoduoAssetSkuService) GetAssetSku(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *PinduoduoAssetSkuService) UpdateAssetSku(ctx context.Context, stockID *bson.ObjectID, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PinduoduoAssetSkuService) DeleteAssetSku(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// PinduoduoStockService 拼多多库存服务
|
||||
type PinduoduoStockService struct {
|
||||
*BasePlatformService
|
||||
}
|
||||
|
||||
func NewPinduoduoStockService(config *entity.ChannelConfig) *PinduoduoStockService {
|
||||
return &PinduoduoStockService{
|
||||
BasePlatformService: NewBasePlatformService(consts.SyncPlatformPinduoduo, config),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PinduoduoStockService) SyncStock(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PinduoduoStockService) GetStock(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *PinduoduoStockService) UpdateStock(ctx context.Context, stockID *bson.ObjectID, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PinduoduoStockService) DeleteStock(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// XianyuAssetService 闲鱼资产服务
|
||||
type XianyuAssetService struct {
|
||||
*BasePlatformService
|
||||
}
|
||||
|
||||
func NewXianyuAssetService(config *entity.ChannelConfig) *XianyuAssetService {
|
||||
return &XianyuAssetService{
|
||||
BasePlatformService: NewBasePlatformService(consts.SyncPlatformXianyu, config),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *XianyuAssetService) SyncAsset(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *XianyuAssetService) GetAsset(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *XianyuAssetService) UpdateAsset(ctx context.Context, stockID *bson.ObjectID, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *XianyuAssetService) DeleteAsset(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// XianyuAssetSkuService 闲鱼资产SKU服务
|
||||
type XianyuAssetSkuService struct {
|
||||
*BasePlatformService
|
||||
}
|
||||
|
||||
func NewXianyuAssetSkuService(config *entity.ChannelConfig) *XianyuAssetSkuService {
|
||||
return &XianyuAssetSkuService{
|
||||
BasePlatformService: NewBasePlatformService(consts.SyncPlatformXianyu, config),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *XianyuAssetSkuService) SyncAssetSku(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *XianyuAssetSkuService) GetAssetSku(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *XianyuAssetSkuService) UpdateAssetSku(ctx context.Context, stockID *bson.ObjectID, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *XianyuAssetSkuService) DeleteAssetSku(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// XianyuStockService 闲鱼库存服务
|
||||
type XianyuStockService struct {
|
||||
*BasePlatformService
|
||||
}
|
||||
|
||||
func NewXianyuStockService(config *entity.ChannelConfig) *XianyuStockService {
|
||||
return &XianyuStockService{
|
||||
BasePlatformService: NewBasePlatformService(consts.SyncPlatformXianyu, config),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *XianyuStockService) SyncStock(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *XianyuStockService) GetStock(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *XianyuStockService) UpdateStock(ctx context.Context, stockID *bson.ObjectID, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *XianyuStockService) DeleteStock(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// BlockchainAssetService 区块链资产服务
|
||||
type BlockchainAssetService struct {
|
||||
*BasePlatformService
|
||||
}
|
||||
|
||||
func NewBlockchainAssetService(config *entity.ChannelConfig) *BlockchainAssetService {
|
||||
return &BlockchainAssetService{
|
||||
BasePlatformService: NewBasePlatformService(consts.SyncPlatformBlockchain, config),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *BlockchainAssetService) SyncAsset(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *BlockchainAssetService) GetAsset(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *BlockchainAssetService) UpdateAsset(ctx context.Context, stockID *bson.ObjectID, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *BlockchainAssetService) DeleteAsset(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// BlockchainAssetSkuService 区块链资产SKU服务
|
||||
type BlockchainAssetSkuService struct {
|
||||
*BasePlatformService
|
||||
}
|
||||
|
||||
func NewBlockchainAssetSkuService(config *entity.ChannelConfig) *BlockchainAssetSkuService {
|
||||
return &BlockchainAssetSkuService{
|
||||
BasePlatformService: NewBasePlatformService(consts.SyncPlatformBlockchain, config),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *BlockchainAssetSkuService) SyncAssetSku(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *BlockchainAssetSkuService) GetAssetSku(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *BlockchainAssetSkuService) UpdateAssetSku(ctx context.Context, stockID *bson.ObjectID, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *BlockchainAssetSkuService) DeleteAssetSku(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// BlockchainStockService 区块链库存服务
|
||||
type BlockchainStockService struct {
|
||||
*BasePlatformService
|
||||
}
|
||||
|
||||
func NewBlockchainStockService(config *entity.ChannelConfig) *BlockchainStockService {
|
||||
return &BlockchainStockService{
|
||||
BasePlatformService: NewBasePlatformService(consts.SyncPlatformBlockchain, config),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *BlockchainStockService) SyncStock(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *BlockchainStockService) GetStock(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *BlockchainStockService) UpdateStock(ctx context.Context, stockID *bson.ObjectID, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *BlockchainStockService) DeleteStock(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// InternalAssetService 内部资产服务(使用 assets-sync 中的完整实现)
|
||||
type InternalAssetService struct {
|
||||
*BasePlatformService
|
||||
}
|
||||
|
||||
func NewInternalAssetService(config *entity.ChannelConfig) *InternalAssetService {
|
||||
return &InternalAssetService{
|
||||
BasePlatformService: NewBasePlatformService(consts.SyncPlatformInternal, config),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *InternalAssetService) SyncAsset(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
// TODO: 实现内部资产同步逻辑
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InternalAssetService) GetAsset(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *InternalAssetService) UpdateAsset(ctx context.Context, stockID *bson.ObjectID, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InternalAssetService) DeleteAsset(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// InternalAssetSkuService 内部资产SKU服务
|
||||
type InternalAssetSkuService struct {
|
||||
*BasePlatformService
|
||||
}
|
||||
|
||||
func NewInternalAssetSkuService(config *entity.ChannelConfig) *InternalAssetSkuService {
|
||||
return &InternalAssetSkuService{
|
||||
BasePlatformService: NewBasePlatformService(consts.SyncPlatformInternal, config),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *InternalAssetSkuService) SyncAssetSku(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InternalAssetSkuService) GetAssetSku(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *InternalAssetSkuService) UpdateAssetSku(ctx context.Context, stockID *bson.ObjectID, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InternalAssetSkuService) DeleteAssetSku(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// InternalStockService 内部库存服务
|
||||
type InternalStockService struct {
|
||||
*BasePlatformService
|
||||
}
|
||||
|
||||
func NewInternalStockService(config *entity.ChannelConfig) *InternalStockService {
|
||||
return &InternalStockService{
|
||||
BasePlatformService: NewBasePlatformService(consts.SyncPlatformInternal, config),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *InternalStockService) SyncStock(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InternalStockService) GetStock(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *InternalStockService) UpdateStock(ctx context.Context, stockID *bson.ObjectID, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InternalStockService) DeleteStock(ctx context.Context, stockID *bson.ObjectID) error {
|
||||
return nil
|
||||
}
|
||||
292
service/sync/sync_service.go
Normal file
292
service/sync/sync_service.go
Normal file
@@ -0,0 +1,292 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
consts "assets/consts/public"
|
||||
dao "assets/dao/sync"
|
||||
dto "assets/model/dto/sync"
|
||||
entity "assets/model/entity/sync"
|
||||
"context"
|
||||
|
||||
"gitea.com/red-future/common/utils"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/grpool"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type syncService struct{}
|
||||
|
||||
// Sync 同步服务
|
||||
var Sync = new(syncService)
|
||||
|
||||
// PlatformFactory 平台服务工厂实例
|
||||
var platformFactory = NewPlatformServiceFactory()
|
||||
|
||||
// SyncPool 同步任务协程池,限制并发数避免goroutine爆炸
|
||||
var SyncPool = grpool.New(20)
|
||||
|
||||
// CreateSyncTask 创建同步任务
|
||||
func (s *syncService) CreateSyncTask(ctx context.Context, req *dto.CreateSyncTaskReq) (*bson.ObjectID, error) {
|
||||
task := &entity.SyncTask{
|
||||
Platform: req.Platform,
|
||||
SyncType: req.SyncType,
|
||||
Status: consts.SyncStatusPending,
|
||||
AssetID: req.AssetID,
|
||||
AssetSKUID: req.AssetSKUID,
|
||||
StockID: req.StockID,
|
||||
ErrorMessage: "",
|
||||
ErrorCount: 0,
|
||||
}
|
||||
|
||||
err := dao.SyncTask.Insert(ctx, task)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return task.Id, nil
|
||||
}
|
||||
|
||||
// GetSyncTask 获取同步任务详情
|
||||
func (s *syncService) GetSyncTask(ctx context.Context, id *bson.ObjectID) (res *dto.GetSyncTaskRes, err error) {
|
||||
task, err := dao.SyncTask.GetOne(ctx, id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = &dto.GetSyncTaskRes{}
|
||||
err = utils.Struct(task, &res.SyncTaskItem)
|
||||
return
|
||||
}
|
||||
|
||||
// ListSyncTasks 获取同步任务列表
|
||||
func (s *syncService) ListSyncTasks(ctx context.Context, req *dto.ListSyncTaskReq) (list []*dto.SyncTaskItem, total int64, err error) {
|
||||
tasks, total, err := dao.SyncTask.List(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = utils.Struct(tasks, &list)
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateSyncTaskStatus 更新同步任务状态
|
||||
func (s *syncService) UpdateSyncTaskStatus(ctx context.Context, req *dto.UpdateSyncTaskStatusReq) error {
|
||||
return dao.SyncTask.UpdateStatus(ctx, req.ID, req.Status, req.ErrorMessage)
|
||||
}
|
||||
|
||||
// SyncAsset 同步资产
|
||||
func (s *syncService) SyncAsset(ctx context.Context, req *dto.SyncAssetReq) (*bson.ObjectID, error) {
|
||||
// 创建同步任务
|
||||
taskReq := &dto.CreateSyncTaskReq{
|
||||
Platform: req.Platform,
|
||||
SyncType: consts.SyncTypeIncremental,
|
||||
AssetID: req.AssetID,
|
||||
}
|
||||
|
||||
taskID, err := s.CreateSyncTask(ctx, taskReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 异步执行同步任务(使用协程池限制并发)
|
||||
asyncCtx := context.WithoutCancel(ctx)
|
||||
SyncPool.Add(asyncCtx, func(ctx context.Context) {
|
||||
s.executeAssetSync(ctx, taskID, req.AssetID, req.Platform)
|
||||
})
|
||||
|
||||
return taskID, nil
|
||||
}
|
||||
|
||||
// SyncAssetSku 同步资产SKU
|
||||
func (s *syncService) SyncAssetSku(ctx context.Context, req *dto.SyncAssetSkuReq) (*bson.ObjectID, error) {
|
||||
taskReq := &dto.CreateSyncTaskReq{
|
||||
Platform: req.Platform,
|
||||
SyncType: consts.SyncTypeIncremental,
|
||||
AssetSKUID: req.AssetSKUID,
|
||||
}
|
||||
|
||||
taskID, err := s.CreateSyncTask(ctx, taskReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 异步执行同步任务(使用协程池限制并发)
|
||||
asyncCtx := context.WithoutCancel(ctx)
|
||||
SyncPool.Add(asyncCtx, func(ctx context.Context) {
|
||||
s.executeAssetSkuSync(ctx, taskID, req.AssetSKUID, req.Platform)
|
||||
})
|
||||
|
||||
return taskID, nil
|
||||
}
|
||||
|
||||
// SyncStock 同步库存
|
||||
func (s *syncService) SyncStock(ctx context.Context, req *dto.SyncStockReq) (*bson.ObjectID, error) {
|
||||
taskReq := &dto.CreateSyncTaskReq{
|
||||
Platform: req.Platform,
|
||||
SyncType: consts.SyncTypeIncremental,
|
||||
StockID: req.StockID,
|
||||
}
|
||||
|
||||
taskID, err := s.CreateSyncTask(ctx, taskReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 异步执行同步任务(使用协程池限制并发)
|
||||
asyncCtx := context.WithoutCancel(ctx)
|
||||
SyncPool.Add(asyncCtx, func(ctx context.Context) {
|
||||
s.executeStockSync(ctx, taskID, req.StockID, req.Platform)
|
||||
})
|
||||
|
||||
return taskID, nil
|
||||
}
|
||||
|
||||
// BatchSyncAssets 批量同步资产
|
||||
func (s *syncService) BatchSyncAssets(ctx context.Context, req *dto.BatchSyncAssetsReq) ([]*bson.ObjectID, error) {
|
||||
var taskIDs []*bson.ObjectID
|
||||
for _, assetID := range req.AssetIDs {
|
||||
taskReq := &dto.CreateSyncTaskReq{
|
||||
Platform: req.Platform,
|
||||
SyncType: consts.SyncTypeIncremental,
|
||||
AssetID: assetID,
|
||||
}
|
||||
|
||||
taskID, err := s.CreateSyncTask(ctx, taskReq)
|
||||
if err != nil {
|
||||
return taskIDs, err
|
||||
}
|
||||
taskIDs = append(taskIDs, taskID)
|
||||
|
||||
// 异步执行同步任务(使用协程池限制并发)
|
||||
asyncCtx := context.WithoutCancel(ctx)
|
||||
currentAssetID := assetID
|
||||
SyncPool.Add(asyncCtx, func(ctx context.Context) {
|
||||
s.executeAssetSync(ctx, taskID, currentAssetID, req.Platform)
|
||||
})
|
||||
}
|
||||
|
||||
return taskIDs, nil
|
||||
}
|
||||
|
||||
// GetPlatformSyncStatus 获取平台同步状态
|
||||
func (s *syncService) GetPlatformSyncStatus(ctx context.Context, req *dto.GetPlatformSyncStatusReq) (*dto.GetPlatformSyncStatusRes, error) {
|
||||
// 统计各状态任务数量
|
||||
totalReq := &dto.ListSyncTaskReq{Platform: req.Platform}
|
||||
totalReq.PageNum = 1
|
||||
totalReq.PageSize = 1
|
||||
_, total, err := dao.SyncTask.List(ctx, totalReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
successReq := &dto.ListSyncTaskReq{Platform: req.Platform, Status: consts.SyncStatusSuccess}
|
||||
successReq.PageNum = 1
|
||||
successReq.PageSize = 1
|
||||
_, successCount, err := dao.SyncTask.List(ctx, successReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
failedReq := &dto.ListSyncTaskReq{Platform: req.Platform, Status: consts.SyncStatusFailed}
|
||||
failedReq.PageNum = 1
|
||||
failedReq.PageSize = 1
|
||||
_, failedCount, err := dao.SyncTask.List(ctx, failedReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dto.GetPlatformSyncStatusRes{
|
||||
Platform: req.Platform,
|
||||
IsEnabled: true,
|
||||
SyncCount: total,
|
||||
SuccessCount: successCount,
|
||||
FailedCount: failedCount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// executeAssetSync 执行资产同步
|
||||
func (s *syncService) executeAssetSync(ctx context.Context, taskID, assetID *bson.ObjectID, platform consts.SyncPlatform) {
|
||||
// 更新任务状态为同步中
|
||||
dao.SyncTask.UpdateStatus(ctx, taskID, consts.SyncStatusSyncing, "")
|
||||
|
||||
// 获取平台服务
|
||||
assetService := platformFactory.CreateAssetService(platform)
|
||||
|
||||
// 执行同步
|
||||
err := assetService.SyncAsset(ctx, assetID)
|
||||
if err != nil {
|
||||
// 同步失败
|
||||
g.Log().Error(ctx, "资产同步失败", g.Map{
|
||||
"task_id": taskID,
|
||||
"asset_id": assetID,
|
||||
"platform": string(platform),
|
||||
"error": err.Error(),
|
||||
})
|
||||
dao.SyncTask.UpdateStatus(ctx, taskID, consts.SyncStatusFailed, err.Error())
|
||||
} else {
|
||||
// 同步成功
|
||||
g.Log().Info(ctx, "资产同步成功", g.Map{
|
||||
"task_id": taskID,
|
||||
"asset_id": assetID,
|
||||
"platform": string(platform),
|
||||
})
|
||||
dao.SyncTask.UpdateStatus(ctx, taskID, consts.SyncStatusSuccess, "")
|
||||
}
|
||||
}
|
||||
|
||||
// executeAssetSkuSync 执行资产SKU同步
|
||||
func (s *syncService) executeAssetSkuSync(ctx context.Context, taskID, assetSkuID *bson.ObjectID, platform consts.SyncPlatform) {
|
||||
// 更新任务状态为同步中
|
||||
dao.SyncTask.UpdateStatus(ctx, taskID, consts.SyncStatusSyncing, "")
|
||||
|
||||
// 获取平台服务
|
||||
assetSkuService := platformFactory.CreateAssetSkuService(platform)
|
||||
|
||||
// 执行同步
|
||||
err := assetSkuService.SyncAssetSku(ctx, assetSkuID)
|
||||
if err != nil {
|
||||
// 同步失败
|
||||
g.Log().Error(ctx, "资产SKU同步失败", g.Map{
|
||||
"task_id": taskID,
|
||||
"asset_sku_id": assetSkuID,
|
||||
"platform": string(platform),
|
||||
"error": err.Error(),
|
||||
})
|
||||
dao.SyncTask.UpdateStatus(ctx, taskID, consts.SyncStatusFailed, err.Error())
|
||||
} else {
|
||||
// 同步成功
|
||||
g.Log().Info(ctx, "资产SKU同步成功", g.Map{
|
||||
"task_id": taskID,
|
||||
"asset_sku_id": assetSkuID,
|
||||
"platform": string(platform),
|
||||
})
|
||||
dao.SyncTask.UpdateStatus(ctx, taskID, consts.SyncStatusSuccess, "")
|
||||
}
|
||||
}
|
||||
|
||||
// executeStockSync 执行库存同步
|
||||
func (s *syncService) executeStockSync(ctx context.Context, taskID, stockID *bson.ObjectID, platform consts.SyncPlatform) {
|
||||
// 更新任务状态为同步中
|
||||
dao.SyncTask.UpdateStatus(ctx, taskID, consts.SyncStatusSyncing, "")
|
||||
|
||||
// 获取平台服务
|
||||
stockService := platformFactory.CreateStockService(platform)
|
||||
|
||||
// 执行同步
|
||||
err := stockService.SyncStock(ctx, stockID)
|
||||
if err != nil {
|
||||
// 同步失败
|
||||
g.Log().Error(ctx, "库存同步失败", g.Map{
|
||||
"task_id": taskID,
|
||||
"stock_id": stockID,
|
||||
"platform": string(platform),
|
||||
"error": err.Error(),
|
||||
})
|
||||
dao.SyncTask.UpdateStatus(ctx, taskID, consts.SyncStatusFailed, err.Error())
|
||||
} else {
|
||||
// 同步成功
|
||||
g.Log().Info(ctx, "库存同步成功", g.Map{
|
||||
"task_id": taskID,
|
||||
"stock_id": stockID,
|
||||
"platform": string(platform),
|
||||
})
|
||||
dao.SyncTask.UpdateStatus(ctx, taskID, consts.SyncStatusSuccess, "")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user