Files
whale-town-end/src/gateway/zulip/dynamic_config.controller.ts
moyin ed04b8c92d docs(zulip): 完善Zulip业务模块功能文档
范围: src/business/zulip/README.md
- 补充对外提供的接口章节(14个公共方法)
- 添加使用的项目内部依赖说明(7个依赖)
- 完善核心特性描述(5个特性)
- 添加潜在风险评估(4个风险及缓解措施)
- 优化文档结构和内容完整性
2026-01-15 10:53:04 +08:00

603 lines
14 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.
/**
* 统一配置管理控制器
*
* 功能描述:
* - 提供统一配置管理的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
);
}
}
}