This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user