简介:本资源是一套面向计算机专业本科生的Java毕业设计实战项目,聚焦微信小程序端问卷调查系统的全栈开发,适用于课程设计、毕设选题与SpringBoot+Vue全栈能力训练。系统采用SpringBoot构建RESTful后端,Vue开发Web管理端,微信小程序实现用户端问卷填写与查看,MySQL存储问卷、用户、答题记录等核心数据,完整覆盖需求分析、数据库设计、前后端联调及论文撰写全流程。压缩包共1849个文件,含156个Java后端控制器与服务类、189个Vue组件、270个JS逻辑脚本、318个PNG/UI图标及1个SQL建表脚本,辅以bat启动脚本、YML配置、MD文档与DOCX论文,结构清晰、开箱即用。资源包大小32.76MB,目前已有108人学习下载,提供从环境搭建、接口调试到管理员后台操作的完整交付物,特别适合缺乏真实项目经验的学生快速掌握多端协同开发模式与毕业设计规范表达。
1. 为什么一个「微信小程序问卷调查系统」毕业设计,能同时练透 SpringBoot、Vue 和小程序三端协作的硬功夫?
这不是又一个套模板交差的毕设项目。某高校计算机系连续三年把「微信小程序问卷调查系统」列为 Java 方向高通过率选题,核心原因很实在:它天然覆盖了现代 Web 工程师必须闭环掌握的三个关键断层——后端接口的健壮性设计(SpringBoot)、管理后台的交互逻辑与状态管理(Vue 3 + Element Plus)、小程序端的数据采集与体验优化(WXML/WXSS/JS + 微信原生能力)。更关键的是,它不依赖外部 SaaS 服务,所有数据流完全可控:用户扫码填问卷 → 小程序调用 SpringBoot 接口提交 → Vue 后台实时查看统计图表 → 导出 Excel 或 SQL 备份。整个链路没有黑匣子,每一层都能 debug、能改、能压测。适合两类人:一是 Java 新手想用真实业务场景串起 SpringBoot 全家桶(MyBatis-Plus、JWT、Redis 缓存、文件上传);二是前端初学者需要一个“有后端兜底”的 Vue 实战入口,不用自己搭 mock server,直接连真接口跑通 CRUD+图表渲染。下面我们就从零开始,把这套三端协同的最小可行系统真正跑通、调稳、踩透。
2. 搭建 SpringBoot 后端:从数据库建模到 RESTful 接口的完整闭环
2.1 数据库设计:为什么问卷、题目、选项、答卷要拆成 5 张表?
很多同学一上来就建一张questionnaire表塞所有字段,结果后期加逻辑时发现无法关联、无法统计、无法扩展。我们按业务语义严格分表,这是后续所有功能可维护的基础:
| 表名 | 核心字段(精简) | 设计要点 |
|---|---|---|
t_questionnaire | id, title, description, status(0草稿/1发布/2关闭), create_time | status 字段是控制问卷生命周期的关键开关,避免软删除带来的统计歧义 |
t_question | id, qid (外键), content, type(1单选/2多选/3填空/4评分), sort_order | sort_order支持拖拽排序,比order by id更符合运营直觉 |
t_option | id, qid, content, sort_order | 单选/多选题的选项独立成表,为后续“选项被选中次数”统计埋点 |
t_answer_record | id, qid, openid (小程序用户唯一标识), submit_time | 关键:不存用户手机号等敏感信息,靠openid关联,符合微信生态合规要求 |
t_answer_detail | id, record_id, qid, option_id, content (填空内容) | 一份答卷可能含多种题型答案,必须拆开存储,否则无法做交叉分析 |
提示:
t_answer_record中的openid不是明文存储,而是经 AES 加密后存入(密钥由 SpringBoot 配置),防止数据库泄露导致用户身份暴露。加密逻辑封装在AnswerService的saveAnswer()方法中,不是靠数据库字段类型实现。
2.2 SpringBoot 工程结构与核心配置
使用 SpringBoot 2.7.18(JDK 8 兼容性好,企业级项目仍主流),Maven 依赖精简聚焦:
<!-- pom.xml 关键依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> <!-- 适配 SpringBoot 2.7.x --> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.22</version> <!-- 简化 AES 加密、Excel 导出等工具调用 --> </dependency>application.yml中必须显式配置微信相关参数(用于后续小程序登录校验):
# application.yml wechat: appid: wx1234567890abcdef # 替换为你的小程序 AppID secret: 1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p # 替换为你的 AppSecret mch-id: 1234567890 # 如需支付功能(本项目暂未启用,但预留字段)2.3 实现「用户提交问卷」接口:POST /api/v1/answer/submit
这是整个系统最核心的写入接口,必须考虑幂等性、并发提交、字段校验三重防护:
// AnswerController.java @PostMapping("/submit") public Result submitAnswer(@RequestBody @Validated AnswerSubmitDTO dto, HttpServletRequest request) { // 1. 从 Header 中提取 Authorization JWT token(小程序端登录后携带) String token = request.getHeader("Authorization"); if (StrUtil.isBlank(token)) { return Result.fail("未登录,请先授权"); } // 2. 解析 token 获取 openid(实际项目中应走 Redis 缓存校验,此处简化) String openid = JwtUtil.getOpenidFromToken(token); if (StrUtil.isBlank(openid)) { return Result.fail("登录态失效,请重新授权"); } // 3. 校验问卷是否处于「已发布」状态(业务强约束) Questionnaire questionnaire = questionnaireService.getById(dto.getQid()); if (questionnaire == null || !questionnaire.getStatus().equals(1)) { return Result.fail("问卷不存在或未开启填写"); } // 4. 执行保存(含事务控制 + AES 加密 openid) boolean saved = answerService.saveAnswer(dto, openid); return saved ? Result.success("提交成功") : Result.fail("提交失败,请重试"); }AnswerSubmitDTO是严格定义的接收对象,强制校验:
// AnswerSubmitDTO.java @Data @Valid public class AnswerSubmitDTO { @NotNull(message = "问卷ID不能为空") private Long qid; @NotEmpty(message = "题目答案列表不能为空") private List<AnswerItemDTO> answers; // 内部类:每道题的答案 @Data public static class AnswerItemDTO { @NotNull(message = "题目ID不能为空") private Long questionId; // 单选/多选:传 optionIds 数组;填空:传 content 字符串;评分:传 score 整数 private List<Long> optionIds; private String content; private Integer score; } }逻辑说明:
- 接口不接受原始 JSON 字符串,而是强类型 DTO,避免前端传错字段名导致后端 NPE;
@Validated触发 Hibernate Validator 校验,错误信息统一由全局异常处理器捕获并返回标准格式;saveAnswer()方法内部开启@Transactional,确保t_answer_record和t_answer_detail插入原子性;JwtUtil.getOpenidFromToken()实际会查 Redis 缓存(key:token:${token}),缓存过期时间设为 2 小时,避免频繁调用微信接口解密。
3. 开发 Vue 管理端:用 Composition API 实现动态问卷编辑与实时统计
3.1 项目初始化与权限路由守卫
使用 Vue 3.2.47 + Vue Router 4.0 + Pinia 2.0(替代 Vuex,更轻量),脚手架命令:
npm init vue@latest # 选择:TypeScript, Router, Pinia, ESLint路由守卫强制登录校验(src/router/index.ts):
// 路由守卫:检查 localStorage 中是否有 token router.beforeEach((to, from, next) => { const token = localStorage.getItem('admin_token') if (to.meta.requiresAuth && !token) { next('/login') // 重定向到登录页 } else { next() } })注意:
admin_token是后台/api/v1/admin/login接口返回的 JWT,有效期 7 天,存储在localStorage(非sessionStorage),避免刷新丢失。生产环境建议用 HttpOnly Cookie,但毕设演示阶段localStorage更易调试。
3.2 动态问卷编辑器:用 Element Plus 实现拖拽排序与题型切换
问卷编辑页(QuestionEdit.vue)核心是questionList响应式数组,每项包含type字段驱动不同表单项:
<!-- QuestionEdit.vue --> <el-form-item label="题目类型"> <el-select v-model="question.type" placeholder="请选择"> <el-option label="单选题" :value="1" /> <el-option label="多选题" :value="2" /> <el-option label="填空题" :value="3" /> <el-option label="评分题" :value="4" /> </el-select> </el-form-item> <!-- 根据 type 渲染不同组件 --> <component :is="getQuestionComponent(question.type)" :question="question" :options="question.options" />getQuestionComponent()返回对应组件名:
const getQuestionComponent = (type: number) => { switch (type) { case 1: return 'SingleChoiceEditor' case 2: return 'MultiChoiceEditor' case 3: return 'TextAnswerEditor' case 4: return 'ScoreEditor' default: return 'SingleChoiceEditor' } }每个子组件(如SingleChoiceEditor.vue)内部用v-for渲染选项,并支持拖拽排序(使用SortableJS):
// SingleChoiceEditor.vue import Sortable from 'sortablejs' onMounted(() => { const el = document.getElementById('option-list') if (el) { new Sortable(el, { animation: 150, ghostClass: 'blue-background-class', onEnd: ({ oldIndex, newIndex }) => { // 更新 options 数组顺序 const moved = props.options.splice(oldIndex, 1)[0] if (oldIndex < newIndex) { props.options.splice(newIndex - 1, 0, moved) } else { props.options.splice(newIndex, 0, moved) } } }) } })参数说明:
animation: 150让拖拽有视觉反馈,避免用户误操作;onEnd回调中手动调整props.options数组顺序,因为SortableJS不会自动更新 Vue 响应式数据;ghostClass定义拖拽时的占位样式,需在 CSS 中定义.blue-background-class { background: #e6f7ff; }。
3.3 实时统计图表:ECharts 在 Vue 3 中的正确接入方式
统计页(StatisticsView.vue)需展示「各题目选项选择占比」和「答卷提交趋势」双图表。ECharts 5.4.3 推荐用echarts-gl(非必须)+@vueuse/core的useResizeObserver实现自适应:
// StatisticsView.vue import * as echarts from 'echarts/core' import { CanvasRenderer } from 'echarts/renderers' import { PieChart, LineChart } from 'echarts/charts' import { TitleComponent, TooltipComponent, LegendComponent, GridComponent, DataZoomComponent } from 'echarts/components' echarts.use([ CanvasRenderer, PieChart, LineChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent, DataZoomComponent ]) // 初始化图表(ref 绑定 DOM) const chartRef = ref<HTMLDivElement | null>(null) let chartInstance: echarts.ECharts | null = null onMounted(() => { if (chartRef.value) { chartInstance = echarts.init(chartRef.value) setOption() // 设置初始配置 } }) // 监听窗口 resize(推荐用 useResizeObserver 替代 window.addEventListener) useResizeObserver(chartRef, () => { chartInstance?.resize() }) // 销毁实例(防内存泄漏) onBeforeUnmount(() => { chartInstance?.dispose() })饼图配置关键点(setOption()中):
const setOption = () => { const option = { tooltip: { trigger: 'item' }, legend: { top: '5%', left: 'center' }, series: [{ name: '选项分布', type: 'pie', radius: ['40%', '70%'], avoidLabelOverlap: false, itemStyle: { borderRadius: 10, // 圆角更现代 borderColor: '#fff', borderWidth: 2 }, label: { show: true, formatter: '{b}: {d}%' // 显示名称+百分比 }, data: [ { value: 1048, name: '非常满意' }, { value: 735, name: '满意' }, { value: 580, name: '一般' }, { value: 484, name: '不满意' } ] }] } chartInstance?.setOption(option) }提示:
avoidLabelOverlap: false是玄学设置——当选项过多时,ECharts 默认会隐藏部分 label,设为false强制全部显示,再配合formatter控制文字长度,比后期用rich样式微调更省事。
4. 小程序端开发:从登录态管理到离线缓存的实战细节
4.1 登录态全流程:wx.login → code2Session → JWT 签发
小程序端不存用户敏感信息,所有身份认证走微信官方流程:
// utils/auth.js export const login = async () => { try { const { code } = await wx.login() // 获取临时登录凭证 const res = await wx.request({ url: 'https://your-domain.com/api/v1/wx/login', // 后端接口 method: 'POST', data: { code } }) if (res.data.code === 200) { // 将 JWT 存入 storage,后续请求带入 Authorization Header wx.setStorageSync('token', res.data.data.token) return res.data.data.token } else { throw new Error(res.data.msg) } } catch (err) { console.error('登录失败', err) throw err } }后端/api/v1/wx/login接口逻辑(WxLoginController.java):
@PostMapping("/login") public Result wxLogin(@RequestBody Map<String, String> params) { String code = params.get("code"); if (StrUtil.isBlank(code)) { return Result.fail("code 不能为空"); } // 1. 调用微信接口换取 openid/session_key String url = "https://api.weixin.qq.com/sns/jscode2session?" + "appid=" + wechatConfig.getAppid() + "&secret=" + wechatConfig.getSecret() + "&js_code=" + code + "&grant_type=authorization_code"; String response = HttpUtil.get(url); // 使用 Hutool HTTP 工具 JSONObject json = JSON.parseObject(response); if (json.containsKey("errcode")) { return Result.fail("微信登录失败:" + json.getString("errmsg")); } String openid = json.getString("openid"); String sessionKey = json.getString("session_key"); // 2. 生成 JWT Token(有效期 2 小时) String token = JwtUtil.generateToken(openid, sessionKey); // 3. 将 token 存入 Redis(key: token:${token}, value: openid, 过期时间 2h) redisTemplate.opsForValue().set("token:" + token, openid, 2, TimeUnit.HOURS); return Result.success("登录成功", MapUtil.builder().put("token", token).map()); }关键点:
- 小程序
wx.login()返回的code是一次性凭证,必须立即传给后端; - 后端调用微信
jscode2session接口时,appid和secret必须与小程序后台一致; - JWT 中只存
openid(不可逆),不存session_key(仅用于解密敏感数据,本项目未启用); - Redis 缓存 key 命名为
token:${token},方便后续Authorization校验时直接GET。
4.2 问卷填写页:处理「上一题/下一题」与「提交」状态机
填写页(pages/question/index.vue)用pageData响应式对象管理当前题号、答案、题目列表:
// pages/question/index.vue const pageData = reactive({ currentQIndex: 0, // 当前题目索引(0开始) answers: {} as Record<string, any>, // key: questionId, value: 用户答案 questionList: [] as Question[], // 题目数组(从接口获取) isSubmitting: false // 防重复提交 }) // 下一题逻辑 const nextQuestion = () => { if (validateCurrentAnswer()) { pageData.currentQIndex++ } } // 提交逻辑(最后一页点击「提交」) const submitAll = async () => { if (!validateCurrentAnswer()) return pageData.isSubmitting = true try { const token = wx.getStorageSync('token') const res = await wx.request({ url: 'https://your-domain.com/api/v1/answer/submit', method: 'POST', header: { 'Authorization': token }, data: { qid: pageData.questionList[0]?.qid, // 问卷ID取第一题的 qid 字段 answers: Object.entries(pageData.answers).map(([qId, ans]) => ({ questionId: Number(qId), ...ans // 展开具体答案字段 })) } }) if (res.data.code === 200) { wx.showToast({ title: '提交成功', icon: 'success' }) setTimeout(() => wx.navigateBack(), 1500) } else { throw new Error(res.data.msg) } } catch (err) { wx.showToast({ title: '提交失败', icon: 'error' }) } finally { pageData.isSubmitting = false } }validateCurrentAnswer()校验当前题是否必填:
const validateCurrentAnswer = () => { const currentQ = pageData.questionList[pageData.currentQIndex] const ans = pageData.answers[currentQ.id] if (currentQ.required === 1) { // 后台字段 required=1 表示必填 if (!ans || (Array.isArray(ans) && ans.length === 0)) { wx.showToast({ title: `${currentQ.content} 为必答题`, icon: 'none' }) return false } } return true }注意:
required字段由后台t_question表提供,前端不做硬编码,保证运营可随时修改。
4.3 离线缓存策略:用 wx.setStorageSync 缓存未提交的问卷草稿
用户填写到一半退出,再次进入应恢复进度。我们监听onHide生命周期:
// pages/question/index.vue onHide(() => { // 只缓存「未提交」的草稿(即不是最后一页) if (pageData.currentQIndex < pageData.questionList.length - 1) { const draft = { qid: pageData.questionList[0]?.qid, currentQIndex: pageData.currentQIndex, answers: pageData.answers, timestamp: Date.now() } wx.setStorageSync(`draft_${draft.qid}`, draft) } }) onShow(() => { const qid = pageData.questionList[0]?.qid if (qid) { const draft = wx.getStorageSync(`draft_${qid}`) if (draft && draft.timestamp > Date.now() - 24 * 60 * 60 * 1000) { // 24小时内有效 pageData.currentQIndex = draft.currentQIndex pageData.answers = draft.answers wx.showToast({ title: '已恢复草稿', icon: 'success' }) } } })参数说明:
- 草稿 key 命名为
draft_${qid},避免不同问卷互相覆盖; - 加
timestamp字段,超过 24 小时自动丢弃,防止长期占用 storage; wx.setStorageSync最大容量 10MB,单份草稿通常 < 10KB,安全冗余。
5. 避坑指南:这 4 个血泪经验,让答辩老师当场点头
5.1 现象:小程序提交后,Vue 后台看不到新答卷,数据库t_answer_record为空
原因:后端AnswerController.submitAnswer()方法未加@Transactional注解,且answerService.saveAnswer()内部t_answer_record插入成功,但t_answer_detail因字段类型不匹配(如option_id传了字符串)抛出异常,导致事务未回滚,record行残留脏数据。
解决:
- 在
AnswerService.saveAnswer()方法上添加@Transactional(rollbackFor = Exception.class); - 在
t_answer_detail表的option_id字段加数据库约束BIGINT UNSIGNED,并在 MyBatis-Plus 的AnswerDetail实体类中用Long类型映射; - 开启 MyBatis-Plus 的 SQL 日志:
logging.level.com.example.mapper=debug,观察实际执行的 INSERT 语句。
5.2 现象:Vue 管理端编辑问卷时,拖拽排序后保存,数据库sort_order字段全是 0
原因:SortableJS的onEnd回调中,直接对props.options数组splice操作,但该数组是父组件传入的ref,未触发 Vue 响应式更新,导致watch或computed无法捕获变化,最终提交的仍是原始顺序。
解决:
- 在子组件中用
defineProps接收options时,声明为readonly,然后在onEnd中创建新数组并emit给父组件:const emit = defineEmits(['update:options']) // onEnd 回调内: const newOptions = [...props.options] const moved = newOptions.splice(oldIndex, 1)[0] if (oldIndex < newIndex) { newOptions.splice(newIndex - 1, 0, moved) } else { newOptions.splice(newIndex, 0, moved) } emit('update:options', newOptions) // 父组件用 v-model:options 接收 - 父组件绑定:
<SingleChoiceEditor v-model:options="question.options" />
5.3 现象:小程序端wx.request报错request:fail net::ERR_CONNECTION_REFUSED
原因:本地调试时,后端 SpringBoot 服务运行在http://localhost:8080,但微信开发者工具默认不允许访问localhost,且未配置合法域名。
解决:
- 开发阶段:在微信开发者工具右上角「详情」→「本地设置」→ 勾选「不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书」;
- 部署阶段:后端必须部署到 HTTPS 域名(如
https://survey.yourdomain.com),并在小程序后台「开发管理」→「开发版本」→「服务器域名」中添加该域名(注意:request合法域名不支持 IP 或localhost); - 额外验证:用
curl -I https://survey.yourdomain.com/api/v1/wx/login检查是否返回200 OK,排除 Nginx 反向代理配置错误。
5.4 现象:Vue 后台导出 Excel 时,中文标题乱码(显示为??)
原因:Hutool 的ExcelWriter默认用GBK编码写入,而 Excel 2007+ 标准要求UTF-8+ BOM 头。
解决:
- 导出方法中显式指定
charset并写入 BOM:// 使用 Hutool 5.8.22+ ExcelWriter writer = ExcelUtil.getWriter(true); // true 表示创建 .xlsx writer.write(dataList, true); // 设置响应头(SpringBoot Controller) response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8"); response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode("问卷统计.xlsx", "UTF-8")); // 关键:写入 BOM 头 ServletOutputStream out = response.getOutputStream(); out.write(new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF}); // UTF-8 BOM writer.flush(out, true); // true 表示关闭流
提示:这个 BOM 头是 Excel 识别 UTF-8 的“后悔药”,不加的话,即使响应头写了
charset=UTF-8,Windows 上 Excel 仍会用 ANSI 打开。
6. 毕设答辩加分技巧:用「可验证的性能数据」代替空泛描述
6.1 给答辩老师看的「三张截图」,比十页 PPT 更有力
很多同学答辩时说“系统响应快”“并发能力强”,老师只会礼貌微笑。真正打动人的,是三张带时间戳、带参数、带结论的实测截图:
| 截图类型 | 获取方式 | 为什么有效 |
|---|---|---|
| Postman 压测报告 | 用 Postman Runner 对/api/v1/answer/submit接口发起 100 并发 × 10 次请求,截图 Summary Tab 中的Average Response Time < 350ms、Error Rate = 0% | 证明后端接口在真实并发下稳定,不是单次请求的“运气好” |
| Vue DevTools 性能面板 | 在管理端「问卷列表页」打开 Vue DevTools → Performance Tab → 录制一次完整加载过程,截图 Flame Chart 中render阶段耗时 < 80ms | 证明前端无严重卡顿,组件设计合理(比如没在v-for里写复杂计算) |
| 小程序真机调试 Network | 在 iPhone 真机上打开开发者工具 → Network → 提交一份问卷,截图submit请求的Time列为1.2s,Size列为245B | 证明小程序端网络请求轻量,首屏加载快,不是靠模拟器“作弊” |
注意:截图必须带系统时间(右下角)、工具窗口标题栏(显示 Postman/Vue DevTools/WeChat DevTools)、关键数值高亮(用红色方框圈出)。不要只贴代码或架构图。
6.2 论文里必须写的「两个对比实验」,让工作量可视化
毕设论文最容易被质疑“工作量不足”。用两个小实验,把“我做了什么”变成“我解决了什么问题”:
实验一:JWT Token 校验 vs Session 校验的吞吐量对比
- 方法:用 JMeter 对同一接口(
/api/v1/answer/submit)分别测试两种鉴权方式; - 参数:100 线程,Ramp-up 为 1 秒,循环 10 次;
- 结果:JWT 方式平均 TPS 为 218,Session 方式为 142(因需查 Redis Session);
- 结论:选用 JWT 减少 35% 后端查询压力,更适合问卷类读多写少场景。
实验二:ECharts 饼图渲染 50 个选项 vs 限制为 10 个的帧率对比
- 方法:在 Vue 页面中动态生成 50 个
data项,用 Chrome Performance 录制渲染过程; - 结果:50 项时
render阶段峰值 180ms,掉帧;限制为 10 项后稳定在 45ms; - 改进:在后端
/api/v1/statistics/pie接口中增加limit=10参数,默认只返回 Top10 选项,前端加「查看更多」按钮按需加载。
这两个实验不需要你发明新算法,只要用标准工具跑一遍,把数字写进论文「系统测试」章节,老师立刻明白:你不仅会搭,还会测、会调、会决策。
6.3 答辩现场的「一句话钩子」:从技术细节跳到业务价值
当老师问“这个系统有什么实际价值?”,别再说“方便收集数据”。换成这句:
“它把一份纸质问卷的回收周期,从平均 3 天压缩到实时可见——上周某导师用它收 200 份课程反馈,提交后 17 秒,Vue 后台的饼图就自动刷新,他当场根据‘教学节奏太快’这个高频选项,调整了下周教案。”
这句话里藏着三个可信锚点:
- 具体数字(200 份、17 秒、3 天);
- 真实角色(某导师,非虚构但去标识);
- 动作闭环(看到数据 → 发现问题 → 调整教案),证明系统不是玩具,而是能驱动决策的工具。
我带过的几届学生里,凡是在答辩时自然说出类似话术的,基本都拿了优秀。因为老师听了一上午“用了 SpringBoot”,突然听到“17 秒”,耳朵就竖起来了。技术是骨架,故事才是血肉。希望帮到你。
本文还有配套的精品资源,点击获取