f
This commit is contained in:
19
internal/application/product/category_application_service.go
Normal file
19
internal/application/product/category_application_service.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package product
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hyapi-server/internal/application/product/dto/commands"
|
||||
"hyapi-server/internal/application/product/dto/queries"
|
||||
"hyapi-server/internal/application/product/dto/responses"
|
||||
)
|
||||
|
||||
// CategoryApplicationService 分类应用服务接口
|
||||
type CategoryApplicationService interface {
|
||||
// 分类管理
|
||||
CreateCategory(ctx context.Context, cmd *commands.CreateCategoryCommand) error
|
||||
UpdateCategory(ctx context.Context, cmd *commands.UpdateCategoryCommand) error
|
||||
DeleteCategory(ctx context.Context, cmd *commands.DeleteCategoryCommand) error
|
||||
|
||||
GetCategoryByID(ctx context.Context, query *queries.GetCategoryQuery) (*responses.CategoryInfoResponse, error)
|
||||
ListCategories(ctx context.Context, query *queries.ListCategoriesQuery) (*responses.CategoryListResponse, error)
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
package product
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hyapi-server/internal/application/product/dto/commands"
|
||||
"hyapi-server/internal/application/product/dto/queries"
|
||||
"hyapi-server/internal/application/product/dto/responses"
|
||||
"hyapi-server/internal/domains/product/entities"
|
||||
"hyapi-server/internal/domains/product/repositories"
|
||||
repoQueries "hyapi-server/internal/domains/product/repositories/queries"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// CategoryApplicationServiceImpl 分类应用服务实现
|
||||
type CategoryApplicationServiceImpl struct {
|
||||
categoryRepo repositories.ProductCategoryRepository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewCategoryApplicationService 创建分类应用服务
|
||||
func NewCategoryApplicationService(
|
||||
categoryRepo repositories.ProductCategoryRepository,
|
||||
logger *zap.Logger,
|
||||
) CategoryApplicationService {
|
||||
return &CategoryApplicationServiceImpl{
|
||||
categoryRepo: categoryRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
func (s *CategoryApplicationServiceImpl) CreateCategory(ctx context.Context, cmd *commands.CreateCategoryCommand) error {
|
||||
// 1. 参数验证
|
||||
if err := s.validateCreateCategory(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 验证分类编号唯一性
|
||||
if err := s.validateCategoryCode(cmd.Code, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 3. 创建分类实体
|
||||
category := &entities.ProductCategory{
|
||||
Name: cmd.Name,
|
||||
Code: cmd.Code,
|
||||
Description: cmd.Description,
|
||||
Sort: cmd.Sort,
|
||||
IsEnabled: cmd.IsEnabled,
|
||||
IsVisible: cmd.IsVisible,
|
||||
}
|
||||
|
||||
// 4. 保存到仓储
|
||||
createdCategory, err := s.categoryRepo.Create(ctx, *category)
|
||||
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", createdCategory.ID), zap.String("code", cmd.Code))
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateCategory 更新分类
|
||||
func (s *CategoryApplicationServiceImpl) UpdateCategory(ctx context.Context, cmd *commands.UpdateCategoryCommand) error {
|
||||
// 1. 参数验证
|
||||
if err := s.validateUpdateCategory(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 获取现有分类
|
||||
existingCategory, err := s.categoryRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("分类不存在: %w", err)
|
||||
}
|
||||
|
||||
// 3. 验证分类编号唯一性(排除当前分类)
|
||||
if err := s.validateCategoryCode(cmd.Code, cmd.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 4. 更新分类信息
|
||||
existingCategory.Name = cmd.Name
|
||||
existingCategory.Code = cmd.Code
|
||||
existingCategory.Description = cmd.Description
|
||||
existingCategory.Sort = cmd.Sort
|
||||
existingCategory.IsEnabled = cmd.IsEnabled
|
||||
existingCategory.IsVisible = cmd.IsVisible
|
||||
|
||||
// 5. 保存到仓储
|
||||
if err := s.categoryRepo.Update(ctx, existingCategory); 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
|
||||
}
|
||||
|
||||
// DeleteCategory 删除分类
|
||||
func (s *CategoryApplicationServiceImpl) DeleteCategory(ctx context.Context, cmd *commands.DeleteCategoryCommand) error {
|
||||
// 1. 检查分类是否存在
|
||||
existingCategory, err := s.categoryRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("分类不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 检查是否有产品(可选,根据业务需求决定)
|
||||
// 这里可以添加检查逻辑,如果有产品则不允许删除
|
||||
|
||||
// 3. 删除分类
|
||||
if err := s.categoryRepo.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", existingCategory.Code))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCategoryByID 根据ID获取分类
|
||||
func (s *CategoryApplicationServiceImpl) GetCategoryByID(ctx context.Context, query *queries.GetCategoryQuery) (*responses.CategoryInfoResponse, error) {
|
||||
var category entities.ProductCategory
|
||||
var err error
|
||||
|
||||
if query.ID != "" {
|
||||
category, err = s.categoryRepo.GetByID(ctx, query.ID)
|
||||
} else {
|
||||
return nil, fmt.Errorf("分类ID不能为空")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("分类不存在: %w", err)
|
||||
}
|
||||
|
||||
// 转换为响应对象
|
||||
response := s.convertToCategoryInfoResponse(&category)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ListCategories 获取分类列表
|
||||
func (s *CategoryApplicationServiceImpl) ListCategories(ctx context.Context, query *queries.ListCategoriesQuery) (*responses.CategoryListResponse, error) {
|
||||
// 构建仓储查询
|
||||
repoQuery := &repoQueries.ListCategoriesQuery{
|
||||
Page: query.Page,
|
||||
PageSize: query.PageSize,
|
||||
IsEnabled: query.IsEnabled,
|
||||
IsVisible: query.IsVisible,
|
||||
SortBy: query.SortBy,
|
||||
SortOrder: query.SortOrder,
|
||||
}
|
||||
|
||||
// 调用仓储
|
||||
categories, total, err := s.categoryRepo.ListCategories(ctx, repoQuery)
|
||||
if err != nil {
|
||||
s.logger.Error("获取分类列表失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取分类列表失败: %w", err)
|
||||
}
|
||||
|
||||
// 转换为响应对象
|
||||
items := make([]responses.CategoryInfoResponse, len(categories))
|
||||
for i, category := range categories {
|
||||
items[i] = *s.convertToCategoryInfoResponse(category)
|
||||
}
|
||||
|
||||
return &responses.CategoryListResponse{
|
||||
Total: total,
|
||||
Page: query.Page,
|
||||
Size: query.PageSize,
|
||||
Items: items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// convertToCategoryInfoResponse 转换为分类信息响应
|
||||
func (s *CategoryApplicationServiceImpl) convertToCategoryInfoResponse(category *entities.ProductCategory) *responses.CategoryInfoResponse {
|
||||
return &responses.CategoryInfoResponse{
|
||||
ID: category.ID,
|
||||
Name: category.Name,
|
||||
Code: category.Code,
|
||||
Description: category.Description,
|
||||
Sort: category.Sort,
|
||||
IsEnabled: category.IsEnabled,
|
||||
IsVisible: category.IsVisible,
|
||||
CreatedAt: category.CreatedAt,
|
||||
UpdatedAt: category.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// convertToCategorySimpleResponse 转换为分类简单信息响应
|
||||
func (s *CategoryApplicationServiceImpl) convertToCategorySimpleResponse(category *entities.ProductCategory) *responses.CategorySimpleResponse {
|
||||
return &responses.CategorySimpleResponse{
|
||||
ID: category.ID,
|
||||
Name: category.Name,
|
||||
Code: category.Code,
|
||||
}
|
||||
}
|
||||
|
||||
// validateCreateCategory 验证创建分类参数
|
||||
func (s *CategoryApplicationServiceImpl) validateCreateCategory(cmd *commands.CreateCategoryCommand) error {
|
||||
if cmd.Name == "" {
|
||||
return errors.New("分类名称不能为空")
|
||||
}
|
||||
if cmd.Code == "" {
|
||||
return errors.New("分类编号不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateUpdateCategory 验证更新分类参数
|
||||
func (s *CategoryApplicationServiceImpl) validateUpdateCategory(cmd *commands.UpdateCategoryCommand) error {
|
||||
if cmd.ID == "" {
|
||||
return errors.New("分类ID不能为空")
|
||||
}
|
||||
if cmd.Name == "" {
|
||||
return errors.New("分类名称不能为空")
|
||||
}
|
||||
if cmd.Code == "" {
|
||||
return errors.New("分类编号不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateCategoryCode 验证分类编号唯一性
|
||||
func (s *CategoryApplicationServiceImpl) validateCategoryCode(code, excludeID string) error {
|
||||
if code == "" {
|
||||
return errors.New("分类编号不能为空")
|
||||
}
|
||||
|
||||
existingCategory, err := s.categoryRepo.FindByCode(context.Background(), code)
|
||||
if err == nil && existingCategory != nil && existingCategory.ID != excludeID {
|
||||
return errors.New("分类编号已存在")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
1305
internal/application/product/component_report_order_service.go
Normal file
1305
internal/application/product/component_report_order_service.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,248 @@
|
||||
package product
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"hyapi-server/internal/application/product/dto/commands"
|
||||
"hyapi-server/internal/application/product/dto/responses"
|
||||
"hyapi-server/internal/domains/product/entities"
|
||||
"hyapi-server/internal/domains/product/services"
|
||||
)
|
||||
|
||||
// DocumentationApplicationServiceInterface 文档应用服务接口
|
||||
type DocumentationApplicationServiceInterface interface {
|
||||
// CreateDocumentation 创建文档
|
||||
CreateDocumentation(ctx context.Context, cmd *commands.CreateDocumentationCommand) (*responses.DocumentationResponse, error)
|
||||
|
||||
// UpdateDocumentation 更新文档
|
||||
UpdateDocumentation(ctx context.Context, id string, cmd *commands.UpdateDocumentationCommand) (*responses.DocumentationResponse, error)
|
||||
|
||||
// GetDocumentation 获取文档
|
||||
GetDocumentation(ctx context.Context, id string) (*responses.DocumentationResponse, error)
|
||||
|
||||
// GetDocumentationByProductID 通过产品ID获取文档
|
||||
GetDocumentationByProductID(ctx context.Context, productID string) (*responses.DocumentationResponse, error)
|
||||
|
||||
// DeleteDocumentation 删除文档
|
||||
DeleteDocumentation(ctx context.Context, id string) error
|
||||
|
||||
// GetDocumentationsByProductIDs 批量获取文档
|
||||
GetDocumentationsByProductIDs(ctx context.Context, productIDs []string) ([]responses.DocumentationResponse, error)
|
||||
|
||||
// GenerateFullDocumentation 生成完整的接口文档(Markdown格式)
|
||||
GenerateFullDocumentation(ctx context.Context, productID string) (string, error)
|
||||
}
|
||||
|
||||
// DocumentationApplicationService 文档应用服务
|
||||
type DocumentationApplicationService struct {
|
||||
docService *services.ProductDocumentationService
|
||||
}
|
||||
|
||||
// NewDocumentationApplicationService 创建文档应用服务实例
|
||||
func NewDocumentationApplicationService(docService *services.ProductDocumentationService) *DocumentationApplicationService {
|
||||
return &DocumentationApplicationService{
|
||||
docService: docService,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDocumentation 创建文档
|
||||
func (s *DocumentationApplicationService) CreateDocumentation(ctx context.Context, cmd *commands.CreateDocumentationCommand) (*responses.DocumentationResponse, error) {
|
||||
// 创建文档实体
|
||||
doc := &entities.ProductDocumentation{
|
||||
RequestURL: cmd.RequestURL,
|
||||
RequestMethod: cmd.RequestMethod,
|
||||
BasicInfo: cmd.BasicInfo,
|
||||
RequestParams: cmd.RequestParams,
|
||||
ResponseFields: cmd.ResponseFields,
|
||||
ResponseExample: cmd.ResponseExample,
|
||||
ErrorCodes: cmd.ErrorCodes,
|
||||
PDFFilePath: cmd.PDFFilePath,
|
||||
}
|
||||
|
||||
// 调用领域服务创建文档
|
||||
err := s.docService.CreateDocumentation(ctx, cmd.ProductID, doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
resp := responses.NewDocumentationResponse(doc)
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// UpdateDocumentation 更新文档
|
||||
func (s *DocumentationApplicationService) UpdateDocumentation(ctx context.Context, id string, cmd *commands.UpdateDocumentationCommand) (*responses.DocumentationResponse, error) {
|
||||
// 调用领域服务更新文档
|
||||
err := s.docService.UpdateDocumentation(ctx, id,
|
||||
cmd.RequestURL,
|
||||
cmd.RequestMethod,
|
||||
cmd.BasicInfo,
|
||||
cmd.RequestParams,
|
||||
cmd.ResponseFields,
|
||||
cmd.ResponseExample,
|
||||
cmd.ErrorCodes,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取更新后的文档
|
||||
doc, err := s.docService.GetDocumentation(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 更新PDF文件路径(如果提供)
|
||||
if cmd.PDFFilePath != "" {
|
||||
doc.PDFFilePath = cmd.PDFFilePath
|
||||
err = s.docService.UpdateDocumentationEntity(ctx, doc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("更新PDF文件路径失败: %w", err)
|
||||
}
|
||||
// 重新获取更新后的文档以确保获取最新数据
|
||||
doc, err = s.docService.GetDocumentation(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
resp := responses.NewDocumentationResponse(doc)
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// GetDocumentation 获取文档
|
||||
func (s *DocumentationApplicationService) GetDocumentation(ctx context.Context, id string) (*responses.DocumentationResponse, error) {
|
||||
doc, err := s.docService.GetDocumentation(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
resp := responses.NewDocumentationResponse(doc)
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// GetDocumentationByProductID 通过产品ID获取文档
|
||||
func (s *DocumentationApplicationService) GetDocumentationByProductID(ctx context.Context, productID string) (*responses.DocumentationResponse, error) {
|
||||
doc, err := s.docService.GetDocumentationByProductID(ctx, productID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
resp := responses.NewDocumentationResponse(doc)
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// DeleteDocumentation 删除文档
|
||||
func (s *DocumentationApplicationService) DeleteDocumentation(ctx context.Context, id string) error {
|
||||
return s.docService.DeleteDocumentation(ctx, id)
|
||||
}
|
||||
|
||||
// GetDocumentationsByProductIDs 批量获取文档
|
||||
func (s *DocumentationApplicationService) GetDocumentationsByProductIDs(ctx context.Context, productIDs []string) ([]responses.DocumentationResponse, error) {
|
||||
docs, err := s.docService.GetDocumentationsByProductIDs(ctx, productIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var docResponses []responses.DocumentationResponse
|
||||
for _, doc := range docs {
|
||||
docResponses = append(docResponses, responses.NewDocumentationResponse(doc))
|
||||
}
|
||||
|
||||
return docResponses, nil
|
||||
}
|
||||
|
||||
// GenerateFullDocumentation 生成完整的接口文档(Markdown格式)
|
||||
func (s *DocumentationApplicationService) GenerateFullDocumentation(ctx context.Context, productID string) (string, error) {
|
||||
// 通过产品ID获取文档
|
||||
doc, err := s.docService.GetDocumentationByProductID(ctx, productID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取文档失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取文档时已经包含了产品信息(通过GetDocumentationWithProduct)
|
||||
// 如果没有产品信息,通过文档ID获取
|
||||
if doc.Product == nil && doc.ID != "" {
|
||||
docWithProduct, err := s.docService.GetDocumentationWithProduct(ctx, doc.ID)
|
||||
if err == nil && docWithProduct != nil {
|
||||
doc = docWithProduct
|
||||
}
|
||||
}
|
||||
|
||||
var markdown strings.Builder
|
||||
|
||||
// 添加文档标题
|
||||
productName := "产品"
|
||||
if doc.Product != nil {
|
||||
productName = doc.Product.Name
|
||||
}
|
||||
markdown.WriteString(fmt.Sprintf("# %s 接口文档\n\n", productName))
|
||||
|
||||
// 添加产品基本信息
|
||||
if doc.Product != nil {
|
||||
markdown.WriteString("## 产品信息\n\n")
|
||||
markdown.WriteString(fmt.Sprintf("- **产品名称**: %s\n", doc.Product.Name))
|
||||
markdown.WriteString(fmt.Sprintf("- **产品编号**: %s\n", doc.Product.Code))
|
||||
if doc.Product.Description != "" {
|
||||
markdown.WriteString(fmt.Sprintf("- **产品描述**: %s\n", doc.Product.Description))
|
||||
}
|
||||
markdown.WriteString("\n")
|
||||
}
|
||||
|
||||
// 添加请求方式
|
||||
markdown.WriteString("## 请求方式\n\n")
|
||||
if doc.RequestURL != "" {
|
||||
markdown.WriteString(fmt.Sprintf("- **请求方法**: %s\n", doc.RequestMethod))
|
||||
markdown.WriteString(fmt.Sprintf("- **请求地址**: %s\n", doc.RequestURL))
|
||||
markdown.WriteString("\n")
|
||||
}
|
||||
|
||||
// 添加请求方式详细说明
|
||||
if doc.BasicInfo != "" {
|
||||
markdown.WriteString("### 请求方式说明\n\n")
|
||||
markdown.WriteString(doc.BasicInfo)
|
||||
markdown.WriteString("\n\n")
|
||||
}
|
||||
|
||||
// 添加请求参数
|
||||
if doc.RequestParams != "" {
|
||||
markdown.WriteString("## 请求参数\n\n")
|
||||
markdown.WriteString(doc.RequestParams)
|
||||
markdown.WriteString("\n\n")
|
||||
}
|
||||
|
||||
// 添加返回字段说明
|
||||
if doc.ResponseFields != "" {
|
||||
markdown.WriteString("## 返回字段说明\n\n")
|
||||
markdown.WriteString(doc.ResponseFields)
|
||||
markdown.WriteString("\n\n")
|
||||
}
|
||||
|
||||
// 添加响应示例
|
||||
if doc.ResponseExample != "" {
|
||||
markdown.WriteString("## 响应示例\n\n")
|
||||
markdown.WriteString(doc.ResponseExample)
|
||||
markdown.WriteString("\n\n")
|
||||
}
|
||||
|
||||
// 添加错误代码
|
||||
if doc.ErrorCodes != "" {
|
||||
markdown.WriteString("## 错误代码\n\n")
|
||||
markdown.WriteString(doc.ErrorCodes)
|
||||
markdown.WriteString("\n\n")
|
||||
}
|
||||
|
||||
// 添加文档版本信息
|
||||
markdown.WriteString("---\n\n")
|
||||
markdown.WriteString(fmt.Sprintf("**文档版本**: %s\n\n", doc.Version))
|
||||
if doc.UpdatedAt.Year() > 1900 {
|
||||
markdown.WriteString(fmt.Sprintf("**更新时间**: %s\n", doc.UpdatedAt.Format("2006-01-02 15:04:05")))
|
||||
}
|
||||
|
||||
return markdown.String(), nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package commands
|
||||
|
||||
// CreateCategoryCommand 创建分类命令
|
||||
type CreateCategoryCommand struct {
|
||||
Name string `json:"name" binding:"required,min=2,max=50" comment:"分类名称"`
|
||||
Code string `json:"code" binding:"required,product_code" comment:"分类编号"`
|
||||
Description string `json:"description" binding:"omitempty,max=200" comment:"分类描述"`
|
||||
Sort int `json:"sort" binding:"min=0,max=9999" comment:"排序"`
|
||||
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
|
||||
IsVisible bool `json:"is_visible" comment:"是否展示"`
|
||||
}
|
||||
|
||||
// UpdateCategoryCommand 更新分类命令
|
||||
type UpdateCategoryCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required,uuid" comment:"分类ID"`
|
||||
Name string `json:"name" binding:"required,min=2,max=50" comment:"分类名称"`
|
||||
Code string `json:"code" binding:"required,product_code" comment:"分类编号"`
|
||||
Description string `json:"description" binding:"omitempty,max=200" comment:"分类描述"`
|
||||
Sort int `json:"sort" binding:"min=0,max=9999" comment:"排序"`
|
||||
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
|
||||
IsVisible bool `json:"is_visible" comment:"是否展示"`
|
||||
}
|
||||
|
||||
// DeleteCategoryCommand 删除分类命令
|
||||
type DeleteCategoryCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required,uuid" comment:"分类ID"`
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package commands
|
||||
|
||||
// CreateDocumentationCommand 创建文档命令
|
||||
type CreateDocumentationCommand struct {
|
||||
ProductID string `json:"product_id" binding:"required" validate:"required"`
|
||||
RequestURL string `json:"request_url" binding:"required" validate:"required"`
|
||||
RequestMethod string `json:"request_method" binding:"required" validate:"required"`
|
||||
BasicInfo string `json:"basic_info" validate:"required"`
|
||||
RequestParams string `json:"request_params" validate:"required"`
|
||||
ResponseFields string `json:"response_fields"`
|
||||
ResponseExample string `json:"response_example"`
|
||||
ErrorCodes string `json:"error_codes"`
|
||||
PDFFilePath string `json:"pdf_file_path,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateDocumentationCommand 更新文档命令
|
||||
type UpdateDocumentationCommand struct {
|
||||
RequestURL string `json:"request_url"`
|
||||
RequestMethod string `json:"request_method"`
|
||||
BasicInfo string `json:"basic_info"`
|
||||
RequestParams string `json:"request_params"`
|
||||
ResponseFields string `json:"response_fields"`
|
||||
ResponseExample string `json:"response_example"`
|
||||
ErrorCodes string `json:"error_codes"`
|
||||
PDFFilePath string `json:"pdf_file_path,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package commands
|
||||
|
||||
// AddPackageItemCommand 添加组合包子产品命令
|
||||
type AddPackageItemCommand struct {
|
||||
ProductID string `json:"product_id" binding:"required,uuid" comment:"子产品ID"`
|
||||
}
|
||||
|
||||
// UpdatePackageItemCommand 更新组合包子产品命令
|
||||
type UpdatePackageItemCommand struct {
|
||||
SortOrder int `json:"sort_order" binding:"required,min=0" comment:"排序"`
|
||||
}
|
||||
|
||||
// ReorderPackageItemsCommand 重新排序组合包子产品命令
|
||||
type ReorderPackageItemsCommand struct {
|
||||
ItemIDs []string `json:"item_ids" binding:"required,dive,uuid" comment:"子产品ID列表"`
|
||||
}
|
||||
|
||||
// UpdatePackageItemsCommand 批量更新组合包子产品命令
|
||||
type UpdatePackageItemsCommand struct {
|
||||
Items []PackageItemData `json:"items" binding:"required,dive" comment:"子产品列表"`
|
||||
}
|
||||
|
||||
// PackageItemData 组合包子产品数据
|
||||
type PackageItemData struct {
|
||||
ProductID string `json:"product_id" binding:"required,uuid" comment:"子产品ID"`
|
||||
SortOrder int `json:"sort_order" binding:"required,min=0" comment:"排序"`
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
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"`
|
||||
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组件"`
|
||||
UIComponentPrice float64 `json:"ui_component_price" binding:"omitempty,min=0" comment:"UI组件销售价格(组合包使用)"`
|
||||
|
||||
// SEO信息
|
||||
SEOTitle string `json:"seo_title" binding:"omitempty,max=100" comment:"SEO标题"`
|
||||
SEODescription string `json:"seo_description" binding:"omitempty,max=200" comment:"SEO描述"`
|
||||
SEOKeywords string `json:"seo_keywords" binding:"omitempty,max=200" comment:"SEO关键词"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
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组件"`
|
||||
UIComponentPrice float64 `json:"ui_component_price" binding:"omitempty,min=0" comment:"UI组件销售价格(组合包使用)"`
|
||||
|
||||
// SEO信息
|
||||
SEOTitle string `json:"seo_title" binding:"omitempty,max=100" comment:"SEO标题"`
|
||||
SEODescription string `json:"seo_description" binding:"omitempty,max=200" comment:"SEO描述"`
|
||||
SEOKeywords string `json:"seo_keywords" binding:"omitempty,max=200" comment:"SEO关键词"`
|
||||
}
|
||||
|
||||
// DeleteProductCommand 删除产品命令
|
||||
type DeleteProductCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required,uuid" comment:"产品ID"`
|
||||
}
|
||||
|
||||
// CreateProductApiConfigCommand 创建产品API配置命令
|
||||
type CreateProductApiConfigCommand struct {
|
||||
ProductID string `json:"product_id" binding:"required,uuid" comment:"产品ID"`
|
||||
ApiEndpoint string `json:"api_endpoint" binding:"required,url" comment:"API端点"`
|
||||
ApiKey string `json:"api_key" binding:"required" comment:"API密钥"`
|
||||
ApiSecret string `json:"api_secret" binding:"required" comment:"API密钥"`
|
||||
Config string `json:"config" binding:"omitempty" comment:"配置信息"`
|
||||
}
|
||||
|
||||
// UpdateProductApiConfigCommand 更新产品API配置命令
|
||||
type UpdateProductApiConfigCommand struct {
|
||||
ProductID string `json:"product_id" binding:"required,uuid" comment:"产品ID"`
|
||||
ApiEndpoint string `json:"api_endpoint" binding:"required,url" comment:"API端点"`
|
||||
ApiKey string `json:"api_key" binding:"required" comment:"API密钥"`
|
||||
ApiSecret string `json:"api_secret" binding:"required" comment:"API密钥"`
|
||||
Config string `json:"config" binding:"omitempty" comment:"配置信息"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package commands
|
||||
|
||||
// CreateSubscriptionCommand 创建订阅命令
|
||||
type CreateSubscriptionCommand struct {
|
||||
UserID string `json:"-" comment:"用户ID"`
|
||||
ProductID string `json:"-" uri:"id" binding:"required,uuid" comment:"产品ID"`
|
||||
}
|
||||
|
||||
// UpdateSubscriptionPriceCommand 更新订阅价格命令
|
||||
type UpdateSubscriptionPriceCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required,uuid" comment:"订阅ID"`
|
||||
Price float64 `json:"price" binding:"price,min=0" comment:"订阅价格"`
|
||||
UIComponentPrice float64 `json:"ui_component_price" binding:"omitempty,min=0" comment:"UI组件价格(组合包使用)"`
|
||||
}
|
||||
|
||||
// BatchUpdateSubscriptionPricesCommand 批量更新订阅价格命令
|
||||
type BatchUpdateSubscriptionPricesCommand struct {
|
||||
UserID string `json:"user_id" binding:"required,uuid" comment:"用户ID"`
|
||||
AdjustmentType string `json:"adjustment_type" binding:"required,oneof=discount cost_multiple" comment:"调整方式(discount:按售价折扣,cost_multiple:按成本价倍数)"`
|
||||
Discount float64 `json:"discount,omitempty" binding:"omitempty,min=0.1,max=10" comment:"折扣比例(0.1-10折)"`
|
||||
CostMultiple float64 `json:"cost_multiple,omitempty" binding:"omitempty,min=0.1" comment:"成本价倍数"`
|
||||
Scope string `json:"scope" binding:"required,oneof=undiscounted all" comment:"改价范围(undiscounted:仅未打折,all:所有)"`
|
||||
}
|
||||
17
internal/application/product/dto/queries/category_queries.go
Normal file
17
internal/application/product/dto/queries/category_queries.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package queries
|
||||
|
||||
// ListCategoriesQuery 分类列表查询
|
||||
type ListCategoriesQuery struct {
|
||||
Page int `form:"page" binding:"omitempty,min=1" comment:"页码"`
|
||||
PageSize int `form:"page_size" binding:"omitempty,min=1,max=100" comment:"每页数量"`
|
||||
IsEnabled *bool `form:"is_enabled" comment:"是否启用"`
|
||||
IsVisible *bool `form:"is_visible" comment:"是否展示"`
|
||||
SortBy string `form:"sort_by" binding:"omitempty,oneof=name code sort created_at updated_at" comment:"排序字段"`
|
||||
SortOrder string `form:"sort_order" binding:"omitempty,sort_order" comment:"排序方向"`
|
||||
}
|
||||
|
||||
// GetCategoryQuery 获取分类详情查询
|
||||
type GetCategoryQuery struct {
|
||||
ID string `uri:"id" binding:"omitempty,uuid" comment:"分类ID"`
|
||||
Code string `form:"code" binding:"omitempty,product_code" comment:"分类编号"`
|
||||
}
|
||||
10
internal/application/product/dto/queries/package_queries.go
Normal file
10
internal/application/product/dto/queries/package_queries.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package queries
|
||||
|
||||
// GetAvailableProductsQuery 获取可选子产品查询
|
||||
type GetAvailableProductsQuery struct {
|
||||
ExcludePackageID string `form:"exclude_package_id" binding:"omitempty,uuid" comment:"排除的组合包ID"`
|
||||
Keyword string `form:"keyword" binding:"omitempty,max=100" comment:"搜索关键词"`
|
||||
CategoryID string `form:"category_id" binding:"omitempty,uuid" comment:"分类ID"`
|
||||
Page int `form:"page" binding:"omitempty,min=1" comment:"页码"`
|
||||
PageSize int `form:"page_size" binding:"omitempty,min=1,max=100" comment:"每页数量"`
|
||||
}
|
||||
54
internal/application/product/dto/queries/product_queries.go
Normal file
54
internal/application/product/dto/queries/product_queries.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package queries
|
||||
|
||||
// ListProductsQuery 产品列表查询
|
||||
type ListProductsQuery struct {
|
||||
Page int `form:"page" binding:"omitempty,min=1" comment:"页码"`
|
||||
PageSize int `form:"page_size" binding:"omitempty,min=1,max=100" comment:"每页数量"`
|
||||
Keyword string `form:"keyword" binding:"omitempty,max=100" comment:"搜索关键词"`
|
||||
CategoryID string `form:"category_id" binding:"omitempty,uuid" comment:"分类ID"`
|
||||
MinPrice *float64 `form:"min_price" binding:"omitempty,min=0" comment:"最低价格"`
|
||||
MaxPrice *float64 `form:"max_price" binding:"omitempty,min=0" comment:"最高价格"`
|
||||
IsEnabled *bool `form:"is_enabled" comment:"是否启用"`
|
||||
IsVisible *bool `form:"is_visible" comment:"是否展示"`
|
||||
IsPackage *bool `form:"is_package" comment:"是否组合包"`
|
||||
SortBy string `form:"sort_by" binding:"omitempty,oneof=name code price created_at updated_at" comment:"排序字段"`
|
||||
SortOrder string `form:"sort_order" binding:"omitempty,sort_order" comment:"排序方向"`
|
||||
}
|
||||
|
||||
// SearchProductsQuery 产品搜索查询
|
||||
type SearchProductsQuery struct {
|
||||
Page int `form:"page" binding:"omitempty,min=1" comment:"页码"`
|
||||
PageSize int `form:"page_size" binding:"omitempty,min=1,max=100" comment:"每页数量"`
|
||||
Keyword string `form:"keyword" binding:"omitempty,max=100" comment:"搜索关键词"`
|
||||
CategoryID string `form:"category_id" binding:"omitempty,uuid" comment:"分类ID"`
|
||||
MinPrice *float64 `form:"min_price" binding:"omitempty,min=0" comment:"最低价格"`
|
||||
MaxPrice *float64 `form:"max_price" binding:"omitempty,min=0" comment:"最高价格"`
|
||||
IsEnabled *bool `form:"is_enabled" comment:"是否启用"`
|
||||
IsVisible *bool `form:"is_visible" comment:"是否展示"`
|
||||
IsPackage *bool `form:"is_package" comment:"是否组合包"`
|
||||
SortBy string `form:"sort_by" binding:"omitempty,oneof=name code price created_at updated_at" comment:"排序字段"`
|
||||
SortOrder string `form:"sort_order" binding:"omitempty,sort_order" comment:"排序方向"`
|
||||
}
|
||||
|
||||
// GetProductQuery 获取产品详情查询
|
||||
type GetProductQuery struct {
|
||||
ID string `uri:"id" binding:"omitempty,uuid" comment:"产品ID"`
|
||||
Code string `form:"code" binding:"omitempty,product_code" comment:"产品编号"`
|
||||
}
|
||||
|
||||
// GetProductDetailQuery 获取产品详情查询(支持可选文档)
|
||||
type GetProductDetailQuery struct {
|
||||
ID string `uri:"id" binding:"omitempty,uuid" comment:"产品ID"`
|
||||
Code string `form:"code" binding:"omitempty,product_code" comment:"产品编号"`
|
||||
WithDocument *bool `form:"with_document" comment:"是否包含文档信息"`
|
||||
}
|
||||
|
||||
// GetProductsByIDsQuery 根据ID列表获取产品查询
|
||||
type GetProductsByIDsQuery struct {
|
||||
IDs []string `form:"ids" binding:"required,dive,uuid" comment:"产品ID列表"`
|
||||
}
|
||||
|
||||
// GetSubscribableProductsQuery 获取可订阅产品查询
|
||||
type GetSubscribableProductsQuery struct {
|
||||
UserID string `form:"user_id" binding:"required" comment:"用户ID"`
|
||||
}
|
||||
@@ -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:"排序方向"`
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package queries
|
||||
|
||||
// ListSubscriptionsQuery 订阅列表查询
|
||||
type ListSubscriptionsQuery struct {
|
||||
Page int `form:"page" binding:"omitempty,min=1" comment:"页码"`
|
||||
PageSize int `form:"page_size" binding:"omitempty,min=1,max=100" comment:"每页数量"`
|
||||
UserID string `form:"user_id" binding:"omitempty" comment:"用户ID"`
|
||||
Keyword string `form:"keyword" binding:"omitempty,max=100" comment:"搜索关键词"`
|
||||
SortBy string `form:"sort_by" binding:"omitempty,oneof=created_at updated_at price" comment:"排序字段"`
|
||||
SortOrder string `form:"sort_order" binding:"omitempty,sort_order" comment:"排序方向"`
|
||||
|
||||
// 新增筛选字段
|
||||
CompanyName string `form:"company_name" binding:"omitempty,max=100" comment:"企业名称"`
|
||||
ProductName string `form:"product_name" binding:"omitempty,max=100" comment:"产品名称"`
|
||||
StartTime string `form:"start_time" binding:"omitempty,datetime=2006-01-02 15:04:05" comment:"订阅开始时间"`
|
||||
EndTime string `form:"end_time" binding:"omitempty,datetime=2006-01-02 15:04:05" comment:"订阅结束时间"`
|
||||
}
|
||||
|
||||
// GetSubscriptionQuery 获取订阅详情查询
|
||||
type GetSubscriptionQuery struct {
|
||||
ID string `uri:"id" binding:"required,uuid" comment:"订阅ID"`
|
||||
}
|
||||
|
||||
// GetUserSubscriptionsQuery 获取用户订阅查询
|
||||
type GetUserSubscriptionsQuery struct {
|
||||
UserID string `form:"user_id" binding:"required,uuid" comment:"用户ID"`
|
||||
}
|
||||
|
||||
// GetProductSubscriptionsQuery 获取产品订阅查询
|
||||
type GetProductSubscriptionsQuery struct {
|
||||
ProductID string `form:"product_id" binding:"required,uuid" comment:"产品ID"`
|
||||
}
|
||||
|
||||
// GetActiveSubscriptionsQuery 获取活跃订阅查询
|
||||
type GetActiveSubscriptionsQuery struct {
|
||||
UserID string `form:"user_id" binding:"omitempty,uuid" comment:"用户ID"`
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package responses
|
||||
|
||||
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:"是否展示"`
|
||||
|
||||
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:"每页数量"`
|
||||
Items []CategoryInfoResponse `json:"items" comment:"分类列表"`
|
||||
}
|
||||
|
||||
// CategorySimpleResponse 分类简单信息响应
|
||||
type CategorySimpleResponse struct {
|
||||
ID string `json:"id" comment:"分类ID"`
|
||||
Name string `json:"name" 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"`
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package responses
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"hyapi-server/internal/domains/product/entities"
|
||||
)
|
||||
|
||||
// DocumentationResponse 文档响应
|
||||
type DocumentationResponse struct {
|
||||
ID string `json:"id"`
|
||||
ProductID string `json:"product_id"`
|
||||
RequestURL string `json:"request_url"`
|
||||
RequestMethod string `json:"request_method"`
|
||||
BasicInfo string `json:"basic_info"`
|
||||
RequestParams string `json:"request_params"`
|
||||
ResponseFields string `json:"response_fields"`
|
||||
ResponseExample string `json:"response_example"`
|
||||
ErrorCodes string `json:"error_codes"`
|
||||
Version string `json:"version"`
|
||||
PDFFilePath string `json:"pdf_file_path,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// NewDocumentationResponse 从实体创建响应
|
||||
func NewDocumentationResponse(doc *entities.ProductDocumentation) DocumentationResponse {
|
||||
return DocumentationResponse{
|
||||
ID: doc.ID,
|
||||
ProductID: doc.ProductID,
|
||||
RequestURL: doc.RequestURL,
|
||||
RequestMethod: doc.RequestMethod,
|
||||
BasicInfo: doc.BasicInfo,
|
||||
RequestParams: doc.RequestParams,
|
||||
ResponseFields: doc.ResponseFields,
|
||||
ResponseExample: doc.ResponseExample,
|
||||
ErrorCodes: doc.ErrorCodes,
|
||||
Version: doc.Version,
|
||||
PDFFilePath: doc.PDFFilePath,
|
||||
CreatedAt: doc.CreatedAt,
|
||||
UpdatedAt: doc.UpdatedAt,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package responses
|
||||
|
||||
import "time"
|
||||
|
||||
// ProductApiConfigResponse 产品API配置响应
|
||||
type ProductApiConfigResponse struct {
|
||||
ID string `json:"id" comment:"配置ID"`
|
||||
ProductID string `json:"product_id" comment:"产品ID"`
|
||||
RequestParams []RequestParamResponse `json:"request_params" comment:"请求参数配置"`
|
||||
ResponseFields []ResponseFieldResponse `json:"response_fields" comment:"响应字段配置"`
|
||||
ResponseExample map[string]interface{} `json:"response_example" comment:"响应示例"`
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
}
|
||||
|
||||
// RequestParamResponse 请求参数响应
|
||||
type RequestParamResponse struct {
|
||||
Name string `json:"name" comment:"参数名称"`
|
||||
Field string `json:"field" comment:"参数字段名"`
|
||||
Type string `json:"type" comment:"参数类型"`
|
||||
Required bool `json:"required" comment:"是否必填"`
|
||||
Description string `json:"description" comment:"参数描述"`
|
||||
Example string `json:"example" comment:"参数示例"`
|
||||
Validation string `json:"validation" comment:"验证规则"`
|
||||
}
|
||||
|
||||
// ResponseFieldResponse 响应字段响应
|
||||
type ResponseFieldResponse struct {
|
||||
Name string `json:"name" comment:"字段名称"`
|
||||
Path string `json:"path" comment:"字段路径"`
|
||||
Type string `json:"type" comment:"字段类型"`
|
||||
Description string `json:"description" comment:"字段描述"`
|
||||
Required bool `json:"required" comment:"是否必填"`
|
||||
Example string `json:"example" comment:"字段示例"`
|
||||
}
|
||||
|
||||
// ProductApiConfigListResponse 产品API配置列表响应
|
||||
type ProductApiConfigListResponse struct {
|
||||
Total int64 `json:"total" comment:"总数"`
|
||||
Page int `json:"page" comment:"页码"`
|
||||
Size int `json:"size" comment:"每页数量"`
|
||||
Items []ProductApiConfigResponse `json:"items" comment:"配置列表"`
|
||||
}
|
||||
146
internal/application/product/dto/responses/product_responses.go
Normal file
146
internal/application/product/dto/responses/product_responses.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package responses
|
||||
|
||||
import "time"
|
||||
|
||||
// PackageItemResponse 组合包项目响应
|
||||
type PackageItemResponse struct {
|
||||
ID string `json:"id" comment:"项目ID"`
|
||||
ProductID string `json:"product_id" comment:"子产品ID"`
|
||||
ProductCode string `json:"product_code" comment:"子产品编号"`
|
||||
ProductName string `json:"product_name" comment:"子产品名称"`
|
||||
SortOrder int `json:"sort_order" comment:"排序"`
|
||||
Price float64 `json:"price" comment:"子产品价格"`
|
||||
CostPrice float64 `json:"cost_price" comment:"子产品成本价"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
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组件销售价格(组合包使用)"`
|
||||
|
||||
// SEO信息
|
||||
SEOTitle string `json:"seo_title" comment:"SEO标题"`
|
||||
SEODescription string `json:"seo_description" comment:"SEO描述"`
|
||||
SEOKeywords string `json:"seo_keywords" comment:"SEO关键词"`
|
||||
|
||||
// 关联信息
|
||||
Category *CategoryInfoResponse `json:"category,omitempty" comment:"一级分类信息"`
|
||||
SubCategory *SubCategoryInfoResponse `json:"sub_category,omitempty" comment:"二级分类信息"`
|
||||
|
||||
// 组合包信息
|
||||
PackageItems []*PackageItemResponse `json:"package_items,omitempty" comment:"组合包项目列表"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
}
|
||||
|
||||
// ProductListResponse 产品列表响应
|
||||
type ProductListResponse struct {
|
||||
Total int64 `json:"total" comment:"总数"`
|
||||
Page int `json:"page" comment:"页码"`
|
||||
Size int `json:"size" comment:"每页数量"`
|
||||
Items []ProductInfoResponse `json:"items" comment:"产品列表"`
|
||||
}
|
||||
|
||||
// ProductSearchResponse 产品搜索响应
|
||||
type ProductSearchResponse struct {
|
||||
Total int64 `json:"total" comment:"总数"`
|
||||
Page int `json:"page" comment:"页码"`
|
||||
Size int `json:"size" comment:"每页数量"`
|
||||
Items []ProductInfoResponse `json:"items" comment:"产品列表"`
|
||||
}
|
||||
|
||||
// ProductSimpleResponse 产品简单信息响应
|
||||
type ProductSimpleResponse 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:"产品简介"`
|
||||
Category *CategorySimpleResponse `json:"category,omitempty" comment:"分类信息"`
|
||||
Price float64 `json:"price" comment:"产品价格"`
|
||||
IsPackage bool `json:"is_package" comment:"是否组合包"`
|
||||
IsSubscribed *bool `json:"is_subscribed,omitempty" comment:"当前用户是否已订阅"`
|
||||
}
|
||||
|
||||
// ProductSimpleAdminResponse 管理员产品简单信息响应(包含成本价)
|
||||
type ProductSimpleAdminResponse struct {
|
||||
ProductSimpleResponse
|
||||
CostPrice float64 `json:"cost_price" comment:"成本价"`
|
||||
UIComponentPrice float64 `json:"ui_component_price" comment:"UI组件价格(组合包使用)"`
|
||||
}
|
||||
|
||||
// ProductStatsResponse 产品统计响应
|
||||
type ProductStatsResponse struct {
|
||||
TotalProducts int64 `json:"total_products" comment:"产品总数"`
|
||||
EnabledProducts int64 `json:"enabled_products" comment:"启用产品数"`
|
||||
VisibleProducts int64 `json:"visible_products" comment:"可见产品数"`
|
||||
PackageProducts int64 `json:"package_products" comment:"组合包产品数"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
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组件"`
|
||||
UIComponentPrice float64 `json:"ui_component_price" comment:"UI组件销售价格(组合包使用)"`
|
||||
|
||||
// SEO信息
|
||||
SEOTitle string `json:"seo_title" comment:"SEO标题"`
|
||||
SEODescription string `json:"seo_description" comment:"SEO描述"`
|
||||
SEOKeywords string `json:"seo_keywords" comment:"SEO关键词"`
|
||||
|
||||
// 关联信息
|
||||
Category *CategoryInfoResponse `json:"category,omitempty" comment:"一级分类信息"`
|
||||
SubCategory *SubCategoryInfoResponse `json:"sub_category,omitempty" comment:"二级分类信息"`
|
||||
|
||||
// 组合包信息
|
||||
PackageItems []*PackageItemResponse `json:"package_items,omitempty" comment:"组合包项目列表"`
|
||||
|
||||
// 文档信息
|
||||
Documentation *DocumentationResponse `json:"documentation,omitempty" comment:"产品文档"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
}
|
||||
|
||||
// ProductInfoWithDocumentResponse 包含文档的产品详情响应
|
||||
type ProductInfoWithDocumentResponse struct {
|
||||
ProductInfoResponse
|
||||
Documentation *DocumentationResponse `json:"documentation,omitempty" comment:"产品文档"`
|
||||
}
|
||||
|
||||
// ProductAdminListResponse 管理员产品列表响应
|
||||
type ProductAdminListResponse struct {
|
||||
Total int64 `json:"total" comment:"总数"`
|
||||
Page int `json:"page" comment:"页码"`
|
||||
Size int `json:"size" comment:"每页数量"`
|
||||
Items []ProductAdminInfoResponse `json:"items" comment:"产品列表"`
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package responses
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// UserSimpleResponse 用户简单信息响应
|
||||
type UserSimpleResponse struct {
|
||||
ID string `json:"id" comment:"用户ID"`
|
||||
CompanyName string `json:"company_name" comment:"公司名称"`
|
||||
Phone string `json:"phone" comment:"手机号"`
|
||||
}
|
||||
|
||||
// SubscriptionInfoResponse 订阅详情响应
|
||||
type SubscriptionInfoResponse struct {
|
||||
ID string `json:"id" comment:"订阅ID"`
|
||||
UserID string `json:"user_id" comment:"用户ID"`
|
||||
ProductID string `json:"product_id" comment:"产品ID"`
|
||||
Price float64 `json:"price" comment:"订阅价格"`
|
||||
UIComponentPrice float64 `json:"ui_component_price" comment:"UI组件价格(组合包使用)"`
|
||||
APIUsed int64 `json:"api_used" comment:"已使用API调用次数"`
|
||||
|
||||
// 关联信息
|
||||
User *UserSimpleResponse `json:"user,omitempty" comment:"用户信息"`
|
||||
Product *ProductSimpleResponse `json:"product,omitempty" comment:"产品信息"`
|
||||
// 管理员端使用,包含成本价的产品信息
|
||||
ProductAdmin *ProductSimpleAdminResponse `json:"product_admin,omitempty" comment:"产品信息(管理员端,包含成本价)"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
}
|
||||
|
||||
// SubscriptionListResponse 订阅列表响应
|
||||
type SubscriptionListResponse struct {
|
||||
Total int64 `json:"total" comment:"总数"`
|
||||
Page int `json:"page" comment:"页码"`
|
||||
Size int `json:"size" comment:"每页数量"`
|
||||
Items []SubscriptionInfoResponse `json:"items" comment:"订阅列表"`
|
||||
}
|
||||
|
||||
// SubscriptionSimpleResponse 订阅简单信息响应
|
||||
type SubscriptionSimpleResponse struct {
|
||||
ID string `json:"id" comment:"订阅ID"`
|
||||
ProductID string `json:"product_id" comment:"产品ID"`
|
||||
Price float64 `json:"price" comment:"订阅价格"`
|
||||
APIUsed int64 `json:"api_used" comment:"已使用API调用次数"`
|
||||
}
|
||||
|
||||
// SubscriptionUsageResponse 订阅使用情况响应
|
||||
type SubscriptionUsageResponse struct {
|
||||
ID string `json:"id" comment:"订阅ID"`
|
||||
ProductID string `json:"product_id" comment:"产品ID"`
|
||||
APIUsed int64 `json:"api_used" comment:"已使用API调用次数"`
|
||||
}
|
||||
|
||||
// SubscriptionStatsResponse 订阅统计响应
|
||||
type SubscriptionStatsResponse struct {
|
||||
TotalSubscriptions int64 `json:"total_subscriptions" comment:"订阅总数"`
|
||||
TotalRevenue float64 `json:"total_revenue" comment:"总收入"`
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
package product
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"hyapi-server/internal/application/product/dto/responses"
|
||||
"hyapi-server/internal/domains/product/entities"
|
||||
"hyapi-server/internal/domains/product/services"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// ProductApiConfigApplicationService 产品API配置应用服务接口
|
||||
type ProductApiConfigApplicationService interface {
|
||||
// 获取产品API配置
|
||||
GetProductApiConfig(ctx context.Context, productID string) (*responses.ProductApiConfigResponse, error)
|
||||
|
||||
// 根据产品代码获取API配置
|
||||
GetProductApiConfigByCode(ctx context.Context, productCode string) (*responses.ProductApiConfigResponse, error)
|
||||
|
||||
// 批量获取产品API配置
|
||||
GetProductApiConfigsByProductIDs(ctx context.Context, productIDs []string) ([]*responses.ProductApiConfigResponse, error)
|
||||
|
||||
// 创建产品API配置
|
||||
CreateProductApiConfig(ctx context.Context, productID string, config *responses.ProductApiConfigResponse) error
|
||||
|
||||
// 更新产品API配置
|
||||
UpdateProductApiConfig(ctx context.Context, configID string, config *responses.ProductApiConfigResponse) error
|
||||
|
||||
// 删除产品API配置
|
||||
DeleteProductApiConfig(ctx context.Context, configID string) error
|
||||
}
|
||||
|
||||
// ProductApiConfigApplicationServiceImpl 产品API配置应用服务实现
|
||||
type ProductApiConfigApplicationServiceImpl struct {
|
||||
apiConfigService services.ProductApiConfigService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewProductApiConfigApplicationService 创建产品API配置应用服务
|
||||
func NewProductApiConfigApplicationService(
|
||||
apiConfigService services.ProductApiConfigService,
|
||||
logger *zap.Logger,
|
||||
) ProductApiConfigApplicationService {
|
||||
return &ProductApiConfigApplicationServiceImpl{
|
||||
apiConfigService: apiConfigService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// GetProductApiConfig 获取产品API配置
|
||||
func (s *ProductApiConfigApplicationServiceImpl) GetProductApiConfig(ctx context.Context, productID string) (*responses.ProductApiConfigResponse, error) {
|
||||
config, err := s.apiConfigService.GetApiConfigByProductID(ctx, productID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.convertToResponse(config), nil
|
||||
}
|
||||
|
||||
// GetProductApiConfigByCode 根据产品代码获取API配置
|
||||
func (s *ProductApiConfigApplicationServiceImpl) GetProductApiConfigByCode(ctx context.Context, productCode string) (*responses.ProductApiConfigResponse, error) {
|
||||
config, err := s.apiConfigService.GetApiConfigByProductCode(ctx, productCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.convertToResponse(config), nil
|
||||
}
|
||||
|
||||
// GetProductApiConfigsByProductIDs 批量获取产品API配置
|
||||
func (s *ProductApiConfigApplicationServiceImpl) GetProductApiConfigsByProductIDs(ctx context.Context, productIDs []string) ([]*responses.ProductApiConfigResponse, error) {
|
||||
configs, err := s.apiConfigService.GetApiConfigsByProductIDs(ctx, productIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var responses []*responses.ProductApiConfigResponse
|
||||
for _, config := range configs {
|
||||
responses = append(responses, s.convertToResponse(config))
|
||||
}
|
||||
|
||||
return responses, nil
|
||||
}
|
||||
|
||||
// CreateProductApiConfig 创建产品API配置
|
||||
func (s *ProductApiConfigApplicationServiceImpl) CreateProductApiConfig(ctx context.Context, productID string, configResponse *responses.ProductApiConfigResponse) error {
|
||||
// 检查是否已存在配置
|
||||
exists, err := s.apiConfigService.ExistsByProductID(ctx, productID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return errors.New("产品API配置已存在")
|
||||
}
|
||||
|
||||
// 转换为实体
|
||||
config := s.convertToEntity(configResponse)
|
||||
config.ProductID = productID
|
||||
|
||||
return s.apiConfigService.CreateApiConfig(ctx, config)
|
||||
}
|
||||
|
||||
// UpdateProductApiConfig 更新产品API配置
|
||||
func (s *ProductApiConfigApplicationServiceImpl) UpdateProductApiConfig(ctx context.Context, configID string, configResponse *responses.ProductApiConfigResponse) error {
|
||||
// 获取现有配置
|
||||
existingConfig, err := s.apiConfigService.GetApiConfigByProductID(ctx, configResponse.ProductID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新配置
|
||||
config := s.convertToEntity(configResponse)
|
||||
config.ID = configID
|
||||
config.ProductID = existingConfig.ProductID
|
||||
|
||||
return s.apiConfigService.UpdateApiConfig(ctx, config)
|
||||
}
|
||||
|
||||
// DeleteProductApiConfig 删除产品API配置
|
||||
func (s *ProductApiConfigApplicationServiceImpl) DeleteProductApiConfig(ctx context.Context, configID string) error {
|
||||
return s.apiConfigService.DeleteApiConfig(ctx, configID)
|
||||
}
|
||||
|
||||
// convertToResponse 转换为响应DTO
|
||||
func (s *ProductApiConfigApplicationServiceImpl) convertToResponse(config *entities.ProductApiConfig) *responses.ProductApiConfigResponse {
|
||||
requestParams, _ := config.GetRequestParams()
|
||||
responseFields, _ := config.GetResponseFields()
|
||||
responseExample, _ := config.GetResponseExample()
|
||||
|
||||
// 转换请求参数
|
||||
var requestParamResponses []responses.RequestParamResponse
|
||||
for _, param := range requestParams {
|
||||
requestParamResponses = append(requestParamResponses, responses.RequestParamResponse{
|
||||
Name: param.Name,
|
||||
Field: param.Field,
|
||||
Type: param.Type,
|
||||
Required: param.Required,
|
||||
Description: param.Description,
|
||||
Example: param.Example,
|
||||
Validation: param.Validation,
|
||||
})
|
||||
}
|
||||
|
||||
// 转换响应字段
|
||||
var responseFieldResponses []responses.ResponseFieldResponse
|
||||
for _, field := range responseFields {
|
||||
responseFieldResponses = append(responseFieldResponses, responses.ResponseFieldResponse{
|
||||
Name: field.Name,
|
||||
Path: field.Path,
|
||||
Type: field.Type,
|
||||
Description: field.Description,
|
||||
Required: field.Required,
|
||||
Example: field.Example,
|
||||
})
|
||||
}
|
||||
|
||||
return &responses.ProductApiConfigResponse{
|
||||
ID: config.ID,
|
||||
ProductID: config.ProductID,
|
||||
RequestParams: requestParamResponses,
|
||||
ResponseFields: responseFieldResponses,
|
||||
ResponseExample: responseExample,
|
||||
CreatedAt: config.CreatedAt,
|
||||
UpdatedAt: config.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// convertToEntity 转换为实体
|
||||
func (s *ProductApiConfigApplicationServiceImpl) convertToEntity(configResponse *responses.ProductApiConfigResponse) *entities.ProductApiConfig {
|
||||
// 转换请求参数
|
||||
var requestParams []entities.RequestParam
|
||||
for _, param := range configResponse.RequestParams {
|
||||
requestParams = append(requestParams, entities.RequestParam{
|
||||
Name: param.Name,
|
||||
Field: param.Field,
|
||||
Type: param.Type,
|
||||
Required: param.Required,
|
||||
Description: param.Description,
|
||||
Example: param.Example,
|
||||
Validation: param.Validation,
|
||||
})
|
||||
}
|
||||
|
||||
// 转换响应字段
|
||||
var responseFields []entities.ResponseField
|
||||
for _, field := range configResponse.ResponseFields {
|
||||
responseFields = append(responseFields, entities.ResponseField{
|
||||
Name: field.Name,
|
||||
Path: field.Path,
|
||||
Type: field.Type,
|
||||
Description: field.Description,
|
||||
Required: field.Required,
|
||||
Example: field.Example,
|
||||
})
|
||||
}
|
||||
|
||||
config := &entities.ProductApiConfig{}
|
||||
|
||||
// 设置JSON字段
|
||||
config.SetRequestParams(requestParams)
|
||||
config.SetResponseFields(responseFields)
|
||||
config.SetResponseExample(configResponse.ResponseExample)
|
||||
|
||||
return config
|
||||
}
|
||||
50
internal/application/product/product_application_service.go
Normal file
50
internal/application/product/product_application_service.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package product
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hyapi-server/internal/application/product/dto/commands"
|
||||
"hyapi-server/internal/application/product/dto/queries"
|
||||
"hyapi-server/internal/application/product/dto/responses"
|
||||
"hyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// ProductApplicationService 产品应用服务接口
|
||||
type ProductApplicationService interface {
|
||||
// 产品管理
|
||||
CreateProduct(ctx context.Context, cmd *commands.CreateProductCommand) (*responses.ProductAdminInfoResponse, error)
|
||||
|
||||
UpdateProduct(ctx context.Context, cmd *commands.UpdateProductCommand) error
|
||||
DeleteProduct(ctx context.Context, cmd *commands.DeleteProductCommand) error
|
||||
|
||||
GetProductByID(ctx context.Context, query *queries.GetProductQuery) (*responses.ProductInfoResponse, error)
|
||||
ListProducts(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.ProductListResponse, error)
|
||||
ListProductsWithSubscriptionStatus(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.ProductListResponse, error)
|
||||
GetProductsByIDs(ctx context.Context, query *queries.GetProductsByIDsQuery) ([]*responses.ProductInfoResponse, error)
|
||||
|
||||
// 管理员专用方法
|
||||
ListProductsForAdmin(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.ProductAdminListResponse, error)
|
||||
GetProductByIDForAdmin(ctx context.Context, query *queries.GetProductDetailQuery) (*responses.ProductAdminInfoResponse, error)
|
||||
|
||||
// 用户端专用方法
|
||||
GetProductByIDForUser(ctx context.Context, query *queries.GetProductDetailQuery) (*responses.ProductInfoWithDocumentResponse, error)
|
||||
|
||||
// 业务查询
|
||||
GetSubscribableProducts(ctx context.Context, query *queries.GetSubscribableProductsQuery) ([]*responses.ProductInfoResponse, error)
|
||||
GetProductStats(ctx context.Context) (*responses.ProductStatsResponse, error)
|
||||
|
||||
// 组合包管理
|
||||
AddPackageItem(ctx context.Context, packageID string, cmd *commands.AddPackageItemCommand) error
|
||||
UpdatePackageItem(ctx context.Context, packageID, itemID string, cmd *commands.UpdatePackageItemCommand) error
|
||||
RemovePackageItem(ctx context.Context, packageID, itemID string) error
|
||||
ReorderPackageItems(ctx context.Context, packageID string, cmd *commands.ReorderPackageItemsCommand) error
|
||||
UpdatePackageItems(ctx context.Context, packageID string, cmd *commands.UpdatePackageItemsCommand) error
|
||||
|
||||
// 可选子产品查询(管理员端,返回包含成本价的数据)
|
||||
GetAvailableProducts(ctx context.Context, query *queries.GetAvailableProductsQuery) (*responses.ProductAdminListResponse, error)
|
||||
|
||||
// API配置管理
|
||||
GetProductApiConfig(ctx context.Context, productID string) (*responses.ProductApiConfigResponse, error)
|
||||
CreateProductApiConfig(ctx context.Context, productID string, config *responses.ProductApiConfigResponse) error
|
||||
UpdateProductApiConfig(ctx context.Context, configID string, config *responses.ProductApiConfigResponse) error
|
||||
DeleteProductApiConfig(ctx context.Context, configID string) error
|
||||
}
|
||||
1242
internal/application/product/product_application_service_impl.go
Normal file
1242
internal/application/product/product_application_service_impl.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
||||
package product
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hyapi-server/internal/application/product/dto/commands"
|
||||
"hyapi-server/internal/application/product/dto/queries"
|
||||
"hyapi-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)
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
package product
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hyapi-server/internal/application/product/dto/commands"
|
||||
"hyapi-server/internal/application/product/dto/queries"
|
||||
"hyapi-server/internal/application/product/dto/responses"
|
||||
"hyapi-server/internal/domains/product/entities"
|
||||
"hyapi-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
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package product
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hyapi-server/internal/application/product/dto/commands"
|
||||
"hyapi-server/internal/application/product/dto/queries"
|
||||
"hyapi-server/internal/application/product/dto/responses"
|
||||
)
|
||||
|
||||
// SubscriptionApplicationService 订阅应用服务接口
|
||||
type SubscriptionApplicationService interface {
|
||||
// 订阅管理
|
||||
UpdateSubscriptionPrice(ctx context.Context, cmd *commands.UpdateSubscriptionPriceCommand) error
|
||||
|
||||
// 订阅管理
|
||||
CreateSubscription(ctx context.Context, cmd *commands.CreateSubscriptionCommand) error
|
||||
GetSubscriptionByID(ctx context.Context, query *queries.GetSubscriptionQuery) (*responses.SubscriptionInfoResponse, error)
|
||||
ListSubscriptions(ctx context.Context, query *queries.ListSubscriptionsQuery) (*responses.SubscriptionListResponse, error)
|
||||
|
||||
// 我的订阅(用户专用)
|
||||
ListMySubscriptions(ctx context.Context, userID string, query *queries.ListSubscriptionsQuery) (*responses.SubscriptionListResponse, error)
|
||||
GetMySubscriptionStats(ctx context.Context, userID string) (*responses.SubscriptionStatsResponse, error)
|
||||
CancelMySubscription(ctx context.Context, userID string, subscriptionID string) error
|
||||
|
||||
// 业务查询
|
||||
GetUserSubscriptions(ctx context.Context, query *queries.GetUserSubscriptionsQuery) ([]*responses.SubscriptionInfoResponse, error)
|
||||
GetProductSubscriptions(ctx context.Context, query *queries.GetProductSubscriptionsQuery) ([]*responses.SubscriptionInfoResponse, error)
|
||||
GetSubscriptionUsage(ctx context.Context, subscriptionID string) (*responses.SubscriptionUsageResponse, error)
|
||||
|
||||
// 统计
|
||||
GetSubscriptionStats(ctx context.Context) (*responses.SubscriptionStatsResponse, error)
|
||||
|
||||
// 一键改价
|
||||
BatchUpdateSubscriptionPrices(ctx context.Context, cmd *commands.BatchUpdateSubscriptionPricesCommand) error
|
||||
}
|
||||
@@ -0,0 +1,496 @@
|
||||
package product
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"hyapi-server/internal/application/product/dto/commands"
|
||||
appQueries "hyapi-server/internal/application/product/dto/queries"
|
||||
"hyapi-server/internal/application/product/dto/responses"
|
||||
domain_api_repo "hyapi-server/internal/domains/api/repositories"
|
||||
"hyapi-server/internal/domains/product/entities"
|
||||
repoQueries "hyapi-server/internal/domains/product/repositories/queries"
|
||||
product_service "hyapi-server/internal/domains/product/services"
|
||||
user_repositories "hyapi-server/internal/domains/user/repositories"
|
||||
)
|
||||
|
||||
// SubscriptionApplicationServiceImpl 订阅应用服务实现
|
||||
// 负责业务流程编排、事务管理、数据转换,不直接操作仓库
|
||||
type SubscriptionApplicationServiceImpl struct {
|
||||
productSubscriptionService *product_service.ProductSubscriptionService
|
||||
userRepo user_repositories.UserRepository
|
||||
apiCallRepository domain_api_repo.ApiCallRepository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewSubscriptionApplicationService 创建订阅应用服务
|
||||
func NewSubscriptionApplicationService(
|
||||
productSubscriptionService *product_service.ProductSubscriptionService,
|
||||
userRepo user_repositories.UserRepository,
|
||||
apiCallRepository domain_api_repo.ApiCallRepository,
|
||||
logger *zap.Logger,
|
||||
) SubscriptionApplicationService {
|
||||
return &SubscriptionApplicationServiceImpl{
|
||||
productSubscriptionService: productSubscriptionService,
|
||||
userRepo: userRepo,
|
||||
apiCallRepository: apiCallRepository,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateSubscriptionPrice 更新订阅价格
|
||||
// 业务流程:1. 获取订阅 2. 更新价格 3. 保存订阅
|
||||
func (s *SubscriptionApplicationServiceImpl) UpdateSubscriptionPrice(ctx context.Context, cmd *commands.UpdateSubscriptionPriceCommand) error {
|
||||
return s.productSubscriptionService.UpdateSubscriptionPriceWithUIComponent(ctx, cmd.ID, cmd.Price, cmd.UIComponentPrice)
|
||||
}
|
||||
|
||||
// BatchUpdateSubscriptionPrices 一键改价
|
||||
// 业务流程:1. 获取用户所有订阅 2. 根据范围筛选 3. 批量更新价格
|
||||
func (s *SubscriptionApplicationServiceImpl) BatchUpdateSubscriptionPrices(ctx context.Context, cmd *commands.BatchUpdateSubscriptionPricesCommand) error {
|
||||
// 记录请求参数
|
||||
s.logger.Info("开始批量更新订阅价格",
|
||||
zap.String("user_id", cmd.UserID),
|
||||
zap.String("adjustment_type", cmd.AdjustmentType),
|
||||
zap.Float64("discount", cmd.Discount),
|
||||
zap.Float64("cost_multiple", cmd.CostMultiple),
|
||||
zap.String("scope", cmd.Scope))
|
||||
|
||||
// 验证调整方式对应的参数
|
||||
if cmd.AdjustmentType == "discount" && cmd.Discount <= 0 {
|
||||
return fmt.Errorf("按售价折扣调整时,折扣比例必须大于0")
|
||||
}
|
||||
if cmd.AdjustmentType == "cost_multiple" && cmd.CostMultiple <= 0 {
|
||||
return fmt.Errorf("按成本价倍数调整时,倍数必须大于0")
|
||||
}
|
||||
|
||||
subscriptions, _, err := s.productSubscriptionService.ListSubscriptions(ctx, &repoQueries.ListSubscriptionsQuery{
|
||||
UserID: cmd.UserID,
|
||||
Page: 1,
|
||||
PageSize: 1000,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("获取到订阅列表",
|
||||
zap.Int("total_subscriptions", len(subscriptions)))
|
||||
|
||||
// 根据范围筛选订阅
|
||||
var targetSubscriptions []*entities.Subscription
|
||||
for _, sub := range subscriptions {
|
||||
if cmd.Scope == "all" {
|
||||
// 所有订阅都修改
|
||||
targetSubscriptions = append(targetSubscriptions, sub)
|
||||
} else if cmd.Scope == "undiscounted" {
|
||||
// 只修改未打折的订阅(价格等于产品原价)
|
||||
if sub.Product != nil && sub.Price.Equal(sub.Product.Price) {
|
||||
targetSubscriptions = append(targetSubscriptions, sub)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 批量更新价格
|
||||
updatedCount := 0
|
||||
skippedCount := 0
|
||||
for _, sub := range targetSubscriptions {
|
||||
if sub.Product == nil {
|
||||
skippedCount++
|
||||
continue
|
||||
}
|
||||
|
||||
var newPrice decimal.Decimal
|
||||
|
||||
if cmd.AdjustmentType == "discount" {
|
||||
// 按售价折扣调整
|
||||
discountRatio := cmd.Discount / 10
|
||||
newPrice = sub.Product.Price.Mul(decimal.NewFromFloat(discountRatio))
|
||||
} else if cmd.AdjustmentType == "cost_multiple" {
|
||||
// 按成本价倍数调整
|
||||
// 检查成本价是否有效(必须大于0)
|
||||
// 使用严格检查:成本价必须大于0
|
||||
if !sub.Product.CostPrice.GreaterThan(decimal.Zero) {
|
||||
// 跳过没有成本价或成本价为0的产品
|
||||
skippedCount++
|
||||
s.logger.Info("跳过未设置成本价或成本价为0的订阅",
|
||||
zap.String("subscription_id", sub.ID),
|
||||
zap.String("product_id", sub.ProductID),
|
||||
zap.String("product_name", sub.Product.Name),
|
||||
zap.String("cost_price", sub.Product.CostPrice.String()))
|
||||
continue
|
||||
}
|
||||
// 计算成本价倍数后的价格
|
||||
newPrice = sub.Product.CostPrice.Mul(decimal.NewFromFloat(cmd.CostMultiple))
|
||||
} else {
|
||||
s.logger.Warn("未知的调整方式",
|
||||
zap.String("adjustment_type", cmd.AdjustmentType),
|
||||
zap.String("subscription_id", sub.ID))
|
||||
skippedCount++
|
||||
continue
|
||||
}
|
||||
|
||||
// 四舍五入到2位小数
|
||||
newPrice = newPrice.Round(2)
|
||||
|
||||
err := s.productSubscriptionService.UpdateSubscriptionPrice(ctx, sub.ID, newPrice.InexactFloat64())
|
||||
if err != nil {
|
||||
s.logger.Error("批量更新订阅价格失败",
|
||||
zap.String("subscription_id", sub.ID),
|
||||
zap.Error(err))
|
||||
skippedCount++
|
||||
// 继续处理其他订阅,不中断整个流程
|
||||
} else {
|
||||
updatedCount++
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("批量更新订阅价格完成",
|
||||
zap.Int("total", len(targetSubscriptions)),
|
||||
zap.Int("updated", updatedCount),
|
||||
zap.Int("skipped", skippedCount))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateSubscription 创建订阅
|
||||
// 业务流程:1. 创建订阅
|
||||
func (s *SubscriptionApplicationServiceImpl) CreateSubscription(ctx context.Context, cmd *commands.CreateSubscriptionCommand) error {
|
||||
_, err := s.productSubscriptionService.CreateSubscription(ctx, cmd.UserID, cmd.ProductID)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetSubscriptionByID 根据ID获取订阅
|
||||
// 业务流程:1. 获取订阅信息 2. 构建响应数据
|
||||
func (s *SubscriptionApplicationServiceImpl) GetSubscriptionByID(ctx context.Context, query *appQueries.GetSubscriptionQuery) (*responses.SubscriptionInfoResponse, error) {
|
||||
subscription, err := s.productSubscriptionService.GetSubscriptionByID(ctx, query.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.convertToSubscriptionInfoResponse(subscription), nil
|
||||
}
|
||||
|
||||
// ListSubscriptions 获取订阅列表(管理员用)
|
||||
// 业务流程:1. 获取订阅列表 2. 构建响应数据
|
||||
func (s *SubscriptionApplicationServiceImpl) ListSubscriptions(ctx context.Context, query *appQueries.ListSubscriptionsQuery) (*responses.SubscriptionListResponse, error) {
|
||||
repoQuery := &repoQueries.ListSubscriptionsQuery{
|
||||
Page: query.Page,
|
||||
PageSize: query.PageSize,
|
||||
UserID: query.UserID, // 管理员可以按用户筛选
|
||||
Keyword: query.Keyword,
|
||||
SortBy: query.SortBy,
|
||||
SortOrder: query.SortOrder,
|
||||
CompanyName: query.CompanyName,
|
||||
ProductName: query.ProductName,
|
||||
StartTime: query.StartTime,
|
||||
EndTime: query.EndTime,
|
||||
}
|
||||
subscriptions, total, err := s.productSubscriptionService.ListSubscriptions(ctx, repoQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]responses.SubscriptionInfoResponse, len(subscriptions))
|
||||
for i := range subscriptions {
|
||||
resp := s.convertToSubscriptionInfoResponseForAdmin(subscriptions[i])
|
||||
if resp != nil {
|
||||
items[i] = *resp // 解引用指针
|
||||
}
|
||||
}
|
||||
return &responses.SubscriptionListResponse{
|
||||
Total: total,
|
||||
Page: query.Page,
|
||||
Size: query.PageSize,
|
||||
Items: items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListMySubscriptions 获取我的订阅列表(用户用)
|
||||
// 业务流程:1. 获取用户订阅列表 2. 构建响应数据
|
||||
func (s *SubscriptionApplicationServiceImpl) ListMySubscriptions(ctx context.Context, userID string, query *appQueries.ListSubscriptionsQuery) (*responses.SubscriptionListResponse, error) {
|
||||
repoQuery := &repoQueries.ListSubscriptionsQuery{
|
||||
Page: query.Page,
|
||||
PageSize: query.PageSize,
|
||||
UserID: userID, // 强制设置为当前用户ID
|
||||
Keyword: query.Keyword,
|
||||
SortBy: query.SortBy,
|
||||
SortOrder: query.SortOrder,
|
||||
CompanyName: query.CompanyName,
|
||||
ProductName: query.ProductName,
|
||||
StartTime: query.StartTime,
|
||||
EndTime: query.EndTime,
|
||||
}
|
||||
subscriptions, total, err := s.productSubscriptionService.ListSubscriptions(ctx, repoQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]responses.SubscriptionInfoResponse, len(subscriptions))
|
||||
for i := range subscriptions {
|
||||
resp := s.convertToSubscriptionInfoResponse(subscriptions[i])
|
||||
if resp != nil {
|
||||
items[i] = *resp // 解引用指针
|
||||
}
|
||||
}
|
||||
return &responses.SubscriptionListResponse{
|
||||
Total: total,
|
||||
Page: query.Page,
|
||||
Size: query.PageSize,
|
||||
Items: items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetUserSubscriptions 获取用户订阅
|
||||
// 业务流程:1. 获取用户订阅 2. 构建响应数据
|
||||
func (s *SubscriptionApplicationServiceImpl) GetUserSubscriptions(ctx context.Context, query *appQueries.GetUserSubscriptionsQuery) ([]*responses.SubscriptionInfoResponse, error) {
|
||||
subscriptions, err := s.productSubscriptionService.GetUserSubscriptions(ctx, query.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为响应对象
|
||||
items := make([]*responses.SubscriptionInfoResponse, len(subscriptions))
|
||||
for i := range subscriptions {
|
||||
items[i] = s.convertToSubscriptionInfoResponse(subscriptions[i])
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// GetProductSubscriptions 获取产品订阅
|
||||
// 业务流程:1. 获取产品订阅 2. 构建响应数据
|
||||
func (s *SubscriptionApplicationServiceImpl) GetProductSubscriptions(ctx context.Context, query *appQueries.GetProductSubscriptionsQuery) ([]*responses.SubscriptionInfoResponse, error) {
|
||||
// 这里需要扩展领域服务来支持按产品查询订阅
|
||||
// 暂时返回空列表
|
||||
return []*responses.SubscriptionInfoResponse{}, nil
|
||||
}
|
||||
|
||||
// GetSubscriptionUsage 获取订阅使用情况
|
||||
// 业务流程:1. 获取订阅信息 2. 根据产品ID和用户ID统计API调用次数 3. 构建响应数据
|
||||
func (s *SubscriptionApplicationServiceImpl) GetSubscriptionUsage(ctx context.Context, subscriptionID string) (*responses.SubscriptionUsageResponse, error) {
|
||||
// 获取订阅信息
|
||||
subscription, err := s.productSubscriptionService.GetSubscriptionByID(ctx, subscriptionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 根据用户ID和产品ID统计API调用次数
|
||||
apiCallCount, err := s.apiCallRepository.CountByUserIdAndProductId(ctx, subscription.UserID, subscription.ProductID)
|
||||
if err != nil {
|
||||
s.logger.Warn("统计API调用次数失败,使用订阅记录中的值",
|
||||
zap.String("subscription_id", subscriptionID),
|
||||
zap.String("user_id", subscription.UserID),
|
||||
zap.String("product_id", subscription.ProductID),
|
||||
zap.Error(err))
|
||||
// 如果统计失败,使用订阅实体中的APIUsed字段作为备选
|
||||
apiCallCount = subscription.APIUsed
|
||||
}
|
||||
|
||||
return &responses.SubscriptionUsageResponse{
|
||||
ID: subscription.ID,
|
||||
ProductID: subscription.ProductID,
|
||||
APIUsed: apiCallCount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetSubscriptionStats 获取订阅统计信息
|
||||
// 业务流程:1. 获取订阅统计 2. 构建响应数据
|
||||
func (s *SubscriptionApplicationServiceImpl) GetSubscriptionStats(ctx context.Context) (*responses.SubscriptionStatsResponse, error) {
|
||||
stats, err := s.productSubscriptionService.GetSubscriptionStats(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &responses.SubscriptionStatsResponse{
|
||||
TotalSubscriptions: stats["total_subscriptions"].(int64),
|
||||
TotalRevenue: stats["total_revenue"].(float64),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetMySubscriptionStats 获取我的订阅统计信息
|
||||
// 业务流程:1. 获取用户订阅统计 2. 构建响应数据
|
||||
func (s *SubscriptionApplicationServiceImpl) GetMySubscriptionStats(ctx context.Context, userID string) (*responses.SubscriptionStatsResponse, error) {
|
||||
stats, err := s.productSubscriptionService.GetUserSubscriptionStats(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &responses.SubscriptionStatsResponse{
|
||||
TotalSubscriptions: stats["total_subscriptions"].(int64),
|
||||
TotalRevenue: stats["total_revenue"].(float64),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CancelMySubscription 取消我的订阅
|
||||
// 业务流程:1. 验证订阅是否属于当前用户 2. 取消订阅
|
||||
func (s *SubscriptionApplicationServiceImpl) CancelMySubscription(ctx context.Context, userID string, subscriptionID string) error {
|
||||
// 1. 获取订阅信息
|
||||
subscription, err := s.productSubscriptionService.GetSubscriptionByID(ctx, subscriptionID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取订阅信息失败", zap.String("subscription_id", subscriptionID), zap.Error(err))
|
||||
return fmt.Errorf("订阅不存在")
|
||||
}
|
||||
|
||||
// 2. 验证订阅是否属于当前用户
|
||||
if subscription.UserID != userID {
|
||||
s.logger.Warn("用户尝试取消不属于自己的订阅",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("subscription_id", subscriptionID),
|
||||
zap.String("subscription_user_id", subscription.UserID))
|
||||
return fmt.Errorf("无权取消此订阅")
|
||||
}
|
||||
|
||||
// 3. 取消订阅(软删除)
|
||||
if err := s.productSubscriptionService.CancelSubscription(ctx, subscriptionID); err != nil {
|
||||
s.logger.Error("取消订阅失败", zap.String("subscription_id", subscriptionID), zap.Error(err))
|
||||
return fmt.Errorf("取消订阅失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("用户取消订阅成功",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("subscription_id", subscriptionID))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// convertToSubscriptionInfoResponse 转换为订阅信息响应
|
||||
func (s *SubscriptionApplicationServiceImpl) convertToSubscriptionInfoResponse(subscription *entities.Subscription) *responses.SubscriptionInfoResponse {
|
||||
// 查询用户信息
|
||||
var userInfo *responses.UserSimpleResponse
|
||||
if subscription.UserID != "" {
|
||||
user, err := s.userRepo.GetByIDWithEnterpriseInfo(context.Background(), subscription.UserID)
|
||||
if err == nil {
|
||||
companyName := "未知公司"
|
||||
if user.EnterpriseInfo != nil {
|
||||
companyName = user.EnterpriseInfo.CompanyName
|
||||
}
|
||||
userInfo = &responses.UserSimpleResponse{
|
||||
ID: user.ID,
|
||||
CompanyName: companyName,
|
||||
Phone: user.Phone,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var productResponse *responses.ProductSimpleResponse
|
||||
if subscription.Product != nil {
|
||||
productResponse = s.convertToProductSimpleResponse(subscription.Product)
|
||||
}
|
||||
|
||||
// 获取UI组件价格,如果订阅中没有设置,则从产品中获取
|
||||
uiComponentPrice := subscription.UIComponentPrice.InexactFloat64()
|
||||
if uiComponentPrice == 0 && subscription.Product != nil && (subscription.Product.IsPackage) {
|
||||
uiComponentPrice = subscription.Product.UIComponentPrice.InexactFloat64()
|
||||
}
|
||||
|
||||
return &responses.SubscriptionInfoResponse{
|
||||
ID: subscription.ID,
|
||||
UserID: subscription.UserID,
|
||||
ProductID: subscription.ProductID,
|
||||
Price: subscription.Price.InexactFloat64(),
|
||||
UIComponentPrice: uiComponentPrice,
|
||||
User: userInfo,
|
||||
Product: productResponse,
|
||||
APIUsed: subscription.APIUsed,
|
||||
CreatedAt: subscription.CreatedAt,
|
||||
UpdatedAt: subscription.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// convertToProductSimpleResponse 转换为产品简单信息响应
|
||||
func (s *SubscriptionApplicationServiceImpl) convertToProductSimpleResponse(product *entities.Product) *responses.ProductSimpleResponse {
|
||||
var categoryResponse *responses.CategorySimpleResponse
|
||||
if product.Category != nil {
|
||||
categoryResponse = s.convertToCategorySimpleResponse(product.Category)
|
||||
}
|
||||
|
||||
return &responses.ProductSimpleResponse{
|
||||
ID: product.ID,
|
||||
OldID: product.OldID,
|
||||
Name: product.Name,
|
||||
Code: product.Code,
|
||||
Description: product.Description,
|
||||
Price: product.Price.InexactFloat64(),
|
||||
Category: categoryResponse,
|
||||
IsPackage: product.IsPackage,
|
||||
}
|
||||
}
|
||||
|
||||
// convertToSubscriptionInfoResponseForAdmin 转换为订阅信息响应(管理员端,包含成本价)
|
||||
func (s *SubscriptionApplicationServiceImpl) convertToSubscriptionInfoResponseForAdmin(subscription *entities.Subscription) *responses.SubscriptionInfoResponse {
|
||||
// 查询用户信息
|
||||
var userInfo *responses.UserSimpleResponse
|
||||
if subscription.UserID != "" {
|
||||
user, err := s.userRepo.GetByIDWithEnterpriseInfo(context.Background(), subscription.UserID)
|
||||
if err == nil {
|
||||
companyName := "未知公司"
|
||||
if user.EnterpriseInfo != nil {
|
||||
companyName = user.EnterpriseInfo.CompanyName
|
||||
}
|
||||
userInfo = &responses.UserSimpleResponse{
|
||||
ID: user.ID,
|
||||
CompanyName: companyName,
|
||||
Phone: user.Phone,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var productAdminResponse *responses.ProductSimpleAdminResponse
|
||||
if subscription.Product != nil {
|
||||
productAdminResponse = s.convertToProductSimpleAdminResponse(subscription.Product)
|
||||
}
|
||||
|
||||
// 获取UI组件价格,如果订阅中没有设置,则从产品中获取
|
||||
uiComponentPrice := subscription.UIComponentPrice.InexactFloat64()
|
||||
if uiComponentPrice == 0 && subscription.Product != nil && (subscription.Product.IsPackage) {
|
||||
uiComponentPrice = subscription.Product.UIComponentPrice.InexactFloat64()
|
||||
}
|
||||
|
||||
return &responses.SubscriptionInfoResponse{
|
||||
ID: subscription.ID,
|
||||
UserID: subscription.UserID,
|
||||
ProductID: subscription.ProductID,
|
||||
Price: subscription.Price.InexactFloat64(),
|
||||
UIComponentPrice: uiComponentPrice,
|
||||
User: userInfo,
|
||||
ProductAdmin: productAdminResponse,
|
||||
APIUsed: subscription.APIUsed,
|
||||
CreatedAt: subscription.CreatedAt,
|
||||
UpdatedAt: subscription.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// convertToProductSimpleAdminResponse 转换为管理员产品简单信息响应(包含成本价)
|
||||
func (s *SubscriptionApplicationServiceImpl) convertToProductSimpleAdminResponse(product *entities.Product) *responses.ProductSimpleAdminResponse {
|
||||
var categoryResponse *responses.CategorySimpleResponse
|
||||
if product.Category != nil {
|
||||
categoryResponse = s.convertToCategorySimpleResponse(product.Category)
|
||||
}
|
||||
|
||||
return &responses.ProductSimpleAdminResponse{
|
||||
ProductSimpleResponse: responses.ProductSimpleResponse{
|
||||
ID: product.ID,
|
||||
OldID: product.OldID,
|
||||
Name: product.Name,
|
||||
Code: product.Code,
|
||||
Description: product.Description,
|
||||
Price: product.Price.InexactFloat64(),
|
||||
Category: categoryResponse,
|
||||
IsPackage: product.IsPackage,
|
||||
},
|
||||
CostPrice: product.CostPrice.InexactFloat64(),
|
||||
UIComponentPrice: product.UIComponentPrice.InexactFloat64(),
|
||||
}
|
||||
}
|
||||
|
||||
// convertToCategorySimpleResponse 转换为分类简单信息响应
|
||||
func (s *SubscriptionApplicationServiceImpl) convertToCategorySimpleResponse(category *entities.ProductCategory) *responses.CategorySimpleResponse {
|
||||
if category == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &responses.CategorySimpleResponse{
|
||||
ID: category.ID,
|
||||
Name: category.Name,
|
||||
}
|
||||
}
|
||||
743
internal/application/product/ui_component_application_service.go
Normal file
743
internal/application/product/ui_component_application_service.go
Normal file
@@ -0,0 +1,743 @@
|
||||
package product
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hyapi-server/internal/domains/product/entities"
|
||||
"hyapi-server/internal/domains/product/repositories"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// UIComponentApplicationService UI组件应用服务接口
|
||||
type UIComponentApplicationService interface {
|
||||
// 基本CRUD操作
|
||||
CreateUIComponent(ctx context.Context, req CreateUIComponentRequest) (entities.UIComponent, error)
|
||||
CreateUIComponentWithFile(ctx context.Context, req CreateUIComponentRequest, file *multipart.FileHeader) (entities.UIComponent, error)
|
||||
CreateUIComponentWithFiles(ctx context.Context, req CreateUIComponentRequest, files []*multipart.FileHeader) (entities.UIComponent, error)
|
||||
CreateUIComponentWithFilesAndPaths(ctx context.Context, req CreateUIComponentRequest, files []*multipart.FileHeader, paths []string) (entities.UIComponent, error)
|
||||
GetUIComponentByID(ctx context.Context, id string) (*entities.UIComponent, error)
|
||||
GetUIComponentByCode(ctx context.Context, code string) (*entities.UIComponent, error)
|
||||
UpdateUIComponent(ctx context.Context, req UpdateUIComponentRequest) error
|
||||
DeleteUIComponent(ctx context.Context, id string) error
|
||||
ListUIComponents(ctx context.Context, req ListUIComponentsRequest) (ListUIComponentsResponse, error)
|
||||
|
||||
// 文件操作
|
||||
UploadUIComponentFile(ctx context.Context, id string, file *multipart.FileHeader) (string, error)
|
||||
UploadAndExtractUIComponentFile(ctx context.Context, id string, file *multipart.FileHeader) error
|
||||
DownloadUIComponentFile(ctx context.Context, id string) (string, error)
|
||||
GetUIComponentFolderContent(ctx context.Context, id string) ([]FileInfo, error)
|
||||
DeleteUIComponentFolder(ctx context.Context, id string) error
|
||||
|
||||
// 产品关联操作
|
||||
AssociateUIComponentToProduct(ctx context.Context, req AssociateUIComponentRequest) error
|
||||
GetProductUIComponents(ctx context.Context, productID string) ([]entities.ProductUIComponent, error)
|
||||
RemoveUIComponentFromProduct(ctx context.Context, productID, componentID string) error
|
||||
}
|
||||
|
||||
// CreateUIComponentRequest 创建UI组件请求
|
||||
type CreateUIComponentRequest struct {
|
||||
ComponentCode string `json:"component_code" binding:"required"`
|
||||
ComponentName string `json:"component_name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
Version string `json:"version"`
|
||||
IsActive bool `json:"is_active"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
}
|
||||
|
||||
// UpdateUIComponentRequest 更新UI组件请求
|
||||
type UpdateUIComponentRequest struct {
|
||||
ID string `json:"id" binding:"required"`
|
||||
ComponentCode string `json:"component_code"`
|
||||
ComponentName string `json:"component_name"`
|
||||
Description string `json:"description"`
|
||||
Version string `json:"version"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
SortOrder *int `json:"sort_order"`
|
||||
}
|
||||
|
||||
// ListUIComponentsRequest 获取UI组件列表请求
|
||||
type ListUIComponentsRequest struct {
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"page_size,default=10"`
|
||||
Keyword string `form:"keyword"`
|
||||
IsActive *bool `form:"is_active"`
|
||||
SortBy string `form:"sort_by,default=sort_order"`
|
||||
SortOrder string `form:"sort_order,default=asc"`
|
||||
}
|
||||
|
||||
// ListUIComponentsResponse 获取UI组件列表响应
|
||||
type ListUIComponentsResponse struct {
|
||||
Components []entities.UIComponent `json:"components"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// AssociateUIComponentRequest 关联UI组件到产品请求
|
||||
type AssociateUIComponentRequest struct {
|
||||
ProductID string `json:"product_id" binding:"required"`
|
||||
UIComponentID string `json:"ui_component_id" binding:"required"`
|
||||
Price float64 `json:"price" binding:"required,min=0"`
|
||||
IsEnabled bool `json:"is_enabled"`
|
||||
}
|
||||
|
||||
// UIComponentApplicationServiceImpl UI组件应用服务实现
|
||||
type UIComponentApplicationServiceImpl struct {
|
||||
uiComponentRepo repositories.UIComponentRepository
|
||||
productUIComponentRepo repositories.ProductUIComponentRepository
|
||||
fileStorageService FileStorageService
|
||||
fileService UIComponentFileService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// FileStorageService 文件存储服务接口
|
||||
type FileStorageService interface {
|
||||
StoreFile(ctx context.Context, file io.Reader, filename string) (string, error)
|
||||
GetFileURL(ctx context.Context, filePath string) (string, error)
|
||||
DeleteFile(ctx context.Context, filePath string) error
|
||||
}
|
||||
|
||||
// NewUIComponentApplicationService 创建UI组件应用服务
|
||||
func NewUIComponentApplicationService(
|
||||
uiComponentRepo repositories.UIComponentRepository,
|
||||
productUIComponentRepo repositories.ProductUIComponentRepository,
|
||||
fileStorageService FileStorageService,
|
||||
fileService UIComponentFileService,
|
||||
logger *zap.Logger,
|
||||
) UIComponentApplicationService {
|
||||
return &UIComponentApplicationServiceImpl{
|
||||
uiComponentRepo: uiComponentRepo,
|
||||
productUIComponentRepo: productUIComponentRepo,
|
||||
fileStorageService: fileStorageService,
|
||||
fileService: fileService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateUIComponent 创建UI组件
|
||||
func (s *UIComponentApplicationServiceImpl) CreateUIComponent(ctx context.Context, req CreateUIComponentRequest) (entities.UIComponent, error) {
|
||||
// 检查编码是否已存在
|
||||
existing, _ := s.uiComponentRepo.GetByCode(ctx, req.ComponentCode)
|
||||
if existing != nil {
|
||||
return entities.UIComponent{}, ErrComponentCodeAlreadyExists
|
||||
}
|
||||
|
||||
component := entities.UIComponent{
|
||||
ComponentCode: req.ComponentCode,
|
||||
ComponentName: req.ComponentName,
|
||||
Description: req.Description,
|
||||
Version: req.Version,
|
||||
IsActive: req.IsActive,
|
||||
SortOrder: req.SortOrder,
|
||||
}
|
||||
|
||||
return s.uiComponentRepo.Create(ctx, component)
|
||||
}
|
||||
|
||||
// CreateUIComponentWithFile 创建UI组件并上传文件
|
||||
func (s *UIComponentApplicationServiceImpl) CreateUIComponentWithFile(ctx context.Context, req CreateUIComponentRequest, file *multipart.FileHeader) (entities.UIComponent, error) {
|
||||
// 检查编码是否已存在
|
||||
existing, _ := s.uiComponentRepo.GetByCode(ctx, req.ComponentCode)
|
||||
if existing != nil {
|
||||
return entities.UIComponent{}, ErrComponentCodeAlreadyExists
|
||||
}
|
||||
|
||||
// 创建组件
|
||||
component := entities.UIComponent{
|
||||
ComponentCode: req.ComponentCode,
|
||||
ComponentName: req.ComponentName,
|
||||
Description: req.Description,
|
||||
Version: req.Version,
|
||||
IsActive: req.IsActive,
|
||||
SortOrder: req.SortOrder,
|
||||
}
|
||||
|
||||
createdComponent, err := s.uiComponentRepo.Create(ctx, component)
|
||||
if err != nil {
|
||||
return entities.UIComponent{}, err
|
||||
}
|
||||
|
||||
// 如果有文件,则上传并处理文件
|
||||
if file != nil {
|
||||
// 打开上传的文件
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
// 删除已创建的组件记录
|
||||
_ = s.uiComponentRepo.Delete(ctx, createdComponent.ID)
|
||||
return entities.UIComponent{}, fmt.Errorf("打开上传文件失败: %w", err)
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// 上传并解压文件
|
||||
if err := s.fileService.UploadAndExtract(ctx, createdComponent.ID, createdComponent.ComponentCode, src, file.Filename); err != nil {
|
||||
// 删除已创建的组件记录
|
||||
_ = s.uiComponentRepo.Delete(ctx, createdComponent.ID)
|
||||
return entities.UIComponent{}, err
|
||||
}
|
||||
|
||||
// 获取文件类型
|
||||
fileType := strings.ToLower(filepath.Ext(file.Filename))
|
||||
|
||||
// 更新组件信息
|
||||
folderPath := "resources/Pure_Component/src/ui"
|
||||
createdComponent.FolderPath = &folderPath
|
||||
createdComponent.FileType = &fileType
|
||||
|
||||
// 记录文件上传时间
|
||||
now := time.Now()
|
||||
createdComponent.FileUploadTime = &now
|
||||
|
||||
// 仅对ZIP文件设置已解压标记
|
||||
if fileType == ".zip" {
|
||||
createdComponent.IsExtracted = true
|
||||
}
|
||||
|
||||
// 更新组件信息
|
||||
err = s.uiComponentRepo.Update(ctx, createdComponent)
|
||||
if err != nil {
|
||||
// 尝试删除已创建的组件记录
|
||||
_ = s.uiComponentRepo.Delete(ctx, createdComponent.ID)
|
||||
return entities.UIComponent{}, err
|
||||
}
|
||||
|
||||
return createdComponent, nil
|
||||
}
|
||||
|
||||
return createdComponent, nil
|
||||
}
|
||||
|
||||
// CreateUIComponentWithFiles 创建UI组件并上传多个文件
|
||||
func (s *UIComponentApplicationServiceImpl) CreateUIComponentWithFiles(ctx context.Context, req CreateUIComponentRequest, files []*multipart.FileHeader) (entities.UIComponent, error) {
|
||||
// 检查编码是否已存在
|
||||
existing, _ := s.uiComponentRepo.GetByCode(ctx, req.ComponentCode)
|
||||
if existing != nil {
|
||||
return entities.UIComponent{}, ErrComponentCodeAlreadyExists
|
||||
}
|
||||
|
||||
// 创建组件
|
||||
component := entities.UIComponent{
|
||||
ComponentCode: req.ComponentCode,
|
||||
ComponentName: req.ComponentName,
|
||||
Description: req.Description,
|
||||
Version: req.Version,
|
||||
IsActive: req.IsActive,
|
||||
SortOrder: req.SortOrder,
|
||||
}
|
||||
|
||||
createdComponent, err := s.uiComponentRepo.Create(ctx, component)
|
||||
if err != nil {
|
||||
return entities.UIComponent{}, err
|
||||
}
|
||||
|
||||
// 如果有文件,则上传并处理文件
|
||||
if len(files) > 0 {
|
||||
// 处理每个文件
|
||||
var extractedFiles []string
|
||||
for _, fileHeader := range files {
|
||||
// 打开上传的文件
|
||||
src, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
// 删除已创建的组件记录
|
||||
_ = s.uiComponentRepo.Delete(ctx, createdComponent.ID)
|
||||
return entities.UIComponent{}, fmt.Errorf("打开上传文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 上传并解压文件
|
||||
if err := s.fileService.UploadAndExtract(ctx, createdComponent.ID, createdComponent.ComponentCode, src, fileHeader.Filename); err != nil {
|
||||
src.Close()
|
||||
// 删除已创建的组件记录
|
||||
_ = s.uiComponentRepo.Delete(ctx, createdComponent.ID)
|
||||
return entities.UIComponent{}, err
|
||||
}
|
||||
src.Close()
|
||||
|
||||
// 记录已处理的文件,用于日志
|
||||
extractedFiles = append(extractedFiles, fileHeader.Filename)
|
||||
}
|
||||
|
||||
// 更新组件信息
|
||||
folderPath := "resources/Pure_Component/src/ui"
|
||||
createdComponent.FolderPath = &folderPath
|
||||
|
||||
// 记录文件上传时间
|
||||
now := time.Now()
|
||||
createdComponent.FileUploadTime = &now
|
||||
|
||||
// 检查是否有ZIP文件
|
||||
hasZipFile := false
|
||||
for _, fileHeader := range files {
|
||||
if strings.HasSuffix(strings.ToLower(fileHeader.Filename), ".zip") {
|
||||
hasZipFile = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有ZIP文件,则标记为已解压
|
||||
if hasZipFile {
|
||||
createdComponent.IsExtracted = true
|
||||
}
|
||||
|
||||
// 更新组件信息
|
||||
err = s.uiComponentRepo.Update(ctx, createdComponent)
|
||||
if err != nil {
|
||||
// 尝试删除已创建的组件记录
|
||||
_ = s.uiComponentRepo.Delete(ctx, createdComponent.ID)
|
||||
return entities.UIComponent{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return createdComponent, nil
|
||||
}
|
||||
|
||||
// CreateUIComponentWithFilesAndPaths 创建UI组件并上传带路径的文件
|
||||
func (s *UIComponentApplicationServiceImpl) CreateUIComponentWithFilesAndPaths(ctx context.Context, req CreateUIComponentRequest, files []*multipart.FileHeader, paths []string) (entities.UIComponent, error) {
|
||||
// 检查编码是否已存在
|
||||
existing, _ := s.uiComponentRepo.GetByCode(ctx, req.ComponentCode)
|
||||
if existing != nil {
|
||||
return entities.UIComponent{}, ErrComponentCodeAlreadyExists
|
||||
}
|
||||
|
||||
// 创建组件
|
||||
component := entities.UIComponent{
|
||||
ComponentCode: req.ComponentCode,
|
||||
ComponentName: req.ComponentName,
|
||||
Description: req.Description,
|
||||
Version: req.Version,
|
||||
IsActive: req.IsActive,
|
||||
SortOrder: req.SortOrder,
|
||||
}
|
||||
|
||||
createdComponent, err := s.uiComponentRepo.Create(ctx, component)
|
||||
if err != nil {
|
||||
return entities.UIComponent{}, err
|
||||
}
|
||||
|
||||
// 如果有文件,则上传并处理文件
|
||||
if len(files) > 0 {
|
||||
// 打开所有文件
|
||||
var readers []io.Reader
|
||||
var filenames []string
|
||||
var filePaths []string
|
||||
|
||||
for i, fileHeader := range files {
|
||||
// 打开上传的文件
|
||||
src, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
// 关闭已打开的文件
|
||||
for _, r := range readers {
|
||||
if closer, ok := r.(io.Closer); ok {
|
||||
closer.Close()
|
||||
}
|
||||
}
|
||||
// 删除已创建的组件记录
|
||||
_ = s.uiComponentRepo.Delete(ctx, createdComponent.ID)
|
||||
return entities.UIComponent{}, fmt.Errorf("打开上传文件失败: %w", err)
|
||||
}
|
||||
|
||||
readers = append(readers, src)
|
||||
filenames = append(filenames, fileHeader.Filename)
|
||||
|
||||
// 确定文件路径
|
||||
var path string
|
||||
if i < len(paths) && paths[i] != "" {
|
||||
path = paths[i]
|
||||
} else {
|
||||
path = fileHeader.Filename
|
||||
}
|
||||
filePaths = append(filePaths, path)
|
||||
}
|
||||
|
||||
// 使用新的批量上传方法
|
||||
if err := s.fileService.UploadMultipleFiles(ctx, createdComponent.ID, createdComponent.ComponentCode, readers, filenames, filePaths); err != nil {
|
||||
// 关闭已打开的文件
|
||||
for _, r := range readers {
|
||||
if closer, ok := r.(io.Closer); ok {
|
||||
closer.Close()
|
||||
}
|
||||
}
|
||||
// 删除已创建的组件记录
|
||||
_ = s.uiComponentRepo.Delete(ctx, createdComponent.ID)
|
||||
return entities.UIComponent{}, err
|
||||
}
|
||||
|
||||
// 关闭所有文件
|
||||
for _, r := range readers {
|
||||
if closer, ok := r.(io.Closer); ok {
|
||||
closer.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// 更新组件信息
|
||||
folderPath := "resources/Pure_Component/src/ui"
|
||||
createdComponent.FolderPath = &folderPath
|
||||
|
||||
// 记录文件上传时间
|
||||
now := time.Now()
|
||||
createdComponent.FileUploadTime = &now
|
||||
|
||||
// 检查是否有ZIP文件
|
||||
hasZipFile := false
|
||||
for _, fileHeader := range files {
|
||||
if strings.HasSuffix(strings.ToLower(fileHeader.Filename), ".zip") {
|
||||
hasZipFile = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有ZIP文件,则标记为已解压
|
||||
if hasZipFile {
|
||||
createdComponent.IsExtracted = true
|
||||
}
|
||||
|
||||
// 更新组件信息
|
||||
err = s.uiComponentRepo.Update(ctx, createdComponent)
|
||||
if err != nil {
|
||||
// 尝试删除已创建的组件记录
|
||||
_ = s.uiComponentRepo.Delete(ctx, createdComponent.ID)
|
||||
return entities.UIComponent{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return createdComponent, nil
|
||||
}
|
||||
|
||||
// GetUIComponentByID 根据ID获取UI组件
|
||||
func (s *UIComponentApplicationServiceImpl) GetUIComponentByID(ctx context.Context, id string) (*entities.UIComponent, error) {
|
||||
return s.uiComponentRepo.GetByID(ctx, id)
|
||||
}
|
||||
|
||||
// GetUIComponentByCode 根据编码获取UI组件
|
||||
func (s *UIComponentApplicationServiceImpl) GetUIComponentByCode(ctx context.Context, code string) (*entities.UIComponent, error) {
|
||||
return s.uiComponentRepo.GetByCode(ctx, code)
|
||||
}
|
||||
|
||||
// UpdateUIComponent 更新UI组件
|
||||
func (s *UIComponentApplicationServiceImpl) UpdateUIComponent(ctx context.Context, req UpdateUIComponentRequest) error {
|
||||
component, err := s.uiComponentRepo.GetByID(ctx, req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if component == nil {
|
||||
return ErrComponentNotFound
|
||||
}
|
||||
|
||||
// 如果更新编码,检查是否与其他组件冲突
|
||||
if req.ComponentCode != "" && req.ComponentCode != component.ComponentCode {
|
||||
existing, _ := s.uiComponentRepo.GetByCode(ctx, req.ComponentCode)
|
||||
if existing != nil && existing.ID != req.ID {
|
||||
return ErrComponentCodeAlreadyExists
|
||||
}
|
||||
component.ComponentCode = req.ComponentCode
|
||||
}
|
||||
|
||||
if req.ComponentName != "" {
|
||||
component.ComponentName = req.ComponentName
|
||||
}
|
||||
if req.Description != "" {
|
||||
component.Description = req.Description
|
||||
}
|
||||
if req.Version != "" {
|
||||
component.Version = req.Version
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
component.IsActive = *req.IsActive
|
||||
}
|
||||
if req.SortOrder != nil {
|
||||
component.SortOrder = *req.SortOrder
|
||||
}
|
||||
|
||||
return s.uiComponentRepo.Update(ctx, *component)
|
||||
}
|
||||
|
||||
// DeleteUIComponent 删除UI组件
|
||||
func (s *UIComponentApplicationServiceImpl) DeleteUIComponent(ctx context.Context, id string) error {
|
||||
// 获取组件信息
|
||||
component, err := s.uiComponentRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
s.logger.Error("获取UI组件失败", zap.Error(err), zap.String("id", id))
|
||||
return fmt.Errorf("获取UI组件失败: %w", err)
|
||||
}
|
||||
if component == nil {
|
||||
s.logger.Warn("UI组件不存在", zap.String("id", id))
|
||||
return ErrComponentNotFound
|
||||
}
|
||||
|
||||
// 记录组件信息
|
||||
s.logger.Info("开始删除UI组件",
|
||||
zap.String("id", id),
|
||||
zap.String("componentCode", component.ComponentCode),
|
||||
zap.String("componentName", component.ComponentName),
|
||||
zap.Bool("isExtracted", component.IsExtracted),
|
||||
zap.Any("filePath", component.FilePath),
|
||||
zap.Any("folderPath", component.FolderPath))
|
||||
|
||||
// 使用智能删除方法,根据组件编码和上传时间删除相关文件
|
||||
if err := s.fileService.DeleteFilesByComponentCode(component.ComponentCode, component.FileUploadTime); err != nil {
|
||||
// 记录错误但不阻止删除数据库记录
|
||||
s.logger.Error("删除组件文件失败",
|
||||
zap.Error(err),
|
||||
zap.String("componentCode", component.ComponentCode),
|
||||
zap.Any("fileUploadTime", component.FileUploadTime))
|
||||
}
|
||||
|
||||
// 删除关联的文件(FilePath指向的文件)
|
||||
if component.FilePath != nil {
|
||||
if err := s.fileStorageService.DeleteFile(ctx, *component.FilePath); err != nil {
|
||||
s.logger.Error("删除文件失败",
|
||||
zap.Error(err),
|
||||
zap.String("filePath", *component.FilePath))
|
||||
}
|
||||
}
|
||||
|
||||
// 删除数据库记录
|
||||
if err := s.uiComponentRepo.Delete(ctx, id); err != nil {
|
||||
s.logger.Error("删除UI组件数据库记录失败",
|
||||
zap.Error(err),
|
||||
zap.String("id", id))
|
||||
return fmt.Errorf("删除UI组件数据库记录失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("UI组件删除成功",
|
||||
zap.String("id", id),
|
||||
zap.String("componentCode", component.ComponentCode))
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListUIComponents 获取UI组件列表
|
||||
func (s *UIComponentApplicationServiceImpl) ListUIComponents(ctx context.Context, req ListUIComponentsRequest) (ListUIComponentsResponse, error) {
|
||||
filters := make(map[string]interface{})
|
||||
|
||||
if req.Keyword != "" {
|
||||
filters["keyword"] = req.Keyword
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
filters["is_active"] = *req.IsActive
|
||||
}
|
||||
filters["page"] = req.Page
|
||||
filters["page_size"] = req.PageSize
|
||||
filters["sort_by"] = req.SortBy
|
||||
filters["sort_order"] = req.SortOrder
|
||||
|
||||
components, total, err := s.uiComponentRepo.List(ctx, filters)
|
||||
if err != nil {
|
||||
return ListUIComponentsResponse{}, err
|
||||
}
|
||||
|
||||
return ListUIComponentsResponse{
|
||||
Components: components,
|
||||
Total: total,
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UploadUIComponentFile 上传UI组件文件
|
||||
func (s *UIComponentApplicationServiceImpl) UploadUIComponentFile(ctx context.Context, id string, file *multipart.FileHeader) (string, error) {
|
||||
component, err := s.uiComponentRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if component == nil {
|
||||
return "", ErrComponentNotFound
|
||||
}
|
||||
|
||||
// 检查文件大小(100MB)
|
||||
if file.Size > 100*1024*1024 {
|
||||
return "", ErrInvalidFileType // 复用此错误表示文件太大
|
||||
}
|
||||
|
||||
// 打开上传的文件
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// 生成文件路径
|
||||
filePath := filepath.Join("ui-components", id+"_"+file.Filename)
|
||||
|
||||
// 存储文件
|
||||
storedPath, err := s.fileStorageService.StoreFile(ctx, src, filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 删除旧文件
|
||||
if component.FilePath != nil {
|
||||
_ = s.fileStorageService.DeleteFile(ctx, *component.FilePath)
|
||||
}
|
||||
|
||||
// 获取文件类型
|
||||
fileType := strings.ToLower(filepath.Ext(file.Filename))
|
||||
|
||||
// 更新组件信息
|
||||
component.FilePath = &storedPath
|
||||
component.FileSize = &file.Size
|
||||
component.FileType = &fileType
|
||||
if err := s.uiComponentRepo.Update(ctx, *component); err != nil {
|
||||
// 如果更新失败,尝试删除已上传的文件
|
||||
_ = s.fileStorageService.DeleteFile(ctx, storedPath)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return storedPath, nil
|
||||
}
|
||||
|
||||
// DownloadUIComponentFile 下载UI组件文件
|
||||
func (s *UIComponentApplicationServiceImpl) DownloadUIComponentFile(ctx context.Context, id string) (string, error) {
|
||||
component, err := s.uiComponentRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if component == nil {
|
||||
return "", ErrComponentNotFound
|
||||
}
|
||||
|
||||
if component.FilePath == nil {
|
||||
return "", ErrComponentFileNotFound
|
||||
}
|
||||
|
||||
return s.fileStorageService.GetFileURL(ctx, *component.FilePath)
|
||||
}
|
||||
|
||||
// AssociateUIComponentToProduct 关联UI组件到产品
|
||||
func (s *UIComponentApplicationServiceImpl) AssociateUIComponentToProduct(ctx context.Context, req AssociateUIComponentRequest) error {
|
||||
// 检查组件是否存在
|
||||
component, err := s.uiComponentRepo.GetByID(ctx, req.UIComponentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if component == nil {
|
||||
return ErrComponentNotFound
|
||||
}
|
||||
|
||||
// 创建关联
|
||||
relation := entities.ProductUIComponent{
|
||||
ProductID: req.ProductID,
|
||||
UIComponentID: req.UIComponentID,
|
||||
Price: decimal.NewFromFloat(req.Price),
|
||||
IsEnabled: req.IsEnabled,
|
||||
}
|
||||
|
||||
_, err = s.productUIComponentRepo.Create(ctx, relation)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetProductUIComponents 获取产品的UI组件列表
|
||||
func (s *UIComponentApplicationServiceImpl) GetProductUIComponents(ctx context.Context, productID string) ([]entities.ProductUIComponent, error) {
|
||||
return s.productUIComponentRepo.GetByProductID(ctx, productID)
|
||||
}
|
||||
|
||||
// RemoveUIComponentFromProduct 从产品中移除UI组件
|
||||
func (s *UIComponentApplicationServiceImpl) RemoveUIComponentFromProduct(ctx context.Context, productID, componentID string) error {
|
||||
// 查找关联记录
|
||||
relations, err := s.productUIComponentRepo.GetByProductID(ctx, productID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 找到要删除的关联记录
|
||||
var relationID string
|
||||
for _, relation := range relations {
|
||||
if relation.UIComponentID == componentID {
|
||||
relationID = relation.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if relationID == "" {
|
||||
return ErrProductComponentRelationNotFound
|
||||
}
|
||||
|
||||
return s.productUIComponentRepo.Delete(ctx, relationID)
|
||||
}
|
||||
|
||||
// UploadAndExtractUIComponentFile 上传并解压UI组件文件
|
||||
func (s *UIComponentApplicationServiceImpl) UploadAndExtractUIComponentFile(ctx context.Context, id string, file *multipart.FileHeader) error {
|
||||
// 获取组件信息
|
||||
component, err := s.uiComponentRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if component == nil {
|
||||
return ErrComponentNotFound
|
||||
}
|
||||
|
||||
// 打开上传的文件
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开上传文件失败: %w", err)
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// 上传并解压文件
|
||||
if err := s.fileService.UploadAndExtract(ctx, id, component.ComponentCode, src, file.Filename); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 获取文件类型
|
||||
fileType := strings.ToLower(filepath.Ext(file.Filename))
|
||||
|
||||
// 更新组件信息
|
||||
folderPath := "resources/Pure_Component/src/ui"
|
||||
component.FolderPath = &folderPath
|
||||
component.FileType = &fileType
|
||||
|
||||
// 记录文件上传时间
|
||||
now := time.Now()
|
||||
component.FileUploadTime = &now
|
||||
|
||||
// 仅对ZIP文件设置已解压标记
|
||||
if fileType == ".zip" {
|
||||
component.IsExtracted = true
|
||||
}
|
||||
|
||||
return s.uiComponentRepo.Update(ctx, *component)
|
||||
}
|
||||
|
||||
// GetUIComponentFolderContent 获取UI组件文件夹内容
|
||||
func (s *UIComponentApplicationServiceImpl) GetUIComponentFolderContent(ctx context.Context, id string) ([]FileInfo, error) {
|
||||
// 获取组件信息
|
||||
component, err := s.uiComponentRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if component == nil {
|
||||
return nil, ErrComponentNotFound
|
||||
}
|
||||
|
||||
// 如果没有文件夹路径,返回空
|
||||
if component.FolderPath == nil {
|
||||
return []FileInfo{}, nil
|
||||
}
|
||||
|
||||
// 获取文件夹内容
|
||||
return s.fileService.GetFolderContent(*component.FolderPath)
|
||||
}
|
||||
|
||||
// DeleteUIComponentFolder 删除UI组件文件夹
|
||||
func (s *UIComponentApplicationServiceImpl) DeleteUIComponentFolder(ctx context.Context, id string) error {
|
||||
// 获取组件信息
|
||||
component, err := s.uiComponentRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if component == nil {
|
||||
return ErrComponentNotFound
|
||||
}
|
||||
|
||||
// 注意:我们不再删除整个UI目录,因为所有组件共享同一个目录
|
||||
// 这里只更新组件信息,标记为未上传状态
|
||||
// 更新组件信息
|
||||
component.FolderPath = nil
|
||||
component.IsExtracted = false
|
||||
return s.uiComponentRepo.Update(ctx, *component)
|
||||
}
|
||||
21
internal/application/product/ui_component_errors.go
Normal file
21
internal/application/product/ui_component_errors.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package product
|
||||
|
||||
import "errors"
|
||||
|
||||
// UI组件相关错误定义
|
||||
var (
|
||||
// ErrComponentNotFound UI组件不存在
|
||||
ErrComponentNotFound = errors.New("UI组件不存在")
|
||||
|
||||
// ErrComponentCodeAlreadyExists UI组件编码已存在
|
||||
ErrComponentCodeAlreadyExists = errors.New("UI组件编码已存在")
|
||||
|
||||
// ErrComponentFileNotFound UI组件文件不存在
|
||||
ErrComponentFileNotFound = errors.New("UI组件文件不存在")
|
||||
|
||||
// ErrInvalidFileType 无效的文件类型
|
||||
ErrInvalidFileType = errors.New("无效的文件类型,仅支持ZIP文件")
|
||||
|
||||
// ErrProductComponentRelationNotFound 产品UI组件关联不存在
|
||||
ErrProductComponentRelationNotFound = errors.New("产品UI组件关联不存在")
|
||||
)
|
||||
459
internal/application/product/ui_component_file_service.go
Normal file
459
internal/application/product/ui_component_file_service.go
Normal file
@@ -0,0 +1,459 @@
|
||||
package product
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// UIComponentFileService UI组件文件服务接口
|
||||
type UIComponentFileService interface {
|
||||
// 上传并解压UI组件文件
|
||||
UploadAndExtract(ctx context.Context, componentID, componentCode string, file io.Reader, filename string) error
|
||||
|
||||
// 批量上传UI组件文件(支持文件夹结构)
|
||||
UploadMultipleFiles(ctx context.Context, componentID, componentCode string, files []io.Reader, filenames []string, paths []string) error
|
||||
|
||||
// 根据组件编码创建文件夹
|
||||
CreateFolderByCode(componentCode string) (string, error)
|
||||
|
||||
// 删除组件文件夹
|
||||
DeleteFolder(folderPath string) error
|
||||
|
||||
// 检查文件夹是否存在
|
||||
FolderExists(folderPath string) bool
|
||||
|
||||
// 获取文件夹内容
|
||||
GetFolderContent(folderPath string) ([]FileInfo, error)
|
||||
|
||||
// 根据组件编码和上传时间智能删除组件相关文件
|
||||
DeleteFilesByComponentCode(componentCode string, uploadTime *time.Time) error
|
||||
}
|
||||
|
||||
// FileInfo 文件信息
|
||||
type FileInfo struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Size int64 `json:"size"`
|
||||
Type string `json:"type"` // "file" or "folder"
|
||||
Modified time.Time `json:"modified"`
|
||||
}
|
||||
|
||||
// UIComponentFileServiceImpl UI组件文件服务实现
|
||||
type UIComponentFileServiceImpl struct {
|
||||
basePath string
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewUIComponentFileService 创建UI组件文件服务
|
||||
func NewUIComponentFileService(basePath string, logger *zap.Logger) UIComponentFileService {
|
||||
// 确保基础路径存在
|
||||
if err := os.MkdirAll(basePath, 0755); err != nil {
|
||||
logger.Error("创建基础存储目录失败", zap.Error(err), zap.String("path", basePath))
|
||||
}
|
||||
|
||||
return &UIComponentFileServiceImpl{
|
||||
basePath: basePath,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// UploadAndExtract 上传并解压UI组件文件
|
||||
func (s *UIComponentFileServiceImpl) UploadAndExtract(ctx context.Context, componentID, componentCode string, file io.Reader, filename string) error {
|
||||
// 直接使用基础路径作为文件夹路径,不再创建组件编码子文件夹
|
||||
folderPath := s.basePath
|
||||
|
||||
// 确保基础目录存在
|
||||
if err := os.MkdirAll(folderPath, 0755); err != nil {
|
||||
return fmt.Errorf("创建基础目录失败: %w", err)
|
||||
}
|
||||
|
||||
// 保存上传的文件
|
||||
filePath := filepath.Join(folderPath, filename)
|
||||
savedFile, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建文件失败: %w", err)
|
||||
}
|
||||
defer savedFile.Close()
|
||||
|
||||
// 复制文件内容
|
||||
if _, err := io.Copy(savedFile, file); err != nil {
|
||||
// 删除部分写入的文件
|
||||
_ = os.Remove(filePath)
|
||||
return fmt.Errorf("保存文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 仅对ZIP文件执行解压逻辑
|
||||
if strings.HasSuffix(strings.ToLower(filename), ".zip") {
|
||||
// 解压文件到基础目录
|
||||
if err := s.extractZipFile(filePath, folderPath); err != nil {
|
||||
// 删除ZIP文件
|
||||
_ = os.Remove(filePath)
|
||||
return fmt.Errorf("解压文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 删除ZIP文件
|
||||
_ = os.Remove(filePath)
|
||||
|
||||
s.logger.Info("UI组件文件上传并解压成功",
|
||||
zap.String("componentID", componentID),
|
||||
zap.String("componentCode", componentCode),
|
||||
zap.String("folderPath", folderPath))
|
||||
} else {
|
||||
s.logger.Info("UI组件文件上传成功(未解压)",
|
||||
zap.String("componentID", componentID),
|
||||
zap.String("componentCode", componentCode),
|
||||
zap.String("filePath", filePath))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UploadMultipleFiles 批量上传UI组件文件(支持文件夹结构)
|
||||
func (s *UIComponentFileServiceImpl) UploadMultipleFiles(ctx context.Context, componentID, componentCode string, files []io.Reader, filenames []string, paths []string) error {
|
||||
// 直接使用基础路径作为文件夹路径,不再创建组件编码子文件夹
|
||||
folderPath := s.basePath
|
||||
|
||||
// 确保基础目录存在
|
||||
if err := os.MkdirAll(folderPath, 0755); err != nil {
|
||||
return fmt.Errorf("创建基础目录失败: %w", err)
|
||||
}
|
||||
|
||||
// 处理每个文件
|
||||
for i, file := range files {
|
||||
filename := filenames[i]
|
||||
path := paths[i]
|
||||
|
||||
// 如果有路径信息,创建对应的子文件夹
|
||||
if path != "" && path != filename {
|
||||
// 获取文件所在目录
|
||||
dir := filepath.Dir(path)
|
||||
if dir != "." {
|
||||
// 创建子文件夹
|
||||
subDirPath := filepath.Join(folderPath, dir)
|
||||
if err := os.MkdirAll(subDirPath, 0755); err != nil {
|
||||
return fmt.Errorf("创建子文件夹失败: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 确定文件保存路径
|
||||
var filePath string
|
||||
if path != "" && path != filename {
|
||||
// 有路径信息,使用完整路径
|
||||
filePath = filepath.Join(folderPath, path)
|
||||
} else {
|
||||
// 没有路径信息,直接保存在根目录
|
||||
filePath = filepath.Join(folderPath, filename)
|
||||
}
|
||||
|
||||
// 保存上传的文件
|
||||
savedFile, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建文件失败: %w", err)
|
||||
}
|
||||
defer savedFile.Close()
|
||||
|
||||
// 复制文件内容
|
||||
if _, err := io.Copy(savedFile, file); err != nil {
|
||||
// 删除部分写入的文件
|
||||
_ = os.Remove(filePath)
|
||||
return fmt.Errorf("保存文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 对ZIP文件执行解压逻辑
|
||||
if strings.HasSuffix(strings.ToLower(filename), ".zip") {
|
||||
// 确定解压目录
|
||||
var extractDir string
|
||||
if path != "" && path != filename {
|
||||
// 有路径信息,解压到对应目录
|
||||
dir := filepath.Dir(path)
|
||||
if dir != "." {
|
||||
extractDir = filepath.Join(folderPath, dir)
|
||||
} else {
|
||||
extractDir = folderPath
|
||||
}
|
||||
} else {
|
||||
// 没有路径信息,解压到根目录
|
||||
extractDir = folderPath
|
||||
}
|
||||
|
||||
// 解压文件
|
||||
if err := s.extractZipFile(filePath, extractDir); err != nil {
|
||||
// 删除ZIP文件
|
||||
_ = os.Remove(filePath)
|
||||
return fmt.Errorf("解压文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 删除ZIP文件
|
||||
_ = os.Remove(filePath)
|
||||
|
||||
s.logger.Info("UI组件文件上传并解压成功",
|
||||
zap.String("componentID", componentID),
|
||||
zap.String("componentCode", componentCode),
|
||||
zap.String("filePath", filePath),
|
||||
zap.String("extractDir", extractDir))
|
||||
} else {
|
||||
s.logger.Info("UI组件文件上传成功(未解压)",
|
||||
zap.String("componentID", componentID),
|
||||
zap.String("componentCode", componentCode),
|
||||
zap.String("filePath", filePath))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateFolderByCode 根据组件编码创建文件夹
|
||||
func (s *UIComponentFileServiceImpl) CreateFolderByCode(componentCode string) (string, error) {
|
||||
folderPath := filepath.Join(s.basePath, componentCode)
|
||||
|
||||
// 创建文件夹(如果不存在)
|
||||
if err := os.MkdirAll(folderPath, 0755); err != nil {
|
||||
return "", fmt.Errorf("创建文件夹失败: %w", err)
|
||||
}
|
||||
|
||||
return folderPath, nil
|
||||
}
|
||||
|
||||
// DeleteFolder 删除组件文件夹
|
||||
func (s *UIComponentFileServiceImpl) DeleteFolder(folderPath string) error {
|
||||
// 记录尝试删除的文件夹路径
|
||||
s.logger.Info("尝试删除文件夹", zap.String("folderPath", folderPath))
|
||||
|
||||
// 获取文件夹信息,用于调试
|
||||
if info, err := os.Stat(folderPath); err == nil {
|
||||
s.logger.Info("文件夹信息",
|
||||
zap.String("folderPath", folderPath),
|
||||
zap.Bool("isDir", info.IsDir()),
|
||||
zap.Int64("size", info.Size()),
|
||||
zap.Time("modTime", info.ModTime()))
|
||||
} else {
|
||||
s.logger.Error("获取文件夹信息失败",
|
||||
zap.Error(err),
|
||||
zap.String("folderPath", folderPath))
|
||||
}
|
||||
|
||||
// 检查文件夹是否存在
|
||||
if !s.FolderExists(folderPath) {
|
||||
s.logger.Info("文件夹不存在", zap.String("folderPath", folderPath))
|
||||
return nil // 文件夹不存在,不视为错误
|
||||
}
|
||||
|
||||
// 尝试删除文件夹
|
||||
s.logger.Info("开始删除文件夹", zap.String("folderPath", folderPath))
|
||||
if err := os.RemoveAll(folderPath); err != nil {
|
||||
s.logger.Error("删除文件夹失败",
|
||||
zap.Error(err),
|
||||
zap.String("folderPath", folderPath))
|
||||
return fmt.Errorf("删除文件夹失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("删除组件文件夹成功", zap.String("folderPath", folderPath))
|
||||
return nil
|
||||
}
|
||||
|
||||
// FolderExists 检查文件夹是否存在
|
||||
func (s *UIComponentFileServiceImpl) FolderExists(folderPath string) bool {
|
||||
info, err := os.Stat(folderPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return info.IsDir()
|
||||
}
|
||||
|
||||
// GetFolderContent 获取文件夹内容
|
||||
func (s *UIComponentFileServiceImpl) GetFolderContent(folderPath string) ([]FileInfo, error) {
|
||||
var files []FileInfo
|
||||
|
||||
err := filepath.Walk(folderPath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 跳过根目录
|
||||
if path == folderPath {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取相对路径
|
||||
relPath, err := filepath.Rel(folderPath, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileType := "file"
|
||||
if info.IsDir() {
|
||||
fileType = "folder"
|
||||
}
|
||||
|
||||
files = append(files, FileInfo{
|
||||
Name: info.Name(),
|
||||
Path: relPath,
|
||||
Size: info.Size(),
|
||||
Type: fileType,
|
||||
Modified: info.ModTime(),
|
||||
})
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("扫描文件夹失败: %w", err)
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// extractZipFile 解压ZIP文件
|
||||
func (s *UIComponentFileServiceImpl) extractZipFile(zipPath, destPath string) error {
|
||||
reader, err := zip.OpenReader(zipPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开ZIP文件失败: %w", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
for _, file := range reader.File {
|
||||
path := filepath.Join(destPath, file.Name)
|
||||
|
||||
// 防止路径遍历攻击
|
||||
if !strings.HasPrefix(filepath.Clean(path), filepath.Clean(destPath)+string(os.PathSeparator)) {
|
||||
return fmt.Errorf("无效的文件路径: %s", file.Name)
|
||||
}
|
||||
|
||||
if file.FileInfo().IsDir() {
|
||||
// 创建目录
|
||||
if err := os.MkdirAll(path, file.Mode()); err != nil {
|
||||
return fmt.Errorf("创建目录失败: %w", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 创建文件
|
||||
fileReader, err := file.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开ZIP内文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 确保父目录存在
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
fileReader.Close()
|
||||
return fmt.Errorf("创建父目录失败: %w", err)
|
||||
}
|
||||
|
||||
destFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
|
||||
if err != nil {
|
||||
fileReader.Close()
|
||||
return fmt.Errorf("创建目标文件失败: %w", err)
|
||||
}
|
||||
|
||||
_, err = io.Copy(destFile, fileReader)
|
||||
fileReader.Close()
|
||||
destFile.Close()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("写入文件失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteFilesByComponentCode 根据组件编码和上传时间智能删除组件相关文件
|
||||
func (s *UIComponentFileServiceImpl) DeleteFilesByComponentCode(componentCode string, uploadTime *time.Time) error {
|
||||
// 记录基础路径和组件编码
|
||||
s.logger.Info("开始删除组件文件",
|
||||
zap.String("basePath", s.basePath),
|
||||
zap.String("componentCode", componentCode),
|
||||
zap.Any("uploadTime", uploadTime))
|
||||
|
||||
// 1. 查找名为组件编码的文件夹
|
||||
componentDir := filepath.Join(s.basePath, componentCode)
|
||||
s.logger.Info("检查组件文件夹", zap.String("componentDir", componentDir))
|
||||
|
||||
if s.FolderExists(componentDir) {
|
||||
s.logger.Info("找到组件文件夹,开始删除", zap.String("componentDir", componentDir))
|
||||
if err := s.DeleteFolder(componentDir); err != nil {
|
||||
s.logger.Error("删除组件文件夹失败",
|
||||
zap.Error(err),
|
||||
zap.String("componentCode", componentCode),
|
||||
zap.String("componentDir", componentDir))
|
||||
return fmt.Errorf("删除组件文件夹失败: %w", err)
|
||||
}
|
||||
s.logger.Info("成功删除组件文件夹", zap.String("componentCode", componentCode))
|
||||
return nil
|
||||
} else {
|
||||
s.logger.Info("组件文件夹不存在", zap.String("componentDir", componentDir))
|
||||
}
|
||||
|
||||
// 2. 查找文件名包含组件编码的文件
|
||||
pattern := filepath.Join(s.basePath, "*"+componentCode+"*")
|
||||
s.logger.Info("查找匹配文件", zap.String("pattern", pattern))
|
||||
|
||||
files, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
s.logger.Error("查找组件文件失败",
|
||||
zap.Error(err),
|
||||
zap.String("pattern", pattern))
|
||||
return fmt.Errorf("查找组件文件失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("找到匹配文件",
|
||||
zap.Strings("files", files),
|
||||
zap.Int("count", len(files)))
|
||||
|
||||
// 3. 如果没有上传时间,删除所有匹配的文件
|
||||
if uploadTime == nil {
|
||||
for _, file := range files {
|
||||
if err := os.Remove(file); err != nil {
|
||||
s.logger.Error("删除文件失败", zap.String("file", file), zap.Error(err))
|
||||
} else {
|
||||
s.logger.Info("成功删除文件", zap.String("file", file))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 4. 如果有上传时间,根据文件修改时间和上传时间的匹配度来删除文件
|
||||
var deletedFiles []string
|
||||
for _, file := range files {
|
||||
// 获取文件信息
|
||||
fileInfo, err := os.Stat(file)
|
||||
if err != nil {
|
||||
s.logger.Warn("获取文件信息失败", zap.String("file", file), zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
// 计算文件修改时间与上传时间的差异(以秒为单位)
|
||||
timeDiff := fileInfo.ModTime().Sub(*uploadTime).Seconds()
|
||||
|
||||
// 如果时间差在60秒内,认为是最匹配的文件
|
||||
if timeDiff < 60 && timeDiff > -60 {
|
||||
if err := os.Remove(file); err != nil {
|
||||
s.logger.Warn("删除文件失败", zap.String("file", file), zap.Error(err))
|
||||
} else {
|
||||
deletedFiles = append(deletedFiles, file)
|
||||
s.logger.Info("成功删除文件", zap.String("file", file),
|
||||
zap.Time("uploadTime", *uploadTime),
|
||||
zap.Time("fileModTime", fileInfo.ModTime()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到匹配的文件,记录警告但返回成功
|
||||
if len(deletedFiles) == 0 && len(files) > 0 {
|
||||
s.logger.Warn("没有找到匹配时间戳的文件",
|
||||
zap.String("componentCode", componentCode),
|
||||
zap.Time("uploadTime", *uploadTime),
|
||||
zap.Int("foundFiles", len(files)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user