This commit is contained in:
2026-01-09 15:58:09 +08:00
parent bd76520d22
commit ead5f17b7c
18 changed files with 1175 additions and 121 deletions

View File

@@ -2,17 +2,18 @@ package commands
// CreateProductCommand 创建产品命令 // CreateProductCommand 创建产品命令
type CreateProductCommand struct { type CreateProductCommand struct {
Name string `json:"name" binding:"required,min=2,max=100" comment:"产品名称"` Name string `json:"name" binding:"required,min=2,max=100" comment:"产品名称"`
Code string `json:"code" binding:"required,product_code" comment:"产品编号"` Code string `json:"code" binding:"required,product_code" comment:"产品编号"`
Description string `json:"description" binding:"omitempty,max=500" comment:"产品描述"` Description string `json:"description" binding:"omitempty,max=500" comment:"产品描述"`
Content string `json:"content" binding:"omitempty,max=5000" comment:"产品内容"` Content string `json:"content" binding:"omitempty,max=5000" comment:"产品内容"`
CategoryID string `json:"category_id" binding:"required,uuid" comment:"产品分类ID"` CategoryID string `json:"category_id" binding:"required,uuid" comment:"一级分类ID"`
Price float64 `json:"price" binding:"price,min=0" comment:"产品价格"` SubCategoryID *string `json:"sub_category_id" binding:"omitempty,uuid" comment:"二级分类ID"`
CostPrice float64 `json:"cost_price" binding:"omitempty,min=0" comment:"成本价"` Price float64 `json:"price" binding:"price,min=0" comment:"产品价格"`
Remark string `json:"remark" binding:"omitempty,max=1000" comment:"备注"` CostPrice float64 `json:"cost_price" binding:"omitempty,min=0" comment:"成本价"`
IsEnabled bool `json:"is_enabled" comment:"是否启用"` Remark string `json:"remark" binding:"omitempty,max=1000" comment:"备注"`
IsVisible bool `json:"is_visible" comment:"是否展示"` IsEnabled bool `json:"is_enabled" comment:"是否启用"`
IsPackage bool `json:"is_package" comment:"是否组合包"` IsVisible bool `json:"is_visible" comment:"是否展示"`
IsPackage bool `json:"is_package" comment:"是否组合包"`
// UI组件相关字段 // UI组件相关字段
SellUIComponent bool `json:"sell_ui_component" comment:"是否出售UI组件"` SellUIComponent bool `json:"sell_ui_component" comment:"是否出售UI组件"`
@@ -26,18 +27,19 @@ type CreateProductCommand struct {
// UpdateProductCommand 更新产品命令 // UpdateProductCommand 更新产品命令
type UpdateProductCommand struct { type UpdateProductCommand struct {
ID string `json:"-" uri:"id" binding:"required,uuid" comment:"产品ID"` ID string `json:"-" uri:"id" binding:"required,uuid" comment:"产品ID"`
Name string `json:"name" binding:"required,min=2,max=100" comment:"产品名称"` Name string `json:"name" binding:"required,min=2,max=100" comment:"产品名称"`
Code string `json:"code" binding:"required,product_code" comment:"产品编号"` Code string `json:"code" binding:"required,product_code" comment:"产品编号"`
Description string `json:"description" binding:"omitempty,max=500" comment:"产品描述"` Description string `json:"description" binding:"omitempty,max=500" comment:"产品描述"`
Content string `json:"content" binding:"omitempty,max=5000" comment:"产品内容"` Content string `json:"content" binding:"omitempty,max=5000" comment:"产品内容"`
CategoryID string `json:"category_id" binding:"required,uuid" comment:"产品分类ID"` CategoryID string `json:"category_id" binding:"required,uuid" comment:"一级分类ID"`
Price float64 `json:"price" binding:"price,min=0" comment:"产品价格"` SubCategoryID *string `json:"sub_category_id" binding:"omitempty,uuid" comment:"二级分类ID"`
CostPrice float64 `json:"cost_price" binding:"omitempty,min=0" comment:"成本价"` Price float64 `json:"price" binding:"price,min=0" comment:"产品价格"`
Remark string `json:"remark" binding:"omitempty,max=1000" comment:"备注"` CostPrice float64 `json:"cost_price" binding:"omitempty,min=0" comment:"成本价"`
IsEnabled bool `json:"is_enabled" comment:"是否启用"` Remark string `json:"remark" binding:"omitempty,max=1000" comment:"备注"`
IsVisible bool `json:"is_visible" comment:"是否展示"` IsEnabled bool `json:"is_enabled" comment:"是否启用"`
IsPackage bool `json:"is_package" comment:"是否组合包"` IsVisible bool `json:"is_visible" comment:"是否展示"`
IsPackage bool `json:"is_package" comment:"是否组合包"`
// UI组件相关字段 // UI组件相关字段
SellUIComponent bool `json:"sell_ui_component" comment:"是否出售UI组件"` SellUIComponent bool `json:"sell_ui_component" comment:"是否出售UI组件"`

View File

@@ -0,0 +1,29 @@
package commands
// CreateSubCategoryCommand 创建二级分类命令
type CreateSubCategoryCommand struct {
Name string `json:"name" binding:"required,min=2,max=100" comment:"二级分类名称"`
Code string `json:"code" binding:"required,min=2,max=50" comment:"二级分类编号"`
Description string `json:"description" binding:"omitempty,max=500" comment:"二级分类描述"`
CategoryID string `json:"category_id" binding:"required,uuid" comment:"一级分类ID"`
Sort int `json:"sort" binding:"min=0" comment:"排序"`
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
IsVisible bool `json:"is_visible" comment:"是否展示"`
}
// UpdateSubCategoryCommand 更新二级分类命令
type UpdateSubCategoryCommand struct {
ID string `json:"-" uri:"id" binding:"required,uuid" comment:"二级分类ID"`
Name string `json:"name" binding:"required,min=2,max=100" comment:"二级分类名称"`
Code string `json:"code" binding:"required,min=2,max=50" comment:"二级分类编号"`
Description string `json:"description" binding:"omitempty,max=500" comment:"二级分类描述"`
CategoryID string `json:"category_id" binding:"required,uuid" comment:"一级分类ID"`
Sort int `json:"sort" binding:"min=0" comment:"排序"`
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
IsVisible bool `json:"is_visible" comment:"是否展示"`
}
// DeleteSubCategoryCommand 删除二级分类命令
type DeleteSubCategoryCommand struct {
ID string `json:"-" uri:"id" binding:"required,uuid" comment:"二级分类ID"`
}

View File

@@ -0,0 +1,17 @@
package queries
// GetSubCategoryQuery 获取二级分类查询
type GetSubCategoryQuery struct {
ID string `json:"id" form:"id" binding:"omitempty,uuid" comment:"二级分类ID"`
}
// ListSubCategoriesQuery 获取二级分类列表查询
type ListSubCategoriesQuery struct {
Page int `json:"page" form:"page" binding:"min=1" comment:"页码"`
PageSize int `json:"page_size" form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
CategoryID string `json:"category_id" form:"category_id" binding:"omitempty,uuid" comment:"一级分类ID"`
IsEnabled *bool `json:"is_enabled" form:"is_enabled" comment:"是否启用"`
IsVisible *bool `json:"is_visible" form:"is_visible" comment:"是否展示"`
SortBy string `json:"sort_by" form:"sort_by" comment:"排序字段"`
SortOrder string `json:"sort_order" form:"sort_order" comment:"排序方向"`
}

View File

@@ -4,13 +4,13 @@ import "time"
// CategoryInfoResponse 分类详情响应 // CategoryInfoResponse 分类详情响应
type CategoryInfoResponse struct { type CategoryInfoResponse struct {
ID string `json:"id" comment:"分类ID"` ID string `json:"id" comment:"分类ID"`
Name string `json:"name" comment:"分类名称"` Name string `json:"name" comment:"分类名称"`
Code string `json:"code" comment:"分类编号"` Code string `json:"code" comment:"分类编号"`
Description string `json:"description" comment:"分类描述"` Description string `json:"description" comment:"分类描述"`
Sort int `json:"sort" comment:"排序"` Sort int `json:"sort" comment:"排序"`
IsEnabled bool `json:"is_enabled" comment:"是否启用"` IsEnabled bool `json:"is_enabled" comment:"是否启用"`
IsVisible bool `json:"is_visible" comment:"是否展示"` IsVisible bool `json:"is_visible" comment:"是否展示"`
CreatedAt time.Time `json:"created_at" comment:"创建时间"` CreatedAt time.Time `json:"created_at" comment:"创建时间"`
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"` UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
@@ -18,9 +18,9 @@ type CategoryInfoResponse struct {
// CategoryListResponse 分类列表响应 // CategoryListResponse 分类列表响应
type CategoryListResponse struct { type CategoryListResponse struct {
Total int64 `json:"total" comment:"总数"` Total int64 `json:"total" comment:"总数"`
Page int `json:"page" comment:"页码"` Page int `json:"page" comment:"页码"`
Size int `json:"size" comment:"每页数量"` Size int `json:"size" comment:"每页数量"`
Items []CategoryInfoResponse `json:"items" comment:"分类列表"` Items []CategoryInfoResponse `json:"items" comment:"分类列表"`
} }
@@ -30,3 +30,37 @@ type CategorySimpleResponse struct {
Name string `json:"name" comment:"分类名称"` Name string `json:"name" comment:"分类名称"`
Code string `json:"code" comment:"分类编号"` Code string `json:"code" comment:"分类编号"`
} }
// SubCategoryInfoResponse 二级分类详情响应
type SubCategoryInfoResponse struct {
ID string `json:"id" comment:"二级分类ID"`
Name string `json:"name" comment:"二级分类名称"`
Code string `json:"code" comment:"二级分类编号"`
Description string `json:"description" comment:"二级分类描述"`
CategoryID string `json:"category_id" comment:"一级分类ID"`
Sort int `json:"sort" comment:"排序"`
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
IsVisible bool `json:"is_visible" comment:"是否展示"`
// 关联信息
Category *CategoryInfoResponse `json:"category,omitempty" comment:"一级分类信息"`
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
}
// SubCategoryListResponse 二级分类列表响应
type SubCategoryListResponse struct {
Total int64 `json:"total" comment:"总数"`
Page int `json:"page" comment:"页码"`
Size int `json:"size" comment:"每页数量"`
Items []SubCategoryInfoResponse `json:"items" comment:"二级分类列表"`
}
// SubCategorySimpleResponse 二级分类简单信息响应
type SubCategorySimpleResponse struct {
ID string `json:"id" comment:"二级分类ID"`
Name string `json:"name" comment:"二级分类名称"`
Code string `json:"code" comment:"二级分类编号"`
CategoryID string `json:"category_id" comment:"一级分类ID"`
}

View File

@@ -15,17 +15,18 @@ type PackageItemResponse struct {
// ProductInfoResponse 产品详情响应 // ProductInfoResponse 产品详情响应
type ProductInfoResponse struct { type ProductInfoResponse struct {
ID string `json:"id" comment:"产品ID"` ID string `json:"id" comment:"产品ID"`
OldID *string `json:"old_id,omitempty" comment:"旧产品ID"` OldID *string `json:"old_id,omitempty" comment:"旧产品ID"`
Name string `json:"name" comment:"产品名称"` Name string `json:"name" comment:"产品名称"`
Code string `json:"code" comment:"产品编号"` Code string `json:"code" comment:"产品编号"`
Description string `json:"description" comment:"产品简介"` Description string `json:"description" comment:"产品简介"`
Content string `json:"content" comment:"产品内容"` Content string `json:"content" comment:"产品内容"`
CategoryID string `json:"category_id" comment:"产品分类ID"` CategoryID string `json:"category_id" comment:"一级分类ID"`
Price float64 `json:"price" comment:"产品价格"` SubCategoryID *string `json:"sub_category_id,omitempty" comment:"二级分类ID"`
IsEnabled bool `json:"is_enabled" comment:"是否启用"` Price float64 `json:"price" comment:"产品价格"`
IsPackage bool `json:"is_package" comment:"是否组合包"` IsEnabled bool `json:"is_enabled" comment:"是否启用"`
IsSubscribed *bool `json:"is_subscribed,omitempty" comment:"当前用户是否已订阅"` IsPackage bool `json:"is_package" comment:"是否组合包"`
IsSubscribed *bool `json:"is_subscribed,omitempty" comment:"当前用户是否已订阅"`
// UI组件相关字段 // UI组件相关字段
SellUIComponent bool `json:"sell_ui_component" comment:"是否出售UI组件"` SellUIComponent bool `json:"sell_ui_component" comment:"是否出售UI组件"`
UIComponentPrice float64 `json:"ui_component_price" comment:"UI组件销售价格组合包使用"` UIComponentPrice float64 `json:"ui_component_price" comment:"UI组件销售价格组合包使用"`
@@ -36,7 +37,8 @@ type ProductInfoResponse struct {
SEOKeywords string `json:"seo_keywords" comment:"SEO关键词"` SEOKeywords string `json:"seo_keywords" comment:"SEO关键词"`
// 关联信息 // 关联信息
Category *CategoryInfoResponse `json:"category,omitempty" comment:"分类信息"` Category *CategoryInfoResponse `json:"category,omitempty" comment:"一级分类信息"`
SubCategory *SubCategoryInfoResponse `json:"sub_category,omitempty" comment:"二级分类信息"`
// 组合包信息 // 组合包信息
PackageItems []*PackageItemResponse `json:"package_items,omitempty" comment:"组合包项目列表"` PackageItems []*PackageItemResponse `json:"package_items,omitempty" comment:"组合包项目列表"`
@@ -91,19 +93,20 @@ type ProductStatsResponse struct {
// ProductAdminInfoResponse 管理员产品详情响应 // ProductAdminInfoResponse 管理员产品详情响应
type ProductAdminInfoResponse struct { type ProductAdminInfoResponse struct {
ID string `json:"id" comment:"产品ID"` ID string `json:"id" comment:"产品ID"`
OldID *string `json:"old_id,omitempty" comment:"旧产品ID"` OldID *string `json:"old_id,omitempty" comment:"旧产品ID"`
Name string `json:"name" comment:"产品名称"` Name string `json:"name" comment:"产品名称"`
Code string `json:"code" comment:"产品编号"` Code string `json:"code" comment:"产品编号"`
Description string `json:"description" comment:"产品简介"` Description string `json:"description" comment:"产品简介"`
Content string `json:"content" comment:"产品内容"` Content string `json:"content" comment:"产品内容"`
CategoryID string `json:"category_id" comment:"产品分类ID"` CategoryID string `json:"category_id" comment:"一级分类ID"`
Price float64 `json:"price" comment:"产品价格"` SubCategoryID *string `json:"sub_category_id,omitempty" comment:"二级分类ID"`
CostPrice float64 `json:"cost_price" comment:"成本价"` Price float64 `json:"price" comment:"产品价格"`
Remark string `json:"remark" comment:"备注"` CostPrice float64 `json:"cost_price" comment:"成本价"`
IsEnabled bool `json:"is_enabled" comment:"是否启用"` Remark string `json:"remark" comment:"备注"`
IsVisible bool `json:"is_visible" comment:"是否可见"` IsEnabled bool `json:"is_enabled" comment:"是否启用"`
IsPackage bool `json:"is_package" comment:"是否组合包"` IsVisible bool `json:"is_visible" comment:"是否可见"`
IsPackage bool `json:"is_package" comment:"是否组合包"`
// UI组件相关字段 // UI组件相关字段
SellUIComponent bool `json:"sell_ui_component" comment:"是否出售UI组件"` SellUIComponent bool `json:"sell_ui_component" comment:"是否出售UI组件"`
@@ -115,7 +118,8 @@ type ProductAdminInfoResponse struct {
SEOKeywords string `json:"seo_keywords" comment:"SEO关键词"` SEOKeywords string `json:"seo_keywords" comment:"SEO关键词"`
// 关联信息 // 关联信息
Category *CategoryInfoResponse `json:"category,omitempty" comment:"分类信息"` Category *CategoryInfoResponse `json:"category,omitempty" comment:"一级分类信息"`
SubCategory *SubCategoryInfoResponse `json:"sub_category,omitempty" comment:"二级分类信息"`
// 组合包信息 // 组合包信息
PackageItems []*PackageItemResponse `json:"package_items,omitempty" comment:"组合包项目列表"` PackageItems []*PackageItemResponse `json:"package_items,omitempty" comment:"组合包项目列表"`

View File

@@ -50,7 +50,7 @@ func NewProductApplicationService(
} }
// CreateProduct 创建产品 // CreateProduct 创建产品
// 业务流程<EFBFBD>?. 构建产品实体 2. 创建产品 // 业务流程1. 构建产品实体 2. 创建产品
func (s *ProductApplicationServiceImpl) CreateProduct(ctx context.Context, cmd *commands.CreateProductCommand) (*responses.ProductAdminInfoResponse, error) { func (s *ProductApplicationServiceImpl) CreateProduct(ctx context.Context, cmd *commands.CreateProductCommand) (*responses.ProductAdminInfoResponse, error) {
// 1. 构建产品实体 // 1. 构建产品实体
product := &entities.Product{ product := &entities.Product{
@@ -59,6 +59,7 @@ func (s *ProductApplicationServiceImpl) CreateProduct(ctx context.Context, cmd *
Description: cmd.Description, Description: cmd.Description,
Content: cmd.Content, Content: cmd.Content,
CategoryID: cmd.CategoryID, CategoryID: cmd.CategoryID,
SubCategoryID: cmd.SubCategoryID,
Price: decimal.NewFromFloat(cmd.Price), Price: decimal.NewFromFloat(cmd.Price),
CostPrice: decimal.NewFromFloat(cmd.CostPrice), CostPrice: decimal.NewFromFloat(cmd.CostPrice),
Remark: cmd.Remark, Remark: cmd.Remark,
@@ -97,6 +98,7 @@ func (s *ProductApplicationServiceImpl) UpdateProduct(ctx context.Context, cmd *
existingProduct.Description = cmd.Description existingProduct.Description = cmd.Description
existingProduct.Content = cmd.Content existingProduct.Content = cmd.Content
existingProduct.CategoryID = cmd.CategoryID existingProduct.CategoryID = cmd.CategoryID
existingProduct.SubCategoryID = cmd.SubCategoryID
existingProduct.Price = decimal.NewFromFloat(cmd.Price) existingProduct.Price = decimal.NewFromFloat(cmd.Price)
existingProduct.CostPrice = decimal.NewFromFloat(cmd.CostPrice) existingProduct.CostPrice = decimal.NewFromFloat(cmd.CostPrice)
existingProduct.Remark = cmd.Remark existingProduct.Remark = cmd.Remark
@@ -490,30 +492,36 @@ func (s *ProductApplicationServiceImpl) GetProductByIDForUser(ctx context.Contex
// convertToProductInfoResponse 转换为产品信息响应 // convertToProductInfoResponse 转换为产品信息响应
func (s *ProductApplicationServiceImpl) convertToProductInfoResponse(product *entities.Product) *responses.ProductInfoResponse { func (s *ProductApplicationServiceImpl) convertToProductInfoResponse(product *entities.Product) *responses.ProductInfoResponse {
response := &responses.ProductInfoResponse{ response := &responses.ProductInfoResponse{
ID: product.ID, ID: product.ID,
OldID: product.OldID, OldID: product.OldID,
Name: product.Name, Name: product.Name,
Code: product.Code, Code: product.Code,
Description: product.Description, Description: product.Description,
Content: product.Content, Content: product.Content,
CategoryID: product.CategoryID, CategoryID: product.CategoryID,
Price: product.Price.InexactFloat64(), SubCategoryID: product.SubCategoryID,
IsEnabled: product.IsEnabled, Price: product.Price.InexactFloat64(),
IsPackage: product.IsPackage, IsEnabled: product.IsEnabled,
SellUIComponent: product.SellUIComponent, IsPackage: product.IsPackage,
SellUIComponent: product.SellUIComponent,
UIComponentPrice: product.UIComponentPrice.InexactFloat64(), UIComponentPrice: product.UIComponentPrice.InexactFloat64(),
SEOTitle: product.SEOTitle, SEOTitle: product.SEOTitle,
SEODescription: product.SEODescription, SEODescription: product.SEODescription,
SEOKeywords: product.SEOKeywords, SEOKeywords: product.SEOKeywords,
CreatedAt: product.CreatedAt, CreatedAt: product.CreatedAt,
UpdatedAt: product.UpdatedAt, UpdatedAt: product.UpdatedAt,
} }
// 添加分类信息 // 添加一级分类信息
if product.Category != nil { if product.Category != nil {
response.Category = s.convertToCategoryInfoResponse(product.Category) response.Category = s.convertToCategoryInfoResponse(product.Category)
} }
// 添加二级分类信息
if product.SubCategory != nil {
response.SubCategory = s.convertToSubCategoryInfoResponse(product.SubCategory)
}
// 转换组合包项目信息 // 转换组合包项目信息
if product.IsPackage && len(product.PackageItems) > 0 { if product.IsPackage && len(product.PackageItems) > 0 {
response.PackageItems = make([]*responses.PackageItemResponse, len(product.PackageItems)) response.PackageItems = make([]*responses.PackageItemResponse, len(product.PackageItems))
@@ -536,33 +544,39 @@ func (s *ProductApplicationServiceImpl) convertToProductInfoResponse(product *en
// convertToProductAdminInfoResponse 转换为管理员产品信息响应 // convertToProductAdminInfoResponse 转换为管理员产品信息响应
func (s *ProductApplicationServiceImpl) convertToProductAdminInfoResponse(product *entities.Product) *responses.ProductAdminInfoResponse { func (s *ProductApplicationServiceImpl) convertToProductAdminInfoResponse(product *entities.Product) *responses.ProductAdminInfoResponse {
response := &responses.ProductAdminInfoResponse{ response := &responses.ProductAdminInfoResponse{
ID: product.ID, ID: product.ID,
OldID: product.OldID, OldID: product.OldID,
Name: product.Name, Name: product.Name,
Code: product.Code, Code: product.Code,
Description: product.Description, Description: product.Description,
Content: product.Content, Content: product.Content,
CategoryID: product.CategoryID, CategoryID: product.CategoryID,
Price: product.Price.InexactFloat64(), SubCategoryID: product.SubCategoryID,
CostPrice: product.CostPrice.InexactFloat64(), Price: product.Price.InexactFloat64(),
Remark: product.Remark, CostPrice: product.CostPrice.InexactFloat64(),
IsEnabled: product.IsEnabled, Remark: product.Remark,
IsVisible: product.IsVisible, // 管理员可以看到可见状态 IsEnabled: product.IsEnabled,
IsPackage: product.IsPackage, IsVisible: product.IsVisible, // 管理员可以看到可见状态
SellUIComponent: product.SellUIComponent, IsPackage: product.IsPackage,
SellUIComponent: product.SellUIComponent,
UIComponentPrice: product.UIComponentPrice.InexactFloat64(), UIComponentPrice: product.UIComponentPrice.InexactFloat64(),
SEOTitle: product.SEOTitle, SEOTitle: product.SEOTitle,
SEODescription: product.SEODescription, SEODescription: product.SEODescription,
SEOKeywords: product.SEOKeywords, SEOKeywords: product.SEOKeywords,
CreatedAt: product.CreatedAt, CreatedAt: product.CreatedAt,
UpdatedAt: product.UpdatedAt, UpdatedAt: product.UpdatedAt,
} }
// 添加分类信息 // 添加一级分类信息
if product.Category != nil { if product.Category != nil {
response.Category = s.convertToCategoryInfoResponse(product.Category) response.Category = s.convertToCategoryInfoResponse(product.Category)
} }
// 添加二级分类信息
if product.SubCategory != nil {
response.SubCategory = s.convertToSubCategoryInfoResponse(product.SubCategory)
}
// 转换组合包项目信息 // 转换组合包项目信息
if product.IsPackage && len(product.PackageItems) > 0 { if product.IsPackage && len(product.PackageItems) > 0 {
response.PackageItems = make([]*responses.PackageItemResponse, len(product.PackageItems)) response.PackageItems = make([]*responses.PackageItemResponse, len(product.PackageItems))
@@ -594,6 +608,28 @@ func (s *ProductApplicationServiceImpl) convertToCategoryInfoResponse(category *
} }
} }
func (s *ProductApplicationServiceImpl) convertToSubCategoryInfoResponse(subCategory *entities.ProductSubCategory) *responses.SubCategoryInfoResponse {
response := &responses.SubCategoryInfoResponse{
ID: subCategory.ID,
Name: subCategory.Name,
Code: subCategory.Code,
Description: subCategory.Description,
CategoryID: subCategory.CategoryID,
Sort: subCategory.Sort,
IsEnabled: subCategory.IsEnabled,
IsVisible: subCategory.IsVisible,
CreatedAt: subCategory.CreatedAt,
UpdatedAt: subCategory.UpdatedAt,
}
// 添加一级分类信息
if subCategory.Category != nil {
response.Category = s.convertToCategoryInfoResponse(subCategory.Category)
}
return response
}
// GetProductApiConfig 获取产品API配置 // GetProductApiConfig 获取产品API配置
func (s *ProductApplicationServiceImpl) GetProductApiConfig(ctx context.Context, productID string) (*responses.ProductApiConfigResponse, error) { func (s *ProductApplicationServiceImpl) GetProductApiConfig(ctx context.Context, productID string) (*responses.ProductApiConfigResponse, error) {
return s.productApiConfigAppService.GetProductApiConfig(ctx, productID) return s.productApiConfigAppService.GetProductApiConfig(ctx, productID)

View File

@@ -0,0 +1,20 @@
package product
import (
"context"
"tyapi-server/internal/application/product/dto/commands"
"tyapi-server/internal/application/product/dto/queries"
"tyapi-server/internal/application/product/dto/responses"
)
// SubCategoryApplicationService 二级分类应用服务接口
type SubCategoryApplicationService interface {
// 二级分类管理
CreateSubCategory(ctx context.Context, cmd *commands.CreateSubCategoryCommand) error
UpdateSubCategory(ctx context.Context, cmd *commands.UpdateSubCategoryCommand) error
DeleteSubCategory(ctx context.Context, cmd *commands.DeleteSubCategoryCommand) error
GetSubCategoryByID(ctx context.Context, query *queries.GetSubCategoryQuery) (*responses.SubCategoryInfoResponse, error)
ListSubCategories(ctx context.Context, query *queries.ListSubCategoriesQuery) (*responses.SubCategoryListResponse, error)
ListSubCategoriesByCategoryID(ctx context.Context, categoryID string) ([]*responses.SubCategorySimpleResponse, error)
}

View File

@@ -0,0 +1,322 @@
package product
import (
"context"
"errors"
"fmt"
"tyapi-server/internal/application/product/dto/commands"
"tyapi-server/internal/application/product/dto/queries"
"tyapi-server/internal/application/product/dto/responses"
"tyapi-server/internal/domains/product/entities"
"tyapi-server/internal/domains/product/repositories"
"go.uber.org/zap"
)
// SubCategoryApplicationServiceImpl 二级分类应用服务实现
type SubCategoryApplicationServiceImpl struct {
categoryRepo repositories.ProductCategoryRepository
subCategoryRepo repositories.ProductSubCategoryRepository
logger *zap.Logger
}
// NewSubCategoryApplicationService 创建二级分类应用服务
func NewSubCategoryApplicationService(
categoryRepo repositories.ProductCategoryRepository,
subCategoryRepo repositories.ProductSubCategoryRepository,
logger *zap.Logger,
) SubCategoryApplicationService {
return &SubCategoryApplicationServiceImpl{
categoryRepo: categoryRepo,
subCategoryRepo: subCategoryRepo,
logger: logger,
}
}
// CreateSubCategory 创建二级分类
func (s *SubCategoryApplicationServiceImpl) CreateSubCategory(ctx context.Context, cmd *commands.CreateSubCategoryCommand) error {
// 1. 参数验证
if err := s.validateCreateSubCategory(cmd); err != nil {
return err
}
// 2. 验证一级分类是否存在
category, err := s.categoryRepo.GetByID(ctx, cmd.CategoryID)
if err != nil {
return fmt.Errorf("一级分类不存在: %w", err)
}
if !category.IsValid() {
return errors.New("一级分类已禁用或删除")
}
// 3. 验证二级分类编号唯一性
if err := s.validateSubCategoryCode(cmd.Code, "", cmd.CategoryID); err != nil {
return err
}
// 4. 创建二级分类实体
subCategory := &entities.ProductSubCategory{
Name: cmd.Name,
Code: cmd.Code,
Description: cmd.Description,
CategoryID: cmd.CategoryID,
Sort: cmd.Sort,
IsEnabled: cmd.IsEnabled,
IsVisible: cmd.IsVisible,
}
// 5. 保存到仓储
createdSubCategory, err := s.subCategoryRepo.Create(ctx, *subCategory)
if err != nil {
s.logger.Error("创建二级分类失败", zap.Error(err), zap.String("code", cmd.Code))
return fmt.Errorf("创建二级分类失败: %w", err)
}
s.logger.Info("创建二级分类成功", zap.String("id", createdSubCategory.ID), zap.String("code", cmd.Code))
return nil
}
// UpdateSubCategory 更新二级分类
func (s *SubCategoryApplicationServiceImpl) UpdateSubCategory(ctx context.Context, cmd *commands.UpdateSubCategoryCommand) error {
// 1. 参数验证
if err := s.validateUpdateSubCategory(cmd); err != nil {
return err
}
// 2. 获取现有二级分类
existingSubCategory, err := s.subCategoryRepo.GetByID(ctx, cmd.ID)
if err != nil {
return fmt.Errorf("二级分类不存在: %w", err)
}
// 3. 验证一级分类是否存在
category, err := s.categoryRepo.GetByID(ctx, cmd.CategoryID)
if err != nil {
return fmt.Errorf("一级分类不存在: %w", err)
}
if !category.IsValid() {
return errors.New("一级分类已禁用或删除")
}
// 4. 验证二级分类编号唯一性(排除当前分类)
if err := s.validateSubCategoryCode(cmd.Code, cmd.ID, cmd.CategoryID); err != nil {
return err
}
// 5. 更新二级分类信息
existingSubCategory.Name = cmd.Name
existingSubCategory.Code = cmd.Code
existingSubCategory.Description = cmd.Description
existingSubCategory.CategoryID = cmd.CategoryID
existingSubCategory.Sort = cmd.Sort
existingSubCategory.IsEnabled = cmd.IsEnabled
existingSubCategory.IsVisible = cmd.IsVisible
// 6. 保存到仓储
if err := s.subCategoryRepo.Update(ctx, *existingSubCategory); err != nil {
s.logger.Error("更新二级分类失败", zap.Error(err), zap.String("id", cmd.ID))
return fmt.Errorf("更新二级分类失败: %w", err)
}
s.logger.Info("更新二级分类成功", zap.String("id", cmd.ID), zap.String("code", cmd.Code))
return nil
}
// DeleteSubCategory 删除二级分类
func (s *SubCategoryApplicationServiceImpl) DeleteSubCategory(ctx context.Context, cmd *commands.DeleteSubCategoryCommand) error {
// 1. 检查二级分类是否存在
existingSubCategory, err := s.subCategoryRepo.GetByID(ctx, cmd.ID)
if err != nil {
return fmt.Errorf("二级分类不存在: %w", err)
}
// 2. 删除二级分类
if err := s.subCategoryRepo.Delete(ctx, cmd.ID); err != nil {
s.logger.Error("删除二级分类失败", zap.Error(err), zap.String("id", cmd.ID))
return fmt.Errorf("删除二级分类失败: %w", err)
}
s.logger.Info("删除二级分类成功", zap.String("id", cmd.ID), zap.String("code", existingSubCategory.Code))
return nil
}
// GetSubCategoryByID 根据ID获取二级分类
func (s *SubCategoryApplicationServiceImpl) GetSubCategoryByID(ctx context.Context, query *queries.GetSubCategoryQuery) (*responses.SubCategoryInfoResponse, error) {
subCategory, err := s.subCategoryRepo.GetByID(ctx, query.ID)
if err != nil {
return nil, fmt.Errorf("二级分类不存在: %w", err)
}
// 加载一级分类信息
if subCategory.CategoryID != "" {
category, err := s.categoryRepo.GetByID(ctx, subCategory.CategoryID)
if err == nil {
subCategory.Category = &category
}
}
// 转换为响应对象
response := s.convertToSubCategoryInfoResponse(subCategory)
return response, nil
}
// ListSubCategories 获取二级分类列表
func (s *SubCategoryApplicationServiceImpl) ListSubCategories(ctx context.Context, query *queries.ListSubCategoriesQuery) (*responses.SubCategoryListResponse, error) {
// 构建查询条件
categoryID := query.CategoryID
isEnabled := query.IsEnabled
isVisible := query.IsVisible
var subCategories []*entities.ProductSubCategory
var err error
// 根据条件查询
if categoryID != "" {
// 按一级分类查询
subCategories, err = s.subCategoryRepo.FindByCategoryID(ctx, categoryID)
} else {
// 查询所有二级分类
subCategories, err = s.subCategoryRepo.List(ctx)
}
if err != nil {
s.logger.Error("获取二级分类列表失败", zap.Error(err))
return nil, fmt.Errorf("获取二级分类列表失败: %w", err)
}
// 过滤状态
filteredSubCategories := make([]*entities.ProductSubCategory, 0)
for _, subCategory := range subCategories {
if isEnabled != nil && *isEnabled != subCategory.IsEnabled {
continue
}
if isVisible != nil && *isVisible != subCategory.IsVisible {
continue
}
filteredSubCategories = append(filteredSubCategories, subCategory)
}
// 加载一级分类信息
for _, subCategory := range filteredSubCategories {
if subCategory.CategoryID != "" {
category, err := s.categoryRepo.GetByID(ctx, subCategory.CategoryID)
if err == nil {
subCategory.Category = &category
}
}
}
// 转换为响应对象
items := make([]responses.SubCategoryInfoResponse, len(filteredSubCategories))
for i, subCategory := range filteredSubCategories {
items[i] = *s.convertToSubCategoryInfoResponse(subCategory)
}
return &responses.SubCategoryListResponse{
Total: int64(len(items)),
Page: query.Page,
Size: query.PageSize,
Items: items,
}, nil
}
// ListSubCategoriesByCategoryID 根据一级分类ID获取二级分类列表
func (s *SubCategoryApplicationServiceImpl) ListSubCategoriesByCategoryID(ctx context.Context, categoryID string) ([]*responses.SubCategorySimpleResponse, error) {
subCategories, err := s.subCategoryRepo.FindByCategoryID(ctx, categoryID)
if err != nil {
return nil, fmt.Errorf("获取二级分类列表失败: %w", err)
}
// 转换为响应对象
items := make([]*responses.SubCategorySimpleResponse, len(subCategories))
for i, subCategory := range subCategories {
items[i] = &responses.SubCategorySimpleResponse{
ID: subCategory.ID,
Name: subCategory.Name,
Code: subCategory.Code,
CategoryID: subCategory.CategoryID,
}
}
return items, nil
}
// convertToSubCategoryInfoResponse 转换为二级分类信息响应
func (s *SubCategoryApplicationServiceImpl) convertToSubCategoryInfoResponse(subCategory *entities.ProductSubCategory) *responses.SubCategoryInfoResponse {
response := &responses.SubCategoryInfoResponse{
ID: subCategory.ID,
Name: subCategory.Name,
Code: subCategory.Code,
Description: subCategory.Description,
CategoryID: subCategory.CategoryID,
Sort: subCategory.Sort,
IsEnabled: subCategory.IsEnabled,
IsVisible: subCategory.IsVisible,
CreatedAt: subCategory.CreatedAt,
UpdatedAt: subCategory.UpdatedAt,
}
// 添加一级分类信息
if subCategory.Category != nil {
response.Category = &responses.CategoryInfoResponse{
ID: subCategory.Category.ID,
Name: subCategory.Category.Name,
Description: subCategory.Category.Description,
Sort: subCategory.Category.Sort,
IsEnabled: subCategory.Category.IsEnabled,
IsVisible: subCategory.Category.IsVisible,
CreatedAt: subCategory.Category.CreatedAt,
UpdatedAt: subCategory.Category.UpdatedAt,
}
}
return response
}
// validateCreateSubCategory 验证创建二级分类参数
func (s *SubCategoryApplicationServiceImpl) validateCreateSubCategory(cmd *commands.CreateSubCategoryCommand) error {
if cmd.Name == "" {
return errors.New("二级分类名称不能为空")
}
if cmd.Code == "" {
return errors.New("二级分类编号不能为空")
}
if cmd.CategoryID == "" {
return errors.New("一级分类ID不能为空")
}
return nil
}
// validateUpdateSubCategory 验证更新二级分类参数
func (s *SubCategoryApplicationServiceImpl) validateUpdateSubCategory(cmd *commands.UpdateSubCategoryCommand) error {
if cmd.ID == "" {
return errors.New("二级分类ID不能为空")
}
if cmd.Name == "" {
return errors.New("二级分类名称不能为空")
}
if cmd.Code == "" {
return errors.New("二级分类编号不能为空")
}
if cmd.CategoryID == "" {
return errors.New("一级分类ID不能为空")
}
return nil
}
// validateSubCategoryCode 验证二级分类编号唯一性
func (s *SubCategoryApplicationServiceImpl) validateSubCategoryCode(code, excludeID, categoryID string) error {
if code == "" {
return errors.New("二级分类编号不能为空")
}
existingSubCategory, err := s.subCategoryRepo.FindByCode(context.Background(), code)
if err == nil && existingSubCategory != nil && existingSubCategory.ID != excludeID {
// 如果指定了分类ID检查是否在同一分类下
if categoryID == "" || existingSubCategory.CategoryID == categoryID {
return errors.New("二级分类编号已存在")
}
}
return nil
}

View File

@@ -557,6 +557,11 @@ func NewContainer() *Container {
product_repo.NewGormProductCategoryRepository, product_repo.NewGormProductCategoryRepository,
fx.As(new(domain_product_repo.ProductCategoryRepository)), fx.As(new(domain_product_repo.ProductCategoryRepository)),
), ),
// 产品二级分类仓储 - 同时注册具体类型和接口类型
fx.Annotate(
product_repo.NewGormProductSubCategoryRepository,
fx.As(new(domain_product_repo.ProductSubCategoryRepository)),
),
// 订阅仓储 - 同时注册具体类型和接口类型 // 订阅仓储 - 同时注册具体类型和接口类型
fx.Annotate( fx.Annotate(
product_repo.NewGormSubscriptionRepository, product_repo.NewGormSubscriptionRepository,
@@ -1002,6 +1007,11 @@ func NewContainer() *Container {
product.NewCategoryApplicationService, product.NewCategoryApplicationService,
fx.As(new(product.CategoryApplicationService)), fx.As(new(product.CategoryApplicationService)),
), ),
// 二级分类应用服务 - 绑定到接口
fx.Annotate(
product.NewSubCategoryApplicationService,
fx.As(new(product.SubCategoryApplicationService)),
),
fx.Annotate( fx.Annotate(
product.NewDocumentationApplicationService, product.NewDocumentationApplicationService,
fx.As(new(product.DocumentationApplicationServiceInterface)), fx.As(new(product.DocumentationApplicationServiceInterface)),
@@ -1198,6 +1208,8 @@ func NewContainer() *Container {
handlers.NewProductHandler, handlers.NewProductHandler,
// 产品管理员HTTP处理器 // 产品管理员HTTP处理器
handlers.NewProductAdminHandler, handlers.NewProductAdminHandler,
// 二级分类HTTP处理器
handlers.NewSubCategoryHandler,
// API Handler // API Handler
handlers.NewApiHandler, handlers.NewApiHandler,
// 统计HTTP处理器 // 统计HTTP处理器
@@ -1266,6 +1278,8 @@ func NewContainer() *Container {
routes.NewProductRoutes, routes.NewProductRoutes,
// 产品管理员路由 // 产品管理员路由
routes.NewProductAdminRoutes, routes.NewProductAdminRoutes,
// 二级分类路由
routes.NewSubCategoryRoutes,
// 组件报告订单路由 // 组件报告订单路由
routes.NewComponentReportOrderRoutes, routes.NewComponentReportOrderRoutes,
// UI组件路由 // UI组件路由
@@ -1384,6 +1398,7 @@ func RegisterRoutes(
financeRoutes *routes.FinanceRoutes, financeRoutes *routes.FinanceRoutes,
productRoutes *routes.ProductRoutes, productRoutes *routes.ProductRoutes,
productAdminRoutes *routes.ProductAdminRoutes, productAdminRoutes *routes.ProductAdminRoutes,
subCategoryRoutes *routes.SubCategoryRoutes,
componentReportOrderRoutes *routes.ComponentReportOrderRoutes, componentReportOrderRoutes *routes.ComponentReportOrderRoutes,
uiComponentRoutes *routes.UIComponentRoutes, uiComponentRoutes *routes.UIComponentRoutes,
articleRoutes *routes.ArticleRoutes, articleRoutes *routes.ArticleRoutes,
@@ -1406,6 +1421,7 @@ func RegisterRoutes(
financeRoutes.Register(router) financeRoutes.Register(router)
productRoutes.Register(router) productRoutes.Register(router)
productAdminRoutes.Register(router) productAdminRoutes.Register(router)
subCategoryRoutes.Register(router)
componentReportOrderRoutes.Register(router) componentReportOrderRoutes.Register(router)
uiComponentRoutes.Register(router) uiComponentRoutes.Register(router)

View File

@@ -10,19 +10,20 @@ import (
// Product 产品实体 // Product 产品实体
type Product struct { type Product struct {
ID string `gorm:"primaryKey;type:varchar(36)" comment:"产品ID"` ID string `gorm:"primaryKey;type:varchar(36)" comment:"产品ID"`
OldID *string `gorm:"type:varchar(36);index" comment:"旧产品ID用于兼容"` OldID *string `gorm:"type:varchar(36);index" comment:"旧产品ID用于兼容"`
Name string `gorm:"type:varchar(100);not null" comment:"产品名称"` Name string `gorm:"type:varchar(100);not null" comment:"产品名称"`
Code string `gorm:"type:varchar(50);uniqueIndex;not null" comment:"产品编号"` Code string `gorm:"type:varchar(50);uniqueIndex;not null" comment:"产品编号"`
Description string `gorm:"type:text" comment:"产品简介"` Description string `gorm:"type:text" comment:"产品简介"`
Content string `gorm:"type:text" comment:"产品内容"` Content string `gorm:"type:text" comment:"产品内容"`
CategoryID string `gorm:"type:varchar(36);not null" comment:"产品分类ID"` CategoryID string `gorm:"type:varchar(36);not null" comment:"一级分类ID"`
Price decimal.Decimal `gorm:"type:decimal(10,2);not null;default:0" comment:"产品价格"` SubCategoryID *string `gorm:"type:varchar(36);index" comment:"二级分类ID"`
CostPrice decimal.Decimal `gorm:"type:decimal(10,2);default:0" comment:"成本价"` Price decimal.Decimal `gorm:"type:decimal(10,2);not null;default:0" comment:"产品价格"`
Remark string `gorm:"type:text" comment:"备注"` CostPrice decimal.Decimal `gorm:"type:decimal(10,2);default:0" comment:"成本价"`
IsEnabled bool `gorm:"default:false" comment:"是否启用"` Remark string `gorm:"type:text" comment:"备注"`
IsVisible bool `gorm:"default:false" comment:"是否展示"` IsEnabled bool `gorm:"default:false" comment:"是否启用"`
IsPackage bool `gorm:"default:false" comment:"是否组合包"` IsVisible bool `gorm:"default:false" comment:"是否展示"`
IsPackage bool `gorm:"default:false" comment:"是否组合包"`
// 组合包相关关联 // 组合包相关关联
PackageItems []*ProductPackageItem `gorm:"foreignKey:PackageID" comment:"组合包项目列表"` PackageItems []*ProductPackageItem `gorm:"foreignKey:PackageID" comment:"组合包项目列表"`
// UI组件相关字段 // UI组件相关字段
@@ -34,7 +35,8 @@ type Product struct {
SEOKeywords string `gorm:"type:text" comment:"SEO关键词"` SEOKeywords string `gorm:"type:text" comment:"SEO关键词"`
// 关联关系 // 关联关系
Category *ProductCategory `gorm:"foreignKey:CategoryID" comment:"产品分类"` Category *ProductCategory `gorm:"foreignKey:CategoryID" comment:"一级分类"`
SubCategory *ProductSubCategory `gorm:"foreignKey:SubCategoryID" comment:"二级分类"`
Documentation *ProductDocumentation `gorm:"foreignKey:ProductID" comment:"产品文档"` Documentation *ProductDocumentation `gorm:"foreignKey:ProductID" comment:"产品文档"`
CreatedAt time.Time `gorm:"autoCreateTime" comment:"创建时间"` CreatedAt time.Time `gorm:"autoCreateTime" comment:"创建时间"`
@@ -118,3 +120,34 @@ func (p *Product) GetOldID() string {
func (p *Product) HasOldID() bool { func (p *Product) HasOldID() bool {
return p.OldID != nil && *p.OldID != "" return p.OldID != nil && *p.OldID != ""
} }
// HasSubCategory 检查是否有二级分类
func (p *Product) HasSubCategory() bool {
return p.SubCategoryID != nil && *p.SubCategoryID != ""
}
// GetFullCategoryPath 获取完整分类路径(一级分类/二级分类)
func (p *Product) GetFullCategoryPath() string {
if p.Category == nil {
return ""
}
if p.SubCategory != nil {
return p.Category.Name + " / " + p.SubCategory.Name
}
return p.Category.Name
}
// GetFullCategoryCode 获取完整分类编号(一级分类编号.二级分类编号)
func (p *Product) GetFullCategoryCode() string {
if p.Category == nil {
return ""
}
if p.SubCategory != nil {
return p.Category.Code + "." + p.SubCategory.Code
}
return p.Category.Code
}

View File

@@ -18,7 +18,8 @@ type ProductCategory struct {
IsVisible bool `gorm:"default:true" comment:"是否展示"` IsVisible bool `gorm:"default:true" comment:"是否展示"`
// 关联关系 // 关联关系
Products []Product `gorm:"foreignKey:CategoryID" comment:"产品列表"` Products []Product `gorm:"foreignKey:CategoryID" comment:"产品列表"`
SubCategories []ProductSubCategory `gorm:"foreignKey:CategoryID" comment:"二级分类列表"`
CreatedAt time.Time `gorm:"autoCreateTime" comment:"创建时间"` CreatedAt time.Time `gorm:"autoCreateTime" comment:"创建时间"`
UpdatedAt time.Time `gorm:"autoUpdateTime" comment:"更新时间"` UpdatedAt time.Time `gorm:"autoUpdateTime" comment:"更新时间"`

View File

@@ -0,0 +1,82 @@
package entities
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
// ProductSubCategory 产品二级分类实体
type ProductSubCategory struct {
ID string `gorm:"primaryKey;type:varchar(36)" comment:"二级分类ID"`
Name string `gorm:"type:varchar(100);not null" comment:"二级分类名称"`
Code string `gorm:"type:varchar(50);uniqueIndex;not null" comment:"二级分类编号"`
Description string `gorm:"type:text" comment:"二级分类描述"`
CategoryID string `gorm:"type:varchar(36);not null;index" comment:"一级分类ID"`
Sort int `gorm:"default:0" comment:"排序"`
IsEnabled bool `gorm:"default:true" comment:"是否启用"`
IsVisible bool `gorm:"default:true" comment:"是否展示"`
// 关联关系
Category *ProductCategory `gorm:"foreignKey:CategoryID" comment:"一级分类"`
Products []Product `gorm:"foreignKey:SubCategoryID" comment:"产品列表"`
CreatedAt time.Time `gorm:"autoCreateTime" comment:"创建时间"`
UpdatedAt time.Time `gorm:"autoUpdateTime" comment:"更新时间"`
DeletedAt gorm.DeletedAt `gorm:"index" comment:"软删除时间"`
}
// BeforeCreate GORM钩子创建前自动生成UUID
func (psc *ProductSubCategory) BeforeCreate(tx *gorm.DB) error {
if psc.ID == "" {
psc.ID = uuid.New().String()
}
return nil
}
// IsValid 检查二级分类是否有效
func (psc *ProductSubCategory) IsValid() bool {
return psc.DeletedAt.Time.IsZero() && psc.IsEnabled
}
// IsVisibleToUser 检查二级分类是否对用户可见
func (psc *ProductSubCategory) IsVisibleToUser() bool {
return psc.IsValid() && psc.IsVisible
}
// Enable 启用二级分类
func (psc *ProductSubCategory) Enable() {
psc.IsEnabled = true
}
// Disable 禁用二级分类
func (psc *ProductSubCategory) Disable() {
psc.IsEnabled = false
}
// Show 显示二级分类
func (psc *ProductSubCategory) Show() {
psc.IsVisible = true
}
// Hide 隐藏二级分类
func (psc *ProductSubCategory) Hide() {
psc.IsVisible = false
}
// GetFullPath 获取完整路径(一级分类/二级分类)
func (psc *ProductSubCategory) GetFullPath() string {
if psc.Category != nil {
return psc.Category.Name + " / " + psc.Name
}
return psc.Name
}
// GetFullCode 获取完整编号(一级分类编号.二级分类编号)
func (psc *ProductSubCategory) GetFullCode() string {
if psc.Category != nil {
return psc.Category.Code + "." + psc.Code
}
return psc.Code
}

View File

@@ -0,0 +1,22 @@
package repositories
import (
"context"
"tyapi-server/internal/domains/product/entities"
)
// ProductSubCategoryRepository 产品二级分类仓储接口
type ProductSubCategoryRepository interface {
// 基础CRUD方法
GetByID(ctx context.Context, id string) (*entities.ProductSubCategory, error)
Create(ctx context.Context, category entities.ProductSubCategory) (*entities.ProductSubCategory, error)
Update(ctx context.Context, category entities.ProductSubCategory) error
Delete(ctx context.Context, id string) error
List(ctx context.Context) ([]*entities.ProductSubCategory, error)
// 查询方法
FindByCode(ctx context.Context, code string) (*entities.ProductSubCategory, error)
FindByCategoryID(ctx context.Context, categoryID string) ([]*entities.ProductSubCategory, error)
FindVisible(ctx context.Context) ([]*entities.ProductSubCategory, error)
FindEnabled(ctx context.Context) ([]*entities.ProductSubCategory, error)
}

View File

@@ -18,21 +18,24 @@ import (
// ProductManagementService 产品管理领域服务 // ProductManagementService 产品管理领域服务
// 负责产品的基本管理操作,包括创建、查询、更新等 // 负责产品的基本管理操作,包括创建、查询、更新等
type ProductManagementService struct { type ProductManagementService struct {
productRepo repositories.ProductRepository productRepo repositories.ProductRepository
categoryRepo repositories.ProductCategoryRepository categoryRepo repositories.ProductCategoryRepository
logger *zap.Logger subCategoryRepo repositories.ProductSubCategoryRepository
logger *zap.Logger
} }
// NewProductManagementService 创建产品管理领域服务 // NewProductManagementService 创建产品管理领域服务
func NewProductManagementService( func NewProductManagementService(
productRepo repositories.ProductRepository, productRepo repositories.ProductRepository,
categoryRepo repositories.ProductCategoryRepository, categoryRepo repositories.ProductCategoryRepository,
subCategoryRepo repositories.ProductSubCategoryRepository,
logger *zap.Logger, logger *zap.Logger,
) *ProductManagementService { ) *ProductManagementService {
return &ProductManagementService{ return &ProductManagementService{
productRepo: productRepo, productRepo: productRepo,
categoryRepo: categoryRepo, categoryRepo: categoryRepo,
logger: logger, subCategoryRepo: subCategoryRepo,
logger: logger,
} }
} }
@@ -306,6 +309,21 @@ func (s *ProductManagementService) ValidateProduct(product *entities.Product) er
} }
} }
// 验证二级分类是否存在(如果设置了二级分类)
if product.SubCategoryID != nil && *product.SubCategoryID != "" {
subCategory, err := s.subCategoryRepo.GetByID(context.Background(), *product.SubCategoryID)
if err != nil {
return fmt.Errorf("产品二级分类不存在: %w", err)
}
if !subCategory.IsValid() {
return errors.New("产品二级分类已禁用或删除")
}
// 验证二级分类是否属于指定的一级分类
if subCategory.CategoryID != product.CategoryID {
return errors.New("二级分类不属于指定的一级分类")
}
}
return nil return nil
} }

View File

@@ -116,7 +116,7 @@ func (r *GormComponentReportRepository) GetUserDownloadedProductCodes(ctx contex
func (r *GormComponentReportRepository) GetDownloadByPaymentOrderID(ctx context.Context, orderID string) (*entities.ComponentReportDownload, error) { func (r *GormComponentReportRepository) GetDownloadByPaymentOrderID(ctx context.Context, orderID string) (*entities.ComponentReportDownload, error) {
var download entities.ComponentReportDownload var download entities.ComponentReportDownload
err := r.GetDB(ctx).Where("order_number = ?", orderID).First(&download).Error err := r.GetDB(ctx).Where("order_id = ?", orderID).First(&download).Error
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, gorm.ErrRecordNotFound return nil, gorm.ErrRecordNotFound

View File

@@ -0,0 +1,137 @@
package repositories
import (
"context"
"errors"
"tyapi-server/internal/domains/product/entities"
"tyapi-server/internal/domains/product/repositories"
"tyapi-server/internal/shared/database"
"go.uber.org/zap"
"gorm.io/gorm"
)
const (
ProductSubCategoriesTable = "product_sub_categories"
)
type GormProductSubCategoryRepository struct {
*database.CachedBaseRepositoryImpl
}
var _ repositories.ProductSubCategoryRepository = (*GormProductSubCategoryRepository)(nil)
func NewGormProductSubCategoryRepository(db *gorm.DB, logger *zap.Logger) repositories.ProductSubCategoryRepository {
return &GormProductSubCategoryRepository{
CachedBaseRepositoryImpl: database.NewCachedBaseRepositoryImpl(db, logger, ProductSubCategoriesTable),
}
}
// Create 创建二级分类
func (r *GormProductSubCategoryRepository) Create(ctx context.Context, category entities.ProductSubCategory) (*entities.ProductSubCategory, error) {
err := r.CreateEntity(ctx, &category)
if err != nil {
return nil, err
}
return &category, nil
}
// GetByID 根据ID获取二级分类
func (r *GormProductSubCategoryRepository) GetByID(ctx context.Context, id string) (*entities.ProductSubCategory, error) {
var entity entities.ProductSubCategory
err := r.SmartGetByID(ctx, id, &entity)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, gorm.ErrRecordNotFound
}
return nil, err
}
return &entity, nil
}
// Update 更新二级分类
func (r *GormProductSubCategoryRepository) Update(ctx context.Context, category entities.ProductSubCategory) error {
return r.UpdateEntity(ctx, &category)
}
// Delete 删除二级分类
func (r *GormProductSubCategoryRepository) Delete(ctx context.Context, id string) error {
return r.DeleteEntity(ctx, id, &entities.ProductSubCategory{})
}
// List 获取所有二级分类
func (r *GormProductSubCategoryRepository) List(ctx context.Context) ([]*entities.ProductSubCategory, error) {
var categories []entities.ProductSubCategory
err := r.GetDB(ctx).Order("sort ASC, created_at DESC").Find(&categories).Error
if err != nil {
return nil, err
}
// 转换为指针切片
result := make([]*entities.ProductSubCategory, len(categories))
for i := range categories {
result[i] = &categories[i]
}
return result, nil
}
// FindByCode 根据编号查找二级分类
func (r *GormProductSubCategoryRepository) FindByCode(ctx context.Context, code string) (*entities.ProductSubCategory, error) {
var entity entities.ProductSubCategory
err := r.GetDB(ctx).Where("code = ?", code).First(&entity).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, gorm.ErrRecordNotFound
}
return nil, err
}
return &entity, nil
}
// FindByCategoryID 根据一级分类ID查找二级分类
func (r *GormProductSubCategoryRepository) FindByCategoryID(ctx context.Context, categoryID string) ([]*entities.ProductSubCategory, error) {
var categories []entities.ProductSubCategory
err := r.GetDB(ctx).Where("category_id = ?", categoryID).Order("sort ASC, created_at DESC").Find(&categories).Error
if err != nil {
return nil, err
}
// 转换为指针切片
result := make([]*entities.ProductSubCategory, len(categories))
for i := range categories {
result[i] = &categories[i]
}
return result, nil
}
// FindVisible 查找可见的二级分类
func (r *GormProductSubCategoryRepository) FindVisible(ctx context.Context) ([]*entities.ProductSubCategory, error) {
var categories []entities.ProductSubCategory
err := r.GetDB(ctx).Where("is_visible = ? AND is_enabled = ?", true, true).Order("sort ASC, created_at DESC").Find(&categories).Error
if err != nil {
return nil, err
}
// 转换为指针切片
result := make([]*entities.ProductSubCategory, len(categories))
for i := range categories {
result[i] = &categories[i]
}
return result, nil
}
// FindEnabled 查找启用的二级分类
func (r *GormProductSubCategoryRepository) FindEnabled(ctx context.Context) ([]*entities.ProductSubCategory, error) {
var categories []entities.ProductSubCategory
err := r.GetDB(ctx).Where("is_enabled = ?", true).Order("sort ASC, created_at DESC").Find(&categories).Error
if err != nil {
return nil, err
}
// 转换为指针切片
result := make([]*entities.ProductSubCategory, len(categories))
for i := range categories {
result[i] = &categories[i]
}
return result, nil
}

View File

@@ -0,0 +1,229 @@
package handlers
import (
"tyapi-server/internal/application/product"
"tyapi-server/internal/application/product/dto/commands"
"tyapi-server/internal/application/product/dto/queries"
"tyapi-server/internal/shared/interfaces"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// SubCategoryHandler 二级分类HTTP处理器
type SubCategoryHandler struct {
subCategoryAppService product.SubCategoryApplicationService
responseBuilder interfaces.ResponseBuilder
validator interfaces.RequestValidator
logger *zap.Logger
}
// NewSubCategoryHandler 创建二级分类HTTP处理器
func NewSubCategoryHandler(
subCategoryAppService product.SubCategoryApplicationService,
responseBuilder interfaces.ResponseBuilder,
validator interfaces.RequestValidator,
logger *zap.Logger,
) *SubCategoryHandler {
return &SubCategoryHandler{
subCategoryAppService: subCategoryAppService,
responseBuilder: responseBuilder,
validator: validator,
logger: logger,
}
}
// CreateSubCategory 创建二级分类
// @Summary 创建二级分类
// @Description 管理员创建新二级分类
// @Tags 二级分类管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body commands.CreateSubCategoryCommand true "创建二级分类请求"
// @Success 201 {object} map[string]interface{} "二级分类创建成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/admin/sub-categories [post]
func (h *SubCategoryHandler) CreateSubCategory(c *gin.Context) {
var cmd commands.CreateSubCategoryCommand
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
return
}
if err := h.subCategoryAppService.CreateSubCategory(c.Request.Context(), &cmd); err != nil {
h.logger.Error("创建二级分类失败", zap.Error(err))
h.responseBuilder.BadRequest(c, err.Error())
return
}
h.responseBuilder.Created(c, nil, "二级分类创建成功")
}
// UpdateSubCategory 更新二级分类
// @Summary 更新二级分类
// @Description 管理员更新二级分类信息
// @Tags 二级分类管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path string true "二级分类ID"
// @Param request body commands.UpdateSubCategoryCommand true "更新二级分类请求"
// @Success 200 {object} map[string]interface{} "二级分类更新成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 404 {object} map[string]interface{} "二级分类不存在"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/admin/sub-categories/{id} [put]
func (h *SubCategoryHandler) UpdateSubCategory(c *gin.Context) {
var cmd commands.UpdateSubCategoryCommand
cmd.ID = c.Param("id")
if cmd.ID == "" {
h.responseBuilder.BadRequest(c, "二级分类ID不能为空")
return
}
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
return
}
if err := h.subCategoryAppService.UpdateSubCategory(c.Request.Context(), &cmd); err != nil {
h.logger.Error("更新二级分类失败", zap.Error(err))
h.responseBuilder.BadRequest(c, err.Error())
return
}
h.responseBuilder.Success(c, nil, "二级分类更新成功")
}
// DeleteSubCategory 删除二级分类
// @Summary 删除二级分类
// @Description 管理员删除二级分类
// @Tags 二级分类管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path string true "二级分类ID"
// @Success 200 {object} map[string]interface{} "二级分类删除成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 404 {object} map[string]interface{} "二级分类不存在"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/admin/sub-categories/{id} [delete]
func (h *SubCategoryHandler) DeleteSubCategory(c *gin.Context) {
cmd := commands.DeleteSubCategoryCommand{ID: c.Param("id")}
if err := h.validator.ValidateParam(c, &cmd); err != nil {
return
}
if err := h.subCategoryAppService.DeleteSubCategory(c.Request.Context(), &cmd); err != nil {
h.logger.Error("删除二级分类失败", zap.Error(err))
h.responseBuilder.BadRequest(c, err.Error())
return
}
h.responseBuilder.Success(c, nil, "二级分类删除成功")
}
// GetSubCategory 获取二级分类详情
// @Summary 获取二级分类详情
// @Description 获取指定二级分类的详细信息
// @Tags 二级分类管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path string true "二级分类ID"
// @Success 200 {object} responses.SubCategoryInfoResponse "二级分类信息"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 404 {object} map[string]interface{} "二级分类不存在"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/admin/sub-categories/{id} [get]
func (h *SubCategoryHandler) GetSubCategory(c *gin.Context) {
query := &queries.GetSubCategoryQuery{ID: c.Param("id")}
if err := h.validator.ValidateParam(c, query); err != nil {
return
}
result, err := h.subCategoryAppService.GetSubCategoryByID(c.Request.Context(), query)
if err != nil {
h.logger.Error("获取二级分类失败", zap.Error(err))
h.responseBuilder.NotFound(c, err.Error())
return
}
h.responseBuilder.Success(c, result, "获取二级分类成功")
}
// ListSubCategories 获取二级分类列表
// @Summary 获取二级分类列表
// @Description 获取二级分类列表,支持筛选和分页
// @Tags 二级分类管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param category_id query string false "一级分类ID"
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @Param is_enabled query bool false "是否启用"
// @Param is_visible query bool false "是否展示"
// @Param sort_by query string false "排序字段"
// @Param sort_order query string false "排序方向"
// @Success 200 {object} responses.SubCategoryListResponse "二级分类列表"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/admin/sub-categories [get]
func (h *SubCategoryHandler) ListSubCategories(c *gin.Context) {
query := &queries.ListSubCategoriesQuery{}
if err := h.validator.ValidateQuery(c, query); err != nil {
return
}
// 设置默认分页参数
if query.Page == 0 {
query.Page = 1
}
if query.PageSize == 0 {
query.PageSize = 20
}
result, err := h.subCategoryAppService.ListSubCategories(c.Request.Context(), query)
if err != nil {
h.logger.Error("获取二级分类列表失败", zap.Error(err))
h.responseBuilder.BadRequest(c, err.Error())
return
}
h.responseBuilder.Success(c, result, "获取二级分类列表成功")
}
// ListSubCategoriesByCategory 根据一级分类ID获取二级分类列表级联选择用
// @Summary 根据一级分类获取二级分类列表
// @Description 根据一级分类ID获取二级分类简单列表用于级联选择
// @Tags 二级分类管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path string true "一级分类ID"
// @Success 200 {object} []responses.SubCategorySimpleResponse "二级分类简单列表"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/admin/categories/{id}/sub-categories [get]
func (h *SubCategoryHandler) ListSubCategoriesByCategory(c *gin.Context) {
categoryID := c.Param("id")
if categoryID == "" {
h.responseBuilder.BadRequest(c, "一级分类ID不能为空")
return
}
result, err := h.subCategoryAppService.ListSubCategoriesByCategoryID(c.Request.Context(), categoryID)
if err != nil {
h.logger.Error("获取二级分类列表失败", zap.Error(err))
h.responseBuilder.BadRequest(c, err.Error())
return
}
h.responseBuilder.Success(c, result, "获取二级分类列表成功")
}

View File

@@ -0,0 +1,52 @@
package routes
import (
"tyapi-server/internal/infrastructure/http/handlers"
sharedhttp "tyapi-server/internal/shared/http"
"tyapi-server/internal/shared/middleware"
)
// SubCategoryRoutes 二级分类路由
type SubCategoryRoutes struct {
handler *handlers.SubCategoryHandler
auth *middleware.JWTAuthMiddleware
admin *middleware.AdminAuthMiddleware
}
// NewSubCategoryRoutes 创建二级分类路由
func NewSubCategoryRoutes(
handler *handlers.SubCategoryHandler,
auth *middleware.JWTAuthMiddleware,
admin *middleware.AdminAuthMiddleware,
) *SubCategoryRoutes {
return &SubCategoryRoutes{
handler: handler,
auth: auth,
admin: admin,
}
}
// Register 注册路由
func (r *SubCategoryRoutes) Register(router *sharedhttp.GinRouter) {
engine := router.GetEngine()
adminGroup := engine.Group("/api/v1/admin")
adminGroup.Use(r.auth.Handle())
adminGroup.Use(r.admin.Handle())
{
// 二级分类管理
subCategories := adminGroup.Group("/sub-categories")
{
subCategories.POST("", r.handler.CreateSubCategory) // 创建二级分类
subCategories.PUT("/:id", r.handler.UpdateSubCategory) // 更新二级分类
subCategories.DELETE("/:id", r.handler.DeleteSubCategory) // 删除二级分类
subCategories.GET("/:id", r.handler.GetSubCategory) // 获取二级分类详情
subCategories.GET("", r.handler.ListSubCategories) // 获取二级分类列表
}
// 一级分类下的二级分类路由(级联选择)
categoryAdmin := adminGroup.Group("/product-categories")
{
categoryAdmin.GET("/:id/sub-categories", r.handler.ListSubCategoriesByCategory) // 根据一级分类获取二级分类列表
}
}
}