diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index ac314d8..0000000
--- a/Dockerfile
+++ /dev/null
@@ -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"]
\ No newline at end of file
diff --git a/README.md b/README.md
index e3663f1..1768a65 100644
--- a/README.md
+++ b/README.md
@@ -124,29 +124,48 @@ pnpm run dev
### 第二步:熟悉项目架构 🏗️
+**📁 项目文件结构总览**
+
```
-项目根目录/
-├── src/ # 源代码目录
-│ ├── business/ # 业务功能模块(按功能组织)
-│ │ ├── auth/ # 🔐 用户认证模块
-│ │ ├── user-mgmt/ # 👥 用户管理模块
-│ │ ├── admin/ # 🛡️ 管理员模块
-│ │ ├── security/ # 🔒 安全模块
-│ │ └── shared/ # 🔗 共享组件
-│ ├── core/ # 核心技术服务
-│ │ ├── db/ # 数据库层(支持MySQL/内存双模式)
-│ │ ├── redis/ # Redis缓存服务(支持真实Redis/文件存储)
-│ │ ├── login_core/ # 登录核心服务
-│ │ ├── admin_core/ # 管理员核心服务
-│ │ └── utils/ # 工具服务(邮件、验证码、日志)
-│ ├── app.module.ts # 应用主模块
-│ └── main.ts # 应用入口
-├── client/ # 前端管理界面
-├── docs/ # 项目文档
-├── test/ # 测试文件
-├── redis-data/ # Redis文件存储数据
-├── logs/ # 日志文件
-└── 配置文件 # .env, package.json, tsconfig.json等
+whale-town-end/ # 🐋 项目根目录
+├── 📂 src/ # 源代码目录
+│ ├── 📂 business/ # 🎯 业务功能模块(按功能组织)
+│ │ ├── 📂 auth/ # 🔐 用户认证模块
+│ │ ├── 📂 user-mgmt/ # 👥 用户管理模块
+│ │ ├── 📂 admin/ # 🛡️ 管理员模块
+│ │ ├── 📂 security/ # 🔒 安全防护模块
+│ │ ├── 📂 zulip/ # 💬 Zulip集成模块
+│ │ └── 📂 shared/ # 🔗 共享业务组件
+│ ├── 📂 core/ # ⚙️ 核心技术服务
+│ │ ├── 📂 db/ # 🗄️ 数据库层(MySQL/内存双模式)
+│ │ ├── 📂 redis/ # 🔴 Redis缓存(真实Redis/文件存储)
+│ │ ├── 📂 login_core/ # 🔑 登录核心服务
+│ │ ├── 📂 admin_core/ # 👑 管理员核心服务
+│ │ ├── 📂 zulip/ # 💬 Zulip核心服务
+│ │ └── 📂 utils/ # 🛠️ 工具服务(邮件、验证码、日志)
+│ ├── 📄 app.module.ts # 🏠 应用主模块
+│ └── 📄 main.ts # 🚀 应用入口点
+├── 📂 client/ # 🎨 前端管理界面
+│ ├── 📂 src/ # 前端源码
+│ ├── 📂 dist/ # 前端构建产物
+│ ├── 📄 package.json # 前端依赖配置
+│ └── 📄 vite.config.ts # Vite构建配置
+├── 📂 docs/ # 📚 项目文档中心
+│ ├── 📂 api/ # 🔌 API接口文档
+│ ├── 📂 development/ # 💻 开发指南
+│ ├── 📂 deployment/ # 🚀 部署文档
+│ ├── 📄 ARCHITECTURE.md # 🏗️ 架构设计文档
+│ └── 📄 README.md # 📖 文档导航中心
+├── 📂 test/ # 🧪 测试文件目录
+├── 📂 config/ # ⚙️ 配置文件目录
+├── 📂 logs/ # 📝 日志文件存储
+├── 📂 redis-data/ # 💾 Redis文件存储数据
+├── 📂 dist/ # 📦 后端构建产物
+├── 📄 .env # 🔧 环境变量配置
+├── 📄 package.json # 📋 项目依赖配置
+├── 📄 docker-compose.yml # 🐳 Docker编排配置
+├── 📄 Dockerfile # 🐳 Docker镜像配置
+└── 📄 README.md # 📖 项目主文档(当前文件)
```
**架构特点:**
diff --git a/deploy.sh.example b/deploy.sh.example
deleted file mode 100644
index 7e97101..0000000
--- a/deploy.sh.example
+++ /dev/null
@@ -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 "警告:服务健康检查失败"
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
deleted file mode 100644
index 957b459..0000000
--- a/docker-compose.yml
+++ /dev/null
@@ -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:
\ No newline at end of file
diff --git a/docs/API_STATUS_CODES.md b/docs/API_STATUS_CODES.md
deleted file mode 100644
index 1042acb..0000000
--- a/docs/API_STATUS_CODES.md
+++ /dev/null
@@ -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
-
-
- ✅ 验证码已发送,请查收邮件
-
-
-
-
- ⚠️ 测试模式:验证码是 123456
-
- 请配置邮件服务以启用真实发送
-
-
-
-
- ❌ 发送失败:邮箱格式错误
-
-```
-
-## 📝 开发建议
-
-### 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)
\ No newline at end of file
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index b1b471e..3b858e2 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -1,187 +1,773 @@
-# 🏗️ 项目架构设计
+# 🏗️ Whale Town 项目架构设计
-## 整体架构
+> 基于业务功能模块化的现代化后端架构,支持双模式运行,开发测试零依赖,生产部署高性能。
-Whale Town 采用分层架构设计,确保代码的可维护性和可扩展性。
+## 📋 目录
+
+- [🎯 架构概述](#-架构概述)
+- [📁 目录结构详解](#-目录结构详解)
+- [🏗️ 分层架构设计](#️-分层架构设计)
+- [🔄 双模式架构](#-双模式架构)
+- [📦 模块依赖关系](#-模块依赖关系)
+- [🚀 扩展指南](#-扩展指南)
+
+---
+
+## 🎯 架构概述
+
+Whale Town 采用**业务功能模块化架构**,将代码按业务功能而非技术组件组织,确保高内聚、低耦合的设计原则。
+
+### 🌟 核心设计理念
+
+- **业务驱动** - 按业务功能组织代码,而非技术分层
+- **双模式支持** - 开发测试零依赖,生产部署高性能
+- **清晰分层** - 业务层 → 核心层 → 数据层,职责明确
+- **模块化设计** - 每个模块独立完整,可单独测试和部署
+- **配置驱动** - 通过环境变量控制运行模式和行为
+
+### 🛠️ 技术栈
+
+#### 后端技术栈
+- **框架**: NestJS 11.x (基于Express)
+- **语言**: TypeScript 5.x
+- **数据库**: MySQL + TypeORM (生产) / 内存数据库 (开发)
+- **缓存**: Redis + IORedis (生产) / 文件存储 (开发)
+- **认证**: JWT + bcrypt
+- **验证**: class-validator + class-transformer
+- **文档**: Swagger/OpenAPI
+- **测试**: Jest + Supertest
+- **日志**: Pino + nestjs-pino
+- **WebSocket**: Socket.IO
+- **邮件**: Nodemailer
+- **集成**: Zulip API
+
+#### 前端技术栈
+- **框架**: React 18.x
+- **构建工具**: Vite 7.x
+- **UI库**: Ant Design 5.x
+- **路由**: React Router DOM 6.x
+- **语言**: TypeScript 5.x
+
+### 📊 整体架构图
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ 🌐 API接口层 │
+│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
+│ │ 🔗 REST API │ │ 🔌 WebSocket │ │ 📄 Swagger UI │ │
+│ │ (HTTP接口) │ │ (实时通信) │ │ (API文档) │ │
+│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
+└─────────────────────────────────────────────────────────────────┘
+ ⬇️
+┌─────────────────────────────────────────────────────────────────┐
+│ 🎯 业务功能模块层 │
+│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
+│ │ 🔐 用户认证 │ │ 👥 用户管理 │ │ 🛡️ 管理员 │ │
+│ │ (auth) │ │ (user_mgmt) │ │ (admin) │ │
+│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
+│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
+│ │ 💬 Zulip集成 │ │ 🔗 共享组件 │ │ │ │
+│ │ (zulip) │ │ (shared) │ │ │ │
+│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
+└─────────────────────────────────────────────────────────────────┘
+ ⬇️
+┌─────────────────────────────────────────────────────────────────┐
+│ ⚙️ 核心技术服务层 │
+│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
+│ │ 🔑 登录核心 │ │ 👑 管理员核心 │ │ 💬 Zulip核心 │ │
+│ │ (auth_core) │ │ (admin_core) │ │ (zulip) │ │
+│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
+│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
+│ │ 🛡️ 安全核心 │ │ 🛠️ 工具服务 │ │ 📧 邮件服务 │ │
+│ │ (security_core)│ │ (utils) │ │ (email) │ │
+│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
+└─────────────────────────────────────────────────────────────────┘
+ ⬇️
+┌─────────────────────────────────────────────────────────────────┐
+│ 🗄️ 数据存储层 │
+│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
+│ │ 🗃️ 数据库 │ │ 🔴 Redis缓存 │ │ 📁 文件存储 │ │
+│ │ (MySQL/内存) │ │ (Redis/文件) │ │ (logs/data) │ │
+│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
+└─────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## 📁 目录结构详解
+
+### 🎯 业务功能模块 (`src/business/`)
+
+> **设计原则**: 按业务功能组织,每个模块包含完整的业务逻辑
+
+```
+src/business/
+├── 📂 auth/ # 🔐 用户认证模块
+│ ├── 📄 auth.module.ts # 模块定义
+│ ├── 📂 controllers/ # 控制器
+│ │ └── 📄 login.controller.ts # 登录接口控制器
+│ ├── 📂 services/ # 业务服务
+│ │ ├── 📄 login.service.ts # 登录业务逻辑
+│ │ └── 📄 login.service.spec.ts # 登录服务测试
+│ ├── 📂 dto/ # 数据传输对象
+│ │ ├── 📄 login.dto.ts # 登录请求DTO
+│ │ └── 📄 login_response.dto.ts # 登录响应DTO
+│ └── 📂 guards/ # 权限守卫(预留)
+│
+├── 📂 user-mgmt/ # 👥 用户管理模块
+│ ├── 📄 user-mgmt.module.ts # 模块定义
+│ ├── 📂 controllers/ # 控制器
+│ │ └── 📄 user-status.controller.ts # 用户状态管理接口
+│ ├── 📂 services/ # 业务服务
+│ │ └── 📄 user-management.service.ts # 用户管理逻辑
+│ ├── 📂 dto/ # 数据传输对象
+│ │ ├── 📄 user-status.dto.ts # 用户状态DTO
+│ │ └── 📄 user-status-response.dto.ts # 状态响应DTO
+│ ├── 📂 enums/ # 枚举定义
+│ │ └── 📄 user-status.enum.ts # 用户状态枚举
+│ └── 📂 tests/ # 测试文件(预留)
+│
+├── 📂 admin/ # 🛡️ 管理员模块
+│ ├── 📄 admin.controller.ts # 管理员接口
+│ ├── 📄 admin.service.ts # 管理员业务逻辑
+│ ├── 📄 admin.module.ts # 模块定义
+│ ├── 📄 admin.service.spec.ts # 管理员服务测试
+│ ├── 📂 dto/ # 数据传输对象
+│ └── 📂 guards/ # 权限守卫
+│
+├── 📂 zulip/ # 💬 Zulip集成模块
+│ ├── 📄 zulip.service.ts # Zulip业务服务
+│ ├── 📄 zulip_websocket.gateway.ts # WebSocket网关
+│ ├── 📄 zulip.module.ts # 模块定义
+│ ├── 📂 interfaces/ # 接口定义
+│ └── 📂 services/ # 子服务
+│ ├── 📄 message_filter.service.ts # 消息过滤
+│ └── 📄 session_cleanup.service.ts # 会话清理
+│
+└── 📂 shared/ # 🔗 共享业务组件
+ ├── 📂 dto/ # 共享数据传输对象
+ └── 📄 index.ts # 导出文件
+```
+
+### ⚙️ 核心技术服务 (`src/core/`)
+
+> **设计原则**: 提供技术基础设施,支持业务模块运行
+
+```
+src/core/
+├── 📂 db/ # 🗄️ 数据库层
+│ └── 📂 users/ # 用户数据服务
+│ ├── 📄 users.service.ts # MySQL数据库实现
+│ ├── 📄 users_memory.service.ts # 内存数据库实现
+│ ├── 📄 users.dto.ts # 用户数据传输对象
+│ ├── 📄 users.entity.ts # 用户实体定义
+│ ├── 📄 users.module.ts # 用户数据模块
+│ └── 📄 users.service.spec.ts # 用户服务测试
+│
+├── 📂 redis/ # 🔴 Redis缓存层
+│ ├── 📄 redis.module.ts # Redis模块
+│ ├── 📄 real-redis.service.ts # Redis真实实现
+│ ├── 📄 file-redis.service.ts # 文件存储实现
+│ └── 📄 redis.interface.ts # Redis服务接口
+│
+├── 📂 login_core/ # 🔑 登录核心服务
+│ ├── 📄 login_core.service.ts # 登录核心逻辑
+│ ├── 📄 login_core.module.ts # 模块定义
+│ └── 📄 login_core.service.spec.ts # 登录核心测试
+│
+├── 📂 admin_core/ # 👑 管理员核心服务
+│ ├── 📄 admin_core.service.ts # 管理员核心逻辑
+│ ├── 📄 admin_core.module.ts # 模块定义
+│ └── 📄 admin_core.service.spec.ts # 管理员核心测试
+│
+├── 📂 zulip/ # 💬 Zulip核心服务
+│ ├── 📄 zulip-core.module.ts # Zulip核心模块
+│ ├── 📂 config/ # 配置文件
+│ ├── 📂 interfaces/ # 接口定义
+│ ├── 📂 services/ # 核心服务
+│ ├── 📂 types/ # 类型定义
+│ └── 📄 index.ts # 导出文件
+│
+├── 📂 security_core/ # 🛡️ 安全核心模块
+│ ├── 📄 security_core.module.ts # 安全模块定义
+│ ├── 📂 guards/ # 安全守卫
+│ │ └── 📄 throttle.guard.ts # 频率限制守卫
+│ ├── 📂 interceptors/ # 拦截器
+│ │ └── 📄 timeout.interceptor.ts # 超时拦截器
+│ ├── 📂 middleware/ # 中间件
+│ │ ├── 📄 maintenance.middleware.ts # 维护模式中间件
+│ │ └── 📄 content_type.middleware.ts # 内容类型中间件
+│ └── 📂 decorators/ # 装饰器
+│ ├── 📄 throttle.decorator.ts # 频率限制装饰器
+│ └── 📄 timeout.decorator.ts # 超时装饰器
+│
+└── 📂 utils/ # 🛠️ 工具服务
+ ├── 📂 email/ # 📧 邮件服务
+ │ ├── 📄 email.service.ts # 邮件发送服务
+ │ ├── 📄 email.module.ts # 邮件模块
+ │ └── 📄 email.service.spec.ts # 邮件服务测试
+ ├── 📂 verification/ # 🔢 验证码服务
+ │ ├── 📄 verification.service.ts # 验证码生成验证
+ │ ├── 📄 verification.module.ts # 验证码模块
+ │ └── 📄 verification.service.spec.ts # 验证码服务测试
+ └── 📂 logger/ # 📝 日志服务
+ ├── 📄 logger.service.ts # 日志记录服务
+ ├── 📄 logger.module.ts # 日志模块
+ ├── 📄 logger.config.ts # 日志配置
+ └── 📄 log_management.service.ts # 日志管理服务
+```
+
+### 🎨 前端管理界面 (`client/`)
+
+> **设计原则**: 独立的前端项目,提供管理员后台功能,基于React + Vite + Ant Design
+
+```
+client/
+├── 📂 src/ # 前端源码
+│ ├── 📂 app/ # 应用组件
+│ │ ├── 📄 App.tsx # 应用主组件
+│ │ └── 📄 AdminLayout.tsx # 管理员布局组件
+│ ├── 📂 pages/ # 页面组件
+│ │ ├── 📄 LoginPage.tsx # 登录页面
+│ │ ├── 📄 UsersPage.tsx # 用户管理页面
+│ │ └── 📄 LogsPage.tsx # 日志管理页面
+│ ├── 📂 lib/ # 工具库
+│ │ ├── 📄 api.ts # API客户端
+│ │ └── 📄 adminAuth.ts # 管理员认证服务
+│ └── 📄 main.tsx # 应用入口
+├── 📂 dist/ # 构建产物
+├── 📄 package.json # 前端依赖
+├── 📄 vite.config.ts # Vite配置
+└── 📄 tsconfig.json # TypeScript配置
+```
+
+### 📚 文档中心 (`docs/`)
+
+> **设计原则**: 完整的项目文档,支持开发者快速上手
+
+```
+docs/
+├── 📄 README.md # 📖 文档导航中心
+├── 📄 ARCHITECTURE.md # 🏗️ 架构设计文档
+├── 📄 CONTRIBUTORS.md # 🤝 贡献者指南
+│
+├── 📂 api/ # 🔌 API接口文档
+│ ├── 📄 README.md # API文档使用指南
+│ └── 📄 api-documentation.md # 完整API接口文档
+│
+├── 📂 development/ # 💻 开发指南
+│ ├── 📄 backend_development_guide.md # 后端开发规范
+│ ├── 📄 git_commit_guide.md # Git提交规范
+│ ├── 📄 AI辅助开发规范指南.md # AI辅助开发指南
+│ └── 📄 TESTING.md # 测试指南
+│
+└── 📂 deployment/ # 🚀 部署文档
+ └── 📄 DEPLOYMENT.md # 生产环境部署指南
+```
+
+### 🧪 测试文件 (`test/`)
+
+> **设计原则**: 完整的测试覆盖,确保代码质量
+
+```
+test/
+├── 📂 unit/ # 单元测试
+├── 📂 integration/ # 集成测试
+├── 📂 e2e/ # 端到端测试
+└── 📂 fixtures/ # 测试数据
+```
+
+### ⚙️ 配置文件
+
+> **设计原则**: 清晰的配置管理,支持多环境部署
+
+```
+项目根目录/
+├── 📄 .env # 🔧 环境变量配置
+├── 📄 .env.example # 🔧 环境变量示例
+├── 📄 .env.production.example # 🔧 生产环境示例
+├── 📄 package.json # 📋 后端项目依赖配置
+├── 📄 pnpm-workspace.yaml # 📦 pnpm工作空间配置
+├── 📄 tsconfig.json # 📘 TypeScript配置
+├── 📄 jest.config.js # 🧪 Jest测试配置
+├── 📄 nest-cli.json # 🏠 NestJS CLI配置
+└── 📄 ecosystem.config.js # 🚀 PM2进程管理配置
+
+client/
+├── 📄 package.json # 📋 前端项目依赖配置
+├── 📄 vite.config.ts # ⚡ Vite构建配置
+└── 📄 tsconfig.json # 📘 前端TypeScript配置
+```
+
+---
+
+## 🏗️ 分层架构设计
+
+### 📊 架构分层说明
```
┌─────────────────────────────────────────────────────────────┐
-│ API 层 │
+│ 🌐 表现层 (Presentation) │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
-│ │ HTTP API │ │ WebSocket │ │ GraphQL │ │
-│ │ (REST) │ │ (Socket.IO) │ │ (预留) │ │
+│ │ Controllers │ │ WebSocket │ │ Swagger UI │ │
+│ │ (HTTP接口) │ │ Gateways │ │ (API文档) │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
- │
+ ⬇️
┌─────────────────────────────────────────────────────────────┐
-│ 业务逻辑层 │
+│ 🎯 业务层 (Business) │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
-│ │ 用户认证 │ │ 游戏逻辑 │ │ 社交功能 │ │
-│ │ (Login) │ │ (Game) │ │ (Social) │ │
+│ │ Auth Module │ │ UserMgmt │ │ Admin Module │ │
+│ │ (用户认证) │ │ Module │ │ (管理员) │ │
+│ │ │ │ (用户管理) │ │ │ │
+│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
+│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
+│ │ Security Module │ │ Zulip Module │ │ Shared Module │ │
+│ │ (安全防护) │ │ (Zulip集成) │ │ (共享组件) │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
- │
+ ⬇️
┌─────────────────────────────────────────────────────────────┐
-│ 核心服务层 │
+│ ⚙️ 服务层 (Service) │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
-│ │ 邮件服务 │ │ 验证码服务 │ │ 日志服务 │ │
-│ │ (Email) │ │ (Verification)│ │ (Logger) │ │
+│ │ Login Core │ │ Admin Core │ │ Zulip Core │ │
+│ │ (登录核心) │ │ (管理员核心) │ │ (Zulip核心) │ │
+│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
+│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
+│ │ Email Service │ │ Verification │ │ Logger Service │ │
+│ │ (邮件服务) │ │ Service │ │ (日志服务) │ │
+│ │ │ │ (验证码服务) │ │ │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
- │
+ ⬇️
┌─────────────────────────────────────────────────────────────┐
-│ 数据访问层 │
+│ 🗄️ 数据层 (Data) │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
-│ │ 用户数据 │ │ Redis缓存 │ │ 文件存储 │ │
-│ │ (Users) │ │ (Cache) │ │ (Files) │ │
+│ │ Users Service │ │ Redis Service │ │ File Storage │ │
+│ │ (用户数据) │ │ (缓存服务) │ │ (文件存储) │ │
+│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
+│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
+│ │ MySQL/Memory │ │ Redis/File │ │ Logs/Data │ │
+│ │ (数据库) │ │ (缓存实现) │ │ (日志数据) │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
-## 模块依赖关系
+### 🔄 数据流向
+
+#### 用户登录流程示例
```
-AppModule
-├── ConfigModule (全局配置)
-├── LoggerModule (日志系统)
-├── RedisModule (缓存服务)
-├── UsersModule (用户管理)
-│ ├── UsersService (数据库模式)
-│ └── UsersMemoryService (内存模式)
-├── EmailModule (邮件服务)
-├── VerificationModule (验证码服务)
-├── LoginCoreModule (登录核心)
-└── LoginModule (登录业务)
+1. 📱 用户请求 → LoginController.login()
+2. 🔍 参数验证 → class-validator装饰器
+3. 🎯 业务逻辑 → LoginService.login()
+4. ⚙️ 核心服务 → LoginCoreService.validateUser()
+5. 📧 发送验证码 → VerificationService.generate()
+6. 💾 存储数据 → UsersService.findByEmail() + RedisService.set()
+7. 📝 记录日志 → LoggerService.log()
+8. ✅ 返回响应 → 用户收到登录结果
```
-## 数据流向
+#### 管理员操作流程示例
-### 用户注册流程
```
-1. 用户请求 → LoginController
-2. 参数验证 → LoginService
-3. 发送验证码 → LoginCoreService
-4. 生成验证码 → VerificationService
-5. 发送邮件 → EmailService
-6. 存储验证码 → RedisService
-7. 返回响应 → 用户
+1. 🛡️ 管理员请求 → AdminController.resetUserPassword()
+2. 🔐 权限验证 → AdminGuard.canActivate()
+3. 🎯 业务逻辑 → AdminService.resetPassword()
+4. ⚙️ 核心服务 → AdminCoreService.resetUserPassword()
+5. 🔑 密码加密 → bcrypt.hash()
+6. 💾 更新数据 → UsersService.update()
+7. 📧 通知用户 → EmailService.sendPasswordReset()
+8. 📝 审计日志 → LoggerService.audit()
+9. ✅ 返回响应 → 管理员收到操作结果
```
-### 双模式架构
+---
-项目支持开发测试模式和生产部署模式的无缝切换:
+## 🔄 双模式架构
-#### 开发测试模式
-- **数据库**: 内存存储 (UsersMemoryService)
-- **缓存**: 文件存储 (FileRedisService)
-- **邮件**: 控制台输出 (测试模式)
-- **优势**: 无需外部依赖,快速启动测试
+### 🎯 设计目标
-#### 生产部署模式
-- **数据库**: MySQL (UsersService + TypeORM)
-- **缓存**: Redis (RealRedisService + IORedis)
-- **邮件**: SMTP服务器 (生产模式)
-- **优势**: 高性能,高可用,数据持久化
+- **开发测试**: 零依赖快速启动,无需安装MySQL、Redis等外部服务
+- **生产部署**: 高性能、高可用,支持集群和负载均衡
-## 设计原则
+### 📊 模式对比
-### 1. 单一职责原则
-每个模块只负责一个特定的功能领域:
-- `LoginModule`: 只处理登录相关业务
-- `EmailModule`: 只处理邮件发送
-- `VerificationModule`: 只处理验证码逻辑
+| 功能模块 | 🧪 开发测试模式 | 🚀 生产部署模式 |
+|----------|----------------|----------------|
+| **数据库** | 内存存储 (UsersMemoryService) | MySQL (UsersService + TypeORM) |
+| **缓存** | 文件存储 (FileRedisService) | Redis (RealRedisService + IORedis) |
+| **邮件** | 控制台输出 (测试模式) | SMTP服务器 (生产模式) |
+| **日志** | 控制台 + 文件 | 结构化日志 + 日志轮转 |
+| **配置** | `.env` 默认配置 | 环境变量 + 配置中心 |
-### 2. 依赖注入
-使用NestJS的依赖注入系统:
-- 接口抽象: `IRedisService`, `IUsersService`
-- 实现切换: 根据配置自动选择实现类
-- 测试友好: 易于Mock和单元测试
+### ⚙️ 模式切换配置
-### 3. 配置驱动
-通过环境变量控制行为:
-- `USE_FILE_REDIS`: 选择Redis实现
-- `DB_HOST`: 数据库连接配置
-- `EMAIL_HOST`: 邮件服务配置
+#### 开发测试模式 (.env)
+```bash
+# 数据存储模式
+USE_FILE_REDIS=true # 使用文件存储代替Redis
+NODE_ENV=development # 开发环境
-### 4. 错误处理
-统一的错误处理机制:
-- HTTP异常: `BadRequestException`, `UnauthorizedException`
-- 业务异常: 自定义异常类
-- 日志记录: 结构化错误日志
+# 数据库配置(注释掉,使用内存数据库)
+# DB_HOST=localhost
+# DB_USERNAME=root
+# DB_PASSWORD=password
-## 扩展指南
+# 邮件配置(注释掉,使用测试模式)
+# EMAIL_HOST=smtp.gmail.com
+# EMAIL_USER=your_email@gmail.com
+# EMAIL_PASS=your_password
+```
-### 添加新的业务模块
+#### 生产部署模式 (.env.production)
+```bash
+# 数据存储模式
+USE_FILE_REDIS=false # 使用真实Redis
+NODE_ENV=production # 生产环境
-1. **创建业务模块**
- ```bash
- nest g module business/game
- nest g controller business/game
- nest g service business/game
- ```
+# 数据库配置
+DB_HOST=your_mysql_host
+DB_PORT=3306
+DB_USERNAME=your_username
+DB_PASSWORD=your_password
+DB_DATABASE=whale_town
-2. **创建核心服务**
- ```bash
- nest g module core/game_core
- nest g service core/game_core
- ```
+# Redis配置
+REDIS_HOST=your_redis_host
+REDIS_PORT=6379
+REDIS_PASSWORD=your_redis_password
-3. **添加数据模型**
- ```bash
- nest g module core/db/games
- nest g service core/db/games
- ```
+# 邮件配置
+EMAIL_HOST=smtp.gmail.com
+EMAIL_PORT=587
+EMAIL_USER=your_email@gmail.com
+EMAIL_PASS=your_app_password
+```
-4. **更新主模块**
- 在 `app.module.ts` 中导入新模块
+### 🔧 实现机制
-### 添加新的工具服务
+#### 依赖注入切换
+```typescript
+// redis.module.ts
+@Module({
+ providers: [
+ {
+ provide: 'IRedisService',
+ useFactory: (configService: ConfigService) => {
+ const useFileRedis = configService.get('USE_FILE_REDIS');
+ return useFileRedis
+ ? new FileRedisService()
+ : new RealRedisService(configService);
+ },
+ inject: [ConfigService],
+ },
+ ],
+})
+export class RedisModule {}
+```
-1. **创建工具模块**
- ```bash
- nest g module core/utils/notification
- nest g service core/utils/notification
- ```
+#### 配置驱动服务选择
+```typescript
+// users.module.ts
+@Module({
+ providers: [
+ {
+ provide: 'IUsersService',
+ useFactory: (configService: ConfigService) => {
+ const dbHost = configService.get('DB_HOST');
+ return dbHost
+ ? new UsersService()
+ : new UsersMemoryService();
+ },
+ inject: [ConfigService],
+ },
+ ],
+})
+export class UsersModule {}
+```
-2. **实现服务接口**
- 定义抽象接口和具体实现
+---
-3. **添加配置支持**
- 在环境变量中添加相关配置
+## 📦 模块依赖关系
-4. **编写测试用例**
- 确保功能正确性和代码覆盖率
+### 🏗️ 模块依赖图
-## 性能优化
+```
+AppModule (应用主模块)
+├── 📊 ConfigModule (全局配置)
+├── 📝 LoggerModule (日志系统)
+├── 🔴 RedisModule (缓存服务)
+│ ├── RealRedisService (真实Redis)
+│ └── FileRedisService (文件存储)
+├── 🗄️ UsersModule (用户数据)
+│ ├── UsersService (MySQL数据库)
+│ └── UsersMemoryService (内存数据库)
+├── 📧 EmailModule (邮件服务)
+├── 🔢 VerificationModule (验证码服务)
+├── 🔑 LoginCoreModule (登录核心)
+├── 👑 AdminCoreModule (管理员核心)
+├── 💬 ZulipCoreModule (Zulip核心)
+├── 🔒 SecurityCoreModule (安全核心)
+│
+├── 🎯 业务功能模块
+│ ├── 🔐 AuthModule (用户认证)
+│ │ └── 依赖: LoginCoreModule, EmailModule, VerificationModule, SecurityCoreModule
+│ ├── 👥 UserMgmtModule (用户管理)
+│ │ └── 依赖: UsersModule, LoggerModule, SecurityCoreModule
+│ ├── 🛡️ AdminModule (管理员)
+│ │ └── 依赖: AdminCoreModule, UsersModule, SecurityCoreModule
+│ ├── 💬 ZulipModule (Zulip集成)
+│ │ └── 依赖: ZulipCoreModule, RedisModule
+│ └── 🔗 SharedModule (共享组件)
+```
-### 1. 缓存策略
-- **Redis缓存**: 验证码、会话信息
+### 🔄 模块交互流程
+
+#### 用户认证流程
+```
+AuthController → LoginService → LoginCoreService
+ ↓
+EmailService ← VerificationService ← RedisService
+ ↓
+ UsersService
+```
+
+#### 管理员操作流程
+```
+AdminController → AdminService → AdminCoreService
+ ↓
+LoggerService ← UsersService ← RedisService
+```
+
+#### 安全防护流程
+```
+SecurityGuard → RedisService (频率限制)
+ → LoggerService (审计日志)
+ → ConfigService (维护模式)
+```
+
+---
+
+## 🚀 扩展指南
+
+### 📝 添加新的业务模块
+
+#### 1. 创建业务模块结构
+```bash
+# 创建模块目录
+mkdir -p src/business/game/{dto,enums,guards,interfaces}
+
+# 生成NestJS模块文件
+nest g module business/game
+nest g controller business/game
+nest g service business/game
+```
+
+#### 2. 实现业务逻辑
+```typescript
+// src/business/game/game.module.ts
+@Module({
+ imports: [
+ GameCoreModule, # 依赖核心服务
+ UsersModule, # 依赖用户数据
+ RedisModule, # 依赖缓存服务
+ ],
+ controllers: [GameController],
+ providers: [GameService],
+ exports: [GameService],
+})
+export class GameModule {}
+```
+
+#### 3. 创建对应的核心服务
+```bash
+# 创建核心服务
+mkdir -p src/core/game_core
+nest g module core/game_core
+nest g service core/game_core
+```
+
+#### 4. 更新主模块
+```typescript
+// src/app.module.ts
+@Module({
+ imports: [
+ // ... 其他模块
+ GameModule, # 添加新的业务模块
+ ],
+})
+export class AppModule {}
+```
+
+### 🛠️ 添加新的工具服务
+
+#### 1. 创建工具服务
+```bash
+mkdir -p src/core/utils/notification
+nest g module core/utils/notification
+nest g service core/utils/notification
+```
+
+#### 2. 定义服务接口
+```typescript
+// src/core/utils/notification/notification.interface.ts
+export interface INotificationService {
+ sendPush(userId: string, message: string): Promise;
+ sendSMS(phone: string, message: string): Promise;
+}
+```
+
+#### 3. 实现服务
+```typescript
+// src/core/utils/notification/notification.service.ts
+@Injectable()
+export class NotificationService implements INotificationService {
+ async sendPush(userId: string, message: string): Promise {
+ // 实现推送通知逻辑
+ }
+
+ async sendSMS(phone: string, message: string): Promise {
+ // 实现短信发送逻辑
+ }
+}
+```
+
+#### 4. 配置依赖注入
+```typescript
+// src/core/utils/notification/notification.module.ts
+@Module({
+ providers: [
+ {
+ provide: 'INotificationService',
+ useClass: NotificationService,
+ },
+ ],
+ exports: ['INotificationService'],
+})
+export class NotificationModule {}
+```
+
+### 🔌 添加新的API接口
+
+#### 1. 定义DTO
+```typescript
+// src/business/game/dto/create-game.dto.ts
+export class CreateGameDto {
+ @IsString()
+ @IsNotEmpty()
+ name: string;
+
+ @IsString()
+ @IsOptional()
+ description?: string;
+}
+```
+
+#### 2. 实现Controller
+```typescript
+// src/business/game/game.controller.ts
+@Controller('game')
+@ApiTags('游戏管理')
+export class GameController {
+ constructor(private readonly gameService: GameService) {}
+
+ @Post()
+ @ApiOperation({ summary: '创建游戏' })
+ async createGame(@Body() createGameDto: CreateGameDto) {
+ return this.gameService.create(createGameDto);
+ }
+}
+```
+
+#### 3. 实现Service
+```typescript
+// src/business/game/game.service.ts
+@Injectable()
+export class GameService {
+ constructor(
+ @Inject('IGameCoreService')
+ private readonly gameCoreService: IGameCoreService,
+ ) {}
+
+ async create(createGameDto: CreateGameDto) {
+ return this.gameCoreService.createGame(createGameDto);
+ }
+}
+```
+
+#### 4. 添加测试用例
+```typescript
+// src/business/game/game.service.spec.ts
+describe('GameService', () => {
+ let service: GameService;
+ let gameCoreService: jest.Mocked;
+
+ beforeEach(async () => {
+ const module: TestingModule = await Test.createTestingModule({
+ providers: [
+ GameService,
+ {
+ provide: 'IGameCoreService',
+ useValue: {
+ createGame: jest.fn(),
+ },
+ },
+ ],
+ }).compile();
+
+ service = module.get(GameService);
+ gameCoreService = module.get('IGameCoreService');
+ });
+
+ it('should create game', async () => {
+ const createGameDto = { name: 'Test Game' };
+ const expectedResult = { id: 1, ...createGameDto };
+
+ gameCoreService.createGame.mockResolvedValue(expectedResult);
+
+ const result = await service.create(createGameDto);
+
+ expect(result).toEqual(expectedResult);
+ expect(gameCoreService.createGame).toHaveBeenCalledWith(createGameDto);
+ });
+});
+```
+
+### 📊 性能优化建议
+
+#### 1. 缓存策略
+- **Redis缓存**: 用户会话、验证码、频繁查询数据
- **内存缓存**: 配置信息、静态数据
- **CDN缓存**: 静态资源文件
-### 2. 数据库优化
-- **连接池**: 复用数据库连接
-- **索引优化**: 关键字段建立索引
-- **查询优化**: 避免N+1查询问题
+#### 2. 数据库优化
+- **连接池**: 复用数据库连接,减少连接开销
+- **索引优化**: 为查询字段建立合适的索引
+- **查询优化**: 避免N+1查询,使用JOIN优化关联查询
-### 3. 日志优化
-- **异步日志**: 使用Pino的异步写入
-- **日志分级**: 生产环境只记录必要日志
+#### 3. 日志优化
+- **异步日志**: 使用Pino的异步写入功能
+- **日志分级**: 生产环境只记录ERROR和WARN级别
- **日志轮转**: 自动清理过期日志文件
-## 安全考虑
+### 🔒 安全加固建议
-### 1. 数据验证
-- **输入验证**: class-validator装饰器
-- **类型检查**: TypeScript静态类型
-- **SQL注入**: TypeORM参数化查询
+#### 1. 数据验证
+- **输入验证**: 使用class-validator进行严格验证
+- **类型检查**: TypeScript静态类型检查
+- **SQL注入防护**: TypeORM参数化查询
-### 2. 认证授权
-- **密码加密**: bcrypt哈希算法
-- **会话管理**: Redis存储会话信息
-- **权限控制**: 基于角色的访问控制
+#### 2. 认证授权
+- **密码安全**: bcrypt加密,强密码策略
+- **会话管理**: JWT + Redis会话存储
+- **权限控制**: 基于角色的访问控制(RBAC)
-### 3. 通信安全
+#### 3. 通信安全
- **HTTPS**: 生产环境强制HTTPS
-- **CORS**: 跨域请求控制
-- **Rate Limiting**: API请求频率限制
\ No newline at end of file
+- **CORS**: 严格的跨域请求控制
+- **Rate Limiting**: API请求频率限制
+
+---
+
+**🏗️ 通过清晰的架构设计,Whale Town 实现了高内聚、低耦合的模块化架构,支持快速开发和灵活部署!**
\ No newline at end of file
diff --git a/docs/CONTRIBUTORS.md b/docs/CONTRIBUTORS.md
index d36f7fb..9b35a49 100644
--- a/docs/CONTRIBUTORS.md
+++ b/docs/CONTRIBUTORS.md
@@ -9,18 +9,22 @@
**moyin** - 主要维护者
- Gitea: [@moyin](https://gitea.xinghangee.icu/moyin)
- Email: xinghang_a@proton.me
-- 提交数: **66 commits**
+- 提交数: **112 commits**
- 主要贡献:
- 🚀 项目架构设计与初始化
- 🔐 完整用户认证系统实现
- 📧 邮箱验证系统设计与开发
- 🗄️ Redis缓存服务(文件存储+真实Redis双模式)
- 📝 完整的API文档系统(Swagger UI + OpenAPI)
- - 🧪 测试框架搭建与114个测试用例编写
+ - 🧪 测试框架搭建与507个测试用例编写
- 📊 高性能日志系统集成(Pino)
- 🔧 项目配置优化与部署方案
- 🐛 验证码TTL重置关键问题修复
- 📚 完整的项目文档体系建设
+ - 🏗️ **Zulip模块架构重构** - 业务功能模块化架构设计与实现
+ - 📖 **架构文档重写** - 详细的架构设计文档和开发者指南
+ - 🔄 **验证码冷却时间优化** - 自动清除机制设计与实现
+ - 📋 **文档清理优化** - 项目文档结构化整理和维护体系建立
### 🌟 核心开发者
@@ -28,18 +32,21 @@
- Gitea: [@ANGJustinl](https://gitea.xinghangee.icu/ANGJustinl)
- GitHub: [@ANGJustinl](https://github.com/ANGJustinl)
- Email: 96008766+ANGJustinl@users.noreply.github.com
-- 提交数: **2 commits**
+- 提交数: **7 commits**
- 主要贡献:
- 🔄 邮箱验证流程重构与优化
- 💾 基于内存的用户服务实现
- 🛠️ API响应处理改进
- 🧪 测试用例完善与错误修复
- 📚 系统架构优化
+ - 💬 **Zulip集成系统** - 完整的Zulip实时通信系统开发
+ - 🔧 **E2E测试修复** - Zulip集成的端到端测试优化
+ - 🎯 **验证码登录测试** - 验证码登录功能测试用例编写
**jianuo** - 核心开发者
- Gitea: [@jianuo](https://gitea.xinghangee.icu/jianuo)
- Email: 32106500027@e.gzhu.edu.cn
-- 提交数: **6 commits**
+- 提交数: **11 commits**
- 主要贡献:
- 🎛️ **管理员后台系统** - 完整的前后端管理界面开发
- 📊 **日志管理功能** - 运行时日志查看与下载系统
@@ -48,14 +55,42 @@
- ⚙️ **TypeScript配置优化** - Node16模块解析配置
- 🐳 **Docker部署优化** - 容器化部署问题修复
- 📖 **技术栈文档更新** - 项目技术栈说明完善
+ - 🔧 **项目配置优化** - 构建和开发环境配置改进
## 贡献统计
| 贡献者 | 提交数 | 主要领域 | 贡献占比 |
|--------|--------|----------|----------|
-| moyin | 66 | 架构设计、核心功能、文档、测试 | 88% |
-| jianuo | 6 | 管理员后台、日志系统、部署优化 | 8% |
-| angjustinl | 2 | 功能优化、测试、重构 | 3% |
+| moyin | 112 | 架构设计、核心功能、文档、测试、Zulip重构 | 86% |
+| jianuo | 11 | 管理员后台、日志系统、部署优化、配置管理 | 8% |
+| angjustinl | 7 | Zulip集成、功能优化、测试、重构 | 5% |
+
+## 🌟 最新重要贡献
+
+### 🏗️ Zulip模块架构重构 (2025年12月31日)
+**主要贡献者**: moyin, angjustinl
+
+这是项目历史上最重要的架构重构之一:
+
+- **架构重构**: 实现业务功能模块化架构,将Zulip模块按照业务层和核心层进行清晰分离
+- **代码迁移**: 36个文件的重构和迁移,涉及2773行代码的新增和125行的删除
+- **依赖注入**: 通过接口抽象实现业务层与核心层的完全解耦
+- **测试完善**: 所有507个测试用例通过,确保重构的安全性
+
+### 📚 项目文档体系优化 (2025年12月31日)
+**主要贡献者**: moyin
+
+- **架构文档重写**: `docs/ARCHITECTURE.md` 从简单架构图扩展为800+行的完整架构设计文档
+- **README优化**: 采用总分结构设计,详细的文件结构总览
+- **文档清理**: 新增 `docs/DOCUMENT_CLEANUP.md` 记录文档维护过程
+- **开发者体验**: 建立完整的文档导航体系,提升开发者上手体验
+
+### 💬 Zulip集成系统 (2025年12月25日)
+**主要贡献者**: angjustinl
+
+- **完整集成**: 实现与Zulip的完整集成,支持实时通信功能
+- **WebSocket支持**: 建立稳定的WebSocket连接和消息处理机制
+- **测试覆盖**: 完善的E2E测试确保集成功能的稳定性
## 项目里程碑
@@ -72,6 +107,13 @@
- **12月20日**: jianuo完善日志管理功能
- **12月21日**: jianuo添加管理员后台单元测试
- **12月22日**: 管理员后台功能合并到主分支
+- **12月25日**: angjustinl开发完整的Zulip集成系统
+- **12月25日**: 实现验证码冷却时间自动清除机制
+- **12月25日**: 完成邮箱冲突检测优化v1.1.1
+- **12月25日**: 升级项目版本到v1.1.0
+- **12月31日**: **重大架构重构** - 完成Zulip模块业务功能模块化架构重构
+- **12月31日**: **文档体系优化** - 项目文档结构化整理和架构文档重写
+- **12月31日**: **测试覆盖完善** - 所有507个测试用例通过,测试覆盖率达到新高
## 如何成为贡献者
diff --git a/docs/README.md b/docs/README.md
index c471c23..33ba129 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -27,7 +27,7 @@
### 📋 **项目管理**
- [贡献指南](CONTRIBUTORS.md) - 如何参与项目贡献
-- [文档清理说明](DOCUMENT_CLEANUP.md) - 文档维护记录
+- [文档清理说明](DOCUMENT_CLEANUP.md) - 文档维护和优化记录
## 🏗️ **文档结构说明**
diff --git a/docs/development/ab164782cdc17e22f9bdf443c7e1e96c.png b/docs/development/ab164782cdc17e22f9bdf443c7e1e96c.png
new file mode 100644
index 0000000..5678dae
Binary files /dev/null and b/docs/development/ab164782cdc17e22f9bdf443c7e1e96c.png differ
diff --git a/docs/development/git_commit_guide.md b/docs/development/git_commit_guide.md
index 484726a..c05d6db 100644
--- a/docs/development/git_commit_guide.md
+++ b/docs/development/git_commit_guide.md
@@ -1,3 +1,7 @@
+
+
+
+
# Git 提交规范
本文档定义了项目的 Git 提交信息格式规范,以保持提交历史的清晰和一致性。
diff --git a/docs/systems/zulip/api.md b/docs/systems/zulip/api.md
index 86e3548..35af5b0 100644
--- a/docs/systems/zulip/api.md
+++ b/docs/systems/zulip/api.md
@@ -5,7 +5,7 @@
### 连接地址
```
-ws://localhost:3000/game
+wss://localhost:3000/game
```
### 连接参数
diff --git a/docs/systems/zulip/quick_tests/test-registered-user.js b/docs/systems/zulip/quick_tests/test-registered-user.js
new file mode 100644
index 0000000..b2e0b1c
--- /dev/null
+++ b/docs/systems/zulip/quick_tests/test-registered-user.js
@@ -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();
diff --git a/nest-cli.json b/nest-cli.json
index f9aa683..98676bf 100644
--- a/nest-cli.json
+++ b/nest-cli.json
@@ -3,6 +3,12 @@
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
- "deleteOutDir": true
+ "deleteOutDir": true,
+ "assets": [
+ {
+ "include": "../config/**/*",
+ "outDir": "./dist"
+ }
+ ]
}
}
diff --git a/src/app.module.ts b/src/app.module.ts
index 858b3cd..9835288 100644
--- a/src/app.module.ts
+++ b/src/app.module.ts
@@ -8,12 +8,13 @@ import { LoggerModule } from './core/utils/logger/logger.module';
import { UsersModule } from './core/db/users/users.module';
import { LoginCoreModule } from './core/login_core/login_core.module';
import { AuthModule } from './business/auth/auth.module';
+import { ZulipModule } from './business/zulip/zulip.module';
import { RedisModule } from './core/redis/redis.module';
import { AdminModule } from './business/admin/admin.module';
import { UserMgmtModule } from './business/user-mgmt/user-mgmt.module';
-import { SecurityModule } from './business/security/security.module';
-import { MaintenanceMiddleware } from './business/security/middleware/maintenance.middleware';
-import { ContentTypeMiddleware } from './business/security/middleware/content-type.middleware';
+import { SecurityCoreModule } from './core/security_core/security_core.module';
+import { MaintenanceMiddleware } from './core/security_core/middleware/maintenance.middleware';
+import { ContentTypeMiddleware } from './core/security_core/middleware/content_type.middleware';
/**
* 检查数据库配置是否完整 by angjustinl 2025-12-17
@@ -67,9 +68,10 @@ function isDatabaseConfigured(): boolean {
isDatabaseConfigured() ? UsersModule.forDatabase() : UsersModule.forMemory(),
LoginCoreModule,
AuthModule,
+ ZulipModule,
UserMgmtModule,
AdminModule,
- SecurityModule,
+ SecurityCoreModule,
],
controllers: [AppController],
providers: [
diff --git a/src/business/admin/admin.controller.ts b/src/business/admin/admin.controller.ts
index cdf2b16..ba9088c 100644
--- a/src/business/admin/admin.controller.ts
+++ b/src/business/admin/admin.controller.ts
@@ -25,7 +25,7 @@ import {
AdminUserResponseDto,
AdminRuntimeLogsResponseDto
} from './dto/admin-response.dto';
-import { Throttle, ThrottlePresets } from '../security/decorators/throttle.decorator';
+import { Throttle, ThrottlePresets } from '../../core/security_core/decorators/throttle.decorator';
import type { Response } from 'express';
import * as fs from 'fs';
import * as path from 'path';
diff --git a/src/business/auth/auth.module.ts b/src/business/auth/auth.module.ts
index 28e8065..60e8e66 100644
--- a/src/business/auth/auth.module.ts
+++ b/src/business/auth/auth.module.ts
@@ -16,11 +16,19 @@ import { Module } from '@nestjs/common';
import { LoginController } from './controllers/login.controller';
import { LoginService } from './services/login.service';
import { LoginCoreModule } from '../../core/login_core/login_core.module';
+import { ZulipCoreModule } from '../../core/zulip/zulip-core.module';
+import { ZulipAccountsModule } from '../../core/db/zulip_accounts/zulip_accounts.module';
@Module({
- imports: [LoginCoreModule],
+ imports: [
+ LoginCoreModule,
+ ZulipCoreModule,
+ ZulipAccountsModule.forRoot(),
+ ],
controllers: [LoginController],
- providers: [LoginService],
+ providers: [
+ LoginService,
+ ],
exports: [LoginService],
})
export class AuthModule {}
\ No newline at end of file
diff --git a/src/business/auth/controllers/login.controller.ts b/src/business/auth/controllers/login.controller.ts
index 37d4a7c..c1d94af 100644
--- a/src/business/auth/controllers/login.controller.ts
+++ b/src/business/auth/controllers/login.controller.ts
@@ -33,8 +33,8 @@ import {
TestModeEmailVerificationResponseDto,
SuccessEmailVerificationResponseDto
} from '../dto/login_response.dto';
-import { Throttle, ThrottlePresets } from '../../security/decorators/throttle.decorator';
-import { Timeout, TimeoutPresets } from '../../security/decorators/timeout.decorator';
+import { Throttle, ThrottlePresets } from '../../../core/security_core/decorators/throttle.decorator';
+import { Timeout, TimeoutPresets } from '../../../core/security_core/decorators/timeout.decorator';
@ApiTags('auth')
@Controller('auth')
diff --git a/src/business/auth/services/login.service.ts b/src/business/auth/services/login.service.ts
index a0f6503..a9bcc0c 100644
--- a/src/business/auth/services/login.service.ts
+++ b/src/business/auth/services/login.service.ts
@@ -16,9 +16,12 @@
* @since 2025-12-17
*/
-import { Injectable, Logger } from '@nestjs/common';
+import { Injectable, Logger, Inject } from '@nestjs/common';
import { LoginCoreService, LoginRequest, RegisterRequest, GitHubOAuthRequest, PasswordResetRequest, AuthResult, VerificationCodeLoginRequest } from '../../../core/login_core/login_core.service';
import { Users } from '../../../core/db/users/users.entity';
+import { ZulipAccountService } from '../../../core/zulip/services/zulip_account.service';
+import { ZulipAccountsRepository } from '../../../core/db/zulip_accounts/zulip_accounts.repository';
+import { ApiKeySecurityService } from '../../../core/zulip/services/api_key_security.service';
/**
* 登录响应数据接口
@@ -65,6 +68,10 @@ export class LoginService {
constructor(
private readonly loginCoreService: LoginCoreService,
+ private readonly zulipAccountService: ZulipAccountService,
+ @Inject('ZulipAccountsRepository')
+ private readonly zulipAccountsRepository: ZulipAccountsRepository,
+ private readonly apiKeySecurityService: ApiKeySecurityService,
) {}
/**
@@ -116,36 +123,106 @@ export class LoginService {
* @returns 注册响应
*/
async register(registerRequest: RegisterRequest): Promise> {
+ const startTime = Date.now();
+
try {
this.logger.log(`用户注册尝试: ${registerRequest.username}`);
- // 调用核心服务进行注册
+ // 1. 初始化Zulip管理员客户端
+ await this.initializeZulipAdminClient();
+
+ // 2. 调用核心服务进行注册
const authResult = await this.loginCoreService.register(registerRequest);
- // 生成访问令牌
+ // 3. 创建Zulip账号(使用相同的邮箱和密码)
+ let zulipAccountCreated = false;
+ try {
+ if (registerRequest.email && registerRequest.password) {
+ await this.createZulipAccountForUser(authResult.user, registerRequest.password);
+ zulipAccountCreated = true;
+
+ this.logger.log(`Zulip账号创建成功: ${registerRequest.username}`, {
+ operation: 'register',
+ gameUserId: authResult.user.id.toString(),
+ email: registerRequest.email,
+ });
+ } else {
+ this.logger.warn(`跳过Zulip账号创建:缺少邮箱或密码`, {
+ operation: 'register',
+ username: registerRequest.username,
+ hasEmail: !!registerRequest.email,
+ hasPassword: !!registerRequest.password,
+ });
+ }
+ } catch (zulipError) {
+ const err = zulipError as Error;
+ this.logger.error(`Zulip账号创建失败,回滚用户注册`, {
+ operation: 'register',
+ username: registerRequest.username,
+ gameUserId: authResult.user.id.toString(),
+ zulipError: err.message,
+ }, err.stack);
+
+ // 回滚游戏用户注册
+ try {
+ await this.loginCoreService.deleteUser(authResult.user.id);
+ this.logger.log(`用户注册回滚成功: ${registerRequest.username}`);
+ } catch (rollbackError) {
+ const rollbackErr = rollbackError as Error;
+ this.logger.error(`用户注册回滚失败`, {
+ operation: 'register',
+ username: registerRequest.username,
+ gameUserId: authResult.user.id.toString(),
+ rollbackError: rollbackErr.message,
+ }, rollbackErr.stack);
+ }
+
+ // 抛出原始错误
+ throw new Error(`注册失败:Zulip账号创建失败 - ${err.message}`);
+ }
+
+ // 4. 生成访问令牌
const accessToken = this.generateAccessToken(authResult.user);
- // 格式化响应数据
+ // 5. 格式化响应数据
const response: LoginResponse = {
user: this.formatUserInfo(authResult.user),
access_token: accessToken,
is_new_user: true,
- message: '注册成功'
+ message: zulipAccountCreated ? '注册成功,Zulip账号已同步创建' : '注册成功'
};
- this.logger.log(`用户注册成功: ${authResult.user.username} (ID: ${authResult.user.id})`);
+ const duration = Date.now() - startTime;
+
+ this.logger.log(`用户注册成功: ${authResult.user.username} (ID: ${authResult.user.id})`, {
+ operation: 'register',
+ gameUserId: authResult.user.id.toString(),
+ username: authResult.user.username,
+ zulipAccountCreated,
+ duration,
+ timestamp: new Date().toISOString(),
+ });
return {
success: true,
data: response,
- message: '注册成功'
+ message: response.message
};
} catch (error) {
- this.logger.error(`用户注册失败: ${registerRequest.username}`, error instanceof Error ? error.stack : String(error));
+ const duration = Date.now() - startTime;
+ const err = error as Error;
+
+ this.logger.error(`用户注册失败: ${registerRequest.username}`, {
+ operation: 'register',
+ username: registerRequest.username,
+ error: err.message,
+ duration,
+ timestamp: new Date().toISOString(),
+ }, err.stack);
return {
success: false,
- message: error instanceof Error ? error.message : '注册失败',
+ message: err.message || '注册失败',
error_code: 'REGISTER_FAILED'
};
}
@@ -592,4 +669,171 @@ export class LoginService {
};
}
}
+
+ /**
+ * 初始化Zulip管理员客户端
+ *
+ * 功能描述:
+ * 使用环境变量中的管理员凭证初始化Zulip客户端
+ *
+ * 业务逻辑:
+ * 1. 从环境变量获取管理员配置
+ * 2. 验证配置完整性
+ * 3. 初始化ZulipAccountService的管理员客户端
+ *
+ * @throws Error 当配置缺失或初始化失败时
+ * @private
+ */
+ private async initializeZulipAdminClient(): Promise {
+ 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 {
+ 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;
+ }
+ }
}
\ No newline at end of file
diff --git a/src/business/auth/services/login.service.zulip-account.spec.ts b/src/business/auth/services/login.service.zulip-account.spec.ts
new file mode 100644
index 0000000..4011b47
--- /dev/null
+++ b/src/business/auth/services/login.service.zulip-account.spec.ts
@@ -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;
+ let zulipAccountService: jest.Mocked;
+ let zulipAccountsRepository: jest.Mocked;
+ let apiKeySecurityService: jest.Mocked;
+
+ // 测试用的模拟数据生成器
+ 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);
+ 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 }
+ );
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/business/user-mgmt/controllers/user-status.controller.ts b/src/business/user-mgmt/controllers/user-status.controller.ts
index e58d197..724dcdd 100644
--- a/src/business/user-mgmt/controllers/user-status.controller.ts
+++ b/src/business/user-mgmt/controllers/user-status.controller.ts
@@ -20,8 +20,8 @@ import { Body, Controller, Get, HttpCode, HttpStatus, Param, Put, Post, UseGuard
import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { AdminGuard } from '../../admin/guards/admin.guard';
import { UserManagementService } from '../services/user-management.service';
-import { Throttle, ThrottlePresets } from '../../security/decorators/throttle.decorator';
-import { Timeout, TimeoutPresets } from '../../security/decorators/timeout.decorator';
+import { Throttle, ThrottlePresets } from '../../../core/security_core/decorators/throttle.decorator';
+import { Timeout, TimeoutPresets } from '../../../core/security_core/decorators/timeout.decorator';
import { UserStatusDto, BatchUserStatusDto } from '../dto/user-status.dto';
import { UserStatusResponseDto, BatchUserStatusResponseDto, UserStatusStatsResponseDto } from '../dto/user-status-response.dto';
diff --git a/src/business/zulip/README.md b/src/business/zulip/README.md
new file mode 100644
index 0000000..3cba68c
--- /dev/null
+++ b/src/business/zulip/README.md
@@ -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 {
+ // 业务逻辑:验证和处理
+ 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调用逻辑
+ });
+});
+```
+
+这种架构设计确保了业务逻辑与技术实现的清晰分离,提高了代码的可维护性和可测试性。
\ No newline at end of file
diff --git a/src/business/zulip/services/message-filter.service.spec.ts b/src/business/zulip/services/message_filter.service.spec.ts
similarity index 97%
rename from src/business/zulip/services/message-filter.service.spec.ts
rename to src/business/zulip/services/message_filter.service.spec.ts
index 904356c..a2bf502 100644
--- a/src/business/zulip/services/message-filter.service.spec.ts
+++ b/src/business/zulip/services/message_filter.service.spec.ts
@@ -12,8 +12,8 @@
import { Test, TestingModule } from '@nestjs/testing';
import * as fc from 'fast-check';
-import { MessageFilterService, ViolationType, ContentFilterResult } from './message-filter.service';
-import { ConfigManagerService } from './config-manager.service';
+import { MessageFilterService, ViolationType } from './message_filter.service';
+import { IZulipConfigService } from '../../../core/zulip/interfaces/zulip-core.interfaces';
import { AppLoggerService } from '../../../core/utils/logger/logger.service';
import { IRedisService } from '../../../core/redis/redis.interface';
@@ -21,7 +21,7 @@ describe('MessageFilterService', () => {
let service: MessageFilterService;
let mockLogger: jest.Mocked;
let mockRedisService: jest.Mocked;
- let mockConfigManager: jest.Mocked;
+ let mockConfigManager: jest.Mocked;
// 内存存储模拟Redis
let memoryStore: Map;
@@ -100,6 +100,14 @@ describe('MessageFilterService', () => {
hasMap: jest.fn().mockImplementation((mapId: string) => {
return ['novice_village', 'tavern', 'market'].includes(mapId);
}),
+ getMapIdByStream: jest.fn(),
+ getTopicByObject: jest.fn(),
+ getZulipConfig: jest.fn(),
+ hasStream: jest.fn(),
+ getAllMapIds: jest.fn(),
+ getAllStreams: jest.fn(),
+ reloadConfig: jest.fn(),
+ validateConfig: jest.fn(),
} as any;
const module: TestingModule = await Test.createTestingModule({
@@ -114,7 +122,7 @@ describe('MessageFilterService', () => {
useValue: mockRedisService,
},
{
- provide: ConfigManagerService,
+ provide: 'ZULIP_CONFIG_SERVICE',
useValue: mockConfigManager,
},
],
diff --git a/src/business/zulip/services/message-filter.service.ts b/src/business/zulip/services/message_filter.service.ts
similarity index 96%
rename from src/business/zulip/services/message-filter.service.ts
rename to src/business/zulip/services/message_filter.service.ts
index aad005b..54ff578 100644
--- a/src/business/zulip/services/message-filter.service.ts
+++ b/src/business/zulip/services/message_filter.service.ts
@@ -30,7 +30,7 @@
import { Injectable, Logger, Inject, forwardRef } from '@nestjs/common';
import { IRedisService } from '../../../core/redis/redis.interface';
-import { ConfigManagerService } from './config-manager.service';
+import { IZulipConfigService } from '../../../core/zulip/interfaces/zulip-core.interfaces';
/**
* 内容过滤结果接口
@@ -90,6 +90,28 @@ export interface SensitiveWordConfig {
category?: string;
}
+/**
+ * 消息过滤服务类
+ *
+ * 职责:
+ * - 实施内容审核和频率控制
+ * - 敏感词过滤和权限验证
+ * - 防止恶意操作和滥用
+ * - 与ConfigManager集成实现位置权限验证
+ *
+ * 主要方法:
+ * - filterContent(): 内容过滤,敏感词检查
+ * - checkRateLimit(): 频率限制检查
+ * - validatePermission(): 权限验证,防止位置欺诈
+ * - validateMessage(): 综合消息验证
+ * - logViolation(): 记录违规行为
+ *
+ * 使用场景:
+ * - 消息发送前的内容审核
+ * - 频率限制和防刷屏
+ * - 权限验证和安全控制
+ * - 违规行为监控和记录
+ */
@Injectable()
export class MessageFilterService {
private readonly RATE_LIMIT_PREFIX = 'zulip:rate_limit:';
@@ -127,8 +149,8 @@ export class MessageFilterService {
constructor(
@Inject('REDIS_SERVICE')
private readonly redisService: IRedisService,
- @Inject(forwardRef(() => ConfigManagerService))
- private readonly configManager: ConfigManagerService,
+ @Inject('ZULIP_CONFIG_SERVICE')
+ private readonly configManager: IZulipConfigService,
) {
this.logger.log('MessageFilterService初始化完成');
}
diff --git a/src/business/zulip/services/session_cleanup.service.spec.ts b/src/business/zulip/services/session_cleanup.service.spec.ts
new file mode 100644
index 0000000..3e0469d
--- /dev/null
+++ b/src/business/zulip/services/session_cleanup.service.spec.ts
@@ -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;
+ let mockZulipClientPool: jest.Mocked;
+
+ // 模拟清理结果
+ const createMockCleanupResult = (overrides: Partial = {}): 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);
+ });
+
+ 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(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 = {
+ 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 = {
+ 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 = {
+ 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(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);
+
+ // 模拟模块初始化
+ 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);
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/business/zulip/services/session-cleanup.service.ts b/src/business/zulip/services/session_cleanup.service.ts
similarity index 86%
rename from src/business/zulip/services/session-cleanup.service.ts
rename to src/business/zulip/services/session_cleanup.service.ts
index 3f5fc0c..66f1639 100644
--- a/src/business/zulip/services/session-cleanup.service.ts
+++ b/src/business/zulip/services/session_cleanup.service.ts
@@ -21,9 +21,9 @@
* @since 2025-12-25
*/
-import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
-import { SessionManagerService } from './session-manager.service';
-import { ZulipClientPoolService } from './zulip-client-pool.service';
+import { Injectable, Logger, OnModuleInit, OnModuleDestroy, Inject } from '@nestjs/common';
+import { SessionManagerService } from './session_manager.service';
+import { IZulipClientPoolService } from '../../../core/zulip/interfaces/zulip-core.interfaces';
/**
* 清理任务配置接口
@@ -55,6 +55,28 @@ export interface CleanupResult {
error?: string;
}
+/**
+ * 会话清理服务类
+ *
+ * 职责:
+ * - 定时清理过期的游戏会话
+ * - 释放无效的Zulip客户端资源
+ * - 维护会话数据的一致性
+ * - 提供会话清理统计和监控
+ *
+ * 主要方法:
+ * - startCleanup(): 启动定时清理任务
+ * - stopCleanup(): 停止清理任务
+ * - performCleanup(): 执行一次清理操作
+ * - getCleanupStats(): 获取清理统计信息
+ * - updateConfig(): 更新清理配置
+ *
+ * 使用场景:
+ * - 系统启动时自动开始清理任务
+ * - 定期清理过期会话和资源
+ * - 系统关闭时停止清理任务
+ * - 监控清理效果和系统健康
+ */
@Injectable()
export class SessionCleanupService implements OnModuleInit, OnModuleDestroy {
private cleanupInterval: NodeJS.Timeout | null = null;
@@ -70,7 +92,8 @@ export class SessionCleanupService implements OnModuleInit, OnModuleDestroy {
constructor(
private readonly sessionManager: SessionManagerService,
- private readonly zulipClientPool: ZulipClientPoolService,
+ @Inject('ZULIP_CLIENT_POOL_SERVICE')
+ private readonly zulipClientPool: IZulipClientPoolService,
) {
this.logger.log('SessionCleanupService初始化完成');
}
@@ -176,7 +199,8 @@ export class SessionCleanupService implements OnModuleInit, OnModuleDestroy {
// 2. 注销对应的Zulip事件队列
let deregisteredQueues = 0;
- for (const queueId of cleanupResult.zulipQueueIds) {
+ const queueIds = cleanupResult?.zulipQueueIds || [];
+ for (const queueId of queueIds) {
try {
// 根据queueId找到对应的用户并注销队列
// 注意:这里需要通过某种方式找到queueId对应的userId
@@ -200,7 +224,7 @@ export class SessionCleanupService implements OnModuleInit, OnModuleDestroy {
const duration = Date.now() - startTime;
const result: CleanupResult = {
- cleanedSessions: cleanupResult.cleanedCount,
+ cleanedSessions: cleanupResult?.cleanedCount || 0,
deregisteredQueues,
duration,
timestamp: new Date(),
diff --git a/src/business/zulip/services/session-manager.service.spec.ts b/src/business/zulip/services/session_manager.service.spec.ts
similarity index 97%
rename from src/business/zulip/services/session-manager.service.spec.ts
rename to src/business/zulip/services/session_manager.service.spec.ts
index 9cd3db2..fef1cce 100644
--- a/src/business/zulip/services/session-manager.service.spec.ts
+++ b/src/business/zulip/services/session_manager.service.spec.ts
@@ -12,8 +12,8 @@
import { Test, TestingModule } from '@nestjs/testing';
import * as fc from 'fast-check';
-import { SessionManagerService, GameSession, Position } from './session-manager.service';
-import { ConfigManagerService } from './config-manager.service';
+import { SessionManagerService, GameSession, Position } from './session_manager.service';
+import { IZulipConfigService } from '../../../core/zulip/interfaces/zulip-core.interfaces';
import { AppLoggerService } from '../../../core/utils/logger/logger.service';
import { IRedisService } from '../../../core/redis/redis.interface';
@@ -21,7 +21,7 @@ describe('SessionManagerService', () => {
let service: SessionManagerService;
let mockLogger: jest.Mocked;
let mockRedisService: jest.Mocked;
- let mockConfigManager: jest.Mocked;
+ let mockConfigManager: jest.Mocked;
// 内存存储模拟Redis
let memoryStore: Map;
@@ -57,9 +57,15 @@ describe('SessionManagerService', () => {
};
return streamMap[mapId] || 'General';
}),
+ getMapIdByStream: jest.fn(),
getTopicByObject: jest.fn().mockReturnValue('General'),
- getMapConfig: jest.fn(),
- getAllMaps: jest.fn(),
+ getZulipConfig: jest.fn(),
+ hasMap: jest.fn(),
+ hasStream: jest.fn(),
+ getAllMapIds: jest.fn(),
+ getAllStreams: jest.fn(),
+ reloadConfig: jest.fn(),
+ validateConfig: jest.fn(),
} as any;
// 创建模拟Redis服务,使用内存存储
@@ -135,7 +141,7 @@ describe('SessionManagerService', () => {
useValue: mockRedisService,
},
{
- provide: ConfigManagerService,
+ provide: 'ZULIP_CONFIG_SERVICE',
useValue: mockConfigManager,
},
],
diff --git a/src/business/zulip/services/session-manager.service.ts b/src/business/zulip/services/session_manager.service.ts
similarity index 95%
rename from src/business/zulip/services/session-manager.service.ts
rename to src/business/zulip/services/session_manager.service.ts
index 3db5580..5490201 100644
--- a/src/business/zulip/services/session-manager.service.ts
+++ b/src/business/zulip/services/session_manager.service.ts
@@ -35,8 +35,8 @@
import { Injectable, Logger, Inject } from '@nestjs/common';
import { IRedisService } from '../../../core/redis/redis.interface';
-import { ConfigManagerService } from './config-manager.service';
-import { Internal, Constants } from '../interfaces/zulip.interfaces';
+import { IZulipConfigService } from '../../../core/zulip/interfaces/zulip-core.interfaces';
+import { Internal, Constants } from '../../../core/zulip/interfaces/zulip.interfaces';
/**
* 游戏会话接口 - 重新导出以保持向后兼容
@@ -78,6 +78,29 @@ export interface SessionStats {
newestSession?: Date;
}
+/**
+ * 会话管理服务类
+ *
+ * 职责:
+ * - 维护WebSocket连接ID与Zulip队列ID的映射关系
+ * - 管理玩家位置跟踪和上下文注入
+ * - 提供空间过滤和会话查询功能
+ * - 支持会话状态的序列化和反序列化
+ *
+ * 主要方法:
+ * - createSession(): 创建会话并绑定Socket_ID与Zulip_Queue_ID
+ * - getSession(): 获取会话信息
+ * - injectContext(): 上下文注入,根据位置确定Stream/Topic
+ * - getSocketsInMap(): 空间过滤,获取指定地图的所有Socket
+ * - updatePlayerPosition(): 更新玩家位置
+ * - destroySession(): 销毁会话
+ *
+ * 使用场景:
+ * - 玩家登录时创建会话映射
+ * - 消息路由时进行上下文注入
+ * - 消息分发时进行空间过滤
+ * - 玩家登出时清理会话数据
+ */
@Injectable()
export class SessionManagerService {
private readonly SESSION_PREFIX = 'zulip:session:';
@@ -91,7 +114,8 @@ export class SessionManagerService {
constructor(
@Inject('REDIS_SERVICE')
private readonly redisService: IRedisService,
- private readonly configManager: ConfigManagerService,
+ @Inject('ZULIP_CONFIG_SERVICE')
+ private readonly configManager: IZulipConfigService,
) {
this.logger.log('SessionManagerService初始化完成');
}
@@ -170,6 +194,9 @@ export class SessionManagerService {
* @param initialMap 初始地图(可选)
* @param initialPosition 初始位置(可选)
* @returns Promise 创建的会话对象
+ *
+ * @throws Error 当参数验证失败时
+ * @throws Error 当Redis操作失败时
*/
async createSession(
socketId: string,
@@ -378,6 +405,8 @@ export class SessionManagerService {
* @param socketId WebSocket连接ID
* @param mapId 地图ID(可选,用于覆盖当前地图)
* @returns Promise 上下文信息
+ *
+ * @throws Error 当会话不存在时
*/
async injectContext(socketId: string, mapId?: string): Promise {
this.logger.debug('开始上下文注入', {
diff --git a/src/business/zulip/services/zulip-event-processor.service.spec.ts b/src/business/zulip/services/zulip_event_processor.service.spec.ts
similarity index 97%
rename from src/business/zulip/services/zulip-event-processor.service.spec.ts
rename to src/business/zulip/services/zulip_event_processor.service.spec.ts
index bfd3b5b..ef0cf63 100644
--- a/src/business/zulip/services/zulip-event-processor.service.spec.ts
+++ b/src/business/zulip/services/zulip_event_processor.service.spec.ts
@@ -24,18 +24,17 @@ import {
ZulipMessage,
GameMessage,
MessageDistributor,
-} from './zulip-event-processor.service';
-import { SessionManagerService, GameSession } from './session-manager.service';
-import { ConfigManagerService } from './config-manager.service';
-import { ZulipClientPoolService } from './zulip-client-pool.service';
+} from './zulip_event_processor.service';
+import { SessionManagerService, GameSession } from './session_manager.service';
+import { IZulipConfigService, IZulipClientPoolService } from '../../../core/zulip/interfaces/zulip-core.interfaces';
import { AppLoggerService } from '../../../core/utils/logger/logger.service';
describe('ZulipEventProcessorService', () => {
let service: ZulipEventProcessorService;
let mockLogger: jest.Mocked;
let mockSessionManager: jest.Mocked;
- let mockConfigManager: jest.Mocked;
- let mockClientPool: jest.Mocked;
+ let mockConfigManager: jest.Mocked;
+ let mockClientPool: jest.Mocked;
let mockDistributor: jest.Mocked;
// 创建模拟Zulip消息
@@ -87,14 +86,26 @@ describe('ZulipEventProcessorService', () => {
mockConfigManager = {
getMapIdByStream: jest.fn(),
getStreamByMap: jest.fn(),
- getMapConfig: jest.fn(),
+ getTopicByObject: jest.fn(),
+ getZulipConfig: jest.fn(),
+ hasMap: jest.fn(),
+ hasStream: jest.fn(),
getAllMapIds: jest.fn(),
+ getAllStreams: jest.fn(),
+ reloadConfig: jest.fn(),
+ validateConfig: jest.fn(),
} as any;
mockClientPool = {
getUserClient: jest.fn(),
createUserClient: jest.fn(),
destroyUserClient: jest.fn(),
+ hasUserClient: jest.fn(),
+ sendMessage: jest.fn(),
+ registerEventQueue: jest.fn(),
+ deregisterEventQueue: jest.fn(),
+ getPoolStats: jest.fn(),
+ cleanupIdleClients: jest.fn(),
} as any;
mockDistributor = {
@@ -114,11 +125,11 @@ describe('ZulipEventProcessorService', () => {
useValue: mockSessionManager,
},
{
- provide: ConfigManagerService,
+ provide: 'ZULIP_CONFIG_SERVICE',
useValue: mockConfigManager,
},
{
- provide: ZulipClientPoolService,
+ provide: 'ZULIP_CLIENT_POOL_SERVICE',
useValue: mockClientPool,
},
],
diff --git a/src/business/zulip/services/zulip-event-processor.service.ts b/src/business/zulip/services/zulip_event_processor.service.ts
similarity index 96%
rename from src/business/zulip/services/zulip-event-processor.service.ts
rename to src/business/zulip/services/zulip_event_processor.service.ts
index d3f69b7..b034c33 100644
--- a/src/business/zulip/services/zulip-event-processor.service.ts
+++ b/src/business/zulip/services/zulip_event_processor.service.ts
@@ -31,9 +31,8 @@
*/
import { Injectable, OnModuleDestroy, Inject, forwardRef, Logger } from '@nestjs/common';
-import { SessionManagerService } from './session-manager.service';
-import { ConfigManagerService } from './config-manager.service';
-import { ZulipClientPoolService } from './zulip-client-pool.service';
+import { SessionManagerService } from './session_manager.service';
+import { IZulipConfigService, IZulipClientPoolService } from '../../../core/zulip/interfaces/zulip-core.interfaces';
/**
* Zulip消息接口
@@ -94,6 +93,28 @@ export interface EventProcessingStats {
lastEventTime?: Date;
}
+/**
+ * Zulip事件处理服务类
+ *
+ * 职责:
+ * - 处理从Zulip接收的事件队列消息
+ * - 将Zulip消息转换为游戏协议格式
+ * - 管理事件队列的生命周期
+ * - 提供消息分发和路由功能
+ *
+ * 主要方法:
+ * - processEvents(): 处理Zulip事件队列
+ * - processMessage(): 处理单个消息事件
+ * - startProcessing(): 启动事件处理
+ * - stopProcessing(): 停止事件处理
+ * - registerQueue(): 注册新的事件队列
+ *
+ * 使用场景:
+ * - 接收Zulip服务器推送的消息
+ * - 将Zulip消息转发给游戏客户端
+ * - 管理多用户的事件队列
+ * - 消息格式转换和过滤
+ */
@Injectable()
export class ZulipEventProcessorService implements OnModuleDestroy {
private readonly logger = new Logger(ZulipEventProcessorService.name);
@@ -109,9 +130,10 @@ export class ZulipEventProcessorService implements OnModuleDestroy {
constructor(
private readonly sessionManager: SessionManagerService,
- private readonly configManager: ConfigManagerService,
- @Inject(forwardRef(() => ZulipClientPoolService))
- private readonly clientPool: ZulipClientPoolService,
+ @Inject('ZULIP_CONFIG_SERVICE')
+ private readonly configManager: IZulipConfigService,
+ @Inject('ZULIP_CLIENT_POOL_SERVICE')
+ private readonly clientPool: IZulipClientPoolService,
) {
this.logger.log('ZulipEventProcessorService初始化完成');
}
diff --git a/src/business/zulip/zulip.module.ts b/src/business/zulip/zulip.module.ts
index e587d58..13590aa 100644
--- a/src/business/zulip/zulip.module.ts
+++ b/src/business/zulip/zulip.module.ts
@@ -2,26 +2,32 @@
* Zulip集成业务模块
*
* 功能描述:
- * - 整合Zulip集成相关的控制器、服务和依赖
- * - 提供完整的Zulip集成功能模块
- * - 实现游戏与Zulip的无缝通信桥梁
- * - 支持WebSocket网关、会话管理、消息过滤等核心功能
- * - 启动时自动检查并创建所有地图对应的Zulip Streams
+ * - 整合Zulip集成相关的业务逻辑和控制器
+ * - 提供完整的Zulip集成业务功能模块
+ * - 实现游戏与Zulip的业务逻辑协调
+ * - 支持WebSocket网关、会话管理、消息过滤等业务功能
*
- * 核心服务:
- * - ZulipService: 主协调服务,处理登录、消息发送等核心业务
+ * 架构设计:
+ * - 业务逻辑层:处理游戏相关的业务规则和流程
+ * - 核心服务层:封装技术实现细节和第三方API调用
+ * - 通过依赖注入实现业务层与技术层的解耦
+ *
+ * 业务服务:
+ * - ZulipService: 主协调服务,处理登录、消息发送等核心业务流程
* - ZulipWebSocketGateway: WebSocket统一网关,处理客户端连接
- * - ZulipClientPoolService: Zulip客户端池管理
- * - SessionManagerService: 会话状态管理
- * - MessageFilterService: 消息过滤和安全控制
+ * - SessionManagerService: 会话状态管理和业务逻辑
+ * - MessageFilterService: 消息过滤和业务规则控制
+ *
+ * 核心服务(通过ZulipCoreModule提供):
+ * - ZulipClientService: Zulip REST API封装
+ * - ZulipClientPoolService: 客户端池管理
* - ConfigManagerService: 配置管理和热重载
- * - StreamInitializerService: Stream初始化和自动创建
- * - ErrorHandlerService: 错误处理和服务降级
- * - MonitoringService: 系统监控和告警
- * - ApiKeySecurityService: API Key安全存储
+ * - ZulipEventProcessorService: 事件处理和消息转换
+ * - 其他技术支持服务
*
* 依赖模块:
- * - LoginModule: 用户认证和会话管理
+ * - ZulipCoreModule: Zulip核心技术服务
+ * - LoginCoreModule: 用户认证和会话管理
* - RedisModule: 会话状态缓存
* - LoggerModule: 日志记录服务
*
@@ -29,65 +35,47 @@
* - 游戏客户端通过WebSocket连接进行实时聊天
* - 游戏内消息与Zulip社群的双向同步
* - 基于位置的聊天上下文管理
- * - 系统启动时自动初始化所有地图对应的Streams
+ * - 业务规则驱动的消息过滤和权限控制
*
* @author angjustinl
- * @version 1.0.0
- * @since 2025-12-25
+ * @version 2.0.0
+ * @since 2025-12-31
*/
import { Module } from '@nestjs/common';
-import { ZulipWebSocketGateway } from './zulip-websocket.gateway';
+import { ZulipWebSocketGateway } from './zulip_websocket.gateway';
import { ZulipService } from './zulip.service';
-import { ZulipClientService } from './services/zulip-client.service';
-import { ZulipClientPoolService } from './services/zulip-client-pool.service';
-import { SessionManagerService } from './services/session-manager.service';
-import { SessionCleanupService } from './services/session-cleanup.service';
-import { MessageFilterService } from './services/message-filter.service';
-import { ZulipEventProcessorService } from './services/zulip-event-processor.service';
-import { ConfigManagerService } from './services/config-manager.service';
-import { ErrorHandlerService } from './services/error-handler.service';
-import { MonitoringService } from './services/monitoring.service';
-import { ApiKeySecurityService } from './services/api-key-security.service';
-import { StreamInitializerService } from './services/stream-initializer.service';
+import { SessionManagerService } from './services/session_manager.service';
+import { MessageFilterService } from './services/message_filter.service';
+import { ZulipEventProcessorService } from './services/zulip_event_processor.service';
+import { SessionCleanupService } from './services/session_cleanup.service';
+import { ZulipCoreModule } from '../../core/zulip/zulip-core.module';
import { RedisModule } from '../../core/redis/redis.module';
import { LoggerModule } from '../../core/utils/logger/logger.module';
-import { LoginModule } from '../login/login.module';
+import { LoginCoreModule } from '../../core/login_core/login_core.module';
@Module({
imports: [
+ // Zulip核心服务模块 - 提供技术实现相关的核心服务
+ ZulipCoreModule,
// Redis模块 - 提供会话状态缓存和数据存储
RedisModule,
// 日志模块 - 提供统一的日志记录服务
LoggerModule,
// 登录模块 - 提供用户认证和Token验证
- LoginModule,
+ LoginCoreModule,
],
providers: [
// 主协调服务 - 整合各子服务,提供统一业务接口
ZulipService,
- // Zulip客户端服务 - 封装Zulip REST API调用
- ZulipClientService,
- // Zulip客户端池服务 - 管理用户专用Zulip客户端实例
- ZulipClientPoolService,
// 会话管理服务 - 维护Socket_ID与Zulip_Queue_ID的映射关系
SessionManagerService,
- // 会话清理服务 - 定时清理过期会话
- SessionCleanupService,
// 消息过滤服务 - 敏感词过滤、频率限制、权限验证
MessageFilterService,
// Zulip事件处理服务 - 处理Zulip事件队列消息
ZulipEventProcessorService,
- // 配置管理服务 - 地图映射配置和系统配置管理
- ConfigManagerService,
- // Stream初始化服务 - 启动时检查并创建所有地图对应的Streams
- StreamInitializerService,
- // 错误处理服务 - 错误处理、重试机制、服务降级
- ErrorHandlerService,
- // 监控服务 - 系统监控、健康检查、告警
- MonitoringService,
- // API Key安全服务 - API Key加密存储和安全日志
- ApiKeySecurityService,
+ // 会话清理服务 - 定时清理过期会话
+ SessionCleanupService,
// WebSocket网关 - 处理游戏客户端WebSocket连接
ZulipWebSocketGateway,
],
@@ -95,26 +83,14 @@ import { LoginModule } from '../login/login.module';
exports: [
// 导出主服务供其他模块使用
ZulipService,
- // 导出Zulip客户端服务
- ZulipClientService,
- // 导出客户端池服务
- ZulipClientPoolService,
// 导出会话管理服务
SessionManagerService,
- // 导出会话清理服务
- SessionCleanupService,
// 导出消息过滤服务
MessageFilterService,
- // 导出配置管理服务
- ConfigManagerService,
- // 导出Stream初始化服务
- StreamInitializerService,
- // 导出错误处理服务
- ErrorHandlerService,
- // 导出监控服务
- MonitoringService,
- // 导出API Key安全服务
- ApiKeySecurityService,
+ // 导出事件处理服务
+ ZulipEventProcessorService,
+ // 导出会话清理服务
+ SessionCleanupService,
// 导出WebSocket网关
ZulipWebSocketGateway,
],
diff --git a/src/business/zulip/zulip.service.spec.ts b/src/business/zulip/zulip.service.spec.ts
new file mode 100644
index 0000000..aa021ce
--- /dev/null
+++ b/src/business/zulip/zulip.service.spec.ts
@@ -0,0 +1,1134 @@
+/**
+ * Zulip集成主服务测试
+ *
+ * 功能描述:
+ * - 测试ZulipService的核心功能
+ * - 包含属性测试验证玩家登录流程完整性
+ * - 包含属性测试验证消息发送流程完整性
+ * - 包含属性测试验证位置更新和上下文注入
+ *
+ * **Feature: zulip-integration, Property 1: 玩家登录流程完整性**
+ * **Validates: Requirements 1.1, 1.2, 1.3, 1.4, 1.5**
+ *
+ * **Feature: zulip-integration, Property 3: 消息发送流程完整性**
+ * **Validates: Requirements 3.1, 3.2, 3.3, 3.4, 3.5**
+ *
+ * **Feature: zulip-integration, Property 6: 位置更新和上下文注入**
+ * **Validates: Requirements 4.1, 4.2, 4.3, 4.4**
+ *
+ * @author angjustinl
+ * @version 1.0.0
+ * @since 2025-12-31
+ */
+
+import { Test, TestingModule } from '@nestjs/testing';
+import * as fc from 'fast-check';
+import {
+ ZulipService,
+ PlayerLoginRequest,
+ ChatMessageRequest,
+ PositionUpdateRequest,
+ LoginResponse,
+ ChatMessageResponse,
+} from './zulip.service';
+import { SessionManagerService, GameSession } from './services/session_manager.service';
+import { MessageFilterService } from './services/message_filter.service';
+import { ZulipEventProcessorService } from './services/zulip_event_processor.service';
+import {
+ IZulipClientPoolService,
+ IZulipConfigService,
+ ZulipClientInstance,
+ SendMessageResult,
+} from '../../core/zulip/interfaces/zulip-core.interfaces';
+
+describe('ZulipService', () => {
+ let service: ZulipService;
+ let mockZulipClientPool: jest.Mocked;
+ let mockSessionManager: jest.Mocked;
+ let mockMessageFilter: jest.Mocked;
+ let mockEventProcessor: jest.Mocked;
+ let mockConfigManager: jest.Mocked;
+
+ // 创建模拟的Zulip客户端实例
+ const createMockClientInstance = (overrides: Partial = {}): ZulipClientInstance => ({
+ userId: 'test-user-123',
+ config: {
+ username: 'test@example.com',
+ apiKey: 'test-api-key',
+ realm: 'https://zulip.example.com',
+ },
+ client: {},
+ queueId: 'queue-123',
+ lastEventId: 0,
+ createdAt: new Date(),
+ lastActivity: new Date(),
+ isValid: true,
+ ...overrides,
+ });
+
+ // 创建模拟的游戏会话
+ const createMockSession = (overrides: Partial = {}): GameSession => ({
+ socketId: 'socket-123',
+ userId: 'user-123',
+ username: 'TestPlayer',
+ zulipQueueId: 'queue-123',
+ currentMap: 'whale_port',
+ position: { x: 400, y: 300 },
+ lastActivity: new Date(),
+ createdAt: new Date(),
+ ...overrides,
+ });
+
+ beforeEach(async () => {
+ jest.clearAllMocks();
+
+ 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;
+
+ mockSessionManager = {
+ createSession: jest.fn(),
+ getSession: jest.fn(),
+ destroySession: jest.fn(),
+ updatePlayerPosition: jest.fn(),
+ getSocketsInMap: jest.fn(),
+ injectContext: jest.fn(),
+ cleanupExpiredSessions: jest.fn(),
+ } as any;
+
+ mockMessageFilter = {
+ validateMessage: jest.fn(),
+ filterContent: jest.fn(),
+ checkRateLimit: jest.fn(),
+ validatePermission: jest.fn(),
+ logViolation: jest.fn(),
+ } as any;
+
+ mockEventProcessor = {
+ startEventProcessing: jest.fn(),
+ stopEventProcessing: jest.fn(),
+ registerEventQueue: jest.fn(),
+ unregisterEventQueue: jest.fn(),
+ processMessageEvent: jest.fn(),
+ setMessageDistributor: jest.fn(),
+ getProcessingStats: jest.fn(),
+ } as any;
+
+ mockConfigManager = {
+ getStreamByMap: jest.fn(),
+ getMapIdByStream: jest.fn(),
+ getTopicByObject: jest.fn(),
+ getZulipConfig: jest.fn(),
+ hasMap: jest.fn(),
+ hasStream: jest.fn(),
+ getAllMapIds: jest.fn(),
+ getAllStreams: jest.fn(),
+ reloadConfig: jest.fn(),
+ validateConfig: jest.fn(),
+ } as any;
+
+ const module: TestingModule = await Test.createTestingModule({
+ providers: [
+ ZulipService,
+ {
+ provide: 'ZULIP_CLIENT_POOL_SERVICE',
+ useValue: mockZulipClientPool,
+ },
+ {
+ provide: SessionManagerService,
+ useValue: mockSessionManager,
+ },
+ {
+ provide: MessageFilterService,
+ useValue: mockMessageFilter,
+ },
+ {
+ provide: ZulipEventProcessorService,
+ useValue: mockEventProcessor,
+ },
+ {
+ provide: 'ZULIP_CONFIG_SERVICE',
+ useValue: mockConfigManager,
+ },
+ ],
+ }).compile();
+
+ service = module.get(ZulipService);
+ });
+
+ it('should be defined', () => {
+ expect(service).toBeDefined();
+ });
+
+ describe('handlePlayerLogin - 处理玩家登录', () => {
+ it('应该成功处理有效Token的登录请求', async () => {
+ const loginRequest: PlayerLoginRequest = {
+ token: 'valid_token_123',
+ socketId: 'socket-456',
+ };
+
+ const mockSession = createMockSession({
+ socketId: 'socket-456',
+ userId: 'user_valid_to',
+ username: 'Player_lid_to',
+ });
+
+ mockConfigManager.getZulipConfig.mockReturnValue({
+ zulipServerUrl: 'https://zulip.example.com',
+ });
+
+ mockZulipClientPool.createUserClient.mockResolvedValue(
+ createMockClientInstance({
+ userId: 'user_valid_to',
+ queueId: 'queue-789',
+ })
+ );
+
+ mockSessionManager.createSession.mockResolvedValue(mockSession);
+
+ const result = await service.handlePlayerLogin(loginRequest);
+
+ expect(result.success).toBe(true);
+ expect(result.userId).toBe('user_valid_to');
+ expect(result.username).toBe('Player_valid');
+ expect(result.currentMap).toBe('whale_port');
+ expect(mockSessionManager.createSession).toHaveBeenCalled();
+ });
+
+ it('应该拒绝无效Token的登录请求', async () => {
+ const loginRequest: PlayerLoginRequest = {
+ token: 'invalid_token',
+ socketId: 'socket-456',
+ };
+
+ const result = await service.handlePlayerLogin(loginRequest);
+
+ expect(result.success).toBe(false);
+ expect(result.error).toBe('Token验证失败');
+ expect(mockSessionManager.createSession).not.toHaveBeenCalled();
+ });
+
+ it('应该处理空Token的情况', async () => {
+ const loginRequest: PlayerLoginRequest = {
+ token: '',
+ socketId: 'socket-456',
+ };
+
+ const result = await service.handlePlayerLogin(loginRequest);
+
+ expect(result.success).toBe(false);
+ expect(result.error).toBe('Token不能为空');
+ });
+
+ it('应该处理空socketId的情况', async () => {
+ const loginRequest: PlayerLoginRequest = {
+ token: 'valid_token',
+ socketId: '',
+ };
+
+ const result = await service.handlePlayerLogin(loginRequest);
+
+ expect(result.success).toBe(false);
+ expect(result.error).toBe('socketId不能为空');
+ });
+ it('应该在Zulip客户端创建失败时使用本地模式', async () => {
+ const loginRequest: PlayerLoginRequest = {
+ token: 'real_user_token_with_zulip_key_123', // 有API Key的Token
+ socketId: 'socket-456',
+ };
+
+ const mockSession = createMockSession({
+ socketId: 'socket-456',
+ userId: 'user_real_user_',
+ });
+
+ mockConfigManager.getZulipConfig.mockReturnValue({
+ zulipServerUrl: 'https://zulip.example.com',
+ });
+
+ // 模拟Zulip客户端创建失败
+ mockZulipClientPool.createUserClient.mockRejectedValue(new Error('Zulip连接失败'));
+ mockSessionManager.createSession.mockResolvedValue(mockSession);
+
+ const result = await service.handlePlayerLogin(loginRequest);
+
+ // 应该成功登录(本地模式)
+ expect(result.success).toBe(true);
+ expect(mockSessionManager.createSession).toHaveBeenCalled();
+ });
+ });
+
+ describe('handlePlayerLogout - 处理玩家登出', () => {
+ it('应该成功处理玩家登出', async () => {
+ const socketId = 'socket-123';
+ const mockSession = createMockSession({ socketId, userId: 'user-123' });
+
+ mockSessionManager.getSession.mockResolvedValue(mockSession);
+ mockZulipClientPool.destroyUserClient.mockResolvedValue();
+ mockSessionManager.destroySession.mockResolvedValue(undefined);
+
+ await service.handlePlayerLogout(socketId);
+
+ expect(mockSessionManager.getSession).toHaveBeenCalledWith(socketId);
+ expect(mockZulipClientPool.destroyUserClient).toHaveBeenCalledWith('user-123');
+ expect(mockSessionManager.destroySession).toHaveBeenCalledWith(socketId);
+ });
+
+ it('应该处理会话不存在的情况', async () => {
+ const socketId = 'non-existent-socket';
+
+ mockSessionManager.getSession.mockResolvedValue(null);
+
+ await service.handlePlayerLogout(socketId);
+
+ expect(mockSessionManager.getSession).toHaveBeenCalledWith(socketId);
+ expect(mockZulipClientPool.destroyUserClient).not.toHaveBeenCalled();
+ expect(mockSessionManager.destroySession).not.toHaveBeenCalled();
+ });
+
+ it('应该在Zulip客户端清理失败时继续执行会话清理', async () => {
+ const socketId = 'socket-123';
+ const mockSession = createMockSession({ socketId, userId: 'user-123' });
+
+ mockSessionManager.getSession.mockResolvedValue(mockSession);
+ mockZulipClientPool.destroyUserClient.mockRejectedValue(new Error('清理失败'));
+ mockSessionManager.destroySession.mockResolvedValue(undefined);
+
+ await service.handlePlayerLogout(socketId);
+
+ expect(mockZulipClientPool.destroyUserClient).toHaveBeenCalledWith('user-123');
+ expect(mockSessionManager.destroySession).toHaveBeenCalledWith(socketId);
+ });
+ });
+
+ describe('sendChatMessage - 发送聊天消息', () => {
+ it('应该成功发送聊天消息', async () => {
+ const chatRequest: ChatMessageRequest = {
+ socketId: 'socket-123',
+ content: 'Hello, world!',
+ scope: 'local',
+ };
+
+ const mockSession = createMockSession({
+ socketId: 'socket-123',
+ userId: 'user-123',
+ currentMap: 'tavern',
+ });
+
+ mockSessionManager.getSession.mockResolvedValue(mockSession);
+ mockSessionManager.injectContext.mockResolvedValue({
+ stream: 'Tavern',
+ topic: 'General',
+ });
+
+ mockMessageFilter.validateMessage.mockResolvedValue({
+ allowed: true,
+ filteredContent: 'Hello, world!',
+ });
+
+ mockZulipClientPool.sendMessage.mockResolvedValue({
+ success: true,
+ messageId: 12345,
+ });
+
+ const result = await service.sendChatMessage(chatRequest);
+
+ expect(result.success).toBe(true);
+ expect(result.messageId).toBe(12345);
+ expect(mockZulipClientPool.sendMessage).toHaveBeenCalledWith(
+ 'user-123',
+ 'Tavern',
+ 'General',
+ 'Hello, world!'
+ );
+ });
+
+ it('应该拒绝会话不存在的消息发送', async () => {
+ const chatRequest: ChatMessageRequest = {
+ socketId: 'non-existent-socket',
+ content: 'Hello, world!',
+ scope: 'local',
+ };
+
+ mockSessionManager.getSession.mockResolvedValue(null);
+
+ const result = await service.sendChatMessage(chatRequest);
+
+ expect(result.success).toBe(false);
+ expect(result.error).toBe('会话不存在,请重新登录');
+ expect(mockZulipClientPool.sendMessage).not.toHaveBeenCalled();
+ });
+
+ it('应该拒绝未通过验证的消息', async () => {
+ const chatRequest: ChatMessageRequest = {
+ socketId: 'socket-123',
+ content: '敏感词内容',
+ scope: 'local',
+ };
+
+ const mockSession = createMockSession({ socketId: 'socket-123' });
+
+ mockSessionManager.getSession.mockResolvedValue(mockSession);
+ mockSessionManager.injectContext.mockResolvedValue({
+ stream: 'Tavern',
+ topic: 'General',
+ });
+
+ mockMessageFilter.validateMessage.mockResolvedValue({
+ allowed: false,
+ reason: '消息包含敏感词',
+ });
+
+ const result = await service.sendChatMessage(chatRequest);
+
+ expect(result.success).toBe(false);
+ expect(result.error).toBe('消息包含敏感词');
+ expect(mockZulipClientPool.sendMessage).not.toHaveBeenCalled();
+ });
+
+ it('应该在Zulip发送失败时仍返回成功(本地模式)', async () => {
+ const chatRequest: ChatMessageRequest = {
+ socketId: 'socket-123',
+ content: 'Hello, world!',
+ scope: 'local',
+ };
+
+ const mockSession = createMockSession({ socketId: 'socket-123' });
+
+ mockSessionManager.getSession.mockResolvedValue(mockSession);
+ mockSessionManager.injectContext.mockResolvedValue({
+ stream: 'Tavern',
+ topic: 'General',
+ });
+
+ mockMessageFilter.validateMessage.mockResolvedValue({
+ allowed: true,
+ filteredContent: 'Hello, world!',
+ });
+
+ mockZulipClientPool.sendMessage.mockResolvedValue({
+ success: false,
+ error: 'Zulip服务不可用',
+ });
+
+ const result = await service.sendChatMessage(chatRequest);
+
+ expect(result.success).toBe(true); // 本地模式下仍返回成功
+ });
+ });
+
+ describe('updatePlayerPosition - 更新玩家位置', () => {
+ it('应该成功更新玩家位置', async () => {
+ const positionRequest: PositionUpdateRequest = {
+ socketId: 'socket-123',
+ x: 500,
+ y: 400,
+ mapId: 'tavern',
+ };
+
+ mockSessionManager.updatePlayerPosition.mockResolvedValue(true);
+
+ const result = await service.updatePlayerPosition(positionRequest);
+
+ expect(result).toBe(true);
+ expect(mockSessionManager.updatePlayerPosition).toHaveBeenCalledWith(
+ 'socket-123',
+ 'tavern',
+ 500,
+ 400
+ );
+ });
+
+ it('应该拒绝空socketId的位置更新', async () => {
+ const positionRequest: PositionUpdateRequest = {
+ socketId: '',
+ x: 500,
+ y: 400,
+ mapId: 'tavern',
+ };
+
+ const result = await service.updatePlayerPosition(positionRequest);
+
+ expect(result).toBe(false);
+ expect(mockSessionManager.updatePlayerPosition).not.toHaveBeenCalled();
+ });
+
+ it('应该拒绝空mapId的位置更新', async () => {
+ const positionRequest: PositionUpdateRequest = {
+ socketId: 'socket-123',
+ x: 500,
+ y: 400,
+ mapId: '',
+ };
+
+ const result = await service.updatePlayerPosition(positionRequest);
+
+ expect(result).toBe(false);
+ expect(mockSessionManager.updatePlayerPosition).not.toHaveBeenCalled();
+ });
+ });
+ /**
+ * 属性测试: 玩家登录流程完整性
+ *
+ * **Feature: zulip-integration, Property 1: 玩家登录流程完整性**
+ * **Validates: Requirements 1.1, 1.2, 1.3, 1.4, 1.5**
+ *
+ * 对于任何有效的游戏Token,系统应该能够验证Token,创建Zulip客户端,
+ * 建立会话映射,并返回成功的登录响应
+ */
+ describe('Property 1: 玩家登录流程完整性', () => {
+ /**
+ * 属性: 对于任何有效的Token和socketId,登录应该成功并创建会话
+ * 验证需求 1.1: 游戏客户端连接时系统应验证游戏Token的有效性
+ * 验证需求 1.2: Token验证成功后系统应获取用户的Zulip API Key
+ * 验证需求 1.3: 获取API Key后系统应创建用户专用的Zulip客户端实例
+ */
+ it('对于任何有效的Token和socketId,登录应该成功并创建会话', async () => {
+ await fc.assert(
+ fc.asyncProperty(
+ // 生成有效的Token(不以'invalid'开头)
+ fc.string({ minLength: 8, maxLength: 50 })
+ .filter(s => !s.startsWith('invalid') && s.trim().length > 0),
+ // 生成有效的socketId
+ fc.string({ minLength: 5, maxLength: 30 })
+ .filter(s => s.trim().length > 0),
+ async (token, socketId) => {
+ const trimmedToken = token.trim();
+ const trimmedSocketId = socketId.trim();
+
+ const loginRequest: PlayerLoginRequest = {
+ token: trimmedToken,
+ socketId: trimmedSocketId,
+ };
+
+ const expectedUserId = `user_${trimmedToken.substring(0, 8)}`;
+ const expectedUsername = `Player_${expectedUserId.substring(5, 10)}`;
+
+ const mockSession = createMockSession({
+ socketId: trimmedSocketId,
+ userId: expectedUserId,
+ username: expectedUsername,
+ });
+
+ mockConfigManager.getZulipConfig.mockReturnValue({
+ zulipServerUrl: 'https://zulip.example.com',
+ });
+
+ mockSessionManager.createSession.mockResolvedValue(mockSession);
+
+ const result = await service.handlePlayerLogin(loginRequest);
+
+ // 验证登录成功
+ expect(result.success).toBe(true);
+ expect(result.userId).toBe(expectedUserId);
+ expect(result.username).toBe(expectedUsername);
+ expect(result.currentMap).toBe('whale_port');
+ expect(result.sessionId).toBeDefined();
+
+ // 验证会话创建被调用
+ expect(mockSessionManager.createSession).toHaveBeenCalledWith(
+ trimmedSocketId,
+ expectedUserId,
+ expect.any(String), // zulipQueueId
+ expectedUsername,
+ 'whale_port',
+ { x: 400, y: 300 }
+ );
+ }
+ ),
+ { numRuns: 100 }
+ );
+ }, 60000);
+
+ /**
+ * 属性: 对于任何无效的Token,登录应该失败
+ * 验证需求 1.1: 游戏客户端连接时系统应验证游戏Token的有效性
+ */
+ it('对于任何无效的Token,登录应该失败', async () => {
+ await fc.assert(
+ fc.asyncProperty(
+ // 生成无效的Token(以'invalid'开头)
+ fc.string({ minLength: 1, maxLength: 30 })
+ .map(s => `invalid${s}`),
+ // 生成有效的socketId
+ fc.string({ minLength: 5, maxLength: 30 })
+ .filter(s => s.trim().length > 0),
+ async (invalidToken, socketId) => {
+ const loginRequest: PlayerLoginRequest = {
+ token: invalidToken,
+ socketId: socketId.trim(),
+ };
+
+ const result = await service.handlePlayerLogin(loginRequest);
+
+ // 验证登录失败
+ expect(result.success).toBe(false);
+ expect(result.error).toBe('Token验证失败');
+ expect(result.userId).toBeUndefined();
+ expect(result.sessionId).toBeUndefined();
+
+ // 验证没有创建会话
+ expect(mockSessionManager.createSession).not.toHaveBeenCalled();
+ }
+ ),
+ { numRuns: 50 }
+ );
+ }, 30000);
+
+ /**
+ * 属性: 对于空或无效的参数,登录应该返回相应的错误信息
+ * 验证需求 1.1: 系统应正确处理无效的登录请求
+ */
+ it('对于空或无效的参数,登录应该返回相应的错误信息', async () => {
+ await fc.assert(
+ fc.asyncProperty(
+ // 生成可能为空或以'invalid'开头的Token
+ fc.oneof(
+ fc.constant(''), // 空字符串
+ fc.constant(' '), // 只有空格
+ fc.string({ minLength: 1, maxLength: 50 }).map(s => 'invalid' + s), // 以invalid开头
+ ),
+ // 生成可能为空的socketId
+ fc.option(fc.string({ minLength: 1, maxLength: 30 }), { nil: '' }),
+ async (token, socketId) => {
+ // 重置mock调用历史
+ jest.clearAllMocks();
+
+ const loginRequest: PlayerLoginRequest = {
+ token: token || '',
+ socketId: socketId || '',
+ };
+
+ const result = await service.handlePlayerLogin(loginRequest);
+
+ // 验证登录失败
+ expect(result.success).toBe(false);
+ expect(result.error).toBeDefined();
+
+ if (!token || token.trim().length === 0) {
+ expect(result.error).toBe('Token不能为空');
+ } else if (!socketId || socketId.trim().length === 0) {
+ expect(result.error).toBe('socketId不能为空');
+ } else if (token.startsWith('invalid')) {
+ expect(result.error).toBe('Token验证失败');
+ }
+
+ // 验证没有创建会话
+ expect(mockSessionManager.createSession).not.toHaveBeenCalled();
+ }
+ ),
+ { numRuns: 50 }
+ );
+ }, 30000);
+
+ /**
+ * 属性: 对于有Zulip API Key的用户,应该尝试创建Zulip客户端
+ * 验证需求 1.2: Token验证成功后系统应获取用户的Zulip API Key
+ * 验证需求 1.3: 获取API Key后系统应创建用户专用的Zulip客户端实例
+ */
+ it('对于有Zulip API Key的用户,应该尝试创建Zulip客户端', async () => {
+ await fc.assert(
+ fc.asyncProperty(
+ // 生成包含特定标识的Token(表示有API Key)
+ fc.constantFrom(
+ 'real_user_token_with_zulip_key_123',
+ 'token_with_lCPWCPf_key',
+ 'token_with_W2KhXaQx_key'
+ ),
+ // 生成有效的socketId
+ fc.string({ minLength: 5, maxLength: 30 })
+ .filter(s => s.trim().length > 0),
+ async (tokenWithApiKey, socketId) => {
+ const loginRequest: PlayerLoginRequest = {
+ token: tokenWithApiKey,
+ socketId: socketId.trim(),
+ };
+
+ const mockClientInstance = createMockClientInstance({
+ userId: `user_${tokenWithApiKey.substring(0, 8)}`,
+ queueId: 'test-queue-123',
+ });
+
+ const mockSession = createMockSession({
+ socketId: socketId.trim(),
+ zulipQueueId: 'test-queue-123',
+ });
+
+ mockConfigManager.getZulipConfig.mockReturnValue({
+ zulipServerUrl: 'https://zulip.example.com',
+ });
+
+ mockZulipClientPool.createUserClient.mockResolvedValue(mockClientInstance);
+ mockSessionManager.createSession.mockResolvedValue(mockSession);
+
+ const result = await service.handlePlayerLogin(loginRequest);
+
+ // 验证登录成功
+ expect(result.success).toBe(true);
+
+ // 验证尝试创建了Zulip客户端
+ expect(mockZulipClientPool.createUserClient).toHaveBeenCalledWith(
+ expect.any(String),
+ expect.objectContaining({
+ username: expect.any(String),
+ apiKey: 'lCPWCPfGh7WUHxwN56GF8oYXOpqNfGF8',
+ realm: 'https://zulip.example.com',
+ })
+ );
+ }
+ ),
+ { numRuns: 30 }
+ );
+ }, 30000);
+ });
+ /**
+ * 属性测试: 消息发送流程完整性
+ *
+ * **Feature: zulip-integration, Property 3: 消息发送流程完整性**
+ * **Validates: Requirements 3.1, 3.2, 3.3, 3.4, 3.5**
+ *
+ * 对于任何有效的聊天消息请求,系统应该进行内容过滤、权限验证、
+ * 上下文注入,并成功发送到对应的Zulip Stream/Topic
+ */
+ describe('Property 3: 消息发送流程完整性', () => {
+ /**
+ * 属性: 对于任何有效会话的消息发送请求,应该成功处理并发送
+ * 验证需求 3.1: 游戏客户端发送聊天消息时系统应获取玩家当前位置
+ * 验证需求 3.2: 获取位置后系统应根据位置确定目标Stream和Topic
+ * 验证需求 3.3: 确定目标后系统应进行消息内容过滤和频率检查
+ */
+ it('对于任何有效会话的消息发送请求,应该成功处理并发送', async () => {
+ await fc.assert(
+ fc.asyncProperty(
+ // 生成有效的socketId
+ fc.string({ minLength: 5, maxLength: 30 })
+ .filter(s => s.trim().length > 0),
+ // 生成有效的消息内容
+ fc.string({ minLength: 1, maxLength: 200 })
+ .filter(s => s.trim().length > 0 && !/[敏感词|违禁词]/.test(s)),
+ // 生成地图和Stream映射
+ fc.record({
+ mapId: fc.constantFrom('tavern', 'novice_village', 'market'),
+ streamName: fc.constantFrom('Tavern', 'Novice Village', 'Market'),
+ }),
+ async (socketId, content, mapping) => {
+ const chatRequest: ChatMessageRequest = {
+ socketId: socketId.trim(),
+ content: content.trim(),
+ scope: 'local',
+ };
+
+ const mockSession = createMockSession({
+ socketId: socketId.trim(),
+ userId: `user_${socketId.substring(0, 8)}`,
+ currentMap: mapping.mapId,
+ });
+
+ mockSessionManager.getSession.mockResolvedValue(mockSession);
+ mockSessionManager.injectContext.mockResolvedValue({
+ stream: mapping.streamName,
+ topic: 'General',
+ });
+
+ mockMessageFilter.validateMessage.mockResolvedValue({
+ allowed: true,
+ filteredContent: content.trim(),
+ });
+
+ mockZulipClientPool.sendMessage.mockResolvedValue({
+ success: true,
+ messageId: Math.floor(Math.random() * 1000000),
+ });
+
+ const result = await service.sendChatMessage(chatRequest);
+
+ // 验证消息发送成功
+ expect(result.success).toBe(true);
+ expect(result.messageId).toBeDefined();
+
+ // 验证调用了正确的方法
+ expect(mockSessionManager.getSession).toHaveBeenCalledWith(socketId.trim());
+ expect(mockSessionManager.injectContext).toHaveBeenCalledWith(socketId.trim());
+ expect(mockMessageFilter.validateMessage).toHaveBeenCalledWith(
+ mockSession.userId,
+ content.trim(),
+ mapping.streamName,
+ mapping.mapId
+ );
+ expect(mockZulipClientPool.sendMessage).toHaveBeenCalledWith(
+ mockSession.userId,
+ mapping.streamName,
+ 'General',
+ content.trim()
+ );
+ }
+ ),
+ { numRuns: 100 }
+ );
+ }, 60000);
+
+ /**
+ * 属性: 对于任何不存在的会话,消息发送应该失败
+ * 验证需求 3.1: 系统应验证会话的有效性
+ */
+ it('对于任何不存在的会话,消息发送应该失败', async () => {
+ await fc.assert(
+ fc.asyncProperty(
+ // 生成不存在的socketId
+ fc.string({ minLength: 5, maxLength: 30 })
+ .filter(s => s.trim().length > 0)
+ .map(s => `nonexistent_${s}`),
+ // 生成任意消息内容
+ fc.string({ minLength: 1, maxLength: 200 })
+ .filter(s => s.trim().length > 0),
+ async (nonExistentSocketId, content) => {
+ const chatRequest: ChatMessageRequest = {
+ socketId: nonExistentSocketId,
+ content: content.trim(),
+ scope: 'local',
+ };
+
+ mockSessionManager.getSession.mockResolvedValue(null);
+
+ const result = await service.sendChatMessage(chatRequest);
+
+ // 验证消息发送失败
+ expect(result.success).toBe(false);
+ expect(result.error).toBe('会话不存在,请重新登录');
+
+ // 验证没有进行后续处理
+ expect(mockMessageFilter.validateMessage).not.toHaveBeenCalled();
+ expect(mockZulipClientPool.sendMessage).not.toHaveBeenCalled();
+ }
+ ),
+ { numRuns: 50 }
+ );
+ }, 30000);
+
+ /**
+ * 属性: 对于任何未通过验证的消息,发送应该失败
+ * 验证需求 3.3: 确定目标后系统应进行消息内容过滤和频率检查
+ */
+ it('对于任何未通过验证的消息,发送应该失败', async () => {
+ await fc.assert(
+ fc.asyncProperty(
+ // 生成有效的socketId
+ fc.string({ minLength: 5, maxLength: 30 })
+ .filter(s => s.trim().length > 0),
+ // 生成可能包含敏感词的消息内容
+ fc.string({ minLength: 1, maxLength: 200 })
+ .filter(s => s.trim().length > 0),
+ // 生成验证失败的原因
+ fc.constantFrom(
+ '消息包含敏感词',
+ '发送频率过快',
+ '权限不足',
+ '消息长度超限'
+ ),
+ async (socketId, content, failureReason) => {
+ const chatRequest: ChatMessageRequest = {
+ socketId: socketId.trim(),
+ content: content.trim(),
+ scope: 'local',
+ };
+
+ const mockSession = createMockSession({
+ socketId: socketId.trim(),
+ });
+
+ mockSessionManager.getSession.mockResolvedValue(mockSession);
+ mockSessionManager.injectContext.mockResolvedValue({
+ stream: 'Tavern',
+ topic: 'General',
+ });
+
+ mockMessageFilter.validateMessage.mockResolvedValue({
+ allowed: false,
+ reason: failureReason,
+ });
+
+ const result = await service.sendChatMessage(chatRequest);
+
+ // 验证消息发送失败
+ expect(result.success).toBe(false);
+ expect(result.error).toBe(failureReason);
+
+ // 验证没有发送到Zulip
+ expect(mockZulipClientPool.sendMessage).not.toHaveBeenCalled();
+ }
+ ),
+ { numRuns: 50 }
+ );
+ }, 30000);
+
+ /**
+ * 属性: 即使Zulip发送失败,系统也应该返回成功(本地模式)
+ * 验证需求 3.5: 发送消息到Zulip时系统应处理发送失败的情况
+ */
+ it('即使Zulip发送失败,系统也应该返回成功(本地模式)', async () => {
+ await fc.assert(
+ fc.asyncProperty(
+ // 生成有效的socketId
+ fc.string({ minLength: 5, maxLength: 30 })
+ .filter(s => s.trim().length > 0),
+ // 生成有效的消息内容
+ fc.string({ minLength: 1, maxLength: 200 })
+ .filter(s => s.trim().length > 0),
+ // 生成Zulip错误信息
+ fc.constantFrom(
+ 'Zulip服务不可用',
+ '网络连接超时',
+ 'API Key无效',
+ 'Stream不存在'
+ ),
+ async (socketId, content, zulipError) => {
+ const chatRequest: ChatMessageRequest = {
+ socketId: socketId.trim(),
+ content: content.trim(),
+ scope: 'local',
+ };
+
+ const mockSession = createMockSession({
+ socketId: socketId.trim(),
+ });
+
+ mockSessionManager.getSession.mockResolvedValue(mockSession);
+ mockSessionManager.injectContext.mockResolvedValue({
+ stream: 'Tavern',
+ topic: 'General',
+ });
+
+ mockMessageFilter.validateMessage.mockResolvedValue({
+ allowed: true,
+ filteredContent: content.trim(),
+ });
+
+ mockZulipClientPool.sendMessage.mockResolvedValue({
+ success: false,
+ error: zulipError,
+ });
+
+ const result = await service.sendChatMessage(chatRequest);
+
+ // 验证本地模式下仍返回成功
+ expect(result.success).toBe(true);
+ expect(result.messageId).toBeUndefined();
+ }
+ ),
+ { numRuns: 50 }
+ );
+ }, 30000);
+ });
+ /**
+ * 属性测试: 位置更新和上下文注入
+ *
+ * **Feature: zulip-integration, Property 6: 位置更新和上下文注入**
+ * **Validates: Requirements 4.1, 4.2, 4.3, 4.4**
+ *
+ * 对于任何位置更新请求,系统应该正确更新玩家位置信息,
+ * 并在消息发送时根据位置进行上下文注入
+ */
+ describe('Property 6: 位置更新和上下文注入', () => {
+ /**
+ * 属性: 对于任何有效的位置更新请求,应该成功更新位置
+ * 验证需求 4.1: 玩家移动时系统应更新玩家在游戏世界中的位置信息
+ * 验证需求 4.2: 更新位置时系统应验证位置的有效性
+ */
+ it('对于任何有效的位置更新请求,应该成功更新位置', async () => {
+ await fc.assert(
+ fc.asyncProperty(
+ // 生成有效的socketId
+ fc.string({ minLength: 5, maxLength: 30 })
+ .filter(s => s.trim().length > 0),
+ // 生成有效的坐标
+ fc.record({
+ x: fc.integer({ min: 0, max: 2000 }),
+ y: fc.integer({ min: 0, max: 2000 }),
+ }),
+ // 生成有效的地图ID
+ fc.constantFrom('tavern', 'novice_village', 'market', 'whale_port'),
+ async (socketId, position, mapId) => {
+ const positionRequest: PositionUpdateRequest = {
+ socketId: socketId.trim(),
+ x: position.x,
+ y: position.y,
+ mapId,
+ };
+
+ mockSessionManager.updatePlayerPosition.mockResolvedValue(true);
+
+ const result = await service.updatePlayerPosition(positionRequest);
+
+ // 验证位置更新成功
+ expect(result).toBe(true);
+
+ // 验证调用了正确的方法
+ expect(mockSessionManager.updatePlayerPosition).toHaveBeenCalledWith(
+ socketId.trim(),
+ mapId,
+ position.x,
+ position.y
+ );
+ }
+ ),
+ { numRuns: 100 }
+ );
+ }, 60000);
+
+ /**
+ * 属性: 对于任何无效的参数,位置更新应该失败
+ * 验证需求 4.2: 更新位置时系统应验证位置的有效性
+ */
+ it('对于任何无效的参数,位置更新应该失败', async () => {
+ await fc.assert(
+ fc.asyncProperty(
+ // 生成可能为空的socketId
+ fc.option(fc.string({ minLength: 1, maxLength: 30 }), { nil: '' }),
+ // 生成可能为空的mapId
+ fc.option(fc.constantFrom('tavern', 'market'), { nil: '' }),
+ // 生成坐标
+ fc.record({
+ x: fc.integer({ min: 0, max: 2000 }),
+ y: fc.integer({ min: 0, max: 2000 }),
+ }),
+ async (socketId, mapId, position) => {
+ // 重置mock调用历史
+ jest.clearAllMocks();
+
+ const positionRequest: PositionUpdateRequest = {
+ socketId: socketId || '',
+ x: position.x,
+ y: position.y,
+ mapId: mapId || '',
+ };
+
+ const result = await service.updatePlayerPosition(positionRequest);
+
+ if (!socketId || socketId.trim().length === 0 ||
+ !mapId || mapId.trim().length === 0) {
+ // 验证位置更新失败
+ expect(result).toBe(false);
+
+ // 验证没有调用SessionManager
+ expect(mockSessionManager.updatePlayerPosition).not.toHaveBeenCalled();
+ }
+ }
+ ),
+ { numRuns: 50 }
+ );
+ }, 30000);
+
+ /**
+ * 属性: 位置更新失败时应该正确处理错误
+ * 验证需求 4.1: 系统应正确处理位置更新过程中的错误
+ */
+ it('位置更新失败时应该正确处理错误', async () => {
+ await fc.assert(
+ fc.asyncProperty(
+ // 生成有效的socketId
+ fc.string({ minLength: 5, maxLength: 30 })
+ .filter(s => s.trim().length > 0),
+ // 生成有效的坐标
+ fc.record({
+ x: fc.integer({ min: 0, max: 2000 }),
+ y: fc.integer({ min: 0, max: 2000 }),
+ }),
+ // 生成有效的地图ID
+ fc.constantFrom('tavern', 'novice_village', 'market'),
+ async (socketId, position, mapId) => {
+ const positionRequest: PositionUpdateRequest = {
+ socketId: socketId.trim(),
+ x: position.x,
+ y: position.y,
+ mapId,
+ };
+
+ // 模拟SessionManager抛出错误
+ mockSessionManager.updatePlayerPosition.mockRejectedValue(
+ new Error('位置更新失败')
+ );
+
+ const result = await service.updatePlayerPosition(positionRequest);
+
+ // 验证位置更新失败
+ expect(result).toBe(false);
+ }
+ ),
+ { numRuns: 50 }
+ );
+ }, 30000);
+ });
+
+ describe('processZulipMessage - 处理Zulip消息', () => {
+ it('应该正确处理Zulip消息并确定目标玩家', async () => {
+ const zulipMessage = {
+ id: 12345,
+ sender_full_name: 'Alice',
+ sender_email: 'alice@example.com',
+ content: 'Hello everyone!',
+ display_recipient: 'Tavern',
+ stream_name: 'Tavern',
+ };
+
+ mockConfigManager.getMapIdByStream.mockReturnValue('tavern');
+ mockSessionManager.getSocketsInMap.mockResolvedValue(['socket-1', 'socket-2']);
+
+ const result = await service.processZulipMessage(zulipMessage);
+
+ expect(result.targetSockets).toEqual(['socket-1', 'socket-2']);
+ expect(result.message.t).toBe('chat_render');
+ expect(result.message.from).toBe('Alice');
+ expect(result.message.txt).toBe('Hello everyone!');
+ expect(result.message.bubble).toBe(true);
+ });
+
+ it('应该在未知Stream时返回空的目标列表', async () => {
+ const zulipMessage = {
+ id: 12345,
+ sender_full_name: 'Alice',
+ content: 'Hello!',
+ display_recipient: 'UnknownStream',
+ };
+
+ mockConfigManager.getMapIdByStream.mockReturnValue(null);
+
+ const result = await service.processZulipMessage(zulipMessage);
+
+ expect(result.targetSockets).toEqual([]);
+ });
+ });
+
+ describe('辅助方法', () => {
+ it('getSession - 应该返回会话信息', async () => {
+ const socketId = 'socket-123';
+ const mockSession = createMockSession({ socketId });
+
+ mockSessionManager.getSession.mockResolvedValue(mockSession);
+
+ const result = await service.getSession(socketId);
+
+ expect(result).toBe(mockSession);
+ expect(mockSessionManager.getSession).toHaveBeenCalledWith(socketId);
+ });
+
+ it('getSocketsInMap - 应该返回地图中的Socket列表', async () => {
+ const mapId = 'tavern';
+ const socketIds = ['socket-1', 'socket-2', 'socket-3'];
+
+ mockSessionManager.getSocketsInMap.mockResolvedValue(socketIds);
+
+ const result = await service.getSocketsInMap(mapId);
+
+ expect(result).toBe(socketIds);
+ expect(mockSessionManager.getSocketsInMap).toHaveBeenCalledWith(mapId);
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/business/zulip/zulip.service.ts b/src/business/zulip/zulip.service.ts
index 0390857..1386948 100644
--- a/src/business/zulip/zulip.service.ts
+++ b/src/business/zulip/zulip.service.ts
@@ -22,14 +22,16 @@
* @since 2025-12-25
*/
-import { Injectable, Logger } from '@nestjs/common';
+import { Injectable, Logger, Inject } from '@nestjs/common';
import { randomUUID } from 'crypto';
-import { ZulipClientPoolService } from './services/zulip-client-pool.service';
-import { SessionManagerService } from './services/session-manager.service';
-import { MessageFilterService } from './services/message-filter.service';
-import { ZulipEventProcessorService } from './services/zulip-event-processor.service';
-import { ConfigManagerService } from './services/config-manager.service';
-import { ErrorHandlerService } from './services/error-handler.service';
+import { SessionManagerService } from './services/session_manager.service';
+import { MessageFilterService } from './services/message_filter.service';
+import { ZulipEventProcessorService } from './services/zulip_event_processor.service';
+import {
+ IZulipClientPoolService,
+ IZulipConfigService,
+} from '../../core/zulip/interfaces/zulip-core.interfaces';
+import { ApiKeySecurityService } from '../../core/zulip/services/api_key_security.service';
/**
* 玩家登录请求接口
@@ -79,18 +81,41 @@ export interface ChatMessageResponse {
error?: string;
}
+/**
+ * Zulip集成主服务类
+ *
+ * 职责:
+ * - 作为Zulip集成系统的主要协调服务
+ * - 整合各个子服务,提供统一的业务接口
+ * - 处理游戏客户端与Zulip之间的核心业务逻辑
+ * - 管理玩家会话和消息路由
+ *
+ * 主要方法:
+ * - handlePlayerLogin(): 处理玩家登录和Zulip客户端初始化
+ * - handlePlayerLogout(): 处理玩家登出和资源清理
+ * - sendChatMessage(): 处理游戏聊天消息发送到Zulip
+ * - updatePlayerPosition(): 更新玩家位置信息
+ *
+ * 使用场景:
+ * - WebSocket网关调用处理消息路由
+ * - 会话管理和状态维护
+ * - 消息格式转换和过滤
+ * - 游戏与Zulip的双向通信桥梁
+ */
@Injectable()
export class ZulipService {
private readonly logger = new Logger(ZulipService.name);
private readonly DEFAULT_MAP = 'whale_port';
constructor(
- private readonly zulipClientPool: ZulipClientPoolService,
+ @Inject('ZULIP_CLIENT_POOL_SERVICE')
+ private readonly zulipClientPool: IZulipClientPoolService,
private readonly sessionManager: SessionManagerService,
private readonly messageFilter: MessageFilterService,
private readonly eventProcessor: ZulipEventProcessorService,
- private readonly configManager: ConfigManagerService,
- private readonly errorHandler: ErrorHandlerService,
+ @Inject('ZULIP_CONFIG_SERVICE')
+ private readonly configManager: IZulipConfigService,
+ private readonly apiKeySecurityService: ApiKeySecurityService,
) {
this.logger.log('ZulipService初始化完成');
}
@@ -295,36 +320,38 @@ export class ZulipService {
// 从Token中提取用户ID(模拟)
const userId = `user_${token.substring(0, 8)}`;
- // 为测试用户提供真实的 Zulip API Key
+ // 从ApiKeySecurityService获取真实的Zulip API Key
let zulipApiKey = undefined;
let zulipEmail = undefined;
- // 检查是否是配置了真实 Zulip API Key 的测试用户
- const hasTestApiKey = token.includes('lCPWCPf');
- const hasUserApiKey = token.includes('W2KhXaQx');
- const hasOldApiKey = token.includes('MZ1jEMQo');
- const isRealUserToken = token === 'real_user_token_with_zulip_key_123';
-
- this.logger.log('Token检查', {
- operation: 'validateGameToken',
- userId,
- tokenPrefix: token.substring(0, 20),
- hasUserApiKey,
- hasOldApiKey,
- isRealUserToken,
- });
-
- if (isRealUserToken || hasUserApiKey || hasTestApiKey || hasOldApiKey) {
- // 使用用户的真实 API Key
- // 注意:这个API Key对应的Zulip用户邮箱是 user8@zulip.xinghangee.icu
- zulipApiKey = 'lCPWCPfGh7WUHxwN56GF8oYXOpqNfGF8';
- zulipEmail = 'angjustinl@mail.angforever.top';
+ try {
+ // 尝试从Redis获取存储的API Key
+ const apiKeyResult = await this.apiKeySecurityService.getApiKey(userId);
- this.logger.log('配置真实Zulip API Key', {
+ if (apiKeyResult.success && apiKeyResult.apiKey) {
+ zulipApiKey = apiKeyResult.apiKey;
+ // TODO: 从数据库获取用户的Zulip邮箱
+ // 暂时使用模拟数据
+ zulipEmail = 'angjustinl@163.com';
+
+ this.logger.log('从存储获取到Zulip API Key', {
+ operation: 'validateGameToken',
+ userId,
+ hasApiKey: true,
+ zulipEmail,
+ });
+ } else {
+ this.logger.debug('用户没有存储的Zulip API Key', {
+ operation: 'validateGameToken',
+ userId,
+ });
+ }
+ } catch (error) {
+ const err = error as Error;
+ this.logger.warn('获取Zulip API Key失败', {
operation: 'validateGameToken',
userId,
- zulipEmail,
- hasApiKey: true,
+ error: err.message,
});
}
@@ -332,7 +359,6 @@ export class ZulipService {
userId,
username: `Player_${userId.substring(5, 10)}`,
email: `${userId}@example.com`,
- // 实际项目中从数据库获取
zulipEmail,
zulipApiKey,
};
diff --git a/src/business/zulip/zulip-integration.e2e.spec.ts b/src/business/zulip/zulip_integration.e2e.spec.ts
similarity index 99%
rename from src/business/zulip/zulip-integration.e2e.spec.ts
rename to src/business/zulip/zulip_integration.e2e.spec.ts
index 084b348..b71da8f 100644
--- a/src/business/zulip/zulip-integration.e2e.spec.ts
+++ b/src/business/zulip/zulip_integration.e2e.spec.ts
@@ -56,7 +56,7 @@ describeE2E('Zulip Integration E2E Tests', () => {
});
client.on('connect', () => resolve(client));
- client.on('connect_error', (err) => reject(err));
+ client.on('connect_error', (err: any) => reject(err));
setTimeout(() => reject(new Error('Connection timeout')), 5000);
});
diff --git a/src/business/zulip/zulip-websocket.gateway.spec.ts b/src/business/zulip/zulip_websocket.gateway.spec.ts
similarity index 99%
rename from src/business/zulip/zulip-websocket.gateway.spec.ts
rename to src/business/zulip/zulip_websocket.gateway.spec.ts
index 4f6a6e5..c1fc883 100644
--- a/src/business/zulip/zulip-websocket.gateway.spec.ts
+++ b/src/business/zulip/zulip_websocket.gateway.spec.ts
@@ -16,9 +16,9 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Logger } from '@nestjs/common';
import * as fc from 'fast-check';
-import { ZulipWebSocketGateway } from './zulip-websocket.gateway';
+import { ZulipWebSocketGateway } from './zulip_websocket.gateway';
import { ZulipService, LoginResponse, ChatMessageResponse } from './zulip.service';
-import { SessionManagerService, GameSession } from './services/session-manager.service';
+import { SessionManagerService, GameSession } from './services/session_manager.service';
import { Server, Socket } from 'socket.io';
describe('ZulipWebSocketGateway', () => {
diff --git a/src/business/zulip/zulip-websocket.gateway.ts b/src/business/zulip/zulip_websocket.gateway.ts
similarity index 95%
rename from src/business/zulip/zulip-websocket.gateway.ts
rename to src/business/zulip/zulip_websocket.gateway.ts
index d90dba1..50e0598 100644
--- a/src/business/zulip/zulip-websocket.gateway.ts
+++ b/src/business/zulip/zulip_websocket.gateway.ts
@@ -35,7 +35,7 @@ import {
import { Server, Socket } from 'socket.io';
import { Injectable, Logger } from '@nestjs/common';
import { ZulipService } from './zulip.service';
-import { SessionManagerService } from './services/session-manager.service';
+import { SessionManagerService } from './services/session_manager.service';
/**
* 登录消息接口 - 按guide.md格式
@@ -96,6 +96,29 @@ interface ClientData {
connectedAt: Date;
}
+/**
+ * Zulip WebSocket网关类
+ *
+ * 职责:
+ * - 处理所有Godot游戏客户端的WebSocket连接
+ * - 实现游戏协议到Zulip协议的转换
+ * - 提供统一的消息路由和权限控制
+ * - 管理客户端连接状态和会话
+ *
+ * 主要方法:
+ * - handleConnection(): 处理客户端连接建立
+ * - handleDisconnect(): 处理客户端连接断开
+ * - handleLogin(): 处理登录消息
+ * - handleChat(): 处理聊天消息
+ * - handlePositionUpdate(): 处理位置更新
+ * - sendChatRender(): 向客户端发送聊天渲染消息
+ *
+ * 使用场景:
+ * - 游戏客户端WebSocket通信的统一入口
+ * - 消息协议转换和路由分发
+ * - 连接状态管理和权限验证
+ * - 实时消息推送和广播
+ */
@Injectable()
@WebSocketGateway({
cors: { origin: '*' },
diff --git a/src/core/db/users/users.entity.ts b/src/core/db/users/users.entity.ts
index 1a785cc..9a4bdb2 100644
--- a/src/core/db/users/users.entity.ts
+++ b/src/core/db/users/users.entity.ts
@@ -19,8 +19,9 @@
* @since 2025-12-17
*/
-import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, OneToOne } from 'typeorm';
import { UserStatus } from '../../../business/user-mgmt/enums/user-status.enum';
+import { ZulipAccounts } from '../zulip_accounts/zulip_accounts.entity';
/**
* 用户实体类
@@ -432,4 +433,25 @@ export class Users {
comment: '更新时间'
})
updated_at: Date;
+
+ /**
+ * 关联的Zulip账号
+ *
+ * 关系设计:
+ * - 类型:一对一关系(OneToOne)
+ * - 外键:在ZulipAccounts表中
+ * - 级联:不设置级联删除,保证数据安全
+ *
+ * 业务规则:
+ * - 每个游戏用户最多关联一个Zulip账号
+ * - 支持延迟加载,提高查询性能
+ * - 可选关联,不是所有用户都有Zulip账号
+ *
+ * 使用场景:
+ * - 游戏内聊天功能集成
+ * - 跨平台消息同步
+ * - 用户身份验证和权限管理
+ */
+ @OneToOne(() => ZulipAccounts, zulipAccount => zulipAccount.gameUser)
+ zulipAccount?: ZulipAccounts;
}
\ No newline at end of file
diff --git a/src/core/db/zulip_accounts/zulip_accounts.entity.ts b/src/core/db/zulip_accounts/zulip_accounts.entity.ts
new file mode 100644
index 0000000..10034bf
--- /dev/null
+++ b/src/core/db/zulip_accounts/zulip_accounts.entity.ts
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/src/core/db/zulip_accounts/zulip_accounts.module.ts b/src/core/db/zulip_accounts/zulip_accounts.module.ts
new file mode 100644
index 0000000..6c288ef
--- /dev/null
+++ b/src/core/db/zulip_accounts/zulip_accounts.module.ts
@@ -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();
+ }
+}
diff --git a/src/core/db/zulip_accounts/zulip_accounts.repository.ts b/src/core/db/zulip_accounts/zulip_accounts.repository.ts
new file mode 100644
index 0000000..9991d03
--- /dev/null
+++ b/src/core/db/zulip_accounts/zulip_accounts.repository.ts
@@ -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,
+ ) {}
+
+ /**
+ * 创建新的Zulip账号关联
+ *
+ * @param createDto 创建数据
+ * @returns Promise 创建的关联记录
+ */
+ async create(createDto: CreateZulipAccountDto): Promise {
+ const zulipAccount = this.repository.create(createDto);
+ return await this.repository.save(zulipAccount);
+ }
+
+ /**
+ * 根据游戏用户ID查找Zulip账号关联
+ *
+ * @param gameUserId 游戏用户ID
+ * @param includeGameUser 是否包含游戏用户信息
+ * @returns Promise 关联记录或null
+ */
+ async findByGameUserId(gameUserId: bigint, includeGameUser: boolean = false): Promise {
+ const relations = includeGameUser ? ['gameUser'] : [];
+
+ return await this.repository.findOne({
+ where: { gameUserId },
+ relations,
+ });
+ }
+
+ /**
+ * 根据Zulip用户ID查找账号关联
+ *
+ * @param zulipUserId Zulip用户ID
+ * @param includeGameUser 是否包含游戏用户信息
+ * @returns Promise 关联记录或null
+ */
+ async findByZulipUserId(zulipUserId: number, includeGameUser: boolean = false): Promise {
+ const relations = includeGameUser ? ['gameUser'] : [];
+
+ return await this.repository.findOne({
+ where: { zulipUserId },
+ relations,
+ });
+ }
+
+ /**
+ * 根据Zulip邮箱查找账号关联
+ *
+ * @param zulipEmail Zulip邮箱
+ * @param includeGameUser 是否包含游戏用户信息
+ * @returns Promise 关联记录或null
+ */
+ async findByZulipEmail(zulipEmail: string, includeGameUser: boolean = false): Promise {
+ const relations = includeGameUser ? ['gameUser'] : [];
+
+ return await this.repository.findOne({
+ where: { zulipEmail },
+ relations,
+ });
+ }
+
+ /**
+ * 根据ID查找Zulip账号关联
+ *
+ * @param id 关联记录ID
+ * @param includeGameUser 是否包含游戏用户信息
+ * @returns Promise 关联记录或null
+ */
+ async findById(id: bigint, includeGameUser: boolean = false): Promise {
+ const relations = includeGameUser ? ['gameUser'] : [];
+
+ return await this.repository.findOne({
+ where: { id },
+ relations,
+ });
+ }
+
+ /**
+ * 更新Zulip账号关联
+ *
+ * @param id 关联记录ID
+ * @param updateDto 更新数据
+ * @returns Promise 更新后的记录或null
+ */
+ async update(id: bigint, updateDto: UpdateZulipAccountDto): Promise {
+ await this.repository.update({ id }, updateDto);
+ return await this.findById(id);
+ }
+
+ /**
+ * 根据游戏用户ID更新Zulip账号关联
+ *
+ * @param gameUserId 游戏用户ID
+ * @param updateDto 更新数据
+ * @returns Promise 更新后的记录或null
+ */
+ async updateByGameUserId(gameUserId: bigint, updateDto: UpdateZulipAccountDto): Promise {
+ await this.repository.update({ gameUserId }, updateDto);
+ return await this.findByGameUserId(gameUserId);
+ }
+
+ /**
+ * 删除Zulip账号关联
+ *
+ * @param id 关联记录ID
+ * @returns Promise 是否删除成功
+ */
+ async delete(id: bigint): Promise {
+ const result = await this.repository.delete({ id });
+ return result.affected > 0;
+ }
+
+ /**
+ * 根据游戏用户ID删除Zulip账号关联
+ *
+ * @param gameUserId 游戏用户ID
+ * @returns Promise 是否删除成功
+ */
+ async deleteByGameUserId(gameUserId: bigint): Promise {
+ const result = await this.repository.delete({ gameUserId });
+ return result.affected > 0;
+ }
+
+ /**
+ * 查询多个Zulip账号关联
+ *
+ * @param options 查询选项
+ * @returns Promise 关联记录列表
+ */
+ async findMany(options: ZulipAccountQueryOptions = {}): Promise {
+ const { includeGameUser, ...whereOptions } = options;
+ const relations = includeGameUser ? ['gameUser'] : [];
+
+ // 构建查询条件
+ const where: FindOptionsWhere = {};
+ 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 需要验证的账号列表
+ */
+ async findAccountsNeedingVerification(maxAge: number = 24 * 60 * 60 * 1000): Promise {
+ 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 错误状态的账号列表
+ */
+ async findErrorAccounts(maxRetryCount: number = 3): Promise {
+ return await this.repository.find({
+ where: { status: 'error' },
+ order: { updatedAt: 'ASC' },
+ });
+ }
+
+ /**
+ * 批量更新账号状态
+ *
+ * @param ids 账号ID列表
+ * @param status 新状态
+ * @returns Promise 更新的记录数
+ */
+ async batchUpdateStatus(ids: bigint[], status: 'active' | 'inactive' | 'suspended' | 'error'): Promise {
+ const result = await this.repository
+ .createQueryBuilder()
+ .update(ZulipAccounts)
+ .set({ status })
+ .whereInIds(ids)
+ .execute();
+
+ return result.affected || 0;
+ }
+
+ /**
+ * 统计各状态的账号数量
+ *
+ * @returns Promise> 状态统计
+ */
+ async getStatusStatistics(): Promise> {
+ const result = await this.repository
+ .createQueryBuilder('zulip_accounts')
+ .select('zulip_accounts.status', 'status')
+ .addSelect('COUNT(*)', 'count')
+ .groupBy('zulip_accounts.status')
+ .getRawMany();
+
+ const statistics: Record = {};
+ result.forEach(row => {
+ statistics[row.status] = parseInt(row.count, 10);
+ });
+
+ return statistics;
+ }
+
+ /**
+ * 检查邮箱是否已存在
+ *
+ * @param zulipEmail Zulip邮箱
+ * @param excludeId 排除的记录ID(用于更新时检查)
+ * @returns Promise 是否已存在
+ */
+ async existsByEmail(zulipEmail: string, excludeId?: bigint): Promise {
+ 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 是否已存在
+ */
+ async existsByZulipUserId(zulipUserId: number, excludeId?: bigint): Promise {
+ 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;
+ }
+}
\ No newline at end of file
diff --git a/src/core/db/zulip_accounts/zulip_accounts_memory.repository.ts b/src/core/db/zulip_accounts/zulip_accounts_memory.repository.ts
new file mode 100644
index 0000000..e31e6cf
--- /dev/null
+++ b/src/core/db/zulip_accounts/zulip_accounts_memory.repository.ts
@@ -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 = new Map();
+ private currentId: bigint = BigInt(1);
+
+ /**
+ * 创建新的Zulip账号关联
+ *
+ * @param createDto 创建数据
+ * @returns Promise 创建的关联记录
+ */
+ async create(createDto: CreateZulipAccountDto): Promise {
+ 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 关联记录或null
+ */
+ async findByGameUserId(gameUserId: bigint, includeGameUser: boolean = false): Promise {
+ 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 关联记录或null
+ */
+ async findByZulipUserId(zulipUserId: number, includeGameUser: boolean = false): Promise {
+ for (const account of this.accounts.values()) {
+ if (account.zulipUserId === zulipUserId) {
+ return account;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 根据Zulip邮箱查找账号关联
+ *
+ * @param zulipEmail Zulip邮箱
+ * @param includeGameUser 是否包含游戏用户信息(内存模式忽略)
+ * @returns Promise 关联记录或null
+ */
+ async findByZulipEmail(zulipEmail: string, includeGameUser: boolean = false): Promise {
+ for (const account of this.accounts.values()) {
+ if (account.zulipEmail === zulipEmail) {
+ return account;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 根据ID查找Zulip账号关联
+ *
+ * @param id 关联记录ID
+ * @param includeGameUser 是否包含游戏用户信息(内存模式忽略)
+ * @returns Promise 关联记录或null
+ */
+ async findById(id: bigint, includeGameUser: boolean = false): Promise {
+ return this.accounts.get(id) || null;
+ }
+
+ /**
+ * 更新Zulip账号关联
+ *
+ * @param id 关联记录ID
+ * @param updateDto 更新数据
+ * @returns Promise 更新后的记录或null
+ */
+ async update(id: bigint, updateDto: UpdateZulipAccountDto): Promise {
+ 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 更新后的记录或null
+ */
+ async updateByGameUserId(gameUserId: bigint, updateDto: UpdateZulipAccountDto): Promise {
+ 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 是否删除成功
+ */
+ async delete(id: bigint): Promise {
+ return this.accounts.delete(id);
+ }
+
+ /**
+ * 根据游戏用户ID删除Zulip账号关联
+ *
+ * @param gameUserId 游戏用户ID
+ * @returns Promise 是否删除成功
+ */
+ async deleteByGameUserId(gameUserId: bigint): Promise {
+ for (const [id, account] of this.accounts.entries()) {
+ if (account.gameUserId === gameUserId) {
+ return this.accounts.delete(id);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 查询多个Zulip账号关联
+ *
+ * @param options 查询选项
+ * @returns Promise 关联记录列表
+ */
+ async findMany(options: ZulipAccountQueryOptions = {}): Promise {
+ 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 需要验证的账号列表
+ */
+ async findAccountsNeedingVerification(maxAge: number = 24 * 60 * 60 * 1000): Promise {
+ 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 错误状态的账号列表
+ */
+ async findErrorAccounts(maxRetryCount: number = 3): Promise {
+ 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 更新的记录数
+ */
+ async batchUpdateStatus(ids: bigint[], status: 'active' | 'inactive' | 'suspended' | 'error'): Promise {
+ 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> 状态统计
+ */
+ async getStatusStatistics(): Promise> {
+ const statistics: Record = {};
+
+ 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 是否已存在
+ */
+ async existsByEmail(zulipEmail: string, excludeId?: bigint): Promise {
+ 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 是否已存在
+ */
+ async existsByZulipUserId(zulipUserId: number, excludeId?: bigint): Promise {
+ for (const [id, account] of this.accounts.entries()) {
+ if (account.zulipUserId === zulipUserId && (!excludeId || id !== excludeId)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/core/login_core/login_core.service.ts b/src/core/login_core/login_core.service.ts
index a53f79f..38aac56 100644
--- a/src/core/login_core/login_core.service.ts
+++ b/src/core/login_core/login_core.service.ts
@@ -826,4 +826,36 @@ export class LoginCoreService {
VerificationCodeType.EMAIL_VERIFICATION
);
}
+
+ /**
+ * 删除用户
+ *
+ * 功能描述:
+ * 删除指定的用户记录,用于注册失败时的回滚操作
+ *
+ * 业务逻辑:
+ * 1. 验证用户是否存在
+ * 2. 执行用户删除操作
+ * 3. 返回删除结果
+ *
+ * @param userId 用户ID
+ * @returns Promise 是否删除成功
+ * @throws NotFoundException 用户不存在时
+ */
+ async deleteUser(userId: bigint): Promise {
+ // 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;
+ }
+ }
}
\ No newline at end of file
diff --git a/src/business/security/decorators/throttle.decorator.ts b/src/core/security_core/decorators/throttle.decorator.ts
similarity index 100%
rename from src/business/security/decorators/throttle.decorator.ts
rename to src/core/security_core/decorators/throttle.decorator.ts
diff --git a/src/business/security/decorators/timeout.decorator.ts b/src/core/security_core/decorators/timeout.decorator.ts
similarity index 100%
rename from src/business/security/decorators/timeout.decorator.ts
rename to src/core/security_core/decorators/timeout.decorator.ts
diff --git a/src/business/security/guards/throttle.guard.ts b/src/core/security_core/guards/throttle.guard.ts
similarity index 100%
rename from src/business/security/guards/throttle.guard.ts
rename to src/core/security_core/guards/throttle.guard.ts
diff --git a/src/business/security/index.ts b/src/core/security_core/index.ts
similarity index 71%
rename from src/business/security/index.ts
rename to src/core/security_core/index.ts
index 1453eb8..7781f83 100644
--- a/src/business/security/index.ts
+++ b/src/core/security_core/index.ts
@@ -1,5 +1,5 @@
/**
- * 安全功能模块导出
+ * 核心安全模块导出
*
* 功能概述:
* - 频率限制和防护机制
@@ -10,14 +10,14 @@
*/
// 模块
-export * from './security.module';
+export * from './security_core.module';
// 守卫
export * from './guards/throttle.guard';
// 中间件
export * from './middleware/maintenance.middleware';
-export * from './middleware/content-type.middleware';
+export * from './middleware/content_type.middleware';
// 拦截器
export * from './interceptors/timeout.interceptor';
diff --git a/src/business/security/interceptors/timeout.interceptor.ts b/src/core/security_core/interceptors/timeout.interceptor.ts
similarity index 100%
rename from src/business/security/interceptors/timeout.interceptor.ts
rename to src/core/security_core/interceptors/timeout.interceptor.ts
diff --git a/src/business/security/middleware/content-type.middleware.ts b/src/core/security_core/middleware/content_type.middleware.ts
similarity index 100%
rename from src/business/security/middleware/content-type.middleware.ts
rename to src/core/security_core/middleware/content_type.middleware.ts
diff --git a/src/business/security/middleware/maintenance.middleware.ts b/src/core/security_core/middleware/maintenance.middleware.ts
similarity index 100%
rename from src/business/security/middleware/maintenance.middleware.ts
rename to src/core/security_core/middleware/maintenance.middleware.ts
diff --git a/src/business/security/security.module.ts b/src/core/security_core/security_core.module.ts
similarity index 84%
rename from src/business/security/security.module.ts
rename to src/core/security_core/security_core.module.ts
index 80cdd83..4ea6f7a 100644
--- a/src/business/security/security.module.ts
+++ b/src/core/security_core/security_core.module.ts
@@ -1,11 +1,11 @@
/**
- * 安全功能模块
+ * 核心安全模块
*
* 功能描述:
- * - 整合所有安全相关功能
+ * - 提供系统级安全防护功能
* - 频率限制和请求超时控制
* - 维护模式和内容类型验证
- * - 系统安全防护机制
+ * - 全局安全中间件和守卫
*
* @author kiro-ai
* @version 1.0.0
@@ -34,4 +34,4 @@ import { TimeoutInterceptor } from './interceptors/timeout.interceptor';
],
exports: [ThrottleGuard, TimeoutInterceptor],
})
-export class SecurityModule {}
\ No newline at end of file
+export class SecurityCoreModule {}
\ No newline at end of file
diff --git a/src/business/zulip/config/index.ts b/src/core/zulip/config/index.ts
similarity index 100%
rename from src/business/zulip/config/index.ts
rename to src/core/zulip/config/index.ts
diff --git a/src/business/zulip/config/zulip.config.ts b/src/core/zulip/config/zulip.config.ts
similarity index 100%
rename from src/business/zulip/config/zulip.config.ts
rename to src/core/zulip/config/zulip.config.ts
diff --git a/src/core/zulip/index.ts b/src/core/zulip/index.ts
new file mode 100644
index 0000000..5583b5d
--- /dev/null
+++ b/src/core/zulip/index.ts
@@ -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';
\ No newline at end of file
diff --git a/src/core/zulip/interfaces/zulip-core.interfaces.ts b/src/core/zulip/interfaces/zulip-core.interfaces.ts
new file mode 100644
index 0000000..db8d38a
--- /dev/null
+++ b/src/core/zulip/interfaces/zulip-core.interfaces.ts
@@ -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;
+
+ /**
+ * 验证API Key有效性
+ */
+ validateApiKey(clientInstance: ZulipClientInstance): Promise;
+
+ /**
+ * 发送消息到指定Stream/Topic
+ */
+ sendMessage(
+ clientInstance: ZulipClientInstance,
+ stream: string,
+ topic: string,
+ content: string,
+ ): Promise;
+
+ /**
+ * 注册事件队列
+ */
+ registerQueue(
+ clientInstance: ZulipClientInstance,
+ eventTypes?: string[],
+ ): Promise;
+
+ /**
+ * 注销事件队列
+ */
+ deregisterQueue(clientInstance: ZulipClientInstance): Promise;
+
+ /**
+ * 获取事件队列中的事件
+ */
+ getEvents(
+ clientInstance: ZulipClientInstance,
+ dontBlock?: boolean,
+ ): Promise;
+
+ /**
+ * 销毁客户端实例
+ */
+ destroyClient(clientInstance: ZulipClientInstance): Promise;
+}
+
+/**
+ * Zulip客户端池服务接口
+ *
+ * 职责:
+ * - 管理用户专用的Zulip客户端实例
+ * - 维护客户端连接池和生命周期
+ * - 处理客户端的创建、销毁和状态管理
+ */
+export interface IZulipClientPoolService {
+ /**
+ * 为用户创建专用Zulip客户端
+ */
+ createUserClient(userId: string, config: ZulipClientConfig): Promise;
+
+ /**
+ * 获取用户的Zulip客户端
+ */
+ getUserClient(userId: string): Promise;
+
+ /**
+ * 检查用户客户端是否存在
+ */
+ hasUserClient(userId: string): boolean;
+
+ /**
+ * 发送消息到指定Stream/Topic
+ */
+ sendMessage(
+ userId: string,
+ stream: string,
+ topic: string,
+ content: string,
+ ): Promise;
+
+ /**
+ * 注册事件队列
+ */
+ registerEventQueue(userId: string): Promise;
+
+ /**
+ * 注销事件队列
+ */
+ deregisterEventQueue(userId: string): Promise;
+
+ /**
+ * 销毁用户客户端
+ */
+ destroyUserClient(userId: string): Promise;
+
+ /**
+ * 获取客户端池统计信息
+ */
+ getPoolStats(): PoolStats;
+
+ /**
+ * 清理过期客户端
+ */
+ cleanupIdleClients(maxIdleMinutes?: number): Promise;
+}
+
+/**
+ * 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;
+
+ /**
+ * 验证配置有效性
+ */
+ validateConfig(): Promise<{ valid: boolean; errors: string[] }>;
+}
+
+/**
+ * Zulip事件处理服务接口
+ *
+ * 职责:
+ * - 处理从Zulip接收的事件队列消息
+ * - 将Zulip消息转换为游戏协议格式
+ * - 管理事件队列的生命周期
+ */
+export interface IZulipEventProcessorService {
+ /**
+ * 启动事件处理循环
+ */
+ startEventProcessing(): Promise;
+
+ /**
+ * 停止事件处理循环
+ */
+ stopEventProcessing(): Promise;
+
+ /**
+ * 注册事件队列
+ */
+ registerEventQueue(queueId: string, userId: string, lastEventId?: number): Promise;
+
+ /**
+ * 注销事件队列
+ */
+ unregisterEventQueue(queueId: string): Promise;
+
+ /**
+ * 处理Zulip消息事件
+ */
+ processMessageEvent(event: any, senderUserId: string): Promise;
+
+ /**
+ * 设置消息分发器
+ */
+ setMessageDistributor(distributor: any): void;
+
+ /**
+ * 获取事件处理统计信息
+ */
+ getProcessingStats(): any;
+}
\ No newline at end of file
diff --git a/src/business/zulip/interfaces/zulip.interfaces.ts b/src/core/zulip/interfaces/zulip.interfaces.ts
similarity index 100%
rename from src/business/zulip/interfaces/zulip.interfaces.ts
rename to src/core/zulip/interfaces/zulip.interfaces.ts
diff --git a/src/business/zulip/services/api-key-security.service.spec.ts b/src/core/zulip/services/api_key_security.service.spec.ts
similarity index 99%
rename from src/business/zulip/services/api-key-security.service.spec.ts
rename to src/core/zulip/services/api_key_security.service.spec.ts
index 995e283..a5698b2 100644
--- a/src/business/zulip/services/api-key-security.service.spec.ts
+++ b/src/core/zulip/services/api_key_security.service.spec.ts
@@ -17,7 +17,7 @@ import {
ApiKeySecurityService,
SecurityEventType,
SecuritySeverity,
-} from './api-key-security.service';
+} from './api_key_security.service';
import { IRedisService } from '../../../core/redis/redis.interface';
describe('ApiKeySecurityService', () => {
diff --git a/src/business/zulip/services/api-key-security.service.ts b/src/core/zulip/services/api_key_security.service.ts
similarity index 97%
rename from src/business/zulip/services/api-key-security.service.ts
rename to src/core/zulip/services/api_key_security.service.ts
index bdcfd2c..a27ae3a 100644
--- a/src/business/zulip/services/api-key-security.service.ts
+++ b/src/core/zulip/services/api_key_security.service.ts
@@ -100,6 +100,28 @@ export interface GetApiKeyResult {
message?: string;
}
+/**
+ * API密钥安全服务类
+ *
+ * 职责:
+ * - 管理Zulip API密钥的安全存储
+ * - 提供API密钥的加密和解密功能
+ * - 记录API密钥的访问日志
+ * - 监控API密钥的使用情况和安全事件
+ *
+ * 主要方法:
+ * - storeApiKey(): 安全存储加密的API密钥
+ * - retrieveApiKey(): 检索并解密API密钥
+ * - validateApiKey(): 验证API密钥的有效性
+ * - logSecurityEvent(): 记录安全相关事件
+ * - getAccessStats(): 获取API密钥访问统计
+ *
+ * 使用场景:
+ * - 用户API密钥的安全存储
+ * - API密钥访问时的解密操作
+ * - 安全事件的监控和记录
+ * - API密钥使用情况的统计分析
+ */
@Injectable()
export class ApiKeySecurityService {
private readonly logger = new Logger(ApiKeySecurityService.name);
diff --git a/src/business/zulip/services/config-manager.service.spec.ts b/src/core/zulip/services/config_manager.service.spec.ts
similarity index 99%
rename from src/business/zulip/services/config-manager.service.spec.ts
rename to src/core/zulip/services/config_manager.service.spec.ts
index 49b7f94..39a91bb 100644
--- a/src/business/zulip/services/config-manager.service.spec.ts
+++ b/src/core/zulip/services/config_manager.service.spec.ts
@@ -12,8 +12,8 @@
import { Test, TestingModule } from '@nestjs/testing';
import * as fc from 'fast-check';
-import { ConfigManagerService, MapConfig, ZulipConfig } from './config-manager.service';
-import { AppLoggerService } from '../../../core/utils/logger/logger.service';
+import { ConfigManagerService, MapConfig, ZulipConfig } from './config_manager.service';
+import { AppLoggerService } from '../../utils/logger/logger.service';
import * as fs from 'fs';
import * as path from 'path';
diff --git a/src/business/zulip/services/config-manager.service.ts b/src/core/zulip/services/config_manager.service.ts
similarity index 95%
rename from src/business/zulip/services/config-manager.service.ts
rename to src/core/zulip/services/config_manager.service.ts
index a569824..5ee40d0 100644
--- a/src/business/zulip/services/config-manager.service.ts
+++ b/src/core/zulip/services/config_manager.service.ts
@@ -108,6 +108,28 @@ export interface InteractionObject extends InteractionObjectConfig {
mapId: string; // 所属地图ID
}
+/**
+ * 配置管理服务类
+ *
+ * 职责:
+ * - 管理地图到Zulip Stream的映射配置
+ * - 提供Zulip服务器连接配置
+ * - 支持配置文件的热重载
+ * - 验证配置的完整性和有效性
+ *
+ * 主要方法:
+ * - loadMapConfig(): 加载地图配置文件
+ * - getStreamByMap(): 根据地图ID获取对应的Stream
+ * - getZulipConfig(): 获取Zulip服务器配置
+ * - validateConfig(): 验证配置文件格式
+ * - enableConfigWatcher(): 启用配置文件监控
+ *
+ * 使用场景:
+ * - 系统启动时加载配置
+ * - 消息路由时查找Stream映射
+ * - 配置文件变更时自动重载
+ * - 配置验证和错误处理
+ */
@Injectable()
export class ConfigManagerService implements OnModuleDestroy {
private mapConfigs: Map = new Map();
@@ -117,10 +139,39 @@ export class ConfigManagerService implements OnModuleDestroy {
private configLoadTime: Date;
private configWatcher: fs.FSWatcher | null = null;
private isWatcherEnabled: boolean = false;
- private readonly CONFIG_DIR = path.join(process.cwd(), 'config', 'zulip');
+ private readonly CONFIG_DIR = this.getConfigDir();
private readonly MAP_CONFIG_FILE = 'map-config.json';
private readonly logger = new Logger(ConfigManagerService.name);
+ /**
+ * 获取配置目录路径
+ *
+ * 在开发环境中使用 config/zulip
+ * 在生产环境中使用 dist/zulip (编译后的位置)
+ */
+ private getConfigDir(): string {
+ const isDevelopment = process.env.NODE_ENV !== 'production';
+
+ if (isDevelopment) {
+ // 开发环境:使用源码目录
+ return path.join(process.cwd(), 'config', 'zulip');
+ } else {
+ // 生产环境:使用编译后的目录
+ const distConfigPath = path.join(process.cwd(), 'dist', 'zulip');
+ const rootConfigPath = path.join(process.cwd(), 'config', 'zulip');
+
+ // 优先使用 dist/zulip,如果不存在则回退到 config/zulip
+ if (fs.existsSync(distConfigPath)) {
+ return distConfigPath;
+ } else if (fs.existsSync(rootConfigPath)) {
+ return rootConfigPath;
+ } else {
+ // 都不存在,使用默认路径
+ return distConfigPath;
+ }
+ }
+ }
+
constructor() {
this.logger.log('ConfigManagerService初始化完成');
@@ -216,6 +267,9 @@ export class ConfigManagerService implements OnModuleDestroy {
* 4. 存储到内存映射
*
* @returns Promise
+ *
+ * @throws Error 当配置格式无效时
+ * @throws Error 当文件读取失败时
*/
async loadMapConfig(): Promise {
this.logger.log('开始加载地图配置', {
diff --git a/src/business/zulip/services/error-handler.service.spec.ts b/src/core/zulip/services/error_handler.service.spec.ts
similarity index 99%
rename from src/business/zulip/services/error-handler.service.spec.ts
rename to src/core/zulip/services/error_handler.service.spec.ts
index 42c426c..0baf916 100644
--- a/src/business/zulip/services/error-handler.service.spec.ts
+++ b/src/core/zulip/services/error_handler.service.spec.ts
@@ -23,7 +23,7 @@ import {
LoadStatus,
ErrorHandlingResult,
RetryConfig,
-} from './error-handler.service';
+} from './error_handler.service';
import { AppLoggerService } from '../../../core/utils/logger/logger.service';
describe('ErrorHandlerService', () => {
diff --git a/src/business/zulip/services/error-handler.service.ts b/src/core/zulip/services/error_handler.service.ts
similarity index 97%
rename from src/business/zulip/services/error-handler.service.ts
rename to src/core/zulip/services/error_handler.service.ts
index 0041fe3..f1f12c2 100644
--- a/src/business/zulip/services/error-handler.service.ts
+++ b/src/core/zulip/services/error_handler.service.ts
@@ -115,6 +115,28 @@ export enum LoadStatus {
CRITICAL = 'critical',
}
+/**
+ * 错误处理服务类
+ *
+ * 职责:
+ * - 统一处理系统错误和异常
+ * - 实现重试机制和服务降级
+ * - 监控系统健康状态和负载
+ * - 提供错误恢复和告警功能
+ *
+ * 主要方法:
+ * - handleError(): 处理各类错误和异常
+ * - retryWithBackoff(): 带退避策略的重试机制
+ * - enableDegradedMode(): 启用服务降级模式
+ * - getServiceStatus(): 获取服务状态
+ * - recordError(): 记录错误统计
+ *
+ * 使用场景:
+ * - Zulip API调用失败时的错误处理
+ * - 网络连接异常的重试机制
+ * - 系统负载过高时的服务降级
+ * - 错误监控和告警通知
+ */
@Injectable()
export class ErrorHandlerService extends EventEmitter implements OnModuleDestroy {
private readonly logger = new Logger(ErrorHandlerService.name);
diff --git a/src/business/zulip/services/monitoring.service.spec.ts b/src/core/zulip/services/monitoring.service.spec.ts
similarity index 100%
rename from src/business/zulip/services/monitoring.service.spec.ts
rename to src/core/zulip/services/monitoring.service.spec.ts
diff --git a/src/business/zulip/services/monitoring.service.ts b/src/core/zulip/services/monitoring.service.ts
similarity index 96%
rename from src/business/zulip/services/monitoring.service.ts
rename to src/core/zulip/services/monitoring.service.ts
index 34ef9d3..8a6f65a 100644
--- a/src/business/zulip/services/monitoring.service.ts
+++ b/src/core/zulip/services/monitoring.service.ts
@@ -182,6 +182,29 @@ export interface MonitoringStats {
};
}
+/**
+ * 监控服务类
+ *
+ * 职责:
+ * - 监控Zulip集成系统的运行状态
+ * - 收集和统计系统性能指标
+ * - 提供健康检查和告警功能
+ * - 生成系统监控报告
+ *
+ * 主要方法:
+ * - recordConnection(): 记录连接统计
+ * - recordApiCall(): 记录API调用统计
+ * - recordMessage(): 记录消息统计
+ * - triggerAlert(): 触发告警
+ * - getSystemStats(): 获取系统统计信息
+ * - performHealthCheck(): 执行健康检查
+ *
+ * 使用场景:
+ * - 系统性能监控和统计
+ * - 异常情况的告警通知
+ * - 系统健康状态检查
+ * - 运维数据的收集和分析
+ */
@Injectable()
export class MonitoringService extends EventEmitter implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(MonitoringService.name);
diff --git a/src/business/zulip/services/stream-initializer.service.ts b/src/core/zulip/services/stream_initializer.service.ts
similarity index 92%
rename from src/business/zulip/services/stream-initializer.service.ts
rename to src/core/zulip/services/stream_initializer.service.ts
index cbf2339..06b644b 100644
--- a/src/business/zulip/services/stream-initializer.service.ts
+++ b/src/core/zulip/services/stream_initializer.service.ts
@@ -21,8 +21,30 @@
*/
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
-import { ConfigManagerService } from './config-manager.service';
+import { ConfigManagerService } from './config_manager.service';
+/**
+ * Stream初始化服务类
+ *
+ * 职责:
+ * - 系统启动时自动检查并创建Zulip Streams
+ * - 确保所有地图对应的Stream都存在
+ * - 验证Stream配置的完整性
+ * - 提供Stream初始化状态监控
+ *
+ * 主要方法:
+ * - onModuleInit(): 模块初始化时自动执行
+ * - initializeStreams(): 初始化所有必需的Streams
+ * - createStreamIfNotExists(): 检查并创建单个Stream
+ * - validateStreamConfig(): 验证Stream配置
+ * - getInitializationStatus(): 获取初始化状态
+ *
+ * 使用场景:
+ * - 系统启动时自动初始化Streams
+ * - 确保消息路由的目标Stream存在
+ * - 新增地图时自动创建对应Stream
+ * - 系统部署和配置验证
+ */
@Injectable()
export class StreamInitializerService implements OnModuleInit {
private readonly logger = new Logger(StreamInitializerService.name);
diff --git a/src/core/zulip/services/zulip_account.service.ts b/src/core/zulip/services/zulip_account.service.ts
new file mode 100644
index 0000000..162ea7c
--- /dev/null
+++ b/src/core/zulip/services/zulip_account.service.ts
@@ -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();
+
+ constructor() {
+ this.logger.log('ZulipAccountService初始化完成');
+ }
+
+ /**
+ * 初始化管理员客户端
+ *
+ * 功能描述:
+ * 使用管理员凭证初始化Zulip客户端,用于创建用户账号
+ *
+ * @param adminConfig 管理员配置
+ * @returns Promise 是否初始化成功
+ */
+ async initializeAdminClient(adminConfig: ZulipClientConfig): Promise {
+ 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 创建结果
+ */
+ async createZulipAccount(request: CreateZulipAccountRequest): Promise {
+ 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 生成结果
+ */
+ async generateApiKeyForUser(email: string, password: string): Promise {
+ 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 验证结果
+ */
+ async validateZulipAccount(email: string, apiKey?: string): Promise {
+ 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 是否关联成功
+ */
+ async linkGameAccount(
+ gameUserId: string,
+ zulipUserId: number,
+ zulipEmail: string,
+ zulipApiKey: string,
+ ): Promise {
+ 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 是否解除成功
+ */
+ async unlinkGameAccount(gameUserId: string): Promise {
+ 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 用户是否存在
+ * @private
+ */
+ private async checkUserExists(email: string): Promise {
+ 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 zulip-js初始化函数
+ * @private
+ */
+ private async loadZulipModule(): Promise {
+ 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}`);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/business/zulip/services/zulip-client.service.spec.ts b/src/core/zulip/services/zulip_client.service.spec.ts
similarity index 99%
rename from src/business/zulip/services/zulip-client.service.spec.ts
rename to src/core/zulip/services/zulip_client.service.spec.ts
index 4316892..f2d0ddf 100644
--- a/src/business/zulip/services/zulip-client.service.spec.ts
+++ b/src/core/zulip/services/zulip_client.service.spec.ts
@@ -12,7 +12,7 @@
import { Test, TestingModule } from '@nestjs/testing';
import * as fc from 'fast-check';
-import { ZulipClientService, ZulipClientConfig, ZulipClientInstance } from './zulip-client.service';
+import { ZulipClientService, ZulipClientConfig, ZulipClientInstance } from './zulip_client.service';
import { AppLoggerService } from '../../../core/utils/logger/logger.service';
describe('ZulipClientService', () => {
diff --git a/src/business/zulip/services/zulip-client.service.ts b/src/core/zulip/services/zulip_client.service.ts
similarity index 96%
rename from src/business/zulip/services/zulip-client.service.ts
rename to src/core/zulip/services/zulip_client.service.ts
index 07cc647..5355612 100644
--- a/src/business/zulip/services/zulip-client.service.ts
+++ b/src/core/zulip/services/zulip_client.service.ts
@@ -77,6 +77,28 @@ export interface GetEventsResult {
error?: string;
}
+/**
+ * Zulip客户端服务类
+ *
+ * 职责:
+ * - 封装Zulip REST API调用
+ * - 处理Zulip客户端的创建和配置
+ * - 管理事件队列的注册和轮询
+ * - 提供消息发送和接收功能
+ *
+ * 主要方法:
+ * - createClient(): 创建并初始化Zulip客户端
+ * - registerQueue(): 注册Zulip事件队列
+ * - sendMessage(): 发送消息到Zulip Stream
+ * - getEvents(): 获取Zulip事件
+ * - validateConfig(): 验证客户端配置
+ *
+ * 使用场景:
+ * - 为每个用户创建独立的Zulip客户端
+ * - 处理与Zulip服务器的所有通信
+ * - 消息的发送和事件的接收
+ * - API调用的错误处理和重试
+ */
@Injectable()
export class ZulipClientService {
private readonly logger = new Logger(ZulipClientService.name);
diff --git a/src/business/zulip/services/zulip-client-pool.service.spec.ts b/src/core/zulip/services/zulip_client_pool.service.spec.ts
similarity index 98%
rename from src/business/zulip/services/zulip-client-pool.service.spec.ts
rename to src/core/zulip/services/zulip_client_pool.service.spec.ts
index 2cdd754..4a5bb64 100644
--- a/src/business/zulip/services/zulip-client-pool.service.spec.ts
+++ b/src/core/zulip/services/zulip_client_pool.service.spec.ts
@@ -12,9 +12,9 @@
*/
import { Test, TestingModule } from '@nestjs/testing';
-import { ZulipClientPoolService, PoolStats } from './zulip-client-pool.service';
-import { ZulipClientService, ZulipClientConfig, ZulipClientInstance } from './zulip-client.service';
-import { AppLoggerService } from '../../../core/utils/logger/logger.service';
+import { ZulipClientPoolService, PoolStats } from './zulip_client_pool.service';
+import { ZulipClientService, ZulipClientConfig, ZulipClientInstance } from './zulip_client.service';
+import { AppLoggerService } from '../../utils/logger/logger.service';
describe('ZulipClientPoolService', () => {
let service: ZulipClientPoolService;
diff --git a/src/business/zulip/services/zulip-client-pool.service.ts b/src/core/zulip/services/zulip_client_pool.service.ts
similarity index 95%
rename from src/business/zulip/services/zulip-client-pool.service.ts
rename to src/core/zulip/services/zulip_client_pool.service.ts
index 5e43e14..743d539 100644
--- a/src/business/zulip/services/zulip-client-pool.service.ts
+++ b/src/core/zulip/services/zulip_client_pool.service.ts
@@ -35,7 +35,7 @@ import {
SendMessageResult,
RegisterQueueResult,
GetEventsResult,
-} from './zulip-client.service';
+} from './zulip_client.service';
/**
* 用户客户端信息接口
@@ -57,6 +57,28 @@ export interface PoolStats {
clientIds: string[];
}
+/**
+ * Zulip客户端池服务类
+ *
+ * 职责:
+ * - 管理用户专用的Zulip客户端实例
+ * - 维护客户端连接池和生命周期
+ * - 处理客户端的创建、销毁和状态管理
+ * - 提供客户端池统计和监控功能
+ *
+ * 主要方法:
+ * - createUserClient(): 为用户创建专用Zulip客户端
+ * - getUserClient(): 获取用户的Zulip客户端
+ * - destroyUserClient(): 销毁用户的Zulip客户端
+ * - getPoolStats(): 获取客户端池统计信息
+ * - startEventPolling(): 启动事件轮询
+ *
+ * 使用场景:
+ * - 玩家登录时创建专用客户端
+ * - 消息发送时获取客户端实例
+ * - 玩家登出时清理客户端资源
+ * - 系统监控和性能统计
+ */
@Injectable()
export class ZulipClientPoolService implements OnModuleDestroy {
private readonly clientPool = new Map();
diff --git a/src/business/zulip/types/zulip-js.d.ts b/src/core/zulip/types/zulip-js.d.ts
similarity index 100%
rename from src/business/zulip/types/zulip-js.d.ts
rename to src/core/zulip/types/zulip-js.d.ts
diff --git a/src/core/zulip/zulip-core.module.ts b/src/core/zulip/zulip-core.module.ts
new file mode 100644
index 0000000..134ee45
--- /dev/null
+++ b/src/core/zulip/zulip-core.module.ts
@@ -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 {}
\ No newline at end of file
diff --git a/test-comprehensive.ps1 b/test-comprehensive.ps1
deleted file mode 100644
index 41d36f6..0000000
--- a/test-comprehensive.ps1
+++ /dev/null
@@ -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
\ No newline at end of file
diff --git a/test/core/db/users.test.ts b/test/core/db/users.test.ts
deleted file mode 100644
index e69de29..0000000
diff --git a/tsconfig.json b/tsconfig.json
index 37fb249..d48d43a 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,15 +1,16 @@
{
"compilerOptions": {
"target": "ES2020",
- "module": "Node16",
+ "module": "commonjs",
"lib": ["ES2020"],
- "moduleResolution": "node16",
+ "moduleResolution": "node",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"strict": true,
+ "noImplicitAny": false,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
@@ -20,5 +21,5 @@
"typeRoots": ["./node_modules/@types"]
},
"include": ["src/**/*"],
- "exclude": ["node_modules", "dist"]
+ "exclude": ["node_modules", "dist", "client"]
}
diff --git a/webhook-handler.js.example b/webhook-handler.js.example
deleted file mode 100644
index 3a97c12..0000000
--- a/webhook-handler.js.example
+++ /dev/null
@@ -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);
- });
-});
\ No newline at end of file