feat(zulip): 添加动态配置控制器和账户业务服务

范围:src/business/zulip/
- 添加dynamic_config.controller.ts动态配置管理控制器
- 添加services/zulip_accounts_business.service.ts账户业务服务
- 完善zulip业务模块功能架构
This commit is contained in:
moyin
2026-01-12 19:39:57 +08:00
parent e6de8a75b7
commit ea97167a32
2 changed files with 1107 additions and 0 deletions

View File

@@ -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<string, any>): 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<string, any>): 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<string, any>, duration?: number): void {
this.logger.info(`${operation}成功`, {
module: this.moduleName,
operation,
context,
duration,
timestamp: new Date().toISOString()
});
}
/**
* 记录操作开始日志
*/
protected logStart(operation: string, context?: Record<string, any>): void {
this.logger.info(`开始${operation}`, {
module: this.moduleName,
operation,
context,
timestamp: new Date().toISOString()
});
}
/**
* 创建性能监控器
*/
protected createPerformanceMonitor(operation: string, context?: Record<string, any>) {
const startTime = Date.now();
this.logStart(operation, context);
return {
success: (additionalContext?: Record<string, any>) => {
const duration = Date.now() - startTime;
this.logSuccess(operation, { ...context, ...additionalContext }, duration);
},
error: (error: unknown, additionalContext?: Record<string, any>) => {
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<ZulipAccountResponseDto> 创建结果
*
* @throws ConflictException 当关联已存在时
*/
async create(createDto: CreateZulipAccountDto): Promise<ZulipAccountResponseDto> {
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<ZulipAccountResponseDto | null> 关联信息或null
*/
async findByGameUserId(gameUserId: string, includeGameUser: boolean = false): Promise<ZulipAccountResponseDto | null> {
const cacheKey = this.buildCacheKey('game_user', gameUserId, includeGameUser);
try {
const cached = await this.cacheManager.get<ZulipAccountResponseDto>(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<ZulipAccountStatsResponseDto> 状态统计信息
*/
async getStatusStatistics(): Promise<ZulipAccountStatsResponseDto> {
const cacheKey = this.buildCacheKey('stats');
try {
const cached = await this.cacheManager.get<ZulipAccountStatsResponseDto>(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<void>
* @private
*/
private async clearRelatedCache(gameUserId?: string, zulipUserId?: number, zulipEmail?: string): Promise<void> {
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
});
}
}
}