feat(login, zulip): 引入 JWT 验证并重构 API 密钥管理 #33

Closed
ANGJustinl wants to merge 13 commits from ANGJustinl/whale-town-end:master into main
14 changed files with 2698 additions and 38 deletions
Showing only changes of commit 2b87eac495 - Show all commits

View File

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

View File

@@ -0,0 +1,232 @@
/**
* 测试新注册用户的Zulip账号功能
*
* 功能:
* 1. 验证新注册用户可以通过游戏服务器登录
* 2. 验证Zulip账号已正确创建和关联
* 3. 验证用户可以通过WebSocket发送消息到Zulip
* 4. 验证用户可以接收来自Zulip的消息
*
* 使用方法:
* node docs/systems/zulip/quick_tests/test-registered-user.js
*/
const io = require('socket.io-client');
const axios = require('axios');
// 配置
const GAME_SERVER = 'http://localhost:3000';
const TEST_USER = {
username: 'angtest123',
password: 'angtest123',
email: 'angjustinl@163.com'
};
/**
* 步骤1: 登录游戏服务器获取token
*/
async function loginToGameServer() {
console.log('📝 步骤 1: 登录游戏服务器');
console.log(` 用户名: ${TEST_USER.username}`);
try {
const response = await axios.post(`${GAME_SERVER}/auth/login`, {
identifier: TEST_USER.username,
password: TEST_USER.password
});
if (response.data.success) {
console.log('✅ 登录成功');
console.log(` 用户ID: ${response.data.data.user.id}`);
console.log(` 昵称: ${response.data.data.user.nickname}`);
console.log(` 邮箱: ${response.data.data.user.email}`);
console.log(` Token: ${response.data.data.access_token.substring(0, 20)}...`);
return {
userId: response.data.data.user.id,
username: response.data.data.user.username,
token: response.data.data.access_token
};
} else {
throw new Error(response.data.message || '登录失败');
}
} catch (error) {
console.error('❌ 登录失败:', error.response?.data?.message || error.message);
throw error;
}
}
/**
* 步骤2: 通过WebSocket连接并测试Zulip集成
*/
async function testZulipIntegration(userInfo) {
console.log('\n📡 步骤 2: 测试 Zulip 集成');
console.log(` 连接到: ${GAME_SERVER}/game`);
return new Promise((resolve, reject) => {
const socket = io(`${GAME_SERVER}/game`, {
transports: ['websocket'],
timeout: 20000
});
let testStep = 0;
let testResults = {
connected: false,
loggedIn: false,
messageSent: false,
messageReceived: false
};
// 连接成功
socket.on('connect', () => {
console.log('✅ WebSocket 连接成功');
testResults.connected = true;
testStep = 1;
// 发送登录消息
const loginMessage = {
type: 'login',
token: userInfo.token
};
console.log('📤 发送登录消息...');
socket.emit('login', loginMessage);
});
// 登录成功
socket.on('login_success', (data) => {
console.log('✅ 登录成功');
console.log(` 会话ID: ${data.sessionId}`);
console.log(` 用户ID: ${data.userId}`);
console.log(` 用户名: ${data.username}`);
console.log(` 当前地图: ${data.currentMap}`);
testResults.loggedIn = true;
testStep = 2;
// 等待Zulip客户端初始化
console.log('\n⏳ 等待 3 秒让 Zulip 客户端初始化...');
setTimeout(() => {
const chatMessage = {
t: 'chat',
content: `🎮 【注册用户测试】来自 ${userInfo.username} 的消息!\n` +
`时间: ${new Date().toLocaleString()}\n` +
`这是通过新注册账号发送的测试消息。`,
scope: 'local'
};
console.log('📤 发送测试消息到 Zulip...');
console.log(` 内容: ${chatMessage.content.split('\n')[0]}`);
socket.emit('chat', chatMessage);
}, 3000);
});
// 消息发送成功
socket.on('chat_sent', (data) => {
console.log('✅ 消息发送成功');
console.log(` 消息ID: ${data.id || '未知'}`);
testResults.messageSent = true;
testStep = 3;
// 等待一段时间接收消息
setTimeout(() => {
console.log('\n📊 测试完成,断开连接...');
socket.disconnect();
}, 5000);
});
// 接收到消息
socket.on('chat_render', (data) => {
console.log('📨 收到来自 Zulip 的消息:');
console.log(` 发送者: ${data.from}`);
console.log(` 内容: ${data.txt}`);
console.log(` Stream: ${data.stream || '未知'}`);
console.log(` Topic: ${data.topic || '未知'}`);
testResults.messageReceived = true;
});
// 错误处理
socket.on('error', (error) => {
console.error('❌ 收到错误:', JSON.stringify(error, null, 2));
});
// 连接断开
socket.on('disconnect', () => {
console.log('\n🔌 WebSocket 连接已关闭');
resolve(testResults);
});
// 连接错误
socket.on('connect_error', (error) => {
console.error('❌ 连接错误:', error.message);
reject(error);
});
// 超时保护
setTimeout(() => {
if (socket.connected) {
socket.disconnect();
}
}, 15000);
});
}
/**
* 打印测试结果
*/
function printTestResults(results) {
console.log('\n' + '='.repeat(60));
console.log('📊 测试结果汇总');
console.log('='.repeat(60));
const checks = [
{ name: 'WebSocket 连接', passed: results.connected },
{ name: '游戏服务器登录', passed: results.loggedIn },
{ name: '发送消息到 Zulip', passed: results.messageSent },
{ name: '接收 Zulip 消息', passed: results.messageReceived }
];
checks.forEach(check => {
const icon = check.passed ? '✅' : '❌';
console.log(`${icon} ${check.name}: ${check.passed ? '通过' : '失败'}`);
});
const passedCount = checks.filter(c => c.passed).length;
const totalCount = checks.length;
console.log('='.repeat(60));
console.log(`总计: ${passedCount}/${totalCount} 项测试通过`);
if (passedCount === totalCount) {
console.log('\n🎉 所有测试通过Zulip账号创建和集成功能正常');
console.log('💡 提示: 请访问 https://zulip.xinghangee.icu/ 查看发送的消息');
} else {
console.log('\n⚠ 部分测试失败,请检查日志');
}
console.log('='.repeat(60));
}
/**
* 主测试流程
*/
async function runTest() {
console.log('🚀 开始测试新注册用户的 Zulip 集成功能');
console.log('='.repeat(60));
try {
// 步骤1: 登录
const userInfo = await loginToGameServer();
// 步骤2: 测试Zulip集成
const results = await testZulipIntegration(userInfo);
// 打印结果
printTestResults(results);
process.exit(results.connected && results.loggedIn && results.messageSent ? 0 : 1);
} catch (error) {
console.error('\n❌ 测试失败:', error.message);
process.exit(1);
}
}
// 运行测试
runTest();

View File

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

View File

@@ -16,9 +16,12 @@
* @since 2025-12-17
*/
import { Injectable, Logger } from '@nestjs/common';
import { Injectable, Logger, Inject } from '@nestjs/common';
import { LoginCoreService, LoginRequest, RegisterRequest, GitHubOAuthRequest, PasswordResetRequest, AuthResult, VerificationCodeLoginRequest } from '../../../core/login_core/login_core.service';
import { Users } from '../../../core/db/users/users.entity';
import { ZulipAccountService } from '../../../core/zulip/services/zulip_account.service';
import { ZulipAccountsRepository } from '../../../core/db/zulip_accounts/zulip_accounts.repository';
import { ApiKeySecurityService } from '../../../core/zulip/services/api_key_security.service';
/**
* 登录响应数据接口
@@ -65,6 +68,10 @@ export class LoginService {
constructor(
private readonly loginCoreService: LoginCoreService,
private readonly zulipAccountService: ZulipAccountService,
@Inject('ZulipAccountsRepository')
private readonly zulipAccountsRepository: ZulipAccountsRepository,
private readonly apiKeySecurityService: ApiKeySecurityService,
) {}
/**
@@ -116,36 +123,106 @@ export class LoginService {
* @returns 注册响应
*/
async register(registerRequest: RegisterRequest): Promise<ApiResponse<LoginResponse>> {
const startTime = Date.now();
try {
this.logger.log(`用户注册尝试: ${registerRequest.username}`);
// 调用核心服务进行注册
// 1. 初始化Zulip管理员客户端
await this.initializeZulipAdminClient();
// 2. 调用核心服务进行注册
const authResult = await this.loginCoreService.register(registerRequest);
// 生成访问令牌
// 3. 创建Zulip账号使用相同的邮箱和密码
let zulipAccountCreated = false;
try {
if (registerRequest.email && registerRequest.password) {
await this.createZulipAccountForUser(authResult.user, registerRequest.password);
zulipAccountCreated = true;
this.logger.log(`Zulip账号创建成功: ${registerRequest.username}`, {
operation: 'register',
gameUserId: authResult.user.id.toString(),
email: registerRequest.email,
});
} else {
this.logger.warn(`跳过Zulip账号创建缺少邮箱或密码`, {
operation: 'register',
username: registerRequest.username,
hasEmail: !!registerRequest.email,
hasPassword: !!registerRequest.password,
});
}
} catch (zulipError) {
const err = zulipError as Error;
this.logger.error(`Zulip账号创建失败回滚用户注册`, {
operation: 'register',
username: registerRequest.username,
gameUserId: authResult.user.id.toString(),
zulipError: err.message,
}, err.stack);
// 回滚游戏用户注册
try {
await this.loginCoreService.deleteUser(authResult.user.id);
this.logger.log(`用户注册回滚成功: ${registerRequest.username}`);
} catch (rollbackError) {
const rollbackErr = rollbackError as Error;
this.logger.error(`用户注册回滚失败`, {
operation: 'register',
username: registerRequest.username,
gameUserId: authResult.user.id.toString(),
rollbackError: rollbackErr.message,
}, rollbackErr.stack);
}
// 抛出原始错误
throw new Error(`注册失败Zulip账号创建失败 - ${err.message}`);
}
// 4. 生成访问令牌
const accessToken = this.generateAccessToken(authResult.user);
// 格式化响应数据
// 5. 格式化响应数据
const response: LoginResponse = {
user: this.formatUserInfo(authResult.user),
access_token: accessToken,
is_new_user: true,
message: '注册成功'
message: zulipAccountCreated ? '注册成功Zulip账号已同步创建' : '注册成功'
};
this.logger.log(`用户注册成功: ${authResult.user.username} (ID: ${authResult.user.id})`);
const duration = Date.now() - startTime;
this.logger.log(`用户注册成功: ${authResult.user.username} (ID: ${authResult.user.id})`, {
operation: 'register',
gameUserId: authResult.user.id.toString(),
username: authResult.user.username,
zulipAccountCreated,
duration,
timestamp: new Date().toISOString(),
});
return {
success: true,
data: response,
message: '注册成功'
message: response.message
};
} catch (error) {
this.logger.error(`用户注册失败: ${registerRequest.username}`, error instanceof Error ? error.stack : String(error));
const duration = Date.now() - startTime;
const err = error as Error;
this.logger.error(`用户注册失败: ${registerRequest.username}`, {
operation: 'register',
username: registerRequest.username,
error: err.message,
duration,
timestamp: new Date().toISOString(),
}, err.stack);
return {
success: false,
message: error instanceof Error ? error.message : '注册失败',
message: err.message || '注册失败',
error_code: 'REGISTER_FAILED'
};
}
@@ -592,4 +669,171 @@ export class LoginService {
};
}
}
/**
* 初始化Zulip管理员客户端
*
* 功能描述:
* 使用环境变量中的管理员凭证初始化Zulip客户端
*
* 业务逻辑:
* 1. 从环境变量获取管理员配置
* 2. 验证配置完整性
* 3. 初始化ZulipAccountService的管理员客户端
*
* @throws Error 当配置缺失或初始化失败时
* @private
*/
private async initializeZulipAdminClient(): Promise<void> {
try {
// 从环境变量获取管理员配置
const adminConfig = {
realm: process.env.ZULIP_SERVER_URL || '',
username: process.env.ZULIP_BOT_EMAIL || '',
apiKey: process.env.ZULIP_BOT_API_KEY || '',
};
// 验证配置完整性
if (!adminConfig.realm || !adminConfig.username || !adminConfig.apiKey) {
throw new Error('Zulip管理员配置不完整请检查环境变量 ZULIP_SERVER_URL, ZULIP_BOT_EMAIL, ZULIP_BOT_API_KEY');
}
// 初始化管理员客户端
const initialized = await this.zulipAccountService.initializeAdminClient(adminConfig);
if (!initialized) {
throw new Error('Zulip管理员客户端初始化失败');
}
this.logger.log('Zulip管理员客户端初始化成功', {
operation: 'initializeZulipAdminClient',
realm: adminConfig.realm,
adminEmail: adminConfig.username,
});
} catch (error) {
const err = error as Error;
this.logger.error('Zulip管理员客户端初始化失败', {
operation: 'initializeZulipAdminClient',
error: err.message,
}, err.stack);
throw error;
}
}
/**
* 为用户创建Zulip账号
*
* 功能描述:
* 为新注册的游戏用户创建对应的Zulip账号并建立关联
*
* 业务逻辑:
* 1. 使用相同的邮箱和密码创建Zulip账号
* 2. 加密存储API Key
* 3. 在数据库中建立关联关系
* 4. 处理创建失败的情况
*
* @param gameUser 游戏用户信息
* @param password 用户密码(明文)
* @throws Error 当Zulip账号创建失败时
* @private
*/
private async createZulipAccountForUser(gameUser: Users, password: string): Promise<void> {
const startTime = Date.now();
this.logger.log('开始为用户创建Zulip账号', {
operation: 'createZulipAccountForUser',
gameUserId: gameUser.id.toString(),
email: gameUser.email,
nickname: gameUser.nickname,
});
try {
// 1. 检查是否已存在Zulip账号关联
const existingAccount = await this.zulipAccountsRepository.findByGameUserId(gameUser.id);
if (existingAccount) {
this.logger.warn('用户已存在Zulip账号关联跳过创建', {
operation: 'createZulipAccountForUser',
gameUserId: gameUser.id.toString(),
existingZulipUserId: existingAccount.zulipUserId,
});
return;
}
// 2. 创建Zulip账号
const createResult = await this.zulipAccountService.createZulipAccount({
email: gameUser.email,
fullName: gameUser.nickname,
password: password,
});
if (!createResult.success) {
throw new Error(createResult.error || 'Zulip账号创建失败');
}
// 3. 存储API Key
if (createResult.apiKey) {
await this.apiKeySecurityService.storeApiKey(
gameUser.id.toString(),
createResult.apiKey
);
}
// 4. 在数据库中创建关联记录
await this.zulipAccountsRepository.create({
gameUserId: gameUser.id,
zulipUserId: createResult.userId!,
zulipEmail: createResult.email!,
zulipFullName: gameUser.nickname,
zulipApiKeyEncrypted: createResult.apiKey ? 'stored_in_redis' : '', // 标记API Key已存储在Redis中
status: 'active',
});
// 5. 建立游戏账号与Zulip账号的内存关联用于当前会话
if (createResult.apiKey) {
await this.zulipAccountService.linkGameAccount(
gameUser.id.toString(),
createResult.userId!,
createResult.email!,
createResult.apiKey
);
}
const duration = Date.now() - startTime;
this.logger.log('Zulip账号创建和关联成功', {
operation: 'createZulipAccountForUser',
gameUserId: gameUser.id.toString(),
zulipUserId: createResult.userId,
zulipEmail: createResult.email,
hasApiKey: !!createResult.apiKey,
duration,
});
} catch (error) {
const err = error as Error;
const duration = Date.now() - startTime;
this.logger.error('为用户创建Zulip账号失败', {
operation: 'createZulipAccountForUser',
gameUserId: gameUser.id.toString(),
email: gameUser.email,
error: err.message,
duration,
}, err.stack);
// 清理可能创建的部分数据
try {
await this.zulipAccountsRepository.deleteByGameUserId(gameUser.id);
} catch (cleanupError) {
this.logger.warn('清理Zulip账号关联数据失败', {
operation: 'createZulipAccountForUser',
gameUserId: gameUser.id.toString(),
cleanupError: (cleanupError as Error).message,
});
}
throw error;
}
}
}

View File

@@ -0,0 +1,520 @@
/**
* LoginService Zulip账号创建属性测试
*
* 功能描述:
* - 测试用户注册时Zulip账号创建的一致性
* - 验证账号关联和数据完整性
* - 测试失败回滚机制
*
* 属性测试:
* - 属性 13: Zulip账号创建一致性
* - 验证需求: 账号创建成功率和数据一致性
*
* @author angjustinl
* @version 1.0.0
* @since 2025-01-05
*/
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import * as fc from 'fast-check';
import { LoginService } from './login.service';
import { LoginCoreService, RegisterRequest } from '../../../core/login_core/login_core.service';
import { ZulipAccountService } from '../../../core/zulip/services/zulip_account.service';
import { ZulipAccountsRepository } from '../../../core/db/zulip_accounts/zulip_accounts.repository';
import { ApiKeySecurityService } from '../../../core/zulip/services/api_key_security.service';
import { Users } from '../../../core/db/users/users.entity';
import { ZulipAccounts } from '../../../core/db/zulip_accounts/zulip_accounts.entity';
describe('LoginService - Zulip账号创建属性测试', () => {
let loginService: LoginService;
let loginCoreService: jest.Mocked<LoginCoreService>;
let zulipAccountService: jest.Mocked<ZulipAccountService>;
let zulipAccountsRepository: jest.Mocked<ZulipAccountsRepository>;
let apiKeySecurityService: jest.Mocked<ApiKeySecurityService>;
// 测试用的模拟数据生成器
const validEmailArb = fc.string({ minLength: 5, maxLength: 50 })
.filter(s => s.includes('@') && s.includes('.'))
.map(s => `test_${s.replace(/[^a-zA-Z0-9@._-]/g, '')}@example.com`);
const validUsernameArb = fc.string({ minLength: 3, maxLength: 20 })
.filter(s => /^[a-zA-Z0-9_]+$/.test(s));
const validNicknameArb = fc.string({ minLength: 2, maxLength: 50 })
.filter(s => s.trim().length > 0);
const validPasswordArb = fc.string({ minLength: 8, maxLength: 20 })
.filter(s => /[a-zA-Z]/.test(s) && /\d/.test(s));
const registerRequestArb = fc.record({
username: validUsernameArb,
email: validEmailArb,
nickname: validNicknameArb,
password: validPasswordArb,
});
beforeEach(async () => {
// 创建模拟服务
const mockLoginCoreService = {
register: jest.fn(),
deleteUser: jest.fn(),
};
const mockZulipAccountService = {
initializeAdminClient: jest.fn(),
createZulipAccount: jest.fn(),
linkGameAccount: jest.fn(),
};
const mockZulipAccountsRepository = {
findByGameUserId: jest.fn(),
create: jest.fn(),
deleteByGameUserId: jest.fn(),
};
const mockApiKeySecurityService = {
storeApiKey: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
LoginService,
{
provide: LoginCoreService,
useValue: mockLoginCoreService,
},
{
provide: ZulipAccountService,
useValue: mockZulipAccountService,
},
{
provide: 'ZulipAccountsRepository',
useValue: mockZulipAccountsRepository,
},
{
provide: ApiKeySecurityService,
useValue: mockApiKeySecurityService,
},
],
}).compile();
loginService = module.get<LoginService>(LoginService);
loginCoreService = module.get(LoginCoreService);
zulipAccountService = module.get(ZulipAccountService);
zulipAccountsRepository = module.get('ZulipAccountsRepository');
apiKeySecurityService = module.get(ApiKeySecurityService);
// 设置环境变量模拟
process.env.ZULIP_SERVER_URL = 'https://test.zulip.com';
process.env.ZULIP_BOT_EMAIL = 'bot@test.zulip.com';
process.env.ZULIP_BOT_API_KEY = 'test_api_key_123';
});
afterEach(() => {
jest.clearAllMocks();
// 清理环境变量
delete process.env.ZULIP_SERVER_URL;
delete process.env.ZULIP_BOT_EMAIL;
delete process.env.ZULIP_BOT_API_KEY;
});
/**
* 属性 13: Zulip账号创建一致性
*
* 验证需求: 账号创建成功率和数据一致性
*
* 测试内容:
* 1. 成功注册时游戏账号和Zulip账号都应该被创建
* 2. 账号关联信息应该正确存储
* 3. Zulip账号创建失败时游戏账号应该被回滚
* 4. 数据一致性:邮箱、昵称等信息应该保持一致
*/
describe('属性 13: Zulip账号创建一致性', () => {
it('应该在成功注册时创建一致的游戏账号和Zulip账号', async () => {
await fc.assert(
fc.asyncProperty(registerRequestArb, async (registerRequest) => {
// 准备测试数据
const mockGameUser: Users = {
id: BigInt(Math.floor(Math.random() * 1000000)),
username: registerRequest.username,
email: registerRequest.email,
nickname: registerRequest.nickname,
password_hash: 'hashed_password',
role: 1,
created_at: new Date(),
updated_at: new Date(),
} as Users;
const mockZulipResult = {
success: true,
userId: Math.floor(Math.random() * 1000000),
email: registerRequest.email,
apiKey: 'zulip_api_key_' + Math.random().toString(36),
};
const mockZulipAccount: ZulipAccounts = {
id: BigInt(Math.floor(Math.random() * 1000000)),
gameUserId: mockGameUser.id,
zulipUserId: mockZulipResult.userId,
zulipEmail: mockZulipResult.email,
zulipFullName: registerRequest.nickname,
zulipApiKeyEncrypted: 'encrypted_' + mockZulipResult.apiKey,
status: 'active',
createdAt: new Date(),
updatedAt: new Date(),
} as ZulipAccounts;
// 设置模拟行为
zulipAccountService.initializeAdminClient.mockResolvedValue(true);
loginCoreService.register.mockResolvedValue({
user: mockGameUser,
isNewUser: true,
});
zulipAccountsRepository.findByGameUserId.mockResolvedValue(null);
zulipAccountService.createZulipAccount.mockResolvedValue(mockZulipResult);
apiKeySecurityService.storeApiKey.mockResolvedValue(undefined);
zulipAccountsRepository.create.mockResolvedValue(mockZulipAccount);
zulipAccountService.linkGameAccount.mockResolvedValue(true);
// 执行注册
const result = await loginService.register(registerRequest);
// 验证结果
expect(result.success).toBe(true);
expect(result.data?.user.username).toBe(registerRequest.username);
expect(result.data?.user.email).toBe(registerRequest.email);
expect(result.data?.user.nickname).toBe(registerRequest.nickname);
expect(result.data?.is_new_user).toBe(true);
// 验证Zulip管理员客户端初始化
expect(zulipAccountService.initializeAdminClient).toHaveBeenCalledWith({
realm: 'https://test.zulip.com',
username: 'bot@test.zulip.com',
apiKey: 'test_api_key_123',
});
// 验证游戏用户注册
expect(loginCoreService.register).toHaveBeenCalledWith(registerRequest);
// 验证Zulip账号创建
expect(zulipAccountService.createZulipAccount).toHaveBeenCalledWith({
email: registerRequest.email,
fullName: registerRequest.nickname,
password: registerRequest.password,
});
// 验证API Key存储
expect(apiKeySecurityService.storeApiKey).toHaveBeenCalledWith(
mockGameUser.id.toString(),
mockZulipResult.apiKey
);
// 验证账号关联创建
expect(zulipAccountsRepository.create).toHaveBeenCalledWith({
gameUserId: mockGameUser.id,
zulipUserId: mockZulipResult.userId,
zulipEmail: mockZulipResult.email,
zulipFullName: registerRequest.nickname,
zulipApiKeyEncrypted: 'stored_in_redis',
status: 'active',
});
// 验证内存关联
expect(zulipAccountService.linkGameAccount).toHaveBeenCalledWith(
mockGameUser.id.toString(),
mockZulipResult.userId,
mockZulipResult.email,
mockZulipResult.apiKey
);
}),
{ numRuns: 100 }
);
});
it('应该在Zulip账号创建失败时回滚游戏账号', async () => {
await fc.assert(
fc.asyncProperty(registerRequestArb, async (registerRequest) => {
// 准备测试数据
const mockGameUser: Users = {
id: BigInt(Math.floor(Math.random() * 1000000)),
username: registerRequest.username,
email: registerRequest.email,
nickname: registerRequest.nickname,
password_hash: 'hashed_password',
role: 1,
created_at: new Date(),
updated_at: new Date(),
} as Users;
// 设置模拟行为 - Zulip账号创建失败
zulipAccountService.initializeAdminClient.mockResolvedValue(true);
loginCoreService.register.mockResolvedValue({
user: mockGameUser,
isNewUser: true,
});
zulipAccountsRepository.findByGameUserId.mockResolvedValue(null);
zulipAccountService.createZulipAccount.mockResolvedValue({
success: false,
error: 'Zulip服务器连接失败',
errorCode: 'CONNECTION_FAILED',
});
loginCoreService.deleteUser.mockResolvedValue(true);
// 执行注册
const result = await loginService.register(registerRequest);
// 验证结果 - 注册应该失败
expect(result.success).toBe(false);
expect(result.message).toContain('Zulip账号创建失败');
// 验证游戏用户被创建
expect(loginCoreService.register).toHaveBeenCalledWith(registerRequest);
// 验证Zulip账号创建尝试
expect(zulipAccountService.createZulipAccount).toHaveBeenCalledWith({
email: registerRequest.email,
fullName: registerRequest.nickname,
password: registerRequest.password,
});
// 验证游戏用户被回滚删除
expect(loginCoreService.deleteUser).toHaveBeenCalledWith(mockGameUser.id);
// 验证没有创建账号关联
expect(zulipAccountsRepository.create).not.toHaveBeenCalled();
expect(zulipAccountService.linkGameAccount).not.toHaveBeenCalled();
}),
{ numRuns: 100 }
);
});
it('应该正确处理已存在Zulip账号关联的情况', async () => {
await fc.assert(
fc.asyncProperty(registerRequestArb, async (registerRequest) => {
// 准备测试数据
const mockGameUser: Users = {
id: BigInt(Math.floor(Math.random() * 1000000)),
username: registerRequest.username,
email: registerRequest.email,
nickname: registerRequest.nickname,
password_hash: 'hashed_password',
role: 1,
created_at: new Date(),
updated_at: new Date(),
} as Users;
const existingZulipAccount: ZulipAccounts = {
id: BigInt(Math.floor(Math.random() * 1000000)),
gameUserId: mockGameUser.id,
zulipUserId: 12345,
zulipEmail: registerRequest.email,
zulipFullName: registerRequest.nickname,
zulipApiKeyEncrypted: 'existing_encrypted_key',
status: 'active',
createdAt: new Date(),
updatedAt: new Date(),
} as ZulipAccounts;
// 设置模拟行为 - 已存在Zulip账号关联
zulipAccountService.initializeAdminClient.mockResolvedValue(true);
loginCoreService.register.mockResolvedValue({
user: mockGameUser,
isNewUser: true,
});
zulipAccountsRepository.findByGameUserId.mockResolvedValue(existingZulipAccount);
// 执行注册
const result = await loginService.register(registerRequest);
// 验证结果 - 注册应该成功
expect(result.success).toBe(true);
expect(result.data?.user.username).toBe(registerRequest.username);
// 验证游戏用户被创建
expect(loginCoreService.register).toHaveBeenCalledWith(registerRequest);
// 验证检查了现有关联
expect(zulipAccountsRepository.findByGameUserId).toHaveBeenCalledWith(mockGameUser.id);
// 验证没有尝试创建新的Zulip账号
expect(zulipAccountService.createZulipAccount).not.toHaveBeenCalled();
expect(zulipAccountsRepository.create).not.toHaveBeenCalled();
}),
{ numRuns: 100 }
);
});
it('应该正确处理缺少邮箱或密码的注册请求', async () => {
await fc.assert(
fc.asyncProperty(
fc.record({
username: validUsernameArb,
nickname: validNicknameArb,
email: fc.option(validEmailArb, { nil: undefined }),
password: fc.option(validPasswordArb, { nil: undefined }),
}),
async (registerRequest) => {
// 只测试缺少邮箱或密码的情况
if (registerRequest.email && registerRequest.password) {
return; // 跳过完整数据的情况
}
// 准备测试数据
const mockGameUser: Users = {
id: BigInt(Math.floor(Math.random() * 1000000)),
username: registerRequest.username,
email: registerRequest.email || null,
nickname: registerRequest.nickname,
password_hash: registerRequest.password ? 'hashed_password' : null,
role: 1,
created_at: new Date(),
updated_at: new Date(),
} as Users;
// 设置模拟行为
zulipAccountService.initializeAdminClient.mockResolvedValue(true);
loginCoreService.register.mockResolvedValue({
user: mockGameUser,
isNewUser: true,
});
// 执行注册
const result = await loginService.register(registerRequest as RegisterRequest);
// 验证结果 - 注册应该成功但跳过Zulip账号创建
expect(result.success).toBe(true);
expect(result.data?.user.username).toBe(registerRequest.username);
expect(result.data?.message).toBe('注册成功'); // 不包含Zulip创建信息
// 验证游戏用户被创建
expect(loginCoreService.register).toHaveBeenCalledWith(registerRequest);
// 验证没有尝试创建Zulip账号
expect(zulipAccountService.createZulipAccount).not.toHaveBeenCalled();
expect(zulipAccountsRepository.create).not.toHaveBeenCalled();
}
),
{ numRuns: 50 }
);
});
it('应该正确处理Zulip管理员客户端初始化失败', async () => {
await fc.assert(
fc.asyncProperty(registerRequestArb, async (registerRequest) => {
// 设置模拟行为 - 管理员客户端初始化失败
zulipAccountService.initializeAdminClient.mockResolvedValue(false);
// 执行注册
const result = await loginService.register(registerRequest);
// 验证结果 - 注册应该失败
expect(result.success).toBe(false);
expect(result.message).toContain('Zulip管理员客户端初始化失败');
// 验证没有尝试创建游戏用户
expect(loginCoreService.register).not.toHaveBeenCalled();
// 验证没有尝试创建Zulip账号
expect(zulipAccountService.createZulipAccount).not.toHaveBeenCalled();
}),
{ numRuns: 50 }
);
});
it('应该正确处理环境变量缺失的情况', async () => {
await fc.assert(
fc.asyncProperty(registerRequestArb, async (registerRequest) => {
// 清除环境变量
delete process.env.ZULIP_SERVER_URL;
delete process.env.ZULIP_BOT_EMAIL;
delete process.env.ZULIP_BOT_API_KEY;
// 执行注册
const result = await loginService.register(registerRequest);
// 验证结果 - 注册应该失败
expect(result.success).toBe(false);
expect(result.message).toContain('Zulip管理员配置不完整');
// 验证没有尝试创建游戏用户
expect(loginCoreService.register).not.toHaveBeenCalled();
// 恢复环境变量
process.env.ZULIP_SERVER_URL = 'https://test.zulip.com';
process.env.ZULIP_BOT_EMAIL = 'bot@test.zulip.com';
process.env.ZULIP_BOT_API_KEY = 'test_api_key_123';
}),
{ numRuns: 30 }
);
});
});
/**
* 数据一致性验证测试
*
* 验证游戏账号和Zulip账号之间的数据一致性
*/
describe('数据一致性验证', () => {
it('应该确保游戏账号和Zulip账号使用相同的邮箱和昵称', async () => {
await fc.assert(
fc.asyncProperty(registerRequestArb, async (registerRequest) => {
// 准备测试数据
const mockGameUser: Users = {
id: BigInt(Math.floor(Math.random() * 1000000)),
username: registerRequest.username,
email: registerRequest.email,
nickname: registerRequest.nickname,
password_hash: 'hashed_password',
role: 1,
created_at: new Date(),
updated_at: new Date(),
} as Users;
const mockZulipResult = {
success: true,
userId: Math.floor(Math.random() * 1000000),
email: registerRequest.email,
apiKey: 'zulip_api_key_' + Math.random().toString(36),
};
// 设置模拟行为
zulipAccountService.initializeAdminClient.mockResolvedValue(true);
loginCoreService.register.mockResolvedValue({
user: mockGameUser,
isNewUser: true,
});
zulipAccountsRepository.findByGameUserId.mockResolvedValue(null);
zulipAccountService.createZulipAccount.mockResolvedValue(mockZulipResult);
apiKeySecurityService.storeApiKey.mockResolvedValue(undefined);
zulipAccountsRepository.create.mockResolvedValue({} as ZulipAccounts);
zulipAccountService.linkGameAccount.mockResolvedValue(true);
// 执行注册
await loginService.register(registerRequest);
// 验证Zulip账号创建时使用了正确的数据
expect(zulipAccountService.createZulipAccount).toHaveBeenCalledWith({
email: registerRequest.email, // 相同的邮箱
fullName: registerRequest.nickname, // 相同的昵称
password: registerRequest.password, // 相同的密码
});
// 验证账号关联存储了正确的数据
expect(zulipAccountsRepository.create).toHaveBeenCalledWith(
expect.objectContaining({
gameUserId: mockGameUser.id,
zulipUserId: mockZulipResult.userId,
zulipEmail: registerRequest.email, // 相同的邮箱
zulipFullName: registerRequest.nickname, // 相同的昵称
zulipApiKeyEncrypted: 'stored_in_redis',
status: 'active',
})
);
}),
{ numRuns: 100 }
);
});
});
});

View File

@@ -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初始化完成');
@@ -321,36 +323,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';
try {
// 尝试从Redis获取存储的API Key
const apiKeyResult = await this.apiKeySecurityService.getApiKey(userId);
this.logger.log('Token检查', {
if (apiKeyResult.success && apiKeyResult.apiKey) {
zulipApiKey = apiKeyResult.apiKey;
// TODO: 从数据库获取用户的Zulip邮箱
// 暂时使用模拟数据
zulipEmail = 'angjustinl@163.com';
this.logger.log('从存储获取到Zulip API Key', {
operation: 'validateGameToken',
userId,
tokenPrefix: token.substring(0, 20),
hasUserApiKey,
hasOldApiKey,
isRealUserToken,
});
if (isRealUserToken || hasUserApiKey || hasTestApiKey || hasOldApiKey) {
// 使用用户的真实 API Key
// 注意这个API Key对应的Zulip用户邮箱是 user8@zulip.xinghangee.icu
zulipApiKey = 'lCPWCPfGh7WUHxwN56GF8oYXOpqNfGF8';
zulipEmail = 'angjustinl@mail.angforever.top';
this.logger.log('配置真实Zulip API Key', {
operation: 'validateGameToken',
userId,
zulipEmail,
hasApiKey: true,
zulipEmail,
});
} else {
this.logger.debug('用户没有存储的Zulip API Key', {
operation: 'validateGameToken',
userId,
});
}
} catch (error) {
const err = error as Error;
this.logger.warn('获取Zulip API Key失败', {
operation: 'validateGameToken',
userId,
error: err.message,
});
}
@@ -358,7 +362,6 @@ export class ZulipService {
userId,
username: `Player_${userId.substring(5, 10)}`,
email: `${userId}@example.com`,
// 实际项目中从数据库获取
zulipEmail,
zulipApiKey,
};

View File

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

View File

@@ -0,0 +1,185 @@
/**
* Zulip账号关联实体
*
* 功能描述:
* - 存储游戏用户与Zulip账号的关联关系
* - 管理Zulip账号的基本信息和状态
* - 提供账号验证和同步功能
*
* 关联关系:
* - 与Users表建立一对一关系
* - 存储Zulip用户ID、邮箱、API Key等信息
*
* @author angjustinl
* @version 1.0.0
* @since 2025-01-05
*/
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToOne, JoinColumn, Index } from 'typeorm';
import { Users } from '../users/users.entity';
@Entity('zulip_accounts')
@Index(['zulip_user_id'], { unique: true })
@Index(['zulip_email'], { unique: true })
export class ZulipAccounts {
/**
* 主键ID
*/
@PrimaryGeneratedColumn('increment', { type: 'bigint' })
id: bigint;
/**
* 关联的游戏用户ID
*/
@Column({ type: 'bigint', name: 'game_user_id', comment: '关联的游戏用户ID' })
gameUserId: bigint;
/**
* Zulip用户ID
*/
@Column({ type: 'int', name: 'zulip_user_id', comment: 'Zulip服务器上的用户ID' })
zulipUserId: number;
/**
* Zulip用户邮箱
*/
@Column({ type: 'varchar', length: 255, name: 'zulip_email', comment: 'Zulip账号邮箱地址' })
zulipEmail: string;
/**
* Zulip用户全名
*/
@Column({ type: 'varchar', length: 100, name: 'zulip_full_name', comment: 'Zulip账号全名' })
zulipFullName: string;
/**
* Zulip API Key加密存储
*/
@Column({ type: 'text', name: 'zulip_api_key_encrypted', comment: '加密存储的Zulip API Key' })
zulipApiKeyEncrypted: string;
/**
* 账号状态
* - active: 正常激活状态
* - inactive: 未激活状态
* - suspended: 暂停状态
* - error: 错误状态
*/
@Column({
type: 'enum',
enum: ['active', 'inactive', 'suspended', 'error'],
default: 'active',
comment: '账号状态active-正常inactive-未激活suspended-暂停error-错误'
})
status: 'active' | 'inactive' | 'suspended' | 'error';
/**
* 最后验证时间
*/
@Column({ type: 'timestamp', name: 'last_verified_at', nullable: true, comment: '最后一次验证Zulip账号的时间' })
lastVerifiedAt: Date | null;
/**
* 最后同步时间
*/
@Column({ type: 'timestamp', name: 'last_synced_at', nullable: true, comment: '最后一次同步数据的时间' })
lastSyncedAt: Date | null;
/**
* 错误信息
*/
@Column({ type: 'text', name: 'error_message', nullable: true, comment: '最后一次操作的错误信息' })
errorMessage: string | null;
/**
* 重试次数
*/
@Column({ type: 'int', name: 'retry_count', default: 0, comment: '创建或同步失败的重试次数' })
retryCount: number;
/**
* 创建时间
*/
@CreateDateColumn({ name: 'created_at', comment: '记录创建时间' })
createdAt: Date;
/**
* 更新时间
*/
@UpdateDateColumn({ name: 'updated_at', comment: '记录最后更新时间' })
updatedAt: Date;
/**
* 关联的游戏用户
*/
@OneToOne(() => Users, user => user.zulipAccount)
@JoinColumn({ name: 'game_user_id' })
gameUser: Users;
/**
* 检查账号是否处于正常状态
*
* @returns boolean 是否为正常状态
*/
isActive(): boolean {
return this.status === 'active';
}
/**
* 检查账号是否需要重新验证
*
* @param maxAge 最大验证间隔毫秒默认24小时
* @returns boolean 是否需要重新验证
*/
needsVerification(maxAge: number = 24 * 60 * 60 * 1000): boolean {
if (!this.lastVerifiedAt) {
return true;
}
const now = new Date();
const timeDiff = now.getTime() - this.lastVerifiedAt.getTime();
return timeDiff > maxAge;
}
/**
* 更新验证时间
*/
updateVerificationTime(): void {
this.lastVerifiedAt = new Date();
}
/**
* 更新同步时间
*/
updateSyncTime(): void {
this.lastSyncedAt = new Date();
}
/**
* 设置错误状态
*
* @param errorMessage 错误信息
*/
setError(errorMessage: string): void {
this.status = 'error';
this.errorMessage = errorMessage;
this.retryCount += 1;
}
/**
* 清除错误状态
*/
clearError(): void {
if (this.status === 'error') {
this.status = 'active';
this.errorMessage = null;
}
}
/**
* 重置重试计数
*/
resetRetryCount(): void {
this.retryCount = 0;
}
}

View File

@@ -0,0 +1,81 @@
/**
* Zulip账号关联数据模块
*
* 功能描述:
* - 提供Zulip账号关联数据的访问接口
* - 封装TypeORM实体和Repository
* - 为业务层提供数据访问服务
* - 支持数据库和内存模式的动态切换
*
* @author angjustinl
* @version 1.0.0
* @since 2025-01-05
*/
import { Module, DynamicModule, Global } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ZulipAccounts } from './zulip_accounts.entity';
import { ZulipAccountsRepository } from './zulip_accounts.repository';
import { ZulipAccountsMemoryRepository } from './zulip_accounts_memory.repository';
/**
* 检查数据库配置是否完整
*
* @returns 是否配置了数据库
*/
function isDatabaseConfigured(): boolean {
const requiredEnvVars = ['DB_HOST', 'DB_PORT', 'DB_USERNAME', 'DB_PASSWORD', 'DB_NAME'];
return requiredEnvVars.every(varName => process.env[varName]);
}
@Global()
@Module({})
export class ZulipAccountsModule {
/**
* 创建数据库模式的Zulip账号模块
*
* @returns 配置了TypeORM的动态模块
*/
static forDatabase(): DynamicModule {
return {
module: ZulipAccountsModule,
imports: [TypeOrmModule.forFeature([ZulipAccounts])],
providers: [
{
provide: 'ZulipAccountsRepository',
useClass: ZulipAccountsRepository,
},
],
exports: ['ZulipAccountsRepository', TypeOrmModule],
};
}
/**
* 创建内存模式的Zulip账号模块
*
* @returns 配置了内存存储的动态模块
*/
static forMemory(): DynamicModule {
return {
module: ZulipAccountsModule,
providers: [
{
provide: 'ZulipAccountsRepository',
useClass: ZulipAccountsMemoryRepository,
},
],
exports: ['ZulipAccountsRepository'],
};
}
/**
* 根据环境自动选择模式
*
* @returns 动态模块
*/
static forRoot(): DynamicModule {
return isDatabaseConfigured()
? ZulipAccountsModule.forDatabase()
: ZulipAccountsModule.forMemory();
}
}

View File

@@ -0,0 +1,323 @@
/**
* Zulip账号关联数据访问层
*
* 功能描述:
* - 提供Zulip账号关联数据的CRUD操作
* - 封装复杂查询逻辑和数据库交互
* - 实现数据访问层的业务逻辑抽象
*
* 主要功能:
* - 账号关联的创建、查询、更新、删除
* - 支持按游戏用户ID、Zulip用户ID、邮箱查询
* - 提供账号状态管理和批量操作
*
* @author angjustinl
* @version 1.0.0
* @since 2025-01-05
*/
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, FindOptionsWhere } from 'typeorm';
import { ZulipAccounts } from './zulip_accounts.entity';
/**
* 创建Zulip账号关联的数据传输对象
*/
export interface CreateZulipAccountDto {
gameUserId: bigint;
zulipUserId: number;
zulipEmail: string;
zulipFullName: string;
zulipApiKeyEncrypted: string;
status?: 'active' | 'inactive' | 'suspended' | 'error';
}
/**
* 更新Zulip账号关联的数据传输对象
*/
export interface UpdateZulipAccountDto {
zulipFullName?: string;
zulipApiKeyEncrypted?: string;
status?: 'active' | 'inactive' | 'suspended' | 'error';
lastVerifiedAt?: Date;
lastSyncedAt?: Date;
errorMessage?: string;
retryCount?: number;
}
/**
* Zulip账号查询条件
*/
export interface ZulipAccountQueryOptions {
gameUserId?: bigint;
zulipUserId?: number;
zulipEmail?: string;
status?: 'active' | 'inactive' | 'suspended' | 'error';
includeGameUser?: boolean;
}
@Injectable()
export class ZulipAccountsRepository {
constructor(
@InjectRepository(ZulipAccounts)
private readonly repository: Repository<ZulipAccounts>,
) {}
/**
* 创建新的Zulip账号关联
*
* @param createDto 创建数据
* @returns Promise<ZulipAccounts> 创建的关联记录
*/
async create(createDto: CreateZulipAccountDto): Promise<ZulipAccounts> {
const zulipAccount = this.repository.create(createDto);
return await this.repository.save(zulipAccount);
}
/**
* 根据游戏用户ID查找Zulip账号关联
*
* @param gameUserId 游戏用户ID
* @param includeGameUser 是否包含游戏用户信息
* @returns Promise<ZulipAccounts | null> 关联记录或null
*/
async findByGameUserId(gameUserId: bigint, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
const relations = includeGameUser ? ['gameUser'] : [];
return await this.repository.findOne({
where: { gameUserId },
relations,
});
}
/**
* 根据Zulip用户ID查找账号关联
*
* @param zulipUserId Zulip用户ID
* @param includeGameUser 是否包含游戏用户信息
* @returns Promise<ZulipAccounts | null> 关联记录或null
*/
async findByZulipUserId(zulipUserId: number, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
const relations = includeGameUser ? ['gameUser'] : [];
return await this.repository.findOne({
where: { zulipUserId },
relations,
});
}
/**
* 根据Zulip邮箱查找账号关联
*
* @param zulipEmail Zulip邮箱
* @param includeGameUser 是否包含游戏用户信息
* @returns Promise<ZulipAccounts | null> 关联记录或null
*/
async findByZulipEmail(zulipEmail: string, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
const relations = includeGameUser ? ['gameUser'] : [];
return await this.repository.findOne({
where: { zulipEmail },
relations,
});
}
/**
* 根据ID查找Zulip账号关联
*
* @param id 关联记录ID
* @param includeGameUser 是否包含游戏用户信息
* @returns Promise<ZulipAccounts | null> 关联记录或null
*/
async findById(id: bigint, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
const relations = includeGameUser ? ['gameUser'] : [];
return await this.repository.findOne({
where: { id },
relations,
});
}
/**
* 更新Zulip账号关联
*
* @param id 关联记录ID
* @param updateDto 更新数据
* @returns Promise<ZulipAccounts | null> 更新后的记录或null
*/
async update(id: bigint, updateDto: UpdateZulipAccountDto): Promise<ZulipAccounts | null> {
await this.repository.update({ id }, updateDto);
return await this.findById(id);
}
/**
* 根据游戏用户ID更新Zulip账号关联
*
* @param gameUserId 游戏用户ID
* @param updateDto 更新数据
* @returns Promise<ZulipAccounts | null> 更新后的记录或null
*/
async updateByGameUserId(gameUserId: bigint, updateDto: UpdateZulipAccountDto): Promise<ZulipAccounts | null> {
await this.repository.update({ gameUserId }, updateDto);
return await this.findByGameUserId(gameUserId);
}
/**
* 删除Zulip账号关联
*
* @param id 关联记录ID
* @returns Promise<boolean> 是否删除成功
*/
async delete(id: bigint): Promise<boolean> {
const result = await this.repository.delete({ id });
return result.affected > 0;
}
/**
* 根据游戏用户ID删除Zulip账号关联
*
* @param gameUserId 游戏用户ID
* @returns Promise<boolean> 是否删除成功
*/
async deleteByGameUserId(gameUserId: bigint): Promise<boolean> {
const result = await this.repository.delete({ gameUserId });
return result.affected > 0;
}
/**
* 查询多个Zulip账号关联
*
* @param options 查询选项
* @returns Promise<ZulipAccounts[]> 关联记录列表
*/
async findMany(options: ZulipAccountQueryOptions = {}): Promise<ZulipAccounts[]> {
const { includeGameUser, ...whereOptions } = options;
const relations = includeGameUser ? ['gameUser'] : [];
// 构建查询条件
const where: FindOptionsWhere<ZulipAccounts> = {};
if (whereOptions.gameUserId) where.gameUserId = whereOptions.gameUserId;
if (whereOptions.zulipUserId) where.zulipUserId = whereOptions.zulipUserId;
if (whereOptions.zulipEmail) where.zulipEmail = whereOptions.zulipEmail;
if (whereOptions.status) where.status = whereOptions.status;
return await this.repository.find({
where,
relations,
order: { createdAt: 'DESC' },
});
}
/**
* 获取需要验证的账号列表
*
* @param maxAge 最大验证间隔毫秒默认24小时
* @returns Promise<ZulipAccounts[]> 需要验证的账号列表
*/
async findAccountsNeedingVerification(maxAge: number = 24 * 60 * 60 * 1000): Promise<ZulipAccounts[]> {
const cutoffTime = new Date(Date.now() - maxAge);
return await this.repository
.createQueryBuilder('zulip_accounts')
.where('zulip_accounts.status = :status', { status: 'active' })
.andWhere(
'(zulip_accounts.last_verified_at IS NULL OR zulip_accounts.last_verified_at < :cutoffTime)',
{ cutoffTime }
)
.orderBy('zulip_accounts.last_verified_at', 'ASC', 'NULLS FIRST')
.getMany();
}
/**
* 获取错误状态的账号列表
*
* @param maxRetryCount 最大重试次数默认3次
* @returns Promise<ZulipAccounts[]> 错误状态的账号列表
*/
async findErrorAccounts(maxRetryCount: number = 3): Promise<ZulipAccounts[]> {
return await this.repository.find({
where: { status: 'error' },
order: { updatedAt: 'ASC' },
});
}
/**
* 批量更新账号状态
*
* @param ids 账号ID列表
* @param status 新状态
* @returns Promise<number> 更新的记录数
*/
async batchUpdateStatus(ids: bigint[], status: 'active' | 'inactive' | 'suspended' | 'error'): Promise<number> {
const result = await this.repository
.createQueryBuilder()
.update(ZulipAccounts)
.set({ status })
.whereInIds(ids)
.execute();
return result.affected || 0;
}
/**
* 统计各状态的账号数量
*
* @returns Promise<Record<string, number>> 状态统计
*/
async getStatusStatistics(): Promise<Record<string, number>> {
const result = await this.repository
.createQueryBuilder('zulip_accounts')
.select('zulip_accounts.status', 'status')
.addSelect('COUNT(*)', 'count')
.groupBy('zulip_accounts.status')
.getRawMany();
const statistics: Record<string, number> = {};
result.forEach(row => {
statistics[row.status] = parseInt(row.count, 10);
});
return statistics;
}
/**
* 检查邮箱是否已存在
*
* @param zulipEmail Zulip邮箱
* @param excludeId 排除的记录ID用于更新时检查
* @returns Promise<boolean> 是否已存在
*/
async existsByEmail(zulipEmail: string, excludeId?: bigint): Promise<boolean> {
const queryBuilder = this.repository
.createQueryBuilder('zulip_accounts')
.where('zulip_accounts.zulip_email = :zulipEmail', { zulipEmail });
if (excludeId) {
queryBuilder.andWhere('zulip_accounts.id != :excludeId', { excludeId });
}
const count = await queryBuilder.getCount();
return count > 0;
}
/**
* 检查Zulip用户ID是否已存在
*
* @param zulipUserId Zulip用户ID
* @param excludeId 排除的记录ID用于更新时检查
* @returns Promise<boolean> 是否已存在
*/
async existsByZulipUserId(zulipUserId: number, excludeId?: bigint): Promise<boolean> {
const queryBuilder = this.repository
.createQueryBuilder('zulip_accounts')
.where('zulip_accounts.zulip_user_id = :zulipUserId', { zulipUserId });
if (excludeId) {
queryBuilder.andWhere('zulip_accounts.id != :excludeId', { excludeId });
}
const count = await queryBuilder.getCount();
return count > 0;
}
}

View File

@@ -0,0 +1,299 @@
/**
* Zulip账号关联内存数据访问层
*
* 功能描述:
* - 提供Zulip账号关联数据的内存存储实现
* - 用于开发和测试环境
* - 实现与数据库版本相同的接口
*
* @author angjustinl
* @version 1.0.0
* @since 2025-01-05
*/
import { Injectable } from '@nestjs/common';
import { ZulipAccounts } from './zulip_accounts.entity';
import {
CreateZulipAccountDto,
UpdateZulipAccountDto,
ZulipAccountQueryOptions,
} from './zulip_accounts.repository';
@Injectable()
export class ZulipAccountsMemoryRepository {
private accounts: Map<bigint, ZulipAccounts> = new Map();
private currentId: bigint = BigInt(1);
/**
* 创建新的Zulip账号关联
*
* @param createDto 创建数据
* @returns Promise<ZulipAccounts> 创建的关联记录
*/
async create(createDto: CreateZulipAccountDto): Promise<ZulipAccounts> {
const account = new ZulipAccounts();
account.id = this.currentId++;
account.gameUserId = createDto.gameUserId;
account.zulipUserId = createDto.zulipUserId;
account.zulipEmail = createDto.zulipEmail;
account.zulipFullName = createDto.zulipFullName;
account.zulipApiKeyEncrypted = createDto.zulipApiKeyEncrypted;
account.status = createDto.status || 'active';
account.createdAt = new Date();
account.updatedAt = new Date();
this.accounts.set(account.id, account);
return account;
}
/**
* 根据游戏用户ID查找Zulip账号关联
*
* @param gameUserId 游戏用户ID
* @param includeGameUser 是否包含游戏用户信息(内存模式忽略)
* @returns Promise<ZulipAccounts | null> 关联记录或null
*/
async findByGameUserId(gameUserId: bigint, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
for (const account of this.accounts.values()) {
if (account.gameUserId === gameUserId) {
return account;
}
}
return null;
}
/**
* 根据Zulip用户ID查找账号关联
*
* @param zulipUserId Zulip用户ID
* @param includeGameUser 是否包含游戏用户信息(内存模式忽略)
* @returns Promise<ZulipAccounts | null> 关联记录或null
*/
async findByZulipUserId(zulipUserId: number, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
for (const account of this.accounts.values()) {
if (account.zulipUserId === zulipUserId) {
return account;
}
}
return null;
}
/**
* 根据Zulip邮箱查找账号关联
*
* @param zulipEmail Zulip邮箱
* @param includeGameUser 是否包含游戏用户信息(内存模式忽略)
* @returns Promise<ZulipAccounts | null> 关联记录或null
*/
async findByZulipEmail(zulipEmail: string, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
for (const account of this.accounts.values()) {
if (account.zulipEmail === zulipEmail) {
return account;
}
}
return null;
}
/**
* 根据ID查找Zulip账号关联
*
* @param id 关联记录ID
* @param includeGameUser 是否包含游戏用户信息(内存模式忽略)
* @returns Promise<ZulipAccounts | null> 关联记录或null
*/
async findById(id: bigint, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
return this.accounts.get(id) || null;
}
/**
* 更新Zulip账号关联
*
* @param id 关联记录ID
* @param updateDto 更新数据
* @returns Promise<ZulipAccounts | null> 更新后的记录或null
*/
async update(id: bigint, updateDto: UpdateZulipAccountDto): Promise<ZulipAccounts | null> {
const account = this.accounts.get(id);
if (!account) {
return null;
}
Object.assign(account, updateDto);
account.updatedAt = new Date();
return account;
}
/**
* 根据游戏用户ID更新Zulip账号关联
*
* @param gameUserId 游戏用户ID
* @param updateDto 更新数据
* @returns Promise<ZulipAccounts | null> 更新后的记录或null
*/
async updateByGameUserId(gameUserId: bigint, updateDto: UpdateZulipAccountDto): Promise<ZulipAccounts | null> {
const account = await this.findByGameUserId(gameUserId);
if (!account) {
return null;
}
Object.assign(account, updateDto);
account.updatedAt = new Date();
return account;
}
/**
* 删除Zulip账号关联
*
* @param id 关联记录ID
* @returns Promise<boolean> 是否删除成功
*/
async delete(id: bigint): Promise<boolean> {
return this.accounts.delete(id);
}
/**
* 根据游戏用户ID删除Zulip账号关联
*
* @param gameUserId 游戏用户ID
* @returns Promise<boolean> 是否删除成功
*/
async deleteByGameUserId(gameUserId: bigint): Promise<boolean> {
for (const [id, account] of this.accounts.entries()) {
if (account.gameUserId === gameUserId) {
return this.accounts.delete(id);
}
}
return false;
}
/**
* 查询多个Zulip账号关联
*
* @param options 查询选项
* @returns Promise<ZulipAccounts[]> 关联记录列表
*/
async findMany(options: ZulipAccountQueryOptions = {}): Promise<ZulipAccounts[]> {
let results = Array.from(this.accounts.values());
if (options.gameUserId) {
results = results.filter(a => a.gameUserId === options.gameUserId);
}
if (options.zulipUserId) {
results = results.filter(a => a.zulipUserId === options.zulipUserId);
}
if (options.zulipEmail) {
results = results.filter(a => a.zulipEmail === options.zulipEmail);
}
if (options.status) {
results = results.filter(a => a.status === options.status);
}
// 按创建时间降序排序
results.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
return results;
}
/**
* 获取需要验证的账号列表
*
* @param maxAge 最大验证间隔毫秒默认24小时
* @returns Promise<ZulipAccounts[]> 需要验证的账号列表
*/
async findAccountsNeedingVerification(maxAge: number = 24 * 60 * 60 * 1000): Promise<ZulipAccounts[]> {
const cutoffTime = new Date(Date.now() - maxAge);
return Array.from(this.accounts.values())
.filter(account =>
account.status === 'active' &&
(!account.lastVerifiedAt || account.lastVerifiedAt < cutoffTime)
)
.sort((a, b) => {
if (!a.lastVerifiedAt) return -1;
if (!b.lastVerifiedAt) return 1;
return a.lastVerifiedAt.getTime() - b.lastVerifiedAt.getTime();
});
}
/**
* 获取错误状态的账号列表
*
* @param maxRetryCount 最大重试次数(内存模式忽略)
* @returns Promise<ZulipAccounts[]> 错误状态的账号列表
*/
async findErrorAccounts(maxRetryCount: number = 3): Promise<ZulipAccounts[]> {
return Array.from(this.accounts.values())
.filter(account => account.status === 'error')
.sort((a, b) => a.updatedAt.getTime() - b.updatedAt.getTime());
}
/**
* 批量更新账号状态
*
* @param ids 账号ID列表
* @param status 新状态
* @returns Promise<number> 更新的记录数
*/
async batchUpdateStatus(ids: bigint[], status: 'active' | 'inactive' | 'suspended' | 'error'): Promise<number> {
let count = 0;
for (const id of ids) {
const account = this.accounts.get(id);
if (account) {
account.status = status;
account.updatedAt = new Date();
count++;
}
}
return count;
}
/**
* 统计各状态的账号数量
*
* @returns Promise<Record<string, number>> 状态统计
*/
async getStatusStatistics(): Promise<Record<string, number>> {
const statistics: Record<string, number> = {};
for (const account of this.accounts.values()) {
const status = account.status;
statistics[status] = (statistics[status] || 0) + 1;
}
return statistics;
}
/**
* 检查邮箱是否已存在
*
* @param zulipEmail Zulip邮箱
* @param excludeId 排除的记录ID用于更新时检查
* @returns Promise<boolean> 是否已存在
*/
async existsByEmail(zulipEmail: string, excludeId?: bigint): Promise<boolean> {
for (const [id, account] of this.accounts.entries()) {
if (account.zulipEmail === zulipEmail && (!excludeId || id !== excludeId)) {
return true;
}
}
return false;
}
/**
* 检查Zulip用户ID是否已存在
*
* @param zulipUserId Zulip用户ID
* @param excludeId 排除的记录ID用于更新时检查
* @returns Promise<boolean> 是否已存在
*/
async existsByZulipUserId(zulipUserId: number, excludeId?: bigint): Promise<boolean> {
for (const [id, account] of this.accounts.entries()) {
if (account.zulipUserId === zulipUserId && (!excludeId || id !== excludeId)) {
return true;
}
}
return false;
}
}

View File

@@ -826,4 +826,36 @@ export class LoginCoreService {
VerificationCodeType.EMAIL_VERIFICATION
);
}
/**
* 删除用户
*
* 功能描述:
* 删除指定的用户记录,用于注册失败时的回滚操作
*
* 业务逻辑:
* 1. 验证用户是否存在
* 2. 执行用户删除操作
* 3. 返回删除结果
*
* @param userId 用户ID
* @returns Promise<boolean> 是否删除成功
* @throws NotFoundException 用户不存在时
*/
async deleteUser(userId: bigint): Promise<boolean> {
// 1. 验证用户是否存在
const user = await this.usersService.findOne(userId);
if (!user) {
throw new NotFoundException('用户不存在');
}
// 2. 执行删除操作
try {
await this.usersService.remove(userId);
return true;
} catch (error) {
console.error(`删除用户失败: ${userId}`, error);
return false;
}
}
}

View File

@@ -0,0 +1,708 @@
/**
* Zulip账号管理核心服务
*
* 功能描述:
* - 自动创建Zulip用户账号
* - 生成API Key并安全存储
* - 处理账号创建失败场景
* - 管理用户账号与游戏账号的关联
*
* 主要方法:
* - createZulipAccount(): 创建新的Zulip用户账号
* - generateApiKey(): 为用户生成API Key
* - validateZulipAccount(): 验证Zulip账号有效性
* - linkGameAccount(): 关联游戏账号与Zulip账号
*
* 使用场景:
* - 用户注册时自动创建Zulip账号
* - API Key管理和更新
* - 账号关联和映射存储
*
* @author angjustinl
* @version 1.0.0
* @since 2025-01-05
*/
import { Injectable, Logger } from '@nestjs/common';
import { ZulipClientConfig } from '../interfaces/zulip-core.interfaces';
/**
* Zulip账号创建请求接口
*/
export interface CreateZulipAccountRequest {
email: string;
fullName: string;
password?: string;
shortName?: string;
}
/**
* Zulip账号创建结果接口
*/
export interface CreateZulipAccountResult {
success: boolean;
userId?: number;
email?: string;
apiKey?: string;
error?: string;
errorCode?: string;
}
/**
* API Key生成结果接口
*/
export interface GenerateApiKeyResult {
success: boolean;
apiKey?: string;
error?: string;
}
/**
* 账号验证结果接口
*/
export interface ValidateAccountResult {
success: boolean;
isValid?: boolean;
userInfo?: any;
error?: string;
}
/**
* 账号关联信息接口
*/
export interface AccountLinkInfo {
gameUserId: string;
zulipUserId: number;
zulipEmail: string;
zulipApiKey: string;
createdAt: Date;
lastVerified?: Date;
isActive: boolean;
}
/**
* Zulip账号管理服务类
*
* 职责:
* - 处理Zulip用户账号的创建和管理
* - 管理API Key的生成和存储
* - 维护游戏账号与Zulip账号的关联关系
* - 提供账号验证和状态检查功能
*
* 主要方法:
* - createZulipAccount(): 创建新的Zulip用户账号
* - generateApiKey(): 为现有用户生成API Key
* - validateZulipAccount(): 验证Zulip账号有效性
* - linkGameAccount(): 建立游戏账号与Zulip账号的关联
* - unlinkGameAccount(): 解除账号关联
*
* 使用场景:
* - 用户注册流程中自动创建Zulip账号
* - API Key管理和更新
* - 账号状态监控和维护
* - 跨平台账号同步
*/
@Injectable()
export class ZulipAccountService {
private readonly logger = new Logger(ZulipAccountService.name);
private adminClient: any = null;
private readonly accountLinks = new Map<string, AccountLinkInfo>();
constructor() {
this.logger.log('ZulipAccountService初始化完成');
}
/**
* 初始化管理员客户端
*
* 功能描述:
* 使用管理员凭证初始化Zulip客户端用于创建用户账号
*
* @param adminConfig 管理员配置
* @returns Promise<boolean> 是否初始化成功
*/
async initializeAdminClient(adminConfig: ZulipClientConfig): Promise<boolean> {
this.logger.log('初始化Zulip管理员客户端', {
operation: 'initializeAdminClient',
realm: adminConfig.realm,
timestamp: new Date().toISOString(),
});
try {
// 动态导入zulip-js
const zulipInit = await this.loadZulipModule();
// 创建管理员客户端
this.adminClient = await zulipInit({
username: adminConfig.username,
apiKey: adminConfig.apiKey,
realm: adminConfig.realm,
});
// 验证管理员权限
const profile = await this.adminClient.users.me.getProfile();
if (profile.result !== 'success') {
throw new Error(`管理员客户端验证失败: ${profile.msg || '未知错误'}`);
}
this.logger.log('管理员客户端初始化成功', {
operation: 'initializeAdminClient',
adminEmail: profile.email,
isAdmin: profile.is_admin,
timestamp: new Date().toISOString(),
});
return true;
} catch (error) {
const err = error as Error;
this.logger.error('管理员客户端初始化失败', {
operation: 'initializeAdminClient',
error: err.message,
timestamp: new Date().toISOString(),
}, err.stack);
return false;
}
}
/**
* 创建Zulip用户账号
*
* 功能描述:
* 使用管理员权限在Zulip服务器上创建新的用户账号
*
* 业务逻辑:
* 1. 验证管理员客户端是否已初始化
* 2. 检查邮箱是否已存在
* 3. 生成用户密码(如果未提供)
* 4. 调用Zulip API创建用户
* 5. 为新用户生成API Key
* 6. 返回创建结果
*
* @param request 账号创建请求
* @returns Promise<CreateZulipAccountResult> 创建结果
*/
async createZulipAccount(request: CreateZulipAccountRequest): Promise<CreateZulipAccountResult> {
const startTime = Date.now();
this.logger.log('开始创建Zulip账号', {
operation: 'createZulipAccount',
email: request.email,
fullName: request.fullName,
timestamp: new Date().toISOString(),
});
try {
// 1. 验证管理员客户端
if (!this.adminClient) {
throw new Error('管理员客户端未初始化');
}
// 2. 验证请求参数
if (!request.email || !request.email.trim()) {
throw new Error('邮箱地址不能为空');
}
if (!request.fullName || !request.fullName.trim()) {
throw new Error('用户全名不能为空');
}
// 3. 检查邮箱格式
if (!this.isValidEmail(request.email)) {
throw new Error('邮箱格式无效');
}
// 4. 检查用户是否已存在
const existingUser = await this.checkUserExists(request.email);
if (existingUser) {
this.logger.warn('用户已存在', {
operation: 'createZulipAccount',
email: request.email,
});
return {
success: false,
error: '用户已存在',
errorCode: 'USER_ALREADY_EXISTS',
};
}
// 5. 生成密码(如果未提供)
const password = request.password || this.generateRandomPassword();
const shortName = request.shortName || this.generateShortName(request.email);
// 6. 创建用户参数
const createParams = {
email: request.email,
password: password,
full_name: request.fullName,
short_name: shortName,
};
// 7. 调用Zulip API创建用户
const createResponse = await this.adminClient.users.create(createParams);
if (createResponse.result !== 'success') {
this.logger.warn('Zulip用户创建失败', {
operation: 'createZulipAccount',
email: request.email,
error: createResponse.msg,
});
return {
success: false,
error: createResponse.msg || '用户创建失败',
errorCode: 'ZULIP_CREATE_FAILED',
};
}
// 8. 为新用户生成API Key
const apiKeyResult = await this.generateApiKeyForUser(request.email, password);
if (!apiKeyResult.success) {
this.logger.warn('API Key生成失败但用户已创建', {
operation: 'createZulipAccount',
email: request.email,
error: apiKeyResult.error,
});
// 用户已创建但API Key生成失败
return {
success: true,
userId: createResponse.user_id,
email: request.email,
error: `用户创建成功但API Key生成失败: ${apiKeyResult.error}`,
errorCode: 'API_KEY_GENERATION_FAILED',
};
}
const duration = Date.now() - startTime;
this.logger.log('Zulip账号创建成功', {
operation: 'createZulipAccount',
email: request.email,
userId: createResponse.user_id,
hasApiKey: !!apiKeyResult.apiKey,
duration,
timestamp: new Date().toISOString(),
});
return {
success: true,
userId: createResponse.user_id,
email: request.email,
apiKey: apiKeyResult.apiKey,
};
} catch (error) {
const err = error as Error;
const duration = Date.now() - startTime;
this.logger.error('创建Zulip账号失败', {
operation: 'createZulipAccount',
email: request.email,
error: err.message,
duration,
timestamp: new Date().toISOString(),
}, err.stack);
return {
success: false,
error: err.message,
errorCode: 'ACCOUNT_CREATION_FAILED',
};
}
}
/**
* 为用户生成API Key
*
* 功能描述:
* 使用用户凭证获取API Key
*
* @param email 用户邮箱
* @param password 用户密码
* @returns Promise<GenerateApiKeyResult> 生成结果
*/
async generateApiKeyForUser(email: string, password: string): Promise<GenerateApiKeyResult> {
this.logger.log('为用户生成API Key', {
operation: 'generateApiKeyForUser',
email,
timestamp: new Date().toISOString(),
});
try {
// 动态导入zulip-js
const zulipInit = await this.loadZulipModule();
// 使用用户凭证获取API Key
const userClient = await zulipInit({
username: email,
password: password,
realm: this.getRealmFromAdminClient(),
});
// 验证客户端并获取API Key
const profile = await userClient.users.me.getProfile();
if (profile.result !== 'success') {
throw new Error(`API Key获取失败: ${profile.msg || '未知错误'}`);
}
// 从客户端配置中提取API Key
const apiKey = userClient.config?.apiKey;
if (!apiKey) {
throw new Error('无法从客户端配置中获取API Key');
}
this.logger.log('API Key生成成功', {
operation: 'generateApiKeyForUser',
email,
timestamp: new Date().toISOString(),
});
return {
success: true,
apiKey: apiKey,
};
} catch (error) {
const err = error as Error;
this.logger.error('API Key生成失败', {
operation: 'generateApiKeyForUser',
email,
error: err.message,
timestamp: new Date().toISOString(),
}, err.stack);
return {
success: false,
error: err.message,
};
}
}
/**
* 验证Zulip账号有效性
*
* 功能描述:
* 验证指定的Zulip账号是否存在且有效
*
* @param email 用户邮箱
* @param apiKey 用户API Key可选
* @returns Promise<ValidateAccountResult> 验证结果
*/
async validateZulipAccount(email: string, apiKey?: string): Promise<ValidateAccountResult> {
this.logger.log('验证Zulip账号', {
operation: 'validateZulipAccount',
email,
hasApiKey: !!apiKey,
timestamp: new Date().toISOString(),
});
try {
if (apiKey) {
// 使用API Key验证
const zulipInit = await this.loadZulipModule();
const userClient = await zulipInit({
username: email,
apiKey: apiKey,
realm: this.getRealmFromAdminClient(),
});
const profile = await userClient.users.me.getProfile();
if (profile.result === 'success') {
this.logger.log('账号验证成功API Key', {
operation: 'validateZulipAccount',
email,
userId: profile.user_id,
});
return {
success: true,
isValid: true,
userInfo: profile,
};
} else {
return {
success: true,
isValid: false,
error: profile.msg || 'API Key验证失败',
};
}
} else {
// 仅检查用户是否存在
const userExists = await this.checkUserExists(email);
this.logger.log('账号存在性检查完成', {
operation: 'validateZulipAccount',
email,
exists: userExists,
});
return {
success: true,
isValid: userExists,
};
}
} catch (error) {
const err = error as Error;
this.logger.error('账号验证失败', {
operation: 'validateZulipAccount',
email,
error: err.message,
timestamp: new Date().toISOString(),
}, err.stack);
return {
success: false,
error: err.message,
};
}
}
/**
* 关联游戏账号与Zulip账号
*
* 功能描述:
* 建立游戏用户ID与Zulip账号的映射关系
*
* @param gameUserId 游戏用户ID
* @param zulipUserId Zulip用户ID
* @param zulipEmail Zulip邮箱
* @param zulipApiKey Zulip API Key
* @returns Promise<boolean> 是否关联成功
*/
async linkGameAccount(
gameUserId: string,
zulipUserId: number,
zulipEmail: string,
zulipApiKey: string,
): Promise<boolean> {
this.logger.log('关联游戏账号与Zulip账号', {
operation: 'linkGameAccount',
gameUserId,
zulipUserId,
zulipEmail,
timestamp: new Date().toISOString(),
});
try {
// 验证参数
if (!gameUserId || !zulipUserId || !zulipEmail || !zulipApiKey) {
throw new Error('关联参数不完整');
}
// 创建关联信息
const linkInfo: AccountLinkInfo = {
gameUserId,
zulipUserId,
zulipEmail,
zulipApiKey,
createdAt: new Date(),
isActive: true,
};
// 存储关联信息(实际项目中应存储到数据库)
this.accountLinks.set(gameUserId, linkInfo);
this.logger.log('账号关联成功', {
operation: 'linkGameAccount',
gameUserId,
zulipUserId,
zulipEmail,
timestamp: new Date().toISOString(),
});
return true;
} catch (error) {
const err = error as Error;
this.logger.error('账号关联失败', {
operation: 'linkGameAccount',
gameUserId,
zulipUserId,
error: err.message,
timestamp: new Date().toISOString(),
}, err.stack);
return false;
}
}
/**
* 解除游戏账号与Zulip账号的关联
*
* @param gameUserId 游戏用户ID
* @returns Promise<boolean> 是否解除成功
*/
async unlinkGameAccount(gameUserId: string): Promise<boolean> {
this.logger.log('解除账号关联', {
operation: 'unlinkGameAccount',
gameUserId,
timestamp: new Date().toISOString(),
});
try {
const linkInfo = this.accountLinks.get(gameUserId);
if (linkInfo) {
linkInfo.isActive = false;
this.accountLinks.delete(gameUserId);
this.logger.log('账号关联解除成功', {
operation: 'unlinkGameAccount',
gameUserId,
zulipEmail: linkInfo.zulipEmail,
});
}
return true;
} catch (error) {
const err = error as Error;
this.logger.error('解除账号关联失败', {
operation: 'unlinkGameAccount',
gameUserId,
error: err.message,
timestamp: new Date().toISOString(),
}, err.stack);
return false;
}
}
/**
* 获取游戏账号的Zulip关联信息
*
* @param gameUserId 游戏用户ID
* @returns AccountLinkInfo | null 关联信息
*/
getAccountLink(gameUserId: string): AccountLinkInfo | null {
return this.accountLinks.get(gameUserId) || null;
}
/**
* 获取所有账号关联信息
*
* @returns AccountLinkInfo[] 所有关联信息
*/
getAllAccountLinks(): AccountLinkInfo[] {
return Array.from(this.accountLinks.values()).filter(link => link.isActive);
}
/**
* 检查用户是否已存在
*
* @param email 用户邮箱
* @returns Promise<boolean> 用户是否存在
* @private
*/
private async checkUserExists(email: string): Promise<boolean> {
try {
if (!this.adminClient) {
return false;
}
// 获取所有用户列表
const usersResponse = await this.adminClient.users.retrieve();
if (usersResponse.result === 'success') {
const users = usersResponse.members || [];
return users.some((user: any) => user.email === email);
}
return false;
} catch (error) {
const err = error as Error;
this.logger.warn('检查用户存在性失败', {
operation: 'checkUserExists',
email,
error: err.message,
});
return false;
}
}
/**
* 验证邮箱格式
*
* @param email 邮箱地址
* @returns boolean 是否为有效邮箱
* @private
*/
private isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
/**
* 生成随机密码
*
* @returns string 随机密码
* @private
*/
private generateRandomPassword(): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';
let password = '';
for (let i = 0; i < 12; i++) {
password += chars.charAt(Math.floor(Math.random() * chars.length));
}
return password;
}
/**
* 从邮箱生成短名称
*
* @param email 邮箱地址
* @returns string 短名称
* @private
*/
private generateShortName(email: string): string {
const localPart = email.split('@')[0];
// 移除特殊字符,只保留字母数字和下划线
return localPart.replace(/[^a-zA-Z0-9_]/g, '').toLowerCase();
}
/**
* 从管理员客户端获取Realm
*
* @returns string Realm URL
* @private
*/
private getRealmFromAdminClient(): string {
if (!this.adminClient || !this.adminClient.config) {
throw new Error('管理员客户端未初始化或配置缺失');
}
return this.adminClient.config.realm;
}
/**
* 动态加载zulip-js模块
*
* @returns Promise<any> zulip-js初始化函数
* @private
*/
private async loadZulipModule(): Promise<any> {
try {
// 使用动态导入加载zulip-js
const zulipModule = await import('zulip-js');
return zulipModule.default || zulipModule;
} catch (error) {
const err = error as Error;
this.logger.error('加载zulip-js模块失败', {
operation: 'loadZulipModule',
error: err.message,
timestamp: new Date().toISOString(),
}, err.stack);
throw new Error(`加载zulip-js模块失败: ${err.message}`);
}
}
}

View File

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