diff --git a/internal/application/product/subscription_application_service.go b/internal/application/product/subscription_application_service.go index 620078f..0d3f6dd 100644 --- a/internal/application/product/subscription_application_service.go +++ b/internal/application/product/subscription_application_service.go @@ -20,6 +20,7 @@ type SubscriptionApplicationService interface { // 我的订阅(用户专用) ListMySubscriptions(ctx context.Context, userID string, query *queries.ListSubscriptionsQuery) (*responses.SubscriptionListResponse, error) GetMySubscriptionStats(ctx context.Context, userID string) (*responses.SubscriptionStatsResponse, error) + CancelMySubscription(ctx context.Context, userID string, subscriptionID string) error // 业务查询 GetUserSubscriptions(ctx context.Context, query *queries.GetUserSubscriptionsQuery) ([]*responses.SubscriptionInfoResponse, error) diff --git a/internal/application/product/subscription_application_service_impl.go b/internal/application/product/subscription_application_service_impl.go index 7d762dd..f687b37 100644 --- a/internal/application/product/subscription_application_service_impl.go +++ b/internal/application/product/subscription_application_service_impl.go @@ -304,6 +304,38 @@ func (s *SubscriptionApplicationServiceImpl) GetMySubscriptionStats(ctx context. }, nil } +// CancelMySubscription 取消我的订阅 +// 业务流程:1. 验证订阅是否属于当前用户 2. 取消订阅 +func (s *SubscriptionApplicationServiceImpl) CancelMySubscription(ctx context.Context, userID string, subscriptionID string) error { + // 1. 获取订阅信息 + subscription, err := s.productSubscriptionService.GetSubscriptionByID(ctx, subscriptionID) + if err != nil { + s.logger.Error("获取订阅信息失败", zap.String("subscription_id", subscriptionID), zap.Error(err)) + return fmt.Errorf("订阅不存在") + } + + // 2. 验证订阅是否属于当前用户 + if subscription.UserID != userID { + s.logger.Warn("用户尝试取消不属于自己的订阅", + zap.String("user_id", userID), + zap.String("subscription_id", subscriptionID), + zap.String("subscription_user_id", subscription.UserID)) + return fmt.Errorf("无权取消此订阅") + } + + // 3. 取消订阅(软删除) + if err := s.productSubscriptionService.CancelSubscription(ctx, subscriptionID); err != nil { + s.logger.Error("取消订阅失败", zap.String("subscription_id", subscriptionID), zap.Error(err)) + return fmt.Errorf("取消订阅失败: %w", err) + } + + s.logger.Info("用户取消订阅成功", + zap.String("user_id", userID), + zap.String("subscription_id", subscriptionID)) + + return nil +} + // convertToSubscriptionInfoResponse 转换为订阅信息响应 func (s *SubscriptionApplicationServiceImpl) convertToSubscriptionInfoResponse(subscription *entities.Subscription) *responses.SubscriptionInfoResponse { // 查询用户信息 diff --git a/internal/infrastructure/database/repositories/api/gorm_api_call_repository.go b/internal/infrastructure/database/repositories/api/gorm_api_call_repository.go index 847908b..d6e135b 100644 --- a/internal/infrastructure/database/repositories/api/gorm_api_call_repository.go +++ b/internal/infrastructure/database/repositories/api/gorm_api_call_repository.go @@ -14,7 +14,7 @@ import ( ) const ( - ApiCallsTable = "api_calls" + ApiCallsTable = "api_calls" ApiCallCacheTTL = 10 * time.Minute ) @@ -212,7 +212,7 @@ func (r *GormApiCallRepository) ListByUserIdWithFiltersAndProductName(ctx contex // 转换为entities.ApiCall并构建产品名称映射 var calls []*entities.ApiCall productNameMap := make(map[string]string) - + for _, c := range callsWithProduct { call := c.ApiCall calls = append(calls, &call) @@ -237,7 +237,7 @@ func (r *GormApiCallRepository) CountByUserIdAndDateRange(ctx context.Context, u // GetDailyStatsByUserId 获取用户每日API调用统计 func (r *GormApiCallRepository) GetDailyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error) { var results []map[string]interface{} - + // 构建SQL查询 - 使用PostgreSQL语法,使用具体的日期范围 sql := ` SELECT @@ -250,19 +250,19 @@ func (r *GormApiCallRepository) GetDailyStatsByUserId(ctx context.Context, userI GROUP BY DATE(created_at) ORDER BY date ASC ` - + err := r.GetDB(ctx).Raw(sql, userId, startDate.Format("2006-01-02"), endDate.Format("2006-01-02")).Scan(&results).Error if err != nil { return nil, err } - + return results, nil } // GetMonthlyStatsByUserId 获取用户每月API调用统计 func (r *GormApiCallRepository) GetMonthlyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error) { var results []map[string]interface{} - + // 构建SQL查询 - 使用PostgreSQL语法,使用具体的日期范围 sql := ` SELECT @@ -275,12 +275,12 @@ func (r *GormApiCallRepository) GetMonthlyStatsByUserId(ctx context.Context, use GROUP BY TO_CHAR(created_at, 'YYYY-MM') ORDER BY month ASC ` - + err := r.GetDB(ctx).Raw(sql, userId, startDate, endDate).Scan(&results).Error if err != nil { return nil, err } - + return results, nil } @@ -332,6 +332,12 @@ func (r *GormApiCallRepository) ListWithFiltersAndProductName(ctx context.Contex whereArgs = append(whereArgs, "%"+productName+"%") } + // 企业名称筛选 + if companyName, ok := filters["company_name"].(string); ok && companyName != "" { + whereCondition += " AND ei.company_name LIKE ?" + whereArgs = append(whereArgs, "%"+companyName+"%") + } + // 状态筛选 if status, ok := filters["status"].(string); ok && status != "" { whereCondition += " AND ac.status = ?" @@ -340,9 +346,12 @@ func (r *GormApiCallRepository) ListWithFiltersAndProductName(ctx context.Contex } // 构建JOIN查询 + // 需要JOIN product表获取产品名称,JOIN users和enterprise_infos表获取企业名称 query := r.GetDB(ctx).Table("api_calls ac"). Select("ac.*, p.name as product_name"). Joins("LEFT JOIN product p ON ac.product_id = p.id"). + Joins("LEFT JOIN users u ON ac.user_id = u.id"). + Joins("LEFT JOIN enterprise_infos ei ON u.id = ei.user_id"). Where(whereCondition, whereArgs...) // 获取总数 @@ -374,7 +383,7 @@ func (r *GormApiCallRepository) ListWithFiltersAndProductName(ctx context.Contex // 转换为entities.ApiCall并构建产品名称映射 var calls []*entities.ApiCall productNameMap := make(map[string]string) - + for _, c := range callsWithProduct { call := c.ApiCall calls = append(calls, &call) @@ -406,7 +415,7 @@ func (r *GormApiCallRepository) GetSystemCallsByDateRange(ctx context.Context, s // GetSystemDailyStats 获取系统每日API调用统计 func (r *GormApiCallRepository) GetSystemDailyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) { var results []map[string]interface{} - + sql := ` SELECT DATE(created_at) as date, @@ -417,19 +426,19 @@ func (r *GormApiCallRepository) GetSystemDailyStats(ctx context.Context, startDa GROUP BY DATE(created_at) ORDER BY date ASC ` - + err := r.GetDB(ctx).Raw(sql, startDate.Format("2006-01-02"), endDate.Format("2006-01-02")).Scan(&results).Error if err != nil { return nil, err } - + return results, nil } // GetSystemMonthlyStats 获取系统每月API调用统计 func (r *GormApiCallRepository) GetSystemMonthlyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) { var results []map[string]interface{} - + sql := ` SELECT TO_CHAR(created_at, 'YYYY-MM') as month, @@ -440,12 +449,12 @@ func (r *GormApiCallRepository) GetSystemMonthlyStats(ctx context.Context, start GROUP BY TO_CHAR(created_at, 'YYYY-MM') ORDER BY month ASC ` - + err := r.GetDB(ctx).Raw(sql, startDate, endDate).Scan(&results).Error if err != nil { return nil, err } - + return results, nil } @@ -453,7 +462,7 @@ func (r *GormApiCallRepository) GetSystemMonthlyStats(ctx context.Context, start func (r *GormApiCallRepository) GetApiPopularityRanking(ctx context.Context, period string, limit int) ([]map[string]interface{}, error) { var sql string var args []interface{} - + switch period { case "today": sql = ` @@ -508,12 +517,12 @@ func (r *GormApiCallRepository) GetApiPopularityRanking(ctx context.Context, per default: return nil, fmt.Errorf("不支持的时间周期: %s", period) } - + var results []map[string]interface{} err := r.GetDB(ctx).Raw(sql, args...).Scan(&results).Error if err != nil { return nil, err } - + return results, nil -} \ No newline at end of file +} diff --git a/internal/infrastructure/database/repositories/finance/gorm_wallet_transaction_repository.go b/internal/infrastructure/database/repositories/finance/gorm_wallet_transaction_repository.go index b67084a..8cb7b07 100644 --- a/internal/infrastructure/database/repositories/finance/gorm_wallet_transaction_repository.go +++ b/internal/infrastructure/database/repositories/finance/gorm_wallet_transaction_repository.go @@ -179,7 +179,7 @@ func (r *GormWalletTransactionRepository) GetTotalAmountByUserIdAndDateRange(ctx // GetDailyStatsByUserId 获取用户每日消费统计 func (r *GormWalletTransactionRepository) GetDailyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error) { var results []map[string]interface{} - + // 构建SQL查询 - 使用PostgreSQL语法,使用具体的日期范围 sql := ` SELECT @@ -192,19 +192,19 @@ func (r *GormWalletTransactionRepository) GetDailyStatsByUserId(ctx context.Cont GROUP BY DATE(created_at) ORDER BY date ASC ` - + err := r.GetDB(ctx).Raw(sql, userId, startDate.Format("2006-01-02"), endDate.Format("2006-01-02")).Scan(&results).Error if err != nil { return nil, err } - + return results, nil } // GetMonthlyStatsByUserId 获取用户每月消费统计 func (r *GormWalletTransactionRepository) GetMonthlyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error) { var results []map[string]interface{} - + // 构建SQL查询 - 使用PostgreSQL语法,使用具体的日期范围 sql := ` SELECT @@ -217,12 +217,12 @@ func (r *GormWalletTransactionRepository) GetMonthlyStatsByUserId(ctx context.Co GROUP BY TO_CHAR(created_at, 'YYYY-MM') ORDER BY month ASC ` - + err := r.GetDB(ctx).Raw(sql, userId, startDate, endDate).Scan(&results).Error if err != nil { return nil, err } - + return results, nil } @@ -358,7 +358,7 @@ func (r *GormWalletTransactionRepository) ListByUserIdWithFiltersAndProductName( // 转换为entities.WalletTransaction并构建产品名称映射 var transactions []*entities.WalletTransaction productNameMap := make(map[string]string) - + for _, t := range transactionsWithProduct { transaction := t.WalletTransaction transactions = append(transactions, &transaction) @@ -410,6 +410,12 @@ func (r *GormWalletTransactionRepository) ListWithFiltersAndProductName(ctx cont whereArgs = append(whereArgs, "%"+productName+"%") } + // 企业名称筛选 + if companyName, ok := filters["company_name"].(string); ok && companyName != "" { + whereCondition += " AND ei.company_name LIKE ?" + whereArgs = append(whereArgs, "%"+companyName+"%") + } + // 金额范围筛选 if minAmount, ok := filters["min_amount"].(string); ok && minAmount != "" { whereCondition += " AND wt.amount >= ?" @@ -422,9 +428,12 @@ func (r *GormWalletTransactionRepository) ListWithFiltersAndProductName(ctx cont } // 构建JOIN查询 + // 需要JOIN product表获取产品名称,JOIN users和enterprise_infos表获取企业名称 query := r.GetDB(ctx).Table("wallet_transactions wt"). Select("wt.*, p.name as product_name"). Joins("LEFT JOIN product p ON wt.product_id = p.id"). + Joins("LEFT JOIN users u ON wt.user_id = u.id"). + Joins("LEFT JOIN enterprise_infos ei ON u.id = ei.user_id"). Where(whereCondition, whereArgs...) // 获取总数 @@ -456,7 +465,7 @@ func (r *GormWalletTransactionRepository) ListWithFiltersAndProductName(ctx cont // 转换为entities.WalletTransaction并构建产品名称映射 var transactions []*entities.WalletTransaction productNameMap := make(map[string]string) - + for _, t := range transactionsWithProduct { transaction := t.WalletTransaction transactions = append(transactions, &transaction) @@ -575,7 +584,7 @@ func (r *GormWalletTransactionRepository) GetSystemAmountByDateRange(ctx context // GetSystemDailyStats 获取系统每日消费统计 func (r *GormWalletTransactionRepository) GetSystemDailyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) { var results []map[string]interface{} - + sql := ` SELECT DATE(created_at) as date, @@ -586,19 +595,19 @@ func (r *GormWalletTransactionRepository) GetSystemDailyStats(ctx context.Contex GROUP BY DATE(created_at) ORDER BY date ASC ` - + err := r.GetDB(ctx).Raw(sql, startDate.Format("2006-01-02"), endDate.Format("2006-01-02")).Scan(&results).Error if err != nil { return nil, err } - + return results, nil } // GetSystemMonthlyStats 获取系统每月消费统计 func (r *GormWalletTransactionRepository) GetSystemMonthlyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) { var results []map[string]interface{} - + sql := ` SELECT TO_CHAR(created_at, 'YYYY-MM') as month, @@ -609,11 +618,11 @@ func (r *GormWalletTransactionRepository) GetSystemMonthlyStats(ctx context.Cont GROUP BY TO_CHAR(created_at, 'YYYY-MM') ORDER BY month ASC ` - + err := r.GetDB(ctx).Raw(sql, startDate, endDate).Scan(&results).Error if err != nil { return nil, err } - + return results, nil -} \ No newline at end of file +} diff --git a/internal/infrastructure/http/handlers/product_admin_handler.go b/internal/infrastructure/http/handlers/product_admin_handler.go index ba27534..45fa14e 100644 --- a/internal/infrastructure/http/handlers/product_admin_handler.go +++ b/internal/infrastructure/http/handlers/product_admin_handler.go @@ -797,9 +797,9 @@ func (h *ProductAdminHandler) BatchUpdateSubscriptionPrices(c *gin.Context) { } h.responseBuilder.Success(c, map[string]interface{}{ - "user_id": cmd.UserID, + "user_id": cmd.UserID, "discount": cmd.Discount, - "scope": cmd.Scope, + "scope": cmd.Scope, }, "一键改价成功") } @@ -1113,8 +1113,6 @@ func (h *ProductAdminHandler) DeleteProductDocumentation(c *gin.Context) { h.responseBuilder.Success(c, nil, "文档删除成功") } - - // GetAdminWalletTransactions 获取管理端消费记录 // @Summary 获取管理端消费记录 // @Description 管理员获取消费记录,支持筛选和分页 @@ -1173,6 +1171,11 @@ func (h *ProductAdminHandler) GetAdminWalletTransactions(c *gin.Context) { filters["product_name"] = productName } + // 企业名称筛选 + if companyName := c.Query("company_name"); companyName != "" { + filters["company_name"] = companyName + } + // 金额范围筛选 if minAmount := c.Query("min_amount"); minAmount != "" { filters["min_amount"] = minAmount @@ -1475,6 +1478,26 @@ func (h *ProductAdminHandler) GetAdminApiCalls(c *gin.Context) { filters["product_ids"] = productIds } + // 产品名称筛选 + if productName := c.Query("product_name"); productName != "" { + filters["product_name"] = productName + } + + // 企业名称筛选 + if companyName := c.Query("company_name"); companyName != "" { + filters["company_name"] = companyName + } + + // 交易ID筛选 + if transactionId := c.Query("transaction_id"); transactionId != "" { + filters["transaction_id"] = transactionId + } + + // 状态筛选 + if status := c.Query("status"); status != "" { + filters["status"] = status + } + // 时间范围筛选 if startTime := c.Query("start_time"); startTime != "" { if t, err := time.Parse("2006-01-02 15:04:05", startTime); err == nil { diff --git a/internal/infrastructure/http/handlers/product_handler.go b/internal/infrastructure/http/handlers/product_handler.go index 3343068..d19ddba 100644 --- a/internal/infrastructure/http/handlers/product_handler.go +++ b/internal/infrastructure/http/handlers/product_handler.go @@ -582,32 +582,77 @@ func (h *ProductHandler) GetMySubscriptionUsage(c *gin.Context) { return } - // 先获取订阅信息以验证权限 + result, err := h.subAppService.GetSubscriptionUsage(c.Request.Context(), subscriptionID) + if err != nil { + h.logger.Error("获取我的订阅使用情况失败", zap.Error(err), zap.String("user_id", userID), zap.String("subscription_id", subscriptionID)) + h.responseBuilder.NotFound(c, "订阅不存在") + return + } + + // 验证订阅是否属于当前用户(通过获取订阅详情来验证) var query queries.GetSubscriptionQuery query.ID = subscriptionID - subscription, err := h.subAppService.GetSubscriptionByID(c.Request.Context(), &query) if err != nil { - h.logger.Error("获取订阅信息失败", zap.Error(err), zap.String("user_id", userID), zap.String("subscription_id", subscriptionID)) + h.logger.Error("获取订阅详情失败", zap.Error(err), zap.String("subscription_id", subscriptionID)) h.responseBuilder.NotFound(c, "订阅不存在") return } // 验证订阅是否属于当前用户 if subscription.UserID != userID { - h.logger.Error("用户尝试访问不属于自己的订阅使用情况", zap.String("user_id", userID), zap.String("subscription_user_id", subscription.UserID), zap.String("subscription_id", subscriptionID)) + h.logger.Error("用户尝试访问不属于自己的订阅", zap.String("user_id", userID), zap.String("subscription_user_id", subscription.UserID), zap.String("subscription_id", subscriptionID)) h.responseBuilder.Forbidden(c, "无权访问此订阅") return } - usage, err := h.subAppService.GetSubscriptionUsage(c.Request.Context(), subscriptionID) - if err != nil { - h.logger.Error("获取订阅使用情况失败", zap.Error(err)) - h.responseBuilder.BadRequest(c, err.Error()) + h.responseBuilder.Success(c, result, "获取我的订阅使用情况成功") +} + +// CancelMySubscription 取消我的订阅 +// @Summary 取消我的订阅 +// @Description 取消指定的订阅(软删除) +// @Tags 我的订阅 +// @Accept json +// @Produce json +// @Security Bearer +// @Param id path string true "订阅ID" +// @Success 200 {object} map[string]interface{} "取消订阅成功" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 401 {object} map[string]interface{} "未认证" +// @Failure 403 {object} map[string]interface{} "无权操作" +// @Failure 404 {object} map[string]interface{} "订阅不存在" +// @Failure 500 {object} map[string]interface{} "服务器内部错误" +// @Router /api/v1/my/subscriptions/{id}/cancel [post] +func (h *ProductHandler) CancelMySubscription(c *gin.Context) { + userID := c.GetString("user_id") + if userID == "" { + h.responseBuilder.Unauthorized(c, "用户未登录") return } - h.responseBuilder.Success(c, usage, "获取使用情况成功") + subscriptionID := c.Param("id") + if subscriptionID == "" { + h.responseBuilder.BadRequest(c, "订阅ID不能为空") + return + } + + err := h.subAppService.CancelMySubscription(c.Request.Context(), userID, subscriptionID) + if err != nil { + h.logger.Error("取消订阅失败", zap.Error(err), zap.String("user_id", userID), zap.String("subscription_id", subscriptionID)) + + // 根据错误类型返回不同的响应 + if err.Error() == "订阅不存在" { + h.responseBuilder.NotFound(c, "订阅不存在") + } else if err.Error() == "无权取消此订阅" { + h.responseBuilder.Forbidden(c, "无权取消此订阅") + } else { + h.responseBuilder.BadRequest(c, err.Error()) + } + return + } + + h.responseBuilder.Success(c, nil, "取消订阅成功") } // GetProductDocumentation 获取产品文档 diff --git a/internal/infrastructure/http/routes/product_routes.go b/internal/infrastructure/http/routes/product_routes.go index caa3126..6f9f01a 100644 --- a/internal/infrastructure/http/routes/product_routes.go +++ b/internal/infrastructure/http/routes/product_routes.go @@ -83,6 +83,9 @@ func (r *ProductRoutes) Register(router *sharedhttp.GinRouter) { // 获取订阅使用情况 subscriptions.GET("/:id/usage", r.productHandler.GetMySubscriptionUsage) + + // 取消订阅 + subscriptions.POST("/:id/cancel", r.productHandler.CancelMySubscription) } } diff --git a/internal/shared/pdf/font_manager.go b/internal/shared/pdf/font_manager.go index 1a76878..c3a0d16 100644 --- a/internal/shared/pdf/font_manager.go +++ b/internal/shared/pdf/font_manager.go @@ -43,7 +43,7 @@ func (fm *FontManager) LoadChineseFont(pdf *gofpdf.Fpdf) bool { fontPaths := fm.getChineseFontPaths() if len(fontPaths) == 0 { - fm.logger.Warn("未找到中文字体文件") + // 字体文件不存在,使用系统默认字体,不记录警告 return false } @@ -55,7 +55,7 @@ func (fm *FontManager) LoadChineseFont(pdf *gofpdf.Fpdf) bool { } } - fm.logger.Warn("无法加载中文字体文件") + // 无法加载字体,使用系统默认字体,不记录警告 return false } @@ -146,14 +146,17 @@ func (fm *FontManager) getWatermarkFontPaths() []string { func (fm *FontManager) buildFontPaths(fontNames []string) []string { var fontPaths []string - // 方式1: 优先使用相对路径(Linux风格,使用正斜杠)- 最常用 - for _, fontName := range fontNames { - // 相对路径(相对于项目根目录) - relativePaths := []string{ - "internal/shared/pdf/fonts/" + fontName, - "./internal/shared/pdf/fonts/" + fontName, + // 方式1: 服务器绝对路径(Linux环境,优先检查,因为服务器上文件通常在这里) + if runtime.GOOS == "linux" { + commonServerPaths := []string{ + "/www/tyapi-server/internal/shared/pdf/fonts", + "/app/internal/shared/pdf/fonts", + } + for _, basePath := range commonServerPaths { + for _, fontName := range fontNames { + fontPaths = append(fontPaths, filepath.Join(basePath, fontName)) + } } - fontPaths = append(fontPaths, relativePaths...) } // 方式2: 使用 pdf/fonts/ 目录(相对于当前文件) @@ -194,17 +197,13 @@ func (fm *FontManager) buildFontPaths(fontNames []string) []string { } } - // 方式6: 服务器绝对路径(作为最后的后备方案) - if runtime.GOOS == "linux" { - commonServerPaths := []string{ - "/www/tyapi-server/internal/shared/pdf/fonts", - "/app/internal/shared/pdf/fonts", - } - for _, basePath := range commonServerPaths { - for _, fontName := range fontNames { - fontPaths = append(fontPaths, filepath.Join(basePath, fontName)) - } + // 方式6: 相对路径(作为最后的后备方案) + for _, fontName := range fontNames { + relativePaths := []string{ + "internal/shared/pdf/fonts/" + fontName, + "./internal/shared/pdf/fonts/" + fontName, } + fontPaths = append(fontPaths, relativePaths...) } // 过滤出实际存在的字体文件 @@ -216,22 +215,7 @@ func (fm *FontManager) buildFontPaths(fontNames []string) []string { } } - // 只记录关键错误,并显示调试信息 - if len(existingFonts) == 0 { - workDir, _ := os.Getwd() - execPath, _ := os.Executable() - fm.logger.Warn("未找到字体文件", - zap.Strings("font_names", fontNames), - zap.String("work_dir", workDir), - zap.String("exec_path", execPath), - zap.String("base_dir", fm.baseDir), - zap.String("first_tried_path", func() string { - if len(fontPaths) > 0 { - return fontPaths[0] - } - return "none" - }())) - } + // 字体文件不存在时不记录警告,使用系统默认字体即可 return existingFonts } diff --git a/internal/shared/pdf/page_builder.go b/internal/shared/pdf/page_builder.go index 10ba811..2cb305c 100644 --- a/internal/shared/pdf/page_builder.go +++ b/internal/shared/pdf/page_builder.go @@ -65,7 +65,7 @@ func (pb *PageBuilder) AddFirstPage(pdf *gofpdf.Fpdf, product *entities.Product, pb.addWatermark(pdf, chineseFontAvailable) // 封面页布局 - 居中显示 - pageWidth, pageHeight := pdf.GetPageSize() + pageWidth, _ := pdf.GetPageSize() // 标题区域(页面中上部) pdf.SetY(80) @@ -128,14 +128,21 @@ func (pb *PageBuilder) AddFirstPage(pdf *gofpdf.Fpdf, product *entities.Product, } } - // 底部信息(价格等) + // 价格信息(右下角,在产品详情之后) if !product.Price.IsZero() { - pdf.SetY(pageHeight - 60) + // 获取产品详情结束后的Y坐标,稍微下移显示价格 + contentEndY := pdf.GetY() + pdf.SetY(contentEndY + 5) pdf.SetTextColor(0, 0, 0) - pb.fontManager.SetFont(pdf, "", 12) - _, lineHt = pdf.GetFontSize() - pdf.CellFormat(0, lineHt, fmt.Sprintf("价格:%s 元", product.Price.String()), "", 1, "C", false, 0, "") + pb.fontManager.SetFont(pdf, "", 14) + _, priceLineHt := pdf.GetFontSize() + priceText := fmt.Sprintf("价格:%s 元", product.Price.String()) + textWidth := pdf.GetStringWidth(priceText) + // 右对齐:从页面宽度减去文本宽度和右边距(15mm) + pdf.SetX(pageWidth - textWidth - 15) + pdf.CellFormat(textWidth, priceLineHt, priceText, "", 0, "R", false, 0, "") } + } // AddDocumentationPages 添加接口文档页面 diff --git a/internal/shared/pdf/pdf_generator_refactored.go b/internal/shared/pdf/pdf_generator_refactored.go index a00082d..5b65340 100644 --- a/internal/shared/pdf/pdf_generator_refactored.go +++ b/internal/shared/pdf/pdf_generator_refactored.go @@ -61,32 +61,39 @@ func (g *PDFGeneratorRefactored) findLogo() { _, filename, _, _ := runtime.Caller(0) baseDir := filepath.Dir(filename) - // 优先使用相对路径(Linux风格,使用正斜杠) + // 优先使用 baseDir(最可靠,基于当前文件位置) logoPaths := []string{ - "internal/shared/pdf/天远数据.png", // 相对于项目根目录(最常用) - "./internal/shared/pdf/天远数据.png", // 当前目录下的相对路径 - filepath.Join(baseDir, "天远数据.png"), // 相对当前文件 + filepath.Join(baseDir, "天远数据.png"), // 相对当前文件(最优先) } - // 尝试相对路径 - for _, logoPath := range logoPaths { - if _, err := os.Stat(logoPath); err == nil { - g.logoPath = logoPath - return - } + // 尝试相对于工作目录的路径 + if workDir, err := os.Getwd(); err == nil { + logoPaths = append(logoPaths, + filepath.Join(workDir, "internal", "shared", "pdf", "天远数据.png"), + filepath.Join(workDir, "tyapi-server", "internal", "shared", "pdf", "天远数据.png"), + ) } - // 尝试服务器绝对路径(后备方案) + // 尝试服务器绝对路径(Linux环境,优先查找 /www/tyapi-server) if runtime.GOOS == "linux" { serverPaths := []string{ "/www/tyapi-server/internal/shared/pdf/天远数据.png", "/app/internal/shared/pdf/天远数据.png", } - for _, logoPath := range serverPaths { - if _, err := os.Stat(logoPath); err == nil { - g.logoPath = logoPath - return - } + logoPaths = append(logoPaths, serverPaths...) + } + + // 尝试相对路径(作为最后的后备方案) + logoPaths = append(logoPaths, + "internal/shared/pdf/天远数据.png", + "./internal/shared/pdf/天远数据.png", + ) + + // 尝试所有路径 + for _, logoPath := range logoPaths { + if _, err := os.Stat(logoPath); err == nil { + g.logoPath = logoPath + return } } @@ -132,7 +139,6 @@ func (g *PDFGeneratorRefactored) generatePDF(product *entities.Product, doc *ent } }() - // 创建PDF文档 (A4大小,gofpdf v2 默认支持UTF-8) pdf := gofpdf.New("P", "mm", "A4", "") // 优化边距,减少空白 @@ -149,7 +155,6 @@ func (g *PDFGeneratorRefactored) generatePDF(product *entities.Product, doc *ent pdf.SetAuthor("TYAPI Server", true) pdf.SetCreator("TYAPI Server", true) - // 创建页面构建器 pageBuilder := NewPageBuilder(g.logger, g.fontManager, g.textProcessor, g.markdownProc, g.tableParser, g.tableRenderer, g.jsonProcessor, g.logoPath, g.watermarkText) @@ -173,4 +178,3 @@ func (g *PDFGeneratorRefactored) generatePDF(product *entities.Product, doc *ent return pdfBytes, nil } -