add购买记录功能

This commit is contained in:
2025-12-22 18:32:34 +08:00
parent 65a61d0336
commit 7f8554fa12
314 changed files with 4029 additions and 83496 deletions

View File

@@ -231,6 +231,7 @@ func (a *Application) autoMigrate(db *gorm.DB) error {
&financeEntities.AlipayOrder{},
&financeEntities.InvoiceApplication{},
&financeEntities.UserInvoiceInfo{},
&financeEntities.PurchaseOrder{}, //购买组件订单表
// 产品域
&productEntities.Product{},

View File

@@ -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),

View File

@@ -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"`
}

View File

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

View File

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

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

View File

@@ -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描述"`

View File

@@ -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描述"`

View File

@@ -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,
}
// 添加分类信息

View File

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

View File

@@ -2772,18 +2772,19 @@ func (s *StatisticsApplicationServiceImpl) AdminGetTodayCertifiedEnterprises(ctx
var enterprises []map[string]interface{}
for _, cert := range completedCertifications {
// 获取企业信息
enterpriseInfo, err := s.enterpriseInfoRepo.GetByUserID(ctx, cert.UserID)
if err != nil {
s.logger.Warn("获取企业信息失败", zap.String("user_id", cert.UserID), zap.Error(err))
continue
}
// 获取用户基本信息(仅需要用户名)
user, err := s.userRepo.GetByID(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))
continue
}
// 获取企业信息
enterpriseInfo := user.EnterpriseInfo
if enterpriseInfo == nil {
s.logger.Warn("用户没有企业信息", zap.String("user_id", cert.UserID))
continue
}
enterprise := map[string]interface{}{
"id": cert.ID,

View File

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

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

View File

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

View File

@@ -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()
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"
}

View File

@@ -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, "获取管理端购买记录成功")
}

View File

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

View File

@@ -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("组件报告订单路由注册完成")
}

View File

@@ -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) // 管理员购买记录分页
}
// 管理员发票相关路由组

View File

@@ -81,8 +81,6 @@ func (r *ProductAdminRoutes) Register(router *sharedhttp.GinRouter) {
subscriptions.POST("/batch-update-prices", r.handler.BatchUpdateSubscriptionPrices)
}
// 消费记录管理
walletTransactions := adminGroup.Group("/wallet-transactions")
{

View File

@@ -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")

View File

@@ -193,6 +193,13 @@ componentReportHandler := component_report.NewComponentReportHandler(
productRepo,
docRepo,
apiConfigRepo,
componentReportRepo,
purchaseOrderRepo,
rechargeRecordRepo,
alipayOrderRepo,
wechatOrderRepo,
aliPayService,
wechatPayService,
logger,
)

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

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

View File

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

File diff suppressed because it is too large Load Diff

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

View File

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