2026-03-18 10:18:03 +08:00
|
|
|
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"
|
|
|
|
|
"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) {
|
|
|
|
|
|
|
|
|
|
// 构建分类路径和层级
|
2026-03-19 17:45:06 +08:00
|
|
|
parent, err := dao.Category.GetOne(ctx, &dto.GetCategoryReq{Id: req.ParentId})
|
2026-03-18 10:18:03 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-03-19 17:45:06 +08:00
|
|
|
if g.IsEmpty(parent) {
|
|
|
|
|
return nil, errors.New("父分类不存在")
|
|
|
|
|
}
|
|
|
|
|
req.Path = parent.Path + DefaultPathSeparator + gconv.String(req.ParentId)
|
2026-03-18 10:18:03 +08:00
|
|
|
req.Level = parent.Level + 1
|
|
|
|
|
|
|
|
|
|
err = gfdb.DB(ctx).Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
2026-03-19 17:45:06 +08:00
|
|
|
var id int64
|
|
|
|
|
req.IsLeafNode = gconv.PtrBool(true)
|
|
|
|
|
if id, err = dao.Category.Insert(ctx, req); err != nil {
|
2026-03-18 10:18:03 +08:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新父节点的 IsLeafNode 为 false
|
|
|
|
|
if err = s.updateLeafNode(ctx, req.ParentId, false); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res = &dto.CreateCategoryRes{
|
2026-03-19 17:45:06 +08:00
|
|
|
Id: id,
|
2026-03-18 10:18:03 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// updateLeafNode 更新分类的 IsLeafNode 字段
|
2026-03-19 17:45:06 +08:00
|
|
|
func (s *CategoryService) updateLeafNode(ctx context.Context, categoryId int64, isLeaf bool) (err error) {
|
|
|
|
|
_, err = dao.Category.Update(ctx, &dto.UpdateCategoryReq{
|
|
|
|
|
Id: categoryId,
|
|
|
|
|
IsLeafNode: &isLeaf,
|
2026-03-18 10:18:03 +08:00
|
|
|
})
|
2026-03-19 17:45:06 +08:00
|
|
|
return
|
2026-03-18 10:18:03 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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)
|
2026-03-19 17:45:06 +08:00
|
|
|
|
|
|
|
|
if err = gconv.Scan(one, &res); err != nil {
|
|
|
|
|
panic(err)
|
2026-03-18 10:18:03 +08:00
|
|
|
}
|
2026-03-19 17:45:06 +08:00
|
|
|
|
|
|
|
|
return res, err
|
2026-03-18 10:18:03 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
}
|
2026-03-19 17:45:06 +08:00
|
|
|
tree, err := s.buildTree(ctx, list, 0)
|
2026-03-18 10:18:03 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return &dto.GetCategoryTreeRes{Tree: tree}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// buildTree 构建树形结构
|
2026-03-19 17:45:06 +08:00
|
|
|
func (s *CategoryService) buildTree(ctx context.Context, categories []entity.Category, parentId int64) ([]*dto.CategoryTreeNode, error) {
|
2026-03-18 10:18:03 +08:00
|
|
|
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 判断分类是否为指定父节点的子节点
|
2026-03-19 17:45:06 +08:00
|
|
|
func (s *CategoryService) isChildOf(childParentId, parentId int64) bool {
|
2026-03-18 10:18:03 +08:00
|
|
|
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
|
|
|
|
|
}
|
2026-03-19 17:45:06 +08:00
|
|
|
children, err := s.buildTree(ctx, allCategories, category.Id)
|
2026-03-18 10:18:03 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return res, err
|
|
|
|
|
}
|
|
|
|
|
res.Children = children
|
|
|
|
|
res.IsLeafNode = len(children) == 0
|
|
|
|
|
return res, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// hasChildren 检查分类是否有子分类
|
2026-03-19 17:45:06 +08:00
|
|
|
func (s *CategoryService) hasChildren(ctx context.Context, parentId int64) (bool, error) {
|
2026-03-18 10:18:03 +08:00
|
|
|
count, err := dao.Category.Count(ctx, &dto.ListCategoryReq{
|
|
|
|
|
ParentId: parentId,
|
|
|
|
|
})
|
|
|
|
|
return count > 0, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// updateLeafNodeIfNoChildren 如果父分类没有子分类了,更新为叶子节点
|
2026-03-19 17:45:06 +08:00
|
|
|
func (s *CategoryService) updateLeafNodeIfNoChildren(ctx context.Context, parentId int64) error {
|
2026-03-18 10:18:03 +08:00
|
|
|
hasChildren, err := s.hasChildren(ctx, parentId)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return s.updateLeafNode(ctx, parentId, !hasChildren)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// UpdateStatus 更新分类状态
|
2026-03-19 17:45:06 +08:00
|
|
|
func (s *CategoryService) UpdateStatus(ctx context.Context, req *dto.UpdateCategoryStatusReq) (err error) {
|
2026-03-18 10:18:03 +08:00
|
|
|
var updateReq *dto.UpdateCategoryReq
|
2026-03-19 17:45:06 +08:00
|
|
|
if err = gconv.Struct(req, &updateReq); err != nil {
|
|
|
|
|
return
|
2026-03-18 10:18:03 +08:00
|
|
|
}
|
2026-03-19 17:45:06 +08:00
|
|
|
_, err = dao.Category.Update(ctx, updateReq)
|
|
|
|
|
return
|
2026-03-18 10:18:03 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 {
|
2026-03-19 17:45:06 +08:00
|
|
|
if rows, err := dao.Category.Update(ctx, req); err != nil && rows > 0 {
|
2026-03-18 10:18:03 +08:00
|
|
|
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 判断父分类是否发生变化
|
2026-03-19 17:45:06 +08:00
|
|
|
func (s *CategoryService) parentIdChanged(newParentId, oldParentId int64) bool {
|
2026-03-18 10:18:03 +08:00
|
|
|
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 {
|
|
|
|
|
// 检查是否有子分类
|
2026-03-19 17:45:06 +08:00
|
|
|
hasChildren, err := s.hasChildren(ctx, req.Id)
|
2026-03-18 10:18:03 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if hasChildren {
|
|
|
|
|
return errors.New("该分类下有子分类,无法删除")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查是否有资产
|
|
|
|
|
if count, err := dao.Asset.Count(ctx, &dto.ListAssetReq{
|
2026-03-19 17:45:06 +08:00
|
|
|
CategoryId: req.Id,
|
2026-03-18 10:18:03 +08:00
|
|
|
}); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
} else if count > 0 {
|
|
|
|
|
return errors.New("该分类下有资产信息,无法删除")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取分类信息用于后续操作
|
2026-03-19 17:45:06 +08:00
|
|
|
category, err := dao.Category.GetOne(ctx, &dto.GetCategoryReq{Id: req.Id})
|
2026-03-18 10:18:03 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
err = gfdb.DB(ctx).Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
|
|
|
|
// 删除分类
|
2026-03-19 17:45:06 +08:00
|
|
|
if rows, err := dao.Category.Delete(ctx, req); err != nil && rows > 0 {
|
2026-03-18 10:18:03 +08:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 如果删除的是叶子节点,更新父节点为叶子节点
|
|
|
|
|
if !g.IsEmpty(category.ParentId) {
|
|
|
|
|
return s.updateLeafNodeIfNoChildren(ctx, category.ParentId)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|