add购买记录功能
1
.gitignore
vendored
@@ -48,3 +48,4 @@ resources/Pure_Component/
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
cmd/api/__debug_bin*
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
## 一、功能概述
|
||||
|
||||
在产品详情页面添加"下载示例报告"功能,允许用户下载与产品对应的前端组件报告。报告文件位于 `resources/Pure Component/src/ui` 目录下,通过产品编号(product_code)匹配对应的文件夹或文件。
|
||||
在产品详情页面添加"下载示例报告"功能,允许用户下载与产品对应的前端组件报告。报告文件位于 `resources/Pure_Component/src/ui` 目录下,通过产品编号(product_code)匹配对应的文件夹或文件。
|
||||
|
||||
## 二、核心需求
|
||||
|
||||
### 2.1 基本功能
|
||||
|
||||
1. **报告匹配**:根据子产品的 `product_code` 模糊匹配 `resources/Pure Component/src/ui` 下的文件夹或文件
|
||||
1. **报告匹配**:根据子产品的 `product_code` 模糊匹配 `resources/Pure_Component/src/ui` 下的文件夹或文件
|
||||
- 支持前缀匹配(如产品编号为 `DWBG6A2C`,文件夹可能是 `DWBG6A2C` 或 `多cDWBG6A2C`)
|
||||
- 匹配规则:文件夹名称包含产品编号,或产品编号包含文件夹名称的核心部分
|
||||
|
||||
@@ -537,7 +537,7 @@ func (s *ComponentReportServiceImpl) MatchProductCodeToPath(ctx context.Context,
|
||||
}
|
||||
|
||||
// 2. 扫描目录
|
||||
basePath := "resources/Pure Component/src/ui"
|
||||
basePath := "resources/Pure_Component/src/ui"
|
||||
entries, err := os.ReadDir(basePath)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
@@ -807,7 +807,7 @@ func (s *ComponentReportServiceImpl) GenerateZipFile(ctx context.Context, produc
|
||||
defer zipWriter.Close()
|
||||
|
||||
// 3. 遍历子产品,添加UI组件文件到ZIP
|
||||
basePath := "resources/Pure Component/src/ui"
|
||||
basePath := "resources/Pure_Component/src/ui"
|
||||
for _, productCode := range subProductCodes {
|
||||
path, fileType, err := s.MatchProductCodeToPath(ctx, productCode)
|
||||
if err != nil {
|
||||
@@ -847,7 +847,7 @@ func (s *ComponentReportServiceImpl) GenerateZipFile(ctx context.Context, produc
|
||||
|
||||
// 5. 添加其他必要的文件(如果需要)
|
||||
// 例如:复制 public 目录下的其他文件(如果有)
|
||||
publicBasePath := "resources/Pure Component/public"
|
||||
publicBasePath := "resources/Pure_Component/public"
|
||||
publicFiles, err := os.ReadDir(publicBasePath)
|
||||
if err == nil {
|
||||
for _, file := range publicFiles {
|
||||
|
||||
@@ -231,6 +231,7 @@ func (a *Application) autoMigrate(db *gorm.DB) error {
|
||||
&financeEntities.AlipayOrder{},
|
||||
&financeEntities.InvoiceApplication{},
|
||||
&financeEntities.UserInvoiceInfo{},
|
||||
&financeEntities.PurchaseOrder{}, //购买组件订单表
|
||||
|
||||
// 产品域
|
||||
&productEntities.Product{},
|
||||
|
||||
@@ -1292,7 +1292,7 @@ func (s *ApiApplicationServiceImpl) UpdateUserBalanceAlertSettings(ctx context.C
|
||||
// TestBalanceAlertSms 测试余额预警短信
|
||||
func (s *ApiApplicationServiceImpl) TestBalanceAlertSms(ctx context.Context, userID string, phone string, balance float64, alertType string) error {
|
||||
// 获取用户信息以获取企业名称
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
user, err := s.userRepo.GetByIDWithEnterpriseInfo(ctx, userID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取用户信息失败",
|
||||
zap.String("user_id", userID),
|
||||
|
||||
@@ -125,3 +125,45 @@ type UserSimpleResponse struct {
|
||||
CompanyName string `json:"company_name"`
|
||||
Phone string `json:"phone"`
|
||||
}
|
||||
|
||||
// PurchaseRecordResponse 购买记录响应
|
||||
type PurchaseRecordResponse struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
OrderNo string `json:"order_no"`
|
||||
TradeNo *string `json:"trade_no,omitempty"`
|
||||
ProductID string `json:"product_id"`
|
||||
ProductCode string `json:"product_code"`
|
||||
ProductName string `json:"product_name"`
|
||||
Category string `json:"category,omitempty"`
|
||||
Subject string `json:"subject"`
|
||||
Amount decimal.Decimal `json:"amount"`
|
||||
PayAmount *decimal.Decimal `json:"pay_amount,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Platform string `json:"platform"`
|
||||
PayChannel string `json:"pay_channel"`
|
||||
PaymentType string `json:"payment_type"`
|
||||
BuyerID string `json:"buyer_id,omitempty"`
|
||||
SellerID string `json:"seller_id,omitempty"`
|
||||
ReceiptAmount decimal.Decimal `json:"receipt_amount,omitempty"`
|
||||
NotifyTime *time.Time `json:"notify_time,omitempty"`
|
||||
ReturnTime *time.Time `json:"return_time,omitempty"`
|
||||
PayTime *time.Time `json:"pay_time,omitempty"`
|
||||
FilePath *string `json:"file_path,omitempty"`
|
||||
FileSize *int64 `json:"file_size,omitempty"`
|
||||
Remark string `json:"remark,omitempty"`
|
||||
ErrorCode string `json:"error_code,omitempty"`
|
||||
ErrorMessage string `json:"error_message,omitempty"`
|
||||
CompanyName string `json:"company_name,omitempty"`
|
||||
User *UserSimpleResponse `json:"user,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// PurchaseRecordListResponse 购买记录列表响应
|
||||
type PurchaseRecordListResponse struct {
|
||||
Items []PurchaseRecordResponse `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Size int `json:"size"`
|
||||
}
|
||||
|
||||
@@ -43,6 +43,10 @@ type FinanceApplicationService interface {
|
||||
GetUserRechargeRecords(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error)
|
||||
GetAdminRechargeRecords(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error)
|
||||
|
||||
// 购买记录
|
||||
GetUserPurchaseRecords(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.PurchaseRecordListResponse, error)
|
||||
GetAdminPurchaseRecords(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.PurchaseRecordListResponse, error)
|
||||
|
||||
// 获取充值配置
|
||||
GetRechargeConfig(ctx context.Context) (*responses.RechargeConfigResponse, error)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
"tyapi-server/internal/application/finance/dto/commands"
|
||||
"tyapi-server/internal/application/finance/dto/queries"
|
||||
@@ -15,6 +14,7 @@ import (
|
||||
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/component_report"
|
||||
"tyapi-server/internal/shared/database"
|
||||
"tyapi-server/internal/shared/export"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
@@ -36,6 +36,7 @@ type FinanceApplicationServiceImpl struct {
|
||||
alipayOrderRepo finance_repositories.AlipayOrderRepository
|
||||
wechatOrderRepo finance_repositories.WechatOrderRepository
|
||||
rechargeRecordRepo finance_repositories.RechargeRecordRepository
|
||||
purchaseOrderRepo finance_repositories.PurchaseOrderRepository
|
||||
componentReportRepo product_repositories.ComponentReportRepository
|
||||
userRepo user_repositories.UserRepository
|
||||
txManager *database.TransactionManager
|
||||
@@ -54,6 +55,7 @@ func NewFinanceApplicationService(
|
||||
alipayOrderRepo finance_repositories.AlipayOrderRepository,
|
||||
wechatOrderRepo finance_repositories.WechatOrderRepository,
|
||||
rechargeRecordRepo finance_repositories.RechargeRecordRepository,
|
||||
purchaseOrderRepo finance_repositories.PurchaseOrderRepository,
|
||||
componentReportRepo product_repositories.ComponentReportRepository,
|
||||
userRepo user_repositories.UserRepository,
|
||||
txManager *database.TransactionManager,
|
||||
@@ -70,6 +72,7 @@ func NewFinanceApplicationService(
|
||||
alipayOrderRepo: alipayOrderRepo,
|
||||
wechatOrderRepo: wechatOrderRepo,
|
||||
rechargeRecordRepo: rechargeRecordRepo,
|
||||
purchaseOrderRepo: purchaseOrderRepo,
|
||||
componentReportRepo: componentReportRepo,
|
||||
userRepo: userRepo,
|
||||
txManager: txManager,
|
||||
@@ -854,13 +857,7 @@ func (s *FinanceApplicationServiceImpl) HandleAlipayCallback(ctx context.Context
|
||||
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("处理支付宝支付成功失败",
|
||||
@@ -886,20 +883,52 @@ func (s *FinanceApplicationServiceImpl) processAlipayPaymentSuccess(ctx context.
|
||||
return err
|
||||
}
|
||||
|
||||
// 直接调用充值记录服务处理支付成功逻辑
|
||||
// 该服务内部会处理所有必要的检查、事务和更新操作
|
||||
// 如果是组件报告下载订单,服务会自动跳过钱包余额增加
|
||||
err = s.rechargeRecordService.HandleAlipayPaymentSuccess(ctx, outTradeNo, amount, tradeNo)
|
||||
// 查找支付宝订单
|
||||
alipayOrder, err := s.alipayOrderRepo.GetByOutTradeNo(ctx, outTradeNo)
|
||||
if err != nil {
|
||||
s.logger.Error("处理支付宝支付成功失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
s.logger.Error("查找支付宝订单失败", zap.String("out_trade_no", outTradeNo), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查并更新组件报告下载记录状态(如果存在)
|
||||
s.updateComponentReportDownloadStatus(ctx, outTradeNo)
|
||||
if alipayOrder == nil {
|
||||
s.logger.Error("支付宝订单不存在", zap.String("out_trade_no", outTradeNo))
|
||||
return fmt.Errorf("支付宝订单不存在")
|
||||
}
|
||||
|
||||
// 判断是否为充值订单还是购买订单
|
||||
_, err = s.rechargeRecordRepo.GetByID(ctx, alipayOrder.RechargeID)
|
||||
if err == nil {
|
||||
// 这是充值订单,调用充值记录服务处理支付成功逻辑
|
||||
err = s.rechargeRecordService.HandleAlipayPaymentSuccess(ctx, outTradeNo, amount, tradeNo)
|
||||
if err != nil {
|
||||
s.logger.Error("处理支付宝充值支付成功失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// 尝试查找购买订单
|
||||
_, err = s.purchaseOrderRepo.GetByID(ctx, alipayOrder.RechargeID)
|
||||
if err == nil {
|
||||
// 这是购买订单(可能是示例报告购买订单),调用处理购买订单支付成功逻辑
|
||||
err = s.processPurchaseOrderPaymentSuccess(ctx, alipayOrder.RechargeID, tradeNo, amount, buyerID, sellerID)
|
||||
if err != nil {
|
||||
s.logger.Error("处理支付宝购买订单支付成功失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("purchase_order_id", alipayOrder.RechargeID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
s.logger.Error("无法确定订单类型",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("recharge_id", alipayOrder.RechargeID),
|
||||
)
|
||||
return fmt.Errorf("无法确定订单类型")
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("支付宝支付成功处理完成",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
@@ -1477,30 +1506,7 @@ func (s *FinanceApplicationServiceImpl) HandleWechatPayCallback(ctx context.Cont
|
||||
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("处理微信支付成功失败",
|
||||
@@ -1535,26 +1541,34 @@ func (s *FinanceApplicationServiceImpl) processWechatPaymentSuccess(ctx context.
|
||||
return fmt.Errorf("微信订单不存在")
|
||||
}
|
||||
|
||||
// 查找对应的充值记录
|
||||
// 判断是否为充值订单还是购买订单
|
||||
rechargeRecord, err := s.rechargeRecordService.GetByID(ctx, wechatOrder.RechargeID)
|
||||
if err != nil {
|
||||
s.logger.Error("查找充值记录失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("recharge_id", wechatOrder.RechargeID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("查找充值记录失败: %w", err)
|
||||
if err == nil {
|
||||
// 这是充值订单,继续原有的处理逻辑
|
||||
} else {
|
||||
// 尝试查找购买订单
|
||||
_, err = s.purchaseOrderRepo.GetByID(ctx, wechatOrder.RechargeID)
|
||||
if err == nil {
|
||||
// 这是购买订单(可能是示例报告购买订单),调用处理购买订单支付成功逻辑
|
||||
err = s.processPurchaseOrderPaymentSuccess(ctx, wechatOrder.RechargeID, transactionID, amount, "", "")
|
||||
if err != nil {
|
||||
s.logger.Error("处理微信购买订单支付成功失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("purchase_order_id", wechatOrder.RechargeID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
s.logger.Error("无法确定订单类型",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("recharge_id", wechatOrder.RechargeID),
|
||||
)
|
||||
return fmt.Errorf("无法确定订单类型")
|
||||
}
|
||||
}
|
||||
|
||||
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("微信支付订单已处理成功,跳过重复处理",
|
||||
@@ -1562,12 +1576,7 @@ 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
|
||||
}
|
||||
|
||||
@@ -1638,33 +1647,17 @@ func (s *FinanceApplicationServiceImpl) processWechatPaymentSuccess(ctx context.
|
||||
)
|
||||
}
|
||||
|
||||
// 检查是否是组件报告下载订单(通过备注判断)
|
||||
isComponentReportOrder := strings.Contains(rechargeRecord.Notes, "购买") && strings.Contains(rechargeRecord.Notes, "报告示例")
|
||||
|
||||
if isComponentReportOrder {
|
||||
s.logger.Info("步骤5: 检测到组件报告下载订单,不增加钱包余额",
|
||||
// 充值到钱包(包含赠送金额)
|
||||
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("recharge_id", rechargeRecord.ID),
|
||||
zap.String("notes", rechargeRecord.Notes),
|
||||
zap.String("user_id", rechargeRecord.UserID),
|
||||
zap.String("total_amount", totalRechargeAmount.String()),
|
||||
zap.Error(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 err
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -1680,105 +1673,129 @@ 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)
|
||||
// processPurchaseOrderPaymentSuccess 处理购买订单支付成功的逻辑
|
||||
func (s *FinanceApplicationServiceImpl) processPurchaseOrderPaymentSuccess(ctx context.Context, purchaseOrderID, tradeNo string, amount decimal.Decimal, buyerID, sellerID string) error {
|
||||
// 查找购买订单
|
||||
purchaseOrder, err := s.purchaseOrderRepo.GetByID(ctx, purchaseOrderID)
|
||||
if err != nil {
|
||||
s.logger.Info("未找到组件报告下载记录,可能不是组件报告下载订单",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
s.logger.Error("查找购买订单失败",
|
||||
zap.String("purchase_order_id", purchaseOrderID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return
|
||||
return fmt.Errorf("查找购买订单失败: %w", err)
|
||||
}
|
||||
|
||||
if download == nil {
|
||||
s.logger.Info("组件报告下载记录为空,跳过更新",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
if purchaseOrder == nil {
|
||||
s.logger.Error("购买订单不存在",
|
||||
zap.String("purchase_order_id", purchaseOrderID),
|
||||
)
|
||||
return
|
||||
return fmt.Errorf("购买订单不存在")
|
||||
}
|
||||
|
||||
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),
|
||||
// 检查订单状态,如果已支付则跳过
|
||||
if purchaseOrder.Status == finance_entities.PurchaseOrderStatusPaid {
|
||||
s.logger.Info("购买订单已支付,跳过处理",
|
||||
zap.String("purchase_order_id", purchaseOrderID),
|
||||
)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
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)
|
||||
// 更新购买订单状态
|
||||
purchaseOrder.MarkPaid(tradeNo, buyerID, sellerID, amount, amount)
|
||||
err = s.purchaseOrderRepo.Update(ctx, purchaseOrder)
|
||||
if err != nil {
|
||||
s.logger.Error("更新组件报告下载记录状态失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("download_id", download.ID),
|
||||
s.logger.Error("更新购买订单状态失败",
|
||||
zap.String("purchase_order_id", purchaseOrderID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return
|
||||
return fmt.Errorf("更新购买订单状态失败: %w", err)
|
||||
}
|
||||
|
||||
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),
|
||||
// 更新对应的支付订单状态(微信或支付宝)
|
||||
if purchaseOrder.PayChannel == "alipay" {
|
||||
alipayOrder, err := s.alipayOrderRepo.GetByRechargeID(ctx, purchaseOrderID)
|
||||
if err == nil && alipayOrder != nil {
|
||||
alipayOrder.MarkSuccess(tradeNo, buyerID, sellerID, amount, amount)
|
||||
err = s.alipayOrderRepo.Update(ctx, *alipayOrder)
|
||||
if err != nil {
|
||||
s.logger.Error("更新支付宝订单状态失败",
|
||||
zap.String("out_trade_no", alipayOrder.OutTradeNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if purchaseOrder.PayChannel == "wechat" {
|
||||
wechatOrder, err := s.wechatOrderRepo.GetByRechargeID(ctx, purchaseOrderID)
|
||||
if err == nil && wechatOrder != nil {
|
||||
wechatOrder.MarkSuccess(tradeNo, buyerID, sellerID, amount, amount)
|
||||
err = s.wechatOrderRepo.Update(ctx, *wechatOrder)
|
||||
if err != nil {
|
||||
s.logger.Error("更新微信订单状态失败",
|
||||
zap.String("out_trade_no", wechatOrder.OutTradeNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是组件报告购买,需要生成并更新报告文件
|
||||
download, err := s.componentReportRepo.GetDownloadByPaymentOrderID(ctx, purchaseOrderID)
|
||||
if err == nil && download != nil {
|
||||
// 创建报告生成器
|
||||
zipGenerator := component_report.NewZipGenerator(s.logger)
|
||||
|
||||
// 生成报告文件
|
||||
zipPath, err := zipGenerator.GenerateZipFile(
|
||||
ctx,
|
||||
download.ProductID,
|
||||
[]string{download.ProductCode}, // 使用简化后的只包含主产品编号的列表
|
||||
nil, // 使用默认的JSON生成器
|
||||
"", // 使用默认路径
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
s.logger.Error("生成组件报告文件失败",
|
||||
zap.String("download_id", download.ID),
|
||||
zap.String("purchase_order_id", purchaseOrderID),
|
||||
zap.Error(err),
|
||||
)
|
||||
// 不中断流程,即使生成文件失败也继续处理
|
||||
} else {
|
||||
// 更新下载记录的文件路径
|
||||
download.FilePath = &zipPath
|
||||
err = s.componentReportRepo.UpdateDownload(ctx, download)
|
||||
if err != nil {
|
||||
s.logger.Error("更新下载记录文件路径失败",
|
||||
zap.String("download_id", download.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
} else {
|
||||
s.logger.Info("组件报告文件生成成功",
|
||||
zap.String("download_id", download.ID),
|
||||
zap.String("file_path", zipPath),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("购买订单支付成功处理完成",
|
||||
zap.String("purchase_order_id", purchaseOrderID),
|
||||
zap.String("trade_no", tradeNo),
|
||||
zap.String("amount", amount.String()),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleWechatRefundCallback 处理微信退款回调
|
||||
@@ -1842,3 +1859,163 @@ func (s *FinanceApplicationServiceImpl) HandleWechatRefundCallback(ctx context.C
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUserPurchaseRecords 获取用户购买记录
|
||||
func (s *FinanceApplicationServiceImpl) GetUserPurchaseRecords(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.PurchaseRecordListResponse, error) {
|
||||
// 确保 filters 不为 nil
|
||||
if filters == nil {
|
||||
filters = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// 添加 user_id 筛选条件,确保只能查询当前用户的记录
|
||||
filters["user_id"] = userID
|
||||
|
||||
// 获取总数
|
||||
total, err := s.purchaseOrderRepo.CountByFilters(ctx, filters)
|
||||
if err != nil {
|
||||
s.logger.Error("统计用户购买记录失败", zap.Error(err), zap.String("userID", userID))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 查询用户购买记录(使用筛选和分页功能)
|
||||
orders, err := s.purchaseOrderRepo.GetByFilters(ctx, filters, options)
|
||||
if err != nil {
|
||||
s.logger.Error("查询用户购买记录失败", zap.Error(err), zap.String("userID", userID))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为响应DTO
|
||||
var items []responses.PurchaseRecordResponse
|
||||
for _, order := range orders {
|
||||
item := responses.PurchaseRecordResponse{
|
||||
ID: order.ID,
|
||||
UserID: order.UserID,
|
||||
OrderNo: order.OrderNo,
|
||||
TradeNo: order.TradeNo,
|
||||
ProductID: order.ProductID,
|
||||
ProductCode: order.ProductCode,
|
||||
ProductName: order.ProductName,
|
||||
Category: order.Category,
|
||||
Subject: order.Subject,
|
||||
Amount: order.Amount,
|
||||
PayAmount: order.PayAmount,
|
||||
Status: string(order.Status),
|
||||
Platform: order.Platform,
|
||||
PayChannel: order.PayChannel,
|
||||
PaymentType: order.PaymentType,
|
||||
BuyerID: order.BuyerID,
|
||||
SellerID: order.SellerID,
|
||||
ReceiptAmount: order.ReceiptAmount,
|
||||
NotifyTime: order.NotifyTime,
|
||||
ReturnTime: order.ReturnTime,
|
||||
PayTime: order.PayTime,
|
||||
FilePath: order.FilePath,
|
||||
FileSize: order.FileSize,
|
||||
Remark: order.Remark,
|
||||
ErrorCode: order.ErrorCode,
|
||||
ErrorMessage: order.ErrorMessage,
|
||||
CreatedAt: order.CreatedAt,
|
||||
UpdatedAt: order.UpdatedAt,
|
||||
}
|
||||
|
||||
// 获取用户信息和企业名称
|
||||
user, err := s.userRepo.GetByIDWithEnterpriseInfo(ctx, order.UserID)
|
||||
if err == nil {
|
||||
companyName := "未知企业"
|
||||
if user.EnterpriseInfo != nil {
|
||||
companyName = user.EnterpriseInfo.CompanyName
|
||||
}
|
||||
item.CompanyName = companyName
|
||||
item.User = &responses.UserSimpleResponse{
|
||||
ID: user.ID,
|
||||
CompanyName: companyName,
|
||||
Phone: user.Phone,
|
||||
}
|
||||
}
|
||||
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
return &responses.PurchaseRecordListResponse{
|
||||
Items: items,
|
||||
Total: total,
|
||||
Page: options.Page,
|
||||
Size: options.PageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetAdminPurchaseRecords 获取管理端购买记录
|
||||
func (s *FinanceApplicationServiceImpl) GetAdminPurchaseRecords(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.PurchaseRecordListResponse, error) {
|
||||
// 获取总数
|
||||
total, err := s.purchaseOrderRepo.CountByFilters(ctx, filters)
|
||||
if err != nil {
|
||||
s.logger.Error("统计管理端购买记录失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 查询购买记录
|
||||
orders, err := s.purchaseOrderRepo.GetByFilters(ctx, filters, options)
|
||||
if err != nil {
|
||||
s.logger.Error("查询管理端购买记录失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为响应DTO
|
||||
var items []responses.PurchaseRecordResponse
|
||||
for _, order := range orders {
|
||||
item := responses.PurchaseRecordResponse{
|
||||
ID: order.ID,
|
||||
UserID: order.UserID,
|
||||
OrderNo: order.OrderNo,
|
||||
TradeNo: order.TradeNo,
|
||||
ProductID: order.ProductID,
|
||||
ProductCode: order.ProductCode,
|
||||
ProductName: order.ProductName,
|
||||
Category: order.Category,
|
||||
Subject: order.Subject,
|
||||
Amount: order.Amount,
|
||||
PayAmount: order.PayAmount,
|
||||
Status: string(order.Status),
|
||||
Platform: order.Platform,
|
||||
PayChannel: order.PayChannel,
|
||||
PaymentType: order.PaymentType,
|
||||
BuyerID: order.BuyerID,
|
||||
SellerID: order.SellerID,
|
||||
ReceiptAmount: order.ReceiptAmount,
|
||||
NotifyTime: order.NotifyTime,
|
||||
ReturnTime: order.ReturnTime,
|
||||
PayTime: order.PayTime,
|
||||
FilePath: order.FilePath,
|
||||
FileSize: order.FileSize,
|
||||
Remark: order.Remark,
|
||||
ErrorCode: order.ErrorCode,
|
||||
ErrorMessage: order.ErrorMessage,
|
||||
CreatedAt: order.CreatedAt,
|
||||
UpdatedAt: order.UpdatedAt,
|
||||
}
|
||||
|
||||
// 获取用户信息和企业名称
|
||||
user, err := s.userRepo.GetByIDWithEnterpriseInfo(ctx, order.UserID)
|
||||
if err == nil {
|
||||
companyName := "未知企业"
|
||||
if user.EnterpriseInfo != nil {
|
||||
companyName = user.EnterpriseInfo.CompanyName
|
||||
}
|
||||
item.CompanyName = companyName
|
||||
item.User = &responses.UserSimpleResponse{
|
||||
ID: user.ID,
|
||||
CompanyName: companyName,
|
||||
Phone: user.Phone,
|
||||
}
|
||||
}
|
||||
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
return &responses.PurchaseRecordListResponse{
|
||||
Items: items,
|
||||
Total: total,
|
||||
Page: options.Page,
|
||||
Size: options.PageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
752
internal/application/product/component_report_order_service.go
Normal file
@@ -0,0 +1,752 @@
|
||||
package product
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/domains/finance/entities"
|
||||
financeRepositories "tyapi-server/internal/domains/finance/repositories"
|
||||
productEntities "tyapi-server/internal/domains/product/entities"
|
||||
productRepositories "tyapi-server/internal/domains/product/repositories"
|
||||
"tyapi-server/internal/shared/component_report"
|
||||
"tyapi-server/internal/shared/payment"
|
||||
)
|
||||
|
||||
// ComponentReportOrderService 组件报告订单服务
|
||||
type ComponentReportOrderService struct {
|
||||
productRepo productRepositories.ProductRepository
|
||||
docRepo productRepositories.ProductDocumentationRepository
|
||||
apiConfigRepo productRepositories.ProductApiConfigRepository
|
||||
purchaseOrderRepo financeRepositories.PurchaseOrderRepository
|
||||
componentReportRepo productRepositories.ComponentReportRepository
|
||||
rechargeRecordRepo financeRepositories.RechargeRecordRepository
|
||||
alipayOrderRepo financeRepositories.AlipayOrderRepository
|
||||
wechatOrderRepo financeRepositories.WechatOrderRepository
|
||||
aliPayService *payment.AliPayService
|
||||
wechatPayService *payment.WechatPayService
|
||||
exampleJSONGenerator *component_report.ExampleJSONGenerator
|
||||
zipGenerator *component_report.ZipGenerator
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewComponentReportOrderService 创建组件报告订单服务
|
||||
func NewComponentReportOrderService(
|
||||
productRepo productRepositories.ProductRepository,
|
||||
docRepo productRepositories.ProductDocumentationRepository,
|
||||
apiConfigRepo productRepositories.ProductApiConfigRepository,
|
||||
purchaseOrderRepo financeRepositories.PurchaseOrderRepository,
|
||||
componentReportRepo productRepositories.ComponentReportRepository,
|
||||
rechargeRecordRepo financeRepositories.RechargeRecordRepository,
|
||||
alipayOrderRepo financeRepositories.AlipayOrderRepository,
|
||||
wechatOrderRepo financeRepositories.WechatOrderRepository,
|
||||
aliPayService *payment.AliPayService,
|
||||
wechatPayService *payment.WechatPayService,
|
||||
logger *zap.Logger,
|
||||
) *ComponentReportOrderService {
|
||||
exampleJSONGenerator := component_report.NewExampleJSONGenerator(productRepo, docRepo, apiConfigRepo, logger)
|
||||
zipGenerator := component_report.NewZipGenerator(logger)
|
||||
|
||||
return &ComponentReportOrderService{
|
||||
productRepo: productRepo,
|
||||
docRepo: docRepo,
|
||||
apiConfigRepo: apiConfigRepo,
|
||||
purchaseOrderRepo: purchaseOrderRepo,
|
||||
componentReportRepo: componentReportRepo,
|
||||
rechargeRecordRepo: rechargeRecordRepo,
|
||||
alipayOrderRepo: alipayOrderRepo,
|
||||
wechatOrderRepo: wechatOrderRepo,
|
||||
aliPayService: aliPayService,
|
||||
wechatPayService: wechatPayService,
|
||||
exampleJSONGenerator: exampleJSONGenerator,
|
||||
zipGenerator: zipGenerator,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateOrderInfo 获取订单信息
|
||||
func (s *ComponentReportOrderService) GetOrderInfo(ctx context.Context, userID, productID string) (*OrderInfo, error) {
|
||||
s.logger.Info("开始获取订单信息", zap.String("user_id", userID), zap.String("product_id", productID))
|
||||
|
||||
// 获取产品信息
|
||||
product, err := s.productRepo.GetByID(ctx, productID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取产品信息失败", zap.Error(err), zap.String("product_id", productID))
|
||||
return nil, fmt.Errorf("获取产品信息失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("获取产品信息成功",
|
||||
zap.String("product_id", product.ID),
|
||||
zap.String("product_code", product.Code),
|
||||
zap.String("product_name", product.Name),
|
||||
zap.Bool("is_package", product.IsPackage),
|
||||
zap.String("price", product.Price.String()),
|
||||
)
|
||||
|
||||
// 检查是否为组合包
|
||||
if !product.IsPackage {
|
||||
s.logger.Error("产品不是组合包", zap.String("product_id", productID), zap.String("product_code", product.Code))
|
||||
return nil, fmt.Errorf("只有组合包产品才能下载示例报告")
|
||||
}
|
||||
|
||||
// 获取组合包子产品
|
||||
packageItems, err := s.productRepo.GetPackageItems(ctx, productID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取组合包子产品失败", zap.Error(err), zap.String("product_id", productID))
|
||||
return nil, fmt.Errorf("获取组合包子产品失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("获取组合包子产品成功",
|
||||
zap.String("product_id", productID),
|
||||
zap.Int("package_items_count", len(packageItems)),
|
||||
)
|
||||
|
||||
// 获取用户已购买的产品编号列表
|
||||
purchasedCodes, err := s.purchaseOrderRepo.GetUserPurchasedProductCodes(ctx, userID)
|
||||
if err != nil {
|
||||
s.logger.Warn("获取用户已购买产品编号失败", zap.Error(err), zap.String("user_id", userID))
|
||||
purchasedCodes = []string{}
|
||||
}
|
||||
|
||||
s.logger.Info("获取用户已购买产品编号列表",
|
||||
zap.String("user_id", userID),
|
||||
zap.Strings("purchased_codes", purchasedCodes),
|
||||
zap.Int("purchased_count", len(purchasedCodes)),
|
||||
)
|
||||
|
||||
// 创建已购买编号的map用于快速查找
|
||||
purchasedMap := make(map[string]bool)
|
||||
for _, code := range purchasedCodes {
|
||||
purchasedMap[code] = true
|
||||
}
|
||||
|
||||
// 使用产品的UIComponentPrice作为最终价格
|
||||
finalPrice := product.UIComponentPrice
|
||||
s.logger.Info("使用UI组件价格",
|
||||
zap.String("product_id", productID),
|
||||
zap.String("product_ui_component_price", finalPrice.String()),
|
||||
)
|
||||
|
||||
// 准备子产品信息列表(仅用于展示,不参与价格计算)
|
||||
var subProducts []SubProductPriceInfo
|
||||
for _, item := range packageItems {
|
||||
var subProduct productEntities.Product
|
||||
var productCode string
|
||||
var productName string
|
||||
var price decimal.Decimal
|
||||
|
||||
if item.Product != nil {
|
||||
subProduct = *item.Product
|
||||
productCode = subProduct.Code
|
||||
productName = subProduct.Name
|
||||
price = subProduct.Price
|
||||
} else {
|
||||
subProduct, err = s.productRepo.GetByID(ctx, item.ProductID)
|
||||
if err != nil {
|
||||
s.logger.Warn("获取子产品信息失败", zap.Error(err), zap.String("product_id", item.ProductID))
|
||||
continue
|
||||
}
|
||||
productCode = subProduct.Code
|
||||
productName = subProduct.Name
|
||||
price = subProduct.Price
|
||||
}
|
||||
|
||||
if productCode == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查是否已购买
|
||||
isPurchased := purchasedMap[productCode]
|
||||
|
||||
subProducts = append(subProducts, SubProductPriceInfo{
|
||||
ProductID: subProduct.ID,
|
||||
ProductCode: productCode,
|
||||
ProductName: productName,
|
||||
Price: price.String(),
|
||||
IsPurchased: isPurchased,
|
||||
})
|
||||
}
|
||||
|
||||
// 检查用户是否有已支付的下载记录(针对当前产品)
|
||||
hasPaidDownload := false
|
||||
orders, _, err := s.purchaseOrderRepo.GetByUserID(ctx, userID, 100, 0)
|
||||
if err == nil {
|
||||
s.logger.Info("检查用户已支付的下载记录",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("product_id", productID),
|
||||
zap.Int("orders_count", len(orders)),
|
||||
)
|
||||
|
||||
for _, order := range orders {
|
||||
if order.ProductID == productID && order.Status == entities.PurchaseOrderStatusPaid {
|
||||
hasPaidDownload = true
|
||||
s.logger.Info("找到有效的已支付下载记录",
|
||||
zap.String("order_id", order.ID),
|
||||
zap.String("order_no", order.OrderNo),
|
||||
zap.String("product_id", order.ProductID),
|
||||
zap.String("purchase_status", string(order.Status)),
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s.logger.Warn("获取用户订单失败", zap.Error(err), zap.String("user_id", userID))
|
||||
}
|
||||
|
||||
// 如果可以下载:价格为0(免费)或者用户已支付
|
||||
canDownload := finalPrice.IsZero() || hasPaidDownload
|
||||
|
||||
s.logger.Info("最终订单信息",
|
||||
zap.String("product_id", productID),
|
||||
zap.String("product_code", product.Code),
|
||||
zap.String("product_name", product.Name),
|
||||
zap.Int("sub_products_count", len(subProducts)),
|
||||
zap.String("price", finalPrice.String()),
|
||||
zap.Strings("purchased_product_codes", purchasedCodes),
|
||||
zap.Bool("has_paid_download", hasPaidDownload),
|
||||
zap.Bool("can_download", canDownload),
|
||||
)
|
||||
|
||||
// 记录每个子产品的信息
|
||||
for i, subProduct := range subProducts {
|
||||
s.logger.Info("子产品详情",
|
||||
zap.Int("index", i),
|
||||
zap.String("sub_product_id", subProduct.ProductID),
|
||||
zap.String("sub_product_code", subProduct.ProductCode),
|
||||
zap.String("sub_product_name", subProduct.ProductName),
|
||||
zap.String("price", subProduct.Price),
|
||||
zap.Bool("is_purchased", subProduct.IsPurchased),
|
||||
)
|
||||
}
|
||||
|
||||
return &OrderInfo{
|
||||
ProductID: productID,
|
||||
ProductCode: product.Code,
|
||||
ProductName: product.Name,
|
||||
IsPackage: true,
|
||||
SubProducts: subProducts,
|
||||
Price: finalPrice.String(),
|
||||
PurchasedProductCodes: purchasedCodes,
|
||||
CanDownload: canDownload,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreatePaymentOrder 创建支付订单
|
||||
func (s *ComponentReportOrderService) CreatePaymentOrder(ctx context.Context, req *CreatePaymentOrderRequest) (*CreatePaymentOrderResponse, error) {
|
||||
// 获取产品信息
|
||||
product, err := s.productRepo.GetByID(ctx, req.ProductID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取产品信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查是否为组合包
|
||||
if !product.IsPackage {
|
||||
return nil, fmt.Errorf("只有组合包产品才能下载示例报告")
|
||||
}
|
||||
|
||||
// 获取组合包子产品
|
||||
packageItems, err := s.productRepo.GetPackageItems(ctx, req.ProductID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取组合包子产品失败: %w", err)
|
||||
}
|
||||
|
||||
// 使用产品的UIComponentPrice作为价格
|
||||
finalPrice := product.UIComponentPrice
|
||||
s.logger.Info("使用UI组件价格创建支付订单",
|
||||
zap.String("product_id", req.ProductID),
|
||||
zap.String("product_ui_component_price", finalPrice.String()),
|
||||
)
|
||||
|
||||
// 检查价格是否为0
|
||||
if finalPrice.IsZero() {
|
||||
return s.createFreeOrder(ctx, req, &product, nil, nil, finalPrice)
|
||||
}
|
||||
|
||||
// 准备子产品信息列表(仅用于展示)
|
||||
var subProductCodes []string
|
||||
var subProductIDs []string
|
||||
|
||||
for _, item := range packageItems {
|
||||
var subProduct productEntities.Product
|
||||
var productCode string
|
||||
|
||||
if item.Product != nil {
|
||||
subProduct = *item.Product
|
||||
productCode = subProduct.Code
|
||||
} else {
|
||||
subProduct, err = s.productRepo.GetByID(ctx, item.ProductID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
productCode = subProduct.Code
|
||||
}
|
||||
|
||||
if productCode == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 收集所有子产品信息
|
||||
subProductCodes = append(subProductCodes, productCode)
|
||||
subProductIDs = append(subProductIDs, subProduct.ID)
|
||||
}
|
||||
|
||||
// 生成商户订单号
|
||||
var outTradeNo string
|
||||
if req.PaymentType == "alipay" {
|
||||
outTradeNo = s.aliPayService.GenerateOutTradeNo()
|
||||
} else {
|
||||
outTradeNo = s.wechatPayService.GenerateOutTradeNo()
|
||||
}
|
||||
|
||||
// 创建购买订单 - 设置为待支付状态
|
||||
purchaseOrder := entities.NewPurchaseOrder(
|
||||
req.UserID,
|
||||
req.ProductID,
|
||||
product.Code,
|
||||
product.Name,
|
||||
fmt.Sprintf("组件报告下载-%s", product.Name),
|
||||
finalPrice,
|
||||
req.Platform, // 使用传入的平台参数
|
||||
req.PaymentType,
|
||||
req.PaymentType,
|
||||
)
|
||||
|
||||
// 设置为待支付状态
|
||||
purchaseOrder.Status = entities.PurchaseOrderStatusCreated
|
||||
|
||||
createdPurchaseOrder, err := s.purchaseOrderRepo.Create(ctx, purchaseOrder)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建购买订单失败: %w", err)
|
||||
}
|
||||
|
||||
// 创建相应的支付记录(未支付状态)
|
||||
if req.PaymentType == "alipay" {
|
||||
// 使用工厂方法创建支付宝订单
|
||||
alipayOrder := entities.NewAlipayOrder(createdPurchaseOrder.ID, outTradeNo,
|
||||
fmt.Sprintf("组件报告下载-%s", product.Name), finalPrice, req.Platform)
|
||||
// 设置为待支付状态
|
||||
alipayOrder.Status = entities.AlipayOrderStatusPending
|
||||
|
||||
_, err = s.alipayOrderRepo.Create(ctx, *alipayOrder)
|
||||
if err != nil {
|
||||
s.logger.Error("创建支付宝订单记录失败", zap.Error(err))
|
||||
}
|
||||
} else {
|
||||
// 使用工厂方法创建微信订单
|
||||
wechatOrder := entities.NewWechatOrder(createdPurchaseOrder.ID, outTradeNo,
|
||||
fmt.Sprintf("组件报告下载-%s", product.Name), finalPrice, req.Platform)
|
||||
// 设置为待支付状态
|
||||
wechatOrder.Status = entities.WechatOrderStatusPending
|
||||
|
||||
_, err = s.wechatOrderRepo.Create(ctx, *wechatOrder)
|
||||
if err != nil {
|
||||
s.logger.Error("创建微信订单记录失败", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// 调用真实支付接口创建支付订单
|
||||
var payURL string
|
||||
var codeURL string
|
||||
|
||||
if req.PaymentType == "alipay" {
|
||||
// 调用支付宝支付接口
|
||||
payURL, err = s.aliPayService.CreateAlipayOrder(ctx, req.Platform, finalPrice,
|
||||
fmt.Sprintf("组件报告下载-%s", product.Name), outTradeNo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建支付宝支付订单失败: %w", err)
|
||||
}
|
||||
} else if req.PaymentType == "wechat" {
|
||||
// 调用微信支付接口
|
||||
floatValue, _ := finalPrice.Float64() // 忽略第二个返回值
|
||||
result, err := s.wechatPayService.CreateWechatOrder(ctx, floatValue,
|
||||
fmt.Sprintf("组件报告下载-%s", product.Name), outTradeNo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建微信支付订单失败: %w", err)
|
||||
}
|
||||
|
||||
// 微信支付返回的是map格式,提取code_url
|
||||
if resultMap, ok := result.(map[string]string); ok {
|
||||
codeURL = resultMap["code_url"]
|
||||
}
|
||||
}
|
||||
|
||||
// 创建一个临时下载记录,用于跟踪支付状态,但不生成报告文件
|
||||
download := &productEntities.ComponentReportDownload{
|
||||
UserID: req.UserID,
|
||||
ProductID: req.ProductID,
|
||||
ProductCode: product.Code,
|
||||
ProductName: product.Name,
|
||||
OrderID: &createdPurchaseOrder.ID, // 关联购买订单ID
|
||||
OrderNumber: &outTradeNo, // 外部订单号
|
||||
ExpiresAt: calculateExpiryTime(), // 30天后过期
|
||||
// 注意:这里不设置FilePath,因为文件将在支付成功后生成
|
||||
}
|
||||
|
||||
err = s.componentReportRepo.Create(ctx, download)
|
||||
if err != nil {
|
||||
s.logger.Error("创建下载记录失败", zap.Error(err))
|
||||
// 不中断流程,即使创建下载记录失败也继续返回订单信息
|
||||
}
|
||||
|
||||
// 返回支付响应,包含支付URL
|
||||
response := &CreatePaymentOrderResponse{
|
||||
OrderID: download.ID,
|
||||
OrderNo: createdPurchaseOrder.OrderNo,
|
||||
PaymentType: req.PaymentType,
|
||||
Amount: finalPrice.String(),
|
||||
PayURL: payURL,
|
||||
CodeURL: codeURL,
|
||||
}
|
||||
|
||||
s.logger.Info("支付订单创建成功",
|
||||
zap.String("order_id", download.ID),
|
||||
zap.String("purchase_order_id", createdPurchaseOrder.ID),
|
||||
zap.String("user_id", req.UserID),
|
||||
zap.String("product_id", req.ProductID),
|
||||
zap.String("payment_type", req.PaymentType),
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
)
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// createFreeOrder 创建免费订单
|
||||
func (s *ComponentReportOrderService) createFreeOrder(
|
||||
ctx context.Context,
|
||||
req *CreatePaymentOrderRequest,
|
||||
product *productEntities.Product,
|
||||
subProductCodes []string,
|
||||
subProductIDs []string,
|
||||
finalPrice decimal.Decimal,
|
||||
) (*CreatePaymentOrderResponse, error) {
|
||||
// 序列化子产品列表
|
||||
// 简化后的实体不再需要序列化子产品列表
|
||||
|
||||
// 创建免费订单
|
||||
purchaseOrder := entities.NewPurchaseOrder(
|
||||
req.UserID,
|
||||
req.ProductID,
|
||||
product.Code,
|
||||
product.Name,
|
||||
fmt.Sprintf("组件报告下载-%s", product.Name),
|
||||
finalPrice,
|
||||
"app",
|
||||
"free",
|
||||
"free",
|
||||
)
|
||||
|
||||
// 设置为已支付状态
|
||||
purchaseOrder.Status = entities.PurchaseOrderStatusPaid
|
||||
now := time.Now()
|
||||
purchaseOrder.PayTime = &now
|
||||
|
||||
createdPurchaseOrder, err := s.purchaseOrderRepo.Create(ctx, purchaseOrder)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建免费订单失败: %w", err)
|
||||
}
|
||||
|
||||
// 创建下载记录
|
||||
download := &productEntities.ComponentReportDownload{
|
||||
UserID: req.UserID,
|
||||
ProductID: req.ProductID,
|
||||
ProductCode: product.Code,
|
||||
ProductName: product.Name,
|
||||
OrderID: &createdPurchaseOrder.ID, // 关联购买订单ID
|
||||
OrderNumber: &createdPurchaseOrder.OrderNo, // 外部订单号
|
||||
ExpiresAt: calculateExpiryTime(), // 30天后过期
|
||||
}
|
||||
|
||||
err = s.componentReportRepo.Create(ctx, download)
|
||||
if err != nil {
|
||||
s.logger.Error("创建下载记录失败", zap.Error(err))
|
||||
// 不中断流程,即使创建下载记录失败也继续返回订单信息
|
||||
}
|
||||
|
||||
return &CreatePaymentOrderResponse{
|
||||
OrderID: download.ID,
|
||||
OrderNo: createdPurchaseOrder.OrderNo,
|
||||
PaymentType: "free",
|
||||
Amount: "0.00",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// generateReportFile 生成报告文件
|
||||
func (s *ComponentReportOrderService) generateReportFile(ctx context.Context, download *productEntities.ComponentReportDownload) (string, error) {
|
||||
// 解析子产品编号列表
|
||||
// 简化后的实体只使用主产品编号
|
||||
subProductCodes := []string{download.ProductCode}
|
||||
|
||||
// 生成筛选后的组件ZIP文件
|
||||
zipPath, err := s.zipGenerator.GenerateZipFile(
|
||||
ctx,
|
||||
download.ProductID,
|
||||
subProductCodes,
|
||||
s.exampleJSONGenerator,
|
||||
"", // 使用默认路径
|
||||
)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("生成筛选后的组件ZIP文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 更新下载记录的文件路径
|
||||
download.FilePath = &zipPath
|
||||
err = s.componentReportRepo.UpdateDownload(ctx, download)
|
||||
if err != nil {
|
||||
s.logger.Error("更新下载记录文件信息失败", zap.Error(err), zap.String("download_id", download.ID))
|
||||
// 即使更新失败,也返回文件路径,因为文件已经生成
|
||||
}
|
||||
|
||||
s.logger.Info("报告文件生成成功",
|
||||
zap.String("download_id", download.ID),
|
||||
zap.String("file_path", zipPath),
|
||||
zap.String("product_id", download.ProductID),
|
||||
zap.String("product_code", download.ProductCode))
|
||||
|
||||
return zipPath, nil
|
||||
}
|
||||
|
||||
// CheckPaymentStatus 检查支付状态
|
||||
func (s *ComponentReportOrderService) CheckPaymentStatus(ctx context.Context, orderID string) (*CheckPaymentStatusResponse, error) {
|
||||
// 获取下载记录信息
|
||||
download, err := s.componentReportRepo.GetDownloadByID(ctx, orderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取下载记录信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 使用OrderID查询购买订单状态来判断支付状态
|
||||
var paymentStatus string
|
||||
var canDownload bool
|
||||
|
||||
if download.OrderID != nil {
|
||||
// 查询购买订单状态
|
||||
purchaseOrder, err := s.purchaseOrderRepo.GetByID(ctx, *download.OrderID)
|
||||
if err != nil {
|
||||
s.logger.Error("查询购买订单失败", zap.Error(err), zap.String("order_id", *download.OrderID))
|
||||
paymentStatus = "unknown"
|
||||
} else {
|
||||
// 根据购买订单状态设置支付状态
|
||||
switch purchaseOrder.Status {
|
||||
case entities.PurchaseOrderStatusPaid:
|
||||
paymentStatus = "success"
|
||||
canDownload = true
|
||||
case entities.PurchaseOrderStatusCreated:
|
||||
paymentStatus = "pending"
|
||||
canDownload = false
|
||||
case entities.PurchaseOrderStatusCancelled:
|
||||
paymentStatus = "cancelled"
|
||||
canDownload = false
|
||||
case entities.PurchaseOrderStatusFailed:
|
||||
paymentStatus = "failed"
|
||||
canDownload = false
|
||||
default:
|
||||
paymentStatus = "unknown"
|
||||
canDownload = false
|
||||
}
|
||||
}
|
||||
} else if download.OrderNumber != nil {
|
||||
// 兼容旧的支付订单逻辑
|
||||
paymentStatus = "success" // 简化处理,有支付订单号就认为已支付
|
||||
canDownload = true
|
||||
} else {
|
||||
paymentStatus = "pending"
|
||||
canDownload = false
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
if download.IsExpired() {
|
||||
canDownload = false
|
||||
}
|
||||
|
||||
// 返回支付状态
|
||||
return &CheckPaymentStatusResponse{
|
||||
OrderID: download.ID,
|
||||
PaymentStatus: paymentStatus,
|
||||
CanDownload: canDownload,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DownloadFile 下载文件
|
||||
func (s *ComponentReportOrderService) DownloadFile(ctx context.Context, orderID string) (string, error) {
|
||||
// 获取下载记录信息
|
||||
download, err := s.componentReportRepo.GetDownloadByID(ctx, orderID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取下载记录信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 使用OrderID查询购买订单状态来判断支付状态
|
||||
var canDownload bool
|
||||
|
||||
if download.OrderID != nil {
|
||||
// 查询购买订单状态
|
||||
purchaseOrder, err := s.purchaseOrderRepo.GetByID(ctx, *download.OrderID)
|
||||
if err != nil {
|
||||
s.logger.Error("查询购买订单失败", zap.Error(err), zap.String("order_id", *download.OrderID))
|
||||
canDownload = false
|
||||
} else {
|
||||
// 检查购买订单状态
|
||||
canDownload = purchaseOrder.Status == entities.PurchaseOrderStatusPaid
|
||||
}
|
||||
} else if download.OrderNumber != nil {
|
||||
// 兼容旧的支付订单逻辑
|
||||
canDownload = true // 简化处理,有支付订单号就认为已支付
|
||||
} else {
|
||||
canDownload = false
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
if download.IsExpired() {
|
||||
canDownload = false
|
||||
}
|
||||
|
||||
if !canDownload {
|
||||
return "", fmt.Errorf("订单未支付或已过期,无法下载文件")
|
||||
}
|
||||
|
||||
// 检查文件是否已存在
|
||||
if download.FilePath != nil && *download.FilePath != "" {
|
||||
// 文件已存在,直接返回文件路径
|
||||
s.logger.Info("返回已存在的文件路径",
|
||||
zap.String("order_id", orderID),
|
||||
zap.String("file_path", *download.FilePath))
|
||||
return *download.FilePath, nil
|
||||
}
|
||||
|
||||
// 文件不存在,生成文件
|
||||
filePath, err := s.generateReportFile(ctx, download)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("生成报告文件失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("成功生成报告文件",
|
||||
zap.String("order_id", orderID),
|
||||
zap.String("file_path", filePath))
|
||||
|
||||
return filePath, nil
|
||||
}
|
||||
|
||||
// GetUserOrders 获取用户订单列表
|
||||
func (s *ComponentReportOrderService) GetUserOrders(ctx context.Context, userID string, limit, offset int) ([]*UserOrderResponse, int64, error) {
|
||||
// 获取用户的下载记录
|
||||
downloads, err := s.componentReportRepo.GetUserDownloads(ctx, userID, nil)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 转换为响应格式
|
||||
result := make([]*UserOrderResponse, 0, len(downloads))
|
||||
for _, download := range downloads {
|
||||
// 使用OrderID查询购买订单状态来判断支付状态
|
||||
var purchaseStatus string = "pending"
|
||||
var paymentType string = "unknown"
|
||||
var paymentTime *time.Time = nil
|
||||
|
||||
if download.OrderID != nil {
|
||||
// 查询购买订单状态
|
||||
purchaseOrder, err := s.purchaseOrderRepo.GetByID(ctx, *download.OrderID)
|
||||
if err == nil {
|
||||
switch purchaseOrder.Status {
|
||||
case entities.PurchaseOrderStatusPaid:
|
||||
purchaseStatus = "paid"
|
||||
paymentTime = purchaseOrder.PayTime
|
||||
case entities.PurchaseOrderStatusCreated:
|
||||
purchaseStatus = "created"
|
||||
case entities.PurchaseOrderStatusCancelled:
|
||||
purchaseStatus = "cancelled"
|
||||
case entities.PurchaseOrderStatusFailed:
|
||||
purchaseStatus = "failed"
|
||||
}
|
||||
paymentType = purchaseOrder.PayChannel
|
||||
}
|
||||
} else if download.OrderNumber != nil {
|
||||
// 兼容旧的支付订单逻辑
|
||||
purchaseStatus = "paid" // 简化处理,有支付订单号就认为已支付
|
||||
paymentType = "unknown"
|
||||
}
|
||||
|
||||
result = append(result, &UserOrderResponse{
|
||||
ID: download.ID,
|
||||
OrderNo: "",
|
||||
ProductID: download.ProductID,
|
||||
ProductCode: download.ProductCode,
|
||||
PaymentType: paymentType,
|
||||
PurchaseStatus: purchaseStatus,
|
||||
Price: "0.00", // 下载记录不存储价格信息
|
||||
CreatedAt: download.CreatedAt,
|
||||
PaymentTime: paymentTime,
|
||||
})
|
||||
}
|
||||
|
||||
return result, int64(len(result)), nil
|
||||
}
|
||||
|
||||
// calculateExpiryTime 计算下载有效期(从创建日起30天)
|
||||
func calculateExpiryTime() *time.Time {
|
||||
now := time.Now()
|
||||
expiry := now.AddDate(0, 0, 30) // 30天后过期
|
||||
return &expiry
|
||||
}
|
||||
|
||||
// 数据结构定义
|
||||
|
||||
// OrderInfo 订单信息
|
||||
type OrderInfo struct {
|
||||
ProductID string `json:"product_id"`
|
||||
ProductCode string `json:"product_code"`
|
||||
ProductName string `json:"product_name"`
|
||||
IsPackage bool `json:"is_package"`
|
||||
SubProducts []SubProductPriceInfo `json:"sub_products"`
|
||||
Price string `json:"price"` // UI组件价格
|
||||
PurchasedProductCodes []string `json:"purchased_product_codes"`
|
||||
CanDownload bool `json:"can_download"`
|
||||
}
|
||||
|
||||
// SubProductPriceInfo 子产品价格信息
|
||||
type SubProductPriceInfo struct {
|
||||
ProductID string `json:"product_id"`
|
||||
ProductCode string `json:"product_code"`
|
||||
ProductName string `json:"product_name"`
|
||||
Price string `json:"price"`
|
||||
IsPurchased bool `json:"is_purchased"`
|
||||
}
|
||||
|
||||
// CreatePaymentOrderRequest 创建支付订单请求
|
||||
type CreatePaymentOrderRequest struct {
|
||||
UserID string `json:"user_id"`
|
||||
ProductID string `json:"product_id"`
|
||||
PaymentType string `json:"payment_type"` // wechat 或 alipay
|
||||
Platform string `json:"platform"` // 支付平台:app, h5, pc(可选,默认根据User-Agent判断)
|
||||
SubProductCodes []string `json:"sub_product_codes,omitempty"`
|
||||
}
|
||||
|
||||
// CreatePaymentOrderResponse 创建支付订单响应
|
||||
type CreatePaymentOrderResponse struct {
|
||||
OrderID string `json:"order_id"`
|
||||
OrderNo string `json:"order_no"`
|
||||
CodeURL string `json:"code_url"` // 支付二维码URL(微信)
|
||||
PayURL string `json:"pay_url"` // 支付链接(支付宝)
|
||||
PaymentType string `json:"payment_type"`
|
||||
Amount string `json:"amount"`
|
||||
}
|
||||
|
||||
// CheckPaymentStatusResponse 检查支付状态响应
|
||||
type CheckPaymentStatusResponse struct {
|
||||
OrderID string `json:"order_id"` // 订单ID
|
||||
PaymentStatus string `json:"payment_status"` // 支付状态:pending, success, failed
|
||||
CanDownload bool `json:"can_download"` // 是否可以下载
|
||||
}
|
||||
|
||||
// UserOrderResponse 用户订单响应
|
||||
type UserOrderResponse struct {
|
||||
ID string `json:"id"`
|
||||
OrderNo string `json:"order_no"`
|
||||
ProductID string `json:"product_id"`
|
||||
ProductCode string `json:"product_code"`
|
||||
PaymentType string `json:"payment_type"`
|
||||
PurchaseStatus string `json:"purchase_status"`
|
||||
Price string `json:"price"` // UI组件价格
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
PaymentTime *time.Time `json:"payment_time"`
|
||||
}
|
||||
@@ -14,6 +14,10 @@ type CreateProductCommand struct {
|
||||
IsVisible bool `json:"is_visible" comment:"是否展示"`
|
||||
IsPackage bool `json:"is_package" comment:"是否组合包"`
|
||||
|
||||
// UI组件相关字段
|
||||
SellUIComponent bool `json:"sell_ui_component" comment:"是否出售UI组件"`
|
||||
UIComponentPrice float64 `json:"ui_component_price" binding:"omitempty,min=0" comment:"UI组件销售价格(组合包使用)"`
|
||||
|
||||
// SEO信息
|
||||
SEOTitle string `json:"seo_title" binding:"omitempty,max=100" comment:"SEO标题"`
|
||||
SEODescription string `json:"seo_description" binding:"omitempty,max=200" comment:"SEO描述"`
|
||||
@@ -35,6 +39,10 @@ type UpdateProductCommand struct {
|
||||
IsVisible bool `json:"is_visible" comment:"是否展示"`
|
||||
IsPackage bool `json:"is_package" comment:"是否组合包"`
|
||||
|
||||
// UI组件相关字段
|
||||
SellUIComponent bool `json:"sell_ui_component" comment:"是否出售UI组件"`
|
||||
UIComponentPrice float64 `json:"ui_component_price" binding:"omitempty,min=0" comment:"UI组件销售价格(组合包使用)"`
|
||||
|
||||
// SEO信息
|
||||
SEOTitle string `json:"seo_title" binding:"omitempty,max=100" comment:"SEO标题"`
|
||||
SEODescription string `json:"seo_description" binding:"omitempty,max=200" comment:"SEO描述"`
|
||||
|
||||
@@ -15,17 +15,20 @@ type PackageItemResponse struct {
|
||||
|
||||
// ProductInfoResponse 产品详情响应
|
||||
type ProductInfoResponse struct {
|
||||
ID string `json:"id" comment:"产品ID"`
|
||||
OldID *string `json:"old_id,omitempty" comment:"旧产品ID"`
|
||||
Name string `json:"name" comment:"产品名称"`
|
||||
Code string `json:"code" comment:"产品编号"`
|
||||
Description string `json:"description" comment:"产品简介"`
|
||||
Content string `json:"content" comment:"产品内容"`
|
||||
CategoryID string `json:"category_id" comment:"产品分类ID"`
|
||||
Price float64 `json:"price" comment:"产品价格"`
|
||||
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
|
||||
IsPackage bool `json:"is_package" comment:"是否组合包"`
|
||||
IsSubscribed *bool `json:"is_subscribed,omitempty" comment:"当前用户是否已订阅"`
|
||||
ID string `json:"id" comment:"产品ID"`
|
||||
OldID *string `json:"old_id,omitempty" comment:"旧产品ID"`
|
||||
Name string `json:"name" comment:"产品名称"`
|
||||
Code string `json:"code" comment:"产品编号"`
|
||||
Description string `json:"description" comment:"产品简介"`
|
||||
Content string `json:"content" comment:"产品内容"`
|
||||
CategoryID string `json:"category_id" comment:"产品分类ID"`
|
||||
Price float64 `json:"price" comment:"产品价格"`
|
||||
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
|
||||
IsPackage bool `json:"is_package" comment:"是否组合包"`
|
||||
IsSubscribed *bool `json:"is_subscribed,omitempty" comment:"当前用户是否已订阅"`
|
||||
// UI组件相关字段
|
||||
SellUIComponent bool `json:"sell_ui_component" comment:"是否出售UI组件"`
|
||||
UIComponentPrice float64 `json:"ui_component_price" comment:"UI组件销售价格(组合包使用)"`
|
||||
|
||||
// SEO信息
|
||||
SEOTitle string `json:"seo_title" comment:"SEO标题"`
|
||||
@@ -60,15 +63,15 @@ type ProductSearchResponse struct {
|
||||
|
||||
// ProductSimpleResponse 产品简单信息响应
|
||||
type ProductSimpleResponse struct {
|
||||
ID string `json:"id" comment:"产品ID"`
|
||||
OldID *string `json:"old_id,omitempty" comment:"旧产品ID"`
|
||||
Name string `json:"name" comment:"产品名称"`
|
||||
Code string `json:"code" comment:"产品编号"`
|
||||
Description string `json:"description" comment:"产品简介"`
|
||||
Category *CategorySimpleResponse `json:"category,omitempty" comment:"分类信息"`
|
||||
Price float64 `json:"price" comment:"产品价格"`
|
||||
IsPackage bool `json:"is_package" comment:"是否组合包"`
|
||||
IsSubscribed *bool `json:"is_subscribed,omitempty" comment:"当前用户是否已订阅"`
|
||||
ID string `json:"id" comment:"产品ID"`
|
||||
OldID *string `json:"old_id,omitempty" comment:"旧产品ID"`
|
||||
Name string `json:"name" comment:"产品名称"`
|
||||
Code string `json:"code" comment:"产品编号"`
|
||||
Description string `json:"description" comment:"产品简介"`
|
||||
Category *CategorySimpleResponse `json:"category,omitempty" comment:"分类信息"`
|
||||
Price float64 `json:"price" comment:"产品价格"`
|
||||
IsPackage bool `json:"is_package" comment:"是否组合包"`
|
||||
IsSubscribed *bool `json:"is_subscribed,omitempty" comment:"当前用户是否已订阅"`
|
||||
}
|
||||
|
||||
// ProductSimpleAdminResponse 管理员产品简单信息响应(包含成本价)
|
||||
@@ -101,6 +104,10 @@ type ProductAdminInfoResponse struct {
|
||||
IsVisible bool `json:"is_visible" comment:"是否可见"`
|
||||
IsPackage bool `json:"is_package" comment:"是否组合包"`
|
||||
|
||||
// UI组件相关字段
|
||||
SellUIComponent bool `json:"sell_ui_component" comment:"是否出售UI组件"`
|
||||
UIComponentPrice float64 `json:"ui_component_price" comment:"UI组件销售价格(组合包使用)"`
|
||||
|
||||
// SEO信息
|
||||
SEOTitle string `json:"seo_title" comment:"SEO标题"`
|
||||
SEODescription string `json:"seo_description" comment:"SEO描述"`
|
||||
|
||||
@@ -54,20 +54,22 @@ func NewProductApplicationService(
|
||||
func (s *ProductApplicationServiceImpl) CreateProduct(ctx context.Context, cmd *commands.CreateProductCommand) (*responses.ProductAdminInfoResponse, error) {
|
||||
// 1. 构建产品实体
|
||||
product := &entities.Product{
|
||||
Name: cmd.Name,
|
||||
Code: cmd.Code,
|
||||
Description: cmd.Description,
|
||||
Content: cmd.Content,
|
||||
CategoryID: cmd.CategoryID,
|
||||
Price: decimal.NewFromFloat(cmd.Price),
|
||||
CostPrice: decimal.NewFromFloat(cmd.CostPrice),
|
||||
Remark: cmd.Remark,
|
||||
IsEnabled: cmd.IsEnabled,
|
||||
IsVisible: cmd.IsVisible,
|
||||
IsPackage: cmd.IsPackage,
|
||||
SEOTitle: cmd.SEOTitle,
|
||||
SEODescription: cmd.SEODescription,
|
||||
SEOKeywords: cmd.SEOKeywords,
|
||||
Name: cmd.Name,
|
||||
Code: cmd.Code,
|
||||
Description: cmd.Description,
|
||||
Content: cmd.Content,
|
||||
CategoryID: cmd.CategoryID,
|
||||
Price: decimal.NewFromFloat(cmd.Price),
|
||||
CostPrice: decimal.NewFromFloat(cmd.CostPrice),
|
||||
Remark: cmd.Remark,
|
||||
IsEnabled: cmd.IsEnabled,
|
||||
IsVisible: cmd.IsVisible,
|
||||
IsPackage: cmd.IsPackage,
|
||||
SellUIComponent: cmd.SellUIComponent,
|
||||
UIComponentPrice: decimal.NewFromFloat(cmd.UIComponentPrice),
|
||||
SEOTitle: cmd.SEOTitle,
|
||||
SEODescription: cmd.SEODescription,
|
||||
SEOKeywords: cmd.SEOKeywords,
|
||||
}
|
||||
|
||||
// 2. 创建产品
|
||||
@@ -101,6 +103,8 @@ func (s *ProductApplicationServiceImpl) UpdateProduct(ctx context.Context, cmd *
|
||||
existingProduct.IsEnabled = cmd.IsEnabled
|
||||
existingProduct.IsVisible = cmd.IsVisible
|
||||
existingProduct.IsPackage = cmd.IsPackage
|
||||
existingProduct.SellUIComponent = cmd.SellUIComponent
|
||||
existingProduct.UIComponentPrice = decimal.NewFromFloat(cmd.UIComponentPrice)
|
||||
existingProduct.SEOTitle = cmd.SEOTitle
|
||||
existingProduct.SEODescription = cmd.SEODescription
|
||||
existingProduct.SEOKeywords = cmd.SEOKeywords
|
||||
@@ -486,21 +490,23 @@ func (s *ProductApplicationServiceImpl) GetProductByIDForUser(ctx context.Contex
|
||||
// convertToProductInfoResponse 转换为产品信息响应
|
||||
func (s *ProductApplicationServiceImpl) convertToProductInfoResponse(product *entities.Product) *responses.ProductInfoResponse {
|
||||
response := &responses.ProductInfoResponse{
|
||||
ID: product.ID,
|
||||
OldID: product.OldID,
|
||||
Name: product.Name,
|
||||
Code: product.Code,
|
||||
Description: product.Description,
|
||||
Content: product.Content,
|
||||
CategoryID: product.CategoryID,
|
||||
Price: product.Price.InexactFloat64(),
|
||||
IsEnabled: product.IsEnabled,
|
||||
IsPackage: product.IsPackage,
|
||||
SEOTitle: product.SEOTitle,
|
||||
SEODescription: product.SEODescription,
|
||||
SEOKeywords: product.SEOKeywords,
|
||||
CreatedAt: product.CreatedAt,
|
||||
UpdatedAt: product.UpdatedAt,
|
||||
ID: product.ID,
|
||||
OldID: product.OldID,
|
||||
Name: product.Name,
|
||||
Code: product.Code,
|
||||
Description: product.Description,
|
||||
Content: product.Content,
|
||||
CategoryID: product.CategoryID,
|
||||
Price: product.Price.InexactFloat64(),
|
||||
IsEnabled: product.IsEnabled,
|
||||
IsPackage: product.IsPackage,
|
||||
SellUIComponent: product.SellUIComponent,
|
||||
UIComponentPrice: product.UIComponentPrice.InexactFloat64(),
|
||||
SEOTitle: product.SEOTitle,
|
||||
SEODescription: product.SEODescription,
|
||||
SEOKeywords: product.SEOKeywords,
|
||||
CreatedAt: product.CreatedAt,
|
||||
UpdatedAt: product.UpdatedAt,
|
||||
}
|
||||
|
||||
// 添加分类信息
|
||||
@@ -530,24 +536,26 @@ func (s *ProductApplicationServiceImpl) convertToProductInfoResponse(product *en
|
||||
// convertToProductAdminInfoResponse 转换为管理员产品信息响应
|
||||
func (s *ProductApplicationServiceImpl) convertToProductAdminInfoResponse(product *entities.Product) *responses.ProductAdminInfoResponse {
|
||||
response := &responses.ProductAdminInfoResponse{
|
||||
ID: product.ID,
|
||||
OldID: product.OldID,
|
||||
Name: product.Name,
|
||||
Code: product.Code,
|
||||
Description: product.Description,
|
||||
Content: product.Content,
|
||||
CategoryID: product.CategoryID,
|
||||
Price: product.Price.InexactFloat64(),
|
||||
CostPrice: product.CostPrice.InexactFloat64(),
|
||||
Remark: product.Remark,
|
||||
IsEnabled: product.IsEnabled,
|
||||
IsVisible: product.IsVisible, // 管理员可以看到可见状态
|
||||
IsPackage: product.IsPackage,
|
||||
SEOTitle: product.SEOTitle,
|
||||
SEODescription: product.SEODescription,
|
||||
SEOKeywords: product.SEOKeywords,
|
||||
CreatedAt: product.CreatedAt,
|
||||
UpdatedAt: product.UpdatedAt,
|
||||
ID: product.ID,
|
||||
OldID: product.OldID,
|
||||
Name: product.Name,
|
||||
Code: product.Code,
|
||||
Description: product.Description,
|
||||
Content: product.Content,
|
||||
CategoryID: product.CategoryID,
|
||||
Price: product.Price.InexactFloat64(),
|
||||
CostPrice: product.CostPrice.InexactFloat64(),
|
||||
Remark: product.Remark,
|
||||
IsEnabled: product.IsEnabled,
|
||||
IsVisible: product.IsVisible, // 管理员可以看到可见状态
|
||||
IsPackage: product.IsPackage,
|
||||
SellUIComponent: product.SellUIComponent,
|
||||
UIComponentPrice: product.UIComponentPrice.InexactFloat64(),
|
||||
SEOTitle: product.SEOTitle,
|
||||
SEODescription: product.SEODescription,
|
||||
SEOKeywords: product.SEOKeywords,
|
||||
CreatedAt: product.CreatedAt,
|
||||
UpdatedAt: product.UpdatedAt,
|
||||
}
|
||||
|
||||
// 添加分类信息
|
||||
|
||||
@@ -182,7 +182,7 @@ func (s *UIComponentApplicationServiceImpl) CreateUIComponentWithFile(ctx contex
|
||||
fileType := strings.ToLower(filepath.Ext(file.Filename))
|
||||
|
||||
// 更新组件信息
|
||||
folderPath := "resources/Pure Component/src/ui"
|
||||
folderPath := "resources/Pure_Component/src/ui"
|
||||
createdComponent.FolderPath = &folderPath
|
||||
createdComponent.FileType = &fileType
|
||||
|
||||
@@ -255,7 +255,7 @@ func (s *UIComponentApplicationServiceImpl) CreateUIComponentWithFiles(ctx conte
|
||||
}
|
||||
|
||||
// 更新组件信息
|
||||
folderPath := "resources/Pure Component/src/ui"
|
||||
folderPath := "resources/Pure_Component/src/ui"
|
||||
createdComponent.FolderPath = &folderPath
|
||||
|
||||
// 检查是否有ZIP文件
|
||||
@@ -363,7 +363,7 @@ func (s *UIComponentApplicationServiceImpl) CreateUIComponentWithFilesAndPaths(c
|
||||
}
|
||||
|
||||
// 更新组件信息
|
||||
folderPath := "resources/Pure Component/src/ui"
|
||||
folderPath := "resources/Pure_Component/src/ui"
|
||||
createdComponent.FolderPath = &folderPath
|
||||
|
||||
// 检查是否有ZIP文件
|
||||
@@ -634,7 +634,7 @@ func (s *UIComponentApplicationServiceImpl) UploadAndExtractUIComponentFile(ctx
|
||||
fileType := strings.ToLower(filepath.Ext(file.Filename))
|
||||
|
||||
// 更新组件信息
|
||||
folderPath := "resources/Pure Component/src/ui"
|
||||
folderPath := "resources/Pure_Component/src/ui"
|
||||
component.FolderPath = &folderPath
|
||||
component.FileType = &fileType
|
||||
|
||||
|
||||
@@ -2772,16 +2772,17 @@ func (s *StatisticsApplicationServiceImpl) AdminGetTodayCertifiedEnterprises(ctx
|
||||
var enterprises []map[string]interface{}
|
||||
for _, cert := range completedCertifications {
|
||||
// 获取企业信息
|
||||
enterpriseInfo, err := s.enterpriseInfoRepo.GetByUserID(ctx, cert.UserID)
|
||||
// 使用预加载方法一次性获取用户和企业信息
|
||||
user, err := s.userRepo.GetByIDWithEnterpriseInfo(ctx, cert.UserID)
|
||||
if err != nil {
|
||||
s.logger.Warn("获取企业信息失败", zap.String("user_id", cert.UserID), zap.Error(err))
|
||||
s.logger.Warn("获取用户信息失败", zap.String("user_id", cert.UserID), zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
// 获取用户基本信息(仅需要用户名)
|
||||
user, err := s.userRepo.GetByID(ctx, cert.UserID)
|
||||
if err != nil {
|
||||
s.logger.Warn("获取用户信息失败", zap.String("user_id", cert.UserID), zap.Error(err))
|
||||
// 获取企业信息
|
||||
enterpriseInfo := user.EnterpriseInfo
|
||||
if enterpriseInfo == nil {
|
||||
s.logger.Warn("用户没有企业信息", zap.String("user_id", cert.UserID))
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -571,6 +571,11 @@ func NewContainer() *Container {
|
||||
product_repo.NewGormComponentReportRepository,
|
||||
fx.As(new(domain_product_repo.ComponentReportRepository)),
|
||||
),
|
||||
// 购买订单仓储
|
||||
fx.Annotate(
|
||||
finance_repo.NewGormPurchaseOrderRepository,
|
||||
fx.As(new(domain_finance_repo.PurchaseOrderRepository)),
|
||||
),
|
||||
// UI组件仓储 - 同时注册具体类型和接口类型
|
||||
fx.Annotate(
|
||||
product_repo.NewGormUIComponentRepository,
|
||||
@@ -893,6 +898,7 @@ func NewContainer() *Container {
|
||||
alipayOrderRepo domain_finance_repo.AlipayOrderRepository,
|
||||
wechatOrderRepo domain_finance_repo.WechatOrderRepository,
|
||||
rechargeRecordRepo domain_finance_repo.RechargeRecordRepository,
|
||||
purchaseOrderRepo domain_finance_repo.PurchaseOrderRepository,
|
||||
userRepo domain_user_repo.UserRepository,
|
||||
txManager *shared_database.TransactionManager,
|
||||
logger *zap.Logger,
|
||||
@@ -909,6 +915,7 @@ func NewContainer() *Container {
|
||||
alipayOrderRepo,
|
||||
wechatOrderRepo,
|
||||
rechargeRecordRepo,
|
||||
purchaseOrderRepo,
|
||||
componentReportRepo,
|
||||
userRepo,
|
||||
txManager,
|
||||
@@ -950,6 +957,34 @@ func NewContainer() *Container {
|
||||
},
|
||||
fx.As(new(product.ProductApplicationService)),
|
||||
),
|
||||
// 组件报告订单服务
|
||||
func(
|
||||
productRepo domain_product_repo.ProductRepository,
|
||||
docRepo domain_product_repo.ProductDocumentationRepository,
|
||||
apiConfigRepo domain_product_repo.ProductApiConfigRepository,
|
||||
purchaseOrderRepo domain_finance_repo.PurchaseOrderRepository,
|
||||
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,
|
||||
) *product.ComponentReportOrderService {
|
||||
return product.NewComponentReportOrderService(
|
||||
productRepo,
|
||||
docRepo,
|
||||
apiConfigRepo,
|
||||
purchaseOrderRepo,
|
||||
componentReportRepo,
|
||||
rechargeRecordRepo,
|
||||
alipayOrderRepo,
|
||||
wechatOrderRepo,
|
||||
aliPayService,
|
||||
wechatPayService,
|
||||
logger,
|
||||
)
|
||||
},
|
||||
// 产品API配置应用服务 - 绑定到接口
|
||||
fx.Annotate(
|
||||
product.NewProductApiConfigApplicationService,
|
||||
@@ -1055,7 +1090,7 @@ func NewContainer() *Container {
|
||||
logger *zap.Logger,
|
||||
) product.UIComponentApplicationService {
|
||||
// 创建UI组件文件服务
|
||||
basePath := "resources/Pure Component/src/ui"
|
||||
basePath := "resources/Pure_Component/src/ui"
|
||||
fileService := product.NewUIComponentFileService(basePath, logger)
|
||||
|
||||
return product.NewUIComponentApplicationService(
|
||||
@@ -1183,6 +1218,7 @@ func NewContainer() *Container {
|
||||
docRepo domain_product_repo.ProductDocumentationRepository,
|
||||
apiConfigRepo domain_product_repo.ProductApiConfigRepository,
|
||||
componentReportRepo domain_product_repo.ComponentReportRepository,
|
||||
purchaseOrderRepo domain_finance_repo.PurchaseOrderRepository,
|
||||
rechargeRecordRepo domain_finance_repo.RechargeRecordRepository,
|
||||
alipayOrderRepo domain_finance_repo.AlipayOrderRepository,
|
||||
wechatOrderRepo domain_finance_repo.WechatOrderRepository,
|
||||
@@ -1190,7 +1226,14 @@ func NewContainer() *Container {
|
||||
wechatPayService *payment.WechatPayService,
|
||||
logger *zap.Logger,
|
||||
) *component_report.ComponentReportHandler {
|
||||
return component_report.NewComponentReportHandler(productRepo, docRepo, apiConfigRepo, componentReportRepo, rechargeRecordRepo, alipayOrderRepo, wechatOrderRepo, aliPayService, wechatPayService, logger)
|
||||
return component_report.NewComponentReportHandler(productRepo, docRepo, apiConfigRepo, componentReportRepo, purchaseOrderRepo, rechargeRecordRepo, alipayOrderRepo, wechatOrderRepo, aliPayService, wechatPayService, logger)
|
||||
},
|
||||
// 组件报告订单处理器
|
||||
func(
|
||||
componentReportOrderService *product.ComponentReportOrderService,
|
||||
logger *zap.Logger,
|
||||
) *handlers.ComponentReportOrderHandler {
|
||||
return handlers.NewComponentReportOrderHandler(componentReportOrderService, logger)
|
||||
},
|
||||
// UI组件HTTP处理器
|
||||
func(
|
||||
@@ -1215,6 +1258,8 @@ func NewContainer() *Container {
|
||||
routes.NewProductRoutes,
|
||||
// 产品管理员路由
|
||||
routes.NewProductAdminRoutes,
|
||||
// 组件报告订单路由
|
||||
routes.NewComponentReportOrderRoutes,
|
||||
// UI组件路由
|
||||
routes.NewUIComponentRoutes,
|
||||
// 文章路由
|
||||
@@ -1331,6 +1376,7 @@ func RegisterRoutes(
|
||||
financeRoutes *routes.FinanceRoutes,
|
||||
productRoutes *routes.ProductRoutes,
|
||||
productAdminRoutes *routes.ProductAdminRoutes,
|
||||
componentReportOrderRoutes *routes.ComponentReportOrderRoutes,
|
||||
uiComponentRoutes *routes.UIComponentRoutes,
|
||||
articleRoutes *routes.ArticleRoutes,
|
||||
announcementRoutes *routes.AnnouncementRoutes,
|
||||
@@ -1352,6 +1398,7 @@ func RegisterRoutes(
|
||||
financeRoutes.Register(router)
|
||||
productRoutes.Register(router)
|
||||
productAdminRoutes.Register(router)
|
||||
componentReportOrderRoutes.Register(router)
|
||||
uiComponentRoutes.Register(router)
|
||||
|
||||
articleRoutes.Register(router)
|
||||
|
||||
180
internal/domains/finance/entities/purchase_order.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// PurchaseOrderStatus 购买订单状态枚举(通用)
|
||||
type PurchaseOrderStatus string
|
||||
|
||||
const (
|
||||
PurchaseOrderStatusCreated PurchaseOrderStatus = "created" // 已创建
|
||||
PurchaseOrderStatusPaid PurchaseOrderStatus = "paid" // 已支付
|
||||
PurchaseOrderStatusFailed PurchaseOrderStatus = "failed" // 支付失败
|
||||
PurchaseOrderStatusCancelled PurchaseOrderStatus = "cancelled" // 已取消
|
||||
PurchaseOrderStatusRefunded PurchaseOrderStatus = "refunded" // 已退款
|
||||
PurchaseOrderStatusClosed PurchaseOrderStatus = "closed" // 已关闭
|
||||
)
|
||||
|
||||
// PurchaseOrder 购买订单实体(统一表 ty_purchase_orders,兼容多支付渠道)
|
||||
type PurchaseOrder struct {
|
||||
// 基础标识
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"购买订单唯一标识"`
|
||||
UserID string `gorm:"type:varchar(36);not null;index" json:"user_id" comment:"购买用户ID"`
|
||||
OrderNo string `gorm:"type:varchar(64);not null;uniqueIndex" json:"order_no" comment:"商户订单号"`
|
||||
TradeNo *string `gorm:"type:varchar(64);uniqueIndex" json:"trade_no,omitempty" comment:"第三方支付交易号"`
|
||||
|
||||
// 产品信息
|
||||
ProductID string `gorm:"type:varchar(36);not null;index" json:"product_id" comment:"产品ID"`
|
||||
ProductCode string `gorm:"type:varchar(50);not null" json:"product_code" comment:"产品编号"`
|
||||
ProductName string `gorm:"type:varchar(200);not null" json:"product_name" comment:"产品名称"`
|
||||
Category string `gorm:"type:varchar(50)" json:"category,omitempty" comment:"产品分类"`
|
||||
|
||||
// 订单信息
|
||||
Subject string `gorm:"type:varchar(200);not null" json:"subject" comment:"订单标题"`
|
||||
Amount decimal.Decimal `gorm:"type:decimal(20,8);not null" json:"amount" comment:"订单金额"`
|
||||
PayAmount *decimal.Decimal `gorm:"type:decimal(20,8)" json:"pay_amount,omitempty" comment:"实际支付金额"`
|
||||
Status PurchaseOrderStatus `gorm:"type:varchar(20);not null;default:'created';index" json:"status" comment:"订单状态"`
|
||||
Platform string `gorm:"type:varchar(20);not null" json:"platform" comment:"下单平台:app/h5/pc/wx_h5/wx_mini等"`
|
||||
PayChannel string `gorm:"type:varchar(20);default:'alipay';index" json:"pay_channel" comment:"支付渠道:alipay/wechat"`
|
||||
PaymentType string `gorm:"type:varchar(20);not null" json:"payment_type" comment:"支付类型:alipay, wechat, free"`
|
||||
|
||||
// 支付渠道返回信息
|
||||
BuyerID string `gorm:"type:varchar(64)" json:"buyer_id,omitempty" comment:"买家ID(支付渠道方)"`
|
||||
SellerID string `gorm:"type:varchar(64)" json:"seller_id,omitempty" comment:"卖家ID(支付渠道方)"`
|
||||
ReceiptAmount decimal.Decimal `gorm:"type:decimal(20,8)" json:"receipt_amount,omitempty" comment:"实收金额"`
|
||||
|
||||
// 回调信息
|
||||
NotifyTime *time.Time `gorm:"index" json:"notify_time,omitempty" comment:"异步通知时间"`
|
||||
ReturnTime *time.Time `gorm:"index" json:"return_time,omitempty" comment:"同步返回时间"`
|
||||
PayTime *time.Time `gorm:"index" json:"pay_time,omitempty" comment:"支付完成时间"`
|
||||
|
||||
// 文件信息
|
||||
FilePath *string `gorm:"type:varchar(500)" json:"file_path,omitempty" comment:"产品文件路径"`
|
||||
FileSize *int64 `gorm:"type:bigint" json:"file_size,omitempty" comment:"文件大小(字节)"`
|
||||
|
||||
// 备注信息
|
||||
Remark string `gorm:"type:varchar(500)" json:"remark,omitempty" comment:"备注信息"`
|
||||
|
||||
// 错误信息
|
||||
ErrorCode string `gorm:"type:varchar(64)" json:"error_code,omitempty" comment:"错误码"`
|
||||
ErrorMessage string `gorm:"type:text" json:"error_message,omitempty" 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:"-" comment:"软删除时间"`
|
||||
}
|
||||
|
||||
// TableName 指定数据库表名
|
||||
func (PurchaseOrder) TableName() string {
|
||||
return "ty_purchase_orders"
|
||||
}
|
||||
|
||||
// BeforeCreate GORM钩子:创建前自动生成UUID和订单号
|
||||
func (p *PurchaseOrder) BeforeCreate(tx *gorm.DB) error {
|
||||
if p.ID == "" {
|
||||
p.ID = uuid.New().String()
|
||||
}
|
||||
if p.OrderNo == "" {
|
||||
p.OrderNo = generatePurchaseOrderNo()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// generatePurchaseOrderNo 生成购买订单号
|
||||
func generatePurchaseOrderNo() string {
|
||||
// 使用时间戳+随机数生成唯一订单号,例如:PO202312200001
|
||||
timestamp := time.Now().Format("20060102")
|
||||
random := fmt.Sprintf("%04d", rand.Intn(9999))
|
||||
return fmt.Sprintf("PO%s%s", timestamp, random)
|
||||
}
|
||||
|
||||
// IsCreated 检查是否为已创建状态
|
||||
func (p *PurchaseOrder) IsCreated() bool {
|
||||
return p.Status == PurchaseOrderStatusCreated
|
||||
}
|
||||
|
||||
// IsPaid 检查是否为已支付状态
|
||||
func (p *PurchaseOrder) IsPaid() bool {
|
||||
return p.Status == PurchaseOrderStatusPaid
|
||||
}
|
||||
|
||||
// IsFailed 检查是否为支付失败状态
|
||||
func (p *PurchaseOrder) IsFailed() bool {
|
||||
return p.Status == PurchaseOrderStatusFailed
|
||||
}
|
||||
|
||||
// IsCancelled 检查是否为已取消状态
|
||||
func (p *PurchaseOrder) IsCancelled() bool {
|
||||
return p.Status == PurchaseOrderStatusCancelled
|
||||
}
|
||||
|
||||
// IsRefunded 检查是否为已退款状态
|
||||
func (p *PurchaseOrder) IsRefunded() bool {
|
||||
return p.Status == PurchaseOrderStatusRefunded
|
||||
}
|
||||
|
||||
// IsClosed 检查是否为已关闭状态
|
||||
func (p *PurchaseOrder) IsClosed() bool {
|
||||
return p.Status == PurchaseOrderStatusClosed
|
||||
}
|
||||
|
||||
// MarkPaid 标记为已支付
|
||||
func (p *PurchaseOrder) MarkPaid(tradeNo, buyerID, sellerID string, payAmount, receiptAmount decimal.Decimal) {
|
||||
p.Status = PurchaseOrderStatusPaid
|
||||
p.TradeNo = &tradeNo
|
||||
p.BuyerID = buyerID
|
||||
p.SellerID = sellerID
|
||||
p.PayAmount = &payAmount
|
||||
p.ReceiptAmount = receiptAmount
|
||||
now := time.Now()
|
||||
p.PayTime = &now
|
||||
p.NotifyTime = &now
|
||||
}
|
||||
|
||||
// MarkFailed 标记为支付失败
|
||||
func (p *PurchaseOrder) MarkFailed(errorCode, errorMessage string) {
|
||||
p.Status = PurchaseOrderStatusFailed
|
||||
p.ErrorCode = errorCode
|
||||
p.ErrorMessage = errorMessage
|
||||
}
|
||||
|
||||
// MarkCancelled 标记为已取消
|
||||
func (p *PurchaseOrder) MarkCancelled() {
|
||||
p.Status = PurchaseOrderStatusCancelled
|
||||
}
|
||||
|
||||
// MarkRefunded 标记为已退款
|
||||
func (p *PurchaseOrder) MarkRefunded() {
|
||||
p.Status = PurchaseOrderStatusRefunded
|
||||
}
|
||||
|
||||
// MarkClosed 标记为已关闭
|
||||
func (p *PurchaseOrder) MarkClosed() {
|
||||
p.Status = PurchaseOrderStatusClosed
|
||||
}
|
||||
|
||||
// NewPurchaseOrder 通用工厂方法 - 创建购买订单(支持多支付渠道)
|
||||
func NewPurchaseOrder(userID, productID, productCode, productName, subject string, amount decimal.Decimal, platform, payChannel, paymentType string) *PurchaseOrder {
|
||||
return &PurchaseOrder{
|
||||
ID: uuid.New().String(),
|
||||
UserID: userID,
|
||||
OrderNo: generatePurchaseOrderNo(),
|
||||
ProductID: productID,
|
||||
ProductCode: productCode,
|
||||
ProductName: productName,
|
||||
Subject: subject,
|
||||
Amount: amount,
|
||||
Status: PurchaseOrderStatusCreated,
|
||||
Platform: platform,
|
||||
PayChannel: payChannel,
|
||||
PaymentType: paymentType,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
finance_entities "tyapi-server/internal/domains/finance/entities"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// PurchaseOrderRepository 购买订单仓储接口
|
||||
type PurchaseOrderRepository interface {
|
||||
// 创建订单
|
||||
Create(ctx context.Context, order *finance_entities.PurchaseOrder) (*finance_entities.PurchaseOrder, error)
|
||||
|
||||
// 更新订单
|
||||
Update(ctx context.Context, order *finance_entities.PurchaseOrder) error
|
||||
|
||||
// 根据ID获取订单
|
||||
GetByID(ctx context.Context, id string) (*finance_entities.PurchaseOrder, error)
|
||||
|
||||
// 根据订单号获取订单
|
||||
GetByOrderNo(ctx context.Context, orderNo string) (*finance_entities.PurchaseOrder, error)
|
||||
|
||||
// 根据用户ID获取订单列表
|
||||
GetByUserID(ctx context.Context, userID string, limit, offset int) ([]*finance_entities.PurchaseOrder, int64, error)
|
||||
|
||||
// 根据产品ID和用户ID获取订单
|
||||
GetByUserIDAndProductID(ctx context.Context, userID, productID string) (*finance_entities.PurchaseOrder, error)
|
||||
|
||||
// 根据支付类型和第三方交易号获取订单
|
||||
GetByPaymentTypeAndTransactionID(ctx context.Context, paymentType, transactionID string) (*finance_entities.PurchaseOrder, error)
|
||||
|
||||
// 根据交易号获取订单
|
||||
GetByTradeNo(ctx context.Context, tradeNo string) (*finance_entities.PurchaseOrder, error)
|
||||
|
||||
// 更新支付状态
|
||||
UpdatePaymentStatus(ctx context.Context, orderID string, status finance_entities.PurchaseOrderStatus, tradeNo *string, payAmount, receiptAmount *string, paymentTime *time.Time) error
|
||||
|
||||
// 获取用户已购买的产品编号列表
|
||||
GetUserPurchasedProductCodes(ctx context.Context, userID string) ([]string, error)
|
||||
|
||||
// 检查用户是否已购买指定产品
|
||||
HasUserPurchased(ctx context.Context, userID string, productCode string) (bool, error)
|
||||
|
||||
// 获取即将过期的订单(用于清理)
|
||||
GetExpiringOrders(ctx context.Context, before time.Time, limit int) ([]*finance_entities.PurchaseOrder, error)
|
||||
|
||||
// 获取已过期订单(用于清理)
|
||||
GetExpiredOrders(ctx context.Context, limit int) ([]*finance_entities.PurchaseOrder, error)
|
||||
|
||||
// 获取用户已支付的产品ID列表
|
||||
GetUserPaidProductIDs(ctx context.Context, userID string) ([]string, error)
|
||||
|
||||
// 根据状态获取订单列表
|
||||
GetByStatus(ctx context.Context, status finance_entities.PurchaseOrderStatus, limit, offset int) ([]*finance_entities.PurchaseOrder, int64, error)
|
||||
|
||||
// 根据筛选条件获取订单列表
|
||||
GetByFilters(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) ([]*finance_entities.PurchaseOrder, error)
|
||||
|
||||
// 根据筛选条件统计订单数量
|
||||
CountByFilters(ctx context.Context, filters map[string]interface{}) (int64, error)
|
||||
}
|
||||
@@ -4,29 +4,31 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ComponentReportDownload 组件报告下载记录
|
||||
type ComponentReportDownload struct {
|
||||
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天)"`
|
||||
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:"产品编号"`
|
||||
ProductName string `gorm:"type:varchar(200);not null" comment:"产品名称"`
|
||||
|
||||
// 直接关联购买订单
|
||||
OrderID *string `gorm:"type:varchar(36);index" comment:"关联的购买订单ID"`
|
||||
OrderNumber *string `gorm:"type:varchar(64);index" comment:"关联的购买订单号"`
|
||||
|
||||
// 组合包相关字段(从购买记录复制)
|
||||
SubProductIDs string `gorm:"type:text" comment:"子产品ID列表(JSON数组,组合包使用)"`
|
||||
SubProductCodes string `gorm:"type:text" comment:"子产品编号列表(JSON数组)"`
|
||||
|
||||
// 下载相关信息
|
||||
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" comment:"创建时间"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" comment:"更新时间"`
|
||||
@@ -46,11 +48,6 @@ func (c *ComponentReportDownload) BeforeCreate(tx *gorm.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsPaid 检查是否已支付
|
||||
func (c *ComponentReportDownload) IsPaid() bool {
|
||||
return c.PaymentStatus == "success"
|
||||
}
|
||||
|
||||
// IsExpired 检查是否已过期
|
||||
func (c *ComponentReportDownload) IsExpired() bool {
|
||||
if c.ExpiresAt == nil {
|
||||
@@ -61,5 +58,6 @@ func (c *ComponentReportDownload) IsExpired() bool {
|
||||
|
||||
// CanDownload 检查是否可以下载
|
||||
func (c *ComponentReportDownload) CanDownload() bool {
|
||||
return c.IsPaid() && !c.IsExpired()
|
||||
// 下载记录存在即表示用户有下载权限,只需检查是否过期
|
||||
return !c.IsExpired()
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
// ComponentReportRepository 组件报告仓储接口
|
||||
type ComponentReportRepository interface {
|
||||
// 创建下载记录
|
||||
CreateDownload(ctx context.Context, download *entities.ComponentReportDownload) (*entities.ComponentReportDownload, error)
|
||||
Create(ctx context.Context, download *entities.ComponentReportDownload) error
|
||||
|
||||
// 更新下载记录
|
||||
UpdateDownload(ctx context.Context, download *entities.ComponentReportDownload) error
|
||||
@@ -20,6 +20,15 @@ type ComponentReportRepository interface {
|
||||
// 获取用户的下载记录列表
|
||||
GetUserDownloads(ctx context.Context, userID string, productID *string) ([]*entities.ComponentReportDownload, error)
|
||||
|
||||
// 获取用户有效的下载记录
|
||||
GetActiveDownload(ctx context.Context, userID, productID string) (*entities.ComponentReportDownload, error)
|
||||
|
||||
// 更新下载记录文件路径
|
||||
UpdateFilePath(ctx context.Context, downloadID, filePath string) error
|
||||
|
||||
// 增加下载次数
|
||||
IncrementDownloadCount(ctx context.Context, downloadID string) error
|
||||
|
||||
// 检查用户是否已下载过指定产品编号的组件
|
||||
HasUserDownloaded(ctx context.Context, userID string, productCode string) (bool, error)
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ func (s *UserAggregateServiceImpl) CreateUser(ctx context.Context, phone, passwo
|
||||
func (s *UserAggregateServiceImpl) LoadUser(ctx context.Context, userID string) (*entities.User, error) {
|
||||
s.logger.Debug("加载用户聚合根", zap.String("user_id", userID))
|
||||
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
user, err := s.userRepo.GetByIDWithEnterpriseInfo(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("用户不存在: %w", err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/domains/finance/entities"
|
||||
"tyapi-server/internal/domains/finance/repositories"
|
||||
"tyapi-server/internal/shared/database"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
PurchaseOrdersTable = "ty_purchase_orders"
|
||||
)
|
||||
|
||||
type GormPurchaseOrderRepository struct {
|
||||
*database.CachedBaseRepositoryImpl
|
||||
}
|
||||
|
||||
var _ repositories.PurchaseOrderRepository = (*GormPurchaseOrderRepository)(nil)
|
||||
|
||||
func NewGormPurchaseOrderRepository(db *gorm.DB, logger *zap.Logger) repositories.PurchaseOrderRepository {
|
||||
return &GormPurchaseOrderRepository{
|
||||
CachedBaseRepositoryImpl: database.NewCachedBaseRepositoryImpl(db, logger, PurchaseOrdersTable),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *GormPurchaseOrderRepository) Create(ctx context.Context, order *entities.PurchaseOrder) (*entities.PurchaseOrder, error) {
|
||||
err := r.CreateEntity(ctx, order)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return order, nil
|
||||
}
|
||||
|
||||
func (r *GormPurchaseOrderRepository) Update(ctx context.Context, order *entities.PurchaseOrder) error {
|
||||
return r.UpdateEntity(ctx, order)
|
||||
}
|
||||
|
||||
func (r *GormPurchaseOrderRepository) GetByID(ctx context.Context, id string) (*entities.PurchaseOrder, error) {
|
||||
var order entities.PurchaseOrder
|
||||
err := r.SmartGetByID(ctx, id, &order)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &order, nil
|
||||
}
|
||||
|
||||
func (r *GormPurchaseOrderRepository) GetByOrderNo(ctx context.Context, orderNo string) (*entities.PurchaseOrder, error) {
|
||||
var order entities.PurchaseOrder
|
||||
err := r.GetDB(ctx).Where("order_no = ?", orderNo).First(&order).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &order, nil
|
||||
}
|
||||
|
||||
func (r *GormPurchaseOrderRepository) GetByUserID(ctx context.Context, userID string, limit, offset int) ([]*entities.PurchaseOrder, int64, error) {
|
||||
var orders []entities.PurchaseOrder
|
||||
var count int64
|
||||
|
||||
db := r.GetDB(ctx).Where("user_id = ?", userID)
|
||||
|
||||
// 获取总数
|
||||
err := db.Model(&entities.PurchaseOrder{}).Count(&count).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
err = db.Order("created_at DESC").
|
||||
Limit(limit).
|
||||
Offset(offset).
|
||||
Find(&orders).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
result := make([]*entities.PurchaseOrder, len(orders))
|
||||
for i := range orders {
|
||||
result[i] = &orders[i]
|
||||
}
|
||||
|
||||
return result, count, nil
|
||||
}
|
||||
|
||||
func (r *GormPurchaseOrderRepository) GetByUserIDAndProductID(ctx context.Context, userID, productID string) (*entities.PurchaseOrder, error) {
|
||||
var order entities.PurchaseOrder
|
||||
err := r.GetDB(ctx).
|
||||
Where("user_id = ? AND product_id = ? AND status = ?", userID, productID, entities.PurchaseOrderStatusPaid).
|
||||
First(&order).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &order, nil
|
||||
}
|
||||
|
||||
func (r *GormPurchaseOrderRepository) GetByPaymentTypeAndTransactionID(ctx context.Context, paymentType, transactionID string) (*entities.PurchaseOrder, error) {
|
||||
var order entities.PurchaseOrder
|
||||
err := r.GetDB(ctx).
|
||||
Where("payment_type = ? AND trade_no = ?", paymentType, transactionID).
|
||||
First(&order).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &order, nil
|
||||
}
|
||||
|
||||
func (r *GormPurchaseOrderRepository) GetByTradeNo(ctx context.Context, tradeNo string) (*entities.PurchaseOrder, error) {
|
||||
var order entities.PurchaseOrder
|
||||
err := r.GetDB(ctx).Where("trade_no = ?", tradeNo).First(&order).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &order, nil
|
||||
}
|
||||
|
||||
func (r *GormPurchaseOrderRepository) UpdatePaymentStatus(ctx context.Context, orderID string, status entities.PurchaseOrderStatus, tradeNo *string, payAmount, receiptAmount *string, paymentTime *time.Time) error {
|
||||
updates := map[string]interface{}{
|
||||
"status": status,
|
||||
}
|
||||
|
||||
if tradeNo != nil {
|
||||
updates["trade_no"] = *tradeNo
|
||||
}
|
||||
|
||||
if payAmount != nil {
|
||||
updates["pay_amount"] = *payAmount
|
||||
}
|
||||
|
||||
if receiptAmount != nil {
|
||||
updates["receipt_amount"] = *receiptAmount
|
||||
}
|
||||
|
||||
if paymentTime != nil {
|
||||
updates["pay_time"] = *paymentTime
|
||||
updates["notify_time"] = *paymentTime
|
||||
}
|
||||
|
||||
err := r.GetDB(ctx).
|
||||
Model(&entities.PurchaseOrder{}).
|
||||
Where("id = ?", orderID).
|
||||
Updates(updates).Error
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *GormPurchaseOrderRepository) GetUserPurchasedProductCodes(ctx context.Context, userID string) ([]string, error) {
|
||||
var orders []entities.PurchaseOrder
|
||||
err := r.GetDB(ctx).
|
||||
Select("product_code").
|
||||
Where("user_id = ? AND status = ?", userID, entities.PurchaseOrderStatusPaid).
|
||||
Find(&orders).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
codesMap := make(map[string]bool)
|
||||
for _, order := range orders {
|
||||
// 添加主产品编号
|
||||
if order.ProductCode != "" {
|
||||
codesMap[order.ProductCode] = true
|
||||
}
|
||||
}
|
||||
|
||||
codes := make([]string, 0, len(codesMap))
|
||||
for code := range codesMap {
|
||||
codes = append(codes, code)
|
||||
}
|
||||
return codes, nil
|
||||
}
|
||||
|
||||
func (r *GormPurchaseOrderRepository) GetUserPaidProductIDs(ctx context.Context, userID string) ([]string, error) {
|
||||
var orders []entities.PurchaseOrder
|
||||
err := r.GetDB(ctx).
|
||||
Select("product_id").
|
||||
Where("user_id = ? AND status = ?", userID, entities.PurchaseOrderStatusPaid).
|
||||
Find(&orders).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
idsMap := make(map[string]bool)
|
||||
for _, order := range orders {
|
||||
// 添加主产品ID
|
||||
if order.ProductID != "" {
|
||||
idsMap[order.ProductID] = true
|
||||
}
|
||||
}
|
||||
|
||||
ids := make([]string, 0, len(idsMap))
|
||||
for id := range idsMap {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (r *GormPurchaseOrderRepository) HasUserPurchased(ctx context.Context, userID string, productCode string) (bool, error) {
|
||||
var count int64
|
||||
err := r.GetDB(ctx).Model(&entities.PurchaseOrder{}).
|
||||
Where("user_id = ? AND product_code = ? AND status = ?", userID, productCode, entities.PurchaseOrderStatusPaid).
|
||||
Count(&count).Error
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (r *GormPurchaseOrderRepository) GetExpiringOrders(ctx context.Context, before time.Time, limit int) ([]*entities.PurchaseOrder, error) {
|
||||
// 购买订单实体没有过期时间字段,此方法返回空结果
|
||||
return []*entities.PurchaseOrder{}, nil
|
||||
}
|
||||
|
||||
func (r *GormPurchaseOrderRepository) GetExpiredOrders(ctx context.Context, limit int) ([]*entities.PurchaseOrder, error) {
|
||||
// 购买订单实体没有过期时间字段,此方法返回空结果
|
||||
return []*entities.PurchaseOrder{}, nil
|
||||
}
|
||||
|
||||
func (r *GormPurchaseOrderRepository) GetByStatus(ctx context.Context, status entities.PurchaseOrderStatus, limit, offset int) ([]*entities.PurchaseOrder, int64, error) {
|
||||
var orders []entities.PurchaseOrder
|
||||
var count int64
|
||||
|
||||
db := r.GetDB(ctx).Where("status = ?", status)
|
||||
|
||||
// 获取总数
|
||||
err := db.Model(&entities.PurchaseOrder{}).Count(&count).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
err = db.Order("created_at DESC").
|
||||
Limit(limit).
|
||||
Offset(offset).
|
||||
Find(&orders).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
result := make([]*entities.PurchaseOrder, len(orders))
|
||||
for i := range orders {
|
||||
result[i] = &orders[i]
|
||||
}
|
||||
|
||||
return result, count, nil
|
||||
}
|
||||
|
||||
func (r *GormPurchaseOrderRepository) GetByFilters(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) ([]*entities.PurchaseOrder, error) {
|
||||
var orders []entities.PurchaseOrder
|
||||
|
||||
db := r.GetDB(ctx)
|
||||
|
||||
// 应用筛选条件
|
||||
if filters != nil {
|
||||
if userID, ok := filters["user_id"]; ok {
|
||||
db = db.Where("user_id = ?", userID)
|
||||
}
|
||||
if status, ok := filters["status"]; ok && status != "" {
|
||||
db = db.Where("status = ?", status)
|
||||
}
|
||||
if paymentType, ok := filters["payment_type"]; ok && paymentType != "" {
|
||||
db = db.Where("payment_type = ?", paymentType)
|
||||
}
|
||||
if payChannel, ok := filters["pay_channel"]; ok && payChannel != "" {
|
||||
db = db.Where("pay_channel = ?", payChannel)
|
||||
}
|
||||
if startTime, ok := filters["start_time"]; ok && startTime != "" {
|
||||
db = db.Where("created_at >= ?", startTime)
|
||||
}
|
||||
if endTime, ok := filters["end_time"]; ok && endTime != "" {
|
||||
db = db.Where("created_at <= ?", endTime)
|
||||
}
|
||||
}
|
||||
|
||||
// 应用排序和分页
|
||||
// 默认按创建时间倒序
|
||||
db = db.Order("created_at DESC")
|
||||
|
||||
// 应用分页
|
||||
if options.PageSize > 0 {
|
||||
db = db.Limit(options.PageSize)
|
||||
}
|
||||
|
||||
if options.Page > 0 {
|
||||
db = db.Offset((options.Page - 1) * options.PageSize)
|
||||
}
|
||||
|
||||
// 执行查询
|
||||
err := db.Find(&orders).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为指针切片
|
||||
result := make([]*entities.PurchaseOrder, len(orders))
|
||||
for i := range orders {
|
||||
result[i] = &orders[i]
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *GormPurchaseOrderRepository) CountByFilters(ctx context.Context, filters map[string]interface{}) (int64, error) {
|
||||
var count int64
|
||||
|
||||
db := r.GetDB(ctx).Model(&entities.PurchaseOrder{})
|
||||
|
||||
// 应用筛选条件
|
||||
if filters != nil {
|
||||
if userID, ok := filters["user_id"]; ok {
|
||||
db = db.Where("user_id = ?", userID)
|
||||
}
|
||||
if status, ok := filters["status"]; ok && status != "" {
|
||||
db = db.Where("status = ?", status)
|
||||
}
|
||||
if paymentType, ok := filters["payment_type"]; ok && paymentType != "" {
|
||||
db = db.Where("payment_type = ?", paymentType)
|
||||
}
|
||||
if payChannel, ok := filters["pay_channel"]; ok && payChannel != "" {
|
||||
db = db.Where("pay_channel = ?", payChannel)
|
||||
}
|
||||
if startTime, ok := filters["start_time"]; ok && startTime != "" {
|
||||
db = db.Where("created_at >= ?", startTime)
|
||||
}
|
||||
if endTime, ok := filters["end_time"]; ok && endTime != "" {
|
||||
db = db.Where("created_at <= ?", endTime)
|
||||
}
|
||||
}
|
||||
|
||||
// 执行计数
|
||||
err := db.Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/domains/product/entities"
|
||||
"tyapi-server/internal/domains/product/repositories"
|
||||
@@ -29,12 +30,8 @@ func NewGormComponentReportRepository(db *gorm.DB, logger *zap.Logger) repositor
|
||||
}
|
||||
}
|
||||
|
||||
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) Create(ctx context.Context, download *entities.ComponentReportDownload) error {
|
||||
return r.CreateEntity(ctx, download)
|
||||
}
|
||||
|
||||
func (r *GormComponentReportRepository) UpdateDownload(ctx context.Context, download *entities.ComponentReportDownload) error {
|
||||
@@ -55,7 +52,7 @@ func (r *GormComponentReportRepository) GetDownloadByID(ctx context.Context, id
|
||||
|
||||
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")
|
||||
query := r.GetDB(ctx).Where("user_id = ?", userID)
|
||||
|
||||
if productID != nil && *productID != "" {
|
||||
query = query.Where("product_id = ?", *productID)
|
||||
@@ -76,7 +73,7 @@ func (r *GormComponentReportRepository) GetUserDownloads(ctx context.Context, us
|
||||
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").
|
||||
Where("user_id = ? AND product_code = ?", userID, productCode).
|
||||
Count(&count).Error
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -88,7 +85,7 @@ func (r *GormComponentReportRepository) GetUserDownloadedProductCodes(ctx contex
|
||||
var downloads []entities.ComponentReportDownload
|
||||
err := r.GetDB(ctx).
|
||||
Select("DISTINCT sub_product_codes").
|
||||
Where("user_id = ? AND payment_status = ?", userID, "success").
|
||||
Where("user_id = ?", userID).
|
||||
Find(&downloads).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -119,7 +116,7 @@ func (r *GormComponentReportRepository) GetUserDownloadedProductCodes(ctx contex
|
||||
|
||||
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
|
||||
err := r.GetDB(ctx).Where("order_number = ?", orderID).First(&download).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
@@ -128,3 +125,65 @@ func (r *GormComponentReportRepository) GetDownloadByPaymentOrderID(ctx context.
|
||||
}
|
||||
return &download, nil
|
||||
}
|
||||
|
||||
// GetActiveDownload 获取用户有效的下载记录
|
||||
func (r *GormComponentReportRepository) GetActiveDownload(ctx context.Context, userID, productID string) (*entities.ComponentReportDownload, error) {
|
||||
var download entities.ComponentReportDownload
|
||||
|
||||
// 先尝试查找有支付订单号的下载记录(已支付)
|
||||
err := r.GetDB(ctx).
|
||||
Where("user_id = ? AND product_id = ? AND order_number IS NOT NULL AND deleted_at IS NULL", userID, productID).
|
||||
Order("created_at DESC").
|
||||
First(&download).Error
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// 如果没有找到有支付订单号的记录,尝试查找任何有效的下载记录
|
||||
err = r.GetDB(ctx).
|
||||
Where("user_id = ? AND product_id = ? AND deleted_at IS NULL", userID, productID).
|
||||
Order("created_at DESC").
|
||||
First(&download).Error
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 如果找到了下载记录,检查关联的购买订单状态
|
||||
if download.OrderID != nil {
|
||||
// 这里需要查询购买订单状态,但当前仓库没有依赖购买订单仓库
|
||||
// 所以只检查是否有过期时间设置,如果有则认为已支付
|
||||
if download.ExpiresAt == nil {
|
||||
return nil, nil // 没有过期时间,表示未支付
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否已过期
|
||||
if download.IsExpired() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &download, nil
|
||||
}
|
||||
|
||||
// UpdateFilePath 更新下载记录文件路径
|
||||
func (r *GormComponentReportRepository) UpdateFilePath(ctx context.Context, downloadID, filePath string) error {
|
||||
return r.GetDB(ctx).Model(&entities.ComponentReportDownload{}).Where("id = ?", downloadID).Update("file_path", filePath).Error
|
||||
}
|
||||
|
||||
// IncrementDownloadCount 增加下载次数
|
||||
func (r *GormComponentReportRepository) IncrementDownloadCount(ctx context.Context, downloadID string) error {
|
||||
now := time.Now()
|
||||
return r.GetDB(ctx).Model(&entities.ComponentReportDownload{}).
|
||||
Where("id = ?", downloadID).
|
||||
Updates(map[string]interface{}{
|
||||
"download_count": gorm.Expr("download_count + 1"),
|
||||
"last_download_at": &now,
|
||||
}).Error
|
||||
}
|
||||
|
||||
@@ -0,0 +1,398 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/application/product"
|
||||
)
|
||||
|
||||
// ComponentReportOrderHandler 组件报告订单处理器
|
||||
type ComponentReportOrderHandler struct {
|
||||
service *product.ComponentReportOrderService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewComponentReportOrderHandler 创建组件报告订单处理器
|
||||
func NewComponentReportOrderHandler(
|
||||
service *product.ComponentReportOrderService,
|
||||
logger *zap.Logger,
|
||||
) *ComponentReportOrderHandler {
|
||||
return &ComponentReportOrderHandler{
|
||||
service: service,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CheckDownloadAvailability 检查下载可用性
|
||||
// GET /api/v1/products/:id/component-report/check
|
||||
func (h *ComponentReportOrderHandler) CheckDownloadAvailability(c *gin.Context) {
|
||||
h.logger.Info("开始检查下载可用性")
|
||||
|
||||
productID := c.Param("id")
|
||||
h.logger.Info("获取产品ID", zap.String("product_id", productID))
|
||||
|
||||
if productID == "" {
|
||||
h.logger.Error("产品ID不能为空")
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"message": "产品ID不能为空",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
userID := c.GetString("user_id")
|
||||
h.logger.Info("获取用户ID", zap.String("user_id", userID))
|
||||
|
||||
if userID == "" {
|
||||
h.logger.Error("用户未登录")
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": 401,
|
||||
"message": "用户未登录",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 调用服务获取订单信息,检查是否可以下载
|
||||
orderInfo, err := h.service.GetOrderInfo(c.Request.Context(), userID, productID)
|
||||
if err != nil {
|
||||
h.logger.Error("获取订单信息失败", zap.Error(err), zap.String("product_id", productID), zap.String("user_id", userID))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"message": "获取订单信息失败",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("获取订单信息成功", zap.Bool("can_download", orderInfo.CanDownload), zap.Bool("is_package", orderInfo.IsPackage))
|
||||
|
||||
// 返回检查结果
|
||||
message := "需要购买"
|
||||
if orderInfo.CanDownload {
|
||||
message = "可以下载"
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"data": gin.H{
|
||||
"can_download": orderInfo.CanDownload,
|
||||
"is_package": orderInfo.IsPackage,
|
||||
"message": message,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetDownloadInfo 获取下载信息和价格计算
|
||||
// GET /api/v1/products/:id/component-report/info
|
||||
func (h *ComponentReportOrderHandler) GetDownloadInfo(c *gin.Context) {
|
||||
h.logger.Info("开始获取下载信息和价格计算")
|
||||
|
||||
productID := c.Param("id")
|
||||
h.logger.Info("获取产品ID", zap.String("product_id", productID))
|
||||
|
||||
if productID == "" {
|
||||
h.logger.Error("产品ID不能为空")
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"message": "产品ID不能为空",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
userID := c.GetString("user_id")
|
||||
h.logger.Info("获取用户ID", zap.String("user_id", userID))
|
||||
|
||||
if userID == "" {
|
||||
h.logger.Error("用户未登录")
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": 401,
|
||||
"message": "用户未登录",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
orderInfo, err := h.service.GetOrderInfo(c.Request.Context(), userID, productID)
|
||||
if err != nil {
|
||||
h.logger.Error("获取订单信息失败", zap.Error(err), zap.String("product_id", productID), zap.String("user_id", userID))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"message": "获取订单信息失败",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 记录详细的订单信息
|
||||
h.logger.Info("获取订单信息成功",
|
||||
zap.String("product_id", orderInfo.ProductID),
|
||||
zap.String("product_code", orderInfo.ProductCode),
|
||||
zap.String("product_name", orderInfo.ProductName),
|
||||
zap.Bool("is_package", orderInfo.IsPackage),
|
||||
zap.Int("sub_products_count", len(orderInfo.SubProducts)),
|
||||
zap.String("price", orderInfo.Price),
|
||||
zap.Strings("purchased_product_codes", orderInfo.PurchasedProductCodes),
|
||||
zap.Bool("can_download", orderInfo.CanDownload),
|
||||
)
|
||||
|
||||
// 记录子产品详情
|
||||
for i, subProduct := range orderInfo.SubProducts {
|
||||
h.logger.Info("子产品信息",
|
||||
zap.Int("index", i),
|
||||
zap.String("sub_product_id", subProduct.ProductID),
|
||||
zap.String("sub_product_code", subProduct.ProductCode),
|
||||
zap.String("sub_product_name", subProduct.ProductName),
|
||||
zap.String("price", subProduct.Price),
|
||||
zap.Bool("is_purchased", subProduct.IsPurchased),
|
||||
)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"data": orderInfo,
|
||||
})
|
||||
}
|
||||
|
||||
// CreatePaymentOrder 创建支付订单
|
||||
// POST /api/v1/products/:id/component-report/create-order
|
||||
func (h *ComponentReportOrderHandler) CreatePaymentOrder(c *gin.Context) {
|
||||
h.logger.Info("开始创建支付订单")
|
||||
|
||||
productID := c.Param("id")
|
||||
h.logger.Info("获取产品ID", zap.String("product_id", productID))
|
||||
|
||||
if productID == "" {
|
||||
h.logger.Error("产品ID不能为空")
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"message": "产品ID不能为空",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
userID := c.GetString("user_id")
|
||||
h.logger.Info("获取用户ID", zap.String("user_id", userID))
|
||||
|
||||
if userID == "" {
|
||||
h.logger.Error("用户未登录")
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": 401,
|
||||
"message": "用户未登录",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req product.CreatePaymentOrderRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.logger.Error("请求参数错误", zap.Error(err))
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"message": "请求参数错误",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 记录请求参数
|
||||
h.logger.Info("支付订单请求参数",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("product_id", productID),
|
||||
zap.String("payment_type", req.PaymentType),
|
||||
zap.String("platform", req.Platform),
|
||||
zap.Strings("sub_product_codes", req.SubProductCodes),
|
||||
)
|
||||
|
||||
// 设置用户ID和产品ID
|
||||
req.UserID = userID
|
||||
req.ProductID = productID
|
||||
|
||||
// 如果未指定支付平台,根据User-Agent判断
|
||||
if req.Platform == "" {
|
||||
userAgent := c.GetHeader("User-Agent")
|
||||
req.Platform = h.detectPlatform(userAgent)
|
||||
h.logger.Info("根据User-Agent检测平台", zap.String("user_agent", userAgent), zap.String("detected_platform", req.Platform))
|
||||
}
|
||||
|
||||
response, err := h.service.CreatePaymentOrder(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
h.logger.Error("创建支付订单失败", zap.Error(err),
|
||||
zap.String("product_id", productID),
|
||||
zap.String("user_id", userID),
|
||||
zap.String("payment_type", req.PaymentType))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"message": "创建支付订单失败",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 记录创建订单成功响应
|
||||
h.logger.Info("创建支付订单成功",
|
||||
zap.String("order_id", response.OrderID),
|
||||
zap.String("order_no", response.OrderNo),
|
||||
zap.String("payment_type", response.PaymentType),
|
||||
zap.String("amount", response.Amount),
|
||||
zap.String("code_url", response.CodeURL),
|
||||
zap.String("pay_url", response.PayURL),
|
||||
)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"data": response,
|
||||
})
|
||||
}
|
||||
|
||||
// CheckPaymentStatus 检查支付状态
|
||||
// GET /api/v1/component-report/check-payment/:orderId
|
||||
func (h *ComponentReportOrderHandler) CheckPaymentStatus(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": 401,
|
||||
"message": "用户未登录",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
orderID := c.Param("orderId")
|
||||
if orderID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"message": "订单ID不能为空",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.service.CheckPaymentStatus(c.Request.Context(), orderID)
|
||||
if err != nil {
|
||||
h.logger.Error("检查支付状态失败", zap.Error(err), zap.String("order_id", orderID))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"message": "检查支付状态失败",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"data": response,
|
||||
})
|
||||
}
|
||||
|
||||
// DownloadFile 下载文件
|
||||
// GET /api/v1/component-report/download/:orderId
|
||||
func (h *ComponentReportOrderHandler) DownloadFile(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": 401,
|
||||
"message": "用户未登录",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
orderID := c.Param("orderId")
|
||||
if orderID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"message": "订单ID不能为空",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
filePath, err := h.service.DownloadFile(c.Request.Context(), orderID)
|
||||
if err != nil {
|
||||
h.logger.Error("下载文件失败", zap.Error(err), zap.String("order_id", orderID))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"message": "下载文件失败",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
c.Header("Content-Type", "application/zip")
|
||||
c.Header("Content-Disposition", "attachment; filename=component_report.zip")
|
||||
|
||||
// 发送文件
|
||||
c.File(filePath)
|
||||
}
|
||||
|
||||
// GetUserOrders 获取用户订单列表
|
||||
// GET /api/v1/component-report/orders
|
||||
func (h *ComponentReportOrderHandler) GetUserOrders(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": 401,
|
||||
"message": "用户未登录",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 解析分页参数
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 10
|
||||
}
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
orders, total, err := h.service.GetUserOrders(c.Request.Context(), userID, pageSize, offset)
|
||||
if err != nil {
|
||||
h.logger.Error("获取用户订单列表失败", zap.Error(err), zap.String("user_id", userID))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"message": "获取用户订单列表失败",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"data": gin.H{
|
||||
"list": orders,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// detectPlatform 根据 User-Agent 检测支付平台类型
|
||||
func (h *ComponentReportOrderHandler) detectPlatform(userAgent string) string {
|
||||
if userAgent == "" {
|
||||
return "h5" // 默认 H5
|
||||
}
|
||||
|
||||
ua := strings.ToLower(userAgent)
|
||||
|
||||
// 检测移动设备
|
||||
if strings.Contains(ua, "mobile") || strings.Contains(ua, "android") ||
|
||||
strings.Contains(ua, "iphone") || strings.Contains(ua, "ipad") {
|
||||
// 检测是否是支付宝或微信内置浏览器
|
||||
if strings.Contains(ua, "alipay") {
|
||||
return "app" // 支付宝 APP
|
||||
}
|
||||
if strings.Contains(ua, "micromessenger") {
|
||||
return "h5" // 微信 H5
|
||||
}
|
||||
return "h5" // 移动端默认 H5
|
||||
}
|
||||
|
||||
// PC 端
|
||||
return "pc"
|
||||
}
|
||||
@@ -1106,3 +1106,192 @@ func (h *FinanceHandler) DebugEventSystem(c *gin.Context) {
|
||||
|
||||
h.responseBuilder.Success(c, debugInfo, "事件系统调试信息")
|
||||
}
|
||||
|
||||
// GetUserPurchaseRecords 获取用户购买记录
|
||||
// @Summary 获取用户购买记录
|
||||
// @Description 获取当前用户的购买记录列表,支持分页和筛选
|
||||
// @Tags 财务管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param page_size query int false "每页数量" default(10)
|
||||
// @Param payment_type query string false "支付类型: alipay, wechat, free"
|
||||
// @Param pay_channel query string false "支付渠道: alipay, wechat"
|
||||
// @Param status query string false "订单状态: created, paid, failed, cancelled, refunded, closed"
|
||||
// @Param start_time query string false "开始时间 (格式: 2006-01-02 15:04:05)"
|
||||
// @Param end_time query string false "结束时间 (格式: 2006-01-02 15:04:05)"
|
||||
// @Param min_amount query string false "最小金额"
|
||||
// @Param max_amount query string false "最大金额"
|
||||
// @Param product_code query string false "产品编号"
|
||||
// @Success 200 {object} responses.PurchaseRecordListResponse "获取成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/v1/finance/purchase-records [get]
|
||||
func (h *FinanceHandler) GetUserPurchaseRecords(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
h.responseBuilder.Unauthorized(c, "用户未登录")
|
||||
return
|
||||
}
|
||||
|
||||
// 解析查询参数
|
||||
page := h.getIntQuery(c, "page", 1)
|
||||
pageSize := h.getIntQuery(c, "page_size", 10)
|
||||
|
||||
// 构建筛选条件
|
||||
filters := make(map[string]interface{})
|
||||
|
||||
// 时间范围筛选
|
||||
if startTime := c.Query("start_time"); startTime != "" {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", startTime); err == nil {
|
||||
filters["start_time"] = t
|
||||
}
|
||||
}
|
||||
if endTime := c.Query("end_time"); endTime != "" {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", endTime); err == nil {
|
||||
filters["end_time"] = t
|
||||
}
|
||||
}
|
||||
|
||||
// 支付类型筛选
|
||||
if paymentType := c.Query("payment_type"); paymentType != "" {
|
||||
filters["payment_type"] = paymentType
|
||||
}
|
||||
|
||||
// 支付渠道筛选
|
||||
if payChannel := c.Query("pay_channel"); payChannel != "" {
|
||||
filters["pay_channel"] = payChannel
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if status := c.Query("status"); status != "" {
|
||||
filters["status"] = status
|
||||
}
|
||||
|
||||
// 产品编号筛选
|
||||
if productCode := c.Query("product_code"); productCode != "" {
|
||||
filters["product_code"] = productCode
|
||||
}
|
||||
|
||||
// 金额范围筛选
|
||||
if minAmount := c.Query("min_amount"); minAmount != "" {
|
||||
filters["min_amount"] = minAmount
|
||||
}
|
||||
if maxAmount := c.Query("max_amount"); maxAmount != "" {
|
||||
filters["max_amount"] = maxAmount
|
||||
}
|
||||
|
||||
// 构建分页选项
|
||||
options := interfaces.ListOptions{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
Sort: "created_at",
|
||||
Order: "desc",
|
||||
}
|
||||
|
||||
result, err := h.appService.GetUserPurchaseRecords(c.Request.Context(), userID, filters, options)
|
||||
if err != nil {
|
||||
h.logger.Error("获取用户购买记录失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, "获取购买记录失败")
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, result, "获取用户购买记录成功")
|
||||
}
|
||||
|
||||
// GetAdminPurchaseRecords 获取管理端购买记录
|
||||
// @Summary 获取管理端购买记录
|
||||
// @Description 获取所有用户的购买记录列表,支持分页和筛选(管理员权限)
|
||||
// @Tags 管理员-财务管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param page_size query int false "每页数量" default(10)
|
||||
// @Param user_id query string false "用户ID"
|
||||
// @Param payment_type query string false "支付类型: alipay, wechat, free"
|
||||
// @Param pay_channel query string false "支付渠道: alipay, wechat"
|
||||
// @Param status query string false "订单状态: created, paid, failed, cancelled, refunded, closed"
|
||||
// @Param start_time query string false "开始时间 (格式: 2006-01-02 15:04:05)"
|
||||
// @Param end_time query string false "结束时间 (格式: 2006-01-02 15:04:05)"
|
||||
// @Param min_amount query string false "最小金额"
|
||||
// @Param max_amount query string false "最大金额"
|
||||
// @Param product_code query string false "产品编号"
|
||||
// @Success 200 {object} responses.PurchaseRecordListResponse "获取成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||
// @Failure 403 {object} map[string]interface{} "权限不足"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/v1/admin/finance/purchase-records [get]
|
||||
func (h *FinanceHandler) GetAdminPurchaseRecords(c *gin.Context) {
|
||||
// 解析查询参数
|
||||
page := h.getIntQuery(c, "page", 1)
|
||||
pageSize := h.getIntQuery(c, "page_size", 10)
|
||||
|
||||
// 构建筛选条件
|
||||
filters := make(map[string]interface{})
|
||||
|
||||
// 用户ID筛选
|
||||
if userID := c.Query("user_id"); userID != "" {
|
||||
filters["user_id"] = userID
|
||||
}
|
||||
|
||||
// 时间范围筛选
|
||||
if startTime := c.Query("start_time"); startTime != "" {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", startTime); err == nil {
|
||||
filters["start_time"] = t
|
||||
}
|
||||
}
|
||||
if endTime := c.Query("end_time"); endTime != "" {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", endTime); err == nil {
|
||||
filters["end_time"] = t
|
||||
}
|
||||
}
|
||||
|
||||
// 支付类型筛选
|
||||
if paymentType := c.Query("payment_type"); paymentType != "" {
|
||||
filters["payment_type"] = paymentType
|
||||
}
|
||||
|
||||
// 支付渠道筛选
|
||||
if payChannel := c.Query("pay_channel"); payChannel != "" {
|
||||
filters["pay_channel"] = payChannel
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if status := c.Query("status"); status != "" {
|
||||
filters["status"] = status
|
||||
}
|
||||
|
||||
// 产品编号筛选
|
||||
if productCode := c.Query("product_code"); productCode != "" {
|
||||
filters["product_code"] = productCode
|
||||
}
|
||||
|
||||
// 金额范围筛选
|
||||
if minAmount := c.Query("min_amount"); minAmount != "" {
|
||||
filters["min_amount"] = minAmount
|
||||
}
|
||||
if maxAmount := c.Query("max_amount"); maxAmount != "" {
|
||||
filters["max_amount"] = maxAmount
|
||||
}
|
||||
|
||||
// 构建分页选项
|
||||
options := interfaces.ListOptions{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
Sort: "created_at",
|
||||
Order: "desc",
|
||||
}
|
||||
|
||||
result, err := h.appService.GetAdminPurchaseRecords(c.Request.Context(), filters, options)
|
||||
if err != nil {
|
||||
h.logger.Error("获取管理端购买记录失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, "获取购买记录失败")
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, result, "获取管理端购买记录成功")
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -13,6 +11,9 @@ import (
|
||||
"tyapi-server/internal/application/product/dto/queries"
|
||||
"tyapi-server/internal/application/product/dto/responses"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// ProductAdminHandler 产品管理员HTTP处理器
|
||||
@@ -296,7 +297,6 @@ func (h *ProductAdminHandler) ListProducts(c *gin.Context) {
|
||||
// 解析查询参数
|
||||
page := h.getIntQuery(c, "page", 1)
|
||||
pageSize := h.getIntQuery(c, "page_size", 10)
|
||||
|
||||
// 构建筛选条件
|
||||
filters := make(map[string]interface{})
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"tyapi-server/internal/infrastructure/http/handlers"
|
||||
sharedhttp "tyapi-server/internal/shared/http"
|
||||
"tyapi-server/internal/shared/middleware"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// ComponentReportOrderRoutes 组件报告订单路由
|
||||
type ComponentReportOrderRoutes struct {
|
||||
componentReportOrderHandler *handlers.ComponentReportOrderHandler
|
||||
auth *middleware.JWTAuthMiddleware
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewComponentReportOrderRoutes 创建组件报告订单路由
|
||||
func NewComponentReportOrderRoutes(
|
||||
componentReportOrderHandler *handlers.ComponentReportOrderHandler,
|
||||
auth *middleware.JWTAuthMiddleware,
|
||||
logger *zap.Logger,
|
||||
) *ComponentReportOrderRoutes {
|
||||
return &ComponentReportOrderRoutes{
|
||||
componentReportOrderHandler: componentReportOrderHandler,
|
||||
auth: auth,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Register 注册组件报告订单相关路由
|
||||
func (r *ComponentReportOrderRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
engine := router.GetEngine()
|
||||
|
||||
// 产品组件报告相关接口 - 需要认证
|
||||
componentReportGroup := engine.Group("/api/v1/products/:id/component-report", r.auth.Handle())
|
||||
{
|
||||
// 检查下载可用性
|
||||
componentReportGroup.GET("/check", r.componentReportOrderHandler.CheckDownloadAvailability)
|
||||
// 获取下载信息
|
||||
componentReportGroup.GET("/info", r.componentReportOrderHandler.GetDownloadInfo)
|
||||
// 创建支付订单
|
||||
componentReportGroup.POST("/create-order", r.componentReportOrderHandler.CreatePaymentOrder)
|
||||
}
|
||||
|
||||
// 组件报告订单相关接口 - 需要认证
|
||||
componentReportOrder := engine.Group("/api/v1/component-report", r.auth.Handle())
|
||||
{
|
||||
// 检查支付状态
|
||||
componentReportOrder.GET("/check-payment/:orderId", r.componentReportOrderHandler.CheckPaymentStatus)
|
||||
// 下载文件
|
||||
componentReportOrder.GET("/download/:orderId", r.componentReportOrderHandler.DownloadFile)
|
||||
// 获取用户订单列表
|
||||
componentReportOrder.GET("/orders", r.componentReportOrderHandler.GetUserOrders)
|
||||
}
|
||||
|
||||
r.logger.Info("组件报告订单路由注册完成")
|
||||
}
|
||||
@@ -69,6 +69,7 @@ func (r *FinanceRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
walletGroup.GET("/recharge-records", r.financeHandler.GetUserRechargeRecords) // 用户充值记录分页
|
||||
walletGroup.GET("/alipay-order-status", r.financeHandler.GetAlipayOrderStatus) // 获取支付宝订单状态
|
||||
walletGroup.GET("/wechat-order-status", r.financeHandler.GetWechatOrderStatus) // 获取微信订单状态
|
||||
financeGroup.GET("/purchase-records", r.financeHandler.GetUserPurchaseRecords) // 用户购买记录分页
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +92,7 @@ func (r *FinanceRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
adminFinanceGroup.POST("/transfer-recharge", r.financeHandler.TransferRecharge) // 对公转账充值
|
||||
adminFinanceGroup.POST("/gift-recharge", r.financeHandler.GiftRecharge) // 赠送充值
|
||||
adminFinanceGroup.GET("/recharge-records", r.financeHandler.GetAdminRechargeRecords) // 管理员充值记录分页
|
||||
adminFinanceGroup.GET("/purchase-records", r.financeHandler.GetAdminPurchaseRecords) // 管理员购买记录分页
|
||||
}
|
||||
|
||||
// 管理员发票相关路由组
|
||||
|
||||
@@ -81,8 +81,6 @@ func (r *ProductAdminRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
subscriptions.POST("/batch-update-prices", r.handler.BatchUpdateSubscriptionPrices)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 消费记录管理
|
||||
walletTransactions := adminGroup.Group("/wallet-transactions")
|
||||
{
|
||||
|
||||
@@ -70,14 +70,7 @@ func (r *ProductRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
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)
|
||||
}
|
||||
// 产品组件报告相关接口 - 已迁移到 ComponentReportOrderRoutes
|
||||
|
||||
// 分类 - 公开接口
|
||||
categories := engine.Group("/api/v1/categories")
|
||||
|
||||
@@ -193,6 +193,13 @@ componentReportHandler := component_report.NewComponentReportHandler(
|
||||
productRepo,
|
||||
docRepo,
|
||||
apiConfigRepo,
|
||||
componentReportRepo,
|
||||
purchaseOrderRepo,
|
||||
rechargeRecordRepo,
|
||||
alipayOrderRepo,
|
||||
wechatOrderRepo,
|
||||
aliPayService,
|
||||
wechatPayService,
|
||||
logger,
|
||||
)
|
||||
|
||||
|
||||
137
internal/shared/component_report/cache_manager.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package component_report
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// CacheManager 缓存管理器
|
||||
type CacheManager struct {
|
||||
cacheDir string
|
||||
ttl time.Duration
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewCacheManager 创建缓存管理器
|
||||
func NewCacheManager(cacheDir string, ttl time.Duration, logger *zap.Logger) *CacheManager {
|
||||
return &CacheManager{
|
||||
cacheDir: cacheDir,
|
||||
ttl: ttl,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CleanExpiredCache 清理过期缓存
|
||||
func (cm *CacheManager) CleanExpiredCache() error {
|
||||
// 确保缓存目录存在
|
||||
if _, err := os.Stat(cm.cacheDir); os.IsNotExist(err) {
|
||||
return nil // 目录不存在,无需清理
|
||||
}
|
||||
|
||||
// 遍历缓存目录
|
||||
err := filepath.Walk(cm.cacheDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 跳过目录
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查文件是否过期
|
||||
if time.Since(info.ModTime()) > cm.ttl {
|
||||
// cm.logger.Debug("删除过期缓存文件",
|
||||
// zap.String("path", path),
|
||||
// zap.Time("mod_time", info.ModTime()),
|
||||
// zap.Duration("age", time.Since(info.ModTime())))
|
||||
|
||||
if err := os.Remove(path); err != nil {
|
||||
cm.logger.Error("删除过期缓存文件失败",
|
||||
zap.Error(err),
|
||||
zap.String("path", path))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("清理过期缓存失败: %w", err)
|
||||
}
|
||||
|
||||
// cm.logger.Info("缓存清理完成", zap.String("cache_dir", cm.cacheDir))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCacheSize 获取缓存总大小
|
||||
func (cm *CacheManager) GetCacheSize() (int64, error) {
|
||||
var totalSize int64
|
||||
|
||||
err := filepath.Walk(cm.cacheDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
totalSize += info.Size()
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("计算缓存大小失败: %w", err)
|
||||
}
|
||||
|
||||
return totalSize, nil
|
||||
}
|
||||
|
||||
// GetCacheCount 获取缓存文件数量
|
||||
func (cm *CacheManager) GetCacheCount() (int, error) {
|
||||
var count int
|
||||
|
||||
err := filepath.Walk(cm.cacheDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
count++
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("统计缓存文件数量失败: %w", err)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// ClearAllCache 清理所有缓存
|
||||
func (cm *CacheManager) ClearAllCache() error {
|
||||
// 确保缓存目录存在
|
||||
if _, err := os.Stat(cm.cacheDir); os.IsNotExist(err) {
|
||||
return nil // 目录不存在,无需清理
|
||||
}
|
||||
|
||||
err := os.RemoveAll(cm.cacheDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("清理所有缓存失败: %w", err)
|
||||
}
|
||||
|
||||
// 重新创建目录
|
||||
if err := os.MkdirAll(cm.cacheDir, 0755); err != nil {
|
||||
return fmt.Errorf("重新创建缓存目录失败: %w", err)
|
||||
}
|
||||
|
||||
// cm.logger.Info("所有缓存已清理", zap.String("cache_dir", cm.cacheDir))
|
||||
return nil
|
||||
}
|
||||
102
internal/shared/component_report/check_payment_status_fix.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package component_report
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
finance_entities "tyapi-server/internal/domains/finance/entities"
|
||||
)
|
||||
|
||||
// CheckPaymentStatusFixed 修复版检查支付状态方法
|
||||
func (h *ComponentReportHandler) CheckPaymentStatusFixed(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": 401,
|
||||
"message": "用户未登录",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
orderID := c.Param("orderId")
|
||||
if orderID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"message": "订单ID不能为空",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 根据订单ID查询下载记录
|
||||
download, err := h.componentReportRepo.GetDownloadByID(c.Request.Context(), orderID)
|
||||
if err != nil {
|
||||
h.logger.Error("查询下载记录失败", zap.Error(err), zap.String("order_id", orderID))
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"code": 404,
|
||||
"message": "订单不存在",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证订单是否属于当前用户
|
||||
if download.UserID != userID {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"code": 403,
|
||||
"message": "无权访问此订单",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 使用购买订单状态来判断支付状态
|
||||
var paymentStatus string
|
||||
var canDownload bool
|
||||
|
||||
// 优先使用OrderID查询购买订单状态
|
||||
if download.OrderID != nil {
|
||||
// 查询购买订单状态
|
||||
purchaseOrder, err := h.purchaseOrderRepo.GetByID(c.Request.Context(), *download.OrderID)
|
||||
if err != nil {
|
||||
h.logger.Error("查询购买订单失败", zap.Error(err), zap.String("order_id", *download.OrderID))
|
||||
paymentStatus = "unknown"
|
||||
} else {
|
||||
// 根据购买订单状态设置支付状态
|
||||
switch purchaseOrder.Status {
|
||||
case finance_entities.PurchaseOrderStatusPaid:
|
||||
paymentStatus = "success"
|
||||
canDownload = true
|
||||
case finance_entities.PurchaseOrderStatusCreated:
|
||||
paymentStatus = "pending"
|
||||
canDownload = false
|
||||
case finance_entities.PurchaseOrderStatusCancelled:
|
||||
paymentStatus = "cancelled"
|
||||
canDownload = false
|
||||
case finance_entities.PurchaseOrderStatusFailed:
|
||||
paymentStatus = "failed"
|
||||
canDownload = false
|
||||
default:
|
||||
paymentStatus = "unknown"
|
||||
canDownload = false
|
||||
}
|
||||
}
|
||||
} else if download.OrderNumber != nil {
|
||||
// 兼容旧的支付订单逻辑
|
||||
paymentStatus = "success" // 简化处理,有支付订单号就认为已支付
|
||||
canDownload = true
|
||||
} else {
|
||||
paymentStatus = "pending"
|
||||
canDownload = false
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
if download.IsExpired() {
|
||||
canDownload = false
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, CheckPaymentStatusResponse{
|
||||
OrderID: download.ID,
|
||||
PaymentStatus: paymentStatus,
|
||||
CanDownload: canDownload,
|
||||
})
|
||||
}
|
||||
@@ -2,12 +2,15 @@ package component_report
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
@@ -21,6 +24,10 @@ type ExampleJSONGenerator struct {
|
||||
docRepo repositories.ProductDocumentationRepository
|
||||
apiConfigRepo repositories.ProductApiConfigRepository
|
||||
logger *zap.Logger
|
||||
// 缓存配置
|
||||
CacheEnabled bool
|
||||
CacheDir string
|
||||
CacheTTL time.Duration
|
||||
}
|
||||
|
||||
// NewExampleJSONGenerator 创建示例JSON生成器
|
||||
@@ -35,6 +42,30 @@ func NewExampleJSONGenerator(
|
||||
docRepo: docRepo,
|
||||
apiConfigRepo: apiConfigRepo,
|
||||
logger: logger,
|
||||
CacheEnabled: true,
|
||||
CacheDir: "storage/component-reports/cache",
|
||||
CacheTTL: 24 * time.Hour, // 默认缓存24小时
|
||||
}
|
||||
}
|
||||
|
||||
// NewExampleJSONGeneratorWithCache 创建带有自定义缓存配置的示例JSON生成器
|
||||
func NewExampleJSONGeneratorWithCache(
|
||||
productRepo repositories.ProductRepository,
|
||||
docRepo repositories.ProductDocumentationRepository,
|
||||
apiConfigRepo repositories.ProductApiConfigRepository,
|
||||
logger *zap.Logger,
|
||||
cacheEnabled bool,
|
||||
cacheDir string,
|
||||
cacheTTL time.Duration,
|
||||
) *ExampleJSONGenerator {
|
||||
return &ExampleJSONGenerator{
|
||||
productRepo: productRepo,
|
||||
docRepo: docRepo,
|
||||
apiConfigRepo: apiConfigRepo,
|
||||
logger: logger,
|
||||
CacheEnabled: cacheEnabled,
|
||||
CacheDir: cacheDir,
|
||||
CacheTTL: cacheTTL,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +85,20 @@ type ExampleJSONItem struct {
|
||||
// productID: 产品ID(可以是组合包或单品)
|
||||
// subProductCodes: 子产品编号列表(如果为空,则处理所有子产品)
|
||||
func (g *ExampleJSONGenerator) GenerateExampleJSON(ctx context.Context, productID string, subProductCodes []string) ([]byte, error) {
|
||||
// 生成缓存键
|
||||
cacheKey := g.generateCacheKey(productID, subProductCodes)
|
||||
|
||||
// 检查缓存
|
||||
if g.CacheEnabled {
|
||||
cachedData, err := g.getCachedData(cacheKey)
|
||||
if err == nil && cachedData != nil {
|
||||
// g.logger.Debug("使用缓存的example.json数据",
|
||||
// zap.String("product_id", productID),
|
||||
// zap.String("cache_key", cacheKey))
|
||||
return cachedData, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 获取产品信息
|
||||
product, err := g.productRepo.GetByID(ctx, productID)
|
||||
if err != nil {
|
||||
@@ -157,12 +202,21 @@ func (g *ExampleJSONGenerator) GenerateExampleJSON(ctx context.Context, productI
|
||||
return nil, fmt.Errorf("序列化example.json失败: %w", err)
|
||||
}
|
||||
|
||||
// 缓存数据
|
||||
if g.CacheEnabled {
|
||||
if err := g.cacheData(cacheKey, jsonData); err != nil {
|
||||
g.logger.Warn("缓存example.json数据失败", zap.Error(err))
|
||||
} else {
|
||||
g.logger.Debug("example.json数据已缓存", zap.String("cache_key", cacheKey))
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
// MatchSubProductCodeToPath 根据子产品编码匹配 UI 组件路径,返回路径和类型(folder/file)
|
||||
func (g *ExampleJSONGenerator) MatchSubProductCodeToPath(ctx context.Context, subProductCode string) (string, string, error) {
|
||||
basePath := filepath.Join("resources", "Pure_Component", "src", "ui")
|
||||
|
||||
entries, err := os.ReadDir(basePath)
|
||||
if err != nil {
|
||||
@@ -172,18 +226,8 @@ func (g *ExampleJSONGenerator) MatchProductCodeToPath(ctx context.Context, produ
|
||||
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)) {
|
||||
// 使用改进的相似性匹配算法
|
||||
if isSimilarCode(subProductCode, name) {
|
||||
path := filepath.Join(basePath, name)
|
||||
fileType := "folder"
|
||||
if !entry.IsDir() {
|
||||
@@ -193,7 +237,7 @@ func (g *ExampleJSONGenerator) MatchProductCodeToPath(ctx context.Context, produ
|
||||
}
|
||||
}
|
||||
|
||||
return "", "", fmt.Errorf("未找到匹配的组件文件: %s", productCode)
|
||||
return "", "", fmt.Errorf("未找到匹配的组件文件: %s", subProductCode)
|
||||
}
|
||||
|
||||
// extractCoreCode 提取文件名中的核心编码部分
|
||||
@@ -206,6 +250,44 @@ func extractCoreCode(name string) string {
|
||||
return name
|
||||
}
|
||||
|
||||
// extractMainCode 从子产品编码或文件夹名称中提取主要编码部分
|
||||
// 处理可能的格式差异,如前缀、后缀等
|
||||
func extractMainCode(code string) string {
|
||||
// 移除常见的前缀,如 C
|
||||
if len(code) > 0 && code[0] == 'C' {
|
||||
return code[1:]
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
// isSimilarCode 判断两个编码是否相似,考虑多种可能的格式差异
|
||||
func isSimilarCode(code1, code2 string) bool {
|
||||
// 直接相等
|
||||
if code1 == code2 {
|
||||
return true
|
||||
}
|
||||
|
||||
// 移除常见前缀后比较
|
||||
mainCode1 := extractMainCode(code1)
|
||||
mainCode2 := extractMainCode(code2)
|
||||
if mainCode1 == mainCode2 || mainCode1 == code2 || code1 == mainCode2 {
|
||||
return true
|
||||
}
|
||||
|
||||
// 包含关系
|
||||
if strings.Contains(code1, code2) || strings.Contains(code2, code1) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 移除前缀后的包含关系
|
||||
if strings.Contains(mainCode1, code2) || strings.Contains(code2, mainCode1) ||
|
||||
strings.Contains(code1, mainCode2) || strings.Contains(mainCode2, code1) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// extractResponseExample 提取产品响应示例数据(优先级:文档 > API配置 > 默认值)
|
||||
func (g *ExampleJSONGenerator) extractResponseExample(ctx context.Context, product *entities.Product) interface{} {
|
||||
var responseData interface{}
|
||||
@@ -216,20 +298,20 @@ func (g *ExampleJSONGenerator) extractResponseExample(ctx context.Context, produ
|
||||
// 尝试直接解析为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),
|
||||
)
|
||||
// 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),
|
||||
)
|
||||
// g.logger.Debug("从Markdown代码块中提取响应示例成功",
|
||||
// zap.String("product_id", product.ID),
|
||||
// zap.String("product_code", product.Code),
|
||||
// )
|
||||
return extractedData
|
||||
}
|
||||
}
|
||||
@@ -240,10 +322,10 @@ func (g *ExampleJSONGenerator) extractResponseExample(ctx context.Context, produ
|
||||
// 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),
|
||||
)
|
||||
// g.logger.Debug("从产品API配置中提取响应示例成功",
|
||||
// zap.String("product_id", product.ID),
|
||||
// zap.String("product_code", product.Code),
|
||||
// )
|
||||
return responseData
|
||||
}
|
||||
}
|
||||
@@ -284,3 +366,57 @@ func extractJSONFromMarkdown(markdown string) interface{} {
|
||||
// 如果提取失败,返回 nil(由调用者决定默认值)
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateCacheKey 生成缓存键
|
||||
func (g *ExampleJSONGenerator) generateCacheKey(productID string, subProductCodes []string) string {
|
||||
// 使用产品ID和子产品编码列表生成MD5哈希
|
||||
data := productID
|
||||
for _, code := range subProductCodes {
|
||||
data += "|" + code
|
||||
}
|
||||
|
||||
hash := md5.Sum([]byte(data))
|
||||
return hex.EncodeToString(hash[:]) + ".json"
|
||||
}
|
||||
|
||||
// getCachedData 获取缓存数据
|
||||
func (g *ExampleJSONGenerator) getCachedData(cacheKey string) ([]byte, error) {
|
||||
// 确保缓存目录存在
|
||||
if err := os.MkdirAll(g.CacheDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("创建缓存目录失败: %w", err)
|
||||
}
|
||||
|
||||
cacheFilePath := filepath.Join(g.CacheDir, cacheKey)
|
||||
|
||||
// 检查文件是否存在
|
||||
fileInfo, err := os.Stat(cacheFilePath)
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil // 文件不存在,但不是错误
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查文件是否过期
|
||||
if time.Since(fileInfo.ModTime()) > g.CacheTTL {
|
||||
// 文件过期,删除
|
||||
os.Remove(cacheFilePath)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
return os.ReadFile(cacheFilePath)
|
||||
}
|
||||
|
||||
// cacheData 缓存数据
|
||||
func (g *ExampleJSONGenerator) cacheData(cacheKey string, data []byte) error {
|
||||
// 确保缓存目录存在
|
||||
if err := os.MkdirAll(g.CacheDir, 0755); err != nil {
|
||||
return fmt.Errorf("创建缓存目录失败: %w", err)
|
||||
}
|
||||
|
||||
cacheFilePath := filepath.Join(g.CacheDir, cacheKey)
|
||||
|
||||
// 写入文件
|
||||
return os.WriteFile(cacheFilePath, data, 0644)
|
||||
}
|
||||
|
||||
172
internal/shared/component_report/handler_fixed.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package component_report
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
finance_entities "tyapi-server/internal/domains/finance/entities"
|
||||
financeRepositories "tyapi-server/internal/domains/finance/repositories"
|
||||
"tyapi-server/internal/domains/product/repositories"
|
||||
"tyapi-server/internal/shared/payment"
|
||||
)
|
||||
|
||||
// ComponentReportHandler 组件报告处理器
|
||||
type ComponentReportHandlerFixed struct {
|
||||
exampleJSONGenerator *ExampleJSONGenerator
|
||||
zipGenerator *ZipGenerator
|
||||
productRepo repositories.ProductRepository
|
||||
componentReportRepo repositories.ComponentReportRepository
|
||||
purchaseOrderRepo financeRepositories.PurchaseOrderRepository
|
||||
rechargeRecordRepo interface {
|
||||
Create(ctx context.Context, record finance_entities.RechargeRecord) (finance_entities.RechargeRecord, error)
|
||||
}
|
||||
alipayOrderRepo interface {
|
||||
Create(ctx context.Context, order finance_entities.AlipayOrder) (finance_entities.AlipayOrder, error)
|
||||
GetByOutTradeNo(ctx context.Context, outTradeNo string) (*finance_entities.AlipayOrder, error)
|
||||
Update(ctx context.Context, order finance_entities.AlipayOrder) error
|
||||
}
|
||||
wechatOrderRepo interface {
|
||||
Create(ctx context.Context, order finance_entities.WechatOrder) (finance_entities.WechatOrder, error)
|
||||
GetByOutTradeNo(ctx context.Context, outTradeNo string) (*finance_entities.WechatOrder, error)
|
||||
Update(ctx context.Context, order finance_entities.WechatOrder) error
|
||||
}
|
||||
aliPayService *payment.AliPayService
|
||||
wechatPayService *payment.WechatPayService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewComponentReportHandlerFixed 创建组件报告处理器(修复版)
|
||||
func NewComponentReportHandlerFixed(
|
||||
productRepo repositories.ProductRepository,
|
||||
docRepo repositories.ProductDocumentationRepository,
|
||||
apiConfigRepo repositories.ProductApiConfigRepository,
|
||||
componentReportRepo repositories.ComponentReportRepository,
|
||||
purchaseOrderRepo financeRepositories.PurchaseOrderRepository,
|
||||
rechargeRecordRepo interface {
|
||||
Create(ctx context.Context, record finance_entities.RechargeRecord) (finance_entities.RechargeRecord, error)
|
||||
},
|
||||
alipayOrderRepo interface {
|
||||
Create(ctx context.Context, order finance_entities.AlipayOrder) (finance_entities.AlipayOrder, error)
|
||||
GetByOutTradeNo(ctx context.Context, outTradeNo string) (*finance_entities.AlipayOrder, error)
|
||||
Update(ctx context.Context, order finance_entities.AlipayOrder) error
|
||||
},
|
||||
wechatOrderRepo interface {
|
||||
Create(ctx context.Context, order finance_entities.WechatOrder) (finance_entities.WechatOrder, error)
|
||||
GetByOutTradeNo(ctx context.Context, outTradeNo string) (*finance_entities.WechatOrder, error)
|
||||
Update(ctx context.Context, order finance_entities.WechatOrder) error
|
||||
},
|
||||
aliPayService *payment.AliPayService,
|
||||
wechatPayService *payment.WechatPayService,
|
||||
logger *zap.Logger,
|
||||
) *ComponentReportHandlerFixed {
|
||||
exampleJSONGenerator := NewExampleJSONGenerator(productRepo, docRepo, apiConfigRepo, logger)
|
||||
zipGenerator := NewZipGenerator(logger)
|
||||
|
||||
return &ComponentReportHandlerFixed{
|
||||
exampleJSONGenerator: exampleJSONGenerator,
|
||||
zipGenerator: zipGenerator,
|
||||
productRepo: productRepo,
|
||||
componentReportRepo: componentReportRepo,
|
||||
purchaseOrderRepo: purchaseOrderRepo,
|
||||
rechargeRecordRepo: rechargeRecordRepo,
|
||||
alipayOrderRepo: alipayOrderRepo,
|
||||
wechatOrderRepo: wechatOrderRepo,
|
||||
aliPayService: aliPayService,
|
||||
wechatPayService: wechatPayService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CheckPaymentStatusFixed 检查支付状态(修复版)
|
||||
func (h *ComponentReportHandlerFixed) CheckPaymentStatusFixed(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": 401,
|
||||
"message": "用户未登录",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
orderID := c.Param("orderId")
|
||||
if orderID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"message": "订单ID不能为空",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 根据订单ID查询下载记录
|
||||
download, err := h.componentReportRepo.GetDownloadByID(c.Request.Context(), orderID)
|
||||
if err != nil {
|
||||
h.logger.Error("查询下载记录失败", zap.Error(err), zap.String("order_id", orderID))
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"code": 404,
|
||||
"message": "订单不存在",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证订单是否属于当前用户
|
||||
if download.UserID != userID {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"code": 403,
|
||||
"message": "无权访问此订单",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 使用购买订单状态来判断支付状态
|
||||
var paymentStatus string
|
||||
var canDownload bool
|
||||
|
||||
if download.OrderID != nil {
|
||||
// 查询购买订单状态
|
||||
purchaseOrder, err := h.purchaseOrderRepo.GetByID(c.Request.Context(), *download.OrderID)
|
||||
if err != nil {
|
||||
h.logger.Error("查询购买订单失败", zap.Error(err), zap.String("OrderID", *download.OrderID))
|
||||
paymentStatus = "unknown"
|
||||
} else {
|
||||
// 根据购买订单状态设置支付状态
|
||||
switch purchaseOrder.Status {
|
||||
case finance_entities.PurchaseOrderStatusPaid:
|
||||
paymentStatus = "success"
|
||||
canDownload = true
|
||||
case finance_entities.PurchaseOrderStatusCreated:
|
||||
paymentStatus = "pending"
|
||||
canDownload = false
|
||||
case finance_entities.PurchaseOrderStatusCancelled:
|
||||
paymentStatus = "cancelled"
|
||||
canDownload = false
|
||||
case finance_entities.PurchaseOrderStatusFailed:
|
||||
paymentStatus = "failed"
|
||||
canDownload = false
|
||||
default:
|
||||
paymentStatus = "unknown"
|
||||
canDownload = false
|
||||
}
|
||||
}
|
||||
} else if download.OrderNumber != nil {
|
||||
// 兼容旧的支付订单逻辑
|
||||
paymentStatus = "success" // 简化处理,有支付订单号就认为已支付
|
||||
canDownload = true
|
||||
} else {
|
||||
paymentStatus = "pending"
|
||||
canDownload = false
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
if download.IsExpired() {
|
||||
canDownload = false
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, CheckPaymentStatusResponse{
|
||||
OrderID: download.ID,
|
||||
PaymentStatus: paymentStatus,
|
||||
CanDownload: canDownload,
|
||||
})
|
||||
}
|
||||
@@ -3,11 +3,14 @@ package component_report
|
||||
import (
|
||||
"archive/zip"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -15,18 +18,35 @@ import (
|
||||
// ZipGenerator ZIP文件生成器
|
||||
type ZipGenerator struct {
|
||||
logger *zap.Logger
|
||||
// 缓存配置
|
||||
CacheEnabled bool
|
||||
CacheDir string
|
||||
CacheTTL time.Duration
|
||||
}
|
||||
|
||||
// NewZipGenerator 创建ZIP文件生成器
|
||||
func NewZipGenerator(logger *zap.Logger) *ZipGenerator {
|
||||
return &ZipGenerator{
|
||||
logger: logger,
|
||||
logger: logger,
|
||||
CacheEnabled: true,
|
||||
CacheDir: "storage/component-reports/cache",
|
||||
CacheTTL: 24 * time.Hour, // 默认缓存24小时
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateZipFile 生成ZIP文件,包含 example.json 和匹配的组件文件
|
||||
// NewZipGeneratorWithCache 创建带有自定义缓存配置的ZIP文件生成器
|
||||
func NewZipGeneratorWithCache(logger *zap.Logger, cacheEnabled bool, cacheDir string, cacheTTL time.Duration) *ZipGenerator {
|
||||
return &ZipGenerator{
|
||||
logger: logger,
|
||||
CacheEnabled: cacheEnabled,
|
||||
CacheDir: cacheDir,
|
||||
CacheTTL: cacheTTL,
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateZipFile 生成ZIP文件,包含 example.json 和根据子产品编码匹配的UI组件文件
|
||||
// productID: 产品ID
|
||||
// subProductCodes: 子产品编号列表(如果为空,则处理所有子产品)
|
||||
// subProductCodes: 子产品编码列表(用于过滤和下载匹配的UI组件)
|
||||
// exampleJSONGenerator: 示例JSON生成器
|
||||
// outputPath: 输出ZIP文件路径(如果为空,则使用默认路径)
|
||||
func (g *ZipGenerator) GenerateZipFile(
|
||||
@@ -36,6 +56,29 @@ func (g *ZipGenerator) GenerateZipFile(
|
||||
exampleJSONGenerator *ExampleJSONGenerator,
|
||||
outputPath string,
|
||||
) (string, error) {
|
||||
// 生成缓存键
|
||||
cacheKey := g.generateCacheKey(productID, subProductCodes)
|
||||
|
||||
// 检查缓存
|
||||
if g.CacheEnabled {
|
||||
cachedPath, err := g.getCachedFile(cacheKey)
|
||||
if err == nil && cachedPath != "" {
|
||||
// g.logger.Debug("使用缓存的ZIP文件",
|
||||
// zap.String("product_id", productID),
|
||||
// zap.String("cache_path", cachedPath))
|
||||
|
||||
// 如果指定了输出路径,复制缓存文件到目标位置
|
||||
if outputPath != "" && outputPath != cachedPath {
|
||||
if err := g.copyFile(cachedPath, outputPath); err != nil {
|
||||
g.logger.Error("复制缓存文件失败", zap.Error(err))
|
||||
} else {
|
||||
return outputPath, nil
|
||||
}
|
||||
}
|
||||
return cachedPath, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 生成 example.json 内容
|
||||
exampleJSON, err := exampleJSONGenerator.GenerateExampleJSON(ctx, productID, subProductCodes)
|
||||
if err != nil {
|
||||
@@ -62,8 +105,8 @@ func (g *ZipGenerator) GenerateZipFile(
|
||||
zipWriter := zip.NewWriter(zipFile)
|
||||
defer zipWriter.Close()
|
||||
|
||||
// 4. 添加 example.json 到 public 目录
|
||||
exampleWriter, err := zipWriter.Create("public/example.json")
|
||||
// 4. 将生成的内容添加到 Pure_Component/public 目录下的 example.json
|
||||
exampleWriter, err := zipWriter.Create("Pure_Component/public/example.json")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建example.json文件失败: %w", err)
|
||||
}
|
||||
@@ -73,14 +116,14 @@ func (g *ZipGenerator) GenerateZipFile(
|
||||
return "", fmt.Errorf("写入example.json失败: %w", err)
|
||||
}
|
||||
|
||||
// 5. 添加整个 src 目录,但过滤 ui 目录下的文件
|
||||
srcBasePath := filepath.Join("resources", "Pure Component", "src")
|
||||
uiBasePath := filepath.Join(srcBasePath, "ui")
|
||||
// 5. 添加整个 Pure_Component 目录,但只包含子产品编码匹配的UI组件文件
|
||||
srcBasePath := filepath.Join("resources", "Pure_Component")
|
||||
uiBasePath := filepath.Join(srcBasePath, "src", "ui")
|
||||
|
||||
// 收集所有匹配的组件名称(文件夹名或文件名)
|
||||
// 根据子产品编码收集所有匹配的组件名称(文件夹名或文件名)
|
||||
matchedNames := make(map[string]bool)
|
||||
for _, productCode := range subProductCodes {
|
||||
path, _, err := exampleJSONGenerator.MatchProductCodeToPath(ctx, productCode)
|
||||
for _, subProductCode := range subProductCodes {
|
||||
path, _, err := exampleJSONGenerator.MatchSubProductCodeToPath(ctx, subProductCode)
|
||||
if err == nil && path != "" {
|
||||
// 获取组件名称(文件夹名或文件名)
|
||||
componentName := filepath.Base(path)
|
||||
@@ -88,20 +131,20 @@ func (g *ZipGenerator) GenerateZipFile(
|
||||
}
|
||||
}
|
||||
|
||||
// 遍历整个 src 目录
|
||||
// 遍历整个 Pure_Component 目录
|
||||
err = filepath.Walk(srcBasePath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 计算相对于 src 的路径
|
||||
// 计算相对于 Pure_Component 的路径
|
||||
relPath, err := filepath.Rel(srcBasePath, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 转换为ZIP路径格式
|
||||
zipPath := filepath.ToSlash(filepath.Join("src", relPath))
|
||||
// 转换为ZIP路径格式,保持在Pure_Component目录下
|
||||
zipPath := filepath.ToSlash(filepath.Join("Pure_Component", relPath))
|
||||
|
||||
// 检查是否在 ui 目录下
|
||||
uiRelPath, err := filepath.Rel(uiBasePath, path)
|
||||
@@ -120,26 +163,19 @@ func (g *ZipGenerator) GenerateZipFile(
|
||||
// 获取文件/文件夹名称
|
||||
fileName := info.Name()
|
||||
|
||||
// 检查是否应该保留:
|
||||
// 1. CBehaviorRiskScan.vue 文件(无论在哪里)
|
||||
// 2. 匹配到的组件文件夹/文件
|
||||
// 检查是否应该保留:匹配到的组件文件夹/文件
|
||||
shouldInclude := false
|
||||
|
||||
// 检查是否是 CBehaviorRiskScan.vue
|
||||
if fileName == "CBehaviorRiskScan.vue" {
|
||||
// 检查是否是匹配的组件(检查组件名称)
|
||||
if matchedNames[fileName] {
|
||||
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
|
||||
}
|
||||
// 检查是否在匹配的组件文件夹内
|
||||
// 获取相对于 ui 的路径的第一部分(组件文件夹名)
|
||||
parts := strings.Split(filepath.ToSlash(uiRelPath), "/")
|
||||
if len(parts) > 0 && parts[0] != "" && parts[0] != "." {
|
||||
if matchedNames[parts[0]] {
|
||||
shouldInclude = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,7 +200,7 @@ func (g *ZipGenerator) GenerateZipFile(
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
g.logger.Warn("添加src目录失败", zap.Error(err))
|
||||
g.logger.Warn("添加Pure_Component目录失败", zap.Error(err))
|
||||
}
|
||||
|
||||
g.logger.Info("成功生成ZIP文件",
|
||||
@@ -174,6 +210,15 @@ func (g *ZipGenerator) GenerateZipFile(
|
||||
zap.Int("sub_product_count", len(subProductCodes)),
|
||||
)
|
||||
|
||||
// 缓存文件
|
||||
if g.CacheEnabled {
|
||||
if err := g.cacheFile(outputPath, cacheKey); err != nil {
|
||||
g.logger.Warn("缓存ZIP文件失败", zap.Error(err))
|
||||
} else {
|
||||
g.logger.Debug("ZIP文件已缓存", zap.String("cache_key", cacheKey))
|
||||
}
|
||||
}
|
||||
|
||||
return outputPath, nil
|
||||
}
|
||||
|
||||
@@ -263,3 +308,197 @@ func (g *ZipGenerator) AddFolderToZipWithPrefix(zipWriter *zip.Writer, folderPat
|
||||
return g.AddFileToZip(zipWriter, path, zipPath)
|
||||
})
|
||||
}
|
||||
|
||||
// GenerateFilteredComponentZip 生成筛选后的组件ZIP文件
|
||||
// productID: 产品ID
|
||||
// subProductCodes: 子产品编号列表(用于筛选组件)
|
||||
// outputPath: 输出ZIP文件路径(如果为空,则使用默认路径)
|
||||
func (g *ZipGenerator) GenerateFilteredComponentZip(
|
||||
ctx context.Context,
|
||||
productID string,
|
||||
subProductCodes []string,
|
||||
outputPath string,
|
||||
) (string, error) {
|
||||
// 1. 确定基础路径
|
||||
basePath := filepath.Join("resources", "Pure_Component")
|
||||
uiBasePath := filepath.Join(basePath, "src", "ui")
|
||||
|
||||
// 2. 确定输出路径
|
||||
if outputPath == "" {
|
||||
// 使用默认路径:storage/component-reports/{productID}_filtered.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_filtered.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. 收集所有匹配的组件名称(文件夹名或文件名)
|
||||
matchedNames := make(map[string]bool)
|
||||
for _, productCode := range subProductCodes {
|
||||
// 简化匹配逻辑,直接使用产品代码作为组件名
|
||||
matchedNames[productCode] = true
|
||||
}
|
||||
|
||||
// 5. 递归添加整个 Pure_Component 目录,但筛选 ui 目录下的内容
|
||||
err = filepath.Walk(basePath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 计算相对于基础路径的相对路径
|
||||
relPath, err := filepath.Rel(basePath, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 转换为ZIP路径格式,保持在Pure_Component目录下
|
||||
zipPath := filepath.ToSlash(filepath.Join("Pure_Component", 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()
|
||||
|
||||
// 检查是否应该保留:匹配到的组件文件夹/文件
|
||||
shouldInclude := false
|
||||
|
||||
// 检查是否是匹配的组件(检查组件名称)
|
||||
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("添加Pure_Component目录失败", zap.Error(err))
|
||||
return "", fmt.Errorf("添加Pure_Component目录失败: %w", err)
|
||||
}
|
||||
|
||||
g.logger.Info("成功生成筛选后的组件ZIP文件",
|
||||
zap.String("product_id", productID),
|
||||
zap.String("output_path", outputPath),
|
||||
zap.Int("matched_components_count", len(matchedNames)),
|
||||
)
|
||||
|
||||
return outputPath, nil
|
||||
}
|
||||
|
||||
// generateCacheKey 生成缓存键
|
||||
func (g *ZipGenerator) generateCacheKey(productID string, subProductCodes []string) string {
|
||||
// 使用产品ID和子产品编码列表生成MD5哈希
|
||||
data := productID
|
||||
for _, code := range subProductCodes {
|
||||
data += "|" + code
|
||||
}
|
||||
|
||||
hash := md5.Sum([]byte(data))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// getCachedFile 获取缓存文件
|
||||
func (g *ZipGenerator) getCachedFile(cacheKey string) (string, error) {
|
||||
// 确保缓存目录存在
|
||||
if err := os.MkdirAll(g.CacheDir, 0755); err != nil {
|
||||
return "", fmt.Errorf("创建缓存目录失败: %w", err)
|
||||
}
|
||||
|
||||
cacheFilePath := filepath.Join(g.CacheDir, cacheKey+".zip")
|
||||
|
||||
// 检查文件是否存在
|
||||
fileInfo, err := os.Stat(cacheFilePath)
|
||||
if os.IsNotExist(err) {
|
||||
return "", nil // 文件不存在,但不是错误
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 检查文件是否过期
|
||||
if time.Since(fileInfo.ModTime()) > g.CacheTTL {
|
||||
// 文件过期,删除
|
||||
os.Remove(cacheFilePath)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return cacheFilePath, nil
|
||||
}
|
||||
|
||||
// cacheFile 缓存文件
|
||||
func (g *ZipGenerator) cacheFile(filePath, cacheKey string) error {
|
||||
// 确保缓存目录存在
|
||||
if err := os.MkdirAll(g.CacheDir, 0755); err != nil {
|
||||
return fmt.Errorf("创建缓存目录失败: %w", err)
|
||||
}
|
||||
|
||||
cacheFilePath := filepath.Join(g.CacheDir, cacheKey+".zip")
|
||||
|
||||
// 复制文件到缓存目录
|
||||
return g.copyFile(filePath, cacheFilePath)
|
||||
}
|
||||
|
||||
// copyFile 复制文件
|
||||
func (g *ZipGenerator) copyFile(src, dst string) error {
|
||||
sourceFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sourceFile.Close()
|
||||
|
||||
destFile, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
_, err = io.Copy(destFile, sourceFile)
|
||||
return err
|
||||
}
|
||||
|
||||
BIN
resources/Pure Component.zip
Normal file
@@ -1,110 +0,0 @@
|
||||
<!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>
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"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
@@ -1,7 +0,0 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// App 根组件,仅用于路由
|
||||
</script>
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
*,
|
||||
*::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;
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/* 统一颜色变量管理文件 */
|
||||
: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;
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 816 B |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 286 B |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 504 B |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 619 B |
|
Before Width: | Height: | Size: 493 B |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 1000 B |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |