docs(zulip): 完善Zulip业务模块功能文档
范围: src/business/zulip/README.md - 补充对外提供的接口章节(14个公共方法) - 添加使用的项目内部依赖说明(7个依赖) - 完善核心特性描述(5个特性) - 添加潜在风险评估(4个风险及缓解措施) - 优化文档结构和内容完整性
This commit is contained in:
603
src/gateway/zulip/dynamic_config.controller.ts
Normal file
603
src/gateway/zulip/dynamic_config.controller.ts
Normal file
@@ -0,0 +1,603 @@
|
||||
/**
|
||||
* 统一配置管理控制器
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供统一配置管理的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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user