1655 lines
40 KiB
Markdown
1655 lines
40 KiB
Markdown
|
|
# 后端管理开发文档
|
|||
|
|
|
|||
|
|
## 1. 项目概述
|
|||
|
|
|
|||
|
|
### 1.1 项目简介
|
|||
|
|
解班客后端管理系统是一个基于Node.js的企业级后端服务,为管理后台、小程序和官网提供统一的API服务和数据管理功能。
|
|||
|
|
|
|||
|
|
### 1.2 技术栈
|
|||
|
|
- **运行环境**:Node.js 18.x
|
|||
|
|
- **开发框架**:Express.js 4.x
|
|||
|
|
- **开发语言**:TypeScript 5.x
|
|||
|
|
- **数据库**:MySQL 8.0 + Redis 7.x
|
|||
|
|
- **ORM框架**:TypeORM 0.3.x
|
|||
|
|
- **认证授权**:JWT + RBAC
|
|||
|
|
- **文件存储**:阿里云OSS
|
|||
|
|
- **消息队列**:Redis + Bull
|
|||
|
|
- **日志系统**:Winston
|
|||
|
|
- **API文档**:Swagger/OpenAPI
|
|||
|
|
- **测试框架**:Jest + Supertest
|
|||
|
|
- **代码规范**:ESLint + Prettier
|
|||
|
|
|
|||
|
|
### 1.3 项目结构
|
|||
|
|
```
|
|||
|
|
backend/
|
|||
|
|
├── src/
|
|||
|
|
│ ├── controllers/ # 控制器
|
|||
|
|
│ │ ├── admin/ # 管理后台控制器
|
|||
|
|
│ │ ├── app/ # 小程序控制器
|
|||
|
|
│ │ └── common/ # 通用控制器
|
|||
|
|
│ ├── services/ # 业务服务层
|
|||
|
|
│ │ ├── user/ # 用户服务
|
|||
|
|
│ │ ├── travel/ # 旅行服务
|
|||
|
|
│ │ ├── animal/ # 动物服务
|
|||
|
|
│ │ ├── order/ # 订单服务
|
|||
|
|
│ │ └── common/ # 通用服务
|
|||
|
|
│ ├── models/ # 数据模型
|
|||
|
|
│ │ ├── entities/ # 实体定义
|
|||
|
|
│ │ ├── dto/ # 数据传输对象
|
|||
|
|
│ │ └── vo/ # 视图对象
|
|||
|
|
│ ├── middleware/ # 中间件
|
|||
|
|
│ │ ├── auth/ # 认证中间件
|
|||
|
|
│ │ ├── validation/ # 验证中间件
|
|||
|
|
│ │ └── common/ # 通用中间件
|
|||
|
|
│ ├── utils/ # 工具函数
|
|||
|
|
│ │ ├── database/ # 数据库工具
|
|||
|
|
│ │ ├── cache/ # 缓存工具
|
|||
|
|
│ │ ├── file/ # 文件处理
|
|||
|
|
│ │ └── common/ # 通用工具
|
|||
|
|
│ ├── config/ # 配置文件
|
|||
|
|
│ │ ├── database.ts # 数据库配置
|
|||
|
|
│ │ ├── redis.ts # Redis配置
|
|||
|
|
│ │ └── app.ts # 应用配置
|
|||
|
|
│ ├── routes/ # 路由定义
|
|||
|
|
│ │ ├── admin/ # 管理后台路由
|
|||
|
|
│ │ ├── app/ # 小程序路由
|
|||
|
|
│ │ └── common/ # 通用路由
|
|||
|
|
│ ├── jobs/ # 定时任务
|
|||
|
|
│ ├── migrations/ # 数据库迁移
|
|||
|
|
│ ├── seeds/ # 数据种子
|
|||
|
|
│ ├── app.ts # 应用入口
|
|||
|
|
│ └── server.ts # 服务器启动
|
|||
|
|
├── tests/ # 测试文件
|
|||
|
|
│ ├── unit/ # 单元测试
|
|||
|
|
│ ├── integration/ # 集成测试
|
|||
|
|
│ └── e2e/ # 端到端测试
|
|||
|
|
├── docs/ # 文档
|
|||
|
|
├── scripts/ # 脚本文件
|
|||
|
|
├── .env.example # 环境变量示例
|
|||
|
|
├── package.json
|
|||
|
|
├── tsconfig.json
|
|||
|
|
├── jest.config.js
|
|||
|
|
└── README.md
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 2. 开发环境搭建
|
|||
|
|
|
|||
|
|
### 2.1 环境要求
|
|||
|
|
- Node.js >= 18.0.0
|
|||
|
|
- npm >= 9.0.0 或 yarn >= 1.22.0
|
|||
|
|
- MySQL >= 8.0.0
|
|||
|
|
- Redis >= 7.0.0
|
|||
|
|
- Git >= 2.0.0
|
|||
|
|
|
|||
|
|
### 2.2 环境搭建步骤
|
|||
|
|
|
|||
|
|
#### 2.2.1 克隆项目
|
|||
|
|
```bash
|
|||
|
|
git clone https://github.com/jiebanke/backend.git
|
|||
|
|
cd backend
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 2.2.2 安装依赖
|
|||
|
|
```bash
|
|||
|
|
npm install
|
|||
|
|
# 或
|
|||
|
|
yarn install
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 2.2.3 配置环境变量
|
|||
|
|
```bash
|
|||
|
|
# 复制环境配置文件
|
|||
|
|
cp .env.example .env.development
|
|||
|
|
cp .env.example .env.production
|
|||
|
|
|
|||
|
|
# 编辑配置文件
|
|||
|
|
vim .env.development
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 2.2.4 数据库初始化
|
|||
|
|
```bash
|
|||
|
|
# 创建数据库
|
|||
|
|
npm run db:create
|
|||
|
|
|
|||
|
|
# 运行迁移
|
|||
|
|
npm run migration:run
|
|||
|
|
|
|||
|
|
# 运行种子数据
|
|||
|
|
npm run seed:run
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 2.2.5 启动开发服务器
|
|||
|
|
```bash
|
|||
|
|
npm run dev
|
|||
|
|
# 或
|
|||
|
|
yarn dev
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2.3 开发工具配置
|
|||
|
|
|
|||
|
|
#### 2.3.1 VSCode配置
|
|||
|
|
推荐安装以下插件:
|
|||
|
|
- TypeScript Importer
|
|||
|
|
- ESLint
|
|||
|
|
- Prettier
|
|||
|
|
- REST Client
|
|||
|
|
- MySQL
|
|||
|
|
- Redis
|
|||
|
|
|
|||
|
|
#### 2.3.2 数据库工具
|
|||
|
|
- MySQL Workbench
|
|||
|
|
- Navicat
|
|||
|
|
- DBeaver
|
|||
|
|
- Redis Desktop Manager
|
|||
|
|
|
|||
|
|
## 3. 开发计划与任务分解
|
|||
|
|
|
|||
|
|
### 3.1 开发阶段划分
|
|||
|
|
|
|||
|
|
#### 阶段一:基础框架搭建(预计8个工作日)
|
|||
|
|
- 项目初始化和环境配置
|
|||
|
|
- 数据库设计和迁移
|
|||
|
|
- 基础中间件开发
|
|||
|
|
- 认证授权系统
|
|||
|
|
|
|||
|
|
#### 阶段二:核心业务开发(预计25个工作日)
|
|||
|
|
- 用户管理系统
|
|||
|
|
- 旅行结伴功能
|
|||
|
|
- 动物认领功能
|
|||
|
|
- 订单支付系统
|
|||
|
|
|
|||
|
|
#### 阶段三:管理功能开发(预计15个工作日)
|
|||
|
|
- 管理后台API
|
|||
|
|
- 数据统计分析
|
|||
|
|
- 系统配置管理
|
|||
|
|
- 日志审计功能
|
|||
|
|
|
|||
|
|
#### 阶段四:优化和部署(预计10个工作日)
|
|||
|
|
- 性能优化
|
|||
|
|
- 安全加固
|
|||
|
|
- 测试和修复
|
|||
|
|
- 部署和监控
|
|||
|
|
|
|||
|
|
### 3.2 详细任务分解
|
|||
|
|
|
|||
|
|
#### 3.2.1 阶段一:基础框架搭建
|
|||
|
|
|
|||
|
|
##### 任务1.1:项目初始化(2个工作日)
|
|||
|
|
**负责人**:后端架构师 + 后端开发工程师
|
|||
|
|
**工时估算**:16人时
|
|||
|
|
**任务描述**:
|
|||
|
|
- 创建Express.js项目结构
|
|||
|
|
- 配置TypeScript和构建工具
|
|||
|
|
- 设置代码规范和Git hooks
|
|||
|
|
- 配置开发环境
|
|||
|
|
|
|||
|
|
**具体子任务**:
|
|||
|
|
1. 创建Express + TypeScript项目(4人时)
|
|||
|
|
2. 配置ESLint、Prettier和Git hooks(4人时)
|
|||
|
|
3. 设置环境变量和配置管理(4人时)
|
|||
|
|
4. 配置开发和构建脚本(4人时)
|
|||
|
|
|
|||
|
|
**验收标准**:
|
|||
|
|
- 项目可以正常启动和热重载
|
|||
|
|
- 代码规范检查通过
|
|||
|
|
- TypeScript编译无错误
|
|||
|
|
|
|||
|
|
##### 任务1.2:数据库设计和迁移(3个工作日)
|
|||
|
|
**负责人**:后端开发工程师 + 数据库工程师
|
|||
|
|
**工时估算**:24人时
|
|||
|
|
**任务描述**:
|
|||
|
|
- 设计数据库表结构
|
|||
|
|
- 创建TypeORM实体
|
|||
|
|
- 编写数据库迁移
|
|||
|
|
- 创建种子数据
|
|||
|
|
|
|||
|
|
**具体子任务**:
|
|||
|
|
1. 设计核心业务表结构(8人时)
|
|||
|
|
2. 创建TypeORM实体和关系(8人时)
|
|||
|
|
3. 编写数据库迁移脚本(4人时)
|
|||
|
|
4. 创建测试和演示数据(4人时)
|
|||
|
|
|
|||
|
|
**验收标准**:
|
|||
|
|
- 数据库表结构完整
|
|||
|
|
- 实体关系正确
|
|||
|
|
- 迁移脚本可正常执行
|
|||
|
|
|
|||
|
|
##### 任务1.3:基础中间件开发(2个工作日)
|
|||
|
|
**负责人**:后端开发工程师
|
|||
|
|
**工时估算**:16人时
|
|||
|
|
**任务描述**:
|
|||
|
|
- 请求日志中间件
|
|||
|
|
- 错误处理中间件
|
|||
|
|
- 参数验证中间件
|
|||
|
|
- 跨域处理中间件
|
|||
|
|
|
|||
|
|
**具体子任务**:
|
|||
|
|
1. 请求日志和响应时间中间件(4人时)
|
|||
|
|
2. 全局错误处理中间件(4人时)
|
|||
|
|
3. 请求参数验证中间件(4人时)
|
|||
|
|
4. CORS和安全头中间件(4人时)
|
|||
|
|
|
|||
|
|
**验收标准**:
|
|||
|
|
- 中间件功能完整
|
|||
|
|
- 错误处理规范
|
|||
|
|
- 日志记录准确
|
|||
|
|
|
|||
|
|
##### 任务1.4:认证授权系统(1个工作日)
|
|||
|
|
**负责人**:后端开发工程师
|
|||
|
|
**工时估算**:8人时
|
|||
|
|
**任务描述**:
|
|||
|
|
- JWT认证实现
|
|||
|
|
- RBAC权限控制
|
|||
|
|
- 用户会话管理
|
|||
|
|
- 权限验证中间件
|
|||
|
|
|
|||
|
|
**具体子任务**:
|
|||
|
|
1. JWT生成和验证(3人时)
|
|||
|
|
2. RBAC权限模型实现(3人时)
|
|||
|
|
3. 权限验证中间件(2人时)
|
|||
|
|
|
|||
|
|
**验收标准**:
|
|||
|
|
- JWT认证正常
|
|||
|
|
- 权限控制精确
|
|||
|
|
- 会话管理安全
|
|||
|
|
|
|||
|
|
#### 3.2.2 阶段二:核心业务开发
|
|||
|
|
|
|||
|
|
##### 任务2.1:用户管理系统(6个工作日)
|
|||
|
|
**负责人**:后端开发工程师
|
|||
|
|
**工时估算**:48人时
|
|||
|
|
**任务描述**:
|
|||
|
|
- 用户注册登录
|
|||
|
|
- 用户信息管理
|
|||
|
|
- 实名认证功能
|
|||
|
|
- 用户行为记录
|
|||
|
|
|
|||
|
|
**具体子任务**:
|
|||
|
|
1. 微信登录和手机号登录(12人时)
|
|||
|
|
2. 用户信息CRUD操作(10人时)
|
|||
|
|
3. 实名认证流程和验证(12人时)
|
|||
|
|
4. 用户行为日志记录(8人时)
|
|||
|
|
5. 用户状态管理(6人时)
|
|||
|
|
|
|||
|
|
**验收标准**:
|
|||
|
|
- 登录流程完整
|
|||
|
|
- 用户信息管理功能齐全
|
|||
|
|
- 实名认证流程正确
|
|||
|
|
|
|||
|
|
##### 任务2.2:旅行结伴功能(7个工作日)
|
|||
|
|
**负责人**:后端开发工程师
|
|||
|
|
**工时估算**:56人时
|
|||
|
|
**任务描述**:
|
|||
|
|
- 旅行活动管理
|
|||
|
|
- 参与申请处理
|
|||
|
|
- 活动状态管理
|
|||
|
|
- 地理位置服务
|
|||
|
|
|
|||
|
|
**具体子任务**:
|
|||
|
|
1. 旅行活动CRUD操作(14人时)
|
|||
|
|
2. 参与申请和审核流程(12人时)
|
|||
|
|
3. 活动状态和生命周期管理(10人时)
|
|||
|
|
4. 地理位置搜索和推荐(10人时)
|
|||
|
|
5. 活动评价和反馈(10人时)
|
|||
|
|
|
|||
|
|
**验收标准**:
|
|||
|
|
- 活动管理功能完整
|
|||
|
|
- 申请流程顺畅
|
|||
|
|
- 地理位置服务准确
|
|||
|
|
|
|||
|
|
##### 任务2.3:动物认领功能(6个工作日)
|
|||
|
|
**负责人**:后端开发工程师
|
|||
|
|
**工时估算**:48人时
|
|||
|
|
**任务描述**:
|
|||
|
|
- 动物信息管理
|
|||
|
|
- 认领申请处理
|
|||
|
|
- 认领记录跟踪
|
|||
|
|
- 动物状态更新
|
|||
|
|
|
|||
|
|
**具体子任务**:
|
|||
|
|
1. 动物信息CRUD操作(12人时)
|
|||
|
|
2. 认领申请和审核流程(12人时)
|
|||
|
|
3. 认领记录和历史跟踪(10人时)
|
|||
|
|
4. 动物状态和健康记录(8人时)
|
|||
|
|
5. 认领动态和通知(6人时)
|
|||
|
|
|
|||
|
|
**验收标准**:
|
|||
|
|
- 动物信息管理完整
|
|||
|
|
- 认领流程清晰
|
|||
|
|
- 状态跟踪准确
|
|||
|
|
|
|||
|
|
##### 任务2.4:订单支付系统(6个工作日)
|
|||
|
|
**负责人**:后端开发工程师
|
|||
|
|
**工时估算**:48人时
|
|||
|
|
**任务描述**:
|
|||
|
|
- 订单创建和管理
|
|||
|
|
- 支付接口集成
|
|||
|
|
- 订单状态跟踪
|
|||
|
|
- 退款处理功能
|
|||
|
|
|
|||
|
|
**具体子任务**:
|
|||
|
|
1. 订单创建和状态管理(12人时)
|
|||
|
|
2. 微信支付接口集成(15人时)
|
|||
|
|
3. 支付回调和状态同步(10人时)
|
|||
|
|
4. 退款申请和处理(8人时)
|
|||
|
|
5. 订单查询和统计(3人时)
|
|||
|
|
|
|||
|
|
**验收标准**:
|
|||
|
|
- 订单管理功能完整
|
|||
|
|
- 支付流程稳定
|
|||
|
|
- 退款处理正确
|
|||
|
|
|
|||
|
|
#### 3.2.3 阶段三:管理功能开发
|
|||
|
|
|
|||
|
|
##### 任务3.1:管理后台API(5个工作日)
|
|||
|
|
**负责人**:后端开发工程师
|
|||
|
|
**工时估算**:40人时
|
|||
|
|
**任务描述**:
|
|||
|
|
- 管理员认证授权
|
|||
|
|
- 用户管理接口
|
|||
|
|
- 内容管理接口
|
|||
|
|
- 系统配置接口
|
|||
|
|
|
|||
|
|
**具体子任务**:
|
|||
|
|
1. 管理员登录和权限管理(10人时)
|
|||
|
|
2. 用户管理相关接口(10人时)
|
|||
|
|
3. 内容审核和管理接口(10人时)
|
|||
|
|
4. 系统配置和参数管理(10人时)
|
|||
|
|
|
|||
|
|
**验收标准**:
|
|||
|
|
- 管理接口功能完整
|
|||
|
|
- 权限控制严格
|
|||
|
|
- 数据操作安全
|
|||
|
|
|
|||
|
|
##### 任务3.2:数据统计分析(4个工作日)
|
|||
|
|
**负责人**:后端开发工程师
|
|||
|
|
**工时估算**:32人时
|
|||
|
|
**任务描述**:
|
|||
|
|
- 用户数据统计
|
|||
|
|
- 业务数据分析
|
|||
|
|
- 财务数据报表
|
|||
|
|
- 实时数据监控
|
|||
|
|
|
|||
|
|
**具体子任务**:
|
|||
|
|
1. 用户增长和活跃度统计(10人时)
|
|||
|
|
2. 业务数据分析和报表(10人时)
|
|||
|
|
3. 财务收支统计(8人时)
|
|||
|
|
4. 实时数据监控接口(4人时)
|
|||
|
|
|
|||
|
|
**验收标准**:
|
|||
|
|
- 统计数据准确
|
|||
|
|
- 报表生成正确
|
|||
|
|
- 实时监控有效
|
|||
|
|
|
|||
|
|
##### 任务3.3:系统配置管理(3个工作日)
|
|||
|
|
**负责人**:后端开发工程师
|
|||
|
|
**工时估算**:24人时
|
|||
|
|
**任务描述**:
|
|||
|
|
- 系统参数配置
|
|||
|
|
- 字典数据管理
|
|||
|
|
- 配置缓存机制
|
|||
|
|
- 配置变更通知
|
|||
|
|
|
|||
|
|
**具体子任务**:
|
|||
|
|
1. 系统参数CRUD操作(8人时)
|
|||
|
|
2. 字典数据管理接口(6人时)
|
|||
|
|
3. 配置缓存和更新机制(6人时)
|
|||
|
|
4. 配置变更通知和日志(4人时)
|
|||
|
|
|
|||
|
|
**验收标准**:
|
|||
|
|
- 配置管理功能完整
|
|||
|
|
- 缓存机制有效
|
|||
|
|
- 变更通知及时
|
|||
|
|
|
|||
|
|
##### 任务3.4:日志审计功能(3个工作日)
|
|||
|
|
**负责人**:后端开发工程师
|
|||
|
|
**工时估算**:24人时
|
|||
|
|
**任务描述**:
|
|||
|
|
- 操作日志记录
|
|||
|
|
- 审计日志查询
|
|||
|
|
- 日志分析统计
|
|||
|
|
- 日志归档清理
|
|||
|
|
|
|||
|
|
**具体子任务**:
|
|||
|
|
1. 操作日志记录和存储(8人时)
|
|||
|
|
2. 审计日志查询接口(8人时)
|
|||
|
|
3. 日志分析和统计(4人时)
|
|||
|
|
4. 日志归档和清理机制(4人时)
|
|||
|
|
|
|||
|
|
**验收标准**:
|
|||
|
|
- 日志记录完整
|
|||
|
|
- 查询功能强大
|
|||
|
|
- 归档机制有效
|
|||
|
|
|
|||
|
|
#### 3.2.4 阶段四:优化和部署
|
|||
|
|
|
|||
|
|
##### 任务4.1:性能优化(3个工作日)
|
|||
|
|
**负责人**:后端开发工程师
|
|||
|
|
**工时估算**:24人时
|
|||
|
|
**任务描述**:
|
|||
|
|
- 数据库查询优化
|
|||
|
|
- 缓存策略优化
|
|||
|
|
- 接口性能优化
|
|||
|
|
- 并发处理优化
|
|||
|
|
|
|||
|
|
**具体子任务**:
|
|||
|
|
1. SQL查询优化和索引调整(8人时)
|
|||
|
|
2. Redis缓存策略优化(6人时)
|
|||
|
|
3. 接口响应时间优化(6人时)
|
|||
|
|
4. 并发处理和连接池优化(4人时)
|
|||
|
|
|
|||
|
|
**验收标准**:
|
|||
|
|
- 查询性能提升50%
|
|||
|
|
- 缓存命中率>80%
|
|||
|
|
- 接口响应时间<500ms
|
|||
|
|
|
|||
|
|
##### 任务4.2:安全加固(2个工作日)
|
|||
|
|
**负责人**:后端开发工程师
|
|||
|
|
**工时估算**:16人时
|
|||
|
|
**任务描述**:
|
|||
|
|
- 输入验证加强
|
|||
|
|
- SQL注入防护
|
|||
|
|
- XSS攻击防护
|
|||
|
|
- 接口限流保护
|
|||
|
|
|
|||
|
|
**具体子任务**:
|
|||
|
|
1. 输入参数验证和过滤(6人时)
|
|||
|
|
2. SQL注入和XSS防护(4人时)
|
|||
|
|
3. 接口限流和防刷机制(4人时)
|
|||
|
|
4. 敏感数据加密存储(2人时)
|
|||
|
|
|
|||
|
|
**验收标准**:
|
|||
|
|
- 安全漏洞修复
|
|||
|
|
- 防护机制有效
|
|||
|
|
- 敏感数据安全
|
|||
|
|
|
|||
|
|
##### 任务4.3:测试和修复(3个工作日)
|
|||
|
|
**负责人**:后端开发工程师 + 测试工程师
|
|||
|
|
**工时估算**:24人时
|
|||
|
|
**任务描述**:
|
|||
|
|
- 单元测试编写
|
|||
|
|
- 集成测试执行
|
|||
|
|
- 性能测试验证
|
|||
|
|
- Bug修复和优化
|
|||
|
|
|
|||
|
|
**具体子任务**:
|
|||
|
|
1. 单元测试编写和执行(10人时)
|
|||
|
|
2. 集成测试和API测试(8人时)
|
|||
|
|
3. 性能测试和压力测试(4人时)
|
|||
|
|
4. Bug修复和代码优化(2人时)
|
|||
|
|
|
|||
|
|
**验收标准**:
|
|||
|
|
- 测试覆盖率>80%
|
|||
|
|
- 集成测试通过
|
|||
|
|
- 性能指标达标
|
|||
|
|
|
|||
|
|
##### 任务4.4:部署和监控(2个工作日)
|
|||
|
|
**负责人**:后端开发工程师 + 运维工程师
|
|||
|
|
**工时估算**:16人时
|
|||
|
|
**任务描述**:
|
|||
|
|
- 生产环境部署
|
|||
|
|
- 监控系统配置
|
|||
|
|
- 日志收集配置
|
|||
|
|
- 备份恢复机制
|
|||
|
|
|
|||
|
|
**具体子任务**:
|
|||
|
|
1. Docker容器化和部署(6人时)
|
|||
|
|
2. 监控和告警配置(4人时)
|
|||
|
|
3. 日志收集和分析配置(4人时)
|
|||
|
|
4. 数据备份和恢复测试(2人时)
|
|||
|
|
|
|||
|
|
**验收标准**:
|
|||
|
|
- 部署流程自动化
|
|||
|
|
- 监控系统正常
|
|||
|
|
- 备份机制有效
|
|||
|
|
|
|||
|
|
## 4. 开发规范
|
|||
|
|
|
|||
|
|
### 4.1 代码规范
|
|||
|
|
|
|||
|
|
#### 4.1.1 项目结构规范
|
|||
|
|
```typescript
|
|||
|
|
// src/controllers/admin/user.controller.ts
|
|||
|
|
import { Request, Response } from 'express'
|
|||
|
|
import { UserService } from '@/services/user/user.service'
|
|||
|
|
import { CreateUserDto, UpdateUserDto, UserQueryDto } from '@/models/dto/user.dto'
|
|||
|
|
import { ApiResponse } from '@/utils/response'
|
|||
|
|
import { validateDto } from '@/middleware/validation'
|
|||
|
|
|
|||
|
|
export class AdminUserController {
|
|||
|
|
private userService: UserService
|
|||
|
|
|
|||
|
|
constructor() {
|
|||
|
|
this.userService = new UserService()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取用户列表
|
|||
|
|
*/
|
|||
|
|
async getUsers(req: Request, res: Response) {
|
|||
|
|
try {
|
|||
|
|
const query = req.query as UserQueryDto
|
|||
|
|
const result = await this.userService.getUsers(query)
|
|||
|
|
|
|||
|
|
return ApiResponse.success(res, result, '获取用户列表成功')
|
|||
|
|
} catch (error) {
|
|||
|
|
return ApiResponse.error(res, error.message)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 创建用户
|
|||
|
|
*/
|
|||
|
|
async createUser(req: Request, res: Response) {
|
|||
|
|
try {
|
|||
|
|
const createUserDto = await validateDto(CreateUserDto, req.body)
|
|||
|
|
const user = await this.userService.createUser(createUserDto)
|
|||
|
|
|
|||
|
|
return ApiResponse.success(res, user, '创建用户成功')
|
|||
|
|
} catch (error) {
|
|||
|
|
return ApiResponse.error(res, error.message)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 更新用户
|
|||
|
|
*/
|
|||
|
|
async updateUser(req: Request, res: Response) {
|
|||
|
|
try {
|
|||
|
|
const { id } = req.params
|
|||
|
|
const updateUserDto = await validateDto(UpdateUserDto, req.body)
|
|||
|
|
const user = await this.userService.updateUser(Number(id), updateUserDto)
|
|||
|
|
|
|||
|
|
return ApiResponse.success(res, user, '更新用户成功')
|
|||
|
|
} catch (error) {
|
|||
|
|
return ApiResponse.error(res, error.message)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 删除用户
|
|||
|
|
*/
|
|||
|
|
async deleteUser(req: Request, res: Response) {
|
|||
|
|
try {
|
|||
|
|
const { id } = req.params
|
|||
|
|
await this.userService.deleteUser(Number(id))
|
|||
|
|
|
|||
|
|
return ApiResponse.success(res, null, '删除用户成功')
|
|||
|
|
} catch (error) {
|
|||
|
|
return ApiResponse.error(res, error.message)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 4.1.2 服务层规范
|
|||
|
|
```typescript
|
|||
|
|
// src/services/user/user.service.ts
|
|||
|
|
import { Repository } from 'typeorm'
|
|||
|
|
import { AppDataSource } from '@/config/database'
|
|||
|
|
import { User } from '@/models/entities/user.entity'
|
|||
|
|
import { CreateUserDto, UpdateUserDto, UserQueryDto } from '@/models/dto/user.dto'
|
|||
|
|
import { UserVo } from '@/models/vo/user.vo'
|
|||
|
|
import { PageResult } from '@/types/common'
|
|||
|
|
import { CacheService } from '@/utils/cache'
|
|||
|
|
import { LoggerService } from '@/utils/logger'
|
|||
|
|
|
|||
|
|
export class UserService {
|
|||
|
|
private userRepository: Repository<User>
|
|||
|
|
private cacheService: CacheService
|
|||
|
|
private logger: LoggerService
|
|||
|
|
|
|||
|
|
constructor() {
|
|||
|
|
this.userRepository = AppDataSource.getRepository(User)
|
|||
|
|
this.cacheService = new CacheService()
|
|||
|
|
this.logger = new LoggerService('UserService')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取用户列表
|
|||
|
|
*/
|
|||
|
|
async getUsers(query: UserQueryDto): Promise<PageResult<UserVo>> {
|
|||
|
|
try {
|
|||
|
|
const { page = 1, limit = 10, keyword, status } = query
|
|||
|
|
const queryBuilder = this.userRepository.createQueryBuilder('user')
|
|||
|
|
|
|||
|
|
// 关键词搜索
|
|||
|
|
if (keyword) {
|
|||
|
|
queryBuilder.andWhere(
|
|||
|
|
'(user.username LIKE :keyword OR user.email LIKE :keyword OR user.phone LIKE :keyword)',
|
|||
|
|
{ keyword: `%${keyword}%` }
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 状态筛选
|
|||
|
|
if (status !== undefined) {
|
|||
|
|
queryBuilder.andWhere('user.status = :status', { status })
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 分页
|
|||
|
|
queryBuilder
|
|||
|
|
.skip((page - 1) * limit)
|
|||
|
|
.take(limit)
|
|||
|
|
.orderBy('user.created_at', 'DESC')
|
|||
|
|
|
|||
|
|
const [users, total] = await queryBuilder.getManyAndCount()
|
|||
|
|
|
|||
|
|
// 转换为VO
|
|||
|
|
const userVos = users.map(user => new UserVo(user))
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
data: userVos,
|
|||
|
|
total,
|
|||
|
|
page,
|
|||
|
|
limit,
|
|||
|
|
pages: Math.ceil(total / limit)
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
this.logger.error('获取用户列表失败', error)
|
|||
|
|
throw error
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 根据ID获取用户
|
|||
|
|
*/
|
|||
|
|
async getUserById(id: number): Promise<UserVo | null> {
|
|||
|
|
try {
|
|||
|
|
// 先从缓存获取
|
|||
|
|
const cacheKey = `user:${id}`
|
|||
|
|
let user = await this.cacheService.get<User>(cacheKey)
|
|||
|
|
|
|||
|
|
if (!user) {
|
|||
|
|
// 缓存未命中,从数据库获取
|
|||
|
|
user = await this.userRepository.findOne({
|
|||
|
|
where: { id },
|
|||
|
|
relations: ['profile', 'roles']
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
if (user) {
|
|||
|
|
// 存入缓存,过期时间30分钟
|
|||
|
|
await this.cacheService.set(cacheKey, user, 1800)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return user ? new UserVo(user) : null
|
|||
|
|
} catch (error) {
|
|||
|
|
this.logger.error('获取用户详情失败', error)
|
|||
|
|
throw error
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 创建用户
|
|||
|
|
*/
|
|||
|
|
async createUser(createUserDto: CreateUserDto): Promise<UserVo> {
|
|||
|
|
try {
|
|||
|
|
// 检查用户名是否已存在
|
|||
|
|
const existingUser = await this.userRepository.findOne({
|
|||
|
|
where: [
|
|||
|
|
{ username: createUserDto.username },
|
|||
|
|
{ email: createUserDto.email }
|
|||
|
|
]
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
if (existingUser) {
|
|||
|
|
throw new Error('用户名或邮箱已存在')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 创建用户
|
|||
|
|
const user = this.userRepository.create(createUserDto)
|
|||
|
|
const savedUser = await this.userRepository.save(user)
|
|||
|
|
|
|||
|
|
this.logger.info('创建用户成功', { userId: savedUser.id })
|
|||
|
|
return new UserVo(savedUser)
|
|||
|
|
} catch (error) {
|
|||
|
|
this.logger.error('创建用户失败', error)
|
|||
|
|
throw error
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 更新用户
|
|||
|
|
*/
|
|||
|
|
async updateUser(id: number, updateUserDto: UpdateUserDto): Promise<UserVo> {
|
|||
|
|
try {
|
|||
|
|
const user = await this.userRepository.findOne({ where: { id } })
|
|||
|
|
if (!user) {
|
|||
|
|
throw new Error('用户不存在')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新用户信息
|
|||
|
|
Object.assign(user, updateUserDto)
|
|||
|
|
const updatedUser = await this.userRepository.save(user)
|
|||
|
|
|
|||
|
|
// 清除缓存
|
|||
|
|
await this.cacheService.del(`user:${id}`)
|
|||
|
|
|
|||
|
|
this.logger.info('更新用户成功', { userId: id })
|
|||
|
|
return new UserVo(updatedUser)
|
|||
|
|
} catch (error) {
|
|||
|
|
this.logger.error('更新用户失败', error)
|
|||
|
|
throw error
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 删除用户
|
|||
|
|
*/
|
|||
|
|
async deleteUser(id: number): Promise<void> {
|
|||
|
|
try {
|
|||
|
|
const user = await this.userRepository.findOne({ where: { id } })
|
|||
|
|
if (!user) {
|
|||
|
|
throw new Error('用户不存在')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 软删除
|
|||
|
|
await this.userRepository.softDelete(id)
|
|||
|
|
|
|||
|
|
// 清除缓存
|
|||
|
|
await this.cacheService.del(`user:${id}`)
|
|||
|
|
|
|||
|
|
this.logger.info('删除用户成功', { userId: id })
|
|||
|
|
} catch (error) {
|
|||
|
|
this.logger.error('删除用户失败', error)
|
|||
|
|
throw error
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 4.1.3 数据模型规范
|
|||
|
|
```typescript
|
|||
|
|
// src/models/entities/user.entity.ts
|
|||
|
|
import {
|
|||
|
|
Entity,
|
|||
|
|
PrimaryGeneratedColumn,
|
|||
|
|
Column,
|
|||
|
|
CreateDateColumn,
|
|||
|
|
UpdateDateColumn,
|
|||
|
|
DeleteDateColumn,
|
|||
|
|
OneToOne,
|
|||
|
|
ManyToMany,
|
|||
|
|
JoinTable
|
|||
|
|
} from 'typeorm'
|
|||
|
|
import { UserProfile } from './user-profile.entity'
|
|||
|
|
import { Role } from './role.entity'
|
|||
|
|
|
|||
|
|
@Entity('users')
|
|||
|
|
export class User {
|
|||
|
|
@PrimaryGeneratedColumn()
|
|||
|
|
id: number
|
|||
|
|
|
|||
|
|
@Column({ unique: true, length: 50 })
|
|||
|
|
username: string
|
|||
|
|
|
|||
|
|
@Column({ unique: true, length: 100 })
|
|||
|
|
email: string
|
|||
|
|
|
|||
|
|
@Column({ length: 20, nullable: true })
|
|||
|
|
phone: string
|
|||
|
|
|
|||
|
|
@Column({ length: 255 })
|
|||
|
|
password: string
|
|||
|
|
|
|||
|
|
@Column({ type: 'tinyint', default: 1, comment: '状态:0-禁用,1-启用' })
|
|||
|
|
status: number
|
|||
|
|
|
|||
|
|
@Column({ type: 'datetime', nullable: true, comment: '最后登录时间' })
|
|||
|
|
last_login_at: Date
|
|||
|
|
|
|||
|
|
@CreateDateColumn()
|
|||
|
|
created_at: Date
|
|||
|
|
|
|||
|
|
@UpdateDateColumn()
|
|||
|
|
updated_at: Date
|
|||
|
|
|
|||
|
|
@DeleteDateColumn()
|
|||
|
|
deleted_at: Date
|
|||
|
|
|
|||
|
|
// 关联关系
|
|||
|
|
@OneToOne(() => UserProfile, profile => profile.user)
|
|||
|
|
profile: UserProfile
|
|||
|
|
|
|||
|
|
@ManyToMany(() => Role, role => role.users)
|
|||
|
|
@JoinTable({
|
|||
|
|
name: 'user_roles',
|
|||
|
|
joinColumn: { name: 'user_id', referencedColumnName: 'id' },
|
|||
|
|
inverseJoinColumn: { name: 'role_id', referencedColumnName: 'id' }
|
|||
|
|
})
|
|||
|
|
roles: Role[]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/models/dto/user.dto.ts
|
|||
|
|
import { IsString, IsEmail, IsOptional, IsNumber, MinLength, MaxLength } from 'class-validator'
|
|||
|
|
|
|||
|
|
export class CreateUserDto {
|
|||
|
|
@IsString()
|
|||
|
|
@MinLength(3)
|
|||
|
|
@MaxLength(50)
|
|||
|
|
username: string
|
|||
|
|
|
|||
|
|
@IsEmail()
|
|||
|
|
email: string
|
|||
|
|
|
|||
|
|
@IsOptional()
|
|||
|
|
@IsString()
|
|||
|
|
@MaxLength(20)
|
|||
|
|
phone?: string
|
|||
|
|
|
|||
|
|
@IsString()
|
|||
|
|
@MinLength(6)
|
|||
|
|
password: string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export class UpdateUserDto {
|
|||
|
|
@IsOptional()
|
|||
|
|
@IsString()
|
|||
|
|
@MinLength(3)
|
|||
|
|
@MaxLength(50)
|
|||
|
|
username?: string
|
|||
|
|
|
|||
|
|
@IsOptional()
|
|||
|
|
@IsEmail()
|
|||
|
|
email?: string
|
|||
|
|
|
|||
|
|
@IsOptional()
|
|||
|
|
@IsString()
|
|||
|
|
@MaxLength(20)
|
|||
|
|
phone?: string
|
|||
|
|
|
|||
|
|
@IsOptional()
|
|||
|
|
@IsNumber()
|
|||
|
|
status?: number
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export class UserQueryDto {
|
|||
|
|
@IsOptional()
|
|||
|
|
@IsNumber()
|
|||
|
|
page?: number
|
|||
|
|
|
|||
|
|
@IsOptional()
|
|||
|
|
@IsNumber()
|
|||
|
|
limit?: number
|
|||
|
|
|
|||
|
|
@IsOptional()
|
|||
|
|
@IsString()
|
|||
|
|
keyword?: string
|
|||
|
|
|
|||
|
|
@IsOptional()
|
|||
|
|
@IsNumber()
|
|||
|
|
status?: number
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/models/vo/user.vo.ts
|
|||
|
|
import { User } from '@/models/entities/user.entity'
|
|||
|
|
|
|||
|
|
export class UserVo {
|
|||
|
|
id: number
|
|||
|
|
username: string
|
|||
|
|
email: string
|
|||
|
|
phone: string
|
|||
|
|
status: number
|
|||
|
|
last_login_at: Date
|
|||
|
|
created_at: Date
|
|||
|
|
updated_at: Date
|
|||
|
|
|
|||
|
|
constructor(user: User) {
|
|||
|
|
this.id = user.id
|
|||
|
|
this.username = user.username
|
|||
|
|
this.email = user.email
|
|||
|
|
this.phone = user.phone
|
|||
|
|
this.status = user.status
|
|||
|
|
this.last_login_at = user.last_login_at
|
|||
|
|
this.created_at = user.created_at
|
|||
|
|
this.updated_at = user.updated_at
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.2 API设计规范
|
|||
|
|
|
|||
|
|
#### 4.2.1 路由规范
|
|||
|
|
```typescript
|
|||
|
|
// src/routes/admin/user.routes.ts
|
|||
|
|
import { Router } from 'express'
|
|||
|
|
import { AdminUserController } from '@/controllers/admin/user.controller'
|
|||
|
|
import { authMiddleware } from '@/middleware/auth'
|
|||
|
|
import { permissionMiddleware } from '@/middleware/permission'
|
|||
|
|
|
|||
|
|
const router = Router()
|
|||
|
|
const userController = new AdminUserController()
|
|||
|
|
|
|||
|
|
// 用户管理路由
|
|||
|
|
router.get(
|
|||
|
|
'/users',
|
|||
|
|
authMiddleware,
|
|||
|
|
permissionMiddleware('user:view'),
|
|||
|
|
userController.getUsers.bind(userController)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
router.get(
|
|||
|
|
'/users/:id',
|
|||
|
|
authMiddleware,
|
|||
|
|
permissionMiddleware('user:view'),
|
|||
|
|
userController.getUserById.bind(userController)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
router.post(
|
|||
|
|
'/users',
|
|||
|
|
authMiddleware,
|
|||
|
|
permissionMiddleware('user:create'),
|
|||
|
|
userController.createUser.bind(userController)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
router.put(
|
|||
|
|
'/users/:id',
|
|||
|
|
authMiddleware,
|
|||
|
|
permissionMiddleware('user:update'),
|
|||
|
|
userController.updateUser.bind(userController)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
router.delete(
|
|||
|
|
'/users/:id',
|
|||
|
|
authMiddleware,
|
|||
|
|
permissionMiddleware('user:delete'),
|
|||
|
|
userController.deleteUser.bind(userController)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
export default router
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 4.2.2 响应格式规范
|
|||
|
|
```typescript
|
|||
|
|
// src/utils/response.ts
|
|||
|
|
import { Response } from 'express'
|
|||
|
|
|
|||
|
|
export interface ApiResponseData<T = any> {
|
|||
|
|
code: number
|
|||
|
|
message: string
|
|||
|
|
data: T
|
|||
|
|
timestamp: number
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export class ApiResponse {
|
|||
|
|
/**
|
|||
|
|
* 成功响应
|
|||
|
|
*/
|
|||
|
|
static success<T>(res: Response, data: T, message = '操作成功'): Response {
|
|||
|
|
return res.json({
|
|||
|
|
code: 200,
|
|||
|
|
message,
|
|||
|
|
data,
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 错误响应
|
|||
|
|
*/
|
|||
|
|
static error(res: Response, message = '操作失败', code = 500): Response {
|
|||
|
|
return res.status(code).json({
|
|||
|
|
code,
|
|||
|
|
message,
|
|||
|
|
data: null,
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 参数错误响应
|
|||
|
|
*/
|
|||
|
|
static badRequest(res: Response, message = '参数错误'): Response {
|
|||
|
|
return this.error(res, message, 400)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 未授权响应
|
|||
|
|
*/
|
|||
|
|
static unauthorized(res: Response, message = '未授权访问'): Response {
|
|||
|
|
return this.error(res, message, 401)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 禁止访问响应
|
|||
|
|
*/
|
|||
|
|
static forbidden(res: Response, message = '禁止访问'): Response {
|
|||
|
|
return this.error(res, message, 403)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 资源不存在响应
|
|||
|
|
*/
|
|||
|
|
static notFound(res: Response, message = '资源不存在'): Response {
|
|||
|
|
return this.error(res, message, 404)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.3 数据库操作规范
|
|||
|
|
|
|||
|
|
#### 4.3.1 查询优化规范
|
|||
|
|
```typescript
|
|||
|
|
// src/services/travel/travel.service.ts
|
|||
|
|
export class TravelService {
|
|||
|
|
/**
|
|||
|
|
* 获取旅行活动列表(优化版本)
|
|||
|
|
*/
|
|||
|
|
async getTravelActivities(query: TravelQueryDto): Promise<PageResult<TravelVo>> {
|
|||
|
|
const { page = 1, limit = 10, city, start_date, end_date, status } = query
|
|||
|
|
|
|||
|
|
// 使用QueryBuilder进行复杂查询
|
|||
|
|
const queryBuilder = this.travelRepository
|
|||
|
|
.createQueryBuilder('travel')
|
|||
|
|
.leftJoinAndSelect('travel.creator', 'creator')
|
|||
|
|
.leftJoinAndSelect('travel.participants', 'participants')
|
|||
|
|
.select([
|
|||
|
|
'travel.id',
|
|||
|
|
'travel.title',
|
|||
|
|
'travel.description',
|
|||
|
|
'travel.city',
|
|||
|
|
'travel.start_date',
|
|||
|
|
'travel.end_date',
|
|||
|
|
'travel.max_participants',
|
|||
|
|
'travel.status',
|
|||
|
|
'travel.created_at',
|
|||
|
|
'creator.id',
|
|||
|
|
'creator.username',
|
|||
|
|
'creator.avatar_url',
|
|||
|
|
'participants.id'
|
|||
|
|
])
|
|||
|
|
|
|||
|
|
// 城市筛选
|
|||
|
|
if (city) {
|
|||
|
|
queryBuilder.andWhere('travel.city = :city', { city })
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 日期范围筛选
|
|||
|
|
if (start_date) {
|
|||
|
|
queryBuilder.andWhere('travel.start_date >= :start_date', { start_date })
|
|||
|
|
}
|
|||
|
|
if (end_date) {
|
|||
|
|
queryBuilder.andWhere('travel.end_date <= :end_date', { end_date })
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 状态筛选
|
|||
|
|
if (status !== undefined) {
|
|||
|
|
queryBuilder.andWhere('travel.status = :status', { status })
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 分页和排序
|
|||
|
|
queryBuilder
|
|||
|
|
.skip((page - 1) * limit)
|
|||
|
|
.take(limit)
|
|||
|
|
.orderBy('travel.created_at', 'DESC')
|
|||
|
|
|
|||
|
|
const [travels, total] = await queryBuilder.getManyAndCount()
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
data: travels.map(travel => new TravelVo(travel)),
|
|||
|
|
total,
|
|||
|
|
page,
|
|||
|
|
limit,
|
|||
|
|
pages: Math.ceil(total / limit)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 批量更新操作
|
|||
|
|
*/
|
|||
|
|
async batchUpdateTravelStatus(ids: number[], status: number): Promise<void> {
|
|||
|
|
await this.travelRepository
|
|||
|
|
.createQueryBuilder()
|
|||
|
|
.update(Travel)
|
|||
|
|
.set({ status, updated_at: new Date() })
|
|||
|
|
.where('id IN (:...ids)', { ids })
|
|||
|
|
.execute()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 4.3.2 事务处理规范
|
|||
|
|
```typescript
|
|||
|
|
// src/services/order/order.service.ts
|
|||
|
|
import { DataSource } from 'typeorm'
|
|||
|
|
|
|||
|
|
export class OrderService {
|
|||
|
|
constructor(private dataSource: DataSource) {}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 创建订单(事务处理)
|
|||
|
|
*/
|
|||
|
|
async createOrder(createOrderDto: CreateOrderDto): Promise<OrderVo> {
|
|||
|
|
const queryRunner = this.dataSource.createQueryRunner()
|
|||
|
|
await queryRunner.connect()
|
|||
|
|
await queryRunner.startTransaction()
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 1. 创建订单
|
|||
|
|
const order = queryRunner.manager.create(Order, {
|
|||
|
|
...createOrderDto,
|
|||
|
|
order_no: this.generateOrderNo(),
|
|||
|
|
status: OrderStatus.PENDING
|
|||
|
|
})
|
|||
|
|
const savedOrder = await queryRunner.manager.save(order)
|
|||
|
|
|
|||
|
|
// 2. 创建订单项
|
|||
|
|
const orderItems = createOrderDto.items.map(item =>
|
|||
|
|
queryRunner.manager.create(OrderItem, {
|
|||
|
|
...item,
|
|||
|
|
order_id: savedOrder.id
|
|||
|
|
})
|
|||
|
|
)
|
|||
|
|
await queryRunner.manager.save(orderItems)
|
|||
|
|
|
|||
|
|
// 3. 更新库存
|
|||
|
|
for (const item of createOrderDto.items) {
|
|||
|
|
await queryRunner.manager.decrement(
|
|||
|
|
Product,
|
|||
|
|
{ id: item.product_id },
|
|||
|
|
'stock',
|
|||
|
|
item.quantity
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 4. 记录操作日志
|
|||
|
|
const log = queryRunner.manager.create(OperationLog, {
|
|||
|
|
user_id: createOrderDto.user_id,
|
|||
|
|
action: 'CREATE_ORDER',
|
|||
|
|
target_type: 'ORDER',
|
|||
|
|
target_id: savedOrder.id,
|
|||
|
|
details: JSON.stringify(createOrderDto)
|
|||
|
|
})
|
|||
|
|
await queryRunner.manager.save(log)
|
|||
|
|
|
|||
|
|
await queryRunner.commitTransaction()
|
|||
|
|
|
|||
|
|
return new OrderVo(savedOrder)
|
|||
|
|
} catch (error) {
|
|||
|
|
await queryRunner.rollbackTransaction()
|
|||
|
|
throw error
|
|||
|
|
} finally {
|
|||
|
|
await queryRunner.release()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 5. 质量保证
|
|||
|
|
|
|||
|
|
### 5.1 测试策略
|
|||
|
|
|
|||
|
|
#### 5.1.1 单元测试
|
|||
|
|
```typescript
|
|||
|
|
// tests/unit/services/user.service.spec.ts
|
|||
|
|
import { UserService } from '@/services/user/user.service'
|
|||
|
|
import { User } from '@/models/entities/user.entity'
|
|||
|
|
import { CreateUserDto } from '@/models/dto/user.dto'
|
|||
|
|
|
|||
|
|
describe('UserService', () => {
|
|||
|
|
let userService: UserService
|
|||
|
|
let mockUserRepository: any
|
|||
|
|
|
|||
|
|
beforeEach(() => {
|
|||
|
|
mockUserRepository = {
|
|||
|
|
findOne: jest.fn(),
|
|||
|
|
create: jest.fn(),
|
|||
|
|
save: jest.fn(),
|
|||
|
|
softDelete: jest.fn(),
|
|||
|
|
createQueryBuilder: jest.fn()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
userService = new UserService()
|
|||
|
|
userService['userRepository'] = mockUserRepository
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
describe('createUser', () => {
|
|||
|
|
it('should create user successfully', async () => {
|
|||
|
|
const createUserDto: CreateUserDto = {
|
|||
|
|
username: 'testuser',
|
|||
|
|
email: 'test@example.com',
|
|||
|
|
password: 'password123'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const mockUser = { id: 1, ...createUserDto }
|
|||
|
|
|
|||
|
|
mockUserRepository.findOne.mockResolvedValue(null)
|
|||
|
|
mockUserRepository.create.mockReturnValue(mockUser)
|
|||
|
|
mockUserRepository.save.mockResolvedValue(mockUser)
|
|||
|
|
|
|||
|
|
const result = await userService.createUser(createUserDto)
|
|||
|
|
|
|||
|
|
expect(result.username).toBe(createUserDto.username)
|
|||
|
|
expect(result.email).toBe(createUserDto.email)
|
|||
|
|
expect(mockUserRepository.save).toHaveBeenCalledWith(mockUser)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('should throw error when user already exists', async () => {
|
|||
|
|
const createUserDto: CreateUserDto = {
|
|||
|
|
username: 'testuser',
|
|||
|
|
email: 'test@example.com',
|
|||
|
|
password: 'password123'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
mockUserRepository.findOne.mockResolvedValue({ id: 1 })
|
|||
|
|
|
|||
|
|
await expect(userService.createUser(createUserDto))
|
|||
|
|
.rejects.toThrow('用户名或邮箱已存在')
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 5.1.2 集成测试
|
|||
|
|
```typescript
|
|||
|
|
// tests/integration/controllers/user.controller.spec.ts
|
|||
|
|
import request from 'supertest'
|
|||
|
|
import { app } from '@/app'
|
|||
|
|
import { AppDataSource } from '@/config/database'
|
|||
|
|
|
|||
|
|
describe('User Controller Integration Tests', () => {
|
|||
|
|
let authToken: string
|
|||
|
|
|
|||
|
|
beforeAll(async () => {
|
|||
|
|
await AppDataSource.initialize()
|
|||
|
|
|
|||
|
|
// 获取认证token
|
|||
|
|
const loginResponse = await request(app)
|
|||
|
|
.post('/api/auth/login')
|
|||
|
|
.send({
|
|||
|
|
username: 'admin',
|
|||
|
|
password: 'admin123'
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
authToken = loginResponse.body.data.access_token
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
afterAll(async () => {
|
|||
|
|
await AppDataSource.destroy()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
describe('GET /api/admin/users', () => {
|
|||
|
|
it('should return user list', async () => {
|
|||
|
|
const response = await request(app)
|
|||
|
|
.get('/api/admin/users')
|
|||
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|||
|
|
.expect(200)
|
|||
|
|
|
|||
|
|
expect(response.body.code).toBe(200)
|
|||
|
|
expect(response.body.data).toHaveProperty('data')
|
|||
|
|
expect(response.body.data).toHaveProperty('total')
|
|||
|
|
expect(Array.isArray(response.body.data.data)).toBe(true)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('should return 401 without auth token', async () => {
|
|||
|
|
await request(app)
|
|||
|
|
.get('/api/admin/users')
|
|||
|
|
.expect(401)
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
describe('POST /api/admin/users', () => {
|
|||
|
|
it('should create user successfully', async () => {
|
|||
|
|
const newUser = {
|
|||
|
|
username: 'newuser',
|
|||
|
|
email: 'newuser@example.com',
|
|||
|
|
password: 'password123'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const response = await request(app)
|
|||
|
|
.post('/api/admin/users')
|
|||
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|||
|
|
.send(newUser)
|
|||
|
|
.expect(200)
|
|||
|
|
|
|||
|
|
expect(response.body.code).toBe(200)
|
|||
|
|
expect(response.body.data.username).toBe(newUser.username)
|
|||
|
|
expect(response.body.data.email).toBe(newUser.email)
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 5.2 代码质量检查
|
|||
|
|
|
|||
|
|
#### 5.2.1 ESLint配置
|
|||
|
|
```javascript
|
|||
|
|
// .eslintrc.js
|
|||
|
|
module.exports = {
|
|||
|
|
parser: '@typescript-eslint/parser',
|
|||
|
|
extends: [
|
|||
|
|
'@typescript-eslint/recommended',
|
|||
|
|
'prettier'
|
|||
|
|
],
|
|||
|
|
plugins: ['@typescript-eslint'],
|
|||
|
|
rules: {
|
|||
|
|
'@typescript-eslint/no-unused-vars': 'error',
|
|||
|
|
'@typescript-eslint/explicit-function-return-type': 'warn',
|
|||
|
|
'@typescript-eslint/no-explicit-any': 'warn',
|
|||
|
|
'prefer-const': 'error',
|
|||
|
|
'no-var': 'error'
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 5.2.2 代码审查流程
|
|||
|
|
1. 开发者提交Pull Request
|
|||
|
|
2. 自动化检查(ESLint、TypeScript、测试)
|
|||
|
|
3. 同行代码审查
|
|||
|
|
4. 技术负责人审查
|
|||
|
|
5. 合并到主分支
|
|||
|
|
|
|||
|
|
## 6. 部署和运维
|
|||
|
|
|
|||
|
|
### 6.1 Docker配置
|
|||
|
|
|
|||
|
|
#### 6.1.1 Dockerfile
|
|||
|
|
```dockerfile
|
|||
|
|
# Dockerfile
|
|||
|
|
FROM node:18-alpine
|
|||
|
|
|
|||
|
|
WORKDIR /app
|
|||
|
|
|
|||
|
|
# 复制package文件
|
|||
|
|
COPY package*.json ./
|
|||
|
|
|
|||
|
|
# 安装依赖
|
|||
|
|
RUN npm ci --only=production
|
|||
|
|
|
|||
|
|
# 复制源代码
|
|||
|
|
COPY . .
|
|||
|
|
|
|||
|
|
# 构建应用
|
|||
|
|
RUN npm run build
|
|||
|
|
|
|||
|
|
# 暴露端口
|
|||
|
|
EXPOSE 3000
|
|||
|
|
|
|||
|
|
# 启动应用
|
|||
|
|
CMD ["npm", "start"]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 6.1.2 Docker Compose
|
|||
|
|
```yaml
|
|||
|
|
# docker-compose.yml
|
|||
|
|
version: '3.8'
|
|||
|
|
|
|||
|
|
services:
|
|||
|
|
app:
|
|||
|
|
build: .
|
|||
|
|
ports:
|
|||
|
|
- "3000:3000"
|
|||
|
|
environment:
|
|||
|
|
- NODE_ENV=production
|
|||
|
|
- DB_HOST=mysql
|
|||
|
|
- REDIS_HOST=redis
|
|||
|
|
depends_on:
|
|||
|
|
- mysql
|
|||
|
|
- redis
|
|||
|
|
volumes:
|
|||
|
|
- ./logs:/app/logs
|
|||
|
|
|
|||
|
|
mysql:
|
|||
|
|
image: mysql:8.0
|
|||
|
|
environment:
|
|||
|
|
MYSQL_ROOT_PASSWORD: rootpassword
|
|||
|
|
MYSQL_DATABASE: jiebanke
|
|||
|
|
MYSQL_USER: jiebanke
|
|||
|
|
MYSQL_PASSWORD: password
|
|||
|
|
ports:
|
|||
|
|
- "3306:3306"
|
|||
|
|
volumes:
|
|||
|
|
- mysql_data:/var/lib/mysql
|
|||
|
|
|
|||
|
|
redis:
|
|||
|
|
image: redis:7-alpine
|
|||
|
|
ports:
|
|||
|
|
- "6379:6379"
|
|||
|
|
volumes:
|
|||
|
|
- redis_data:/data
|
|||
|
|
|
|||
|
|
volumes:
|
|||
|
|
mysql_data:
|
|||
|
|
redis_data:
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6.2 CI/CD配置
|
|||
|
|
|
|||
|
|
#### 6.2.1 GitHub Actions
|
|||
|
|
```yaml
|
|||
|
|
# .github/workflows/deploy.yml
|
|||
|
|
name: Deploy to Production
|
|||
|
|
|
|||
|
|
on:
|
|||
|
|
push:
|
|||
|
|
branches: [ main ]
|
|||
|
|
|
|||
|
|
jobs:
|
|||
|
|
test:
|
|||
|
|
runs-on: ubuntu-latest
|
|||
|
|
|
|||
|
|
services:
|
|||
|
|
mysql:
|
|||
|
|
image: mysql:8.0
|
|||
|
|
env:
|
|||
|
|
MYSQL_ROOT_PASSWORD: root
|
|||
|
|
MYSQL_DATABASE: test
|
|||
|
|
options: >-
|
|||
|
|
--health-cmd="mysqladmin ping"
|
|||
|
|
--health-interval=10s
|
|||
|
|
--health-timeout=5s
|
|||
|
|
--health-retries=3
|
|||
|
|
|
|||
|
|
redis:
|
|||
|
|
image: redis:7-alpine
|
|||
|
|
options: >-
|
|||
|
|
--health-cmd="redis-cli ping"
|
|||
|
|
--health-interval=10s
|
|||
|
|
--health-timeout=5s
|
|||
|
|
--health-retries=3
|
|||
|
|
|
|||
|
|
steps:
|
|||
|
|
- uses: actions/checkout@v3
|
|||
|
|
|
|||
|
|
- name: Setup Node.js
|
|||
|
|
uses: actions/setup-node@v3
|
|||
|
|
with:
|
|||
|
|
node-version: '18'
|
|||
|
|
cache: 'npm'
|
|||
|
|
|
|||
|
|
- name: Install dependencies
|
|||
|
|
run: npm ci
|
|||
|
|
|
|||
|
|
- name: Run linting
|
|||
|
|
run: npm run lint
|
|||
|
|
|
|||
|
|
- name: Run tests
|
|||
|
|
run: npm run test
|
|||
|
|
env:
|
|||
|
|
DB_HOST: localhost
|
|||
|
|
DB_PORT: 3306
|
|||
|
|
DB_USERNAME: root
|
|||
|
|
DB_PASSWORD: root
|
|||
|
|
DB_DATABASE: test
|
|||
|
|
REDIS_HOST: localhost
|
|||
|
|
REDIS_PORT: 6379
|
|||
|
|
|
|||
|
|
deploy:
|
|||
|
|
needs: test
|
|||
|
|
runs-on: ubuntu-latest
|
|||
|
|
|
|||
|
|
steps:
|
|||
|
|
- uses: actions/checkout@v3
|
|||
|
|
|
|||
|
|
- name: Build Docker image
|
|||
|
|
run: docker build -t jiebanke/backend:${{ github.sha }} .
|
|||
|
|
|
|||
|
|
- name: Push to registry
|
|||
|
|
run: |
|
|||
|
|
echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
|
|||
|
|
docker push jiebanke/backend:${{ github.sha }}
|
|||
|
|
|
|||
|
|
- name: Deploy to server
|
|||
|
|
uses: appleboy/ssh-action@v0.1.5
|
|||
|
|
with:
|
|||
|
|
host: ${{ secrets.HOST }}
|
|||
|
|
username: ${{ secrets.USERNAME }}
|
|||
|
|
key: ${{ secrets.SSH_KEY }}
|
|||
|
|
script: |
|
|||
|
|
docker pull jiebanke/backend:${{ github.sha }}
|
|||
|
|
docker stop jiebanke-backend || true
|
|||
|
|
docker rm jiebanke-backend || true
|
|||
|
|
docker run -d --name jiebanke-backend \
|
|||
|
|
-p 3000:3000 \
|
|||
|
|
--env-file /opt/jiebanke/.env \
|
|||
|
|
jiebanke/backend:${{ github.sha }}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6.3 监控和日志
|
|||
|
|
|
|||
|
|
#### 6.3.1 日志配置
|
|||
|
|
```typescript
|
|||
|
|
// src/utils/logger.ts
|
|||
|
|
import winston from 'winston'
|
|||
|
|
import DailyRotateFile from 'winston-daily-rotate-file'
|
|||
|
|
|
|||
|
|
export class LoggerService {
|
|||
|
|
private logger: winston.Logger
|
|||
|
|
|
|||
|
|
constructor(private context: string) {
|
|||
|
|
this.logger = winston.createLogger({
|
|||
|
|
level: process.env.LOG_LEVEL || 'info',
|
|||
|
|
format: winston.format.combine(
|
|||
|
|
winston.format.timestamp(),
|
|||
|
|
winston.format.errors({ stack: true }),
|
|||
|
|
winston.format.json()
|
|||
|
|
),
|
|||
|
|
defaultMeta: { service: 'jiebanke-backend', context: this.context },
|
|||
|
|
transports: [
|
|||
|
|
// 控制台输出
|
|||
|
|
new winston.transports.Console({
|
|||
|
|
format: winston.format.combine(
|
|||
|
|
winston.format.colorize(),
|
|||
|
|
winston.format.simple()
|
|||
|
|
)
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
// 错误日志文件
|
|||
|
|
new DailyRotateFile({
|
|||
|
|
filename: 'logs/error-%DATE%.log',
|
|||
|
|
datePattern: 'YYYY-MM-DD',
|
|||
|
|
level: 'error',
|
|||
|
|
maxSize: '20m',
|
|||
|
|
maxFiles: '14d'
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
// 应用日志文件
|
|||
|
|
new DailyRotateFile({
|
|||
|
|
filename: 'logs/app-%DATE%.log',
|
|||
|
|
datePattern: 'YYYY-MM-DD',
|
|||
|
|
maxSize: '20m',
|
|||
|
|
maxFiles: '14d'
|
|||
|
|
})
|
|||
|
|
]
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
info(message: string, meta?: any) {
|
|||
|
|
this.logger.info(message, meta)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
error(message: string, error?: any) {
|
|||
|
|
this.logger.error(message, { error: error?.stack || error })
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
warn(message: string, meta?: any) {
|
|||
|
|
this.logger.warn(message, meta)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
debug(message: string, meta?: any) {
|
|||
|
|
this.logger.debug(message, meta)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 6.3.2 性能监控
|
|||
|
|
```typescript
|
|||
|
|
// src/middleware/monitor.ts
|
|||
|
|
import { Request, Response, NextFunction } from 'express'
|
|||
|
|
import { LoggerService } from '@/utils/logger'
|
|||
|
|
|
|||
|
|
const logger = new LoggerService('Monitor')
|
|||
|
|
|
|||
|
|
export const performanceMonitor = (req: Request, res: Response, next: NextFunction) => {
|
|||
|
|
const startTime = Date.now()
|
|||
|
|
|
|||
|
|
res.on('finish', () => {
|
|||
|
|
const duration = Date.now() - startTime
|
|||
|
|
const { method, originalUrl, ip } = req
|
|||
|
|
const { statusCode } = res
|
|||
|
|
|
|||
|
|
// 记录请求日志
|
|||
|
|
logger.info('HTTP Request', {
|
|||
|
|
method,
|
|||
|
|
url: originalUrl,
|
|||
|
|
statusCode,
|
|||
|
|
duration,
|
|||
|
|
ip,
|
|||
|
|
userAgent: req.get('User-Agent')
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 慢查询告警
|
|||
|
|
if (duration > 2000) {
|
|||
|
|
logger.warn('Slow Request', {
|
|||
|
|
method,
|
|||
|
|
url: originalUrl,
|
|||
|
|
duration
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 错误状态告警
|
|||
|
|
if (statusCode >= 500) {
|
|||
|
|
logger.error('Server Error', {
|
|||
|
|
method,
|
|||
|
|
url: originalUrl,
|
|||
|
|
statusCode
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
next()
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 7. 总结
|
|||
|
|
|
|||
|
|
本开发文档详细规划了解班客后端管理系统的开发计划,包括:
|
|||
|
|
|
|||
|
|
### 7.1 开发计划
|
|||
|
|
- **总工期**:58个工作日
|
|||
|
|
- **团队规模**:2-3名后端开发工程师
|
|||
|
|
- **关键里程碑**:基础框架、核心业务、管理功能、优化部署
|
|||
|
|
|
|||
|
|
### 7.2 技术架构
|
|||
|
|
- **后端框架**:Express.js + TypeScript
|
|||
|
|
- **数据库**:MySQL + Redis
|
|||
|
|
- **ORM框架**:TypeORM
|
|||
|
|
- **认证授权**:JWT + RBAC
|
|||
|
|
|
|||
|
|
### 7.3 质量保证
|
|||
|
|
- **代码规范**:ESLint + Prettier
|
|||
|
|
- **测试策略**:单元测试 + 集成测试
|
|||
|
|
- **性能优化**:查询优化、缓存策略
|
|||
|
|
- **监控体系**:日志监控 + 性能监控
|
|||
|
|
|
|||
|
|
### 7.4 部署运维
|
|||
|
|
- **容器化**:Docker + Docker Compose
|
|||
|
|
- **CI/CD**:GitHub Actions自动化部署
|
|||
|
|
- **监控日志**:Winston日志系统
|
|||
|
|
- **性能监控**:请求监控和告警
|
|||
|
|
|
|||
|
|
通过严格按照本开发文档执行,可以确保后端管理系统的高质量交付和稳定运行。
|