背景痛点:为什么“能跑”≠“能毕业”
每年答辩季,老师最爱问的三句话:
- “如果两个人同时下单最后一只笼子,你怎么保证不超卖?”
- “订单状态是‘已支付’,但库位没锁住,寄养师却点了‘拒绝’,数据对不上,谁背锅?”
- “演示时明明点了支付,刷新页面又回到‘待支付’,这是特性还是 Bug?”
大多数同学把功能点堆完就以为大功告成:界面能跑、数据库有数据、PPT 做得飞起。结果现场一并发,并发问题、事务缺失、状态漂移全暴露。老师一句话:“代码健壮性不够,工作量不饱满”,直接打回重做。
痛点总结:
- 没有事务边界,支付成功写库失败,订单卡在“中间态”。
- 订单状态靠 if-else 硬编码,新增一个“退款中”状态要改七八个类。
- 接口不做幂等,用户双击提交就生成两条订单,老板看了直摇头。
- 测试数据随手插,主键 ID 写死,现场演示一重启全乱套。
想一次过关,得把“能跑”升级为“可靠”。下面给出一条“新手也能复制”的落地路线。
技术选型:Spring Boot 为什么更适合“小白”快速出活
毕业设计周期通常 8-10 周,还要留时间写论文、做 PPT。选技术栈的第一指标是:开发效率 + 身边能问到人。
| 框架 | 上手机器成本 | 生态成熟度 | 打包部署 | 身边可问到的“学长”数量 |
|---|---|---|---|---|
| Django | 低,但 Python 语法对纯 Java 课背景学生有切换成本 | 高 | 需要额外解决静态文件收集 | 少 |
| Node.js(Koa/Nest) | 中,异步思维门槛高,毕设答辩老师多数看不懂 Promise 链 | 高 | 单文件可跑,但 TS 编译、部署脚本要自己写 | 一般 |
| Spring Boot | 高,但国内教材、网课、身边代码库全是 Spring 系列;IDEA 一键生成工程 | 极高 | 直接 java -jar,内嵌 Tomcat,服务器只需装 JRE | 极多 |
结论:Spring Boot 不是最“潮”,却是最能在有限时间内让你把“事务、状态机、安全、部署”一条龙跑通,而且老师、学长、GitHub 样例一搜一大把,出了问题能搜到答案,比炫技更重要。
核心实现:用户-宠物-订单三元模型 + 状态机
1. 业务实体关系
- 一个用户(User)可拥有多只宠物(Pet)。
- 一个用户可下多个订单(Order),但一个订单只寄养一只宠物。
- 订单与笼位(Cage)多对一:一个笼位在同一时段只能被一个订单占用。
ER 图简化如下:
2. 订单状态机——别写 if-else,用枚举 + 状态模式
订单状态一共 5 个:
WAIT_PAY待支付PAID已支付BOARDING寄养中FINISHED已完成CANCE_refund退款中(扩展备用)
状态迁移规则:
- 只有
WAIT_PAY能到PAID。 - 只有
PAID能到BOARDING。 - 只有
BOARDING能到FINISHED。
代码里用枚举把迁移规则写死,避免魔法值:
public enum OrderState { WAIT_PAY { @Override public OrderState pay() { return PAID; } @Override public OrderState cancel() { return CANCELLED; } }, PAID { @Override public OrderState start() { return BOARDING; } }, BOARDING { @Override public OrderState finish() { return FINISHED; } }, FINISHED, CANCELLED; public OrderState pay() { throw newIllegalTransition(); } public OrderState cancel() { throw newIllegalTransition(); } public OrderState start() { throw newIllegalTransition(); } public OrderState finish() { throw newIllegalTransition(); } private IllegalStateException newIllegalTransition() { return new IllegalStateException("非法状态迁移"); } }Service 层只关心业务动作,不盲写 if:
order.setState(order.getState().pay()); // 直接调用对应方法3. 关键代码:下单接口(含事务 + 幂等)
需求:同一用户、同一宠物、同一时段,只能存在一笔待支付订单。
实现要点:
- 数据库层给(user_id, pet_id, start_date, end_date, state)建联合唯一索引,状态为
WAIT_PAY时才算冲突。 - 程序层用 Redis 分布式锁(或简单
synchronized本机锁)兜底,防止 1 秒内并发打到不同 JVM。 - Service 方法加
@Transactional,保证“扣减笼位库存 + 写订单”原子性。 - 前端传入幂等令牌
idempotentToken,后端用INSERT ... ON DUPLICATE KEY UPDATE避免重复写。
代码片段(精简):
@RestController @RequiredArgsConstructor @RequestMapping("/api/orders") public class OrderController { private final OrderService orderService; @PostMapping public ApiResult<Long> create(@RequestBody CreateOrderRequest req, @RequestHeader String idempotentToken) { Long orderId = orderService.createOrder(req, idempotentToken); return ApiResult.success(orderId); } } @Service @RequiredArgsConstructor public class OrderService { private final OrderRepository orderRepo; private final CageRepository cageRepo; @Transactional public Long createOrder(CreateOrderRequest req, String token) { // 1. 幂等校验:令牌已存在则直接返回 Optional<Order> exist = orderRepo.findByIdempotentToken(token); if (exist.isPresent()) { return exist.get().getId(); } // 2. 库存预扣 Cage cage = cageRepo.findByIdWithLock(req.getCageId()) .orElseThrow(() -> new BizException("笼位不存在")); if (cage.getAvailable() <= 0) { throw new BizException("笼位已满"); } cage.decrement(); // 乐观锁版本号在 XML 里写 UPDATE ... SET available = available -1, version = version +1 ... // 3. 构建订单 Order order = Order.builder() .userId(req.getUserId()) .petId(req.getPetId()) .cageId(req.getCageId()) .startDate(req.getStartDate()) .endDate(req.getEndDate()) .state(OrderState.WAIT_PAY) .idempotentToken(token) .build(); orderRepo.save(order); return order.getId(); } }注意:
findByIdWithLock用SELECT ... FOR UPDATE把笼位行锁提前,避免两个订单同时读到“剩余 1 个”。- 事务范围只包住“库存扣减 + 订单写入”,支付回调、消息通知另起事务,减少锁时间。
- 幂等令牌建议用前端 UUID,放在 Header,防重复提交。
性能与安全:把“老师随口一问”提前解决
1. 冷启动延迟
Spring Boot FatJar 第一次解压、JVM 字节码验证、MyBatis 映射扫描,30 秒起步。演示现场重启一次,台下老师开始刷手机。解决:
- 本地演示用 Spring Boot 的
spring-context-indexer提前建索引,启动缩短 20%。 - 服务器用
java -server -XX:TieredStopAtLevel=1 -noverify先快速启动,再换成正常参数。 - 把 jar 放
/dev/shm内存盘,磁盘 IO 瓶颈消失。
2. SQL 注入
MyBatis 只写#{}占位符,不用${}字符串拼接;额外给 MySQL 账号开SELECT/INSERT/UPDATE权限,禁止DROP权限,即使代码有漏也炸不掉库。
3. 敏感信息脱敏
- 日志用
logback-desensitize正则把手机号、身份证打码。 - 返回给前端的 VO 用 Jackson 注解
@JsonSerialize(using = MaskSerializer.class)统一脱敏,避免谁忘了在前端“*”号。
生产环境避坑指南:那些“本地能跑,上线就炸”的细节
服务器时区
把 Docker 容器、MySQL、JVM 全设成Asia/Shanghai,写入docker-compose.yml:environment: TZ: Asia/Shanghai否则“今天下单明天入住”算天数会少一天,账单金额对不上。
测试数据隔离
写application-test.yml指定spring.datasource.url=jdbc:mysql://.../pet_test,并给 Test 容器建独立 Schema。演示前一键flyway clean migrate,保证主键自增 ID 从 1 开始,PPT 截图与现场数据一致。避免 N+1
订单列表要展示宠物名称、笼位编号。直接在OrderMapper.xml写LEFT JOIN pet p ON o.pet_id = p.id LEFT JOIN cage c ON o.cage_id = c.id,一次把列带回来。切忌 for 循环里再查宠物。日志级别
线上开INFO即可,MyBatis SQL 日志别开DEBUG,否则高并发瞬间把磁盘打满。用async-appender异步写日志,请求线程不阻塞。演示脚本
提前准备demo.sh:- 调用下单接口 3 次,生成 3 笔不同状态订单。
- 调用支付模拟回调,把订单推到
PAID。 - 调用“开始寄养”“完成寄养”把状态走到终态。 现场只需
./demo.sh | jq,JSON 高亮输出,老师一看“接口流畅、状态正确”,印象分直接 +20。
下一步:把“可用”升级成“好用”
整个系统已满足毕业答辩的“功能 + 可靠”双要求。但如果想继续炫技,可以思考:
- 微信小程序前端:用微信登录换取
openid当用户标识,后端新增wx_login接口,统一User表即可。模板消息推送“宠物喂食视频”提醒,体验更闭环。 - 引入 Redis 缓存:热点笼位库存、订单状态读多写少,用
Redis + Lua脚本做库存预扣,MySQL 压力骤降;还能给接口加RateLimiter,防学生室友帮你“压测”。
把这两个点写进论文“展望”章节,老师会觉得你“有产业视角”,分数再提一档。
以上就是在校生版“宠物寄养系统”落地笔记。代码量确不多,但把事务、状态、并发、安全、部署这些“工程化”细节串成线,足以让毕设从“能跑”进化到“敢上线”。祝你一次答辩通关,早日把键盘收好去毕业旅行。