背景痛点:为什么“能跑起来”≠“能毕业”
做智能快递柜毕设,最容易踩的坑不是代码写不出来,而是“功能一箩筐,一演示就翻车”。我当年也一样:
- 把“开柜”理解成“点按钮→弹门锁”,结果同一条取件码被两个人同时扫码,柜门弹了两次,包裹不翼而飞。
- 用一张单表记录柜格状态,字段只有
status=0/1,网络延迟重试时,前端连点三次“存件”,数据库直接变成负数。 - 答辩当天,评委老师一句“如果100个快递员同时存件,你的系统怎么保证不串柜?”当场社死。
归根结底,我们忽略了两个关键词:状态机与幂等性。把快递柜当成“带锁的格子”远远不够,它更像一个分布式状态节点:柜格空闲→已占用→待取件→已取件→空闲,每一步都要可追踪、可回滚、可并发安全。
技术选型:为什么不是 Flask 也不是 MongoDB
班里一半同学用 Python/Node 快速搭原型,确实两天就能跑通;但毕设不是 Demo,评委关心“可靠性、扩展性、工程化”。我最后选了 Spring Boot + MySQL + Redis,理由简单粗暴:
- Spring Boot 一站式:内嵌 Tomcat、AOP、事务、定时任务全配好,省得自己搭积木。
- MySQL 事务与行级锁:柜格状态变更是“钱”级别操作,必须 ACID;纯内存或 MongoDB 宕机就全丢。
- Redis 轻量级分布式锁:防止同一柜格并发开柜,SETNX 一条命令搞定,比 MySQL 乐观锁快十倍。
- 就业友好:校招 Java 岗位最多,把毕设写成 Spring 项目,简历直接贴 GitHub 地址,面试官秒懂。
核心实现:三张表 + 两条定时任务
1. 业务模型
- 柜格表
cabinet_cell(id, cabinet_id, cell_no, status, order_token, expire_time) - 订单表
delivery_order(id, order_token, take_code, status, create_time, take_time) - 快递员表
courier(id, name, sms_code, daily_counter)—— 防短信刷爆
status 全部用enum字面量,杜绝魔法数。
2. 状态机
- 空闲 → 已占用(快递员扫柜码,系统生成 order_token 与 take_code)
- 已占用 → 待取件(关门后硬件上报,后台把 status 改为 WAIT_TAKE)
- 待取件 → 已取件(用户输取件码,校验通过后弹柜)
- 已取件 → 空闲(关门上报,定时任务清理)
所有状态变更走CellService.changeStatus(Long cellId, CellStatus target, String expectedToken),内部用数据库行锁保证只有一笔成功。
3. 取件码生成
6 位数字太短,易被爆破;UUID 太长,大爷大妈输着崩溃。折中:
String takeCode = RandomStringUtils.randomNumeric(8); redisTemplate.opsForValue() .set(RedisKey.TAKE_CODE + takeCode, orderToken, 24, TimeUnit.HOURS);校验时直接查 Redis,O(1) 且自带 TTL,订单完成后同步删除。
4. 超时清理
Spring 的@Scheduled(cron = "0 0/30 * * * ?")每 30 分钟扫一次:
- 把
expire_time < now()且status=WAIT_TAKE的订单找出来; - 调用
changeStatus回滚柜格为空闲; - 给快递员发短信提醒“包裹已退回网点”。
代码片段:开柜接口 + 分布式锁
@RestController @RequestMapping("/cell") public class CellController { @Resource private CellService cellService; @Resource private StringRedisTemplate redisTemplate; /** * 用户输取件码开柜 */ @PostMapping("/open") public ApiResult<Void> open(@RequestBody OpenReq req) { // 1. 防刷:验证码错误 5 次即封 IP String lockKey = RedisKey.IP_LOCK + ServletUtil.getClientIP(RequestContext.get()); if (redisTemplate.opsForValue().increment(lockKey) > 5) { throw new BizException("操作过于频繁,请30分钟后再试"); } // 2. 取件码 -> 订单令牌 String orderToken = redisTemplate.opsForValue() .get(RedisKey.TAKE_CODE + req.getTakeCode()); if (orderToken == null) { throw new BizException("取件码已失效或错误"); } // 3. 分布式锁:保证同一柜格只能开一次 String cellLock = RedisKey.CELL_LOCK + orderToken; Boolean locked = redisTemplate.opsForValue() .setIfAbsent(cellLock, "1", 10, TimeUnit.SECONDS); if (!locked) { throw new BizException("柜格正在打开中,请勿重复点击"); } try { cellService.openCell(orderToken); // 内部改状态+调用硬件开门 } finally { redisTemplate.delete(cellLock); } return ApiResult.success(); } }- 10 秒过期防止死锁;
- finally 块必删,极端宕机也能靠 TTL 兜底。
性能与安全:高并发下的“柜门误开”怎么防
- 硬件层面:柜控板必须支持“开门指令序列号”,后台发送
open(uuid),硬件只执行序号最新的命令,重复包直接丢弃。 - 软件层面:Redis 锁 + 数据库唯一索引行锁双保险,压测 500 线程同时点“取件”,QPS 800+ 无串柜。
- 短信防刷:快递员每日限 50 条,超量走图形验证码 + 人工审核,成本 0 元。
- 前端防抖:按钮点击后 2 秒置灰,虽治标不治本,但能挡住 80% 误操作。
生产环境避坑:Mock 硬件 + 演示一键重置
1. 硬件 Mock
真机 2000 块一台,实验室只给借三天。我写了个HardwareMockServer:
@Component @Profile("mock") public class HardwareMockServer { @EventListener public void onOpenEvent(OpenCellEvent e)){ log.warn("[MOCK] 柜格{}已弹开,10秒后自动关门", e.getCellNo()); // 模拟关门上报 Executors.newSingleThreadScheduledExecutor() .schedule(() -> hardwareClient.reportClose(e.getCellNo()), 10, TimeUnit.SECONDS); } }Spring profile 设为mock,评委看演示时一样有“弹开→倒计时→自动关门”效果,成本 0 元。
2. 数据一键重置
答辩前老师最爱点“再存一个”。我在前端藏了“初始化”按钮,调用:
TRUNCATE delivery_order; UPDATE cabinet_cell SET status='FREE', order_token=NULL, expire_time=NULL;5 秒回到处女状态,老师随便玩。
效果展示
- 左侧实时显示 24 格状态,右侧滚动超时订单;
- WebSocket 推送,无需手动刷新,演示观感瞬间高级。
还能怎么玩?—— 多柜组网 & 小程序
单柜跑通后,只需要:
- 在
cabinet表新增gateway_url、secret_key,把不同学校的柜子当成节点; - 订单号加上
cabinet_id前缀,分布式数据库用sharding-jdbc按柜机分表; - 微信小程序原生扫码,调同一套后端 API,前端模板消息推送“包裹入柜”。
把这套思路写进论文“分布式快递柜网络架构设计”,老师直接给优秀。
写在最后
整个毕设做下来,代码量而不堆,功能少而精,评委更看重“为什么这样设计”而不是“用了多少框架”。如果你也在纠结“智能快递柜”怎么下手,不妨先跑通上面最薄的三张表 + 两条定时任务,再逐步加硬件、加并发、加网络。下一步,试试把柜机部署到实验室门口,让同学真·扫码取快递——当听见“嘭”一声柜门弹开,你会明白,所谓毕业设计,其实就是把课本里的“事务、锁、状态机”变成触手可及的烟火气。祝你答辩顺利,代码不崩。