From ea97167a3218392aaaffdb76b4c6a55647b7d1ff Mon Sep 17 00:00:00 2001 From: moyin <244344649@qq.com> Date: Mon, 12 Jan 2026 19:39:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(zulip):=20=E6=B7=BB=E5=8A=A0=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E9=85=8D=E7=BD=AE=E6=8E=A7=E5=88=B6=E5=99=A8=E5=92=8C?= =?UTF-8?q?=E8=B4=A6=E6=88=B7=E4=B8=9A=E5=8A=A1=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 范围:src/business/zulip/ - 添加dynamic_config.controller.ts动态配置管理控制器 - 添加services/zulip_accounts_business.service.ts账户业务服务 - 完善zulip业务模块功能架构 --- .../zulip/dynamic_config.controller.ts | 586 ++++++++++++++++++ .../zulip_accounts_business.service.ts | 521 ++++++++++++++++ 2 files changed, 1107 insertions(+) create mode 100644 src/business/zulip/dynamic_config.controller.ts create mode 100644 src/business/zulip/services/zulip_accounts_business.service.ts diff --git a/src/business/zulip/dynamic_config.controller.ts b/src/business/zulip/dynamic_config.controller.ts new file mode 100644 index 0000000..bee23d0 --- /dev/null +++ b/src/business/zulip/dynamic_config.controller.ts @@ -0,0 +1,586 @@ +/** + * 统一配置管理控制器 + * + * 功能描述: + * - 提供统一配置管理的REST API接口 + * - 支持配置查询、同步、状态检查 + * - 提供备份管理功能 + * + * @author assistant + * @version 2.0.0 + * @since 2026-01-12 + */ + +import { + Controller, + Get, + Post, + Query, + HttpStatus, + HttpException, + Logger, +} from '@nestjs/common'; +import { + ApiTags, + ApiOperation, + ApiResponse, + ApiQuery, +} from '@nestjs/swagger'; +import { DynamicConfigManagerService } from '../../core/zulip_core/services/dynamic_config_manager.service'; + +@ApiTags('unified-config') +@Controller('api/zulip/config') +export class DynamicConfigController { + private readonly logger = new Logger(DynamicConfigController.name); + + constructor( + private readonly configManager: DynamicConfigManagerService, + ) {} + + /** + * 获取当前配置 + */ + @Get() + @ApiOperation({ + summary: '获取当前配置', + description: '获取当前的统一配置(自动从本地加载,如需最新数据请先调用同步接口)' + }) + @ApiResponse({ + status: 200, + description: '配置获取成功', + schema: { + type: 'object', + properties: { + success: { type: 'boolean' }, + data: { type: 'object' }, + source: { type: 'string', enum: ['remote', 'local', 'default'] }, + timestamp: { type: 'string' } + } + } + }) + async getCurrentConfig() { + try { + this.logger.log('获取当前配置'); + + const config = await this.configManager.getConfig(); + const status = this.configManager.getConfigStatus(); + + return { + success: true, + data: config, + source: config.source || 'unknown', + lastSyncTime: status.lastSyncTime, + mapCount: status.mapCount, + objectCount: status.objectCount, + timestamp: new Date().toISOString() + }; + + } catch (error) { + this.logger.error('获取配置失败', { + error: (error as Error).message, + }); + + throw new HttpException( + { + success: false, + error: (error as Error).message, + timestamp: new Date().toISOString() + }, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + + /** + * 获取配置状态 + */ + @Get('status') + @ApiOperation({ + summary: '获取配置状态', + description: '获取统一配置管理器的状态信息' + }) + @ApiResponse({ + status: 200, + description: '状态获取成功' + }) + async getConfigStatus() { + try { + const status = this.configManager.getConfigStatus(); + + return { + success: true, + data: { + ...status, + lastSyncTimeAgo: status.lastSyncTime ? + Math.round((Date.now() - status.lastSyncTime.getTime()) / 60000) : null, + }, + timestamp: new Date().toISOString() + }; + + } catch (error) { + this.logger.error('获取配置状态失败', { + error: (error as Error).message, + }); + + throw new HttpException( + { + success: false, + error: (error as Error).message, + timestamp: new Date().toISOString() + }, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + + /** + * 测试Zulip连接 + */ + @Get('test-connection') + @ApiOperation({ + summary: '测试Zulip连接', + description: '测试与Zulip服务器的连接状态' + }) + @ApiResponse({ + status: 200, + description: '连接测试完成' + }) + async testConnection() { + try { + this.logger.log('测试Zulip连接'); + + const connected = await this.configManager.testZulipConnection(); + + return { + success: true, + data: { + connected, + message: connected ? 'Zulip连接正常' : 'Zulip连接失败' + }, + timestamp: new Date().toISOString() + }; + + } catch (error) { + this.logger.error('连接测试失败', { + error: (error as Error).message, + }); + + return { + success: false, + data: { + connected: false, + message: '连接测试异常' + }, + error: (error as Error).message, + timestamp: new Date().toISOString() + }; + } + } + + /** + * 同步远程配置 + */ + @Post('sync') + @ApiOperation({ + summary: '同步远程配置', + description: '手动触发从Zulip服务器同步配置到本地文件' + }) + @ApiResponse({ + status: 200, + description: '配置同步完成' + }) + async syncConfig() { + try { + this.logger.log('手动同步配置'); + + const result = await this.configManager.syncConfig(); + + return { + success: result.success, + data: { + source: result.source, + mapCount: result.mapCount, + objectCount: result.objectCount, + lastUpdated: result.lastUpdated, + backupCreated: result.backupCreated, + message: result.success ? '配置同步成功' : '配置同步失败' + }, + error: result.error, + timestamp: new Date().toISOString() + }; + + } catch (error) { + this.logger.error('同步配置失败', { + error: (error as Error).message, + }); + + throw new HttpException( + { + success: false, + error: (error as Error).message, + timestamp: new Date().toISOString() + }, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + + /** + * 获取Stream列表 + */ + @Get('streams') + @ApiOperation({ + summary: '获取Zulip Stream列表', + description: '直接从Zulip服务器获取Stream列表' + }) + @ApiResponse({ + status: 200, + description: 'Stream列表获取成功' + }) + async getStreams() { + try { + this.logger.log('获取Zulip Stream列表'); + + const streams = await this.configManager.getZulipStreams(); + + return { + success: true, + data: { + streams: streams.map(stream => ({ + id: stream.stream_id, + name: stream.name, + description: stream.description, + isPublic: !stream.invite_only, + isWebPublic: stream.is_web_public + })), + count: streams.length + }, + timestamp: new Date().toISOString() + }; + + } catch (error) { + this.logger.error('获取Stream列表失败', { + error: (error as Error).message, + }); + + throw new HttpException( + { + success: false, + error: (error as Error).message, + timestamp: new Date().toISOString() + }, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + + /** + * 获取指定Stream的Topic列表 + */ + @Get('topics') + @ApiOperation({ + summary: '获取Stream的Topic列表', + description: '获取指定Stream的所有Topic' + }) + @ApiQuery({ + name: 'streamId', + description: 'Stream ID', + required: true, + type: 'number' + }) + @ApiResponse({ + status: 200, + description: 'Topic列表获取成功' + }) + async getTopics(@Query('streamId') streamId: string) { + try { + const streamIdNum = parseInt(streamId, 10); + if (isNaN(streamIdNum)) { + throw new Error('无效的Stream ID'); + } + + this.logger.log('获取Stream Topic列表', { streamId: streamIdNum }); + + const topics = await this.configManager.getZulipTopics(streamIdNum); + + return { + success: true, + data: { + streamId: streamIdNum, + topics: topics.map(topic => ({ + name: topic.name, + lastMessageId: topic.max_id + })), + count: topics.length + }, + timestamp: new Date().toISOString() + }; + + } catch (error) { + this.logger.error('获取Topic列表失败', { + streamId, + error: (error as Error).message, + }); + + throw new HttpException( + { + success: false, + error: (error as Error).message, + timestamp: new Date().toISOString() + }, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + + /** + * 查询地图配置 + */ + @Get('maps') + @ApiOperation({ + summary: '获取地图配置列表', + description: '获取所有地图的配置信息' + }) + @ApiResponse({ + status: 200, + description: '地图配置获取成功' + }) + async getMaps() { + try { + this.logger.log('获取地图配置列表'); + + const maps = await this.configManager.getAllMapConfigs(); + + return { + success: true, + data: { + maps: maps.map(map => ({ + mapId: map.mapId, + mapName: map.mapName, + zulipStream: map.zulipStream, + zulipStreamId: map.zulipStreamId, + description: map.description, + isPublic: map.isPublic, + objectCount: map.interactionObjects?.length || 0 + })), + count: maps.length + }, + timestamp: new Date().toISOString() + }; + + } catch (error) { + this.logger.error('获取地图配置失败', { + error: (error as Error).message, + }); + + throw new HttpException( + { + success: false, + error: (error as Error).message, + timestamp: new Date().toISOString() + }, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + + /** + * 根据地图ID获取Stream + */ + @Get('map-to-stream') + @ApiOperation({ + summary: '地图ID转Stream名称', + description: '根据地图ID获取对应的Zulip Stream名称' + }) + @ApiQuery({ + name: 'mapId', + description: '地图ID', + required: true, + type: 'string' + }) + @ApiResponse({ + status: 200, + description: '转换成功' + }) + async mapToStream(@Query('mapId') mapId: string) { + try { + this.logger.log('地图ID转Stream', { mapId }); + + const streamName = await this.configManager.getStreamByMap(mapId); + + return { + success: true, + data: { + mapId, + streamName, + found: !!streamName + }, + timestamp: new Date().toISOString() + }; + + } catch (error) { + this.logger.error('地图ID转Stream失败', { + mapId, + error: (error as Error).message, + }); + + throw new HttpException( + { + success: false, + error: (error as Error).message, + timestamp: new Date().toISOString() + }, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + + /** + * 根据Stream名称获取地图ID + */ + @Get('stream-to-map') + @ApiOperation({ + summary: 'Stream名称转地图ID', + description: '根据Zulip Stream名称获取对应的地图ID' + }) + @ApiQuery({ + name: 'streamName', + description: 'Stream名称', + required: true, + type: 'string' + }) + @ApiResponse({ + status: 200, + description: '转换成功' + }) + async streamToMap(@Query('streamName') streamName: string) { + try { + this.logger.log('Stream转地图ID', { streamName }); + + const mapId = await this.configManager.getMapIdByStream(streamName); + + return { + success: true, + data: { + streamName, + mapId, + found: !!mapId + }, + timestamp: new Date().toISOString() + }; + + } catch (error) { + this.logger.error('Stream转地图ID失败', { + streamName, + error: (error as Error).message, + }); + + throw new HttpException( + { + success: false, + error: (error as Error).message, + timestamp: new Date().toISOString() + }, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + + /** + * 获取备份文件列表 + */ + @Get('backups') + @ApiOperation({ + summary: '获取备份文件列表', + description: '获取所有配置备份文件的列表' + }) + @ApiResponse({ + status: 200, + description: '备份列表获取成功' + }) + async getBackups() { + try { + this.logger.log('获取备份文件列表'); + + const backups = this.configManager.getBackupFiles(); + + return { + success: true, + data: { + backups: backups.map(backup => ({ + name: backup.name, + size: backup.size, + created: backup.created, + sizeKB: Math.round(backup.size / 1024) + })), + count: backups.length + }, + timestamp: new Date().toISOString() + }; + + } catch (error) { + this.logger.error('获取备份列表失败', { + error: (error as Error).message, + }); + + throw new HttpException( + { + success: false, + error: (error as Error).message, + timestamp: new Date().toISOString() + }, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + + /** + * 从备份恢复配置 + */ + @Post('restore') + @ApiOperation({ + summary: '从备份恢复配置', + description: '从指定的备份文件恢复配置' + }) + @ApiQuery({ + name: 'backupFile', + description: '备份文件名', + required: true, + type: 'string' + }) + @ApiResponse({ + status: 200, + description: '配置恢复完成' + }) + async restoreFromBackup(@Query('backupFile') backupFile: string) { + try { + this.logger.log('从备份恢复配置', { backupFile }); + + const success = await this.configManager.restoreFromBackup(backupFile); + + return { + success, + data: { + backupFile, + message: success ? '配置恢复成功' : '配置恢复失败' + }, + timestamp: new Date().toISOString() + }; + + } catch (error) { + this.logger.error('配置恢复失败', { + backupFile, + error: (error as Error).message, + }); + + throw new HttpException( + { + success: false, + error: (error as Error).message, + timestamp: new Date().toISOString() + }, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } +} \ No newline at end of file diff --git a/src/business/zulip/services/zulip_accounts_business.service.ts b/src/business/zulip/services/zulip_accounts_business.service.ts new file mode 100644 index 0000000..5caccb6 --- /dev/null +++ b/src/business/zulip/services/zulip_accounts_business.service.ts @@ -0,0 +1,521 @@ +/** + * Zulip账号关联业务服务 + * + * 功能描述: + * - 提供Zulip账号关联的完整业务逻辑 + * - 管理账号关联的生命周期 + * - 处理账号验证和同步 + * - 提供统计和监控功能 + * - 实现业务异常转换和错误处理 + * - 集成缓存机制提升查询性能 + * - 支持批量操作和性能监控 + * + * 职责分离: + * - 业务逻辑:处理复杂的业务规则和流程 + * - 异常转换:将Repository层异常转换为业务异常 + * - DTO转换:实体对象与响应DTO之间的转换 + * - 缓存管理:管理热点数据的缓存策略 + * - 性能监控:记录操作耗时和性能指标 + * - 日志记录:使用AppLoggerService记录结构化日志 + * + * 最近修改: + * - 2026-01-12: 架构优化 - 从core/zulip_core移动到business/zulip,符合架构分层规范 (修改者: moyin) + * - 2026-01-12: 代码质量优化 - 清理未使用的导入,移除冗余DTO引用 (修改者: moyin) + * + * @author angjustinl + * @version 2.1.0 + * @since 2026-01-12 + * @lastModified 2026-01-12 + */ + +import { Injectable, Inject, ConflictException, NotFoundException } from '@nestjs/common'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { Cache } from 'cache-manager'; +import { AppLoggerService } from '../../../core/utils/logger/logger.service'; +import { + CreateZulipAccountDto, + ZulipAccountResponseDto, + ZulipAccountStatsResponseDto, +} from '../../../core/db/zulip_accounts/zulip_accounts.dto'; + +/** + * Zulip账号关联业务服务基类 + */ +abstract class BaseZulipAccountsBusinessService { + protected readonly logger: AppLoggerService; + protected readonly moduleName: string; + + constructor( + @Inject(AppLoggerService) logger: AppLoggerService, + moduleName: string = 'ZulipAccountsBusinessService' + ) { + this.logger = logger; + this.moduleName = moduleName; + } + + /** + * 统一的错误格式化方法 + */ + protected formatError(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + return String(error); + } + + /** + * 统一的异常处理方法 + */ + protected handleServiceError(error: unknown, operation: string, context?: Record): never { + const errorMessage = this.formatError(error); + + this.logger.error(`${operation}失败`, { + module: this.moduleName, + operation, + error: errorMessage, + context, + timestamp: new Date().toISOString() + }, error instanceof Error ? error.stack : undefined); + + if (error instanceof ConflictException || + error instanceof NotFoundException) { + throw error; + } + + throw new ConflictException(`${operation}失败,请稍后重试`); + } + + /** + * 搜索异常的特殊处理 + */ + protected handleSearchError(error: unknown, operation: string, context?: Record): any[] { + const errorMessage = this.formatError(error); + + this.logger.warn(`${operation}失败,返回空结果`, { + module: this.moduleName, + operation, + error: errorMessage, + context, + timestamp: new Date().toISOString() + }); + + return []; + } + + /** + * 记录操作成功日志 + */ + protected logSuccess(operation: string, context?: Record, duration?: number): void { + this.logger.info(`${operation}成功`, { + module: this.moduleName, + operation, + context, + duration, + timestamp: new Date().toISOString() + }); + } + + /** + * 记录操作开始日志 + */ + protected logStart(operation: string, context?: Record): void { + this.logger.info(`开始${operation}`, { + module: this.moduleName, + operation, + context, + timestamp: new Date().toISOString() + }); + } + + /** + * 创建性能监控器 + */ + protected createPerformanceMonitor(operation: string, context?: Record) { + const startTime = Date.now(); + this.logStart(operation, context); + + return { + success: (additionalContext?: Record) => { + const duration = Date.now() - startTime; + this.logSuccess(operation, { ...context, ...additionalContext }, duration); + }, + error: (error: unknown, additionalContext?: Record) => { + const duration = Date.now() - startTime; + this.handleServiceError(error, operation, { + ...context, + ...additionalContext, + duration + }); + } + }; + } + + /** + * 解析游戏用户ID为BigInt类型 + */ + protected parseGameUserId(gameUserId: string): bigint { + try { + return BigInt(gameUserId); + } catch (error) { + throw new ConflictException(`无效的游戏用户ID格式: ${gameUserId}`); + } + } + + /** + * 批量解析ID数组为BigInt类型 + */ + protected parseIds(ids: string[]): bigint[] { + try { + return ids.map(id => BigInt(id)); + } catch (error) { + throw new ConflictException(`无效的ID格式: ${ids.join(', ')}`); + } + } + + /** + * 解析单个ID为BigInt类型 + */ + protected parseId(id: string): bigint { + try { + return BigInt(id); + } catch (error) { + throw new ConflictException(`无效的ID格式: ${id}`); + } + } + + /** + * 抽象方法:将实体转换为响应DTO + */ + protected abstract toResponseDto(entity: any): any; + + /** + * 将实体数组转换为响应DTO数组 + */ + protected toResponseDtoArray(entities: any[]): any[] { + return entities.map(entity => this.toResponseDto(entity)); + } + + /** + * 构建列表响应对象 + */ + protected buildListResponse(entities: any[]): any { + const responseAccounts = this.toResponseDtoArray(entities); + return { + accounts: responseAccounts, + total: responseAccounts.length, + count: responseAccounts.length, + }; + } +} + +/** + * Zulip账号关联业务服务类 + * + * 职责: + * - 处理Zulip账号关联的业务逻辑 + * - 管理账号关联的生命周期和状态 + * - 提供业务级别的异常处理和转换 + * - 实现缓存策略和性能优化 + * + * 主要方法: + * - create(): 创建Zulip账号关联 + * - findByGameUserId(): 根据游戏用户ID查找关联 + * - getStatusStatistics(): 获取账号状态统计 + * - toResponseDto(): 实体到DTO的转换 + * + * 使用场景: + * - 用户注册时创建Zulip账号关联 + * - 查询用户的Zulip账号信息 + * - 系统监控和统计分析 + * - 账号状态管理和维护 + */ +@Injectable() +export class ZulipAccountsBusinessService extends BaseZulipAccountsBusinessService { + // 缓存键前缀 + private static readonly CACHE_PREFIX = 'zulip_accounts'; + private static readonly CACHE_TTL = 300; // 5分钟缓存 + private static readonly STATS_CACHE_TTL = 60; // 统计数据1分钟缓存 + + constructor( + @Inject('ZulipAccountsRepository') private readonly repository: any, + @Inject(AppLoggerService) logger: AppLoggerService, + @Inject(CACHE_MANAGER) private readonly cacheManager: Cache, + ) { + super(logger, 'ZulipAccountsBusinessService'); + this.logger.info('ZulipAccountsBusinessService初始化完成', { + module: 'ZulipAccountsBusinessService', + operation: 'constructor', + cacheEnabled: !!this.cacheManager + }); + } + + /** + * 创建Zulip账号关联 + * + * 功能描述: + * 创建游戏用户与Zulip账号的关联关系 + * + * 业务逻辑: + * 1. 验证游戏用户ID格式 + * 2. 调用Repository层创建关联 + * 3. 处理业务异常(重复关联等) + * 4. 清理相关缓存 + * 5. 转换为业务响应DTO + * + * @param createDto 创建关联的数据传输对象 + * @returns Promise 创建结果 + * + * @throws ConflictException 当关联已存在时 + */ + async create(createDto: CreateZulipAccountDto): Promise { + const monitor = this.createPerformanceMonitor('创建Zulip账号关联', { + gameUserId: createDto.gameUserId + }); + + try { + const account = await this.repository.create({ + gameUserId: this.parseGameUserId(createDto.gameUserId), + zulipUserId: createDto.zulipUserId, + zulipEmail: createDto.zulipEmail, + zulipFullName: createDto.zulipFullName, + zulipApiKeyEncrypted: createDto.zulipApiKeyEncrypted, + status: createDto.status || 'active', + }); + + await this.clearRelatedCache(createDto.gameUserId, createDto.zulipUserId, createDto.zulipEmail); + + const result = this.toResponseDto(account); + monitor.success({ + accountId: account.id.toString(), + status: account.status + }); + + return result; + + } catch (error) { + if (error instanceof Error) { + if (error.message.includes('already has a Zulip account')) { + const conflictError = new ConflictException(`游戏用户 ${createDto.gameUserId} 已存在Zulip账号关联`); + monitor.error(conflictError); + } + if (error.message.includes('is already linked')) { + if (error.message.includes('Zulip user')) { + const conflictError = new ConflictException(`Zulip用户 ${createDto.zulipUserId} 已被关联到其他游戏账号`); + monitor.error(conflictError); + } + if (error.message.includes('Zulip email')) { + const conflictError = new ConflictException(`Zulip邮箱 ${createDto.zulipEmail} 已被关联到其他游戏账号`); + monitor.error(conflictError); + } + } + } + + monitor.error(error); + } + } + + /** + * 根据游戏用户ID查找关联(带缓存) + * + * 功能描述: + * 根据游戏用户ID查找对应的Zulip账号关联信息 + * + * 业务逻辑: + * 1. 检查缓存中是否存在 + * 2. 缓存未命中时查询Repository + * 3. 转换为业务响应DTO + * 4. 更新缓存 + * 5. 记录查询性能指标 + * + * @param gameUserId 游戏用户ID + * @param includeGameUser 是否包含游戏用户信息 + * @returns Promise 关联信息或null + */ + async findByGameUserId(gameUserId: string, includeGameUser: boolean = false): Promise { + const cacheKey = this.buildCacheKey('game_user', gameUserId, includeGameUser); + + try { + const cached = await this.cacheManager.get(cacheKey); + if (cached) { + this.logger.debug('缓存命中', { + module: this.moduleName, + operation: 'findByGameUserId', + gameUserId, + cacheKey + }); + return cached; + } + + const monitor = this.createPerformanceMonitor('根据游戏用户ID查找关联', { gameUserId }); + + const account = await this.repository.findByGameUserId(this.parseGameUserId(gameUserId), includeGameUser); + + if (!account) { + this.logger.debug('未找到Zulip账号关联', { + module: this.moduleName, + operation: 'findByGameUserId', + gameUserId + }); + monitor.success({ found: false }); + return null; + } + + const result = this.toResponseDto(account); + + await this.cacheManager.set(cacheKey, result, ZulipAccountsBusinessService.CACHE_TTL); + + monitor.success({ found: true, cached: true }); + return result; + + } catch (error) { + this.handleServiceError(error, '根据游戏用户ID查找关联', { gameUserId }); + } + } + + /** + * 获取账号状态统计(带缓存) + * + * 功能描述: + * 获取所有Zulip账号关联的状态统计信息 + * + * 业务逻辑: + * 1. 检查统计数据缓存 + * 2. 缓存未命中时查询Repository + * 3. 计算总计数据 + * 4. 更新缓存 + * 5. 返回统计结果 + * + * @returns Promise 状态统计信息 + */ + async getStatusStatistics(): Promise { + const cacheKey = this.buildCacheKey('stats'); + + try { + const cached = await this.cacheManager.get(cacheKey); + if (cached) { + this.logger.debug('统计数据缓存命中', { + module: this.moduleName, + operation: 'getStatusStatistics', + cacheKey + }); + return cached; + } + + const monitor = this.createPerformanceMonitor('获取账号状态统计'); + + const statistics = await this.repository.getStatusStatistics(); + + const result = { + active: statistics.active || 0, + inactive: statistics.inactive || 0, + suspended: statistics.suspended || 0, + error: statistics.error || 0, + total: (statistics.active || 0) + (statistics.inactive || 0) + + (statistics.suspended || 0) + (statistics.error || 0), + }; + + await this.cacheManager.set(cacheKey, result, ZulipAccountsBusinessService.STATS_CACHE_TTL); + + monitor.success({ + total: result.total, + cached: true + }); + + return result; + + } catch (error) { + this.handleServiceError(error, '获取账号状态统计'); + } + } + + /** + * 将实体转换为响应DTO + * + * 功能描述: + * 将Repository层返回的实体对象转换为业务层的响应DTO + * + * @param account 实体对象 + * @returns ZulipAccountResponseDto 响应DTO + */ + protected toResponseDto(account: any): ZulipAccountResponseDto { + return { + id: account.id.toString(), + gameUserId: account.gameUserId.toString(), + zulipUserId: account.zulipUserId, + zulipEmail: account.zulipEmail, + zulipFullName: account.zulipFullName, + status: account.status, + lastVerifiedAt: account.lastVerifiedAt?.toISOString(), + lastSyncedAt: account.lastSyncedAt?.toISOString(), + errorMessage: account.errorMessage, + retryCount: account.retryCount, + createdAt: account.createdAt.toISOString(), + updatedAt: account.updatedAt.toISOString(), + gameUser: account.gameUser, + }; + } + + /** + * 构建缓存键 + * + * @param type 缓存类型 + * @param identifier 标识符 + * @param includeGameUser 是否包含游戏用户信息 + * @returns string 缓存键 + * @private + */ + private buildCacheKey(type: string, identifier?: string, includeGameUser?: boolean): string { + const parts = [ZulipAccountsBusinessService.CACHE_PREFIX, type]; + if (identifier) parts.push(identifier); + if (includeGameUser) parts.push('with_user'); + return parts.join(':'); + } + + /** + * 清除相关缓存 + * + * @param gameUserId 游戏用户ID + * @param zulipUserId Zulip用户ID + * @param zulipEmail Zulip邮箱 + * @returns Promise + * @private + */ + private async clearRelatedCache(gameUserId?: string, zulipUserId?: number, zulipEmail?: string): Promise { + const keysToDelete: string[] = []; + + keysToDelete.push(this.buildCacheKey('stats')); + + if (gameUserId) { + keysToDelete.push(this.buildCacheKey('game_user', gameUserId, false)); + keysToDelete.push(this.buildCacheKey('game_user', gameUserId, true)); + } + + if (zulipUserId) { + keysToDelete.push(this.buildCacheKey('zulip_user', zulipUserId.toString(), false)); + keysToDelete.push(this.buildCacheKey('zulip_user', zulipUserId.toString(), true)); + } + + if (zulipEmail) { + keysToDelete.push(this.buildCacheKey('zulip_email', zulipEmail, false)); + keysToDelete.push(this.buildCacheKey('zulip_email', zulipEmail, true)); + } + + try { + await Promise.all(keysToDelete.map(key => this.cacheManager.del(key))); + + this.logger.debug('清除相关缓存', { + module: this.moduleName, + operation: 'clearRelatedCache', + keysCount: keysToDelete.length, + keys: keysToDelete + }); + } catch (error) { + this.logger.warn('清除缓存失败', { + module: this.moduleName, + operation: 'clearRelatedCache', + error: this.formatError(error), + keys: keysToDelete + }); + } + } +} \ No newline at end of file