addui
This commit is contained in:
@@ -239,6 +239,9 @@ func (a *Application) autoMigrate(db *gorm.DB) error {
|
||||
&productEntities.Subscription{},
|
||||
&productEntities.ProductDocumentation{},
|
||||
&productEntities.ProductApiConfig{},
|
||||
&productEntities.ComponentReportDownload{},
|
||||
&productEntities.UIComponent{},
|
||||
&productEntities.ProductUIComponent{},
|
||||
|
||||
// 文章域
|
||||
&articleEntities.Article{},
|
||||
@@ -281,6 +284,7 @@ func createLogger(cfg *config.Config) (*zap.Logger, error) {
|
||||
if cfg.Logger.Format == "" {
|
||||
config.Encoding = "json"
|
||||
}
|
||||
|
||||
if cfg.Logger.Output == "" {
|
||||
config.OutputPaths = []string{"stdout"}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -55,6 +55,7 @@ import (
|
||||
asynq "tyapi-server/internal/infrastructure/task/implementations/asynq"
|
||||
task_interfaces "tyapi-server/internal/infrastructure/task/interfaces"
|
||||
task_repositories "tyapi-server/internal/infrastructure/task/repositories"
|
||||
component_report "tyapi-server/internal/shared/component_report"
|
||||
shared_database "tyapi-server/internal/shared/database"
|
||||
"tyapi-server/internal/shared/esign"
|
||||
shared_events "tyapi-server/internal/shared/events"
|
||||
@@ -565,6 +566,21 @@ func NewContainer() *Container {
|
||||
product_repo.NewGormProductDocumentationRepository,
|
||||
fx.As(new(domain_product_repo.ProductDocumentationRepository)),
|
||||
),
|
||||
// 组件报告下载记录仓储
|
||||
fx.Annotate(
|
||||
product_repo.NewGormComponentReportRepository,
|
||||
fx.As(new(domain_product_repo.ComponentReportRepository)),
|
||||
),
|
||||
// UI组件仓储 - 同时注册具体类型和接口类型
|
||||
fx.Annotate(
|
||||
product_repo.NewGormUIComponentRepository,
|
||||
fx.As(new(domain_product_repo.UIComponentRepository)),
|
||||
),
|
||||
// 产品UI组件关联仓储 - 同时注册具体类型和接口类型
|
||||
fx.Annotate(
|
||||
product_repo.NewGormProductUIComponentRepository,
|
||||
fx.As(new(domain_product_repo.ProductUIComponentRepository)),
|
||||
),
|
||||
),
|
||||
|
||||
// 仓储层 - 文章域
|
||||
@@ -882,6 +898,7 @@ func NewContainer() *Container {
|
||||
logger *zap.Logger,
|
||||
config *config.Config,
|
||||
exportManager *export.ExportManager,
|
||||
componentReportRepo domain_product_repo.ComponentReportRepository,
|
||||
) finance.FinanceApplicationService {
|
||||
return finance.NewFinanceApplicationService(
|
||||
aliPayClient,
|
||||
@@ -892,6 +909,7 @@ func NewContainer() *Container {
|
||||
alipayOrderRepo,
|
||||
wechatOrderRepo,
|
||||
rechargeRecordRepo,
|
||||
componentReportRepo,
|
||||
userRepo,
|
||||
txManager,
|
||||
logger,
|
||||
@@ -1028,6 +1046,27 @@ func NewContainer() *Container {
|
||||
},
|
||||
fx.As(new(statistics.StatisticsApplicationService)),
|
||||
),
|
||||
// UI组件应用服务 - 绑定到接口
|
||||
fx.Annotate(
|
||||
func(
|
||||
uiComponentRepo domain_product_repo.UIComponentRepository,
|
||||
productUIComponentRepo domain_product_repo.ProductUIComponentRepository,
|
||||
fileStorageService *storage.LocalFileStorageService,
|
||||
logger *zap.Logger,
|
||||
) product.UIComponentApplicationService {
|
||||
// 创建UI组件文件服务
|
||||
basePath := "resources/Pure Component/src/ui"
|
||||
fileService := product.NewUIComponentFileService(basePath, logger)
|
||||
|
||||
return product.NewUIComponentApplicationService(
|
||||
uiComponentRepo,
|
||||
productUIComponentRepo,
|
||||
fileStorageService,
|
||||
fileService,
|
||||
)
|
||||
},
|
||||
fx.As(new(product.UIComponentApplicationService)),
|
||||
),
|
||||
),
|
||||
|
||||
// PDF查找服务
|
||||
@@ -1086,6 +1125,24 @@ func NewContainer() *Container {
|
||||
return cacheManager, nil
|
||||
},
|
||||
),
|
||||
// 本地文件存储服务
|
||||
fx.Provide(
|
||||
func(logger *zap.Logger) *storage.LocalFileStorageService {
|
||||
// 使用默认配置:基础存储目录在项目根目录下的storage目录
|
||||
basePath := "storage"
|
||||
|
||||
// 可以通过环境变量覆盖
|
||||
if envBasePath := os.Getenv("FILE_STORAGE_BASE_PATH"); envBasePath != "" {
|
||||
basePath = envBasePath
|
||||
}
|
||||
|
||||
logger.Info("本地文件存储服务已初始化",
|
||||
zap.String("base_path", basePath),
|
||||
)
|
||||
|
||||
return storage.NewLocalFileStorageService(basePath, logger)
|
||||
},
|
||||
),
|
||||
// HTTP处理器
|
||||
fx.Provide(
|
||||
// 用户HTTP处理器
|
||||
@@ -1120,6 +1177,30 @@ func NewContainer() *Container {
|
||||
) *handlers.AnnouncementHandler {
|
||||
return handlers.NewAnnouncementHandler(appService, responseBuilder, validator, logger)
|
||||
},
|
||||
// 组件报告处理器
|
||||
func(
|
||||
productRepo domain_product_repo.ProductRepository,
|
||||
docRepo domain_product_repo.ProductDocumentationRepository,
|
||||
apiConfigRepo domain_product_repo.ProductApiConfigRepository,
|
||||
componentReportRepo domain_product_repo.ComponentReportRepository,
|
||||
rechargeRecordRepo domain_finance_repo.RechargeRecordRepository,
|
||||
alipayOrderRepo domain_finance_repo.AlipayOrderRepository,
|
||||
wechatOrderRepo domain_finance_repo.WechatOrderRepository,
|
||||
aliPayService *payment.AliPayService,
|
||||
wechatPayService *payment.WechatPayService,
|
||||
logger *zap.Logger,
|
||||
) *component_report.ComponentReportHandler {
|
||||
return component_report.NewComponentReportHandler(productRepo, docRepo, apiConfigRepo, componentReportRepo, rechargeRecordRepo, alipayOrderRepo, wechatOrderRepo, aliPayService, wechatPayService, logger)
|
||||
},
|
||||
// UI组件HTTP处理器
|
||||
func(
|
||||
uiComponentAppService product.UIComponentApplicationService,
|
||||
responseBuilder interfaces.ResponseBuilder,
|
||||
validator interfaces.RequestValidator,
|
||||
logger *zap.Logger,
|
||||
) *handlers.UIComponentHandler {
|
||||
return handlers.NewUIComponentHandler(uiComponentAppService, responseBuilder, validator, logger)
|
||||
},
|
||||
),
|
||||
|
||||
// 路由注册
|
||||
@@ -1134,6 +1215,8 @@ func NewContainer() *Container {
|
||||
routes.NewProductRoutes,
|
||||
// 产品管理员路由
|
||||
routes.NewProductAdminRoutes,
|
||||
// UI组件路由
|
||||
routes.NewUIComponentRoutes,
|
||||
// 文章路由
|
||||
routes.NewArticleRoutes,
|
||||
// 公告路由
|
||||
@@ -1248,10 +1331,13 @@ func RegisterRoutes(
|
||||
financeRoutes *routes.FinanceRoutes,
|
||||
productRoutes *routes.ProductRoutes,
|
||||
productAdminRoutes *routes.ProductAdminRoutes,
|
||||
uiComponentRoutes *routes.UIComponentRoutes,
|
||||
articleRoutes *routes.ArticleRoutes,
|
||||
announcementRoutes *routes.AnnouncementRoutes,
|
||||
apiRoutes *routes.ApiRoutes,
|
||||
statisticsRoutes *routes.StatisticsRoutes,
|
||||
jwtAuth *middleware.JWTAuthMiddleware,
|
||||
adminAuth *middleware.AdminAuthMiddleware,
|
||||
cfg *config.Config,
|
||||
logger *zap.Logger,
|
||||
) {
|
||||
@@ -1266,6 +1352,13 @@ func RegisterRoutes(
|
||||
financeRoutes.Register(router)
|
||||
productRoutes.Register(router)
|
||||
productAdminRoutes.Register(router)
|
||||
|
||||
// UI组件路由需要特殊处理,因为它需要管理员中间件
|
||||
engine := router.GetEngine()
|
||||
adminGroup := engine.Group("/api/v1/admin")
|
||||
adminGroup.Use(adminAuth.Handle())
|
||||
uiComponentRoutes.RegisterRoutes(adminGroup, adminAuth)
|
||||
|
||||
articleRoutes.Register(router)
|
||||
announcementRoutes.Register(router)
|
||||
statisticsRoutes.Register(router)
|
||||
|
||||
@@ -33,8 +33,8 @@ func ProcessIVYZ81NCRequest(ctx context.Context, params []byte, deps *processors
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"name": encryptedName,
|
||||
"idcard": encryptedIDCard,
|
||||
"name": encryptedName,
|
||||
"idcard": encryptedIDCard,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -62,4 +62,4 @@ func ProcessIVYZ81NCRequest(ctx context.Context, params []byte, deps *processors
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,23 +165,35 @@ func (r *RechargeRecord) SetTransferOrderID(orderID string) {
|
||||
|
||||
// NewAlipayRechargeRecord 工厂方法 - 创建支付宝充值记录
|
||||
func NewAlipayRechargeRecord(userID string, amount decimal.Decimal, alipayOrderID string) *RechargeRecord {
|
||||
return NewAlipayRechargeRecordWithNotes(userID, amount, alipayOrderID, "")
|
||||
}
|
||||
|
||||
// NewAlipayRechargeRecordWithNotes 工厂方法 - 创建支付宝充值记录(带备注)
|
||||
func NewAlipayRechargeRecordWithNotes(userID string, amount decimal.Decimal, alipayOrderID, notes string) *RechargeRecord {
|
||||
return &RechargeRecord{
|
||||
UserID: userID,
|
||||
Amount: amount,
|
||||
RechargeType: RechargeTypeAlipay,
|
||||
Status: RechargeStatusPending,
|
||||
AlipayOrderID: &alipayOrderID,
|
||||
Notes: notes,
|
||||
}
|
||||
}
|
||||
|
||||
// NewWechatRechargeRecord 工厂方法 - 创建微信充值记录
|
||||
func NewWechatRechargeRecord(userID string, amount decimal.Decimal, wechatOrderID string) *RechargeRecord {
|
||||
return NewWechatRechargeRecordWithNotes(userID, amount, wechatOrderID, "")
|
||||
}
|
||||
|
||||
// NewWechatRechargeRecordWithNotes 工厂方法 - 创建微信充值记录(带备注)
|
||||
func NewWechatRechargeRecordWithNotes(userID string, amount decimal.Decimal, wechatOrderID, notes string) *RechargeRecord {
|
||||
return &RechargeRecord{
|
||||
UserID: userID,
|
||||
Amount: amount,
|
||||
RechargeType: RechargeTypeWechat,
|
||||
Status: RechargeStatusPending,
|
||||
WechatOrderID: &wechatOrderID,
|
||||
Notes: notes,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package services
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"go.uber.org/zap"
|
||||
@@ -295,8 +296,21 @@ func (s *RechargeRecordServiceImpl) HandleAlipayPaymentSuccess(ctx context.Conte
|
||||
return nil
|
||||
}
|
||||
|
||||
// 计算充值赠送金额
|
||||
bonusAmount := calculateAlipayRechargeBonus(amount, &s.cfg.Wallet)
|
||||
// 检查是否是组件报告下载订单(通过备注判断)
|
||||
isComponentReportOrder := strings.Contains(rechargeRecord.Notes, "购买") && strings.Contains(rechargeRecord.Notes, "报告示例")
|
||||
|
||||
s.logger.Info("处理支付宝支付成功回调",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("recharge_id", rechargeRecord.ID),
|
||||
zap.String("notes", rechargeRecord.Notes),
|
||||
zap.Bool("is_component_report", isComponentReportOrder),
|
||||
)
|
||||
|
||||
// 计算充值赠送金额(组件报告下载订单不需要赠送)
|
||||
bonusAmount := decimal.Zero
|
||||
if !isComponentReportOrder {
|
||||
bonusAmount = calculateAlipayRechargeBonus(amount, &s.cfg.Wallet)
|
||||
}
|
||||
totalAmount := amount.Add(bonusAmount)
|
||||
|
||||
// 在事务中执行所有更新操作
|
||||
@@ -309,14 +323,22 @@ func (s *RechargeRecordServiceImpl) HandleAlipayPaymentSuccess(ctx context.Conte
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新充值记录状态为成功
|
||||
rechargeRecord.MarkSuccess()
|
||||
err = s.rechargeRecordRepo.Update(txCtx, rechargeRecord)
|
||||
// 更新充值记录状态为成功(使用UpdateStatus方法直接更新状态字段)
|
||||
err = s.rechargeRecordRepo.UpdateStatus(txCtx, rechargeRecord.ID, entities.RechargeStatusSuccess)
|
||||
if err != nil {
|
||||
s.logger.Error("更新充值记录状态失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果是组件报告下载订单,不增加钱包余额,不创建赠送记录
|
||||
if isComponentReportOrder {
|
||||
s.logger.Info("组件报告下载订单,跳过钱包余额增加和赠送",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("recharge_id", rechargeRecord.ID),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 如果有赠送金额,创建赠送充值记录
|
||||
if bonusAmount.GreaterThan(decimal.Zero) {
|
||||
giftRechargeRecord := entities.NewGiftRechargeRecord(rechargeRecord.UserID, bonusAmount, "充值活动赠送")
|
||||
@@ -355,6 +377,10 @@ func (s *RechargeRecordServiceImpl) HandleAlipayPaymentSuccess(ctx context.Conte
|
||||
zap.String("recharge_id", rechargeRecord.ID),
|
||||
zap.String("order_id", alipayOrder.ID))
|
||||
|
||||
// 检查是否有组件报告下载记录需要更新
|
||||
// 注意:这里需要在调用方(finance应用服务)中处理,因为这里没有组件报告下载的repository
|
||||
// 但为了保持服务层的独立性,我们通过事件或回调来处理
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ComponentReportCache 报告文件匹配缓存
|
||||
type ComponentReportCache struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"缓存ID"`
|
||||
ProductCode string `gorm:"type:varchar(50);not null;uniqueIndex" json:"product_code" comment:"产品编号"`
|
||||
MatchedPath string `gorm:"type:varchar(500);not null" json:"matched_path" comment:"匹配到的文件夹/文件路径"`
|
||||
FileType string `gorm:"type:varchar(20);not null" json:"file_type" comment:"文件类型:folder, file"`
|
||||
CacheKey string `gorm:"type:varchar(64);not null;uniqueIndex" json:"cache_key" comment:"缓存键"`
|
||||
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
|
||||
}
|
||||
|
||||
// TableName 指定数据库表名
|
||||
func (ComponentReportCache) TableName() string {
|
||||
return "component_report_cache"
|
||||
}
|
||||
|
||||
// BeforeCreate GORM钩子:创建前自动生成UUID
|
||||
func (c *ComponentReportCache) BeforeCreate(tx *gorm.DB) error {
|
||||
if c.ID == "" {
|
||||
c.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -10,30 +10,30 @@ import (
|
||||
|
||||
// ComponentReportDownload 组件报告下载记录
|
||||
type ComponentReportDownload struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"下载记录ID"`
|
||||
UserID string `gorm:"type:varchar(36);not null;index" json:"user_id" comment:"用户ID"`
|
||||
ProductID string `gorm:"type:varchar(36);not null;index" json:"product_id" comment:"产品ID"`
|
||||
ProductCode string `gorm:"type:varchar(50);not null;index" json:"product_code" comment:"产品编号"`
|
||||
SubProductIDs string `gorm:"type:text" json:"sub_product_ids" comment:"子产品ID列表(JSON数组)"`
|
||||
SubProductCodes string `gorm:"type:text" json:"sub_product_codes" comment:"子产品编号列表(JSON数组)"`
|
||||
DownloadPrice decimal.Decimal `gorm:"type:decimal(10,2);not null" json:"download_price" comment:"实际支付价格"`
|
||||
OriginalPrice decimal.Decimal `gorm:"type:decimal(10,2);not null" json:"original_price" comment:"原始总价"`
|
||||
DiscountAmount decimal.Decimal `gorm:"type:decimal(10,2);default:0" json:"discount_amount" comment:"减免金额"`
|
||||
PaymentOrderID *string `gorm:"type:varchar(64)" json:"payment_order_id,omitempty" comment:"支付订单号"`
|
||||
PaymentType *string `gorm:"type:varchar(20)" json:"payment_type,omitempty" comment:"支付类型:alipay, wechat"`
|
||||
PaymentStatus string `gorm:"type:varchar(20);default:'pending';index" json:"payment_status" comment:"支付状态:pending, success, failed"`
|
||||
FilePath *string `gorm:"type:varchar(500)" json:"file_path,omitempty" comment:"生成的ZIP文件路径"`
|
||||
FileHash *string `gorm:"type:varchar(64)" json:"file_hash,omitempty" comment:"文件哈希值"`
|
||||
DownloadCount int `gorm:"default:0" json:"download_count" comment:"下载次数"`
|
||||
LastDownloadAt *time.Time `json:"last_download_at,omitempty" comment:"最后下载时间"`
|
||||
ExpiresAt *time.Time `gorm:"index" json:"expires_at,omitempty" comment:"下载有效期"`
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" comment:"下载记录ID"`
|
||||
UserID string `gorm:"type:varchar(36);not null;index" comment:"用户ID"`
|
||||
ProductID string `gorm:"type:varchar(36);not null;index" comment:"产品ID"`
|
||||
ProductCode string `gorm:"type:varchar(50);not null;index" comment:"产品编号"`
|
||||
SubProductIDs string `gorm:"type:text" comment:"子产品ID列表(JSON数组,组合包使用)"`
|
||||
SubProductCodes string `gorm:"type:text" comment:"子产品编号列表(JSON数组)"`
|
||||
DownloadPrice decimal.Decimal `gorm:"type:decimal(10,2);not null" comment:"实际支付价格"`
|
||||
OriginalPrice decimal.Decimal `gorm:"type:decimal(10,2);not null" comment:"原始总价"`
|
||||
DiscountAmount decimal.Decimal `gorm:"type:decimal(10,2);default:0" comment:"减免金额"`
|
||||
PaymentOrderID *string `gorm:"type:varchar(64);index" comment:"支付订单号(关联充值记录)"`
|
||||
PaymentType *string `gorm:"type:varchar(20)" comment:"支付类型:alipay, wechat"`
|
||||
PaymentStatus string `gorm:"type:varchar(20);default:'pending';index" comment:"支付状态:pending, success, failed"`
|
||||
FilePath *string `gorm:"type:varchar(500)" comment:"生成的ZIP文件路径(用于二次下载)"`
|
||||
FileHash *string `gorm:"type:varchar(64)" comment:"文件哈希值(用于缓存验证)"`
|
||||
DownloadCount int `gorm:"default:0" comment:"下载次数"`
|
||||
LastDownloadAt *time.Time `comment:"最后下载时间"`
|
||||
ExpiresAt *time.Time `gorm:"index" comment:"下载有效期(支付成功后30天)"`
|
||||
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" comment:"创建时间"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" comment:"更新时间"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" comment:"软删除时间"`
|
||||
}
|
||||
|
||||
// TableName 指定数据库表名
|
||||
// TableName 指定表名
|
||||
func (ComponentReportDownload) TableName() string {
|
||||
return "component_report_downloads"
|
||||
}
|
||||
@@ -46,8 +46,8 @@ func (c *ComponentReportDownload) BeforeCreate(tx *gorm.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsPaymentSuccess 检查是否支付成功
|
||||
func (c *ComponentReportDownload) IsPaymentSuccess() bool {
|
||||
// IsPaid 检查是否已支付
|
||||
func (c *ComponentReportDownload) IsPaid() bool {
|
||||
return c.PaymentStatus == "success"
|
||||
}
|
||||
|
||||
@@ -61,25 +61,5 @@ func (c *ComponentReportDownload) IsExpired() bool {
|
||||
|
||||
// CanDownload 检查是否可以下载
|
||||
func (c *ComponentReportDownload) CanDownload() bool {
|
||||
return c.IsPaymentSuccess() && !c.IsExpired()
|
||||
return c.IsPaid() && !c.IsExpired()
|
||||
}
|
||||
|
||||
// MarkPaymentSuccess 标记支付成功
|
||||
func (c *ComponentReportDownload) MarkPaymentSuccess(orderID string, paymentType string) {
|
||||
c.PaymentOrderID = &orderID
|
||||
paymentTypeStr := paymentType
|
||||
c.PaymentType = &paymentTypeStr
|
||||
c.PaymentStatus = "success"
|
||||
|
||||
// 设置30天有效期
|
||||
expiresAt := time.Now().Add(30 * 24 * time.Hour)
|
||||
c.ExpiresAt = &expiresAt
|
||||
}
|
||||
|
||||
// IncrementDownloadCount 增加下载次数
|
||||
func (c *ComponentReportDownload) IncrementDownloadCount() {
|
||||
c.DownloadCount++
|
||||
now := time.Now()
|
||||
c.LastDownloadAt = &now
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,9 @@ type Product struct {
|
||||
IsPackage bool `gorm:"default:false" comment:"是否组合包"`
|
||||
// 组合包相关关联
|
||||
PackageItems []*ProductPackageItem `gorm:"foreignKey:PackageID" comment:"组合包项目列表"`
|
||||
// UI组件相关字段
|
||||
SellUIComponent bool `gorm:"default:false" comment:"是否出售UI组件"`
|
||||
UIComponentPrice decimal.Decimal `gorm:"type:decimal(10,2);default:0" comment:"UI组件销售价格(组合包使用)"`
|
||||
// SEO信息
|
||||
SEOTitle string `gorm:"type:varchar(200)" comment:"SEO标题"`
|
||||
SEODescription string `gorm:"type:text" comment:"SEO描述"`
|
||||
|
||||
36
internal/domains/product/entities/product_ui_component.go
Normal file
36
internal/domains/product/entities/product_ui_component.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ProductUIComponent 产品UI组件关联实体
|
||||
type ProductUIComponent struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" comment:"关联ID"`
|
||||
ProductID string `gorm:"type:varchar(36);not null;index" comment:"产品ID"`
|
||||
UIComponentID string `gorm:"type:varchar(36);not null;index" comment:"UI组件ID"`
|
||||
Price decimal.Decimal `gorm:"type:decimal(10,2);not null;default:0" comment:"销售价格"`
|
||||
IsEnabled bool `gorm:"default:true" comment:"是否启用销售"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" comment:"创建时间"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" comment:"更新时间"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" comment:"软删除时间"`
|
||||
|
||||
// 关联关系
|
||||
Product *Product `gorm:"foreignKey:ProductID" comment:"产品"`
|
||||
UIComponent *UIComponent `gorm:"foreignKey:UIComponentID" comment:"UI组件"`
|
||||
}
|
||||
|
||||
func (ProductUIComponent) TableName() string {
|
||||
return "product_ui_components"
|
||||
}
|
||||
|
||||
func (p *ProductUIComponent) BeforeCreate(tx *gorm.DB) error {
|
||||
if p.ID == "" {
|
||||
p.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
39
internal/domains/product/entities/ui_component.go
Normal file
39
internal/domains/product/entities/ui_component.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// UIComponent UI组件实体
|
||||
type UIComponent struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"组件ID"`
|
||||
ComponentCode string `gorm:"type:varchar(50);not null;uniqueIndex" json:"component_code" comment:"组件编码"`
|
||||
ComponentName string `gorm:"type:varchar(100);not null" json:"component_name" comment:"组件名称"`
|
||||
Description string `gorm:"type:text" json:"description" comment:"组件描述"`
|
||||
FilePath *string `gorm:"type:varchar(500)" json:"file_path" comment:"组件文件路径"`
|
||||
FileHash *string `gorm:"type:varchar(64)" json:"file_hash" comment:"文件哈希值"`
|
||||
FileSize *int64 `gorm:"type:bigint" json:"file_size" comment:"文件大小"`
|
||||
FileType *string `gorm:"type:varchar(50)" json:"file_type" comment:"文件类型"`
|
||||
FolderPath *string `gorm:"type:varchar(500)" json:"folder_path" comment:"组件文件夹路径"`
|
||||
IsExtracted bool `gorm:"default:false" json:"is_extracted" comment:"是否已解压"`
|
||||
Version string `gorm:"type:varchar(20)" json:"version" comment:"组件版本"`
|
||||
IsActive bool `gorm:"default:true" json:"is_active" comment:"是否启用"`
|
||||
SortOrder int `gorm:"default:0" json:"sort_order" comment:"排序"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at" comment:"软删除时间"`
|
||||
}
|
||||
|
||||
func (UIComponent) TableName() string {
|
||||
return "ui_components"
|
||||
}
|
||||
|
||||
func (u *UIComponent) BeforeCreate(tx *gorm.DB) error {
|
||||
if u.ID == "" {
|
||||
u.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -2,23 +2,31 @@ package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"tyapi-server/internal/domains/product/entities"
|
||||
)
|
||||
|
||||
// ComponentReportRepository 组件报告仓储接口
|
||||
type ComponentReportRepository interface {
|
||||
// 下载记录相关
|
||||
// 创建下载记录
|
||||
CreateDownload(ctx context.Context, download *entities.ComponentReportDownload) (*entities.ComponentReportDownload, error)
|
||||
|
||||
// 更新下载记录
|
||||
UpdateDownload(ctx context.Context, download *entities.ComponentReportDownload) error
|
||||
|
||||
// 根据ID获取下载记录
|
||||
GetDownloadByID(ctx context.Context, id string) (*entities.ComponentReportDownload, error)
|
||||
|
||||
// 获取用户的下载记录列表
|
||||
GetUserDownloads(ctx context.Context, userID string, productID *string) ([]*entities.ComponentReportDownload, error)
|
||||
|
||||
// 检查用户是否已下载过指定产品编号的组件
|
||||
HasUserDownloaded(ctx context.Context, userID string, productCode string) (bool, error)
|
||||
|
||||
// 获取用户已下载的产品编号列表
|
||||
GetUserDownloadedProductCodes(ctx context.Context, userID string) ([]string, error)
|
||||
|
||||
// 根据支付订单号获取下载记录
|
||||
GetDownloadByPaymentOrderID(ctx context.Context, orderID string) (*entities.ComponentReportDownload, error)
|
||||
|
||||
// 缓存相关
|
||||
GetCacheByProductCode(ctx context.Context, productCode string) (*entities.ComponentReportCache, error)
|
||||
CreateCache(ctx context.Context, cache *entities.ComponentReportCache) error
|
||||
UpdateCache(ctx context.Context, cache *entities.ComponentReportCache) error
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"tyapi-server/internal/domains/product/entities"
|
||||
)
|
||||
|
||||
// ProductUIComponentRepository 产品UI组件关联仓储接口
|
||||
type ProductUIComponentRepository interface {
|
||||
Create(ctx context.Context, relation entities.ProductUIComponent) (entities.ProductUIComponent, error)
|
||||
GetByProductID(ctx context.Context, productID string) ([]entities.ProductUIComponent, error)
|
||||
GetByUIComponentID(ctx context.Context, componentID string) ([]entities.ProductUIComponent, error)
|
||||
Delete(ctx context.Context, id string) error
|
||||
DeleteByProductID(ctx context.Context, productID string) error
|
||||
BatchCreate(ctx context.Context, relations []entities.ProductUIComponent) error
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"tyapi-server/internal/domains/product/entities"
|
||||
)
|
||||
|
||||
// UIComponentRepository UI组件仓储接口
|
||||
type UIComponentRepository interface {
|
||||
Create(ctx context.Context, component entities.UIComponent) (entities.UIComponent, error)
|
||||
GetByID(ctx context.Context, id string) (*entities.UIComponent, error)
|
||||
GetByCode(ctx context.Context, code string) (*entities.UIComponent, error)
|
||||
List(ctx context.Context, filters map[string]interface{}) ([]entities.UIComponent, int64, error)
|
||||
Update(ctx context.Context, component entities.UIComponent) error
|
||||
Delete(ctx context.Context, id string) error
|
||||
GetByCodes(ctx context.Context, codes []string) ([]entities.UIComponent, error)
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/product/entities"
|
||||
"tyapi-server/internal/domains/product/repositories"
|
||||
"tyapi-server/internal/shared/database"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
ComponentReportDownloadsTable = "component_report_downloads"
|
||||
)
|
||||
|
||||
type GormComponentReportRepository struct {
|
||||
*database.CachedBaseRepositoryImpl
|
||||
}
|
||||
|
||||
var _ repositories.ComponentReportRepository = (*GormComponentReportRepository)(nil)
|
||||
|
||||
func NewGormComponentReportRepository(db *gorm.DB, logger *zap.Logger) repositories.ComponentReportRepository {
|
||||
return &GormComponentReportRepository{
|
||||
CachedBaseRepositoryImpl: database.NewCachedBaseRepositoryImpl(db, logger, ComponentReportDownloadsTable),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *GormComponentReportRepository) CreateDownload(ctx context.Context, download *entities.ComponentReportDownload) (*entities.ComponentReportDownload, error) {
|
||||
err := r.CreateEntity(ctx, download)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return download, nil
|
||||
}
|
||||
|
||||
func (r *GormComponentReportRepository) UpdateDownload(ctx context.Context, download *entities.ComponentReportDownload) error {
|
||||
return r.UpdateEntity(ctx, download)
|
||||
}
|
||||
|
||||
func (r *GormComponentReportRepository) GetDownloadByID(ctx context.Context, id string) (*entities.ComponentReportDownload, error) {
|
||||
var download entities.ComponentReportDownload
|
||||
err := r.SmartGetByID(ctx, id, &download)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &download, nil
|
||||
}
|
||||
|
||||
func (r *GormComponentReportRepository) GetUserDownloads(ctx context.Context, userID string, productID *string) ([]*entities.ComponentReportDownload, error) {
|
||||
var downloads []entities.ComponentReportDownload
|
||||
query := r.GetDB(ctx).Where("user_id = ? AND payment_status = ?", userID, "success")
|
||||
|
||||
if productID != nil && *productID != "" {
|
||||
query = query.Where("product_id = ?", *productID)
|
||||
}
|
||||
|
||||
err := query.Order("created_at DESC").Find(&downloads).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]*entities.ComponentReportDownload, len(downloads))
|
||||
for i := range downloads {
|
||||
result[i] = &downloads[i]
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *GormComponentReportRepository) HasUserDownloaded(ctx context.Context, userID string, productCode string) (bool, error) {
|
||||
var count int64
|
||||
err := r.GetDB(ctx).Model(&entities.ComponentReportDownload{}).
|
||||
Where("user_id = ? AND product_code = ? AND payment_status = ?", userID, productCode, "success").
|
||||
Count(&count).Error
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (r *GormComponentReportRepository) GetUserDownloadedProductCodes(ctx context.Context, userID string) ([]string, error) {
|
||||
var downloads []entities.ComponentReportDownload
|
||||
err := r.GetDB(ctx).
|
||||
Select("DISTINCT sub_product_codes").
|
||||
Where("user_id = ? AND payment_status = ?", userID, "success").
|
||||
Find(&downloads).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
codesMap := make(map[string]bool)
|
||||
for _, download := range downloads {
|
||||
if download.SubProductCodes != "" {
|
||||
var codes []string
|
||||
if err := json.Unmarshal([]byte(download.SubProductCodes), &codes); err == nil {
|
||||
for _, code := range codes {
|
||||
codesMap[code] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
// 也添加主产品编号
|
||||
if download.ProductCode != "" {
|
||||
codesMap[download.ProductCode] = true
|
||||
}
|
||||
}
|
||||
|
||||
codes := make([]string, 0, len(codesMap))
|
||||
for code := range codesMap {
|
||||
codes = append(codes, code)
|
||||
}
|
||||
return codes, nil
|
||||
}
|
||||
|
||||
func (r *GormComponentReportRepository) GetDownloadByPaymentOrderID(ctx context.Context, orderID string) (*entities.ComponentReportDownload, error) {
|
||||
var download entities.ComponentReportDownload
|
||||
err := r.GetDB(ctx).Where("payment_order_id = ?", orderID).First(&download).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &download, nil
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"tyapi-server/internal/domains/product/entities"
|
||||
"tyapi-server/internal/domains/product/repositories"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GormProductUIComponentRepository 产品UI组件关联仓储实现
|
||||
type GormProductUIComponentRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewGormProductUIComponentRepository 创建产品UI组件关联仓储实例
|
||||
func NewGormProductUIComponentRepository(db *gorm.DB) repositories.ProductUIComponentRepository {
|
||||
return &GormProductUIComponentRepository{db: db}
|
||||
}
|
||||
|
||||
// Create 创建产品UI组件关联
|
||||
func (r *GormProductUIComponentRepository) Create(ctx context.Context, relation entities.ProductUIComponent) (entities.ProductUIComponent, error) {
|
||||
if err := r.db.WithContext(ctx).Create(&relation).Error; err != nil {
|
||||
return entities.ProductUIComponent{}, fmt.Errorf("创建产品UI组件关联失败: %w", err)
|
||||
}
|
||||
return relation, nil
|
||||
}
|
||||
|
||||
// GetByProductID 根据产品ID获取UI组件关联列表
|
||||
func (r *GormProductUIComponentRepository) GetByProductID(ctx context.Context, productID string) ([]entities.ProductUIComponent, error) {
|
||||
var relations []entities.ProductUIComponent
|
||||
if err := r.db.WithContext(ctx).
|
||||
Preload("UIComponent").
|
||||
Where("product_id = ?", productID).
|
||||
Find(&relations).Error; err != nil {
|
||||
return nil, fmt.Errorf("获取产品UI组件关联列表失败: %w", err)
|
||||
}
|
||||
return relations, nil
|
||||
}
|
||||
|
||||
// GetByUIComponentID 根据UI组件ID获取产品关联列表
|
||||
func (r *GormProductUIComponentRepository) GetByUIComponentID(ctx context.Context, componentID string) ([]entities.ProductUIComponent, error) {
|
||||
var relations []entities.ProductUIComponent
|
||||
if err := r.db.WithContext(ctx).
|
||||
Preload("Product").
|
||||
Where("ui_component_id = ?", componentID).
|
||||
Find(&relations).Error; err != nil {
|
||||
return nil, fmt.Errorf("获取UI组件产品关联列表失败: %w", err)
|
||||
}
|
||||
return relations, nil
|
||||
}
|
||||
|
||||
// Delete 删除产品UI组件关联
|
||||
func (r *GormProductUIComponentRepository) Delete(ctx context.Context, id string) error {
|
||||
if err := r.db.WithContext(ctx).Delete(&entities.ProductUIComponent{}, id).Error; err != nil {
|
||||
return fmt.Errorf("删除产品UI组件关联失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteByProductID 根据产品ID删除所有关联
|
||||
func (r *GormProductUIComponentRepository) DeleteByProductID(ctx context.Context, productID string) error {
|
||||
if err := r.db.WithContext(ctx).Where("product_id = ?", productID).Delete(&entities.ProductUIComponent{}).Error; err != nil {
|
||||
return fmt.Errorf("根据产品ID删除UI组件关联失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BatchCreate 批量创建产品UI组件关联
|
||||
func (r *GormProductUIComponentRepository) BatchCreate(ctx context.Context, relations []entities.ProductUIComponent) error {
|
||||
if len(relations) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := r.db.WithContext(ctx).CreateInBatches(relations, 100).Error; err != nil {
|
||||
return fmt.Errorf("批量创建产品UI组件关联失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"tyapi-server/internal/domains/product/entities"
|
||||
"tyapi-server/internal/domains/product/repositories"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GormUIComponentRepository UI组件仓储实现
|
||||
type GormUIComponentRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewGormUIComponentRepository 创建UI组件仓储实例
|
||||
func NewGormUIComponentRepository(db *gorm.DB) repositories.UIComponentRepository {
|
||||
return &GormUIComponentRepository{db: db}
|
||||
}
|
||||
|
||||
// Create 创建UI组件
|
||||
func (r *GormUIComponentRepository) Create(ctx context.Context, component entities.UIComponent) (entities.UIComponent, error) {
|
||||
if err := r.db.WithContext(ctx).Create(&component).Error; err != nil {
|
||||
return entities.UIComponent{}, fmt.Errorf("创建UI组件失败: %w", err)
|
||||
}
|
||||
return component, nil
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取UI组件
|
||||
func (r *GormUIComponentRepository) GetByID(ctx context.Context, id string) (*entities.UIComponent, error) {
|
||||
var component entities.UIComponent
|
||||
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&component).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("获取UI组件失败: %w", err)
|
||||
}
|
||||
return &component, nil
|
||||
}
|
||||
|
||||
// GetByCode 根据编码获取UI组件
|
||||
func (r *GormUIComponentRepository) GetByCode(ctx context.Context, code string) (*entities.UIComponent, error) {
|
||||
var component entities.UIComponent
|
||||
if err := r.db.WithContext(ctx).Where("component_code = ?", code).First(&component).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("获取UI组件失败: %w", err)
|
||||
}
|
||||
return &component, nil
|
||||
}
|
||||
|
||||
// List 获取UI组件列表
|
||||
func (r *GormUIComponentRepository) List(ctx context.Context, filters map[string]interface{}) ([]entities.UIComponent, int64, error) {
|
||||
var components []entities.UIComponent
|
||||
var total int64
|
||||
|
||||
query := r.db.WithContext(ctx).Model(&entities.UIComponent{})
|
||||
|
||||
// 应用过滤条件
|
||||
if isActive, ok := filters["is_active"]; ok {
|
||||
query = query.Where("is_active = ?", isActive)
|
||||
}
|
||||
|
||||
if keyword, ok := filters["keyword"]; ok && keyword != "" {
|
||||
query = query.Where("component_name LIKE ? OR component_code LIKE ? OR description LIKE ?",
|
||||
"%"+keyword.(string)+"%", "%"+keyword.(string)+"%", "%"+keyword.(string)+"%")
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("获取UI组件总数失败: %w", err)
|
||||
}
|
||||
|
||||
// 分页
|
||||
if page, ok := filters["page"]; ok {
|
||||
if pageSize, ok := filters["page_size"]; ok {
|
||||
offset := (page.(int) - 1) * pageSize.(int)
|
||||
query = query.Offset(offset).Limit(pageSize.(int))
|
||||
}
|
||||
}
|
||||
|
||||
// 排序
|
||||
if sortBy, ok := filters["sort_by"]; ok {
|
||||
if sortOrder, ok := filters["sort_order"]; ok {
|
||||
query = query.Order(fmt.Sprintf("%s %s", sortBy, sortOrder))
|
||||
}
|
||||
} else {
|
||||
query = query.Order("sort_order ASC, created_at DESC")
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
if err := query.Find(&components).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("获取UI组件列表失败: %w", err)
|
||||
}
|
||||
|
||||
return components, total, nil
|
||||
}
|
||||
|
||||
// Update 更新UI组件
|
||||
func (r *GormUIComponentRepository) Update(ctx context.Context, component entities.UIComponent) error {
|
||||
if err := r.db.WithContext(ctx).Save(&component).Error; err != nil {
|
||||
return fmt.Errorf("更新UI组件失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除UI组件
|
||||
func (r *GormUIComponentRepository) Delete(ctx context.Context, id string) error {
|
||||
if err := r.db.WithContext(ctx).Delete(&entities.UIComponent{}, id).Error; err != nil {
|
||||
return fmt.Errorf("删除UI组件失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByCodes 根据编码列表获取UI组件
|
||||
func (r *GormUIComponentRepository) GetByCodes(ctx context.Context, codes []string) ([]entities.UIComponent, error) {
|
||||
var components []entities.UIComponent
|
||||
if len(codes) == 0 {
|
||||
return components, nil
|
||||
}
|
||||
|
||||
if err := r.db.WithContext(ctx).Where("component_code IN ?", codes).Find(&components).Error; err != nil {
|
||||
return nil, fmt.Errorf("根据编码列表获取UI组件失败: %w", err)
|
||||
}
|
||||
|
||||
return components, nil
|
||||
}
|
||||
115
internal/infrastructure/external/storage/local_file_storage_service.go
vendored
Normal file
115
internal/infrastructure/external/storage/local_file_storage_service.go
vendored
Normal file
@@ -0,0 +1,115 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// LocalFileStorageService 本地文件存储服务
|
||||
type LocalFileStorageService struct {
|
||||
basePath string
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// LocalFileStorageConfig 本地文件存储配置
|
||||
type LocalFileStorageConfig struct {
|
||||
BasePath string `yaml:"base_path"`
|
||||
}
|
||||
|
||||
// NewLocalFileStorageService 创建本地文件存储服务
|
||||
func NewLocalFileStorageService(basePath string, logger *zap.Logger) *LocalFileStorageService {
|
||||
// 确保基础路径存在
|
||||
if err := os.MkdirAll(basePath, 0755); err != nil {
|
||||
logger.Error("创建基础存储目录失败", zap.Error(err), zap.String("path", basePath))
|
||||
}
|
||||
|
||||
return &LocalFileStorageService{
|
||||
basePath: basePath,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// StoreFile 存储文件
|
||||
func (s *LocalFileStorageService) StoreFile(ctx context.Context, file io.Reader, filename string) (string, error) {
|
||||
// 构建完整文件路径
|
||||
fullPath := filepath.Join(s.basePath, filename)
|
||||
|
||||
// 确保目录存在
|
||||
dir := filepath.Dir(fullPath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
s.logger.Error("创建目录失败", zap.Error(err), zap.String("dir", dir))
|
||||
return "", fmt.Errorf("创建目录失败: %w", err)
|
||||
}
|
||||
|
||||
// 创建文件
|
||||
dst, err := os.Create(fullPath)
|
||||
if err != nil {
|
||||
s.logger.Error("创建文件失败", zap.Error(err), zap.String("path", fullPath))
|
||||
return "", fmt.Errorf("创建文件失败: %w", err)
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
// 复制文件内容
|
||||
if _, err := io.Copy(dst, file); err != nil {
|
||||
s.logger.Error("写入文件失败", zap.Error(err), zap.String("path", fullPath))
|
||||
// 删除部分写入的文件
|
||||
_ = os.Remove(fullPath)
|
||||
return "", fmt.Errorf("写入文件失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("文件存储成功", zap.String("path", fullPath))
|
||||
return fullPath, nil
|
||||
}
|
||||
|
||||
// StoreMultipartFile 存储multipart文件
|
||||
func (s *LocalFileStorageService) StoreMultipartFile(ctx context.Context, file *multipart.FileHeader, filename string) (string, error) {
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("打开上传文件失败: %w", err)
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
return s.StoreFile(ctx, src, filename)
|
||||
}
|
||||
|
||||
// GetFileURL 获取文件URL
|
||||
func (s *LocalFileStorageService) GetFileURL(ctx context.Context, filePath string) (string, error) {
|
||||
// 检查文件是否存在
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("文件不存在: %s", filePath)
|
||||
}
|
||||
|
||||
// 返回文件路径(在实际应用中,这里应该返回可访问的URL)
|
||||
return filePath, nil
|
||||
}
|
||||
|
||||
// DeleteFile 删除文件
|
||||
func (s *LocalFileStorageService) DeleteFile(ctx context.Context, filePath string) error {
|
||||
if err := os.Remove(filePath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// 文件不存在,不视为错误
|
||||
return nil
|
||||
}
|
||||
s.logger.Error("删除文件失败", zap.Error(err), zap.String("path", filePath))
|
||||
return fmt.Errorf("删除文件失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("文件删除成功", zap.String("path", filePath))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetFileReader 获取文件读取器
|
||||
func (s *LocalFileStorageService) GetFileReader(ctx context.Context, filePath string) (io.ReadCloser, error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("打开文件失败: %w", err)
|
||||
}
|
||||
|
||||
return file, nil
|
||||
}
|
||||
110
internal/infrastructure/external/storage/local_file_storage_service_impl.go
vendored
Normal file
110
internal/infrastructure/external/storage/local_file_storage_service_impl.go
vendored
Normal file
@@ -0,0 +1,110 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// LocalFileStorageServiceImpl 本地文件存储服务实现
|
||||
type LocalFileStorageServiceImpl struct {
|
||||
basePath string
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewLocalFileStorageServiceImpl 创建本地文件存储服务实现
|
||||
func NewLocalFileStorageServiceImpl(basePath string, logger *zap.Logger) *LocalFileStorageServiceImpl {
|
||||
// 确保基础路径存在
|
||||
if err := os.MkdirAll(basePath, 0755); err != nil {
|
||||
logger.Error("创建基础存储目录失败", zap.Error(err), zap.String("path", basePath))
|
||||
}
|
||||
|
||||
return &LocalFileStorageServiceImpl{
|
||||
basePath: basePath,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// StoreFile 存储文件
|
||||
func (s *LocalFileStorageServiceImpl) StoreFile(ctx context.Context, file io.Reader, filename string) (string, error) {
|
||||
// 构建完整文件路径
|
||||
fullPath := filepath.Join(s.basePath, filename)
|
||||
|
||||
// 确保目录存在
|
||||
dir := filepath.Dir(fullPath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
s.logger.Error("创建目录失败", zap.Error(err), zap.String("dir", dir))
|
||||
return "", fmt.Errorf("创建目录失败: %w", err)
|
||||
}
|
||||
|
||||
// 创建文件
|
||||
dst, err := os.Create(fullPath)
|
||||
if err != nil {
|
||||
s.logger.Error("创建文件失败", zap.Error(err), zap.String("path", fullPath))
|
||||
return "", fmt.Errorf("创建文件失败: %w", err)
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
// 复制文件内容
|
||||
if _, err := io.Copy(dst, file); err != nil {
|
||||
s.logger.Error("写入文件失败", zap.Error(err), zap.String("path", fullPath))
|
||||
// 删除部分写入的文件
|
||||
_ = os.Remove(fullPath)
|
||||
return "", fmt.Errorf("写入文件失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("文件存储成功", zap.String("path", fullPath))
|
||||
return fullPath, nil
|
||||
}
|
||||
|
||||
// StoreMultipartFile 存储multipart文件
|
||||
func (s *LocalFileStorageServiceImpl) StoreMultipartFile(ctx context.Context, file *multipart.FileHeader, filename string) (string, error) {
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("打开上传文件失败: %w", err)
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
return s.StoreFile(ctx, src, filename)
|
||||
}
|
||||
|
||||
// GetFileURL 获取文件URL
|
||||
func (s *LocalFileStorageServiceImpl) GetFileURL(ctx context.Context, filePath string) (string, error) {
|
||||
// 检查文件是否存在
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("文件不存在: %s", filePath)
|
||||
}
|
||||
|
||||
// 返回文件路径(在实际应用中,这里应该返回可访问的URL)
|
||||
return filePath, nil
|
||||
}
|
||||
|
||||
// DeleteFile 删除文件
|
||||
func (s *LocalFileStorageServiceImpl) DeleteFile(ctx context.Context, filePath string) error {
|
||||
if err := os.Remove(filePath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// 文件不存在,不视为错误
|
||||
return nil
|
||||
}
|
||||
s.logger.Error("删除文件失败", zap.Error(err), zap.String("path", filePath))
|
||||
return fmt.Errorf("删除文件失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("文件删除成功", zap.String("path", filePath))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetFileReader 获取文件读取器
|
||||
func (s *LocalFileStorageServiceImpl) GetFileReader(ctx context.Context, filePath string) (io.ReadCloser, error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("打开文件失败: %w", err)
|
||||
}
|
||||
|
||||
return file, nil
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/application/product"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// FileDownloadHandler 文件下载处理器
|
||||
type FileDownloadHandler struct {
|
||||
uiComponentAppService product.UIComponentApplicationService
|
||||
responseBuilder interfaces.ResponseBuilder
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewFileDownloadHandler 创建文件下载处理器
|
||||
func NewFileDownloadHandler(
|
||||
uiComponentAppService product.UIComponentApplicationService,
|
||||
responseBuilder interfaces.ResponseBuilder,
|
||||
logger *zap.Logger,
|
||||
) *FileDownloadHandler {
|
||||
return &FileDownloadHandler{
|
||||
uiComponentAppService: uiComponentAppService,
|
||||
responseBuilder: responseBuilder,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// DownloadUIComponentFile 下载UI组件文件
|
||||
// @Summary 下载UI组件文件
|
||||
// @Description 下载UI组件文件
|
||||
// @Tags 文件下载
|
||||
// @Accept json
|
||||
// @Produce application/octet-stream
|
||||
// @Param id path string true "UI组件ID"
|
||||
// @Success 200 {file} file "文件内容"
|
||||
// @Failure 400 {object} interfaces.Response "请求参数错误"
|
||||
// @Failure 404 {object} interfaces.Response "UI组件不存在或文件不存在"
|
||||
// @Failure 500 {object} interfaces.Response "服务器内部错误"
|
||||
// @Router /api/v1/ui-components/{id}/download [get]
|
||||
func (h *FileDownloadHandler) DownloadUIComponentFile(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
h.responseBuilder.BadRequest(c, "UI组件ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取UI组件信息
|
||||
component, err := h.uiComponentAppService.GetUIComponentByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
h.logger.Error("获取UI组件失败", zap.Error(err), zap.String("id", id))
|
||||
h.responseBuilder.InternalError(c, "获取UI组件失败")
|
||||
return
|
||||
}
|
||||
|
||||
if component == nil {
|
||||
h.responseBuilder.NotFound(c, "UI组件不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if component.FilePath == nil {
|
||||
h.responseBuilder.NotFound(c, "UI组件文件不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取文件路径
|
||||
filePath, err := h.uiComponentAppService.DownloadUIComponentFile(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
h.logger.Error("获取UI组件文件路径失败", zap.Error(err), zap.String("id", id))
|
||||
h.responseBuilder.InternalError(c, "获取UI组件文件路径失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 设置下载文件名
|
||||
fileName := component.ComponentName
|
||||
if !strings.HasSuffix(strings.ToLower(fileName), ".zip") {
|
||||
fileName += ".zip"
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
c.Header("Content-Description", "File Transfer")
|
||||
c.Header("Content-Transfer-Encoding", "binary")
|
||||
c.Header("Content-Disposition", "attachment; filename="+fileName)
|
||||
c.Header("Content-Type", "application/octet-stream")
|
||||
|
||||
// 发送文件
|
||||
c.File(filePath)
|
||||
}
|
||||
551
internal/infrastructure/http/handlers/ui_component_handler.go
Normal file
551
internal/infrastructure/http/handlers/ui_component_handler.go
Normal file
@@ -0,0 +1,551 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/application/product"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// UIComponentHandler UI组件HTTP处理器
|
||||
type UIComponentHandler struct {
|
||||
uiComponentAppService product.UIComponentApplicationService
|
||||
responseBuilder interfaces.ResponseBuilder
|
||||
validator interfaces.RequestValidator
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewUIComponentHandler 创建UI组件HTTP处理器
|
||||
func NewUIComponentHandler(
|
||||
uiComponentAppService product.UIComponentApplicationService,
|
||||
responseBuilder interfaces.ResponseBuilder,
|
||||
validator interfaces.RequestValidator,
|
||||
logger *zap.Logger,
|
||||
) *UIComponentHandler {
|
||||
return &UIComponentHandler{
|
||||
uiComponentAppService: uiComponentAppService,
|
||||
responseBuilder: responseBuilder,
|
||||
validator: validator,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateUIComponent 创建UI组件
|
||||
// @Summary 创建UI组件
|
||||
// @Description 管理员创建新的UI组件
|
||||
// @Tags UI组件管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body product.CreateUIComponentRequest true "创建UI组件请求"
|
||||
// @Success 200 {object} interfaces.Response{data=entities.UIComponent} "创建成功"
|
||||
// @Failure 400 {object} interfaces.Response "请求参数错误"
|
||||
// @Failure 500 {object} interfaces.Response "服务器内部错误"
|
||||
// @Router /api/v1/admin/ui-components [post]
|
||||
func (h *UIComponentHandler) CreateUIComponent(c *gin.Context) {
|
||||
var req product.CreateUIComponentRequest
|
||||
|
||||
// 一次性读取请求体并绑定到结构体
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.logger.Error("验证创建UI组件请求失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, fmt.Sprintf("请求参数错误: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 使用结构体数据记录日志
|
||||
h.logger.Info("创建UI组件请求数据",
|
||||
zap.String("component_code", req.ComponentCode),
|
||||
zap.String("component_name", req.ComponentName),
|
||||
zap.String("description", req.Description),
|
||||
zap.String("version", req.Version),
|
||||
zap.Bool("is_active", req.IsActive),
|
||||
zap.Int("sort_order", req.SortOrder))
|
||||
|
||||
component, err := h.uiComponentAppService.CreateUIComponent(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
h.logger.Error("创建UI组件失败", zap.Error(err), zap.String("component_code", req.ComponentCode))
|
||||
if err == product.ErrComponentCodeAlreadyExists {
|
||||
h.responseBuilder.BadRequest(c, "UI组件编码已存在")
|
||||
return
|
||||
}
|
||||
h.responseBuilder.InternalError(c, fmt.Sprintf("创建UI组件失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, component)
|
||||
}
|
||||
|
||||
// CreateUIComponentWithFile 创建UI组件并上传文件
|
||||
// @Summary 创建UI组件并上传文件
|
||||
// @Description 管理员创建新的UI组件并同时上传文件
|
||||
// @Tags UI组件管理
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param component_code formData string true "组件编码"
|
||||
// @Param component_name formData string true "组件名称"
|
||||
// @Param description formData string false "组件描述"
|
||||
// @Param version formData string false "组件版本"
|
||||
// @Param is_active formData bool false "是否启用"
|
||||
// @Param sort_order formData int false "排序"
|
||||
// @Param file formData file true "组件文件"
|
||||
// @Success 200 {object} interfaces.Response{data=entities.UIComponent} "创建成功"
|
||||
// @Failure 400 {object} interfaces.Response "请求参数错误"
|
||||
// @Failure 500 {object} interfaces.Response "服务器内部错误"
|
||||
// @Router /api/v1/admin/ui-components/create-with-file [post]
|
||||
func (h *UIComponentHandler) CreateUIComponentWithFile(c *gin.Context) {
|
||||
// 创建请求结构体
|
||||
var req product.CreateUIComponentRequest
|
||||
|
||||
// 从表单数据中获取组件信息
|
||||
req.ComponentCode = c.PostForm("component_code")
|
||||
req.ComponentName = c.PostForm("component_name")
|
||||
req.Description = c.PostForm("description")
|
||||
req.Version = c.PostForm("version")
|
||||
req.IsActive = c.PostForm("is_active") == "true"
|
||||
|
||||
if sortOrderStr := c.PostForm("sort_order"); sortOrderStr != "" {
|
||||
if sortOrder, err := strconv.Atoi(sortOrderStr); err == nil {
|
||||
req.SortOrder = sortOrder
|
||||
}
|
||||
}
|
||||
|
||||
// 验证必需字段
|
||||
if req.ComponentCode == "" {
|
||||
h.responseBuilder.BadRequest(c, "组件编码不能为空")
|
||||
return
|
||||
}
|
||||
if req.ComponentName == "" {
|
||||
h.responseBuilder.BadRequest(c, "组件名称不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取上传的文件
|
||||
form, err := c.MultipartForm()
|
||||
if err != nil {
|
||||
h.logger.Error("获取表单数据失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, "获取表单数据失败")
|
||||
return
|
||||
}
|
||||
|
||||
files := form.File["files"]
|
||||
if len(files) == 0 {
|
||||
h.responseBuilder.BadRequest(c, "请上传组件文件")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查文件大小(100MB)
|
||||
for _, fileHeader := range files {
|
||||
if fileHeader.Size > 100*1024*1024 {
|
||||
h.responseBuilder.BadRequest(c, fmt.Sprintf("文件 %s 大小不能超过100MB", fileHeader.Filename))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 获取路径信息
|
||||
paths := c.PostFormArray("paths")
|
||||
|
||||
// 记录请求日志
|
||||
h.logger.Info("创建UI组件并上传文件请求",
|
||||
zap.String("component_code", req.ComponentCode),
|
||||
zap.String("component_name", req.ComponentName),
|
||||
zap.String("description", req.Description),
|
||||
zap.String("version", req.Version),
|
||||
zap.Bool("is_active", req.IsActive),
|
||||
zap.Int("sort_order", req.SortOrder),
|
||||
zap.Int("files_count", len(files)),
|
||||
zap.Strings("paths", paths))
|
||||
|
||||
// 调用应用服务创建组件并上传文件
|
||||
component, err := h.uiComponentAppService.CreateUIComponentWithFilesAndPaths(c.Request.Context(), req, files, paths)
|
||||
if err != nil {
|
||||
h.logger.Error("创建UI组件并上传文件失败", zap.Error(err), zap.String("component_code", req.ComponentCode))
|
||||
if err == product.ErrComponentCodeAlreadyExists {
|
||||
h.responseBuilder.BadRequest(c, "UI组件编码已存在")
|
||||
return
|
||||
}
|
||||
h.responseBuilder.InternalError(c, fmt.Sprintf("创建UI组件并上传文件失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, component)
|
||||
}
|
||||
|
||||
// GetUIComponent 获取UI组件详情
|
||||
// @Summary 获取UI组件详情
|
||||
// @Description 根据ID获取UI组件详情
|
||||
// @Tags UI组件管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "UI组件ID"
|
||||
// @Success 200 {object} interfaces.Response{data=entities.UIComponent} "获取成功"
|
||||
// @Failure 400 {object} interfaces.Response "请求参数错误"
|
||||
// @Failure 404 {object} interfaces.Response "UI组件不存在"
|
||||
// @Failure 500 {object} interfaces.Response "服务器内部错误"
|
||||
// @Router /api/v1/admin/ui-components/{id} [get]
|
||||
func (h *UIComponentHandler) GetUIComponent(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
h.responseBuilder.BadRequest(c, "UI组件ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
component, err := h.uiComponentAppService.GetUIComponentByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
h.logger.Error("获取UI组件失败", zap.Error(err), zap.String("id", id))
|
||||
h.responseBuilder.InternalError(c, "获取UI组件失败")
|
||||
return
|
||||
}
|
||||
|
||||
if component == nil {
|
||||
h.responseBuilder.NotFound(c, "UI组件不存在")
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, component)
|
||||
}
|
||||
|
||||
// UpdateUIComponent 更新UI组件
|
||||
// @Summary 更新UI组件
|
||||
// @Description 更新UI组件信息
|
||||
// @Tags UI组件管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "UI组件ID"
|
||||
// @Param request body product.UpdateUIComponentRequest true "更新UI组件请求"
|
||||
// @Success 200 {object} interfaces.Response "更新成功"
|
||||
// @Failure 400 {object} interfaces.Response "请求参数错误"
|
||||
// @Failure 404 {object} interfaces.Response "UI组件不存在"
|
||||
// @Failure 500 {object} interfaces.Response "服务器内部错误"
|
||||
// @Router /api/v1/admin/ui-components/{id} [put]
|
||||
func (h *UIComponentHandler) UpdateUIComponent(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
h.responseBuilder.BadRequest(c, "UI组件ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
var req product.UpdateUIComponentRequest
|
||||
|
||||
// 设置ID
|
||||
req.ID = id
|
||||
|
||||
// 验证请求
|
||||
if err := h.validator.Validate(c, &req); err != nil {
|
||||
h.logger.Error("验证更新UI组件请求失败", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
err := h.uiComponentAppService.UpdateUIComponent(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
h.logger.Error("更新UI组件失败", zap.Error(err), zap.String("id", id))
|
||||
if err == product.ErrComponentNotFound {
|
||||
h.responseBuilder.NotFound(c, "UI组件不存在")
|
||||
return
|
||||
}
|
||||
if err == product.ErrComponentCodeAlreadyExists {
|
||||
h.responseBuilder.BadRequest(c, "UI组件编码已存在")
|
||||
return
|
||||
}
|
||||
h.responseBuilder.InternalError(c, "更新UI组件失败")
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, nil)
|
||||
}
|
||||
|
||||
// DeleteUIComponent 删除UI组件
|
||||
// @Summary 删除UI组件
|
||||
// @Description 删除UI组件
|
||||
// @Tags UI组件管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "UI组件ID"
|
||||
// @Success 200 {object} interfaces.Response "删除成功"
|
||||
// @Failure 400 {object} interfaces.Response "请求参数错误"
|
||||
// @Failure 404 {object} interfaces.Response "UI组件不存在"
|
||||
// @Failure 500 {object} interfaces.Response "服务器内部错误"
|
||||
// @Router /api/v1/admin/ui-components/{id} [delete]
|
||||
func (h *UIComponentHandler) DeleteUIComponent(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
h.responseBuilder.BadRequest(c, "UI组件ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
err := h.uiComponentAppService.DeleteUIComponent(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
h.logger.Error("删除UI组件失败", zap.Error(err), zap.String("id", id))
|
||||
if err == product.ErrComponentNotFound {
|
||||
h.responseBuilder.NotFound(c, "UI组件不存在")
|
||||
return
|
||||
}
|
||||
h.responseBuilder.InternalError(c, "删除UI组件失败")
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, nil)
|
||||
}
|
||||
|
||||
// ListUIComponents 获取UI组件列表
|
||||
// @Summary 获取UI组件列表
|
||||
// @Description 获取UI组件列表,支持分页和筛选
|
||||
// @Tags UI组件管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param page_size query int false "每页数量" default(10)
|
||||
// @Param keyword query string false "关键词搜索"
|
||||
// @Param is_active query bool false "是否启用"
|
||||
// @Param sort_by query string false "排序字段" default(sort_order)
|
||||
// @Param sort_order query string false "排序方向" default(asc)
|
||||
// @Success 200 {object} interfaces.Response{data=product.ListUIComponentsResponse} "获取成功"
|
||||
// @Failure 500 {object} interfaces.Response "服务器内部错误"
|
||||
// @Router /api/v1/admin/ui-components [get]
|
||||
func (h *UIComponentHandler) ListUIComponents(c *gin.Context) {
|
||||
// 解析查询参数
|
||||
req := product.ListUIComponentsRequest{}
|
||||
|
||||
if pageStr := c.Query("page"); pageStr != "" {
|
||||
if page, err := strconv.Atoi(pageStr); err == nil {
|
||||
req.Page = page
|
||||
}
|
||||
}
|
||||
|
||||
if pageSizeStr := c.Query("page_size"); pageSizeStr != "" {
|
||||
if pageSize, err := strconv.Atoi(pageSizeStr); err == nil {
|
||||
req.PageSize = pageSize
|
||||
}
|
||||
}
|
||||
|
||||
req.Keyword = c.Query("keyword")
|
||||
|
||||
if isActiveStr := c.Query("is_active"); isActiveStr != "" {
|
||||
if isActive, err := strconv.ParseBool(isActiveStr); err == nil {
|
||||
req.IsActive = &isActive
|
||||
}
|
||||
}
|
||||
|
||||
req.SortBy = c.DefaultQuery("sort_by", "sort_order")
|
||||
req.SortOrder = c.DefaultQuery("sort_order", "asc")
|
||||
|
||||
response, err := h.uiComponentAppService.ListUIComponents(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
h.logger.Error("获取UI组件列表失败", zap.Error(err))
|
||||
h.responseBuilder.InternalError(c, "获取UI组件列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, response)
|
||||
}
|
||||
|
||||
// UploadUIComponentFile 上传UI组件文件
|
||||
// @Summary 上传UI组件文件
|
||||
// @Description 上传UI组件文件
|
||||
// @Tags UI组件管理
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param id path string true "UI组件ID"
|
||||
// @Param file formData file true "UI组件文件(ZIP格式)"
|
||||
// @Success 200 {object} interfaces.Response{data=string} "上传成功,返回文件路径"
|
||||
// @Failure 400 {object} interfaces.Response "请求参数错误"
|
||||
// @Failure 404 {object} interfaces.Response "UI组件不存在"
|
||||
// @Failure 500 {object} interfaces.Response "服务器内部错误"
|
||||
// @Router /api/v1/admin/ui-components/{id}/upload [post]
|
||||
func (h *UIComponentHandler) UploadUIComponentFile(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
h.responseBuilder.BadRequest(c, "UI组件ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取上传的文件
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
h.logger.Error("获取上传文件失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, "获取上传文件失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查文件大小(100MB)
|
||||
if file.Size > 100*1024*1024 {
|
||||
h.responseBuilder.BadRequest(c, "文件大小不能超过100MB")
|
||||
return
|
||||
}
|
||||
|
||||
filePath, err := h.uiComponentAppService.UploadUIComponentFile(c.Request.Context(), id, file)
|
||||
if err != nil {
|
||||
h.logger.Error("上传UI组件文件失败", zap.Error(err), zap.String("id", id))
|
||||
if err == product.ErrComponentNotFound {
|
||||
h.responseBuilder.NotFound(c, "UI组件不存在")
|
||||
return
|
||||
}
|
||||
if err == product.ErrInvalidFileType {
|
||||
h.responseBuilder.BadRequest(c, "文件类型错误")
|
||||
return
|
||||
}
|
||||
h.responseBuilder.InternalError(c, "上传UI组件文件失败")
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, filePath)
|
||||
}
|
||||
|
||||
// UploadAndExtractUIComponentFile 上传并解压UI组件文件
|
||||
// @Summary 上传并解压UI组件文件
|
||||
// @Description 上传文件并自动解压到组件文件夹(仅ZIP文件支持解压)
|
||||
// @Tags UI组件管理
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param id path string true "UI组件ID"
|
||||
// @Param file formData file true "UI组件文件(任意格式,ZIP格式支持自动解压)"
|
||||
// @Success 200 {object} interfaces.Response "上传成功"
|
||||
// @Failure 400 {object} interfaces.Response "请求参数错误"
|
||||
// @Failure 404 {object} interfaces.Response "UI组件不存在"
|
||||
// @Failure 500 {object} interfaces.Response "服务器内部错误"
|
||||
// @Router /api/v1/admin/ui-components/{id}/upload-extract [post]
|
||||
func (h *UIComponentHandler) UploadAndExtractUIComponentFile(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
h.responseBuilder.BadRequest(c, "UI组件ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取上传的文件
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
h.logger.Error("获取上传文件失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, "获取上传文件失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查文件大小(100MB)
|
||||
if file.Size > 100*1024*1024 {
|
||||
h.responseBuilder.BadRequest(c, "文件大小不能超过100MB")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.uiComponentAppService.UploadAndExtractUIComponentFile(c.Request.Context(), id, file)
|
||||
if err != nil {
|
||||
h.logger.Error("上传并解压UI组件文件失败", zap.Error(err), zap.String("id", id))
|
||||
if err == product.ErrComponentNotFound {
|
||||
h.responseBuilder.NotFound(c, "UI组件不存在")
|
||||
return
|
||||
}
|
||||
if err == product.ErrInvalidFileType {
|
||||
h.responseBuilder.BadRequest(c, "文件类型错误")
|
||||
return
|
||||
}
|
||||
h.responseBuilder.InternalError(c, "上传并解压UI组件文件失败")
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, nil)
|
||||
}
|
||||
|
||||
// GetUIComponentFolderContent 获取UI组件文件夹内容
|
||||
// @Summary 获取UI组件文件夹内容
|
||||
// @Description 获取UI组件文件夹内容
|
||||
// @Tags UI组件管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "UI组件ID"
|
||||
// @Success 200 {object} interfaces.Response{data=[]FileInfo} "获取成功"
|
||||
// @Failure 400 {object} interfaces.Response "请求参数错误"
|
||||
// @Failure 404 {object} interfaces.Response "UI组件不存在"
|
||||
// @Failure 500 {object} interfaces.Response "服务器内部错误"
|
||||
// @Router /api/v1/admin/ui-components/{id}/folder-content [get]
|
||||
func (h *UIComponentHandler) GetUIComponentFolderContent(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
h.responseBuilder.BadRequest(c, "UI组件ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
files, err := h.uiComponentAppService.GetUIComponentFolderContent(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
h.logger.Error("获取UI组件文件夹内容失败", zap.Error(err), zap.String("id", id))
|
||||
if err == product.ErrComponentNotFound {
|
||||
h.responseBuilder.NotFound(c, "UI组件不存在")
|
||||
return
|
||||
}
|
||||
h.responseBuilder.InternalError(c, "获取UI组件文件夹内容失败")
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, files)
|
||||
}
|
||||
|
||||
// DeleteUIComponentFolder 删除UI组件文件夹
|
||||
// @Summary 删除UI组件文件夹
|
||||
// @Description 删除UI组件文件夹
|
||||
// @Tags UI组件管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "UI组件ID"
|
||||
// @Success 200 {object} interfaces.Response "删除成功"
|
||||
// @Failure 400 {object} interfaces.Response "请求参数错误"
|
||||
// @Failure 404 {object} interfaces.Response "UI组件不存在"
|
||||
// @Failure 500 {object} interfaces.Response "服务器内部错误"
|
||||
// @Router /api/v1/admin/ui-components/{id}/folder [delete]
|
||||
func (h *UIComponentHandler) DeleteUIComponentFolder(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
h.responseBuilder.BadRequest(c, "UI组件ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
err := h.uiComponentAppService.DeleteUIComponentFolder(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
h.logger.Error("删除UI组件文件夹失败", zap.Error(err), zap.String("id", id))
|
||||
if err == product.ErrComponentNotFound {
|
||||
h.responseBuilder.NotFound(c, "UI组件不存在")
|
||||
return
|
||||
}
|
||||
h.responseBuilder.InternalError(c, "删除UI组件文件夹失败")
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, nil)
|
||||
}
|
||||
|
||||
// DownloadUIComponentFile 下载UI组件文件
|
||||
// @Summary 下载UI组件文件
|
||||
// @Description 下载UI组件文件
|
||||
// @Tags UI组件管理
|
||||
// @Accept json
|
||||
// @Produce application/octet-stream
|
||||
// @Param id path string true "UI组件ID"
|
||||
// @Success 200 {file} file "文件内容"
|
||||
// @Failure 400 {object} interfaces.Response "请求参数错误"
|
||||
// @Failure 404 {object} interfaces.Response "UI组件不存在或文件不存在"
|
||||
// @Failure 500 {object} interfaces.Response "服务器内部错误"
|
||||
// @Router /api/v1/admin/ui-components/{id}/download [get]
|
||||
func (h *UIComponentHandler) DownloadUIComponentFile(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
h.responseBuilder.BadRequest(c, "UI组件ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
filePath, err := h.uiComponentAppService.DownloadUIComponentFile(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
h.logger.Error("下载UI组件文件失败", zap.Error(err), zap.String("id", id))
|
||||
if err == product.ErrComponentNotFound {
|
||||
h.responseBuilder.NotFound(c, "UI组件不存在")
|
||||
return
|
||||
}
|
||||
if err == product.ErrComponentFileNotFound {
|
||||
h.responseBuilder.NotFound(c, "UI组件文件不存在")
|
||||
return
|
||||
}
|
||||
h.responseBuilder.InternalError(c, "下载UI组件文件失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 这里应该实现文件下载逻辑,返回文件内容
|
||||
// 由于我们使用的是本地文件存储,可以直接返回文件
|
||||
c.File(filePath)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package routes
|
||||
|
||||
import (
|
||||
"tyapi-server/internal/infrastructure/http/handlers"
|
||||
component_report "tyapi-server/internal/shared/component_report"
|
||||
sharedhttp "tyapi-server/internal/shared/http"
|
||||
"tyapi-server/internal/shared/middleware"
|
||||
|
||||
@@ -11,7 +12,7 @@ import (
|
||||
// ProductRoutes 产品路由
|
||||
type ProductRoutes struct {
|
||||
productHandler *handlers.ProductHandler
|
||||
componentReportHandler *handlers.ComponentReportHandler
|
||||
componentReportHandler *component_report.ComponentReportHandler
|
||||
auth *middleware.JWTAuthMiddleware
|
||||
optionalAuth *middleware.OptionalAuthMiddleware
|
||||
logger *zap.Logger
|
||||
@@ -20,7 +21,7 @@ type ProductRoutes struct {
|
||||
// NewProductRoutes 创建产品路由
|
||||
func NewProductRoutes(
|
||||
productHandler *handlers.ProductHandler,
|
||||
componentReportHandler *handlers.ComponentReportHandler,
|
||||
componentReportHandler *component_report.ComponentReportHandler,
|
||||
auth *middleware.JWTAuthMiddleware,
|
||||
optionalAuth *middleware.OptionalAuthMiddleware,
|
||||
logger *zap.Logger,
|
||||
@@ -58,19 +59,24 @@ func (r *ProductRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
|
||||
// 订阅产品(需要认证)
|
||||
products.POST("/:id/subscribe", r.auth.Handle(), r.productHandler.SubscribeProduct)
|
||||
}
|
||||
|
||||
// 组件报告相关路由(需要认证)
|
||||
componentReport := products.Group("/:id/component-report", r.auth.Handle())
|
||||
{
|
||||
// 获取报告下载信息
|
||||
componentReport.GET("/info", r.componentReportHandler.GetReportDownloadInfo)
|
||||
|
||||
// 创建支付订单(暂时注释,后续实现)
|
||||
// componentReport.POST("/create-order", r.componentReportHandler.CreateReportPaymentOrder)
|
||||
|
||||
// 下载报告文件
|
||||
componentReport.GET("/download/:downloadId", r.componentReportHandler.DownloadReport)
|
||||
}
|
||||
// 组件报告 - 需要认证
|
||||
componentReport := engine.Group("/api/v1/component-report", r.auth.Handle())
|
||||
{
|
||||
// 生成并下载 example.json 文件
|
||||
componentReport.POST("/download-example-json", r.componentReportHandler.DownloadExampleJSON)
|
||||
// 生成并下载示例报告ZIP文件
|
||||
componentReport.POST("/generate-and-download", r.componentReportHandler.GenerateAndDownloadZip)
|
||||
}
|
||||
|
||||
// 产品组件报告相关接口 - 需要认证
|
||||
componentReportGroup := products.Group("/:id/component-report", r.auth.Handle())
|
||||
{
|
||||
componentReportGroup.GET("/check", r.componentReportHandler.CheckDownloadAvailability)
|
||||
componentReportGroup.GET("/info", r.componentReportHandler.GetDownloadInfo)
|
||||
componentReportGroup.POST("/create-order", r.componentReportHandler.CreatePaymentOrder)
|
||||
componentReportGroup.GET("/check-payment/:orderId", r.componentReportHandler.CheckPaymentStatus)
|
||||
}
|
||||
|
||||
// 分类 - 公开接口
|
||||
@@ -103,9 +109,6 @@ func (r *ProductRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
// 取消订阅
|
||||
subscriptions.POST("/:id/cancel", r.productHandler.CancelMySubscription)
|
||||
}
|
||||
|
||||
// 我的组件报告下载历史
|
||||
my.GET("/component-reports", r.componentReportHandler.GetUserDownloadHistory)
|
||||
}
|
||||
|
||||
r.logger.Info("产品路由注册完成")
|
||||
|
||||
48
internal/infrastructure/http/routes/ui_component_routes.go
Normal file
48
internal/infrastructure/http/routes/ui_component_routes.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/infrastructure/http/handlers"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// UIComponentRoutes UI组件路由
|
||||
type UIComponentRoutes struct {
|
||||
uiComponentHandler *handlers.UIComponentHandler
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewUIComponentRoutes 创建UI组件路由
|
||||
func NewUIComponentRoutes(
|
||||
uiComponentHandler *handlers.UIComponentHandler,
|
||||
logger *zap.Logger,
|
||||
) *UIComponentRoutes {
|
||||
return &UIComponentRoutes{
|
||||
uiComponentHandler: uiComponentHandler,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册UI组件路由
|
||||
func (r *UIComponentRoutes) RegisterRoutes(router *gin.RouterGroup, authMiddleware interfaces.Middleware) {
|
||||
uiComponentGroup := router.Group("/ui-components")
|
||||
uiComponentGroup.Use(authMiddleware.Handle())
|
||||
{
|
||||
// UI组件管理
|
||||
uiComponentGroup.POST("", r.uiComponentHandler.CreateUIComponent) // 创建UI组件
|
||||
uiComponentGroup.POST("/create-with-file", r.uiComponentHandler.CreateUIComponentWithFile) // 创建UI组件并上传文件
|
||||
uiComponentGroup.GET("", r.uiComponentHandler.ListUIComponents) // 获取UI组件列表
|
||||
uiComponentGroup.GET("/:id", r.uiComponentHandler.GetUIComponent) // 获取UI组件详情
|
||||
uiComponentGroup.PUT("/:id", r.uiComponentHandler.UpdateUIComponent) // 更新UI组件
|
||||
uiComponentGroup.DELETE("/:id", r.uiComponentHandler.DeleteUIComponent) // 删除UI组件
|
||||
|
||||
// 文件操作
|
||||
uiComponentGroup.POST("/:id/upload", r.uiComponentHandler.UploadUIComponentFile) // 上传UI组件文件
|
||||
uiComponentGroup.POST("/:id/upload-extract", r.uiComponentHandler.UploadAndExtractUIComponentFile) // 上传并解压UI组件文件
|
||||
uiComponentGroup.GET("/:id/folder-content", r.uiComponentHandler.GetUIComponentFolderContent) // 获取UI组件文件夹内容
|
||||
uiComponentGroup.DELETE("/:id/folder", r.uiComponentHandler.DeleteUIComponentFolder) // 删除UI组件文件夹
|
||||
uiComponentGroup.GET("/:id/download", r.uiComponentHandler.DownloadUIComponentFile) // 下载UI组件文件
|
||||
}
|
||||
}
|
||||
204
internal/shared/component_report/README.md
Normal file
204
internal/shared/component_report/README.md
Normal file
@@ -0,0 +1,204 @@
|
||||
# 组件报告生成服务
|
||||
|
||||
这个服务用于生成产品示例报告的 `example.json` 文件,并打包成 ZIP 文件供下载。
|
||||
|
||||
## 功能概述
|
||||
|
||||
1. **生成 example.json 文件**:根据组合包子产品的响应示例数据生成符合格式要求的 JSON 文件
|
||||
2. **打包 ZIP 文件**:将生成的 `example.json` 文件打包成 ZIP 格式
|
||||
3. **HTTP 接口**:提供 HTTP 接口用于生成和下载文件
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
component_report/
|
||||
├── example_json_generator.go # 示例JSON生成器
|
||||
├── zip_generator.go # ZIP文件生成器
|
||||
├── handler.go # HTTP处理器
|
||||
└── README.md # 说明文档
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 直接使用生成器
|
||||
|
||||
```go
|
||||
// 创建生成器
|
||||
exampleJSONGenerator := component_report.NewExampleJSONGenerator(
|
||||
productRepo,
|
||||
docRepo,
|
||||
apiConfigRepo,
|
||||
logger,
|
||||
)
|
||||
|
||||
// 生成 example.json
|
||||
jsonData, err := exampleJSONGenerator.GenerateExampleJSON(
|
||||
ctx,
|
||||
productID, // 产品ID(可以是组合包或单品)
|
||||
subProductCodes, // 子产品编号列表(可选,如果为空则处理所有子产品)
|
||||
)
|
||||
```
|
||||
|
||||
### 2. 生成 ZIP 文件
|
||||
|
||||
```go
|
||||
// 创建ZIP生成器
|
||||
zipGenerator := component_report.NewZipGenerator(logger)
|
||||
|
||||
// 生成ZIP文件
|
||||
zipPath, err := zipGenerator.GenerateZipFile(
|
||||
ctx,
|
||||
productID,
|
||||
subProductCodes,
|
||||
exampleJSONGenerator,
|
||||
outputPath, // 输出路径(可选,如果为空则使用默认路径)
|
||||
)
|
||||
```
|
||||
|
||||
### 3. 使用 HTTP 接口
|
||||
|
||||
#### 生成 example.json
|
||||
|
||||
```http
|
||||
POST /api/v1/component-report/generate-example-json
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"product_id": "产品ID",
|
||||
"sub_product_codes": ["子产品编号1", "子产品编号2"] // 可选
|
||||
}
|
||||
```
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"product_id": "产品ID",
|
||||
"json_content": "生成的JSON内容",
|
||||
"json_size": 1234
|
||||
}
|
||||
```
|
||||
|
||||
#### 生成 ZIP 文件
|
||||
|
||||
```http
|
||||
POST /api/v1/component-report/generate-zip
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"product_id": "产品ID",
|
||||
"sub_product_codes": ["子产品编号1", "子产品编号2"], // 可选
|
||||
"output_path": "自定义输出路径" // 可选
|
||||
}
|
||||
```
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "ZIP文件生成成功",
|
||||
"zip_path": "storage/component-reports/xxx_example.json.zip",
|
||||
"file_size": 12345,
|
||||
"file_name": "xxx_example.json.zip"
|
||||
}
|
||||
```
|
||||
|
||||
#### 生成并下载 ZIP 文件
|
||||
|
||||
```http
|
||||
POST /api/v1/component-report/generate-and-download
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"product_id": "产品ID",
|
||||
"sub_product_codes": ["子产品编号1", "子产品编号2"] // 可选
|
||||
}
|
||||
```
|
||||
|
||||
响应:直接返回 ZIP 文件流
|
||||
|
||||
#### 下载已生成的 ZIP 文件
|
||||
|
||||
```http
|
||||
GET /api/v1/component-report/download-zip/:product_id
|
||||
```
|
||||
|
||||
响应:直接返回 ZIP 文件流
|
||||
|
||||
## example.json 格式
|
||||
|
||||
生成的 `example.json` 文件格式如下:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"feature": {
|
||||
"featureName": "产品名称",
|
||||
"sort": 1
|
||||
},
|
||||
"data": {
|
||||
"apiID": "产品编号",
|
||||
"data": {
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": { ... }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"feature": {
|
||||
"featureName": "另一个产品名称",
|
||||
"sort": 2
|
||||
},
|
||||
"data": {
|
||||
"apiID": "另一个产品编号",
|
||||
"data": { ... }
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## 响应示例数据提取优先级
|
||||
|
||||
1. **产品文档的 `response_example` 字段**(JSON格式)
|
||||
2. **产品文档的 `response_example` 字段**(Markdown代码块中的JSON)
|
||||
3. **产品API配置的 `response_example` 字段**
|
||||
4. **默认空对象** `{}`(如果都没有)
|
||||
|
||||
## ZIP 文件结构
|
||||
|
||||
生成的 ZIP 文件结构:
|
||||
|
||||
```
|
||||
component-report.zip
|
||||
└── public/
|
||||
└── example.json
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 确保 `storage/component-reports` 目录存在且有写权限
|
||||
2. 如果产品是组合包,会遍历所有子产品(或指定的子产品)生成响应示例
|
||||
3. 如果某个子产品没有响应示例数据,会使用空对象 `{}` 作为默认值
|
||||
4. ZIP 文件会保存在 `storage/component-reports` 目录下,文件名为 `{productID}_example.json.zip`
|
||||
|
||||
## 集成到路由
|
||||
|
||||
如果需要使用 HTTP 接口,需要在路由中注册:
|
||||
|
||||
```go
|
||||
// 创建处理器
|
||||
componentReportHandler := component_report.NewComponentReportHandler(
|
||||
productRepo,
|
||||
docRepo,
|
||||
apiConfigRepo,
|
||||
logger,
|
||||
)
|
||||
|
||||
// 注册路由
|
||||
router.POST("/api/v1/component-report/generate-example-json", componentReportHandler.GenerateExampleJSON)
|
||||
router.POST("/api/v1/component-report/generate-zip", componentReportHandler.GenerateZip)
|
||||
router.POST("/api/v1/component-report/generate-and-download", componentReportHandler.GenerateAndDownloadZip)
|
||||
router.GET("/api/v1/component-report/download-zip/:product_id", componentReportHandler.DownloadZip)
|
||||
```
|
||||
286
internal/shared/component_report/example_json_generator.go
Normal file
286
internal/shared/component_report/example_json_generator.go
Normal file
@@ -0,0 +1,286 @@
|
||||
package component_report
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/domains/product/entities"
|
||||
"tyapi-server/internal/domains/product/repositories"
|
||||
)
|
||||
|
||||
// ExampleJSONGenerator 示例JSON生成器
|
||||
type ExampleJSONGenerator struct {
|
||||
productRepo repositories.ProductRepository
|
||||
docRepo repositories.ProductDocumentationRepository
|
||||
apiConfigRepo repositories.ProductApiConfigRepository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewExampleJSONGenerator 创建示例JSON生成器
|
||||
func NewExampleJSONGenerator(
|
||||
productRepo repositories.ProductRepository,
|
||||
docRepo repositories.ProductDocumentationRepository,
|
||||
apiConfigRepo repositories.ProductApiConfigRepository,
|
||||
logger *zap.Logger,
|
||||
) *ExampleJSONGenerator {
|
||||
return &ExampleJSONGenerator{
|
||||
productRepo: productRepo,
|
||||
docRepo: docRepo,
|
||||
apiConfigRepo: apiConfigRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// ExampleJSONItem example.json 中的单个项
|
||||
type ExampleJSONItem struct {
|
||||
Feature struct {
|
||||
FeatureName string `json:"featureName"`
|
||||
Sort int `json:"sort"`
|
||||
} `json:"feature"`
|
||||
Data struct {
|
||||
APIID string `json:"apiID"`
|
||||
Data interface{} `json:"data"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// GenerateExampleJSON 生成 example.json 文件内容
|
||||
// productID: 产品ID(可以是组合包或单品)
|
||||
// subProductCodes: 子产品编号列表(如果为空,则处理所有子产品)
|
||||
func (g *ExampleJSONGenerator) GenerateExampleJSON(ctx context.Context, productID string, subProductCodes []string) ([]byte, error) {
|
||||
// 1. 获取产品信息
|
||||
product, err := g.productRepo.GetByID(ctx, productID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取产品信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 构建 example.json 数组
|
||||
var examples []ExampleJSONItem
|
||||
|
||||
if product.IsPackage {
|
||||
// 组合包:遍历子产品
|
||||
packageItems, err := g.productRepo.GetPackageItems(ctx, productID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取组合包子产品失败: %w", err)
|
||||
}
|
||||
|
||||
for sort, item := range packageItems {
|
||||
// 如果指定了子产品编号列表,只处理列表中的产品
|
||||
if len(subProductCodes) > 0 {
|
||||
found := false
|
||||
for _, code := range subProductCodes {
|
||||
if item.Product != nil && item.Product.Code == code {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 获取子产品信息
|
||||
var subProduct entities.Product
|
||||
if item.Product != nil {
|
||||
subProduct = *item.Product
|
||||
} else {
|
||||
subProduct, err = g.productRepo.GetByID(ctx, item.ProductID)
|
||||
if err != nil {
|
||||
g.logger.Warn("获取子产品信息失败",
|
||||
zap.String("product_id", item.ProductID),
|
||||
zap.Error(err),
|
||||
)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 获取响应示例数据
|
||||
responseData := g.extractResponseExample(ctx, &subProduct)
|
||||
|
||||
// 获取产品名称和编号
|
||||
productName := subProduct.Name
|
||||
productCode := subProduct.Code
|
||||
|
||||
// 构建示例项
|
||||
example := ExampleJSONItem{
|
||||
Feature: struct {
|
||||
FeatureName string `json:"featureName"`
|
||||
Sort int `json:"sort"`
|
||||
}{
|
||||
FeatureName: productName,
|
||||
Sort: sort + 1,
|
||||
},
|
||||
Data: struct {
|
||||
APIID string `json:"apiID"`
|
||||
Data interface{} `json:"data"`
|
||||
}{
|
||||
APIID: productCode,
|
||||
Data: responseData,
|
||||
},
|
||||
}
|
||||
|
||||
examples = append(examples, example)
|
||||
}
|
||||
} else {
|
||||
// 单品
|
||||
responseData := g.extractResponseExample(ctx, &product)
|
||||
|
||||
example := ExampleJSONItem{
|
||||
Feature: struct {
|
||||
FeatureName string `json:"featureName"`
|
||||
Sort int `json:"sort"`
|
||||
}{
|
||||
FeatureName: product.Name,
|
||||
Sort: 1,
|
||||
},
|
||||
Data: struct {
|
||||
APIID string `json:"apiID"`
|
||||
Data interface{} `json:"data"`
|
||||
}{
|
||||
APIID: product.Code,
|
||||
Data: responseData,
|
||||
},
|
||||
}
|
||||
|
||||
examples = append(examples, example)
|
||||
}
|
||||
|
||||
// 3. 序列化为JSON
|
||||
jsonData, err := json.MarshalIndent(examples, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("序列化example.json失败: %w", err)
|
||||
}
|
||||
|
||||
return jsonData, nil
|
||||
}
|
||||
|
||||
// MatchProductCodeToPath 根据产品编码匹配 UI 组件路径,返回路径和类型(folder/file)
|
||||
func (g *ExampleJSONGenerator) MatchProductCodeToPath(ctx context.Context, productCode string) (string, string, error) {
|
||||
basePath := filepath.Join("resources", "Pure Component", "src", "ui")
|
||||
|
||||
entries, err := os.ReadDir(basePath)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("读取组件目录失败: %w", err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
|
||||
// 精确匹配
|
||||
if name == productCode {
|
||||
path := filepath.Join(basePath, name)
|
||||
fileType := "folder"
|
||||
if !entry.IsDir() {
|
||||
fileType = "file"
|
||||
}
|
||||
return path, fileType, nil
|
||||
}
|
||||
|
||||
// 模糊匹配:文件夹名称包含产品编号,或产品编号包含文件夹名称的核心部分
|
||||
if strings.Contains(name, productCode) || strings.Contains(productCode, extractCoreCode(name)) {
|
||||
path := filepath.Join(basePath, name)
|
||||
fileType := "folder"
|
||||
if !entry.IsDir() {
|
||||
fileType = "file"
|
||||
}
|
||||
return path, fileType, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", "", fmt.Errorf("未找到匹配的组件文件: %s", productCode)
|
||||
}
|
||||
|
||||
// extractCoreCode 提取文件名中的核心编码部分
|
||||
func extractCoreCode(name string) string {
|
||||
for i, r := range name {
|
||||
if (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
|
||||
return name[i:]
|
||||
}
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// extractResponseExample 提取产品响应示例数据(优先级:文档 > API配置 > 默认值)
|
||||
func (g *ExampleJSONGenerator) extractResponseExample(ctx context.Context, product *entities.Product) interface{} {
|
||||
var responseData interface{}
|
||||
|
||||
// 1. 优先从产品文档中获取
|
||||
doc, err := g.docRepo.FindByProductID(ctx, product.ID)
|
||||
if err == nil && doc != nil && doc.ResponseExample != "" {
|
||||
// 尝试直接解析为JSON
|
||||
err := json.Unmarshal([]byte(doc.ResponseExample), &responseData)
|
||||
if err == nil {
|
||||
g.logger.Debug("从产品文档中提取响应示例成功",
|
||||
zap.String("product_id", product.ID),
|
||||
zap.String("product_code", product.Code),
|
||||
)
|
||||
return responseData
|
||||
}
|
||||
|
||||
// 如果解析失败,尝试从Markdown代码块中提取JSON
|
||||
extractedData := extractJSONFromMarkdown(doc.ResponseExample)
|
||||
if extractedData != nil {
|
||||
g.logger.Debug("从Markdown代码块中提取响应示例成功",
|
||||
zap.String("product_id", product.ID),
|
||||
zap.String("product_code", product.Code),
|
||||
)
|
||||
return extractedData
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 如果文档中没有,尝试从产品API配置中获取
|
||||
apiConfig, err := g.apiConfigRepo.FindByProductID(ctx, product.ID)
|
||||
if err == nil && apiConfig != nil && apiConfig.ResponseExample != "" {
|
||||
// API配置的响应示例通常是 JSON 字符串
|
||||
err := json.Unmarshal([]byte(apiConfig.ResponseExample), &responseData)
|
||||
if err == nil {
|
||||
g.logger.Debug("从产品API配置中提取响应示例成功",
|
||||
zap.String("product_id", product.ID),
|
||||
zap.String("product_code", product.Code),
|
||||
)
|
||||
return responseData
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 如果都没有,返回默认空对象
|
||||
g.logger.Warn("未找到响应示例数据,使用默认空对象",
|
||||
zap.String("product_id", product.ID),
|
||||
zap.String("product_code", product.Code),
|
||||
)
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
|
||||
// extractJSONFromMarkdown 从Markdown代码块中提取JSON
|
||||
func extractJSONFromMarkdown(markdown string) interface{} {
|
||||
// 查找 ```json 代码块
|
||||
re := regexp.MustCompile("(?s)```json\\s*(.*?)\\s*```")
|
||||
matches := re.FindStringSubmatch(markdown)
|
||||
|
||||
if len(matches) > 1 {
|
||||
var jsonData interface{}
|
||||
err := json.Unmarshal([]byte(matches[1]), &jsonData)
|
||||
if err == nil {
|
||||
return jsonData
|
||||
}
|
||||
}
|
||||
|
||||
// 也尝试查找 ``` 代码块(可能是其他格式)
|
||||
re2 := regexp.MustCompile("(?s)```\\s*(.*?)\\s*```")
|
||||
matches2 := re2.FindStringSubmatch(markdown)
|
||||
if len(matches2) > 1 {
|
||||
var jsonData interface{}
|
||||
err := json.Unmarshal([]byte(matches2[1]), &jsonData)
|
||||
if err == nil {
|
||||
return jsonData
|
||||
}
|
||||
}
|
||||
|
||||
// 如果提取失败,返回 nil(由调用者决定默认值)
|
||||
return nil
|
||||
}
|
||||
1343
internal/shared/component_report/handler.go
Normal file
1343
internal/shared/component_report/handler.go
Normal file
File diff suppressed because it is too large
Load Diff
265
internal/shared/component_report/zip_generator.go
Normal file
265
internal/shared/component_report/zip_generator.go
Normal file
@@ -0,0 +1,265 @@
|
||||
package component_report
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// ZipGenerator ZIP文件生成器
|
||||
type ZipGenerator struct {
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewZipGenerator 创建ZIP文件生成器
|
||||
func NewZipGenerator(logger *zap.Logger) *ZipGenerator {
|
||||
return &ZipGenerator{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateZipFile 生成ZIP文件,包含 example.json 和匹配的组件文件
|
||||
// productID: 产品ID
|
||||
// subProductCodes: 子产品编号列表(如果为空,则处理所有子产品)
|
||||
// exampleJSONGenerator: 示例JSON生成器
|
||||
// outputPath: 输出ZIP文件路径(如果为空,则使用默认路径)
|
||||
func (g *ZipGenerator) GenerateZipFile(
|
||||
ctx context.Context,
|
||||
productID string,
|
||||
subProductCodes []string,
|
||||
exampleJSONGenerator *ExampleJSONGenerator,
|
||||
outputPath string,
|
||||
) (string, error) {
|
||||
// 1. 生成 example.json 内容
|
||||
exampleJSON, err := exampleJSONGenerator.GenerateExampleJSON(ctx, productID, subProductCodes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("生成example.json失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 确定输出路径
|
||||
if outputPath == "" {
|
||||
// 使用默认路径:storage/component-reports/{productID}.zip
|
||||
outputDir := "storage/component-reports"
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
return "", fmt.Errorf("创建输出目录失败: %w", err)
|
||||
}
|
||||
outputPath = filepath.Join(outputDir, fmt.Sprintf("%s_example.json.zip", productID))
|
||||
}
|
||||
|
||||
// 3. 创建ZIP文件
|
||||
zipFile, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建ZIP文件失败: %w", err)
|
||||
}
|
||||
defer zipFile.Close()
|
||||
|
||||
zipWriter := zip.NewWriter(zipFile)
|
||||
defer zipWriter.Close()
|
||||
|
||||
// 4. 添加 example.json 到 public 目录
|
||||
exampleWriter, err := zipWriter.Create("public/example.json")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建example.json文件失败: %w", err)
|
||||
}
|
||||
|
||||
_, err = exampleWriter.Write(exampleJSON)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("写入example.json失败: %w", err)
|
||||
}
|
||||
|
||||
// 5. 添加整个 src 目录,但过滤 ui 目录下的文件
|
||||
srcBasePath := filepath.Join("resources", "Pure Component", "src")
|
||||
uiBasePath := filepath.Join(srcBasePath, "ui")
|
||||
|
||||
// 收集所有匹配的组件名称(文件夹名或文件名)
|
||||
matchedNames := make(map[string]bool)
|
||||
for _, productCode := range subProductCodes {
|
||||
path, _, err := exampleJSONGenerator.MatchProductCodeToPath(ctx, productCode)
|
||||
if err == nil && path != "" {
|
||||
// 获取组件名称(文件夹名或文件名)
|
||||
componentName := filepath.Base(path)
|
||||
matchedNames[componentName] = true
|
||||
}
|
||||
}
|
||||
|
||||
// 遍历整个 src 目录
|
||||
err = filepath.Walk(srcBasePath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 计算相对于 src 的路径
|
||||
relPath, err := filepath.Rel(srcBasePath, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 转换为ZIP路径格式
|
||||
zipPath := filepath.ToSlash(filepath.Join("src", relPath))
|
||||
|
||||
// 检查是否在 ui 目录下
|
||||
uiRelPath, err := filepath.Rel(uiBasePath, path)
|
||||
isInUIDir := err == nil && !strings.HasPrefix(uiRelPath, "..")
|
||||
|
||||
if isInUIDir {
|
||||
// 如果是 ui 目录本身,直接添加
|
||||
if uiRelPath == "." || uiRelPath == "" {
|
||||
if info.IsDir() {
|
||||
_, err = zipWriter.Create(zipPath + "/")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取文件/文件夹名称
|
||||
fileName := info.Name()
|
||||
|
||||
// 检查是否应该保留:
|
||||
// 1. CBehaviorRiskScan.vue 文件(无论在哪里)
|
||||
// 2. 匹配到的组件文件夹/文件
|
||||
shouldInclude := false
|
||||
|
||||
// 检查是否是 CBehaviorRiskScan.vue
|
||||
if fileName == "CBehaviorRiskScan.vue" {
|
||||
shouldInclude = true
|
||||
} else {
|
||||
// 检查是否是匹配的组件(检查组件名称)
|
||||
if matchedNames[fileName] {
|
||||
shouldInclude = true
|
||||
} else {
|
||||
// 检查是否在匹配的组件文件夹内
|
||||
// 获取相对于 ui 的路径的第一部分(组件文件夹名)
|
||||
parts := strings.Split(filepath.ToSlash(uiRelPath), "/")
|
||||
if len(parts) > 0 && parts[0] != "" && parts[0] != "." {
|
||||
if matchedNames[parts[0]] {
|
||||
shouldInclude = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !shouldInclude {
|
||||
// 跳过不匹配的文件/文件夹
|
||||
if info.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是目录,创建目录项
|
||||
if info.IsDir() {
|
||||
_, err = zipWriter.Create(zipPath + "/")
|
||||
return err
|
||||
}
|
||||
|
||||
// 添加文件
|
||||
return g.AddFileToZip(zipWriter, path, zipPath)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
g.logger.Warn("添加src目录失败", zap.Error(err))
|
||||
}
|
||||
|
||||
g.logger.Info("成功生成ZIP文件",
|
||||
zap.String("product_id", productID),
|
||||
zap.String("output_path", outputPath),
|
||||
zap.Int("example_json_size", len(exampleJSON)),
|
||||
zap.Int("sub_product_count", len(subProductCodes)),
|
||||
)
|
||||
|
||||
return outputPath, nil
|
||||
}
|
||||
|
||||
// AddFileToZip 添加文件到ZIP
|
||||
func (g *ZipGenerator) AddFileToZip(zipWriter *zip.Writer, filePath string, zipPath string) error {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开文件失败: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
writer, err := zipWriter.Create(zipPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建ZIP文件项失败: %w", err)
|
||||
}
|
||||
|
||||
_, err = io.Copy(writer, file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("复制文件内容失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddFolderToZip 递归添加文件夹到ZIP
|
||||
func (g *ZipGenerator) AddFolderToZip(zipWriter *zip.Writer, folderPath string, basePath string) error {
|
||||
return filepath.Walk(folderPath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 计算相对路径
|
||||
relPath, err := filepath.Rel(basePath, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 转换为ZIP路径格式(使用正斜杠)
|
||||
zipPath := filepath.ToSlash(relPath)
|
||||
|
||||
return g.AddFileToZip(zipWriter, path, zipPath)
|
||||
})
|
||||
}
|
||||
|
||||
// AddFileToZipWithTarget 将单个文件添加到ZIP的指定目标路径
|
||||
func (g *ZipGenerator) AddFileToZipWithTarget(zipWriter *zip.Writer, filePath string, targetPath string) error {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开文件失败: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
writer, err := zipWriter.Create(filepath.ToSlash(targetPath))
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建ZIP文件项失败: %w", err)
|
||||
}
|
||||
|
||||
_, err = io.Copy(writer, file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("复制文件内容失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddFolderToZipWithPrefix 递归添加文件夹到ZIP,并在ZIP内添加路径前缀
|
||||
func (g *ZipGenerator) AddFolderToZipWithPrefix(zipWriter *zip.Writer, folderPath string, basePath string, prefix string) error {
|
||||
return filepath.Walk(folderPath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(basePath, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
zipPath := filepath.ToSlash(filepath.Join(prefix, relPath))
|
||||
return g.AddFileToZip(zipWriter, path, zipPath)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user