SpringAI + SpringDoc + Knife4j 构建企业级智能问卷系统

0 阅读14分钟

SpringAI + SpringDoc + Knife4j 构建企业级智能问卷系统

面向生产环境,本文以企业智能问卷平台为案例,系统讲透如何基于 Spring Boot 3、Spring AI、SpringDoc、Knife4j 构建一套可扩展、可治理、可承载高并发流量的智能问卷系统。文章不仅给出配置和代码,更重点补足架构设计、并发控制、异步化改造、缓存一致性、幂等、防刷、限流、观测与部署等工程化能力。

一、为什么企业需要 “智能问卷系统”?

在很多企业数字化场景里,问卷系统并不是一个简单的 CRUD 系统,而是同时具备以下特征的平台型能力:

  • 业务侧需要快速创建活动问卷、调研问卷、满意度问卷、NPS 问卷、招聘测评问卷
  • 运营侧需要 AI 辅助生成题目、题型建议、问卷结构优化、答卷摘要分析
  • 平台侧需要开放 API 给 CRM、OA、营销系统、SaaS 租户接入
  • 高峰期存在大量用户同时答题、提交、查询结果、导出分析报表
  • 管理后台要求文档清晰、接口规范、联调高效、权限可控、系统易扩展

这类系统进入生产环境会暴露四类典型问题:

  1. 业务复杂度上升后,单体 Controller + Service 的代码结构难以承载变化。
  2. AI 调用存在高延迟、高成本、不稳定、限流等天然问题,不能直接塞进同步请求链路。
  3. 活动高峰下,问卷查询、提交、统计分析会形成读写热点,数据库和线程池都容易被打满。
  4. 接口一多,文档和实际代码容易漂移,前后端和测试协作成本快速上涨。

企业级智能问卷系统设计目标:

  • 低耦合:业务域、AI 域、文档域、基础设施域解耦
  • 高并发:承载活动流量峰值,具备削峰、隔离、降级能力
  • 可扩展:题型、问卷模板、AI 模型、租户能力平滑演进
  • 可治理:接口标准化、链路可观测、故障可定位、成本可控制

二、项目目标与技术选型

2.1 目标定义

系统核心能力:

  • 问卷模板管理:创建、编辑、发布、下线、复制模板
  • AI 智能生成:根据业务主题自动生成问卷草稿
  • AI 答卷解析:对答卷文本做聚类、摘要、情绪分析、风险识别
  • 高并发答题:支持大规模用户提交答卷
  • 开放文档门户:自动生成 OpenAPI 文档并提供增强 UI
  • 平台治理:限流、幂等、缓存、监控、审计、安全控制

2.2 技术栈

组件版本建议作用
Java21 LTS虚拟线程、性能优化、长期支持
Spring Boot3.3.x应用基础框架
Spring Web6.xREST API
Spring Validation6.x参数校验
Spring Data JPA / MyBatis按团队偏好数据访问
Spring AI1.0.x模型统一接入层
SpringDoc OpenAPI2.3.xOpenAPI 文档生成
Knife4j4.4.xOpenAPI 可视化增强
PostgreSQL16核心业务数据存储
Redis7.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”

企业系统四层架构:

  1. 接入层
  2. 应用层
  3. 领域层
  4. 基础设施层
客户端/管理后台/第三方系统
→ 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 到数秒
  • 成功率不稳定:超时、限流、网络异常、格式错误
  • 成本敏感
  • 不适合与数据库事务强绑定

正确设计:

  1. 前台提交 “生成请求”
  2. 服务端创建任务记录
  3. 异步消费任务调用大模型
  4. 结果结构化校验后入库
  5. 轮询 / 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_tokentask_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 注入防护、文档权限、导出限流

十九、真实案例:峰值改造效果

指标改造前改造后
问卷详情接口 P99420ms45ms
答卷提交接口 P99780ms130ms
AI 生成接口同步耗时8s+前台返回 < 80ms
峰值吞吐1200 QPS6500 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

二十一、测试策略

单元测试、集成测试、契约测试、压测、故障演练

二十二、文章小结

  1. 文档自动化是接口治理起点
  2. AI 调用必须任务化、异步化、可治理
  3. 高并发 = 缩短主链路 + 隔离慢依赖 + 异步非关键逻辑
  4. 幂等、限流、防刷、缓存、快照是稳定性基石
  5. 统计分析走事件驱动,不依赖数据库
  6. 虚拟线程不替代连接池与资源隔离
  7. 可观测、安全、灰度是可运营关键
  8. 企业设计核心:可用性、演进性、治理能力取舍

二十三、完整版参考配置清单

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 能力不破坏主链路稳定性
  • 让接口文档成为协作资产
  • 高并发下保持可用、可扩展、可观测、可治理