首页 / 资讯中心 / 文章详情

基于SpringBoot+Vue+微信小程序的行李寄存系统实战:状态机与并发防超卖

基于SpringBoot+Vue+微信小程序的行李寄存系统实战:状态机与并发防超卖 ★ FEATURED ARTICLE
简介这是一套面向Java全栈开发者与计算机专业学生的出行行李寄存系统完整项目采用SpringBootVue微信小程序前后端分离架构解决出行场景下行李寄存的线上化需求。资源包共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/payCallbackSpringBoot 把订单置为「已支付待存」并下发开柜指令用户存完在小程序点「已存入」状态变「已存入」取件时输入取件码校验后开柜状态变「已取件」同时结算费用。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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单表状态机核心 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;订单表的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-typegetPhoneNumber上不能直接调用这是微信的硬性限制。后端拿到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 } });页面里用stylepadding-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现象本地跑得好好的部署后小程序调接口 404Vue 后台正常。原因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)校验时比对哈希。这样即使数据库被拖取件码也不会直接泄露。盐值放配置文件不要硬编码在代码里。这套系统我前后改过三版第一版没加锁高峰期超卖第二版加了锁但没做幂等支付回调重复开柜第三版才把状态机和日志补齐。如果你正准备做行李寄存系统建议先把状态机和柜格锁这两块钉死剩下的都是体力活。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站