Files
tyapi-server/internal/shared/component_report/handler.go
2025-12-19 17:05:09 +08:00

1344 lines
43 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"
"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
productRepo repositories.ProductRepository
componentReportRepo repositories.ComponentReportRepository
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,
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)
return &ComponentReportHandler{
exampleJSONGenerator: exampleJSONGenerator,
zipGenerator: zipGenerator,
productRepo: productRepo,
componentReportRepo: componentReportRepo,
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, GenerateExampleJSONResponse{
ProductID: req.ProductID,
JSONContent: string(jsonData),
JSONSize: len(jsonData),
})
}
// 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{
"code": 200,
"message": "ZIP文件生成成功",
"zip_path": zipPath,
"file_size": fileInfo.Size(),
"file_name": filepath.Base(zipPath),
})
}
// 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
}
// 生成 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
}
// 验证用户是否已支付
// 检查用户是否有已支付的下载记录
downloads, err := h.componentReportRepo.GetUserDownloads(c.Request.Context(), userID, &req.ProductID)
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
}
// 检查是否有已支付的下载记录
hasPaid := false
for _, download := range downloads {
if download.PaymentStatus == "success" && !download.IsExpired() {
hasPaid = true
break
}
}
if !hasPaid {
h.logger.Warn("用户未支付,拒绝下载",
zap.String("user_id", userID),
zap.String("product_id", req.ProductID),
)
c.JSON(http.StatusForbidden, gin.H{
"code": 403,
"message": "请先支付后再下载",
})
return
}
// 生成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, CheckDownloadAvailabilityResponse{
CanDownload: false,
IsPackage: false,
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, CheckDownloadAvailabilityResponse{
CanDownload: false,
IsPackage: true,
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.MatchProductCodeToPath(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, CheckDownloadAvailabilityResponse{
CanDownload: canDownload,
IsPackage: true,
AllSubProductsExist: allExist,
MissingSubProducts: missingSubProducts,
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"`
OriginalTotalPrice string `json:"original_total_price"`
DiscountAmount string `json:"discount_amount"`
FinalPrice string `json:"final_price"`
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"`
Price string `json:"price"`
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
}
// 获取用户已下载的产品编号列表
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
}
// 计算价格
originalTotal := decimal.Zero
discountAmount := decimal.Zero
var subProducts []SubProductPriceInfo
for _, item := range packageItems {
var subProduct entities.Product
var productCode string
var productName string
var price decimal.Decimal
if item.Product != nil {
subProduct = *item.Product
productCode = subProduct.Code
productName = subProduct.Name
price = subProduct.Price
} else {
// 如果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
price = subProduct.Price
}
if productCode == "" {
continue
}
// 检查是否已下载
isDownloaded := downloadedMap[productCode]
originalTotal = originalTotal.Add(price)
if isDownloaded {
discountAmount = discountAmount.Add(price)
}
subProducts = append(subProducts, SubProductPriceInfo{
ProductID: subProduct.ID,
ProductCode: productCode,
ProductName: productName,
Price: price.String(),
IsDownloaded: isDownloaded,
})
}
finalPrice := originalTotal.Sub(discountAmount)
// ========== 测试阶段强制设置支付金额为0.01 ==========
// TODO: 测试完成后请删除或注释掉以下3行代码恢复原始价格计算
if finalPrice.GreaterThan(decimal.Zero) {
finalPrice = decimal.NewFromFloat(0.01)
}
// ====================================================
// 检查用户是否有已支付的下载记录(针对当前产品)
// can_download 应该基于实际支付状态,而不是价格
hasPaidDownload := false
downloads, err := h.componentReportRepo.GetUserDownloads(c.Request.Context(), userID, &productID)
if err == nil {
for _, download := range downloads {
if download.PaymentStatus == "success" && !download.IsExpired() {
hasPaidDownload = true
break
}
}
}
// 如果可以下载价格为0免费或者用户已支付
canDownload := finalPrice.IsZero() || hasPaidDownload
c.JSON(http.StatusOK, GetDownloadInfoResponse{
ProductID: productID,
ProductCode: product.Code,
ProductName: product.Name,
IsPackage: true,
SubProducts: subProducts,
OriginalTotalPrice: originalTotal.String(),
DiscountAmount: discountAmount.String(),
FinalPrice: finalPrice.String(),
DownloadedProductCodes: downloadedCodes,
CanDownload: canDownload,
})
}
// 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
}
// 计算价格
originalTotal := decimal.Zero
discountAmount := decimal.Zero
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 !downloadedMap[productCode] {
originalTotal = originalTotal.Add(subProduct.Price)
} else {
discountAmount = discountAmount.Add(subProduct.Price)
}
}
finalPrice := originalTotal.Sub(discountAmount)
// ========== 测试阶段强制设置支付金额为0.01 ==========
// TODO: 测试完成后请删除或注释掉以下2行代码恢复原始价格计算
if finalPrice.GreaterThan(decimal.Zero) {
finalPrice = decimal.NewFromFloat(0.01)
}
// ====================================================
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
}
// 创建下载记录
download := &entities.ComponentReportDownload{
UserID: userID,
ProductID: productID,
ProductCode: product.Code,
SubProductIDs: string(subProductIDsJSON),
SubProductCodes: string(subProductCodesJSON),
DownloadPrice: finalPrice,
OriginalPrice: originalTotal,
DiscountAmount: discountAmount,
PaymentStatus: "pending",
PaymentType: &req.PaymentType,
}
// 记录创建前的详细信息用于调试
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("original_price", originalTotal.String()),
zap.String("discount_amount", discountAmount.String()),
zap.Int("sub_product_count", len(subProductCodes)),
)
createdDownload, err := h.componentReportRepo.CreateDownload(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
}
// 生成商户订单号
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("download_id", createdDownload.ID),
zap.String("out_trade_no", outTradeNo),
zap.String("payment_type", req.PaymentType),
zap.String("amount", finalPrice.String()),
zap.String("product_name", product.Name),
)
// 创建充值记录和支付订单记录
var rechargeRecord finance_entities.RechargeRecord
if req.PaymentType == "alipay" {
h.logger.Info("步骤1: 创建支付宝充值记录",
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("步骤2: 创建支付宝订单记录",
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("步骤1: 创建微信充值记录",
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("步骤2: 创建微信订单记录",
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
}
}
// 更新下载记录的支付订单号
createdDownload.PaymentOrderID = &outTradeNo
err = h.componentReportRepo.UpdateDownload(c.Request.Context(), createdDownload)
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: createdDownload.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", createdDownload.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, response)
}
// 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主动查询支付订单状态
if download.PaymentStatus == "pending" && download.PaymentOrderID != nil {
outTradeNo := *download.PaymentOrderID
h.logger.Info("订单状态为pending主动查询支付订单状态",
zap.String("order_id", orderID),
zap.String("out_trade_no", outTradeNo),
zap.String("payment_type", func() string {
if download.PaymentType != nil {
return *download.PaymentType
}
return "unknown"
}()),
)
// 根据支付类型查询订单状态
if download.PaymentType != nil && *download.PaymentType == "wechat" {
// 查询微信订单状态
wechatOrder, err := h.wechatOrderRepo.GetByOutTradeNo(c.Request.Context(), outTradeNo)
if err == nil && wechatOrder != nil {
// 如果订单状态为pending主动查询微信订单状态
if wechatOrder.Status == finance_entities.WechatOrderStatusPending {
h.logger.Info("微信订单状态为pending主动查询微信支付状态",
zap.String("out_trade_no", outTradeNo),
)
// 调用微信查询接口
transaction, err := h.wechatPayService.QueryOrderStatus(c.Request.Context(), outTradeNo)
if err == nil && transaction != nil {
tradeState := ""
transactionID := ""
if transaction.TradeState != nil {
tradeState = *transaction.TradeState
}
if transaction.TransactionId != nil {
transactionID = *transaction.TransactionId
}
h.logger.Info("微信查询订单状态返回",
zap.String("out_trade_no", outTradeNo),
zap.String("trade_state", tradeState),
zap.String("transaction_id", transactionID),
)
// 如果支付成功,更新订单状态(这里需要调用 FinanceApplicationService 的方法)
// 由于没有直接访问 FinanceApplicationService我们只更新下载记录状态
if tradeState == "SUCCESS" {
h.logger.Info("检测到微信支付成功,更新下载记录状态",
zap.String("out_trade_no", outTradeNo),
zap.String("download_id", download.ID),
)
// 更新微信订单状态
payAmount := decimal.Zero
if transaction.Amount != nil && transaction.Amount.Total != nil {
payAmount = decimal.NewFromInt(*transaction.Amount.Total).Div(decimal.NewFromInt(100))
}
wechatOrder.MarkSuccess(transactionID, "", "", payAmount, payAmount)
err = h.wechatOrderRepo.Update(c.Request.Context(), *wechatOrder)
if err != nil {
h.logger.Error("更新微信订单状态失败", zap.Error(err))
}
// 更新下载记录状态
download.PaymentStatus = "success"
// 设置过期时间30天后
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))
} else {
h.logger.Info("下载记录状态更新成功",
zap.String("download_id", download.ID),
zap.String("payment_status", download.PaymentStatus),
)
}
}
}
} else if wechatOrder.Status == finance_entities.WechatOrderStatusSuccess {
// 订单已成功,但下载记录状态未更新,更新下载记录
if download.PaymentStatus != "success" {
h.logger.Info("微信订单已成功,但下载记录状态未更新,更新下载记录",
zap.String("out_trade_no", outTradeNo),
zap.String("download_id", download.ID),
)
download.PaymentStatus = "success"
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))
}
}
}
}
} else if download.PaymentType != nil && *download.PaymentType == "alipay" {
// 查询支付宝订单状态
alipayOrder, err := h.alipayOrderRepo.GetByOutTradeNo(c.Request.Context(), outTradeNo)
if err == nil && alipayOrder != nil {
// 如果订单状态为pending主动查询支付宝订单状态
if alipayOrder.Status == finance_entities.AlipayOrderStatusPending {
h.logger.Info("支付宝订单状态为pending主动查询支付宝支付状态",
zap.String("out_trade_no", outTradeNo),
)
// 调用支付宝查询接口
alipayResp, err := h.aliPayService.QueryOrderStatus(c.Request.Context(), outTradeNo)
if err == nil && alipayResp != nil {
alipayStatus := alipayResp.TradeStatus
h.logger.Info("支付宝查询订单状态返回",
zap.String("out_trade_no", outTradeNo),
zap.String("trade_status", string(alipayStatus)),
zap.String("trade_no", alipayResp.TradeNo),
)
// 如果支付成功,更新订单状态
if alipayStatus == "TRADE_SUCCESS" || alipayStatus == "TRADE_FINISHED" {
h.logger.Info("检测到支付宝支付成功,更新下载记录状态",
zap.String("out_trade_no", outTradeNo),
zap.String("download_id", download.ID),
)
// 更新支付宝订单状态
amount, _ := decimal.NewFromString(alipayResp.TotalAmount)
alipayOrder.MarkSuccess(alipayResp.TradeNo, "", "", amount, amount)
err = h.alipayOrderRepo.Update(c.Request.Context(), *alipayOrder)
if err != nil {
h.logger.Error("更新支付宝订单状态失败", zap.Error(err))
}
// 更新下载记录状态
download.PaymentStatus = "success"
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))
} else {
h.logger.Info("下载记录状态更新成功",
zap.String("download_id", download.ID),
zap.String("payment_status", download.PaymentStatus),
)
}
}
}
} else if alipayOrder.Status == finance_entities.AlipayOrderStatusSuccess {
// 订单已成功,但下载记录状态未更新,更新下载记录
if download.PaymentStatus != "success" {
h.logger.Info("支付宝订单已成功,但下载记录状态未更新,更新下载记录",
zap.String("out_trade_no", outTradeNo),
zap.String("download_id", download.ID),
)
download.PaymentStatus = "success"
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))
}
}
}
}
}
// 重新获取更新后的下载记录
updatedDownload, err := h.componentReportRepo.GetDownloadByID(c.Request.Context(), orderID)
if err == nil && updatedDownload != nil {
download = updatedDownload
}
}
// 返回支付状态
canDownload := download.PaymentStatus == "success" && !download.IsExpired()
c.JSON(http.StatusOK, CheckPaymentStatusResponse{
OrderID: download.ID,
PaymentStatus: download.PaymentStatus,
CanDownload: canDownload,
})
}
// 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"
}