Compare commits
204 Commits
928c3700aa
...
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 | ||
|
|
55cfda0532 | ||
| dd856b9ba6 | |||
|
|
07601b6d79 | ||
|
|
7429de3cf4 | ||
|
|
0192934c66 | ||
|
|
64370c3206 | ||
|
|
a78df48101 | ||
|
|
0005dc773c | ||
|
|
946d328be6 | ||
|
|
841a58886e | ||
|
|
91565f716d | ||
| 417b01323e | |||
| b3de6dec5f | |||
|
|
d683f0d5da | ||
|
|
aae77866ac | ||
|
|
8a19bb7daa | ||
| a8e29c6a46 | |||
|
|
9f606abbb2 | ||
|
|
7385c63ffd | ||
| 8d5a44d985 | |||
|
|
d59e9531e2 | ||
| 28a39935b7 | |||
|
|
68debdcb40 | ||
|
|
9ad98f74d9 | ||
|
|
f6fa1ca1e3 | ||
| 578cba39a7 | |||
|
|
404ef5d3e0 | ||
| e537e782a9 | |||
|
|
cb25703892 | ||
| 64230db651 | |||
|
|
612755de63 | ||
|
|
e6d8c28806 | ||
|
|
47a738067a | ||
|
|
85d488a508 | ||
|
|
032c97a1fc | ||
| 0313b78852 | |||
|
|
d80d2c5cb8 | ||
| 2fb46967c7 | |||
|
|
9b35a1c500 | ||
|
|
43c9cbc863 | ||
|
|
a4a3a60db7 | ||
|
|
8166c95af4 | ||
|
|
ec2e346ded | ||
|
|
dd4fb6edd3 | ||
| 17c16588aa | |||
| 8fc2b53c00 | |||
| 4e2f46223e | |||
| f079a80b66 | |||
| bc16187fe0 | |||
|
|
b99b77e08b | ||
| 7afd9a52fa | |||
|
|
7924cfb201 | ||
| d322db242d | |||
|
|
d4a7b36129 | ||
| 243ca05028 | |||
|
|
3cfebbc4c4 | ||
|
|
6dece752ef | ||
| 76d794571c | |||
|
|
26ea5ac815 | ||
|
|
5161d614d0 | ||
|
|
11387f7046 |
142
.env.example
Normal file
142
.env.example
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
# 环境配置模板
|
||||||
|
# 复制此文件为 .env 并根据需要修改配置
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# 测试模式配置(开发/测试环境推荐)
|
||||||
|
# ===========================================
|
||||||
|
# 使用以下配置可以在没有数据库和邮件服务器的情况下进行测试
|
||||||
|
# 1. 复制此文件为 .env
|
||||||
|
# 2. 保持数据库和邮件配置为注释状态
|
||||||
|
# 3. 运行 npm run dev 启动服务
|
||||||
|
# 4. 运行测试脚本:./test-api.ps1 (Windows) 或 ./test-api.sh (Linux/macOS)
|
||||||
|
|
||||||
|
# 应用配置
|
||||||
|
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
|
||||||
|
|
||||||
|
# Redis 配置(测试模式:使用文件存储)
|
||||||
|
USE_FILE_REDIS=true
|
||||||
|
REDIS_HOST=localhost
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_PASSWORD=
|
||||||
|
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
|
||||||
|
|
||||||
|
# Redis 配置(生产环境取消注释并设置 USE_FILE_REDIS=false)
|
||||||
|
# USE_FILE_REDIS=false
|
||||||
|
# REDIS_HOST=your_redis_host
|
||||||
|
# REDIS_PORT=6379
|
||||||
|
# REDIS_PASSWORD=your_redis_password
|
||||||
|
# REDIS_DB=0
|
||||||
|
|
||||||
|
# 邮件服务配置(生产环境取消注释)
|
||||||
|
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
|
||||||
|
# LOG_LEVEL=info
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# Zulip 集成配置
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
|
# Zulip 配置模式
|
||||||
|
# static: 使用静态配置文件 (config/zulip/map-config.json)
|
||||||
|
# dynamic: 从Zulip服务器动态获取Stream作为地图
|
||||||
|
# hybrid: 混合模式,优先动态,回退静态 (推荐)
|
||||||
|
ZULIP_CONFIG_MODE=hybrid
|
||||||
|
|
||||||
|
# Zulip 服务器配置
|
||||||
|
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 错误处理配置
|
||||||
|
ZULIP_DEGRADED_MODE_ENABLED=false
|
||||||
|
ZULIP_AUTO_RECONNECT_ENABLED=true
|
||||||
|
ZULIP_MAX_RECONNECT_ATTEMPTS=5
|
||||||
|
ZULIP_RECONNECT_BASE_DELAY=5000
|
||||||
|
ZULIP_API_TIMEOUT=30000
|
||||||
|
ZULIP_MAX_RETRIES=3
|
||||||
|
|
||||||
|
# Zulip 连接限制配置
|
||||||
|
ZULIP_MAX_CONNECTIONS=100
|
||||||
|
ZULIP_SESSION_TIMEOUT=30
|
||||||
|
ZULIP_CLEANUP_INTERVAL=5
|
||||||
|
|
||||||
|
# Zulip 消息配置
|
||||||
|
ZULIP_MESSAGE_RATE_LIMIT=10
|
||||||
|
ZULIP_MESSAGE_MAX_LENGTH=10000
|
||||||
|
ZULIP_CONTENT_FILTER_ENABLED=true
|
||||||
|
# ZULIP_SENSITIVE_WORDS_PATH=config/zulip/sensitive-words.txt
|
||||||
|
|
||||||
|
# Zulip 允许的Stream列表(逗号分隔,空表示允许所有)
|
||||||
|
# ZULIP_ALLOWED_STREAMS=General,Novice Village,Tavern
|
||||||
|
|
||||||
|
# WebSocket配置
|
||||||
|
# WEBSOCKET_PORT=3000
|
||||||
|
# WEBSOCKET_NAMESPACE=/game
|
||||||
|
# WEBSOCKET_PING_INTERVAL=25000
|
||||||
|
# WEBSOCKET_PING_TIMEOUT=5000
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# 监控配置
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
|
# 健康检查间隔(毫秒)
|
||||||
|
MONITORING_HEALTH_CHECK_INTERVAL=60000
|
||||||
|
|
||||||
|
# 错误率阈值(0-1)
|
||||||
|
MONITORING_ERROR_RATE_THRESHOLD=0.1
|
||||||
|
|
||||||
|
# API响应时间阈值(毫秒)
|
||||||
|
MONITORING_RESPONSE_TIME_THRESHOLD=5000
|
||||||
|
|
||||||
|
# 内存使用阈值(0-1)
|
||||||
|
MONITORING_MEMORY_THRESHOLD=0.9
|
||||||
@@ -16,6 +16,16 @@ PORT=3000
|
|||||||
JWT_SECRET=your_jwt_secret_key_here_at_least_32_characters
|
JWT_SECRET=your_jwt_secret_key_here_at_least_32_characters
|
||||||
JWT_EXPIRES_IN=7d
|
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 配置(用于验证码存储)
|
||||||
# 生产环境使用真实Redis服务
|
# 生产环境使用真实Redis服务
|
||||||
USE_FILE_REDIS=false
|
USE_FILE_REDIS=false
|
||||||
|
|||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -44,3 +44,9 @@ coverage/
|
|||||||
|
|
||||||
# Redis数据文件(本地开发用)
|
# Redis数据文件(本地开发用)
|
||||||
redis-data/
|
redis-data/
|
||||||
|
|
||||||
|
.kiro/
|
||||||
|
|
||||||
|
config/
|
||||||
|
docs/merge-requests
|
||||||
|
docs/ai-reading/me.config.json
|
||||||
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 服务状态
|
|
||||||
- 验证数据库用户权限
|
|
||||||
- 检查网络连接
|
|
||||||
26
Dockerfile
26
Dockerfile
@@ -1,26 +0,0 @@
|
|||||||
# 使用官方 Node.js 镜像
|
|
||||||
FROM node:18-alpine
|
|
||||||
|
|
||||||
# 设置工作目录
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# 安装 pnpm
|
|
||||||
RUN npm install -g pnpm
|
|
||||||
|
|
||||||
# 复制 package.json 和 pnpm-lock.yaml
|
|
||||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
|
||||||
|
|
||||||
# 安装依赖
|
|
||||||
RUN pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
# 复制源代码
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# 构建应用
|
|
||||||
RUN pnpm run build
|
|
||||||
|
|
||||||
# 暴露端口
|
|
||||||
EXPOSE 3000
|
|
||||||
|
|
||||||
# 启动应用
|
|
||||||
CMD ["pnpm", "run", "start:prod"]
|
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Whale Town Team
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
519
README.md
519
README.md
@@ -1,341 +1,224 @@
|
|||||||
# Pixel Game Server
|
# 🐋 Whale Town - 像素游戏后端服务
|
||||||
|
|
||||||
一个基于 NestJS 的 2D 像素风游戏后端服务
|
> 基于 NestJS 的现代化 2D 像素游戏后端,采用四层架构(Gateway-Business-Core-Data),支持用户认证、实时通信、Zulip集成、管理员后台。
|
||||||
|
|
||||||
---
|
[](https://nodejs.org/)
|
||||||
|
[](https://nestjs.com/)
|
||||||
|
[](https://www.typescriptlang.org/)
|
||||||
|
[](https://reactjs.org/)
|
||||||
|
[](#)
|
||||||
|
|
||||||
## 🚨 开发者必读警告
|
## 🎯 核心特性
|
||||||
|
|
||||||
**⚠️ 在开始任何开发工作之前,请务必阅读:[AI 辅助开发规范指南](./docs/AI辅助开发规范指南.md)**
|
- 🔐 用户认证:多方式登录、验证码登录、GitHub OAuth
|
||||||
|
- 🌐 实时通信:原生WebSocket、位置广播、地图房间管理
|
||||||
|
- 💬 Zulip集成:游戏内聊天与Zulip社群双向同步
|
||||||
|
- 👑 管理员后台:React界面、用户管理、日志监控
|
||||||
|
- 🛡️ 安全防护:频率限制、维护模式、JWT认证
|
||||||
|
- 🗄️ 灵活存储:MySQL/内存双模式、Redis/文件双模式
|
||||||
|
- 📚 完整文档:Swagger UI、WebSocket测试工具
|
||||||
|
|
||||||
**📢 重要提醒:**
|
## 🚀 快速开始
|
||||||
- 🚫 **未阅读 AI 辅助指南的代码将无法通过审查**
|
|
||||||
- 🤖 **学会使用 AI 助手可以让你的开发效率提升 300%**
|
|
||||||
- 📝 **AI 可以帮你自动生成符合规范的代码和注释**
|
|
||||||
- 🔍 **AI 可以实时检查你的代码质量**
|
|
||||||
|
|
||||||
**👉 [立即阅读 AI 辅助开发指南](./docs/AI辅助开发规范指南.md)**
|
### 环境要求
|
||||||
|
- Node.js >= 18.0.0
|
||||||
|
- pnpm >= 8.0.0
|
||||||
|
|
||||||
---
|
### 安装运行
|
||||||
|
|
||||||
## 技术栈
|
|
||||||
|
|
||||||
- **NestJS** `^10.4.20` - 渐进式 Node.js 框架
|
|
||||||
- **TypeScript** `^5.9.3` - 类型安全
|
|
||||||
- **Socket.IO** - WebSocket 实时通信支持
|
|
||||||
- **RxJS** `^7.8.2` - 响应式编程库
|
|
||||||
- **Pino** `^10.1.0` - 高性能日志库
|
|
||||||
- **Jest** `^29.7.0` - 测试框架
|
|
||||||
|
|
||||||
### 核心依赖
|
|
||||||
|
|
||||||
**生产环境:**
|
|
||||||
- `@nestjs/common` `^10.4.20` - NestJS 核心功能
|
|
||||||
- `@nestjs/core` `^10.4.20` - NestJS 核心模块
|
|
||||||
- `@nestjs/config` `^4.0.2` - 配置管理
|
|
||||||
- `@nestjs/platform-express` `^10.4.20` - Express 平台适配器
|
|
||||||
- `@nestjs/websockets` `^10.4.20` - WebSocket 支持
|
|
||||||
- `@nestjs/platform-socket.io` `^10.4.20` - Socket.IO 适配器
|
|
||||||
- `nestjs-pino` `^4.5.0` - Pino 日志集成
|
|
||||||
- `pino` `^10.1.0` - 高性能日志库
|
|
||||||
- `reflect-metadata` `^0.1.14` - 装饰器元数据支持
|
|
||||||
- `rxjs` `^7.8.2` - 响应式编程
|
|
||||||
|
|
||||||
**开发环境:**
|
|
||||||
- `@nestjs/cli` `^10.4.9` - NestJS 命令行工具
|
|
||||||
- `@nestjs/schematics` `^10.2.3` - NestJS 代码生成器
|
|
||||||
- `@nestjs/testing` `^10.4.20` - 测试工具
|
|
||||||
- `@types/jest` `^29.5.14` - Jest 类型定义
|
|
||||||
- `@types/node` `^20.19.26` - Node.js 类型定义
|
|
||||||
- `jest` `^29.7.0` - 测试框架
|
|
||||||
- `ts-jest` `^29.2.5` - TypeScript Jest 支持
|
|
||||||
- `ts-node` `^10.9.2` - TypeScript 运行时
|
|
||||||
- `typescript` `^5.9.3` - TypeScript 编译器
|
|
||||||
- `pino-pretty` `^13.1.3` - Pino 美化输出
|
|
||||||
|
|
||||||
## 🚨 重要:开发前必读
|
|
||||||
|
|
||||||
### ⚠️ 所有开发者必须先阅读 AI 辅助开发指南
|
|
||||||
|
|
||||||
**在开始任何开发工作之前,请务必阅读:[AI 辅助开发规范指南](./docs/AI辅助开发规范指南.md)**
|
|
||||||
|
|
||||||
这个指南将教你如何:
|
|
||||||
- 🤖 使用 AI 助手遵循项目规范
|
|
||||||
- 📝 自动生成规范的代码和注释
|
|
||||||
- 🔍 实时检查代码质量
|
|
||||||
- 🚀 显著提高开发效率和代码质量
|
|
||||||
|
|
||||||
**不阅读此指南直接开发,代码审查将无法通过!**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 开发规范
|
|
||||||
|
|
||||||
### 命名规范
|
|
||||||
|
|
||||||
项目采用统一的命名规范,确保代码风格一致:
|
|
||||||
|
|
||||||
- **文件/文件夹**:下划线分隔(如 `order_controller.ts`)
|
|
||||||
- **变量/函数**:小驼峰命名(如 `userName`、`async queryUserInfo()`)
|
|
||||||
- **类/构造函数**:大驼峰命名(如 `UserModel`、`OrderService`)
|
|
||||||
- **常量**:全大写 + 下划线(如 `PORT`、`DB_HOST`)
|
|
||||||
- **接口路由**:全小写 + 短横线(如 `/user/get-info`、`/order/create-order`)
|
|
||||||
|
|
||||||
详细规范请查看:[命名规范文档](./docs/naming_convention.md)
|
|
||||||
|
|
||||||
### Git 提交规范
|
|
||||||
|
|
||||||
项目采用约定式提交规范,提交信息格式:`<类型>:<简短描述>`
|
|
||||||
|
|
||||||
**常用提交类型:**
|
|
||||||
|
|
||||||
- `feat` - 新增功能
|
|
||||||
- `fix` - 修复 Bug
|
|
||||||
- `docs` - 文档更新
|
|
||||||
- `style` - 代码格式调整
|
|
||||||
- `refactor` - 代码重构
|
|
||||||
- `perf` - 性能优化
|
|
||||||
- `test` - 测试相关
|
|
||||||
- `chore` - 构建/工具变动
|
|
||||||
|
|
||||||
**后端特定类型:**
|
|
||||||
|
|
||||||
- `api` - API 接口
|
|
||||||
- `db` - 数据库
|
|
||||||
- `websocket` - WebSocket
|
|
||||||
- `auth` - 认证授权
|
|
||||||
- `dto` - 数据传输对象
|
|
||||||
- `service` - 服务层
|
|
||||||
|
|
||||||
**核心原则:**
|
|
||||||
|
|
||||||
- ⭐ 一次提交只做一件事
|
|
||||||
- 使用中文冒号 `:`
|
|
||||||
- 简短明确(不超过 50 字符)
|
|
||||||
- 能拆分就拆分,保持提交历史清晰
|
|
||||||
|
|
||||||
**示例:**
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git commit -m "feat:实现玩家注册和登录功能"
|
# 1. 克隆项目
|
||||||
git commit -m "fix:修复房间加入时的并发问题"
|
git clone <repository-url>
|
||||||
git commit -m "api:添加玩家信息查询接口"
|
cd whale-town-end
|
||||||
```
|
|
||||||
|
|
||||||
详细规范请查看:[Git 提交规范文档](./docs/git_commit_guide.md)
|
# 2. 安装依赖
|
||||||
|
|
||||||
### 后端开发规范
|
|
||||||
|
|
||||||
项目要求严格的代码质量和可维护性标准:
|
|
||||||
|
|
||||||
**核心要求:**
|
|
||||||
|
|
||||||
- **完整注释**:每个模块、类、方法都必须有详细注释
|
|
||||||
- **全面业务逻辑**:考虑所有可能情况,包括异常和边界条件
|
|
||||||
- **关键日志记录**:重要操作必须记录日志,便于问题排查
|
|
||||||
- **防御性编程**:对所有输入进行验证,实现健壮的错误处理
|
|
||||||
|
|
||||||
**注释要求:**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
/**
|
|
||||||
* 玩家服务类
|
|
||||||
*
|
|
||||||
* 职责:处理玩家相关的业务逻辑
|
|
||||||
* 主要方法:createPlayer(), updatePlayerInfo(), getPlayerById()
|
|
||||||
* 使用场景:玩家注册登录流程、个人陈列室数据管理
|
|
||||||
*/
|
|
||||||
@Injectable()
|
|
||||||
export class PlayerService {
|
|
||||||
/**
|
|
||||||
* 创建新玩家
|
|
||||||
* @param email 玩家邮箱地址
|
|
||||||
* @param nickname 玩家昵称
|
|
||||||
* @returns Promise<Player> 创建成功的玩家对象
|
|
||||||
* @throws BadRequestException 当邮箱格式错误时
|
|
||||||
*/
|
|
||||||
async createPlayer(email: string, nickname: string): Promise<Player> {
|
|
||||||
// 详细的业务逻辑实现...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
详细规范请查看:[后端开发规范指南](./docs/backend_development_guide.md)
|
|
||||||
|
|
||||||
## 📚 开发文档
|
|
||||||
|
|
||||||
### 🔥 必读文档
|
|
||||||
- **[AI 辅助开发规范指南](./docs/AI辅助开发规范指南.md)** - 🚨 **所有开发者必读!** 教你如何使用 AI 遵循项目规范
|
|
||||||
|
|
||||||
### 📋 规范文档
|
|
||||||
- [后端开发规范](./docs/backend_development_guide.md) - 注释标准、业务逻辑设计和日志记录要求
|
|
||||||
- [Git 提交规范](./docs/git_commit_guide.md) - Git 提交信息格式和最佳实践
|
|
||||||
- [命名规范](./docs/naming_convention.md) - 项目命名规范和最佳实践
|
|
||||||
- [NestJS 使用指南](./docs/nestjs_guide.md) - 详细的 NestJS 开发指南,包含实战案例
|
|
||||||
|
|
||||||
### 📖 API 文档
|
|
||||||
- **[API 文档总览](./docs/api/README.md)** - API 文档使用指南和快速开始
|
|
||||||
- **[Swagger UI](http://localhost:3000/api-docs)** - 交互式 API 文档(需启动服务器)
|
|
||||||
- [详细接口文档](./docs/api/api-documentation.md) - 完整的 API 接口说明
|
|
||||||
- [OpenAPI 规范](./docs/api/openapi.yaml) - 标准化的 API 描述文件
|
|
||||||
- [Postman 集合](./docs/api/postman-collection.json) - 可导入的 API 测试集合
|
|
||||||
|
|
||||||
### 💡 使用建议
|
|
||||||
1. **开发前**:先读 AI 辅助指南,了解如何用 AI 帮助遵循规范
|
|
||||||
2. **开发中**:参考具体规范文档,使用 AI 实时检查代码质量
|
|
||||||
3. **API 开发**:使用 Swagger UI 进行接口测试,参考 API 文档进行开发
|
|
||||||
4. **提交前**:用 AI 检查代码和提交信息是否符合规范
|
|
||||||
|
|
||||||
## 前置要求
|
|
||||||
|
|
||||||
- **Node.js** >= 18.0.0 默认:24.7.0
|
|
||||||
- **pnpm** >= 8.0.0(推荐)默认:10.25.0
|
|
||||||
|
|
||||||
如果还没有安装 pnpm,请先安装:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install -g pnpm
|
|
||||||
```
|
|
||||||
|
|
||||||
检查版本:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
node --version
|
|
||||||
pnpm --version
|
|
||||||
```
|
|
||||||
|
|
||||||
## 安装依赖
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm install
|
pnpm install
|
||||||
```
|
|
||||||
|
|
||||||
## 开发
|
# 3. 配置环境(测试模式,无需数据库)
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
启动开发服务器(支持热重载):
|
# 4. 启动服务
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm dev
|
|
||||||
```
|
|
||||||
|
|
||||||
服务器将运行在 `http://localhost:3000`
|
|
||||||
|
|
||||||
## 测试
|
|
||||||
|
|
||||||
运行测试:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm test
|
|
||||||
```
|
|
||||||
|
|
||||||
运行测试并监听文件变化:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm test:watch
|
|
||||||
```
|
|
||||||
|
|
||||||
运行测试并生成覆盖率报告:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm test:cov
|
|
||||||
```
|
|
||||||
|
|
||||||
## 构建
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm build
|
|
||||||
```
|
|
||||||
|
|
||||||
## 生产环境运行
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm start:prod
|
|
||||||
```
|
|
||||||
|
|
||||||
## 项目结构
|
|
||||||
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
├── api/ # API 接口层(控制器、网关)
|
|
||||||
├── business/ # 业务逻辑层
|
|
||||||
├── core/ # 核心功能模块
|
|
||||||
│ ├── db/ # 数据库相关
|
|
||||||
│ └── utils/ # 工具函数
|
|
||||||
│ └── logger/ # 日志系统
|
|
||||||
├── main.ts # 应用入口
|
|
||||||
├── app.module.ts # 根模块
|
|
||||||
├── app.controller.ts # 根控制器
|
|
||||||
└── app.service.ts # 根服务
|
|
||||||
test/
|
|
||||||
├── api/ # API 测试
|
|
||||||
└── service/ # 服务测试
|
|
||||||
docs/ # 项目文档
|
|
||||||
├── api/ # API 接口文档
|
|
||||||
│ ├── README.md # API 文档使用指南
|
|
||||||
│ ├── api-documentation.md # 详细接口文档
|
|
||||||
│ ├── openapi.yaml # OpenAPI 规范文件
|
|
||||||
│ └── postman-collection.json # Postman 测试集合
|
|
||||||
├── systems/ # 系统设计文档
|
|
||||||
│ ├── logger/ # 日志系统文档
|
|
||||||
│ └── user-auth/ # 用户认证系统文档
|
|
||||||
├── backend_development_guide.md # 后端开发规范
|
|
||||||
├── git_commit_guide.md # Git 提交规范
|
|
||||||
├── naming_convention.md # 命名规范
|
|
||||||
├── nestjs_guide.md # NestJS 使用指南
|
|
||||||
└── AI辅助开发规范指南.md # AI 辅助开发指南
|
|
||||||
```
|
|
||||||
|
|
||||||
## 核心功能
|
|
||||||
|
|
||||||
### 🔐 用户认证系统
|
|
||||||
|
|
||||||
完整的用户认证解决方案,支持多种登录方式和安全特性:
|
|
||||||
|
|
||||||
- 用户名/邮箱/手机号登录
|
|
||||||
- GitHub OAuth 第三方登录
|
|
||||||
- 密码重置和修改功能
|
|
||||||
- bcrypt 密码加密
|
|
||||||
- 基于角色的权限控制
|
|
||||||
|
|
||||||
**详细文档**: [用户认证系统文档](./docs/systems/user-auth/README.md)
|
|
||||||
|
|
||||||
### 📖 API 文档系统
|
|
||||||
|
|
||||||
集成了完整的 API 文档解决方案,提供多种格式的接口文档:
|
|
||||||
|
|
||||||
- **Swagger UI** - 交互式 API 文档界面
|
|
||||||
- **OpenAPI 规范** - 标准化的 API 描述文件
|
|
||||||
- **Postman 集合** - 可导入的 API 测试集合
|
|
||||||
- **详细文档** - 包含示例和最佳实践的完整说明
|
|
||||||
|
|
||||||
**快速访问**:
|
|
||||||
```bash
|
|
||||||
# 启动服务器
|
|
||||||
pnpm run dev
|
pnpm run dev
|
||||||
|
|
||||||
# 访问 Swagger UI 文档
|
|
||||||
# 浏览器打开: http://localhost:3000/api-docs
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**详细文档**: [API 文档说明](./docs/api/README.md)
|
访问:http://localhost:3000
|
||||||
|
|
||||||
### 📊 日志系统
|
### 前端管理界面
|
||||||
|
|
||||||
基于 Pino 的高性能日志系统,提供结构化日志记录:
|
```bash
|
||||||
|
# 启动管理后台
|
||||||
|
cd client
|
||||||
|
pnpm install
|
||||||
|
pnpm run dev
|
||||||
|
```
|
||||||
|
|
||||||
- 高性能日志记录
|
访问:http://localhost:5173
|
||||||
- 自动敏感信息过滤
|
默认账号:admin / Admin123456
|
||||||
- 多级别日志控制
|
|
||||||
- 请求上下文绑定
|
|
||||||
|
|
||||||
**详细文档**: [日志系统文档](./docs/systems/logger/README.md)
|
### 在线体验
|
||||||
|
|
||||||
**💡 提示:使用 [AI 辅助开发指南](./docs/AI辅助开发规范指南.md) 可以让 AI 帮你自动生成符合规范的代码!**
|
- API文档:https://whaletownend.xinghangee.icu/api-docs
|
||||||
|
- WebSocket测试:https://whaletownend.xinghangee.icu/websocket-test
|
||||||
|
|
||||||
## 下一步
|
## 🏗️ 项目架构
|
||||||
|
|
||||||
- 在 `src/api/` 目录下创建游戏相关的控制器和网关
|
### 四层架构设计
|
||||||
- 在 `src/model/` 目录下定义游戏数据模型
|
|
||||||
- 在 `src/service/` 目录下实现游戏业务逻辑
|
```
|
||||||
- 使用 NestJS CLI 快速生成模块:`nest g module game`
|
Gateway Layer (网关层)
|
||||||
- 添加 WebSocket 网关实现实时游戏逻辑
|
↓ 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/ # 项目文档
|
||||||
|
├── test/ # 测试文件
|
||||||
|
└── config/ # 配置文件
|
||||||
|
```
|
||||||
|
|
||||||
|
详细架构:[docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md)
|
||||||
|
|
||||||
|
## 🛠️ 技术栈
|
||||||
|
|
||||||
|
**后端:** NestJS 11 + TypeScript 5 + MySQL + Redis + WebSocket
|
||||||
|
**前端:** React 18 + Vite 7 + Ant Design 5
|
||||||
|
**测试:** Jest + Supertest(99个测试用例)
|
||||||
|
**部署:** Docker + PM2 + Nginx
|
||||||
|
|
||||||
|
## 📊 开发命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 开发
|
||||||
|
pnpm run dev # 启动开发服务器
|
||||||
|
pnpm run build # 构建项目
|
||||||
|
pnpm run start:prod # 生产环境运行
|
||||||
|
|
||||||
|
# 测试
|
||||||
|
pnpm test # 运行单元测试
|
||||||
|
pnpm run test:cov # 测试覆盖率
|
||||||
|
.\test-comprehensive.ps1 # API功能测试
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🌍 环境配置
|
||||||
|
|
||||||
|
### 开发环境(默认)
|
||||||
|
```bash
|
||||||
|
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_HOST=your_redis_host
|
||||||
|
REDIS_PASSWORD=your_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
|
||||||
|
```
|
||||||
|
|
||||||
|
详细配置:[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) - 测试说明
|
||||||
|
|
||||||
|
### 🤖 AI代码检查指南
|
||||||
|
|
||||||
|
项目提供了完整的AI辅助代码检查流程,帮助确保代码质量和规范性。
|
||||||
|
|
||||||
|
**快速开始:**
|
||||||
|
|
||||||
|
向AI发送以下prompt开始代码检查:
|
||||||
|
|
||||||
|
```
|
||||||
|
请使用 docs/ai-reading 中readme的规范对 [模块路径] 进行完整的代码检查。
|
||||||
|
```
|
||||||
|
|
||||||
|
**如何使用:**
|
||||||
|
- AI会按照7个步骤逐步执行检查(命名规范、注释标准、代码质量、架构层级、测试覆盖、文档生成、代码提交)
|
||||||
|
- 每个步骤完成后会提供检查报告,等待确认后继续下一步
|
||||||
|
- 如有问题会自动修复并重新验证
|
||||||
|
- 这里建议每个步骤结束后,人工确认是否执行了修复,如果进行了修复,请告诉他:请重新执行一遍该步骤,看看是否有遗漏。
|
||||||
|
|
||||||
|
详细说明:[docs/ai-reading/README.md](./docs/ai-reading/README.md) | 开发者规范:[docs/开发者代码检查规范.md](./docs/开发者代码检查规范.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)
|
||||||
|
|
||||||
|
## 📝 版本历史
|
||||||
|
|
||||||
|
- **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)
|
||||||
|
|
||||||
|
## 📄 许可证
|
||||||
|
|
||||||
|
[MIT License](./LICENSE)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
**🐋 Whale Town - 让像素世界更精彩 !**
|
||||||
|
|
||||||
|
Made with ❤️ by the Whale Town Team
|
||||||
|
|
||||||
|
[⭐ Star](https://gitea.xinghangee.icu/datawhale/whale-town-end) | [🍴 Fork](https://gitea.xinghangee.icu/datawhale/whale-town-end/fork) | [📖 Docs](./docs/) | [🐛 Issues](https://gitea.xinghangee.icu/datawhale/whale-town-end/issues)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
# 验证码问题调试脚本
|
|
||||||
# 作者: moyin
|
|
||||||
# 日期: 2025-12-17
|
|
||||||
|
|
||||||
$baseUrl = "http://localhost:3000"
|
|
||||||
$testEmail = "debug@example.com"
|
|
||||||
|
|
||||||
Write-Host "=== 验证码问题调试脚本 ===" -ForegroundColor Green
|
|
||||||
|
|
||||||
# 步骤1: 发送验证码
|
|
||||||
Write-Host "`n1. 发送验证码..." -ForegroundColor Yellow
|
|
||||||
$sendBody = @{
|
|
||||||
email = $testEmail
|
|
||||||
} | ConvertTo-Json
|
|
||||||
|
|
||||||
try {
|
|
||||||
$sendResponse = Invoke-RestMethod -Uri "$baseUrl/auth/send-email-verification" -Method POST -Body $sendBody -ContentType "application/json"
|
|
||||||
Write-Host "发送响应: $($sendResponse | ConvertTo-Json -Depth 3)" -ForegroundColor Cyan
|
|
||||||
|
|
||||||
if ($sendResponse.success) {
|
|
||||||
Write-Host "✅ 验证码发送成功" -ForegroundColor Green
|
|
||||||
|
|
||||||
# 步骤2: 立即查看验证码调试信息
|
|
||||||
Write-Host "`n2. 查看验证码调试信息..." -ForegroundColor Yellow
|
|
||||||
$debugResponse = Invoke-RestMethod -Uri "$baseUrl/auth/debug-verification-code" -Method POST -Body $sendBody -ContentType "application/json"
|
|
||||||
Write-Host "调试信息: $($debugResponse | ConvertTo-Json -Depth 5)" -ForegroundColor Cyan
|
|
||||||
|
|
||||||
# 步骤3: 故意输入错误验证码
|
|
||||||
Write-Host "`n3. 测试错误验证码..." -ForegroundColor Yellow
|
|
||||||
$wrongVerifyBody = @{
|
|
||||||
email = $testEmail
|
|
||||||
verification_code = "000000"
|
|
||||||
} | ConvertTo-Json
|
|
||||||
|
|
||||||
try {
|
|
||||||
$wrongResponse = Invoke-RestMethod -Uri "$baseUrl/auth/verify-email" -Method POST -Body $wrongVerifyBody -ContentType "application/json"
|
|
||||||
Write-Host "错误验证响应: $($wrongResponse | ConvertTo-Json -Depth 3)" -ForegroundColor Red
|
|
||||||
} catch {
|
|
||||||
Write-Host "错误验证失败(预期): $($_.Exception.Message)" -ForegroundColor Yellow
|
|
||||||
if ($_.Exception.Response) {
|
|
||||||
$errorResponse = $_.Exception.Response.GetResponseStream()
|
|
||||||
$reader = New-Object System.IO.StreamReader($errorResponse)
|
|
||||||
$errorBody = $reader.ReadToEnd()
|
|
||||||
Write-Host "错误详情: $errorBody" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# 步骤4: 再次查看调试信息
|
|
||||||
Write-Host "`n4. 错误验证后的调试信息..." -ForegroundColor Yellow
|
|
||||||
$debugResponse2 = Invoke-RestMethod -Uri "$baseUrl/auth/debug-verification-code" -Method POST -Body $sendBody -ContentType "application/json"
|
|
||||||
Write-Host "调试信息: $($debugResponse2 | ConvertTo-Json -Depth 5)" -ForegroundColor Cyan
|
|
||||||
|
|
||||||
# 步骤5: 再次尝试错误验证码
|
|
||||||
Write-Host "`n5. 再次测试错误验证码..." -ForegroundColor Yellow
|
|
||||||
try {
|
|
||||||
$wrongResponse2 = Invoke-RestMethod -Uri "$baseUrl/auth/verify-email" -Method POST -Body $wrongVerifyBody -ContentType "application/json"
|
|
||||||
Write-Host "第二次错误验证响应: $($wrongResponse2 | ConvertTo-Json -Depth 3)" -ForegroundColor Red
|
|
||||||
} catch {
|
|
||||||
Write-Host "第二次错误验证失败: $($_.Exception.Message)" -ForegroundColor Yellow
|
|
||||||
if ($_.Exception.Response) {
|
|
||||||
$errorResponse = $_.Exception.Response.GetResponseStream()
|
|
||||||
$reader = New-Object System.IO.StreamReader($errorResponse)
|
|
||||||
$errorBody = $reader.ReadToEnd()
|
|
||||||
Write-Host "错误详情: $errorBody" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# 步骤6: 最终调试信息
|
|
||||||
Write-Host "`n6. 最终调试信息..." -ForegroundColor Yellow
|
|
||||||
$debugResponse3 = Invoke-RestMethod -Uri "$baseUrl/auth/debug-verification-code" -Method POST -Body $sendBody -ContentType "application/json"
|
|
||||||
Write-Host "最终调试信息: $($debugResponse3 | ConvertTo-Json -Depth 5)" -ForegroundColor Cyan
|
|
||||||
|
|
||||||
# 步骤7: 使用正确验证码(如果有的话)
|
|
||||||
if ($sendResponse.data.verification_code) {
|
|
||||||
Write-Host "`n7. 使用正确验证码..." -ForegroundColor Yellow
|
|
||||||
$correctVerifyBody = @{
|
|
||||||
email = $testEmail
|
|
||||||
verification_code = $sendResponse.data.verification_code
|
|
||||||
} | ConvertTo-Json
|
|
||||||
|
|
||||||
try {
|
|
||||||
$correctResponse = Invoke-RestMethod -Uri "$baseUrl/auth/verify-email" -Method POST -Body $correctVerifyBody -ContentType "application/json"
|
|
||||||
Write-Host "正确验证响应: $($correctResponse | ConvertTo-Json -Depth 3)" -ForegroundColor Green
|
|
||||||
} catch {
|
|
||||||
Write-Host "正确验证也失败了: $($_.Exception.Message)" -ForegroundColor Red
|
|
||||||
if ($_.Exception.Response) {
|
|
||||||
$errorResponse = $_.Exception.Response.GetResponseStream()
|
|
||||||
$reader = New-Object System.IO.StreamReader($errorResponse)
|
|
||||||
$errorBody = $reader.ReadToEnd()
|
|
||||||
Write-Host "错误详情: $errorBody" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
Write-Host "❌ 验证码发送失败: $($sendResponse.message)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
Write-Host "❌ 请求失败: $($_.Exception.Message)" -ForegroundColor Red
|
|
||||||
if ($_.Exception.Response) {
|
|
||||||
$errorResponse = $_.Exception.Response.GetResponseStream()
|
|
||||||
$reader = New-Object System.IO.StreamReader($errorResponse)
|
|
||||||
$errorBody = $reader.ReadToEnd()
|
|
||||||
Write-Host "错误详情: $errorBody" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "`n=== 调试完成 ===" -ForegroundColor Green
|
|
||||||
Write-Host "请查看上述输出,重点关注:" -ForegroundColor Yellow
|
|
||||||
Write-Host "1. TTL值的变化" -ForegroundColor White
|
|
||||||
Write-Host "2. attempts字段的变化" -ForegroundColor White
|
|
||||||
Write-Host "3. 验证码是否被意外删除" -ForegroundColor White
|
|
||||||
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,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:
|
|
||||||
518
docs/ARCHITECTURE.md
Normal file
518
docs/ARCHITECTURE.md
Normal file
@@ -0,0 +1,518 @@
|
|||||||
|
# 🏗️ Whale Town 项目架构设计
|
||||||
|
|
||||||
|
> 基于四层架构(Gateway-Business-Core-Data)的现代化后端设计,支持双模式运行,开发测试零依赖,生产部署高性能。
|
||||||
|
|
||||||
|
## 📋 目录
|
||||||
|
|
||||||
|
- [架构概述](#架构概述)
|
||||||
|
- [四层架构设计](#四层架构设计)
|
||||||
|
- [目录结构](#目录结构)
|
||||||
|
- [双模式架构](#双模式架构)
|
||||||
|
- [模块依赖关系](#模块依赖关系)
|
||||||
|
- [数据流向](#数据流向)
|
||||||
|
- [扩展指南](#扩展指南)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 架构概述
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四层架构设计
|
||||||
|
|
||||||
|
### 架构层次图
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 🌐 Gateway Layer (网关层) │
|
||||||
|
│ HTTP/WebSocket协议处理、数据验证、路由管理、认证守卫 │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
⬇️
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 🎯 Business Layer (业务层) │
|
||||||
|
│ 业务逻辑实现、服务协调、业务规则验证、事务管理 │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
⬇️
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ ⚙️ Core Layer (核心层) │
|
||||||
|
│ 技术基础设施、数据访问、外部系统集成、工具服务 │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
⬇️
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 🗄️ 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 (用户数据)
|
||||||
|
├── EmailModule (邮件服务)
|
||||||
|
├── VerificationModule (验证码服务)
|
||||||
|
├── LoginCoreModule (登录核心)
|
||||||
|
├── AdminCoreModule (管理员核心)
|
||||||
|
├── ZulipCoreModule (Zulip核心)
|
||||||
|
├── SecurityCoreModule (安全核心)
|
||||||
|
│
|
||||||
|
├── Gateway Layer
|
||||||
|
│ ├── AuthGatewayModule
|
||||||
|
│ └── LocationBroadcastGatewayModule
|
||||||
|
│
|
||||||
|
└── Business Layer
|
||||||
|
├── AuthModule
|
||||||
|
├── UserMgmtModule
|
||||||
|
├── AdminModule
|
||||||
|
├── ZulipModule
|
||||||
|
├── LocationBroadcastModule
|
||||||
|
└── NoticeModule
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 数据流向
|
||||||
|
|
||||||
|
### 用户登录流程
|
||||||
|
|
||||||
|
```
|
||||||
|
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)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 管理员操作流程
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 管理员请求 → AdminController (Gateway)
|
||||||
|
2. 权限验证 → AdminGuard
|
||||||
|
3. 业务逻辑 → AdminService (Business)
|
||||||
|
4. 核心服务 → AdminCoreService (Core)
|
||||||
|
5. 数据更新 → UsersService (Core)
|
||||||
|
6. 审计日志 → LoggerService (Core)
|
||||||
|
7. 返回响应 → 管理员收到结果
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 扩展指南
|
||||||
|
|
||||||
|
### 添加新的业务模块
|
||||||
|
|
||||||
|
1. **创建目录结构**
|
||||||
|
```bash
|
||||||
|
mkdir -p src/gateway/game
|
||||||
|
mkdir -p src/business/game
|
||||||
|
mkdir -p src/core/game_core
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **实现网关层**
|
||||||
|
```typescript
|
||||||
|
// src/gateway/game/game.controller.ts
|
||||||
|
@Controller('game')
|
||||||
|
export class GameController {
|
||||||
|
constructor(private readonly gameService: GameService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
async createGame(@Body() dto: CreateGameDto) {
|
||||||
|
return this.gameService.create(dto);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **实现核心层**
|
||||||
|
```typescript
|
||||||
|
// src/core/game_core/game_core.service.ts
|
||||||
|
@Injectable()
|
||||||
|
export class GameCoreService {
|
||||||
|
async createGame(dto: CreateGameDto) {
|
||||||
|
// 数据访问逻辑
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **注册模块**
|
||||||
|
```typescript
|
||||||
|
// src/app.module.ts
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
// ...
|
||||||
|
GameGatewayModule,
|
||||||
|
GameModule,
|
||||||
|
GameCoreModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 性能优化建议
|
||||||
|
|
||||||
|
1. **缓存策略**
|
||||||
|
- 用户会话 → Redis
|
||||||
|
- 验证码 → Redis(短期)
|
||||||
|
- 配置信息 → 内存缓存
|
||||||
|
|
||||||
|
2. **数据库优化**
|
||||||
|
- 添加索引
|
||||||
|
- 使用连接池
|
||||||
|
- 避免N+1查询
|
||||||
|
|
||||||
|
3. **日志优化**
|
||||||
|
- 异步写入
|
||||||
|
- 日志分级
|
||||||
|
- 日志轮转
|
||||||
|
|
||||||
|
### 安全加固建议
|
||||||
|
|
||||||
|
1. **数据验证**
|
||||||
|
- 使用class-validator
|
||||||
|
- TypeScript类型检查
|
||||||
|
- SQL注入防护
|
||||||
|
|
||||||
|
2. **认证授权**
|
||||||
|
- JWT认证
|
||||||
|
- 角色权限控制
|
||||||
|
- 会话管理
|
||||||
|
|
||||||
|
3. **通信安全**
|
||||||
|
- HTTPS强制
|
||||||
|
- CORS配置
|
||||||
|
- 频率限制
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 参考文档
|
||||||
|
|
||||||
|
- [架构重构文档](./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接口相关文档,包含:
|
- [项目概述](../README.md) - 项目介绍和快速开始指南
|
||||||
- **api-documentation.md** - 详细的API接口文档
|
- [架构设计](ARCHITECTURE.md) - 系统架构和设计理念
|
||||||
- **openapi.yaml** - OpenAPI 3.0规范文件
|
|
||||||
- **postman-collection.json** - Postman测试集合
|
|
||||||
- **README.md** - API文档使用说明
|
|
||||||
|
|
||||||
### 📁 systems/
|
### 🔌 **API文档**
|
||||||
系统设计文档,包含:
|
- [API接口文档](api/api-documentation.md) - 完整的API接口说明(17个接口)
|
||||||
- **logger/** - 日志系统文档
|
- [API状态码](API_STATUS_CODES.md) - HTTP状态码和错误代码说明
|
||||||
- **user-auth/** - 用户认证系统文档
|
- [OpenAPI规范](api/openapi.yaml) - 机器可读的API规范文件
|
||||||
|
- [API使用指南](api/README.md) - API文档使用说明
|
||||||
|
|
||||||
### 📄 其他文档
|
### 💻 **开发指南**
|
||||||
- **AI辅助开发规范指南.md** - AI开发规范
|
- [后端开发指南](development/backend_development_guide.md) - 后端开发规范和最佳实践
|
||||||
- **backend_development_guide.md** - 后端开发指南
|
- [NestJS指南](development/nestjs_guide.md) - NestJS框架使用指南
|
||||||
- **git_commit_guide.md** - Git提交规范
|
- [命名规范](development/naming_convention.md) - 代码命名规范
|
||||||
- **naming_convention.md** - 命名规范
|
- [Git提交规范](development/git_commit_guide.md) - Git提交消息规范
|
||||||
- **nestjs_guide.md** - NestJS开发指南
|
- [AI辅助开发规范](development/AI辅助开发规范指南.md) - AI辅助开发最佳实践
|
||||||
- **日志系统详细说明.md** - 日志系统说明
|
- [测试指南](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` 的内容复制粘贴到编辑器中
|
- **AI辅助** - 提升开发效率的AI使用指南
|
||||||
3. 即可查看可视化的API文档
|
- **测试覆盖** - 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"
|
|
||||||
}'
|
|
||||||
|
|
||||||
# 测试用户注册
|
📧 **联系我们**:如有文档相关问题,请通过项目Issue或邮件联系维护团队。
|
||||||
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/)
|
|
||||||
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接口文档
|
||||||
|
|
||||||
本目录包含了像素游戏服务器用户认证API的完整文档。
|
本目录包含了 Whale Town 像素游戏服务器的完整API文档,采用业务功能模块化设计,提供17个接口覆盖所有核心功能。
|
||||||
|
|
||||||
## 📋 文档文件说明
|
## 📋 文档文件说明
|
||||||
|
|
||||||
### 1. api-documentation.md
|
### 1. api-documentation.md
|
||||||
详细的API接口文档,包含:
|
详细的API接口文档,包含:
|
||||||
|
- **17个API接口** - 用户认证、用户管理、管理员功能、安全防护
|
||||||
- 接口概述和通用响应格式
|
- 接口概述和通用响应格式
|
||||||
- 每个接口的详细说明、参数、响应示例
|
- 每个接口的详细说明、参数、响应示例
|
||||||
- 错误代码说明
|
- 错误代码说明和状态码映射
|
||||||
- 数据验证规则
|
- 数据验证规则和业务逻辑
|
||||||
- 使用示例(JavaScript/TypeScript 和 cURL)
|
- 使用示例(JavaScript/TypeScript 和 cURL)
|
||||||
|
|
||||||
### 2. openapi.yaml
|
### 2. openapi.yaml
|
||||||
OpenAPI 3.0规范文件,可以用于:
|
OpenAPI 3.0规范文件,可以用于:
|
||||||
- 导入到Swagger Editor查看和编辑
|
- 导入到Swagger Editor查看和编辑
|
||||||
- 生成客户端SDK
|
- 生成客户端SDK(支持多种语言)
|
||||||
- 集成到API网关
|
- 集成到API网关和测试工具
|
||||||
- 自动化测试
|
- 自动化测试和文档生成
|
||||||
|
|
||||||
### 3. postman-collection.json
|
### 3. postman-collection.json
|
||||||
Postman集合文件,包含:
|
Postman集合文件,包含:
|
||||||
- 所有API接口的请求示例
|
- 所有17个API接口的请求示例
|
||||||
- 预设的请求参数
|
- 预设的请求参数和环境变量
|
||||||
- 响应示例
|
- 完整的响应示例和测试脚本
|
||||||
- 可直接导入Postman进行测试
|
- 可直接导入Postman进行测试
|
||||||
|
|
||||||
## 🚀 快速开始
|
## 🚀 快速开始
|
||||||
@@ -34,7 +35,7 @@ Postman集合文件,包含:
|
|||||||
# 启动开发服务器
|
# 启动开发服务器
|
||||||
pnpm run dev
|
pnpm run dev
|
||||||
|
|
||||||
# 访问Swagger UI
|
# 访问Swagger UI(推荐)
|
||||||
# 浏览器打开: http://localhost:3000/api-docs
|
# 浏览器打开: http://localhost:3000/api-docs
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -64,78 +65,144 @@ openapi-generator generate -i docs/api/openapi.yaml -g typescript-axios -o ./cli
|
|||||||
|
|
||||||
## 📊 API接口概览
|
## 📊 API接口概览
|
||||||
|
|
||||||
|
### 🔐 用户认证模块 (9个接口)
|
||||||
| 接口 | 方法 | 路径 | 描述 |
|
| 接口 | 方法 | 路径 | 描述 |
|
||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
| 用户登录 | POST | /auth/login | 支持用户名、邮箱或手机号登录 |
|
| 用户登录 | POST | /auth/login | 支持用户名、邮箱或手机号登录 |
|
||||||
| 用户注册 | POST | /auth/register | 创建新用户账户 |
|
| 用户注册 | POST | /auth/register | 创建新用户账户 |
|
||||||
| GitHub OAuth | POST | /auth/github | 使用GitHub账户登录或注册 |
|
| GitHub OAuth | POST | /auth/github | 使用GitHub账户登录或注册 |
|
||||||
| 发送验证码 | POST | /auth/forgot-password | 发送密码重置验证码 |
|
| 发送重置验证码 | POST | /auth/forgot-password | 发送密码重置验证码 |
|
||||||
| 重置密码 | POST | /auth/reset-password | 使用验证码重置密码 |
|
| 重置密码 | POST | /auth/reset-password | 使用验证码重置密码 |
|
||||||
| 修改密码 | PUT | /auth/change-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
|
```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 \
|
curl -X POST http://localhost:3000/auth/login \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
"identifier": "testuser",
|
"identifier": "testuser",
|
||||||
"password": "password123"
|
"password": "Test123456"
|
||||||
}'
|
}'
|
||||||
|
|
||||||
# 测试用户注册
|
# 4. 测试管理员登录
|
||||||
curl -X POST http://localhost:3000/auth/register \
|
curl -X POST http://localhost:3000/admin/auth/login \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
"username": "newuser",
|
"username": "admin",
|
||||||
"password": "password123",
|
"password": "Admin123456"
|
||||||
"nickname": "新用户",
|
|
||||||
"email": "newuser@example.com"
|
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
### 使用JavaScript测试
|
### 使用JavaScript测试
|
||||||
|
|
||||||
```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',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
identifier: 'testuser',
|
identifier: 'testuser',
|
||||||
password: 'password123'
|
password: 'Test123456'
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const loginData = await loginResponse.json();
|
||||||
console.log(data);
|
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
|
1. **开发环境**: 当前配置适用于开发环境,生产环境需要使用HTTPS
|
||||||
2. **认证**: 实际应用中应实现JWT认证机制
|
2. **认证机制**: 项目使用JWT认证,管理员使用独立的Token系统
|
||||||
3. **限流**: 建议对认证接口实施限流策略
|
3. **频率限制**: 已实现API频率限制,登录接口2次/分钟,管理员操作10次/分钟
|
||||||
4. **验证码**: 示例中返回验证码仅用于演示,生产环境不应返回
|
4. **用户状态**: 支持6种用户状态管理(active、inactive、locked、banned、deleted、pending)
|
||||||
5. **错误处理**: 建议实现统一的错误处理机制
|
5. **测试模式**: 邮件服务支持测试模式,验证码会在控制台输出
|
||||||
|
6. **存储模式**: 支持Redis文件存储和内存数据库,便于无依赖测试
|
||||||
|
7. **安全防护**: 实现了维护模式、内容类型检查、超时控制等安全机制
|
||||||
|
|
||||||
## 🔄 更新文档
|
## 🔄 更新文档
|
||||||
|
|
||||||
当API接口发生变化时,请同步更新以下文件:
|
当API接口发生变化时,请同步更新以下文件:
|
||||||
1. 更新DTO类的Swagger装饰器
|
1. 更新Controller和DTO类的Swagger装饰器
|
||||||
2. 更新 `api-documentation.md`
|
2. 更新 `api-documentation.md` 接口文档
|
||||||
3. 更新 `openapi.yaml`
|
3. 更新 `openapi.yaml` 规范文件
|
||||||
4. 更新 `postman-collection.json`
|
4. 更新 `postman-collection.json` 测试集合
|
||||||
5. 重新生成Swagger文档
|
5. 重新生成Swagger文档并验证
|
||||||
|
|
||||||
## 🔗 相关链接
|
## 🔗 相关链接
|
||||||
|
|
||||||
- [NestJS Swagger文档](https://docs.nestjs.com/openapi/introduction)
|
- [NestJS Swagger文档](https://docs.nestjs.com/openapi/introduction)
|
||||||
- [OpenAPI规范](https://swagger.io/specification/)
|
- [OpenAPI规范](https://swagger.io/specification/)
|
||||||
- [Postman文档](https://learning.postman.com/docs/getting-started/introduction/)
|
- [Postman文档](https://learning.postman.com/docs/getting-started/introduction/)
|
||||||
- [Swagger Editor](https://editor.swagger.io/)
|
- [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) | 注释、日志、业务逻辑 | 注释完整性、日志规范 |
|
| [后端开发规范](./backend_development_guide.md) | 注释、日志、业务逻辑 | 注释完整性、日志规范 |
|
||||||
| [NestJS 使用指南](./nestjs_guide.md) | 框架最佳实践 | 架构设计、代码组织 |
|
| [NestJS 使用指南](./nestjs_guide.md) | 框架最佳实践 | 架构设计、代码组织 |
|
||||||
|
|
||||||
|
**📝 重要:修改记录注释规范**
|
||||||
|
|
||||||
|
当对现有文件进行修改时,必须在文件头注释中添加修改记录,格式如下:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* 文件功能描述
|
||||||
|
*
|
||||||
|
* 最近修改:
|
||||||
|
* - YYYY-MM-DD: 修改类型 - 具体修改内容描述
|
||||||
|
* - YYYY-MM-DD: 修改类型 - 具体修改内容描述
|
||||||
|
*
|
||||||
|
* @author 原作者
|
||||||
|
* @version x.x.x (修改后递增版本号)
|
||||||
|
* @since 创建日期
|
||||||
|
* @lastModified 最后修改日期
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
**📏 重要限制:修改记录只保留最近5次修改,超出时删除最旧记录,保持注释简洁。**
|
||||||
|
|
||||||
|
**修改类型包括:**
|
||||||
|
- `代码规范优化` - 命名规范、注释规范、代码清理等
|
||||||
|
- `功能新增` - 添加新的功能或方法
|
||||||
|
- `功能修改` - 修改现有功能的实现
|
||||||
|
- `Bug修复` - 修复代码缺陷
|
||||||
|
- `性能优化` - 提升代码性能
|
||||||
|
- `重构` - 代码结构调整但功能不变
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🤖 AI 辅助开发工作流程
|
## 🤖 AI 辅助开发工作流程
|
||||||
@@ -89,6 +118,7 @@
|
|||||||
- 模块级注释(功能描述、依赖模块、作者、版本)
|
- 模块级注释(功能描述、依赖模块、作者、版本)
|
||||||
- 类级注释(职责、主要方法、使用场景)
|
- 类级注释(职责、主要方法、使用场景)
|
||||||
- 方法级注释(功能描述、业务逻辑、参数、返回值、异常、示例)
|
- 方法级注释(功能描述、业务逻辑、参数、返回值、异常、示例)
|
||||||
|
- 修改记录注释(如果是修改现有文件,添加修改记录和更新版本号)
|
||||||
|
|
||||||
2. 按照命名规范:
|
2. 按照命名规范:
|
||||||
- 类名使用大驼峰
|
- 类名使用大驼峰
|
||||||
@@ -229,6 +259,7 @@
|
|||||||
□ 模块级注释(功能描述、依赖模块、作者、版本)
|
□ 模块级注释(功能描述、依赖模块、作者、版本)
|
||||||
□ 类级注释(职责、主要方法、使用场景)
|
□ 类级注释(职责、主要方法、使用场景)
|
||||||
□ 方法级注释(功能描述、业务逻辑、参数、返回值、异常、示例)
|
□ 方法级注释(功能描述、业务逻辑、参数、返回值、异常、示例)
|
||||||
|
□ 修改记录注释(如果是修改现有文件,必须添加修改记录和更新版本号,只保留最近5次修改)
|
||||||
□ 文件命名使用下划线分隔
|
□ 文件命名使用下划线分隔
|
||||||
□ 类名使用大驼峰命名
|
□ 类名使用大驼峰命名
|
||||||
□ 方法名使用小驼峰命名
|
□ 方法名使用小驼峰命名
|
||||||
@@ -312,7 +343,50 @@ AI 会生成包含完整注释和异常处理的代码。
|
|||||||
请按照 Git 提交规范生成提交信息。
|
请按照 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. 异常处理模板
|
3. 异常处理模板
|
||||||
4. 日志记录模板
|
4. 日志记录模板
|
||||||
5. 参数验证模板
|
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 提交规范
|
||||||
|
|
||||||
本文档定义了项目的 Git 提交信息格式规范,以保持提交历史的清晰和一致性。
|
本文档定义了项目的 Git 提交信息格式规范,以保持提交历史的清晰和一致性。
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
- [常量命名](#常量命名)
|
- [常量命名](#常量命名)
|
||||||
- [接口路由命名](#接口路由命名)
|
- [接口路由命名](#接口路由命名)
|
||||||
- [TypeScript 特定规范](#typescript-特定规范)
|
- [TypeScript 特定规范](#typescript-特定规范)
|
||||||
|
- [注释命名规范](#注释命名规范)
|
||||||
- [命名示例](#命名示例)
|
- [命名示例](#命名示例)
|
||||||
|
|
||||||
## 文件和文件夹命名
|
## 文件和文件夹命名
|
||||||
@@ -331,6 +332,111 @@ class Repository<type, key> { }
|
|||||||
@IsString({ message: 'name_must_be_string' })
|
@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 前缀
|
- [ ] 布尔变量使用 is/has/can 前缀
|
||||||
- [ ] 避免使用无意义的缩写
|
- [ ] 避免使用无意义的缩写
|
||||||
|
- [ ] 注释使用标准JSDoc标签
|
||||||
|
- [ ] 修改记录使用标准化修改类型
|
||||||
|
- [ ] 版本号遵循语义化版本规范
|
||||||
|
- [ ] 修改现有文件时添加了修改记录和更新版本号
|
||||||
|
- [ ] 修改记录只保留最近5次,保持注释简洁
|
||||||
|
|
||||||
## 工具配置
|
## 工具配置
|
||||||
|
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
- [WebSocket 实时通信](#websocket-实时通信)
|
- [WebSocket 实时通信](#websocket-实时通信)
|
||||||
- [数据验证](#数据验证)
|
- [数据验证](#数据验证)
|
||||||
- [异常处理](#异常处理)
|
- [异常处理](#异常处理)
|
||||||
|
- [注释规范](#注释规范)
|
||||||
|
|
||||||
## 核心概念
|
## 核心概念
|
||||||
|
|
||||||
@@ -453,6 +454,142 @@ export class RoomController {
|
|||||||
7. **日志记录**:使用内置 Logger 或集成第三方日志库
|
7. **日志记录**:使用内置 Logger 或集成第三方日志库
|
||||||
8. **测试**:编写单元测试和 E2E 测试
|
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/)
|
- [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. **用户管理**
|
|
||||||
- 用户状态管理(激活/禁用)
|
|
||||||
- 用户角色权限细化
|
|
||||||
- 用户行为日志记录
|
|
||||||
386
docs/systems/zulip/README.md
Normal file
386
docs/systems/zulip/README.md
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
# Zulip 集成系统文档
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
Zulip 集成系统是一个为 2D 社交 MMO 游戏设计的跨平台聊天解决方案。该系统实现了游戏内外的无缝互通,让不玩游戏的 Zulip 社群成员也能与游戏内玩家实时交流。
|
||||||
|
|
||||||
|
### 核心设计理念
|
||||||
|
|
||||||
|
系统采用 **统一网关 (Unified Gateway)** 架构,利用 Zulip 的 Stream-Topic 线程模型与游戏世界的空间概念进行映射:
|
||||||
|
|
||||||
|
| 游戏概念 | Zulip 概念 | 示例 |
|
||||||
|
|---------|-----------|------|
|
||||||
|
| Game World / Map | Stream | #Novice_Village |
|
||||||
|
| Interactive Object / Event | Topic | Notice Board, Tavern Gossip |
|
||||||
|
| Whisper / Party | Private Message | 私聊消息 |
|
||||||
|
|
||||||
|
### 架构优势
|
||||||
|
|
||||||
|
1. **客户端极度简化**: Godot 客户端无需处理 HTTP 请求、Long Polling 或复杂 JSON 解析
|
||||||
|
2. **安全性**: Zulip API Key 永不下发到客户端,位置欺诈完全消除
|
||||||
|
3. **协议统一**: 单一 WebSocket 协议,网络层代码减半
|
||||||
|
|
||||||
|
## 系统架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐ WebSocket ┌─────────────────────────────────────┐
|
||||||
|
│ Godot Client │◄──────────────────►│ NestJS 中间件服务器 │
|
||||||
|
│ (Game Client) │ Game Protocol │ ┌─────────────────────────────────┐│
|
||||||
|
└─────────────────┘ │ │ WebSocket Gateway ││
|
||||||
|
│ │ ├─ Session Manager ││
|
||||||
|
│ │ ├─ Message Filter ││
|
||||||
|
│ │ └─ Zulip Client Pool ││
|
||||||
|
│ └─────────────────────────────────┘│
|
||||||
|
└──────────────┬──────────────────────┘
|
||||||
|
│ REST API / Long Polling
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ Zulip Server │
|
||||||
|
│ ├─ REST API │
|
||||||
|
│ └─ Event Queue │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 核心组件
|
||||||
|
|
||||||
|
### 1. WebSocket Gateway (`zulip-websocket.gateway.ts`)
|
||||||
|
|
||||||
|
统一网关,处理所有 Godot 客户端连接,实现游戏协议到 Zulip 协议的转换。
|
||||||
|
|
||||||
|
**主要功能:**
|
||||||
|
- 连接认证和会话管理
|
||||||
|
- 消息路由和协议转换
|
||||||
|
- 权限控制和上下文注入
|
||||||
|
|
||||||
|
**支持的消息类型:**
|
||||||
|
- `login`: 玩家登录
|
||||||
|
- `chat`: 发送聊天消息
|
||||||
|
- `position_update`: 位置更新
|
||||||
|
- `logout`: 玩家登出
|
||||||
|
|
||||||
|
### 2. Session Manager (`session-manager.service.ts`)
|
||||||
|
|
||||||
|
会话管理器,维护 Socket_ID 与 Zulip_Queue_ID 的绑定关系。
|
||||||
|
|
||||||
|
**主要功能:**
|
||||||
|
- 会话创建/销毁
|
||||||
|
- 玩家位置跟踪
|
||||||
|
- 上下文注入(根据位置确定 Stream/Topic)
|
||||||
|
- 空间过滤(获取指定地图的所有 Socket)
|
||||||
|
|
||||||
|
### 3. Zulip Client Pool (`zulip-client-pool.service.ts`)
|
||||||
|
|
||||||
|
Zulip 客户端池,为每个用户维护专用的 Zulip 客户端实例。
|
||||||
|
|
||||||
|
**主要功能:**
|
||||||
|
- API Key 管理
|
||||||
|
- 事件队列注册
|
||||||
|
- 消息发送/接收
|
||||||
|
- 客户端生命周期管理
|
||||||
|
|
||||||
|
### 4. Message Filter (`message-filter.service.ts`)
|
||||||
|
|
||||||
|
消息过滤器,实施内容审核和频率控制。
|
||||||
|
|
||||||
|
**主要功能:**
|
||||||
|
- 敏感词过滤
|
||||||
|
- 频率限制(默认 10 条/分钟)
|
||||||
|
- 消息长度限制(默认 1000 字符)
|
||||||
|
- 重复内容检测
|
||||||
|
- 权限验证
|
||||||
|
|
||||||
|
### 5. Config Manager (`config-manager.service.ts`)
|
||||||
|
|
||||||
|
配置管理器,管理地图映射配置和系统参数。
|
||||||
|
|
||||||
|
**主要功能:**
|
||||||
|
- 地图到 Stream 的映射
|
||||||
|
- 交互对象到 Topic 的映射
|
||||||
|
- 配置热重载
|
||||||
|
- 配置验证
|
||||||
|
|
||||||
|
### 6. Stream Initializer Service (`stream-initializer.service.ts`)
|
||||||
|
|
||||||
|
Stream 初始化服务,在系统启动时自动检查并创建缺失的 Zulip Streams。
|
||||||
|
|
||||||
|
**主要功能:**
|
||||||
|
- 启动时自动检查所有地图对应的 Streams
|
||||||
|
- 自动创建缺失的 Streams
|
||||||
|
- 使用 Bot API Key 或管理员账号创建 Streams
|
||||||
|
- 记录初始化结果和错误
|
||||||
|
|
||||||
|
**权限说明:**
|
||||||
|
- Bot 账号可能缺少创建 Stream 的权限
|
||||||
|
- 建议使用管理员账号手动创建 Streams
|
||||||
|
- 或在 Zulip 服务器中为 Bot 授予相应权限
|
||||||
|
|
||||||
|
### 7. Monitoring Service (`monitoring.service.ts`)
|
||||||
|
|
||||||
|
监控服务,提供系统健康检查和指标收集。
|
||||||
|
|
||||||
|
**主要功能:**
|
||||||
|
- 连接指标监控
|
||||||
|
- 消息指标监控
|
||||||
|
- 系统健康检查
|
||||||
|
- 告警通知
|
||||||
|
|
||||||
|
## 消息协议
|
||||||
|
|
||||||
|
### 客户端发送格式
|
||||||
|
|
||||||
|
#### 登录消息
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "login",
|
||||||
|
"token": "user_game_token"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 聊天消息
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"t": "chat",
|
||||||
|
"content": "Hello",
|
||||||
|
"scope": "local"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 位置更新
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"t": "position",
|
||||||
|
"x": 100,
|
||||||
|
"y": 200,
|
||||||
|
"mapId": "novice_village"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 客户端接收格式
|
||||||
|
|
||||||
|
#### 聊天渲染消息
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"t": "chat_render",
|
||||||
|
"from": "User_B",
|
||||||
|
"txt": "Hi",
|
||||||
|
"bubble": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 登录确认
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"t": "login_success",
|
||||||
|
"sessionId": "session_123",
|
||||||
|
"currentMap": "novice_village"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 错误消息
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"t": "error",
|
||||||
|
"code": "RATE_LIMIT",
|
||||||
|
"message": "消息发送过于频繁,请稍后再试"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置说明
|
||||||
|
|
||||||
|
### 环境变量配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Zulip 服务器配置
|
||||||
|
ZULIP_SERVER_URL=https://your-zulip-server.com
|
||||||
|
ZULIP_BOT_EMAIL=bot@your-zulip-server.com
|
||||||
|
ZULIP_BOT_API_KEY=your-bot-api-key
|
||||||
|
|
||||||
|
# WebSocket 配置
|
||||||
|
WEBSOCKET_PORT=3001
|
||||||
|
WEBSOCKET_NAMESPACE=/game
|
||||||
|
|
||||||
|
# 消息配置
|
||||||
|
MESSAGE_RATE_LIMIT=10 # 消息频率限制(条/分钟)
|
||||||
|
MESSAGE_MAX_LENGTH=1000 # 消息最大长度
|
||||||
|
|
||||||
|
# 会话配置
|
||||||
|
SESSION_TIMEOUT=30 # 会话超时时间(分钟)
|
||||||
|
CLEANUP_INTERVAL=5 # 清理间隔(分钟)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stream 初始化
|
||||||
|
|
||||||
|
系统在启动时会自动检查并尝试创建缺失的 Zulip Streams。
|
||||||
|
|
||||||
|
**注意事项:**
|
||||||
|
|
||||||
|
- Bot 账号可能缺少创建 Stream 的权限
|
||||||
|
- 建议使用管理员账号预先创建所有 Streams
|
||||||
|
- 或在 Zulip 服务器中为 Bot 授予 "Create streams" 权限
|
||||||
|
|
||||||
|
**手动创建 Streams:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 使用测试脚本创建所有地图区域的 Streams
|
||||||
|
node test-stream-initialization.js
|
||||||
|
```
|
||||||
|
|
||||||
|
详细配置说明请参考 [配置管理指南](./configuration.md)。
|
||||||
|
|
||||||
|
### 地图映射配置
|
||||||
|
|
||||||
|
配置文件位置: `config/zulip/map-config.json`
|
||||||
|
|
||||||
|
系统支持 9 个地图区域,每个区域对应一个 Zulip Stream:
|
||||||
|
|
||||||
|
1. **鲸之港 (Whale Port)** - 中心城区,默认出生点
|
||||||
|
2. **南瓜谷 (Pumpkin Valley)** - 新手学习区
|
||||||
|
3. **Offer 城 (Offer City)** - 职业发展区
|
||||||
|
4. **模型工厂 (Model Factory)** - AI/代码构建区
|
||||||
|
5. **内核岛 (Kernel Island)** - 核心技术研究区
|
||||||
|
6. **摸鱼海滩 (Moyu Beach)** - 休闲娱乐区
|
||||||
|
7. **天梯峰 (Ladder Peak)** - 挑战竞赛区
|
||||||
|
8. **星河湾 (Galaxy Bay)** - 创意设计区
|
||||||
|
9. **数据遗迹 (Data Ruins)** - 数据库归档区
|
||||||
|
|
||||||
|
配置示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"description": "基于像素大地图的 Zulip 映射配置",
|
||||||
|
"maps": [
|
||||||
|
{
|
||||||
|
"mapId": "whale_port",
|
||||||
|
"mapName": "鲸之港",
|
||||||
|
"zulipStream": "Whale Port",
|
||||||
|
"description": "中心城区,交通枢纽与主要聚会点",
|
||||||
|
"interactionObjects": [
|
||||||
|
{
|
||||||
|
"objectId": "whale_statue",
|
||||||
|
"objectName": "鲸鱼雕像",
|
||||||
|
"zulipTopic": "Announcements",
|
||||||
|
"position": { "x": 600, "y": 400 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据流程
|
||||||
|
|
||||||
|
### 发送消息流程 (游戏 → Zulip)
|
||||||
|
|
||||||
|
1. 玩家在游戏中输入消息
|
||||||
|
2. Godot 客户端通过 WebSocket 发送 `chat` 消息
|
||||||
|
3. WebSocket Gateway 接收消息
|
||||||
|
4. Session Manager 获取玩家当前位置
|
||||||
|
5. 上下文注入:根据位置确定目标 Stream/Topic
|
||||||
|
6. Message Filter 进行内容过滤和频率检查
|
||||||
|
7. Zulip Client Pool 使用用户的 API Key 发送消息到 Zulip
|
||||||
|
8. 返回发送确认给客户端
|
||||||
|
|
||||||
|
### 接收消息流程 (Zulip → 游戏)
|
||||||
|
|
||||||
|
1. Zulip 服务器推送消息事件到 Event Queue
|
||||||
|
2. Zulip Event Processor 接收并处理事件
|
||||||
|
3. Session Manager 进行空间过滤,确定目标玩家
|
||||||
|
4. 消息转换为游戏协议格式
|
||||||
|
5. WebSocket Gateway 推送 `chat_render` 消息给目标客户端
|
||||||
|
6. Godot 客户端显示聊天气泡
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
### 错误码说明
|
||||||
|
|
||||||
|
| 错误码 | 说明 | 处理建议 |
|
||||||
|
|-------|------|---------|
|
||||||
|
| `AUTH_FAILED` | 认证失败 | 检查 Token 有效性 |
|
||||||
|
| `RATE_LIMIT` | 频率限制 | 等待后重试 |
|
||||||
|
| `CONTENT_FILTERED` | 内容被过滤 | 修改消息内容 |
|
||||||
|
| `PERMISSION_DENIED` | 权限不足 | 检查用户权限 |
|
||||||
|
| `ZULIP_ERROR` | Zulip 服务错误 | 系统自动重试 |
|
||||||
|
| `SESSION_EXPIRED` | 会话过期 | 重新登录 |
|
||||||
|
|
||||||
|
### 降级策略
|
||||||
|
|
||||||
|
当 Zulip 服务不可用时,系统会自动切换到本地聊天模式:
|
||||||
|
- 消息仅在游戏内传播
|
||||||
|
- 不同步到 Zulip
|
||||||
|
- 服务恢复后自动切换回正常模式
|
||||||
|
|
||||||
|
## 安全机制
|
||||||
|
|
||||||
|
### API Key 安全
|
||||||
|
|
||||||
|
- API Key 加密存储在数据库中
|
||||||
|
- 永不下发到客户端
|
||||||
|
- 支持强制刷新机制
|
||||||
|
|
||||||
|
### 消息安全
|
||||||
|
|
||||||
|
- 敏感词过滤
|
||||||
|
- 频率限制防刷屏
|
||||||
|
- 位置验证防欺诈
|
||||||
|
- 消息长度限制
|
||||||
|
|
||||||
|
### 连接安全
|
||||||
|
|
||||||
|
- Token 验证
|
||||||
|
- 会话超时自动断开
|
||||||
|
- 异常连接检测和拒绝
|
||||||
|
|
||||||
|
## 监控指标
|
||||||
|
|
||||||
|
### 连接指标
|
||||||
|
- `zulip.connections.active`: 活跃连接数
|
||||||
|
- `zulip.connections.total`: 总连接数
|
||||||
|
- `zulip.connections.errors`: 连接错误数
|
||||||
|
|
||||||
|
### 消息指标
|
||||||
|
- `zulip.messages.sent`: 发送消息数
|
||||||
|
- `zulip.messages.received`: 接收消息数
|
||||||
|
- `zulip.messages.filtered`: 被过滤消息数
|
||||||
|
- `zulip.messages.latency`: 消息延迟
|
||||||
|
|
||||||
|
### 系统指标
|
||||||
|
- `zulip.sessions.active`: 活跃会话数
|
||||||
|
- `zulip.zulip_clients.active`: 活跃 Zulip 客户端数
|
||||||
|
- `zulip.event_queues.active`: 活跃事件队列数
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [zulip-js 库使用指南](./zulip-js.md)
|
||||||
|
- [API 接口文档](./api.md)
|
||||||
|
- [WebSocket 协议详解](./websocket-protocol.md)
|
||||||
|
- [配置管理指南](./configuration.md)
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
### 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)
|
||||||
|
- 添加地图区域描述字段
|
||||||
|
- 修复上下文注入使用 ConfigManager
|
||||||
|
- 改进错误处理和日志记录
|
||||||
|
|
||||||
|
### v1.0.0 (2025-12-25)
|
||||||
|
- 初始版本发布
|
||||||
|
- 实现 WebSocket Gateway 统一网关
|
||||||
|
- 实现 Session Manager 会话管理
|
||||||
|
- 实现 Zulip Client Pool 客户端池
|
||||||
|
- 实现 Message Filter 消息过滤
|
||||||
|
- 实现 Config Manager 配置管理
|
||||||
|
- 实现 Monitoring Service 监控服务
|
||||||
|
- 完成集成测试覆盖
|
||||||
254
docs/systems/zulip/ZULIP_INTEGRATION_SUMMARY.md
Normal file
254
docs/systems/zulip/ZULIP_INTEGRATION_SUMMARY.md
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
# Zulip集成系统测试总结
|
||||||
|
|
||||||
|
## 测试日期
|
||||||
|
2025-12-25
|
||||||
|
|
||||||
|
## 测试环境
|
||||||
|
|
||||||
|
### Zulip服务器配置
|
||||||
|
|
||||||
|
- **服务器URL**: <https://zulip.xinghangee.icu/>
|
||||||
|
- **Bot邮箱**: <cbot-bot@zulip.xinghangee.icu>
|
||||||
|
- **Bot API Key**: 3k61GqxVkc...x3F3STksF (已配置在.env)
|
||||||
|
|
||||||
|
### 测试用户配置
|
||||||
|
|
||||||
|
- **用户API Key**: W2KhXaQxJ...0c9nPXaalh5
|
||||||
|
- **Zulip用户邮箱**: <user8@zulip.xinghangee.icu>
|
||||||
|
- **用户全名**: ANGJustinl
|
||||||
|
- **用户ID**: 8
|
||||||
|
- **权限**: 管理员
|
||||||
|
|
||||||
|
## 测试结果
|
||||||
|
|
||||||
|
### ✅ 1. API Key验证测试
|
||||||
|
|
||||||
|
**测试脚本**: `test-api-key-validation.js`
|
||||||
|
|
||||||
|
**结果**: 通过
|
||||||
|
|
||||||
|
- API Key验证成功
|
||||||
|
- 用户信息获取正常
|
||||||
|
- 用户邮箱: <user8@zulip.xinghangee.icu>
|
||||||
|
- 用户全名: ANGJustinl
|
||||||
|
|
||||||
|
### ✅ 2. Stream管理测试
|
||||||
|
|
||||||
|
**测试脚本**: `test-list-subscriptions.js`, `test-subscribe-stream.js`
|
||||||
|
|
||||||
|
**结果**: 通过
|
||||||
|
|
||||||
|
- 成功列出用户订阅的Streams (Zulip, general, 沙箱)
|
||||||
|
- 成功创建"Novice Village" Stream
|
||||||
|
- 成功订阅新创建的Stream
|
||||||
|
- 测试消息发送成功 (Message ID: 17, 19)
|
||||||
|
|
||||||
|
### ✅ 3. Zulip客户端创建测试
|
||||||
|
|
||||||
|
**测试方法**: 服务器日志验证
|
||||||
|
|
||||||
|
**结果**: 通过
|
||||||
|
|
||||||
|
- Zulip客户端创建成功
|
||||||
|
- 事件队列注册成功 (Queue ID: 9b7c31ed-29a5-4419-b482-2fe549e26cc4)
|
||||||
|
- 客户端生命周期管理正常
|
||||||
|
- 客户端销毁和清理正常
|
||||||
|
|
||||||
|
### ✅ 4. 端到端集成测试
|
||||||
|
|
||||||
|
**测试脚本**: `test-user-api-key.js`
|
||||||
|
|
||||||
|
**结果**: 通过
|
||||||
|
|
||||||
|
- WebSocket连接成功
|
||||||
|
- 登录流程正常
|
||||||
|
- 会话ID生成正常
|
||||||
|
- 用户ID: user_W2KhXaQx
|
||||||
|
- 用户名: Player_W2KhX
|
||||||
|
- 当前地图: whale_port (更新后)
|
||||||
|
- 消息发送成功
|
||||||
|
- Message ID: 20-25, 51-52
|
||||||
|
- 所有消息成功发送到Zulip服务器
|
||||||
|
- 支持多地图消息路由 (Whale Port, Pumpkin Valley)
|
||||||
|
- 目标Topic: General
|
||||||
|
|
||||||
|
### ✅ 5. 单元测试和集成测试
|
||||||
|
|
||||||
|
**测试套件**: `src/business/zulip/zulip-integration.e2e.spec.ts`
|
||||||
|
|
||||||
|
**结果**: 22/22 通过
|
||||||
|
|
||||||
|
- WebSocket连接和会话管理 ✓
|
||||||
|
- Zulip客户端生命周期管理 ✓
|
||||||
|
- 消息路由和权限验证 ✓
|
||||||
|
- 消息格式转换完整性 ✓
|
||||||
|
- 消息接收和分发 ✓
|
||||||
|
- 会话状态一致性 ✓
|
||||||
|
- 内容安全和频率控制 ✓
|
||||||
|
- API Key安全存储 ✓
|
||||||
|
- 错误处理和服务降级 ✓
|
||||||
|
- 操作确认和日志记录 ✓
|
||||||
|
- 系统监控和告警 ✓
|
||||||
|
- 配置验证 ✓
|
||||||
|
|
||||||
|
### ✅ 6. Stream初始化测试
|
||||||
|
|
||||||
|
**测试脚本**: `test-stream-initialization.js`
|
||||||
|
|
||||||
|
**结果**: 部分通过
|
||||||
|
|
||||||
|
- Stream 初始化服务正常启动
|
||||||
|
- 成功检测缺失的 Streams
|
||||||
|
- Bot 账号权限不足,无法自动创建 Streams
|
||||||
|
- 使用管理员账号手动创建 Streams 成功
|
||||||
|
- 所有 9 个地图区域的 Streams 已创建
|
||||||
|
|
||||||
|
### ✅ 7. 多地图消息路由测试
|
||||||
|
|
||||||
|
**测试脚本**: `test-user-api-key.js` (更新版)
|
||||||
|
|
||||||
|
**结果**: 通过
|
||||||
|
|
||||||
|
- 成功在 Whale Port 发送消息 (Message ID: 51)
|
||||||
|
- 成功切换到 Pumpkin Valley
|
||||||
|
- 成功在 Pumpkin Valley 发送消息 (Message ID: 52)
|
||||||
|
- 上下文注入正确使用 ConfigManager
|
||||||
|
- 消息路由到正确的 Stream
|
||||||
|
|
||||||
|
## 关键发现
|
||||||
|
|
||||||
|
### 1. API Key和用户邮箱映射
|
||||||
|
|
||||||
|
- 用户API Key对应的Zulip邮箱是 `user8@zulip.xinghangee.icu`
|
||||||
|
- 不是 `cbot-bot@zulip.xinghangee.icu`
|
||||||
|
- 已在代码中修正 (`src/business/zulip/zulip.service.ts`)
|
||||||
|
|
||||||
|
### 2. Stream创建和权限
|
||||||
|
|
||||||
|
- Bot 账号 (cbot-bot) 缺少创建 Stream 的权限
|
||||||
|
- 需要使用管理员账号手动创建 Streams
|
||||||
|
- 或在 Zulip 服务器中为 Bot 授予 Stream 创建权限
|
||||||
|
- 已使用管理员账号成功创建所有 9 个地图区域的 Streams
|
||||||
|
|
||||||
|
### 3. 地图配置更新
|
||||||
|
|
||||||
|
- 系统从 2 个地图区域扩展到 9 个地图区域
|
||||||
|
- 默认出生点从 `novice_village` 更改为 `whale_port`
|
||||||
|
- 添加了地图区域描述字段 (`description`)
|
||||||
|
- 配置版本从 1.0.0 升级到 2.0.0
|
||||||
|
|
||||||
|
### 4. 消息路由改进
|
||||||
|
|
||||||
|
- 修复了 SessionManager 使用硬编码 Stream 映射的问题
|
||||||
|
- 现在使用 ConfigManager 动态获取 Stream 映射
|
||||||
|
- 支持多地图消息路由,消息自动发送到玩家当前地图对应的 Stream
|
||||||
|
- 已验证 Whale Port 和 Pumpkin Valley 的消息路由正常
|
||||||
|
|
||||||
|
### 5. 消息发送验证
|
||||||
|
|
||||||
|
- 所有消息都成功发送到Zulip服务器
|
||||||
|
- 返回真实的Message ID (20-25, 51-52)
|
||||||
|
- 可以在Zulip网页界面查看消息
|
||||||
|
- 支持跨地图消息发送
|
||||||
|
|
||||||
|
## 系统状态
|
||||||
|
|
||||||
|
### ✅ 核心功能
|
||||||
|
|
||||||
|
- [x] WebSocket连接管理
|
||||||
|
- [x] 用户登录和会话管理
|
||||||
|
- [x] Zulip客户端创建和管理
|
||||||
|
- [x] 事件队列注册和管理
|
||||||
|
- [x] 消息发送到Zulip
|
||||||
|
- [x] 消息格式转换
|
||||||
|
- [x] 多地图消息路由
|
||||||
|
- [x] Stream 自动初始化检查
|
||||||
|
- [x] 错误处理和降级
|
||||||
|
- [x] 日志记录和监控
|
||||||
|
|
||||||
|
### ✅ 配置管理
|
||||||
|
|
||||||
|
- [x] 环境变量配置
|
||||||
|
- [x] 9 区域地图映射配置
|
||||||
|
- [x] API Key安全存储
|
||||||
|
- [x] 配置验证
|
||||||
|
- [x] 动态 Stream 映射
|
||||||
|
|
||||||
|
### ✅ 测试覆盖
|
||||||
|
|
||||||
|
- [x] 单元测试 (22个测试用例)
|
||||||
|
- [x] 集成测试 (端到端流程)
|
||||||
|
- [x] 真实Zulip服务器测试
|
||||||
|
- [x] 多地图消息路由测试
|
||||||
|
- [x] Stream 初始化测试
|
||||||
|
- [x] 错误场景测试
|
||||||
|
|
||||||
|
# !!!stream-initializer.service.ts - 404行处仍有todo需要完成, 现在没前端我搞不清楚咋做:(
|
||||||
|
|
||||||
|
## 下一步建议
|
||||||
|
|
||||||
|
### 1. Stream 权限配置
|
||||||
|
|
||||||
|
- [ ] 在 Zulip 服务器中为 Bot 账号授予创建 Stream 的权限
|
||||||
|
- [ ] 或使用管理员账号预先创建所有 Streams
|
||||||
|
- [ ] 验证所有 9 个地图区域的 Streams 已创建
|
||||||
|
|
||||||
|
### 2. 生产环境准备
|
||||||
|
|
||||||
|
- [ ] 配置生产环境的Zulip服务器
|
||||||
|
- [ ] 设置API Key加密密钥 (ZULIP_API_KEY_ENCRYPTION_KEY)
|
||||||
|
- [ ] 配置邮件服务用于通知
|
||||||
|
- [ ] 设置监控和告警
|
||||||
|
- [ ] 配置所有地图区域的 Streams
|
||||||
|
|
||||||
|
### 3. 功能增强
|
||||||
|
|
||||||
|
- [ ] 实现从Zulip接收消息的事件轮询
|
||||||
|
- [ ] 实现双向消息同步
|
||||||
|
- [ ] 实现用户权限管理
|
||||||
|
- [ ] 添加地图切换动画和提示
|
||||||
|
- [ ] 实现跨地图私聊功能
|
||||||
|
|
||||||
|
### 4. 性能优化
|
||||||
|
|
||||||
|
- [ ] 优化客户端池管理
|
||||||
|
- [ ] 实现消息批量发送
|
||||||
|
- [ ] 添加消息缓存机制
|
||||||
|
- [ ] 优化事件队列轮询频率
|
||||||
|
- [ ] 实现 Stream 订阅缓存
|
||||||
|
|
||||||
|
### 5. 文档完善
|
||||||
|
|
||||||
|
- [x] 系统架构文档
|
||||||
|
- [x] API文档
|
||||||
|
- [x] WebSocket协议文档
|
||||||
|
- [x] 配置文档 (已更新 9 区域配置)
|
||||||
|
- [x] Stream 初始化文档
|
||||||
|
- [ ] 部署文档
|
||||||
|
- [ ] 运维手册
|
||||||
|
|
||||||
|
## 结论
|
||||||
|
|
||||||
|
Zulip集成系统已成功完成开发和测试,所有核心功能正常工作。系统已通过:
|
||||||
|
|
||||||
|
- 22个单元测试和集成测试
|
||||||
|
- 真实Zulip服务器的端到端测试
|
||||||
|
- 多地图消息路由验证
|
||||||
|
- Stream 初始化服务测试
|
||||||
|
- 消息发送和接收验证
|
||||||
|
|
||||||
|
**最新更新 (v2.0.0):**
|
||||||
|
|
||||||
|
- 地图配置从 2 个区域扩展到 9 个区域
|
||||||
|
- 实现 Stream 自动初始化检查服务
|
||||||
|
- 修复上下文注入使用动态配置
|
||||||
|
- 改进错误处理和日志记录
|
||||||
|
- 更新默认出生点为鲸之港
|
||||||
|
|
||||||
|
系统已准备好进入下一阶段的开发和部署。建议优先配置 Stream 创建权限或手动创建所有地图区域的 Streams。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**测试人员**: ANGJustinl
|
||||||
|
**审核状态**: 待确认
|
||||||
|
**文档版本**: 1.0.0
|
||||||
285
docs/systems/zulip/api.md
Normal file
285
docs/systems/zulip/api.md
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
# Zulip 集成系统 API 文档
|
||||||
|
|
||||||
|
## WebSocket 连接
|
||||||
|
|
||||||
|
### 连接地址
|
||||||
|
|
||||||
|
```
|
||||||
|
wss://localhost:3000/game
|
||||||
|
```
|
||||||
|
|
||||||
|
### 连接参数
|
||||||
|
|
||||||
|
连接时无需额外参数,认证通过 `login` 消息完成。
|
||||||
|
|
||||||
|
## 消息类型
|
||||||
|
|
||||||
|
### 1. 登录 (login)
|
||||||
|
|
||||||
|
**请求:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "login",
|
||||||
|
"token": "user_game_token"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**成功响应:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"t": "login_success",
|
||||||
|
"sessionId": "session_abc123",
|
||||||
|
"currentMap": "novice_village",
|
||||||
|
"username": "player_name"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**失败响应:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"t": "error",
|
||||||
|
"code": "AUTH_FAILED",
|
||||||
|
"message": "Token 验证失败"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 发送聊天消息 (chat)
|
||||||
|
|
||||||
|
**请求:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"t": "chat",
|
||||||
|
"content": "Hello, world!",
|
||||||
|
"scope": "local"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数说明:**
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|-----|------|-----|------|
|
||||||
|
| t | string | 是 | 固定值 "chat" |
|
||||||
|
| content | string | 是 | 消息内容,最大 1000 字符 |
|
||||||
|
| scope | string | 是 | 消息范围: "local" 或具体 topic 名称 |
|
||||||
|
|
||||||
|
**成功响应:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"t": "chat_sent",
|
||||||
|
"messageId": "msg_123",
|
||||||
|
"timestamp": 1703500800000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**失败响应:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"t": "error",
|
||||||
|
"code": "RATE_LIMIT",
|
||||||
|
"message": "消息发送过于频繁,请稍后再试"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 接收聊天消息 (chat_render)
|
||||||
|
|
||||||
|
**服务器推送:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"t": "chat_render",
|
||||||
|
"from": "other_player",
|
||||||
|
"txt": "Hi there!",
|
||||||
|
"bubble": true,
|
||||||
|
"timestamp": 1703500800000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数说明:**
|
||||||
|
| 参数 | 类型 | 说明 |
|
||||||
|
|-----|------|------|
|
||||||
|
| t | string | 固定值 "chat_render" |
|
||||||
|
| from | string | 发送者名称 |
|
||||||
|
| txt | string | 消息内容 |
|
||||||
|
| bubble | boolean | 是否显示气泡 |
|
||||||
|
| timestamp | number | 消息时间戳 |
|
||||||
|
|
||||||
|
### 4. 位置更新 (position_update)
|
||||||
|
|
||||||
|
**请求:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"t": "position",
|
||||||
|
"x": 150,
|
||||||
|
"y": 200,
|
||||||
|
"mapId": "novice_village"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数说明:**
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|-----|------|-----|------|
|
||||||
|
| t | string | 是 | 固定值 "position" |
|
||||||
|
| x | number | 是 | X 坐标 |
|
||||||
|
| y | number | 是 | Y 坐标 |
|
||||||
|
| mapId | string | 是 | 地图 ID |
|
||||||
|
|
||||||
|
**响应:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"t": "position_updated",
|
||||||
|
"stream": "Novice Village",
|
||||||
|
"topic": "General"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 登出 (logout)
|
||||||
|
|
||||||
|
**请求:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "logout"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"t": "logout_success"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 错误码
|
||||||
|
|
||||||
|
| 错误码 | HTTP 等效 | 说明 | 处理建议 |
|
||||||
|
|-------|----------|------|---------|
|
||||||
|
| `AUTH_FAILED` | 401 | 认证失败,Token 无效或过期 | 重新获取 Token 并登录 |
|
||||||
|
| `RATE_LIMIT` | 429 | 消息发送频率超限 | 等待 60 秒后重试 |
|
||||||
|
| `CONTENT_FILTERED` | 400 | 消息内容被过滤 | 修改消息内容后重试 |
|
||||||
|
| `CONTENT_TOO_LONG` | 400 | 消息内容超长 | 缩短消息长度 |
|
||||||
|
| `PERMISSION_DENIED` | 403 | 权限不足 | 检查用户权配置 |
|
||||||
|
| `SESSION_EXPIRED` | 401 | 会话已过期 | 重新登录 |
|
||||||
|
| `SESSION_NOT_FOUND` | 404 | 会话不存在 | 重新登录 |
|
||||||
|
| `ZULIP_ERROR` | 502 | Zulip 服务错误 | 系统自动重试,无需处理 |
|
||||||
|
| `INTERNAL_ERROR` | 500 | 内部服务器错误 | 联系管理员 |
|
||||||
|
|
||||||
|
## 频率限制
|
||||||
|
|
||||||
|
### 消息发送限制
|
||||||
|
|
||||||
|
- 默认限制: 10 条/分钟
|
||||||
|
- 超限后返回 `RATE_LIMIT` 错误
|
||||||
|
- 限制窗口: 滑动窗口,60 秒
|
||||||
|
|
||||||
|
### 连接限制
|
||||||
|
|
||||||
|
- 单用户最大连接数: 3
|
||||||
|
- 超限后新连接被拒绝
|
||||||
|
|
||||||
|
## 消息过滤规则
|
||||||
|
|
||||||
|
### 内容过滤
|
||||||
|
|
||||||
|
1. **敏感词过滤**: 包含敏感词的消息将被拒绝
|
||||||
|
2. **长度限制**: 消息最大 1000 字符
|
||||||
|
3. **重复检测**: 连续发送相同内容将被拒绝
|
||||||
|
|
||||||
|
### 权限验证
|
||||||
|
|
||||||
|
1. **位置验证**: 只能向当前所在地图对应的 Stream 发送消息
|
||||||
|
2. **Stream 权限**: 只能访问配置中允许的 Stream
|
||||||
|
|
||||||
|
## 示例代码
|
||||||
|
|
||||||
|
### JavaScript/TypeScript
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 连接 WebSocket
|
||||||
|
const socket = new WebSocket('ws://localhost:3000/game');
|
||||||
|
|
||||||
|
// 连接成功
|
||||||
|
socket.onopen = () => {
|
||||||
|
// 发送登录消息
|
||||||
|
socket.send(JSON.stringify({
|
||||||
|
type: 'login',
|
||||||
|
token: 'your_game_token'
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 接收消息
|
||||||
|
socket.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
|
switch (data.t) {
|
||||||
|
case 'login_success':
|
||||||
|
console.log('登录成功:', data.sessionId);
|
||||||
|
break;
|
||||||
|
case 'chat_render':
|
||||||
|
console.log(`${data.from}: ${data.txt}`);
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
console.error(`错误 [${data.code}]: ${data.message}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 发送聊天消息
|
||||||
|
function sendChat(content: string) {
|
||||||
|
socket.send(JSON.stringify({
|
||||||
|
t: 'chat',
|
||||||
|
content: content,
|
||||||
|
scope: 'local'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新位置
|
||||||
|
function updatePosition(x: number, y: number, mapId: string) {
|
||||||
|
socket.send(JSON.stringify({
|
||||||
|
t: 'position',
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
mapId: mapId
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 健康检查接口
|
||||||
|
|
||||||
|
### GET /health
|
||||||
|
|
||||||
|
检查系统健康状态。
|
||||||
|
|
||||||
|
**响应:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "healthy",
|
||||||
|
"components": {
|
||||||
|
"websocket": "healthy",
|
||||||
|
"zulip": "healthy",
|
||||||
|
"redis": "healthy"
|
||||||
|
},
|
||||||
|
"metrics": {
|
||||||
|
"activeConnections": 42,
|
||||||
|
"activeSessions": 40,
|
||||||
|
"messagesSentLastMinute": 156
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET /metrics
|
||||||
|
|
||||||
|
获取系统指标(Prometheus 格式)。
|
||||||
|
|
||||||
|
**响应:**
|
||||||
|
```
|
||||||
|
# HELP zulip_connections_active Active WebSocket connections
|
||||||
|
# TYPE zulip_connections_active gauge
|
||||||
|
zulip_connections_active 42
|
||||||
|
|
||||||
|
# HELP zulip_messages_sent_total Total messages sent
|
||||||
|
# TYPE zulip_messages_sent_total counter
|
||||||
|
zulip_messages_sent_total 15678
|
||||||
|
|
||||||
|
# HELP zulip_message_latency_seconds Message processing latency
|
||||||
|
# TYPE zulip_message_latency_seconds histogram
|
||||||
|
zulip_message_latency_seconds_bucket{le="0.1"} 14500
|
||||||
|
zulip_message_latency_seconds_bucket{le="0.5"} 15600
|
||||||
|
zulip_message_latency_seconds_bucket{le="1"} 15678
|
||||||
|
```
|
||||||
516
docs/systems/zulip/configuration.md
Normal file
516
docs/systems/zulip/configuration.md
Normal file
@@ -0,0 +1,516 @@
|
|||||||
|
# 配置管理指南
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
Zulip 集成系统支持多种配置方式,包括环境变量、配置文件和运行时配置。本文档详细说明各配置项的用途和设置方法。
|
||||||
|
|
||||||
|
## 环境变量配置
|
||||||
|
|
||||||
|
### Zulip 服务器配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Zulip 服务器 URL
|
||||||
|
ZULIP_SERVER_URL=https://zulip.xinghangee.icu/
|
||||||
|
|
||||||
|
# Zulip Bot 邮箱
|
||||||
|
ZULIP_BOT_EMAIL=cbot-bot@zulip.xinghangee.icu
|
||||||
|
|
||||||
|
# Zulip Bot API Key
|
||||||
|
ZULIP_BOT_API_KEY=your-bot-api-key
|
||||||
|
|
||||||
|
# Zulip Realm (可选,默认从 URL 推断)
|
||||||
|
ZULIP_REALM=your-realm
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebSocket 配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# WebSocket 端口
|
||||||
|
WEBSOCKET_PORT=3000
|
||||||
|
|
||||||
|
# WebSocket 命名空间
|
||||||
|
WEBSOCKET_NAMESPACE=/game
|
||||||
|
|
||||||
|
# 最大连接数
|
||||||
|
WEBSOCKET_MAX_CONNECTIONS=100
|
||||||
|
|
||||||
|
# 连接超时时间 (毫秒)
|
||||||
|
WEBSOCKET_TIMEOUT=60000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 消息配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 消息频率限制 (条/分钟)
|
||||||
|
MESSAGE_RATE_LIMIT=10
|
||||||
|
|
||||||
|
# 消息最大长度 (字符)
|
||||||
|
MESSAGE_MAX_LENGTH=1000
|
||||||
|
|
||||||
|
# 是否启用内容过滤
|
||||||
|
ENABLE_CONTENT_FILTER=true
|
||||||
|
|
||||||
|
# 是否启用重复检测
|
||||||
|
ENABLE_DUPLICATE_DETECTION=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 会话配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 会话超时时间 (分钟)
|
||||||
|
SESSION_TIMEOUT=30
|
||||||
|
|
||||||
|
# 会话清理间隔 (分钟)
|
||||||
|
SESSION_CLEANUP_INTERVAL=5
|
||||||
|
|
||||||
|
# 最大会话数
|
||||||
|
MAX_SESSIONS=5000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Redis 配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Redis 主机
|
||||||
|
REDIS_HOST=localhost
|
||||||
|
|
||||||
|
# Redis 端口
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
# Redis 密码 (可选)
|
||||||
|
REDIS_PASSWORD=
|
||||||
|
|
||||||
|
# Redis 数据库索引
|
||||||
|
REDIS_DB=0
|
||||||
|
|
||||||
|
# Redis 键前缀
|
||||||
|
REDIS_KEY_PREFIX=zulip:
|
||||||
|
```
|
||||||
|
|
||||||
|
### 日志配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 日志级别 (debug, info, warn, error)
|
||||||
|
LOG_LEVEL=info
|
||||||
|
|
||||||
|
# 是否启用结构化日志
|
||||||
|
LOG_STRUCTURED=true
|
||||||
|
|
||||||
|
# 日志文件路径 (可选)
|
||||||
|
LOG_FILE_PATH=logs/zulip.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置文件
|
||||||
|
|
||||||
|
### 地图映射配置
|
||||||
|
|
||||||
|
文件位置: `config/zulip/map-config.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "2.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 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "offer_city",
|
||||||
|
"mapName": "Offer 城",
|
||||||
|
"zulipStream": "Offer City",
|
||||||
|
"description": "职业发展、面试与商务区",
|
||||||
|
"interactionObjects": [
|
||||||
|
{
|
||||||
|
"objectId": "skyscrapers",
|
||||||
|
"objectName": "摩天大楼",
|
||||||
|
"zulipTopic": "Career Talk",
|
||||||
|
"position": { "x": 350, "y": 650 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mapId": "model_factory",
|
||||||
|
"mapName": "模型工厂",
|
||||||
|
"zulipStream": "Model Factory",
|
||||||
|
"description": "AI模型训练、代码构建与工业区",
|
||||||
|
"interactionObjects": [
|
||||||
|
{
|
||||||
|
"objectId": "assembly_line",
|
||||||
|
"objectName": "流水线",
|
||||||
|
"zulipTopic": "Code Review",
|
||||||
|
"position": { "x": 400, "y": 200 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
系统现在支持 9 个地图区域:
|
||||||
|
|
||||||
|
1. **鲸之港 (Whale Port)** - 中心城区,交通枢纽与主要聚会点
|
||||||
|
2. **南瓜谷 (Pumpkin Valley)** - 新手成长、基础资源与学习社区
|
||||||
|
3. **Offer 城 (Offer City)** - 职业发展、面试与商务区
|
||||||
|
4. **模型工厂 (Model Factory)** - AI模型训练、代码构建与工业区
|
||||||
|
5. **内核岛 (Kernel Island)** - 核心技术研究、底层原理与算法
|
||||||
|
6. **摸鱼海滩 (Moyu Beach)** - 休闲娱乐、水贴与非技术话题
|
||||||
|
7. **天梯峰 (Ladder Peak)** - 挑战、竞赛与排行榜
|
||||||
|
8. **星河湾 (Galaxy Bay)** - 创意、设计与灵感
|
||||||
|
9. **数据遗迹 (Data Ruins)** - 数据库、归档与历史记录
|
||||||
|
|
||||||
|
### 配置字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|-----|------|-----|------|
|
||||||
|
| version | string | 是 | 配置版本号 |
|
||||||
|
| lastModified | string | 否 | 最后修改时间 (ISO 8601) |
|
||||||
|
| description | string | 否 | 配置文件描述 |
|
||||||
|
| maps | array | 是 | 地图配置数组 |
|
||||||
|
| maps[].mapId | string | 是 | 地图唯一标识 |
|
||||||
|
| maps[].mapName | string | 是 | 地图显示名称 |
|
||||||
|
| maps[].zulipStream | string | 是 | 对应的 Zulip Stream |
|
||||||
|
| maps[].description | string | 否 | 地图区域描述 |
|
||||||
|
| maps[].defaultTopic | string | 否 | 默认 Topic,默认 "General" |
|
||||||
|
| maps[].interactionObjects | array | 否 | 交互对象配置 |
|
||||||
|
| interactionObjects[].objectId | string | 是 | 对象唯一标识 |
|
||||||
|
| interactionObjects[].objectName | string | 是 | 对象显示名称 |
|
||||||
|
| interactionObjects[].zulipTopic | string | 是 | 对应的 Zulip Topic |
|
||||||
|
| interactionObjects[].position | object | 是 | 对象位置坐标 |
|
||||||
|
| interactionObjects[].radius | number | 否 | 交互半径,默认 50 |
|
||||||
|
|
||||||
|
### 敏感词配置
|
||||||
|
|
||||||
|
文件位置: `config/zulip/sensitive-words.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"words": [
|
||||||
|
"敏感词1",
|
||||||
|
"敏感词2"
|
||||||
|
],
|
||||||
|
"patterns": [
|
||||||
|
"正则表达式1",
|
||||||
|
"正则表达式2"
|
||||||
|
],
|
||||||
|
"replacements": {
|
||||||
|
"原词": "替换词"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 允许的 Stream 配置
|
||||||
|
|
||||||
|
文件位置: `config/zulip/allowed-streams.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"streams": [
|
||||||
|
"Novice Village",
|
||||||
|
"Market Square",
|
||||||
|
"Guild Hall",
|
||||||
|
"Arena"
|
||||||
|
],
|
||||||
|
"privateStreams": [
|
||||||
|
"Admin",
|
||||||
|
"Moderators"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 运行时配置
|
||||||
|
|
||||||
|
### 通过 API 更新配置
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 更新消息频率限制
|
||||||
|
await configManager.updateConfig('messageRateLimit', 20);
|
||||||
|
|
||||||
|
// 更新会话超时时间
|
||||||
|
await configManager.updateConfig('sessionTimeout', 60);
|
||||||
|
|
||||||
|
// 重新加载地图配置
|
||||||
|
await configManager.reloadMapConfig();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置热重载
|
||||||
|
|
||||||
|
系统支持配置热重载,无需重启服务:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 发送 SIGHUP 信号触发配置重载
|
||||||
|
kill -HUP <pid>
|
||||||
|
```
|
||||||
|
|
||||||
|
或通过 API:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/admin/config/reload \
|
||||||
|
-H "Authorization: Bearer <admin_token>"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置验证
|
||||||
|
|
||||||
|
### 启动时验证
|
||||||
|
|
||||||
|
系统在启动时会验证所有配置的有效性:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 配置验证示例
|
||||||
|
const configValidator = new ConfigValidator();
|
||||||
|
|
||||||
|
// 验证环境变量
|
||||||
|
configValidator.validateEnv({
|
||||||
|
ZULIP_SERVER_URL: { required: true, type: 'url' },
|
||||||
|
ZULIP_BOT_EMAIL: { required: true, type: 'email' },
|
||||||
|
ZULIP_BOT_API_KEY: { required: true, type: 'string' },
|
||||||
|
MESSAGE_RATE_LIMIT: { required: false, type: 'number', default: 10 },
|
||||||
|
});
|
||||||
|
|
||||||
|
// 验证地图配置
|
||||||
|
configValidator.validateMapConfig(mapConfig);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 验证错误处理
|
||||||
|
|
||||||
|
配置验证失败时,系统会:
|
||||||
|
|
||||||
|
1. 记录详细的错误日志
|
||||||
|
2. 输出错误信息到控制台
|
||||||
|
3. 阻止服务启动(严重错误)或使用默认值(非严重错误)
|
||||||
|
|
||||||
|
```
|
||||||
|
[ERROR] 配置验证失败:
|
||||||
|
- ZULIP_SERVER_URL: 必填项未设置
|
||||||
|
- MESSAGE_RATE_LIMIT: 值必须大于 0
|
||||||
|
- map-config.json: maps[0].zulipStream 不能为空
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stream 初始化
|
||||||
|
|
||||||
|
### 自动初始化服务
|
||||||
|
|
||||||
|
系统在启动时会自动检查所有地图配置中定义的 Zulip Streams 是否存在。如果发现缺失的 Streams,会尝试自动创建。
|
||||||
|
|
||||||
|
**服务配置:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Stream 初始化服务会在系统启动 5 秒后自动运行
|
||||||
|
// 位置: src/core/zulip_core/services/stream_initializer.service.ts
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class StreamInitializerService implements OnModuleInit {
|
||||||
|
async onModuleInit() {
|
||||||
|
// 延迟 5 秒启动,确保其他服务已就绪
|
||||||
|
setTimeout(() => {
|
||||||
|
this.initializeStreams();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 权限要求
|
||||||
|
|
||||||
|
创建 Zulip Streams 需要特定权限:
|
||||||
|
|
||||||
|
- **Bot 账号**: 默认情况下可能缺少创建 Stream 的权限
|
||||||
|
- **管理员账号**: 拥有完整的 Stream 创建权限
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
|
||||||
|
1. **方案一**: 在 Zulip 服务器中为 Bot 账号授予创建 Stream 的权限
|
||||||
|
- 登录 Zulip 管理后台
|
||||||
|
- 找到 Bot 账号设置
|
||||||
|
- 授予 "Create streams" 权限
|
||||||
|
|
||||||
|
2. **方案二**: 使用管理员账号手动创建 Streams
|
||||||
|
- 使用提供的测试脚本 `test-stream-initialization.js`
|
||||||
|
- 配置管理员 API Key
|
||||||
|
- 运行脚本自动创建所有 Streams
|
||||||
|
|
||||||
|
3. **方案三**: 在 Zulip 网页界面手动创建
|
||||||
|
- 登录 Zulip 网页界面
|
||||||
|
- 创建对应的 Streams (参考 `config/zulip/map-config.json`)
|
||||||
|
|
||||||
|
### 手动创建 Streams
|
||||||
|
|
||||||
|
使用测试脚本创建所有地图区域的 Streams:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 编辑 test-stream-initialization.js,配置管理员 API Key
|
||||||
|
# 然后运行脚本
|
||||||
|
node test-stream-initialization.js
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本会自动创建以下 Streams:
|
||||||
|
|
||||||
|
- Whale Port (鲸之港)
|
||||||
|
- Pumpkin Valley (南瓜谷)
|
||||||
|
- Offer City (Offer 城)
|
||||||
|
- Model Factory (模型工厂)
|
||||||
|
- Kernel Island (内核岛)
|
||||||
|
- Moyu Beach (摸鱼海滩)
|
||||||
|
- Ladder Peak (天梯峰)
|
||||||
|
- Galaxy Bay (星河湾)
|
||||||
|
- Data Ruins (数据遗迹)
|
||||||
|
|
||||||
|
### 初始化日志
|
||||||
|
|
||||||
|
系统会记录 Stream 初始化的详细日志:
|
||||||
|
|
||||||
|
```
|
||||||
|
[INFO] 开始初始化 Zulip Streams...
|
||||||
|
[INFO] 检查 Stream: Whale Port
|
||||||
|
[INFO] Stream 已存在: Whale Port
|
||||||
|
[WARN] Stream 不存在,尝试创建: Pumpkin Valley
|
||||||
|
[INFO] Stream 创建成功: Pumpkin Valley
|
||||||
|
[ERROR] Stream 创建失败: Offer City - Insufficient permission
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置最佳实践
|
||||||
|
|
||||||
|
### 1. 使用环境变量管理敏感信息
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 不要在代码中硬编码敏感信息
|
||||||
|
# 使用环境变量或密钥管理服务
|
||||||
|
|
||||||
|
# 开发环境
|
||||||
|
export ZULIP_BOT_API_KEY=dev-api-key
|
||||||
|
|
||||||
|
# 生产环境 (使用密钥管理服务)
|
||||||
|
export ZULIP_BOT_API_KEY=$(aws secretsmanager get-secret-value --secret-id zulip-api-key --query SecretString --output text)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 分环境配置
|
||||||
|
|
||||||
|
```
|
||||||
|
config/
|
||||||
|
├── zulip/
|
||||||
|
│ ├── map-config.json # 默认配置
|
||||||
|
│ ├── map-config.dev.json # 开发环境
|
||||||
|
│ ├── map-config.staging.json # 预发布环境
|
||||||
|
│ └── map-config.prod.json # 生产环境
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 根据环境加载配置
|
||||||
|
const env = process.env.NODE_ENV || 'development';
|
||||||
|
const configPath = `config/zulip/map-config.${env}.json`;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 配置版本控制
|
||||||
|
|
||||||
|
- 将配置文件纳入版本控制
|
||||||
|
- 使用 `.env.example` 提供配置模板
|
||||||
|
- 敏感配置使用 `.gitignore` 排除
|
||||||
|
|
||||||
|
### 4. 配置文档化
|
||||||
|
|
||||||
|
为每个配置项提供清晰的文档说明:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* 消息频率限制配置
|
||||||
|
*
|
||||||
|
* @description 限制用户每分钟可发送的消息数量
|
||||||
|
* @default 10
|
||||||
|
* @range 1-100
|
||||||
|
* @env MESSAGE_RATE_LIMIT
|
||||||
|
*/
|
||||||
|
messageRateLimit: number;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 常见配置问题
|
||||||
|
|
||||||
|
#### 1. Zulip 连接失败
|
||||||
|
|
||||||
|
```
|
||||||
|
错误: ZULIP_CONNECTION_FAILED
|
||||||
|
原因: 无法连接到 Zulip 服务器
|
||||||
|
```
|
||||||
|
|
||||||
|
检查项:
|
||||||
|
- `ZULIP_SERVER_URL` 是否正确
|
||||||
|
- 网络是否可达
|
||||||
|
- API Key 是否有效
|
||||||
|
|
||||||
|
#### 2. 地图配置加载失败
|
||||||
|
|
||||||
|
```
|
||||||
|
错误: MAP_CONFIG_LOAD_FAILED
|
||||||
|
原因: 地图配置文件格式错误
|
||||||
|
```
|
||||||
|
|
||||||
|
检查项:
|
||||||
|
- JSON 格式是否正确
|
||||||
|
- 必填字段是否完整
|
||||||
|
- 字段类型是否正确
|
||||||
|
|
||||||
|
#### 3. Redis 连接失败
|
||||||
|
|
||||||
|
```
|
||||||
|
错误: REDIS_CONNECTION_FAILED
|
||||||
|
原因: 无法连接到 Redis 服务器
|
||||||
|
```
|
||||||
|
|
||||||
|
检查项:
|
||||||
|
- `REDIS_HOST` 和 `REDIS_PORT` 是否正确
|
||||||
|
- Redis 服务是否运行
|
||||||
|
- 密码是否正确
|
||||||
|
|
||||||
|
### 配置诊断命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查配置有效性
|
||||||
|
npm run config:validate
|
||||||
|
|
||||||
|
# 显示当前配置
|
||||||
|
npm run config:show
|
||||||
|
|
||||||
|
# 测试 Zulip 连接
|
||||||
|
npm run config:test-zulip
|
||||||
|
|
||||||
|
# 测试 Redis 连接
|
||||||
|
npm run config:test-redis
|
||||||
|
```
|
||||||
216
docs/systems/zulip/guide.md
Normal file
216
docs/systems/zulip/guide.md
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
游戏属性: 2d社交属性, 无战斗的社群mmo游戏
|
||||||
|
核心目的(游戏内外无缝互通): 不玩这个游戏但是在zulip的社群成员, 也可以跨平台和游戏内的成员聊天
|
||||||
|
|
||||||
|
核心设计理念:
|
||||||
|
Stream (流) -> Topic (话题) 线程模型,天然契合 MMO 中的 Zone (区域) -> Context (情境) 逻辑
|
||||||
|
我们需要解决的核心问题是:如何将2D 空间位置(Game State)映射到Zulip 的信息组织形式(Message State),同时利用 Zulip 的 API Key 机制完成无缝认证
|
||||||
|
|
||||||
|
---
|
||||||
|
1. 核心逻辑架构 (The Core Logic)
|
||||||
|
在设计 API 之前,我们需要定义 mappings(映射关系):
|
||||||
|
- Game World / Map ←→ Zulip Stream (e.g., #Novice_Village)
|
||||||
|
- Interactive Object / Event ←→ Zulip Topic (e.g., Notice Board, Tavern Gossip)
|
||||||
|
- Whisper / Party ←→ Zulip Private Message
|
||||||
|
|
||||||
|
---
|
||||||
|
架构图示:
|
||||||
|
Client (Game) $$\xrightarrow{\text{Game Token}}$$ Game Middleware API $$\xrightarrow{\text{Zulip API Key}}$$ Zulip Server
|
||||||
|
$$Client (Godot) \xleftrightarrow{\text{WebSocket}} Node.js Server \xleftrightarrow{\text{REST/Long-Poll}} Zulip Server$$
|
||||||
|
设计理由:不建议让客户端直接直连 Zulip。我们需要一层中间件(Middleware)来控制权限、注入游戏数据(如玩家坐标、当前的动作状态),并防止用户在该 API Key 下进行非游戏允许的 Zulip 操作(如随意创建 Stream)。
|
||||||
|
|
||||||
|
---
|
||||||
|
2. 设计思路一: "统一网关"(Unified Gateway)
|
||||||
|
2.1 详细数据流设计 (Data Flow)
|
||||||
|
我们需要在 Node.js 中维护一个 Session Manager。
|
||||||
|
A. 登录与握手 (Initialization)
|
||||||
|
1. Godot: 发送登录包 {"type": "login", "token": "user_game_token"}。
|
||||||
|
2. Node.js:
|
||||||
|
- 验证游戏 Token。
|
||||||
|
- 查找该用户的 Zulip API Key(通常存储在数据库中,或者首次登录时让用户提供)。
|
||||||
|
- 关键步骤: Node.js 服务器为该特定用户实例化一个 Zulip Client,并向 Zulip 申请注册一个 Event Queue。
|
||||||
|
- 将 Socket_ID 与 Zulip_Queue_ID 绑定。
|
||||||
|
B. 发送消息 (Upstream: Godot -> Node -> Zulip)
|
||||||
|
1. Godot: 玩家输入 "Hello",Godot 通过 WebSocket 发送简化的包:
|
||||||
|
2. JSON
|
||||||
|
{
|
||||||
|
"t": "chat",
|
||||||
|
"content": "Hello",
|
||||||
|
"scope": "local" // 或者 "topic_name"
|
||||||
|
}
|
||||||
|
1. Node.js:
|
||||||
|
- 收到包,解析出这是聊天请求。
|
||||||
|
- 上下文注入: Node 知道玩家当前在 Map_101 (对应 Zulip Stream #Tavern)。
|
||||||
|
- API 调用: Node 使用该用户的 Zulip Client,调用 Zulip API 发送消息到 #Tavern。
|
||||||
|
- 优势: 这里可以做风控(比如禁止发脏话、频率限制),Godot 端根本无法绕过。
|
||||||
|
C. 接收消息 (Downstream: Zulip -> Node -> Godot)
|
||||||
|
1. Node.js:
|
||||||
|
- 服务器内部有一个循环(或者异步监听器),轮询 Zulip 的事件队列。
|
||||||
|
- 收到 Zulip 的 message 事件:User_B 在 #Tavern 说了 "Hi"。
|
||||||
|
- 空间过滤: Node 检查当前连接的所有 WebSocket,找出所有位于 Map_101 的玩家。
|
||||||
|
- 广播: 将消息打包成游戏协议,通过 WebSocket 推送给这些玩家:
|
||||||
|
2. JSON
|
||||||
|
{
|
||||||
|
"t": "chat_render",
|
||||||
|
"from": "User_B",
|
||||||
|
"txt": "Hi",
|
||||||
|
"bubble": true
|
||||||
|
}
|
||||||
|
1. Godot: 收到包,直接调用 show_bubble()。
|
||||||
|
|
||||||
|
---
|
||||||
|
2.3 这个方案的权衡分析 (Trade-off Analysis)
|
||||||
|
这种改变带来的本质变化:
|
||||||
|
优势 (The Wins)
|
||||||
|
1. 客户端极度简化 (Thin Client):
|
||||||
|
- Godot 里不需要写 HTTP Request,不需要处理 Long Polling 的异常断连,不需要解析复杂的 JSON 结构。
|
||||||
|
- Godot 只需要处理 on_websocket_packet_received。
|
||||||
|
2. 安全性 (Security):
|
||||||
|
- Zulip API Key 永不下发: 用户的 Zulip 凭证永远只保存在服务器端。如果客户端直接拿 Key,黑客可以通过解包 Godot 拿到 Key 然后去 Zulip 乱发消息。
|
||||||
|
- 位置欺诈完全消除: 因为 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();
|
||||||
174
docs/systems/zulip/quick_tests/test-list-subscriptions.js
Normal file
174
docs/systems/zulip/quick_tests/test-list-subscriptions.js
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
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('='.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: zulipAuth.email,
|
||||||
|
apiKey: zulipAuth.apiKey,
|
||||||
|
realm: 'https://zulip.xinghangee.icu/'
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = await zulip(config);
|
||||||
|
|
||||||
|
// 获取用户信息
|
||||||
|
console.log('\n👤 获取用户信息...');
|
||||||
|
const profile = await client.users.me.getProfile();
|
||||||
|
console.log('用户:', profile.full_name, `(${profile.email})`);
|
||||||
|
console.log('是否管理员:', profile.is_admin);
|
||||||
|
|
||||||
|
// 获取用户订阅的 Streams
|
||||||
|
console.log('\n📋 获取用户订阅的 Streams...');
|
||||||
|
const subscriptions = await client.streams.subscriptions.retrieve();
|
||||||
|
|
||||||
|
if (subscriptions.result === 'success') {
|
||||||
|
console.log(`\n✅ 找到 ${subscriptions.subscriptions.length} 个订阅的 Streams:`);
|
||||||
|
subscriptions.subscriptions.forEach(sub => {
|
||||||
|
console.log(` - ${sub.name} (ID: ${sub.stream_id})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 检查是否有 "Novice Village"
|
||||||
|
const noviceVillage = subscriptions.subscriptions.find(s => s.name === 'Pumpkin Valley');
|
||||||
|
if (noviceVillage) {
|
||||||
|
console.log('\n✅ "Pumpkin Valley" Stream 已存在!');
|
||||||
|
|
||||||
|
// 测试发送消息
|
||||||
|
console.log('\n📤 测试发送消息...');
|
||||||
|
const result = await client.messages.send({
|
||||||
|
type: 'stream',
|
||||||
|
to: 'Pumpkin Valley',
|
||||||
|
subject: 'General',
|
||||||
|
content: '测试消息:系统集成测试成功 🎮'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.result === 'success') {
|
||||||
|
console.log('✅ 消息发送成功! Message ID:', result.id);
|
||||||
|
} else {
|
||||||
|
console.log('❌ 消息发送失败:', result.msg);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('\n⚠️ "Pumpkin Valley" Stream 不存在');
|
||||||
|
console.log('💡 请在 Zulip 网页界面手动创建该 Stream,或使用管理员账号创建');
|
||||||
|
|
||||||
|
// 尝试发送到第一个可用的 Stream
|
||||||
|
if (subscriptions.subscriptions.length > 0) {
|
||||||
|
const firstStream = subscriptions.subscriptions[0];
|
||||||
|
console.log(`\n📤 尝试发送消息到 "${firstStream.name}"...`);
|
||||||
|
const result = await client.messages.send({
|
||||||
|
type: 'stream',
|
||||||
|
to: firstStream.name,
|
||||||
|
subject: 'Test',
|
||||||
|
content: '测试消息:验证系统可以发送消息 🎮'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.result === 'success') {
|
||||||
|
console.log('✅ 消息发送成功! Message ID:', result.id);
|
||||||
|
console.log(`💡 系统工作正常,只需创建 "Novice Village" Stream 即可`);
|
||||||
|
} else {
|
||||||
|
console.log('❌ 消息发送失败:', result.msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('❌ 获取订阅失败:', subscriptions.msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('\n❌ 操作失败:', error.message);
|
||||||
|
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();
|
||||||
183
docs/systems/zulip/quick_tests/test-user-api-key.js
Normal file
183
docs/systems/zulip/quick_tests/test-user-api-key.js
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
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('='.repeat(60));
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
let testStep = 0;
|
||||||
|
|
||||||
|
socket.on('connect', () => {
|
||||||
|
console.log('✅ WebSocket 连接成功');
|
||||||
|
testStep = 1;
|
||||||
|
|
||||||
|
// 使用真实的 JWT token
|
||||||
|
const loginMessage = {
|
||||||
|
type: 'login',
|
||||||
|
token: userInfo.token
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('📤 步骤 3: 发送登录消息(使用 JWT Token)');
|
||||||
|
socket.emit('login', loginMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('login_success', (data) => {
|
||||||
|
console.log('✅ 步骤 3 完成: 登录成功');
|
||||||
|
console.log(' 会话ID:', data.sessionId);
|
||||||
|
console.log(' 用户ID:', data.userId);
|
||||||
|
console.log(' 用户名:', data.username);
|
||||||
|
console.log(' 当前地图:', data.currentMap);
|
||||||
|
testStep = 2;
|
||||||
|
|
||||||
|
// 等待 Zulip 客户端初始化
|
||||||
|
console.log('\n⏳ 等待 3 秒让 Zulip 客户端初始化...');
|
||||||
|
setTimeout(() => {
|
||||||
|
const chatMessage = {
|
||||||
|
t: 'chat',
|
||||||
|
content: `🎮 【用户API Key测试】来自 ${userInfo.username} 的消息!\n` +
|
||||||
|
`时间: ${new Date().toLocaleString()}\n` +
|
||||||
|
`使用真实 API Key 发送此消息。`,
|
||||||
|
scope: 'local'
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('📤 步骤 4: 发送消息到 Zulip(使用真实 API Key)');
|
||||||
|
console.log(' 目标 Stream: Whale Port');
|
||||||
|
socket.emit('chat', chatMessage);
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('chat_sent', (data) => {
|
||||||
|
console.log('✅ 步骤 4 完成: 消息发送成功');
|
||||||
|
console.log(' 响应:', JSON.stringify(data, null, 2));
|
||||||
|
|
||||||
|
// 只在第一次收到 chat_sent 时发送第二条消息
|
||||||
|
if (testStep === 2) {
|
||||||
|
testStep = 3;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
// 先切换到 Pumpkin Valley 地图
|
||||||
|
console.log('\n📤 步骤 5: 切换到 Pumpkin Valley 地图');
|
||||||
|
const positionUpdate = {
|
||||||
|
t: 'position',
|
||||||
|
x: 150,
|
||||||
|
y: 400,
|
||||||
|
mapId: 'pumpkin_valley'
|
||||||
|
};
|
||||||
|
socket.emit('position_update', positionUpdate);
|
||||||
|
|
||||||
|
// 等待位置更新后发送消息
|
||||||
|
setTimeout(() => {
|
||||||
|
const chatMessage2 = {
|
||||||
|
t: 'chat',
|
||||||
|
content: '🎃 在南瓜谷发送的测试消息!',
|
||||||
|
scope: 'local'
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('📤 步骤 6: 在 Pumpkin Valley 发送消息');
|
||||||
|
socket.emit('chat', chatMessage2);
|
||||||
|
}, 1000);
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('chat_render', (data) => {
|
||||||
|
console.log('\n📨 收到来自 Zulip 的消息:');
|
||||||
|
console.log(' 发送者:', data.from);
|
||||||
|
console.log(' 内容:', data.txt);
|
||||||
|
console.log(' Stream:', data.stream || '未知');
|
||||||
|
console.log(' Topic:', data.topic || '未知');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('error', (error) => {
|
||||||
|
console.log('❌ 收到错误:', JSON.stringify(error, null, 2));
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
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(' ⚠️ 部分测试未完成');
|
||||||
|
}
|
||||||
|
console.log('='.repeat(60));
|
||||||
|
process.exit(testStep >= 3 ? 0 : 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('connect_error', (error) => {
|
||||||
|
console.error('❌ 连接错误:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 20秒后自动关闭(给足够时间完成测试)
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('\n⏰ 测试时间到,关闭连接');
|
||||||
|
socket.disconnect();
|
||||||
|
}, 20000);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('\n❌ 测试失败:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行测试
|
||||||
|
testWithUserApiKey();
|
||||||
431
docs/systems/zulip/websocket-protocol.md
Normal file
431
docs/systems/zulip/websocket-protocol.md
Normal file
@@ -0,0 +1,431 @@
|
|||||||
|
# WebSocket 协议详解
|
||||||
|
|
||||||
|
## 协议概述
|
||||||
|
|
||||||
|
Zulip 集成系统使用 WebSocket 协议实现游戏客户端与服务器之间的实时双向通信。所有消息采用 JSON 格式编码。
|
||||||
|
|
||||||
|
## 连接生命周期
|
||||||
|
|
||||||
|
### 1. 建立连接
|
||||||
|
|
||||||
|
```
|
||||||
|
Client Server
|
||||||
|
| |
|
||||||
|
|-------- WebSocket Connect --------->|
|
||||||
|
| |
|
||||||
|
|<------- Connection Accepted --------|
|
||||||
|
| |
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 认证握手
|
||||||
|
|
||||||
|
```
|
||||||
|
Client Server
|
||||||
|
| |
|
||||||
|
|-------- login message ------------->|
|
||||||
|
| |
|
||||||
|
| [验证 Token] |
|
||||||
|
| [创建 Zulip Client] |
|
||||||
|
| [注册 Event Queue] |
|
||||||
|
| [创建 Session] |
|
||||||
|
| |
|
||||||
|
|<------- login_success --------------|
|
||||||
|
| |
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 消息交换
|
||||||
|
|
||||||
|
```
|
||||||
|
Client Server Zulip
|
||||||
|
| | |
|
||||||
|
|-------- chat message -------------->| |
|
||||||
|
| |-------- POST /messages ---------->|
|
||||||
|
| |<------- 200 OK -------------------|
|
||||||
|
|<------- chat_sent ------------------| |
|
||||||
|
| | |
|
||||||
|
| |<------- Event Queue Message ------|
|
||||||
|
|<------- chat_render ----------------| |
|
||||||
|
| | |
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 断开连接
|
||||||
|
|
||||||
|
```
|
||||||
|
Client Server
|
||||||
|
| |
|
||||||
|
|-------- logout message ------------>|
|
||||||
|
| |
|
||||||
|
| [清理 Session] |
|
||||||
|
| [注销 Event Queue] |
|
||||||
|
| [销毁 Zulip Client] |
|
||||||
|
| |
|
||||||
|
|<------- logout_success -------------|
|
||||||
|
| |
|
||||||
|
|-------- WebSocket Close ----------->|
|
||||||
|
| |
|
||||||
|
```
|
||||||
|
|
||||||
|
## 消息格式规范
|
||||||
|
|
||||||
|
### 消息结构
|
||||||
|
|
||||||
|
所有消息都是 JSON 对象,包含以下基本字段:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|-----|------|------|
|
||||||
|
| `type` 或 `t` | string | 消息类型标识 |
|
||||||
|
| 其他字段 | any | 根据消息类型不同而变化 |
|
||||||
|
|
||||||
|
### 消息类型标识
|
||||||
|
|
||||||
|
- 客户端发送的消息使用 `type` 或 `t` 字段
|
||||||
|
- 服务器响应的消息统一使用 `t` 字段
|
||||||
|
|
||||||
|
## 客户端消息
|
||||||
|
|
||||||
|
### LOGIN - 登录认证
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "login",
|
||||||
|
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|-----|------|-----|------|
|
||||||
|
| type | string | 是 | 固定值 "login" |
|
||||||
|
| token | string | 是 | 游戏认证 Token |
|
||||||
|
|
||||||
|
### CHAT - 发送聊天消息
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"t": "chat",
|
||||||
|
"content": "Hello, everyone!",
|
||||||
|
"scope": "local"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|-----|------|-----|------|
|
||||||
|
| t | string | 是 | 固定值 "chat" |
|
||||||
|
| content | string | 是 | 消息内容 (1-1000 字符) |
|
||||||
|
| scope | string | 是 | 消息范围 |
|
||||||
|
|
||||||
|
**scope 取值:**
|
||||||
|
- `"local"`: 当前地图的默认 Topic
|
||||||
|
- `"topic_name"`: 指定的 Topic 名称
|
||||||
|
|
||||||
|
### POSITION - 位置更新
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"t": "position",
|
||||||
|
"x": 150.5,
|
||||||
|
"y": 200.3,
|
||||||
|
"mapId": "novice_village"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|-----|------|-----|------|
|
||||||
|
| t | string | 是 | 固定值 "position" |
|
||||||
|
| x | number | 是 | X 坐标 |
|
||||||
|
| y | number | 是 | Y 坐标 |
|
||||||
|
| mapId | string | 是 | 地图 ID |
|
||||||
|
|
||||||
|
### LOGOUT - 登出
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "logout"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|-----|------|-----|------|
|
||||||
|
| type | string | 是 | 固定值 "logout" |
|
||||||
|
|
||||||
|
## 服务器消息
|
||||||
|
|
||||||
|
### LOGIN_SUCCESS - 登录成功
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"t": "login_success",
|
||||||
|
"sessionId": "sess_abc123def456",
|
||||||
|
"currentMap": "novice_village",
|
||||||
|
"username": "player_name",
|
||||||
|
"stream": "Novice Village"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|-----|------|------|
|
||||||
|
| t | string | 固定值 "login_success" |
|
||||||
|
| sessionId | string | 会话 ID |
|
||||||
|
| currentMap | string | 当前地图 ID |
|
||||||
|
| username | string | 用户名 |
|
||||||
|
| stream | string | 当前 Zulip Stream |
|
||||||
|
|
||||||
|
### CHAT_SENT - 消息发送确认
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"t": "chat_sent",
|
||||||
|
"messageId": "msg_789xyz",
|
||||||
|
"timestamp": 1703500800000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|-----|------|------|
|
||||||
|
| t | string | 固定值 "chat_sent" |
|
||||||
|
| messageId | string | Zulip 消息 ID |
|
||||||
|
| timestamp | number | 发送时间戳 (毫秒) |
|
||||||
|
|
||||||
|
### CHAT_RENDER - 接收聊天消息
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"t": "chat_render",
|
||||||
|
"from": "other_player",
|
||||||
|
"txt": "Hi there!",
|
||||||
|
"bubble": true,
|
||||||
|
"timestamp": 1703500800000,
|
||||||
|
"stream": "Novice Village",
|
||||||
|
"topic": "General"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|-----|------|------|
|
||||||
|
| t | string | 固定值 "chat_render" |
|
||||||
|
| from | string | 发送者名称 |
|
||||||
|
| txt | string | 消息内容 |
|
||||||
|
| bubble | boolean | 是否显示气泡 |
|
||||||
|
| timestamp | number | 消息时间戳 |
|
||||||
|
| stream | string | 来源 Stream |
|
||||||
|
| topic | string | 来源 Topic |
|
||||||
|
|
||||||
|
### POSITION_UPDATED - 位置更新确认
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"t": "position_updated",
|
||||||
|
"stream": "Novice Village",
|
||||||
|
"topic": "General"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|-----|------|------|
|
||||||
|
| t | string | 固定值 "position_updated" |
|
||||||
|
| stream | string | 新的 Zulip Stream |
|
||||||
|
| topic | string | 新的 Zulip Topic |
|
||||||
|
|
||||||
|
### LOGOUT_SUCCESS - 登出成功
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"t": "logout_success"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ERROR - 错误消息
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"t": "error",
|
||||||
|
"code": "RATE_LIMIT",
|
||||||
|
"message": "消息发送过于频繁,请稍后再试",
|
||||||
|
"details": {
|
||||||
|
"retryAfter": 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|-----|------|------|
|
||||||
|
| t | string | 固定值 "error" |
|
||||||
|
| code | string | 错误码 |
|
||||||
|
| message | string | 错误描述 |
|
||||||
|
| details | object | 可选,额外错误信息 |
|
||||||
|
|
||||||
|
## 心跳机制
|
||||||
|
|
||||||
|
### 客户端心跳
|
||||||
|
|
||||||
|
客户端应每 30 秒发送一次心跳消息:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"t": "ping"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 服务器响应
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"t": "pong",
|
||||||
|
"timestamp": 1703500800000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 超时处理
|
||||||
|
|
||||||
|
- 服务器在 60 秒内未收到任何消息将断开连接
|
||||||
|
- 客户端应在连接断开后自动重连
|
||||||
|
|
||||||
|
## 重连策略
|
||||||
|
|
||||||
|
### 指数退避算法
|
||||||
|
|
||||||
|
```
|
||||||
|
重试间隔 = min(baseDelay * 2^attempt, maxDelay)
|
||||||
|
|
||||||
|
baseDelay = 1000ms
|
||||||
|
maxDelay = 30000ms
|
||||||
|
```
|
||||||
|
|
||||||
|
### 重连流程
|
||||||
|
|
||||||
|
1. 检测到连接断开
|
||||||
|
2. 等待重试间隔
|
||||||
|
3. 尝试重新连接
|
||||||
|
4. 连接成功后重新发送 login 消息
|
||||||
|
5. 恢复会话状态
|
||||||
|
|
||||||
|
### 示例代码
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class ReconnectingWebSocket {
|
||||||
|
private baseDelay = 1000;
|
||||||
|
private maxDelay = 30000;
|
||||||
|
private attempt = 0;
|
||||||
|
|
||||||
|
private getDelay(): number {
|
||||||
|
const delay = Math.min(
|
||||||
|
this.baseDelay * Math.pow(2, this.attempt),
|
||||||
|
this.maxDelay
|
||||||
|
);
|
||||||
|
this.attempt++;
|
||||||
|
return delay;
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetDelay(): void {
|
||||||
|
this.attempt = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async reconnect(): Promise<void> {
|
||||||
|
const delay = this.getDelay();
|
||||||
|
console.log(`等待 ${delay}ms 后重连...`);
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.connect();
|
||||||
|
this.resetDelay();
|
||||||
|
} catch (error) {
|
||||||
|
await this.reconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 消息序列化
|
||||||
|
|
||||||
|
### 发送消息
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function sendMessage(socket: WebSocket, message: object): void {
|
||||||
|
const json = JSON.stringify(message);
|
||||||
|
socket.send(json);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 接收消息
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
socket.onmessage = (event: MessageEvent) => {
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(event.data);
|
||||||
|
handleMessage(message);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('消息解析失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 并发处理
|
||||||
|
|
||||||
|
### 消息顺序
|
||||||
|
|
||||||
|
- 同一客户端的消息按发送顺序处理
|
||||||
|
- 不同客户端的消息可能并发处理
|
||||||
|
- 服务器响应顺序可能与请求顺序不同
|
||||||
|
|
||||||
|
### 消息确认
|
||||||
|
|
||||||
|
对于需要确认的操作(如发送聊天消息),客户端应:
|
||||||
|
|
||||||
|
1. 生成唯一的请求 ID
|
||||||
|
2. 等待对应的响应
|
||||||
|
3. 设置超时处理
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function sendChatWithConfirmation(
|
||||||
|
socket: WebSocket,
|
||||||
|
content: string,
|
||||||
|
timeout: number = 5000
|
||||||
|
): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
reject(new Error('发送超时'));
|
||||||
|
}, timeout);
|
||||||
|
|
||||||
|
const handler = (event: MessageEvent) => {
|
||||||
|
const message = JSON.parse(event.data);
|
||||||
|
if (message.t === 'chat_sent') {
|
||||||
|
clearTimeout(timer);
|
||||||
|
socket.removeEventListener('message', handler);
|
||||||
|
resolve();
|
||||||
|
} else if (message.t === 'error') {
|
||||||
|
clearTimeout(timer);
|
||||||
|
socket.removeEventListener('message', handler);
|
||||||
|
reject(new Error(message.message));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.addEventListener('message', handler);
|
||||||
|
|
||||||
|
socket.send(JSON.stringify({
|
||||||
|
t: 'chat',
|
||||||
|
content: content,
|
||||||
|
scope: 'local'
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安全考虑
|
||||||
|
|
||||||
|
### Token 安全
|
||||||
|
|
||||||
|
- Token 仅在 login 消息中传输一次
|
||||||
|
- 服务器验证后不再需要 Token
|
||||||
|
- Token 应有合理的过期时间
|
||||||
|
|
||||||
|
### 消息验证
|
||||||
|
|
||||||
|
- 服务器验证所有消息格式
|
||||||
|
- 拒绝格式错误的消息
|
||||||
|
- 记录异常消息日志
|
||||||
|
|
||||||
|
### 防重放攻击
|
||||||
|
|
||||||
|
- 使用时间戳验证消息新鲜度
|
||||||
|
- 拒绝过期的消息
|
||||||
|
- 检测重复的消息 ID
|
||||||
175
docs/systems/zulip/zulip-js.md
Normal file
175
docs/systems/zulip/zulip-js.md
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
# zulip-js 
|
||||||
|
|
||||||
|
Javascript library to access the Zulip API
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
## Initialization
|
||||||
|
|
||||||
|
### With API Key
|
||||||
|
|
||||||
|
```js
|
||||||
|
const zulipInit = require('zulip-js');
|
||||||
|
const config = {
|
||||||
|
username: process.env.ZULIP_USERNAME,
|
||||||
|
apiKey: process.env.ZULIP_API_KEY,
|
||||||
|
realm: process.env.ZULIP_REALM,
|
||||||
|
};
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const zulip = await zulipInit(config);
|
||||||
|
// The zulip object now initialized with config
|
||||||
|
console.log(await zulip.streams.subscriptions.retrieve());
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Username & Password
|
||||||
|
|
||||||
|
You will need to first retrieve the API key by calling `await zulipInit(config)`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const zulipInit = require('zulip-js');
|
||||||
|
const config = {
|
||||||
|
username: process.env.ZULIP_USERNAME,
|
||||||
|
password: process.env.ZULIP_PASSWORD,
|
||||||
|
realm: process.env.ZULIP_REALM,
|
||||||
|
};
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
// Fetch API Key
|
||||||
|
const zulip = await zulipInit(config);
|
||||||
|
// The zulip object now contains the API Key
|
||||||
|
console.log(await zulip.streams.subscriptions.retrieve());
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
### With zuliprc
|
||||||
|
|
||||||
|
Create a file called `zuliprc` (in the same directory as your code) which looks like:
|
||||||
|
|
||||||
|
```
|
||||||
|
[api]
|
||||||
|
email=cordelia@zulip.com
|
||||||
|
key=wlueAg7cQXqKpUgIaPP3dmF4vibZXal7
|
||||||
|
site=http://localhost:9991
|
||||||
|
```
|
||||||
|
|
||||||
|
Please remember to add this file to your `.gitignore`! Calling `await zulipInit({ zuliprc: 'zuliprc' })` will read this file.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const zulipInit = require('zulip-js');
|
||||||
|
const path = require('path');
|
||||||
|
const zuliprc = path.resolve(__dirname, 'zuliprc');
|
||||||
|
(async () => {
|
||||||
|
const zulip = await zulipInit({ zuliprc });
|
||||||
|
// The zulip object now contains the config from the zuliprc file
|
||||||
|
console.log(await zulip.streams.subscriptions.retrieve());
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Please see some examples in [the examples directory](https://github.com/zulip/zulip-js/tree/main/examples).
|
||||||
|
|
||||||
|
Also, to easily test an API endpoint while developing, you can run:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ npm run build
|
||||||
|
$ npm run call <method> <endpoint> [optional: json_params] [optional: path to zuliprc file]
|
||||||
|
$ # For example:
|
||||||
|
$ npm run call GET /users/me
|
||||||
|
$ npm run call GET /users/me '' ~/path/to/my/zuliprc
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported endpoints
|
||||||
|
|
||||||
|
We support the following endpoints and are striving to have complete coverage of the API. If you want to use some endpoint we do not support presently, you can directly call it as follows:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const params = {
|
||||||
|
to: 'bot testing',
|
||||||
|
type: 'stream',
|
||||||
|
subject: 'Testing zulip-js',
|
||||||
|
content: 'Something is horribly wrong....',
|
||||||
|
};
|
||||||
|
|
||||||
|
await zulip.callEndpoint('/messages', 'POST', params);
|
||||||
|
```
|
||||||
|
|
||||||
|
| Function to call | API Endpoint | Documentation |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `zulip.accounts.retrieve()` | POST `/fetch_api_key` | returns a promise that you can use to retrieve your `API key`. |
|
||||||
|
| `zulip.emojis.retrieve()` | GET `/realm/emoji` | retrieves the list of realm specific emojis. |
|
||||||
|
| `zulip.events.retrieve()` | GET `/events` | retrieves events from a queue. You can pass it a params object with the id of the queue you are interested in, the last event id that you have received and wish to acknowledge. You can also specify whether the server should not block on this request until there is a new event (the default is to block). |
|
||||||
|
| `zulip.messages.send()` | POST `/messages` | returns a promise that can be used to send a message. |
|
||||||
|
| `zulip.messages.retrieve()` | GET `/messages` | returns a promise that can be used to retrieve messages from a stream. You need to specify the id of the message to be used as an anchor. Use `1000000000` to retrieve the most recent message, or [`zulip.users.me.pointer.retrieve()`](#fetching-a-pointer-for-a-user) to get the id of the last message the user read. |
|
||||||
|
| `zulip.messages.render()` | POST `/messages/render` | returns a promise that can be used to get rendered HTML for a message text. |
|
||||||
|
| `zulip.messages.update()` | PATCH `/messages/<msg_id>` | updates the content or topic of the message with the given `msg_id`. |
|
||||||
|
| `zulip.messages.flags.add()` | POST `/messages/flags` | add a flag to a list of messages. Its params are `flag` which is one of `[read, starred, mentioned, wildcard_mentioned, has_alert_word, historical]` and `messages` which is a list of messageIDs. |
|
||||||
|
| `zulip.messages.flags.remove()` | POST `/messages/flags` | remove a flag from a list of messages. Its params are `flag` which is one of `[read, starred, mentioned, wildcard_mentioned, has_alert_word, historical]` and `messages` which is a list of messageIDs. |
|
||||||
|
| `zulip.messages.getById()` | GET `/messages/<msg_id>` | returns a message by its id. |
|
||||||
|
| `zulip.messages.getHistoryById()` | GET `/messages/<msg_id>/history` | return the history of a message |
|
||||||
|
| `zulip.messages.deleteReactionById()` | DELETE `/messages/<msg_id>/reactions` | deletes reactions on a message by message id |
|
||||||
|
| `zulip.messages.deleteById()` | DELETE `/messages/<msg_id>` | delete the message with the provided message id if the user has permission to do so. |
|
||||||
|
| `zulip.queues.register()` | POST `/register` | registers a new queue. You can pass it a params object with the types of events you are interested in and whether you want to receive raw text or html (using markdown). |
|
||||||
|
| `zulip.queues.deregister()` | DELETE `/events` | deletes a previously registered queue. |
|
||||||
|
| `zulip.reactions.add()` | POST `/reactions` | add a reaction to a message. Accepts a params object with `message_id`, `emoji_name`, `emoji_code` and `reaction_type` (default is `unicode_emoji`). |
|
||||||
|
| `zulip.reactions.remove()` | DELETE `/reactions` | remove a reaction from a message. Accepts a params object with `message_id` and `emoji_code` and `reaction_type` (default is `unicode_emoji`). |
|
||||||
|
| `zulip.streams.retrieve()` | GET `/streams` | returns a promise that can be used to retrieve all streams. |
|
||||||
|
| `zulip.streams.getStreamId()` | GET `/get_stream_id` | returns a promise that can be used to retrieve a stream's id. |
|
||||||
|
| `zulip.streams.subscriptions.retrieve()` | GET `/users/me/subscriptions` | returns a promise that can be used to retrieve the user's subscriptions. |
|
||||||
|
| `zulip.streams.deleteById()` | DELETE `/streams/<stream_id>` | delete the stream with the provided stream id if the user has permission to do so. |
|
||||||
|
| `zulip.streams.topics.retrieve()` | GET `/users/me/<stream_id>/topics` | retrieves all the topics in a specific stream. |
|
||||||
|
| `zulip.typing.send()` | POST `/typing` | can be used to send a typing notification. The parameters required are `to` (either a username or a list of usernames) and `op` (either `start` or `stop`). |
|
||||||
|
| `zulip.users.retrieve()` | GET `/users` | retrieves all users for this realm. |
|
||||||
|
| `zulip.users.me.pointer.retrieve()` | GET `/users/me/pointer` | retrieves a pointer for a user. The pointer is the id of the last message the user read. This can then be used as an anchor message id for subsequent API calls. |
|
||||||
|
| `zulip.users.me.getProfile()` | GET `/users/me` | retrieves the profile of the user/bot. |
|
||||||
|
| `zulip.users.me.subscriptions()` | POST `/users/me/subscriptions` | subscribes a user to a stream/streams. |
|
||||||
|
| `zulip.users.create()` | POST `/users` | create a new user. |
|
||||||
|
| `zulip.users.me.alertWords.retrieve()` | GET `/users/me/alert_words` | get array of a user's alert words. |
|
||||||
|
| `zulip.users.me.subscriptions.remove()` | DELETE `/users/me/subscriptions` | remove subscriptions. |
|
||||||
|
| `zulip.users.me.pointer.update()` | POST `users/me/pointer` | updates the pointer for the user, for moving the home view. Accepts a message id. This has the side effect of marking some messages as read. Will not return success if the message id is invalid. Will always succeed if the id is less than the current value of the pointer (the id of the last message read). |
|
||||||
|
| `zulip.server.settings()` | GET `/server_settings` | returns a dictionary of server settings. |
|
||||||
|
| `zulip.filters.retrieve()` | GET `realm/filters` | return a list of filters in a realm |
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
|
||||||
|
Use `npm test` to run the tests.
|
||||||
|
|
||||||
|
## Writing Tests
|
||||||
|
|
||||||
|
Currently, we have a simple testing framework which stubs our network requests and also allows us to test the input passed to it. This is what a sample test for an API endpoint looks like:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const chai = require('chai');
|
||||||
|
const users = require('../../lib/resources/users'); // File to test.
|
||||||
|
const common = require('../common'); // Common functions for tests.
|
||||||
|
|
||||||
|
chai.should();
|
||||||
|
|
||||||
|
describe('Users', () => {
|
||||||
|
it('should fetch users', async () => {
|
||||||
|
const params = {
|
||||||
|
subject: 'test',
|
||||||
|
content: 'sample test',
|
||||||
|
};
|
||||||
|
const validator = (url, options) => {
|
||||||
|
// Function to test the network request parameters.
|
||||||
|
url.should.equal(`${common.config.apiURL}/users`);
|
||||||
|
Object.keys(options.body.data).length.should.equal(4);
|
||||||
|
options.body.data.subject.should.equal(params.subject);
|
||||||
|
options.body.data.content.should.equal(params.content);
|
||||||
|
};
|
||||||
|
const output = {
|
||||||
|
// The data returned by the API in JSON format.
|
||||||
|
already_subscribed: {},
|
||||||
|
result: 'success',
|
||||||
|
};
|
||||||
|
common.stubNetwork(validator, output); // Stub the network modules.
|
||||||
|
const data = await users(common.config).retrieve(params);
|
||||||
|
data.should.have.property('result', 'success'); // Function call.
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Each pull request should contain relevant tests as well as example usage.
|
||||||
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 = {
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
moduleFileExtensions: ['js', 'json', 'ts'],
|
moduleFileExtensions: ['js', 'json', 'ts'],
|
||||||
rootDir: 'src',
|
roots: ['<rootDir>/src', '<rootDir>/test'],
|
||||||
testRegex: '.*\\.spec\\.ts$',
|
testRegex: '.*\\.(spec|e2e-spec|integration-spec|perf-spec)\\.ts$',
|
||||||
transform: {
|
transform: {
|
||||||
'^.+\\.(t|j)s$': 'ts-jest',
|
'^.+\\.(t|j)s$': 'ts-jest',
|
||||||
},
|
},
|
||||||
@@ -11,6 +12,18 @@ module.exports = {
|
|||||||
coverageDirectory: '../coverage',
|
coverageDirectory: '../coverage',
|
||||||
testEnvironment: 'node',
|
testEnvironment: 'node',
|
||||||
moduleNameMapper: {
|
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",
|
"collection": "@nestjs/schematics",
|
||||||
"sourceRoot": "src",
|
"sourceRoot": "src",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"deleteOutDir": true
|
"deleteOutDir": true,
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"include": "../config/**/*",
|
||||||
|
"outDir": "./dist"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
38
package.json
38
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "pixel-game-server",
|
"name": "pixel-game-server",
|
||||||
"version": "1.0.0",
|
"version": "1.2.0",
|
||||||
"description": "A 2D pixel art game server built with NestJS",
|
"description": "A 2D pixel art game server built with NestJS - 完整的游戏服务器,包含用户认证、位置广播、聊天系统、管理员后台等功能模块",
|
||||||
"main": "dist/main.js",
|
"main": "dist/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nest start --watch",
|
"dev": "nest start --watch",
|
||||||
@@ -10,7 +10,12 @@
|
|||||||
"start:prod": "node dist/main.js",
|
"start:prod": "node dist/main.js",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watch",
|
"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": [
|
"keywords": [
|
||||||
"game",
|
"game",
|
||||||
@@ -22,40 +27,59 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@nestjs/cache-manager": "^3.1.0",
|
||||||
"@nestjs/common": "^11.1.9",
|
"@nestjs/common": "^11.1.9",
|
||||||
"@nestjs/config": "^4.0.2",
|
"@nestjs/config": "^4.0.2",
|
||||||
"@nestjs/core": "^11.1.9",
|
"@nestjs/core": "^11.1.9",
|
||||||
"@nestjs/platform-express": "^10.4.20",
|
"@nestjs/jwt": "^11.0.2",
|
||||||
"@nestjs/platform-socket.io": "^10.4.20",
|
"@nestjs/platform-express": "^11.1.11",
|
||||||
|
"@nestjs/platform-ws": "^11.1.11",
|
||||||
"@nestjs/schedule": "^4.1.2",
|
"@nestjs/schedule": "^4.1.2",
|
||||||
"@nestjs/swagger": "^11.2.3",
|
"@nestjs/swagger": "^11.2.3",
|
||||||
|
"@nestjs/throttler": "^6.5.0",
|
||||||
"@nestjs/typeorm": "^11.0.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",
|
"@types/bcrypt": "^6.0.0",
|
||||||
|
"archiver": "^7.0.1",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
|
"cache-manager": "^7.2.8",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.3",
|
"class-validator": "^0.14.3",
|
||||||
"ioredis": "^5.8.2",
|
"ioredis": "^5.8.2",
|
||||||
|
"jsonwebtoken": "^9.0.3",
|
||||||
"mysql2": "^3.16.0",
|
"mysql2": "^3.16.0",
|
||||||
"nestjs-pino": "^4.5.0",
|
"nestjs-pino": "^4.5.0",
|
||||||
|
"nock": "^14.0.10",
|
||||||
|
"node-fetch": "^3.3.2",
|
||||||
"nodemailer": "^6.10.1",
|
"nodemailer": "^6.10.1",
|
||||||
"pino": "^10.1.0",
|
"pino": "^10.1.0",
|
||||||
"reflect-metadata": "^0.1.14",
|
"reflect-metadata": "^0.1.14",
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"swagger-ui-express": "^5.0.1",
|
"swagger-ui-express": "^5.0.1",
|
||||||
"typeorm": "^0.3.28"
|
"typeorm": "^0.3.28",
|
||||||
|
"uuid": "^13.0.0",
|
||||||
|
"ws": "^8.18.3",
|
||||||
|
"zulip-js": "^2.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@faker-js/faker": "^10.2.0",
|
||||||
"@nestjs/cli": "^10.4.9",
|
"@nestjs/cli": "^10.4.9",
|
||||||
"@nestjs/schematics": "^10.2.3",
|
"@nestjs/schematics": "^10.2.3",
|
||||||
"@nestjs/testing": "^10.4.20",
|
"@nestjs/testing": "^10.4.20",
|
||||||
|
"@types/express": "^5.0.6",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^20.19.27",
|
"@types/node": "^20.19.27",
|
||||||
"@types/nodemailer": "^6.4.14",
|
"@types/nodemailer": "^6.4.14",
|
||||||
"@types/supertest": "^6.0.3",
|
"@types/supertest": "^6.0.3",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
|
"cross-env": "^10.1.0",
|
||||||
|
"fast-check": "^4.5.2",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"pino-pretty": "^13.1.3",
|
"pino-pretty": "^13.1.3",
|
||||||
|
"sqlite3": "^5.1.7",
|
||||||
"supertest": "^7.1.4",
|
"supertest": "^7.1.4",
|
||||||
"ts-jest": "^29.2.5",
|
"ts-jest": "^29.2.5",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
packages:
|
||||||
|
- 'client'
|
||||||
|
|
||||||
ignoredBuiltDependencies:
|
ignoredBuiltDependencies:
|
||||||
- '@nestjs/core'
|
- '@nestjs/core'
|
||||||
- '@scarf/scarf'
|
- '@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,12 +1,48 @@
|
|||||||
import { Controller, Get } from '@nestjs/common';
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
|
import { AppStatusResponseDto, ErrorResponseDto } from './business/shared';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用根控制器
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* - 提供应用基础信息和健康检查接口
|
||||||
|
* - 用于监控服务运行状态
|
||||||
|
*
|
||||||
|
* @author moyin
|
||||||
|
* @version 1.0.0
|
||||||
|
* @since 2025-12-17
|
||||||
|
*/
|
||||||
|
@ApiTags('App')
|
||||||
@Controller()
|
@Controller()
|
||||||
export class AppController {
|
export class AppController {
|
||||||
constructor(private readonly appService: AppService) {}
|
constructor(private readonly appService: AppService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取应用状态
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* 返回应用的基本运行状态信息,用于健康检查和监控
|
||||||
|
*
|
||||||
|
* @returns 应用状态信息
|
||||||
|
*/
|
||||||
@Get()
|
@Get()
|
||||||
getStatus(): string {
|
@ApiOperation({
|
||||||
|
summary: '获取应用状态',
|
||||||
|
description: '返回应用的基本运行状态信息,包括服务名称、版本、运行时间等。用于健康检查和服务监控。'
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: '成功获取应用状态',
|
||||||
|
type: AppStatusResponseDto
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 500,
|
||||||
|
description: '服务器内部错误',
|
||||||
|
type: ErrorResponseDto
|
||||||
|
})
|
||||||
|
getStatus(): AppStatusResponseDto {
|
||||||
return this.appService.getStatus();
|
return this.appService.getStatus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,49 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
import { LoggerModule } from './core/utils/logger/logger.module';
|
import { LoggerModule } from './core/utils/logger/logger.module';
|
||||||
import { UsersModule } from './core/db/users/users.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 { 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 { 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
|
||||||
|
*
|
||||||
|
* @returns 是否配置了数据库
|
||||||
|
*/
|
||||||
|
function isDatabaseConfigured(): boolean {
|
||||||
|
const requiredEnvVars = ['DB_HOST', 'DB_PORT', 'DB_USERNAME', 'DB_PASSWORD', 'DB_NAME'];
|
||||||
|
return requiredEnvVars.every(varName => process.env[varName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用主模块
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* - 整合所有功能模块
|
||||||
|
* - 配置全局服务和中间件
|
||||||
|
* - 支持数据库和内存存储的自动切换
|
||||||
|
*
|
||||||
|
* 存储模式选择:
|
||||||
|
* - 如果配置了数据库环境变量,使用数据库模式
|
||||||
|
* - 如果未配置数据库,自动回退到内存模式
|
||||||
|
* - 内存模式适用于快速开发和测试
|
||||||
|
*/
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({
|
ConfigModule.forRoot({
|
||||||
@@ -17,21 +52,61 @@ import { RedisModule } from './core/redis/redis.module';
|
|||||||
}),
|
}),
|
||||||
LoggerModule,
|
LoggerModule,
|
||||||
RedisModule,
|
RedisModule,
|
||||||
TypeOrmModule.forRoot({
|
// 条件导入TypeORM模块
|
||||||
type: 'mysql',
|
...(isDatabaseConfigured() ? [
|
||||||
host: process.env.DB_HOST,
|
TypeOrmModule.forRoot({
|
||||||
port: parseInt(process.env.DB_PORT),
|
type: 'mysql',
|
||||||
username: process.env.DB_USERNAME,
|
host: process.env.DB_HOST,
|
||||||
password: process.env.DB_PASSWORD,
|
port: parseInt(process.env.DB_PORT),
|
||||||
database: process.env.DB_NAME,
|
username: process.env.DB_USERNAME,
|
||||||
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
password: process.env.DB_PASSWORD,
|
||||||
synchronize: false,
|
database: process.env.DB_NAME,
|
||||||
}),
|
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
||||||
UsersModule,
|
synchronize: false,
|
||||||
|
// 字符集配置 - 支持中文和emoji
|
||||||
|
charset: 'utf8mb4',
|
||||||
|
// 添加连接超时和重试配置
|
||||||
|
connectTimeout: 10000,
|
||||||
|
retryAttempts: 3,
|
||||||
|
retryDelay: 3000,
|
||||||
|
}),
|
||||||
|
] : []),
|
||||||
|
// 根据数据库配置选择用户模块模式
|
||||||
|
isDatabaseConfigured() ? UsersModule.forDatabase() : UsersModule.forMemory(),
|
||||||
|
// Zulip账号关联模块 - 全局单例,其他模块无需重复导入
|
||||||
|
ZulipAccountsModule.forRoot(),
|
||||||
LoginCoreModule,
|
LoginCoreModule,
|
||||||
LoginModule,
|
AuthGatewayModule, // 认证网关模块
|
||||||
|
ChatGatewayModule, // 聊天网关模块
|
||||||
|
ZulipGatewayModule, // Zulip网关模块(HTTP API接口)
|
||||||
|
ZulipModule, // Zulip业务模块(业务逻辑)
|
||||||
|
UserMgmtModule,
|
||||||
|
AdminModule,
|
||||||
|
SecurityCoreModule,
|
||||||
|
LocationBroadcastModule,
|
||||||
|
NoticeModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
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,8 +1,52 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { AppStatusResponseDto } from './business/shared';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用服务类
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* - 提供应用基础服务
|
||||||
|
* - 返回应用运行状态信息
|
||||||
|
*
|
||||||
|
* @author angjustinl
|
||||||
|
* @version 1.0.0
|
||||||
|
* @since 2025-12-17
|
||||||
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AppService {
|
export class AppService {
|
||||||
getStatus(): string {
|
private readonly startTime: number;
|
||||||
return 'Pixel Game Server is running!';
|
|
||||||
|
constructor(private readonly configService: ConfigService) {
|
||||||
|
this.startTime = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取应用状态
|
||||||
|
*
|
||||||
|
* @returns 应用状态信息
|
||||||
|
*/
|
||||||
|
getStatus(): AppStatusResponseDto {
|
||||||
|
const isDatabaseConfigured = this.isDatabaseConfigured();
|
||||||
|
|
||||||
|
return {
|
||||||
|
service: 'Pixel Game Server',
|
||||||
|
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'),
|
||||||
|
storageMode: isDatabaseConfigured ? 'database' : 'memory'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查数据库配置是否完整
|
||||||
|
*
|
||||||
|
* @returns 是否配置了数据库
|
||||||
|
*/
|
||||||
|
private isDatabaseConfigured(): boolean {
|
||||||
|
const requiredEnvVars = ['DB_HOST', 'DB_PORT', 'DB_USERNAME', 'DB_PASSWORD', 'DB_NAME'];
|
||||||
|
return requiredEnvVars.every(varName => this.configService.get<string>(varName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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