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

View File

@@ -0,0 +1,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架构设计变得更加直观和高效
你觉得这个可视化界面策划方案如何?需要我开始实现吗?

View File

@@ -0,0 +1,653 @@
# DDD代码生成器策划方案
## 概述
DDD代码生成器是一个命令行工具用于快速生成符合DDD架构模式的领域代码。支持生成完整的领域结构包括实体、仓储、应用服务、DTO、HTTP处理器等大幅提升开发效率。
## 功能特性
### 1. 核心功能
#### 1.1 领域生成
- **完整领域生成**: 一次性生成整个领域的所有代码文件
- **实体生成**: 生成实体、枚举、业务方法
- **仓储生成**: 生成仓储接口和GORM实现
- **应用服务生成**: 生成应用服务接口和实现
- **DTO生成**: 生成命令、查询、响应DTO
- **HTTP层生成**: 生成处理器和路由
- **依赖注入配置**: 自动更新容器配置
#### 1.2 增量功能
- **新增实体**: 在现有领域中添加新实体
- **新增服务**: 为现有实体添加新的应用服务
- **新增API**: 为现有实体添加新的HTTP接口
- **字段扩展**: 为现有实体添加新字段
#### 1.3 模板功能
- **自定义模板**: 支持自定义代码模板
- **模板变量**: 支持丰富的模板变量替换
- **多语言支持**: 支持中文和英文模板
### 2. 高级功能
#### 2.1 智能分析
- **依赖分析**: 自动分析实体间关系
- **命名规范**: 自动生成符合规范的命名
- **类型推断**: 根据字段名自动推断数据类型
- **验证规则**: 自动生成字段验证规则
#### 2.2 代码质量
- **Linter集成**: 生成后自动运行代码检查
- **格式化**: 自动格式化生成的代码
- **测试生成**: 自动生成单元测试和集成测试
- **文档生成**: 自动生成API文档注释
#### 2.3 项目管理
- **项目扫描**: 扫描现有项目结构
- **配置管理**: 管理生成器配置
- **历史记录**: 记录生成历史
- **回滚功能**: 支持代码回滚
## 技术架构
### 1. 整体架构
```
DDD Generator
├── CLI Interface # 命令行接口
├── Core Engine # 核心引擎
├── Template Engine # 模板引擎
├── Code Analyzer # 代码分析器
├── File Manager # 文件管理器
└── Validator # 验证器
```
### 2. 核心组件
#### 2.1 CLI Interface
- **命令解析**: 解析命令行参数
- **交互式输入**: 提供友好的交互界面
- **帮助系统**: 提供详细的帮助信息
- **配置管理**: 管理用户配置
#### 2.2 Core Engine
- **生成流程控制**: 控制代码生成流程
- **依赖管理**: 管理组件间依赖关系
- **错误处理**: 统一的错误处理机制
- **日志系统**: 详细的日志记录
#### 2.3 Template Engine
- **模板解析**: 解析Go模板语法
- **变量替换**: 执行模板变量替换
- **条件渲染**: 支持条件渲染逻辑
- **循环渲染**: 支持循环渲染逻辑
#### 2.4 Code Analyzer
- **AST解析**: 解析Go代码AST
- **依赖分析**: 分析代码依赖关系
- **结构分析**: 分析项目结构
- **命名分析**: 分析命名规范
#### 2.5 File Manager
- **文件操作**: 安全的文件读写操作
- **目录管理**: 创建和管理目录结构
- **备份恢复**: 文件备份和恢复
- **权限管理**: 文件权限管理
#### 2.6 Validator
- **语法验证**: 验证生成的代码语法
- **规范检查**: 检查代码规范
- **依赖验证**: 验证依赖关系
- **冲突检测**: 检测文件冲突
## 使用场景
### 1. 新项目初始化
```bash
# 生成完整的用户域
ddd-gen domain user --entities user,role,permission --features auth,profile
# 生成产品域
ddd-gen domain product --entities product,category,order --features catalog,order
```
### 2. 现有项目扩展
```bash
# 在用户域中添加新实体
ddd-gen entity user subscription --fields id,name,price,status
# 为产品添加新服务
ddd-gen service product inventory --methods check,update,reserve
# 添加新的API接口
ddd-gen api product search --methods search,filter,sort
```
### 3. 批量操作
```bash
# 批量生成多个域
ddd-gen batch --config domains.yaml
# 批量添加字段
ddd-gen batch-fields --config fields.yaml
```
## 配置文件
### 1. 项目配置 (ddd-config.yaml)
```yaml
project:
name: "tyapi-server"
module: "tyapi-server/internal"
author: "开发团队"
version: "1.0.0"
templates:
path: "./templates"
language: "zh"
style: "standard"
output:
path: "./internal"
backup: true
format: true
test: true
validation:
lint: true
test: true
docs: true
```
### 2. 领域配置 (domain-config.yaml)
```yaml
domain:
name: "user"
description: "用户管理域"
entities:
- name: "user"
fields:
- name: "id"
type: "string"
tag: "gorm:\"primaryKey;type:varchar(36)\""
comment: "用户ID"
- name: "username"
type: "string"
tag: "gorm:\"type:varchar(50);uniqueIndex;not null\""
comment: "用户名"
validation: "required,min=3,max=50"
- name: "email"
type: "string"
tag: "gorm:\"type:varchar(100);uniqueIndex;not null\""
comment: "邮箱"
validation: "required,email"
- name: "password"
type: "string"
tag: "gorm:\"type:varchar(255);not null\""
comment: "密码"
validation: "required,min=6"
- name: "status"
type: "UserStatus"
tag: "gorm:\"type:varchar(20);default:'active'\""
comment: "用户状态"
methods:
- name: "IsActive"
return: "bool"
body: "return u.Status == UserStatusActive"
- name: "CanLogin"
return: "bool"
body: "return u.IsActive() && !u.IsDeleted()"
- name: "role"
fields:
- name: "id"
type: "string"
tag: "gorm:\"primaryKey;type:varchar(36)\""
comment: "角色ID"
- name: "name"
type: "string"
tag: "gorm:\"type:varchar(50);uniqueIndex;not null\""
comment: "角色名称"
validation: "required,min=2,max=50"
- name: "description"
type: "string"
tag: "gorm:\"type:text\""
comment: "角色描述"
enums:
- name: "UserStatus"
values:
- name: "Active"
value: "active"
comment: "激活状态"
- name: "Inactive"
value: "inactive"
comment: "未激活状态"
- name: "Suspended"
value: "suspended"
comment: "暂停状态"
services:
- name: "user"
methods:
- name: "CreateUser"
command: "CreateUserCommand"
response: "UserInfoResponse"
- name: "UpdateUser"
command: "UpdateUserCommand"
response: "UserInfoResponse"
- name: "DeleteUser"
command: "DeleteUserCommand"
- name: "GetUserByID"
query: "GetUserQuery"
response: "UserInfoResponse"
- name: "ListUsers"
query: "ListUsersQuery"
response: "UserListResponse"
- name: "EnableUser"
command: "EnableUserCommand"
- name: "DisableUser"
command: "DisableUserCommand"
apis:
- path: "/users"
methods:
- method: "GET"
handler: "ListUsers"
summary: "获取用户列表"
- method: "POST"
handler: "CreateUser"
summary: "创建用户"
- path: "/users/:id"
methods:
- method: "GET"
handler: "GetUserDetail"
summary: "获取用户详情"
- method: "PUT"
handler: "UpdateUser"
summary: "更新用户"
- method: "DELETE"
handler: "DeleteUser"
summary: "删除用户"
- path: "/users/:id/enable"
methods:
- method: "POST"
handler: "EnableUser"
summary: "启用用户"
- path: "/users/:id/disable"
methods:
- method: "POST"
handler: "DisableUser"
summary: "禁用用户"
```
## 命令行接口
### 1. 主要命令
```bash
# 生成完整领域
ddd-gen domain <domain-name> [options]
# 生成实体
ddd-gen entity <domain-name> <entity-name> [options]
# 生成服务
ddd-gen service <domain-name> <service-name> [options]
# 生成API
ddd-gen api <domain-name> <api-name> [options]
# 生成DTO
ddd-gen dto <domain-name> <dto-type> [options]
# 生成测试
ddd-gen test <domain-name> <test-type> [options]
# 项目初始化
ddd-gen init [options]
# 配置管理
ddd-gen config [subcommand] [options]
# 模板管理
ddd-gen template [subcommand] [options]
```
### 2. 选项参数
```bash
# 通用选项
--config, -c 指定配置文件
--output, -o 指定输出目录
--template, -t 指定模板目录
--force, -f 强制覆盖文件
--dry-run 试运行模式
--verbose, -v 详细输出
--quiet, -q 静默模式
# 领域选项
--entities 指定实体列表
--features 指定功能特性
--services 指定服务列表
--apis 指定API列表
# 实体选项
--fields 指定字段定义
--methods 指定业务方法
--enums 指定枚举定义
--relations 指定关联关系
# 服务选项
--methods 指定服务方法
--commands 指定命令DTO
--queries 指定查询DTO
--responses 指定响应DTO
```
### 3. 交互式模式
```bash
# 交互式生成领域
ddd-gen domain --interactive
# 交互式生成实体
ddd-gen entity --interactive
# 交互式配置
ddd-gen config --interactive
```
## 模板系统
### 1. 模板结构
```
templates/
├── domain/ # 领域模板
│ ├── entities/ # 实体模板
│ ├── repositories/ # 仓储模板
│ ├── services/ # 服务模板
│ └── http/ # HTTP模板
├── dto/ # DTO模板
├── test/ # 测试模板
├── config/ # 配置模板
└── docs/ # 文档模板
```
### 2. 模板变量
```go
// 基础变量
{{.DomainName}} // 领域名称
{{.EntityName}} // 实体名称
{{.ServiceName}} // 服务名称
{{.PackageName}} // 包名称
{{.ModulePath}} // 模块路径
// 实体变量
{{.Fields}} // 字段列表
{{.Methods}} // 方法列表
{{.Enums}} // 枚举列表
{{.Relations}} // 关联关系
// 服务变量
{{.Commands}} // 命令列表
{{.Queries}} // 查询列表
{{.Responses}} // 响应列表
// API变量
{{.Routes}} // 路由列表
{{.Handlers}} // 处理器列表
{{.Middlewares}} // 中间件列表
// 项目变量
{{.ProjectName}} // 项目名称
{{.Author}} // 作者
{{.Version}} // 版本
{{.Timestamp}} // 时间戳
```
### 3. 条件渲染
```go
{{if .HasEnums}}
// 枚举定义
{{range .Enums}}
type {{.Name}} {{.Type}}
const (
{{range .Values}}
{{.Name}}{{.Name}} {{.Type}} = "{{.Value}}"
{{end}}
)
{{end}}
{{end}}
{{if .HasRelations}}
// 关联关系
{{range .Relations}}
{{.FieldName}} {{.Type}} `gorm:"foreignKey:{{.ForeignKey}}"`
{{end}}
{{end}}
```
## 代码分析
### 1. AST分析
```go
// 分析现有代码结构
type CodeAnalyzer struct {
parser *ast.Parser
types *types.Info
}
// 分析实体结构
func (ca *CodeAnalyzer) AnalyzeEntity(filePath string) (*EntityInfo, error) {
// 解析Go文件AST
// 提取结构体信息
// 分析字段和方法
// 识别关联关系
}
// 分析依赖关系
func (ca *CodeAnalyzer) AnalyzeDependencies(packagePath string) (*DependencyInfo, error) {
// 分析包依赖
// 识别循环依赖
// 生成依赖图
}
```
### 2. 命名分析
```go
// 命名规范检查
type NamingAnalyzer struct {
rules []NamingRule
}
// 检查命名规范
func (na *NamingAnalyzer) CheckNaming(name, type string) error {
// 检查命名规范
// 提供建议
// 自动修正
}
```
## 验证系统
### 1. 语法验证
```go
// 验证生成的代码
func ValidateCode(code string) error {
// 解析Go代码
// 检查语法错误
// 验证类型
// 检查导入
}
```
### 2. 规范检查
```go
// 检查代码规范
func CheckCodeStyle(code string) []StyleIssue {
// 检查格式
// 检查命名
// 检查注释
// 检查复杂度
}
```
### 3. 依赖验证
```go
// 验证依赖关系
func ValidateDependencies(domain *Domain) error {
// 检查循环依赖
// 验证接口实现
// 检查导入路径
}
```
## 扩展功能
### 1. 插件系统
```go
// 插件接口
type Plugin interface {
Name() string
Execute(ctx *Context) error
Validate(ctx *Context) error
}
// 内置插件
var BuiltinPlugins = []Plugin{
&LinterPlugin{},
&TestPlugin{},
&DocsPlugin{},
&MigrationPlugin{},
}
```
### 2. 自定义模板
```go
// 模板注册
func RegisterTemplate(name, content string) error {
// 验证模板语法
// 注册模板
// 更新索引
}
```
### 3. 代码转换
```go
// 代码转换器
type CodeTransformer struct {
rules []TransformRule
}
// 转换现有代码
func (ct *CodeTransformer) Transform(code string) (string, error) {
// 应用转换规则
// 保持代码结构
// 更新引用
}
```
## 部署方案
### 1. 安装方式
```bash
# Go安装
go install github.com/your-org/ddd-gen@latest
# 二进制安装
curl -L https://github.com/your-org/ddd-gen/releases/latest/download/ddd-gen-$(uname -s)-$(uname -m) -o ddd-gen
chmod +x ddd-gen
sudo mv ddd-gen /usr/local/bin/
# Docker安装
docker run --rm -v $(pwd):/workspace ddd-gen domain user
```
### 2. 配置管理
```bash
# 初始化配置
ddd-gen init
# 查看配置
ddd-gen config show
# 编辑配置
ddd-gen config edit
# 验证配置
ddd-gen config validate
```
### 3. 集成CI/CD
```yaml
# GitHub Actions
- name: Generate DDD Code
run: |
ddd-gen domain user --config domains/user.yaml
ddd-gen domain product --config domains/product.yaml
- name: Validate Generated Code
run: |
go mod tidy
go vet ./...
golangci-lint run
```
## 开发计划
### 第一阶段:核心功能
- [ ] CLI框架搭建
- [ ] 基础模板系统
- [ ] 实体生成功能
- [ ] 仓储生成功能
- [ ] 应用服务生成功能
### 第二阶段:完整功能
- [ ] HTTP层生成
- [ ] DTO生成
- [ ] 测试生成
- [ ] 依赖注入配置
- [ ] 代码验证
### 第三阶段:高级功能
- [ ] 代码分析
- [ ] 智能建议
- [ ] 插件系统
- [ ] 自定义模板
- [ ] 批量操作
### 第四阶段:优化完善
- [ ] 性能优化
- [ ] 错误处理
- [ ] 文档完善
- [ ] 测试覆盖
- [ ] 用户反馈
## 总结
这个DDD代码生成器将显著提升开发效率确保代码质量和架构一致性。通过配置驱动的方式可以快速生成符合DDD架构的完整代码结构同时支持灵活的定制和扩展。
关键优势:
1. **高效开发**: 大幅减少重复代码编写
2. **架构一致**: 确保所有代码符合DDD规范
3. **质量保证**: 内置代码验证和测试生成
4. **灵活扩展**: 支持自定义模板和插件
5. **易于使用**: 友好的CLI和交互界面
你觉得这个策划方案如何?需要我开始实现吗?

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,42 @@
package queries
// ListProductsQuery 产品列表查询
type ListProductsQuery struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Keyword string `json:"keyword"`
CategoryID string `json:"category_id"`
MinPrice *float64 `json:"min_price"`
MaxPrice *float64 `json:"max_price"`
IsEnabled *bool `json:"is_enabled"`
IsVisible *bool `json:"is_visible"`
IsPackage *bool `json:"is_package"`
SortBy string `json:"sort_by"`
SortOrder string `json:"sort_order"`
}
// SearchProductsQuery 产品搜索查询
type SearchProductsQuery struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Keyword string `json:"keyword"`
CategoryID string `json:"category_id"`
MinPrice *float64 `json:"min_price"`
MaxPrice *float64 `json:"max_price"`
IsEnabled *bool `json:"is_enabled"`
IsVisible *bool `json:"is_visible"`
IsPackage *bool `json:"is_package"`
SortBy string `json:"sort_by"`
SortOrder string `json:"sort_order"`
}
// GetProductQuery 获取产品详情查询
type GetProductQuery struct {
ID string `json:"id"`
Code string `json:"code"`
}
// GetProductsByIDsQuery 根据ID列表获取产品查询
type GetProductsByIDsQuery struct {
IDs []string `json:"ids"`
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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