1. 毕设选题为什么盯上“扶贫爱心超市”
1.1 一个传统爱心超市的真实痛点
扶贫爱心超市这个题目,我最初是在毕设选题清单里扫到的,第一眼觉得也就是个“进销存换皮”,但真正做需求调研时才发现,这类公益物资管理场景的痛点远比想象中扎手。
我当时实地看过两个线下运营点,情况高度一致:墙上贴着积分兑换规则,货架上摆着米面油和洗衣液,但柜台底下压着一摞手写登记本。每一页记着“张三,3月12日,领大米5kg,扣50积分”。单看一条记录没毛病,但想盘点库存就得一页一页加,想知道哪些物资快过期了没人管,困难群众还剩多少积分得靠工作人员回忆,捐赠物资进来也只有一张手写收条。这些问题归纳起来有四类:
- 物资台账不统一。入库单、领用登记、库存余量散落在不同本子或不同人手里,核对一次要耗掉半天。
- 积分发放和核销缺约束。谁发的分、为什么发、扣了多少,流程是模糊的,事后追溯基本不可能。
- 多角色协同效率低。管理员、志愿者、受助群众之间信息不透明,受助群众到店才发现没货,或者有货但积分不够。
- 统计报表缺失。月度发放量、物资类别分布、积分核销率这些运营指标,靠手工根本算不出来。
正因如此,“信息化系统”才有明确的用武之地。把物资流转、积分流水、角色权限、统计报表串成一套系统,至少能把运营成本降下来,把数据留痕做扎实。这个题目最大的优势是:它有真实的业务原型,不管你面对哪个学校、哪个导师,对方都能快速理解你要做什么,需求论证环节几乎不会被卡。
1.2 从毕设角度评估这个题目的性价比
毕设选题最怕三种情况:题目太虚,讲不清楚做什么;技术太老,答辩老师觉得没含量;功能太杂,做到最后烂尾。扶贫爱心超市恰好避开了这三个坑。
从技术覆盖面上看,它踩中了 Java Web 毕设的标准考点:MVC 分层、MyBatis 持久层、数据库设计、事务管理、权限控制、图表统计。难度梯度很友好,既有基础功能的“保底分”,又有并发控制、状态机设计这类“加分项”。从业务角度看,它不是生造的题库项目,而是真实存在的公益场景,答辩时你能理直气壮地说“我做过业务调研”,这比那些通用“XX管理系统”的模板项目有说服力得多。
从开发量上做横向对比:电商系统的核心流程包含购物车、订单、支付、库存扣减、售后退款,随便一个模块就能撑起一篇毕设,但关联逻辑多、联调麻烦;纯信息管理系统,比如新闻发布、公告管理,做起来快但技术点单薄;爱心超市系统介于两者之间——核心流程清晰(入库、领用、积分、统计),业务规则不算复杂,一个人六周左右能完整拿下,还留有余地做界面优化和扩展功能。
我的结论很明确:这类“公益+信息化”的题目,在 Java 毕设里属于性价比很高的选择。它有一个明确的核心业务闭环,又不会像电商那样把战线拉得太长。
2. 系统总体设计:从业务流程到技术选型
2.1 先理清业务,再谈代码
很多同学拿到题目第一件事就是装环境写代码,这是毕设最常见也是最致命的错误。系统设计的第一张图纸应该是业务流程,不是数据库表,更不是 Controller 代码。
我拿到题目后做的第一件事,是把角色和业务动作画清楚。这个系统里有四类核心角色:
- 系统管理员:负责用户管理、物资审批、数据统计、系统配置。
- 管理人员(爱心超市运营者):负责物资入库、物资上架、积分发放、领用核销。
- 志愿者:可以协助管理物资、查看库存,但不具备积分发放权限。
- 受助群众(用户端):查看物资、查看积分、在线申请兑换、到店领取。
业务主链路是:社会捐赠或采购入库 → 物资审核上架 → 管理员发放积分 → 用户浏览商品 → 用积分提交兑换申请 → 管理人员核销出库 → 库存扣减、积分扣减、流水记录。
这条链路看起来简单,但每个环节都有分支。物资入库可能是捐赠入库,也可能是采购入库,捐赠涉及保质期登记;积分可能是定期定额发放,也可能是一次性活动奖励;兑换申请可能被驳回,驳回后积分要退回。这些分支在设计阶段就要全部列出来,落到数据库和接口上,否则后面半路加需求才是真折磨。
我建议动笔写代码之前,至少要产出三张图:
- 角色权限矩阵:每个角色能操作哪些菜单、哪些接口。
- 业务流程图:从物资入库到领用完成的完整链路,标注每个节点的操作人。
- 状态流转图:物资的上下架状态、兑换单的状态、账户积分的状态。
这三张图花不了多少时间,但对系统清晰度和答辩质量帮助极大。答辩时把这三张图往 PPT 上一放,老师基本能确认你是真做过东西,不是拿别人的代码改个标题。
2.2 技术选型:Spring Boot + MyBatis 的标准打法
Java 毕设的技术栈没什么悬念,Spring Boot 已经是绝对主流,几乎没人再用 SSH。我当时用的方案是:
- 后端框架:Spring Boot 2.7.x,配合 Spring MVC
- 持久层:MyBatis(配合 MyBatis Generator 生成基础 CRUD)
- 数据库:MySQL 8.0
- 前端:Vue 2 + Element UI(基于若依框架的前端模板改造),或者简单点用 Thymeleaf 做服务端渲染
- 构建工具:Maven
- 权限控制:Spring Security + JWT
为什么选 Spring Boot?理由很实在:它有自动配置,起步依赖帮你把版本兼容的坑填了一大半;内嵌 Tomcat,本地开发直接 run main 方法就能启动;配套的 Spring Security、Spring Data 生态成熟,文档充足,遇到问题搜得到答案。
持久层在 MyBatis 和 JPA 之间,我更推荐 MyBatis。原因有二:第一,毕设的增删改查如果全用 JPA 的自动方法,答辩时“你写的 SQL 在哪里”这种问题会很难收场;MyBatis 把 SQL 写在 Mapper XML 里,老师一眼能看到你亲手写的 SQL,话语权完全不一样。第二,MyBatis 对复杂查询、动态 SQL 的支持更直观,比如物资列表按条件筛选——物资名称模糊查询、分类筛选、上下架状态筛选——直接写动态 SQL 标签就行。
如果你用若依这类脚手架,会更省事,它已经把 Spring Security、Redis、定时任务、代码生成器都集成了,你只需要关注业务模块本身。但我的建议是:不管用不用脚手架,核心业务逻辑——库存扣减、积分流水——一定要自己完整写一遍,不要全部靠代码生成器生成完事,否则到答辩环节一深问就容易露馅。
2.3 模块怎么切:不走极端,但也不能一锅烩
模块划分直接决定开发效率和后期维护体验。有些人喜欢把功能全堆在几个 Controller 里,结果一个 UserController 几百行,后期想加功能到处找代码。
这个项目我建议按业务域划分,而不是按技术层划分:
- 系统管理模块:用户管理、角色管理、菜单权限。
- 物资管理模块:物资分类、物资信息、入库管理、出库管理、库存管理。
- 积分管理模块:积分发放、积分扣减、积分流水查询。
- 兑换管理模块:用户提交兑换申请、管理人员审核、核销出库。
- 统计报表模块:物资发放统计、积分核销统计、月度报表、图表展示。
- 平台门户模块:首页信息展示、公告发布、用户积分与物资浏览。
按业务域切分的好处是:每个模块对应一个包,包内再按 controller、service、mapper、entity 分层。这样既符合开发规范,又能在答辩时说清楚模块边界,不至于被问到“这个删除逻辑放哪个方法里”时当场卡壳。
同时我建议,不要在项目里设置无意义的过度抽象层。毕设项目不是企业级框架表演,抽象层次越多,阅读成本越高。简单直接:一个 Service 处理一个业务域的核心逻辑,用 @Transactional 管理事务,必要的地方加锁或使用乐观锁,完全够用。
3. 数据库设计:一张表一张表过
3.1 核心表的字段与关系
数据库是整个系统能跑起来的地基,也是答辩时最容易被追问的部分。我的设计里有九张核心表,挑重点说:
sys_user(用户表)
- 字段:id、username、password(BCrypt 加密存储)、real_name、phone、user_type(管理员/运营人员/志愿者/群众)、points_balance(积分余额)、status、create_time。
- 说明:user_type 字段用字典值存储,对应不同角色;积分余额直接冗余在用户表上,但必须配合积分流水表保证一致性,后面会详细讲。
sys_role 与 sys_user_role(角色与用户关联表)
- 虽然业务上可以按 user_type 区分角色,但还是建议把角色单独建模,因为不同用户可能会有多种身份,答辩时也更规范。
goods_category(物资分类表):id、name、sort_order。
goods(物资表):id、category_id、name、unit(单位:kg/袋/桶)、specification(规格:5kg/10kg)、stock_quantity(当前库存)、point_price(所需积分)、status(0下架 1上架)、create_time。
- 注意:当前库存是冗余字段,真正的权威数据在库存流水表中,每次出入库都写流水并更新快照。
stock_record(库存流水表):id、goods_id、change_type(1入库 2出库 3盘点调整)、change_quantity(正负值)、before_quantity、after_quantity、operator_id、remark、create_time。
- 这张表是整个物资模块的核心,任何库存变化都要在这里留下记录。
points_record(积分流水表):id、user_id、change_type(1发放 2兑换扣减 3退回)、change_points(正负值)、before_points、after_points、order_id(关联兑换单,可为空)、remark、create_time。
exchange_order(兑换单表):id、order_no、user_id、total_points、status(0待审核 1已通过待领取 2已领取 3已驳回)、reviewer_id、review_time、create_time。
exchange_order_item(兑换单明细表):id、order_id、goods_id、goods_name、point_price、quantity、subtotal。
donation_record(捐赠记录表):id、donor_name、donor_phone、goods_name、goods_quantity、donation_time、accept_admin_id、remark。
这个表结构的主线逻辑是:所有余额和库存都是“冗余快照”,真正的信任来源是流水表。每次发积分、扣积分、入库、出库,都必须写流水,然后更新快照字段,这两步必须放在同一个数据库事务里。
3.2 积分流水设计:别把余额当存储直接改
积分是受助群众在平台里的“资产”,它的正确设计是流水式而非余额式。用户页面显示“当前积分:200”,这个 200 只是给用户看的快照;真正判断操作是否合法,要看积分流水的合计。否则工作人员手滑直接 UPDATE 用户表把余额改成 999,这条数据的来源就完全无法追溯了。
积分发放和扣减的推荐做法,是用一条积分流水分录记录每一次变动的前后值。发放逻辑做成一个“加积分”的 Service 方法:校验发放对象、查当前余额、计算新余额、插入流水、更新余额,全部包在一个事务里。同理,兑换扣减积分时,要校验余额是否充足,扣减成功后生成兑换单明细与积分流水。如果兑换单被驳回,则新增一条积分退回流水。这样整个积分的生命周期就有了完整审计链。
这里有一个细节很多人会忽略:points_record表里的 before_points 和 after_points 字段一定要记录,不要嫌冗余。这两个字段将来在做对账、排查数据异常时就是救命稻草,答辩时也是可以主动展示的设计亮点。
3.3 物资状态机:从入库到领用的状态流转
物资不是“进了系统就是上架”这么简单。我的项目里定义了完整的状态流转:
- 草稿(待入库):工作人员登记物资信息,但尚未实际到货。
- 已入库(在库可用):物资到货验收入库,库存增加。
- 已上架(可兑换):物资审核通过,用户端可见,产生积分兑换入口。
- 已下架(暂停兑换):库存不足、商品过期或需盘点时下架,用户端不可见但库存仍在。
- 已出库(核销/领用):用户提交兑换单,运营人员核销出库。
重点解释一下“兑换申请核销”的逻辑:用户提交兑换申请后,不直接扣库存和积分,而是生成兑换单(状态为待审核)。运营人员在后台审核时,判断库存是否充足、积分是否足够。审核通过后,库存和积分才真正扣减,兑换单状态变为“待领取”;用户到店领取时,运营人员点击“确认领取”,兑换单状态变为“已领取”;驳回时积分退回。
这个设计解决了核心一致性问题:防止“用户在页面上看到库存数量,提交申请却发现库存已经没了”这种尴尬。当然也有人用“下单即锁定库存”的模式,但对公益系统来说,待审核更贴合实际运营节奏,因为运营人员就是“人肉库存数据库”,人工审核本就是场景特色。
状态机的每一步变化都要在代码里用枚举或常量定义,不要让状态字段散落在一堆魔法数字里。后面写 SQL 条件筛选时,你会非常感谢这个设计。
4. 核心功能实现:不是所有代码都值得写
4.1 登录与角色权限:三分钟搞定?没那么简单
很多同学把登录功能理解为“用一张表存用户名密码,登录时查一下,Session 存个用户 ID,页面显示欢迎 XXX”。这确实是最基础的做法,但放到毕设里,“设计感”不够。
我这里用的是 Spring Security + JWT 的方案,支持前后端分离。前端登录成功后拿到 token,后续请求在请求头携带Authorization: Bearer xxx,后端通过自定义过滤器解析 token 并塞入 SecurityContext。这样做的好处:一是无状态,不依赖 Session,分布式部署时不用考虑 session 共享问题,老师问起来你能说得清楚;二是权限控制可以在方法级别用 @PreAuthorize 注解实现,比如“只有运营人员能调用积分发放接口”。
角色权限这块,我强烈建议做一个简单的权限矩阵,维护“角色-菜单-操作”的对应关系。界面上用 Vue Router 的前置路由守卫做按钮级控制,后端每个写操作接口上都必须加权限校验,绝对不能只靠前端隐藏按钮来“防盗”。我在项目里吃过亏:第一次做完只做了前端隐藏,后台管理接口直接被人拿 Postman 一路调到底,库存被改乱了之后才意识到权限必须从后端做。
还有一个容易被忽略的点:密码存储。如果明文存密码,老师翻一下数据库就能挑出毛病。用 BCrypt 加密,存储格式像$2a$10$...,登录时用 matches 方法比对。加盐逻辑内置,不用自己造轮子,两行代码的事。
4.2 物资入库与领用:库存扣减的正确姿势
物资模块最容易出问题的点,是库存扣减时出现负数或者数据不一致。
推荐做法,入库和出库都走统一的 Service 方法,所有操作必须包裹在事务里。以“发放物资(出库)”为例,伪代码如下:
@Transactional(rollbackFor = Exception.class) public void exchangeDelivery(Long orderId, Long adminId) { // 1. 查询兑换单,校验状态必须是"已通过待领取" ExchangeOrder order = exchangeOrderMapper.selectById(orderId); if (order == null || !ExchangeStatus.PENDING.equals(order.getStatus())) { throw new BusinessException("兑换单不存在或状态错误"); } // 2. 查询兑换单明细,逐个扣减库存,必须加行锁 List<ExchangeOrderItem> items = exchangeOrderItemMapper.selectByOrderId(orderId); for (ExchangeOrderItem item : items) { Goods goods = goodsMapper.selectByIdForUpdate(item.getGoodsId()); if (goods.getStockQuantity() < item.getQuantity()) { throw new BusinessException("【" + goods.getName() + "】库存不足"); } // 3. 扣减库存 goodsMapper.decreaseStock(goods.getId(), item.getQuantity()); // 4. 写库存流水 stockRecordMapper.insert(StockRecord.build(goods.getId(), -item.getQuantity(), adminId, "兑换出库")); } // 5. 更新兑换单状态为"已领取" exchangeOrderMapper.updateStatus(order.getId(), ExchangeStatus.FINISHED.getCode(), adminId); }关键点有两个。第一个是selectByIdForUpdate,这一步用SELECT ... FOR UPDATE给物资记录上了行锁,防止两个管理员同时核销同一商品时出现脏读。第二个是“先校验、后操作、再写流水”的顺序不能乱,校验不通过直接抛异常,事务回滚保证不会出现扣了库存但没写流水的情况。
4.3 积分兑换的并发控制:别让两个人同时花掉同一积分
积分并发问题比库存还要隐蔽。我压测的时候复现过一个经典问题:用户 A 只剩 50 积分,同时提交了两次兑换 50 积分的申请,第一单通过了,第二单如果后端只是“先查积分余额再扣减”,两次查询都拿到 50 分,就会导致积分透支。
解决方案我用的是乐观锁版本号。即在用户表上维护一个 version 字段,每次更新积分余额时带上版本条件:
UPDATE sys_user SET points_balance = points_balance - #{cost}, version = version + 1 WHERE id = #{userId} AND version = #{oldVersion}如果 update 影响行数为 0,说明版本不一致(有并发操作),直接提示“您的积分正在变动中,请刷新后重试”。一个 UPDATE 语句就解决了问题,不需要 Redis 分布式锁,毕设阶段数据库层面的乐观锁完全够用。
兑换单审核时同理:管理员点击“通过”的那一刻,要重新到明细里校验每件商品的库存并锁定,而不是直接用审核前查询到的库存快照做判断。上面代码里已经用selectByIdForUpdate处理了这件事,这里再强调一次。
4.4 数据统计与可视化:让答辩多点亮点
统计报表是爱心超市系统的“门面担当”。物资发放了多少、哪类物资消耗得最快、积分核销率怎么样、月度趋势如何,这些数据做出来,往 PPT 上一放,能把项目的信息化水平拉上一个档次。
我实现的时候用了一个 Dashboard 页面,包含几个核心图表:
- 物资发放总量趋势(近 6 个月折线图)
- 物资类别发放占比(饼图)
- 积分发放/核销月度对比(柱状图)
- 兑换单状态分布(漏斗图)
后端统一写一个 StatisticsController,用 group by 聚合查询返回 VO,前端直接用 ECharts 绑定。SQL 示例:
SELECT DATE_FORMAT(create_time, '%Y-%m') AS month, SUM(change_points) AS issue_points FROM points_record WHERE change_type = 1 GROUP BY month ORDER BY month;实测发现,如果表没做索引,这类 group by 聚合在数据量到 5 万行时会有明显掉速。所以建表时记得给 create_time 和 change_type 加联合索引。这种细节你自己不写根本发现不了,写了就是踩过坑的经验,答辩时可以主动提。
5. 实操过程:从环境搭建到跑通第一个接口
5.1 环境准备:JDK、MySQL、IDEA、Maven 一步到位
我用的版本组合是 JDK 8 + Maven 3.6 + MySQL 8.0 + IDEA 2023.1。注意 JDK 8 和 MySQL 8.0 的时区问题:数据库连接 URL 一定要加上serverTimezone=Asia/Shanghai,否则启动时会报服务器时区无法识别的错误。这个坑几乎每个第一次连 MySQL 8 的人都会踩一遍。
MySQL 建议本地安装即可,不用折腾容器化。安装时注意字符集选 utf8mb4,不然存生僻字或特殊符号会报错。建库建用户后,权限只给当前库,不要用 root 到处跑。IDEA 里几个配置:Maven 用阿里云镜像加速依赖下载,设置 JDK 版本为 1.8,打开注解处理器(lombok 需要)。这一步如果不配,后面写实体类用 @Data 注解时会直接编译报错。
5.2 从零搭起 Spring Boot 项目骨架
最快的方式是去 start.spring.io 生成骨架,勾选 Spring Web、MySQL Driver、Lombok,然后引入mybatis-spring-boot-starter、validation、jwt等依赖。项目结构我建议如下:
com.example.ahss ├── config // 配置类:MyBatis驼峰映射、跨域、JWT拦截器、安全配置 ├── controller // 接收请求、参数校验、返回统一响应体 ├── service // 业务逻辑层,事务和锁都在这 ├── mapper // MyBatis Mapper接口 ├── entity // 实体类 ├── dto // 请求/响应对象,避免直接把实体暴露给前端 ├── vo // 视图对象,比如统计图表数据 ├── common // 工具类、常量、统一异常、统一返回类 ├── enums // 状态枚举 └── AimsApplication.java统一响应体我写了一个Result<T> { code, message, data }类,所有 Controller 都返回这个结构。这是一件小事,但极大提升了前后端联调效率,也显得项目规范。全局异常处理器也要写,用 @RestControllerAdvice 捕获业务异常和系统异常,不要让堆栈信息裸奔给前端。
5.3 前端页面怎么处理:不精前端也能拿得出手
前端这块我不建议从零手搓。我的做法是使用 Vue 2 + Element UI + 若依框架的前端模板改造。若依自带表格、表单、分页、弹窗组件,改起来很快,你只需要专注业务页面。
具体说几个核心页面实现:
- 物资列表页:用 el-table 展示物资信息,顶部筛选框按分类、名称、状态筛选,分页参数传给后端。
- 兑换单审核页:el-table 带多选复选框,点审核弹出抽屉显示明细,通过/驳回按钮回调后端接口。
- 用户中心页:积分余额展示、待领取的兑换单 tab、可兑换物资卡片墙。
前后端跨域问题,在后端配一个 CorsConfiguration Bean,把允许的来源、方法、头都放开。本机开发联调阶段不用太纠结,能跑通就行。
6. 常见问题与排查技巧实录
6.1 数据库连接超时的排查
现象:项目启动后第一次请求耗时几秒,但过了一段时间再发请求直接报MySQL Connection is not available或Communications link failure。
原因:MySQL 默认 wait_timeout 是 8 小时。连接空闲久了被服务端断开,而连接池里的旧连接不知道,还在傻等。解决方式有两个:一是连接 URL 上加autoReconnect=true;二是在 HikariCP 配置里设置maxLifetime略小于 MySQL 的 wait_timeout,并设置idleTimeout。我项目里设的 maxLifetime 是 560000ms,等一段时间自动剔除失效连接,问题就消失了。
6.2 MyBatis 动态 SQL 的坑
动态 SQL 的坑主要出在<if test="...">的参数判断上。搜索条件是 String 类型的 name,判断为空要用name != null and name != '';参数是 Integer 类型的 status,判断就写成status != null。有些人乱用 OGNL 表达式,比如status != '',结果 0 被当成了空字符串,查询条件直接被丢弃,数据全量返回。
另一个常见坑是<foreach>标签在 in 查询里循环明细项,如果集合为空,会生成IN ()语法错误。解决办法是在外层套<if test="list != null and list.size() > 0">。
6.3 积分并发扣减的压测与修复
4.3 节已经说了乐观锁的解决方式,这里补充一下压测验证方法。我用 JMeter 开 20 个线程,并发提交同一用户的两次兑换申请。修复前,日志里能看到两次扣减都成功,用户积分出现负数;修复后,第二次 update 影响行数为 0,被乐观锁拦截,抛出提示。这个过程我录了屏,压在答辩 PPT 里,老师一看就能理解项目深度。
7. 答辩准备与项目扩展方向
7.1 答辩时老师最常问的问题
结合我旁听多场答辩和做评委的经验,老师面对这种系统类项目,翻来覆去就是这几个问题,提前准备好答案就好:
为什么不用 JPA 要用 MyBatis?
答:复杂查询和动态 SQL 写起来直观,答辩时能展示 SQL 能力;JPA 自动方法多做简单 CRUD。积分余额和积分流水怎么保证一致?
答:事务 + 流水表,先查流水合计再更新余额,版本号乐观锁控制并发。库存扣减遇到并发怎么办?
答:行锁(SELECT FOR UPDATE)+ 库存流水,事务包裹全部操作。系统部署方案是什么?
答:后端 jar 包 + 前端打包静态文件 + Nginx 反代 + MySQL,一台服务器就能跑。项目难点在哪里?
答:并发扣减的一致性和多维统计查询的性能优化。
不用背答案,但要能说到点上。老师最反感的是“数据一致性就是加个事务吧”这种听起来像背书的回答。
7.2 这个系统还能怎么扩展
毕设答辩之后,如果想继续做下去,扩展空间不小:
- 增加小程序端:让受助群众在手机上浏览物资、看积分、提交兑换,后端接口完全复用。
- 增加消息通知:兑换单审核结果、库存补货提醒,用 WebSocket 或短信推送。
- 增加可视化大屏:LED 大屏展示当日发放数量、积分消耗量、库存预警,效果非常直观。
- 对接条码/二维码:入库扫码录入,出库扫码核销,把线下操作和信息系统的距离拉近。
- 引入数据分析:根据历史领用数据预测哪些物资更容易缺货,提前补货。
这些扩展方向,选一个做,都能让系统从“毕设级别项目”升级成“可以落地试点的公益信息化方案”。
最后说点个人体会。这个项目最让我意外的不是技术本身,而是这些看似常见的 CRUD 功能,一旦放进真实业务场景里,到处都藏着一致性、权限、状态的细节。当初我以为“爱心超市”就是个增强版进销存,真正动手才发现积分流水、兑换审核、并发扣减这些设计,每一项都能写出一篇故事。如果你也在做类似的毕设,我的建议是不要急着写代码,先把业务流程画明白,把数据库表设计清楚,后面自然行云流水。希望这篇总结能让你少踩几个我踩过的坑,答辩顺利。