diff --git a/internal/application/product/dto/commands/product_commands.go b/internal/application/product/dto/commands/product_commands.go index 306ac25..a6606a0 100644 --- a/internal/application/product/dto/commands/product_commands.go +++ b/internal/application/product/dto/commands/product_commands.go @@ -2,17 +2,18 @@ package commands // CreateProductCommand 创建产品命令 type CreateProductCommand struct { - Name string `json:"name" binding:"required,min=2,max=100" comment:"产品名称"` - Code string `json:"code" binding:"required,product_code" comment:"产品编号"` - Description string `json:"description" binding:"omitempty,max=500" comment:"产品描述"` - Content string `json:"content" binding:"omitempty,max=5000" comment:"产品内容"` - CategoryID string `json:"category_id" binding:"required,uuid" comment:"产品分类ID"` - Price float64 `json:"price" binding:"price,min=0" comment:"产品价格"` - CostPrice float64 `json:"cost_price" binding:"omitempty,min=0" comment:"成本价"` - Remark string `json:"remark" binding:"omitempty,max=1000" comment:"备注"` - IsEnabled bool `json:"is_enabled" comment:"是否启用"` - IsVisible bool `json:"is_visible" comment:"是否展示"` - IsPackage bool `json:"is_package" comment:"是否组合包"` + Name string `json:"name" binding:"required,min=2,max=100" comment:"产品名称"` + Code string `json:"code" binding:"required,product_code" comment:"产品编号"` + Description string `json:"description" binding:"omitempty,max=500" comment:"产品描述"` + Content string `json:"content" binding:"omitempty,max=5000" comment:"产品内容"` + CategoryID string `json:"category_id" binding:"required,uuid" comment:"一级分类ID"` + SubCategoryID *string `json:"sub_category_id" binding:"omitempty,uuid" comment:"二级分类ID"` + Price float64 `json:"price" binding:"price,min=0" comment:"产品价格"` + CostPrice float64 `json:"cost_price" binding:"omitempty,min=0" comment:"成本价"` + Remark string `json:"remark" binding:"omitempty,max=1000" comment:"备注"` + IsEnabled bool `json:"is_enabled" comment:"是否启用"` + IsVisible bool `json:"is_visible" comment:"是否展示"` + IsPackage bool `json:"is_package" comment:"是否组合包"` // UI组件相关字段 SellUIComponent bool `json:"sell_ui_component" comment:"是否出售UI组件"` @@ -26,18 +27,19 @@ type CreateProductCommand struct { // UpdateProductCommand 更新产品命令 type UpdateProductCommand 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,product_code" comment:"产品编号"` - Description string `json:"description" binding:"omitempty,max=500" comment:"产品描述"` - Content string `json:"content" binding:"omitempty,max=5000" comment:"产品内容"` - CategoryID string `json:"category_id" binding:"required,uuid" comment:"产品分类ID"` - Price float64 `json:"price" binding:"price,min=0" comment:"产品价格"` - CostPrice float64 `json:"cost_price" binding:"omitempty,min=0" comment:"成本价"` - Remark string `json:"remark" binding:"omitempty,max=1000" comment:"备注"` - IsEnabled bool `json:"is_enabled" comment:"是否启用"` - IsVisible bool `json:"is_visible" comment:"是否展示"` - IsPackage bool `json:"is_package" comment:"是否组合包"` + 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,product_code" comment:"产品编号"` + Description string `json:"description" binding:"omitempty,max=500" comment:"产品描述"` + Content string `json:"content" binding:"omitempty,max=5000" comment:"产品内容"` + CategoryID string `json:"category_id" binding:"required,uuid" comment:"一级分类ID"` + SubCategoryID *string `json:"sub_category_id" binding:"omitempty,uuid" comment:"二级分类ID"` + Price float64 `json:"price" binding:"price,min=0" comment:"产品价格"` + CostPrice float64 `json:"cost_price" binding:"omitempty,min=0" comment:"成本价"` + Remark string `json:"remark" binding:"omitempty,max=1000" comment:"备注"` + IsEnabled bool `json:"is_enabled" comment:"是否启用"` + IsVisible bool `json:"is_visible" comment:"是否展示"` + IsPackage bool `json:"is_package" comment:"是否组合包"` // UI组件相关字段 SellUIComponent bool `json:"sell_ui_component" comment:"是否出售UI组件"` diff --git a/internal/application/product/dto/commands/sub_category_commands.go b/internal/application/product/dto/commands/sub_category_commands.go new file mode 100644 index 0000000..4350df7 --- /dev/null +++ b/internal/application/product/dto/commands/sub_category_commands.go @@ -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"` +} diff --git a/internal/application/product/dto/queries/sub_category_queries.go b/internal/application/product/dto/queries/sub_category_queries.go new file mode 100644 index 0000000..208cb8c --- /dev/null +++ b/internal/application/product/dto/queries/sub_category_queries.go @@ -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:"排序方向"` +} diff --git a/internal/application/product/dto/responses/category_responses.go b/internal/application/product/dto/responses/category_responses.go index d6c8a46..ea1599e 100644 --- a/internal/application/product/dto/responses/category_responses.go +++ b/internal/application/product/dto/responses/category_responses.go @@ -4,23 +4,23 @@ import "time" // CategoryInfoResponse 分类详情响应 type CategoryInfoResponse struct { - ID string `json:"id" comment:"分类ID"` - Name string `json:"name" comment:"分类名称"` - Code string `json:"code" comment:"分类编号"` - Description string `json:"description" comment:"分类描述"` - Sort int `json:"sort" comment:"排序"` - IsEnabled bool `json:"is_enabled" comment:"是否启用"` - IsVisible bool `json:"is_visible" comment:"是否展示"` - + ID string `json:"id" comment:"分类ID"` + Name string `json:"name" comment:"分类名称"` + Code string `json:"code" comment:"分类编号"` + Description string `json:"description" comment:"分类描述"` + Sort int `json:"sort" comment:"排序"` + IsEnabled bool `json:"is_enabled" comment:"是否启用"` + IsVisible bool `json:"is_visible" comment:"是否展示"` + CreatedAt time.Time `json:"created_at" comment:"创建时间"` UpdatedAt time.Time `json:"updated_at" comment:"更新时间"` } // CategoryListResponse 分类列表响应 type CategoryListResponse struct { - Total int64 `json:"total" comment:"总数"` - Page int `json:"page" comment:"页码"` - Size int `json:"size" comment:"每页数量"` + Total int64 `json:"total" comment:"总数"` + Page int `json:"page" comment:"页码"` + Size int `json:"size" comment:"每页数量"` Items []CategoryInfoResponse `json:"items" comment:"分类列表"` } @@ -29,4 +29,38 @@ type CategorySimpleResponse struct { ID string `json:"id" comment:"分类ID"` Name string `json:"name" comment:"分类名称"` Code string `json:"code" comment:"分类编号"` -} \ No newline at end of file +} + +// 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"` +} diff --git a/internal/application/product/dto/responses/product_responses.go b/internal/application/product/dto/responses/product_responses.go index 2dc695d..2e06137 100644 --- a/internal/application/product/dto/responses/product_responses.go +++ b/internal/application/product/dto/responses/product_responses.go @@ -15,17 +15,18 @@ type PackageItemResponse struct { // ProductInfoResponse 产品详情响应 type ProductInfoResponse struct { - ID string `json:"id" comment:"产品ID"` - OldID *string `json:"old_id,omitempty" comment:"旧产品ID"` - Name string `json:"name" comment:"产品名称"` - Code string `json:"code" comment:"产品编号"` - Description string `json:"description" comment:"产品简介"` - Content string `json:"content" comment:"产品内容"` - CategoryID string `json:"category_id" comment:"产品分类ID"` - Price float64 `json:"price" comment:"产品价格"` - IsEnabled bool `json:"is_enabled" comment:"是否启用"` - IsPackage bool `json:"is_package" comment:"是否组合包"` - IsSubscribed *bool `json:"is_subscribed,omitempty" comment:"当前用户是否已订阅"` + ID string `json:"id" comment:"产品ID"` + OldID *string `json:"old_id,omitempty" comment:"旧产品ID"` + Name string `json:"name" comment:"产品名称"` + Code string `json:"code" comment:"产品编号"` + Description string `json:"description" comment:"产品简介"` + Content string `json:"content" comment:"产品内容"` + CategoryID string `json:"category_id" comment:"一级分类ID"` + SubCategoryID *string `json:"sub_category_id,omitempty" comment:"二级分类ID"` + Price float64 `json:"price" comment:"产品价格"` + IsEnabled bool `json:"is_enabled" comment:"是否启用"` + IsPackage bool `json:"is_package" comment:"是否组合包"` + IsSubscribed *bool `json:"is_subscribed,omitempty" comment:"当前用户是否已订阅"` // UI组件相关字段 SellUIComponent bool `json:"sell_ui_component" comment:"是否出售UI组件"` UIComponentPrice float64 `json:"ui_component_price" comment:"UI组件销售价格(组合包使用)"` @@ -36,7 +37,8 @@ type ProductInfoResponse struct { 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:"组合包项目列表"` @@ -91,19 +93,20 @@ type ProductStatsResponse struct { // ProductAdminInfoResponse 管理员产品详情响应 type ProductAdminInfoResponse struct { - ID string `json:"id" comment:"产品ID"` - OldID *string `json:"old_id,omitempty" comment:"旧产品ID"` - Name string `json:"name" comment:"产品名称"` - Code string `json:"code" comment:"产品编号"` - Description string `json:"description" comment:"产品简介"` - Content string `json:"content" comment:"产品内容"` - CategoryID string `json:"category_id" comment:"产品分类ID"` - Price float64 `json:"price" comment:"产品价格"` - CostPrice float64 `json:"cost_price" comment:"成本价"` - Remark string `json:"remark" comment:"备注"` - IsEnabled bool `json:"is_enabled" comment:"是否启用"` - IsVisible bool `json:"is_visible" comment:"是否可见"` - IsPackage bool `json:"is_package" comment:"是否组合包"` + ID string `json:"id" comment:"产品ID"` + OldID *string `json:"old_id,omitempty" comment:"旧产品ID"` + Name string `json:"name" comment:"产品名称"` + Code string `json:"code" comment:"产品编号"` + Description string `json:"description" comment:"产品简介"` + Content string `json:"content" comment:"产品内容"` + CategoryID string `json:"category_id" comment:"一级分类ID"` + SubCategoryID *string `json:"sub_category_id,omitempty" comment:"二级分类ID"` + Price float64 `json:"price" comment:"产品价格"` + CostPrice float64 `json:"cost_price" comment:"成本价"` + Remark string `json:"remark" comment:"备注"` + IsEnabled bool `json:"is_enabled" comment:"是否启用"` + IsVisible bool `json:"is_visible" comment:"是否可见"` + IsPackage bool `json:"is_package" comment:"是否组合包"` // UI组件相关字段 SellUIComponent bool `json:"sell_ui_component" comment:"是否出售UI组件"` @@ -115,7 +118,8 @@ type ProductAdminInfoResponse struct { 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:"组合包项目列表"` diff --git a/internal/application/product/product_application_service_impl.go b/internal/application/product/product_application_service_impl.go index b2f7d03..82be60d 100644 --- a/internal/application/product/product_application_service_impl.go +++ b/internal/application/product/product_application_service_impl.go @@ -50,7 +50,7 @@ func NewProductApplicationService( } // CreateProduct 创建产品 -// 业务流程�?. 构建产品实体 2. 创建产品 +// 业务流程:1. 构建产品实体 2. 创建产品 func (s *ProductApplicationServiceImpl) CreateProduct(ctx context.Context, cmd *commands.CreateProductCommand) (*responses.ProductAdminInfoResponse, error) { // 1. 构建产品实体 product := &entities.Product{ @@ -59,6 +59,7 @@ func (s *ProductApplicationServiceImpl) CreateProduct(ctx context.Context, cmd * Description: cmd.Description, Content: cmd.Content, CategoryID: cmd.CategoryID, + SubCategoryID: cmd.SubCategoryID, Price: decimal.NewFromFloat(cmd.Price), CostPrice: decimal.NewFromFloat(cmd.CostPrice), Remark: cmd.Remark, @@ -97,6 +98,7 @@ func (s *ProductApplicationServiceImpl) UpdateProduct(ctx context.Context, cmd * existingProduct.Description = cmd.Description existingProduct.Content = cmd.Content existingProduct.CategoryID = cmd.CategoryID + existingProduct.SubCategoryID = cmd.SubCategoryID existingProduct.Price = decimal.NewFromFloat(cmd.Price) existingProduct.CostPrice = decimal.NewFromFloat(cmd.CostPrice) existingProduct.Remark = cmd.Remark @@ -490,30 +492,36 @@ func (s *ProductApplicationServiceImpl) GetProductByIDForUser(ctx context.Contex // convertToProductInfoResponse 转换为产品信息响应 func (s *ProductApplicationServiceImpl) convertToProductInfoResponse(product *entities.Product) *responses.ProductInfoResponse { response := &responses.ProductInfoResponse{ - ID: product.ID, - OldID: product.OldID, - Name: product.Name, - Code: product.Code, - Description: product.Description, - Content: product.Content, - CategoryID: product.CategoryID, - Price: product.Price.InexactFloat64(), - IsEnabled: product.IsEnabled, - IsPackage: product.IsPackage, - SellUIComponent: product.SellUIComponent, + ID: product.ID, + OldID: product.OldID, + Name: product.Name, + Code: product.Code, + Description: product.Description, + Content: product.Content, + CategoryID: product.CategoryID, + SubCategoryID: product.SubCategoryID, + Price: product.Price.InexactFloat64(), + IsEnabled: product.IsEnabled, + IsPackage: product.IsPackage, + SellUIComponent: product.SellUIComponent, UIComponentPrice: product.UIComponentPrice.InexactFloat64(), - SEOTitle: product.SEOTitle, - SEODescription: product.SEODescription, - SEOKeywords: product.SEOKeywords, - CreatedAt: product.CreatedAt, - UpdatedAt: product.UpdatedAt, + SEOTitle: product.SEOTitle, + SEODescription: product.SEODescription, + SEOKeywords: product.SEOKeywords, + CreatedAt: product.CreatedAt, + UpdatedAt: product.UpdatedAt, } - // 添加分类信息 + // 添加一级分类信息 if product.Category != nil { response.Category = s.convertToCategoryInfoResponse(product.Category) } + // 添加二级分类信息 + if product.SubCategory != nil { + response.SubCategory = s.convertToSubCategoryInfoResponse(product.SubCategory) + } + // 转换组合包项目信息 if product.IsPackage && len(product.PackageItems) > 0 { response.PackageItems = make([]*responses.PackageItemResponse, len(product.PackageItems)) @@ -536,33 +544,39 @@ func (s *ProductApplicationServiceImpl) convertToProductInfoResponse(product *en // convertToProductAdminInfoResponse 转换为管理员产品信息响应 func (s *ProductApplicationServiceImpl) convertToProductAdminInfoResponse(product *entities.Product) *responses.ProductAdminInfoResponse { response := &responses.ProductAdminInfoResponse{ - ID: product.ID, - OldID: product.OldID, - Name: product.Name, - Code: product.Code, - Description: product.Description, - Content: product.Content, - CategoryID: product.CategoryID, - Price: product.Price.InexactFloat64(), - CostPrice: product.CostPrice.InexactFloat64(), - Remark: product.Remark, - IsEnabled: product.IsEnabled, - IsVisible: product.IsVisible, // 管理员可以看到可见状态 - IsPackage: product.IsPackage, - SellUIComponent: product.SellUIComponent, + ID: product.ID, + OldID: product.OldID, + Name: product.Name, + Code: product.Code, + Description: product.Description, + Content: product.Content, + CategoryID: product.CategoryID, + SubCategoryID: product.SubCategoryID, + Price: product.Price.InexactFloat64(), + CostPrice: product.CostPrice.InexactFloat64(), + Remark: product.Remark, + IsEnabled: product.IsEnabled, + IsVisible: product.IsVisible, // 管理员可以看到可见状态 + IsPackage: product.IsPackage, + SellUIComponent: product.SellUIComponent, UIComponentPrice: product.UIComponentPrice.InexactFloat64(), - SEOTitle: product.SEOTitle, - SEODescription: product.SEODescription, - SEOKeywords: product.SEOKeywords, - CreatedAt: product.CreatedAt, - UpdatedAt: product.UpdatedAt, + SEOTitle: product.SEOTitle, + SEODescription: product.SEODescription, + SEOKeywords: product.SEOKeywords, + CreatedAt: product.CreatedAt, + UpdatedAt: product.UpdatedAt, } - // 添加分类信息 + // 添加一级分类信息 if product.Category != nil { response.Category = s.convertToCategoryInfoResponse(product.Category) } + // 添加二级分类信息 + if product.SubCategory != nil { + response.SubCategory = s.convertToSubCategoryInfoResponse(product.SubCategory) + } + // 转换组合包项目信息 if product.IsPackage && len(product.PackageItems) > 0 { 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配置 func (s *ProductApplicationServiceImpl) GetProductApiConfig(ctx context.Context, productID string) (*responses.ProductApiConfigResponse, error) { return s.productApiConfigAppService.GetProductApiConfig(ctx, productID) diff --git a/internal/application/product/sub_category_application_service.go b/internal/application/product/sub_category_application_service.go new file mode 100644 index 0000000..4da2108 --- /dev/null +++ b/internal/application/product/sub_category_application_service.go @@ -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) +} diff --git a/internal/application/product/sub_category_application_service_impl.go b/internal/application/product/sub_category_application_service_impl.go new file mode 100644 index 0000000..18b0bcd --- /dev/null +++ b/internal/application/product/sub_category_application_service_impl.go @@ -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 +} diff --git a/internal/container/container.go b/internal/container/container.go index 717919c..2e2ebef 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -557,6 +557,11 @@ func NewContainer() *Container { product_repo.NewGormProductCategoryRepository, fx.As(new(domain_product_repo.ProductCategoryRepository)), ), + // 产品二级分类仓储 - 同时注册具体类型和接口类型 + fx.Annotate( + product_repo.NewGormProductSubCategoryRepository, + fx.As(new(domain_product_repo.ProductSubCategoryRepository)), + ), // 订阅仓储 - 同时注册具体类型和接口类型 fx.Annotate( product_repo.NewGormSubscriptionRepository, @@ -1002,6 +1007,11 @@ func NewContainer() *Container { product.NewCategoryApplicationService, fx.As(new(product.CategoryApplicationService)), ), + // 二级分类应用服务 - 绑定到接口 + fx.Annotate( + product.NewSubCategoryApplicationService, + fx.As(new(product.SubCategoryApplicationService)), + ), fx.Annotate( product.NewDocumentationApplicationService, fx.As(new(product.DocumentationApplicationServiceInterface)), @@ -1198,6 +1208,8 @@ func NewContainer() *Container { handlers.NewProductHandler, // 产品管理员HTTP处理器 handlers.NewProductAdminHandler, + // 二级分类HTTP处理器 + handlers.NewSubCategoryHandler, // API Handler handlers.NewApiHandler, // 统计HTTP处理器 @@ -1266,6 +1278,8 @@ func NewContainer() *Container { routes.NewProductRoutes, // 产品管理员路由 routes.NewProductAdminRoutes, + // 二级分类路由 + routes.NewSubCategoryRoutes, // 组件报告订单路由 routes.NewComponentReportOrderRoutes, // UI组件路由 @@ -1384,6 +1398,7 @@ func RegisterRoutes( financeRoutes *routes.FinanceRoutes, productRoutes *routes.ProductRoutes, productAdminRoutes *routes.ProductAdminRoutes, + subCategoryRoutes *routes.SubCategoryRoutes, componentReportOrderRoutes *routes.ComponentReportOrderRoutes, uiComponentRoutes *routes.UIComponentRoutes, articleRoutes *routes.ArticleRoutes, @@ -1406,6 +1421,7 @@ func RegisterRoutes( financeRoutes.Register(router) productRoutes.Register(router) productAdminRoutes.Register(router) + subCategoryRoutes.Register(router) componentReportOrderRoutes.Register(router) uiComponentRoutes.Register(router) diff --git a/internal/domains/product/entities/product.go b/internal/domains/product/entities/product.go index 1692b8c..5917bcd 100644 --- a/internal/domains/product/entities/product.go +++ b/internal/domains/product/entities/product.go @@ -10,19 +10,20 @@ import ( // Product 产品实体 type Product struct { - ID string `gorm:"primaryKey;type:varchar(36)" comment:"产品ID"` - OldID *string `gorm:"type:varchar(36);index" 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:"产品简介"` - Content string `gorm:"type:text" comment:"产品内容"` - CategoryID string `gorm:"type:varchar(36);not null" comment:"产品分类ID"` - Price decimal.Decimal `gorm:"type:decimal(10,2);not null;default:0" comment:"产品价格"` - CostPrice decimal.Decimal `gorm:"type:decimal(10,2);default:0" comment:"成本价"` - Remark string `gorm:"type:text" comment:"备注"` - IsEnabled bool `gorm:"default:false" comment:"是否启用"` - IsVisible bool `gorm:"default:false" comment:"是否展示"` - IsPackage bool `gorm:"default:false" comment:"是否组合包"` + ID string `gorm:"primaryKey;type:varchar(36)" comment:"产品ID"` + OldID *string `gorm:"type:varchar(36);index" 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:"产品简介"` + Content string `gorm:"type:text" comment:"产品内容"` + CategoryID string `gorm:"type:varchar(36);not null" comment:"一级分类ID"` + SubCategoryID *string `gorm:"type:varchar(36);index" comment:"二级分类ID"` + Price decimal.Decimal `gorm:"type:decimal(10,2);not null;default:0" comment:"产品价格"` + CostPrice decimal.Decimal `gorm:"type:decimal(10,2);default:0" comment:"成本价"` + Remark string `gorm:"type:text" comment:"备注"` + IsEnabled bool `gorm:"default:false" comment:"是否启用"` + IsVisible bool `gorm:"default:false" comment:"是否展示"` + IsPackage bool `gorm:"default:false" comment:"是否组合包"` // 组合包相关关联 PackageItems []*ProductPackageItem `gorm:"foreignKey:PackageID" comment:"组合包项目列表"` // UI组件相关字段 @@ -34,7 +35,8 @@ type Product struct { 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:"产品文档"` CreatedAt time.Time `gorm:"autoCreateTime" comment:"创建时间"` @@ -118,3 +120,34 @@ func (p *Product) GetOldID() string { func (p *Product) HasOldID() bool { 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 +} diff --git a/internal/domains/product/entities/product_category.go b/internal/domains/product/entities/product_category.go index 2ce1ae4..eb83e15 100644 --- a/internal/domains/product/entities/product_category.go +++ b/internal/domains/product/entities/product_category.go @@ -18,7 +18,8 @@ type ProductCategory struct { 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:"创建时间"` UpdatedAt time.Time `gorm:"autoUpdateTime" comment:"更新时间"` diff --git a/internal/domains/product/entities/product_sub_category.go b/internal/domains/product/entities/product_sub_category.go new file mode 100644 index 0000000..6ecac3c --- /dev/null +++ b/internal/domains/product/entities/product_sub_category.go @@ -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 +} diff --git a/internal/domains/product/repositories/product_sub_category_repository_interface.go b/internal/domains/product/repositories/product_sub_category_repository_interface.go new file mode 100644 index 0000000..9714aba --- /dev/null +++ b/internal/domains/product/repositories/product_sub_category_repository_interface.go @@ -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) +} diff --git a/internal/domains/product/services/product_management_service.go b/internal/domains/product/services/product_management_service.go index 9468f04..9b784d7 100644 --- a/internal/domains/product/services/product_management_service.go +++ b/internal/domains/product/services/product_management_service.go @@ -18,21 +18,24 @@ import ( // ProductManagementService 产品管理领域服务 // 负责产品的基本管理操作,包括创建、查询、更新等 type ProductManagementService struct { - productRepo repositories.ProductRepository - categoryRepo repositories.ProductCategoryRepository - logger *zap.Logger + productRepo repositories.ProductRepository + categoryRepo repositories.ProductCategoryRepository + subCategoryRepo repositories.ProductSubCategoryRepository + logger *zap.Logger } // NewProductManagementService 创建产品管理领域服务 func NewProductManagementService( productRepo repositories.ProductRepository, categoryRepo repositories.ProductCategoryRepository, + subCategoryRepo repositories.ProductSubCategoryRepository, logger *zap.Logger, ) *ProductManagementService { return &ProductManagementService{ - productRepo: productRepo, - categoryRepo: categoryRepo, - logger: logger, + productRepo: productRepo, + categoryRepo: categoryRepo, + 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 } diff --git a/internal/infrastructure/database/repositories/product/gorm_component_report_repository.go b/internal/infrastructure/database/repositories/product/gorm_component_report_repository.go index e566a96..50202e2 100644 --- a/internal/infrastructure/database/repositories/product/gorm_component_report_repository.go +++ b/internal/infrastructure/database/repositories/product/gorm_component_report_repository.go @@ -116,7 +116,7 @@ func (r *GormComponentReportRepository) GetUserDownloadedProductCodes(ctx contex func (r *GormComponentReportRepository) GetDownloadByPaymentOrderID(ctx context.Context, orderID string) (*entities.ComponentReportDownload, error) { 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 errors.Is(err, gorm.ErrRecordNotFound) { return nil, gorm.ErrRecordNotFound diff --git a/internal/infrastructure/database/repositories/product/gorm_product_sub_category_repository.go b/internal/infrastructure/database/repositories/product/gorm_product_sub_category_repository.go new file mode 100644 index 0000000..c6dc64a --- /dev/null +++ b/internal/infrastructure/database/repositories/product/gorm_product_sub_category_repository.go @@ -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 +} diff --git a/internal/infrastructure/http/handlers/sub_category_handler.go b/internal/infrastructure/http/handlers/sub_category_handler.go new file mode 100644 index 0000000..614eeef --- /dev/null +++ b/internal/infrastructure/http/handlers/sub_category_handler.go @@ -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, "获取二级分类列表成功") +} diff --git a/internal/infrastructure/http/routes/sub_category_routes.go b/internal/infrastructure/http/routes/sub_category_routes.go new file mode 100644 index 0000000..0de95ec --- /dev/null +++ b/internal/infrastructure/http/routes/sub_category_routes.go @@ -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) // 根据一级分类获取二级分类列表 + } + } +}