2025-09-01 18:29:59 +08:00
|
|
|
|
//nolint:unused
|
2025-07-15 13:21:34 +08:00
|
|
|
|
package handlers
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2025-12-03 12:03:42 +08:00
|
|
|
|
"fmt"
|
|
|
|
|
|
"net/http"
|
2025-07-28 01:46:39 +08:00
|
|
|
|
"strconv"
|
2025-12-03 12:03:42 +08:00
|
|
|
|
"strings"
|
2025-07-15 13:21:34 +08:00
|
|
|
|
"tyapi-server/internal/application/product"
|
|
|
|
|
|
"tyapi-server/internal/application/product/dto/commands"
|
|
|
|
|
|
"tyapi-server/internal/application/product/dto/queries"
|
2025-09-01 18:29:59 +08:00
|
|
|
|
_ "tyapi-server/internal/application/product/dto/responses"
|
2025-12-03 12:03:42 +08:00
|
|
|
|
"tyapi-server/internal/domains/product/entities"
|
2025-07-15 13:21:34 +08:00
|
|
|
|
"tyapi-server/internal/shared/interfaces"
|
2025-12-03 12:03:42 +08:00
|
|
|
|
"tyapi-server/internal/shared/pdf"
|
2025-07-15 13:21:34 +08:00
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
2025-12-11 11:14:31 +08:00
|
|
|
|
"github.com/shopspring/decimal"
|
2025-07-15 13:21:34 +08:00
|
|
|
|
"go.uber.org/zap"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// ProductHandler 产品相关HTTP处理器
|
|
|
|
|
|
type ProductHandler struct {
|
2025-12-03 12:03:42 +08:00
|
|
|
|
appService product.ProductApplicationService
|
|
|
|
|
|
apiConfigService product.ProductApiConfigApplicationService
|
|
|
|
|
|
categoryService product.CategoryApplicationService
|
|
|
|
|
|
subAppService product.SubscriptionApplicationService
|
2025-07-31 15:41:00 +08:00
|
|
|
|
documentationAppService product.DocumentationApplicationServiceInterface
|
2025-12-03 12:03:42 +08:00
|
|
|
|
responseBuilder interfaces.ResponseBuilder
|
|
|
|
|
|
validator interfaces.RequestValidator
|
|
|
|
|
|
pdfGenerator *pdf.PDFGenerator
|
2025-12-04 18:10:14 +08:00
|
|
|
|
pdfCacheManager *pdf.PDFCacheManager
|
2025-12-03 12:03:42 +08:00
|
|
|
|
logger *zap.Logger
|
2025-07-15 13:21:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NewProductHandler 创建产品HTTP处理器
|
|
|
|
|
|
func NewProductHandler(
|
|
|
|
|
|
appService product.ProductApplicationService,
|
2025-07-28 01:46:39 +08:00
|
|
|
|
apiConfigService product.ProductApiConfigApplicationService,
|
2025-07-15 13:21:34 +08:00
|
|
|
|
categoryService product.CategoryApplicationService,
|
|
|
|
|
|
subAppService product.SubscriptionApplicationService,
|
2025-07-31 15:41:00 +08:00
|
|
|
|
documentationAppService product.DocumentationApplicationServiceInterface,
|
2025-07-15 13:21:34 +08:00
|
|
|
|
responseBuilder interfaces.ResponseBuilder,
|
2025-07-20 20:53:26 +08:00
|
|
|
|
validator interfaces.RequestValidator,
|
2025-12-03 12:03:42 +08:00
|
|
|
|
pdfGenerator *pdf.PDFGenerator,
|
2025-12-04 18:10:14 +08:00
|
|
|
|
pdfCacheManager *pdf.PDFCacheManager,
|
2025-07-15 13:21:34 +08:00
|
|
|
|
logger *zap.Logger,
|
|
|
|
|
|
) *ProductHandler {
|
|
|
|
|
|
return &ProductHandler{
|
2025-12-03 12:03:42 +08:00
|
|
|
|
appService: appService,
|
|
|
|
|
|
apiConfigService: apiConfigService,
|
|
|
|
|
|
categoryService: categoryService,
|
|
|
|
|
|
subAppService: subAppService,
|
2025-07-31 15:41:00 +08:00
|
|
|
|
documentationAppService: documentationAppService,
|
2025-12-03 12:03:42 +08:00
|
|
|
|
responseBuilder: responseBuilder,
|
|
|
|
|
|
validator: validator,
|
|
|
|
|
|
pdfGenerator: pdfGenerator,
|
2025-12-04 18:10:14 +08:00
|
|
|
|
pdfCacheManager: pdfCacheManager,
|
2025-12-03 12:03:42 +08:00
|
|
|
|
logger: logger,
|
2025-07-15 13:21:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ListProducts 获取产品列表(数据大厅)
|
|
|
|
|
|
// @Summary 获取产品列表
|
2025-07-30 00:51:22 +08:00
|
|
|
|
// @Description 分页获取可用的产品列表,支持筛选,默认只返回可见的产品
|
2025-07-15 13:21:34 +08:00
|
|
|
|
// @Tags 数据大厅
|
|
|
|
|
|
// @Accept json
|
|
|
|
|
|
// @Produce json
|
|
|
|
|
|
// @Param page query int false "页码" default(1)
|
|
|
|
|
|
// @Param page_size query int false "每页数量" default(10)
|
|
|
|
|
|
// @Param keyword query string false "搜索关键词"
|
|
|
|
|
|
// @Param category_id query string false "分类ID"
|
|
|
|
|
|
// @Param is_enabled query bool false "是否启用"
|
|
|
|
|
|
// @Param is_visible query bool false "是否可见"
|
|
|
|
|
|
// @Param is_package query bool false "是否组合包"
|
2025-07-29 00:30:32 +08:00
|
|
|
|
// @Param is_subscribed query bool false "是否已订阅(需要认证)"
|
2025-07-15 13:21:34 +08:00
|
|
|
|
// @Param sort_by query string false "排序字段"
|
|
|
|
|
|
// @Param sort_order query string false "排序方向" Enums(asc, desc)
|
|
|
|
|
|
// @Success 200 {object} responses.ProductListResponse "获取产品列表成功"
|
|
|
|
|
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
|
|
|
|
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
|
|
|
|
|
// @Router /api/v1/products [get]
|
|
|
|
|
|
func (h *ProductHandler) ListProducts(c *gin.Context) {
|
2025-07-29 00:30:32 +08:00
|
|
|
|
// 获取当前用户ID(可选认证)
|
|
|
|
|
|
userID := h.getCurrentUserID(c)
|
|
|
|
|
|
|
2025-07-28 01:46:39 +08:00
|
|
|
|
// 解析查询参数
|
|
|
|
|
|
page := h.getIntQuery(c, "page", 1)
|
|
|
|
|
|
pageSize := h.getIntQuery(c, "page_size", 10)
|
|
|
|
|
|
|
|
|
|
|
|
// 构建筛选条件
|
|
|
|
|
|
filters := make(map[string]interface{})
|
|
|
|
|
|
|
|
|
|
|
|
// 搜索关键词筛选
|
|
|
|
|
|
if keyword := c.Query("keyword"); keyword != "" {
|
|
|
|
|
|
filters["keyword"] = keyword
|
2025-07-15 13:21:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-28 01:46:39 +08:00
|
|
|
|
// 分类ID筛选
|
|
|
|
|
|
if categoryID := c.Query("category_id"); categoryID != "" {
|
|
|
|
|
|
filters["category_id"] = categoryID
|
2025-07-15 13:21:34 +08:00
|
|
|
|
}
|
2025-07-28 01:46:39 +08:00
|
|
|
|
|
|
|
|
|
|
// 启用状态筛选
|
|
|
|
|
|
if isEnabled := c.Query("is_enabled"); isEnabled != "" {
|
|
|
|
|
|
if enabled, err := strconv.ParseBool(isEnabled); err == nil {
|
|
|
|
|
|
filters["is_enabled"] = enabled
|
|
|
|
|
|
}
|
2025-07-15 13:21:34 +08:00
|
|
|
|
}
|
2025-07-28 01:46:39 +08:00
|
|
|
|
|
2025-07-30 00:51:22 +08:00
|
|
|
|
// 可见状态筛选 - 用户端默认只显示可见的产品
|
2025-07-28 01:46:39 +08:00
|
|
|
|
if isVisible := c.Query("is_visible"); isVisible != "" {
|
|
|
|
|
|
if visible, err := strconv.ParseBool(isVisible); err == nil {
|
|
|
|
|
|
filters["is_visible"] = visible
|
|
|
|
|
|
}
|
2025-07-30 00:51:22 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
// 如果没有指定可见状态,默认只显示可见的产品
|
|
|
|
|
|
filters["is_visible"] = true
|
2025-07-28 01:46:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 产品类型筛选
|
|
|
|
|
|
if isPackage := c.Query("is_package"); isPackage != "" {
|
|
|
|
|
|
if pkg, err := strconv.ParseBool(isPackage); err == nil {
|
|
|
|
|
|
filters["is_package"] = pkg
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-29 00:30:32 +08:00
|
|
|
|
// 订阅状态筛选(需要认证)
|
|
|
|
|
|
if userID != "" {
|
|
|
|
|
|
if isSubscribed := c.Query("is_subscribed"); isSubscribed != "" {
|
|
|
|
|
|
if subscribed, err := strconv.ParseBool(isSubscribed); err == nil {
|
|
|
|
|
|
filters["is_subscribed"] = subscribed
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 添加用户ID到筛选条件
|
|
|
|
|
|
filters["user_id"] = userID
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-28 01:46:39 +08:00
|
|
|
|
// 排序字段
|
|
|
|
|
|
sortBy := c.Query("sort_by")
|
|
|
|
|
|
if sortBy == "" {
|
|
|
|
|
|
sortBy = "created_at"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 排序方向
|
|
|
|
|
|
sortOrder := c.Query("sort_order")
|
|
|
|
|
|
if sortOrder == "" {
|
|
|
|
|
|
sortOrder = "desc"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 构建分页选项
|
|
|
|
|
|
options := interfaces.ListOptions{
|
|
|
|
|
|
Page: page,
|
|
|
|
|
|
PageSize: pageSize,
|
|
|
|
|
|
Sort: sortBy,
|
|
|
|
|
|
Order: sortOrder,
|
2025-07-15 13:21:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-28 01:46:39 +08:00
|
|
|
|
result, err := h.appService.ListProducts(c.Request.Context(), filters, options)
|
2025-07-15 13:21:34 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
h.logger.Error("获取产品列表失败", zap.Error(err))
|
|
|
|
|
|
h.responseBuilder.InternalError(c, "获取产品列表失败")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
h.responseBuilder.Success(c, result, "获取产品列表成功")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-28 01:46:39 +08:00
|
|
|
|
// getIntQuery 获取整数查询参数
|
|
|
|
|
|
func (h *ProductHandler) getIntQuery(c *gin.Context, key string, defaultValue int) int {
|
|
|
|
|
|
if value := c.Query(key); value != "" {
|
|
|
|
|
|
if intValue, err := strconv.Atoi(value); err == nil && intValue > 0 {
|
|
|
|
|
|
return intValue
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return defaultValue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-29 00:30:32 +08:00
|
|
|
|
// getCurrentUserID 获取当前用户ID
|
|
|
|
|
|
func (h *ProductHandler) getCurrentUserID(c *gin.Context) string {
|
|
|
|
|
|
if userID, exists := c.Get("user_id"); exists {
|
|
|
|
|
|
if id, ok := userID.(string); ok {
|
|
|
|
|
|
return id
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-15 13:21:34 +08:00
|
|
|
|
// GetProductDetail 获取产品详情
|
|
|
|
|
|
// @Summary 获取产品详情
|
2025-11-03 13:32:05 +08:00
|
|
|
|
// @Description 获取产品详细信息,详情接口不受 is_visible 字段影响,可通过直接访问查看任何产品
|
2025-07-15 13:21:34 +08:00
|
|
|
|
// @Tags 数据大厅
|
|
|
|
|
|
// @Accept json
|
|
|
|
|
|
// @Produce json
|
|
|
|
|
|
// @Param id path string true "产品ID"
|
2025-07-31 15:41:00 +08:00
|
|
|
|
// @Param with_document query bool false "是否包含文档信息"
|
|
|
|
|
|
// @Success 200 {object} responses.ProductInfoWithDocumentResponse "获取产品详情成功"
|
2025-07-15 13:21:34 +08:00
|
|
|
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
2025-11-03 13:32:05 +08:00
|
|
|
|
// @Failure 404 {object} map[string]interface{} "产品不存在"
|
2025-07-15 13:21:34 +08:00
|
|
|
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
|
|
|
|
|
// @Router /api/v1/products/{id} [get]
|
|
|
|
|
|
func (h *ProductHandler) GetProductDetail(c *gin.Context) {
|
2025-07-31 15:41:00 +08:00
|
|
|
|
var query queries.GetProductDetailQuery
|
2025-07-15 13:21:34 +08:00
|
|
|
|
query.ID = c.Param("id")
|
|
|
|
|
|
if query.ID == "" {
|
|
|
|
|
|
h.responseBuilder.BadRequest(c, "产品ID不能为空")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-31 15:41:00 +08:00
|
|
|
|
// 解析可选参数
|
|
|
|
|
|
if withDocument := c.Query("with_document"); withDocument != "" {
|
|
|
|
|
|
if withDoc, err := strconv.ParseBool(withDocument); err == nil {
|
|
|
|
|
|
query.WithDocument = &withDoc
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-30 00:51:22 +08:00
|
|
|
|
result, err := h.appService.GetProductByIDForUser(c.Request.Context(), &query)
|
2025-07-15 13:21:34 +08:00
|
|
|
|
if err != nil {
|
2025-07-31 15:41:00 +08:00
|
|
|
|
h.logger.Error("获取产品详情失败", zap.Error(err))
|
2025-11-03 13:32:05 +08:00
|
|
|
|
h.responseBuilder.NotFound(c, "产品不存在")
|
2025-07-15 13:21:34 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
h.responseBuilder.Success(c, result, "获取产品详情成功")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// SubscribeProduct 订阅产品
|
|
|
|
|
|
// @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 404 {object} map[string]interface{} "产品不存在"
|
|
|
|
|
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
|
|
|
|
|
// @Router /api/v1/products/{id}/subscribe [post]
|
|
|
|
|
|
func (h *ProductHandler) SubscribeProduct(c *gin.Context) {
|
|
|
|
|
|
userID := c.GetString("user_id") // 从JWT中间件获取
|
|
|
|
|
|
if userID == "" {
|
2025-07-20 20:53:26 +08:00
|
|
|
|
h.responseBuilder.Unauthorized(c, "用户未登录")
|
2025-07-15 13:21:34 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var cmd commands.CreateSubscriptionCommand
|
2025-07-20 20:53:26 +08:00
|
|
|
|
if err := h.validator.ValidateParam(c, &cmd); err != nil {
|
2025-07-15 13:21:34 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-20 20:53:26 +08:00
|
|
|
|
// 设置用户ID
|
2025-07-15 13:21:34 +08:00
|
|
|
|
cmd.UserID = userID
|
|
|
|
|
|
|
|
|
|
|
|
if err := h.subAppService.CreateSubscription(c.Request.Context(), &cmd); err != nil {
|
2025-07-20 20:53:26 +08:00
|
|
|
|
h.logger.Error("订阅产品失败", zap.Error(err), zap.String("user_id", userID), zap.String("product_id", cmd.ProductID))
|
2025-07-15 13:21:34 +08:00
|
|
|
|
h.responseBuilder.BadRequest(c, err.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
h.responseBuilder.Success(c, nil, "订阅成功")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetProductStats 获取产品统计信息
|
|
|
|
|
|
// @Summary 获取产品统计
|
|
|
|
|
|
// @Description 获取产品相关的统计信息
|
|
|
|
|
|
// @Tags 数据大厅
|
|
|
|
|
|
// @Accept json
|
|
|
|
|
|
// @Produce json
|
|
|
|
|
|
// @Success 200 {object} responses.ProductStatsResponse "获取统计信息成功"
|
|
|
|
|
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
|
|
|
|
|
// @Router /api/v1/products/stats [get]
|
|
|
|
|
|
func (h *ProductHandler) GetProductStats(c *gin.Context) {
|
|
|
|
|
|
result, err := h.appService.GetProductStats(c.Request.Context())
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
h.logger.Error("获取产品统计失败", zap.Error(err))
|
|
|
|
|
|
h.responseBuilder.InternalError(c, "获取产品统计失败")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
h.responseBuilder.Success(c, result, "获取产品统计成功")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-28 01:46:39 +08:00
|
|
|
|
// GetProductApiConfig 获取产品API配置
|
|
|
|
|
|
// @Summary 获取产品API配置
|
|
|
|
|
|
// @Description 根据产品ID获取API配置信息
|
|
|
|
|
|
// @Tags 产品API配置
|
|
|
|
|
|
// @Accept json
|
|
|
|
|
|
// @Produce json
|
|
|
|
|
|
// @Param id path string true "产品ID"
|
|
|
|
|
|
// @Success 200 {object} responses.ProductApiConfigResponse "获取成功"
|
|
|
|
|
|
// @Failure 400 {object} interfaces.APIResponse "请求参数错误"
|
|
|
|
|
|
// @Failure 404 {object} interfaces.APIResponse "配置不存在"
|
|
|
|
|
|
// @Router /api/v1/products/{id}/api-config [get]
|
|
|
|
|
|
func (h *ProductHandler) GetProductApiConfig(c *gin.Context) {
|
|
|
|
|
|
productID := c.Param("id")
|
|
|
|
|
|
if productID == "" {
|
|
|
|
|
|
h.responseBuilder.BadRequest(c, "产品ID不能为空")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
config, err := h.apiConfigService.GetProductApiConfig(c.Request.Context(), productID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
h.logger.Error("获取产品API配置失败", zap.Error(err), zap.String("product_id", productID))
|
|
|
|
|
|
h.responseBuilder.NotFound(c, "产品API配置不存在")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
h.responseBuilder.Success(c, config, "获取产品API配置成功")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetProductApiConfigByCode 根据产品代码获取API配置
|
|
|
|
|
|
// @Summary 根据产品代码获取API配置
|
|
|
|
|
|
// @Description 根据产品代码获取API配置信息
|
|
|
|
|
|
// @Tags 产品API配置
|
|
|
|
|
|
// @Accept json
|
|
|
|
|
|
// @Produce json
|
|
|
|
|
|
// @Param product_code path string true "产品代码"
|
|
|
|
|
|
// @Success 200 {object} responses.ProductApiConfigResponse "获取成功"
|
|
|
|
|
|
// @Failure 400 {object} interfaces.APIResponse "请求参数错误"
|
|
|
|
|
|
// @Failure 404 {object} interfaces.APIResponse "配置不存在"
|
|
|
|
|
|
// @Router /api/v1/products/code/{product_code}/api-config [get]
|
|
|
|
|
|
func (h *ProductHandler) GetProductApiConfigByCode(c *gin.Context) {
|
|
|
|
|
|
productCode := c.Param("product_code")
|
|
|
|
|
|
if productCode == "" {
|
|
|
|
|
|
h.responseBuilder.BadRequest(c, "产品代码不能为空")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
config, err := h.apiConfigService.GetProductApiConfigByCode(c.Request.Context(), productCode)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
h.logger.Error("根据产品代码获取API配置失败", zap.Error(err), zap.String("product_code", productCode))
|
|
|
|
|
|
h.responseBuilder.NotFound(c, "产品API配置不存在")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
h.responseBuilder.Success(c, config, "获取产品API配置成功")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-15 13:21:34 +08:00
|
|
|
|
// ================ 分类相关方法 ================
|
|
|
|
|
|
|
|
|
|
|
|
// ListCategories 获取分类列表
|
|
|
|
|
|
// @Summary 获取分类列表
|
2025-07-20 20:53:26 +08:00
|
|
|
|
// @Description 获取产品分类列表,支持筛选
|
2025-07-15 13:21:34 +08:00
|
|
|
|
// @Tags 数据大厅
|
|
|
|
|
|
// @Accept json
|
|
|
|
|
|
// @Produce json
|
2025-07-20 20:53:26 +08:00
|
|
|
|
// @Param page query int false "页码" default(1)
|
|
|
|
|
|
// @Param page_size query int false "每页数量" default(10)
|
|
|
|
|
|
// @Param is_enabled query bool false "是否启用"
|
|
|
|
|
|
// @Param is_visible query bool false "是否可见"
|
2025-07-15 13:21:34 +08:00
|
|
|
|
// @Success 200 {object} responses.CategoryListResponse "获取分类列表成功"
|
|
|
|
|
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
|
|
|
|
|
// @Router /api/v1/categories [get]
|
|
|
|
|
|
func (h *ProductHandler) ListCategories(c *gin.Context) {
|
2025-07-20 20:53:26 +08:00
|
|
|
|
var query queries.ListCategoriesQuery
|
|
|
|
|
|
if err := h.validator.ValidateQuery(c, &query); err != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 设置默认值
|
|
|
|
|
|
if query.Page <= 0 {
|
|
|
|
|
|
query.Page = 1
|
|
|
|
|
|
}
|
|
|
|
|
|
if query.PageSize <= 0 {
|
|
|
|
|
|
query.PageSize = 10
|
|
|
|
|
|
}
|
|
|
|
|
|
if query.PageSize > 100 {
|
|
|
|
|
|
query.PageSize = 100
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-15 13:21:34 +08:00
|
|
|
|
// 调用应用服务
|
2025-07-20 20:53:26 +08:00
|
|
|
|
categories, err := h.categoryService.ListCategories(c.Request.Context(), &query)
|
2025-07-15 13:21:34 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
h.logger.Error("获取分类列表失败", zap.Error(err))
|
|
|
|
|
|
h.responseBuilder.InternalError(c, "获取分类列表失败")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-07-20 20:53:26 +08:00
|
|
|
|
|
2025-07-15 13:21:34 +08:00
|
|
|
|
// 返回结果
|
|
|
|
|
|
h.responseBuilder.Success(c, categories, "获取分类列表成功")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetCategoryDetail 获取分类详情
|
|
|
|
|
|
// @Summary 获取分类详情
|
|
|
|
|
|
// @Description 根据分类ID获取分类详细信息
|
|
|
|
|
|
// @Tags 数据大厅
|
|
|
|
|
|
// @Accept json
|
|
|
|
|
|
// @Produce json
|
|
|
|
|
|
// @Param id path string true "分类ID"
|
|
|
|
|
|
// @Success 200 {object} responses.CategoryInfoResponse "获取分类详情成功"
|
|
|
|
|
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
|
|
|
|
|
// @Failure 404 {object} map[string]interface{} "分类不存在"
|
|
|
|
|
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
|
|
|
|
|
// @Router /api/v1/categories/{id} [get]
|
|
|
|
|
|
func (h *ProductHandler) GetCategoryDetail(c *gin.Context) {
|
|
|
|
|
|
categoryID := c.Param("id")
|
|
|
|
|
|
if categoryID == "" {
|
|
|
|
|
|
h.responseBuilder.BadRequest(c, "分类ID不能为空")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-07-20 20:53:26 +08:00
|
|
|
|
|
2025-07-15 13:21:34 +08:00
|
|
|
|
// 构建查询命令
|
|
|
|
|
|
query := &queries.GetCategoryQuery{
|
|
|
|
|
|
ID: categoryID,
|
|
|
|
|
|
}
|
2025-07-20 20:53:26 +08:00
|
|
|
|
|
2025-07-15 13:21:34 +08:00
|
|
|
|
// 调用应用服务
|
|
|
|
|
|
category, err := h.categoryService.GetCategoryByID(c.Request.Context(), query)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
h.logger.Error("获取分类详情失败", zap.String("category_id", categoryID), zap.Error(err))
|
|
|
|
|
|
h.responseBuilder.NotFound(c, "分类不存在")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-07-20 20:53:26 +08:00
|
|
|
|
|
2025-07-15 13:21:34 +08:00
|
|
|
|
// 返回结果
|
|
|
|
|
|
h.responseBuilder.Success(c, category, "获取分类详情成功")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ================ 我的订阅相关方法 ================
|
|
|
|
|
|
|
|
|
|
|
|
// ListMySubscriptions 获取我的订阅列表
|
|
|
|
|
|
// @Summary 获取我的订阅列表
|
|
|
|
|
|
// @Description 获取当前用户的订阅列表
|
|
|
|
|
|
// @Tags 我的订阅
|
|
|
|
|
|
// @Accept json
|
|
|
|
|
|
// @Produce json
|
|
|
|
|
|
// @Security Bearer
|
|
|
|
|
|
// @Param page query int false "页码" default(1)
|
|
|
|
|
|
// @Param page_size query int false "每页数量" default(10)
|
2025-08-02 02:54:21 +08:00
|
|
|
|
// @Param keyword query string false "搜索关键词"
|
|
|
|
|
|
// @Param product_name query string false "产品名称"
|
|
|
|
|
|
// @Param start_time query string false "订阅开始时间" format(date-time)
|
|
|
|
|
|
// @Param end_time query string false "订阅结束时间" format(date-time)
|
2025-07-15 13:21:34 +08:00
|
|
|
|
// @Param sort_by query string false "排序字段"
|
|
|
|
|
|
// @Param sort_order query string false "排序方向" Enums(asc, desc)
|
|
|
|
|
|
// @Success 200 {object} responses.SubscriptionListResponse "获取订阅列表成功"
|
|
|
|
|
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
|
|
|
|
|
// @Failure 401 {object} map[string]interface{} "未认证"
|
|
|
|
|
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
|
|
|
|
|
// @Router /api/v1/my/subscriptions [get]
|
|
|
|
|
|
func (h *ProductHandler) ListMySubscriptions(c *gin.Context) {
|
|
|
|
|
|
userID := c.GetString("user_id")
|
|
|
|
|
|
if userID == "" {
|
2025-07-20 20:53:26 +08:00
|
|
|
|
h.responseBuilder.Unauthorized(c, "用户未登录")
|
2025-07-15 13:21:34 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var query queries.ListSubscriptionsQuery
|
2025-07-20 20:53:26 +08:00
|
|
|
|
if err := h.validator.ValidateQuery(c, &query); err != nil {
|
2025-08-02 02:54:21 +08:00
|
|
|
|
return
|
2025-07-15 13:21:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 设置默认值
|
|
|
|
|
|
if query.Page <= 0 {
|
|
|
|
|
|
query.Page = 1
|
|
|
|
|
|
}
|
|
|
|
|
|
if query.PageSize <= 0 {
|
|
|
|
|
|
query.PageSize = 10
|
|
|
|
|
|
}
|
2025-11-29 14:28:16 +08:00
|
|
|
|
if query.PageSize > 1000 {
|
|
|
|
|
|
query.PageSize = 1000
|
2025-07-15 13:21:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-02 02:54:21 +08:00
|
|
|
|
// 设置默认排序
|
|
|
|
|
|
if query.SortBy == "" {
|
|
|
|
|
|
query.SortBy = "created_at"
|
|
|
|
|
|
}
|
|
|
|
|
|
if query.SortOrder == "" {
|
|
|
|
|
|
query.SortOrder = "desc"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 用户端不支持企业名称筛选,清空该字段
|
|
|
|
|
|
query.CompanyName = ""
|
|
|
|
|
|
|
2025-07-28 23:28:24 +08:00
|
|
|
|
result, err := h.subAppService.ListMySubscriptions(c.Request.Context(), userID, &query)
|
2025-07-15 13:21:34 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
h.logger.Error("获取我的订阅列表失败", zap.Error(err), zap.String("user_id", userID))
|
|
|
|
|
|
h.responseBuilder.InternalError(c, "获取我的订阅列表失败")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
h.responseBuilder.Success(c, result, "获取我的订阅列表成功")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetMySubscriptionStats 获取我的订阅统计
|
|
|
|
|
|
// @Summary 获取我的订阅统计
|
|
|
|
|
|
// @Description 获取当前用户的订阅统计信息
|
|
|
|
|
|
// @Tags 我的订阅
|
|
|
|
|
|
// @Accept json
|
|
|
|
|
|
// @Produce json
|
|
|
|
|
|
// @Security Bearer
|
|
|
|
|
|
// @Success 200 {object} responses.SubscriptionStatsResponse "获取订阅统计成功"
|
|
|
|
|
|
// @Failure 401 {object} map[string]interface{} "未认证"
|
|
|
|
|
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
|
|
|
|
|
// @Router /api/v1/my/subscriptions/stats [get]
|
|
|
|
|
|
func (h *ProductHandler) GetMySubscriptionStats(c *gin.Context) {
|
|
|
|
|
|
userID := c.GetString("user_id")
|
|
|
|
|
|
if userID == "" {
|
2025-07-20 20:53:26 +08:00
|
|
|
|
h.responseBuilder.Unauthorized(c, "用户未登录")
|
2025-07-15 13:21:34 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-28 23:28:24 +08:00
|
|
|
|
result, err := h.subAppService.GetMySubscriptionStats(c.Request.Context(), userID)
|
2025-07-15 13:21:34 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
h.logger.Error("获取我的订阅统计失败", zap.Error(err), zap.String("user_id", userID))
|
|
|
|
|
|
h.responseBuilder.InternalError(c, "获取我的订阅统计失败")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
h.responseBuilder.Success(c, result, "获取我的订阅统计成功")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetMySubscriptionDetail 获取我的订阅详情
|
|
|
|
|
|
// @Summary 获取我的订阅详情
|
|
|
|
|
|
// @Description 获取指定订阅的详细信息
|
|
|
|
|
|
// @Tags 我的订阅
|
|
|
|
|
|
// @Accept json
|
|
|
|
|
|
// @Produce json
|
|
|
|
|
|
// @Security Bearer
|
|
|
|
|
|
// @Param id path string true "订阅ID"
|
|
|
|
|
|
// @Success 200 {object} responses.SubscriptionInfoResponse "获取订阅详情成功"
|
|
|
|
|
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
|
|
|
|
|
// @Failure 401 {object} map[string]interface{} "未认证"
|
|
|
|
|
|
// @Failure 404 {object} map[string]interface{} "订阅不存在"
|
|
|
|
|
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
|
|
|
|
|
// @Router /api/v1/my/subscriptions/{id} [get]
|
|
|
|
|
|
func (h *ProductHandler) GetMySubscriptionDetail(c *gin.Context) {
|
|
|
|
|
|
userID := c.GetString("user_id")
|
|
|
|
|
|
if userID == "" {
|
2025-07-20 20:53:26 +08:00
|
|
|
|
h.responseBuilder.Unauthorized(c, "用户未登录")
|
2025-07-15 13:21:34 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
subscriptionID := c.Param("id")
|
|
|
|
|
|
if subscriptionID == "" {
|
|
|
|
|
|
h.responseBuilder.BadRequest(c, "订阅ID不能为空")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var query queries.GetSubscriptionQuery
|
|
|
|
|
|
query.ID = subscriptionID
|
|
|
|
|
|
|
|
|
|
|
|
result, 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.responseBuilder.NotFound(c, "订阅不存在")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-02 02:54:21 +08:00
|
|
|
|
// 验证订阅是否属于当前用户
|
|
|
|
|
|
if result.UserID != userID {
|
|
|
|
|
|
h.logger.Error("用户尝试访问不属于自己的订阅", zap.String("user_id", userID), zap.String("subscription_user_id", result.UserID), zap.String("subscription_id", subscriptionID))
|
|
|
|
|
|
h.responseBuilder.Forbidden(c, "无权访问此订阅")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-15 13:21:34 +08:00
|
|
|
|
h.responseBuilder.Success(c, result, "获取我的订阅详情成功")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetMySubscriptionUsage 获取我的订阅使用情况
|
|
|
|
|
|
// @Summary 获取我的订阅使用情况
|
|
|
|
|
|
// @Description 获取指定订阅的使用情况统计
|
|
|
|
|
|
// @Tags 我的订阅
|
|
|
|
|
|
// @Accept json
|
|
|
|
|
|
// @Produce json
|
|
|
|
|
|
// @Security Bearer
|
|
|
|
|
|
// @Param id path string true "订阅ID"
|
2025-07-31 15:41:00 +08:00
|
|
|
|
// @Success 200 {object} map[string]interface{} "获取使用情况成功"
|
2025-07-15 13:21:34 +08:00
|
|
|
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
|
|
|
|
|
// @Failure 401 {object} map[string]interface{} "未认证"
|
|
|
|
|
|
// @Failure 404 {object} map[string]interface{} "订阅不存在"
|
|
|
|
|
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
|
|
|
|
|
// @Router /api/v1/my/subscriptions/{id}/usage [get]
|
|
|
|
|
|
func (h *ProductHandler) GetMySubscriptionUsage(c *gin.Context) {
|
2025-08-02 02:54:21 +08:00
|
|
|
|
userID := c.GetString("user_id")
|
|
|
|
|
|
if userID == "" {
|
|
|
|
|
|
h.responseBuilder.Unauthorized(c, "用户未登录")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-31 15:41:00 +08:00
|
|
|
|
subscriptionID := c.Param("id")
|
|
|
|
|
|
if subscriptionID == "" {
|
|
|
|
|
|
h.responseBuilder.BadRequest(c, "订阅ID不能为空")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-04 12:30:33 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证订阅是否属于当前用户(通过获取订阅详情来验证)
|
2025-08-02 02:54:21 +08:00
|
|
|
|
var query queries.GetSubscriptionQuery
|
|
|
|
|
|
query.ID = subscriptionID
|
|
|
|
|
|
subscription, err := h.subAppService.GetSubscriptionByID(c.Request.Context(), &query)
|
|
|
|
|
|
if err != nil {
|
2025-12-04 12:30:33 +08:00
|
|
|
|
h.logger.Error("获取订阅详情失败", zap.Error(err), zap.String("subscription_id", subscriptionID))
|
2025-08-02 02:54:21 +08:00
|
|
|
|
h.responseBuilder.NotFound(c, "订阅不存在")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证订阅是否属于当前用户
|
|
|
|
|
|
if subscription.UserID != userID {
|
2025-12-04 12:30:33 +08:00
|
|
|
|
h.logger.Error("用户尝试访问不属于自己的订阅", zap.String("user_id", userID), zap.String("subscription_user_id", subscription.UserID), zap.String("subscription_id", subscriptionID))
|
2025-08-02 02:54:21 +08:00
|
|
|
|
h.responseBuilder.Forbidden(c, "无权访问此订阅")
|
2025-07-15 13:21:34 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-04 12:30:33 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
subscriptionID := c.Param("id")
|
|
|
|
|
|
if subscriptionID == "" {
|
|
|
|
|
|
h.responseBuilder.BadRequest(c, "订阅ID不能为空")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
err := h.subAppService.CancelMySubscription(c.Request.Context(), userID, subscriptionID)
|
2025-07-31 15:41:00 +08:00
|
|
|
|
if err != nil {
|
2025-12-04 12:30:33 +08:00
|
|
|
|
h.logger.Error("取消订阅失败", zap.Error(err), zap.String("user_id", userID), zap.String("subscription_id", subscriptionID))
|
2025-12-04 18:10:14 +08:00
|
|
|
|
|
2025-12-04 12:30:33 +08:00
|
|
|
|
// 根据错误类型返回不同的响应
|
|
|
|
|
|
if err.Error() == "订阅不存在" {
|
|
|
|
|
|
h.responseBuilder.NotFound(c, "订阅不存在")
|
|
|
|
|
|
} else if err.Error() == "无权取消此订阅" {
|
|
|
|
|
|
h.responseBuilder.Forbidden(c, "无权取消此订阅")
|
|
|
|
|
|
} else {
|
|
|
|
|
|
h.responseBuilder.BadRequest(c, err.Error())
|
|
|
|
|
|
}
|
2025-07-15 13:21:34 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-04 12:30:33 +08:00
|
|
|
|
h.responseBuilder.Success(c, nil, "取消订阅成功")
|
2025-07-31 15:41:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetProductDocumentation 获取产品文档
|
|
|
|
|
|
// @Summary 获取产品文档
|
|
|
|
|
|
// @Description 获取指定产品的文档信息
|
|
|
|
|
|
// @Tags 数据大厅
|
|
|
|
|
|
// @Accept json
|
|
|
|
|
|
// @Produce json
|
|
|
|
|
|
// @Param id path string true "产品ID"
|
2025-09-01 18:29:59 +08:00
|
|
|
|
// @Success 200 {object} responses.DocumentationResponse "获取产品文档成功"
|
2025-07-31 15:41:00 +08:00
|
|
|
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
2025-09-01 18:29:59 +08:00
|
|
|
|
// @Failure 404 {object} map[string]interface{} "产品不存在"
|
2025-07-31 15:41:00 +08:00
|
|
|
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
|
|
|
|
|
// @Router /api/v1/products/{id}/documentation [get]
|
|
|
|
|
|
func (h *ProductHandler) GetProductDocumentation(c *gin.Context) {
|
|
|
|
|
|
productID := c.Param("id")
|
|
|
|
|
|
if productID == "" {
|
|
|
|
|
|
h.responseBuilder.BadRequest(c, "产品ID不能为空")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
doc, err := h.documentationAppService.GetDocumentationByProductID(c.Request.Context(), productID)
|
2025-07-15 13:21:34 +08:00
|
|
|
|
if err != nil {
|
2025-07-31 15:41:00 +08:00
|
|
|
|
h.logger.Error("获取产品文档失败", zap.Error(err))
|
|
|
|
|
|
h.responseBuilder.NotFound(c, "文档不存在")
|
2025-07-15 13:21:34 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-31 15:41:00 +08:00
|
|
|
|
h.responseBuilder.Success(c, doc, "获取文档成功")
|
2025-07-15 13:21:34 +08:00
|
|
|
|
}
|
2025-12-03 12:03:42 +08:00
|
|
|
|
|
|
|
|
|
|
// 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
|
2025-12-04 18:10:14 +08:00
|
|
|
|
var docVersion string
|
2025-12-03 12:03:42 +08:00
|
|
|
|
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,
|
|
|
|
|
|
}
|
2025-12-04 18:10:14 +08:00
|
|
|
|
docVersion = doc.Version
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 如果没有文档,使用默认版本号
|
|
|
|
|
|
docVersion = "1.0"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-11 11:14:31 +08:00
|
|
|
|
// 如果是组合包,获取子产品的文档信息
|
|
|
|
|
|
var subProductDocs []*entities.ProductDocumentation
|
|
|
|
|
|
if product.IsPackage && len(product.PackageItems) > 0 {
|
|
|
|
|
|
h.logger.Info("检测到组合包,开始获取子产品文档",
|
|
|
|
|
|
zap.String("product_id", productID),
|
|
|
|
|
|
zap.Int("sub_product_count", len(product.PackageItems)),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 收集所有子产品的ID
|
|
|
|
|
|
subProductIDs := make([]string, 0, len(product.PackageItems))
|
|
|
|
|
|
for _, item := range product.PackageItems {
|
|
|
|
|
|
subProductIDs = append(subProductIDs, item.ProductID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 批量获取子产品的文档
|
|
|
|
|
|
subDocs, err := h.documentationAppService.GetDocumentationsByProductIDs(c.Request.Context(), subProductIDs)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
h.logger.Warn("获取组合包子产品文档失败",
|
|
|
|
|
|
zap.String("product_id", productID),
|
|
|
|
|
|
zap.Error(err),
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 转换为entity类型,并按PackageItems的顺序排序
|
|
|
|
|
|
docMap := make(map[string]*entities.ProductDocumentation)
|
|
|
|
|
|
for i := range subDocs {
|
|
|
|
|
|
docMap[subDocs[i].ProductID] = &entities.ProductDocumentation{
|
|
|
|
|
|
ID: subDocs[i].ID,
|
|
|
|
|
|
ProductID: subDocs[i].ProductID,
|
|
|
|
|
|
RequestURL: subDocs[i].RequestURL,
|
|
|
|
|
|
RequestMethod: subDocs[i].RequestMethod,
|
|
|
|
|
|
BasicInfo: subDocs[i].BasicInfo,
|
|
|
|
|
|
RequestParams: subDocs[i].RequestParams,
|
|
|
|
|
|
ResponseFields: subDocs[i].ResponseFields,
|
|
|
|
|
|
ResponseExample: subDocs[i].ResponseExample,
|
|
|
|
|
|
ErrorCodes: subDocs[i].ErrorCodes,
|
|
|
|
|
|
Version: subDocs[i].Version,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 按PackageItems的顺序构建子产品文档列表
|
|
|
|
|
|
for _, item := range product.PackageItems {
|
|
|
|
|
|
if subDoc, exists := docMap[item.ProductID]; exists {
|
|
|
|
|
|
subProductDocs = append(subProductDocs, subDoc)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
h.logger.Info("成功获取组合包子产品文档",
|
|
|
|
|
|
zap.String("product_id", productID),
|
|
|
|
|
|
zap.Int("total_sub_products", len(product.PackageItems)),
|
|
|
|
|
|
zap.Int("docs_found", len(subProductDocs)),
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-04 18:10:14 +08:00
|
|
|
|
// 尝试从缓存获取PDF
|
|
|
|
|
|
var pdfBytes []byte
|
|
|
|
|
|
var cacheHit bool
|
|
|
|
|
|
|
|
|
|
|
|
if h.pdfCacheManager != nil {
|
|
|
|
|
|
var cacheErr error
|
|
|
|
|
|
pdfBytes, cacheHit, cacheErr = h.pdfCacheManager.Get(productID, docVersion)
|
|
|
|
|
|
if cacheErr != nil {
|
|
|
|
|
|
h.logger.Warn("从缓存获取PDF失败,将重新生成",
|
|
|
|
|
|
zap.String("product_id", productID),
|
|
|
|
|
|
zap.Error(cacheErr),
|
|
|
|
|
|
)
|
|
|
|
|
|
} else if cacheHit {
|
|
|
|
|
|
h.logger.Info("PDF缓存命中",
|
|
|
|
|
|
zap.String("product_id", productID),
|
|
|
|
|
|
zap.String("version", docVersion),
|
|
|
|
|
|
zap.Int("pdf_size", len(pdfBytes)),
|
|
|
|
|
|
)
|
|
|
|
|
|
// 直接返回缓存的PDF
|
|
|
|
|
|
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, "|", "_")
|
|
|
|
|
|
|
|
|
|
|
|
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.Header("X-Cache", "HIT") // 添加缓存命中标识
|
|
|
|
|
|
c.Data(http.StatusOK, "application/pdf", pdfBytes)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-12-03 12:03:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-04 18:10:14 +08:00
|
|
|
|
// 缓存未命中,需要生成PDF
|
|
|
|
|
|
h.logger.Info("PDF缓存未命中,开始生成PDF",
|
2025-12-03 12:03:42 +08:00
|
|
|
|
zap.String("product_id", productID),
|
|
|
|
|
|
zap.String("product_name", product.Name),
|
2025-12-04 18:10:14 +08:00
|
|
|
|
zap.String("version", docVersion),
|
2025-12-03 12:03:42 +08:00
|
|
|
|
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))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
|
2025-12-11 11:14:31 +08:00
|
|
|
|
// 构建Product实体(用于PDF生成)
|
|
|
|
|
|
productEntity := &entities.Product{
|
|
|
|
|
|
ID: product.ID,
|
|
|
|
|
|
Name: product.Name,
|
|
|
|
|
|
Code: product.Code,
|
|
|
|
|
|
Description: product.Description,
|
|
|
|
|
|
Content: product.Content,
|
|
|
|
|
|
IsPackage: product.IsPackage,
|
|
|
|
|
|
Price: decimal.NewFromFloat(product.Price),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果是组合包,添加子产品信息
|
|
|
|
|
|
if product.IsPackage && len(product.PackageItems) > 0 {
|
|
|
|
|
|
productEntity.PackageItems = make([]*entities.ProductPackageItem, len(product.PackageItems))
|
|
|
|
|
|
for i, item := range product.PackageItems {
|
|
|
|
|
|
productEntity.PackageItems[i] = &entities.ProductPackageItem{
|
|
|
|
|
|
ID: item.ID,
|
|
|
|
|
|
PackageID: product.ID,
|
|
|
|
|
|
ProductID: item.ProductID,
|
|
|
|
|
|
SortOrder: item.SortOrder,
|
|
|
|
|
|
Product: &entities.Product{
|
|
|
|
|
|
ID: item.ProductID,
|
|
|
|
|
|
Code: item.ProductCode,
|
|
|
|
|
|
Name: item.ProductName,
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-03 12:03:42 +08:00
|
|
|
|
// 直接调用PDF生成器(简化版本,不使用goroutine)
|
2025-12-11 11:14:31 +08:00
|
|
|
|
h.logger.Info("开始调用PDF生成器",
|
|
|
|
|
|
zap.Bool("is_package", product.IsPackage),
|
|
|
|
|
|
zap.Int("sub_product_docs_count", len(subProductDocs)),
|
2025-12-03 12:03:42 +08:00
|
|
|
|
)
|
2025-12-11 11:14:31 +08:00
|
|
|
|
|
|
|
|
|
|
// 使用重构后的生成器
|
|
|
|
|
|
refactoredGen := pdf.NewPDFGeneratorRefactored(h.logger)
|
|
|
|
|
|
var genErr error
|
|
|
|
|
|
|
|
|
|
|
|
if product.IsPackage && len(subProductDocs) > 0 {
|
|
|
|
|
|
// 组合包:使用支持子产品文档的方法
|
|
|
|
|
|
pdfBytes, genErr = refactoredGen.GenerateProductPDFWithSubProducts(
|
|
|
|
|
|
c.Request.Context(),
|
|
|
|
|
|
productEntity,
|
|
|
|
|
|
docEntity,
|
|
|
|
|
|
subProductDocs,
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 普通产品:使用标准方法
|
|
|
|
|
|
pdfBytes, genErr = refactoredGen.GenerateProductPDFFromEntity(
|
|
|
|
|
|
c.Request.Context(),
|
|
|
|
|
|
productEntity,
|
|
|
|
|
|
docEntity,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2025-12-03 12:03:42 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-04 18:10:14 +08:00
|
|
|
|
// 保存到缓存(异步,不阻塞响应)
|
|
|
|
|
|
if h.pdfCacheManager != nil {
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
if err := h.pdfCacheManager.Set(productID, docVersion, pdfBytes); err != nil {
|
|
|
|
|
|
h.logger.Warn("保存PDF到缓存失败",
|
|
|
|
|
|
zap.String("product_id", productID),
|
|
|
|
|
|
zap.String("version", docVersion),
|
|
|
|
|
|
zap.Error(err),
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-03 12:03:42 +08:00
|
|
|
|
// 生成文件名(清理文件名中的非法字符)
|
|
|
|
|
|
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)),
|
2025-12-04 18:10:14 +08:00
|
|
|
|
zap.Bool("cached", false),
|
2025-12-03 12:03:42 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 设置响应头并返回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)))
|
2025-12-04 18:10:14 +08:00
|
|
|
|
c.Header("X-Cache", "MISS") // 添加缓存未命中标识
|
2025-12-03 12:03:42 +08:00
|
|
|
|
c.Data(http.StatusOK, "application/pdf", pdfBytes)
|
|
|
|
|
|
}
|