Merge pull request 'main' (#2) from main into zulip_dev

Reviewed-on: ANGJustinl/whale-town-end#2
This commit is contained in:
2026-01-05 17:43:18 +08:00
74 changed files with 6335 additions and 1137 deletions

View File

@@ -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"]

View File

@@ -124,29 +124,48 @@ pnpm run dev
### 第二步:熟悉项目架构 🏗️ ### 第二步:熟悉项目架构 🏗️
**📁 项目文件结构总览**
``` ```
项目根目录/ whale-town-end/ # 🐋 项目根目录
├── src/ # 源代码目录 ├── 📂 src/ # 源代码目录
│ ├── business/ # 业务功能模块(按功能组织) │ ├── 📂 business/ # 🎯 业务功能模块(按功能组织)
│ │ ├── auth/ # 🔐 用户认证模块 │ │ ├── 📂 auth/ # 🔐 用户认证模块
│ │ ├── user-mgmt/ # 👥 用户管理模块 │ │ ├── 📂 user-mgmt/ # 👥 用户管理模块
│ │ ├── admin/ # 🛡️ 管理员模块 │ │ ├── 📂 admin/ # 🛡️ 管理员模块
│ │ ├── security/ # 🔒 安全模块 │ │ ├── 📂 security/ # 🔒 安全防护模块
│ │ ── shared/ # 🔗 共享组件 │ │ ── 📂 zulip/ # 💬 Zulip集成模块
├── core/ # 核心技术服务 │ └── 📂 shared/ # 🔗 共享业务组件
│ ├── db/ # 数据库层支持MySQL/内存双模式) │ ├── 📂 core/ # ⚙️ 核心技术服务
│ │ ├── redis/ # Redis缓存服务支持真实Redis/文件存储 │ │ ├── 📂 db/ # 🗄️ 数据库层MySQL/内存双模式
│ │ ├── login_core/ # 登录核心服务 │ │ ├── 📂 redis/ # 🔴 Redis缓存真实Redis/文件存储)
│ │ ├── admin_core/ # 管理员核心服务 │ │ ├── 📂 login_core/ # 🔑 登录核心服务
│ │ ── utils/ # 工具服务(邮件、验证码、日志) │ │ ── 📂 admin_core/ # 👑 管理员核心服务
│ ├── app.module.ts # 应用主模块 │ ├── 📂 zulip/ # 💬 Zulip核心服务
│ └── main.ts # 应用入口 │ └── 📂 utils/ # 🛠️ 工具服务(邮件、验证码、日志)
├── client/ # 前端管理界面 │ ├── 📄 app.module.ts # 🏠 应用主模块
├── docs/ # 项目文档 │ └── 📄 main.ts # 🚀 应用入口点
├── test/ # 测试文件 ├── 📂 client/ # 🎨 前端管理界面
├── redis-data/ # Redis文件存储数据 │ ├── 📂 src/ # 前端源码
├── logs/ # 日志文件 │ ├── 📂 dist/ # 前端构建产物
└── 配置文件 # .env, package.json, tsconfig.json等 │ ├── 📄 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 # 📖 项目主文档(当前文件)
``` ```
**架构特点:** **架构特点:**

View File

@@ -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 "警告:服务健康检查失败"

View File

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

View File

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

View File

@@ -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 层 🌐 API接口层
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ HTTP API │ │ WebSocket │ │ GraphQL │ │ 🔗 REST API │ │ 🔌 WebSocket │ │ 📄 Swagger UI
│ │ (REST) │ │ (Socket.IO) │ │ (预留) │ │ │ │ (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 # 用户状态管理接口
│ (Login) │ │ (Game) │ (Social) │ │ ├── 📂 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 # 模块定义
│ (Email) │ │ (Verification)│ │ (Logger) │ │ ├── 📄 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配置
```
---
## 🏗️ 分层架构设计
### 📊 架构分层说明
```
┌─────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────┐
数据访问层 🌐 表现层 (Presentation)
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 用户数据 │ │ Redis缓存 │ │ 文件存储 │ │ │ │ Controllers │ │ WebSocket │ │ Swagger UI │ │
│ │ (Users) │ │ (Cache) │ │ (Files) │ │ │ │ (HTTP接口) │ │ Gateways │ │ (API文档) │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
⬇️
┌─────────────────────────────────────────────────────────────┐
│ 🎯 业务层 (Business) │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Auth Module │ │ UserMgmt │ │ Admin Module │ │
│ │ (用户认证) │ │ Module │ │ (管理员) │ │
│ │ │ │ (用户管理) │ │ │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Security Module │ │ Zulip Module │ │ Shared Module │ │
│ │ (安全防护) │ │ (Zulip集成) │ │ (共享组件) │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
⬇️
┌─────────────────────────────────────────────────────────────┐
│ ⚙️ 服务层 (Service) │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Login Core │ │ Admin Core │ │ Zulip Core │ │
│ │ (登录核心) │ │ (管理员核心) │ │ (Zulip核心) │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Email Service │ │ Verification │ │ Logger Service │ │
│ │ (邮件服务) │ │ Service │ │ (日志服务) │ │
│ │ │ │ (验证码服务) │ │ │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
⬇️
┌─────────────────────────────────────────────────────────────┐
│ 🗄️ 数据层 (Data) │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Users Service │ │ Redis Service │ │ File Storage │ │
│ │ (用户数据) │ │ (缓存服务) │ │ (文件存储) │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ MySQL/Memory │ │ Redis/File │ │ Logs/Data │ │
│ │ (数据库) │ │ (缓存实现) │ │ (日志数据) │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────┘
``` ```
## 模块依赖关系 ### 🔄 数据流向
#### 用户登录流程示例
``` ```
AppModule 1. 📱 用户请求 → LoginController.login()
├── ConfigModule (全局配置) 2. 🔍 参数验证 → class-validator装饰器
├── LoggerModule (日志系统) 3. 🎯 业务逻辑 → LoginService.login()
├── RedisModule (缓存服务) 4. ⚙️ 核心服务 → LoginCoreService.validateUser()
├── UsersModule (用户管理) 5. 📧 发送验证码 → VerificationService.generate()
│ ├── UsersService (数据库模式) 6. 💾 存储数据 → UsersService.findByEmail() + RedisService.set()
│ └── UsersMemoryService (内存模式) 7. 📝 记录日志 → LoggerService.log()
├── EmailModule (邮件服务) 8. ✅ 返回响应 → 用户收到登录结果
├── VerificationModule (验证码服务)
├── LoginCoreModule (登录核心)
└── LoginModule (登录业务)
``` ```
## 数据流向 #### 管理员操作流程示例
### 用户注册流程
``` ```
1. 用户请求 → LoginController 1. 🛡️ 管理员请求 → AdminController.resetUserPassword()
2. 参数验证 → LoginService 2. 🔐 权限验证 → AdminGuard.canActivate()
3. 发送验证码 → LoginCoreService 3. 🎯 业务逻辑 → AdminService.resetPassword()
4. 生成验证码 → VerificationService 4. ⚙️ 核心服务 → AdminCoreService.resetUserPassword()
5. 发送邮件 → EmailService 5. 🔑 密码加密 → bcrypt.hash()
6. 存储验证码 → RedisService 6. 💾 更新数据 → UsersService.update()
7. 返回响应 → 用户 7. 📧 通知用户 → EmailService.sendPasswordReset()
8. 📝 审计日志 → LoggerService.audit()
9. ✅ 返回响应 → 管理员收到操作结果
``` ```
### 双模式架构 ---
项目支持开发测试模式和生产部署模式的无缝切换: ## 🔄 双模式架构
#### 开发测试模式 ### 🎯 设计目标
- **数据库**: 内存存储 (UsersMemoryService)
- **缓存**: 文件存储 (FileRedisService)
- **邮件**: 控制台输出 (测试模式)
- **优势**: 无需外部依赖,快速启动测试
#### 生产部署模式 - **开发测试**: 零依赖快速启动无需安装MySQL、Redis等外部服务
- **数据库**: MySQL (UsersService + TypeORM) - **生产部署**: 高性能、高可用,支持集群和负载均衡
- **缓存**: Redis (RealRedisService + IORedis)
- **邮件**: SMTP服务器 (生产模式)
- **优势**: 高性能,高可用,数据持久化
## 设计原则 ### 📊 模式对比
### 1. 单一职责原则 | 功能模块 | 🧪 开发测试模式 | 🚀 生产部署模式 |
每个模块只负责一个特定的功能领域: |----------|----------------|----------------|
- `LoginModule`: 只处理登录相关业务 | **数据库** | 内存存储 (UsersMemoryService) | MySQL (UsersService + TypeORM) |
- `EmailModule`: 只处理邮件发送 | **缓存** | 文件存储 (FileRedisService) | Redis (RealRedisService + IORedis) |
- `VerificationModule`: 只处理验证码逻辑 | **邮件** | 控制台输出 (测试模式) | SMTP服务器 (生产模式) |
| **日志** | 控制台 + 文件 | 结构化日志 + 日志轮转 |
| **配置** | `.env` 默认配置 | 环境变量 + 配置中心 |
### 2. 依赖注入 ### ⚙️ 模式切换配置
使用NestJS的依赖注入系统
- 接口抽象: `IRedisService`, `IUsersService`
- 实现切换: 根据配置自动选择实现类
- 测试友好: 易于Mock和单元测试
### 3. 配置驱动 #### 开发测试模式 (.env)
通过环境变量控制行为:
- `USE_FILE_REDIS`: 选择Redis实现
- `DB_HOST`: 数据库连接配置
- `EMAIL_HOST`: 邮件服务配置
### 4. 错误处理
统一的错误处理机制:
- HTTP异常: `BadRequestException`, `UnauthorizedException`
- 业务异常: 自定义异常类
- 日志记录: 结构化错误日志
## 扩展指南
### 添加新的业务模块
1. **创建业务模块**
```bash ```bash
# 数据存储模式
USE_FILE_REDIS=true # 使用文件存储代替Redis
NODE_ENV=development # 开发环境
# 数据库配置(注释掉,使用内存数据库)
# 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 # 生产环境
# 数据库配置
DB_HOST=your_mysql_host
DB_PORT=3306
DB_USERNAME=your_username
DB_PASSWORD=your_password
DB_DATABASE=whale_town
# Redis配置
REDIS_HOST=your_redis_host
REDIS_PORT=6379
REDIS_PASSWORD=your_redis_password
# 邮件配置
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USER=your_email@gmail.com
EMAIL_PASS=your_app_password
```
### 🔧 实现机制
#### 依赖注入切换
```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 {}
```
#### 配置驱动服务选择
```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 {}
```
---
## 📦 模块依赖关系
### 🏗️ 模块依赖图
```
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 (共享组件)
```
### 🔄 模块交互流程
#### 用户认证流程
```
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 module business/game
nest g controller business/game nest g controller business/game
nest g service business/game nest g service business/game
``` ```
2. **创建核心服务** #### 2. 实现业务逻辑
```typescript
// src/business/game/game.module.ts
@Module({
imports: [
GameCoreModule, #
UsersModule, #
RedisModule, #
],
controllers: [GameController],
providers: [GameService],
exports: [GameService],
})
export class GameModule {}
```
#### 3. 创建对应的核心服务
```bash ```bash
# 创建核心服务
mkdir -p src/core/game_core
nest g module core/game_core nest g module core/game_core
nest g service core/game_core nest g service core/game_core
``` ```
3. **添加数据模型** #### 4. 更新主模块
```bash ```typescript
nest g module core/db/games // src/app.module.ts
nest g service core/db/games @Module({
imports: [
// ... 其他模块
GameModule, #
],
})
export class AppModule {}
``` ```
4. **更新主模块** ### 🛠️ 添加新的工具服务
在 `app.module.ts` 中导入新模块
### 添加新的工具服务 #### 1. 创建工具服务
1. **创建工具模块**
```bash ```bash
mkdir -p src/core/utils/notification
nest g module core/utils/notification nest g module core/utils/notification
nest g service core/utils/notification nest g service core/utils/notification
``` ```
2. **实现服务接口** #### 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. **添加配置支持** #### 3. 实现服务
在环境变量中添加相关配置 ```typescript
// src/core/utils/notification/notification.service.ts
@Injectable()
export class NotificationService implements INotificationService {
async sendPush(userId: string, message: string): Promise<void> {
// 实现推送通知逻辑
}
4. **编写测试用例** 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 {}
```
### 1. 缓存策略 ### 🔌 添加新的API接口
- **Redis缓存**: 验证码、会话信息
#### 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缓存**: 静态资源文件 - **CDN缓存**: 静态资源文件
### 2. 数据库优化 #### 2. 数据库优化
- **连接池**: 复用数据库连接 - **连接池**: 复用数据库连接,减少连接开销
- **索引优化**: 关键字段建立索引 - **索引优化**: 为查询字段建立合适的索引
- **查询优化**: 避免N+1查询问题 - **查询优化**: 避免N+1查询使用JOIN优化关联查询
### 3. 日志优化 #### 3. 日志优化
- **异步日志**: 使用Pino的异步写入 - **异步日志**: 使用Pino的异步写入功能
- **日志分级**: 生产环境只记录必要日志 - **日志分级**: 生产环境只记录ERROR和WARN级别
- **日志轮转**: 自动清理过期日志文件 - **日志轮转**: 自动清理过期日志文件
## 安全考虑 ### 🔒 安全加固建议
### 1. 数据验证 #### 1. 数据验证
- **输入验证**: class-validator装饰器 - **输入验证**: 使用class-validator进行严格验证
- **类型检查**: TypeScript静态类型 - **类型检查**: TypeScript静态类型检查
- **SQL注入**: TypeORM参数化查询 - **SQL注入防护**: TypeORM参数化查询
### 2. 认证授权 #### 2. 认证授权
- **密码加密**: bcrypt哈希算法 - **密码安全**: bcrypt加密,强密码策略
- **会话管理**: Redis存储会话信息 - **会话管理**: JWT + Redis会话存储
- **权限控制**: 基于角色的访问控制 - **权限控制**: 基于角色的访问控制(RBAC)
### 3. 通信安全 #### 3. 通信安全
- **HTTPS**: 生产环境强制HTTPS - **HTTPS**: 生产环境强制HTTPS
- **CORS**: 跨域请求控制 - **CORS**: 严格的跨域请求控制
- **Rate Limiting**: API请求频率限制 - **Rate Limiting**: API请求频率限制
---
**🏗️ 通过清晰的架构设计Whale Town 实现了高内聚、低耦合的模块化架构,支持快速开发和灵活部署!**

View File

@@ -9,18 +9,22 @@
**moyin** - 主要维护者 **moyin** - 主要维护者
- Gitea: [@moyin](https://gitea.xinghangee.icu/moyin) - Gitea: [@moyin](https://gitea.xinghangee.icu/moyin)
- Email: xinghang_a@proton.me - Email: xinghang_a@proton.me
- 提交数: **66 commits** - 提交数: **112 commits**
- 主要贡献: - 主要贡献:
- 🚀 项目架构设计与初始化 - 🚀 项目架构设计与初始化
- 🔐 完整用户认证系统实现 - 🔐 完整用户认证系统实现
- 📧 邮箱验证系统设计与开发 - 📧 邮箱验证系统设计与开发
- 🗄️ Redis缓存服务文件存储+真实Redis双模式 - 🗄️ Redis缓存服务文件存储+真实Redis双模式
- 📝 完整的API文档系统Swagger UI + OpenAPI - 📝 完整的API文档系统Swagger UI + OpenAPI
- 🧪 测试框架搭建与114个测试用例编写 - 🧪 测试框架搭建与507个测试用例编写
- 📊 高性能日志系统集成Pino - 📊 高性能日志系统集成Pino
- 🔧 项目配置优化与部署方案 - 🔧 项目配置优化与部署方案
- 🐛 验证码TTL重置关键问题修复 - 🐛 验证码TTL重置关键问题修复
- 📚 完整的项目文档体系建设 - 📚 完整的项目文档体系建设
- 🏗️ **Zulip模块架构重构** - 业务功能模块化架构设计与实现
- 📖 **架构文档重写** - 详细的架构设计文档和开发者指南
- 🔄 **验证码冷却时间优化** - 自动清除机制设计与实现
- 📋 **文档清理优化** - 项目文档结构化整理和维护体系建立
### 🌟 核心开发者 ### 🌟 核心开发者
@@ -28,18 +32,21 @@
- Gitea: [@ANGJustinl](https://gitea.xinghangee.icu/ANGJustinl) - Gitea: [@ANGJustinl](https://gitea.xinghangee.icu/ANGJustinl)
- GitHub: [@ANGJustinl](https://github.com/ANGJustinl) - GitHub: [@ANGJustinl](https://github.com/ANGJustinl)
- Email: 96008766+ANGJustinl@users.noreply.github.com - Email: 96008766+ANGJustinl@users.noreply.github.com
- 提交数: **2 commits** - 提交数: **7 commits**
- 主要贡献: - 主要贡献:
- 🔄 邮箱验证流程重构与优化 - 🔄 邮箱验证流程重构与优化
- 💾 基于内存的用户服务实现 - 💾 基于内存的用户服务实现
- 🛠️ API响应处理改进 - 🛠️ API响应处理改进
- 🧪 测试用例完善与错误修复 - 🧪 测试用例完善与错误修复
- 📚 系统架构优化 - 📚 系统架构优化
- 💬 **Zulip集成系统** - 完整的Zulip实时通信系统开发
- 🔧 **E2E测试修复** - Zulip集成的端到端测试优化
- 🎯 **验证码登录测试** - 验证码登录功能测试用例编写
**jianuo** - 核心开发者 **jianuo** - 核心开发者
- Gitea: [@jianuo](https://gitea.xinghangee.icu/jianuo) - Gitea: [@jianuo](https://gitea.xinghangee.icu/jianuo)
- Email: 32106500027@e.gzhu.edu.cn - Email: 32106500027@e.gzhu.edu.cn
- 提交数: **6 commits** - 提交数: **11 commits**
- 主要贡献: - 主要贡献:
- 🎛️ **管理员后台系统** - 完整的前后端管理界面开发 - 🎛️ **管理员后台系统** - 完整的前后端管理界面开发
- 📊 **日志管理功能** - 运行时日志查看与下载系统 - 📊 **日志管理功能** - 运行时日志查看与下载系统
@@ -48,14 +55,42 @@
- ⚙️ **TypeScript配置优化** - Node16模块解析配置 - ⚙️ **TypeScript配置优化** - Node16模块解析配置
- 🐳 **Docker部署优化** - 容器化部署问题修复 - 🐳 **Docker部署优化** - 容器化部署问题修复
- 📖 **技术栈文档更新** - 项目技术栈说明完善 - 📖 **技术栈文档更新** - 项目技术栈说明完善
- 🔧 **项目配置优化** - 构建和开发环境配置改进
## 贡献统计 ## 贡献统计
| 贡献者 | 提交数 | 主要领域 | 贡献占比 | | 贡献者 | 提交数 | 主要领域 | 贡献占比 |
|--------|--------|----------|----------| |--------|--------|----------|----------|
| moyin | 66 | 架构设计、核心功能、文档、测试 | 88% | | moyin | 112 | 架构设计、核心功能、文档、测试、Zulip重构 | 86% |
| jianuo | 6 | 管理员后台、日志系统、部署优化 | 8% | | jianuo | 11 | 管理员后台、日志系统、部署优化、配置管理 | 8% |
| angjustinl | 2 | 功能优化、测试、重构 | 3% | | 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月20日**: jianuo完善日志管理功能
- **12月21日**: jianuo添加管理员后台单元测试 - **12月21日**: jianuo添加管理员后台单元测试
- **12月22日**: 管理员后台功能合并到主分支 - **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个测试用例通过测试覆盖率达到新高
## 如何成为贡献者 ## 如何成为贡献者

View File

@@ -27,7 +27,7 @@
### 📋 **项目管理** ### 📋 **项目管理**
- [贡献指南](CONTRIBUTORS.md) - 如何参与项目贡献 - [贡献指南](CONTRIBUTORS.md) - 如何参与项目贡献
- [文档清理说明](DOCUMENT_CLEANUP.md) - 文档维护记录 - [文档清理说明](DOCUMENT_CLEANUP.md) - 文档维护和优化记录
## 🏗️ **文档结构说明** ## 🏗️ **文档结构说明**

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 KiB

View File

@@ -1,3 +1,7 @@
![alt text](ab164782cdc17e22f9bdf443c7e1e96c.png)
# Git 提交规范 # Git 提交规范
本文档定义了项目的 Git 提交信息格式规范,以保持提交历史的清晰和一致性。 本文档定义了项目的 Git 提交信息格式规范,以保持提交历史的清晰和一致性。

View File

@@ -5,7 +5,7 @@
### 连接地址 ### 连接地址
``` ```
ws://localhost:3000/game wss://localhost:3000/game
``` ```
### 连接参数 ### 连接参数

View 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();

View File

@@ -3,6 +3,12 @@
"collection": "@nestjs/schematics", "collection": "@nestjs/schematics",
"sourceRoot": "src", "sourceRoot": "src",
"compilerOptions": { "compilerOptions": {
"deleteOutDir": true "deleteOutDir": true,
"assets": [
{
"include": "../config/**/*",
"outDir": "./dist"
}
]
} }
} }

View File

@@ -8,12 +8,13 @@ import { LoggerModule } from './core/utils/logger/logger.module';
import { UsersModule } from './core/db/users/users.module'; import { UsersModule } from './core/db/users/users.module';
import { LoginCoreModule } from './core/login_core/login_core.module'; import { LoginCoreModule } from './core/login_core/login_core.module';
import { AuthModule } from './business/auth/auth.module'; import { AuthModule } from './business/auth/auth.module';
import { ZulipModule } from './business/zulip/zulip.module';
import { RedisModule } from './core/redis/redis.module'; import { RedisModule } from './core/redis/redis.module';
import { AdminModule } from './business/admin/admin.module'; import { AdminModule } from './business/admin/admin.module';
import { UserMgmtModule } from './business/user-mgmt/user-mgmt.module'; import { UserMgmtModule } from './business/user-mgmt/user-mgmt.module';
import { SecurityModule } from './business/security/security.module'; import { SecurityCoreModule } from './core/security_core/security_core.module';
import { MaintenanceMiddleware } from './business/security/middleware/maintenance.middleware'; import { MaintenanceMiddleware } from './core/security_core/middleware/maintenance.middleware';
import { ContentTypeMiddleware } from './business/security/middleware/content-type.middleware'; import { ContentTypeMiddleware } from './core/security_core/middleware/content_type.middleware';
/** /**
* 检查数据库配置是否完整 by angjustinl 2025-12-17 * 检查数据库配置是否完整 by angjustinl 2025-12-17
@@ -67,9 +68,10 @@ function isDatabaseConfigured(): boolean {
isDatabaseConfigured() ? UsersModule.forDatabase() : UsersModule.forMemory(), isDatabaseConfigured() ? UsersModule.forDatabase() : UsersModule.forMemory(),
LoginCoreModule, LoginCoreModule,
AuthModule, AuthModule,
ZulipModule,
UserMgmtModule, UserMgmtModule,
AdminModule, AdminModule,
SecurityModule, SecurityCoreModule,
], ],
controllers: [AppController], controllers: [AppController],
providers: [ providers: [

View File

@@ -25,7 +25,7 @@ import {
AdminUserResponseDto, AdminUserResponseDto,
AdminRuntimeLogsResponseDto AdminRuntimeLogsResponseDto
} from './dto/admin-response.dto'; } 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 type { Response } from 'express';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';

View File

@@ -16,11 +16,19 @@ import { Module } from '@nestjs/common';
import { LoginController } from './controllers/login.controller'; import { LoginController } from './controllers/login.controller';
import { LoginService } from './services/login.service'; import { LoginService } from './services/login.service';
import { LoginCoreModule } from '../../core/login_core/login_core.module'; 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({ @Module({
imports: [LoginCoreModule], imports: [
LoginCoreModule,
ZulipCoreModule,
ZulipAccountsModule.forRoot(),
],
controllers: [LoginController], controllers: [LoginController],
providers: [LoginService], providers: [
LoginService,
],
exports: [LoginService], exports: [LoginService],
}) })
export class AuthModule {} export class AuthModule {}

View File

@@ -33,8 +33,8 @@ import {
TestModeEmailVerificationResponseDto, TestModeEmailVerificationResponseDto,
SuccessEmailVerificationResponseDto SuccessEmailVerificationResponseDto
} from '../dto/login_response.dto'; } from '../dto/login_response.dto';
import { Throttle, ThrottlePresets } from '../../security/decorators/throttle.decorator'; import { Throttle, ThrottlePresets } from '../../../core/security_core/decorators/throttle.decorator';
import { Timeout, TimeoutPresets } from '../../security/decorators/timeout.decorator'; import { Timeout, TimeoutPresets } from '../../../core/security_core/decorators/timeout.decorator';
@ApiTags('auth') @ApiTags('auth')
@Controller('auth') @Controller('auth')

View File

@@ -16,9 +16,12 @@
* @since 2025-12-17 * @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 { LoginCoreService, LoginRequest, RegisterRequest, GitHubOAuthRequest, PasswordResetRequest, AuthResult, VerificationCodeLoginRequest } from '../../../core/login_core/login_core.service';
import { Users } from '../../../core/db/users/users.entity'; 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( constructor(
private readonly loginCoreService: LoginCoreService, 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 注册响应 * @returns 注册响应
*/ */
async register(registerRequest: RegisterRequest): Promise<ApiResponse<LoginResponse>> { async register(registerRequest: RegisterRequest): Promise<ApiResponse<LoginResponse>> {
const startTime = Date.now();
try { try {
this.logger.log(`用户注册尝试: ${registerRequest.username}`); this.logger.log(`用户注册尝试: ${registerRequest.username}`);
// 调用核心服务进行注册 // 1. 初始化Zulip管理员客户端
await this.initializeZulipAdminClient();
// 2. 调用核心服务进行注册
const authResult = await this.loginCoreService.register(registerRequest); 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); const accessToken = this.generateAccessToken(authResult.user);
// 格式化响应数据 // 5. 格式化响应数据
const response: LoginResponse = { const response: LoginResponse = {
user: this.formatUserInfo(authResult.user), user: this.formatUserInfo(authResult.user),
access_token: accessToken, access_token: accessToken,
is_new_user: true, 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 { return {
success: true, success: true,
data: response, data: response,
message: '注册成功' message: response.message
}; };
} catch (error) { } 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 { return {
success: false, success: false,
message: error instanceof Error ? error.message : '注册失败', message: err.message || '注册失败',
error_code: 'REGISTER_FAILED' 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;
}
}
} }

View 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 }
);
});
});
});

View File

@@ -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 { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { AdminGuard } from '../../admin/guards/admin.guard'; import { AdminGuard } from '../../admin/guards/admin.guard';
import { UserManagementService } from '../services/user-management.service'; import { UserManagementService } from '../services/user-management.service';
import { Throttle, ThrottlePresets } from '../../security/decorators/throttle.decorator'; import { Throttle, ThrottlePresets } from '../../../core/security_core/decorators/throttle.decorator';
import { Timeout, TimeoutPresets } from '../../security/decorators/timeout.decorator'; import { Timeout, TimeoutPresets } from '../../../core/security_core/decorators/timeout.decorator';
import { UserStatusDto, BatchUserStatusDto } from '../dto/user-status.dto'; import { UserStatusDto, BatchUserStatusDto } from '../dto/user-status.dto';
import { UserStatusResponseDto, BatchUserStatusResponseDto, UserStatusStatsResponseDto } from '../dto/user-status-response.dto'; import { UserStatusResponseDto, BatchUserStatusResponseDto, UserStatusStatsResponseDto } from '../dto/user-status-response.dto';

View 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调用逻辑
});
});
```
这种架构设计确保了业务逻辑与技术实现的清晰分离,提高了代码的可维护性和可测试性。

View File

@@ -12,8 +12,8 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import * as fc from 'fast-check'; import * as fc from 'fast-check';
import { MessageFilterService, ViolationType, ContentFilterResult } from './message-filter.service'; import { MessageFilterService, ViolationType } from './message_filter.service';
import { ConfigManagerService } from './config-manager.service'; import { IZulipConfigService } from '../../../core/zulip/interfaces/zulip-core.interfaces';
import { AppLoggerService } from '../../../core/utils/logger/logger.service'; import { AppLoggerService } from '../../../core/utils/logger/logger.service';
import { IRedisService } from '../../../core/redis/redis.interface'; import { IRedisService } from '../../../core/redis/redis.interface';
@@ -21,7 +21,7 @@ describe('MessageFilterService', () => {
let service: MessageFilterService; let service: MessageFilterService;
let mockLogger: jest.Mocked<AppLoggerService>; let mockLogger: jest.Mocked<AppLoggerService>;
let mockRedisService: jest.Mocked<IRedisService>; let mockRedisService: jest.Mocked<IRedisService>;
let mockConfigManager: jest.Mocked<ConfigManagerService>; let mockConfigManager: jest.Mocked<IZulipConfigService>;
// 内存存储模拟Redis // 内存存储模拟Redis
let memoryStore: Map<string, { value: string; expireAt?: number }>; let memoryStore: Map<string, { value: string; expireAt?: number }>;
@@ -100,6 +100,14 @@ describe('MessageFilterService', () => {
hasMap: jest.fn().mockImplementation((mapId: string) => { hasMap: jest.fn().mockImplementation((mapId: string) => {
return ['novice_village', 'tavern', 'market'].includes(mapId); 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; } as any;
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
@@ -114,7 +122,7 @@ describe('MessageFilterService', () => {
useValue: mockRedisService, useValue: mockRedisService,
}, },
{ {
provide: ConfigManagerService, provide: 'ZULIP_CONFIG_SERVICE',
useValue: mockConfigManager, useValue: mockConfigManager,
}, },
], ],

View File

@@ -30,7 +30,7 @@
import { Injectable, Logger, Inject, forwardRef } from '@nestjs/common'; import { Injectable, Logger, Inject, forwardRef } from '@nestjs/common';
import { IRedisService } from '../../../core/redis/redis.interface'; 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; category?: string;
} }
/**
*
*
*
* -
* -
* -
* - ConfigManager集成实现位置权限验证
*
*
* - filterContent():
* - checkRateLimit():
* - validatePermission():
* - validateMessage():
* - logViolation():
*
* 使
* -
* -
* -
* -
*/
@Injectable() @Injectable()
export class MessageFilterService { export class MessageFilterService {
private readonly RATE_LIMIT_PREFIX = 'zulip:rate_limit:'; private readonly RATE_LIMIT_PREFIX = 'zulip:rate_limit:';
@@ -127,8 +149,8 @@ export class MessageFilterService {
constructor( constructor(
@Inject('REDIS_SERVICE') @Inject('REDIS_SERVICE')
private readonly redisService: IRedisService, private readonly redisService: IRedisService,
@Inject(forwardRef(() => ConfigManagerService)) @Inject('ZULIP_CONFIG_SERVICE')
private readonly configManager: ConfigManagerService, private readonly configManager: IZulipConfigService,
) { ) {
this.logger.log('MessageFilterService初始化完成'); this.logger.log('MessageFilterService初始化完成');
} }

View 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);
});
});
});

View File

@@ -21,9 +21,9 @@
* @since 2025-12-25 * @since 2025-12-25
*/ */
import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; import { Injectable, Logger, OnModuleInit, OnModuleDestroy, Inject } from '@nestjs/common';
import { SessionManagerService } from './session-manager.service'; import { SessionManagerService } from './session_manager.service';
import { ZulipClientPoolService } from './zulip-client-pool.service'; import { IZulipClientPoolService } from '../../../core/zulip/interfaces/zulip-core.interfaces';
/** /**
* *
@@ -55,6 +55,28 @@ export interface CleanupResult {
error?: string; error?: string;
} }
/**
*
*
*
* -
* - Zulip客户端资源
* -
* -
*
*
* - startCleanup():
* - stopCleanup():
* - performCleanup():
* - getCleanupStats():
* - updateConfig():
*
* 使
* -
* -
* -
* -
*/
@Injectable() @Injectable()
export class SessionCleanupService implements OnModuleInit, OnModuleDestroy { export class SessionCleanupService implements OnModuleInit, OnModuleDestroy {
private cleanupInterval: NodeJS.Timeout | null = null; private cleanupInterval: NodeJS.Timeout | null = null;
@@ -70,7 +92,8 @@ export class SessionCleanupService implements OnModuleInit, OnModuleDestroy {
constructor( constructor(
private readonly sessionManager: SessionManagerService, private readonly sessionManager: SessionManagerService,
private readonly zulipClientPool: ZulipClientPoolService, @Inject('ZULIP_CLIENT_POOL_SERVICE')
private readonly zulipClientPool: IZulipClientPoolService,
) { ) {
this.logger.log('SessionCleanupService初始化完成'); this.logger.log('SessionCleanupService初始化完成');
} }
@@ -176,7 +199,8 @@ export class SessionCleanupService implements OnModuleInit, OnModuleDestroy {
// 2. 注销对应的Zulip事件队列 // 2. 注销对应的Zulip事件队列
let deregisteredQueues = 0; let deregisteredQueues = 0;
for (const queueId of cleanupResult.zulipQueueIds) { const queueIds = cleanupResult?.zulipQueueIds || [];
for (const queueId of queueIds) {
try { try {
// 根据queueId找到对应的用户并注销队列 // 根据queueId找到对应的用户并注销队列
// 注意这里需要通过某种方式找到queueId对应的userId // 注意这里需要通过某种方式找到queueId对应的userId
@@ -200,7 +224,7 @@ export class SessionCleanupService implements OnModuleInit, OnModuleDestroy {
const duration = Date.now() - startTime; const duration = Date.now() - startTime;
const result: CleanupResult = { const result: CleanupResult = {
cleanedSessions: cleanupResult.cleanedCount, cleanedSessions: cleanupResult?.cleanedCount || 0,
deregisteredQueues, deregisteredQueues,
duration, duration,
timestamp: new Date(), timestamp: new Date(),

View File

@@ -12,8 +12,8 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import * as fc from 'fast-check'; import * as fc from 'fast-check';
import { SessionManagerService, GameSession, Position } from './session-manager.service'; import { SessionManagerService, GameSession, Position } from './session_manager.service';
import { ConfigManagerService } from './config-manager.service'; import { IZulipConfigService } from '../../../core/zulip/interfaces/zulip-core.interfaces';
import { AppLoggerService } from '../../../core/utils/logger/logger.service'; import { AppLoggerService } from '../../../core/utils/logger/logger.service';
import { IRedisService } from '../../../core/redis/redis.interface'; import { IRedisService } from '../../../core/redis/redis.interface';
@@ -21,7 +21,7 @@ describe('SessionManagerService', () => {
let service: SessionManagerService; let service: SessionManagerService;
let mockLogger: jest.Mocked<AppLoggerService>; let mockLogger: jest.Mocked<AppLoggerService>;
let mockRedisService: jest.Mocked<IRedisService>; let mockRedisService: jest.Mocked<IRedisService>;
let mockConfigManager: jest.Mocked<ConfigManagerService>; let mockConfigManager: jest.Mocked<IZulipConfigService>;
// 内存存储模拟Redis // 内存存储模拟Redis
let memoryStore: Map<string, { value: string; expireAt?: number }>; let memoryStore: Map<string, { value: string; expireAt?: number }>;
@@ -57,9 +57,15 @@ describe('SessionManagerService', () => {
}; };
return streamMap[mapId] || 'General'; return streamMap[mapId] || 'General';
}), }),
getMapIdByStream: jest.fn(),
getTopicByObject: jest.fn().mockReturnValue('General'), getTopicByObject: jest.fn().mockReturnValue('General'),
getMapConfig: jest.fn(), getZulipConfig: jest.fn(),
getAllMaps: jest.fn(), hasMap: jest.fn(),
hasStream: jest.fn(),
getAllMapIds: jest.fn(),
getAllStreams: jest.fn(),
reloadConfig: jest.fn(),
validateConfig: jest.fn(),
} as any; } as any;
// 创建模拟Redis服务使用内存存储 // 创建模拟Redis服务使用内存存储
@@ -135,7 +141,7 @@ describe('SessionManagerService', () => {
useValue: mockRedisService, useValue: mockRedisService,
}, },
{ {
provide: ConfigManagerService, provide: 'ZULIP_CONFIG_SERVICE',
useValue: mockConfigManager, useValue: mockConfigManager,
}, },
], ],

View File

@@ -35,8 +35,8 @@
import { Injectable, Logger, Inject } from '@nestjs/common'; import { Injectable, Logger, Inject } from '@nestjs/common';
import { IRedisService } from '../../../core/redis/redis.interface'; import { IRedisService } from '../../../core/redis/redis.interface';
import { ConfigManagerService } from './config-manager.service'; import { IZulipConfigService } from '../../../core/zulip/interfaces/zulip-core.interfaces';
import { Internal, Constants } from '../interfaces/zulip.interfaces'; import { Internal, Constants } from '../../../core/zulip/interfaces/zulip.interfaces';
/** /**
* - * -
@@ -78,6 +78,29 @@ export interface SessionStats {
newestSession?: Date; newestSession?: Date;
} }
/**
*
*
*
* - WebSocket连接ID与Zulip队列ID的映射关系
* -
* -
* -
*
*
* - createSession(): Socket_ID与Zulip_Queue_ID
* - getSession():
* - injectContext(): Stream/Topic
* - getSocketsInMap(): Socket
* - updatePlayerPosition():
* - destroySession():
*
* 使
* -
* -
* -
* -
*/
@Injectable() @Injectable()
export class SessionManagerService { export class SessionManagerService {
private readonly SESSION_PREFIX = 'zulip:session:'; private readonly SESSION_PREFIX = 'zulip:session:';
@@ -91,7 +114,8 @@ export class SessionManagerService {
constructor( constructor(
@Inject('REDIS_SERVICE') @Inject('REDIS_SERVICE')
private readonly redisService: IRedisService, private readonly redisService: IRedisService,
private readonly configManager: ConfigManagerService, @Inject('ZULIP_CONFIG_SERVICE')
private readonly configManager: IZulipConfigService,
) { ) {
this.logger.log('SessionManagerService初始化完成'); this.logger.log('SessionManagerService初始化完成');
} }
@@ -170,6 +194,9 @@ export class SessionManagerService {
* @param initialMap * @param initialMap
* @param initialPosition * @param initialPosition
* @returns Promise<GameSession> * @returns Promise<GameSession>
*
* @throws Error
* @throws Error Redis操作失败时
*/ */
async createSession( async createSession(
socketId: string, socketId: string,
@@ -378,6 +405,8 @@ export class SessionManagerService {
* @param socketId WebSocket连接ID * @param socketId WebSocket连接ID
* @param mapId ID * @param mapId ID
* @returns Promise<ContextInfo> * @returns Promise<ContextInfo>
*
* @throws Error
*/ */
async injectContext(socketId: string, mapId?: string): Promise<ContextInfo> { async injectContext(socketId: string, mapId?: string): Promise<ContextInfo> {
this.logger.debug('开始上下文注入', { this.logger.debug('开始上下文注入', {

View File

@@ -24,18 +24,17 @@ import {
ZulipMessage, ZulipMessage,
GameMessage, GameMessage,
MessageDistributor, MessageDistributor,
} from './zulip-event-processor.service'; } from './zulip_event_processor.service';
import { SessionManagerService, GameSession } from './session-manager.service'; import { SessionManagerService, GameSession } from './session_manager.service';
import { ConfigManagerService } from './config-manager.service'; import { IZulipConfigService, IZulipClientPoolService } from '../../../core/zulip/interfaces/zulip-core.interfaces';
import { ZulipClientPoolService } from './zulip-client-pool.service';
import { AppLoggerService } from '../../../core/utils/logger/logger.service'; import { AppLoggerService } from '../../../core/utils/logger/logger.service';
describe('ZulipEventProcessorService', () => { describe('ZulipEventProcessorService', () => {
let service: ZulipEventProcessorService; let service: ZulipEventProcessorService;
let mockLogger: jest.Mocked<AppLoggerService>; let mockLogger: jest.Mocked<AppLoggerService>;
let mockSessionManager: jest.Mocked<SessionManagerService>; let mockSessionManager: jest.Mocked<SessionManagerService>;
let mockConfigManager: jest.Mocked<ConfigManagerService>; let mockConfigManager: jest.Mocked<IZulipConfigService>;
let mockClientPool: jest.Mocked<ZulipClientPoolService>; let mockClientPool: jest.Mocked<IZulipClientPoolService>;
let mockDistributor: jest.Mocked<MessageDistributor>; let mockDistributor: jest.Mocked<MessageDistributor>;
// 创建模拟Zulip消息 // 创建模拟Zulip消息
@@ -87,14 +86,26 @@ describe('ZulipEventProcessorService', () => {
mockConfigManager = { mockConfigManager = {
getMapIdByStream: jest.fn(), getMapIdByStream: jest.fn(),
getStreamByMap: jest.fn(), getStreamByMap: jest.fn(),
getMapConfig: jest.fn(), getTopicByObject: jest.fn(),
getZulipConfig: jest.fn(),
hasMap: jest.fn(),
hasStream: jest.fn(),
getAllMapIds: jest.fn(), getAllMapIds: jest.fn(),
getAllStreams: jest.fn(),
reloadConfig: jest.fn(),
validateConfig: jest.fn(),
} as any; } as any;
mockClientPool = { mockClientPool = {
getUserClient: jest.fn(), getUserClient: jest.fn(),
createUserClient: jest.fn(), createUserClient: jest.fn(),
destroyUserClient: 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; } as any;
mockDistributor = { mockDistributor = {
@@ -114,11 +125,11 @@ describe('ZulipEventProcessorService', () => {
useValue: mockSessionManager, useValue: mockSessionManager,
}, },
{ {
provide: ConfigManagerService, provide: 'ZULIP_CONFIG_SERVICE',
useValue: mockConfigManager, useValue: mockConfigManager,
}, },
{ {
provide: ZulipClientPoolService, provide: 'ZULIP_CLIENT_POOL_SERVICE',
useValue: mockClientPool, useValue: mockClientPool,
}, },
], ],

View File

@@ -31,9 +31,8 @@
*/ */
import { Injectable, OnModuleDestroy, Inject, forwardRef, Logger } from '@nestjs/common'; import { Injectable, OnModuleDestroy, Inject, forwardRef, Logger } from '@nestjs/common';
import { SessionManagerService } from './session-manager.service'; import { SessionManagerService } from './session_manager.service';
import { ConfigManagerService } from './config-manager.service'; import { IZulipConfigService, IZulipClientPoolService } from '../../../core/zulip/interfaces/zulip-core.interfaces';
import { ZulipClientPoolService } from './zulip-client-pool.service';
/** /**
* Zulip消息接口 * Zulip消息接口
@@ -94,6 +93,28 @@ export interface EventProcessingStats {
lastEventTime?: Date; lastEventTime?: Date;
} }
/**
* Zulip事件处理服务类
*
*
* - Zulip接收的事件队列消息
* - Zulip消息转换为游戏协议格式
* -
* -
*
*
* - processEvents(): Zulip事件队列
* - processMessage():
* - startProcessing():
* - stopProcessing():
* - registerQueue():
*
* 使
* - Zulip服务器推送的消息
* - Zulip消息转发给游戏客户端
* -
* -
*/
@Injectable() @Injectable()
export class ZulipEventProcessorService implements OnModuleDestroy { export class ZulipEventProcessorService implements OnModuleDestroy {
private readonly logger = new Logger(ZulipEventProcessorService.name); private readonly logger = new Logger(ZulipEventProcessorService.name);
@@ -109,9 +130,10 @@ export class ZulipEventProcessorService implements OnModuleDestroy {
constructor( constructor(
private readonly sessionManager: SessionManagerService, private readonly sessionManager: SessionManagerService,
private readonly configManager: ConfigManagerService, @Inject('ZULIP_CONFIG_SERVICE')
@Inject(forwardRef(() => ZulipClientPoolService)) private readonly configManager: IZulipConfigService,
private readonly clientPool: ZulipClientPoolService, @Inject('ZULIP_CLIENT_POOL_SERVICE')
private readonly clientPool: IZulipClientPoolService,
) { ) {
this.logger.log('ZulipEventProcessorService初始化完成'); this.logger.log('ZulipEventProcessorService初始化完成');
} }

View File

@@ -2,26 +2,32 @@
* Zulip集成业务模块 * Zulip集成业务模块
* *
* 功能描述: * 功能描述:
* - 整合Zulip集成相关的控制器、服务和依赖 * - 整合Zulip集成相关的业务逻辑和控制器
* - 提供完整的Zulip集成功能模块 * - 提供完整的Zulip集成业务功能模块
* - 实现游戏与Zulip的无缝通信桥梁 * - 实现游戏与Zulip的业务逻辑协调
* - 支持WebSocket网关、会话管理、消息过滤等核心功能 * - 支持WebSocket网关、会话管理、消息过滤等业务功能
* - 启动时自动检查并创建所有地图对应的Zulip Streams
* *
* 核心服务 * 架构设计
* - ZulipService: 主协调服务,处理登录、消息发送等核心业务 * - 业务逻辑层:处理游戏相关的业务规则和流程
* - 核心服务层封装技术实现细节和第三方API调用
* - 通过依赖注入实现业务层与技术层的解耦
*
* 业务服务:
* - ZulipService: 主协调服务,处理登录、消息发送等核心业务流程
* - ZulipWebSocketGateway: WebSocket统一网关处理客户端连接 * - ZulipWebSocketGateway: WebSocket统一网关处理客户端连接
* - ZulipClientPoolService: Zulip客户端池管理 * - SessionManagerService: 会话状态管理和业务逻辑
* - SessionManagerService: 会话状态管理 * - MessageFilterService: 消息过滤和业务规则控制
* - MessageFilterService: 消息过滤和安全控制 *
* 核心服务通过ZulipCoreModule提供
* - ZulipClientService: Zulip REST API封装
* - ZulipClientPoolService: 客户端池管理
* - ConfigManagerService: 配置管理和热重载 * - ConfigManagerService: 配置管理和热重载
* - StreamInitializerService: Stream初始化和自动创建 * - ZulipEventProcessorService: 事件处理和消息转换
* - ErrorHandlerService: 错误处理和服务降级 * - 其他技术支持服务
* - MonitoringService: 系统监控和告警
* - ApiKeySecurityService: API Key安全存储
* *
* 依赖模块: * 依赖模块:
* - LoginModule: 用户认证和会话管理 * - ZulipCoreModule: Zulip核心技术服务
* - LoginCoreModule: 用户认证和会话管理
* - RedisModule: 会话状态缓存 * - RedisModule: 会话状态缓存
* - LoggerModule: 日志记录服务 * - LoggerModule: 日志记录服务
* *
@@ -29,65 +35,47 @@
* - 游戏客户端通过WebSocket连接进行实时聊天 * - 游戏客户端通过WebSocket连接进行实时聊天
* - 游戏内消息与Zulip社群的双向同步 * - 游戏内消息与Zulip社群的双向同步
* - 基于位置的聊天上下文管理 * - 基于位置的聊天上下文管理
* - 系统启动时自动初始化所有地图对应的Streams * - 业务规则驱动的消息过滤和权限控制
* *
* @author angjustinl * @author angjustinl
* @version 1.0.0 * @version 2.0.0
* @since 2025-12-25 * @since 2025-12-31
*/ */
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ZulipWebSocketGateway } from './zulip-websocket.gateway'; import { ZulipWebSocketGateway } from './zulip_websocket.gateway';
import { ZulipService } from './zulip.service'; import { ZulipService } from './zulip.service';
import { ZulipClientService } from './services/zulip-client.service'; import { SessionManagerService } from './services/session_manager.service';
import { ZulipClientPoolService } from './services/zulip-client-pool.service'; import { MessageFilterService } from './services/message_filter.service';
import { SessionManagerService } from './services/session-manager.service'; import { ZulipEventProcessorService } from './services/zulip_event_processor.service';
import { SessionCleanupService } from './services/session-cleanup.service'; import { SessionCleanupService } from './services/session_cleanup.service';
import { MessageFilterService } from './services/message-filter.service'; import { ZulipCoreModule } from '../../core/zulip/zulip-core.module';
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 { RedisModule } from '../../core/redis/redis.module'; import { RedisModule } from '../../core/redis/redis.module';
import { LoggerModule } from '../../core/utils/logger/logger.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({ @Module({
imports: [ imports: [
// Zulip核心服务模块 - 提供技术实现相关的核心服务
ZulipCoreModule,
// Redis模块 - 提供会话状态缓存和数据存储 // Redis模块 - 提供会话状态缓存和数据存储
RedisModule, RedisModule,
// 日志模块 - 提供统一的日志记录服务 // 日志模块 - 提供统一的日志记录服务
LoggerModule, LoggerModule,
// 登录模块 - 提供用户认证和Token验证 // 登录模块 - 提供用户认证和Token验证
LoginModule, LoginCoreModule,
], ],
providers: [ providers: [
// 主协调服务 - 整合各子服务,提供统一业务接口 // 主协调服务 - 整合各子服务,提供统一业务接口
ZulipService, ZulipService,
// Zulip客户端服务 - 封装Zulip REST API调用
ZulipClientService,
// Zulip客户端池服务 - 管理用户专用Zulip客户端实例
ZulipClientPoolService,
// 会话管理服务 - 维护Socket_ID与Zulip_Queue_ID的映射关系 // 会话管理服务 - 维护Socket_ID与Zulip_Queue_ID的映射关系
SessionManagerService, SessionManagerService,
// 会话清理服务 - 定时清理过期会话
SessionCleanupService,
// 消息过滤服务 - 敏感词过滤、频率限制、权限验证 // 消息过滤服务 - 敏感词过滤、频率限制、权限验证
MessageFilterService, MessageFilterService,
// Zulip事件处理服务 - 处理Zulip事件队列消息 // Zulip事件处理服务 - 处理Zulip事件队列消息
ZulipEventProcessorService, ZulipEventProcessorService,
// 配置管理服务 - 地图映射配置和系统配置管理 // 会话清理服务 - 定时清理过期会话
ConfigManagerService, SessionCleanupService,
// Stream初始化服务 - 启动时检查并创建所有地图对应的Streams
StreamInitializerService,
// 错误处理服务 - 错误处理、重试机制、服务降级
ErrorHandlerService,
// 监控服务 - 系统监控、健康检查、告警
MonitoringService,
// API Key安全服务 - API Key加密存储和安全日志
ApiKeySecurityService,
// WebSocket网关 - 处理游戏客户端WebSocket连接 // WebSocket网关 - 处理游戏客户端WebSocket连接
ZulipWebSocketGateway, ZulipWebSocketGateway,
], ],
@@ -95,26 +83,14 @@ import { LoginModule } from '../login/login.module';
exports: [ exports: [
// 导出主服务供其他模块使用 // 导出主服务供其他模块使用
ZulipService, ZulipService,
// 导出Zulip客户端服务
ZulipClientService,
// 导出客户端池服务
ZulipClientPoolService,
// 导出会话管理服务 // 导出会话管理服务
SessionManagerService, SessionManagerService,
// 导出会话清理服务
SessionCleanupService,
// 导出消息过滤服务 // 导出消息过滤服务
MessageFilterService, MessageFilterService,
// 导出配置管理服务 // 导出事件处理服务
ConfigManagerService, ZulipEventProcessorService,
// 导出Stream初始化服务 // 导出会话清理服务
StreamInitializerService, SessionCleanupService,
// 导出错误处理服务
ErrorHandlerService,
// 导出监控服务
MonitoringService,
// 导出API Key安全服务
ApiKeySecurityService,
// 导出WebSocket网关 // 导出WebSocket网关
ZulipWebSocketGateway, ZulipWebSocketGateway,
], ],

File diff suppressed because it is too large Load Diff

View File

@@ -22,14 +22,16 @@
* @since 2025-12-25 * @since 2025-12-25
*/ */
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger, Inject } from '@nestjs/common';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import { ZulipClientPoolService } from './services/zulip-client-pool.service'; import { SessionManagerService } from './services/session_manager.service';
import { SessionManagerService } from './services/session-manager.service'; import { MessageFilterService } from './services/message_filter.service';
import { MessageFilterService } from './services/message-filter.service'; import { ZulipEventProcessorService } from './services/zulip_event_processor.service';
import { ZulipEventProcessorService } from './services/zulip-event-processor.service'; import {
import { ConfigManagerService } from './services/config-manager.service'; IZulipClientPoolService,
import { ErrorHandlerService } from './services/error-handler.service'; 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; error?: string;
} }
/**
* Zulip集成主服务类
*
* 职责:
* - 作为Zulip集成系统的主要协调服务
* - 整合各个子服务,提供统一的业务接口
* - 处理游戏客户端与Zulip之间的核心业务逻辑
* - 管理玩家会话和消息路由
*
* 主要方法:
* - handlePlayerLogin(): 处理玩家登录和Zulip客户端初始化
* - handlePlayerLogout(): 处理玩家登出和资源清理
* - sendChatMessage(): 处理游戏聊天消息发送到Zulip
* - updatePlayerPosition(): 更新玩家位置信息
*
* 使用场景:
* - WebSocket网关调用处理消息路由
* - 会话管理和状态维护
* - 消息格式转换和过滤
* - 游戏与Zulip的双向通信桥梁
*/
@Injectable() @Injectable()
export class ZulipService { export class ZulipService {
private readonly logger = new Logger(ZulipService.name); private readonly logger = new Logger(ZulipService.name);
private readonly DEFAULT_MAP = 'whale_port'; private readonly DEFAULT_MAP = 'whale_port';
constructor( constructor(
private readonly zulipClientPool: ZulipClientPoolService, @Inject('ZULIP_CLIENT_POOL_SERVICE')
private readonly zulipClientPool: IZulipClientPoolService,
private readonly sessionManager: SessionManagerService, private readonly sessionManager: SessionManagerService,
private readonly messageFilter: MessageFilterService, private readonly messageFilter: MessageFilterService,
private readonly eventProcessor: ZulipEventProcessorService, private readonly eventProcessor: ZulipEventProcessorService,
private readonly configManager: ConfigManagerService, @Inject('ZULIP_CONFIG_SERVICE')
private readonly errorHandler: ErrorHandlerService, private readonly configManager: IZulipConfigService,
private readonly apiKeySecurityService: ApiKeySecurityService,
) { ) {
this.logger.log('ZulipService初始化完成'); this.logger.log('ZulipService初始化完成');
} }
@@ -295,36 +320,38 @@ export class ZulipService {
// 从Token中提取用户ID模拟 // 从Token中提取用户ID模拟
const userId = `user_${token.substring(0, 8)}`; const userId = `user_${token.substring(0, 8)}`;
// 为测试用户提供真实的 Zulip API Key // 从ApiKeySecurityService获取真实的Zulip API Key
let zulipApiKey = undefined; let zulipApiKey = undefined;
let zulipEmail = undefined; let zulipEmail = undefined;
// 检查是否是配置了真实 Zulip API Key 的测试用户 try {
const hasTestApiKey = token.includes('lCPWCPf'); // 尝试从Redis获取存储的API Key
const hasUserApiKey = token.includes('W2KhXaQx'); const apiKeyResult = await this.apiKeySecurityService.getApiKey(userId);
const hasOldApiKey = token.includes('MZ1jEMQo');
const isRealUserToken = token === 'real_user_token_with_zulip_key_123';
this.logger.log('Token检查', { if (apiKeyResult.success && apiKeyResult.apiKey) {
zulipApiKey = apiKeyResult.apiKey;
// TODO: 从数据库获取用户的Zulip邮箱
// 暂时使用模拟数据
zulipEmail = 'angjustinl@163.com';
this.logger.log('从存储获取到Zulip API Key', {
operation: 'validateGameToken', operation: 'validateGameToken',
userId, 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';
this.logger.log('配置真实Zulip API Key', {
operation: 'validateGameToken',
userId,
zulipEmail,
hasApiKey: true, 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,
error: err.message,
}); });
} }
@@ -332,7 +359,6 @@ export class ZulipService {
userId, userId,
username: `Player_${userId.substring(5, 10)}`, username: `Player_${userId.substring(5, 10)}`,
email: `${userId}@example.com`, email: `${userId}@example.com`,
// 实际项目中从数据库获取
zulipEmail, zulipEmail,
zulipApiKey, zulipApiKey,
}; };

View File

@@ -56,7 +56,7 @@ describeE2E('Zulip Integration E2E Tests', () => {
}); });
client.on('connect', () => resolve(client)); 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); setTimeout(() => reject(new Error('Connection timeout')), 5000);
}); });

View File

@@ -16,9 +16,9 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { Logger } from '@nestjs/common'; import { Logger } from '@nestjs/common';
import * as fc from 'fast-check'; 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 { 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'; import { Server, Socket } from 'socket.io';
describe('ZulipWebSocketGateway', () => { describe('ZulipWebSocketGateway', () => {

View File

@@ -35,7 +35,7 @@ import {
import { Server, Socket } from 'socket.io'; import { Server, Socket } from 'socket.io';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { ZulipService } from './zulip.service'; import { ZulipService } from './zulip.service';
import { SessionManagerService } from './services/session-manager.service'; import { SessionManagerService } from './services/session_manager.service';
/** /**
* - guide.md格式 * - guide.md格式
@@ -96,6 +96,29 @@ interface ClientData {
connectedAt: Date; connectedAt: Date;
} }
/**
* Zulip WebSocket网关类
*
*
* - Godot游戏客户端的WebSocket连接
* - Zulip协议的转换
* -
* -
*
*
* - handleConnection():
* - handleDisconnect():
* - handleLogin():
* - handleChat():
* - handlePositionUpdate():
* - sendChatRender():
*
* 使
* - WebSocket通信的统一入口
* -
* -
* - 广
*/
@Injectable() @Injectable()
@WebSocketGateway({ @WebSocketGateway({
cors: { origin: '*' }, cors: { origin: '*' },

View File

@@ -19,8 +19,9 @@
* @since 2025-12-17 * @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 { 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: '更新时间' comment: '更新时间'
}) })
updated_at: Date; updated_at: Date;
/**
* 关联的Zulip账号
*
* 关系设计:
* - 类型一对一关系OneToOne
* - 外键在ZulipAccounts表中
* - 级联:不设置级联删除,保证数据安全
*
* 业务规则:
* - 每个游戏用户最多关联一个Zulip账号
* - 支持延迟加载,提高查询性能
* - 可选关联不是所有用户都有Zulip账号
*
* 使用场景:
* - 游戏内聊天功能集成
* - 跨平台消息同步
* - 用户身份验证和权限管理
*/
@OneToOne(() => ZulipAccounts, zulipAccount => zulipAccount.gameUser)
zulipAccount?: ZulipAccounts;
} }

View 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;
}
}

View 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();
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -826,4 +826,36 @@ export class LoginCoreService {
VerificationCodeType.EMAIL_VERIFICATION 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;
}
}
} }

View File

@@ -1,5 +1,5 @@
/** /**
* *
* *
* *
* - * -
@@ -10,14 +10,14 @@
*/ */
// 模块 // 模块
export * from './security.module'; export * from './security_core.module';
// 守卫 // 守卫
export * from './guards/throttle.guard'; export * from './guards/throttle.guard';
// 中间件 // 中间件
export * from './middleware/maintenance.middleware'; export * from './middleware/maintenance.middleware';
export * from './middleware/content-type.middleware'; export * from './middleware/content_type.middleware';
// 拦截器 // 拦截器
export * from './interceptors/timeout.interceptor'; export * from './interceptors/timeout.interceptor';

View File

@@ -1,11 +1,11 @@
/** /**
* *
* *
* *
* - * -
* - * -
* - * -
* - * -
* *
* @author kiro-ai * @author kiro-ai
* @version 1.0.0 * @version 1.0.0
@@ -34,4 +34,4 @@ import { TimeoutInterceptor } from './interceptors/timeout.interceptor';
], ],
exports: [ThrottleGuard, TimeoutInterceptor], exports: [ThrottleGuard, TimeoutInterceptor],
}) })
export class SecurityModule {} export class SecurityCoreModule {}

26
src/core/zulip/index.ts Normal file
View 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';

View 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;
}

View File

@@ -17,7 +17,7 @@ import {
ApiKeySecurityService, ApiKeySecurityService,
SecurityEventType, SecurityEventType,
SecuritySeverity, SecuritySeverity,
} from './api-key-security.service'; } from './api_key_security.service';
import { IRedisService } from '../../../core/redis/redis.interface'; import { IRedisService } from '../../../core/redis/redis.interface';
describe('ApiKeySecurityService', () => { describe('ApiKeySecurityService', () => {

View File

@@ -100,6 +100,28 @@ export interface GetApiKeyResult {
message?: string; message?: string;
} }
/**
* API密钥安全服务类
*
*
* - Zulip API密钥的安全存储
* - API密钥的加密和解密功能
* - API密钥的访问日志
* - API密钥的使用情况和安全事件
*
*
* - storeApiKey(): API密钥
* - retrieveApiKey(): API密钥
* - validateApiKey(): API密钥的有效性
* - logSecurityEvent():
* - getAccessStats(): API密钥访问统计
*
* 使
* - API密钥的安全存储
* - API密钥访问时的解密操作
* -
* - API密钥使用情况的统计分析
*/
@Injectable() @Injectable()
export class ApiKeySecurityService { export class ApiKeySecurityService {
private readonly logger = new Logger(ApiKeySecurityService.name); private readonly logger = new Logger(ApiKeySecurityService.name);

View File

@@ -12,8 +12,8 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import * as fc from 'fast-check'; import * as fc from 'fast-check';
import { ConfigManagerService, MapConfig, ZulipConfig } from './config-manager.service'; import { ConfigManagerService, MapConfig, ZulipConfig } from './config_manager.service';
import { AppLoggerService } from '../../../core/utils/logger/logger.service'; import { AppLoggerService } from '../../utils/logger/logger.service';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';

View File

@@ -108,6 +108,28 @@ export interface InteractionObject extends InteractionObjectConfig {
mapId: string; // 所属地图ID mapId: string; // 所属地图ID
} }
/**
*
*
*
* - Zulip Stream的映射配置
* - Zulip服务器连接配置
* -
* -
*
*
* - loadMapConfig():
* - getStreamByMap(): ID获取对应的Stream
* - getZulipConfig(): Zulip服务器配置
* - validateConfig():
* - enableConfigWatcher():
*
* 使
* -
* - Stream映射
* -
* -
*/
@Injectable() @Injectable()
export class ConfigManagerService implements OnModuleDestroy { export class ConfigManagerService implements OnModuleDestroy {
private mapConfigs: Map<string, MapConfig> = new Map(); private mapConfigs: Map<string, MapConfig> = new Map();
@@ -117,10 +139,39 @@ export class ConfigManagerService implements OnModuleDestroy {
private configLoadTime: Date; private configLoadTime: Date;
private configWatcher: fs.FSWatcher | null = null; private configWatcher: fs.FSWatcher | null = null;
private isWatcherEnabled: boolean = false; 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 MAP_CONFIG_FILE = 'map-config.json';
private readonly logger = new Logger(ConfigManagerService.name); 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() { constructor() {
this.logger.log('ConfigManagerService初始化完成'); this.logger.log('ConfigManagerService初始化完成');
@@ -216,6 +267,9 @@ export class ConfigManagerService implements OnModuleDestroy {
* 4. * 4.
* *
* @returns Promise<void> * @returns Promise<void>
*
* @throws Error
* @throws Error
*/ */
async loadMapConfig(): Promise<void> { async loadMapConfig(): Promise<void> {
this.logger.log('开始加载地图配置', { this.logger.log('开始加载地图配置', {

View File

@@ -23,7 +23,7 @@ import {
LoadStatus, LoadStatus,
ErrorHandlingResult, ErrorHandlingResult,
RetryConfig, RetryConfig,
} from './error-handler.service'; } from './error_handler.service';
import { AppLoggerService } from '../../../core/utils/logger/logger.service'; import { AppLoggerService } from '../../../core/utils/logger/logger.service';
describe('ErrorHandlerService', () => { describe('ErrorHandlerService', () => {

View File

@@ -115,6 +115,28 @@ export enum LoadStatus {
CRITICAL = 'critical', CRITICAL = 'critical',
} }
/**
*
*
*
* -
* -
* -
* -
*
*
* - handleError():
* - retryWithBackoff(): 退
* - enableDegradedMode():
* - getServiceStatus():
* - recordError():
*
* 使
* - Zulip API调用失败时的错误处理
* -
* -
* -
*/
@Injectable() @Injectable()
export class ErrorHandlerService extends EventEmitter implements OnModuleDestroy { export class ErrorHandlerService extends EventEmitter implements OnModuleDestroy {
private readonly logger = new Logger(ErrorHandlerService.name); private readonly logger = new Logger(ErrorHandlerService.name);

View File

@@ -182,6 +182,29 @@ export interface MonitoringStats {
}; };
} }
/**
*
*
*
* - Zulip集成系统的运行状态
* -
* -
* -
*
*
* - recordConnection():
* - recordApiCall(): API调用统计
* - recordMessage():
* - triggerAlert():
* - getSystemStats():
* - performHealthCheck():
*
* 使
* -
* -
* -
* -
*/
@Injectable() @Injectable()
export class MonitoringService extends EventEmitter implements OnModuleInit, OnModuleDestroy { export class MonitoringService extends EventEmitter implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(MonitoringService.name); private readonly logger = new Logger(MonitoringService.name);

View File

@@ -21,8 +21,30 @@
*/ */
import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; 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() @Injectable()
export class StreamInitializerService implements OnModuleInit { export class StreamInitializerService implements OnModuleInit {
private readonly logger = new Logger(StreamInitializerService.name); private readonly logger = new Logger(StreamInitializerService.name);

View 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}`);
}
}
}

View File

@@ -12,7 +12,7 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import * as fc from 'fast-check'; 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'; import { AppLoggerService } from '../../../core/utils/logger/logger.service';
describe('ZulipClientService', () => { describe('ZulipClientService', () => {

View File

@@ -77,6 +77,28 @@ export interface GetEventsResult {
error?: string; error?: string;
} }
/**
* Zulip客户端服务类
*
*
* - Zulip REST API调用
* - Zulip客户端的创建和配置
* -
* -
*
*
* - createClient(): Zulip客户端
* - registerQueue(): Zulip事件队列
* - sendMessage(): Zulip Stream
* - getEvents(): Zulip事件
* - validateConfig():
*
* 使
* - Zulip客户端
* - Zulip服务器的所有通信
* -
* - API调用的错误处理和重试
*/
@Injectable() @Injectable()
export class ZulipClientService { export class ZulipClientService {
private readonly logger = new Logger(ZulipClientService.name); private readonly logger = new Logger(ZulipClientService.name);

View File

@@ -12,9 +12,9 @@
*/ */
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { ZulipClientPoolService, PoolStats } from './zulip-client-pool.service'; import { ZulipClientPoolService, PoolStats } from './zulip_client_pool.service';
import { ZulipClientService, ZulipClientConfig, ZulipClientInstance } from './zulip-client.service'; import { ZulipClientService, ZulipClientConfig, ZulipClientInstance } from './zulip_client.service';
import { AppLoggerService } from '../../../core/utils/logger/logger.service'; import { AppLoggerService } from '../../utils/logger/logger.service';
describe('ZulipClientPoolService', () => { describe('ZulipClientPoolService', () => {
let service: ZulipClientPoolService; let service: ZulipClientPoolService;

View File

@@ -35,7 +35,7 @@ import {
SendMessageResult, SendMessageResult,
RegisterQueueResult, RegisterQueueResult,
GetEventsResult, GetEventsResult,
} from './zulip-client.service'; } from './zulip_client.service';
/** /**
* *
@@ -57,6 +57,28 @@ export interface PoolStats {
clientIds: string[]; clientIds: string[];
} }
/**
* Zulip客户端池服务类
*
*
* - Zulip客户端实例
* -
* -
* -
*
*
* - createUserClient(): Zulip客户端
* - getUserClient(): Zulip客户端
* - destroyUserClient(): Zulip客户端
* - getPoolStats():
* - startEventPolling():
*
* 使
* -
* -
* -
* -
*/
@Injectable() @Injectable()
export class ZulipClientPoolService implements OnModuleDestroy { export class ZulipClientPoolService implements OnModuleDestroy {
private readonly clientPool = new Map<string, UserClientInfo>(); private readonly clientPool = new Map<string, UserClientInfo>();

View 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 {}

View File

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

View File

@@ -1,15 +1,16 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2020", "target": "ES2020",
"module": "Node16", "module": "commonjs",
"lib": ["ES2020"], "lib": ["ES2020"],
"moduleResolution": "node16", "moduleResolution": "node",
"declaration": true, "declaration": true,
"removeComments": true, "removeComments": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"strict": true, "strict": true,
"noImplicitAny": false,
"esModuleInterop": true, "esModuleInterop": true,
"skipLibCheck": true, "skipLibCheck": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
@@ -20,5 +21,5 @@
"typeRoots": ["./node_modules/@types"] "typeRoots": ["./node_modules/@types"]
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "dist"] "exclude": ["node_modules", "dist", "client"]
} }

View File

@@ -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);
});
});