forked from datawhale/whale-town-end
feat(zulip): 添加动态配置控制器和账户业务服务
范围:src/business/zulip/ - 添加dynamic_config.controller.ts动态配置管理控制器 - 添加services/zulip_accounts_business.service.ts账户业务服务 - 完善zulip业务模块功能架构
This commit is contained in:
586
src/business/zulip/dynamic_config.controller.ts
Normal file
586
src/business/zulip/dynamic_config.controller.ts
Normal file
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
521
src/business/zulip/services/zulip_accounts_business.service.ts
Normal file
521
src/business/zulip/services/zulip_accounts_business.service.ts
Normal 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user