addui
This commit is contained in:
@@ -3,11 +3,8 @@ package finance
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/smartwalle/alipay/v3"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/services/payments"
|
||||
"go.uber.org/zap"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
"tyapi-server/internal/application/finance/dto/commands"
|
||||
"tyapi-server/internal/application/finance/dto/queries"
|
||||
@@ -16,11 +13,17 @@ import (
|
||||
finance_entities "tyapi-server/internal/domains/finance/entities"
|
||||
finance_repositories "tyapi-server/internal/domains/finance/repositories"
|
||||
finance_services "tyapi-server/internal/domains/finance/services"
|
||||
product_repositories "tyapi-server/internal/domains/product/repositories"
|
||||
user_repositories "tyapi-server/internal/domains/user/repositories"
|
||||
"tyapi-server/internal/shared/database"
|
||||
"tyapi-server/internal/shared/export"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
"tyapi-server/internal/shared/payment"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/smartwalle/alipay/v3"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/services/payments"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// FinanceApplicationServiceImpl 财务应用服务实现
|
||||
@@ -33,6 +36,7 @@ type FinanceApplicationServiceImpl struct {
|
||||
alipayOrderRepo finance_repositories.AlipayOrderRepository
|
||||
wechatOrderRepo finance_repositories.WechatOrderRepository
|
||||
rechargeRecordRepo finance_repositories.RechargeRecordRepository
|
||||
componentReportRepo product_repositories.ComponentReportRepository
|
||||
userRepo user_repositories.UserRepository
|
||||
txManager *database.TransactionManager
|
||||
exportManager *export.ExportManager
|
||||
@@ -50,6 +54,7 @@ func NewFinanceApplicationService(
|
||||
alipayOrderRepo finance_repositories.AlipayOrderRepository,
|
||||
wechatOrderRepo finance_repositories.WechatOrderRepository,
|
||||
rechargeRecordRepo finance_repositories.RechargeRecordRepository,
|
||||
componentReportRepo product_repositories.ComponentReportRepository,
|
||||
userRepo user_repositories.UserRepository,
|
||||
txManager *database.TransactionManager,
|
||||
logger *zap.Logger,
|
||||
@@ -65,6 +70,7 @@ func NewFinanceApplicationService(
|
||||
alipayOrderRepo: alipayOrderRepo,
|
||||
wechatOrderRepo: wechatOrderRepo,
|
||||
rechargeRecordRepo: rechargeRecordRepo,
|
||||
componentReportRepo: componentReportRepo,
|
||||
userRepo: userRepo,
|
||||
txManager: txManager,
|
||||
exportManager: exportManager,
|
||||
@@ -815,6 +821,8 @@ func (s *FinanceApplicationServiceImpl) batchGetCompanyNamesForRechargeRecords(c
|
||||
|
||||
// HandleAlipayCallback 处理支付宝回调
|
||||
func (s *FinanceApplicationServiceImpl) HandleAlipayCallback(ctx context.Context, r *http.Request) error {
|
||||
s.logger.Info("========== 开始处理支付宝支付回调 ==========")
|
||||
|
||||
// 解析并验证支付宝回调通知
|
||||
notification, err := s.aliPayClient.HandleAliPaymentNotification(r)
|
||||
if err != nil {
|
||||
@@ -834,14 +842,25 @@ func (s *FinanceApplicationServiceImpl) HandleAlipayCallback(ctx context.Context
|
||||
|
||||
// 检查交易状态
|
||||
if !s.aliPayClient.IsAlipayPaymentSuccess(notification) {
|
||||
s.logger.Warn("支付宝交易未成功",
|
||||
s.logger.Warn("支付宝交易未成功,跳过处理",
|
||||
zap.String("out_trade_no", notification.OutTradeNo),
|
||||
zap.String("trade_status", string(notification.TradeStatus)),
|
||||
)
|
||||
return nil // 不返回错误,因为这是正常的业务状态
|
||||
}
|
||||
|
||||
// 使用公共方法处理支付成功逻辑
|
||||
s.logger.Info("支付宝支付成功,开始处理业务逻辑",
|
||||
zap.String("out_trade_no", notification.OutTradeNo),
|
||||
zap.String("trade_no", notification.TradeNo),
|
||||
)
|
||||
|
||||
// 先检查是否是组件报告下载的支付订单
|
||||
s.logger.Info("步骤1: 检查是否是组件报告下载订单",
|
||||
zap.String("out_trade_no", notification.OutTradeNo),
|
||||
)
|
||||
|
||||
// 使用公共方法处理支付成功逻辑(包括更新充值记录状态)
|
||||
// 无论是组件报告下载订单还是普通充值订单,都需要更新充值记录状态
|
||||
err = s.processAlipayPaymentSuccess(ctx, notification.OutTradeNo, notification.TradeNo, notification.TotalAmount, notification.BuyerId, notification.SellerId)
|
||||
if err != nil {
|
||||
s.logger.Error("处理支付宝支付成功失败",
|
||||
@@ -851,6 +870,7 @@ func (s *FinanceApplicationServiceImpl) HandleAlipayCallback(ctx context.Context
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("========== 支付宝支付回调处理完成 ==========")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -868,6 +888,7 @@ func (s *FinanceApplicationServiceImpl) processAlipayPaymentSuccess(ctx context.
|
||||
|
||||
// 直接调用充值记录服务处理支付成功逻辑
|
||||
// 该服务内部会处理所有必要的检查、事务和更新操作
|
||||
// 如果是组件报告下载订单,服务会自动跳过钱包余额增加
|
||||
err = s.rechargeRecordService.HandleAlipayPaymentSuccess(ctx, outTradeNo, amount, tradeNo)
|
||||
if err != nil {
|
||||
s.logger.Error("处理支付宝支付成功失败",
|
||||
@@ -877,6 +898,9 @@ func (s *FinanceApplicationServiceImpl) processAlipayPaymentSuccess(ctx context.
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查并更新组件报告下载记录状态(如果存在)
|
||||
s.updateComponentReportDownloadStatus(ctx, outTradeNo)
|
||||
|
||||
s.logger.Info("支付宝支付成功处理完成",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("trade_no", tradeNo),
|
||||
@@ -1398,6 +1422,8 @@ func (s *FinanceApplicationServiceImpl) updateWechatOrderStatus(ctx context.Cont
|
||||
|
||||
// HandleWechatPayCallback 处理微信支付回调
|
||||
func (s *FinanceApplicationServiceImpl) HandleWechatPayCallback(ctx context.Context, r *http.Request) error {
|
||||
s.logger.Info("========== 开始处理微信支付回调 ==========")
|
||||
|
||||
if s.wechatPayService == nil {
|
||||
s.logger.Error("微信支付服务未初始化")
|
||||
return fmt.Errorf("微信支付服务未初始化")
|
||||
@@ -1439,14 +1465,42 @@ func (s *FinanceApplicationServiceImpl) HandleWechatPayCallback(ctx context.Cont
|
||||
|
||||
// 检查交易状态
|
||||
if tradeState != payment.TradeStateSuccess {
|
||||
s.logger.Warn("微信支付交易未成功",
|
||||
s.logger.Warn("微信支付交易未成功,跳过处理",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("trade_state", tradeState),
|
||||
)
|
||||
return nil // 不返回错误,因为这是正常的业务状态
|
||||
}
|
||||
|
||||
// 处理支付成功逻辑
|
||||
s.logger.Info("微信支付成功,开始处理业务逻辑",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("transaction_id", transactionID),
|
||||
)
|
||||
|
||||
// 先检查是否是组件报告下载的支付订单
|
||||
s.logger.Info("步骤1: 检查是否是组件报告下载订单",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
)
|
||||
|
||||
// 检查组件报告下载记录
|
||||
download, err := s.componentReportRepo.GetDownloadByPaymentOrderID(ctx, outTradeNo)
|
||||
if err == nil && download != nil {
|
||||
s.logger.Info("步骤2: 发现组件报告下载订单,直接更新下载记录状态",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("download_id", download.ID),
|
||||
zap.String("product_id", download.ProductID),
|
||||
zap.String("current_status", download.PaymentStatus),
|
||||
)
|
||||
s.updateComponentReportDownloadStatus(ctx, outTradeNo)
|
||||
s.logger.Info("========== 组件报告下载订单处理完成 ==========")
|
||||
return nil
|
||||
}
|
||||
|
||||
s.logger.Info("步骤3: 不是组件报告下载订单,按充值流程处理",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
)
|
||||
|
||||
// 处理支付成功逻辑(充值流程)
|
||||
err = s.processWechatPaymentSuccess(ctx, outTradeNo, transactionID, totalAmount)
|
||||
if err != nil {
|
||||
s.logger.Error("处理微信支付成功失败",
|
||||
@@ -1458,6 +1512,7 @@ func (s *FinanceApplicationServiceImpl) HandleWechatPayCallback(ctx context.Cont
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("========== 微信支付回调处理完成 ==========")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1491,6 +1546,15 @@ func (s *FinanceApplicationServiceImpl) processWechatPaymentSuccess(ctx context.
|
||||
return fmt.Errorf("查找充值记录失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("步骤4: 检查充值记录备注,判断是否为组件报告下载订单",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("recharge_id", rechargeRecord.ID),
|
||||
zap.String("notes", rechargeRecord.Notes),
|
||||
)
|
||||
|
||||
// 检查是否是组件报告下载订单(通过备注判断)
|
||||
isComponentReportOrder := strings.Contains(rechargeRecord.Notes, "购买") && strings.Contains(rechargeRecord.Notes, "报告示例")
|
||||
|
||||
// 检查订单和充值记录状态,如果都已成功则跳过(只记录一次日志)
|
||||
if wechatOrder.Status == finance_entities.WechatOrderStatusSuccess && rechargeRecord.Status == finance_entities.RechargeStatusSuccess {
|
||||
s.logger.Info("微信支付订单已处理成功,跳过重复处理",
|
||||
@@ -1498,7 +1562,12 @@ func (s *FinanceApplicationServiceImpl) processWechatPaymentSuccess(ctx context.
|
||||
zap.String("transaction_id", transactionID),
|
||||
zap.String("order_id", wechatOrder.ID),
|
||||
zap.String("recharge_id", rechargeRecord.ID),
|
||||
zap.Bool("is_component_report", isComponentReportOrder),
|
||||
)
|
||||
// 如果是组件报告下载订单,确保更新下载记录状态
|
||||
if isComponentReportOrder {
|
||||
s.updateComponentReportDownloadStatus(ctx, outTradeNo)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1538,9 +1607,8 @@ func (s *FinanceApplicationServiceImpl) processWechatPaymentSuccess(ctx context.
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新充值记录状态为成功
|
||||
rechargeRecord.MarkSuccess()
|
||||
err = s.rechargeRecordRepo.Update(txCtx, *rechargeRecord)
|
||||
// 更新充值记录状态为成功(使用UpdateStatus方法直接更新状态字段)
|
||||
err = s.rechargeRecordRepo.UpdateStatus(txCtx, rechargeRecord.ID, finance_entities.RechargeStatusSuccess)
|
||||
if err != nil {
|
||||
s.logger.Error("更新充值记录状态失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
@@ -1570,17 +1638,33 @@ func (s *FinanceApplicationServiceImpl) processWechatPaymentSuccess(ctx context.
|
||||
)
|
||||
}
|
||||
|
||||
// 充值到钱包(包含赠送金额)
|
||||
totalRechargeAmount := amount.Add(bonusAmount)
|
||||
err = s.walletService.Recharge(txCtx, rechargeRecord.UserID, totalRechargeAmount)
|
||||
if err != nil {
|
||||
s.logger.Error("充值到钱包失败",
|
||||
// 检查是否是组件报告下载订单(通过备注判断)
|
||||
isComponentReportOrder := strings.Contains(rechargeRecord.Notes, "购买") && strings.Contains(rechargeRecord.Notes, "报告示例")
|
||||
|
||||
if isComponentReportOrder {
|
||||
s.logger.Info("步骤5: 检测到组件报告下载订单,不增加钱包余额",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("user_id", rechargeRecord.UserID),
|
||||
zap.String("total_amount", totalRechargeAmount.String()),
|
||||
zap.Error(err),
|
||||
zap.String("recharge_id", rechargeRecord.ID),
|
||||
zap.String("notes", rechargeRecord.Notes),
|
||||
)
|
||||
return err
|
||||
// 组件报告下载订单不增加钱包余额,只更新订单和充值记录状态
|
||||
} else {
|
||||
s.logger.Info("步骤5: 普通充值订单,增加钱包余额",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("recharge_id", rechargeRecord.ID),
|
||||
)
|
||||
// 充值到钱包(包含赠送金额)
|
||||
totalRechargeAmount := amount.Add(bonusAmount)
|
||||
err = s.walletService.Recharge(txCtx, rechargeRecord.UserID, totalRechargeAmount)
|
||||
if err != nil {
|
||||
s.logger.Error("充值到钱包失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("user_id", rechargeRecord.UserID),
|
||||
zap.String("total_amount", totalRechargeAmount.String()),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -1596,17 +1680,107 @@ func (s *FinanceApplicationServiceImpl) processWechatPaymentSuccess(ctx context.
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果是组件报告下载订单,更新下载记录状态
|
||||
if isComponentReportOrder {
|
||||
s.logger.Info("步骤6: 更新组件报告下载记录状态",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
)
|
||||
s.updateComponentReportDownloadStatus(ctx, outTradeNo)
|
||||
}
|
||||
|
||||
s.logger.Info("微信支付成功处理完成",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("transaction_id", transactionID),
|
||||
zap.String("amount", amount.String()),
|
||||
zap.String("bonus_amount", bonusAmount.String()),
|
||||
zap.String("user_id", rechargeRecord.UserID),
|
||||
zap.Bool("is_component_report", isComponentReportOrder),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateComponentReportDownloadStatus 更新组件报告下载记录状态
|
||||
func (s *FinanceApplicationServiceImpl) updateComponentReportDownloadStatus(ctx context.Context, outTradeNo string) {
|
||||
s.logger.Info("========== 开始更新组件报告下载记录状态 ==========",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
)
|
||||
|
||||
if s.componentReportRepo == nil {
|
||||
s.logger.Warn("组件报告下载Repository未初始化,跳过更新")
|
||||
return
|
||||
}
|
||||
|
||||
// 根据支付订单号查找组件报告下载记录
|
||||
download, err := s.componentReportRepo.GetDownloadByPaymentOrderID(ctx, outTradeNo)
|
||||
if err != nil {
|
||||
s.logger.Info("未找到组件报告下载记录,可能不是组件报告下载订单",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if download == nil {
|
||||
s.logger.Info("组件报告下载记录为空,跳过更新",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
s.logger.Info("步骤1: 找到组件报告下载记录",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("download_id", download.ID),
|
||||
zap.String("product_id", download.ProductID),
|
||||
zap.String("current_status", download.PaymentStatus),
|
||||
)
|
||||
|
||||
// 如果已经是成功状态,跳过
|
||||
if download.PaymentStatus == "success" {
|
||||
s.logger.Info("组件报告下载记录已是成功状态,跳过更新",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("download_id", download.ID),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
s.logger.Info("步骤2: 更新支付状态为成功",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("download_id", download.ID),
|
||||
)
|
||||
|
||||
// 更新支付状态为成功
|
||||
download.PaymentStatus = "success"
|
||||
|
||||
// 设置过期时间(30天后)
|
||||
expiresAt := time.Now().Add(30 * 24 * time.Hour)
|
||||
download.ExpiresAt = &expiresAt
|
||||
|
||||
s.logger.Info("步骤3: 保存更新后的下载记录",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("download_id", download.ID),
|
||||
zap.String("expires_at", expiresAt.Format("2006-01-02 15:04:05")),
|
||||
)
|
||||
|
||||
// 更新记录
|
||||
err = s.componentReportRepo.UpdateDownload(ctx, download)
|
||||
if err != nil {
|
||||
s.logger.Error("更新组件报告下载记录状态失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("download_id", download.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
s.logger.Info("========== 组件报告下载记录状态更新成功 ==========",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("download_id", download.ID),
|
||||
zap.String("product_id", download.ProductID),
|
||||
zap.String("payment_status", download.PaymentStatus),
|
||||
)
|
||||
}
|
||||
|
||||
// HandleWechatRefundCallback 处理微信退款回调
|
||||
func (s *FinanceApplicationServiceImpl) HandleWechatRefundCallback(ctx context.Context, r *http.Request) error {
|
||||
if s.wechatPayService == nil {
|
||||
|
||||
686
internal/application/product/ui_component_application_service.go
Normal file
686
internal/application/product/ui_component_application_service.go
Normal file
@@ -0,0 +1,686 @@
|
||||
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)
|
||||
}
|
||||
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组件关联不存在")
|
||||
)
|
||||
341
internal/application/product/ui_component_file_service.go
Normal file
341
internal/application/product/ui_component_file_service.go
Normal file
@@ -0,0 +1,341 @@
|
||||
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)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if !s.FolderExists(folderPath) {
|
||||
return nil // 文件夹不存在,不视为错误
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(folderPath); err != nil {
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user