addui
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
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)
|
||||
})
|
||||
}
|
||||
110
resources/Pure Component/index.html
Normal file
@@ -0,0 +1,110 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, maximum-scale=3, user-scalable=no"
|
||||
/>
|
||||
<title>报告查看器</title>
|
||||
<style>
|
||||
html {
|
||||
font-size: 16px;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-width: 320px;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 481px) and (max-width: 768px) {
|
||||
html {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
#app-loading {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #fff;
|
||||
z-index: 9999;
|
||||
font-family: Arial, sans-serif;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 5px solid #ccc;
|
||||
border-top: 5px solid #3498db;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app-loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-text">加载中</div>
|
||||
</div>
|
||||
<div id="app"></div>
|
||||
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const loadingElement = document.getElementById('app-loading');
|
||||
if (loadingElement) {
|
||||
loadingElement.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
loadingElement.parentNode.removeChild(loadingElement);
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
35
resources/Pure Component/package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "report-viewer",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^11.3.0",
|
||||
"axios": "^1.7.7",
|
||||
"echarts": "^5.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"vant": "^4.9.9",
|
||||
"vue": "^3.5.12",
|
||||
"vue-echarts": "^7.0.3",
|
||||
"vue-router": "^4.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vant/auto-import-resolver": "^1.3.0",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"@vitejs/plugin-vue-jsx": "^4.0.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.49",
|
||||
"sass-embedded": "^1.81.0",
|
||||
"tailwindcss": "^3.4.15",
|
||||
"terser": "^5.43.1",
|
||||
"unplugin-auto-import": "^0.18.5",
|
||||
"unplugin-vue-components": "^0.27.5",
|
||||
"vite": "^5.4.10"
|
||||
},
|
||||
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
||||
}
|
||||
3101
resources/Pure Component/pnpm-lock.yaml
generated
Normal file
7
resources/Pure Component/postcss.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
||||
10278
resources/Pure Component/public/example.json
Normal file
8
resources/Pure Component/src/App.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// App 根组件,仅用于路由
|
||||
</script>
|
||||
|
||||
25
resources/Pure Component/src/assets/base.css
Normal file
@@ -0,0 +1,25 @@
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
html {
|
||||
margin: auto !important;
|
||||
/* @apply max-w-lg; */
|
||||
min-width: 320px;
|
||||
}
|
||||
body {
|
||||
background-color: #f8f8f8;
|
||||
min-height: 100vh;
|
||||
transition: color 0.5s, background-color 0.5s;
|
||||
line-height: 1.6;
|
||||
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
54
resources/Pure Component/src/assets/colors.css
Normal file
@@ -0,0 +1,54 @@
|
||||
/* 统一颜色变量管理文件 */
|
||||
:root {
|
||||
/* ===== 主题色系 ===== */
|
||||
--color-primary: #5d7eeb;
|
||||
--color-primary-50: #f0f3ff;
|
||||
--color-primary-100: #e1e8ff;
|
||||
--color-primary-200: #c3d1ff;
|
||||
--color-primary-300: #a5baff;
|
||||
--color-primary-400: #87a3ff;
|
||||
--color-primary-500: #5d7eeb;
|
||||
--color-primary-600: #4a63bc;
|
||||
--color-primary-700: #38488d;
|
||||
--color-primary-800: #252d5e;
|
||||
--color-primary-900: #13122f;
|
||||
|
||||
--color-primary-light: rgba(93, 126, 235, 0.1);
|
||||
--color-primary-medium: rgba(93, 126, 235, 0.15);
|
||||
--color-primary-dark: rgba(93, 126, 235, 0.8);
|
||||
|
||||
/* ===== 语义化颜色 ===== */
|
||||
--color-success: #07c160;
|
||||
--color-warning: #ff976a;
|
||||
--color-danger: #ee0a24;
|
||||
--color-info: #1989fa;
|
||||
|
||||
/* ===== 中性色系 ===== */
|
||||
--color-gray-50: #fafafa;
|
||||
--color-gray-100: #f5f5f5;
|
||||
--color-gray-200: #e5e5e5;
|
||||
--color-gray-300: #d4d4d4;
|
||||
--color-gray-400: #a3a3a3;
|
||||
--color-gray-500: #737373;
|
||||
--color-gray-600: #525252;
|
||||
--color-gray-700: #404040;
|
||||
--color-gray-800: #262626;
|
||||
--color-gray-900: #171717;
|
||||
|
||||
/* ===== 文本颜色 ===== */
|
||||
--color-text-primary: #323233;
|
||||
--color-text-secondary: #646566;
|
||||
--color-text-tertiary: #969799;
|
||||
|
||||
/* ===== 背景颜色 ===== */
|
||||
--color-bg-primary: #ffffff;
|
||||
--color-bg-secondary: #fafafa;
|
||||
--color-bg-tertiary: #f8f8f8;
|
||||
|
||||
/* ===== 边框颜色 ===== */
|
||||
--color-border-primary: #ebedf0;
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
background-color: var(--color-primary) !important;
|
||||
}
|
||||
75
resources/Pure Component/src/assets/images/empty.svg
Normal file
@@ -0,0 +1,75 @@
|
||||
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 1024 1024" height="1024px" width="1024px">
|
||||
<title>空空如也</title>
|
||||
<defs>
|
||||
<rect rx="22.1405405" height="1024" width="1024" y="0" x="0" id="path-1"></rect>
|
||||
<linearGradient id="linearGradient-3" y2="64.8840762%" x2="50%" y1="-33.7184979%" x1="115.913479%">
|
||||
<stop offset="0%" stop-color="#6CADFF"></stop>
|
||||
<stop offset="100%" stop-opacity="0" stop-color="#FFFFFF"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="linearGradient-4" y2="100%" x2="70.4980572%" y1="-20.569195%" x1="10.5031837%">
|
||||
<stop offset="0%" stop-color="#6CADFF"></stop>
|
||||
<stop offset="100%" stop-opacity="0" stop-color="#FFFFFF"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="linearGradient-5" y2="104.73608%" x2="38.801584%" y1="-97.78046%" x1="100.191761%">
|
||||
<stop offset="0%" stop-color="#6CADFF"></stop>
|
||||
<stop offset="100%" stop-color="#FFFFFF"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="linearGradient-6" y2="100%" x2="50%" y1="-27.9013949%" x1="50%">
|
||||
<stop offset="0%" stop-color="#6CADFF"></stop>
|
||||
<stop offset="100%" stop-opacity="0" stop-color="#FFFFFF"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="linearGradient-7" y2="100%" x2="50%" y1="-27.9013949%" x1="50%">
|
||||
<stop offset="0%" stop-color="#6CADFF"></stop>
|
||||
<stop offset="100%" stop-opacity="0" stop-color="#FFFFFF"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="linearGradient-8" y2="100%" x2="50%" y1="-221.1569%" x1="50%">
|
||||
<stop offset="0%" stop-color="#D2D2D2"></stop>
|
||||
<stop offset="100%" stop-opacity="0" stop-color="#D2D2D2"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="linearGradient-9" y2="53.7335012%" x2="73.0360423%" y1="48.1527472%" x1="67.5652976%">
|
||||
<stop offset="0%" stop-opacity="0" stop-color="#858585"></stop>
|
||||
<stop offset="100%" stop-opacity="0.5" stop-color="#616161"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g fill-rule="evenodd" fill="none" stroke-width="1" stroke="none" id="空空如也">
|
||||
<g>
|
||||
<mask fill="white" id="mask-2">
|
||||
<use xlink:href="#path-1"></use>
|
||||
</mask>
|
||||
<g id="蒙版"></g>
|
||||
<g mask="url(#mask-2)" id="编组-3">
|
||||
<g transform="translate(0, 238.0108)">
|
||||
<g fill-rule="evenodd" fill="none" stroke-width="1" stroke="none" id="编组-2">
|
||||
<g fill-rule="nonzero" transform="translate(162.5599, 0)" id="编组">
|
||||
<polygon points="592.450826 498.100707 589.3555 489.432325 587.255101 479.632083 586.398359 469.500567 586.868185 459.341443 588.001295 452.660716 589.852963 446.421689 592.367915 440.541544 595.711972 435.158313 599.857498 430.520452 604.887401 426.517537 606.932527 428.091097 608.342006 430.133964 609.060564 432.508107 609.032927 435.075494 608.286732 439.989418 607.070711 451.308006 606.794343 463.813666 607.540538 469.25211 608.894743 472.371623 610.248947 473.448269 612.072979 473.393057 614.864299 471.791891 619.037461 467.319668 626.084854 456.884481 638.963619 434.136879 651.870021 411.527309 659.000324 400.705634 667.015006 390.325661 673.758394 383.451689 680.225413 378.42734 686.471338 374.921338 693.159452 372.491982 699.156645 371.470549 704.628738 371.691399 709.8521 373.126928 714.191083 375.639102 717.783871 379.283135 720.32646 383.755358 721.818849 389.331833 722.150491 396.343837 721.155565 403.328234 718.530066 411.610128 713.942351 421.493188 708.884811 429.471413 700.400302 443.19175 696.006046 451.031943 692.219799 458.706498 689.345568 466.270628 688.212458 472.09556 688.46119 475.601562 689.760121 477.920492 692.109252 479.438839 695.232214 479.714902 698.299903 479.052351 705.070927 476.540176 711.924862 473.393057 718.778797 470.383969 725.384001 468.258282 729.805894 467.595731 734.144877 467.8994 738.511497 469.224503 741.966102 471.377796 744.702148 474.055608 746.857821 477.368366 748.985858 483.000054 750.008421 489.349506 750.036057 495.864596 749.206952 502.158835 746.526179 511.40695 742.104286 520.737884 736.21764 529.406266 728.866242 537.16364 723.283601 541.608256 717.203498 545.25229 710.570657 548.123346 703.606175 550.000575 696.199503 550.745946 688.212458 550.359458 675.554788 548.09574 661.238907 544.396494 646.923027 539.537783 632.855878 533.464394 623.76336 528.688502 615.582857 523.526121 608.203822 517.977252 601.543344 511.683013 596.375256 505.140317 592.533736 498.349164" fill="url(#linearGradient-3)" id="路径"></polygon>
|
||||
<polygon points="39.9075893 440.458725 31.7823599 436.096928 23.6571305 430.216783 16.222822 423.287598 9.75580266 415.447406 4.58771456 406.834236 1.21602073 397.834578 0.0552736694 391.623157 0 385.411737 1.05019972 379.117498 3.59278851 378.896647 5.99719313 379.421167 8.12522941 380.635845 9.83871316 382.54068 12.6023966 386.654021 19.2905106 395.87453 27.4986505 405.315889 31.6994494 408.849497 34.7947749 410.257419 36.5082587 410.146993 37.8348267 408.877103 38.8297528 405.840409 38.9403001 399.711807 37.1439059 387.26136 31.3401706 361.863552 25.7022563 336.465744 23.7676779 323.601202 22.8003886 310.488203 23.242578 300.881206 24.6520566 292.820163 26.9459139 286.056616 30.2899709 279.789983 34.0762172 275.014091 38.2770161 271.508089 43.1134622 269.078734 48.0051819 268.0573 53.1179963 268.333363 57.9820792 269.934529 62.8185253 273.081649 67.7655187 278.050785 71.6899493 283.903324 75.3103746 291.798729 78.5162474 302.206309 80.1191838 311.509637 83.0210515 327.383267 85.0385404 336.134468 87.4153082 344.361149 90.3448127 351.870067 93.4125013 356.977234 95.9550901 359.378984 98.4700421 360.234779 101.206089 359.765472 103.748678 357.888243 105.572709 355.320856 108.916766 348.916191 111.873907 342.014613 114.913959 335.195853 118.368563 329.23289 121.215157 325.782101 124.642125 323.159501 128.78765 321.254665 132.794991 320.509295 136.636511 320.674933 140.450394 321.696366 145.81194 324.429391 150.841844 328.459913 155.236101 333.291018 158.856526 338.508611 162.670409 345.575827 166.788298 354.823942 170.381086 364.762215 173.448775 375.639102 175.604448 386.764446 176.5441 397.641334 176.378279 404.874188 175.41099 411.499703 173.697506 417.628304 171.04437 423.315205 167.396308 428.173916 162.642772 432.314863 157.032495 435.62762 150.344381 438.691921 142.440246 441.424946 130.058944 444.572066 116.2958 446.835784 102.173378 448.160887 87.9956817 448.547375 74.0390802 447.967642 61.3537731 446.504508 49.4422973 443.964727 40.1563208 440.707182" fill="url(#linearGradient-4)" id="路径"></polygon>
|
||||
<path fill="url(#linearGradient-5)" id="形状结合" d="M648.498327,284.510663 L644.71208,289.9215 L641.589118,295.939676 L639.350534,302.344341 L638.217424,308.914643 L638.355608,315.567765 L640.013818,322.165674 L642.224765,326.058164 L644.988449,328.92922 L647.94559,331.579426 L650.571089,334.505696 L652.284573,338.039304 L652.83731,343.588173 L651.648926,349.689168 L649.576163,355.458887 L646.619022,360.952544 L640.428371,370.780391 L633.408615,379.945687 L625.615028,388.53125 L630.092195,393.086292 L633.159883,398.524736 L634.735183,404.570518 L634.707546,410.947577 L633.215157,417.186603 L630.755479,423.066748 L627.411422,428.477585 L623.210623,433.336296 L620.115297,436.234959 L616.384325,438.25022 L612.459894,438.691921 L608.535464,438.25022 L595.960704,435.517195 L583.77286,431.624705 L571.944295,426.57275 L566.555112,423.729299 L561.608118,420.250904 L557.545504,416.027138 L554.864731,410.91997 L554.035626,406.723811 L554.25672,403.769935 L555.251646,401.699462 L557.877146,399.049256 L561.41466,396.895963 L562.685955,396.178199 L566.30638,393.886875 L569.180611,390.822574 L571.253374,386.129501 L572.524668,380.939514 L574.127604,370.55954 L575.150167,359.075314 L575.896362,352.781075 L577.444025,347.121781 L579.212782,343.864236 L581.783008,341.352061 L585.292886,339.557651 L592.533736,338.260154 L599.74695,336.935051 L604.389938,334.754152 L608.176185,331.827883 L611.243873,328.128637 L614.974846,321.696366 L618.180719,314.518725 L618.899277,312.751921 L622.630249,304.663271 L624.785922,300.908813 L627.273238,297.568449 L630.313289,294.86303 L634.292994,292.461281 L642.611681,288.430759 L646.28738,286.387892 L648.498327,284.510663 Z M619.009824,341.73855 L615.195941,346.514442 L606.158696,359.323771 L600.935334,367.854122 L595.463241,378.013245 L590.626795,388.807313 L586.702364,400.346752 L584.795423,408.269764 L583.717586,416.192776 L583.468855,424.115788 L595.325057,424.115788 L596.319983,416.109957 L599.912771,395.929742 L602.925186,383.313657 L607.153622,369.234437 L612.432257,355.238037 L619.009824,341.73855 Z"></path>
|
||||
<polygon points="125.250135 60.7614951 125.250135 60.7614951 124.559214 54.3016178 122.431178 48.4214731 119.059484 43.2314863 114.55468 38.9249014 109.165497 35.7777818 102.947209 33.9005525 96.4525532 33.5416704 90.3171759 34.7011355 84.6239879 37.21331 79.6769945 40.9677686 75.6972903 45.7712671 72.8783332 51.6238055 66.8258663 52.3415696 61.3537731 54.5500746 56.6278743 58.111289 52.9245385 62.9147875 50.658318 68.5464754 49.9673972 74.3990138 50.8517759 80.2515521 53.3114542 85.8004211 57.1529742 90.4934943 61.9894203 93.8890708 67.5444241 95.931938 73.569254 96.4564579 123.011551 96.4564579 127.626903 95.7663001 131.634244 94.1375276 135.171759 91.5149279 137.963079 88.1193514 139.78711 84.1992549 140.699126 79.6442132 140.367484 74.9787463 139.013279 70.8654057 136.664148 67.1661597 133.513549 64.1294653 129.727302 62.0037792" fill="url(#linearGradient-6)" id="路径"></polygon>
|
||||
<polygon points="329.569254 33.7073083 329.569254 33.5416704 329.127065 28.130833 327.911044 23.0788777 325.921192 18.3305919 321.665119 11.9259273 316.054842 6.65312145 311.715859 3.89249014 306.934686 1.84962298 301.656051 0.496913635 296.266868 0 291.071143 0.35888207 286.041239 1.49074091 278.993846 4.61025428 272.858469 9.22050857 269.376228 13.0301798 266.557271 17.3643709 264.318687 22.305901 259.205873 22.8856335 254.507611 24.2383429 250.196265 26.364029 246.299471 29.2074792 243.010688 32.6030557 240.302278 36.5783648 238.284789 40.9677686 237.096405 45.6608418 236.681853 50.7956161 237.234589 55.902784 238.588794 60.5682509 240.744467 64.8748357 243.591061 68.7673259 246.990392 72.0524771 250.970096 74.7855021 255.364353 76.7731567 260.062615 77.9878345 265.203066 78.3743228 326.612113 78.3743228 332.360574 77.6565587 337.501026 75.5860852 341.950556 72.3285403 345.488071 68.1047744 347.892475 63.1080317 348.997949 57.4211312 348.611033 51.6514118 346.842276 46.378606 343.885134 41.7683517 339.877793 37.9862868 335.041347 35.2808681 329.403433 33.8453398" fill="url(#linearGradient-7)" id="路径"></polygon>
|
||||
</g>
|
||||
<polygon points="1024 550.359458 999.596675 534.403009 973.839145 519.164324 946.672136 504.615797 918.040376 490.785034 889.159883 478.224161 859.118644 466.491478 827.833747 455.669804 795.305193 445.703925 762.693728 437.007936 729.114974 429.360987 694.541293 422.735472 658.917413 417.186603 623.348807 412.880018 587.034006 409.760505 549.945374 407.883276 512 407.193118 474.054626 407.883276 436.965994 409.760505 400.623556 412.880018 365.082587 417.186603 329.458707 422.735472 294.885026 429.360987 261.306272 437.007936 228.694807 445.703925 196.166253 455.669804 164.881356 466.491478 134.840117 478.224161 105.931987 490.785034 77.3278635 504.615797 50.160855 519.164324 24.4033251 534.403009 0 550.359458" fill-rule="nonzero" fill="url(#linearGradient-8)" id="路径"></polygon>
|
||||
</g>
|
||||
<polygon points="168.585366 389.215532 314.612039 389.215532 461.536985 469.266954 317.214646 465.594981" fill-rule="nonzero" fill="url(#linearGradient-9)" stroke="none" id="矩形"></polygon>
|
||||
<polygon points="481.155803 208.25638 479.722777 299.451613 688.569371 236.303345" fill-rule="nonzero" fill="#B8D6FF" stroke="none" id="路径"></polygon>
|
||||
<polygon points="314.788219 244.959395 481.155803 208.631248 481.155803 264.00952" fill-rule="nonzero" fill="#9CC6FF" stroke="none" id="路径"></polygon>
|
||||
<polygon points="314.788219 244.959395 511.147006 264.384388 511.147006 512.547202 314.788219 465.075243" fill-rule="nonzero" fill="#64ADFF" stroke="none" id="路径"></polygon>
|
||||
<polygon points="314.788219 244.959395 511.283486 263.617612 489.889892 383.428742 314.788219 346.994453" fill-rule="nonzero" fill="#429BFF" stroke="none" id="矩形"></polygon>
|
||||
<polygon points="511.147006 264.384388 688.569371 236.303345 688.569371 458.600245 511.147006 512.547202" opacity="0.99" fill-rule="nonzero" fill="#9CC5FF" stroke="none" id="路径"></polygon>
|
||||
<polygon points="511.283486 264.997809 671.897967 239.34913 688.569371 344.292902 535.025228 383.428742" fill-rule="nonzero" fill="#64ADFF" stroke="none" id="矩形"></polygon>
|
||||
<polygon points="314.788219 244.959395 267.566573 324.806343 465.801946 362.565804 511.147006 264.384388" fill-rule="nonzero" fill="#9CC6FF" stroke="none" id="路径"></polygon>
|
||||
<polygon points="511.147006 264.384388 566.898574 362.565804 745.583366 317.786082 688.569371 236.303345" opacity="0.99" fill-rule="nonzero" fill="#9DC6FF" stroke="none" id="路径"></polygon>
|
||||
<path stroke-dasharray="11.05477807439905,8.29108355579929" fill="none" stroke-width="5.52738904" stroke="#9DC6FF" id="路径-19" d="M583.139543,151.843183 C532.695151,184.875351 501.824507,214.257045 511.001904,232.450753 C523.475834,257.179661 544.659409,246.913618 547.874,236.537816 C551.088588,226.162013 542.242035,205.908265 523.475834,216.933951 C504.709635,227.959637 484.261479,247.732311 479.722777,267.098145"></path>
|
||||
<g transform="translate(555.0939, 41.4059)" fill-rule="evenodd" fill="none" stroke-width="1" stroke="none" id="飞机">
|
||||
<polygon points="163.057977 9.09494702e-13 0 30.854292 41.4554178 58.3378535" fill-rule="nonzero" fill="#9DC6FF" id="路径-16备份"></polygon>
|
||||
<polygon points="163.057977 0 41.4554178 58.3378535 41.4554178 104.894966" fill-rule="nonzero" fill="#64ADFF" id="路径-16备份-2"></polygon>
|
||||
<polygon points="163.057977 0 41.4554178 58.3378535 65.4910753 84.1769753" fill-rule="nonzero" fill="#429BFF" id="路径-16备份-2"></polygon>
|
||||
<polygon points="163.057977 0 58.9237102 70.0692202 108.951745 102.134572" fill-rule="nonzero" fill="#9DC6FF" id="路径-16备份"></polygon>
|
||||
<line stroke-linecap="round" stroke-width="2.76369452" stroke="#429BFF" id="路径-16" y2="32.1173295" x2="0.501635492" y1="58.3378535" x1="41.4554178"></line>
|
||||
<line stroke-linecap="round" stroke-width="2.76369452" stroke="#429BFF" id="路径-17" y2="101.744072" x2="107.648858" y1="71.0666294" x1="59.7211085"></line>
|
||||
<line stroke-linecap="round" stroke-width="2.76369452" stroke="#429BFF" id="路径-18" y2="103.502333" x2="41.4554178" y1="58.3378535" x1="41.4554178"></line>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 11 KiB |
BIN
resources/Pure Component/src/assets/images/report/ajgl.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
resources/Pure Component/src/assets/images/report/ajlxfb.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 190 KiB |
BIN
resources/Pure Component/src/assets/images/report/bysj.png
Normal file
|
After Width: | Height: | Size: 816 B |
BIN
resources/Pure Component/src/assets/images/report/dkxwfx.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 37 KiB |
BIN
resources/Pure Component/src/assets/images/report/dwtzls.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
resources/Pure Component/src/assets/images/report/fqzgz.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
resources/Pure Component/src/assets/images/report/fqzpf.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
resources/Pure Component/src/assets/images/report/fsbq.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
resources/Pure Component/src/assets/images/report/fx.png
Normal file
|
After Width: | Height: | Size: 286 B |
BIN
resources/Pure Component/src/assets/images/report/fxbs.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
resources/Pure Component/src/assets/images/report/fxgl.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
resources/Pure Component/src/assets/images/report/fxmd.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
resources/Pure Component/src/assets/images/report/fxzbxq.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
resources/Pure Component/src/assets/images/report/fxzl.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
resources/Pure Component/src/assets/images/report/fybgt.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
resources/Pure Component/src/assets/images/report/gazdryhy.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
resources/Pure Component/src/assets/images/report/gazdryhycp.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
resources/Pure Component/src/assets/images/report/gfx.png
Normal file
|
After Width: | Height: | Size: 504 B |
BIN
resources/Pure Component/src/assets/images/report/gl.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
resources/Pure Component/src/assets/images/report/glfxjd.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
resources/Pure Component/src/assets/images/report/glfxjd2.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
resources/Pure Component/src/assets/images/report/glsfz.png
Normal file
|
After Width: | Height: | Size: 619 B |
BIN
resources/Pure Component/src/assets/images/report/glsjh.png
Normal file
|
After Width: | Height: | Size: 493 B |
BIN
resources/Pure Component/src/assets/images/report/glzdryhy2.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 32 KiB |
BIN
resources/Pure Component/src/assets/images/report/gsdfx.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
resources/Pure Component/src/assets/images/report/hktj.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
resources/Pure Component/src/assets/images/report/hkylfx.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 41 KiB |
BIN
resources/Pure Component/src/assets/images/report/j24gyfkqk.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
resources/Pure Component/src/assets/images/report/jdpg.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
resources/Pure Component/src/assets/images/report/jdpggl.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
resources/Pure Component/src/assets/images/report/jgfx.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
resources/Pure Component/src/assets/images/report/jgztxx.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
resources/Pure Component/src/assets/images/report/jyyc.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
resources/Pure Component/src/assets/images/report/khlxjdbx.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
resources/Pure Component/src/assets/images/report/lsjdxw.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
resources/Pure Component/src/assets/images/report/lyqk.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
resources/Pure Component/src/assets/images/report/lyxxxq.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 30 KiB |
BIN
resources/Pure Component/src/assets/images/report/ms.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
resources/Pure Component/src/assets/images/report/mzfxbz.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
resources/Pure Component/src/assets/images/report/qsgg.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
resources/Pure Component/src/assets/images/report/qspc.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
resources/Pure Component/src/assets/images/report/rkpm.png
Normal file
|
After Width: | Height: | Size: 1000 B |
BIN
resources/Pure Component/src/assets/images/report/rzls.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
resources/Pure Component/src/assets/images/report/sagg.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
resources/Pure Component/src/assets/images/report/sdszhycp.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
resources/Pure Component/src/assets/images/report/sdszryhy.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
resources/Pure Component/src/assets/images/report/sfxxhy.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
resources/Pure Component/src/assets/images/report/sfz.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
resources/Pure Component/src/assets/images/report/shjy.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
resources/Pure Component/src/assets/images/report/sjh.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |