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