Files
tyapi-server/internal/shared/component_report/handler.go

1344 lines
43 KiB
Go
Raw Normal View History

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