This commit is contained in:
2025-12-19 17:05:09 +08:00
parent cc3472ff40
commit 39c46937ea
307 changed files with 87686 additions and 129 deletions

View File

@@ -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 {

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

View 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组件关联不存在")
)

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