简介:这是一套面向Java全栈开发者与计算机专业学生的出行行李寄存系统完整项目,采用SpringBoot+Vue+微信小程序前后端分离架构,解决出行场景下行李寄存的线上化需求。资源包共1584个文件,约45.93MB,包含137个vue组件、273个js脚本、107个java后端源码、86个wxml与86个wxss小程序页面样式,以及sql建表脚本、json配置、png/jpg界面素材和bat启动脚本等,覆盖前端页面、后端接口、数据库与部署脚本各环节。系统功能涵盖用户注册登录、寄存点查询、在线预订、寄存时间选择、费用支付、取件验证,以及用户管理、寄存点管理与数据统计分析等后台模块,并集成RESTful API、微信授权登录与HTTPS传输。已有36人学习下载,适合作为课程设计、毕业设计或全栈练手参考,可帮助读者快速理解三端协作流程与业务落地思路。
1. 行李寄存系统为什么值得用 SpringBoot + Vue + 微信小程序重做一遍
景区门口、火车站地下层、演唱会散场口,都有一块写着「行李寄存」的牌子,背后往往是一本登记簿加一把钥匙。真做起来才知道,这套流程的痛点不在「存」和「取」,而在高峰期并发登记、超时计费、柜格状态同步这三件事上。用 SpringBoot 做后端、Vue 做管理端、微信小程序做用户端,是目前做行李寄存系统最稳的组合:小程序负责扫码、下单、取件码展示,Vue 后台负责柜格配置、订单核销、营收统计,SpringBoot 把订单状态机和计费规则收在一处,避免三端各算各的。
这套方案适合谁?适合想做一个能真实跑起来的物联网+业务混合系统的开发者,也适合景区、商场、酒店这类有寄存场景、想自建一套轻量系统的团队。它不追求高精尖,追求的是三端状态一致、计费可追溯、柜格不超卖。下面按「先立住原理、再动手复现、最后讲坑」的顺序拆开讲,中间会给出可直接抄的建表、接口和状态机代码。
2. 三端职责怎么切:SpringBoot 管状态、Vue 管配置、小程序管交互
2.1 为什么不让小程序直接连数据库
很多新手第一反应是「小程序直接调云开发数据库不就行了」。行李寄存的核心是订单状态机:待支付 → 已支付待存 → 已存入 → 待取件 → 已取件 → 已完成/超时。这个状态流转必须有一个权威方,否则用户端显示「已存入」、后台显示「待存」,柜门就开不了。SpringBoot 作为唯一权威,所有状态变更走同一个 Service,小程序和 Vue 都只是它的视图层。
另一个原因是计费。寄存按小时或按天计费,超时加收,涉及金额计算和退款,必须放在服务端,前端只展示结果。小程序端拿到的是「当前费用」和「应付金额」,不参与计算逻辑。
2.2 三端接口边界与数据流
| 端 | 技术栈 | 核心职责 | 典型接口 |
|---|---|---|---|
| 用户端 | 微信小程序 | 扫码开柜、下单支付、取件码展示 | /api/order/create、/api/order/pickup |
| 管理端 | Vue3 + Element Plus | 柜格配置、订单核销、营收报表 | /api/admin/locker、/api/admin/order |
| 服务端 | SpringBoot | 状态机、计费、柜格锁、支付回调 | /api/order/payCallback |
数据流是这样的:用户在小程序扫码,拿到柜格编号,调/api/order/create创建订单,SpringBoot 用 Redis 锁住这个柜格,生成订单号返回;用户支付后微信回调/api/order/payCallback,SpringBoot 把订单置为「已支付待存」并下发开柜指令;用户存完在小程序点「已存入」,状态变「已存入」;取件时输入取件码,校验后开柜,状态变「已取件」,同时结算费用。
2.3 柜格锁与并发防超卖
高峰期十个人同时扫同一个柜格,如果不加锁,会生成十个订单,柜门只开一次,剩下九个用户投诉。常见做法是用 Redis 的SETNX做分布式锁,key 是locker:lock:{lockerId},过期时间 30 秒,拿到锁的才能创建订单。
// LockerService.java 柜格锁定逻辑 public Order createOrder(Long lockerId, Long userId) { String lockKey = "locker:lock:" + lockerId; // SETNX 加锁,30秒自动过期,防止死锁 Boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, userId.toString(), 30, TimeUnit.SECONDS); if (Boolean.FALSE.equals(locked)) { throw new BizException("该柜格正在被占用,请稍后重试"); } try { Locker locker = lockerMapper.selectById(lockerId); if (locker.getStatus() != 0) { // 0=空闲 throw new BizException("柜格已被占用"); } locker.setStatus(1); // 1=锁定中 lockerMapper.updateById(locker); return orderMapper.insert(buildOrder(lockerId, userId)); } finally { // 注意:这里不立即释放锁,等支付回调或超时释放 } }这段代码的关键点是锁不立即释放。如果创建订单后就释放锁,另一个用户马上又能锁同一个柜格。正确做法是锁的过期时间覆盖「创建订单到支付完成」的窗口,支付回调里再显式删除锁。参数上,30 秒是经验值,微信支付一般 10 秒内回调,留足余量;如果业务允许货到付款,这个时间要拉长到 5 分钟。
3. 数据库与状态机:行李寄存系统最容易翻车的地方
3.1 四张核心表怎么设计
行李寄存系统不需要几十张表,四张就够:柜格表、订单表、计费规则表、用户表。柜格表存物理柜格,订单表存业务流水,计费规则表存不同区域的单价,用户表存微信 openid 和手机号。
-- 柜格表:一个柜格一行,支持大小格 CREATE TABLE `locker` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `locker_no` VARCHAR(32) NOT NULL COMMENT '柜格编号,如 A-01-03', `area_id` BIGINT NOT NULL COMMENT '区域ID,对应计费规则', `size` TINYINT NOT NULL COMMENT '0小格 1中格 2大格', `status` TINYINT DEFAULT 0 COMMENT '0空闲 1锁定中 2已占用 3故障', `version` INT DEFAULT 0 COMMENT '乐观锁版本号', UNIQUE KEY `uk_locker_no` (`locker_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单表:状态机核心 CREATE TABLE `order` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `order_no` VARCHAR(32) NOT NULL, `user_id` BIGINT NOT NULL, `locker_id` BIGINT NOT NULL, `status` TINYINT DEFAULT 0 COMMENT '0待支付 1已支付待存 2已存入 3待取件 4已取件 5已完成 6已取消', `pickup_code` VARCHAR(6) NOT NULL COMMENT '6位取件码', `start_time` DATETIME COMMENT '存入时间', `end_time` DATETIME COMMENT '取件时间', `amount` DECIMAL(10,2) DEFAULT 0 COMMENT '订单金额', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user` (`user_id`), KEY `idx_locker` (`locker_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;订单表的status字段是整个系统的黑匣子,所有业务逻辑都围绕它转。pickup_code是 6 位随机码,用户取件时输入,服务端校验后开柜。注意start_time和end_time分开存,计费时用TIMESTAMPDIFF算时长,不要用订单创建时间,因为用户可能支付后很久才去存。
3.2 状态机用枚举还是状态模式
新手容易写成if (status == 1) { ... } else if (status == 2) { ... }散落在各个 Service 里,改一个状态要翻十个文件。常见做法是用枚举定义合法流转,在 Service 入口统一校验。
// OrderStatus.java 状态枚举与合法流转 public enum OrderStatus { PENDING_PAY(0, "待支付"), PAID_WAIT_STORE(1, "已支付待存"), STORED(2, "已存入"), WAIT_PICKUP(3, "待取件"), PICKED(4, "已取件"), FINISHED(5, "已完成"), CANCELLED(6, "已取消"); private final int code; private final String desc; // 定义每个状态能流转到哪些状态 public static boolean canTransfer(int from, int to) { switch (from) { case 0: return to == 1 || to == 6; // 待支付 -> 已支付/已取消 case 1: return to == 2 || to == 6; // 已支付 -> 已存入/已取消 case 2: return to == 3; // 已存入 -> 待取件 case 3: return to == 4; // 待取件 -> 已取件 case 4: return to == 5; // 已取件 -> 已完成 default: return false; } } }canTransfer把流转规则收在一处,Service 里只调一次if (!OrderStatus.canTransfer(old, new)) throw ...。这样加状态、改规则只动一个文件。参数上,code用 0 开始连续编号,方便前端映射;不要用 100、200 这种跳跃编号,后期做报表聚合会难受。
3.3 计费规则怎么落到代码里
计费规则表存区域单价和计费单位,比如「A区小格 2元/小时,不足1小时按1小时」。服务端算费用时,先算时长,再按规则向上取整。
// BillingService.java 计费核心 public BigDecimal calcAmount(Order order, BillingRule rule) { long minutes = Duration.between(order.getStartTime(), order.getEndTime() == null ? LocalDateTime.now() : order.getEndTime()) .toMinutes(); // 向上取整:不足一个计费单位按一个算 long units = (minutes + rule.getUnitMinutes() - 1) / rule.getUnitMinutes(); BigDecimal amount = rule.getUnitPrice().multiply(BigDecimal.valueOf(units)); // 封顶价保护,防止用户忘记取件产生天价账单 if (rule.getMaxAmount() != null && amount.compareTo(rule.getMaxAmount()) > 0) { amount = rule.getMaxAmount(); } return amount; }unitMinutes是计费单位,按小时就是 60,按天就是 1440。maxAmount是封顶价,这个参数很关键,没有它用户存一周可能被收几百块,投诉率飙升。我一般设成日封顶价的 3 倍,既保护用户也保护平台口碑。
4. 微信小程序端:登录、扫码、取件码三个动作怎么接
4.1 微信登录获取手机号的正确姿势
小程序登录分两步:先wx.login拿 code 换 openid,再getPhoneNumber拿手机号。很多教程只讲第一步,导致用户下单时没有手机号,取件通知发不出去。
// pages/login/login.js 小程序登录 Page({ onLogin() { wx.login({ success: (res) => { // 第一步:code 换 openid 和 sessionKey wx.request({ url: 'https://your-domain/api/auth/login', method: 'POST', data: { code: res.code }, success: (loginRes) => { const { token } = loginRes.data; wx.setStorageSync('token', token); // 第二步:获取手机号,需要用户点击按钮触发 this.getPhone(token); } }); } }); }, getPhone(token) { // 注意:getPhoneNumber 必须由 button 的 open-type 触发 wx.getPhoneNumber({ success: (res) => { // res.code 是手机号凭证,发给后端解密 wx.request({ url: 'https://your-domain/api/auth/bindPhone', method: 'POST', header: { Authorization: token }, data: { phoneCode: res.code } }); } }); } });关键点:getPhoneNumber必须绑定在<button open-type="getPhoneNumber">上,不能直接调用,这是微信的硬性限制。后端拿到phoneCode后调微信接口换手机号,不要在前端解密。参数上,token存 storage,每次请求带在 header 里,过期时间设 7 天,和微信 session 对齐。
4.2 扫码开柜与取件码校验
扫码用wx.scanCode,拿到柜格编号后调创建订单接口。取件码是 6 位数字,用户输入后调/api/order/pickup,服务端校验订单状态和取件码,通过后下发开柜指令。
// pages/scan/scan.js 扫码存件 Page({ onScan() { wx.scanCode({ onlyFromCamera: true, // 只允许相机扫码,防止相册选图作弊 success: (res) => { const lockerNo = res.result; // 柜格编号 wx.request({ url: 'https://your-domain/api/order/create', method: 'POST', header: { Authorization: wx.getStorageSync('token') }, data: { lockerNo }, success: (orderRes) => { // 拿到订单号,跳转支付 wx.navigateTo({ url: `/pages/pay/pay?orderNo=${orderRes.data.orderNo}` }); } }); } }); } });onlyFromCamera: true这个参数建议加上,否则用户可以选相册里的二维码截图,容易被薅羊毛。取件码校验时,服务端要同时校验订单状态必须是「待取件」或「已存入」,且取件码匹配,两个条件缺一不可。
4.3 小程序顶部导航栏高度适配
小程序自定义导航栏时,顶部高度要动态计算,否则刘海屏和安卓机显示不一致。常见做法是用wx.getSystemInfoSync拿状态栏高度,加上导航栏固定高度。
// app.js 全局计算导航栏高度 App({ onLaunch() { const sysInfo = wx.getSystemInfoSync(); // 状态栏高度 + 44px 导航栏(iOS 标准) const navHeight = sysInfo.statusBarHeight + 44; this.globalData.navHeight = navHeight; this.globalData.statusBarHeight = sysInfo.statusBarHeight; }, globalData: { navHeight: 0, statusBarHeight: 0 } });页面里用style="padding-top: {{navHeight}}px"撑开。注意安卓部分机型statusBarHeight返回 0,要做兜底,默认给 20px。这个坑我踩过,测试机正常,用户机顶部被遮住,血泪经验。
5. 避坑与排查:行李寄存系统上线前必须过的五道坎
5.1 支付回调重复执行导致重复开柜
现象:用户支付一次,柜门开了两次,或者订单状态被改成「已存入」后又变回「已支付」。原因:微信支付回调可能重试,如果回调接口没有幂等处理,同一笔订单会被处理多次。解决:回调入口先查订单状态,如果已经是「已支付待存」或之后的状态,直接返回成功,不重复处理。用order_no做唯一索引,插入流水时靠数据库兜底。
5.2 柜格状态与订单状态不一致
现象:后台看柜格是「已占用」,但订单已经「已完成」,柜格释放不了。原因:订单完成和柜格释放是两个操作,中间如果抛异常,只完成了一个。解决:把「订单置为已完成」和「柜格置为空闲」放在同一个事务里,或者用消息队列做最终一致。我一般用本地事务表,订单完成后发一条消息,消费者负责释放柜格,失败重试三次。
5.3 取件码被暴力破解
现象:有人用脚本遍历 6 位取件码,试图开别人的柜子。原因:取件码只有 6 位数字,空间 100 万,如果没有频率限制,跑一遍不用太久。解决:同一用户连续输错 5 次锁定 10 分钟,同一 IP 每分钟最多请求 10 次。取件码生成时用SecureRandom,不要用Math.random。另外取件码只在订单维度有效,不要全局唯一,否则容易被枚举。
5.4 小程序请求超时但订单已创建
现象:用户点「存件」后网络卡顿,小程序提示失败,但后台已经生成了订单,柜格被锁。原因:网络超时和业务失败是两回事,前端把超时当失败处理了。解决:创建订单接口做成幂等,用userId + lockerId + 时间窗口做去重。前端超时后不要立即让用户重试,先查一次订单状态,如果已创建就跳支付页。这个坑在弱网环境特别常见。
5.5 SpringBoot 版本太高导致小程序端接口 404
现象:本地跑得好好的,部署后小程序调接口 404,Vue 后台正常。原因:SpringBoot 3.x 默认把路径匹配策略改了,或者 context-path 配置和小程序请求路径对不上。解决:检查server.servlet.context-path和小程序request的 baseUrl 是否一致。SpringBoot 3.x 用PathPatternParser,如果用了自定义拦截器,要确认匹配规则。我一般在小程序端把 baseUrl 抽成配置,切换环境只改一处。
6. 用状态机日志和压测把系统钉死
上线前我习惯做两件事:给订单状态变更加日志表,以及用 JMeter 压一遍创建订单接口。状态日志表很简单,每次状态变更插一条order_status_log,记录order_id、from_status、to_status、operator、create_time。出问题时不用猜,直接查这张表就知道谁在什么时候改了状态。这个习惯帮我省了无数次扯皮。
压测的重点是创建订单接口,因为它是并发入口。用 JMeter 开 50 个线程,每个线程循环 20 次调/api/order/create,观察 Redis 锁的命中率和数据库连接池。如果出现大量「柜格正在被占用」,说明锁粒度太粗,可以考虑按区域分片锁。压测时把日志级别调到 WARN,否则日志 IO 会成为瓶颈。
最后一个技巧:取件码不要存明文。虽然 6 位数字明文存也没大问题,但养成习惯,存MD5(pickup_code + salt),校验时比对哈希。这样即使数据库被拖,取件码也不会直接泄露。盐值放配置文件,不要硬编码在代码里。
这套系统我前后改过三版,第一版没加锁,高峰期超卖;第二版加了锁但没做幂等,支付回调重复开柜;第三版才把状态机和日志补齐。如果你正准备做行李寄存系统,建议先把状态机和柜格锁这两块钉死,剩下的都是体力活。希望帮到你。
本文还有配套的精品资源,点击获取