Files
tyapi-server/internal/application/product/ui_component_application_service.go
2025-12-19 17:05:09 +08:00

687 lines
21 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}