配置管理系统设计与实现
核心目标:动态读取模板配置,支持多环境路径适配与模板激活管理。
1 ) 方案1
配置类结构定义
typescript
// src/config/template.config.ts
import { registerAs } from '@nestjs/config';
export default registerAs('template', () => ({
templates: [
{
templateID: 1,
templateFilePath: './templates/survey.json', // 模板文件路径
active: true, // 当前激活状态
}
],
resultType: 0, // 0=文件存储, 1=数据库, 2=ES
resultFilePath: './results/survey_result.json' // 结果存储路径
}));
关键配置说明
templateID:模板唯一标识,用于匹配问卷模板与统计结果。templateFilePath:模板文件路径(兼容Windows/Linux路径格式)。active:标记当前生效模板(允许多模板存在,但仅一个生效)。resultType:结果存储类型(文件存储适用于轻量级分布式场景如HDFS)。
2 )方案2
核心配置模块设计
typescript
// src/config/template.config.ts
import { registerAs } from '@nestjs/config';
export default registerAs('templateConfig', () => ({
templates: [
{
templateId: 1,
filePath: `${process.cwd()}/templates/questionnaire.json`,
active: true
},
// 可扩展多个模板
{
templateId: 2,
filePath: `${process.cwd()}/templates/survey_v2.json`,
active: false
}
],
resultConfig: {
resultType: 0, // 0=文件存储, 1=数据库, 2=Elasticsearch
filePath: `${process.cwd()}/results/statistics.json`
}
}));
领域模型定义
typescript
// src/entities/template.entity.ts
export class QuestionnaireTemplate {
constructor(
public readonly templateId: number,
public filePath: string,
public active: boolean
) {}
}
// src/entities/result-config.entity.ts
export class ResultConfig {
constructor(
public readonly resultType: number,
public filePath: string
) {}
}
业务逻辑层实现(Service)
1 ) 方案1
核心功能:
- 获取激活的问卷模板
- 接收用户提交数据
- 生成统计结果报表
typescript
// src/template/template.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as fs from 'fs/promises';
import { TemplateConfig } from './config/template.config';
@Injectable()
export class TemplateService {
private readonly logger = new Logger(TemplateService.name);
constructor(private configService: ConfigService) {}
// 1. 获取激活模板
async getActiveTemplate() {
const { templates } = this.configService.get<TemplateConfig>('template');
const activeTemplate = templates.find(tpl => tpl.active);
if (!activeTemplate) throw new Error('No active template found');
const rawData = await fs.readFile(activeTemplate.templateFilePath, 'utf8');
return {
templateID: activeTemplate.templateID,
template: JSON.parse(rawData) // 返回解析后的JSON数组
};
}
// 2. 上报用户提交数据
async reportData(reportData: any) {
this.logger.debug(`Received report: ${JSON.stringify(reportData)}`);
// 后续接入Kafka生产端
}
// 3. 获取统计结果(根据resultType路由)
async getStatistics(templateID?: number) {
const { resultType, resultFilePath } = this.configService.get<TemplateConfig>('template');
if (resultType === 0) {
const rawData = await fs.readFile(resultFilePath, 'utf8');
return JSON.parse(rawData);
}
// TODO: 扩展其他存储类型逻辑
}
}
2 ) 方案2
typescript
// src/services/template.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as fs from 'fs/promises';
import { QuestionnaireTemplate, ResultConfig } from '../entities';
@Injectable()
export class TemplateService {
private readonly logger = new Logger(TemplateService.name);
constructor(private readonly configService: ConfigService) {}
// 获取激活模板
async getActiveTemplate(): Promise<QuestionnaireTemplate> {
const { templates } = this.configService.get('templateConfig');
const activeTemplate = templates.find(t => t.active);
if (!activeTemplate) {
this.logger.error('No active template configured');
throw new Error('ACTIVE_TEMPLATE_NOT_FOUND');
}
return new QuestionnaireTemplate(
activeTemplate.templateId,
activeTemplate.filePath,
activeTemplate.active
);
}
// 获取模板内容
async getTemplateContent(filePath: string): Promise<any> {
try {
const data = await fs.readFile(filePath, 'utf-8');
return JSON.parse(data);
} catch (error) {
this.logger.error(`Template read error: ${filePath}`, error.stack);
throw new Error('TEMPLATE_READ_FAILURE');
}
}
// 获取统计结果
async getStatisticalResult(): Promise<any> {
const { resultConfig } = this.configService.get('templateConfig');
switch (resultConfig.resultType) {
case 0: // 文件存储
return this.getResultFromFile(resultConfig.filePath);
case 1: // 数据库
return this.getResultFromDatabase();
case 2: // Elasticsearch
return this.getResultFromElasticsearch();
default:
throw new Error('INVALID_RESULT_TYPE');
}
}
private async getResultFromFile(path: string): Promise<any> {
try {
const rawData = await fs.readFile(path, 'utf-8');
return JSON.parse(rawData);
} catch (error) {
this.logger.error(`Result file read error: ${path}`, error.stack);
throw new Error('RESULT_READ_FAILURE');
}
}
}
控制器层(Controller)
1 ) 方案1
RESTful接口设计:
typescript
// src/template/template.controller.ts
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { TemplateService } from './template.service';
@Controller('template')
export class TemplateController {
constructor(private readonly templateService: TemplateService) {}
@Get() // GET /template
async getTemplate() {
return this.templateService.getActiveTemplate();
}
@Post('report') // POST /template/report
async report(@Body() reportData: any) {
await this.templateService.reportData(reportData);
return { status: 'success', message: 'Data reported' };
}
@Get('statistics/:templateID?') // GET /template/statistics/1
async getStats(@Param('templateID') templateID?: number) {
return this.templateService.getStatistics(templateID);
}
}
2 )方案2
Kafka生产者服务
typescript
// src/services/kafka.producer.service.ts
import { Injectable } from '@nestjs/common';
import { ClientKafka } from '@nestjs/microservices';
@Injectable()
export class KafkaProducerService {
constructor(private readonly client: ClientKafka) {}
async sendQuestionnaireResponse(responseData: object): Promise<void> {
try {
await this.client.emit('questionnaire.responses', {
value: JSON.stringify(responseData),
timestamp: new Date().toISOString()
});
} catch (error) {
throw new Error('KAFKA_PRODUCE_ERROR');
}
}
}
控制器
typescript
// src/controllers/template.controller.ts
import { Controller, Get, Post, Body } from '@nestjs/common';
import { TemplateService } from '../services/template.service';
import { KafkaProducerService } from '../services/kafka.producer.service';
@Controller('api/templates')
export class TemplateController {
constructor(
private readonly templateService: TemplateService,
private readonly kafkaProducer: KafkaProducerService
) {}
@Get('active')
async getActiveTemplate() {
const template = await this.templateService.getActiveTemplate();
const content = await this.templateService.getTemplateContent(template.filePath);
return {
templateId: template.templateId,
content
};
}
@Post('responses')
async submitResponse(@Body() responseData: any) {
await this.kafkaProducer.sendQuestionnaireResponse(responseData);
return { status: 'received', timestamp: new Date() };
}
@Get('statistics')
async getStatistics() {
return this.templateService.getStatisticalResult();
}
}
文件存储规范
1 ) 问卷模板格式 (survey.json):
json
[
{
"questionID": "Q1",
"question": "您的年龄段是?",
"defaultAnswer": "",
"operations": [
{ "value": "A", "label": "18-25岁" },
{ "value": "B", "label": "26-35岁" }
]
}
]
2 ) 统计结果格式 (survey_result.json):
json
{
"templateID": 1,
"totalRespondents": 150,
"results": {
"Q1": { "A": 80, "B": 70 },
"Q2": { "A": 60, "B": 90 }
}
}
适用场景:数据分析场景下,文件存储可作为HDFS/分布式存储的轻量化替代方案。
工程示例:Kafka生产端集成方案
目标:将用户提交数据异步推送至Kafka,解耦数据处理流程。
1 ) 方案1:NestJS原生Kafka模块
typescript
// src/kafka/kafka.producer.ts
import { Injectable, OnModuleInit } from '@nestjs/common';
import { ClientKafka, ClientProxyFactory, Transport } from '@nestjs/microservices';
@Injectable()
export class KafkaProducer {
private client: ClientKafka;
onModuleInit() {
this.client = ClientProxyFactory.create({
transport: Transport.KAFKA,
options: {
client: { brokers: ['localhost:9092'] },
producer: { allowAutoTopicCreation: true }
}
}) as ClientKafka;
}
async send(topic: string, data: any) {
await this.client.emit(topic, JSON.stringify(data));
}
}
// 在TemplateService中调用
async reportData(reportData: any) {
const producer = new KafkaProducer();
await producer.send('survey_reports', reportData);
}
2 ) 方案2:kafkajs库直连
typescript
// src/kafka/kafkajs.producer.ts
import { Injectable } from '@nestjs/common';
import { Kafka, Producer } from 'kafkajs';
@Injectable()
export class KafkaJsProducer {
private producer: Producer;
constructor() {
const kafka = new Kafka({ brokers: ['localhost:9092'] });
this.producer = kafka.producer();
this.producer.connect();
}
async send(topic: string, message: any) {
await this.producer.send({
topic,
messages: [{ value: JSON.stringify(message) }]
});
}
}
3 ) 方案3:Schema Registry集成(Avro序列化)
typescript
// src/kafka/avro.producer.ts
import { Injectable } from '@nestjs/common';
import { Kafka, Producer } from 'kafkajs';
import { SchemaRegistry } from '@kafkajs/confluent-schema-registry';
@Injectable()
export class AvroProducer {
private producer: Producer;
private registry = new SchemaRegistry({ host: 'http://schema-registry:8081' });
constructor() {
const kafka = new Kafka({ brokers: ['localhost:9092'] });
this.producer = kafka.producer();
this.producer.connect();
}
async send(topic: string, schemaId: number, message: any) {
const encodedValue = await this.registry.encode(schemaId, message);
await this.producer.send({
topic,
messages: [{ value: encodedValue }]
});
}
}
关键Kafka运维命令
bash
# 创建Topic
kafka-topics.sh --create --bootstrap-server localhost:9092 \
--topic survey_reports --partitions 3 --replication-factor 1
# 查看消息
kafka-console-consumer.sh --bootstrap-server localhost:9092 \
--topic survey_reports --from-beginning
# 查看Topic详情
kafka-topics.sh --describe --bootstrap-server localhost:9092 \
--topic survey_reports
配置与部署注意事项
1 ) 路径兼容性处理
使用path模块解决多平台路径差异:
typescript
import * as path from 'path';
const templatePath = path.resolve(__dirname, config.templateFilePath);
2 ) 配置校验
使用class-validator强化校验:
typescript
import { IsNumber, IsBoolean } from 'class-validator';
class TemplateConfigItem {
@IsNumber() templateID: number;
@IsBoolean() active: boolean;
}
3 ) 错误处理增强
typescript
async getStatistics() {
try {
// ...业务逻辑
} catch (error) {
this.logger.error(`Failed to read result file: ${error.stack}`);
throw new HttpException('Result data unavailable', 503);
}
}
工程示例:1
1 ) 配置加载优化
typescript
// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import templateConfig from './config/template.config';
@Module({
imports: [
ConfigModule.forRoot({
load: [templateConfig], // 预加载配置
isGlobal: true
}),
],
})
export class AppModule {}
2 ) Kafka异步处理管道
typescript
// main.ts 启动入口
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions } from '@nestjs/microservices';
import { KafkaOptions } from '@nestjs/microservices/interfaces/microservice-configuration.interface';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 附加Kafka消费者
app.connectMicroservice<MicroserviceOptions>({
transport: Transport.KAFKA,
options: {
client: { brokers: ['kafka:9092'] },
consumer: { groupId: 'result-processor' }
}
});
await app.startAllMicroservices();
await app.listen(3000);
}
bootstrap();
3 ) 统计结果处理器
typescript
// src/consumers/result.processor.ts
import { Controller } from '@nestjs/common';
import { Kafka, Consumer, EachMessagePayload } from 'kafkajs';
@Controller()
export class ResultProcessor {
private consumer: Consumer;
constructor() {
const kafka = new Kafka({ brokers: ['kafka:9092'] });
this.consumer = kafka.consumer({ groupId: 'result-processor' });
this.consumer.connect().then(() => {
this.consumer.subscribe({ topic: 'survey_reports' });
this.consumer.run({ eachMessage: this.processMessage.bind(this) });
});
}
private async processMessage({ message }: EachMessagePayload) {
const reportData = JSON.parse(message.value.toString());
// 此处实现统计逻辑并更新结果文件
}
}
工程示例:2
1 ) 方案1:基础文件存储方案
typescript
// 配置示例 (.env)
TEMPLATE_CONFIG='{
"templates": [{
"templateId": 101,
"filePath": "./data/templates/survey_v1.json",
"active": true
}],
"resultConfig": {
"resultType": 0,
"filePath": "./data/results/stats_v1.json"
}
}'
// 模板文件示例 (survey_v1.json)
[
{
"questionId": "Q1",
"content": "您的年龄段是?",
"options": [
{"value": "A", "label": "18岁以下"},
{"value": "B", "label": "18-25岁"}
]
}
]
2 ) 方案2:数据库集成方案
typescript
// src/services/database.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ResponseEntity } from '../entities';
@Injectable()
export class DatabaseService {
constructor(
@InjectRepository(ResponseEntity)
private responseRepo: Repository<ResponseEntity>
) {}
async saveResponse(response: any): Promise<void> {
await this.responseRepo.save({
templateId: response.templateId,
userId: response.userId,
answers: JSON.stringify(response.answers),
createdAt: new Date()
});
}
async getStatistics(templateId: number): Promise<any> {
// 实现聚合查询逻辑
}
}
3 ) 方案3:Kafka+Elasticsearch方案
typescript
// src/consumers/response.consumer.ts
import { Processor, Process } from '@nestjs/bull';
import { ElasticsearchService } from '@nestjs/elasticsearch';
@Processor('response-queue')
export class ResponseConsumer {
constructor(private readonly esService: ElasticsearchService) {}
@Process()
async processResponse(job: Job) {
const response = job.data;
await this.esService.index({
index: 'questionnaire-responses',
body: {
templateId: response.templateId,
timestamp: new Date(),
...response.answers
}
});
}
}
工程示例:3
1 ) 方案 1:原生 KafkaJS 生产者
typescript
// kafka.producer.ts
import { Injectable, OnModuleInit } from '@nestjs/common';
import { Kafka, Producer, ProducerRecord } from 'kafkajs';
@Injectable()
export class KafkaProducer implements OnModuleInit {
private producer: Producer;
async onModuleInit() {
const kafka = new Kafka({ brokers: ['localhost:9092'] });
this.producer = kafka.producer();
await this.producer.connect();
}
async sendMessage(topic: string, message: any) {
const record: ProducerRecord = {
topic,
messages: [{ value: JSON.stringify(message) }],
};
await this.producer.send(record);
}
}
// 在 SurveyService 中调用
import { KafkaProducer } from './kafka.producer';
//) {
const kafkaProducer = new KafkaProducer();
await kafkaProducer.sendMessage('survey-responses', data);
}
2 ) 方案 2:NestJS 官方 Kafka 微服务
typescript
// main.ts
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.connectMicroservice<MicroserviceOptions>({
transport: Transport.KAFKA,
options: {
client: { brokers: ['localhost:9092'] },
consumer: { groupId: 'survey-consumer' }
}
});
await app.startAllMicroservices();
await app.listen(3000);
}
bootstrap();
// survey.controller.ts(添加消费者)
@EventPattern('survey-responses')
handleSurvey.log('Received data:', data);
}
3 ) 方案 3:自定义 Kafka 装饰器
typescript
// kafka.decorator.ts
import { Inject, Injectable } from '@nestjs/common';
import { ClientKafka } from '@nestjs/microservices';
@Injectable()
export class KafkaService {
constructor(@Inject('KAFKA_CLIENT') private client: ClientKafka) {}
async emitEvent(topic: string.client.emit(topic, { value: JSON.stringify(data) });
}
}
// 模块注册
@Module({
imports: [
ClientsModule.register([
{
name: 'KAFKA_CLIENT',
transport: Transport.KAFKA,
options: { /* ... */ }
}
])
],
providers: [KafkaService]
})
Kafka完整配置
typescript
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Kafka微服务配置
app.connectMicroservice<MicroserviceOptions>({
transport: Transport.KAFKA,
options: {
client: {
brokers: ['kafka-server:9092'],
},
consumer: {
groupId: 'questionnaire-consumer'
}
}
});
await app.startAllMicroservices();
await app.listen(3000);
}
bootstrap();
bash
Kafka命令行操作
创建Topic
kafka-topics.sh --create --bootstrap-server localhost:9092 \
--topic questionnaire.responses \
--partitions 3 \
--replication-factor 2
查看消息
kafka-console-consumer.sh --bootstrap-server localhost:9092 \
--topic questionnaire.responses \
--from-beginning
关键技术点解析
1 ) 动态配置管理
-
使用
@nestjs/config实现多环境配置 -
路径处理:
process.cwd()确保跨平台兼容性 -
多模板支持:通过
active标志切换模板 -
配置文件热更新
-
使用
@nestjs/config动态加载环境变量:typescriptConfigModule.forRoot({ isGlobal: true, envFilePath: '.env' })
2 ) 存储策略抽象
typescript
// 存储策略接口
interface Result Promise<void>;
retrieve(templateId: number): Promise<any>;
}
// 文件存储实现
class FileStorage implements ResultStorage {
// 实现具体方法
}
3 ) 异常处理强化
typescript
// 全局异常过滤器
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: any, host: ArgumentsHost) {
// 统一错误格式处理
}
}
4 ) 性能优化
- 文件操作:使用
fs/promises异步API - 大数据处理:流式读取(createReadStream)
- 缓存机制:对静态模板加入Redis缓存
5 ) 文件路径兼容性
-
使用
path模块解决跨平台路径问题:typescriptimport * as path from 'path'; const filePath = path.join(__dirname, '../templates/survey.json');
6 ) Kafka 消息可靠性
- 重试机制:配置
retry: { retries: 3 } - ACK 策略:
acks: -1(所有副本确认)
7 ) 统计结果存储设计
json
{
"templateId": 1,
"totalResponses": 100,
"results": [
{ "questionId": "Q1", "A": 40, "B": 60 },
{ "questionId": "Q2", "A": 30, "B": 70 }
]
}
系统架构图
tree
客户端 → NestJS控制器 → Kafka生产者 → Kafka集群
↓
NestJS消费者 → 存储层(文件/DB/ES)
↓
统计服务 ← 结果查询接口
总结
本文完整实现了基于NestJS的微信问卷服务,核心创新点:
- 动态配置驱动:通过
@nestjs/config实现多模板热切换。 - 存储扩展性:通过
resultType支持文件/DB/ES多种存储方案。 - 异步处理能力:集成Kafka实现用户提交数据的削峰填谷。
- 跨平台适配:使用
path模块解决Windows/Linux路径兼容问题。
初学者提示:
- Kafka:分布式消息队列,用于解耦生产者和消费者。
- Schema Registry:确保消息格式兼容性的schema管理服务(需配合Avro使用)。
- HDFS:Hadoop分布式文件系统,适用于海量数据存储场景。
通过标准化JSON接口设计与模块化工程结构,本方案可直接扩展至企业级问卷系统,支持高并发数据采集与实时统计分析。