From 5140bd1a5419a7d84aa7d6d6576dce3a17e1ad35 Mon Sep 17 00:00:00 2001 From: moyin <244344649@qq.com> Date: Wed, 31 Dec 2025 15:43:15 +0800 Subject: [PATCH 1/6] =?UTF-8?q?docs=EF=BC=9A=E4=BC=98=E5=8C=96=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E6=96=87=E6=A1=A3=E7=BB=93=E6=9E=84=E5=92=8C=E6=9E=B6?= =?UTF-8?q?=E6=9E=84=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 优化主README.md的文件结构总览,采用总分结构设计 - 大幅改进docs/ARCHITECTURE.md,详细说明业务功能模块化架构 - 新增docs/DOCUMENT_CLEANUP.md记录文档清理过程 - 更新docs/README.md添加新文档的导航链接 本次更新完善了项目文档体系,便于开发者快速理解项目架构 --- README.md | 63 +-- docs/ARCHITECTURE.md | 812 +++++++++++++++++++++++++++++++++------ docs/DOCUMENT_CLEANUP.md | 142 +++++++ docs/README.md | 2 +- 4 files changed, 870 insertions(+), 149 deletions(-) create mode 100644 docs/DOCUMENT_CLEANUP.md 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/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index b1b471e..514dca2 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,187 +1,747 @@ -# 🏗️ 项目架构设计 +# 🏗️ Whale Town 项目架构设计 -## 整体架构 +> 基于业务功能模块化的现代化后端架构,支持双模式运行,开发测试零依赖,生产部署高性能。 -Whale Town 采用分层架构设计,确保代码的可维护性和可扩展性。 +## 📋 目录 + +- [🎯 架构概述](#-架构概述) +- [📁 目录结构详解](#-目录结构详解) +- [🏗️ 分层架构设计](#️-分层架构设计) +- [🔄 双模式架构](#-双模式架构) +- [📦 模块依赖关系](#-模块依赖关系) +- [🚀 扩展指南](#-扩展指南) + +--- + +## 🎯 架构概述 + +Whale Town 采用**业务功能模块化架构**,将代码按业务功能而非技术组件组织,确保高内聚、低耦合的设计原则。 + +### 🌟 核心设计理念 + +- **业务驱动** - 按业务功能组织代码,而非技术分层 +- **双模式支持** - 开发测试零依赖,生产部署高性能 +- **清晰分层** - 业务层 → 核心层 → 数据层,职责明确 +- **模块化设计** - 每个模块独立完整,可单独测试和部署 +- **配置驱动** - 通过环境变量控制运行模式和行为 + +### 📊 整体架构图 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 🌐 API接口层 │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ 🔗 REST API │ │ 🔌 WebSocket │ │ 📄 Swagger UI │ │ +│ │ (HTTP接口) │ │ (实时通信) │ │ (API文档) │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + ⬇️ +┌─────────────────────────────────────────────────────────────────┐ +│ 🎯 业务功能模块层 │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ 🔐 用户认证 │ │ 👥 用户管理 │ │ 🛡️ 管理员 │ │ +│ │ (auth) │ │ (user-mgmt) │ │ (admin) │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ 🔒 安全防护 │ │ 💬 Zulip集成 │ │ 🔗 共享组件 │ │ +│ │ (security) │ │ (zulip) │ │ (shared) │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + ⬇️ +┌─────────────────────────────────────────────────────────────────┐ +│ ⚙️ 核心技术服务层 │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ 🔑 登录核心 │ │ 👑 管理员核心 │ │ 💬 Zulip核心 │ │ +│ │ (login_core) │ │ (admin_core) │ │ (zulip) │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ 🛠️ 工具服务 │ │ 📧 邮件服务 │ │ 📝 日志服务 │ │ +│ │ (utils) │ │ (email) │ │ (logger) │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + ⬇️ +┌─────────────────────────────────────────────────────────────────┐ +│ 🗄️ 数据存储层 │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ 🗃️ 数据库 │ │ 🔴 Redis缓存 │ │ 📁 文件存储 │ │ +│ │ (MySQL/内存) │ │ (Redis/文件) │ │ (logs/data) │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 📁 目录结构详解 + +### 🎯 业务功能模块 (`src/business/`) + +> **设计原则**: 按业务功能组织,每个模块包含完整的业务逻辑 + +``` +src/business/ +├── 📂 auth/ # 🔐 用户认证模块 +│ ├── 📄 auth.controller.ts # HTTP接口控制器 +│ ├── 📄 auth.service.ts # 业务逻辑服务 +│ ├── 📄 auth.module.ts # 模块定义 +│ ├── 📂 dto/ # 数据传输对象 +│ │ ├── 📄 login.dto.ts # 登录请求DTO +│ │ ├── 📄 register.dto.ts # 注册请求DTO +│ │ └── 📄 reset-password.dto.ts # 重置密码DTO +│ └── 📂 __tests__/ # 单元测试 +│ └── 📄 auth.service.spec.ts +│ +├── 📂 user-mgmt/ # 👥 用户管理模块 +│ ├── 📄 user-mgmt.controller.ts # 用户管理接口 +│ ├── 📄 user-mgmt.service.ts # 用户状态管理逻辑 +│ ├── 📄 user-mgmt.module.ts # 模块定义 +│ ├── 📂 dto/ # 数据传输对象 +│ │ ├── 📄 update-status.dto.ts # 状态更新DTO +│ │ └── 📄 batch-status.dto.ts # 批量操作DTO +│ └── 📂 enums/ # 枚举定义 +│ └── 📄 user-status.enum.ts # 用户状态枚举 +│ +├── 📂 admin/ # 🛡️ 管理员模块 +│ ├── 📄 admin.controller.ts # 管理员接口 +│ ├── 📄 admin.service.ts # 管理员业务逻辑 +│ ├── 📄 admin.module.ts # 模块定义 +│ ├── 📂 dto/ # 数据传输对象 +│ └── 📂 guards/ # 权限守卫 +│ └── 📄 admin.guard.ts # 管理员权限验证 +│ +├── 📂 security/ # 🔒 安全防护模块 +│ ├── 📄 security.module.ts # 安全模块定义 +│ ├── 📂 guards/ # 安全守卫 +│ │ ├── 📄 throttle.guard.ts # 频率限制守卫 +│ │ ├── 📄 maintenance.guard.ts # 维护模式守卫 +│ │ └── 📄 content-type.guard.ts # 内容类型守卫 +│ └── 📂 interceptors/ # 拦截器 +│ └── 📄 timeout.interceptor.ts # 超时拦截器 +│ +├── 📂 zulip/ # 💬 Zulip集成模块 +│ ├── 📄 zulip.service.ts # Zulip业务服务 +│ ├── 📄 zulip_websocket.gateway.ts # WebSocket网关 +│ ├── 📄 zulip.module.ts # 模块定义 +│ └── 📂 services/ # 子服务 +│ ├── 📄 message_filter.service.ts # 消息过滤 +│ └── 📄 session_cleanup.service.ts # 会话清理 +│ +└── 📂 shared/ # 🔗 共享业务组件 + ├── 📂 decorators/ # 装饰器 + ├── 📂 pipes/ # 管道 + ├── 📂 filters/ # 异常过滤器 + └── 📂 interfaces/ # 接口定义 +``` + +### ⚙️ 核心技术服务 (`src/core/`) + +> **设计原则**: 提供技术基础设施,支持业务模块运行 + +``` +src/core/ +├── 📂 db/ # 🗄️ 数据库层 +│ ├── 📄 db.module.ts # 数据库模块 +│ ├── 📂 users/ # 用户数据服务 +│ │ ├── 📄 users.service.ts # MySQL数据库实现 +│ │ ├── 📄 users-memory.service.ts # 内存数据库实现 +│ │ ├── 📄 users.interface.ts # 用户服务接口 +│ │ └── 📄 user.entity.ts # 用户实体定义 +│ └── 📂 entities/ # 数据库实体 +│ └── 📄 *.entity.ts # 各种实体定义 +│ +├── 📂 redis/ # 🔴 Redis缓存层 +│ ├── 📄 redis.module.ts # Redis模块 +│ ├── 📄 redis.service.ts # Redis真实实现 +│ ├── 📄 file-redis.service.ts # 文件存储实现 +│ └── 📄 redis.interface.ts # Redis服务接口 +│ +├── 📂 login_core/ # 🔑 登录核心服务 +│ ├── 📄 login-core.service.ts # 登录核心逻辑 +│ ├── 📄 login-core.module.ts # 模块定义 +│ └── 📄 login-core.interface.ts # 接口定义 +│ +├── 📂 admin_core/ # 👑 管理员核心服务 +│ ├── 📄 admin-core.service.ts # 管理员核心逻辑 +│ ├── 📄 admin-core.module.ts # 模块定义 +│ └── 📄 admin-core.interface.ts # 接口定义 +│ +├── 📂 zulip/ # 💬 Zulip核心服务 +│ ├── 📄 zulip-core.module.ts # Zulip核心模块 +│ ├── 📄 zulip-api.service.ts # Zulip API服务 +│ └── 📄 zulip-websocket.service.ts # WebSocket服务 +│ +└── 📂 utils/ # 🛠️ 工具服务 + ├── 📂 email/ # 📧 邮件服务 + │ ├── 📄 email.service.ts # 邮件发送服务 + │ ├── 📄 email.module.ts # 邮件模块 + │ └── 📄 email.interface.ts # 邮件接口 + ├── 📂 verification/ # 🔢 验证码服务 + │ ├── 📄 verification.service.ts # 验证码生成验证 + │ └── 📄 verification.module.ts # 验证码模块 + └── 📂 logger/ # 📝 日志服务 + ├── 📄 logger.service.ts # 日志记录服务 + └── 📄 logger.module.ts # 日志模块 +``` + +### 🎨 前端管理界面 (`client/`) + +> **设计原则**: 独立的前端项目,提供管理员后台功能 + +``` +client/ +├── 📂 src/ # 前端源码 +│ ├── 📂 components/ # 通用组件 +│ │ ├── 📄 Layout.tsx # 布局组件 +│ │ ├── 📄 UserTable.tsx # 用户表格组件 +│ │ └── 📄 LogViewer.tsx # 日志查看组件 +│ ├── 📂 pages/ # 页面组件 +│ │ ├── 📄 Login.tsx # 登录页面 +│ │ ├── 📄 Dashboard.tsx # 仪表板 +│ │ ├── 📄 UserManagement.tsx # 用户管理 +│ │ └── 📄 LogManagement.tsx # 日志管理 +│ ├── 📂 services/ # API服务 +│ │ ├── 📄 api.ts # API客户端 +│ │ ├── 📄 auth.ts # 认证服务 +│ │ └── 📄 users.ts # 用户服务 +│ ├── 📂 utils/ # 工具函数 +│ ├── 📂 types/ # TypeScript类型 +│ ├── 📄 App.tsx # 应用主组件 +│ └── 📄 main.tsx # 应用入口 +├── 📂 dist/ # 构建产物 +├── 📄 package.json # 前端依赖 +├── 📄 vite.config.ts # Vite配置 +└── 📄 tsconfig.json # TypeScript配置 +``` + +### 📚 文档中心 (`docs/`) + +> **设计原则**: 完整的项目文档,支持开发者快速上手 + +``` +docs/ +├── 📄 README.md # 📖 文档导航中心 +├── 📄 ARCHITECTURE.md # 🏗️ 架构设计文档 +├── 📄 API_STATUS_CODES.md # 📋 API状态码说明 +├── 📄 CONTRIBUTORS.md # 🤝 贡献者指南 +│ +├── 📂 api/ # 🔌 API接口文档 +│ ├── 📄 README.md # API文档使用指南 +│ ├── 📄 api-documentation.md # 完整API接口文档 +│ ├── 📄 openapi.yaml # OpenAPI规范文件 +│ └── 📄 postman-collection.json # Postman测试集合 +│ +├── 📂 development/ # 💻 开发指南 +│ ├── 📄 backend_development_guide.md # 后端开发规范 +│ ├── 📄 git_commit_guide.md # Git提交规范 +│ ├── 📄 AI辅助开发规范指南.md # AI辅助开发指南 +│ ├── 📄 TESTING.md # 测试指南 +│ └── 📄 naming_convention.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配置 +├── 📄 docker-compose.yml # 🐳 Docker编排配置 +├── 📄 Dockerfile # 🐳 Docker镜像配置 +└── 📄 ecosystem.config.js # 🚀 PM2进程管理配置 +``` + +--- + +## 🏗️ 分层架构设计 + +### 📊 架构分层说明 ``` ┌─────────────────────────────────────────────────────────────┐ -│ 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. 📱 用户请求 → AuthController.register() +2. 🔍 参数验证 → class-validator装饰器 +3. 🎯 业务逻辑 → AuthService.register() +4. ⚙️ 核心服务 → LoginCoreService.createUser() +5. 📧 发送邮件 → EmailService.sendVerificationCode() +6. 🔢 生成验证码 → VerificationService.generate() +7. 💾 存储数据 → UsersService.create() + RedisService.set() +8. 📝 记录日志 → LoggerService.log() +9. ✅ 返回响应 → 用户收到成功响应 ``` -## 数据流向 +#### 管理员操作流程示例 -### 用户注册流程 ``` -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 (RedisService + 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 RedisService(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 (缓存服务) +│ ├── RedisService (真实Redis) +│ └── FileRedisService (文件存储) +├── 🗄️ UsersModule (用户数据) +│ ├── UsersService (MySQL数据库) +│ └── UsersMemoryService (内存数据库) +├── 📧 EmailModule (邮件服务) +├── 🔢 VerificationModule (验证码服务) +├── 🔑 LoginCoreModule (登录核心) +├── 👑 AdminCoreModule (管理员核心) +├── 💬 ZulipCoreModule (Zulip核心) +│ +├── 🎯 业务功能模块 +│ ├── 🔐 AuthModule (用户认证) +│ │ └── 依赖: LoginCoreModule, EmailModule, VerificationModule +│ ├── 👥 UserMgmtModule (用户管理) +│ │ └── 依赖: UsersModule, LoggerModule +│ ├── 🛡️ AdminModule (管理员) +│ │ └── 依赖: AdminCoreModule, UsersModule +│ ├── 🔒 SecurityModule (安全防护) +│ │ └── 依赖: RedisModule, LoggerModule +│ ├── 💬 ZulipModule (Zulip集成) +│ │ └── 依赖: ZulipCoreModule, RedisModule +│ └── 🔗 SharedModule (共享组件) +``` -### 1. 缓存策略 -- **Redis缓存**: 验证码、会话信息 +### 🔄 模块交互流程 + +#### 用户认证流程 +``` +AuthController → AuthService → 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/DOCUMENT_CLEANUP.md b/docs/DOCUMENT_CLEANUP.md new file mode 100644 index 0000000..5a0df87 --- /dev/null +++ b/docs/DOCUMENT_CLEANUP.md @@ -0,0 +1,142 @@ +# 📝 文档清理说明 + +> 记录项目文档整理和优化的过程,确保文档结构清晰、内容准确。 + +## 🎯 清理目标 + +- **删除多余README** - 移除重复和过时的README文件 +- **优化主文档** - 改进项目主README的文件格式和结构说明 +- **完善架构文档** - 详细说明项目架构和文件夹组织结构 +- **统一文档风格** - 采用总分结构,方便开发者理解 + +## 📊 文档清理结果 + +### ✅ 保留的README文件 + +| 文件路径 | 保留原因 | 主要内容 | +|----------|----------|----------| +| `README.md` | 项目主文档 | 项目介绍、快速开始、技术栈、功能特性 | +| `docs/README.md` | 文档导航中心 | 文档结构说明、导航链接 | +| `client/README.md` | 前端项目文档 | 前端管理界面的独立说明 | +| `docs/api/README.md` | API文档指南 | API文档使用说明和快速测试 | +| `src/business/zulip/README.md` | 模块架构说明 | Zulip模块重构的详细说明 | + +### ❌ 删除的README文件 + +**无** - 经过分析,所有现有README文件都有其存在价值,未删除任何文件。 + +### 🔄 优化的文档 + +#### 1. 主README.md优化 +- **文件结构总览** - 添加了详细的项目文件结构说明 +- **图标化展示** - 使用emoji图标让结构更直观 +- **层次化组织** - 按照总分结构组织内容 + +#### 2. 架构文档大幅改进 (docs/ARCHITECTURE.md) +- **完整重写** - 从简单的架构图扩展为完整的架构设计文档 +- **目录结构详解** - 详细说明每个文件夹的作用和内容 +- **分层架构设计** - 清晰的架构分层和模块依赖关系 +- **双模式架构** - 详细说明开发测试模式和生产部署模式 +- **扩展指南** - 提供添加新模块和功能的详细指导 + +## 📁 文档结构优化 + +### 🎯 总分结构设计 + +采用**总分结构**组织文档,便于开发者快速理解: + +``` +📚 文档层次结构 +├── 🏠 项目总览 (README.md) +│ ├── 🎯 项目简介和特性 +│ ├── 🚀 快速开始指南 +│ ├── 📁 文件结构总览 ⭐ 新增 +│ ├── 🛠️ 技术栈说明 +│ └── 📚 文档导航链接 +│ +├── 🏗️ 架构设计 (docs/ARCHITECTURE.md) ⭐ 大幅改进 +│ ├── 📊 整体架构图 +│ ├── 📁 目录结构详解 +│ ├── 🏗️ 分层架构设计 +│ ├── 🔄 双模式架构 +│ └── 🚀 扩展指南 +│ +├── 📖 文档中心 (docs/README.md) +│ ├── 📋 文档导航 +│ ├── 🏗️ 文档结构说明 +│ └── 📝 文档维护原则 +│ +├── 🔌 API文档 (docs/api/README.md) +│ ├── 📊 API接口概览 +│ ├── 🚀 快速开始 +│ └── 🧪 测试指南 +│ +└── 🎨 前端文档 (client/README.md) + ├── 🚀 快速开始 + ├── 🎯 核心功能 + └── 🔧 开发指南 +``` + +### 📊 文档内容优化 + +#### 1. 视觉化改进 +- **emoji图标** - 使用统一的emoji图标系统 +- **表格展示** - 用表格清晰展示对比信息 +- **代码示例** - 提供完整的代码示例和配置 +- **架构图** - 使用ASCII艺术绘制清晰的架构图 + +#### 2. 结构化内容 +- **目录导航** - 每个长文档都有详细目录 +- **分层说明** - 按照业务功能模块化的原则组织 +- **实用指南** - 提供具体的操作步骤和扩展指南 + +#### 3. 开发者友好 +- **快速上手** - 新开发者指南,从规范学习到架构理解 +- **总分结构** - 先总览后详细,便于快速理解 +- **实际案例** - 提供真实的代码示例和使用场景 + +## 🎯 文档维护原则 + +### ✅ 保留标准 +- **长期价值** - 对整个项目生命周期都有价值 +- **参考价值** - 开发、部署、维护时需要查阅 +- **规范指导** - 团队协作和代码质量保证 + +### ❌ 清理标准 +- **阶段性文档** - 只在特定开发阶段有用 +- **临时记录** - 会议记录、临时决策等 +- **过时信息** - 已经不适用的旧版本文档 + +### 🔄 更新策略 +- **及时更新** - 功能变更时同步更新相关文档 +- **版本控制** - 重要变更记录版本历史 +- **定期审查** - 定期检查文档的准确性和有效性 + +## 📈 改进效果 + +### 🎯 开发者体验提升 +- **快速理解** - 通过总分结构快速掌握项目架构 +- **准确信息** - 文档与实际代码结构完全一致 +- **实用指导** - 提供具体的开发和扩展指南 + +### 📚 文档质量提升 +- **结构清晰** - 层次分明的文档组织结构 +- **内容完整** - 覆盖项目的所有重要方面 +- **易于维护** - 明确的维护原则和更新策略 + +### 🚀 项目可维护性提升 +- **架构清晰** - 详细的架构文档便于理解和扩展 +- **规范统一** - 统一的文档风格和组织原则 +- **知识传承** - 完整的文档体系便于团队协作 + +--- + +**📝 通过系统性的文档清理和优化,项目文档现在更加清晰、准确、实用!** + +## 📅 清理记录 + +- **清理时间**: 2025年12月31日 +- **清理范围**: 项目根目录及所有子目录的README文件 +- **主要改进**: 架构文档完全重写,主README结构优化 +- **保留文件**: 5个README文件全部保留 +- **删除文件**: 0个(所有文件都有价值) \ No newline at end of file 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) - 文档维护和优化记录 ## 🏗️ **文档结构说明** -- 2.25.1 From 2d10131838db35f266a6fb2a770dccc90af6f36e Mon Sep 17 00:00:00 2001 From: moyin <244344649@qq.com> Date: Wed, 31 Dec 2025 15:44:36 +0800 Subject: [PATCH 2/6] =?UTF-8?q?refactor=EF=BC=9A=E9=87=8D=E6=9E=84Zulip?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E6=8C=89=E4=B8=9A=E5=8A=A1=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E5=8C=96=E6=9E=B6=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将技术实现服务从business层迁移到core层 - 创建src/core/zulip/核心服务模块,包含API客户端、连接池等技术服务 - 保留src/business/zulip/业务逻辑,专注游戏相关的业务规则 - 通过依赖注入实现业务层与核心层的解耦 - 更新模块导入关系,确保架构分层清晰 重构后的架构符合单一职责原则,提高了代码的可维护性和可测试性 --- src/app.module.ts | 2 + src/business/zulip/README.md | 172 +++ ...spec.ts => message_filter.service.spec.ts} | 16 +- ...r.service.ts => message_filter.service.ts} | 28 +- .../services/session_cleanup.service.spec.ts | 650 ++++++++++ ....service.ts => session_cleanup.service.ts} | 36 +- ...pec.ts => session_manager.service.spec.ts} | 18 +- ....service.ts => session_manager.service.ts} | 35 +- ... => zulip_event_processor.service.spec.ts} | 29 +- ...ce.ts => zulip_event_processor.service.ts} | 34 +- src/business/zulip/zulip.module.ts | 104 +- src/business/zulip/zulip.service.spec.ts | 1134 +++++++++++++++++ src/business/zulip/zulip.service.ts | 43 +- ....spec.ts => zulip_integration.e2e.spec.ts} | 2 +- ...pec.ts => zulip_websocket.gateway.spec.ts} | 4 +- ....gateway.ts => zulip_websocket.gateway.ts} | 25 +- src/{business => core}/zulip/config/index.ts | 0 .../zulip/config/zulip.config.ts | 0 src/core/zulip/index.ts | 26 + .../zulip/interfaces/zulip-core.interfaces.ts | 294 +++++ .../zulip/interfaces/zulip.interfaces.ts | 0 .../api_key_security.service.spec.ts} | 2 +- .../services/api_key_security.service.ts} | 22 + .../services/config_manager.service.spec.ts} | 4 +- .../zulip/services/config_manager.service.ts} | 25 + .../services/error_handler.service.spec.ts} | 2 +- .../zulip/services/error_handler.service.ts} | 22 + .../zulip/services/monitoring.service.spec.ts | 0 .../zulip/services/monitoring.service.ts | 23 + .../services/stream_initializer.service.ts} | 24 +- .../services/zulip_client.service.spec.ts} | 2 +- .../zulip/services/zulip_client.service.ts} | 22 + .../zulip_client_pool.service.spec.ts} | 6 +- .../services/zulip_client_pool.service.ts} | 24 +- .../zulip/types/zulip-js.d.ts | 0 src/core/zulip/zulip-core.module.ts | 68 + 36 files changed, 2773 insertions(+), 125 deletions(-) create mode 100644 src/business/zulip/README.md rename src/business/zulip/services/{message-filter.service.spec.ts => message_filter.service.spec.ts} (97%) rename src/business/zulip/services/{message-filter.service.ts => message_filter.service.ts} (96%) create mode 100644 src/business/zulip/services/session_cleanup.service.spec.ts rename src/business/zulip/services/{session-cleanup.service.ts => session_cleanup.service.ts} (86%) rename src/business/zulip/services/{session-manager.service.spec.ts => session_manager.service.spec.ts} (97%) rename src/business/zulip/services/{session-manager.service.ts => session_manager.service.ts} (95%) rename src/business/zulip/services/{zulip-event-processor.service.spec.ts => zulip_event_processor.service.spec.ts} (97%) rename src/business/zulip/services/{zulip-event-processor.service.ts => zulip_event_processor.service.ts} (96%) create mode 100644 src/business/zulip/zulip.service.spec.ts rename src/business/zulip/{zulip-integration.e2e.spec.ts => zulip_integration.e2e.spec.ts} (99%) rename src/business/zulip/{zulip-websocket.gateway.spec.ts => zulip_websocket.gateway.spec.ts} (99%) rename src/business/zulip/{zulip-websocket.gateway.ts => zulip_websocket.gateway.ts} (95%) rename src/{business => core}/zulip/config/index.ts (100%) rename src/{business => core}/zulip/config/zulip.config.ts (100%) create mode 100644 src/core/zulip/index.ts create mode 100644 src/core/zulip/interfaces/zulip-core.interfaces.ts rename src/{business => core}/zulip/interfaces/zulip.interfaces.ts (100%) rename src/{business/zulip/services/api-key-security.service.spec.ts => core/zulip/services/api_key_security.service.spec.ts} (99%) rename src/{business/zulip/services/api-key-security.service.ts => core/zulip/services/api_key_security.service.ts} (97%) rename src/{business/zulip/services/config-manager.service.spec.ts => core/zulip/services/config_manager.service.spec.ts} (99%) rename src/{business/zulip/services/config-manager.service.ts => core/zulip/services/config_manager.service.ts} (98%) rename src/{business/zulip/services/error-handler.service.spec.ts => core/zulip/services/error_handler.service.spec.ts} (99%) rename src/{business/zulip/services/error-handler.service.ts => core/zulip/services/error_handler.service.ts} (97%) rename src/{business => core}/zulip/services/monitoring.service.spec.ts (100%) rename src/{business => core}/zulip/services/monitoring.service.ts (96%) rename src/{business/zulip/services/stream-initializer.service.ts => core/zulip/services/stream_initializer.service.ts} (92%) rename src/{business/zulip/services/zulip-client.service.spec.ts => core/zulip/services/zulip_client.service.spec.ts} (99%) rename src/{business/zulip/services/zulip-client.service.ts => core/zulip/services/zulip_client.service.ts} (96%) rename src/{business/zulip/services/zulip-client-pool.service.spec.ts => core/zulip/services/zulip_client_pool.service.spec.ts} (98%) rename src/{business/zulip/services/zulip-client-pool.service.ts => core/zulip/services/zulip_client_pool.service.ts} (95%) rename src/{business => core}/zulip/types/zulip-js.d.ts (100%) create mode 100644 src/core/zulip/zulip-core.module.ts diff --git a/src/app.module.ts b/src/app.module.ts index 858b3cd..2355bfc 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,6 +8,7 @@ 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'; @@ -67,6 +68,7 @@ function isDatabaseConfigured(): boolean { isDatabaseConfigured() ? UsersModule.forDatabase() : UsersModule.forMemory(), LoginCoreModule, AuthModule, + ZulipModule, UserMgmtModule, AdminModule, SecurityModule, 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..32faf11 100644 --- a/src/business/zulip/zulip.service.ts +++ b/src/business/zulip/zulip.service.ts @@ -22,14 +22,15 @@ * @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'; /** * 玩家登录请求接口 @@ -79,18 +80,40 @@ 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, ) { this.logger.log('ZulipService初始化完成'); } 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/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 98% rename from src/business/zulip/services/config-manager.service.ts rename to src/core/zulip/services/config_manager.service.ts index a569824..a340599 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(); @@ -216,6 +238,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/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..e30d2c1 --- /dev/null +++ b/src/core/zulip/zulip-core.module.ts @@ -0,0 +1,68 @@ +/** + * 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 { 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, + + // 直接提供类(用于内部依赖) + ZulipClientService, + ZulipClientPoolService, + ConfigManagerService, + ], + exports: [ + // 导出接口标识符供业务层使用 + 'ZULIP_CLIENT_SERVICE', + 'ZULIP_CLIENT_POOL_SERVICE', + 'ZULIP_CONFIG_SERVICE', + + // 导出辅助服务 + ApiKeySecurityService, + ErrorHandlerService, + MonitoringService, + StreamInitializerService, + ], +}) +export class ZulipCoreModule {} \ No newline at end of file -- 2.25.1 From faf93a30e1e0d062e39722ea037b1db4ff5217c2 Mon Sep 17 00:00:00 2001 From: moyin <244344649@qq.com> Date: Wed, 31 Dec 2025 15:45:26 +0800 Subject: [PATCH 3/6] =?UTF-8?q?chore=EF=BC=9A=E6=9B=B4=E6=96=B0=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=96=87=E4=BB=B6=E5=92=8C=E9=A1=B9=E7=9B=AE=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新tsconfig.json配置以支持新的模块结构 - 添加REFACTORING_SUMMARY.md记录重构过程 - 更新git_commit_guide.md完善提交规范 - 添加相关图片资源 这些配置和文档更新支持项目架构重构后的正常运行 --- REFACTORING_SUMMARY.md | 163 ++++++++++++++++++ .../ab164782cdc17e22f9bdf443c7e1e96c.png | Bin 0 -> 495106 bytes docs/development/git_commit_guide.md | 4 + tsconfig.json | 5 +- 4 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 REFACTORING_SUMMARY.md create mode 100644 docs/development/ab164782cdc17e22f9bdf443c7e1e96c.png diff --git a/REFACTORING_SUMMARY.md b/REFACTORING_SUMMARY.md new file mode 100644 index 0000000..b27665d --- /dev/null +++ b/REFACTORING_SUMMARY.md @@ -0,0 +1,163 @@ +# Zulip模块重构总结 + +## 重构完成情况 + +✅ **重构已完成** - 项目编译成功,架构符合分层设计原则 + +## 重构内容 + +### 1. 架构分层重构 + +#### 移动到核心服务层 (`src/core/zulip/`) +以下技术实现相关的服务已移动到核心服务层: + +- `zulip_client.service.ts` - Zulip REST API封装 +- `zulip_client_pool.service.ts` - 客户端连接池管理 +- `config_manager.service.ts` - 配置文件管理和热重载 +- `api_key_security.service.ts` - API Key安全存储 +- `error_handler.service.ts` - 错误处理和重试机制 +- `monitoring.service.ts` - 系统监控和健康检查 +- `stream_initializer.service.ts` - Stream初始化服务 + +#### 保留在业务逻辑层 (`src/business/zulip/`) +以下业务逻辑相关的服务保留在业务层: + +- `zulip.service.ts` - 主要业务协调服务 +- `zulip_websocket.gateway.ts` - WebSocket业务网关 +- `session_manager.service.ts` - 游戏会话业务逻辑 +- `message_filter.service.ts` - 消息过滤业务规则 +- `zulip_event_processor.service.ts` - 事件处理业务逻辑 +- `session_cleanup.service.ts` - 会话清理业务逻辑 + +### 2. 依赖注入重构 + +#### 创建接口抽象 +- 新增 `src/core/zulip/interfaces/zulip-core.interfaces.ts` +- 定义核心服务接口:`IZulipClientService`、`IZulipClientPoolService`、`IZulipConfigService` + +#### 更新依赖注入 +业务层服务现在通过接口依赖核心服务: + +```typescript +// 旧方式 - 直接依赖具体实现 +constructor( + private readonly zulipClientPool: ZulipClientPoolService, +) {} + +// 新方式 - 通过接口依赖 +constructor( + @Inject('ZULIP_CLIENT_POOL_SERVICE') + private readonly zulipClientPool: IZulipClientPoolService, +) {} +``` + +### 3. 模块结构重构 + +#### 核心服务模块 +- 新增 `ZulipCoreModule` - 提供所有技术实现服务 +- 通过依赖注入标识符导出服务 + +#### 业务逻辑模块 +- 更新 `ZulipModule` - 导入核心模块,专注业务逻辑 +- 移除技术实现相关的服务提供者 + +### 4. 文件移动记录 + +#### 移动到核心层的文件 +``` +src/business/zulip/services/ → src/core/zulip/services/ +├── zulip_client.service.ts +├── zulip_client_pool.service.ts +├── config_manager.service.ts +├── api_key_security.service.ts +├── error_handler.service.ts +├── monitoring.service.ts +├── stream_initializer.service.ts +└── 对应的 .spec.ts 测试文件 + +src/business/zulip/ → src/core/zulip/ +├── interfaces/ +├── config/ +└── types/ +``` + +## 架构优势 + +### 1. 符合分层架构原则 +- **业务层**:只关注游戏相关的业务逻辑和规则 +- **核心层**:只处理技术实现和第三方API调用 + +### 2. 依赖倒置 +- 业务层依赖接口,不依赖具体实现 +- 核心层提供接口实现 +- 便于测试和替换实现 + +### 3. 单一职责 +- 每个服务职责明确 +- 业务逻辑与技术实现分离 +- 代码更易维护和理解 + +### 4. 可测试性 +- 业务逻辑可以独立测试 +- 通过Mock接口进行单元测试 +- 技术实现可以独立验证 + +## 当前状态 + +### ✅ 已完成 +- [x] 文件移动和重新组织 +- [x] 接口定义和抽象 +- [x] 依赖注入重构 +- [x] 模块结构调整 +- [x] 编译通过验证 +- [x] 测试文件的依赖注入配置更新 +- [x] 所有测试用例通过验证 + +### ✅ 测试修复完成 +- [x] `zulip_event_processor.service.spec.ts` - 更新依赖注入配置 +- [x] `message_filter.service.spec.ts` - 已通过测试 +- [x] `session_manager.service.spec.ts` - 已通过测试 +- [x] 核心服务测试文件导入路径修复 +- [x] 所有Zulip相关测试通过 + +## 使用指南 + +### 业务层开发 +```typescript +// 在业务服务中使用核心服务 +@Injectable() +export class MyBusinessService { + constructor( + @Inject('ZULIP_CLIENT_POOL_SERVICE') + private readonly zulipClientPool: IZulipClientPoolService, + ) {} +} +``` + +### 测试配置 +```typescript +// 测试中Mock核心服务 +const mockZulipClientPool: IZulipClientPoolService = { + sendMessage: jest.fn().mockResolvedValue({ success: true }), + // ... +}; + +const module = await Test.createTestingModule({ + providers: [ + MyBusinessService, + { provide: 'ZULIP_CLIENT_POOL_SERVICE', useValue: mockZulipClientPool }, + ], +}).compile(); +``` + +## 总结 + +重构成功实现了以下目标: + +1. **架构合规**:符合项目的分层架构设计原则 +2. **职责分离**:业务逻辑与技术实现清晰分离 +3. **依赖解耦**:通过接口实现依赖倒置 +4. **可维护性**:代码结构更清晰,易于维护和扩展 +5. **可测试性**:业务逻辑可以独立测试 + +项目现在具有更好的架构设计,为后续开发和维护奠定了良好基础。 \ No newline at end of file diff --git a/docs/development/ab164782cdc17e22f9bdf443c7e1e96c.png b/docs/development/ab164782cdc17e22f9bdf443c7e1e96c.png new file mode 100644 index 0000000000000000000000000000000000000000..5678daee66868725cd00df0f6cc4600d9c29ebcf GIT binary patch literal 495106 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 Za!K< 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

jH3nXGO6toHkB{Cm>RpMR2; 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 - - -
- ⚠️ 测试模式:验证码是 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 514dca2..3b858e2 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -25,6 +25,29 @@ 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 + ### 📊 整体架构图 ``` @@ -40,11 +63,11 @@ Whale Town 采用**业务功能模块化架构**,将代码按业务功能而 │ 🎯 业务功能模块层 │ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ │ 🔐 用户认证 │ │ 👥 用户管理 │ │ 🛡️ 管理员 │ │ -│ │ (auth) │ │ (user-mgmt) │ │ (admin) │ │ +│ │ (auth) │ │ (user_mgmt) │ │ (admin) │ │ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ -│ │ 🔒 安全防护 │ │ 💬 Zulip集成 │ │ 🔗 共享组件 │ │ -│ │ (security) │ │ (zulip) │ │ (shared) │ │ +│ │ 💬 Zulip集成 │ │ 🔗 共享组件 │ │ │ │ +│ │ (zulip) │ │ (shared) │ │ │ │ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ⬇️ @@ -52,11 +75,11 @@ Whale Town 采用**业务功能模块化架构**,将代码按业务功能而 │ ⚙️ 核心技术服务层 │ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ │ 🔑 登录核心 │ │ 👑 管理员核心 │ │ 💬 Zulip核心 │ │ -│ │ (login_core) │ │ (admin_core) │ │ (zulip) │ │ +│ │ (auth_core) │ │ (admin_core) │ │ (zulip) │ │ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ -│ │ 🛠️ 工具服务 │ │ 📧 邮件服务 │ │ 📝 日志服务 │ │ -│ │ (utils) │ │ (email) │ │ (logger) │ │ +│ │ 🛡️ 安全核心 │ │ 🛠️ 工具服务 │ │ 📧 邮件服务 │ │ +│ │ (security_core)│ │ (utils) │ │ (email) │ │ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ⬇️ @@ -80,56 +103,50 @@ Whale Town 采用**业务功能模块化架构**,将代码按业务功能而 ``` src/business/ ├── 📂 auth/ # 🔐 用户认证模块 -│ ├── 📄 auth.controller.ts # HTTP接口控制器 -│ ├── 📄 auth.service.ts # 业务逻辑服务 │ ├── 📄 auth.module.ts # 模块定义 +│ ├── 📂 controllers/ # 控制器 +│ │ └── 📄 login.controller.ts # 登录接口控制器 +│ ├── 📂 services/ # 业务服务 +│ │ ├── 📄 login.service.ts # 登录业务逻辑 +│ │ └── 📄 login.service.spec.ts # 登录服务测试 │ ├── 📂 dto/ # 数据传输对象 │ │ ├── 📄 login.dto.ts # 登录请求DTO -│ │ ├── 📄 register.dto.ts # 注册请求DTO -│ │ └── 📄 reset-password.dto.ts # 重置密码DTO -│ └── 📂 __tests__/ # 单元测试 -│ └── 📄 auth.service.spec.ts +│ │ └── 📄 login_response.dto.ts # 登录响应DTO +│ └── 📂 guards/ # 权限守卫(预留) │ ├── 📂 user-mgmt/ # 👥 用户管理模块 -│ ├── 📄 user-mgmt.controller.ts # 用户管理接口 -│ ├── 📄 user-mgmt.service.ts # 用户状态管理逻辑 │ ├── 📄 user-mgmt.module.ts # 模块定义 +│ ├── 📂 controllers/ # 控制器 +│ │ └── 📄 user-status.controller.ts # 用户状态管理接口 +│ ├── 📂 services/ # 业务服务 +│ │ └── 📄 user-management.service.ts # 用户管理逻辑 │ ├── 📂 dto/ # 数据传输对象 -│ │ ├── 📄 update-status.dto.ts # 状态更新DTO -│ │ └── 📄 batch-status.dto.ts # 批量操作DTO -│ └── 📂 enums/ # 枚举定义 -│ └── 📄 user-status.enum.ts # 用户状态枚举 +│ │ ├── 📄 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/ # 权限守卫 -│ └── 📄 admin.guard.ts # 管理员权限验证 -│ -├── 📂 security/ # 🔒 安全防护模块 -│ ├── 📄 security.module.ts # 安全模块定义 -│ ├── 📂 guards/ # 安全守卫 -│ │ ├── 📄 throttle.guard.ts # 频率限制守卫 -│ │ ├── 📄 maintenance.guard.ts # 维护模式守卫 -│ │ └── 📄 content-type.guard.ts # 内容类型守卫 -│ └── 📂 interceptors/ # 拦截器 -│ └── 📄 timeout.interceptor.ts # 超时拦截器 │ ├── 📂 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/ # 🔗 共享业务组件 - ├── 📂 decorators/ # 装饰器 - ├── 📂 pipes/ # 管道 - ├── 📂 filters/ # 异常过滤器 - └── 📂 interfaces/ # 接口定义 + ├── 📂 dto/ # 共享数据传输对象 + └── 📄 index.ts # 导出文件 ``` ### ⚙️ 核心技术服务 (`src/core/`) @@ -139,72 +156,84 @@ src/business/ ``` src/core/ ├── 📂 db/ # 🗄️ 数据库层 -│ ├── 📄 db.module.ts # 数据库模块 -│ ├── 📂 users/ # 用户数据服务 -│ │ ├── 📄 users.service.ts # MySQL数据库实现 -│ │ ├── 📄 users-memory.service.ts # 内存数据库实现 -│ │ ├── 📄 users.interface.ts # 用户服务接口 -│ │ └── 📄 user.entity.ts # 用户实体定义 -│ └── 📂 entities/ # 数据库实体 -│ └── 📄 *.entity.ts # 各种实体定义 +│ └── 📂 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模块 -│ ├── 📄 redis.service.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.interface.ts # 接口定义 +│ ├── 📄 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.interface.ts # 接口定义 +│ ├── 📄 admin_core.service.ts # 管理员核心逻辑 +│ ├── 📄 admin_core.module.ts # 模块定义 +│ └── 📄 admin_core.service.spec.ts # 管理员核心测试 │ ├── 📂 zulip/ # 💬 Zulip核心服务 │ ├── 📄 zulip-core.module.ts # Zulip核心模块 -│ ├── 📄 zulip-api.service.ts # Zulip API服务 -│ └── 📄 zulip-websocket.service.ts # WebSocket服务 +│ ├── 📂 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.interface.ts # 邮件接口 + │ └── 📄 email.service.spec.ts # 邮件服务测试 ├── 📂 verification/ # 🔢 验证码服务 │ ├── 📄 verification.service.ts # 验证码生成验证 - │ └── 📄 verification.module.ts # 验证码模块 + │ ├── 📄 verification.module.ts # 验证码模块 + │ └── 📄 verification.service.spec.ts # 验证码服务测试 └── 📂 logger/ # 📝 日志服务 ├── 📄 logger.service.ts # 日志记录服务 - └── 📄 logger.module.ts # 日志模块 + ├── 📄 logger.module.ts # 日志模块 + ├── 📄 logger.config.ts # 日志配置 + └── 📄 log_management.service.ts # 日志管理服务 ``` ### 🎨 前端管理界面 (`client/`) -> **设计原则**: 独立的前端项目,提供管理员后台功能 +> **设计原则**: 独立的前端项目,提供管理员后台功能,基于React + Vite + Ant Design ``` client/ ├── 📂 src/ # 前端源码 -│ ├── 📂 components/ # 通用组件 -│ │ ├── 📄 Layout.tsx # 布局组件 -│ │ ├── 📄 UserTable.tsx # 用户表格组件 -│ │ └── 📄 LogViewer.tsx # 日志查看组件 +│ ├── 📂 app/ # 应用组件 +│ │ ├── 📄 App.tsx # 应用主组件 +│ │ └── 📄 AdminLayout.tsx # 管理员布局组件 │ ├── 📂 pages/ # 页面组件 -│ │ ├── 📄 Login.tsx # 登录页面 -│ │ ├── 📄 Dashboard.tsx # 仪表板 -│ │ ├── 📄 UserManagement.tsx # 用户管理 -│ │ └── 📄 LogManagement.tsx # 日志管理 -│ ├── 📂 services/ # API服务 +│ │ ├── 📄 LoginPage.tsx # 登录页面 +│ │ ├── 📄 UsersPage.tsx # 用户管理页面 +│ │ └── 📄 LogsPage.tsx # 日志管理页面 +│ ├── 📂 lib/ # 工具库 │ │ ├── 📄 api.ts # API客户端 -│ │ ├── 📄 auth.ts # 认证服务 -│ │ └── 📄 users.ts # 用户服务 -│ ├── 📂 utils/ # 工具函数 -│ ├── 📂 types/ # TypeScript类型 -│ ├── 📄 App.tsx # 应用主组件 +│ │ └── 📄 adminAuth.ts # 管理员认证服务 │ └── 📄 main.tsx # 应用入口 ├── 📂 dist/ # 构建产物 ├── 📄 package.json # 前端依赖 @@ -220,21 +249,17 @@ client/ docs/ ├── 📄 README.md # 📖 文档导航中心 ├── 📄 ARCHITECTURE.md # 🏗️ 架构设计文档 -├── 📄 API_STATUS_CODES.md # 📋 API状态码说明 ├── 📄 CONTRIBUTORS.md # 🤝 贡献者指南 │ ├── 📂 api/ # 🔌 API接口文档 │ ├── 📄 README.md # API文档使用指南 -│ ├── 📄 api-documentation.md # 完整API接口文档 -│ ├── 📄 openapi.yaml # OpenAPI规范文件 -│ └── 📄 postman-collection.json # Postman测试集合 +│ └── 📄 api-documentation.md # 完整API接口文档 │ ├── 📂 development/ # 💻 开发指南 │ ├── 📄 backend_development_guide.md # 后端开发规范 │ ├── 📄 git_commit_guide.md # Git提交规范 │ ├── 📄 AI辅助开发规范指南.md # AI辅助开发指南 -│ ├── 📄 TESTING.md # 测试指南 -│ └── 📄 naming_convention.md # 命名规范 +│ └── 📄 TESTING.md # 测试指南 │ └── 📂 deployment/ # 🚀 部署文档 └── 📄 DEPLOYMENT.md # 生产环境部署指南 @@ -261,14 +286,17 @@ test/ ├── 📄 .env # 🔧 环境变量配置 ├── 📄 .env.example # 🔧 环境变量示例 ├── 📄 .env.production.example # 🔧 生产环境示例 -├── 📄 package.json # 📋 项目依赖配置 +├── 📄 package.json # 📋 后端项目依赖配置 ├── 📄 pnpm-workspace.yaml # 📦 pnpm工作空间配置 ├── 📄 tsconfig.json # 📘 TypeScript配置 ├── 📄 jest.config.js # 🧪 Jest测试配置 ├── 📄 nest-cli.json # 🏠 NestJS CLI配置 -├── 📄 docker-compose.yml # 🐳 Docker编排配置 -├── 📄 Dockerfile # 🐳 Docker镜像配置 └── 📄 ecosystem.config.js # 🚀 PM2进程管理配置 + +client/ +├── 📄 package.json # 📋 前端项目依赖配置 +├── 📄 vite.config.ts # ⚡ Vite构建配置 +└── 📄 tsconfig.json # 📘 前端TypeScript配置 ``` --- @@ -327,18 +355,17 @@ test/ ### 🔄 数据流向 -#### 用户注册流程示例 +#### 用户登录流程示例 ``` -1. 📱 用户请求 → AuthController.register() +1. 📱 用户请求 → LoginController.login() 2. 🔍 参数验证 → class-validator装饰器 -3. 🎯 业务逻辑 → AuthService.register() -4. ⚙️ 核心服务 → LoginCoreService.createUser() -5. 📧 发送邮件 → EmailService.sendVerificationCode() -6. 🔢 生成验证码 → VerificationService.generate() -7. 💾 存储数据 → UsersService.create() + RedisService.set() -8. 📝 记录日志 → LoggerService.log() -9. ✅ 返回响应 → 用户收到成功响应 +3. 🎯 业务逻辑 → LoginService.login() +4. ⚙️ 核心服务 → LoginCoreService.validateUser() +5. 📧 发送验证码 → VerificationService.generate() +6. 💾 存储数据 → UsersService.findByEmail() + RedisService.set() +7. 📝 记录日志 → LoggerService.log() +8. ✅ 返回响应 → 用户收到登录结果 ``` #### 管理员操作流程示例 @@ -369,7 +396,7 @@ test/ | 功能模块 | 🧪 开发测试模式 | 🚀 生产部署模式 | |----------|----------------|----------------| | **数据库** | 内存存储 (UsersMemoryService) | MySQL (UsersService + TypeORM) | -| **缓存** | 文件存储 (FileRedisService) | Redis (RedisService + IORedis) | +| **缓存** | 文件存储 (FileRedisService) | Redis (RealRedisService + IORedis) | | **邮件** | 控制台输出 (测试模式) | SMTP服务器 (生产模式) | | **日志** | 控制台 + 文件 | 结构化日志 + 日志轮转 | | **配置** | `.env` 默认配置 | 环境变量 + 配置中心 | @@ -431,7 +458,7 @@ EMAIL_PASS=your_app_password const useFileRedis = configService.get('USE_FILE_REDIS'); return useFileRedis ? new FileRedisService() - : new RedisService(configService); + : new RealRedisService(configService); }, inject: [ConfigService], }, @@ -471,7 +498,7 @@ AppModule (应用主模块) ├── 📊 ConfigModule (全局配置) ├── 📝 LoggerModule (日志系统) ├── 🔴 RedisModule (缓存服务) -│ ├── RedisService (真实Redis) +│ ├── RealRedisService (真实Redis) │ └── FileRedisService (文件存储) ├── 🗄️ UsersModule (用户数据) │ ├── UsersService (MySQL数据库) @@ -481,16 +508,15 @@ AppModule (应用主模块) ├── 🔑 LoginCoreModule (登录核心) ├── 👑 AdminCoreModule (管理员核心) ├── 💬 ZulipCoreModule (Zulip核心) +├── 🔒 SecurityCoreModule (安全核心) │ ├── 🎯 业务功能模块 │ ├── 🔐 AuthModule (用户认证) -│ │ └── 依赖: LoginCoreModule, EmailModule, VerificationModule +│ │ └── 依赖: LoginCoreModule, EmailModule, VerificationModule, SecurityCoreModule │ ├── 👥 UserMgmtModule (用户管理) -│ │ └── 依赖: UsersModule, LoggerModule +│ │ └── 依赖: UsersModule, LoggerModule, SecurityCoreModule │ ├── 🛡️ AdminModule (管理员) -│ │ └── 依赖: AdminCoreModule, UsersModule -│ ├── 🔒 SecurityModule (安全防护) -│ │ └── 依赖: RedisModule, LoggerModule +│ │ └── 依赖: AdminCoreModule, UsersModule, SecurityCoreModule │ ├── 💬 ZulipModule (Zulip集成) │ │ └── 依赖: ZulipCoreModule, RedisModule │ └── 🔗 SharedModule (共享组件) @@ -500,7 +526,7 @@ AppModule (应用主模块) #### 用户认证流程 ``` -AuthController → AuthService → LoginCoreService +AuthController → LoginService → LoginCoreService ↓ EmailService ← VerificationService ← RedisService ↓ diff --git a/docs/DOCUMENT_CLEANUP.md b/docs/DOCUMENT_CLEANUP.md deleted file mode 100644 index 5a0df87..0000000 --- a/docs/DOCUMENT_CLEANUP.md +++ /dev/null @@ -1,142 +0,0 @@ -# 📝 文档清理说明 - -> 记录项目文档整理和优化的过程,确保文档结构清晰、内容准确。 - -## 🎯 清理目标 - -- **删除多余README** - 移除重复和过时的README文件 -- **优化主文档** - 改进项目主README的文件格式和结构说明 -- **完善架构文档** - 详细说明项目架构和文件夹组织结构 -- **统一文档风格** - 采用总分结构,方便开发者理解 - -## 📊 文档清理结果 - -### ✅ 保留的README文件 - -| 文件路径 | 保留原因 | 主要内容 | -|----------|----------|----------| -| `README.md` | 项目主文档 | 项目介绍、快速开始、技术栈、功能特性 | -| `docs/README.md` | 文档导航中心 | 文档结构说明、导航链接 | -| `client/README.md` | 前端项目文档 | 前端管理界面的独立说明 | -| `docs/api/README.md` | API文档指南 | API文档使用说明和快速测试 | -| `src/business/zulip/README.md` | 模块架构说明 | Zulip模块重构的详细说明 | - -### ❌ 删除的README文件 - -**无** - 经过分析,所有现有README文件都有其存在价值,未删除任何文件。 - -### 🔄 优化的文档 - -#### 1. 主README.md优化 -- **文件结构总览** - 添加了详细的项目文件结构说明 -- **图标化展示** - 使用emoji图标让结构更直观 -- **层次化组织** - 按照总分结构组织内容 - -#### 2. 架构文档大幅改进 (docs/ARCHITECTURE.md) -- **完整重写** - 从简单的架构图扩展为完整的架构设计文档 -- **目录结构详解** - 详细说明每个文件夹的作用和内容 -- **分层架构设计** - 清晰的架构分层和模块依赖关系 -- **双模式架构** - 详细说明开发测试模式和生产部署模式 -- **扩展指南** - 提供添加新模块和功能的详细指导 - -## 📁 文档结构优化 - -### 🎯 总分结构设计 - -采用**总分结构**组织文档,便于开发者快速理解: - -``` -📚 文档层次结构 -├── 🏠 项目总览 (README.md) -│ ├── 🎯 项目简介和特性 -│ ├── 🚀 快速开始指南 -│ ├── 📁 文件结构总览 ⭐ 新增 -│ ├── 🛠️ 技术栈说明 -│ └── 📚 文档导航链接 -│ -├── 🏗️ 架构设计 (docs/ARCHITECTURE.md) ⭐ 大幅改进 -│ ├── 📊 整体架构图 -│ ├── 📁 目录结构详解 -│ ├── 🏗️ 分层架构设计 -│ ├── 🔄 双模式架构 -│ └── 🚀 扩展指南 -│ -├── 📖 文档中心 (docs/README.md) -│ ├── 📋 文档导航 -│ ├── 🏗️ 文档结构说明 -│ └── 📝 文档维护原则 -│ -├── 🔌 API文档 (docs/api/README.md) -│ ├── 📊 API接口概览 -│ ├── 🚀 快速开始 -│ └── 🧪 测试指南 -│ -└── 🎨 前端文档 (client/README.md) - ├── 🚀 快速开始 - ├── 🎯 核心功能 - └── 🔧 开发指南 -``` - -### 📊 文档内容优化 - -#### 1. 视觉化改进 -- **emoji图标** - 使用统一的emoji图标系统 -- **表格展示** - 用表格清晰展示对比信息 -- **代码示例** - 提供完整的代码示例和配置 -- **架构图** - 使用ASCII艺术绘制清晰的架构图 - -#### 2. 结构化内容 -- **目录导航** - 每个长文档都有详细目录 -- **分层说明** - 按照业务功能模块化的原则组织 -- **实用指南** - 提供具体的操作步骤和扩展指南 - -#### 3. 开发者友好 -- **快速上手** - 新开发者指南,从规范学习到架构理解 -- **总分结构** - 先总览后详细,便于快速理解 -- **实际案例** - 提供真实的代码示例和使用场景 - -## 🎯 文档维护原则 - -### ✅ 保留标准 -- **长期价值** - 对整个项目生命周期都有价值 -- **参考价值** - 开发、部署、维护时需要查阅 -- **规范指导** - 团队协作和代码质量保证 - -### ❌ 清理标准 -- **阶段性文档** - 只在特定开发阶段有用 -- **临时记录** - 会议记录、临时决策等 -- **过时信息** - 已经不适用的旧版本文档 - -### 🔄 更新策略 -- **及时更新** - 功能变更时同步更新相关文档 -- **版本控制** - 重要变更记录版本历史 -- **定期审查** - 定期检查文档的准确性和有效性 - -## 📈 改进效果 - -### 🎯 开发者体验提升 -- **快速理解** - 通过总分结构快速掌握项目架构 -- **准确信息** - 文档与实际代码结构完全一致 -- **实用指导** - 提供具体的开发和扩展指南 - -### 📚 文档质量提升 -- **结构清晰** - 层次分明的文档组织结构 -- **内容完整** - 覆盖项目的所有重要方面 -- **易于维护** - 明确的维护原则和更新策略 - -### 🚀 项目可维护性提升 -- **架构清晰** - 详细的架构文档便于理解和扩展 -- **规范统一** - 统一的文档风格和组织原则 -- **知识传承** - 完整的文档体系便于团队协作 - ---- - -**📝 通过系统性的文档清理和优化,项目文档现在更加清晰、准确、实用!** - -## 📅 清理记录 - -- **清理时间**: 2025年12月31日 -- **清理范围**: 项目根目录及所有子目录的README文件 -- **主要改进**: 架构文档完全重写,主README结构优化 -- **保留文件**: 5个README文件全部保留 -- **删除文件**: 0个(所有文件都有价值) \ No newline at end of file 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 2355bfc..9835288 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -12,9 +12,9 @@ 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 @@ -71,7 +71,7 @@ function isDatabaseConfigured(): boolean { 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/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/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/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/core/zulip/services/config_manager.service.ts b/src/core/zulip/services/config_manager.service.ts index a340599..5ee40d0 100644 --- a/src/core/zulip/services/config_manager.service.ts +++ b/src/core/zulip/services/config_manager.service.ts @@ -139,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初始化完成'); 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 e6523f6..d48d43a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,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 -- 2.25.1 From 6ad8d804497a821a0100c6e10db6c2fe7d2ece89 Mon Sep 17 00:00:00 2001 From: angjustinl <96008766+ANGJustinl@users.noreply.github.com> Date: Mon, 5 Jan 2026 17:41:54 +0800 Subject: [PATCH 6/6] feat(zulip): Add Zulip account management and integrate with auth system - Add ZulipAccountsEntity, repository, and module for persistent Zulip account storage - Create ZulipAccountService in core layer for managing Zulip account lifecycle - Integrate Zulip account creation into login flow via LoginService - Add comprehensive test suite for Zulip account creation during user registration - Create quick test script for validating registered user Zulip integration - Update UsersEntity to support Zulip account associations - Update auth module to include Zulip and ZulipAccounts dependencies - Fix WebSocket connection protocol from ws:// to wss:// in API documentation - Enhance LoginCoreService to coordinate Zulip account provisioning during authentication --- docs/systems/zulip/api.md | 2 +- .../zulip/quick_tests/test-registered-user.js | 232 ++++++ src/business/auth/auth.module.ts | 12 +- src/business/auth/services/login.service.ts | 262 ++++++- .../login.service.zulip-account.spec.ts | 520 +++++++++++++ src/business/zulip/zulip.service.ts | 53 +- src/core/db/users/users.entity.ts | 24 +- .../zulip_accounts/zulip_accounts.entity.ts | 185 +++++ .../zulip_accounts/zulip_accounts.module.ts | 81 ++ .../zulip_accounts.repository.ts | 323 ++++++++ .../zulip_accounts_memory.repository.ts | 299 ++++++++ src/core/login_core/login_core.service.ts | 32 + .../zulip/services/zulip_account.service.ts | 708 ++++++++++++++++++ src/core/zulip/zulip-core.module.ts | 3 + 14 files changed, 2698 insertions(+), 38 deletions(-) create mode 100644 docs/systems/zulip/quick_tests/test-registered-user.js create mode 100644 src/business/auth/services/login.service.zulip-account.spec.ts create mode 100644 src/core/db/zulip_accounts/zulip_accounts.entity.ts create mode 100644 src/core/db/zulip_accounts/zulip_accounts.module.ts create mode 100644 src/core/db/zulip_accounts/zulip_accounts.repository.ts create mode 100644 src/core/db/zulip_accounts/zulip_accounts_memory.repository.ts create mode 100644 src/core/zulip/services/zulip_account.service.ts 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/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/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/zulip/zulip.service.ts b/src/business/zulip/zulip.service.ts index 32faf11..1386948 100644 --- a/src/business/zulip/zulip.service.ts +++ b/src/business/zulip/zulip.service.ts @@ -31,6 +31,7 @@ import { IZulipClientPoolService, IZulipConfigService, } from '../../core/zulip/interfaces/zulip-core.interfaces'; +import { ApiKeySecurityService } from '../../core/zulip/services/api_key_security.service'; /** * 玩家登录请求接口 @@ -114,6 +115,7 @@ export class ZulipService { private readonly eventProcessor: ZulipEventProcessorService, @Inject('ZULIP_CONFIG_SERVICE') private readonly configManager: IZulipConfigService, + private readonly apiKeySecurityService: ApiKeySecurityService, ) { this.logger.log('ZulipService初始化完成'); } @@ -318,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, }); } @@ -355,7 +359,6 @@ export class ZulipService { userId, username: `Player_${userId.substring(5, 10)}`, email: `${userId}@example.com`, - // 实际项目中从数据库获取 zulipEmail, zulipApiKey, }; 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/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/core/zulip/zulip-core.module.ts b/src/core/zulip/zulip-core.module.ts index e30d2c1..134ee45 100644 --- a/src/core/zulip/zulip-core.module.ts +++ b/src/core/zulip/zulip-core.module.ts @@ -19,6 +19,7 @@ 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({ @@ -46,6 +47,7 @@ import { RedisModule } from '../redis/redis.module'; ErrorHandlerService, MonitoringService, StreamInitializerService, + ZulipAccountService, // 直接提供类(用于内部依赖) ZulipClientService, @@ -63,6 +65,7 @@ import { RedisModule } from '../redis/redis.module'; ErrorHandlerService, MonitoringService, StreamInitializerService, + ZulipAccountService, ], }) export class ZulipCoreModule {} \ No newline at end of file -- 2.25.1

M^OWpylBxAD?0(KS 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 literal 0 HcmV?d00001 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 @@ + + +![alt text](ab164782cdc17e22f9bdf443c7e1e96c.png) + # Git 提交规范 本文档定义了项目的 Git 提交信息格式规范,以保持提交历史的清晰和一致性。 diff --git a/tsconfig.json b/tsconfig.json index 37fb249..e6523f6 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, -- 2.25.1 From 29b8b05a2a63aea47d8e536e9f19a09b44451d3f Mon Sep 17 00:00:00 2001 From: moyin <244344649@qq.com> Date: Wed, 31 Dec 2025 16:14:23 +0800 Subject: [PATCH 4/6] =?UTF-8?q?docs=EF=BC=9A=E6=9B=B4=E6=96=B0=E8=B4=A1?= =?UTF-8?q?=E7=8C=AE=E8=80=85=E4=BF=A1=E6=81=AF=E5=92=8C=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E9=87=8C=E7=A8=8B=E7=A2=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新所有贡献者的提交数统计(moyin: 112, jianuo: 11, angjustinl: 7) - 添加最新重要贡献记录,包括Zulip模块架构重构和文档体系优化 - 更新项目里程碑,记录12月31日的重大架构重构 - 完善贡献者的主要贡献描述,反映最新的工作成果 本次更新确保贡献者信息与实际提交记录保持一致 --- docs/CONTRIBUTORS.md | 56 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 7 deletions(-) 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个测试用例通过,测试覆盖率达到新高 ## 如何成为贡献者 -- 2.25.1 From 70c020a97cc5bdbc8d46ad8591de3b6c7cd9d564 Mon Sep 17 00:00:00 2001 From: moyin <244344649@qq.com> Date: Sun, 4 Jan 2026 19:34:16 +0800 Subject: [PATCH 5/6] =?UTF-8?q?refactor=EF=BC=9A=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=AE=89=E5=85=A8=E6=A8=A1=E5=9D=97=E6=9E=B6=E6=9E=84=EF=BC=8C?= =?UTF-8?q?=E5=B0=86security=E6=A8=A1=E5=9D=97=E8=BF=81=E7=A7=BB=E8=87=B3c?= =?UTF-8?q?ore=E5=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将src/business/security模块迁移至src/core/security_core - 更新模块导入路径和依赖关系 - 统一安全相关组件的命名规范(content_type.middleware.ts) - 清理过时的配置文件和文档 - 更新架构文档以反映新的模块结构 此次重构符合业务功能模块化架构设计原则,将技术基础设施 服务统一放置在core层,提高代码组织的清晰度和可维护性。 --- Dockerfile | 31 -- REFACTORING_SUMMARY.md | 163 --------- deploy.sh.example | 54 --- docker-compose.yml | 36 -- docs/API_STATUS_CODES.md | 257 -------------- docs/ARCHITECTURE.md | 214 ++++++----- docs/DOCUMENT_CLEANUP.md | 142 -------- nest-cli.json | 8 +- src/app.module.ts | 8 +- src/business/admin/admin.controller.ts | 2 +- .../auth/controllers/login.controller.ts | 4 +- .../controllers/user-status.controller.ts | 4 +- .../decorators/throttle.decorator.ts | 0 .../decorators/timeout.decorator.ts | 0 .../security_core}/guards/throttle.guard.ts | 0 .../security => core/security_core}/index.ts | 6 +- .../interceptors/timeout.interceptor.ts | 0 .../middleware/content_type.middleware.ts} | 0 .../middleware/maintenance.middleware.ts | 0 .../security_core/security_core.module.ts} | 8 +- .../zulip/services/config_manager.service.ts | 31 +- test-comprehensive.ps1 | 333 ------------------ test/core/db/users.test.ts | 0 tsconfig.json | 2 +- webhook-handler.js.example | 86 ----- 25 files changed, 174 insertions(+), 1215 deletions(-) delete mode 100644 Dockerfile delete mode 100644 REFACTORING_SUMMARY.md delete mode 100644 deploy.sh.example delete mode 100644 docker-compose.yml delete mode 100644 docs/API_STATUS_CODES.md delete mode 100644 docs/DOCUMENT_CLEANUP.md rename src/{business/security => core/security_core}/decorators/throttle.decorator.ts (100%) rename src/{business/security => core/security_core}/decorators/timeout.decorator.ts (100%) rename src/{business/security => core/security_core}/guards/throttle.guard.ts (100%) rename src/{business/security => core/security_core}/index.ts (71%) rename src/{business/security => core/security_core}/interceptors/timeout.interceptor.ts (100%) rename src/{business/security/middleware/content-type.middleware.ts => core/security_core/middleware/content_type.middleware.ts} (100%) rename src/{business/security => core/security_core}/middleware/maintenance.middleware.ts (100%) rename src/{business/security/security.module.ts => core/security_core/security_core.module.ts} (84%) delete mode 100644 test-comprehensive.ps1 delete mode 100644 test/core/db/users.test.ts delete mode 100644 webhook-handler.js.example 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/REFACTORING_SUMMARY.md b/REFACTORING_SUMMARY.md deleted file mode 100644 index b27665d..0000000 --- a/REFACTORING_SUMMARY.md +++ /dev/null @@ -1,163 +0,0 @@ -# Zulip模块重构总结 - -## 重构完成情况 - -✅ **重构已完成** - 项目编译成功,架构符合分层设计原则 - -## 重构内容 - -### 1. 架构分层重构 - -#### 移动到核心服务层 (`src/core/zulip/`) -以下技术实现相关的服务已移动到核心服务层: - -- `zulip_client.service.ts` - Zulip REST API封装 -- `zulip_client_pool.service.ts` - 客户端连接池管理 -- `config_manager.service.ts` - 配置文件管理和热重载 -- `api_key_security.service.ts` - API Key安全存储 -- `error_handler.service.ts` - 错误处理和重试机制 -- `monitoring.service.ts` - 系统监控和健康检查 -- `stream_initializer.service.ts` - Stream初始化服务 - -#### 保留在业务逻辑层 (`src/business/zulip/`) -以下业务逻辑相关的服务保留在业务层: - -- `zulip.service.ts` - 主要业务协调服务 -- `zulip_websocket.gateway.ts` - WebSocket业务网关 -- `session_manager.service.ts` - 游戏会话业务逻辑 -- `message_filter.service.ts` - 消息过滤业务规则 -- `zulip_event_processor.service.ts` - 事件处理业务逻辑 -- `session_cleanup.service.ts` - 会话清理业务逻辑 - -### 2. 依赖注入重构 - -#### 创建接口抽象 -- 新增 `src/core/zulip/interfaces/zulip-core.interfaces.ts` -- 定义核心服务接口:`IZulipClientService`、`IZulipClientPoolService`、`IZulipConfigService` - -#### 更新依赖注入 -业务层服务现在通过接口依赖核心服务: - -```typescript -// 旧方式 - 直接依赖具体实现 -constructor( - private readonly zulipClientPool: ZulipClientPoolService, -) {} - -// 新方式 - 通过接口依赖 -constructor( - @Inject('ZULIP_CLIENT_POOL_SERVICE') - private readonly zulipClientPool: IZulipClientPoolService, -) {} -``` - -### 3. 模块结构重构 - -#### 核心服务模块 -- 新增 `ZulipCoreModule` - 提供所有技术实现服务 -- 通过依赖注入标识符导出服务 - -#### 业务逻辑模块 -- 更新 `ZulipModule` - 导入核心模块,专注业务逻辑 -- 移除技术实现相关的服务提供者 - -### 4. 文件移动记录 - -#### 移动到核心层的文件 -``` -src/business/zulip/services/ → src/core/zulip/services/ -├── zulip_client.service.ts -├── zulip_client_pool.service.ts -├── config_manager.service.ts -├── api_key_security.service.ts -├── error_handler.service.ts -├── monitoring.service.ts -├── stream_initializer.service.ts -└── 对应的 .spec.ts 测试文件 - -src/business/zulip/ → src/core/zulip/ -├── interfaces/ -├── config/ -└── types/ -``` - -## 架构优势 - -### 1. 符合分层架构原则 -- **业务层**:只关注游戏相关的业务逻辑和规则 -- **核心层**:只处理技术实现和第三方API调用 - -### 2. 依赖倒置 -- 业务层依赖接口,不依赖具体实现 -- 核心层提供接口实现 -- 便于测试和替换实现 - -### 3. 单一职责 -- 每个服务职责明确 -- 业务逻辑与技术实现分离 -- 代码更易维护和理解 - -### 4. 可测试性 -- 业务逻辑可以独立测试 -- 通过Mock接口进行单元测试 -- 技术实现可以独立验证 - -## 当前状态 - -### ✅ 已完成 -- [x] 文件移动和重新组织 -- [x] 接口定义和抽象 -- [x] 依赖注入重构 -- [x] 模块结构调整 -- [x] 编译通过验证 -- [x] 测试文件的依赖注入配置更新 -- [x] 所有测试用例通过验证 - -### ✅ 测试修复完成 -- [x] `zulip_event_processor.service.spec.ts` - 更新依赖注入配置 -- [x] `message_filter.service.spec.ts` - 已通过测试 -- [x] `session_manager.service.spec.ts` - 已通过测试 -- [x] 核心服务测试文件导入路径修复 -- [x] 所有Zulip相关测试通过 - -## 使用指南 - -### 业务层开发 -```typescript -// 在业务服务中使用核心服务 -@Injectable() -export class MyBusinessService { - constructor( - @Inject('ZULIP_CLIENT_POOL_SERVICE') - private readonly zulipClientPool: IZulipClientPoolService, - ) {} -} -``` - -### 测试配置 -```typescript -// 测试中Mock核心服务 -const mockZulipClientPool: IZulipClientPoolService = { - sendMessage: jest.fn().mockResolvedValue({ success: true }), - // ... -}; - -const module = await Test.createTestingModule({ - providers: [ - MyBusinessService, - { provide: 'ZULIP_CLIENT_POOL_SERVICE', useValue: mockZulipClientPool }, - ], -}).compile(); -``` - -## 总结 - -重构成功实现了以下目标: - -1. **架构合规**:符合项目的分层架构设计原则 -2. **职责分离**:业务逻辑与技术实现清晰分离 -3. **依赖解耦**:通过接口实现依赖倒置 -4. **可维护性**:代码结构更清晰,易于维护和扩展 -5. **可测试性**:业务逻辑可以独立测试 - -项目现在具有更好的架构设计,为后续开发和维护奠定了良好基础。 \ No newline at end of file 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 - -