diff --git a/docs/DDD代码生成器可视化界面策划方案.md b/docs/DDD代码生成器可视化界面策划方案.md new file mode 100644 index 0000000..1a69c46 --- /dev/null +++ b/docs/DDD代码生成器可视化界面策划方案.md @@ -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架构设计变得更加直观和高效! + +你觉得这个可视化界面策划方案如何?需要我开始实现吗? \ No newline at end of file diff --git a/docs/DDD代码生成器策划方案.md b/docs/DDD代码生成器策划方案.md new file mode 100644 index 0000000..f0d9be2 --- /dev/null +++ b/docs/DDD代码生成器策划方案.md @@ -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 [options] + +# 生成实体 +ddd-gen entity [options] + +# 生成服务 +ddd-gen service [options] + +# 生成API +ddd-gen api [options] + +# 生成DTO +ddd-gen dto [options] + +# 生成测试 +ddd-gen test [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和交互界面 + +你觉得这个策划方案如何?需要我开始实现吗? \ No newline at end of file diff --git a/docs/DDD领域开发指南.md b/docs/DDD领域开发指南.md new file mode 100644 index 0000000..9c46019 --- /dev/null +++ b/docs/DDD领域开发指南.md @@ -0,0 +1,1232 @@ +# DDD领域开发指南 + +## 概述 + +本文档描述了在tyapi-server项目中如何按照DDD(领域驱动设计)架构模式新增领域或领域内功能的标准流程和最佳实践。 + +## 目录结构 + +``` +tyapi-server-gin/ +├── internal/ +│ ├── domains/ # 领域层 +│ │ └── {domain}/ # 具体领域 +│ │ ├── entities/ # 实体 +│ │ ├── repositories/ # 仓储接口 +│ │ └── services/ # 领域服务 +│ ├── application/ # 应用层 +│ │ └── {domain}/ # 具体领域 +│ │ ├── dto/ # 数据传输对象 +│ │ └── {service}_application_service.go +│ ├── infrastructure/ # 基础设施层 +│ │ ├── database/repositories/ # 仓储实现 +│ │ └── http/ # HTTP层 +│ │ ├── handlers/ # 处理器 +│ │ └── routes/ # 路由 +│ └── container/ # 依赖注入容器 +└── docs/ # 文档 +``` + +## 新增领域的完整流程 + +### 第一步:创建实体 (Entities) + +#### 1.1 主实体文件 +```go +// internal/domains/{domain}/entities/{entity}.go +package entities + +import ( + "time" + "gorm.io/gorm" +) + +// {Entity} 实体 +type {Entity} struct { + ID string `gorm:"primaryKey;type:varchar(36)" comment:"ID"` + Name string `gorm:"type:varchar(100);not null" comment:"名称"` + Description string `gorm:"type:text" comment:"描述"` + IsEnabled bool `gorm:"default:true" comment:"是否启用"` + IsVisible bool `gorm:"default:true" comment:"是否可见"` + + CreatedAt time.Time `gorm:"autoCreateTime" comment:"创建时间"` + UpdatedAt time.Time `gorm:"autoUpdateTime" comment:"更新时间"` + DeletedAt gorm.DeletedAt `gorm:"index" comment:"软删除时间"` +} + +// 业务方法 +func (e *{Entity}) IsValid() bool { + return e.DeletedAt.Time.IsZero() && e.IsEnabled +} + +func (e *{Entity}) Enable() { + e.IsEnabled = true +} + +func (e *{Entity}) Disable() { + e.IsEnabled = false +} +``` + +#### 1.2 枚举文件(如需要) +```go +// internal/domains/{domain}/entities/{entity}_enums.go +package entities + +// {Entity}Status 状态枚举 +type {Entity}Status string + +const ( + {Entity}StatusActive {Entity}Status = "active" + {Entity}StatusInactive {Entity}Status = "inactive" + {Entity}StatusPending {Entity}Status = "pending" +) +``` + +### 第二步:创建仓储接口 (Repository Interfaces) + +#### 2.1 仓储接口定义 +```go +// internal/domains/{domain}/repositories/{entity}_repository_interface.go +package repositories + +import ( + "context" + "tyapi-server/internal/domains/{domain}/entities" + "tyapi-server/internal/shared/interfaces" +) + +// {Entity}Repository 仓储接口 +type {Entity}Repository interface { + // 继承基础Repository接口 + interfaces.Repository[entities.{Entity}] + + // 业务特定方法 + FindByStatus(ctx context.Context, status entities.{Entity}Status) ([]*entities.{Entity}, error) + FindByUserID(ctx context.Context, userID string) ([]*entities.{Entity}, error) + CountByStatus(ctx context.Context, status entities.{Entity}Status) (int64, error) +} +``` + +#### 2.2 查询对象 +```go +// internal/domains/{domain}/repositories/queries/{entity}_queries.go +package queries + +import "tyapi-server/internal/domains/{domain}/entities" + +// List{Entity}sQuery 列表查询 +type List{Entity}sQuery struct { + Page int `form:"page" binding:"min=1" comment:"页码"` + PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"` + Status entities.{Entity}Status `form:"status" comment:"状态"` + UserID string `form:"user_id" comment:"用户ID"` + SortBy string `form:"sort_by" comment:"排序字段"` + SortOrder string `form:"sort_order" comment:"排序方向"` +} + +// Get{Entity}Query 获取详情查询 +type Get{Entity}Query struct { + ID string `uri:"id" binding:"required" comment:"ID"` +} +``` + +### 第三步:创建应用服务 (Application Services) + +#### 3.1 应用服务接口 +```go +// internal/application/{domain}/{entity}_application_service.go +package {domain} + +import ( + "context" + "tyapi-server/internal/application/{domain}/dto/commands" + "tyapi-server/internal/application/{domain}/dto/queries" + "tyapi-server/internal/application/{domain}/dto/responses" +) + +// {Entity}ApplicationService 应用服务接口 +type {Entity}ApplicationService interface { + // 基础CRUD操作 + Create{Entity}(ctx context.Context, cmd *commands.Create{Entity}Command) error + Update{Entity}(ctx context.Context, cmd *commands.Update{Entity}Command) error + Delete{Entity}(ctx context.Context, cmd *commands.Delete{Entity}Command) error + Get{Entity}ByID(ctx context.Context, query *queries.Get{Entity}Query) (*responses.{Entity}InfoResponse, error) + List{Entity}s(ctx context.Context, query *queries.List{Entity}sQuery) (*responses.{Entity}ListResponse, error) + + // 业务操作 + Enable{Entity}(ctx context.Context, cmd *commands.Enable{Entity}Command) error + Disable{Entity}(ctx context.Context, cmd *commands.Disable{Entity}Command) error + + // 统计 + Get{Entity}Stats(ctx context.Context) (*responses.{Entity}StatsResponse, error) +} +``` + +#### 3.2 应用服务实现 +```go +// internal/application/{domain}/{entity}_application_service_impl.go +package {domain} + +import ( + "context" + "fmt" + "tyapi-server/internal/application/{domain}/dto/commands" + "tyapi-server/internal/application/{domain}/dto/queries" + "tyapi-server/internal/application/{domain}/dto/responses" + "tyapi-server/internal/domains/{domain}/entities" + "tyapi-server/internal/domains/{domain}/repositories" + repoQueries "tyapi-server/internal/domains/{domain}/repositories/queries" + + "go.uber.org/zap" +) + +// {Entity}ApplicationServiceImpl 应用服务实现 +type {Entity}ApplicationServiceImpl struct { + {entity}Repo repositories.{Entity}Repository + logger *zap.Logger +} + +// New{Entity}ApplicationService 创建应用服务 +func New{Entity}ApplicationService( + {entity}Repo repositories.{Entity}Repository, + logger *zap.Logger, +) {Entity}ApplicationService { + return &{Entity}ApplicationServiceImpl{ + {entity}Repo: {entity}Repo, + logger: logger, + } +} + +// Create{Entity} 创建实体 +func (s *{Entity}ApplicationServiceImpl) Create{Entity}(ctx context.Context, cmd *commands.Create{Entity}Command) error { + // 1. 参数验证 + if err := s.validateCreate{Entity}(cmd); err != nil { + return err + } + + // 2. 创建实体 + {entity} := entities.{Entity}{ + Name: cmd.Name, + Description: cmd.Description, + IsEnabled: cmd.IsEnabled, + IsVisible: cmd.IsVisible, + } + + // 3. 保存到仓储 + _, err := s.{entity}Repo.Create(ctx, {entity}) + if err != nil { + s.logger.Error("创建{Entity}失败", zap.Error(err)) + return fmt.Errorf("创建{Entity}失败: %w", err) + } + + s.logger.Info("创建{Entity}成功", zap.String("name", cmd.Name)) + return nil +} + +// 其他方法实现... +``` + +### 第四步:创建DTO (Data Transfer Objects) + +#### 4.1 命令DTO +```go +// internal/application/{domain}/dto/commands/{entity}_commands.go +package commands + +// Create{Entity}Command 创建命令 +type Create{Entity}Command struct { + Name string `json:"name" binding:"required" comment:"名称"` + Description string `json:"description" comment:"描述"` + IsEnabled bool `json:"is_enabled" comment:"是否启用"` + IsVisible bool `json:"is_visible" comment:"是否可见"` +} + +// Update{Entity}Command 更新命令 +type Update{Entity}Command struct { + ID string `json:"-"` + Name string `json:"name" comment:"名称"` + Description string `json:"description" comment:"描述"` + IsEnabled *bool `json:"is_enabled" comment:"是否启用"` + IsVisible *bool `json:"is_visible" comment:"是否可见"` +} + +// Delete{Entity}Command 删除命令 +type Delete{Entity}Command struct { + ID string `json:"-"` +} + +// Enable{Entity}Command 启用命令 +type Enable{Entity}Command struct { + ID string `json:"-"` +} + +// Disable{Entity}Command 禁用命令 +type Disable{Entity}Command struct { + ID string `json:"-"` +} +``` + +#### 4.2 查询DTO +```go +// internal/application/{domain}/dto/queries/{entity}_queries.go +package queries + +import "tyapi-server/internal/domains/{domain}/entities" + +// List{Entity}sQuery 列表查询 +type List{Entity}sQuery struct { + Page int `form:"page" binding:"min=1" comment:"页码"` + PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"` + Status entities.{Entity}Status `form:"status" comment:"状态"` + UserID string `form:"user_id" comment:"用户ID"` + SortBy string `form:"sort_by" comment:"排序字段"` + SortOrder string `form:"sort_order" comment:"排序方向"` +} + +// Get{Entity}Query 获取详情查询 +type Get{Entity}Query struct { + ID string `uri:"id" binding:"required" comment:"ID"` +} +``` + +#### 4.3 响应DTO +```go +// internal/application/{domain}/dto/responses/{entity}_responses.go +package responses + +import "time" + +// {Entity}InfoResponse 详情响应 +type {Entity}InfoResponse struct { + ID string `json:"id" comment:"ID"` + Name string `json:"name" comment:"名称"` + Description string `json:"description" comment:"描述"` + IsEnabled bool `json:"is_enabled" comment:"是否启用"` + IsVisible bool `json:"is_visible" comment:"是否可见"` + CreatedAt time.Time `json:"created_at" comment:"创建时间"` + UpdatedAt time.Time `json:"updated_at" comment:"更新时间"` +} + +// {Entity}ListResponse 列表响应 +type {Entity}ListResponse struct { + Total int64 `json:"total" comment:"总数"` + Page int `json:"page" comment:"页码"` + Size int `json:"size" comment:"每页数量"` + Items []{Entity}InfoResponse `json:"items" comment:"列表"` +} + +// {Entity}StatsResponse 统计响应 +type {Entity}StatsResponse struct { + Total{Entity}s int64 `json:"total_{entity}s" comment:"总数"` + Enabled{Entity}s int64 `json:"enabled_{entity}s" comment:"启用数"` + Visible{Entity}s int64 `json:"visible_{entity}s" comment:"可见数"` +} +``` + +### 第五步:实现仓储 (Repository Implementation) + +#### 5.1 GORM仓储实现 +```go +// internal/infrastructure/database/repositories/{domain}/gorm_{entity}_repository.go +package repositories + +import ( + "context" + "tyapi-server/internal/domains/{domain}/entities" + "tyapi-server/internal/domains/{domain}/repositories" + "tyapi-server/internal/domains/{domain}/repositories/queries" + "tyapi-server/internal/shared/interfaces" + + "go.uber.org/zap" + "gorm.io/gorm" +) + +// Gorm{Entity}Repository GORM仓储实现 +type Gorm{Entity}Repository struct { + db *gorm.DB + logger *zap.Logger +} + +// 编译时检查接口实现 +var _ repositories.{Entity}Repository = (*Gorm{Entity}Repository)(nil) + +// NewGorm{Entity}Repository 创建GORM仓储 +func NewGorm{Entity}Repository(db *gorm.DB, logger *zap.Logger) repositories.{Entity}Repository { + return &Gorm{Entity}Repository{ + db: db, + logger: logger, + } +} + +// Create 创建实体 +func (r *Gorm{Entity}Repository) Create(ctx context.Context, entity entities.{Entity}) (entities.{Entity}, error) { + r.logger.Info("创建{Entity}", zap.String("id", entity.ID)) + err := r.db.WithContext(ctx).Create(&entity).Error + return entity, err +} + +// GetByID 根据ID获取实体 +func (r *Gorm{Entity}Repository) GetByID(ctx context.Context, id string) (entities.{Entity}, error) { + var entity entities.{Entity} + err := r.db.WithContext(ctx).Where("id = ?", id).First(&entity).Error + return entity, err +} + +// Update 更新实体 +func (r *Gorm{Entity}Repository) Update(ctx context.Context, entity entities.{Entity}) error { + r.logger.Info("更新{Entity}", zap.String("id", entity.ID)) + return r.db.WithContext(ctx).Save(&entity).Error +} + +// Delete 删除实体 +func (r *Gorm{Entity}Repository) Delete(ctx context.Context, id string) error { + r.logger.Info("删除{Entity}", zap.String("id", id)) + return r.db.WithContext(ctx).Delete(&entities.{Entity}{}, "id = ?", id).Error +} + +// List{Entity}s 获取列表 +func (r *Gorm{Entity}Repository) List{Entity}s(ctx context.Context, query *queries.List{Entity}sQuery) ([]*entities.{Entity}, int64, error) { + var {entity}s []entities.{Entity} + var total int64 + + dbQuery := r.db.WithContext(ctx).Model(&entities.{Entity}{}) + + // 应用筛选条件 + if query.Status != "" { + dbQuery = dbQuery.Where("status = ?", query.Status) + } + if query.UserID != "" { + dbQuery = dbQuery.Where("user_id = ?", query.UserID) + } + + // 获取总数 + 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(&{entity}s).Error; err != nil { + return nil, 0, err + } + + // 转换为指针切片 + result := make([]*entities.{Entity}, len({entity}s)) + for i := range {entity}s { + result[i] = &{entity}s[i] + } + + return result, total, nil +} + +// 基础Repository接口方法 +func (r *Gorm{Entity}Repository) Count(ctx context.Context, options interfaces.CountOptions) (int64, error) { + var count int64 + query := r.db.WithContext(ctx).Model(&entities.{Entity}{}) + + // 应用筛选条件 + 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 +} + +// 其他基础方法实现... +``` + +### 第六步:创建HTTP处理器 (HTTP Handlers) + +#### 6.1 HTTP处理器 +```go +// internal/infrastructure/http/handlers/{entity}_handler.go +package handlers + +import ( + "tyapi-server/internal/application/{domain}" + "tyapi-server/internal/application/{domain}/dto/commands" + "tyapi-server/internal/application/{domain}/dto/queries" + "tyapi-server/internal/shared/interfaces" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +// {Entity}Handler HTTP处理器 +type {Entity}Handler struct { + appService {domain}.{Entity}ApplicationService + responseBuilder interfaces.ResponseBuilder + logger *zap.Logger +} + +// New{Entity}Handler 创建HTTP处理器 +func New{Entity}Handler( + appService {domain}.{Entity}ApplicationService, + responseBuilder interfaces.ResponseBuilder, + logger *zap.Logger, +) *{Entity}Handler { + return &{Entity}Handler{ + appService: appService, + responseBuilder: responseBuilder, + logger: logger, + } +} + +// List{Entity}s 获取列表 +// @Summary 获取{Entity}列表 +// @Description 分页获取{Entity}列表,支持筛选 +// @Tags {Entity}管理 +// @Accept json +// @Produce json +// @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.{Entity}ListResponse "获取列表成功" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 500 {object} map[string]interface{} "服务器内部错误" +// @Router /api/v1/{entity}s [get] +func (h *{Entity}Handler) List{Entity}s(c *gin.Context) { + var query queries.List{Entity}sQuery + 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.List{Entity}s(c.Request.Context(), &query) + if err != nil { + h.logger.Error("获取{Entity}列表失败", zap.Error(err)) + h.responseBuilder.InternalError(c, "获取{Entity}列表失败") + return + } + + h.responseBuilder.Success(c, result, "获取{Entity}列表成功") +} + +// Get{Entity}Detail 获取详情 +// @Summary 获取{Entity}详情 +// @Description 根据ID获取{Entity}详细信息 +// @Tags {Entity}管理 +// @Accept json +// @Produce json +// @Param id path string true "{Entity}ID" +// @Success 200 {object} responses.{Entity}InfoResponse "获取详情成功" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 404 {object} map[string]interface{} "{Entity}不存在" +// @Failure 500 {object} map[string]interface{} "服务器内部错误" +// @Router /api/v1/{entity}s/{id} [get] +func (h *{Entity}Handler) Get{Entity}Detail(c *gin.Context) { + var query queries.Get{Entity}Query + query.ID = c.Param("id") + + if query.ID == "" { + h.responseBuilder.BadRequest(c, "{Entity}ID不能为空") + return + } + + result, err := h.appService.Get{Entity}ByID(c.Request.Context(), &query) + if err != nil { + h.logger.Error("获取{Entity}详情失败", zap.Error(err), zap.String("{entity}_id", query.ID)) + h.responseBuilder.NotFound(c, "{Entity}不存在") + return + } + + h.responseBuilder.Success(c, result, "获取{Entity}详情成功") +} + +// Create{Entity} 创建{Entity} +// @Summary 创建{Entity} +// @Description 创建新的{Entity} +// @Tags {Entity}管理 +// @Accept json +// @Produce json +// @Param request body commands.Create{Entity}Command true "创建请求" +// @Success 200 {object} map[string]interface{} "创建成功" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 500 {object} map[string]interface{} "服务器内部错误" +// @Router /api/v1/{entity}s [post] +func (h *{Entity}Handler) Create{Entity}(c *gin.Context) { + var cmd commands.Create{Entity}Command + if err := c.ShouldBindJSON(&cmd); err != nil { + h.responseBuilder.BadRequest(c, "请求参数错误") + return + } + + if err := h.appService.Create{Entity}(c.Request.Context(), &cmd); err != nil { + h.logger.Error("创建{Entity}失败", zap.Error(err)) + h.responseBuilder.BadRequest(c, err.Error()) + return + } + + h.responseBuilder.Success(c, nil, "创建{Entity}成功") +} + +// Update{Entity} 更新{Entity} +// @Summary 更新{Entity} +// @Description 更新{Entity}信息 +// @Tags {Entity}管理 +// @Accept json +// @Produce json +// @Param id path string true "{Entity}ID" +// @Param request body commands.Update{Entity}Command true "更新请求" +// @Success 200 {object} map[string]interface{} "更新成功" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 404 {object} map[string]interface{} "{Entity}不存在" +// @Failure 500 {object} map[string]interface{} "服务器内部错误" +// @Router /api/v1/{entity}s/{id} [put] +func (h *{Entity}Handler) Update{Entity}(c *gin.Context) { + id := c.Param("id") + if id == "" { + h.responseBuilder.BadRequest(c, "{Entity}ID不能为空") + return + } + + var cmd commands.Update{Entity}Command + if err := c.ShouldBindJSON(&cmd); err != nil { + h.responseBuilder.BadRequest(c, "请求参数错误") + return + } + + cmd.ID = id + + if err := h.appService.Update{Entity}(c.Request.Context(), &cmd); err != nil { + h.logger.Error("更新{Entity}失败", zap.Error(err), zap.String("{entity}_id", id)) + h.responseBuilder.BadRequest(c, err.Error()) + return + } + + h.responseBuilder.Success(c, nil, "更新{Entity}成功") +} + +// Delete{Entity} 删除{Entity} +// @Summary 删除{Entity} +// @Description 删除指定的{Entity} +// @Tags {Entity}管理 +// @Accept json +// @Produce json +// @Param id path string true "{Entity}ID" +// @Success 200 {object} map[string]interface{} "删除成功" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 404 {object} map[string]interface{} "{Entity}不存在" +// @Failure 500 {object} map[string]interface{} "服务器内部错误" +// @Router /api/v1/{entity}s/{id} [delete] +func (h *{Entity}Handler) Delete{Entity}(c *gin.Context) { + id := c.Param("id") + if id == "" { + h.responseBuilder.BadRequest(c, "{Entity}ID不能为空") + return + } + + var cmd commands.Delete{Entity}Command + cmd.ID = id + + if err := h.appService.Delete{Entity}(c.Request.Context(), &cmd); err != nil { + h.logger.Error("删除{Entity}失败", zap.Error(err), zap.String("{entity}_id", id)) + h.responseBuilder.BadRequest(c, err.Error()) + return + } + + h.responseBuilder.Success(c, nil, "删除{Entity}成功") +} + +// Enable{Entity} 启用{Entity} +// @Summary 启用{Entity} +// @Description 启用指定的{Entity} +// @Tags {Entity}管理 +// @Accept json +// @Produce json +// @Param id path string true "{Entity}ID" +// @Success 200 {object} map[string]interface{} "启用成功" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 404 {object} map[string]interface{} "{Entity}不存在" +// @Failure 500 {object} map[string]interface{} "服务器内部错误" +// @Router /api/v1/{entity}s/{id}/enable [post] +func (h *{Entity}Handler) Enable{Entity}(c *gin.Context) { + id := c.Param("id") + if id == "" { + h.responseBuilder.BadRequest(c, "{Entity}ID不能为空") + return + } + + var cmd commands.Enable{Entity}Command + cmd.ID = id + + if err := h.appService.Enable{Entity}(c.Request.Context(), &cmd); err != nil { + h.logger.Error("启用{Entity}失败", zap.Error(err), zap.String("{entity}_id", id)) + h.responseBuilder.BadRequest(c, err.Error()) + return + } + + h.responseBuilder.Success(c, nil, "启用{Entity}成功") +} + +// Disable{Entity} 禁用{Entity} +// @Summary 禁用{Entity} +// @Description 禁用指定的{Entity} +// @Tags {Entity}管理 +// @Accept json +// @Produce json +// @Param id path string true "{Entity}ID" +// @Success 200 {object} map[string]interface{} "禁用成功" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 404 {object} map[string]interface{} "{Entity}不存在" +// @Failure 500 {object} map[string]interface{} "服务器内部错误" +// @Router /api/v1/{entity}s/{id}/disable [post] +func (h *{Entity}Handler) Disable{Entity}(c *gin.Context) { + id := c.Param("id") + if id == "" { + h.responseBuilder.BadRequest(c, "{Entity}ID不能为空") + return + } + + var cmd commands.Disable{Entity}Command + cmd.ID = id + + if err := h.appService.Disable{Entity}(c.Request.Context(), &cmd); err != nil { + h.logger.Error("禁用{Entity}失败", zap.Error(err), zap.String("{entity}_id", id)) + h.responseBuilder.BadRequest(c, err.Error()) + return + } + + h.responseBuilder.Success(c, nil, "禁用{Entity}成功") +} + +// Get{Entity}Stats 获取统计信息 +// @Summary 获取{Entity}统计信息 +// @Description 获取{Entity}相关的统计信息 +// @Tags {Entity}管理 +// @Accept json +// @Produce json +// @Success 200 {object} responses.{Entity}StatsResponse "获取统计成功" +// @Failure 500 {object} map[string]interface{} "服务器内部错误" +// @Router /api/v1/{entity}s/stats [get] +func (h *{Entity}Handler) Get{Entity}Stats(c *gin.Context) { + result, err := h.appService.Get{Entity}Stats(c.Request.Context()) + if err != nil { + h.logger.Error("获取{Entity}统计失败", zap.Error(err)) + h.responseBuilder.InternalError(c, "获取{Entity}统计失败") + return + } + + h.responseBuilder.Success(c, result, "获取{Entity}统计成功") +} +``` + +### 第七步:创建路由 (Routes) + +#### 7.1 路由配置 +```go +// internal/infrastructure/http/routes/{entity}_routes.go +package routes + +import ( + "tyapi-server/internal/infrastructure/http/handlers" + "tyapi-server/internal/shared/http" + + "github.com/gin-gonic/gin" +) + +// Register{Entity}Routes 注册{Entity}路由 +func Register{Entity}Routes(router *gin.RouterGroup, handler *handlers.{Entity}Handler) { + {entity}Group := router.Group("/{entity}s") + { + // 基础CRUD操作 + {entity}Group.GET("", handler.List{Entity}s) // 获取列表 + {entity}Group.GET("/:id", handler.Get{Entity}Detail) // 获取详情 + {entity}Group.POST("", handler.Create{Entity}) // 创建 + {entity}Group.PUT("/:id", handler.Update{Entity}) // 更新 + {entity}Group.DELETE("/:id", handler.Delete{Entity}) // 删除 + + // 业务操作 + {entity}Group.POST("/:id/enable", handler.Enable{Entity}) // 启用 + {entity}Group.POST("/:id/disable", handler.Disable{Entity}) // 禁用 + + // 统计信息 + {entity}Group.GET("/stats", handler.Get{Entity}Stats) // 统计 + } +} +``` + +### 第八步:配置依赖注入 (Dependency Injection) + +#### 8.1 容器配置 +```go +// internal/container/container.go 中添加 +func (c *Container) register{Entity}Services() { + // 注册仓储 + c.{entity}Repository = database.NewGorm{Entity}Repository(c.db, c.logger) + + // 注册应用服务 + c.{entity}ApplicationService = application.New{Entity}ApplicationService( + c.{entity}Repository, + c.logger, + ) + + // 注册HTTP处理器 + c.{entity}Handler = handlers.New{Entity}Handler( + c.{entity}ApplicationService, + c.responseBuilder, + c.logger, + ) +} + +// 在registerAllServices方法中调用 +func (c *Container) registerAllServices() { + // ... 其他服务注册 + c.register{Entity}Services() +} + +// 在registerAllRoutes方法中注册路由 +func (c *Container) registerAllRoutes() { + // ... 其他路由注册 + routes.Register{Entity}Routes(c.apiGroup, c.{entity}Handler) +} +``` + +## 测试指南 + +### 单元测试 + +#### 1. 实体测试 +```go +// internal/domains/{domain}/entities/{entity}_test.go +package entities + +import ( + "testing" + "time" +) + +func Test{Entity}_IsValid(t *testing.T) { + tests := []struct { + name string + {entity} {Entity} + want bool + }{ + { + name: "有效的{Entity}", + {entity}: {Entity}{ + ID: "test-id", + Name: "测试{Entity}", + IsEnabled: true, + }, + want: true, + }, + { + name: "禁用的{Entity}", + {entity}: {Entity}{ + ID: "test-id", + Name: "测试{Entity}", + IsEnabled: false, + }, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.{entity}.IsValid(); got != tt.want { + t.Errorf("{Entity}.IsValid() = %v, want %v", got, tt.want) + } + }) + } +} +``` + +#### 2. 应用服务测试 +```go +// internal/application/{domain}/{entity}_application_service_test.go +package {domain} + +import ( + "context" + "testing" + "tyapi-server/internal/application/{domain}/dto/commands" + "tyapi-server/internal/domains/{domain}/entities" + + "github.com/stretchr/testify/mock" + "go.uber.org/zap" +) + +// Mock{Entity}Repository 模拟仓储 +type Mock{Entity}Repository struct { + mock.Mock +} + +func (m *Mock{Entity}Repository) Create(ctx context.Context, entity entities.{Entity}) (entities.{Entity}, error) { + args := m.Called(ctx, entity) + return args.Get(0).(entities.{Entity}), args.Error(1) +} + +// 其他方法实现... + +func Test{Entity}ApplicationService_Create{Entity}(t *testing.T) { + mockRepo := new(Mock{Entity}Repository) + logger := zap.NewNop() + + service := &{Entity}ApplicationServiceImpl{ + {entity}Repo: mockRepo, + logger: logger, + } + + cmd := &commands.Create{Entity}Command{ + Name: "测试{Entity}", + Description: "测试描述", + IsEnabled: true, + } + + expected{Entity} := entities.{Entity}{ + Name: cmd.Name, + Description: cmd.Description, + IsEnabled: cmd.IsEnabled, + } + + mockRepo.On("Create", mock.Anything, expected{Entity}).Return(expected{Entity}, nil) + + err := service.Create{Entity}(context.Background(), cmd) + + assert.NoError(t, err) + mockRepo.AssertExpectations(t) +} +``` + +### 集成测试 + +#### 1. HTTP处理器测试 +```go +// internal/infrastructure/http/handlers/{entity}_handler_test.go +package handlers + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "tyapi-server/internal/application/{domain}/dto/commands" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func Test{Entity}Handler_Create{Entity}(t *testing.T) { + gin.SetMode(gin.TestMode) + + mockAppService := new(Mock{Entity}ApplicationService) + mockResponseBuilder := new(MockResponseBuilder) + + handler := &{Entity}Handler{ + appService: mockAppService, + responseBuilder: mockResponseBuilder, + } + + cmd := commands.Create{Entity}Command{ + Name: "测试{Entity}", + Description: "测试描述", + IsEnabled: true, + } + + body, _ := json.Marshal(cmd) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest("POST", "/{entity}s", bytes.NewBuffer(body)) + c.Request.Header.Set("Content-Type", "application/json") + + mockAppService.On("Create{Entity}", mock.Anything, &cmd).Return(nil) + mockResponseBuilder.On("Success", c, nil, "创建{Entity}成功").Return() + + handler.Create{Entity}(c) + + assert.Equal(t, http.StatusOK, w.Code) + mockAppService.AssertExpectations(t) + mockResponseBuilder.AssertExpectations(t) +} +``` + +## 最佳实践 + +### 1. 命名规范 + +#### 实体命名 +- 实体名称使用单数形式,首字母大写 +- 文件名使用小写,下划线分隔 +- 示例:`Product` → `product.go` + +#### 仓储命名 +- 接口名称:`{Entity}Repository` +- 实现名称:`Gorm{Entity}Repository` +- 文件名:`{entity}_repository_interface.go`、`gorm_{entity}_repository.go` + +#### 应用服务命名 +- 接口名称:`{Entity}ApplicationService` +- 实现名称:`{Entity}ApplicationServiceImpl` +- 文件名:`{entity}_application_service.go`、`{entity}_application_service_impl.go` + +#### HTTP处理器命名 +- 结构体名称:`{Entity}Handler` +- 文件名:`{entity}_handler.go` + +### 2. 错误处理 + +#### 统一错误响应 +```go +// 使用统一的响应构建器 +h.responseBuilder.BadRequest(c, "请求参数错误") +h.responseBuilder.NotFound(c, "{Entity}不存在") +h.responseBuilder.InternalError(c, "服务器内部错误") +``` + +#### 业务错误处理 +```go +// 在应用服务中抛出业务错误 +if entity.IsDeleted() { + return fmt.Errorf("{Entity}已被删除") +} + +if !entity.IsEnabled { + return fmt.Errorf("{Entity}已被禁用") +} +``` + +### 3. 日志记录 + +#### 结构化日志 +```go +// 使用结构化字段 +logger.Info("创建{Entity}成功", + zap.String("id", entity.ID), + zap.String("name", entity.Name), +) + +logger.Error("删除{Entity}失败", + zap.Error(err), + zap.String("id", id), +) +``` + +#### 日志级别 +- `Info`: 业务操作成功 +- `Warn`: 业务警告(如重复操作) +- `Error`: 业务错误和系统错误 +- `Debug`: 调试信息 + +### 4. 参数验证 + +#### 请求参数验证 +```go +// 使用binding标签进行验证 +type Create{Entity}Command struct { + Name string `json:"name" binding:"required,min=1,max=100" comment:"名称"` + Description string `json:"description" binding:"max=500" comment:"描述"` + IsEnabled bool `json:"is_enabled" comment:"是否启用"` +} +``` + +#### 业务规则验证 +```go +// 在应用服务中进行业务规则验证 +func (s *{Entity}ApplicationServiceImpl) validateCreate{Entity}(cmd *commands.Create{Entity}Command) error { + if cmd.Name == "" { + return fmt.Errorf("名称不能为空") + } + + if len(cmd.Name) > 100 { + return fmt.Errorf("名称长度不能超过100个字符") + } + + return nil +} +``` + +### 5. 数据库设计 + +#### 表结构规范 +```go +type {Entity} struct { + ID string `gorm:"primaryKey;type:varchar(36)" comment:"ID"` + Name string `gorm:"type:varchar(100);not null;index" comment:"名称"` + Description string `gorm:"type:text" comment:"描述"` + IsEnabled bool `gorm:"default:true;index" comment:"是否启用"` + IsVisible bool `gorm:"default:true" comment:"是否可见"` + + CreatedAt time.Time `gorm:"autoCreateTime;index" comment:"创建时间"` + UpdatedAt time.Time `gorm:"autoUpdateTime" comment:"更新时间"` + DeletedAt gorm.DeletedAt `gorm:"index" comment:"软删除时间"` +} +``` + +#### 索引设计 +- 主键:`ID` +- 业务索引:`Name`、`IsEnabled`、`CreatedAt` +- 软删除索引:`DeletedAt` + +### 6. API设计 + +#### RESTful规范 +``` +GET /api/v1/{entity}s # 获取列表 +GET /api/v1/{entity}s/:id # 获取详情 +POST /api/v1/{entity}s # 创建 +PUT /api/v1/{entity}s/:id # 更新 +DELETE /api/v1/{entity}s/:id # 删除 +POST /api/v1/{entity}s/:id/enable # 启用 +POST /api/v1/{entity}s/:id/disable # 禁用 +GET /api/v1/{entity}s/stats # 统计 +``` + +#### 查询参数 +``` +GET /api/v1/{entity}s?page=1&page_size=10&status=active&sort_by=created_at&sort_order=desc +``` + +#### 响应格式 +```json +{ + "code": 200, + "message": "操作成功", + "data": { + "total": 100, + "page": 1, + "size": 10, + "items": [...] + } +} +``` + +### 7. 性能优化 + +#### 分页查询 +```go +// 使用LIMIT和OFFSET进行分页 +dbQuery = dbQuery.Offset(offset).Limit(pageSize) +``` + +#### 索引优化 +```go +// 为常用查询字段添加索引 +`gorm:"index"` // 普通索引 +`gorm:"uniqueIndex"` // 唯一索引 +`gorm:"index:idx_name_status"` // 复合索引 +``` + +#### 缓存策略 +```go +// 对于频繁查询的数据使用缓存 +func (r *Gorm{Entity}Repository) GetByID(ctx context.Context, id string) (entities.{Entity}, error) { + // 先从缓存获取 + if cached, err := r.cache.Get(ctx, "entity:"+id); err == nil { + return cached, nil + } + + // 从数据库获取 + var entity entities.{Entity} + err := r.db.WithContext(ctx).Where("id = ?", id).First(&entity).Error + if err != nil { + return entity, err + } + + // 存入缓存 + r.cache.Set(ctx, "entity:"+id, entity, time.Hour) + return entity, nil +} +``` + +## 常见问题 + +### 1. 循环依赖 +**问题**: 领域层和应用层之间出现循环依赖 +**解决**: 使用接口解耦,领域层定义接口,应用层实现接口 + +### 2. 事务管理 +**问题**: 跨仓储操作需要事务支持 +**解决**: 在应用服务层使用事务装饰器 +```go +func (s *{Entity}ApplicationServiceImpl) Create{Entity}WithTransaction(ctx context.Context, cmd *commands.Create{Entity}Command) error { + return s.db.Transaction(func(tx *gorm.DB) error { + // 事务操作 + return nil + }) +} +``` + +### 3. 并发控制 +**问题**: 高并发场景下的数据一致性问题 +**解决**: 使用乐观锁或悲观锁 +```go +// 乐观锁 +type {Entity} struct { + Version int `gorm:"default:1" comment:"版本号"` +} + +// 悲观锁 +func (r *Gorm{Entity}Repository) GetByIDForUpdate(ctx context.Context, id string) (entities.{Entity}, error) { + var entity entities.{Entity} + err := r.db.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", id).First(&entity).Error + return entity, err +} +``` + +### 4. 数据迁移 +**问题**: 实体结构变更需要数据库迁移 +**解决**: 使用GORM的AutoMigrate或手动编写迁移脚本 +```go +// 自动迁移 +db.AutoMigrate(&entities.{Entity}{}) + +// 手动迁移 +db.Exec("ALTER TABLE {entity}s ADD COLUMN new_field VARCHAR(255)") +``` + +## 总结 + +本文档提供了在tyapi-server项目中新增领域或领域内功能的完整指南。遵循DDD架构模式,确保代码的可维护性、可扩展性和可测试性。 + +关键要点: +1. **分层架构**: 严格遵循领域层、应用层、基础设施层的分层原则 +2. **依赖倒置**: 通过接口实现依赖倒置,降低耦合度 +3. **单一职责**: 每个组件只负责自己的职责 +4. **开闭原则**: 对扩展开放,对修改关闭 +5. **测试驱动**: 编写充分的单元测试和集成测试 + +通过遵循本指南,可以快速、规范地在项目中新增领域功能,保持代码的一致性和质量。 \ No newline at end of file diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index b4371cc..4b70539 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -176,7 +176,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "管理员认证" + "管理员管理" ], "summary": "管理员登录", "parameters": [ @@ -540,6 +540,103 @@ const docTemplate = `{ } } }, + "/api/v1/categories": { + "get": { + "description": "获取产品分类列表,支持层级筛选", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "数据大厅" + ], + "summary": "获取分类列表", + "parameters": [ + { + "type": "string", + "description": "父级分类ID", + "name": "parent_id", + "in": "query" + }, + { + "type": "integer", + "description": "分类层级", + "name": "level", + "in": "query" + } + ], + "responses": { + "200": { + "description": "获取分类列表成功", + "schema": { + "$ref": "#/definitions/responses.CategoryListResponse" + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/v1/categories/{id}": { + "get": { + "description": "根据分类ID获取分类详细信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "数据大厅" + ], + "summary": "获取分类详情", + "parameters": [ + { + "type": "string", + "description": "分类ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "获取分类详情成功", + "schema": { + "$ref": "#/definitions/responses.CategoryInfoResponse" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "分类不存在", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/v1/certification": { "post": { "security": [ @@ -1095,7 +1192,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "认证管理" + "企业认证" ], "summary": "上传营业执照并同步OCR识别", "parameters": [ @@ -1734,6 +1831,534 @@ const docTemplate = `{ } } }, + "/api/v1/my/subscriptions": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "获取当前用户的订阅列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "我的订阅" + ], + "summary": "获取我的订阅列表", + "parameters": [ + { + "type": "integer", + "default": 1, + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "每页数量", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "订阅状态", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "排序字段", + "name": "sort_by", + "in": "query" + }, + { + "enum": [ + "asc", + "desc" + ], + "type": "string", + "description": "排序方向", + "name": "sort_order", + "in": "query" + } + ], + "responses": { + "200": { + "description": "获取订阅列表成功", + "schema": { + "$ref": "#/definitions/responses.SubscriptionListResponse" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "未认证", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/v1/my/subscriptions/stats": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "获取当前用户的订阅统计信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "我的订阅" + ], + "summary": "获取我的订阅统计", + "responses": { + "200": { + "description": "获取订阅统计成功", + "schema": { + "$ref": "#/definitions/responses.SubscriptionStatsResponse" + } + }, + "401": { + "description": "未认证", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/v1/my/subscriptions/{id}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "获取指定订阅的详细信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "我的订阅" + ], + "summary": "获取我的订阅详情", + "parameters": [ + { + "type": "string", + "description": "订阅ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "获取订阅详情成功", + "schema": { + "$ref": "#/definitions/responses.SubscriptionInfoResponse" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "未认证", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "订阅不存在", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/v1/my/subscriptions/{id}/usage": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "获取指定订阅的使用情况统计", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "我的订阅" + ], + "summary": "获取我的订阅使用情况", + "parameters": [ + { + "type": "string", + "description": "订阅ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "获取使用情况成功", + "schema": { + "$ref": "#/definitions/responses.SubscriptionUsageResponse" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "未认证", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "订阅不存在", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/v1/products": { + "get": { + "description": "分页获取可用的产品列表,支持筛选", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "数据大厅" + ], + "summary": "获取产品列表", + "parameters": [ + { + "type": "integer", + "default": 1, + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "每页数量", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "搜索关键词", + "name": "keyword", + "in": "query" + }, + { + "type": "string", + "description": "分类ID", + "name": "category_id", + "in": "query" + }, + { + "type": "number", + "description": "最低价格", + "name": "min_price", + "in": "query" + }, + { + "type": "number", + "description": "最高价格", + "name": "max_price", + "in": "query" + }, + { + "type": "boolean", + "description": "是否启用", + "name": "is_enabled", + "in": "query" + }, + { + "type": "boolean", + "description": "是否可见", + "name": "is_visible", + "in": "query" + }, + { + "type": "boolean", + "description": "是否组合包", + "name": "is_package", + "in": "query" + }, + { + "type": "string", + "description": "排序字段", + "name": "sort_by", + "in": "query" + }, + { + "enum": [ + "asc", + "desc" + ], + "type": "string", + "description": "排序方向", + "name": "sort_order", + "in": "query" + } + ], + "responses": { + "200": { + "description": "获取产品列表成功", + "schema": { + "$ref": "#/definitions/responses.ProductListResponse" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/v1/products/stats": { + "get": { + "description": "获取产品相关的统计信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "数据大厅" + ], + "summary": "获取产品统计", + "responses": { + "200": { + "description": "获取统计信息成功", + "schema": { + "$ref": "#/definitions/responses.ProductStatsResponse" + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/v1/products/{id}": { + "get": { + "description": "根据产品ID获取产品详细信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "数据大厅" + ], + "summary": "获取产品详情", + "parameters": [ + { + "type": "string", + "description": "产品ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "获取产品详情成功", + "schema": { + "$ref": "#/definitions/responses.ProductInfoResponse" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "产品不存在", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/v1/products/{id}/subscribe": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "用户订阅指定产品", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "数据大厅" + ], + "summary": "订阅产品", + "parameters": [ + { + "type": "string", + "description": "产品ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "订阅请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/commands.CreateSubscriptionCommand" + } + } + ], + "responses": { + "200": { + "description": "订阅成功", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "未认证", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "产品不存在", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/v1/users/login-password": { "post": { "description": "使用手机号和密码进行用户登录,返回JWT令牌", @@ -2009,6 +2634,62 @@ const docTemplate = `{ } } }, + "/api/v1/users/reset-password": { + "post": { + "description": "使用手机号、验证码和新密码重置用户密码(忘记密码时使用)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户认证" + ], + "summary": "重置密码", + "parameters": [ + { + "description": "重置密码请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/commands.ResetPasswordCommand" + } + } + ], + "responses": { + "200": { + "description": "密码重置成功", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "请求参数错误或验证码无效", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "用户不存在", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/v1/users/send-code": { "post": { "description": "向指定手机号发送验证码,支持注册、登录、修改密码等场景", @@ -2163,6 +2844,34 @@ const docTemplate = `{ } } }, + "commands.CreateSubscriptionCommand": { + "type": "object", + "required": [ + "product_id", + "user_id" + ], + "properties": { + "api_limit": { + "type": "integer" + }, + "auto_renew": { + "type": "boolean" + }, + "duration": { + "type": "string" + }, + "price": { + "type": "number", + "minimum": 0 + }, + "product_id": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, "commands.CreateUserSecretsCommand": { "type": "object", "required": [ @@ -2288,6 +2997,36 @@ const docTemplate = `{ } } }, + "commands.ResetPasswordCommand": { + "description": "重置用户密码请求参数(忘记密码时使用)", + "type": "object", + "required": [ + "code", + "confirm_new_password", + "new_password", + "phone" + ], + "properties": { + "code": { + "type": "string", + "example": "123456" + }, + "confirm_new_password": { + "type": "string", + "example": "newpassword123" + }, + "new_password": { + "type": "string", + "maxLength": 128, + "minLength": 6, + "example": "newpassword123" + }, + "phone": { + "type": "string", + "example": "13800138000" + } + } + }, "commands.SendCodeCommand": { "description": "发送短信验证码请求参数", "type": "object", @@ -2668,6 +3407,78 @@ const docTemplate = `{ } } }, + "responses.CategoryInfoResponse": { + "type": "object", + "properties": { + "children": { + "type": "array", + "items": { + "$ref": "#/definitions/responses.CategoryInfoResponse" + } + }, + "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": { + "description": "关联信息", + "allOf": [ + { + "$ref": "#/definitions/responses.CategoryInfoResponse" + } + ] + }, + "parent_id": { + "type": "string" + }, + "sort": { + "type": "integer" + }, + "updated_at": { + "type": "string" + } + } + }, + "responses.CategoryListResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/responses.CategoryInfoResponse" + } + }, + "page": { + "type": "integer" + }, + "size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, "responses.CertificationResponse": { "type": "object", "properties": { @@ -2769,6 +3580,125 @@ const docTemplate = `{ } } }, + "responses.ProductInfoResponse": { + "type": "object", + "properties": { + "category": { + "description": "关联信息", + "allOf": [ + { + "$ref": "#/definitions/responses.CategoryInfoResponse" + } + ] + }, + "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" + } + } + }, + "responses.ProductListResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/responses.ProductInfoResponse" + } + }, + "page": { + "type": "integer" + }, + "size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "responses.ProductSimpleResponse": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "is_package": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "price": { + "type": "number" + } + } + }, + "responses.ProductStatsResponse": { + "type": "object", + "properties": { + "enabled_products": { + "type": "integer" + }, + "package_products": { + "type": "integer" + }, + "total_products": { + "type": "integer" + }, + "visible_products": { + "type": "integer" + } + } + }, "responses.RegisterUserResponse": { "description": "用户注册成功响应", "type": "object", @@ -2783,6 +3713,85 @@ const docTemplate = `{ } } }, + "responses.SubscriptionInfoResponse": { + "type": "object", + "properties": { + "api_used": { + "type": "integer" + }, + "created_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "price": { + "type": "number" + }, + "product": { + "description": "关联信息", + "allOf": [ + { + "$ref": "#/definitions/responses.ProductSimpleResponse" + } + ] + }, + "product_id": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, + "responses.SubscriptionListResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/responses.SubscriptionInfoResponse" + } + }, + "page": { + "type": "integer" + }, + "size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "responses.SubscriptionStatsResponse": { + "type": "object", + "properties": { + "total_revenue": { + "type": "number" + }, + "total_subscriptions": { + "type": "integer" + } + } + }, + "responses.SubscriptionUsageResponse": { + "type": "object", + "properties": { + "api_used": { + "type": "integer" + }, + "id": { + "type": "string" + }, + "product_id": { + "type": "string" + } + } + }, "responses.TransactionResponse": { "type": "object", "properties": { @@ -2860,6 +3869,10 @@ const docTemplate = `{ "type": "string", "example": "123e4567-e89b-12d3-a456-426614174000" }, + "is_certified": { + "type": "boolean", + "example": false + }, "phone": { "type": "string", "example": "13800138000" diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 9b2f91f..9d07b19 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -170,7 +170,7 @@ "application/json" ], "tags": [ - "管理员认证" + "管理员管理" ], "summary": "管理员登录", "parameters": [ @@ -534,6 +534,103 @@ } } }, + "/api/v1/categories": { + "get": { + "description": "获取产品分类列表,支持层级筛选", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "数据大厅" + ], + "summary": "获取分类列表", + "parameters": [ + { + "type": "string", + "description": "父级分类ID", + "name": "parent_id", + "in": "query" + }, + { + "type": "integer", + "description": "分类层级", + "name": "level", + "in": "query" + } + ], + "responses": { + "200": { + "description": "获取分类列表成功", + "schema": { + "$ref": "#/definitions/responses.CategoryListResponse" + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/v1/categories/{id}": { + "get": { + "description": "根据分类ID获取分类详细信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "数据大厅" + ], + "summary": "获取分类详情", + "parameters": [ + { + "type": "string", + "description": "分类ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "获取分类详情成功", + "schema": { + "$ref": "#/definitions/responses.CategoryInfoResponse" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "分类不存在", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/v1/certification": { "post": { "security": [ @@ -1089,7 +1186,7 @@ "application/json" ], "tags": [ - "认证管理" + "企业认证" ], "summary": "上传营业执照并同步OCR识别", "parameters": [ @@ -1728,6 +1825,534 @@ } } }, + "/api/v1/my/subscriptions": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "获取当前用户的订阅列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "我的订阅" + ], + "summary": "获取我的订阅列表", + "parameters": [ + { + "type": "integer", + "default": 1, + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "每页数量", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "订阅状态", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "排序字段", + "name": "sort_by", + "in": "query" + }, + { + "enum": [ + "asc", + "desc" + ], + "type": "string", + "description": "排序方向", + "name": "sort_order", + "in": "query" + } + ], + "responses": { + "200": { + "description": "获取订阅列表成功", + "schema": { + "$ref": "#/definitions/responses.SubscriptionListResponse" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "未认证", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/v1/my/subscriptions/stats": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "获取当前用户的订阅统计信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "我的订阅" + ], + "summary": "获取我的订阅统计", + "responses": { + "200": { + "description": "获取订阅统计成功", + "schema": { + "$ref": "#/definitions/responses.SubscriptionStatsResponse" + } + }, + "401": { + "description": "未认证", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/v1/my/subscriptions/{id}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "获取指定订阅的详细信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "我的订阅" + ], + "summary": "获取我的订阅详情", + "parameters": [ + { + "type": "string", + "description": "订阅ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "获取订阅详情成功", + "schema": { + "$ref": "#/definitions/responses.SubscriptionInfoResponse" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "未认证", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "订阅不存在", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/v1/my/subscriptions/{id}/usage": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "获取指定订阅的使用情况统计", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "我的订阅" + ], + "summary": "获取我的订阅使用情况", + "parameters": [ + { + "type": "string", + "description": "订阅ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "获取使用情况成功", + "schema": { + "$ref": "#/definitions/responses.SubscriptionUsageResponse" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "未认证", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "订阅不存在", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/v1/products": { + "get": { + "description": "分页获取可用的产品列表,支持筛选", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "数据大厅" + ], + "summary": "获取产品列表", + "parameters": [ + { + "type": "integer", + "default": 1, + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "每页数量", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "搜索关键词", + "name": "keyword", + "in": "query" + }, + { + "type": "string", + "description": "分类ID", + "name": "category_id", + "in": "query" + }, + { + "type": "number", + "description": "最低价格", + "name": "min_price", + "in": "query" + }, + { + "type": "number", + "description": "最高价格", + "name": "max_price", + "in": "query" + }, + { + "type": "boolean", + "description": "是否启用", + "name": "is_enabled", + "in": "query" + }, + { + "type": "boolean", + "description": "是否可见", + "name": "is_visible", + "in": "query" + }, + { + "type": "boolean", + "description": "是否组合包", + "name": "is_package", + "in": "query" + }, + { + "type": "string", + "description": "排序字段", + "name": "sort_by", + "in": "query" + }, + { + "enum": [ + "asc", + "desc" + ], + "type": "string", + "description": "排序方向", + "name": "sort_order", + "in": "query" + } + ], + "responses": { + "200": { + "description": "获取产品列表成功", + "schema": { + "$ref": "#/definitions/responses.ProductListResponse" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/v1/products/stats": { + "get": { + "description": "获取产品相关的统计信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "数据大厅" + ], + "summary": "获取产品统计", + "responses": { + "200": { + "description": "获取统计信息成功", + "schema": { + "$ref": "#/definitions/responses.ProductStatsResponse" + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/v1/products/{id}": { + "get": { + "description": "根据产品ID获取产品详细信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "数据大厅" + ], + "summary": "获取产品详情", + "parameters": [ + { + "type": "string", + "description": "产品ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "获取产品详情成功", + "schema": { + "$ref": "#/definitions/responses.ProductInfoResponse" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "产品不存在", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/v1/products/{id}/subscribe": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "用户订阅指定产品", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "数据大厅" + ], + "summary": "订阅产品", + "parameters": [ + { + "type": "string", + "description": "产品ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "订阅请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/commands.CreateSubscriptionCommand" + } + } + ], + "responses": { + "200": { + "description": "订阅成功", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "未认证", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "产品不存在", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/v1/users/login-password": { "post": { "description": "使用手机号和密码进行用户登录,返回JWT令牌", @@ -2003,6 +2628,62 @@ } } }, + "/api/v1/users/reset-password": { + "post": { + "description": "使用手机号、验证码和新密码重置用户密码(忘记密码时使用)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户认证" + ], + "summary": "重置密码", + "parameters": [ + { + "description": "重置密码请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/commands.ResetPasswordCommand" + } + } + ], + "responses": { + "200": { + "description": "密码重置成功", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "请求参数错误或验证码无效", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "用户不存在", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/v1/users/send-code": { "post": { "description": "向指定手机号发送验证码,支持注册、登录、修改密码等场景", @@ -2157,6 +2838,34 @@ } } }, + "commands.CreateSubscriptionCommand": { + "type": "object", + "required": [ + "product_id", + "user_id" + ], + "properties": { + "api_limit": { + "type": "integer" + }, + "auto_renew": { + "type": "boolean" + }, + "duration": { + "type": "string" + }, + "price": { + "type": "number", + "minimum": 0 + }, + "product_id": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, "commands.CreateUserSecretsCommand": { "type": "object", "required": [ @@ -2282,6 +2991,36 @@ } } }, + "commands.ResetPasswordCommand": { + "description": "重置用户密码请求参数(忘记密码时使用)", + "type": "object", + "required": [ + "code", + "confirm_new_password", + "new_password", + "phone" + ], + "properties": { + "code": { + "type": "string", + "example": "123456" + }, + "confirm_new_password": { + "type": "string", + "example": "newpassword123" + }, + "new_password": { + "type": "string", + "maxLength": 128, + "minLength": 6, + "example": "newpassword123" + }, + "phone": { + "type": "string", + "example": "13800138000" + } + } + }, "commands.SendCodeCommand": { "description": "发送短信验证码请求参数", "type": "object", @@ -2662,6 +3401,78 @@ } } }, + "responses.CategoryInfoResponse": { + "type": "object", + "properties": { + "children": { + "type": "array", + "items": { + "$ref": "#/definitions/responses.CategoryInfoResponse" + } + }, + "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": { + "description": "关联信息", + "allOf": [ + { + "$ref": "#/definitions/responses.CategoryInfoResponse" + } + ] + }, + "parent_id": { + "type": "string" + }, + "sort": { + "type": "integer" + }, + "updated_at": { + "type": "string" + } + } + }, + "responses.CategoryListResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/responses.CategoryInfoResponse" + } + }, + "page": { + "type": "integer" + }, + "size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, "responses.CertificationResponse": { "type": "object", "properties": { @@ -2763,6 +3574,125 @@ } } }, + "responses.ProductInfoResponse": { + "type": "object", + "properties": { + "category": { + "description": "关联信息", + "allOf": [ + { + "$ref": "#/definitions/responses.CategoryInfoResponse" + } + ] + }, + "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" + } + } + }, + "responses.ProductListResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/responses.ProductInfoResponse" + } + }, + "page": { + "type": "integer" + }, + "size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "responses.ProductSimpleResponse": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "is_package": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "price": { + "type": "number" + } + } + }, + "responses.ProductStatsResponse": { + "type": "object", + "properties": { + "enabled_products": { + "type": "integer" + }, + "package_products": { + "type": "integer" + }, + "total_products": { + "type": "integer" + }, + "visible_products": { + "type": "integer" + } + } + }, "responses.RegisterUserResponse": { "description": "用户注册成功响应", "type": "object", @@ -2777,6 +3707,85 @@ } } }, + "responses.SubscriptionInfoResponse": { + "type": "object", + "properties": { + "api_used": { + "type": "integer" + }, + "created_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "price": { + "type": "number" + }, + "product": { + "description": "关联信息", + "allOf": [ + { + "$ref": "#/definitions/responses.ProductSimpleResponse" + } + ] + }, + "product_id": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, + "responses.SubscriptionListResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/responses.SubscriptionInfoResponse" + } + }, + "page": { + "type": "integer" + }, + "size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "responses.SubscriptionStatsResponse": { + "type": "object", + "properties": { + "total_revenue": { + "type": "number" + }, + "total_subscriptions": { + "type": "integer" + } + } + }, + "responses.SubscriptionUsageResponse": { + "type": "object", + "properties": { + "api_used": { + "type": "integer" + }, + "id": { + "type": "string" + }, + "product_id": { + "type": "string" + } + } + }, "responses.TransactionResponse": { "type": "object", "properties": { @@ -2854,6 +3863,10 @@ "type": "string", "example": "123e4567-e89b-12d3-a456-426614174000" }, + "is_certified": { + "type": "boolean", + "example": false + }, "phone": { "type": "string", "example": "13800138000" diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index 0345be5..e029582 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -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: diff --git a/internal/application/product/category_application_service_impl.go b/internal/application/product/category_application_service_impl.go new file mode 100644 index 0000000..69386bd --- /dev/null +++ b/internal/application/product/category_application_service_impl.go @@ -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 +} \ No newline at end of file diff --git a/internal/application/product/dto/commands/category_commands.go b/internal/application/product/dto/commands/category_commands.go new file mode 100644 index 0000000..86df6ab --- /dev/null +++ b/internal/application/product/dto/commands/category_commands.go @@ -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:"新的排序"` +} \ No newline at end of file diff --git a/internal/application/product/dto/commands/product_commands.go b/internal/application/product/dto/commands/product_commands.go new file mode 100644 index 0000000..97a6088 --- /dev/null +++ b/internal/application/product/dto/commands/product_commands.go @@ -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关键词"` +} \ No newline at end of file diff --git a/internal/application/product/dto/commands/subscription_commands.go b/internal/application/product/dto/commands/subscription_commands.go new file mode 100644 index 0000000..360f624 --- /dev/null +++ b/internal/application/product/dto/commands/subscription_commands.go @@ -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调用限制"` +} \ No newline at end of file diff --git a/internal/application/product/dto/queries/category_queries.go b/internal/application/product/dto/queries/category_queries.go new file mode 100644 index 0000000..6109db2 --- /dev/null +++ b/internal/application/product/dto/queries/category_queries.go @@ -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"` +} \ No newline at end of file diff --git a/internal/application/product/dto/queries/product_queries.go b/internal/application/product/dto/queries/product_queries.go new file mode 100644 index 0000000..3c9da69 --- /dev/null +++ b/internal/application/product/dto/queries/product_queries.go @@ -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"` +} \ No newline at end of file diff --git a/internal/application/product/dto/queries/subscription_queries.go b/internal/application/product/dto/queries/subscription_queries.go new file mode 100644 index 0000000..c6dc6af --- /dev/null +++ b/internal/application/product/dto/queries/subscription_queries.go @@ -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"` +} diff --git a/internal/application/product/dto/responses/category_responses.go b/internal/application/product/dto/responses/category_responses.go new file mode 100644 index 0000000..06c358b --- /dev/null +++ b/internal/application/product/dto/responses/category_responses.go @@ -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:"可见分类数"` +} \ No newline at end of file diff --git a/internal/application/product/dto/responses/product_responses.go b/internal/application/product/dto/responses/product_responses.go new file mode 100644 index 0000000..1e05d94 --- /dev/null +++ b/internal/application/product/dto/responses/product_responses.go @@ -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:"组合包产品数"` +} \ No newline at end of file diff --git a/internal/application/product/dto/responses/subscription_responses.go b/internal/application/product/dto/responses/subscription_responses.go new file mode 100644 index 0000000..10c9538 --- /dev/null +++ b/internal/application/product/dto/responses/subscription_responses.go @@ -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:"总收入"` +} \ No newline at end of file diff --git a/internal/application/product/product_application_service.go b/internal/application/product/product_application_service.go new file mode 100644 index 0000000..5746a68 --- /dev/null +++ b/internal/application/product/product_application_service.go @@ -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) +} \ No newline at end of file diff --git a/internal/application/product/product_application_service_impl.go b/internal/application/product/product_application_service_impl.go new file mode 100644 index 0000000..1b2079f --- /dev/null +++ b/internal/application/product/product_application_service_impl.go @@ -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, + } +} diff --git a/internal/application/product/subscription_application_service_impl.go b/internal/application/product/subscription_application_service_impl.go new file mode 100644 index 0000000..68e0159 --- /dev/null +++ b/internal/application/product/subscription_application_service_impl.go @@ -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, + } +} \ No newline at end of file diff --git a/internal/application/user/dto/commands/user_commands.go b/internal/application/user/dto/commands/user_commands.go index 90ea008..5becc62 100644 --- a/internal/application/user/dto/commands/user_commands.go +++ b/internal/application/user/dto/commands/user_commands.go @@ -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 { diff --git a/internal/application/user/dto/responses/user_responses.go b/internal/application/user/dto/responses/user_responses.go index 2bf9125..8318640 100644 --- a/internal/application/user/dto/responses/user_responses.go +++ b/internal/application/user/dto/responses/user_responses.go @@ -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"` } diff --git a/internal/application/user/user_application_service.go b/internal/application/user/user_application_service.go index 1cf33f5..f443b89 100644 --- a/internal/application/user/user_application_service.go +++ b/internal/application/user/user_application_service.go @@ -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 } diff --git a/internal/application/user/user_application_service_impl.go b/internal/application/user/user_application_service_impl.go index de1a22d..b91e35c 100644 --- a/internal/application/user/user_application_service_impl.go +++ b/internal/application/user/user_application_service_impl.go @@ -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 diff --git a/internal/container/container.go b/internal/container/container.go index ca86e7b..e0474c5 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -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() diff --git a/internal/domains/product/entities/product.go b/internal/domains/product/entities/product.go new file mode 100644 index 0000000..afa7f17 --- /dev/null +++ b/internal/domains/product/entities/product.go @@ -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 +} \ No newline at end of file diff --git a/internal/domains/product/entities/product_category.go b/internal/domains/product/entities/product_category.go new file mode 100644 index 0000000..e428773 --- /dev/null +++ b/internal/domains/product/entities/product_category.go @@ -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 +} \ No newline at end of file diff --git a/internal/domains/product/entities/product_documentation.go b/internal/domains/product/entities/product_documentation.go new file mode 100644 index 0000000..2c7bb33 --- /dev/null +++ b/internal/domains/product/entities/product_documentation.go @@ -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" + } +} \ No newline at end of file diff --git a/internal/domains/product/entities/subscription.go b/internal/domains/product/entities/subscription.go new file mode 100644 index 0000000..da02f51 --- /dev/null +++ b/internal/domains/product/entities/subscription.go @@ -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 +} diff --git a/internal/domains/product/repositories/product_category_repository_interface.go b/internal/domains/product/repositories/product_category_repository_interface.go new file mode 100644 index 0000000..dd77ce4 --- /dev/null +++ b/internal/domains/product/repositories/product_category_repository_interface.go @@ -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) +} \ No newline at end of file diff --git a/internal/domains/product/repositories/product_repository_interface.go b/internal/domains/product/repositories/product_repository_interface.go new file mode 100644 index 0000000..ad03e5e --- /dev/null +++ b/internal/domains/product/repositories/product_repository_interface.go @@ -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) +} \ No newline at end of file diff --git a/internal/domains/product/repositories/queries/category_queries.go b/internal/domains/product/repositories/queries/category_queries.go new file mode 100644 index 0000000..0aaaeb3 --- /dev/null +++ b/internal/domains/product/repositories/queries/category_queries.go @@ -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"` +} \ No newline at end of file diff --git a/internal/domains/product/repositories/queries/product_queries.go b/internal/domains/product/repositories/queries/product_queries.go new file mode 100644 index 0000000..b4c6efa --- /dev/null +++ b/internal/domains/product/repositories/queries/product_queries.go @@ -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"` +} \ No newline at end of file diff --git a/internal/domains/product/repositories/queries/subscription_queries.go b/internal/domains/product/repositories/queries/subscription_queries.go new file mode 100644 index 0000000..5d2b9bc --- /dev/null +++ b/internal/domains/product/repositories/queries/subscription_queries.go @@ -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"` +} \ No newline at end of file diff --git a/internal/domains/product/repositories/subscription_repository_interface.go b/internal/domains/product/repositories/subscription_repository_interface.go new file mode 100644 index 0000000..257be82 --- /dev/null +++ b/internal/domains/product/repositories/subscription_repository_interface.go @@ -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) +} \ No newline at end of file diff --git a/internal/domains/product/services/product_service.go b/internal/domains/product/services/product_service.go new file mode 100644 index 0000000..e3694d4 --- /dev/null +++ b/internal/domains/product/services/product_service.go @@ -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 +} \ No newline at end of file diff --git a/internal/domains/user/entities/user.go b/internal/domains/user/entities/user.go index 5672611..4554ba8 100644 --- a/internal/domains/user/entities/user.go +++ b/internal/domains/user/entities/user.go @@ -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 { // 检查用户是否被删除 diff --git a/internal/infrastructure/database/repositories/product/gorm_product_category_repository.go b/internal/infrastructure/database/repositories/product/gorm_product_category_repository.go new file mode 100644 index 0000000..512e6f1 --- /dev/null +++ b/internal/infrastructure/database/repositories/product/gorm_product_category_repository.go @@ -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 +} \ No newline at end of file diff --git a/internal/infrastructure/database/repositories/product/gorm_product_repository.go b/internal/infrastructure/database/repositories/product/gorm_product_repository.go new file mode 100644 index 0000000..e570f8f --- /dev/null +++ b/internal/infrastructure/database/repositories/product/gorm_product_repository.go @@ -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 +} \ No newline at end of file diff --git a/internal/infrastructure/database/repositories/product/gorm_subscription_repository.go b/internal/infrastructure/database/repositories/product/gorm_subscription_repository.go new file mode 100644 index 0000000..469b0f6 --- /dev/null +++ b/internal/infrastructure/database/repositories/product/gorm_subscription_repository.go @@ -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 +} \ No newline at end of file diff --git a/internal/infrastructure/http/handlers/admin_handler.go b/internal/infrastructure/http/handlers/admin_handler.go index 394b43e..efebe67 100644 --- a/internal/infrastructure/http/handlers/admin_handler.go +++ b/internal/infrastructure/http/handlers/admin_handler.go @@ -33,7 +33,7 @@ func NewAdminHandler( // Login 管理员登录 // @Summary 管理员登录 // @Description 使用用户名和密码进行管理员登录,返回JWT令牌 -// @Tags 管理员认证 +// @Tags 管理员管理 // @Accept json // @Produce json // @Param request body commands.AdminLoginCommand true "管理员登录请求" diff --git a/internal/infrastructure/http/handlers/product_handler.go b/internal/infrastructure/http/handlers/product_handler.go new file mode 100644 index 0000000..7db5b25 --- /dev/null +++ b/internal/infrastructure/http/handlers/product_handler.go @@ -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, "获取我的订阅使用情况成功") +} + + + diff --git a/internal/infrastructure/http/handlers/user_handler.go b/internal/infrastructure/http/handlers/user_handler.go index c3c2b3a..0a1824f 100644 --- a/internal/infrastructure/http/handlers/user_handler.go +++ b/internal/infrastructure/http/handlers/user_handler.go @@ -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 { diff --git a/internal/infrastructure/http/routes/product_routes.go b/internal/infrastructure/http/routes/product_routes.go new file mode 100644 index 0000000..a87ed56 --- /dev/null +++ b/internal/infrastructure/http/routes/product_routes.go @@ -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("产品路由注册完成") +} diff --git a/internal/infrastructure/http/routes/user_routes.go b/internal/infrastructure/http/routes/user_routes.go index 354fb0e..e209a4a 100644 --- a/internal/infrastructure/http/routes/user_routes.go +++ b/internal/infrastructure/http/routes/user_routes.go @@ -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("")