fix
This commit is contained in:
@@ -41,3 +41,21 @@ type UpdateProductCommand struct {
|
||||
type DeleteProductCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required,uuid" comment:"产品ID"`
|
||||
}
|
||||
|
||||
// CreateProductApiConfigCommand 创建产品API配置命令
|
||||
type CreateProductApiConfigCommand struct {
|
||||
ProductID string `json:"product_id" binding:"required,uuid" comment:"产品ID"`
|
||||
ApiEndpoint string `json:"api_endpoint" binding:"required,url" comment:"API端点"`
|
||||
ApiKey string `json:"api_key" binding:"required" comment:"API密钥"`
|
||||
ApiSecret string `json:"api_secret" binding:"required" comment:"API密钥"`
|
||||
Config string `json:"config" binding:"omitempty" comment:"配置信息"`
|
||||
}
|
||||
|
||||
// UpdateProductApiConfigCommand 更新产品API配置命令
|
||||
type UpdateProductApiConfigCommand struct {
|
||||
ProductID string `json:"product_id" binding:"required,uuid" comment:"产品ID"`
|
||||
ApiEndpoint string `json:"api_endpoint" binding:"required,url" comment:"API端点"`
|
||||
ApiKey string `json:"api_key" binding:"required" comment:"API密钥"`
|
||||
ApiSecret string `json:"api_secret" binding:"required" comment:"API密钥"`
|
||||
Config string `json:"config" binding:"omitempty" comment:"配置信息"`
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ type ProductInfoResponse struct {
|
||||
CategoryID string `json:"category_id" comment:"产品分类ID"`
|
||||
Price float64 `json:"price" comment:"产品价格"`
|
||||
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
|
||||
IsVisible bool `json:"is_visible" comment:"是否展示"`
|
||||
IsPackage bool `json:"is_package" comment:"是否组合包"`
|
||||
IsSubscribed *bool `json:"is_subscribed,omitempty" comment:"当前用户是否已订阅"`
|
||||
|
||||
// SEO信息
|
||||
SEOTitle string `json:"seo_title" comment:"SEO标题"`
|
||||
@@ -65,6 +65,7 @@ type ProductSimpleResponse struct {
|
||||
Category *CategorySimpleResponse `json:"category,omitempty" comment:"分类信息"`
|
||||
Price float64 `json:"price" comment:"产品价格"`
|
||||
IsPackage bool `json:"is_package" comment:"是否组合包"`
|
||||
IsSubscribed *bool `json:"is_subscribed,omitempty" comment:"当前用户是否已订阅"`
|
||||
}
|
||||
|
||||
// ProductStatsResponse 产品统计响应
|
||||
|
||||
@@ -17,6 +17,7 @@ type ProductApplicationService interface {
|
||||
|
||||
GetProductByID(ctx context.Context, query *queries.GetProductQuery) (*responses.ProductInfoResponse, error)
|
||||
ListProducts(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.ProductListResponse, error)
|
||||
ListProductsWithSubscriptionStatus(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.ProductListResponse, error)
|
||||
GetProductsByIDs(ctx context.Context, query *queries.GetProductsByIDsQuery) ([]*responses.ProductInfoResponse, error)
|
||||
|
||||
// 业务查询
|
||||
|
||||
@@ -18,10 +18,10 @@ import (
|
||||
// ProductApplicationServiceImpl 产品应用服务实现
|
||||
// 负责业务流程编排、事务管理、数据转换,不直接操作仓库
|
||||
type ProductApplicationServiceImpl struct {
|
||||
productManagementService *product_service.ProductManagementService
|
||||
productSubscriptionService *product_service.ProductSubscriptionService
|
||||
productApiConfigAppService ProductApiConfigApplicationService
|
||||
logger *zap.Logger
|
||||
productManagementService *product_service.ProductManagementService
|
||||
productSubscriptionService *product_service.ProductSubscriptionService
|
||||
productApiConfigAppService ProductApiConfigApplicationService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewProductApplicationService 创建产品应用服务
|
||||
@@ -32,10 +32,10 @@ func NewProductApplicationService(
|
||||
logger *zap.Logger,
|
||||
) ProductApplicationService {
|
||||
return &ProductApplicationServiceImpl{
|
||||
productManagementService: productManagementService,
|
||||
productSubscriptionService: productSubscriptionService,
|
||||
productApiConfigAppService: productApiConfigAppService,
|
||||
logger: logger,
|
||||
productManagementService: productManagementService,
|
||||
productSubscriptionService: productSubscriptionService,
|
||||
productApiConfigAppService: productApiConfigAppService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +99,11 @@ func (s *ProductApplicationServiceImpl) DeleteProduct(ctx context.Context, cmd *
|
||||
// ListProducts 获取产品列表
|
||||
// 业务流程:1. 获取产品列表 2. 构建响应数据
|
||||
func (s *ProductApplicationServiceImpl) ListProducts(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.ProductListResponse, error) {
|
||||
// 检查是否有用户ID,如果有则使用带订阅状态的方法
|
||||
if userID, ok := filters["user_id"].(string); ok && userID != "" {
|
||||
return s.ListProductsWithSubscriptionStatus(ctx, filters, options)
|
||||
}
|
||||
|
||||
// 调用领域服务获取产品列表
|
||||
products, total, err := s.productManagementService.ListProducts(ctx, filters, options)
|
||||
if err != nil {
|
||||
@@ -119,6 +124,36 @@ func (s *ProductApplicationServiceImpl) ListProducts(ctx context.Context, filter
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListProductsWithSubscriptionStatus 获取产品列表(包含订阅状态)
|
||||
// 业务流程:1. 获取产品列表和订阅状态 2. 构建响应数据
|
||||
func (s *ProductApplicationServiceImpl) ListProductsWithSubscriptionStatus(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.ProductListResponse, error) {
|
||||
// 调用领域服务获取产品列表(包含订阅状态)
|
||||
products, subscriptionStatusMap, total, err := s.productManagementService.ListProductsWithSubscriptionStatus(ctx, filters, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为响应对象
|
||||
items := make([]responses.ProductInfoResponse, len(products))
|
||||
for i := range products {
|
||||
item := s.convertToProductInfoResponse(products[i])
|
||||
|
||||
// 设置订阅状态
|
||||
if isSubscribed, exists := subscriptionStatusMap[products[i].ID]; exists {
|
||||
item.IsSubscribed = &isSubscribed
|
||||
}
|
||||
|
||||
items[i] = *item
|
||||
}
|
||||
|
||||
return &responses.ProductListResponse{
|
||||
Total: total,
|
||||
Page: options.Page,
|
||||
Size: options.PageSize,
|
||||
Items: items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetProductsByIDs 根据ID列表获取产品
|
||||
// 业务流程:1. 获取产品列表 2. 构建响应数据
|
||||
func (s *ProductApplicationServiceImpl) GetProductsByIDs(ctx context.Context, query *appQueries.GetProductsByIDsQuery) ([]*responses.ProductInfoResponse, error) {
|
||||
@@ -353,21 +388,20 @@ func (s *ProductApplicationServiceImpl) GetAvailableProducts(ctx context.Context
|
||||
// convertToProductInfoResponse 转换为产品信息响应
|
||||
func (s *ProductApplicationServiceImpl) convertToProductInfoResponse(product *entities.Product) *responses.ProductInfoResponse {
|
||||
response := &responses.ProductInfoResponse{
|
||||
ID: product.ID,
|
||||
Name: product.Name,
|
||||
Code: product.Code,
|
||||
Description: product.Description,
|
||||
Content: product.Content,
|
||||
CategoryID: product.CategoryID,
|
||||
Price: product.Price.InexactFloat64(),
|
||||
IsEnabled: product.IsEnabled,
|
||||
IsVisible: product.IsVisible,
|
||||
IsPackage: product.IsPackage,
|
||||
SEOTitle: product.SEOTitle,
|
||||
ID: product.ID,
|
||||
Name: product.Name,
|
||||
Code: product.Code,
|
||||
Description: product.Description,
|
||||
Content: product.Content,
|
||||
CategoryID: product.CategoryID,
|
||||
Price: product.Price.InexactFloat64(),
|
||||
IsEnabled: product.IsEnabled,
|
||||
IsPackage: product.IsPackage,
|
||||
SEOTitle: product.SEOTitle,
|
||||
SEODescription: product.SEODescription,
|
||||
SEOKeywords: product.SEOKeywords,
|
||||
CreatedAt: product.CreatedAt,
|
||||
UpdatedAt: product.UpdatedAt,
|
||||
SEOKeywords: product.SEOKeywords,
|
||||
CreatedAt: product.CreatedAt,
|
||||
UpdatedAt: product.UpdatedAt,
|
||||
}
|
||||
|
||||
// 添加分类信息
|
||||
@@ -423,4 +457,4 @@ func (s *ProductApplicationServiceImpl) UpdateProductApiConfig(ctx context.Conte
|
||||
// DeleteProductApiConfig 删除产品API配置
|
||||
func (s *ProductApplicationServiceImpl) DeleteProductApiConfig(ctx context.Context, configID string) error {
|
||||
return s.productApiConfigAppService.DeleteProductApiConfig(ctx, configID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ type ProductRepository interface {
|
||||
|
||||
// 复杂查询方法
|
||||
ListProducts(ctx context.Context, query *queries.ListProductsQuery) ([]*entities.Product, int64, error)
|
||||
ListProductsWithSubscriptionStatus(ctx context.Context, query *queries.ListProductsQuery) ([]*entities.Product, map[string]bool, int64, error)
|
||||
|
||||
// 业务查询方法
|
||||
FindSubscribableProducts(ctx context.Context, userID string) ([]*entities.Product, error)
|
||||
|
||||
@@ -2,17 +2,19 @@ package queries
|
||||
|
||||
// ListProductsQuery 产品列表查询
|
||||
type ListProductsQuery struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Keyword string `json:"keyword"`
|
||||
CategoryID string `json:"category_id"`
|
||||
MinPrice *float64 `json:"min_price"`
|
||||
MaxPrice *float64 `json:"max_price"`
|
||||
IsEnabled *bool `json:"is_enabled"`
|
||||
IsVisible *bool `json:"is_visible"`
|
||||
IsPackage *bool `json:"is_package"`
|
||||
SortBy string `json:"sort_by"`
|
||||
SortOrder string `json:"sort_order"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Keyword string `json:"keyword"`
|
||||
CategoryID string `json:"category_id"`
|
||||
MinPrice *float64 `json:"min_price"`
|
||||
MaxPrice *float64 `json:"max_price"`
|
||||
IsEnabled *bool `json:"is_enabled"`
|
||||
IsVisible *bool `json:"is_visible"`
|
||||
IsPackage *bool `json:"is_package"`
|
||||
UserID string `json:"user_id"` // 用户ID,用于查询订阅状态
|
||||
IsSubscribed *bool `json:"is_subscribed"` // 是否已订阅筛选
|
||||
SortBy string `json:"sort_by"`
|
||||
SortOrder string `json:"sort_order"`
|
||||
}
|
||||
|
||||
// SearchProductsQuery 产品搜索查询
|
||||
|
||||
@@ -334,4 +334,55 @@ func (s *ProductManagementService) ListProducts(ctx context.Context, filters map
|
||||
)
|
||||
|
||||
return products, total, nil
|
||||
}
|
||||
|
||||
// ListProductsWithSubscriptionStatus 获取产品列表(包含订阅状态)
|
||||
func (s *ProductManagementService) ListProductsWithSubscriptionStatus(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) ([]*entities.Product, map[string]bool, int64, error) {
|
||||
// 构建查询条件
|
||||
query := &queries.ListProductsQuery{
|
||||
Page: options.Page,
|
||||
PageSize: options.PageSize,
|
||||
SortBy: options.Sort,
|
||||
SortOrder: options.Order,
|
||||
}
|
||||
|
||||
// 应用筛选条件
|
||||
if keyword, ok := filters["keyword"].(string); ok && keyword != "" {
|
||||
query.Keyword = keyword
|
||||
}
|
||||
if categoryID, ok := filters["category_id"].(string); ok && categoryID != "" {
|
||||
query.CategoryID = categoryID
|
||||
}
|
||||
if isEnabled, ok := filters["is_enabled"].(bool); ok {
|
||||
query.IsEnabled = &isEnabled
|
||||
}
|
||||
if isVisible, ok := filters["is_visible"].(bool); ok {
|
||||
query.IsVisible = &isVisible
|
||||
}
|
||||
if isPackage, ok := filters["is_package"].(bool); ok {
|
||||
query.IsPackage = &isPackage
|
||||
}
|
||||
if userID, ok := filters["user_id"].(string); ok && userID != "" {
|
||||
query.UserID = userID
|
||||
}
|
||||
if isSubscribed, ok := filters["is_subscribed"].(bool); ok {
|
||||
query.IsSubscribed = &isSubscribed
|
||||
}
|
||||
|
||||
// 调用仓储层获取产品列表(包含订阅状态)
|
||||
products, subscriptionStatusMap, total, err := s.productRepo.ListProductsWithSubscriptionStatus(ctx, query)
|
||||
if err != nil {
|
||||
s.logger.Error("获取产品列表失败", zap.Error(err))
|
||||
return nil, nil, 0, fmt.Errorf("获取产品列表失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("产品列表查询成功",
|
||||
zap.Int("count", len(products)),
|
||||
zap.Int64("total", total),
|
||||
zap.Int("page", options.Page),
|
||||
zap.Int("page_size", options.PageSize),
|
||||
zap.String("user_id", query.UserID),
|
||||
)
|
||||
|
||||
return products, subscriptionStatusMap, total, nil
|
||||
}
|
||||
@@ -183,6 +183,110 @@ func (r *GormProductRepository) ListProducts(ctx context.Context, query *queries
|
||||
return result, total, nil
|
||||
}
|
||||
|
||||
// ListProductsWithSubscriptionStatus 获取产品列表(包含订阅状态)
|
||||
func (r *GormProductRepository) ListProductsWithSubscriptionStatus(ctx context.Context, query *queries.ListProductsQuery) ([]*entities.Product, map[string]bool, int64, error) {
|
||||
var productEntities []entities.Product
|
||||
var total int64
|
||||
|
||||
dbQuery := r.GetDB(ctx).Model(&entities.Product{})
|
||||
|
||||
// 应用筛选条件
|
||||
if query.Keyword != "" {
|
||||
dbQuery = dbQuery.Where("name LIKE ? OR description LIKE ? OR code LIKE ?",
|
||||
"%"+query.Keyword+"%", "%"+query.Keyword+"%", "%"+query.Keyword+"%")
|
||||
}
|
||||
if query.CategoryID != "" {
|
||||
dbQuery = dbQuery.Where("category_id = ?", query.CategoryID)
|
||||
}
|
||||
if query.MinPrice != nil {
|
||||
dbQuery = dbQuery.Where("price >= ?", *query.MinPrice)
|
||||
}
|
||||
if query.MaxPrice != nil {
|
||||
dbQuery = dbQuery.Where("price <= ?", *query.MaxPrice)
|
||||
}
|
||||
if query.IsEnabled != nil {
|
||||
dbQuery = dbQuery.Where("is_enabled = ?", *query.IsEnabled)
|
||||
}
|
||||
if query.IsVisible != nil {
|
||||
dbQuery = dbQuery.Where("is_visible = ?", *query.IsVisible)
|
||||
}
|
||||
if query.IsPackage != nil {
|
||||
dbQuery = dbQuery.Where("is_package = ?", *query.IsPackage)
|
||||
}
|
||||
|
||||
// 如果指定了用户ID,添加订阅状态筛选
|
||||
if query.UserID != "" && query.IsSubscribed != nil {
|
||||
if *query.IsSubscribed {
|
||||
// 筛选已订阅的产品
|
||||
dbQuery = dbQuery.Where("EXISTS (SELECT 1 FROM subscription WHERE subscription.product_id = product.id AND subscription.user_id = ?)", query.UserID)
|
||||
} else {
|
||||
// 筛选未订阅的产品
|
||||
dbQuery = dbQuery.Where("NOT EXISTS (SELECT 1 FROM subscription WHERE subscription.product_id = product.id AND subscription.user_id = ?)", query.UserID)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
if err := dbQuery.Count(&total).Error; err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
// 应用排序
|
||||
if query.SortBy != "" {
|
||||
order := query.SortBy
|
||||
if query.SortOrder == "desc" {
|
||||
order += " DESC"
|
||||
} else {
|
||||
order += " ASC"
|
||||
}
|
||||
dbQuery = dbQuery.Order(order)
|
||||
} else {
|
||||
dbQuery = dbQuery.Order("created_at DESC")
|
||||
}
|
||||
|
||||
// 应用分页
|
||||
if query.Page > 0 && query.PageSize > 0 {
|
||||
offset := (query.Page - 1) * query.PageSize
|
||||
dbQuery = dbQuery.Offset(offset).Limit(query.PageSize)
|
||||
}
|
||||
|
||||
// 预加载分类信息并获取数据
|
||||
if err := dbQuery.Preload("Category").Find(&productEntities).Error; err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
// 转换为指针切片
|
||||
result := make([]*entities.Product, len(productEntities))
|
||||
for i := range productEntities {
|
||||
result[i] = &productEntities[i]
|
||||
}
|
||||
|
||||
// 获取订阅状态映射
|
||||
subscriptionStatusMap := make(map[string]bool)
|
||||
if query.UserID != "" && len(result) > 0 {
|
||||
productIDs := make([]string, len(result))
|
||||
for i, product := range result {
|
||||
productIDs[i] = product.ID
|
||||
}
|
||||
|
||||
// 查询用户的订阅状态
|
||||
var subscriptions []struct {
|
||||
ProductID string `gorm:"column:product_id"`
|
||||
}
|
||||
err := r.GetDB(ctx).Table("subscription").
|
||||
Select("product_id").
|
||||
Where("user_id = ? AND product_id IN ?", query.UserID, productIDs).
|
||||
Find(&subscriptions).Error
|
||||
|
||||
if err == nil {
|
||||
for _, sub := range subscriptions {
|
||||
subscriptionStatusMap[sub.ProductID] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, subscriptionStatusMap, total, nil
|
||||
}
|
||||
|
||||
// FindSubscribableProducts 查找可订阅产品
|
||||
func (r *GormProductRepository) FindSubscribableProducts(ctx context.Context, userID string) ([]*entities.Product, error) {
|
||||
var productEntities []entities.Product
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
SubscriptionsTable = "subscriptions"
|
||||
SubscriptionsTable = "subscription"
|
||||
SubscriptionCacheTTL = 60 * time.Minute
|
||||
)
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ func NewProductHandler(
|
||||
// @Param is_enabled query bool false "是否启用"
|
||||
// @Param is_visible query bool false "是否可见"
|
||||
// @Param is_package query bool false "是否组合包"
|
||||
// @Param is_subscribed query bool false "是否已订阅(需要认证)"
|
||||
// @Param sort_by query string false "排序字段"
|
||||
// @Param sort_order query string false "排序方向" Enums(asc, desc)
|
||||
// @Success 200 {object} responses.ProductListResponse "获取产品列表成功"
|
||||
@@ -63,6 +64,9 @@ func NewProductHandler(
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/v1/products [get]
|
||||
func (h *ProductHandler) ListProducts(c *gin.Context) {
|
||||
// 获取当前用户ID(可选认证)
|
||||
userID := h.getCurrentUserID(c)
|
||||
|
||||
// 解析查询参数
|
||||
page := h.getIntQuery(c, "page", 1)
|
||||
pageSize := h.getIntQuery(c, "page_size", 10)
|
||||
@@ -101,6 +105,17 @@ func (h *ProductHandler) ListProducts(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// 订阅状态筛选(需要认证)
|
||||
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
|
||||
}
|
||||
|
||||
// 排序字段
|
||||
sortBy := c.Query("sort_by")
|
||||
if sortBy == "" {
|
||||
@@ -141,6 +156,16 @@ func (h *ProductHandler) getIntQuery(c *gin.Context, key string, defaultValue in
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// 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 ""
|
||||
}
|
||||
|
||||
// GetProductDetail 获取产品详情
|
||||
// @Summary 获取产品详情
|
||||
// @Description 根据产品ID获取产品详细信息
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
type ProductRoutes struct {
|
||||
productHandler *handlers.ProductHandler
|
||||
auth *middleware.JWTAuthMiddleware
|
||||
optionalAuth *middleware.OptionalAuthMiddleware
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
@@ -19,11 +20,13 @@ type ProductRoutes struct {
|
||||
func NewProductRoutes(
|
||||
productHandler *handlers.ProductHandler,
|
||||
auth *middleware.JWTAuthMiddleware,
|
||||
optionalAuth *middleware.OptionalAuthMiddleware,
|
||||
logger *zap.Logger,
|
||||
) *ProductRoutes {
|
||||
return &ProductRoutes{
|
||||
productHandler: productHandler,
|
||||
auth: auth,
|
||||
optionalAuth: optionalAuth,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
@@ -36,7 +39,7 @@ func (r *ProductRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
products := engine.Group("/api/v1/products")
|
||||
{
|
||||
// 获取产品列表(分页+筛选)
|
||||
products.GET("", r.productHandler.ListProducts)
|
||||
products.GET("", r.optionalAuth.Handle(), r.productHandler.ListProducts)
|
||||
|
||||
// 获取产品统计
|
||||
products.GET("/stats", r.productHandler.GetProductStats)
|
||||
|
||||
Reference in New Issue
Block a user