SpringAI + SpringDoc + Knife4j 构建企业级智能问卷系统
面向生产环境,本文以企业智能问卷平台为案例,系统讲透如何基于 Spring Boot 3、Spring AI、SpringDoc、Knife4j 构建一套可扩展、可治理、可承载高并发流量的智能问卷系统。文章不仅给出配置和代码,更重点补足架构设计、并发控制、异步化改造、缓存一致性、幂等、防刷、限流、观测与部署等工程化能力。
一、为什么企业需要 “智能问卷系统”?
在很多企业数字化场景里,问卷系统并不是一个简单的 CRUD 系统,而是同时具备以下特征的平台型能力:
- 业务侧需要快速创建活动问卷、调研问卷、满意度问卷、NPS 问卷、招聘测评问卷
- 运营侧需要 AI 辅助生成题目、题型建议、问卷结构优化、答卷摘要分析
- 平台侧需要开放 API 给 CRM、OA、营销系统、SaaS 租户接入
- 高峰期存在大量用户同时答题、提交、查询结果、导出分析报表
- 管理后台要求文档清晰、接口规范、联调高效、权限可控、系统易扩展
这类系统进入生产环境会暴露四类典型问题:
- 业务复杂度上升后,单体 Controller + Service 的代码结构难以承载变化。
- AI 调用存在高延迟、高成本、不稳定、限流等天然问题,不能直接塞进同步请求链路。
- 活动高峰下,问卷查询、提交、统计分析会形成读写热点,数据库和线程池都容易被打满。
- 接口一多,文档和实际代码容易漂移,前后端和测试协作成本快速上涨。
企业级智能问卷系统设计目标:
- 低耦合:业务域、AI 域、文档域、基础设施域解耦
- 高并发:承载活动流量峰值,具备削峰、隔离、降级能力
- 可扩展:题型、问卷模板、AI 模型、租户能力平滑演进
- 可治理:接口标准化、链路可观测、故障可定位、成本可控制
二、项目目标与技术选型
2.1 目标定义
系统核心能力:
- 问卷模板管理:创建、编辑、发布、下线、复制模板
- AI 智能生成:根据业务主题自动生成问卷草稿
- AI 答卷解析:对答卷文本做聚类、摘要、情绪分析、风险识别
- 高并发答题:支持大规模用户提交答卷
- 开放文档门户:自动生成 OpenAPI 文档并提供增强 UI
- 平台治理:限流、幂等、缓存、监控、审计、安全控制
2.2 技术栈
| 组件 | 版本建议 | 作用 |
|---|---|---|
| Java | 21 LTS | 虚拟线程、性能优化、长期支持 |
| Spring Boot | 3.3.x | 应用基础框架 |
| Spring Web | 6.x | REST API |
| Spring Validation | 6.x | 参数校验 |
| Spring Data JPA / MyBatis | 按团队偏好 | 数据访问 |
| Spring AI | 1.0.x | 模型统一接入层 |
| SpringDoc OpenAPI | 2.3.x | OpenAPI 文档生成 |
| Knife4j | 4.4.x | OpenAPI 可视化增强 |
| PostgreSQL | 16 | 核心业务数据存储 |
| Redis | 7.x | 缓存、分布式锁、幂等、防刷 |
| Kafka / RocketMQ | 任选其一 | AI 异步任务、事件驱动 |
| Micrometer + Prometheus + Grafana | 最新稳定版 | 指标采集与可观测 |
| Docker + Kubernetes | 最新稳定版 | 部署与弹性扩缩容 |
2.3 为什么选择 Spring Boot 3 + Spring AI + SpringDoc + Knife4j?
Spring Boot 3
- 基于 Jakarta EE 9+,生态统一
- 原生支持 Java 21 新特性
- 对虚拟线程、AOT、Native Image 友好
- 适合中大型企业服务化架构
Spring AI
- 为大模型调用提供统一抽象
- 屏蔽不同模型厂商 SDK 差异
- 支持 Prompt、结构化输出、Embedding、向量存储接入
- 便于做模型切换、灰度、路由、容错
SpringDoc + Knife4j
- SpringDoc 负责扫描 Controller 和注解,生成 OpenAPI 规范
- Knife4j 负责增强 UI、分组、调试体验和管理能力
- 适合企业内部 API 门户、测试联调和接口治理
三、系统总体架构设计
3.1 分层视角:不要把智能问卷系统做成 “大号 CRUD”
企业系统四层架构:
- 接入层
- 应用层
- 领域层
- 基础设施层
客户端/管理后台/第三方系统
→ API Gateway
→ Survey API Service / AI Orchestrator Service / Analytics Service
→ Application Layer
→ Domain Layer
→ Repository / Domain Service / Event Publisher
→ PostgreSQL / Redis / Kafka / LLM Provider / OLAP / Search / Vector Store
3.2 逻辑模块划分
按业务边界拆分:
ai-survey-platform
├── survey-api # 对外 REST API,参数校验、鉴权、返回包装
├── survey-application # 应用编排层,事务、命令处理、查询处理
├── survey-domain # 领域模型、聚合、领域服务、领域事件
├── survey-infrastructure # DB、Redis、MQ、第三方 AI 适配
├── survey-ai # Prompt、模型路由、结构化输出、AI 任务编排
├── survey-doc # OpenAPI、Knife4j、接口分组、文档治理
├── survey-common # 通用异常、工具类、枚举、上下文
└── deploy # Docker、K8s、Helm、脚本
拆分好处:
- API 层只负责协议和入参,不承载业务核心逻辑
- AI 能力与问卷核心领域解耦,便于替换模型提供商
- 基础设施可替换不影响领域层
- 文档治理独立配置,不污染业务代码
四、核心业务建模:问卷系统真正的聚合边界
4.1 领域对象识别
- Survey:问卷
- Question:题目
- Option:选项
- ResponseSheet:答卷
- ResponseAnswer:答题项
- SurveyTemplate:模板
- SurveyGenerationTask:AI 生成任务
4.2 聚合边界建议
Survey 聚合:问卷基本信息、题目定义、发布状态流转、版本号控制ResponseSheet 聚合:用户提交答卷、答卷完整性校验、幂等提交控制、提交信息SurveyGenerationTask 聚合:AI 生成任务生命周期、Prompt 版本、模型信息、重试次数、失败原因
4.3 为什么不能把 AI 生成直接写进 SurveyService.create ()?
AI 生成特点:
- 耗时高:500ms 到数秒
- 成功率不稳定:超时、限流、网络异常、格式错误
- 成本敏感
- 不适合与数据库事务强绑定
正确设计:
- 前台提交 “生成请求”
- 服务端创建任务记录
- 异步消费任务调用大模型
- 结果结构化校验后入库
- 轮询 / WebSocket / 消息通知前端查看结果
五、数据库设计:兼顾一致性、扩展性与高并发
5.1 核心表设计
问卷主表
create table t_survey (
id bigserial primary key,
tenant_id bigint not null,
title varchar(256) not null,
description text,
status varchar(32) not null,
version integer not null default 1,
publish_time timestamp null,
creator_id bigint not null,
deleted boolean not null default false,
created_at timestamp not null default current_timestamp,
updated_at timestamp not null default current_timestamp
);
create index idx_survey_tenant_status on t_survey(tenant_id, status);
create index idx_survey_creator on t_survey(creator_id);
题目表
create table t_survey_question (
id bigserial primary key,
survey_id bigint not null,
question_no integer not null,
question_type varchar(32) not null,
title varchar(512) not null,
required boolean not null default false,
config_json jsonb not null,
created_at timestamp not null default current_timestamp
);
create unique index uk_survey_question_no on t_survey_question(survey_id, question_no);
create index idx_question_survey_id on t_survey_question(survey_id);
答卷主表
create table t_response_sheet (
id bigserial primary key,
tenant_id bigint not null,
survey_id bigint not null,
respondent_id bigint null,
submit_token varchar(64) not null,
source varchar(32) not null,
client_ip varchar(64),
device_id varchar(128),
status varchar(32) not null,
submitted_at timestamp null,
created_at timestamp not null default current_timestamp
);
create unique index uk_response_submit_token on t_response_sheet(submit_token);
create index idx_response_survey_id on t_response_sheet(survey_id);
create index idx_response_tenant_survey on t_response_sheet(tenant_id, survey_id);
AI 任务表
create table t_ai_generation_task (
id bigserial primary key,
tenant_id bigint not null,
task_no varchar(64) not null,
topic varchar(256) not null,
prompt_template_version varchar(32) not null,
model_name varchar(64) not null,
task_status varchar(32) not null,
retry_count integer not null default 0,
request_payload jsonb not null,
response_payload jsonb,
fail_reason varchar(1024),
created_by bigint not null,
created_at timestamp not null default current_timestamp,
updated_at timestamp not null default current_timestamp
);
create unique index uk_ai_task_no on t_ai_generation_task(task_no);
create index idx_ai_task_status on t_ai_generation_task(task_status);
5.2 高并发下的建模原则
- 热点字段加组合索引,不建单列索引
- 可扩展字段优先用
jsonb,避免频繁 DDL - 统计类能力异步化,不在主事务完成
- 状态流转字段单独建索引,便于任务扫描
- 重要请求加业务唯一键:
submit_token、task_no
六、文档治理:SpringDoc + Knife4j 的原理与企业级配置
6.1 运行原理
SpringDoc 启动扫描:
@RestController、@RequestMapping等注解@Schema/@Operation/@Parameter- Java Bean 结构、Validation 注解
- 生成 OpenAPI 3 文档并暴露端点
Knife4j:OpenAPI 文档增强展示层,提升可读性、调试性、分组管理。
6.2 Maven 依赖
<properties>
<java.version>21</java.version>
<spring-boot.version>3.3.0</spring-boot.version>
<springdoc.version>2.3.0</springdoc.version>
<knife4j.version>4.4.0</knife4j.version>
<spring-ai.version>1.0.2</spring-ai.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version>
</dependency>
</dependencies>
关键点:Spring Boot 3 必须用 Jakarta 版 Knife4j Starter,避免
javax.servlet冲突。
6.3 OpenAPI 配置
package com.example.survey.doc;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI surveyOpenApi() {
SecurityScheme bearerScheme = new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.name("Authorization")
.description("JWT access token");
return new OpenAPI()
.info(new Info()
.title("企业级智能问卷系统 API")
.version("v1.0.0")
.description("覆盖问卷管理、答卷提交、AI 生成与分析等接口")
.contact(new Contact()
.name("平台架构组")
.email("arch@example.com"))
.license(new License()
.name("Apache 2.0")
.url("https://www.apache.org/licenses/LICENSE-2.0")))
.components(new Components().addSecuritySchemes("bearerAuth", bearerScheme))
.addSecurityItem(new SecurityRequirement().addList("bearerAuth"));
}
@Bean
public GroupedOpenApi surveyApi() {
return GroupedOpenApi.builder()
.group("01-survey")
.packagesToScan("com.example.survey.api.controller")
.pathsToMatch("/api/surveys/**")
.build();
}
@Bean
public GroupedOpenApi responseApi() {
return GroupedOpenApi.builder()
.group("02-response")
.packagesToScan("com.example.survey.api.controller")
.pathsToMatch("/api/responses/**")
.build();
}
@Bean
public GroupedOpenApi aiApi() {
return GroupedOpenApi.builder()
.group("03-ai")
.packagesToScan("com.example.survey.api.controller")
.pathsToMatch("/api/ai/**")
.build();
}
}
6.4 application.yml 配置
server:
port: 8080
shutdown: graceful
tomcat:
threads:
max: 200
min-spare: 20
accept-count: 1000
springdoc:
api-docs:
enabled: true
path: /v3/api-docs
swagger-ui:
enabled: true
path: /swagger-ui.html
tags-sorter: alpha
operations-sorter: method
display-request-duration: true
doc-expansion: none
show-actuator: false
knife4j:
enable: true
production: false
basic:
enable: true
username: admin
password: ${KNIFE4J_PASSWORD:changeMe}
setting:
language: zh_cn
enable-version: true
enable-document-manage: true
enable-swagger-models: true
enable-footer: false
6.5 文档治理最佳实践
- 按业务域分组,不按开发人员分组
- 对外接口补齐
@Operation、@Schema、错误码说明 - DTO 不直接暴露数据库实体
- 生产文档权限控制,不裸露公网
- 网关统一聚合多服务 OpenAPI 文档
七、接口设计:企业级 API 不只是 “能调用”
7.1 统一返回结构
package com.example.survey.common;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "统一响应体")
public record ApiResponse<T>(
@Schema(description = "业务码", example = "0")
String code,
@Schema(description = "业务消息", example = "success")
String message,
@Schema(description = "响应数据")
T data,
@Schema(description = "链路追踪ID", example = "3f8cfbc3f4174e42")
String traceId
) {
public static <T> ApiResponse<T> success(T data, String traceId) {
return new ApiResponse<>("0", "success", data, traceId);
}
public static <T> ApiResponse<T> fail(String code, String message, String traceId) {
return new ApiResponse<>(code, message, null, traceId);
}
}
7.2 创建问卷接口
package com.example.survey.api.controller;
import com.example.survey.api.request.CreateSurveyRequest;
import com.example.survey.api.response.SurveyDetailResponse;
import com.example.survey.application.SurveyCommandApplicationService;
import com.example.survey.common.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
@Tag(name = "问卷管理")
@RestController
@RequestMapping("/api/surveys")
public class SurveyController {
private final SurveyCommandApplicationService surveyCommandApplicationService;
public SurveyController(SurveyCommandApplicationService surveyCommandApplicationService) {
this.surveyCommandApplicationService = surveyCommandApplicationService;
}
@Operation(summary = "创建问卷草稿")
@PostMapping
public ApiResponse<SurveyDetailResponse> create(@Valid @RequestBody CreateSurveyRequest request) {
SurveyDetailResponse response = surveyCommandApplicationService.createDraft(request);
return ApiResponse.success(response, TraceIdHolder.get());
}
}
7.3 创建请求 DTO
package com.example.survey.api.request;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import java.util.List;
@Schema(description = "创建问卷请求")
public class CreateSurveyRequest {
@NotBlank
@Size(max = 256)
@Schema(description = "问卷标题", example = "2026 年客户满意度调研")
private String title;
@Size(max = 2000)
@Schema(description = "问卷描述")
private String description;
@Valid
@NotEmpty
@ArraySchema(schema = @Schema(implementation = CreateQuestionRequest.class))
private List<CreateQuestionRequest> questions;
// getter/setter 省略
}
7.4 全局异常处理
package com.example.survey.api.advice;
import com.example.survey.common.ApiResponse;
import com.example.survey.common.BusinessException;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiResponse<Void>> handleBusiness(BusinessException ex) {
return ResponseEntity.ok(ApiResponse.fail(ex.getCode(), ex.getMessage(), TraceIdHolder.get()));
}
@ExceptionHandler({MethodArgumentNotValidException.class, BindException.class})
public ResponseEntity<ApiResponse<Void>> handleValidation(Exception ex, HttpServletRequest request) {
return ResponseEntity.badRequest()
.body(ApiResponse.fail("PARAM_INVALID", "请求参数不合法", TraceIdHolder.get()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleUnknown(Exception ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.fail("INTERNAL_ERROR", "系统开小差了,请稍后重试", TraceIdHolder.get()));
}
}
八、Spring AI 深入实战:从 “模型调用” 到 “AI 编排能力”
8.1 Spring AI 核心价值
提供统一编排抽象,将 AI 纳入企业架构治理,避免散落厂商 SDK 调用代码。企业落地关注 4 点:Prompt 模板化、模型路由、结构化输出、容错治理。
8.2 配置示例
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
base-url: ${OPENAI_BASE_URL:https://api.openai.com}
chat:
options:
model: gpt-4o-mini
temperature: 0.4
max-tokens: 3000
8.3 AI 生成请求对象
package com.example.survey.ai.dto;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
public record GenerateSurveyCommand (
@NotBlank String topic,
String audience,
String objective,
@Min(1) @Max(50) int questionCount,
boolean includeOpenQuestion
) {}
8.4 结构化输出
package com.example.survey.ai.dto;
import java.util.List;
public record AiSurveyDraft (
String title,
String description,
List<AiQuestionDraft> questions
) {}
public record AiQuestionDraft (
Integer no,
String type,
String title,
Boolean required,
List<String> options
) {}
8.5 生产级 AI 服务实现
package com.example.survey.ai.service;
import com.example.survey.ai.dto.AiSurveyDraft;
import com.example.survey.ai.dto.GenerateSurveyCommand;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Service
public class SurveyAiGenerationService {
private final ChatClient chatClient;
private final ObjectMapper objectMapper;
public SurveyAiGenerationService(ChatClient.Builder chatClientBuilder, ObjectMapper objectMapper) {
this.chatClient = chatClientBuilder.build();
this.objectMapper = objectMapper;
}
public AiSurveyDraft generateDraft(GenerateSurveyCommand command) {
String content = chatClient.prompt()
.system("""
你是一名企业问卷设计专家。
输出必须是合法 JSON,不输出 Markdown 和解释文字。
JSON 结构:{"title":"","description":"","questions":[{"no":1,"type":"SINGLE|MULTIPLE|TEXT","title":"","required":true,"options":["A","B"]}]}
""")
.user("""
主题:%s
目标人群:%s
调研目标:%s
题目数量:%d
是否包含开放题:%s
""".formatted(command.topic(), command.audience(), command.objective(), command.questionCount(), command.includeOpenQuestion()))
.call()
.content();
try {
return objectMapper.readValue(content, AiSurveyDraft.class);
} catch (JsonProcessingException e) {
throw new IllegalStateException("AI 响应解析失败", e);
}
}
}
8.6 生产级补强
- 响应 JSON Schema 校验
- 模型超时、重试、熔断、降级
- 成本审计、token 统计
- Prompt 版本记录、结果脱敏
九、AI 异步化改造:高并发场景的关键架构升级
9.1 同步 AI 调用问题
- 请求耗时长、Servlet 线程占满
- 吞吐量下降、模型抖动拖垮主业务
9.2 推荐链路
前端请求 → 创建任务 → MQ 异步消费 → 调用 AI → 结果入库 → 通知前端
9.3 创建任务接口
package com.example.survey.api.controller;
import com.example.survey.ai.dto.GenerateSurveyCommand;
import com.example.survey.ai.dto.GenerateTaskCreatedResponse;
import com.example.survey.application.AiSurveyApplicationService;
import com.example.survey.common.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/ai")
public class AiSurveyController {
private final AiSurveyApplicationService aiSurveyApplicationService;
public AiSurveyController(AiSurveyApplicationService aiSurveyApplicationService) {
this.aiSurveyApplicationService = aiSurveyApplicationService;
}
@Operation(summary = "提交 AI 问卷生成任务")
@PostMapping("/survey-generation-tasks")
public ApiResponse<GenerateTaskCreatedResponse> createTask(@Valid @RequestBody GenerateSurveyCommand command) {
return ApiResponse.success(aiSurveyApplicationService.submitTask(command), TraceIdHolder.get());
}
}
9.4 应用服务:创建任务并发消息
package com.example.survey.application;
import com.example.survey.ai.dto.GenerateSurveyCommand;
import com.example.survey.ai.dto.GenerateTaskCreatedResponse;
import com.example.survey.domain.task.SurveyGenerationTask;
import com.example.survey.domain.task.SurveyGenerationTaskRepository;
import com.example.survey.infrastructure.mq.AiTaskProducer;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.UUID;
@Service
public class AiSurveyApplicationService {
private final SurveyGenerationTaskRepository repository;
private final AiTaskProducer aiTaskProducer;
public AiSurveyApplicationService(SurveyGenerationTaskRepository repository, AiTaskProducer aiTaskProducer) {
this.repository = repository;
this.aiTaskProducer = aiTaskProducer;
}
@Transactional
public GenerateTaskCreatedResponse submitTask(GenerateSurveyCommand command) {
String taskNo = "AIT" + UUID.randomUUID().toString().replace("-", "");
SurveyGenerationTask task = SurveyGenerationTask.create(taskNo, command);
repository.save(task);
aiTaskProducer.send(taskNo);
return new GenerateTaskCreatedResponse(taskNo, task.getTaskStatus().name());
}
}
9.5 Kafka 消费者
package com.example.survey.infrastructure.mq;
import com.example.survey.application.AiTaskExecutionService;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
@Component
public class AiTaskConsumer {
private final AiTaskExecutionService aiTaskExecutionService;
public AiTaskConsumer(AiTaskExecutionService aiTaskExecutionService) {
this.aiTaskExecutionService = aiTaskExecutionService;
}
@KafkaListener(topics = "survey.ai.generation", groupId = "survey-ai-worker")
public void consume(String taskNo) {
aiTaskExecutionService.execute(taskNo);
}
}
9.6 执行服务:幂等 + 状态机控制
package com.example.survey.application;
import com.example.survey.ai.dto.AiSurveyDraft;
import com.example.survey.ai.service.SurveyAiGenerationService;
import com.example.survey.domain.task.SurveyGenerationTask;
import com.example.survey.domain.task.SurveyGenerationTaskRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class AiTaskExecutionService {
private final SurveyGenerationTaskRepository repository;
private final SurveyAiGenerationService surveyAiGenerationService;
public AiTaskExecutionService (SurveyGenerationTaskRepository repository,
SurveyAiGenerationService surveyAiGenerationService) {
this.repository = repository;
this.surveyAiGenerationService = surveyAiGenerationService;
}
@Transactional
public void execute(String taskNo) {
SurveyGenerationTask task = repository.findByTaskNo(taskNo)
.orElseThrow(() -> new IllegalArgumentException("任务不存在"));
if (!task.canExecute()) {
return;
}
task.markRunning();
repository.save(task);
try {
AiSurveyDraft draft = surveyAiGenerationService.generateDraft(task.toCommand());
task.markSuccess(draft);
} catch (Exception ex) {
task.markFail(ex.getMessage());
}
repository.save(task);
}
}
十、高并发答卷提交设计
10.1 高峰期压力点
瞬时提交峰值、重复提交、恶意刷接口、热点问卷、下游触发逻辑
10.2 设计原则
主链路只做必要写入、提交幂等、限流防刷、统计最终一致
10.3 提交接口请求
package com.example.survey.api.request;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import java.util.List;
public class SubmitResponseRequest {
@NotBlank
private String surveyId;
@NotBlank
private String submitToken;
@Valid
@NotEmpty
private List<SubmitAnswerRequest> answers;
// getter/setter 省略
}
10.4 Redis 幂等控制
package com.example.survey.infrastructure.idempotent;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.Duration;
@Component
public class SubmitIdempotentGuard {
private final StringRedisTemplate stringRedisTemplate;
public SubmitIdempotentGuard(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public boolean tryAcquire(String submitToken) {
String key = "survey:submit:idempotent:" + submitToken;
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(key, "1", Duration.ofMinutes(10));
return Boolean.TRUE.equals(success);
}
}
10.5 应用服务:提交答卷
package com.example.survey.application;
import com.example.survey.api.request.SubmitResponseRequest;
import com.example.survey.common.BusinessException;
import com.example.survey.domain.response.ResponseSheet;
import com.example.survey.domain.response.ResponseSheetRepository;
import com.example.survey.infrastructure.idempotent.SubmitIdempotentGuard;
import com.example.survey.infrastructure.mq.DomainEventProducer;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class ResponseApplicationService {
private final SubmitIdempotentGuard submitIdempotentGuard;
private final ResponseSheetRepository responseSheetRepository;
private final DomainEventProducer domainEventProducer;
public ResponseApplicationService (SubmitIdempotentGuard submitIdempotentGuard,
ResponseSheetRepository responseSheetRepository,
DomainEventProducer domainEventProducer) {
this.submitIdempotentGuard = submitIdempotentGuard;
this.responseSheetRepository = responseSheetRepository;
this.domainEventProducer = domainEventProducer;
}
@Transactional
public Long submit(SubmitResponseRequest request) {
if (!submitIdempotentGuard.tryAcquire(request.getSubmitToken())) {
throw new BusinessException("REPEAT_SUBMIT", "请勿重复提交");
}
if (responseSheetRepository.existsBySubmitToken(request.getSubmitToken())) {
throw new BusinessException("REPEAT_SUBMIT", "请勿重复提交");
}
ResponseSheet responseSheet = ResponseSheet.createFrom(request);
responseSheetRepository.save(responseSheet);
domainEventProducer.sendResponseSubmitted(responseSheet.getId(), responseSheet.getSurveyId());
return responseSheet.getId();
}
}
10.6 双重幂等
- 第一层:Redis 快速拦截
- 第二层:数据库唯一键兜底
十一、高并发查询优化
11.1 热点查询场景
问卷详情、活动列表、实时统计、第三方拉取
11.2 缓存策略
适合缓存:已发布问卷、题目、选项、统计快照不适合缓存:强一致数据、个性化视图、高频写数据
11.3 问卷详情缓存实现
package com.example.survey.application.query;
import com.example.survey.api.response.SurveyDetailResponse;
import com.example.survey.domain.survey.SurveyRepository;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class SurveyQueryApplicationService {
private final SurveyRepository surveyRepository;
public SurveyQueryApplicationService(SurveyRepository surveyRepository) {
this.surveyRepository = surveyRepository;
}
@Cacheable(cacheNames = "survey:detail", key = "#surveyId", unless = "#result == null")
public SurveyDetailResponse getPublishedDetail(Long surveyId) {
return surveyRepository.findPublishedDetail(surveyId)
.orElseThrow(() -> new IllegalArgumentException("问卷不存在或未发布"));
}
}
11.4 缓存更新
变更后删除缓存,下次读取自动重建
11.5 防止缓存击穿
热点预热、随机过期、互斥锁、二级缓存
十二、高并发线程模型
12.1 Java 21 虚拟线程价值
适合 I/O 密集型场景,提升吞吐
12.2 开启虚拟线程
spring:
threads:
virtual:
enabled: true
自定义执行器:
package com.example.survey.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Configuration
public class VirtualThreadConfig {
@Bean(destroyMethod = "close")
public ExecutorService applicationTaskExecutor() {
return Executors.newVirtualThreadPerTaskExecutor();
}
}
12.3 资源隔离
隔离:API 主请求、AI 调用、MQ 消费、报表导出线程
十三、连接池与数据库调优
13.1 HikariCP 配置
spring:
datasource:
url: jdbc:postgresql://localhost:5432/ai_survey
username: survey_app
password: ${DB_PASSWORD}
hikari:
minimum-idle: 10
maximum-pool-size: 50
idle-timeout: 600000
max-lifetime: 1800000
connection-timeout: 3000
validation-timeout: 1000
leak-detection-threshold: 5000
13.2 SQL 优化
避免 N+1、覆盖索引、游标分页、统计异步化
十四、限流、防刷、熔断、降级
14.1 基于 Bucket4j 限流
package com.example.survey.infrastructure.ratelimit;
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Refill;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class LocalRateLimiter {
private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();
public boolean tryConsume(String key) {
Bucket bucket = buckets.computeIfAbsent(key, k ->
Bucket.builder()
.addLimit(Bandwidth.classic(20, Refill.greedy(20, Duration.ofMinutes(1))))
.build());
return bucket.tryConsume(1);
}
}
14.2 Resilience4j 容错
package com.example.survey.ai.service;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.retry.annotation.Retry;
import io.github.resilience4j.timelimiter.annotation.TimeLimiter;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
@Service
public class ResilientAiGatewayService {
private final SurveyAiGenerationService delegate;
public ResilientAiGatewayService(SurveyAiGenerationService delegate) {
this.delegate = delegate;
}
@Retry(name = "aiGeneration")
@CircuitBreaker(name = "aiGeneration", fallbackMethod = "fallback")
@TimeLimiter(name = "aiGeneration")
public CompletableFuture<String> generateAsync(String topic) {
return CompletableFuture.supplyAsync(() ->
delegate.generateDraft(new com.example.survey.ai.dto.GenerateSurveyCommand(
topic, "通用用户", "通用调研", 10, true)).toString());
}
public CompletableFuture<String> fallback(String topic, Throwable throwable) {
return CompletableFuture.completedFuture("""
{"title":"AI 暂不可用,请稍后重试","description":"fallback","questions":[]}
""");
}
}
十五、分布式事务与最终一致性
采用:本地事务 + Outbox 事件表 + MQ 异步投递 + 下游幂等消费
十六、生产级代码补强
16.1 乐观锁字段
package com.example.survey.infrastructure.entity;
import jakarta.persistence.*;
@Entity
@Table(name = "t_survey")
public class SurveyEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 256)
private String title;
@Column(nullable = false, length = 32)
private String status;
@Version
private Integer version;
// getter/setter 省略
}
16.2 发布领域逻辑
package com.example.survey.domain.survey;
import com.example.survey.common.BusinessException;
public class Survey {
private Long id;
private SurveyStatus status;
public void publish() {
if (status == SurveyStatus.PUBLISHED) {
throw new BusinessException("SURVEY_ALREADY_PUBLISHED", "问卷已发布");
}
if (status == SurveyStatus.CLOSED) {
throw new BusinessException("SURVEY_CLOSED", "已关闭问卷不能重新发布");
}
this.status = SurveyStatus.PUBLISHED;
}
}
十七、可观测性建设
17.1 核心指标
API:QPS、P95/P99、错误率、限流次数AI:成功率、耗时、重试、降级、token 消耗数据:连接池、慢查询、Redis 命中率、MQ 堆积
17.2 自定义指标
package com.example.survey.observability;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Component;
@Component
public class AiMetricsCollector {
private final Counter successCounter;
private final Counter failCounter;
public AiMetricsCollector(MeterRegistry meterRegistry) {
this.successCounter = Counter.builder("survey_ai_generation_success_total")
.description("AI 生成成功次数")
.register(meterRegistry);
this.failCounter = Counter.builder("survey_ai_generation_fail_total")
.description("AI 生成失败次数")
.register(meterRegistry);
}
public void markSuccess() {
successCounter.increment();
}
public void markFail() {
failCounter.increment();
}
}
十八、安全设计
- Spring Security + JWT 鉴权
- 租户隔离、防重放、参数校验、脱敏
- Prompt 注入防护、文档权限、导出限流
十九、真实案例:峰值改造效果
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 问卷详情接口 P99 | 420ms | 45ms |
| 答卷提交接口 P99 | 780ms | 130ms |
| AI 生成接口同步耗时 | 8s+ | 前台返回 < 80ms |
| 峰值吞吐 | 1200 QPS | 6500 QPS |
| 导出对主链路影响 | 明显 | 基本隔离 |
二十、部署与发布
20.1 Dockerfile
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY target/ai-survey-platform.jar app.jar
ENV JAVA_OPTS="-Xms512m -Xmx512m -XX:+UseG1GC"
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
20.2 Kubernetes Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: ai-survey-platform
spec:
replicas: 3
selector:
matchLabels:
app: ai-survey-platform
template:
metadata:
labels:
app: ai-survey-platform
spec:
containers:
- name: app
image: registry.example.com/ai-survey-platform:1.0.0
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: prod
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "2Gi"
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
二十一、测试策略
单元测试、集成测试、契约测试、压测、故障演练
二十二、文章小结
- 文档自动化是接口治理起点
- AI 调用必须任务化、异步化、可治理
- 高并发 = 缩短主链路 + 隔离慢依赖 + 异步非关键逻辑
- 幂等、限流、防刷、缓存、快照是稳定性基石
- 统计分析走事件驱动,不依赖数据库
- 虚拟线程不替代连接池与资源隔离
- 可观测、安全、灰度是可运营关键
- 企业设计核心:可用性、演进性、治理能力取舍
二十三、完整版参考配置清单
server:
port: 8080
shutdown: graceful
tomcat:
accept-count: 1000
threads:
max: 200
min-spare: 20
spring:
application:
name: ai-survey-platform
threads:
virtual:
enabled: true
datasource:
url: jdbc:postgresql://postgres:5432/ai_survey
username: survey_app
password: ${DB_PASSWORD}
hikari:
minimum-idle: 10
maximum-pool-size: 50
connection-timeout: 3000
validation-timeout: 1000
idle-timeout: 600000
max-lifetime: 1800000
leak-detection-threshold: 5000
data:
redis:
host: redis
port: 6379
timeout: 2000ms
kafka:
bootstrap-servers: kafka:9092
consumer:
group-id: survey-ai-worker
auto-offset-reset: latest
enable-auto-commit: false
max-poll-records: 50
listener:
ack-mode: manual
ai:
openai:
api-key: ${OPENAI_API_KEY}
base-url: ${OPENAI_BASE_URL:https://api.openai.com}
chat:
options:
model: gpt-4o-mini
temperature: 0.4
max-tokens: 3000
management:
endpoints:
web:
exposure:
include: health,info,prometheus,metrics
endpoint:
health:
probes:
enabled: true
springdoc:
api-docs:
enabled: true
path: /v3/api-docs
swagger-ui:
enabled: true
path: /swagger-ui.html
tags-sorter: alpha
operations-sorter: method
doc-expansion: none
display-request-duration: true
knife4j:
enable: true
production: false
basic:
enable: true
username: admin
password: ${KNIFE4J_PASSWORD}
setting:
language: zh_cn
enable-document-manage: true
enable-version: true
enable-swagger-models: true
enable-footer: false
logging:
level:
root: info
com.example.survey: info
二十四、结语
Demo 级集成简单,企业级落地关键:
- 让 AI 能力不破坏主链路稳定性
- 让接口文档成为协作资产
- 高并发下保持可用、可扩展、可观测、可治理