拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

共享雨伞租赁系统实战:SpringBoot与微信小程序扫码租借全流程

共享雨伞租赁系统实战:SpringBoot与微信小程序扫码租借全流程 简介这份资源是微信小程序共享雨伞租赁系统的完整毕业设计文档面向计算机相关专业学生及需要Spring Boot实战项目的开发者帮助解决共享租赁场景下的系统设计与实现问题。文档围绕用户管理、雨伞类型管理、归还点管理、租赁订单管理、归还信息管理、租赁费用管理、留言板管理及系统管理等核心模块展开采用Java语言、Spring Boot框架与MySQL数据库在Windows 10环境下完成开发并配有摘要、目录、技术分析等完整论文结构。资源包共1个docx文件大小约3.07MB内容涵盖需求分析、系统设计到功能实现的完整流程。目前已有62人学习下载适合作为课程设计、毕业设计参考也可用于学习Spring Boot与微信小程序结合的项目开发思路帮助读者快速理解共享经济类系统的业务逻辑与数据库设计方法。1. 共享雨伞租赁系统从扫码到还伞SpringBoot 和微信小程序怎么分工地铁口出来突然下雨旁边伞架上一排二维码微信扫一下、付个押金、拿走伞到目的地找个伞架还回去——这套动作背后跑的就是共享雨伞租赁系统。它要解决的核心问题不复杂让用户用微信小程序完成扫码、租借、归还、扣费让运营方在后台看到每把伞在哪、谁在用、什么时候该收钱。技术选型上SpringBoot 做后端接口和业务逻辑微信小程序做用户端入口两者通过 HTTPS 接口通信这是目前最常见的组合。适合谁看如果你手上有类似的小程序项目要落地或者正在做 SpringBoot 的课程设计、毕业设计又或者想搞清楚「扫码租借」这类物联网轻量场景怎么用纯软件方案跑通这篇笔记能帮你把从建表到联调的路径走一遍。我不会只讲概念会把参数、接口、踩过的坑都摊开说。2. 先想清楚数据模型伞、订单、用户三张表怎么设计2.1 为什么共享雨伞的数据模型比想象中容易翻车很多人拿到这个题目第一反应是「不就三张表吗」然后建了用户表、雨伞表、订单表就开始写代码。跑到一半发现一把伞可能被多次租借每次租借要记录取伞时间、还伞时间、取伞地点、还伞地点、押金状态、租金金额用户可能同时租多把伞伞架本身也需要管理。如果一开始没把「伞的当前状态」和「历史订单」分开后面查「这把伞现在在哪」就得全表扫描订单表性能直接崩。我一般会把模型拆成四张核心表用户表user、雨伞表umbrella、伞架表umbrella_station、订单表rental_order。雨伞表里存一个 status 字段标记当前状态0 可借、1 已借出、2 维修中、3 丢失再存一个 current_station_id 表示当前所在伞架。订单表只记录历史流水不承担「当前状态」的查询职责。这样查「某伞架有多少把可借的伞」只需要SELECT COUNT(*) FROM umbrella WHERE current_station_id ? AND status 0走索引很快。用户表里要区分微信 openid 和系统内部 user_id。openid 是微信侧的唯一标识但不同小程序 appid 下 openid 不同所以内部还是要生成自己的主键。押金字段我建议单独放一张 deposit_record 表因为押金涉及退款流程和租赁订单的生命周期不一致混在一起后面对账会非常痛苦。2.2 建表 SQL 与关键字段说明下面是我实际用过的建表语句MySQL 8.0 下跑过字段类型和索引都调过-- 用户表 CREATE TABLE user ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 内部用户ID, openid VARCHAR(64) NOT NULL COMMENT 微信openid, nickname VARCHAR(64) DEFAULT NULL COMMENT 昵称, phone VARCHAR(20) DEFAULT NULL COMMENT 手机号, deposit_status TINYINT DEFAULT 0 COMMENT 押金状态 0未缴 1已缴 2已退, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 伞架表 CREATE TABLE umbrella_station ( id BIGINT NOT NULL AUTO_INCREMENT, name VARCHAR(128) NOT NULL COMMENT 伞架名称, address VARCHAR(256) DEFAULT NULL COMMENT 地址, longitude DECIMAL(10,7) DEFAULT NULL COMMENT 经度, latitude DECIMAL(10,7) DEFAULT NULL COMMENT 纬度, capacity INT DEFAULT 20 COMMENT 可容纳伞数, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT伞架表; -- 雨伞表 CREATE TABLE umbrella ( id BIGINT NOT NULL AUTO_INCREMENT, qr_code VARCHAR(64) NOT NULL COMMENT 二维码编号, status TINYINT DEFAULT 0 COMMENT 0可借 1已借出 2维修 3丢失, current_station_id BIGINT DEFAULT NULL COMMENT 当前所在伞架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_qr_code (qr_code), KEY idx_station_status (current_station_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT雨伞表; -- 租赁订单表 CREATE TABLE rental_order ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 订单号, user_id BIGINT NOT NULL, umbrella_id BIGINT NOT NULL, rent_station_id BIGINT NOT NULL COMMENT 租借伞架, return_station_id BIGINT DEFAULT NULL COMMENT 归还伞架, rent_time DATETIME NOT NULL, return_time DATETIME DEFAULT NULL, fee DECIMAL(10,2) DEFAULT 0.00 COMMENT 租金, status TINYINT DEFAULT 0 COMMENT 0进行中 1已完成 2异常, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_status (user_id, status), KEY idx_umbrella (umbrella_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT租赁订单表;逻辑说明umbrella表的idx_station_status联合索引是给「查某伞架可借伞列表」用的rental_order的idx_user_status是给「查用户当前进行中的订单」用的。参数上qr_code我建议用「伞架编号序号」的规则生成比如ST001-U003这样扫码后能直接解析出伞架信息减少一次查询。fee用 DECIMAL 而不是 FLOAT避免金额计算出现 0.30000000000000004 这种玄学问题。注意openid字段长度给 64 是留余量实际微信返回的 openid 是 28 位左右但不同渠道可能不同别卡太死。3. SpringBoot 后端扫码租借接口怎么写才不翻车3.1 微信登录换 openid 的接口链路小程序端调用wx.login()拿到临时 code传给后端后端拿 code appid secret 去调微信的jscode2session接口换 openid 和 session_key。这一步是后面所有业务的前提。SpringBoot 里我一般用 RestTemplate 或 WebClient 发这个请求返回结果解析后存用户表。RestController RequestMapping(/api/auth) public class AuthController { Value(${wechat.appid}) private String appid; Value(${wechat.secret}) private String secret; Autowired private UserService userService; PostMapping(/login) public Result login(RequestBody LoginDTO dto) { // 1. 用 code 换 openid String url https://api.weixin.qq.com/sns/jscode2session ?appid appid secret secret js_code dto.getCode() grant_typeauthorization_code; ResponseEntityString resp new RestTemplate().getForEntity(url, String.class); JSONObject json JSON.parseObject(resp.getBody()); String openid json.getString(openid); if (openid null) { return Result.fail(微信登录失败: json.getString(errmsg)); } // 2. 查库或注册 User user userService.findOrCreateByOpenid(openid); // 3. 生成自己的 tokenJWT 或简单 UUID 存 Redis String token userService.generateToken(user.getId()); return Result.ok(Map.of(token, token, userId, user.getId())); } }逻辑说明jscode2session的 code 只能用一次有效期 5 分钟所以后端拿到后要立即处理不能缓存。参数上grant_type固定authorization_code。返回的session_key不要下发给小程序端留在后端做解密用比如获取手机号。findOrCreateByOpenid里要做并发控制两个请求同时带同一个新 openid 进来可能插两条用INSERT ... ON DUPLICATE KEY UPDATE或先查后插加唯一索引兜底。3.2 扫码租借的核心事务与锁扫码租借的流程是用户扫伞上的二维码 → 小程序把 qr_code 传给后端 → 后端校验这把伞是否可借 → 校验用户押金是否已缴 → 创建订单 → 更新伞状态为已借出。这四步必须在一个事务里而且要考虑并发两个人同时扫同一把伞不能都租成功。Service public class RentalService { Autowired private UmbrellaMapper umbrellaMapper; Autowired private RentalOrderMapper orderMapper; Autowired private UserMapper userMapper; Transactional(rollbackFor Exception.class) public RentalOrder rent(Long userId, String qrCode, Long stationId) { // 1. 查伞加行锁SELECT ... FOR UPDATE Umbrella umbrella umbrellaMapper.selectByQrCodeForUpdate(qrCode); if (umbrella null) { throw new BizException(伞不存在); } if (umbrella.getStatus() ! 0) { throw new BizException(该伞当前不可借); } // 2. 校验押金 User user userMapper.selectById(userId); if (user.getDepositStatus() ! 1) { throw new BizException(请先缴纳押金); } // 3. 校验用户是否有未归还订单 int activeCount orderMapper.countActiveByUserId(userId); if (activeCount 0) { throw new BizException(您有未归还的雨伞); } // 4. 创建订单 RentalOrder order new RentalOrder(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setUmbrellaId(umbrella.getId()); order.setRentStationId(stationId); order.setRentTime(new Date()); order.setStatus(0); orderMapper.insert(order); // 5. 更新伞状态 umbrella.setStatus(1); umbrella.setCurrentStationId(null); umbrellaMapper.updateById(umbrella); return order; } }逻辑说明selectByQrCodeForUpdate对应 SQL 是SELECT * FROM umbrella WHERE qr_code ? FOR UPDATE这会在该行上加排他锁第二个并发请求会阻塞直到第一个事务提交然后读到 status1 直接抛异常。参数上Transactional的rollbackFor Exception.class确保任何异常都回滚别用默认的只回滚 RuntimeException。generateOrderNo我一般用「时间戳用户ID后四位随机数」长度控制在 32 以内。提示如果并发量不大FOR UPDATE完全够用。如果伞架分布在多个城市、单表数据量大可以考虑按 station_id 分表但那是后话别过早优化。3.3 归还接口与费用计算归还的逻辑是用户扫伞架上的二维码或直接在小程序里点归还→ 后端根据当前用户查进行中的订单 → 计算租金 → 更新订单状态和伞状态。费用计算规则我见过几种按小时计费、按天计费、前 30 分钟免费之后每小时 1 元。不管哪种核心是把计费规则抽成独立方法别散落在业务代码里。public BigDecimal calculateFee(Date rentTime, Date returnTime) { long minutes (returnTime.getTime() - rentTime.getTime()) / (1000 * 60); if (minutes 30) { return BigDecimal.ZERO; } long hours (minutes - 30 59) / 60; // 不足一小时按一小时 return new BigDecimal(hours).multiply(new BigDecimal(1.00)); }逻辑说明(minutes - 30 59) / 60是向上取整的写法避免用 Math.ceil 带来浮点问题。参数上免费时长和单价建议放配置表或 Nacos 配置中心别硬编码运营随时可能调价。归还时同样要对伞加锁防止同一把伞被两个归还请求同时处理。4. 微信小程序端扫码、请求封装和状态同步4.1 扫码接口与页面跳转小程序端调wx.scanCode拿到二维码内容然后带着 token 调后端租借接口。这里有个细节扫码结果可能是伞的 qr_code也可能是伞架的编号要在前端做一次判断或者统一由后端解析。// pages/scan/scan.js Page({ data: { scanning: false }, onScanTap() { if (this.data.scanning) return; this.setData({ scanning: true }); wx.scanCode({ onlyFromCamera: false, scanType: [qrCode], success: (res) { const qrCode res.result; this.rentUmbrella(qrCode); }, fail: () { wx.showToast({ title: 扫码取消, icon: none }); }, complete: () { this.setData({ scanning: false }); } }); }, rentUmbrella(qrCode) { wx.showLoading({ title: 处理中 }); wx.request({ url: getApp().globalData.baseUrl /api/rental/rent, method: POST, header: { Authorization: wx.getStorageSync(token) }, data: { qrCode: qrCode }, success: (res) { if (res.data.code 200) { wx.showToast({ title: 租借成功 }); wx.navigateTo({ url: /pages/order/detail?id res.data.data.id }); } else { wx.showToast({ title: res.data.msg, icon: none }); } }, fail: () { wx.showToast({ title: 网络异常, icon: none }); }, complete: () wx.hideLoading() }); } });逻辑说明scanType: [qrCode]限制只识别二维码避免扫到条形码。scanning标志位防止用户连点导致重复请求。参数上onlyFromCamera: false允许从相册选图方便测试。请求头里的 token 从本地缓存取登录后写入。4.2 请求封装与 token 过期处理小程序里如果每个页面都写一遍wx.request后面改 baseUrl 或加统一错误处理会非常痛苦。我一般封装一个 request 工具统一处理 token 注入、401 跳登录、loading 显示。// utils/request.js const BASE_URL https://your-domain.com; function request(options) { return new Promise((resolve, reject) { const token wx.getStorageSync(token); wx.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, Authorization: token || }, success: (res) { if (res.statusCode 401) { wx.removeStorageSync(token); wx.navigateTo({ url: /pages/login/login }); reject(new Error(未登录)); return; } if (res.data.code ! 200) { wx.showToast({ title: res.data.msg || 请求失败, icon: none }); reject(new Error(res.data.msg)); return; } resolve(res.data.data); }, fail: (err) { wx.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); } module.exports { request };逻辑说明用 Promise 包一层页面里就能用await request({...})的写法。401 统一跳登录页避免每个接口单独判断。参数上Content-Type固定application/json如果后端用 form 接收要改成application/x-www-form-urlencoded。注意小程序请求域名必须在微信公众平台配置合法域名本地开发时可以在开发者工具里勾选「不校验合法域名」但上线前一定要配好 HTTPS 域名。5. 避坑与排查那些让我加班到凌晨的问题5.1 扫码后提示「伞不存在」但数据库里明明有现象用户扫码后端返回伞不存在但去数据库查 qr_code 确实存在。原因通常是二维码内容带了前后空格或换行或者小程序端res.result拿到的是完整 URL 而不是纯编号。解决后端在selectByQrCode之前先qrCode.trim()如果二维码是 URL 格式用正则提取最后一段。我一般会在生成二维码时就只放纯编号不放 URL。5.2 并发租借同一把伞两个人都成功了现象压测时两个请求同时租同一把伞都返回成功数据库里出现两条进行中订单。原因是没有加行锁或者用了SELECT之后在 Java 层判断状态两个线程都读到 status0。解决用SELECT ... FOR UPDATE在事务内锁定该行或者用乐观锁UPDATE umbrella SET status1 WHERE id? AND status0判断 affected rows 是否为 1。我一般用悲观锁简单直接。5.3 归还时费用算出来是负数现象用户还伞费用显示 -0.5 元。原因是 rent_time 和 return_time 取了不同时区的时间或者 return_time 比 rent_time 还早服务器时间被改过。解决统一用数据库的NOW()或后端new Date()别用小程序端传的时间。计算前先判断returnTime.after(rentTime)不满足就抛异常并记录日志。5.4 小程序端 token 过期后页面白屏现象用户放了一天再打开小程序点任何按钮都没反应。原因是 token 存在 Redis 里设了 24 小时过期但前端没处理 401请求失败后 Promise reject 没人 catch。解决在 request 封装里统一处理 401 跳登录页面里用 try-catch 包住 await 调用或者用.catch(() {})兜底。5.5 押金退款后用户还能继续租伞现象用户申请退押金押金状态改成已退但还能扫码租伞。原因是租借接口只校验了deposit_status ! 1就抛异常但退款流程里可能先改了状态再处理退款中间有时间窗口。解决退款时先冻结用户加一个frozen字段等退款完成再解冻并改押金状态。或者租借时同时校验「押金已缴」和「无进行中退款单」。6. 进阶用定时任务和状态机把异常订单收干净系统跑起来之后最烦的不是正常租还而是异常订单用户租了伞一直不还、伞被借走后伞架离线、订单状态卡在「进行中」好几天。我一般会加一个定时任务每天凌晨扫一遍超过 48 小时未归还的订单自动标记为异常并给用户发订阅消息提醒。SpringBoot 里用Scheduled就能做但要注意多实例部署时加分布式锁否则每个实例都跑一遍。Component public class OrderMonitorTask { Autowired private RentalOrderMapper orderMapper; Autowired private RedisTemplateString, String redisTemplate; Scheduled(cron 0 0 2 * * ?) // 每天凌晨2点 public void checkOverdueOrders() { String lockKey lock:order:monitor; Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, 1, 30, TimeUnit.MINUTES); if (Boolean.FALSE.equals(locked)) { return; // 其他实例已在执行 } try { Date deadline DateUtils.addHours(new Date(), -48); ListRentalOrder overdue orderMapper.selectOverdue(deadline); for (RentalOrder order : overdue) { order.setStatus(2); // 异常 orderMapper.updateById(order); // 发订阅消息提醒用户 sendRemindMessage(order.getUserId(), order.getUmbrellaId()); } } finally { redisTemplate.delete(lockKey); } } }逻辑说明setIfAbsent是 Redis 的 SETNX只有第一个实例能拿到锁锁过期时间 30 分钟防止死锁。参数上 cron 表达式0 0 2 * * ?表示每天 2:00 执行避开白天高峰。selectOverdue的 SQL 是SELECT * FROM rental_order WHERE status 0 AND rent_time #{deadline}走idx_user_status索引。验证方法本地测试时把 cron 改成0 */1 * * * ?每分钟跑一次手动插一条 rent_time 是 3 天前的订单看是否被标记为异常。上线前记得改回来。状态机方面我建议把订单状态流转画成一张表每个状态允许的操作写清楚别在代码里到处 if-else。比如「进行中」只能变成「已完成」或「异常」「已完成」不能再变。这样后面加「暂停计费」「续借」功能时不会乱。当前状态允许操作目标状态进行中(0)归还已完成(1)进行中(0)超时未还异常(2)异常(2)人工处理已完成(1)已完成(1)无—最后说个血泪经验这个系统最容易被忽略的不是代码而是二维码的物理维护。伞上的二维码被雨淋湿、被刮花用户扫不出来就会打客服电话。我后来在每把伞的伞柄内侧也贴了一个备用码成本几乎为零但客服量降了一半。做这类软硬结合的项目别只盯着屏幕多想想线下会发生什么。希望帮到你。本文还有配套的精品资源点击获取
返回列表