temp
This commit is contained in:
953
docs/DDD代码生成器可视化界面策划方案.md
Normal file
953
docs/DDD代码生成器可视化界面策划方案.md
Normal file
@@ -0,0 +1,953 @@
|
||||
# DDD代码生成器可视化界面策划方案
|
||||
|
||||
## 概述
|
||||
|
||||
DDD代码生成器可视化界面是一个基于Web的图形化工具,提供直观的拖拽式界面来设计和生成DDD架构代码。通过可视化界面,用户可以更轻松地设计领域模型、配置实体关系、预览生成的代码,大幅降低DDD架构的学习和使用门槛。
|
||||
|
||||
## 界面架构
|
||||
|
||||
### 1. 整体布局
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ DDD代码生成器 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 导航栏: [项目] [领域] [实体] [服务] [API] [配置] [生成] │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 工具栏: [新建] [打开] [保存] [导入] [导出] [预览] [生成] │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 左侧面板 │ 主工作区 │ 右侧面板 │
|
||||
│ ├─ 项目树 │ ├─ 可视化设计器 │ ├─ 属性面板 │
|
||||
│ ├─ 组件库 │ ├─ 关系图 │ ├─ 代码预览 │
|
||||
│ └─ 模板库 │ └─ 配置表单 │ └─ 日志面板 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2. 核心界面组件
|
||||
|
||||
#### 2.1 导航栏
|
||||
- **项目管理**: 项目列表、新建项目、项目设置
|
||||
- **领域设计**: 领域列表、领域配置、领域关系
|
||||
- **实体管理**: 实体设计器、字段配置、业务方法
|
||||
- **服务配置**: 应用服务、领域服务、仓储配置
|
||||
- **API设计**: 接口设计、路由配置、响应格式
|
||||
- **代码生成**: 生成配置、预览、导出
|
||||
|
||||
#### 2.2 左侧面板
|
||||
- **项目树**: 显示项目结构和文件组织
|
||||
- **组件库**: 拖拽式组件(实体、枚举、服务、API)
|
||||
- **模板库**: 预定义模板和自定义模板
|
||||
|
||||
#### 2.3 主工作区
|
||||
- **可视化设计器**: 拖拽式界面设计
|
||||
- **关系图**: 实体间关系可视化
|
||||
- **配置表单**: 详细的配置选项
|
||||
|
||||
#### 2.4 右侧面板
|
||||
- **属性面板**: 选中元素的属性编辑
|
||||
- **代码预览**: 实时预览生成的代码
|
||||
- **日志面板**: 操作日志和错误信息
|
||||
|
||||
## 功能模块
|
||||
|
||||
### 1. 项目管理系统
|
||||
|
||||
#### 1.1 项目创建向导
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 新建项目向导 │
|
||||
├─────────────────────────────────────────┤
|
||||
│ 步骤 1: 项目基本信息 │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ 项目名称: [tyapi-server ] │ │
|
||||
│ │ 模块路径: [tyapi-server/internal] │ │
|
||||
│ │ 作者: [开发团队 ] │ │
|
||||
│ │ 版本: [1.0.0 ] │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 步骤 2: 项目结构选择 │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ ☑ 标准DDD结构 │ │
|
||||
│ │ ☑ 包含认证模块 │ │
|
||||
│ │ ☑ 包含日志系统 │ │
|
||||
│ │ ☑ 包含测试框架 │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 步骤 3: 模板选择 │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ ☑ 中文注释模板 │ │
|
||||
│ │ ☑ RESTful API模板 │ │
|
||||
│ │ ☑ Swagger文档模板 │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [上一步] [下一步] [完成] │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 1.2 项目仪表板
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 项目仪表板 │
|
||||
├─────────────────────────────────────────┤
|
||||
│ 项目概览 │
|
||||
│ ┌─────────────┬─────────────┬─────────┐ │
|
||||
│ │ 领域数量 │ 实体数量 │ API数量 │ │
|
||||
│ │ 3 │ 12 │ 24 │ │
|
||||
│ └─────────────┴─────────────┴─────────┘ │
|
||||
│ │
|
||||
│ 最近活动 │
|
||||
│ • 2024-01-15 14:30 创建用户域 │
|
||||
│ • 2024-01-15 14:25 添加产品实体 │
|
||||
│ • 2024-01-15 14:20 生成订单API │
|
||||
│ │
|
||||
│ 快速操作 │
|
||||
│ [新建领域] [导入配置] [生成代码] [导出] │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2. 领域设计器
|
||||
|
||||
#### 2.1 领域概览
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 领域设计器 │
|
||||
├─────────────────────────────────────────┤
|
||||
│ 领域列表 │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ 📁 用户域 (user) │ │
|
||||
│ │ ├─ 👤 用户实体 (3个字段) │ │
|
||||
│ │ ├─ 🔐 角色实体 (2个字段) │ │
|
||||
│ │ └─ 🛡️ 权限实体 (2个字段) │ │
|
||||
│ │ │ │
|
||||
│ │ 📁 产品域 (product) │ │
|
||||
│ │ ├─ 📦 产品实体 (5个字段) │ │
|
||||
│ │ ├─ 🏷️ 分类实体 (3个字段) │ │
|
||||
│ │ └─ 📄 文档实体 (4个字段) │ │
|
||||
│ │ │ │
|
||||
│ │ 📁 订单域 (order) │ │
|
||||
│ │ ├─ 🛒 订单实体 (6个字段) │ │
|
||||
│ │ └─ 📋 订单项实体 (4个字段) │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [新建领域] [编辑领域] [删除领域] │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 2.2 领域关系图
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 领域关系图 │
|
||||
├─────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────┐ ┌─────────┐ │
|
||||
│ │ 用户域 │ │ 产品域 │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ ┌─────┐ │ │ ┌─────┐ │ │
|
||||
│ │ │用户 │ │ │ │产品 │ │ │
|
||||
│ │ └─────┘ │ │ └─────┘ │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ ┌─────┐ │ │ ┌─────┐ │ │
|
||||
│ │ │角色 │ │ │ │分类 │ │ │
|
||||
│ │ └─────┘ │ │ └─────┘ │ │
|
||||
│ └─────────┘ └─────────┘ │
|
||||
│ │ │ │
|
||||
│ └────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────┐ │
|
||||
│ │ 订单域 │ │
|
||||
│ │ │ │
|
||||
│ │ ┌─────┐ │ │
|
||||
│ │ │订单 │ │ │
|
||||
│ │ └─────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ ┌─────┐ │ │
|
||||
│ │ │订单项│ │ │
|
||||
│ │ └─────┘ │ │
|
||||
│ └─────────┘ │
|
||||
│ │
|
||||
│ [添加关系] [编辑关系] [删除关系] │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3. 实体设计器
|
||||
|
||||
#### 3.1 实体可视化设计
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 实体设计器 │
|
||||
├─────────────────────────────────────────┤
|
||||
│ 实体: 用户 (User) │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ ┌─────────────────────────────────┐ │ │
|
||||
│ │ │ 用户实体 │ │ │
|
||||
│ │ ├─────────────────────────────────┤ │ │
|
||||
│ │ │ ID string (主键) │ │ │
|
||||
│ │ │ Username string (唯一) │ │ │
|
||||
│ │ │ Email string (唯一) │ │ │
|
||||
│ │ │ Password string (密码) │ │ │
|
||||
│ │ │ Status UserStatus (状态) │ │ │
|
||||
│ │ │ CreatedAt time.Time (创建时间) │ │ │
|
||||
│ │ │ UpdatedAt time.Time (更新时间) │ │ │
|
||||
│ │ │ DeletedAt gorm.DeletedAt │ │ │
|
||||
│ │ └─────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 业务方法 │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ ☑ IsActive() bool │ │
|
||||
│ │ ☑ CanLogin() bool │ │
|
||||
│ │ ☑ IsDeleted() bool │ │
|
||||
│ │ ☑ Enable() │ │
|
||||
│ │ ☑ Disable() │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [添加字段] [编辑字段] [删除字段] │
|
||||
│ [添加方法] [编辑方法] [删除方法] │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 3.2 字段配置面板
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 字段配置 │
|
||||
├─────────────────────────────────────────┤
|
||||
│ 字段名称: [Username ] │
|
||||
│ 字段类型: [string ▼] │
|
||||
│ 字段标签: [gorm:"type:varchar(50);uniqueIndex;not null"] │
|
||||
│ 字段注释: [用户名] │
|
||||
│ │
|
||||
│ 验证规则 │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ ☑ required (必填) │ │
|
||||
│ │ ☑ min=3 (最小长度3) │ │
|
||||
│ │ ☑ max=50 (最大长度50) │ │
|
||||
│ │ ☑ unique (唯一性) │ │
|
||||
│ │ ☑ pattern=^[a-zA-Z0-9_]+$ (正则) │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 数据库配置 │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ 类型: varchar(50) │ │
|
||||
│ │ 索引: ☑ 普通索引 ☑ 唯一索引 │ │
|
||||
│ │ 约束: ☑ NOT NULL ☑ 默认值 │ │
|
||||
│ │ 默认值: [ ] │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [保存] [取消] [预览代码] │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4. 服务配置器
|
||||
|
||||
#### 4.1 应用服务设计
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 应用服务配置 │
|
||||
├─────────────────────────────────────────┤
|
||||
│ 服务: 用户应用服务 (UserApplicationService) │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ 基础CRUD操作 │ │
|
||||
│ │ ☑ CreateUser (创建用户) │ │
|
||||
│ │ ☑ UpdateUser (更新用户) │ │
|
||||
│ │ ☑ DeleteUser (删除用户) │ │
|
||||
│ │ ☑ GetUserByID (获取用户) │ │
|
||||
│ │ ☑ ListUsers (用户列表) │ │
|
||||
│ │ │ │
|
||||
│ │ 业务操作 │ │
|
||||
│ │ ☑ EnableUser (启用用户) │ │
|
||||
│ │ ☑ DisableUser (禁用用户) │ │
|
||||
│ │ ☑ ResetPassword (重置密码) │ │
|
||||
│ │ ☑ ChangeStatus (修改状态) │ │
|
||||
│ │ │ │
|
||||
│ │ 统计功能 │ │
|
||||
│ │ ☑ GetUserStats (用户统计) │ │
|
||||
│ │ ☑ GetUserCount (用户数量) │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ DTO配置 │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ 命令DTO: ☑ CreateUserCommand │ │
|
||||
│ │ 查询DTO: ☑ GetUserQuery │ │
|
||||
│ │ 响应DTO: ☑ UserInfoResponse │ │
|
||||
│ │ 列表DTO: ☑ UserListResponse │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [添加方法] [编辑方法] [删除方法] │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 5. API设计器
|
||||
|
||||
#### 5.1 接口设计
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ API接口设计 │
|
||||
├─────────────────────────────────────────┤
|
||||
│ 接口列表 │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ GET /api/v1/users │ │
|
||||
│ │ POST /api/v1/users │ │
|
||||
│ │ GET /api/v1/users/:id │ │
|
||||
│ │ PUT /api/v1/users/:id │ │
|
||||
│ │ DELETE /api/v1/users/:id │ │
|
||||
│ │ POST /api/v1/users/:id/enable │ │
|
||||
│ │ POST /api/v1/users/:id/disable │ │
|
||||
│ │ GET /api/v1/users/stats │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 接口详情 │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ 路径: /api/v1/users │ │
|
||||
│ │ 方法: GET │ │
|
||||
│ │ 处理器: ListUsers │ │
|
||||
│ │ 描述: 获取用户列表 │ │
|
||||
│ │ │ │
|
||||
│ │ 查询参数: │ │
|
||||
│ │ • page (int) - 页码 │ │
|
||||
│ │ • page_size (int) - 每页数量 │ │
|
||||
│ │ • status (string) - 状态筛选 │ │
|
||||
│ │ • sort_by (string) - 排序字段 │ │
|
||||
│ │ │ │
|
||||
│ │ 响应格式: │ │
|
||||
│ │ • 200: UserListResponse │ │
|
||||
│ │ • 400: ErrorResponse │ │
|
||||
│ │ • 500: ErrorResponse │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [添加接口] [编辑接口] [删除接口] │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 6. 代码预览器
|
||||
|
||||
#### 6.1 实时代码预览
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 代码预览 │
|
||||
├─────────────────────────────────────────┤
|
||||
│ 文件类型: [实体 ▼] [仓储] [服务] [API] │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ // 用户实体 │ │
|
||||
│ │ package entities │ │
|
||||
│ │ │ │
|
||||
│ │ import ( │ │
|
||||
│ │ "time" │ │
|
||||
│ │ "gorm.io/gorm" │ │
|
||||
│ │ ) │ │
|
||||
│ │ │ │
|
||||
│ │ // User 用户实体 │ │
|
||||
│ │ type User struct { │ │
|
||||
│ │ ID string `gorm:"primaryKey;type:varchar(36)" comment:"用户ID"` │
|
||||
│ │ Username string `gorm:"type:varchar(50);uniqueIndex;not null" comment:"用户名"` │
|
||||
│ │ Email string `gorm:"type:varchar(100);uniqueIndex;not null" comment:"邮箱"` │
|
||||
│ │ Password string `gorm:"type:varchar(255);not null" comment:"密码"` │
|
||||
│ │ Status UserStatus `gorm:"type:varchar(20);default:'active'" comment:"用户状态"` │
|
||||
│ │ CreatedAt time.Time `gorm:"autoCreateTime" comment:"创建时间"` │
|
||||
│ │ UpdatedAt time.Time `gorm:"autoUpdateTime" comment:"更新时间"` │
|
||||
│ │ DeletedAt gorm.DeletedAt `gorm:"index" comment:"软删除时间"` │
|
||||
│ │ } │ │
|
||||
│ │ │ │
|
||||
│ │ // 业务方法 │ │
|
||||
│ │ func (u *User) IsActive() bool { │ │
|
||||
│ │ return u.Status == UserStatusActive │
|
||||
│ │ } │ │
|
||||
│ │ │ │
|
||||
│ │ func (u *User) CanLogin() bool { │ │
|
||||
│ │ return u.IsActive() && !u.IsDeleted() │
|
||||
│ │ } │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [复制代码] [下载文件] [格式化] [语法检查] │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 7. 生成配置器
|
||||
|
||||
#### 7.1 生成选项配置
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 生成配置 │
|
||||
├─────────────────────────────────────────┤
|
||||
│ 生成选项 │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ ☑ 生成实体文件 │ │
|
||||
│ │ ☑ 生成仓储接口和实现 │ │
|
||||
│ │ ☑ 生成应用服务 │ │
|
||||
│ │ ☑ 生成DTO对象 │ │
|
||||
│ │ ☑ 生成HTTP处理器 │ │
|
||||
│ │ ☑ 生成路由配置 │ │
|
||||
│ │ ☑ 生成单元测试 │ │
|
||||
│ │ ☑ 生成集成测试 │ │
|
||||
│ │ ☑ 生成API文档 │ │
|
||||
│ │ ☑ 更新依赖注入配置 │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 输出配置 │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ 输出目录: [./internal ] │ │
|
||||
│ │ 备份文件: ☑ 是 ☐ 否 │ │
|
||||
│ │ 格式化代码: ☑ 是 ☐ 否 │ │
|
||||
│ │ 运行测试: ☑ 是 ☐ 否 │ │
|
||||
│ │ 语法检查: ☑ 是 ☐ 否 │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 模板配置 │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ 语言: [中文 ▼] [英文] │ │
|
||||
│ │ 风格: [标准 ▼] [简洁] [详细] │ │
|
||||
│ │ 注释风格: [中文 ▼] [英文] [混合] │ │
|
||||
│ │ 错误处理: [统一 ▼] [详细] [简洁] │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [生成代码] [预览生成] [保存配置] │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 技术实现
|
||||
|
||||
### 1. 前端技术栈
|
||||
|
||||
#### 1.1 核心框架
|
||||
```javascript
|
||||
// 技术栈选择
|
||||
- React 18 + TypeScript
|
||||
- Vite (构建工具)
|
||||
- Tailwind CSS (样式框架)
|
||||
- Zustand (状态管理)
|
||||
- React Router (路由管理)
|
||||
```
|
||||
|
||||
#### 1.2 UI组件库
|
||||
```javascript
|
||||
// 组件库
|
||||
- Ant Design / Element Plus
|
||||
- React Flow (流程图)
|
||||
- Monaco Editor (代码编辑器)
|
||||
- React DnD (拖拽功能)
|
||||
- React Hook Form (表单管理)
|
||||
```
|
||||
|
||||
#### 1.3 可视化库
|
||||
```javascript
|
||||
// 可视化组件
|
||||
- D3.js (数据可视化)
|
||||
- React Flow (流程图)
|
||||
- Vis.js (关系图)
|
||||
- Chart.js (图表)
|
||||
```
|
||||
|
||||
### 2. 后端技术栈
|
||||
|
||||
#### 2.1 API服务
|
||||
```go
|
||||
// 后端技术栈
|
||||
- Gin (Web框架)
|
||||
- GORM (数据库ORM)
|
||||
- JWT (认证)
|
||||
- Swagger (API文档)
|
||||
- WebSocket (实时通信)
|
||||
```
|
||||
|
||||
#### 2.2 代码生成引擎
|
||||
```go
|
||||
// 生成引擎
|
||||
- Go Template (模板引擎)
|
||||
- AST (代码分析)
|
||||
- File System (文件操作)
|
||||
- Git (版本控制)
|
||||
```
|
||||
|
||||
### 3. 数据存储
|
||||
|
||||
#### 3.1 数据库设计
|
||||
```sql
|
||||
-- 项目表
|
||||
CREATE TABLE projects (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
module_path VARCHAR(200) NOT NULL,
|
||||
author VARCHAR(100),
|
||||
version VARCHAR(20),
|
||||
config JSON,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 领域表
|
||||
CREATE TABLE domains (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
project_id VARCHAR(36) NOT NULL,
|
||||
name VARCHAR(50) NOT NULL,
|
||||
description TEXT,
|
||||
config JSON,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (project_id) REFERENCES projects(id)
|
||||
);
|
||||
|
||||
-- 实体表
|
||||
CREATE TABLE entities (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
domain_id VARCHAR(36) NOT NULL,
|
||||
name VARCHAR(50) NOT NULL,
|
||||
description TEXT,
|
||||
fields JSON,
|
||||
methods JSON,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (domain_id) REFERENCES domains(id)
|
||||
);
|
||||
|
||||
-- 服务表
|
||||
CREATE TABLE services (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
domain_id VARCHAR(36) NOT NULL,
|
||||
name VARCHAR(50) NOT NULL,
|
||||
type ENUM('application', 'domain', 'infrastructure'),
|
||||
methods JSON,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (domain_id) REFERENCES domains(id)
|
||||
);
|
||||
|
||||
-- API表
|
||||
CREATE TABLE apis (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
domain_id VARCHAR(36) NOT NULL,
|
||||
path VARCHAR(200) NOT NULL,
|
||||
method VARCHAR(10) NOT NULL,
|
||||
handler VARCHAR(100),
|
||||
summary TEXT,
|
||||
config JSON,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (domain_id) REFERENCES domains(id)
|
||||
);
|
||||
```
|
||||
|
||||
## 部署方案
|
||||
|
||||
### 1. 开发环境
|
||||
|
||||
#### 1.1 本地开发
|
||||
```bash
|
||||
# 前端开发
|
||||
cd ddd-gen-ui
|
||||
npm install
|
||||
npm run dev
|
||||
|
||||
# 后端开发
|
||||
cd ddd-gen-server
|
||||
go mod tidy
|
||||
go run main.go
|
||||
|
||||
# 数据库
|
||||
docker-compose up -d postgres redis
|
||||
```
|
||||
|
||||
#### 1.2 Docker开发
|
||||
```yaml
|
||||
# docker-compose.dev.yml
|
||||
version: '3.8'
|
||||
services:
|
||||
frontend:
|
||||
build: ./ddd-gen-ui
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- ./ddd-gen-ui:/app
|
||||
- /app/node_modules
|
||||
environment:
|
||||
- REACT_APP_API_URL=http://localhost:8080
|
||||
|
||||
backend:
|
||||
build: ./ddd-gen-server
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./ddd-gen-server:/app
|
||||
environment:
|
||||
- DB_HOST=postgres
|
||||
- REDIS_HOST=redis
|
||||
|
||||
postgres:
|
||||
image: postgres:15
|
||||
environment:
|
||||
- POSTGRES_DB=ddd_gen
|
||||
- POSTGRES_USER=ddd_gen
|
||||
- POSTGRES_PASSWORD=ddd_gen
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
```
|
||||
|
||||
### 2. 生产环境
|
||||
|
||||
#### 2.1 容器化部署
|
||||
```yaml
|
||||
# docker-compose.prod.yml
|
||||
version: '3.8'
|
||||
services:
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf
|
||||
- ./ssl:/etc/nginx/ssl
|
||||
depends_on:
|
||||
- frontend
|
||||
- backend
|
||||
|
||||
frontend:
|
||||
build: ./ddd-gen-ui
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- REACT_APP_API_URL=https://api.ddd-gen.com
|
||||
|
||||
backend:
|
||||
build: ./ddd-gen-server
|
||||
environment:
|
||||
- GIN_MODE=release
|
||||
- DB_HOST=postgres
|
||||
- REDIS_HOST=redis
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
|
||||
postgres:
|
||||
image: postgres:15
|
||||
environment:
|
||||
- POSTGRES_DB=ddd_gen
|
||||
- POSTGRES_USER=ddd_gen
|
||||
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
restart: unless-stopped
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
```
|
||||
|
||||
#### 2.2 Kubernetes部署
|
||||
```yaml
|
||||
# k8s-deployment.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: ddd-gen-frontend
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: ddd-gen-frontend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: ddd-gen-frontend
|
||||
spec:
|
||||
containers:
|
||||
- name: frontend
|
||||
image: ddd-gen/frontend:latest
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
resources:
|
||||
requests:
|
||||
memory: "128Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "256Mi"
|
||||
cpu: "200m"
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: ddd-gen-frontend-service
|
||||
spec:
|
||||
selector:
|
||||
app: ddd-gen-frontend
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 3000
|
||||
type: LoadBalancer
|
||||
```
|
||||
|
||||
## 用户体验设计
|
||||
|
||||
### 1. 交互设计
|
||||
|
||||
#### 1.1 拖拽操作
|
||||
- **实体拖拽**: 从组件库拖拽实体到设计器
|
||||
- **字段拖拽**: 拖拽字段到实体中
|
||||
- **关系拖拽**: 拖拽连线建立实体关系
|
||||
- **方法拖拽**: 拖拽方法到实体或服务中
|
||||
|
||||
#### 1.2 快捷键支持
|
||||
```javascript
|
||||
// 快捷键配置
|
||||
const shortcuts = {
|
||||
'Ctrl+N': '新建项目',
|
||||
'Ctrl+O': '打开项目',
|
||||
'Ctrl+S': '保存项目',
|
||||
'Ctrl+Shift+S': '另存为',
|
||||
'Ctrl+Z': '撤销',
|
||||
'Ctrl+Y': '重做',
|
||||
'Ctrl+D': '复制',
|
||||
'Ctrl+X': '剪切',
|
||||
'Ctrl+V': '粘贴',
|
||||
'Delete': '删除',
|
||||
'F5': '刷新',
|
||||
'Ctrl+P': '预览',
|
||||
'Ctrl+G': '生成代码'
|
||||
};
|
||||
```
|
||||
|
||||
#### 1.3 智能提示
|
||||
- **字段类型提示**: 根据字段名自动推荐类型
|
||||
- **验证规则提示**: 根据字段类型推荐验证规则
|
||||
- **方法名提示**: 根据实体名推荐方法名
|
||||
- **API路径提示**: 根据RESTful规范推荐路径
|
||||
|
||||
### 2. 响应式设计
|
||||
|
||||
#### 2.1 多设备支持
|
||||
```css
|
||||
/* 响应式断点 */
|
||||
@media (max-width: 768px) {
|
||||
/* 移动端布局 */
|
||||
.sidebar { display: none; }
|
||||
.main-content { width: 100%; }
|
||||
.toolbar { flex-direction: column; }
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
/* 平板端布局 */
|
||||
.sidebar { width: 200px; }
|
||||
.main-content { width: calc(100% - 200px); }
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
/* 桌面端布局 */
|
||||
.sidebar { width: 250px; }
|
||||
.main-content { width: calc(100% - 250px); }
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2 主题支持
|
||||
```javascript
|
||||
// 主题配置
|
||||
const themes = {
|
||||
light: {
|
||||
primary: '#1890ff',
|
||||
background: '#ffffff',
|
||||
text: '#000000',
|
||||
border: '#d9d9d9'
|
||||
},
|
||||
dark: {
|
||||
primary: '#177ddc',
|
||||
background: '#141414',
|
||||
text: '#ffffff',
|
||||
border: '#434343'
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## 扩展功能
|
||||
|
||||
### 1. 插件系统
|
||||
|
||||
#### 1.1 插件接口
|
||||
```typescript
|
||||
// 插件接口定义
|
||||
interface Plugin {
|
||||
name: string;
|
||||
version: string;
|
||||
description: string;
|
||||
author: string;
|
||||
icon: string;
|
||||
|
||||
// 生命周期钩子
|
||||
onInstall(): void;
|
||||
onUninstall(): void;
|
||||
onActivate(): void;
|
||||
onDeactivate(): void;
|
||||
|
||||
// 功能钩子
|
||||
onEntityCreate?(entity: Entity): void;
|
||||
onEntityUpdate?(entity: Entity): void;
|
||||
onCodeGenerate?(config: GenerateConfig): void;
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2 内置插件
|
||||
```javascript
|
||||
// 内置插件列表
|
||||
const builtinPlugins = [
|
||||
{
|
||||
name: 'linter',
|
||||
description: '代码规范检查',
|
||||
icon: '🔍'
|
||||
},
|
||||
{
|
||||
name: 'test-generator',
|
||||
description: '测试代码生成',
|
||||
icon: '🧪'
|
||||
},
|
||||
{
|
||||
name: 'docs-generator',
|
||||
description: '文档生成',
|
||||
icon: '📚'
|
||||
},
|
||||
{
|
||||
name: 'migration-generator',
|
||||
description: '数据库迁移生成',
|
||||
icon: '🗄️'
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
### 2. 协作功能
|
||||
|
||||
#### 2.1 实时协作
|
||||
```javascript
|
||||
// WebSocket实时通信
|
||||
const socket = new WebSocket('ws://localhost:8080/ws');
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
switch (data.type) {
|
||||
case 'entity_updated':
|
||||
updateEntity(data.entity);
|
||||
break;
|
||||
case 'user_joined':
|
||||
addCollaborator(data.user);
|
||||
break;
|
||||
case 'user_left':
|
||||
removeCollaborator(data.user);
|
||||
break;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### 2.2 版本控制
|
||||
```javascript
|
||||
// Git集成
|
||||
class GitManager {
|
||||
async init() {
|
||||
// 初始化Git仓库
|
||||
}
|
||||
|
||||
async commit(message) {
|
||||
// 提交更改
|
||||
}
|
||||
|
||||
async push() {
|
||||
// 推送到远程
|
||||
}
|
||||
|
||||
async pull() {
|
||||
// 拉取更新
|
||||
}
|
||||
|
||||
async branch(name) {
|
||||
// 创建分支
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 导入导出
|
||||
|
||||
#### 3.1 格式支持
|
||||
```javascript
|
||||
// 支持的导入格式
|
||||
const importFormats = [
|
||||
'json', // JSON配置
|
||||
'yaml', // YAML配置
|
||||
'xml', // XML配置
|
||||
'sql', // 数据库结构
|
||||
'openapi', // OpenAPI规范
|
||||
'swagger', // Swagger文档
|
||||
'plantuml', // PlantUML图
|
||||
'mermaid' // Mermaid图
|
||||
];
|
||||
|
||||
// 支持的导出格式
|
||||
const exportFormats = [
|
||||
'json', // JSON配置
|
||||
'yaml', // YAML配置
|
||||
'zip', // 完整项目
|
||||
'tar.gz', // 压缩包
|
||||
'docker', // Docker镜像
|
||||
'helm', // Helm Chart
|
||||
'terraform' // Terraform配置
|
||||
];
|
||||
```
|
||||
|
||||
## 开发计划
|
||||
|
||||
### 第一阶段:基础界面 (4周)
|
||||
- [ ] 项目管理系统
|
||||
- [ ] 基础UI框架
|
||||
- [ ] 实体设计器
|
||||
- [ ] 字段配置器
|
||||
|
||||
### 第二阶段:核心功能 (6周)
|
||||
- [ ] 领域设计器
|
||||
- [ ] 服务配置器
|
||||
- [ ] API设计器
|
||||
- [ ] 代码预览器
|
||||
|
||||
### 第三阶段:高级功能 (4周)
|
||||
- [ ] 可视化关系图
|
||||
- [ ] 拖拽操作
|
||||
- [ ] 实时协作
|
||||
- [ ] 插件系统
|
||||
|
||||
### 第四阶段:优化完善 (2周)
|
||||
- [ ] 性能优化
|
||||
- [ ] 用户体验
|
||||
- [ ] 文档完善
|
||||
- [ ] 测试覆盖
|
||||
|
||||
## 总结
|
||||
|
||||
这个可视化界面将为DDD代码生成器提供:
|
||||
|
||||
### 🎯 核心优势
|
||||
1. **直观易用**: 图形化界面,降低学习门槛
|
||||
2. **可视化设计**: 拖拽式操作,所见即所得
|
||||
3. **实时预览**: 代码实时预览,即时反馈
|
||||
4. **协作支持**: 多人实时协作,提升团队效率
|
||||
5. **扩展性强**: 插件系统,支持功能扩展
|
||||
|
||||
### 🚀 技术特色
|
||||
1. **现代化技术栈**: React + TypeScript + Go
|
||||
2. **响应式设计**: 支持多设备访问
|
||||
3. **实时通信**: WebSocket实时协作
|
||||
4. **版本控制**: Git集成
|
||||
5. **容器化部署**: Docker + Kubernetes
|
||||
|
||||
### 💡 创新点
|
||||
1. **可视化DDD设计**: 首次将DDD设计可视化
|
||||
2. **智能代码生成**: 基于配置的智能生成
|
||||
3. **实时协作**: 支持多人同时编辑
|
||||
4. **插件生态**: 可扩展的插件系统
|
||||
|
||||
这个可视化界面将大大提升DDD代码生成器的易用性和实用性,让DDD架构设计变得更加直观和高效!
|
||||
|
||||
你觉得这个可视化界面策划方案如何?需要我开始实现吗?
|
||||
653
docs/DDD代码生成器策划方案.md
Normal file
653
docs/DDD代码生成器策划方案.md
Normal file
@@ -0,0 +1,653 @@
|
||||
# DDD代码生成器策划方案
|
||||
|
||||
## 概述
|
||||
|
||||
DDD代码生成器是一个命令行工具,用于快速生成符合DDD架构模式的领域代码。支持生成完整的领域结构,包括实体、仓储、应用服务、DTO、HTTP处理器等,大幅提升开发效率。
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 1. 核心功能
|
||||
|
||||
#### 1.1 领域生成
|
||||
- **完整领域生成**: 一次性生成整个领域的所有代码文件
|
||||
- **实体生成**: 生成实体、枚举、业务方法
|
||||
- **仓储生成**: 生成仓储接口和GORM实现
|
||||
- **应用服务生成**: 生成应用服务接口和实现
|
||||
- **DTO生成**: 生成命令、查询、响应DTO
|
||||
- **HTTP层生成**: 生成处理器和路由
|
||||
- **依赖注入配置**: 自动更新容器配置
|
||||
|
||||
#### 1.2 增量功能
|
||||
- **新增实体**: 在现有领域中添加新实体
|
||||
- **新增服务**: 为现有实体添加新的应用服务
|
||||
- **新增API**: 为现有实体添加新的HTTP接口
|
||||
- **字段扩展**: 为现有实体添加新字段
|
||||
|
||||
#### 1.3 模板功能
|
||||
- **自定义模板**: 支持自定义代码模板
|
||||
- **模板变量**: 支持丰富的模板变量替换
|
||||
- **多语言支持**: 支持中文和英文模板
|
||||
|
||||
### 2. 高级功能
|
||||
|
||||
#### 2.1 智能分析
|
||||
- **依赖分析**: 自动分析实体间关系
|
||||
- **命名规范**: 自动生成符合规范的命名
|
||||
- **类型推断**: 根据字段名自动推断数据类型
|
||||
- **验证规则**: 自动生成字段验证规则
|
||||
|
||||
#### 2.2 代码质量
|
||||
- **Linter集成**: 生成后自动运行代码检查
|
||||
- **格式化**: 自动格式化生成的代码
|
||||
- **测试生成**: 自动生成单元测试和集成测试
|
||||
- **文档生成**: 自动生成API文档注释
|
||||
|
||||
#### 2.3 项目管理
|
||||
- **项目扫描**: 扫描现有项目结构
|
||||
- **配置管理**: 管理生成器配置
|
||||
- **历史记录**: 记录生成历史
|
||||
- **回滚功能**: 支持代码回滚
|
||||
|
||||
## 技术架构
|
||||
|
||||
### 1. 整体架构
|
||||
|
||||
```
|
||||
DDD Generator
|
||||
├── CLI Interface # 命令行接口
|
||||
├── Core Engine # 核心引擎
|
||||
├── Template Engine # 模板引擎
|
||||
├── Code Analyzer # 代码分析器
|
||||
├── File Manager # 文件管理器
|
||||
└── Validator # 验证器
|
||||
```
|
||||
|
||||
### 2. 核心组件
|
||||
|
||||
#### 2.1 CLI Interface
|
||||
- **命令解析**: 解析命令行参数
|
||||
- **交互式输入**: 提供友好的交互界面
|
||||
- **帮助系统**: 提供详细的帮助信息
|
||||
- **配置管理**: 管理用户配置
|
||||
|
||||
#### 2.2 Core Engine
|
||||
- **生成流程控制**: 控制代码生成流程
|
||||
- **依赖管理**: 管理组件间依赖关系
|
||||
- **错误处理**: 统一的错误处理机制
|
||||
- **日志系统**: 详细的日志记录
|
||||
|
||||
#### 2.3 Template Engine
|
||||
- **模板解析**: 解析Go模板语法
|
||||
- **变量替换**: 执行模板变量替换
|
||||
- **条件渲染**: 支持条件渲染逻辑
|
||||
- **循环渲染**: 支持循环渲染逻辑
|
||||
|
||||
#### 2.4 Code Analyzer
|
||||
- **AST解析**: 解析Go代码AST
|
||||
- **依赖分析**: 分析代码依赖关系
|
||||
- **结构分析**: 分析项目结构
|
||||
- **命名分析**: 分析命名规范
|
||||
|
||||
#### 2.5 File Manager
|
||||
- **文件操作**: 安全的文件读写操作
|
||||
- **目录管理**: 创建和管理目录结构
|
||||
- **备份恢复**: 文件备份和恢复
|
||||
- **权限管理**: 文件权限管理
|
||||
|
||||
#### 2.6 Validator
|
||||
- **语法验证**: 验证生成的代码语法
|
||||
- **规范检查**: 检查代码规范
|
||||
- **依赖验证**: 验证依赖关系
|
||||
- **冲突检测**: 检测文件冲突
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 1. 新项目初始化
|
||||
```bash
|
||||
# 生成完整的用户域
|
||||
ddd-gen domain user --entities user,role,permission --features auth,profile
|
||||
|
||||
# 生成产品域
|
||||
ddd-gen domain product --entities product,category,order --features catalog,order
|
||||
```
|
||||
|
||||
### 2. 现有项目扩展
|
||||
```bash
|
||||
# 在用户域中添加新实体
|
||||
ddd-gen entity user subscription --fields id,name,price,status
|
||||
|
||||
# 为产品添加新服务
|
||||
ddd-gen service product inventory --methods check,update,reserve
|
||||
|
||||
# 添加新的API接口
|
||||
ddd-gen api product search --methods search,filter,sort
|
||||
```
|
||||
|
||||
### 3. 批量操作
|
||||
```bash
|
||||
# 批量生成多个域
|
||||
ddd-gen batch --config domains.yaml
|
||||
|
||||
# 批量添加字段
|
||||
ddd-gen batch-fields --config fields.yaml
|
||||
```
|
||||
|
||||
## 配置文件
|
||||
|
||||
### 1. 项目配置 (ddd-config.yaml)
|
||||
```yaml
|
||||
project:
|
||||
name: "tyapi-server"
|
||||
module: "tyapi-server/internal"
|
||||
author: "开发团队"
|
||||
version: "1.0.0"
|
||||
|
||||
templates:
|
||||
path: "./templates"
|
||||
language: "zh"
|
||||
style: "standard"
|
||||
|
||||
output:
|
||||
path: "./internal"
|
||||
backup: true
|
||||
format: true
|
||||
test: true
|
||||
|
||||
validation:
|
||||
lint: true
|
||||
test: true
|
||||
docs: true
|
||||
```
|
||||
|
||||
### 2. 领域配置 (domain-config.yaml)
|
||||
```yaml
|
||||
domain:
|
||||
name: "user"
|
||||
description: "用户管理域"
|
||||
|
||||
entities:
|
||||
- name: "user"
|
||||
fields:
|
||||
- name: "id"
|
||||
type: "string"
|
||||
tag: "gorm:\"primaryKey;type:varchar(36)\""
|
||||
comment: "用户ID"
|
||||
- name: "username"
|
||||
type: "string"
|
||||
tag: "gorm:\"type:varchar(50);uniqueIndex;not null\""
|
||||
comment: "用户名"
|
||||
validation: "required,min=3,max=50"
|
||||
- name: "email"
|
||||
type: "string"
|
||||
tag: "gorm:\"type:varchar(100);uniqueIndex;not null\""
|
||||
comment: "邮箱"
|
||||
validation: "required,email"
|
||||
- name: "password"
|
||||
type: "string"
|
||||
tag: "gorm:\"type:varchar(255);not null\""
|
||||
comment: "密码"
|
||||
validation: "required,min=6"
|
||||
- name: "status"
|
||||
type: "UserStatus"
|
||||
tag: "gorm:\"type:varchar(20);default:'active'\""
|
||||
comment: "用户状态"
|
||||
methods:
|
||||
- name: "IsActive"
|
||||
return: "bool"
|
||||
body: "return u.Status == UserStatusActive"
|
||||
- name: "CanLogin"
|
||||
return: "bool"
|
||||
body: "return u.IsActive() && !u.IsDeleted()"
|
||||
|
||||
- name: "role"
|
||||
fields:
|
||||
- name: "id"
|
||||
type: "string"
|
||||
tag: "gorm:\"primaryKey;type:varchar(36)\""
|
||||
comment: "角色ID"
|
||||
- name: "name"
|
||||
type: "string"
|
||||
tag: "gorm:\"type:varchar(50);uniqueIndex;not null\""
|
||||
comment: "角色名称"
|
||||
validation: "required,min=2,max=50"
|
||||
- name: "description"
|
||||
type: "string"
|
||||
tag: "gorm:\"type:text\""
|
||||
comment: "角色描述"
|
||||
|
||||
enums:
|
||||
- name: "UserStatus"
|
||||
values:
|
||||
- name: "Active"
|
||||
value: "active"
|
||||
comment: "激活状态"
|
||||
- name: "Inactive"
|
||||
value: "inactive"
|
||||
comment: "未激活状态"
|
||||
- name: "Suspended"
|
||||
value: "suspended"
|
||||
comment: "暂停状态"
|
||||
|
||||
services:
|
||||
- name: "user"
|
||||
methods:
|
||||
- name: "CreateUser"
|
||||
command: "CreateUserCommand"
|
||||
response: "UserInfoResponse"
|
||||
- name: "UpdateUser"
|
||||
command: "UpdateUserCommand"
|
||||
response: "UserInfoResponse"
|
||||
- name: "DeleteUser"
|
||||
command: "DeleteUserCommand"
|
||||
- name: "GetUserByID"
|
||||
query: "GetUserQuery"
|
||||
response: "UserInfoResponse"
|
||||
- name: "ListUsers"
|
||||
query: "ListUsersQuery"
|
||||
response: "UserListResponse"
|
||||
- name: "EnableUser"
|
||||
command: "EnableUserCommand"
|
||||
- name: "DisableUser"
|
||||
command: "DisableUserCommand"
|
||||
|
||||
apis:
|
||||
- path: "/users"
|
||||
methods:
|
||||
- method: "GET"
|
||||
handler: "ListUsers"
|
||||
summary: "获取用户列表"
|
||||
- method: "POST"
|
||||
handler: "CreateUser"
|
||||
summary: "创建用户"
|
||||
- path: "/users/:id"
|
||||
methods:
|
||||
- method: "GET"
|
||||
handler: "GetUserDetail"
|
||||
summary: "获取用户详情"
|
||||
- method: "PUT"
|
||||
handler: "UpdateUser"
|
||||
summary: "更新用户"
|
||||
- method: "DELETE"
|
||||
handler: "DeleteUser"
|
||||
summary: "删除用户"
|
||||
- path: "/users/:id/enable"
|
||||
methods:
|
||||
- method: "POST"
|
||||
handler: "EnableUser"
|
||||
summary: "启用用户"
|
||||
- path: "/users/:id/disable"
|
||||
methods:
|
||||
- method: "POST"
|
||||
handler: "DisableUser"
|
||||
summary: "禁用用户"
|
||||
```
|
||||
|
||||
## 命令行接口
|
||||
|
||||
### 1. 主要命令
|
||||
|
||||
```bash
|
||||
# 生成完整领域
|
||||
ddd-gen domain <domain-name> [options]
|
||||
|
||||
# 生成实体
|
||||
ddd-gen entity <domain-name> <entity-name> [options]
|
||||
|
||||
# 生成服务
|
||||
ddd-gen service <domain-name> <service-name> [options]
|
||||
|
||||
# 生成API
|
||||
ddd-gen api <domain-name> <api-name> [options]
|
||||
|
||||
# 生成DTO
|
||||
ddd-gen dto <domain-name> <dto-type> [options]
|
||||
|
||||
# 生成测试
|
||||
ddd-gen test <domain-name> <test-type> [options]
|
||||
|
||||
# 项目初始化
|
||||
ddd-gen init [options]
|
||||
|
||||
# 配置管理
|
||||
ddd-gen config [subcommand] [options]
|
||||
|
||||
# 模板管理
|
||||
ddd-gen template [subcommand] [options]
|
||||
```
|
||||
|
||||
### 2. 选项参数
|
||||
|
||||
```bash
|
||||
# 通用选项
|
||||
--config, -c 指定配置文件
|
||||
--output, -o 指定输出目录
|
||||
--template, -t 指定模板目录
|
||||
--force, -f 强制覆盖文件
|
||||
--dry-run 试运行模式
|
||||
--verbose, -v 详细输出
|
||||
--quiet, -q 静默模式
|
||||
|
||||
# 领域选项
|
||||
--entities 指定实体列表
|
||||
--features 指定功能特性
|
||||
--services 指定服务列表
|
||||
--apis 指定API列表
|
||||
|
||||
# 实体选项
|
||||
--fields 指定字段定义
|
||||
--methods 指定业务方法
|
||||
--enums 指定枚举定义
|
||||
--relations 指定关联关系
|
||||
|
||||
# 服务选项
|
||||
--methods 指定服务方法
|
||||
--commands 指定命令DTO
|
||||
--queries 指定查询DTO
|
||||
--responses 指定响应DTO
|
||||
```
|
||||
|
||||
### 3. 交互式模式
|
||||
|
||||
```bash
|
||||
# 交互式生成领域
|
||||
ddd-gen domain --interactive
|
||||
|
||||
# 交互式生成实体
|
||||
ddd-gen entity --interactive
|
||||
|
||||
# 交互式配置
|
||||
ddd-gen config --interactive
|
||||
```
|
||||
|
||||
## 模板系统
|
||||
|
||||
### 1. 模板结构
|
||||
|
||||
```
|
||||
templates/
|
||||
├── domain/ # 领域模板
|
||||
│ ├── entities/ # 实体模板
|
||||
│ ├── repositories/ # 仓储模板
|
||||
│ ├── services/ # 服务模板
|
||||
│ └── http/ # HTTP模板
|
||||
├── dto/ # DTO模板
|
||||
├── test/ # 测试模板
|
||||
├── config/ # 配置模板
|
||||
└── docs/ # 文档模板
|
||||
```
|
||||
|
||||
### 2. 模板变量
|
||||
|
||||
```go
|
||||
// 基础变量
|
||||
{{.DomainName}} // 领域名称
|
||||
{{.EntityName}} // 实体名称
|
||||
{{.ServiceName}} // 服务名称
|
||||
{{.PackageName}} // 包名称
|
||||
{{.ModulePath}} // 模块路径
|
||||
|
||||
// 实体变量
|
||||
{{.Fields}} // 字段列表
|
||||
{{.Methods}} // 方法列表
|
||||
{{.Enums}} // 枚举列表
|
||||
{{.Relations}} // 关联关系
|
||||
|
||||
// 服务变量
|
||||
{{.Commands}} // 命令列表
|
||||
{{.Queries}} // 查询列表
|
||||
{{.Responses}} // 响应列表
|
||||
|
||||
// API变量
|
||||
{{.Routes}} // 路由列表
|
||||
{{.Handlers}} // 处理器列表
|
||||
{{.Middlewares}} // 中间件列表
|
||||
|
||||
// 项目变量
|
||||
{{.ProjectName}} // 项目名称
|
||||
{{.Author}} // 作者
|
||||
{{.Version}} // 版本
|
||||
{{.Timestamp}} // 时间戳
|
||||
```
|
||||
|
||||
### 3. 条件渲染
|
||||
|
||||
```go
|
||||
{{if .HasEnums}}
|
||||
// 枚举定义
|
||||
{{range .Enums}}
|
||||
type {{.Name}} {{.Type}}
|
||||
const (
|
||||
{{range .Values}}
|
||||
{{.Name}}{{.Name}} {{.Type}} = "{{.Value}}"
|
||||
{{end}}
|
||||
)
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{if .HasRelations}}
|
||||
// 关联关系
|
||||
{{range .Relations}}
|
||||
{{.FieldName}} {{.Type}} `gorm:"foreignKey:{{.ForeignKey}}"`
|
||||
{{end}}
|
||||
{{end}}
|
||||
```
|
||||
|
||||
## 代码分析
|
||||
|
||||
### 1. AST分析
|
||||
|
||||
```go
|
||||
// 分析现有代码结构
|
||||
type CodeAnalyzer struct {
|
||||
parser *ast.Parser
|
||||
types *types.Info
|
||||
}
|
||||
|
||||
// 分析实体结构
|
||||
func (ca *CodeAnalyzer) AnalyzeEntity(filePath string) (*EntityInfo, error) {
|
||||
// 解析Go文件AST
|
||||
// 提取结构体信息
|
||||
// 分析字段和方法
|
||||
// 识别关联关系
|
||||
}
|
||||
|
||||
// 分析依赖关系
|
||||
func (ca *CodeAnalyzer) AnalyzeDependencies(packagePath string) (*DependencyInfo, error) {
|
||||
// 分析包依赖
|
||||
// 识别循环依赖
|
||||
// 生成依赖图
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 命名分析
|
||||
|
||||
```go
|
||||
// 命名规范检查
|
||||
type NamingAnalyzer struct {
|
||||
rules []NamingRule
|
||||
}
|
||||
|
||||
// 检查命名规范
|
||||
func (na *NamingAnalyzer) CheckNaming(name, type string) error {
|
||||
// 检查命名规范
|
||||
// 提供建议
|
||||
// 自动修正
|
||||
}
|
||||
```
|
||||
|
||||
## 验证系统
|
||||
|
||||
### 1. 语法验证
|
||||
|
||||
```go
|
||||
// 验证生成的代码
|
||||
func ValidateCode(code string) error {
|
||||
// 解析Go代码
|
||||
// 检查语法错误
|
||||
// 验证类型
|
||||
// 检查导入
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 规范检查
|
||||
|
||||
```go
|
||||
// 检查代码规范
|
||||
func CheckCodeStyle(code string) []StyleIssue {
|
||||
// 检查格式
|
||||
// 检查命名
|
||||
// 检查注释
|
||||
// 检查复杂度
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 依赖验证
|
||||
|
||||
```go
|
||||
// 验证依赖关系
|
||||
func ValidateDependencies(domain *Domain) error {
|
||||
// 检查循环依赖
|
||||
// 验证接口实现
|
||||
// 检查导入路径
|
||||
}
|
||||
```
|
||||
|
||||
## 扩展功能
|
||||
|
||||
### 1. 插件系统
|
||||
|
||||
```go
|
||||
// 插件接口
|
||||
type Plugin interface {
|
||||
Name() string
|
||||
Execute(ctx *Context) error
|
||||
Validate(ctx *Context) error
|
||||
}
|
||||
|
||||
// 内置插件
|
||||
var BuiltinPlugins = []Plugin{
|
||||
&LinterPlugin{},
|
||||
&TestPlugin{},
|
||||
&DocsPlugin{},
|
||||
&MigrationPlugin{},
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 自定义模板
|
||||
|
||||
```go
|
||||
// 模板注册
|
||||
func RegisterTemplate(name, content string) error {
|
||||
// 验证模板语法
|
||||
// 注册模板
|
||||
// 更新索引
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 代码转换
|
||||
|
||||
```go
|
||||
// 代码转换器
|
||||
type CodeTransformer struct {
|
||||
rules []TransformRule
|
||||
}
|
||||
|
||||
// 转换现有代码
|
||||
func (ct *CodeTransformer) Transform(code string) (string, error) {
|
||||
// 应用转换规则
|
||||
// 保持代码结构
|
||||
// 更新引用
|
||||
}
|
||||
```
|
||||
|
||||
## 部署方案
|
||||
|
||||
### 1. 安装方式
|
||||
|
||||
```bash
|
||||
# Go安装
|
||||
go install github.com/your-org/ddd-gen@latest
|
||||
|
||||
# 二进制安装
|
||||
curl -L https://github.com/your-org/ddd-gen/releases/latest/download/ddd-gen-$(uname -s)-$(uname -m) -o ddd-gen
|
||||
chmod +x ddd-gen
|
||||
sudo mv ddd-gen /usr/local/bin/
|
||||
|
||||
# Docker安装
|
||||
docker run --rm -v $(pwd):/workspace ddd-gen domain user
|
||||
```
|
||||
|
||||
### 2. 配置管理
|
||||
|
||||
```bash
|
||||
# 初始化配置
|
||||
ddd-gen init
|
||||
|
||||
# 查看配置
|
||||
ddd-gen config show
|
||||
|
||||
# 编辑配置
|
||||
ddd-gen config edit
|
||||
|
||||
# 验证配置
|
||||
ddd-gen config validate
|
||||
```
|
||||
|
||||
### 3. 集成CI/CD
|
||||
|
||||
```yaml
|
||||
# GitHub Actions
|
||||
- name: Generate DDD Code
|
||||
run: |
|
||||
ddd-gen domain user --config domains/user.yaml
|
||||
ddd-gen domain product --config domains/product.yaml
|
||||
|
||||
- name: Validate Generated Code
|
||||
run: |
|
||||
go mod tidy
|
||||
go vet ./...
|
||||
golangci-lint run
|
||||
```
|
||||
|
||||
## 开发计划
|
||||
|
||||
### 第一阶段:核心功能
|
||||
- [ ] CLI框架搭建
|
||||
- [ ] 基础模板系统
|
||||
- [ ] 实体生成功能
|
||||
- [ ] 仓储生成功能
|
||||
- [ ] 应用服务生成功能
|
||||
|
||||
### 第二阶段:完整功能
|
||||
- [ ] HTTP层生成
|
||||
- [ ] DTO生成
|
||||
- [ ] 测试生成
|
||||
- [ ] 依赖注入配置
|
||||
- [ ] 代码验证
|
||||
|
||||
### 第三阶段:高级功能
|
||||
- [ ] 代码分析
|
||||
- [ ] 智能建议
|
||||
- [ ] 插件系统
|
||||
- [ ] 自定义模板
|
||||
- [ ] 批量操作
|
||||
|
||||
### 第四阶段:优化完善
|
||||
- [ ] 性能优化
|
||||
- [ ] 错误处理
|
||||
- [ ] 文档完善
|
||||
- [ ] 测试覆盖
|
||||
- [ ] 用户反馈
|
||||
|
||||
## 总结
|
||||
|
||||
这个DDD代码生成器将显著提升开发效率,确保代码质量和架构一致性。通过配置驱动的方式,可以快速生成符合DDD架构的完整代码结构,同时支持灵活的定制和扩展。
|
||||
|
||||
关键优势:
|
||||
1. **高效开发**: 大幅减少重复代码编写
|
||||
2. **架构一致**: 确保所有代码符合DDD规范
|
||||
3. **质量保证**: 内置代码验证和测试生成
|
||||
4. **灵活扩展**: 支持自定义模板和插件
|
||||
5. **易于使用**: 友好的CLI和交互界面
|
||||
|
||||
你觉得这个策划方案如何?需要我开始实现吗?
|
||||
1232
docs/DDD领域开发指南.md
Normal file
1232
docs/DDD领域开发指南.md
Normal file
File diff suppressed because it is too large
Load Diff
1017
docs/swagger/docs.go
1017
docs/swagger/docs.go
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -68,6 +68,25 @@ definitions:
|
||||
- role
|
||||
- username
|
||||
type: object
|
||||
commands.CreateSubscriptionCommand:
|
||||
properties:
|
||||
api_limit:
|
||||
type: integer
|
||||
auto_renew:
|
||||
type: boolean
|
||||
duration:
|
||||
type: string
|
||||
price:
|
||||
minimum: 0
|
||||
type: number
|
||||
product_id:
|
||||
type: string
|
||||
user_id:
|
||||
type: string
|
||||
required:
|
||||
- product_id
|
||||
- user_id
|
||||
type: object
|
||||
commands.CreateUserSecretsCommand:
|
||||
properties:
|
||||
expires_at:
|
||||
@@ -156,6 +175,29 @@ definitions:
|
||||
- password
|
||||
- phone
|
||||
type: object
|
||||
commands.ResetPasswordCommand:
|
||||
description: 重置用户密码请求参数(忘记密码时使用)
|
||||
properties:
|
||||
code:
|
||||
example: "123456"
|
||||
type: string
|
||||
confirm_new_password:
|
||||
example: newpassword123
|
||||
type: string
|
||||
new_password:
|
||||
example: newpassword123
|
||||
maxLength: 128
|
||||
minLength: 6
|
||||
type: string
|
||||
phone:
|
||||
example: "13800138000"
|
||||
type: string
|
||||
required:
|
||||
- code
|
||||
- confirm_new_password
|
||||
- new_password
|
||||
- phone
|
||||
type: object
|
||||
commands.SendCodeCommand:
|
||||
description: 发送短信验证码请求参数
|
||||
properties:
|
||||
@@ -428,6 +470,52 @@ definitions:
|
||||
total_operations:
|
||||
type: integer
|
||||
type: object
|
||||
responses.CategoryInfoResponse:
|
||||
properties:
|
||||
children:
|
||||
items:
|
||||
$ref: '#/definitions/responses.CategoryInfoResponse'
|
||||
type: array
|
||||
code:
|
||||
type: string
|
||||
created_at:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
is_enabled:
|
||||
type: boolean
|
||||
is_visible:
|
||||
type: boolean
|
||||
level:
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
parent:
|
||||
allOf:
|
||||
- $ref: '#/definitions/responses.CategoryInfoResponse'
|
||||
description: 关联信息
|
||||
parent_id:
|
||||
type: string
|
||||
sort:
|
||||
type: integer
|
||||
updated_at:
|
||||
type: string
|
||||
type: object
|
||||
responses.CategoryListResponse:
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
$ref: '#/definitions/responses.CategoryInfoResponse'
|
||||
type: array
|
||||
page:
|
||||
type: integer
|
||||
size:
|
||||
type: integer
|
||||
total:
|
||||
type: integer
|
||||
type: object
|
||||
responses.CertificationResponse:
|
||||
properties:
|
||||
completed_at:
|
||||
@@ -496,6 +584,83 @@ definitions:
|
||||
user:
|
||||
$ref: '#/definitions/responses.UserProfileResponse'
|
||||
type: object
|
||||
responses.ProductInfoResponse:
|
||||
properties:
|
||||
category:
|
||||
allOf:
|
||||
- $ref: '#/definitions/responses.CategoryInfoResponse'
|
||||
description: 关联信息
|
||||
category_id:
|
||||
type: string
|
||||
code:
|
||||
type: string
|
||||
content:
|
||||
type: string
|
||||
created_at:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
is_enabled:
|
||||
type: boolean
|
||||
is_package:
|
||||
type: boolean
|
||||
is_visible:
|
||||
type: boolean
|
||||
name:
|
||||
type: string
|
||||
price:
|
||||
type: number
|
||||
seo_description:
|
||||
type: string
|
||||
seo_keywords:
|
||||
type: string
|
||||
seo_title:
|
||||
description: SEO信息
|
||||
type: string
|
||||
updated_at:
|
||||
type: string
|
||||
type: object
|
||||
responses.ProductListResponse:
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
$ref: '#/definitions/responses.ProductInfoResponse'
|
||||
type: array
|
||||
page:
|
||||
type: integer
|
||||
size:
|
||||
type: integer
|
||||
total:
|
||||
type: integer
|
||||
type: object
|
||||
responses.ProductSimpleResponse:
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
is_package:
|
||||
type: boolean
|
||||
name:
|
||||
type: string
|
||||
price:
|
||||
type: number
|
||||
type: object
|
||||
responses.ProductStatsResponse:
|
||||
properties:
|
||||
enabled_products:
|
||||
type: integer
|
||||
package_products:
|
||||
type: integer
|
||||
total_products:
|
||||
type: integer
|
||||
visible_products:
|
||||
type: integer
|
||||
type: object
|
||||
responses.RegisterUserResponse:
|
||||
description: 用户注册成功响应
|
||||
properties:
|
||||
@@ -506,6 +671,56 @@ definitions:
|
||||
example: "13800138000"
|
||||
type: string
|
||||
type: object
|
||||
responses.SubscriptionInfoResponse:
|
||||
properties:
|
||||
api_used:
|
||||
type: integer
|
||||
created_at:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
price:
|
||||
type: number
|
||||
product:
|
||||
allOf:
|
||||
- $ref: '#/definitions/responses.ProductSimpleResponse'
|
||||
description: 关联信息
|
||||
product_id:
|
||||
type: string
|
||||
updated_at:
|
||||
type: string
|
||||
user_id:
|
||||
type: string
|
||||
type: object
|
||||
responses.SubscriptionListResponse:
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
$ref: '#/definitions/responses.SubscriptionInfoResponse'
|
||||
type: array
|
||||
page:
|
||||
type: integer
|
||||
size:
|
||||
type: integer
|
||||
total:
|
||||
type: integer
|
||||
type: object
|
||||
responses.SubscriptionStatsResponse:
|
||||
properties:
|
||||
total_revenue:
|
||||
type: number
|
||||
total_subscriptions:
|
||||
type: integer
|
||||
type: object
|
||||
responses.SubscriptionUsageResponse:
|
||||
properties:
|
||||
api_used:
|
||||
type: integer
|
||||
id:
|
||||
type: string
|
||||
product_id:
|
||||
type: string
|
||||
type: object
|
||||
responses.TransactionResponse:
|
||||
properties:
|
||||
amount:
|
||||
@@ -558,6 +773,9 @@ definitions:
|
||||
id:
|
||||
example: 123e4567-e89b-12d3-a456-426614174000
|
||||
type: string
|
||||
is_certified:
|
||||
example: false
|
||||
type: boolean
|
||||
phone:
|
||||
example: "13800138000"
|
||||
type: string
|
||||
@@ -905,7 +1123,7 @@ paths:
|
||||
type: object
|
||||
summary: 管理员登录
|
||||
tags:
|
||||
- 管理员认证
|
||||
- 管理员管理
|
||||
/api/v1/admin/change-password:
|
||||
post:
|
||||
consumes:
|
||||
@@ -973,6 +1191,71 @@ paths:
|
||||
summary: 获取管理员统计信息
|
||||
tags:
|
||||
- 管理员管理
|
||||
/api/v1/categories:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 获取产品分类列表,支持层级筛选
|
||||
parameters:
|
||||
- description: 父级分类ID
|
||||
in: query
|
||||
name: parent_id
|
||||
type: string
|
||||
- description: 分类层级
|
||||
in: query
|
||||
name: level
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 获取分类列表成功
|
||||
schema:
|
||||
$ref: '#/definitions/responses.CategoryListResponse'
|
||||
"500":
|
||||
description: 服务器内部错误
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
summary: 获取分类列表
|
||||
tags:
|
||||
- 数据大厅
|
||||
/api/v1/categories/{id}:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 根据分类ID获取分类详细信息
|
||||
parameters:
|
||||
- description: 分类ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 获取分类详情成功
|
||||
schema:
|
||||
$ref: '#/definitions/responses.CategoryInfoResponse'
|
||||
"400":
|
||||
description: 请求参数错误
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"404":
|
||||
description: 分类不存在
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"500":
|
||||
description: 服务器内部错误
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
summary: 获取分类详情
|
||||
tags:
|
||||
- 数据大厅
|
||||
/api/v1/certification:
|
||||
post:
|
||||
consumes:
|
||||
@@ -1364,7 +1647,7 @@ paths:
|
||||
- Bearer: []
|
||||
summary: 上传营业执照并同步OCR识别
|
||||
tags:
|
||||
- 认证管理
|
||||
- 企业认证
|
||||
/api/v1/finance/secrets:
|
||||
get:
|
||||
consumes:
|
||||
@@ -1756,6 +2039,358 @@ paths:
|
||||
summary: 钱包提现
|
||||
tags:
|
||||
- 钱包管理
|
||||
/api/v1/my/subscriptions:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 获取当前用户的订阅列表
|
||||
parameters:
|
||||
- default: 1
|
||||
description: 页码
|
||||
in: query
|
||||
name: page
|
||||
type: integer
|
||||
- default: 10
|
||||
description: 每页数量
|
||||
in: query
|
||||
name: page_size
|
||||
type: integer
|
||||
- description: 订阅状态
|
||||
in: query
|
||||
name: status
|
||||
type: string
|
||||
- description: 排序字段
|
||||
in: query
|
||||
name: sort_by
|
||||
type: string
|
||||
- description: 排序方向
|
||||
enum:
|
||||
- asc
|
||||
- desc
|
||||
in: query
|
||||
name: sort_order
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 获取订阅列表成功
|
||||
schema:
|
||||
$ref: '#/definitions/responses.SubscriptionListResponse'
|
||||
"400":
|
||||
description: 请求参数错误
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"401":
|
||||
description: 未认证
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"500":
|
||||
description: 服务器内部错误
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: 获取我的订阅列表
|
||||
tags:
|
||||
- 我的订阅
|
||||
/api/v1/my/subscriptions/{id}:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 获取指定订阅的详细信息
|
||||
parameters:
|
||||
- description: 订阅ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 获取订阅详情成功
|
||||
schema:
|
||||
$ref: '#/definitions/responses.SubscriptionInfoResponse'
|
||||
"400":
|
||||
description: 请求参数错误
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"401":
|
||||
description: 未认证
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"404":
|
||||
description: 订阅不存在
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"500":
|
||||
description: 服务器内部错误
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: 获取我的订阅详情
|
||||
tags:
|
||||
- 我的订阅
|
||||
/api/v1/my/subscriptions/{id}/usage:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 获取指定订阅的使用情况统计
|
||||
parameters:
|
||||
- description: 订阅ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 获取使用情况成功
|
||||
schema:
|
||||
$ref: '#/definitions/responses.SubscriptionUsageResponse'
|
||||
"400":
|
||||
description: 请求参数错误
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"401":
|
||||
description: 未认证
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"404":
|
||||
description: 订阅不存在
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"500":
|
||||
description: 服务器内部错误
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: 获取我的订阅使用情况
|
||||
tags:
|
||||
- 我的订阅
|
||||
/api/v1/my/subscriptions/stats:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 获取当前用户的订阅统计信息
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 获取订阅统计成功
|
||||
schema:
|
||||
$ref: '#/definitions/responses.SubscriptionStatsResponse'
|
||||
"401":
|
||||
description: 未认证
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"500":
|
||||
description: 服务器内部错误
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: 获取我的订阅统计
|
||||
tags:
|
||||
- 我的订阅
|
||||
/api/v1/products:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 分页获取可用的产品列表,支持筛选
|
||||
parameters:
|
||||
- default: 1
|
||||
description: 页码
|
||||
in: query
|
||||
name: page
|
||||
type: integer
|
||||
- default: 10
|
||||
description: 每页数量
|
||||
in: query
|
||||
name: page_size
|
||||
type: integer
|
||||
- description: 搜索关键词
|
||||
in: query
|
||||
name: keyword
|
||||
type: string
|
||||
- description: 分类ID
|
||||
in: query
|
||||
name: category_id
|
||||
type: string
|
||||
- description: 最低价格
|
||||
in: query
|
||||
name: min_price
|
||||
type: number
|
||||
- description: 最高价格
|
||||
in: query
|
||||
name: max_price
|
||||
type: number
|
||||
- description: 是否启用
|
||||
in: query
|
||||
name: is_enabled
|
||||
type: boolean
|
||||
- description: 是否可见
|
||||
in: query
|
||||
name: is_visible
|
||||
type: boolean
|
||||
- description: 是否组合包
|
||||
in: query
|
||||
name: is_package
|
||||
type: boolean
|
||||
- description: 排序字段
|
||||
in: query
|
||||
name: sort_by
|
||||
type: string
|
||||
- description: 排序方向
|
||||
enum:
|
||||
- asc
|
||||
- desc
|
||||
in: query
|
||||
name: sort_order
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 获取产品列表成功
|
||||
schema:
|
||||
$ref: '#/definitions/responses.ProductListResponse'
|
||||
"400":
|
||||
description: 请求参数错误
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"500":
|
||||
description: 服务器内部错误
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
summary: 获取产品列表
|
||||
tags:
|
||||
- 数据大厅
|
||||
/api/v1/products/{id}:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 根据产品ID获取产品详细信息
|
||||
parameters:
|
||||
- description: 产品ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 获取产品详情成功
|
||||
schema:
|
||||
$ref: '#/definitions/responses.ProductInfoResponse'
|
||||
"400":
|
||||
description: 请求参数错误
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"404":
|
||||
description: 产品不存在
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"500":
|
||||
description: 服务器内部错误
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
summary: 获取产品详情
|
||||
tags:
|
||||
- 数据大厅
|
||||
/api/v1/products/{id}/subscribe:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 用户订阅指定产品
|
||||
parameters:
|
||||
- description: 产品ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: 订阅请求
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/commands.CreateSubscriptionCommand'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 订阅成功
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"400":
|
||||
description: 请求参数错误
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"401":
|
||||
description: 未认证
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"404":
|
||||
description: 产品不存在
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"500":
|
||||
description: 服务器内部错误
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: 订阅产品
|
||||
tags:
|
||||
- 数据大厅
|
||||
/api/v1/products/stats:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 获取产品相关的统计信息
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 获取统计信息成功
|
||||
schema:
|
||||
$ref: '#/definitions/responses.ProductStatsResponse'
|
||||
"500":
|
||||
description: 服务器内部错误
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
summary: 获取产品统计
|
||||
tags:
|
||||
- 数据大厅
|
||||
/api/v1/users/login-password:
|
||||
post:
|
||||
consumes:
|
||||
@@ -1939,6 +2574,44 @@ paths:
|
||||
summary: 用户注册
|
||||
tags:
|
||||
- 用户认证
|
||||
/api/v1/users/reset-password:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 使用手机号、验证码和新密码重置用户密码(忘记密码时使用)
|
||||
parameters:
|
||||
- description: 重置密码请求
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/commands.ResetPasswordCommand'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 密码重置成功
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"400":
|
||||
description: 请求参数错误或验证码无效
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"404":
|
||||
description: 用户不存在
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"500":
|
||||
description: 服务器内部错误
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
summary: 重置密码
|
||||
tags:
|
||||
- 用户认证
|
||||
/api/v1/users/send-code:
|
||||
post:
|
||||
consumes:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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:"新的排序"`
|
||||
}
|
||||
@@ -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关键词"`
|
||||
}
|
||||
@@ -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调用限制"`
|
||||
}
|
||||
35
internal/application/product/dto/queries/category_queries.go
Normal file
35
internal/application/product/dto/queries/category_queries.go
Normal 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"`
|
||||
}
|
||||
47
internal/application/product/dto/queries/product_queries.go
Normal file
47
internal/application/product/dto/queries/product_queries.go
Normal 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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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:"可见分类数"`
|
||||
}
|
||||
@@ -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:"组合包产品数"`
|
||||
}
|
||||
@@ -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:"总收入"`
|
||||
}
|
||||
78
internal/application/product/product_application_service.go
Normal file
78
internal/application/product/product_application_service.go
Normal 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)
|
||||
}
|
||||
452
internal/application/product/product_application_service_impl.go
Normal file
452
internal/application/product/product_application_service_impl.go
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
93
internal/domains/product/entities/product.go
Normal file
93
internal/domains/product/entities/product.go
Normal 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
|
||||
}
|
||||
80
internal/domains/product/entities/product_category.go
Normal file
80
internal/domains/product/entities/product_category.go
Normal 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
|
||||
}
|
||||
69
internal/domains/product/entities/product_documentation.go
Normal file
69
internal/domains/product/entities/product_documentation.go
Normal 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"
|
||||
}
|
||||
}
|
||||
59
internal/domains/product/entities/subscription.go
Normal file
59
internal/domains/product/entities/subscription.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
151
internal/domains/product/services/product_service.go
Normal file
151
internal/domains/product/services/product_service.go
Normal 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
|
||||
}
|
||||
@@ -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 {
|
||||
// 检查用户是否被删除
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -33,7 +33,7 @@ func NewAdminHandler(
|
||||
// Login 管理员登录
|
||||
// @Summary 管理员登录
|
||||
// @Description 使用用户名和密码进行管理员登录,返回JWT令牌
|
||||
// @Tags 管理员认证
|
||||
// @Tags 管理员管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body commands.AdminLoginCommand true "管理员登录请求"
|
||||
|
||||
444
internal/infrastructure/http/handlers/product_handler.go
Normal file
444
internal/infrastructure/http/handlers/product_handler.go
Normal 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, "获取我的订阅使用情况成功")
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
81
internal/infrastructure/http/routes/product_routes.go
Normal file
81
internal/infrastructure/http/routes/product_routes.go
Normal 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("产品路由注册完成")
|
||||
}
|
||||
@@ -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("")
|
||||
|
||||
Reference in New Issue
Block a user