Dockerfile

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

View File

@@ -0,0 +1,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)
}

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

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

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

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

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

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

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

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

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

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

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

View File

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

File diff suppressed because it is too large Load Diff

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

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

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

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

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

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

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

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

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

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

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

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

View 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, "")
}
}