This commit is contained in:
2025-12-03 12:03:42 +08:00
parent 1cf64e831c
commit 63252fa30f
27 changed files with 7167 additions and 36 deletions

View File

@@ -3,6 +3,7 @@ package repositories
import (
"context"
"errors"
"fmt"
"strings"
"time"
"tyapi-server/internal/domains/finance/entities"
@@ -10,6 +11,7 @@ import (
"tyapi-server/internal/shared/database"
"tyapi-server/internal/shared/interfaces"
"github.com/shopspring/decimal"
"go.uber.org/zap"
"gorm.io/gorm"
)
@@ -90,11 +92,33 @@ func (r *GormRechargeRecordRepository) Count(ctx context.Context, options interf
query := r.GetDB(ctx).Model(&entities.RechargeRecord{})
if options.Filters != nil {
for key, value := range options.Filters {
query = query.Where(key+" = ?", value)
// 特殊处理时间范围过滤器
if key == "start_time" {
if startTime, ok := value.(time.Time); ok {
query = query.Where("created_at >= ?", startTime)
}
} else if key == "end_time" {
if endTime, ok := value.(time.Time); ok {
query = query.Where("created_at <= ?", endTime)
}
} else if key == "min_amount" {
// 处理最小金额支持string、int、int64类型
if amount, err := r.parseAmount(value); err == nil {
query = query.Where("amount >= ?", amount)
}
} else if key == "max_amount" {
// 处理最大金额支持string、int、int64类型
if amount, err := r.parseAmount(value); err == nil {
query = query.Where("amount <= ?", amount)
}
} else {
// 其他过滤器使用等值查询
query = query.Where(key+" = ?", value)
}
}
}
if options.Search != "" {
query = query.Where("user_id LIKE ? OR transfer_order_id LIKE ? OR alipay_order_id LIKE ?",
query = query.Where("user_id LIKE ? OR transfer_order_id LIKE ? OR alipay_order_id LIKE ?",
"%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%")
}
return count, query.Count(&count).Error
@@ -109,7 +133,7 @@ func (r *GormRechargeRecordRepository) Exists(ctx context.Context, id string) (b
func (r *GormRechargeRecordRepository) List(ctx context.Context, options interfaces.ListOptions) ([]entities.RechargeRecord, error) {
var records []entities.RechargeRecord
query := r.GetDB(ctx).Model(&entities.RechargeRecord{})
if options.Filters != nil {
for key, value := range options.Filters {
// 特殊处理 user_ids 过滤器
@@ -117,17 +141,38 @@ func (r *GormRechargeRecordRepository) List(ctx context.Context, options interfa
if userIds, ok := value.(string); ok && userIds != "" {
query = query.Where("user_id IN ?", strings.Split(userIds, ","))
}
} else if key == "start_time" {
// 处理开始时间范围
if startTime, ok := value.(time.Time); ok {
query = query.Where("created_at >= ?", startTime)
}
} else if key == "end_time" {
// 处理结束时间范围
if endTime, ok := value.(time.Time); ok {
query = query.Where("created_at <= ?", endTime)
}
} else if key == "min_amount" {
// 处理最小金额支持string、int、int64类型
if amount, err := r.parseAmount(value); err == nil {
query = query.Where("amount >= ?", amount)
}
} else if key == "max_amount" {
// 处理最大金额支持string、int、int64类型
if amount, err := r.parseAmount(value); err == nil {
query = query.Where("amount <= ?", amount)
}
} else {
// 其他过滤器使用等值查询
query = query.Where(key+" = ?", value)
}
}
}
if options.Search != "" {
query = query.Where("user_id LIKE ? OR transfer_order_id LIKE ? OR alipay_order_id LIKE ?",
query = query.Where("user_id LIKE ? OR transfer_order_id LIKE ? OR alipay_order_id LIKE ?",
"%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%")
}
if options.Sort != "" {
order := "ASC"
if options.Order == "desc" || options.Order == "DESC" {
@@ -137,12 +182,12 @@ func (r *GormRechargeRecordRepository) List(ctx context.Context, options interfa
} else {
query = query.Order("created_at DESC")
}
if options.Page > 0 && options.PageSize > 0 {
offset := (options.Page - 1) * options.PageSize
query = query.Offset(offset).Limit(options.PageSize)
}
err := query.Find(&records).Error
return records, err
}
@@ -209,7 +254,7 @@ func (r *GormRechargeRecordRepository) GetTotalAmountByUserIdAndDateRange(ctx co
// GetDailyStatsByUserId 获取用户每日充值统计(排除赠送)
func (r *GormRechargeRecordRepository) GetDailyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error) {
var results []map[string]interface{}
// 构建SQL查询 - 使用PostgreSQL语法使用具体的日期范围
sql := `
SELECT
@@ -224,19 +269,19 @@ func (r *GormRechargeRecordRepository) GetDailyStatsByUserId(ctx context.Context
GROUP BY DATE(created_at)
ORDER BY date ASC
`
err := r.GetDB(ctx).Raw(sql, userId, entities.RechargeStatusSuccess, entities.RechargeTypeGift, 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 *GormRechargeRecordRepository) GetMonthlyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error) {
var results []map[string]interface{}
// 构建SQL查询 - 使用PostgreSQL语法使用具体的日期范围
sql := `
SELECT
@@ -251,12 +296,12 @@ func (r *GormRechargeRecordRepository) GetMonthlyStatsByUserId(ctx context.Conte
GROUP BY TO_CHAR(created_at, 'YYYY-MM')
ORDER BY month ASC
`
err := r.GetDB(ctx).Raw(sql, userId, entities.RechargeStatusSuccess, entities.RechargeTypeGift, startDate, endDate).Scan(&results).Error
if err != nil {
return nil, err
}
return results, nil
}
@@ -283,7 +328,7 @@ func (r *GormRechargeRecordRepository) GetSystemAmountByDateRange(ctx context.Co
// GetSystemDailyStats 获取系统每日充值统计(排除赠送)
func (r *GormRechargeRecordRepository) GetSystemDailyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) {
var results []map[string]interface{}
sql := `
SELECT
DATE(created_at) as date,
@@ -296,19 +341,19 @@ func (r *GormRechargeRecordRepository) GetSystemDailyStats(ctx context.Context,
GROUP BY DATE(created_at)
ORDER BY date ASC
`
err := r.GetDB(ctx).Raw(sql, entities.RechargeStatusSuccess, entities.RechargeTypeGift, 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 *GormRechargeRecordRepository) 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,
@@ -321,11 +366,32 @@ func (r *GormRechargeRecordRepository) GetSystemMonthlyStats(ctx context.Context
GROUP BY TO_CHAR(created_at, 'YYYY-MM')
ORDER BY month ASC
`
err := r.GetDB(ctx).Raw(sql, entities.RechargeStatusSuccess, entities.RechargeTypeGift, startDate, endDate).Scan(&results).Error
if err != nil {
return nil, err
}
return results, nil
}
}
// parseAmount 解析金额值支持string、int、int64类型转换为decimal.Decimal
func (r *GormRechargeRecordRepository) parseAmount(value interface{}) (decimal.Decimal, error) {
switch v := value.(type) {
case string:
if v == "" {
return decimal.Zero, fmt.Errorf("empty string")
}
return decimal.NewFromString(v)
case int:
return decimal.NewFromInt(int64(v)), nil
case int64:
return decimal.NewFromInt(v), nil
case float64:
return decimal.NewFromFloat(v), nil
case decimal.Decimal:
return v, nil
default:
return decimal.Zero, fmt.Errorf("unsupported type: %T", value)
}
}

View File

@@ -2,12 +2,17 @@
package handlers
import (
"fmt"
"net/http"
"strconv"
"strings"
"tyapi-server/internal/application/product"
"tyapi-server/internal/application/product/dto/commands"
"tyapi-server/internal/application/product/dto/queries"
_ "tyapi-server/internal/application/product/dto/responses"
"tyapi-server/internal/domains/product/entities"
"tyapi-server/internal/shared/interfaces"
"tyapi-server/internal/shared/pdf"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
@@ -15,14 +20,15 @@ import (
// ProductHandler 产品相关HTTP处理器
type ProductHandler struct {
appService product.ProductApplicationService
apiConfigService product.ProductApiConfigApplicationService
categoryService product.CategoryApplicationService
subAppService product.SubscriptionApplicationService
appService product.ProductApplicationService
apiConfigService product.ProductApiConfigApplicationService
categoryService product.CategoryApplicationService
subAppService product.SubscriptionApplicationService
documentationAppService product.DocumentationApplicationServiceInterface
responseBuilder interfaces.ResponseBuilder
validator interfaces.RequestValidator
logger *zap.Logger
responseBuilder interfaces.ResponseBuilder
validator interfaces.RequestValidator
pdfGenerator *pdf.PDFGenerator
logger *zap.Logger
}
// NewProductHandler 创建产品HTTP处理器
@@ -34,17 +40,19 @@ func NewProductHandler(
documentationAppService product.DocumentationApplicationServiceInterface,
responseBuilder interfaces.ResponseBuilder,
validator interfaces.RequestValidator,
pdfGenerator *pdf.PDFGenerator,
logger *zap.Logger,
) *ProductHandler {
return &ProductHandler{
appService: appService,
apiConfigService: apiConfigService,
categoryService: categoryService,
subAppService: subAppService,
appService: appService,
apiConfigService: apiConfigService,
categoryService: categoryService,
subAppService: subAppService,
documentationAppService: documentationAppService,
responseBuilder: responseBuilder,
validator: validator,
logger: logger,
responseBuilder: responseBuilder,
validator: validator,
pdfGenerator: pdfGenerator,
logger: logger,
}
}
@@ -630,3 +638,168 @@ func (h *ProductHandler) GetProductDocumentation(c *gin.Context) {
h.responseBuilder.Success(c, doc, "获取文档成功")
}
// DownloadProductDocumentation 下载产品接口文档PDF文件
// @Summary 下载产品接口文档
// @Description 根据产品ID从数据库获取产品信息和文档信息动态生成PDF文档并下载。
// @Tags 数据大厅
// @Accept json
// @Produce application/pdf
// @Param id path string true "产品ID"
// @Success 200 {file} file "PDF文档文件"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 404 {object} map[string]interface{} "产品不存在"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/products/{id}/documentation/download [get]
func (h *ProductHandler) DownloadProductDocumentation(c *gin.Context) {
productID := c.Param("id")
if productID == "" {
h.responseBuilder.BadRequest(c, "产品ID不能为空")
return
}
// 检查PDF生成器是否可用
if h.pdfGenerator == nil {
h.logger.Error("PDF生成器未初始化")
h.responseBuilder.InternalError(c, "PDF生成器未初始化")
return
}
// 获取产品信息
product, err := h.appService.GetProductByID(c.Request.Context(), &queries.GetProductQuery{ID: productID})
if err != nil {
h.logger.Error("获取产品信息失败", zap.Error(err))
h.responseBuilder.NotFound(c, "产品不存在")
return
}
// 检查产品编码是否存在
if product.Code == "" {
h.logger.Warn("产品编码为空", zap.String("product_id", productID))
h.responseBuilder.BadRequest(c, "产品编码不存在")
return
}
h.logger.Info("开始生成PDF文档",
zap.String("product_id", productID),
zap.String("product_code", product.Code),
zap.String("product_name", product.Name),
)
// 获取产品文档信息
doc, docErr := h.documentationAppService.GetDocumentationByProductID(c.Request.Context(), productID)
if docErr != nil {
h.logger.Warn("获取产品文档失败,将只生成产品基本信息",
zap.String("product_id", productID),
zap.Error(docErr),
)
}
// 将响应类型转换为entity类型
var docEntity *entities.ProductDocumentation
if doc != nil {
docEntity = &entities.ProductDocumentation{
ID: doc.ID,
ProductID: doc.ProductID,
RequestURL: doc.RequestURL,
RequestMethod: doc.RequestMethod,
BasicInfo: doc.BasicInfo,
RequestParams: doc.RequestParams,
ResponseFields: doc.ResponseFields,
ResponseExample: doc.ResponseExample,
ErrorCodes: doc.ErrorCodes,
Version: doc.Version,
}
}
// 使用数据库数据生成PDF
h.logger.Info("准备调用PDF生成器",
zap.String("product_id", productID),
zap.String("product_name", product.Name),
zap.Bool("has_doc", docEntity != nil),
)
defer func() {
if r := recover(); r != nil {
h.logger.Error("PDF生成过程中发生panic",
zap.String("product_id", productID),
zap.Any("panic_value", r),
)
// 确保在panic时也能返回响应
if !c.Writer.Written() {
h.responseBuilder.InternalError(c, fmt.Sprintf("生成PDF文档时发生错误: %v", r))
}
}
}()
// 直接调用PDF生成器简化版本不使用goroutine
h.logger.Info("开始调用PDF生成器")
pdfBytes, genErr := h.pdfGenerator.GenerateProductPDF(
c.Request.Context(),
product.ID,
product.Name,
product.Code,
product.Description,
product.Content,
product.Price,
docEntity,
)
h.logger.Info("PDF生成器调用返回",
zap.String("product_id", productID),
zap.Bool("has_error", genErr != nil),
zap.Int("pdf_size", len(pdfBytes)),
)
if genErr != nil {
h.logger.Error("生成PDF文档失败",
zap.String("product_id", productID),
zap.String("product_code", product.Code),
zap.Error(genErr),
)
h.responseBuilder.InternalError(c, fmt.Sprintf("生成PDF文档失败: %s", genErr.Error()))
return
}
h.logger.Info("PDF生成器调用完成",
zap.String("product_id", productID),
zap.Int("pdf_size", len(pdfBytes)),
)
if len(pdfBytes) == 0 {
h.logger.Error("生成的PDF文档为空",
zap.String("product_id", productID),
zap.String("product_code", product.Code),
)
h.responseBuilder.InternalError(c, "生成的PDF文档为空")
return
}
// 生成文件名(清理文件名中的非法字符)
fileName := fmt.Sprintf("%s_接口文档.pdf", product.Name)
if product.Name == "" {
fileName = fmt.Sprintf("%s_接口文档.pdf", product.Code)
}
// 清理文件名中的非法字符
fileName = strings.ReplaceAll(fileName, "/", "_")
fileName = strings.ReplaceAll(fileName, "\\", "_")
fileName = strings.ReplaceAll(fileName, ":", "_")
fileName = strings.ReplaceAll(fileName, "*", "_")
fileName = strings.ReplaceAll(fileName, "?", "_")
fileName = strings.ReplaceAll(fileName, "\"", "_")
fileName = strings.ReplaceAll(fileName, "<", "_")
fileName = strings.ReplaceAll(fileName, ">", "_")
fileName = strings.ReplaceAll(fileName, "|", "_")
h.logger.Info("成功生成PDF文档",
zap.String("product_id", productID),
zap.String("product_code", product.Code),
zap.String("file_name", fileName),
zap.Int("file_size", len(pdfBytes)),
)
// 设置响应头并返回PDF文件
c.Header("Content-Type", "application/pdf")
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fileName))
c.Header("Content-Length", fmt.Sprintf("%d", len(pdfBytes)))
c.Data(http.StatusOK, "application/pdf", pdfBytes)
}

View File

@@ -51,6 +51,7 @@ func (r *ProductRoutes) Register(router *sharedhttp.GinRouter) {
products.GET("/:id", r.productHandler.GetProductDetail)
products.GET("/:id/api-config", r.productHandler.GetProductApiConfig)
products.GET("/:id/documentation", r.productHandler.GetProductDocumentation)
products.GET("/:id/documentation/download", r.productHandler.DownloadProductDocumentation)
// 订阅产品(需要认证)
products.POST("/:id/subscribe", r.auth.Handle(), r.productHandler.SubscribeProduct)