This commit is contained in:
2025-07-15 13:21:34 +08:00
parent 807004f78d
commit 83bf9aea7d
44 changed files with 9798 additions and 8 deletions

View File

@@ -0,0 +1,495 @@
package product
import (
"context"
"fmt"
"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/domains/product/repositories"
repoQueries "tyapi-server/internal/domains/product/repositories/queries"
"tyapi-server/internal/shared/interfaces"
"go.uber.org/zap"
)
// CategoryApplicationServiceImpl 分类应用服务实现
type CategoryApplicationServiceImpl struct {
categoryRepo repositories.ProductCategoryRepository
logger *zap.Logger
}
// NewCategoryApplicationService 创建分类应用服务
func NewCategoryApplicationService(
categoryRepo repositories.ProductCategoryRepository,
logger *zap.Logger,
) CategoryApplicationService {
return &CategoryApplicationServiceImpl{
categoryRepo: categoryRepo,
logger: logger,
}
}
// CreateCategory 创建分类
func (s *CategoryApplicationServiceImpl) CreateCategory(ctx context.Context, cmd *commands.CreateCategoryCommand) error {
// 1. 参数验证
if err := s.validateCreateCategory(cmd); err != nil {
return err
}
// 2. 检查父分类是否存在
if cmd.ParentID != nil && *cmd.ParentID != "" {
_, err := s.categoryRepo.GetByID(ctx, *cmd.ParentID)
if err != nil {
return fmt.Errorf("父分类不存在: %w", err)
}
}
// 3. 创建分类实体
category := entities.ProductCategory{
Name: cmd.Name,
Code: cmd.Code,
Description: cmd.Description,
ParentID: cmd.ParentID,
Level: cmd.Level,
Sort: cmd.Sort,
IsEnabled: cmd.IsEnabled,
IsVisible: cmd.IsVisible,
}
// 4. 保存到仓储
_, err := s.categoryRepo.Create(ctx, category)
if err != nil {
s.logger.Error("创建分类失败", zap.Error(err))
return fmt.Errorf("创建分类失败: %w", err)
}
s.logger.Info("创建分类成功", zap.String("name", cmd.Name))
return nil
}
// UpdateCategory 更新分类
func (s *CategoryApplicationServiceImpl) UpdateCategory(ctx context.Context, cmd *commands.UpdateCategoryCommand) error {
// 1. 获取现有分类
category, err := s.categoryRepo.GetByID(ctx, cmd.ID)
if err != nil {
return fmt.Errorf("分类不存在: %w", err)
}
// 2. 更新字段
if cmd.Name != "" {
category.Name = cmd.Name
}
if cmd.Code != "" {
category.Code = cmd.Code
}
if cmd.Description != "" {
category.Description = cmd.Description
}
if cmd.ParentID != nil {
category.ParentID = cmd.ParentID
}
if cmd.Level > 0 {
category.Level = cmd.Level
}
if cmd.Sort > 0 {
category.Sort = cmd.Sort
}
if cmd.IsEnabled != nil {
category.IsEnabled = *cmd.IsEnabled
}
if cmd.IsVisible != nil {
category.IsVisible = *cmd.IsVisible
}
// 3. 保存到仓储
if err := s.categoryRepo.Update(ctx, category); err != nil {
s.logger.Error("更新分类失败", zap.Error(err))
return fmt.Errorf("更新分类失败: %w", err)
}
s.logger.Info("更新分类成功", zap.String("id", cmd.ID))
return nil
}
// DeleteCategory 删除分类
func (s *CategoryApplicationServiceImpl) DeleteCategory(ctx context.Context, cmd *commands.DeleteCategoryCommand) error {
// 1. 检查分类是否存在
_, err := s.categoryRepo.GetByID(ctx, cmd.ID)
if err != nil {
return fmt.Errorf("分类不存在: %w", err)
}
// 2. 检查是否有子分类
children, err := s.categoryRepo.FindByParentID(ctx, &cmd.ID)
if err != nil {
s.logger.Error("检查子分类失败", zap.Error(err))
return fmt.Errorf("检查子分类失败: %w", err)
}
if len(children) > 0 {
return fmt.Errorf("分类下有子分类,无法删除")
}
// 3. 删除分类
if err := s.categoryRepo.Delete(ctx, cmd.ID); err != nil {
s.logger.Error("删除分类失败", zap.Error(err))
return fmt.Errorf("删除分类失败: %w", err)
}
s.logger.Info("删除分类成功", zap.String("id", cmd.ID))
return nil
}
// GetCategoryByID 根据ID获取分类
func (s *CategoryApplicationServiceImpl) GetCategoryByID(ctx context.Context, query *queries.GetCategoryQuery) (*responses.CategoryInfoResponse, error) {
var category entities.ProductCategory
var err error
if query.ID != "" {
category, err = s.categoryRepo.GetByID(ctx, query.ID)
} else {
return nil, fmt.Errorf("分类ID不能为空")
}
if err != nil {
return nil, fmt.Errorf("分类不存在: %w", err)
}
// 转换为响应对象
response := s.convertToCategoryInfoResponse(&category)
// 加载父分类信息
if category.ParentID != nil && *category.ParentID != "" {
parent, err := s.categoryRepo.GetByID(ctx, *category.ParentID)
if err == nil {
parentResponse := s.convertToCategoryInfoResponse(&parent)
response.Parent = parentResponse
}
}
return response, nil
}
// ListCategories 获取分类列表
func (s *CategoryApplicationServiceImpl) ListCategories(ctx context.Context, query *queries.ListCategoriesQuery) (*responses.CategoryListResponse, error) {
// 构建仓储查询
repoQuery := &repoQueries.ListCategoriesQuery{
Page: query.Page,
PageSize: query.PageSize,
ParentID: query.ParentID,
Level: query.Level,
IsEnabled: query.IsEnabled,
IsVisible: query.IsVisible,
SortBy: query.SortBy,
SortOrder: query.SortOrder,
}
// 调用仓储
categories, total, err := s.categoryRepo.ListCategories(ctx, repoQuery)
if err != nil {
s.logger.Error("获取分类列表失败", zap.Error(err))
return nil, fmt.Errorf("获取分类列表失败: %w", err)
}
// 转换为响应对象
items := make([]responses.CategoryInfoResponse, len(categories))
for i, category := range categories {
items[i] = *s.convertToCategoryInfoResponse(category)
}
return &responses.CategoryListResponse{
Total: total,
Page: query.Page,
Size: query.PageSize,
Items: items,
}, nil
}
// EnableCategory 启用分类
func (s *CategoryApplicationServiceImpl) EnableCategory(ctx context.Context, cmd *commands.EnableCategoryCommand) error {
category, err := s.categoryRepo.GetByID(ctx, cmd.ID)
if err != nil {
return fmt.Errorf("分类不存在: %w", err)
}
category.IsEnabled = true
if err := s.categoryRepo.Update(ctx, category); err != nil {
s.logger.Error("启用分类失败", zap.Error(err))
return fmt.Errorf("启用分类失败: %w", err)
}
s.logger.Info("启用分类成功", zap.String("id", cmd.ID))
return nil
}
// DisableCategory 禁用分类
func (s *CategoryApplicationServiceImpl) DisableCategory(ctx context.Context, cmd *commands.DisableCategoryCommand) error {
category, err := s.categoryRepo.GetByID(ctx, cmd.ID)
if err != nil {
return fmt.Errorf("分类不存在: %w", err)
}
category.IsEnabled = false
if err := s.categoryRepo.Update(ctx, category); err != nil {
s.logger.Error("禁用分类失败", zap.Error(err))
return fmt.Errorf("禁用分类失败: %w", err)
}
s.logger.Info("禁用分类成功", zap.String("id", cmd.ID))
return nil
}
// ShowCategory 显示分类
func (s *CategoryApplicationServiceImpl) ShowCategory(ctx context.Context, cmd *commands.ShowCategoryCommand) error {
category, err := s.categoryRepo.GetByID(ctx, cmd.ID)
if err != nil {
return fmt.Errorf("分类不存在: %w", err)
}
category.IsVisible = true
if err := s.categoryRepo.Update(ctx, category); err != nil {
s.logger.Error("显示分类失败", zap.Error(err))
return fmt.Errorf("显示分类失败: %w", err)
}
s.logger.Info("显示分类成功", zap.String("id", cmd.ID))
return nil
}
// HideCategory 隐藏分类
func (s *CategoryApplicationServiceImpl) HideCategory(ctx context.Context, cmd *commands.HideCategoryCommand) error {
category, err := s.categoryRepo.GetByID(ctx, cmd.ID)
if err != nil {
return fmt.Errorf("分类不存在: %w", err)
}
category.IsVisible = false
if err := s.categoryRepo.Update(ctx, category); err != nil {
s.logger.Error("隐藏分类失败", zap.Error(err))
return fmt.Errorf("隐藏分类失败: %w", err)
}
s.logger.Info("隐藏分类成功", zap.String("id", cmd.ID))
return nil
}
// MoveCategory 移动分类
func (s *CategoryApplicationServiceImpl) MoveCategory(ctx context.Context, cmd *commands.MoveCategoryCommand) error {
category, err := s.categoryRepo.GetByID(ctx, cmd.ID)
if err != nil {
return fmt.Errorf("分类不存在: %w", err)
}
// 检查目标父分类是否存在
if cmd.ParentID != nil && *cmd.ParentID != "" {
_, err := s.categoryRepo.GetByID(ctx, *cmd.ParentID)
if err != nil {
return fmt.Errorf("目标父分类不存在: %w", err)
}
}
category.ParentID = cmd.ParentID
if cmd.Sort > 0 {
category.Sort = cmd.Sort
}
if err := s.categoryRepo.Update(ctx, category); err != nil {
s.logger.Error("移动分类失败", zap.Error(err))
return fmt.Errorf("移动分类失败: %w", err)
}
s.logger.Info("移动分类成功", zap.String("id", cmd.ID))
return nil
}
// GetCategoryTree 获取分类树
func (s *CategoryApplicationServiceImpl) GetCategoryTree(ctx context.Context, query *queries.GetCategoryTreeQuery) (*responses.CategoryTreeResponse, error) {
categories, err := s.categoryRepo.GetCategoryTree(ctx)
if err != nil {
s.logger.Error("获取分类树失败", zap.Error(err))
return nil, fmt.Errorf("获取分类树失败: %w", err)
}
// 构建树形结构
tree := s.buildCategoryTree(categories, query.IncludeDisabled, query.IncludeHidden)
return &responses.CategoryTreeResponse{
Categories: tree,
}, nil
}
// GetCategoriesByLevel 根据层级获取分类
func (s *CategoryApplicationServiceImpl) GetCategoriesByLevel(ctx context.Context, query *queries.GetCategoriesByLevelQuery) ([]*responses.CategoryInfoResponse, error) {
categories, err := s.categoryRepo.FindCategoriesByLevel(ctx, query.Level)
if err != nil {
s.logger.Error("根据层级获取分类失败", zap.Error(err))
return nil, fmt.Errorf("根据层级获取分类失败: %w", err)
}
// 转换为响应对象
items := make([]*responses.CategoryInfoResponse, len(categories))
for i, category := range categories {
items[i] = s.convertToCategoryInfoResponse(category)
}
return items, nil
}
// GetCategoryPath 获取分类路径
func (s *CategoryApplicationServiceImpl) GetCategoryPath(ctx context.Context, query *queries.GetCategoryPathQuery) (*responses.CategoryPathResponse, error) {
path, err := s.buildCategoryPath(ctx, query.CategoryID)
if err != nil {
s.logger.Error("获取分类路径失败", zap.Error(err))
return nil, fmt.Errorf("获取分类路径失败: %w", err)
}
// 转换为正确的类型
pathItems := make([]responses.CategorySimpleResponse, len(path))
for i, item := range path {
pathItems[i] = *item
}
return &responses.CategoryPathResponse{
Path: pathItems,
}, nil
}
// GetCategoryStats 获取分类统计
func (s *CategoryApplicationServiceImpl) GetCategoryStats(ctx context.Context) (*responses.CategoryStatsResponse, error) {
// 使用正确的CountOptions
total, err := s.categoryRepo.Count(ctx, interfaces.CountOptions{})
if err != nil {
s.logger.Error("获取分类总数失败", zap.Error(err))
return nil, fmt.Errorf("获取分类总数失败: %w", err)
}
enabled, err := s.categoryRepo.Count(ctx, interfaces.CountOptions{
Filters: map[string]interface{}{"is_enabled": true},
})
if err != nil {
s.logger.Error("获取启用分类数失败", zap.Error(err))
return nil, fmt.Errorf("获取启用分类数失败: %w", err)
}
visible, err := s.categoryRepo.Count(ctx, interfaces.CountOptions{
Filters: map[string]interface{}{"is_visible": true},
})
if err != nil {
s.logger.Error("获取可见分类数失败", zap.Error(err))
return nil, fmt.Errorf("获取可见分类数失败: %w", err)
}
return &responses.CategoryStatsResponse{
TotalCategories: total,
EnabledCategories: enabled,
VisibleCategories: visible,
RootCategories: total - enabled, // 简化计算,实际应该查询根分类数量
}, nil
}
// 私有方法
func (s *CategoryApplicationServiceImpl) validateCreateCategory(cmd *commands.CreateCategoryCommand) error {
if cmd.Name == "" {
return fmt.Errorf("分类名称不能为空")
}
if cmd.Level <= 0 {
return fmt.Errorf("分类层级必须大于0")
}
return nil
}
func (s *CategoryApplicationServiceImpl) convertToCategoryInfoResponse(category *entities.ProductCategory) *responses.CategoryInfoResponse {
return &responses.CategoryInfoResponse{
ID: category.ID,
Name: category.Name,
Code: category.Code,
Description: category.Description,
ParentID: category.ParentID,
Level: category.Level,
Sort: category.Sort,
IsEnabled: category.IsEnabled,
IsVisible: category.IsVisible,
CreatedAt: category.CreatedAt,
UpdatedAt: category.UpdatedAt,
}
}
func (s *CategoryApplicationServiceImpl) convertToCategorySimpleResponse(category *entities.ProductCategory) *responses.CategorySimpleResponse {
return &responses.CategorySimpleResponse{
ID: category.ID,
Name: category.Name,
Code: category.Code,
ParentID: category.ParentID,
Level: category.Level,
}
}
func (s *CategoryApplicationServiceImpl) buildCategoryTree(categories []*entities.ProductCategory, includeDisabled, includeHidden bool) []responses.CategoryInfoResponse {
// 构建ID到分类的映射
categoryMap := make(map[string]*entities.ProductCategory)
for _, category := range categories {
// 根据过滤条件决定是否包含
if !includeDisabled && !category.IsEnabled {
continue
}
if !includeHidden && !category.IsVisible {
continue
}
categoryMap[category.ID] = category
}
// 构建树形结构
var roots []responses.CategoryInfoResponse
for _, category := range categoryMap {
if category.ParentID == nil || *category.ParentID == "" {
// 根节点
root := *s.convertToCategoryInfoResponse(category)
root.Children = s.findChildren(category.ID, categoryMap)
roots = append(roots, root)
}
}
return roots
}
func (s *CategoryApplicationServiceImpl) findChildren(parentID string, categoryMap map[string]*entities.ProductCategory) []responses.CategoryInfoResponse {
var children []responses.CategoryInfoResponse
for _, category := range categoryMap {
if category.ParentID != nil && *category.ParentID == parentID {
child := *s.convertToCategoryInfoResponse(category)
child.Children = s.findChildren(category.ID, categoryMap)
children = append(children, child)
}
}
return children
}
func (s *CategoryApplicationServiceImpl) buildCategoryPath(ctx context.Context, categoryID string) ([]*responses.CategorySimpleResponse, error) {
var path []*responses.CategorySimpleResponse
currentID := categoryID
for currentID != "" {
category, err := s.categoryRepo.GetByID(ctx, currentID)
if err != nil {
return nil, fmt.Errorf("获取分类失败: %w", err)
}
path = append([]*responses.CategorySimpleResponse{
s.convertToCategorySimpleResponse(&category),
}, path...)
if category.ParentID != nil {
currentID = *category.ParentID
} else {
currentID = ""
}
}
return path, nil
}

View File

@@ -0,0 +1,58 @@
package commands
// CreateCategoryCommand 创建分类命令
type CreateCategoryCommand struct {
Name string `json:"name" binding:"required" comment:"分类名称"`
Code string `json:"code" binding:"required" comment:"分类编号"`
Description string `json:"description" comment:"分类描述"`
ParentID *string `json:"parent_id" comment:"父分类ID"`
Level int `json:"level" binding:"min=1" comment:"分类层级"`
Sort int `json:"sort" comment:"排序"`
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
IsVisible bool `json:"is_visible" comment:"是否展示"`
}
// UpdateCategoryCommand 更新分类命令
type UpdateCategoryCommand struct {
ID string `json:"-"`
Name string `json:"name" comment:"分类名称"`
Code string `json:"code" comment:"分类编号"`
Description string `json:"description" comment:"分类描述"`
ParentID *string `json:"parent_id" comment:"父分类ID"`
Level int `json:"level" binding:"min=1" comment:"分类层级"`
Sort int `json:"sort" comment:"排序"`
IsEnabled *bool `json:"is_enabled" comment:"是否启用"`
IsVisible *bool `json:"is_visible" comment:"是否展示"`
}
// DeleteCategoryCommand 删除分类命令
type DeleteCategoryCommand struct {
ID string `json:"-"`
}
// EnableCategoryCommand 启用分类命令
type EnableCategoryCommand struct {
ID string `json:"-"`
}
// DisableCategoryCommand 禁用分类命令
type DisableCategoryCommand struct {
ID string `json:"-"`
}
// ShowCategoryCommand 显示分类命令
type ShowCategoryCommand struct {
ID string `json:"-"`
}
// HideCategoryCommand 隐藏分类命令
type HideCategoryCommand struct {
ID string `json:"-"`
}
// MoveCategoryCommand 移动分类命令
type MoveCategoryCommand struct {
ID string `json:"-"`
ParentID *string `json:"parent_id" comment:"新的父分类ID"`
Sort int `json:"sort" comment:"新的排序"`
}

View File

@@ -0,0 +1,71 @@
package commands
// CreateProductCommand 创建产品命令
type CreateProductCommand struct {
Name string `json:"name" binding:"required" comment:"产品名称"`
Code string `json:"code" binding:"required" comment:"产品编号"`
Description string `json:"description" comment:"产品简介"`
Content string `json:"content" comment:"产品内容"`
CategoryID string `json:"category_id" binding:"required" comment:"产品分类ID"`
Price float64 `json:"price" binding:"min=0" comment:"产品价格"`
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
IsVisible bool `json:"is_visible" comment:"是否展示"`
IsPackage bool `json:"is_package" comment:"是否组合包"`
// SEO信息
SEOTitle string `json:"seo_title" comment:"SEO标题"`
SEODescription string `json:"seo_description" comment:"SEO描述"`
SEOKeywords string `json:"seo_keywords" comment:"SEO关键词"`
}
// UpdateProductCommand 更新产品命令
type UpdateProductCommand struct {
ID string `json:"-"`
Name string `json:"name" comment:"产品名称"`
Code string `json:"code" comment:"产品编号"`
Description string `json:"description" comment:"产品简介"`
Content string `json:"content" comment:"产品内容"`
CategoryID string `json:"category_id" comment:"产品分类ID"`
Price float64 `json:"price" binding:"min=0" comment:"产品价格"`
IsEnabled *bool `json:"is_enabled" comment:"是否启用"`
IsVisible *bool `json:"is_visible" comment:"是否展示"`
IsPackage *bool `json:"is_package" comment:"是否组合包"`
// SEO信息
SEOTitle string `json:"seo_title" comment:"SEO标题"`
SEODescription string `json:"seo_description" comment:"SEO描述"`
SEOKeywords string `json:"seo_keywords" comment:"SEO关键词"`
}
// DeleteProductCommand 删除产品命令
type DeleteProductCommand struct {
ID string `json:"-"`
}
// EnableProductCommand 启用产品命令
type EnableProductCommand struct {
ID string `json:"-"`
}
// DisableProductCommand 禁用产品命令
type DisableProductCommand struct {
ID string `json:"-"`
}
// ShowProductCommand 显示产品命令
type ShowProductCommand struct {
ID string `json:"-"`
}
// HideProductCommand 隐藏产品命令
type HideProductCommand struct {
ID string `json:"-"`
}
// UpdateProductSEOCommand 更新产品SEO信息命令
type UpdateProductSEOCommand struct {
ID string `json:"-"`
SEOTitle string `json:"seo_title" comment:"SEO标题"`
SEODescription string `json:"seo_description" comment:"SEO描述"`
SEOKeywords string `json:"seo_keywords" comment:"SEO关键词"`
}

View File

@@ -0,0 +1,57 @@
package commands
// CreateSubscriptionCommand 创建订阅命令
type CreateSubscriptionCommand struct {
UserID string `json:"user_id" binding:"required" comment:"用户ID"`
ProductID string `json:"product_id" binding:"required" comment:"产品ID"`
Price float64 `json:"price" binding:"min=0" comment:"订阅价格"`
APILimit int64 `json:"api_limit" comment:"API调用限制"`
AutoRenew bool `json:"auto_renew" comment:"是否自动续费"`
Duration string `json:"duration" comment:"订阅时长"`
}
// UpdateSubscriptionCommand 更新订阅命令
type UpdateSubscriptionCommand struct {
ID string `json:"-"`
Price float64 `json:"price" binding:"min=0" comment:"订阅价格"`
APILimit int64 `json:"api_limit" comment:"API调用限制"`
AutoRenew *bool `json:"auto_renew" comment:"是否自动续费"`
}
// CancelSubscriptionCommand 取消订阅命令
type CancelSubscriptionCommand struct {
ID string `json:"-"`
}
// RenewSubscriptionCommand 续费订阅命令
type RenewSubscriptionCommand struct {
ID string `json:"-"`
Duration string `json:"duration" binding:"required" comment:"续费时长"`
}
// ActivateSubscriptionCommand 激活订阅命令
type ActivateSubscriptionCommand struct {
ID string `json:"-"`
}
// DeactivateSubscriptionCommand 停用订阅命令
type DeactivateSubscriptionCommand struct {
ID string `json:"-"`
}
// UpdateAPIUsageCommand 更新API使用量命令
type UpdateAPIUsageCommand struct {
ID string `json:"-"`
APIUsed int64 `json:"api_used" binding:"min=0" comment:"API使用量"`
}
// ResetAPIUsageCommand 重置API使用量命令
type ResetAPIUsageCommand struct {
ID string `json:"-"`
}
// SetAPILimitCommand 设置API限制命令
type SetAPILimitCommand struct {
ID string `json:"-"`
APILimit int64 `json:"api_limit" binding:"min=0" comment:"API调用限制"`
}

View File

@@ -0,0 +1,35 @@
package queries
// ListCategoriesQuery 分类列表查询
type ListCategoriesQuery struct {
Page int `form:"page" binding:"min=1" comment:"页码"`
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
ParentID *string `form:"parent_id" comment:"父分类ID"`
Level *int `form:"level" comment:"分类层级"`
IsEnabled *bool `form:"is_enabled" comment:"是否启用"`
IsVisible *bool `form:"is_visible" comment:"是否展示"`
SortBy string `form:"sort_by" comment:"排序字段"`
SortOrder string `form:"sort_order" comment:"排序方向"`
}
// GetCategoryQuery 获取分类详情查询
type GetCategoryQuery struct {
ID string `uri:"id" comment:"分类ID"`
Code string `form:"code" comment:"分类编号"`
}
// GetCategoryTreeQuery 获取分类树查询
type GetCategoryTreeQuery struct {
IncludeDisabled bool `form:"include_disabled" comment:"是否包含禁用分类"`
IncludeHidden bool `form:"include_hidden" comment:"是否包含隐藏分类"`
}
// GetCategoriesByLevelQuery 根据层级获取分类查询
type GetCategoriesByLevelQuery struct {
Level int `form:"level" binding:"min=1" comment:"分类层级"`
}
// GetCategoryPathQuery 获取分类路径查询
type GetCategoryPathQuery struct {
CategoryID string `uri:"category_id" binding:"required" comment:"分类ID"`
}

View File

@@ -0,0 +1,47 @@
package queries
// ListProductsQuery 产品列表查询
type ListProductsQuery struct {
Page int `form:"page" binding:"min=1" comment:"页码"`
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
Keyword string `form:"keyword" comment:"搜索关键词"`
CategoryID string `form:"category_id" comment:"分类ID"`
MinPrice *float64 `form:"min_price" comment:"最低价格"`
MaxPrice *float64 `form:"max_price" comment:"最高价格"`
IsEnabled *bool `form:"is_enabled" comment:"是否启用"`
IsVisible *bool `form:"is_visible" comment:"是否展示"`
IsPackage *bool `form:"is_package" comment:"是否组合包"`
SortBy string `form:"sort_by" comment:"排序字段"`
SortOrder string `form:"sort_order" comment:"排序方向"`
}
// SearchProductsQuery 产品搜索查询
type SearchProductsQuery struct {
Page int `form:"page" binding:"min=1" comment:"页码"`
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
Keyword string `form:"keyword" comment:"搜索关键词"`
CategoryID string `form:"category_id" comment:"分类ID"`
MinPrice *float64 `form:"min_price" comment:"最低价格"`
MaxPrice *float64 `form:"max_price" comment:"最高价格"`
IsEnabled *bool `form:"is_enabled" comment:"是否启用"`
IsVisible *bool `form:"is_visible" comment:"是否展示"`
IsPackage *bool `form:"is_package" comment:"是否组合包"`
SortBy string `form:"sort_by" comment:"排序字段"`
SortOrder string `form:"sort_order" comment:"排序方向"`
}
// GetProductQuery 获取产品详情查询
type GetProductQuery struct {
ID string `uri:"id" comment:"产品ID"`
Code string `form:"code" comment:"产品编号"`
}
// GetProductsByIDsQuery 根据ID列表获取产品查询
type GetProductsByIDsQuery struct {
IDs []string `form:"ids" binding:"required" comment:"产品ID列表"`
}
// GetSubscribableProductsQuery 获取可订阅产品查询
type GetSubscribableProductsQuery struct {
UserID string `form:"user_id" binding:"required" comment:"用户ID"`
}

View File

@@ -0,0 +1,36 @@
package queries
import "tyapi-server/internal/domains/product/entities"
// ListSubscriptionsQuery 订阅列表查询
type ListSubscriptionsQuery struct {
Page int `form:"page" binding:"min=1" comment:"页码"`
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
UserID string `form:"user_id" comment:"用户ID"`
ProductID string `form:"product_id" comment:"产品ID"`
Status entities.SubscriptionStatus `form:"status" comment:"订阅状态"`
SortBy string `form:"sort_by" comment:"排序字段"`
SortOrder string `form:"sort_order" comment:"排序方向"`
}
// GetSubscriptionQuery 获取订阅详情查询
type GetSubscriptionQuery struct {
ID string `uri:"id" binding:"required" comment:"订阅ID"`
}
// GetUserSubscriptionsQuery 获取用户订阅查询
type GetUserSubscriptionsQuery struct {
UserID string `form:"user_id" binding:"required" comment:"用户ID"`
Status *entities.SubscriptionStatus `form:"status" comment:"订阅状态"`
}
// GetProductSubscriptionsQuery 获取产品订阅查询
type GetProductSubscriptionsQuery struct {
ProductID string `form:"product_id" binding:"required" comment:"产品ID"`
Status *entities.SubscriptionStatus `form:"status" comment:"订阅状态"`
}
// GetActiveSubscriptionsQuery 获取活跃订阅查询
type GetActiveSubscriptionsQuery struct {
UserID string `form:"user_id" comment:"用户ID"`
}

View File

@@ -0,0 +1,58 @@
package responses
import "time"
// CategoryInfoResponse 分类详情响应
type CategoryInfoResponse struct {
ID string `json:"id" comment:"分类ID"`
Name string `json:"name" comment:"分类名称"`
Code string `json:"code" comment:"分类编号"`
Description string `json:"description" comment:"分类描述"`
ParentID *string `json:"parent_id" comment:"父分类ID"`
Level int `json:"level" comment:"分类层级"`
Sort int `json:"sort" comment:"排序"`
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
IsVisible bool `json:"is_visible" comment:"是否展示"`
// 关联信息
Parent *CategoryInfoResponse `json:"parent,omitempty" comment:"父分类"`
Children []CategoryInfoResponse `json:"children,omitempty" comment:"子分类"`
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
}
// CategoryListResponse 分类列表响应
type CategoryListResponse struct {
Total int64 `json:"total" comment:"总数"`
Page int `json:"page" comment:"页码"`
Size int `json:"size" comment:"每页数量"`
Items []CategoryInfoResponse `json:"items" comment:"分类列表"`
}
// CategoryTreeResponse 分类树响应
type CategoryTreeResponse struct {
Categories []CategoryInfoResponse `json:"categories" comment:"分类树"`
}
// CategorySimpleResponse 分类简单信息响应
type CategorySimpleResponse struct {
ID string `json:"id" comment:"分类ID"`
Name string `json:"name" comment:"分类名称"`
Code string `json:"code" comment:"分类编号"`
ParentID *string `json:"parent_id" comment:"父分类ID"`
Level int `json:"level" comment:"分类层级"`
}
// CategoryPathResponse 分类路径响应
type CategoryPathResponse struct {
Path []CategorySimpleResponse `json:"path" comment:"分类路径"`
}
// CategoryStatsResponse 分类统计响应
type CategoryStatsResponse struct {
TotalCategories int64 `json:"total_categories" comment:"分类总数"`
RootCategories int64 `json:"root_categories" comment:"根分类数"`
EnabledCategories int64 `json:"enabled_categories" comment:"启用分类数"`
VisibleCategories int64 `json:"visible_categories" comment:"可见分类数"`
}

View File

@@ -0,0 +1,62 @@
package responses
import "time"
// ProductInfoResponse 产品详情响应
type ProductInfoResponse struct {
ID string `json:"id" comment:"产品ID"`
Name string `json:"name" comment:"产品名称"`
Code string `json:"code" comment:"产品编号"`
Description string `json:"description" comment:"产品简介"`
Content string `json:"content" comment:"产品内容"`
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:"是否组合包"`
// SEO信息
SEOTitle string `json:"seo_title" comment:"SEO标题"`
SEODescription string `json:"seo_description" comment:"SEO描述"`
SEOKeywords string `json:"seo_keywords" comment:"SEO关键词"`
// 关联信息
Category *CategoryInfoResponse `json:"category,omitempty" comment:"分类信息"`
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
}
// ProductListResponse 产品列表响应
type ProductListResponse struct {
Total int64 `json:"total" comment:"总数"`
Page int `json:"page" comment:"页码"`
Size int `json:"size" comment:"每页数量"`
Items []ProductInfoResponse `json:"items" comment:"产品列表"`
}
// ProductSearchResponse 产品搜索响应
type ProductSearchResponse struct {
Total int64 `json:"total" comment:"总数"`
Page int `json:"page" comment:"页码"`
Size int `json:"size" comment:"每页数量"`
Items []ProductInfoResponse `json:"items" comment:"产品列表"`
}
// ProductSimpleResponse 产品简单信息响应
type ProductSimpleResponse struct {
ID string `json:"id" comment:"产品ID"`
Name string `json:"name" comment:"产品名称"`
Code string `json:"code" comment:"产品编号"`
Description string `json:"description" comment:"产品简介"`
Price float64 `json:"price" comment:"产品价格"`
IsPackage bool `json:"is_package" comment:"是否组合包"`
}
// ProductStatsResponse 产品统计响应
type ProductStatsResponse struct {
TotalProducts int64 `json:"total_products" comment:"产品总数"`
EnabledProducts int64 `json:"enabled_products" comment:"启用产品数"`
VisibleProducts int64 `json:"visible_products" comment:"可见产品数"`
PackageProducts int64 `json:"package_products" comment:"组合包产品数"`
}

View File

@@ -0,0 +1,49 @@
package responses
import (
"time"
)
// SubscriptionInfoResponse 订阅详情响应
type SubscriptionInfoResponse struct {
ID string `json:"id" comment:"订阅ID"`
UserID string `json:"user_id" comment:"用户ID"`
ProductID string `json:"product_id" comment:"产品ID"`
Price float64 `json:"price" comment:"订阅价格"`
APIUsed int64 `json:"api_used" comment:"已使用API调用次数"`
// 关联信息
Product *ProductSimpleResponse `json:"product,omitempty" comment:"产品信息"`
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
}
// SubscriptionListResponse 订阅列表响应
type SubscriptionListResponse struct {
Total int64 `json:"total" comment:"总数"`
Page int `json:"page" comment:"页码"`
Size int `json:"size" comment:"每页数量"`
Items []SubscriptionInfoResponse `json:"items" comment:"订阅列表"`
}
// SubscriptionSimpleResponse 订阅简单信息响应
type SubscriptionSimpleResponse struct {
ID string `json:"id" comment:"订阅ID"`
ProductID string `json:"product_id" comment:"产品ID"`
Price float64 `json:"price" comment:"订阅价格"`
APIUsed int64 `json:"api_used" comment:"已使用API调用次数"`
}
// SubscriptionUsageResponse 订阅使用情况响应
type SubscriptionUsageResponse struct {
ID string `json:"id" comment:"订阅ID"`
ProductID string `json:"product_id" comment:"产品ID"`
APIUsed int64 `json:"api_used" comment:"已使用API调用次数"`
}
// SubscriptionStatsResponse 订阅统计响应
type SubscriptionStatsResponse struct {
TotalSubscriptions int64 `json:"total_subscriptions" comment:"订阅总数"`
TotalRevenue float64 `json:"total_revenue" comment:"总收入"`
}

View File

@@ -0,0 +1,78 @@
package product
import (
"context"
"tyapi-server/internal/application/product/dto/commands"
"tyapi-server/internal/application/product/dto/queries"
"tyapi-server/internal/application/product/dto/responses"
)
// ProductApplicationService 产品应用服务接口
type ProductApplicationService interface {
// 产品管理
CreateProduct(ctx context.Context, cmd *commands.CreateProductCommand) error
UpdateProduct(ctx context.Context, cmd *commands.UpdateProductCommand) error
DeleteProduct(ctx context.Context, cmd *commands.DeleteProductCommand) error
GetProductByID(ctx context.Context, query *queries.GetProductQuery) (*responses.ProductInfoResponse, error)
ListProducts(ctx context.Context, query *queries.ListProductsQuery) (*responses.ProductListResponse, error)
GetProductsByIDs(ctx context.Context, query *queries.GetProductsByIDsQuery) ([]*responses.ProductInfoResponse, error)
// 产品状态管理
EnableProduct(ctx context.Context, cmd *commands.EnableProductCommand) error
DisableProduct(ctx context.Context, cmd *commands.DisableProductCommand) error
ShowProduct(ctx context.Context, cmd *commands.ShowProductCommand) error
HideProduct(ctx context.Context, cmd *commands.HideProductCommand) error
// SEO管理
UpdateProductSEO(ctx context.Context, cmd *commands.UpdateProductSEOCommand) error
// 业务查询
GetSubscribableProducts(ctx context.Context, query *queries.GetSubscribableProductsQuery) ([]*responses.ProductInfoResponse, error)
GetProductStats(ctx context.Context) (*responses.ProductStatsResponse, error)
}
// CategoryApplicationService 分类应用服务接口
type CategoryApplicationService interface {
// 分类管理
CreateCategory(ctx context.Context, cmd *commands.CreateCategoryCommand) error
UpdateCategory(ctx context.Context, cmd *commands.UpdateCategoryCommand) error
DeleteCategory(ctx context.Context, cmd *commands.DeleteCategoryCommand) error
GetCategoryByID(ctx context.Context, query *queries.GetCategoryQuery) (*responses.CategoryInfoResponse, error)
ListCategories(ctx context.Context, query *queries.ListCategoriesQuery) (*responses.CategoryListResponse, error)
// 分类状态管理
EnableCategory(ctx context.Context, cmd *commands.EnableCategoryCommand) error
DisableCategory(ctx context.Context, cmd *commands.DisableCategoryCommand) error
ShowCategory(ctx context.Context, cmd *commands.ShowCategoryCommand) error
HideCategory(ctx context.Context, cmd *commands.HideCategoryCommand) error
// 分类结构管理
MoveCategory(ctx context.Context, cmd *commands.MoveCategoryCommand) error
GetCategoryTree(ctx context.Context, query *queries.GetCategoryTreeQuery) (*responses.CategoryTreeResponse, error)
GetCategoriesByLevel(ctx context.Context, query *queries.GetCategoriesByLevelQuery) ([]*responses.CategoryInfoResponse, error)
GetCategoryPath(ctx context.Context, query *queries.GetCategoryPathQuery) (*responses.CategoryPathResponse, error)
// 统计
GetCategoryStats(ctx context.Context) (*responses.CategoryStatsResponse, error)
}
// SubscriptionApplicationService 订阅应用服务接口
type SubscriptionApplicationService interface {
// 订阅管理
CreateSubscription(ctx context.Context, cmd *commands.CreateSubscriptionCommand) error
UpdateSubscription(ctx context.Context, cmd *commands.UpdateSubscriptionCommand) error
GetSubscriptionByID(ctx context.Context, query *queries.GetSubscriptionQuery) (*responses.SubscriptionInfoResponse, error)
ListSubscriptions(ctx context.Context, query *queries.ListSubscriptionsQuery) (*responses.SubscriptionListResponse, error)
// API使用管理
UpdateAPIUsage(ctx context.Context, cmd *commands.UpdateAPIUsageCommand) error
ResetAPIUsage(ctx context.Context, cmd *commands.ResetAPIUsageCommand) error
// 业务查询
GetUserSubscriptions(ctx context.Context, query *queries.GetUserSubscriptionsQuery) ([]*responses.SubscriptionInfoResponse, error)
GetProductSubscriptions(ctx context.Context, query *queries.GetProductSubscriptionsQuery) ([]*responses.SubscriptionInfoResponse, error)
GetSubscriptionUsage(ctx context.Context, subscriptionID string) (*responses.SubscriptionUsageResponse, error)
// 统计
GetSubscriptionStats(ctx context.Context) (*responses.SubscriptionStatsResponse, error)
}

View File

@@ -0,0 +1,452 @@
package product
import (
"context"
"errors"
"fmt"
"tyapi-server/internal/application/product/dto/commands"
appQueries "tyapi-server/internal/application/product/dto/queries"
"tyapi-server/internal/application/product/dto/responses"
"tyapi-server/internal/domains/product/entities"
"tyapi-server/internal/domains/product/repositories"
repoQueries "tyapi-server/internal/domains/product/repositories/queries"
"tyapi-server/internal/domains/product/services"
"go.uber.org/zap"
)
// ProductApplicationServiceImpl 产品应用服务实现
type ProductApplicationServiceImpl struct {
productRepo repositories.ProductRepository
categoryRepo repositories.ProductCategoryRepository
subscriptionRepo repositories.SubscriptionRepository
productService *services.ProductService
logger *zap.Logger
}
// NewProductApplicationService 创建产品应用服务
func NewProductApplicationService(
productRepo repositories.ProductRepository,
categoryRepo repositories.ProductCategoryRepository,
subscriptionRepo repositories.SubscriptionRepository,
productService *services.ProductService,
logger *zap.Logger,
) ProductApplicationService {
return &ProductApplicationServiceImpl{
productRepo: productRepo,
categoryRepo: categoryRepo,
subscriptionRepo: subscriptionRepo,
productService: productService,
logger: logger,
}
}
// CreateProduct 创建产品
func (s *ProductApplicationServiceImpl) CreateProduct(ctx context.Context, cmd *commands.CreateProductCommand) error {
// 1. 参数验证
if err := s.validateCreateProduct(cmd); err != nil {
return err
}
// 2. 验证产品编号唯一性
if err := s.productService.ValidateProductCode(cmd.Code, ""); err != nil {
return err
}
// 3. 创建产品实体
product := &entities.Product{
Name: cmd.Name,
Code: cmd.Code,
Description: cmd.Description,
Content: cmd.Content,
CategoryID: cmd.CategoryID,
Price: cmd.Price,
IsEnabled: cmd.IsEnabled,
IsVisible: cmd.IsVisible,
IsPackage: cmd.IsPackage,
}
// 4. 设置SEO信息
product.UpdateSEO(cmd.SEOTitle, cmd.SEODescription, cmd.SEOKeywords)
// 5. 调用领域服务验证
if err := s.productService.ValidateProduct(product); err != nil {
return err
}
// 6. 保存到仓储
if _, err := s.productRepo.Create(ctx, *product); err != nil {
s.logger.Error("创建产品失败", zap.Error(err))
return fmt.Errorf("创建产品失败: %w", err)
}
s.logger.Info("创建产品成功", zap.String("id", product.ID), zap.String("name", product.Name))
return nil
}
// UpdateProduct 更新产品
func (s *ProductApplicationServiceImpl) UpdateProduct(ctx context.Context, cmd *commands.UpdateProductCommand) error {
// 1. 获取现有产品
existingProduct, err := s.productRepo.GetByID(ctx, cmd.ID)
if err != nil {
return fmt.Errorf("产品不存在: %w", err)
}
// 2. 参数验证
if err := s.validateUpdateProduct(cmd); err != nil {
return err
}
// 3. 验证产品编号唯一性(如果修改了编号)
if cmd.Code != "" && cmd.Code != existingProduct.Code {
if err := s.productService.ValidateProductCode(cmd.Code, cmd.ID); err != nil {
return err
}
existingProduct.Code = cmd.Code
}
// 4. 更新字段
if cmd.Name != "" {
existingProduct.Name = cmd.Name
}
if cmd.Description != "" {
existingProduct.Description = cmd.Description
}
if cmd.Content != "" {
existingProduct.Content = cmd.Content
}
if cmd.CategoryID != "" {
existingProduct.CategoryID = cmd.CategoryID
}
if cmd.Price >= 0 {
existingProduct.Price = cmd.Price
}
if cmd.IsEnabled != nil {
existingProduct.IsEnabled = *cmd.IsEnabled
}
if cmd.IsVisible != nil {
existingProduct.IsVisible = *cmd.IsVisible
}
if cmd.IsPackage != nil {
existingProduct.IsPackage = *cmd.IsPackage
}
// 5. 更新SEO信息
if cmd.SEOTitle != "" || cmd.SEODescription != "" || cmd.SEOKeywords != "" {
existingProduct.UpdateSEO(cmd.SEOTitle, cmd.SEODescription, cmd.SEOKeywords)
}
// 6. 调用领域服务验证
if err := s.productService.ValidateProduct(&existingProduct); err != nil {
return err
}
// 7. 保存到仓储
if err := s.productRepo.Update(ctx, existingProduct); err != nil {
s.logger.Error("更新产品失败", zap.Error(err))
return fmt.Errorf("更新产品失败: %w", err)
}
s.logger.Info("更新产品成功", zap.String("id", existingProduct.ID))
return nil
}
// DeleteProduct 删除产品
func (s *ProductApplicationServiceImpl) DeleteProduct(ctx context.Context, cmd *commands.DeleteProductCommand) error {
// 1. 检查产品是否存在
product, err := s.productRepo.GetByID(ctx, cmd.ID)
if err != nil {
return fmt.Errorf("产品不存在: %w", err)
}
// 2. 检查是否有活跃订阅
subscriptions, err := s.subscriptionRepo.FindByProductID(ctx, cmd.ID)
if err == nil && len(subscriptions) > 0 {
return errors.New("产品存在订阅,无法删除")
}
// 3. 删除产品
if err := s.productRepo.Delete(ctx, cmd.ID); err != nil {
s.logger.Error("删除产品失败", zap.Error(err))
return fmt.Errorf("删除产品失败: %w", err)
}
s.logger.Info("删除产品成功", zap.String("id", cmd.ID), zap.String("name", product.Name))
return nil
}
// ListProducts 获取产品列表
func (s *ProductApplicationServiceImpl) ListProducts(ctx context.Context, query *appQueries.ListProductsQuery) (*responses.ProductListResponse, error) {
// 构建仓储查询
repoQuery := &repoQueries.ListProductsQuery{
Keyword: query.Keyword,
CategoryID: query.CategoryID,
MinPrice: query.MinPrice,
MaxPrice: query.MaxPrice,
IsEnabled: query.IsEnabled,
IsVisible: query.IsVisible,
IsPackage: query.IsPackage,
Page: query.Page,
PageSize: query.PageSize,
SortBy: query.SortBy,
SortOrder: query.SortOrder,
}
// 调用仓储
products, total, err := s.productRepo.ListProducts(ctx, repoQuery)
if err != nil {
s.logger.Error("获取产品列表失败", zap.Error(err))
return nil, fmt.Errorf("获取产品列表失败: %w", err)
}
// 转换为响应对象
items := make([]responses.ProductInfoResponse, len(products))
for i := range products {
items[i] = *s.convertToProductInfoResponse(products[i])
}
return &responses.ProductListResponse{
Total: total,
Page: query.Page,
Size: query.PageSize,
Items: items,
}, nil
}
// GetProductsByIDs 根据ID列表获取产品
func (s *ProductApplicationServiceImpl) GetProductsByIDs(ctx context.Context, query *appQueries.GetProductsByIDsQuery) ([]*responses.ProductInfoResponse, error) {
products, err := s.productRepo.GetByIDs(ctx, query.IDs)
if err != nil {
s.logger.Error("获取产品列表失败", zap.Error(err))
return nil, fmt.Errorf("获取产品列表失败: %w", err)
}
// 转换为响应对象
items := make([]*responses.ProductInfoResponse, len(products))
for i := range products {
items[i] = s.convertToProductInfoResponse(&products[i])
}
return items, nil
}
// GetSubscribableProducts 获取可订阅的产品
func (s *ProductApplicationServiceImpl) GetSubscribableProducts(ctx context.Context, query *appQueries.GetSubscribableProductsQuery) ([]*responses.ProductInfoResponse, error) {
products, err := s.productRepo.FindSubscribableProducts(ctx, query.UserID)
if err != nil {
s.logger.Error("获取可订阅产品失败", zap.Error(err))
return nil, fmt.Errorf("获取可订阅产品失败: %w", err)
}
// 转换为响应对象
items := make([]*responses.ProductInfoResponse, len(products))
for i := range products {
items[i] = s.convertToProductInfoResponse(products[i])
}
return items, nil
}
// GetProductByID 根据ID获取产品
func (s *ProductApplicationServiceImpl) GetProductByID(ctx context.Context, query *appQueries.GetProductQuery) (*responses.ProductInfoResponse, error) {
var product *entities.Product
var err error
if query.ID != "" {
p, err := s.productRepo.GetByID(ctx, query.ID)
if err != nil {
return nil, fmt.Errorf("产品不存在: %w", err)
}
product = &p
} else if query.Code != "" {
product, err = s.productRepo.FindByCode(ctx, query.Code)
if err != nil {
return nil, fmt.Errorf("产品不存在: %w", err)
}
} else {
return nil, errors.New("产品ID或编号不能为空")
}
// 转换为响应对象
response := s.convertToProductInfoResponse(product)
// 加载分类信息
if product.CategoryID != "" {
category, err := s.categoryRepo.GetByID(ctx, product.CategoryID)
if err == nil {
response.Category = s.convertToCategoryInfoResponse(&category)
}
}
return response, nil
}
// EnableProduct 启用产品
func (s *ProductApplicationServiceImpl) EnableProduct(ctx context.Context, cmd *commands.EnableProductCommand) error {
product, err := s.productRepo.GetByID(ctx, cmd.ID)
if err != nil {
return fmt.Errorf("产品不存在: %w", err)
}
product.Enable()
if err := s.productRepo.Update(ctx, product); err != nil {
s.logger.Error("启用产品失败", zap.Error(err))
return fmt.Errorf("启用产品失败: %w", err)
}
s.logger.Info("启用产品成功", zap.String("id", cmd.ID))
return nil
}
// DisableProduct 禁用产品
func (s *ProductApplicationServiceImpl) DisableProduct(ctx context.Context, cmd *commands.DisableProductCommand) error {
product, err := s.productRepo.GetByID(ctx, cmd.ID)
if err != nil {
return fmt.Errorf("产品不存在: %w", err)
}
product.Disable()
if err := s.productRepo.Update(ctx, product); err != nil {
s.logger.Error("禁用产品失败", zap.Error(err))
return fmt.Errorf("禁用产品失败: %w", err)
}
s.logger.Info("禁用产品成功", zap.String("id", cmd.ID))
return nil
}
// ShowProduct 显示产品
func (s *ProductApplicationServiceImpl) ShowProduct(ctx context.Context, cmd *commands.ShowProductCommand) error {
product, err := s.productRepo.GetByID(ctx, cmd.ID)
if err != nil {
return fmt.Errorf("产品不存在: %w", err)
}
product.Show()
if err := s.productRepo.Update(ctx, product); err != nil {
s.logger.Error("显示产品失败", zap.Error(err))
return fmt.Errorf("显示产品失败: %w", err)
}
s.logger.Info("显示产品成功", zap.String("id", cmd.ID))
return nil
}
// HideProduct 隐藏产品
func (s *ProductApplicationServiceImpl) HideProduct(ctx context.Context, cmd *commands.HideProductCommand) error {
product, err := s.productRepo.GetByID(ctx, cmd.ID)
if err != nil {
return fmt.Errorf("产品不存在: %w", err)
}
product.Hide()
if err := s.productRepo.Update(ctx, product); err != nil {
s.logger.Error("隐藏产品失败", zap.Error(err))
return fmt.Errorf("隐藏产品失败: %w", err)
}
s.logger.Info("隐藏产品成功", zap.String("id", cmd.ID))
return nil
}
// UpdateProductSEO 更新产品SEO信息
func (s *ProductApplicationServiceImpl) UpdateProductSEO(ctx context.Context, cmd *commands.UpdateProductSEOCommand) error {
product, err := s.productRepo.GetByID(ctx, cmd.ID)
if err != nil {
return fmt.Errorf("产品不存在: %w", err)
}
product.UpdateSEO(cmd.SEOTitle, cmd.SEODescription, cmd.SEOKeywords)
if err := s.productRepo.Update(ctx, product); err != nil {
s.logger.Error("更新产品SEO失败", zap.Error(err))
return fmt.Errorf("更新产品SEO失败: %w", err)
}
s.logger.Info("更新产品SEO成功", zap.String("id", cmd.ID))
return nil
}
// GetProductStats 获取产品统计
func (s *ProductApplicationServiceImpl) GetProductStats(ctx context.Context) (*responses.ProductStatsResponse, error) {
stats, err := s.productService.GetProductStats()
if err != nil {
s.logger.Error("获取产品统计失败", zap.Error(err))
return nil, fmt.Errorf("获取产品统计失败: %w", err)
}
return &responses.ProductStatsResponse{
TotalProducts: stats["total"],
EnabledProducts: stats["enabled"],
VisibleProducts: stats["visible"],
PackageProducts: 0, // 需要单独统计
}, nil
}
// 私有方法
func (s *ProductApplicationServiceImpl) validateCreateProduct(cmd *commands.CreateProductCommand) error {
if cmd.Name == "" {
return errors.New("产品名称不能为空")
}
if cmd.Code == "" {
return errors.New("产品编号不能为空")
}
if cmd.CategoryID == "" {
return errors.New("产品分类不能为空")
}
if cmd.Price < 0 {
return errors.New("产品价格不能为负数")
}
return nil
}
func (s *ProductApplicationServiceImpl) validateUpdateProduct(cmd *commands.UpdateProductCommand) error {
if cmd.ID == "" {
return errors.New("产品ID不能为空")
}
if cmd.Price < 0 {
return errors.New("产品价格不能为负数")
}
return nil
}
func (s *ProductApplicationServiceImpl) convertToProductInfoResponse(product *entities.Product) *responses.ProductInfoResponse {
return &responses.ProductInfoResponse{
ID: product.ID,
Name: product.Name,
Code: product.Code,
Description: product.Description,
Content: product.Content,
CategoryID: product.CategoryID,
Price: product.Price,
IsEnabled: product.IsEnabled,
IsVisible: product.IsVisible,
IsPackage: product.IsPackage,
SEOTitle: product.SEOTitle,
SEODescription: product.SEODescription,
SEOKeywords: product.SEOKeywords,
CreatedAt: product.CreatedAt,
UpdatedAt: product.UpdatedAt,
}
}
func (s *ProductApplicationServiceImpl) convertToCategoryInfoResponse(category *entities.ProductCategory) *responses.CategoryInfoResponse {
return &responses.CategoryInfoResponse{
ID: category.ID,
Name: category.Name,
Code: category.Code,
Description: category.Description,
ParentID: category.ParentID,
Level: category.Level,
Sort: category.Sort,
IsEnabled: category.IsEnabled,
IsVisible: category.IsVisible,
CreatedAt: category.CreatedAt,
UpdatedAt: category.UpdatedAt,
}
}

View File

@@ -0,0 +1,354 @@
package product
import (
"context"
"errors"
"fmt"
"time"
"tyapi-server/internal/application/product/dto/commands"
appQueries "tyapi-server/internal/application/product/dto/queries"
"tyapi-server/internal/application/product/dto/responses"
"tyapi-server/internal/domains/product/entities"
"tyapi-server/internal/domains/product/repositories"
repoQueries "tyapi-server/internal/domains/product/repositories/queries"
"tyapi-server/internal/domains/product/services"
"go.uber.org/zap"
)
// SubscriptionApplicationServiceImpl 订阅应用服务实现
type SubscriptionApplicationServiceImpl struct {
subscriptionRepo repositories.SubscriptionRepository
productRepo repositories.ProductRepository
productService *services.ProductService
logger *zap.Logger
}
// NewSubscriptionApplicationService 创建订阅应用服务
func NewSubscriptionApplicationService(
subscriptionRepo repositories.SubscriptionRepository,
productRepo repositories.ProductRepository,
productService *services.ProductService,
logger *zap.Logger,
) SubscriptionApplicationService {
return &SubscriptionApplicationServiceImpl{
subscriptionRepo: subscriptionRepo,
productRepo: productRepo,
productService: productService,
logger: logger,
}
}
// CreateSubscription 创建订阅
func (s *SubscriptionApplicationServiceImpl) CreateSubscription(ctx context.Context, cmd *commands.CreateSubscriptionCommand) error {
// 1. 参数验证
if err := s.validateCreateSubscription(cmd); err != nil {
return err
}
// 2. 检查产品是否存在且可订阅
canSubscribe, err := s.productService.CanUserSubscribeProduct(cmd.UserID, cmd.ProductID)
if err != nil {
return err
}
if !canSubscribe {
return errors.New("产品不可订阅或用户已有活跃订阅")
}
// 3. 检查产品是否存在
_, err = s.productRepo.GetByID(ctx, cmd.ProductID)
if err != nil {
return fmt.Errorf("产品不存在: %w", err)
}
// 5. 创建订阅实体
subscription := entities.Subscription{
UserID: cmd.UserID,
ProductID: cmd.ProductID,
Status: entities.SubscriptionStatusActive,
Price: cmd.Price,
APIUsed: 0,
}
// 6. 保存到仓储
createdSubscription, err := s.subscriptionRepo.Create(ctx, subscription)
if err != nil {
s.logger.Error("创建订阅失败", zap.Error(err))
return fmt.Errorf("创建订阅失败: %w", err)
}
s.logger.Info("创建订阅成功", zap.String("id", createdSubscription.ID), zap.String("user_id", cmd.UserID), zap.String("product_id", cmd.ProductID))
return nil
}
// UpdateSubscription 更新订阅
func (s *SubscriptionApplicationServiceImpl) UpdateSubscription(ctx context.Context, cmd *commands.UpdateSubscriptionCommand) error {
// 1. 获取现有订阅
subscription, err := s.subscriptionRepo.GetByID(ctx, cmd.ID)
if err != nil {
return fmt.Errorf("订阅不存在: %w", err)
}
// 2. 更新字段
if cmd.Price >= 0 {
subscription.Price = cmd.Price
}
// 3. 保存到仓储
if err := s.subscriptionRepo.Update(ctx, subscription); err != nil {
s.logger.Error("更新订阅失败", zap.Error(err))
return fmt.Errorf("更新订阅失败: %w", err)
}
s.logger.Info("更新订阅成功", zap.String("id", cmd.ID))
return nil
}
// GetSubscriptionByID 根据ID获取订阅
func (s *SubscriptionApplicationServiceImpl) GetSubscriptionByID(ctx context.Context, query *appQueries.GetSubscriptionQuery) (*responses.SubscriptionInfoResponse, error) {
subscription, err := s.subscriptionRepo.GetByID(ctx, query.ID)
if err != nil {
return nil, fmt.Errorf("订阅不存在: %w", err)
}
// 转换为响应对象
response := s.convertToSubscriptionInfoResponse(&subscription)
// 加载产品信息
product, err := s.productRepo.GetByID(ctx, subscription.ProductID)
if err == nil {
response.Product = s.convertToProductSimpleResponse(&product)
}
return response, nil
}
// ListSubscriptions 获取订阅列表
func (s *SubscriptionApplicationServiceImpl) ListSubscriptions(ctx context.Context, query *appQueries.ListSubscriptionsQuery) (*responses.SubscriptionListResponse, error) {
// 构建仓储查询
repoQuery := &repoQueries.ListSubscriptionsQuery{
Page: query.Page,
PageSize: query.PageSize,
UserID: query.UserID,
ProductID: query.ProductID,
Status: query.Status,
SortBy: query.SortBy,
SortOrder: query.SortOrder,
}
// 调用仓储
subscriptions, total, err := s.subscriptionRepo.ListSubscriptions(ctx, repoQuery)
if err != nil {
s.logger.Error("获取订阅列表失败", zap.Error(err))
return nil, fmt.Errorf("获取订阅列表失败: %w", err)
}
// 转换为响应对象
items := make([]responses.SubscriptionInfoResponse, len(subscriptions))
for i, subscription := range subscriptions {
items[i] = *s.convertToSubscriptionInfoResponse(subscription)
}
return &responses.SubscriptionListResponse{
Total: total,
Page: query.Page,
Size: query.PageSize,
Items: items,
}, nil
}
// UpdateAPIUsage 更新API使用量
func (s *SubscriptionApplicationServiceImpl) UpdateAPIUsage(ctx context.Context, cmd *commands.UpdateAPIUsageCommand) error {
subscription, err := s.subscriptionRepo.GetByID(ctx, cmd.ID)
if err != nil {
return fmt.Errorf("订阅不存在: %w", err)
}
subscription.IncrementAPIUsage(cmd.APIUsed)
if err := s.subscriptionRepo.Update(ctx, subscription); err != nil {
s.logger.Error("更新API使用量失败", zap.Error(err))
return fmt.Errorf("更新API使用量失败: %w", err)
}
s.logger.Info("更新API使用量成功", zap.String("id", cmd.ID), zap.Int64("api_used", cmd.APIUsed))
return nil
}
// ResetAPIUsage 重置API使用量
func (s *SubscriptionApplicationServiceImpl) ResetAPIUsage(ctx context.Context, cmd *commands.ResetAPIUsageCommand) error {
subscription, err := s.subscriptionRepo.GetByID(ctx, cmd.ID)
if err != nil {
return fmt.Errorf("订阅不存在: %w", err)
}
subscription.ResetAPIUsage()
if err := s.subscriptionRepo.Update(ctx, subscription); err != nil {
s.logger.Error("重置API使用量失败", zap.Error(err))
return fmt.Errorf("重置API使用量失败: %w", err)
}
s.logger.Info("重置API使用量成功", zap.String("id", cmd.ID))
return nil
}
// GetUserSubscriptions 获取用户订阅
func (s *SubscriptionApplicationServiceImpl) GetUserSubscriptions(ctx context.Context, query *appQueries.GetUserSubscriptionsQuery) ([]*responses.SubscriptionInfoResponse, error) {
subscriptions, err := s.subscriptionRepo.FindByUserID(ctx, query.UserID)
if err != nil {
s.logger.Error("获取用户订阅失败", zap.Error(err))
return nil, fmt.Errorf("获取用户订阅失败: %w", err)
}
// 过滤状态
if query.Status != nil {
filtered := make([]*entities.Subscription, 0)
for _, sub := range subscriptions {
if sub.Status == *query.Status {
filtered = append(filtered, sub)
}
}
subscriptions = filtered
}
// 转换为响应对象
items := make([]*responses.SubscriptionInfoResponse, len(subscriptions))
for i, subscription := range subscriptions {
items[i] = s.convertToSubscriptionInfoResponse(subscription)
}
return items, nil
}
// GetProductSubscriptions 获取产品订阅
func (s *SubscriptionApplicationServiceImpl) GetProductSubscriptions(ctx context.Context, query *appQueries.GetProductSubscriptionsQuery) ([]*responses.SubscriptionInfoResponse, error) {
subscriptions, err := s.subscriptionRepo.FindByProductID(ctx, query.ProductID)
if err != nil {
s.logger.Error("获取产品订阅失败", zap.Error(err))
return nil, fmt.Errorf("获取产品订阅失败: %w", err)
}
// 过滤状态
if query.Status != nil {
filtered := make([]*entities.Subscription, 0)
for _, sub := range subscriptions {
if sub.Status == *query.Status {
filtered = append(filtered, sub)
}
}
subscriptions = filtered
}
// 转换为响应对象
items := make([]*responses.SubscriptionInfoResponse, len(subscriptions))
for i, subscription := range subscriptions {
items[i] = s.convertToSubscriptionInfoResponse(subscription)
}
return items, nil
}
// GetSubscriptionUsage 获取订阅使用情况
func (s *SubscriptionApplicationServiceImpl) GetSubscriptionUsage(ctx context.Context, subscriptionID string) (*responses.SubscriptionUsageResponse, error) {
subscription, err := s.subscriptionRepo.GetByID(ctx, subscriptionID)
if err != nil {
return nil, fmt.Errorf("订阅不存在: %w", err)
}
return &responses.SubscriptionUsageResponse{
ID: subscription.ID,
ProductID: subscription.ProductID,
APIUsed: subscription.APIUsed,
}, nil
}
// GetSubscriptionStats 获取订阅统计
func (s *SubscriptionApplicationServiceImpl) GetSubscriptionStats(ctx context.Context) (*responses.SubscriptionStatsResponse, error) {
// 获取各种状态的订阅数量
total, err := s.subscriptionRepo.CountActive(ctx)
if err != nil {
s.logger.Error("获取订阅统计失败", zap.Error(err))
return nil, fmt.Errorf("获取订阅统计失败: %w", err)
}
// TODO: 计算总收入,需要从订单系统获取
totalRevenue := 0.0
return &responses.SubscriptionStatsResponse{
TotalSubscriptions: total,
TotalRevenue: totalRevenue,
}, nil
}
// 私有方法
func (s *SubscriptionApplicationServiceImpl) validateCreateSubscription(cmd *commands.CreateSubscriptionCommand) error {
if cmd.UserID == "" {
return errors.New("用户ID不能为空")
}
if cmd.ProductID == "" {
return errors.New("产品ID不能为空")
}
if cmd.Price < 0 {
return errors.New("订阅价格不能为负数")
}
if cmd.APILimit < 0 {
return errors.New("API调用限制不能为负数")
}
return nil
}
func (s *SubscriptionApplicationServiceImpl) calculateEndDate(duration string) (*time.Time, error) {
if duration == "" {
return nil, nil // 永久订阅
}
d, err := s.parseDuration(duration)
if err != nil {
return nil, err
}
endDate := time.Now().Add(d)
return &endDate, nil
}
func (s *SubscriptionApplicationServiceImpl) parseDuration(duration string) (time.Duration, error) {
switch duration {
case "7d":
return 7 * 24 * time.Hour, nil
case "30d":
return 30 * 24 * time.Hour, nil
case "90d":
return 90 * 24 * time.Hour, nil
case "365d":
return 365 * 24 * time.Hour, nil
default:
return time.ParseDuration(duration)
}
}
func (s *SubscriptionApplicationServiceImpl) convertToSubscriptionInfoResponse(subscription *entities.Subscription) *responses.SubscriptionInfoResponse {
return &responses.SubscriptionInfoResponse{
ID: subscription.ID,
UserID: subscription.UserID,
ProductID: subscription.ProductID,
Price: subscription.Price,
APIUsed: subscription.APIUsed,
CreatedAt: subscription.CreatedAt,
UpdatedAt: subscription.UpdatedAt,
}
}
func (s *SubscriptionApplicationServiceImpl) convertToProductSimpleResponse(product *entities.Product) *responses.ProductSimpleResponse {
return &responses.ProductSimpleResponse{
ID: product.ID,
Name: product.Name,
Code: product.Code,
Description: product.Description,
Price: product.Price,
IsPackage: product.IsPackage,
}
}

View File

@@ -33,6 +33,15 @@ type ChangePasswordCommand struct {
Code string `json:"code" binding:"required,len=6" example:"123456"`
}
// ResetPasswordCommand 重置密码命令
// @Description 重置用户密码请求参数(忘记密码时使用)
type ResetPasswordCommand struct {
Phone string `json:"phone" binding:"required,len=11" example:"13800138000"`
NewPassword string `json:"new_password" binding:"required,min=6,max=128" example:"newpassword123"`
ConfirmNewPassword string `json:"confirm_new_password" binding:"required,eqfield=NewPassword" example:"newpassword123"`
Code string `json:"code" binding:"required,len=6" example:"123456"`
}
// SendCodeCommand 发送验证码命令
// @Description 发送短信验证码请求参数
type SendCodeCommand struct {

View File

@@ -43,6 +43,7 @@ type UserProfileResponse struct {
ID string `json:"id" example:"123e4567-e89b-12d3-a456-426614174000"`
Phone string `json:"phone" example:"13800138000"`
EnterpriseInfo *EnterpriseInfoResponse `json:"enterprise_info,omitempty"`
IsCertified bool `json:"is_certified" example:"false"`
CreatedAt time.Time `json:"created_at" example:"2024-01-01T00:00:00Z"`
UpdatedAt time.Time `json:"updated_at" example:"2024-01-01T00:00:00Z"`
}

View File

@@ -13,6 +13,7 @@ type UserApplicationService interface {
LoginWithPassword(ctx context.Context, cmd *commands.LoginWithPasswordCommand) (*responses.LoginUserResponse, error)
LoginWithSMS(ctx context.Context, cmd *commands.LoginWithSMSCommand) (*responses.LoginUserResponse, error)
ChangePassword(ctx context.Context, cmd *commands.ChangePasswordCommand) error
ResetPassword(ctx context.Context, cmd *commands.ResetPasswordCommand) error
GetUserProfile(ctx context.Context, userID string) (*responses.UserProfileResponse, error)
SendCode(ctx context.Context, cmd *commands.SendCodeCommand, clientIP, userAgent string) error
}

View File

@@ -178,6 +178,34 @@ func (s *UserApplicationServiceImpl) ChangePassword(ctx context.Context, cmd *co
return nil
}
// ResetPassword 重置密码
func (s *UserApplicationServiceImpl) ResetPassword(ctx context.Context, cmd *commands.ResetPasswordCommand) error {
user, err := s.userRepo.GetByPhone(ctx, cmd.Phone)
if err != nil {
return fmt.Errorf("用户不存在")
}
if err := s.smsCodeService.VerifyCode(ctx, cmd.Phone, cmd.Code, entities.SMSSceneResetPassword); err != nil {
return fmt.Errorf("验证码错误或已过期")
}
if err := user.ResetPassword(cmd.NewPassword, cmd.ConfirmNewPassword); err != nil {
return err
}
if err := s.userRepo.Update(ctx, *user); err != nil {
return fmt.Errorf("密码更新失败: %w", err)
}
event := events.NewUserPasswordChangedEvent(user.ID, user.Phone, "")
if err := s.eventBus.Publish(ctx, event); err != nil {
s.logger.Warn("发布密码重置事件失败", zap.Error(err))
}
s.logger.Info("密码重置成功", zap.String("user_id", user.ID), zap.String("phone", user.Phone))
return nil
}
// GetUserProfile 获取用户信息
func (s *UserApplicationServiceImpl) GetUserProfile(ctx context.Context, userID string) (*responses.UserProfileResponse, error) {
if userID == "" {
@@ -200,6 +228,7 @@ func (s *UserApplicationServiceImpl) GetUserProfile(ctx context.Context, userID
enterpriseInfo, err := s.enterpriseInfoRepo.GetByUserID(ctx, userID)
if err != nil {
s.logger.Debug("用户暂无企业信息", zap.String("user_id", userID))
response.IsCertified = false
} else {
response.EnterpriseInfo = &responses.EnterpriseInfoResponse{
ID: enterpriseInfo.ID,
@@ -214,6 +243,7 @@ func (s *UserApplicationServiceImpl) GetUserProfile(ctx context.Context, userID
CreatedAt: enterpriseInfo.CreatedAt,
UpdatedAt: enterpriseInfo.UpdatedAt,
}
response.IsCertified = enterpriseInfo.IsCertified
}
return response, nil

View File

@@ -11,6 +11,7 @@ import (
"tyapi-server/internal/application/admin"
"tyapi-server/internal/application/certification"
"tyapi-server/internal/application/finance"
"tyapi-server/internal/application/product"
"tyapi-server/internal/application/user"
"tyapi-server/internal/config"
domain_admin_repo "tyapi-server/internal/domains/admin/repositories"
@@ -19,6 +20,8 @@ import (
certification_service "tyapi-server/internal/domains/certification/services"
domain_finance_repo "tyapi-server/internal/domains/finance/repositories"
finance_service "tyapi-server/internal/domains/finance/services"
domain_product_repo "tyapi-server/internal/domains/product/repositories"
product_service "tyapi-server/internal/domains/product/services"
domain_user_repo "tyapi-server/internal/domains/user/repositories"
user_service "tyapi-server/internal/domains/user/services"
"tyapi-server/internal/infrastructure/cache"
@@ -26,6 +29,7 @@ import (
admin_repo "tyapi-server/internal/infrastructure/database/repositories/admin"
certification_repo "tyapi-server/internal/infrastructure/database/repositories/certification"
finance_repo "tyapi-server/internal/infrastructure/database/repositories/finance"
product_repo "tyapi-server/internal/infrastructure/database/repositories/product"
user_repo "tyapi-server/internal/infrastructure/database/repositories/user"
"tyapi-server/internal/infrastructure/external/ocr"
"tyapi-server/internal/infrastructure/external/sms"
@@ -286,6 +290,25 @@ func NewContainer() *Container {
),
),
// 仓储层 - 产品域
fx.Provide(
// 产品仓储 - 同时注册具体类型和接口类型
fx.Annotate(
product_repo.NewGormProductRepository,
fx.As(new(domain_product_repo.ProductRepository)),
),
// 产品分类仓储 - 同时注册具体类型和接口类型
fx.Annotate(
product_repo.NewGormProductCategoryRepository,
fx.As(new(domain_product_repo.ProductCategoryRepository)),
),
// 订阅仓储 - 同时注册具体类型和接口类型
fx.Annotate(
product_repo.NewGormSubscriptionRepository,
fx.As(new(domain_product_repo.SubscriptionRepository)),
),
),
// 领域服务
fx.Provide(
user_service.NewUserService,
@@ -295,6 +318,7 @@ func NewContainer() *Container {
certification_service.NewCertificationService,
certification_service.NewCertificationStateMachine,
finance_service.NewFinanceService,
product_service.NewProductService,
),
// 应用服务
@@ -319,6 +343,21 @@ func NewContainer() *Container {
finance.NewFinanceApplicationService,
fx.As(new(finance.FinanceApplicationService)),
),
// 产品应用服务 - 绑定到接口
fx.Annotate(
product.NewProductApplicationService,
fx.As(new(product.ProductApplicationService)),
),
// 分类应用服务 - 绑定到接口
fx.Annotate(
product.NewCategoryApplicationService,
fx.As(new(product.CategoryApplicationService)),
),
// 订阅应用服务 - 绑定到接口
fx.Annotate(
product.NewSubscriptionApplicationService,
fx.As(new(product.SubscriptionApplicationService)),
),
),
// HTTP处理器
@@ -331,14 +370,22 @@ func NewContainer() *Container {
handlers.NewCertificationHandler,
// 财务HTTP处理器
handlers.NewFinanceHandler,
// 产品HTTP处理器
handlers.NewProductHandler,
),
// 路由注册
// 路由注册
fx.Provide(
// 用户路由
routes.NewUserRoutes,
// 管理员路由
routes.NewAdminRoutes,
// 认证路由
routes.NewCertificationRoutes,
// 财务路由
routes.NewFinanceRoutes,
// 产品路由
routes.NewProductRoutes,
),
// 应用生命周期
@@ -416,6 +463,7 @@ func RegisterRoutes(
adminRoutes *routes.AdminRoutes,
certificationRoutes *routes.CertificationRoutes,
financeRoutes *routes.FinanceRoutes,
productRoutes *routes.ProductRoutes,
cfg *config.Config,
logger *zap.Logger,
) {
@@ -426,6 +474,7 @@ func RegisterRoutes(
adminRoutes.Register(router)
certificationRoutes.Register(router)
financeRoutes.Register(router)
productRoutes.Register(router)
// 打印注册的路由信息
router.PrintRoutes()

View File

@@ -0,0 +1,93 @@
package entities
import (
"time"
"gorm.io/gorm"
)
// Product 产品实体
type Product struct {
ID string `gorm:"primaryKey;type:varchar(36)" comment:"产品ID"`
Name string `gorm:"type:varchar(100);not null" comment:"产品名称"`
Code string `gorm:"type:varchar(50);uniqueIndex;not null" comment:"产品编号"`
Description string `gorm:"type:text" comment:"产品简介"`
Content string `gorm:"type:longtext" comment:"产品内容"`
CategoryID string `gorm:"type:varchar(36);not null" comment:"产品分类ID"`
Price float64 `gorm:"type:decimal(10,2);not null;default:0" comment:"产品价格"`
IsEnabled bool `gorm:"default:true" comment:"是否启用"`
IsVisible bool `gorm:"default:true" comment:"是否展示"`
IsPackage bool `gorm:"default:false" comment:"是否组合包"`
// SEO信息
SEOTitle string `gorm:"type:varchar(200)" comment:"SEO标题"`
SEODescription string `gorm:"type:text" comment:"SEO描述"`
SEOKeywords string `gorm:"type:text" comment:"SEO关键词"`
// 关联关系
Category *ProductCategory `gorm:"foreignKey:CategoryID" comment:"产品分类"`
CreatedAt time.Time `gorm:"autoCreateTime" comment:"创建时间"`
UpdatedAt time.Time `gorm:"autoUpdateTime" comment:"更新时间"`
DeletedAt gorm.DeletedAt `gorm:"index" comment:"软删除时间"`
}
// IsValid 检查产品是否有效
func (p *Product) IsValid() bool {
return p.DeletedAt.Time.IsZero() && p.IsEnabled
}
// IsVisibleToUser 检查产品是否对用户可见
func (p *Product) IsVisibleToUser() bool {
return p.IsValid() && p.IsVisible
}
// CanBeSubscribed 检查产品是否可以订阅
func (p *Product) CanBeSubscribed() bool {
return p.IsValid() && p.IsVisible
}
// GetDisplayPrice 获取显示价格
func (p *Product) GetDisplayPrice() float64 {
if p.Price < 0 {
return 0
}
return p.Price
}
// UpdateSEO 更新SEO信息
func (p *Product) UpdateSEO(title, description, keywords string) {
p.SEOTitle = title
p.SEODescription = description
p.SEOKeywords = keywords
}
// Enable 启用产品
func (p *Product) Enable() {
p.IsEnabled = true
}
// Disable 禁用产品
func (p *Product) Disable() {
p.IsEnabled = false
}
// Show 显示产品
func (p *Product) Show() {
p.IsVisible = true
}
// Hide 隐藏产品
func (p *Product) Hide() {
p.IsVisible = false
}
// SetAsPackage 设置为组合包
func (p *Product) SetAsPackage() {
p.IsPackage = true
}
// SetAsSingle 设置为单个产品
func (p *Product) SetAsSingle() {
p.IsPackage = false
}

View File

@@ -0,0 +1,80 @@
package entities
import (
"time"
"gorm.io/gorm"
)
// ProductCategory 产品分类实体
type ProductCategory struct {
ID string `gorm:"primaryKey;type:varchar(36)" comment:"分类ID"`
Name string `gorm:"type:varchar(100);not null" comment:"分类名称"`
Code string `gorm:"type:varchar(50);uniqueIndex;not null" comment:"分类编号"`
Description string `gorm:"type:text" comment:"分类描述"`
ParentID *string `gorm:"type:varchar(36)" comment:"父分类ID"`
Level int `gorm:"default:1" comment:"分类层级"`
Sort int `gorm:"default:0" comment:"排序"`
IsEnabled bool `gorm:"default:true" comment:"是否启用"`
IsVisible bool `gorm:"default:true" comment:"是否展示"`
// 关联关系
Parent *ProductCategory `gorm:"foreignKey:ParentID" comment:"父分类"`
Children []ProductCategory `gorm:"foreignKey:ParentID" comment:"子分类"`
Products []Product `gorm:"foreignKey:CategoryID" comment:"产品列表"`
CreatedAt time.Time `gorm:"autoCreateTime" comment:"创建时间"`
UpdatedAt time.Time `gorm:"autoUpdateTime" comment:"更新时间"`
DeletedAt gorm.DeletedAt `gorm:"index" comment:"软删除时间"`
}
// IsValid 检查分类是否有效
func (pc *ProductCategory) IsValid() bool {
return pc.DeletedAt.Time.IsZero() && pc.IsEnabled
}
// IsVisibleToUser 检查分类是否对用户可见
func (pc *ProductCategory) IsVisibleToUser() bool {
return pc.IsValid() && pc.IsVisible
}
// IsRoot 检查是否为根分类
func (pc *ProductCategory) IsRoot() bool {
return pc.ParentID == nil || *pc.ParentID == ""
}
// IsLeaf 检查是否为叶子分类
func (pc *ProductCategory) IsLeaf() bool {
return len(pc.Children) == 0
}
// GetFullPath 获取完整分类路径
func (pc *ProductCategory) GetFullPath() string {
if pc.IsRoot() {
return pc.Name
}
if pc.Parent != nil {
return pc.Parent.GetFullPath() + " > " + pc.Name
}
return pc.Name
}
// Enable 启用分类
func (pc *ProductCategory) Enable() {
pc.IsEnabled = true
}
// Disable 禁用分类
func (pc *ProductCategory) Disable() {
pc.IsEnabled = false
}
// Show 显示分类
func (pc *ProductCategory) Show() {
pc.IsVisible = true
}
// Hide 隐藏分类
func (pc *ProductCategory) Hide() {
pc.IsVisible = false
}

View File

@@ -0,0 +1,69 @@
package entities
import (
"time"
"gorm.io/gorm"
)
// ProductDocumentation 产品文档实体
type ProductDocumentation struct {
ID string `gorm:"primaryKey;type:varchar(36)" comment:"文档ID"`
ProductID string `gorm:"type:varchar(36);not null;uniqueIndex" comment:"产品ID"`
Title string `gorm:"type:varchar(200);not null" comment:"文档标题"`
Content string `gorm:"type:longtext;not null" comment:"文档内容"`
UsageGuide string `gorm:"type:longtext" comment:"使用指南"`
APIDocs string `gorm:"type:longtext" comment:"API文档"`
Examples string `gorm:"type:longtext" comment:"使用示例"`
FAQ string `gorm:"type:longtext" comment:"常见问题"`
Version string `gorm:"type:varchar(20);default:'1.0'" comment:"文档版本"`
Published bool `gorm:"default:false" comment:"是否已发布"`
// 关联关系
Product *Product `gorm:"foreignKey:ProductID" comment:"产品"`
CreatedAt time.Time `gorm:"autoCreateTime" comment:"创建时间"`
UpdatedAt time.Time `gorm:"autoUpdateTime" comment:"更新时间"`
DeletedAt gorm.DeletedAt `gorm:"index" comment:"软删除时间"`
}
// IsValid 检查文档是否有效
func (pd *ProductDocumentation) IsValid() bool {
return pd.DeletedAt.Time.IsZero()
}
// IsPublished 检查文档是否已发布
func (pd *ProductDocumentation) IsPublished() bool {
return pd.Published
}
// Publish 发布文档
func (pd *ProductDocumentation) Publish() {
pd.Published = true
}
// Unpublish 取消发布文档
func (pd *ProductDocumentation) Unpublish() {
pd.Published = false
}
// UpdateContent 更新文档内容
func (pd *ProductDocumentation) UpdateContent(title, content, usageGuide, apiDocs, examples, faq string) {
pd.Title = title
pd.Content = content
pd.UsageGuide = usageGuide
pd.APIDocs = apiDocs
pd.Examples = examples
pd.FAQ = faq
}
// IncrementVersion 增加版本号
func (pd *ProductDocumentation) IncrementVersion() {
// 简单的版本号递增逻辑,实际项目中可能需要更复杂的版本管理
if pd.Version == "" {
pd.Version = "1.0"
} else {
// 这里可以实现更复杂的版本号递增逻辑
pd.Version = pd.Version + ".1"
}
}

View File

@@ -0,0 +1,59 @@
package entities
import (
"time"
"gorm.io/gorm"
)
// SubscriptionStatus 订阅状态枚举
type SubscriptionStatus string
const (
SubscriptionStatusActive SubscriptionStatus = "active" // 活跃
SubscriptionStatusInactive SubscriptionStatus = "inactive" // 非活跃
SubscriptionStatusExpired SubscriptionStatus = "expired" // 已过期
SubscriptionStatusCanceled SubscriptionStatus = "canceled" // 已取消
)
// Subscription 订阅实体
type Subscription struct {
ID string `gorm:"primaryKey;type:varchar(36)" comment:"订阅ID"`
UserID string `gorm:"type:varchar(36);not null;index" comment:"用户ID"`
ProductID string `gorm:"type:varchar(36);not null;index" comment:"产品ID"`
Status SubscriptionStatus `gorm:"type:varchar(20);not null;default:'active'" comment:"订阅状态"`
Price float64 `gorm:"type:decimal(10,2);not null" comment:"订阅价格"`
APIUsed int64 `gorm:"default:0" comment:"已使用API调用次数"`
// 关联关系
Product *Product `gorm:"foreignKey:ProductID" comment:"产品"`
CreatedAt time.Time `gorm:"autoCreateTime" comment:"创建时间"`
UpdatedAt time.Time `gorm:"autoUpdateTime" comment:"更新时间"`
DeletedAt gorm.DeletedAt `gorm:"index" comment:"软删除时间"`
}
// IsValid 检查订阅是否有效
func (s *Subscription) IsValid() bool {
return s.DeletedAt.Time.IsZero()
}
// IncrementAPIUsage 增加API使用次数
func (s *Subscription) IncrementAPIUsage(count int64) {
s.APIUsed += count
}
// Activate 激活订阅
func (s *Subscription) Activate() {
s.Status = SubscriptionStatusActive
}
// Deactivate 停用订阅
func (s *Subscription) Deactivate() {
s.Status = SubscriptionStatusInactive
}
// ResetAPIUsage 重置API使用次数
func (s *Subscription) ResetAPIUsage() {
s.APIUsed = 0
}

View File

@@ -0,0 +1,33 @@
package repositories
import (
"context"
"tyapi-server/internal/domains/product/entities"
"tyapi-server/internal/domains/product/repositories/queries"
"tyapi-server/internal/shared/interfaces"
)
// ProductCategoryRepository 产品分类仓储接口
type ProductCategoryRepository interface {
interfaces.Repository[entities.ProductCategory]
// 基础查询方法
FindByCode(ctx context.Context, code string) (*entities.ProductCategory, error)
FindByParentID(ctx context.Context, parentID *string) ([]*entities.ProductCategory, error)
FindRootCategories(ctx context.Context) ([]*entities.ProductCategory, error)
FindVisible(ctx context.Context) ([]*entities.ProductCategory, error)
FindEnabled(ctx context.Context) ([]*entities.ProductCategory, error)
// 复杂查询方法
ListCategories(ctx context.Context, query *queries.ListCategoriesQuery) ([]*entities.ProductCategory, int64, error)
GetCategoryTree(ctx context.Context) ([]*entities.ProductCategory, error)
// 业务查询方法
FindCategoriesByLevel(ctx context.Context, level int) ([]*entities.ProductCategory, error)
FindCategoryPath(ctx context.Context, categoryID string) ([]*entities.ProductCategory, error)
// 统计方法
CountByParent(ctx context.Context, parentID *string) (int64, error)
CountEnabled(ctx context.Context) (int64, error)
CountVisible(ctx context.Context) (int64, error)
}

View File

@@ -0,0 +1,31 @@
package repositories
import (
"context"
"tyapi-server/internal/domains/product/entities"
"tyapi-server/internal/domains/product/repositories/queries"
"tyapi-server/internal/shared/interfaces"
)
// ProductRepository 产品仓储接口
type ProductRepository interface {
interfaces.Repository[entities.Product]
// 基础查询方法
FindByCode(ctx context.Context, code string) (*entities.Product, error)
FindByCategoryID(ctx context.Context, categoryID string) ([]*entities.Product, error)
FindVisible(ctx context.Context) ([]*entities.Product, error)
FindEnabled(ctx context.Context) ([]*entities.Product, error)
// 复杂查询方法
ListProducts(ctx context.Context, query *queries.ListProductsQuery) ([]*entities.Product, int64, error)
// 业务查询方法
FindSubscribableProducts(ctx context.Context, userID string) ([]*entities.Product, error)
FindProductsByIDs(ctx context.Context, ids []string) ([]*entities.Product, error)
// 统计方法
CountByCategory(ctx context.Context, categoryID string) (int64, error)
CountEnabled(ctx context.Context) (int64, error)
CountVisible(ctx context.Context) (int64, error)
}

View File

@@ -0,0 +1,25 @@
package queries
// ListCategoriesQuery 分类列表查询
type ListCategoriesQuery struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
ParentID *string `json:"parent_id"`
Level *int `json:"level"`
IsEnabled *bool `json:"is_enabled"`
IsVisible *bool `json:"is_visible"`
SortBy string `json:"sort_by"`
SortOrder string `json:"sort_order"`
}
// GetCategoryQuery 获取分类详情查询
type GetCategoryQuery struct {
ID string `json:"id"`
Code string `json:"code"`
}
// GetCategoryTreeQuery 获取分类树查询
type GetCategoryTreeQuery struct {
IncludeDisabled bool `json:"include_disabled"`
IncludeHidden bool `json:"include_hidden"`
}

View File

@@ -0,0 +1,42 @@
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"`
}
// SearchProductsQuery 产品搜索查询
type SearchProductsQuery 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"`
}
// GetProductQuery 获取产品详情查询
type GetProductQuery struct {
ID string `json:"id"`
Code string `json:"code"`
}
// GetProductsByIDsQuery 根据ID列表获取产品查询
type GetProductsByIDsQuery struct {
IDs []string `json:"ids"`
}

View File

@@ -0,0 +1,31 @@
package queries
import "tyapi-server/internal/domains/product/entities"
// ListSubscriptionsQuery 订阅列表查询
type ListSubscriptionsQuery struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
UserID string `json:"user_id"`
ProductID string `json:"product_id"`
Status entities.SubscriptionStatus `json:"status"`
SortBy string `json:"sort_by"`
SortOrder string `json:"sort_order"`
}
// GetSubscriptionQuery 获取订阅详情查询
type GetSubscriptionQuery struct {
ID string `json:"id"`
}
// GetUserSubscriptionsQuery 获取用户订阅查询
type GetUserSubscriptionsQuery struct {
UserID string `json:"user_id"`
Status *entities.SubscriptionStatus `json:"status"`
}
// GetProductSubscriptionsQuery 获取产品订阅查询
type GetProductSubscriptionsQuery struct {
ProductID string `json:"product_id"`
Status *entities.SubscriptionStatus `json:"status"`
}

View File

@@ -0,0 +1,34 @@
package repositories
import (
"context"
"tyapi-server/internal/domains/product/entities"
"tyapi-server/internal/domains/product/repositories/queries"
"tyapi-server/internal/shared/interfaces"
)
// SubscriptionRepository 订阅仓储接口
type SubscriptionRepository interface {
interfaces.Repository[entities.Subscription]
// 基础查询方法
FindByUserID(ctx context.Context, userID string) ([]*entities.Subscription, error)
FindByProductID(ctx context.Context, productID string) ([]*entities.Subscription, error)
FindByUserAndProduct(ctx context.Context, userID, productID string) (*entities.Subscription, error)
FindActive(ctx context.Context) ([]*entities.Subscription, error)
// 复杂查询方法
ListSubscriptions(ctx context.Context, query *queries.ListSubscriptionsQuery) ([]*entities.Subscription, int64, error)
FindUserActiveSubscriptions(ctx context.Context, userID string) ([]*entities.Subscription, error)
FindExpiredSubscriptions(ctx context.Context) ([]*entities.Subscription, error)
// 业务查询方法
FindSubscriptionsByStatus(ctx context.Context, status entities.SubscriptionStatus) ([]*entities.Subscription, error)
FindSubscriptionsByDateRange(ctx context.Context, startDate, endDate string) ([]*entities.Subscription, error)
// 统计方法
CountByUser(ctx context.Context, userID string) (int64, error)
CountByProduct(ctx context.Context, productID string) (int64, error)
CountByStatus(ctx context.Context, status entities.SubscriptionStatus) (int64, error)
CountActive(ctx context.Context) (int64, error)
}

View File

@@ -0,0 +1,151 @@
package services
import (
"errors"
"fmt"
"strings"
"tyapi-server/internal/domains/product/entities"
"tyapi-server/internal/domains/product/repositories"
)
// ProductService 产品领域服务
type ProductService struct {
productRepo repositories.ProductRepository
categoryRepo repositories.ProductCategoryRepository
subscriptionRepo repositories.SubscriptionRepository
}
// NewProductService 创建产品领域服务
func NewProductService(
productRepo repositories.ProductRepository,
categoryRepo repositories.ProductCategoryRepository,
subscriptionRepo repositories.SubscriptionRepository,
) *ProductService {
return &ProductService{
productRepo: productRepo,
categoryRepo: categoryRepo,
subscriptionRepo: subscriptionRepo,
}
}
// ValidateProduct 验证产品
func (s *ProductService) ValidateProduct(product *entities.Product) error {
if product == nil {
return errors.New("产品不能为空")
}
if strings.TrimSpace(product.Name) == "" {
return errors.New("产品名称不能为空")
}
if strings.TrimSpace(product.Code) == "" {
return errors.New("产品编号不能为空")
}
if product.Price < 0 {
return errors.New("产品价格不能为负数")
}
// 验证分类是否存在
if product.CategoryID != "" {
category, err := s.categoryRepo.GetByID(nil, product.CategoryID)
if err != nil {
return fmt.Errorf("产品分类不存在: %w", err)
}
if !category.IsValid() {
return errors.New("产品分类已禁用或删除")
}
}
return nil
}
// ValidateProductCode 验证产品编号唯一性
func (s *ProductService) ValidateProductCode(code string, excludeID string) error {
if strings.TrimSpace(code) == "" {
return errors.New("产品编号不能为空")
}
existingProduct, err := s.productRepo.FindByCode(nil, code)
if err == nil && existingProduct != nil && existingProduct.ID != excludeID {
return errors.New("产品编号已存在")
}
return nil
}
// CanUserSubscribeProduct 检查用户是否可以订阅产品
func (s *ProductService) CanUserSubscribeProduct(userID string, productID string) (bool, error) {
// 检查产品是否存在且可订阅
product, err := s.productRepo.GetByID(nil, productID)
if err != nil {
return false, fmt.Errorf("产品不存在: %w", err)
}
if !product.CanBeSubscribed() {
return false, errors.New("产品不可订阅")
}
// 检查用户是否已有该产品的订阅
existingSubscription, err := s.subscriptionRepo.FindByUserAndProduct(nil, userID, productID)
if err == nil && existingSubscription != nil {
return false, errors.New("用户已有该产品的订阅")
}
return true, nil
}
// GetProductWithCategory 获取产品及其分类信息
func (s *ProductService) GetProductWithCategory(productID string) (*entities.Product, error) {
product, err := s.productRepo.GetByID(nil, productID)
if err != nil {
return nil, fmt.Errorf("产品不存在: %w", err)
}
// 加载分类信息
if product.CategoryID != "" {
category, err := s.categoryRepo.GetByID(nil, product.CategoryID)
if err == nil {
product.Category = &category
}
}
return &product, nil
}
// GetVisibleProducts 获取可见产品列表
func (s *ProductService) GetVisibleProducts() ([]*entities.Product, error) {
return s.productRepo.FindVisible(nil)
}
// GetEnabledProducts 获取启用产品列表
func (s *ProductService) GetEnabledProducts() ([]*entities.Product, error) {
return s.productRepo.FindEnabled(nil)
}
// GetProductsByCategory 根据分类获取产品
func (s *ProductService) GetProductsByCategory(categoryID string) ([]*entities.Product, error) {
return s.productRepo.FindByCategoryID(nil, categoryID)
}
// GetProductStats 获取产品统计信息
func (s *ProductService) GetProductStats() (map[string]int64, error) {
stats := make(map[string]int64)
total, err := s.productRepo.CountByCategory(nil, "")
if err == nil {
stats["total"] = total
}
enabled, err := s.productRepo.CountEnabled(nil)
if err == nil {
stats["enabled"] = enabled
}
visible, err := s.productRepo.CountVisible(nil)
if err == nil {
stats["visible"] = visible
}
return stats, nil
}

View File

@@ -134,6 +134,28 @@ func (u *User) SetPassword(password string) error {
return nil
}
// ResetPassword 重置密码(忘记密码时使用)
func (u *User) ResetPassword(newPassword, confirmPassword string) error {
// 1. 验证确认密码
if newPassword != confirmPassword {
return NewValidationError("新密码和确认新密码不匹配")
}
// 2. 验证新密码强度
if err := u.validatePasswordStrength(newPassword); err != nil {
return err
}
// 3. 更新密码
hashedPassword, err := u.hashPassword(newPassword)
if err != nil {
return fmt.Errorf("密码加密失败: %w", err)
}
u.Password = hashedPassword
return nil
}
// CanLogin 检查用户是否可以登录
func (u *User) CanLogin() bool {
// 检查用户是否被删除

View File

@@ -0,0 +1,367 @@
package repositories
import (
"context"
"tyapi-server/internal/domains/product/entities"
"tyapi-server/internal/domains/product/repositories"
"tyapi-server/internal/domains/product/repositories/queries"
"tyapi-server/internal/shared/interfaces"
"go.uber.org/zap"
"gorm.io/gorm"
)
// GormProductCategoryRepository GORM产品分类仓储实现
type GormProductCategoryRepository struct {
db *gorm.DB
logger *zap.Logger
}
// 编译时检查接口实现
var _ repositories.ProductCategoryRepository = (*GormProductCategoryRepository)(nil)
// NewGormProductCategoryRepository 创建GORM产品分类仓储
func NewGormProductCategoryRepository(db *gorm.DB, logger *zap.Logger) repositories.ProductCategoryRepository {
return &GormProductCategoryRepository{
db: db,
logger: logger,
}
}
// Create 创建产品分类
func (r *GormProductCategoryRepository) Create(ctx context.Context, entity entities.ProductCategory) (entities.ProductCategory, error) {
r.logger.Info("创建产品分类", zap.String("id", entity.ID), zap.String("name", entity.Name))
err := r.db.WithContext(ctx).Create(&entity).Error
return entity, err
}
// GetByID 根据ID获取产品分类
func (r *GormProductCategoryRepository) GetByID(ctx context.Context, id string) (entities.ProductCategory, error) {
var entity entities.ProductCategory
err := r.db.WithContext(ctx).Where("id = ?", id).First(&entity).Error
return entity, err
}
// Update 更新产品分类
func (r *GormProductCategoryRepository) Update(ctx context.Context, entity entities.ProductCategory) error {
r.logger.Info("更新产品分类", zap.String("id", entity.ID))
return r.db.WithContext(ctx).Save(&entity).Error
}
// Delete 删除产品分类
func (r *GormProductCategoryRepository) Delete(ctx context.Context, id string) error {
r.logger.Info("删除产品分类", zap.String("id", id))
return r.db.WithContext(ctx).Delete(&entities.ProductCategory{}, "id = ?", id).Error
}
// FindByCode 根据编号查找产品分类
func (r *GormProductCategoryRepository) FindByCode(ctx context.Context, code string) (*entities.ProductCategory, error) {
var entity entities.ProductCategory
err := r.db.WithContext(ctx).Where("code = ?", code).First(&entity).Error
if err != nil {
return nil, err
}
return &entity, nil
}
// FindByParentID 根据父级ID查找产品分类
func (r *GormProductCategoryRepository) FindByParentID(ctx context.Context, parentID *string) ([]*entities.ProductCategory, error) {
var categories []entities.ProductCategory
query := r.db.WithContext(ctx)
if parentID == nil {
query = query.Where("parent_id IS NULL")
} else {
query = query.Where("parent_id = ?", *parentID)
}
err := query.Find(&categories).Error
if err != nil {
return nil, err
}
// 转换为指针切片
result := make([]*entities.ProductCategory, len(categories))
for i := range categories {
result[i] = &categories[i]
}
return result, nil
}
// FindRootCategories 查找根分类
func (r *GormProductCategoryRepository) FindRootCategories(ctx context.Context) ([]*entities.ProductCategory, error) {
return r.FindByParentID(ctx, nil)
}
// FindVisible 查找可见分类
func (r *GormProductCategoryRepository) FindVisible(ctx context.Context) ([]*entities.ProductCategory, error) {
var categories []entities.ProductCategory
err := r.db.WithContext(ctx).Where("is_visible = ? AND is_enabled = ?", true, true).Find(&categories).Error
if err != nil {
return nil, err
}
// 转换为指针切片
result := make([]*entities.ProductCategory, len(categories))
for i := range categories {
result[i] = &categories[i]
}
return result, nil
}
// FindEnabled 查找启用分类
func (r *GormProductCategoryRepository) FindEnabled(ctx context.Context) ([]*entities.ProductCategory, error) {
var categories []entities.ProductCategory
err := r.db.WithContext(ctx).Where("is_enabled = ?", true).Find(&categories).Error
if err != nil {
return nil, err
}
// 转换为指针切片
result := make([]*entities.ProductCategory, len(categories))
for i := range categories {
result[i] = &categories[i]
}
return result, nil
}
// ListCategories 获取分类列表
func (r *GormProductCategoryRepository) ListCategories(ctx context.Context, query *queries.ListCategoriesQuery) ([]*entities.ProductCategory, int64, error) {
var categories []entities.ProductCategory
var total int64
dbQuery := r.db.WithContext(ctx).Model(&entities.ProductCategory{})
// 应用筛选条件
if query.ParentID != nil {
dbQuery = dbQuery.Where("parent_id = ?", *query.ParentID)
}
if query.IsEnabled != nil {
dbQuery = dbQuery.Where("is_enabled = ?", *query.IsEnabled)
}
if query.IsVisible != nil {
dbQuery = dbQuery.Where("is_visible = ?", *query.IsVisible)
}
// 获取总数
if err := dbQuery.Count(&total).Error; err != nil {
return 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("sort_order ASC, 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.Find(&categories).Error; err != nil {
return nil, 0, err
}
// 转换为指针切片
result := make([]*entities.ProductCategory, len(categories))
for i := range categories {
result[i] = &categories[i]
}
return result, total, nil
}
// GetCategoryTree 获取分类树
func (r *GormProductCategoryRepository) GetCategoryTree(ctx context.Context) ([]*entities.ProductCategory, error) {
var categories []entities.ProductCategory
err := r.db.WithContext(ctx).Where("is_enabled = ?", true).Order("sort_order ASC, created_at ASC").Find(&categories).Error
if err != nil {
return nil, err
}
// 转换为指针切片
result := make([]*entities.ProductCategory, len(categories))
for i := range categories {
result[i] = &categories[i]
}
return result, nil
}
// FindCategoriesByLevel 根据层级查找分类
func (r *GormProductCategoryRepository) FindCategoriesByLevel(ctx context.Context, level int) ([]*entities.ProductCategory, error) {
var categories []entities.ProductCategory
err := r.db.WithContext(ctx).Where("level = ? AND is_enabled = ?", level, true).Find(&categories).Error
if err != nil {
return nil, err
}
// 转换为指针切片
result := make([]*entities.ProductCategory, len(categories))
for i := range categories {
result[i] = &categories[i]
}
return result, nil
}
// FindCategoryPath 查找分类路径
func (r *GormProductCategoryRepository) FindCategoryPath(ctx context.Context, categoryID string) ([]*entities.ProductCategory, error) {
// 这里需要递归查找父级分类,简化实现
var entity entities.ProductCategory
err := r.db.WithContext(ctx).Where("id = ?", categoryID).First(&entity).Error
if err != nil {
return nil, err
}
result := []*entities.ProductCategory{&entity}
return result, nil
}
// CountByParent 统计父级下的分类数量
func (r *GormProductCategoryRepository) CountByParent(ctx context.Context, parentID *string) (int64, error) {
var count int64
query := r.db.WithContext(ctx).Model(&entities.ProductCategory{})
if parentID == nil {
query = query.Where("parent_id IS NULL")
} else {
query = query.Where("parent_id = ?", *parentID)
}
err := query.Count(&count).Error
return count, err
}
// CountEnabled 统计启用分类数量
func (r *GormProductCategoryRepository) CountEnabled(ctx context.Context) (int64, error) {
var count int64
err := r.db.WithContext(ctx).Model(&entities.ProductCategory{}).Where("is_enabled = ?", true).Count(&count).Error
return count, err
}
// CountVisible 统计可见分类数量
func (r *GormProductCategoryRepository) CountVisible(ctx context.Context) (int64, error) {
var count int64
err := r.db.WithContext(ctx).Model(&entities.ProductCategory{}).Where("is_visible = ? AND is_enabled = ?", true, true).Count(&count).Error
return count, err
}
// 基础Repository接口方法
// Count 返回分类总数
func (r *GormProductCategoryRepository) Count(ctx context.Context, options interfaces.CountOptions) (int64, error) {
var count int64
query := r.db.WithContext(ctx).Model(&entities.ProductCategory{})
// 应用筛选条件
if options.Filters != nil {
for key, value := range options.Filters {
query = query.Where(key+" = ?", value)
}
}
// 应用搜索条件
if options.Search != "" {
query = query.Where("name LIKE ? OR description LIKE ?", "%"+options.Search+"%", "%"+options.Search+"%")
}
err := query.Count(&count).Error
return count, err
}
// GetByIDs 根据ID列表获取分类
func (r *GormProductCategoryRepository) GetByIDs(ctx context.Context, ids []string) ([]entities.ProductCategory, error) {
var categories []entities.ProductCategory
err := r.db.WithContext(ctx).Where("id IN ?", ids).Find(&categories).Error
return categories, err
}
// CreateBatch 批量创建分类
func (r *GormProductCategoryRepository) CreateBatch(ctx context.Context, categories []entities.ProductCategory) error {
return r.db.WithContext(ctx).Create(&categories).Error
}
// UpdateBatch 批量更新分类
func (r *GormProductCategoryRepository) UpdateBatch(ctx context.Context, categories []entities.ProductCategory) error {
return r.db.WithContext(ctx).Save(&categories).Error
}
// DeleteBatch 批量删除分类
func (r *GormProductCategoryRepository) DeleteBatch(ctx context.Context, ids []string) error {
return r.db.WithContext(ctx).Delete(&entities.ProductCategory{}, "id IN ?", ids).Error
}
// List 获取分类列表(基础方法)
func (r *GormProductCategoryRepository) List(ctx context.Context, options interfaces.ListOptions) ([]entities.ProductCategory, error) {
var categories []entities.ProductCategory
query := r.db.WithContext(ctx).Model(&entities.ProductCategory{})
// 应用筛选条件
if options.Filters != nil {
for key, value := range options.Filters {
query = query.Where(key+" = ?", value)
}
}
// 应用搜索条件
if options.Search != "" {
query = query.Where("name LIKE ? OR description LIKE ?", "%"+options.Search+"%", "%"+options.Search+"%")
}
// 应用排序
if options.Sort != "" {
order := options.Sort
if options.Order == "desc" {
order += " DESC"
} else {
order += " ASC"
}
query = query.Order(order)
}
// 应用分页
if options.Page > 0 && options.PageSize > 0 {
offset := (options.Page - 1) * options.PageSize
query = query.Offset(offset).Limit(options.PageSize)
}
err := query.Find(&categories).Error
return categories, err
}
// Exists 检查分类是否存在
func (r *GormProductCategoryRepository) Exists(ctx context.Context, id string) (bool, error) {
var count int64
err := r.db.WithContext(ctx).Model(&entities.ProductCategory{}).Where("id = ?", id).Count(&count).Error
return count > 0, err
}
// SoftDelete 软删除分类
func (r *GormProductCategoryRepository) SoftDelete(ctx context.Context, id string) error {
return r.db.WithContext(ctx).Delete(&entities.ProductCategory{}, "id = ?", id).Error
}
// Restore 恢复软删除的分类
func (r *GormProductCategoryRepository) Restore(ctx context.Context, id string) error {
return r.db.WithContext(ctx).Unscoped().Model(&entities.ProductCategory{}).Where("id = ?", id).Update("deleted_at", nil).Error
}
// WithTx 使用事务
func (r *GormProductCategoryRepository) WithTx(tx interface{}) interfaces.Repository[entities.ProductCategory] {
if gormTx, ok := tx.(*gorm.DB); ok {
return &GormProductCategoryRepository{
db: gormTx,
logger: r.logger,
}
}
return r
}

View File

@@ -0,0 +1,347 @@
package repositories
import (
"context"
"tyapi-server/internal/domains/product/entities"
"tyapi-server/internal/domains/product/repositories"
"tyapi-server/internal/domains/product/repositories/queries"
"tyapi-server/internal/shared/interfaces"
"go.uber.org/zap"
"gorm.io/gorm"
)
// GormProductRepository GORM产品仓储实现
type GormProductRepository struct {
db *gorm.DB
logger *zap.Logger
}
// 编译时检查接口实现
var _ repositories.ProductRepository = (*GormProductRepository)(nil)
// NewGormProductRepository 创建GORM产品仓储
func NewGormProductRepository(db *gorm.DB, logger *zap.Logger) repositories.ProductRepository {
return &GormProductRepository{
db: db,
logger: logger,
}
}
// Create 创建产品
func (r *GormProductRepository) Create(ctx context.Context, entity entities.Product) (entities.Product, error) {
r.logger.Info("创建产品", zap.String("id", entity.ID), zap.String("name", entity.Name))
err := r.db.WithContext(ctx).Create(&entity).Error
return entity, err
}
// GetByID 根据ID获取产品
func (r *GormProductRepository) GetByID(ctx context.Context, id string) (entities.Product, error) {
var entity entities.Product
err := r.db.WithContext(ctx).Where("id = ?", id).First(&entity).Error
return entity, err
}
// Update 更新产品
func (r *GormProductRepository) Update(ctx context.Context, entity entities.Product) error {
r.logger.Info("更新产品", zap.String("id", entity.ID))
return r.db.WithContext(ctx).Save(&entity).Error
}
// Delete 删除产品
func (r *GormProductRepository) Delete(ctx context.Context, id string) error {
r.logger.Info("删除产品", zap.String("id", id))
return r.db.WithContext(ctx).Delete(&entities.Product{}, "id = ?", id).Error
}
// FindByCode 根据编号查找产品
func (r *GormProductRepository) FindByCode(ctx context.Context, code string) (*entities.Product, error) {
var entity entities.Product
err := r.db.WithContext(ctx).Where("code = ?", code).First(&entity).Error
if err != nil {
return nil, err
}
return &entity, nil
}
// FindByCategoryID 根据分类ID查找产品
func (r *GormProductRepository) FindByCategoryID(ctx context.Context, categoryID string) ([]*entities.Product, error) {
var productEntities []entities.Product
err := r.db.WithContext(ctx).Where("category_id = ?", categoryID).Find(&productEntities).Error
if err != nil {
return nil, err
}
// 转换为指针切片
result := make([]*entities.Product, len(productEntities))
for i := range productEntities {
result[i] = &productEntities[i]
}
return result, nil
}
// FindVisible 查找可见产品
func (r *GormProductRepository) FindVisible(ctx context.Context) ([]*entities.Product, error) {
var productEntities []entities.Product
err := r.db.WithContext(ctx).Where("is_visible = ? AND is_enabled = ?", true, true).Find(&productEntities).Error
if err != nil {
return nil, err
}
// 转换为指针切片
result := make([]*entities.Product, len(productEntities))
for i := range productEntities {
result[i] = &productEntities[i]
}
return result, nil
}
// FindEnabled 查找启用产品
func (r *GormProductRepository) FindEnabled(ctx context.Context) ([]*entities.Product, error) {
var productEntities []entities.Product
err := r.db.WithContext(ctx).Where("is_enabled = ?", true).Find(&productEntities).Error
if err != nil {
return nil, err
}
// 转换为指针切片
result := make([]*entities.Product, len(productEntities))
for i := range productEntities {
result[i] = &productEntities[i]
}
return result, nil
}
// ListProducts 获取产品列表
func (r *GormProductRepository) ListProducts(ctx context.Context, query *queries.ListProductsQuery) ([]*entities.Product, int64, error) {
var productEntities []entities.Product
var total int64
dbQuery := r.db.WithContext(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)
}
// 获取总数
if err := dbQuery.Count(&total).Error; err != nil {
return 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.Find(&productEntities).Error; err != nil {
return nil, 0, err
}
// 转换为指针切片
result := make([]*entities.Product, len(productEntities))
for i := range productEntities {
result[i] = &productEntities[i]
}
return result, total, nil
}
// FindSubscribableProducts 查找可订阅产品
func (r *GormProductRepository) FindSubscribableProducts(ctx context.Context, userID string) ([]*entities.Product, error) {
var productEntities []entities.Product
err := r.db.WithContext(ctx).Where("is_enabled = ? AND is_visible = ?", true, true).Find(&productEntities).Error
if err != nil {
return nil, err
}
// 转换为指针切片
result := make([]*entities.Product, len(productEntities))
for i := range productEntities {
result[i] = &productEntities[i]
}
return result, nil
}
// FindProductsByIDs 根据ID列表查找产品
func (r *GormProductRepository) FindProductsByIDs(ctx context.Context, ids []string) ([]*entities.Product, error) {
var productEntities []entities.Product
err := r.db.WithContext(ctx).Where("id IN ?", ids).Find(&productEntities).Error
if err != nil {
return nil, err
}
// 转换为指针切片
result := make([]*entities.Product, len(productEntities))
for i := range productEntities {
result[i] = &productEntities[i]
}
return result, nil
}
// CountByCategory 统计分类下的产品数量
func (r *GormProductRepository) CountByCategory(ctx context.Context, categoryID string) (int64, error) {
var count int64
query := r.db.WithContext(ctx).Model(&entities.Product{})
if categoryID != "" {
query = query.Where("category_id = ?", categoryID)
}
err := query.Count(&count).Error
return count, err
}
// CountEnabled 统计启用产品数量
func (r *GormProductRepository) CountEnabled(ctx context.Context) (int64, error) {
var count int64
err := r.db.WithContext(ctx).Model(&entities.Product{}).Where("is_enabled = ?", true).Count(&count).Error
return count, err
}
// CountVisible 统计可见产品数量
func (r *GormProductRepository) CountVisible(ctx context.Context) (int64, error) {
var count int64
err := r.db.WithContext(ctx).Model(&entities.Product{}).Where("is_visible = ? AND is_enabled = ?", true, true).Count(&count).Error
return count, err
}
// Count 返回产品总数
func (r *GormProductRepository) Count(ctx context.Context, options interfaces.CountOptions) (int64, error) {
var count int64
query := r.db.WithContext(ctx).Model(&entities.Product{})
// 应用筛选条件
if options.Filters != nil {
for key, value := range options.Filters {
query = query.Where(key+" = ?", value)
}
}
// 应用搜索条件
if options.Search != "" {
query = query.Where("name LIKE ? OR description LIKE ?", "%"+options.Search+"%", "%"+options.Search+"%")
}
err := query.Count(&count).Error
return count, err
}
// GetByIDs 根据ID列表获取产品
func (r *GormProductRepository) GetByIDs(ctx context.Context, ids []string) ([]entities.Product, error) {
var products []entities.Product
err := r.db.WithContext(ctx).Where("id IN ?", ids).Find(&products).Error
return products, err
}
// CreateBatch 批量创建产品
func (r *GormProductRepository) CreateBatch(ctx context.Context, products []entities.Product) error {
return r.db.WithContext(ctx).Create(&products).Error
}
// UpdateBatch 批量更新产品
func (r *GormProductRepository) UpdateBatch(ctx context.Context, products []entities.Product) error {
return r.db.WithContext(ctx).Save(&products).Error
}
// DeleteBatch 批量删除产品
func (r *GormProductRepository) DeleteBatch(ctx context.Context, ids []string) error {
return r.db.WithContext(ctx).Delete(&entities.Product{}, "id IN ?", ids).Error
}
// List 获取产品列表(基础方法)
func (r *GormProductRepository) List(ctx context.Context, options interfaces.ListOptions) ([]entities.Product, error) {
var products []entities.Product
query := r.db.WithContext(ctx).Model(&entities.Product{})
// 应用筛选条件
if options.Filters != nil {
for key, value := range options.Filters {
query = query.Where(key+" = ?", value)
}
}
// 应用搜索条件
if options.Search != "" {
query = query.Where("name LIKE ? OR description LIKE ?", "%"+options.Search+"%", "%"+options.Search+"%")
}
// 应用排序
if options.Sort != "" {
order := options.Sort
if options.Order == "desc" {
order += " DESC"
} else {
order += " ASC"
}
query = query.Order(order)
}
// 应用分页
if options.Page > 0 && options.PageSize > 0 {
offset := (options.Page - 1) * options.PageSize
query = query.Offset(offset).Limit(options.PageSize)
}
err := query.Find(&products).Error
return products, err
}
// Exists 检查产品是否存在
func (r *GormProductRepository) Exists(ctx context.Context, id string) (bool, error) {
var count int64
err := r.db.WithContext(ctx).Model(&entities.Product{}).Where("id = ?", id).Count(&count).Error
return count > 0, err
}
// SoftDelete 软删除产品
func (r *GormProductRepository) SoftDelete(ctx context.Context, id string) error {
return r.db.WithContext(ctx).Delete(&entities.Product{}, "id = ?", id).Error
}
// Restore 恢复软删除的产品
func (r *GormProductRepository) Restore(ctx context.Context, id string) error {
return r.db.WithContext(ctx).Unscoped().Model(&entities.Product{}).Where("id = ?", id).Update("deleted_at", nil).Error
}
// WithTx 使用事务
func (r *GormProductRepository) WithTx(tx interface{}) interfaces.Repository[entities.Product] {
if gormTx, ok := tx.(*gorm.DB); ok {
return &GormProductRepository{
db: gormTx,
logger: r.logger,
}
}
return r
}

View File

@@ -0,0 +1,374 @@
package repositories
import (
"context"
"time"
"tyapi-server/internal/domains/product/entities"
"tyapi-server/internal/domains/product/repositories"
"tyapi-server/internal/domains/product/repositories/queries"
"tyapi-server/internal/shared/interfaces"
"go.uber.org/zap"
"gorm.io/gorm"
)
// GormSubscriptionRepository GORM订阅仓储实现
type GormSubscriptionRepository struct {
db *gorm.DB
logger *zap.Logger
}
// 编译时检查接口实现
var _ repositories.SubscriptionRepository = (*GormSubscriptionRepository)(nil)
// NewGormSubscriptionRepository 创建GORM订阅仓储
func NewGormSubscriptionRepository(db *gorm.DB, logger *zap.Logger) repositories.SubscriptionRepository {
return &GormSubscriptionRepository{
db: db,
logger: logger,
}
}
// Create 创建订阅
func (r *GormSubscriptionRepository) Create(ctx context.Context, entity entities.Subscription) (entities.Subscription, error) {
r.logger.Info("创建订阅", zap.String("id", entity.ID), zap.String("user_id", entity.UserID))
err := r.db.WithContext(ctx).Create(&entity).Error
return entity, err
}
// GetByID 根据ID获取订阅
func (r *GormSubscriptionRepository) GetByID(ctx context.Context, id string) (entities.Subscription, error) {
var entity entities.Subscription
err := r.db.WithContext(ctx).Where("id = ?", id).First(&entity).Error
return entity, err
}
// Update 更新订阅
func (r *GormSubscriptionRepository) Update(ctx context.Context, entity entities.Subscription) error {
r.logger.Info("更新订阅", zap.String("id", entity.ID))
return r.db.WithContext(ctx).Save(&entity).Error
}
// Delete 删除订阅
func (r *GormSubscriptionRepository) Delete(ctx context.Context, id string) error {
r.logger.Info("删除订阅", zap.String("id", id))
return r.db.WithContext(ctx).Delete(&entities.Subscription{}, "id = ?", id).Error
}
// FindByUserID 根据用户ID查找订阅
func (r *GormSubscriptionRepository) FindByUserID(ctx context.Context, userID string) ([]*entities.Subscription, error) {
var subscriptions []entities.Subscription
err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&subscriptions).Error
if err != nil {
return nil, err
}
// 转换为指针切片
result := make([]*entities.Subscription, len(subscriptions))
for i := range subscriptions {
result[i] = &subscriptions[i]
}
return result, nil
}
// FindByProductID 根据产品ID查找订阅
func (r *GormSubscriptionRepository) FindByProductID(ctx context.Context, productID string) ([]*entities.Subscription, error) {
var subscriptions []entities.Subscription
err := r.db.WithContext(ctx).Where("product_id = ?", productID).Find(&subscriptions).Error
if err != nil {
return nil, err
}
// 转换为指针切片
result := make([]*entities.Subscription, len(subscriptions))
for i := range subscriptions {
result[i] = &subscriptions[i]
}
return result, nil
}
// FindByUserAndProduct 根据用户和产品查找订阅
func (r *GormSubscriptionRepository) FindByUserAndProduct(ctx context.Context, userID, productID string) (*entities.Subscription, error) {
var entity entities.Subscription
err := r.db.WithContext(ctx).Where("user_id = ? AND product_id = ?", userID, productID).First(&entity).Error
if err != nil {
return nil, err
}
return &entity, nil
}
// FindActive 查找活跃订阅
func (r *GormSubscriptionRepository) FindActive(ctx context.Context) ([]*entities.Subscription, error) {
var subscriptions []entities.Subscription
err := r.db.WithContext(ctx).Where("status = ?", entities.SubscriptionStatusActive).Find(&subscriptions).Error
if err != nil {
return nil, err
}
// 转换为指针切片
result := make([]*entities.Subscription, len(subscriptions))
for i := range subscriptions {
result[i] = &subscriptions[i]
}
return result, nil
}
// ListSubscriptions 获取订阅列表
func (r *GormSubscriptionRepository) ListSubscriptions(ctx context.Context, query *queries.ListSubscriptionsQuery) ([]*entities.Subscription, int64, error) {
var subscriptions []entities.Subscription
var total int64
dbQuery := r.db.WithContext(ctx).Model(&entities.Subscription{})
// 应用筛选条件
if query.UserID != "" {
dbQuery = dbQuery.Where("user_id = ?", query.UserID)
}
if query.ProductID != "" {
dbQuery = dbQuery.Where("product_id = ?", query.ProductID)
}
if query.Status != "" {
dbQuery = dbQuery.Where("status = ?", query.Status)
}
// 获取总数
if err := dbQuery.Count(&total).Error; err != nil {
return 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.Find(&subscriptions).Error; err != nil {
return nil, 0, err
}
// 转换为指针切片
result := make([]*entities.Subscription, len(subscriptions))
for i := range subscriptions {
result[i] = &subscriptions[i]
}
return result, total, nil
}
// FindUserActiveSubscriptions 查找用户活跃订阅
func (r *GormSubscriptionRepository) FindUserActiveSubscriptions(ctx context.Context, userID string) ([]*entities.Subscription, error) {
var subscriptions []entities.Subscription
err := r.db.WithContext(ctx).Where("user_id = ? AND status = ?", userID, entities.SubscriptionStatusActive).Find(&subscriptions).Error
if err != nil {
return nil, err
}
// 转换为指针切片
result := make([]*entities.Subscription, len(subscriptions))
for i := range subscriptions {
result[i] = &subscriptions[i]
}
return result, nil
}
// FindExpiredSubscriptions 查找过期订阅
func (r *GormSubscriptionRepository) FindExpiredSubscriptions(ctx context.Context) ([]*entities.Subscription, error) {
var subscriptions []entities.Subscription
now := time.Now()
err := r.db.WithContext(ctx).Where("end_date IS NOT NULL AND end_date < ? AND status = ?", now, entities.SubscriptionStatusActive).Find(&subscriptions).Error
if err != nil {
return nil, err
}
// 转换为指针切片
result := make([]*entities.Subscription, len(subscriptions))
for i := range subscriptions {
result[i] = &subscriptions[i]
}
return result, nil
}
// FindSubscriptionsByStatus 根据状态查找订阅
func (r *GormSubscriptionRepository) FindSubscriptionsByStatus(ctx context.Context, status entities.SubscriptionStatus) ([]*entities.Subscription, error) {
var subscriptions []entities.Subscription
err := r.db.WithContext(ctx).Where("status = ?", status).Find(&subscriptions).Error
if err != nil {
return nil, err
}
// 转换为指针切片
result := make([]*entities.Subscription, len(subscriptions))
for i := range subscriptions {
result[i] = &subscriptions[i]
}
return result, nil
}
// FindSubscriptionsByDateRange 根据日期范围查找订阅
func (r *GormSubscriptionRepository) FindSubscriptionsByDateRange(ctx context.Context, startDate, endDate string) ([]*entities.Subscription, error) {
var subscriptions []entities.Subscription
err := r.db.WithContext(ctx).Where("created_at BETWEEN ? AND ?", startDate, endDate).Find(&subscriptions).Error
if err != nil {
return nil, err
}
// 转换为指针切片
result := make([]*entities.Subscription, len(subscriptions))
for i := range subscriptions {
result[i] = &subscriptions[i]
}
return result, nil
}
// CountByUser 统计用户订阅数量
func (r *GormSubscriptionRepository) CountByUser(ctx context.Context, userID string) (int64, error) {
var count int64
err := r.db.WithContext(ctx).Model(&entities.Subscription{}).Where("user_id = ?", userID).Count(&count).Error
return count, err
}
// CountByProduct 统计产品订阅数量
func (r *GormSubscriptionRepository) CountByProduct(ctx context.Context, productID string) (int64, error) {
var count int64
err := r.db.WithContext(ctx).Model(&entities.Subscription{}).Where("product_id = ?", productID).Count(&count).Error
return count, err
}
// CountByStatus 根据状态统计订阅数量
func (r *GormSubscriptionRepository) CountByStatus(ctx context.Context, status entities.SubscriptionStatus) (int64, error) {
var count int64
err := r.db.WithContext(ctx).Model(&entities.Subscription{}).Where("status = ?", status).Count(&count).Error
return count, err
}
// CountActive 统计活跃订阅数量
func (r *GormSubscriptionRepository) CountActive(ctx context.Context) (int64, error) {
var count int64
err := r.db.WithContext(ctx).Model(&entities.Subscription{}).Where("status = ?", entities.SubscriptionStatusActive).Count(&count).Error
return count, err
}
// 基础Repository接口方法
// Count 返回订阅总数
func (r *GormSubscriptionRepository) Count(ctx context.Context, options interfaces.CountOptions) (int64, error) {
var count int64
query := r.db.WithContext(ctx).Model(&entities.Subscription{})
// 应用筛选条件
if options.Filters != nil {
for key, value := range options.Filters {
query = query.Where(key+" = ?", value)
}
}
// 应用搜索条件
if options.Search != "" {
query = query.Where("user_id LIKE ? OR product_id LIKE ?", "%"+options.Search+"%", "%"+options.Search+"%")
}
err := query.Count(&count).Error
return count, err
}
// GetByIDs 根据ID列表获取订阅
func (r *GormSubscriptionRepository) GetByIDs(ctx context.Context, ids []string) ([]entities.Subscription, error) {
var subscriptions []entities.Subscription
err := r.db.WithContext(ctx).Where("id IN ?", ids).Find(&subscriptions).Error
return subscriptions, err
}
// CreateBatch 批量创建订阅
func (r *GormSubscriptionRepository) CreateBatch(ctx context.Context, subscriptions []entities.Subscription) error {
return r.db.WithContext(ctx).Create(&subscriptions).Error
}
// UpdateBatch 批量更新订阅
func (r *GormSubscriptionRepository) UpdateBatch(ctx context.Context, subscriptions []entities.Subscription) error {
return r.db.WithContext(ctx).Save(&subscriptions).Error
}
// DeleteBatch 批量删除订阅
func (r *GormSubscriptionRepository) DeleteBatch(ctx context.Context, ids []string) error {
return r.db.WithContext(ctx).Delete(&entities.Subscription{}, "id IN ?", ids).Error
}
// List 获取订阅列表(基础方法)
func (r *GormSubscriptionRepository) List(ctx context.Context, options interfaces.ListOptions) ([]entities.Subscription, error) {
var subscriptions []entities.Subscription
query := r.db.WithContext(ctx).Model(&entities.Subscription{})
// 应用筛选条件
if options.Filters != nil {
for key, value := range options.Filters {
query = query.Where(key+" = ?", value)
}
}
// 应用搜索条件
if options.Search != "" {
query = query.Where("user_id LIKE ? OR product_id LIKE ?", "%"+options.Search+"%", "%"+options.Search+"%")
}
// 应用排序
if options.Sort != "" {
order := options.Sort
if options.Order == "desc" {
order += " DESC"
} else {
order += " ASC"
}
query = query.Order(order)
}
// 应用分页
if options.Page > 0 && options.PageSize > 0 {
offset := (options.Page - 1) * options.PageSize
query = query.Offset(offset).Limit(options.PageSize)
}
err := query.Find(&subscriptions).Error
return subscriptions, err
}
// Exists 检查订阅是否存在
func (r *GormSubscriptionRepository) Exists(ctx context.Context, id string) (bool, error) {
var count int64
err := r.db.WithContext(ctx).Model(&entities.Subscription{}).Where("id = ?", id).Count(&count).Error
return count > 0, err
}
// SoftDelete 软删除订阅
func (r *GormSubscriptionRepository) SoftDelete(ctx context.Context, id string) error {
return r.db.WithContext(ctx).Delete(&entities.Subscription{}, "id = ?", id).Error
}
// Restore 恢复软删除的订阅
func (r *GormSubscriptionRepository) Restore(ctx context.Context, id string) error {
return r.db.WithContext(ctx).Unscoped().Model(&entities.Subscription{}).Where("id = ?", id).Update("deleted_at", nil).Error
}
// WithTx 使用事务
func (r *GormSubscriptionRepository) WithTx(tx interface{}) interfaces.Repository[entities.Subscription] {
if gormTx, ok := tx.(*gorm.DB); ok {
return &GormSubscriptionRepository{
db: gormTx,
logger: r.logger,
}
}
return r
}

View File

@@ -33,7 +33,7 @@ func NewAdminHandler(
// Login 管理员登录
// @Summary 管理员登录
// @Description 使用用户名和密码进行管理员登录返回JWT令牌
// @Tags 管理员认证
// @Tags 管理员管理
// @Accept json
// @Produce json
// @Param request body commands.AdminLoginCommand true "管理员登录请求"

View File

@@ -0,0 +1,444 @@
package handlers
import (
"strconv"
"tyapi-server/internal/application/product"
"tyapi-server/internal/application/product/dto/commands"
"tyapi-server/internal/application/product/dto/queries"
"tyapi-server/internal/shared/interfaces"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// ProductHandler 产品相关HTTP处理器
type ProductHandler struct {
appService product.ProductApplicationService
categoryService product.CategoryApplicationService
subAppService product.SubscriptionApplicationService
responseBuilder interfaces.ResponseBuilder
logger *zap.Logger
}
// NewProductHandler 创建产品HTTP处理器
func NewProductHandler(
appService product.ProductApplicationService,
categoryService product.CategoryApplicationService,
subAppService product.SubscriptionApplicationService,
responseBuilder interfaces.ResponseBuilder,
logger *zap.Logger,
) *ProductHandler {
return &ProductHandler{
appService: appService,
categoryService: categoryService,
subAppService: subAppService,
responseBuilder: responseBuilder,
logger: logger,
}
}
// ListProducts 获取产品列表(数据大厅)
// @Summary 获取产品列表
// @Description 分页获取可用的产品列表,支持筛选
// @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 min_price query number false "最低价格"
// @Param max_price query number false "最高价格"
// @Param is_enabled query bool false "是否启用"
// @Param is_visible query bool false "是否可见"
// @Param is_package query bool false "是否组合包"
// @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) {
var query queries.ListProductsQuery
if err := c.ShouldBindQuery(&query); err != nil {
h.responseBuilder.BadRequest(c, "请求参数错误")
return
}
// 设置默认值
if query.Page <= 0 {
query.Page = 1
}
if query.PageSize <= 0 {
query.PageSize = 10
}
if query.PageSize > 100 {
query.PageSize = 100
}
result, err := h.appService.ListProducts(c.Request.Context(), &query)
if err != nil {
h.logger.Error("获取产品列表失败", zap.Error(err))
h.responseBuilder.InternalError(c, "获取产品列表失败")
return
}
h.responseBuilder.Success(c, result, "获取产品列表成功")
}
// GetProductDetail 获取产品详情
// @Summary 获取产品详情
// @Description 根据产品ID获取产品详细信息
// @Tags 数据大厅
// @Accept json
// @Produce json
// @Param id path string true "产品ID"
// @Success 200 {object} responses.ProductInfoResponse "获取产品详情成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 404 {object} map[string]interface{} "产品不存在"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/products/{id} [get]
func (h *ProductHandler) GetProductDetail(c *gin.Context) {
var query queries.GetProductQuery
query.ID = c.Param("id")
if query.ID == "" {
h.responseBuilder.BadRequest(c, "产品ID不能为空")
return
}
result, err := h.appService.GetProductByID(c.Request.Context(), &query)
if err != nil {
h.logger.Error("获取产品详情失败", zap.Error(err), zap.String("product_id", query.ID))
h.responseBuilder.NotFound(c, "产品不存在")
return
}
h.responseBuilder.Success(c, result, "获取产品详情成功")
}
// SubscribeProduct 订阅产品
// @Summary 订阅产品
// @Description 用户订阅指定产品
// @Tags 数据大厅
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path string true "产品ID"
// @Param request body commands.CreateSubscriptionCommand true "订阅请求"
// @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 == "" {
h.responseBuilder.Unauthorized(c, "用户未认证")
return
}
productID := c.Param("id")
if productID == "" {
h.responseBuilder.BadRequest(c, "产品ID不能为空")
return
}
var cmd commands.CreateSubscriptionCommand
if err := c.ShouldBindJSON(&cmd); err != nil {
h.responseBuilder.BadRequest(c, "请求参数错误")
return
}
// 设置用户ID和产品ID
cmd.UserID = userID
cmd.ProductID = productID
// 设置默认值
if cmd.APILimit <= 0 {
cmd.APILimit = 1000 // 默认API调用限制
}
if cmd.Duration == "" {
cmd.Duration = "30d" // 默认订阅30天
}
if err := h.subAppService.CreateSubscription(c.Request.Context(), &cmd); err != nil {
h.logger.Error("订阅产品失败", zap.Error(err), zap.String("user_id", userID), zap.String("product_id", productID))
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, "获取产品统计成功")
}
// ================ 分类相关方法 ================
// ListCategories 获取分类列表
// @Summary 获取分类列表
// @Description 获取产品分类列表,支持层级筛选
// @Tags 数据大厅
// @Accept json
// @Produce json
// @Param parent_id query string false "父级分类ID"
// @Param level query int false "分类层级"
// @Success 200 {object} responses.CategoryListResponse "获取分类列表成功"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/categories [get]
func (h *ProductHandler) ListCategories(c *gin.Context) {
// 解析查询参数
parentID := c.Query("parent_id")
levelStr := c.Query("level")
// 构建查询命令
query := &queries.ListCategoriesQuery{
Page: 1,
PageSize: 100,
SortBy: "sort_order",
SortOrder: "asc",
}
// 设置父级分类ID
if parentID != "" {
query.ParentID = &parentID
}
// 设置分类层级
if levelStr != "" {
if level, err := strconv.Atoi(levelStr); err == nil {
query.Level = &level
}
}
// 调用应用服务
categories, err := h.categoryService.ListCategories(c.Request.Context(), query)
if err != nil {
h.logger.Error("获取分类列表失败", zap.Error(err))
h.responseBuilder.InternalError(c, "获取分类列表失败")
return
}
// 返回结果
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
}
// 构建查询命令
query := &queries.GetCategoryQuery{
ID: categoryID,
}
// 调用应用服务
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
}
// 返回结果
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)
// @Param status query string false "订阅状态"
// @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 == "" {
h.responseBuilder.Unauthorized(c, "用户未认证")
return
}
var query queries.ListSubscriptionsQuery
if err := c.ShouldBindQuery(&query); err != nil {
h.responseBuilder.BadRequest(c, "请求参数错误")
return
}
// 设置默认值
if query.Page <= 0 {
query.Page = 1
}
if query.PageSize <= 0 {
query.PageSize = 10
}
if query.PageSize > 100 {
query.PageSize = 100
}
// 设置用户ID
query.UserID = userID
result, err := h.subAppService.ListSubscriptions(c.Request.Context(), &query)
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 == "" {
h.responseBuilder.Unauthorized(c, "用户未认证")
return
}
result, err := h.subAppService.GetSubscriptionStats(c.Request.Context())
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 == "" {
h.responseBuilder.Unauthorized(c, "用户未认证")
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
}
h.responseBuilder.Success(c, result, "获取我的订阅详情成功")
}
// GetMySubscriptionUsage 获取我的订阅使用情况
// @Summary 获取我的订阅使用情况
// @Description 获取指定订阅的使用情况统计
// @Tags 我的订阅
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path string true "订阅ID"
// @Success 200 {object} responses.SubscriptionUsageResponse "获取使用情况成功"
// @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) {
userID := c.GetString("user_id")
if userID == "" {
h.responseBuilder.Unauthorized(c, "用户未认证")
return
}
subscriptionID := c.Param("id")
if subscriptionID == "" {
h.responseBuilder.BadRequest(c, "订阅ID不能为空")
return
}
result, err := h.subAppService.GetSubscriptionUsage(c.Request.Context(), subscriptionID)
if err != nil {
h.logger.Error("获取我的订阅使用情况失败", zap.Error(err), zap.String("user_id", userID), zap.String("subscription_id", subscriptionID))
h.responseBuilder.NotFound(c, "订阅不存在")
return
}
h.responseBuilder.Success(c, result, "获取我的订阅使用情况成功")
}

View File

@@ -213,6 +213,33 @@ func (h *UserHandler) ChangePassword(c *gin.Context) {
h.response.Success(c, nil, "密码修改成功")
}
// ResetPassword 重置密码
// @Summary 重置密码
// @Description 使用手机号、验证码和新密码重置用户密码(忘记密码时使用)
// @Tags 用户认证
// @Accept json
// @Produce json
// @Param request body commands.ResetPasswordCommand true "重置密码请求"
// @Success 200 {object} map[string]interface{} "密码重置成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误或验证码无效"
// @Failure 404 {object} map[string]interface{} "用户不存在"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/users/reset-password [post]
func (h *UserHandler) ResetPassword(c *gin.Context) {
var cmd commands.ResetPasswordCommand
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
return
}
if err := h.appService.ResetPassword(c.Request.Context(), &cmd); err != nil {
h.logger.Error("重置密码失败", zap.Error(err))
h.response.BadRequest(c, err.Error())
return
}
h.response.Success(c, nil, "密码重置成功")
}
// getCurrentUserID 获取当前用户ID
func (h *UserHandler) getCurrentUserID(c *gin.Context) string {
if userID, exists := c.Get("user_id"); exists {

View File

@@ -0,0 +1,81 @@
package routes
import (
"tyapi-server/internal/infrastructure/http/handlers"
sharedhttp "tyapi-server/internal/shared/http"
"tyapi-server/internal/shared/middleware"
"go.uber.org/zap"
)
// ProductRoutes 产品路由
type ProductRoutes struct {
productHandler *handlers.ProductHandler
auth *middleware.JWTAuthMiddleware
logger *zap.Logger
}
// NewProductRoutes 创建产品路由
func NewProductRoutes(
productHandler *handlers.ProductHandler,
auth *middleware.JWTAuthMiddleware,
logger *zap.Logger,
) *ProductRoutes {
return &ProductRoutes{
productHandler: productHandler,
auth: auth,
logger: logger,
}
}
// Register 注册产品相关路由
func (r *ProductRoutes) Register(router *sharedhttp.GinRouter) {
engine := router.GetEngine()
// 数据大厅 - 公开接口
products := engine.Group("/api/v1/products")
{
// 获取产品列表(分页+筛选)
products.GET("", r.productHandler.ListProducts)
// 获取产品详情
products.GET("/:id", r.productHandler.GetProductDetail)
// 获取产品统计
products.GET("/stats", r.productHandler.GetProductStats)
// 订阅产品(需要认证)
products.POST("/:id/subscribe", r.auth.Handle(), r.productHandler.SubscribeProduct)
}
// 分类 - 公开接口
categories := engine.Group("/api/v1/categories")
{
// 获取分类列表
categories.GET("", r.productHandler.ListCategories)
// 获取分类详情
categories.GET("/:id", r.productHandler.GetCategoryDetail)
}
// 我的订阅 - 需要认证
my := engine.Group("/api/v1/my", r.auth.Handle())
{
subscriptions := my.Group("/subscriptions")
{
// 获取我的订阅列表
subscriptions.GET("", r.productHandler.ListMySubscriptions)
// 获取我的订阅统计
subscriptions.GET("/stats", r.productHandler.GetMySubscriptionStats)
// 获取订阅详情
subscriptions.GET("/:id", r.productHandler.GetMySubscriptionDetail)
// 获取订阅使用情况
subscriptions.GET("/:id/usage", r.productHandler.GetMySubscriptionUsage)
}
}
r.logger.Info("产品路由注册完成")
}

View File

@@ -39,6 +39,7 @@ func (r *UserRoutes) Register(router *sharedhttp.GinRouter) {
usersGroup.POST("/register", r.handler.Register) // 用户注册
usersGroup.POST("/login-password", r.handler.LoginWithPassword) // 密码登录
usersGroup.POST("/login-sms", r.handler.LoginWithSMS) // 短信验证码登录
usersGroup.POST("/reset-password", r.handler.ResetPassword) // 重置密码
// 需要认证的路由
authenticated := usersGroup.Group("")