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