ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Springboot+Vue在线问卷系统:核心设计与部署避坑指南

Springboot+Vue在线问卷系统:核心设计与部署避坑指南 简介这是一份基于Springboot与Vue技术栈开发的在线问卷调查系统完整项目面向计算机专业毕业设计、课程设计或期末大作业场景同时也适合Java学习者进行实战练习。系统涵盖用户登录认证、问卷设计编辑、在线填写、数据收集与结果展示等核心功能前后端分离架构清晰可直接作为毕设项目使用。资源包共755个文件压缩包大小约18.28MB包含java后端源码、vue前端组件、js逻辑文件、css样式、sql数据库脚本、bat启动脚本以及开发说明文档与部署、讲解视频等各类型文件分工明确便于按模块理解与二次开发。目前已吸引446人学习浏览。整体经过严格调试提供部署视频、代码讲解视频和全套软件能够帮助使用者快速搭建环境、理解设计思路并完成项目落地兼具学习与工程实用价值。1. 在线问卷调查系统不只是“做个表单”这套SpringbootVue方案解决什么问题在第三方问卷平台上收集客户反馈导出数据要开会员、问卷链接发进微信群总被广告拦截、想跟自家用户体系打通又完全没门路——这才是很多团队想自建一套“基于SpringbootVue的在线问卷调查系统”的真实动力。它不是一个表单生成器而是把问卷从创建、发布、填写、统计到导出整条链路都做成私有化服务Spring Boot 负责把问卷接口和统计结果稳定暴露出来Vue 负责在浏览器里把不同题型动态渲染成可填写页面。这套方案适合两类人业务侧想摆脱外部平台、把问卷和内部用户数据打通的团队以及需要一份完整课设或内部工具底座的开发者。它能跑通的最短路径是三张表、四个接口和一个按题型映射组件的前端页面。2. 技术选型与数据模型SpringbootVue为什么好搭问卷表结构怎样设计才不返工2.1 从Springboot项目结构说起这个选型组合的长处和边界Spring Boot 能成为这类系统的默认起点是因为它把“跑起来”这件事的成本压到了极低内置 Tomcat、JPA/MyBatis-Plus 都有成熟集成、配置项收敛得很整齐做一个管理后台类的应用几乎不需要关心容器和部署细节。Vue 这边的好处则体现在动态渲染题型上——问卷题目类型经常是“单选、多选、填空、打分、矩阵量表”混着来Vue 的组件映射和 v-model 机制正好适合“根据后端下发的题目元数据生成对应控件”这种需求。两个框架可以前后端分离开发也可以在最后把前端构建产物放进 Spring Boot 的静态目录打成一个 jar 直接部署。这个组合的边界也很清晰它适合公司内部调研、课程设计、中小规模私域问卷不适合当百万级并发答题引擎。真要上大规模需要把答卷提交改成 MQ 异步落库、用 Redis 做计数缓存那已经是另一个体量的架构。下面要说的设计和实现都按照“团队两三人、日答卷量几百到几千、数据实时可统计”这个量级来组织这个量级下追求的不是分布式能力而是结构清晰、改动成本低。项目结构一般是前后端分离两个工程后端按 controller、service、repository、entity 分层前端按 views、router、api、components 划分。后端选 JPA 还是 MyBatis-Plus 更多是团队习惯问题我用 JPA 多一些因为这类系统的实体关系简单JPA 可以直接声明唯一约束来做答卷幂等省掉不少手工 SQL。如果团队更熟 MyBatis-Plus表结构设计思路完全通用不影响后续内容。2.2 四张核心表问卷、题目、答卷的取舍问卷系统的表结构设计有一个流派之争一派把题型和选项都拆成独立表一派用 JSON 字段承载可变结构。我站在 JSON 派因为问卷题型的天性就是“不稳定”——今天加一个矩阵量表明天加一个 NPS 打分如果每种题型都建表后端会陷入无穷无尽的 DDL 和兼容逻辑。用 JSON 存选项和答案代价是统计时需要在内存里聚合但在这个量级完全够用。表关键字段职责surveyid, title, description, status, start_time, end_time, snapshot_json问卷主表status 控制生命周期questionid, survey_id, type, title, required, sort_no, options_json题目表题型由 type 区分选项放 JSONanswer_recordid, survey_id, user_key, answer_json, submit_time答卷主表user_key 做防重答案放 JSON这里最关键的两个决定第一题目表里的选项用 options_json 存比如[{label:满意,value:1},{label:不满意,value:2}]这样单选、多选、下拉、矩阵的选项都能用同一张表承载。第二答卷表用 answer_json 存“题目ID 到 答案”的映射比如{12:1,15:[a,b],18:这是自由文本}问卷发布后改题目不会破坏历史数据解析时按快照结构去读就行。这种设计有一个安全感的代价数据库层面没法对答案字段做 group by 统计所有报表都要把 answer_record 捞出来在服务内存里聚合。但只要单份问卷回收量在几千份以内这个操作的耗时是几十毫秒级完全不是瓶颈。真正要注意的是给 answer_record 加上(survey_id, user_key)唯一约束这个索引既是防重的底线也是统计查询findBySurveyId的加速手段。2.3 状态机和权限边界草稿、发布、关闭背后的规则问卷不是建好就能填的它的生命周期需要被严格管理。我用一个 int 字段 status 表示0 草稿、1 发布、2 关闭。状态流转只有三条合法路径草稿可以发布、发布可以手动关闭、关闭的问卷不能再回到发布态。这个状态机虽然简单但必须在后端接口层强制校验不能只靠前端按钮隐藏。三条规则写清楚只有草稿状态允许增删改题目。发布后题目被快照锁定管理员只能改标题和描述。发布操作要把当前题目列表序列化进 survey.snapshot_json统计和答卷解析一律基于快照而不是实时读 question 表。发布时可以设定 end_time由定时任务把过期问卷自动置为关闭用户访问已关闭问卷时填写页直接提示“问卷已截止”。权限边界上后台管理接口创建、编辑、统计需要登录填写提交接口匿名可用。如果需要实名填写就把 user_key 换成登录后的 userId匿名模式下由前端生成一个随机字符串作为 user_key虽然防不了刷但至少能给每个设备留下稳定标识。3. 后端实现三张表、四个接口把问卷从创建到统计跑通3.1 实体类与建表JPA注解怎么写才不会有坑先看两个核心实体的写法。Survey 表除了基本字段我特意留了 snapshotJson这是发布一瞬间的题目全量快照后面所有统计都从这里读。Entity Table(name survey) public class Survey { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; NotBlank(message 问卷标题不能为空) Size(max 80, message 标题最长80个字符) private String title; Column(columnDefinition text) private String description; private Integer status; // 0草稿 1发布 2关闭 private LocalDateTime startTime; private LocalDateTime endTime; private LocalDateTime createTime; Column(columnDefinition text) private String snapshotJson; // 发布时写入题目快照 }逻辑说明status 用 Integer 而不是枚举是为了查询和未来扩展方便代价是代码可读性差一些所以业务代码里应该定义一个SurveyStatus常量类来约束取值。标题的NotBlank和Size属于基础校验真正值得关注的是 LocalDateTime 字段——Spring Boot 默认的 Jackson 配置对 LocalDateTime 序列化格式是 ISO 字符串前端拿到的是2025-01-01T10:00:00而不是2025-01-01 10:00:00。如果想让时间显示成习惯格式需要在 application.yml 里配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8注意这个配置对java.util.Date生效对 LocalDateTime 不生效。LocalDateTime 必须单独加JsonFormat(pattern yyyy-MM-dd HH:mm:ss)。这个细节经常被忽略前端拿到的数据格式不对就会报一堆解析错误属于“配置两分钟排查两小时”的典型坑。Question 表的关键在于组合索引和 optionsJsonEntity Table(name question, indexes { Index(name idx_survey_sort, columnList survey_id, sort_no) }) public class Question { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; private Long surveyId; private String type; // single multi text score matrix private String title; private Boolean required; private Integer sortNo; Column(columnDefinition text) private String optionsJson; // [{label:满意,value:1}] }逻辑说明(survey_id, sort_no)组合索引保证按问卷查题目时排序高效这在发布快照和编辑回显时都会用到。optionsJson 用 text 类型避免长度限制矩阵题这类复杂题型的选项结构会比其他题型大不少varchar(255) 一定不够。type 字段虽然可以写成枚举但我建议在数据库里存字符串因为前端组件映射、后端统计分支都要拿这个字符串做判断存枚举数字反而要多一次翻译。答案表的唯一约束是整个系统防重复提交的最后防线Entity Table(name answer_record, uniqueConstraints { UniqueConstraint(name uk_survey_user, columnNames {survey_id, user_key}) }) public class AnswerRecord { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; private Long surveyId; private String userKey; // 匿名设备ID或登录用户ID Column(columnDefinition text) private String answerJson; // {12:1,15:[a,b],18:自由文本} private LocalDateTime submitTime; }这个唯一约束的意义是即使应用层逻辑漏了判重数据库也会因为主键冲突直接拒绝第二次插入而不是默默写进一条脏数据。注意 user_key 在匿名模式下也不能为空否则唯一约束对 NULL 值不生效等于白设。3.2 创建问卷与题型校验Controller 里的参数细节创建问卷的接口不需要接收题目列表一次请求只做一件事避免一个超长 JSON 让接口变得又臭又难排查。创建接口的核心逻辑就三行PostMapping(/api/survey) public Survey create(Valid RequestBody SurveyDTO dto) { Survey survey new Survey(); survey.setTitle(dto.getTitle()); survey.setDescription(dto.getDescription()); survey.setStatus(0); // 新建问卷只能是草稿 survey.setCreateTime(LocalDateTime.now()); return surveyRepository.save(survey); }参数说明DTO 里只需要 title 和 descriptionstatus、createTime 都由后端填充不允许前端传入避免有人通过构造请求直接创建“已发布”状态的问卷。这里Valid会触发 3.1 节里的NotBlank和Size校验校验失败时 Spring Boot 返回 400 和默认错误结构前端 axios 拦截器统一处理即可。添加题目的接口要带状态判断这是发布锁定的第一道闸门PostMapping(/api/survey/{id}/questions) Transactional public ListQuestion addQuestion(PathVariable Long id, Valid RequestBody QuestionDTO q) { Survey survey surveyRepository.findById(id) .orElseThrow(() - new IllegalArgumentException(问卷不存在)); if (survey.getStatus() ! 0) { throw new IllegalStateException(问卷已发布不能修改题目); } Question question new Question(); question.setSurveyId(id); question.setType(q.getType()); question.setTitle(q.getTitle()); question.setRequired(q.getRequired()); question.setSortNo(q.getSortNo()); question.setOptionsJson(q.getOptionsJson()); questionRepository.save(question); return questionRepository.findBySurveyIdOrderBySortNo(id); }逻辑说明返回排序后的完整题目列表让前端编辑页能直接刷新左侧题目栏不用再单独拉一次接口。Transactional放在这里是因为保存题目和查询列表是两步操作虽然现在只有一张表写入但后续如果再扩展题目副本表事务就能兜住部分失败的情况。发布接口是状态机的核心快照在这里写入PutMapping(/api/survey/{id}/publish) Transactional public Survey publish(PathVariable Long id) { Survey survey surveyRepository.findById(id) .orElseThrow(() - new IllegalArgumentException(问卷不存在)); if (survey.getStatus() ! 0) { throw new IllegalStateException(只有草稿状态可以发布); } if (questionRepository.countBySurveyId(id) 0) { throw new IllegalStateException(空问卷不能发布); } ListQuestion questions questionRepository.findBySurveyIdOrderBySortNo(id); survey.setSnapshotJson(JSON.toJSONString(questions)); // 题目快照 survey.setStatus(1); survey.setStartTime(LocalDateTime.now()); return surveyRepository.save(survey); }参数说明快照的意义在于问卷发布后题目表和选项内容都可能因为人工误操作而变化但统计和答卷查看必须基于填卷人当时看到的题目版本。这个设计在后续改版、复制问卷时尤其有用——老问卷的快照永远留在旧记录里新问卷用新的快照互不污染。3.3 答卷提交与统计幂等控制怎么落在代码里提交答卷是系统里并发压力最大的接口逻辑上要做三件事校验问卷状态、生成或读取 user_key、落库。代码长这样PostMapping(/api/survey/{id}/submit) Transactional public AnswerRecord submit(PathVariable Long id, RequestHeader(value X-User-Key, required false) String userKey, RequestBody MapString, Object answerJson) { if (userKey null || userKey.isBlank()) { userKey UUID.randomUUID().toString(); // 匿名兜底仅用于留痕 } Survey survey surveyRepository.findById(id) .orElseThrow(() - new IllegalArgumentException(问卷不存在)); if (survey.getStatus() ! 1) { throw new IllegalStateException(问卷不在填写期); } AnswerRecord record new AnswerRecord(); record.setSurveyId(id); record.setUserKey(userKey); record.setAnswerJson(JSON.toJSONString(answerJson)); record.setSubmitTime(LocalDateTime.now()); return answerRecordRepository.save(record); }逻辑说明如果调用方没带 user_key后端用 UUID 生成一个随机串作为兜底这只能保证每条答卷有标识防不了重复提交。真正防重靠两层前端把用户标识存储在 localStorage 并随请求头发送后端依赖(survey_id, user_key)唯一约束拦截第二份答卷。如果想做到“同一设备 5 分钟内不能重复提交”需要再加一个 Redis 限频后面会专门讲。统计接口是整个后端的收尾。因为答案存在 answerJson 里SQL 的 group by 用不上常规做法是把答卷捞出来在内存里聚合并返回给图表GetMapping(/api/survey/{id}/statistics) public MapString, Object statistics(PathVariable Long id) { ListQuestion snapshotQuestions getSnapshotQuestions(id); ListAnswerRecord records answerRecordRepository.findBySurveyId(id); MapString, Object result new HashMap(); for (Question q : snapshotQuestions) { if (single.equals(q.getType()) || score.equals(q.getType())) { MapString, Integer countMap new HashMap(); for (AnswerRecord record : records) { JSONObject answer JSON.parseObject(record.getAnswerJson()); String val answer.getString(q.getId().toString()); countMap.merge(val, 1, Integer::sum); } result.put(q.getId().toString(), countMap); } // 文本题不聚合直接返回原始列表供人工查看 } return result; }参数说明这里的核心思路是把快照题目作为遍历主键而不是从 answerJson 里反推有哪些题目。因为有些题目可能发布后被跳过、有些人答题时漏答了以快照为准才能保证统计结果里每道题都在并且顺序稳定。多选和矩阵题型的聚合逻辑会复杂一些需要在内存里先拆分选项再计数量级小的时候完全顶得住。4. Vue前端落地填写页动态渲染、设计器与统计图表4.1 路由设计管理端和填写端用Vue路由怎么拆前端路由建议分成两个语义完全不同的区管理端和填写端。管理端路径带/admin前缀方便统一做登录拦截填写端路径用/s/:id这种短链接外发给用户时没有心理障碍也便于在微信、钉钉里直接打开。Vue 3 里用 vue-router 4 的写法// src/router/index.js import { createRouter, createWebHistory } from vue-router const router createRouter({ history: createWebHistory(), routes: [ { path: /admin/survey, component: () import(../views/SurveyList.vue) }, { path: /admin/survey/edit/:id, component: () import(../views/SurveyEdit.vue) }, { path: /s/:id, component: () import(../views/SurveyFill.vue) }, { path: /admin/survey/stat/:id, component: () import(../views/SurveyStat.vue) } ] })这里有个值得现在就说清楚的抉择createWebHistory 是 history 模式URL 干净但部署到 Spring Boot 静态目录后用户刷新/admin/survey/stat/12这类地址会直接 404因为后端没有对应的控制器路由。createWebHashHistory 用#分隔刷新永远不会 404代价是 URL 里多一个#。我的建议是如果确定要前后端同 jar 部署可以先不纠结用 history 模式 后端 forward 兜底这个方案在第5章会给出具体代码。如果只是本地调试或纯分离部署history 模式没有任何问题。路由守卫这里不必做得太重管理端四个路由加一个全局判断就够本地 localStorage 有 token 就放行没有就跳登录页。填写端/s/:id永远放行因为匿名填写是问卷系统的基本能力。4.2 动态题型渲染核心是组件映射表不是一堆v-if问卷填写页最忌讳的就是在页面里写十几层 v-if 判题型那会让模板膨胀到没法维护。正确做法是维护一张“题型字符串到组件”的映射表用 Vue 的动态组件component :is...渲染!-- SurveyFill.vue 核心模板 -- template div v-forq in questions :keyq.id classquestion-card p classquestion-title {{ q.sortNo }}. {{ q.title }} span v-ifq.required classrequired*/span /p component :iscomponentMap[q.type] :questionq v-modelanswers[q.id] / /div /template script setup import { ref } from vue import SingleChoice from ./questions/SingleChoice.vue import MultiChoice from ./questions/MultiChoice.vue import TextAnswer from ./questions/TextAnswer.vue const componentMap { single: SingleChoice, multi: MultiChoice, text: TextAnswer } const answers ref({}) /script逻辑说明后端返回的题目列表里每道题都带 type 字段前端直接componentMap[q.type]找到对应组件。以后加题型只需要新建一个组件并在 componentMap 里注册填写页主模板完全不用动。这是 Vue 动态组件在表单场景里最典型的应用也是这个系统扩展性的前端保障。单选组件的内部实现要特别注意 v-model 的自定义组件协议!-- questions/SingleChoice.vue -- template el-radio-group v-modellocalValue el-radio v-foropt in options :keyopt.value :labelopt.value {{ opt.label }} /el-radio /el-radio-group /template script setup import { computed } from vue const props defineProps({ question: Object, modelValue: [String, Number] }) const emit defineEmits([update:modelValue]) const options JSON.parse(props.question.optionsJson || []) const localValue computed({ get: () props.modelValue, set: v emit(update:modelValue, v) }) /script参数说明父组件写v-modelanswers[q.id]时Vue 会自动拆成modelValueprop 和update:modelValue事件。子组件必须显式声明这两个东西否则父组件的数据永远收不到回传。这种写法在单选、多选、文本题里完全一致只是内部控件不同这也是组件映射方案能成立的前提。编辑器的题目配置部分常见做法是用一个弹窗表单管理题目标题、题型、必填、选项列表左右结构左侧显示题目列表右侧显示选中题目的配置项。拖拽排序可以直接引入 sortablejs也可以先用上移下移按钮代替——功能上没区别只是体验差一点。作为内部工具按钮方案少一个依赖反而是更稳的选择。4.3 统计可视化Vue里用ECharts把统计数据变成图表统计页不复杂但有两个容易翻车的点图表容器高度和初始化时机。看代码// SurveyStat.vue 中统计图表初始化 import * as echarts from echarts import { nextTick, onMounted, onUnmounted } from vue import axios from axios let chart null async function loadStat() { const res await axios.get(/api/survey/${route.params.id}/statistics) const div document.getElementById(statChart) await nextTick() // 等待 DOM 渲染完成 chart echarts.init(div) chart.setOption({ tooltip: { trigger: item }, xAxis: { type: category, data: res.data.labels }, yAxis: { type: value }, series: [{ type: bar, data: res.data.values }] }) } onMounted(loadStat) onUnmounted(() { if (chart) chart.dispose() // 防止内存泄漏 })逻辑说明ECharts 初始化时如果容器没有宽度和高度图表会以 0 尺寸渲染最后呈现一片空白。所以容器 div 必须有显式高度比如styleheight: 400px。如果统计页里有多个图表每个图表用一个独立的 div 和独立 chart 实例不要复用。路由切换离开页面时调用 dispose 释放实例这是很容易被忽略的内存管理细节。参数说明xAxis 的 data 对应单选选项的 labelseries 的 data 对应每个选项的回收数量。后端把统计数据按题目 ID 组织前端按快照顺序遍历渲染单选用饼图或柱状图多选用横向柱状图矩阵题用热力图。文本题不画图直接以表格列表展示原始回答。5. 避坑问卷系统开发中最常翻车的5个场景5.1 发布后改了题目统计数据对不上现象问卷上线两天后管理员觉得某个选项的措辞不准确直接在管理端改了选项文字。结果统计页出现两套标签三分之一的旧答卷挂在旧选项上新选项的统计永远少一截整体数字对不上图表像劈叉一样难看。原因发布后的题目没有做快照也没有锁定机制。question 表里同一道题的选项被直接覆盖旧答卷的答案 JSON 仍然指向旧选项值统计时按当前选项标签展示导致答案和标签错位。解决发布接口里把题目列表序列化进 survey.snapshot_json统计一律基于快照。同时在编辑接口加状态判断status 不等于 0 时增删改题目直接抛异常。如果业务上确实要改题正确做法是复制一份问卷草稿再改而不是原地编辑已发布的问卷。这套机制在 3.2 节的发布代码里已经体现关键是团队要形成约定不能只靠前端隐藏按钮。5.2 同一用户刷问卷后台统计被污染现象问卷链接发进微信群后回收量在半小时内暴涨其中大量是同一设备反复提交。后台显示的回收数和真实参与人数完全对不上统计报表不敢用。原因匿名接口没有做用户标识约束。后端唯一索引虽然存在但前端没有把稳定标识传给后端每次请求都生成新的 user_key唯一约束形同虚设。填卷人只要不断刷新页面重新打开链接就能无限次提交。解决三层配合。第一层前端在 localStorage 里生成一个 uuid 作为设备标识每次提交通过请求头X-User-Key带上。第二层后端对同一 user_key 的重复提交直接返回“已提交过”并在 Redis 里做短时间窗限频比如 5 分钟内同一标识只能提交一次。第三层数据库唯一索引兜底应用层漏判时数据库也能拒绝重复插入。匿名模式挡不住“换设备刷”但能挡住绝大多数手滑和无聊操作真正要防刷就得做成登录后填写user_key 取用户 ID代价是填写门槛变高问卷回收率下降这个度需要业务方自己权衡。5.3 填写页在微信和旧浏览器里样式错乱现象Chrome 里一切正常问卷发到微信群里同事用内置浏览器打开flex 布局挤成一团字体忽大忽小选项之间间距也乱了。这就是跨浏览器支持没做透的典型翻车现场。原因问卷链接的外发场景决定了它会被微信、钉钉、企业微信等内置 WebView 打开这些内核版本往往落后于最新 Chrome。样式表里用了较新的 CSS 特性构建时又没有自动加浏览器前缀同时移动端页面缺少 viewport 配置导致页面按 980px 宽度缩放渲染。解决在 index.html 里加meta nameviewport contentwidthdevice-width, initial-scale1.0构建工具里确保 Autoprefixer 开启布局上尽量用 flex 而不是 grid必要场景给 grid 写 flex 降级。上线前用 Chrome 移动模拟器过一遍再用微信开发者工具或真实手机微信里打开实测一次。这个坑在问卷系统里特别高发因为填写页的使用场景天然偏向移动端。5.4 Vue打包后放进Springboot子路由刷新404现象npm run build后把 dist 目录复制到src/main/resources/static访问首页正常但一刷新/admin/survey/stat/12就报 Whitelabel 404。前端路由跳转没问题唯独刷新页面就炸。原因前端用的是 history 模式URL 由前端路由管理但刷新时浏览器会向 Spring Boot 发起真实 HTTP 请求。后端找不到/admin/survey/stat/12这个静态资源或 Controller 映射自然返回 404。hash 模式没有这个问题但 URL 里会多一个#。解决在 Spring Boot 里加一个转发控制器把前端路由路径统一 forward 到 index.html由前端路由接管后续解析Controller public class SpaForwardController { RequestMapping(value {/admin/**, /s/{id:[0-9]}}) public String forward() { return forward:/index.html; } }逻辑说明/admin/**覆盖管理端所有路由/s/{id:[0-9]}只匹配数字 ID 的填写页路径避免误伤其他资源。API 路径因为统一以/api开头完全不受影响。如果以后加了非管理端的页面记得在 value 里补上新路径前缀。另一种省事方案是前端直接改用 hash 模式适合不在乎 URL 美观度的内部工具但外发问卷时#在部分渠道里会被截断所以我的建议是保留 history 模式配合后端转发。5.5 提交高峰丢数据或用户提交时提示失败现象一个大型培训结束后几十人同时点提交第二天对账发现回收数比预期少了几条还有人反馈提交按钮转圈很久后提示失败。原因提交接口没有做限流瞬间流量把数据库连接池打满后续请求全部超时或者事务边界没控制好主表写入成功但明细表写入失败又没有回滚产生半截数据。解决提交接口用Transactional保证原子性这在本系统里是标准操作数据库连接池调大最小空闲连接数避免冷启动时连接不够用入口加 Redis 限频最简单的做法是令牌桶每秒放行 50 或 100 个提交请求超出直接返回“系统繁忙”。如果团队不想引 Redis也可以用 Guava 的 RateLimiter 做单机限流但多实例部署时就不准了。问卷系统一般不会有持续高并发但“瞬时峰值”是常态这个限流是性价比最高的保护。6. 进阶验证与部署技巧把前端塞进Springboot单jar压测和验收一起做整套系统开发完成后我习惯在上线前把“构建—部署—压测—验收”这四件事一次跑完避免本地好好的、一上服务器就各种联调事故。部署方式上最后选择把 Vue 构建产物放进 Spring Boot 静态目录打成单 jar这样服务器上只需要一个 Java 进程不需要单独装 Nginx也彻底绕开了前后端跨域问题。cd survey-web npm run build rm -rf ../survey-backend/src/main/resources/static/* cp -r dist/* ../survey-backend/src/main/resources/static/ cd ../survey-backend mvn clean package -DskipTests java -jar target/survey.jar --server.port8080这个流程的关键是每次前端改动后必须重新 build 并覆盖 static 目录否则线上跑的永远是上一次的页面。有些人会嫌这种方式每次都要重新打 jar但作为内部工具一个月发一次版的节奏完全够用而且单 jar 部署在故障排查时少很多环节。如果团队已经有 Nginx那也可以走前后端分离部署前端静态文件交给 NginxAPI 反代到 Spring Boot两种方式没有对错之分只看团队运维习惯。部署完成后我的验收清单一般是这样管理员创建一份包含所有题型的问卷发布后在无痕窗口里填写并提交到统计页核对每个题型的回收数用同一个浏览器再提交一次确认被防重拦截用管理端尝试编辑已发布问卷的题目确认被后端拒绝。这三步过完系统的主链路就没有大问题。压测可以用 JMeter 对/api/survey/{id}/submit发起 200 并发持续 1 分钟关注两个指标错误率和平均响应时间。如果错误率超过 1%优先检查数据库连接池和限流参数而不是盲目加机器。这些年做过好几个类似的管理系统有一个教训印象特别深早先做问卷存储时图省事把选项用逗号拼接存在题目表里单选和多选各存一份结果后来要加矩阵题整个统计逻辑全部返工改成 JSON 结构才彻底解脱。技术上不复杂但当时就是觉得“先跑起来再说”结果一个月后加倍补课。技术债这种东西早晚要还不如一开始就把数据模型按扩展性设计好。希望这套从表结构到前后端联调的思路能帮你少走这段弯路希望帮到你。本文还有配套的精品资源点击获取
返回列表