This commit is contained in:
2025-12-04 12:30:33 +08:00
parent 7b45b43a0e
commit 4ce8fe4023
10 changed files with 224 additions and 107 deletions

View File

@@ -20,6 +20,7 @@ type SubscriptionApplicationService interface {
// 我的订阅(用户专用) // 我的订阅(用户专用)
ListMySubscriptions(ctx context.Context, userID string, query *queries.ListSubscriptionsQuery) (*responses.SubscriptionListResponse, error) ListMySubscriptions(ctx context.Context, userID string, query *queries.ListSubscriptionsQuery) (*responses.SubscriptionListResponse, error)
GetMySubscriptionStats(ctx context.Context, userID string) (*responses.SubscriptionStatsResponse, 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) GetUserSubscriptions(ctx context.Context, query *queries.GetUserSubscriptionsQuery) ([]*responses.SubscriptionInfoResponse, error)

View File

@@ -304,6 +304,38 @@ func (s *SubscriptionApplicationServiceImpl) GetMySubscriptionStats(ctx context.
}, nil }, 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 转换为订阅信息响应 // convertToSubscriptionInfoResponse 转换为订阅信息响应
func (s *SubscriptionApplicationServiceImpl) convertToSubscriptionInfoResponse(subscription *entities.Subscription) *responses.SubscriptionInfoResponse { func (s *SubscriptionApplicationServiceImpl) convertToSubscriptionInfoResponse(subscription *entities.Subscription) *responses.SubscriptionInfoResponse {
// 查询用户信息 // 查询用户信息

View File

@@ -14,7 +14,7 @@ import (
) )
const ( const (
ApiCallsTable = "api_calls" ApiCallsTable = "api_calls"
ApiCallCacheTTL = 10 * time.Minute ApiCallCacheTTL = 10 * time.Minute
) )
@@ -212,7 +212,7 @@ func (r *GormApiCallRepository) ListByUserIdWithFiltersAndProductName(ctx contex
// 转换为entities.ApiCall并构建产品名称映射 // 转换为entities.ApiCall并构建产品名称映射
var calls []*entities.ApiCall var calls []*entities.ApiCall
productNameMap := make(map[string]string) productNameMap := make(map[string]string)
for _, c := range callsWithProduct { for _, c := range callsWithProduct {
call := c.ApiCall call := c.ApiCall
calls = append(calls, &call) calls = append(calls, &call)
@@ -237,7 +237,7 @@ func (r *GormApiCallRepository) CountByUserIdAndDateRange(ctx context.Context, u
// GetDailyStatsByUserId 获取用户每日API调用统计 // GetDailyStatsByUserId 获取用户每日API调用统计
func (r *GormApiCallRepository) GetDailyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error) { func (r *GormApiCallRepository) GetDailyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error) {
var results []map[string]interface{} var results []map[string]interface{}
// 构建SQL查询 - 使用PostgreSQL语法使用具体的日期范围 // 构建SQL查询 - 使用PostgreSQL语法使用具体的日期范围
sql := ` sql := `
SELECT SELECT
@@ -250,19 +250,19 @@ func (r *GormApiCallRepository) GetDailyStatsByUserId(ctx context.Context, userI
GROUP BY DATE(created_at) GROUP BY DATE(created_at)
ORDER BY date ASC ORDER BY date ASC
` `
err := r.GetDB(ctx).Raw(sql, userId, startDate.Format("2006-01-02"), endDate.Format("2006-01-02")).Scan(&results).Error err := r.GetDB(ctx).Raw(sql, userId, startDate.Format("2006-01-02"), endDate.Format("2006-01-02")).Scan(&results).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
return results, nil return results, nil
} }
// GetMonthlyStatsByUserId 获取用户每月API调用统计 // GetMonthlyStatsByUserId 获取用户每月API调用统计
func (r *GormApiCallRepository) GetMonthlyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error) { func (r *GormApiCallRepository) GetMonthlyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error) {
var results []map[string]interface{} var results []map[string]interface{}
// 构建SQL查询 - 使用PostgreSQL语法使用具体的日期范围 // 构建SQL查询 - 使用PostgreSQL语法使用具体的日期范围
sql := ` sql := `
SELECT SELECT
@@ -275,12 +275,12 @@ func (r *GormApiCallRepository) GetMonthlyStatsByUserId(ctx context.Context, use
GROUP BY TO_CHAR(created_at, 'YYYY-MM') GROUP BY TO_CHAR(created_at, 'YYYY-MM')
ORDER BY month ASC ORDER BY month ASC
` `
err := r.GetDB(ctx).Raw(sql, userId, startDate, endDate).Scan(&results).Error err := r.GetDB(ctx).Raw(sql, userId, startDate, endDate).Scan(&results).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
return results, nil return results, nil
} }
@@ -332,6 +332,12 @@ func (r *GormApiCallRepository) ListWithFiltersAndProductName(ctx context.Contex
whereArgs = append(whereArgs, "%"+productName+"%") 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 != "" { if status, ok := filters["status"].(string); ok && status != "" {
whereCondition += " AND ac.status = ?" whereCondition += " AND ac.status = ?"
@@ -340,9 +346,12 @@ func (r *GormApiCallRepository) ListWithFiltersAndProductName(ctx context.Contex
} }
// 构建JOIN查询 // 构建JOIN查询
// 需要JOIN product表获取产品名称JOIN users和enterprise_infos表获取企业名称
query := r.GetDB(ctx).Table("api_calls ac"). query := r.GetDB(ctx).Table("api_calls ac").
Select("ac.*, p.name as product_name"). Select("ac.*, p.name as product_name").
Joins("LEFT JOIN product p ON ac.product_id = p.id"). 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...) Where(whereCondition, whereArgs...)
// 获取总数 // 获取总数
@@ -374,7 +383,7 @@ func (r *GormApiCallRepository) ListWithFiltersAndProductName(ctx context.Contex
// 转换为entities.ApiCall并构建产品名称映射 // 转换为entities.ApiCall并构建产品名称映射
var calls []*entities.ApiCall var calls []*entities.ApiCall
productNameMap := make(map[string]string) productNameMap := make(map[string]string)
for _, c := range callsWithProduct { for _, c := range callsWithProduct {
call := c.ApiCall call := c.ApiCall
calls = append(calls, &call) calls = append(calls, &call)
@@ -406,7 +415,7 @@ func (r *GormApiCallRepository) GetSystemCallsByDateRange(ctx context.Context, s
// GetSystemDailyStats 获取系统每日API调用统计 // GetSystemDailyStats 获取系统每日API调用统计
func (r *GormApiCallRepository) GetSystemDailyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) { func (r *GormApiCallRepository) GetSystemDailyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) {
var results []map[string]interface{} var results []map[string]interface{}
sql := ` sql := `
SELECT SELECT
DATE(created_at) as date, DATE(created_at) as date,
@@ -417,19 +426,19 @@ func (r *GormApiCallRepository) GetSystemDailyStats(ctx context.Context, startDa
GROUP BY DATE(created_at) GROUP BY DATE(created_at)
ORDER BY date ASC ORDER BY date ASC
` `
err := r.GetDB(ctx).Raw(sql, startDate.Format("2006-01-02"), endDate.Format("2006-01-02")).Scan(&results).Error err := r.GetDB(ctx).Raw(sql, startDate.Format("2006-01-02"), endDate.Format("2006-01-02")).Scan(&results).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
return results, nil return results, nil
} }
// GetSystemMonthlyStats 获取系统每月API调用统计 // GetSystemMonthlyStats 获取系统每月API调用统计
func (r *GormApiCallRepository) GetSystemMonthlyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) { func (r *GormApiCallRepository) GetSystemMonthlyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) {
var results []map[string]interface{} var results []map[string]interface{}
sql := ` sql := `
SELECT SELECT
TO_CHAR(created_at, 'YYYY-MM') as month, 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') GROUP BY TO_CHAR(created_at, 'YYYY-MM')
ORDER BY month ASC ORDER BY month ASC
` `
err := r.GetDB(ctx).Raw(sql, startDate, endDate).Scan(&results).Error err := r.GetDB(ctx).Raw(sql, startDate, endDate).Scan(&results).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
return results, nil 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) { func (r *GormApiCallRepository) GetApiPopularityRanking(ctx context.Context, period string, limit int) ([]map[string]interface{}, error) {
var sql string var sql string
var args []interface{} var args []interface{}
switch period { switch period {
case "today": case "today":
sql = ` sql = `
@@ -508,12 +517,12 @@ func (r *GormApiCallRepository) GetApiPopularityRanking(ctx context.Context, per
default: default:
return nil, fmt.Errorf("不支持的时间周期: %s", period) return nil, fmt.Errorf("不支持的时间周期: %s", period)
} }
var results []map[string]interface{} var results []map[string]interface{}
err := r.GetDB(ctx).Raw(sql, args...).Scan(&results).Error err := r.GetDB(ctx).Raw(sql, args...).Scan(&results).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
return results, nil return results, nil
} }

View File

@@ -179,7 +179,7 @@ func (r *GormWalletTransactionRepository) GetTotalAmountByUserIdAndDateRange(ctx
// GetDailyStatsByUserId 获取用户每日消费统计 // GetDailyStatsByUserId 获取用户每日消费统计
func (r *GormWalletTransactionRepository) GetDailyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error) { func (r *GormWalletTransactionRepository) GetDailyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error) {
var results []map[string]interface{} var results []map[string]interface{}
// 构建SQL查询 - 使用PostgreSQL语法使用具体的日期范围 // 构建SQL查询 - 使用PostgreSQL语法使用具体的日期范围
sql := ` sql := `
SELECT SELECT
@@ -192,19 +192,19 @@ func (r *GormWalletTransactionRepository) GetDailyStatsByUserId(ctx context.Cont
GROUP BY DATE(created_at) GROUP BY DATE(created_at)
ORDER BY date ASC ORDER BY date ASC
` `
err := r.GetDB(ctx).Raw(sql, userId, startDate.Format("2006-01-02"), endDate.Format("2006-01-02")).Scan(&results).Error err := r.GetDB(ctx).Raw(sql, userId, startDate.Format("2006-01-02"), endDate.Format("2006-01-02")).Scan(&results).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
return results, nil return results, nil
} }
// GetMonthlyStatsByUserId 获取用户每月消费统计 // GetMonthlyStatsByUserId 获取用户每月消费统计
func (r *GormWalletTransactionRepository) GetMonthlyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error) { func (r *GormWalletTransactionRepository) GetMonthlyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error) {
var results []map[string]interface{} var results []map[string]interface{}
// 构建SQL查询 - 使用PostgreSQL语法使用具体的日期范围 // 构建SQL查询 - 使用PostgreSQL语法使用具体的日期范围
sql := ` sql := `
SELECT SELECT
@@ -217,12 +217,12 @@ func (r *GormWalletTransactionRepository) GetMonthlyStatsByUserId(ctx context.Co
GROUP BY TO_CHAR(created_at, 'YYYY-MM') GROUP BY TO_CHAR(created_at, 'YYYY-MM')
ORDER BY month ASC ORDER BY month ASC
` `
err := r.GetDB(ctx).Raw(sql, userId, startDate, endDate).Scan(&results).Error err := r.GetDB(ctx).Raw(sql, userId, startDate, endDate).Scan(&results).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
return results, nil return results, nil
} }
@@ -358,7 +358,7 @@ func (r *GormWalletTransactionRepository) ListByUserIdWithFiltersAndProductName(
// 转换为entities.WalletTransaction并构建产品名称映射 // 转换为entities.WalletTransaction并构建产品名称映射
var transactions []*entities.WalletTransaction var transactions []*entities.WalletTransaction
productNameMap := make(map[string]string) productNameMap := make(map[string]string)
for _, t := range transactionsWithProduct { for _, t := range transactionsWithProduct {
transaction := t.WalletTransaction transaction := t.WalletTransaction
transactions = append(transactions, &transaction) transactions = append(transactions, &transaction)
@@ -410,6 +410,12 @@ func (r *GormWalletTransactionRepository) ListWithFiltersAndProductName(ctx cont
whereArgs = append(whereArgs, "%"+productName+"%") 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 != "" { if minAmount, ok := filters["min_amount"].(string); ok && minAmount != "" {
whereCondition += " AND wt.amount >= ?" whereCondition += " AND wt.amount >= ?"
@@ -422,9 +428,12 @@ func (r *GormWalletTransactionRepository) ListWithFiltersAndProductName(ctx cont
} }
// 构建JOIN查询 // 构建JOIN查询
// 需要JOIN product表获取产品名称JOIN users和enterprise_infos表获取企业名称
query := r.GetDB(ctx).Table("wallet_transactions wt"). query := r.GetDB(ctx).Table("wallet_transactions wt").
Select("wt.*, p.name as product_name"). Select("wt.*, p.name as product_name").
Joins("LEFT JOIN product p ON wt.product_id = p.id"). 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...) Where(whereCondition, whereArgs...)
// 获取总数 // 获取总数
@@ -456,7 +465,7 @@ func (r *GormWalletTransactionRepository) ListWithFiltersAndProductName(ctx cont
// 转换为entities.WalletTransaction并构建产品名称映射 // 转换为entities.WalletTransaction并构建产品名称映射
var transactions []*entities.WalletTransaction var transactions []*entities.WalletTransaction
productNameMap := make(map[string]string) productNameMap := make(map[string]string)
for _, t := range transactionsWithProduct { for _, t := range transactionsWithProduct {
transaction := t.WalletTransaction transaction := t.WalletTransaction
transactions = append(transactions, &transaction) transactions = append(transactions, &transaction)
@@ -575,7 +584,7 @@ func (r *GormWalletTransactionRepository) GetSystemAmountByDateRange(ctx context
// GetSystemDailyStats 获取系统每日消费统计 // GetSystemDailyStats 获取系统每日消费统计
func (r *GormWalletTransactionRepository) GetSystemDailyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) { func (r *GormWalletTransactionRepository) GetSystemDailyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) {
var results []map[string]interface{} var results []map[string]interface{}
sql := ` sql := `
SELECT SELECT
DATE(created_at) as date, DATE(created_at) as date,
@@ -586,19 +595,19 @@ func (r *GormWalletTransactionRepository) GetSystemDailyStats(ctx context.Contex
GROUP BY DATE(created_at) GROUP BY DATE(created_at)
ORDER BY date ASC ORDER BY date ASC
` `
err := r.GetDB(ctx).Raw(sql, startDate.Format("2006-01-02"), endDate.Format("2006-01-02")).Scan(&results).Error err := r.GetDB(ctx).Raw(sql, startDate.Format("2006-01-02"), endDate.Format("2006-01-02")).Scan(&results).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
return results, nil return results, nil
} }
// GetSystemMonthlyStats 获取系统每月消费统计 // GetSystemMonthlyStats 获取系统每月消费统计
func (r *GormWalletTransactionRepository) GetSystemMonthlyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) { func (r *GormWalletTransactionRepository) GetSystemMonthlyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) {
var results []map[string]interface{} var results []map[string]interface{}
sql := ` sql := `
SELECT SELECT
TO_CHAR(created_at, 'YYYY-MM') as month, 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') GROUP BY TO_CHAR(created_at, 'YYYY-MM')
ORDER BY month ASC ORDER BY month ASC
` `
err := r.GetDB(ctx).Raw(sql, startDate, endDate).Scan(&results).Error err := r.GetDB(ctx).Raw(sql, startDate, endDate).Scan(&results).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
return results, nil return results, nil
} }

View File

@@ -797,9 +797,9 @@ func (h *ProductAdminHandler) BatchUpdateSubscriptionPrices(c *gin.Context) {
} }
h.responseBuilder.Success(c, map[string]interface{}{ h.responseBuilder.Success(c, map[string]interface{}{
"user_id": cmd.UserID, "user_id": cmd.UserID,
"discount": cmd.Discount, "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, "文档删除成功") h.responseBuilder.Success(c, nil, "文档删除成功")
} }
// GetAdminWalletTransactions 获取管理端消费记录 // GetAdminWalletTransactions 获取管理端消费记录
// @Summary 获取管理端消费记录 // @Summary 获取管理端消费记录
// @Description 管理员获取消费记录,支持筛选和分页 // @Description 管理员获取消费记录,支持筛选和分页
@@ -1173,6 +1171,11 @@ func (h *ProductAdminHandler) GetAdminWalletTransactions(c *gin.Context) {
filters["product_name"] = productName filters["product_name"] = productName
} }
// 企业名称筛选
if companyName := c.Query("company_name"); companyName != "" {
filters["company_name"] = companyName
}
// 金额范围筛选 // 金额范围筛选
if minAmount := c.Query("min_amount"); minAmount != "" { if minAmount := c.Query("min_amount"); minAmount != "" {
filters["min_amount"] = minAmount filters["min_amount"] = minAmount
@@ -1475,6 +1478,26 @@ func (h *ProductAdminHandler) GetAdminApiCalls(c *gin.Context) {
filters["product_ids"] = productIds 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 startTime := c.Query("start_time"); startTime != "" {
if t, err := time.Parse("2006-01-02 15:04:05", startTime); err == nil { if t, err := time.Parse("2006-01-02 15:04:05", startTime); err == nil {

View File

@@ -582,32 +582,77 @@ func (h *ProductHandler) GetMySubscriptionUsage(c *gin.Context) {
return 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 var query queries.GetSubscriptionQuery
query.ID = subscriptionID query.ID = subscriptionID
subscription, err := h.subAppService.GetSubscriptionByID(c.Request.Context(), &query) subscription, err := h.subAppService.GetSubscriptionByID(c.Request.Context(), &query)
if err != nil { 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, "订阅不存在") h.responseBuilder.NotFound(c, "订阅不存在")
return return
} }
// 验证订阅是否属于当前用户 // 验证订阅是否属于当前用户
if subscription.UserID != userID { 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, "无权访问此订阅") h.responseBuilder.Forbidden(c, "无权访问此订阅")
return return
} }
usage, err := h.subAppService.GetSubscriptionUsage(c.Request.Context(), subscriptionID) h.responseBuilder.Success(c, result, "获取我的订阅使用情况成功")
if err != nil { }
h.logger.Error("获取订阅使用情况失败", zap.Error(err))
h.responseBuilder.BadRequest(c, err.Error()) // 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 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 获取产品文档 // GetProductDocumentation 获取产品文档

View File

@@ -83,6 +83,9 @@ func (r *ProductRoutes) Register(router *sharedhttp.GinRouter) {
// 获取订阅使用情况 // 获取订阅使用情况
subscriptions.GET("/:id/usage", r.productHandler.GetMySubscriptionUsage) subscriptions.GET("/:id/usage", r.productHandler.GetMySubscriptionUsage)
// 取消订阅
subscriptions.POST("/:id/cancel", r.productHandler.CancelMySubscription)
} }
} }

View File

@@ -43,7 +43,7 @@ func (fm *FontManager) LoadChineseFont(pdf *gofpdf.Fpdf) bool {
fontPaths := fm.getChineseFontPaths() fontPaths := fm.getChineseFontPaths()
if len(fontPaths) == 0 { if len(fontPaths) == 0 {
fm.logger.Warn("未找到中文字体文件") // 字体文件不存在,使用系统默认字体,不记录警告
return false return false
} }
@@ -55,7 +55,7 @@ func (fm *FontManager) LoadChineseFont(pdf *gofpdf.Fpdf) bool {
} }
} }
fm.logger.Warn("无法加载中文字体文件") // 无法加载字体,使用系统默认字体,不记录警告
return false return false
} }
@@ -146,14 +146,17 @@ func (fm *FontManager) getWatermarkFontPaths() []string {
func (fm *FontManager) buildFontPaths(fontNames []string) []string { func (fm *FontManager) buildFontPaths(fontNames []string) []string {
var fontPaths []string var fontPaths []string
// 方式1: 优先使用相对路径Linux风格,使用正斜杠)- 最常用 // 方式1: 服务器绝对路径Linux环境,优先检查,因为服务器上文件通常在这里)
for _, fontName := range fontNames { if runtime.GOOS == "linux" {
// 相对路径(相对于项目根目录) commonServerPaths := []string{
relativePaths := []string{ "/www/tyapi-server/internal/shared/pdf/fonts",
"internal/shared/pdf/fonts/" + fontName, "/app/internal/shared/pdf/fonts",
"./internal/shared/pdf/fonts/" + fontName, }
for _, basePath := range commonServerPaths {
for _, fontName := range fontNames {
fontPaths = append(fontPaths, filepath.Join(basePath, fontName))
}
} }
fontPaths = append(fontPaths, relativePaths...)
} }
// 方式2: 使用 pdf/fonts/ 目录(相对于当前文件) // 方式2: 使用 pdf/fonts/ 目录(相对于当前文件)
@@ -194,17 +197,13 @@ func (fm *FontManager) buildFontPaths(fontNames []string) []string {
} }
} }
// 方式6: 服务器绝对路径(作为最后的后备方案) // 方式6: 对路径(作为最后的后备方案)
if runtime.GOOS == "linux" { for _, fontName := range fontNames {
commonServerPaths := []string{ relativePaths := []string{
"/www/tyapi-server/internal/shared/pdf/fonts", "internal/shared/pdf/fonts/" + fontName,
"/app/internal/shared/pdf/fonts", "./internal/shared/pdf/fonts/" + fontName,
}
for _, basePath := range commonServerPaths {
for _, fontName := range fontNames {
fontPaths = append(fontPaths, filepath.Join(basePath, 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 return existingFonts
} }

View File

@@ -65,7 +65,7 @@ func (pb *PageBuilder) AddFirstPage(pdf *gofpdf.Fpdf, product *entities.Product,
pb.addWatermark(pdf, chineseFontAvailable) pb.addWatermark(pdf, chineseFontAvailable)
// 封面页布局 - 居中显示 // 封面页布局 - 居中显示
pageWidth, pageHeight := pdf.GetPageSize() pageWidth, _ := pdf.GetPageSize()
// 标题区域(页面中上部) // 标题区域(页面中上部)
pdf.SetY(80) pdf.SetY(80)
@@ -128,14 +128,21 @@ func (pb *PageBuilder) AddFirstPage(pdf *gofpdf.Fpdf, product *entities.Product,
} }
} }
// 底部信息(价格等 // 价格信息(右下角,在产品详情之后
if !product.Price.IsZero() { if !product.Price.IsZero() {
pdf.SetY(pageHeight - 60) // 获取产品详情结束后的Y坐标稍微下移显示价格
contentEndY := pdf.GetY()
pdf.SetY(contentEndY + 5)
pdf.SetTextColor(0, 0, 0) pdf.SetTextColor(0, 0, 0)
pb.fontManager.SetFont(pdf, "", 12) pb.fontManager.SetFont(pdf, "", 14)
_, lineHt = pdf.GetFontSize() _, priceLineHt := pdf.GetFontSize()
pdf.CellFormat(0, lineHt, fmt.Sprintf("价格:%s 元", product.Price.String()), "", 1, "C", false, 0, "") 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 添加接口文档页面 // AddDocumentationPages 添加接口文档页面

View File

@@ -61,32 +61,39 @@ func (g *PDFGeneratorRefactored) findLogo() {
_, filename, _, _ := runtime.Caller(0) _, filename, _, _ := runtime.Caller(0)
baseDir := filepath.Dir(filename) baseDir := filepath.Dir(filename)
// 优先使用相对路径Linux风格使用正斜杠 // 优先使用 baseDir最可靠基于当前文件位置
logoPaths := []string{ logoPaths := []string{
"internal/shared/pdf/天远数据.png", // 相对于项目根目录(最常用 filepath.Join(baseDir, "天远数据.png"), // 相对当前文件(最优先
"./internal/shared/pdf/天远数据.png", // 当前目录下的相对路径
filepath.Join(baseDir, "天远数据.png"), // 相对当前文件
} }
// 尝试相对路径 // 尝试相对于工作目录的路径
for _, logoPath := range logoPaths { if workDir, err := os.Getwd(); err == nil {
if _, err := os.Stat(logoPath); err == nil { logoPaths = append(logoPaths,
g.logoPath = logoPath filepath.Join(workDir, "internal", "shared", "pdf", "天远数据.png"),
return filepath.Join(workDir, "tyapi-server", "internal", "shared", "pdf", "天远数据.png"),
} )
} }
// 尝试服务器绝对路径(后备方案 // 尝试服务器绝对路径(Linux环境优先查找 /www/tyapi-server
if runtime.GOOS == "linux" { if runtime.GOOS == "linux" {
serverPaths := []string{ serverPaths := []string{
"/www/tyapi-server/internal/shared/pdf/天远数据.png", "/www/tyapi-server/internal/shared/pdf/天远数据.png",
"/app/internal/shared/pdf/天远数据.png", "/app/internal/shared/pdf/天远数据.png",
} }
for _, logoPath := range serverPaths { logoPaths = append(logoPaths, serverPaths...)
if _, err := os.Stat(logoPath); err == nil { }
g.logoPath = logoPath
return // 尝试相对路径(作为最后的后备方案)
} 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文档 (A4大小gofpdf v2 默认支持UTF-8)
pdf := gofpdf.New("P", "mm", "A4", "") 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.SetAuthor("TYAPI Server", true)
pdf.SetCreator("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) 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 return pdfBytes, nil
} }