范围: src/business/zulip/README.md - 补充对外提供的接口章节(14个公共方法) - 添加使用的项目内部依赖说明(7个依赖) - 完善核心特性描述(5个特性) - 添加潜在风险评估(4个风险及缓解措施) - 优化文档结构和内容完整性
603 lines
14 KiB
TypeScript
603 lines
14 KiB
TypeScript
/**
|
||
* 统一配置管理控制器
|
||
*
|
||
* 功能描述:
|
||
* - 提供统一配置管理的REST API接口
|
||
* - 支持配置查询、同步、状态检查
|
||
* - 提供备份管理功能
|
||
*
|
||
* 架构定位:
|
||
* - 层级:Gateway层(网关层)
|
||
* - 职责:HTTP协议处理、API接口暴露
|
||
* - 依赖:调用Business层的ZulipModule服务
|
||
*
|
||
* 职责分离:
|
||
* - API接口:提供RESTful风格的配置管理接口
|
||
* - 协议处理:处理HTTP请求和响应
|
||
* - 参数验证:验证请求参数格式
|
||
* - 错误转换:将业务异常转换为HTTP响应
|
||
*
|
||
* 最近修改:
|
||
* - 2026-01-14: 架构优化 - 从Business层迁移到Gateway层,符合四层架构规范 (修改者: moyin)
|
||
* - 2026-01-14: 代码规范优化 - 完善文件头注释和职责分离描述 (修改者: moyin)
|
||
* - 2026-01-12: 功能新增 - 初始创建统一配置管理控制器 (修改者: moyin)
|
||
*
|
||
* @author moyin
|
||
* @version 3.0.0
|
||
* @since 2026-01-12
|
||
* @lastModified 2026-01-14
|
||
*/
|
||
|
||
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
|
||
);
|
||
}
|
||
}
|
||
} |