forked from datawhale/whale-town-end
Compare commits
187 Commits
55cfda0532
...
feature/ch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd2a197288 | ||
| 01787d701c | |||
| 6e7de1a11a | |||
|
|
d92a078fc7 | ||
| 9785908ca9 | |||
| 592a745b8f | |||
|
|
cde20c6fd7 | ||
|
|
a8de2564b6 | ||
|
|
9f4d291619 | ||
|
|
4f18f0fec6 | ||
|
|
519394645a | ||
|
|
223ba2abb8 | ||
|
|
e54d5e3939 | ||
| 299627dac7 | |||
| ae3a256c52 | |||
|
|
434766beb5 | ||
| 97ea698f38 | |||
|
|
8132300e38 | ||
|
|
4265943375 | ||
|
|
7eceb6d6d6 | ||
|
|
662694ba9f | ||
|
|
ed04b8c92d | ||
|
|
30a4a2813d | ||
|
|
5bcf3cb678 | ||
|
|
3f3c29354e | ||
| 3cb2c1d8dd | |||
|
|
260ae2c559 | ||
|
|
cc1b081c3a | ||
|
|
ff996b0dea | ||
| 23bb3e0274 | |||
| 30d5e0f0a6 | |||
|
|
d5d175cd1c | ||
|
|
5bc7cdb532 | ||
| 963ebbd90d | |||
|
|
a147883e05 | ||
|
|
cf431c210a | ||
|
|
41c33d6159 | ||
|
|
8bcd22ea50 | ||
|
|
43874892b7 | ||
|
|
f1dd8cd14a | ||
|
|
f9a79461a0 | ||
|
|
73e3e0153c | ||
|
|
f7c3983cc1 | ||
| efbc5c4084 | |||
| 9948727e9d | |||
|
|
5af44f95d5 | ||
|
|
59128ea9a6 | ||
|
|
f5eda2ea34 | ||
|
|
efac782243 | ||
|
|
03f0cd6bab | ||
|
|
ea97167a32 | ||
|
|
e6de8a75b7 | ||
|
|
0cf2cf163c | ||
|
|
75ce4a2778 | ||
|
|
6459896b0a | ||
|
|
ac989fe985 | ||
|
|
7abd27aed0 | ||
|
|
1b4c952666 | ||
|
|
4b349e0cd9 | ||
|
|
267f1b2263 | ||
|
|
16ae78ed12 | ||
|
|
7ee0442641 | ||
|
|
57a059e58f | ||
|
|
ba8bd9cc7e | ||
|
|
c936961280 | ||
| 1a56e8da24 | |||
|
|
4d83b44ea5 | ||
|
|
b3181b54bc | ||
|
|
28bea2f001 | ||
|
|
c5a04b01a1 | ||
|
|
874ccfa879 | ||
|
|
a2d630d864 | ||
| 23c54215e1 | |||
| 7edd6e61b2 | |||
| dde3e03faf | |||
|
|
d04ab7f75f | ||
| f4ce162a38 | |||
| 086070c736 | |||
|
|
b9d5301801 | ||
| 53c5ef3af8 | |||
|
|
f09298617e | ||
|
|
ef04786207 | ||
| 5b56589dea | |||
|
|
ca21982857 | ||
|
|
f840d3e708 | ||
|
|
ef618d5222 | ||
|
|
9e0e07b07c | ||
|
|
3904a782c7 | ||
|
|
75ac7ac0f8 | ||
| 522f415f20 | |||
|
|
5f662ef091 | ||
|
|
8816b29b0a | ||
|
|
cbf4120ddd | ||
|
|
e9dc887c59 | ||
| ece4e6f5a2 | |||
|
|
931ccc4440 | ||
|
|
72bd69655e | ||
|
|
71bc317c57 | ||
|
|
c31cbe559d | ||
|
|
6924416bbd | ||
|
|
0f37130832 | ||
|
|
c2a1c6862d | ||
|
|
569a69c00e | ||
|
|
dd5cc48b49 | ||
|
|
bb796a2469 | ||
| 4fa4bd1a70 | |||
|
|
2bcbaeb030 | ||
|
|
dd91264d0c | ||
|
|
003091494f | ||
|
|
b01ea38a17 | ||
|
|
a30ef52c5a | ||
|
|
d1fc396db7 | ||
|
|
7fd6740090 | ||
|
|
4bda65d593 | ||
| 179f0f66eb | |||
| 1b380e4bb9 | |||
|
|
8f9a6e7f9d | ||
| 07d9c736fa | |||
| 5e1afc2875 | |||
|
|
3733717d1f | ||
|
|
470b0b8dbf | ||
| c2ecb3c1a7 | |||
|
|
6ad8d80449 | ||
| fcb81f80d9 | |||
| 065d3f2fc6 | |||
|
|
f335b72f6d | ||
|
|
3bf1b6f474 | ||
|
|
38f9f81b6c | ||
|
|
4818279fac | ||
|
|
270e7e5bd2 | ||
|
|
e282c9dd16 | ||
|
|
d8b7143f60 | ||
|
|
6002f53cbc | ||
| 9cb172d645 | |||
|
|
70c020a97c | ||
| 67ade48ad7 | |||
|
|
29b8b05a2a | ||
| bbf3476d75 | |||
|
|
faf93a30e1 | ||
|
|
2d10131838 | ||
|
|
5140bd1a54 | ||
|
|
3dd5f23d79 | ||
|
|
daaf5c3f22 | ||
| dd856b9ba6 | |||
|
|
07601b6d79 | ||
|
|
7429de3cf4 | ||
|
|
0192934c66 | ||
|
|
64370c3206 | ||
|
|
a78df48101 | ||
|
|
0005dc773c | ||
|
|
946d328be6 | ||
|
|
841a58886e | ||
|
|
91565f716d | ||
| 417b01323e | |||
| b3de6dec5f | |||
|
|
d683f0d5da | ||
|
|
aae77866ac | ||
|
|
8a19bb7daa | ||
| a8e29c6a46 | |||
|
|
9f606abbb2 | ||
|
|
7385c63ffd | ||
| 8d5a44d985 | |||
|
|
d59e9531e2 | ||
| 28a39935b7 | |||
|
|
68debdcb40 | ||
|
|
9ad98f74d9 | ||
| 578cba39a7 | |||
|
|
404ef5d3e0 | ||
| e537e782a9 | |||
|
|
cb25703892 | ||
| 64230db651 | |||
|
|
612755de63 | ||
|
|
e6d8c28806 | ||
|
|
47a738067a | ||
|
|
85d488a508 | ||
|
|
032c97a1fc | ||
| 0313b78852 | |||
|
|
d80d2c5cb8 | ||
| 2fb46967c7 | |||
|
|
43c9cbc863 | ||
|
|
a4a3a60db7 | ||
|
|
8166c95af4 | ||
|
|
ec2e346ded | ||
|
|
dd4fb6edd3 | ||
| 17c16588aa | |||
| 8fc2b53c00 | |||
|
|
11387f7046 |
57
.env.example
57
.env.example
@@ -15,6 +15,29 @@ NODE_ENV=development
|
||||
PORT=3000
|
||||
LOG_LEVEL=debug
|
||||
|
||||
# ===========================================
|
||||
# 测试用户配置
|
||||
# ===========================================
|
||||
# 用于测试邮箱冲突逻辑的真实用户
|
||||
TEST_USER_EMAIL=your_test_email@example.com
|
||||
TEST_USER_USERNAME=your_test_username
|
||||
TEST_USER_PASSWORD=your_test_password
|
||||
TEST_USER_NICKNAME=测试用户
|
||||
|
||||
# ===========================================
|
||||
# 管理员后台配置(开发环境推荐配置)
|
||||
# ===========================================
|
||||
# 管理员Token签名密钥(至少16字符,生产环境务必使用强随机值)
|
||||
ADMIN_TOKEN_SECRET=dev_admin_token_secret_change_me_32chars
|
||||
# 管理员Token有效期(秒),默认8小时
|
||||
ADMIN_TOKEN_TTL_SECONDS=28800
|
||||
|
||||
# 启动引导创建管理员账号(仅当 enabled=true 时生效)
|
||||
ADMIN_BOOTSTRAP_ENABLED=true
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=Admin123456
|
||||
ADMIN_NICKNAME=管理员
|
||||
|
||||
# JWT 配置
|
||||
JWT_SECRET=test_jwt_secret_key_for_development_only_32chars
|
||||
JWT_EXPIRES_IN=7d
|
||||
@@ -31,11 +54,11 @@ REDIS_DB=0
|
||||
# ===========================================
|
||||
|
||||
# 数据库配置(生产环境取消注释)
|
||||
# DB_HOST=your_mysql_host
|
||||
# DB_PORT=3306
|
||||
# DB_USERNAME=your_db_username
|
||||
# DB_PASSWORD=your_db_password
|
||||
# DB_NAME=your_db_name
|
||||
DB_HOST=your_mysql_host
|
||||
DB_PORT=3306
|
||||
DB_USERNAME=your_db_username
|
||||
DB_PASSWORD=your_db_password
|
||||
DB_NAME=your_db_name
|
||||
|
||||
# Redis 配置(生产环境取消注释并设置 USE_FILE_REDIS=false)
|
||||
# USE_FILE_REDIS=false
|
||||
@@ -45,12 +68,12 @@ REDIS_DB=0
|
||||
# REDIS_DB=0
|
||||
|
||||
# 邮件服务配置(生产环境取消注释)
|
||||
# EMAIL_HOST=smtp.gmail.com
|
||||
# EMAIL_PORT=587
|
||||
# EMAIL_SECURE=false
|
||||
# EMAIL_USER=your_email@gmail.com
|
||||
# EMAIL_PASS=your_app_password
|
||||
# EMAIL_FROM="Whale Town Game" <noreply@whaletown.com>
|
||||
EMAIL_HOST=smtp.163.com
|
||||
EMAIL_PORT=465
|
||||
EMAIL_SECURE=true
|
||||
EMAIL_USER=your_email@163.com
|
||||
EMAIL_PASS=your_email_app_password
|
||||
EMAIL_FROM="whaletown <your_email@163.com>"
|
||||
|
||||
# 生产环境设置(生产环境取消注释)
|
||||
# NODE_ENV=production
|
||||
@@ -60,13 +83,19 @@ REDIS_DB=0
|
||||
# Zulip 集成配置
|
||||
# ===========================================
|
||||
|
||||
# Zulip 配置模式
|
||||
# static: 使用静态配置文件 (config/zulip/map-config.json)
|
||||
# dynamic: 从Zulip服务器动态获取Stream作为地图
|
||||
# hybrid: 混合模式,优先动态,回退静态 (推荐)
|
||||
ZULIP_CONFIG_MODE=hybrid
|
||||
|
||||
# Zulip 服务器配置
|
||||
ZULIP_SERVER_URL=https://zulip.xinghangee.icu/
|
||||
ZULIP_BOT_EMAIL=cbot-bot@zulip.xinghangee.icu
|
||||
ZULIP_SERVER_URL=https://your-zulip-server.com/
|
||||
ZULIP_BOT_EMAIL=your-bot@your-zulip-server.com
|
||||
ZULIP_BOT_API_KEY=your_bot_api_key
|
||||
|
||||
# Zulip API Key加密密钥(生产环境必须配置,至少32字符)
|
||||
# ZULIP_API_KEY_ENCRYPTION_KEY=your_32_character_encryption_key_here
|
||||
ZULIP_API_KEY_ENCRYPTION_KEY=your_32_character_encryption_key_here
|
||||
|
||||
# Zulip 错误处理配置
|
||||
ZULIP_DEGRADED_MODE_ENABLED=false
|
||||
|
||||
@@ -16,6 +16,16 @@ PORT=3000
|
||||
JWT_SECRET=your_jwt_secret_key_here_at_least_32_characters
|
||||
JWT_EXPIRES_IN=7d
|
||||
|
||||
# 管理员后台配置(生产环境必须配置)
|
||||
ADMIN_TOKEN_SECRET=please_use_a_strong_random_secret_at_least_32_chars
|
||||
ADMIN_TOKEN_TTL_SECONDS=28800
|
||||
|
||||
# 启动引导创建管理员账号(建议仅首次部署临时开启,创建后关闭)
|
||||
ADMIN_BOOTSTRAP_ENABLED=false
|
||||
# ADMIN_USERNAME=admin
|
||||
# ADMIN_PASSWORD=please_set_a_strong_password
|
||||
# ADMIN_NICKNAME=管理员
|
||||
|
||||
# Redis 配置(用于验证码存储)
|
||||
# 生产环境使用真实Redis服务
|
||||
USE_FILE_REDIS=false
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -44,3 +44,9 @@ coverage/
|
||||
|
||||
# Redis数据文件(本地开发用)
|
||||
redis-data/
|
||||
|
||||
.kiro/
|
||||
|
||||
config/
|
||||
docs/merge-requests
|
||||
docs/ai-reading/me.config.json
|
||||
@@ -1,98 +0,0 @@
|
||||
# 贡献者名单
|
||||
|
||||
感谢所有为 Whale Town 项目做出贡献的开发者们!🎉
|
||||
|
||||
## 核心贡献者
|
||||
|
||||
### 🏆 主要维护者
|
||||
|
||||
**moyin** - 主要维护者
|
||||
- Gitea: [@moyin](https://gitea.xinghangee.icu/moyin)
|
||||
- Email: xinghang_a@proton.me
|
||||
- 提交数: **66 commits**
|
||||
- 主要贡献:
|
||||
- 🚀 项目架构设计与初始化
|
||||
- 🔐 完整用户认证系统实现
|
||||
- 📧 邮箱验证系统设计与开发
|
||||
- 🗄️ Redis缓存服务(文件存储+真实Redis双模式)
|
||||
- 📝 完整的API文档系统(Swagger UI + OpenAPI)
|
||||
- 🧪 测试框架搭建与114个测试用例编写
|
||||
- 📊 高性能日志系统集成(Pino)
|
||||
- 🔧 项目配置优化与部署方案
|
||||
- 🐛 验证码TTL重置关键问题修复
|
||||
- 📚 完整的项目文档体系建设
|
||||
|
||||
### 🌟 核心开发者
|
||||
|
||||
**angjustinl** - 核心开发者
|
||||
- Gitea: [@ANGJustinl](https://gitea.xinghangee.icu/ANGJustinl)
|
||||
- GitHub: [@ANGJustinl](https://github.com/ANGJustinl)
|
||||
- Email: 96008766+ANGJustinl@users.noreply.github.com
|
||||
- 提交数: **2 commits**
|
||||
- 主要贡献:
|
||||
- 🔄 邮箱验证流程重构与优化
|
||||
- 💾 基于内存的用户服务实现
|
||||
- 🛠️ API响应处理改进
|
||||
- 🧪 测试用例完善与错误修复
|
||||
- 📚 系统架构优化
|
||||
|
||||
**jianuo** - 核心开发者
|
||||
- Gitea: [@jianuo](https://gitea.xinghangee.icu/jianuo)
|
||||
- Email: 32106500027@e.gzhu.edu.cn
|
||||
- 提交数: **3 commits**
|
||||
- 主要贡献:
|
||||
- 🐳 Docker部署问题修复
|
||||
- 📖 项目文档错误修复
|
||||
- 🔧 部署配置优化
|
||||
|
||||
## 贡献统计
|
||||
|
||||
| 贡献者 | 提交数 | 主要领域 | 贡献占比 |
|
||||
|--------|--------|----------|----------|
|
||||
| moyin | 66 | 架构设计、核心功能、文档、测试 | 93% |
|
||||
| jianuo | 3 | 部署、文档 | 4% |
|
||||
| angjustinl | 2 | 功能优化、测试、重构 | 3% |
|
||||
|
||||
## 项目里程碑
|
||||
|
||||
### 2025年12月
|
||||
- **12月17日**: 项目初始化,完成基础架构搭建
|
||||
- **12月17日**: 实现完整的用户认证系统
|
||||
- **12月17日**: 完成API文档系统集成
|
||||
- **12月17日**: 实现邮箱验证系统
|
||||
- **12月17日**: 修复验证码TTL重置关键问题
|
||||
- **12月18日**: angjustinl重构邮箱验证流程,引入内存用户服务
|
||||
- **12月18日**: jianuo修复Docker部署问题
|
||||
- **12月18日**: 完成测试用例修复和优化
|
||||
|
||||
## 如何成为贡献者
|
||||
|
||||
我们欢迎所有形式的贡献!无论是:
|
||||
|
||||
- 🐛 **Bug修复** - 发现并修复问题
|
||||
- ✨ **新功能** - 添加有价值的功能
|
||||
- 📚 **文档改进** - 完善项目文档
|
||||
- 🧪 **测试用例** - 提高代码覆盖率
|
||||
- 🎨 **代码优化** - 改进代码质量
|
||||
- 💡 **建议反馈** - 提出改进建议
|
||||
|
||||
### 贡献流程
|
||||
|
||||
1. Fork 项目到你的Gitea账户
|
||||
2. 创建功能分支:`git checkout -b feature/your-feature`
|
||||
3. 提交你的更改:`git commit -m "feat:添加新功能"`
|
||||
4. 推送到分支:`git push origin feature/your-feature`
|
||||
5. 创建Pull Request
|
||||
|
||||
### 贡献规范
|
||||
|
||||
请在贡献前阅读:
|
||||
- [AI辅助开发规范指南](./docs/AI辅助开发规范指南.md)
|
||||
- [后端开发规范](./docs/backend_development_guide.md)
|
||||
- [Git提交规范](./docs/git_commit_guide.md)
|
||||
|
||||
---
|
||||
|
||||
**再次感谢所有贡献者的辛勤付出!** 🙏
|
||||
|
||||
*如果你的名字没有出现在列表中,请联系我们或提交PR更新此文件。*
|
||||
216
DEPLOYMENT.md
216
DEPLOYMENT.md
@@ -1,216 +0,0 @@
|
||||
# 部署指南
|
||||
|
||||
本文档详细说明如何部署 Pixel Game Server 到生产环境。
|
||||
|
||||
## 前置要求
|
||||
|
||||
- Node.js 18+
|
||||
- pnpm 包管理器
|
||||
- MySQL 8.0+
|
||||
- PM2 进程管理器(推荐)
|
||||
- Nginx(可选,用于反向代理)
|
||||
|
||||
## 部署步骤
|
||||
|
||||
### 1. 服务器环境准备
|
||||
|
||||
```bash
|
||||
# 安装 Node.js (使用 NodeSource 仓库)
|
||||
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
|
||||
# 安装 pnpm
|
||||
curl -fsSL https://get.pnpm.io/install.sh | sh
|
||||
source ~/.bashrc
|
||||
|
||||
# 安装 PM2
|
||||
npm install -g pm2
|
||||
|
||||
# 安装 MySQL
|
||||
sudo apt update
|
||||
sudo apt install mysql-server
|
||||
sudo mysql_secure_installation
|
||||
```
|
||||
|
||||
### 2. 克隆项目
|
||||
|
||||
```bash
|
||||
# 创建项目目录
|
||||
sudo mkdir -p /var/www
|
||||
cd /var/www
|
||||
|
||||
# 克隆项目(替换为你的实际仓库地址)
|
||||
git clone https://gitea.xinghangee.icu/datawhale/whale-town-end.git
|
||||
cd whale-town-end
|
||||
```
|
||||
|
||||
### 3. 配置环境
|
||||
|
||||
```bash
|
||||
# 复制环境配置文件
|
||||
cp .env.production.example .env.production
|
||||
|
||||
# 编辑环境配置(填入实际的数据库信息)
|
||||
nano .env.production
|
||||
|
||||
# 复制部署脚本
|
||||
cp deploy.sh.example deploy.sh
|
||||
chmod +x deploy.sh
|
||||
|
||||
# 编辑部署脚本(修改路径配置)
|
||||
nano deploy.sh
|
||||
|
||||
# 复制 webhook 处理器
|
||||
cp webhook-handler.js.example webhook-handler.js
|
||||
|
||||
# 编辑 webhook 处理器(修改密钥和路径)
|
||||
nano webhook-handler.js
|
||||
```
|
||||
|
||||
### 4. 数据库设置
|
||||
|
||||
```bash
|
||||
# 登录 MySQL
|
||||
sudo mysql -u root -p
|
||||
|
||||
# 创建数据库和用户
|
||||
CREATE DATABASE pixel_game_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
CREATE USER 'pixel_game'@'localhost' IDENTIFIED BY 'your_secure_password';
|
||||
GRANT ALL PRIVILEGES ON pixel_game_db.* TO 'pixel_game'@'localhost';
|
||||
FLUSH PRIVILEGES;
|
||||
EXIT;
|
||||
```
|
||||
|
||||
### 5. 安装依赖和构建
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
# 构建项目
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
### 6. 启动服务
|
||||
|
||||
```bash
|
||||
# 使用 PM2 启动应用
|
||||
pm2 start ecosystem.config.js --env production
|
||||
|
||||
# 保存 PM2 配置
|
||||
pm2 save
|
||||
|
||||
# 设置开机自启
|
||||
pm2 startup
|
||||
# 按照提示执行显示的命令
|
||||
```
|
||||
|
||||
### 7. 配置 Nginx(可选)
|
||||
|
||||
创建 Nginx 配置文件:
|
||||
|
||||
```bash
|
||||
sudo nano /etc/nginx/sites-available/whale-town-end
|
||||
```
|
||||
|
||||
添加以下内容:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
location /webhook {
|
||||
proxy_pass http://localhost:9000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
启用站点:
|
||||
|
||||
```bash
|
||||
sudo ln -s /etc/nginx/sites-available/whale-town-end /etc/nginx/sites-enabled/
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
## Gitea Webhook 配置
|
||||
|
||||
1. 在 Gitea 仓库中进入 **Settings** → **Webhooks**
|
||||
2. 点击 **Add Webhook** → **Gitea**
|
||||
3. 配置:
|
||||
- **Target URL**: `http://your-server.com:9000/webhook` 或 `http://your-domain.com/webhook`
|
||||
- **HTTP Method**: `POST`
|
||||
- **POST Content Type**: `application/json`
|
||||
- **Secret**: 与 `webhook-handler.js` 中的 `SECRET` 一致
|
||||
- **Trigger On**: 选择 `Push events`
|
||||
- **Branch filter**: `main`
|
||||
|
||||
## 验证部署
|
||||
|
||||
```bash
|
||||
# 检查服务状态
|
||||
pm2 status
|
||||
|
||||
# 查看日志
|
||||
pm2 logs whale-town-end
|
||||
pm2 logs webhook-handler
|
||||
|
||||
# 测试 API
|
||||
curl http://localhost:3000/
|
||||
curl http://localhost:3000/api-docs
|
||||
```
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
# 重启服务
|
||||
pm2 restart whale-town-end
|
||||
|
||||
# 查看日志
|
||||
pm2 logs whale-town-end --lines 100
|
||||
|
||||
# 手动部署
|
||||
bash deploy.sh
|
||||
|
||||
# 更新代码(不重启)
|
||||
git pull origin main
|
||||
pnpm install
|
||||
pnpm run build
|
||||
pm2 reload whale-town-end
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 服务无法启动
|
||||
- 检查环境变量配置
|
||||
- 检查数据库连接
|
||||
- 查看 PM2 日志
|
||||
|
||||
### Webhook 不工作
|
||||
- 检查防火墙设置
|
||||
- 验证 webhook URL 可访问性
|
||||
- 检查 Gitea webhook 日志
|
||||
- 验证签名密钥是否一致
|
||||
|
||||
### 数据库连接失败
|
||||
- 检查 MySQL 服务状态
|
||||
- 验证数据库用户权限
|
||||
- 检查网络连接
|
||||
31
Dockerfile
31
Dockerfile
@@ -1,31 +0,0 @@
|
||||
# 使用官方 Node.js 镜像
|
||||
FROM node:lts-alpine
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 设置构建参数
|
||||
ARG NPM_REGISTRY=https://registry.npmmirror.com
|
||||
|
||||
# 设置 npm 和 pnpm 镜像源
|
||||
RUN npm config set registry ${NPM_REGISTRY} && \
|
||||
npm install -g pnpm && \
|
||||
pnpm config set registry ${NPM_REGISTRY}
|
||||
|
||||
# 复制 package.json
|
||||
COPY package.json pnpm-workspace.yaml ./
|
||||
|
||||
# 安装依赖
|
||||
RUN pnpm install
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 构建应用
|
||||
RUN pnpm run build
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 3000
|
||||
|
||||
# 启动应用
|
||||
CMD ["pnpm", "run", "start:prod"]
|
||||
420
README.md
420
README.md
@@ -1,33 +1,30 @@
|
||||
# 🐋 Whale Town - 像素游戏后端服务
|
||||
|
||||
> 一个基于 NestJS 的现代化 2D 像素风游戏后端服务,支持实时通信、用户认证、邮箱验证等完整功能。
|
||||
> 基于 NestJS 的现代化 2D 像素游戏后端,采用四层架构(Gateway-Business-Core-Data),支持用户认证、实时通信、Zulip集成、管理员后台。
|
||||
|
||||
[](https://nodejs.org/)
|
||||
[](https://nestjs.com/)
|
||||
[](https://nestjs.com/)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](./LICENSE)
|
||||
[](https://reactjs.org/)
|
||||
[](#)
|
||||
|
||||
## 🎯 项目简介
|
||||
## 🎯 核心特性
|
||||
|
||||
Whale Town 是一个功能完整的像素游戏后端服务,提供:
|
||||
|
||||
- 🔐 **完整用户认证系统** - 支持邮箱验证、密码重置、第三方登录
|
||||
- 📧 **智能邮件服务** - 支持测试模式和生产模式自动切换
|
||||
- 🗄️ **灵活存储方案** - Redis文件存储 + 内存数据库,支持无依赖测试
|
||||
- 🚀 **高性能架构** - 基于NestJS,支持WebSocket实时通信
|
||||
- 📚 **完整API文档** - Swagger UI + OpenAPI规范
|
||||
- 🧪 **全面测试覆盖** - 单元测试 + API功能测试
|
||||
|
||||
---
|
||||
- 🔐 用户认证:多方式登录、验证码登录、GitHub OAuth
|
||||
- 🌐 实时通信:原生WebSocket、位置广播、地图房间管理
|
||||
- 💬 Zulip集成:游戏内聊天与Zulip社群双向同步
|
||||
- 👑 管理员后台:React界面、用户管理、日志监控
|
||||
- 🛡️ 安全防护:频率限制、维护模式、JWT认证
|
||||
- 🗄️ 灵活存储:MySQL/内存双模式、Redis/文件双模式
|
||||
- 📚 完整文档:Swagger UI、WebSocket测试工具
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 📋 环境要求
|
||||
### 环境要求
|
||||
- Node.js >= 18.0.0
|
||||
- pnpm >= 8.0.0
|
||||
|
||||
- **Node.js** >= 18.0.0 (推荐 24.7.0)
|
||||
- **pnpm** >= 8.0.0 (推荐 10.25.0)
|
||||
|
||||
### 🛠️ 安装与运行
|
||||
### 安装运行
|
||||
|
||||
```bash
|
||||
# 1. 克隆项目
|
||||
@@ -37,355 +34,188 @@ cd whale-town-end
|
||||
# 2. 安装依赖
|
||||
pnpm install
|
||||
|
||||
# 3. 配置环境(测试模式,无需数据库和邮件服务器)
|
||||
# 3. 配置环境(测试模式,无需数据库)
|
||||
cp .env.example .env
|
||||
|
||||
# 4. 启动开发服务器
|
||||
# 4. 启动服务
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
🎉 **服务启动成功!** 访问 http://localhost:3000
|
||||
访问:http://localhost:3000
|
||||
|
||||
### 🧪 快速测试
|
||||
### 前端管理界面
|
||||
|
||||
```bash
|
||||
# Windows
|
||||
.\test-api.ps1
|
||||
|
||||
# Linux/macOS
|
||||
./test-api.sh
|
||||
# 启动管理后台
|
||||
cd client
|
||||
pnpm install
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
**测试内容:**
|
||||
- ✅ 邮箱验证码发送与验证
|
||||
- ✅ 用户注册与登录
|
||||
- ✅ Redis文件存储功能
|
||||
- ✅ 邮件测试模式
|
||||
访问:http://localhost:5173
|
||||
默认账号:admin / Admin123456
|
||||
|
||||
---
|
||||
### 在线体验
|
||||
|
||||
## 🎓 新开发者指南
|
||||
- API文档:https://whaletownend.xinghangee.icu/api-docs
|
||||
- WebSocket测试:https://whaletownend.xinghangee.icu/websocket-test
|
||||
|
||||
### 第一步:了解项目规范 📚
|
||||
## 🏗️ 项目架构
|
||||
|
||||
**⚠️ 重要:在开始开发前,请务必阅读以下文档**
|
||||
|
||||
1. **[AI辅助开发规范指南](./docs/AI辅助开发规范指南.md)** 🤖
|
||||
- 学会使用AI助手提升开发效率300%
|
||||
- 自动生成符合规范的代码和注释
|
||||
- 实时检查代码质量
|
||||
|
||||
2. **[后端开发规范](./docs/backend_development_guide.md)** 📝
|
||||
- 代码注释标准
|
||||
- 业务逻辑设计原则
|
||||
- 日志记录要求
|
||||
|
||||
3. **[Git提交规范](./docs/git_commit_guide.md)** 🔄
|
||||
- 提交信息格式
|
||||
- 分支管理策略
|
||||
|
||||
### 第二步:熟悉项目架构 🏗️
|
||||
### 四层架构设计
|
||||
|
||||
```
|
||||
项目根目录/
|
||||
├── src/ # 源代码目录
|
||||
│ ├── api/ # API接口层(预留,用于游戏相关控制器)
|
||||
│ ├── business/ # 业务逻辑层
|
||||
│ │ └── login/ # 登录业务模块
|
||||
│ ├── core/ # 核心功能模块
|
||||
│ │ ├── db/ # 数据库层
|
||||
│ │ │ └── users/ # 用户数据模型(支持MySQL/内存双模式)
|
||||
│ │ ├── redis/ # Redis缓存服务(支持真实Redis/文件存储)
|
||||
│ │ ├── login_core/ # 登录核心服务
|
||||
│ │ └── utils/ # 工具服务
|
||||
│ │ ├── email/ # 邮件服务(支持SMTP/测试模式)
|
||||
│ │ ├── verification/ # 验证码服务
|
||||
│ │ └── logger/ # 日志系统
|
||||
│ ├── dto/ # 数据传输对象
|
||||
│ ├── types/ # TypeScript类型定义
|
||||
│ ├── app.module.ts # 应用主模块
|
||||
│ └── main.ts # 应用入口
|
||||
Gateway Layer (网关层)
|
||||
↓ HTTP/WebSocket协议处理、数据验证
|
||||
Business Layer (业务层)
|
||||
↓ 业务逻辑实现、服务协调
|
||||
Core Layer (核心层)
|
||||
↓ 技术基础设施、数据访问
|
||||
Data Layer (数据层)
|
||||
↓ 数据持久化、缓存管理
|
||||
```
|
||||
|
||||
### 目录结构
|
||||
|
||||
```
|
||||
whale-town-end/
|
||||
├── src/
|
||||
│ ├── gateway/ # 网关层:auth, location_broadcast
|
||||
│ ├── business/ # 业务层:auth, user_mgmt, admin, zulip, notice
|
||||
│ ├── core/ # 核心层:db, redis, login_core, admin_core, utils
|
||||
│ ├── app.module.ts
|
||||
│ └── main.ts
|
||||
├── client/ # React管理界面
|
||||
├── docs/ # 项目文档
|
||||
│ ├── api/ # API文档
|
||||
│ └── systems/ # 系统设计文档
|
||||
├── test/ # 测试文件
|
||||
├── redis-data/ # Redis文件存储数据
|
||||
├── logs/ # 日志文件
|
||||
└── 配置文件 # .env, package.json, tsconfig.json等
|
||||
└── config/ # 配置文件
|
||||
```
|
||||
|
||||
**架构特点:**
|
||||
- 🏗️ **分层架构** - API层 → 业务层 → 核心层 → 数据层
|
||||
- 🔄 **双模式支持** - 开发测试模式 + 生产部署模式
|
||||
- 📦 **模块化设计** - 每个功能独立模块,便于维护扩展
|
||||
- 🧪 **测试友好** - 完整的单元测试和集成测试覆盖
|
||||
|
||||
### 第三步:体验核心功能 🎮
|
||||
|
||||
1. **API文档系统** 📖
|
||||
```bash
|
||||
# 启动服务后访问
|
||||
http://localhost:3000/api-docs
|
||||
```
|
||||
|
||||
2. **用户认证系统** 🔐
|
||||
- 邮箱验证码注册
|
||||
- 多方式登录(用户名/邮箱/手机号)
|
||||
- 密码重置功能
|
||||
|
||||
3. **实时通信** 🌐
|
||||
- WebSocket支持
|
||||
- Socket.IO集成
|
||||
|
||||
### 第四步:开始贡献 🤝
|
||||
|
||||
1. **Fork项目** 到你的Gitea账户
|
||||
2. **创建功能分支**:`git checkout -b feature/your-feature`
|
||||
3. **遵循规范开发**(使用AI助手帮助)
|
||||
4. **提交代码**:`git commit -m "feat:添加新功能"`
|
||||
5. **创建Pull Request**
|
||||
|
||||
---
|
||||
详细架构:[docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md)
|
||||
|
||||
## 🛠️ 技术栈
|
||||
|
||||
### 🚀 核心框架
|
||||
- **NestJS** `^11.1.9` - 企业级Node.js框架,提供依赖注入、模块化等特性
|
||||
- **TypeScript** `^5.9.3` - 类型安全的JavaScript超集
|
||||
- **Express** `^10.4.20` - 基于Express的HTTP服务器
|
||||
- **RxJS** `^7.8.2` - 响应式编程库,处理异步数据流
|
||||
**后端:** NestJS 11 + TypeScript 5 + MySQL + Redis + WebSocket
|
||||
**前端:** React 18 + Vite 7 + Ant Design 5
|
||||
**测试:** Jest + Supertest(99个测试用例)
|
||||
**部署:** Docker + PM2 + Nginx
|
||||
|
||||
### 🌐 实时通信
|
||||
- **Socket.IO** `^10.4.20` - WebSocket实时双向通信
|
||||
- **@nestjs/websockets** - NestJS WebSocket网关支持
|
||||
- **@nestjs/platform-socket.io** - Socket.IO平台适配器
|
||||
|
||||
### 🗄️ 数据存储
|
||||
- **TypeORM** `^0.3.28` - 强大的ORM框架,支持多种数据库
|
||||
- **MySQL2** `^3.16.0` - 高性能MySQL驱动
|
||||
- **IORedis** `^5.8.2` - Redis客户端,支持集群和哨兵模式
|
||||
- **文件存储** - 自研Redis文件存储适配器,支持无Redis开发
|
||||
|
||||
### 🔐 安全认证
|
||||
- **bcrypt** `^6.0.0` - 密码加密哈希算法
|
||||
- **class-validator** `^0.14.3` - 数据验证装饰器
|
||||
- **class-transformer** `^0.5.1` - 对象转换和序列化
|
||||
|
||||
### 📧 通信服务
|
||||
- **Nodemailer** `^6.10.1` - 邮件发送服务
|
||||
- **Axios** `^1.13.2` - HTTP客户端,支持第三方API调用
|
||||
|
||||
### 📚 API文档
|
||||
- **Swagger UI** `^5.0.1` - 交互式API文档界面
|
||||
- **@nestjs/swagger** `^11.2.3` - NestJS Swagger集成
|
||||
|
||||
### 📊 日志监控
|
||||
- **Pino** `^10.1.0` - 高性能结构化日志库
|
||||
- **nestjs-pino** `^4.5.0` - NestJS Pino集成
|
||||
- **pino-pretty** `^13.1.3` - Pino日志美化输出
|
||||
|
||||
### 🧪 测试框架
|
||||
- **Jest** `^29.7.0` - JavaScript测试框架
|
||||
- **Supertest** `^7.1.4` - HTTP断言测试
|
||||
- **@nestjs/testing** `^10.4.20` - NestJS测试工具
|
||||
|
||||
### ⚙️ 开发工具
|
||||
- **@nestjs/cli** `^10.4.9` - NestJS命令行工具
|
||||
- **ts-jest** `^29.2.5` - TypeScript Jest支持
|
||||
- **ts-node** `^10.9.2` - TypeScript运行时
|
||||
- **pnpm** - 快速、节省磁盘空间的包管理器
|
||||
|
||||
### 🔄 任务调度
|
||||
- **@nestjs/schedule** `^4.1.2` - 定时任务和计划任务支持
|
||||
|
||||
### 📦 构建部署
|
||||
- **Docker** - 容器化部署
|
||||
- **PM2** - 生产环境进程管理
|
||||
- **Nginx** - 反向代理和负载均衡
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 核心功能
|
||||
|
||||
### 🔐 用户认证系统
|
||||
- **多方式登录** - 用户名/邮箱/手机号
|
||||
- **邮箱验证** - 完整的验证码流程
|
||||
- **密码安全** - bcrypt加密 + 强度验证
|
||||
- **第三方登录** - GitHub OAuth支持
|
||||
- **权限控制** - 基于角色的访问控制
|
||||
|
||||
### 📧 智能邮件服务
|
||||
- **测试模式** - 控制台输出,无需SMTP服务器
|
||||
- **生产模式** - 支持主流邮件服务商
|
||||
- **模板系统** - 验证码、欢迎邮件等模板
|
||||
- **自动切换** - 根据配置自动选择模式
|
||||
|
||||
### 🗄️ 灵活存储方案
|
||||
- **Redis文件存储** - 开发测试无需Redis服务器
|
||||
- **内存数据库** - 无需MySQL即可运行
|
||||
- **生产就绪** - 支持MySQL + Redis部署
|
||||
- **自动切换** - 根据配置自动选择存储方式
|
||||
|
||||
### 📚 完整API文档
|
||||
- **Swagger UI** - 交互式API文档
|
||||
- **OpenAPI规范** - 标准化接口描述
|
||||
- **Postman集合** - 可导入的测试集合
|
||||
- **实时更新** - 代码变更自动同步文档
|
||||
|
||||
### 🧪 全面测试覆盖
|
||||
- **单元测试** - 114个测试用例全部通过
|
||||
- **API测试** - 跨平台测试脚本
|
||||
- **集成测试** - 完整业务流程验证
|
||||
- **测试模式** - 无依赖快速测试
|
||||
|
||||
---
|
||||
|
||||
## 📊 开发与测试
|
||||
|
||||
### 🔧 开发命令
|
||||
## 📊 开发命令
|
||||
|
||||
```bash
|
||||
# 开发服务器(热重载)
|
||||
pnpm run dev
|
||||
# 开发
|
||||
pnpm run dev # 启动开发服务器
|
||||
pnpm run build # 构建项目
|
||||
pnpm run start:prod # 生产环境运行
|
||||
|
||||
# 构建项目
|
||||
pnpm run build
|
||||
|
||||
# 生产环境运行
|
||||
pnpm run start:prod
|
||||
|
||||
# 代码检查
|
||||
pnpm run lint
|
||||
|
||||
# 格式化代码
|
||||
pnpm run format
|
||||
# 测试
|
||||
pnpm test # 运行单元测试
|
||||
pnpm run test:cov # 测试覆盖率
|
||||
.\test-comprehensive.ps1 # API功能测试
|
||||
```
|
||||
|
||||
### 🧪 测试命令
|
||||
## 🌍 环境配置
|
||||
|
||||
### 开发环境(默认)
|
||||
```bash
|
||||
# 运行所有单元测试
|
||||
pnpm test
|
||||
|
||||
# 监听模式运行测试
|
||||
pnpm run test:watch
|
||||
|
||||
# 生成测试覆盖率报告
|
||||
pnpm run test:cov
|
||||
|
||||
# API功能测试
|
||||
.\test-api.ps1 # Windows
|
||||
./test-api.sh # Linux/macOS
|
||||
```
|
||||
|
||||
### 📈 测试覆盖率
|
||||
|
||||
- **单元测试**: 114个测试用例 ✅
|
||||
- **功能测试**: 用户认证、邮件验证、数据存储 ✅
|
||||
- **集成测试**: 完整业务流程 ✅
|
||||
|
||||
---
|
||||
|
||||
## 🌍 部署配置
|
||||
|
||||
### 测试环境(默认)
|
||||
```bash
|
||||
# 无需数据库和邮件服务器
|
||||
USE_FILE_REDIS=true
|
||||
USE_FILE_REDIS=true # 使用文件存储(无需Redis)
|
||||
NODE_ENV=development
|
||||
# 数据库和邮件配置保持注释状态
|
||||
# 无需配置数据库和邮件
|
||||
```
|
||||
|
||||
### 生产环境
|
||||
```bash
|
||||
# 启用真实服务
|
||||
USE_FILE_REDIS=false
|
||||
NODE_ENV=production
|
||||
|
||||
# 配置数据库
|
||||
# 数据库
|
||||
DB_HOST=your_mysql_host
|
||||
DB_USERNAME=your_username
|
||||
DB_PASSWORD=your_password
|
||||
|
||||
# 配置Redis
|
||||
# Redis
|
||||
REDIS_HOST=your_redis_host
|
||||
REDIS_PASSWORD=your_password
|
||||
|
||||
# 配置邮件服务
|
||||
EMAIL_HOST=smtp.gmail.com
|
||||
EMAIL_USER=your_email@gmail.com
|
||||
EMAIL_PASS=your_app_password
|
||||
# 邮件
|
||||
EMAIL_HOST=smtp.163.com
|
||||
EMAIL_USER=your_email@163.com
|
||||
EMAIL_PASS=your_password
|
||||
|
||||
# Zulip
|
||||
ZULIP_SERVER_URL=https://your-zulip.com/
|
||||
ZULIP_BOT_API_KEY=your_api_key
|
||||
```
|
||||
|
||||
详细部署指南:[DEPLOYMENT.md](./DEPLOYMENT.md)
|
||||
详细配置:[docs/deployment/DEPLOYMENT.md](./docs/deployment/DEPLOYMENT.md)
|
||||
|
||||
---
|
||||
## 📚 文档
|
||||
|
||||
## 📚 文档中心
|
||||
- [架构设计](./docs/ARCHITECTURE.md) - 四层架构详解
|
||||
- [开发规范](./docs/development/backend_development_guide.md) - 代码规范
|
||||
- [Git规范](./docs/development/git_commit_guide.md) - 提交规范
|
||||
- [API文档](http://localhost:3000/api-docs) - Swagger UI
|
||||
- [测试指南](./docs/development/TESTING.md) - 测试说明
|
||||
|
||||
### 🎯 新手必读
|
||||
1. **[AI辅助开发指南](./docs/AI辅助开发规范指南.md)** - 提升开发效率300%
|
||||
2. **[后端开发规范](./docs/backend_development_guide.md)** - 代码质量标准
|
||||
3. **[Git提交规范](./docs/git_commit_guide.md)** - 版本控制最佳实践
|
||||
### 🤖 AI代码检查指南
|
||||
|
||||
### 📖 API文档
|
||||
- **[Swagger UI](http://localhost:3000/api-docs)** - 交互式API文档
|
||||
- **[API文档总览](./docs/api/README.md)** - 使用指南
|
||||
- **[OpenAPI规范](./docs/api/openapi.yaml)** - 标准化描述
|
||||
- **[Postman集合](./docs/api/postman-collection.json)** - 测试集合
|
||||
项目提供了完整的AI辅助代码检查流程,帮助确保代码质量和规范性。
|
||||
|
||||
### 🏗️ 系统设计
|
||||
- **[用户认证系统](./docs/systems/user-auth/README.md)** - 认证架构设计
|
||||
- **[邮件验证系统](./docs/systems/email-verification/README.md)** - 验证流程设计
|
||||
- **[日志系统](./docs/systems/logger/README.md)** - 日志架构设计
|
||||
**快速开始:**
|
||||
|
||||
### 🧪 测试指南
|
||||
- **[测试指南](./TESTING.md)** - 完整测试说明
|
||||
- **[单元测试](./src/**/*.spec.ts)** - 测试用例参考
|
||||
向AI发送以下prompt开始代码检查:
|
||||
|
||||
---
|
||||
```
|
||||
请使用 docs/ai-reading 中readme的规范对 [模块路径] 进行完整的代码检查。
|
||||
```
|
||||
|
||||
## 🤝 贡献者
|
||||
**如何使用:**
|
||||
- AI会按照7个步骤逐步执行检查(命名规范、注释标准、代码质量、架构层级、测试覆盖、文档生成、代码提交)
|
||||
- 每个步骤完成后会提供检查报告,等待确认后继续下一步
|
||||
- 如有问题会自动修复并重新验证
|
||||
- 这里建议每个步骤结束后,人工确认是否执行了修复,如果进行了修复,请告诉他:请重新执行一遍该步骤,看看是否有遗漏。
|
||||
|
||||
感谢所有为项目做出贡献的开发者!
|
||||
详细说明:[docs/ai-reading/README.md](./docs/ai-reading/README.md) | 开发者规范:[docs/开发者代码检查规范.md](./docs/开发者代码检查规范.md)
|
||||
|
||||
### 🏆 核心团队
|
||||
- **[moyin](https://gitea.xinghangee.icu/moyin)** - 核心开发者
|
||||
- **[jianuo](https://gitea.xinghangee.icu/jianuo)** - 核心开发者
|
||||
- **[angjustinl](https://gitea.xinghangee.icu/ANGJustinl)** - 核心开发者
|
||||
## 🤝 参与贡献
|
||||
|
||||
查看完整贡献者名单:[CONTRIBUTORS.md](./CONTRIBUTORS.md)
|
||||
### 贡献流程
|
||||
1. Fork项目
|
||||
2. 创建分支:`git checkout -b feature/your-feature`
|
||||
3. 开发功能(遵循开发规范)
|
||||
4. 运行测试:`pnpm test`
|
||||
5. 提交代码:`git commit -m "feat: 添加新功能"`
|
||||
6. 创建Pull Request
|
||||
|
||||
### 🌟 如何贡献
|
||||
### 核心团队
|
||||
- [moyin](https://gitea.xinghangee.icu/moyin)
|
||||
- [jianuo](https://gitea.xinghangee.icu/jianuo)
|
||||
- [angjustinl](https://gitea.xinghangee.icu/ANGJustinl)
|
||||
|
||||
我们欢迎所有形式的贡献:
|
||||
完整贡献者:[docs/CONTRIBUTORS.md](./docs/CONTRIBUTORS.md)
|
||||
|
||||
1. **<EFBFBD> Bug修复** - 发现并修复问题
|
||||
2. **✨ 新功能** - 添加有价值的功能
|
||||
3. **<EFBFBD> 文档改馈进** - 完善项目文档
|
||||
4. **🧪 测试用例** - 提高代码覆盖率
|
||||
5. **💡 建议反馈** - 提出改进建议
|
||||
## 📝 版本历史
|
||||
|
||||
**贡献流程:**
|
||||
1. Fork项目 → 2. 创建分支 → 3. 开发功能 → 4. 提交PR
|
||||
- **v2.1.0** (2026-01) - WebSocket架构升级、地图房间管理
|
||||
- **v2.0.0** (2025-12) - 四层架构重构、Zulip集成、管理员后台
|
||||
- **v1.2.0** (2025-11) - 用户管理、安全防护、邮件服务
|
||||
- **v1.0.0** (2025-10) - 项目初始化、用户认证、双模式存储
|
||||
|
||||
---
|
||||
## 📞 联系方式
|
||||
|
||||
## 📞 联系我们
|
||||
|
||||
- **项目地址**: [Gitea Repository](https://gitea.xinghangee.icu/datawhale/whale-town-end)
|
||||
- **问题反馈**: [Issues](https://gitea.xinghangee.icu/datawhale/whale-town-end/issues)
|
||||
- **功能建议**: [Discussions](https://gitea.xinghangee.icu/datawhale/whale-town-end/discussions)
|
||||
- 项目地址:[Gitea Repository](https://gitea.xinghangee.icu/datawhale/whale-town-end)
|
||||
- 问题反馈:[Issues](https://gitea.xinghangee.icu/datawhale/whale-town-end/issues)
|
||||
- 功能建议:[Discussions](https://gitea.xinghangee.icu/datawhale/whale-town-end/discussions)
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 [MIT License](./LICENSE) 开源协议。
|
||||
[MIT License](./LICENSE)
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
**🐋 Whale Town - 让像素世界更精彩!**
|
||||
**🐋 Whale Town - 让像素世界更精彩 !**
|
||||
|
||||
Made with ❤️ by the Whale Town Team
|
||||
|
||||
|
||||
138
TESTING.md
138
TESTING.md
@@ -1,138 +0,0 @@
|
||||
# 测试指南
|
||||
|
||||
本项目支持**无数据库和无邮件服务器**的测试模式,让你可以快速验证所有功能!
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 环境配置
|
||||
|
||||
```bash
|
||||
# 复制环境配置文件
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
默认配置已经设置为测试模式,无需修改即可使用。
|
||||
|
||||
### 2. 启动服务
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 启动开发服务器
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 3. 运行测试
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
.\test-api.ps1
|
||||
```
|
||||
|
||||
**Linux/macOS:**
|
||||
```bash
|
||||
./test-api.sh
|
||||
```
|
||||
|
||||
**自定义参数:**
|
||||
```bash
|
||||
# Windows
|
||||
.\test-api.ps1 -BaseUrl "http://localhost:3000" -TestEmail "custom@example.com"
|
||||
|
||||
# Linux/macOS
|
||||
./test-api.sh "http://localhost:3000" "custom@example.com"
|
||||
```
|
||||
|
||||
## 🧪 测试功能
|
||||
|
||||
测试脚本会验证以下功能:
|
||||
|
||||
- ✅ **邮箱验证码发送** - 生成6位数验证码
|
||||
- ✅ **邮箱验证码验证** - 验证码校验和清理
|
||||
- ✅ **用户注册** - 完整的用户注册流程
|
||||
- ✅ **用户登录** - 用户名/邮箱/手机号登录
|
||||
|
||||
## 🔧 测试模式特性
|
||||
|
||||
- 🗄️ **Redis 文件存储** - 使用 `redis-data/redis.json` 存储验证码
|
||||
- 📧 **邮件测试模式** - 邮件内容输出到控制台,无需真实SMTP
|
||||
- 💾 **内存用户存储** - 无需数据库,用户数据存储在内存中
|
||||
- 🔄 **自动切换** - 根据配置自动选择存储模式
|
||||
|
||||
## 📊 单元测试
|
||||
|
||||
```bash
|
||||
# 运行所有单元测试
|
||||
npm test
|
||||
|
||||
# 监听模式
|
||||
npm run test:watch
|
||||
|
||||
# 生成覆盖率报告
|
||||
npm run test:cov
|
||||
```
|
||||
|
||||
## 🌐 生产环境配置
|
||||
|
||||
要切换到生产环境,编辑 `.env` 文件:
|
||||
|
||||
```bash
|
||||
# 启用数据库(取消注释并填入真实数据)
|
||||
DB_HOST=your_mysql_host
|
||||
DB_PORT=3306
|
||||
DB_USERNAME=your_db_username
|
||||
DB_PASSWORD=your_db_password
|
||||
DB_NAME=your_db_name
|
||||
|
||||
# 启用真实Redis(取消注释并设置)
|
||||
USE_FILE_REDIS=false
|
||||
REDIS_HOST=your_redis_host
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=your_redis_password
|
||||
|
||||
# 启用邮件服务(取消注释并填入真实数据)
|
||||
EMAIL_HOST=smtp.gmail.com
|
||||
EMAIL_PORT=587
|
||||
EMAIL_USER=your_email@gmail.com
|
||||
EMAIL_PASS=your_app_password
|
||||
EMAIL_FROM="Whale Town Game" <noreply@whaletown.com>
|
||||
|
||||
# 生产环境设置
|
||||
NODE_ENV=production
|
||||
LOG_LEVEL=info
|
||||
```
|
||||
|
||||
## 🔍 故障排除
|
||||
|
||||
### 服务启动失败
|
||||
- 检查端口3000是否被占用
|
||||
- 确认Node.js版本 >= 18.0.0
|
||||
- 运行 `npm install` 重新安装依赖
|
||||
|
||||
### 测试脚本执行失败
|
||||
- 确认服务器正在运行
|
||||
- 检查防火墙设置
|
||||
- 在Linux/macOS上确保脚本有执行权限:`chmod +x test-api.sh`
|
||||
|
||||
### Redis文件存储问题
|
||||
- 检查 `redis-data` 目录权限
|
||||
- 确认 `USE_FILE_REDIS=true` 设置正确
|
||||
|
||||
### 邮件测试模式问题
|
||||
- 确认邮件配置为注释状态
|
||||
- 检查服务器控制台日志输出
|
||||
|
||||
## 📝 测试数据
|
||||
|
||||
测试完成后,你可以查看:
|
||||
|
||||
- `redis-data/redis.json` - 验证码存储数据
|
||||
- 服务器控制台 - 邮件内容输出
|
||||
- 测试脚本输出 - API响应结果
|
||||
|
||||
## 🎯 下一步
|
||||
|
||||
- 查看 [API 文档](http://localhost:3000/api-docs) 了解更多接口
|
||||
- 阅读 [开发规范](./docs/backend_development_guide.md) 开始开发
|
||||
- 使用 [AI 辅助指南](./docs/AI辅助开发规范指南.md) 提高开发效率
|
||||
4
client/.env.example
Normal file
4
client/.env.example
Normal file
@@ -0,0 +1,4 @@
|
||||
# 前端后台配置
|
||||
# 复制为 .env.local
|
||||
|
||||
VITE_API_BASE_URL=http://localhost:3000
|
||||
222
client/README.md
Normal file
222
client/README.md
Normal file
@@ -0,0 +1,222 @@
|
||||
# 🎛️ Whale Town 管理员前端界面
|
||||
|
||||
基于 React + Vite + Ant Design 构建的现代化管理员后台界面。
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 📋 环境要求
|
||||
- Node.js >= 18.0.0
|
||||
- pnpm >= 8.0.0
|
||||
|
||||
### 🛠️ 安装与运行
|
||||
|
||||
```bash
|
||||
# 1. 确保后端服务已启动
|
||||
cd ..
|
||||
pnpm run dev
|
||||
|
||||
# 2. 安装前端依赖
|
||||
cd client
|
||||
pnpm install
|
||||
|
||||
# 3. 启动开发服务器
|
||||
pnpm run dev
|
||||
|
||||
# 4. 访问管理界面
|
||||
# 浏览器打开: http://localhost:5173
|
||||
```
|
||||
|
||||
### 🔑 默认登录信息
|
||||
- **用户名**: admin
|
||||
- **密码**: Admin123456
|
||||
|
||||
## 🎯 核心功能
|
||||
|
||||
### 🔐 管理员认证
|
||||
- 独立的Token认证系统
|
||||
- 安全的登录验证
|
||||
- 自动Token刷新
|
||||
|
||||
### 👥 用户管理
|
||||
- 用户列表查看和搜索
|
||||
- 用户状态管理
|
||||
- 用户密码重置
|
||||
- 分页和排序功能
|
||||
|
||||
### 📊 系统监控
|
||||
- 实时日志查看
|
||||
- 日志文件下载
|
||||
- 系统状态监控
|
||||
|
||||
### 🎨 界面特性
|
||||
- 响应式设计,支持移动端
|
||||
- 现代化UI组件
|
||||
- 暗色/亮色主题切换
|
||||
- 国际化支持
|
||||
|
||||
## 🏗️ 技术栈
|
||||
|
||||
### 🚀 核心框架
|
||||
- **React** `^18.0.0` - 前端UI框架
|
||||
- **Vite** `^5.0.0` - 现代化构建工具
|
||||
- **TypeScript** `^5.0.0` - 类型安全的JavaScript
|
||||
|
||||
### 🎨 UI组件
|
||||
- **Ant Design** `^5.0.0` - 企业级UI组件库
|
||||
- **Ant Design Icons** - 图标库
|
||||
- **CSS Modules** - 样式模块化
|
||||
|
||||
### 🔧 开发工具
|
||||
- **ESLint** - 代码质量检查
|
||||
- **Prettier** - 代码格式化
|
||||
- **Husky** - Git钩子管理
|
||||
|
||||
### 🌐 HTTP客户端
|
||||
- **Axios** - HTTP请求库
|
||||
- **React Query** - 数据获取和缓存
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
client/
|
||||
├── src/
|
||||
│ ├── components/ # 通用组件
|
||||
│ ├── pages/ # 页面组件
|
||||
│ ├── services/ # API服务
|
||||
│ ├── utils/ # 工具函数
|
||||
│ ├── types/ # TypeScript类型定义
|
||||
│ ├── styles/ # 全局样式
|
||||
│ ├── App.tsx # 应用主组件
|
||||
│ └── main.tsx # 应用入口
|
||||
├── public/ # 静态资源
|
||||
├── index.html # HTML模板
|
||||
├── vite.config.ts # Vite配置
|
||||
├── tsconfig.json # TypeScript配置
|
||||
└── package.json # 项目配置
|
||||
```
|
||||
|
||||
## 🔧 开发命令
|
||||
|
||||
```bash
|
||||
# 启动开发服务器
|
||||
pnpm run dev
|
||||
|
||||
# 构建生产版本
|
||||
pnpm run build
|
||||
|
||||
# 预览生产构建
|
||||
pnpm run preview
|
||||
|
||||
# 代码检查
|
||||
pnpm run lint
|
||||
|
||||
# 代码格式化
|
||||
pnpm run format
|
||||
|
||||
# 类型检查
|
||||
pnpm run type-check
|
||||
```
|
||||
|
||||
## 🌍 环境配置
|
||||
|
||||
### 开发环境 (.env.local)
|
||||
```bash
|
||||
VITE_API_BASE_URL=http://localhost:3000
|
||||
VITE_APP_TITLE=Whale Town 管理后台
|
||||
```
|
||||
|
||||
### 生产环境 (.env.production)
|
||||
```bash
|
||||
VITE_API_BASE_URL=https://your-api-domain.com
|
||||
VITE_APP_TITLE=Whale Town 管理后台
|
||||
```
|
||||
|
||||
## 🔗 API集成
|
||||
|
||||
### 认证接口
|
||||
- `POST /admin/auth/login` - 管理员登录
|
||||
- 自动Token管理和刷新
|
||||
|
||||
### 用户管理接口
|
||||
- `GET /admin/users` - 获取用户列表
|
||||
- `GET /admin/users/:id` - 获取用户详情
|
||||
- `POST /admin/users/:id/reset-password` - 重置用户密码
|
||||
- `PUT /admin/users/:id/status` - 修改用户状态
|
||||
|
||||
### 系统接口
|
||||
- `GET /admin/logs/runtime` - 获取运行日志
|
||||
- `GET /admin/logs/archive` - 下载日志归档
|
||||
|
||||
## 🎨 界面预览
|
||||
|
||||
### 登录页面
|
||||
- 简洁的登录表单
|
||||
- 输入验证和错误提示
|
||||
- 记住登录状态
|
||||
|
||||
### 用户管理页面
|
||||
- 用户列表表格
|
||||
- 搜索和筛选功能
|
||||
- 用户状态管理
|
||||
- 密码重置操作
|
||||
|
||||
### 日志管理页面
|
||||
- 实时日志显示
|
||||
- 日志级别筛选
|
||||
- 日志文件下载
|
||||
|
||||
## 🚀 部署指南
|
||||
|
||||
### 构建生产版本
|
||||
```bash
|
||||
# 构建
|
||||
pnpm run build
|
||||
|
||||
# 构建产物在 dist/ 目录
|
||||
```
|
||||
|
||||
### 部署到Nginx
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
location / {
|
||||
root /path/to/client/dist;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🤝 开发规范
|
||||
|
||||
### 代码风格
|
||||
- 使用TypeScript进行类型检查
|
||||
- 遵循ESLint和Prettier规范
|
||||
- 组件使用函数式组件和Hooks
|
||||
|
||||
### 文件命名
|
||||
- 组件文件使用PascalCase:`UserList.tsx`
|
||||
- 工具文件使用camelCase:`apiClient.ts`
|
||||
- 样式文件使用kebab-case:`user-list.module.css`
|
||||
|
||||
### 提交规范
|
||||
- 遵循项目Git提交规范
|
||||
- 提交前自动运行代码检查
|
||||
|
||||
## 📞 技术支持
|
||||
|
||||
如有问题,请查看:
|
||||
1. [后端API文档](../docs/api/README.md)
|
||||
2. [项目架构文档](../docs/ARCHITECTURE.md)
|
||||
3. [开发规范指南](../docs/development/)
|
||||
|
||||
---
|
||||
|
||||
**🎛️ 现代化管理界面,让后台管理更高效!**
|
||||
12
client/index.html
Normal file
12
client/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Whale Town Admin</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
24
client/package.json
Normal file
24
client/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "whale-town-admin",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"antd": "^5.27.3",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.30.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.24",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"@vitejs/plugin-react": "^5.0.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.1.3"
|
||||
}
|
||||
}
|
||||
61
client/src/app/AdminLayout.tsx
Normal file
61
client/src/app/AdminLayout.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Layout, Menu, Typography } from 'antd';
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { clearAuth } from '../lib/adminAuth';
|
||||
|
||||
const { Header, Content, Sider } = Layout;
|
||||
|
||||
export function AdminLayout() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const selectedKey = location.pathname.startsWith('/logs')
|
||||
? 'logs'
|
||||
: location.pathname.startsWith('/users')
|
||||
? 'users'
|
||||
: 'users';
|
||||
|
||||
return (
|
||||
<Layout style={{ minHeight: '100vh' }}>
|
||||
<Sider width={220}>
|
||||
<div style={{ padding: 16 }}>
|
||||
<Typography.Title level={5} style={{ color: 'white', margin: 0 }}>
|
||||
Whale Town Admin
|
||||
</Typography.Title>
|
||||
</div>
|
||||
<Menu
|
||||
theme="dark"
|
||||
mode="inline"
|
||||
selectedKeys={[selectedKey]}
|
||||
items={[
|
||||
{
|
||||
key: 'users',
|
||||
label: '用户管理',
|
||||
onClick: () => navigate('/users'),
|
||||
},
|
||||
{
|
||||
key: 'logs',
|
||||
label: '运行日志',
|
||||
onClick: () => navigate('/logs'),
|
||||
},
|
||||
{
|
||||
key: 'logout',
|
||||
label: '退出登录',
|
||||
onClick: () => {
|
||||
clearAuth();
|
||||
navigate('/login');
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Sider>
|
||||
<Layout>
|
||||
<Header style={{ background: 'white', display: 'flex', alignItems: 'center' }}>
|
||||
<Typography.Text>后台管理</Typography.Text>
|
||||
</Header>
|
||||
<Content style={{ padding: 16 }}>
|
||||
<Outlet />
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
28
client/src/app/App.tsx
Normal file
28
client/src/app/App.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { ConfigProvider } from 'antd';
|
||||
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
||||
import { AdminLayout } from './AdminLayout';
|
||||
import { LoginPage } from '../pages/LoginPage';
|
||||
import { UsersPage } from '../pages/UsersPage';
|
||||
import { LogsPage } from '../pages/LogsPage';
|
||||
import { isAuthed } from '../lib/adminAuth';
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<ConfigProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route
|
||||
path="/"
|
||||
element={isAuthed() ? <AdminLayout /> : <Navigate to="/login" replace />}
|
||||
>
|
||||
<Route index element={<Navigate to="/users" replace />} />
|
||||
<Route path="users" element={<UsersPage />} />
|
||||
<Route path="logs" element={<LogsPage />} />
|
||||
</Route>
|
||||
<Route path="*" element={<Navigate to={isAuthed() ? '/users' : '/login'} replace />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
17
client/src/lib/adminAuth.ts
Normal file
17
client/src/lib/adminAuth.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
const TOKEN_KEY = 'whale_town_admin_token';
|
||||
|
||||
export function getToken(): string | null {
|
||||
return localStorage.getItem(TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function setToken(token: string): void {
|
||||
localStorage.setItem(TOKEN_KEY, token);
|
||||
}
|
||||
|
||||
export function clearAuth(): void {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function isAuthed(): boolean {
|
||||
return Boolean(getToken());
|
||||
}
|
||||
130
client/src/lib/api.ts
Normal file
130
client/src/lib/api.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { getToken, clearAuth } from './adminAuth';
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000';
|
||||
|
||||
export class ApiError extends Error {
|
||||
status: number;
|
||||
|
||||
constructor(message: string, status: number) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
function parseFilenameFromContentDisposition(contentDisposition: string | null): string | null {
|
||||
if (!contentDisposition) return null;
|
||||
|
||||
// Prefer RFC 5987 filename*=UTF-8''...
|
||||
const filenameStarMatch = contentDisposition.match(/filename\*=(?:UTF-8''|utf-8'')([^;]+)/);
|
||||
if (filenameStarMatch?.[1]) {
|
||||
try {
|
||||
return decodeURIComponent(filenameStarMatch[1].trim().replace(/^"|"$/g, ''));
|
||||
} catch {
|
||||
return filenameStarMatch[1].trim().replace(/^"|"$/g, '');
|
||||
}
|
||||
}
|
||||
|
||||
const filenameMatch = contentDisposition.match(/filename=([^;]+)/);
|
||||
if (filenameMatch?.[1]) {
|
||||
return filenameMatch[1].trim().replace(/^"|"$/g, '');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const token = getToken();
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}${path}`, {
|
||||
...init,
|
||||
headers: {
|
||||
...headers,
|
||||
...(init?.headers || {}),
|
||||
},
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (res.status === 401) {
|
||||
clearAuth();
|
||||
}
|
||||
|
||||
const data = (await res.json().catch(() => ({}))) as any;
|
||||
|
||||
if (!res.ok) {
|
||||
throw new ApiError(data?.message || `请求失败: ${res.status}`, res.status);
|
||||
}
|
||||
|
||||
return data as T;
|
||||
}
|
||||
|
||||
async function requestDownload(path: string, init?: RequestInit): Promise<{ blob: Blob; filename: string }>
|
||||
{
|
||||
const token = getToken();
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
...(init?.headers as any),
|
||||
};
|
||||
|
||||
// Do NOT force Content-Type for downloads (GET binary)
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}${path}`, {
|
||||
...init,
|
||||
headers,
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (res.status === 401) {
|
||||
clearAuth();
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '');
|
||||
// Try to extract message from JSON-ish body
|
||||
let message = `请求失败: ${res.status}`;
|
||||
try {
|
||||
const maybeJson = JSON.parse(text || '{}');
|
||||
message = maybeJson?.message || message;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
throw new ApiError(message, res.status);
|
||||
}
|
||||
|
||||
const filename =
|
||||
parseFilenameFromContentDisposition(res.headers.get('content-disposition')) || 'logs.tar.gz';
|
||||
const blob = await res.blob();
|
||||
return { blob, filename };
|
||||
}
|
||||
|
||||
export const api = {
|
||||
adminLogin: (identifier: string, password: string) =>
|
||||
request<any>('/admin/auth/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ identifier, password }),
|
||||
}),
|
||||
|
||||
listUsers: (limit = 100, offset = 0) =>
|
||||
request<any>(`/admin/users?limit=${encodeURIComponent(limit)}&offset=${encodeURIComponent(offset)}`),
|
||||
|
||||
resetUserPassword: (userId: string, newPassword: string) =>
|
||||
request<any>(`/admin/users/${encodeURIComponent(userId)}/reset-password`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ new_password: newPassword }),
|
||||
}),
|
||||
|
||||
getRuntimeLogs: (lines = 200) =>
|
||||
request<any>(`/admin/logs/runtime?lines=${encodeURIComponent(lines)}`),
|
||||
|
||||
downloadLogsArchive: () => requestDownload('/admin/logs/archive'),
|
||||
};
|
||||
9
client/src/main.tsx
Normal file
9
client/src/main.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { App } from './app/App';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
50
client/src/pages/LoginPage.tsx
Normal file
50
client/src/pages/LoginPage.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Button, Card, Form, Input, Typography, message } from 'antd';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { api } from '../lib/api';
|
||||
import { setToken } from '../lib/adminAuth';
|
||||
|
||||
type LoginValues = {
|
||||
identifier: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export function LoginPage() {
|
||||
const navigate = useNavigate();
|
||||
const [form] = Form.useForm<LoginValues>();
|
||||
|
||||
const onFinish = async (values: LoginValues) => {
|
||||
try {
|
||||
const res = await api.adminLogin(values.identifier, values.password);
|
||||
if (!res?.success || !res?.data?.access_token) {
|
||||
throw new Error(res?.message || '登录失败');
|
||||
}
|
||||
|
||||
setToken(res.data.access_token);
|
||||
message.success('登录成功');
|
||||
navigate('/users');
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '登录失败');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<Card style={{ width: 420 }}>
|
||||
<Typography.Title level={4} style={{ marginTop: 0 }}>
|
||||
管理员登录
|
||||
</Typography.Title>
|
||||
<Form form={form} layout="vertical" onFinish={onFinish}>
|
||||
<Form.Item name="identifier" label="用户名/邮箱/手机号" rules={[{ required: true }]}>
|
||||
<Input placeholder="admin" autoComplete="username" />
|
||||
</Form.Item>
|
||||
<Form.Item name="password" label="密码" rules={[{ required: true }]}>
|
||||
<Input.Password placeholder="请输入密码" autoComplete="current-password" />
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit" block>
|
||||
登录
|
||||
</Button>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
106
client/src/pages/LogsPage.tsx
Normal file
106
client/src/pages/LogsPage.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Alert, Button, Card, InputNumber, Space, Typography } from 'antd';
|
||||
import { api, ApiError } from '../lib/api';
|
||||
|
||||
export function LogsPage() {
|
||||
const [lines, setLines] = useState<number>(200);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [downloadLoading, setDownloadLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [file, setFile] = useState<string>('');
|
||||
const [updatedAt, setUpdatedAt] = useState<string>('');
|
||||
const [logLines, setLogLines] = useState<string[]>([]);
|
||||
|
||||
const logText = useMemo(() => logLines.join('\n'), [logLines]);
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await api.getRuntimeLogs(lines);
|
||||
if (!res?.success) {
|
||||
setError(res?.message || '运行日志获取失败');
|
||||
return;
|
||||
}
|
||||
setFile(res?.data?.file || '');
|
||||
setUpdatedAt(res?.data?.updated_at || '');
|
||||
setLogLines(Array.isArray(res?.data?.lines) ? res.data.lines : []);
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError) {
|
||||
setError(e.message);
|
||||
} else {
|
||||
setError(e instanceof Error ? e.message : '运行日志获取失败');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const downloadArchive = async () => {
|
||||
setDownloadLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const { blob, filename } = await api.downloadLogsArchive();
|
||||
const url = URL.createObjectURL(blob);
|
||||
try {
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename || 'logs.tar.gz';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
} finally {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError) {
|
||||
setError(e.message);
|
||||
} else {
|
||||
setError(e instanceof Error ? e.message : '日志下载失败');
|
||||
}
|
||||
} finally {
|
||||
setDownloadLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
||||
{error ? <Alert type="error" message={error} /> : null}
|
||||
|
||||
<Card
|
||||
title="运行日志"
|
||||
extra={
|
||||
<Space>
|
||||
<span>行数</span>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={2000}
|
||||
value={lines}
|
||||
onChange={(v) => setLines(typeof v === 'number' ? v : 200)}
|
||||
/>
|
||||
<Button onClick={() => void load()} loading={loading}>
|
||||
刷新
|
||||
</Button>
|
||||
<Button onClick={() => void downloadArchive()} loading={downloadLoading}>
|
||||
下载日志压缩包
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="small">
|
||||
<Typography.Text type="secondary">
|
||||
{file ? `文件:${file}` : '文件:-'}
|
||||
{updatedAt ? ` 更新时间:${updatedAt}` : ''}
|
||||
</Typography.Text>
|
||||
|
||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>{logText || '暂无日志'}</pre>
|
||||
</Space>
|
||||
</Card>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
161
client/src/pages/UsersPage.tsx
Normal file
161
client/src/pages/UsersPage.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import { Button, Card, Form, Input, Modal, Space, Table, Typography, message } from 'antd';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { api } from '../lib/api';
|
||||
|
||||
type UserRow = {
|
||||
id: string;
|
||||
username: string;
|
||||
nickname: string;
|
||||
email?: string;
|
||||
email_verified: boolean;
|
||||
phone?: string;
|
||||
role: number;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
type ResetValues = {
|
||||
newPassword: string;
|
||||
};
|
||||
|
||||
export function UsersPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [rows, setRows] = useState<UserRow[]>([]);
|
||||
const [resetOpen, setResetOpen] = useState(false);
|
||||
const [resetUserId, setResetUserId] = useState<string | null>(null);
|
||||
const [resetForm] = Form.useForm<ResetValues>();
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{ title: 'ID', dataIndex: 'id', key: 'id', width: 90 },
|
||||
{ title: '用户名', dataIndex: 'username', key: 'username' },
|
||||
{ title: '昵称', dataIndex: 'nickname', key: 'nickname' },
|
||||
{ title: '邮箱', dataIndex: 'email', key: 'email' },
|
||||
{
|
||||
title: '邮箱验证',
|
||||
dataIndex: 'email_verified',
|
||||
key: 'email_verified',
|
||||
render: (v: boolean) => (v ? '已验证' : '未验证'),
|
||||
width: 100,
|
||||
},
|
||||
{ title: '手机号', dataIndex: 'phone', key: 'phone' },
|
||||
{ title: '角色', dataIndex: 'role', key: 'role', width: 80 },
|
||||
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 180 },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 160,
|
||||
render: (_: any, row: UserRow) => (
|
||||
<Space>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setResetUserId(row.id);
|
||||
resetForm.resetFields();
|
||||
setResetOpen(true);
|
||||
}}
|
||||
>
|
||||
重置密码
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
],
|
||||
[resetForm],
|
||||
);
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await api.listUsers(200, 0);
|
||||
const users = res?.data?.users || [];
|
||||
setRows(
|
||||
users.map((u: any) => ({
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
nickname: u.nickname,
|
||||
email: u.email || undefined,
|
||||
email_verified: Boolean(u.email_verified),
|
||||
phone: u.phone || undefined,
|
||||
role: u.role,
|
||||
created_at: u.created_at,
|
||||
})),
|
||||
);
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '加载失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, []);
|
||||
|
||||
const onResetOk = async () => {
|
||||
try {
|
||||
const values = await resetForm.validateFields();
|
||||
if (!resetUserId) return;
|
||||
|
||||
await api.resetUserPassword(resetUserId, values.newPassword);
|
||||
message.success('密码已重置');
|
||||
setResetOpen(false);
|
||||
} catch (e: any) {
|
||||
if (e?.errorFields) return;
|
||||
message.error(e?.message || '重置失败');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={12}>
|
||||
<Space style={{ justifyContent: 'space-between', width: '100%' }}>
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>
|
||||
用户管理
|
||||
</Typography.Title>
|
||||
<Button onClick={load} loading={loading}>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
columns={columns as any}
|
||||
dataSource={rows}
|
||||
pagination={false}
|
||||
/>
|
||||
</Space>
|
||||
|
||||
<Modal
|
||||
title={`重置密码${resetUserId ? `(用户ID: ${resetUserId})` : ''}`}
|
||||
open={resetOpen}
|
||||
onOk={onResetOk}
|
||||
onCancel={() => setResetOpen(false)}
|
||||
okText="确认"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Form form={resetForm} layout="vertical">
|
||||
<Form.Item
|
||||
label="新密码"
|
||||
name="newPassword"
|
||||
rules={[
|
||||
{ required: true, message: '请输入新密码' },
|
||||
{ min: 8, message: '至少8位' },
|
||||
{
|
||||
validator: (_, v) => {
|
||||
const hasLetter = /[a-zA-Z]/.test(v || '');
|
||||
const hasNumber = /\d/.test(v || '');
|
||||
if (!v) return Promise.resolve();
|
||||
if (!hasLetter || !hasNumber) return Promise.reject(new Error('必须包含字母和数字'));
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.Password placeholder="例如 NewPass1234" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
19
client/tsconfig.json
Normal file
19
client/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
"moduleResolution": "Bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
"strict": true,
|
||||
"types": ["vite/client"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
9
client/vite.config.ts
Normal file
9
client/vite.config.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 5173,
|
||||
},
|
||||
});
|
||||
@@ -1,149 +0,0 @@
|
||||
# Zulip配置目录
|
||||
|
||||
本目录包含Zulip集成系统的配置文件。
|
||||
|
||||
## 文件说明
|
||||
|
||||
### map-config.json
|
||||
|
||||
地图映射配置文件,定义游戏地图到Zulip Stream/Topic的映射关系。
|
||||
|
||||
#### 配置结构
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"lastModified": "2025-12-25T00:00:00.000Z",
|
||||
"description": "配置描述",
|
||||
"maps": [
|
||||
{
|
||||
"mapId": "地图唯一标识",
|
||||
"mapName": "地图显示名称",
|
||||
"zulipStream": "对应的Zulip Stream名称",
|
||||
"interactionObjects": [
|
||||
{
|
||||
"objectId": "交互对象唯一标识",
|
||||
"objectName": "交互对象显示名称",
|
||||
"zulipTopic": "对应的Zulip Topic名称",
|
||||
"position": { "x": 100, "y": 150 }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 字段说明
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| version | string | 否 | 配置版本号 |
|
||||
| lastModified | string | 否 | 最后修改时间(ISO 8601格式) |
|
||||
| description | string | 否 | 配置描述 |
|
||||
| maps | array | 是 | 地图配置数组 |
|
||||
|
||||
##### 地图配置 (MapConfig)
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| mapId | string | 是 | 地图唯一标识,如 "novice_village" |
|
||||
| mapName | string | 是 | 地图显示名称,如 "新手村" |
|
||||
| zulipStream | string | 是 | 对应的Zulip Stream名称 |
|
||||
| interactionObjects | array | 是 | 交互对象配置数组 |
|
||||
|
||||
##### 交互对象配置 (InteractionObject)
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| objectId | string | 是 | 交互对象唯一标识 |
|
||||
| objectName | string | 是 | 交互对象显示名称 |
|
||||
| zulipTopic | string | 是 | 对应的Zulip Topic名称 |
|
||||
| position | object | 是 | 对象在地图中的位置 |
|
||||
| position.x | number | 是 | X坐标 |
|
||||
| position.y | number | 是 | Y坐标 |
|
||||
|
||||
## 配置示例
|
||||
|
||||
### 新手村配置
|
||||
|
||||
```json
|
||||
{
|
||||
"mapId": "novice_village",
|
||||
"mapName": "新手村",
|
||||
"zulipStream": "Novice Village",
|
||||
"interactionObjects": [
|
||||
{
|
||||
"objectId": "notice_board",
|
||||
"objectName": "公告板",
|
||||
"zulipTopic": "Notice Board",
|
||||
"position": { "x": 100, "y": 150 }
|
||||
},
|
||||
{
|
||||
"objectId": "village_well",
|
||||
"objectName": "村井",
|
||||
"zulipTopic": "Village Well",
|
||||
"position": { "x": 200, "y": 200 }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 酒馆配置
|
||||
|
||||
```json
|
||||
{
|
||||
"mapId": "tavern",
|
||||
"mapName": "酒馆",
|
||||
"zulipStream": "Tavern",
|
||||
"interactionObjects": [
|
||||
{
|
||||
"objectId": "bar_counter",
|
||||
"objectName": "吧台",
|
||||
"zulipTopic": "Bar Counter",
|
||||
"position": { "x": 150, "y": 100 }
|
||||
},
|
||||
{
|
||||
"objectId": "fireplace",
|
||||
"objectName": "壁炉",
|
||||
"zulipTopic": "Fireplace Chat",
|
||||
"position": { "x": 300, "y": 200 }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 热重载
|
||||
|
||||
配置文件支持热重载,修改后无需重启服务即可生效。
|
||||
|
||||
### 启用配置监听
|
||||
|
||||
在代码中调用:
|
||||
|
||||
```typescript
|
||||
configManagerService.enableConfigWatcher();
|
||||
```
|
||||
|
||||
### 手动重载配置
|
||||
|
||||
```typescript
|
||||
await configManagerService.reloadConfig();
|
||||
```
|
||||
|
||||
## 验证配置
|
||||
|
||||
系统启动时会自动验证配置文件的有效性。验证规则包括:
|
||||
|
||||
1. mapId必须是非空字符串
|
||||
2. mapName必须是非空字符串
|
||||
3. zulipStream必须是非空字符串
|
||||
4. interactionObjects必须是数组
|
||||
5. 每个交互对象必须有有效的objectId、objectName、zulipTopic和position
|
||||
6. position.x和position.y必须是有效数字
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **Stream名称**: Zulip Stream名称区分大小写,请确保与Zulip服务器上的Stream名称完全匹配
|
||||
2. **Topic名称**: Topic名称同样区分大小写
|
||||
3. **位置坐标**: 位置坐标用于空间过滤,确保与游戏客户端的坐标系统一致
|
||||
4. **唯一性**: mapId和objectId在各自范围内必须唯一
|
||||
@@ -1,205 +0,0 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"lastModified": "2025-12-25T20:00:00.000Z",
|
||||
"description": "基于设计图的 Zulip 映射配置",
|
||||
"maps": [
|
||||
{
|
||||
"mapId": "whale_port",
|
||||
"mapName": "鲸之港",
|
||||
"zulipStream": "Whale Port",
|
||||
"description": "中心城区,交通枢纽与主要聚会点",
|
||||
"interactionObjects": [
|
||||
{
|
||||
"objectId": "whale_statue",
|
||||
"objectName": "鲸鱼雕像",
|
||||
"zulipTopic": "Announcements",
|
||||
"position": { "x": 600, "y": 400 }
|
||||
},
|
||||
{
|
||||
"objectId": "clock_tower",
|
||||
"objectName": "大本钟",
|
||||
"zulipTopic": "General Chat",
|
||||
"position": { "x": 550, "y": 350 }
|
||||
},
|
||||
{
|
||||
"objectId": "city_metro",
|
||||
"objectName": "地铁入口",
|
||||
"zulipTopic": "Transportation",
|
||||
"position": { "x": 600, "y": 550 }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mapId": "offer_city",
|
||||
"mapName": "Offer 城",
|
||||
"zulipStream": "Offer City",
|
||||
"description": "职业发展、面试与商务区",
|
||||
"interactionObjects": [
|
||||
{
|
||||
"objectId": "skyscrapers",
|
||||
"objectName": "摩天大楼",
|
||||
"zulipTopic": "Career Talk",
|
||||
"position": { "x": 350, "y": 650 }
|
||||
},
|
||||
{
|
||||
"objectId": "business_center",
|
||||
"objectName": "商务中心",
|
||||
"zulipTopic": "Interview Prep",
|
||||
"position": { "x": 300, "y": 700 }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mapId": "model_factory",
|
||||
"mapName": "模型工厂",
|
||||
"zulipStream": "Model Factory",
|
||||
"description": "AI模型训练、代码构建与工业区",
|
||||
"interactionObjects": [
|
||||
{
|
||||
"objectId": "assembly_line",
|
||||
"objectName": "流水线",
|
||||
"zulipTopic": "Code Review",
|
||||
"position": { "x": 400, "y": 200 }
|
||||
},
|
||||
{
|
||||
"objectId": "gear_tower",
|
||||
"objectName": "齿轮塔",
|
||||
"zulipTopic": "DevOps & CI/CD",
|
||||
"position": { "x": 450, "y": 180 }
|
||||
},
|
||||
{
|
||||
"objectId": "cable_car_station",
|
||||
"objectName": "缆车站",
|
||||
"zulipTopic": "Deployments",
|
||||
"position": { "x": 350, "y": 220 }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mapId": "kernel_island",
|
||||
"mapName": "内核岛",
|
||||
"zulipStream": "Kernel Island",
|
||||
"description": "核心技术研究、底层原理与算法",
|
||||
"interactionObjects": [
|
||||
{
|
||||
"objectId": "crystal_core",
|
||||
"objectName": "能量水晶",
|
||||
"zulipTopic": "Core Algorithms",
|
||||
"position": { "x": 600, "y": 150 }
|
||||
},
|
||||
{
|
||||
"objectId": "floating_rocks",
|
||||
"objectName": "浮空石",
|
||||
"zulipTopic": "System Architecture",
|
||||
"position": { "x": 650, "y": 180 }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mapId": "pumpkin_valley",
|
||||
"mapName": "南瓜谷",
|
||||
"zulipStream": "Pumpkin Valley",
|
||||
"description": "新手成长、基础资源与学习社区",
|
||||
"interactionObjects": [
|
||||
{
|
||||
"objectId": "pumpkin_patch",
|
||||
"objectName": "南瓜田",
|
||||
"zulipTopic": "Tutorials",
|
||||
"position": { "x": 150, "y": 400 }
|
||||
},
|
||||
{
|
||||
"objectId": "farm_house",
|
||||
"objectName": "农舍",
|
||||
"zulipTopic": "Study Group",
|
||||
"position": { "x": 200, "y": 450 }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mapId": "moyu_beach",
|
||||
"mapName": "摸鱼海滩",
|
||||
"zulipStream": "Moyu Beach",
|
||||
"description": "休闲娱乐、水贴与非技术话题",
|
||||
"interactionObjects": [
|
||||
{
|
||||
"objectId": "beach_umbrella",
|
||||
"objectName": "遮阳伞",
|
||||
"zulipTopic": "Random Chat",
|
||||
"position": { "x": 850, "y": 200 }
|
||||
},
|
||||
{
|
||||
"objectId": "lighthouse",
|
||||
"objectName": "灯塔",
|
||||
"zulipTopic": "Music & Movies",
|
||||
"position": { "x": 800, "y": 100 }
|
||||
},
|
||||
{
|
||||
"objectId": "fishing_dock",
|
||||
"objectName": "栈桥",
|
||||
"zulipTopic": "Gaming",
|
||||
"position": { "x": 750, "y": 250 }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mapId": "ladder_peak",
|
||||
"mapName": "天梯峰",
|
||||
"zulipStream": "Ladder Peak",
|
||||
"description": "挑战、竞赛与排行榜",
|
||||
"interactionObjects": [
|
||||
{
|
||||
"objectId": "summit_flag",
|
||||
"objectName": "峰顶旗帜",
|
||||
"zulipTopic": "Leaderboard",
|
||||
"position": { "x": 150, "y": 100 }
|
||||
},
|
||||
{
|
||||
"objectId": "snowy_path",
|
||||
"objectName": "雪径",
|
||||
"zulipTopic": "Challenges",
|
||||
"position": { "x": 200, "y": 150 }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mapId": "galaxy_bay",
|
||||
"mapName": "星河湾",
|
||||
"zulipStream": "Galaxy Bay",
|
||||
"description": "创意、设计与灵感",
|
||||
"interactionObjects": [
|
||||
{
|
||||
"objectId": "starfish",
|
||||
"objectName": "巨型海星",
|
||||
"zulipTopic": "UI/UX Design",
|
||||
"position": { "x": 100, "y": 700 }
|
||||
},
|
||||
{
|
||||
"objectId": "palm_tree",
|
||||
"objectName": "椰子树",
|
||||
"zulipTopic": "Art & Assets",
|
||||
"position": { "x": 150, "y": 650 }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mapId": "data_ruins",
|
||||
"mapName": "数据遗迹",
|
||||
"zulipStream": "Data Ruins",
|
||||
"description": "数据库、归档与历史记录",
|
||||
"interactionObjects": [
|
||||
{
|
||||
"objectId": "ruined_gate",
|
||||
"objectName": "遗迹之门",
|
||||
"zulipTopic": "Database Schema",
|
||||
"position": { "x": 900, "y": 700 }
|
||||
},
|
||||
{
|
||||
"objectId": "ancient_monolith",
|
||||
"objectName": "石碑",
|
||||
"zulipTopic": "Archives",
|
||||
"position": { "x": 950, "y": 650 }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 部署脚本模板 - 用于 Gitea Webhook 自动部署
|
||||
# 复制此文件为 deploy.sh 并根据服务器环境修改配置
|
||||
set -e
|
||||
|
||||
echo "开始部署 Pixel Game Server..."
|
||||
|
||||
# 项目路径(根据你的服务器实际路径修改)
|
||||
PROJECT_PATH="/var/www/pixel-game-server"
|
||||
BACKUP_PATH="/var/backups/pixel-game-server"
|
||||
|
||||
# 创建备份
|
||||
echo "创建备份..."
|
||||
mkdir -p $BACKUP_PATH
|
||||
cp -r $PROJECT_PATH $BACKUP_PATH/backup-$(date +%Y%m%d-%H%M%S)
|
||||
|
||||
# 进入项目目录
|
||||
cd $PROJECT_PATH
|
||||
|
||||
# 拉取最新代码
|
||||
echo "拉取最新代码..."
|
||||
git pull origin main
|
||||
|
||||
# 安装/更新依赖
|
||||
echo "安装依赖..."
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
# 构建项目
|
||||
echo "构建项目..."
|
||||
pnpm run build
|
||||
|
||||
# 重启服务
|
||||
echo "重启服务..."
|
||||
if command -v pm2 &> /dev/null; then
|
||||
# 使用 PM2
|
||||
pm2 restart pixel-game-server || pm2 start dist/main.js --name pixel-game-server
|
||||
elif command -v docker-compose &> /dev/null; then
|
||||
# 使用 Docker Compose
|
||||
docker-compose down
|
||||
docker-compose up -d --build
|
||||
else
|
||||
# 使用 systemd
|
||||
sudo systemctl restart pixel-game-server
|
||||
fi
|
||||
|
||||
echo "部署完成!"
|
||||
|
||||
# 清理旧备份(保留最近5个)
|
||||
find $BACKUP_PATH -maxdepth 1 -type d -name "backup-*" | sort -r | tail -n +6 | xargs rm -rf
|
||||
|
||||
echo "服务状态检查..."
|
||||
sleep 5
|
||||
curl -f http://localhost:3000/health || echo "警告:服务健康检查失败"
|
||||
@@ -1,36 +0,0 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DB_HOST=mysql
|
||||
- DB_PORT=3306
|
||||
- DB_USERNAME=pixel_game
|
||||
- DB_PASSWORD=your_password
|
||||
- DB_NAME=pixel_game_db
|
||||
depends_on:
|
||||
- mysql
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=root_password
|
||||
- MYSQL_DATABASE=pixel_game_db
|
||||
- MYSQL_USER=pixel_game
|
||||
- MYSQL_PASSWORD=your_password
|
||||
ports:
|
||||
- "3306:3306"
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
@@ -1,257 +0,0 @@
|
||||
# API 状态码说明
|
||||
|
||||
## 📊 概述
|
||||
|
||||
本文档说明了项目中使用的 HTTP 状态码,特别是针对邮件发送功能的特殊状态码处理。
|
||||
|
||||
## 🔢 标准状态码
|
||||
|
||||
| 状态码 | 含义 | 使用场景 |
|
||||
|--------|------|----------|
|
||||
| 200 | OK | 请求成功 |
|
||||
| 201 | Created | 资源创建成功(如用户注册) |
|
||||
| 400 | Bad Request | 请求参数错误 |
|
||||
| 401 | Unauthorized | 未授权(如密码错误) |
|
||||
| 403 | Forbidden | 权限不足 |
|
||||
| 404 | Not Found | 资源不存在 |
|
||||
| 409 | Conflict | 资源冲突(如用户名已存在) |
|
||||
| 429 | Too Many Requests | 请求频率过高 |
|
||||
| 500 | Internal Server Error | 服务器内部错误 |
|
||||
|
||||
## 🎯 特殊状态码
|
||||
|
||||
### 206 Partial Content - 测试模式
|
||||
|
||||
**使用场景:** 邮件发送功能在测试模式下使用
|
||||
|
||||
**含义:** 请求部分成功,但未完全达到预期效果
|
||||
|
||||
**具体应用:**
|
||||
- 验证码已生成,但邮件未真实发送
|
||||
- 功能正常工作,但处于测试/开发模式
|
||||
- 用户可以获得验证码进行测试,但需要知道这不是真实发送
|
||||
|
||||
**响应示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"data": {
|
||||
"verification_code": "123456",
|
||||
"is_test_mode": true
|
||||
},
|
||||
"message": "⚠️ 测试模式:验证码已生成但未真实发送。请在控制台查看验证码,或配置邮件服务以启用真实发送。",
|
||||
"error_code": "TEST_MODE_ONLY"
|
||||
}
|
||||
```
|
||||
|
||||
## 📧 邮件发送接口状态码
|
||||
|
||||
### 发送邮箱验证码 - POST /auth/send-email-verification
|
||||
|
||||
| 状态码 | 场景 | 响应 |
|
||||
|--------|------|------|
|
||||
| 200 | 邮件真实发送成功 | `{ "success": true, "message": "验证码已发送,请查收邮件" }` |
|
||||
| 206 | 测试模式 | `{ "success": false, "error_code": "TEST_MODE_ONLY", "data": { "verification_code": "123456", "is_test_mode": true } }` |
|
||||
| 400 | 参数错误 | `{ "success": false, "message": "邮箱格式错误" }` |
|
||||
| 429 | 发送频率过高 | `{ "success": false, "message": "发送频率过高,请稍后重试" }` |
|
||||
|
||||
### 发送密码重置验证码 - POST /auth/forgot-password
|
||||
|
||||
| 状态码 | 场景 | 响应 |
|
||||
|--------|------|------|
|
||||
| 200 | 邮件真实发送成功 | `{ "success": true, "message": "验证码已发送,请查收" }` |
|
||||
| 206 | 测试模式 | `{ "success": false, "error_code": "TEST_MODE_ONLY", "data": { "verification_code": "123456", "is_test_mode": true } }` |
|
||||
| 400 | 参数错误 | `{ "success": false, "message": "邮箱格式错误" }` |
|
||||
| 404 | 用户不存在 | `{ "success": false, "message": "用户不存在" }` |
|
||||
|
||||
### 重新发送邮箱验证码 - POST /auth/resend-email-verification
|
||||
|
||||
| 状态码 | 场景 | 响应 |
|
||||
|--------|------|------|
|
||||
| 200 | 邮件真实发送成功 | `{ "success": true, "message": "验证码已重新发送,请查收邮件" }` |
|
||||
| 206 | 测试模式 | `{ "success": false, "error_code": "TEST_MODE_ONLY", "data": { "verification_code": "123456", "is_test_mode": true } }` |
|
||||
| 400 | 邮箱已验证或用户不存在 | `{ "success": false, "message": "邮箱已验证,无需重复验证" }` |
|
||||
| 429 | 发送频率过高 | `{ "success": false, "message": "发送频率过高,请稍后重试" }` |
|
||||
|
||||
## 🔄 模式切换
|
||||
|
||||
### 测试模式 → 真实发送模式
|
||||
|
||||
**配置前(测试模式):**
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/auth/send-email-verification \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email": "test@example.com"}'
|
||||
|
||||
# 响应:206 Partial Content
|
||||
{
|
||||
"success": false,
|
||||
"data": {
|
||||
"verification_code": "123456",
|
||||
"is_test_mode": true
|
||||
},
|
||||
"message": "⚠️ 测试模式:验证码已生成但未真实发送...",
|
||||
"error_code": "TEST_MODE_ONLY"
|
||||
}
|
||||
```
|
||||
|
||||
**配置后(真实发送模式):**
|
||||
```bash
|
||||
# 同样的请求
|
||||
curl -X POST http://localhost:3000/auth/send-email-verification \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email": "test@example.com"}'
|
||||
|
||||
# 响应:200 OK
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"is_test_mode": false
|
||||
},
|
||||
"message": "验证码已发送,请查收邮件"
|
||||
}
|
||||
```
|
||||
|
||||
## 💡 前端处理建议
|
||||
|
||||
### JavaScript 示例
|
||||
|
||||
```javascript
|
||||
async function sendEmailVerification(email) {
|
||||
try {
|
||||
const response = await fetch('/auth/send-email-verification', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.status === 200) {
|
||||
// 真实发送成功
|
||||
showSuccess('验证码已发送,请查收邮件');
|
||||
} else if (response.status === 206) {
|
||||
// 测试模式
|
||||
showWarning(`测试模式:验证码是 ${data.data.verification_code}`);
|
||||
showInfo('请配置邮件服务以启用真实发送');
|
||||
} else {
|
||||
// 其他错误
|
||||
showError(data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
showError('网络错误,请稍后重试');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### React 示例
|
||||
|
||||
```jsx
|
||||
const handleSendVerification = async (email) => {
|
||||
try {
|
||||
const response = await fetch('/auth/send-email-verification', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
switch (response.status) {
|
||||
case 200:
|
||||
setMessage({ type: 'success', text: '验证码已发送,请查收邮件' });
|
||||
break;
|
||||
case 206:
|
||||
setMessage({
|
||||
type: 'warning',
|
||||
text: `测试模式:验证码是 ${data.data.verification_code}`
|
||||
});
|
||||
setShowConfigTip(true);
|
||||
break;
|
||||
case 400:
|
||||
setMessage({ type: 'error', text: data.message });
|
||||
break;
|
||||
case 429:
|
||||
setMessage({ type: 'error', text: '发送频率过高,请稍后重试' });
|
||||
break;
|
||||
default:
|
||||
setMessage({ type: 'error', text: '发送失败,请稍后重试' });
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({ type: 'error', text: '网络错误,请稍后重试' });
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## 🎨 UI 展示建议
|
||||
|
||||
### 测试模式提示
|
||||
|
||||
```html
|
||||
<!-- 成功状态 (200) -->
|
||||
<div class="alert alert-success">
|
||||
✅ 验证码已发送,请查收邮件
|
||||
</div>
|
||||
|
||||
<!-- 测试模式 (206) -->
|
||||
<div class="alert alert-warning">
|
||||
⚠️ 测试模式:验证码是 123456
|
||||
<br>
|
||||
<small>请配置邮件服务以启用真实发送</small>
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 (400+) -->
|
||||
<div class="alert alert-danger">
|
||||
❌ 发送失败:邮箱格式错误
|
||||
</div>
|
||||
```
|
||||
|
||||
## 📝 开发建议
|
||||
|
||||
### 1. 状态码检查
|
||||
|
||||
```javascript
|
||||
// 推荐:明确检查状态码
|
||||
if (response.status === 206) {
|
||||
// 处理测试模式
|
||||
} else if (response.status === 200) {
|
||||
// 处理真实发送
|
||||
}
|
||||
|
||||
// 不推荐:只检查 success 字段
|
||||
if (data.success) {
|
||||
// 可能遗漏测试模式的情况
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 错误处理
|
||||
|
||||
```javascript
|
||||
// 推荐:根据 error_code 进行精确处理
|
||||
switch (data.error_code) {
|
||||
case 'TEST_MODE_ONLY':
|
||||
handleTestMode(data);
|
||||
break;
|
||||
case 'SEND_CODE_FAILED':
|
||||
handleSendFailure(data);
|
||||
break;
|
||||
default:
|
||||
handleGenericError(data);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 用户体验
|
||||
|
||||
- **测试模式**:清晰提示用户当前处于测试模式
|
||||
- **配置引导**:提供配置邮件服务的链接或说明
|
||||
- **验证码显示**:在测试模式下直接显示验证码
|
||||
- **状态区分**:用不同的颜色和图标区分不同状态
|
||||
|
||||
## 🔗 相关文档
|
||||
|
||||
- [邮件服务配置指南](./EMAIL_CONFIGURATION.md)
|
||||
- [快速启动指南](./QUICK_START.md)
|
||||
- [API 文档](./api/README.md)
|
||||
@@ -1,187 +1,518 @@
|
||||
# 🏗️ 项目架构设计
|
||||
# 🏗️ Whale Town 项目架构设计
|
||||
|
||||
## 整体架构
|
||||
> 基于四层架构(Gateway-Business-Core-Data)的现代化后端设计,支持双模式运行,开发测试零依赖,生产部署高性能。
|
||||
|
||||
Whale Town 采用分层架构设计,确保代码的可维护性和可扩展性。
|
||||
## 📋 目录
|
||||
|
||||
- [架构概述](#架构概述)
|
||||
- [四层架构设计](#四层架构设计)
|
||||
- [目录结构](#目录结构)
|
||||
- [双模式架构](#双模式架构)
|
||||
- [模块依赖关系](#模块依赖关系)
|
||||
- [数据流向](#数据流向)
|
||||
- [扩展指南](#扩展指南)
|
||||
|
||||
---
|
||||
|
||||
## 架构概述
|
||||
|
||||
Whale Town 采用**四层架构设计**(Gateway-Business-Core-Data),将协议处理、业务逻辑、技术基础设施和数据存储清晰分离。
|
||||
|
||||
### 核心设计理念
|
||||
|
||||
- **职责分离** - 每层职责明确,互不干扰
|
||||
- **双模式支持** - 开发测试零依赖,生产部署高性能
|
||||
- **依赖单向** - 上层依赖下层,下层不依赖上层
|
||||
- **模块化设计** - 每个模块独立完整,可单独测试
|
||||
- **配置驱动** - 通过环境变量控制运行模式
|
||||
|
||||
### 技术栈
|
||||
|
||||
**后端:** NestJS 11 + TypeScript 5 + MySQL + Redis + 原生WebSocket
|
||||
**前端:** React 18 + Vite 7 + Ant Design 5
|
||||
**测试:** Jest + Supertest(99个测试用例)
|
||||
**部署:** Docker + PM2 + Nginx
|
||||
|
||||
---
|
||||
|
||||
## 四层架构设计
|
||||
|
||||
### 架构层次图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ API 层 │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ HTTP API │ │ WebSocket │ │ GraphQL │ │
|
||||
│ │ (REST) │ │ (Socket.IO) │ │ (预留) │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||
│ 🌐 Gateway Layer (网关层) │
|
||||
│ HTTP/WebSocket协议处理、数据验证、路由管理、认证守卫 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
⬇️
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 业务逻辑层 │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ 用户认证 │ │ 游戏逻辑 │ │ 社交功能 │ │
|
||||
│ │ (Login) │ │ (Game) │ │ (Social) │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||
│ 🎯 Business Layer (业务层) │
|
||||
│ 业务逻辑实现、服务协调、业务规则验证、事务管理 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
⬇️
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 核心服务层 │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ 邮件服务 │ │ 验证码服务 │ │ 日志服务 │ │
|
||||
│ │ (Email) │ │ (Verification)│ │ (Logger) │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||
│ ⚙️ Core Layer (核心层) │
|
||||
│ 技术基础设施、数据访问、外部系统集成、工具服务 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
⬇️
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 数据访问层 │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ 用户数据 │ │ Redis缓存 │ │ 文件存储 │ │
|
||||
│ │ (Users) │ │ (Cache) │ │ (Files) │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||
│ 🗄️ Data Layer (数据层) │
|
||||
│ 数据持久化、缓存管理、文件存储 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 各层职责
|
||||
|
||||
#### 🌐 Gateway Layer(网关层)
|
||||
|
||||
**位置:** `src/gateway/`
|
||||
|
||||
**职责:**
|
||||
- HTTP/WebSocket协议处理
|
||||
- 请求参数验证(DTO)
|
||||
- 路由管理
|
||||
- 认证守卫(JWT验证)
|
||||
- 错误转换(业务错误 → HTTP状态码)
|
||||
- API文档(Swagger)
|
||||
|
||||
**原则:**
|
||||
- ✅ 只做协议转换,不做业务逻辑
|
||||
- ✅ 使用DTO进行数据验证
|
||||
- ✅ 统一的错误处理
|
||||
- ❌ 不直接访问数据库
|
||||
- ❌ 不包含业务规则
|
||||
|
||||
**示例模块:**
|
||||
- `gateway/auth/` - 认证网关(登录、注册接口)
|
||||
- `gateway/location_broadcast/` - 位置广播网关(WebSocket)
|
||||
|
||||
#### 🎯 Business Layer(业务层)
|
||||
|
||||
**位置:** `src/business/`
|
||||
|
||||
**职责:**
|
||||
- 业务逻辑实现
|
||||
- 业务流程控制
|
||||
- 服务协调
|
||||
- 业务规则验证
|
||||
- 事务管理
|
||||
|
||||
**原则:**
|
||||
- ✅ 实现所有业务逻辑
|
||||
- ✅ 协调多个Core层服务
|
||||
- ✅ 返回统一的业务响应
|
||||
- ❌ 不处理HTTP协议
|
||||
- ❌ 不直接访问数据库
|
||||
|
||||
**示例模块:**
|
||||
- `business/auth/` - 用户认证业务
|
||||
- `business/user_mgmt/` - 用户管理业务
|
||||
- `business/admin/` - 管理员业务
|
||||
- `business/zulip/` - Zulip集成业务
|
||||
- `business/location_broadcast/` - 位置广播业务
|
||||
- `business/notice/` - 公告业务
|
||||
|
||||
#### ⚙️ Core Layer(核心层)
|
||||
|
||||
**位置:** `src/core/`
|
||||
|
||||
**职责:**
|
||||
- 数据访问(数据库、缓存)
|
||||
- 基础设施(Redis、消息队列)
|
||||
- 外部系统集成(Zulip API)
|
||||
- 技术实现细节
|
||||
- 工具服务(邮件、验证码、日志)
|
||||
|
||||
**原则:**
|
||||
- ✅ 提供技术基础设施
|
||||
- ✅ 数据持久化和缓存
|
||||
- ✅ 外部API集成
|
||||
- ❌ 不包含业务逻辑
|
||||
- ❌ 不处理HTTP协议
|
||||
|
||||
**示例模块:**
|
||||
- `core/db/users/` - 用户数据服务
|
||||
- `core/redis/` - Redis缓存服务
|
||||
- `core/login_core/` - 登录核心服务
|
||||
- `core/admin_core/` - 管理员核心服务
|
||||
- `core/zulip_core/` - Zulip核心服务
|
||||
- `core/security_core/` - 安全核心服务
|
||||
- `core/utils/` - 工具服务(邮件、验证码、日志)
|
||||
|
||||
#### <20>️ Data Layer(数据层)
|
||||
|
||||
**位置:** 数据库、Redis、文件系统
|
||||
|
||||
**职责:**
|
||||
- 数据持久化
|
||||
- 缓存管理
|
||||
- 文件存储
|
||||
|
||||
**实现:**
|
||||
- MySQL / 内存数据库
|
||||
- Redis / 文件存储
|
||||
- 日志文件 / 数据文件
|
||||
|
||||
---
|
||||
|
||||
## 目录结构
|
||||
|
||||
### 整体结构
|
||||
|
||||
```
|
||||
whale-town-end/
|
||||
├── src/
|
||||
│ ├── gateway/ # 🌐 网关层
|
||||
│ │ ├── auth/ # 认证网关
|
||||
│ │ └── location_broadcast/ # 位置广播网关
|
||||
│ ├── business/ # 🎯 业务层
|
||||
│ │ ├── auth/ # 用户认证业务
|
||||
│ │ ├── user_mgmt/ # 用户管理业务
|
||||
│ │ ├── admin/ # 管理员业务
|
||||
│ │ ├── zulip/ # Zulip集成业务
|
||||
│ │ ├── location_broadcast/ # 位置广播业务
|
||||
│ │ └── notice/ # 公告业务
|
||||
│ ├── core/ # ⚙️ 核心层
|
||||
│ │ ├── db/users/ # 用户数据服务
|
||||
│ │ ├── redis/ # Redis缓存服务
|
||||
│ │ ├── login_core/ # 登录核心服务
|
||||
│ │ ├── admin_core/ # 管理员核心服务
|
||||
│ │ ├── zulip_core/ # Zulip核心服务
|
||||
│ │ ├── security_core/ # 安全核心服务
|
||||
│ │ └── utils/ # 工具服务
|
||||
│ ├── app.module.ts # 应用主模块
|
||||
│ └── main.ts # 应用入口
|
||||
├── client/ # 🎨 前端管理界面
|
||||
├── docs/ # 📚 项目文档
|
||||
├── test/ # 🧪 测试文件
|
||||
└── config/ # ⚙️ 配置文件
|
||||
```
|
||||
|
||||
### 网关层结构
|
||||
|
||||
```
|
||||
src/gateway/
|
||||
├── auth/ # 认证网关
|
||||
│ ├── login.controller.ts
|
||||
│ ├── register.controller.ts
|
||||
│ ├── jwt_auth.guard.ts
|
||||
│ ├── current_user.decorator.ts
|
||||
│ ├── dto/
|
||||
│ └── auth.gateway.module.ts
|
||||
└── location_broadcast/ # 位置广播网关
|
||||
├── location_broadcast.gateway.ts
|
||||
└── location_broadcast.gateway.module.ts
|
||||
```
|
||||
|
||||
### 业务层结构
|
||||
|
||||
```
|
||||
src/business/
|
||||
├── auth/ # 用户认证业务
|
||||
│ ├── login.service.ts
|
||||
│ ├── register.service.ts
|
||||
│ └── auth.module.ts
|
||||
├── user_mgmt/ # 用户管理业务
|
||||
│ ├── user_management.service.ts
|
||||
│ ├── dto/
|
||||
│ ├── enums/
|
||||
│ └── user_mgmt.module.ts
|
||||
├── admin/ # 管理员业务
|
||||
│ ├── admin.service.ts
|
||||
│ └── admin.module.ts
|
||||
├── zulip/ # Zulip集成业务
|
||||
│ ├── zulip.service.ts
|
||||
│ ├── services/
|
||||
│ └── zulip.module.ts
|
||||
├── location_broadcast/ # 位置广播业务
|
||||
│ ├── location_broadcast.service.ts
|
||||
│ └── location_broadcast.module.ts
|
||||
└── notice/ # 公告业务
|
||||
├── notice.service.ts
|
||||
└── notice.module.ts
|
||||
```
|
||||
|
||||
### 核心层结构
|
||||
|
||||
```
|
||||
src/core/
|
||||
├── db/users/ # 用户数据服务
|
||||
│ ├── users.service.ts # MySQL实现
|
||||
│ ├── users_memory.service.ts # 内存实现
|
||||
│ ├── users.entity.ts
|
||||
│ └── users.module.ts
|
||||
├── redis/ # Redis缓存服务
|
||||
│ ├── real_redis.service.ts
|
||||
│ ├── file_redis.service.ts
|
||||
│ └── redis.module.ts
|
||||
├── login_core/ # 登录核心服务
|
||||
│ ├── login_core.service.ts
|
||||
│ └── login_core.module.ts
|
||||
├── admin_core/ # 管理员核心服务
|
||||
│ ├── admin_core.service.ts
|
||||
│ └── admin_core.module.ts
|
||||
├── zulip_core/ # Zulip核心服务
|
||||
│ ├── services/
|
||||
│ ├── config/
|
||||
│ └── zulip_core.module.ts
|
||||
├── security_core/ # 安全核心服务
|
||||
│ ├── guards/
|
||||
│ ├── interceptors/
|
||||
│ ├── middleware/
|
||||
│ └── security_core.module.ts
|
||||
└── utils/ # 工具服务
|
||||
├── email/
|
||||
├── verification/
|
||||
└── logger/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 双模式架构
|
||||
|
||||
### 模式对比
|
||||
|
||||
| 功能 | 开发模式 | 生产模式 |
|
||||
|------|---------|---------|
|
||||
| 数据库 | 内存存储 | MySQL |
|
||||
| 缓存 | 文件存储 | Redis |
|
||||
| 邮件 | 控制台输出 | SMTP服务器 |
|
||||
| 日志 | 控制台+文件 | 结构化日志 |
|
||||
|
||||
### 配置示例
|
||||
|
||||
**开发模式:**
|
||||
```bash
|
||||
USE_FILE_REDIS=true
|
||||
NODE_ENV=development
|
||||
# 无需配置数据库和邮件
|
||||
```
|
||||
|
||||
**生产模式:**
|
||||
```bash
|
||||
USE_FILE_REDIS=false
|
||||
NODE_ENV=production
|
||||
DB_HOST=your_mysql_host
|
||||
REDIS_HOST=your_redis_host
|
||||
EMAIL_HOST=smtp.163.com
|
||||
```
|
||||
|
||||
### 实现机制
|
||||
|
||||
通过依赖注入和工厂模式实现自动切换:
|
||||
|
||||
```typescript
|
||||
@Module({
|
||||
providers: [
|
||||
{
|
||||
provide: 'IRedisService',
|
||||
useFactory: (config: ConfigService) => {
|
||||
return config.get('USE_FILE_REDIS')
|
||||
? new FileRedisService()
|
||||
: new RealRedisService(config);
|
||||
},
|
||||
inject: [ConfigService],
|
||||
},
|
||||
],
|
||||
})
|
||||
export class RedisModule {}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 模块依赖关系
|
||||
|
||||
### 依赖方向
|
||||
|
||||
```
|
||||
Gateway Layer
|
||||
↓ 依赖
|
||||
Business Layer
|
||||
↓ 依赖
|
||||
Core Layer
|
||||
↓ 依赖
|
||||
Data Layer
|
||||
```
|
||||
|
||||
### 模块依赖图
|
||||
|
||||
```
|
||||
AppModule
|
||||
├── ConfigModule (全局配置)
|
||||
├── LoggerModule (日志系统)
|
||||
├── RedisModule (缓存服务)
|
||||
├── UsersModule (用户管理)
|
||||
│ ├── UsersService (数据库模式)
|
||||
│ └── UsersMemoryService (内存模式)
|
||||
├── UsersModule (用户数据)
|
||||
├── EmailModule (邮件服务)
|
||||
├── VerificationModule (验证码服务)
|
||||
├── LoginCoreModule (登录核心)
|
||||
└── LoginModule (登录业务)
|
||||
├── AdminCoreModule (管理员核心)
|
||||
├── ZulipCoreModule (Zulip核心)
|
||||
├── SecurityCoreModule (安全核心)
|
||||
│
|
||||
├── Gateway Layer
|
||||
│ ├── AuthGatewayModule
|
||||
│ └── LocationBroadcastGatewayModule
|
||||
│
|
||||
└── Business Layer
|
||||
├── AuthModule
|
||||
├── UserMgmtModule
|
||||
├── AdminModule
|
||||
├── ZulipModule
|
||||
├── LocationBroadcastModule
|
||||
└── NoticeModule
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据流向
|
||||
|
||||
### 用户注册流程
|
||||
### 用户登录流程
|
||||
|
||||
```
|
||||
1. 用户请求 → LoginController
|
||||
2. 参数验证 → LoginService
|
||||
3. 发送验证码 → LoginCoreService
|
||||
4. 生成验证码 → VerificationService
|
||||
5. 发送邮件 → EmailService
|
||||
6. 存储验证码 → RedisService
|
||||
7. 返回响应 → 用户
|
||||
1. 用户请求 → LoginController (Gateway)
|
||||
2. 参数验证 → DTO Validation
|
||||
3. 业务逻辑 → LoginService (Business)
|
||||
4. 核心服务 → LoginCoreService (Core)
|
||||
5. 数据访问 → UsersService + RedisService (Core)
|
||||
6. 数据存储 → MySQL/Memory + Redis/File (Data)
|
||||
7. 返回响应 → 用户收到结果
|
||||
```
|
||||
|
||||
### 双模式架构
|
||||
### WebSocket消息流程
|
||||
|
||||
项目支持开发测试模式和生产部署模式的无缝切换:
|
||||
```
|
||||
1. WebSocket连接 → LocationBroadcastGateway (Gateway)
|
||||
2. 消息验证 → JWT验证
|
||||
3. 业务处理 → LocationBroadcastService (Business)
|
||||
4. 房间管理 → 地图分组逻辑
|
||||
5. 消息广播 → 同地图用户
|
||||
6. Zulip同步 → ZulipService (Business)
|
||||
```
|
||||
|
||||
#### 开发测试模式
|
||||
- **数据库**: 内存存储 (UsersMemoryService)
|
||||
- **缓存**: 文件存储 (FileRedisService)
|
||||
- **邮件**: 控制台输出 (测试模式)
|
||||
- **优势**: 无需外部依赖,快速启动测试
|
||||
### 管理员操作流程
|
||||
|
||||
#### 生产部署模式
|
||||
- **数据库**: MySQL (UsersService + TypeORM)
|
||||
- **缓存**: Redis (RealRedisService + IORedis)
|
||||
- **邮件**: SMTP服务器 (生产模式)
|
||||
- **优势**: 高性能,高可用,数据持久化
|
||||
```
|
||||
1. 管理员请求 → AdminController (Gateway)
|
||||
2. 权限验证 → AdminGuard
|
||||
3. 业务逻辑 → AdminService (Business)
|
||||
4. 核心服务 → AdminCoreService (Core)
|
||||
5. 数据更新 → UsersService (Core)
|
||||
6. 审计日志 → LoggerService (Core)
|
||||
7. 返回响应 → 管理员收到结果
|
||||
```
|
||||
|
||||
## 设计原则
|
||||
|
||||
### 1. 单一职责原则
|
||||
每个模块只负责一个特定的功能领域:
|
||||
- `LoginModule`: 只处理登录相关业务
|
||||
- `EmailModule`: 只处理邮件发送
|
||||
- `VerificationModule`: 只处理验证码逻辑
|
||||
|
||||
### 2. 依赖注入
|
||||
使用NestJS的依赖注入系统:
|
||||
- 接口抽象: `IRedisService`, `IUsersService`
|
||||
- 实现切换: 根据配置自动选择实现类
|
||||
- 测试友好: 易于Mock和单元测试
|
||||
|
||||
### 3. 配置驱动
|
||||
通过环境变量控制行为:
|
||||
- `USE_FILE_REDIS`: 选择Redis实现
|
||||
- `DB_HOST`: 数据库连接配置
|
||||
- `EMAIL_HOST`: 邮件服务配置
|
||||
|
||||
### 4. 错误处理
|
||||
统一的错误处理机制:
|
||||
- HTTP异常: `BadRequestException`, `UnauthorizedException`
|
||||
- 业务异常: 自定义异常类
|
||||
- 日志记录: 结构化错误日志
|
||||
---
|
||||
|
||||
## 扩展指南
|
||||
|
||||
### 添加新的业务模块
|
||||
|
||||
1. **创建业务模块**
|
||||
```bash
|
||||
nest g module business/game
|
||||
nest g controller business/game
|
||||
nest g service business/game
|
||||
```
|
||||
1. **创建目录结构**
|
||||
```bash
|
||||
mkdir -p src/gateway/game
|
||||
mkdir -p src/business/game
|
||||
mkdir -p src/core/game_core
|
||||
```
|
||||
|
||||
2. **创建核心服务**
|
||||
```bash
|
||||
nest g module core/game_core
|
||||
nest g service core/game_core
|
||||
```
|
||||
2. **实现网关层**
|
||||
```typescript
|
||||
// src/gateway/game/game.controller.ts
|
||||
@Controller('game')
|
||||
export class GameController {
|
||||
constructor(private readonly gameService: GameService) {}
|
||||
|
||||
3. **添加数据模型**
|
||||
```bash
|
||||
nest g module core/db/games
|
||||
nest g service core/db/games
|
||||
```
|
||||
@Post()
|
||||
async createGame(@Body() dto: CreateGameDto) {
|
||||
return this.gameService.create(dto);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. **更新主模块**
|
||||
在 `app.module.ts` 中导入新模块
|
||||
3. **实现业务层**
|
||||
```typescript
|
||||
// src/business/game/game.service.ts
|
||||
@Injectable()
|
||||
export class GameService {
|
||||
constructor(
|
||||
@Inject('IGameCoreService')
|
||||
private readonly gameCoreService: IGameCoreService,
|
||||
) {}
|
||||
|
||||
### 添加新的工具服务
|
||||
async create(dto: CreateGameDto) {
|
||||
// 业务逻辑
|
||||
return this.gameCoreService.createGame(dto);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1. **创建工具模块**
|
||||
```bash
|
||||
nest g module core/utils/notification
|
||||
nest g service core/utils/notification
|
||||
```
|
||||
4. **实现核心层**
|
||||
```typescript
|
||||
// src/core/game_core/game_core.service.ts
|
||||
@Injectable()
|
||||
export class GameCoreService {
|
||||
async createGame(dto: CreateGameDto) {
|
||||
// 数据访问逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **实现服务接口**
|
||||
定义抽象接口和具体实现
|
||||
5. **注册模块**
|
||||
```typescript
|
||||
// src/app.module.ts
|
||||
@Module({
|
||||
imports: [
|
||||
// ...
|
||||
GameGatewayModule,
|
||||
GameModule,
|
||||
GameCoreModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
```
|
||||
|
||||
3. **添加配置支持**
|
||||
在环境变量中添加相关配置
|
||||
### 性能优化建议
|
||||
|
||||
4. **编写测试用例**
|
||||
确保功能正确性和代码覆盖率
|
||||
1. **缓存策略**
|
||||
- 用户会话 → Redis
|
||||
- 验证码 → Redis(短期)
|
||||
- 配置信息 → 内存缓存
|
||||
|
||||
## 性能优化
|
||||
2. **数据库优化**
|
||||
- 添加索引
|
||||
- 使用连接池
|
||||
- 避免N+1查询
|
||||
|
||||
### 1. 缓存策略
|
||||
- **Redis缓存**: 验证码、会话信息
|
||||
- **内存缓存**: 配置信息、静态数据
|
||||
- **CDN缓存**: 静态资源文件
|
||||
3. **日志优化**
|
||||
- 异步写入
|
||||
- 日志分级
|
||||
- 日志轮转
|
||||
|
||||
### 2. 数据库优化
|
||||
- **连接池**: 复用数据库连接
|
||||
- **索引优化**: 关键字段建立索引
|
||||
- **查询优化**: 避免N+1查询问题
|
||||
### 安全加固建议
|
||||
|
||||
### 3. 日志优化
|
||||
- **异步日志**: 使用Pino的异步写入
|
||||
- **日志分级**: 生产环境只记录必要日志
|
||||
- **日志轮转**: 自动清理过期日志文件
|
||||
1. **数据验证**
|
||||
- 使用class-validator
|
||||
- TypeScript类型检查
|
||||
- SQL注入防护
|
||||
|
||||
## 安全考虑
|
||||
2. **认证授权**
|
||||
- JWT认证
|
||||
- 角色权限控制
|
||||
- 会话管理
|
||||
|
||||
### 1. 数据验证
|
||||
- **输入验证**: class-validator装饰器
|
||||
- **类型检查**: TypeScript静态类型
|
||||
- **SQL注入**: TypeORM参数化查询
|
||||
3. **通信安全**
|
||||
- HTTPS强制
|
||||
- CORS配置
|
||||
- 频率限制
|
||||
|
||||
### 2. 认证授权
|
||||
- **密码加密**: bcrypt哈希算法
|
||||
- **会话管理**: Redis存储会话信息
|
||||
- **权限控制**: 基于角色的访问控制
|
||||
---
|
||||
|
||||
### 3. 通信安全
|
||||
- **HTTPS**: 生产环境强制HTTPS
|
||||
- **CORS**: 跨域请求控制
|
||||
- **Rate Limiting**: API请求频率限制
|
||||
## 参考文档
|
||||
|
||||
- [架构重构文档](./ARCHITECTURE_REFACTORING.md) - 四层架构迁移指南
|
||||
- [网关层README](../src/gateway/auth/README.md) - 网关层详细说明
|
||||
- [开发规范](./development/backend_development_guide.md) - 代码规范
|
||||
- [部署指南](./deployment/DEPLOYMENT.md) - 生产环境部署
|
||||
|
||||
---
|
||||
|
||||
**🏗️ 通过清晰的四层架构设计,Whale Town 实现了职责分离、高内聚、低耦合的现代化架构!**
|
||||
|
||||
295
docs/ARCHITECTURE_REFACTORING.md
Normal file
295
docs/ARCHITECTURE_REFACTORING.md
Normal file
@@ -0,0 +1,295 @@
|
||||
# 架构重构文档
|
||||
|
||||
## 重构目标
|
||||
|
||||
将现有的混合架构重构为清晰的4层架构,实现更好的关注点分离和代码组织。
|
||||
|
||||
## 架构对比
|
||||
|
||||
### 重构前
|
||||
|
||||
```
|
||||
src/
|
||||
├── business/auth/ # 混合了Gateway和Business职责
|
||||
│ ├── login.controller.ts # HTTP协议处理
|
||||
│ ├── login.service.ts # 业务逻辑
|
||||
│ ├── jwt_auth.guard.ts # 认证守卫
|
||||
│ └── dto/ # 数据传输对象
|
||||
└── core/login_core/ # 核心层
|
||||
└── login_core.service.ts # 数据访问和基础设施
|
||||
```
|
||||
|
||||
### 重构后
|
||||
|
||||
```
|
||||
src/
|
||||
├── gateway/auth/ # 网关层(新增)
|
||||
│ ├── login.controller.ts # HTTP协议处理
|
||||
│ ├── register.controller.ts # HTTP协议处理
|
||||
│ ├── jwt_auth.guard.ts # 认证守卫
|
||||
│ ├── dto/ # 数据传输对象
|
||||
│ └── auth.gateway.module.ts # 网关模块
|
||||
├── business/auth/ # 业务层(精简)
|
||||
│ ├── login.service.ts # 登录业务逻辑
|
||||
│ ├── register.service.ts # 注册业务逻辑
|
||||
│ └── auth.module.ts # 业务模块
|
||||
└── core/login_core/ # 核心层(不变)
|
||||
└── login_core.service.ts # 数据访问和基础设施
|
||||
```
|
||||
|
||||
## 4层架构说明
|
||||
|
||||
### 1. Transport Layer(传输层)- 可选
|
||||
|
||||
**位置**:`src/transport/`
|
||||
|
||||
**职责**:
|
||||
- 底层网络通信和连接管理
|
||||
- WebSocket服务器、TCP/UDP服务器
|
||||
- 原生Socket连接池管理
|
||||
|
||||
**说明**:对于HTTP应用,NestJS已经提供了传输层,无需额外实现。对于WebSocket等特殊协议,可以在此层实现。
|
||||
|
||||
### 2. Gateway Layer(网关层)
|
||||
|
||||
**位置**:`src/gateway/`
|
||||
|
||||
**职责**:
|
||||
- HTTP协议处理和请求响应
|
||||
- 数据验证(DTO)
|
||||
- 路由管理
|
||||
- 认证守卫
|
||||
- 错误转换(业务错误 → HTTP状态码)
|
||||
- API文档
|
||||
|
||||
**原则**:
|
||||
- ✅ 只做协议转换,不做业务逻辑
|
||||
- ✅ 使用DTO进行数据验证
|
||||
- ✅ 统一的错误处理
|
||||
- ❌ 不直接访问数据库
|
||||
- ❌ 不包含业务规则
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
@Controller('auth')
|
||||
export class LoginController {
|
||||
constructor(private readonly loginService: LoginService) {}
|
||||
|
||||
@Post('login')
|
||||
async login(@Body() loginDto: LoginDto, @Res() res: Response): Promise<void> {
|
||||
// 只做协议转换
|
||||
const result = await this.loginService.login({
|
||||
identifier: loginDto.identifier,
|
||||
password: loginDto.password
|
||||
});
|
||||
|
||||
// 转换为HTTP响应
|
||||
this.handleResponse(result, res);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Business Layer(业务层)
|
||||
|
||||
**位置**:`src/business/`
|
||||
|
||||
**职责**:
|
||||
- 业务逻辑实现
|
||||
- 业务流程控制
|
||||
- 服务协调
|
||||
- 业务规则验证
|
||||
- 事务管理
|
||||
|
||||
**原则**:
|
||||
- ✅ 实现所有业务逻辑
|
||||
- ✅ 协调多个Core层服务
|
||||
- ✅ 返回统一的业务响应
|
||||
- ❌ 不处理HTTP协议
|
||||
- ❌ 不直接访问数据库
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
@Injectable()
|
||||
export class LoginService {
|
||||
async login(loginRequest: LoginRequest): Promise<ApiResponse<LoginResponse>> {
|
||||
try {
|
||||
// 1. 调用核心服务进行认证
|
||||
const authResult = await this.loginCoreService.login(loginRequest);
|
||||
|
||||
// 2. 业务逻辑:验证Zulip账号
|
||||
await this.validateAndUpdateZulipApiKey(authResult.user);
|
||||
|
||||
// 3. 生成JWT令牌
|
||||
const tokenPair = await this.loginCoreService.generateTokenPair(authResult.user);
|
||||
|
||||
// 4. 返回业务响应
|
||||
return {
|
||||
success: true,
|
||||
data: { user: this.formatUserInfo(authResult.user), ...tokenPair },
|
||||
message: '登录成功'
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: error.message,
|
||||
error_code: 'LOGIN_FAILED'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Core Layer(核心层)
|
||||
|
||||
**位置**:`src/core/`
|
||||
|
||||
**职责**:
|
||||
- 数据访问(数据库、缓存)
|
||||
- 基础设施(Redis、消息队列)
|
||||
- 外部系统集成
|
||||
- 技术实现细节
|
||||
|
||||
**原则**:
|
||||
- ✅ 提供技术基础设施
|
||||
- ✅ 数据持久化和缓存
|
||||
- ✅ 外部API集成
|
||||
- ❌ 不包含业务逻辑
|
||||
- ❌ 不处理HTTP协议
|
||||
|
||||
## 数据流向
|
||||
|
||||
```
|
||||
客户端请求
|
||||
↓
|
||||
Gateway Layer (Controller)
|
||||
↓ 调用
|
||||
Business Layer (Service)
|
||||
↓ 调用
|
||||
Core Layer (Data Access)
|
||||
↓
|
||||
数据库/缓存/外部API
|
||||
```
|
||||
|
||||
## 依赖关系
|
||||
|
||||
```
|
||||
Gateway → Business → Core
|
||||
```
|
||||
|
||||
- Gateway层依赖Business层
|
||||
- Business层依赖Core层
|
||||
- Core层不依赖任何业务层
|
||||
- 依赖方向单向,不允许反向依赖
|
||||
|
||||
## 重构步骤
|
||||
|
||||
### 第一阶段:登录注册模块(已完成)
|
||||
|
||||
1. ✅ 创建`src/gateway/auth/`目录
|
||||
2. ✅ 移动Controller到Gateway层
|
||||
3. ✅ 移动DTO到Gateway层
|
||||
4. ✅ 移动Guard到Gateway层
|
||||
5. ✅ 创建`AuthGatewayModule`
|
||||
6. ✅ 更新Business层模块,移除Controller
|
||||
7. ✅ 更新`app.module.ts`使用新的Gateway模块
|
||||
8. ✅ 创建架构文档
|
||||
|
||||
### 第二阶段:其他业务模块(待进行)
|
||||
|
||||
- [ ] 重构`location_broadcast`模块
|
||||
- [ ] 重构`user_mgmt`模块
|
||||
- [ ] 重构`admin`模块
|
||||
- [ ] 重构`zulip`模块
|
||||
- [ ] 重构`notice`模块
|
||||
|
||||
### 第三阶段:WebSocket模块(待进行)
|
||||
|
||||
- [ ] 创建`src/transport/websocket/`
|
||||
- [ ] 实现原生WebSocket服务器
|
||||
- [ ] 创建`src/gateway/location-broadcast/`
|
||||
- [ ] 移动WebSocket Gateway到Gateway层
|
||||
|
||||
## 迁移指南
|
||||
|
||||
### 如何判断代码应该放在哪一层?
|
||||
|
||||
**Gateway层**:
|
||||
- 包含`@Controller()`装饰器
|
||||
- 包含`@Get()`, `@Post()`等HTTP方法装饰器
|
||||
- 包含`@Body()`, `@Param()`, `@Query()`等参数装饰器
|
||||
- 包含DTO类(`class LoginDto`)
|
||||
- 包含Guard类(`class JwtAuthGuard`)
|
||||
|
||||
**Business层**:
|
||||
- 包含`@Injectable()`装饰器
|
||||
- 包含业务逻辑方法
|
||||
- 协调多个服务
|
||||
- 返回`ApiResponse<T>`格式的响应
|
||||
|
||||
**Core层**:
|
||||
- 包含数据库访问代码
|
||||
- 包含Redis操作代码
|
||||
- 包含外部API调用
|
||||
- 包含技术实现细节
|
||||
|
||||
### 重构Checklist
|
||||
|
||||
对于每个模块:
|
||||
|
||||
1. [ ] 识别Controller文件
|
||||
2. [ ] 创建对应的Gateway目录
|
||||
3. [ ] 移动Controller到Gateway层
|
||||
4. [ ] 移动DTO到Gateway层的`dto/`目录
|
||||
5. [ ] 移动Guard到Gateway层
|
||||
6. [ ] 创建Gateway Module
|
||||
7. [ ] 更新Business Module,移除Controller
|
||||
8. [ ] 更新imports,修正路径
|
||||
9. [ ] 更新app.module.ts
|
||||
10. [ ] 运行测试确保功能正常
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 保持层级职责清晰
|
||||
|
||||
每一层只做自己职责范围内的事情,不要越界。
|
||||
|
||||
### 2. 使用统一的响应格式
|
||||
|
||||
Business层返回统一的`ApiResponse<T>`格式:
|
||||
|
||||
```typescript
|
||||
interface ApiResponse<T = any> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message: string;
|
||||
error_code?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 错误处理分层
|
||||
|
||||
- Gateway层:将业务错误转换为HTTP状态码
|
||||
- Business层:捕获异常并转换为业务错误
|
||||
- Core层:抛出技术异常
|
||||
|
||||
### 4. 依赖注入
|
||||
|
||||
使用NestJS的依赖注入系统,通过Module配置依赖关系。
|
||||
|
||||
### 5. 文档完善
|
||||
|
||||
每个层级都应该有README文档说明职责和使用方法。
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **渐进式重构**:不要一次性重构所有模块,逐个模块进行
|
||||
2. **保持测试**:重构后运行测试确保功能正常
|
||||
3. **向后兼容**:重构过程中保持API接口不变
|
||||
4. **代码审查**:重构代码需要经过代码审查
|
||||
5. **文档更新**:及时更新相关文档
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [NestJS官方文档](https://docs.nestjs.com/)
|
||||
- [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
|
||||
- [Hexagonal Architecture](https://alistair.cockburn.us/hexagonal-architecture/)
|
||||
182
docs/CONTRIBUTORS.md
Normal file
182
docs/CONTRIBUTORS.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# 贡献者名单
|
||||
|
||||
感谢所有为 Whale Town 项目做出贡献的开发者们!🎉
|
||||
|
||||
## 核心贡献者
|
||||
|
||||
### <20> 项目维护者
|
||||
|
||||
**moyin** - 项目维护者
|
||||
- Gitea: [@moyin](https://gitea.xinghangee.icu/moyin)
|
||||
- Email: xinghang_a@proton.me
|
||||
- 提交数: **166 commits** (不含合并提交)
|
||||
- 主要贡献:
|
||||
- 🚀 **项目架构设计** - 四层架构(Gateway-Business-Core-Data)设计与实现
|
||||
- <20> **用户认证系统** - 完整的登录、注册、JWT认证、验证码登录
|
||||
- 📧 **邮箱验证系统** - 邮件服务、验证码服务、冷却时间机制
|
||||
- <20>️ **双模式架构** - Redis缓存(文件/真实)、用户服务(内存/数据库)
|
||||
- <20> **API文档系统** - Swagger UI、OpenAPI规范、WebSocket文档
|
||||
- 🧪 **测试框架** - Jest配置、507+测试用例、集成测试、E2E测试
|
||||
- <20> **日志系统** - Pino高性能日志、结构化日志、日志管理
|
||||
- 🏗️ **架构重构** - Zulip模块重构、认证模块分层、安全模块迁移
|
||||
- 📚 **文档体系** - 架构文档、开发规范、AI代码检查指南、部署文档
|
||||
- 🎮 **游戏功能** - 位置广播系统、通知系统、地图房间管理
|
||||
- 🔧 **项目配置** - TypeScript配置、构建配置、环境配置、Docker部署
|
||||
- 🐛 **问题修复** - 验证码TTL重置、依赖注入、HTTP状态码、数据库管理
|
||||
|
||||
### 🌟 核心开发者
|
||||
|
||||
**jianuo** - 核心开发者
|
||||
- Gitea: [@jianuo](https://gitea.xinghangee.icu/jianuo)
|
||||
- Email: 32106500027@e.gzhu.edu.cn
|
||||
- 提交数: **10 commits** (不含合并提交)
|
||||
- 主要贡献:
|
||||
- 🎛️ **管理员后台系统** - React前端界面、Ant Design组件、完整CRUD功能
|
||||
- 📊 **日志管理功能** - 运行时日志查看、日志下载、日志分析
|
||||
- <20> **管理员认证** - 独立Token认证、权限控制、会话管理
|
||||
- 🧪 **单元测试** - 管理员功能测试用例、测试覆盖率提升
|
||||
- ⚙️ **TypeScript配置** - Node16模块解析、编译配置优化
|
||||
- 🐳 **Docker部署** - 容器化部署问题修复、部署脚本优化
|
||||
- 📖 **文档维护** - 技术栈文档、部署文档、错误修复文档
|
||||
|
||||
**angjustinl** - 核心开发者
|
||||
- Gitea: [@ANGJustinl](https://gitea.xinghangee.icu/ANGJustinl)
|
||||
- GitHub: [@ANGJustinl](https://github.com/ANGJustinl)
|
||||
- Email: 96008766+ANGJustinl@users.noreply.github.com
|
||||
- 提交数: **9 commits** (不含合并提交)
|
||||
- 主要贡献:
|
||||
- <20> **Zulip集成系统** - 完整的Zulip实时通信系统、WebSocket连接、消息同步
|
||||
- 🔑 **JWT认证重构** - JWT验证机制、API密钥管理、Token刷新
|
||||
- <20> **邮箱验证重构** - 验证流程优化、内存用户服务、API响应改进
|
||||
- <20> **验证码登录** - 验证码登录功能实现、测试用例编写
|
||||
- 🧪 **测试优化** - E2E测试修复、测试断言更新、测试覆盖完善
|
||||
- 🏗️ **Zulip账户管理** - Zulip账户创建、绑定、同步机制
|
||||
|
||||
## 贡献统计
|
||||
|
||||
| 贡献者 | 提交数 | 主要领域 | 贡献占比 |
|
||||
|--------|--------|----------|----------|
|
||||
| moyin | 166 | 架构设计、核心功能、文档、测试、重构 | 89.7% |
|
||||
| jianuo | 10 | 管理员后台、日志系统、部署优化 | 5.4% |
|
||||
| angjustinl | 9 | Zulip集成、JWT认证、验证码登录 | 4.9% |
|
||||
|
||||
## 🌟 最新重要贡献
|
||||
|
||||
### 🏗️ 四层架构重构与规范化 (2026年1月)
|
||||
**主要贡献者**: moyin
|
||||
|
||||
项目完成了重大的架构升级和代码规范化工作:
|
||||
|
||||
- **认证模块重构** (1月14日): 将Gateway层组件从Business层分离,实现清晰的四层架构
|
||||
- **依赖注入优化** (1月14日): 修复AuthGatewayModule依赖注入问题,完善NestJS模块系统
|
||||
- **AI代码检查体系** (1月14日): 建立完整的AI辅助代码检查流程和规范文档
|
||||
- **架构文档完善** (1月14日): 新增架构重构文档、Gateway层规范、NestJS命名规范
|
||||
- **代码规范优化** (1月12日): 完善多个核心模块的代码规范和测试覆盖
|
||||
|
||||
### 📚 代码质量与测试提升 (2026年1月)
|
||||
**主要贡献者**: moyin
|
||||
|
||||
- **测试覆盖完善** (1月12日): 完善users、zulip、verification等模块测试覆盖
|
||||
- **文档体系建设** (1月12日): 添加开发者代码检查规范、AI代码检查执行指南
|
||||
- **性能优化** (1月12日): 集成高性能缓存系统和结构化日志
|
||||
- **模块功能扩展** (1月12日): 添加Zulip动态配置控制器和账户业务服务
|
||||
|
||||
### 🎮 游戏功能扩展 (2026年1月)
|
||||
**主要贡献者**: moyin
|
||||
|
||||
- **通知系统** (1月10日): 实现完整的通知系统核心功能和数据库支持
|
||||
- **WebSocket优化** (1月9日): 统一WebSocket网关配置、增强测试页面用户体验
|
||||
- **原生WebSocket** (1月9日): 移除Socket.IO依赖,实现原生WebSocket支持
|
||||
- **位置广播系统** (1月8日): 实现位置广播系统和端到端测试
|
||||
- **管理员系统** (1月8日): 完善管理员系统核心功能和用户管理模块
|
||||
|
||||
### 🏗️ Zulip模块架构重构 (2025年12月)
|
||||
**主要贡献者**: moyin, angjustinl
|
||||
|
||||
- **架构重构** (12月31日): 实现业务功能模块化架构,清晰分离业务层和核心层
|
||||
- **Zulip集成** (12月25日): angjustinl开发完整的Zulip实时通信系统
|
||||
- **JWT认证** (1月6日): angjustinl引入JWT验证并重构API密钥管理
|
||||
- **账户管理** (1月5日): angjustinl添加Zulip账户管理和认证系统集成
|
||||
|
||||
## 项目里程碑
|
||||
|
||||
### 2026年1月
|
||||
- **1月14日**: 🏗️ 认证模块四层架构重构,Gateway层与Business层清晰分离
|
||||
- **1月14日**: 🔧 修复AuthGatewayModule依赖注入问题,完善模块系统
|
||||
- **1月14日**: 📚 建立AI代码检查体系,添加完整的规范文档
|
||||
- **1月14日**: 📖 新增架构重构文档和NestJS框架规范说明
|
||||
- **1月12日**: ✨ 完善多个核心模块的代码规范和测试覆盖
|
||||
- **1月12日**: 🧪 添加Zulip业务模块完整测试覆盖
|
||||
- **1月12日**: 📝 添加开发者代码检查规范和AI检查执行指南
|
||||
- **1月12日**: ⚡ 集成高性能缓存系统和结构化日志
|
||||
- **1月10日**: 🔔 实现通知系统核心功能和数据库支持
|
||||
- **1月10日**: 🐛 修复数据库管理服务的关键问题
|
||||
- **1月9日**: 🌐 统一WebSocket网关配置,增强测试页面
|
||||
- **1月9日**: 🔄 移除Socket.IO依赖,实现原生WebSocket支持
|
||||
- **1月8日**: 📍 实现位置广播系统和端到端测试
|
||||
- **1月8日**: 👑 完善管理员系统核心功能
|
||||
- **1月8日**: 🏗️ 项目架构重构和命名规范化
|
||||
- **1月7日**: 📦 升级到v2.0.0版本
|
||||
- **1月6日**: 🔑 angjustinl引入JWT验证并重构API密钥管理
|
||||
- **1月5日**: 👤 angjustinl添加Zulip账户管理和认证系统集成
|
||||
- **1月4日**: 🛡️ 重构安全模块架构,迁移至core层
|
||||
|
||||
### 2025年12月
|
||||
- **12月31日**: 🏗️ Zulip模块业务功能模块化架构重构
|
||||
- **12月31日**: 📚 项目文档结构化整理和架构文档重写
|
||||
- **12月25日**: 💬 angjustinl开发完整的Zulip集成系统
|
||||
- **12月25日**: 🔄 实现验证码冷却时间自动清除机制
|
||||
- **12月25日**: 📧 完成邮箱冲突检测优化v1.1.1
|
||||
- **12月25日**: 🎯 angjustinl实现验证码登录功能
|
||||
- **12月25日**: 📈 升级项目版本到v1.1.0
|
||||
- **12月24日**: 🐛 修复注册逻辑和HTTP状态码问题
|
||||
- **12月24日**: 🔧 修复API状态码和限流配置问题
|
||||
- **12月24日**: 🏗️ 重构项目结构和业务模块架构
|
||||
- **12月23日**: 📖 全面更新API接口文档
|
||||
- **12月22日**: 🎛️ jianuo的管理员后台功能合并到主分支
|
||||
- **12月19日**: 👑 jianuo开发管理员后台系统
|
||||
- **12月19日**: 📊 jianuo完善日志管理功能
|
||||
- **12月19日**: 🧪 jianuo添加管理员后台单元测试
|
||||
- **12月19日**: ⚙️ jianuo优化TypeScript配置
|
||||
- **12月18日**: 🔄 angjustinl重构邮箱验证流程,引入内存用户服务
|
||||
- **12月18日**: 🐳 jianuo修复Docker部署问题
|
||||
- **12月18日**: 🧪 完成测试用例修复和优化
|
||||
- **12月17日**: 🐛 修复验证码TTL重置关键问题
|
||||
- **12月17日**: 📧 实现完整的邮箱验证系统
|
||||
- **12月17日**: 🗄️ 实现Redis缓存服务(双模式)
|
||||
- **12月17日**: 📝 完成API文档系统集成
|
||||
- **12月17日**: 🔐 实现完整的用户认证系统
|
||||
- **12月17日**: 🚀 项目初始化,完成基础架构搭建
|
||||
|
||||
## 如何成为贡献者
|
||||
|
||||
我们欢迎所有形式的贡献!无论是:
|
||||
|
||||
- 🐛 **Bug修复** - 发现并修复问题
|
||||
- ✨ **新功能** - 添加有价值的功能
|
||||
- 📚 **文档改进** - 完善项目文档
|
||||
- 🧪 **测试用例** - 提高代码覆盖率
|
||||
- 🎨 **代码优化** - 改进代码质量
|
||||
- 💡 **建议反馈** - 提出改进建议
|
||||
|
||||
### 贡献流程
|
||||
|
||||
1. Fork 项目到你的Gitea账户
|
||||
2. 创建功能分支:`git checkout -b feature/your-feature`
|
||||
3. 提交你的更改:`git commit -m "feat:添加新功能"`
|
||||
4. 推送到分支:`git push origin feature/your-feature`
|
||||
5. 创建Pull Request
|
||||
|
||||
### 贡献规范
|
||||
|
||||
请在贡献前阅读:
|
||||
- [开发者代码检查规范](./开发者代码检查规范.md)
|
||||
- [后端开发规范](./development/backend_development_guide.md)
|
||||
- [Git提交规范](./development/git_commit_guide.md)
|
||||
- [AI代码检查指南](./ai-reading/README.md)
|
||||
|
||||
---
|
||||
|
||||
**再次感谢所有贡献者的辛勤付出!** 🙏
|
||||
|
||||
*如果你的名字没有出现在列表中,请联系我们或提交PR更新此文件。*
|
||||
202
docs/README.md
202
docs/README.md
@@ -1,139 +1,107 @@
|
||||
# 项目文档
|
||||
# 📚 Pixel Game Server 文档中心
|
||||
|
||||
本目录包含了像素游戏服务器的完整文档。
|
||||
欢迎来到 Whale Town 项目文档中心!这里包含了项目的完整文档,帮助你快速了解和使用项目。
|
||||
|
||||
## 文档结构
|
||||
## 📖 **文档导航**
|
||||
|
||||
### 📁 api/
|
||||
API接口相关文档,包含:
|
||||
- **api-documentation.md** - 详细的API接口文档
|
||||
- **openapi.yaml** - OpenAPI 3.0规范文件
|
||||
- **postman-collection.json** - Postman测试集合
|
||||
- **README.md** - API文档使用说明
|
||||
### 🚀 **快速开始**
|
||||
- [项目概述](../README.md) - 项目介绍和快速开始指南
|
||||
- [架构设计](ARCHITECTURE.md) - 系统架构和设计理念
|
||||
|
||||
### 📁 systems/
|
||||
系统设计文档,包含:
|
||||
- **logger/** - 日志系统文档
|
||||
- **user-auth/** - 用户认证系统文档
|
||||
### 🔌 **API文档**
|
||||
- [API接口文档](api/api-documentation.md) - 完整的API接口说明(17个接口)
|
||||
- [API状态码](API_STATUS_CODES.md) - HTTP状态码和错误代码说明
|
||||
- [OpenAPI规范](api/openapi.yaml) - 机器可读的API规范文件
|
||||
- [API使用指南](api/README.md) - API文档使用说明
|
||||
|
||||
### 📄 其他文档
|
||||
- **AI辅助开发规范指南.md** - AI开发规范
|
||||
- **backend_development_guide.md** - 后端开发指南
|
||||
- **git_commit_guide.md** - Git提交规范
|
||||
- **naming_convention.md** - 命名规范
|
||||
- **nestjs_guide.md** - NestJS开发指南
|
||||
- **日志系统详细说明.md** - 日志系统说明
|
||||
### 💻 **开发指南**
|
||||
- [后端开发指南](development/backend_development_guide.md) - 后端开发规范和最佳实践
|
||||
- [NestJS指南](development/nestjs_guide.md) - NestJS框架使用指南
|
||||
- [命名规范](development/naming_convention.md) - 代码命名规范
|
||||
- [Git提交规范](development/git_commit_guide.md) - Git提交消息规范
|
||||
- [AI辅助开发规范](development/AI辅助开发规范指南.md) - AI辅助开发最佳实践
|
||||
- [测试指南](development/TESTING.md) - 测试策略和规范
|
||||
|
||||
## 如何使用
|
||||
### 🚀 **部署运维**
|
||||
- [部署指南](deployment/DEPLOYMENT.md) - 生产环境部署说明
|
||||
|
||||
### 1. 启动服务器并查看Swagger文档
|
||||
### 📋 **项目管理**
|
||||
- [贡献指南](CONTRIBUTORS.md) - 如何参与项目贡献
|
||||
- [文档清理说明](DOCUMENT_CLEANUP.md) - 文档维护和优化记录
|
||||
|
||||
```bash
|
||||
# 启动开发服务器
|
||||
pnpm run dev
|
||||
## 🏗️ **文档结构说明**
|
||||
|
||||
# 访问Swagger UI
|
||||
# 浏览器打开: http://localhost:3000/api-docs
|
||||
```
|
||||
docs/
|
||||
├── README.md # 📚 文档中心首页
|
||||
├── ARCHITECTURE.md # 🏗️ 架构文档
|
||||
├── API_STATUS_CODES.md # 📋 API状态码
|
||||
├── CONTRIBUTORS.md # 🤝 贡献指南
|
||||
├── DOCUMENT_CLEANUP.md # 📝 文档清理说明
|
||||
│
|
||||
├── api/ # 🔌 API文档
|
||||
│ ├── api-documentation.md # API接口文档
|
||||
│ ├── openapi.yaml # OpenAPI规范
|
||||
│ ├── postman-collection.json # Postman测试集合
|
||||
│ └── README.md # API文档说明
|
||||
│
|
||||
├── development/ # 💻 开发指南
|
||||
│ ├── backend_development_guide.md
|
||||
│ ├── nestjs_guide.md
|
||||
│ ├── naming_convention.md
|
||||
│ ├── git_commit_guide.md
|
||||
│ ├── AI辅助开发规范指南.md
|
||||
│ └── TESTING.md
|
||||
│
|
||||
└── deployment/ # 🚀 部署文档
|
||||
└── DEPLOYMENT.md
|
||||
```
|
||||
|
||||
### 2. 使用Postman测试API
|
||||
## 🎯 **文档特色**
|
||||
|
||||
1. 打开Postman
|
||||
2. 点击 Import 按钮
|
||||
3. 选择 `docs/postman-collection.json` 文件
|
||||
4. 导入后即可看到所有API接口
|
||||
5. 修改环境变量 `baseUrl` 为你的服务器地址(默认:http://localhost:3000)
|
||||
### ✨ **业务功能模块化**
|
||||
文档结构与代码架构保持一致,按业务功能组织:
|
||||
- **用户认证模块** - 登录、注册、密码管理
|
||||
- **用户管理模块** - 状态管理、批量操作
|
||||
- **管理员模块** - 后台管理、权限控制
|
||||
- **安全模块** - 频率限制、维护模式
|
||||
|
||||
### 3. 使用OpenAPI规范
|
||||
### 📊 **完整API覆盖**
|
||||
- **17个API接口** - 涵盖所有业务功能
|
||||
- **交互式文档** - Swagger UI实时测试
|
||||
- **标准化规范** - OpenAPI 3.0标准
|
||||
- **测试集合** - Postman一键导入
|
||||
|
||||
#### 在Swagger Editor中查看
|
||||
1. 访问 [Swagger Editor](https://editor.swagger.io/)
|
||||
2. 将 `docs/openapi.yaml` 的内容复制粘贴到编辑器中
|
||||
3. 即可查看可视化的API文档
|
||||
### 🔧 **开发者友好**
|
||||
- **规范指导** - 命名、提交、开发规范
|
||||
- **AI辅助** - 提升开发效率的AI使用指南
|
||||
- **测试覆盖** - 140个测试用例全覆盖
|
||||
- **部署就绪** - 生产环境部署指南
|
||||
|
||||
#### 生成客户端SDK
|
||||
```bash
|
||||
# 使用swagger-codegen生成JavaScript客户端
|
||||
swagger-codegen generate -i docs/openapi.yaml -l javascript -o ./client-sdk
|
||||
## 📝 **文档维护原则**
|
||||
|
||||
# 使用openapi-generator生成TypeScript客户端
|
||||
openapi-generator generate -i docs/openapi.yaml -g typescript-axios -o ./client-sdk
|
||||
```
|
||||
### ✅ **保留的文档类型**
|
||||
- **长期有用**:对整个项目生命周期都有价值的文档
|
||||
- **参考价值**:开发、部署、维护时需要查阅的文档
|
||||
- **规范指南**:团队协作和代码质量保证的规范
|
||||
|
||||
## API接口概览
|
||||
### ❌ **不保留的文档类型**
|
||||
- **阶段性文档**:只在特定开发阶段有用的文档
|
||||
- **临时记录**:会议记录、临时决策等
|
||||
- **过时信息**:已经不适用的旧版本文档
|
||||
|
||||
| 接口 | 方法 | 路径 | 描述 |
|
||||
|------|------|------|------|
|
||||
| 用户登录 | POST | /auth/login | 支持用户名、邮箱或手机号登录 |
|
||||
| 用户注册 | POST | /auth/register | 创建新用户账户 |
|
||||
| GitHub OAuth | POST | /auth/github | 使用GitHub账户登录或注册 |
|
||||
| 发送验证码 | POST | /auth/forgot-password | 发送密码重置验证码 |
|
||||
| 重置密码 | POST | /auth/reset-password | 使用验证码重置密码 |
|
||||
| 修改密码 | PUT | /auth/change-password | 修改用户密码 |
|
||||
### 🔄 **文档更新策略**
|
||||
- **及时更新**:功能变更时同步更新相关文档
|
||||
- **版本控制**:重要变更记录版本历史
|
||||
- **定期审查**:定期检查文档的准确性和有效性
|
||||
|
||||
## 快速测试
|
||||
## 🤝 **如何贡献文档**
|
||||
|
||||
### 使用cURL测试登录接口
|
||||
1. **发现问题**:发现文档错误或缺失时,请提交Issue
|
||||
2. **改进文档**:按照项目规范提交Pull Request
|
||||
3. **新增文档**:新功能开发时同步编写相关文档
|
||||
4. **审查文档**:参与文档审查,确保质量和准确性
|
||||
|
||||
```bash
|
||||
# 测试用户登录
|
||||
curl -X POST http://localhost:3000/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"identifier": "testuser",
|
||||
"password": "password123"
|
||||
}'
|
||||
---
|
||||
|
||||
# 测试用户注册
|
||||
curl -X POST http://localhost:3000/auth/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"username": "newuser",
|
||||
"password": "password123",
|
||||
"nickname": "新用户",
|
||||
"email": "newuser@example.com"
|
||||
}'
|
||||
```
|
||||
|
||||
### 使用JavaScript测试
|
||||
|
||||
```javascript
|
||||
// 用户登录
|
||||
const response = await fetch('http://localhost:3000/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
identifier: 'testuser',
|
||||
password: 'password123'
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
console.log(data);
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **开发环境**: 当前配置适用于开发环境,生产环境需要使用HTTPS
|
||||
2. **认证**: 实际应用中应实现JWT认证机制
|
||||
3. **限流**: 建议对认证接口实施限流策略
|
||||
4. **验证码**: 示例中返回验证码仅用于演示,生产环境不应返回
|
||||
5. **错误处理**: 建议实现统一的错误处理机制
|
||||
|
||||
## 更新文档
|
||||
|
||||
当API接口发生变化时,请同步更新以下文件:
|
||||
1. 更新DTO类的Swagger装饰器
|
||||
2. 更新 `api-documentation.md`
|
||||
3. 更新 `openapi.yaml`
|
||||
4. 更新 `postman-collection.json`
|
||||
5. 重新生成Swagger文档
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [NestJS Swagger文档](https://docs.nestjs.com/openapi/introduction)
|
||||
- [OpenAPI规范](https://swagger.io/specification/)
|
||||
- [Postman文档](https://learning.postman.com/docs/getting-started/introduction/)
|
||||
- [Swagger Editor](https://editor.swagger.io/)
|
||||
📧 **联系我们**:如有文档相关问题,请通过项目Issue或邮件联系维护团队。
|
||||
397
docs/ai-reading/README.md
Normal file
397
docs/ai-reading/README.md
Normal file
@@ -0,0 +1,397 @@
|
||||
# AI Code Inspection Guide - Whale Town Game Server
|
||||
|
||||
## 🎯 Pre-execution Setup
|
||||
|
||||
### 🚀 User Information Setup
|
||||
**Before starting any inspection steps, run the user information script:**
|
||||
|
||||
```bash
|
||||
# Enter AI-reading directory
|
||||
cd docs/ai-reading
|
||||
|
||||
# Run user information setup script
|
||||
node tools/setup-user-info.js
|
||||
```
|
||||
|
||||
#### Script Functions
|
||||
- Automatically get current date (YYYY-MM-DD format)
|
||||
- Check if config file exists or date matches
|
||||
- Prompt for username/nickname input if needed
|
||||
- Save to `me.config.json` file for AI inspection steps
|
||||
|
||||
#### Config File Format
|
||||
```json
|
||||
{
|
||||
"date": "2026-01-13",
|
||||
"name": "Developer Name"
|
||||
}
|
||||
```
|
||||
|
||||
### 📋 Using Config in AI Inspection Steps
|
||||
When AI executes inspection steps, get user info from config file:
|
||||
|
||||
```javascript
|
||||
// Read config file
|
||||
const fs = require('fs');
|
||||
const config = JSON.parse(fs.readFileSync('docs/ai-reading/me.config.json', 'utf-8'));
|
||||
|
||||
// Get user information
|
||||
const userDate = config.date; // e.g.: "2026-01-13"
|
||||
const userName = config.name; // e.g.: "John"
|
||||
|
||||
// Use for modification records and @author fields
|
||||
const modifyRecord = `- ${userDate}: Code standard optimization - Clean unused imports (Modified by: ${userName})`;
|
||||
```
|
||||
|
||||
### 🏗️ Project Characteristics
|
||||
This project is a **NestJS Game Server** with the following features:
|
||||
- **Dual-mode Architecture**: Supports both database and memory modes
|
||||
- **Real-time Communication**: WebSocket-based real-time bidirectional communication
|
||||
- **Property Testing**: Admin modules use fast-check for randomized testing
|
||||
- **Layered Architecture**: Core layer (technical implementation) + Business layer (business logic)
|
||||
|
||||
## 🔄 Execution Principles
|
||||
|
||||
### 🚨 Mid-step Start Requirements (Important)
|
||||
**If AI starts execution from any intermediate step (not starting from step 1), must first complete the following preparation:**
|
||||
|
||||
#### 📋 Mandatory Information Collection
|
||||
Before executing any intermediate step, AI must:
|
||||
1. **Collect user current date**: For modification records and timestamp updates
|
||||
2. **Collect user name**: For @author field handling and modification records
|
||||
3. **Confirm project characteristics**: Identify NestJS game server project features
|
||||
|
||||
#### 🔍 Global Context Acquisition
|
||||
AI must first understand:
|
||||
- **Project Architecture**: Dual-mode architecture (database+memory), layered structure (Core+Business)
|
||||
- **Tech Stack**: NestJS, WebSocket, Jest testing, fast-check property testing
|
||||
- **File Structure**: Overall file organization of current project
|
||||
- **Existing Standards**: Established naming, commenting, testing standards in project
|
||||
|
||||
#### 🎯 Execution Flow Constraints
|
||||
```
|
||||
Mid-step Start Request
|
||||
↓
|
||||
🚨 Mandatory User Info Collection (date, name)
|
||||
↓
|
||||
🚨 Mandatory Project Characteristics & Context Identification
|
||||
↓
|
||||
🚨 Mandatory Understanding of Target Step Requirements
|
||||
↓
|
||||
Start Executing Specified Step
|
||||
```
|
||||
|
||||
**⚠️ Violation Handling: If AI skips information collection and directly executes intermediate steps, user should require AI to restart and complete preparation work.**
|
||||
|
||||
### ⚠️ Mandatory Requirements
|
||||
- **Step-by-step Execution**: Execute one step at a time, strictly no step skipping or merging
|
||||
- **Wait for Confirmation**: Must wait for user confirmation after each step before proceeding
|
||||
- **Modification Verification**: Must re-execute current step after any file modification
|
||||
- **🔥 Must Re-execute Current Step After Modification**: If any modification behavior occurs during current step (file modification, renaming, moving, etc.), AI must immediately re-execute the complete check of that step, cannot directly proceed to next step
|
||||
- **Re-check After Problem Fix**: If current step has problems requiring modification, AI must re-execute the step after solving problems to ensure no other issues are missed
|
||||
- **User Info Usage**: All date fields use user-provided real dates, @author fields handled correctly
|
||||
|
||||
### 🎯 Execution Flow
|
||||
```
|
||||
User Requests Code Inspection
|
||||
↓
|
||||
Collect User Info (date, name)
|
||||
↓
|
||||
Identify Project Characteristics
|
||||
↓
|
||||
Execute Step 1 → Provide Report → Wait for Confirmation
|
||||
↓
|
||||
[If Modification Occurs] → 🔥 Immediately Re-execute Step 1 → Verification Report → Wait for Confirmation
|
||||
↓
|
||||
Execute Step 2 → Provide Report → Wait for Confirmation
|
||||
↓
|
||||
[If Modification Occurs] → 🔥 Immediately Re-execute Step 2 → Verification Report → Wait for Confirmation
|
||||
↓
|
||||
Execute Step 3 → Provide Report → Wait for Confirmation
|
||||
↓
|
||||
[If Modification Occurs] → 🔥 Immediately Re-execute Step 3 → Verification Report → Wait for Confirmation
|
||||
↓
|
||||
Execute Step 4 → Provide Report → Wait for Confirmation
|
||||
↓
|
||||
[If Modification Occurs] → 🔥 Immediately Re-execute Step 4 → Verification Report → Wait for Confirmation
|
||||
↓
|
||||
Execute Step 5 → Provide Report → Wait for Confirmation
|
||||
↓
|
||||
[If Modification Occurs] → 🔥 Immediately Re-execute Step 5 → Verification Report → Wait for Confirmation
|
||||
↓
|
||||
Execute Step 6 → Provide Report → Wait for Confirmation
|
||||
↓
|
||||
[If Modification Occurs] → 🔥 Immediately Re-execute Step 6 → Verification Report → Wait for Confirmation
|
||||
↓
|
||||
Execute Step 7 → Provide Report → Wait for Confirmation
|
||||
↓
|
||||
[If Modification Occurs] → 🔥 Immediately Re-execute Step 7 → Verification Report → Wait for Confirmation
|
||||
|
||||
⚠️ Key Rule: After any modification behavior in any step, must immediately re-execute that step!
|
||||
```
|
||||
|
||||
## 📚 Step Execution Guide
|
||||
|
||||
### Step 1: Naming Convention Check
|
||||
**Read when executing:** `step1-naming-convention.md`
|
||||
**Focus on:** Folder structure flattening, game server special file types
|
||||
**After completion:** Provide inspection report, wait for user confirmation
|
||||
|
||||
### Step 2: Comment Standard Check
|
||||
**Read when executing:** `step2-comment-standard.md`
|
||||
**Focus on:** @author field handling, modification record updates, timestamp rules
|
||||
**After completion:** Provide inspection report, wait for user confirmation
|
||||
|
||||
### Step 3: Code Quality Check
|
||||
**Read when executing:** `step3-code-quality.md`
|
||||
**Focus on:** TODO item handling, unused code cleanup
|
||||
**After completion:** Provide inspection report, wait for user confirmation
|
||||
|
||||
### Step 4: Architecture Layer Check
|
||||
**Read when executing:** `step4-architecture-layer.md`
|
||||
**Focus on:** Core layer naming standards, dependency relationship checks
|
||||
**After completion:** Provide inspection report, wait for user confirmation
|
||||
|
||||
### Step 5: Test Coverage Check
|
||||
**Read when executing:** `step5-test-coverage.md`
|
||||
**Focus on:** Strict one-to-one test mapping, test file locations, test execution verification
|
||||
**After completion:** Provide inspection report, wait for user confirmation
|
||||
|
||||
#### 🧪 Test File Debugging Standards
|
||||
**When debugging test files, must follow this workflow:**
|
||||
|
||||
1. **Read jest.config.js Configuration**
|
||||
- Check jest.config.js to understand test environment configuration
|
||||
- Confirm testRegex patterns and file matching rules
|
||||
- Understand moduleNameMapper and other configuration items
|
||||
|
||||
2. **Use Existing Test Commands in package.json**
|
||||
- **Forbidden to customize jest commands**: Must use test commands defined in package.json scripts
|
||||
- **Common Test Commands**:
|
||||
- `npm run test` - Run all tests
|
||||
- `npm run test:unit` - Run unit tests (.spec.ts files)
|
||||
- `npm run test:integration` - Run integration tests (.integration.spec.ts files)
|
||||
- `npm run test:e2e` - Run end-to-end tests (.e2e.spec.ts files)
|
||||
- `npm run test:watch` - Run tests in watch mode
|
||||
- `npm run test:cov` - Run tests and generate coverage report
|
||||
- `npm run test:debug` - Run tests in debug mode
|
||||
- `npm run test:isolated` - Run tests in isolation
|
||||
|
||||
3. **Specific Module Test Commands**
|
||||
- **Zulip Module Tests**:
|
||||
- `npm run test:zulip` - Run all Zulip-related tests
|
||||
- `npm run test:zulip:unit` - Run Zulip unit tests
|
||||
- `npm run test:zulip:integration` - Run Zulip integration tests
|
||||
- `npm run test:zulip:e2e` - Run Zulip end-to-end tests
|
||||
- `npm run test:zulip:performance` - Run Zulip performance tests
|
||||
|
||||
4. **Test Execution Verification Workflow**
|
||||
```
|
||||
Discover Test Issue → Read jest.config.js → Choose Appropriate npm run test:xxx Command → Execute Test → Analyze Results → Fix Issues → Re-execute Test
|
||||
```
|
||||
|
||||
5. **Test Command Selection Principles**
|
||||
- **Single File Test**: Use `npm run test -- file_path`
|
||||
- **Specific Type Test**: Use corresponding test:xxx command
|
||||
- **Debug Test**: Prioritize `npm run test:debug`
|
||||
- **CI/CD Environment**: Use `npm run test:isolated`
|
||||
|
||||
### Step 6: Function Documentation Generation
|
||||
**Read when executing:** `step6-documentation.md`
|
||||
**Focus on:** API interface documentation, WebSocket event documentation
|
||||
**After completion:** Provide inspection report, wait for user confirmation
|
||||
|
||||
### Step 7: Code Commit
|
||||
**Read when executing:** `step7-code-commit.md`
|
||||
**Focus on:** Git change verification, modification record consistency check, standardized commit process
|
||||
**After completion:** Provide inspection report, wait for user confirmation
|
||||
|
||||
## 📋 Unified Report Template
|
||||
|
||||
Use this template for reporting after each step completion:
|
||||
|
||||
```
|
||||
## Step X: [Step Name] Inspection Report
|
||||
|
||||
### 🔍 Inspection Results
|
||||
[List of discovered issues]
|
||||
|
||||
### 🛠️ Correction Plan
|
||||
[Specific correction suggestions]
|
||||
|
||||
### ✅ Completion Status
|
||||
- Check Item 1 ✓/✗
|
||||
- Check Item 2 ✓/✗
|
||||
|
||||
**Please confirm correction plan, proceed to next step after confirmation**
|
||||
```
|
||||
|
||||
## 🚨 Global Constraints
|
||||
|
||||
### 📝 File Modification Record Standards (Important)
|
||||
**After each modification execution, file headers need to update modification records and related information**
|
||||
|
||||
#### Modification Type Definitions
|
||||
- `Code Standard Optimization` - Naming standards, comment standards, code cleanup, etc.
|
||||
- `Feature Addition` - Adding new features or methods
|
||||
- `Feature Modification` - Modifying existing feature implementations
|
||||
- `Bug Fix` - Fixing code defects
|
||||
- `Performance Optimization` - Improving code performance
|
||||
- `Refactoring` - Code structure adjustment but functionality unchanged
|
||||
|
||||
#### Modification Record Format Requirements
|
||||
```typescript
|
||||
/**
|
||||
* Recent Modifications:
|
||||
* - [User Date]: Code Standard Optimization - Clean unused imports (Modified by: [User Name])
|
||||
* - 2024-01-06: Bug Fix - Fix email validation logic error (Modified by: Li Si)
|
||||
* - 2024-01-05: Feature Addition - Add user verification code login feature (Modified by: Wang Wu)
|
||||
*
|
||||
* @author [Processed Author Name]
|
||||
* @version x.x.x
|
||||
* @since [Creation Date]
|
||||
* @lastModified [User Date]
|
||||
*/
|
||||
```
|
||||
|
||||
#### 🔢 Recent Modification Record Quantity Limit
|
||||
- **Maximum 5 Records**: Recent modification records keep maximum of 5 latest records
|
||||
- **Auto-delete When Exceeded**: When adding new modification records, if exceeding 5, automatically delete oldest records
|
||||
- **Maintain Time Order**: Records arranged in reverse chronological order, newest at top
|
||||
- **Complete Record Retention**: Each record must include complete date, modification type, description and modifier information
|
||||
|
||||
#### Version Number Increment Rules
|
||||
- **Patch Version +1**: Code standard optimization, bug fixes (1.0.0 → 1.0.1)
|
||||
- **Minor Version +1**: Feature addition, feature modification (1.0.1 → 1.1.0)
|
||||
- **Major Version +1**: Refactoring, architecture changes (1.1.0 → 2.0.0)
|
||||
|
||||
#### Time Update Rules
|
||||
- **Check Only No Modification**: If only checking without actually modifying file content, **do not update** @lastModified field
|
||||
- **Update Only on Actual Modification**: Only update @lastModified field and add modification records when actually modifying file content
|
||||
- **Git Change Detection**: Check if files have actual changes through `git status` and `git diff`, only add modification records and update timestamps when git shows files are modified
|
||||
|
||||
#### 🚨 Important Emphasis: Pure Check Steps Do Not Update Modification Records
|
||||
**When AI executes code inspection steps, if code already meets standards and needs no modification, then:**
|
||||
- **Forbidden to Add Modification Records**: Do not add records like "AI code inspection step X: XXX check and optimization"
|
||||
- **Forbidden to Update Timestamps**: Do not update @lastModified field
|
||||
- **Forbidden to Increment Version Numbers**: Do not modify @version field
|
||||
- **Only add modification records when actually modifying code content, comment content, structure, etc.**
|
||||
|
||||
**Wrong Example**:
|
||||
```typescript
|
||||
// ❌ Wrong: Only checked without modification but added modification record
|
||||
/**
|
||||
* Recent Modifications:
|
||||
* - 2026-01-12: Code Standard Optimization - AI code inspection step 2: Comment standard check and optimization (Modified by: moyin) // This is wrong!
|
||||
* - 2026-01-07: Feature Addition - Add user verification feature (Modified by: Zhang San)
|
||||
*/
|
||||
```
|
||||
|
||||
**Correct Example**:
|
||||
```typescript
|
||||
// ✅ Correct: Check found compliance with standards, do not add modification records
|
||||
/**
|
||||
* Recent Modifications:
|
||||
* - 2026-01-07: Feature Addition - Add user verification feature (Modified by: Zhang San) // Keep original records unchanged
|
||||
*/
|
||||
```
|
||||
|
||||
### @author Field Handling Standards
|
||||
- **Retention Principle**: Human names must be retained, cannot be arbitrarily modified
|
||||
- **AI Identifier Replacement**: Only AI identifiers (kiro, ChatGPT, Claude, AI, etc.) can be replaced with user names
|
||||
- **Judgment Example**: `@author kiro` → Can replace, `@author Zhang San` → Must retain
|
||||
|
||||
### Game Server Special Requirements
|
||||
- **WebSocket Files**: Gateway files must have complete connection and message processing tests
|
||||
- **Dual-mode Services**: Both memory services and database services need complete test coverage
|
||||
- **Property Testing**: Admin modules use fast-check for property testing
|
||||
- **Test Separation**: Strictly distinguish unit tests, integration tests, E2E tests, performance tests
|
||||
|
||||
## 🔧 Modification Verification Process
|
||||
|
||||
### 🔥 Immediately Re-execute Rule After Modification (Important)
|
||||
**After any modification behavior occurs in any step, AI must immediately re-execute that step, cannot directly proceed to next step!**
|
||||
|
||||
#### Modification Behaviors Include But Not Limited To:
|
||||
- File content modification (code, comments, configuration, etc.)
|
||||
- File renaming
|
||||
- File moving
|
||||
- File deletion
|
||||
- New file creation
|
||||
- Folder structure adjustment
|
||||
|
||||
#### Mandatory Execution Process:
|
||||
```
|
||||
Step Execution → Discover Issues → Execute Modifications → 🔥 Immediately Re-execute That Step → Verify No Omissions → User Confirmation → Next Step
|
||||
```
|
||||
|
||||
### Re-check Process After Problem Fix
|
||||
When issues are discovered and modifications made in any step, must follow this process:
|
||||
|
||||
1. **Execute Modification Operations**
|
||||
- Make specific modifications based on discovered issues
|
||||
- Ensure modification content is accurate
|
||||
- **Update file header modification records, version numbers and @lastModified fields**
|
||||
|
||||
2. **🔥 Immediately Re-execute Current Step**
|
||||
- **Cannot skip this step!**
|
||||
- Complete re-execution of all check items for that step
|
||||
- Cannot only check modified parts, must comprehensively re-check
|
||||
|
||||
3. **Provide Verification Report**
|
||||
- Confirm previously discovered issues are resolved
|
||||
- Confirm no new issues introduced
|
||||
- Confirm no other issues omitted
|
||||
|
||||
4. **Wait for User Confirmation**
|
||||
- Provide complete verification report
|
||||
- Wait for user confirmation before proceeding to next step
|
||||
|
||||
### Verification Report Template
|
||||
```
|
||||
## Step X: Modification Verification Report
|
||||
|
||||
### 🔧 Executed Modification Operations
|
||||
- Modification Type: [File modification/renaming/moving/deletion, etc.]
|
||||
- Modification Content: [Specific modification description]
|
||||
- Affected Files: [List of affected files]
|
||||
|
||||
### 📝 Updated Modification Records
|
||||
- Added Modification Record: [User Date]: [Modification Type] - [Modification Content] (Modified by: [User Name])
|
||||
- Updated Version Number: [Old Version] → [New Version]
|
||||
- Updated Timestamp: @lastModified [User Date]
|
||||
|
||||
### 🔍 Re-executed Step X Complete Check Results
|
||||
[Complete re-execution results of all check items for that step]
|
||||
|
||||
### ✅ Verification Status
|
||||
- Original Issues Resolved ✓
|
||||
- Modification Records Updated ✓
|
||||
- No New Issues Introduced ✓
|
||||
- No Other Issues Omitted ✓
|
||||
- Step X Check Completely Passed ✓
|
||||
|
||||
**🔥 Important: This step has completed modification and re-verification, please confirm before proceeding to next step**
|
||||
```
|
||||
|
||||
### Importance of Re-checking
|
||||
- **Ensure Completeness**: Avoid omitting other issues during modification process
|
||||
- **Prevent New Issues**: Ensure modifications do not introduce new problems
|
||||
- **Maintain Quality**: Each step reaches complete inspection standards
|
||||
- **Maintain Consistency**: Ensure rigor throughout entire inspection process
|
||||
- **🔥 Mandatory Execution**: Cannot skip this step after modifications
|
||||
|
||||
## ⚡ Key Success Factors
|
||||
|
||||
- **Strict Step-by-step Execution**: No step skipping, no merged execution
|
||||
- **🔥 Immediately Re-execute After Modification**: Must immediately re-execute current step after any modification behavior, cannot directly proceed to next step
|
||||
- **Must Re-check After Problem Fix**: Must re-execute entire step after file modification to ensure no omissions
|
||||
- **Must Update Modification Records**: Must update file header modification records, version numbers and timestamps after each file modification
|
||||
- **Real Modification Verification**: Verify modification effects through tools
|
||||
- **Accurate User Info Usage**: Correctly apply date and name information
|
||||
- **Project Characteristic Adaptation**: Optimize inspections for game server characteristics
|
||||
- **Complete Report Provision**: Provide detailed inspection reports for each step
|
||||
|
||||
---
|
||||
|
||||
**Before starting execution, please first run `node tools/setup-user-info.js` to set user information!**
|
||||
251
docs/ai-reading/step1-naming-convention.md
Normal file
251
docs/ai-reading/step1-naming-convention.md
Normal file
@@ -0,0 +1,251 @@
|
||||
# 步骤1:命名规范检查
|
||||
|
||||
## ⚠️ 执行前必读规范
|
||||
|
||||
**🔥 重要:在执行本步骤之前,AI必须先完整阅读同级目录下的 `README.md` 文件!**
|
||||
|
||||
该README文件包含:
|
||||
- 🎯 执行前准备和用户信息收集要求
|
||||
- 🔄 强制执行原则和分步执行流程
|
||||
- 🔥 修改后立即重新执行当前步骤的强制规则
|
||||
- 📝 文件修改记录规范和版本号递增规则
|
||||
- 🧪 测试文件调试规范和测试指令使用规范
|
||||
- 🚨 全局约束和游戏服务器特殊要求
|
||||
|
||||
**不阅读README直接执行步骤将导致执行不规范,违反项目要求!**
|
||||
|
||||
---
|
||||
|
||||
## 🎯 检查目标
|
||||
检查和修正所有命名规范问题,确保项目代码命名一致性。
|
||||
|
||||
## 📋 命名规范标准
|
||||
|
||||
### 文件和文件夹命名
|
||||
|
||||
#### 🚨 NestJS 框架文件命名规范(重要)
|
||||
**本项目使用 NestJS 框架,框架相关文件命名规则:**
|
||||
|
||||
**命名组成 = 文件名(snake_case) + 类型标识符(点分隔) + 扩展名**
|
||||
|
||||
```
|
||||
✅ 正确的 NestJS 文件命名:
|
||||
- login.controller.ts # 单词文件名 + .controller
|
||||
- user_profile.service.ts # snake_case文件名 + .service
|
||||
- auth_core.module.ts # snake_case文件名 + .module
|
||||
- login_request.dto.ts # snake_case文件名 + .dto
|
||||
- jwt_auth.guard.ts # snake_case文件名 + .guard
|
||||
- current_user.decorator.ts # snake_case文件名 + .decorator
|
||||
- user_profile.controller.spec.ts # snake_case文件名 + .controller.spec
|
||||
|
||||
❌ 错误的命名示例:
|
||||
- loginController.ts # 错误!应该是 login.controller.ts
|
||||
- user-profile.service.ts # 错误!应该是 user_profile.service.ts
|
||||
- authCore.module.ts # 错误!应该是 auth_core.module.ts
|
||||
- login_controller.ts # 错误!类型标识符应该用点分隔,不是下划线
|
||||
```
|
||||
|
||||
**关键规则:**
|
||||
1. **文件名部分**:使用 snake_case(如 `user_profile`、`auth_core`)
|
||||
2. **类型标识符**:使用点分隔(如 `.controller`、`.service`)
|
||||
3. **完整格式**:`文件名.类型标识符.ts`(如 `user_profile.service.ts`)
|
||||
|
||||
**NestJS 文件类型标识符(必须使用点分隔):**
|
||||
- `.controller.ts` - 控制器(如 `user_auth.controller.ts`)
|
||||
- `.service.ts` - 服务(如 `user_profile.service.ts`)
|
||||
- `.module.ts` - 模块(如 `auth_core.module.ts`)
|
||||
- `.dto.ts` - 数据传输对象(如 `login_request.dto.ts`)
|
||||
- `.entity.ts` - 实体(如 `user_account.entity.ts`)
|
||||
- `.interface.ts` - 接口(如 `game_config.interface.ts`)
|
||||
- `.guard.ts` - 守卫(如 `jwt_auth.guard.ts`)
|
||||
- `.interceptor.ts` - 拦截器(如 `response_transform.interceptor.ts`)
|
||||
- `.pipe.ts` - 管道(如 `validation_pipe.pipe.ts`)
|
||||
- `.filter.ts` - 过滤器(如 `http_exception.filter.ts`)
|
||||
- `.decorator.ts` - 装饰器(如 `current_user.decorator.ts`)
|
||||
- `.middleware.ts` - 中间件(如 `logger_middleware.middleware.ts`)
|
||||
- `.spec.ts` - 单元测试(如 `user_profile.service.spec.ts`)
|
||||
- `.e2e.spec.ts` - E2E 测试(如 `auth_flow.e2e.spec.ts`)
|
||||
|
||||
**命名规则说明:**
|
||||
1. **文件名使用 snake_case**:多个单词用下划线连接(如 `user_profile`、`auth_core`)
|
||||
2. **类型标识符使用点分隔**:遵循 NestJS/Angular 风格(如 `.controller`、`.service`)
|
||||
3. **组合格式**:`snake_case文件名.类型标识符.ts`
|
||||
4. **社区标准**:这是本项目结合 NestJS 规范和 snake_case 约定的标准做法
|
||||
|
||||
#### 普通文件和文件夹命名
|
||||
- **规则**:snake_case(下划线分隔),保持项目一致性
|
||||
- **适用范围**:非 NestJS 框架文件、工具类、配置文件、普通文件夹等
|
||||
- **示例**:
|
||||
```
|
||||
✅ 正确:user_utils.ts, admin_operation_log.ts, config_loader.ts
|
||||
❌ 错误:UserUtils.ts, user-utils.ts, adminOperationLog.ts
|
||||
```
|
||||
|
||||
### 变量和函数命名
|
||||
- **规则**:camelCase(小驼峰命名)
|
||||
- **示例**:
|
||||
```typescript
|
||||
✅ 正确:const userName = 'test'; function getUserInfo() {}
|
||||
❌ 错误:const UserName = 'test'; function GetUserInfo() {}
|
||||
```
|
||||
|
||||
### 类和接口命名
|
||||
- **规则**:PascalCase(大驼峰命名)
|
||||
- **示例**:
|
||||
```typescript
|
||||
✅ 正确:class UserService {} interface GameConfig {}
|
||||
❌ 错误:class userService {} interface gameConfig {}
|
||||
```
|
||||
|
||||
### 常量命名
|
||||
- **规则**:SCREAMING_SNAKE_CASE(全大写+下划线)
|
||||
- **示例**:
|
||||
```typescript
|
||||
✅ 正确:const MAX_RETRY_COUNT = 3; const SALT_ROUNDS = 10;
|
||||
❌ 错误:const maxRetryCount = 3; const saltRounds = 10;
|
||||
```
|
||||
|
||||
### 路由命名
|
||||
- **规则**:kebab-case(短横线分隔)
|
||||
- **示例**:
|
||||
```typescript
|
||||
✅ 正确:@Get('user/get-info') @Post('room/join-room')
|
||||
❌ 错误:@Get('user/getInfo') @Post('room/joinRoom')
|
||||
```
|
||||
|
||||
## 🎮 游戏服务器特殊文件类型
|
||||
|
||||
### WebSocket相关文件
|
||||
```
|
||||
✅ 正确命名:
|
||||
- location_broadcast.gateway.ts # WebSocket网关
|
||||
- websocket_auth.guard.ts # WebSocket认证守卫
|
||||
- realtime_chat.service.ts # 实时通信服务
|
||||
```
|
||||
|
||||
### 双模式服务文件
|
||||
```
|
||||
✅ 正确命名:
|
||||
- users_memory.service.ts # 内存模式服务
|
||||
- users_database.service.ts # 数据库模式服务
|
||||
- file_redis.service.ts # Redis文件存储
|
||||
```
|
||||
|
||||
### 测试文件分类
|
||||
```
|
||||
✅ 正确命名:
|
||||
- user.service.spec.ts # 单元测试
|
||||
- admin.integration.spec.ts # 集成测试
|
||||
- location.property.spec.ts # 属性测试(管理员模块)
|
||||
- auth.e2e.spec.ts # E2E测试
|
||||
- websocket.perf.spec.ts # 性能测试
|
||||
```
|
||||
|
||||
## 🏗️ 文件夹结构检查
|
||||
|
||||
### 检查方法(必须使用工具)
|
||||
1. **使用listDirectory工具**:`listDirectory(path, depth=2)`获取完整结构
|
||||
2. **统计文件数量**:逐个文件夹统计文件数量
|
||||
3. **识别单文件文件夹**:只有1个文件的文件夹
|
||||
4. **执行扁平化**:将文件移动到上级目录
|
||||
5. **更新引用路径**:修改所有import语句
|
||||
|
||||
### 扁平化标准
|
||||
- **1个文件**:必须扁平化处理
|
||||
- **2个文件**:建议扁平化处理(除非是完整功能模块)
|
||||
- **≥3个文件**:保持独立文件夹
|
||||
- **完整功能模块**:即使文件较少也可保持独立(需特殊说明)
|
||||
|
||||
### 测试文件位置规范(重要)
|
||||
- ✅ **正确**:测试文件与源文件放在同一目录
|
||||
- ❌ **错误**:测试文件放在单独的tests/、test/、spec/、__tests__/文件夹
|
||||
|
||||
```
|
||||
✅ 正确结构:
|
||||
src/business/auth/
|
||||
├── auth.service.ts
|
||||
├── auth.service.spec.ts
|
||||
├── auth.controller.ts
|
||||
└── auth.controller.spec.ts
|
||||
|
||||
❌ 错误结构:
|
||||
src/business/auth/
|
||||
├── auth.service.ts
|
||||
├── auth.controller.ts
|
||||
└── tests/
|
||||
├── auth.service.spec.ts
|
||||
└── auth.controller.spec.ts
|
||||
```
|
||||
|
||||
## 🔧 Core层命名规则
|
||||
|
||||
### 业务支撑模块(使用_core后缀)
|
||||
专门为特定业务功能提供技术支撑:
|
||||
```
|
||||
✅ 正确:
|
||||
- location_broadcast_core/ # 为位置广播业务提供技术支撑
|
||||
- admin_core/ # 为管理员业务提供技术支撑
|
||||
- user_auth_core/ # 为用户认证业务提供技术支撑
|
||||
```
|
||||
|
||||
### 通用工具模块(不使用后缀)
|
||||
提供可复用的数据访问或技术服务:
|
||||
```
|
||||
✅ 正确:
|
||||
- user_profiles/ # 通用用户档案数据访问
|
||||
- redis/ # 通用Redis技术封装
|
||||
- logger/ # 通用日志工具服务
|
||||
```
|
||||
|
||||
### 判断方法
|
||||
```
|
||||
1. 模块是否专门为某个特定业务服务?
|
||||
├─ 是 → 使用_core后缀
|
||||
└─ 否 → 不使用后缀
|
||||
|
||||
2. 实际案例:
|
||||
- user_profiles: 通用数据访问 → 不使用后缀 ✓
|
||||
- location_broadcast_core: 专门为位置广播服务 → 使用_core后缀 ✓
|
||||
```
|
||||
|
||||
## ⚠️ 常见检查错误
|
||||
|
||||
1. **只看文件夹名称,不检查内容**
|
||||
2. **凭印象判断,不使用工具获取准确数据**
|
||||
3. **遗漏单文件或双文件文件夹的识别**
|
||||
4. **忽略测试文件夹扁平化**:认为tests文件夹是"标准结构"
|
||||
5. **🚨 错误地要求修改 NestJS 框架文件命名**:
|
||||
- ❌ 错误:要求将 `login.controller.ts` 改为 `login_controller.ts`(类型标识符不能用下划线)
|
||||
- ❌ 错误:要求将 `userProfile.service.ts` 改为 `userProfile.service.ts`(文件名应该用 snake_case)
|
||||
- ✅ 正确:`user_profile.service.ts`(文件名用 snake_case + 类型标识符用点分隔)
|
||||
- **判断方法**:
|
||||
- 检查类型标识符是否用点分隔(`.controller`、`.service` 等)
|
||||
- 检查文件名本身是否用 snake_case
|
||||
- 完整格式:`snake_case文件名.类型标识符.ts`
|
||||
|
||||
## 🔍 检查执行步骤
|
||||
|
||||
1. **使用listDirectory工具检查目标文件夹结构**
|
||||
2. **逐个检查文件和文件夹命名是否符合规范**
|
||||
3. **统计每个文件夹的文件数量**
|
||||
4. **识别需要扁平化的文件夹(1-2个文件)**
|
||||
5. **检查Core层模块命名是否正确**
|
||||
6. **执行必要的文件移动和重命名操作**
|
||||
7. **更新所有相关的import路径引用**
|
||||
8. **验证修改后的结构和命名**
|
||||
|
||||
## 🔥 重要提醒
|
||||
|
||||
**如果在本步骤中执行了任何修改操作(文件重命名、移动、删除等),必须立即重新执行步骤1的完整检查!**
|
||||
|
||||
- ✅ 执行修改 → 🔥 立即重新执行步骤1 → 提供验证报告 → 等待用户确认
|
||||
- ❌ 执行修改 → 直接进入步骤2(错误做法)
|
||||
|
||||
**🚨 重要强调:纯检查步骤不更新修改记录**
|
||||
**如果检查发现命名已经符合规范,无需任何修改,则:**
|
||||
- ❌ **禁止添加检查记录**:不要添加"AI代码检查步骤1:命名规范检查和优化"
|
||||
- ❌ **禁止更新时间戳**:不要修改@lastModified字段
|
||||
- ❌ **禁止递增版本号**:不要修改@version字段
|
||||
- ✅ **仅提供检查报告**:说明检查结果,确认符合规范
|
||||
|
||||
**不能跳过重新检查环节!**
|
||||
290
docs/ai-reading/step2-comment-standard.md
Normal file
290
docs/ai-reading/step2-comment-standard.md
Normal file
@@ -0,0 +1,290 @@
|
||||
# 步骤2:注释规范检查
|
||||
|
||||
## ⚠️ 执行前必读规范
|
||||
|
||||
**🔥 重要:在执行本步骤之前,AI必须先完整阅读同级目录下的 `README.md` 文件!**
|
||||
|
||||
该README文件包含:
|
||||
- 🎯 执行前准备和用户信息收集要求
|
||||
- 🔄 强制执行原则和分步执行流程
|
||||
- 🔥 修改后立即重新执行当前步骤的强制规则
|
||||
- 📝 文件修改记录规范和版本号递增规则
|
||||
- 🧪 测试文件调试规范和测试指令使用规范
|
||||
- 🚨 全局约束和游戏服务器特殊要求
|
||||
|
||||
**不阅读README直接执行步骤将导致执行不规范,违反项目要求!**
|
||||
|
||||
---
|
||||
|
||||
## 🎯 检查目标
|
||||
检查和完善所有注释规范,确保文件头、类、方法注释的完整性和准确性。
|
||||
|
||||
## 📋 注释规范标准
|
||||
|
||||
### 文件头注释(必须包含)
|
||||
```typescript
|
||||
/**
|
||||
* 文件功能描述
|
||||
*
|
||||
* 功能描述:
|
||||
* - 主要功能点1
|
||||
* - 主要功能点2
|
||||
* - 主要功能点3
|
||||
*
|
||||
* 职责分离:
|
||||
* - 职责描述1
|
||||
* - 职责描述2
|
||||
*
|
||||
* 最近修改:
|
||||
* - [用户日期]: 修改类型 - 修改内容 (修改者: [用户名称])
|
||||
* - [历史日期]: 修改类型 - 修改内容 (修改者: 历史修改者)
|
||||
*
|
||||
* @author [处理后的作者名称]
|
||||
* @version x.x.x
|
||||
* @since [创建日期]
|
||||
* @lastModified [用户日期]
|
||||
*/
|
||||
```
|
||||
|
||||
### 类注释(必须包含)
|
||||
```typescript
|
||||
/**
|
||||
* 类功能描述
|
||||
*
|
||||
* 职责:
|
||||
* - 主要职责1
|
||||
* - 主要职责2
|
||||
*
|
||||
* 主要方法:
|
||||
* - method1() - 方法1功能
|
||||
* - method2() - 方法2功能
|
||||
*
|
||||
* 使用场景:
|
||||
* - 场景描述
|
||||
*/
|
||||
@Injectable()
|
||||
export class ExampleService {
|
||||
// 类实现
|
||||
}
|
||||
```
|
||||
|
||||
### 方法注释(必须包含)
|
||||
```typescript
|
||||
/**
|
||||
* 方法功能描述
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 步骤1描述
|
||||
* 2. 步骤2描述
|
||||
* 3. 步骤3描述
|
||||
*
|
||||
* @param paramName 参数描述
|
||||
* @returns 返回值描述
|
||||
* @throws ExceptionType 异常情况描述
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const result = await service.methodName(param);
|
||||
* ```
|
||||
*/
|
||||
async methodName(paramName: ParamType): Promise<ReturnType> {
|
||||
// 方法实现
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 @author字段处理规范
|
||||
|
||||
### 处理原则
|
||||
- **保留人名**:如果@author是人名,必须保留不变
|
||||
- **替换AI标识**:只有AI标识才可替换为用户名称
|
||||
|
||||
### 判断标准
|
||||
```typescript
|
||||
// ✅ 可以替换的AI标识
|
||||
@author kiro → 替换为 @author [用户名称]
|
||||
@author ChatGPT → 替换为 @author [用户名称]
|
||||
@author Claude → 替换为 @author [用户名称]
|
||||
@author AI → 替换为 @author [用户名称]
|
||||
|
||||
// ❌ 必须保留的人名
|
||||
@author 张三 → 保留为 @author 张三
|
||||
@author John Smith → 保留为 @author John Smith
|
||||
@author 李四 → 保留为 @author 李四
|
||||
```
|
||||
|
||||
## 📝 修改记录规范
|
||||
|
||||
### 检查要点
|
||||
步骤2需要检查文件头注释中的修改记录是否符合全局规范(详见README.md全局约束部分):
|
||||
|
||||
- ✅ 修改记录格式是否正确
|
||||
- ✅ 修改类型是否准确
|
||||
- ✅ 用户日期和名称是否正确使用
|
||||
- ✅ 版本号是否按规则递增
|
||||
- ✅ @lastModified字段是否正确更新
|
||||
|
||||
### 常见检查项
|
||||
```typescript
|
||||
// ✅ 检查修改记录格式
|
||||
/**
|
||||
* 最近修改:
|
||||
* - [用户日期]: 代码规范优化 - 清理未使用的导入 (修改者: [用户名称])
|
||||
* - 历史记录...
|
||||
*/
|
||||
|
||||
// ✅ 检查版本号递增
|
||||
@version 1.0.1 // 代码规范优化应该递增修订版本
|
||||
|
||||
// ✅ 检查时间戳更新
|
||||
@lastModified [用户日期] // 只有实际修改才更新
|
||||
```
|
||||
|
||||
**注意:具体的修改记录规范请参考README.md中的全局约束部分**
|
||||
|
||||
## 📊 版本号递增规则
|
||||
|
||||
### 检查要点
|
||||
步骤2需要检查版本号是否按照全局规范正确递增(详见README.md全局约束部分):
|
||||
|
||||
- ✅ 代码规范优化、Bug修复 → 修订版本+1
|
||||
- ✅ 功能新增、功能修改 → 次版本+1
|
||||
- ✅ 重构、架构变更 → 主版本+1
|
||||
|
||||
### 检查示例
|
||||
```typescript
|
||||
// 检查版本号递增是否正确
|
||||
@version 1.0.0 → @version 1.0.1 // 代码规范优化
|
||||
@version 1.0.1 → @version 1.1.0 // 功能新增
|
||||
@version 1.1.0 → @version 2.0.0 // 重构
|
||||
```
|
||||
|
||||
## ⏰ 时间更新规则
|
||||
|
||||
### 检查要点
|
||||
步骤2需要检查时间戳更新是否符合全局规范(详见README.md全局约束部分):
|
||||
|
||||
- ✅ 仅检查不修改时,不更新@lastModified字段
|
||||
- ✅ 实际修改文件内容时,才更新@lastModified字段
|
||||
- ✅ 使用Git变更检测确认文件是否真正被修改
|
||||
|
||||
### 🚨 重要强调:纯检查不更新修改记录
|
||||
**步骤2注释规范检查时,如果发现注释已经符合规范,无需任何修改,则:**
|
||||
|
||||
#### 禁止的操作
|
||||
- ❌ **禁止添加检查记录**:不要添加"AI代码检查步骤2:注释规范检查和优化"
|
||||
- ❌ **禁止更新时间戳**:不要修改@lastModified字段
|
||||
- ❌ **禁止递增版本号**:不要修改@version字段
|
||||
- ❌ **禁止修改任何现有内容**:包括修改记录、作者信息等
|
||||
|
||||
#### 正确的做法
|
||||
- ✅ **仅进行检查**:验证注释规范是否符合要求
|
||||
- ✅ **提供检查报告**:说明检查结果和符合情况
|
||||
- ✅ **保持文件不变**:如果符合规范就不修改任何内容
|
||||
|
||||
### 实际修改才更新的情况
|
||||
**只有在以下情况下才需要更新修改记录:**
|
||||
- 添加了缺失的文件头注释
|
||||
- 补充了不完整的类注释
|
||||
- 完善了缺失的方法注释
|
||||
- 修正了错误的@author字段(AI标识替换为用户名)
|
||||
- 修复了格式错误的注释结构
|
||||
|
||||
### Git变更检测检查
|
||||
```bash
|
||||
git status # 检查是否有文件被修改
|
||||
git diff [filename] # 检查具体修改内容
|
||||
```
|
||||
|
||||
**只有git显示文件被修改时,才需要添加修改记录和更新时间戳**
|
||||
|
||||
**注意:具体的时间更新规则请参考README.md中的全局约束部分**
|
||||
|
||||
## 🎮 游戏服务器特殊注释要求
|
||||
|
||||
### WebSocket Gateway注释
|
||||
```typescript
|
||||
/**
|
||||
* 位置广播WebSocket网关
|
||||
*
|
||||
* 功能描述:
|
||||
* - 处理客户端WebSocket连接
|
||||
* - 实时广播用户位置更新
|
||||
* - 管理游戏房间成员
|
||||
*
|
||||
* WebSocket事件:
|
||||
* - connection: 客户端连接事件
|
||||
* - position_update: 位置更新事件
|
||||
* - disconnect: 客户端断开事件
|
||||
*/
|
||||
```
|
||||
|
||||
### 双模式服务注释
|
||||
```typescript
|
||||
/**
|
||||
* 用户服务(内存模式)
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供用户数据的内存存储访问
|
||||
* - 支持开发测试和故障降级场景
|
||||
* - 与数据库模式保持接口一致性
|
||||
*
|
||||
* 模式特点:
|
||||
* - 数据存储在内存Map中
|
||||
* - 应用重启后数据丢失
|
||||
* - 适用于开发测试环境
|
||||
*/
|
||||
```
|
||||
|
||||
### 属性测试注释
|
||||
```typescript
|
||||
/**
|
||||
* 管理员服务属性测试
|
||||
*
|
||||
* 功能描述:
|
||||
* - 使用fast-check进行基于属性的随机测试
|
||||
* - 验证管理员操作的正确性和边界条件
|
||||
* - 自动发现潜在的边界情况问题
|
||||
*
|
||||
* 测试策略:
|
||||
* - 随机生成用户状态变更
|
||||
* - 验证操作结果的一致性
|
||||
* - 检查异常处理的完整性
|
||||
*/
|
||||
```
|
||||
|
||||
## 🔍 检查执行步骤
|
||||
|
||||
1. **检查文件头注释完整性**
|
||||
- 功能描述是否清晰
|
||||
- 职责分离是否明确
|
||||
- 修改记录是否使用用户信息
|
||||
- @author字段是否正确处理
|
||||
|
||||
2. **检查类注释完整性**
|
||||
- 职责描述是否清晰
|
||||
- 主要方法是否列出
|
||||
- 使用场景是否说明
|
||||
|
||||
3. **检查方法注释完整性**
|
||||
- 业务逻辑步骤是否详细
|
||||
- @param、@returns、@throws是否完整
|
||||
- @example是否提供
|
||||
|
||||
4. **验证修改记录和版本号**
|
||||
- 使用git检查文件是否有实际变更
|
||||
- 根据修改类型正确递增版本号
|
||||
- 只有实际修改才更新时间戳
|
||||
|
||||
5. **特殊文件类型注释检查**
|
||||
- WebSocket Gateway的事件说明
|
||||
- 双模式服务的模式特点
|
||||
- 属性测试的测试策略
|
||||
|
||||
## 🔥 重要提醒
|
||||
|
||||
**如果在本步骤中执行了任何修改操作(添加注释、更新修改记录、修正@author字段等),必须立即重新执行步骤2的完整检查!**
|
||||
|
||||
- ✅ 执行修改 → 🔥 立即重新执行步骤2 → 提供验证报告 → 等待用户确认
|
||||
- ❌ 执行修改 → 直接进入步骤3(错误做法)
|
||||
|
||||
**不能跳过重新检查环节!**
|
||||
578
docs/ai-reading/step3-code-quality.md
Normal file
578
docs/ai-reading/step3-code-quality.md
Normal file
@@ -0,0 +1,578 @@
|
||||
# 步骤3:代码质量检查
|
||||
|
||||
## ⚠️ 执行前必读规范
|
||||
|
||||
**🔥 重要:在执行本步骤之前,AI必须先完整阅读同级目录下的 `README.md` 文件!**
|
||||
|
||||
该README文件包含:
|
||||
- 🎯 执行前准备和用户信息收集要求
|
||||
- 🔄 强制执行原则和分步执行流程
|
||||
- 🔥 修改后立即重新执行当前步骤的强制规则
|
||||
- 📝 文件修改记录规范和版本号递增规则
|
||||
- 🧪 测试文件调试规范和测试指令使用规范
|
||||
- 🚨 全局约束和游戏服务器特殊要求
|
||||
|
||||
**不阅读README直接执行步骤将导致执行不规范,违反项目要求!**
|
||||
|
||||
---
|
||||
|
||||
## 🎯 检查目标
|
||||
清理和优化代码质量,消除未使用代码、规范常量定义、处理TODO项。
|
||||
|
||||
## 🧹 未使用代码清理
|
||||
|
||||
### 清理未使用的导入
|
||||
```typescript
|
||||
// ❌ 错误:导入未使用的模块
|
||||
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { User, Admin } from './user.entity';
|
||||
import * as crypto from 'crypto'; // 未使用
|
||||
import { RedisService } from '../redis/redis.service'; // 未使用
|
||||
|
||||
// ✅ 正确:只导入使用的模块
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { User } from './user.entity';
|
||||
```
|
||||
|
||||
### 清理未使用的变量
|
||||
```typescript
|
||||
// ❌ 错误:定义但未使用的变量
|
||||
const unusedVariable = 'test';
|
||||
let tempData = [];
|
||||
|
||||
// ✅ 正确:删除未使用的变量
|
||||
// 只保留实际使用的变量
|
||||
```
|
||||
|
||||
### 清理未使用的方法
|
||||
```typescript
|
||||
// ❌ 错误:定义但未调用的私有方法
|
||||
private generateVerificationCode(): string {
|
||||
// 如果这个方法没有被调用,应该删除
|
||||
}
|
||||
|
||||
// ✅ 正确:删除未使用的私有方法
|
||||
// 或者确保方法被正确调用
|
||||
```
|
||||
|
||||
## 📊 常量定义规范
|
||||
|
||||
### 使用SCREAMING_SNAKE_CASE
|
||||
```typescript
|
||||
// ✅ 正确:使用全大写+下划线
|
||||
const SALT_ROUNDS = 10;
|
||||
const MAX_LOGIN_ATTEMPTS = 5;
|
||||
const DEFAULT_PAGE_SIZE = 20;
|
||||
const WEBSOCKET_TIMEOUT = 30000;
|
||||
const MAX_ROOM_CAPACITY = 100;
|
||||
|
||||
// ❌ 错误:使用小驼峰
|
||||
const saltRounds = 10;
|
||||
const maxLoginAttempts = 5;
|
||||
const defaultPageSize = 20;
|
||||
```
|
||||
|
||||
### 提取魔法数字为常量
|
||||
```typescript
|
||||
// ❌ 错误:使用魔法数字
|
||||
if (attempts > 5) {
|
||||
throw new Error('Too many attempts');
|
||||
}
|
||||
setTimeout(callback, 30000);
|
||||
|
||||
// ✅ 正确:提取为常量
|
||||
const MAX_LOGIN_ATTEMPTS = 5;
|
||||
const WEBSOCKET_TIMEOUT = 30000;
|
||||
|
||||
if (attempts > MAX_LOGIN_ATTEMPTS) {
|
||||
throw new Error('Too many attempts');
|
||||
}
|
||||
setTimeout(callback, WEBSOCKET_TIMEOUT);
|
||||
```
|
||||
|
||||
## 📏 方法长度检查
|
||||
|
||||
### 长度限制
|
||||
- **建议**:方法不超过50行
|
||||
- **原则**:一个方法只做一件事
|
||||
- **拆分**:复杂方法拆分为多个小方法
|
||||
|
||||
### 方法拆分示例
|
||||
```typescript
|
||||
// ❌ 错误:方法过长(超过50行)
|
||||
async processUserRegistration(userData: CreateUserDto): Promise<User> {
|
||||
// 验证用户数据
|
||||
// 检查邮箱是否存在
|
||||
// 生成密码哈希
|
||||
// 创建用户记录
|
||||
// 发送欢迎邮件
|
||||
// 记录操作日志
|
||||
// 返回用户信息
|
||||
// ... 超过50行的复杂逻辑
|
||||
}
|
||||
|
||||
// ✅ 正确:拆分为多个小方法
|
||||
async processUserRegistration(userData: CreateUserDto): Promise<User> {
|
||||
await this.validateUserData(userData);
|
||||
await this.checkEmailExists(userData.email);
|
||||
const hashedPassword = await this.generatePasswordHash(userData.password);
|
||||
const user = await this.createUserRecord({ ...userData, password: hashedPassword });
|
||||
await this.sendWelcomeEmail(user.email);
|
||||
await this.logUserRegistration(user.id);
|
||||
return user;
|
||||
}
|
||||
|
||||
private async validateUserData(userData: CreateUserDto): Promise<void> {
|
||||
// 验证逻辑
|
||||
}
|
||||
|
||||
private async checkEmailExists(email: string): Promise<void> {
|
||||
// 邮箱检查逻辑
|
||||
}
|
||||
```
|
||||
|
||||
## 🔄 代码重复消除
|
||||
|
||||
### 识别重复代码
|
||||
```typescript
|
||||
// ❌ 错误:重复的验证逻辑
|
||||
async createUser(userData: CreateUserDto): Promise<User> {
|
||||
if (!userData.email || !userData.name) {
|
||||
throw new BadRequestException('Required fields missing');
|
||||
}
|
||||
if (!this.isValidEmail(userData.email)) {
|
||||
throw new BadRequestException('Invalid email format');
|
||||
}
|
||||
// 创建用户逻辑
|
||||
}
|
||||
|
||||
async updateUser(id: string, userData: UpdateUserDto): Promise<User> {
|
||||
if (!userData.email || !userData.name) {
|
||||
throw new BadRequestException('Required fields missing');
|
||||
}
|
||||
if (!this.isValidEmail(userData.email)) {
|
||||
throw new BadRequestException('Invalid email format');
|
||||
}
|
||||
// 更新用户逻辑
|
||||
}
|
||||
|
||||
// ✅ 正确:抽象为可复用方法
|
||||
async createUser(userData: CreateUserDto): Promise<User> {
|
||||
this.validateUserData(userData);
|
||||
// 创建用户逻辑
|
||||
}
|
||||
|
||||
async updateUser(id: string, userData: UpdateUserDto): Promise<User> {
|
||||
this.validateUserData(userData);
|
||||
// 更新用户逻辑
|
||||
}
|
||||
|
||||
private validateUserData(userData: CreateUserDto | UpdateUserDto): void {
|
||||
if (!userData.email || !userData.name) {
|
||||
throw new BadRequestException('Required fields missing');
|
||||
}
|
||||
if (!this.isValidEmail(userData.email)) {
|
||||
throw new BadRequestException('Invalid email format');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🚨 异常处理完整性检查(关键规范)
|
||||
|
||||
### 问题定义
|
||||
**异常吞没(Exception Swallowing)** 是指在 catch 块中捕获异常后,只记录日志但不重新抛出,导致:
|
||||
- 调用方无法感知错误
|
||||
- 方法返回 undefined 而非声明的类型
|
||||
- 数据不一致或静默失败
|
||||
- 难以调试和定位问题
|
||||
|
||||
### 检查规则
|
||||
|
||||
#### 规则1:catch 块必须有明确的异常处理策略
|
||||
```typescript
|
||||
// ❌ 严重错误:catch 块吞没异常
|
||||
async create(createDto: CreateDto): Promise<ResponseDto> {
|
||||
try {
|
||||
const result = await this.repository.create(createDto);
|
||||
return this.toResponseDto(result);
|
||||
} catch (error) {
|
||||
this.logger.error('创建失败', error);
|
||||
// 错误:没有 throw,方法返回 undefined
|
||||
// 但声明返回 Promise<ResponseDto>
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 错误:只记录日志不处理
|
||||
async findById(id: string): Promise<Entity> {
|
||||
try {
|
||||
return await this.repository.findById(id);
|
||||
} catch (error) {
|
||||
monitor.error(error);
|
||||
// 错误:异常被吞没,调用方无法感知
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 正确:重新抛出异常
|
||||
async create(createDto: CreateDto): Promise<ResponseDto> {
|
||||
try {
|
||||
const result = await this.repository.create(createDto);
|
||||
return this.toResponseDto(result);
|
||||
} catch (error) {
|
||||
this.logger.error('创建失败', error);
|
||||
throw error; // 必须重新抛出
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 正确:转换为特定异常类型
|
||||
async create(createDto: CreateDto): Promise<ResponseDto> {
|
||||
try {
|
||||
const result = await this.repository.create(createDto);
|
||||
return this.toResponseDto(result);
|
||||
} catch (error) {
|
||||
this.logger.error('创建失败', error);
|
||||
if (error.message.includes('duplicate')) {
|
||||
throw new ConflictException('记录已存在');
|
||||
}
|
||||
throw error; // 其他错误继续抛出
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 正确:返回错误响应(仅限顶层API)
|
||||
async create(createDto: CreateDto): Promise<ApiResponse<ResponseDto>> {
|
||||
try {
|
||||
const result = await this.repository.create(createDto);
|
||||
return { success: true, data: this.toResponseDto(result) };
|
||||
} catch (error) {
|
||||
this.logger.error('创建失败', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
errorCode: 'CREATE_FAILED'
|
||||
}; // 顶层API可以返回错误响应
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 规则2:Service 层方法必须传播异常
|
||||
```typescript
|
||||
// ❌ 错误:Service 层吞没异常
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
async update(id: string, dto: UpdateDto): Promise<ResponseDto> {
|
||||
try {
|
||||
const result = await this.repository.update(id, dto);
|
||||
return this.toResponseDto(result);
|
||||
} catch (error) {
|
||||
this.logger.error('更新失败', { id, error });
|
||||
// 错误:Service 层不应吞没异常
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 正确:Service 层传播异常
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
async update(id: string, dto: UpdateDto): Promise<ResponseDto> {
|
||||
try {
|
||||
const result = await this.repository.update(id, dto);
|
||||
return this.toResponseDto(result);
|
||||
} catch (error) {
|
||||
this.logger.error('更新失败', { id, error });
|
||||
throw error; // 传播给调用方处理
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 规则3:Repository 层必须传播数据库异常
|
||||
```typescript
|
||||
// ❌ 错误:Repository 层吞没数据库异常
|
||||
@Injectable()
|
||||
export class UserRepository {
|
||||
async findById(id: bigint): Promise<User | null> {
|
||||
try {
|
||||
return await this.repository.findOne({ where: { id } });
|
||||
} catch (error) {
|
||||
this.logger.error('查询失败', { id, error });
|
||||
// 错误:数据库异常被吞没,调用方以为查询成功但返回 null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 正确:Repository 层传播异常
|
||||
@Injectable()
|
||||
export class UserRepository {
|
||||
async findById(id: bigint): Promise<User | null> {
|
||||
try {
|
||||
return await this.repository.findOne({ where: { id } });
|
||||
} catch (error) {
|
||||
this.logger.error('查询失败', { id, error });
|
||||
throw error; // 数据库异常必须传播
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 异常处理层级规范
|
||||
|
||||
| 层级 | 异常处理策略 | 说明 |
|
||||
|------|-------------|------|
|
||||
| **Repository 层** | 必须 throw | 数据访问异常必须传播 |
|
||||
| **Service 层** | 必须 throw | 业务异常必须传播给调用方 |
|
||||
| **Business 层** | 必须 throw | 业务逻辑异常必须传播 |
|
||||
| **Gateway/Controller 层** | 可以转换为 HTTP 响应 | 顶层可以将异常转换为错误响应 |
|
||||
|
||||
### 检查清单
|
||||
|
||||
- [ ] **所有 catch 块是否有 throw 语句?**
|
||||
- [ ] **方法返回类型与实际返回是否一致?**(避免返回 undefined)
|
||||
- [ ] **Service/Repository 层是否传播异常?**
|
||||
- [ ] **只有顶层 API 才能将异常转换为错误响应?**
|
||||
- [ ] **异常日志是否包含足够的上下文信息?**
|
||||
|
||||
### 快速检查命令
|
||||
```bash
|
||||
# 搜索可能吞没异常的 catch 块(没有 throw 的 catch)
|
||||
# 在代码审查时重点关注这些位置
|
||||
grep -rn "catch.*error" --include="*.ts" | grep -v "throw"
|
||||
```
|
||||
|
||||
### 常见错误模式
|
||||
|
||||
#### 模式1:性能监控后忘记抛出
|
||||
```typescript
|
||||
// ❌ 常见错误
|
||||
} catch (error) {
|
||||
monitor.error(error); // 只记录监控
|
||||
// 忘记 throw error;
|
||||
}
|
||||
|
||||
// ✅ 正确
|
||||
} catch (error) {
|
||||
monitor.error(error);
|
||||
throw error; // 必须抛出
|
||||
}
|
||||
```
|
||||
|
||||
#### 模式2:条件分支遗漏 throw
|
||||
```typescript
|
||||
// ❌ 常见错误
|
||||
} catch (error) {
|
||||
if (error.code === 'DUPLICATE') {
|
||||
throw new ConflictException('已存在');
|
||||
}
|
||||
// else 分支忘记 throw
|
||||
this.logger.error(error);
|
||||
}
|
||||
|
||||
// ✅ 正确
|
||||
} catch (error) {
|
||||
if (error.code === 'DUPLICATE') {
|
||||
throw new ConflictException('已存在');
|
||||
}
|
||||
this.logger.error(error);
|
||||
throw error; // else 分支也要抛出
|
||||
}
|
||||
```
|
||||
|
||||
#### 模式3:返回类型不匹配
|
||||
```typescript
|
||||
// ❌ 错误:声明返回 Promise<Entity> 但可能返回 undefined
|
||||
async findById(id: string): Promise<Entity> {
|
||||
try {
|
||||
return await this.repo.findById(id);
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
// 没有 throw,TypeScript 不会报错但运行时返回 undefined
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 正确
|
||||
async findById(id: string): Promise<Entity> {
|
||||
try {
|
||||
return await this.repo.findById(id);
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🚫 TODO项处理(强制要求)
|
||||
|
||||
### 处理原则
|
||||
**最终文件不能包含TODO项**,必须:
|
||||
1. **真正实现功能**
|
||||
2. **删除未完成代码**
|
||||
|
||||
### 常见TODO处理
|
||||
```typescript
|
||||
// ❌ 错误:包含TODO项的代码
|
||||
async getUserProfile(id: string): Promise<UserProfile> {
|
||||
// TODO: 实现用户档案查询
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
async sendSmsVerification(phone: string): Promise<void> {
|
||||
// TODO: 集成短信服务提供商
|
||||
throw new Error('SMS service not implemented');
|
||||
}
|
||||
|
||||
// ✅ 正确:真正实现功能
|
||||
async getUserProfile(id: string): Promise<UserProfile> {
|
||||
const profile = await this.userProfileRepository.findOne({
|
||||
where: { userId: id }
|
||||
});
|
||||
|
||||
if (!profile) {
|
||||
throw new NotFoundException('用户档案不存在');
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
// ✅ 正确:如果功能不需要,删除方法
|
||||
// 删除sendSmsVerification方法及其调用
|
||||
```
|
||||
|
||||
## 🎮 游戏服务器特殊质量要求
|
||||
|
||||
### WebSocket连接管理
|
||||
```typescript
|
||||
// ✅ 正确:完整的连接管理
|
||||
const MAX_CONNECTIONS_PER_ROOM = 100;
|
||||
const CONNECTION_TIMEOUT = 30000;
|
||||
const HEARTBEAT_INTERVAL = 10000;
|
||||
|
||||
@WebSocketGateway()
|
||||
export class LocationBroadcastGateway {
|
||||
private readonly connections = new Map<string, Socket>();
|
||||
|
||||
handleConnection(client: Socket): void {
|
||||
this.validateConnection(client);
|
||||
this.setupHeartbeat(client);
|
||||
this.trackConnection(client);
|
||||
}
|
||||
|
||||
private validateConnection(client: Socket): void {
|
||||
// 连接验证逻辑
|
||||
}
|
||||
|
||||
private setupHeartbeat(client: Socket): void {
|
||||
// 心跳检测逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 双模式服务质量
|
||||
```typescript
|
||||
// ✅ 正确:确保两种模式行为一致
|
||||
const DEFAULT_USER_STATUS = UserStatus.PENDING;
|
||||
const MAX_BATCH_SIZE = 1000;
|
||||
|
||||
@Injectable()
|
||||
export class UsersMemoryService {
|
||||
private readonly users = new Map<string, User>();
|
||||
|
||||
async create(userData: CreateUserDto): Promise<User> {
|
||||
this.validateUserData(userData);
|
||||
const user = this.buildUserEntity(userData);
|
||||
this.users.set(user.id, user);
|
||||
return user;
|
||||
}
|
||||
|
||||
private validateUserData(userData: CreateUserDto): void {
|
||||
// 与数据库模式相同的验证逻辑
|
||||
}
|
||||
|
||||
private buildUserEntity(userData: CreateUserDto): User {
|
||||
// 与数据库模式相同的实体构建逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 属性测试质量
|
||||
```typescript
|
||||
// ✅ 正确:完整的属性测试实现
|
||||
import * as fc from 'fast-check';
|
||||
|
||||
const PROPERTY_TEST_RUNS = 1000;
|
||||
const MAX_USER_ID = 1000000;
|
||||
|
||||
describe('AdminService Properties', () => {
|
||||
it('should handle any valid user status update', () => {
|
||||
fc.assert(fc.property(
|
||||
fc.integer({ min: 1, max: MAX_USER_ID }),
|
||||
fc.constantFrom(...Object.values(UserStatus)),
|
||||
async (userId, status) => {
|
||||
try {
|
||||
const result = await adminService.updateUserStatus(userId, status);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.status).toBe(status);
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(NotFoundException || BadRequestException);
|
||||
}
|
||||
}
|
||||
), { numRuns: PROPERTY_TEST_RUNS });
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 🔍 检查执行步骤
|
||||
|
||||
1. **扫描未使用的导入**
|
||||
- 检查每个import语句是否被使用
|
||||
- 删除未使用的导入
|
||||
|
||||
2. **扫描未使用的变量和方法**
|
||||
- 检查变量是否被引用
|
||||
- 检查私有方法是否被调用
|
||||
- 删除未使用的代码
|
||||
|
||||
3. **检查常量定义**
|
||||
- 识别魔法数字和字符串
|
||||
- 提取为SCREAMING_SNAKE_CASE常量
|
||||
- 确保常量命名清晰
|
||||
|
||||
4. **检查方法长度**
|
||||
- 统计每个方法的行数
|
||||
- 识别超过50行的方法
|
||||
- 建议拆分复杂方法
|
||||
|
||||
5. **识别重复代码**
|
||||
- 查找相似的代码块
|
||||
- 抽象为可复用的工具方法
|
||||
- 消除代码重复
|
||||
|
||||
6. **🚨 检查异常处理完整性(关键步骤)**
|
||||
- 扫描所有 catch 块
|
||||
- 检查是否有 throw 语句
|
||||
- 验证 Service/Repository 层是否传播异常
|
||||
- 确认方法返回类型与实际返回一致
|
||||
- 识别异常吞没模式并修复
|
||||
|
||||
7. **处理所有TODO项**
|
||||
- 搜索所有TODO注释
|
||||
- 要求真正实现功能或删除代码
|
||||
- 确保最终文件无TODO项
|
||||
|
||||
8. **游戏服务器特殊检查**
|
||||
- WebSocket连接管理完整性
|
||||
- 双模式服务行为一致性
|
||||
- 属性测试实现质量
|
||||
|
||||
## 🔥 重要提醒
|
||||
|
||||
**如果在本步骤中执行了任何修改操作(删除未使用代码、提取常量、实现TODO项等),必须立即重新执行步骤3的完整检查!**
|
||||
|
||||
- ✅ 执行修改 → 🔥 立即重新执行步骤3 → 提供验证报告 → 等待用户确认
|
||||
- ❌ 执行修改 → 直接进入步骤4(错误做法)
|
||||
|
||||
**🚨 重要强调:纯检查步骤不更新修改记录**
|
||||
**如果检查发现代码质量已经符合规范,无需任何修改,则:**
|
||||
- ❌ **禁止添加检查记录**:不要添加"AI代码检查步骤3:代码质量检查和优化"
|
||||
- ❌ **禁止更新时间戳**:不要修改@lastModified字段
|
||||
- ❌ **禁止递增版本号**:不要修改@version字段
|
||||
- ✅ **仅提供检查报告**:说明检查结果,确认符合规范
|
||||
|
||||
**不能跳过重新检查环节!**
|
||||
860
docs/ai-reading/step4-architecture-layer.md
Normal file
860
docs/ai-reading/step4-architecture-layer.md
Normal file
@@ -0,0 +1,860 @@
|
||||
# 步骤4:架构分层检查
|
||||
|
||||
## ⚠️ 执行前必读规范
|
||||
|
||||
**🔥 重要:在执行本步骤之前,AI必须先完整阅读同级目录下的 `README.md` 文件!**
|
||||
|
||||
该README文件包含:
|
||||
- 🎯 执行前准备和用户信息收集要求
|
||||
- 🔄 强制执行原则和分步执行流程
|
||||
- 🔥 修改后立即重新执行当前步骤的强制规则
|
||||
- 📝 文件修改记录规范和版本号递增规则
|
||||
- 🧪 测试文件调试规范和测试指令使用规范
|
||||
- 🚨 全局约束和游戏服务器特殊要求
|
||||
|
||||
**不阅读README直接执行步骤将导致执行不规范,违反项目要求!**
|
||||
|
||||
---
|
||||
|
||||
## 🎯 检查目标
|
||||
检查架构分层的合规性,确保Core层和Business层职责清晰、依赖关系正确。
|
||||
|
||||
## 🏗️ 架构层级识别
|
||||
|
||||
### 项目分层结构
|
||||
```
|
||||
src/
|
||||
├── gateway/ # Gateway层:网关层(HTTP协议处理)
|
||||
│ ├── auth/ # 认证网关
|
||||
│ ├── users/ # 用户网关
|
||||
│ └── admin/ # 管理网关
|
||||
├── business/ # Business层:业务逻辑层
|
||||
│ ├── auth/ # 认证业务
|
||||
│ ├── users/ # 用户业务
|
||||
│ └── admin/ # 管理业务
|
||||
├── core/ # Core层:技术实现层
|
||||
│ ├── db/ # 数据访问
|
||||
│ ├── redis/ # 缓存服务
|
||||
│ └── utils/ # 工具服务
|
||||
└── common/ # 公共层:通用组件
|
||||
```
|
||||
|
||||
### 4层架构说明
|
||||
|
||||
**Gateway Layer(网关层)**
|
||||
- 位置:`src/gateway/`
|
||||
- 职责:HTTP协议处理、数据验证、路由管理、认证守卫、错误转换
|
||||
- 依赖:Business层
|
||||
|
||||
**Business Layer(业务层)**
|
||||
- 位置:`src/business/`
|
||||
- 职责:业务逻辑实现、业务流程控制、服务协调、业务规则验证
|
||||
- 依赖:Core层
|
||||
|
||||
**Core Layer(核心层)**
|
||||
- 位置:`src/core/`
|
||||
- 职责:数据访问、基础设施、外部系统集成、技术实现细节
|
||||
- 依赖:无(或第三方库)
|
||||
|
||||
### 检查范围
|
||||
- **限制范围**:仅检查当前执行检查的文件夹
|
||||
- **不跨模块**:不考虑其他同层功能模块
|
||||
- **专注职责**:确保当前模块职责清晰
|
||||
- **按层检查**:根据文件夹所在层级应用对应的检查规则
|
||||
|
||||
## 🌐 Gateway层规范检查
|
||||
|
||||
### 职责定义
|
||||
**Gateway层专注HTTP协议处理,不包含业务逻辑**
|
||||
|
||||
### Gateway层协议处理示例
|
||||
```typescript
|
||||
// ✅ 正确:Gateway层只做协议转换
|
||||
@Controller('auth')
|
||||
export class LoginController {
|
||||
constructor(private readonly loginService: LoginService) {}
|
||||
|
||||
@Post('login')
|
||||
async login(@Body() loginDto: LoginDto, @Res() res: Response): Promise<void> {
|
||||
// 1. 接收HTTP请求,使用DTO验证
|
||||
// 2. 调用Business层服务
|
||||
const result = await this.loginService.login({
|
||||
identifier: loginDto.identifier,
|
||||
password: loginDto.password
|
||||
});
|
||||
|
||||
// 3. 将业务响应转换为HTTP响应
|
||||
this.handleResponse(result, res);
|
||||
}
|
||||
|
||||
private handleResponse(result: any, res: Response): void {
|
||||
if (result.success) {
|
||||
res.status(HttpStatus.OK).json(result);
|
||||
} else {
|
||||
const statusCode = this.getErrorStatusCode(result);
|
||||
res.status(statusCode).json(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 错误:Gateway层包含业务逻辑
|
||||
@Controller('auth')
|
||||
export class LoginController {
|
||||
@Post('login')
|
||||
async login(@Body() loginDto: LoginDto): Promise<any> {
|
||||
// 错误:在Controller中实现业务逻辑
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { username: loginDto.identifier }
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException('用户不存在');
|
||||
}
|
||||
|
||||
const isValid = await bcrypt.compare(loginDto.password, user.password);
|
||||
if (!isValid) {
|
||||
throw new UnauthorizedException('密码错误');
|
||||
}
|
||||
|
||||
// ... 更多业务逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Gateway层依赖关系检查
|
||||
```typescript
|
||||
// ✅ 允许的导入
|
||||
import { Controller, Post, Body, Res } from '@nestjs/common'; # NestJS框架
|
||||
import { Response } from 'express'; # Express类型
|
||||
import { LoginService } from '../../business/auth/login.service'; # Business层服务
|
||||
import { LoginDto } from './dto/login.dto'; # 同层DTO
|
||||
import { JwtAuthGuard } from './jwt_auth.guard'; # 同层Guard
|
||||
|
||||
// ❌ 禁止的导入
|
||||
import { LoginCoreService } from '../../core/login_core/login_core.service'; # 跳过Business层直接调用Core层
|
||||
import { UsersRepository } from '../../core/db/users/users.repository'; # 直接访问数据层
|
||||
import { RedisService } from '../../core/redis/redis.service'; # 直接访问技术服务
|
||||
```
|
||||
|
||||
### Gateway层文件类型检查
|
||||
```typescript
|
||||
// ✅ Gateway层应该包含的文件类型
|
||||
- *.controller.ts # HTTP控制器
|
||||
- *.dto.ts # 数据传输对象
|
||||
- *.guard.ts # 认证/授权守卫
|
||||
- *.decorator.ts # 参数装饰器
|
||||
- *.interceptor.ts # 拦截器
|
||||
- *.filter.ts # 异常过滤器
|
||||
- *.gateway.module.ts # 网关模块
|
||||
|
||||
// ❌ Gateway层不应该包含的文件类型
|
||||
- *.service.ts # 业务服务(应在Business层)
|
||||
- *.repository.ts # 数据仓库(应在Core层)
|
||||
- *.entity.ts # 数据实体(应在Core层)
|
||||
```
|
||||
|
||||
### Gateway层职责检查清单
|
||||
- [ ] Controller方法是否只做协议转换?
|
||||
- [ ] 是否使用DTO进行数据验证?
|
||||
- [ ] 是否调用Business层服务而非Core层?
|
||||
- [ ] 是否有统一的错误处理机制?
|
||||
- [ ] 是否包含Swagger API文档?
|
||||
- [ ] 是否使用限流和超时保护?
|
||||
|
||||
## 🔧 Core层规范检查
|
||||
|
||||
### 职责定义
|
||||
**Core层专注技术实现,不包含业务逻辑**
|
||||
|
||||
### 命名规范检查
|
||||
|
||||
#### 业务支撑模块(使用_core后缀)
|
||||
专门为特定业务功能提供技术支撑:
|
||||
```typescript
|
||||
✅ 正确示例:
|
||||
src/core/location_broadcast_core/ # 为位置广播业务提供技术支撑
|
||||
src/core/admin_core/ # 为管理员业务提供技术支撑
|
||||
src/core/user_auth_core/ # 为用户认证业务提供技术支撑
|
||||
src/core/zulip_core/ # 为Zulip集成提供技术支撑
|
||||
|
||||
❌ 错误示例:
|
||||
src/core/location_broadcast/ # 应该是location_broadcast_core
|
||||
src/core/admin/ # 应该是admin_core
|
||||
```
|
||||
|
||||
#### 通用工具模块(不使用后缀)
|
||||
提供可复用的数据访问或技术服务:
|
||||
```typescript
|
||||
✅ 正确示例:
|
||||
src/core/db/user_profiles/ # 通用的用户档案数据访问
|
||||
src/core/redis/ # 通用的Redis技术封装
|
||||
src/core/utils/logger/ # 通用的日志工具服务
|
||||
src/core/db/zulip_accounts/ # 通用的Zulip账户数据访问
|
||||
|
||||
❌ 错误示例:
|
||||
src/core/db/user_profiles_core/ # 应该是user_profiles(通用工具)
|
||||
src/core/redis_core/ # 应该是redis(通用工具)
|
||||
```
|
||||
|
||||
### 命名判断流程
|
||||
```
|
||||
1. 模块是否专门为某个特定业务功能服务?
|
||||
├─ 是 → 检查模块名称是否体现业务领域
|
||||
│ ├─ 是 → 使用 _core 后缀
|
||||
│ └─ 否 → 重新设计模块职责
|
||||
└─ 否 → 模块是否提供通用的技术服务?
|
||||
├─ 是 → 不使用 _core 后缀
|
||||
└─ 否 → 重新评估模块定位
|
||||
|
||||
2. 实际案例判断:
|
||||
- user_profiles: 通用的用户档案数据访问 → 不使用后缀 ✓
|
||||
- location_broadcast_core: 专门为位置广播业务服务 → 使用_core后缀 ✓
|
||||
- redis: 通用的缓存技术服务 → 不使用后缀 ✓
|
||||
- zulip_core: 专门为Zulip集成业务服务 → 使用_core后缀 ✓
|
||||
```
|
||||
|
||||
### Core层技术实现示例
|
||||
```typescript
|
||||
// ✅ 正确:Core层专注技术实现
|
||||
@Injectable()
|
||||
export class LocationBroadcastCoreService {
|
||||
/**
|
||||
* 广播位置更新到指定房间
|
||||
*
|
||||
* 技术实现:
|
||||
* 1. 验证WebSocket连接状态
|
||||
* 2. 序列化位置数据
|
||||
* 3. 通过Socket.IO广播消息
|
||||
* 4. 记录广播性能指标
|
||||
* 5. 处理广播异常和重试
|
||||
*/
|
||||
async broadcastToRoom(roomId: string, data: PositionData): Promise<void> {
|
||||
const room = this.server.sockets.adapter.rooms.get(roomId);
|
||||
if (!room) {
|
||||
throw new NotFoundException(`Room ${roomId} not found`);
|
||||
}
|
||||
|
||||
this.server.to(roomId).emit('position-update', data);
|
||||
this.metricsService.recordBroadcast(roomId, data.userId);
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 错误:Core层包含业务逻辑
|
||||
@Injectable()
|
||||
export class LocationBroadcastCoreService {
|
||||
async broadcastUserPosition(userId: string, position: Position): Promise<void> {
|
||||
// 错误:包含了用户权限检查的业务概念
|
||||
const user = await this.userService.findById(userId);
|
||||
if (user.status !== UserStatus.ACTIVE) {
|
||||
throw new ForbiddenException('用户状态不允许位置广播');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Core层依赖关系检查
|
||||
```typescript
|
||||
// ✅ 允许的导入
|
||||
import { Injectable } from '@nestjs/common'; # NestJS框架
|
||||
import { Server } from 'socket.io'; # 第三方技术库
|
||||
import { RedisService } from '../redis/redis.service'; # 其他Core层模块
|
||||
import * as crypto from 'crypto'; # Node.js内置模块
|
||||
|
||||
// ❌ 禁止的导入
|
||||
import { UserBusinessService } from '../../business/users/user.service'; # Business层模块
|
||||
import { AdminController } from '../../business/admin/admin.controller'; # Business层模块
|
||||
```
|
||||
|
||||
## 💼 Business层规范检查
|
||||
|
||||
### 职责定义
|
||||
**Business层专注业务逻辑实现,不关心底层技术细节**
|
||||
|
||||
### 业务逻辑完备性检查
|
||||
```typescript
|
||||
// ✅ 正确:完整的业务逻辑
|
||||
@Injectable()
|
||||
export class UserBusinessService {
|
||||
/**
|
||||
* 用户注册业务流程
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证用户信息完整性
|
||||
* 2. 检查用户名/邮箱是否已存在
|
||||
* 3. 验证邮箱格式和域名白名单
|
||||
* 4. 生成用户唯一标识
|
||||
* 5. 设置默认用户权限
|
||||
* 6. 发送欢迎邮件
|
||||
* 7. 记录注册日志
|
||||
* 8. 返回注册结果
|
||||
*/
|
||||
async registerUser(registerData: RegisterUserDto): Promise<UserResult> {
|
||||
await this.validateUserBusinessRules(registerData);
|
||||
const user = await this.userCoreService.create(registerData);
|
||||
await this.emailService.sendWelcomeEmail(user.email);
|
||||
await this.logService.recordUserRegistration(user.id);
|
||||
return this.buildUserResult(user);
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 错误:业务逻辑不完整
|
||||
@Injectable()
|
||||
export class UserBusinessService {
|
||||
async registerUser(registerData: RegisterUserDto): Promise<User> {
|
||||
// 只是简单调用数据库保存,缺少业务验证和流程
|
||||
return this.userRepository.save(registerData);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Business层依赖关系检查
|
||||
```typescript
|
||||
// ✅ 允许的导入
|
||||
import { UserCoreService } from '../../core/user_auth_core/user_core.service'; # 对应Core层业务支撑
|
||||
import { CacheService } from '../../core/redis/cache.service'; # Core层通用工具
|
||||
import { EmailService } from '../../core/utils/email.service'; # Core层通用工具
|
||||
import { OtherBusinessService } from '../other/other.service'; # 其他Business层(谨慎)
|
||||
|
||||
// ❌ 禁止的导入
|
||||
import { createConnection } from 'typeorm'; # 直接技术实现
|
||||
import * as Redis from 'ioredis'; # 直接技术实现
|
||||
import { DatabaseConnection } from '../../core/db/connection'; # 底层技术细节
|
||||
```
|
||||
|
||||
## 🚨 常见架构违规
|
||||
|
||||
### Gateway层违规示例
|
||||
```typescript
|
||||
// ❌ 错误:Gateway层包含业务逻辑
|
||||
@Controller('users')
|
||||
export class UserController {
|
||||
@Post('register')
|
||||
async register(@Body() registerDto: RegisterDto): Promise<User> {
|
||||
// 违规:在Controller中实现业务验证
|
||||
if (registerDto.age < 18) {
|
||||
throw new BadRequestException('用户年龄必须大于18岁');
|
||||
}
|
||||
|
||||
// 违规:在Controller中协调多个服务
|
||||
const user = await this.userCoreService.create(registerDto);
|
||||
await this.emailService.sendWelcomeEmail(user.email);
|
||||
await this.zulipService.createAccount(user);
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 错误:Gateway层直接调用Core层
|
||||
@Controller('auth')
|
||||
export class LoginController {
|
||||
constructor(
|
||||
private readonly loginCoreService: LoginCoreService, // 违规:跳过Business层
|
||||
) {}
|
||||
|
||||
@Post('login')
|
||||
async login(@Body() loginDto: LoginDto): Promise<any> {
|
||||
// 违规:直接调用Core层服务
|
||||
return this.loginCoreService.login(loginDto);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Business层违规示例
|
||||
```typescript
|
||||
// ❌ 错误:Business层包含技术实现细节
|
||||
@Injectable()
|
||||
export class UserBusinessService {
|
||||
async createUser(userData: CreateUserDto): Promise<User> {
|
||||
// 违规:直接操作Redis连接
|
||||
const redis = new Redis({ host: 'localhost', port: 6379 });
|
||||
await redis.set(`user:${userData.id}`, JSON.stringify(userData));
|
||||
|
||||
// 违规:直接写SQL语句
|
||||
const sql = 'INSERT INTO users (name, email) VALUES (?, ?)';
|
||||
await this.database.query(sql, [userData.name, userData.email]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Core层违规示例
|
||||
```typescript
|
||||
// ❌ 错误:Core层包含业务逻辑
|
||||
@Injectable()
|
||||
export class DatabaseService {
|
||||
async saveUser(userData: CreateUserDto): Promise<User> {
|
||||
// 违规:包含用户注册的业务验证
|
||||
if (userData.age < 18) {
|
||||
throw new BadRequestException('用户年龄必须大于18岁');
|
||||
}
|
||||
|
||||
// 违规:包含业务规则
|
||||
if (userData.email.endsWith('@competitor.com')) {
|
||||
throw new ForbiddenException('不允许竞争对手注册');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🎮 游戏服务器架构特殊检查
|
||||
|
||||
### WebSocket Gateway分层
|
||||
```typescript
|
||||
// ✅ 正确:Gateway在Business层,调用Core层服务
|
||||
@WebSocketGateway()
|
||||
export class LocationBroadcastGateway {
|
||||
constructor(
|
||||
private readonly locationBroadcastCore: LocationBroadcastCoreService,
|
||||
private readonly userProfiles: UserProfilesService,
|
||||
) {}
|
||||
|
||||
@SubscribeMessage('position_update')
|
||||
async handlePositionUpdate(client: Socket, data: PositionData): Promise<void> {
|
||||
// 业务逻辑:验证、权限检查
|
||||
await this.validateUserPermission(client.userId);
|
||||
|
||||
// 调用Core层技术实现
|
||||
await this.locationBroadcastCore.broadcastToRoom(client.roomId, data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 双模式服务分层
|
||||
```typescript
|
||||
// ✅ 正确:Business层统一接口,Core层不同实现
|
||||
@Injectable()
|
||||
export class UsersBusinessService {
|
||||
constructor(
|
||||
@Inject('USERS_SERVICE')
|
||||
private readonly usersCore: UsersMemoryService | UsersDatabaseService,
|
||||
) {}
|
||||
|
||||
async createUser(userData: CreateUserDto): Promise<User> {
|
||||
// 业务逻辑:验证、权限、流程
|
||||
await this.validateUserBusinessRules(userData);
|
||||
|
||||
// 调用Core层(内存或数据库模式)
|
||||
const user = await this.usersCore.create(userData);
|
||||
|
||||
// 业务逻辑:后续处理
|
||||
await this.sendWelcomeNotification(user);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 NestJS依赖注入检查(重要)
|
||||
|
||||
### 依赖注入完整性检查
|
||||
**在NestJS中,如果一个类(如Guard、Service、Controller)需要注入其他服务,必须确保该服务在模块的imports中可访问。**
|
||||
|
||||
### 常见依赖注入问题
|
||||
```typescript
|
||||
// ❌ 错误:JwtAuthGuard需要LoginCoreService,但模块未导入LoginCoreModule
|
||||
@Module({
|
||||
imports: [
|
||||
AuthModule, // AuthModule虽然导入了LoginCoreModule,但没有重新导出
|
||||
],
|
||||
providers: [
|
||||
JwtAuthGuard, // 错误:无法注入LoginCoreService
|
||||
],
|
||||
})
|
||||
export class AuthGatewayModule {}
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard {
|
||||
constructor(
|
||||
private readonly loginCoreService: LoginCoreService, // 注入失败!
|
||||
) {}
|
||||
}
|
||||
|
||||
// ✅ 正确方案1:直接导入需要的Core模块
|
||||
@Module({
|
||||
imports: [
|
||||
AuthModule,
|
||||
LoginCoreModule, // 直接导入,使LoginCoreService可用
|
||||
],
|
||||
providers: [
|
||||
JwtAuthGuard, // 现在可以成功注入LoginCoreService
|
||||
],
|
||||
})
|
||||
export class AuthGatewayModule {}
|
||||
|
||||
// ✅ 正确方案2:在中间模块重新导出
|
||||
@Module({
|
||||
imports: [LoginCoreModule],
|
||||
exports: [LoginCoreModule], // 重新导出,让导入AuthModule的模块也能访问
|
||||
})
|
||||
export class AuthModule {}
|
||||
```
|
||||
|
||||
### 依赖注入检查规则
|
||||
|
||||
#### 1. 检查Provider的构造函数依赖
|
||||
```typescript
|
||||
// 对于每个Provider(Service、Guard、Interceptor等)
|
||||
@Injectable()
|
||||
export class SomeGuard {
|
||||
constructor(
|
||||
private readonly serviceA: ServiceA, // 依赖1
|
||||
private readonly serviceB: ServiceB, // 依赖2
|
||||
) {}
|
||||
}
|
||||
|
||||
// 检查清单:
|
||||
// ✓ ServiceA是否在当前模块的imports中?
|
||||
// ✓ ServiceB是否在当前模块的imports中?
|
||||
// ✓ 如果不在,是否需要添加对应的Module到imports?
|
||||
```
|
||||
|
||||
#### 2. 检查Module的导出完整性
|
||||
```typescript
|
||||
// ❌ 错误:导入了模块但没有导出,导致上层模块无法访问
|
||||
@Module({
|
||||
imports: [LoginCoreModule],
|
||||
providers: [LoginService],
|
||||
exports: [LoginService], // 只导出了LoginService,没有导出LoginCoreModule
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
// 如果上层模块需要直接使用LoginCoreService:
|
||||
@Module({
|
||||
imports: [AuthModule], // 无法访问LoginCoreService
|
||||
providers: [JwtAuthGuard], // JwtAuthGuard需要LoginCoreService,会失败
|
||||
})
|
||||
export class AuthGatewayModule {}
|
||||
|
||||
// ✅ 正确:根据需要导出Module
|
||||
@Module({
|
||||
imports: [LoginCoreModule],
|
||||
providers: [LoginService],
|
||||
exports: [
|
||||
LoginService,
|
||||
LoginCoreModule, // 导出Module,让上层也能访问
|
||||
],
|
||||
})
|
||||
export class AuthModule {}
|
||||
```
|
||||
|
||||
#### 3. 检查跨层依赖的模块导入
|
||||
```typescript
|
||||
// Gateway层的Guard直接依赖Core层Service的情况
|
||||
@Injectable()
|
||||
export class JwtAuthGuard {
|
||||
constructor(
|
||||
private readonly loginCoreService: LoginCoreService, // 直接依赖Core层
|
||||
) {}
|
||||
}
|
||||
|
||||
// 检查清单:
|
||||
// ✓ AuthGatewayModule是否导入了LoginCoreModule?
|
||||
// ✓ 如果通过AuthModule间接导入,AuthModule是否导出了LoginCoreModule?
|
||||
// ✓ 是否符合架构分层原则(Gateway可以直接依赖Core用于技术实现)?
|
||||
```
|
||||
|
||||
### 依赖注入检查步骤
|
||||
|
||||
1. **扫描所有Injectable类**
|
||||
- 找出所有使用@Injectable()装饰器的类
|
||||
- 包括Service、Guard、Interceptor、Pipe等
|
||||
|
||||
2. **分析构造函数依赖**
|
||||
- 检查每个类的constructor参数
|
||||
- 列出所有需要注入的服务
|
||||
|
||||
3. **检查Module的imports**
|
||||
- 确认每个依赖的服务是否在Module的imports中
|
||||
- 检查imports的Module是否导出了需要的服务
|
||||
|
||||
4. **验证依赖链完整性**
|
||||
- 如果A模块导入B模块,B模块导入C模块
|
||||
- 确认A模块是否能访问C模块的服务(取决于B是否导出C)
|
||||
|
||||
5. **检查常见错误模式**
|
||||
- Guard/Interceptor依赖Service但模块未导入
|
||||
- 中间模块导入但未导出,导致上层无法访问
|
||||
- 循环依赖问题
|
||||
|
||||
### 依赖注入错误识别
|
||||
|
||||
#### 典型错误信息
|
||||
```
|
||||
Nest can't resolve dependencies of the JwtAuthGuard (?).
|
||||
Please make sure that the argument LoginCoreService at index [0]
|
||||
is available in the AuthGatewayModule context.
|
||||
```
|
||||
|
||||
#### 错误分析流程
|
||||
```
|
||||
1. 识别问题类:JwtAuthGuard
|
||||
2. 识别缺失依赖:LoginCoreService(索引0)
|
||||
3. 识别所在模块:AuthGatewayModule
|
||||
4. 检查解决方案:
|
||||
├─ LoginCoreService在哪个Module中提供?
|
||||
│ └─ 答:LoginCoreModule
|
||||
├─ AuthGatewayModule是否导入了LoginCoreModule?
|
||||
│ └─ 否 → 需要添加到imports
|
||||
└─ 如果通过其他Module间接导入,该Module是否导出了LoginCoreModule?
|
||||
└─ 否 → 需要在中间Module的exports中添加
|
||||
```
|
||||
|
||||
### 依赖注入最佳实践
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:明确的依赖关系
|
||||
@Module({
|
||||
imports: [
|
||||
// 业务层模块
|
||||
AuthModule,
|
||||
// 直接需要的核心层模块(用于Guard等技术组件)
|
||||
LoginCoreModule,
|
||||
],
|
||||
controllers: [LoginController],
|
||||
providers: [JwtAuthGuard],
|
||||
exports: [JwtAuthGuard],
|
||||
})
|
||||
export class AuthGatewayModule {}
|
||||
|
||||
// ✅ 推荐:完整的导出链
|
||||
@Module({
|
||||
imports: [LoginCoreModule, UsersModule],
|
||||
providers: [LoginService],
|
||||
exports: [
|
||||
LoginService, // 导出自己的服务
|
||||
LoginCoreModule, // 导出依赖的模块(如果上层需要)
|
||||
],
|
||||
})
|
||||
export class AuthModule {}
|
||||
```
|
||||
|
||||
## 🔍 检查执行步骤
|
||||
|
||||
1. **识别当前模块的层级**
|
||||
- 确定是Gateway层、Business层还是Core层
|
||||
- 检查文件夹路径和命名
|
||||
- 根据层级应用对应的检查规则
|
||||
|
||||
2. **Gateway层检查(如果是Gateway层)**
|
||||
- 检查是否只包含协议处理代码
|
||||
- 检查是否使用DTO进行数据验证
|
||||
- 检查是否只调用Business层服务
|
||||
- 检查是否有统一的错误处理
|
||||
- 检查文件类型是否符合Gateway层规范
|
||||
|
||||
3. **Business层检查(如果是Business层)**
|
||||
- 检查是否只包含业务逻辑
|
||||
- 检查是否协调多个Core层服务
|
||||
- 检查是否返回统一的业务响应
|
||||
- 检查是否不包含HTTP协议处理
|
||||
|
||||
4. **Core层检查(如果是Core层)**
|
||||
- 检查Core层命名规范
|
||||
- 业务支撑模块是否使用_core后缀
|
||||
- 通用工具模块是否不使用后缀
|
||||
- 根据模块职责判断命名正确性
|
||||
- 检查是否只包含技术实现
|
||||
|
||||
5. **检查职责分离**
|
||||
- Gateway层是否只做协议转换
|
||||
- Business层是否只包含业务逻辑
|
||||
- Core层是否只包含技术实现
|
||||
- 是否有跨层职责混乱
|
||||
|
||||
6. **🔥 检查依赖注入完整性(关键步骤)**
|
||||
- 扫描所有Injectable类的构造函数依赖
|
||||
- 检查Module的imports是否包含所有依赖的Module
|
||||
- 验证中间Module是否正确导出了需要的服务
|
||||
- 确认依赖链的完整性和可访问性
|
||||
- 识别并修复常见的依赖注入错误
|
||||
|
||||
7. **检查依赖关系**
|
||||
- Gateway层是否只依赖Business层
|
||||
- Business层是否只依赖Core层
|
||||
- Core层是否不依赖业务层
|
||||
- 依赖注入是否正确使用
|
||||
|
||||
8. **检查架构违规**
|
||||
- 识别常见的分层违规模式
|
||||
- 检查技术实现和业务逻辑的边界
|
||||
- 检查协议处理和业务逻辑的边界
|
||||
- 确保架构清晰度
|
||||
|
||||
9. **游戏服务器特殊检查**
|
||||
- WebSocket Gateway的分层正确性
|
||||
- 双模式服务的架构设计
|
||||
- 实时通信组件的职责分离
|
||||
|
||||
10. **🚀 应用启动验证(强制步骤)**
|
||||
- 执行 `pnpm dev` 或 `npm run dev` 启动应用
|
||||
- 验证应用能够成功启动,无模块依赖错误
|
||||
- 检查控制台是否有依赖注入失败的错误信息
|
||||
- 如有启动错误,必须修复后重新验证
|
||||
|
||||
## 🚀 应用启动验证(强制要求)
|
||||
|
||||
### 为什么需要启动验证?
|
||||
**静态代码检查无法发现所有的模块依赖问题!** 以下问题只有在应用启动时才会暴露:
|
||||
|
||||
1. **Module exports 配置错误**:导出了不属于当前模块的服务
|
||||
2. **依赖注入链断裂**:中间模块未正确导出依赖
|
||||
3. **循环依赖问题**:模块间存在循环引用
|
||||
4. **Provider 注册遗漏**:服务未在正确的模块中注册
|
||||
5. **CacheModule/ConfigModule 等全局模块缺失**
|
||||
|
||||
### 常见启动错误示例
|
||||
|
||||
#### 错误1:导出不属于当前模块的服务
|
||||
```
|
||||
UnknownExportException [Error]: Nest cannot export a provider/module that
|
||||
is not a part of the currently processed module (ZulipModule).
|
||||
Please verify whether the exported DynamicConfigManagerService is available
|
||||
in this particular context.
|
||||
```
|
||||
|
||||
**原因**:ZulipModule 尝试导出 DynamicConfigManagerService,但该服务来自 ZulipCoreModule,不是 ZulipModule 自己的 provider。
|
||||
|
||||
**修复方案**:
|
||||
```typescript
|
||||
// ❌ 错误:直接导出其他模块的服务
|
||||
@Module({
|
||||
imports: [ZulipCoreModule],
|
||||
exports: [DynamicConfigManagerService], // 错误!
|
||||
})
|
||||
export class ZulipModule {}
|
||||
|
||||
// ✅ 正确:导出整个模块
|
||||
@Module({
|
||||
imports: [ZulipCoreModule],
|
||||
exports: [ZulipCoreModule], // 正确:导出模块而非服务
|
||||
})
|
||||
export class ZulipModule {}
|
||||
```
|
||||
|
||||
#### 错误2:依赖注入失败
|
||||
```
|
||||
Nest can't resolve dependencies of the JwtAuthGuard (?).
|
||||
Please make sure that the argument LoginCoreService at index [0]
|
||||
is available in the ZulipGatewayModule context.
|
||||
```
|
||||
|
||||
**原因**:JwtAuthGuard 需要 LoginCoreService,但 ZulipGatewayModule 没有导入 LoginCoreModule。
|
||||
|
||||
**修复方案**:
|
||||
```typescript
|
||||
// ❌ 错误:缺少必要的模块导入
|
||||
@Module({
|
||||
imports: [ZulipModule, AuthModule],
|
||||
providers: [JwtAuthGuard],
|
||||
})
|
||||
export class ZulipGatewayModule {}
|
||||
|
||||
// ✅ 正确:添加缺失的模块导入
|
||||
@Module({
|
||||
imports: [
|
||||
ZulipModule,
|
||||
AuthModule,
|
||||
LoginCoreModule, // 添加:JwtAuthGuard 依赖 LoginCoreService
|
||||
],
|
||||
providers: [JwtAuthGuard],
|
||||
})
|
||||
export class ZulipGatewayModule {}
|
||||
```
|
||||
|
||||
#### 错误3:CACHE_MANAGER 未注册
|
||||
```
|
||||
Nest can't resolve dependencies of the SomeService (?).
|
||||
Please make sure that the argument "CACHE_MANAGER" at index [2]
|
||||
is available in the SomeModule context.
|
||||
```
|
||||
|
||||
**原因**:服务使用了 @Inject(CACHE_MANAGER),但模块未导入 CacheModule。
|
||||
|
||||
**修复方案**:
|
||||
```typescript
|
||||
// ❌ 错误:缺少 CacheModule
|
||||
@Module({
|
||||
imports: [OtherModule],
|
||||
providers: [SomeService],
|
||||
})
|
||||
export class SomeModule {}
|
||||
|
||||
// ✅ 正确:添加 CacheModule
|
||||
import { CacheModule } from '@nestjs/cache-manager';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
CacheModule.register(), // 添加缓存模块
|
||||
OtherModule,
|
||||
],
|
||||
providers: [SomeService],
|
||||
})
|
||||
export class SomeModule {}
|
||||
```
|
||||
|
||||
### 启动验证执行流程
|
||||
|
||||
```bash
|
||||
# 1. 执行启动命令
|
||||
pnpm dev
|
||||
# 或
|
||||
npm run dev
|
||||
|
||||
# 2. 观察控制台输出,检查是否有以下错误类型:
|
||||
# - UnknownExportException
|
||||
# - Nest can't resolve dependencies
|
||||
# - Circular dependency detected
|
||||
# - Module not found
|
||||
|
||||
# 3. 如果启动成功,应该看到类似输出:
|
||||
# [Nest] LOG [NestFactory] Starting Nest application...
|
||||
# [Nest] LOG [RoutesResolver] AppController {/}: +Xms
|
||||
# [Nest] LOG [NestApplication] Nest application successfully started +Xms
|
||||
|
||||
# 4. 验证健康检查接口
|
||||
curl http://localhost:3000/health
|
||||
# 应返回:{"status":"ok",...}
|
||||
```
|
||||
|
||||
### 启动验证检查清单
|
||||
|
||||
- [ ] 执行 `pnpm dev` 或 `npm run dev`
|
||||
- [ ] 确认无 UnknownExportException 错误
|
||||
- [ ] 确认无依赖注入失败错误
|
||||
- [ ] 确认无循环依赖错误
|
||||
- [ ] 确认应用成功启动并监听端口
|
||||
- [ ] 验证健康检查接口返回正常
|
||||
- [ ] 如有错误,修复后重新启动验证
|
||||
|
||||
### 🚨 启动验证失败处理
|
||||
|
||||
**如果启动验证失败,必须:**
|
||||
1. **分析错误信息**:识别具体的模块和依赖问题
|
||||
2. **定位问题模块**:找到报错的 Module 文件
|
||||
3. **修复依赖配置**:
|
||||
- 添加缺失的 imports
|
||||
- 修正错误的 exports
|
||||
- 注册缺失的 providers
|
||||
4. **重新启动验证**:修复后必须再次执行启动验证
|
||||
5. **记录修改**:更新文件头部的修改记录
|
||||
|
||||
**🔥 重要:启动验证是步骤4的强制完成条件,不能跳过!**
|
||||
|
||||
## 🔥 重要提醒
|
||||
|
||||
**如果在本步骤中执行了任何修改操作(调整分层结构、修正依赖关系、重构代码等),必须立即重新执行步骤4的完整检查!**
|
||||
|
||||
- ✅ 执行修改 → 🔥 立即重新执行步骤4 → 提供验证报告 → 等待用户确认
|
||||
- ❌ 执行修改 → 直接进入步骤5(错误做法)
|
||||
|
||||
**🚨 重要强调:纯检查步骤不更新修改记录**
|
||||
**如果检查发现架构分层已经符合规范,无需任何修改,则:**
|
||||
- ❌ **禁止添加检查记录**:不要添加"AI代码检查步骤4:架构分层检查和优化"
|
||||
- ❌ **禁止更新时间戳**:不要修改@lastModified字段
|
||||
- ❌ **禁止递增版本号**:不要修改@version字段
|
||||
- ✅ **仅提供检查报告**:说明检查结果,确认符合规范
|
||||
|
||||
**🚀 步骤4完成的强制条件:**
|
||||
1. **架构分层检查通过**:Gateway/Business/Core层职责清晰
|
||||
2. **依赖注入检查通过**:所有Module的imports/exports配置正确
|
||||
3. **🔥 应用启动验证通过**:执行 `pnpm dev` 应用能成功启动,无依赖错误
|
||||
|
||||
**不能跳过应用启动验证环节!如果启动失败,必须修复后重新执行整个步骤4!**
|
||||
706
docs/ai-reading/step5-test-coverage.md
Normal file
706
docs/ai-reading/step5-test-coverage.md
Normal file
@@ -0,0 +1,706 @@
|
||||
# 步骤5:测试覆盖检查
|
||||
|
||||
## ⚠️ 执行前必读规范
|
||||
|
||||
**🔥 重要:在执行本步骤之前,AI必须先完整阅读同级目录下的 `README.md` 文件!**
|
||||
|
||||
该README文件包含:
|
||||
- 🎯 执行前准备和用户信息收集要求
|
||||
- 🔄 强制执行原则和分步执行流程
|
||||
- 🔥 修改后立即重新执行当前步骤的强制规则
|
||||
- 📝 文件修改记录规范和版本号递增规则
|
||||
- 🧪 测试文件调试规范和测试指令使用规范
|
||||
- 🚨 全局约束和游戏服务器特殊要求
|
||||
|
||||
**不阅读README直接执行步骤将导致执行不规范,违反项目要求!**
|
||||
|
||||
---
|
||||
|
||||
## 🎯 检查目标
|
||||
检查测试文件的完整性和覆盖率,确保严格的一对一测试映射和测试分离。
|
||||
|
||||
## 📋 测试文件存在性检查
|
||||
|
||||
### 需要测试文件的类型
|
||||
```typescript
|
||||
✅ 必须有测试文件:
|
||||
- *.service.ts # Service类 - 业务逻辑类
|
||||
- *.controller.ts # Controller类 - 控制器类
|
||||
- *.gateway.ts # Gateway类 - WebSocket网关类
|
||||
- *.guard.ts # Guard类 - 守卫类(游戏服务器安全重要)
|
||||
- *.interceptor.ts # Interceptor类 - 拦截器类(日志监控重要)
|
||||
- *.middleware.ts # Middleware类 - 中间件类(性能监控重要)
|
||||
|
||||
❌ 不需要测试文件:
|
||||
- *.dto.ts # DTO类 - 数据传输对象
|
||||
- *.interface.ts # Interface文件 - 接口定义
|
||||
- *.constants.ts # Constants文件 - 常量定义
|
||||
- *.config.ts # Config文件 - 配置文件
|
||||
- *.utils.ts # 简单Utils工具类(复杂工具类需要)
|
||||
```
|
||||
|
||||
### 测试文件命名规范
|
||||
```typescript
|
||||
✅ 正确的一对一映射:
|
||||
src/business/auth/auth.service.ts
|
||||
src/business/auth/auth.service.spec.ts
|
||||
|
||||
src/core/location_broadcast_core/location_broadcast_core.service.ts
|
||||
src/core/location_broadcast_core/location_broadcast_core.service.spec.ts
|
||||
|
||||
src/business/admin/admin.gateway.ts
|
||||
src/business/admin/admin.gateway.spec.ts
|
||||
|
||||
❌ 错误的命名:
|
||||
src/business/auth/auth_services.spec.ts # 测试多个service,违反一对一原则
|
||||
src/business/auth/auth_test.spec.ts # 命名不对应
|
||||
```
|
||||
|
||||
## 🔥 严格一对一测试映射(重要)
|
||||
|
||||
### 强制要求
|
||||
- **严格对应**:每个测试文件必须严格对应一个源文件
|
||||
- **禁止多对一**:不允许一个测试文件测试多个源文件
|
||||
- **禁止一对多**:不允许一个源文件的测试分散在多个测试文件
|
||||
- **命名对应**:测试文件名必须与源文件名完全对应(除.spec.ts后缀外)
|
||||
|
||||
### 测试范围严格限制
|
||||
```typescript
|
||||
// ✅ 正确:只测试LoginService的功能
|
||||
// 文件:src/business/auth/login.service.spec.ts
|
||||
describe('LoginService', () => {
|
||||
describe('validateUser', () => {
|
||||
it('should validate user credentials', () => {
|
||||
// 只测试LoginService.validateUser方法
|
||||
// 使用Mock隔离UserRepository等外部依赖
|
||||
});
|
||||
|
||||
it('should throw error for invalid credentials', () => {
|
||||
// 测试异常情况
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateToken', () => {
|
||||
it('should generate valid JWT token', () => {
|
||||
// 只测试LoginService.generateToken方法
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ❌ 错误:在LoginService测试中测试其他服务
|
||||
describe('LoginService', () => {
|
||||
it('should integrate with UserRepository', () => {
|
||||
// 错误:这是集成测试,应该移到test/integration/
|
||||
});
|
||||
|
||||
it('should work with EmailService', () => {
|
||||
// 错误:测试了EmailService的功能,违反范围限制
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 🏗️ 测试分离架构(强制要求)
|
||||
|
||||
### 顶层test目录结构
|
||||
```
|
||||
test/
|
||||
├── integration/ # 集成测试 - 测试多个模块间的交互
|
||||
│ ├── auth_integration.spec.ts
|
||||
│ ├── location_broadcast_integration.spec.ts
|
||||
│ └── zulip_integration.spec.ts
|
||||
├── e2e/ # 端到端测试 - 完整业务流程测试
|
||||
│ ├── user_registration_e2e.spec.ts
|
||||
│ ├── location_broadcast_e2e.spec.ts
|
||||
│ └── admin_operations_e2e.spec.ts
|
||||
├── performance/ # 性能测试 - WebSocket和高并发测试
|
||||
│ ├── websocket_performance.spec.ts
|
||||
│ ├── database_performance.spec.ts
|
||||
│ └── memory_usage.spec.ts
|
||||
├── property/ # 属性测试 - 基于属性的随机测试
|
||||
│ ├── admin_property.spec.ts
|
||||
│ ├── user_validation_property.spec.ts
|
||||
│ └── position_update_property.spec.ts
|
||||
└── fixtures/ # 测试数据和工具
|
||||
├── test_data.ts
|
||||
└── test_helpers.ts
|
||||
```
|
||||
|
||||
### 测试类型分离要求
|
||||
```typescript
|
||||
// ✅ 正确:单元测试只在源文件同目录
|
||||
// 文件位置:src/business/auth/login.service.spec.ts
|
||||
describe('LoginService Unit Tests', () => {
|
||||
// 只测试LoginService的单个方法功能
|
||||
// 使用Mock隔离所有外部依赖
|
||||
});
|
||||
|
||||
// ✅ 正确:集成测试统一在test/integration/
|
||||
// 文件位置:test/integration/auth_integration.spec.ts
|
||||
describe('Auth Integration Tests', () => {
|
||||
it('should integrate LoginService with UserRepository and TokenService', () => {
|
||||
// 测试多个模块间的真实交互
|
||||
});
|
||||
});
|
||||
|
||||
// ✅ 正确:E2E测试统一在test/e2e/
|
||||
// 文件位置:test/e2e/user_auth_e2e.spec.ts
|
||||
describe('User Authentication E2E Tests', () => {
|
||||
it('should handle complete user login flow', () => {
|
||||
// 端到端完整业务流程测试
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 🎮 游戏服务器特殊测试要求
|
||||
|
||||
### WebSocket Gateway测试
|
||||
```typescript
|
||||
// ✅ 正确:完整的WebSocket测试
|
||||
// 文件:src/business/location/location_broadcast.gateway.spec.ts
|
||||
describe('LocationBroadcastGateway', () => {
|
||||
let gateway: LocationBroadcastGateway;
|
||||
let mockServer: jest.Mocked<Server>;
|
||||
|
||||
beforeEach(async () => {
|
||||
// 设置Mock服务器和依赖
|
||||
});
|
||||
|
||||
describe('handleConnection', () => {
|
||||
it('should accept valid WebSocket connection with JWT token', () => {
|
||||
// 正常连接测试
|
||||
});
|
||||
|
||||
it('should reject connection with invalid JWT token', () => {
|
||||
// 异常连接测试
|
||||
});
|
||||
|
||||
it('should handle connection when room is at capacity limit', () => {
|
||||
// 边界情况测试
|
||||
});
|
||||
});
|
||||
|
||||
describe('handlePositionUpdate', () => {
|
||||
it('should broadcast position to all room members', () => {
|
||||
// 实时通信测试
|
||||
});
|
||||
|
||||
it('should validate position data format', () => {
|
||||
// 数据验证测试
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleDisconnect', () => {
|
||||
it('should clean up user resources on disconnect', () => {
|
||||
// 断开连接测试
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 双模式服务测试
|
||||
```typescript
|
||||
// ✅ 正确:内存服务测试
|
||||
// 文件:src/core/users/users_memory.service.spec.ts
|
||||
describe('UsersMemoryService', () => {
|
||||
it('should create user in memory storage', () => {
|
||||
// 测试内存模式特定功能
|
||||
});
|
||||
|
||||
it('should handle concurrent access correctly', () => {
|
||||
// 测试内存模式并发处理
|
||||
});
|
||||
});
|
||||
|
||||
// ✅ 正确:数据库服务测试
|
||||
// 文件:src/core/users/users_database.service.spec.ts
|
||||
describe('UsersDatabaseService', () => {
|
||||
it('should create user in database', () => {
|
||||
// 测试数据库模式特定功能
|
||||
});
|
||||
|
||||
it('should handle database transaction correctly', () => {
|
||||
// 测试数据库事务处理
|
||||
});
|
||||
});
|
||||
|
||||
// ✅ 正确:双模式一致性测试(集成测试)
|
||||
// 文件:test/integration/users_dual_mode_integration.spec.ts
|
||||
describe('Users Dual Mode Integration', () => {
|
||||
it('should have identical behavior for user creation', () => {
|
||||
// 测试两种模式行为一致性
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 属性测试(管理员模块)
|
||||
```typescript
|
||||
// ✅ 正确:属性测试
|
||||
// 文件:test/property/admin_property.spec.ts
|
||||
import * as fc from 'fast-check';
|
||||
|
||||
describe('AdminService Properties', () => {
|
||||
it('should handle any valid user status update', () => {
|
||||
fc.assert(fc.property(
|
||||
fc.integer({ min: 1, max: 1000000 }), // userId
|
||||
fc.constantFrom(...Object.values(UserStatus)), // status
|
||||
async (userId, status) => {
|
||||
try {
|
||||
const result = await adminService.updateUserStatus(userId, status);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.status).toBe(status);
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(NotFoundException || BadRequestException);
|
||||
}
|
||||
}
|
||||
));
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 📍 测试文件位置规范
|
||||
|
||||
### 正确位置
|
||||
```
|
||||
✅ 正确:测试文件与源文件同目录
|
||||
src/business/auth/
|
||||
├── auth.service.ts
|
||||
├── auth.service.spec.ts # 单元测试
|
||||
├── auth.controller.ts
|
||||
└── auth.controller.spec.ts # 单元测试
|
||||
|
||||
src/core/location_broadcast_core/
|
||||
├── location_broadcast_core.service.ts
|
||||
└── location_broadcast_core.service.spec.ts
|
||||
```
|
||||
|
||||
### 错误位置(必须修正)
|
||||
```
|
||||
❌ 错误:测试文件在单独文件夹
|
||||
src/business/auth/
|
||||
├── auth.service.ts
|
||||
├── auth.controller.ts
|
||||
└── tests/ # 错误:单独的测试文件夹
|
||||
├── auth.service.spec.ts # 应该移到上级目录
|
||||
└── auth.controller.spec.ts
|
||||
|
||||
src/business/auth/
|
||||
├── auth.service.ts
|
||||
├── auth.controller.ts
|
||||
└── __tests__/ # 错误:单独的测试文件夹
|
||||
└── auth.spec.ts # 应该拆分并移到上级目录
|
||||
```
|
||||
|
||||
## 🧪 测试执行验证(强制要求)
|
||||
|
||||
### 测试命令执行
|
||||
```bash
|
||||
# 单元测试(严格限制:只执行.spec.ts文件)
|
||||
npm run test:unit
|
||||
# 等价于: jest --testPathPattern=spec.ts --testPathIgnorePatterns="integration|e2e|performance|property"
|
||||
|
||||
# 集成测试(统一在test/integration/目录执行)
|
||||
npm run test:integration
|
||||
# 等价于: jest test/integration/
|
||||
|
||||
# E2E测试(统一在test/e2e/目录执行)
|
||||
npm run test:e2e
|
||||
# 等价于: jest test/e2e/
|
||||
|
||||
# 属性测试(统一在test/property/目录执行)
|
||||
npm run test:property
|
||||
# 等价于: jest test/property/
|
||||
|
||||
# 性能测试(统一在test/performance/目录执行)
|
||||
npm run test:performance
|
||||
# 等价于: jest test/performance/
|
||||
|
||||
# 🔥 特定文件或目录测试(步骤5专用指令)
|
||||
pnpm test (文件夹或者文件的相对地址)
|
||||
# 示例:
|
||||
pnpm test src/core/zulip_core # 测试整个zulip_core模块
|
||||
pnpm test src/core/zulip_core/services # 测试services目录
|
||||
pnpm test src/core/zulip_core/services/config_manager.service.spec.ts # 测试单个文件
|
||||
pnpm test test/integration/zulip_integration.spec.ts # 测试集成测试文件
|
||||
```
|
||||
|
||||
### 🔥 强制测试执行要求(重要)
|
||||
|
||||
**步骤5完成前必须确保所有检查范围内的测试通过**
|
||||
|
||||
#### 测试执行验证流程
|
||||
1. **识别检查范围**:确定当前检查涉及的所有模块和文件
|
||||
2. **执行范围内测试**:运行所有相关的单元测试、集成测试
|
||||
3. **修复测试失败**:解决所有测试失败问题(类型错误、逻辑错误等)
|
||||
4. **验证测试通过**:确保所有测试都能成功执行
|
||||
5. **提供测试报告**:展示测试执行结果和覆盖率
|
||||
|
||||
#### 测试失败处理原则
|
||||
```bash
|
||||
# 🔥 如果发现测试失败,必须修复后才能完成步骤5
|
||||
|
||||
# 1. 运行特定模块测试(推荐使用pnpm test指令)
|
||||
pnpm test src/core/zulip_core # 测试整个模块
|
||||
pnpm test src/core/zulip_core/services # 测试services目录
|
||||
pnpm test src/core/zulip_core/services/config_manager.service.spec.ts # 测试单个文件
|
||||
|
||||
# 2. 分析失败原因
|
||||
# - 类型错误:修正TypeScript类型定义
|
||||
# - 接口不匹配:更新接口或Mock对象
|
||||
# - 逻辑错误:修正业务逻辑实现
|
||||
# - 依赖问题:更新依赖注入或Mock配置
|
||||
|
||||
# 3. 修复后重新运行测试
|
||||
pnpm test src/core/zulip_core # 重新测试修复后的模块
|
||||
|
||||
# 4. 确保所有测试通过后才完成步骤5
|
||||
```
|
||||
|
||||
#### 测试执行成功标准
|
||||
- ✅ **零失败测试**:所有相关测试必须通过(0 failed)
|
||||
- ✅ **零错误测试**:所有测试套件必须成功运行(0 error)
|
||||
- ✅ **完整覆盖**:所有检查范围内的文件都有测试执行
|
||||
- ✅ **类型安全**:无TypeScript编译错误
|
||||
- ✅ **依赖正确**:所有Mock和依赖注入正确配置
|
||||
|
||||
#### 测试执行报告模板
|
||||
```
|
||||
## 测试执行验证报告
|
||||
|
||||
### 🧪 测试执行结果
|
||||
- 执行命令:pnpm test src/core/zulip_core
|
||||
- 测试套件:X passed, 0 failed
|
||||
- 测试用例:X passed, 0 failed
|
||||
- 覆盖率:X% statements, X% branches, X% functions, X% lines
|
||||
|
||||
### 🔧 修复的问题
|
||||
- 类型错误修复:[具体修复内容]
|
||||
- 接口更新:[具体更新内容]
|
||||
- Mock配置:[具体配置内容]
|
||||
|
||||
### ✅ 验证状态
|
||||
- 所有测试通过 ✓
|
||||
- 无编译错误 ✓
|
||||
- 依赖注入正确 ✓
|
||||
- Mock配置完整 ✓
|
||||
|
||||
**测试执行验证完成,可以进行下一步骤**
|
||||
```
|
||||
|
||||
### 测试执行顺序
|
||||
1. **第一阶段**:单元测试(快速反馈)
|
||||
2. **第二阶段**:集成测试(模块协作)
|
||||
3. **第三阶段**:E2E测试(业务流程)
|
||||
4. **第四阶段**:性能测试(系统性能)
|
||||
|
||||
### 🚨 测试执行失败处理
|
||||
如果在测试执行过程中发现失败,必须:
|
||||
1. **立即停止步骤5进程**
|
||||
2. **分析并修复所有测试失败**
|
||||
3. **重新执行完整的步骤5检查**
|
||||
4. **确保所有测试通过后才能进入步骤6**
|
||||
|
||||
## 🔍 检查执行步骤
|
||||
|
||||
1. **扫描需要测试的文件类型**
|
||||
- 识别所有.service.ts、.controller.ts、.gateway.ts等文件
|
||||
- 检查是否有对应的.spec.ts测试文件
|
||||
|
||||
2. **验证一对一测试映射**
|
||||
- 确保每个测试文件严格对应一个源文件
|
||||
- 检查测试文件命名是否正确对应
|
||||
|
||||
3. **检查测试范围限制**
|
||||
- 确保测试内容严格限于对应源文件功能
|
||||
- 识别跨文件测试和混合测试
|
||||
|
||||
4. **检查测试文件位置**
|
||||
- 确保单元测试与源文件在同一目录
|
||||
- 识别需要扁平化的测试文件夹
|
||||
|
||||
5. **分离集成测试和E2E测试**
|
||||
- 将集成测试移动到test/integration/
|
||||
- 将E2E测试移动到test/e2e/
|
||||
- 将性能测试移动到test/performance/
|
||||
- 将属性测试移动到test/property/
|
||||
|
||||
6. **游戏服务器特殊检查**
|
||||
- WebSocket Gateway的完整测试覆盖
|
||||
- 双模式服务的一致性测试
|
||||
- 属性测试的正确实现
|
||||
|
||||
7. **🔥 强制执行测试验证(关键步骤)**
|
||||
- 运行检查范围内的所有相关测试
|
||||
- 修复所有测试失败问题
|
||||
- 确保测试覆盖率达标
|
||||
- 验证测试质量和有效性
|
||||
- **只有所有测试通过才能完成步骤5**
|
||||
|
||||
## 🔥 重要提醒
|
||||
|
||||
**如果在本步骤中执行了任何修改操作(创建测试文件、移动测试文件、修正测试内容、修复测试失败等),必须立即重新执行步骤5的完整检查!**
|
||||
|
||||
- ✅ 执行修改 → 🔥 立即重新执行步骤5 → 🧪 强制执行测试验证 → 提供验证报告 → 等待用户确认
|
||||
- ❌ 执行修改 → 直接进入步骤6(错误做法)
|
||||
|
||||
**🚨 重要强调:纯检查步骤不更新修改记录**
|
||||
**如果检查发现测试覆盖已经符合规范,无需任何修改,则:**
|
||||
- ❌ **禁止添加检查记录**:不要添加"AI代码检查步骤5:测试覆盖检查和优化"
|
||||
- ❌ **禁止更新时间戳**:不要修改@lastModified字段
|
||||
- ❌ **禁止递增版本号**:不要修改@version字段
|
||||
- ✅ **仅提供检查报告**:说明检查结果,确认符合规范
|
||||
|
||||
**🚨 步骤5完成的强制条件:**
|
||||
1. **测试文件完整性检查通过**
|
||||
2. **测试映射关系检查通过**
|
||||
3. **测试分离架构检查通过**
|
||||
4. **🔥 所有检查范围内的测试必须执行成功(零失败)**
|
||||
|
||||
**不能跳过测试执行验证环节!如果测试失败,必须修复后重新执行整个步骤5!**
|
||||
|
||||
---
|
||||
|
||||
## ✅ zulip_core模块步骤5检查完成报告
|
||||
|
||||
### 📋 检查范围
|
||||
- **模块**:src/core/zulip_core
|
||||
- **检查日期**:2026-01-12
|
||||
- **检查人员**:moyin
|
||||
|
||||
### 🧪 测试执行验证结果
|
||||
|
||||
#### 执行命令
|
||||
```bash
|
||||
npx jest src/core/zulip_core --testTimeout=15000
|
||||
```
|
||||
|
||||
#### 测试结果统计
|
||||
- **测试套件**:11 passed, 0 failed
|
||||
- **测试用例**:367 passed, 0 failed
|
||||
- **执行时间**:11.841s
|
||||
- **覆盖状态**:✅ 完整覆盖
|
||||
|
||||
#### 修复的关键问题
|
||||
1. **DynamicConfigManagerService测试失败修复**:
|
||||
- 修正了Zulip凭据初始化顺序问题
|
||||
- 修复了Mock配置的fs.existsSync行为
|
||||
- 解决了环境变量设置时机问题
|
||||
- 修正了测试用例的预期错误消息
|
||||
|
||||
2. **测试文件完整性验证**:
|
||||
- 确认所有service文件都有对应的.spec.ts测试文件
|
||||
- 验证了严格的一对一测试映射关系
|
||||
- 检查了测试文件位置的正确性
|
||||
|
||||
### 📊 测试覆盖详情
|
||||
|
||||
#### 通过的测试套件
|
||||
1. ✅ api_key_security.service.spec.ts (53 tests)
|
||||
2. ✅ config_manager.service.spec.ts (45 tests)
|
||||
3. ✅ dynamic_config_manager.service.spec.ts (32 tests)
|
||||
4. ✅ monitoring.service.spec.ts (15 tests)
|
||||
5. ✅ stream_initializer.service.spec.ts (11 tests)
|
||||
6. ✅ user_management.service.spec.ts (16 tests)
|
||||
7. ✅ user_registration.service.spec.ts (9 tests)
|
||||
8. ✅ zulip_account.service.spec.ts (26 tests)
|
||||
9. ✅ zulip_client.service.spec.ts (19 tests)
|
||||
10. ✅ zulip_client_pool.service.spec.ts (23 tests)
|
||||
11. ✅ zulip_core.module.spec.ts (118 tests)
|
||||
|
||||
#### 测试质量验证
|
||||
- **单元测试隔离**:✅ 所有测试使用Mock隔离外部依赖
|
||||
- **测试范围限制**:✅ 每个测试文件严格测试对应的单个服务
|
||||
- **错误处理覆盖**:✅ 包含完整的异常情况测试
|
||||
- **边界条件测试**:✅ 覆盖各种边界和异常场景
|
||||
|
||||
### 🔧 修改记录
|
||||
|
||||
#### 文件修改详情
|
||||
- **修改文件**:src/core/zulip_core/services/dynamic_config_manager.service.spec.ts
|
||||
- **修改时间**:2026-01-12
|
||||
- **修改人员**:moyin
|
||||
- **修改内容**:
|
||||
- 修正了beforeEach中环境变量设置顺序
|
||||
- 修复了无凭据测试的服务实例创建
|
||||
- 修正了fs.existsSync的Mock行为
|
||||
- 更新了错误消息的预期值
|
||||
|
||||
### ✅ 验证状态确认
|
||||
|
||||
- **测试文件完整性**:✅ 通过
|
||||
- **一对一测试映射**:✅ 通过
|
||||
- **测试分离架构**:✅ 通过
|
||||
- **测试执行验证**:✅ 通过(0失败,367通过)
|
||||
- **类型安全检查**:✅ 通过
|
||||
- **依赖注入配置**:✅ 通过
|
||||
|
||||
### 🎯 步骤5完成确认
|
||||
|
||||
**zulip_core模块的步骤5测试覆盖检查已完成,所有强制条件均已满足:**
|
||||
|
||||
1. ✅ 测试文件完整性检查通过
|
||||
2. ✅ 测试映射关系检查通过
|
||||
3. ✅ 测试分离架构检查通过
|
||||
4. ✅ 所有测试执行成功(零失败)
|
||||
|
||||
**可以进入下一步骤的开发工作。**
|
||||
|
||||
---
|
||||
|
||||
## ✅ Zulip模块完整步骤5检查完成报告
|
||||
|
||||
### 📋 检查范围
|
||||
- **模块**:Zulip相关所有模块
|
||||
- src/core/zulip_core (12个源文件)
|
||||
- src/core/db/zulip_accounts (5个源文件)
|
||||
- src/business/zulip (13个源文件)
|
||||
- **检查日期**:2026-01-12
|
||||
- **检查人员**:moyin
|
||||
|
||||
### 🧪 测试执行验证结果
|
||||
|
||||
#### 最终测试状态
|
||||
- **总测试套件**:30个
|
||||
- **通过测试套件**:30个 ✅
|
||||
- **失败测试套件**:0个 ✅
|
||||
- **总测试用例**:907个
|
||||
- **通过测试用例**:907个 ✅
|
||||
- **失败测试用例**:0个 ✅
|
||||
|
||||
#### 执行的测试命令
|
||||
```bash
|
||||
# 核心模块测试
|
||||
pnpm test src/core/zulip_core
|
||||
# 结果:12个测试套件通过,394个测试通过
|
||||
|
||||
# 数据库模块测试
|
||||
pnpm test src/core/db/zulip_accounts
|
||||
# 结果:5个测试套件通过,156个测试通过
|
||||
|
||||
# 业务模块测试
|
||||
pnpm test src/business/zulip
|
||||
# 结果:13个测试套件通过,357个测试通过
|
||||
```
|
||||
|
||||
### 🔧 修复的测试问题
|
||||
|
||||
#### 1. chat.controller.spec.ts
|
||||
- **问题**:错误处理测试期望HttpException但收到Error
|
||||
- **修复**:修改mock实现抛出HttpException而不是Error
|
||||
- **状态**:✅ 已修复
|
||||
- **修改记录**:已更新文件头部修改记录
|
||||
|
||||
#### 2. zulip.service.spec.ts
|
||||
- **问题**:消息内容断言失败,实际内容包含额外的游戏消息ID
|
||||
- **修复**:使用expect.stringContaining()匹配包含原始内容的字符串
|
||||
- **状态**:✅ 已修复
|
||||
- **修改记录**:已更新文件头部修改记录
|
||||
|
||||
#### 3. zulip_accounts.controller.spec.ts
|
||||
- **问题**:日志记录测试中多次调用的参数期望不匹配
|
||||
- **修复**:使用toHaveBeenNthCalledWith()精确匹配特定调用的参数
|
||||
- **状态**:✅ 已修复
|
||||
- **修改记录**:已更新文件头部修改记录
|
||||
|
||||
### 📊 测试覆盖详情
|
||||
|
||||
#### 核心模块 (src/core/zulip_core)
|
||||
✅ **完整覆盖** - 所有12个源文件都有对应的测试文件
|
||||
- api_key_security.service.spec.ts
|
||||
- config_manager.service.spec.ts
|
||||
- dynamic_config_manager.service.spec.ts
|
||||
- monitoring.service.spec.ts
|
||||
- stream_initializer.service.spec.ts
|
||||
- user_management.service.spec.ts
|
||||
- user_registration.service.spec.ts
|
||||
- zulip_account.service.spec.ts
|
||||
- zulip_client.service.spec.ts
|
||||
- zulip_client_pool.service.spec.ts
|
||||
- zulip_core.module.spec.ts
|
||||
- zulip_event_queue.service.spec.ts
|
||||
|
||||
#### 数据库模块 (src/core/db/zulip_accounts)
|
||||
✅ **完整覆盖** - 所有5个源文件都有对应的测试文件
|
||||
- zulip_accounts.repository.spec.ts
|
||||
- zulip_accounts_memory.repository.spec.ts
|
||||
- zulip_accounts.entity.spec.ts
|
||||
- zulip_accounts.module.spec.ts
|
||||
- zulip_accounts.service.spec.ts
|
||||
|
||||
#### 业务模块 (src/business/zulip)
|
||||
✅ **完整覆盖** - 所有13个源文件都有对应的测试文件
|
||||
- chat.controller.spec.ts
|
||||
- clean_websocket.gateway.spec.ts
|
||||
- dynamic_config.controller.spec.ts
|
||||
- websocket_docs.controller.spec.ts
|
||||
- websocket_openapi.controller.spec.ts
|
||||
- websocket_test.controller.spec.ts
|
||||
- zulip.service.spec.ts
|
||||
- zulip_accounts.controller.spec.ts
|
||||
- services/message_filter.service.spec.ts
|
||||
- services/session_cleanup.service.spec.ts
|
||||
- services/session_manager.service.spec.ts
|
||||
- services/zulip_accounts_business.service.spec.ts
|
||||
- services/zulip_event_processor.service.spec.ts
|
||||
|
||||
### 🎯 测试质量验证
|
||||
|
||||
#### 功能覆盖率
|
||||
- **登录流程**: ✅ 完整覆盖(包括属性测试)
|
||||
- **消息发送**: ✅ 完整覆盖(包括属性测试)
|
||||
- **位置更新**: ✅ 完整覆盖(包括属性测试)
|
||||
- **会话管理**: ✅ 完整覆盖
|
||||
- **配置管理**: ✅ 完整覆盖
|
||||
- **错误处理**: ✅ 完整覆盖
|
||||
- **WebSocket集成**: ✅ 完整覆盖
|
||||
- **数据库操作**: ✅ 完整覆盖
|
||||
|
||||
#### 属性测试覆盖
|
||||
- **Property 1**: 玩家登录流程完整性 ✅
|
||||
- **Property 3**: 消息发送流程完整性 ✅
|
||||
- **Property 6**: 位置更新和上下文注入 ✅
|
||||
- **Property 7**: 内容安全和频率控制 ✅
|
||||
|
||||
#### 测试架构验证
|
||||
- **单元测试隔离**: ✅ 所有测试使用Mock隔离外部依赖
|
||||
- **一对一测试映射**: ✅ 每个测试文件严格对应一个源文件
|
||||
- **测试范围限制**: ✅ 测试内容严格限于对应源文件功能
|
||||
- **错误处理覆盖**: ✅ 包含完整的异常情况测试
|
||||
- **边界条件测试**: ✅ 覆盖各种边界和异常场景
|
||||
|
||||
### 🔧 修改文件记录
|
||||
|
||||
#### 修改的测试文件
|
||||
1. **src/business/zulip/chat.controller.spec.ts**
|
||||
- 修改时间:2026-01-12
|
||||
- 修改人员:moyin
|
||||
- 修改内容:修复错误处理测试中的异常类型期望
|
||||
|
||||
2. **src/business/zulip/zulip.service.spec.ts**
|
||||
- 修改时间:2026-01-12
|
||||
- 修改人员:moyin
|
||||
- 修改内容:修复消息内容断言,使用stringContaining匹配
|
||||
|
||||
3. **src/business/zulip/zulip_accounts.controller.spec.ts**
|
||||
- 修改时间:2026-01-12
|
||||
- 修改人员:moyin
|
||||
- 修改内容:修复日志记录测试的参数期望
|
||||
|
||||
### ✅ 最终验证状态确认
|
||||
|
||||
- **测试文件完整性**:✅ 通过(30/30文件有测试)
|
||||
- **一对一测试映射**:✅ 通过(严格对应关系)
|
||||
- **测试分离架构**:✅ 通过(单元测试在源文件同目录)
|
||||
- **测试执行验证**:✅ 通过(907个测试全部通过,0失败)
|
||||
- **类型安全检查**:✅ 通过(无TypeScript编译错误)
|
||||
- **依赖注入配置**:✅ 通过(Mock配置正确)
|
||||
|
||||
### 🎯 步骤5完成确认
|
||||
|
||||
**Zulip模块的步骤5测试覆盖检查已完成,所有强制条件均已满足:**
|
||||
|
||||
1. ✅ 测试文件完整性检查通过(100%覆盖率)
|
||||
2. ✅ 测试映射关系检查通过(严格一对一映射)
|
||||
3. ✅ 测试分离架构检查通过(单元测试正确位置)
|
||||
4. ✅ 所有测试执行成功(907个测试通过,0失败)
|
||||
|
||||
**🎉 Zulip模块具备完整的测试覆盖率和高质量的测试代码,可以进入下一步骤的开发工作。**
|
||||
350
docs/ai-reading/step6-documentation.md
Normal file
350
docs/ai-reading/step6-documentation.md
Normal file
@@ -0,0 +1,350 @@
|
||||
# 步骤6:功能文档生成
|
||||
|
||||
## ⚠️ 执行前必读规范
|
||||
|
||||
**🔥 重要:在执行本步骤之前,AI必须先完整阅读同级目录下的 `README.md` 文件!**
|
||||
|
||||
该README文件包含:
|
||||
- 🎯 执行前准备和用户信息收集要求
|
||||
- 🔄 强制执行原则和分步执行流程
|
||||
- 🔥 修改后立即重新执行当前步骤的强制规则
|
||||
- 📝 文件修改记录规范和版本号递增规则
|
||||
- 🧪 测试文件调试规范和测试指令使用规范
|
||||
- 🚨 全局约束和游戏服务器特殊要求
|
||||
|
||||
**不阅读README直接执行步骤将导致执行不规范,违反项目要求!**
|
||||
|
||||
---
|
||||
|
||||
## 🎯 检查目标
|
||||
生成和维护功能模块的README文档,确保文档内容完整、准确、实用。
|
||||
|
||||
## 📚 README文档结构
|
||||
|
||||
### 必须包含的章节
|
||||
每个功能模块文件夹都必须有README.md文档,包含以下结构:
|
||||
|
||||
```markdown
|
||||
# [模块名称] [中文描述]
|
||||
|
||||
[模块名称] 是 [一段话总结文件夹的整体功能和作用,说明其在项目中的定位和价值]。
|
||||
|
||||
## 对外提供的接口
|
||||
|
||||
### create()
|
||||
创建新用户记录,支持数据验证和唯一性检查。
|
||||
|
||||
### findByEmail()
|
||||
根据邮箱地址查询用户,用于登录验证和账户找回。
|
||||
|
||||
## 对外API接口(如适用)
|
||||
|
||||
### POST /api/auth/login
|
||||
用户登录接口,支持用户名/邮箱/手机号多种方式登录。
|
||||
|
||||
### GET /api/users/:id
|
||||
根据用户ID获取用户详细信息。
|
||||
|
||||
## WebSocket事件接口(如适用)
|
||||
|
||||
### 'connection'
|
||||
客户端建立WebSocket连接,需要提供JWT认证token。
|
||||
|
||||
### 'position_update'
|
||||
接收客户端位置更新,广播给房间内其他用户。
|
||||
|
||||
## 使用的项目内部依赖
|
||||
|
||||
### UserStatus (来自 business/user-mgmt/enums/user-status.enum)
|
||||
用户状态枚举,定义用户的激活、禁用、待验证等状态值。
|
||||
|
||||
## 核心特性
|
||||
|
||||
### 双存储模式支持
|
||||
- 数据库模式:使用TypeORM连接MySQL,适用于生产环境
|
||||
- 内存模式:使用Map存储,适用于开发测试和故障降级
|
||||
|
||||
## 潜在风险
|
||||
|
||||
### 内存模式数据丢失风险
|
||||
- 内存存储在应用重启后数据会丢失
|
||||
- 建议仅在开发测试环境使用
|
||||
```
|
||||
|
||||
## 🔌 对外接口文档
|
||||
|
||||
### 公共方法描述
|
||||
每个公共方法必须有一句话功能说明:
|
||||
|
||||
```markdown
|
||||
## 对外提供的接口
|
||||
|
||||
### create(userData: CreateUserDto): Promise<User>
|
||||
创建新用户记录,支持数据验证和唯一性检查。
|
||||
|
||||
### findById(id: string): Promise<User>
|
||||
根据用户ID查询用户信息,用于身份验证和数据获取。
|
||||
|
||||
### updateStatus(id: string, status: UserStatus): Promise<User>
|
||||
更新用户状态,支持激活、禁用、待验证等状态切换。
|
||||
|
||||
### delete(id: string): Promise<void>
|
||||
删除用户记录及相关数据,执行软删除保留审计信息。
|
||||
|
||||
### findByEmail(email: string): Promise<User>
|
||||
根据邮箱地址查询用户,用于登录验证和账户找回。
|
||||
```
|
||||
|
||||
## 🌐 API接口文档(Business模块)
|
||||
|
||||
### HTTP API接口
|
||||
如果business模块开放了可访问的API,必须列出所有API:
|
||||
|
||||
```markdown
|
||||
## 对外API接口
|
||||
|
||||
### POST /api/auth/login
|
||||
用户登录接口,支持用户名/邮箱/手机号多种方式登录。
|
||||
|
||||
### GET /api/users/:id
|
||||
根据用户ID获取用户详细信息。
|
||||
|
||||
### PUT /api/users/:id/status
|
||||
更新指定用户的状态(激活/禁用/待验证)。
|
||||
|
||||
### DELETE /api/users/:id
|
||||
删除指定用户账户及相关数据。
|
||||
|
||||
### GET /api/users/search
|
||||
根据条件搜索用户,支持邮箱、用户名、状态等筛选。
|
||||
|
||||
### POST /api/users/batch
|
||||
批量创建用户,支持Excel导入和数据验证。
|
||||
```
|
||||
|
||||
## 🔌 WebSocket接口文档(Gateway模块)
|
||||
|
||||
### WebSocket事件接口
|
||||
Gateway模块需要详细的WebSocket事件文档:
|
||||
|
||||
```markdown
|
||||
## WebSocket事件接口
|
||||
|
||||
### 'connection'
|
||||
客户端建立WebSocket连接,需要提供JWT认证token。
|
||||
- 输入: `{ token: string }`
|
||||
- 输出: 连接成功确认
|
||||
|
||||
### 'position_update'
|
||||
接收客户端位置更新,广播给房间内其他用户。
|
||||
- 输入: `{ x: number, y: number, timestamp: number }`
|
||||
- 输出: 广播给房间成员
|
||||
|
||||
### 'join_room'
|
||||
用户加入游戏房间,建立实时通信连接。
|
||||
- 输入: `{ roomId: string }`
|
||||
- 输出: `{ success: boolean, members: string[] }`
|
||||
|
||||
### 'chat_message'
|
||||
处理聊天消息,支持Zulip集成和消息过滤。
|
||||
- 输入: `{ message: string, roomId: string }`
|
||||
- 输出: 广播给房间成员或转发到Zulip
|
||||
|
||||
### 'disconnect'
|
||||
客户端断开连接,清理相关资源和通知其他用户。
|
||||
- 输入: 无
|
||||
- 输出: 通知房间其他成员
|
||||
```
|
||||
|
||||
## 🔗 内部依赖分析
|
||||
|
||||
### 依赖列表格式
|
||||
```markdown
|
||||
## 使用的项目内部依赖
|
||||
|
||||
### UserStatus (来自 business/user-mgmt/enums/user-status.enum)
|
||||
用户状态枚举,定义用户的激活、禁用、待验证等状态值。
|
||||
|
||||
### CreateUserDto (本模块)
|
||||
用户创建数据传输对象,提供完整的数据验证规则和类型定义。
|
||||
|
||||
### LoggerService (来自 core/utils/logger)
|
||||
日志服务,用于记录用户操作和系统事件。
|
||||
|
||||
### CacheService (来自 core/redis)
|
||||
缓存服务,用于提升用户查询性能和会话管理。
|
||||
|
||||
### EmailService (来自 core/utils/email)
|
||||
邮件服务,用于发送用户注册验证和通知邮件。
|
||||
```
|
||||
|
||||
## ⭐ 核心特性识别
|
||||
|
||||
### 技术特性
|
||||
```markdown
|
||||
## 核心特性
|
||||
|
||||
### 双存储模式支持
|
||||
- 数据库模式:使用TypeORM连接MySQL,适用于生产环境
|
||||
- 内存模式:使用Map存储,适用于开发测试和故障降级
|
||||
- 动态模块配置:通过UsersModule.forDatabase()和forMemory()灵活切换
|
||||
- 自动检测:根据环境变量自动选择存储模式
|
||||
|
||||
### 实时通信能力
|
||||
- WebSocket支持:基于Socket.IO的实时双向通信
|
||||
- 房间管理:支持用户加入/离开游戏房间
|
||||
- 位置广播:实时广播用户位置更新给房间成员
|
||||
- 连接管理:自动处理连接断开和重连机制
|
||||
|
||||
### 数据完整性保障
|
||||
- 唯一性约束检查:用户名、邮箱、手机号、GitHub ID
|
||||
- 数据验证:使用class-validator进行输入验证
|
||||
- 事务支持:批量操作支持回滚机制
|
||||
- 双模式一致性:确保内存模式和数据库模式行为一致
|
||||
|
||||
### 性能优化与监控
|
||||
- 查询优化:使用索引和查询缓存
|
||||
- 批量操作:支持批量创建和更新
|
||||
- 内存缓存:热点数据缓存机制
|
||||
- 性能监控:WebSocket连接数、消息处理延迟等指标
|
||||
- 属性测试:使用fast-check进行随机化测试
|
||||
```
|
||||
|
||||
## ⚠️ 潜在风险评估
|
||||
|
||||
### 风险分类和描述
|
||||
```markdown
|
||||
## 潜在风险
|
||||
|
||||
### 内存模式数据丢失风险
|
||||
- 内存存储在应用重启后数据会丢失
|
||||
- 不适用于生产环境的持久化需求
|
||||
- 建议仅在开发测试环境使用
|
||||
- 缓解措施:提供数据导出/导入功能
|
||||
|
||||
### WebSocket连接管理风险
|
||||
- 大量并发连接可能导致内存泄漏
|
||||
- 网络不稳定时连接频繁断开重连
|
||||
- 房间成员过多时广播性能下降
|
||||
- 缓解措施:连接数限制、心跳检测、分片广播
|
||||
|
||||
### 实时通信性能风险
|
||||
- 高频位置更新可能导致服务器压力
|
||||
- 消息广播延迟影响游戏体验
|
||||
- WebSocket消息丢失或重复
|
||||
- 缓解措施:消息限流、优先级队列、消息确认机制
|
||||
|
||||
### 双模式一致性风险
|
||||
- 内存模式和数据库模式行为可能不一致
|
||||
- 模式切换时数据同步问题
|
||||
- 测试覆盖不完整导致隐藏差异
|
||||
- 缓解措施:统一接口抽象、完整的对比测试
|
||||
|
||||
### 安全风险
|
||||
- WebSocket连接缺少足够的认证验证
|
||||
- 用户位置信息泄露风险
|
||||
- 管理员权限过度集中
|
||||
- 缓解措施:JWT认证、数据脱敏、权限细分
|
||||
```
|
||||
|
||||
## 🎮 游戏服务器特殊文档要求
|
||||
|
||||
### 实时通信协议说明
|
||||
```markdown
|
||||
### 实时通信协议
|
||||
- 协议类型:WebSocket (Socket.IO)
|
||||
- 认证方式:JWT Token验证
|
||||
- 心跳间隔:10秒
|
||||
- 超时设置:30秒无响应自动断开
|
||||
- 重连策略:指数退避,最大重试5次
|
||||
```
|
||||
|
||||
### 双模式切换指南
|
||||
```markdown
|
||||
### 双模式切换指南
|
||||
- 环境变量:`STORAGE_MODE=database|memory`
|
||||
- 切换命令:`npm run switch:database` 或 `npm run switch:memory`
|
||||
- 数据迁移:提供内存到数据库的数据导出/导入工具
|
||||
- 性能对比:内存模式响应时间<1ms,数据库模式<10ms
|
||||
```
|
||||
|
||||
### 属性测试策略说明
|
||||
```markdown
|
||||
### 属性测试策略
|
||||
- 测试框架:fast-check
|
||||
- 测试范围:管理员操作、用户状态变更、权限验证
|
||||
- 随机化参数:用户ID(1-1000000)、状态枚举、权限级别
|
||||
- 执行次数:每个属性测试运行1000次随机用例
|
||||
- 失败处理:自动收集失败用例,生成最小化复现案例
|
||||
```
|
||||
|
||||
## 📝 文档质量标准
|
||||
|
||||
### 内容质量要求
|
||||
- **准确性**:所有信息必须与代码实现一致
|
||||
- **完整性**:覆盖所有公共接口和重要功能
|
||||
- **简洁性**:每个说明控制在一句话内,突出核心要点
|
||||
- **实用性**:提供对开发者有价值的信息和建议
|
||||
|
||||
### 语言表达规范
|
||||
- 使用中文进行描述,专业术语可保留英文
|
||||
- 语言简洁明了,避免冗长的句子
|
||||
- 统一术语使用,保持前后一致
|
||||
- 避免主观评价,客观描述功能和特性
|
||||
|
||||
## 🔍 检查执行步骤
|
||||
|
||||
1. **检查README文件存在性**
|
||||
- 确保每个功能模块文件夹都有README.md
|
||||
- 检查文档结构是否完整
|
||||
|
||||
2. **验证对外接口文档**
|
||||
- 列出所有公共方法
|
||||
- 为每个方法提供一句话功能说明
|
||||
- 确保接口描述准确
|
||||
|
||||
3. **检查API接口文档**
|
||||
- 如果是business模块且开放API,必须列出所有API
|
||||
- 每个API提供一句话功能说明
|
||||
- 包含请求方法和路径
|
||||
|
||||
4. **检查WebSocket接口文档**
|
||||
- Gateway模块必须详细说明WebSocket事件
|
||||
- 包含输入输出格式
|
||||
- 说明事件处理逻辑
|
||||
|
||||
5. **验证内部依赖分析**
|
||||
- 列出所有项目内部依赖
|
||||
- 说明每个依赖的用途
|
||||
- 确保依赖关系准确
|
||||
|
||||
6. **检查核心特性描述**
|
||||
- 识别技术特性、功能特性、质量特性
|
||||
- 突出游戏服务器特殊特性
|
||||
- 描述双模式、实时通信等特点
|
||||
|
||||
7. **评估潜在风险**
|
||||
- 识别技术风险、业务风险、运维风险、安全风险
|
||||
- 提供风险缓解措施
|
||||
- 特别关注游戏服务器特有风险
|
||||
|
||||
8. **验证文档与代码一致性**
|
||||
- 确保文档内容与实际代码实现一致
|
||||
- 检查接口签名、参数类型等准确性
|
||||
- 验证特性描述的真实性
|
||||
|
||||
## 🔥 重要提醒
|
||||
|
||||
**如果在本步骤中执行了任何修改操作(创建README文件、更新文档内容、修正接口描述等),必须立即重新执行步骤6的完整检查!**
|
||||
|
||||
- ✅ 执行修改 → 🔥 立即重新执行步骤6 → 提供验证报告 → 等待用户确认
|
||||
- ❌ 执行修改 → 直接结束检查(错误做法)
|
||||
|
||||
**🚨 重要强调:纯检查步骤不更新修改记录**
|
||||
**如果检查发现功能文档已经符合规范,无需任何修改,则:**
|
||||
- ❌ **禁止添加检查记录**:不要添加"AI代码检查步骤6:功能文档检查和优化"
|
||||
- ❌ **禁止更新时间戳**:不要修改@lastModified字段
|
||||
- ❌ **禁止递增版本号**:不要修改@version字段
|
||||
- ✅ **仅提供检查报告**:说明检查结果,确认符合规范
|
||||
|
||||
**不能跳过重新检查环节!**
|
||||
774
docs/ai-reading/step7-code-commit.md
Normal file
774
docs/ai-reading/step7-code-commit.md
Normal file
@@ -0,0 +1,774 @@
|
||||
# 步骤7:代码提交
|
||||
|
||||
## ⚠️ 执行前必读规范
|
||||
|
||||
**🔥 重要:在执行本步骤之前,AI必须先完整阅读同级目录下的 `README.md` 文件!**
|
||||
|
||||
该README文件包含:
|
||||
- 🎯 执行前准备和用户信息收集要求
|
||||
- 🔄 强制执行原则和分步执行流程
|
||||
- 🔥 修改后立即重新执行当前步骤的强制规则
|
||||
- 📝 文件修改记录规范和版本号递增规则
|
||||
- 🧪 测试文件调试规范和测试指令使用规范
|
||||
- 🚨 全局约束和游戏服务器特殊要求
|
||||
|
||||
**不阅读README直接执行步骤将导致执行不规范,违反项目要求!**
|
||||
|
||||
---
|
||||
|
||||
## 🎯 检查目标
|
||||
完成代码修改后的规范化提交流程,确保代码变更记录清晰、分支管理规范、提交信息符合项目标准。
|
||||
|
||||
## 📋 执行前置条件
|
||||
- 已完成前6个步骤的代码检查和修改
|
||||
- 所有修改的文件已更新修改记录和版本信息
|
||||
- 代码能够正常运行且通过测试
|
||||
|
||||
## 🚨 协作规范和范围控制
|
||||
|
||||
### 绝对禁止的操作
|
||||
**以下操作严格禁止,违反将影响其他AI的工作:**
|
||||
|
||||
1. **禁止暂存范围外代码**
|
||||
```bash
|
||||
# ❌ 绝对禁止
|
||||
git stash push [范围外文件]
|
||||
git stash push -m "消息" [范围外文件]
|
||||
```
|
||||
|
||||
2. **禁止重置范围外代码**
|
||||
```bash
|
||||
# ❌ 绝对禁止
|
||||
git reset HEAD [范围外文件]
|
||||
git checkout -- [范围外文件]
|
||||
```
|
||||
|
||||
3. **禁止移动或隐藏范围外代码**
|
||||
```bash
|
||||
# ❌ 绝对禁止
|
||||
git mv [范围外文件] [其他位置]
|
||||
git rm [范围外文件]
|
||||
```
|
||||
|
||||
### 协作原则
|
||||
- **范围外代码必须保持原状**:其他AI需要处理这些代码
|
||||
- **只处理自己的范围**:严格按照检查任务的文件夹范围执行
|
||||
- **不影响其他工作流**:任何操作都不能影响其他AI的检查任务
|
||||
|
||||
## 🔍 Git变更检查与校验
|
||||
|
||||
### 1. 检查Git状态和变更内容
|
||||
```bash
|
||||
# 查看当前工作区状态
|
||||
git status
|
||||
|
||||
# 查看具体变更内容
|
||||
git diff
|
||||
|
||||
# 查看已暂存的变更
|
||||
git diff --cached
|
||||
```
|
||||
|
||||
### 2. 文件修改记录校验
|
||||
**重要**:检查每个修改文件的头部信息是否与实际修改内容一致
|
||||
|
||||
#### 校验内容包括:
|
||||
- **修改记录**:最新的修改记录是否准确描述了本次变更
|
||||
- **修改类型**:记录的修改类型(代码规范优化、功能新增等)是否与实际修改匹配
|
||||
- **修改者信息**:是否使用了正确的用户名称
|
||||
- **修改日期**:是否使用了用户提供的真实日期
|
||||
- **版本号**:是否按照规则正确递增
|
||||
- **@lastModified**:是否更新为当前修改日期
|
||||
|
||||
#### 校验方法:
|
||||
1. 逐个检查修改文件的头部注释
|
||||
2. 对比git diff显示的实际修改内容
|
||||
3. 确认修改记录描述与实际变更一致
|
||||
4. 如发现不一致,立即修正文件头部信息
|
||||
|
||||
### 3. 修改记录不一致的处理
|
||||
如果发现文件头部的修改记录与实际修改内容不符:
|
||||
|
||||
```typescript
|
||||
// ❌ 错误示例:记录说是"功能新增",但实际只是代码清理
|
||||
/**
|
||||
* 最近修改:
|
||||
* - 2024-01-12: 功能新增 - 添加新的用户验证功能 (修改者: 张三)
|
||||
*/
|
||||
// 实际修改:只是删除了未使用的导入和注释优化
|
||||
|
||||
// ✅ 正确修正:
|
||||
/**
|
||||
* 最近修改:
|
||||
* - 2024-01-12: 代码规范优化 - 清理未使用导入和优化注释 (修改者: 张三)
|
||||
*/
|
||||
```
|
||||
|
||||
## 🌿 分支管理规范
|
||||
|
||||
### 🔥 重要原则:严格范围限制
|
||||
**🚨 绝对禁止:不得暂存、提交或以任何方式处理检查范围外的代码!**
|
||||
|
||||
- ✅ **正确做法**:只提交当前检查任务涉及的文件和文件夹
|
||||
- ❌ **严格禁止**:提交其他模块、其他开发者负责的文件
|
||||
- ❌ **严格禁止**:使用git stash暂存其他范围的代码
|
||||
- ❌ **严格禁止**:以任何方式移动、隐藏或处理范围外的代码
|
||||
- ⚠️ **检查要求**:提交前必须确认所有变更文件都在当前检查范围内
|
||||
- 🔥 **协作原则**:其他范围的代码必须保持原状,供其他AI处理
|
||||
|
||||
### 分支命名规范
|
||||
根据修改类型和检查范围创建对应的分支:
|
||||
|
||||
```bash
|
||||
# 代码规范优化分支(指定检查范围)
|
||||
feature/code-standard-[模块名称]-[日期]
|
||||
# 示例:feature/code-standard-auth-20240112
|
||||
# 示例:feature/code-standard-zulip-20240112
|
||||
|
||||
# Bug修复分支(指定模块)
|
||||
fix/[模块名称]-[具体问题描述]
|
||||
# 示例:fix/auth-login-validation-issue
|
||||
# 示例:fix/zulip-message-handling-bug
|
||||
|
||||
# 功能新增分支(指定模块)
|
||||
feature/[模块名称]-[功能名称]
|
||||
# 示例:feature/auth-multi-factor-authentication
|
||||
# 示例:feature/zulip-message-encryption
|
||||
|
||||
# 重构分支(指定模块)
|
||||
refactor/[模块名称]-[重构内容]
|
||||
# 示例:refactor/auth-service-architecture
|
||||
# 示例:refactor/zulip-websocket-handler
|
||||
|
||||
# 性能优化分支(指定模块)
|
||||
perf/[模块名称]-[优化内容]
|
||||
# 示例:perf/auth-token-validation
|
||||
# 示例:perf/zulip-message-processing
|
||||
|
||||
# 文档更新分支(指定范围)
|
||||
docs/[模块名称]-[文档类型]
|
||||
# 示例:docs/auth-api-documentation
|
||||
# 示例:docs/zulip-integration-guide
|
||||
```
|
||||
|
||||
### 创建和切换分支
|
||||
```bash
|
||||
# 🔥 重要:在当前分支基础上创建新分支(不切换到主分支)
|
||||
# 查看当前分支状态
|
||||
git status
|
||||
git branch
|
||||
|
||||
# 直接在当前分支基础上创建并切换到新分支(包含检查范围标识)
|
||||
git checkout -b feature/code-standard-[模块名称]-[日期]
|
||||
|
||||
# 示例:如果当前检查auth模块
|
||||
git checkout -b feature/code-standard-auth-20240112
|
||||
|
||||
# 示例:如果当前检查zulip模块
|
||||
git checkout -b feature/code-standard-zulip-20240112
|
||||
```
|
||||
|
||||
### 🔍 提交前范围检查
|
||||
在执行任何git操作前,必须进行范围检查:
|
||||
|
||||
```bash
|
||||
# 1. 查看当前变更的文件
|
||||
git status
|
||||
|
||||
# 2. 检查变更文件是否都在检查范围内
|
||||
git diff --name-only
|
||||
|
||||
# 3. 🚨 重要:如果发现范围外的文件,绝对不能暂存或提交!
|
||||
# 正确做法:只添加范围内的文件,忽略范围外的文件
|
||||
git add [范围内的具体文件路径]
|
||||
|
||||
# 4. ❌ 错误做法:不要使用以下命令处理范围外文件
|
||||
# git stash push [范围外文件] # 禁止!会影响其他AI
|
||||
# git reset HEAD [范围外文件] # 禁止!会影响其他AI
|
||||
# git add -i # 谨慎使用,容易误选范围外文件
|
||||
```
|
||||
|
||||
### 📂 检查范围示例
|
||||
|
||||
#### 正确的范围控制
|
||||
```bash
|
||||
# 如果检查任务是 "auth 模块代码规范优化"
|
||||
# ✅ 应该包含的文件:
|
||||
src/business/auth/
|
||||
src/core/auth/
|
||||
test/business/auth/
|
||||
test/core/auth/
|
||||
docs/auth/
|
||||
|
||||
# ❌ 不应该包含的文件:
|
||||
src/business/zulip/ # 其他模块
|
||||
src/business/user-mgmt/ # 其他模块
|
||||
client/ # 前端代码
|
||||
config/ # 配置文件(除非明确要求)
|
||||
```
|
||||
|
||||
#### 范围检查命令
|
||||
```bash
|
||||
# 检查当前变更是否超出范围
|
||||
git diff --name-only | grep -v "^src/business/auth/" | grep -v "^test/.*auth" | grep -v "^docs/.*auth"
|
||||
|
||||
# 如果上述命令有输出,说明存在范围外的文件,需要排除
|
||||
```
|
||||
|
||||
## 📝 提交信息规范
|
||||
|
||||
### 提交类型映射
|
||||
根据实际修改内容选择正确的提交类型:
|
||||
|
||||
| 修改内容 | 提交类型 | 示例 |
|
||||
|---------|---------|------|
|
||||
| 命名规范调整、注释优化、代码清理 | `style` | `style:统一TypeScript代码风格和注释规范` |
|
||||
| 清理未使用代码、优化导入 | `refactor` | `refactor:清理未使用的导入和死代码` |
|
||||
| 添加新功能、新方法 | `feat` | `feat:添加用户身份验证功能` |
|
||||
| 修复Bug、错误处理 | `fix` | `fix:修复用户登录时的并发问题` |
|
||||
| 性能改进、算法优化 | `perf` | `perf:优化数据库查询性能` |
|
||||
| 代码结构调整、重构 | `refactor` | `refactor:重构用户管理服务架构` |
|
||||
| 添加或修改测试 | `test` | `test:添加用户服务单元测试` |
|
||||
| 更新文档、README | `docs` | `docs:更新API接口文档` |
|
||||
| API接口相关 | `api` | `api:添加用户信息查询接口` |
|
||||
| 数据库相关 | `db` | `db:创建用户表结构` |
|
||||
| WebSocket相关 | `websocket` | `websocket:实现实时消息推送` |
|
||||
| 认证授权相关 | `auth` | `auth:实现JWT身份验证机制` |
|
||||
| 配置文件相关 | `config` | `config:添加Redis缓存配置` |
|
||||
|
||||
### 提交信息格式
|
||||
```bash
|
||||
<类型>(<范围>):<简短描述>
|
||||
|
||||
范围:<具体的文件/文件夹范围>
|
||||
[可选的详细描述]
|
||||
|
||||
[可选的关联信息]
|
||||
```
|
||||
|
||||
### 提交信息示例
|
||||
|
||||
#### 单一类型修改(明确范围)
|
||||
```bash
|
||||
# 代码规范优化
|
||||
git commit -m "style(auth):统一命名规范和注释格式
|
||||
|
||||
范围:src/business/auth/, src/core/auth/
|
||||
- 调整文件和变量命名符合项目规范
|
||||
- 优化注释格式和内容完整性
|
||||
- 清理代码格式和缩进问题"
|
||||
|
||||
# Bug修复
|
||||
git commit -m "fix(zulip):修复消息处理时的并发问题
|
||||
|
||||
范围:src/business/zulip/services/
|
||||
- 修复消息队列处理逻辑错误
|
||||
- 添加并发控制机制
|
||||
- 优化错误提示信息"
|
||||
|
||||
# 功能新增
|
||||
git commit -m "feat(auth):实现多因素认证系统
|
||||
|
||||
范围:src/business/auth/, src/core/auth/
|
||||
- 添加TOTP验证支持
|
||||
- 实现短信验证功能
|
||||
- 支持备用验证码"
|
||||
```
|
||||
|
||||
#### 多文件相关修改(明确范围)
|
||||
```bash
|
||||
git commit -m "refactor(user-mgmt):重构用户管理模块架构
|
||||
|
||||
范围:src/business/user-mgmt/, src/core/db/users/
|
||||
涉及文件:
|
||||
- src/business/user-mgmt/user.service.ts
|
||||
- src/business/user-mgmt/user.controller.ts
|
||||
- src/core/db/users/users.repository.ts
|
||||
|
||||
主要改进:
|
||||
- 分离业务逻辑和数据访问层
|
||||
- 优化服务接口设计
|
||||
- 提升代码可维护性"
|
||||
```
|
||||
|
||||
## 🔄 提交执行流程
|
||||
|
||||
### 🔥 范围控制原则
|
||||
**🚨 在执行任何提交操作前,必须确保所有变更文件都在当前检查任务的范围内!**
|
||||
**🚨 绝对禁止暂存、重置或以任何方式处理范围外的代码!**
|
||||
|
||||
### 1. 范围检查与文件筛选
|
||||
```bash
|
||||
# 第一步:查看所有变更文件
|
||||
git status
|
||||
git diff --name-only
|
||||
|
||||
# 第二步:识别范围内和范围外的文件
|
||||
# 假设当前检查任务是 "auth 模块优化"
|
||||
# 范围内文件示例:
|
||||
# - src/business/auth/
|
||||
# - src/core/auth/
|
||||
# - test/business/auth/
|
||||
# - test/core/auth/
|
||||
# - docs/auth/
|
||||
|
||||
# 第三步:🚨 重要 - 只添加范围内的文件,绝对不处理范围外文件
|
||||
git add src/business/auth/
|
||||
git add src/core/auth/
|
||||
git add test/business/auth/
|
||||
git add test/core/auth/
|
||||
git add docs/auth/
|
||||
|
||||
# ❌ 禁止使用交互式添加(容易误选范围外文件)
|
||||
# git add -i # 不推荐,风险太高
|
||||
```
|
||||
|
||||
### 2. 分阶段提交(推荐)
|
||||
将不同类型的修改分别提交,保持提交历史清晰:
|
||||
|
||||
```bash
|
||||
# 第一步:提交代码规范优化(仅限检查范围内)
|
||||
git add src/business/auth/ src/core/auth/
|
||||
git commit -m "style(auth):优化auth模块代码规范
|
||||
|
||||
范围:src/business/auth/, src/core/auth/
|
||||
- 统一命名规范和注释格式
|
||||
- 清理未使用的导入
|
||||
- 调整代码结构和缩进"
|
||||
|
||||
# 第二步:提交功能改进(如果有,仅限范围内)
|
||||
git add src/business/auth/enhanced-features/
|
||||
git commit -m "feat(auth):添加用户状态管理功能
|
||||
|
||||
范围:src/business/auth/
|
||||
- 实现用户激活/禁用功能
|
||||
- 添加状态变更日志记录
|
||||
- 支持批量状态操作"
|
||||
|
||||
# 第三步:提交测试相关(仅限范围内)
|
||||
git add test/business/auth/ test/core/auth/
|
||||
git commit -m "test(auth):完善auth模块测试覆盖
|
||||
|
||||
范围:test/business/auth/, test/core/auth/
|
||||
- 添加缺失的单元测试
|
||||
- 补充集成测试用例
|
||||
- 提升测试覆盖率到95%以上"
|
||||
|
||||
# 第四步:提交文档更新(仅限范围内)
|
||||
git add docs/auth/ src/business/auth/README.md src/core/auth/README.md
|
||||
git commit -m "docs(auth):更新auth模块文档
|
||||
|
||||
范围:docs/auth/, auth模块README文件
|
||||
- 完善API接口文档
|
||||
- 更新功能模块README
|
||||
- 添加使用示例和注意事项"
|
||||
```
|
||||
|
||||
### 3. 使用交互式暂存(精确控制)
|
||||
```bash
|
||||
# 交互式选择要提交的代码块(仅限范围内文件)
|
||||
git add -p src/business/auth/login.service.ts
|
||||
|
||||
# 选择代码规范相关的修改
|
||||
# 提交第一部分
|
||||
git commit -m "style(auth):优化login.service代码规范"
|
||||
|
||||
# 暂存剩余的功能修改
|
||||
git add src/business/auth/login.service.ts
|
||||
git commit -m "feat(auth):添加多因素认证支持"
|
||||
```
|
||||
|
||||
### 4. 范围外文件处理
|
||||
🚨 **重要:绝对不能处理范围外的文件!**
|
||||
|
||||
```bash
|
||||
# ✅ 正确做法:查看范围外的文件,但不做任何处理
|
||||
git status | findstr /v "auth" # 假设检查范围是auth模块,查看非auth文件
|
||||
|
||||
# ✅ 正确做法:只添加范围内的文件
|
||||
git add src/business/auth/
|
||||
git add src/core/auth/
|
||||
git add test/business/auth/
|
||||
|
||||
# ❌ 错误做法:不要重置、暂存或移动范围外文件
|
||||
# git checkout -- src/business/zulip/some-file.ts # 禁止!
|
||||
# git stash push src/business/zulip/ # 禁止!会影响其他AI
|
||||
# git reset HEAD src/business/user-mgmt/ # 禁止!会影响其他AI
|
||||
|
||||
# 🔥 协作原则:范围外文件必须保持原状,供其他AI处理
|
||||
```
|
||||
|
||||
### 5. 提交前最终检查
|
||||
```bash
|
||||
# 检查暂存区内容(确保只有范围内文件)
|
||||
git diff --cached --name-only
|
||||
|
||||
# 确认所有文件都在检查范围内
|
||||
git diff --cached --name-only | grep -E "^(src|test|docs)/(business|core)/auth/"
|
||||
|
||||
# 确认提交信息准确性
|
||||
git commit --dry-run
|
||||
|
||||
# 执行提交
|
||||
git commit -m "提交信息"
|
||||
```
|
||||
|
||||
## 📄 合并文档生成
|
||||
|
||||
### 🔥 重要规范:独立合并文档生成
|
||||
**在完成代码提交后,必须在docs目录中生成一个独立的合并md文档,方便最后统一完成合并操作。**
|
||||
|
||||
#### 合并文档命名规范
|
||||
```
|
||||
docs/merge-requests/[模块名称]-code-standard-[日期].md
|
||||
```
|
||||
|
||||
#### 合并文档存放位置
|
||||
- **目录路径**:`docs/merge-requests/`
|
||||
- **文件命名**:`[模块名称]-code-standard-[日期].md`
|
||||
- **示例文件名**:
|
||||
- `auth-code-standard-20240112.md`
|
||||
- `zulip-code-standard-20240112.md`
|
||||
- `user-mgmt-code-standard-20240112.md`
|
||||
|
||||
#### 创建合并文档目录
|
||||
如果`docs/merge-requests/`目录不存在,需要先创建:
|
||||
```bash
|
||||
mkdir -p docs/merge-requests
|
||||
```
|
||||
|
||||
### 合并请求文档模板
|
||||
完成所有提交后,在`docs/merge-requests/`目录中生成独立的合并文档:
|
||||
|
||||
```markdown
|
||||
# 代码规范优化合并请求
|
||||
|
||||
## 📋 变更概述
|
||||
本次合并请求包含对 [具体模块/功能] 的代码规范优化和质量提升。
|
||||
|
||||
## 🔍 主要变更内容
|
||||
|
||||
### 代码规范优化
|
||||
- **命名规范**:调整文件、类、方法命名符合项目规范
|
||||
- **注释规范**:完善注释内容,统一注释格式
|
||||
- **代码清理**:移除未使用的导入、变量和死代码
|
||||
- **格式统一**:统一代码缩进、换行和空格使用
|
||||
|
||||
### 功能改进(如适用)
|
||||
- **新增功能**:[具体描述新增的功能]
|
||||
- **Bug修复**:[具体描述修复的问题]
|
||||
- **性能优化**:[具体描述优化的内容]
|
||||
|
||||
### 测试完善(如适用)
|
||||
- **测试覆盖**:补充缺失的单元测试和集成测试
|
||||
- **测试质量**:提升测试用例的完整性和准确性
|
||||
|
||||
### 文档更新(如适用)
|
||||
- **API文档**:更新接口文档和使用说明
|
||||
- **README文档**:完善功能模块说明和使用指南
|
||||
|
||||
## 📊 影响范围
|
||||
- **修改文件数量**:[数量] 个文件
|
||||
- **新增代码行数**:+[数量] 行
|
||||
- **删除代码行数**:-[数量] 行
|
||||
- **测试覆盖率**:从 [原覆盖率]% 提升到 [新覆盖率]%
|
||||
|
||||
## 🧪 测试验证
|
||||
- [ ] 所有单元测试通过
|
||||
- [ ] 集成测试通过
|
||||
- [ ] E2E测试通过
|
||||
- [ ] 性能测试通过(如适用)
|
||||
- [ ] 手动功能验证通过
|
||||
|
||||
## 🔗 相关链接
|
||||
- 相关Issue:#[Issue编号]
|
||||
- 设计文档:[链接]
|
||||
- API文档:[链接]
|
||||
|
||||
## 📝 审查要点
|
||||
请重点关注以下方面:
|
||||
1. **代码规范**:命名、注释、格式是否符合项目标准
|
||||
2. **功能正确性**:新增或修改的功能是否按预期工作
|
||||
3. **测试完整性**:测试用例是否充分覆盖变更内容
|
||||
4. **文档准确性**:文档是否与代码实现保持一致
|
||||
5. **性能影响**:变更是否对系统性能产生负面影响
|
||||
|
||||
## ⚠️ 注意事项
|
||||
- 本次变更主要为代码质量提升,不涉及业务逻辑重大变更
|
||||
- 所有修改都经过充分测试验证
|
||||
- 建议在非高峰期进行合并部署
|
||||
|
||||
## 🚀 部署说明
|
||||
- **部署环境**:[测试环境/生产环境]
|
||||
- **部署时间**:[建议的部署时间]
|
||||
- **回滚方案**:如有问题可快速回滚到上一版本
|
||||
- **监控要点**:关注 [具体的监控指标]
|
||||
```
|
||||
|
||||
### 🚨 合并文档不纳入Git提交
|
||||
**重要:合并文档仅用于本地记录和合并操作参考,不应加入到Git提交中!**
|
||||
|
||||
#### 原因说明
|
||||
- 合并文档是临时性的操作记录,不属于项目代码的一部分
|
||||
- 避免在代码仓库中产生大量临时文档
|
||||
- 合并完成后,相关信息已体现在Git提交历史和PR记录中
|
||||
|
||||
#### 操作规范
|
||||
```bash
|
||||
# ❌ 禁止将合并文档加入Git提交
|
||||
git add docs/merge-requests/ # 禁止!
|
||||
|
||||
# ✅ 正确做法:确保合并文档不被提交
|
||||
# 方法1:在.gitignore中已配置忽略(推荐)
|
||||
# 方法2:提交时明确排除
|
||||
git add . -- ':!docs/merge-requests/'
|
||||
|
||||
# ✅ 检查暂存区,确认没有合并文档
|
||||
git diff --cached --name-only | grep "merge-requests"
|
||||
# 如果有输出,需要取消暂存
|
||||
git reset HEAD docs/merge-requests/
|
||||
```
|
||||
|
||||
#### .gitignore 配置建议
|
||||
确保项目的 `.gitignore` 文件中包含:
|
||||
```
|
||||
# 合并文档目录(不纳入版本控制)
|
||||
docs/merge-requests/
|
||||
```
|
||||
|
||||
### 📝 独立合并文档创建示例
|
||||
|
||||
#### 1. 创建合并文档目录(如果不存在)
|
||||
```bash
|
||||
mkdir -p docs/merge-requests
|
||||
```
|
||||
|
||||
#### 2. 生成具体的合并文档
|
||||
假设当前检查的是auth模块,日期是2024-01-12,则创建文件:
|
||||
`docs/merge-requests/auth-code-standard-20240112.md`
|
||||
|
||||
#### 3. 合并文档内容示例
|
||||
```markdown
|
||||
# Auth模块代码规范优化合并请求
|
||||
|
||||
## 📋 变更概述
|
||||
本次合并请求包含对Auth模块的代码规范优化和质量提升,涉及登录、注册、权限验证等核心功能。
|
||||
|
||||
## 🔍 主要变更内容
|
||||
|
||||
### 代码规范优化
|
||||
- **命名规范**:统一service、controller、entity文件命名
|
||||
- **注释规范**:完善JSDoc注释,添加参数和返回值说明
|
||||
- **代码清理**:移除未使用的导入和死代码
|
||||
- **格式统一**:统一TypeScript代码缩进和换行
|
||||
|
||||
### 功能改进
|
||||
- **错误处理**:完善异常捕获和错误提示
|
||||
- **类型安全**:添加缺失的TypeScript类型定义
|
||||
- **性能优化**:优化数据库查询和缓存策略
|
||||
|
||||
### 测试完善
|
||||
- **测试覆盖**:补充登录服务和注册控制器的单元测试
|
||||
- **集成测试**:添加JWT认证流程的集成测试
|
||||
- **E2E测试**:完善用户注册登录的端到端测试
|
||||
|
||||
## 📊 影响范围
|
||||
- **修改文件数量**:15个文件
|
||||
- **涉及模块**:src/business/auth/, src/core/auth/, test/business/auth/
|
||||
- **新增代码行数**:+245行
|
||||
- **删除代码行数**:-89行
|
||||
- **测试覆盖率**:从78%提升到95%
|
||||
|
||||
## 🧪 测试验证
|
||||
- [x] 所有单元测试通过 (npm run test:auth:unit)
|
||||
- [x] 集成测试通过 (npm run test:auth:integration)
|
||||
- [x] E2E测试通过 (npm run test:auth:e2e)
|
||||
- [x] 手动功能验证通过
|
||||
|
||||
## 🔗 相关信息
|
||||
- **分支名称**:feature/code-standard-auth-20240112
|
||||
- **远程仓库**:origin
|
||||
- **检查日期**:2024-01-12
|
||||
- **检查人员**:[用户名称]
|
||||
|
||||
## 📝 合并后操作
|
||||
1. 验证生产环境功能正常
|
||||
2. 监控登录注册成功率
|
||||
3. 关注系统性能指标
|
||||
4. 更新相关文档链接
|
||||
|
||||
---
|
||||
**文档生成时间**:2024-01-12
|
||||
**对应分支**:feature/code-standard-auth-20240112
|
||||
**合并状态**:待合并
|
||||
```
|
||||
|
||||
#### 4. 在PR中引用合并文档
|
||||
创建Pull Request时,在描述中添加:
|
||||
```markdown
|
||||
## 📄 详细合并文档
|
||||
请查看独立合并文档:`docs/merge-requests/auth-code-standard-20240112.md`
|
||||
|
||||
该文档包含完整的变更说明、测试验证结果和合并后操作指南。
|
||||
```
|
||||
|
||||
## 🔧 执行步骤总结
|
||||
|
||||
### 完整执行流程
|
||||
1. **Git变更检查**
|
||||
- 执行 `git status` 和 `git diff` 查看变更
|
||||
- 确认所有修改文件都在当前检查任务的范围内
|
||||
- 排除或暂存范围外的文件
|
||||
|
||||
2. **修改记录校验**
|
||||
- 逐个检查修改文件的头部注释
|
||||
- 确认修改记录与实际变更内容一致
|
||||
- 如有不一致,立即修正
|
||||
|
||||
3. **创建功能分支**
|
||||
- 🔥 **在当前分支基础上**创建新分支(不切换到主分支)
|
||||
- 根据修改类型和检查范围创建合适的分支
|
||||
- 使用规范的分支命名格式(包含模块标识)
|
||||
|
||||
4. **分类提交代码**
|
||||
- 按修改类型分别提交(style、feat、fix、docs等)
|
||||
- 使用规范的提交信息格式(包含范围标识)
|
||||
- 每次提交保持原子性(一次提交只做一件事)
|
||||
- 确保每次提交只包含检查范围内的文件
|
||||
|
||||
5. **推送到指定远程仓库**
|
||||
- 询问用户要推送到哪个远程仓库
|
||||
- 使用 `git push [远程仓库名] [分支名]` 推送到指定远程仓库
|
||||
- 验证推送结果和分支状态
|
||||
|
||||
6. **生成独立合并文档**
|
||||
- 在 `docs/merge-requests/` 目录中创建独立的合并md文档
|
||||
- 使用规范的文件命名:`[模块名称]-code-standard-[日期].md`
|
||||
- 包含完整的变更概述、影响范围、测试验证等信息
|
||||
- 方便后续统一进行合并操作管理
|
||||
|
||||
7. **创建PR和关联文档**
|
||||
- 在指定的远程仓库创建Pull Request
|
||||
- 在PR描述中引用独立合并文档的路径
|
||||
- 明确标注检查范围和变更内容
|
||||
|
||||
## 🚀 推送到远程仓库
|
||||
|
||||
### 📋 执行前询问
|
||||
**在推送前,AI必须询问用户以下信息:**
|
||||
1. **目标远程仓库名称**:要推送到哪个远程仓库?(如:origin、whale-town-end、upstream等)
|
||||
2. **确认分支名称**:确认要推送的分支名称是否正确
|
||||
|
||||
### 推送新分支到指定远程仓库
|
||||
完成所有提交后,将分支推送到用户指定的远程仓库:
|
||||
|
||||
```bash
|
||||
# 推送新分支到指定远程仓库([远程仓库名]由用户提供)
|
||||
git push [远程仓库名] feature/code-standard-[模块名称]-[日期]
|
||||
|
||||
# 示例:推送到origin远程仓库
|
||||
git push origin feature/code-standard-auth-20240112
|
||||
|
||||
# 示例:推送到whale-town-end远程仓库
|
||||
git push whale-town-end feature/code-standard-auth-20240112
|
||||
|
||||
# 示例:推送到upstream远程仓库
|
||||
git push upstream feature/code-standard-zulip-20240112
|
||||
|
||||
# 如果是首次推送该分支,设置上游跟踪
|
||||
git push -u [远程仓库名] feature/code-standard-auth-20240112
|
||||
```
|
||||
|
||||
### 验证推送结果
|
||||
```bash
|
||||
# 查看远程分支状态
|
||||
git branch -r
|
||||
|
||||
# 确认分支已成功推送到指定远程仓库
|
||||
git ls-remote [远程仓库名] | grep feature/code-standard-[模块名称]-[日期]
|
||||
|
||||
# 查看指定远程仓库的所有分支
|
||||
git ls-remote [远程仓库名]
|
||||
```
|
||||
|
||||
### 远程仓库配置检查
|
||||
如果推送时遇到问题,可以检查远程仓库配置:
|
||||
|
||||
```bash
|
||||
# 查看当前配置的所有远程仓库
|
||||
git remote -v
|
||||
|
||||
# 如果没有指定的远程仓库,需要添加
|
||||
git remote add [远程仓库名] [仓库URL]
|
||||
|
||||
# 验证指定远程仓库连接
|
||||
git remote show [远程仓库名]
|
||||
```
|
||||
|
||||
### 🔍 常见远程仓库名称
|
||||
- **origin**:通常是默认的远程仓库
|
||||
- **upstream**:通常指向原始项目仓库
|
||||
- **whale-town-end**:项目特定的远程仓库名
|
||||
- **fork**:个人fork的仓库
|
||||
- **dev**:开发环境仓库
|
||||
|
||||
## ⚠️ 重要注意事项
|
||||
|
||||
### 提交原则
|
||||
- **范围限制**:只提交当前检查任务范围内的文件,不涉及其他模块
|
||||
- **原子性**:每次提交只包含一个逻辑改动
|
||||
- **完整性**:每次提交的代码都应该能正常运行
|
||||
- **描述性**:提交信息要清晰描述改动内容、范围和原因
|
||||
- **一致性**:文件修改记录必须与实际修改内容一致
|
||||
- **合并文档排除**:`docs/merge-requests/` 目录下的合并文档不纳入Git提交
|
||||
|
||||
### 质量保证
|
||||
- 提交前必须验证代码能正常运行
|
||||
- 确保所有测试通过
|
||||
- 检查代码格式和规范符合项目标准
|
||||
- 验证文档与代码实现保持一致
|
||||
|
||||
### 协作规范
|
||||
- 遵循项目的分支管理策略
|
||||
- 推送前询问并确认目标远程仓库
|
||||
- 提供清晰的合并请求说明
|
||||
- 及时响应代码审查意见
|
||||
- 保持提交历史的清晰和可追溯性
|
||||
|
||||
## 🔥 重要提醒
|
||||
|
||||
**如果在本步骤中执行了任何修改操作(修正文件头部信息、调整提交内容、更新文档等),必须立即重新执行步骤7的完整检查!**
|
||||
|
||||
- ✅ 执行修改 → 🔥 立即重新执行步骤7 → 提供验证报告 → 等待用户确认
|
||||
- ❌ 执行修改 → 直接结束检查(错误做法)
|
||||
|
||||
**🚨 重要强调:纯检查步骤不更新修改记录**
|
||||
**如果检查发现代码提交已经符合规范,无需任何修改,则:**
|
||||
- ❌ **禁止添加检查记录**:不要添加"AI代码检查步骤7:代码提交检查和优化"
|
||||
- ❌ **禁止更新时间戳**:不要修改@lastModified字段
|
||||
- ❌ **禁止递增版本号**:不要修改@version字段
|
||||
- ✅ **仅提供检查报告**:说明检查结果,确认符合规范
|
||||
|
||||
**不能跳过重新检查环节!**
|
||||
|
||||
### 🔥 合并文档生成强制要求
|
||||
**每次完成代码提交后,必须在docs/merge-requests/目录中生成独立的合并md文档!**
|
||||
|
||||
- ✅ 完成提交 → 生成独立合并文档 → 在PR中引用文档路径
|
||||
- ❌ 完成提交 → 直接创建PR(缺少独立文档)
|
||||
|
||||
**独立合并文档是统一管理合并操作的重要依据,不能省略!**
|
||||
|
||||
## 📋 执行前必须询问的信息
|
||||
|
||||
**在执行推送操作前,AI必须询问用户:**
|
||||
|
||||
1. **目标远程仓库名称**
|
||||
- 问题:请问要推送到哪个远程仓库?
|
||||
- 示例回答:origin / whale-town-end / upstream / 其他
|
||||
|
||||
2. **确认分支名称**
|
||||
- 问题:确认要推送的分支名称是:feature/code-standard-[模块名称]-[日期] 吗?
|
||||
- 等待用户确认或提供正确的分支名称
|
||||
|
||||
**只有获得用户明确回答后,才能执行推送操作!**
|
||||
115
docs/ai-reading/tools/setup-user-info.js
Normal file
115
docs/ai-reading/tools/setup-user-info.js
Normal file
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* AI代码检查用户信息管理脚本
|
||||
*
|
||||
* 功能:获取当前日期和用户名称,保存到me.config.json供AI检查步骤使用
|
||||
*
|
||||
* @author AI助手
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-13
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const readline = require('readline');
|
||||
|
||||
const configPath = path.join(__dirname, '..', 'me.config.json');
|
||||
|
||||
// 获取当前日期(YYYY-MM-DD格式)
|
||||
function getCurrentDate() {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(now.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
// 读取现有配置
|
||||
function readConfig() {
|
||||
try {
|
||||
if (!fs.existsSync(configPath)) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
||||
} catch (error) {
|
||||
console.error('❌ 读取配置文件失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
function saveConfig(config) {
|
||||
try {
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||
console.log('✅ 配置已保存');
|
||||
} catch (error) {
|
||||
console.error('❌ 保存配置失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 提示用户输入名称
|
||||
function promptUserName() {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
rl.question('👤 请输入您的名称或昵称: ', (name) => {
|
||||
rl.close();
|
||||
resolve(name.trim());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 主执行逻辑
|
||||
async function main() {
|
||||
console.log('🚀 AI代码检查 - 用户信息设置');
|
||||
|
||||
const currentDate = getCurrentDate();
|
||||
console.log('📅 当前日期:', currentDate);
|
||||
|
||||
const existingConfig = readConfig();
|
||||
|
||||
// 如果配置存在且日期匹配,直接返回
|
||||
if (existingConfig && existingConfig.date === currentDate) {
|
||||
console.log('✅ 配置已是最新,当前用户:', existingConfig.name);
|
||||
return existingConfig;
|
||||
}
|
||||
|
||||
// 需要更新配置
|
||||
console.log('🔄 需要更新用户信息...');
|
||||
const userName = await promptUserName();
|
||||
|
||||
if (!userName) {
|
||||
console.error('❌ 用户名称不能为空');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const config = {
|
||||
date: currentDate,
|
||||
name: userName
|
||||
};
|
||||
|
||||
saveConfig(config);
|
||||
console.log('🎉 设置完成!', config);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
// 导出函数供其他脚本使用
|
||||
function getConfig() {
|
||||
return readConfig();
|
||||
}
|
||||
|
||||
// 如果直接运行此脚本
|
||||
if (require.main === module) {
|
||||
main().catch((error) => {
|
||||
console.error('❌ 脚本执行失败:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { getConfig, getCurrentDate };
|
||||
@@ -1,29 +1,30 @@
|
||||
# API接口文档
|
||||
|
||||
本目录包含了像素游戏服务器用户认证API的完整文档。
|
||||
本目录包含了 Whale Town 像素游戏服务器的完整API文档,采用业务功能模块化设计,提供17个接口覆盖所有核心功能。
|
||||
|
||||
## 📋 文档文件说明
|
||||
|
||||
### 1. api-documentation.md
|
||||
详细的API接口文档,包含:
|
||||
- **17个API接口** - 用户认证、用户管理、管理员功能、安全防护
|
||||
- 接口概述和通用响应格式
|
||||
- 每个接口的详细说明、参数、响应示例
|
||||
- 错误代码说明
|
||||
- 数据验证规则
|
||||
- 错误代码说明和状态码映射
|
||||
- 数据验证规则和业务逻辑
|
||||
- 使用示例(JavaScript/TypeScript 和 cURL)
|
||||
|
||||
### 2. openapi.yaml
|
||||
OpenAPI 3.0规范文件,可以用于:
|
||||
- 导入到Swagger Editor查看和编辑
|
||||
- 生成客户端SDK
|
||||
- 集成到API网关
|
||||
- 自动化测试
|
||||
- 生成客户端SDK(支持多种语言)
|
||||
- 集成到API网关和测试工具
|
||||
- 自动化测试和文档生成
|
||||
|
||||
### 3. postman-collection.json
|
||||
Postman集合文件,包含:
|
||||
- 所有API接口的请求示例
|
||||
- 预设的请求参数
|
||||
- 响应示例
|
||||
- 所有17个API接口的请求示例
|
||||
- 预设的请求参数和环境变量
|
||||
- 完整的响应示例和测试脚本
|
||||
- 可直接导入Postman进行测试
|
||||
|
||||
## 🚀 快速开始
|
||||
@@ -34,7 +35,7 @@ Postman集合文件,包含:
|
||||
# 启动开发服务器
|
||||
pnpm run dev
|
||||
|
||||
# 访问Swagger UI
|
||||
# 访问Swagger UI(推荐)
|
||||
# 浏览器打开: http://localhost:3000/api-docs
|
||||
```
|
||||
|
||||
@@ -64,74 +65,138 @@ openapi-generator generate -i docs/api/openapi.yaml -g typescript-axios -o ./cli
|
||||
|
||||
## 📊 API接口概览
|
||||
|
||||
### 🔐 用户认证模块 (9个接口)
|
||||
| 接口 | 方法 | 路径 | 描述 |
|
||||
|------|------|------|------|
|
||||
| 用户登录 | POST | /auth/login | 支持用户名、邮箱或手机号登录 |
|
||||
| 用户注册 | POST | /auth/register | 创建新用户账户 |
|
||||
| GitHub OAuth | POST | /auth/github | 使用GitHub账户登录或注册 |
|
||||
| 发送验证码 | POST | /auth/forgot-password | 发送密码重置验证码 |
|
||||
| 发送重置验证码 | POST | /auth/forgot-password | 发送密码重置验证码 |
|
||||
| 重置密码 | POST | /auth/reset-password | 使用验证码重置密码 |
|
||||
| 修改密码 | PUT | /auth/change-password | 修改用户密码 |
|
||||
| 发送邮箱验证码 | POST | /auth/send-email-verification | 发送邮箱验证码 |
|
||||
| 验证邮箱 | POST | /auth/verify-email | 验证邮箱验证码 |
|
||||
| 重发邮箱验证码 | POST | /auth/resend-email-verification | 重新发送邮箱验证码 |
|
||||
|
||||
### 👥 用户管理模块 (3个接口)
|
||||
| 接口 | 方法 | 路径 | 描述 |
|
||||
|------|------|------|------|
|
||||
| 修改用户状态 | PUT | /admin/users/:id/status | 修改指定用户状态 |
|
||||
| 批量修改状态 | POST | /admin/users/batch-status | 批量修改用户状态 |
|
||||
| 用户状态统计 | GET | /admin/users/status-stats | 获取各状态用户统计 |
|
||||
|
||||
### 🛡️ 管理员模块 (4个接口)
|
||||
| 接口 | 方法 | 路径 | 描述 |
|
||||
|------|------|------|------|
|
||||
| 管理员登录 | POST | /admin/auth/login | 管理员身份认证 |
|
||||
| 获取用户列表 | GET | /admin/users | 分页获取用户列表 |
|
||||
| 获取用户详情 | GET | /admin/users/:id | 获取指定用户信息 |
|
||||
| 重置用户密码 | POST | /admin/users/:id/reset-password | 管理员重置用户密码 |
|
||||
|
||||
### 📊 系统状态 (1个接口)
|
||||
| 接口 | 方法 | 路径 | 描述 |
|
||||
|------|------|------|------|
|
||||
| 应用状态 | GET | / | 获取应用运行状态和系统信息 |
|
||||
|
||||
## 🧪 快速测试
|
||||
|
||||
### 使用cURL测试登录接口
|
||||
### 使用cURL测试核心接口
|
||||
|
||||
```bash
|
||||
# 测试用户登录
|
||||
# 1. 测试应用状态
|
||||
curl -X GET http://localhost:3000/
|
||||
|
||||
# 2. 测试用户注册
|
||||
curl -X POST http://localhost:3000/auth/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"username": "testuser",
|
||||
"password": "Test123456",
|
||||
"nickname": "测试用户",
|
||||
"email": "test@example.com"
|
||||
}'
|
||||
|
||||
# 3. 测试用户登录
|
||||
curl -X POST http://localhost:3000/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"identifier": "testuser",
|
||||
"password": "password123"
|
||||
"password": "Test123456"
|
||||
}'
|
||||
|
||||
# 测试用户注册
|
||||
curl -X POST http://localhost:3000/auth/register \
|
||||
# 4. 测试管理员登录
|
||||
curl -X POST http://localhost:3000/admin/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"username": "newuser",
|
||||
"password": "password123",
|
||||
"nickname": "新用户",
|
||||
"email": "newuser@example.com"
|
||||
"username": "admin",
|
||||
"password": "Admin123456"
|
||||
}'
|
||||
```
|
||||
|
||||
### 使用JavaScript测试
|
||||
|
||||
```javascript
|
||||
// 用户注册
|
||||
const registerResponse = await fetch('http://localhost:3000/auth/register', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: 'testuser',
|
||||
password: 'Test123456',
|
||||
nickname: '测试用户',
|
||||
email: 'test@example.com'
|
||||
})
|
||||
});
|
||||
|
||||
// 用户登录
|
||||
const response = await fetch('http://localhost:3000/auth/login', {
|
||||
const loginResponse = await fetch('http://localhost:3000/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
identifier: 'testuser',
|
||||
password: 'password123'
|
||||
password: 'Test123456'
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
console.log(data);
|
||||
const loginData = await loginResponse.json();
|
||||
console.log('登录结果:', loginData);
|
||||
```
|
||||
|
||||
### 使用自动化测试脚本
|
||||
|
||||
```bash
|
||||
# Windows PowerShell
|
||||
.\test-api.ps1
|
||||
|
||||
# Linux/macOS Bash
|
||||
./test-api.sh
|
||||
|
||||
# 自定义测试参数
|
||||
.\test-api.ps1 -BaseUrl "http://localhost:3000" -TestEmail "custom@example.com"
|
||||
```
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
1. **开发环境**: 当前配置适用于开发环境,生产环境需要使用HTTPS
|
||||
2. **认证**: 实际应用中应实现JWT认证机制
|
||||
3. **限流**: 建议对认证接口实施限流策略
|
||||
4. **验证码**: 示例中返回验证码仅用于演示,生产环境不应返回
|
||||
5. **错误处理**: 建议实现统一的错误处理机制
|
||||
2. **认证机制**: 项目使用JWT认证,管理员使用独立的Token系统
|
||||
3. **频率限制**: 已实现API频率限制,登录接口2次/分钟,管理员操作10次/分钟
|
||||
4. **用户状态**: 支持6种用户状态管理(active、inactive、locked、banned、deleted、pending)
|
||||
5. **测试模式**: 邮件服务支持测试模式,验证码会在控制台输出
|
||||
6. **存储模式**: 支持Redis文件存储和内存数据库,便于无依赖测试
|
||||
7. **安全防护**: 实现了维护模式、内容类型检查、超时控制等安全机制
|
||||
|
||||
## 🔄 更新文档
|
||||
|
||||
当API接口发生变化时,请同步更新以下文件:
|
||||
1. 更新DTO类的Swagger装饰器
|
||||
2. 更新 `api-documentation.md`
|
||||
3. 更新 `openapi.yaml`
|
||||
4. 更新 `postman-collection.json`
|
||||
5. 重新生成Swagger文档
|
||||
1. 更新Controller和DTO类的Swagger装饰器
|
||||
2. 更新 `api-documentation.md` 接口文档
|
||||
3. 更新 `openapi.yaml` 规范文件
|
||||
4. 更新 `postman-collection.json` 测试集合
|
||||
5. 重新生成Swagger文档并验证
|
||||
|
||||
## 🔗 相关链接
|
||||
|
||||
@@ -139,3 +204,5 @@ console.log(data);
|
||||
- [OpenAPI规范](https://swagger.io/specification/)
|
||||
- [Postman文档](https://learning.postman.com/docs/getting-started/introduction/)
|
||||
- [Swagger Editor](https://editor.swagger.io/)
|
||||
- [项目架构文档](../ARCHITECTURE.md)
|
||||
- [开发规范指南](../development/)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,856 +0,0 @@
|
||||
# 后端开发规范指南
|
||||
|
||||
## 一、文档概述
|
||||
|
||||
### 1.1 文档目的
|
||||
|
||||
本文档定义了 Datawhale Town 后端开发的编码规范、注释标准、业务逻辑设计原则和日志记录要求,确保代码质量、可维护性和系统稳定性。
|
||||
|
||||
### 1.2 适用范围
|
||||
|
||||
- 所有后端开发人员
|
||||
- 代码审查人员
|
||||
- 系统维护人员
|
||||
|
||||
---
|
||||
|
||||
## 二、注释规范
|
||||
|
||||
### 2.1 模块注释
|
||||
|
||||
每个功能模块文件必须包含模块级注释,说明模块用途、主要功能和依赖关系。
|
||||
|
||||
**格式要求:**
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 玩家管理模块
|
||||
*
|
||||
* 功能描述:
|
||||
* - 处理玩家注册、登录、信息更新等核心功能
|
||||
* - 管理玩家角色皮肤和个人资料
|
||||
* - 提供玩家数据的 CRUD 操作
|
||||
*
|
||||
* 依赖模块:
|
||||
* - AuthService: 身份验证服务
|
||||
* - DatabaseService: 数据库操作服务
|
||||
* - LoggerService: 日志记录服务
|
||||
*
|
||||
* @author 开发者姓名
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-13
|
||||
*/
|
||||
```
|
||||
|
||||
### 2.2 类注释
|
||||
|
||||
每个类必须包含类级注释,说明类的职责、主要方法和使用场景。
|
||||
|
||||
**格式要求:**
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 玩家服务类
|
||||
*
|
||||
* 职责:
|
||||
* - 处理玩家相关的业务逻辑
|
||||
* - 管理玩家状态和数据
|
||||
* - 提供玩家操作的统一接口
|
||||
*
|
||||
* 主要方法:
|
||||
* - createPlayer(): 创建新玩家
|
||||
* - updatePlayerInfo(): 更新玩家信息
|
||||
* - getPlayerById(): 根据ID获取玩家信息
|
||||
*
|
||||
* 使用场景:
|
||||
* - 玩家注册登录流程
|
||||
* - 个人陈列室数据管理
|
||||
* - 广场玩家状态同步
|
||||
*/
|
||||
@Injectable()
|
||||
export class PlayerService {
|
||||
// 类实现
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 方法注释
|
||||
|
||||
每个方法必须包含详细的方法注释,说明功能、参数、返回值、异常和业务逻辑。
|
||||
|
||||
**格式要求:**
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 创建新玩家
|
||||
*
|
||||
* 功能描述:
|
||||
* 根据邮箱创建新的玩家账户,包含基础信息初始化和默认配置设置
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证邮箱格式和白名单
|
||||
* 2. 检查邮箱是否已存在
|
||||
* 3. 生成唯一玩家ID
|
||||
* 4. 初始化默认角色皮肤和个人信息
|
||||
* 5. 创建对应的个人陈列室
|
||||
* 6. 记录创建日志
|
||||
*
|
||||
* @param email 玩家邮箱地址,必须符合邮箱格式且在白名单中
|
||||
* @param nickname 玩家昵称,长度3-20字符,不能包含特殊字符
|
||||
* @param avatarSkin 角色皮肤ID,必须是1-8之间的有效值
|
||||
* @returns Promise<Player> 创建成功的玩家对象
|
||||
*
|
||||
* @throws BadRequestException 当邮箱格式错误或不在白名单中
|
||||
* @throws ConflictException 当邮箱已存在时
|
||||
* @throws InternalServerErrorException 当数据库操作失败时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const player = await playerService.createPlayer(
|
||||
* 'user@datawhale.club',
|
||||
* '数据鲸鱼',
|
||||
* '1'
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
async createPlayer(
|
||||
email: string,
|
||||
nickname: string,
|
||||
avatarSkin: string
|
||||
): Promise<Player> {
|
||||
// 方法实现
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 复杂业务逻辑注释
|
||||
|
||||
对于复杂的业务逻辑,必须添加行内注释说明每个步骤的目的和处理逻辑。
|
||||
|
||||
**示例:**
|
||||
|
||||
```typescript
|
||||
async joinRoom(roomId: string, playerId: string): Promise<Room> {
|
||||
// 1. 参数验证 - 确保房间ID和玩家ID格式正确
|
||||
if (!roomId || !playerId) {
|
||||
this.logger.warn(`房间加入失败:参数无效`, { roomId, playerId });
|
||||
throw new BadRequestException('房间ID和玩家ID不能为空');
|
||||
}
|
||||
|
||||
// 2. 获取房间信息 - 检查房间是否存在
|
||||
const room = await this.roomRepository.findById(roomId);
|
||||
if (!room) {
|
||||
this.logger.warn(`房间加入失败:房间不存在`, { roomId, playerId });
|
||||
throw new NotFoundException('房间不存在');
|
||||
}
|
||||
|
||||
// 3. 检查房间状态 - 只有等待中的房间才能加入
|
||||
if (room.status !== RoomStatus.WAITING) {
|
||||
this.logger.warn(`房间加入失败:房间状态不允许加入`, {
|
||||
roomId,
|
||||
playerId,
|
||||
currentStatus: room.status
|
||||
});
|
||||
throw new BadRequestException('游戏已开始,无法加入房间');
|
||||
}
|
||||
|
||||
// 4. 检查房间容量 - 防止超过最大人数限制
|
||||
if (room.players.length >= room.maxPlayers) {
|
||||
this.logger.warn(`房间加入失败:房间已满`, {
|
||||
roomId,
|
||||
playerId,
|
||||
currentPlayers: room.players.length,
|
||||
maxPlayers: room.maxPlayers
|
||||
});
|
||||
throw new BadRequestException('房间已满');
|
||||
}
|
||||
|
||||
// 5. 检查玩家是否已在房间中 - 防止重复加入
|
||||
if (room.players.includes(playerId)) {
|
||||
this.logger.info(`玩家已在房间中,跳过加入操作`, { roomId, playerId });
|
||||
return room;
|
||||
}
|
||||
|
||||
// 6. 执行加入操作 - 更新房间玩家列表
|
||||
try {
|
||||
room.players.push(playerId);
|
||||
const updatedRoom = await this.roomRepository.save(room);
|
||||
|
||||
// 7. 记录成功日志
|
||||
this.logger.info(`玩家成功加入房间`, {
|
||||
roomId,
|
||||
playerId,
|
||||
currentPlayers: updatedRoom.players.length,
|
||||
maxPlayers: updatedRoom.maxPlayers
|
||||
});
|
||||
|
||||
return updatedRoom;
|
||||
} catch (error) {
|
||||
// 8. 异常处理 - 记录错误并抛出
|
||||
this.logger.error(`房间加入操作数据库错误`, {
|
||||
roomId,
|
||||
playerId,
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
throw new InternalServerErrorException('房间加入失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、业务逻辑设计原则
|
||||
|
||||
### 3.1 全面性原则
|
||||
|
||||
每个业务方法必须考虑所有可能的情况,包括正常流程、异常情况和边界条件。
|
||||
|
||||
**必须考虑的情况:**
|
||||
|
||||
| 类别 | 具体情况 | 处理方式 |
|
||||
|------|---------|---------|
|
||||
| **输入验证** | 参数为空、格式错误、超出范围 | 参数校验 + 异常抛出 |
|
||||
| **权限检查** | 未登录、权限不足、令牌过期 | 身份验证 + 权限验证 |
|
||||
| **资源状态** | 资源不存在、状态不正确、已被占用 | 状态检查 + 业务规则验证 |
|
||||
| **并发控制** | 同时操作、数据竞争、锁冲突 | 事务处理 + 乐观锁/悲观锁 |
|
||||
| **系统异常** | 数据库连接失败、网络超时、内存不足 | 异常捕获 + 降级处理 |
|
||||
| **业务规则** | 违反业务约束、超出限制、状态冲突 | 业务规则验证 + 友好提示 |
|
||||
|
||||
### 3.2 防御性编程
|
||||
|
||||
采用防御性编程思想,对所有外部输入和依赖进行验证和保护。
|
||||
|
||||
**实现要求:**
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 更新玩家信息 - 防御性编程示例
|
||||
*/
|
||||
async updatePlayerInfo(
|
||||
playerId: string,
|
||||
updateData: UpdatePlayerDto
|
||||
): Promise<Player> {
|
||||
// 1. 输入参数防御性检查
|
||||
if (!playerId) {
|
||||
this.logger.warn('更新玩家信息失败:玩家ID为空');
|
||||
throw new BadRequestException('玩家ID不能为空');
|
||||
}
|
||||
|
||||
if (!updateData || Object.keys(updateData).length === 0) {
|
||||
this.logger.warn('更新玩家信息失败:更新数据为空', { playerId });
|
||||
throw new BadRequestException('更新数据不能为空');
|
||||
}
|
||||
|
||||
// 2. 数据格式验证
|
||||
if (updateData.nickname) {
|
||||
if (updateData.nickname.length < 3 || updateData.nickname.length > 20) {
|
||||
this.logger.warn('更新玩家信息失败:昵称长度不符合要求', {
|
||||
playerId,
|
||||
nicknameLength: updateData.nickname.length
|
||||
});
|
||||
throw new BadRequestException('昵称长度必须在3-20字符之间');
|
||||
}
|
||||
}
|
||||
|
||||
if (updateData.avatarSkin) {
|
||||
const validSkins = ['1', '2', '3', '4', '5', '6', '7', '8'];
|
||||
if (!validSkins.includes(updateData.avatarSkin)) {
|
||||
this.logger.warn('更新玩家信息失败:角色皮肤ID无效', {
|
||||
playerId,
|
||||
avatarSkin: updateData.avatarSkin
|
||||
});
|
||||
throw new BadRequestException('角色皮肤ID必须在1-8之间');
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 玩家存在性检查
|
||||
const existingPlayer = await this.playerRepository.findById(playerId);
|
||||
if (!existingPlayer) {
|
||||
this.logger.warn('更新玩家信息失败:玩家不存在', { playerId });
|
||||
throw new NotFoundException('玩家不存在');
|
||||
}
|
||||
|
||||
// 4. 昵称唯一性检查(如果更新昵称)
|
||||
if (updateData.nickname && updateData.nickname !== existingPlayer.nickname) {
|
||||
const nicknameExists = await this.playerRepository.findByNickname(updateData.nickname);
|
||||
if (nicknameExists) {
|
||||
this.logger.warn('更新玩家信息失败:昵称已存在', {
|
||||
playerId,
|
||||
nickname: updateData.nickname
|
||||
});
|
||||
throw new ConflictException('昵称已被使用');
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 执行更新操作(使用事务保证数据一致性)
|
||||
try {
|
||||
const updatedPlayer = await this.playerRepository.update(playerId, updateData);
|
||||
|
||||
this.logger.info('玩家信息更新成功', {
|
||||
playerId,
|
||||
updatedFields: Object.keys(updateData),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return updatedPlayer;
|
||||
} catch (error) {
|
||||
this.logger.error('更新玩家信息数据库操作失败', {
|
||||
playerId,
|
||||
updateData,
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
throw new InternalServerErrorException('更新失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 异常处理策略
|
||||
|
||||
建立统一的异常处理策略,确保所有异常都能被正确捕获和处理。
|
||||
|
||||
**异常分类和处理:**
|
||||
|
||||
| 异常类型 | HTTP状态码 | 处理策略 | 日志级别 |
|
||||
|---------|-----------|---------|---------|
|
||||
| **BadRequestException** | 400 | 参数验证失败,返回具体错误信息 | WARN |
|
||||
| **UnauthorizedException** | 401 | 身份验证失败,要求重新登录 | WARN |
|
||||
| **ForbiddenException** | 403 | 权限不足,拒绝访问 | WARN |
|
||||
| **NotFoundException** | 404 | 资源不存在,返回友好提示 | WARN |
|
||||
| **ConflictException** | 409 | 资源冲突,如重复创建 | WARN |
|
||||
| **InternalServerErrorException** | 500 | 系统内部错误,记录详细日志 | ERROR |
|
||||
|
||||
---
|
||||
|
||||
## 四、日志系统使用指南
|
||||
|
||||
### 4.1 日志服务简介
|
||||
|
||||
项目使用统一的 `AppLoggerService` 提供日志记录功能,集成了 Pino 高性能日志库,支持自动敏感信息过滤和请求上下文绑定。
|
||||
|
||||
### 4.2 在服务中使用日志
|
||||
|
||||
**依赖注入:**
|
||||
|
||||
```typescript
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AppLoggerService } from '../core/utils/logger/logger.service';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(
|
||||
private readonly logger: AppLoggerService
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 日志级别和使用场景
|
||||
|
||||
| 级别 | 使用场景 | 示例 |
|
||||
|------|---------|------|
|
||||
| **ERROR** | 系统错误、异常情况、数据库连接失败 | 数据库连接超时、第三方服务调用失败 |
|
||||
| **WARN** | 业务警告、参数错误、权限不足 | 用户输入无效参数、尝试访问不存在的资源 |
|
||||
| **INFO** | 重要业务操作、状态变更、关键流程 | 用户登录成功、房间创建、玩家加入广场 |
|
||||
| **DEBUG** | 调试信息、详细执行流程 | 方法调用参数、中间计算结果 |
|
||||
| **FATAL** | 致命错误、系统不可用 | 数据库完全不可用、关键服务宕机 |
|
||||
| **TRACE** | 极细粒度调试信息 | 循环内的变量状态、算法执行步骤 |
|
||||
|
||||
### 4.4 标准日志格式
|
||||
|
||||
**推荐的日志上下文格式:**
|
||||
|
||||
```typescript
|
||||
// 成功操作日志
|
||||
this.logger.info('操作描述', {
|
||||
operation: '操作类型',
|
||||
userId: '用户ID',
|
||||
resourceId: '资源ID',
|
||||
params: '关键参数',
|
||||
result: '操作结果',
|
||||
duration: '执行时间(ms)',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// 警告日志
|
||||
this.logger.warn('警告描述', {
|
||||
operation: '操作类型',
|
||||
userId: '用户ID',
|
||||
reason: '警告原因',
|
||||
params: '相关参数',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// 错误日志
|
||||
this.logger.error('错误描述', {
|
||||
operation: '操作类型',
|
||||
userId: '用户ID',
|
||||
error: error.message,
|
||||
params: '相关参数',
|
||||
timestamp: new Date().toISOString()
|
||||
}, error.stack);
|
||||
```
|
||||
|
||||
### 4.5 请求上下文绑定
|
||||
|
||||
**在 Controller 中使用:**
|
||||
|
||||
```typescript
|
||||
@Controller('users')
|
||||
export class UserController {
|
||||
constructor(private readonly logger: AppLoggerService) {}
|
||||
|
||||
@Get(':id')
|
||||
async getUser(@Param('id') id: string, @Req() req: Request) {
|
||||
// 绑定请求上下文
|
||||
const requestLogger = this.logger.bindRequest(req, 'UserController');
|
||||
|
||||
requestLogger.info('开始获取用户信息', { userId: id });
|
||||
|
||||
try {
|
||||
const user = await this.userService.findById(id);
|
||||
requestLogger.info('用户信息获取成功', { userId: id });
|
||||
return user;
|
||||
} catch (error) {
|
||||
requestLogger.error('用户信息获取失败', error.stack, {
|
||||
userId: id,
|
||||
reason: error.message
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.6 业务方法日志记录最佳实践
|
||||
|
||||
**完整的业务方法日志记录示例:**
|
||||
|
||||
```typescript
|
||||
async createPlayer(email: string, nickname: string): Promise<Player> {
|
||||
const startTime = Date.now();
|
||||
|
||||
this.logger.info('开始创建玩家', {
|
||||
operation: 'createPlayer',
|
||||
email,
|
||||
nickname,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
try {
|
||||
// 1. 参数验证
|
||||
if (!email || !nickname) {
|
||||
this.logger.warn('创建玩家失败:参数无效', {
|
||||
operation: 'createPlayer',
|
||||
email,
|
||||
nickname,
|
||||
reason: 'invalid_parameters'
|
||||
});
|
||||
throw new BadRequestException('邮箱和昵称不能为空');
|
||||
}
|
||||
|
||||
// 2. 邮箱格式验证
|
||||
if (!this.isValidEmail(email)) {
|
||||
this.logger.warn('创建玩家失败:邮箱格式无效', {
|
||||
operation: 'createPlayer',
|
||||
email,
|
||||
nickname
|
||||
});
|
||||
throw new BadRequestException('邮箱格式不正确');
|
||||
}
|
||||
|
||||
// 3. 检查邮箱是否已存在
|
||||
const existingPlayer = await this.playerRepository.findByEmail(email);
|
||||
if (existingPlayer) {
|
||||
this.logger.warn('创建玩家失败:邮箱已存在', {
|
||||
operation: 'createPlayer',
|
||||
email,
|
||||
nickname,
|
||||
existingPlayerId: existingPlayer.id
|
||||
});
|
||||
throw new ConflictException('邮箱已被使用');
|
||||
}
|
||||
|
||||
// 4. 创建玩家
|
||||
const player = await this.playerRepository.create({
|
||||
email,
|
||||
nickname,
|
||||
avatarSkin: '1', // 默认皮肤
|
||||
createTime: new Date()
|
||||
});
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.logger.info('玩家创建成功', {
|
||||
operation: 'createPlayer',
|
||||
playerId: player.id,
|
||||
email,
|
||||
nickname,
|
||||
duration,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return player;
|
||||
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (error instanceof BadRequestException ||
|
||||
error instanceof ConflictException) {
|
||||
// 业务异常,重新抛出
|
||||
throw error;
|
||||
}
|
||||
|
||||
// 系统异常,记录详细日志
|
||||
this.logger.error('创建玩家系统异常', {
|
||||
operation: 'createPlayer',
|
||||
email,
|
||||
nickname,
|
||||
error: error.message,
|
||||
duration,
|
||||
timestamp: new Date().toISOString()
|
||||
}, error.stack);
|
||||
|
||||
throw new InternalServerErrorException('创建玩家失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.7 必须记录日志的操作
|
||||
|
||||
| 操作类型 | 日志级别 | 记录内容 |
|
||||
|---------|---------|---------|
|
||||
| **用户认证** | INFO/WARN | 登录成功/失败、令牌生成/验证 |
|
||||
| **数据变更** | INFO | 创建、更新、删除操作 |
|
||||
| **权限检查** | WARN | 权限验证失败、非法访问尝试 |
|
||||
| **系统异常** | ERROR | 异常堆栈、错误上下文、影响范围 |
|
||||
| **性能监控** | INFO | 慢查询、高并发操作、资源使用 |
|
||||
| **安全事件** | WARN/ERROR | 恶意请求、频繁操作、异常行为 |
|
||||
|
||||
### 4.8 敏感信息保护
|
||||
|
||||
日志系统会自动过滤以下敏感字段:
|
||||
- `password` - 密码
|
||||
- `token` - 令牌
|
||||
- `secret` - 密钥
|
||||
- `authorization` - 授权信息
|
||||
- `cardNo` - 卡号
|
||||
|
||||
**注意:** 包含这些关键词的字段会被自动替换为 `[REDACTED]`
|
||||
|
||||
---
|
||||
|
||||
## 五、代码审查检查清单
|
||||
|
||||
### 5.1 注释检查
|
||||
|
||||
- [ ] 模块文件包含完整的模块级注释
|
||||
- [ ] 每个类都有详细的类级注释
|
||||
- [ ] 每个公共方法都有完整的方法注释
|
||||
- [ ] 复杂业务逻辑有行内注释说明
|
||||
- [ ] 注释内容准确,与代码实现一致
|
||||
|
||||
### 5.2 业务逻辑检查
|
||||
|
||||
- [ ] 考虑了所有可能的输入情况
|
||||
- [ ] 包含完整的参数验证
|
||||
- [ ] 处理了所有可能的异常情况
|
||||
- [ ] 实现了适当的权限检查
|
||||
- [ ] 考虑了并发和竞态条件
|
||||
|
||||
### 5.3 日志记录检查
|
||||
|
||||
- [ ] 关键业务操作都有日志记录
|
||||
- [ ] 日志级别使用正确
|
||||
- [ ] 日志格式符合规范
|
||||
- [ ] 包含足够的上下文信息
|
||||
- [ ] 敏感信息已脱敏处理
|
||||
|
||||
### 5.4 异常处理检查
|
||||
|
||||
- [ ] 所有异常都被正确捕获
|
||||
- [ ] 异常类型选择合适
|
||||
- [ ] 异常信息对用户友好
|
||||
- [ ] 系统异常有详细的错误日志
|
||||
- [ ] 不会泄露敏感的系统信息
|
||||
|
||||
---
|
||||
|
||||
## 六、最佳实践示例
|
||||
|
||||
### 6.1 完整的服务类示例
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 广场管理服务
|
||||
*
|
||||
* 功能描述:
|
||||
* - 管理中央广场的玩家状态和位置同步
|
||||
* - 处理玩家进入和离开广场的逻辑
|
||||
* - 维护广场在线玩家列表(最多50人)
|
||||
*
|
||||
* 依赖模块:
|
||||
* - PlayerService: 玩家信息服务
|
||||
* - WebSocketGateway: WebSocket通信网关
|
||||
* - RedisService: 缓存服务
|
||||
* - LoggerService: 日志记录服务
|
||||
*
|
||||
* @author 开发团队
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-13
|
||||
*/
|
||||
@Injectable()
|
||||
export class PlazaService {
|
||||
private readonly logger = new Logger(PlazaService.name);
|
||||
private readonly MAX_PLAYERS = 50;
|
||||
|
||||
constructor(
|
||||
private readonly playerService: PlayerService,
|
||||
private readonly redisService: RedisService,
|
||||
private readonly webSocketGateway: WebSocketGateway
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 玩家进入广场
|
||||
*
|
||||
* 功能描述:
|
||||
* 处理玩家进入中央广场的逻辑,包括人数限制检查、位置分配和状态同步
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证玩家身份和权限
|
||||
* 2. 检查广场当前人数是否超限
|
||||
* 3. 为玩家分配初始位置
|
||||
* 4. 更新Redis中的在线玩家列表
|
||||
* 5. 向其他玩家广播新玩家进入消息
|
||||
* 6. 向新玩家发送当前广场状态
|
||||
*
|
||||
* @param playerId 玩家ID,必须是有效的已注册玩家
|
||||
* @param socketId WebSocket连接ID,用于消息推送
|
||||
* @returns Promise<PlazaPlayerInfo> 玩家在广场的信息
|
||||
*
|
||||
* @throws UnauthorizedException 当玩家身份验证失败时
|
||||
* @throws BadRequestException 当广场人数已满时
|
||||
* @throws InternalServerErrorException 当系统操作失败时
|
||||
*/
|
||||
async enterPlaza(playerId: string, socketId: string): Promise<PlazaPlayerInfo> {
|
||||
const startTime = Date.now();
|
||||
|
||||
this.logger.info('玩家尝试进入广场', {
|
||||
operation: 'enterPlaza',
|
||||
playerId,
|
||||
socketId,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
try {
|
||||
// 1. 验证玩家身份
|
||||
const player = await this.playerService.getPlayerById(playerId);
|
||||
if (!player) {
|
||||
this.logger.warn('进入广场失败:玩家不存在', {
|
||||
operation: 'enterPlaza',
|
||||
playerId,
|
||||
socketId
|
||||
});
|
||||
throw new UnauthorizedException('玩家身份验证失败');
|
||||
}
|
||||
|
||||
// 2. 检查广场人数限制
|
||||
const currentPlayers = await this.redisService.scard('plaza:online_players');
|
||||
if (currentPlayers >= this.MAX_PLAYERS) {
|
||||
this.logger.warn('进入广场失败:人数已满', {
|
||||
operation: 'enterPlaza',
|
||||
playerId,
|
||||
currentPlayers,
|
||||
maxPlayers: this.MAX_PLAYERS
|
||||
});
|
||||
throw new BadRequestException('广场人数已满,请稍后再试');
|
||||
}
|
||||
|
||||
// 3. 检查玩家是否已在广场中
|
||||
const isAlreadyInPlaza = await this.redisService.sismember('plaza:online_players', playerId);
|
||||
if (isAlreadyInPlaza) {
|
||||
this.logger.info('玩家已在广场中,更新连接信息', {
|
||||
operation: 'enterPlaza',
|
||||
playerId,
|
||||
socketId
|
||||
});
|
||||
|
||||
// 更新Socket连接映射
|
||||
await this.redisService.hset('plaza:player_sockets', playerId, socketId);
|
||||
|
||||
// 获取当前位置信息
|
||||
const existingInfo = await this.redisService.hget('plaza:player_positions', playerId);
|
||||
return JSON.parse(existingInfo);
|
||||
}
|
||||
|
||||
// 4. 为玩家分配初始位置(广场中心附近随机位置)
|
||||
const initialPosition = this.generateInitialPosition();
|
||||
|
||||
const playerInfo: PlazaPlayerInfo = {
|
||||
playerId: player.id,
|
||||
nickname: player.nickname,
|
||||
avatarSkin: player.avatarSkin,
|
||||
position: initialPosition,
|
||||
lastUpdate: new Date(),
|
||||
socketId
|
||||
};
|
||||
|
||||
// 5. 更新Redis中的玩家状态
|
||||
await Promise.all([
|
||||
this.redisService.sadd('plaza:online_players', playerId),
|
||||
this.redisService.hset('plaza:player_positions', playerId, JSON.stringify(playerInfo)),
|
||||
this.redisService.hset('plaza:player_sockets', playerId, socketId),
|
||||
this.redisService.expire('plaza:player_positions', 3600), // 1小时过期
|
||||
this.redisService.expire('plaza:player_sockets', 3600)
|
||||
]);
|
||||
|
||||
// 6. 向其他玩家广播新玩家进入消息
|
||||
this.webSocketGateway.broadcastToPlaza('player_entered', {
|
||||
playerId: player.id,
|
||||
nickname: player.nickname,
|
||||
avatarSkin: player.avatarSkin,
|
||||
position: initialPosition
|
||||
}, socketId); // 排除新进入的玩家
|
||||
|
||||
// 7. 向新玩家发送当前广场状态
|
||||
const allPlayers = await this.getAllPlazaPlayers();
|
||||
this.webSocketGateway.sendToPlayer(socketId, 'plaza_state', {
|
||||
players: allPlayers.filter(p => p.playerId !== playerId),
|
||||
totalPlayers: allPlayers.length
|
||||
});
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.logger.info('玩家成功进入广场', {
|
||||
operation: 'enterPlaza',
|
||||
playerId,
|
||||
socketId,
|
||||
position: initialPosition,
|
||||
totalPlayers: currentPlayers + 1,
|
||||
duration,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return playerInfo;
|
||||
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (error instanceof UnauthorizedException ||
|
||||
error instanceof BadRequestException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logger.error('玩家进入广场系统异常', {
|
||||
operation: 'enterPlaza',
|
||||
playerId,
|
||||
socketId,
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
duration,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
throw new InternalServerErrorException('进入广场失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成初始位置
|
||||
*
|
||||
* 功能描述:
|
||||
* 在广场中心附近生成随机的初始位置,避免玩家重叠
|
||||
*
|
||||
* @returns Position 包含x、y坐标的位置对象
|
||||
* @private
|
||||
*/
|
||||
private generateInitialPosition(): Position {
|
||||
// 广场中心坐标 (400, 300),在半径100像素范围内随机分配
|
||||
const centerX = 400;
|
||||
const centerY = 300;
|
||||
const radius = 100;
|
||||
|
||||
const angle = Math.random() * 2 * Math.PI;
|
||||
const distance = Math.random() * radius;
|
||||
|
||||
const x = Math.round(centerX + distance * Math.cos(angle));
|
||||
const y = Math.round(centerY + distance * Math.sin(angle));
|
||||
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有广场玩家信息
|
||||
*
|
||||
* @returns Promise<PlazaPlayerInfo[]> 广场中所有玩家的信息列表
|
||||
* @private
|
||||
*/
|
||||
private async getAllPlazaPlayers(): Promise<PlazaPlayerInfo[]> {
|
||||
try {
|
||||
const playerIds = await this.redisService.smembers('plaza:online_players');
|
||||
const playerInfos = await Promise.all(
|
||||
playerIds.map(async (playerId) => {
|
||||
const info = await this.redisService.hget('plaza:player_positions', playerId);
|
||||
return info ? JSON.parse(info) : null;
|
||||
})
|
||||
);
|
||||
|
||||
return playerInfos.filter(info => info !== null);
|
||||
} catch (error) {
|
||||
this.logger.error('获取广场玩家列表失败', {
|
||||
operation: 'getAllPlazaPlayers',
|
||||
error: error.message
|
||||
});
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、工具和配置
|
||||
|
||||
### 7.1 推荐的开发工具
|
||||
|
||||
| 工具 | 用途 | 配置说明 |
|
||||
|------|------|---------|
|
||||
| **ESLint** | 代码规范检查 | 配置注释规范检查规则 |
|
||||
| **Prettier** | 代码格式化 | 统一代码格式 |
|
||||
| **TSDoc** | 文档生成 | 从注释生成API文档 |
|
||||
| **SonarQube** | 代码质量分析 | 检查代码覆盖率和复杂度 |
|
||||
|
||||
### 7.2 日志配置示例
|
||||
|
||||
```typescript
|
||||
// logger.config.ts
|
||||
export const loggerConfig = {
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.json()
|
||||
),
|
||||
transports: [
|
||||
new winston.transports.Console(),
|
||||
new winston.transports.File({
|
||||
filename: 'logs/error.log',
|
||||
level: 'error'
|
||||
}),
|
||||
new winston.transports.File({
|
||||
filename: 'logs/combined.log'
|
||||
})
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、总结
|
||||
|
||||
本规范文档定义了后端开发的核心要求:
|
||||
|
||||
1. **完整的注释体系**:模块、类、方法三级注释,确保代码可读性
|
||||
2. **全面的业务逻辑**:考虑所有可能情况,实现防御性编程
|
||||
3. **规范的日志记录**:关键操作必须记录,便于问题排查和系统监控
|
||||
4. **统一的异常处理**:分类处理不同类型异常,提供友好的用户体验
|
||||
|
||||
遵循本规范将显著提高代码质量、系统稳定性和团队协作效率。所有开发人员必须严格按照本规范进行开发,代码审查时将重点检查这些方面的实现。
|
||||
418
docs/deployment/DEPLOYMENT.md
Normal file
418
docs/deployment/DEPLOYMENT.md
Normal file
@@ -0,0 +1,418 @@
|
||||
# 🚀 Whale Town 部署指南
|
||||
|
||||
本文档详细说明如何部署 Whale Town 像素游戏后端服务到生产环境。
|
||||
|
||||
## 📋 前置要求
|
||||
|
||||
### 基础环境
|
||||
- **Node.js** 18+ (推荐 20.x LTS)
|
||||
- **pnpm** 包管理器
|
||||
- **MySQL** 8.0+
|
||||
- **Redis** 6.0+ (可选,支持文件存储模式)
|
||||
- **PM2** 进程管理器(推荐)
|
||||
- **Nginx** 反向代理(推荐)
|
||||
|
||||
### 新增要求 (管理员后台)
|
||||
- **Web服务器** (Nginx/Apache) - 用于前端管理界面
|
||||
- **SSL证书** (推荐) - 保护管理后台安全
|
||||
|
||||
## 部署步骤
|
||||
|
||||
### 1. 服务器环境准备
|
||||
|
||||
```bash
|
||||
# 安装 Node.js (使用 NodeSource 仓库)
|
||||
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
|
||||
# 安装 pnpm
|
||||
curl -fsSL https://get.pnpm.io/install.sh | sh
|
||||
source ~/.bashrc
|
||||
|
||||
# 安装 PM2
|
||||
npm install -g pm2
|
||||
|
||||
# 安装 MySQL
|
||||
sudo apt update
|
||||
sudo apt install mysql-server
|
||||
sudo mysql_secure_installation
|
||||
```
|
||||
|
||||
### 2. 克隆项目
|
||||
|
||||
```bash
|
||||
# 创建项目目录
|
||||
sudo mkdir -p /var/www
|
||||
cd /var/www
|
||||
|
||||
# 克隆项目(替换为你的实际仓库地址)
|
||||
git clone https://gitea.xinghangee.icu/datawhale/whale-town-end.git
|
||||
cd whale-town-end
|
||||
```
|
||||
|
||||
### 3. 配置环境
|
||||
|
||||
```bash
|
||||
# 复制环境配置文件
|
||||
cp .env.production.example .env.production
|
||||
|
||||
# 编辑环境配置(填入实际的数据库信息)
|
||||
nano .env.production
|
||||
|
||||
# 复制部署脚本
|
||||
cp deploy.sh.example deploy.sh
|
||||
chmod +x deploy.sh
|
||||
|
||||
# 编辑部署脚本(修改路径配置)
|
||||
nano deploy.sh
|
||||
|
||||
# 复制 webhook 处理器
|
||||
cp webhook-handler.js.example webhook-handler.js
|
||||
|
||||
# 编辑 webhook 处理器(修改密钥和路径)
|
||||
nano webhook-handler.js
|
||||
```
|
||||
|
||||
### 4. 数据库设置
|
||||
|
||||
```bash
|
||||
# 登录 MySQL
|
||||
sudo mysql -u root -p
|
||||
|
||||
# 创建数据库和用户
|
||||
CREATE DATABASE pixel_game_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
CREATE USER 'pixel_game'@'localhost' IDENTIFIED BY 'your_secure_password';
|
||||
GRANT ALL PRIVILEGES ON pixel_game_db.* TO 'pixel_game'@'localhost';
|
||||
FLUSH PRIVILEGES;
|
||||
EXIT;
|
||||
```
|
||||
|
||||
### 5. 安装依赖和构建
|
||||
|
||||
```bash
|
||||
# 安装后端依赖
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
# 安装前端依赖 (新增)
|
||||
cd client
|
||||
pnpm install --frozen-lockfile
|
||||
cd ..
|
||||
|
||||
# 构建后端
|
||||
pnpm run build
|
||||
|
||||
# 构建前端管理界面 (新增)
|
||||
cd client
|
||||
pnpm run build
|
||||
cd ..
|
||||
```
|
||||
|
||||
### 6. 启动服务
|
||||
|
||||
```bash
|
||||
# 使用 PM2 启动应用
|
||||
pm2 start ecosystem.config.js --env production
|
||||
|
||||
# 保存 PM2 配置
|
||||
pm2 save
|
||||
|
||||
# 设置开机自启
|
||||
pm2 startup
|
||||
# 按照提示执行显示的命令
|
||||
```
|
||||
|
||||
### 7. 配置 Nginx
|
||||
|
||||
#### 方案一: 分离部署 (推荐)
|
||||
|
||||
创建后端API配置:
|
||||
```bash
|
||||
sudo nano /etc/nginx/sites-available/whale-town-api
|
||||
```
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name api.whaletown.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
创建前端管理界面配置:
|
||||
```bash
|
||||
sudo nano /etc/nginx/sites-available/whale-town-admin
|
||||
```
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name admin.whaletown.com;
|
||||
root /var/www/whale-town-end/client/dist;
|
||||
index index.html;
|
||||
|
||||
# SPA路由支持
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# API代理
|
||||
location /api/ {
|
||||
proxy_pass http://api.whaletown.com/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
# 静态资源缓存
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 方案二: 单域名部署
|
||||
|
||||
创建统一配置:
|
||||
```bash
|
||||
sudo nano /etc/nginx/sites-available/whale-town-unified
|
||||
```
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name whaletown.com;
|
||||
|
||||
# API接口
|
||||
location /api/ {
|
||||
proxy_pass http://localhost:3000/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# 管理后台
|
||||
location /admin/ {
|
||||
alias /var/www/whale-town-end/client/dist/;
|
||||
try_files $uri $uri/ /admin/index.html;
|
||||
}
|
||||
|
||||
# 主站点 (可选)
|
||||
location / {
|
||||
proxy_pass http://localhost:3000;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
启用配置:
|
||||
```bash
|
||||
# 启用站点
|
||||
sudo ln -s /etc/nginx/sites-available/whale-town-* /etc/nginx/sites-enabled/
|
||||
|
||||
# 测试配置
|
||||
sudo nginx -t
|
||||
|
||||
# 重载配置
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
## 🔒 SSL证书配置 (推荐)
|
||||
|
||||
### 使用 Let's Encrypt
|
||||
```bash
|
||||
# 安装 Certbot
|
||||
sudo apt install certbot python3-certbot-nginx
|
||||
|
||||
# 为API域名申请证书
|
||||
sudo certbot --nginx -d api.whaletown.com
|
||||
|
||||
# 为管理后台申请证书
|
||||
sudo certbot --nginx -d admin.whaletown.com
|
||||
|
||||
# 设置自动续期
|
||||
sudo crontab -e
|
||||
# 添加: 0 12 * * * /usr/bin/certbot renew --quiet
|
||||
```
|
||||
|
||||
## 🎛️ 管理员后台配置
|
||||
|
||||
### 环境变量配置
|
||||
在 `.env.production` 中添加:
|
||||
```bash
|
||||
# 管理员Token配置 (必须)
|
||||
ADMIN_TOKEN_SECRET=your_super_strong_random_secret_at_least_32_chars
|
||||
ADMIN_TOKEN_TTL_SECONDS=28800
|
||||
|
||||
# 首次部署启用管理员引导
|
||||
ADMIN_BOOTSTRAP_ENABLED=true
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=YourStrongPassword123!
|
||||
ADMIN_NICKNAME=系统管理员
|
||||
|
||||
# CORS配置 (如果前后端分离)
|
||||
CORS_ORIGIN=https://admin.whaletown.com
|
||||
```
|
||||
|
||||
### 访问管理后台
|
||||
- **地址**: https://admin.whaletown.com
|
||||
- **默认账号**: admin / YourStrongPassword123!
|
||||
|
||||
**⚠️ 重要**: 首次登录后立即修改密码并关闭引导功能 (`ADMIN_BOOTSTRAP_ENABLED=false`)
|
||||
|
||||
## 📡 Gitea Webhook 配置
|
||||
1. 在 Gitea 仓库中进入 **Settings** → **Webhooks**
|
||||
3. 配置:
|
||||
- **Target URL**: `http://your-server.com:9000/webhook` 或 `http://your-domain.com/webhook`
|
||||
- **HTTP Method**: `POST`
|
||||
- **POST Content Type**: `application/json`
|
||||
- **Secret**: 与 `webhook-handler.js` 中的 `SECRET` 一致
|
||||
- **Trigger On**: 选择 `Push events`
|
||||
- **Branch filter**: `main`
|
||||
|
||||
## ✅ 验证部署
|
||||
|
||||
### 基础服务检查
|
||||
```bash
|
||||
# 检查PM2服务状态
|
||||
pm2 status
|
||||
|
||||
# 检查后端API
|
||||
curl http://localhost:3000/
|
||||
curl http://localhost:3000/api-docs
|
||||
|
||||
# 检查前端管理界面
|
||||
curl -I https://admin.whaletown.com
|
||||
```
|
||||
|
||||
### 管理员后台测试
|
||||
```bash
|
||||
# 测试管理员登录API
|
||||
curl -X POST https://api.whaletown.com/admin/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"identifier":"admin","password":"YourStrongPassword123!"}'
|
||||
|
||||
# 访问管理界面
|
||||
# 浏览器打开: https://admin.whaletown.com
|
||||
```
|
||||
|
||||
### 功能验证清单
|
||||
- [ ] 后端API服务正常响应
|
||||
- [ ] API文档可访问
|
||||
- [ ] 前端管理界面加载正常
|
||||
- [ ] 管理员登录功能正常
|
||||
- [ ] 用户管理功能正常
|
||||
- [ ] 日志查看功能正常
|
||||
- [ ] SSL证书配置正确
|
||||
|
||||
## 🔧 常用命令
|
||||
|
||||
### 服务管理
|
||||
```bash
|
||||
# 重启后端服务
|
||||
pm2 restart whale-town-end
|
||||
|
||||
# 重启前端服务 (如果使用PM2托管)
|
||||
pm2 restart whale-town-admin
|
||||
|
||||
# 查看服务日志
|
||||
pm2 logs whale-town-end --lines 100
|
||||
pm2 logs whale-town-admin --lines 100
|
||||
|
||||
# 手动部署
|
||||
bash deploy.sh
|
||||
```
|
||||
|
||||
### 更新部署
|
||||
```bash
|
||||
# 更新后端
|
||||
git pull origin main
|
||||
pnpm install
|
||||
pnpm run build
|
||||
pm2 reload whale-town-end
|
||||
|
||||
# 更新前端管理界面
|
||||
cd client
|
||||
git pull origin main
|
||||
pnpm install
|
||||
pnpm run build
|
||||
sudo systemctl reload nginx
|
||||
cd ..
|
||||
```
|
||||
|
||||
### 日志管理
|
||||
```bash
|
||||
# 查看应用日志
|
||||
tail -f logs/app.log
|
||||
|
||||
# 查看管理员操作日志
|
||||
tail -f logs/admin.log
|
||||
|
||||
# 查看Nginx日志
|
||||
sudo tail -f /var/log/nginx/access.log
|
||||
sudo tail -f /var/log/nginx/error.log
|
||||
```
|
||||
|
||||
## 🚨 故障排除
|
||||
|
||||
### 后端服务问题
|
||||
**服务无法启动**
|
||||
- 检查环境变量配置 (`cat .env.production`)
|
||||
- 检查数据库连接 (`mysql -u pixel_game -p`)
|
||||
- 查看PM2日志 (`pm2 logs whale-town-end`)
|
||||
- 检查端口占用 (`netstat -tlnp | grep 3000`)
|
||||
|
||||
**管理员登录失败**
|
||||
- 验证 `ADMIN_TOKEN_SECRET` 配置
|
||||
- 检查管理员账号是否创建
|
||||
- 查看后端错误日志
|
||||
- 确认密码复杂度要求
|
||||
|
||||
### 前端管理界面问题
|
||||
**界面无法访问**
|
||||
- 检查前端构建是否成功 (`ls -la client/dist/`)
|
||||
- 验证Nginx配置 (`sudo nginx -t`)
|
||||
- 检查域名解析
|
||||
- 查看Nginx错误日志
|
||||
|
||||
**API请求失败**
|
||||
- 检查CORS配置
|
||||
- 验证API代理设置
|
||||
- 确认后端服务状态
|
||||
- 检查防火墙规则
|
||||
|
||||
### 数据库连接问题
|
||||
**连接失败**
|
||||
- 检查MySQL服务状态 (`sudo systemctl status mysql`)
|
||||
- 验证数据库用户权限
|
||||
- 检查网络连接
|
||||
- 确认数据库配置
|
||||
|
||||
### SSL证书问题
|
||||
**证书验证失败**
|
||||
- 检查证书有效期 (`sudo certbot certificates`)
|
||||
- 验证域名解析
|
||||
- 重新申请证书 (`sudo certbot --nginx -d your-domain.com`)
|
||||
|
||||
### 性能问题
|
||||
**响应缓慢**
|
||||
- 检查系统资源使用 (`htop`, `df -h`)
|
||||
- 优化数据库查询
|
||||
- 配置Redis缓存
|
||||
- 启用Nginx压缩
|
||||
|
||||
### 日志文件过大
|
||||
**磁盘空间不足**
|
||||
- 配置日志轮转 (`sudo nano /etc/logrotate.d/whale-town`)
|
||||
- 清理旧日志文件
|
||||
- 监控磁盘使用情况
|
||||
2101
docs/development/AI代码检查规范.md
Normal file
2101
docs/development/AI代码检查规范.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -37,6 +37,35 @@
|
||||
| [后端开发规范](./backend_development_guide.md) | 注释、日志、业务逻辑 | 注释完整性、日志规范 |
|
||||
| [NestJS 使用指南](./nestjs_guide.md) | 框架最佳实践 | 架构设计、代码组织 |
|
||||
|
||||
**📝 重要:修改记录注释规范**
|
||||
|
||||
当对现有文件进行修改时,必须在文件头注释中添加修改记录,格式如下:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 文件功能描述
|
||||
*
|
||||
* 最近修改:
|
||||
* - YYYY-MM-DD: 修改类型 - 具体修改内容描述
|
||||
* - YYYY-MM-DD: 修改类型 - 具体修改内容描述
|
||||
*
|
||||
* @author 原作者
|
||||
* @version x.x.x (修改后递增版本号)
|
||||
* @since 创建日期
|
||||
* @lastModified 最后修改日期
|
||||
*/
|
||||
```
|
||||
|
||||
**📏 重要限制:修改记录只保留最近5次修改,超出时删除最旧记录,保持注释简洁。**
|
||||
|
||||
**修改类型包括:**
|
||||
- `代码规范优化` - 命名规范、注释规范、代码清理等
|
||||
- `功能新增` - 添加新的功能或方法
|
||||
- `功能修改` - 修改现有功能的实现
|
||||
- `Bug修复` - 修复代码缺陷
|
||||
- `性能优化` - 提升代码性能
|
||||
- `重构` - 代码结构调整但功能不变
|
||||
|
||||
---
|
||||
|
||||
## 🤖 AI 辅助开发工作流程
|
||||
@@ -89,6 +118,7 @@
|
||||
- 模块级注释(功能描述、依赖模块、作者、版本)
|
||||
- 类级注释(职责、主要方法、使用场景)
|
||||
- 方法级注释(功能描述、业务逻辑、参数、返回值、异常、示例)
|
||||
- 修改记录注释(如果是修改现有文件,添加修改记录和更新版本号)
|
||||
|
||||
2. 按照命名规范:
|
||||
- 类名使用大驼峰
|
||||
@@ -229,6 +259,7 @@
|
||||
□ 模块级注释(功能描述、依赖模块、作者、版本)
|
||||
□ 类级注释(职责、主要方法、使用场景)
|
||||
□ 方法级注释(功能描述、业务逻辑、参数、返回值、异常、示例)
|
||||
□ 修改记录注释(如果是修改现有文件,必须添加修改记录和更新版本号,只保留最近5次修改)
|
||||
□ 文件命名使用下划线分隔
|
||||
□ 类名使用大驼峰命名
|
||||
□ 方法名使用小驼峰命名
|
||||
@@ -312,7 +343,50 @@ AI 会生成包含完整注释和异常处理的代码。
|
||||
请按照 Git 提交规范生成提交信息。
|
||||
```
|
||||
|
||||
### 案例2:代码审查场景
|
||||
### 案例3:修改现有文件规范
|
||||
|
||||
#### 修改现有代码时的注释更新
|
||||
|
||||
```
|
||||
我需要修改现有的 login_core.service.ts 文件,进行以下优化:
|
||||
- 清理未使用的导入 (EmailSendResult, crypto)
|
||||
- 修复常量命名 (saltRounds -> SALT_ROUNDS)
|
||||
- 删除未使用的私有方法 (generateVerificationCode)
|
||||
|
||||
请帮我:
|
||||
1. 在文件头注释中添加修改记录
|
||||
2. 更新版本号 (1.0.0 -> 1.0.1)
|
||||
3. 添加 @lastModified 标记
|
||||
4. 确保修改记录格式符合规范
|
||||
5. 只保留最近5次修改记录,保持注释简洁
|
||||
|
||||
修改记录格式要求:
|
||||
- 日期格式:YYYY-MM-DD
|
||||
- 修改类型:代码规范优化
|
||||
- 描述要具体明确
|
||||
- 最多保留5条记录
|
||||
```
|
||||
|
||||
#### AI 生成的修改记录示例
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 登录核心服务
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2025-01-07: 代码规范优化 - 清理未使用的导入(EmailSendResult, crypto)
|
||||
* - 2025-01-07: 代码规范优化 - 修复常量命名(saltRounds -> SALT_ROUNDS)
|
||||
* - 2025-01-07: 代码规范优化 - 删除未使用的私有方法(generateVerificationCode)
|
||||
* - 2025-01-07: 代码质量提升 - 确保完全符合项目命名规范和注释规范
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @since 2025-12-17
|
||||
* @lastModified 2025-01-07
|
||||
*/
|
||||
```
|
||||
|
||||
### 案例4:代码审查场景
|
||||
|
||||
#### 现有代码检查
|
||||
|
||||
@@ -380,6 +454,14 @@ AI 会生成包含完整注释和异常处理的代码。
|
||||
- 日志记录
|
||||
- 规范命名
|
||||
|
||||
## 代码修改模板
|
||||
修改现有文件时,请:
|
||||
- 在文件头注释添加修改记录
|
||||
- 更新版本号(递增小版本号)
|
||||
- 添加 @lastModified 标记
|
||||
- 修改记录格式:YYYY-MM-DD: 修改类型 - 具体描述
|
||||
- 只保留最近5次修改记录,保持注释简洁
|
||||
|
||||
## 代码检查模板
|
||||
请检查代码规范符合性:
|
||||
[保存检查清单]
|
||||
@@ -397,6 +479,7 @@ AI 会生成包含完整注释和异常处理的代码。
|
||||
3. 异常处理模板
|
||||
4. 日志记录模板
|
||||
5. 参数验证模板
|
||||
6. 文件修改记录注释模板
|
||||
|
||||
每个模板都要包含完整的注释和最佳实践。
|
||||
```
|
||||
276
docs/development/TESTING.md
Normal file
276
docs/development/TESTING.md
Normal file
@@ -0,0 +1,276 @@
|
||||
# 测试指南
|
||||
|
||||
本项目支持**无数据库和无邮件服务器**的测试模式,让你可以快速验证所有功能!
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 环境配置
|
||||
|
||||
```bash
|
||||
# 复制环境配置文件
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
默认配置已经设置为测试模式,无需修改即可使用。
|
||||
|
||||
### 2. 启动服务
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 启动开发服务器
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 3. 运行测试
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
.\test-api.ps1
|
||||
```
|
||||
|
||||
**Linux/macOS:**
|
||||
```bash
|
||||
./test-api.sh
|
||||
```
|
||||
|
||||
**自定义参数:**
|
||||
```bash
|
||||
# Windows
|
||||
.\test-api.ps1 -BaseUrl "http://localhost:3000" -TestEmail "custom@example.com"
|
||||
|
||||
# Linux/macOS
|
||||
./test-api.sh "http://localhost:3000" "custom@example.com"
|
||||
```
|
||||
|
||||
## 🧪 测试功能
|
||||
|
||||
### API功能测试
|
||||
测试脚本会验证以下核心功能:
|
||||
|
||||
**用户认证模块:**
|
||||
- ✅ **邮箱验证码发送** - 生成6位数验证码,测试模式输出到控制台
|
||||
- ✅ **邮箱验证码验证** - 验证码校验和自动清理
|
||||
- ✅ **用户注册** - 完整的用户注册流程,包含邮箱验证
|
||||
- ✅ **用户登录** - 支持用户名/邮箱/手机号多种方式登录
|
||||
|
||||
**系统状态测试:**
|
||||
- ✅ **应用状态检查** - 验证服务器运行状态和系统信息
|
||||
- ✅ **Redis文件存储** - 验证验证码存储和读取功能
|
||||
- ✅ **内存数据库** - 验证用户数据存储功能
|
||||
|
||||
### 单元测试覆盖
|
||||
|
||||
**核心服务测试(7个测试套件,140个测试用例):**
|
||||
|
||||
1. **LoginCoreService** - 登录核心服务(15个测试)
|
||||
- 用户登录成功/失败场景
|
||||
- 用户注册功能测试
|
||||
- GitHub OAuth登录测试
|
||||
- 密码重置和修改功能
|
||||
- 用户状态验证(active、inactive、locked等)
|
||||
|
||||
2. **AdminService** - 管理员服务测试
|
||||
- 管理员登录认证
|
||||
- 用户列表管理
|
||||
- 用户密码重置
|
||||
- 日志管理功能
|
||||
|
||||
3. **VerificationService** - 验证码服务测试
|
||||
- 验证码生成和验证
|
||||
- 频率限制机制
|
||||
- Redis存储操作
|
||||
- 错误处理和边界条件
|
||||
|
||||
4. **EmailService** - 邮件服务测试
|
||||
- 邮件发送功能(测试模式和生产模式)
|
||||
- 验证码邮件模板
|
||||
- 连接验证和错误处理
|
||||
- SMTP配置测试
|
||||
|
||||
5. **UsersService** - 用户数据服务测试
|
||||
- 用户CRUD操作
|
||||
- 用户查询功能
|
||||
- 数据验证和约束
|
||||
|
||||
6. **AdminCoreService** - 管理员核心服务测试
|
||||
- 管理员认证逻辑
|
||||
- 权限验证
|
||||
- 管理员引导创建
|
||||
|
||||
7. **LoggerService** - 日志服务测试
|
||||
- 日志记录功能
|
||||
- 敏感信息过滤
|
||||
- 日志级别控制
|
||||
|
||||
### E2E端到端测试
|
||||
|
||||
**登录功能完整流程测试:**
|
||||
- 用户注册 → 邮箱验证 → 登录验证
|
||||
- GitHub OAuth登录流程
|
||||
- 密码重置完整流程
|
||||
- 错误处理和边界条件测试
|
||||
|
||||
## 🔧 测试模式特性
|
||||
|
||||
- 🗄️ **Redis 文件存储** - 使用 `redis-data/redis.json` 存储验证码
|
||||
- 📧 **邮件测试模式** - 邮件内容输出到控制台,无需真实SMTP
|
||||
- 💾 **内存用户存储** - 无需数据库,用户数据存储在内存中
|
||||
- 🔄 **自动切换** - 根据配置自动选择存储模式
|
||||
|
||||
## 📊 单元测试
|
||||
|
||||
### 运行测试命令
|
||||
|
||||
```bash
|
||||
# 运行所有单元测试
|
||||
npm test
|
||||
|
||||
# 监听模式(开发时使用)
|
||||
npm run test:watch
|
||||
|
||||
# 生成覆盖率报告
|
||||
npm run test:cov
|
||||
|
||||
# 运行特定测试文件
|
||||
npm test -- src/core/login_core/login_core.service.spec.ts
|
||||
```
|
||||
|
||||
### 测试覆盖情况
|
||||
|
||||
**测试统计:**
|
||||
- 测试套件:7个
|
||||
- 测试用例:140个
|
||||
- 覆盖率:100%通过
|
||||
|
||||
**测试文件列表:**
|
||||
```
|
||||
src/core/login_core/login_core.service.spec.ts # 登录核心服务
|
||||
src/business/admin/admin.service.spec.ts # 管理员服务
|
||||
src/core/utils/verification/verification.service.spec.ts # 验证码服务
|
||||
src/core/utils/email/email.service.spec.ts # 邮件服务
|
||||
src/core/db/users/users.service.spec.ts # 用户数据服务
|
||||
src/core/admin_core/admin_core.service.spec.ts # 管理员核心服务
|
||||
src/core/utils/logger/logger.service.spec.ts # 日志服务
|
||||
test/business/login.e2e-spec.ts # E2E端到端测试
|
||||
```
|
||||
|
||||
### 测试场景覆盖
|
||||
|
||||
**正常流程测试:**
|
||||
- 用户注册、登录、密码管理
|
||||
- 邮箱验证码发送和验证
|
||||
- 管理员认证和用户管理
|
||||
- 系统状态和日志功能
|
||||
|
||||
**异常情况测试:**
|
||||
- 无效输入和参数验证
|
||||
- 网络连接失败处理
|
||||
- 权限验证和访问控制
|
||||
- 频率限制和安全防护
|
||||
|
||||
**边界条件测试:**
|
||||
- 密码强度验证
|
||||
- 验证码过期处理
|
||||
- 用户状态变更
|
||||
- 数据库连接异常
|
||||
|
||||
## 🌐 生产环境配置
|
||||
|
||||
要切换到生产环境,编辑 `.env` 文件:
|
||||
|
||||
```bash
|
||||
# 启用数据库(取消注释并填入真实数据)
|
||||
DB_HOST=your_mysql_host
|
||||
DB_PORT=3306
|
||||
DB_USERNAME=your_db_username
|
||||
DB_PASSWORD=your_db_password
|
||||
DB_NAME=your_db_name
|
||||
|
||||
# 启用真实Redis(取消注释并设置)
|
||||
USE_FILE_REDIS=false
|
||||
REDIS_HOST=your_redis_host
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=your_redis_password
|
||||
|
||||
# 启用邮件服务(取消注释并填入真实数据)
|
||||
EMAIL_HOST=smtp.gmail.com
|
||||
EMAIL_PORT=587
|
||||
EMAIL_USER=your_email@gmail.com
|
||||
EMAIL_PASS=your_app_password
|
||||
EMAIL_FROM="Whale Town Game" <noreply@whaletown.com>
|
||||
|
||||
# 生产环境设置
|
||||
NODE_ENV=production
|
||||
LOG_LEVEL=info
|
||||
```
|
||||
|
||||
## 🔍 故障排除
|
||||
|
||||
### 服务启动失败
|
||||
- **端口占用**:检查端口3000是否被占用,使用 `netstat -ano | findstr :3000` 查看
|
||||
- **Node.js版本**:确认Node.js版本 >= 18.0.0,使用 `node --version` 检查
|
||||
- **依赖问题**:运行 `npm install` 或 `pnpm install` 重新安装依赖
|
||||
- **权限问题**:确保有足够的文件读写权限
|
||||
|
||||
### 测试脚本执行失败
|
||||
- **服务器状态**:确认服务器正在运行,访问 http://localhost:3000 检查
|
||||
- **网络连接**:检查防火墙设置,确保端口3000可访问
|
||||
- **脚本权限**:在Linux/macOS上确保脚本有执行权限:`chmod +x test-api.sh`
|
||||
- **PowerShell策略**:Windows上可能需要设置执行策略:`Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser`
|
||||
|
||||
### 单元测试失败
|
||||
- **依赖冲突**:清理node_modules并重新安装:`rm -rf node_modules && npm install`
|
||||
- **TypeScript错误**:运行 `npm run build` 检查编译错误
|
||||
- **环境变量**:确保测试环境变量配置正确
|
||||
- **数据库连接**:测试模式下应该使用内存数据库,检查配置
|
||||
|
||||
### Redis文件存储问题
|
||||
- **目录权限**:检查 `redis-data` 目录的读写权限
|
||||
- **配置设置**:确认 `USE_FILE_REDIS=true` 设置正确
|
||||
- **文件锁定**:确保redis.json文件没有被其他进程锁定
|
||||
- **磁盘空间**:检查磁盘空间是否充足
|
||||
|
||||
### 邮件测试模式问题
|
||||
- **配置检查**:确认邮件配置为注释状态(测试模式)
|
||||
- **控制台输出**:检查服务器控制台是否有邮件内容输出
|
||||
- **日志级别**:确保日志级别设置为info或debug以查看详细输出
|
||||
|
||||
### 常见错误解决
|
||||
|
||||
**EADDRINUSE错误:**
|
||||
```bash
|
||||
# 查找占用端口的进程
|
||||
netstat -ano | findstr :3000
|
||||
# 结束进程(Windows)
|
||||
taskkill /PID <进程ID> /F
|
||||
```
|
||||
|
||||
**权限错误:**
|
||||
```bash
|
||||
# Linux/macOS设置权限
|
||||
chmod +x test-api.sh
|
||||
chmod 755 redis-data/
|
||||
```
|
||||
|
||||
**模块未找到错误:**
|
||||
```bash
|
||||
# 清理并重新安装
|
||||
rm -rf node_modules package-lock.json
|
||||
npm install
|
||||
```
|
||||
|
||||
## 📝 测试数据
|
||||
|
||||
测试完成后,你可以查看:
|
||||
|
||||
- `redis-data/redis.json` - 验证码存储数据
|
||||
- 服务器控制台 - 邮件内容输出
|
||||
- 测试脚本输出 - API响应结果
|
||||
|
||||
## 🎯 下一步
|
||||
|
||||
- 查看 [API 文档](http://localhost:3000/api-docs) 了解更多接口
|
||||
- 阅读 [开发规范](./docs/backend_development_guide.md) 开始开发
|
||||
- 使用 [AI 辅助指南](./docs/AI辅助开发规范指南.md) 提高开发效率
|
||||
BIN
docs/development/ab164782cdc17e22f9bdf443c7e1e96c.png
Normal file
BIN
docs/development/ab164782cdc17e22f9bdf443c7e1e96c.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 484 KiB |
688
docs/development/backend_development_guide.md
Normal file
688
docs/development/backend_development_guide.md
Normal file
@@ -0,0 +1,688 @@
|
||||
# 后端开发规范指南
|
||||
|
||||
本文档定义了基于四层架构的后端开发规范,包括架构规范、注释规范、日志规范、代码质量规范等。
|
||||
|
||||
## 📋 目录
|
||||
|
||||
- [架构规范](#架构规范)
|
||||
- [注释规范](#注释规范)
|
||||
- [日志规范](#日志规范)
|
||||
- [异常处理规范](#异常处理规范)
|
||||
- [代码质量规范](#代码质量规范)
|
||||
- [最佳实践](#最佳实践)
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 架构规范
|
||||
|
||||
### 四层架构原则
|
||||
|
||||
项目采用 **Gateway-Business-Core-Data** 四层架构,每层职责明确:
|
||||
|
||||
```
|
||||
Gateway Layer (网关层)
|
||||
↓ 依赖
|
||||
Business Layer (业务层)
|
||||
↓ 依赖
|
||||
Core Layer (核心层)
|
||||
↓ 依赖
|
||||
Data Layer (数据层)
|
||||
```
|
||||
|
||||
### 各层职责
|
||||
|
||||
#### 🌐 Gateway Layer(网关层)
|
||||
|
||||
**位置:** `src/gateway/`
|
||||
|
||||
**职责:**
|
||||
- HTTP/WebSocket协议处理
|
||||
- 请求参数验证(DTO)
|
||||
- 路由管理
|
||||
- 认证守卫
|
||||
- 错误转换
|
||||
|
||||
**规范:**
|
||||
```typescript
|
||||
// ✅ 正确:只做协议转换
|
||||
@Controller('auth')
|
||||
export class LoginController {
|
||||
constructor(private readonly loginService: LoginService) {}
|
||||
|
||||
@Post('login')
|
||||
async login(@Body() dto: LoginDto, @Res() res: Response) {
|
||||
const result = await this.loginService.login(dto);
|
||||
this.handleResponse(result, res);
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 错误:包含业务逻辑
|
||||
@Controller('auth')
|
||||
export class LoginController {
|
||||
@Post('login')
|
||||
async login(@Body() dto: LoginDto) {
|
||||
const user = await this.usersService.findByEmail(dto.email);
|
||||
const isValid = await bcrypt.compare(dto.password, user.password);
|
||||
// ... 更多业务逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 🎯 Business Layer(业务层)
|
||||
|
||||
**位置:** `src/business/`
|
||||
|
||||
**职责:**
|
||||
- 业务逻辑实现
|
||||
- 服务协调
|
||||
- 业务规则验证
|
||||
- 事务管理
|
||||
|
||||
**规范:**
|
||||
```typescript
|
||||
// ✅ 正确:实现业务逻辑
|
||||
@Injectable()
|
||||
export class LoginService {
|
||||
constructor(
|
||||
private readonly loginCoreService: LoginCoreService,
|
||||
private readonly emailService: EmailService,
|
||||
) {}
|
||||
|
||||
async login(dto: LoginDto): Promise<ApiResponse<LoginResponse>> {
|
||||
try {
|
||||
// 1. 调用核心服务验证
|
||||
const user = await this.loginCoreService.validateUser(dto);
|
||||
|
||||
// 2. 业务逻辑:生成Token
|
||||
const tokens = await this.loginCoreService.generateTokens(user);
|
||||
|
||||
// 3. 业务逻辑:发送登录通知
|
||||
await this.emailService.sendLoginNotification(user.email);
|
||||
|
||||
return { success: true, data: tokens };
|
||||
} catch (error) {
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 错误:直接访问数据库
|
||||
@Injectable()
|
||||
export class LoginService {
|
||||
async login(dto: LoginDto) {
|
||||
const user = await this.userRepository.findOne({ email: dto.email });
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### ⚙️ Core Layer(核心层)
|
||||
|
||||
**位置:** `src/core/`
|
||||
|
||||
**职责:**
|
||||
- 数据访问
|
||||
- 基础设施
|
||||
- 外部系统集成
|
||||
- 工具服务
|
||||
|
||||
**规范:**
|
||||
```typescript
|
||||
// ✅ 正确:提供技术基础设施
|
||||
@Injectable()
|
||||
export class LoginCoreService {
|
||||
constructor(
|
||||
@Inject('IUsersService')
|
||||
private readonly usersService: IUsersService,
|
||||
@Inject('IRedisService')
|
||||
private readonly redisService: IRedisService,
|
||||
) {}
|
||||
|
||||
async validateUser(dto: LoginDto): Promise<User> {
|
||||
const user = await this.usersService.findByEmail(dto.email);
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('用户不存在');
|
||||
}
|
||||
|
||||
const isValid = await bcrypt.compare(dto.password, user.password);
|
||||
if (!isValid) {
|
||||
throw new UnauthorizedException('密码错误');
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 错误:包含业务逻辑
|
||||
@Injectable()
|
||||
export class LoginCoreService {
|
||||
async validateUser(dto: LoginDto) {
|
||||
// 发送邮件通知 - 这是业务逻辑,应该在Business层
|
||||
await this.emailService.sendLoginNotification(user.email);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 模块组织规范
|
||||
|
||||
```typescript
|
||||
// 模块命名:功能名.module.ts
|
||||
// 服务命名:功能名.service.ts
|
||||
// 控制器命名:功能名.controller.ts
|
||||
// 网关命名:功能名.gateway.ts
|
||||
|
||||
// ✅ 正确的模块结构
|
||||
src/
|
||||
├── gateway/
|
||||
│ └── auth/
|
||||
│ ├── login.controller.ts
|
||||
│ ├── register.controller.ts
|
||||
│ ├── jwt_auth.guard.ts
|
||||
│ ├── dto/
|
||||
│ └── auth.gateway.module.ts
|
||||
├── business/
|
||||
│ └── auth/
|
||||
│ ├── login.service.ts
|
||||
│ ├── register.service.ts
|
||||
│ └── auth.module.ts
|
||||
└── core/
|
||||
└── login_core/
|
||||
├── login_core.service.ts
|
||||
└── login_core.module.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## <20> 注释规规范
|
||||
|
||||
### 文件头注释
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 用户登录服务
|
||||
*
|
||||
* 功能描述:
|
||||
* - 处理用户登录业务逻辑
|
||||
* - 协调登录核心服务和邮件服务
|
||||
* - 生成JWT令牌
|
||||
*
|
||||
* 架构层级:Business Layer
|
||||
*
|
||||
* 依赖服务:
|
||||
* - LoginCoreService: 登录核心逻辑
|
||||
* - EmailService: 邮件发送服务
|
||||
*
|
||||
* @author 作者名
|
||||
* @version 1.0.0
|
||||
* @since 2025-01-01
|
||||
*/
|
||||
```
|
||||
|
||||
### 类注释
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 登录业务服务
|
||||
*
|
||||
* 职责:
|
||||
* - 实现用户登录业务逻辑
|
||||
* - 协调核心服务完成登录流程
|
||||
* - 处理登录相关的业务规则
|
||||
*
|
||||
* 主要方法:
|
||||
* - login() - 用户登录
|
||||
* - verificationCodeLogin() - 验证码登录
|
||||
* - refreshToken() - 刷新令牌
|
||||
*/
|
||||
@Injectable()
|
||||
export class LoginService {
|
||||
// 实现
|
||||
}
|
||||
```
|
||||
|
||||
### 方法注释(三级标准)
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 用户登录
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 调用核心服务验证用户凭证
|
||||
* 2. 生成访问令牌和刷新令牌
|
||||
* 3. 发送登录成功通知邮件
|
||||
* 4. 记录登录日志
|
||||
* 5. 返回登录结果
|
||||
*
|
||||
* @param dto 登录请求数据
|
||||
* @returns 登录结果,包含用户信息和令牌
|
||||
* @throws UnauthorizedException 用户名或密码错误
|
||||
* @throws ForbiddenException 用户状态不允许登录
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const result = await loginService.login({
|
||||
* identifier: 'user@example.com',
|
||||
* password: 'password123'
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
async login(dto: LoginDto): Promise<ApiResponse<LoginResponse>> {
|
||||
// 实现
|
||||
}
|
||||
```
|
||||
|
||||
### 修改记录规范
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 最近修改:
|
||||
* - 2025-01-15: 架构重构 - 迁移到四层架构,分离网关层和业务层
|
||||
* - 2025-01-10: 功能新增 - 添加验证码登录功能
|
||||
* - 2025-01-08: Bug修复 - 修复Token刷新逻辑错误
|
||||
* - 2025-01-05: 代码规范优化 - 统一异常处理格式
|
||||
* - 2025-01-03: 性能优化 - 优化数据库查询性能
|
||||
*
|
||||
* @version 2.0.0
|
||||
* @lastModified 2025-01-15
|
||||
*/
|
||||
```
|
||||
|
||||
**修改记录原则:**
|
||||
- 只保留最近5次修改
|
||||
- 包含日期、类型、描述
|
||||
- 重大版本更新标注版本号
|
||||
|
||||
---
|
||||
|
||||
## 📊 日志规范
|
||||
|
||||
### 日志级别使用
|
||||
|
||||
```typescript
|
||||
// ERROR - 系统错误,需要立即处理
|
||||
this.logger.error('用户登录失败', {
|
||||
userId,
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
|
||||
// WARN - 警告信息,需要关注
|
||||
this.logger.warn('用户多次登录失败', {
|
||||
userId,
|
||||
attemptCount,
|
||||
ip: request.ip
|
||||
});
|
||||
|
||||
// INFO - 重要的业务操作
|
||||
this.logger.info('用户登录成功', {
|
||||
userId,
|
||||
loginTime: new Date(),
|
||||
ip: request.ip
|
||||
});
|
||||
|
||||
// DEBUG - 调试信息(仅开发环境)
|
||||
this.logger.debug('验证用户密码', {
|
||||
userId,
|
||||
passwordHash: '***'
|
||||
});
|
||||
```
|
||||
|
||||
### 日志格式规范
|
||||
|
||||
```typescript
|
||||
// ✅ 正确:结构化日志
|
||||
this.logger.info('操作描述', {
|
||||
userId: 'user123',
|
||||
action: 'login',
|
||||
timestamp: new Date(),
|
||||
metadata: { ip: '192.168.1.1' }
|
||||
});
|
||||
|
||||
// ❌ 错误:字符串拼接
|
||||
this.logger.info(`用户${userId}登录成功`);
|
||||
```
|
||||
|
||||
### 敏感信息处理
|
||||
|
||||
```typescript
|
||||
// ✅ 正确:隐藏敏感信息
|
||||
this.logger.info('用户注册', {
|
||||
email: user.email,
|
||||
password: '***', // 密码不记录
|
||||
apiKey: '***' // API密钥不记录
|
||||
});
|
||||
|
||||
// ❌ 错误:暴露敏感信息
|
||||
this.logger.info('用户注册', {
|
||||
email: user.email,
|
||||
password: user.password, // 危险!
|
||||
apiKey: user.apiKey // 危险!
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 异常处理规范
|
||||
|
||||
### 异常类型使用
|
||||
|
||||
```typescript
|
||||
// 400 - 客户端请求错误
|
||||
throw new BadRequestException('参数格式错误');
|
||||
|
||||
// 401 - 未授权
|
||||
throw new UnauthorizedException('用户名或密码错误');
|
||||
|
||||
// 403 - 禁止访问
|
||||
throw new ForbiddenException('用户状态不允许此操作');
|
||||
|
||||
// 404 - 资源不存在
|
||||
throw new NotFoundException('用户不存在');
|
||||
|
||||
// 409 - 资源冲突
|
||||
throw new ConflictException('用户名已存在');
|
||||
|
||||
// 500 - 服务器内部错误
|
||||
throw new InternalServerErrorException('系统内部错误');
|
||||
```
|
||||
|
||||
### 分层异常处理
|
||||
|
||||
```typescript
|
||||
// Gateway Layer - 转换为HTTP响应
|
||||
@Controller('auth')
|
||||
export class LoginController {
|
||||
@Post('login')
|
||||
async login(@Body() dto: LoginDto, @Res() res: Response) {
|
||||
const result = await this.loginService.login(dto);
|
||||
|
||||
if (result.success) {
|
||||
res.status(HttpStatus.OK).json(result);
|
||||
} else {
|
||||
const statusCode = this.getErrorStatusCode(result);
|
||||
res.status(statusCode).json(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Business Layer - 返回业务响应
|
||||
@Injectable()
|
||||
export class LoginService {
|
||||
async login(dto: LoginDto): Promise<ApiResponse<LoginResponse>> {
|
||||
try {
|
||||
const user = await this.loginCoreService.validateUser(dto);
|
||||
const tokens = await this.loginCoreService.generateTokens(user);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: tokens,
|
||||
message: '登录成功'
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error('登录失败', { dto, error: error.message });
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: error.message,
|
||||
error_code: 'LOGIN_FAILED'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Core Layer - 抛出技术异常
|
||||
@Injectable()
|
||||
export class LoginCoreService {
|
||||
async validateUser(dto: LoginDto): Promise<User> {
|
||||
const user = await this.usersService.findByEmail(dto.email);
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('用户不存在');
|
||||
}
|
||||
|
||||
const isValid = await bcrypt.compare(dto.password, user.password);
|
||||
if (!isValid) {
|
||||
throw new UnauthorizedException('密码错误');
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 代码质量规范
|
||||
|
||||
### 代码检查清单
|
||||
|
||||
提交代码前确保:
|
||||
|
||||
- [ ] **架构规范**
|
||||
- [ ] 代码放在正确的架构层
|
||||
- [ ] 没有跨层直接调用(如Gateway直接调用Core)
|
||||
- [ ] 依赖方向正确(上层依赖下层)
|
||||
- [ ] 模块职责单一明确
|
||||
|
||||
- [ ] **注释完整性**
|
||||
- [ ] 文件头注释包含架构层级说明
|
||||
- [ ] 类注释说明职责和主要方法
|
||||
- [ ] 方法注释包含业务逻辑和技术实现
|
||||
- [ ] 修改记录保持最近5次
|
||||
|
||||
- [ ] **代码质量**
|
||||
- [ ] 没有未使用的导入和变量
|
||||
- [ ] 常量使用正确命名(UPPER_SNAKE_CASE)
|
||||
- [ ] 方法长度合理(不超过50行)
|
||||
- [ ] 单一职责原则
|
||||
|
||||
- [ ] **日志规范**
|
||||
- [ ] 关键操作记录日志
|
||||
- [ ] 使用结构化日志格式
|
||||
- [ ] 敏感信息已隐藏
|
||||
- [ ] 日志级别使用正确
|
||||
|
||||
- [ ] **异常处理**
|
||||
- [ ] 所有异常情况都处理
|
||||
- [ ] 异常类型使用正确
|
||||
- [ ] 错误信息清晰明确
|
||||
- [ ] 记录了错误日志
|
||||
|
||||
---
|
||||
|
||||
## 💡 最佳实践
|
||||
|
||||
### 1. 遵循四层架构
|
||||
|
||||
```typescript
|
||||
// ✅ 正确:清晰的层次调用
|
||||
// Gateway → Business → Core → Data
|
||||
|
||||
// Gateway Layer
|
||||
@Controller('users')
|
||||
export class UsersController {
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
|
||||
@Get(':id')
|
||||
async getUser(@Param('id') id: string) {
|
||||
return this.usersService.getUserById(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Business Layer
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(private readonly usersCoreService: UsersCoreService) {}
|
||||
|
||||
async getUserById(id: string): Promise<ApiResponse<User>> {
|
||||
try {
|
||||
const user = await this.usersCoreService.findUserById(id);
|
||||
return { success: true, data: user };
|
||||
} catch (error) {
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Core Layer
|
||||
@Injectable()
|
||||
export class UsersCoreService {
|
||||
constructor(
|
||||
@Inject('IUsersService')
|
||||
private readonly usersDataService: IUsersService
|
||||
) {}
|
||||
|
||||
async findUserById(id: string): Promise<User> {
|
||||
const user = await this.usersDataService.findOne(id);
|
||||
if (!user) {
|
||||
throw new NotFoundException('用户不存在');
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 使用依赖注入接口
|
||||
|
||||
```typescript
|
||||
// ✅ 正确:使用接口依赖注入
|
||||
@Injectable()
|
||||
export class LoginCoreService {
|
||||
constructor(
|
||||
@Inject('IUsersService')
|
||||
private readonly usersService: IUsersService,
|
||||
@Inject('IRedisService')
|
||||
private readonly redisService: IRedisService,
|
||||
) {}
|
||||
}
|
||||
|
||||
// ❌ 错误:直接依赖具体实现
|
||||
@Injectable()
|
||||
export class LoginCoreService {
|
||||
constructor(
|
||||
private readonly usersService: UsersService,
|
||||
private readonly redisService: RealRedisService,
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 统一响应格式
|
||||
|
||||
```typescript
|
||||
// 定义统一的响应接口
|
||||
export interface ApiResponse<T = any> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message: string;
|
||||
error_code?: string;
|
||||
}
|
||||
|
||||
// Business Layer 返回统一格式
|
||||
async login(dto: LoginDto): Promise<ApiResponse<LoginResponse>> {
|
||||
try {
|
||||
const result = await this.loginCoreService.validateUser(dto);
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
message: '登录成功'
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: error.message,
|
||||
error_code: 'LOGIN_FAILED'
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 防御性编程
|
||||
|
||||
```typescript
|
||||
async processPayment(dto: PaymentDto): Promise<ApiResponse<PaymentResult>> {
|
||||
// 1. 参数验证
|
||||
if (!dto.amount || dto.amount <= 0) {
|
||||
return {
|
||||
success: false,
|
||||
message: '支付金额必须大于0',
|
||||
error_code: 'INVALID_AMOUNT'
|
||||
};
|
||||
}
|
||||
|
||||
// 2. 业务规则验证
|
||||
const user = await this.usersService.findOne(dto.userId);
|
||||
if (!user) {
|
||||
return {
|
||||
success: false,
|
||||
message: '用户不存在',
|
||||
error_code: 'USER_NOT_FOUND'
|
||||
};
|
||||
}
|
||||
|
||||
// 3. 状态检查
|
||||
if (user.status !== UserStatus.ACTIVE) {
|
||||
return {
|
||||
success: false,
|
||||
message: '用户状态不允许支付',
|
||||
error_code: 'USER_INACTIVE'
|
||||
};
|
||||
}
|
||||
|
||||
// 4. 执行业务逻辑
|
||||
return this.executePayment(dto);
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 测试驱动开发
|
||||
|
||||
```typescript
|
||||
// 先写测试
|
||||
describe('LoginService', () => {
|
||||
it('should login successfully with valid credentials', async () => {
|
||||
const dto = { identifier: 'test@example.com', password: 'password123' };
|
||||
const result = await loginService.login(dto);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toHaveProperty('accessToken');
|
||||
});
|
||||
|
||||
it('should return error with invalid credentials', async () => {
|
||||
const dto = { identifier: 'test@example.com', password: 'wrong' };
|
||||
const result = await loginService.login(dto);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error_code).toBe('LOGIN_FAILED');
|
||||
});
|
||||
});
|
||||
|
||||
// 再写实现
|
||||
@Injectable()
|
||||
export class LoginService {
|
||||
async login(dto: LoginDto): Promise<ApiResponse<LoginResponse>> {
|
||||
// 实现逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 总结
|
||||
|
||||
遵循开发规范能够:
|
||||
|
||||
1. **清晰的架构** - 四层架构确保职责分离
|
||||
2. **高质量代码** - 完整的注释和规范的实现
|
||||
3. **易于维护** - 清晰的文档和日志便于问题定位
|
||||
4. **团队协作** - 统一的规范减少沟通成本
|
||||
5. **系统稳定** - 完善的异常处理和防御性编程
|
||||
|
||||
**记住:好的代码不仅要能运行,更要符合架构设计、易于理解、便于维护和扩展。**
|
||||
|
||||
---
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
- [架构设计文档](../ARCHITECTURE.md) - 四层架构详解
|
||||
- [架构重构文档](../ARCHITECTURE_REFACTORING.md) - 架构迁移指南
|
||||
- [Git提交规范](./git_commit_guide.md) - 版本控制规范
|
||||
- [测试指南](./TESTING.md) - 测试规范和最佳实践
|
||||
@@ -1,3 +1,7 @@
|
||||
|
||||
|
||||

|
||||
|
||||
# Git 提交规范
|
||||
|
||||
本文档定义了项目的 Git 提交信息格式规范,以保持提交历史的清晰和一致性。
|
||||
@@ -10,6 +10,7 @@
|
||||
- [常量命名](#常量命名)
|
||||
- [接口路由命名](#接口路由命名)
|
||||
- [TypeScript 特定规范](#typescript-特定规范)
|
||||
- [注释命名规范](#注释命名规范)
|
||||
- [命名示例](#命名示例)
|
||||
|
||||
## 文件和文件夹命名
|
||||
@@ -331,6 +332,111 @@ class Repository<type, key> { }
|
||||
@IsString({ message: 'name_must_be_string' })
|
||||
```
|
||||
|
||||
## 注释命名规范
|
||||
|
||||
### 注释标签命名
|
||||
|
||||
**规则:使用标准JSDoc标签**
|
||||
|
||||
```typescript
|
||||
✅ 正确示例:
|
||||
@param userId 用户ID
|
||||
@returns 用户信息
|
||||
@throws NotFoundException 用户不存在时
|
||||
@author moyin
|
||||
@version 1.0.0
|
||||
@since 2025-01-07
|
||||
@lastModified 2025-01-07
|
||||
|
||||
❌ 错误示例:
|
||||
@参数 userId 用户ID
|
||||
@返回 用户信息
|
||||
@异常 NotFoundException 用户不存在时
|
||||
@作者 moyin
|
||||
```
|
||||
|
||||
### 修改记录命名
|
||||
|
||||
**规则:使用标准化的修改类型**
|
||||
|
||||
```typescript
|
||||
✅ 正确示例:
|
||||
- 2025-01-07: 代码规范优化 - 清理未使用的导入
|
||||
- 2025-01-07: 功能新增 - 添加用户验证功能
|
||||
- 2025-01-07: Bug修复 - 修复登录验证逻辑
|
||||
- 2025-01-07: 性能优化 - 优化数据库查询
|
||||
- 2025-01-07: 重构 - 重构用户服务架构
|
||||
|
||||
❌ 错误示例:
|
||||
- 2025-01-07: 修改 - 改了一些代码
|
||||
- 2025-01-07: 更新 - 更新了功能
|
||||
- 2025-01-07: 优化 - 优化了性能
|
||||
- 2025-01-07: 调整 - 调整了结构
|
||||
```
|
||||
|
||||
**📏 长度限制:修改记录只保留最近5次修改,保持文件头注释简洁。**
|
||||
|
||||
### 注释内容命名
|
||||
|
||||
**规则:使用清晰描述性的中文**
|
||||
|
||||
```typescript
|
||||
✅ 正确示例:
|
||||
/** 用户唯一标识符 */
|
||||
userId: string;
|
||||
|
||||
/** 用户邮箱地址,用于登录和通知 */
|
||||
email: string;
|
||||
|
||||
/** 用户状态:active-激活, inactive-未激活, banned-已封禁 */
|
||||
status: UserStatus;
|
||||
|
||||
/**
|
||||
* 验证用户登录凭据
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证用户名或邮箱格式
|
||||
* 2. 查找用户记录
|
||||
* 3. 验证密码哈希值
|
||||
* 4. 检查用户状态
|
||||
*/
|
||||
|
||||
❌ 错误示例:
|
||||
/** id */
|
||||
userId: string;
|
||||
|
||||
/** 邮箱 */
|
||||
email: string;
|
||||
|
||||
/** 状态 */
|
||||
status: UserStatus;
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*/
|
||||
```
|
||||
|
||||
### 版本号命名规范
|
||||
|
||||
**规则:使用语义化版本号**
|
||||
|
||||
```typescript
|
||||
✅ 正确示例:
|
||||
@version 1.0.0 // 主版本.次版本.修订版本
|
||||
@version 1.2.3 // 功能更新
|
||||
@version 2.0.0 // 重大更新
|
||||
|
||||
修改时版本递增规则:
|
||||
- 代码规范优化、Bug修复 → 修订版本 +1 (1.0.0 → 1.0.1)
|
||||
- 功能新增、功能修改 → 次版本 +1 (1.0.1 → 1.1.0)
|
||||
- 重构、架构变更 → 主版本 +1 (1.1.0 → 2.0.0)
|
||||
|
||||
❌ 错误示例:
|
||||
@version v1 // 缺少详细版本号
|
||||
@version 1 // 格式不规范
|
||||
@version latest // 不明确的版本标识
|
||||
```
|
||||
|
||||
## 命名示例
|
||||
|
||||
### 完整的模块示例
|
||||
@@ -483,6 +589,11 @@ export class CreatePlayerDto {
|
||||
- [ ] 函数名清晰表达其功能
|
||||
- [ ] 布尔变量使用 is/has/can 前缀
|
||||
- [ ] 避免使用无意义的缩写
|
||||
- [ ] 注释使用标准JSDoc标签
|
||||
- [ ] 修改记录使用标准化修改类型
|
||||
- [ ] 版本号遵循语义化版本规范
|
||||
- [ ] 修改现有文件时添加了修改记录和更新版本号
|
||||
- [ ] 修改记录只保留最近5次,保持注释简洁
|
||||
|
||||
## 工具配置
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
- [WebSocket 实时通信](#websocket-实时通信)
|
||||
- [数据验证](#数据验证)
|
||||
- [异常处理](#异常处理)
|
||||
- [注释规范](#注释规范)
|
||||
|
||||
## 核心概念
|
||||
|
||||
@@ -453,6 +454,142 @@ export class RoomController {
|
||||
7. **日志记录**:使用内置 Logger 或集成第三方日志库
|
||||
8. **测试**:编写单元测试和 E2E 测试
|
||||
|
||||
## 注释规范
|
||||
|
||||
### 文件头注释
|
||||
|
||||
每个 TypeScript 文件都应该包含完整的文件头注释:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 文件功能描述
|
||||
*
|
||||
* 功能描述:
|
||||
* - 主要功能点1
|
||||
* - 主要功能点2
|
||||
* - 主要功能点3
|
||||
*
|
||||
* 职责分离:
|
||||
* - 职责描述1
|
||||
* - 职责描述2
|
||||
*
|
||||
* 最近修改:
|
||||
* - YYYY-MM-DD: 修改类型 - 具体修改内容描述
|
||||
* - YYYY-MM-DD: 修改类型 - 具体修改内容描述
|
||||
*
|
||||
* @author 作者名
|
||||
* @version x.x.x
|
||||
* @since 创建日期
|
||||
* @lastModified 最后修改日期
|
||||
*/
|
||||
```
|
||||
|
||||
### 类注释
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 类功能描述
|
||||
*
|
||||
* 职责:
|
||||
* - 主要职责1
|
||||
* - 主要职责2
|
||||
*
|
||||
* 主要方法:
|
||||
* - method1() - 方法1功能
|
||||
* - method2() - 方法2功能
|
||||
*
|
||||
* 使用场景:
|
||||
* - 场景描述
|
||||
*/
|
||||
@Injectable()
|
||||
export class ExampleService {
|
||||
// 类实现
|
||||
}
|
||||
```
|
||||
|
||||
### 方法注释
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 方法功能描述
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 步骤1描述
|
||||
* 2. 步骤2描述
|
||||
* 3. 步骤3描述
|
||||
*
|
||||
* @param param1 参数1描述
|
||||
* @param param2 参数2描述
|
||||
* @returns 返回值描述
|
||||
* @throws ExceptionType 异常情况描述
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const result = await service.methodName(param1, param2);
|
||||
* ```
|
||||
*/
|
||||
async methodName(param1: string, param2: number): Promise<ResultType> {
|
||||
// 方法实现
|
||||
}
|
||||
```
|
||||
|
||||
### 接口注释
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 接口功能描述
|
||||
*/
|
||||
export interface ExampleInterface {
|
||||
/** 字段1描述 */
|
||||
field1: string;
|
||||
|
||||
/** 字段2描述 */
|
||||
field2: number;
|
||||
|
||||
/** 可选字段描述 */
|
||||
optionalField?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### 修改记录规范
|
||||
|
||||
当修改现有文件时,必须在文件头注释中添加修改记录:
|
||||
|
||||
#### 修改类型定义
|
||||
|
||||
- **代码规范优化** - 命名规范、注释规范、代码清理等
|
||||
- **功能新增** - 添加新的功能或方法
|
||||
- **功能修改** - 修改现有功能的实现
|
||||
- **Bug修复** - 修复代码缺陷
|
||||
- **性能优化** - 提升代码性能
|
||||
- **重构** - 代码结构调整但功能不变
|
||||
|
||||
#### 修改记录格式
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 最近修改:
|
||||
* - 2025-01-07: 代码规范优化 - 清理未使用的导入(EmailSendResult, crypto)
|
||||
* - 2025-01-07: 代码规范优化 - 修复常量命名(saltRounds -> SALT_ROUNDS)
|
||||
* - 2025-01-07: 功能新增 - 添加用户验证码登录功能
|
||||
* - 2025-01-06: Bug修复 - 修复邮箱验证逻辑错误
|
||||
*
|
||||
* @version 1.0.1 (修改后需要递增版本号)
|
||||
* @lastModified 2025-01-07
|
||||
*/
|
||||
```
|
||||
|
||||
**📏 修改记录长度限制:只保留最近5次修改,超出时删除最旧记录,保持注释简洁。**
|
||||
|
||||
### 注释最佳实践
|
||||
|
||||
1. **保持更新**:修改代码时同步更新注释
|
||||
2. **描述意图**:注释应该说明"为什么"而不只是"做什么"
|
||||
3. **业务逻辑**:复杂的业务逻辑必须有详细的步骤说明
|
||||
4. **异常处理**:明确说明可能抛出的异常和处理方式
|
||||
5. **示例代码**:复杂方法提供使用示例
|
||||
6. **版本管理**:修改文件时必须更新修改记录和版本号
|
||||
|
||||
## 更多资源
|
||||
|
||||
- [NestJS 官方文档](https://docs.nestjs.com/)
|
||||
@@ -1,265 +0,0 @@
|
||||
# 邮箱验证系统
|
||||
|
||||
## 概述
|
||||
|
||||
邮箱验证系统提供完整的邮箱验证功能,包括验证码生成、发送、验证和管理。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 📧 邮箱验证码发送
|
||||
- 🔐 验证码安全验证
|
||||
- ⏰ 验证码过期管理
|
||||
- 🚫 防刷机制(频率限制)
|
||||
- 📊 验证统计和监控
|
||||
|
||||
## 系统架构
|
||||
|
||||
```
|
||||
邮箱验证系统
|
||||
├── 验证码服务 (VerificationService)
|
||||
│ ├── 验证码生成
|
||||
│ ├── 验证码验证
|
||||
│ └── 防刷机制
|
||||
├── 邮件服务 (EmailService)
|
||||
│ ├── 验证码邮件发送
|
||||
│ ├── 欢迎邮件发送
|
||||
│ └── 邮件模板管理
|
||||
└── Redis缓存
|
||||
├── 验证码存储
|
||||
├── 冷却时间管理
|
||||
└── 发送频率限制
|
||||
```
|
||||
|
||||
## 核心组件
|
||||
|
||||
### 1. 验证码服务 (VerificationService)
|
||||
|
||||
负责验证码的生成、验证和管理:
|
||||
|
||||
- **验证码生成**:6位数字验证码
|
||||
- **验证码验证**:支持多次尝试限制
|
||||
- **过期管理**:5分钟有效期
|
||||
- **防刷机制**:60秒冷却时间,每小时最多5次
|
||||
|
||||
### 2. 邮件服务 (EmailService)
|
||||
|
||||
负责邮件的发送和模板管理:
|
||||
|
||||
- **验证码邮件**:发送验证码到用户邮箱
|
||||
- **欢迎邮件**:用户注册成功后发送
|
||||
- **模板支持**:支持HTML邮件模板
|
||||
|
||||
### 3. Redis缓存
|
||||
|
||||
负责数据的临时存储:
|
||||
|
||||
- **验证码存储**:`verification_code:${type}:${identifier}`
|
||||
- **冷却时间**:`verification_cooldown:${type}:${identifier}`
|
||||
- **发送频率**:`verification_hourly:${type}:${identifier}:${date}:${hour}`
|
||||
|
||||
## 使用流程
|
||||
|
||||
### 注册流程中的邮箱验证
|
||||
|
||||
1. **发送验证码**
|
||||
```typescript
|
||||
POST /auth/send-email-verification
|
||||
{
|
||||
"email": "user@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
2. **用户注册**
|
||||
```typescript
|
||||
POST /auth/register
|
||||
{
|
||||
"username": "testuser",
|
||||
"password": "password123",
|
||||
"nickname": "测试用户",
|
||||
"email": "user@example.com",
|
||||
"email_verification_code": "123456"
|
||||
}
|
||||
```
|
||||
|
||||
### 独立邮箱验证
|
||||
|
||||
1. **验证邮箱**
|
||||
```typescript
|
||||
POST /auth/verify-email
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"verification_code": "123456"
|
||||
}
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 环境变量
|
||||
|
||||
```bash
|
||||
# 邮件服务配置
|
||||
SMTP_HOST=smtp.example.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=your-email@example.com
|
||||
SMTP_PASS=your-password
|
||||
SMTP_FROM=noreply@example.com
|
||||
|
||||
# Redis配置
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
```
|
||||
|
||||
### 验证码配置
|
||||
|
||||
```typescript
|
||||
// 验证码长度
|
||||
CODE_LENGTH = 6
|
||||
|
||||
// 验证码过期时间(秒)
|
||||
CODE_EXPIRE_TIME = 300 // 5分钟
|
||||
|
||||
// 最大验证尝试次数
|
||||
MAX_ATTEMPTS = 3
|
||||
|
||||
// 发送冷却时间(秒)
|
||||
RATE_LIMIT_TIME = 60 // 1分钟
|
||||
|
||||
// 每小时最大发送次数
|
||||
MAX_SENDS_PER_HOUR = 5
|
||||
```
|
||||
|
||||
## API接口
|
||||
|
||||
### 发送邮箱验证码
|
||||
|
||||
- **接口**:`POST /auth/send-email-verification`
|
||||
- **描述**:向指定邮箱发送验证码
|
||||
- **参数**:
|
||||
```typescript
|
||||
{
|
||||
email: string; // 邮箱地址
|
||||
}
|
||||
```
|
||||
|
||||
### 验证邮箱验证码
|
||||
|
||||
- **接口**:`POST /auth/verify-email`
|
||||
- **描述**:使用验证码验证邮箱
|
||||
- **参数**:
|
||||
```typescript
|
||||
{
|
||||
email: string; // 邮箱地址
|
||||
verification_code: string; // 6位数字验证码
|
||||
}
|
||||
```
|
||||
|
||||
### 重新发送验证码
|
||||
|
||||
- **接口**:`POST /auth/resend-email-verification`
|
||||
- **描述**:重新向指定邮箱发送验证码
|
||||
- **参数**:
|
||||
```typescript
|
||||
{
|
||||
email: string; // 邮箱地址
|
||||
}
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 常见错误码
|
||||
|
||||
- `VERIFICATION_CODE_NOT_FOUND`:验证码不存在或已过期
|
||||
- `VERIFICATION_CODE_INVALID`:验证码错误
|
||||
- `TOO_MANY_ATTEMPTS`:验证尝试次数过多
|
||||
- `RATE_LIMIT_EXCEEDED`:发送频率过高
|
||||
- `EMAIL_SEND_FAILED`:邮件发送失败
|
||||
|
||||
### 错误响应格式
|
||||
|
||||
```typescript
|
||||
{
|
||||
success: false,
|
||||
message: "错误描述",
|
||||
error_code: "ERROR_CODE"
|
||||
}
|
||||
```
|
||||
|
||||
## 监控和日志
|
||||
|
||||
### 关键指标
|
||||
|
||||
- 验证码发送成功率
|
||||
- 验证码验证成功率
|
||||
- 邮件发送延迟
|
||||
- Redis连接状态
|
||||
|
||||
### 日志记录
|
||||
|
||||
- 验证码生成和验证日志
|
||||
- 邮件发送状态日志
|
||||
- 错误和异常日志
|
||||
- 性能监控日志
|
||||
|
||||
## 安全考虑
|
||||
|
||||
### 防刷机制
|
||||
|
||||
1. **发送频率限制**:每个邮箱60秒内只能发送一次
|
||||
2. **每小时限制**:每个邮箱每小时最多发送5次
|
||||
3. **验证尝试限制**:每个验证码最多尝试3次
|
||||
|
||||
### 数据安全
|
||||
|
||||
1. **验证码加密存储**:Redis中的验证码经过加密
|
||||
2. **过期自动清理**:验证码5分钟后自动过期
|
||||
3. **日志脱敏**:日志中不记录完整验证码
|
||||
|
||||
## 部署指南
|
||||
|
||||
详细的部署说明请参考:[deployment-guide.md](./deployment-guide.md)
|
||||
|
||||
## 测试
|
||||
|
||||
### 单元测试
|
||||
|
||||
```bash
|
||||
# 运行验证服务测试
|
||||
npm test -- verification.service.spec.ts
|
||||
|
||||
# 运行邮件服务测试
|
||||
npm test -- email.service.spec.ts
|
||||
```
|
||||
|
||||
### 集成测试
|
||||
|
||||
```bash
|
||||
# 运行邮箱验证集成测试
|
||||
npm run test:e2e -- email-verification
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **验证码收不到**
|
||||
- 检查SMTP配置
|
||||
- 检查邮箱是否在垃圾邮件中
|
||||
- 检查网络连接
|
||||
|
||||
2. **验证码验证失败**
|
||||
- 检查验证码是否过期
|
||||
- 检查验证码输入是否正确
|
||||
- 检查Redis连接状态
|
||||
|
||||
3. **发送频率限制**
|
||||
- 等待冷却时间结束
|
||||
- 检查是否达到每小时限制
|
||||
|
||||
## 更新日志
|
||||
|
||||
- **v1.0.0** (2025-12-17)
|
||||
- 初始版本发布
|
||||
- 支持基本的邮箱验证功能
|
||||
- 集成Redis缓存
|
||||
- 添加防刷机制
|
||||
@@ -1,316 +0,0 @@
|
||||
# 邮箱验证功能部署指南
|
||||
|
||||
## 概述
|
||||
|
||||
本指南详细说明如何部署和配置邮箱验证功能,包括Redis缓存、邮件服务配置等。
|
||||
|
||||
## 1. 安装依赖
|
||||
|
||||
```bash
|
||||
# 安装新增的依赖包
|
||||
pnpm install ioredis nodemailer
|
||||
|
||||
# 安装类型定义
|
||||
pnpm install -D @types/nodemailer
|
||||
```
|
||||
|
||||
## 2. Redis 服务配置
|
||||
|
||||
### 2.1 安装 Redis
|
||||
|
||||
#### Ubuntu/Debian
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install redis-server
|
||||
sudo systemctl start redis-server
|
||||
sudo systemctl enable redis-server
|
||||
```
|
||||
|
||||
#### CentOS/RHEL
|
||||
```bash
|
||||
sudo yum install redis
|
||||
sudo systemctl start redis
|
||||
sudo systemctl enable redis
|
||||
```
|
||||
|
||||
#### Docker 方式
|
||||
```bash
|
||||
docker run -d --name redis -p 6379:6379 redis:7-alpine
|
||||
```
|
||||
|
||||
### 2.2 Redis 配置验证
|
||||
|
||||
```bash
|
||||
# 测试 Redis 连接
|
||||
redis-cli ping
|
||||
# 应该返回 PONG
|
||||
```
|
||||
|
||||
## 3. 邮件服务配置
|
||||
|
||||
### 3.1 Gmail 配置示例
|
||||
|
||||
1. **启用两步验证**:
|
||||
- 登录 Google 账户
|
||||
- 进入"安全性"设置
|
||||
- 启用"两步验证"
|
||||
|
||||
2. **生成应用专用密码**:
|
||||
- 在"安全性"设置中找到"应用专用密码"
|
||||
- 生成新的应用密码
|
||||
- 记录生成的16位密码
|
||||
|
||||
3. **环境变量配置**:
|
||||
```env
|
||||
EMAIL_HOST=smtp.gmail.com
|
||||
EMAIL_PORT=587
|
||||
EMAIL_SECURE=false
|
||||
EMAIL_USER=your-email@gmail.com
|
||||
EMAIL_PASS=your-16-digit-app-password
|
||||
EMAIL_FROM="Whale Town Game" <noreply@gmail.com>
|
||||
```
|
||||
|
||||
### 3.2 其他邮件服务商配置
|
||||
|
||||
#### 163邮箱
|
||||
```env
|
||||
EMAIL_HOST=smtp.163.com
|
||||
EMAIL_PORT=587
|
||||
EMAIL_SECURE=false
|
||||
EMAIL_USER=your-email@163.com
|
||||
EMAIL_PASS=your-authorization-code
|
||||
```
|
||||
|
||||
#### QQ邮箱
|
||||
```env
|
||||
EMAIL_HOST=smtp.qq.com
|
||||
EMAIL_PORT=587
|
||||
EMAIL_SECURE=false
|
||||
EMAIL_USER=your-email@qq.com
|
||||
EMAIL_PASS=your-authorization-code
|
||||
```
|
||||
|
||||
#### 阿里云邮件推送
|
||||
```env
|
||||
EMAIL_HOST=smtpdm.aliyun.com
|
||||
EMAIL_PORT=587
|
||||
EMAIL_SECURE=false
|
||||
EMAIL_USER=your-smtp-username
|
||||
EMAIL_PASS=your-smtp-password
|
||||
```
|
||||
|
||||
## 4. 环境变量配置
|
||||
|
||||
### 4.1 创建环境配置文件
|
||||
|
||||
```bash
|
||||
# 复制环境变量模板
|
||||
cp .env.production.example .env
|
||||
|
||||
# 编辑环境变量
|
||||
nano .env
|
||||
```
|
||||
|
||||
### 4.2 完整的环境变量配置
|
||||
|
||||
```env
|
||||
# 数据库配置
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USERNAME=pixel_game
|
||||
DB_PASSWORD=your_db_password
|
||||
DB_NAME=pixel_game_db
|
||||
|
||||
# 应用配置
|
||||
NODE_ENV=production
|
||||
PORT=3000
|
||||
|
||||
# JWT 配置
|
||||
JWT_SECRET=your_jwt_secret_key_here_at_least_32_characters
|
||||
JWT_EXPIRES_IN=7d
|
||||
|
||||
# Redis 配置
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
REDIS_DB=0
|
||||
|
||||
# 邮件服务配置
|
||||
EMAIL_HOST=smtp.gmail.com
|
||||
EMAIL_PORT=587
|
||||
EMAIL_SECURE=false
|
||||
EMAIL_USER=your-email@gmail.com
|
||||
EMAIL_PASS=your-app-password
|
||||
EMAIL_FROM="Whale Town Game" <noreply@whaletown.com>
|
||||
```
|
||||
|
||||
## 5. 数据库迁移
|
||||
|
||||
由于添加了新的字段,需要更新数据库结构:
|
||||
|
||||
```sql
|
||||
-- 添加邮箱验证状态字段
|
||||
ALTER TABLE users ADD COLUMN email_verified BOOLEAN NOT NULL DEFAULT FALSE COMMENT '邮箱是否已验证';
|
||||
|
||||
-- 为已有用户设置默认值
|
||||
UPDATE users SET email_verified = FALSE WHERE email_verified IS NULL;
|
||||
|
||||
-- 如果是OAuth用户且有邮箱,可以设为已验证
|
||||
UPDATE users SET email_verified = TRUE WHERE github_id IS NOT NULL AND email IS NOT NULL;
|
||||
```
|
||||
|
||||
## 6. 启动和测试
|
||||
|
||||
### 6.1 启动应用
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
pnpm install
|
||||
|
||||
# 构建应用
|
||||
pnpm run build
|
||||
|
||||
# 启动应用
|
||||
pnpm run start:prod
|
||||
```
|
||||
|
||||
### 6.2 功能测试
|
||||
|
||||
#### 测试邮箱验证码发送
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/auth/send-email-verification \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email":"test@example.com"}'
|
||||
```
|
||||
|
||||
#### 测试邮箱验证
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/auth/verify-email \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email":"test@example.com","verification_code":"123456"}'
|
||||
```
|
||||
|
||||
#### 测试密码重置
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/auth/forgot-password \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"identifier":"test@example.com"}'
|
||||
```
|
||||
|
||||
## 7. 监控和日志
|
||||
|
||||
### 7.1 查看应用日志
|
||||
|
||||
```bash
|
||||
# PM2 日志
|
||||
pm2 logs pixel-game-server
|
||||
|
||||
# 或者查看文件日志
|
||||
tail -f logs/dev.log
|
||||
```
|
||||
|
||||
### 7.2 Redis 监控
|
||||
|
||||
```bash
|
||||
# 查看 Redis 信息
|
||||
redis-cli info
|
||||
|
||||
# 监控 Redis 命令
|
||||
redis-cli monitor
|
||||
|
||||
# 查看验证码相关的键
|
||||
redis-cli keys "verification_*"
|
||||
```
|
||||
|
||||
### 7.3 邮件发送监控
|
||||
|
||||
应用会记录邮件发送的日志,包括:
|
||||
- 发送成功/失败状态
|
||||
- 收件人信息
|
||||
- 发送时间
|
||||
- 错误信息(如果有)
|
||||
|
||||
## 8. 故障排除
|
||||
|
||||
### 8.1 Redis 连接问题
|
||||
|
||||
**问题**:Redis连接失败
|
||||
```
|
||||
Redis连接错误: Error: connect ECONNREFUSED 127.0.0.1:6379
|
||||
```
|
||||
|
||||
**解决方案**:
|
||||
1. 检查Redis服务状态:`sudo systemctl status redis`
|
||||
2. 启动Redis服务:`sudo systemctl start redis`
|
||||
3. 检查防火墙设置
|
||||
4. 验证Redis配置文件
|
||||
|
||||
### 8.2 邮件发送问题
|
||||
|
||||
**问题**:邮件发送失败
|
||||
```
|
||||
邮件发送失败: Error: Invalid login: 535-5.7.8 Username and Password not accepted
|
||||
```
|
||||
|
||||
**解决方案**:
|
||||
1. 检查邮箱用户名和密码
|
||||
2. 确认已启用应用专用密码(Gmail)
|
||||
3. 检查邮件服务商的SMTP设置
|
||||
4. 验证网络连接
|
||||
|
||||
### 8.3 验证码问题
|
||||
|
||||
**问题**:验证码验证失败
|
||||
|
||||
**解决方案**:
|
||||
1. 检查Redis中是否存在验证码:`redis-cli get verification_code:email_verification:test@example.com`
|
||||
2. 检查验证码是否过期
|
||||
3. 验证验证码格式(6位数字)
|
||||
4. 检查应用日志
|
||||
|
||||
## 9. 安全建议
|
||||
|
||||
### 9.1 邮件服务安全
|
||||
|
||||
1. **使用应用专用密码**:不要使用主密码
|
||||
2. **启用TLS/SSL**:确保邮件传输加密
|
||||
3. **限制发送频率**:防止邮件轰炸
|
||||
4. **监控发送量**:避免被标记为垃圾邮件
|
||||
|
||||
### 9.2 Redis 安全
|
||||
|
||||
1. **设置密码**:`requirepass your_redis_password`
|
||||
2. **绑定IP**:`bind 127.0.0.1`
|
||||
3. **禁用危险命令**:`rename-command FLUSHDB ""`
|
||||
4. **定期备份**:设置Redis数据备份
|
||||
|
||||
### 9.3 验证码安全
|
||||
|
||||
1. **设置过期时间**:默认5分钟
|
||||
2. **限制尝试次数**:最多3次
|
||||
3. **防刷机制**:60秒冷却时间
|
||||
4. **记录日志**:监控异常行为
|
||||
|
||||
## 10. 性能优化
|
||||
|
||||
### 10.1 Redis 优化
|
||||
|
||||
```redis
|
||||
# Redis 配置优化
|
||||
maxmemory 256mb
|
||||
maxmemory-policy allkeys-lru
|
||||
save 900 1
|
||||
save 300 10
|
||||
save 60 10000
|
||||
```
|
||||
|
||||
### 10.2 邮件发送优化
|
||||
|
||||
1. **连接池**:复用SMTP连接
|
||||
2. **异步发送**:不阻塞主流程
|
||||
3. **队列机制**:处理大量邮件
|
||||
4. **失败重试**:自动重试机制
|
||||
|
||||
---
|
||||
|
||||
*部署完成后,建议进行完整的功能测试,确保所有邮箱验证功能正常工作。*
|
||||
@@ -1,80 +0,0 @@
|
||||
# 日志系统
|
||||
|
||||
## 概述
|
||||
|
||||
项目集成了完整的日志系统,基于 Pino 高性能日志库,提供结构化日志记录、自动敏感信息过滤和多级别日志控制。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 🚀 高性能日志记录
|
||||
- 🔒 自动敏感信息过滤
|
||||
- 🎯 多级别日志控制
|
||||
- 🔍 请求上下文绑定
|
||||
- 📊 结构化日志输出
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 基础用法
|
||||
|
||||
```typescript
|
||||
import { AppLoggerService } from './core/utils/logger/logger.service';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(private readonly logger: AppLoggerService) {}
|
||||
|
||||
async createUser(userData: CreateUserDto) {
|
||||
this.logger.info('开始创建用户', {
|
||||
operation: 'createUser',
|
||||
email: userData.email,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
try {
|
||||
const user = await this.userRepository.save(userData);
|
||||
|
||||
this.logger.info('用户创建成功', {
|
||||
operation: 'createUser',
|
||||
userId: user.id,
|
||||
email: userData.email
|
||||
});
|
||||
|
||||
return user;
|
||||
} catch (error) {
|
||||
this.logger.error('用户创建失败', {
|
||||
operation: 'createUser',
|
||||
email: userData.email,
|
||||
error: error.message
|
||||
}, error.stack);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 日志级别
|
||||
|
||||
- `error`: 错误信息
|
||||
- `warn`: 警告信息
|
||||
- `info`: 一般信息
|
||||
- `debug`: 调试信息
|
||||
|
||||
## 配置
|
||||
|
||||
日志配置位于 `src/core/utils/logger/logger.config.ts`,支持:
|
||||
|
||||
- 日志级别设置
|
||||
- 输出格式配置
|
||||
- 敏感信息过滤规则
|
||||
- 文件输出配置
|
||||
|
||||
## 敏感信息过滤
|
||||
|
||||
系统自动过滤以下敏感信息:
|
||||
- 密码字段
|
||||
- 令牌信息
|
||||
- 个人身份信息
|
||||
- 支付相关信息
|
||||
|
||||
详细使用方法请参考:[后端开发规范指南 - 日志系统使用指南](../../backend_development_guide.md#四日志系统使用指南)
|
||||
@@ -1,363 +0,0 @@
|
||||
# 日志系统详细说明
|
||||
|
||||
## 📋 概述
|
||||
|
||||
本项目的日志系统基于 Pino 高性能日志库构建,提供完整的日志记录、管理和分析功能。
|
||||
|
||||
---
|
||||
|
||||
## 🗂️ 日志文件结构
|
||||
|
||||
### 开发环境 (`NODE_ENV=development`)
|
||||
|
||||
```
|
||||
logs/
|
||||
└── dev.log # 开发环境综合日志(所有级别)
|
||||
```
|
||||
|
||||
**输出方式:**
|
||||
- 🖥️ **控制台**:彩色美化输出,便于开发调试
|
||||
- 📁 **文件**:保存到 `logs/dev.log`,便于问题追踪
|
||||
|
||||
### 生产环境 (`NODE_ENV=production`)
|
||||
|
||||
```
|
||||
logs/
|
||||
├── app.log # 应用综合日志(info及以上级别)
|
||||
├── error.log # 错误日志(error和fatal级别)
|
||||
├── access.log # HTTP访问日志(请求响应记录)
|
||||
├── app.log.gz # 压缩的历史日志文件
|
||||
├── error.log.gz # 压缩的历史错误日志
|
||||
└── access.log.gz # 压缩的历史访问日志
|
||||
```
|
||||
|
||||
**输出方式:**
|
||||
- 📁 **文件**:分类保存到不同的日志文件
|
||||
- 🖥️ **控制台**:仅输出 warn 及以上级别(用于容器日志收集)
|
||||
|
||||
---
|
||||
|
||||
## 📊 日志级别和用途
|
||||
|
||||
| 级别 | 数值 | 用途 | 保存位置 | 示例场景 |
|
||||
|------|------|------|----------|----------|
|
||||
| **TRACE** | 10 | 极细粒度调试 | dev.log | 循环内变量状态 |
|
||||
| **DEBUG** | 20 | 开发调试信息 | dev.log | 方法调用参数 |
|
||||
| **INFO** | 30 | 重要业务操作 | app.log, dev.log | 用户登录成功 |
|
||||
| **WARN** | 40 | 警告信息 | app.log, dev.log, 控制台 | 参数验证失败 |
|
||||
| **ERROR** | 50 | 错误信息 | error.log, app.log, 控制台 | 数据库连接失败 |
|
||||
| **FATAL** | 60 | 致命错误 | error.log, app.log, 控制台 | 系统不可用 |
|
||||
|
||||
---
|
||||
|
||||
## 🔄 日志轮转和管理
|
||||
|
||||
### 自动轮转策略
|
||||
|
||||
| 文件类型 | 轮转频率 | 文件大小限制 | 保留时间 | 压缩策略 |
|
||||
|----------|----------|--------------|----------|----------|
|
||||
| **app.log** | 每日 | 10MB | 7天 | 7天后压缩 |
|
||||
| **error.log** | 每日 | 10MB | 30天 | 7天后压缩 |
|
||||
| **access.log** | 每日 | 50MB | 14天 | 7天后压缩 |
|
||||
| **dev.log** | 手动 | 无限制 | 无限制 | 不压缩 |
|
||||
|
||||
### 定时任务
|
||||
|
||||
| 任务 | 执行时间 | 功能 |
|
||||
|------|----------|------|
|
||||
| **日志清理** | 每天 02:00 | 删除过期日志文件 |
|
||||
| **日志压缩** | 每周日 03:00 | 压缩7天前的日志文件 |
|
||||
| **健康监控** | 每小时 | 监控日志系统状态 |
|
||||
| **统计报告** | 每天 09:00 | 输出日志统计信息 |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 如何使用日志系统
|
||||
|
||||
### 基本使用
|
||||
|
||||
```typescript
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AppLoggerService } from '../core/utils/logger/logger.service';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(private readonly logger: AppLoggerService) {}
|
||||
|
||||
async createUser(userData: CreateUserDto) {
|
||||
// 记录操作开始
|
||||
this.logger.info('开始创建用户', {
|
||||
operation: 'createUser',
|
||||
email: userData.email,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
try {
|
||||
const user = await this.userRepository.save(userData);
|
||||
|
||||
// 记录成功操作
|
||||
this.logger.info('用户创建成功', {
|
||||
operation: 'createUser',
|
||||
userId: user.id,
|
||||
email: userData.email,
|
||||
duration: Date.now() - startTime
|
||||
});
|
||||
|
||||
return user;
|
||||
} catch (error) {
|
||||
// 记录错误
|
||||
this.logger.error('用户创建失败', {
|
||||
operation: 'createUser',
|
||||
email: userData.email,
|
||||
error: error.message
|
||||
}, error.stack);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 请求上下文绑定
|
||||
|
||||
```typescript
|
||||
@Controller('users')
|
||||
export class UserController {
|
||||
constructor(private readonly logger: AppLoggerService) {}
|
||||
|
||||
@Get(':id')
|
||||
async getUser(@Param('id') id: string, @Req() req: Request) {
|
||||
// 绑定请求上下文
|
||||
const requestLogger = this.logger.bindRequest(req, 'UserController');
|
||||
|
||||
requestLogger.info('开始获取用户信息', { userId: id });
|
||||
|
||||
try {
|
||||
const user = await this.userService.findById(id);
|
||||
requestLogger.info('用户信息获取成功', { userId: id });
|
||||
return user;
|
||||
} catch (error) {
|
||||
requestLogger.error('用户信息获取失败', error.stack, {
|
||||
userId: id,
|
||||
reason: error.message
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 日志格式详解
|
||||
|
||||
### 开发环境日志格式
|
||||
|
||||
```
|
||||
🕐 2024-12-13 14:30:25 📝 INFO pixel-game-server [UserService] 用户创建成功
|
||||
operation: "createUser"
|
||||
userId: "user_123"
|
||||
email: "user@example.com"
|
||||
duration: 45
|
||||
```
|
||||
|
||||
### 生产环境日志格式 (JSON)
|
||||
|
||||
```json
|
||||
{
|
||||
"level": 30,
|
||||
"time": 1702456225000,
|
||||
"pid": 12345,
|
||||
"hostname": "server-01",
|
||||
"app": "pixel-game-server",
|
||||
"version": "1.0.0",
|
||||
"msg": "用户创建成功",
|
||||
"operation": "createUser",
|
||||
"userId": "user_123",
|
||||
"email": "user@example.com",
|
||||
"duration": 45,
|
||||
"reqId": "req_1702456225_abc123"
|
||||
}
|
||||
```
|
||||
|
||||
### HTTP 请求日志格式
|
||||
|
||||
```json
|
||||
{
|
||||
"level": 30,
|
||||
"time": 1702456225000,
|
||||
"req": {
|
||||
"id": "req_1702456225_abc123",
|
||||
"method": "POST",
|
||||
"url": "/api/users",
|
||||
"headers": {
|
||||
"host": "localhost:3000",
|
||||
"user-agent": "Mozilla/5.0...",
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"ip": "127.0.0.1"
|
||||
},
|
||||
"res": {
|
||||
"statusCode": 201,
|
||||
"responseTime": 45
|
||||
},
|
||||
"msg": "POST /api/users completed in 45ms"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 问题排查指南
|
||||
|
||||
### 1. 如何查找特定用户的操作日志?
|
||||
|
||||
```bash
|
||||
# 在日志文件中搜索特定用户ID
|
||||
grep "userId.*user_123" logs/app.log
|
||||
|
||||
# 搜索特定操作
|
||||
grep "operation.*createUser" logs/app.log
|
||||
|
||||
# 搜索特定时间段的日志
|
||||
grep "2024-12-13 14:" logs/app.log
|
||||
```
|
||||
|
||||
### 2. 如何查找错误日志?
|
||||
|
||||
```bash
|
||||
# 查看所有错误日志
|
||||
cat logs/error.log
|
||||
|
||||
# 查看最近的错误
|
||||
tail -f logs/error.log
|
||||
|
||||
# 搜索特定错误
|
||||
grep "数据库连接失败" logs/error.log
|
||||
```
|
||||
|
||||
### 3. 如何分析性能问题?
|
||||
|
||||
```bash
|
||||
# 查找响应时间超过1000ms的请求
|
||||
grep "responseTime.*[0-9][0-9][0-9][0-9]" logs/access.log
|
||||
|
||||
# 查找特定接口的性能数据
|
||||
grep "POST /api/users" logs/access.log | grep responseTime
|
||||
```
|
||||
|
||||
### 4. 如何监控系统健康状态?
|
||||
|
||||
```bash
|
||||
# 查看日志统计信息
|
||||
grep "日志系统健康状态报告" logs/app.log
|
||||
|
||||
# 查看日志清理记录
|
||||
grep "日志清理任务完成" logs/app.log
|
||||
|
||||
# 查看压缩记录
|
||||
grep "日志压缩任务完成" logs/app.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📈 日志分析和监控
|
||||
|
||||
### 日志统计信息
|
||||
|
||||
系统会自动收集以下统计信息:
|
||||
|
||||
- **文件数量**:当前日志文件总数
|
||||
- **总大小**:所有日志文件占用的磁盘空间
|
||||
- **错误日志数量**:错误级别日志文件数量
|
||||
- **最旧/最新文件**:日志文件的时间范围
|
||||
- **平均文件大小**:单个日志文件的平均大小
|
||||
|
||||
### 健康监控告警
|
||||
|
||||
系统会在以下情况发出警告:
|
||||
|
||||
- 📊 **磁盘空间告警**:日志文件总大小超过阈值
|
||||
- ⚠️ **错误日志告警**:错误日志数量异常增长
|
||||
- 🔧 **清理失败告警**:日志清理任务执行失败
|
||||
- 💾 **压缩失败告警**:日志压缩任务执行失败
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ 配置说明
|
||||
|
||||
### 环境变量配置
|
||||
|
||||
```bash
|
||||
# 应用名称
|
||||
APP_NAME=pixel-game-server
|
||||
|
||||
# 环境标识
|
||||
NODE_ENV=development
|
||||
|
||||
# 日志级别
|
||||
LOG_LEVEL=debug
|
||||
|
||||
# 日志目录
|
||||
LOG_DIR=./logs
|
||||
|
||||
# 日志保留天数
|
||||
LOG_MAX_FILES=7d
|
||||
|
||||
# 单个日志文件最大大小
|
||||
LOG_MAX_SIZE=10m
|
||||
```
|
||||
|
||||
### 高级配置选项
|
||||
|
||||
如需自定义日志配置,可以修改 `src/core/utils/logger/logger.config.ts`:
|
||||
|
||||
```typescript
|
||||
// 自定义日志轮转策略
|
||||
{
|
||||
target: 'pino-roll',
|
||||
options: {
|
||||
file: path.join(logDir, 'app.log'),
|
||||
frequency: 'daily', // 轮转频率:daily, hourly, weekly
|
||||
size: '10m', // 文件大小限制
|
||||
limit: {
|
||||
count: 7, // 保留文件数量
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚨 注意事项
|
||||
|
||||
### 安全考虑
|
||||
|
||||
1. **敏感信息过滤**:系统自动过滤密码、token等敏感字段
|
||||
2. **访问控制**:确保日志文件只有授权用户可以访问
|
||||
3. **传输加密**:生产环境建议使用加密传输日志
|
||||
|
||||
### 性能考虑
|
||||
|
||||
1. **异步写入**:Pino 使用异步写入,不会阻塞主线程
|
||||
2. **日志级别**:生产环境建议使用 info 及以上级别
|
||||
3. **文件轮转**:及时清理和压缩日志文件,避免占用过多磁盘空间
|
||||
|
||||
### 运维建议
|
||||
|
||||
1. **监控磁盘空间**:定期检查日志目录的磁盘使用情况
|
||||
2. **备份重要日志**:对于重要的错误日志,建议定期备份
|
||||
3. **日志分析**:可以集成 ELK Stack 等日志分析工具
|
||||
4. **告警设置**:配置日志监控告警,及时发现系统问题
|
||||
|
||||
---
|
||||
|
||||
## 🔗 相关文档
|
||||
|
||||
- [后端开发规范 - 日志系统使用指南](./backend_development_guide.md#四日志系统使用指南)
|
||||
- [AI 辅助开发规范指南](./AI辅助开发规范指南.md)
|
||||
- [Pino 官方文档](https://getpino.io/)
|
||||
- [NestJS Pino 集成文档](https://github.com/iamolegga/nestjs-pino)
|
||||
|
||||
---
|
||||
|
||||
**💡 提示:使用 [AI 辅助开发指南](./AI辅助开发规范指南.md) 可以让 AI 帮你自动生成符合规范的日志代码!**
|
||||
@@ -1,334 +0,0 @@
|
||||
# 用户认证系统
|
||||
|
||||
## 概述
|
||||
|
||||
用户认证系统提供完整的用户注册、登录、密码管理功能,支持传统用户名密码登录和第三方OAuth登录。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 🔐 多种登录方式:用户名/邮箱/手机号登录
|
||||
- 📝 用户注册和信息管理
|
||||
- 🐙 GitHub OAuth 第三方登录
|
||||
- 🔄 密码重置和修改
|
||||
- 🛡️ bcrypt 密码加密
|
||||
- 🎯 基于角色的权限控制
|
||||
|
||||
## 架构设计
|
||||
|
||||
### 分层结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── business/login/ # 业务逻辑层
|
||||
│ ├── login.controller.ts # HTTP 控制器
|
||||
│ ├── login.service.ts # 业务服务
|
||||
│ ├── login.dto.ts # 数据传输对象
|
||||
│ ├── login.service.spec.ts # 业务服务测试
|
||||
│ └── login.module.ts # 业务模块
|
||||
├── core/
|
||||
│ ├── login_core/ # 核心功能层
|
||||
│ │ ├── login_core.service.ts # 核心认证逻辑
|
||||
│ │ ├── login_core.service.spec.ts # 核心服务测试
|
||||
│ │ └── login_core.module.ts # 核心模块
|
||||
│ └── db/users/ # 数据访问层
|
||||
│ ├── users.entity.ts # 用户实体
|
||||
│ ├── users.service.ts # 用户数据服务
|
||||
│ └── users.dto.ts # 用户 DTO
|
||||
```
|
||||
|
||||
### 职责分离
|
||||
|
||||
#### 1. 业务逻辑层 (Business Layer)
|
||||
- **位置**: `src/business/login/`
|
||||
- **职责**:
|
||||
- 处理HTTP请求和响应
|
||||
- 数据格式化和验证
|
||||
- 业务流程控制
|
||||
- 错误处理和日志记录
|
||||
|
||||
#### 2. 核心功能层 (Core Layer)
|
||||
- **位置**: `src/core/login_core/`
|
||||
- **职责**:
|
||||
- 认证核心算法实现
|
||||
- 密码加密和验证
|
||||
- 用户查找和匹配
|
||||
- 令牌生成和验证
|
||||
|
||||
#### 3. 数据访问层 (Data Access Layer)
|
||||
- **位置**: `src/core/db/users/`
|
||||
- **职责**:
|
||||
- 数据库操作封装
|
||||
- 实体关系映射
|
||||
- 数据完整性保证
|
||||
- 查询优化
|
||||
|
||||
## API 接口
|
||||
|
||||
### 用户注册
|
||||
|
||||
```bash
|
||||
POST /auth/register
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "testuser",
|
||||
"password": "password123",
|
||||
"nickname": "测试用户",
|
||||
"email": "test@example.com",
|
||||
"phone": "+8613800138000"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"user": {
|
||||
"id": "1",
|
||||
"username": "testuser",
|
||||
"nickname": "测试用户",
|
||||
"email": "test@example.com",
|
||||
"phone": "+8613800138000",
|
||||
"avatar_url": null,
|
||||
"role": 1,
|
||||
"created_at": "2025-12-17T10:00:00.000Z"
|
||||
},
|
||||
"access_token": "eyJ1c2VySWQiOiIxIiwidXNlcm5hbWUiOiJ0ZXN0dXNlciJ9...",
|
||||
"is_new_user": true,
|
||||
"message": "注册成功"
|
||||
},
|
||||
"message": "注册成功"
|
||||
}
|
||||
```
|
||||
|
||||
### 用户登录
|
||||
|
||||
```bash
|
||||
POST /auth/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"identifier": "testuser", # 支持用户名/邮箱/手机号
|
||||
"password": "password123"
|
||||
}
|
||||
```
|
||||
|
||||
### GitHub OAuth登录
|
||||
|
||||
```bash
|
||||
POST /auth/github
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"github_id": "12345678",
|
||||
"username": "githubuser",
|
||||
"nickname": "GitHub用户",
|
||||
"email": "github@example.com",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/12345678"
|
||||
}
|
||||
```
|
||||
|
||||
### 密码重置
|
||||
|
||||
```bash
|
||||
# 1. 发送验证码
|
||||
POST /auth/forgot-password
|
||||
{
|
||||
"identifier": "test@example.com"
|
||||
}
|
||||
|
||||
# 2. 重置密码
|
||||
POST /auth/reset-password
|
||||
{
|
||||
"identifier": "test@example.com",
|
||||
"verification_code": "123456",
|
||||
"new_password": "newpassword123"
|
||||
}
|
||||
```
|
||||
|
||||
### 修改密码
|
||||
|
||||
```bash
|
||||
PUT /auth/change-password
|
||||
{
|
||||
"user_id": "1",
|
||||
"old_password": "password123",
|
||||
"new_password": "newpassword123"
|
||||
}
|
||||
```
|
||||
|
||||
## 数据模型
|
||||
|
||||
### 用户实体 (Users Entity)
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: bigint, // 主键ID
|
||||
username: string, // 用户名(唯一)
|
||||
email?: string, // 邮箱(唯一,可选)
|
||||
phone?: string, // 手机号(唯一,可选)
|
||||
password_hash?: string, // 密码哈希(OAuth用户为空)
|
||||
nickname: string, // 显示昵称
|
||||
github_id?: string, // GitHub ID(唯一,可选)
|
||||
avatar_url?: string, // 头像URL
|
||||
role: number, // 用户角色(1-普通,9-管理员)
|
||||
created_at: Date, // 创建时间
|
||||
updated_at: Date // 更新时间
|
||||
}
|
||||
```
|
||||
|
||||
### 数据库设计特点
|
||||
|
||||
1. **唯一性约束**: username, email, phone, github_id
|
||||
2. **索引优化**: 主键、唯一索引、角色索引
|
||||
3. **字符集支持**: utf8mb4,支持emoji
|
||||
4. **数据类型**: BIGINT主键,VARCHAR字段,DATETIME时间戳
|
||||
|
||||
## 安全机制
|
||||
|
||||
### 1. 密码安全
|
||||
- **加密算法**: bcrypt (saltRounds=12)
|
||||
- **强度验证**: 最少8位,包含字母和数字
|
||||
- **存储安全**: 只存储哈希值,不存储明文
|
||||
|
||||
### 2. 数据验证
|
||||
- **输入验证**: class-validator装饰器
|
||||
- **SQL注入防护**: TypeORM参数化查询
|
||||
- **XSS防护**: 数据转义和验证
|
||||
|
||||
### 3. 访问控制
|
||||
- **令牌机制**: 基于用户信息的访问令牌
|
||||
- **角色权限**: 基于角色的访问控制(RBAC)
|
||||
- **会话管理**: 令牌生成和验证
|
||||
|
||||
### 4. 错误处理
|
||||
- **统一异常**: NestJS异常过滤器
|
||||
- **日志记录**: 操作日志和错误日志
|
||||
- **信息脱敏**: 敏感信息自动脱敏
|
||||
|
||||
## 测试覆盖
|
||||
|
||||
### 单元测试
|
||||
- 核心服务测试:`src/core/login_core/login_core.service.spec.ts`
|
||||
- 业务服务测试:`src/business/login/login.service.spec.ts`
|
||||
|
||||
### 集成测试
|
||||
- 端到端测试:`test/business/login.e2e-spec.ts`
|
||||
|
||||
### 测试用例
|
||||
- 用户注册和登录流程
|
||||
- GitHub OAuth认证
|
||||
- 密码重置和修改
|
||||
- 数据验证和错误处理
|
||||
- 安全性测试
|
||||
|
||||
## 使用示例
|
||||
|
||||
### JavaScript/TypeScript
|
||||
|
||||
```javascript
|
||||
// 用户注册
|
||||
const registerResponse = await fetch('/auth/register', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: 'testuser',
|
||||
password: 'password123',
|
||||
nickname: '测试用户',
|
||||
email: 'test@example.com'
|
||||
})
|
||||
});
|
||||
|
||||
const registerData = await registerResponse.json();
|
||||
console.log(registerData);
|
||||
|
||||
// 用户登录
|
||||
const loginResponse = await fetch('/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
identifier: 'testuser',
|
||||
password: 'password123'
|
||||
})
|
||||
});
|
||||
|
||||
const loginData = await loginResponse.json();
|
||||
console.log(loginData);
|
||||
```
|
||||
|
||||
### curl 命令
|
||||
|
||||
```bash
|
||||
# 用户注册
|
||||
curl -X POST http://localhost:3000/auth/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"username": "testuser",
|
||||
"password": "password123",
|
||||
"nickname": "测试用户",
|
||||
"email": "test@example.com"
|
||||
}'
|
||||
|
||||
# 用户登录
|
||||
curl -X POST http://localhost:3000/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"identifier": "testuser",
|
||||
"password": "password123"
|
||||
}'
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 常见错误代码
|
||||
|
||||
- `LOGIN_FAILED`: 登录失败
|
||||
- `REGISTER_FAILED`: 注册失败
|
||||
- `GITHUB_OAUTH_FAILED`: GitHub登录失败
|
||||
- `SEND_CODE_FAILED`: 发送验证码失败
|
||||
- `RESET_PASSWORD_FAILED`: 密码重置失败
|
||||
- `CHANGE_PASSWORD_FAILED`: 密码修改失败
|
||||
|
||||
### 错误响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "错误描述",
|
||||
"error_code": "ERROR_CODE"
|
||||
}
|
||||
```
|
||||
|
||||
## 扩展功能
|
||||
|
||||
### 计划中的功能
|
||||
|
||||
1. **JWT令牌管理**
|
||||
- 访问令牌和刷新令牌
|
||||
- 令牌黑名单机制
|
||||
- 自动刷新功能
|
||||
|
||||
2. **多因子认证**
|
||||
- 短信验证码
|
||||
- 邮箱验证码
|
||||
- TOTP应用支持
|
||||
|
||||
3. **社交登录扩展**
|
||||
- 微信登录
|
||||
- QQ登录
|
||||
- 微博登录
|
||||
|
||||
4. **安全增强**
|
||||
- 登录失败次数限制
|
||||
- IP白名单/黑名单
|
||||
- 设备指纹识别
|
||||
|
||||
5. **用户管理**
|
||||
- 用户状态管理(激活/禁用)
|
||||
- 用户角色权限细化
|
||||
- 用户行为日志记录
|
||||
@@ -358,7 +358,16 @@ node test-stream-initialization.js
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v2.0.0 (2025-12-25)
|
||||
### v1.1.0 (2026-01-06)
|
||||
- **修复 JWT Token 验证和 API Key 管理**
|
||||
- 修复 `LoginService.generateTokenPair()` 的 JWT 签名冲突问题
|
||||
- `ZulipService` 现在复用 `LoginService.verifyToken()` 进行 Token 验证
|
||||
- 修复消息发送时使用错误的硬编码 API Key 问题
|
||||
- 现在正确从 Redis 读取用户注册时存储的真实 API Key
|
||||
- 添加 `AuthModule` 到 `ZulipModule` 的依赖注入
|
||||
- 消息发送功能现已完全正常工作 ✅
|
||||
|
||||
### v1.0.1 (2025-12-25)
|
||||
- 更新地图配置为 9 区域系统
|
||||
- 添加 Stream Initializer Service 自动初始化服务
|
||||
- 更新默认出生点为鲸之港 (Whale Port)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
### 连接地址
|
||||
|
||||
```
|
||||
ws://localhost:3000/game
|
||||
wss://localhost:3000/game
|
||||
```
|
||||
|
||||
### 连接参数
|
||||
|
||||
@@ -334,7 +334,7 @@ configValidator.validateMapConfig(mapConfig);
|
||||
|
||||
```typescript
|
||||
// Stream 初始化服务会在系统启动 5 秒后自动运行
|
||||
// 位置: src/business/zulip/services/stream-initializer.service.ts
|
||||
// 位置: src/core/zulip_core/services/stream_initializer.service.ts
|
||||
|
||||
@Injectable()
|
||||
export class StreamInitializerService implements OnModuleInit {
|
||||
|
||||
@@ -69,3 +69,148 @@ C. 接收消息 (Downstream: Zulip -> Node -> Godot)
|
||||
- 位置欺诈完全消除: 因为 Stream 的选择权在 Node 手里,玩家无法做到“人在新手村,却往高等级区域频道发消息”。
|
||||
3. 协议统一:
|
||||
- 不再需要处理 HTTP (Zulip) 和 WebSocket (Game) 两种并发逻辑。网络层代码减少一半。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 3. JWT Token 验证和 API Key 管理 (v1.1.0 更新)
|
||||
|
||||
### 3.1 用户注册和 API Key 生成流程
|
||||
|
||||
当用户注册游戏账号时,系统会自动创建对应的 Zulip 账号并获取 API Key:
|
||||
|
||||
```
|
||||
用户注册 (POST /auth/register)
|
||||
↓
|
||||
1. 创建游戏账号 (RegisterService.register)
|
||||
↓
|
||||
2. 初始化 Zulip 管理员客户端
|
||||
↓
|
||||
3. 创建 Zulip 账号 (ZulipAccountService.createZulipAccount)
|
||||
- 使用相同的邮箱和密码
|
||||
- 调用 Zulip API: POST /api/v1/users
|
||||
↓
|
||||
4. 获取 API Key (ZulipAccountService.generateApiKeyForUser)
|
||||
- 使用 fetch_api_key 端点(固定的、基于密码的 Key)
|
||||
- 注意:不使用 regenerate_api_key(会生成新 Key)
|
||||
↓
|
||||
5. 加密存储 API Key (ApiKeySecurityService.storeApiKey)
|
||||
- 使用 AES-256-GCM 加密
|
||||
- 存储到 Redis: zulip:api_key:{userId}
|
||||
↓
|
||||
6. 创建账号关联记录 (ZulipAccountsRepository)
|
||||
- 存储 gameUserId ↔ zulipUserId 映射
|
||||
↓
|
||||
7. 生成 JWT Token (LoginService.generateTokenPair)
|
||||
- 包含用户信息:sub, username, email, role
|
||||
- 返回 access_token 和 refresh_token
|
||||
```
|
||||
|
||||
### 3.2 JWT Token 验证流程
|
||||
|
||||
当用户通过 WebSocket 登录游戏时,系统会验证 JWT Token 并获取 API Key:
|
||||
|
||||
```
|
||||
WebSocket 登录 (login 消息)
|
||||
↓
|
||||
1. ZulipService.validateGameToken(token)
|
||||
↓
|
||||
2. 调用 LoginService.verifyToken(token, 'access')
|
||||
- 验证签名、过期时间、载荷
|
||||
- 提取用户信息:userId, username, email
|
||||
↓
|
||||
3. 从 Redis 获取 API Key (ApiKeySecurityService.getApiKey)
|
||||
- 解密存储的 API Key
|
||||
- 更新访问计数和时间
|
||||
↓
|
||||
4. 创建 Zulip 客户端 (ZulipClientPoolService.createUserClient)
|
||||
- 使用真实的用户 API Key
|
||||
- 注册事件队列
|
||||
↓
|
||||
5. 创建游戏会话 (SessionManagerService.createSession)
|
||||
- 绑定 socketId ↔ zulipQueueId
|
||||
- 记录用户位置信息
|
||||
↓
|
||||
6. 返回登录成功
|
||||
```
|
||||
|
||||
### 3.3 消息发送流程(使用正确的 API Key)
|
||||
|
||||
```
|
||||
发送聊天消息 (chat 消息)
|
||||
↓
|
||||
1. ZulipService.sendChatMessage()
|
||||
↓
|
||||
2. 获取会话信息 (SessionManagerService.getSession)
|
||||
- 获取 userId 和当前位置
|
||||
↓
|
||||
3. 上下文注入 (SessionManagerService.injectContext)
|
||||
- 根据位置确定目标 Stream/Topic
|
||||
↓
|
||||
4. 消息验证 (MessageFilterService.validateMessage)
|
||||
- 内容过滤、频率限制
|
||||
↓
|
||||
5. 发送到 Zulip (ZulipClientPoolService.sendMessage)
|
||||
- 使用用户的真实 API Key
|
||||
- 调用 Zulip API: POST /api/v1/messages
|
||||
↓
|
||||
6. 返回发送结果
|
||||
```
|
||||
|
||||
### 3.4 关键修复说明
|
||||
|
||||
**问题 1: JWT Token 签名冲突**
|
||||
- **原因**: payload 中包含 `iss` 和 `aud`,但 `jwtService.signAsync()` 也通过 options 传递这些值
|
||||
- **修复**: 从 payload 中移除 `iss` 和 `aud`,只通过 options 传递
|
||||
- **文件**: `src/business/auth/services/login.service.ts`
|
||||
|
||||
**问题 2: 使用硬编码的旧 API Key**
|
||||
- **原因**: `ZulipService.validateGameToken()` 使用硬编码的测试 API Key
|
||||
- **修复**: 从 Redis 读取用户注册时存储的真实 API Key
|
||||
- **文件**: `src/business/zulip/zulip.service.ts`
|
||||
|
||||
**问题 3: 重复实现 JWT 验证逻辑**
|
||||
- **原因**: `ZulipService` 自己实现了 JWT 解析
|
||||
- **修复**: 复用 `LoginService.verifyToken()` 方法
|
||||
- **文件**: `src/business/zulip/zulip.service.ts`, `src/business/zulip/zulip.module.ts`
|
||||
|
||||
### 3.5 API Key 安全机制
|
||||
|
||||
**加密存储**:
|
||||
- 使用 AES-256-GCM 算法加密
|
||||
- 加密密钥从环境变量 `ZULIP_API_KEY_ENCRYPTION_KEY` 读取
|
||||
- 存储格式:`{encryptedKey, iv, authTag, createdAt, updatedAt, accessCount}`
|
||||
|
||||
**访问控制**:
|
||||
- 频率限制:每分钟最多 60 次访问
|
||||
- 访问日志:记录每次访问的时间和次数
|
||||
- 安全事件:记录所有关键操作(存储、访问、更新、删除)
|
||||
|
||||
**环境变量配置**:
|
||||
```bash
|
||||
# 生成 64 字符的十六进制密钥(32 字节 = 256 位)
|
||||
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||
|
||||
# 在 .env 文件中配置
|
||||
ZULIP_API_KEY_ENCRYPTION_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
|
||||
```
|
||||
|
||||
### 3.6 测试验证
|
||||
|
||||
使用测试脚本验证功能:
|
||||
|
||||
```bash
|
||||
# 测试注册用户的 Zulip 集成
|
||||
node docs/systems/zulip/quick_tests/test-registered-user.js
|
||||
|
||||
# 验证 API Key 一致性
|
||||
node docs/systems/zulip/quick_tests/verify-api-key.js
|
||||
```
|
||||
|
||||
**预期结果**:
|
||||
- ✅ WebSocket 连接成功
|
||||
- ✅ JWT Token 验证通过
|
||||
- ✅ 从 Redis 获取正确的 API Key
|
||||
- ✅ 消息成功发送到 Zulip
|
||||
|
||||
---
|
||||
|
||||
260
docs/systems/zulip/quick_tests/test-get-messages.js
Normal file
260
docs/systems/zulip/quick_tests/test-get-messages.js
Normal file
@@ -0,0 +1,260 @@
|
||||
/**
|
||||
* 测试通过 WebSocket 接收 Zulip 消息
|
||||
*
|
||||
* 设计理念:
|
||||
* - Zulip API Key 永不下发到客户端
|
||||
* - 所有 Zulip 交互通过游戏服务器的 WebSocket 进行
|
||||
* - 客户端只接收 chat_render 消息,不直接调用 Zulip API
|
||||
*
|
||||
* 功能:
|
||||
* 1. 登录游戏服务器获取 JWT Token
|
||||
* 2. 通过 WebSocket 连接游戏服务器
|
||||
* 3. 在当前地图 (Whale Port) 接收消息
|
||||
* 4. 切换到 Pumpkin Valley 接收消息
|
||||
* 5. 统计接收到的消息数量
|
||||
*
|
||||
* 使用方法:
|
||||
* node docs/systems/zulip/quick_tests/test-get-messages.js
|
||||
*/
|
||||
|
||||
const axios = require('axios');
|
||||
const io = require('socket.io-client');
|
||||
|
||||
// 配置
|
||||
const GAME_SERVER = 'http://localhost:3000';
|
||||
const TEST_USER = {
|
||||
username: 'angtest123',
|
||||
password: 'angtest123',
|
||||
email: 'angjustinl@163.com'
|
||||
};
|
||||
|
||||
// 测试配置
|
||||
const TEST_CONFIG = {
|
||||
whalePortWaitTime: 10000, // 在 Whale Port 等待 10 秒
|
||||
pumpkinValleyWaitTime: 10000, // 在 Pumpkin Valley 等待 10 秒
|
||||
totalTimeout: 30000 // 总超时时间 30 秒
|
||||
};
|
||||
|
||||
/**
|
||||
* 登录游戏服务器获取用户信息
|
||||
*/
|
||||
async function loginToGameServer() {
|
||||
console.log('📝 步骤 1: 登录游戏服务器');
|
||||
console.log(` 用户名: ${TEST_USER.username}`);
|
||||
|
||||
try {
|
||||
const response = await axios.post(`${GAME_SERVER}/auth/login`, {
|
||||
identifier: TEST_USER.username,
|
||||
password: TEST_USER.password
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
console.log('✅ 登录成功');
|
||||
console.log(` 用户ID: ${response.data.data.user.id}`);
|
||||
console.log(` 邮箱: ${response.data.data.user.email}`);
|
||||
return {
|
||||
userId: response.data.data.user.id,
|
||||
username: response.data.data.user.username,
|
||||
email: response.data.data.user.email,
|
||||
token: response.data.data.access_token
|
||||
};
|
||||
} else {
|
||||
throw new Error(response.data.message || '登录失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 登录失败:', error.response?.data?.message || error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 通过 WebSocket 接收消息
|
||||
*/
|
||||
async function receiveMessagesViaWebSocket(userInfo) {
|
||||
console.log('\n📡 步骤 2: 通过 WebSocket 连接并接收消息');
|
||||
console.log(` 连接到: ${GAME_SERVER}/game`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = io(`${GAME_SERVER}/game`, {
|
||||
transports: ['websocket'],
|
||||
timeout: 20000
|
||||
});
|
||||
|
||||
const receivedMessages = {
|
||||
whalePort: [],
|
||||
pumpkinValley: []
|
||||
};
|
||||
|
||||
let currentMap = 'whale_port';
|
||||
let testPhase = 0; // 0: 连接中, 1: Whale Port, 2: Pumpkin Valley, 3: 完成
|
||||
|
||||
// 连接成功
|
||||
socket.on('connect', () => {
|
||||
console.log('✅ WebSocket 连接成功');
|
||||
|
||||
// 发送登录消息
|
||||
const loginMessage = {
|
||||
type: 'login',
|
||||
token: userInfo.token
|
||||
};
|
||||
|
||||
console.log('📤 发送登录消息...');
|
||||
socket.emit('login', loginMessage);
|
||||
});
|
||||
|
||||
// 登录成功
|
||||
socket.on('login_success', (data) => {
|
||||
console.log('✅ 登录成功');
|
||||
console.log(` 会话ID: ${data.sessionId}`);
|
||||
console.log(` 用户ID: ${data.userId}`);
|
||||
console.log(` 当前地图: ${data.currentMap}`);
|
||||
|
||||
testPhase = 1;
|
||||
currentMap = data.currentMap || 'whale_port';
|
||||
|
||||
console.log(`\n📬 步骤 3: 在 Whale Port 接收消息 (等待 ${TEST_CONFIG.whalePortWaitTime / 1000} 秒)`);
|
||||
console.log(' 💡 提示: 请在 Zulip 的 "Whale Port" Stream 发送测试消息');
|
||||
|
||||
// 在 Whale Port 等待一段时间
|
||||
setTimeout(() => {
|
||||
console.log(`\n📊 Whale Port 接收到 ${receivedMessages.whalePort.length} 条消息`);
|
||||
|
||||
// 切换到 Pumpkin Valley
|
||||
console.log(`\n📤 步骤 4: 切换到 Pumpkin Valley`);
|
||||
const positionUpdate = {
|
||||
t: 'position',
|
||||
x: 150,
|
||||
y: 400,
|
||||
mapId: 'pumpkin_valley'
|
||||
};
|
||||
socket.emit('position_update', positionUpdate);
|
||||
|
||||
testPhase = 2;
|
||||
currentMap = 'pumpkin_valley';
|
||||
|
||||
console.log(`\n📬 步骤 5: 在 Pumpkin Valley 接收消息 (等待 ${TEST_CONFIG.pumpkinValleyWaitTime / 1000} 秒)`);
|
||||
console.log(' 💡 提示: 请在 Zulip 的 "Pumpkin Valley" Stream 发送测试消息');
|
||||
|
||||
// 在 Pumpkin Valley 等待一段时间
|
||||
setTimeout(() => {
|
||||
console.log(`\n📊 Pumpkin Valley 接收到 ${receivedMessages.pumpkinValley.length} 条消息`);
|
||||
|
||||
testPhase = 3;
|
||||
console.log('\n📊 测试完成,断开连接...');
|
||||
socket.disconnect();
|
||||
}, TEST_CONFIG.pumpkinValleyWaitTime);
|
||||
}, TEST_CONFIG.whalePortWaitTime);
|
||||
});
|
||||
|
||||
// 接收到消息 (chat_render)
|
||||
socket.on('chat_render', (data) => {
|
||||
const timestamp = new Date().toLocaleTimeString('zh-CN');
|
||||
|
||||
console.log(`\n📨 [${timestamp}] 收到消息:`);
|
||||
console.log(` ├─ 发送者: ${data.from}`);
|
||||
console.log(` ├─ 内容: ${data.txt}`);
|
||||
console.log(` ├─ Stream: ${data.stream || '未知'}`);
|
||||
console.log(` ├─ Topic: ${data.topic || '未知'}`);
|
||||
console.log(` └─ 当前地图: ${currentMap}`);
|
||||
|
||||
// 记录消息
|
||||
const message = {
|
||||
from: data.from,
|
||||
content: data.txt,
|
||||
stream: data.stream,
|
||||
topic: data.topic,
|
||||
timestamp: new Date(),
|
||||
map: currentMap
|
||||
};
|
||||
|
||||
if (testPhase === 1) {
|
||||
receivedMessages.whalePort.push(message);
|
||||
} else if (testPhase === 2) {
|
||||
receivedMessages.pumpkinValley.push(message);
|
||||
}
|
||||
});
|
||||
|
||||
// 错误处理
|
||||
socket.on('error', (error) => {
|
||||
console.error('❌ 收到错误:', JSON.stringify(error, null, 2));
|
||||
});
|
||||
|
||||
// 连接断开
|
||||
socket.on('disconnect', () => {
|
||||
console.log('\n🔌 WebSocket 连接已关闭');
|
||||
resolve(receivedMessages);
|
||||
});
|
||||
|
||||
// 连接错误
|
||||
socket.on('connect_error', (error) => {
|
||||
console.error('❌ 连接错误:', error.message);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
// 总超时保护
|
||||
setTimeout(() => {
|
||||
if (socket.connected) {
|
||||
console.log('\n⏰ 测试超时,关闭连接');
|
||||
socket.disconnect();
|
||||
}
|
||||
}, TEST_CONFIG.totalTimeout);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 主测试流程
|
||||
*/
|
||||
async function runTest() {
|
||||
console.log('🚀 开始测试通过 WebSocket 接收 Zulip 消息');
|
||||
console.log('='.repeat(60));
|
||||
console.log('📋 设计理念: Zulip API Key 永不下发到客户端');
|
||||
console.log('📋 所有消息通过游戏服务器的 WebSocket (chat_render) 接收');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
try {
|
||||
// 步骤1: 登录游戏服务器
|
||||
const userInfo = await loginToGameServer();
|
||||
|
||||
// 步骤2-5: 通过 WebSocket 接收消息
|
||||
const receivedMessages = await receiveMessagesViaWebSocket(userInfo);
|
||||
|
||||
// 步骤6: 统计信息
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('📊 测试结果汇总');
|
||||
console.log('='.repeat(60));
|
||||
console.log(`✅ Whale Port: ${receivedMessages.whalePort.length} 条消息`);
|
||||
console.log(`✅ Pumpkin Valley: ${receivedMessages.pumpkinValley.length} 条消息`);
|
||||
console.log(`📝 总计: ${receivedMessages.whalePort.length + receivedMessages.pumpkinValley.length} 条消息`);
|
||||
|
||||
// 显示详细消息列表
|
||||
if (receivedMessages.whalePort.length > 0) {
|
||||
console.log('\n📬 Whale Port 消息列表:');
|
||||
receivedMessages.whalePort.forEach((msg, index) => {
|
||||
console.log(` ${index + 1}. [${msg.timestamp.toLocaleTimeString()}] ${msg.from}: ${msg.content.substring(0, 50)}${msg.content.length > 50 ? '...' : ''}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (receivedMessages.pumpkinValley.length > 0) {
|
||||
console.log('\n📬 Pumpkin Valley 消息列表:');
|
||||
receivedMessages.pumpkinValley.forEach((msg, index) => {
|
||||
console.log(` ${index + 1}. [${msg.timestamp.toLocaleTimeString()}] ${msg.from}: ${msg.content.substring(0, 50)}${msg.content.length > 50 ? '...' : ''}`);
|
||||
});
|
||||
}
|
||||
|
||||
console.log('='.repeat(60));
|
||||
|
||||
console.log('\n🎉 测试完成!');
|
||||
console.log('💡 提示: 客户端通过 WebSocket 接收消息,无需直接访问 Zulip API');
|
||||
console.log('💡 提示: 访问 https://zulip.xinghangee.icu 查看完整消息历史');
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('\n❌ 测试失败:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 运行测试
|
||||
runTest();
|
||||
@@ -1,15 +1,102 @@
|
||||
const zulip = require('zulip-js');
|
||||
const axios = require('axios');
|
||||
|
||||
// 配置
|
||||
const GAME_SERVER = 'http://localhost:3000';
|
||||
const TEST_USER = {
|
||||
username: 'angtest123',
|
||||
password: 'angtest123',
|
||||
email: 'angjustinl@163.com'
|
||||
};
|
||||
|
||||
/**
|
||||
* 登录游戏服务器获取用户信息
|
||||
*/
|
||||
async function loginToGameServer() {
|
||||
console.log('📝 步骤 1: 登录游戏服务器');
|
||||
console.log(` 用户名: ${TEST_USER.username}`);
|
||||
|
||||
try {
|
||||
const response = await axios.post(`${GAME_SERVER}/auth/login`, {
|
||||
identifier: TEST_USER.username,
|
||||
password: TEST_USER.password
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
console.log('✅ 登录成功');
|
||||
console.log(` 用户ID: ${response.data.data.user.id}`);
|
||||
console.log(` 邮箱: ${response.data.data.user.email}`);
|
||||
return {
|
||||
userId: response.data.data.user.id,
|
||||
username: response.data.data.user.username,
|
||||
email: response.data.data.user.email,
|
||||
token: response.data.data.access_token
|
||||
};
|
||||
} else {
|
||||
throw new Error(response.data.message || '登录失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 登录失败:', error.response?.data?.message || error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用密码获取 Zulip API Key
|
||||
*/
|
||||
async function getZulipApiKey(email, password) {
|
||||
console.log('\n📝 步骤 2: 获取 Zulip API Key');
|
||||
console.log(` 邮箱: ${email}`);
|
||||
|
||||
try {
|
||||
// Zulip API 使用 Basic Auth 和 form data
|
||||
const response = await axios.post(
|
||||
'https://zulip.xinghangee.icu/api/v1/fetch_api_key',
|
||||
`username=${encodeURIComponent(email)}&password=${encodeURIComponent(password)}`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (response.data.result === 'success') {
|
||||
console.log('✅ 成功获取 API Key');
|
||||
console.log(` API Key: ${response.data.api_key.substring(0, 10)}...`);
|
||||
console.log(` 用户ID: ${response.data.user_id}`);
|
||||
return {
|
||||
apiKey: response.data.api_key,
|
||||
email: response.data.email,
|
||||
userId: response.data.user_id
|
||||
};
|
||||
} else {
|
||||
throw new Error(response.data.msg || '获取 API Key 失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 获取 API Key 失败:', error.response?.data?.msg || error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function listSubscriptions() {
|
||||
console.log('🔧 检查用户订阅的 Streams...');
|
||||
console.log('🚀 开始测试用户订阅的 Streams');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
try {
|
||||
// 步骤1: 登录游戏服务器
|
||||
const userInfo = await loginToGameServer();
|
||||
|
||||
// 步骤2: 获取 Zulip API Key
|
||||
const zulipAuth = await getZulipApiKey(userInfo.email, TEST_USER.password);
|
||||
|
||||
console.log('\n📝 步骤 3: 检查用户订阅的 Streams');
|
||||
|
||||
const config = {
|
||||
username: 'angjustinl@mail.angforever.top',
|
||||
apiKey: 'lCPWC...pqNfGF8',
|
||||
username: zulipAuth.email,
|
||||
apiKey: zulipAuth.apiKey,
|
||||
realm: 'https://zulip.xinghangee.icu/'
|
||||
};
|
||||
|
||||
try {
|
||||
const client = await zulip(config);
|
||||
|
||||
// 获取用户信息
|
||||
@@ -29,15 +116,15 @@ async function listSubscriptions() {
|
||||
});
|
||||
|
||||
// 检查是否有 "Novice Village"
|
||||
const noviceVillage = subscriptions.subscriptions.find(s => s.name === 'Novice Village');
|
||||
const noviceVillage = subscriptions.subscriptions.find(s => s.name === 'Pumpkin Valley');
|
||||
if (noviceVillage) {
|
||||
console.log('\n✅ "Novice Village" Stream 已存在!');
|
||||
console.log('\n✅ "Pumpkin Valley" Stream 已存在!');
|
||||
|
||||
// 测试发送消息
|
||||
console.log('\n📤 测试发送消息...');
|
||||
const result = await client.messages.send({
|
||||
type: 'stream',
|
||||
to: 'Novice Village',
|
||||
to: 'Pumpkin Valley',
|
||||
subject: 'General',
|
||||
content: '测试消息:系统集成测试成功 🎮'
|
||||
});
|
||||
@@ -48,7 +135,7 @@ async function listSubscriptions() {
|
||||
console.log('❌ 消息发送失败:', result.msg);
|
||||
}
|
||||
} else {
|
||||
console.log('\n⚠️ "Novice Village" Stream 不存在');
|
||||
console.log('\n⚠️ "Pumpkin Valley" Stream 不存在');
|
||||
console.log('💡 请在 Zulip 网页界面手动创建该 Stream,或使用管理员账号创建');
|
||||
|
||||
// 尝试发送到第一个可用的 Stream
|
||||
@@ -79,7 +166,9 @@ async function listSubscriptions() {
|
||||
if (error.response) {
|
||||
console.error('响应数据:', error.response.data);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 运行测试
|
||||
listSubscriptions();
|
||||
|
||||
232
docs/systems/zulip/quick_tests/test-registered-user.js
Normal file
232
docs/systems/zulip/quick_tests/test-registered-user.js
Normal file
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* 测试新注册用户的Zulip账号功能
|
||||
*
|
||||
* 功能:
|
||||
* 1. 验证新注册用户可以通过游戏服务器登录
|
||||
* 2. 验证Zulip账号已正确创建和关联
|
||||
* 3. 验证用户可以通过WebSocket发送消息到Zulip
|
||||
* 4. 验证用户可以接收来自Zulip的消息
|
||||
*
|
||||
* 使用方法:
|
||||
* node docs/systems/zulip/quick_tests/test-registered-user.js
|
||||
*/
|
||||
|
||||
const io = require('socket.io-client');
|
||||
const axios = require('axios');
|
||||
|
||||
// 配置
|
||||
const GAME_SERVER = 'http://localhost:3000';
|
||||
const TEST_USER = {
|
||||
username: 'angtest123',
|
||||
password: 'angtest123',
|
||||
email: 'angjustinl@163.com'
|
||||
};
|
||||
|
||||
/**
|
||||
* 步骤1: 登录游戏服务器获取token
|
||||
*/
|
||||
async function loginToGameServer() {
|
||||
console.log('📝 步骤 1: 登录游戏服务器');
|
||||
console.log(` 用户名: ${TEST_USER.username}`);
|
||||
|
||||
try {
|
||||
const response = await axios.post(`${GAME_SERVER}/auth/login`, {
|
||||
identifier: TEST_USER.username,
|
||||
password: TEST_USER.password
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
console.log('✅ 登录成功');
|
||||
console.log(` 用户ID: ${response.data.data.user.id}`);
|
||||
console.log(` 昵称: ${response.data.data.user.nickname}`);
|
||||
console.log(` 邮箱: ${response.data.data.user.email}`);
|
||||
console.log(` Token: ${response.data.data.access_token.substring(0, 20)}...`);
|
||||
return {
|
||||
userId: response.data.data.user.id,
|
||||
username: response.data.data.user.username,
|
||||
token: response.data.data.access_token
|
||||
};
|
||||
} else {
|
||||
throw new Error(response.data.message || '登录失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 登录失败:', error.response?.data?.message || error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 步骤2: 通过WebSocket连接并测试Zulip集成
|
||||
*/
|
||||
async function testZulipIntegration(userInfo) {
|
||||
console.log('\n📡 步骤 2: 测试 Zulip 集成');
|
||||
console.log(` 连接到: ${GAME_SERVER}/game`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = io(`${GAME_SERVER}/game`, {
|
||||
transports: ['websocket'],
|
||||
timeout: 20000
|
||||
});
|
||||
|
||||
let testStep = 0;
|
||||
let testResults = {
|
||||
connected: false,
|
||||
loggedIn: false,
|
||||
messageSent: false,
|
||||
messageReceived: false
|
||||
};
|
||||
|
||||
// 连接成功
|
||||
socket.on('connect', () => {
|
||||
console.log('✅ WebSocket 连接成功');
|
||||
testResults.connected = true;
|
||||
testStep = 1;
|
||||
|
||||
// 发送登录消息
|
||||
const loginMessage = {
|
||||
type: 'login',
|
||||
token: userInfo.token
|
||||
};
|
||||
|
||||
console.log('📤 发送登录消息...');
|
||||
socket.emit('login', loginMessage);
|
||||
});
|
||||
|
||||
// 登录成功
|
||||
socket.on('login_success', (data) => {
|
||||
console.log('✅ 登录成功');
|
||||
console.log(` 会话ID: ${data.sessionId}`);
|
||||
console.log(` 用户ID: ${data.userId}`);
|
||||
console.log(` 用户名: ${data.username}`);
|
||||
console.log(` 当前地图: ${data.currentMap}`);
|
||||
testResults.loggedIn = true;
|
||||
testStep = 2;
|
||||
|
||||
// 等待Zulip客户端初始化
|
||||
console.log('\n⏳ 等待 3 秒让 Zulip 客户端初始化...');
|
||||
setTimeout(() => {
|
||||
const chatMessage = {
|
||||
t: 'chat',
|
||||
content: `🎮 【注册用户测试】来自 ${userInfo.username} 的消息!\n` +
|
||||
`时间: ${new Date().toLocaleString()}\n` +
|
||||
`这是通过新注册账号发送的测试消息。`,
|
||||
scope: 'local'
|
||||
};
|
||||
|
||||
console.log('📤 发送测试消息到 Zulip...');
|
||||
console.log(` 内容: ${chatMessage.content.split('\n')[0]}`);
|
||||
socket.emit('chat', chatMessage);
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
// 消息发送成功
|
||||
socket.on('chat_sent', (data) => {
|
||||
console.log('✅ 消息发送成功');
|
||||
console.log(` 消息ID: ${data.id || '未知'}`);
|
||||
testResults.messageSent = true;
|
||||
testStep = 3;
|
||||
|
||||
// 等待一段时间接收消息
|
||||
setTimeout(() => {
|
||||
console.log('\n📊 测试完成,断开连接...');
|
||||
socket.disconnect();
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
// 接收到消息
|
||||
socket.on('chat_render', (data) => {
|
||||
console.log('📨 收到来自 Zulip 的消息:');
|
||||
console.log(` 发送者: ${data.from}`);
|
||||
console.log(` 内容: ${data.txt}`);
|
||||
console.log(` Stream: ${data.stream || '未知'}`);
|
||||
console.log(` Topic: ${data.topic || '未知'}`);
|
||||
testResults.messageReceived = true;
|
||||
});
|
||||
|
||||
// 错误处理
|
||||
socket.on('error', (error) => {
|
||||
console.error('❌ 收到错误:', JSON.stringify(error, null, 2));
|
||||
});
|
||||
|
||||
// 连接断开
|
||||
socket.on('disconnect', () => {
|
||||
console.log('\n🔌 WebSocket 连接已关闭');
|
||||
resolve(testResults);
|
||||
});
|
||||
|
||||
// 连接错误
|
||||
socket.on('connect_error', (error) => {
|
||||
console.error('❌ 连接错误:', error.message);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
// 超时保护
|
||||
setTimeout(() => {
|
||||
if (socket.connected) {
|
||||
socket.disconnect();
|
||||
}
|
||||
}, 15000);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印测试结果
|
||||
*/
|
||||
function printTestResults(results) {
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('📊 测试结果汇总');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
const checks = [
|
||||
{ name: 'WebSocket 连接', passed: results.connected },
|
||||
{ name: '游戏服务器登录', passed: results.loggedIn },
|
||||
{ name: '发送消息到 Zulip', passed: results.messageSent },
|
||||
{ name: '接收 Zulip 消息', passed: results.messageReceived }
|
||||
];
|
||||
|
||||
checks.forEach(check => {
|
||||
const icon = check.passed ? '✅' : '❌';
|
||||
console.log(`${icon} ${check.name}: ${check.passed ? '通过' : '失败'}`);
|
||||
});
|
||||
|
||||
const passedCount = checks.filter(c => c.passed).length;
|
||||
const totalCount = checks.length;
|
||||
|
||||
console.log('='.repeat(60));
|
||||
console.log(`总计: ${passedCount}/${totalCount} 项测试通过`);
|
||||
|
||||
if (passedCount === totalCount) {
|
||||
console.log('\n🎉 所有测试通过!Zulip账号创建和集成功能正常!');
|
||||
console.log('💡 提示: 请访问 https://zulip.xinghangee.icu/ 查看发送的消息');
|
||||
} else {
|
||||
console.log('\n⚠️ 部分测试失败,请检查日志');
|
||||
}
|
||||
console.log('='.repeat(60));
|
||||
}
|
||||
|
||||
/**
|
||||
* 主测试流程
|
||||
*/
|
||||
async function runTest() {
|
||||
console.log('🚀 开始测试新注册用户的 Zulip 集成功能');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
try {
|
||||
// 步骤1: 登录
|
||||
const userInfo = await loginToGameServer();
|
||||
|
||||
// 步骤2: 测试Zulip集成
|
||||
const results = await testZulipIntegration(userInfo);
|
||||
|
||||
// 打印结果
|
||||
printTestResults(results);
|
||||
|
||||
process.exit(results.connected && results.loggedIn && results.messageSent ? 0 : 1);
|
||||
} catch (error) {
|
||||
console.error('\n❌ 测试失败:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 运行测试
|
||||
runTest();
|
||||
@@ -1,13 +1,60 @@
|
||||
const io = require('socket.io-client');
|
||||
const axios = require('axios');
|
||||
|
||||
// 配置
|
||||
const GAME_SERVER = 'http://localhost:3000';
|
||||
const TEST_USER = {
|
||||
username: 'angtest123',
|
||||
password: 'angtest123',
|
||||
email: 'angjustinl@163.com'
|
||||
};
|
||||
|
||||
/**
|
||||
* 登录游戏服务器获取token
|
||||
*/
|
||||
async function loginToGameServer() {
|
||||
console.log('📝 步骤 1: 登录游戏服务器');
|
||||
console.log(` 用户名: ${TEST_USER.username}`);
|
||||
|
||||
try {
|
||||
const response = await axios.post(`${GAME_SERVER}/auth/login`, {
|
||||
identifier: TEST_USER.username,
|
||||
password: TEST_USER.password
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
console.log('✅ 登录成功');
|
||||
console.log(` 用户ID: ${response.data.data.user.id}`);
|
||||
console.log(` 昵称: ${response.data.data.user.nickname}`);
|
||||
console.log(` 邮箱: ${response.data.data.user.email}`);
|
||||
console.log(` Token: ${response.data.data.access_token.substring(0, 20)}...`);
|
||||
return {
|
||||
userId: response.data.data.user.id,
|
||||
username: response.data.data.user.username,
|
||||
token: response.data.data.access_token
|
||||
};
|
||||
} else {
|
||||
throw new Error(response.data.message || '登录失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 登录失败:', error.response?.data?.message || error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 使用用户 API Key 测试 Zulip 集成
|
||||
async function testWithUserApiKey() {
|
||||
console.log('🚀 使用用户 API Key 测试 Zulip 集成...');
|
||||
console.log('📡 用户 API Key: lCPWCPfGh7WU...pqNfGF8');
|
||||
console.log('📡 Zulip 服务器: https://zulip.xinghangee.icu/');
|
||||
console.log('📡 游戏服务器: http://localhost:3000/game');
|
||||
console.log('🚀 开始测试用户 API Key 的 Zulip 集成');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
const socket = io('http://localhost:3000/game', {
|
||||
try {
|
||||
// 登录获取 token
|
||||
const userInfo = await loginToGameServer();
|
||||
|
||||
console.log('\n📡 步骤 2: 连接 WebSocket 并测试 Zulip 集成');
|
||||
console.log(` 连接到: ${GAME_SERVER}/game`);
|
||||
|
||||
const socket = io(`${GAME_SERVER}/game`, {
|
||||
transports: ['websocket'],
|
||||
timeout: 20000
|
||||
});
|
||||
@@ -18,18 +65,18 @@ async function testWithUserApiKey() {
|
||||
console.log('✅ WebSocket 连接成功');
|
||||
testStep = 1;
|
||||
|
||||
// 使用包含用户 API Key 的 token
|
||||
// 使用真实的 JWT token
|
||||
const loginMessage = {
|
||||
type: 'login',
|
||||
token: 'lCPWCPfGh7...fGF8_user_token'
|
||||
token: userInfo.token
|
||||
};
|
||||
|
||||
console.log('📤 步骤 1: 发送登录消息(使用用户 API Key)');
|
||||
console.log('📤 步骤 3: 发送登录消息(使用 JWT Token)');
|
||||
socket.emit('login', loginMessage);
|
||||
});
|
||||
|
||||
socket.on('login_success', (data) => {
|
||||
console.log('✅ 步骤 1 完成: 登录成功');
|
||||
console.log('✅ 步骤 3 完成: 登录成功');
|
||||
console.log(' 会话ID:', data.sessionId);
|
||||
console.log(' 用户ID:', data.userId);
|
||||
console.log(' 用户名:', data.username);
|
||||
@@ -37,24 +84,24 @@ async function testWithUserApiKey() {
|
||||
testStep = 2;
|
||||
|
||||
// 等待 Zulip 客户端初始化
|
||||
console.log('⏳ 等待 3 秒让 Zulip 客户端初始化...');
|
||||
console.log('\n⏳ 等待 3 秒让 Zulip 客户端初始化...');
|
||||
setTimeout(() => {
|
||||
const chatMessage = {
|
||||
t: 'chat',
|
||||
content: '🎮 【用户API Key测试】来自游戏的消息!\\n' +
|
||||
'时间: ' + new Date().toLocaleString() + '\\n' +
|
||||
'使用用户 API Key 发送此消息。',
|
||||
content: `🎮 【用户API Key测试】来自 ${userInfo.username} 的消息!\n` +
|
||||
`时间: ${new Date().toLocaleString()}\n` +
|
||||
`使用真实 API Key 发送此消息。`,
|
||||
scope: 'local'
|
||||
};
|
||||
|
||||
console.log('📤 步骤 2: 发送消息到 Zulip(使用用户 API Key)');
|
||||
console.log('📤 步骤 4: 发送消息到 Zulip(使用真实 API Key)');
|
||||
console.log(' 目标 Stream: Whale Port');
|
||||
socket.emit('chat', chatMessage);
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
socket.on('chat_sent', (data) => {
|
||||
console.log('✅ 步骤 2 完成: 消息发送成功');
|
||||
console.log('✅ 步骤 4 完成: 消息发送成功');
|
||||
console.log(' 响应:', JSON.stringify(data, null, 2));
|
||||
|
||||
// 只在第一次收到 chat_sent 时发送第二条消息
|
||||
@@ -63,7 +110,7 @@ async function testWithUserApiKey() {
|
||||
|
||||
setTimeout(() => {
|
||||
// 先切换到 Pumpkin Valley 地图
|
||||
console.log('📤 步骤 3: 切换到 Pumpkin Valley 地图');
|
||||
console.log('\n📤 步骤 5: 切换到 Pumpkin Valley 地图');
|
||||
const positionUpdate = {
|
||||
t: 'position',
|
||||
x: 150,
|
||||
@@ -80,7 +127,7 @@ async function testWithUserApiKey() {
|
||||
scope: 'local'
|
||||
};
|
||||
|
||||
console.log('📤 步骤 4: 在 Pumpkin Valley 发送消息');
|
||||
console.log('📤 步骤 6: 在 Pumpkin Valley 发送消息');
|
||||
socket.emit('chat', chatMessage2);
|
||||
}, 1000);
|
||||
}, 2000);
|
||||
@@ -88,7 +135,7 @@ async function testWithUserApiKey() {
|
||||
});
|
||||
|
||||
socket.on('chat_render', (data) => {
|
||||
console.log('📨 收到来自 Zulip 的消息:');
|
||||
console.log('\n📨 收到来自 Zulip 的消息:');
|
||||
console.log(' 发送者:', data.from);
|
||||
console.log(' 内容:', data.txt);
|
||||
console.log(' Stream:', data.stream || '未知');
|
||||
@@ -100,15 +147,19 @@ async function testWithUserApiKey() {
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('🔌 WebSocket 连接已关闭');
|
||||
console.log('');
|
||||
console.log('📊 测试结果:');
|
||||
console.log(' 完成步骤:', testStep, '/ 4');
|
||||
console.log('\n🔌 WebSocket 连接已关闭');
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('📊 测试结果汇总');
|
||||
console.log('='.repeat(60));
|
||||
console.log(' 完成步骤:', testStep, '/ 3');
|
||||
if (testStep >= 3) {
|
||||
console.log(' ✅ 核心功能正常!');
|
||||
console.log(' 💡 请检查 Zulip 中的 "Whale Port" 和 "Pumpkin Valley" Streams 查看消息');
|
||||
} else {
|
||||
console.log(' ⚠️ 部分测试未完成');
|
||||
}
|
||||
process.exit(0);
|
||||
console.log('='.repeat(60));
|
||||
process.exit(testStep >= 3 ? 0 : 1);
|
||||
});
|
||||
|
||||
socket.on('connect_error', (error) => {
|
||||
@@ -118,10 +169,15 @@ async function testWithUserApiKey() {
|
||||
|
||||
// 20秒后自动关闭(给足够时间完成测试)
|
||||
setTimeout(() => {
|
||||
console.log('⏰ 测试时间到,关闭连接');
|
||||
console.log('\n⏰ 测试时间到,关闭连接');
|
||||
socket.disconnect();
|
||||
}, 20000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ 测试失败:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🔧 准备测试环境...');
|
||||
testWithUserApiKey().catch(console.error);
|
||||
// 运行测试
|
||||
testWithUserApiKey();
|
||||
399
docs/开发者代码检查规范.md
Normal file
399
docs/开发者代码检查规范.md
Normal file
@@ -0,0 +1,399 @@
|
||||
# 开发者代码检查规范
|
||||
|
||||
## 🎯 规范目标
|
||||
|
||||
本规范旨在确保代码质量、提升开发效率、维护项目一致性。通过系统化的代码检查流程,保障Whale Town游戏服务器项目的代码标准和技术质量。
|
||||
|
||||
## 📋 检查流程概述
|
||||
|
||||
代码检查分为7个步骤,必须按顺序执行,每步完成后等待确认才能进行下一步:
|
||||
|
||||
1. **步骤1:命名规范检查** - 文件、变量、类、常量命名规范
|
||||
2. **步骤2:注释规范检查** - 文件头、类、方法注释完整性
|
||||
3. **步骤3:代码质量检查** - 清理未使用代码、处理TODO项
|
||||
4. **步骤4:架构分层检查** - Core层和Business层职责分离
|
||||
5. **步骤5:测试覆盖检查** - 一对一测试映射、测试分离
|
||||
6. **步骤6:功能文档生成** - README文档、API接口文档
|
||||
7. **步骤7:代码提交** - Git变更校验、规范化提交
|
||||
|
||||
## 🔄 执行原则
|
||||
|
||||
### ⚠️ 强制要求
|
||||
- **分步执行**:每次只执行一个步骤,严禁跳步骤或合并执行
|
||||
- **等待确认**:每步完成后必须等待确认才能进行下一步
|
||||
- **修改验证**:每次修改文件后必须重新检查该步骤并提供验证报告
|
||||
- **🔥 修改后必须重新执行当前步骤**:如果在当前步骤中发生了任何修改行为,必须立即重新执行该步骤的完整检查
|
||||
- **问题修复后重检**:如果当前步骤出现问题需要修改时,必须在解决问题后重新执行该步骤
|
||||
|
||||
## 📚 详细检查标准
|
||||
|
||||
### 步骤1:命名规范检查
|
||||
|
||||
#### 文件和文件夹命名
|
||||
- **规则**:snake_case(下划线分隔)
|
||||
- **示例**:
|
||||
```
|
||||
✅ 正确:user_controller.ts, admin_operation_log_service.ts
|
||||
❌ 错误:UserController.ts, user-service.ts
|
||||
```
|
||||
|
||||
#### 变量和函数命名
|
||||
- **规则**:camelCase(小驼峰命名)
|
||||
- **示例**:
|
||||
```typescript
|
||||
✅ 正确:const userName = 'test'; function getUserInfo() {}
|
||||
❌ 错误:const UserName = 'test'; function GetUserInfo() {}
|
||||
```
|
||||
|
||||
#### 类和接口命名
|
||||
- **规则**:PascalCase(大驼峰命名)
|
||||
- **示例**:
|
||||
```typescript
|
||||
✅ 正确:class UserService {} interface GameConfig {}
|
||||
❌ 错误:class userService {} interface gameConfig {}
|
||||
```
|
||||
|
||||
#### 常量命名
|
||||
- **规则**:SCREAMING_SNAKE_CASE(全大写+下划线)
|
||||
- **示例**:
|
||||
```typescript
|
||||
✅ 正确:const MAX_RETRY_COUNT = 3; const SALT_ROUNDS = 10;
|
||||
❌ 错误:const maxRetryCount = 3; const saltRounds = 10;
|
||||
```
|
||||
|
||||
#### 文件夹结构扁平化
|
||||
- **≤3个文件**:必须扁平化处理
|
||||
- **≥4个文件**:通常保持独立文件夹
|
||||
- **测试文件位置**:测试文件与源文件放在同一目录
|
||||
|
||||
#### Core层命名规则
|
||||
- **业务支撑模块**:使用_core后缀(如location_broadcast_core/)
|
||||
- **通用工具模块**:不使用后缀(如redis/、logger/)
|
||||
|
||||
### 步骤2:注释规范检查
|
||||
|
||||
#### 文件头注释(必须包含)
|
||||
```typescript
|
||||
/**
|
||||
* 文件功能描述
|
||||
*
|
||||
* 功能描述:
|
||||
* - 主要功能点1
|
||||
* - 主要功能点2
|
||||
*
|
||||
* 职责分离:
|
||||
* - 职责描述1
|
||||
* - 职责描述2
|
||||
*
|
||||
* 最近修改:
|
||||
* - [用户日期]: 修改类型 - 修改内容 (修改者: [用户名称])
|
||||
* - [历史日期]: 修改类型 - 修改内容 (修改者: 历史修改者)
|
||||
*
|
||||
* @author [处理后的作者名称]
|
||||
* @version x.x.x
|
||||
* @since [创建日期]
|
||||
* @lastModified [用户日期]
|
||||
*/
|
||||
```
|
||||
|
||||
#### @author字段处理规范
|
||||
- **保留人名**:如果@author是人名,必须保留不变
|
||||
- **替换AI标识**:只有AI标识(kiro、ChatGPT、Claude、AI等)才可替换为用户名称
|
||||
|
||||
#### 修改记录规范
|
||||
- **修改类型**:代码规范优化、功能新增、功能修改、Bug修复、性能优化、重构
|
||||
- **最多保留5条**:超出时自动删除最旧记录
|
||||
- **版本号递增**:
|
||||
- 修订版本+1:代码规范优化、Bug修复
|
||||
- 次版本+1:功能新增、功能修改
|
||||
- 主版本+1:重构、架构变更
|
||||
|
||||
### 步骤3:代码质量检查
|
||||
|
||||
#### 未使用代码清理
|
||||
- 清理未使用的导入
|
||||
- 清理未使用的变量和方法
|
||||
- 删除未调用的私有方法
|
||||
|
||||
#### 常量定义规范
|
||||
- 使用SCREAMING_SNAKE_CASE
|
||||
- 提取魔法数字为常量
|
||||
- 统一常量命名
|
||||
|
||||
#### TODO项处理(强制要求)
|
||||
- **最终文件不能包含TODO项**
|
||||
- 必须真正实现功能或删除未完成代码
|
||||
|
||||
#### 方法长度检查
|
||||
- **建议**:方法不超过50行
|
||||
- **原则**:一个方法只做一件事
|
||||
- **拆分**:复杂方法拆分为多个小方法
|
||||
|
||||
### 步骤4:架构分层检查
|
||||
|
||||
#### Core层规范
|
||||
- **职责**:专注技术实现,不包含业务逻辑
|
||||
- **命名**:业务支撑模块使用_core后缀,通用工具模块不使用后缀
|
||||
- **依赖**:只能导入其他Core层模块和第三方技术库
|
||||
|
||||
#### Business层规范
|
||||
- **职责**:专注业务逻辑实现,不关心底层技术细节
|
||||
- **依赖**:可以导入Core层模块和其他Business层模块
|
||||
- **禁止**:不能直接使用底层技术实现
|
||||
|
||||
### 步骤5:测试覆盖检查
|
||||
|
||||
#### 严格一对一测试映射
|
||||
- **强制要求**:每个测试文件必须严格对应一个源文件
|
||||
- **禁止多对一**:不允许一个测试文件测试多个源文件
|
||||
- **命名对应**:测试文件名必须与源文件名完全对应
|
||||
|
||||
#### 需要测试文件的类型
|
||||
```typescript
|
||||
✅ 必须有测试文件:
|
||||
- *.service.ts # Service类
|
||||
- *.controller.ts # Controller类
|
||||
- *.gateway.ts # Gateway类
|
||||
- *.guard.ts # Guard类
|
||||
- *.interceptor.ts # Interceptor类
|
||||
- *.middleware.ts # Middleware类
|
||||
|
||||
❌ 不需要测试文件:
|
||||
- *.dto.ts # DTO类
|
||||
- *.interface.ts # Interface文件
|
||||
- *.constants.ts # Constants文件
|
||||
```
|
||||
|
||||
#### 测试分离架构
|
||||
```
|
||||
test/
|
||||
├── integration/ # 集成测试
|
||||
├── e2e/ # 端到端测试
|
||||
├── performance/ # 性能测试
|
||||
├── property/ # 属性测试
|
||||
└── fixtures/ # 测试数据和工具
|
||||
```
|
||||
|
||||
### 步骤6:功能文档生成
|
||||
|
||||
#### README文档结构
|
||||
每个功能模块文件夹都必须有README.md文档,包含:
|
||||
- 模块功能描述
|
||||
- 对外提供的接口
|
||||
- 对外API接口(如适用)
|
||||
- WebSocket事件接口(如适用)
|
||||
- 使用的项目内部依赖
|
||||
- 核心特性
|
||||
- 潜在风险
|
||||
|
||||
#### 游戏服务器特殊要求
|
||||
- **WebSocket Gateway**:详细的事件接口文档
|
||||
- **双模式服务**:模式特点和切换指南
|
||||
- **属性测试**:测试策略说明
|
||||
|
||||
### 步骤7:代码提交
|
||||
|
||||
#### Git变更检查
|
||||
- 检查Git状态和变更内容
|
||||
- 校验文件修改记录与实际修改内容一致性
|
||||
- 确认修改记录、版本号、时间戳正确更新
|
||||
|
||||
#### 分支管理规范
|
||||
```bash
|
||||
# 代码规范优化分支
|
||||
feature/code-standard-optimization-[日期]
|
||||
|
||||
# Bug修复分支
|
||||
fix/[具体问题描述]
|
||||
|
||||
# 功能新增分支
|
||||
feature/[功能名称]
|
||||
|
||||
# 重构分支
|
||||
refactor/[模块名称]
|
||||
```
|
||||
|
||||
#### 提交信息规范
|
||||
```bash
|
||||
<类型>:<简短描述>
|
||||
|
||||
[可选的详细描述]
|
||||
```
|
||||
|
||||
提交类型:
|
||||
- `style`:代码规范优化
|
||||
- `refactor`:代码重构
|
||||
- `feat`:新功能
|
||||
- `fix`:Bug修复
|
||||
- `perf`:性能优化
|
||||
- `test`:测试相关
|
||||
- `docs`:文档更新
|
||||
|
||||
## 🎮 游戏服务器特殊要求
|
||||
|
||||
### WebSocket相关
|
||||
- **Gateway文件**:必须有完整的连接、消息处理测试
|
||||
- **实时通信**:心跳检测、重连机制、性能监控
|
||||
- **事件文档**:详细的输入输出格式说明
|
||||
|
||||
### 双模式架构
|
||||
- **内存服务和数据库服务**:都需要完整测试覆盖
|
||||
- **行为一致性**:确保两种模式行为完全一致
|
||||
- **切换机制**:提供模式切换指南和数据迁移工具
|
||||
|
||||
### 属性测试
|
||||
- **管理员模块**:使用fast-check进行属性测试
|
||||
- **随机化测试**:验证边界条件和异常处理
|
||||
- **测试策略**:详细的属性测试实现说明
|
||||
|
||||
## 📋 统一报告模板
|
||||
|
||||
每步完成后使用此模板报告:
|
||||
|
||||
```
|
||||
## 步骤X:[步骤名称]检查报告
|
||||
|
||||
### 🔍 检查结果
|
||||
[发现的问题列表]
|
||||
|
||||
### 🛠️ 修正方案
|
||||
[具体修正建议]
|
||||
|
||||
### ✅ 完成状态
|
||||
- 检查项1 ✓/✗
|
||||
- 检查项2 ✓/✗
|
||||
|
||||
**请确认修正方案,确认后进行下一步骤**
|
||||
```
|
||||
|
||||
## 🚨 全局约束
|
||||
|
||||
### 文件修改记录规范
|
||||
每次执行完修改后,文件顶部都需要更新:
|
||||
- 添加修改记录(最多保留5条)
|
||||
- 更新版本号(按规则递增)
|
||||
- 更新@lastModified字段
|
||||
- 正确处理@author字段
|
||||
|
||||
### 时间更新规则
|
||||
- **仅检查不修改**:不更新@lastModified字段
|
||||
- **实际修改才更新**:只有真正修改了文件内容时才更新
|
||||
- **Git变更检测**:通过git检查文件是否有实际变更
|
||||
|
||||
### 修改验证流程
|
||||
任何步骤中发生修改行为后,必须立即重新执行该步骤:
|
||||
```
|
||||
步骤执行中 → 发现问题 → 执行修改 → 🔥 立即重新执行该步骤 → 验证无遗漏 → 用户确认 → 下一步骤
|
||||
```
|
||||
|
||||
## 🔧 AI-Reading使用指南
|
||||
|
||||
### 什么是AI-Reading
|
||||
|
||||
AI-Reading是一套系统化的代码检查执行指南,专门为Whale Town游戏服务器项目设计。它提供了完整的7步代码检查流程,确保代码质量和项目规范的一致性。
|
||||
|
||||
### 使用场景
|
||||
|
||||
#### 适用情况
|
||||
- **新功能开发完成后**:确保新代码符合项目规范
|
||||
- **Bug修复后**:验证修复代码的质量和规范性
|
||||
- **代码重构时**:保证重构后代码的一致性和质量
|
||||
- **代码审查前**:提前发现和解决规范问题
|
||||
- **项目维护期**:定期检查和优化代码质量
|
||||
|
||||
#### 不适用情况
|
||||
- **紧急热修复**:紧急生产问题修复时可简化流程
|
||||
- **实验性代码**:概念验证或原型开发阶段
|
||||
- **第三方代码集成**:外部库或组件的集成
|
||||
|
||||
### 使用方法
|
||||
|
||||
#### 1. 准备阶段
|
||||
在开始检查前,必须收集以下信息:
|
||||
- **用户当前日期**:用于修改记录和时间戳更新
|
||||
- **用户名称**:用于@author字段处理和修改记录
|
||||
|
||||
#### 2. 执行流程
|
||||
```
|
||||
用户请求代码检查
|
||||
↓
|
||||
收集用户信息(日期、名称)
|
||||
↓
|
||||
识别项目特性(NestJS游戏服务器)
|
||||
↓
|
||||
按顺序执行7个步骤
|
||||
↓
|
||||
每步完成后等待用户确认
|
||||
↓
|
||||
如有修改立即重新执行当前步骤
|
||||
```
|
||||
|
||||
#### 3. 使用AI-Reading的具体步骤
|
||||
|
||||
**第一步:启动检查**
|
||||
```
|
||||
请使用ai-reading对[模块名称]进行代码检查
|
||||
当前日期:[YYYY-MM-DD]
|
||||
用户名称:[您的名称]
|
||||
```
|
||||
|
||||
**第二步:逐步执行**
|
||||
AI会按照以下顺序执行:
|
||||
1. 读取对应步骤的详细指导文档
|
||||
2. 执行该步骤的所有检查项
|
||||
3. 提供详细的检查报告
|
||||
4. 等待用户确认后进行下一步
|
||||
|
||||
**第三步:处理修改**
|
||||
如果某步骤需要修改代码:
|
||||
1. AI会执行必要的修改操作
|
||||
2. 更新文件的修改记录和版本信息
|
||||
3. 立即重新执行该步骤进行验证
|
||||
4. 提供验证报告确认无遗漏问题
|
||||
|
||||
**第四步:完成检查**
|
||||
所有7个步骤完成后:
|
||||
1. 提供完整的检查总结报告
|
||||
2. 确认所有问题已解决
|
||||
3. 代码已准备好进行提交或部署
|
||||
|
||||
### 使用技巧
|
||||
|
||||
#### 高效使用
|
||||
- **批量检查**:可以一次性检查整个模块或功能
|
||||
- **增量检查**:只检查修改的文件和相关依赖
|
||||
- **定期检查**:建议每周对核心模块进行一次完整检查
|
||||
|
||||
#### 注意事项
|
||||
- **不要跳步骤**:必须按顺序完成所有步骤
|
||||
- **确认每一步**:每步完成后仔细检查报告再确认
|
||||
- **保存检查记录**:保留检查报告用于后续参考
|
||||
- **及时处理问题**:发现问题立即修复,不要积累
|
||||
|
||||
#### 常见问题处理
|
||||
- **检查时间过长**:可以分模块进行,不必一次性检查整个项目
|
||||
- **修改冲突**:如果与其他开发者的修改冲突,先解决冲突再继续检查
|
||||
- **测试失败**:如果测试不通过,必须先修复测试再继续后续步骤
|
||||
|
||||
### 最佳实践
|
||||
|
||||
#### 团队协作
|
||||
- **统一标准**:团队成员都使用相同的AI-Reading流程
|
||||
- **代码审查**:在代码审查前先完成AI-Reading检查
|
||||
- **知识分享**:定期分享AI-Reading发现的问题和解决方案
|
||||
|
||||
#### 质量保证
|
||||
- **持续改进**:根据检查结果不断优化代码规范
|
||||
- **文档同步**:确保文档与代码实现保持一致
|
||||
- **测试覆盖**:通过AI-Reading确保测试覆盖率达标
|
||||
|
||||
#### 效率提升
|
||||
- **自动化集成**:考虑将AI-Reading集成到CI/CD流程
|
||||
- **模板使用**:使用标准模板减少重复工作
|
||||
- **工具辅助**:结合IDE插件和代码格式化工具
|
||||
|
||||
通过正确使用AI-Reading,可以显著提升代码质量,减少bug数量,提高开发效率,确保项目的长期可维护性。
|
||||
|
||||
---
|
||||
|
||||
**重要提醒**:使用AI-Reading时,请严格按照7步流程执行,不要跳过任何步骤,确保每一步都得到充分验证后再进行下一步。
|
||||
@@ -1,7 +1,8 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
moduleFileExtensions: ['js', 'json', 'ts'],
|
||||
rootDir: 'src',
|
||||
testRegex: '.*\\.spec\\.ts$',
|
||||
roots: ['<rootDir>/src', '<rootDir>/test'],
|
||||
testRegex: '.*\\.(spec|e2e-spec|integration-spec|perf-spec)\\.ts$',
|
||||
transform: {
|
||||
'^.+\\.(t|j)s$': 'ts-jest',
|
||||
},
|
||||
@@ -11,6 +12,18 @@ module.exports = {
|
||||
coverageDirectory: '../coverage',
|
||||
testEnvironment: 'node',
|
||||
moduleNameMapper: {
|
||||
'^src/(.*)$': '<rootDir>/$1',
|
||||
'^src/(.*)$': '<rootDir>/src/$1',
|
||||
},
|
||||
// 添加异步处理配置
|
||||
testTimeout: 10000,
|
||||
// 强制退出以避免挂起
|
||||
forceExit: true,
|
||||
// 检测打开的句柄
|
||||
detectOpenHandles: true,
|
||||
// 处理 ES 模块
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!(@faker-js/faker)/)',
|
||||
],
|
||||
// 设置测试环境变量
|
||||
setupFilesAfterEnv: ['<rootDir>/test-setup.js'],
|
||||
};
|
||||
@@ -3,6 +3,12 @@
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
"deleteOutDir": true,
|
||||
"assets": [
|
||||
{
|
||||
"include": "../config/**/*",
|
||||
"outDir": "./dist"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
33
package.json
33
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "pixel-game-server",
|
||||
"version": "1.0.0",
|
||||
"description": "A 2D pixel art game server built with NestJS",
|
||||
"version": "1.2.0",
|
||||
"description": "A 2D pixel art game server built with NestJS - 完整的游戏服务器,包含用户认证、位置广播、聊天系统、管理员后台等功能模块",
|
||||
"main": "dist/main.js",
|
||||
"scripts": {
|
||||
"dev": "nest start --watch",
|
||||
@@ -10,7 +10,12 @@
|
||||
"start:prod": "node dist/main.js",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage"
|
||||
"test:cov": "jest --coverage",
|
||||
"test:e2e": "cross-env RUN_E2E_TESTS=true jest --testPathPattern=e2e.spec.ts --runInBand",
|
||||
"test:unit": "jest --testPathPattern=spec.ts --testPathIgnorePatterns=e2e.spec.ts",
|
||||
"test:integration": "jest --testPathPattern=integration.spec.ts --runInBand",
|
||||
"test:property": "jest --testPathPattern=property.spec.ts",
|
||||
"test:all": "cross-env RUN_E2E_TESTS=true jest --runInBand"
|
||||
},
|
||||
"keywords": [
|
||||
"game",
|
||||
@@ -22,28 +27,36 @@
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nestjs/cache-manager": "^3.1.0",
|
||||
"@nestjs/common": "^11.1.9",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.1.9",
|
||||
"@nestjs/platform-express": "^10.4.20",
|
||||
"@nestjs/platform-socket.io": "^10.4.20",
|
||||
"@nestjs/jwt": "^11.0.2",
|
||||
"@nestjs/platform-express": "^11.1.11",
|
||||
"@nestjs/platform-ws": "^11.1.11",
|
||||
"@nestjs/schedule": "^4.1.2",
|
||||
"@nestjs/swagger": "^11.2.3",
|
||||
"@nestjs/throttler": "^6.5.0",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"@nestjs/websockets": "^10.4.20",
|
||||
"@nestjs/websockets": "^11.1.11",
|
||||
"@types/archiver": "^7.0.0",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"archiver": "^7.0.1",
|
||||
"axios": "^1.13.2",
|
||||
"bcrypt": "^6.0.0",
|
||||
"cache-manager": "^7.2.8",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.3",
|
||||
"ioredis": "^5.8.2",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"mysql2": "^3.16.0",
|
||||
"nestjs-pino": "^4.5.0",
|
||||
"nock": "^14.0.10",
|
||||
"node-fetch": "^3.3.2",
|
||||
"nodemailer": "^6.10.1",
|
||||
"pino": "^10.1.0",
|
||||
"reflect-metadata": "^0.1.14",
|
||||
"rxjs": "^7.8.2",
|
||||
"socket.io": "^4.8.3",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"typeorm": "^0.3.28",
|
||||
"uuid": "^13.0.0",
|
||||
@@ -51,18 +64,22 @@
|
||||
"zulip-js": "^2.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^10.2.0",
|
||||
"@nestjs/cli": "^10.4.9",
|
||||
"@nestjs/schematics": "^10.2.3",
|
||||
"@nestjs/testing": "^10.4.20",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^20.19.27",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/supertest": "^6.0.3",
|
||||
"@types/ws": "^8.18.1",
|
||||
"cross-env": "^10.1.0",
|
||||
"fast-check": "^4.5.2",
|
||||
"jest": "^29.7.0",
|
||||
"pino-pretty": "^13.1.3",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"sqlite3": "^5.1.7",
|
||||
"supertest": "^7.1.4",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-node": "^10.9.2",
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
packages:
|
||||
- 'client'
|
||||
|
||||
ignoredBuiltDependencies:
|
||||
- '@nestjs/core'
|
||||
- '@scarf/scarf'
|
||||
|
||||
206
scripts/test-zulip-integration.js
Normal file
206
scripts/test-zulip-integration.js
Normal file
@@ -0,0 +1,206 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Zulip集成测试运行脚本
|
||||
*
|
||||
* 功能描述:
|
||||
* - 运行Zulip消息发送的各种测试
|
||||
* - 检查环境配置
|
||||
* - 提供测试结果报告
|
||||
*
|
||||
* 使用方法:
|
||||
* npm run test:zulip-integration
|
||||
* 或
|
||||
* node scripts/test-zulip-integration.js
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-10
|
||||
*/
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// 颜色输出
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
bright: '\x1b[1m',
|
||||
red: '\x1b[31m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
blue: '\x1b[34m',
|
||||
magenta: '\x1b[35m',
|
||||
cyan: '\x1b[36m',
|
||||
};
|
||||
|
||||
function colorLog(color, message) {
|
||||
console.log(`${colors[color]}${message}${colors.reset}`);
|
||||
}
|
||||
|
||||
function checkEnvironment() {
|
||||
colorLog('cyan', '\n🔍 检查环境配置...\n');
|
||||
|
||||
const requiredEnvVars = [
|
||||
'ZULIP_SERVER_URL',
|
||||
'ZULIP_BOT_EMAIL',
|
||||
'ZULIP_BOT_API_KEY'
|
||||
];
|
||||
|
||||
const optionalEnvVars = [
|
||||
'ZULIP_TEST_STREAM',
|
||||
'ZULIP_TEST_TOPIC'
|
||||
];
|
||||
|
||||
let hasRequired = true;
|
||||
|
||||
// 检查必需的环境变量
|
||||
requiredEnvVars.forEach(varName => {
|
||||
if (process.env[varName]) {
|
||||
colorLog('green', `✅ ${varName}: ${process.env[varName].substring(0, 20)}...`);
|
||||
} else {
|
||||
colorLog('red', `❌ ${varName}: 未设置`);
|
||||
hasRequired = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 检查可选的环境变量
|
||||
optionalEnvVars.forEach(varName => {
|
||||
if (process.env[varName]) {
|
||||
colorLog('yellow', `🔧 ${varName}: ${process.env[varName]}`);
|
||||
} else {
|
||||
colorLog('yellow', `🔧 ${varName}: 使用默认值`);
|
||||
}
|
||||
});
|
||||
|
||||
if (!hasRequired) {
|
||||
colorLog('red', '\n❌ 缺少必需的环境变量!');
|
||||
colorLog('yellow', '\n请设置以下环境变量:');
|
||||
colorLog('yellow', 'export ZULIP_SERVER_URL="https://your-zulip-server.com"');
|
||||
colorLog('yellow', 'export ZULIP_BOT_EMAIL="your-bot@example.com"');
|
||||
colorLog('yellow', 'export ZULIP_BOT_API_KEY="your-api-key"');
|
||||
colorLog('yellow', '\n可选配置:');
|
||||
colorLog('yellow', 'export ZULIP_TEST_STREAM="test-stream"');
|
||||
colorLog('yellow', 'export ZULIP_TEST_TOPIC="API Test"');
|
||||
return false;
|
||||
}
|
||||
|
||||
colorLog('green', '\n✅ 环境配置检查通过!\n');
|
||||
return true;
|
||||
}
|
||||
|
||||
function runTest(testFile, description) {
|
||||
colorLog('blue', `\n🧪 运行测试: ${description}`);
|
||||
colorLog('blue', `📁 文件: ${testFile}\n`);
|
||||
|
||||
try {
|
||||
const command = `npm test -- ${testFile} --verbose`;
|
||||
execSync(command, {
|
||||
stdio: 'inherit',
|
||||
cwd: process.cwd()
|
||||
});
|
||||
colorLog('green', `✅ ${description} - 测试通过\n`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
colorLog('red', `❌ ${description} - 测试失败\n`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
colorLog('bright', '🚀 Zulip集成测试运行器\n');
|
||||
colorLog('bright', '=' .repeat(50));
|
||||
|
||||
// 检查环境配置
|
||||
if (!checkEnvironment()) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const tests = [
|
||||
{
|
||||
file: 'src/core/zulip_core/services/zulip_message_integration.spec.ts',
|
||||
description: 'Zulip消息发送集成测试'
|
||||
},
|
||||
{
|
||||
file: 'test/zulip_integration/chat_message_e2e.spec.ts',
|
||||
description: '聊天消息端到端测试'
|
||||
},
|
||||
{
|
||||
file: 'test/zulip_integration/real_zulip_api.spec.ts',
|
||||
description: '真实Zulip API测试'
|
||||
}
|
||||
];
|
||||
|
||||
let passedTests = 0;
|
||||
let totalTests = tests.length;
|
||||
|
||||
// 运行所有测试
|
||||
tests.forEach(test => {
|
||||
if (fs.existsSync(test.file)) {
|
||||
if (runTest(test.file, test.description)) {
|
||||
passedTests++;
|
||||
}
|
||||
} else {
|
||||
colorLog('yellow', `⚠️ 测试文件不存在: ${test.file}`);
|
||||
totalTests--;
|
||||
}
|
||||
});
|
||||
|
||||
// 输出测试结果
|
||||
colorLog('bright', '\n' + '=' .repeat(50));
|
||||
colorLog('bright', '📊 测试结果汇总');
|
||||
colorLog('bright', '=' .repeat(50));
|
||||
|
||||
if (passedTests === totalTests) {
|
||||
colorLog('green', `🎉 所有测试通过!(${passedTests}/${totalTests})`);
|
||||
colorLog('green', '\n✨ Zulip集成功能正常工作!');
|
||||
} else {
|
||||
colorLog('red', `❌ 部分测试失败 (${passedTests}/${totalTests})`);
|
||||
colorLog('yellow', '\n请检查失败的测试并修复问题。');
|
||||
}
|
||||
|
||||
// 提供有用的信息
|
||||
colorLog('cyan', '\n💡 提示:');
|
||||
colorLog('cyan', '- 确保Zulip服务器可访问');
|
||||
colorLog('cyan', '- 检查API Key权限');
|
||||
colorLog('cyan', '- 确认测试Stream存在');
|
||||
colorLog('cyan', '- 查看详细日志了解错误原因');
|
||||
|
||||
process.exit(passedTests === totalTests ? 0 : 1);
|
||||
}
|
||||
|
||||
// 处理命令行参数
|
||||
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
||||
console.log(`
|
||||
Zulip集成测试运行器
|
||||
|
||||
用法:
|
||||
node scripts/test-zulip-integration.js [选项]
|
||||
|
||||
选项:
|
||||
--help, -h 显示帮助信息
|
||||
--check-env 仅检查环境配置
|
||||
|
||||
环境变量:
|
||||
ZULIP_SERVER_URL Zulip服务器地址 (必需)
|
||||
ZULIP_BOT_EMAIL 机器人邮箱 (必需)
|
||||
ZULIP_BOT_API_KEY API密钥 (必需)
|
||||
ZULIP_TEST_STREAM 测试Stream名称 (可选)
|
||||
ZULIP_TEST_TOPIC 测试Topic名称 (可选)
|
||||
|
||||
示例:
|
||||
export ZULIP_SERVER_URL="https://your-zulip.com"
|
||||
export ZULIP_BOT_EMAIL="bot@example.com"
|
||||
export ZULIP_BOT_API_KEY="your-api-key"
|
||||
node scripts/test-zulip-integration.js
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (process.argv.includes('--check-env')) {
|
||||
checkEnvironment();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// 运行主程序
|
||||
main();
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import { AppService } from './app.service';
|
||||
import { AppStatusResponseDto } from './dto/app.dto';
|
||||
import { ErrorResponseDto } from './dto/error_response.dto';
|
||||
import { AppStatusResponseDto, ErrorResponseDto } from './business/shared';
|
||||
|
||||
/**
|
||||
* 应用根控制器
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { LoggerModule } from './core/utils/logger/logger.module';
|
||||
import { UsersModule } from './core/db/users/users.module';
|
||||
import { ZulipAccountsModule } from './core/db/zulip_accounts/zulip_accounts.module';
|
||||
import { LoginCoreModule } from './core/login_core/login_core.module';
|
||||
import { LoginModule } from './business/login/login.module';
|
||||
import { AuthGatewayModule } from './gateway/auth/auth.gateway.module';
|
||||
import { ChatGatewayModule } from './gateway/chat/chat.gateway.module';
|
||||
import { ZulipGatewayModule } from './gateway/zulip/zulip.gateway.module';
|
||||
import { ZulipModule } from './business/zulip/zulip.module';
|
||||
import { RedisModule } from './core/redis/redis.module';
|
||||
import { AdminModule } from './business/admin/admin.module';
|
||||
import { UserMgmtModule } from './business/user_mgmt/user_mgmt.module';
|
||||
import { SecurityCoreModule } from './core/security_core/security_core.module';
|
||||
import { LocationBroadcastModule } from './business/location_broadcast/location_broadcast.module';
|
||||
import { NoticeModule } from './business/notice/notice.module';
|
||||
import { MaintenanceMiddleware } from './core/security_core/maintenance.middleware';
|
||||
import { ContentTypeMiddleware } from './core/security_core/content_type.middleware';
|
||||
|
||||
/**
|
||||
* 检查数据库配置是否完整 by angjustinl 2025-12-17
|
||||
@@ -52,6 +63,8 @@ function isDatabaseConfigured(): boolean {
|
||||
database: process.env.DB_NAME,
|
||||
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
||||
synchronize: false,
|
||||
// 字符集配置 - 支持中文和emoji
|
||||
charset: 'utf8mb4',
|
||||
// 添加连接超时和重试配置
|
||||
connectTimeout: 10000,
|
||||
retryAttempts: 3,
|
||||
@@ -60,11 +73,40 @@ function isDatabaseConfigured(): boolean {
|
||||
] : []),
|
||||
// 根据数据库配置选择用户模块模式
|
||||
isDatabaseConfigured() ? UsersModule.forDatabase() : UsersModule.forMemory(),
|
||||
// Zulip账号关联模块 - 全局单例,其他模块无需重复导入
|
||||
ZulipAccountsModule.forRoot(),
|
||||
LoginCoreModule,
|
||||
LoginModule,
|
||||
ZulipModule,
|
||||
AuthGatewayModule, // 认证网关模块
|
||||
ChatGatewayModule, // 聊天网关模块
|
||||
ZulipGatewayModule, // Zulip网关模块(HTTP API接口)
|
||||
ZulipModule, // Zulip业务模块(业务逻辑)
|
||||
UserMgmtModule,
|
||||
AdminModule,
|
||||
SecurityCoreModule,
|
||||
LocationBroadcastModule,
|
||||
NoticeModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
providers: [
|
||||
AppService,
|
||||
// 注意:全局拦截器现在由SecurityModule提供
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
export class AppModule implements NestModule {
|
||||
/**
|
||||
* 配置中间件
|
||||
*
|
||||
* @param consumer 中间件消费者
|
||||
*/
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
// 1. 维护模式中间件 - 最高优先级
|
||||
consumer
|
||||
.apply(MaintenanceMiddleware)
|
||||
.forRoutes('*');
|
||||
|
||||
// 2. 内容类型检查中间件
|
||||
consumer
|
||||
.apply(ContentTypeMiddleware)
|
||||
.forRoutes('*');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { AppStatusResponseDto } from './dto/app.dto';
|
||||
import { AppStatusResponseDto } from './business/shared';
|
||||
|
||||
/**
|
||||
* 应用服务类
|
||||
@@ -31,12 +31,12 @@ export class AppService {
|
||||
|
||||
return {
|
||||
service: 'Pixel Game Server',
|
||||
version: '1.0.0',
|
||||
version: '1.1.1',
|
||||
status: 'running',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: Math.floor((Date.now() - this.startTime) / 1000),
|
||||
environment: this.configService.get<string>('NODE_ENV', 'development'),
|
||||
storage_mode: isDatabaseConfigured ? 'database' : 'memory'
|
||||
storageMode: isDatabaseConfigured ? 'database' : 'memory'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
237
src/business/admin/admin.controller.spec.ts
Normal file
237
src/business/admin/admin.controller.spec.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* AdminController 单元测试
|
||||
*
|
||||
* 功能描述:
|
||||
* - 测试管理员控制器的所有HTTP端点
|
||||
* - 验证请求参数处理和响应格式
|
||||
* - 测试权限验证和异常处理
|
||||
*
|
||||
* 职责分离:
|
||||
* - HTTP层测试,不涉及业务逻辑实现
|
||||
* - Mock业务服务,专注控制器逻辑
|
||||
* - 验证请求响应的正确性
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 创建AdminController测试文件,补充测试覆盖
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @since 2026-01-07
|
||||
* @lastModified 2026-01-07
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Response } from 'express';
|
||||
import { AdminController } from './admin.controller';
|
||||
import { AdminService } from './admin.service';
|
||||
import { AdminGuard } from './admin.guard';
|
||||
|
||||
describe('AdminController', () => {
|
||||
let controller: AdminController;
|
||||
let adminService: jest.Mocked<AdminService>;
|
||||
|
||||
const mockAdminService = {
|
||||
login: jest.fn(),
|
||||
listUsers: jest.fn(),
|
||||
getUser: jest.fn(),
|
||||
resetPassword: jest.fn(),
|
||||
getRuntimeLogs: jest.fn(),
|
||||
getLogDirAbsolutePath: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AdminController],
|
||||
providers: [
|
||||
{
|
||||
provide: AdminService,
|
||||
useValue: mockAdminService,
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideGuard(AdminGuard)
|
||||
.useValue({ canActivate: () => true })
|
||||
.compile();
|
||||
|
||||
controller = module.get<AdminController>(AdminController);
|
||||
adminService = module.get(AdminService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('login', () => {
|
||||
it('should login admin successfully', async () => {
|
||||
const loginDto = { identifier: 'admin', password: 'Admin123456' };
|
||||
const expectedResult = {
|
||||
success: true,
|
||||
data: { admin: { id: '1', username: 'admin', role: 9 }, access_token: 'token' },
|
||||
message: '管理员登录成功'
|
||||
};
|
||||
|
||||
adminService.login.mockResolvedValue(expectedResult);
|
||||
|
||||
const result = await controller.login(loginDto);
|
||||
|
||||
expect(adminService.login).toHaveBeenCalledWith('admin', 'Admin123456');
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should handle login failure', async () => {
|
||||
const loginDto = { identifier: 'admin', password: 'wrong' };
|
||||
const expectedResult = {
|
||||
success: false,
|
||||
message: '密码错误',
|
||||
error_code: 'ADMIN_LOGIN_FAILED'
|
||||
};
|
||||
|
||||
adminService.login.mockResolvedValue(expectedResult);
|
||||
|
||||
const result = await controller.login(loginDto);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error_code).toBe('ADMIN_LOGIN_FAILED');
|
||||
});
|
||||
});
|
||||
|
||||
describe('listUsers', () => {
|
||||
it('should list users with default pagination', async () => {
|
||||
const expectedResult = {
|
||||
success: true,
|
||||
data: {
|
||||
users: [{ id: '1', username: 'user1' }],
|
||||
limit: 100,
|
||||
offset: 0
|
||||
},
|
||||
message: '用户列表获取成功'
|
||||
};
|
||||
|
||||
adminService.listUsers.mockResolvedValue(expectedResult);
|
||||
|
||||
const result = await controller.listUsers();
|
||||
|
||||
expect(adminService.listUsers).toHaveBeenCalledWith(100, 0);
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should list users with custom pagination', async () => {
|
||||
const expectedResult = {
|
||||
success: true,
|
||||
data: {
|
||||
users: [],
|
||||
limit: 50,
|
||||
offset: 10
|
||||
},
|
||||
message: '用户列表获取成功'
|
||||
};
|
||||
|
||||
adminService.listUsers.mockResolvedValue(expectedResult);
|
||||
|
||||
const result = await controller.listUsers('50', '10');
|
||||
|
||||
expect(adminService.listUsers).toHaveBeenCalledWith(50, 10);
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUser', () => {
|
||||
it('should get user by id', async () => {
|
||||
const expectedResult = {
|
||||
success: true,
|
||||
data: { user: { id: '123', username: 'testuser' } },
|
||||
message: '用户信息获取成功'
|
||||
};
|
||||
|
||||
adminService.getUser.mockResolvedValue(expectedResult);
|
||||
|
||||
const result = await controller.getUser('123');
|
||||
|
||||
expect(adminService.getUser).toHaveBeenCalledWith(BigInt(123));
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetPassword', () => {
|
||||
it('should reset user password', async () => {
|
||||
const resetDto = { newPassword: 'NewPass1234' };
|
||||
const expectedResult = {
|
||||
success: true,
|
||||
message: '密码重置成功'
|
||||
};
|
||||
|
||||
adminService.resetPassword.mockResolvedValue(expectedResult);
|
||||
|
||||
const result = await controller.resetPassword('123', resetDto);
|
||||
|
||||
expect(adminService.resetPassword).toHaveBeenCalledWith(BigInt(123), 'NewPass1234');
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRuntimeLogs', () => {
|
||||
it('should get runtime logs with default lines', async () => {
|
||||
const expectedResult = {
|
||||
success: true,
|
||||
data: {
|
||||
file: 'app.log',
|
||||
updated_at: '2026-01-07T00:00:00.000Z',
|
||||
lines: ['log line 1', 'log line 2']
|
||||
},
|
||||
message: '运行日志获取成功'
|
||||
};
|
||||
|
||||
adminService.getRuntimeLogs.mockResolvedValue(expectedResult);
|
||||
|
||||
const result = await controller.getRuntimeLogs();
|
||||
|
||||
expect(adminService.getRuntimeLogs).toHaveBeenCalledWith(undefined);
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should get runtime logs with custom lines', async () => {
|
||||
const expectedResult = {
|
||||
success: true,
|
||||
data: {
|
||||
file: 'app.log',
|
||||
updated_at: '2026-01-07T00:00:00.000Z',
|
||||
lines: ['log line 1']
|
||||
},
|
||||
message: '运行日志获取成功'
|
||||
};
|
||||
|
||||
adminService.getRuntimeLogs.mockResolvedValue(expectedResult);
|
||||
|
||||
const result = await controller.getRuntimeLogs('100');
|
||||
|
||||
expect(adminService.getRuntimeLogs).toHaveBeenCalledWith(100);
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('downloadLogsArchive', () => {
|
||||
let mockResponse: Partial<Response>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockResponse = {
|
||||
setHeader: jest.fn(),
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn(),
|
||||
end: jest.fn(),
|
||||
headersSent: false,
|
||||
};
|
||||
});
|
||||
|
||||
it('should handle missing log directory', async () => {
|
||||
adminService.getLogDirAbsolutePath.mockReturnValue('/nonexistent/logs');
|
||||
|
||||
await controller.downloadLogsArchive(mockResponse as Response);
|
||||
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(404);
|
||||
expect(mockResponse.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
message: '日志目录不存在'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
361
src/business/admin/admin.controller.ts
Normal file
361
src/business/admin/admin.controller.ts
Normal file
@@ -0,0 +1,361 @@
|
||||
/**
|
||||
* 管理员控制器
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供管理员登录认证接口
|
||||
* - 提供用户管理相关接口(查询、重置密码)
|
||||
* - 提供系统日志查询和下载功能
|
||||
*
|
||||
* 职责分离:
|
||||
* - HTTP请求处理和参数验证
|
||||
* - 业务逻辑委托给AdminService处理
|
||||
* - 权限控制通过AdminGuard实现
|
||||
*
|
||||
* API端点:
|
||||
* - POST /admin/auth/login 管理员登录
|
||||
* - GET /admin/users 用户列表(需要管理员Token)
|
||||
* - GET /admin/users/:id 用户详情(需要管理员Token)
|
||||
* - POST /admin/users/:id/reset-password 重置指定用户密码(需要管理员Token)
|
||||
* - GET /admin/logs/runtime 获取运行日志尾部(需要管理员Token)
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-09: 代码质量优化 - 将同步文件系统操作改为异步操作,避免阻塞事件循环 (修改者: moyin)
|
||||
* - 2026-01-07: 代码规范优化 - 修正文件命名规范,更新作者信息和修改记录
|
||||
* - 2026-01-08: 注释规范优化 - 补充方法注释,添加@param、@returns、@throws和@example (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.4
|
||||
* @since 2025-12-19
|
||||
* @lastModified 2026-01-09
|
||||
*/
|
||||
|
||||
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Query, UseGuards, ValidationPipe, UsePipes, Res, Logger } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiProduces, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { AdminGuard } from './admin.guard';
|
||||
import { AdminService } from './admin.service';
|
||||
import { AdminLoginDto, AdminResetPasswordDto } from './admin_login.dto';
|
||||
import {
|
||||
AdminLoginResponseDto,
|
||||
AdminUsersResponseDto,
|
||||
AdminCommonResponseDto,
|
||||
AdminUserResponseDto,
|
||||
AdminRuntimeLogsResponseDto
|
||||
} from './admin_response.dto';
|
||||
import { Throttle, ThrottlePresets } from '../../core/security_core/throttle.decorator';
|
||||
import { getCurrentTimestamp } from './admin_utils';
|
||||
import type { Response } from 'express';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { spawn } from 'child_process';
|
||||
import { pipeline } from 'stream';
|
||||
|
||||
@ApiTags('admin')
|
||||
@Controller('admin')
|
||||
export class AdminController {
|
||||
private readonly logger = new Logger(AdminController.name);
|
||||
|
||||
constructor(private readonly adminService: AdminService) {}
|
||||
|
||||
/**
|
||||
* 管理员登录
|
||||
*
|
||||
* 功能描述:
|
||||
* 验证管理员身份并生成JWT Token,仅允许role=9的账户登录后台
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证登录标识符和密码
|
||||
* 2. 检查用户角色是否为管理员(role=9)
|
||||
* 3. 生成JWT Token
|
||||
* 4. 返回登录结果和Token
|
||||
*
|
||||
* @param dto 登录请求数据
|
||||
* @returns 登录结果,包含Token和管理员信息
|
||||
*
|
||||
* @throws UnauthorizedException 当登录失败时
|
||||
* @throws ForbiddenException 当权限不足或账户被禁用时
|
||||
* @throws TooManyRequestsException 当登录尝试过于频繁时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const result = await adminController.login({
|
||||
* identifier: 'admin',
|
||||
* password: 'Admin123456'
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
@ApiOperation({ summary: '管理员登录', description: '仅允许 role=9 的账户登录后台' })
|
||||
@ApiBody({ type: AdminLoginDto })
|
||||
@ApiResponse({ status: 200, description: '登录成功', type: AdminLoginResponseDto })
|
||||
@ApiResponse({ status: 401, description: '登录失败' })
|
||||
@ApiResponse({ status: 403, description: '权限不足或账户被禁用' })
|
||||
@ApiResponse({ status: 429, description: '登录尝试过于频繁' })
|
||||
@Throttle(ThrottlePresets.LOGIN)
|
||||
@Post('auth/login')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
async login(@Body() dto: AdminLoginDto) {
|
||||
return await this.adminService.login(dto.identifier, dto.password);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户列表
|
||||
*
|
||||
* 功能描述:
|
||||
* 分页获取系统中的用户列表,支持限制数量和偏移量参数
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 解析查询参数(limit和offset)
|
||||
* 2. 调用用户服务获取用户列表
|
||||
* 3. 格式化用户数据
|
||||
* 4. 返回分页结果
|
||||
*
|
||||
* @param limit 返回数量,默认100,可选参数
|
||||
* @param offset 偏移量,默认0,可选参数
|
||||
* @returns 用户列表和分页信息
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 获取前20个用户
|
||||
* const result = await adminController.listUsers('20', '0');
|
||||
* ```
|
||||
*/
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({ summary: '获取用户列表', description: '后台用户管理:分页获取用户列表' })
|
||||
@ApiQuery({ name: 'limit', required: false, description: '返回数量(默认100)' })
|
||||
@ApiQuery({ name: 'offset', required: false, description: '偏移量(默认0)' })
|
||||
@ApiResponse({ status: 200, description: '获取成功', type: AdminUsersResponseDto })
|
||||
@UseGuards(AdminGuard)
|
||||
@Get('users')
|
||||
async listUsers(
|
||||
@Query('limit') limit?: string,
|
||||
@Query('offset') offset?: string,
|
||||
) {
|
||||
const parsedLimit = limit ? Number(limit) : 100;
|
||||
const parsedOffset = offset ? Number(offset) : 0;
|
||||
return await this.adminService.listUsers(parsedLimit, parsedOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户详情
|
||||
*
|
||||
* 功能描述:
|
||||
* 根据用户ID获取指定用户的详细信息
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证用户ID格式
|
||||
* 2. 查询用户详细信息
|
||||
* 3. 格式化用户数据
|
||||
* 4. 返回用户详情
|
||||
*
|
||||
* @param id 用户ID字符串
|
||||
* @returns 用户详细信息
|
||||
*
|
||||
* @throws NotFoundException 当用户不存在时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const result = await adminController.getUser('123');
|
||||
* ```
|
||||
*/
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({ summary: '获取用户详情' })
|
||||
@ApiParam({ name: 'id', description: '用户ID' })
|
||||
@ApiResponse({ status: 200, description: '获取成功', type: AdminUserResponseDto })
|
||||
@UseGuards(AdminGuard)
|
||||
@Get('users/:id')
|
||||
async getUser(@Param('id') id: string) {
|
||||
return await this.adminService.getUser(BigInt(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置用户密码
|
||||
*
|
||||
* 功能描述:
|
||||
* 管理员直接为指定用户设置新密码,新密码需满足密码强度规则
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证用户ID和新密码格式
|
||||
* 2. 检查用户是否存在
|
||||
* 3. 验证密码强度规则
|
||||
* 4. 更新用户密码
|
||||
* 5. 记录操作日志
|
||||
*
|
||||
* @param id 用户ID字符串
|
||||
* @param dto 密码重置请求数据
|
||||
* @returns 重置结果
|
||||
*
|
||||
* @throws NotFoundException 当用户不存在时
|
||||
* @throws BadRequestException 当密码不符合强度规则时
|
||||
* @throws TooManyRequestsException 当操作过于频繁时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const result = await adminController.resetPassword('123', {
|
||||
* newPassword: 'NewPass1234'
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({ summary: '重置用户密码', description: '管理员直接为用户设置新密码(需满足密码强度规则)' })
|
||||
@ApiParam({ name: 'id', description: '用户ID' })
|
||||
@ApiBody({ type: AdminResetPasswordDto })
|
||||
@ApiResponse({ status: 200, description: '重置成功', type: AdminCommonResponseDto })
|
||||
@ApiResponse({ status: 429, description: '操作过于频繁' })
|
||||
@UseGuards(AdminGuard)
|
||||
@Throttle(ThrottlePresets.ADMIN_OPERATION)
|
||||
@Post('users/:id/reset-password')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
async resetPassword(@Param('id') id: string, @Body() dto: AdminResetPasswordDto) {
|
||||
return await this.adminService.resetPassword(BigInt(id), dto.newPassword);
|
||||
}
|
||||
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({ summary: '获取运行日志尾部', description: '从 logs/ 目录读取最近的日志行(默认200行)' })
|
||||
@ApiQuery({ name: 'lines', required: false, description: '返回行数(默认200,最大2000)' })
|
||||
@ApiResponse({ status: 200, description: '获取成功', type: AdminRuntimeLogsResponseDto })
|
||||
@UseGuards(AdminGuard)
|
||||
@Get('logs/runtime')
|
||||
async getRuntimeLogs(@Query('lines') lines?: string) {
|
||||
const parsedLines = lines ? Number(lines) : undefined;
|
||||
return await this.adminService.getRuntimeLogs(parsedLines);
|
||||
}
|
||||
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({ summary: '下载全部运行日志', description: '将 logs/ 目录打包为 tar.gz 并下载(需要管理员Token)' })
|
||||
@ApiProduces('application/gzip')
|
||||
@ApiResponse({ status: 200, description: '打包下载成功(tar.gz 二进制流)' })
|
||||
@UseGuards(AdminGuard)
|
||||
@Get('logs/archive')
|
||||
async downloadLogsArchive(@Res() res: Response) {
|
||||
const logDir = this.adminService.getLogDirAbsolutePath();
|
||||
|
||||
// 验证日志目录
|
||||
const dirValidation = await this.validateLogDirectory(logDir, res);
|
||||
if (!dirValidation.isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
this.setArchiveResponseHeaders(res);
|
||||
|
||||
// 创建并处理tar进程
|
||||
await this.createAndHandleTarProcess(logDir, res);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证日志目录是否存在且可用
|
||||
*
|
||||
* @param logDir 日志目录路径
|
||||
* @param res 响应对象
|
||||
* @returns 验证结果
|
||||
*/
|
||||
private async validateLogDirectory(logDir: string, res: Response): Promise<{ isValid: boolean }> {
|
||||
try {
|
||||
const stats = await fs.promises.stat(logDir);
|
||||
if (!stats.isDirectory()) {
|
||||
res.status(404).json({ success: false, message: '日志目录不可用' });
|
||||
return { isValid: false };
|
||||
}
|
||||
return { isValid: true };
|
||||
} catch (error) {
|
||||
res.status(404).json({ success: false, message: '日志目录不存在' });
|
||||
return { isValid: false };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置文件下载的响应头
|
||||
*
|
||||
* @param res 响应对象
|
||||
*/
|
||||
private setArchiveResponseHeaders(res: Response): void {
|
||||
const ts = getCurrentTimestamp().replace(/[:.]/g, '-');
|
||||
const filename = `logs-${ts}.tar.gz`;
|
||||
|
||||
res.setHeader('Content-Type', 'application/gzip');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
||||
res.setHeader('Cache-Control', 'no-store');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建并处理tar进程
|
||||
*
|
||||
* @param logDir 日志目录路径
|
||||
* @param res 响应对象
|
||||
*/
|
||||
private async createAndHandleTarProcess(logDir: string, res: Response): Promise<void> {
|
||||
const parentDir = path.dirname(logDir);
|
||||
const baseName = path.basename(logDir);
|
||||
|
||||
const tar = spawn('tar', ['-czf', '-', '-C', parentDir, baseName], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
// 处理tar进程的stderr输出
|
||||
tar.stderr.on('data', (chunk: Buffer) => {
|
||||
const msg = chunk.toString('utf8').trim();
|
||||
if (msg) {
|
||||
this.logger.warn(`tar stderr: ${msg}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 处理tar进程错误
|
||||
tar.on('error', (err: any) => {
|
||||
this.handleTarProcessError(err, res);
|
||||
});
|
||||
|
||||
// 处理数据流和进程退出
|
||||
await this.handleTarStreams(tar, res);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理tar进程错误
|
||||
*
|
||||
* @param err 错误对象
|
||||
* @param res 响应对象
|
||||
*/
|
||||
private handleTarProcessError(err: any, res: Response): void {
|
||||
this.logger.error('打包日志失败(tar 进程启动失败)', err?.stack || String(err));
|
||||
if (!res.headersSent) {
|
||||
const msg = err?.code === 'ENOENT' ? '服务器缺少 tar 命令,无法打包日志' : '日志打包失败';
|
||||
res.status(500).json({ success: false, message: msg });
|
||||
} else {
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理tar进程的数据流和退出
|
||||
*
|
||||
* @param tar tar进程
|
||||
* @param res 响应对象
|
||||
*/
|
||||
private async handleTarStreams(tar: any, res: Response): Promise<void> {
|
||||
const pipelinePromise = new Promise<void>((resolve, reject) => {
|
||||
pipeline(tar.stdout, res, (err) => (err ? reject(err) : resolve()));
|
||||
});
|
||||
|
||||
const exitPromise = new Promise<void>((resolve, reject) => {
|
||||
tar.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`tar exited with code ${code ?? 'unknown'}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
await pipelinePromise;
|
||||
await exitPromise;
|
||||
} catch (err) {
|
||||
this.logger.error('打包日志失败(tar 执行或输出失败)', err instanceof Error ? err.stack : String(err));
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({ success: false, message: '日志打包失败' });
|
||||
} else {
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
103
src/business/admin/admin.guard.spec.ts
Normal file
103
src/business/admin/admin.guard.spec.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* AdminGuard 单元测试
|
||||
*
|
||||
* 功能描述:
|
||||
* - 测试管理员鉴权守卫的权限验证逻辑
|
||||
* - 验证Token解析和验证的正确性
|
||||
* - 测试各种异常情况的处理
|
||||
*
|
||||
* 职责分离:
|
||||
* - 权限验证测试,专注守卫逻辑
|
||||
* - Mock核心服务,测试守卫行为
|
||||
* - 验证请求拦截和放行的正确性
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-08: 注释规范优化 - 添加文件头注释,完善测试文档说明 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @since 2025-12-19
|
||||
* @lastModified 2026-01-08
|
||||
*/
|
||||
|
||||
import { ExecutionContext, UnauthorizedException } from '@nestjs/common';
|
||||
import { AdminCoreService, AdminAuthPayload } from '../../core/admin_core/admin_core.service';
|
||||
import { AdminGuard } from './admin.guard';
|
||||
|
||||
describe('AdminGuard', () => {
|
||||
const payload: AdminAuthPayload = {
|
||||
adminId: '1',
|
||||
username: 'admin',
|
||||
role: 9,
|
||||
iat: 1,
|
||||
exp: 2,
|
||||
};
|
||||
|
||||
const adminCoreServiceMock: Pick<AdminCoreService, 'verifyToken'> = {
|
||||
verifyToken: jest.fn(),
|
||||
};
|
||||
|
||||
const makeContext = (authorization?: any) => {
|
||||
const req: any = { headers: {} };
|
||||
if (authorization !== undefined) {
|
||||
req.headers['authorization'] = authorization;
|
||||
}
|
||||
|
||||
const ctx: Partial<ExecutionContext> = {
|
||||
switchToHttp: () => ({
|
||||
getRequest: () => req,
|
||||
getResponse: () => ({} as any),
|
||||
getNext: () => ({} as any),
|
||||
}),
|
||||
};
|
||||
|
||||
return { ctx: ctx as ExecutionContext, req };
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should allow access with valid admin token', () => {
|
||||
(adminCoreServiceMock.verifyToken as jest.Mock).mockReturnValue(payload);
|
||||
|
||||
const guard = new AdminGuard(adminCoreServiceMock as AdminCoreService);
|
||||
const { ctx, req } = makeContext('Bearer valid');
|
||||
|
||||
expect(guard.canActivate(ctx)).toBe(true);
|
||||
expect(adminCoreServiceMock.verifyToken).toHaveBeenCalledWith('valid');
|
||||
expect(req.admin).toEqual(payload);
|
||||
});
|
||||
|
||||
it('should deny access without token', () => {
|
||||
const guard = new AdminGuard(adminCoreServiceMock as AdminCoreService);
|
||||
const { ctx } = makeContext(undefined);
|
||||
|
||||
expect(() => guard.canActivate(ctx)).toThrow(UnauthorizedException);
|
||||
});
|
||||
|
||||
it('should deny access with invalid Authorization format', () => {
|
||||
const guard = new AdminGuard(adminCoreServiceMock as AdminCoreService);
|
||||
const { ctx } = makeContext('InvalidFormat');
|
||||
|
||||
expect(() => guard.canActivate(ctx)).toThrow(UnauthorizedException);
|
||||
});
|
||||
|
||||
it('should deny access when verifyToken throws (invalid/expired)', () => {
|
||||
(adminCoreServiceMock.verifyToken as jest.Mock).mockImplementation(() => {
|
||||
throw new UnauthorizedException('Token已过期');
|
||||
});
|
||||
|
||||
const guard = new AdminGuard(adminCoreServiceMock as AdminCoreService);
|
||||
const { ctx } = makeContext('Bearer bad');
|
||||
|
||||
expect(() => guard.canActivate(ctx)).toThrow(UnauthorizedException);
|
||||
});
|
||||
|
||||
it('should deny access when Authorization header is an array', () => {
|
||||
const guard = new AdminGuard(adminCoreServiceMock as AdminCoreService);
|
||||
const { ctx } = makeContext(['Bearer token']);
|
||||
|
||||
expect(() => guard.canActivate(ctx)).toThrow(UnauthorizedException);
|
||||
});
|
||||
});
|
||||
97
src/business/admin/admin.guard.ts
Normal file
97
src/business/admin/admin.guard.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* 管理员鉴权守卫
|
||||
*
|
||||
* 功能描述:
|
||||
* - 保护后台管理接口的访问权限
|
||||
* - 验证Authorization Bearer Token
|
||||
* - 确保只有role=9的管理员可以访问
|
||||
*
|
||||
* 职责分离:
|
||||
* - HTTP请求权限验证
|
||||
* - Token解析和验证
|
||||
* - 管理员身份确认
|
||||
*
|
||||
* 主要方法:
|
||||
* - canActivate() - 权限验证核心逻辑
|
||||
*
|
||||
* 使用场景:
|
||||
* - 后台管理API的权限保护
|
||||
* - 管理员身份验证
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-08: 注释规范优化 - 为接口添加注释,完善文档说明 (修改者: moyin)
|
||||
* - 2026-01-07: 代码规范优化 - 修正文件命名规范,更新作者信息和修改记录
|
||||
* - 2026-01-08: 注释规范优化 - 补充方法注释,添加@param、@returns、@throws和@example (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.3
|
||||
* @since 2025-12-19
|
||||
* @lastModified 2026-01-08
|
||||
*/
|
||||
|
||||
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import { AdminCoreService, AdminAuthPayload } from '../../core/admin_core/admin_core.service';
|
||||
|
||||
/**
|
||||
* 管理员请求接口
|
||||
*
|
||||
* 功能描述:
|
||||
* 扩展Express Request接口,添加管理员认证信息
|
||||
*
|
||||
* 使用场景:
|
||||
* - AdminGuard验证通过后,将管理员信息附加到请求对象
|
||||
* - 控制器方法中获取当前管理员信息
|
||||
*/
|
||||
export interface AdminRequest extends Request {
|
||||
admin?: AdminAuthPayload;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AdminGuard implements CanActivate {
|
||||
constructor(private readonly adminCoreService: AdminCoreService) {}
|
||||
|
||||
/**
|
||||
* 权限验证核心逻辑
|
||||
*
|
||||
* 功能描述:
|
||||
* 验证HTTP请求的Authorization头,确保只有管理员可以访问
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 提取Authorization头
|
||||
* 2. 验证Bearer Token格式
|
||||
* 3. 调用核心服务验证Token
|
||||
* 4. 将管理员信息附加到请求对象
|
||||
*
|
||||
* @param context 执行上下文,包含HTTP请求信息
|
||||
* @returns 是否允许访问,true表示允许
|
||||
*
|
||||
* @throws UnauthorizedException 当缺少Authorization头或格式错误时
|
||||
* @throws UnauthorizedException 当Token无效或过期时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 在控制器方法上使用
|
||||
* @UseGuards(AdminGuard)
|
||||
* @Get('users')
|
||||
* async getUsers() { ... }
|
||||
* ```
|
||||
*/
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const req = context.switchToHttp().getRequest<AdminRequest>();
|
||||
const auth = req.headers['authorization'];
|
||||
|
||||
if (!auth || Array.isArray(auth)) {
|
||||
throw new UnauthorizedException('缺少Authorization头');
|
||||
}
|
||||
|
||||
const [scheme, token] = auth.split(' ');
|
||||
if (scheme !== 'Bearer' || !token) {
|
||||
throw new UnauthorizedException('Authorization格式错误');
|
||||
}
|
||||
|
||||
const payload = this.adminCoreService.verifyToken(token);
|
||||
req.admin = payload;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
79
src/business/admin/admin.module.ts
Normal file
79
src/business/admin/admin.module.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* 管理员业务模块
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供后台管理的HTTP API(管理员登录、用户管理、密码重置等)
|
||||
* - 集成管理员核心服务和日志管理服务
|
||||
* - 导出管理员服务供其他模块使用
|
||||
*
|
||||
* 职责分离:
|
||||
* - 模块依赖管理和服务注册
|
||||
* - HTTP层与业务流程编排
|
||||
* - 核心鉴权与密码策略由AdminCoreService提供
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-08: 注释规范优化 - 修正import路径,创建缺失的控制器和服务文件 (修改者: moyin)
|
||||
* - 2026-01-07: 代码规范优化 - 修正文件命名规范,更新作者信息和修改记录
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.2
|
||||
* @since 2025-12-19
|
||||
* @lastModified 2026-01-08
|
||||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AdminCoreModule } from '../../core/admin_core/admin_core.module';
|
||||
import { LoggerModule } from '../../core/utils/logger/logger.module';
|
||||
import { UsersModule } from '../../core/db/users/users.module';
|
||||
import { UserProfilesModule } from '../../core/db/user_profiles/user_profiles.module';
|
||||
import { AdminController } from './admin.controller';
|
||||
import { AdminService } from './admin.service';
|
||||
import { AdminDatabaseController } from './admin_database.controller';
|
||||
import { AdminOperationLogController } from './admin_operation_log.controller';
|
||||
import { DatabaseManagementService } from './database_management.service';
|
||||
import { AdminOperationLogService } from './admin_operation_log.service';
|
||||
import { AdminOperationLog } from './admin_operation_log.entity';
|
||||
import { AdminDatabaseExceptionFilter } from './admin_database_exception.filter';
|
||||
import { AdminOperationLogInterceptor } from './admin_operation_log.interceptor';
|
||||
|
||||
/**
|
||||
* 检查数据库配置是否完整
|
||||
*
|
||||
* @returns 是否配置了数据库
|
||||
*/
|
||||
function isDatabaseConfigured(): boolean {
|
||||
const requiredEnvVars = ['DB_HOST', 'DB_PORT', 'DB_USERNAME', 'DB_PASSWORD', 'DB_NAME'];
|
||||
return requiredEnvVars.every(varName => process.env[varName]);
|
||||
}
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
AdminCoreModule,
|
||||
LoggerModule,
|
||||
UsersModule,
|
||||
// 根据数据库配置选择UserProfiles模块模式
|
||||
isDatabaseConfigured() ? UserProfilesModule.forDatabase() : UserProfilesModule.forMemory(),
|
||||
// 注意:ZulipAccountsModule 是全局模块,已在 AppModule 中导入,无需重复导入
|
||||
// 注册AdminOperationLog实体
|
||||
TypeOrmModule.forFeature([AdminOperationLog])
|
||||
],
|
||||
controllers: [
|
||||
AdminController,
|
||||
AdminDatabaseController,
|
||||
AdminOperationLogController
|
||||
],
|
||||
providers: [
|
||||
AdminService,
|
||||
DatabaseManagementService,
|
||||
AdminOperationLogService,
|
||||
AdminDatabaseExceptionFilter,
|
||||
AdminOperationLogInterceptor
|
||||
],
|
||||
exports: [
|
||||
AdminService,
|
||||
DatabaseManagementService,
|
||||
AdminOperationLogService
|
||||
], // 导出服务供其他模块使用
|
||||
})
|
||||
export class AdminModule {}
|
||||
290
src/business/admin/admin.service.spec.ts
Normal file
290
src/business/admin/admin.service.spec.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
/**
|
||||
* AdminService 单元测试
|
||||
*
|
||||
* 功能描述:
|
||||
* - 测试管理员业务服务的所有方法
|
||||
* - 验证业务逻辑的正确性
|
||||
* - 测试异常处理和边界情况
|
||||
*
|
||||
* 职责分离:
|
||||
* - 业务逻辑测试,不涉及HTTP层
|
||||
* - Mock核心服务,专注业务服务逻辑
|
||||
* - 验证数据处理和格式化的正确性
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-08: 注释规范优化 - 添加文件头注释,完善测试文档说明 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @since 2025-12-19
|
||||
* @lastModified 2026-01-08
|
||||
*/
|
||||
|
||||
import { NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { AdminService } from './admin.service';
|
||||
import { AdminCoreService } from '../../core/admin_core/admin_core.service';
|
||||
import { LogManagementService } from '../../core/utils/logger/log_management.service';
|
||||
import { Users } from '../../core/db/users/users.entity';
|
||||
import { UserStatus } from '../user_mgmt/user_status.enum';
|
||||
|
||||
describe('AdminService', () => {
|
||||
let service: AdminService;
|
||||
|
||||
const adminCoreServiceMock: Pick<AdminCoreService, 'login' | 'resetUserPassword'> = {
|
||||
login: jest.fn(),
|
||||
resetUserPassword: jest.fn(),
|
||||
};
|
||||
|
||||
const usersServiceMock = {
|
||||
findAll: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
|
||||
const logManagementServiceMock: Pick<LogManagementService, 'getRuntimeLogTail' | 'getLogDirAbsolutePath'> = {
|
||||
getRuntimeLogTail: jest.fn(),
|
||||
getLogDirAbsolutePath: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
service = new AdminService(
|
||||
adminCoreServiceMock as unknown as AdminCoreService,
|
||||
usersServiceMock as any,
|
||||
logManagementServiceMock as unknown as LogManagementService,
|
||||
);
|
||||
});
|
||||
|
||||
it('should login admin successfully', async () => {
|
||||
(adminCoreServiceMock.login as jest.Mock).mockResolvedValue({
|
||||
admin: { id: '1', username: 'admin', nickname: '管理员', role: 9 },
|
||||
access_token: 'token',
|
||||
expires_at: 123,
|
||||
});
|
||||
|
||||
const res = await service.login('admin', 'Admin123456');
|
||||
|
||||
expect(adminCoreServiceMock.login).toHaveBeenCalledWith({ identifier: 'admin', password: 'Admin123456' });
|
||||
expect(res.success).toBe(true);
|
||||
expect(res.data?.admin?.role).toBe(9);
|
||||
expect(res.message).toBe('管理员登录成功');
|
||||
});
|
||||
|
||||
it('should handle login failure', async () => {
|
||||
(adminCoreServiceMock.login as jest.Mock).mockRejectedValue(new Error('密码错误'));
|
||||
|
||||
const res = await service.login('admin', 'bad');
|
||||
|
||||
expect(res.success).toBe(false);
|
||||
expect(res.error_code).toBe('ADMIN_LOGIN_FAILED');
|
||||
expect(res.message).toBe('密码错误');
|
||||
});
|
||||
|
||||
it('should handle non-Error login failure', async () => {
|
||||
(adminCoreServiceMock.login as jest.Mock).mockRejectedValue('boom');
|
||||
|
||||
const res = await service.login('admin', 'bad');
|
||||
|
||||
expect(res.success).toBe(false);
|
||||
expect(res.error_code).toBe('ADMIN_LOGIN_FAILED');
|
||||
expect(res.message).toBe('管理员登录失败');
|
||||
});
|
||||
|
||||
it('should list users with pagination', async () => {
|
||||
const user = {
|
||||
id: BigInt(1),
|
||||
username: 'u1',
|
||||
nickname: 'U1',
|
||||
email: 'u1@test.com',
|
||||
email_verified: true,
|
||||
phone: null,
|
||||
avatar_url: null,
|
||||
role: 1,
|
||||
created_at: new Date('2025-01-01T00:00:00Z'),
|
||||
updated_at: new Date('2025-01-02T00:00:00Z'),
|
||||
} as unknown as Users;
|
||||
|
||||
usersServiceMock.findAll.mockResolvedValue([user]);
|
||||
|
||||
const res = await service.listUsers(100, 0);
|
||||
|
||||
expect(usersServiceMock.findAll).toHaveBeenCalledWith(100, 0);
|
||||
expect(res.success).toBe(true);
|
||||
expect(res.data?.users).toHaveLength(1);
|
||||
expect(res.data?.users[0]).toMatchObject({
|
||||
id: '1',
|
||||
username: 'u1',
|
||||
nickname: 'U1',
|
||||
role: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should get user by id', async () => {
|
||||
const user = {
|
||||
id: BigInt(3),
|
||||
username: 'u3',
|
||||
nickname: 'U3',
|
||||
email: null,
|
||||
email_verified: false,
|
||||
phone: '123',
|
||||
avatar_url: null,
|
||||
role: 1,
|
||||
created_at: new Date('2025-01-01T00:00:00Z'),
|
||||
updated_at: new Date('2025-01-02T00:00:00Z'),
|
||||
} as unknown as Users;
|
||||
|
||||
usersServiceMock.findOne.mockResolvedValue(user);
|
||||
|
||||
const res = await service.getUser(BigInt(3));
|
||||
|
||||
expect(usersServiceMock.findOne).toHaveBeenCalledWith(BigInt(3));
|
||||
expect(res.success).toBe(true);
|
||||
expect(res.data?.user).toMatchObject({ id: '3', username: 'u3', nickname: 'U3' });
|
||||
});
|
||||
|
||||
it('should reset user password', async () => {
|
||||
usersServiceMock.findOne.mockResolvedValue({ id: BigInt(2) } as unknown as Users);
|
||||
(adminCoreServiceMock.resetUserPassword as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
const res = await service.resetPassword(BigInt(2), 'NewPass1234');
|
||||
|
||||
expect(usersServiceMock.findOne).toHaveBeenCalledWith(BigInt(2));
|
||||
expect(adminCoreServiceMock.resetUserPassword).toHaveBeenCalledWith(BigInt(2), 'NewPass1234');
|
||||
expect(res).toEqual({ success: true, message: '密码重置成功' });
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when resetting password for missing user', async () => {
|
||||
usersServiceMock.findOne.mockRejectedValue(new Error('not found'));
|
||||
|
||||
await expect(service.resetPassword(BigInt(999), 'NewPass1234')).rejects.toBeInstanceOf(NotFoundException);
|
||||
});
|
||||
|
||||
it('should get runtime logs', async () => {
|
||||
(logManagementServiceMock.getRuntimeLogTail as jest.Mock).mockResolvedValue({
|
||||
file: 'dev.log',
|
||||
updated_at: '2025-01-01T00:00:00.000Z',
|
||||
lines: ['a', 'b'],
|
||||
});
|
||||
|
||||
const res = await service.getRuntimeLogs(2);
|
||||
|
||||
expect(logManagementServiceMock.getRuntimeLogTail).toHaveBeenCalledWith({ lines: 2 });
|
||||
expect(res.success).toBe(true);
|
||||
expect(res.data?.file).toBe('dev.log');
|
||||
expect(res.data?.lines).toEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
it('should expose log dir absolute path', () => {
|
||||
(logManagementServiceMock.getLogDirAbsolutePath as jest.Mock).mockReturnValue('/abs/logs');
|
||||
|
||||
expect(service.getLogDirAbsolutePath()).toBe('/abs/logs');
|
||||
expect(logManagementServiceMock.getLogDirAbsolutePath).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// 测试新增的用户状态管理方法
|
||||
describe('updateUserStatus', () => {
|
||||
const mockUser = {
|
||||
id: BigInt(1),
|
||||
username: 'testuser',
|
||||
status: UserStatus.ACTIVE
|
||||
} as unknown as Users;
|
||||
|
||||
it('should update user status successfully', async () => {
|
||||
usersServiceMock.findOne.mockResolvedValue(mockUser);
|
||||
usersServiceMock.update.mockResolvedValue({ ...mockUser, status: UserStatus.INACTIVE });
|
||||
|
||||
const result = await service.updateUserStatus(BigInt(1), { status: UserStatus.INACTIVE, reason: 'test' });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toBe('用户状态修改成功');
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when user not found', async () => {
|
||||
usersServiceMock.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(service.updateUserStatus(BigInt(999), { status: UserStatus.INACTIVE, reason: 'test' }))
|
||||
.rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('should return error when status unchanged', async () => {
|
||||
usersServiceMock.findOne.mockResolvedValue({ ...mockUser, status: UserStatus.INACTIVE });
|
||||
|
||||
await expect(service.updateUserStatus(BigInt(1), { status: UserStatus.INACTIVE, reason: 'test' }))
|
||||
.rejects.toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('batchUpdateUserStatus', () => {
|
||||
it('should batch update user status successfully', async () => {
|
||||
const mockUsers = [
|
||||
{ id: BigInt(1), username: 'user1', status: UserStatus.ACTIVE },
|
||||
{ id: BigInt(2), username: 'user2', status: UserStatus.ACTIVE }
|
||||
] as unknown as Users[];
|
||||
|
||||
usersServiceMock.findOne
|
||||
.mockResolvedValueOnce(mockUsers[0])
|
||||
.mockResolvedValueOnce(mockUsers[1]);
|
||||
|
||||
usersServiceMock.update
|
||||
.mockResolvedValueOnce({ ...mockUsers[0], status: UserStatus.INACTIVE })
|
||||
.mockResolvedValueOnce({ ...mockUsers[1], status: UserStatus.INACTIVE });
|
||||
|
||||
const result = await service.batchUpdateUserStatus({
|
||||
userIds: ['1', '2'],
|
||||
status: UserStatus.INACTIVE,
|
||||
reason: 'batch test'
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.result.success_count).toBe(2);
|
||||
expect(result.data?.result.failed_count).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle mixed success and failure', async () => {
|
||||
usersServiceMock.findOne
|
||||
.mockResolvedValueOnce({ id: BigInt(1), status: UserStatus.ACTIVE })
|
||||
.mockResolvedValueOnce(null); // User not found
|
||||
|
||||
usersServiceMock.update.mockResolvedValueOnce({ id: BigInt(1), status: UserStatus.INACTIVE });
|
||||
|
||||
const result = await service.batchUpdateUserStatus({
|
||||
userIds: ['1', '999'],
|
||||
status: UserStatus.INACTIVE,
|
||||
reason: 'mixed test'
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.result.success_count).toBe(1);
|
||||
expect(result.data?.result.failed_count).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserStatusStats', () => {
|
||||
it('should return user status statistics', async () => {
|
||||
const mockUsers = [
|
||||
{ status: UserStatus.ACTIVE },
|
||||
{ status: UserStatus.ACTIVE },
|
||||
{ status: UserStatus.INACTIVE },
|
||||
{ status: null } // Should default to active
|
||||
] as unknown as Users[];
|
||||
|
||||
usersServiceMock.findAll.mockResolvedValue(mockUsers);
|
||||
|
||||
const result = await service.getUserStatusStats();
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.stats.active).toBe(3); // 2 active + 1 null (defaults to active)
|
||||
expect(result.data?.stats.inactive).toBe(1);
|
||||
expect(result.data?.stats.total).toBe(4);
|
||||
});
|
||||
|
||||
it('should handle error when getting stats', async () => {
|
||||
usersServiceMock.findAll.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
const result = await service.getUserStatusStats();
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error_code).toBe('USER_STATUS_STATS_FAILED');
|
||||
});
|
||||
});
|
||||
});
|
||||
592
src/business/admin/admin.service.ts
Normal file
592
src/business/admin/admin.service.ts
Normal file
@@ -0,0 +1,592 @@
|
||||
/**
|
||||
* 管理员业务服务
|
||||
*
|
||||
* 功能描述:
|
||||
* - 管理员登录认证业务逻辑
|
||||
* - 用户管理业务功能(查询、密码重置、状态管理)
|
||||
* - 系统日志管理功能
|
||||
*
|
||||
* 职责分离:
|
||||
* - 业务逻辑编排和数据格式化
|
||||
* - 调用核心服务完成具体操作
|
||||
* - 异常处理和日志记录
|
||||
*
|
||||
* 主要方法:
|
||||
* - login() - 管理员登录认证
|
||||
* - listUsers() - 用户列表查询
|
||||
* - getUser() - 单个用户查询
|
||||
* - resetPassword() - 重置用户密码
|
||||
* - updateUserStatus() - 修改用户状态
|
||||
* - batchUpdateUserStatus() - 批量修改用户状态
|
||||
* - getUserStatusStats() - 获取用户状态统计
|
||||
* - getRuntimeLogs() - 获取运行日志
|
||||
*
|
||||
* 使用场景:
|
||||
* - 后台管理系统的业务逻辑处理
|
||||
* - 管理员权限相关的业务操作
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 修正文件命名规范,更新作者信息和修改记录
|
||||
* - 2026-01-08: 注释规范优化 - 补充方法注释,添加@param、@returns、@throws和@example (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.2
|
||||
* @since 2025-12-19
|
||||
* @lastModified 2026-01-08
|
||||
*/
|
||||
|
||||
import { Inject, Injectable, Logger, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { AdminCoreService } from '../../core/admin_core/admin_core.service';
|
||||
import { Users } from '../../core/db/users/users.entity';
|
||||
import { UsersService } from '../../core/db/users/users.service';
|
||||
import { UsersMemoryService } from '../../core/db/users/users_memory.service';
|
||||
import { LogManagementService } from '../../core/utils/logger/log_management.service';
|
||||
import { UserStatus, getUserStatusDescription } from '../user_mgmt/user_status.enum';
|
||||
import { UserStatusDto, BatchUserStatusDto } from '../user_mgmt/user_status.dto';
|
||||
import { getCurrentTimestamp } from './admin_utils';
|
||||
import { USER_QUERY_LIMITS } from './admin_constants';
|
||||
import {
|
||||
UserStatusResponseDto,
|
||||
BatchUserStatusResponseDto,
|
||||
UserStatusStatsResponseDto,
|
||||
UserStatusInfoDto,
|
||||
BatchOperationResultDto
|
||||
} from '../user_mgmt/user_status_response.dto';
|
||||
|
||||
export interface AdminApiResponse<T = any> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message: string;
|
||||
error_code?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AdminService {
|
||||
private readonly logger = new Logger(AdminService.name);
|
||||
|
||||
constructor(
|
||||
private readonly adminCoreService: AdminCoreService,
|
||||
@Inject('UsersService') private readonly usersService: UsersService | UsersMemoryService,
|
||||
private readonly logManagementService: LogManagementService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 记录操作日志
|
||||
*
|
||||
* @param level 日志级别
|
||||
* @param message 日志消息
|
||||
* @param context 日志上下文
|
||||
*/
|
||||
private logOperation(level: 'log' | 'warn' | 'error', message: string, context: Record<string, any>): void {
|
||||
this.logger[level](message, {
|
||||
...context,
|
||||
timestamp: getCurrentTimestamp()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日志目录绝对路径
|
||||
*
|
||||
* @returns 日志目录的绝对路径
|
||||
*/
|
||||
getLogDirAbsolutePath(): string {
|
||||
return this.logManagementService.getLogDirAbsolutePath();
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员登录
|
||||
*
|
||||
* 功能描述:
|
||||
* 验证管理员身份并生成JWT Token
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 调用核心服务验证登录信息
|
||||
* 2. 生成JWT Token
|
||||
* 3. 返回登录结果
|
||||
*
|
||||
* @param identifier 登录标识符(用户名/邮箱/手机号)
|
||||
* @param password 密码
|
||||
* @returns 登录结果,包含Token和管理员信息
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const result = await adminService.login('admin', 'password123');
|
||||
* ```
|
||||
*/
|
||||
async login(identifier: string, password: string): Promise<AdminApiResponse> {
|
||||
try {
|
||||
const result = await this.adminCoreService.login({ identifier, password });
|
||||
return { success: true, data: result, message: '管理员登录成功' };
|
||||
} catch (error) {
|
||||
this.logger.error(`管理员登录失败: ${identifier}`, error instanceof Error ? error.stack : String(error));
|
||||
return {
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '管理员登录失败',
|
||||
error_code: 'ADMIN_LOGIN_FAILED',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户列表
|
||||
*
|
||||
* 功能描述:
|
||||
* 分页获取系统中的用户列表
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 调用用户服务获取用户数据
|
||||
* 2. 格式化用户信息
|
||||
* 3. 返回分页结果
|
||||
*
|
||||
* @param limit 返回数量限制
|
||||
* @param offset 偏移量
|
||||
* @returns 用户列表和分页信息
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const result = await adminService.listUsers(20, 0);
|
||||
* ```
|
||||
*/
|
||||
async listUsers(limit: number, offset: number): Promise<AdminApiResponse<{ users: any[]; limit: number; offset: number }>> {
|
||||
const users = await this.usersService.findAll(limit, offset);
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
users: users.map((u: Users) => this.formatUser(u)),
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
message: '用户列表获取成功',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户详情
|
||||
*
|
||||
* 功能描述:
|
||||
* 根据用户ID获取指定用户的详细信息
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 查询用户信息
|
||||
* 2. 格式化用户数据
|
||||
* 3. 返回用户详情
|
||||
*
|
||||
* @param id 用户ID
|
||||
* @returns 用户详细信息
|
||||
*
|
||||
* @throws NotFoundException 当用户不存在时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const result = await adminService.getUser(BigInt(123));
|
||||
* ```
|
||||
*/
|
||||
async getUser(id: bigint): Promise<AdminApiResponse<{ user: any }>> {
|
||||
const user = await this.usersService.findOne(id);
|
||||
return {
|
||||
success: true,
|
||||
data: { user: this.formatUser(user) },
|
||||
message: '用户信息获取成功',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置用户密码
|
||||
*
|
||||
* 功能描述:
|
||||
* 管理员直接为指定用户设置新密码
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证用户是否存在
|
||||
* 2. 调用核心服务重置密码
|
||||
* 3. 记录操作日志
|
||||
* 4. 返回重置结果
|
||||
*
|
||||
* @param id 用户ID
|
||||
* @param newPassword 新密码
|
||||
* @returns 重置结果
|
||||
*
|
||||
* @throws NotFoundException 当用户不存在时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const result = await adminService.resetPassword(BigInt(123), 'NewPass1234');
|
||||
* ```
|
||||
*/
|
||||
async resetPassword(id: bigint, newPassword: string): Promise<AdminApiResponse> {
|
||||
// 确认用户存在
|
||||
const user = await this.usersService.findOne(id).catch((): null => null);
|
||||
if (!user) {
|
||||
throw new NotFoundException('用户不存在');
|
||||
}
|
||||
|
||||
await this.adminCoreService.resetUserPassword(id, newPassword);
|
||||
|
||||
this.logger.log(`管理员重置密码成功: userId=${id.toString()}`);
|
||||
|
||||
return { success: true, message: '密码重置成功' };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取运行日志
|
||||
*
|
||||
* 功能描述:
|
||||
* 获取系统运行日志的尾部内容
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 调用日志管理服务获取日志
|
||||
* 2. 返回日志内容和元信息
|
||||
*
|
||||
* @param lines 返回的日志行数,可选参数
|
||||
* @returns 日志内容和元信息
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const result = await adminService.getRuntimeLogs(200);
|
||||
* ```
|
||||
*/
|
||||
async getRuntimeLogs(lines?: number): Promise<AdminApiResponse<{ file: string; updated_at: string; lines: string[] }>> {
|
||||
const result = await this.logManagementService.getRuntimeLogTail({ lines });
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
message: '运行日志获取成功',
|
||||
};
|
||||
}
|
||||
|
||||
private formatUser(user: Users) {
|
||||
return {
|
||||
id: user.id.toString(),
|
||||
username: user.username,
|
||||
nickname: user.nickname,
|
||||
email: user.email,
|
||||
email_verified: user.email_verified,
|
||||
phone: user.phone,
|
||||
avatar_url: user.avatar_url,
|
||||
role: user.role,
|
||||
status: user.status || UserStatus.ACTIVE, // 兼容旧数据
|
||||
created_at: user.created_at,
|
||||
updated_at: user.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化用户状态信息
|
||||
*
|
||||
* @param user 用户实体
|
||||
* @returns 格式化的用户状态信息
|
||||
*/
|
||||
private formatUserStatus(user: Users): UserStatusInfoDto {
|
||||
return {
|
||||
id: user.id.toString(),
|
||||
username: user.username,
|
||||
nickname: user.nickname,
|
||||
status: user.status || UserStatus.ACTIVE,
|
||||
status_description: getUserStatusDescription(user.status || UserStatus.ACTIVE),
|
||||
updated_at: user.updated_at
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改用户状态
|
||||
*
|
||||
* 功能描述:
|
||||
* 管理员修改指定用户的账户状态,支持激活、锁定、禁用等操作
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证用户是否存在
|
||||
* 2. 检查状态变更的合法性
|
||||
* 3. 更新用户状态
|
||||
* 4. 记录状态变更日志
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param userStatusDto 状态修改数据
|
||||
* @returns 修改结果
|
||||
*
|
||||
* @throws NotFoundException 当用户不存在时
|
||||
* @throws BadRequestException 当状态变更不合法时
|
||||
*/
|
||||
async updateUserStatus(userId: bigint, userStatusDto: UserStatusDto): Promise<UserStatusResponseDto> {
|
||||
try {
|
||||
this.logOperation('log', '开始修改用户状态', {
|
||||
operation: 'update_user_status',
|
||||
userId: userId.toString(),
|
||||
newStatus: userStatusDto.status,
|
||||
reason: userStatusDto.reason
|
||||
});
|
||||
|
||||
// 1. 验证用户是否存在
|
||||
const user = await this.usersService.findOne(userId);
|
||||
if (!user) {
|
||||
this.logOperation('warn', '修改用户状态失败:用户不存在', {
|
||||
operation: 'update_user_status',
|
||||
userId: userId.toString()
|
||||
});
|
||||
throw new NotFoundException('用户不存在');
|
||||
}
|
||||
|
||||
// 2. 检查状态变更的合法性
|
||||
if (user.status === userStatusDto.status) {
|
||||
this.logOperation('warn', '修改用户状态失败:状态未发生变化', {
|
||||
operation: 'update_user_status',
|
||||
userId: userId.toString(),
|
||||
currentStatus: user.status,
|
||||
newStatus: userStatusDto.status
|
||||
});
|
||||
throw new BadRequestException('用户状态未发生变化');
|
||||
}
|
||||
|
||||
// 3. 更新用户状态
|
||||
const updatedUser = await this.usersService.update(userId, {
|
||||
status: userStatusDto.status
|
||||
});
|
||||
|
||||
// 4. 记录状态变更日志
|
||||
this.logOperation('log', '用户状态修改成功', {
|
||||
operation: 'update_user_status',
|
||||
userId: userId.toString(),
|
||||
oldStatus: user.status,
|
||||
newStatus: userStatusDto.status,
|
||||
reason: userStatusDto.reason
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
user: this.formatUserStatus(updatedUser),
|
||||
reason: userStatusDto.reason
|
||||
},
|
||||
message: '用户状态修改成功'
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.logOperation('error', '修改用户状态失败', {
|
||||
operation: 'update_user_status',
|
||||
userId: userId.toString(),
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
|
||||
if (error instanceof NotFoundException || error instanceof BadRequestException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: '用户状态修改失败',
|
||||
error_code: 'USER_STATUS_UPDATE_FAILED'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单个用户状态修改
|
||||
*
|
||||
* @param userIdStr 用户ID字符串
|
||||
* @param newStatus 新状态
|
||||
* @returns 处理结果
|
||||
*/
|
||||
private async processSingleUserStatus(
|
||||
userIdStr: string,
|
||||
newStatus: UserStatus
|
||||
): Promise<{ success: true; user: UserStatusInfoDto } | { success: false; error: string }> {
|
||||
try {
|
||||
const userId = BigInt(userIdStr);
|
||||
|
||||
// 验证用户是否存在
|
||||
const user = await this.usersService.findOne(userId);
|
||||
if (!user) {
|
||||
return { success: false, error: '用户不存在' };
|
||||
}
|
||||
|
||||
// 检查状态是否需要变更
|
||||
if (user.status === newStatus) {
|
||||
return { success: false, error: '用户状态未发生变化' };
|
||||
}
|
||||
|
||||
// 更新用户状态
|
||||
const updatedUser = await this.usersService.update(userId, { status: newStatus });
|
||||
return { success: true, user: this.formatUserStatus(updatedUser) };
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '未知错误'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量修改用户状态
|
||||
*
|
||||
* 功能描述:
|
||||
* 管理员批量修改多个用户的账户状态
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证用户ID列表
|
||||
* 2. 逐个处理用户状态修改
|
||||
* 3. 收集成功和失败的结果
|
||||
* 4. 返回批量操作结果
|
||||
*
|
||||
* @param batchUserStatusDto 批量状态修改数据
|
||||
* @returns 批量修改结果
|
||||
*/
|
||||
async batchUpdateUserStatus(batchUserStatusDto: BatchUserStatusDto): Promise<BatchUserStatusResponseDto> {
|
||||
try {
|
||||
this.logOperation('log', '开始批量修改用户状态', {
|
||||
operation: 'batch_update_user_status',
|
||||
userCount: batchUserStatusDto.userIds.length,
|
||||
newStatus: batchUserStatusDto.status,
|
||||
reason: batchUserStatusDto.reason
|
||||
});
|
||||
|
||||
const successUsers: UserStatusInfoDto[] = [];
|
||||
const failedUsers: Array<{ user_id: string; error: string }> = [];
|
||||
|
||||
// 逐个处理用户状态修改
|
||||
for (const userIdStr of batchUserStatusDto.userIds) {
|
||||
const result = await this.processSingleUserStatus(userIdStr, batchUserStatusDto.status);
|
||||
|
||||
if (result.success) {
|
||||
successUsers.push(result.user);
|
||||
} else {
|
||||
failedUsers.push({ user_id: userIdStr, error: (result as { success: false; error: string }).error });
|
||||
}
|
||||
}
|
||||
|
||||
// 构建批量操作结果
|
||||
const operationResult: BatchOperationResultDto = {
|
||||
success_users: successUsers,
|
||||
failed_users: failedUsers,
|
||||
success_count: successUsers.length,
|
||||
failed_count: failedUsers.length,
|
||||
total_count: batchUserStatusDto.userIds.length
|
||||
};
|
||||
|
||||
this.logOperation('log', '批量修改用户状态完成', {
|
||||
operation: 'batch_update_user_status',
|
||||
successCount: operationResult.success_count,
|
||||
failedCount: operationResult.failed_count,
|
||||
totalCount: operationResult.total_count
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
result: operationResult,
|
||||
reason: batchUserStatusDto.reason
|
||||
},
|
||||
message: `批量用户状态修改完成,成功:${operationResult.success_count},失败:${operationResult.failed_count}`
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.logOperation('error', '批量修改用户状态失败', {
|
||||
operation: 'batch_update_user_status',
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: '批量用户状态修改失败',
|
||||
error_code: 'BATCH_USER_STATUS_UPDATE_FAILED'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算用户状态统计
|
||||
*
|
||||
* @param users 用户列表
|
||||
* @returns 状态统计结果
|
||||
*/
|
||||
private calculateUserStatusStats(users: Users[]) {
|
||||
const stats = {
|
||||
active: 0,
|
||||
inactive: 0,
|
||||
locked: 0,
|
||||
banned: 0,
|
||||
deleted: 0,
|
||||
pending: 0,
|
||||
total: users.length
|
||||
};
|
||||
|
||||
users.forEach((user: Users) => {
|
||||
const status = user.status || UserStatus.ACTIVE;
|
||||
switch (status) {
|
||||
case UserStatus.ACTIVE:
|
||||
stats.active++;
|
||||
break;
|
||||
case UserStatus.INACTIVE:
|
||||
stats.inactive++;
|
||||
break;
|
||||
case UserStatus.LOCKED:
|
||||
stats.locked++;
|
||||
break;
|
||||
case UserStatus.BANNED:
|
||||
stats.banned++;
|
||||
break;
|
||||
case UserStatus.DELETED:
|
||||
stats.deleted++;
|
||||
break;
|
||||
case UserStatus.PENDING:
|
||||
stats.pending++;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户状态统计
|
||||
*
|
||||
* 功能描述:
|
||||
* 获取各种用户状态的数量统计信息
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 查询所有用户
|
||||
* 2. 按状态分组统计
|
||||
* 3. 计算各状态数量
|
||||
* 4. 返回统计结果
|
||||
*
|
||||
* @returns 状态统计信息
|
||||
*/
|
||||
async getUserStatusStats(): Promise<UserStatusStatsResponseDto> {
|
||||
try {
|
||||
this.logOperation('log', '开始获取用户状态统计', {
|
||||
operation: 'get_user_status_stats'
|
||||
});
|
||||
|
||||
// 查询所有用户(这里可以优化为直接查询统计信息)
|
||||
const allUsers = await this.usersService.findAll(USER_QUERY_LIMITS.MAX_USERS_FOR_STATS, 0);
|
||||
|
||||
// 计算各状态数量
|
||||
const stats = this.calculateUserStatusStats(allUsers);
|
||||
|
||||
this.logOperation('log', '用户状态统计获取成功', {
|
||||
operation: 'get_user_status_stats',
|
||||
stats
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
stats,
|
||||
timestamp: getCurrentTimestamp()
|
||||
},
|
||||
message: '用户状态统计获取成功'
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.logOperation('error', '获取用户状态统计失败', {
|
||||
operation: 'get_user_status_stats',
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: '用户状态统计获取失败',
|
||||
error_code: 'USER_STATUS_STATS_FAILED'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
185
src/business/admin/admin_constants.ts
Normal file
185
src/business/admin/admin_constants.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* 管理员模块常量定义
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义管理员模块使用的所有常量
|
||||
* - 统一管理配置参数和限制值
|
||||
* - 避免魔法数字的使用
|
||||
* - 提供类型安全的常量访问
|
||||
*
|
||||
* 职责分离:
|
||||
* - 常量集中管理
|
||||
* - 配置参数定义
|
||||
* - 限制值设定
|
||||
* - 敏感字段标识
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-08: 代码质量优化 - 添加日志查询限制和请求ID配置常量,补充用户查询限制常量 (修改者: moyin)
|
||||
* - 2026-01-08: 功能新增 - 创建管理员模块常量定义文件 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.2.0
|
||||
* @since 2026-01-08
|
||||
* @lastModified 2026-01-08
|
||||
*/
|
||||
|
||||
/**
|
||||
* 分页限制常量
|
||||
*/
|
||||
export const PAGINATION_LIMITS = {
|
||||
/** 默认每页数量 */
|
||||
DEFAULT_LIMIT: 20,
|
||||
/** 默认偏移量 */
|
||||
DEFAULT_OFFSET: 0,
|
||||
/** 用户列表最大每页数量 */
|
||||
USER_LIST_MAX_LIMIT: 100,
|
||||
/** 搜索结果最大每页数量 */
|
||||
SEARCH_MAX_LIMIT: 50,
|
||||
/** 日志列表最大每页数量 */
|
||||
LOG_LIST_MAX_LIMIT: 200,
|
||||
/** 批量操作最大数量 */
|
||||
BATCH_OPERATION_MAX_SIZE: 100
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 请求ID前缀常量
|
||||
*/
|
||||
export const REQUEST_ID_PREFIXES = {
|
||||
/** 通用请求 */
|
||||
GENERAL: 'req',
|
||||
/** 错误请求 */
|
||||
ERROR: 'err',
|
||||
/** 管理员操作 */
|
||||
ADMIN_OPERATION: 'admin',
|
||||
/** 数据库操作 */
|
||||
DATABASE_OPERATION: 'db',
|
||||
/** 健康检查 */
|
||||
HEALTH_CHECK: 'health',
|
||||
/** 日志操作 */
|
||||
LOG_OPERATION: 'log'
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 敏感字段列表
|
||||
*/
|
||||
export const SENSITIVE_FIELDS = [
|
||||
'password',
|
||||
'password_hash',
|
||||
'newPassword',
|
||||
'oldPassword',
|
||||
'token',
|
||||
'api_key',
|
||||
'secret',
|
||||
'private_key',
|
||||
'zulipApiKeyEncrypted'
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* 日志保留策略常量
|
||||
*/
|
||||
export const LOG_RETENTION = {
|
||||
/** 默认保留天数 */
|
||||
DEFAULT_DAYS: 90,
|
||||
/** 最少保留天数 */
|
||||
MIN_DAYS: 7,
|
||||
/** 最多保留天数 */
|
||||
MAX_DAYS: 365,
|
||||
/** 敏感操作日志保留天数 */
|
||||
SENSITIVE_OPERATION_DAYS: 180
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 操作类型常量
|
||||
*/
|
||||
export const OPERATION_TYPES = {
|
||||
CREATE: 'CREATE',
|
||||
UPDATE: 'UPDATE',
|
||||
DELETE: 'DELETE',
|
||||
QUERY: 'QUERY',
|
||||
BATCH: 'BATCH'
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 目标类型常量
|
||||
*/
|
||||
export const TARGET_TYPES = {
|
||||
USERS: 'users',
|
||||
USER_PROFILES: 'user_profiles',
|
||||
ZULIP_ACCOUNTS: 'zulip_accounts',
|
||||
ADMIN_LOGS: 'admin_logs'
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 操作结果常量
|
||||
*/
|
||||
export const OPERATION_RESULTS = {
|
||||
SUCCESS: 'SUCCESS',
|
||||
FAILED: 'FAILED'
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 错误码常量
|
||||
*/
|
||||
export const ERROR_CODES = {
|
||||
BAD_REQUEST: 'BAD_REQUEST',
|
||||
UNAUTHORIZED: 'UNAUTHORIZED',
|
||||
FORBIDDEN: 'FORBIDDEN',
|
||||
NOT_FOUND: 'NOT_FOUND',
|
||||
CONFLICT: 'CONFLICT',
|
||||
UNPROCESSABLE_ENTITY: 'UNPROCESSABLE_ENTITY',
|
||||
TOO_MANY_REQUESTS: 'TOO_MANY_REQUESTS',
|
||||
INTERNAL_SERVER_ERROR: 'INTERNAL_SERVER_ERROR',
|
||||
BAD_GATEWAY: 'BAD_GATEWAY',
|
||||
SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',
|
||||
GATEWAY_TIMEOUT: 'GATEWAY_TIMEOUT',
|
||||
UNKNOWN_ERROR: 'UNKNOWN_ERROR'
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* HTTP状态码常量
|
||||
*/
|
||||
export const HTTP_STATUS = {
|
||||
OK: 200,
|
||||
CREATED: 201,
|
||||
BAD_REQUEST: 400,
|
||||
UNAUTHORIZED: 401,
|
||||
FORBIDDEN: 403,
|
||||
NOT_FOUND: 404,
|
||||
CONFLICT: 409,
|
||||
UNPROCESSABLE_ENTITY: 422,
|
||||
TOO_MANY_REQUESTS: 429,
|
||||
INTERNAL_SERVER_ERROR: 500,
|
||||
BAD_GATEWAY: 502,
|
||||
SERVICE_UNAVAILABLE: 503,
|
||||
GATEWAY_TIMEOUT: 504
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 缓存键前缀常量
|
||||
*/
|
||||
export const CACHE_KEYS = {
|
||||
USER_LIST: 'admin:users:list',
|
||||
USER_PROFILE_LIST: 'admin:profiles:list',
|
||||
ZULIP_ACCOUNT_LIST: 'admin:zulip:list',
|
||||
STATISTICS: 'admin:stats'
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 日志查询限制常量
|
||||
*/
|
||||
export const LOG_QUERY_LIMITS = {
|
||||
/** 默认日志查询每页数量 */
|
||||
DEFAULT_LOG_QUERY_LIMIT: 50,
|
||||
/** 敏感操作日志默认查询数量 */
|
||||
SENSITIVE_LOG_DEFAULT_LIMIT: 50
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 用户查询限制常量
|
||||
*/
|
||||
export const USER_QUERY_LIMITS = {
|
||||
/** 用户状态统计查询的最大用户数 */
|
||||
MAX_USERS_FOR_STATS: 10000,
|
||||
/** 管理员操作历史默认查询数量 */
|
||||
ADMIN_HISTORY_DEFAULT_LIMIT: 20
|
||||
} as const;
|
||||
493
src/business/admin/admin_database.controller.spec.ts
Normal file
493
src/business/admin/admin_database.controller.spec.ts
Normal file
@@ -0,0 +1,493 @@
|
||||
/**
|
||||
* AdminDatabaseController 单元测试
|
||||
*
|
||||
* 功能描述:
|
||||
* - 测试管理员数据库管理控制器的所有HTTP端点
|
||||
* - 验证请求参数处理和响应格式
|
||||
* - 测试权限验证和异常处理
|
||||
*
|
||||
* 职责分离:
|
||||
* - HTTP层测试,不涉及业务逻辑实现
|
||||
* - Mock业务服务,专注控制器逻辑
|
||||
* - 验证请求响应的正确性
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-09: 功能新增 - 创建AdminDatabaseController单元测试 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-09
|
||||
* @lastModified 2026-01-09
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AdminDatabaseController } from './admin_database.controller';
|
||||
import { DatabaseManagementService } from './database_management.service';
|
||||
import { AdminOperationLogService } from './admin_operation_log.service';
|
||||
import { AdminGuard } from './admin.guard';
|
||||
|
||||
describe('AdminDatabaseController', () => {
|
||||
let controller: AdminDatabaseController;
|
||||
let databaseService: jest.Mocked<DatabaseManagementService>;
|
||||
|
||||
const mockDatabaseService = {
|
||||
// User management methods
|
||||
getUserList: jest.fn(),
|
||||
getUserById: jest.fn(),
|
||||
searchUsers: jest.fn(),
|
||||
createUser: jest.fn(),
|
||||
updateUser: jest.fn(),
|
||||
deleteUser: jest.fn(),
|
||||
|
||||
// User profile management methods
|
||||
getUserProfileList: jest.fn(),
|
||||
getUserProfileById: jest.fn(),
|
||||
getUserProfilesByMap: jest.fn(),
|
||||
createUserProfile: jest.fn(),
|
||||
updateUserProfile: jest.fn(),
|
||||
deleteUserProfile: jest.fn(),
|
||||
|
||||
// Zulip account management methods
|
||||
getZulipAccountList: jest.fn(),
|
||||
getZulipAccountById: jest.fn(),
|
||||
getZulipAccountStatistics: jest.fn(),
|
||||
createZulipAccount: jest.fn(),
|
||||
updateZulipAccount: jest.fn(),
|
||||
deleteZulipAccount: jest.fn(),
|
||||
batchUpdateZulipAccountStatus: jest.fn(),
|
||||
};
|
||||
|
||||
const mockAdminOperationLogService = {
|
||||
createLog: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AdminDatabaseController],
|
||||
providers: [
|
||||
{
|
||||
provide: DatabaseManagementService,
|
||||
useValue: mockDatabaseService,
|
||||
},
|
||||
{
|
||||
provide: AdminOperationLogService,
|
||||
useValue: mockAdminOperationLogService,
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideGuard(AdminGuard)
|
||||
.useValue({ canActivate: () => true })
|
||||
.compile();
|
||||
|
||||
controller = module.get<AdminDatabaseController>(AdminDatabaseController);
|
||||
databaseService = module.get(DatabaseManagementService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('User Management', () => {
|
||||
describe('getUserList', () => {
|
||||
it('should get user list with default pagination', async () => {
|
||||
const mockResponse = {
|
||||
success: true,
|
||||
data: { items: [], total: 0, limit: 20, offset: 0, has_more: false },
|
||||
message: '用户列表获取成功'
|
||||
};
|
||||
|
||||
databaseService.getUserList.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await controller.getUserList(20, 0);
|
||||
|
||||
expect(databaseService.getUserList).toHaveBeenCalledWith(20, 0);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('should get user list with custom pagination', async () => {
|
||||
const query = { limit: 50, offset: 10 };
|
||||
const mockResponse = {
|
||||
success: true,
|
||||
data: { items: [], total: 0, limit: 50, offset: 10, has_more: false },
|
||||
message: '用户列表获取成功'
|
||||
};
|
||||
|
||||
databaseService.getUserList.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await controller.getUserList(20, 0);
|
||||
|
||||
expect(databaseService.getUserList).toHaveBeenCalledWith(20, 0);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserById', () => {
|
||||
it('should get user by id successfully', async () => {
|
||||
const mockResponse = {
|
||||
success: true,
|
||||
data: { id: '1', username: 'testuser' },
|
||||
message: '用户详情获取成功'
|
||||
};
|
||||
|
||||
databaseService.getUserById.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await controller.getUserById('1');
|
||||
|
||||
expect(databaseService.getUserById).toHaveBeenCalledWith(BigInt(1));
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('searchUsers', () => {
|
||||
it('should search users successfully', async () => {
|
||||
const query = { search: 'admin', limit: 10 };
|
||||
const mockResponse = {
|
||||
success: true,
|
||||
data: { items: [], total: 0, limit: 10, offset: 0, has_more: false },
|
||||
message: '用户搜索成功'
|
||||
};
|
||||
|
||||
databaseService.searchUsers.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await controller.searchUsers('admin', 20);
|
||||
|
||||
expect(databaseService.searchUsers).toHaveBeenCalledWith('admin', 20);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createUser', () => {
|
||||
it('should create user successfully', async () => {
|
||||
const userData = { username: 'newuser', nickname: 'New User', email: 'new@test.com' };
|
||||
const mockResponse = {
|
||||
success: true,
|
||||
data: { id: '1', ...userData },
|
||||
message: '用户创建成功'
|
||||
};
|
||||
|
||||
databaseService.createUser.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await controller.createUser(userData);
|
||||
|
||||
expect(databaseService.createUser).toHaveBeenCalledWith(userData);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateUser', () => {
|
||||
it('should update user successfully', async () => {
|
||||
const updateData = { nickname: 'Updated User' };
|
||||
const mockResponse = {
|
||||
success: true,
|
||||
data: { id: '1', nickname: 'Updated User' },
|
||||
message: '用户更新成功'
|
||||
};
|
||||
|
||||
databaseService.updateUser.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await controller.updateUser('1', updateData);
|
||||
|
||||
expect(databaseService.updateUser).toHaveBeenCalledWith(BigInt(1), updateData);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteUser', () => {
|
||||
it('should delete user successfully', async () => {
|
||||
const mockResponse = {
|
||||
success: true,
|
||||
data: { deleted: true, id: '1' },
|
||||
message: '用户删除成功'
|
||||
};
|
||||
|
||||
databaseService.deleteUser.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await controller.deleteUser('1');
|
||||
|
||||
expect(databaseService.deleteUser).toHaveBeenCalledWith(BigInt(1));
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('User Profile Management', () => {
|
||||
describe('getUserProfileList', () => {
|
||||
it('should get user profile list successfully', async () => {
|
||||
const query = { limit: 20, offset: 0 };
|
||||
const mockResponse = {
|
||||
success: true,
|
||||
data: { items: [], total: 0, limit: 20, offset: 0, has_more: false },
|
||||
message: '用户档案列表获取成功'
|
||||
};
|
||||
|
||||
databaseService.getUserProfileList.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await controller.getUserProfileList(20, 0);
|
||||
|
||||
expect(databaseService.getUserProfileList).toHaveBeenCalledWith(20, 0);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserProfileById', () => {
|
||||
it('should get user profile by id successfully', async () => {
|
||||
const mockResponse = {
|
||||
success: true,
|
||||
data: { id: '1', user_id: '1', bio: 'Test bio' },
|
||||
message: '用户档案详情获取成功'
|
||||
};
|
||||
|
||||
databaseService.getUserProfileById.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await controller.getUserProfileById('1');
|
||||
|
||||
expect(databaseService.getUserProfileById).toHaveBeenCalledWith(BigInt(1));
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserProfilesByMap', () => {
|
||||
it('should get user profiles by map successfully', async () => {
|
||||
const mockResponse = {
|
||||
success: true,
|
||||
data: { items: [], total: 0, limit: 20, offset: 0, has_more: false },
|
||||
message: 'plaza 的用户档案列表获取成功'
|
||||
};
|
||||
|
||||
databaseService.getUserProfilesByMap.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await controller.getUserProfilesByMap('plaza', 20, 0);
|
||||
|
||||
expect(databaseService.getUserProfilesByMap).toHaveBeenCalledWith('plaza', 20, 0);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createUserProfile', () => {
|
||||
it('should create user profile successfully', async () => {
|
||||
const profileData = {
|
||||
user_id: '1',
|
||||
bio: 'Test bio',
|
||||
resume_content: 'Test resume',
|
||||
tags: '["tag1"]',
|
||||
social_links: '{"github":"test"}',
|
||||
skin_id: '1',
|
||||
current_map: 'plaza',
|
||||
pos_x: 100,
|
||||
pos_y: 200,
|
||||
status: 1
|
||||
};
|
||||
|
||||
const mockResponse = {
|
||||
success: true,
|
||||
data: { id: '1', ...profileData },
|
||||
message: '用户档案创建成功'
|
||||
};
|
||||
|
||||
databaseService.createUserProfile.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await controller.createUserProfile(profileData);
|
||||
|
||||
expect(databaseService.createUserProfile).toHaveBeenCalledWith(profileData);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateUserProfile', () => {
|
||||
it('should update user profile successfully', async () => {
|
||||
const updateData = { bio: 'Updated bio' };
|
||||
const mockResponse = {
|
||||
success: true,
|
||||
data: { id: '1', bio: 'Updated bio' },
|
||||
message: '用户档案更新成功'
|
||||
};
|
||||
|
||||
databaseService.updateUserProfile.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await controller.updateUserProfile('1', updateData);
|
||||
|
||||
expect(databaseService.updateUserProfile).toHaveBeenCalledWith(BigInt(1), updateData);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteUserProfile', () => {
|
||||
it('should delete user profile successfully', async () => {
|
||||
const mockResponse = {
|
||||
success: true,
|
||||
data: { deleted: true, id: '1' },
|
||||
message: '用户档案删除成功'
|
||||
};
|
||||
|
||||
databaseService.deleteUserProfile.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await controller.deleteUserProfile('1');
|
||||
|
||||
expect(databaseService.deleteUserProfile).toHaveBeenCalledWith(BigInt(1));
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Zulip Account Management', () => {
|
||||
describe('getZulipAccountList', () => {
|
||||
it('should get zulip account list successfully', async () => {
|
||||
const query = { limit: 20, offset: 0 };
|
||||
const mockResponse = {
|
||||
success: true,
|
||||
data: { items: [], total: 0, limit: 20, offset: 0, has_more: false },
|
||||
message: 'Zulip账号关联列表获取成功'
|
||||
};
|
||||
|
||||
databaseService.getZulipAccountList.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await controller.getZulipAccountList(20, 0);
|
||||
|
||||
expect(databaseService.getZulipAccountList).toHaveBeenCalledWith(20, 0);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getZulipAccountById', () => {
|
||||
it('should get zulip account by id successfully', async () => {
|
||||
const mockResponse = {
|
||||
success: true,
|
||||
data: { id: '1', gameUserId: '1', zulipEmail: 'test@zulip.com' },
|
||||
message: 'Zulip账号关联详情获取成功'
|
||||
};
|
||||
|
||||
databaseService.getZulipAccountById.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await controller.getZulipAccountById('1');
|
||||
|
||||
expect(databaseService.getZulipAccountById).toHaveBeenCalledWith('1');
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getZulipAccountStatistics', () => {
|
||||
it('should get zulip account statistics successfully', async () => {
|
||||
const mockResponse = {
|
||||
success: true,
|
||||
data: { active: 10, inactive: 5, total: 15 },
|
||||
message: 'Zulip账号关联统计获取成功'
|
||||
};
|
||||
|
||||
databaseService.getZulipAccountStatistics.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await controller.getZulipAccountStatistics();
|
||||
|
||||
expect(databaseService.getZulipAccountStatistics).toHaveBeenCalled();
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createZulipAccount', () => {
|
||||
it('should create zulip account successfully', async () => {
|
||||
const accountData = {
|
||||
gameUserId: '1',
|
||||
zulipUserId: 123,
|
||||
zulipEmail: 'test@zulip.com',
|
||||
zulipFullName: 'Test User',
|
||||
zulipApiKeyEncrypted: 'encrypted_key'
|
||||
};
|
||||
|
||||
const mockResponse = {
|
||||
success: true,
|
||||
data: { id: '1', ...accountData },
|
||||
message: 'Zulip账号关联创建成功'
|
||||
};
|
||||
|
||||
databaseService.createZulipAccount.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await controller.createZulipAccount(accountData);
|
||||
|
||||
expect(databaseService.createZulipAccount).toHaveBeenCalledWith(accountData);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateZulipAccount', () => {
|
||||
it('should update zulip account successfully', async () => {
|
||||
const updateData = { zulipFullName: 'Updated Name' };
|
||||
const mockResponse = {
|
||||
success: true,
|
||||
data: { id: '1', zulipFullName: 'Updated Name' },
|
||||
message: 'Zulip账号关联更新成功'
|
||||
};
|
||||
|
||||
databaseService.updateZulipAccount.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await controller.updateZulipAccount('1', updateData);
|
||||
|
||||
expect(databaseService.updateZulipAccount).toHaveBeenCalledWith('1', updateData);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteZulipAccount', () => {
|
||||
it('should delete zulip account successfully', async () => {
|
||||
const mockResponse = {
|
||||
success: true,
|
||||
data: { deleted: true, id: '1' },
|
||||
message: 'Zulip账号关联删除成功'
|
||||
};
|
||||
|
||||
databaseService.deleteZulipAccount.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await controller.deleteZulipAccount('1');
|
||||
|
||||
expect(databaseService.deleteZulipAccount).toHaveBeenCalledWith('1');
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('batchUpdateZulipAccountStatus', () => {
|
||||
it('should batch update zulip account status successfully', async () => {
|
||||
const batchData = {
|
||||
ids: ['1', '2', '3'],
|
||||
status: 'active' as const,
|
||||
reason: 'Batch activation'
|
||||
};
|
||||
|
||||
const mockResponse = {
|
||||
success: true,
|
||||
data: {
|
||||
success_count: 3,
|
||||
failed_count: 0,
|
||||
total_count: 3,
|
||||
reason: 'Batch activation'
|
||||
},
|
||||
message: 'Zulip账号关联批量状态更新完成,成功:3,失败:0'
|
||||
};
|
||||
|
||||
databaseService.batchUpdateZulipAccountStatus.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await controller.batchUpdateZulipAccountStatus(batchData);
|
||||
|
||||
expect(databaseService.batchUpdateZulipAccountStatus).toHaveBeenCalledWith(
|
||||
['1', '2', '3'],
|
||||
'active',
|
||||
'Batch activation'
|
||||
);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Health Check', () => {
|
||||
describe('healthCheck', () => {
|
||||
it('should return health status successfully', async () => {
|
||||
const result = await controller.healthCheck();
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data.status).toBe('healthy');
|
||||
expect(result.data.services).toBeDefined();
|
||||
expect(result.data.services.users).toBe('connected');
|
||||
expect(result.data.services.user_profiles).toBe('connected');
|
||||
expect(result.data.services.zulip_accounts).toBe('connected');
|
||||
expect(result.message).toBe('数据库管理系统运行正常');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
404
src/business/admin/admin_database.controller.ts
Normal file
404
src/business/admin/admin_database.controller.ts
Normal file
@@ -0,0 +1,404 @@
|
||||
/**
|
||||
* 管理员数据库管理控制器
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供管理员专用的数据库管理HTTP接口
|
||||
* - 集成用户、用户档案、Zulip账号关联的CRUD操作
|
||||
* - 实现统一的权限控制和参数验证
|
||||
* - 支持分页查询和搜索功能
|
||||
*
|
||||
* 职责分离:
|
||||
* - HTTP请求处理:接收和验证HTTP请求参数
|
||||
* - 权限控制:通过AdminGuard确保只有管理员可以访问
|
||||
* - 业务委托:将业务逻辑委托给DatabaseManagementService处理
|
||||
* - 响应格式化:返回统一格式的HTTP响应
|
||||
*
|
||||
* API端点分组:
|
||||
* - /admin/database/users/* 用户管理相关接口
|
||||
* - /admin/database/user-profiles/* 用户档案管理相关接口
|
||||
* - /admin/database/zulip-accounts/* Zulip账号关联管理相关接口
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-08: 注释规范优化 - 修正@author字段,更新版本号和修改记录 (修改者: moyin)
|
||||
* - 2026-01-08: 代码质量优化 - 清理未使用的导入 (修改者: moyin)
|
||||
* - 2026-01-08: 文件夹扁平化 - 从controllers/子文件夹移动到上级目录 (修改者: moyin)
|
||||
* - 2026-01-08: 功能新增 - 创建管理员数据库管理控制器 (修改者: assistant)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.1.0
|
||||
* @since 2026-01-08
|
||||
* @lastModified 2026-01-08
|
||||
*/
|
||||
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Param,
|
||||
Query,
|
||||
Body,
|
||||
UseGuards,
|
||||
UseFilters,
|
||||
UseInterceptors,
|
||||
ParseIntPipe,
|
||||
DefaultValuePipe
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiBearerAuth,
|
||||
ApiOperation,
|
||||
ApiParam,
|
||||
ApiQuery,
|
||||
ApiResponse,
|
||||
ApiBody
|
||||
} from '@nestjs/swagger';
|
||||
import { AdminGuard } from './admin.guard';
|
||||
import { AdminDatabaseExceptionFilter } from './admin_database_exception.filter';
|
||||
import { AdminOperationLogInterceptor } from './admin_operation_log.interceptor';
|
||||
import { LogAdminOperation } from './log_admin_operation.decorator';
|
||||
import { DatabaseManagementService, AdminApiResponse, AdminListResponse } from './database_management.service';
|
||||
import {
|
||||
AdminCreateUserDto,
|
||||
AdminUpdateUserDto,
|
||||
AdminBatchUpdateStatusDto,
|
||||
AdminDatabaseResponseDto,
|
||||
AdminHealthCheckResponseDto,
|
||||
AdminCreateUserProfileDto,
|
||||
AdminUpdateUserProfileDto,
|
||||
AdminCreateZulipAccountDto,
|
||||
AdminUpdateZulipAccountDto
|
||||
} from './admin_database.dto';
|
||||
import { PAGINATION_LIMITS, REQUEST_ID_PREFIXES } from './admin_constants';
|
||||
import { safeLimitValue, createSuccessResponse, getCurrentTimestamp } from './admin_utils';
|
||||
|
||||
@ApiTags('admin-database')
|
||||
@Controller('admin/database')
|
||||
@UseGuards(AdminGuard)
|
||||
@UseFilters(AdminDatabaseExceptionFilter)
|
||||
@UseInterceptors(AdminOperationLogInterceptor)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
export class AdminDatabaseController {
|
||||
constructor(
|
||||
private readonly databaseManagementService: DatabaseManagementService
|
||||
) {}
|
||||
|
||||
// ==================== 用户管理接口 ====================
|
||||
|
||||
@ApiOperation({
|
||||
summary: '获取用户列表',
|
||||
description: '分页获取用户列表,支持管理员查看所有用户信息'
|
||||
})
|
||||
@ApiQuery({ name: 'limit', required: false, description: '返回数量(默认20,最大100)', example: 20 })
|
||||
@ApiQuery({ name: 'offset', required: false, description: '偏移量(默认0)', example: 0 })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
@ApiResponse({ status: 401, description: '未授权访问' })
|
||||
@ApiResponse({ status: 403, description: '权限不足' })
|
||||
@LogAdminOperation({
|
||||
operationType: 'QUERY',
|
||||
targetType: 'users',
|
||||
description: '获取用户列表',
|
||||
isSensitive: false
|
||||
})
|
||||
@Get('users')
|
||||
async getUserList(
|
||||
@Query('limit', new DefaultValuePipe(PAGINATION_LIMITS.DEFAULT_LIMIT), ParseIntPipe) limit: number,
|
||||
@Query('offset', new DefaultValuePipe(PAGINATION_LIMITS.DEFAULT_OFFSET), ParseIntPipe) offset: number
|
||||
): Promise<AdminListResponse> {
|
||||
const safeLimit = safeLimitValue(limit, PAGINATION_LIMITS.USER_LIST_MAX_LIMIT);
|
||||
return await this.databaseManagementService.getUserList(safeLimit, offset);
|
||||
}
|
||||
|
||||
@ApiOperation({
|
||||
summary: '获取用户详情',
|
||||
description: '根据用户ID获取详细的用户信息'
|
||||
})
|
||||
@ApiParam({ name: 'id', description: '用户ID', example: '1' })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
@ApiResponse({ status: 404, description: '用户不存在' })
|
||||
@Get('users/:id')
|
||||
async getUserById(@Param('id') id: string): Promise<AdminApiResponse> {
|
||||
return await this.databaseManagementService.getUserById(BigInt(id));
|
||||
}
|
||||
|
||||
@ApiOperation({
|
||||
summary: '搜索用户',
|
||||
description: '根据关键词搜索用户,支持用户名、邮箱、昵称模糊匹配'
|
||||
})
|
||||
@ApiQuery({ name: 'keyword', description: '搜索关键词', example: 'admin' })
|
||||
@ApiQuery({ name: 'limit', required: false, description: '返回数量(默认20,最大50)', example: 20 })
|
||||
@ApiResponse({ status: 200, description: '搜索成功' })
|
||||
@Get('users/search')
|
||||
async searchUsers(
|
||||
@Query('keyword') keyword: string,
|
||||
@Query('limit', new DefaultValuePipe(PAGINATION_LIMITS.DEFAULT_LIMIT), ParseIntPipe) limit: number
|
||||
): Promise<AdminListResponse> {
|
||||
const safeLimit = safeLimitValue(limit, PAGINATION_LIMITS.SEARCH_MAX_LIMIT);
|
||||
return await this.databaseManagementService.searchUsers(keyword, safeLimit);
|
||||
}
|
||||
|
||||
@ApiOperation({
|
||||
summary: '创建用户',
|
||||
description: '创建新用户,需要提供用户名和昵称等基本信息'
|
||||
})
|
||||
@ApiBody({ type: AdminCreateUserDto, description: '用户创建数据' })
|
||||
@ApiResponse({ status: 201, description: '创建成功', type: AdminDatabaseResponseDto })
|
||||
@ApiResponse({ status: 400, description: '请求参数错误' })
|
||||
@ApiResponse({ status: 409, description: '用户名或邮箱已存在' })
|
||||
@LogAdminOperation({
|
||||
operationType: 'CREATE',
|
||||
targetType: 'users',
|
||||
description: '创建用户',
|
||||
isSensitive: true
|
||||
})
|
||||
@Post('users')
|
||||
async createUser(@Body() createUserDto: AdminCreateUserDto): Promise<AdminApiResponse> {
|
||||
return await this.databaseManagementService.createUser(createUserDto);
|
||||
}
|
||||
|
||||
@ApiOperation({
|
||||
summary: '更新用户',
|
||||
description: '根据用户ID更新用户信息'
|
||||
})
|
||||
@ApiParam({ name: 'id', description: '用户ID', example: '1' })
|
||||
@ApiBody({ type: AdminUpdateUserDto, description: '用户更新数据' })
|
||||
@ApiResponse({ status: 200, description: '更新成功', type: AdminDatabaseResponseDto })
|
||||
@ApiResponse({ status: 404, description: '用户不存在' })
|
||||
@Put('users/:id')
|
||||
async updateUser(
|
||||
@Param('id') id: string,
|
||||
@Body() updateUserDto: AdminUpdateUserDto
|
||||
): Promise<AdminApiResponse> {
|
||||
return await this.databaseManagementService.updateUser(BigInt(id), updateUserDto);
|
||||
}
|
||||
|
||||
@ApiOperation({
|
||||
summary: '删除用户',
|
||||
description: '根据用户ID删除用户(软删除)'
|
||||
})
|
||||
@ApiParam({ name: 'id', description: '用户ID', example: '1' })
|
||||
@ApiResponse({ status: 200, description: '删除成功' })
|
||||
@ApiResponse({ status: 404, description: '用户不存在' })
|
||||
@LogAdminOperation({
|
||||
operationType: 'DELETE',
|
||||
targetType: 'users',
|
||||
description: '删除用户',
|
||||
isSensitive: true
|
||||
})
|
||||
@Delete('users/:id')
|
||||
async deleteUser(@Param('id') id: string): Promise<AdminApiResponse> {
|
||||
return await this.databaseManagementService.deleteUser(BigInt(id));
|
||||
}
|
||||
|
||||
// ==================== 用户档案管理接口 ====================
|
||||
|
||||
@ApiOperation({
|
||||
summary: '获取用户档案列表',
|
||||
description: '分页获取用户档案列表,包含位置信息和档案数据'
|
||||
})
|
||||
@ApiQuery({ name: 'limit', required: false, description: '返回数量(默认20,最大100)', example: 20 })
|
||||
@ApiQuery({ name: 'offset', required: false, description: '偏移量(默认0)', example: 0 })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
@Get('user-profiles')
|
||||
async getUserProfileList(
|
||||
@Query('limit', new DefaultValuePipe(PAGINATION_LIMITS.DEFAULT_LIMIT), ParseIntPipe) limit: number,
|
||||
@Query('offset', new DefaultValuePipe(PAGINATION_LIMITS.DEFAULT_OFFSET), ParseIntPipe) offset: number
|
||||
): Promise<AdminListResponse> {
|
||||
const safeLimit = safeLimitValue(limit, PAGINATION_LIMITS.USER_LIST_MAX_LIMIT);
|
||||
return await this.databaseManagementService.getUserProfileList(safeLimit, offset);
|
||||
}
|
||||
|
||||
@ApiOperation({
|
||||
summary: '获取用户档案详情',
|
||||
description: '根据档案ID获取详细的用户档案信息'
|
||||
})
|
||||
@ApiParam({ name: 'id', description: '档案ID', example: '1' })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
@ApiResponse({ status: 404, description: '档案不存在' })
|
||||
@Get('user-profiles/:id')
|
||||
async getUserProfileById(@Param('id') id: string): Promise<AdminApiResponse> {
|
||||
return await this.databaseManagementService.getUserProfileById(BigInt(id));
|
||||
}
|
||||
|
||||
@ApiOperation({
|
||||
summary: '根据地图获取用户档案',
|
||||
description: '获取指定地图中的所有用户档案信息'
|
||||
})
|
||||
@ApiParam({ name: 'mapId', description: '地图ID', example: 'plaza' })
|
||||
@ApiQuery({ name: 'limit', required: false, description: '返回数量(默认20,最大100)', example: 20 })
|
||||
@ApiQuery({ name: 'offset', required: false, description: '偏移量(默认0)', example: 0 })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
@Get('user-profiles/by-map/:mapId')
|
||||
async getUserProfilesByMap(
|
||||
@Param('mapId') mapId: string,
|
||||
@Query('limit', new DefaultValuePipe(PAGINATION_LIMITS.DEFAULT_LIMIT), ParseIntPipe) limit: number,
|
||||
@Query('offset', new DefaultValuePipe(PAGINATION_LIMITS.DEFAULT_OFFSET), ParseIntPipe) offset: number
|
||||
): Promise<AdminListResponse> {
|
||||
const safeLimit = safeLimitValue(limit, PAGINATION_LIMITS.USER_LIST_MAX_LIMIT);
|
||||
return await this.databaseManagementService.getUserProfilesByMap(mapId, safeLimit, offset);
|
||||
}
|
||||
|
||||
@ApiOperation({
|
||||
summary: '创建用户档案',
|
||||
description: '为指定用户创建档案信息'
|
||||
})
|
||||
@ApiBody({ type: AdminCreateUserProfileDto, description: '用户档案创建数据' })
|
||||
@ApiResponse({ status: 201, description: '创建成功' })
|
||||
@ApiResponse({ status: 400, description: '请求参数错误' })
|
||||
@ApiResponse({ status: 409, description: '用户档案已存在' })
|
||||
@Post('user-profiles')
|
||||
async createUserProfile(@Body() createProfileDto: AdminCreateUserProfileDto): Promise<AdminApiResponse> {
|
||||
return await this.databaseManagementService.createUserProfile(createProfileDto);
|
||||
}
|
||||
|
||||
@ApiOperation({
|
||||
summary: '更新用户档案',
|
||||
description: '根据档案ID更新用户档案信息'
|
||||
})
|
||||
@ApiParam({ name: 'id', description: '档案ID', example: '1' })
|
||||
@ApiBody({ type: AdminUpdateUserProfileDto, description: '用户档案更新数据' })
|
||||
@ApiResponse({ status: 200, description: '更新成功' })
|
||||
@ApiResponse({ status: 404, description: '档案不存在' })
|
||||
@Put('user-profiles/:id')
|
||||
async updateUserProfile(
|
||||
@Param('id') id: string,
|
||||
@Body() updateProfileDto: AdminUpdateUserProfileDto
|
||||
): Promise<AdminApiResponse> {
|
||||
return await this.databaseManagementService.updateUserProfile(BigInt(id), updateProfileDto);
|
||||
}
|
||||
|
||||
@ApiOperation({
|
||||
summary: '删除用户档案',
|
||||
description: '根据档案ID删除用户档案'
|
||||
})
|
||||
@ApiParam({ name: 'id', description: '档案ID', example: '1' })
|
||||
@ApiResponse({ status: 200, description: '删除成功' })
|
||||
@ApiResponse({ status: 404, description: '档案不存在' })
|
||||
@Delete('user-profiles/:id')
|
||||
async deleteUserProfile(@Param('id') id: string): Promise<AdminApiResponse> {
|
||||
return await this.databaseManagementService.deleteUserProfile(BigInt(id));
|
||||
}
|
||||
|
||||
// ==================== Zulip账号关联管理接口 ====================
|
||||
|
||||
@ApiOperation({
|
||||
summary: '获取Zulip账号关联列表',
|
||||
description: '分页获取Zulip账号关联列表,包含关联状态和错误信息'
|
||||
})
|
||||
@ApiQuery({ name: 'limit', required: false, description: '返回数量(默认20,最大100)', example: 20 })
|
||||
@ApiQuery({ name: 'offset', required: false, description: '偏移量(默认0)', example: 0 })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
@Get('zulip-accounts')
|
||||
async getZulipAccountList(
|
||||
@Query('limit', new DefaultValuePipe(PAGINATION_LIMITS.DEFAULT_LIMIT), ParseIntPipe) limit: number,
|
||||
@Query('offset', new DefaultValuePipe(PAGINATION_LIMITS.DEFAULT_OFFSET), ParseIntPipe) offset: number
|
||||
): Promise<AdminListResponse> {
|
||||
const safeLimit = safeLimitValue(limit, PAGINATION_LIMITS.USER_LIST_MAX_LIMIT);
|
||||
return await this.databaseManagementService.getZulipAccountList(safeLimit, offset);
|
||||
}
|
||||
|
||||
@ApiOperation({
|
||||
summary: '获取Zulip账号关联详情',
|
||||
description: '根据关联ID获取详细的Zulip账号关联信息'
|
||||
})
|
||||
@ApiParam({ name: 'id', description: '关联ID', example: '1' })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
@ApiResponse({ status: 404, description: '关联不存在' })
|
||||
@Get('zulip-accounts/:id')
|
||||
async getZulipAccountById(@Param('id') id: string): Promise<AdminApiResponse> {
|
||||
return await this.databaseManagementService.getZulipAccountById(id);
|
||||
}
|
||||
|
||||
@ApiOperation({
|
||||
summary: '获取Zulip账号关联统计',
|
||||
description: '获取各种状态的Zulip账号关联数量统计信息'
|
||||
})
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
@Get('zulip-accounts/statistics')
|
||||
async getZulipAccountStatistics(): Promise<AdminApiResponse> {
|
||||
return await this.databaseManagementService.getZulipAccountStatistics();
|
||||
}
|
||||
|
||||
@ApiOperation({
|
||||
summary: '创建Zulip账号关联',
|
||||
description: '创建游戏用户与Zulip账号的关联'
|
||||
})
|
||||
@ApiBody({ type: AdminCreateZulipAccountDto, description: 'Zulip账号关联创建数据' })
|
||||
@ApiResponse({ status: 201, description: '创建成功' })
|
||||
@ApiResponse({ status: 400, description: '请求参数错误' })
|
||||
@ApiResponse({ status: 409, description: '关联已存在' })
|
||||
@Post('zulip-accounts')
|
||||
async createZulipAccount(@Body() createAccountDto: AdminCreateZulipAccountDto): Promise<AdminApiResponse> {
|
||||
return await this.databaseManagementService.createZulipAccount(createAccountDto);
|
||||
}
|
||||
|
||||
@ApiOperation({
|
||||
summary: '更新Zulip账号关联',
|
||||
description: '根据关联ID更新Zulip账号关联信息'
|
||||
})
|
||||
@ApiParam({ name: 'id', description: '关联ID', example: '1' })
|
||||
@ApiBody({ type: AdminUpdateZulipAccountDto, description: 'Zulip账号关联更新数据' })
|
||||
@ApiResponse({ status: 200, description: '更新成功' })
|
||||
@ApiResponse({ status: 404, description: '关联不存在' })
|
||||
@Put('zulip-accounts/:id')
|
||||
async updateZulipAccount(
|
||||
@Param('id') id: string,
|
||||
@Body() updateAccountDto: AdminUpdateZulipAccountDto
|
||||
): Promise<AdminApiResponse> {
|
||||
return await this.databaseManagementService.updateZulipAccount(id, updateAccountDto);
|
||||
}
|
||||
|
||||
@ApiOperation({
|
||||
summary: '删除Zulip账号关联',
|
||||
description: '根据关联ID删除Zulip账号关联'
|
||||
})
|
||||
@ApiParam({ name: 'id', description: '关联ID', example: '1' })
|
||||
@ApiResponse({ status: 200, description: '删除成功' })
|
||||
@ApiResponse({ status: 404, description: '关联不存在' })
|
||||
@Delete('zulip-accounts/:id')
|
||||
async deleteZulipAccount(@Param('id') id: string): Promise<AdminApiResponse> {
|
||||
return await this.databaseManagementService.deleteZulipAccount(id);
|
||||
}
|
||||
|
||||
@ApiOperation({
|
||||
summary: '批量更新Zulip账号状态',
|
||||
description: '批量更新多个Zulip账号关联的状态'
|
||||
})
|
||||
@ApiBody({ type: AdminBatchUpdateStatusDto, description: '批量更新数据' })
|
||||
@ApiResponse({ status: 200, description: '批量更新完成', type: AdminDatabaseResponseDto })
|
||||
@LogAdminOperation({
|
||||
operationType: 'BATCH',
|
||||
targetType: 'zulip_accounts',
|
||||
description: '批量更新Zulip账号状态',
|
||||
isSensitive: true
|
||||
})
|
||||
@Post('zulip-accounts/batch-update-status')
|
||||
async batchUpdateZulipAccountStatus(@Body() batchUpdateDto: AdminBatchUpdateStatusDto): Promise<AdminApiResponse> {
|
||||
return await this.databaseManagementService.batchUpdateZulipAccountStatus(
|
||||
batchUpdateDto.ids,
|
||||
batchUpdateDto.status,
|
||||
batchUpdateDto.reason
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 系统健康检查接口 ====================
|
||||
|
||||
@ApiOperation({
|
||||
summary: '数据库管理系统健康检查',
|
||||
description: '检查数据库管理系统的运行状态和连接情况'
|
||||
})
|
||||
@ApiResponse({ status: 200, description: '系统正常', type: AdminHealthCheckResponseDto })
|
||||
@Get('health')
|
||||
async healthCheck(): Promise<AdminApiResponse> {
|
||||
return createSuccessResponse({
|
||||
status: 'healthy',
|
||||
timestamp: getCurrentTimestamp(),
|
||||
services: {
|
||||
users: 'connected',
|
||||
user_profiles: 'connected',
|
||||
zulip_accounts: 'connected'
|
||||
}
|
||||
}, '数据库管理系统运行正常', REQUEST_ID_PREFIXES.HEALTH_CHECK);
|
||||
}
|
||||
}
|
||||
570
src/business/admin/admin_database.dto.ts
Normal file
570
src/business/admin/admin_database.dto.ts
Normal file
@@ -0,0 +1,570 @@
|
||||
/**
|
||||
* 管理员数据库管理 DTO
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义管理员数据库管理相关的请求和响应数据结构
|
||||
* - 提供完整的数据验证规则
|
||||
* - 支持Swagger文档自动生成
|
||||
*
|
||||
* 职责分离:
|
||||
* - 请求数据结构定义和验证
|
||||
* - 响应数据结构定义
|
||||
* - API文档生成支持
|
||||
* - 类型安全保障
|
||||
*
|
||||
* DTO分类:
|
||||
* - Query DTOs: 查询参数验证
|
||||
* - Create DTOs: 创建操作数据验证
|
||||
* - Update DTOs: 更新操作数据验证
|
||||
* - Response DTOs: 响应数据结构定义
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-08: 注释规范优化 - 修正@author字段,更新版本号和修改记录 (修改者: moyin)
|
||||
* - 2026-01-08: 注释规范优化 - 为所有DTO类添加类注释,完善文档说明 (修改者: moyin)
|
||||
* - 2026-01-08: 文件夹扁平化 - 从dto/子文件夹移动到上级目录 (修改者: moyin)
|
||||
* - 2026-01-08: 功能新增 - 创建管理员数据库管理DTO (修改者: assistant)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.3
|
||||
* @since 2026-01-08
|
||||
* @lastModified 2026-01-08
|
||||
*/
|
||||
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsOptional, IsString, IsInt, Min, Max, IsEnum, IsEmail, IsArray, IsBoolean, IsNumber } from 'class-validator';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { UserStatus } from '../../core/db/users/user_status.enum';
|
||||
|
||||
// ==================== 通用查询 DTOs ====================
|
||||
|
||||
/**
|
||||
* 管理员分页查询DTO
|
||||
*
|
||||
* 功能描述:
|
||||
* 定义分页查询的通用参数结构
|
||||
*
|
||||
* 使用场景:
|
||||
* - 作为其他查询DTO的基类
|
||||
* - 提供统一的分页参数验证
|
||||
*/
|
||||
export class AdminPaginationDto {
|
||||
@ApiPropertyOptional({ description: '返回数量(默认20,最大100)', example: 20, minimum: 1, maximum: 100 })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(100)
|
||||
@Transform(({ value }) => parseInt(value))
|
||||
limit?: number = 20;
|
||||
|
||||
@ApiPropertyOptional({ description: '偏移量(默认0)', example: 0, minimum: 0 })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@Transform(({ value }) => parseInt(value))
|
||||
offset?: number = 0;
|
||||
}
|
||||
|
||||
// ==================== 用户管理 DTOs ====================
|
||||
|
||||
/**
|
||||
* 管理员查询用户DTO
|
||||
*
|
||||
* 功能描述:
|
||||
* 定义用户查询接口的请求参数结构
|
||||
*
|
||||
* 使用场景:
|
||||
* - GET /admin/database/users 接口的查询参数
|
||||
* - 支持关键词搜索和分页查询
|
||||
*/
|
||||
export class AdminQueryUsersDto extends AdminPaginationDto {
|
||||
@ApiPropertyOptional({ description: '搜索关键词(用户名、邮箱、昵称)', example: 'admin' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
search?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '用户状态过滤', enum: UserStatus, example: UserStatus.ACTIVE })
|
||||
@IsOptional()
|
||||
@IsEnum(UserStatus)
|
||||
status?: UserStatus;
|
||||
|
||||
@ApiPropertyOptional({ description: '角色过滤', example: 1 })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@Max(9)
|
||||
role?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员创建用户DTO
|
||||
*
|
||||
* 功能描述:
|
||||
* 定义创建用户接口的请求数据结构和验证规则
|
||||
*
|
||||
* 使用场景:
|
||||
* - POST /admin/database/users 接口的请求体
|
||||
* - 包含用户创建所需的所有必要信息
|
||||
*/
|
||||
export class AdminCreateUserDto {
|
||||
@ApiProperty({ description: '用户名', example: 'newuser' })
|
||||
@IsString()
|
||||
username: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '邮箱', example: 'user@example.com' })
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
email?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '手机号', example: '13800138000' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@ApiProperty({ description: '昵称', example: '新用户' })
|
||||
@IsString()
|
||||
nickname: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '密码哈希', example: 'hashed_password' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
password_hash?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'GitHub ID', example: 'github123' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
github_id?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '头像URL', example: 'https://example.com/avatar.jpg' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
avatar_url?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '角色', example: 1, minimum: 0, maximum: 9 })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@Max(9)
|
||||
role?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '邮箱是否已验证', example: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
email_verified?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ description: '用户状态', enum: UserStatus, example: UserStatus.ACTIVE })
|
||||
@IsOptional()
|
||||
@IsEnum(UserStatus)
|
||||
status?: UserStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员更新用户DTO
|
||||
*
|
||||
* 功能描述:
|
||||
* 定义更新用户接口的请求数据结构和验证规则
|
||||
*
|
||||
* 使用场景:
|
||||
* - PUT /admin/database/users/:id 接口的请求体
|
||||
* - 支持部分字段更新,所有字段都是可选的
|
||||
*/
|
||||
export class AdminUpdateUserDto {
|
||||
@ApiPropertyOptional({ description: '用户名', example: 'updateduser' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
username?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '邮箱', example: 'updated@example.com' })
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
email?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '手机号', example: '13900139000' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '昵称', example: '更新用户' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
nickname?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '头像URL', example: 'https://example.com/new-avatar.jpg' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
avatar_url?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '角色', example: 2, minimum: 0, maximum: 9 })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@Max(9)
|
||||
role?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '邮箱是否已验证', example: true })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
email_verified?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ description: '用户状态', enum: UserStatus, example: UserStatus.INACTIVE })
|
||||
@IsOptional()
|
||||
@IsEnum(UserStatus)
|
||||
status?: UserStatus;
|
||||
}
|
||||
|
||||
// ==================== 用户档案管理 DTOs ====================
|
||||
|
||||
/**
|
||||
* 管理员查询用户档案DTO
|
||||
*
|
||||
* 功能描述:
|
||||
* 定义用户档案查询接口的请求参数结构
|
||||
*
|
||||
* 使用场景:
|
||||
* - GET /admin/database/user-profiles 接口的查询参数
|
||||
* - 支持地图过滤和分页查询
|
||||
*/
|
||||
export class AdminQueryUserProfileDto extends AdminPaginationDto {
|
||||
@ApiPropertyOptional({ description: '当前地图过滤', example: 'plaza' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
current_map?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '状态过滤', example: 1 })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
status?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '用户ID过滤', example: '1' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
user_id?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员创建用户档案DTO
|
||||
*
|
||||
* 功能描述:
|
||||
* 定义创建用户档案接口的请求数据结构和验证规则
|
||||
*
|
||||
* 使用场景:
|
||||
* - POST /admin/database/user-profiles 接口的请求体
|
||||
* - 包含用户档案创建所需的所有信息
|
||||
*/
|
||||
export class AdminCreateUserProfileDto {
|
||||
@ApiProperty({ description: '用户ID', example: '1' })
|
||||
@IsString()
|
||||
user_id: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '个人简介', example: '这是我的个人简介' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
bio?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '简历内容', example: '工作经历和技能' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
resume_content?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '标签', example: '["开发者", "游戏爱好者"]' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
tags?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '社交链接', example: '{"github": "https://github.com/user"}' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
social_links?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '皮肤ID', example: 'skin_001' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
skin_id?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '当前地图', example: 'plaza' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
current_map?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'X坐标', example: 100.5 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
pos_x?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Y坐标', example: 200.3 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
pos_y?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '状态', example: 1 })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
status?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员更新用户档案DTO
|
||||
*
|
||||
* 功能描述:
|
||||
* 定义更新用户档案接口的请求数据结构和验证规则
|
||||
*
|
||||
* 使用场景:
|
||||
* - PUT /admin/database/user-profiles/:id 接口的请求体
|
||||
* - 支持部分字段更新,所有字段都是可选的
|
||||
*/
|
||||
export class AdminUpdateUserProfileDto {
|
||||
@ApiPropertyOptional({ description: '个人简介', example: '更新后的个人简介' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
bio?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '简历内容', example: '更新后的简历内容' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
resume_content?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '标签', example: '["高级开发者", "技术专家"]' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
tags?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '社交链接', example: '{"linkedin": "https://linkedin.com/in/user"}' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
social_links?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '皮肤ID', example: 'skin_002' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
skin_id?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '当前地图', example: 'forest' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
current_map?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'X坐标', example: 150.7 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
pos_x?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Y坐标', example: 250.9 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
pos_y?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '状态', example: 0 })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
status?: number;
|
||||
}
|
||||
|
||||
// ==================== Zulip账号关联管理 DTOs ====================
|
||||
|
||||
/**
|
||||
* 管理员查询Zulip账号DTO
|
||||
*
|
||||
* 功能描述:
|
||||
* 定义Zulip账号关联查询接口的请求参数结构
|
||||
*
|
||||
* 使用场景:
|
||||
* - GET /admin/database/zulip-accounts 接口的查询参数
|
||||
* - 支持用户ID过滤和分页查询
|
||||
*/
|
||||
export class AdminQueryZulipAccountDto extends AdminPaginationDto {
|
||||
@ApiPropertyOptional({ description: '游戏用户ID过滤', example: '1' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
gameUserId?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Zulip用户ID过滤', example: 12345 })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
zulipUserId?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Zulip邮箱过滤', example: 'user@zulip.com' })
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
zulipEmail?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '状态过滤', example: 'active', enum: ['active', 'inactive', 'suspended', 'error'] })
|
||||
@IsOptional()
|
||||
@IsEnum(['active', 'inactive', 'suspended', 'error'])
|
||||
status?: 'active' | 'inactive' | 'suspended' | 'error';
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员创建Zulip账号DTO
|
||||
*
|
||||
* 功能描述:
|
||||
* 定义创建Zulip账号关联接口的请求数据结构和验证规则
|
||||
*
|
||||
* 使用场景:
|
||||
* - POST /admin/database/zulip-accounts 接口的请求体
|
||||
* - 包含Zulip账号关联创建所需的所有信息
|
||||
*/
|
||||
export class AdminCreateZulipAccountDto {
|
||||
@ApiProperty({ description: '游戏用户ID', example: '1' })
|
||||
@IsString()
|
||||
gameUserId: string;
|
||||
|
||||
@ApiProperty({ description: 'Zulip用户ID', example: 12345 })
|
||||
@IsInt()
|
||||
zulipUserId: number;
|
||||
|
||||
@ApiProperty({ description: 'Zulip邮箱', example: 'user@zulip.com' })
|
||||
@IsEmail()
|
||||
zulipEmail: string;
|
||||
|
||||
@ApiProperty({ description: 'Zulip全名', example: '张三' })
|
||||
@IsString()
|
||||
zulipFullName: string;
|
||||
|
||||
@ApiProperty({ description: 'Zulip API密钥(加密)', example: 'encrypted_api_key' })
|
||||
@IsString()
|
||||
zulipApiKeyEncrypted: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '状态', example: 'active', enum: ['active', 'inactive', 'suspended', 'error'] })
|
||||
@IsOptional()
|
||||
@IsEnum(['active', 'inactive', 'suspended', 'error'])
|
||||
status?: 'active' | 'inactive' | 'suspended' | 'error';
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员更新Zulip账号DTO
|
||||
*
|
||||
* 功能描述:
|
||||
* 定义更新Zulip账号关联接口的请求数据结构和验证规则
|
||||
*
|
||||
* 使用场景:
|
||||
* - PUT /admin/database/zulip-accounts/:id 接口的请求体
|
||||
* - 支持部分字段更新,所有字段都是可选的
|
||||
*/
|
||||
export class AdminUpdateZulipAccountDto {
|
||||
@ApiPropertyOptional({ description: 'Zulip全名', example: '李四' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
zulipFullName?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Zulip API密钥(加密)', example: 'new_encrypted_api_key' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
zulipApiKeyEncrypted?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '状态', example: 'suspended', enum: ['active', 'inactive', 'suspended', 'error'] })
|
||||
@IsOptional()
|
||||
@IsEnum(['active', 'inactive', 'suspended', 'error'])
|
||||
status?: 'active' | 'inactive' | 'suspended' | 'error';
|
||||
|
||||
@ApiPropertyOptional({ description: '错误信息', example: '连接超时' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
errorMessage?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '重试次数', example: 3 })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
retryCount?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员批量更新状态DTO
|
||||
*
|
||||
* 功能描述:
|
||||
* 定义批量更新状态接口的请求数据结构和验证规则
|
||||
*
|
||||
* 使用场景:
|
||||
* - POST /admin/database/zulip-accounts/batch-update-status 接口的请求体
|
||||
* - 支持批量更新多个记录的状态
|
||||
*/
|
||||
export class AdminBatchUpdateStatusDto {
|
||||
@ApiProperty({ description: 'ID列表', example: ['1', '2', '3'] })
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
ids: string[];
|
||||
|
||||
@ApiProperty({ description: '目标状态', example: 'active', enum: ['active', 'inactive', 'suspended', 'error'] })
|
||||
@IsEnum(['active', 'inactive', 'suspended', 'error'])
|
||||
status: 'active' | 'inactive' | 'suspended' | 'error';
|
||||
|
||||
@ApiPropertyOptional({ description: '操作原因', example: '批量激活账号' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
// ==================== 响应 DTOs ====================
|
||||
|
||||
/**
|
||||
* 管理员数据库响应DTO
|
||||
*
|
||||
* 功能描述:
|
||||
* 定义管理员数据库操作的通用响应数据结构
|
||||
*
|
||||
* 使用场景:
|
||||
* - 各种数据库管理接口的响应体基类
|
||||
* - 包含操作状态、数据和消息信息
|
||||
*/
|
||||
export class AdminDatabaseResponseDto {
|
||||
@ApiProperty({ description: '是否成功', example: true })
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({ description: '消息', example: '操作成功' })
|
||||
message: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '数据' })
|
||||
data?: any;
|
||||
|
||||
@ApiPropertyOptional({ description: '错误码', example: 'RESOURCE_NOT_FOUND' })
|
||||
error_code?: string;
|
||||
|
||||
@ApiProperty({ description: '时间戳', example: '2026-01-08T10:30:00.000Z' })
|
||||
timestamp: string;
|
||||
|
||||
@ApiProperty({ description: '请求ID', example: 'req_1641636600000_abc123' })
|
||||
request_id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员数据库列表响应DTO
|
||||
*
|
||||
* 功能描述:
|
||||
* 定义管理员数据库列表查询的响应数据结构
|
||||
*
|
||||
* 使用场景:
|
||||
* - 各种列表查询接口的响应体
|
||||
* - 包含列表数据和分页信息
|
||||
*/
|
||||
export class AdminDatabaseListResponseDto extends AdminDatabaseResponseDto {
|
||||
@ApiProperty({ description: '列表数据' })
|
||||
data: {
|
||||
items: any[];
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
has_more: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员健康检查响应DTO
|
||||
*
|
||||
* 功能描述:
|
||||
* 定义系统健康检查接口的响应数据结构
|
||||
*
|
||||
* 使用场景:
|
||||
* - GET /admin/database/health 接口的响应体
|
||||
* - 包含系统健康状态信息
|
||||
*/
|
||||
export class AdminHealthCheckResponseDto extends AdminDatabaseResponseDto {
|
||||
@ApiProperty({ description: '健康检查数据' })
|
||||
data: {
|
||||
status: string;
|
||||
timestamp: string;
|
||||
services: {
|
||||
users: string;
|
||||
user_profiles: string;
|
||||
zulip_accounts: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
435
src/business/admin/admin_database.integration.spec.ts
Normal file
435
src/business/admin/admin_database.integration.spec.ts
Normal file
@@ -0,0 +1,435 @@
|
||||
/**
|
||||
* 管理员数据库管理集成测试
|
||||
*
|
||||
* 功能描述:
|
||||
* - 测试管理员数据库管理的完整功能
|
||||
* - 验证CRUD操作的正确性
|
||||
* - 测试权限控制和错误处理
|
||||
* - 验证响应格式的一致性
|
||||
*
|
||||
* 测试覆盖:
|
||||
* - 用户管理功能测试
|
||||
* - 用户档案管理功能测试
|
||||
* - Zulip账号关联管理功能测试
|
||||
* - 批量操作功能测试
|
||||
* - 错误处理和边界条件测试
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-08: 注释规范优化 - 修正@author字段,更新版本号和修改记录 (修改者: moyin)
|
||||
* - 2026-01-08: 功能新增 - 创建管理员数据库管理集成测试 (修改者: assistant)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @since 2026-01-08
|
||||
* @lastModified 2026-01-08
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AdminDatabaseController } from './admin_database.controller';
|
||||
import { DatabaseManagementService } from './database_management.service';
|
||||
import { AdminOperationLogService } from './admin_operation_log.service';
|
||||
import { AdminOperationLogInterceptor } from './admin_operation_log.interceptor';
|
||||
import { AdminDatabaseExceptionFilter } from './admin_database_exception.filter';
|
||||
import { AdminGuard } from './admin.guard';
|
||||
import { UserStatus } from '../user_mgmt/user_status.enum';
|
||||
|
||||
describe('Admin Database Management Integration Tests', () => {
|
||||
let app: INestApplication;
|
||||
let module: TestingModule;
|
||||
let controller: AdminDatabaseController;
|
||||
let service: DatabaseManagementService;
|
||||
|
||||
// 测试数据
|
||||
const testUser = {
|
||||
username: 'admin-test-user',
|
||||
nickname: '管理员测试用户',
|
||||
email: 'admin-test@example.com',
|
||||
role: 1,
|
||||
status: UserStatus.ACTIVE
|
||||
};
|
||||
|
||||
const testProfile = {
|
||||
user_id: '1',
|
||||
bio: '管理员测试档案',
|
||||
current_map: 'test-plaza',
|
||||
pos_x: 100.5,
|
||||
pos_y: 200.3,
|
||||
status: 1
|
||||
};
|
||||
|
||||
const testZulipAccount = {
|
||||
gameUserId: '1',
|
||||
zulipUserId: 12345,
|
||||
zulipEmail: 'test@zulip.com',
|
||||
zulipFullName: '测试用户',
|
||||
zulipApiKeyEncrypted: 'encrypted_test_key',
|
||||
status: 'active' as const
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
module = await Test.createTestingModule({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
envFilePath: ['.env.test', '.env']
|
||||
})
|
||||
],
|
||||
controllers: [AdminDatabaseController],
|
||||
providers: [
|
||||
DatabaseManagementService,
|
||||
// Mock AdminOperationLogService for testing
|
||||
{
|
||||
provide: AdminOperationLogService,
|
||||
useValue: {
|
||||
createLog: jest.fn().mockResolvedValue({}),
|
||||
queryLogs: jest.fn().mockResolvedValue({ logs: [], total: 0 }),
|
||||
getLogById: jest.fn().mockResolvedValue(null),
|
||||
getStatistics: jest.fn().mockResolvedValue({}),
|
||||
cleanupExpiredLogs: jest.fn().mockResolvedValue(0),
|
||||
getAdminOperationHistory: jest.fn().mockResolvedValue([]),
|
||||
getSensitiveOperations: jest.fn().mockResolvedValue({ logs: [], total: 0 })
|
||||
}
|
||||
},
|
||||
// Mock AdminOperationLogInterceptor
|
||||
{
|
||||
provide: AdminOperationLogInterceptor,
|
||||
useValue: {
|
||||
intercept: jest.fn().mockImplementation((context, next) => next.handle())
|
||||
}
|
||||
},
|
||||
{
|
||||
provide: 'UsersService',
|
||||
useValue: {
|
||||
findAll: jest.fn().mockResolvedValue([]),
|
||||
findOne: jest.fn().mockResolvedValue({ ...testUser, id: BigInt(1) }),
|
||||
create: jest.fn().mockResolvedValue({ ...testUser, id: BigInt(1) }),
|
||||
update: jest.fn().mockResolvedValue({ ...testUser, id: BigInt(1) }),
|
||||
remove: jest.fn().mockResolvedValue(undefined),
|
||||
search: jest.fn().mockResolvedValue([]),
|
||||
count: jest.fn().mockResolvedValue(0)
|
||||
}
|
||||
},
|
||||
{
|
||||
provide: 'IUserProfilesService',
|
||||
useValue: {
|
||||
findAll: jest.fn().mockResolvedValue([]),
|
||||
findOne: jest.fn().mockResolvedValue({ ...testProfile, id: BigInt(1) }),
|
||||
create: jest.fn().mockResolvedValue({ ...testProfile, id: BigInt(1) }),
|
||||
update: jest.fn().mockResolvedValue({ ...testProfile, id: BigInt(1) }),
|
||||
remove: jest.fn().mockResolvedValue(undefined),
|
||||
findByMap: jest.fn().mockResolvedValue([]),
|
||||
count: jest.fn().mockResolvedValue(0)
|
||||
}
|
||||
},
|
||||
{
|
||||
provide: 'ZulipAccountsService',
|
||||
useValue: {
|
||||
findMany: jest.fn().mockResolvedValue({ accounts: [] }),
|
||||
findById: jest.fn().mockResolvedValue(testZulipAccount),
|
||||
create: jest.fn().mockResolvedValue({ ...testZulipAccount, id: '1' }),
|
||||
update: jest.fn().mockResolvedValue({ ...testZulipAccount, id: '1' }),
|
||||
delete: jest.fn().mockResolvedValue(undefined),
|
||||
getStatusStatistics: jest.fn().mockResolvedValue({
|
||||
active: 0,
|
||||
inactive: 0,
|
||||
suspended: 0,
|
||||
error: 0,
|
||||
total: 0
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
.overrideGuard(AdminGuard)
|
||||
.useValue({ canActivate: () => true })
|
||||
.compile();
|
||||
|
||||
app = module.createNestApplication();
|
||||
app.useGlobalFilters(new AdminDatabaseExceptionFilter());
|
||||
await app.init();
|
||||
|
||||
controller = module.get<AdminDatabaseController>(AdminDatabaseController);
|
||||
service = module.get<DatabaseManagementService>(DatabaseManagementService);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
describe('用户管理功能测试', () => {
|
||||
it('应该成功获取用户列表', async () => {
|
||||
const result = await controller.getUserList(20, 0);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toBeDefined();
|
||||
expect(result.data.items).toBeInstanceOf(Array);
|
||||
expect(result.data.total).toBeDefined();
|
||||
expect(result.data.limit).toBe(20);
|
||||
expect(result.data.offset).toBe(0);
|
||||
expect(result.message).toBe('用户列表获取成功');
|
||||
});
|
||||
|
||||
it('应该成功获取用户详情', async () => {
|
||||
const result = await controller.getUserById('1');
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toBeDefined();
|
||||
expect(result.data.username).toBe(testUser.username);
|
||||
expect(result.message).toBe('用户详情获取成功');
|
||||
});
|
||||
|
||||
it('应该成功创建用户', async () => {
|
||||
const result = await controller.createUser(testUser);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toBeDefined();
|
||||
expect(result.data.username).toBe(testUser.username);
|
||||
expect(result.message).toBe('用户创建成功');
|
||||
});
|
||||
|
||||
it('应该成功更新用户', async () => {
|
||||
const updateData = { nickname: '更新后的昵称' };
|
||||
const result = await controller.updateUser('1', updateData);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toBeDefined();
|
||||
expect(result.message).toBe('用户更新成功');
|
||||
});
|
||||
|
||||
it('应该成功删除用户', async () => {
|
||||
const result = await controller.deleteUser('1');
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data.deleted).toBe(true);
|
||||
expect(result.message).toBe('用户删除成功');
|
||||
});
|
||||
|
||||
it('应该成功搜索用户', async () => {
|
||||
const result = await controller.searchUsers('admin', 20);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toBeDefined();
|
||||
expect(result.data.items).toBeInstanceOf(Array);
|
||||
expect(result.message).toBe('用户搜索成功');
|
||||
});
|
||||
});
|
||||
|
||||
describe('用户档案管理功能测试', () => {
|
||||
it('应该成功获取用户档案列表', async () => {
|
||||
const result = await controller.getUserProfileList(20, 0);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toBeDefined();
|
||||
expect(result.data.items).toBeInstanceOf(Array);
|
||||
expect(result.message).toBe('用户档案列表获取成功');
|
||||
});
|
||||
|
||||
it('应该成功获取用户档案详情', async () => {
|
||||
const result = await controller.getUserProfileById('1');
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toBeDefined();
|
||||
expect(result.data.user_id).toBe(testProfile.user_id);
|
||||
expect(result.message).toBe('用户档案详情获取成功');
|
||||
});
|
||||
|
||||
it('应该成功创建用户档案', async () => {
|
||||
const result = await controller.createUserProfile(testProfile);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toBeDefined();
|
||||
expect(result.data.user_id).toBe(testProfile.user_id);
|
||||
expect(result.message).toBe('用户档案创建成功');
|
||||
});
|
||||
|
||||
it('应该成功更新用户档案', async () => {
|
||||
const updateData = { bio: '更新后的简介' };
|
||||
const result = await controller.updateUserProfile('1', updateData);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toBeDefined();
|
||||
expect(result.message).toBe('用户档案更新成功');
|
||||
});
|
||||
|
||||
it('应该成功删除用户档案', async () => {
|
||||
const result = await controller.deleteUserProfile('1');
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data.deleted).toBe(true);
|
||||
expect(result.message).toBe('用户档案删除成功');
|
||||
});
|
||||
|
||||
it('应该成功根据地图获取用户档案', async () => {
|
||||
const result = await controller.getUserProfilesByMap('plaza', 20, 0);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toBeDefined();
|
||||
expect(result.data.items).toBeInstanceOf(Array);
|
||||
expect(result.message).toBe('地图 plaza 的用户档案获取成功');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Zulip账号关联管理功能测试', () => {
|
||||
it('应该成功获取Zulip账号关联列表', async () => {
|
||||
const result = await controller.getZulipAccountList(20, 0);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toBeDefined();
|
||||
expect(result.data.items).toBeInstanceOf(Array);
|
||||
expect(result.message).toBe('Zulip账号关联列表获取成功');
|
||||
});
|
||||
|
||||
it('应该成功获取Zulip账号关联详情', async () => {
|
||||
const result = await controller.getZulipAccountById('1');
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toBeDefined();
|
||||
expect(result.data.gameUserId).toBe(testZulipAccount.gameUserId);
|
||||
expect(result.message).toBe('Zulip账号关联详情获取成功');
|
||||
});
|
||||
|
||||
it('应该成功创建Zulip账号关联', async () => {
|
||||
const result = await controller.createZulipAccount(testZulipAccount);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toBeDefined();
|
||||
expect(result.data.gameUserId).toBe(testZulipAccount.gameUserId);
|
||||
expect(result.message).toBe('Zulip账号关联创建成功');
|
||||
});
|
||||
|
||||
it('应该成功更新Zulip账号关联', async () => {
|
||||
const updateData = { status: 'inactive' as const };
|
||||
const result = await controller.updateZulipAccount('1', updateData);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toBeDefined();
|
||||
expect(result.message).toBe('Zulip账号关联更新成功');
|
||||
});
|
||||
|
||||
it('应该成功删除Zulip账号关联', async () => {
|
||||
const result = await controller.deleteZulipAccount('1');
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data.deleted).toBe(true);
|
||||
expect(result.message).toBe('Zulip账号关联删除成功');
|
||||
});
|
||||
|
||||
it('应该成功批量更新Zulip账号状态', async () => {
|
||||
const batchData = {
|
||||
ids: ['1', '2', '3'],
|
||||
status: 'active' as 'active' | 'inactive' | 'suspended' | 'error',
|
||||
reason: '批量激活测试'
|
||||
};
|
||||
const result = await controller.batchUpdateZulipAccountStatus(batchData);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toBeDefined();
|
||||
expect(result.data.total).toBe(3);
|
||||
expect(result.message).toContain('批量更新完成');
|
||||
});
|
||||
|
||||
it('应该成功获取Zulip账号关联统计', async () => {
|
||||
const result = await controller.getZulipAccountStatistics();
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toBeDefined();
|
||||
expect(result.data.total).toBeDefined();
|
||||
expect(result.message).toBe('Zulip账号关联统计获取成功');
|
||||
});
|
||||
});
|
||||
|
||||
describe('系统功能测试', () => {
|
||||
it('应该成功进行健康检查', async () => {
|
||||
const result = await controller.healthCheck();
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toBeDefined();
|
||||
expect(result.data.status).toBe('healthy');
|
||||
expect(result.data.services).toBeDefined();
|
||||
expect(result.message).toBe('数据库管理系统运行正常');
|
||||
});
|
||||
});
|
||||
|
||||
describe('响应格式一致性测试', () => {
|
||||
it('所有成功响应应该有统一的格式', async () => {
|
||||
const responses = [
|
||||
await controller.getUserList(20, 0),
|
||||
await controller.getUserById('1'),
|
||||
await controller.getUserProfileList(20, 0),
|
||||
await controller.getZulipAccountList(20, 0),
|
||||
await controller.healthCheck()
|
||||
];
|
||||
|
||||
responses.forEach(response => {
|
||||
expect(response).toHaveProperty('success');
|
||||
expect(response).toHaveProperty('message');
|
||||
expect(response).toHaveProperty('data');
|
||||
expect(response).toHaveProperty('timestamp');
|
||||
expect(response).toHaveProperty('request_id');
|
||||
expect(response.success).toBe(true);
|
||||
expect(typeof response.message).toBe('string');
|
||||
expect(typeof response.timestamp).toBe('string');
|
||||
expect(typeof response.request_id).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
it('列表响应应该有分页信息', async () => {
|
||||
const listResponses = [
|
||||
await controller.getUserList(20, 0),
|
||||
await controller.getUserProfileList(20, 0),
|
||||
await controller.getZulipAccountList(20, 0)
|
||||
];
|
||||
|
||||
listResponses.forEach(response => {
|
||||
expect(response.data).toHaveProperty('items');
|
||||
expect(response.data).toHaveProperty('total');
|
||||
expect(response.data).toHaveProperty('limit');
|
||||
expect(response.data).toHaveProperty('offset');
|
||||
expect(response.data).toHaveProperty('has_more');
|
||||
expect(Array.isArray(response.data.items)).toBe(true);
|
||||
expect(typeof response.data.total).toBe('number');
|
||||
expect(typeof response.data.limit).toBe('number');
|
||||
expect(typeof response.data.offset).toBe('number');
|
||||
expect(typeof response.data.has_more).toBe('boolean');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('参数验证测试', () => {
|
||||
it('应该正确处理分页参数限制', async () => {
|
||||
// 测试超过最大限制的情况
|
||||
const result = await controller.getUserList(200, 0);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('应该正确处理搜索参数限制', async () => {
|
||||
const result = await controller.searchUsers('test', 100);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
351
src/business/admin/admin_database_exception.filter.spec.ts
Normal file
351
src/business/admin/admin_database_exception.filter.spec.ts
Normal file
@@ -0,0 +1,351 @@
|
||||
/**
|
||||
* AdminDatabaseExceptionFilter 单元测试
|
||||
*
|
||||
* 功能描述:
|
||||
* - 测试管理员数据库异常过滤器的所有功能
|
||||
* - 验证异常处理和错误响应格式化的正确性
|
||||
* - 测试各种异常类型的处理
|
||||
*
|
||||
* 职责分离:
|
||||
* - 异常过滤器逻辑测试,不涉及具体业务
|
||||
* - Mock HTTP上下文,专注过滤器功能
|
||||
* - 验证错误响应的格式和内容
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-09: 功能新增 - 创建AdminDatabaseExceptionFilter单元测试 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-09
|
||||
* @lastModified 2026-01-09
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ArgumentsHost, HttpStatus, HttpException } from '@nestjs/common';
|
||||
import {
|
||||
BadRequestException,
|
||||
UnauthorizedException,
|
||||
ForbiddenException,
|
||||
NotFoundException,
|
||||
ConflictException,
|
||||
UnprocessableEntityException,
|
||||
InternalServerErrorException,
|
||||
} from '@nestjs/common';
|
||||
import { AdminDatabaseExceptionFilter } from './admin_database_exception.filter';
|
||||
|
||||
describe('AdminDatabaseExceptionFilter', () => {
|
||||
let filter: AdminDatabaseExceptionFilter;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [AdminDatabaseExceptionFilter],
|
||||
}).compile();
|
||||
|
||||
filter = module.get<AdminDatabaseExceptionFilter>(AdminDatabaseExceptionFilter);
|
||||
});
|
||||
|
||||
const createMockArgumentsHost = (requestData: any = {}) => {
|
||||
const mockRequest = {
|
||||
method: 'POST',
|
||||
url: '/admin/database/users',
|
||||
ip: '127.0.0.1',
|
||||
get: jest.fn().mockReturnValue('test-user-agent'),
|
||||
body: { username: 'testuser' },
|
||||
query: { limit: '10' },
|
||||
...requestData,
|
||||
};
|
||||
|
||||
const mockResponse = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn(),
|
||||
};
|
||||
|
||||
const mockHost = {
|
||||
switchToHttp: () => ({
|
||||
getRequest: () => mockRequest,
|
||||
getResponse: () => mockResponse,
|
||||
}),
|
||||
} as ArgumentsHost;
|
||||
|
||||
return { mockHost, mockRequest, mockResponse };
|
||||
};
|
||||
|
||||
describe('catch', () => {
|
||||
it('should handle BadRequestException', () => {
|
||||
const { mockHost, mockResponse } = createMockArgumentsHost();
|
||||
const exception = new BadRequestException('Invalid input data');
|
||||
|
||||
filter.catch(exception, mockHost);
|
||||
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.BAD_REQUEST);
|
||||
expect(mockResponse.json).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
success: false,
|
||||
message: 'Invalid input data',
|
||||
error_code: 'BAD_REQUEST',
|
||||
path: '/admin/database/users',
|
||||
method: 'POST',
|
||||
timestamp: expect.any(String),
|
||||
request_id: expect.any(String),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle UnauthorizedException', () => {
|
||||
const { mockHost, mockResponse } = createMockArgumentsHost();
|
||||
const exception = new UnauthorizedException('Access denied');
|
||||
|
||||
filter.catch(exception, mockHost);
|
||||
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.UNAUTHORIZED);
|
||||
expect(mockResponse.json).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
success: false,
|
||||
message: 'Access denied',
|
||||
error_code: 'UNAUTHORIZED',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle ForbiddenException', () => {
|
||||
const { mockHost, mockResponse } = createMockArgumentsHost();
|
||||
const exception = new ForbiddenException('Insufficient permissions');
|
||||
|
||||
filter.catch(exception, mockHost);
|
||||
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.FORBIDDEN);
|
||||
expect(mockResponse.json).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
success: false,
|
||||
message: 'Insufficient permissions',
|
||||
error_code: 'FORBIDDEN',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle NotFoundException', () => {
|
||||
const { mockHost, mockResponse } = createMockArgumentsHost();
|
||||
const exception = new NotFoundException('User not found');
|
||||
|
||||
filter.catch(exception, mockHost);
|
||||
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.NOT_FOUND);
|
||||
expect(mockResponse.json).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
success: false,
|
||||
message: 'User not found',
|
||||
error_code: 'NOT_FOUND',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle ConflictException', () => {
|
||||
const { mockHost, mockResponse } = createMockArgumentsHost();
|
||||
const exception = new ConflictException('Username already exists');
|
||||
|
||||
filter.catch(exception, mockHost);
|
||||
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.CONFLICT);
|
||||
expect(mockResponse.json).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
success: false,
|
||||
message: 'Username already exists',
|
||||
error_code: 'CONFLICT',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle UnprocessableEntityException', () => {
|
||||
const { mockHost, mockResponse } = createMockArgumentsHost();
|
||||
const exception = new UnprocessableEntityException('Validation failed');
|
||||
|
||||
filter.catch(exception, mockHost);
|
||||
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.UNPROCESSABLE_ENTITY);
|
||||
expect(mockResponse.json).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
success: false,
|
||||
message: 'Validation failed',
|
||||
error_code: 'UNPROCESSABLE_ENTITY',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle InternalServerErrorException', () => {
|
||||
const { mockHost, mockResponse } = createMockArgumentsHost();
|
||||
const exception = new InternalServerErrorException('Database connection failed');
|
||||
|
||||
filter.catch(exception, mockHost);
|
||||
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
expect(mockResponse.json).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
success: false,
|
||||
message: 'Database connection failed',
|
||||
error_code: 'INTERNAL_SERVER_ERROR',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle unknown exceptions', () => {
|
||||
const { mockHost, mockResponse } = createMockArgumentsHost();
|
||||
const exception = new Error('Unknown error');
|
||||
|
||||
filter.catch(exception, mockHost);
|
||||
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
expect(mockResponse.json).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
success: false,
|
||||
message: '系统内部错误,请稍后重试',
|
||||
error_code: 'INTERNAL_SERVER_ERROR',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle exception with object response', () => {
|
||||
const { mockHost, mockResponse } = createMockArgumentsHost();
|
||||
const exception = new BadRequestException({
|
||||
message: 'Validation error',
|
||||
details: [
|
||||
{ field: 'username', constraint: 'minLength', received_value: 'ab' }
|
||||
]
|
||||
});
|
||||
|
||||
filter.catch(exception, mockHost);
|
||||
|
||||
expect(mockResponse.json).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
success: false,
|
||||
message: 'Validation error',
|
||||
error_code: 'BAD_REQUEST',
|
||||
details: [
|
||||
{ field: 'username', constraint: 'minLength', received_value: 'ab' }
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle exception with nested error message', () => {
|
||||
const { mockHost, mockResponse } = createMockArgumentsHost();
|
||||
const exception = new BadRequestException({
|
||||
error: 'Custom error message'
|
||||
});
|
||||
|
||||
filter.catch(exception, mockHost);
|
||||
|
||||
expect(mockResponse.json).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: 'Custom error message',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should sanitize sensitive fields in request body', () => {
|
||||
const { mockHost, mockResponse } = createMockArgumentsHost({
|
||||
body: {
|
||||
username: 'testuser',
|
||||
password: 'secret123',
|
||||
api_key: 'sensitive-key'
|
||||
}
|
||||
});
|
||||
const exception = new BadRequestException('Invalid data');
|
||||
|
||||
filter.catch(exception, mockHost);
|
||||
|
||||
// 验证响应被正确处理(敏感字段在日志中被清理,但不影响响应)
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.BAD_REQUEST);
|
||||
expect(mockResponse.json).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
success: false,
|
||||
message: 'Invalid data',
|
||||
error_code: 'BAD_REQUEST',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle missing user agent', () => {
|
||||
const { mockHost, mockResponse } = createMockArgumentsHost();
|
||||
mockHost.switchToHttp().getRequest().get = jest.fn().mockReturnValue(undefined);
|
||||
|
||||
const exception = new BadRequestException('Test error');
|
||||
|
||||
filter.catch(exception, mockHost);
|
||||
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.BAD_REQUEST);
|
||||
expect(mockResponse.json).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
success: false,
|
||||
message: 'Test error',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle exception with string response', () => {
|
||||
const { mockHost, mockResponse } = createMockArgumentsHost();
|
||||
const exception = new BadRequestException('Simple string error');
|
||||
|
||||
filter.catch(exception, mockHost);
|
||||
|
||||
expect(mockResponse.json).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: 'Simple string error',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should generate unique request IDs', () => {
|
||||
const { mockHost, mockResponse } = createMockArgumentsHost();
|
||||
const exception1 = new BadRequestException('Error 1');
|
||||
const exception2 = new BadRequestException('Error 2');
|
||||
|
||||
filter.catch(exception1, mockHost);
|
||||
const firstCall = mockResponse.json.mock.calls[0][0];
|
||||
|
||||
mockResponse.json.mockClear();
|
||||
filter.catch(exception2, mockHost);
|
||||
const secondCall = mockResponse.json.mock.calls[0][0];
|
||||
|
||||
expect(firstCall.request_id).toBeDefined();
|
||||
expect(secondCall.request_id).toBeDefined();
|
||||
expect(firstCall.request_id).not.toBe(secondCall.request_id);
|
||||
});
|
||||
|
||||
it('should include timestamp in response', () => {
|
||||
const { mockHost, mockResponse } = createMockArgumentsHost();
|
||||
const exception = new BadRequestException('Test error');
|
||||
|
||||
const beforeTime = new Date().toISOString();
|
||||
filter.catch(exception, mockHost);
|
||||
const afterTime = new Date().toISOString();
|
||||
|
||||
const response = mockResponse.json.mock.calls[0][0];
|
||||
expect(response.timestamp).toBeDefined();
|
||||
expect(response.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
|
||||
expect(response.timestamp >= beforeTime).toBe(true);
|
||||
expect(response.timestamp <= afterTime).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle different HTTP status codes', () => {
|
||||
const { mockHost, mockResponse } = createMockArgumentsHost();
|
||||
|
||||
// 创建一个继承自HttpException的异常,模拟429状态码
|
||||
class TooManyRequestsException extends HttpException {
|
||||
constructor(message: string) {
|
||||
super(message, HttpStatus.TOO_MANY_REQUESTS);
|
||||
}
|
||||
}
|
||||
|
||||
const tooManyRequestsException = new TooManyRequestsException('Too many requests');
|
||||
|
||||
filter.catch(tooManyRequestsException, mockHost);
|
||||
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.TOO_MANY_REQUESTS);
|
||||
expect(mockResponse.json).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
error_code: 'TOO_MANY_REQUESTS',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
271
src/business/admin/admin_database_exception.filter.ts
Normal file
271
src/business/admin/admin_database_exception.filter.ts
Normal file
@@ -0,0 +1,271 @@
|
||||
/**
|
||||
* 管理员数据库操作异常过滤器
|
||||
*
|
||||
* 功能描述:
|
||||
* - 统一处理管理员数据库管理操作中的异常
|
||||
* - 标准化错误响应格式
|
||||
* - 记录详细的错误日志
|
||||
* - 提供用户友好的错误信息
|
||||
*
|
||||
* 职责分离:
|
||||
* - 异常捕获:捕获所有未处理的异常
|
||||
* - 错误转换:将系统异常转换为用户友好的错误信息
|
||||
* - 日志记录:记录详细的错误信息用于调试
|
||||
* - 响应格式化:统一错误响应的格式
|
||||
*
|
||||
* 支持的异常类型:
|
||||
* - BadRequestException: 400 - 请求参数错误
|
||||
* - UnauthorizedException: 401 - 未授权访问
|
||||
* - ForbiddenException: 403 - 权限不足
|
||||
* - NotFoundException: 404 - 资源不存在
|
||||
* - ConflictException: 409 - 资源冲突
|
||||
* - UnprocessableEntityException: 422 - 数据验证失败
|
||||
* - InternalServerErrorException: 500 - 系统内部错误
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-08: 注释规范优化 - 修正@author字段,更新版本号和修改记录 (修改者: moyin)
|
||||
* - 2026-01-08: 功能新增 - 创建管理员数据库异常过滤器 (修改者: assistant)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @since 2026-01-08
|
||||
* @lastModified 2026-01-08
|
||||
*/
|
||||
|
||||
import {
|
||||
ExceptionFilter,
|
||||
Catch,
|
||||
ArgumentsHost,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Logger,
|
||||
BadRequestException,
|
||||
UnauthorizedException,
|
||||
ForbiddenException,
|
||||
NotFoundException,
|
||||
ConflictException,
|
||||
UnprocessableEntityException,
|
||||
InternalServerErrorException
|
||||
} from '@nestjs/common';
|
||||
import { Request, Response } from 'express';
|
||||
import { SENSITIVE_FIELDS } from './admin_constants';
|
||||
import { generateRequestId, getCurrentTimestamp } from './admin_utils';
|
||||
|
||||
/**
|
||||
* 错误响应接口
|
||||
*/
|
||||
interface ErrorResponse {
|
||||
success: false;
|
||||
message: string;
|
||||
error_code: string;
|
||||
details?: {
|
||||
field?: string;
|
||||
constraint?: string;
|
||||
received_value?: any;
|
||||
}[];
|
||||
timestamp: string;
|
||||
request_id: string;
|
||||
path: string;
|
||||
method: string;
|
||||
}
|
||||
|
||||
@Catch()
|
||||
export class AdminDatabaseExceptionFilter implements ExceptionFilter {
|
||||
private readonly logger = new Logger(AdminDatabaseExceptionFilter.name);
|
||||
|
||||
catch(exception: any, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest<Request>();
|
||||
|
||||
const errorResponse = this.buildErrorResponse(exception, request);
|
||||
|
||||
// 记录错误日志
|
||||
this.logError(exception, request, errorResponse);
|
||||
|
||||
response.status(errorResponse.status).json({
|
||||
success: errorResponse.body.success,
|
||||
message: errorResponse.body.message,
|
||||
error_code: errorResponse.body.error_code,
|
||||
details: errorResponse.body.details,
|
||||
timestamp: errorResponse.body.timestamp,
|
||||
request_id: errorResponse.body.request_id,
|
||||
path: errorResponse.body.path,
|
||||
method: errorResponse.body.method
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建错误响应
|
||||
*
|
||||
* @param exception 异常对象
|
||||
* @param request 请求对象
|
||||
* @returns 错误响应对象
|
||||
*/
|
||||
private buildErrorResponse(exception: any, request: Request): { status: number; body: ErrorResponse } {
|
||||
let status: number;
|
||||
let message: string;
|
||||
let error_code: string;
|
||||
let details: any[] | undefined;
|
||||
|
||||
if (exception instanceof HttpException) {
|
||||
status = exception.getStatus();
|
||||
const exceptionResponse = exception.getResponse();
|
||||
|
||||
if (typeof exceptionResponse === 'string') {
|
||||
message = exceptionResponse;
|
||||
} else if (typeof exceptionResponse === 'object' && exceptionResponse !== null) {
|
||||
const responseObj = exceptionResponse as any;
|
||||
message = responseObj.message || responseObj.error || exception.message;
|
||||
details = responseObj.details;
|
||||
} else {
|
||||
message = exception.message;
|
||||
}
|
||||
|
||||
// 根据异常类型设置错误码
|
||||
error_code = this.getErrorCodeByException(exception);
|
||||
} else {
|
||||
// 未知异常,返回500
|
||||
status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
message = '系统内部错误,请稍后重试';
|
||||
error_code = 'INTERNAL_SERVER_ERROR';
|
||||
}
|
||||
|
||||
const body: ErrorResponse = {
|
||||
success: false,
|
||||
message,
|
||||
error_code,
|
||||
details,
|
||||
timestamp: getCurrentTimestamp(),
|
||||
request_id: generateRequestId('err'),
|
||||
path: request.url,
|
||||
method: request.method
|
||||
};
|
||||
|
||||
return { status, body };
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据异常类型获取错误码
|
||||
*
|
||||
* @param exception 异常对象
|
||||
* @returns 错误码
|
||||
*/
|
||||
private getErrorCodeByException(exception: HttpException): string {
|
||||
if (exception instanceof BadRequestException) {
|
||||
return 'BAD_REQUEST';
|
||||
}
|
||||
if (exception instanceof UnauthorizedException) {
|
||||
return 'UNAUTHORIZED';
|
||||
}
|
||||
if (exception instanceof ForbiddenException) {
|
||||
return 'FORBIDDEN';
|
||||
}
|
||||
if (exception instanceof NotFoundException) {
|
||||
return 'NOT_FOUND';
|
||||
}
|
||||
if (exception instanceof ConflictException) {
|
||||
return 'CONFLICT';
|
||||
}
|
||||
if (exception instanceof UnprocessableEntityException) {
|
||||
return 'UNPROCESSABLE_ENTITY';
|
||||
}
|
||||
if (exception instanceof InternalServerErrorException) {
|
||||
return 'INTERNAL_SERVER_ERROR';
|
||||
}
|
||||
|
||||
// 根据HTTP状态码设置错误码
|
||||
const status = exception.getStatus();
|
||||
switch (status) {
|
||||
case HttpStatus.BAD_REQUEST:
|
||||
return 'BAD_REQUEST';
|
||||
case HttpStatus.UNAUTHORIZED:
|
||||
return 'UNAUTHORIZED';
|
||||
case HttpStatus.FORBIDDEN:
|
||||
return 'FORBIDDEN';
|
||||
case HttpStatus.NOT_FOUND:
|
||||
return 'NOT_FOUND';
|
||||
case HttpStatus.CONFLICT:
|
||||
return 'CONFLICT';
|
||||
case HttpStatus.UNPROCESSABLE_ENTITY:
|
||||
return 'UNPROCESSABLE_ENTITY';
|
||||
case HttpStatus.TOO_MANY_REQUESTS:
|
||||
return 'TOO_MANY_REQUESTS';
|
||||
case HttpStatus.INTERNAL_SERVER_ERROR:
|
||||
return 'INTERNAL_SERVER_ERROR';
|
||||
case HttpStatus.BAD_GATEWAY:
|
||||
return 'BAD_GATEWAY';
|
||||
case HttpStatus.SERVICE_UNAVAILABLE:
|
||||
return 'SERVICE_UNAVAILABLE';
|
||||
case HttpStatus.GATEWAY_TIMEOUT:
|
||||
return 'GATEWAY_TIMEOUT';
|
||||
default:
|
||||
return 'UNKNOWN_ERROR';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录错误日志
|
||||
*
|
||||
* @param exception 异常对象
|
||||
* @param request 请求对象
|
||||
* @param errorResponse 错误响应对象
|
||||
*/
|
||||
private logError(exception: any, request: Request, errorResponse: { status: number; body: ErrorResponse }): void {
|
||||
const { status, body } = errorResponse;
|
||||
|
||||
const logContext = {
|
||||
request_id: body.request_id,
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
user_agent: request.get('User-Agent'),
|
||||
ip: request.ip,
|
||||
status,
|
||||
error_code: body.error_code,
|
||||
message: body.message,
|
||||
timestamp: body.timestamp
|
||||
};
|
||||
|
||||
if (status >= 500) {
|
||||
// 服务器错误,记录详细的错误信息
|
||||
this.logger.error('服务器内部错误', {
|
||||
...logContext,
|
||||
stack: exception instanceof Error ? exception.stack : undefined,
|
||||
exception_type: exception.constructor?.name,
|
||||
details: body.details
|
||||
});
|
||||
} else if (status >= 400) {
|
||||
// 客户端错误,记录警告信息
|
||||
this.logger.warn('客户端请求错误', {
|
||||
...logContext,
|
||||
request_body: this.sanitizeRequestBody(request.body),
|
||||
query_params: request.query
|
||||
});
|
||||
} else {
|
||||
// 其他情况,记录普通日志
|
||||
this.logger.log('请求处理异常', logContext);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理请求体中的敏感信息
|
||||
*
|
||||
* @param body 请求体
|
||||
* @returns 清理后的请求体
|
||||
*/
|
||||
private sanitizeRequestBody(body: any): any {
|
||||
if (!body || typeof body !== 'object') {
|
||||
return body;
|
||||
}
|
||||
|
||||
const sanitized = { ...body };
|
||||
|
||||
for (const field of SENSITIVE_FIELDS) {
|
||||
if (sanitized[field]) {
|
||||
sanitized[field] = '[REDACTED]';
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
}
|
||||
71
src/business/admin/admin_login.dto.ts
Normal file
71
src/business/admin/admin_login.dto.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 管理员相关 DTO
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义管理员登录与用户密码重置的请求结构
|
||||
* - 提供完整的数据验证规则
|
||||
* - 支持Swagger文档自动生成
|
||||
*
|
||||
* 职责分离:
|
||||
* - 请求数据结构定义
|
||||
* - 输入参数验证规则
|
||||
* - API文档生成支持
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-08: 文件夹扁平化 - 从dto/子文件夹移动到上级目录 (修改者: moyin)
|
||||
* - 2026-01-07: 代码规范优化 - 修正文件命名规范,更新作者信息和修改记录
|
||||
* - 2026-01-08: 注释规范优化 - 补充类注释,完善DTO文档说明 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.3
|
||||
* @since 2025-12-19
|
||||
* @lastModified 2026-01-08
|
||||
*/
|
||||
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString, MinLength } from 'class-validator';
|
||||
|
||||
/**
|
||||
* 管理员登录请求DTO
|
||||
*
|
||||
* 功能描述:
|
||||
* 定义管理员登录接口的请求数据结构和验证规则
|
||||
*
|
||||
* 验证规则:
|
||||
* - identifier: 必填字符串,支持用户名/邮箱/手机号
|
||||
* - password: 必填字符串,管理员密码
|
||||
*
|
||||
* 使用场景:
|
||||
* - POST /admin/auth/login 接口的请求体
|
||||
*/
|
||||
export class AdminLoginDto {
|
||||
@ApiProperty({ description: '登录标识符(用户名/邮箱/手机号)', example: 'admin' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
identifier: string;
|
||||
|
||||
@ApiProperty({ description: '密码', example: 'Admin123456' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员重置密码请求DTO
|
||||
*
|
||||
* 功能描述:
|
||||
* 定义管理员重置用户密码接口的请求数据结构和验证规则
|
||||
*
|
||||
* 验证规则:
|
||||
* - newPassword: 必填字符串,至少8位,需包含字母和数字
|
||||
*
|
||||
* 使用场景:
|
||||
* - POST /admin/users/:id/reset-password 接口的请求体
|
||||
*/
|
||||
export class AdminResetPasswordDto {
|
||||
@ApiProperty({ description: '新密码(至少8位,包含字母和数字)', example: 'NewPass1234' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MinLength(8)
|
||||
newPassword: string;
|
||||
}
|
||||
284
src/business/admin/admin_operation_log.controller.spec.ts
Normal file
284
src/business/admin/admin_operation_log.controller.spec.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
/**
|
||||
* AdminOperationLogController 单元测试
|
||||
*
|
||||
* 功能描述:
|
||||
* - 测试管理员操作日志控制器的所有HTTP端点
|
||||
* - 验证请求参数处理和响应格式
|
||||
* - 测试权限验证和异常处理
|
||||
*
|
||||
* 职责分离:
|
||||
* - HTTP层测试,不涉及业务逻辑实现
|
||||
* - Mock业务服务,专注控制器逻辑
|
||||
* - 验证请求响应的正确性
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-09: 功能新增 - 创建AdminOperationLogController单元测试 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-09
|
||||
* @lastModified 2026-01-09
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AdminOperationLogController } from './admin_operation_log.controller';
|
||||
import { AdminOperationLogService } from './admin_operation_log.service';
|
||||
import { AdminGuard } from './admin.guard';
|
||||
import { AdminOperationLog } from './admin_operation_log.entity';
|
||||
|
||||
describe('AdminOperationLogController', () => {
|
||||
let controller: AdminOperationLogController;
|
||||
let logService: jest.Mocked<AdminOperationLogService>;
|
||||
|
||||
const mockLogService = {
|
||||
queryLogs: jest.fn(),
|
||||
getLogById: jest.fn(),
|
||||
getStatistics: jest.fn(),
|
||||
getSensitiveOperations: jest.fn(),
|
||||
getAdminOperationHistory: jest.fn(),
|
||||
cleanupExpiredLogs: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AdminOperationLogController],
|
||||
providers: [
|
||||
{
|
||||
provide: AdminOperationLogService,
|
||||
useValue: mockLogService,
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideGuard(AdminGuard)
|
||||
.useValue({ canActivate: () => true })
|
||||
.compile();
|
||||
|
||||
controller = module.get<AdminOperationLogController>(AdminOperationLogController);
|
||||
logService = module.get(AdminOperationLogService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getOperationLogs', () => {
|
||||
it('should query logs with default parameters', async () => {
|
||||
const mockLogs = [
|
||||
{ id: 'log1', operation_type: 'CREATE' },
|
||||
{ id: 'log2', operation_type: 'UPDATE' },
|
||||
] as AdminOperationLog[];
|
||||
|
||||
logService.queryLogs.mockResolvedValue({ logs: mockLogs, total: 2 });
|
||||
|
||||
const result = await controller.getOperationLogs(50, 0);
|
||||
|
||||
expect(logService.queryLogs).toHaveBeenCalledWith({
|
||||
limit: 50,
|
||||
offset: 0
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data.items).toEqual(mockLogs);
|
||||
expect(result.data.total).toBe(2);
|
||||
});
|
||||
|
||||
it('should query logs with custom parameters', async () => {
|
||||
const mockLogs = [] as AdminOperationLog[];
|
||||
|
||||
logService.queryLogs.mockResolvedValue({ logs: mockLogs, total: 0 });
|
||||
|
||||
const result = await controller.getOperationLogs(
|
||||
20,
|
||||
10,
|
||||
'admin1',
|
||||
'CREATE',
|
||||
'users',
|
||||
'SUCCESS',
|
||||
'2026-01-01',
|
||||
'2026-01-31',
|
||||
'true'
|
||||
);
|
||||
|
||||
expect(logService.queryLogs).toHaveBeenCalledWith({
|
||||
adminUserId: 'admin1',
|
||||
operationType: 'CREATE',
|
||||
targetType: 'users',
|
||||
operationResult: 'SUCCESS',
|
||||
startDate: new Date('2026-01-01'),
|
||||
endDate: new Date('2026-01-31'),
|
||||
isSensitive: true,
|
||||
limit: 20,
|
||||
offset: 10
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle invalid date parameters', async () => {
|
||||
await expect(controller.getOperationLogs(
|
||||
50,
|
||||
0,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'invalid',
|
||||
'invalid'
|
||||
)).rejects.toThrow('日期格式无效,请使用ISO格式');
|
||||
});
|
||||
|
||||
it('should handle service error', async () => {
|
||||
logService.queryLogs.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
await expect(controller.getOperationLogs(50, 0)).rejects.toThrow('Database error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOperationLogById', () => {
|
||||
it('should get log by id successfully', async () => {
|
||||
const mockLog = {
|
||||
id: 'log1',
|
||||
operation_type: 'CREATE',
|
||||
target_type: 'users'
|
||||
} as AdminOperationLog;
|
||||
|
||||
logService.getLogById.mockResolvedValue(mockLog);
|
||||
|
||||
const result = await controller.getOperationLogById('log1');
|
||||
|
||||
expect(logService.getLogById).toHaveBeenCalledWith('log1');
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toEqual(mockLog);
|
||||
});
|
||||
|
||||
it('should handle log not found', async () => {
|
||||
logService.getLogById.mockResolvedValue(null);
|
||||
|
||||
await expect(controller.getOperationLogById('nonexistent')).rejects.toThrow('操作日志不存在');
|
||||
});
|
||||
|
||||
it('should handle service error', async () => {
|
||||
logService.getLogById.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
await expect(controller.getOperationLogById('log1')).rejects.toThrow('Database error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOperationStatistics', () => {
|
||||
it('should get statistics successfully', async () => {
|
||||
const mockStats = {
|
||||
totalOperations: 100,
|
||||
successfulOperations: 80,
|
||||
failedOperations: 20,
|
||||
operationsByType: { CREATE: 50, UPDATE: 30, DELETE: 20 },
|
||||
operationsByTarget: { users: 60, profiles: 40 },
|
||||
operationsByAdmin: { admin1: 60, admin2: 40 },
|
||||
averageDuration: 150.5,
|
||||
sensitiveOperations: 10,
|
||||
uniqueAdmins: 5
|
||||
};
|
||||
|
||||
logService.getStatistics.mockResolvedValue(mockStats);
|
||||
|
||||
const result = await controller.getOperationStatistics();
|
||||
|
||||
expect(logService.getStatistics).toHaveBeenCalledWith(undefined, undefined);
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toEqual(mockStats);
|
||||
});
|
||||
|
||||
it('should get statistics with date range', async () => {
|
||||
const mockStats = {
|
||||
totalOperations: 50,
|
||||
successfulOperations: 40,
|
||||
failedOperations: 10,
|
||||
operationsByType: {},
|
||||
operationsByTarget: {},
|
||||
operationsByAdmin: {},
|
||||
averageDuration: 100,
|
||||
sensitiveOperations: 5,
|
||||
uniqueAdmins: 3
|
||||
};
|
||||
|
||||
logService.getStatistics.mockResolvedValue(mockStats);
|
||||
|
||||
const result = await controller.getOperationStatistics('2026-01-01', '2026-01-31');
|
||||
|
||||
expect(logService.getStatistics).toHaveBeenCalledWith(
|
||||
new Date('2026-01-01'),
|
||||
new Date('2026-01-31')
|
||||
);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle invalid dates', async () => {
|
||||
await expect(controller.getOperationStatistics('invalid', 'invalid')).rejects.toThrow('日期格式无效,请使用ISO格式');
|
||||
});
|
||||
|
||||
it('should handle service error', async () => {
|
||||
logService.getStatistics.mockRejectedValue(new Error('Statistics error'));
|
||||
|
||||
await expect(controller.getOperationStatistics()).rejects.toThrow('Statistics error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSensitiveOperations', () => {
|
||||
it('should get sensitive operations successfully', async () => {
|
||||
const mockLogs = [
|
||||
{ id: 'log1', is_sensitive: true }
|
||||
] as AdminOperationLog[];
|
||||
|
||||
logService.getSensitiveOperations.mockResolvedValue({ logs: mockLogs, total: 1 });
|
||||
|
||||
const result = await controller.getSensitiveOperations(50, 0);
|
||||
|
||||
expect(logService.getSensitiveOperations).toHaveBeenCalledWith(50, 0);
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data.items).toEqual(mockLogs);
|
||||
expect(result.data.total).toBe(1);
|
||||
});
|
||||
|
||||
it('should get sensitive operations with pagination', async () => {
|
||||
logService.getSensitiveOperations.mockResolvedValue({ logs: [], total: 0 });
|
||||
|
||||
const result = await controller.getSensitiveOperations(20, 10);
|
||||
|
||||
expect(logService.getSensitiveOperations).toHaveBeenCalledWith(20, 10);
|
||||
});
|
||||
|
||||
it('should handle service error', async () => {
|
||||
logService.getSensitiveOperations.mockRejectedValue(new Error('Query error'));
|
||||
|
||||
await expect(controller.getSensitiveOperations(50, 0)).rejects.toThrow('Query error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('cleanupExpiredLogs', () => {
|
||||
it('should cleanup logs successfully', async () => {
|
||||
logService.cleanupExpiredLogs.mockResolvedValue(25);
|
||||
|
||||
const result = await controller.cleanupExpiredLogs(90);
|
||||
|
||||
expect(logService.cleanupExpiredLogs).toHaveBeenCalledWith(90);
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data.deleted_count).toBe(25);
|
||||
});
|
||||
|
||||
it('should cleanup logs with custom retention days', async () => {
|
||||
logService.cleanupExpiredLogs.mockResolvedValue(10);
|
||||
|
||||
const result = await controller.cleanupExpiredLogs(30);
|
||||
|
||||
expect(logService.cleanupExpiredLogs).toHaveBeenCalledWith(30);
|
||||
expect(result.data.deleted_count).toBe(10);
|
||||
});
|
||||
|
||||
it('should handle invalid retention days', async () => {
|
||||
await expect(controller.cleanupExpiredLogs(5)).rejects.toThrow('保留天数必须在7-365天之间');
|
||||
});
|
||||
|
||||
it('should handle service error', async () => {
|
||||
logService.cleanupExpiredLogs.mockRejectedValue(new Error('Cleanup error'));
|
||||
|
||||
await expect(controller.cleanupExpiredLogs(90)).rejects.toThrow('Cleanup error');
|
||||
});
|
||||
});
|
||||
});
|
||||
373
src/business/admin/admin_operation_log.controller.ts
Normal file
373
src/business/admin/admin_operation_log.controller.ts
Normal file
@@ -0,0 +1,373 @@
|
||||
/**
|
||||
* 管理员操作日志控制器
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供管理员操作日志的查询和管理接口
|
||||
* - 支持日志的分页查询和过滤
|
||||
* - 提供操作统计和分析功能
|
||||
* - 支持敏感操作日志的特殊查询
|
||||
*
|
||||
* 职责分离:
|
||||
* - HTTP请求处理:接收和验证HTTP请求参数
|
||||
* - 权限控制:通过AdminGuard确保只有管理员可以访问
|
||||
* - 业务委托:将业务逻辑委托给AdminOperationLogService处理
|
||||
* - 响应格式化:返回统一格式的HTTP响应
|
||||
*
|
||||
* API端点:
|
||||
* - GET /admin/operation-logs 获取操作日志列表
|
||||
* - GET /admin/operation-logs/:id 获取操作日志详情
|
||||
* - GET /admin/operation-logs/statistics 获取操作统计
|
||||
* - GET /admin/operation-logs/sensitive 获取敏感操作日志
|
||||
* - DELETE /admin/operation-logs/cleanup 清理过期日志
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-08: 功能新增 - 创建管理员操作日志控制器 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-08
|
||||
* @lastModified 2026-01-08
|
||||
*/
|
||||
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Delete,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
UseFilters,
|
||||
UseInterceptors,
|
||||
ParseIntPipe,
|
||||
DefaultValuePipe,
|
||||
BadRequestException
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiBearerAuth,
|
||||
ApiOperation,
|
||||
ApiParam,
|
||||
ApiQuery,
|
||||
ApiResponse
|
||||
} from '@nestjs/swagger';
|
||||
import { AdminGuard } from './admin.guard';
|
||||
import { AdminDatabaseExceptionFilter } from './admin_database_exception.filter';
|
||||
import { AdminOperationLogInterceptor } from './admin_operation_log.interceptor';
|
||||
import { LogAdminOperation } from './log_admin_operation.decorator';
|
||||
import { AdminOperationLogService, LogQueryParams } from './admin_operation_log.service';
|
||||
import { PAGINATION_LIMITS, LOG_RETENTION, USER_QUERY_LIMITS } from './admin_constants';
|
||||
import { safeLimitValue, safeOffsetValue, safeDaysToKeep, createSuccessResponse, createListResponse } from './admin_utils';
|
||||
|
||||
@ApiTags('admin-operation-logs')
|
||||
@Controller('admin/operation-logs')
|
||||
@UseGuards(AdminGuard)
|
||||
@UseFilters(AdminDatabaseExceptionFilter)
|
||||
@UseInterceptors(AdminOperationLogInterceptor)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
export class AdminOperationLogController {
|
||||
constructor(
|
||||
private readonly logService: AdminOperationLogService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 获取操作日志列表
|
||||
*
|
||||
* 功能描述:
|
||||
* 分页获取管理员操作日志,支持多种过滤条件
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证查询参数
|
||||
* 2. 构建查询条件
|
||||
* 3. 调用日志服务查询
|
||||
* 4. 返回分页结果
|
||||
*
|
||||
* @param limit 返回数量,默认50,最大200
|
||||
* @param offset 偏移量,默认0
|
||||
* @param adminUserId 管理员用户ID过滤,可选
|
||||
* @param operationType 操作类型过滤,可选
|
||||
* @param targetType 目标类型过滤,可选
|
||||
* @param operationResult 操作结果过滤,可选
|
||||
* @param startDate 开始日期过滤,可选
|
||||
* @param endDate 结束日期过滤,可选
|
||||
* @param isSensitive 是否敏感操作过滤,可选
|
||||
* @returns 操作日志列表和分页信息
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 获取最近50条操作日志
|
||||
* GET /admin/operation-logs?limit=50&offset=0
|
||||
*
|
||||
* // 获取特定管理员的操作日志
|
||||
* GET /admin/operation-logs?adminUserId=123&limit=20
|
||||
*
|
||||
* // 获取敏感操作日志
|
||||
* GET /admin/operation-logs?isSensitive=true
|
||||
* ```
|
||||
*/
|
||||
@ApiOperation({
|
||||
summary: '获取操作日志列表',
|
||||
description: '分页获取管理员操作日志,支持多种过滤条件'
|
||||
})
|
||||
@ApiQuery({ name: 'limit', required: false, description: '返回数量(默认50,最大200)', example: 50 })
|
||||
@ApiQuery({ name: 'offset', required: false, description: '偏移量(默认0)', example: 0 })
|
||||
@ApiQuery({ name: 'adminUserId', required: false, description: '管理员用户ID过滤', example: '123' })
|
||||
@ApiQuery({ name: 'operationType', required: false, description: '操作类型过滤', example: 'CREATE' })
|
||||
@ApiQuery({ name: 'targetType', required: false, description: '目标类型过滤', example: 'users' })
|
||||
@ApiQuery({ name: 'operationResult', required: false, description: '操作结果过滤', example: 'SUCCESS' })
|
||||
@ApiQuery({ name: 'startDate', required: false, description: '开始日期(ISO格式)', example: '2026-01-01T00:00:00.000Z' })
|
||||
@ApiQuery({ name: 'endDate', required: false, description: '结束日期(ISO格式)', example: '2026-01-08T23:59:59.999Z' })
|
||||
@ApiQuery({ name: 'isSensitive', required: false, description: '是否敏感操作', example: true })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
@ApiResponse({ status: 401, description: '未授权访问' })
|
||||
@ApiResponse({ status: 403, description: '权限不足' })
|
||||
@LogAdminOperation({
|
||||
operationType: 'QUERY',
|
||||
targetType: 'admin_logs',
|
||||
description: '获取操作日志列表',
|
||||
isSensitive: false
|
||||
})
|
||||
@Get()
|
||||
async getOperationLogs(
|
||||
@Query('limit', new DefaultValuePipe(PAGINATION_LIMITS.DEFAULT_LIMIT), ParseIntPipe) limit: number,
|
||||
@Query('offset', new DefaultValuePipe(PAGINATION_LIMITS.DEFAULT_OFFSET), ParseIntPipe) offset: number,
|
||||
@Query('adminUserId') adminUserId?: string,
|
||||
@Query('operationType') operationType?: string,
|
||||
@Query('targetType') targetType?: string,
|
||||
@Query('operationResult') operationResult?: string,
|
||||
@Query('startDate') startDate?: string,
|
||||
@Query('endDate') endDate?: string,
|
||||
@Query('isSensitive') isSensitive?: string
|
||||
) {
|
||||
const safeLimit = safeLimitValue(limit, PAGINATION_LIMITS.LOG_LIST_MAX_LIMIT);
|
||||
const safeOffset = safeOffsetValue(offset);
|
||||
|
||||
const queryParams: LogQueryParams = {
|
||||
limit: safeLimit,
|
||||
offset: safeOffset
|
||||
};
|
||||
|
||||
if (adminUserId) queryParams.adminUserId = adminUserId;
|
||||
if (operationType) queryParams.operationType = operationType;
|
||||
if (targetType) queryParams.targetType = targetType;
|
||||
if (operationResult) queryParams.operationResult = operationResult;
|
||||
if (isSensitive !== undefined) queryParams.isSensitive = isSensitive === 'true';
|
||||
|
||||
if (startDate && endDate) {
|
||||
queryParams.startDate = new Date(startDate);
|
||||
queryParams.endDate = new Date(endDate);
|
||||
|
||||
if (isNaN(queryParams.startDate.getTime()) || isNaN(queryParams.endDate.getTime())) {
|
||||
throw new BadRequestException('日期格式无效,请使用ISO格式');
|
||||
}
|
||||
}
|
||||
|
||||
const { logs, total } = await this.logService.queryLogs(queryParams);
|
||||
|
||||
return createListResponse(
|
||||
logs,
|
||||
total,
|
||||
safeLimit,
|
||||
safeOffset,
|
||||
'操作日志列表获取成功'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取操作日志详情
|
||||
*
|
||||
* 功能描述:
|
||||
* 根据日志ID获取操作日志的详细信息
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证日志ID格式
|
||||
* 2. 查询日志详细信息
|
||||
* 3. 返回日志详情
|
||||
*
|
||||
* @param id 日志ID
|
||||
* @returns 操作日志详细信息
|
||||
*
|
||||
* @throws NotFoundException 当日志不存在时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const result = await controller.getOperationLogById('uuid-123');
|
||||
* ```
|
||||
*/
|
||||
@ApiOperation({
|
||||
summary: '获取操作日志详情',
|
||||
description: '根据日志ID获取操作日志的详细信息'
|
||||
})
|
||||
@ApiParam({ name: 'id', description: '日志ID', example: 'uuid-123' })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
@ApiResponse({ status: 404, description: '日志不存在' })
|
||||
@Get(':id')
|
||||
async getOperationLogById(@Param('id') id: string) {
|
||||
const log = await this.logService.getLogById(id);
|
||||
|
||||
if (!log) {
|
||||
throw new BadRequestException('操作日志不存在');
|
||||
}
|
||||
|
||||
return createSuccessResponse(log, '操作日志详情获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取操作统计信息
|
||||
*
|
||||
* 功能描述:
|
||||
* 获取管理员操作的统计信息,包括操作数量、类型分布等
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 解析时间范围参数
|
||||
* 2. 调用统计服务
|
||||
* 3. 返回统计结果
|
||||
*
|
||||
* @param startDate 开始日期,可选
|
||||
* @param endDate 结束日期,可选
|
||||
* @returns 操作统计信息
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 获取全部统计
|
||||
* GET /admin/operation-logs/statistics
|
||||
*
|
||||
* // 获取指定时间范围的统计
|
||||
* GET /admin/operation-logs/statistics?startDate=2026-01-01&endDate=2026-01-08
|
||||
* ```
|
||||
*/
|
||||
@ApiOperation({
|
||||
summary: '获取操作统计信息',
|
||||
description: '获取管理员操作的统计信息,包括操作数量、类型分布等'
|
||||
})
|
||||
@ApiQuery({ name: 'startDate', required: false, description: '开始日期(ISO格式)', example: '2026-01-01T00:00:00.000Z' })
|
||||
@ApiQuery({ name: 'endDate', required: false, description: '结束日期(ISO格式)', example: '2026-01-08T23:59:59.999Z' })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
@Get('statistics')
|
||||
async getOperationStatistics(
|
||||
@Query('startDate') startDate?: string,
|
||||
@Query('endDate') endDate?: string
|
||||
) {
|
||||
let parsedStartDate: Date | undefined;
|
||||
let parsedEndDate: Date | undefined;
|
||||
|
||||
if (startDate && endDate) {
|
||||
parsedStartDate = new Date(startDate);
|
||||
parsedEndDate = new Date(endDate);
|
||||
|
||||
if (isNaN(parsedStartDate.getTime()) || isNaN(parsedEndDate.getTime())) {
|
||||
throw new BadRequestException('日期格式无效,请使用ISO格式');
|
||||
}
|
||||
}
|
||||
|
||||
const statistics = await this.logService.getStatistics(parsedStartDate, parsedEndDate);
|
||||
|
||||
return createSuccessResponse(statistics, '操作统计信息获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取敏感操作日志
|
||||
*
|
||||
* 功能描述:
|
||||
* 获取标记为敏感的操作日志,用于安全审计
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证查询参数
|
||||
* 2. 查询敏感操作日志
|
||||
* 3. 返回分页结果
|
||||
*
|
||||
* @param limit 返回数量,默认50,最大200
|
||||
* @param offset 偏移量,默认0
|
||||
* @returns 敏感操作日志列表
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 获取最近50条敏感操作日志
|
||||
* GET /admin/operation-logs/sensitive?limit=50
|
||||
* ```
|
||||
*/
|
||||
@ApiOperation({
|
||||
summary: '获取敏感操作日志',
|
||||
description: '获取标记为敏感的操作日志,用于安全审计'
|
||||
})
|
||||
@ApiQuery({ name: 'limit', required: false, description: '返回数量(默认50,最大200)', example: 50 })
|
||||
@ApiQuery({ name: 'offset', required: false, description: '偏移量(默认0)', example: 0 })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
@LogAdminOperation({
|
||||
operationType: 'QUERY',
|
||||
targetType: 'admin_logs',
|
||||
description: '获取敏感操作日志',
|
||||
isSensitive: true
|
||||
})
|
||||
@Get('sensitive')
|
||||
async getSensitiveOperations(
|
||||
@Query('limit', new DefaultValuePipe(PAGINATION_LIMITS.DEFAULT_LIMIT), ParseIntPipe) limit: number,
|
||||
@Query('offset', new DefaultValuePipe(PAGINATION_LIMITS.DEFAULT_OFFSET), ParseIntPipe) offset: number
|
||||
) {
|
||||
const safeLimit = safeLimitValue(limit, PAGINATION_LIMITS.LOG_LIST_MAX_LIMIT);
|
||||
const safeOffset = safeOffsetValue(offset);
|
||||
|
||||
const { logs, total } = await this.logService.getSensitiveOperations(safeLimit, safeOffset);
|
||||
|
||||
return createListResponse(
|
||||
logs,
|
||||
total,
|
||||
safeLimit,
|
||||
safeOffset,
|
||||
'敏感操作日志获取成功'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期日志
|
||||
*
|
||||
* 功能描述:
|
||||
* 清理超过指定天数的操作日志,释放存储空间
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证保留天数参数
|
||||
* 2. 调用清理服务
|
||||
* 3. 返回清理结果
|
||||
*
|
||||
* @param daysToKeep 保留天数,默认90天,最少7天,最多365天
|
||||
* @returns 清理结果,包含删除的记录数
|
||||
*
|
||||
* @throws BadRequestException 当保留天数超出范围时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 清理90天前的日志
|
||||
* DELETE /admin/operation-logs/cleanup?daysToKeep=90
|
||||
* ```
|
||||
*/
|
||||
@ApiOperation({
|
||||
summary: '清理过期日志',
|
||||
description: '清理超过指定天数的操作日志,释放存储空间'
|
||||
})
|
||||
@ApiQuery({ name: 'daysToKeep', required: false, description: '保留天数(默认90,最少7,最多365)', example: 90 })
|
||||
@ApiResponse({ status: 200, description: '清理成功' })
|
||||
@ApiResponse({ status: 400, description: '参数错误' })
|
||||
@LogAdminOperation({
|
||||
operationType: 'DELETE',
|
||||
targetType: 'admin_logs',
|
||||
description: '清理过期操作日志',
|
||||
isSensitive: true
|
||||
})
|
||||
@Delete('cleanup')
|
||||
async cleanupExpiredLogs(
|
||||
@Query('daysToKeep', new DefaultValuePipe(LOG_RETENTION.DEFAULT_DAYS), ParseIntPipe) daysToKeep: number
|
||||
) {
|
||||
const safeDays = safeDaysToKeep(daysToKeep, LOG_RETENTION.MIN_DAYS, LOG_RETENTION.MAX_DAYS);
|
||||
|
||||
if (safeDays !== daysToKeep) {
|
||||
throw new BadRequestException(`保留天数必须在${LOG_RETENTION.MIN_DAYS}-${LOG_RETENTION.MAX_DAYS}天之间`);
|
||||
}
|
||||
|
||||
const deletedCount = await this.logService.cleanupExpiredLogs(safeDays);
|
||||
|
||||
return createSuccessResponse({
|
||||
deleted_count: deletedCount,
|
||||
days_to_keep: safeDays,
|
||||
cleanup_date: new Date().toISOString()
|
||||
}, `过期日志清理完成,删除了${deletedCount}条记录`);
|
||||
}
|
||||
}
|
||||
103
src/business/admin/admin_operation_log.entity.ts
Normal file
103
src/business/admin/admin_operation_log.entity.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* 管理员操作日志实体
|
||||
*
|
||||
* 功能描述:
|
||||
* - 记录管理员的所有数据库操作
|
||||
* - 提供详细的审计跟踪
|
||||
* - 支持操作前后数据状态记录
|
||||
* - 便于安全审计和问题排查
|
||||
*
|
||||
* 职责分离:
|
||||
* - 数据持久化:操作日志的数据库存储
|
||||
* - 审计跟踪:完整的操作历史记录
|
||||
* - 安全监控:敏感操作的详细记录
|
||||
* - 问题排查:操作异常的详细信息
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-08: 注释规范优化 - 修正@author字段,更新版本号和修改记录 (修改者: moyin)
|
||||
* - 2026-01-08: 功能新增 - 创建管理员操作日志实体 (修改者: assistant)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @since 2026-01-08
|
||||
* @lastModified 2026-01-08
|
||||
*/
|
||||
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, Index } from 'typeorm';
|
||||
import { OPERATION_TYPES, OPERATION_RESULTS } from './admin_constants';
|
||||
|
||||
@Entity('admin_operation_logs')
|
||||
@Index(['admin_user_id', 'created_at'])
|
||||
@Index(['operation_type', 'created_at'])
|
||||
@Index(['target_type', 'target_id'])
|
||||
export class AdminOperationLog {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, comment: '管理员用户ID' })
|
||||
@Index()
|
||||
admin_user_id: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 100, comment: '管理员用户名' })
|
||||
admin_username: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, comment: '操作类型 (CREATE/UPDATE/DELETE/QUERY/BATCH)' })
|
||||
operation_type: keyof typeof OPERATION_TYPES;
|
||||
|
||||
@Column({ type: 'varchar', length: 100, comment: '目标资源类型 (users/user_profiles/zulip_accounts)' })
|
||||
target_type: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, nullable: true, comment: '目标资源ID' })
|
||||
target_id?: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 200, comment: '操作描述' })
|
||||
operation_description: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 100, comment: 'HTTP方法和路径' })
|
||||
http_method_path: string;
|
||||
|
||||
@Column({ type: 'json', nullable: true, comment: '请求参数' })
|
||||
request_params?: Record<string, any>;
|
||||
|
||||
@Column({ type: 'json', nullable: true, comment: '操作前数据状态' })
|
||||
before_data?: Record<string, any>;
|
||||
|
||||
@Column({ type: 'json', nullable: true, comment: '操作后数据状态' })
|
||||
after_data?: Record<string, any>;
|
||||
|
||||
@Column({ type: 'varchar', length: 20, comment: '操作结果 (SUCCESS/FAILED)' })
|
||||
operation_result: keyof typeof OPERATION_RESULTS;
|
||||
|
||||
@Column({ type: 'text', nullable: true, comment: '错误信息' })
|
||||
error_message?: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, nullable: true, comment: '错误码' })
|
||||
error_code?: string;
|
||||
|
||||
@Column({ type: 'int', comment: '操作耗时(毫秒)' })
|
||||
duration_ms: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 45, nullable: true, comment: '客户端IP地址' })
|
||||
client_ip?: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 500, nullable: true, comment: '用户代理' })
|
||||
user_agent?: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, comment: '请求ID' })
|
||||
request_id: string;
|
||||
|
||||
@Column({ type: 'json', nullable: true, comment: '额外的上下文信息' })
|
||||
context?: Record<string, any>;
|
||||
|
||||
@CreateDateColumn({ comment: '创建时间' })
|
||||
created_at: Date;
|
||||
|
||||
@Column({ type: 'boolean', default: false, comment: '是否为敏感操作' })
|
||||
is_sensitive: boolean;
|
||||
|
||||
@Column({ type: 'int', default: 0, comment: '影响的记录数量' })
|
||||
affected_records: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 100, nullable: true, comment: '批量操作的批次ID' })
|
||||
batch_id?: string;
|
||||
}
|
||||
415
src/business/admin/admin_operation_log.interceptor.spec.ts
Normal file
415
src/business/admin/admin_operation_log.interceptor.spec.ts
Normal file
@@ -0,0 +1,415 @@
|
||||
/**
|
||||
* AdminOperationLogInterceptor 单元测试
|
||||
*
|
||||
* 功能描述:
|
||||
* - 测试管理员操作日志拦截器的所有功能
|
||||
* - 验证操作拦截和日志记录的正确性
|
||||
* - 测试成功和失败场景的处理
|
||||
*
|
||||
* 职责分离:
|
||||
* - 拦截器逻辑测试,不涉及具体业务
|
||||
* - Mock日志服务,专注拦截器功能
|
||||
* - 验证日志记录的完整性和准确性
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-09: 功能新增 - 创建AdminOperationLogInterceptor单元测试 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-09
|
||||
* @lastModified 2026-01-09
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ExecutionContext, CallHandler } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { of, throwError } from 'rxjs';
|
||||
import { AdminOperationLogInterceptor } from './admin_operation_log.interceptor';
|
||||
import { AdminOperationLogService } from './admin_operation_log.service';
|
||||
import { LOG_ADMIN_OPERATION_KEY, LogAdminOperationOptions } from './log_admin_operation.decorator';
|
||||
import { OPERATION_RESULTS } from './admin_constants';
|
||||
|
||||
describe('AdminOperationLogInterceptor', () => {
|
||||
let interceptor: AdminOperationLogInterceptor;
|
||||
let logService: jest.Mocked<AdminOperationLogService>;
|
||||
let reflector: jest.Mocked<Reflector>;
|
||||
|
||||
const mockLogService = {
|
||||
createLog: jest.fn(),
|
||||
};
|
||||
|
||||
const mockReflector = {
|
||||
get: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
AdminOperationLogInterceptor,
|
||||
{
|
||||
provide: AdminOperationLogService,
|
||||
useValue: mockLogService,
|
||||
},
|
||||
{
|
||||
provide: Reflector,
|
||||
useValue: mockReflector,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
interceptor = module.get<AdminOperationLogInterceptor>(AdminOperationLogInterceptor);
|
||||
logService = module.get(AdminOperationLogService);
|
||||
reflector = module.get(Reflector);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const createMockExecutionContext = (requestData: any = {}) => {
|
||||
const mockRequest = {
|
||||
method: 'POST',
|
||||
url: '/admin/users',
|
||||
route: { path: '/admin/users' },
|
||||
params: { id: '1' },
|
||||
query: { limit: '10' },
|
||||
body: { username: 'testuser' },
|
||||
headers: { 'user-agent': 'test-agent' },
|
||||
user: { id: 'admin1', username: 'admin' },
|
||||
ip: '127.0.0.1',
|
||||
...requestData,
|
||||
};
|
||||
|
||||
const mockResponse = {};
|
||||
|
||||
const mockContext = {
|
||||
switchToHttp: () => ({
|
||||
getRequest: () => mockRequest,
|
||||
getResponse: () => mockResponse,
|
||||
}),
|
||||
getHandler: () => ({}),
|
||||
} as ExecutionContext;
|
||||
|
||||
return { mockContext, mockRequest, mockResponse };
|
||||
};
|
||||
|
||||
const createMockCallHandler = (responseData: any = { success: true }) => {
|
||||
return {
|
||||
handle: () => of(responseData),
|
||||
} as CallHandler;
|
||||
};
|
||||
|
||||
describe('intercept', () => {
|
||||
it('should pass through when no log options configured', (done) => {
|
||||
const { mockContext } = createMockExecutionContext();
|
||||
const mockHandler = createMockCallHandler();
|
||||
|
||||
reflector.get.mockReturnValue(undefined);
|
||||
|
||||
interceptor.intercept(mockContext, mockHandler).subscribe({
|
||||
next: (result) => {
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(logService.createLog).not.toHaveBeenCalled();
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should log successful operation', (done) => {
|
||||
const { mockContext } = createMockExecutionContext();
|
||||
const mockHandler = createMockCallHandler({ success: true, data: { id: '1' } });
|
||||
|
||||
const logOptions: LogAdminOperationOptions = {
|
||||
operationType: 'CREATE',
|
||||
targetType: 'users',
|
||||
description: 'Create user',
|
||||
};
|
||||
|
||||
reflector.get.mockReturnValue(logOptions);
|
||||
logService.createLog.mockResolvedValue({} as any);
|
||||
|
||||
interceptor.intercept(mockContext, mockHandler).subscribe({
|
||||
next: (result) => {
|
||||
expect(result).toEqual({ success: true, data: { id: '1' } });
|
||||
|
||||
// 验证日志记录调用
|
||||
expect(logService.createLog).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
adminUserId: 'admin1',
|
||||
adminUsername: 'admin',
|
||||
operationType: 'CREATE',
|
||||
targetType: 'users',
|
||||
operationDescription: 'Create user',
|
||||
httpMethodPath: 'POST /admin/users',
|
||||
operationResult: OPERATION_RESULTS.SUCCESS,
|
||||
targetId: '1',
|
||||
requestParams: expect.objectContaining({
|
||||
params: { id: '1' },
|
||||
query: { limit: '10' },
|
||||
body: { username: 'testuser' },
|
||||
}),
|
||||
afterData: { success: true, data: { id: '1' } },
|
||||
clientIp: '127.0.0.1',
|
||||
userAgent: 'test-agent',
|
||||
})
|
||||
);
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should log failed operation', (done) => {
|
||||
const { mockContext } = createMockExecutionContext();
|
||||
const error = new Error('Operation failed');
|
||||
const mockHandler = {
|
||||
handle: () => throwError(() => error),
|
||||
} as CallHandler;
|
||||
|
||||
const logOptions: LogAdminOperationOptions = {
|
||||
operationType: 'UPDATE',
|
||||
targetType: 'users',
|
||||
description: 'Update user',
|
||||
};
|
||||
|
||||
reflector.get.mockReturnValue(logOptions);
|
||||
logService.createLog.mockResolvedValue({} as any);
|
||||
|
||||
interceptor.intercept(mockContext, mockHandler).subscribe({
|
||||
error: (err) => {
|
||||
expect(err).toBe(error);
|
||||
|
||||
// 验证错误日志记录调用
|
||||
expect(logService.createLog).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
adminUserId: 'admin1',
|
||||
adminUsername: 'admin',
|
||||
operationType: 'UPDATE',
|
||||
targetType: 'users',
|
||||
operationDescription: 'Update user',
|
||||
operationResult: OPERATION_RESULTS.FAILED,
|
||||
errorMessage: 'Operation failed',
|
||||
errorCode: 'UNKNOWN_ERROR',
|
||||
})
|
||||
);
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle missing admin user', (done) => {
|
||||
const { mockContext } = createMockExecutionContext({ user: undefined });
|
||||
const mockHandler = createMockCallHandler();
|
||||
|
||||
const logOptions: LogAdminOperationOptions = {
|
||||
operationType: 'QUERY',
|
||||
targetType: 'users',
|
||||
description: 'Query users',
|
||||
};
|
||||
|
||||
reflector.get.mockReturnValue(logOptions);
|
||||
logService.createLog.mockResolvedValue({} as any);
|
||||
|
||||
interceptor.intercept(mockContext, mockHandler).subscribe({
|
||||
next: () => {
|
||||
expect(logService.createLog).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
adminUserId: 'unknown',
|
||||
adminUsername: 'unknown',
|
||||
})
|
||||
);
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle sensitive operations', (done) => {
|
||||
const { mockContext } = createMockExecutionContext();
|
||||
const mockHandler = createMockCallHandler();
|
||||
|
||||
const logOptions: LogAdminOperationOptions = {
|
||||
operationType: 'DELETE',
|
||||
targetType: 'users',
|
||||
description: 'Delete user',
|
||||
isSensitive: true,
|
||||
};
|
||||
|
||||
reflector.get.mockReturnValue(logOptions);
|
||||
logService.createLog.mockResolvedValue({} as any);
|
||||
|
||||
interceptor.intercept(mockContext, mockHandler).subscribe({
|
||||
next: () => {
|
||||
expect(logService.createLog).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
isSensitive: true,
|
||||
})
|
||||
);
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should disable request params capture when configured', (done) => {
|
||||
const { mockContext } = createMockExecutionContext();
|
||||
const mockHandler = createMockCallHandler();
|
||||
|
||||
const logOptions: LogAdminOperationOptions = {
|
||||
operationType: 'QUERY',
|
||||
targetType: 'users',
|
||||
description: 'Query users',
|
||||
captureRequestParams: false,
|
||||
};
|
||||
|
||||
reflector.get.mockReturnValue(logOptions);
|
||||
logService.createLog.mockResolvedValue({} as any);
|
||||
|
||||
interceptor.intercept(mockContext, mockHandler).subscribe({
|
||||
next: () => {
|
||||
expect(logService.createLog).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
requestParams: undefined,
|
||||
})
|
||||
);
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should disable after data capture when configured', (done) => {
|
||||
const { mockContext } = createMockExecutionContext();
|
||||
const mockHandler = createMockCallHandler({ data: 'sensitive' });
|
||||
|
||||
const logOptions: LogAdminOperationOptions = {
|
||||
operationType: 'QUERY',
|
||||
targetType: 'users',
|
||||
description: 'Query users',
|
||||
captureAfterData: false,
|
||||
};
|
||||
|
||||
reflector.get.mockReturnValue(logOptions);
|
||||
logService.createLog.mockResolvedValue({} as any);
|
||||
|
||||
interceptor.intercept(mockContext, mockHandler).subscribe({
|
||||
next: () => {
|
||||
expect(logService.createLog).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
afterData: undefined,
|
||||
})
|
||||
);
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should extract affected records from response', (done) => {
|
||||
const { mockContext } = createMockExecutionContext();
|
||||
const responseData = {
|
||||
success: true,
|
||||
data: {
|
||||
items: [{ id: '1' }, { id: '2' }, { id: '3' }],
|
||||
total: 3,
|
||||
},
|
||||
};
|
||||
const mockHandler = createMockCallHandler(responseData);
|
||||
|
||||
const logOptions: LogAdminOperationOptions = {
|
||||
operationType: 'QUERY',
|
||||
targetType: 'users',
|
||||
description: 'Query users',
|
||||
};
|
||||
|
||||
reflector.get.mockReturnValue(logOptions);
|
||||
logService.createLog.mockResolvedValue({} as any);
|
||||
|
||||
interceptor.intercept(mockContext, mockHandler).subscribe({
|
||||
next: () => {
|
||||
expect(logService.createLog).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
affectedRecords: 3, // Should extract from items array length
|
||||
})
|
||||
);
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle log service errors gracefully', (done) => {
|
||||
const { mockContext } = createMockExecutionContext();
|
||||
const mockHandler = createMockCallHandler();
|
||||
|
||||
const logOptions: LogAdminOperationOptions = {
|
||||
operationType: 'CREATE',
|
||||
targetType: 'users',
|
||||
description: 'Create user',
|
||||
};
|
||||
|
||||
reflector.get.mockReturnValue(logOptions);
|
||||
logService.createLog.mockRejectedValue(new Error('Log service error'));
|
||||
|
||||
// 即使日志记录失败,原始操作也应该成功
|
||||
interceptor.intercept(mockContext, mockHandler).subscribe({
|
||||
next: (result) => {
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(logService.createLog).toHaveBeenCalled();
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should extract target ID from different sources', (done) => {
|
||||
const { mockContext } = createMockExecutionContext({
|
||||
params: {},
|
||||
body: { id: 'body-id' },
|
||||
query: { id: 'query-id' },
|
||||
});
|
||||
const mockHandler = createMockCallHandler();
|
||||
|
||||
const logOptions: LogAdminOperationOptions = {
|
||||
operationType: 'UPDATE',
|
||||
targetType: 'users',
|
||||
description: 'Update user',
|
||||
};
|
||||
|
||||
reflector.get.mockReturnValue(logOptions);
|
||||
logService.createLog.mockResolvedValue({} as any);
|
||||
|
||||
interceptor.intercept(mockContext, mockHandler).subscribe({
|
||||
next: () => {
|
||||
expect(logService.createLog).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
targetId: 'body-id', // Should prefer body over query
|
||||
})
|
||||
);
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle missing route information', (done) => {
|
||||
const { mockContext } = createMockExecutionContext({
|
||||
route: undefined,
|
||||
url: '/admin/custom-endpoint',
|
||||
});
|
||||
const mockHandler = createMockCallHandler();
|
||||
|
||||
const logOptions: LogAdminOperationOptions = {
|
||||
operationType: 'QUERY',
|
||||
targetType: 'custom',
|
||||
description: 'Custom operation',
|
||||
};
|
||||
|
||||
reflector.get.mockReturnValue(logOptions);
|
||||
logService.createLog.mockResolvedValue({} as any);
|
||||
|
||||
interceptor.intercept(mockContext, mockHandler).subscribe({
|
||||
next: () => {
|
||||
expect(logService.createLog).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
httpMethodPath: 'POST /admin/custom-endpoint',
|
||||
})
|
||||
);
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
203
src/business/admin/admin_operation_log.interceptor.ts
Normal file
203
src/business/admin/admin_operation_log.interceptor.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* 管理员操作日志拦截器
|
||||
*
|
||||
* 功能描述:
|
||||
* - 自动拦截管理员操作并记录日志
|
||||
* - 记录操作前后的数据状态
|
||||
* - 监控操作性能和错误
|
||||
* - 支持敏感操作的特殊处理
|
||||
*
|
||||
* 职责分离:
|
||||
* - 操作拦截:拦截控制器方法的执行
|
||||
* - 数据捕获:记录请求参数和响应数据
|
||||
* - 日志记录:调用日志服务记录操作
|
||||
* - 错误处理:记录操作异常信息
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-08: 注释规范优化 - 修正@author字段,更新版本号和修改记录 (修改者: moyin)
|
||||
* - 2026-01-08: 功能新增 - 创建管理员操作日志拦截器 (修改者: assistant)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @since 2026-01-08
|
||||
* @lastModified 2026-01-08
|
||||
*/
|
||||
|
||||
import {
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
ExecutionContext,
|
||||
CallHandler,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { tap, catchError } from 'rxjs/operators';
|
||||
import { AdminOperationLogService } from './admin_operation_log.service';
|
||||
import { LOG_ADMIN_OPERATION_KEY, LogAdminOperationOptions } from './log_admin_operation.decorator';
|
||||
import { SENSITIVE_FIELDS, OPERATION_RESULTS } from './admin_constants';
|
||||
import { extractClientIp, generateRequestId, sanitizeRequestBody } from './admin_utils';
|
||||
|
||||
@Injectable()
|
||||
export class AdminOperationLogInterceptor implements NestInterceptor {
|
||||
private readonly logger = new Logger(AdminOperationLogInterceptor.name);
|
||||
|
||||
constructor(
|
||||
private readonly reflector: Reflector,
|
||||
private readonly logService: AdminOperationLogService,
|
||||
) {}
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||
const logOptions = this.reflector.get<LogAdminOperationOptions>(
|
||||
LOG_ADMIN_OPERATION_KEY,
|
||||
context.getHandler(),
|
||||
);
|
||||
|
||||
// 如果没有日志配置,直接执行
|
||||
if (!logOptions) {
|
||||
return next.handle();
|
||||
}
|
||||
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const response = context.switchToHttp().getResponse();
|
||||
const startTime = Date.now();
|
||||
|
||||
// 提取请求信息
|
||||
const adminUser = request.user;
|
||||
const clientIp = extractClientIp(request);
|
||||
const userAgent = request.headers['user-agent'] || 'unknown';
|
||||
const httpMethodPath = `${request.method} ${request.route?.path || request.url}`;
|
||||
const requestId = generateRequestId();
|
||||
|
||||
// 提取请求参数
|
||||
const requestParams = logOptions.captureRequestParams !== false ? {
|
||||
params: request.params,
|
||||
query: request.query,
|
||||
body: sanitizeRequestBody(request.body)
|
||||
} : undefined;
|
||||
|
||||
// 提取目标ID(如果存在)
|
||||
const targetId = request.params?.id || request.body?.id || request.query?.id;
|
||||
|
||||
let beforeData: any = undefined;
|
||||
let operationError: any = null;
|
||||
|
||||
return next.handle().pipe(
|
||||
tap((responseData) => {
|
||||
// 操作成功,记录日志
|
||||
this.recordLog({
|
||||
logOptions,
|
||||
adminUser,
|
||||
clientIp,
|
||||
userAgent,
|
||||
httpMethodPath,
|
||||
requestId,
|
||||
requestParams,
|
||||
targetId,
|
||||
beforeData,
|
||||
afterData: logOptions.captureAfterData !== false ? responseData : undefined,
|
||||
operationResult: OPERATION_RESULTS.SUCCESS,
|
||||
durationMs: Date.now() - startTime,
|
||||
affectedRecords: this.extractAffectedRecords(responseData),
|
||||
});
|
||||
}),
|
||||
catchError((error) => {
|
||||
// 操作失败,记录错误日志
|
||||
operationError = error;
|
||||
this.recordLog({
|
||||
logOptions,
|
||||
adminUser,
|
||||
clientIp,
|
||||
userAgent,
|
||||
httpMethodPath,
|
||||
requestId,
|
||||
requestParams,
|
||||
targetId,
|
||||
beforeData,
|
||||
operationResult: OPERATION_RESULTS.FAILED,
|
||||
errorMessage: error.message || String(error),
|
||||
errorCode: error.code || error.status || 'UNKNOWN_ERROR',
|
||||
durationMs: Date.now() - startTime,
|
||||
});
|
||||
|
||||
return throwError(() => error);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录操作日志
|
||||
*/
|
||||
private async recordLog(params: {
|
||||
logOptions: LogAdminOperationOptions;
|
||||
adminUser: any;
|
||||
clientIp: string;
|
||||
userAgent: string;
|
||||
httpMethodPath: string;
|
||||
requestId: string;
|
||||
requestParams?: any;
|
||||
targetId?: string;
|
||||
beforeData?: any;
|
||||
afterData?: any;
|
||||
operationResult: keyof typeof OPERATION_RESULTS;
|
||||
errorMessage?: string;
|
||||
errorCode?: string;
|
||||
durationMs: number;
|
||||
affectedRecords?: number;
|
||||
}) {
|
||||
try {
|
||||
await this.logService.createLog({
|
||||
adminUserId: params.adminUser?.id || 'unknown',
|
||||
adminUsername: params.adminUser?.username || 'unknown',
|
||||
operationType: params.logOptions.operationType,
|
||||
targetType: params.logOptions.targetType,
|
||||
targetId: params.targetId,
|
||||
operationDescription: params.logOptions.description,
|
||||
httpMethodPath: params.httpMethodPath,
|
||||
requestParams: params.requestParams,
|
||||
beforeData: params.beforeData,
|
||||
afterData: params.afterData,
|
||||
operationResult: params.operationResult,
|
||||
errorMessage: params.errorMessage,
|
||||
errorCode: params.errorCode,
|
||||
durationMs: params.durationMs,
|
||||
clientIp: params.clientIp,
|
||||
userAgent: params.userAgent,
|
||||
requestId: params.requestId,
|
||||
isSensitive: params.logOptions.isSensitive || false,
|
||||
affectedRecords: params.affectedRecords || 0,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error('记录操作日志失败', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
adminUserId: params.adminUser?.id,
|
||||
operationType: params.logOptions.operationType,
|
||||
targetType: params.logOptions.targetType,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取影响的记录数量
|
||||
*/
|
||||
private extractAffectedRecords(responseData: any): number {
|
||||
if (!responseData || typeof responseData !== 'object') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 从响应数据中提取影响的记录数
|
||||
if (responseData.data) {
|
||||
if (Array.isArray(responseData.data.items)) {
|
||||
return responseData.data.items.length;
|
||||
}
|
||||
if (responseData.data.total !== undefined) {
|
||||
return responseData.data.total;
|
||||
}
|
||||
if (responseData.data.success !== undefined && responseData.data.failed !== undefined) {
|
||||
return responseData.data.success + responseData.data.failed;
|
||||
}
|
||||
}
|
||||
|
||||
return 1; // 默认为1条记录
|
||||
}
|
||||
}
|
||||
407
src/business/admin/admin_operation_log.service.spec.ts
Normal file
407
src/business/admin/admin_operation_log.service.spec.ts
Normal file
@@ -0,0 +1,407 @@
|
||||
/**
|
||||
* AdminOperationLogService 单元测试
|
||||
*
|
||||
* 功能描述:
|
||||
* - 测试管理员操作日志服务的所有方法
|
||||
* - 验证日志记录和查询的正确性
|
||||
* - 测试统计功能和清理功能
|
||||
*
|
||||
* 职责分离:
|
||||
* - 业务逻辑测试,不涉及HTTP层
|
||||
* - Mock数据库操作,专注服务逻辑
|
||||
* - 验证日志处理的正确性
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-09: 功能新增 - 创建AdminOperationLogService单元测试 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-09
|
||||
* @lastModified 2026-01-09
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { AdminOperationLogService, CreateLogParams, LogQueryParams } from './admin_operation_log.service';
|
||||
import { AdminOperationLog } from './admin_operation_log.entity';
|
||||
|
||||
describe('AdminOperationLogService', () => {
|
||||
let service: AdminOperationLogService;
|
||||
let repository: jest.Mocked<Repository<AdminOperationLog>>;
|
||||
|
||||
const mockRepository = {
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
createQueryBuilder: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
find: jest.fn(),
|
||||
findAndCount: jest.fn(),
|
||||
};
|
||||
|
||||
const mockQueryBuilder = {
|
||||
andWhere: jest.fn().mockReturnThis(),
|
||||
orderBy: jest.fn().mockReturnThis(),
|
||||
limit: jest.fn().mockReturnThis(),
|
||||
offset: jest.fn().mockReturnThis(),
|
||||
getManyAndCount: jest.fn(),
|
||||
getCount: jest.fn(),
|
||||
clone: jest.fn().mockReturnThis(),
|
||||
select: jest.fn().mockReturnThis(),
|
||||
addSelect: jest.fn().mockReturnThis(),
|
||||
groupBy: jest.fn().mockReturnThis(),
|
||||
getRawMany: jest.fn(),
|
||||
getRawOne: jest.fn(),
|
||||
delete: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
execute: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
AdminOperationLogService,
|
||||
{
|
||||
provide: getRepositoryToken(AdminOperationLog),
|
||||
useValue: mockRepository,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<AdminOperationLogService>(AdminOperationLogService);
|
||||
repository = module.get(getRepositoryToken(AdminOperationLog));
|
||||
|
||||
// Setup default mock behavior
|
||||
mockRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('createLog', () => {
|
||||
it('should create log successfully', async () => {
|
||||
const logParams: CreateLogParams = {
|
||||
adminUserId: 'admin1',
|
||||
adminUsername: 'admin',
|
||||
operationType: 'CREATE',
|
||||
targetType: 'users',
|
||||
targetId: '1',
|
||||
operationDescription: 'Create user',
|
||||
httpMethodPath: 'POST /admin/users',
|
||||
operationResult: 'SUCCESS',
|
||||
durationMs: 100,
|
||||
requestId: 'req_123',
|
||||
};
|
||||
|
||||
const mockLog = {
|
||||
id: 'log1',
|
||||
admin_user_id: logParams.adminUserId,
|
||||
admin_username: logParams.adminUsername,
|
||||
operation_type: logParams.operationType,
|
||||
target_type: logParams.targetType,
|
||||
target_id: logParams.targetId,
|
||||
operation_description: logParams.operationDescription,
|
||||
http_method_path: logParams.httpMethodPath,
|
||||
operation_result: logParams.operationResult,
|
||||
duration_ms: logParams.durationMs,
|
||||
request_id: logParams.requestId,
|
||||
is_sensitive: false,
|
||||
affected_records: 0,
|
||||
created_at: new Date(),
|
||||
updated_at: new Date()
|
||||
} as AdminOperationLog;
|
||||
|
||||
mockRepository.create.mockReturnValue(mockLog);
|
||||
mockRepository.save.mockResolvedValue(mockLog);
|
||||
|
||||
const result = await service.createLog(logParams);
|
||||
|
||||
expect(mockRepository.create).toHaveBeenCalledWith({
|
||||
admin_user_id: logParams.adminUserId,
|
||||
admin_username: logParams.adminUsername,
|
||||
operation_type: logParams.operationType,
|
||||
target_type: logParams.targetType,
|
||||
target_id: logParams.targetId,
|
||||
operation_description: logParams.operationDescription,
|
||||
http_method_path: logParams.httpMethodPath,
|
||||
request_params: logParams.requestParams,
|
||||
before_data: logParams.beforeData,
|
||||
after_data: logParams.afterData,
|
||||
operation_result: logParams.operationResult,
|
||||
error_message: logParams.errorMessage,
|
||||
error_code: logParams.errorCode,
|
||||
duration_ms: logParams.durationMs,
|
||||
client_ip: logParams.clientIp,
|
||||
user_agent: logParams.userAgent,
|
||||
request_id: logParams.requestId,
|
||||
context: logParams.context,
|
||||
is_sensitive: false,
|
||||
affected_records: 0,
|
||||
batch_id: logParams.batchId,
|
||||
});
|
||||
|
||||
expect(mockRepository.save).toHaveBeenCalledWith(mockLog);
|
||||
expect(result).toEqual(mockLog);
|
||||
});
|
||||
|
||||
it('should handle creation error', async () => {
|
||||
const logParams: CreateLogParams = {
|
||||
adminUserId: 'admin1',
|
||||
adminUsername: 'admin',
|
||||
operationType: 'CREATE',
|
||||
targetType: 'users',
|
||||
operationDescription: 'Create user',
|
||||
httpMethodPath: 'POST /admin/users',
|
||||
operationResult: 'SUCCESS',
|
||||
durationMs: 100,
|
||||
requestId: 'req_123',
|
||||
};
|
||||
|
||||
mockRepository.create.mockReturnValue({} as AdminOperationLog);
|
||||
mockRepository.save.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
await expect(service.createLog(logParams)).rejects.toThrow('Database error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('queryLogs', () => {
|
||||
it('should query logs successfully', async () => {
|
||||
const queryParams: LogQueryParams = {
|
||||
adminUserId: 'admin1',
|
||||
operationType: 'CREATE',
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
};
|
||||
|
||||
const mockLogs = [
|
||||
{ id: 'log1', admin_user_id: 'admin1' },
|
||||
{ id: 'log2', admin_user_id: 'admin1' },
|
||||
] as AdminOperationLog[];
|
||||
|
||||
mockQueryBuilder.getManyAndCount.mockResolvedValue([mockLogs, 2]);
|
||||
|
||||
const result = await service.queryLogs(queryParams);
|
||||
|
||||
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('log.admin_user_id = :adminUserId', { adminUserId: 'admin1' });
|
||||
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('log.operation_type = :operationType', { operationType: 'CREATE' });
|
||||
expect(mockQueryBuilder.orderBy).toHaveBeenCalledWith('log.created_at', 'DESC');
|
||||
expect(mockQueryBuilder.limit).toHaveBeenCalledWith(10);
|
||||
expect(mockQueryBuilder.offset).toHaveBeenCalledWith(0);
|
||||
|
||||
expect(result.logs).toEqual(mockLogs);
|
||||
expect(result.total).toBe(2);
|
||||
});
|
||||
|
||||
it('should query logs with date range', async () => {
|
||||
const startDate = new Date('2026-01-01');
|
||||
const endDate = new Date('2026-01-31');
|
||||
const queryParams: LogQueryParams = {
|
||||
startDate,
|
||||
endDate,
|
||||
isSensitive: true,
|
||||
};
|
||||
|
||||
const mockLogs = [] as AdminOperationLog[];
|
||||
mockQueryBuilder.getManyAndCount.mockResolvedValue([mockLogs, 0]);
|
||||
|
||||
const result = await service.queryLogs(queryParams);
|
||||
|
||||
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('log.created_at BETWEEN :startDate AND :endDate', {
|
||||
startDate,
|
||||
endDate
|
||||
});
|
||||
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('log.is_sensitive = :isSensitive', { isSensitive: true });
|
||||
});
|
||||
|
||||
it('should handle query error', async () => {
|
||||
const queryParams: LogQueryParams = {};
|
||||
|
||||
mockQueryBuilder.getManyAndCount.mockRejectedValue(new Error('Query error'));
|
||||
|
||||
await expect(service.queryLogs(queryParams)).rejects.toThrow('Query error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLogById', () => {
|
||||
it('should get log by id successfully', async () => {
|
||||
const mockLog = { id: 'log1', admin_user_id: 'admin1' } as AdminOperationLog;
|
||||
|
||||
mockRepository.findOne.mockResolvedValue(mockLog);
|
||||
|
||||
const result = await service.getLogById('log1');
|
||||
|
||||
expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { id: 'log1' } });
|
||||
expect(result).toEqual(mockLog);
|
||||
});
|
||||
|
||||
it('should return null when log not found', async () => {
|
||||
mockRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
const result = await service.getLogById('nonexistent');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle get error', async () => {
|
||||
mockRepository.findOne.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
await expect(service.getLogById('log1')).rejects.toThrow('Database error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStatistics', () => {
|
||||
it('should get statistics successfully', async () => {
|
||||
// Mock basic statistics
|
||||
mockQueryBuilder.getCount
|
||||
.mockResolvedValueOnce(100) // total
|
||||
.mockResolvedValueOnce(80) // successful
|
||||
.mockResolvedValueOnce(10); // sensitive
|
||||
|
||||
// Mock operation type statistics
|
||||
mockQueryBuilder.getRawMany.mockResolvedValueOnce([
|
||||
{ type: 'CREATE', count: '50' },
|
||||
{ type: 'UPDATE', count: '30' },
|
||||
{ type: 'DELETE', count: '20' },
|
||||
]);
|
||||
|
||||
// Mock target type statistics
|
||||
mockQueryBuilder.getRawMany.mockResolvedValueOnce([
|
||||
{ type: 'users', count: '60' },
|
||||
{ type: 'profiles', count: '40' },
|
||||
]);
|
||||
|
||||
// Mock performance statistics
|
||||
mockQueryBuilder.getRawOne
|
||||
.mockResolvedValueOnce({ avgDuration: '150.5' }) // average duration
|
||||
.mockResolvedValueOnce({ uniqueAdmins: '5' }); // unique admins
|
||||
|
||||
const result = await service.getStatistics();
|
||||
|
||||
expect(result.totalOperations).toBe(100);
|
||||
expect(result.successfulOperations).toBe(80);
|
||||
expect(result.failedOperations).toBe(20);
|
||||
expect(result.sensitiveOperations).toBe(10);
|
||||
expect(result.operationsByType).toEqual({
|
||||
CREATE: 50,
|
||||
UPDATE: 30,
|
||||
DELETE: 20,
|
||||
});
|
||||
expect(result.operationsByTarget).toEqual({
|
||||
users: 60,
|
||||
profiles: 40,
|
||||
});
|
||||
expect(result.averageDuration).toBe(150.5);
|
||||
expect(result.uniqueAdmins).toBe(5);
|
||||
});
|
||||
|
||||
it('should get statistics with date range', async () => {
|
||||
const startDate = new Date('2026-01-01');
|
||||
const endDate = new Date('2026-01-31');
|
||||
|
||||
mockQueryBuilder.getCount.mockResolvedValue(50);
|
||||
mockQueryBuilder.getRawMany.mockResolvedValue([]);
|
||||
mockQueryBuilder.getRawOne.mockResolvedValue({ avgDuration: '100', uniqueAdmins: '3' });
|
||||
|
||||
const result = await service.getStatistics(startDate, endDate);
|
||||
|
||||
expect(mockQueryBuilder.where).toHaveBeenCalledWith('log.created_at BETWEEN :startDate AND :endDate', {
|
||||
startDate,
|
||||
endDate
|
||||
});
|
||||
expect(result.totalOperations).toBe(50);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cleanupExpiredLogs', () => {
|
||||
it('should cleanup expired logs successfully', async () => {
|
||||
mockQueryBuilder.execute.mockResolvedValue({ affected: 25 });
|
||||
|
||||
const result = await service.cleanupExpiredLogs(30);
|
||||
|
||||
expect(mockQueryBuilder.delete).toHaveBeenCalled();
|
||||
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('is_sensitive = :sensitive', { sensitive: false });
|
||||
expect(result).toBe(25);
|
||||
});
|
||||
|
||||
it('should use default retention days', async () => {
|
||||
mockQueryBuilder.execute.mockResolvedValue({ affected: 10 });
|
||||
|
||||
const result = await service.cleanupExpiredLogs();
|
||||
|
||||
expect(result).toBe(10);
|
||||
});
|
||||
|
||||
it('should handle cleanup error', async () => {
|
||||
mockQueryBuilder.execute.mockRejectedValue(new Error('Cleanup error'));
|
||||
|
||||
await expect(service.cleanupExpiredLogs(30)).rejects.toThrow('Cleanup error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAdminOperationHistory', () => {
|
||||
it('should get admin operation history successfully', async () => {
|
||||
const mockLogs = [
|
||||
{ id: 'log1', admin_user_id: 'admin1' },
|
||||
{ id: 'log2', admin_user_id: 'admin1' },
|
||||
] as AdminOperationLog[];
|
||||
|
||||
mockRepository.find.mockResolvedValue(mockLogs);
|
||||
|
||||
const result = await service.getAdminOperationHistory('admin1', 10);
|
||||
|
||||
expect(mockRepository.find).toHaveBeenCalledWith({
|
||||
where: { admin_user_id: 'admin1' },
|
||||
order: { created_at: 'DESC' },
|
||||
take: 10
|
||||
});
|
||||
expect(result).toEqual(mockLogs);
|
||||
});
|
||||
|
||||
it('should use default limit', async () => {
|
||||
mockRepository.find.mockResolvedValue([]);
|
||||
|
||||
const result = await service.getAdminOperationHistory('admin1');
|
||||
|
||||
expect(mockRepository.find).toHaveBeenCalledWith({
|
||||
where: { admin_user_id: 'admin1' },
|
||||
order: { created_at: 'DESC' },
|
||||
take: 20 // DEFAULT_LIMIT
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSensitiveOperations', () => {
|
||||
it('should get sensitive operations successfully', async () => {
|
||||
const mockLogs = [
|
||||
{ id: 'log1', is_sensitive: true },
|
||||
] as AdminOperationLog[];
|
||||
|
||||
mockRepository.findAndCount.mockResolvedValue([mockLogs, 1]);
|
||||
|
||||
const result = await service.getSensitiveOperations(10, 0);
|
||||
|
||||
expect(mockRepository.findAndCount).toHaveBeenCalledWith({
|
||||
where: { is_sensitive: true },
|
||||
order: { created_at: 'DESC' },
|
||||
take: 10,
|
||||
skip: 0
|
||||
});
|
||||
expect(result.logs).toEqual(mockLogs);
|
||||
expect(result.total).toBe(1);
|
||||
});
|
||||
|
||||
it('should use default pagination', async () => {
|
||||
mockRepository.findAndCount.mockResolvedValue([[], 0]);
|
||||
|
||||
const result = await service.getSensitiveOperations();
|
||||
|
||||
expect(mockRepository.findAndCount).toHaveBeenCalledWith({
|
||||
where: { is_sensitive: true },
|
||||
order: { created_at: 'DESC' },
|
||||
take: 50, // DEFAULT_LIMIT
|
||||
skip: 0
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
575
src/business/admin/admin_operation_log.service.ts
Normal file
575
src/business/admin/admin_operation_log.service.ts
Normal file
@@ -0,0 +1,575 @@
|
||||
/**
|
||||
* 管理员操作日志服务
|
||||
*
|
||||
* 功能描述:
|
||||
* - 记录管理员的所有数据库操作
|
||||
* - 提供操作日志的查询和统计功能
|
||||
* - 支持敏感操作的特殊标记
|
||||
* - 实现日志的自动清理和归档
|
||||
*
|
||||
* 职责分离:
|
||||
* - 日志记录:记录操作的详细信息
|
||||
* - 日志查询:提供灵活的日志查询接口
|
||||
* - 日志统计:生成操作统计报告
|
||||
* - 日志管理:自动清理和归档功能
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-09: 代码质量优化 - 使用常量替代硬编码字符串,提高代码一致性 (修改者: moyin)
|
||||
* - 2026-01-09: 代码质量优化 - 拆分getStatistics长方法为多个私有方法,提高可读性 (修改者: moyin)
|
||||
* - 2026-01-08: 注释规范优化 - 修正@author字段,更新版本号和修改记录 (修改者: moyin)
|
||||
* - 2026-01-08: 注释规范优化 - 为接口添加注释,完善文档说明 (修改者: moyin)
|
||||
* - 2026-01-08: 注释规范优化 - 添加类注释,完善服务文档说明 (修改者: moyin)
|
||||
* - 2026-01-08: 代码质量优化 - 提取魔法数字为常量,重构长方法,补充导入 (修改者: moyin)
|
||||
* - 2026-01-08: 功能新增 - 创建管理员操作日志服务 (修改者: assistant)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.4.0
|
||||
* @since 2026-01-08
|
||||
* @lastModified 2026-01-09
|
||||
*/
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { AdminOperationLog } from './admin_operation_log.entity';
|
||||
import { LOG_QUERY_LIMITS, USER_QUERY_LIMITS, LOG_RETENTION, OPERATION_TYPES, OPERATION_RESULTS } from './admin_constants';
|
||||
|
||||
/**
|
||||
* 创建日志参数接口
|
||||
*
|
||||
* 功能描述:
|
||||
* 定义创建管理员操作日志所需的所有参数
|
||||
*
|
||||
* 使用场景:
|
||||
* - AdminOperationLogService.createLog()方法的参数类型
|
||||
* - 记录管理员操作的详细信息
|
||||
*/
|
||||
export interface CreateLogParams {
|
||||
adminUserId: string;
|
||||
adminUsername: string;
|
||||
operationType: keyof typeof OPERATION_TYPES;
|
||||
targetType: string;
|
||||
targetId?: string;
|
||||
operationDescription: string;
|
||||
httpMethodPath: string;
|
||||
requestParams?: Record<string, any>;
|
||||
beforeData?: Record<string, any>;
|
||||
afterData?: Record<string, any>;
|
||||
operationResult: keyof typeof OPERATION_RESULTS;
|
||||
errorMessage?: string;
|
||||
errorCode?: string;
|
||||
durationMs: number;
|
||||
clientIp?: string;
|
||||
userAgent?: string;
|
||||
requestId: string;
|
||||
context?: Record<string, any>;
|
||||
isSensitive?: boolean;
|
||||
affectedRecords?: number;
|
||||
batchId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 日志查询参数接口
|
||||
*
|
||||
* 功能描述:
|
||||
* 定义查询管理员操作日志的过滤条件
|
||||
*
|
||||
* 使用场景:
|
||||
* - AdminOperationLogService.queryLogs()方法的参数类型
|
||||
* - 支持多维度的日志查询和过滤
|
||||
*/
|
||||
export interface LogQueryParams {
|
||||
adminUserId?: string;
|
||||
operationType?: string;
|
||||
targetType?: string;
|
||||
operationResult?: string;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
isSensitive?: boolean;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 日志统计信息接口
|
||||
*
|
||||
* 功能描述:
|
||||
* 定义管理员操作日志的统计数据结构
|
||||
*
|
||||
* 使用场景:
|
||||
* - AdminOperationLogService.getStatistics()方法的返回类型
|
||||
* - 提供操作统计和分析数据
|
||||
*/
|
||||
export interface LogStatistics {
|
||||
totalOperations: number;
|
||||
successfulOperations: number;
|
||||
failedOperations: number;
|
||||
operationsByType: Record<string, number>;
|
||||
operationsByTarget: Record<string, number>;
|
||||
operationsByAdmin: Record<string, number>;
|
||||
averageDuration: number;
|
||||
sensitiveOperations: number;
|
||||
uniqueAdmins: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员操作日志服务
|
||||
*
|
||||
* 功能描述:
|
||||
* - 记录管理员的所有数据库操作
|
||||
* - 提供操作日志的查询和统计功能
|
||||
* - 支持敏感操作的特殊标记
|
||||
* - 实现日志的自动清理和归档
|
||||
*
|
||||
* 职责分离:
|
||||
* - 日志记录:记录操作的详细信息
|
||||
* - 日志查询:提供灵活的日志查询接口
|
||||
* - 日志统计:生成操作统计报告
|
||||
* - 日志管理:自动清理和归档功能
|
||||
*
|
||||
* 主要方法:
|
||||
* - createLog() - 创建操作日志记录
|
||||
* - queryLogs() - 查询操作日志
|
||||
* - getLogById() - 获取单个日志详情
|
||||
* - getStatistics() - 获取操作统计
|
||||
* - getSensitiveOperations() - 获取敏感操作日志
|
||||
* - getAdminOperationHistory() - 获取管理员操作历史
|
||||
* - cleanupExpiredLogs() - 清理过期日志
|
||||
*
|
||||
* 使用场景:
|
||||
* - 管理员操作审计
|
||||
* - 安全监控和异常检测
|
||||
* - 系统操作统计分析
|
||||
*/
|
||||
@Injectable()
|
||||
export class AdminOperationLogService {
|
||||
private readonly logger = new Logger(AdminOperationLogService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(AdminOperationLog)
|
||||
private readonly logRepository: Repository<AdminOperationLog>,
|
||||
) {
|
||||
this.logger.log('AdminOperationLogService初始化完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建操作日志
|
||||
*
|
||||
* @param params 日志参数
|
||||
* @returns 创建的日志记录
|
||||
*/
|
||||
async createLog(params: CreateLogParams): Promise<AdminOperationLog> {
|
||||
try {
|
||||
const log = this.logRepository.create({
|
||||
admin_user_id: params.adminUserId,
|
||||
admin_username: params.adminUsername,
|
||||
operation_type: params.operationType,
|
||||
target_type: params.targetType,
|
||||
target_id: params.targetId,
|
||||
operation_description: params.operationDescription,
|
||||
http_method_path: params.httpMethodPath,
|
||||
request_params: params.requestParams,
|
||||
before_data: params.beforeData,
|
||||
after_data: params.afterData,
|
||||
operation_result: params.operationResult,
|
||||
error_message: params.errorMessage,
|
||||
error_code: params.errorCode,
|
||||
duration_ms: params.durationMs,
|
||||
client_ip: params.clientIp,
|
||||
user_agent: params.userAgent,
|
||||
request_id: params.requestId,
|
||||
context: params.context,
|
||||
is_sensitive: params.isSensitive || false,
|
||||
affected_records: params.affectedRecords || 0,
|
||||
batch_id: params.batchId,
|
||||
});
|
||||
|
||||
const savedLog = await this.logRepository.save(log);
|
||||
|
||||
this.logger.log('操作日志记录成功', {
|
||||
logId: savedLog.id,
|
||||
adminUserId: params.adminUserId,
|
||||
operationType: params.operationType,
|
||||
targetType: params.targetType,
|
||||
operationResult: params.operationResult
|
||||
});
|
||||
|
||||
return savedLog;
|
||||
} catch (error) {
|
||||
this.logger.error('操作日志记录失败', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
params
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建查询条件
|
||||
*
|
||||
* @param queryBuilder 查询构建器
|
||||
* @param params 查询参数
|
||||
*/
|
||||
private buildQueryConditions(queryBuilder: any, params: LogQueryParams): void {
|
||||
if (params.adminUserId) {
|
||||
queryBuilder.andWhere('log.admin_user_id = :adminUserId', { adminUserId: params.adminUserId });
|
||||
}
|
||||
|
||||
if (params.operationType) {
|
||||
queryBuilder.andWhere('log.operation_type = :operationType', { operationType: params.operationType });
|
||||
}
|
||||
|
||||
if (params.targetType) {
|
||||
queryBuilder.andWhere('log.target_type = :targetType', { targetType: params.targetType });
|
||||
}
|
||||
|
||||
if (params.operationResult) {
|
||||
queryBuilder.andWhere('log.operation_result = :operationResult', { operationResult: params.operationResult });
|
||||
}
|
||||
|
||||
if (params.startDate && params.endDate) {
|
||||
queryBuilder.andWhere('log.created_at BETWEEN :startDate AND :endDate', {
|
||||
startDate: params.startDate,
|
||||
endDate: params.endDate
|
||||
});
|
||||
}
|
||||
|
||||
if (params.isSensitive !== undefined) {
|
||||
queryBuilder.andWhere('log.is_sensitive = :isSensitive', { isSensitive: params.isSensitive });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询操作日志
|
||||
*
|
||||
* @param params 查询参数
|
||||
* @returns 日志列表和总数
|
||||
*/
|
||||
async queryLogs(params: LogQueryParams): Promise<{ logs: AdminOperationLog[]; total: number }> {
|
||||
try {
|
||||
const queryBuilder = this.logRepository.createQueryBuilder('log');
|
||||
|
||||
// 构建查询条件
|
||||
this.buildQueryConditions(queryBuilder, params);
|
||||
|
||||
// 排序
|
||||
queryBuilder.orderBy('log.created_at', 'DESC');
|
||||
|
||||
// 分页
|
||||
const limit = params.limit || LOG_QUERY_LIMITS.DEFAULT_LOG_QUERY_LIMIT;
|
||||
const offset = params.offset || 0;
|
||||
queryBuilder.limit(limit).offset(offset);
|
||||
|
||||
const [logs, total] = await queryBuilder.getManyAndCount();
|
||||
|
||||
this.logger.log('操作日志查询成功', {
|
||||
total,
|
||||
returned: logs.length,
|
||||
params
|
||||
});
|
||||
|
||||
return { logs, total };
|
||||
} catch (error) {
|
||||
this.logger.error('操作日志查询失败', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
params
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取操作日志详情
|
||||
*
|
||||
* @param id 日志ID
|
||||
* @returns 日志详情
|
||||
*/
|
||||
async getLogById(id: string): Promise<AdminOperationLog | null> {
|
||||
try {
|
||||
const log = await this.logRepository.findOne({ where: { id } });
|
||||
|
||||
if (log) {
|
||||
this.logger.log('操作日志详情获取成功', { logId: id });
|
||||
} else {
|
||||
this.logger.warn('操作日志不存在', { logId: id });
|
||||
}
|
||||
|
||||
return log;
|
||||
} catch (error) {
|
||||
this.logger.error('操作日志详情获取失败', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
logId: id
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取基础统计数据
|
||||
*
|
||||
* @param queryBuilder 查询构建器
|
||||
* @returns 基础统计数据
|
||||
*/
|
||||
private async getBasicStatistics(queryBuilder: any): Promise<{
|
||||
totalOperations: number;
|
||||
successfulOperations: number;
|
||||
failedOperations: number;
|
||||
sensitiveOperations: number;
|
||||
}> {
|
||||
const totalOperations = await queryBuilder.getCount();
|
||||
|
||||
const successfulOperations = await queryBuilder
|
||||
.clone()
|
||||
.andWhere('log.operation_result = :result', { result: OPERATION_RESULTS.SUCCESS })
|
||||
.getCount();
|
||||
|
||||
const failedOperations = totalOperations - successfulOperations;
|
||||
|
||||
const sensitiveOperations = await queryBuilder
|
||||
.clone()
|
||||
.andWhere('log.is_sensitive = :sensitive', { sensitive: true })
|
||||
.getCount();
|
||||
|
||||
return {
|
||||
totalOperations,
|
||||
successfulOperations,
|
||||
failedOperations,
|
||||
sensitiveOperations
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取操作类型统计
|
||||
*
|
||||
* @param queryBuilder 查询构建器
|
||||
* @returns 操作类型统计
|
||||
*/
|
||||
private async getOperationTypeStatistics(queryBuilder: any): Promise<Record<string, number>> {
|
||||
const operationTypeStats = await queryBuilder
|
||||
.clone()
|
||||
.select('log.operation_type', 'type')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.groupBy('log.operation_type')
|
||||
.getRawMany();
|
||||
|
||||
return operationTypeStats.reduce((acc, stat) => {
|
||||
acc[stat.type] = parseInt(stat.count);
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取目标类型统计
|
||||
*
|
||||
* @param queryBuilder 查询构建器
|
||||
* @returns 目标类型统计
|
||||
*/
|
||||
private async getTargetTypeStatistics(queryBuilder: any): Promise<Record<string, number>> {
|
||||
const targetTypeStats = await queryBuilder
|
||||
.clone()
|
||||
.select('log.target_type', 'type')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.groupBy('log.target_type')
|
||||
.getRawMany();
|
||||
|
||||
return targetTypeStats.reduce((acc, stat) => {
|
||||
acc[stat.type] = parseInt(stat.count);
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取管理员统计
|
||||
*
|
||||
* @param queryBuilder 查询构建器
|
||||
* @returns 管理员统计
|
||||
*/
|
||||
private async getAdminStatistics(queryBuilder: any): Promise<Record<string, number>> {
|
||||
const adminStats = await queryBuilder
|
||||
.clone()
|
||||
.select('log.admin_user_id', 'admin')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.groupBy('log.admin_user_id')
|
||||
.getRawMany();
|
||||
|
||||
if (!adminStats || !Array.isArray(adminStats)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return adminStats.reduce((acc, stat) => {
|
||||
acc[stat.admin] = parseInt(stat.count);
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取性能统计
|
||||
*
|
||||
* @param queryBuilder 查询构建器
|
||||
* @returns 性能统计
|
||||
*/
|
||||
private async getPerformanceStatistics(queryBuilder: any): Promise<{
|
||||
averageDuration: number;
|
||||
uniqueAdmins: number;
|
||||
}> {
|
||||
// 平均耗时
|
||||
const avgDurationResult = await queryBuilder
|
||||
.clone()
|
||||
.select('AVG(log.duration_ms)', 'avgDuration')
|
||||
.getRawOne();
|
||||
|
||||
const averageDuration = parseFloat(avgDurationResult?.avgDuration || '0');
|
||||
|
||||
// 唯一管理员数量
|
||||
const uniqueAdminsResult = await queryBuilder
|
||||
.clone()
|
||||
.select('COUNT(DISTINCT log.admin_user_id)', 'uniqueAdmins')
|
||||
.getRawOne();
|
||||
|
||||
const uniqueAdmins = parseInt(uniqueAdminsResult?.uniqueAdmins || '0');
|
||||
|
||||
return { averageDuration, uniqueAdmins };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取操作统计信息
|
||||
*
|
||||
* @param startDate 开始日期
|
||||
* @param endDate 结束日期
|
||||
* @returns 统计信息
|
||||
*/
|
||||
async getStatistics(startDate?: Date, endDate?: Date): Promise<LogStatistics> {
|
||||
try {
|
||||
const queryBuilder = this.logRepository.createQueryBuilder('log');
|
||||
|
||||
if (startDate && endDate) {
|
||||
queryBuilder.where('log.created_at BETWEEN :startDate AND :endDate', {
|
||||
startDate,
|
||||
endDate
|
||||
});
|
||||
}
|
||||
|
||||
// 获取各类统计数据
|
||||
const basicStats = await this.getBasicStatistics(queryBuilder);
|
||||
const operationsByType = await this.getOperationTypeStatistics(queryBuilder);
|
||||
const operationsByTarget = await this.getTargetTypeStatistics(queryBuilder);
|
||||
const operationsByAdmin = await this.getAdminStatistics(queryBuilder);
|
||||
const performanceStats = await this.getPerformanceStatistics(queryBuilder);
|
||||
|
||||
const statistics: LogStatistics = {
|
||||
...basicStats,
|
||||
operationsByType,
|
||||
operationsByTarget,
|
||||
operationsByAdmin,
|
||||
...performanceStats
|
||||
};
|
||||
|
||||
this.logger.log('操作统计获取成功', statistics);
|
||||
|
||||
return statistics;
|
||||
} catch (error) {
|
||||
this.logger.error('操作统计获取失败', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
startDate,
|
||||
endDate
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期日志
|
||||
*
|
||||
* @param daysToKeep 保留天数
|
||||
* @returns 清理的记录数
|
||||
*/
|
||||
async cleanupExpiredLogs(daysToKeep: number = LOG_RETENTION.DEFAULT_DAYS): Promise<number> {
|
||||
try {
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
|
||||
|
||||
const result = await this.logRepository
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.where('created_at < :cutoffDate', { cutoffDate })
|
||||
.andWhere('is_sensitive = :sensitive', { sensitive: false }) // 保留敏感操作日志
|
||||
.execute();
|
||||
|
||||
const deletedCount = result.affected || 0;
|
||||
|
||||
this.logger.log('过期日志清理完成', {
|
||||
deletedCount,
|
||||
cutoffDate,
|
||||
daysToKeep
|
||||
});
|
||||
|
||||
return deletedCount;
|
||||
} catch (error) {
|
||||
this.logger.error('过期日志清理失败', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
daysToKeep
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取管理员操作历史
|
||||
*
|
||||
* @param adminUserId 管理员用户ID
|
||||
* @param limit 限制数量
|
||||
* @returns 操作历史
|
||||
*/
|
||||
async getAdminOperationHistory(adminUserId: string, limit: number = USER_QUERY_LIMITS.ADMIN_HISTORY_DEFAULT_LIMIT): Promise<AdminOperationLog[]> {
|
||||
try {
|
||||
const logs = await this.logRepository.find({
|
||||
where: { admin_user_id: adminUserId },
|
||||
order: { created_at: 'DESC' },
|
||||
take: limit
|
||||
});
|
||||
|
||||
this.logger.log('管理员操作历史获取成功', {
|
||||
adminUserId,
|
||||
count: logs.length
|
||||
});
|
||||
|
||||
return logs;
|
||||
} catch (error) {
|
||||
this.logger.error('管理员操作历史获取失败', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
adminUserId
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取敏感操作日志
|
||||
*
|
||||
* @param limit 限制数量
|
||||
* @param offset 偏移量
|
||||
* @returns 敏感操作日志
|
||||
*/
|
||||
async getSensitiveOperations(limit: number = LOG_QUERY_LIMITS.SENSITIVE_LOG_DEFAULT_LIMIT, offset: number = 0): Promise<{ logs: AdminOperationLog[]; total: number }> {
|
||||
try {
|
||||
const [logs, total] = await this.logRepository.findAndCount({
|
||||
where: { is_sensitive: true },
|
||||
order: { created_at: 'DESC' },
|
||||
take: limit,
|
||||
skip: offset
|
||||
});
|
||||
|
||||
this.logger.log('敏感操作日志获取成功', {
|
||||
total,
|
||||
returned: logs.length
|
||||
});
|
||||
|
||||
return { logs, total };
|
||||
} catch (error) {
|
||||
this.logger.error('敏感操作日志获取失败', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
255
src/business/admin/admin_property_test.base.ts
Normal file
255
src/business/admin/admin_property_test.base.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* 管理员系统属性测试基础框架
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供属性测试的基础工具和断言
|
||||
* - 实现通用的测试数据生成器
|
||||
* - 支持随机化测试和边界条件验证
|
||||
*
|
||||
* 属性测试原理:
|
||||
* - 验证系统在各种输入条件下的通用正确性属性
|
||||
* - 通过大量随机测试用例发现边界问题
|
||||
* - 确保系统行为的一致性和可靠性
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-08: 注释规范优化 - 修正@author字段,更新版本号和修改记录 (修改者: moyin)
|
||||
* - 2026-01-08: 注释规范优化 - 为接口添加注释,完善文档说明 (修改者: moyin)
|
||||
* - 2026-01-08: 功能新增 - 创建属性测试基础框架 (修改者: assistant)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.2
|
||||
* @since 2026-01-08
|
||||
* @lastModified 2026-01-08
|
||||
*/
|
||||
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { UserStatus } from '../user_mgmt/user_status.enum';
|
||||
|
||||
/**
|
||||
* 属性测试配置接口
|
||||
*
|
||||
* 功能描述:
|
||||
* 定义属性测试的运行配置参数
|
||||
*
|
||||
* 使用场景:
|
||||
* - 配置属性测试的迭代次数和超时时间
|
||||
* - 设置随机种子以确保测试的可重现性
|
||||
*/
|
||||
export interface PropertyTestConfig {
|
||||
iterations: number;
|
||||
timeout: number;
|
||||
seed?: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_PROPERTY_CONFIG: PropertyTestConfig = {
|
||||
iterations: 100,
|
||||
timeout: 30000,
|
||||
seed: 12345
|
||||
};
|
||||
|
||||
/**
|
||||
* 属性测试生成器
|
||||
*/
|
||||
export class PropertyTestGenerators {
|
||||
/**
|
||||
* 生成随机用户数据
|
||||
*/
|
||||
static generateUser(seed?: number) {
|
||||
const random = seed ? Math.sin(seed) * 10000 - Math.floor(Math.sin(seed) * 10000) : Math.random();
|
||||
const id = Math.floor(random * 1000000);
|
||||
return {
|
||||
username: `testuser${id}`,
|
||||
nickname: `Test User ${id}`,
|
||||
email: `test${id}@example.com`,
|
||||
phone: `138${String(id).padStart(8, '0').substring(0, 8)}`,
|
||||
role: Math.floor(random * 10),
|
||||
status: ['ACTIVE', 'INACTIVE', 'SUSPENDED'][Math.floor(random * 3)] as any,
|
||||
avatar_url: `https://example.com/avatar${id}.jpg`,
|
||||
github_id: `github${id}`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机用户档案数据
|
||||
*/
|
||||
static generateUserProfile(seed?: number) {
|
||||
const random = seed ? Math.sin(seed) * 10000 - Math.floor(Math.sin(seed) * 10000) : Math.random();
|
||||
const id = Math.floor(random * 1000000);
|
||||
return {
|
||||
user_id: String(id),
|
||||
bio: `This is a test bio for user ${id}`,
|
||||
resume_content: `Test resume content for user ${id}. This is a sample resume.`,
|
||||
tags: JSON.stringify(['developer', 'tester']),
|
||||
social_links: JSON.stringify({
|
||||
github: `https://github.com/user${id}`,
|
||||
linkedin: `https://linkedin.com/in/user${id}`
|
||||
}),
|
||||
skin_id: `skin${id}`,
|
||||
current_map: ['plaza', 'forest', 'beach', 'mountain'][Math.floor(random * 4)],
|
||||
pos_x: random * 1000,
|
||||
pos_y: random * 1000,
|
||||
status: Math.floor(random * 3)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机Zulip账号数据
|
||||
*/
|
||||
static generateZulipAccount(seed?: number) {
|
||||
const random = seed ? Math.sin(seed) * 10000 - Math.floor(Math.sin(seed) * 10000) : Math.random();
|
||||
const id = Math.floor(random * 1000000);
|
||||
const statuses = ['active', 'inactive', 'suspended', 'error'] as const;
|
||||
return {
|
||||
gameUserId: String(id),
|
||||
zulipUserId: Math.floor(random * 999999) + 1,
|
||||
zulipEmail: `zulip${id}@example.com`,
|
||||
zulipFullName: `Zulip User ${id}`,
|
||||
zulipApiKeyEncrypted: `encrypted_key_${id}`,
|
||||
status: statuses[Math.floor(random * 4)]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机分页参数
|
||||
*/
|
||||
static generatePaginationParams(seed?: number) {
|
||||
const random = seed ? Math.sin(seed) * 10000 - Math.floor(Math.sin(seed) * 10000) : Math.random();
|
||||
return {
|
||||
limit: Math.floor(random * 100) + 1,
|
||||
offset: Math.floor(random * 1000)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成边界值测试数据
|
||||
*/
|
||||
static generateBoundaryValues() {
|
||||
return {
|
||||
limits: [0, 1, 50, 100, 101, 999, 1000],
|
||||
offsets: [0, 1, 100, 999, 1000, 9999],
|
||||
strings: ['', 'a', 'x'.repeat(50), 'x'.repeat(255), 'x'.repeat(256)],
|
||||
numbers: [-1, 0, 1, 999, 1000, 9999, 99999]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 属性测试断言工具
|
||||
*/
|
||||
export class PropertyTestAssertions {
|
||||
/**
|
||||
* 验证API响应格式一致性
|
||||
*/
|
||||
static assertApiResponseFormat(response: any, shouldHaveData: boolean = true) {
|
||||
expect(response).toHaveProperty('success');
|
||||
expect(response).toHaveProperty('message');
|
||||
expect(response).toHaveProperty('timestamp');
|
||||
expect(response).toHaveProperty('request_id');
|
||||
|
||||
expect(typeof response.success).toBe('boolean');
|
||||
expect(typeof response.message).toBe('string');
|
||||
expect(typeof response.timestamp).toBe('string');
|
||||
expect(typeof response.request_id).toBe('string');
|
||||
|
||||
if (shouldHaveData && response.success) {
|
||||
expect(response).toHaveProperty('data');
|
||||
}
|
||||
|
||||
if (!response.success) {
|
||||
expect(response).toHaveProperty('error_code');
|
||||
expect(typeof response.error_code).toBe('string');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证列表响应格式
|
||||
*/
|
||||
static assertListResponseFormat(response: any) {
|
||||
this.assertApiResponseFormat(response, true);
|
||||
|
||||
expect(response.data).toHaveProperty('items');
|
||||
expect(response.data).toHaveProperty('total');
|
||||
expect(response.data).toHaveProperty('limit');
|
||||
expect(response.data).toHaveProperty('offset');
|
||||
expect(response.data).toHaveProperty('has_more');
|
||||
|
||||
expect(Array.isArray(response.data.items)).toBe(true);
|
||||
expect(typeof response.data.total).toBe('number');
|
||||
expect(typeof response.data.limit).toBe('number');
|
||||
expect(typeof response.data.offset).toBe('number');
|
||||
expect(typeof response.data.has_more).toBe('boolean');
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证分页逻辑正确性
|
||||
*/
|
||||
static assertPaginationLogic(response: any, requestedLimit: number, requestedOffset: number) {
|
||||
this.assertListResponseFormat(response);
|
||||
|
||||
const { items, total, limit, offset, has_more } = response.data;
|
||||
|
||||
// 验证分页参数
|
||||
expect(limit).toBeLessThanOrEqual(100); // 最大限制
|
||||
expect(offset).toBeGreaterThanOrEqual(0);
|
||||
|
||||
// 验证has_more逻辑
|
||||
const expectedHasMore = offset + items.length < total;
|
||||
expect(has_more).toBe(expectedHasMore);
|
||||
|
||||
// 验证返回项目数量
|
||||
expect(items.length).toBeLessThanOrEqual(limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证CRUD操作一致性
|
||||
*/
|
||||
static assertCrudConsistency(createResponse: any, readResponse: any, updateResponse: any) {
|
||||
// 创建和读取的数据应该一致
|
||||
expect(createResponse.success).toBe(true);
|
||||
expect(readResponse.success).toBe(true);
|
||||
expect(createResponse.data.id).toBe(readResponse.data.id);
|
||||
|
||||
// 更新后的数据应该反映变更
|
||||
expect(updateResponse.success).toBe(true);
|
||||
expect(updateResponse.data.id).toBe(createResponse.data.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 属性测试运行器
|
||||
*/
|
||||
export class PropertyTestRunner {
|
||||
static async runPropertyTest<T>(
|
||||
testName: string,
|
||||
generator: () => T,
|
||||
testFunction: (input: T) => Promise<void>,
|
||||
config: PropertyTestConfig = DEFAULT_PROPERTY_CONFIG
|
||||
): Promise<void> {
|
||||
const logger = new Logger('PropertyTestRunner');
|
||||
logger.log(`Running property test: ${testName} with ${config.iterations} iterations`);
|
||||
|
||||
const failures: Array<{ iteration: number; input: T; error: any }> = [];
|
||||
|
||||
for (let i = 0; i < config.iterations; i++) {
|
||||
try {
|
||||
const input = generator();
|
||||
await testFunction(input);
|
||||
} catch (error) {
|
||||
failures.push({
|
||||
iteration: i,
|
||||
input: generator(), // 重新生成用于错误报告
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
const failureRate = (failures.length / config.iterations) * 100;
|
||||
logger.error(`Property test failed: ${failures.length}/${config.iterations} iterations failed (${failureRate.toFixed(2)}%)`);
|
||||
logger.error('First failure:', failures[0]);
|
||||
throw new Error(`Property test "${testName}" failed with ${failures.length} failures`);
|
||||
}
|
||||
|
||||
logger.log(`Property test "${testName}" passed all ${config.iterations} iterations`);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user