feat(login, zulip): 引入 JWT 验证并重构 API 密钥管理 #34
31
Dockerfile
31
Dockerfile
@@ -1,31 +0,0 @@
|
||||
# 使用官方 Node.js 镜像
|
||||
FROM node:lts-alpine
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 设置构建参数
|
||||
ARG NPM_REGISTRY=https://registry.npmmirror.com
|
||||
|
||||
# 设置 npm 和 pnpm 镜像源
|
||||
RUN npm config set registry ${NPM_REGISTRY} && \
|
||||
npm install -g pnpm && \
|
||||
pnpm config set registry ${NPM_REGISTRY}
|
||||
|
||||
# 复制 package.json
|
||||
COPY package.json pnpm-workspace.yaml ./
|
||||
|
||||
# 安装依赖
|
||||
RUN pnpm install
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 构建应用
|
||||
RUN pnpm run build
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 3000
|
||||
|
||||
# 启动应用
|
||||
CMD ["pnpm", "run", "start:prod"]
|
||||
63
README.md
63
README.md
@@ -124,29 +124,48 @@ pnpm run dev
|
||||
|
||||
### 第二步:熟悉项目架构 🏗️
|
||||
|
||||
**📁 项目文件结构总览**
|
||||
|
||||
```
|
||||
项目根目录/
|
||||
├── src/ # 源代码目录
|
||||
│ ├── business/ # 业务功能模块(按功能组织)
|
||||
│ │ ├── auth/ # 🔐 用户认证模块
|
||||
│ │ ├── user-mgmt/ # 👥 用户管理模块
|
||||
│ │ ├── admin/ # 🛡️ 管理员模块
|
||||
│ │ ├── security/ # 🔒 安全模块
|
||||
│ │ └── shared/ # 🔗 共享组件
|
||||
│ ├── core/ # 核心技术服务
|
||||
│ │ ├── db/ # 数据库层(支持MySQL/内存双模式)
|
||||
│ │ ├── redis/ # Redis缓存服务(支持真实Redis/文件存储)
|
||||
│ │ ├── login_core/ # 登录核心服务
|
||||
│ │ ├── admin_core/ # 管理员核心服务
|
||||
│ │ └── utils/ # 工具服务(邮件、验证码、日志)
|
||||
│ ├── app.module.ts # 应用主模块
|
||||
│ └── main.ts # 应用入口
|
||||
├── client/ # 前端管理界面
|
||||
├── docs/ # 项目文档
|
||||
├── test/ # 测试文件
|
||||
├── redis-data/ # Redis文件存储数据
|
||||
├── logs/ # 日志文件
|
||||
└── 配置文件 # .env, package.json, tsconfig.json等
|
||||
whale-town-end/ # 🐋 项目根目录
|
||||
├── 📂 src/ # 源代码目录
|
||||
│ ├── 📂 business/ # 🎯 业务功能模块(按功能组织)
|
||||
│ │ ├── 📂 auth/ # 🔐 用户认证模块
|
||||
│ │ ├── 📂 user-mgmt/ # 👥 用户管理模块
|
||||
│ │ ├── 📂 admin/ # 🛡️ 管理员模块
|
||||
│ │ ├── 📂 security/ # 🔒 安全防护模块
|
||||
│ │ ├── 📂 zulip/ # 💬 Zulip集成模块
|
||||
│ │ └── 📂 shared/ # 🔗 共享业务组件
|
||||
│ ├── 📂 core/ # ⚙️ 核心技术服务
|
||||
│ │ ├── 📂 db/ # 🗄️ 数据库层(MySQL/内存双模式)
|
||||
│ │ ├── 📂 redis/ # 🔴 Redis缓存(真实Redis/文件存储)
|
||||
│ │ ├── 📂 login_core/ # 🔑 登录核心服务
|
||||
│ │ ├── 📂 admin_core/ # 👑 管理员核心服务
|
||||
│ │ ├── 📂 zulip/ # 💬 Zulip核心服务
|
||||
│ │ └── 📂 utils/ # 🛠️ 工具服务(邮件、验证码、日志)
|
||||
│ ├── 📄 app.module.ts # 🏠 应用主模块
|
||||
│ └── 📄 main.ts # 🚀 应用入口点
|
||||
├── 📂 client/ # 🎨 前端管理界面
|
||||
│ ├── 📂 src/ # 前端源码
|
||||
│ ├── 📂 dist/ # 前端构建产物
|
||||
│ ├── 📄 package.json # 前端依赖配置
|
||||
│ └── 📄 vite.config.ts # Vite构建配置
|
||||
├── 📂 docs/ # 📚 项目文档中心
|
||||
│ ├── 📂 api/ # 🔌 API接口文档
|
||||
│ ├── 📂 development/ # 💻 开发指南
|
||||
│ ├── 📂 deployment/ # 🚀 部署文档
|
||||
│ ├── 📄 ARCHITECTURE.md # 🏗️ 架构设计文档
|
||||
│ └── 📄 README.md # 📖 文档导航中心
|
||||
├── 📂 test/ # 🧪 测试文件目录
|
||||
├── 📂 config/ # ⚙️ 配置文件目录
|
||||
├── 📂 logs/ # 📝 日志文件存储
|
||||
├── 📂 redis-data/ # 💾 Redis文件存储数据
|
||||
├── 📂 dist/ # 📦 后端构建产物
|
||||
├── 📄 .env # 🔧 环境变量配置
|
||||
├── 📄 package.json # 📋 项目依赖配置
|
||||
├── 📄 docker-compose.yml # 🐳 Docker编排配置
|
||||
├── 📄 Dockerfile # 🐳 Docker镜像配置
|
||||
└── 📄 README.md # 📖 项目主文档(当前文件)
|
||||
```
|
||||
|
||||
**架构特点:**
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 部署脚本模板 - 用于 Gitea Webhook 自动部署
|
||||
# 复制此文件为 deploy.sh 并根据服务器环境修改配置
|
||||
set -e
|
||||
|
||||
echo "开始部署 Pixel Game Server..."
|
||||
|
||||
# 项目路径(根据你的服务器实际路径修改)
|
||||
PROJECT_PATH="/var/www/pixel-game-server"
|
||||
BACKUP_PATH="/var/backups/pixel-game-server"
|
||||
|
||||
# 创建备份
|
||||
echo "创建备份..."
|
||||
mkdir -p $BACKUP_PATH
|
||||
cp -r $PROJECT_PATH $BACKUP_PATH/backup-$(date +%Y%m%d-%H%M%S)
|
||||
|
||||
# 进入项目目录
|
||||
cd $PROJECT_PATH
|
||||
|
||||
# 拉取最新代码
|
||||
echo "拉取最新代码..."
|
||||
git pull origin main
|
||||
|
||||
# 安装/更新依赖
|
||||
echo "安装依赖..."
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
# 构建项目
|
||||
echo "构建项目..."
|
||||
pnpm run build
|
||||
|
||||
# 重启服务
|
||||
echo "重启服务..."
|
||||
if command -v pm2 &> /dev/null; then
|
||||
# 使用 PM2
|
||||
pm2 restart pixel-game-server || pm2 start dist/main.js --name pixel-game-server
|
||||
elif command -v docker-compose &> /dev/null; then
|
||||
# 使用 Docker Compose
|
||||
docker-compose down
|
||||
docker-compose up -d --build
|
||||
else
|
||||
# 使用 systemd
|
||||
sudo systemctl restart pixel-game-server
|
||||
fi
|
||||
|
||||
echo "部署完成!"
|
||||
|
||||
# 清理旧备份(保留最近5个)
|
||||
find $BACKUP_PATH -maxdepth 1 -type d -name "backup-*" | sort -r | tail -n +6 | xargs rm -rf
|
||||
|
||||
echo "服务状态检查..."
|
||||
sleep 5
|
||||
curl -f http://localhost:3000/health || echo "警告:服务健康检查失败"
|
||||
@@ -1,36 +0,0 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DB_HOST=mysql
|
||||
- DB_PORT=3306
|
||||
- DB_USERNAME=pixel_game
|
||||
- DB_PASSWORD=your_password
|
||||
- DB_NAME=pixel_game_db
|
||||
depends_on:
|
||||
- mysql
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=root_password
|
||||
- MYSQL_DATABASE=pixel_game_db
|
||||
- MYSQL_USER=pixel_game
|
||||
- MYSQL_PASSWORD=your_password
|
||||
ports:
|
||||
- "3306:3306"
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
@@ -1,257 +0,0 @@
|
||||
# API 状态码说明
|
||||
|
||||
## 📊 概述
|
||||
|
||||
本文档说明了项目中使用的 HTTP 状态码,特别是针对邮件发送功能的特殊状态码处理。
|
||||
|
||||
## 🔢 标准状态码
|
||||
|
||||
| 状态码 | 含义 | 使用场景 |
|
||||
|--------|------|----------|
|
||||
| 200 | OK | 请求成功 |
|
||||
| 201 | Created | 资源创建成功(如用户注册) |
|
||||
| 400 | Bad Request | 请求参数错误 |
|
||||
| 401 | Unauthorized | 未授权(如密码错误) |
|
||||
| 403 | Forbidden | 权限不足 |
|
||||
| 404 | Not Found | 资源不存在 |
|
||||
| 409 | Conflict | 资源冲突(如用户名已存在) |
|
||||
| 429 | Too Many Requests | 请求频率过高 |
|
||||
| 500 | Internal Server Error | 服务器内部错误 |
|
||||
|
||||
## 🎯 特殊状态码
|
||||
|
||||
### 206 Partial Content - 测试模式
|
||||
|
||||
**使用场景:** 邮件发送功能在测试模式下使用
|
||||
|
||||
**含义:** 请求部分成功,但未完全达到预期效果
|
||||
|
||||
**具体应用:**
|
||||
- 验证码已生成,但邮件未真实发送
|
||||
- 功能正常工作,但处于测试/开发模式
|
||||
- 用户可以获得验证码进行测试,但需要知道这不是真实发送
|
||||
|
||||
**响应示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"data": {
|
||||
"verification_code": "123456",
|
||||
"is_test_mode": true
|
||||
},
|
||||
"message": "⚠️ 测试模式:验证码已生成但未真实发送。请在控制台查看验证码,或配置邮件服务以启用真实发送。",
|
||||
"error_code": "TEST_MODE_ONLY"
|
||||
}
|
||||
```
|
||||
|
||||
## 📧 邮件发送接口状态码
|
||||
|
||||
### 发送邮箱验证码 - POST /auth/send-email-verification
|
||||
|
||||
| 状态码 | 场景 | 响应 |
|
||||
|--------|------|------|
|
||||
| 200 | 邮件真实发送成功 | `{ "success": true, "message": "验证码已发送,请查收邮件" }` |
|
||||
| 206 | 测试模式 | `{ "success": false, "error_code": "TEST_MODE_ONLY", "data": { "verification_code": "123456", "is_test_mode": true } }` |
|
||||
| 400 | 参数错误 | `{ "success": false, "message": "邮箱格式错误" }` |
|
||||
| 429 | 发送频率过高 | `{ "success": false, "message": "发送频率过高,请稍后重试" }` |
|
||||
|
||||
### 发送密码重置验证码 - POST /auth/forgot-password
|
||||
|
||||
| 状态码 | 场景 | 响应 |
|
||||
|--------|------|------|
|
||||
| 200 | 邮件真实发送成功 | `{ "success": true, "message": "验证码已发送,请查收" }` |
|
||||
| 206 | 测试模式 | `{ "success": false, "error_code": "TEST_MODE_ONLY", "data": { "verification_code": "123456", "is_test_mode": true } }` |
|
||||
| 400 | 参数错误 | `{ "success": false, "message": "邮箱格式错误" }` |
|
||||
| 404 | 用户不存在 | `{ "success": false, "message": "用户不存在" }` |
|
||||
|
||||
### 重新发送邮箱验证码 - POST /auth/resend-email-verification
|
||||
|
||||
| 状态码 | 场景 | 响应 |
|
||||
|--------|------|------|
|
||||
| 200 | 邮件真实发送成功 | `{ "success": true, "message": "验证码已重新发送,请查收邮件" }` |
|
||||
| 206 | 测试模式 | `{ "success": false, "error_code": "TEST_MODE_ONLY", "data": { "verification_code": "123456", "is_test_mode": true } }` |
|
||||
| 400 | 邮箱已验证或用户不存在 | `{ "success": false, "message": "邮箱已验证,无需重复验证" }` |
|
||||
| 429 | 发送频率过高 | `{ "success": false, "message": "发送频率过高,请稍后重试" }` |
|
||||
|
||||
## 🔄 模式切换
|
||||
|
||||
### 测试模式 → 真实发送模式
|
||||
|
||||
**配置前(测试模式):**
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/auth/send-email-verification \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email": "test@example.com"}'
|
||||
|
||||
# 响应:206 Partial Content
|
||||
{
|
||||
"success": false,
|
||||
"data": {
|
||||
"verification_code": "123456",
|
||||
"is_test_mode": true
|
||||
},
|
||||
"message": "⚠️ 测试模式:验证码已生成但未真实发送...",
|
||||
"error_code": "TEST_MODE_ONLY"
|
||||
}
|
||||
```
|
||||
|
||||
**配置后(真实发送模式):**
|
||||
```bash
|
||||
# 同样的请求
|
||||
curl -X POST http://localhost:3000/auth/send-email-verification \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email": "test@example.com"}'
|
||||
|
||||
# 响应:200 OK
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"is_test_mode": false
|
||||
},
|
||||
"message": "验证码已发送,请查收邮件"
|
||||
}
|
||||
```
|
||||
|
||||
## 💡 前端处理建议
|
||||
|
||||
### JavaScript 示例
|
||||
|
||||
```javascript
|
||||
async function sendEmailVerification(email) {
|
||||
try {
|
||||
const response = await fetch('/auth/send-email-verification', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.status === 200) {
|
||||
// 真实发送成功
|
||||
showSuccess('验证码已发送,请查收邮件');
|
||||
} else if (response.status === 206) {
|
||||
// 测试模式
|
||||
showWarning(`测试模式:验证码是 ${data.data.verification_code}`);
|
||||
showInfo('请配置邮件服务以启用真实发送');
|
||||
} else {
|
||||
// 其他错误
|
||||
showError(data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
showError('网络错误,请稍后重试');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### React 示例
|
||||
|
||||
```jsx
|
||||
const handleSendVerification = async (email) => {
|
||||
try {
|
||||
const response = await fetch('/auth/send-email-verification', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
switch (response.status) {
|
||||
case 200:
|
||||
setMessage({ type: 'success', text: '验证码已发送,请查收邮件' });
|
||||
break;
|
||||
case 206:
|
||||
setMessage({
|
||||
type: 'warning',
|
||||
text: `测试模式:验证码是 ${data.data.verification_code}`
|
||||
});
|
||||
setShowConfigTip(true);
|
||||
break;
|
||||
case 400:
|
||||
setMessage({ type: 'error', text: data.message });
|
||||
break;
|
||||
case 429:
|
||||
setMessage({ type: 'error', text: '发送频率过高,请稍后重试' });
|
||||
break;
|
||||
default:
|
||||
setMessage({ type: 'error', text: '发送失败,请稍后重试' });
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({ type: 'error', text: '网络错误,请稍后重试' });
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## 🎨 UI 展示建议
|
||||
|
||||
### 测试模式提示
|
||||
|
||||
```html
|
||||
<!-- 成功状态 (200) -->
|
||||
<div class="alert alert-success">
|
||||
✅ 验证码已发送,请查收邮件
|
||||
</div>
|
||||
|
||||
<!-- 测试模式 (206) -->
|
||||
<div class="alert alert-warning">
|
||||
⚠️ 测试模式:验证码是 123456
|
||||
<br>
|
||||
<small>请配置邮件服务以启用真实发送</small>
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 (400+) -->
|
||||
<div class="alert alert-danger">
|
||||
❌ 发送失败:邮箱格式错误
|
||||
</div>
|
||||
```
|
||||
|
||||
## 📝 开发建议
|
||||
|
||||
### 1. 状态码检查
|
||||
|
||||
```javascript
|
||||
// 推荐:明确检查状态码
|
||||
if (response.status === 206) {
|
||||
// 处理测试模式
|
||||
} else if (response.status === 200) {
|
||||
// 处理真实发送
|
||||
}
|
||||
|
||||
// 不推荐:只检查 success 字段
|
||||
if (data.success) {
|
||||
// 可能遗漏测试模式的情况
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 错误处理
|
||||
|
||||
```javascript
|
||||
// 推荐:根据 error_code 进行精确处理
|
||||
switch (data.error_code) {
|
||||
case 'TEST_MODE_ONLY':
|
||||
handleTestMode(data);
|
||||
break;
|
||||
case 'SEND_CODE_FAILED':
|
||||
handleSendFailure(data);
|
||||
break;
|
||||
default:
|
||||
handleGenericError(data);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 用户体验
|
||||
|
||||
- **测试模式**:清晰提示用户当前处于测试模式
|
||||
- **配置引导**:提供配置邮件服务的链接或说明
|
||||
- **验证码显示**:在测试模式下直接显示验证码
|
||||
- **状态区分**:用不同的颜色和图标区分不同状态
|
||||
|
||||
## 🔗 相关文档
|
||||
|
||||
- [邮件服务配置指南](./EMAIL_CONFIGURATION.md)
|
||||
- [快速启动指南](./QUICK_START.md)
|
||||
- [API 文档](./api/README.md)
|
||||
@@ -1,187 +1,773 @@
|
||||
# 🏗️ 项目架构设计
|
||||
# 🏗️ Whale Town 项目架构设计
|
||||
|
||||
## 整体架构
|
||||
> 基于业务功能模块化的现代化后端架构,支持双模式运行,开发测试零依赖,生产部署高性能。
|
||||
|
||||
Whale Town 采用分层架构设计,确保代码的可维护性和可扩展性。
|
||||
## 📋 目录
|
||||
|
||||
- [🎯 架构概述](#-架构概述)
|
||||
- [📁 目录结构详解](#-目录结构详解)
|
||||
- [🏗️ 分层架构设计](#️-分层架构设计)
|
||||
- [🔄 双模式架构](#-双模式架构)
|
||||
- [📦 模块依赖关系](#-模块依赖关系)
|
||||
- [🚀 扩展指南](#-扩展指南)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 架构概述
|
||||
|
||||
Whale Town 采用**业务功能模块化架构**,将代码按业务功能而非技术组件组织,确保高内聚、低耦合的设计原则。
|
||||
|
||||
### 🌟 核心设计理念
|
||||
|
||||
- **业务驱动** - 按业务功能组织代码,而非技术分层
|
||||
- **双模式支持** - 开发测试零依赖,生产部署高性能
|
||||
- **清晰分层** - 业务层 → 核心层 → 数据层,职责明确
|
||||
- **模块化设计** - 每个模块独立完整,可单独测试和部署
|
||||
- **配置驱动** - 通过环境变量控制运行模式和行为
|
||||
|
||||
### 🛠️ 技术栈
|
||||
|
||||
#### 后端技术栈
|
||||
- **框架**: NestJS 11.x (基于Express)
|
||||
- **语言**: TypeScript 5.x
|
||||
- **数据库**: MySQL + TypeORM (生产) / 内存数据库 (开发)
|
||||
- **缓存**: Redis + IORedis (生产) / 文件存储 (开发)
|
||||
- **认证**: JWT + bcrypt
|
||||
- **验证**: class-validator + class-transformer
|
||||
- **文档**: Swagger/OpenAPI
|
||||
- **测试**: Jest + Supertest
|
||||
- **日志**: Pino + nestjs-pino
|
||||
- **WebSocket**: Socket.IO
|
||||
- **邮件**: Nodemailer
|
||||
- **集成**: Zulip API
|
||||
|
||||
#### 前端技术栈
|
||||
- **框架**: React 18.x
|
||||
- **构建工具**: Vite 7.x
|
||||
- **UI库**: Ant Design 5.x
|
||||
- **路由**: React Router DOM 6.x
|
||||
- **语言**: TypeScript 5.x
|
||||
|
||||
### 📊 整体架构图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 🌐 API接口层 │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ 🔗 REST API │ │ 🔌 WebSocket │ │ 📄 Swagger UI │ │
|
||||
│ │ (HTTP接口) │ │ (实时通信) │ │ (API文档) │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
⬇️
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 🎯 业务功能模块层 │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ 🔐 用户认证 │ │ 👥 用户管理 │ │ 🛡️ 管理员 │ │
|
||||
│ │ (auth) │ │ (user_mgmt) │ │ (admin) │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ 💬 Zulip集成 │ │ 🔗 共享组件 │ │ │ │
|
||||
│ │ (zulip) │ │ (shared) │ │ │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
⬇️
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ⚙️ 核心技术服务层 │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ 🔑 登录核心 │ │ 👑 管理员核心 │ │ 💬 Zulip核心 │ │
|
||||
│ │ (auth_core) │ │ (admin_core) │ │ (zulip) │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ 🛡️ 安全核心 │ │ 🛠️ 工具服务 │ │ 📧 邮件服务 │ │
|
||||
│ │ (security_core)│ │ (utils) │ │ (email) │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
⬇️
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 🗄️ 数据存储层 │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ 🗃️ 数据库 │ │ 🔴 Redis缓存 │ │ 📁 文件存储 │ │
|
||||
│ │ (MySQL/内存) │ │ (Redis/文件) │ │ (logs/data) │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📁 目录结构详解
|
||||
|
||||
### 🎯 业务功能模块 (`src/business/`)
|
||||
|
||||
> **设计原则**: 按业务功能组织,每个模块包含完整的业务逻辑
|
||||
|
||||
```
|
||||
src/business/
|
||||
├── 📂 auth/ # 🔐 用户认证模块
|
||||
│ ├── 📄 auth.module.ts # 模块定义
|
||||
│ ├── 📂 controllers/ # 控制器
|
||||
│ │ └── 📄 login.controller.ts # 登录接口控制器
|
||||
│ ├── 📂 services/ # 业务服务
|
||||
│ │ ├── 📄 login.service.ts # 登录业务逻辑
|
||||
│ │ └── 📄 login.service.spec.ts # 登录服务测试
|
||||
│ ├── 📂 dto/ # 数据传输对象
|
||||
│ │ ├── 📄 login.dto.ts # 登录请求DTO
|
||||
│ │ └── 📄 login_response.dto.ts # 登录响应DTO
|
||||
│ └── 📂 guards/ # 权限守卫(预留)
|
||||
│
|
||||
├── 📂 user-mgmt/ # 👥 用户管理模块
|
||||
│ ├── 📄 user-mgmt.module.ts # 模块定义
|
||||
│ ├── 📂 controllers/ # 控制器
|
||||
│ │ └── 📄 user-status.controller.ts # 用户状态管理接口
|
||||
│ ├── 📂 services/ # 业务服务
|
||||
│ │ └── 📄 user-management.service.ts # 用户管理逻辑
|
||||
│ ├── 📂 dto/ # 数据传输对象
|
||||
│ │ ├── 📄 user-status.dto.ts # 用户状态DTO
|
||||
│ │ └── 📄 user-status-response.dto.ts # 状态响应DTO
|
||||
│ ├── 📂 enums/ # 枚举定义
|
||||
│ │ └── 📄 user-status.enum.ts # 用户状态枚举
|
||||
│ └── 📂 tests/ # 测试文件(预留)
|
||||
│
|
||||
├── 📂 admin/ # 🛡️ 管理员模块
|
||||
│ ├── 📄 admin.controller.ts # 管理员接口
|
||||
│ ├── 📄 admin.service.ts # 管理员业务逻辑
|
||||
│ ├── 📄 admin.module.ts # 模块定义
|
||||
│ ├── 📄 admin.service.spec.ts # 管理员服务测试
|
||||
│ ├── 📂 dto/ # 数据传输对象
|
||||
│ └── 📂 guards/ # 权限守卫
|
||||
│
|
||||
├── 📂 zulip/ # 💬 Zulip集成模块
|
||||
│ ├── 📄 zulip.service.ts # Zulip业务服务
|
||||
│ ├── 📄 zulip_websocket.gateway.ts # WebSocket网关
|
||||
│ ├── 📄 zulip.module.ts # 模块定义
|
||||
│ ├── 📂 interfaces/ # 接口定义
|
||||
│ └── 📂 services/ # 子服务
|
||||
│ ├── 📄 message_filter.service.ts # 消息过滤
|
||||
│ └── 📄 session_cleanup.service.ts # 会话清理
|
||||
│
|
||||
└── 📂 shared/ # 🔗 共享业务组件
|
||||
├── 📂 dto/ # 共享数据传输对象
|
||||
└── 📄 index.ts # 导出文件
|
||||
```
|
||||
|
||||
### ⚙️ 核心技术服务 (`src/core/`)
|
||||
|
||||
> **设计原则**: 提供技术基础设施,支持业务模块运行
|
||||
|
||||
```
|
||||
src/core/
|
||||
├── 📂 db/ # 🗄️ 数据库层
|
||||
│ └── 📂 users/ # 用户数据服务
|
||||
│ ├── 📄 users.service.ts # MySQL数据库实现
|
||||
│ ├── 📄 users_memory.service.ts # 内存数据库实现
|
||||
│ ├── 📄 users.dto.ts # 用户数据传输对象
|
||||
│ ├── 📄 users.entity.ts # 用户实体定义
|
||||
│ ├── 📄 users.module.ts # 用户数据模块
|
||||
│ └── 📄 users.service.spec.ts # 用户服务测试
|
||||
│
|
||||
├── 📂 redis/ # 🔴 Redis缓存层
|
||||
│ ├── 📄 redis.module.ts # Redis模块
|
||||
│ ├── 📄 real-redis.service.ts # Redis真实实现
|
||||
│ ├── 📄 file-redis.service.ts # 文件存储实现
|
||||
│ └── 📄 redis.interface.ts # Redis服务接口
|
||||
│
|
||||
├── 📂 login_core/ # 🔑 登录核心服务
|
||||
│ ├── 📄 login_core.service.ts # 登录核心逻辑
|
||||
│ ├── 📄 login_core.module.ts # 模块定义
|
||||
│ └── 📄 login_core.service.spec.ts # 登录核心测试
|
||||
│
|
||||
├── 📂 admin_core/ # 👑 管理员核心服务
|
||||
│ ├── 📄 admin_core.service.ts # 管理员核心逻辑
|
||||
│ ├── 📄 admin_core.module.ts # 模块定义
|
||||
│ └── 📄 admin_core.service.spec.ts # 管理员核心测试
|
||||
│
|
||||
├── 📂 zulip/ # 💬 Zulip核心服务
|
||||
│ ├── 📄 zulip-core.module.ts # Zulip核心模块
|
||||
│ ├── 📂 config/ # 配置文件
|
||||
│ ├── 📂 interfaces/ # 接口定义
|
||||
│ ├── 📂 services/ # 核心服务
|
||||
│ ├── 📂 types/ # 类型定义
|
||||
│ └── 📄 index.ts # 导出文件
|
||||
│
|
||||
├── 📂 security_core/ # 🛡️ 安全核心模块
|
||||
│ ├── 📄 security_core.module.ts # 安全模块定义
|
||||
│ ├── 📂 guards/ # 安全守卫
|
||||
│ │ └── 📄 throttle.guard.ts # 频率限制守卫
|
||||
│ ├── 📂 interceptors/ # 拦截器
|
||||
│ │ └── 📄 timeout.interceptor.ts # 超时拦截器
|
||||
│ ├── 📂 middleware/ # 中间件
|
||||
│ │ ├── 📄 maintenance.middleware.ts # 维护模式中间件
|
||||
│ │ └── 📄 content_type.middleware.ts # 内容类型中间件
|
||||
│ └── 📂 decorators/ # 装饰器
|
||||
│ ├── 📄 throttle.decorator.ts # 频率限制装饰器
|
||||
│ └── 📄 timeout.decorator.ts # 超时装饰器
|
||||
│
|
||||
└── 📂 utils/ # 🛠️ 工具服务
|
||||
├── 📂 email/ # 📧 邮件服务
|
||||
│ ├── 📄 email.service.ts # 邮件发送服务
|
||||
│ ├── 📄 email.module.ts # 邮件模块
|
||||
│ └── 📄 email.service.spec.ts # 邮件服务测试
|
||||
├── 📂 verification/ # 🔢 验证码服务
|
||||
│ ├── 📄 verification.service.ts # 验证码生成验证
|
||||
│ ├── 📄 verification.module.ts # 验证码模块
|
||||
│ └── 📄 verification.service.spec.ts # 验证码服务测试
|
||||
└── 📂 logger/ # 📝 日志服务
|
||||
├── 📄 logger.service.ts # 日志记录服务
|
||||
├── 📄 logger.module.ts # 日志模块
|
||||
├── 📄 logger.config.ts # 日志配置
|
||||
└── 📄 log_management.service.ts # 日志管理服务
|
||||
```
|
||||
|
||||
### 🎨 前端管理界面 (`client/`)
|
||||
|
||||
> **设计原则**: 独立的前端项目,提供管理员后台功能,基于React + Vite + Ant Design
|
||||
|
||||
```
|
||||
client/
|
||||
├── 📂 src/ # 前端源码
|
||||
│ ├── 📂 app/ # 应用组件
|
||||
│ │ ├── 📄 App.tsx # 应用主组件
|
||||
│ │ └── 📄 AdminLayout.tsx # 管理员布局组件
|
||||
│ ├── 📂 pages/ # 页面组件
|
||||
│ │ ├── 📄 LoginPage.tsx # 登录页面
|
||||
│ │ ├── 📄 UsersPage.tsx # 用户管理页面
|
||||
│ │ └── 📄 LogsPage.tsx # 日志管理页面
|
||||
│ ├── 📂 lib/ # 工具库
|
||||
│ │ ├── 📄 api.ts # API客户端
|
||||
│ │ └── 📄 adminAuth.ts # 管理员认证服务
|
||||
│ └── 📄 main.tsx # 应用入口
|
||||
├── 📂 dist/ # 构建产物
|
||||
├── 📄 package.json # 前端依赖
|
||||
├── 📄 vite.config.ts # Vite配置
|
||||
└── 📄 tsconfig.json # TypeScript配置
|
||||
```
|
||||
|
||||
### 📚 文档中心 (`docs/`)
|
||||
|
||||
> **设计原则**: 完整的项目文档,支持开发者快速上手
|
||||
|
||||
```
|
||||
docs/
|
||||
├── 📄 README.md # 📖 文档导航中心
|
||||
├── 📄 ARCHITECTURE.md # 🏗️ 架构设计文档
|
||||
├── 📄 CONTRIBUTORS.md # 🤝 贡献者指南
|
||||
│
|
||||
├── 📂 api/ # 🔌 API接口文档
|
||||
│ ├── 📄 README.md # API文档使用指南
|
||||
│ └── 📄 api-documentation.md # 完整API接口文档
|
||||
│
|
||||
├── 📂 development/ # 💻 开发指南
|
||||
│ ├── 📄 backend_development_guide.md # 后端开发规范
|
||||
│ ├── 📄 git_commit_guide.md # Git提交规范
|
||||
│ ├── 📄 AI辅助开发规范指南.md # AI辅助开发指南
|
||||
│ └── 📄 TESTING.md # 测试指南
|
||||
│
|
||||
└── 📂 deployment/ # 🚀 部署文档
|
||||
└── 📄 DEPLOYMENT.md # 生产环境部署指南
|
||||
```
|
||||
|
||||
### 🧪 测试文件 (`test/`)
|
||||
|
||||
> **设计原则**: 完整的测试覆盖,确保代码质量
|
||||
|
||||
```
|
||||
test/
|
||||
├── 📂 unit/ # 单元测试
|
||||
├── 📂 integration/ # 集成测试
|
||||
├── 📂 e2e/ # 端到端测试
|
||||
└── 📂 fixtures/ # 测试数据
|
||||
```
|
||||
|
||||
### ⚙️ 配置文件
|
||||
|
||||
> **设计原则**: 清晰的配置管理,支持多环境部署
|
||||
|
||||
```
|
||||
项目根目录/
|
||||
├── 📄 .env # 🔧 环境变量配置
|
||||
├── 📄 .env.example # 🔧 环境变量示例
|
||||
├── 📄 .env.production.example # 🔧 生产环境示例
|
||||
├── 📄 package.json # 📋 后端项目依赖配置
|
||||
├── 📄 pnpm-workspace.yaml # 📦 pnpm工作空间配置
|
||||
├── 📄 tsconfig.json # 📘 TypeScript配置
|
||||
├── 📄 jest.config.js # 🧪 Jest测试配置
|
||||
├── 📄 nest-cli.json # 🏠 NestJS CLI配置
|
||||
└── 📄 ecosystem.config.js # 🚀 PM2进程管理配置
|
||||
|
||||
client/
|
||||
├── 📄 package.json # 📋 前端项目依赖配置
|
||||
├── 📄 vite.config.ts # ⚡ Vite构建配置
|
||||
└── 📄 tsconfig.json # 📘 前端TypeScript配置
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 分层架构设计
|
||||
|
||||
### 📊 架构分层说明
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ API 层 │
|
||||
│ 🌐 表现层 (Presentation) │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ HTTP API │ │ WebSocket │ │ GraphQL │ │
|
||||
│ │ (REST) │ │ (Socket.IO) │ │ (预留) │ │
|
||||
│ │ Controllers │ │ WebSocket │ │ Swagger UI │ │
|
||||
│ │ (HTTP接口) │ │ Gateways │ │ (API文档) │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
⬇️
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 业务逻辑层 │
|
||||
│ 🎯 业务层 (Business) │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ 用户认证 │ │ 游戏逻辑 │ │ 社交功能 │ │
|
||||
│ │ (Login) │ │ (Game) │ │ (Social) │ │
|
||||
│ │ Auth Module │ │ UserMgmt │ │ Admin Module │ │
|
||||
│ │ (用户认证) │ │ Module │ │ (管理员) │ │
|
||||
│ │ │ │ (用户管理) │ │ │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ Security Module │ │ Zulip Module │ │ Shared Module │ │
|
||||
│ │ (安全防护) │ │ (Zulip集成) │ │ (共享组件) │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
⬇️
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 核心服务层 │
|
||||
│ ⚙️ 服务层 (Service) │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ 邮件服务 │ │ 验证码服务 │ │ 日志服务 │ │
|
||||
│ │ (Email) │ │ (Verification)│ │ (Logger) │ │
|
||||
│ │ Login Core │ │ Admin Core │ │ Zulip Core │ │
|
||||
│ │ (登录核心) │ │ (管理员核心) │ │ (Zulip核心) │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ Email Service │ │ Verification │ │ Logger Service │ │
|
||||
│ │ (邮件服务) │ │ Service │ │ (日志服务) │ │
|
||||
│ │ │ │ (验证码服务) │ │ │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
⬇️
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 数据访问层 │
|
||||
│ 🗄️ 数据层 (Data) │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ 用户数据 │ │ Redis缓存 │ │ 文件存储 │ │
|
||||
│ │ (Users) │ │ (Cache) │ │ (Files) │ │
|
||||
│ │ Users Service │ │ Redis Service │ │ File Storage │ │
|
||||
│ │ (用户数据) │ │ (缓存服务) │ │ (文件存储) │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ MySQL/Memory │ │ Redis/File │ │ Logs/Data │ │
|
||||
│ │ (数据库) │ │ (缓存实现) │ │ (日志数据) │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 模块依赖关系
|
||||
### 🔄 数据流向
|
||||
|
||||
#### 用户登录流程示例
|
||||
|
||||
```
|
||||
AppModule
|
||||
├── ConfigModule (全局配置)
|
||||
├── LoggerModule (日志系统)
|
||||
├── RedisModule (缓存服务)
|
||||
├── UsersModule (用户管理)
|
||||
│ ├── UsersService (数据库模式)
|
||||
│ └── UsersMemoryService (内存模式)
|
||||
├── EmailModule (邮件服务)
|
||||
├── VerificationModule (验证码服务)
|
||||
├── LoginCoreModule (登录核心)
|
||||
└── LoginModule (登录业务)
|
||||
1. 📱 用户请求 → LoginController.login()
|
||||
2. 🔍 参数验证 → class-validator装饰器
|
||||
3. 🎯 业务逻辑 → LoginService.login()
|
||||
4. ⚙️ 核心服务 → LoginCoreService.validateUser()
|
||||
5. 📧 发送验证码 → VerificationService.generate()
|
||||
6. 💾 存储数据 → UsersService.findByEmail() + RedisService.set()
|
||||
7. 📝 记录日志 → LoggerService.log()
|
||||
8. ✅ 返回响应 → 用户收到登录结果
|
||||
```
|
||||
|
||||
## 数据流向
|
||||
#### 管理员操作流程示例
|
||||
|
||||
### 用户注册流程
|
||||
```
|
||||
1. 用户请求 → LoginController
|
||||
2. 参数验证 → LoginService
|
||||
3. 发送验证码 → LoginCoreService
|
||||
4. 生成验证码 → VerificationService
|
||||
5. 发送邮件 → EmailService
|
||||
6. 存储验证码 → RedisService
|
||||
7. 返回响应 → 用户
|
||||
1. 🛡️ 管理员请求 → AdminController.resetUserPassword()
|
||||
2. 🔐 权限验证 → AdminGuard.canActivate()
|
||||
3. 🎯 业务逻辑 → AdminService.resetPassword()
|
||||
4. ⚙️ 核心服务 → AdminCoreService.resetUserPassword()
|
||||
5. 🔑 密码加密 → bcrypt.hash()
|
||||
6. 💾 更新数据 → UsersService.update()
|
||||
7. 📧 通知用户 → EmailService.sendPasswordReset()
|
||||
8. 📝 审计日志 → LoggerService.audit()
|
||||
9. ✅ 返回响应 → 管理员收到操作结果
|
||||
```
|
||||
|
||||
### 双模式架构
|
||||
---
|
||||
|
||||
项目支持开发测试模式和生产部署模式的无缝切换:
|
||||
## 🔄 双模式架构
|
||||
|
||||
#### 开发测试模式
|
||||
- **数据库**: 内存存储 (UsersMemoryService)
|
||||
- **缓存**: 文件存储 (FileRedisService)
|
||||
- **邮件**: 控制台输出 (测试模式)
|
||||
- **优势**: 无需外部依赖,快速启动测试
|
||||
### 🎯 设计目标
|
||||
|
||||
#### 生产部署模式
|
||||
- **数据库**: MySQL (UsersService + TypeORM)
|
||||
- **缓存**: Redis (RealRedisService + IORedis)
|
||||
- **邮件**: SMTP服务器 (生产模式)
|
||||
- **优势**: 高性能,高可用,数据持久化
|
||||
- **开发测试**: 零依赖快速启动,无需安装MySQL、Redis等外部服务
|
||||
- **生产部署**: 高性能、高可用,支持集群和负载均衡
|
||||
|
||||
## 设计原则
|
||||
### 📊 模式对比
|
||||
|
||||
### 1. 单一职责原则
|
||||
每个模块只负责一个特定的功能领域:
|
||||
- `LoginModule`: 只处理登录相关业务
|
||||
- `EmailModule`: 只处理邮件发送
|
||||
- `VerificationModule`: 只处理验证码逻辑
|
||||
| 功能模块 | 🧪 开发测试模式 | 🚀 生产部署模式 |
|
||||
|----------|----------------|----------------|
|
||||
| **数据库** | 内存存储 (UsersMemoryService) | MySQL (UsersService + TypeORM) |
|
||||
| **缓存** | 文件存储 (FileRedisService) | Redis (RealRedisService + IORedis) |
|
||||
| **邮件** | 控制台输出 (测试模式) | SMTP服务器 (生产模式) |
|
||||
| **日志** | 控制台 + 文件 | 结构化日志 + 日志轮转 |
|
||||
| **配置** | `.env` 默认配置 | 环境变量 + 配置中心 |
|
||||
|
||||
### 2. 依赖注入
|
||||
使用NestJS的依赖注入系统:
|
||||
- 接口抽象: `IRedisService`, `IUsersService`
|
||||
- 实现切换: 根据配置自动选择实现类
|
||||
- 测试友好: 易于Mock和单元测试
|
||||
### ⚙️ 模式切换配置
|
||||
|
||||
### 3. 配置驱动
|
||||
通过环境变量控制行为:
|
||||
- `USE_FILE_REDIS`: 选择Redis实现
|
||||
- `DB_HOST`: 数据库连接配置
|
||||
- `EMAIL_HOST`: 邮件服务配置
|
||||
#### 开发测试模式 (.env)
|
||||
```bash
|
||||
# 数据存储模式
|
||||
USE_FILE_REDIS=true # 使用文件存储代替Redis
|
||||
NODE_ENV=development # 开发环境
|
||||
|
||||
### 4. 错误处理
|
||||
统一的错误处理机制:
|
||||
- HTTP异常: `BadRequestException`, `UnauthorizedException`
|
||||
- 业务异常: 自定义异常类
|
||||
- 日志记录: 结构化错误日志
|
||||
# 数据库配置(注释掉,使用内存数据库)
|
||||
# DB_HOST=localhost
|
||||
# DB_USERNAME=root
|
||||
# DB_PASSWORD=password
|
||||
|
||||
## 扩展指南
|
||||
# 邮件配置(注释掉,使用测试模式)
|
||||
# EMAIL_HOST=smtp.gmail.com
|
||||
# EMAIL_USER=your_email@gmail.com
|
||||
# EMAIL_PASS=your_password
|
||||
```
|
||||
|
||||
### 添加新的业务模块
|
||||
#### 生产部署模式 (.env.production)
|
||||
```bash
|
||||
# 数据存储模式
|
||||
USE_FILE_REDIS=false # 使用真实Redis
|
||||
NODE_ENV=production # 生产环境
|
||||
|
||||
1. **创建业务模块**
|
||||
```bash
|
||||
nest g module business/game
|
||||
nest g controller business/game
|
||||
nest g service business/game
|
||||
```
|
||||
# 数据库配置
|
||||
DB_HOST=your_mysql_host
|
||||
DB_PORT=3306
|
||||
DB_USERNAME=your_username
|
||||
DB_PASSWORD=your_password
|
||||
DB_DATABASE=whale_town
|
||||
|
||||
2. **创建核心服务**
|
||||
```bash
|
||||
nest g module core/game_core
|
||||
nest g service core/game_core
|
||||
```
|
||||
# Redis配置
|
||||
REDIS_HOST=your_redis_host
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=your_redis_password
|
||||
|
||||
3. **添加数据模型**
|
||||
```bash
|
||||
nest g module core/db/games
|
||||
nest g service core/db/games
|
||||
```
|
||||
# 邮件配置
|
||||
EMAIL_HOST=smtp.gmail.com
|
||||
EMAIL_PORT=587
|
||||
EMAIL_USER=your_email@gmail.com
|
||||
EMAIL_PASS=your_app_password
|
||||
```
|
||||
|
||||
4. **更新主模块**
|
||||
在 `app.module.ts` 中导入新模块
|
||||
### 🔧 实现机制
|
||||
|
||||
### 添加新的工具服务
|
||||
#### 依赖注入切换
|
||||
```typescript
|
||||
// redis.module.ts
|
||||
@Module({
|
||||
providers: [
|
||||
{
|
||||
provide: 'IRedisService',
|
||||
useFactory: (configService: ConfigService) => {
|
||||
const useFileRedis = configService.get<boolean>('USE_FILE_REDIS');
|
||||
return useFileRedis
|
||||
? new FileRedisService()
|
||||
: new RealRedisService(configService);
|
||||
},
|
||||
inject: [ConfigService],
|
||||
},
|
||||
],
|
||||
})
|
||||
export class RedisModule {}
|
||||
```
|
||||
|
||||
1. **创建工具模块**
|
||||
```bash
|
||||
nest g module core/utils/notification
|
||||
nest g service core/utils/notification
|
||||
```
|
||||
#### 配置驱动服务选择
|
||||
```typescript
|
||||
// users.module.ts
|
||||
@Module({
|
||||
providers: [
|
||||
{
|
||||
provide: 'IUsersService',
|
||||
useFactory: (configService: ConfigService) => {
|
||||
const dbHost = configService.get<string>('DB_HOST');
|
||||
return dbHost
|
||||
? new UsersService()
|
||||
: new UsersMemoryService();
|
||||
},
|
||||
inject: [ConfigService],
|
||||
},
|
||||
],
|
||||
})
|
||||
export class UsersModule {}
|
||||
```
|
||||
|
||||
2. **实现服务接口**
|
||||
定义抽象接口和具体实现
|
||||
---
|
||||
|
||||
3. **添加配置支持**
|
||||
在环境变量中添加相关配置
|
||||
## 📦 模块依赖关系
|
||||
|
||||
4. **编写测试用例**
|
||||
确保功能正确性和代码覆盖率
|
||||
### 🏗️ 模块依赖图
|
||||
|
||||
## 性能优化
|
||||
```
|
||||
AppModule (应用主模块)
|
||||
├── 📊 ConfigModule (全局配置)
|
||||
├── 📝 LoggerModule (日志系统)
|
||||
├── 🔴 RedisModule (缓存服务)
|
||||
│ ├── RealRedisService (真实Redis)
|
||||
│ └── FileRedisService (文件存储)
|
||||
├── 🗄️ UsersModule (用户数据)
|
||||
│ ├── UsersService (MySQL数据库)
|
||||
│ └── UsersMemoryService (内存数据库)
|
||||
├── 📧 EmailModule (邮件服务)
|
||||
├── 🔢 VerificationModule (验证码服务)
|
||||
├── 🔑 LoginCoreModule (登录核心)
|
||||
├── 👑 AdminCoreModule (管理员核心)
|
||||
├── 💬 ZulipCoreModule (Zulip核心)
|
||||
├── 🔒 SecurityCoreModule (安全核心)
|
||||
│
|
||||
├── 🎯 业务功能模块
|
||||
│ ├── 🔐 AuthModule (用户认证)
|
||||
│ │ └── 依赖: LoginCoreModule, EmailModule, VerificationModule, SecurityCoreModule
|
||||
│ ├── 👥 UserMgmtModule (用户管理)
|
||||
│ │ └── 依赖: UsersModule, LoggerModule, SecurityCoreModule
|
||||
│ ├── 🛡️ AdminModule (管理员)
|
||||
│ │ └── 依赖: AdminCoreModule, UsersModule, SecurityCoreModule
|
||||
│ ├── 💬 ZulipModule (Zulip集成)
|
||||
│ │ └── 依赖: ZulipCoreModule, RedisModule
|
||||
│ └── 🔗 SharedModule (共享组件)
|
||||
```
|
||||
|
||||
### 1. 缓存策略
|
||||
- **Redis缓存**: 验证码、会话信息
|
||||
### 🔄 模块交互流程
|
||||
|
||||
#### 用户认证流程
|
||||
```
|
||||
AuthController → LoginService → LoginCoreService
|
||||
↓
|
||||
EmailService ← VerificationService ← RedisService
|
||||
↓
|
||||
UsersService
|
||||
```
|
||||
|
||||
#### 管理员操作流程
|
||||
```
|
||||
AdminController → AdminService → AdminCoreService
|
||||
↓
|
||||
LoggerService ← UsersService ← RedisService
|
||||
```
|
||||
|
||||
#### 安全防护流程
|
||||
```
|
||||
SecurityGuard → RedisService (频率限制)
|
||||
→ LoggerService (审计日志)
|
||||
→ ConfigService (维护模式)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 扩展指南
|
||||
|
||||
### 📝 添加新的业务模块
|
||||
|
||||
#### 1. 创建业务模块结构
|
||||
```bash
|
||||
# 创建模块目录
|
||||
mkdir -p src/business/game/{dto,enums,guards,interfaces}
|
||||
|
||||
# 生成NestJS模块文件
|
||||
nest g module business/game
|
||||
nest g controller business/game
|
||||
nest g service business/game
|
||||
```
|
||||
|
||||
#### 2. 实现业务逻辑
|
||||
```typescript
|
||||
// src/business/game/game.module.ts
|
||||
@Module({
|
||||
imports: [
|
||||
GameCoreModule, # 依赖核心服务
|
||||
UsersModule, # 依赖用户数据
|
||||
RedisModule, # 依赖缓存服务
|
||||
],
|
||||
controllers: [GameController],
|
||||
providers: [GameService],
|
||||
exports: [GameService],
|
||||
})
|
||||
export class GameModule {}
|
||||
```
|
||||
|
||||
#### 3. 创建对应的核心服务
|
||||
```bash
|
||||
# 创建核心服务
|
||||
mkdir -p src/core/game_core
|
||||
nest g module core/game_core
|
||||
nest g service core/game_core
|
||||
```
|
||||
|
||||
#### 4. 更新主模块
|
||||
```typescript
|
||||
// src/app.module.ts
|
||||
@Module({
|
||||
imports: [
|
||||
// ... 其他模块
|
||||
GameModule, # 添加新的业务模块
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
```
|
||||
|
||||
### 🛠️ 添加新的工具服务
|
||||
|
||||
#### 1. 创建工具服务
|
||||
```bash
|
||||
mkdir -p src/core/utils/notification
|
||||
nest g module core/utils/notification
|
||||
nest g service core/utils/notification
|
||||
```
|
||||
|
||||
#### 2. 定义服务接口
|
||||
```typescript
|
||||
// src/core/utils/notification/notification.interface.ts
|
||||
export interface INotificationService {
|
||||
sendPush(userId: string, message: string): Promise<void>;
|
||||
sendSMS(phone: string, message: string): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 实现服务
|
||||
```typescript
|
||||
// src/core/utils/notification/notification.service.ts
|
||||
@Injectable()
|
||||
export class NotificationService implements INotificationService {
|
||||
async sendPush(userId: string, message: string): Promise<void> {
|
||||
// 实现推送通知逻辑
|
||||
}
|
||||
|
||||
async sendSMS(phone: string, message: string): Promise<void> {
|
||||
// 实现短信发送逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. 配置依赖注入
|
||||
```typescript
|
||||
// src/core/utils/notification/notification.module.ts
|
||||
@Module({
|
||||
providers: [
|
||||
{
|
||||
provide: 'INotificationService',
|
||||
useClass: NotificationService,
|
||||
},
|
||||
],
|
||||
exports: ['INotificationService'],
|
||||
})
|
||||
export class NotificationModule {}
|
||||
```
|
||||
|
||||
### 🔌 添加新的API接口
|
||||
|
||||
#### 1. 定义DTO
|
||||
```typescript
|
||||
// src/business/game/dto/create-game.dto.ts
|
||||
export class CreateGameDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 实现Controller
|
||||
```typescript
|
||||
// src/business/game/game.controller.ts
|
||||
@Controller('game')
|
||||
@ApiTags('游戏管理')
|
||||
export class GameController {
|
||||
constructor(private readonly gameService: GameService) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: '创建游戏' })
|
||||
async createGame(@Body() createGameDto: CreateGameDto) {
|
||||
return this.gameService.create(createGameDto);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 实现Service
|
||||
```typescript
|
||||
// src/business/game/game.service.ts
|
||||
@Injectable()
|
||||
export class GameService {
|
||||
constructor(
|
||||
@Inject('IGameCoreService')
|
||||
private readonly gameCoreService: IGameCoreService,
|
||||
) {}
|
||||
|
||||
async create(createGameDto: CreateGameDto) {
|
||||
return this.gameCoreService.createGame(createGameDto);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. 添加测试用例
|
||||
```typescript
|
||||
// src/business/game/game.service.spec.ts
|
||||
describe('GameService', () => {
|
||||
let service: GameService;
|
||||
let gameCoreService: jest.Mocked<IGameCoreService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
GameService,
|
||||
{
|
||||
provide: 'IGameCoreService',
|
||||
useValue: {
|
||||
createGame: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<GameService>(GameService);
|
||||
gameCoreService = module.get('IGameCoreService');
|
||||
});
|
||||
|
||||
it('should create game', async () => {
|
||||
const createGameDto = { name: 'Test Game' };
|
||||
const expectedResult = { id: 1, ...createGameDto };
|
||||
|
||||
gameCoreService.createGame.mockResolvedValue(expectedResult);
|
||||
|
||||
const result = await service.create(createGameDto);
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
expect(gameCoreService.createGame).toHaveBeenCalledWith(createGameDto);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 📊 性能优化建议
|
||||
|
||||
#### 1. 缓存策略
|
||||
- **Redis缓存**: 用户会话、验证码、频繁查询数据
|
||||
- **内存缓存**: 配置信息、静态数据
|
||||
- **CDN缓存**: 静态资源文件
|
||||
|
||||
### 2. 数据库优化
|
||||
- **连接池**: 复用数据库连接
|
||||
- **索引优化**: 关键字段建立索引
|
||||
- **查询优化**: 避免N+1查询问题
|
||||
#### 2. 数据库优化
|
||||
- **连接池**: 复用数据库连接,减少连接开销
|
||||
- **索引优化**: 为查询字段建立合适的索引
|
||||
- **查询优化**: 避免N+1查询,使用JOIN优化关联查询
|
||||
|
||||
### 3. 日志优化
|
||||
- **异步日志**: 使用Pino的异步写入
|
||||
- **日志分级**: 生产环境只记录必要日志
|
||||
#### 3. 日志优化
|
||||
- **异步日志**: 使用Pino的异步写入功能
|
||||
- **日志分级**: 生产环境只记录ERROR和WARN级别
|
||||
- **日志轮转**: 自动清理过期日志文件
|
||||
|
||||
## 安全考虑
|
||||
### 🔒 安全加固建议
|
||||
|
||||
### 1. 数据验证
|
||||
- **输入验证**: class-validator装饰器
|
||||
- **类型检查**: TypeScript静态类型
|
||||
- **SQL注入**: TypeORM参数化查询
|
||||
#### 1. 数据验证
|
||||
- **输入验证**: 使用class-validator进行严格验证
|
||||
- **类型检查**: TypeScript静态类型检查
|
||||
- **SQL注入防护**: TypeORM参数化查询
|
||||
|
||||
### 2. 认证授权
|
||||
- **密码加密**: bcrypt哈希算法
|
||||
- **会话管理**: Redis存储会话信息
|
||||
- **权限控制**: 基于角色的访问控制
|
||||
#### 2. 认证授权
|
||||
- **密码安全**: bcrypt加密,强密码策略
|
||||
- **会话管理**: JWT + Redis会话存储
|
||||
- **权限控制**: 基于角色的访问控制(RBAC)
|
||||
|
||||
### 3. 通信安全
|
||||
#### 3. 通信安全
|
||||
- **HTTPS**: 生产环境强制HTTPS
|
||||
- **CORS**: 跨域请求控制
|
||||
- **Rate Limiting**: API请求频率限制
|
||||
- **CORS**: 严格的跨域请求控制
|
||||
- **Rate Limiting**: API请求频率限制
|
||||
|
||||
---
|
||||
|
||||
**🏗️ 通过清晰的架构设计,Whale Town 实现了高内聚、低耦合的模块化架构,支持快速开发和灵活部署!**
|
||||
@@ -9,18 +9,22 @@
|
||||
**moyin** - 主要维护者
|
||||
- Gitea: [@moyin](https://gitea.xinghangee.icu/moyin)
|
||||
- Email: xinghang_a@proton.me
|
||||
- 提交数: **66 commits**
|
||||
- 提交数: **112 commits**
|
||||
- 主要贡献:
|
||||
- 🚀 项目架构设计与初始化
|
||||
- 🔐 完整用户认证系统实现
|
||||
- 📧 邮箱验证系统设计与开发
|
||||
- 🗄️ Redis缓存服务(文件存储+真实Redis双模式)
|
||||
- 📝 完整的API文档系统(Swagger UI + OpenAPI)
|
||||
- 🧪 测试框架搭建与114个测试用例编写
|
||||
- 🧪 测试框架搭建与507个测试用例编写
|
||||
- 📊 高性能日志系统集成(Pino)
|
||||
- 🔧 项目配置优化与部署方案
|
||||
- 🐛 验证码TTL重置关键问题修复
|
||||
- 📚 完整的项目文档体系建设
|
||||
- 🏗️ **Zulip模块架构重构** - 业务功能模块化架构设计与实现
|
||||
- 📖 **架构文档重写** - 详细的架构设计文档和开发者指南
|
||||
- 🔄 **验证码冷却时间优化** - 自动清除机制设计与实现
|
||||
- 📋 **文档清理优化** - 项目文档结构化整理和维护体系建立
|
||||
|
||||
### 🌟 核心开发者
|
||||
|
||||
@@ -28,18 +32,21 @@
|
||||
- Gitea: [@ANGJustinl](https://gitea.xinghangee.icu/ANGJustinl)
|
||||
- GitHub: [@ANGJustinl](https://github.com/ANGJustinl)
|
||||
- Email: 96008766+ANGJustinl@users.noreply.github.com
|
||||
- 提交数: **2 commits**
|
||||
- 提交数: **7 commits**
|
||||
- 主要贡献:
|
||||
- 🔄 邮箱验证流程重构与优化
|
||||
- 💾 基于内存的用户服务实现
|
||||
- 🛠️ API响应处理改进
|
||||
- 🧪 测试用例完善与错误修复
|
||||
- 📚 系统架构优化
|
||||
- 💬 **Zulip集成系统** - 完整的Zulip实时通信系统开发
|
||||
- 🔧 **E2E测试修复** - Zulip集成的端到端测试优化
|
||||
- 🎯 **验证码登录测试** - 验证码登录功能测试用例编写
|
||||
|
||||
**jianuo** - 核心开发者
|
||||
- Gitea: [@jianuo](https://gitea.xinghangee.icu/jianuo)
|
||||
- Email: 32106500027@e.gzhu.edu.cn
|
||||
- 提交数: **6 commits**
|
||||
- 提交数: **11 commits**
|
||||
- 主要贡献:
|
||||
- 🎛️ **管理员后台系统** - 完整的前后端管理界面开发
|
||||
- 📊 **日志管理功能** - 运行时日志查看与下载系统
|
||||
@@ -48,14 +55,42 @@
|
||||
- ⚙️ **TypeScript配置优化** - Node16模块解析配置
|
||||
- 🐳 **Docker部署优化** - 容器化部署问题修复
|
||||
- 📖 **技术栈文档更新** - 项目技术栈说明完善
|
||||
- 🔧 **项目配置优化** - 构建和开发环境配置改进
|
||||
|
||||
## 贡献统计
|
||||
|
||||
| 贡献者 | 提交数 | 主要领域 | 贡献占比 |
|
||||
|--------|--------|----------|----------|
|
||||
| moyin | 66 | 架构设计、核心功能、文档、测试 | 88% |
|
||||
| jianuo | 6 | 管理员后台、日志系统、部署优化 | 8% |
|
||||
| angjustinl | 2 | 功能优化、测试、重构 | 3% |
|
||||
| moyin | 112 | 架构设计、核心功能、文档、测试、Zulip重构 | 86% |
|
||||
| jianuo | 11 | 管理员后台、日志系统、部署优化、配置管理 | 8% |
|
||||
| angjustinl | 7 | Zulip集成、功能优化、测试、重构 | 5% |
|
||||
|
||||
## 🌟 最新重要贡献
|
||||
|
||||
### 🏗️ Zulip模块架构重构 (2025年12月31日)
|
||||
**主要贡献者**: moyin, angjustinl
|
||||
|
||||
这是项目历史上最重要的架构重构之一:
|
||||
|
||||
- **架构重构**: 实现业务功能模块化架构,将Zulip模块按照业务层和核心层进行清晰分离
|
||||
- **代码迁移**: 36个文件的重构和迁移,涉及2773行代码的新增和125行的删除
|
||||
- **依赖注入**: 通过接口抽象实现业务层与核心层的完全解耦
|
||||
- **测试完善**: 所有507个测试用例通过,确保重构的安全性
|
||||
|
||||
### 📚 项目文档体系优化 (2025年12月31日)
|
||||
**主要贡献者**: moyin
|
||||
|
||||
- **架构文档重写**: `docs/ARCHITECTURE.md` 从简单架构图扩展为800+行的完整架构设计文档
|
||||
- **README优化**: 采用总分结构设计,详细的文件结构总览
|
||||
- **文档清理**: 新增 `docs/DOCUMENT_CLEANUP.md` 记录文档维护过程
|
||||
- **开发者体验**: 建立完整的文档导航体系,提升开发者上手体验
|
||||
|
||||
### 💬 Zulip集成系统 (2025年12月25日)
|
||||
**主要贡献者**: angjustinl
|
||||
|
||||
- **完整集成**: 实现与Zulip的完整集成,支持实时通信功能
|
||||
- **WebSocket支持**: 建立稳定的WebSocket连接和消息处理机制
|
||||
- **测试覆盖**: 完善的E2E测试确保集成功能的稳定性
|
||||
|
||||
## 项目里程碑
|
||||
|
||||
@@ -72,6 +107,13 @@
|
||||
- **12月20日**: jianuo完善日志管理功能
|
||||
- **12月21日**: jianuo添加管理员后台单元测试
|
||||
- **12月22日**: 管理员后台功能合并到主分支
|
||||
- **12月25日**: angjustinl开发完整的Zulip集成系统
|
||||
- **12月25日**: 实现验证码冷却时间自动清除机制
|
||||
- **12月25日**: 完成邮箱冲突检测优化v1.1.1
|
||||
- **12月25日**: 升级项目版本到v1.1.0
|
||||
- **12月31日**: **重大架构重构** - 完成Zulip模块业务功能模块化架构重构
|
||||
- **12月31日**: **文档体系优化** - 项目文档结构化整理和架构文档重写
|
||||
- **12月31日**: **测试覆盖完善** - 所有507个测试用例通过,测试覆盖率达到新高
|
||||
|
||||
## 如何成为贡献者
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
### 📋 **项目管理**
|
||||
- [贡献指南](CONTRIBUTORS.md) - 如何参与项目贡献
|
||||
- [文档清理说明](DOCUMENT_CLEANUP.md) - 文档维护记录
|
||||
- [文档清理说明](DOCUMENT_CLEANUP.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 |
@@ -1,3 +1,7 @@
|
||||
|
||||
|
||||

|
||||
|
||||
# Git 提交规范
|
||||
|
||||
本文档定义了项目的 Git 提交信息格式规范,以保持提交历史的清晰和一致性。
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
### 连接地址
|
||||
|
||||
```
|
||||
ws://localhost:3000/game
|
||||
wss://localhost:3000/game
|
||||
```
|
||||
|
||||
### 连接参数
|
||||
|
||||
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();
|
||||
@@ -3,6 +3,12 @@
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
"deleteOutDir": true,
|
||||
"assets": [
|
||||
{
|
||||
"include": "../config/**/*",
|
||||
"outDir": "./dist"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,13 @@ import { LoggerModule } from './core/utils/logger/logger.module';
|
||||
import { UsersModule } from './core/db/users/users.module';
|
||||
import { LoginCoreModule } from './core/login_core/login_core.module';
|
||||
import { AuthModule } from './business/auth/auth.module';
|
||||
import { ZulipModule } from './business/zulip/zulip.module';
|
||||
import { RedisModule } from './core/redis/redis.module';
|
||||
import { AdminModule } from './business/admin/admin.module';
|
||||
import { UserMgmtModule } from './business/user-mgmt/user-mgmt.module';
|
||||
import { SecurityModule } from './business/security/security.module';
|
||||
import { MaintenanceMiddleware } from './business/security/middleware/maintenance.middleware';
|
||||
import { ContentTypeMiddleware } from './business/security/middleware/content-type.middleware';
|
||||
import { SecurityCoreModule } from './core/security_core/security_core.module';
|
||||
import { MaintenanceMiddleware } from './core/security_core/middleware/maintenance.middleware';
|
||||
import { ContentTypeMiddleware } from './core/security_core/middleware/content_type.middleware';
|
||||
|
||||
/**
|
||||
* 检查数据库配置是否完整 by angjustinl 2025-12-17
|
||||
@@ -67,9 +68,10 @@ function isDatabaseConfigured(): boolean {
|
||||
isDatabaseConfigured() ? UsersModule.forDatabase() : UsersModule.forMemory(),
|
||||
LoginCoreModule,
|
||||
AuthModule,
|
||||
ZulipModule,
|
||||
UserMgmtModule,
|
||||
AdminModule,
|
||||
SecurityModule,
|
||||
SecurityCoreModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
AdminUserResponseDto,
|
||||
AdminRuntimeLogsResponseDto
|
||||
} from './dto/admin-response.dto';
|
||||
import { Throttle, ThrottlePresets } from '../security/decorators/throttle.decorator';
|
||||
import { Throttle, ThrottlePresets } from '../../core/security_core/decorators/throttle.decorator';
|
||||
import type { Response } from 'express';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
@@ -16,11 +16,19 @@ import { Module } from '@nestjs/common';
|
||||
import { LoginController } from './controllers/login.controller';
|
||||
import { LoginService } from './services/login.service';
|
||||
import { LoginCoreModule } from '../../core/login_core/login_core.module';
|
||||
import { ZulipCoreModule } from '../../core/zulip/zulip-core.module';
|
||||
import { ZulipAccountsModule } from '../../core/db/zulip_accounts/zulip_accounts.module';
|
||||
|
||||
@Module({
|
||||
imports: [LoginCoreModule],
|
||||
imports: [
|
||||
LoginCoreModule,
|
||||
ZulipCoreModule,
|
||||
ZulipAccountsModule.forRoot(),
|
||||
],
|
||||
controllers: [LoginController],
|
||||
providers: [LoginService],
|
||||
providers: [
|
||||
LoginService,
|
||||
],
|
||||
exports: [LoginService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
@@ -33,8 +33,8 @@ import {
|
||||
TestModeEmailVerificationResponseDto,
|
||||
SuccessEmailVerificationResponseDto
|
||||
} from '../dto/login_response.dto';
|
||||
import { Throttle, ThrottlePresets } from '../../security/decorators/throttle.decorator';
|
||||
import { Timeout, TimeoutPresets } from '../../security/decorators/timeout.decorator';
|
||||
import { Throttle, ThrottlePresets } from '../../../core/security_core/decorators/throttle.decorator';
|
||||
import { Timeout, TimeoutPresets } from '../../../core/security_core/decorators/timeout.decorator';
|
||||
|
||||
@ApiTags('auth')
|
||||
@Controller('auth')
|
||||
|
||||
@@ -16,9 +16,12 @@
|
||||
* @since 2025-12-17
|
||||
*/
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Injectable, Logger, Inject } from '@nestjs/common';
|
||||
import { LoginCoreService, LoginRequest, RegisterRequest, GitHubOAuthRequest, PasswordResetRequest, AuthResult, VerificationCodeLoginRequest } from '../../../core/login_core/login_core.service';
|
||||
import { Users } from '../../../core/db/users/users.entity';
|
||||
import { ZulipAccountService } from '../../../core/zulip/services/zulip_account.service';
|
||||
import { ZulipAccountsRepository } from '../../../core/db/zulip_accounts/zulip_accounts.repository';
|
||||
import { ApiKeySecurityService } from '../../../core/zulip/services/api_key_security.service';
|
||||
|
||||
/**
|
||||
* 登录响应数据接口
|
||||
@@ -65,6 +68,10 @@ export class LoginService {
|
||||
|
||||
constructor(
|
||||
private readonly loginCoreService: LoginCoreService,
|
||||
private readonly zulipAccountService: ZulipAccountService,
|
||||
@Inject('ZulipAccountsRepository')
|
||||
private readonly zulipAccountsRepository: ZulipAccountsRepository,
|
||||
private readonly apiKeySecurityService: ApiKeySecurityService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -116,36 +123,106 @@ export class LoginService {
|
||||
* @returns 注册响应
|
||||
*/
|
||||
async register(registerRequest: RegisterRequest): Promise<ApiResponse<LoginResponse>> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
this.logger.log(`用户注册尝试: ${registerRequest.username}`);
|
||||
|
||||
// 调用核心服务进行注册
|
||||
// 1. 初始化Zulip管理员客户端
|
||||
await this.initializeZulipAdminClient();
|
||||
|
||||
// 2. 调用核心服务进行注册
|
||||
const authResult = await this.loginCoreService.register(registerRequest);
|
||||
|
||||
// 生成访问令牌
|
||||
// 3. 创建Zulip账号(使用相同的邮箱和密码)
|
||||
let zulipAccountCreated = false;
|
||||
try {
|
||||
if (registerRequest.email && registerRequest.password) {
|
||||
await this.createZulipAccountForUser(authResult.user, registerRequest.password);
|
||||
zulipAccountCreated = true;
|
||||
|
||||
this.logger.log(`Zulip账号创建成功: ${registerRequest.username}`, {
|
||||
operation: 'register',
|
||||
gameUserId: authResult.user.id.toString(),
|
||||
email: registerRequest.email,
|
||||
});
|
||||
} else {
|
||||
this.logger.warn(`跳过Zulip账号创建:缺少邮箱或密码`, {
|
||||
operation: 'register',
|
||||
username: registerRequest.username,
|
||||
hasEmail: !!registerRequest.email,
|
||||
hasPassword: !!registerRequest.password,
|
||||
});
|
||||
}
|
||||
} catch (zulipError) {
|
||||
const err = zulipError as Error;
|
||||
this.logger.error(`Zulip账号创建失败,回滚用户注册`, {
|
||||
operation: 'register',
|
||||
username: registerRequest.username,
|
||||
gameUserId: authResult.user.id.toString(),
|
||||
zulipError: err.message,
|
||||
}, err.stack);
|
||||
|
||||
// 回滚游戏用户注册
|
||||
try {
|
||||
await this.loginCoreService.deleteUser(authResult.user.id);
|
||||
this.logger.log(`用户注册回滚成功: ${registerRequest.username}`);
|
||||
} catch (rollbackError) {
|
||||
const rollbackErr = rollbackError as Error;
|
||||
this.logger.error(`用户注册回滚失败`, {
|
||||
operation: 'register',
|
||||
username: registerRequest.username,
|
||||
gameUserId: authResult.user.id.toString(),
|
||||
rollbackError: rollbackErr.message,
|
||||
}, rollbackErr.stack);
|
||||
}
|
||||
|
||||
// 抛出原始错误
|
||||
throw new Error(`注册失败:Zulip账号创建失败 - ${err.message}`);
|
||||
}
|
||||
|
||||
// 4. 生成访问令牌
|
||||
const accessToken = this.generateAccessToken(authResult.user);
|
||||
|
||||
// 格式化响应数据
|
||||
// 5. 格式化响应数据
|
||||
const response: LoginResponse = {
|
||||
user: this.formatUserInfo(authResult.user),
|
||||
access_token: accessToken,
|
||||
is_new_user: true,
|
||||
message: '注册成功'
|
||||
message: zulipAccountCreated ? '注册成功,Zulip账号已同步创建' : '注册成功'
|
||||
};
|
||||
|
||||
this.logger.log(`用户注册成功: ${authResult.user.username} (ID: ${authResult.user.id})`);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.logger.log(`用户注册成功: ${authResult.user.username} (ID: ${authResult.user.id})`, {
|
||||
operation: 'register',
|
||||
gameUserId: authResult.user.id.toString(),
|
||||
username: authResult.user.username,
|
||||
zulipAccountCreated,
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: response,
|
||||
message: '注册成功'
|
||||
message: response.message
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`用户注册失败: ${registerRequest.username}`, error instanceof Error ? error.stack : String(error));
|
||||
const duration = Date.now() - startTime;
|
||||
const err = error as Error;
|
||||
|
||||
this.logger.error(`用户注册失败: ${registerRequest.username}`, {
|
||||
operation: 'register',
|
||||
username: registerRequest.username,
|
||||
error: err.message,
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
}, err.stack);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '注册失败',
|
||||
message: err.message || '注册失败',
|
||||
error_code: 'REGISTER_FAILED'
|
||||
};
|
||||
}
|
||||
@@ -592,4 +669,171 @@ export class LoginService {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化Zulip管理员客户端
|
||||
*
|
||||
* 功能描述:
|
||||
* 使用环境变量中的管理员凭证初始化Zulip客户端
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 从环境变量获取管理员配置
|
||||
* 2. 验证配置完整性
|
||||
* 3. 初始化ZulipAccountService的管理员客户端
|
||||
*
|
||||
* @throws Error 当配置缺失或初始化失败时
|
||||
* @private
|
||||
*/
|
||||
private async initializeZulipAdminClient(): Promise<void> {
|
||||
try {
|
||||
// 从环境变量获取管理员配置
|
||||
const adminConfig = {
|
||||
realm: process.env.ZULIP_SERVER_URL || '',
|
||||
username: process.env.ZULIP_BOT_EMAIL || '',
|
||||
apiKey: process.env.ZULIP_BOT_API_KEY || '',
|
||||
};
|
||||
|
||||
// 验证配置完整性
|
||||
if (!adminConfig.realm || !adminConfig.username || !adminConfig.apiKey) {
|
||||
throw new Error('Zulip管理员配置不完整,请检查环境变量 ZULIP_SERVER_URL, ZULIP_BOT_EMAIL, ZULIP_BOT_API_KEY');
|
||||
}
|
||||
|
||||
// 初始化管理员客户端
|
||||
const initialized = await this.zulipAccountService.initializeAdminClient(adminConfig);
|
||||
|
||||
if (!initialized) {
|
||||
throw new Error('Zulip管理员客户端初始化失败');
|
||||
}
|
||||
|
||||
this.logger.log('Zulip管理员客户端初始化成功', {
|
||||
operation: 'initializeZulipAdminClient',
|
||||
realm: adminConfig.realm,
|
||||
adminEmail: adminConfig.username,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
this.logger.error('Zulip管理员客户端初始化失败', {
|
||||
operation: 'initializeZulipAdminClient',
|
||||
error: err.message,
|
||||
}, err.stack);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为用户创建Zulip账号
|
||||
*
|
||||
* 功能描述:
|
||||
* 为新注册的游戏用户创建对应的Zulip账号并建立关联
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 使用相同的邮箱和密码创建Zulip账号
|
||||
* 2. 加密存储API Key
|
||||
* 3. 在数据库中建立关联关系
|
||||
* 4. 处理创建失败的情况
|
||||
*
|
||||
* @param gameUser 游戏用户信息
|
||||
* @param password 用户密码(明文)
|
||||
* @throws Error 当Zulip账号创建失败时
|
||||
* @private
|
||||
*/
|
||||
private async createZulipAccountForUser(gameUser: Users, password: string): Promise<void> {
|
||||
const startTime = Date.now();
|
||||
|
||||
this.logger.log('开始为用户创建Zulip账号', {
|
||||
operation: 'createZulipAccountForUser',
|
||||
gameUserId: gameUser.id.toString(),
|
||||
email: gameUser.email,
|
||||
nickname: gameUser.nickname,
|
||||
});
|
||||
|
||||
try {
|
||||
// 1. 检查是否已存在Zulip账号关联
|
||||
const existingAccount = await this.zulipAccountsRepository.findByGameUserId(gameUser.id);
|
||||
if (existingAccount) {
|
||||
this.logger.warn('用户已存在Zulip账号关联,跳过创建', {
|
||||
operation: 'createZulipAccountForUser',
|
||||
gameUserId: gameUser.id.toString(),
|
||||
existingZulipUserId: existingAccount.zulipUserId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 创建Zulip账号
|
||||
const createResult = await this.zulipAccountService.createZulipAccount({
|
||||
email: gameUser.email,
|
||||
fullName: gameUser.nickname,
|
||||
password: password,
|
||||
});
|
||||
|
||||
if (!createResult.success) {
|
||||
throw new Error(createResult.error || 'Zulip账号创建失败');
|
||||
}
|
||||
|
||||
// 3. 存储API Key
|
||||
if (createResult.apiKey) {
|
||||
await this.apiKeySecurityService.storeApiKey(
|
||||
gameUser.id.toString(),
|
||||
createResult.apiKey
|
||||
);
|
||||
}
|
||||
|
||||
// 4. 在数据库中创建关联记录
|
||||
await this.zulipAccountsRepository.create({
|
||||
gameUserId: gameUser.id,
|
||||
zulipUserId: createResult.userId!,
|
||||
zulipEmail: createResult.email!,
|
||||
zulipFullName: gameUser.nickname,
|
||||
zulipApiKeyEncrypted: createResult.apiKey ? 'stored_in_redis' : '', // 标记API Key已存储在Redis中
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
// 5. 建立游戏账号与Zulip账号的内存关联(用于当前会话)
|
||||
if (createResult.apiKey) {
|
||||
await this.zulipAccountService.linkGameAccount(
|
||||
gameUser.id.toString(),
|
||||
createResult.userId!,
|
||||
createResult.email!,
|
||||
createResult.apiKey
|
||||
);
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.logger.log('Zulip账号创建和关联成功', {
|
||||
operation: 'createZulipAccountForUser',
|
||||
gameUserId: gameUser.id.toString(),
|
||||
zulipUserId: createResult.userId,
|
||||
zulipEmail: createResult.email,
|
||||
hasApiKey: !!createResult.apiKey,
|
||||
duration,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.logger.error('为用户创建Zulip账号失败', {
|
||||
operation: 'createZulipAccountForUser',
|
||||
gameUserId: gameUser.id.toString(),
|
||||
email: gameUser.email,
|
||||
error: err.message,
|
||||
duration,
|
||||
}, err.stack);
|
||||
|
||||
// 清理可能创建的部分数据
|
||||
try {
|
||||
await this.zulipAccountsRepository.deleteByGameUserId(gameUser.id);
|
||||
} catch (cleanupError) {
|
||||
this.logger.warn('清理Zulip账号关联数据失败', {
|
||||
operation: 'createZulipAccountForUser',
|
||||
gameUserId: gameUser.id.toString(),
|
||||
cleanupError: (cleanupError as Error).message,
|
||||
});
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
520
src/business/auth/services/login.service.zulip-account.spec.ts
Normal file
520
src/business/auth/services/login.service.zulip-account.spec.ts
Normal file
@@ -0,0 +1,520 @@
|
||||
/**
|
||||
* LoginService Zulip账号创建属性测试
|
||||
*
|
||||
* 功能描述:
|
||||
* - 测试用户注册时Zulip账号创建的一致性
|
||||
* - 验证账号关联和数据完整性
|
||||
* - 测试失败回滚机制
|
||||
*
|
||||
* 属性测试:
|
||||
* - 属性 13: Zulip账号创建一致性
|
||||
* - 验证需求: 账号创建成功率和数据一致性
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.0.0
|
||||
* @since 2025-01-05
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import * as fc from 'fast-check';
|
||||
import { LoginService } from './login.service';
|
||||
import { LoginCoreService, RegisterRequest } from '../../../core/login_core/login_core.service';
|
||||
import { ZulipAccountService } from '../../../core/zulip/services/zulip_account.service';
|
||||
import { ZulipAccountsRepository } from '../../../core/db/zulip_accounts/zulip_accounts.repository';
|
||||
import { ApiKeySecurityService } from '../../../core/zulip/services/api_key_security.service';
|
||||
import { Users } from '../../../core/db/users/users.entity';
|
||||
import { ZulipAccounts } from '../../../core/db/zulip_accounts/zulip_accounts.entity';
|
||||
|
||||
describe('LoginService - Zulip账号创建属性测试', () => {
|
||||
let loginService: LoginService;
|
||||
let loginCoreService: jest.Mocked<LoginCoreService>;
|
||||
let zulipAccountService: jest.Mocked<ZulipAccountService>;
|
||||
let zulipAccountsRepository: jest.Mocked<ZulipAccountsRepository>;
|
||||
let apiKeySecurityService: jest.Mocked<ApiKeySecurityService>;
|
||||
|
||||
// 测试用的模拟数据生成器
|
||||
const validEmailArb = fc.string({ minLength: 5, maxLength: 50 })
|
||||
.filter(s => s.includes('@') && s.includes('.'))
|
||||
.map(s => `test_${s.replace(/[^a-zA-Z0-9@._-]/g, '')}@example.com`);
|
||||
|
||||
const validUsernameArb = fc.string({ minLength: 3, maxLength: 20 })
|
||||
.filter(s => /^[a-zA-Z0-9_]+$/.test(s));
|
||||
|
||||
const validNicknameArb = fc.string({ minLength: 2, maxLength: 50 })
|
||||
.filter(s => s.trim().length > 0);
|
||||
|
||||
const validPasswordArb = fc.string({ minLength: 8, maxLength: 20 })
|
||||
.filter(s => /[a-zA-Z]/.test(s) && /\d/.test(s));
|
||||
|
||||
const registerRequestArb = fc.record({
|
||||
username: validUsernameArb,
|
||||
email: validEmailArb,
|
||||
nickname: validNicknameArb,
|
||||
password: validPasswordArb,
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// 创建模拟服务
|
||||
const mockLoginCoreService = {
|
||||
register: jest.fn(),
|
||||
deleteUser: jest.fn(),
|
||||
};
|
||||
|
||||
const mockZulipAccountService = {
|
||||
initializeAdminClient: jest.fn(),
|
||||
createZulipAccount: jest.fn(),
|
||||
linkGameAccount: jest.fn(),
|
||||
};
|
||||
|
||||
const mockZulipAccountsRepository = {
|
||||
findByGameUserId: jest.fn(),
|
||||
create: jest.fn(),
|
||||
deleteByGameUserId: jest.fn(),
|
||||
};
|
||||
|
||||
const mockApiKeySecurityService = {
|
||||
storeApiKey: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
LoginService,
|
||||
{
|
||||
provide: LoginCoreService,
|
||||
useValue: mockLoginCoreService,
|
||||
},
|
||||
{
|
||||
provide: ZulipAccountService,
|
||||
useValue: mockZulipAccountService,
|
||||
},
|
||||
{
|
||||
provide: 'ZulipAccountsRepository',
|
||||
useValue: mockZulipAccountsRepository,
|
||||
},
|
||||
{
|
||||
provide: ApiKeySecurityService,
|
||||
useValue: mockApiKeySecurityService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
loginService = module.get<LoginService>(LoginService);
|
||||
loginCoreService = module.get(LoginCoreService);
|
||||
zulipAccountService = module.get(ZulipAccountService);
|
||||
zulipAccountsRepository = module.get('ZulipAccountsRepository');
|
||||
apiKeySecurityService = module.get(ApiKeySecurityService);
|
||||
|
||||
// 设置环境变量模拟
|
||||
process.env.ZULIP_SERVER_URL = 'https://test.zulip.com';
|
||||
process.env.ZULIP_BOT_EMAIL = 'bot@test.zulip.com';
|
||||
process.env.ZULIP_BOT_API_KEY = 'test_api_key_123';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// 清理环境变量
|
||||
delete process.env.ZULIP_SERVER_URL;
|
||||
delete process.env.ZULIP_BOT_EMAIL;
|
||||
delete process.env.ZULIP_BOT_API_KEY;
|
||||
});
|
||||
|
||||
/**
|
||||
* 属性 13: Zulip账号创建一致性
|
||||
*
|
||||
* 验证需求: 账号创建成功率和数据一致性
|
||||
*
|
||||
* 测试内容:
|
||||
* 1. 成功注册时,游戏账号和Zulip账号都应该被创建
|
||||
* 2. 账号关联信息应该正确存储
|
||||
* 3. Zulip账号创建失败时,游戏账号应该被回滚
|
||||
* 4. 数据一致性:邮箱、昵称等信息应该保持一致
|
||||
*/
|
||||
describe('属性 13: Zulip账号创建一致性', () => {
|
||||
it('应该在成功注册时创建一致的游戏账号和Zulip账号', async () => {
|
||||
await fc.assert(
|
||||
fc.asyncProperty(registerRequestArb, async (registerRequest) => {
|
||||
// 准备测试数据
|
||||
const mockGameUser: Users = {
|
||||
id: BigInt(Math.floor(Math.random() * 1000000)),
|
||||
username: registerRequest.username,
|
||||
email: registerRequest.email,
|
||||
nickname: registerRequest.nickname,
|
||||
password_hash: 'hashed_password',
|
||||
role: 1,
|
||||
created_at: new Date(),
|
||||
updated_at: new Date(),
|
||||
} as Users;
|
||||
|
||||
const mockZulipResult = {
|
||||
success: true,
|
||||
userId: Math.floor(Math.random() * 1000000),
|
||||
email: registerRequest.email,
|
||||
apiKey: 'zulip_api_key_' + Math.random().toString(36),
|
||||
};
|
||||
|
||||
const mockZulipAccount: ZulipAccounts = {
|
||||
id: BigInt(Math.floor(Math.random() * 1000000)),
|
||||
gameUserId: mockGameUser.id,
|
||||
zulipUserId: mockZulipResult.userId,
|
||||
zulipEmail: mockZulipResult.email,
|
||||
zulipFullName: registerRequest.nickname,
|
||||
zulipApiKeyEncrypted: 'encrypted_' + mockZulipResult.apiKey,
|
||||
status: 'active',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as ZulipAccounts;
|
||||
|
||||
// 设置模拟行为
|
||||
zulipAccountService.initializeAdminClient.mockResolvedValue(true);
|
||||
loginCoreService.register.mockResolvedValue({
|
||||
user: mockGameUser,
|
||||
isNewUser: true,
|
||||
});
|
||||
zulipAccountsRepository.findByGameUserId.mockResolvedValue(null);
|
||||
zulipAccountService.createZulipAccount.mockResolvedValue(mockZulipResult);
|
||||
apiKeySecurityService.storeApiKey.mockResolvedValue(undefined);
|
||||
zulipAccountsRepository.create.mockResolvedValue(mockZulipAccount);
|
||||
zulipAccountService.linkGameAccount.mockResolvedValue(true);
|
||||
|
||||
// 执行注册
|
||||
const result = await loginService.register(registerRequest);
|
||||
|
||||
// 验证结果
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.user.username).toBe(registerRequest.username);
|
||||
expect(result.data?.user.email).toBe(registerRequest.email);
|
||||
expect(result.data?.user.nickname).toBe(registerRequest.nickname);
|
||||
expect(result.data?.is_new_user).toBe(true);
|
||||
|
||||
// 验证Zulip管理员客户端初始化
|
||||
expect(zulipAccountService.initializeAdminClient).toHaveBeenCalledWith({
|
||||
realm: 'https://test.zulip.com',
|
||||
username: 'bot@test.zulip.com',
|
||||
apiKey: 'test_api_key_123',
|
||||
});
|
||||
|
||||
// 验证游戏用户注册
|
||||
expect(loginCoreService.register).toHaveBeenCalledWith(registerRequest);
|
||||
|
||||
// 验证Zulip账号创建
|
||||
expect(zulipAccountService.createZulipAccount).toHaveBeenCalledWith({
|
||||
email: registerRequest.email,
|
||||
fullName: registerRequest.nickname,
|
||||
password: registerRequest.password,
|
||||
});
|
||||
|
||||
// 验证API Key存储
|
||||
expect(apiKeySecurityService.storeApiKey).toHaveBeenCalledWith(
|
||||
mockGameUser.id.toString(),
|
||||
mockZulipResult.apiKey
|
||||
);
|
||||
|
||||
// 验证账号关联创建
|
||||
expect(zulipAccountsRepository.create).toHaveBeenCalledWith({
|
||||
gameUserId: mockGameUser.id,
|
||||
zulipUserId: mockZulipResult.userId,
|
||||
zulipEmail: mockZulipResult.email,
|
||||
zulipFullName: registerRequest.nickname,
|
||||
zulipApiKeyEncrypted: 'stored_in_redis',
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
// 验证内存关联
|
||||
expect(zulipAccountService.linkGameAccount).toHaveBeenCalledWith(
|
||||
mockGameUser.id.toString(),
|
||||
mockZulipResult.userId,
|
||||
mockZulipResult.email,
|
||||
mockZulipResult.apiKey
|
||||
);
|
||||
}),
|
||||
{ numRuns: 100 }
|
||||
);
|
||||
});
|
||||
|
||||
it('应该在Zulip账号创建失败时回滚游戏账号', async () => {
|
||||
await fc.assert(
|
||||
fc.asyncProperty(registerRequestArb, async (registerRequest) => {
|
||||
// 准备测试数据
|
||||
const mockGameUser: Users = {
|
||||
id: BigInt(Math.floor(Math.random() * 1000000)),
|
||||
username: registerRequest.username,
|
||||
email: registerRequest.email,
|
||||
nickname: registerRequest.nickname,
|
||||
password_hash: 'hashed_password',
|
||||
role: 1,
|
||||
created_at: new Date(),
|
||||
updated_at: new Date(),
|
||||
} as Users;
|
||||
|
||||
// 设置模拟行为 - Zulip账号创建失败
|
||||
zulipAccountService.initializeAdminClient.mockResolvedValue(true);
|
||||
loginCoreService.register.mockResolvedValue({
|
||||
user: mockGameUser,
|
||||
isNewUser: true,
|
||||
});
|
||||
zulipAccountsRepository.findByGameUserId.mockResolvedValue(null);
|
||||
zulipAccountService.createZulipAccount.mockResolvedValue({
|
||||
success: false,
|
||||
error: 'Zulip服务器连接失败',
|
||||
errorCode: 'CONNECTION_FAILED',
|
||||
});
|
||||
loginCoreService.deleteUser.mockResolvedValue(true);
|
||||
|
||||
// 执行注册
|
||||
const result = await loginService.register(registerRequest);
|
||||
|
||||
// 验证结果 - 注册应该失败
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toContain('Zulip账号创建失败');
|
||||
|
||||
// 验证游戏用户被创建
|
||||
expect(loginCoreService.register).toHaveBeenCalledWith(registerRequest);
|
||||
|
||||
// 验证Zulip账号创建尝试
|
||||
expect(zulipAccountService.createZulipAccount).toHaveBeenCalledWith({
|
||||
email: registerRequest.email,
|
||||
fullName: registerRequest.nickname,
|
||||
password: registerRequest.password,
|
||||
});
|
||||
|
||||
// 验证游戏用户被回滚删除
|
||||
expect(loginCoreService.deleteUser).toHaveBeenCalledWith(mockGameUser.id);
|
||||
|
||||
// 验证没有创建账号关联
|
||||
expect(zulipAccountsRepository.create).not.toHaveBeenCalled();
|
||||
expect(zulipAccountService.linkGameAccount).not.toHaveBeenCalled();
|
||||
}),
|
||||
{ numRuns: 100 }
|
||||
);
|
||||
});
|
||||
|
||||
it('应该正确处理已存在Zulip账号关联的情况', async () => {
|
||||
await fc.assert(
|
||||
fc.asyncProperty(registerRequestArb, async (registerRequest) => {
|
||||
// 准备测试数据
|
||||
const mockGameUser: Users = {
|
||||
id: BigInt(Math.floor(Math.random() * 1000000)),
|
||||
username: registerRequest.username,
|
||||
email: registerRequest.email,
|
||||
nickname: registerRequest.nickname,
|
||||
password_hash: 'hashed_password',
|
||||
role: 1,
|
||||
created_at: new Date(),
|
||||
updated_at: new Date(),
|
||||
} as Users;
|
||||
|
||||
const existingZulipAccount: ZulipAccounts = {
|
||||
id: BigInt(Math.floor(Math.random() * 1000000)),
|
||||
gameUserId: mockGameUser.id,
|
||||
zulipUserId: 12345,
|
||||
zulipEmail: registerRequest.email,
|
||||
zulipFullName: registerRequest.nickname,
|
||||
zulipApiKeyEncrypted: 'existing_encrypted_key',
|
||||
status: 'active',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as ZulipAccounts;
|
||||
|
||||
// 设置模拟行为 - 已存在Zulip账号关联
|
||||
zulipAccountService.initializeAdminClient.mockResolvedValue(true);
|
||||
loginCoreService.register.mockResolvedValue({
|
||||
user: mockGameUser,
|
||||
isNewUser: true,
|
||||
});
|
||||
zulipAccountsRepository.findByGameUserId.mockResolvedValue(existingZulipAccount);
|
||||
|
||||
// 执行注册
|
||||
const result = await loginService.register(registerRequest);
|
||||
|
||||
// 验证结果 - 注册应该成功
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.user.username).toBe(registerRequest.username);
|
||||
|
||||
// 验证游戏用户被创建
|
||||
expect(loginCoreService.register).toHaveBeenCalledWith(registerRequest);
|
||||
|
||||
// 验证检查了现有关联
|
||||
expect(zulipAccountsRepository.findByGameUserId).toHaveBeenCalledWith(mockGameUser.id);
|
||||
|
||||
// 验证没有尝试创建新的Zulip账号
|
||||
expect(zulipAccountService.createZulipAccount).not.toHaveBeenCalled();
|
||||
expect(zulipAccountsRepository.create).not.toHaveBeenCalled();
|
||||
}),
|
||||
{ numRuns: 100 }
|
||||
);
|
||||
});
|
||||
|
||||
it('应该正确处理缺少邮箱或密码的注册请求', async () => {
|
||||
await fc.assert(
|
||||
fc.asyncProperty(
|
||||
fc.record({
|
||||
username: validUsernameArb,
|
||||
nickname: validNicknameArb,
|
||||
email: fc.option(validEmailArb, { nil: undefined }),
|
||||
password: fc.option(validPasswordArb, { nil: undefined }),
|
||||
}),
|
||||
async (registerRequest) => {
|
||||
// 只测试缺少邮箱或密码的情况
|
||||
if (registerRequest.email && registerRequest.password) {
|
||||
return; // 跳过完整数据的情况
|
||||
}
|
||||
|
||||
// 准备测试数据
|
||||
const mockGameUser: Users = {
|
||||
id: BigInt(Math.floor(Math.random() * 1000000)),
|
||||
username: registerRequest.username,
|
||||
email: registerRequest.email || null,
|
||||
nickname: registerRequest.nickname,
|
||||
password_hash: registerRequest.password ? 'hashed_password' : null,
|
||||
role: 1,
|
||||
created_at: new Date(),
|
||||
updated_at: new Date(),
|
||||
} as Users;
|
||||
|
||||
// 设置模拟行为
|
||||
zulipAccountService.initializeAdminClient.mockResolvedValue(true);
|
||||
loginCoreService.register.mockResolvedValue({
|
||||
user: mockGameUser,
|
||||
isNewUser: true,
|
||||
});
|
||||
|
||||
// 执行注册
|
||||
const result = await loginService.register(registerRequest as RegisterRequest);
|
||||
|
||||
// 验证结果 - 注册应该成功,但跳过Zulip账号创建
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.user.username).toBe(registerRequest.username);
|
||||
expect(result.data?.message).toBe('注册成功'); // 不包含Zulip创建信息
|
||||
|
||||
// 验证游戏用户被创建
|
||||
expect(loginCoreService.register).toHaveBeenCalledWith(registerRequest);
|
||||
|
||||
// 验证没有尝试创建Zulip账号
|
||||
expect(zulipAccountService.createZulipAccount).not.toHaveBeenCalled();
|
||||
expect(zulipAccountsRepository.create).not.toHaveBeenCalled();
|
||||
}
|
||||
),
|
||||
{ numRuns: 50 }
|
||||
);
|
||||
});
|
||||
|
||||
it('应该正确处理Zulip管理员客户端初始化失败', async () => {
|
||||
await fc.assert(
|
||||
fc.asyncProperty(registerRequestArb, async (registerRequest) => {
|
||||
// 设置模拟行为 - 管理员客户端初始化失败
|
||||
zulipAccountService.initializeAdminClient.mockResolvedValue(false);
|
||||
|
||||
// 执行注册
|
||||
const result = await loginService.register(registerRequest);
|
||||
|
||||
// 验证结果 - 注册应该失败
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toContain('Zulip管理员客户端初始化失败');
|
||||
|
||||
// 验证没有尝试创建游戏用户
|
||||
expect(loginCoreService.register).not.toHaveBeenCalled();
|
||||
|
||||
// 验证没有尝试创建Zulip账号
|
||||
expect(zulipAccountService.createZulipAccount).not.toHaveBeenCalled();
|
||||
}),
|
||||
{ numRuns: 50 }
|
||||
);
|
||||
});
|
||||
|
||||
it('应该正确处理环境变量缺失的情况', async () => {
|
||||
await fc.assert(
|
||||
fc.asyncProperty(registerRequestArb, async (registerRequest) => {
|
||||
// 清除环境变量
|
||||
delete process.env.ZULIP_SERVER_URL;
|
||||
delete process.env.ZULIP_BOT_EMAIL;
|
||||
delete process.env.ZULIP_BOT_API_KEY;
|
||||
|
||||
// 执行注册
|
||||
const result = await loginService.register(registerRequest);
|
||||
|
||||
// 验证结果 - 注册应该失败
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toContain('Zulip管理员配置不完整');
|
||||
|
||||
// 验证没有尝试创建游戏用户
|
||||
expect(loginCoreService.register).not.toHaveBeenCalled();
|
||||
|
||||
// 恢复环境变量
|
||||
process.env.ZULIP_SERVER_URL = 'https://test.zulip.com';
|
||||
process.env.ZULIP_BOT_EMAIL = 'bot@test.zulip.com';
|
||||
process.env.ZULIP_BOT_API_KEY = 'test_api_key_123';
|
||||
}),
|
||||
{ numRuns: 30 }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 数据一致性验证测试
|
||||
*
|
||||
* 验证游戏账号和Zulip账号之间的数据一致性
|
||||
*/
|
||||
describe('数据一致性验证', () => {
|
||||
it('应该确保游戏账号和Zulip账号使用相同的邮箱和昵称', async () => {
|
||||
await fc.assert(
|
||||
fc.asyncProperty(registerRequestArb, async (registerRequest) => {
|
||||
// 准备测试数据
|
||||
const mockGameUser: Users = {
|
||||
id: BigInt(Math.floor(Math.random() * 1000000)),
|
||||
username: registerRequest.username,
|
||||
email: registerRequest.email,
|
||||
nickname: registerRequest.nickname,
|
||||
password_hash: 'hashed_password',
|
||||
role: 1,
|
||||
created_at: new Date(),
|
||||
updated_at: new Date(),
|
||||
} as Users;
|
||||
|
||||
const mockZulipResult = {
|
||||
success: true,
|
||||
userId: Math.floor(Math.random() * 1000000),
|
||||
email: registerRequest.email,
|
||||
apiKey: 'zulip_api_key_' + Math.random().toString(36),
|
||||
};
|
||||
|
||||
// 设置模拟行为
|
||||
zulipAccountService.initializeAdminClient.mockResolvedValue(true);
|
||||
loginCoreService.register.mockResolvedValue({
|
||||
user: mockGameUser,
|
||||
isNewUser: true,
|
||||
});
|
||||
zulipAccountsRepository.findByGameUserId.mockResolvedValue(null);
|
||||
zulipAccountService.createZulipAccount.mockResolvedValue(mockZulipResult);
|
||||
apiKeySecurityService.storeApiKey.mockResolvedValue(undefined);
|
||||
zulipAccountsRepository.create.mockResolvedValue({} as ZulipAccounts);
|
||||
zulipAccountService.linkGameAccount.mockResolvedValue(true);
|
||||
|
||||
// 执行注册
|
||||
await loginService.register(registerRequest);
|
||||
|
||||
// 验证Zulip账号创建时使用了正确的数据
|
||||
expect(zulipAccountService.createZulipAccount).toHaveBeenCalledWith({
|
||||
email: registerRequest.email, // 相同的邮箱
|
||||
fullName: registerRequest.nickname, // 相同的昵称
|
||||
password: registerRequest.password, // 相同的密码
|
||||
});
|
||||
|
||||
// 验证账号关联存储了正确的数据
|
||||
expect(zulipAccountsRepository.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
gameUserId: mockGameUser.id,
|
||||
zulipUserId: mockZulipResult.userId,
|
||||
zulipEmail: registerRequest.email, // 相同的邮箱
|
||||
zulipFullName: registerRequest.nickname, // 相同的昵称
|
||||
zulipApiKeyEncrypted: 'stored_in_redis',
|
||||
status: 'active',
|
||||
})
|
||||
);
|
||||
}),
|
||||
{ numRuns: 100 }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -20,8 +20,8 @@ import { Body, Controller, Get, HttpCode, HttpStatus, Param, Put, Post, UseGuard
|
||||
import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { AdminGuard } from '../../admin/guards/admin.guard';
|
||||
import { UserManagementService } from '../services/user-management.service';
|
||||
import { Throttle, ThrottlePresets } from '../../security/decorators/throttle.decorator';
|
||||
import { Timeout, TimeoutPresets } from '../../security/decorators/timeout.decorator';
|
||||
import { Throttle, ThrottlePresets } from '../../../core/security_core/decorators/throttle.decorator';
|
||||
import { Timeout, TimeoutPresets } from '../../../core/security_core/decorators/timeout.decorator';
|
||||
import { UserStatusDto, BatchUserStatusDto } from '../dto/user-status.dto';
|
||||
import { UserStatusResponseDto, BatchUserStatusResponseDto, UserStatusStatsResponseDto } from '../dto/user-status-response.dto';
|
||||
|
||||
|
||||
172
src/business/zulip/README.md
Normal file
172
src/business/zulip/README.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# Zulip集成业务模块
|
||||
|
||||
## 架构重构说明
|
||||
|
||||
本模块已按照项目的分层架构要求进行重构,将技术实现细节移动到核心服务层,业务逻辑保留在业务层。
|
||||
|
||||
### 重构前后对比
|
||||
|
||||
#### 重构前(❌ 违反架构原则)
|
||||
```
|
||||
src/business/zulip/services/
|
||||
├── zulip_client.service.ts # 技术实现:API调用
|
||||
├── zulip_client_pool.service.ts # 技术实现:连接池管理
|
||||
├── config_manager.service.ts # 技术实现:配置管理
|
||||
├── zulip_event_processor.service.ts # 技术实现:事件处理
|
||||
├── session_manager.service.ts # ✅ 业务逻辑:会话管理
|
||||
└── message_filter.service.ts # ✅ 业务逻辑:消息过滤
|
||||
```
|
||||
|
||||
#### 重构后(✅ 符合架构原则)
|
||||
```
|
||||
# 业务逻辑层
|
||||
src/business/zulip/
|
||||
├── zulip.service.ts # 业务协调服务
|
||||
├── zulip_websocket.gateway.ts # WebSocket业务网关
|
||||
└── services/
|
||||
├── session_manager.service.ts # 会话业务逻辑
|
||||
└── message_filter.service.ts # 消息过滤业务规则
|
||||
|
||||
# 核心服务层
|
||||
src/core/zulip/
|
||||
├── interfaces/
|
||||
│ └── zulip-core.interfaces.ts # 核心服务接口定义
|
||||
├── services/
|
||||
│ ├── zulip_client.service.ts # Zulip API封装
|
||||
│ ├── zulip_client_pool.service.ts # 客户端池管理
|
||||
│ ├── config_manager.service.ts # 配置管理
|
||||
│ ├── zulip_event_processor.service.ts # 事件处理
|
||||
│ └── ... # 其他技术服务
|
||||
└── zulip-core.module.ts # 核心服务模块
|
||||
```
|
||||
|
||||
### 架构优势
|
||||
|
||||
#### 1. 单一职责原则
|
||||
- **业务层**:只关注游戏相关的业务逻辑和规则
|
||||
- **核心层**:只处理技术实现和第三方API调用
|
||||
|
||||
#### 2. 依赖注入和接口抽象
|
||||
```typescript
|
||||
// 业务层通过接口依赖核心服务
|
||||
constructor(
|
||||
@Inject('ZULIP_CLIENT_POOL_SERVICE')
|
||||
private readonly zulipClientPool: IZulipClientPoolService,
|
||||
@Inject('ZULIP_CONFIG_SERVICE')
|
||||
private readonly configManager: IZulipConfigService,
|
||||
) {}
|
||||
```
|
||||
|
||||
#### 3. 易于测试和维护
|
||||
- 业务逻辑可以独立测试,不依赖具体的技术实现
|
||||
- 核心服务可以独立替换,不影响业务逻辑
|
||||
- 接口定义清晰,便于理解和维护
|
||||
|
||||
### 服务职责划分
|
||||
|
||||
#### 业务逻辑层服务
|
||||
|
||||
| 服务 | 职责 | 业务价值 |
|
||||
|------|------|----------|
|
||||
| `ZulipService` | 游戏登录/登出业务流程协调 | 处理玩家生命周期管理 |
|
||||
| `SessionManagerService` | 游戏会话状态和上下文管理 | 维护玩家位置和聊天上下文 |
|
||||
| `MessageFilterService` | 消息过滤和业务规则控制 | 实现内容审核和权限验证 |
|
||||
| `ZulipWebSocketGateway` | WebSocket业务协议处理 | 游戏协议转换和路由 |
|
||||
|
||||
#### 核心服务层服务
|
||||
|
||||
| 服务 | 职责 | 技术价值 |
|
||||
|------|------|----------|
|
||||
| `ZulipClientService` | Zulip REST API封装 | 第三方API调用抽象 |
|
||||
| `ZulipClientPoolService` | 客户端连接池管理 | 资源管理和性能优化 |
|
||||
| `ConfigManagerService` | 配置文件管理和热重载 | 系统配置和运维支持 |
|
||||
| `ZulipEventProcessorService` | 事件队列处理和消息转换 | 异步消息处理机制 |
|
||||
|
||||
### 使用示例
|
||||
|
||||
#### 业务层调用核心服务
|
||||
```typescript
|
||||
@Injectable()
|
||||
export class ZulipService {
|
||||
constructor(
|
||||
@Inject('ZULIP_CLIENT_POOL_SERVICE')
|
||||
private readonly zulipClientPool: IZulipClientPoolService,
|
||||
) {}
|
||||
|
||||
async sendChatMessage(request: ChatMessageRequest): Promise<ChatMessageResponse> {
|
||||
// 业务逻辑:验证和处理
|
||||
const session = await this.sessionManager.getSession(request.socketId);
|
||||
const context = await this.sessionManager.injectContext(request.socketId);
|
||||
|
||||
// 调用核心服务:技术实现
|
||||
const result = await this.zulipClientPool.sendMessage(
|
||||
session.userId,
|
||||
context.stream,
|
||||
context.topic,
|
||||
request.content,
|
||||
);
|
||||
|
||||
return { success: result.success, messageId: result.messageId };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 迁移指南
|
||||
|
||||
如果你的代码中直接导入了已移动的服务,请按以下方式更新:
|
||||
|
||||
#### 更新导入路径
|
||||
```typescript
|
||||
// ❌ 旧的导入方式
|
||||
import { ZulipClientPoolService } from './services/zulip_client_pool.service';
|
||||
|
||||
// ✅ 新的导入方式(通过依赖注入)
|
||||
import { IZulipClientPoolService } from '../../core/zulip/interfaces/zulip-core.interfaces';
|
||||
|
||||
constructor(
|
||||
@Inject('ZULIP_CLIENT_POOL_SERVICE')
|
||||
private readonly zulipClientPool: IZulipClientPoolService,
|
||||
) {}
|
||||
```
|
||||
|
||||
#### 更新模块导入
|
||||
```typescript
|
||||
// ✅ 业务模块自动导入核心模块
|
||||
@Module({
|
||||
imports: [
|
||||
ZulipCoreModule, // 自动提供所有核心服务
|
||||
// ...
|
||||
],
|
||||
})
|
||||
export class ZulipModule {}
|
||||
```
|
||||
|
||||
### 测试策略
|
||||
|
||||
#### 业务逻辑测试
|
||||
```typescript
|
||||
// 使用Mock核心服务测试业务逻辑
|
||||
const mockZulipClientPool: IZulipClientPoolService = {
|
||||
sendMessage: jest.fn().mockResolvedValue({ success: true }),
|
||||
// ...
|
||||
};
|
||||
|
||||
const module = await Test.createTestingModule({
|
||||
providers: [
|
||||
ZulipService,
|
||||
{ provide: 'ZULIP_CLIENT_POOL_SERVICE', useValue: mockZulipClientPool },
|
||||
],
|
||||
}).compile();
|
||||
```
|
||||
|
||||
#### 核心服务测试
|
||||
```typescript
|
||||
// 独立测试技术实现
|
||||
describe('ZulipClientService', () => {
|
||||
it('should call Zulip API correctly', async () => {
|
||||
// 测试API调用逻辑
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
这种架构设计确保了业务逻辑与技术实现的清晰分离,提高了代码的可维护性和可测试性。
|
||||
@@ -12,8 +12,8 @@
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import * as fc from 'fast-check';
|
||||
import { MessageFilterService, ViolationType, ContentFilterResult } from './message-filter.service';
|
||||
import { ConfigManagerService } from './config-manager.service';
|
||||
import { MessageFilterService, ViolationType } from './message_filter.service';
|
||||
import { IZulipConfigService } from '../../../core/zulip/interfaces/zulip-core.interfaces';
|
||||
import { AppLoggerService } from '../../../core/utils/logger/logger.service';
|
||||
import { IRedisService } from '../../../core/redis/redis.interface';
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('MessageFilterService', () => {
|
||||
let service: MessageFilterService;
|
||||
let mockLogger: jest.Mocked<AppLoggerService>;
|
||||
let mockRedisService: jest.Mocked<IRedisService>;
|
||||
let mockConfigManager: jest.Mocked<ConfigManagerService>;
|
||||
let mockConfigManager: jest.Mocked<IZulipConfigService>;
|
||||
|
||||
// 内存存储模拟Redis
|
||||
let memoryStore: Map<string, { value: string; expireAt?: number }>;
|
||||
@@ -100,6 +100,14 @@ describe('MessageFilterService', () => {
|
||||
hasMap: jest.fn().mockImplementation((mapId: string) => {
|
||||
return ['novice_village', 'tavern', 'market'].includes(mapId);
|
||||
}),
|
||||
getMapIdByStream: jest.fn(),
|
||||
getTopicByObject: jest.fn(),
|
||||
getZulipConfig: jest.fn(),
|
||||
hasStream: jest.fn(),
|
||||
getAllMapIds: jest.fn(),
|
||||
getAllStreams: jest.fn(),
|
||||
reloadConfig: jest.fn(),
|
||||
validateConfig: jest.fn(),
|
||||
} as any;
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@@ -114,7 +122,7 @@ describe('MessageFilterService', () => {
|
||||
useValue: mockRedisService,
|
||||
},
|
||||
{
|
||||
provide: ConfigManagerService,
|
||||
provide: 'ZULIP_CONFIG_SERVICE',
|
||||
useValue: mockConfigManager,
|
||||
},
|
||||
],
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
import { Injectable, Logger, Inject, forwardRef } from '@nestjs/common';
|
||||
import { IRedisService } from '../../../core/redis/redis.interface';
|
||||
import { ConfigManagerService } from './config-manager.service';
|
||||
import { IZulipConfigService } from '../../../core/zulip/interfaces/zulip-core.interfaces';
|
||||
|
||||
/**
|
||||
* 内容过滤结果接口
|
||||
@@ -90,6 +90,28 @@ export interface SensitiveWordConfig {
|
||||
category?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息过滤服务类
|
||||
*
|
||||
* 职责:
|
||||
* - 实施内容审核和频率控制
|
||||
* - 敏感词过滤和权限验证
|
||||
* - 防止恶意操作和滥用
|
||||
* - 与ConfigManager集成实现位置权限验证
|
||||
*
|
||||
* 主要方法:
|
||||
* - filterContent(): 内容过滤,敏感词检查
|
||||
* - checkRateLimit(): 频率限制检查
|
||||
* - validatePermission(): 权限验证,防止位置欺诈
|
||||
* - validateMessage(): 综合消息验证
|
||||
* - logViolation(): 记录违规行为
|
||||
*
|
||||
* 使用场景:
|
||||
* - 消息发送前的内容审核
|
||||
* - 频率限制和防刷屏
|
||||
* - 权限验证和安全控制
|
||||
* - 违规行为监控和记录
|
||||
*/
|
||||
@Injectable()
|
||||
export class MessageFilterService {
|
||||
private readonly RATE_LIMIT_PREFIX = 'zulip:rate_limit:';
|
||||
@@ -127,8 +149,8 @@ export class MessageFilterService {
|
||||
constructor(
|
||||
@Inject('REDIS_SERVICE')
|
||||
private readonly redisService: IRedisService,
|
||||
@Inject(forwardRef(() => ConfigManagerService))
|
||||
private readonly configManager: ConfigManagerService,
|
||||
@Inject('ZULIP_CONFIG_SERVICE')
|
||||
private readonly configManager: IZulipConfigService,
|
||||
) {
|
||||
this.logger.log('MessageFilterService初始化完成');
|
||||
}
|
||||
650
src/business/zulip/services/session_cleanup.service.spec.ts
Normal file
650
src/business/zulip/services/session_cleanup.service.spec.ts
Normal file
@@ -0,0 +1,650 @@
|
||||
/**
|
||||
* 会话清理定时任务服务测试
|
||||
*
|
||||
* 功能描述:
|
||||
* - 测试SessionCleanupService的核心功能
|
||||
* - 包含属性测试验证定时清理机制
|
||||
* - 包含属性测试验证资源释放完整性
|
||||
*
|
||||
* **Feature: zulip-integration, Property 13: 定时清理机制**
|
||||
* **Validates: Requirements 6.1, 6.2, 6.3**
|
||||
*
|
||||
* **Feature: zulip-integration, Property 14: 资源释放完整性**
|
||||
* **Validates: Requirements 6.4, 6.5**
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-31
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import * as fc from 'fast-check';
|
||||
import {
|
||||
SessionCleanupService,
|
||||
CleanupConfig,
|
||||
CleanupResult
|
||||
} from './session_cleanup.service';
|
||||
import { SessionManagerService } from './session_manager.service';
|
||||
import { IZulipClientPoolService } from '../../../core/zulip/interfaces/zulip-core.interfaces';
|
||||
|
||||
describe('SessionCleanupService', () => {
|
||||
let service: SessionCleanupService;
|
||||
let mockSessionManager: jest.Mocked<SessionManagerService>;
|
||||
let mockZulipClientPool: jest.Mocked<IZulipClientPoolService>;
|
||||
|
||||
// 模拟清理结果
|
||||
const createMockCleanupResult = (overrides: Partial<any> = {}): any => ({
|
||||
cleanedCount: 3,
|
||||
zulipQueueIds: ['queue-1', 'queue-2', 'queue-3'],
|
||||
duration: 150,
|
||||
timestamp: new Date(),
|
||||
...overrides,
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
// Only use fake timers for tests that need them
|
||||
// The concurrent test will use real timers for proper Promise handling
|
||||
|
||||
mockSessionManager = {
|
||||
cleanupExpiredSessions: jest.fn(),
|
||||
getSession: jest.fn(),
|
||||
destroySession: jest.fn(),
|
||||
createSession: jest.fn(),
|
||||
updatePlayerPosition: jest.fn(),
|
||||
getSocketsInMap: jest.fn(),
|
||||
injectContext: jest.fn(),
|
||||
} as any;
|
||||
|
||||
mockZulipClientPool = {
|
||||
createUserClient: jest.fn(),
|
||||
getUserClient: jest.fn(),
|
||||
hasUserClient: jest.fn(),
|
||||
sendMessage: jest.fn(),
|
||||
registerEventQueue: jest.fn(),
|
||||
deregisterEventQueue: jest.fn(),
|
||||
destroyUserClient: jest.fn(),
|
||||
getPoolStats: jest.fn(),
|
||||
cleanupIdleClients: jest.fn(),
|
||||
} as any;
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
SessionCleanupService,
|
||||
{
|
||||
provide: SessionManagerService,
|
||||
useValue: mockSessionManager,
|
||||
},
|
||||
{
|
||||
provide: 'ZULIP_CLIENT_POOL_SERVICE',
|
||||
useValue: mockZulipClientPool,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<SessionCleanupService>(SessionCleanupService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
service.stopCleanupTask();
|
||||
// Only restore timers if they were faked
|
||||
if (jest.isMockFunction(setTimeout)) {
|
||||
jest.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('startCleanupTask - 启动清理任务', () => {
|
||||
it('应该启动定时清理任务', () => {
|
||||
service.startCleanupTask();
|
||||
|
||||
const status = service.getStatus();
|
||||
expect(status.isEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('应该在已启动时不重复启动', () => {
|
||||
service.startCleanupTask();
|
||||
service.startCleanupTask(); // 第二次调用
|
||||
|
||||
const status = service.getStatus();
|
||||
expect(status.isEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('应该立即执行一次清理', async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
mockSessionManager.cleanupExpiredSessions.mockResolvedValue(
|
||||
createMockCleanupResult({ cleanedCount: 2 })
|
||||
);
|
||||
|
||||
service.startCleanupTask();
|
||||
|
||||
// 等待立即执行的清理完成
|
||||
await jest.runOnlyPendingTimersAsync();
|
||||
|
||||
expect(mockSessionManager.cleanupExpiredSessions).toHaveBeenCalledWith(30);
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
describe('stopCleanupTask - 停止清理任务', () => {
|
||||
it('应该停止定时清理任务', () => {
|
||||
service.startCleanupTask();
|
||||
service.stopCleanupTask();
|
||||
|
||||
const status = service.getStatus();
|
||||
expect(status.isEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('应该在未启动时安全停止', () => {
|
||||
service.stopCleanupTask();
|
||||
|
||||
const status = service.getStatus();
|
||||
expect(status.isEnabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('runCleanup - 执行清理', () => {
|
||||
it('应该成功执行清理并返回结果', async () => {
|
||||
const mockResult = createMockCleanupResult({
|
||||
cleanedCount: 5,
|
||||
zulipQueueIds: ['queue-1', 'queue-2', 'queue-3', 'queue-4', 'queue-5'],
|
||||
});
|
||||
|
||||
mockSessionManager.cleanupExpiredSessions.mockResolvedValue(mockResult);
|
||||
|
||||
const result = await service.runCleanup();
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.cleanedSessions).toBe(5);
|
||||
expect(result.deregisteredQueues).toBe(5);
|
||||
expect(result.duration).toBeGreaterThanOrEqual(0); // 修改为 >= 0,因为测试环境可能很快
|
||||
expect(mockSessionManager.cleanupExpiredSessions).toHaveBeenCalledWith(30);
|
||||
});
|
||||
|
||||
it('应该处理清理过程中的错误', async () => {
|
||||
const error = new Error('清理失败');
|
||||
mockSessionManager.cleanupExpiredSessions.mockRejectedValue(error);
|
||||
|
||||
const result = await service.runCleanup();
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('清理失败');
|
||||
expect(result.cleanedSessions).toBe(0);
|
||||
expect(result.deregisteredQueues).toBe(0);
|
||||
});
|
||||
|
||||
it('应该防止并发执行', async () => {
|
||||
let resolveFirst: () => void;
|
||||
const firstPromise = new Promise<any>(resolve => {
|
||||
resolveFirst = () => resolve(createMockCleanupResult());
|
||||
});
|
||||
|
||||
mockSessionManager.cleanupExpiredSessions.mockReturnValueOnce(firstPromise);
|
||||
|
||||
// 同时启动两个清理任务
|
||||
const promise1 = service.runCleanup();
|
||||
const promise2 = service.runCleanup();
|
||||
|
||||
// 第二个应该立即返回失败
|
||||
const result2 = await promise2;
|
||||
expect(result2.success).toBe(false);
|
||||
expect(result2.error).toContain('正在执行中');
|
||||
|
||||
// 完成第一个任务
|
||||
resolveFirst!();
|
||||
const result1 = await promise1;
|
||||
expect(result1.success).toBe(true);
|
||||
}, 15000);
|
||||
|
||||
it('应该记录最后一次清理结果', async () => {
|
||||
const mockResult = createMockCleanupResult({ cleanedCount: 3 });
|
||||
mockSessionManager.cleanupExpiredSessions.mockResolvedValue(mockResult);
|
||||
|
||||
await service.runCleanup();
|
||||
|
||||
const lastResult = service.getLastCleanupResult();
|
||||
expect(lastResult).not.toBeNull();
|
||||
expect(lastResult!.cleanedSessions).toBe(3);
|
||||
expect(lastResult!.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStatus - 获取状态', () => {
|
||||
it('应该返回正确的状态信息', () => {
|
||||
const status = service.getStatus();
|
||||
|
||||
expect(status).toHaveProperty('isRunning');
|
||||
expect(status).toHaveProperty('isEnabled');
|
||||
expect(status).toHaveProperty('config');
|
||||
expect(status).toHaveProperty('lastResult');
|
||||
expect(typeof status.isRunning).toBe('boolean');
|
||||
expect(typeof status.isEnabled).toBe('boolean');
|
||||
});
|
||||
|
||||
it('应该反映任务启动状态', () => {
|
||||
let status = service.getStatus();
|
||||
expect(status.isEnabled).toBe(false);
|
||||
|
||||
service.startCleanupTask();
|
||||
status = service.getStatus();
|
||||
expect(status.isEnabled).toBe(true);
|
||||
|
||||
service.stopCleanupTask();
|
||||
status = service.getStatus();
|
||||
expect(status.isEnabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateConfig - 更新配置', () => {
|
||||
it('应该更新清理配置', () => {
|
||||
const newConfig: Partial<CleanupConfig> = {
|
||||
intervalMs: 10 * 60 * 1000, // 10分钟
|
||||
sessionTimeoutMinutes: 60, // 60分钟
|
||||
};
|
||||
|
||||
service.updateConfig(newConfig);
|
||||
|
||||
const status = service.getStatus();
|
||||
expect(status.config.intervalMs).toBe(10 * 60 * 1000);
|
||||
expect(status.config.sessionTimeoutMinutes).toBe(60);
|
||||
});
|
||||
|
||||
it('应该在配置更改后重启任务', () => {
|
||||
service.startCleanupTask();
|
||||
|
||||
const newConfig: Partial<CleanupConfig> = {
|
||||
intervalMs: 2 * 60 * 1000, // 2分钟
|
||||
};
|
||||
|
||||
service.updateConfig(newConfig);
|
||||
|
||||
const status = service.getStatus();
|
||||
expect(status.isEnabled).toBe(true);
|
||||
expect(status.config.intervalMs).toBe(2 * 60 * 1000);
|
||||
});
|
||||
|
||||
it('应该支持禁用清理任务', () => {
|
||||
service.startCleanupTask();
|
||||
|
||||
service.updateConfig({ enabled: false });
|
||||
|
||||
const status = service.getStatus();
|
||||
expect(status.isEnabled).toBe(false);
|
||||
});
|
||||
});
|
||||
/**
|
||||
* 属性测试: 定时清理机制
|
||||
*
|
||||
* **Feature: zulip-integration, Property 13: 定时清理机制**
|
||||
* **Validates: Requirements 6.1, 6.2, 6.3**
|
||||
*
|
||||
* 系统应该定期清理过期的游戏会话,释放相关资源,
|
||||
* 并确保清理过程不影响正常的游戏服务
|
||||
*/
|
||||
describe('Property 13: 定时清理机制', () => {
|
||||
/**
|
||||
* 属性: 对于任何有效的清理配置,系统应该按配置间隔执行清理
|
||||
* 验证需求 6.1: 系统应定期检查并清理过期的游戏会话
|
||||
*/
|
||||
it('对于任何有效的清理配置,系统应该按配置间隔执行清理', async () => {
|
||||
await fc.assert(
|
||||
fc.asyncProperty(
|
||||
// 生成有效的清理间隔(1-10分钟)
|
||||
fc.integer({ min: 1, max: 10 }).map(minutes => minutes * 60 * 1000),
|
||||
// 生成有效的会话超时时间(10-120分钟)
|
||||
fc.integer({ min: 10, max: 120 }),
|
||||
async (intervalMs, sessionTimeoutMinutes) => {
|
||||
// 重置mock以确保每次测试都是干净的状态
|
||||
jest.clearAllMocks();
|
||||
jest.useFakeTimers();
|
||||
|
||||
const config: Partial<CleanupConfig> = {
|
||||
intervalMs,
|
||||
sessionTimeoutMinutes,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
// 模拟清理结果
|
||||
mockSessionManager.cleanupExpiredSessions.mockResolvedValue(
|
||||
createMockCleanupResult({ cleanedCount: 2 })
|
||||
);
|
||||
|
||||
service.updateConfig(config);
|
||||
service.startCleanupTask();
|
||||
|
||||
// 验证配置被正确设置
|
||||
const status = service.getStatus();
|
||||
expect(status.config.intervalMs).toBe(intervalMs);
|
||||
expect(status.config.sessionTimeoutMinutes).toBe(sessionTimeoutMinutes);
|
||||
expect(status.isEnabled).toBe(true);
|
||||
|
||||
// 验证立即执行了一次清理
|
||||
await jest.runOnlyPendingTimersAsync();
|
||||
expect(mockSessionManager.cleanupExpiredSessions).toHaveBeenCalledWith(sessionTimeoutMinutes);
|
||||
|
||||
service.stopCleanupTask();
|
||||
jest.useRealTimers();
|
||||
}
|
||||
),
|
||||
{ numRuns: 50 }
|
||||
);
|
||||
}, 30000);
|
||||
|
||||
/**
|
||||
* 属性: 对于任何清理操作,都应该记录清理结果和统计信息
|
||||
* 验证需求 6.2: 清理过程中系统应记录清理的会话数量和释放的资源
|
||||
*/
|
||||
it('对于任何清理操作,都应该记录清理结果和统计信息', async () => {
|
||||
await fc.assert(
|
||||
fc.asyncProperty(
|
||||
// 生成清理的会话数量
|
||||
fc.integer({ min: 0, max: 20 }),
|
||||
// 生成Zulip队列ID列表
|
||||
fc.array(
|
||||
fc.string({ minLength: 5, maxLength: 20 }).filter(s => s.trim().length > 0),
|
||||
{ minLength: 0, maxLength: 20 }
|
||||
),
|
||||
async (cleanedCount, queueIds) => {
|
||||
// 重置mock以确保每次测试都是干净的状态
|
||||
jest.clearAllMocks();
|
||||
|
||||
const mockResult = createMockCleanupResult({
|
||||
cleanedCount,
|
||||
zulipQueueIds: queueIds.slice(0, cleanedCount), // 确保队列数量不超过清理数量
|
||||
});
|
||||
|
||||
mockSessionManager.cleanupExpiredSessions.mockResolvedValue(mockResult);
|
||||
|
||||
const result = await service.runCleanup();
|
||||
|
||||
// 验证清理结果被正确记录
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.cleanedSessions).toBe(cleanedCount);
|
||||
expect(result.deregisteredQueues).toBe(Math.min(queueIds.length, cleanedCount));
|
||||
expect(result.duration).toBeGreaterThanOrEqual(0);
|
||||
expect(result.timestamp).toBeInstanceOf(Date);
|
||||
|
||||
// 验证最后一次清理结果被保存
|
||||
const lastResult = service.getLastCleanupResult();
|
||||
expect(lastResult).not.toBeNull();
|
||||
expect(lastResult!.cleanedSessions).toBe(cleanedCount);
|
||||
}
|
||||
),
|
||||
{ numRuns: 50 }
|
||||
);
|
||||
}, 30000);
|
||||
|
||||
/**
|
||||
* 属性: 清理过程中发生错误时,系统应该正确处理并记录错误信息
|
||||
* 验证需求 6.3: 清理过程中出现错误时系统应记录错误信息并继续正常服务
|
||||
*/
|
||||
it('清理过程中发生错误时,系统应该正确处理并记录错误信息', async () => {
|
||||
await fc.assert(
|
||||
fc.asyncProperty(
|
||||
// 生成各种错误消息
|
||||
fc.string({ minLength: 5, maxLength: 100 }).filter(s => s.trim().length > 0),
|
||||
async (errorMessage) => {
|
||||
// 重置mock以确保每次测试都是干净的状态
|
||||
jest.clearAllMocks();
|
||||
|
||||
const error = new Error(errorMessage.trim());
|
||||
mockSessionManager.cleanupExpiredSessions.mockRejectedValue(error);
|
||||
|
||||
const result = await service.runCleanup();
|
||||
|
||||
// 验证错误被正确处理
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe(errorMessage.trim());
|
||||
expect(result.cleanedSessions).toBe(0);
|
||||
expect(result.deregisteredQueues).toBe(0);
|
||||
expect(result.duration).toBeGreaterThanOrEqual(0);
|
||||
|
||||
// 验证错误结果被保存
|
||||
const lastResult = service.getLastCleanupResult();
|
||||
expect(lastResult).not.toBeNull();
|
||||
expect(lastResult!.success).toBe(false);
|
||||
expect(lastResult!.error).toBe(errorMessage.trim());
|
||||
}
|
||||
),
|
||||
{ numRuns: 50 }
|
||||
);
|
||||
}, 30000);
|
||||
|
||||
/**
|
||||
* 属性: 并发清理请求应该被正确处理,避免重复执行
|
||||
* 验证需求 6.1: 系统应避免同时执行多个清理任务
|
||||
*/
|
||||
it('并发清理请求应该被正确处理,避免重复执行', async () => {
|
||||
// 重置mock
|
||||
jest.clearAllMocks();
|
||||
|
||||
// 创建一个可控的Promise,使用实际的异步行为
|
||||
let resolveCleanup: (value: any) => void;
|
||||
const cleanupPromise = new Promise<any>(resolve => {
|
||||
resolveCleanup = resolve;
|
||||
});
|
||||
|
||||
mockSessionManager.cleanupExpiredSessions.mockReturnValue(cleanupPromise);
|
||||
|
||||
// 启动第一个清理请求(应该成功)
|
||||
const promise1 = service.runCleanup();
|
||||
|
||||
// 等待一个微任务周期,确保第一个请求开始执行
|
||||
await Promise.resolve();
|
||||
|
||||
// 启动第二个和第三个清理请求(应该被拒绝)
|
||||
const promise2 = service.runCleanup();
|
||||
const promise3 = service.runCleanup();
|
||||
|
||||
// 第二个和第三个请求应该立即返回失败
|
||||
const result2 = await promise2;
|
||||
const result3 = await promise3;
|
||||
|
||||
expect(result2.success).toBe(false);
|
||||
expect(result2.error).toContain('正在执行中');
|
||||
expect(result3.success).toBe(false);
|
||||
expect(result3.error).toContain('正在执行中');
|
||||
|
||||
// 完成第一个清理操作
|
||||
resolveCleanup!(createMockCleanupResult({ cleanedCount: 1 }));
|
||||
const result1 = await promise1;
|
||||
|
||||
expect(result1.success).toBe(true);
|
||||
}, 10000);
|
||||
});
|
||||
/**
|
||||
* 属性测试: 资源释放完整性
|
||||
*
|
||||
* **Feature: zulip-integration, Property 14: 资源释放完整性**
|
||||
* **Validates: Requirements 6.4, 6.5**
|
||||
*
|
||||
* 清理过期会话时,系统应该完整释放所有相关资源,
|
||||
* 包括Zulip事件队列、内存缓存等,确保不会造成资源泄漏
|
||||
*/
|
||||
describe('Property 14: 资源释放完整性', () => {
|
||||
/**
|
||||
* 属性: 对于任何过期会话,清理时应该释放所有相关的Zulip资源
|
||||
* 验证需求 6.4: 清理会话时系统应注销对应的Zulip事件队列
|
||||
*/
|
||||
it('对于任何过期会话,清理时应该释放所有相关的Zulip资源', async () => {
|
||||
await fc.assert(
|
||||
fc.asyncProperty(
|
||||
// 生成过期会话数量
|
||||
fc.integer({ min: 1, max: 10 }),
|
||||
// 生成每个会话对应的Zulip队列ID
|
||||
fc.array(
|
||||
fc.string({ minLength: 8, maxLength: 20 }).filter(s => s.trim().length > 0),
|
||||
{ minLength: 1, maxLength: 10 }
|
||||
),
|
||||
async (sessionCount, queueIds) => {
|
||||
// 重置mock以确保每次测试都是干净的状态
|
||||
jest.clearAllMocks();
|
||||
|
||||
const actualQueueIds = queueIds.slice(0, sessionCount);
|
||||
const mockResult = createMockCleanupResult({
|
||||
cleanedCount: sessionCount,
|
||||
zulipQueueIds: actualQueueIds,
|
||||
});
|
||||
|
||||
mockSessionManager.cleanupExpiredSessions.mockResolvedValue(mockResult);
|
||||
|
||||
const result = await service.runCleanup();
|
||||
|
||||
// 验证清理成功
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.cleanedSessions).toBe(sessionCount);
|
||||
|
||||
// 验证Zulip队列被处理(这里简化为计数验证)
|
||||
expect(result.deregisteredQueues).toBe(actualQueueIds.length);
|
||||
|
||||
// 验证SessionManager被调用清理过期会话
|
||||
expect(mockSessionManager.cleanupExpiredSessions).toHaveBeenCalledWith(30);
|
||||
}
|
||||
),
|
||||
{ numRuns: 50 }
|
||||
);
|
||||
}, 30000);
|
||||
|
||||
/**
|
||||
* 属性: 清理操作应该是原子性的,要么全部成功要么全部回滚
|
||||
* 验证需求 6.5: 清理过程应确保数据一致性,避免部分清理导致的不一致状态
|
||||
*/
|
||||
it('清理操作应该是原子性的,要么全部成功要么全部回滚', async () => {
|
||||
await fc.assert(
|
||||
fc.asyncProperty(
|
||||
// 生成是否模拟清理失败
|
||||
fc.boolean(),
|
||||
// 生成会话数量
|
||||
fc.integer({ min: 1, max: 5 }),
|
||||
async (shouldFail, sessionCount) => {
|
||||
// 重置mock以确保每次测试都是干净的状态
|
||||
jest.clearAllMocks();
|
||||
|
||||
if (shouldFail) {
|
||||
// 模拟清理失败
|
||||
const error = new Error('清理操作失败');
|
||||
mockSessionManager.cleanupExpiredSessions.mockRejectedValue(error);
|
||||
} else {
|
||||
// 模拟清理成功
|
||||
const mockResult = createMockCleanupResult({
|
||||
cleanedCount: sessionCount,
|
||||
zulipQueueIds: Array.from({ length: sessionCount }, (_, i) => `queue-${i}`),
|
||||
});
|
||||
mockSessionManager.cleanupExpiredSessions.mockResolvedValue(mockResult);
|
||||
}
|
||||
|
||||
const result = await service.runCleanup();
|
||||
|
||||
if (shouldFail) {
|
||||
// 失败时应该没有任何资源被释放
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.cleanedSessions).toBe(0);
|
||||
expect(result.deregisteredQueues).toBe(0);
|
||||
expect(result.error).toBeDefined();
|
||||
} else {
|
||||
// 成功时所有资源都应该被正确处理
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.cleanedSessions).toBe(sessionCount);
|
||||
expect(result.deregisteredQueues).toBe(sessionCount);
|
||||
expect(result.error).toBeUndefined();
|
||||
}
|
||||
|
||||
// 验证结果的一致性
|
||||
expect(result.timestamp).toBeInstanceOf(Date);
|
||||
expect(result.duration).toBeGreaterThanOrEqual(0);
|
||||
}
|
||||
),
|
||||
{ numRuns: 50 }
|
||||
);
|
||||
}, 30000);
|
||||
|
||||
/**
|
||||
* 属性: 清理配置更新应该正确重启清理任务而不丢失状态
|
||||
* 验证需求 6.5: 配置更新时系统应保持服务连续性
|
||||
*/
|
||||
it('清理配置更新应该正确重启清理任务而不丢失状态', async () => {
|
||||
await fc.assert(
|
||||
fc.asyncProperty(
|
||||
// 生成初始配置
|
||||
fc.record({
|
||||
intervalMs: fc.integer({ min: 1, max: 5 }).map(m => m * 60 * 1000),
|
||||
sessionTimeoutMinutes: fc.integer({ min: 10, max: 60 }),
|
||||
}),
|
||||
// 生成新配置
|
||||
fc.record({
|
||||
intervalMs: fc.integer({ min: 1, max: 5 }).map(m => m * 60 * 1000),
|
||||
sessionTimeoutMinutes: fc.integer({ min: 10, max: 60 }),
|
||||
}),
|
||||
async (initialConfig, newConfig) => {
|
||||
// 重置mock以确保每次测试都是干净的状态
|
||||
jest.clearAllMocks();
|
||||
|
||||
// 设置初始配置并启动任务
|
||||
service.updateConfig(initialConfig);
|
||||
service.startCleanupTask();
|
||||
|
||||
let status = service.getStatus();
|
||||
expect(status.isEnabled).toBe(true);
|
||||
expect(status.config.intervalMs).toBe(initialConfig.intervalMs);
|
||||
|
||||
// 更新配置
|
||||
service.updateConfig(newConfig);
|
||||
|
||||
// 验证配置更新后任务仍在运行
|
||||
status = service.getStatus();
|
||||
expect(status.isEnabled).toBe(true);
|
||||
expect(status.config.intervalMs).toBe(newConfig.intervalMs);
|
||||
expect(status.config.sessionTimeoutMinutes).toBe(newConfig.sessionTimeoutMinutes);
|
||||
|
||||
service.stopCleanupTask();
|
||||
}
|
||||
),
|
||||
{ numRuns: 30 }
|
||||
);
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
describe('模块生命周期', () => {
|
||||
it('应该在模块初始化时启动清理任务', async () => {
|
||||
// 重新创建服务实例来测试模块初始化
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
SessionCleanupService,
|
||||
{
|
||||
provide: SessionManagerService,
|
||||
useValue: mockSessionManager,
|
||||
},
|
||||
{
|
||||
provide: 'ZULIP_CLIENT_POOL_SERVICE',
|
||||
useValue: mockZulipClientPool,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
const newService = module.get<SessionCleanupService>(SessionCleanupService);
|
||||
|
||||
// 模拟模块初始化
|
||||
await newService.onModuleInit();
|
||||
|
||||
const status = newService.getStatus();
|
||||
expect(status.isEnabled).toBe(true);
|
||||
|
||||
// 清理
|
||||
await newService.onModuleDestroy();
|
||||
});
|
||||
|
||||
it('应该在模块销毁时停止清理任务', async () => {
|
||||
service.startCleanupTask();
|
||||
|
||||
await service.onModuleDestroy();
|
||||
|
||||
const status = service.getStatus();
|
||||
expect(status.isEnabled).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -21,9 +21,9 @@
|
||||
* @since 2025-12-25
|
||||
*/
|
||||
|
||||
import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||
import { SessionManagerService } from './session-manager.service';
|
||||
import { ZulipClientPoolService } from './zulip-client-pool.service';
|
||||
import { Injectable, Logger, OnModuleInit, OnModuleDestroy, Inject } from '@nestjs/common';
|
||||
import { SessionManagerService } from './session_manager.service';
|
||||
import { IZulipClientPoolService } from '../../../core/zulip/interfaces/zulip-core.interfaces';
|
||||
|
||||
/**
|
||||
* 清理任务配置接口
|
||||
@@ -55,6 +55,28 @@ export interface CleanupResult {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话清理服务类
|
||||
*
|
||||
* 职责:
|
||||
* - 定时清理过期的游戏会话
|
||||
* - 释放无效的Zulip客户端资源
|
||||
* - 维护会话数据的一致性
|
||||
* - 提供会话清理统计和监控
|
||||
*
|
||||
* 主要方法:
|
||||
* - startCleanup(): 启动定时清理任务
|
||||
* - stopCleanup(): 停止清理任务
|
||||
* - performCleanup(): 执行一次清理操作
|
||||
* - getCleanupStats(): 获取清理统计信息
|
||||
* - updateConfig(): 更新清理配置
|
||||
*
|
||||
* 使用场景:
|
||||
* - 系统启动时自动开始清理任务
|
||||
* - 定期清理过期会话和资源
|
||||
* - 系统关闭时停止清理任务
|
||||
* - 监控清理效果和系统健康
|
||||
*/
|
||||
@Injectable()
|
||||
export class SessionCleanupService implements OnModuleInit, OnModuleDestroy {
|
||||
private cleanupInterval: NodeJS.Timeout | null = null;
|
||||
@@ -70,7 +92,8 @@ export class SessionCleanupService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
constructor(
|
||||
private readonly sessionManager: SessionManagerService,
|
||||
private readonly zulipClientPool: ZulipClientPoolService,
|
||||
@Inject('ZULIP_CLIENT_POOL_SERVICE')
|
||||
private readonly zulipClientPool: IZulipClientPoolService,
|
||||
) {
|
||||
this.logger.log('SessionCleanupService初始化完成');
|
||||
}
|
||||
@@ -176,7 +199,8 @@ export class SessionCleanupService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
// 2. 注销对应的Zulip事件队列
|
||||
let deregisteredQueues = 0;
|
||||
for (const queueId of cleanupResult.zulipQueueIds) {
|
||||
const queueIds = cleanupResult?.zulipQueueIds || [];
|
||||
for (const queueId of queueIds) {
|
||||
try {
|
||||
// 根据queueId找到对应的用户并注销队列
|
||||
// 注意:这里需要通过某种方式找到queueId对应的userId
|
||||
@@ -200,7 +224,7 @@ export class SessionCleanupService implements OnModuleInit, OnModuleDestroy {
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
const result: CleanupResult = {
|
||||
cleanedSessions: cleanupResult.cleanedCount,
|
||||
cleanedSessions: cleanupResult?.cleanedCount || 0,
|
||||
deregisteredQueues,
|
||||
duration,
|
||||
timestamp: new Date(),
|
||||
@@ -12,8 +12,8 @@
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import * as fc from 'fast-check';
|
||||
import { SessionManagerService, GameSession, Position } from './session-manager.service';
|
||||
import { ConfigManagerService } from './config-manager.service';
|
||||
import { SessionManagerService, GameSession, Position } from './session_manager.service';
|
||||
import { IZulipConfigService } from '../../../core/zulip/interfaces/zulip-core.interfaces';
|
||||
import { AppLoggerService } from '../../../core/utils/logger/logger.service';
|
||||
import { IRedisService } from '../../../core/redis/redis.interface';
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('SessionManagerService', () => {
|
||||
let service: SessionManagerService;
|
||||
let mockLogger: jest.Mocked<AppLoggerService>;
|
||||
let mockRedisService: jest.Mocked<IRedisService>;
|
||||
let mockConfigManager: jest.Mocked<ConfigManagerService>;
|
||||
let mockConfigManager: jest.Mocked<IZulipConfigService>;
|
||||
|
||||
// 内存存储模拟Redis
|
||||
let memoryStore: Map<string, { value: string; expireAt?: number }>;
|
||||
@@ -57,9 +57,15 @@ describe('SessionManagerService', () => {
|
||||
};
|
||||
return streamMap[mapId] || 'General';
|
||||
}),
|
||||
getMapIdByStream: jest.fn(),
|
||||
getTopicByObject: jest.fn().mockReturnValue('General'),
|
||||
getMapConfig: jest.fn(),
|
||||
getAllMaps: jest.fn(),
|
||||
getZulipConfig: jest.fn(),
|
||||
hasMap: jest.fn(),
|
||||
hasStream: jest.fn(),
|
||||
getAllMapIds: jest.fn(),
|
||||
getAllStreams: jest.fn(),
|
||||
reloadConfig: jest.fn(),
|
||||
validateConfig: jest.fn(),
|
||||
} as any;
|
||||
|
||||
// 创建模拟Redis服务,使用内存存储
|
||||
@@ -135,7 +141,7 @@ describe('SessionManagerService', () => {
|
||||
useValue: mockRedisService,
|
||||
},
|
||||
{
|
||||
provide: ConfigManagerService,
|
||||
provide: 'ZULIP_CONFIG_SERVICE',
|
||||
useValue: mockConfigManager,
|
||||
},
|
||||
],
|
||||
@@ -35,8 +35,8 @@
|
||||
|
||||
import { Injectable, Logger, Inject } from '@nestjs/common';
|
||||
import { IRedisService } from '../../../core/redis/redis.interface';
|
||||
import { ConfigManagerService } from './config-manager.service';
|
||||
import { Internal, Constants } from '../interfaces/zulip.interfaces';
|
||||
import { IZulipConfigService } from '../../../core/zulip/interfaces/zulip-core.interfaces';
|
||||
import { Internal, Constants } from '../../../core/zulip/interfaces/zulip.interfaces';
|
||||
|
||||
/**
|
||||
* 游戏会话接口 - 重新导出以保持向后兼容
|
||||
@@ -78,6 +78,29 @@ export interface SessionStats {
|
||||
newestSession?: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话管理服务类
|
||||
*
|
||||
* 职责:
|
||||
* - 维护WebSocket连接ID与Zulip队列ID的映射关系
|
||||
* - 管理玩家位置跟踪和上下文注入
|
||||
* - 提供空间过滤和会话查询功能
|
||||
* - 支持会话状态的序列化和反序列化
|
||||
*
|
||||
* 主要方法:
|
||||
* - createSession(): 创建会话并绑定Socket_ID与Zulip_Queue_ID
|
||||
* - getSession(): 获取会话信息
|
||||
* - injectContext(): 上下文注入,根据位置确定Stream/Topic
|
||||
* - getSocketsInMap(): 空间过滤,获取指定地图的所有Socket
|
||||
* - updatePlayerPosition(): 更新玩家位置
|
||||
* - destroySession(): 销毁会话
|
||||
*
|
||||
* 使用场景:
|
||||
* - 玩家登录时创建会话映射
|
||||
* - 消息路由时进行上下文注入
|
||||
* - 消息分发时进行空间过滤
|
||||
* - 玩家登出时清理会话数据
|
||||
*/
|
||||
@Injectable()
|
||||
export class SessionManagerService {
|
||||
private readonly SESSION_PREFIX = 'zulip:session:';
|
||||
@@ -91,7 +114,8 @@ export class SessionManagerService {
|
||||
constructor(
|
||||
@Inject('REDIS_SERVICE')
|
||||
private readonly redisService: IRedisService,
|
||||
private readonly configManager: ConfigManagerService,
|
||||
@Inject('ZULIP_CONFIG_SERVICE')
|
||||
private readonly configManager: IZulipConfigService,
|
||||
) {
|
||||
this.logger.log('SessionManagerService初始化完成');
|
||||
}
|
||||
@@ -170,6 +194,9 @@ export class SessionManagerService {
|
||||
* @param initialMap 初始地图(可选)
|
||||
* @param initialPosition 初始位置(可选)
|
||||
* @returns Promise<GameSession> 创建的会话对象
|
||||
*
|
||||
* @throws Error 当参数验证失败时
|
||||
* @throws Error 当Redis操作失败时
|
||||
*/
|
||||
async createSession(
|
||||
socketId: string,
|
||||
@@ -378,6 +405,8 @@ export class SessionManagerService {
|
||||
* @param socketId WebSocket连接ID
|
||||
* @param mapId 地图ID(可选,用于覆盖当前地图)
|
||||
* @returns Promise<ContextInfo> 上下文信息
|
||||
*
|
||||
* @throws Error 当会话不存在时
|
||||
*/
|
||||
async injectContext(socketId: string, mapId?: string): Promise<ContextInfo> {
|
||||
this.logger.debug('开始上下文注入', {
|
||||
@@ -24,18 +24,17 @@ import {
|
||||
ZulipMessage,
|
||||
GameMessage,
|
||||
MessageDistributor,
|
||||
} from './zulip-event-processor.service';
|
||||
import { SessionManagerService, GameSession } from './session-manager.service';
|
||||
import { ConfigManagerService } from './config-manager.service';
|
||||
import { ZulipClientPoolService } from './zulip-client-pool.service';
|
||||
} from './zulip_event_processor.service';
|
||||
import { SessionManagerService, GameSession } from './session_manager.service';
|
||||
import { IZulipConfigService, IZulipClientPoolService } from '../../../core/zulip/interfaces/zulip-core.interfaces';
|
||||
import { AppLoggerService } from '../../../core/utils/logger/logger.service';
|
||||
|
||||
describe('ZulipEventProcessorService', () => {
|
||||
let service: ZulipEventProcessorService;
|
||||
let mockLogger: jest.Mocked<AppLoggerService>;
|
||||
let mockSessionManager: jest.Mocked<SessionManagerService>;
|
||||
let mockConfigManager: jest.Mocked<ConfigManagerService>;
|
||||
let mockClientPool: jest.Mocked<ZulipClientPoolService>;
|
||||
let mockConfigManager: jest.Mocked<IZulipConfigService>;
|
||||
let mockClientPool: jest.Mocked<IZulipClientPoolService>;
|
||||
let mockDistributor: jest.Mocked<MessageDistributor>;
|
||||
|
||||
// 创建模拟Zulip消息
|
||||
@@ -87,14 +86,26 @@ describe('ZulipEventProcessorService', () => {
|
||||
mockConfigManager = {
|
||||
getMapIdByStream: jest.fn(),
|
||||
getStreamByMap: jest.fn(),
|
||||
getMapConfig: jest.fn(),
|
||||
getTopicByObject: jest.fn(),
|
||||
getZulipConfig: jest.fn(),
|
||||
hasMap: jest.fn(),
|
||||
hasStream: jest.fn(),
|
||||
getAllMapIds: jest.fn(),
|
||||
getAllStreams: jest.fn(),
|
||||
reloadConfig: jest.fn(),
|
||||
validateConfig: jest.fn(),
|
||||
} as any;
|
||||
|
||||
mockClientPool = {
|
||||
getUserClient: jest.fn(),
|
||||
createUserClient: jest.fn(),
|
||||
destroyUserClient: jest.fn(),
|
||||
hasUserClient: jest.fn(),
|
||||
sendMessage: jest.fn(),
|
||||
registerEventQueue: jest.fn(),
|
||||
deregisterEventQueue: jest.fn(),
|
||||
getPoolStats: jest.fn(),
|
||||
cleanupIdleClients: jest.fn(),
|
||||
} as any;
|
||||
|
||||
mockDistributor = {
|
||||
@@ -114,11 +125,11 @@ describe('ZulipEventProcessorService', () => {
|
||||
useValue: mockSessionManager,
|
||||
},
|
||||
{
|
||||
provide: ConfigManagerService,
|
||||
provide: 'ZULIP_CONFIG_SERVICE',
|
||||
useValue: mockConfigManager,
|
||||
},
|
||||
{
|
||||
provide: ZulipClientPoolService,
|
||||
provide: 'ZULIP_CLIENT_POOL_SERVICE',
|
||||
useValue: mockClientPool,
|
||||
},
|
||||
],
|
||||
@@ -31,9 +31,8 @@
|
||||
*/
|
||||
|
||||
import { Injectable, OnModuleDestroy, Inject, forwardRef, Logger } from '@nestjs/common';
|
||||
import { SessionManagerService } from './session-manager.service';
|
||||
import { ConfigManagerService } from './config-manager.service';
|
||||
import { ZulipClientPoolService } from './zulip-client-pool.service';
|
||||
import { SessionManagerService } from './session_manager.service';
|
||||
import { IZulipConfigService, IZulipClientPoolService } from '../../../core/zulip/interfaces/zulip-core.interfaces';
|
||||
|
||||
/**
|
||||
* Zulip消息接口
|
||||
@@ -94,6 +93,28 @@ export interface EventProcessingStats {
|
||||
lastEventTime?: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zulip事件处理服务类
|
||||
*
|
||||
* 职责:
|
||||
* - 处理从Zulip接收的事件队列消息
|
||||
* - 将Zulip消息转换为游戏协议格式
|
||||
* - 管理事件队列的生命周期
|
||||
* - 提供消息分发和路由功能
|
||||
*
|
||||
* 主要方法:
|
||||
* - processEvents(): 处理Zulip事件队列
|
||||
* - processMessage(): 处理单个消息事件
|
||||
* - startProcessing(): 启动事件处理
|
||||
* - stopProcessing(): 停止事件处理
|
||||
* - registerQueue(): 注册新的事件队列
|
||||
*
|
||||
* 使用场景:
|
||||
* - 接收Zulip服务器推送的消息
|
||||
* - 将Zulip消息转发给游戏客户端
|
||||
* - 管理多用户的事件队列
|
||||
* - 消息格式转换和过滤
|
||||
*/
|
||||
@Injectable()
|
||||
export class ZulipEventProcessorService implements OnModuleDestroy {
|
||||
private readonly logger = new Logger(ZulipEventProcessorService.name);
|
||||
@@ -109,9 +130,10 @@ export class ZulipEventProcessorService implements OnModuleDestroy {
|
||||
|
||||
constructor(
|
||||
private readonly sessionManager: SessionManagerService,
|
||||
private readonly configManager: ConfigManagerService,
|
||||
@Inject(forwardRef(() => ZulipClientPoolService))
|
||||
private readonly clientPool: ZulipClientPoolService,
|
||||
@Inject('ZULIP_CONFIG_SERVICE')
|
||||
private readonly configManager: IZulipConfigService,
|
||||
@Inject('ZULIP_CLIENT_POOL_SERVICE')
|
||||
private readonly clientPool: IZulipClientPoolService,
|
||||
) {
|
||||
this.logger.log('ZulipEventProcessorService初始化完成');
|
||||
}
|
||||
@@ -2,26 +2,32 @@
|
||||
* Zulip集成业务模块
|
||||
*
|
||||
* 功能描述:
|
||||
* - 整合Zulip集成相关的控制器、服务和依赖
|
||||
* - 提供完整的Zulip集成功能模块
|
||||
* - 实现游戏与Zulip的无缝通信桥梁
|
||||
* - 支持WebSocket网关、会话管理、消息过滤等核心功能
|
||||
* - 启动时自动检查并创建所有地图对应的Zulip Streams
|
||||
* - 整合Zulip集成相关的业务逻辑和控制器
|
||||
* - 提供完整的Zulip集成业务功能模块
|
||||
* - 实现游戏与Zulip的业务逻辑协调
|
||||
* - 支持WebSocket网关、会话管理、消息过滤等业务功能
|
||||
*
|
||||
* 核心服务:
|
||||
* - ZulipService: 主协调服务,处理登录、消息发送等核心业务
|
||||
* 架构设计:
|
||||
* - 业务逻辑层:处理游戏相关的业务规则和流程
|
||||
* - 核心服务层:封装技术实现细节和第三方API调用
|
||||
* - 通过依赖注入实现业务层与技术层的解耦
|
||||
*
|
||||
* 业务服务:
|
||||
* - ZulipService: 主协调服务,处理登录、消息发送等核心业务流程
|
||||
* - ZulipWebSocketGateway: WebSocket统一网关,处理客户端连接
|
||||
* - ZulipClientPoolService: Zulip客户端池管理
|
||||
* - SessionManagerService: 会话状态管理
|
||||
* - MessageFilterService: 消息过滤和安全控制
|
||||
* - SessionManagerService: 会话状态管理和业务逻辑
|
||||
* - MessageFilterService: 消息过滤和业务规则控制
|
||||
*
|
||||
* 核心服务(通过ZulipCoreModule提供):
|
||||
* - ZulipClientService: Zulip REST API封装
|
||||
* - ZulipClientPoolService: 客户端池管理
|
||||
* - ConfigManagerService: 配置管理和热重载
|
||||
* - StreamInitializerService: Stream初始化和自动创建
|
||||
* - ErrorHandlerService: 错误处理和服务降级
|
||||
* - MonitoringService: 系统监控和告警
|
||||
* - ApiKeySecurityService: API Key安全存储
|
||||
* - ZulipEventProcessorService: 事件处理和消息转换
|
||||
* - 其他技术支持服务
|
||||
*
|
||||
* 依赖模块:
|
||||
* - LoginModule: 用户认证和会话管理
|
||||
* - ZulipCoreModule: Zulip核心技术服务
|
||||
* - LoginCoreModule: 用户认证和会话管理
|
||||
* - RedisModule: 会话状态缓存
|
||||
* - LoggerModule: 日志记录服务
|
||||
*
|
||||
@@ -29,65 +35,47 @@
|
||||
* - 游戏客户端通过WebSocket连接进行实时聊天
|
||||
* - 游戏内消息与Zulip社群的双向同步
|
||||
* - 基于位置的聊天上下文管理
|
||||
* - 系统启动时自动初始化所有地图对应的Streams
|
||||
* - 业务规则驱动的消息过滤和权限控制
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-25
|
||||
* @version 2.0.0
|
||||
* @since 2025-12-31
|
||||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ZulipWebSocketGateway } from './zulip-websocket.gateway';
|
||||
import { ZulipWebSocketGateway } from './zulip_websocket.gateway';
|
||||
import { ZulipService } from './zulip.service';
|
||||
import { ZulipClientService } from './services/zulip-client.service';
|
||||
import { ZulipClientPoolService } from './services/zulip-client-pool.service';
|
||||
import { SessionManagerService } from './services/session-manager.service';
|
||||
import { SessionCleanupService } from './services/session-cleanup.service';
|
||||
import { MessageFilterService } from './services/message-filter.service';
|
||||
import { ZulipEventProcessorService } from './services/zulip-event-processor.service';
|
||||
import { ConfigManagerService } from './services/config-manager.service';
|
||||
import { ErrorHandlerService } from './services/error-handler.service';
|
||||
import { MonitoringService } from './services/monitoring.service';
|
||||
import { ApiKeySecurityService } from './services/api-key-security.service';
|
||||
import { StreamInitializerService } from './services/stream-initializer.service';
|
||||
import { SessionManagerService } from './services/session_manager.service';
|
||||
import { MessageFilterService } from './services/message_filter.service';
|
||||
import { ZulipEventProcessorService } from './services/zulip_event_processor.service';
|
||||
import { SessionCleanupService } from './services/session_cleanup.service';
|
||||
import { ZulipCoreModule } from '../../core/zulip/zulip-core.module';
|
||||
import { RedisModule } from '../../core/redis/redis.module';
|
||||
import { LoggerModule } from '../../core/utils/logger/logger.module';
|
||||
import { LoginModule } from '../login/login.module';
|
||||
import { LoginCoreModule } from '../../core/login_core/login_core.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// Zulip核心服务模块 - 提供技术实现相关的核心服务
|
||||
ZulipCoreModule,
|
||||
// Redis模块 - 提供会话状态缓存和数据存储
|
||||
RedisModule,
|
||||
// 日志模块 - 提供统一的日志记录服务
|
||||
LoggerModule,
|
||||
// 登录模块 - 提供用户认证和Token验证
|
||||
LoginModule,
|
||||
LoginCoreModule,
|
||||
],
|
||||
providers: [
|
||||
// 主协调服务 - 整合各子服务,提供统一业务接口
|
||||
ZulipService,
|
||||
// Zulip客户端服务 - 封装Zulip REST API调用
|
||||
ZulipClientService,
|
||||
// Zulip客户端池服务 - 管理用户专用Zulip客户端实例
|
||||
ZulipClientPoolService,
|
||||
// 会话管理服务 - 维护Socket_ID与Zulip_Queue_ID的映射关系
|
||||
SessionManagerService,
|
||||
// 会话清理服务 - 定时清理过期会话
|
||||
SessionCleanupService,
|
||||
// 消息过滤服务 - 敏感词过滤、频率限制、权限验证
|
||||
MessageFilterService,
|
||||
// Zulip事件处理服务 - 处理Zulip事件队列消息
|
||||
ZulipEventProcessorService,
|
||||
// 配置管理服务 - 地图映射配置和系统配置管理
|
||||
ConfigManagerService,
|
||||
// Stream初始化服务 - 启动时检查并创建所有地图对应的Streams
|
||||
StreamInitializerService,
|
||||
// 错误处理服务 - 错误处理、重试机制、服务降级
|
||||
ErrorHandlerService,
|
||||
// 监控服务 - 系统监控、健康检查、告警
|
||||
MonitoringService,
|
||||
// API Key安全服务 - API Key加密存储和安全日志
|
||||
ApiKeySecurityService,
|
||||
// 会话清理服务 - 定时清理过期会话
|
||||
SessionCleanupService,
|
||||
// WebSocket网关 - 处理游戏客户端WebSocket连接
|
||||
ZulipWebSocketGateway,
|
||||
],
|
||||
@@ -95,26 +83,14 @@ import { LoginModule } from '../login/login.module';
|
||||
exports: [
|
||||
// 导出主服务供其他模块使用
|
||||
ZulipService,
|
||||
// 导出Zulip客户端服务
|
||||
ZulipClientService,
|
||||
// 导出客户端池服务
|
||||
ZulipClientPoolService,
|
||||
// 导出会话管理服务
|
||||
SessionManagerService,
|
||||
// 导出会话清理服务
|
||||
SessionCleanupService,
|
||||
// 导出消息过滤服务
|
||||
MessageFilterService,
|
||||
// 导出配置管理服务
|
||||
ConfigManagerService,
|
||||
// 导出Stream初始化服务
|
||||
StreamInitializerService,
|
||||
// 导出错误处理服务
|
||||
ErrorHandlerService,
|
||||
// 导出监控服务
|
||||
MonitoringService,
|
||||
// 导出API Key安全服务
|
||||
ApiKeySecurityService,
|
||||
// 导出事件处理服务
|
||||
ZulipEventProcessorService,
|
||||
// 导出会话清理服务
|
||||
SessionCleanupService,
|
||||
// 导出WebSocket网关
|
||||
ZulipWebSocketGateway,
|
||||
],
|
||||
|
||||
1134
src/business/zulip/zulip.service.spec.ts
Normal file
1134
src/business/zulip/zulip.service.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -22,14 +22,16 @@
|
||||
* @since 2025-12-25
|
||||
*/
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Injectable, Logger, Inject } from '@nestjs/common';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { ZulipClientPoolService } from './services/zulip-client-pool.service';
|
||||
import { SessionManagerService } from './services/session-manager.service';
|
||||
import { MessageFilterService } from './services/message-filter.service';
|
||||
import { ZulipEventProcessorService } from './services/zulip-event-processor.service';
|
||||
import { ConfigManagerService } from './services/config-manager.service';
|
||||
import { ErrorHandlerService } from './services/error-handler.service';
|
||||
import { SessionManagerService } from './services/session_manager.service';
|
||||
import { MessageFilterService } from './services/message_filter.service';
|
||||
import { ZulipEventProcessorService } from './services/zulip_event_processor.service';
|
||||
import {
|
||||
IZulipClientPoolService,
|
||||
IZulipConfigService,
|
||||
} from '../../core/zulip/interfaces/zulip-core.interfaces';
|
||||
import { ApiKeySecurityService } from '../../core/zulip/services/api_key_security.service';
|
||||
|
||||
/**
|
||||
* 玩家登录请求接口
|
||||
@@ -79,18 +81,41 @@ export interface ChatMessageResponse {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zulip集成主服务类
|
||||
*
|
||||
* 职责:
|
||||
* - 作为Zulip集成系统的主要协调服务
|
||||
* - 整合各个子服务,提供统一的业务接口
|
||||
* - 处理游戏客户端与Zulip之间的核心业务逻辑
|
||||
* - 管理玩家会话和消息路由
|
||||
*
|
||||
* 主要方法:
|
||||
* - handlePlayerLogin(): 处理玩家登录和Zulip客户端初始化
|
||||
* - handlePlayerLogout(): 处理玩家登出和资源清理
|
||||
* - sendChatMessage(): 处理游戏聊天消息发送到Zulip
|
||||
* - updatePlayerPosition(): 更新玩家位置信息
|
||||
*
|
||||
* 使用场景:
|
||||
* - WebSocket网关调用处理消息路由
|
||||
* - 会话管理和状态维护
|
||||
* - 消息格式转换和过滤
|
||||
* - 游戏与Zulip的双向通信桥梁
|
||||
*/
|
||||
@Injectable()
|
||||
export class ZulipService {
|
||||
private readonly logger = new Logger(ZulipService.name);
|
||||
private readonly DEFAULT_MAP = 'whale_port';
|
||||
|
||||
constructor(
|
||||
private readonly zulipClientPool: ZulipClientPoolService,
|
||||
@Inject('ZULIP_CLIENT_POOL_SERVICE')
|
||||
private readonly zulipClientPool: IZulipClientPoolService,
|
||||
private readonly sessionManager: SessionManagerService,
|
||||
private readonly messageFilter: MessageFilterService,
|
||||
private readonly eventProcessor: ZulipEventProcessorService,
|
||||
private readonly configManager: ConfigManagerService,
|
||||
private readonly errorHandler: ErrorHandlerService,
|
||||
@Inject('ZULIP_CONFIG_SERVICE')
|
||||
private readonly configManager: IZulipConfigService,
|
||||
private readonly apiKeySecurityService: ApiKeySecurityService,
|
||||
) {
|
||||
this.logger.log('ZulipService初始化完成');
|
||||
}
|
||||
@@ -295,36 +320,38 @@ export class ZulipService {
|
||||
// 从Token中提取用户ID(模拟)
|
||||
const userId = `user_${token.substring(0, 8)}`;
|
||||
|
||||
// 为测试用户提供真实的 Zulip API Key
|
||||
// 从ApiKeySecurityService获取真实的Zulip API Key
|
||||
let zulipApiKey = undefined;
|
||||
let zulipEmail = undefined;
|
||||
|
||||
// 检查是否是配置了真实 Zulip API Key 的测试用户
|
||||
const hasTestApiKey = token.includes('lCPWCPf');
|
||||
const hasUserApiKey = token.includes('W2KhXaQx');
|
||||
const hasOldApiKey = token.includes('MZ1jEMQo');
|
||||
const isRealUserToken = token === 'real_user_token_with_zulip_key_123';
|
||||
|
||||
this.logger.log('Token检查', {
|
||||
operation: 'validateGameToken',
|
||||
userId,
|
||||
tokenPrefix: token.substring(0, 20),
|
||||
hasUserApiKey,
|
||||
hasOldApiKey,
|
||||
isRealUserToken,
|
||||
});
|
||||
|
||||
if (isRealUserToken || hasUserApiKey || hasTestApiKey || hasOldApiKey) {
|
||||
// 使用用户的真实 API Key
|
||||
// 注意:这个API Key对应的Zulip用户邮箱是 user8@zulip.xinghangee.icu
|
||||
zulipApiKey = 'lCPWCPfGh7WUHxwN56GF8oYXOpqNfGF8';
|
||||
zulipEmail = 'angjustinl@mail.angforever.top';
|
||||
try {
|
||||
// 尝试从Redis获取存储的API Key
|
||||
const apiKeyResult = await this.apiKeySecurityService.getApiKey(userId);
|
||||
|
||||
this.logger.log('配置真实Zulip API Key', {
|
||||
if (apiKeyResult.success && apiKeyResult.apiKey) {
|
||||
zulipApiKey = apiKeyResult.apiKey;
|
||||
// TODO: 从数据库获取用户的Zulip邮箱
|
||||
// 暂时使用模拟数据
|
||||
zulipEmail = 'angjustinl@163.com';
|
||||
|
||||
this.logger.log('从存储获取到Zulip API Key', {
|
||||
operation: 'validateGameToken',
|
||||
userId,
|
||||
hasApiKey: true,
|
||||
zulipEmail,
|
||||
});
|
||||
} else {
|
||||
this.logger.debug('用户没有存储的Zulip API Key', {
|
||||
operation: 'validateGameToken',
|
||||
userId,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
this.logger.warn('获取Zulip API Key失败', {
|
||||
operation: 'validateGameToken',
|
||||
userId,
|
||||
zulipEmail,
|
||||
hasApiKey: true,
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -332,7 +359,6 @@ export class ZulipService {
|
||||
userId,
|
||||
username: `Player_${userId.substring(5, 10)}`,
|
||||
email: `${userId}@example.com`,
|
||||
// 实际项目中从数据库获取
|
||||
zulipEmail,
|
||||
zulipApiKey,
|
||||
};
|
||||
|
||||
@@ -56,7 +56,7 @@ describeE2E('Zulip Integration E2E Tests', () => {
|
||||
});
|
||||
|
||||
client.on('connect', () => resolve(client));
|
||||
client.on('connect_error', (err) => reject(err));
|
||||
client.on('connect_error', (err: any) => reject(err));
|
||||
|
||||
setTimeout(() => reject(new Error('Connection timeout')), 5000);
|
||||
});
|
||||
@@ -16,9 +16,9 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import * as fc from 'fast-check';
|
||||
import { ZulipWebSocketGateway } from './zulip-websocket.gateway';
|
||||
import { ZulipWebSocketGateway } from './zulip_websocket.gateway';
|
||||
import { ZulipService, LoginResponse, ChatMessageResponse } from './zulip.service';
|
||||
import { SessionManagerService, GameSession } from './services/session-manager.service';
|
||||
import { SessionManagerService, GameSession } from './services/session_manager.service';
|
||||
import { Server, Socket } from 'socket.io';
|
||||
|
||||
describe('ZulipWebSocketGateway', () => {
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
import { Server, Socket } from 'socket.io';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ZulipService } from './zulip.service';
|
||||
import { SessionManagerService } from './services/session-manager.service';
|
||||
import { SessionManagerService } from './services/session_manager.service';
|
||||
|
||||
/**
|
||||
* 登录消息接口 - 按guide.md格式
|
||||
@@ -96,6 +96,29 @@ interface ClientData {
|
||||
connectedAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zulip WebSocket网关类
|
||||
*
|
||||
* 职责:
|
||||
* - 处理所有Godot游戏客户端的WebSocket连接
|
||||
* - 实现游戏协议到Zulip协议的转换
|
||||
* - 提供统一的消息路由和权限控制
|
||||
* - 管理客户端连接状态和会话
|
||||
*
|
||||
* 主要方法:
|
||||
* - handleConnection(): 处理客户端连接建立
|
||||
* - handleDisconnect(): 处理客户端连接断开
|
||||
* - handleLogin(): 处理登录消息
|
||||
* - handleChat(): 处理聊天消息
|
||||
* - handlePositionUpdate(): 处理位置更新
|
||||
* - sendChatRender(): 向客户端发送聊天渲染消息
|
||||
*
|
||||
* 使用场景:
|
||||
* - 游戏客户端WebSocket通信的统一入口
|
||||
* - 消息协议转换和路由分发
|
||||
* - 连接状态管理和权限验证
|
||||
* - 实时消息推送和广播
|
||||
*/
|
||||
@Injectable()
|
||||
@WebSocketGateway({
|
||||
cors: { origin: '*' },
|
||||
@@ -19,8 +19,9 @@
|
||||
* @since 2025-12-17
|
||||
*/
|
||||
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, OneToOne } from 'typeorm';
|
||||
import { UserStatus } from '../../../business/user-mgmt/enums/user-status.enum';
|
||||
import { ZulipAccounts } from '../zulip_accounts/zulip_accounts.entity';
|
||||
|
||||
/**
|
||||
* 用户实体类
|
||||
@@ -432,4 +433,25 @@ export class Users {
|
||||
comment: '更新时间'
|
||||
})
|
||||
updated_at: Date;
|
||||
|
||||
/**
|
||||
* 关联的Zulip账号
|
||||
*
|
||||
* 关系设计:
|
||||
* - 类型:一对一关系(OneToOne)
|
||||
* - 外键:在ZulipAccounts表中
|
||||
* - 级联:不设置级联删除,保证数据安全
|
||||
*
|
||||
* 业务规则:
|
||||
* - 每个游戏用户最多关联一个Zulip账号
|
||||
* - 支持延迟加载,提高查询性能
|
||||
* - 可选关联,不是所有用户都有Zulip账号
|
||||
*
|
||||
* 使用场景:
|
||||
* - 游戏内聊天功能集成
|
||||
* - 跨平台消息同步
|
||||
* - 用户身份验证和权限管理
|
||||
*/
|
||||
@OneToOne(() => ZulipAccounts, zulipAccount => zulipAccount.gameUser)
|
||||
zulipAccount?: ZulipAccounts;
|
||||
}
|
||||
185
src/core/db/zulip_accounts/zulip_accounts.entity.ts
Normal file
185
src/core/db/zulip_accounts/zulip_accounts.entity.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* Zulip账号关联实体
|
||||
*
|
||||
* 功能描述:
|
||||
* - 存储游戏用户与Zulip账号的关联关系
|
||||
* - 管理Zulip账号的基本信息和状态
|
||||
* - 提供账号验证和同步功能
|
||||
*
|
||||
* 关联关系:
|
||||
* - 与Users表建立一对一关系
|
||||
* - 存储Zulip用户ID、邮箱、API Key等信息
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.0.0
|
||||
* @since 2025-01-05
|
||||
*/
|
||||
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToOne, JoinColumn, Index } from 'typeorm';
|
||||
import { Users } from '../users/users.entity';
|
||||
|
||||
@Entity('zulip_accounts')
|
||||
@Index(['zulip_user_id'], { unique: true })
|
||||
@Index(['zulip_email'], { unique: true })
|
||||
export class ZulipAccounts {
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@PrimaryGeneratedColumn('increment', { type: 'bigint' })
|
||||
id: bigint;
|
||||
|
||||
/**
|
||||
* 关联的游戏用户ID
|
||||
*/
|
||||
@Column({ type: 'bigint', name: 'game_user_id', comment: '关联的游戏用户ID' })
|
||||
gameUserId: bigint;
|
||||
|
||||
/**
|
||||
* Zulip用户ID
|
||||
*/
|
||||
@Column({ type: 'int', name: 'zulip_user_id', comment: 'Zulip服务器上的用户ID' })
|
||||
zulipUserId: number;
|
||||
|
||||
/**
|
||||
* Zulip用户邮箱
|
||||
*/
|
||||
@Column({ type: 'varchar', length: 255, name: 'zulip_email', comment: 'Zulip账号邮箱地址' })
|
||||
zulipEmail: string;
|
||||
|
||||
/**
|
||||
* Zulip用户全名
|
||||
*/
|
||||
@Column({ type: 'varchar', length: 100, name: 'zulip_full_name', comment: 'Zulip账号全名' })
|
||||
zulipFullName: string;
|
||||
|
||||
/**
|
||||
* Zulip API Key(加密存储)
|
||||
*/
|
||||
@Column({ type: 'text', name: 'zulip_api_key_encrypted', comment: '加密存储的Zulip API Key' })
|
||||
zulipApiKeyEncrypted: string;
|
||||
|
||||
/**
|
||||
* 账号状态
|
||||
* - active: 正常激活状态
|
||||
* - inactive: 未激活状态
|
||||
* - suspended: 暂停状态
|
||||
* - error: 错误状态
|
||||
*/
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: ['active', 'inactive', 'suspended', 'error'],
|
||||
default: 'active',
|
||||
comment: '账号状态:active-正常,inactive-未激活,suspended-暂停,error-错误'
|
||||
})
|
||||
status: 'active' | 'inactive' | 'suspended' | 'error';
|
||||
|
||||
/**
|
||||
* 最后验证时间
|
||||
*/
|
||||
@Column({ type: 'timestamp', name: 'last_verified_at', nullable: true, comment: '最后一次验证Zulip账号的时间' })
|
||||
lastVerifiedAt: Date | null;
|
||||
|
||||
/**
|
||||
* 最后同步时间
|
||||
*/
|
||||
@Column({ type: 'timestamp', name: 'last_synced_at', nullable: true, comment: '最后一次同步数据的时间' })
|
||||
lastSyncedAt: Date | null;
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
*/
|
||||
@Column({ type: 'text', name: 'error_message', nullable: true, comment: '最后一次操作的错误信息' })
|
||||
errorMessage: string | null;
|
||||
|
||||
/**
|
||||
* 重试次数
|
||||
*/
|
||||
@Column({ type: 'int', name: 'retry_count', default: 0, comment: '创建或同步失败的重试次数' })
|
||||
retryCount: number;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@CreateDateColumn({ name: 'created_at', comment: '记录创建时间' })
|
||||
createdAt: Date;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@UpdateDateColumn({ name: 'updated_at', comment: '记录最后更新时间' })
|
||||
updatedAt: Date;
|
||||
|
||||
/**
|
||||
* 关联的游戏用户
|
||||
*/
|
||||
@OneToOne(() => Users, user => user.zulipAccount)
|
||||
@JoinColumn({ name: 'game_user_id' })
|
||||
gameUser: Users;
|
||||
|
||||
/**
|
||||
* 检查账号是否处于正常状态
|
||||
*
|
||||
* @returns boolean 是否为正常状态
|
||||
*/
|
||||
isActive(): boolean {
|
||||
return this.status === 'active';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查账号是否需要重新验证
|
||||
*
|
||||
* @param maxAge 最大验证间隔(毫秒),默认24小时
|
||||
* @returns boolean 是否需要重新验证
|
||||
*/
|
||||
needsVerification(maxAge: number = 24 * 60 * 60 * 1000): boolean {
|
||||
if (!this.lastVerifiedAt) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const timeDiff = now.getTime() - this.lastVerifiedAt.getTime();
|
||||
return timeDiff > maxAge;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新验证时间
|
||||
*/
|
||||
updateVerificationTime(): void {
|
||||
this.lastVerifiedAt = new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新同步时间
|
||||
*/
|
||||
updateSyncTime(): void {
|
||||
this.lastSyncedAt = new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置错误状态
|
||||
*
|
||||
* @param errorMessage 错误信息
|
||||
*/
|
||||
setError(errorMessage: string): void {
|
||||
this.status = 'error';
|
||||
this.errorMessage = errorMessage;
|
||||
this.retryCount += 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除错误状态
|
||||
*/
|
||||
clearError(): void {
|
||||
if (this.status === 'error') {
|
||||
this.status = 'active';
|
||||
this.errorMessage = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置重试计数
|
||||
*/
|
||||
resetRetryCount(): void {
|
||||
this.retryCount = 0;
|
||||
}
|
||||
}
|
||||
81
src/core/db/zulip_accounts/zulip_accounts.module.ts
Normal file
81
src/core/db/zulip_accounts/zulip_accounts.module.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Zulip账号关联数据模块
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供Zulip账号关联数据的访问接口
|
||||
* - 封装TypeORM实体和Repository
|
||||
* - 为业务层提供数据访问服务
|
||||
* - 支持数据库和内存模式的动态切换
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.0.0
|
||||
* @since 2025-01-05
|
||||
*/
|
||||
|
||||
import { Module, DynamicModule, Global } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ZulipAccounts } from './zulip_accounts.entity';
|
||||
import { ZulipAccountsRepository } from './zulip_accounts.repository';
|
||||
import { ZulipAccountsMemoryRepository } from './zulip_accounts_memory.repository';
|
||||
|
||||
/**
|
||||
* 检查数据库配置是否完整
|
||||
*
|
||||
* @returns 是否配置了数据库
|
||||
*/
|
||||
function isDatabaseConfigured(): boolean {
|
||||
const requiredEnvVars = ['DB_HOST', 'DB_PORT', 'DB_USERNAME', 'DB_PASSWORD', 'DB_NAME'];
|
||||
return requiredEnvVars.every(varName => process.env[varName]);
|
||||
}
|
||||
|
||||
@Global()
|
||||
@Module({})
|
||||
export class ZulipAccountsModule {
|
||||
/**
|
||||
* 创建数据库模式的Zulip账号模块
|
||||
*
|
||||
* @returns 配置了TypeORM的动态模块
|
||||
*/
|
||||
static forDatabase(): DynamicModule {
|
||||
return {
|
||||
module: ZulipAccountsModule,
|
||||
imports: [TypeOrmModule.forFeature([ZulipAccounts])],
|
||||
providers: [
|
||||
{
|
||||
provide: 'ZulipAccountsRepository',
|
||||
useClass: ZulipAccountsRepository,
|
||||
},
|
||||
],
|
||||
exports: ['ZulipAccountsRepository', TypeOrmModule],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建内存模式的Zulip账号模块
|
||||
*
|
||||
* @returns 配置了内存存储的动态模块
|
||||
*/
|
||||
static forMemory(): DynamicModule {
|
||||
return {
|
||||
module: ZulipAccountsModule,
|
||||
providers: [
|
||||
{
|
||||
provide: 'ZulipAccountsRepository',
|
||||
useClass: ZulipAccountsMemoryRepository,
|
||||
},
|
||||
],
|
||||
exports: ['ZulipAccountsRepository'],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据环境自动选择模式
|
||||
*
|
||||
* @returns 动态模块
|
||||
*/
|
||||
static forRoot(): DynamicModule {
|
||||
return isDatabaseConfigured()
|
||||
? ZulipAccountsModule.forDatabase()
|
||||
: ZulipAccountsModule.forMemory();
|
||||
}
|
||||
}
|
||||
323
src/core/db/zulip_accounts/zulip_accounts.repository.ts
Normal file
323
src/core/db/zulip_accounts/zulip_accounts.repository.ts
Normal file
@@ -0,0 +1,323 @@
|
||||
/**
|
||||
* Zulip账号关联数据访问层
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供Zulip账号关联数据的CRUD操作
|
||||
* - 封装复杂查询逻辑和数据库交互
|
||||
* - 实现数据访问层的业务逻辑抽象
|
||||
*
|
||||
* 主要功能:
|
||||
* - 账号关联的创建、查询、更新、删除
|
||||
* - 支持按游戏用户ID、Zulip用户ID、邮箱查询
|
||||
* - 提供账号状态管理和批量操作
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.0.0
|
||||
* @since 2025-01-05
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, FindOptionsWhere } from 'typeorm';
|
||||
import { ZulipAccounts } from './zulip_accounts.entity';
|
||||
|
||||
/**
|
||||
* 创建Zulip账号关联的数据传输对象
|
||||
*/
|
||||
export interface CreateZulipAccountDto {
|
||||
gameUserId: bigint;
|
||||
zulipUserId: number;
|
||||
zulipEmail: string;
|
||||
zulipFullName: string;
|
||||
zulipApiKeyEncrypted: string;
|
||||
status?: 'active' | 'inactive' | 'suspended' | 'error';
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新Zulip账号关联的数据传输对象
|
||||
*/
|
||||
export interface UpdateZulipAccountDto {
|
||||
zulipFullName?: string;
|
||||
zulipApiKeyEncrypted?: string;
|
||||
status?: 'active' | 'inactive' | 'suspended' | 'error';
|
||||
lastVerifiedAt?: Date;
|
||||
lastSyncedAt?: Date;
|
||||
errorMessage?: string;
|
||||
retryCount?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zulip账号查询条件
|
||||
*/
|
||||
export interface ZulipAccountQueryOptions {
|
||||
gameUserId?: bigint;
|
||||
zulipUserId?: number;
|
||||
zulipEmail?: string;
|
||||
status?: 'active' | 'inactive' | 'suspended' | 'error';
|
||||
includeGameUser?: boolean;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ZulipAccountsRepository {
|
||||
constructor(
|
||||
@InjectRepository(ZulipAccounts)
|
||||
private readonly repository: Repository<ZulipAccounts>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 创建新的Zulip账号关联
|
||||
*
|
||||
* @param createDto 创建数据
|
||||
* @returns Promise<ZulipAccounts> 创建的关联记录
|
||||
*/
|
||||
async create(createDto: CreateZulipAccountDto): Promise<ZulipAccounts> {
|
||||
const zulipAccount = this.repository.create(createDto);
|
||||
return await this.repository.save(zulipAccount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据游戏用户ID查找Zulip账号关联
|
||||
*
|
||||
* @param gameUserId 游戏用户ID
|
||||
* @param includeGameUser 是否包含游戏用户信息
|
||||
* @returns Promise<ZulipAccounts | null> 关联记录或null
|
||||
*/
|
||||
async findByGameUserId(gameUserId: bigint, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
|
||||
const relations = includeGameUser ? ['gameUser'] : [];
|
||||
|
||||
return await this.repository.findOne({
|
||||
where: { gameUserId },
|
||||
relations,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据Zulip用户ID查找账号关联
|
||||
*
|
||||
* @param zulipUserId Zulip用户ID
|
||||
* @param includeGameUser 是否包含游戏用户信息
|
||||
* @returns Promise<ZulipAccounts | null> 关联记录或null
|
||||
*/
|
||||
async findByZulipUserId(zulipUserId: number, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
|
||||
const relations = includeGameUser ? ['gameUser'] : [];
|
||||
|
||||
return await this.repository.findOne({
|
||||
where: { zulipUserId },
|
||||
relations,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据Zulip邮箱查找账号关联
|
||||
*
|
||||
* @param zulipEmail Zulip邮箱
|
||||
* @param includeGameUser 是否包含游戏用户信息
|
||||
* @returns Promise<ZulipAccounts | null> 关联记录或null
|
||||
*/
|
||||
async findByZulipEmail(zulipEmail: string, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
|
||||
const relations = includeGameUser ? ['gameUser'] : [];
|
||||
|
||||
return await this.repository.findOne({
|
||||
where: { zulipEmail },
|
||||
relations,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查找Zulip账号关联
|
||||
*
|
||||
* @param id 关联记录ID
|
||||
* @param includeGameUser 是否包含游戏用户信息
|
||||
* @returns Promise<ZulipAccounts | null> 关联记录或null
|
||||
*/
|
||||
async findById(id: bigint, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
|
||||
const relations = includeGameUser ? ['gameUser'] : [];
|
||||
|
||||
return await this.repository.findOne({
|
||||
where: { id },
|
||||
relations,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新Zulip账号关联
|
||||
*
|
||||
* @param id 关联记录ID
|
||||
* @param updateDto 更新数据
|
||||
* @returns Promise<ZulipAccounts | null> 更新后的记录或null
|
||||
*/
|
||||
async update(id: bigint, updateDto: UpdateZulipAccountDto): Promise<ZulipAccounts | null> {
|
||||
await this.repository.update({ id }, updateDto);
|
||||
return await this.findById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据游戏用户ID更新Zulip账号关联
|
||||
*
|
||||
* @param gameUserId 游戏用户ID
|
||||
* @param updateDto 更新数据
|
||||
* @returns Promise<ZulipAccounts | null> 更新后的记录或null
|
||||
*/
|
||||
async updateByGameUserId(gameUserId: bigint, updateDto: UpdateZulipAccountDto): Promise<ZulipAccounts | null> {
|
||||
await this.repository.update({ gameUserId }, updateDto);
|
||||
return await this.findByGameUserId(gameUserId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除Zulip账号关联
|
||||
*
|
||||
* @param id 关联记录ID
|
||||
* @returns Promise<boolean> 是否删除成功
|
||||
*/
|
||||
async delete(id: bigint): Promise<boolean> {
|
||||
const result = await this.repository.delete({ id });
|
||||
return result.affected > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据游戏用户ID删除Zulip账号关联
|
||||
*
|
||||
* @param gameUserId 游戏用户ID
|
||||
* @returns Promise<boolean> 是否删除成功
|
||||
*/
|
||||
async deleteByGameUserId(gameUserId: bigint): Promise<boolean> {
|
||||
const result = await this.repository.delete({ gameUserId });
|
||||
return result.affected > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询多个Zulip账号关联
|
||||
*
|
||||
* @param options 查询选项
|
||||
* @returns Promise<ZulipAccounts[]> 关联记录列表
|
||||
*/
|
||||
async findMany(options: ZulipAccountQueryOptions = {}): Promise<ZulipAccounts[]> {
|
||||
const { includeGameUser, ...whereOptions } = options;
|
||||
const relations = includeGameUser ? ['gameUser'] : [];
|
||||
|
||||
// 构建查询条件
|
||||
const where: FindOptionsWhere<ZulipAccounts> = {};
|
||||
if (whereOptions.gameUserId) where.gameUserId = whereOptions.gameUserId;
|
||||
if (whereOptions.zulipUserId) where.zulipUserId = whereOptions.zulipUserId;
|
||||
if (whereOptions.zulipEmail) where.zulipEmail = whereOptions.zulipEmail;
|
||||
if (whereOptions.status) where.status = whereOptions.status;
|
||||
|
||||
return await this.repository.find({
|
||||
where,
|
||||
relations,
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取需要验证的账号列表
|
||||
*
|
||||
* @param maxAge 最大验证间隔(毫秒),默认24小时
|
||||
* @returns Promise<ZulipAccounts[]> 需要验证的账号列表
|
||||
*/
|
||||
async findAccountsNeedingVerification(maxAge: number = 24 * 60 * 60 * 1000): Promise<ZulipAccounts[]> {
|
||||
const cutoffTime = new Date(Date.now() - maxAge);
|
||||
|
||||
return await this.repository
|
||||
.createQueryBuilder('zulip_accounts')
|
||||
.where('zulip_accounts.status = :status', { status: 'active' })
|
||||
.andWhere(
|
||||
'(zulip_accounts.last_verified_at IS NULL OR zulip_accounts.last_verified_at < :cutoffTime)',
|
||||
{ cutoffTime }
|
||||
)
|
||||
.orderBy('zulip_accounts.last_verified_at', 'ASC', 'NULLS FIRST')
|
||||
.getMany();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取错误状态的账号列表
|
||||
*
|
||||
* @param maxRetryCount 最大重试次数,默认3次
|
||||
* @returns Promise<ZulipAccounts[]> 错误状态的账号列表
|
||||
*/
|
||||
async findErrorAccounts(maxRetryCount: number = 3): Promise<ZulipAccounts[]> {
|
||||
return await this.repository.find({
|
||||
where: { status: 'error' },
|
||||
order: { updatedAt: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新账号状态
|
||||
*
|
||||
* @param ids 账号ID列表
|
||||
* @param status 新状态
|
||||
* @returns Promise<number> 更新的记录数
|
||||
*/
|
||||
async batchUpdateStatus(ids: bigint[], status: 'active' | 'inactive' | 'suspended' | 'error'): Promise<number> {
|
||||
const result = await this.repository
|
||||
.createQueryBuilder()
|
||||
.update(ZulipAccounts)
|
||||
.set({ status })
|
||||
.whereInIds(ids)
|
||||
.execute();
|
||||
|
||||
return result.affected || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计各状态的账号数量
|
||||
*
|
||||
* @returns Promise<Record<string, number>> 状态统计
|
||||
*/
|
||||
async getStatusStatistics(): Promise<Record<string, number>> {
|
||||
const result = await this.repository
|
||||
.createQueryBuilder('zulip_accounts')
|
||||
.select('zulip_accounts.status', 'status')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.groupBy('zulip_accounts.status')
|
||||
.getRawMany();
|
||||
|
||||
const statistics: Record<string, number> = {};
|
||||
result.forEach(row => {
|
||||
statistics[row.status] = parseInt(row.count, 10);
|
||||
});
|
||||
|
||||
return statistics;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查邮箱是否已存在
|
||||
*
|
||||
* @param zulipEmail Zulip邮箱
|
||||
* @param excludeId 排除的记录ID(用于更新时检查)
|
||||
* @returns Promise<boolean> 是否已存在
|
||||
*/
|
||||
async existsByEmail(zulipEmail: string, excludeId?: bigint): Promise<boolean> {
|
||||
const queryBuilder = this.repository
|
||||
.createQueryBuilder('zulip_accounts')
|
||||
.where('zulip_accounts.zulip_email = :zulipEmail', { zulipEmail });
|
||||
|
||||
if (excludeId) {
|
||||
queryBuilder.andWhere('zulip_accounts.id != :excludeId', { excludeId });
|
||||
}
|
||||
|
||||
const count = await queryBuilder.getCount();
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查Zulip用户ID是否已存在
|
||||
*
|
||||
* @param zulipUserId Zulip用户ID
|
||||
* @param excludeId 排除的记录ID(用于更新时检查)
|
||||
* @returns Promise<boolean> 是否已存在
|
||||
*/
|
||||
async existsByZulipUserId(zulipUserId: number, excludeId?: bigint): Promise<boolean> {
|
||||
const queryBuilder = this.repository
|
||||
.createQueryBuilder('zulip_accounts')
|
||||
.where('zulip_accounts.zulip_user_id = :zulipUserId', { zulipUserId });
|
||||
|
||||
if (excludeId) {
|
||||
queryBuilder.andWhere('zulip_accounts.id != :excludeId', { excludeId });
|
||||
}
|
||||
|
||||
const count = await queryBuilder.getCount();
|
||||
return count > 0;
|
||||
}
|
||||
}
|
||||
299
src/core/db/zulip_accounts/zulip_accounts_memory.repository.ts
Normal file
299
src/core/db/zulip_accounts/zulip_accounts_memory.repository.ts
Normal file
@@ -0,0 +1,299 @@
|
||||
/**
|
||||
* Zulip账号关联内存数据访问层
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供Zulip账号关联数据的内存存储实现
|
||||
* - 用于开发和测试环境
|
||||
* - 实现与数据库版本相同的接口
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.0.0
|
||||
* @since 2025-01-05
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ZulipAccounts } from './zulip_accounts.entity';
|
||||
import {
|
||||
CreateZulipAccountDto,
|
||||
UpdateZulipAccountDto,
|
||||
ZulipAccountQueryOptions,
|
||||
} from './zulip_accounts.repository';
|
||||
|
||||
@Injectable()
|
||||
export class ZulipAccountsMemoryRepository {
|
||||
private accounts: Map<bigint, ZulipAccounts> = new Map();
|
||||
private currentId: bigint = BigInt(1);
|
||||
|
||||
/**
|
||||
* 创建新的Zulip账号关联
|
||||
*
|
||||
* @param createDto 创建数据
|
||||
* @returns Promise<ZulipAccounts> 创建的关联记录
|
||||
*/
|
||||
async create(createDto: CreateZulipAccountDto): Promise<ZulipAccounts> {
|
||||
const account = new ZulipAccounts();
|
||||
account.id = this.currentId++;
|
||||
account.gameUserId = createDto.gameUserId;
|
||||
account.zulipUserId = createDto.zulipUserId;
|
||||
account.zulipEmail = createDto.zulipEmail;
|
||||
account.zulipFullName = createDto.zulipFullName;
|
||||
account.zulipApiKeyEncrypted = createDto.zulipApiKeyEncrypted;
|
||||
account.status = createDto.status || 'active';
|
||||
account.createdAt = new Date();
|
||||
account.updatedAt = new Date();
|
||||
|
||||
this.accounts.set(account.id, account);
|
||||
return account;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据游戏用户ID查找Zulip账号关联
|
||||
*
|
||||
* @param gameUserId 游戏用户ID
|
||||
* @param includeGameUser 是否包含游戏用户信息(内存模式忽略)
|
||||
* @returns Promise<ZulipAccounts | null> 关联记录或null
|
||||
*/
|
||||
async findByGameUserId(gameUserId: bigint, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
|
||||
for (const account of this.accounts.values()) {
|
||||
if (account.gameUserId === gameUserId) {
|
||||
return account;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据Zulip用户ID查找账号关联
|
||||
*
|
||||
* @param zulipUserId Zulip用户ID
|
||||
* @param includeGameUser 是否包含游戏用户信息(内存模式忽略)
|
||||
* @returns Promise<ZulipAccounts | null> 关联记录或null
|
||||
*/
|
||||
async findByZulipUserId(zulipUserId: number, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
|
||||
for (const account of this.accounts.values()) {
|
||||
if (account.zulipUserId === zulipUserId) {
|
||||
return account;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据Zulip邮箱查找账号关联
|
||||
*
|
||||
* @param zulipEmail Zulip邮箱
|
||||
* @param includeGameUser 是否包含游戏用户信息(内存模式忽略)
|
||||
* @returns Promise<ZulipAccounts | null> 关联记录或null
|
||||
*/
|
||||
async findByZulipEmail(zulipEmail: string, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
|
||||
for (const account of this.accounts.values()) {
|
||||
if (account.zulipEmail === zulipEmail) {
|
||||
return account;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查找Zulip账号关联
|
||||
*
|
||||
* @param id 关联记录ID
|
||||
* @param includeGameUser 是否包含游戏用户信息(内存模式忽略)
|
||||
* @returns Promise<ZulipAccounts | null> 关联记录或null
|
||||
*/
|
||||
async findById(id: bigint, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
|
||||
return this.accounts.get(id) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新Zulip账号关联
|
||||
*
|
||||
* @param id 关联记录ID
|
||||
* @param updateDto 更新数据
|
||||
* @returns Promise<ZulipAccounts | null> 更新后的记录或null
|
||||
*/
|
||||
async update(id: bigint, updateDto: UpdateZulipAccountDto): Promise<ZulipAccounts | null> {
|
||||
const account = this.accounts.get(id);
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Object.assign(account, updateDto);
|
||||
account.updatedAt = new Date();
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据游戏用户ID更新Zulip账号关联
|
||||
*
|
||||
* @param gameUserId 游戏用户ID
|
||||
* @param updateDto 更新数据
|
||||
* @returns Promise<ZulipAccounts | null> 更新后的记录或null
|
||||
*/
|
||||
async updateByGameUserId(gameUserId: bigint, updateDto: UpdateZulipAccountDto): Promise<ZulipAccounts | null> {
|
||||
const account = await this.findByGameUserId(gameUserId);
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Object.assign(account, updateDto);
|
||||
account.updatedAt = new Date();
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除Zulip账号关联
|
||||
*
|
||||
* @param id 关联记录ID
|
||||
* @returns Promise<boolean> 是否删除成功
|
||||
*/
|
||||
async delete(id: bigint): Promise<boolean> {
|
||||
return this.accounts.delete(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据游戏用户ID删除Zulip账号关联
|
||||
*
|
||||
* @param gameUserId 游戏用户ID
|
||||
* @returns Promise<boolean> 是否删除成功
|
||||
*/
|
||||
async deleteByGameUserId(gameUserId: bigint): Promise<boolean> {
|
||||
for (const [id, account] of this.accounts.entries()) {
|
||||
if (account.gameUserId === gameUserId) {
|
||||
return this.accounts.delete(id);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询多个Zulip账号关联
|
||||
*
|
||||
* @param options 查询选项
|
||||
* @returns Promise<ZulipAccounts[]> 关联记录列表
|
||||
*/
|
||||
async findMany(options: ZulipAccountQueryOptions = {}): Promise<ZulipAccounts[]> {
|
||||
let results = Array.from(this.accounts.values());
|
||||
|
||||
if (options.gameUserId) {
|
||||
results = results.filter(a => a.gameUserId === options.gameUserId);
|
||||
}
|
||||
if (options.zulipUserId) {
|
||||
results = results.filter(a => a.zulipUserId === options.zulipUserId);
|
||||
}
|
||||
if (options.zulipEmail) {
|
||||
results = results.filter(a => a.zulipEmail === options.zulipEmail);
|
||||
}
|
||||
if (options.status) {
|
||||
results = results.filter(a => a.status === options.status);
|
||||
}
|
||||
|
||||
// 按创建时间降序排序
|
||||
results.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取需要验证的账号列表
|
||||
*
|
||||
* @param maxAge 最大验证间隔(毫秒),默认24小时
|
||||
* @returns Promise<ZulipAccounts[]> 需要验证的账号列表
|
||||
*/
|
||||
async findAccountsNeedingVerification(maxAge: number = 24 * 60 * 60 * 1000): Promise<ZulipAccounts[]> {
|
||||
const cutoffTime = new Date(Date.now() - maxAge);
|
||||
|
||||
return Array.from(this.accounts.values())
|
||||
.filter(account =>
|
||||
account.status === 'active' &&
|
||||
(!account.lastVerifiedAt || account.lastVerifiedAt < cutoffTime)
|
||||
)
|
||||
.sort((a, b) => {
|
||||
if (!a.lastVerifiedAt) return -1;
|
||||
if (!b.lastVerifiedAt) return 1;
|
||||
return a.lastVerifiedAt.getTime() - b.lastVerifiedAt.getTime();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取错误状态的账号列表
|
||||
*
|
||||
* @param maxRetryCount 最大重试次数(内存模式忽略)
|
||||
* @returns Promise<ZulipAccounts[]> 错误状态的账号列表
|
||||
*/
|
||||
async findErrorAccounts(maxRetryCount: number = 3): Promise<ZulipAccounts[]> {
|
||||
return Array.from(this.accounts.values())
|
||||
.filter(account => account.status === 'error')
|
||||
.sort((a, b) => a.updatedAt.getTime() - b.updatedAt.getTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新账号状态
|
||||
*
|
||||
* @param ids 账号ID列表
|
||||
* @param status 新状态
|
||||
* @returns Promise<number> 更新的记录数
|
||||
*/
|
||||
async batchUpdateStatus(ids: bigint[], status: 'active' | 'inactive' | 'suspended' | 'error'): Promise<number> {
|
||||
let count = 0;
|
||||
for (const id of ids) {
|
||||
const account = this.accounts.get(id);
|
||||
if (account) {
|
||||
account.status = status;
|
||||
account.updatedAt = new Date();
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计各状态的账号数量
|
||||
*
|
||||
* @returns Promise<Record<string, number>> 状态统计
|
||||
*/
|
||||
async getStatusStatistics(): Promise<Record<string, number>> {
|
||||
const statistics: Record<string, number> = {};
|
||||
|
||||
for (const account of this.accounts.values()) {
|
||||
const status = account.status;
|
||||
statistics[status] = (statistics[status] || 0) + 1;
|
||||
}
|
||||
|
||||
return statistics;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查邮箱是否已存在
|
||||
*
|
||||
* @param zulipEmail Zulip邮箱
|
||||
* @param excludeId 排除的记录ID(用于更新时检查)
|
||||
* @returns Promise<boolean> 是否已存在
|
||||
*/
|
||||
async existsByEmail(zulipEmail: string, excludeId?: bigint): Promise<boolean> {
|
||||
for (const [id, account] of this.accounts.entries()) {
|
||||
if (account.zulipEmail === zulipEmail && (!excludeId || id !== excludeId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查Zulip用户ID是否已存在
|
||||
*
|
||||
* @param zulipUserId Zulip用户ID
|
||||
* @param excludeId 排除的记录ID(用于更新时检查)
|
||||
* @returns Promise<boolean> 是否已存在
|
||||
*/
|
||||
async existsByZulipUserId(zulipUserId: number, excludeId?: bigint): Promise<boolean> {
|
||||
for (const [id, account] of this.accounts.entries()) {
|
||||
if (account.zulipUserId === zulipUserId && (!excludeId || id !== excludeId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -826,4 +826,36 @@ export class LoginCoreService {
|
||||
VerificationCodeType.EMAIL_VERIFICATION
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
*
|
||||
* 功能描述:
|
||||
* 删除指定的用户记录,用于注册失败时的回滚操作
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证用户是否存在
|
||||
* 2. 执行用户删除操作
|
||||
* 3. 返回删除结果
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @returns Promise<boolean> 是否删除成功
|
||||
* @throws NotFoundException 用户不存在时
|
||||
*/
|
||||
async deleteUser(userId: bigint): Promise<boolean> {
|
||||
// 1. 验证用户是否存在
|
||||
const user = await this.usersService.findOne(userId);
|
||||
if (!user) {
|
||||
throw new NotFoundException('用户不存在');
|
||||
}
|
||||
|
||||
// 2. 执行删除操作
|
||||
try {
|
||||
await this.usersService.remove(userId);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`删除用户失败: ${userId}`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 安全功能模块导出
|
||||
* 核心安全模块导出
|
||||
*
|
||||
* 功能概述:
|
||||
* - 频率限制和防护机制
|
||||
@@ -10,14 +10,14 @@
|
||||
*/
|
||||
|
||||
// 模块
|
||||
export * from './security.module';
|
||||
export * from './security_core.module';
|
||||
|
||||
// 守卫
|
||||
export * from './guards/throttle.guard';
|
||||
|
||||
// 中间件
|
||||
export * from './middleware/maintenance.middleware';
|
||||
export * from './middleware/content-type.middleware';
|
||||
export * from './middleware/content_type.middleware';
|
||||
|
||||
// 拦截器
|
||||
export * from './interceptors/timeout.interceptor';
|
||||
@@ -1,11 +1,11 @@
|
||||
/**
|
||||
* 安全功能模块
|
||||
* 核心安全模块
|
||||
*
|
||||
* 功能描述:
|
||||
* - 整合所有安全相关功能
|
||||
* - 提供系统级安全防护功能
|
||||
* - 频率限制和请求超时控制
|
||||
* - 维护模式和内容类型验证
|
||||
* - 系统安全防护机制
|
||||
* - 全局安全中间件和守卫
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
@@ -34,4 +34,4 @@ import { TimeoutInterceptor } from './interceptors/timeout.interceptor';
|
||||
],
|
||||
exports: [ThrottleGuard, TimeoutInterceptor],
|
||||
})
|
||||
export class SecurityModule {}
|
||||
export class SecurityCoreModule {}
|
||||
26
src/core/zulip/index.ts
Normal file
26
src/core/zulip/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Zulip核心服务模块导出
|
||||
*
|
||||
* 功能描述:
|
||||
* - 统一导出Zulip核心服务的接口和类型
|
||||
* - 为业务层提供清晰的导入路径
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-31
|
||||
*/
|
||||
|
||||
// 导出核心服务接口
|
||||
export * from './interfaces/zulip-core.interfaces';
|
||||
|
||||
// 导出核心服务模块
|
||||
export { ZulipCoreModule } from './zulip-core.module';
|
||||
|
||||
// 导出具体实现类(供内部使用)
|
||||
export { ZulipClientService } from './services/zulip_client.service';
|
||||
export { ZulipClientPoolService } from './services/zulip_client_pool.service';
|
||||
export { ConfigManagerService } from './services/config_manager.service';
|
||||
export { ApiKeySecurityService } from './services/api_key_security.service';
|
||||
export { ErrorHandlerService } from './services/error_handler.service';
|
||||
export { MonitoringService } from './services/monitoring.service';
|
||||
export { StreamInitializerService } from './services/stream_initializer.service';
|
||||
294
src/core/zulip/interfaces/zulip-core.interfaces.ts
Normal file
294
src/core/zulip/interfaces/zulip-core.interfaces.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* Zulip核心服务接口定义
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义Zulip核心服务的抽象接口
|
||||
* - 分离业务逻辑与技术实现
|
||||
* - 支持依赖注入和接口切换
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-31
|
||||
*/
|
||||
|
||||
/**
|
||||
* Zulip客户端配置接口
|
||||
*/
|
||||
export interface ZulipClientConfig {
|
||||
username: string;
|
||||
apiKey: string;
|
||||
realm: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zulip客户端实例接口
|
||||
*/
|
||||
export interface ZulipClientInstance {
|
||||
userId: string;
|
||||
config: ZulipClientConfig;
|
||||
client: any;
|
||||
queueId?: string;
|
||||
lastEventId: number;
|
||||
createdAt: Date;
|
||||
lastActivity: Date;
|
||||
isValid: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息结果接口
|
||||
*/
|
||||
export interface SendMessageResult {
|
||||
success: boolean;
|
||||
messageId?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 事件队列注册结果接口
|
||||
*/
|
||||
export interface RegisterQueueResult {
|
||||
success: boolean;
|
||||
queueId?: string;
|
||||
lastEventId?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取事件结果接口
|
||||
*/
|
||||
export interface GetEventsResult {
|
||||
success: boolean;
|
||||
events?: any[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端池统计信息接口
|
||||
*/
|
||||
export interface PoolStats {
|
||||
totalClients: number;
|
||||
activeClients: number;
|
||||
clientsWithQueues: number;
|
||||
clientIds: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Zulip客户端核心服务接口
|
||||
*
|
||||
* 职责:
|
||||
* - 封装Zulip REST API调用
|
||||
* - 处理API Key验证和错误处理
|
||||
* - 提供消息发送、事件队列管理等核心功能
|
||||
*/
|
||||
export interface IZulipClientService {
|
||||
/**
|
||||
* 创建并初始化Zulip客户端
|
||||
*/
|
||||
createClient(userId: string, config: ZulipClientConfig): Promise<ZulipClientInstance>;
|
||||
|
||||
/**
|
||||
* 验证API Key有效性
|
||||
*/
|
||||
validateApiKey(clientInstance: ZulipClientInstance): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* 发送消息到指定Stream/Topic
|
||||
*/
|
||||
sendMessage(
|
||||
clientInstance: ZulipClientInstance,
|
||||
stream: string,
|
||||
topic: string,
|
||||
content: string,
|
||||
): Promise<SendMessageResult>;
|
||||
|
||||
/**
|
||||
* 注册事件队列
|
||||
*/
|
||||
registerQueue(
|
||||
clientInstance: ZulipClientInstance,
|
||||
eventTypes?: string[],
|
||||
): Promise<RegisterQueueResult>;
|
||||
|
||||
/**
|
||||
* 注销事件队列
|
||||
*/
|
||||
deregisterQueue(clientInstance: ZulipClientInstance): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* 获取事件队列中的事件
|
||||
*/
|
||||
getEvents(
|
||||
clientInstance: ZulipClientInstance,
|
||||
dontBlock?: boolean,
|
||||
): Promise<GetEventsResult>;
|
||||
|
||||
/**
|
||||
* 销毁客户端实例
|
||||
*/
|
||||
destroyClient(clientInstance: ZulipClientInstance): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zulip客户端池服务接口
|
||||
*
|
||||
* 职责:
|
||||
* - 管理用户专用的Zulip客户端实例
|
||||
* - 维护客户端连接池和生命周期
|
||||
* - 处理客户端的创建、销毁和状态管理
|
||||
*/
|
||||
export interface IZulipClientPoolService {
|
||||
/**
|
||||
* 为用户创建专用Zulip客户端
|
||||
*/
|
||||
createUserClient(userId: string, config: ZulipClientConfig): Promise<ZulipClientInstance>;
|
||||
|
||||
/**
|
||||
* 获取用户的Zulip客户端
|
||||
*/
|
||||
getUserClient(userId: string): Promise<ZulipClientInstance | null>;
|
||||
|
||||
/**
|
||||
* 检查用户客户端是否存在
|
||||
*/
|
||||
hasUserClient(userId: string): boolean;
|
||||
|
||||
/**
|
||||
* 发送消息到指定Stream/Topic
|
||||
*/
|
||||
sendMessage(
|
||||
userId: string,
|
||||
stream: string,
|
||||
topic: string,
|
||||
content: string,
|
||||
): Promise<SendMessageResult>;
|
||||
|
||||
/**
|
||||
* 注册事件队列
|
||||
*/
|
||||
registerEventQueue(userId: string): Promise<RegisterQueueResult>;
|
||||
|
||||
/**
|
||||
* 注销事件队列
|
||||
*/
|
||||
deregisterEventQueue(userId: string): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* 销毁用户客户端
|
||||
*/
|
||||
destroyUserClient(userId: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* 获取客户端池统计信息
|
||||
*/
|
||||
getPoolStats(): PoolStats;
|
||||
|
||||
/**
|
||||
* 清理过期客户端
|
||||
*/
|
||||
cleanupIdleClients(maxIdleMinutes?: number): Promise<number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zulip配置管理服务接口
|
||||
*
|
||||
* 职责:
|
||||
* - 管理地图到Zulip Stream的映射配置
|
||||
* - 提供Zulip服务器连接配置
|
||||
* - 支持配置文件的热重载
|
||||
*/
|
||||
export interface IZulipConfigService {
|
||||
/**
|
||||
* 根据地图获取对应的Stream
|
||||
*/
|
||||
getStreamByMap(mapId: string): string | null;
|
||||
|
||||
/**
|
||||
* 根据Stream名称获取地图ID
|
||||
*/
|
||||
getMapIdByStream(streamName: string): string | null;
|
||||
|
||||
/**
|
||||
* 根据交互对象获取Topic
|
||||
*/
|
||||
getTopicByObject(mapId: string, objectId: string): string | null;
|
||||
|
||||
/**
|
||||
* 获取Zulip配置
|
||||
*/
|
||||
getZulipConfig(): any;
|
||||
|
||||
/**
|
||||
* 检查地图是否存在
|
||||
*/
|
||||
hasMap(mapId: string): boolean;
|
||||
|
||||
/**
|
||||
* 检查Stream是否存在
|
||||
*/
|
||||
hasStream(streamName: string): boolean;
|
||||
|
||||
/**
|
||||
* 获取所有地图ID列表
|
||||
*/
|
||||
getAllMapIds(): string[];
|
||||
|
||||
/**
|
||||
* 获取所有Stream名称列表
|
||||
*/
|
||||
getAllStreams(): string[];
|
||||
|
||||
/**
|
||||
* 热重载配置
|
||||
*/
|
||||
reloadConfig(): Promise<void>;
|
||||
|
||||
/**
|
||||
* 验证配置有效性
|
||||
*/
|
||||
validateConfig(): Promise<{ valid: boolean; errors: string[] }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zulip事件处理服务接口
|
||||
*
|
||||
* 职责:
|
||||
* - 处理从Zulip接收的事件队列消息
|
||||
* - 将Zulip消息转换为游戏协议格式
|
||||
* - 管理事件队列的生命周期
|
||||
*/
|
||||
export interface IZulipEventProcessorService {
|
||||
/**
|
||||
* 启动事件处理循环
|
||||
*/
|
||||
startEventProcessing(): Promise<void>;
|
||||
|
||||
/**
|
||||
* 停止事件处理循环
|
||||
*/
|
||||
stopEventProcessing(): Promise<void>;
|
||||
|
||||
/**
|
||||
* 注册事件队列
|
||||
*/
|
||||
registerEventQueue(queueId: string, userId: string, lastEventId?: number): Promise<void>;
|
||||
|
||||
/**
|
||||
* 注销事件队列
|
||||
*/
|
||||
unregisterEventQueue(queueId: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* 处理Zulip消息事件
|
||||
*/
|
||||
processMessageEvent(event: any, senderUserId: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* 设置消息分发器
|
||||
*/
|
||||
setMessageDistributor(distributor: any): void;
|
||||
|
||||
/**
|
||||
* 获取事件处理统计信息
|
||||
*/
|
||||
getProcessingStats(): any;
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
ApiKeySecurityService,
|
||||
SecurityEventType,
|
||||
SecuritySeverity,
|
||||
} from './api-key-security.service';
|
||||
} from './api_key_security.service';
|
||||
import { IRedisService } from '../../../core/redis/redis.interface';
|
||||
|
||||
describe('ApiKeySecurityService', () => {
|
||||
@@ -100,6 +100,28 @@ export interface GetApiKeyResult {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* API密钥安全服务类
|
||||
*
|
||||
* 职责:
|
||||
* - 管理Zulip API密钥的安全存储
|
||||
* - 提供API密钥的加密和解密功能
|
||||
* - 记录API密钥的访问日志
|
||||
* - 监控API密钥的使用情况和安全事件
|
||||
*
|
||||
* 主要方法:
|
||||
* - storeApiKey(): 安全存储加密的API密钥
|
||||
* - retrieveApiKey(): 检索并解密API密钥
|
||||
* - validateApiKey(): 验证API密钥的有效性
|
||||
* - logSecurityEvent(): 记录安全相关事件
|
||||
* - getAccessStats(): 获取API密钥访问统计
|
||||
*
|
||||
* 使用场景:
|
||||
* - 用户API密钥的安全存储
|
||||
* - API密钥访问时的解密操作
|
||||
* - 安全事件的监控和记录
|
||||
* - API密钥使用情况的统计分析
|
||||
*/
|
||||
@Injectable()
|
||||
export class ApiKeySecurityService {
|
||||
private readonly logger = new Logger(ApiKeySecurityService.name);
|
||||
@@ -12,8 +12,8 @@
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import * as fc from 'fast-check';
|
||||
import { ConfigManagerService, MapConfig, ZulipConfig } from './config-manager.service';
|
||||
import { AppLoggerService } from '../../../core/utils/logger/logger.service';
|
||||
import { ConfigManagerService, MapConfig, ZulipConfig } from './config_manager.service';
|
||||
import { AppLoggerService } from '../../utils/logger/logger.service';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
@@ -108,6 +108,28 @@ export interface InteractionObject extends InteractionObjectConfig {
|
||||
mapId: string; // 所属地图ID
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置管理服务类
|
||||
*
|
||||
* 职责:
|
||||
* - 管理地图到Zulip Stream的映射配置
|
||||
* - 提供Zulip服务器连接配置
|
||||
* - 支持配置文件的热重载
|
||||
* - 验证配置的完整性和有效性
|
||||
*
|
||||
* 主要方法:
|
||||
* - loadMapConfig(): 加载地图配置文件
|
||||
* - getStreamByMap(): 根据地图ID获取对应的Stream
|
||||
* - getZulipConfig(): 获取Zulip服务器配置
|
||||
* - validateConfig(): 验证配置文件格式
|
||||
* - enableConfigWatcher(): 启用配置文件监控
|
||||
*
|
||||
* 使用场景:
|
||||
* - 系统启动时加载配置
|
||||
* - 消息路由时查找Stream映射
|
||||
* - 配置文件变更时自动重载
|
||||
* - 配置验证和错误处理
|
||||
*/
|
||||
@Injectable()
|
||||
export class ConfigManagerService implements OnModuleDestroy {
|
||||
private mapConfigs: Map<string, MapConfig> = new Map();
|
||||
@@ -117,10 +139,39 @@ export class ConfigManagerService implements OnModuleDestroy {
|
||||
private configLoadTime: Date;
|
||||
private configWatcher: fs.FSWatcher | null = null;
|
||||
private isWatcherEnabled: boolean = false;
|
||||
private readonly CONFIG_DIR = path.join(process.cwd(), 'config', 'zulip');
|
||||
private readonly CONFIG_DIR = this.getConfigDir();
|
||||
private readonly MAP_CONFIG_FILE = 'map-config.json';
|
||||
private readonly logger = new Logger(ConfigManagerService.name);
|
||||
|
||||
/**
|
||||
* 获取配置目录路径
|
||||
*
|
||||
* 在开发环境中使用 config/zulip
|
||||
* 在生产环境中使用 dist/zulip (编译后的位置)
|
||||
*/
|
||||
private getConfigDir(): string {
|
||||
const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||
|
||||
if (isDevelopment) {
|
||||
// 开发环境:使用源码目录
|
||||
return path.join(process.cwd(), 'config', 'zulip');
|
||||
} else {
|
||||
// 生产环境:使用编译后的目录
|
||||
const distConfigPath = path.join(process.cwd(), 'dist', 'zulip');
|
||||
const rootConfigPath = path.join(process.cwd(), 'config', 'zulip');
|
||||
|
||||
// 优先使用 dist/zulip,如果不存在则回退到 config/zulip
|
||||
if (fs.existsSync(distConfigPath)) {
|
||||
return distConfigPath;
|
||||
} else if (fs.existsSync(rootConfigPath)) {
|
||||
return rootConfigPath;
|
||||
} else {
|
||||
// 都不存在,使用默认路径
|
||||
return distConfigPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.logger.log('ConfigManagerService初始化完成');
|
||||
|
||||
@@ -216,6 +267,9 @@ export class ConfigManagerService implements OnModuleDestroy {
|
||||
* 4. 存储到内存映射
|
||||
*
|
||||
* @returns Promise<void>
|
||||
*
|
||||
* @throws Error 当配置格式无效时
|
||||
* @throws Error 当文件读取失败时
|
||||
*/
|
||||
async loadMapConfig(): Promise<void> {
|
||||
this.logger.log('开始加载地图配置', {
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
LoadStatus,
|
||||
ErrorHandlingResult,
|
||||
RetryConfig,
|
||||
} from './error-handler.service';
|
||||
} from './error_handler.service';
|
||||
import { AppLoggerService } from '../../../core/utils/logger/logger.service';
|
||||
|
||||
describe('ErrorHandlerService', () => {
|
||||
@@ -115,6 +115,28 @@ export enum LoadStatus {
|
||||
CRITICAL = 'critical',
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误处理服务类
|
||||
*
|
||||
* 职责:
|
||||
* - 统一处理系统错误和异常
|
||||
* - 实现重试机制和服务降级
|
||||
* - 监控系统健康状态和负载
|
||||
* - 提供错误恢复和告警功能
|
||||
*
|
||||
* 主要方法:
|
||||
* - handleError(): 处理各类错误和异常
|
||||
* - retryWithBackoff(): 带退避策略的重试机制
|
||||
* - enableDegradedMode(): 启用服务降级模式
|
||||
* - getServiceStatus(): 获取服务状态
|
||||
* - recordError(): 记录错误统计
|
||||
*
|
||||
* 使用场景:
|
||||
* - Zulip API调用失败时的错误处理
|
||||
* - 网络连接异常的重试机制
|
||||
* - 系统负载过高时的服务降级
|
||||
* - 错误监控和告警通知
|
||||
*/
|
||||
@Injectable()
|
||||
export class ErrorHandlerService extends EventEmitter implements OnModuleDestroy {
|
||||
private readonly logger = new Logger(ErrorHandlerService.name);
|
||||
@@ -182,6 +182,29 @@ export interface MonitoringStats {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 监控服务类
|
||||
*
|
||||
* 职责:
|
||||
* - 监控Zulip集成系统的运行状态
|
||||
* - 收集和统计系统性能指标
|
||||
* - 提供健康检查和告警功能
|
||||
* - 生成系统监控报告
|
||||
*
|
||||
* 主要方法:
|
||||
* - recordConnection(): 记录连接统计
|
||||
* - recordApiCall(): 记录API调用统计
|
||||
* - recordMessage(): 记录消息统计
|
||||
* - triggerAlert(): 触发告警
|
||||
* - getSystemStats(): 获取系统统计信息
|
||||
* - performHealthCheck(): 执行健康检查
|
||||
*
|
||||
* 使用场景:
|
||||
* - 系统性能监控和统计
|
||||
* - 异常情况的告警通知
|
||||
* - 系统健康状态检查
|
||||
* - 运维数据的收集和分析
|
||||
*/
|
||||
@Injectable()
|
||||
export class MonitoringService extends EventEmitter implements OnModuleInit, OnModuleDestroy {
|
||||
private readonly logger = new Logger(MonitoringService.name);
|
||||
@@ -21,8 +21,30 @@
|
||||
*/
|
||||
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import { ConfigManagerService } from './config-manager.service';
|
||||
import { ConfigManagerService } from './config_manager.service';
|
||||
|
||||
/**
|
||||
* Stream初始化服务类
|
||||
*
|
||||
* 职责:
|
||||
* - 系统启动时自动检查并创建Zulip Streams
|
||||
* - 确保所有地图对应的Stream都存在
|
||||
* - 验证Stream配置的完整性
|
||||
* - 提供Stream初始化状态监控
|
||||
*
|
||||
* 主要方法:
|
||||
* - onModuleInit(): 模块初始化时自动执行
|
||||
* - initializeStreams(): 初始化所有必需的Streams
|
||||
* - createStreamIfNotExists(): 检查并创建单个Stream
|
||||
* - validateStreamConfig(): 验证Stream配置
|
||||
* - getInitializationStatus(): 获取初始化状态
|
||||
*
|
||||
* 使用场景:
|
||||
* - 系统启动时自动初始化Streams
|
||||
* - 确保消息路由的目标Stream存在
|
||||
* - 新增地图时自动创建对应Stream
|
||||
* - 系统部署和配置验证
|
||||
*/
|
||||
@Injectable()
|
||||
export class StreamInitializerService implements OnModuleInit {
|
||||
private readonly logger = new Logger(StreamInitializerService.name);
|
||||
708
src/core/zulip/services/zulip_account.service.ts
Normal file
708
src/core/zulip/services/zulip_account.service.ts
Normal file
@@ -0,0 +1,708 @@
|
||||
/**
|
||||
* Zulip账号管理核心服务
|
||||
*
|
||||
* 功能描述:
|
||||
* - 自动创建Zulip用户账号
|
||||
* - 生成API Key并安全存储
|
||||
* - 处理账号创建失败场景
|
||||
* - 管理用户账号与游戏账号的关联
|
||||
*
|
||||
* 主要方法:
|
||||
* - createZulipAccount(): 创建新的Zulip用户账号
|
||||
* - generateApiKey(): 为用户生成API Key
|
||||
* - validateZulipAccount(): 验证Zulip账号有效性
|
||||
* - linkGameAccount(): 关联游戏账号与Zulip账号
|
||||
*
|
||||
* 使用场景:
|
||||
* - 用户注册时自动创建Zulip账号
|
||||
* - API Key管理和更新
|
||||
* - 账号关联和映射存储
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.0.0
|
||||
* @since 2025-01-05
|
||||
*/
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ZulipClientConfig } from '../interfaces/zulip-core.interfaces';
|
||||
|
||||
/**
|
||||
* Zulip账号创建请求接口
|
||||
*/
|
||||
export interface CreateZulipAccountRequest {
|
||||
email: string;
|
||||
fullName: string;
|
||||
password?: string;
|
||||
shortName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zulip账号创建结果接口
|
||||
*/
|
||||
export interface CreateZulipAccountResult {
|
||||
success: boolean;
|
||||
userId?: number;
|
||||
email?: string;
|
||||
apiKey?: string;
|
||||
error?: string;
|
||||
errorCode?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* API Key生成结果接口
|
||||
*/
|
||||
export interface GenerateApiKeyResult {
|
||||
success: boolean;
|
||||
apiKey?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 账号验证结果接口
|
||||
*/
|
||||
export interface ValidateAccountResult {
|
||||
success: boolean;
|
||||
isValid?: boolean;
|
||||
userInfo?: any;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 账号关联信息接口
|
||||
*/
|
||||
export interface AccountLinkInfo {
|
||||
gameUserId: string;
|
||||
zulipUserId: number;
|
||||
zulipEmail: string;
|
||||
zulipApiKey: string;
|
||||
createdAt: Date;
|
||||
lastVerified?: Date;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zulip账号管理服务类
|
||||
*
|
||||
* 职责:
|
||||
* - 处理Zulip用户账号的创建和管理
|
||||
* - 管理API Key的生成和存储
|
||||
* - 维护游戏账号与Zulip账号的关联关系
|
||||
* - 提供账号验证和状态检查功能
|
||||
*
|
||||
* 主要方法:
|
||||
* - createZulipAccount(): 创建新的Zulip用户账号
|
||||
* - generateApiKey(): 为现有用户生成API Key
|
||||
* - validateZulipAccount(): 验证Zulip账号有效性
|
||||
* - linkGameAccount(): 建立游戏账号与Zulip账号的关联
|
||||
* - unlinkGameAccount(): 解除账号关联
|
||||
*
|
||||
* 使用场景:
|
||||
* - 用户注册流程中自动创建Zulip账号
|
||||
* - API Key管理和更新
|
||||
* - 账号状态监控和维护
|
||||
* - 跨平台账号同步
|
||||
*/
|
||||
@Injectable()
|
||||
export class ZulipAccountService {
|
||||
private readonly logger = new Logger(ZulipAccountService.name);
|
||||
private adminClient: any = null;
|
||||
private readonly accountLinks = new Map<string, AccountLinkInfo>();
|
||||
|
||||
constructor() {
|
||||
this.logger.log('ZulipAccountService初始化完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化管理员客户端
|
||||
*
|
||||
* 功能描述:
|
||||
* 使用管理员凭证初始化Zulip客户端,用于创建用户账号
|
||||
*
|
||||
* @param adminConfig 管理员配置
|
||||
* @returns Promise<boolean> 是否初始化成功
|
||||
*/
|
||||
async initializeAdminClient(adminConfig: ZulipClientConfig): Promise<boolean> {
|
||||
this.logger.log('初始化Zulip管理员客户端', {
|
||||
operation: 'initializeAdminClient',
|
||||
realm: adminConfig.realm,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
try {
|
||||
// 动态导入zulip-js
|
||||
const zulipInit = await this.loadZulipModule();
|
||||
|
||||
// 创建管理员客户端
|
||||
this.adminClient = await zulipInit({
|
||||
username: adminConfig.username,
|
||||
apiKey: adminConfig.apiKey,
|
||||
realm: adminConfig.realm,
|
||||
});
|
||||
|
||||
// 验证管理员权限
|
||||
const profile = await this.adminClient.users.me.getProfile();
|
||||
|
||||
if (profile.result !== 'success') {
|
||||
throw new Error(`管理员客户端验证失败: ${profile.msg || '未知错误'}`);
|
||||
}
|
||||
|
||||
this.logger.log('管理员客户端初始化成功', {
|
||||
operation: 'initializeAdminClient',
|
||||
adminEmail: profile.email,
|
||||
isAdmin: profile.is_admin,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
this.logger.error('管理员客户端初始化失败', {
|
||||
operation: 'initializeAdminClient',
|
||||
error: err.message,
|
||||
timestamp: new Date().toISOString(),
|
||||
}, err.stack);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Zulip用户账号
|
||||
*
|
||||
* 功能描述:
|
||||
* 使用管理员权限在Zulip服务器上创建新的用户账号
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证管理员客户端是否已初始化
|
||||
* 2. 检查邮箱是否已存在
|
||||
* 3. 生成用户密码(如果未提供)
|
||||
* 4. 调用Zulip API创建用户
|
||||
* 5. 为新用户生成API Key
|
||||
* 6. 返回创建结果
|
||||
*
|
||||
* @param request 账号创建请求
|
||||
* @returns Promise<CreateZulipAccountResult> 创建结果
|
||||
*/
|
||||
async createZulipAccount(request: CreateZulipAccountRequest): Promise<CreateZulipAccountResult> {
|
||||
const startTime = Date.now();
|
||||
|
||||
this.logger.log('开始创建Zulip账号', {
|
||||
operation: 'createZulipAccount',
|
||||
email: request.email,
|
||||
fullName: request.fullName,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
try {
|
||||
// 1. 验证管理员客户端
|
||||
if (!this.adminClient) {
|
||||
throw new Error('管理员客户端未初始化');
|
||||
}
|
||||
|
||||
// 2. 验证请求参数
|
||||
if (!request.email || !request.email.trim()) {
|
||||
throw new Error('邮箱地址不能为空');
|
||||
}
|
||||
|
||||
if (!request.fullName || !request.fullName.trim()) {
|
||||
throw new Error('用户全名不能为空');
|
||||
}
|
||||
|
||||
// 3. 检查邮箱格式
|
||||
if (!this.isValidEmail(request.email)) {
|
||||
throw new Error('邮箱格式无效');
|
||||
}
|
||||
|
||||
// 4. 检查用户是否已存在
|
||||
const existingUser = await this.checkUserExists(request.email);
|
||||
if (existingUser) {
|
||||
this.logger.warn('用户已存在', {
|
||||
operation: 'createZulipAccount',
|
||||
email: request.email,
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
error: '用户已存在',
|
||||
errorCode: 'USER_ALREADY_EXISTS',
|
||||
};
|
||||
}
|
||||
|
||||
// 5. 生成密码(如果未提供)
|
||||
const password = request.password || this.generateRandomPassword();
|
||||
const shortName = request.shortName || this.generateShortName(request.email);
|
||||
|
||||
// 6. 创建用户参数
|
||||
const createParams = {
|
||||
email: request.email,
|
||||
password: password,
|
||||
full_name: request.fullName,
|
||||
short_name: shortName,
|
||||
};
|
||||
|
||||
// 7. 调用Zulip API创建用户
|
||||
const createResponse = await this.adminClient.users.create(createParams);
|
||||
|
||||
if (createResponse.result !== 'success') {
|
||||
this.logger.warn('Zulip用户创建失败', {
|
||||
operation: 'createZulipAccount',
|
||||
email: request.email,
|
||||
error: createResponse.msg,
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
error: createResponse.msg || '用户创建失败',
|
||||
errorCode: 'ZULIP_CREATE_FAILED',
|
||||
};
|
||||
}
|
||||
|
||||
// 8. 为新用户生成API Key
|
||||
const apiKeyResult = await this.generateApiKeyForUser(request.email, password);
|
||||
|
||||
if (!apiKeyResult.success) {
|
||||
this.logger.warn('API Key生成失败,但用户已创建', {
|
||||
operation: 'createZulipAccount',
|
||||
email: request.email,
|
||||
error: apiKeyResult.error,
|
||||
});
|
||||
// 用户已创建,但API Key生成失败
|
||||
return {
|
||||
success: true,
|
||||
userId: createResponse.user_id,
|
||||
email: request.email,
|
||||
error: `用户创建成功,但API Key生成失败: ${apiKeyResult.error}`,
|
||||
errorCode: 'API_KEY_GENERATION_FAILED',
|
||||
};
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.logger.log('Zulip账号创建成功', {
|
||||
operation: 'createZulipAccount',
|
||||
email: request.email,
|
||||
userId: createResponse.user_id,
|
||||
hasApiKey: !!apiKeyResult.apiKey,
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
userId: createResponse.user_id,
|
||||
email: request.email,
|
||||
apiKey: apiKeyResult.apiKey,
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.logger.error('创建Zulip账号失败', {
|
||||
operation: 'createZulipAccount',
|
||||
email: request.email,
|
||||
error: err.message,
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
}, err.stack);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: err.message,
|
||||
errorCode: 'ACCOUNT_CREATION_FAILED',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为用户生成API Key
|
||||
*
|
||||
* 功能描述:
|
||||
* 使用用户凭证获取API Key
|
||||
*
|
||||
* @param email 用户邮箱
|
||||
* @param password 用户密码
|
||||
* @returns Promise<GenerateApiKeyResult> 生成结果
|
||||
*/
|
||||
async generateApiKeyForUser(email: string, password: string): Promise<GenerateApiKeyResult> {
|
||||
this.logger.log('为用户生成API Key', {
|
||||
operation: 'generateApiKeyForUser',
|
||||
email,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
try {
|
||||
// 动态导入zulip-js
|
||||
const zulipInit = await this.loadZulipModule();
|
||||
|
||||
// 使用用户凭证获取API Key
|
||||
const userClient = await zulipInit({
|
||||
username: email,
|
||||
password: password,
|
||||
realm: this.getRealmFromAdminClient(),
|
||||
});
|
||||
|
||||
// 验证客户端并获取API Key
|
||||
const profile = await userClient.users.me.getProfile();
|
||||
|
||||
if (profile.result !== 'success') {
|
||||
throw new Error(`API Key获取失败: ${profile.msg || '未知错误'}`);
|
||||
}
|
||||
|
||||
// 从客户端配置中提取API Key
|
||||
const apiKey = userClient.config?.apiKey;
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error('无法从客户端配置中获取API Key');
|
||||
}
|
||||
|
||||
this.logger.log('API Key生成成功', {
|
||||
operation: 'generateApiKeyForUser',
|
||||
email,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
apiKey: apiKey,
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
|
||||
this.logger.error('API Key生成失败', {
|
||||
operation: 'generateApiKeyForUser',
|
||||
email,
|
||||
error: err.message,
|
||||
timestamp: new Date().toISOString(),
|
||||
}, err.stack);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: err.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证Zulip账号有效性
|
||||
*
|
||||
* 功能描述:
|
||||
* 验证指定的Zulip账号是否存在且有效
|
||||
*
|
||||
* @param email 用户邮箱
|
||||
* @param apiKey 用户API Key(可选)
|
||||
* @returns Promise<ValidateAccountResult> 验证结果
|
||||
*/
|
||||
async validateZulipAccount(email: string, apiKey?: string): Promise<ValidateAccountResult> {
|
||||
this.logger.log('验证Zulip账号', {
|
||||
operation: 'validateZulipAccount',
|
||||
email,
|
||||
hasApiKey: !!apiKey,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
try {
|
||||
if (apiKey) {
|
||||
// 使用API Key验证
|
||||
const zulipInit = await this.loadZulipModule();
|
||||
const userClient = await zulipInit({
|
||||
username: email,
|
||||
apiKey: apiKey,
|
||||
realm: this.getRealmFromAdminClient(),
|
||||
});
|
||||
|
||||
const profile = await userClient.users.me.getProfile();
|
||||
|
||||
if (profile.result === 'success') {
|
||||
this.logger.log('账号验证成功(API Key)', {
|
||||
operation: 'validateZulipAccount',
|
||||
email,
|
||||
userId: profile.user_id,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
isValid: true,
|
||||
userInfo: profile,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: true,
|
||||
isValid: false,
|
||||
error: profile.msg || 'API Key验证失败',
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// 仅检查用户是否存在
|
||||
const userExists = await this.checkUserExists(email);
|
||||
|
||||
this.logger.log('账号存在性检查完成', {
|
||||
operation: 'validateZulipAccount',
|
||||
email,
|
||||
exists: userExists,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
isValid: userExists,
|
||||
};
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
|
||||
this.logger.error('账号验证失败', {
|
||||
operation: 'validateZulipAccount',
|
||||
email,
|
||||
error: err.message,
|
||||
timestamp: new Date().toISOString(),
|
||||
}, err.stack);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: err.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联游戏账号与Zulip账号
|
||||
*
|
||||
* 功能描述:
|
||||
* 建立游戏用户ID与Zulip账号的映射关系
|
||||
*
|
||||
* @param gameUserId 游戏用户ID
|
||||
* @param zulipUserId Zulip用户ID
|
||||
* @param zulipEmail Zulip邮箱
|
||||
* @param zulipApiKey Zulip API Key
|
||||
* @returns Promise<boolean> 是否关联成功
|
||||
*/
|
||||
async linkGameAccount(
|
||||
gameUserId: string,
|
||||
zulipUserId: number,
|
||||
zulipEmail: string,
|
||||
zulipApiKey: string,
|
||||
): Promise<boolean> {
|
||||
this.logger.log('关联游戏账号与Zulip账号', {
|
||||
operation: 'linkGameAccount',
|
||||
gameUserId,
|
||||
zulipUserId,
|
||||
zulipEmail,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
try {
|
||||
// 验证参数
|
||||
if (!gameUserId || !zulipUserId || !zulipEmail || !zulipApiKey) {
|
||||
throw new Error('关联参数不完整');
|
||||
}
|
||||
|
||||
// 创建关联信息
|
||||
const linkInfo: AccountLinkInfo = {
|
||||
gameUserId,
|
||||
zulipUserId,
|
||||
zulipEmail,
|
||||
zulipApiKey,
|
||||
createdAt: new Date(),
|
||||
isActive: true,
|
||||
};
|
||||
|
||||
// 存储关联信息(实际项目中应存储到数据库)
|
||||
this.accountLinks.set(gameUserId, linkInfo);
|
||||
|
||||
this.logger.log('账号关联成功', {
|
||||
operation: 'linkGameAccount',
|
||||
gameUserId,
|
||||
zulipUserId,
|
||||
zulipEmail,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
|
||||
this.logger.error('账号关联失败', {
|
||||
operation: 'linkGameAccount',
|
||||
gameUserId,
|
||||
zulipUserId,
|
||||
error: err.message,
|
||||
timestamp: new Date().toISOString(),
|
||||
}, err.stack);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解除游戏账号与Zulip账号的关联
|
||||
*
|
||||
* @param gameUserId 游戏用户ID
|
||||
* @returns Promise<boolean> 是否解除成功
|
||||
*/
|
||||
async unlinkGameAccount(gameUserId: string): Promise<boolean> {
|
||||
this.logger.log('解除账号关联', {
|
||||
operation: 'unlinkGameAccount',
|
||||
gameUserId,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
try {
|
||||
const linkInfo = this.accountLinks.get(gameUserId);
|
||||
|
||||
if (linkInfo) {
|
||||
linkInfo.isActive = false;
|
||||
this.accountLinks.delete(gameUserId);
|
||||
|
||||
this.logger.log('账号关联解除成功', {
|
||||
operation: 'unlinkGameAccount',
|
||||
gameUserId,
|
||||
zulipEmail: linkInfo.zulipEmail,
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
|
||||
this.logger.error('解除账号关联失败', {
|
||||
operation: 'unlinkGameAccount',
|
||||
gameUserId,
|
||||
error: err.message,
|
||||
timestamp: new Date().toISOString(),
|
||||
}, err.stack);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取游戏账号的Zulip关联信息
|
||||
*
|
||||
* @param gameUserId 游戏用户ID
|
||||
* @returns AccountLinkInfo | null 关联信息
|
||||
*/
|
||||
getAccountLink(gameUserId: string): AccountLinkInfo | null {
|
||||
return this.accountLinks.get(gameUserId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有账号关联信息
|
||||
*
|
||||
* @returns AccountLinkInfo[] 所有关联信息
|
||||
*/
|
||||
getAllAccountLinks(): AccountLinkInfo[] {
|
||||
return Array.from(this.accountLinks.values()).filter(link => link.isActive);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否已存在
|
||||
*
|
||||
* @param email 用户邮箱
|
||||
* @returns Promise<boolean> 用户是否存在
|
||||
* @private
|
||||
*/
|
||||
private async checkUserExists(email: string): Promise<boolean> {
|
||||
try {
|
||||
if (!this.adminClient) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取所有用户列表
|
||||
const usersResponse = await this.adminClient.users.retrieve();
|
||||
|
||||
if (usersResponse.result === 'success') {
|
||||
const users = usersResponse.members || [];
|
||||
return users.some((user: any) => user.email === email);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
this.logger.warn('检查用户存在性失败', {
|
||||
operation: 'checkUserExists',
|
||||
email,
|
||||
error: err.message,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邮箱格式
|
||||
*
|
||||
* @param email 邮箱地址
|
||||
* @returns boolean 是否为有效邮箱
|
||||
* @private
|
||||
*/
|
||||
private isValidEmail(email: string): boolean {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机密码
|
||||
*
|
||||
* @returns string 随机密码
|
||||
* @private
|
||||
*/
|
||||
private generateRandomPassword(): string {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';
|
||||
let password = '';
|
||||
for (let i = 0; i < 12; i++) {
|
||||
password += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return password;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从邮箱生成短名称
|
||||
*
|
||||
* @param email 邮箱地址
|
||||
* @returns string 短名称
|
||||
* @private
|
||||
*/
|
||||
private generateShortName(email: string): string {
|
||||
const localPart = email.split('@')[0];
|
||||
// 移除特殊字符,只保留字母数字和下划线
|
||||
return localPart.replace(/[^a-zA-Z0-9_]/g, '').toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从管理员客户端获取Realm
|
||||
*
|
||||
* @returns string Realm URL
|
||||
* @private
|
||||
*/
|
||||
private getRealmFromAdminClient(): string {
|
||||
if (!this.adminClient || !this.adminClient.config) {
|
||||
throw new Error('管理员客户端未初始化或配置缺失');
|
||||
}
|
||||
return this.adminClient.config.realm;
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态加载zulip-js模块
|
||||
*
|
||||
* @returns Promise<any> zulip-js初始化函数
|
||||
* @private
|
||||
*/
|
||||
private async loadZulipModule(): Promise<any> {
|
||||
try {
|
||||
// 使用动态导入加载zulip-js
|
||||
const zulipModule = await import('zulip-js');
|
||||
return zulipModule.default || zulipModule;
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
this.logger.error('加载zulip-js模块失败', {
|
||||
operation: 'loadZulipModule',
|
||||
error: err.message,
|
||||
timestamp: new Date().toISOString(),
|
||||
}, err.stack);
|
||||
throw new Error(`加载zulip-js模块失败: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import * as fc from 'fast-check';
|
||||
import { ZulipClientService, ZulipClientConfig, ZulipClientInstance } from './zulip-client.service';
|
||||
import { ZulipClientService, ZulipClientConfig, ZulipClientInstance } from './zulip_client.service';
|
||||
import { AppLoggerService } from '../../../core/utils/logger/logger.service';
|
||||
|
||||
describe('ZulipClientService', () => {
|
||||
@@ -77,6 +77,28 @@ export interface GetEventsResult {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zulip客户端服务类
|
||||
*
|
||||
* 职责:
|
||||
* - 封装Zulip REST API调用
|
||||
* - 处理Zulip客户端的创建和配置
|
||||
* - 管理事件队列的注册和轮询
|
||||
* - 提供消息发送和接收功能
|
||||
*
|
||||
* 主要方法:
|
||||
* - createClient(): 创建并初始化Zulip客户端
|
||||
* - registerQueue(): 注册Zulip事件队列
|
||||
* - sendMessage(): 发送消息到Zulip Stream
|
||||
* - getEvents(): 获取Zulip事件
|
||||
* - validateConfig(): 验证客户端配置
|
||||
*
|
||||
* 使用场景:
|
||||
* - 为每个用户创建独立的Zulip客户端
|
||||
* - 处理与Zulip服务器的所有通信
|
||||
* - 消息的发送和事件的接收
|
||||
* - API调用的错误处理和重试
|
||||
*/
|
||||
@Injectable()
|
||||
export class ZulipClientService {
|
||||
private readonly logger = new Logger(ZulipClientService.name);
|
||||
@@ -12,9 +12,9 @@
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ZulipClientPoolService, PoolStats } from './zulip-client-pool.service';
|
||||
import { ZulipClientService, ZulipClientConfig, ZulipClientInstance } from './zulip-client.service';
|
||||
import { AppLoggerService } from '../../../core/utils/logger/logger.service';
|
||||
import { ZulipClientPoolService, PoolStats } from './zulip_client_pool.service';
|
||||
import { ZulipClientService, ZulipClientConfig, ZulipClientInstance } from './zulip_client.service';
|
||||
import { AppLoggerService } from '../../utils/logger/logger.service';
|
||||
|
||||
describe('ZulipClientPoolService', () => {
|
||||
let service: ZulipClientPoolService;
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
SendMessageResult,
|
||||
RegisterQueueResult,
|
||||
GetEventsResult,
|
||||
} from './zulip-client.service';
|
||||
} from './zulip_client.service';
|
||||
|
||||
/**
|
||||
* 用户客户端信息接口
|
||||
@@ -57,6 +57,28 @@ export interface PoolStats {
|
||||
clientIds: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Zulip客户端池服务类
|
||||
*
|
||||
* 职责:
|
||||
* - 管理用户专用的Zulip客户端实例
|
||||
* - 维护客户端连接池和生命周期
|
||||
* - 处理客户端的创建、销毁和状态管理
|
||||
* - 提供客户端池统计和监控功能
|
||||
*
|
||||
* 主要方法:
|
||||
* - createUserClient(): 为用户创建专用Zulip客户端
|
||||
* - getUserClient(): 获取用户的Zulip客户端
|
||||
* - destroyUserClient(): 销毁用户的Zulip客户端
|
||||
* - getPoolStats(): 获取客户端池统计信息
|
||||
* - startEventPolling(): 启动事件轮询
|
||||
*
|
||||
* 使用场景:
|
||||
* - 玩家登录时创建专用客户端
|
||||
* - 消息发送时获取客户端实例
|
||||
* - 玩家登出时清理客户端资源
|
||||
* - 系统监控和性能统计
|
||||
*/
|
||||
@Injectable()
|
||||
export class ZulipClientPoolService implements OnModuleDestroy {
|
||||
private readonly clientPool = new Map<string, UserClientInfo>();
|
||||
71
src/core/zulip/zulip-core.module.ts
Normal file
71
src/core/zulip/zulip-core.module.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Zulip核心服务模块
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供Zulip技术实现相关的核心服务
|
||||
* - 封装第三方API调用和技术细节
|
||||
* - 为业务层提供抽象接口
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-31
|
||||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ZulipClientService } from './services/zulip_client.service';
|
||||
import { ZulipClientPoolService } from './services/zulip_client_pool.service';
|
||||
import { ConfigManagerService } from './services/config_manager.service';
|
||||
import { ApiKeySecurityService } from './services/api_key_security.service';
|
||||
import { ErrorHandlerService } from './services/error_handler.service';
|
||||
import { MonitoringService } from './services/monitoring.service';
|
||||
import { StreamInitializerService } from './services/stream_initializer.service';
|
||||
import { ZulipAccountService } from './services/zulip_account.service';
|
||||
import { RedisModule } from '../redis/redis.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// Redis模块 - ApiKeySecurityService需要REDIS_SERVICE
|
||||
RedisModule,
|
||||
],
|
||||
providers: [
|
||||
// 核心客户端服务
|
||||
{
|
||||
provide: 'ZULIP_CLIENT_SERVICE',
|
||||
useClass: ZulipClientService,
|
||||
},
|
||||
{
|
||||
provide: 'ZULIP_CLIENT_POOL_SERVICE',
|
||||
useClass: ZulipClientPoolService,
|
||||
},
|
||||
{
|
||||
provide: 'ZULIP_CONFIG_SERVICE',
|
||||
useClass: ConfigManagerService,
|
||||
},
|
||||
|
||||
// 辅助服务
|
||||
ApiKeySecurityService,
|
||||
ErrorHandlerService,
|
||||
MonitoringService,
|
||||
StreamInitializerService,
|
||||
ZulipAccountService,
|
||||
|
||||
// 直接提供类(用于内部依赖)
|
||||
ZulipClientService,
|
||||
ZulipClientPoolService,
|
||||
ConfigManagerService,
|
||||
],
|
||||
exports: [
|
||||
// 导出接口标识符供业务层使用
|
||||
'ZULIP_CLIENT_SERVICE',
|
||||
'ZULIP_CLIENT_POOL_SERVICE',
|
||||
'ZULIP_CONFIG_SERVICE',
|
||||
|
||||
// 导出辅助服务
|
||||
ApiKeySecurityService,
|
||||
ErrorHandlerService,
|
||||
MonitoringService,
|
||||
StreamInitializerService,
|
||||
ZulipAccountService,
|
||||
],
|
||||
})
|
||||
export class ZulipCoreModule {}
|
||||
@@ -1,333 +0,0 @@
|
||||
# Comprehensive API Test Script
|
||||
# 综合API测试脚本 - 完整的后端功能测试
|
||||
#
|
||||
# 🧪 测试内容:
|
||||
# 1. 基础API功能(应用状态、注册、登录)
|
||||
# 2. 邮箱验证码流程(发送、验证、冲突检测)
|
||||
# 3. 验证码冷却时间清除功能
|
||||
# 4. 限流保护机制
|
||||
# 5. 密码重置流程
|
||||
# 6. 验证码登录功能
|
||||
# 7. 错误处理和边界条件
|
||||
#
|
||||
# 🚀 使用方法:
|
||||
# .\test-comprehensive.ps1 # 运行完整测试
|
||||
# .\test-comprehensive.ps1 -SkipThrottleTest # 跳过限流测试
|
||||
# .\test-comprehensive.ps1 -SkipCooldownTest # 跳过冷却测试
|
||||
# .\test-comprehensive.ps1 -BaseUrl "https://your-server.com" # 测试远程服务器
|
||||
|
||||
param(
|
||||
[string]$BaseUrl = "http://localhost:3000",
|
||||
[switch]$SkipThrottleTest = $false,
|
||||
[switch]$SkipCooldownTest = $false
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
|
||||
Write-Host "🧪 Comprehensive API Test Suite" -ForegroundColor Green
|
||||
Write-Host "===============================" -ForegroundColor Green
|
||||
Write-Host "Base URL: $BaseUrl" -ForegroundColor Yellow
|
||||
Write-Host "Skip Throttle Test: $SkipThrottleTest" -ForegroundColor Yellow
|
||||
Write-Host "Skip Cooldown Test: $SkipCooldownTest" -ForegroundColor Yellow
|
||||
|
||||
# Helper function to handle API responses
|
||||
function Test-ApiCall {
|
||||
param(
|
||||
[string]$TestName,
|
||||
[string]$Url,
|
||||
[string]$Body,
|
||||
[string]$Method = "POST",
|
||||
[int]$ExpectedStatus = 200,
|
||||
[switch]$Silent = $false
|
||||
)
|
||||
|
||||
if (-not $Silent) {
|
||||
Write-Host "`n📋 $TestName" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Invoke-RestMethod -Uri $Url -Method $Method -Body $Body -ContentType "application/json" -ErrorAction Stop
|
||||
if (-not $Silent) {
|
||||
Write-Host "✅ SUCCESS ($(if ($response.success) { 'true' } else { 'false' }))" -ForegroundColor Green
|
||||
Write-Host "Message: $($response.message)" -ForegroundColor Cyan
|
||||
}
|
||||
return $response
|
||||
} catch {
|
||||
$statusCode = $_.Exception.Response.StatusCode.value__
|
||||
if (-not $Silent) {
|
||||
Write-Host "❌ FAILED ($statusCode)" -ForegroundColor $(if ($statusCode -eq $ExpectedStatus) { "Yellow" } else { "Red" })
|
||||
}
|
||||
|
||||
if ($_.Exception.Response) {
|
||||
$stream = $_.Exception.Response.GetResponseStream()
|
||||
$reader = New-Object System.IO.StreamReader($stream)
|
||||
$responseBody = $reader.ReadToEnd()
|
||||
$reader.Close()
|
||||
$stream.Close()
|
||||
|
||||
if ($responseBody) {
|
||||
try {
|
||||
$errorResponse = $responseBody | ConvertFrom-Json
|
||||
if (-not $Silent) {
|
||||
Write-Host "Message: $($errorResponse.message)" -ForegroundColor Cyan
|
||||
Write-Host "Error Code: $($errorResponse.error_code)" -ForegroundColor Gray
|
||||
}
|
||||
return $errorResponse
|
||||
} catch {
|
||||
if (-not $Silent) {
|
||||
Write-Host "Raw Response: $responseBody" -ForegroundColor Gray
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
# Clear throttle first
|
||||
Write-Host "`n🔄 Clearing throttle records..." -ForegroundColor Blue
|
||||
try {
|
||||
Invoke-RestMethod -Uri "$BaseUrl/auth/debug-clear-throttle" -Method POST | Out-Null
|
||||
Write-Host "✅ Throttle cleared" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "⚠️ Could not clear throttle" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# Test Results Tracking
|
||||
$testResults = @{
|
||||
AppStatus = $false
|
||||
BasicAPI = $false
|
||||
EmailConflict = $false
|
||||
VerificationCodeLogin = $false
|
||||
CooldownClearing = $false
|
||||
ThrottleProtection = $false
|
||||
PasswordReset = $false
|
||||
}
|
||||
|
||||
Write-Host "`n" + "="*60 -ForegroundColor Cyan
|
||||
Write-Host "🧪 Test Suite 0: Application Status" -ForegroundColor Cyan
|
||||
Write-Host "="*60 -ForegroundColor Cyan
|
||||
|
||||
# Test application status
|
||||
$result0 = Test-ApiCall -TestName "Check application status" -Url "$BaseUrl" -Method "GET" -Body ""
|
||||
|
||||
if ($result0 -and $result0.service -eq "Pixel Game Server") {
|
||||
$testResults.AppStatus = $true
|
||||
Write-Host "✅ PASS: Application is running" -ForegroundColor Green
|
||||
Write-Host " Service: $($result0.service)" -ForegroundColor Cyan
|
||||
Write-Host " Version: $($result0.version)" -ForegroundColor Cyan
|
||||
Write-Host " Environment: $($result0.environment)" -ForegroundColor Cyan
|
||||
} else {
|
||||
Write-Host "❌ FAIL: Application status check failed" -ForegroundColor Red
|
||||
}
|
||||
|
||||
Write-Host "`n" + "="*60 -ForegroundColor Cyan
|
||||
Write-Host "🧪 Test Suite 1: Basic API Functionality" -ForegroundColor Cyan
|
||||
Write-Host "="*60 -ForegroundColor Cyan
|
||||
|
||||
# Generate unique test data
|
||||
$testEmail = "comprehensive_test_$(Get-Random)@example.com"
|
||||
$testUsername = "comp_test_$(Get-Random)"
|
||||
|
||||
# Test 1: Send verification code
|
||||
$result1 = Test-ApiCall -TestName "Send email verification code" -Url "$BaseUrl/auth/send-email-verification" -Body (@{
|
||||
email = $testEmail
|
||||
} | ConvertTo-Json)
|
||||
|
||||
if ($result1 -and $result1.data.verification_code) {
|
||||
$verificationCode = $result1.data.verification_code
|
||||
Write-Host "Got verification code: $verificationCode" -ForegroundColor Green
|
||||
|
||||
# Test 2: Register user
|
||||
$result2 = Test-ApiCall -TestName "Register new user" -Url "$BaseUrl/auth/register" -Body (@{
|
||||
username = $testUsername
|
||||
password = "password123"
|
||||
nickname = "Comprehensive Test User"
|
||||
email = $testEmail
|
||||
email_verification_code = $verificationCode
|
||||
} | ConvertTo-Json)
|
||||
|
||||
if ($result2 -and $result2.success) {
|
||||
# Test 3: Login user
|
||||
$result3 = Test-ApiCall -TestName "Login with registered user" -Url "$BaseUrl/auth/login" -Body (@{
|
||||
identifier = $testUsername
|
||||
password = "password123"
|
||||
} | ConvertTo-Json)
|
||||
|
||||
if ($result3 -and $result3.success) {
|
||||
$testResults.BasicAPI = $true
|
||||
Write-Host "✅ PASS: Basic API functionality working" -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "`n" + "="*60 -ForegroundColor Cyan
|
||||
Write-Host "🧪 Test Suite 2: Email Conflict Detection" -ForegroundColor Cyan
|
||||
Write-Host "="*60 -ForegroundColor Cyan
|
||||
|
||||
# Test email conflict detection
|
||||
$result4 = Test-ApiCall -TestName "Test email conflict detection" -Url "$BaseUrl/auth/send-email-verification" -Body (@{
|
||||
email = $testEmail
|
||||
} | ConvertTo-Json) -ExpectedStatus 409
|
||||
|
||||
if ($result4 -and $result4.message -like "*已被注册*") {
|
||||
$testResults.EmailConflict = $true
|
||||
Write-Host "✅ PASS: Email conflict detection working" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "❌ FAIL: Email conflict detection not working" -ForegroundColor Red
|
||||
}
|
||||
|
||||
Write-Host "`n" + "="*60 -ForegroundColor Cyan
|
||||
Write-Host "🧪 Test Suite 3: Verification Code Login" -ForegroundColor Cyan
|
||||
Write-Host "="*60 -ForegroundColor Cyan
|
||||
|
||||
# Test verification code login
|
||||
if ($result2 -and $result2.success) {
|
||||
$userEmail = $result2.data.user.email
|
||||
|
||||
# Send login verification code
|
||||
$result4a = Test-ApiCall -TestName "Send login verification code" -Url "$BaseUrl/auth/send-login-verification-code" -Body (@{
|
||||
identifier = $userEmail
|
||||
} | ConvertTo-Json)
|
||||
|
||||
if ($result4a -and $result4a.data.verification_code) {
|
||||
$loginCode = $result4a.data.verification_code
|
||||
|
||||
# Login with verification code
|
||||
$result4b = Test-ApiCall -TestName "Login with verification code" -Url "$BaseUrl/auth/verification-code-login" -Body (@{
|
||||
identifier = $userEmail
|
||||
verification_code = $loginCode
|
||||
} | ConvertTo-Json)
|
||||
|
||||
if ($result4b -and $result4b.success) {
|
||||
$testResults.VerificationCodeLogin = $true
|
||||
Write-Host "✅ PASS: Verification code login working" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "❌ FAIL: Verification code login failed" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $SkipCooldownTest) {
|
||||
Write-Host "`n" + "="*60 -ForegroundColor Cyan
|
||||
Write-Host "🧪 Test Suite 4: Cooldown Clearing & Password Reset" -ForegroundColor Cyan
|
||||
Write-Host "="*60 -ForegroundColor Cyan
|
||||
|
||||
# Test cooldown clearing with password reset
|
||||
if ($result2 -and $result2.success) {
|
||||
$userEmail = $result2.data.user.email
|
||||
|
||||
# Send password reset code
|
||||
$result5 = Test-ApiCall -TestName "Send password reset code" -Url "$BaseUrl/auth/forgot-password" -Body (@{
|
||||
identifier = $userEmail
|
||||
} | ConvertTo-Json)
|
||||
|
||||
if ($result5 -and $result5.data.verification_code) {
|
||||
$resetCode = $result5.data.verification_code
|
||||
|
||||
# Reset password
|
||||
$result6 = Test-ApiCall -TestName "Reset password (should clear cooldown)" -Url "$BaseUrl/auth/reset-password" -Body (@{
|
||||
identifier = $userEmail
|
||||
verification_code = $resetCode
|
||||
new_password = "newpassword123"
|
||||
} | ConvertTo-Json)
|
||||
|
||||
if ($result6 -and $result6.success) {
|
||||
$testResults.PasswordReset = $true
|
||||
Write-Host "✅ PASS: Password reset working" -ForegroundColor Green
|
||||
|
||||
# Test immediate code sending (should work if cooldown cleared)
|
||||
Start-Sleep -Seconds 1
|
||||
$result7 = Test-ApiCall -TestName "Send reset code immediately (test cooldown clearing)" -Url "$BaseUrl/auth/forgot-password" -Body (@{
|
||||
identifier = $userEmail
|
||||
} | ConvertTo-Json)
|
||||
|
||||
if ($result7 -and $result7.success) {
|
||||
$testResults.CooldownClearing = $true
|
||||
Write-Host "✅ PASS: Cooldown clearing working" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "❌ FAIL: Cooldown not cleared properly" -ForegroundColor Red
|
||||
}
|
||||
} else {
|
||||
Write-Host "❌ FAIL: Password reset failed" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $SkipThrottleTest) {
|
||||
Write-Host "`n" + "="*60 -ForegroundColor Cyan
|
||||
Write-Host "🧪 Test Suite 5: Throttle Protection" -ForegroundColor Cyan
|
||||
Write-Host "="*60 -ForegroundColor Cyan
|
||||
|
||||
$successCount = 0
|
||||
$throttleCount = 0
|
||||
|
||||
Write-Host "Testing throttle limits (making 12 registration requests)..." -ForegroundColor Yellow
|
||||
|
||||
for ($i = 1; $i -le 12; $i++) {
|
||||
$result = Test-ApiCall -TestName "Registration attempt $i" -Url "$BaseUrl/auth/register" -Body (@{
|
||||
username = "throttle_test_$i"
|
||||
password = "password123"
|
||||
nickname = "Throttle Test $i"
|
||||
} | ConvertTo-Json) -Silent
|
||||
|
||||
if ($result -and $result.success) {
|
||||
$successCount++
|
||||
Write-Host " Request $i`: ✅ Success" -ForegroundColor Green
|
||||
} else {
|
||||
$throttleCount++
|
||||
Write-Host " Request $i`: 🚦 Throttled" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Start-Sleep -Milliseconds 100
|
||||
}
|
||||
|
||||
Write-Host "`nThrottle Results: $successCount success, $throttleCount throttled" -ForegroundColor Cyan
|
||||
|
||||
if ($successCount -ge 8 -and $throttleCount -ge 1) {
|
||||
$testResults.ThrottleProtection = $true
|
||||
Write-Host "✅ PASS: Throttle protection working" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "❌ FAIL: Throttle protection not working properly" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "`n🎯 Test Results Summary" -ForegroundColor Green
|
||||
Write-Host "=======================" -ForegroundColor Green
|
||||
|
||||
$passCount = 0
|
||||
$totalTests = 0
|
||||
|
||||
foreach ($test in $testResults.GetEnumerator()) {
|
||||
$totalTests++
|
||||
if ($test.Value) {
|
||||
$passCount++
|
||||
Write-Host "✅ $($test.Key): PASS" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "❌ $($test.Key): FAIL" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "`n📊 Overall Result: $passCount/$totalTests tests passed" -ForegroundColor $(if ($passCount -eq $totalTests) { "Green" } else { "Yellow" })
|
||||
|
||||
if ($passCount -eq $totalTests) {
|
||||
Write-Host "🎉 All tests passed! API is working correctly." -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "⚠️ Some tests failed. Please check the implementation." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host "`n💡 Usage Tips:" -ForegroundColor Cyan
|
||||
Write-Host " • Use -SkipThrottleTest to skip throttle testing" -ForegroundColor White
|
||||
Write-Host " • Use -SkipCooldownTest to skip cooldown testing" -ForegroundColor White
|
||||
Write-Host " • Check server logs for detailed error information" -ForegroundColor White
|
||||
Write-Host " • For production testing: .\test-comprehensive.ps1 -BaseUrl 'https://your-server.com'" -ForegroundColor White
|
||||
|
||||
Write-Host "`n📋 Test Coverage:" -ForegroundColor Cyan
|
||||
Write-Host " ✓ Application Status & Health Check" -ForegroundColor White
|
||||
Write-Host " ✓ User Registration & Login Flow" -ForegroundColor White
|
||||
Write-Host " ✓ Email Verification & Conflict Detection" -ForegroundColor White
|
||||
Write-Host " ✓ Verification Code Login" -ForegroundColor White
|
||||
Write-Host " ✓ Password Reset Flow" -ForegroundColor White
|
||||
Write-Host " ✓ Cooldown Time Clearing" -ForegroundColor White
|
||||
Write-Host " ✓ Rate Limiting & Throttle Protection" -ForegroundColor White
|
||||
@@ -1,15 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "Node16",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2020"],
|
||||
"moduleResolution": "node16",
|
||||
"moduleResolution": "node",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"noImplicitAny": false,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
@@ -20,5 +21,5 @@
|
||||
"typeRoots": ["./node_modules/@types"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"exclude": ["node_modules", "dist", "client"]
|
||||
}
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
const http = require('http');
|
||||
const crypto = require('crypto');
|
||||
const { exec } = require('child_process');
|
||||
|
||||
// 配置 - 复制此文件为 webhook-handler.js 并修改配置
|
||||
const PORT = 9000;
|
||||
const SECRET = 'your_webhook_secret_change_this'; // 与 Gitea 中配置的密钥一致
|
||||
const DEPLOY_SCRIPT = '/var/www/pixel-game-server/deploy.sh'; // 修改为实际路径
|
||||
|
||||
// 验证 Gitea 签名
|
||||
function verifySignature(payload, signature, secret) {
|
||||
const hmac = crypto.createHmac('sha256', secret);
|
||||
hmac.update(payload);
|
||||
const calculatedSignature = hmac.digest('hex');
|
||||
return crypto.timingSafeEqual(
|
||||
Buffer.from(signature, 'hex'),
|
||||
Buffer.from(calculatedSignature, 'hex')
|
||||
);
|
||||
}
|
||||
|
||||
// 创建 HTTP 服务器
|
||||
const server = http.createServer((req, res) => {
|
||||
if (req.method !== 'POST') {
|
||||
res.writeHead(405, { 'Content-Type': 'text/plain' });
|
||||
res.end('Method Not Allowed');
|
||||
return;
|
||||
}
|
||||
|
||||
let body = '';
|
||||
req.on('data', chunk => {
|
||||
body += chunk.toString();
|
||||
});
|
||||
|
||||
req.on('end', () => {
|
||||
try {
|
||||
// 验证签名
|
||||
const signature = req.headers['x-gitea-signature'];
|
||||
if (!signature || !verifySignature(body, signature.replace('sha256=', ''), SECRET)) {
|
||||
console.log('签名验证失败');
|
||||
res.writeHead(401, { 'Content-Type': 'text/plain' });
|
||||
res.end('Unauthorized');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = JSON.parse(body);
|
||||
|
||||
// 检查是否是推送到 main 分支
|
||||
if (payload.ref === 'refs/heads/main') {
|
||||
console.log('收到 main 分支推送,开始部署...');
|
||||
|
||||
// 执行部署脚本
|
||||
exec(`bash ${DEPLOY_SCRIPT}`, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error('部署失败:', error);
|
||||
console.error('stderr:', stderr);
|
||||
} else {
|
||||
console.log('部署成功:', stdout);
|
||||
}
|
||||
});
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end('Deployment triggered');
|
||||
} else {
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end('Not main branch, ignored');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('处理 webhook 失败:', error);
|
||||
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
||||
res.end('Internal Server Error');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`Webhook 处理器运行在端口 ${PORT}`);
|
||||
});
|
||||
|
||||
// 优雅关闭
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('收到 SIGTERM,正在关闭服务器...');
|
||||
server.close(() => {
|
||||
console.log('服务器已关闭');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user