2025-12-19 17:05:09 +08:00
|
|
|
|
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"
|
2025-12-22 18:32:34 +08:00
|
|
|
|
financeRepositories "tyapi-server/internal/domains/finance/repositories"
|
2025-12-19 17:05:09 +08:00
|
|
|
|
"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
|
2025-12-22 18:32:34 +08:00
|
|
|
|
cacheManager *CacheManager
|
2025-12-19 17:05:09 +08:00
|
|
|
|
productRepo repositories.ProductRepository
|
|
|
|
|
|
componentReportRepo repositories.ComponentReportRepository
|
2025-12-22 18:32:34 +08:00
|
|
|
|
purchaseOrderRepo financeRepositories.PurchaseOrderRepository
|
2025-12-19 17:05:09 +08:00
|
|
|
|
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,
|
2025-12-22 18:32:34 +08:00
|
|
|
|
purchaseOrderRepo financeRepositories.PurchaseOrderRepository,
|
2025-12-19 17:05:09 +08:00
|
|
|
|
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)
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// 初始化缓存管理器,默认缓存24小时
|
|
|
|
|
|
cacheManager := NewCacheManager("storage/component-reports/cache", 24*time.Hour, logger)
|
2025-12-19 17:05:09 +08:00
|
|
|
|
|
|
|
|
|
|
return &ComponentReportHandler{
|
|
|
|
|
|
exampleJSONGenerator: exampleJSONGenerator,
|
|
|
|
|
|
zipGenerator: zipGenerator,
|
2025-12-22 18:32:34 +08:00
|
|
|
|
cacheManager: cacheManager,
|
2025-12-19 17:05:09 +08:00
|
|
|
|
productRepo: productRepo,
|
|
|
|
|
|
componentReportRepo: componentReportRepo,
|
2025-12-22 18:32:34 +08:00
|
|
|
|
purchaseOrderRepo: purchaseOrderRepo,
|
2025-12-19 17:05:09 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// 返回符合前端响应拦截器期望的格式
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"data": GenerateExampleJSONResponse{
|
|
|
|
|
|
ProductID: req.ProductID,
|
|
|
|
|
|
JSONContent: string(jsonData),
|
|
|
|
|
|
JSONSize: len(jsonData),
|
|
|
|
|
|
},
|
|
|
|
|
|
"message": "生成示例JSON成功",
|
2025-12-19 17:05:09 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// 返回符合前端响应拦截器期望的格式
|
2025-12-19 17:05:09 +08:00
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
2025-12-22 18:32:34 +08:00
|
|
|
|
"success": true,
|
|
|
|
|
|
"data": gin.H{
|
|
|
|
|
|
"code": 200,
|
|
|
|
|
|
"message": "ZIP文件生成成功",
|
|
|
|
|
|
"zip_path": zipPath,
|
|
|
|
|
|
"file_size": fileInfo.Size(),
|
|
|
|
|
|
"file_name": filepath.Base(zipPath),
|
|
|
|
|
|
},
|
|
|
|
|
|
"message": "ZIP文件生成成功",
|
2025-12-19 17:05:09 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// 清理过期的缓存(非阻塞方式)
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
if err := h.cacheManager.CleanExpiredCache(); err != nil {
|
|
|
|
|
|
h.logger.Warn("清理过期缓存失败", zap.Error(err))
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
|
2025-12-19 17:05:09 +08:00
|
|
|
|
// 生成 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-24 17:50:33 +08:00
|
|
|
|
// 直接检查用户是否有已支付的购买记录,而不是查询下载记录
|
|
|
|
|
|
orders, _, err := h.purchaseOrderRepo.GetByUserID(c.Request.Context(), userID, 100, 0)
|
2025-12-19 17:05:09 +08:00
|
|
|
|
if err != nil {
|
2025-12-24 17:50:33 +08:00
|
|
|
|
h.logger.Error("查询用户购买记录失败", zap.Error(err), zap.String("user_id", userID), zap.String("product_id", req.ProductID))
|
2025-12-19 17:05:09 +08:00
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
|
|
|
|
"code": 500,
|
2025-12-24 17:50:33 +08:00
|
|
|
|
"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 {
|
2026-01-14 15:59:15 +08:00
|
|
|
|
h.logger.Info("发现待支付订单,尝试主动查询支付状态",
|
2025-12-24 17:50:33 +08:00
|
|
|
|
zap.String("order_id", pendingOrder.ID),
|
|
|
|
|
|
zap.String("pay_channel", pendingOrder.PayChannel))
|
|
|
|
|
|
|
|
|
|
|
|
// 如果是支付宝订单,主动查询状态
|
|
|
|
|
|
if pendingOrder.PayChannel == "alipay" && h.aliPayService != nil {
|
|
|
|
|
|
// 这里可以调用支付宝查询服务,但为了简化,我们只记录日志
|
2026-01-14 15:59:15 +08:00
|
|
|
|
h.logger.Info("支付宝订单状态待查询,但当前实现简化处理",
|
2025-12-24 17:50:33 +08:00
|
|
|
|
zap.String("order_id", pendingOrder.ID))
|
|
|
|
|
|
}
|
|
|
|
|
|
// 如果是微信订单,主动查询状态
|
|
|
|
|
|
if pendingOrder.PayChannel == "wechat" && h.wechatPayService != nil {
|
|
|
|
|
|
// 这里可以调用微信查询服务,但为了简化,我们只记录日志
|
2026-01-14 15:59:15 +08:00
|
|
|
|
h.logger.Info("微信订单状态待查询,但当前实现简化处理",
|
2025-12-24 17:50:33 +08:00
|
|
|
|
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": "无下载权限,请先完成购买",
|
2025-12-19 17:05:09 +08:00
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-24 17:50:33 +08:00
|
|
|
|
// 创建下载记录(仅用于记录,不影响下载流程)
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果不存在有效的下载记录,创建一个
|
2025-12-22 18:32:34 +08:00
|
|
|
|
if download == nil {
|
2025-12-24 17:50:33 +08:00
|
|
|
|
download, err = h.createDownloadRecordForPaidOrder(c.Request.Context(), validOrder)
|
2025-12-22 18:32:34 +08:00
|
|
|
|
if err != nil {
|
2025-12-24 17:50:33 +08:00
|
|
|
|
h.logger.Warn("创建下载记录失败,但继续处理下载请求", zap.Error(err), zap.String("order_id", validOrder.ID))
|
2025-12-19 17:05:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// 检查下载记录是否仍有效
|
2025-12-24 17:50:33 +08:00
|
|
|
|
if download != nil && !download.CanDownload() {
|
|
|
|
|
|
h.logger.Warn("下载记录已过期,但基于已支付订单继续处理",
|
2025-12-19 17:05:09 +08:00
|
|
|
|
zap.String("user_id", userID),
|
|
|
|
|
|
zap.String("product_id", req.ProductID),
|
|
|
|
|
|
)
|
2025-12-24 17:50:33 +08:00
|
|
|
|
// 不再阻止下载,因为已确认用户有有效的已支付订单
|
2025-12-19 17:05:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// 更新下载次数和最后下载时间
|
|
|
|
|
|
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))
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
|
2026-01-14 15:59:15 +08:00
|
|
|
|
// 创建带超时的上下文,避免ZIP生成时间过长导致网关超时
|
|
|
|
|
|
// 设置超时时间为 25 秒,略小于服务器的 write_timeout (30秒)
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), 25*time.Second)
|
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
2025-12-19 17:05:09 +08:00
|
|
|
|
// 生成ZIP文件
|
|
|
|
|
|
zipPath, err := h.zipGenerator.GenerateZipFile(
|
2026-01-14 15:59:15 +08:00
|
|
|
|
ctx,
|
2025-12-19 17:05:09 +08:00
|
|
|
|
req.ProductID,
|
|
|
|
|
|
req.SubProductCodes,
|
|
|
|
|
|
h.exampleJSONGenerator,
|
|
|
|
|
|
"", // 使用默认路径
|
|
|
|
|
|
)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
h.logger.Error("生成ZIP文件失败", zap.Error(err), zap.String("product_id", req.ProductID))
|
2026-01-14 15:59:15 +08:00
|
|
|
|
|
|
|
|
|
|
// 检查是否是超时错误
|
|
|
|
|
|
if ctx.Err() == context.DeadlineExceeded {
|
|
|
|
|
|
h.logger.Error("ZIP文件生成超时", zap.String("product_id", req.ProductID))
|
|
|
|
|
|
c.JSON(http.StatusRequestTimeout, gin.H{
|
|
|
|
|
|
"code": 504,
|
|
|
|
|
|
"message": "文件生成超时,请稍后重试",
|
|
|
|
|
|
"error": "请求处理时间过长",
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-19 17:05:09 +08:00
|
|
|
|
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 {
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// 返回符合前端响应拦截器期望的格式
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"data": CheckDownloadAvailabilityResponse{
|
|
|
|
|
|
CanDownload: false,
|
|
|
|
|
|
IsPackage: false,
|
|
|
|
|
|
Message: "只有组合包产品才能下载示例报告",
|
|
|
|
|
|
},
|
|
|
|
|
|
"message": "检查下载可用性成功",
|
2025-12-19 17:05:09 +08:00
|
|
|
|
})
|
|
|
|
|
|
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 {
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// 返回符合前端响应拦截器期望的格式
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"data": CheckDownloadAvailabilityResponse{
|
|
|
|
|
|
CanDownload: false,
|
|
|
|
|
|
IsPackage: true,
|
|
|
|
|
|
Message: "组合包没有子产品",
|
|
|
|
|
|
},
|
|
|
|
|
|
"message": "检查下载可用性成功",
|
2025-12-19 17:05:09 +08:00
|
|
|
|
})
|
|
|
|
|
|
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目录存在
|
2025-12-22 18:32:34 +08:00
|
|
|
|
_, _, err := h.exampleJSONGenerator.MatchSubProductCodeToPath(c.Request.Context(), productCode)
|
2025-12-19 17:05:09 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
missingSubProducts = append(missingSubProducts, productCode)
|
|
|
|
|
|
allExist = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
canDownload := allExist && len(missingSubProducts) == 0
|
|
|
|
|
|
message := "可以下载"
|
|
|
|
|
|
if !canDownload {
|
|
|
|
|
|
message = fmt.Sprintf("以下子产品的UI组件不存在: %v", missingSubProducts)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// 返回符合前端响应拦截器期望的格式
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"data": CheckDownloadAvailabilityResponse{
|
|
|
|
|
|
CanDownload: canDownload,
|
|
|
|
|
|
IsPackage: true,
|
|
|
|
|
|
AllSubProductsExist: allExist,
|
|
|
|
|
|
MissingSubProducts: missingSubProducts,
|
|
|
|
|
|
Message: message,
|
|
|
|
|
|
},
|
|
|
|
|
|
"message": "检查下载可用性成功",
|
2025-12-19 17:05:09 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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"`
|
2025-12-22 18:32:34 +08:00
|
|
|
|
Price string `json:"price"` // UI组件价格
|
2025-12-19 17:05:09 +08:00
|
|
|
|
DownloadedProductCodes []string `json:"downloaded_product_codes"`
|
|
|
|
|
|
CanDownload bool `json:"can_download"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// SubProductPriceInfo 子产品信息
|
2025-12-19 17:05:09 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// 获取用户已购买的产品编号列表
|
|
|
|
|
|
purchasedCodes, err := h.purchaseOrderRepo.GetUserPurchasedProductCodes(c.Request.Context(), userID)
|
2025-12-19 17:05:09 +08:00
|
|
|
|
if err != nil {
|
2025-12-22 18:32:34 +08:00
|
|
|
|
h.logger.Warn("获取用户已购买产品编号失败", zap.Error(err), zap.String("user_id", userID))
|
|
|
|
|
|
purchasedCodes = []string{}
|
2025-12-19 17:05:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// 创建已购买编号的map用于快速查找
|
|
|
|
|
|
purchasedMap := make(map[string]bool)
|
|
|
|
|
|
for _, code := range purchasedCodes {
|
|
|
|
|
|
purchasedMap[code] = true
|
2025-12-19 17:05:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// 使用产品的UIComponentPrice作为价格
|
|
|
|
|
|
finalPrice := product.UIComponentPrice
|
|
|
|
|
|
// h.logger.Info("使用UI组件价格",
|
|
|
|
|
|
// zap.String("product_id", productID),
|
|
|
|
|
|
// zap.String("product_ui_component_price", finalPrice.String()),
|
|
|
|
|
|
// )
|
|
|
|
|
|
|
|
|
|
|
|
// 准备子产品信息列表(仅用于展示,不参与价格计算)
|
2025-12-19 17:05:09 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// 检查是否已购买
|
|
|
|
|
|
isPurchased := purchasedMap[productCode]
|
2025-12-19 17:05:09 +08:00
|
|
|
|
|
|
|
|
|
|
subProducts = append(subProducts, SubProductPriceInfo{
|
|
|
|
|
|
ProductID: subProduct.ID,
|
|
|
|
|
|
ProductCode: productCode,
|
|
|
|
|
|
ProductName: productName,
|
2025-12-22 18:32:34 +08:00
|
|
|
|
IsDownloaded: isPurchased, // 修改字段名但保持功能
|
2025-12-19 17:05:09 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// 检查用户是否有已支付的购买记录(针对当前产品)
|
|
|
|
|
|
// 使用购买订单状态来判断支付状态
|
2025-12-19 17:05:09 +08:00
|
|
|
|
hasPaidDownload := false
|
2025-12-22 18:32:34 +08:00
|
|
|
|
orders, _, err := h.purchaseOrderRepo.GetByUserID(c.Request.Context(), userID, 100, 0)
|
2025-12-19 17:05:09 +08:00
|
|
|
|
if err == nil {
|
2025-12-22 18:32:34 +08:00
|
|
|
|
for _, order := range orders {
|
|
|
|
|
|
if order.ProductID == productID && order.Status == finance_entities.PurchaseOrderStatusPaid {
|
2025-12-19 17:05:09 +08:00
|
|
|
|
hasPaidDownload = true
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果可以下载:价格为0(免费)或者用户已支付
|
|
|
|
|
|
canDownload := finalPrice.IsZero() || hasPaidDownload
|
|
|
|
|
|
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// 返回符合前端响应拦截器期望的格式
|
|
|
|
|
|
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": "获取下载信息成功",
|
2025-12-19 17:05:09 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// 使用产品的UIComponentPrice作为价格
|
|
|
|
|
|
finalPrice := product.UIComponentPrice
|
|
|
|
|
|
// h.logger.Info("使用UI组件价格创建支付订单",
|
|
|
|
|
|
// zap.String("product_id", productID),
|
|
|
|
|
|
// zap.String("product_ui_component_price", finalPrice.String()),
|
|
|
|
|
|
// )
|
|
|
|
|
|
|
|
|
|
|
|
// 准备子产品信息列表(仅用于展示)
|
2025-12-19 17:05:09 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// 收集所有子产品信息
|
2025-12-19 17:05:09 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// 生成商户订单号
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-19 17:05:09 +08:00
|
|
|
|
// 创建下载记录
|
|
|
|
|
|
download := &entities.ComponentReportDownload{
|
|
|
|
|
|
UserID: userID,
|
|
|
|
|
|
ProductID: productID,
|
|
|
|
|
|
ProductCode: product.Code,
|
2025-12-22 18:32:34 +08:00
|
|
|
|
ProductName: product.Name,
|
2025-12-19 17:05:09 +08:00
|
|
|
|
SubProductIDs: string(subProductIDsJSON),
|
|
|
|
|
|
SubProductCodes: string(subProductCodesJSON),
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// 关联购买订单ID
|
2026-01-14 15:59:15 +08:00
|
|
|
|
OrderID: &createdPurchaseOrder.ID,
|
|
|
|
|
|
OrderNumber: &outTradeNo,
|
|
|
|
|
|
DownloadPrice: finalPrice, // 设置下载价格
|
2025-12-19 17:05:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 记录创建前的详细信息用于调试
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// 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)
|
2025-12-19 17:05:09 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// 步骤2: 创建充值记录和支付订单记录
|
2025-12-19 17:05:09 +08:00
|
|
|
|
var rechargeRecord finance_entities.RechargeRecord
|
|
|
|
|
|
if req.PaymentType == "alipay" {
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// h.logger.Info("步骤2: 创建支付宝充值记录",
|
|
|
|
|
|
// zap.String("out_trade_no", outTradeNo),
|
|
|
|
|
|
// zap.String("user_id", userID),
|
|
|
|
|
|
// zap.String("amount", finalPrice.String()),
|
|
|
|
|
|
// zap.String("notes", notes),
|
|
|
|
|
|
// )
|
2025-12-19 17:05:09 +08:00
|
|
|
|
// 使用带备注的工厂方法创建充值记录
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// h.logger.Info("步骤3: 创建支付宝订单记录",
|
|
|
|
|
|
// zap.String("recharge_id", rechargeRecord.ID),
|
|
|
|
|
|
// zap.String("out_trade_no", outTradeNo),
|
|
|
|
|
|
// zap.String("subject", subject),
|
|
|
|
|
|
// )
|
2025-12-19 17:05:09 +08:00
|
|
|
|
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 {
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// h.logger.Info("步骤2: 创建微信充值记录",
|
|
|
|
|
|
// zap.String("out_trade_no", outTradeNo),
|
|
|
|
|
|
// zap.String("user_id", userID),
|
|
|
|
|
|
// zap.String("amount", finalPrice.String()),
|
|
|
|
|
|
// zap.String("notes", notes),
|
|
|
|
|
|
// )
|
2025-12-19 17:05:09 +08:00
|
|
|
|
// 使用带备注的工厂方法创建充值记录
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// h.logger.Info("步骤3: 创建微信订单记录",
|
|
|
|
|
|
// zap.String("recharge_id", rechargeRecord.ID),
|
|
|
|
|
|
// zap.String("out_trade_no", outTradeNo),
|
|
|
|
|
|
// zap.String("subject", subject),
|
|
|
|
|
|
// )
|
2025-12-19 17:05:09 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新下载记录的支付订单号
|
2025-12-22 18:32:34 +08:00
|
|
|
|
download.OrderNumber = &outTradeNo
|
|
|
|
|
|
err = h.componentReportRepo.UpdateDownload(c.Request.Context(), download)
|
2025-12-19 17:05:09 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
h.logger.Error("更新下载记录支付订单号失败", zap.Error(err))
|
|
|
|
|
|
// 不阻断流程,继续执行
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// h.logger.Info("步骤3: 调用支付接口创建订单",
|
|
|
|
|
|
// zap.String("out_trade_no", outTradeNo),
|
|
|
|
|
|
// zap.String("platform", platform),
|
|
|
|
|
|
// )
|
2025-12-19 17:05:09 +08:00
|
|
|
|
|
|
|
|
|
|
// 调用支付服务创建订单
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// h.logger.Info("步骤4: 支付宝订单创建成功",
|
|
|
|
|
|
// zap.String("out_trade_no", outTradeNo),
|
|
|
|
|
|
// zap.String("pay_url", payURL),
|
|
|
|
|
|
// )
|
2025-12-19 17:05:09 +08:00
|
|
|
|
} 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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// h.logger.Info("步骤4: 微信订单创建成功",
|
|
|
|
|
|
// zap.String("out_trade_no", outTradeNo),
|
|
|
|
|
|
// zap.String("code_url", codeURL),
|
|
|
|
|
|
// )
|
2025-12-19 17:05:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
response := CreatePaymentOrderResponse{
|
2025-12-22 18:32:34 +08:00
|
|
|
|
OrderID: createdPurchaseOrder.ID,
|
2025-12-19 17:05:09 +08:00
|
|
|
|
PaymentType: req.PaymentType,
|
|
|
|
|
|
Amount: finalPrice.String(),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if req.PaymentType == "wechat" {
|
|
|
|
|
|
response.CodeURL = codeURL
|
|
|
|
|
|
} else {
|
|
|
|
|
|
response.PayURL = payURL
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// 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),
|
|
|
|
|
|
// )
|
2025-12-19 17:05:09 +08:00
|
|
|
|
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// 返回符合前端响应拦截器期望的格式
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"data": response,
|
|
|
|
|
|
"message": "创建支付订单成功",
|
|
|
|
|
|
})
|
2025-12-19 17:05:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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,主动查询支付订单状态
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// 注意:这里我们检查OrderNumber字段
|
|
|
|
|
|
if download.OrderNumber != nil {
|
|
|
|
|
|
// 兼容旧的支付订单逻辑
|
|
|
|
|
|
outTradeNo := *download.OrderNumber
|
2025-12-19 17:05:09 +08:00
|
|
|
|
h.logger.Info("订单状态为pending,主动查询支付订单状态",
|
|
|
|
|
|
zap.String("order_id", orderID),
|
|
|
|
|
|
zap.String("out_trade_no", outTradeNo),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// 简化处理:如果有支付订单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))
|
2025-12-19 17:05:09 +08:00
|
|
|
|
}
|
2025-12-22 18:32:34 +08:00
|
|
|
|
}
|
2025-12-19 17:05:09 +08:00
|
|
|
|
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// 使用支付订单状态来判断支付状态
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
2025-12-19 17:05:09 +08:00
|
|
|
|
}
|
2025-12-22 18:32:34 +08:00
|
|
|
|
} else if download.OrderNumber != nil {
|
|
|
|
|
|
// 兼容旧的支付订单逻辑
|
|
|
|
|
|
paymentStatus = "success" // 简化处理,有支付订单号就认为已支付
|
|
|
|
|
|
canDownload = true
|
|
|
|
|
|
} else {
|
|
|
|
|
|
paymentStatus = "pending"
|
|
|
|
|
|
canDownload = false
|
2025-12-19 17:05:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// 检查是否过期
|
|
|
|
|
|
if download.IsExpired() {
|
|
|
|
|
|
canDownload = false
|
|
|
|
|
|
}
|
2025-12-19 17:05:09 +08:00
|
|
|
|
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// 返回符合前端响应拦截器期望的格式
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"data": CheckPaymentStatusResponse{
|
|
|
|
|
|
OrderID: download.ID,
|
|
|
|
|
|
PaymentStatus: paymentStatus,
|
|
|
|
|
|
CanDownload: canDownload,
|
|
|
|
|
|
},
|
|
|
|
|
|
"message": "查询支付状态成功",
|
2025-12-19 17:05:09 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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"
|
|
|
|
|
|
}
|
2025-12-22 18:32:34 +08:00
|
|
|
|
|
|
|
|
|
|
// 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. 创建下载记录
|
|
|
|
|
|
download := &entities.ComponentReportDownload{
|
2026-01-14 15:59:15 +08:00
|
|
|
|
UserID: userID,
|
|
|
|
|
|
ProductID: productID,
|
|
|
|
|
|
ProductCode: product.Code,
|
|
|
|
|
|
ProductName: product.Name,
|
|
|
|
|
|
OrderID: &validOrder.ID, // 添加OrderID字段
|
|
|
|
|
|
OrderNumber: &validOrder.OrderNo, // 使用OrderNumber字段
|
|
|
|
|
|
DownloadPrice: validOrder.Amount, // 设置下载价格(从订单获取)
|
|
|
|
|
|
ExpiresAt: calculateExpiryTime(), // 从创建日起30天
|
2025-12-22 18:32:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
}
|
2025-12-24 17:50:33 +08:00
|
|
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建下载记录
|
|
|
|
|
|
download := &entities.ComponentReportDownload{
|
2026-01-14 15:59:15 +08:00
|
|
|
|
UserID: order.UserID,
|
|
|
|
|
|
ProductID: order.ProductID,
|
|
|
|
|
|
ProductCode: order.ProductCode,
|
|
|
|
|
|
ProductName: order.ProductName,
|
|
|
|
|
|
OrderID: &order.ID,
|
|
|
|
|
|
OrderNumber: &order.OrderNo,
|
|
|
|
|
|
DownloadPrice: order.Amount, // 设置下载价格(从订单获取)
|
|
|
|
|
|
ExpiresAt: calculateExpiryTime(),
|
2025-12-24 17:50:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果是组合包,获取子产品信息
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|