简介:本资源是一套面向本科毕业设计的微信小程序问卷调查系统完整实现方案,适用于Java后端开发、Vue前端及小程序全栈学习者,解决校园调研、课程实践、小型组织数据采集等场景中的问卷发布、填写、统计与管理需求。项目采用SpringBoot构建RESTful后端服务,Vue开发Web管理端,微信小程序作为用户端,MySQL存储问卷结构、回答记录与用户信息,技术栈覆盖前后端分离典型架构。压缩包共1849个文件,含156个Java后端控制器与实体类、189个Vue组件、270个JS逻辑脚本、318个PNG界面素材及1个SQL建表脚本,辅以bat启动脚本、yml配置、class编译文件等,整体32.76MB,结构规范便于工程复现。已有108人学习下载,读者可直接导入运行,获取从数据库设计、接口定义、管理后台交互到小程序页面渲染的全流程源码,以及配套论文文档与清晰目录模块划分,是毕业设计快速落地与技术整合的高价值参考范例。
1. 为什么一个“问卷调查系统”毕业设计,能让答辩老师多问十分钟?
这不是一个只拼界面美观的微信小程序作业。它是一次对真实业务闭环能力的硬核检验:用户在微信里扫码填问卷,数据实时落库,管理员用 Vue 页面做可视化统计,SpringBoot 后端扛住并发提交、校验逻辑、权限隔离,SQL 脚本确保环境一键初始化,论文还得把“为什么选 JWT 而不是 Session”“Vue 表单动态渲染题型怎么解耦”讲清楚。我带过几届某高校毕业设计指导,翻车最多的地方不是代码写不出来,而是——前端提交了但后端没收到;管理员能登录却看不到自己创建的问卷;导出 Excel 的选项卡突然空白;论文里写的“高并发处理”在压测时 50 并发就超时。这篇笔记不讲“如何搭建 SpringBoot 环境”,而是聚焦你真正卡住的那几个点:怎么让小程序真能连上你的本地后端?Vue 管理端怎么和 SpringBoot 的权限体系对齐?SQL 脚本执行后为什么总缺一张表?论文里哪些技术细节是答辩必问、必须提前写进正文的?如果你正对着 IDEA 和 HBuilderX 发呆,这篇就是为你写的实操路线图。
2. 搭建可联调的最小闭环:从 SpringBoot 启动到小程序真机扫码
这个系统不是三个独立模块拼起来的,而是一个数据流必须贯通的链条。很多同学本地跑通了后端、也跑通了 Vue 页面,但小程序一扫码就报request:fail network error,根本原因是:开发阶段的跨域、HTTPS、域名白名单、本地服务暴露,四者缺一不可。我们跳过“新建 SpringBoot 项目”的基础步骤,直奔联调关键点。
2.1 SpringBoot 后端:必须暴露的三个配置项
光靠application.yml默认配置,小程序连不上。你需要显式打开 CORS,并确认端口与路径前缀无冲突:
# application.yml server: port: 8080 servlet: context-path: /api # 所有接口统一加 /api 前缀,避免和 Vue 的 /admin 冲突 spring: mvc: cors: configurations: '[/**]': allowed-origins: "https://localhost:8080, https://127.0.0.1:8080, https://your-local-ip:8080" # 小程序开发者工具会走 localhost,真机调试需填本机局域网 IP(如 192.168.1.102) allowed-methods: "GET, POST, PUT, DELETE, OPTIONS" allowed-headers: "*" allow-credentials: true注意:
allowed-origins必须写全协议+域名+端口,不能只写*(小程序不认);allow-credentials: true是为了后续 JWT Token 能带 Cookie;context-path: /api是为 Vue 管理端统一 API 基地址打基础,避免后期改一堆axios配置。
2.2 Vue 管理端:Axios 实例的基地址与拦截器必须绑定后端路径
Vue 项目里别再用http://localhost:8080/xxx这种硬编码。所有请求必须通过统一实例发出,并自动携带认证头:
// src/utils/request.js import axios from 'axios' const request = axios.create({ baseURL: 'http://192.168.1.102:8080/api', // ⚠️ 这里必须是你本机局域网 IP,不是 localhost!真机调试时手机和电脑在同一 WiFi 下才能通 timeout: 10000, headers: { 'Content-Type': 'application/json' } }) // 请求拦截器:自动注入 token(假设你用 localStorage 存) request.interceptors.request.use(config => { const token = localStorage.getItem('admin_token') if (token) { config.headers.Authorization = `Bearer ${token}` // SpringBoot 后端用 @AuthenticationPrincipal 或自定义 Filter 解析此头 } return config }) export default request逻辑说明:
baseURL写192.168.1.102是因为微信开发者工具在“真机调试”模式下,手机访问的是你电脑的局域网 IP,而非localhost;Authorization头格式必须和后端JwtAuthenticationFilter中解析逻辑一致(常见坑:前端传Token xxx,后端却期待Bearer xxx);timeout设为 10 秒是防止问卷提交时网络抖动导致页面假死。
2.3 微信小程序端:wx.request的 URL 必须走 HTTPS 或配置合法域名
这是最常被忽略的致命点。小程序真机运行时,wx.request默认只允许访问 HTTPS 域名,且必须在微信公众平台后台配置“request 合法域名”。开发阶段有两种解法:
方案 A(推荐):用 ngrok 或 localtunnel 暴露本地端口为 HTTPS 链接
# 安装 ngrok(需注册账号获取 authtoken) ngrok http 8080 # 输出类似:https://a1b2c3d4.ngrok.io → 此链接即为你的 HTTPS 入口然后在小程序
app.js中:App({ globalData: { baseUrl: 'https://a1b2c3d4.ngrok.io/api' // 注意:ngrok 生成的域名每次重启会变,需同步改 } })方案 B(仅限开发测试):在小程序开发者工具中勾选「不校验合法域名」
警告:此选项仅在“开发者工具”中生效,真机扫码时无效。务必在上线前切回 HTTPS 方案。
参数说明:
ngrok是目前最稳定的本地 HTTPS 暴露工具,免费版有并发数限制(够毕业设计用);localtunnel无需注册但稳定性略低;绝对不要尝试用 Nginx 反向代理本地 HTTP 为 HTTPS——证书配置复杂且易出错,对毕业设计纯属增加负担。
3. Vue 管理端核心功能落地:动态问卷编辑与实时统计图表
管理端不是静态页面,它要支撑“创建问卷→添加题目→发布→查看统计”全流程。很多同学卡在“题目类型太多,Vue 模板写成一团乱麻”。这里给出可复用的结构化方案。
3.1 动态表单:用v-for+component :is解耦题型渲染
问卷题目分单选、多选、填空、评分等,如果每个都写v-if,维护成本爆炸。正确做法是定义题型组件,由父组件动态挂载:
<!-- QuestionItem.vue --> <template> <div class="question-item"> <h3>{{ question.title }} <span class="required" v-if="question.required">*</span></h3> <component :is="getComponentName(question.type)" :question="question" @change="onQuestionChange" /> </div> </template> <script> import RadioGroup from './components/RadioGroup.vue' import CheckboxGroup from './components/CheckboxGroup.vue' import InputText from './components/InputText.vue' import Rating from './components/Rating.vue' export default { components: { RadioGroup, CheckboxGroup, InputText, Rating }, props: ['question'], methods: { getComponentName(type) { const map = { 'radio': 'RadioGroup', 'checkbox': 'CheckboxGroup', 'text': 'InputText', 'rating': 'Rating' } return map[type] || 'InputText' }, onQuestionChange(value) { this.$emit('change', { id: this.question.id, value }) } } } </script>逻辑说明:
<component :is>是 Vue 动态组件的核心,它让模板保持极简;每个子组件(如RadioGroup.vue)只负责一种题型的 UI 和交互逻辑,例如RadioGroup内部用v-model绑定选中值,触发@change向上冒泡;getComponentName()是映射表,新增题型只需加一行配置,不碰主模板。
3.2 实时统计:ECharts 图表的数据驱动更新策略
统计页不能每次点“刷新”才重绘。应监听路由变化或使用 WebSocket(简易版可用轮询):
// src/views/StatsView.vue export default { data() { return { chartOption: null, pollTimer: null } }, mounted() { this.loadStats() // 每 30 秒拉一次最新数据(比 WebSocket 简单,适合毕业设计) this.pollTimer = setInterval(() => { this.loadStats() }, 30000) }, beforeUnmount() { clearInterval(this.pollTimer) }, methods: { async loadStats() { try { const res = await request.get(`/stats/question/${this.$route.params.id}`) this.chartOption = this.genChartOption(res.data) } catch (e) { console.error('加载统计失败', e) } }, genChartOption(data) { // data 格式示例:{ questionTitle: '您对服务满意度?', options: [{name: '非常满意', value: 42}, {name: '满意', value: 31}] } return { tooltip: { trigger: 'item' }, series: [{ type: 'pie', data: data.options, radius: ['40%', '70%'] }] } } } }参数说明:
setInterval轮询是毕业设计最稳妥的实时方案,30 秒间隔兼顾及时性与服务器压力;genChartOption()是纯数据转换函数,不操作 DOM,符合 Vue 响应式原则;beforeUnmount清理定时器是防内存泄漏的血泪经验——否则切换问卷时旧定时器还在跑,导致数据错乱。
4. SpringBoot 后端关键逻辑实现:JWT 权限控制与问卷提交幂等性
后端不是 CRUD 接口堆砌。两个高频答辩问题:“你怎么保证管理员只能看自己的问卷?”“用户重复提交怎么办?”答案不在数据库字段里,而在 Controller 层的设计。
4.1 JWT 权限拦截:用@PreAuthorize注解精准控制资源
别再手写 Filter 判断角色。Spring Security 提供声明式权限控制,清晰且易测试:
// QuestionController.java @RestController @RequestMapping("/question") public class QuestionController { @PostMapping @PreAuthorize("hasRole('ADMIN')") // 仅 ADMIN 可创建 public Result create(@RequestBody QuestionDTO dto, Authentication auth) { String adminId = auth.getName(); // 从 JWT 中解析出用户名(即 admin_id) dto.setCreatorId(adminId); return Result.success(questionService.create(dto)); } @GetMapping("/{id}") @PreAuthorize("@questionSecurityService.canAccess(#id, authentication)") // 自定义 SpEL 表达式 public Result detail(@PathVariable Long id) { return Result.success(questionService.getById(id)); } }配套的QuestionSecurityService:
@Service public class QuestionSecurityService { public boolean canAccess(Long questionId, Authentication auth) { String currentAdminId = auth.getName(); // 查询数据库:该问卷 creator_id 是否等于 currentAdminId return questionMapper.checkOwnership(questionId, currentAdminId); } }逻辑说明:
@PreAuthorize("hasRole('ADMIN')")是角色级控制,简单直接;@PreAuthorize("@service.method(...)")是对象级控制,解决“管理员 A 不能删管理员 B 的问卷”;auth.getName()默认返回 JWT 中的sub字段(即用户唯一标识),你在生成 Token 时需确保sub填的是admin_id字符串,而非邮箱或昵称。
4.2 问卷提交幂等性:用 Redis + Token 防止重复提交
用户手抖连点两次“提交”,后端必须拒绝第二次。常见错误是只在前端加disabled,这毫无意义:
// SubmitController.java @PostMapping("/submit") public Result submit(@RequestBody SubmitDTO dto, HttpServletRequest request) { // 1. 从 Header 或 Cookie 中提取客户端提交的唯一 token(前端生成并随请求发送) String clientToken = request.getHeader("X-Submit-Token"); if (StringUtils.isBlank(clientToken)) { return Result.fail("缺少提交令牌"); } // 2. Redis 检查该 token 是否已存在(SETNX 命令) Boolean isSet = redisTemplate.opsForValue().setIfAbsent( "submit:token:" + clientToken, "used", Duration.ofMinutes(10) // 10 分钟内有效,覆盖正常提交耗时 ); if (!Boolean.TRUE.equals(isSet)) { return Result.fail("重复提交,请勿频繁点击"); } // 3. 执行实际业务逻辑(保存答案、更新统计等) submitService.handle(dto); return Result.success(); }参数说明:
X-Submit-Token由前端在问卷页加载时生成(如Date.now() + Math.random()),并存入localStorage,每次提交都带上;setIfAbsent是原子操作,天然防并发;Duration.ofMinutes(10)是经验值,太短用户网络慢会误判,太长则无法防真实重复;此方案比数据库唯一索引更轻量,且不依赖表结构变更。
5. SQL 脚本与数据库部署避坑:字段类型、外键约束与初始化顺序
拿到源码包里的schema.sql,直接source进 MySQL 常见报错:“Unknown column ‘xxx’ in ‘field list’”、“Cannot add or update a child row”…… 这些不是语法错,而是脚本执行顺序与约束依赖没对齐。
5.1 必须按顺序执行的三类脚本(缺一不可)
毕业设计的 SQL 脚本绝不能只有一个文件。标准拆分是:
| 脚本文件名 | 执行顺序 | 作用说明 |
|---|---|---|
01_init_db.sql | 第 1 步 | CREATE DATABASE IF NOT EXISTS survey DEFAULT CHARACTER SET utf8mb4; |
02_create_table.sql | 第 2 步 | 建表语句,所有外键字段必须先建好对应主表(如question表必须在answer表之前) |
03_init_data.sql | 第 3 步 | 插入管理员账号、默认问卷模板等初始数据,必须在外键表建完后执行 |
现象 → 原因 → 解决:
- 现象:执行
02_create_table.sql报错ERROR 1005 (HY000): Can't create table 'answer' (errno: 150)- 原因:
answer表中FOREIGN KEY (question_id) REFERENCES question(id),但question表还没建- 解决:检查
02_create_table.sql中建表顺序,把question放在answer之前;用SHOW CREATE TABLE question;确认id字段是PRIMARY KEY且类型为BIGINT(与answer.question_id类型严格一致)
5.2 字段类型陷阱:TEXT vs VARCHAR,DATETIME vs TIMESTAMP
MySQL 8.0+ 对字段类型更严格,毕业设计最容易栽在这两个地方:
content TEXTvscontent VARCHAR(2000):问卷题目描述可能超 255 字,VARCHAR(2000)在某些版本会报Specified key was too long(索引长度超限)。正确做法是TEXT类型,且不对其建索引(搜索用全文索引或应用层模糊匹配)。created_time DATETIMEvscreated_time TIMESTAMP:TIMESTAMP会自动转时区,本地开发时看到的时间和数据库存的不一致。毕业设计一律用DATETIME,并在 Java 实体类中用LocalDateTime接收,避免时区玄学。
-- ✅ 正确建表片段 CREATE TABLE `question` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `title` VARCHAR(200) NOT NULL, `content` TEXT, -- 不要用 VARCHAR(5000) `creator_id` VARCHAR(50) NOT NULL, -- 管理员 ID,字符串类型,与 JWT sub 一致 `created_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `status` TINYINT NOT NULL DEFAULT 0 -- 0=草稿, 1=已发布 ); -- ❌ 错误示范(会导致插入失败) -- `content` VARCHAR(5000), -- 超长 VARCHAR 在 utf8mb4 下索引超限 -- `created_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -- 自动更新会干扰业务时间现象 → 原因 → 解决:
- 现象:Java 代码插入问卷时抛
DataIntegrityViolationException,日志显示Column 'content' cannot be null- 原因:
content字段设了NOT NULL,但前端传空字符串"",MySQL 认为空字符串 ≠ NULL,而TEXT类型允许NULL,VARCHAR不允许空字符串(除非显式设DEFAULT '')- 解决:
content TEXT NULL DEFAULT NULL,后端接收时判空:if (StringUtils.isBlank(dto.getContent())) dto.setContent(null);
6. 论文写作与答辩准备:把技术细节写成“可验证的结论”
答辩老师不关心你用了多少框架,只关心你是否真正理解每个选择背后的 trade-off。论文里“采用 SpringBoot”这种话毫无价值,必须落到具体决策点。以下是三个答辩高频问题及你论文中必须出现的对应段落。
6.1 “为什么用 JWT 而不用 Session?”
论文写法(放在“技术选型”章节):
“本系统采用 JWT(JSON Web Token)实现管理员身份认证,主要基于三点考量:第一,前后端分离架构下,Session 依赖 Cookie 和同源策略,而 Vue 管理端与 SpringBoot 后端部署在不同端口(
http://localhost:8080vshttp://localhost:8081),跨域 Cookie 传递复杂且存在安全风险;第二,JWT 将用户信息加密存储在客户端,服务端无需维护 Session 状态,降低内存开销,符合毕业设计轻量级定位;第三,微信小程序端无法使用 Cookie,JWT 可通过Authorization请求头透传,与小程序wx.request的 header 机制天然契合。实际实现中,Token 有效期设为 2 小时,配合 Refresh Token 机制(见附录 A),在安全性与用户体验间取得平衡。”
答辩技巧:当老师追问“JWT 怎么防篡改”,立刻回答:“签名部分(Signature)使用 HS256 算法,服务端用密钥
survey-secret-key验证,任何字段修改都会导致签名不匹配,io.jsonwebtoken.Jwts.parser().setSigningKey(key).parseClaimsJws(token)会直接抛SignatureException。”
6.2 “问卷提交并发量怎么保障?”
论文写法(放在“性能优化”章节):
“针对问卷提交场景的瞬时并发(如课堂扫码填写),系统未采用数据库乐观锁等重型方案,而是实施三层防护:其一,前端按钮提交后立即置灰并显示加载中,阻断用户重复操作;其二,后端通过 Redis 的
SETNX命令实现分布式令牌(Token)校验,单个提交请求在 10 分钟内仅允许成功一次,该方案平均响应时间 < 15ms(压测数据见表 4.2);其三,MySQL 表结构层面,answer表主键采用BIGINT自增,避免 UUID 导致的索引碎片。经 JMeter 模拟 200 并发用户持续提交,系统平均错误率 < 0.3%,TPS 稳定在 85 左右,满足校园级问卷场景需求。”
答辩技巧:老师若问“Redis 挂了怎么办”,答:“毕业设计阶段 Redis 作为缓存非核心依赖,
SETNX校验失败时降级为日志告警+人工核查,不影响主流程;生产环境可引入 Redis Cluster 或改用数据库唯一索引兜底。”
6.3 “Vue 管理端怎么保证不同管理员数据隔离?”
论文写法(放在“安全性设计”章节):
“数据隔离通过‘服务端强制校验’实现,而非依赖前端路由或菜单隐藏。具体而言,所有涉及问卷详情、统计、删除的接口(如
/question/{id}、/stats/question/{id}),均在 SpringBoot Controller 层使用@PreAuthorize("@questionSecurityService.canAccess(#id, authentication)")注解,调用自定义服务查询数据库确认当前管理员 ID 与目标问卷的creator_id字段完全匹配。该机制杜绝了通过修改 URL 参数越权访问的风险,已在渗透测试中验证(测试报告见附录 C)。”
答辩技巧:拿出 Postman 截图,演示手动修改 URL 中的
id为其他管理员创建的问卷 ID,返回403 Forbidden,证明校验生效。
最后说一句实在话:我见过太多同学,答辩前一周还在调通小程序联调,论文里“系统采用微服务架构”写得天花乱坠,结果连单体应用的事务都没整明白。毕业设计的价值,不在于堆砌高大上名词,而在于把一个闭环场景做扎实——让用户真能填、管理员真能管、数据真能查、论文真能讲清为什么这么选。你照着这篇的路径走,把ngrok链接配好、把@PreAuthorize注解加上、把SETNX逻辑写对、把论文里那三段话补全,答辩时底气就足了。希望帮到你。
本文还有配套的精品资源,点击获取