Kafka: 基于 NestJS 的问卷系统配置与业务实现

配置管理系统设计与实现


核心目标:动态读取模板配置,支持多环境路径适配与模板激活管理。

1 ) 方案1

配置类结构定义

// 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

核心配置模块设计

// 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`
  }
}));

领域模型定义

// 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

核心功能:

  • 获取激活的问卷模板
  • 接收用户提交数据
  • 生成统计结果报表
// 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

// 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接口设计:

// 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生产者服务

// 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');
    }
  }
}

控制器

// 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):

[
  {
    "questionID": "Q1",
    "question": "您的年龄段是?",
    "defaultAnswer": "",
    "operations": [
      { "value": "A", "label": "18-25岁" },
      { "value": "B", "label": "26-35岁" }
    ]
  }
]

2 ) 统计结果格式 (survey_result.json):

{
  "templateID": 1,
  "totalRespondents": 150,
  "results": {
    "Q1": { "A": 80, "B": 70 },
    "Q2": { "A": 60, "B": 90 }
  }
}

适用场景:数据分析场景下,文件存储可作为HDFS/分布式存储的轻量化替代方案。

工程示例:Kafka生产端集成方案


目标:将用户提交数据异步推送至Kafka,解耦数据处理流程。

1 ) 方案1:NestJS原生Kafka模块

// 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库直连

// 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序列化)

// 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运维命令


# 创建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模块解决多平台路径差异:

import * as path from 'path';
const templatePath = path.resolve(__dirname, config.templateFilePath);

2 ) 配置校验

使用class-validator强化校验:

import { IsNumber, IsBoolean } from 'class-validator';
class TemplateConfigItem {
  @IsNumber() templateID: number;
  @IsBoolean() active: boolean;
}

3 ) 错误处理增强

async getStatistics() {
 try {
   // ...业务逻辑 
 } catch (error) {
   this.logger.error(`Failed to read result file: ${error.stack}`);
   throw new HttpException('Result data unavailable', 503);
 }
}

工程示例:1


1 ) 配置加载优化

// 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异步处理管道

// 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 ) 统计结果处理器

// 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:基础文件存储方案

// 配置示例 (.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:数据库集成方案

// 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方案

// 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 生产者

// 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 微服务

// 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 装饰器

// 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完整配置


// 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();
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 动态加载环境变量:
    ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env' })
    

2 ) 存储策略抽象

// 存储策略接口
interface Result Promise<void>;
  retrieve(templateId: number): Promise<any>;
}

// 文件存储实现
class FileStorage implements ResultStorage {
  // 实现具体方法
}

3 ) 异常处理强化

// 全局异常过滤器
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: any, host: ArgumentsHost) {
    // 统一错误格式处理
  }
}

4 ) 性能优化

  • 文件操作:使用fs/promises异步API
  • 大数据处理:流式读取(createReadStream)
  • 缓存机制:对静态模板加入Redis缓存

5 ) 文件路径兼容性

  • 使用 path 模块解决跨平台路径问题:
    import * as path from 'path';
    const filePath = path.join(__dirname, '../templates/survey.json');
    

6 ) Kafka 消息可靠性

  • 重试机制:配置 retry: { retries: 3 }
  • ACK 策略:acks: -1(所有副本确认)

7 ) 统计结果存储设计

{
 "templateId": 1,
 "totalResponses": 100,
 "results": [
   { "questionId": "Q1", "A": 40, "B": 60 },
   { "questionId": "Q2", "A": 30, "B": 70 }
 ]
}

系统架构图


客户端 → NestJS控制器 → Kafka生产者 → Kafka集群
                             ↓
                      NestJS消费者 → 存储层(文件/DB/ES)
                             ↓
                     统计服务 ← 结果查询接口

总结


本文完整实现了基于NestJS的微信问卷服务,核心创新点:

  1. 动态配置驱动:通过@nestjs/config实现多模板热切换。
  2. 存储扩展性:通过resultType支持文件/DB/ES多种存储方案。
  3. 异步处理能力:集成Kafka实现用户提交数据的削峰填谷。
  4. 跨平台适配:使用path模块解决Windows/Linux路径兼容问题。

初学者提示:

  • Kafka:分布式消息队列,用于解耦生产者和消费者。
  • Schema Registry:确保消息格式兼容性的schema管理服务(需配合Avro使用)。
  • HDFS:Hadoop分布式文件系统,适用于海量数据存储场景。

通过标准化JSON接口设计与模块化工程结构,本方案可直接扩展至企业级问卷系统,支持高并发数据采集与实时统计分析。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值