简介:这份资源是微信小程序共享雨伞租赁系统的完整毕业设计文档,面向计算机相关专业学生及需要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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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_type=authorization_code"; ResponseEntity<String> 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. 生成自己的 token(JWT 或简单 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,这会在该行上加排他锁,第二个并发请求会阻塞直到第一个事务提交,然后读到 status=1 直接抛异常。参数上@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 层判断状态,两个线程都读到 status=0。解决:用SELECT ... FOR UPDATE在事务内锁定该行,或者用乐观锁UPDATE umbrella SET status=1 WHERE id=? AND status=0判断 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 RedisTemplate<String, 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); List<RentalOrder> 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) | 无 | — |
最后说个血泪经验:这个系统最容易被忽略的不是代码,而是二维码的物理维护。伞上的二维码被雨淋湿、被刮花,用户扫不出来就会打客服电话。我后来在每把伞的伞柄内侧也贴了一个备用码,成本几乎为零,但客服量降了一半。做这类软硬结合的项目,别只盯着屏幕,多想想线下会发生什么。希望帮到你。
本文还有配套的精品资源,点击获取