Files
whale-town-end/src/business/zulip/services/zulip_accounts_business.service.ts
moyin ea97167a32 feat(zulip): 添加动态配置控制器和账户业务服务
范围:src/business/zulip/
- 添加dynamic_config.controller.ts动态配置管理控制器
- 添加services/zulip_accounts_business.service.ts账户业务服务
- 完善zulip业务模块功能架构
2026-01-12 19:39:57 +08:00

521 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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
});
}
}
}