Files
tyapi-server/internal/shared/component_report/handler.go
2026-01-16 18:37:47 +08:00

1649 lines
52 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package component_report
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/shopspring/decimal"
"go.uber.org/zap"
finance_entities "tyapi-server/internal/domains/finance/entities"
financeRepositories "tyapi-server/internal/domains/finance/repositories"
"tyapi-server/internal/domains/product/entities"
"tyapi-server/internal/domains/product/repositories"
"tyapi-server/internal/shared/payment"
)
// ComponentReportHandler 组件报告处理器
type ComponentReportHandler struct {
exampleJSONGenerator *ExampleJSONGenerator
zipGenerator *ZipGenerator
cacheManager *CacheManager
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
}
// NewComponentReportHandler 创建组件报告处理器
func NewComponentReportHandler(
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,
) *ComponentReportHandler {
exampleJSONGenerator := NewExampleJSONGenerator(productRepo, docRepo, apiConfigRepo, logger)
zipGenerator := NewZipGenerator(logger)
// 初始化缓存管理器默认缓存24小时
cacheManager := NewCacheManager("storage/component-reports/cache", 24*time.Hour, logger)
return &ComponentReportHandler{
exampleJSONGenerator: exampleJSONGenerator,
zipGenerator: zipGenerator,
cacheManager: cacheManager,
productRepo: productRepo,
componentReportRepo: componentReportRepo,
purchaseOrderRepo: purchaseOrderRepo,
rechargeRecordRepo: rechargeRecordRepo,
alipayOrderRepo: alipayOrderRepo,
wechatOrderRepo: wechatOrderRepo,
aliPayService: aliPayService,
wechatPayService: wechatPayService,
logger: logger,
}
}
// GenerateExampleJSONRequest 生成示例JSON请求
type GenerateExampleJSONRequest struct {
ProductID string `json:"product_id" binding:"required"` // 产品ID
SubProductCodes []string `json:"sub_product_codes,omitempty"` // 子产品编号列表(可选)
}
// GenerateExampleJSONResponse 生成示例JSON响应
type GenerateExampleJSONResponse struct {
ProductID string `json:"product_id"`
JSONContent string `json:"json_content"`
JSONSize int `json:"json_size"`
}
// GenerateExampleJSON 生成 example.json 文件内容HTTP接口
// POST /api/v1/component-report/generate-example-json
func (h *ComponentReportHandler) GenerateExampleJSON(c *gin.Context) {
var req GenerateExampleJSONRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "请求参数错误",
"error": err.Error(),
})
return
}
// 生成 example.json
jsonData, err := h.exampleJSONGenerator.GenerateExampleJSON(c.Request.Context(), req.ProductID, req.SubProductCodes)
if err != nil {
h.logger.Error("生成example.json失败", zap.Error(err), zap.String("product_id", req.ProductID))
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "生成example.json失败",
"error": err.Error(),
})
return
}
// 返回符合前端响应拦截器期望的格式
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": GenerateExampleJSONResponse{
ProductID: req.ProductID,
JSONContent: string(jsonData),
JSONSize: len(jsonData),
},
"message": "生成示例JSON成功",
})
}
// GenerateZipRequest 生成ZIP文件请求
type GenerateZipRequest struct {
ProductID string `json:"product_id" binding:"required"` // 产品ID
SubProductCodes []string `json:"sub_product_codes,omitempty"` // 子产品编号列表(可选)
OutputPath string `json:"output_path,omitempty"` // 输出路径(可选)
}
// GenerateZip 生成ZIP文件HTTP接口
// POST /api/v1/component-report/generate-zip
func (h *ComponentReportHandler) GenerateZip(c *gin.Context) {
var req GenerateZipRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "请求参数错误",
"error": err.Error(),
})
return
}
// 生成ZIP文件
zipPath, err := h.zipGenerator.GenerateZipFile(
c.Request.Context(),
req.ProductID,
req.SubProductCodes,
h.exampleJSONGenerator,
req.OutputPath,
)
if err != nil {
h.logger.Error("生成ZIP文件失败", zap.Error(err), zap.String("product_id", req.ProductID))
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "生成ZIP文件失败",
"error": err.Error(),
})
return
}
// 检查文件是否存在
fileInfo, err := os.Stat(zipPath)
if err != nil {
h.logger.Error("ZIP文件不存在", zap.Error(err), zap.String("zip_path", zipPath))
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "ZIP文件不存在",
"error": err.Error(),
})
return
}
// 返回符合前端响应拦截器期望的格式
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"code": 200,
"message": "ZIP文件生成成功",
"zip_path": zipPath,
"file_size": fileInfo.Size(),
"file_name": filepath.Base(zipPath),
},
"message": "ZIP文件生成成功",
})
}
// DownloadZip 下载ZIP文件HTTP接口
// GET /api/v1/component-report/download-zip/:product_id
func (h *ComponentReportHandler) DownloadZip(c *gin.Context) {
productID := c.Param("product_id")
if productID == "" {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "产品ID不能为空",
})
return
}
// 构建ZIP文件路径
zipPath := filepath.Join("storage/component-reports", fmt.Sprintf("%s_example.json.zip", productID))
// 检查文件是否存在
if _, err := os.Stat(zipPath); os.IsNotExist(err) {
c.JSON(http.StatusNotFound, gin.H{
"code": 404,
"message": "ZIP文件不存在请先生成ZIP文件",
})
return
}
// 设置响应头
c.Header("Content-Type", "application/zip")
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filepath.Base(zipPath)))
// 发送文件
c.File(zipPath)
}
// DownloadExampleJSON 生成并下载 example.json 文件HTTP接口
// POST /api/v1/component-report/download-example-json
func (h *ComponentReportHandler) DownloadExampleJSON(c *gin.Context) {
var req GenerateExampleJSONRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "请求参数错误",
"error": err.Error(),
})
return
}
// 清理过期的缓存(非阻塞方式)
go func() {
if err := h.cacheManager.CleanExpiredCache(); err != nil {
h.logger.Warn("清理过期缓存失败", zap.Error(err))
}
}()
// 生成 example.json
jsonData, err := h.exampleJSONGenerator.GenerateExampleJSON(c.Request.Context(), req.ProductID, req.SubProductCodes)
if err != nil {
h.logger.Error("生成example.json失败", zap.Error(err), zap.String("product_id", req.ProductID))
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "生成example.json失败",
"error": err.Error(),
})
return
}
// 设置响应头直接下载JSON文件
c.Header("Content-Type", "application/json; charset=utf-8")
c.Header("Content-Disposition", "attachment; filename=example.json")
// 发送JSON数据
c.Data(http.StatusOK, "application/json; charset=utf-8", jsonData)
}
// GenerateAndDownloadZip 生成并下载ZIP文件HTTP接口
// POST /api/v1/component-report/generate-and-download
func (h *ComponentReportHandler) GenerateAndDownloadZip(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "用户未登录",
})
return
}
var req GenerateZipRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "请求参数错误",
"error": err.Error(),
})
return
}
// 直接检查用户是否有已支付的购买记录。
orders, _, err := h.purchaseOrderRepo.GetByUserID(c.Request.Context(), userID, 100, 0)
if err != nil {
h.logger.Error("查询用户购买记录失败", zap.Error(err), zap.String("user_id", userID), zap.String("product_id", req.ProductID))
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "查询购买记录失败",
})
return
}
// 查找有效的已支付订单
var validOrder *finance_entities.PurchaseOrder
for _, order := range orders {
if order.ProductID == req.ProductID && order.Status == finance_entities.PurchaseOrderStatusPaid {
validOrder = order
break
}
}
// 如果没有找到已支付订单,尝试创建一个(可能订单刚创建但状态未更新)
if validOrder == nil {
// 查找该产品的待支付订单
var pendingOrder *finance_entities.PurchaseOrder
for _, order := range orders {
if order.ProductID == req.ProductID && order.Status == finance_entities.PurchaseOrderStatusCreated {
pendingOrder = order
break
}
}
// 如果有待支付订单,尝试主动查询支付状态
if pendingOrder != nil {
h.logger.Info("发现待支付订单,尝试主动查询支付状态",
zap.String("order_id", pendingOrder.ID),
zap.String("pay_channel", pendingOrder.PayChannel))
// 如果是支付宝订单,主动查询状态
if pendingOrder.PayChannel == "alipay" && h.aliPayService != nil {
// 这里可以调用支付宝查询服务,但为了简化,我们只记录日志
h.logger.Info("支付宝订单状态待查询,但当前实现简化处理",
zap.String("order_id", pendingOrder.ID))
}
// 如果是微信订单,主动查询状态
if pendingOrder.PayChannel == "wechat" && h.wechatPayService != nil {
// 这里可以调用微信查询服务,但为了简化,我们只记录日志
h.logger.Info("微信订单状态待查询,但当前实现简化处理",
zap.String("order_id", pendingOrder.ID))
}
}
h.logger.Error("用户没有已支付的购买记录", zap.String("user_id", userID), zap.String("product_id", req.ProductID))
c.JSON(http.StatusForbidden, gin.H{
"code": 403,
"message": "无下载权限,请先完成购买",
})
return
}
// 创建下载记录(仅用于记录,不影响下载流程)
download, err := h.componentReportRepo.GetActiveDownload(c.Request.Context(), userID, req.ProductID)
if err != nil {
h.logger.Warn("查询现有下载记录失败,将创建新记录", zap.Error(err), zap.String("user_id", userID), zap.String("product_id", req.ProductID))
download = nil
}
// 如果不存在有效的下载记录,创建一个
if download == nil {
download, err = h.createDownloadRecordForPaidOrder(c.Request.Context(), validOrder)
if err != nil {
h.logger.Warn("创建下载记录失败,但继续处理下载请求", zap.Error(err), zap.String("order_id", validOrder.ID))
}
}
// 检查下载记录是否仍有效
if download != nil && !download.CanDownload() {
h.logger.Warn("下载记录已过期,但基于已支付订单继续处理",
zap.String("user_id", userID),
zap.String("product_id", req.ProductID),
)
// 不再阻止下载,因为已确认用户有有效的已支付订单
}
// 更新下载次数和最后下载时间
if download != nil {
err = h.componentReportRepo.IncrementDownloadCount(c.Request.Context(), download.ID)
if err != nil {
h.logger.Warn("更新下载次数失败", zap.Error(err))
// 不影响下载流程,只记录警告
}
}
// 清理过期的缓存(非阻塞方式)
go func() {
if err := h.cacheManager.CleanExpiredCache(); err != nil {
h.logger.Warn("清理过期缓存失败", zap.Error(err))
}
}()
// 生成ZIP文件
zipPath, err := h.zipGenerator.GenerateZipFile(
c.Request.Context(),
req.ProductID,
req.SubProductCodes,
h.exampleJSONGenerator,
"", // 使用默认路径
)
if err != nil {
h.logger.Error("生成ZIP文件失败", zap.Error(err), zap.String("product_id", req.ProductID))
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "生成ZIP文件失败",
"error": err.Error(),
})
return
}
// 检查文件是否存在
if _, err := os.Stat(zipPath); os.IsNotExist(err) {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "ZIP文件生成失败",
})
return
}
// 设置响应头
c.Header("Content-Type", "application/zip")
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filepath.Base(zipPath)))
// 发送文件
c.File(zipPath)
}
// CheckDownloadAvailabilityResponse 检查下载可用性响应
type CheckDownloadAvailabilityResponse struct {
CanDownload bool `json:"can_download"` // 是否可以下载
IsPackage bool `json:"is_package"` // 是否为组合包
AllSubProductsExist bool `json:"all_sub_products_exist"` // 所有子产品是否在ui目录存在
MissingSubProducts []string `json:"missing_sub_products"` // 缺失的子产品编号列表
Message string `json:"message"` // 提示信息
}
// CheckDownloadAvailability 检查下载可用性
// GET /api/v1/products/:id/component-report/check
func (h *ComponentReportHandler) CheckDownloadAvailability(c *gin.Context) {
productID := c.Param("id")
if productID == "" {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "产品ID不能为空",
})
return
}
// 获取产品信息
product, err := h.productRepo.GetByID(c.Request.Context(), productID)
if err != nil {
h.logger.Error("获取产品信息失败", zap.Error(err), zap.String("product_id", productID))
c.JSON(http.StatusNotFound, gin.H{
"code": 404,
"message": "产品不存在",
})
return
}
// 检查是否为组合包
if !product.IsPackage {
// 返回符合前端响应拦截器期望的格式
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": CheckDownloadAvailabilityResponse{
CanDownload: false,
IsPackage: false,
Message: "只有组合包产品才能下载示例报告",
},
"message": "检查下载可用性成功",
})
return
}
// 获取组合包子产品
packageItems, err := h.productRepo.GetPackageItems(c.Request.Context(), productID)
if err != nil {
h.logger.Error("获取组合包子产品失败", zap.Error(err), zap.String("product_id", productID))
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "获取组合包子产品失败",
})
return
}
if len(packageItems) == 0 {
// 返回符合前端响应拦截器期望的格式
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": CheckDownloadAvailabilityResponse{
CanDownload: false,
IsPackage: true,
Message: "组合包没有子产品",
},
"message": "检查下载可用性成功",
})
return
}
// 检查所有子产品是否在ui目录存在
var missingSubProducts []string
allExist := true
for _, item := range packageItems {
var productCode string
if item.Product != nil {
productCode = item.Product.Code
} else {
// 如果Product未加载需要获取子产品信息
subProduct, err := h.productRepo.GetByID(c.Request.Context(), item.ProductID)
if err != nil {
h.logger.Warn("获取子产品信息失败", zap.Error(err), zap.String("product_id", item.ProductID))
missingSubProducts = append(missingSubProducts, item.ProductID)
allExist = false
continue
}
productCode = subProduct.Code
}
if productCode == "" {
missingSubProducts = append(missingSubProducts, item.ProductID)
allExist = false
continue
}
// 检查是否在ui目录存在
_, _, err := h.exampleJSONGenerator.MatchSubProductCodeToPath(c.Request.Context(), productCode)
if err != nil {
missingSubProducts = append(missingSubProducts, productCode)
allExist = false
}
}
canDownload := allExist && len(missingSubProducts) == 0
message := "可以下载"
if !canDownload {
message = fmt.Sprintf("以下子产品的UI组件不存在: %v", missingSubProducts)
}
// 返回符合前端响应拦截器期望的格式
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": CheckDownloadAvailabilityResponse{
CanDownload: canDownload,
IsPackage: true,
AllSubProductsExist: allExist,
MissingSubProducts: missingSubProducts,
Message: message,
},
"message": "检查下载可用性成功",
})
}
// GetDownloadInfoResponse 获取下载信息响应
type GetDownloadInfoResponse 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组件价格
DownloadedProductCodes []string `json:"downloaded_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"`
IsDownloaded bool `json:"is_downloaded"`
}
// GetDownloadInfo 获取下载信息和价格计算
// GET /api/v1/products/:id/component-report/info
func (h *ComponentReportHandler) GetDownloadInfo(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "用户未登录",
})
return
}
productID := c.Param("id")
if productID == "" {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "产品ID不能为空",
})
return
}
// 获取产品信息
product, err := h.productRepo.GetByID(c.Request.Context(), productID)
if err != nil {
h.logger.Error("获取产品信息失败", zap.Error(err), zap.String("product_id", productID))
c.JSON(http.StatusNotFound, gin.H{
"code": 404,
"message": "产品不存在",
})
return
}
// 检查是否为组合包
if !product.IsPackage {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "只有组合包产品才能下载示例报告",
})
return
}
// 获取组合包子产品
packageItems, err := h.productRepo.GetPackageItems(c.Request.Context(), productID)
if err != nil {
h.logger.Error("获取组合包子产品失败", zap.Error(err), zap.String("product_id", productID))
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "获取组合包子产品失败",
})
return
}
// 获取用户已购买的产品编号列表
purchasedCodes, err := h.purchaseOrderRepo.GetUserPurchasedProductCodes(c.Request.Context(), userID)
if err != nil {
h.logger.Warn("获取用户已购买产品编号失败", zap.Error(err), zap.String("user_id", userID))
purchasedCodes = []string{}
}
// 创建已购买编号的map用于快速查找
purchasedMap := make(map[string]bool)
for _, code := range purchasedCodes {
purchasedMap[code] = true
}
// 使用产品的UIComponentPrice作为价格
finalPrice := product.UIComponentPrice
// h.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 entities.Product
var productCode string
var productName string
if item.Product != nil {
subProduct = *item.Product
productCode = subProduct.Code
productName = subProduct.Name
} else {
// 如果Product未加载需要获取子产品信息
subProduct, err = h.productRepo.GetByID(c.Request.Context(), item.ProductID)
if err != nil {
h.logger.Warn("获取子产品信息失败", zap.Error(err), zap.String("product_id", item.ProductID))
continue
}
productCode = subProduct.Code
productName = subProduct.Name
}
if productCode == "" {
continue
}
// 检查是否已购买
isPurchased := purchasedMap[productCode]
subProducts = append(subProducts, SubProductPriceInfo{
ProductID: subProduct.ID,
ProductCode: productCode,
ProductName: productName,
IsDownloaded: isPurchased, // 修改字段名但保持功能
})
}
// 检查用户是否有已支付的购买记录(针对当前产品)
// 使用购买订单状态来判断支付状态
hasPaidDownload := false
orders, _, err := h.purchaseOrderRepo.GetByUserID(c.Request.Context(), userID, 100, 0)
if err == nil {
for _, order := range orders {
if order.ProductID == productID && order.Status == finance_entities.PurchaseOrderStatusPaid {
hasPaidDownload = true
break
}
}
}
// 如果可以下载价格为0免费或者用户已支付
canDownload := finalPrice.IsZero() || hasPaidDownload
// 返回符合前端响应拦截器期望的格式
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": GetDownloadInfoResponse{
ProductID: productID,
ProductCode: product.Code,
ProductName: product.Name,
IsPackage: true,
SubProducts: subProducts,
Price: finalPrice.String(),
DownloadedProductCodes: purchasedCodes, // 修改为购买产品编号
CanDownload: canDownload,
},
"message": "获取下载信息成功",
})
}
// CreatePaymentOrderRequest 创建支付订单请求
type CreatePaymentOrderRequest struct {
PaymentType string `json:"payment_type" binding:"required"` // wechat 或 alipay
Platform string `json:"platform,omitempty"` // 支付平台app, h5, pc可选默认根据User-Agent判断
}
// CreatePaymentOrderResponse 创建支付订单响应
type CreatePaymentOrderResponse struct {
OrderID string `json:"order_id"` // 订单ID
CodeURL string `json:"code_url"` // 支付二维码URL微信
PayURL string `json:"pay_url"` // 支付链接(支付宝)
PaymentType string `json:"payment_type"` // 支付类型
Amount string `json:"amount"` // 支付金额
}
// CreatePaymentOrder 创建支付订单
// POST /api/v1/products/:id/component-report/create-order
func (h *ComponentReportHandler) CreatePaymentOrder(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "用户未登录",
})
return
}
productID := c.Param("id")
if productID == "" {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "产品ID不能为空",
})
return
}
var req CreatePaymentOrderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "请求参数错误",
"error": err.Error(),
})
return
}
if req.PaymentType != "wechat" && req.PaymentType != "alipay" {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "支付类型必须是 wechat 或 alipay",
})
return
}
// 确定支付平台类型app/h5/pc
platform := req.Platform
if platform == "" {
// 根据 User-Agent 判断平台类型
userAgent := c.GetHeader("User-Agent")
platform = h.detectPlatform(userAgent)
}
// 验证平台类型
if req.PaymentType == "alipay" {
if platform != "app" && platform != "h5" && platform != "pc" {
platform = "h5" // 默认使用 H5 支付
}
} else if req.PaymentType == "wechat" {
// 微信支付目前只支持 native扫码支付
platform = "native"
}
// 获取下载信息以计算价格
product, err := h.productRepo.GetByID(c.Request.Context(), productID)
if err != nil {
h.logger.Error("获取产品信息失败", zap.Error(err), zap.String("product_id", productID))
c.JSON(http.StatusNotFound, gin.H{
"code": 404,
"message": "产品不存在",
})
return
}
if !product.IsPackage {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "只有组合包产品才能下载示例报告",
})
return
}
// 获取组合包子产品
packageItems, err := h.productRepo.GetPackageItems(c.Request.Context(), productID)
if err != nil {
h.logger.Error("获取组合包子产品失败", zap.Error(err), zap.String("product_id", productID))
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "获取组合包子产品失败",
})
return
}
// 获取用户已下载的产品编号列表
downloadedCodes, err := h.componentReportRepo.GetUserDownloadedProductCodes(c.Request.Context(), userID)
if err != nil {
h.logger.Warn("获取用户已下载产品编号失败", zap.Error(err), zap.String("user_id", userID))
downloadedCodes = []string{}
}
// 创建已下载编号的map用于快速查找
downloadedMap := make(map[string]bool)
for _, code := range downloadedCodes {
downloadedMap[code] = true
}
// 使用产品的UIComponentPrice作为价格
finalPrice := product.UIComponentPrice
// h.logger.Info("使用UI组件价格创建支付订单",
// zap.String("product_id", productID),
// zap.String("product_ui_component_price", finalPrice.String()),
// )
// 准备子产品信息列表(仅用于展示)
var subProductCodes []string
var subProductIDs []string
for _, item := range packageItems {
var subProduct entities.Product
var productCode string
if item.Product != nil {
subProduct = *item.Product
productCode = subProduct.Code
} else {
subProduct, err = h.productRepo.GetByID(c.Request.Context(), item.ProductID)
if err != nil {
continue
}
productCode = subProduct.Code
}
if productCode == "" {
continue
}
// 收集所有子产品信息
subProductCodes = append(subProductCodes, productCode)
subProductIDs = append(subProductIDs, subProduct.ID)
}
if finalPrice.LessThanOrEqual(decimal.Zero) {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "无需支付,所有子产品已下载",
})
return
}
// 验证数据完整性
if len(subProductCodes) == 0 {
h.logger.Warn("子产品列表为空", zap.String("product_id", productID))
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "子产品列表为空,无法创建下载记录",
})
return
}
// 验证必要字段
if product.Code == "" {
h.logger.Error("产品编号为空", zap.String("product_id", productID))
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "产品编号为空,无法创建下载记录",
})
return
}
// 序列化子产品编号列表
subProductCodesJSON, err := json.Marshal(subProductCodes)
if err != nil {
h.logger.Error("序列化子产品编号列表失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "序列化子产品编号列表失败",
"error": err.Error(),
})
return
}
subProductIDsJSON, err := json.Marshal(subProductIDs)
if err != nil {
h.logger.Error("序列化子产品ID列表失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "序列化子产品ID列表失败",
"error": err.Error(),
})
return
}
// 生成商户订单号
var outTradeNo string
if req.PaymentType == "alipay" {
outTradeNo = h.aliPayService.GenerateOutTradeNo()
} else {
outTradeNo = h.wechatPayService.GenerateOutTradeNo()
}
// 构建订单主题和备注
subject := fmt.Sprintf("组件报告下载-%s", product.Name)
if len(subProductCodes) > 0 {
subject = fmt.Sprintf("组件报告下载-%s(%d个子产品)", product.Name, len(subProductCodes))
}
notes := fmt.Sprintf("购买%s报告示例", product.Name)
// h.logger.Info("========== 开始创建组件报告下载支付订单 ==========",
// zap.String("out_trade_no", outTradeNo),
// zap.String("payment_type", req.PaymentType),
// zap.String("amount", finalPrice.String()),
// zap.String("product_name", product.Name),
// )
// 步骤1: 创建购买订单记录
// h.logger.Info("步骤1: 创建购买订单记录",
// zap.String("out_trade_no", outTradeNo),
// zap.String("user_id", userID),
// zap.String("product_id", productID),
// zap.String("product_code", product.Code),
// zap.String("amount", finalPrice.String()),
// zap.String("payment_type", req.PaymentType),
// )
// 创建购买订单,初始状态为 created
purchaseOrder := finance_entities.NewPurchaseOrder(
userID,
productID,
product.Code,
product.Name,
subject,
finalPrice,
platform,
req.PaymentType,
req.PaymentType,
)
createdPurchaseOrder, err := h.purchaseOrderRepo.Create(c.Request.Context(), purchaseOrder)
if err != nil {
h.logger.Error("创建购买订单失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "创建购买订单失败",
"error": err.Error(),
})
return
}
// 创建下载记录
// 设置原始价格组合包使用UIComponentPrice单品使用Price
var originalPrice decimal.Decimal
if product.IsPackage {
originalPrice = product.UIComponentPrice
} else {
originalPrice = product.Price
}
download := &entities.ComponentReportDownload{
UserID: userID,
ProductID: productID,
ProductCode: product.Code,
ProductName: product.Name,
SubProductIDs: string(subProductIDsJSON),
SubProductCodes: string(subProductCodesJSON),
// 关联购买订单ID
OrderID: &createdPurchaseOrder.ID,
OrderNumber: &outTradeNo,
OriginalPrice: originalPrice, // 设置原始价格
DownloadPrice: finalPrice, // 设置下载价格
}
// 记录创建前的详细信息用于调试
// h.logger.Info("准备创建下载记录",
// zap.String("user_id", userID),
// zap.String("product_id", productID),
// zap.String("product_code", product.Code),
// zap.String("download_price", finalPrice.String()),
// zap.String("ui_component_price", finalPrice.String()),
// zap.String("discount_amount", "0.00"),
// zap.Int("sub_product_count", len(subProductCodes)),
// )
err = h.componentReportRepo.Create(c.Request.Context(), download)
if err != nil {
// 记录详细的错误信息
h.logger.Error("创建下载记录失败",
zap.Error(err),
zap.String("user_id", userID),
zap.String("product_id", productID),
zap.String("product_code", product.Code),
zap.String("download_price", finalPrice.String()),
zap.Any("sub_product_codes", subProductCodes),
zap.Any("sub_product_ids", subProductIDs),
)
// 返回更详细的错误信息(开发环境可以显示,生产环境可以隐藏)
errorMsg := "创建下载记录失败"
if err.Error() != "" {
errorMsg = fmt.Sprintf("创建下载记录失败: %s", err.Error())
}
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": errorMsg,
"error": err.Error(), // 包含具体错误信息便于调试
})
return
}
// 步骤2: 创建充值记录和支付订单记录
var rechargeRecord finance_entities.RechargeRecord
if req.PaymentType == "alipay" {
// h.logger.Info("步骤2: 创建支付宝充值记录",
// zap.String("out_trade_no", outTradeNo),
// zap.String("user_id", userID),
// zap.String("amount", finalPrice.String()),
// zap.String("notes", notes),
// )
// 使用带备注的工厂方法创建充值记录
rechargeRecordPtr := finance_entities.NewAlipayRechargeRecordWithNotes(userID, finalPrice, outTradeNo, notes)
rechargeRecord, err = h.rechargeRecordRepo.Create(c.Request.Context(), *rechargeRecordPtr)
if err != nil {
h.logger.Error("创建支付宝充值记录失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "创建支付宝充值记录失败",
"error": err.Error(),
})
return
}
// h.logger.Info("步骤3: 创建支付宝订单记录",
// zap.String("recharge_id", rechargeRecord.ID),
// zap.String("out_trade_no", outTradeNo),
// zap.String("subject", subject),
// )
alipayOrder := finance_entities.NewAlipayOrder(rechargeRecord.ID, outTradeNo, subject, finalPrice, platform)
_, err = h.alipayOrderRepo.Create(c.Request.Context(), *alipayOrder)
if err != nil {
h.logger.Error("创建支付宝订单记录失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "创建支付宝订单记录失败",
"error": err.Error(),
})
return
}
} else {
// h.logger.Info("步骤2: 创建微信充值记录",
// zap.String("out_trade_no", outTradeNo),
// zap.String("user_id", userID),
// zap.String("amount", finalPrice.String()),
// zap.String("notes", notes),
// )
// 使用带备注的工厂方法创建充值记录
rechargeRecordPtr := finance_entities.NewWechatRechargeRecordWithNotes(userID, finalPrice, outTradeNo, notes)
rechargeRecord, err = h.rechargeRecordRepo.Create(c.Request.Context(), *rechargeRecordPtr)
if err != nil {
h.logger.Error("创建微信充值记录失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "创建微信充值记录失败",
"error": err.Error(),
})
return
}
// h.logger.Info("步骤3: 创建微信订单记录",
// zap.String("recharge_id", rechargeRecord.ID),
// zap.String("out_trade_no", outTradeNo),
// zap.String("subject", subject),
// )
wechatOrder := finance_entities.NewWechatOrder(rechargeRecord.ID, outTradeNo, subject, finalPrice, platform)
_, err = h.wechatOrderRepo.Create(c.Request.Context(), *wechatOrder)
if err != nil {
h.logger.Error("创建微信订单记录失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "创建微信订单记录失败",
"error": err.Error(),
})
return
}
}
// 更新下载记录的支付订单号
download.OrderNumber = &outTradeNo
err = h.componentReportRepo.UpdateDownload(c.Request.Context(), download)
if err != nil {
h.logger.Error("更新下载记录支付订单号失败", zap.Error(err))
// 不阻断流程,继续执行
}
// h.logger.Info("步骤3: 调用支付接口创建订单",
// zap.String("out_trade_no", outTradeNo),
// zap.String("platform", platform),
// )
// 调用支付服务创建订单
var payURL string
var codeURL string
if req.PaymentType == "alipay" {
// 调用支付宝支付服务
payURL, err = h.aliPayService.CreateAlipayOrder(c.Request.Context(), platform, finalPrice, subject, outTradeNo)
if err != nil {
h.logger.Error("创建支付宝订单失败",
zap.Error(err),
zap.String("out_trade_no", outTradeNo),
zap.String("platform", platform),
zap.String("amount", finalPrice.String()),
)
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "创建支付宝订单失败",
"error": err.Error(),
})
return
}
// h.logger.Info("步骤4: 支付宝订单创建成功",
// zap.String("out_trade_no", outTradeNo),
// zap.String("pay_url", payURL),
// )
} else {
// 调用微信支付服务(目前只支持 native 扫码支付)
amountFloat, _ := finalPrice.Float64()
result, err := h.wechatPayService.CreateWechatNativeOrder(c.Request.Context(), amountFloat, subject, outTradeNo)
if err != nil {
h.logger.Error("创建微信支付订单失败",
zap.Error(err),
zap.String("out_trade_no", outTradeNo),
zap.String("amount", finalPrice.String()),
)
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "创建微信支付订单失败",
"error": err.Error(),
})
return
}
// 微信返回的是二维码URL
if resultMap, ok := result.(map[string]string); ok {
if url, exists := resultMap["code_url"]; exists {
codeURL = url
}
} else if resultMap, ok := result.(map[string]interface{}); ok {
// 兼容处理
if url, exists := resultMap["code_url"]; exists {
codeURL = fmt.Sprintf("%v", url)
}
}
// h.logger.Info("步骤4: 微信订单创建成功",
// zap.String("out_trade_no", outTradeNo),
// zap.String("code_url", codeURL),
// )
}
response := CreatePaymentOrderResponse{
OrderID: createdPurchaseOrder.ID,
PaymentType: req.PaymentType,
Amount: finalPrice.String(),
}
if req.PaymentType == "wechat" {
response.CodeURL = codeURL
} else {
response.PayURL = payURL
}
// h.logger.Info("========== 组件报告下载支付订单创建完成 ==========",
// zap.String("order_id", download.ID),
// zap.String("out_trade_no", outTradeNo),
// zap.String("recharge_id", rechargeRecord.ID),
// zap.String("payment_type", req.PaymentType),
// zap.String("platform", platform),
// zap.String("amount", finalPrice.String()),
// zap.String("notes", notes),
// )
// 返回符合前端响应拦截器期望的格式
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": response,
"message": "创建支付订单成功",
})
}
// CheckPaymentStatusResponse 检查支付状态响应
type CheckPaymentStatusResponse struct {
OrderID string `json:"order_id"` // 订单ID
PaymentStatus string `json:"payment_status"` // 支付状态pending, success, failed
CanDownload bool `json:"can_download"` // 是否可以下载
}
// CheckPaymentStatus 检查支付状态
// GET /api/v1/products/:id/component-report/check-payment/:orderId
func (h *ComponentReportHandler) 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
}
// 根据订单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
}
// 如果订单状态是 pending主动查询支付订单状态
// 注意这里我们检查OrderNumber字段
if download.OrderNumber != nil {
// 兼容旧的支付订单逻辑
outTradeNo := *download.OrderNumber
h.logger.Info("订单状态为pending主动查询支付订单状态",
zap.String("order_id", orderID),
zap.String("out_trade_no", outTradeNo),
)
// 简化处理如果有支付订单ID直接标记为成功
expiresAt := time.Now().Add(30 * 24 * time.Hour)
download.ExpiresAt = &expiresAt
err = h.componentReportRepo.UpdateDownload(c.Request.Context(), download)
if err != nil {
h.logger.Error("更新下载记录状态失败", zap.Error(err))
}
}
// 使用支付订单状态来判断支付状态
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, gin.H{
"success": true,
"data": CheckPaymentStatusResponse{
OrderID: download.ID,
PaymentStatus: paymentStatus,
CanDownload: canDownload,
},
"message": "查询支付状态成功",
})
}
// detectPlatform 根据 User-Agent 检测支付平台类型
func (h *ComponentReportHandler) 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"
}
// createDownloadRecordIfEligible 检查用户是否有购买记录,如果有则创建下载记录
func (h *ComponentReportHandler) createDownloadRecordIfEligible(ctx context.Context, userID, productID string) (*entities.ComponentReportDownload, error) {
// 1. 检查用户是否有有效的购买记录
orders, _, err := h.purchaseOrderRepo.GetByUserID(ctx, userID, 100, 0)
if err != nil {
return nil, fmt.Errorf("查询用户购买记录失败: %w", err)
}
var validOrder *finance_entities.PurchaseOrder
for _, order := range orders {
if order.ProductID == productID && order.Status == finance_entities.PurchaseOrderStatusPaid {
validOrder = order
break
}
}
if validOrder == nil {
return nil, fmt.Errorf("无购买记录或购买未支付")
}
// 2. 获取产品信息
product, err := h.productRepo.GetByID(ctx, productID)
if err != nil {
return nil, fmt.Errorf("获取产品信息失败: %w", err)
}
// 3. 创建下载记录
// 设置原始价格组合包使用UIComponentPrice单品使用Price
var originalPrice decimal.Decimal
if product.IsPackage {
originalPrice = product.UIComponentPrice
} else {
originalPrice = product.Price
}
download := &entities.ComponentReportDownload{
UserID: userID,
ProductID: productID,
ProductCode: product.Code,
ProductName: product.Name,
OrderID: &validOrder.ID, // 添加OrderID字段
OrderNumber: &validOrder.OrderNo, // 使用OrderNumber字段
OriginalPrice: originalPrice, // 设置原始价格
DownloadPrice: validOrder.Amount, // 设置下载价格(从订单获取)
ExpiresAt: calculateExpiryTime(), // 从创建日起30天
}
// 4. 如果是组合包,获取子产品信息
if product.IsPackage {
packageItems, err := h.getSubProductsByProductID(ctx, productID)
if err == nil && len(packageItems) > 0 {
var subProductIDs []string
var subProductCodes []string
// 获取子产品的详细信息
for _, item := range packageItems {
if item.Product != nil {
subProductIDs = append(subProductIDs, item.Product.ID)
subProductCodes = append(subProductCodes, item.Product.Code)
} else {
// 如果关联的Product为nil需要单独查询
subProduct, err := h.productRepo.GetByID(ctx, item.ProductID)
if err != nil {
h.logger.Warn("获取子产品信息失败", zap.Error(err), zap.String("product_id", item.ProductID))
continue
}
subProductIDs = append(subProductIDs, subProduct.ID)
subProductCodes = append(subProductCodes, subProduct.Code)
}
}
subProductIDsJSON, _ := json.Marshal(subProductIDs)
subProductCodesJSON, _ := json.Marshal(subProductCodes)
download.SubProductIDs = string(subProductIDsJSON)
download.SubProductCodes = string(subProductCodesJSON)
}
}
// 5. 保存下载记录
err = h.componentReportRepo.Create(ctx, download)
if err != nil {
return nil, fmt.Errorf("创建下载记录失败: %w", err)
}
// h.logger.Info("创建下载记录成功",
// zap.String("user_id", userID),
// zap.String("product_id", productID),
// zap.String("download_id", download.ID),
// )
return download, nil
}
// getSubProductsByProductID 获取产品的子产品信息
func (h *ComponentReportHandler) getSubProductsByProductID(ctx context.Context, productID string) ([]*entities.ProductPackageItem, error) {
// 使用ProductRepository的GetPackageItems方法获取组合包的子产品
return h.productRepo.GetPackageItems(ctx, productID)
}
// ClearCacheResponse 清理缓存响应
type ClearCacheResponse struct {
CacheSizeBefore int64 `json:"cache_size_before"` // 清理前缓存大小(字节)
CacheSizeAfter int64 `json:"cache_size_after"` // 清理后缓存大小(字节)
CacheCount int `json:"cache_count"` // 清理的文件数量
Success bool `json:"success"` // 是否成功
}
// ClearCache 清理缓存接口
// DELETE /api/v1/component-report/cache
func (h *ComponentReportHandler) ClearCache(c *gin.Context) {
// 获取清理前的缓存大小
sizeBefore, err := h.cacheManager.GetCacheSize()
if err != nil {
h.logger.Warn("获取清理前缓存大小失败", zap.Error(err))
sizeBefore = 0
}
// 获取清理前的缓存文件数量
countBefore, err := h.cacheManager.GetCacheCount()
if err != nil {
h.logger.Warn("获取清理前缓存文件数量失败", zap.Error(err))
countBefore = 0
}
// 清理所有缓存
if err := h.cacheManager.ClearAllCache(); err != nil {
h.logger.Error("清理缓存失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "清理缓存失败",
"error": err.Error(),
})
return
}
// 获取清理后的缓存大小应该为0
sizeAfter, _ := h.cacheManager.GetCacheSize()
// 返回符合前端响应拦截器期望的格式
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": ClearCacheResponse{
CacheSizeBefore: sizeBefore,
CacheSizeAfter: sizeAfter,
CacheCount: countBefore,
Success: true,
},
"message": "缓存清理成功",
})
}
// CleanExpiredCache 清理过期缓存接口
// POST /api/v1/component-report/cache/clean-expired
func (h *ComponentReportHandler) CleanExpiredCache(c *gin.Context) {
// 获取清理前的缓存大小
sizeBefore, err := h.cacheManager.GetCacheSize()
if err != nil {
h.logger.Warn("获取清理前缓存大小失败", zap.Error(err))
sizeBefore = 0
}
// 获取清理前的缓存文件数量
countBefore, err := h.cacheManager.GetCacheCount()
if err != nil {
h.logger.Warn("获取清理前缓存文件数量失败", zap.Error(err))
countBefore = 0
}
// 清理过期缓存
if err := h.cacheManager.CleanExpiredCache(); err != nil {
h.logger.Error("清理过期缓存失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "清理过期缓存失败",
"error": err.Error(),
})
return
}
// 获取清理后的缓存大小
sizeAfter, _ := h.cacheManager.GetCacheSize()
// 获取清理后的缓存文件数量
countAfter, _ := h.cacheManager.GetCacheCount()
// 返回符合前端响应拦截器期望的格式
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"cache_size_before": sizeBefore,
"cache_size_after": sizeAfter,
"cache_count_before": countBefore,
"cache_count_after": countAfter,
"cleaned_count": countBefore - countAfter,
},
"message": "过期缓存清理成功",
})
}
// calculateExpiryTime 计算下载有效期从创建日起30天
func calculateExpiryTime() *time.Time {
now := time.Now()
expiry := now.AddDate(0, 0, 30) // 30天后过期
return &expiry
}
// createDownloadRecordForPaidOrder 为已支付订单创建下载记录
func (h *ComponentReportHandler) createDownloadRecordForPaidOrder(ctx context.Context, order *finance_entities.PurchaseOrder) (*entities.ComponentReportDownload, error) {
// 获取产品信息
product, err := h.productRepo.GetByID(ctx, order.ProductID)
if err != nil {
return nil, fmt.Errorf("获取产品信息失败: %w", err)
}
// 创建下载记录
// 设置原始价格组合包使用UIComponentPrice单品使用Price
var originalPrice decimal.Decimal
if product.IsPackage {
originalPrice = product.UIComponentPrice
} else {
originalPrice = product.Price
}
download := &entities.ComponentReportDownload{
UserID: order.UserID,
ProductID: order.ProductID,
ProductCode: order.ProductCode,
ProductName: order.ProductName,
OrderID: &order.ID,
OrderNumber: &order.OrderNo,
OriginalPrice: originalPrice, // 设置原始价格
DownloadPrice: order.Amount, // 设置下载价格(从订单获取)
ExpiresAt: calculateExpiryTime(),
}
// 如果是组合包,获取子产品信息
if product.IsPackage {
packageItems, err := h.getSubProductsByProductID(ctx, order.ProductID)
if err == nil && len(packageItems) > 0 {
var subProductIDs []string
var subProductCodes []string
// 获取子产品的详细信息
for _, item := range packageItems {
if item.Product != nil {
subProductIDs = append(subProductIDs, item.Product.ID)
subProductCodes = append(subProductCodes, item.Product.Code)
} else {
// 如果关联的Product为nil需要单独查询
subProduct, err := h.productRepo.GetByID(ctx, item.ProductID)
if err != nil {
h.logger.Warn("获取子产品信息失败", zap.Error(err), zap.String("product_id", item.ProductID))
continue
}
subProductIDs = append(subProductIDs, subProduct.ID)
subProductCodes = append(subProductCodes, subProduct.Code)
}
}
subProductIDsJSON, _ := json.Marshal(subProductIDs)
subProductCodesJSON, _ := json.Marshal(subProductCodes)
download.SubProductIDs = string(subProductIDsJSON)
download.SubProductCodes = string(subProductCodesJSON)
}
}
// 保存下载记录
err = h.componentReportRepo.Create(ctx, download)
if err != nil {
return nil, fmt.Errorf("创建下载记录失败: %w", err)
}
h.logger.Info("创建下载记录成功",
zap.String("user_id", order.UserID),
zap.String("product_id", order.ProductID),
zap.String("order_id", order.ID),
zap.String("download_id", download.ID),
)
return download, nil
}