1. 企业级在线问卷调查系统架构解析
在企业数字化转型浪潮中,数据采集与分析能力已成为核心竞争力。传统纸质问卷存在回收周期长、统计效率低、数据易失真等痛点,我们团队基于SpringBoot+Vue+MyBatis技术栈构建的企业级在线问卷调查系统,实现了问卷全生命周期的数字化管理。这套系统已在多个行业客户中落地,单项目最高承载过10万+份问卷的并发提交。
技术选型上,我们采用前后端分离架构:后端SpringBoot 2.7提供RESTful API,吞吐量实测可达1500+ QPS;前端Vue 3组合式API开发,配合Element Plus组件库,实现响应式界面;MyBatis-Plus 3.5作为ORM框架,相比原生JDBC开发效率提升40%。数据库选用MySQL 8.0,利用窗口函数优化复杂统计查询。
2. 核心功能模块设计
2.1 问卷管理引擎
问卷建模采用元数据驱动设计,核心实体包括:
- Survey(问卷元信息)
- Question(问题定义)
- Option(选项配置)
- LogicJump(逻辑跳转规则)
java复制// 问卷实体类示例
@Data
@TableName("survey")
public class Survey {
@TableId(type = IdType.AUTO)
private Long surveyId;
private String title;
private String description;
private Integer status; // 0-草稿 1-已发布 2-已归档
@JsonFormat(pattern = "yyyy-MM-dd HH:mm")
private LocalDateTime startTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm")
private LocalDateTime endTime;
// 关联问题集合
@TableField(exist = false)
private List<Question> questions;
}
题型支持方面,我们抽象出基础题型接口:
java复制public interface QuestionType {
Integer SINGLE_CHOICE = 1; // 单选
Integer MULTIPLE_CHOICE = 2; // 多选
Integer TEXT_INPUT = 3; // 填空
Integer MATRIX_SCALE = 4; // 矩阵量表
// 验证问题答案合法性
boolean validateAnswer(AnswerDTO answer);
}
2.2 权限控制系统
采用RBAC模型实现多租户隔离,关键设计点:
- 角色分级:超级管理员→租户管理员→问卷创建者→普通填写者
- 权限粒度控制到API级别,使用Spring Security注解:
java复制@PreAuthorize("hasRole('ADMIN') or @permission.check('survey:delete')")
@DeleteMapping("/{surveyId}")
public R deleteSurvey(@PathVariable Long surveyId) {
// 删除逻辑
}
- 问卷数据权限通过MyBatis拦截器自动注入SQL条件:
sql复制/* 原始SQL */
SELECT * FROM survey WHERE status = 1
/* 拦截后SQL */
SELECT * FROM survey
WHERE status = 1
AND creator_id IN (${currentUser.dataScope})
2.3 高并发提交优化
针对大规模问卷收集场景(如万人级员工满意度调查),我们采用三级缓冲策略:
- 前端防抖+本地缓存:Vue自定义指令实现300ms提交间隔控制
- 服务端异步队列:使用Redis Stream处理峰值流量
java复制// 提交处理伪代码
public R submitAnswer(AnswerDTO dto) {
if (rateLimiter.tryAcquire()) { // 令牌桶限流
redisTemplate.opsForStream().add(
"survey:submit:queue",
ObjectValueWrapper.wrap(dto)
);
return R.ok("提交进入处理队列");
}
return R.error("系统繁忙,请稍后重试");
}
- 批量入库:通过Spring Batch每小时执行一次批量插入
3. 数据库设计与优化
3.1 核心表结构
问卷主表(survey)
| 字段 | 类型 | 说明 | 索引 |
|---|---|---|---|
| survey_id | bigint | 主键 | PK |
| title | varchar(200) | 问卷标题 | IDX_title |
| template_id | bigint | 模板ID | FK |
| status | tinyint | 状态码 | IDX_status |
| answer_count | int | 答卷数 | - |
问题表(question)
sql复制CREATE TABLE `question` (
`question_id` bigint NOT NULL AUTO_INCREMENT,
`survey_id` bigint NOT NULL COMMENT '关联问卷ID',
`content` text COLLATE utf8mb4_unicode_ci,
`question_type` tinyint DEFAULT '1' COMMENT '1-单选 2-多选...',
`required` tinyint DEFAULT '1' COMMENT '是否必答',
`show_order` int DEFAULT '0' COMMENT '展示序号',
`extras` json DEFAULT NULL COMMENT '扩展配置(如选项)',
PRIMARY KEY (`question_id`),
KEY `idx_survey` (`survey_id`),
CONSTRAINT `fk_question_survey` FOREIGN KEY (`survey_id`)
REFERENCES `survey` (`survey_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
3.2 查询性能优化
- 统计报表加速:对常用分析维度创建物化视图
sql复制CREATE MATERIALIZED VIEW survey_stats_mv
REFRESH COMPLETE ON DEMAND
AS
SELECT
survey_id,
COUNT(*) as total_count,
AVG(score) as avg_score,
PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY score) as median
FROM answer_detail
GROUP BY survey_id;
- JSON字段应用:MySQL 8.0的JSON类型存储动态扩展属性
java复制// MyBatis-Plus处理JSON字段
@TableField(typeHandler = FastjsonTypeHandler.class)
private QuestionConfig config;
- 分库分表策略:按survey_id哈希分片,解决单表数据量过大问题
4. 前端工程化实践
4.1 问卷设计器实现
基于Vue 3的拖拽式设计器核心逻辑:
vue复制<template>
<div class="designer-container">
<component
v-for="q in questions"
:key="q.id"
:is="getComponent(q.type)"
v-model="q.content"
@delete="removeQuestion(q.id)"
/>
<el-button @click="addQuestion('text')">添加文本题</el-button>
</div>
</template>
<script setup>
// 动态组件映射
const componentMap = {
text: defineAsyncComponent(() => import('./TextQuestion.vue')),
single: defineAsyncComponent(() => import('./SingleChoice.vue')),
// ...其他题型组件
}
const getComponent = (type) => {
return componentMap[type] || componentMap.text
}
</script>
4.2 状态管理方案
复杂表单状态使用Pinia管理:
javascript复制// stores/survey.js
export const useSurveyStore = defineStore('survey', {
state: () => ({
currentSurvey: null,
questionList: [],
logicJumps: []
}),
actions: {
async loadSurvey(id) {
const { data } = await api.getSurvey(id)
this.currentSurvey = data
this.questionList = data.questions
},
// 问题排序
moveQuestion(fromIndex, toIndex) {
this.questionList.splice(
toIndex, 0, ...this.questionList.splice(fromIndex, 1)
)
}
}
})
4.3 性能优化手段
- 虚拟滚动:处理超长问卷(100+问题)
vue复制<RecycleScroller
:items="questions"
:item-size="80"
key-field="id"
>
<template #default="{ item }">
<QuestionItem :question="item" />
</template>
</RecycleScroller>
- Web Worker处理大数据量导出
javascript复制// worker.js
self.onmessage = (e) => {
const { data } = e
const result = heavyDataProcess(data)
postMessage(result)
}
// 主线程
const worker = new Worker('./worker.js')
worker.postMessage(largeDataSet)
worker.onmessage = (e) => {
downloadExcel(e.data)
}
5. 部署与运维方案
5.1 容器化部署
Docker Compose编排方案:
yaml复制version: '3.8'
services:
backend:
build: ./survey-backend
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
depends_on:
- redis
- mysql
frontend:
build: ./survey-frontend
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:6-alpine
ports:
- "6379:6379"
volumes:
mysql_data:
5.2 监控告警体系
- Spring Boot Actuator集成Prometheus
properties复制# application.properties
management.endpoints.web.exposure.include=health,metrics,prometheus
management.metrics.export.prometheus.enabled=true
-
关键监控指标:
- 问卷提交成功率
- API平均响应时间
- 数据库连接池使用率
- JVM内存占用
-
Grafana看板示例SQL:
sql复制SELECT
time_bucket('1m', timestamp) AS time,
COUNT(*) as submit_count,
AVG(response_time) as avg_time
FROM survey_api_metrics
WHERE path LIKE '/api/submit%'
GROUP BY time
ORDER BY time DESC
6. 典型问题排查实录
6.1 慢SQL优化案例
现象:问卷统计页面在数据量达10万+时加载超时
分析过程:
- 通过Arthas捕获执行SQL:
sql复制SELECT q.content, COUNT(a.id)
FROM question q
LEFT JOIN answer a ON q.question_id = a.question_id
WHERE q.survey_id = 123
GROUP BY q.question_id
- EXPLAIN显示全表扫描answer表
解决方案:
- 增加联合索引:
sql复制ALTER TABLE answer ADD INDEX idx_survey_question (survey_id, question_id);
- 改写为子查询先过滤:
sql复制SELECT
q.content,
(SELECT COUNT(*) FROM answer a
WHERE a.question_id = q.question_id
AND a.survey_id = q.survey_id) as answer_count
FROM question q
WHERE q.survey_id = 123
6.2 内存泄漏排查
现象:服务运行24小时后OOM崩溃
诊断步骤:
- 添加JVM参数收集dump文件:
code复制-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/survey.hprof
- 使用MAT分析发现MyBatis缓存未释放
修复方案:
java复制// 添加SqlSession清理拦截器
@Interceptor
@Signature(type= Executor.class, method="close", args={boolean.class})
public class SqlSessionCleanInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
LocalCacheScope previous = Configuration.getLocalCacheScope();
Configuration.setLocalCacheScope(LocalCacheScope.STATEMENT);
try {
return invocation.proceed();
} finally {
Configuration.setLocalCacheScope(previous);
}
}
}
7. 扩展能力建设
7.1 第三方集成方案
微信小程序对接流程:
- 获取openid实现静默登录
java复制@GetMapping("/wx/auth")
public R wxAuth(@RequestParam String code) {
String url = "https://api.weixin.qq.com/sns/jscode2session?"
+ "appid=" + appId
+ "&secret=" + secret
+ "&js_code=" + code
+ "&grant_type=authorization_code";
WxAuthResponse res = restTemplate.getForObject(url, WxAuthResponse.class);
return R.ok().data(res);
}
- 订阅消息模板配置
json复制{
"touser": "OPENID",
"template_id": "TEMPLATE_ID",
"page": "pages/survey/index?id=123",
"data": {
"thing1": {"value": "员工满意度调查"},
"date2": {"value": "2023-06-15"}
}
}
7.2 数据分析增强
- 使用Apache ECharts实现可视化:
javascript复制// 满意度分布雷达图
const option = {
radar: {
indicator: [
{ name: '工作环境', max: 100 },
{ name: '薪酬福利', max: 100 },
// ...其他维度
]
},
series: [{
type: 'radar',
data: [
{
value: [85, 76, 92],
name: '2023年'
}
]
}]
}
- 文本分析功能集成:
python复制# Python服务处理文本情感分析
@app.route('/analyze/sentiment', methods=['POST'])
def analyze_sentiment():
text = request.json.get('text')
blob = TextBlob(text)
return {
'polarity': blob.sentiment.polarity,
'subjectivity': blob.sentiment.subjectivity
}
8. 安全防护措施
8.1 防注入方案
- MyBatis参数严格校验:
java复制@Intercepts({
@Signature(type= StatementHandler.class,
method="parameterize",
args= Statement.class)
})
public class SqlInjectionInterceptor implements Interceptor {
@Override
public Object intercept(Invocation iv) throws Throwable {
ParameterHandler ph = (ParameterHandler) iv.getTarget();
Field parameterField = ph.getClass().getDeclaredField("parameterObject");
parameterField.setAccessible(true);
Object parameterObject = parameterField.get(ph);
if (parameterObject instanceof String) {
String value = (String) parameterObject;
if (StringUtils.containsAny(value, "'", "\"", ";", "--")) {
throw new IllegalArgumentException("非法参数");
}
}
return iv.proceed();
}
}
- XSS防护全局过滤器:
java复制@WebFilter("/*")
public class XssFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
chain.doFilter(new XssRequestWrapper((HttpServletRequest) req), res);
}
}
// 包装器实现HTML转义
public class XssRequestWrapper extends HttpServletRequestWrapper {
@Override
public String getParameter(String name) {
return HtmlUtils.htmlEscape(super.getParameter(name));
}
}
8.2 数据加密策略
- 敏感字段AES加密:
java复制@ColumnTransformer(
read = "AES_DECRYPT(UNHEX(content), '${aes.key}')",
write = "HEX(AES_ENCRYPT(?, '${aes.key}'))"
)
@Column(name = "content")
private String content;
- 数据库透明加密(TDE)配置:
properties复制# mysql.cnf
[mysqld]
early-plugin-load=keyring_file.so
keyring_file_data=/var/lib/mysql-keyring/keyring
9. 项目演进路线
9.1 技术债清理计划
-
接口规范化:
- 统一返回结构
- 错误码标准化
- Swagger文档自动化
-
代码重构重点:
- 抽取问卷核心引擎为独立模块
- 前端组件按功能域重组
- 构建领域模型DDD化
9.2 微服务化改造
-
服务拆分方案:
- 用户中心服务
- 问卷引擎服务
- 数据分析服务
- 通知服务
-
Spring Cloud Alibaba技术栈:
- Nacos服务发现
- Sentinel流量控制
- Seata分布式事务
-
服务网格补充:
yaml复制# Istio VirtualService
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: survey-vs
spec:
hosts:
- survey.example.com
http:
- route:
- destination:
host: survey-svc
subset: v1
timeout: 5s
retries:
attempts: 3
perTryTimeout: 1s
10. 开发环境配置指南
10.1 本地调试配置
- 后端开发配置:
properties复制# application-dev.properties
spring.datasource.url=jdbc:mysql://localhost:3306/survey_dev?useSSL=false
spring.datasource.username=devuser
spring.datasource.password=dev123
# MyBatis日志
logging.level.com.example.mapper=DEBUG
- 前端代理设置:
javascript复制// vue.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
}
10.2 代码规范检查
- 后端Checkstyle规则:
xml复制<!-- checkstyle.xml -->
<module name="Checker">
<module name="TreeWalker">
<module name="MethodLength">
<property name="max" value="50"/>
</module>
<module name="ParameterNumber">
<property name="max" value="5"/>
</module>
</module>
</module>
- 前端ESLint配置:
json复制{
"rules": {
"vue/max-attributes-per-line": ["error", {
"singleline": 3,
"multiline": 1
}],
"max-len": ["error", {
"code": 120,
"ignoreUrls": true
}]
}
}
11. 效能提升实践
11.1 CI/CD流水线
GitLab Runner配置示例:
yaml复制# .gitlab-ci.yml
stages:
- test
- build
- deploy
unit-test:
stage: test
image: maven:3.8-openjdk-17
script:
- mvn test -B
frontend-build:
stage: build
image: node:16
script:
- npm install
- npm run build
artifacts:
paths:
- dist/
docker-deploy:
stage: deploy
image: docker:20
script:
- docker build -t survey-app .
- docker push registry.example.com/survey:v1.0
11.2 文档自动化
- Swagger接口文档集成:
java复制@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.controller"))
.paths(PathSelectors.any())
.build()
.apiInfo(metaData());
}
}
- 数据库文档生成:
bash复制# 使用SchemaSpy
java -jar schemaspy-6.1.0.jar \
-t mysql \
-db survey_db \
-host localhost \
-u root \
-p password \
-o ./docs/db
12. 项目经验总结
在实际交付过程中,我们积累了几个关键经验点:
-
复杂逻辑跳转的验证
当问卷存在超过5级条件跳转时,前端验证逻辑变得极其复杂。我们最终采用有向无环图(DAG)模型进行校验:javascript复制// 验证跳转逻辑是否成环 function checkLoop(logicJumps) { const graph = new Map() logicJumps.forEach(jump => { if (!graph.has(jump.from)) graph.set(jump.from, []) graph.get(jump.from).push(jump.to) }) return hasCycle(graph) } -
移动端适配陷阱
早期版本在iOS Safari上出现表单提交异常,最终定位是:- 日期选择器时区处理不一致
- 文件上传组件accept属性兼容性问题
解决方案是统一使用第三方组件库并做好真机测试。
-
分页查询优化
当答案数据超过百万时,传统LIMIT分页性能急剧下降。改用游标分页:sql复制SELECT * FROM answer WHERE survey_id = ? AND answer_id > ? ORDER BY answer_id ASC LIMIT 20 -
国际化方案选择
多语言支持初期采用前端i18n方案,后发现管理后台也需要翻译。最终方案:- 静态内容:前端vue-i18n
- 动态内容(如问题文本):后端存储多语言版本
- 管理员界面:单独语言包
这套系统经过三年迭代,目前已在教育、零售、政务等多个领域落地,处理过单日最高50万份问卷提交的峰值压力。技术架构的扩展性得到了充分验证,后续计划加入AI辅助分析等智能特性。