package product import ( "context" "fmt" "io" "mime/multipart" "path/filepath" "strings" "tyapi-server/internal/domains/product/entities" "tyapi-server/internal/domains/product/repositories" "github.com/shopspring/decimal" ) // 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 } // 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, ) UIComponentApplicationService { return &UIComponentApplicationServiceImpl{ uiComponentRepo: uiComponentRepo, productUIComponentRepo: productUIComponentRepo, fileStorageService: fileStorageService, fileService: fileService, } } // 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 // 仅对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 // 检查是否有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 // 检查是否有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 { return err } if component == nil { return ErrComponentNotFound } // 删除关联的文件 if component.FilePath != nil { _ = s.fileStorageService.DeleteFile(ctx, *component.FilePath) } return s.uiComponentRepo.Delete(ctx, id) } // 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 // 仅对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) }