
简介一份围绕Spring Boot与微信小程序开发的共享雨伞租赁系统毕业设计文档面向需要完成类似课题的计算机专业学生、Java开发者及毕业设计人员。文档基于Windows 10环境采用Java语言、Spring Boot框架与MySQL数据库完整梳理了微信小程序端共享雨伞租借归还的业务流程。资源包内含1个docx文件整体大小3.07MB覆盖摘要、关键词、目录、绪论、课题研究背景与意义、国内外研究现状、系统开发技术分析、系统分析与设计、数据库设计、系统实现等内容结构完整。系统功能包括用户管理、雨伞类型管理、归还点管理、租赁订单管理、租赁费用管理、留言板管理等模块并给出了明确的技术架构与实现思路。目前已有62人学习下载对于准备Spring Boot毕业设计或共享经济类系统论文的读者可作为需求分析、系统设计、数据库设计与开发实现的参考范本。1. 共享雨伞从立项到落地为什么是 SpringBoot 微信小程序在上海的雨天地铁口十个人里有六个没带伞共享雨伞的需求从来不需要论证真正难的是“把伞锁好、把账算清、把状态对齐”。一套基于 SpringBoot 的微信小程序共享雨伞租赁系统要解决的不只是“扫码开锁”而是伞具状态、订单计费、押金流转、逾期归还和异常申诉这一整条链路。我拆过类似项目最大的感受是业务看起来轻坑全是藏在状态流转和并发扣款里的。这套资源覆盖了从数据库设计到小程序联调再到支付退款的完整闭环适合正在做课程设计、毕业设计或者想快速搭一个可上线原型的工程师。它不是花架子每个模块都能对到真实业务流程上拿它起步省掉的不只是时间还有踩坑成本。2. 系统架构与数据库设计三张核心表与状态机流转2.1 服务端分层与模块边界SpringBoot 后端我建议直接拆成标准三层Controller 只做参数接收和鉴权Service 持有业务规则Mapper 只负责 SQL。刚开始做共享雨伞这类系统时很容易把所有逻辑塞进 Controller结果改一个计费规则要动三层代码。这套资源里服务端按用户、伞具、伞点、订单、计费、支付六个模块切分模块间通过 Service 接口互相调用而不是直接跨表操作。订单模块和支付模块虽然在同一个进程里也要在代码层分开。这样后续如果要把支付回调拆成独立服务或者接消息队列不需要重写业务代码。我一般还会在每个模块里维护自己的异常码段比如订单异常从 5001 开始、支付异常从 6001 开始排查日志时能直接定位到模块。RestController RequestMapping(/api/umbrella) public class UmbrellaController { Autowired private UmbrellaService umbrellaService; GetMapping(/nearby) public Result nearby(RequestParam Double lat, RequestParam Double lng, RequestParam(defaultValue 3) Integer radiusKm) { return Result.ok(umbrellaService.findNearby(lat, lng, radiusKm)); } }这段代码是附近伞点查询的入口lat和lng是小程序端wx.getLocation拿到的坐标radiusKm默认传 3 公里。实际业务里不会把计算逻辑写在 Controller而是放到UmbrellaService里方便加缓存和做数据权限过滤。2.2 核心表结构伞具、订单、用户共享雨伞系统最核心的三张表是用户表、伞具表、订单表。伞具表要带当前状态空闲、使用中、维修、禁用和所在伞点 ID订单表要记录借出时间、归还时间、计费金额、押金状态用户表不需要冗余太多东西微信小程序场景下存 openid、昵称、押金账户即可。CREATE TABLE t_umbrella ( id bigint NOT NULL AUTO_INCREMENT, umbrella_no varchar(32) NOT NULL COMMENT 伞具编号, point_id bigint DEFAULT NULL COMMENT 当前所在伞点, status tinyint NOT NULL DEFAULT 0 COMMENT 0空闲 1使用中 2维修 3禁用, lng decimal(10,6) DEFAULT NULL COMMENT 经度, lat decimal(10,6) DEFAULT NULL COMMENT 纬度, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_umbrella_no (umbrella_no) ) ENGINEInnoDB COMMENT伞具表; CREATE TABLE t_order ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL, user_id bigint NOT NULL, umbrella_id bigint NOT NULL, borrow_time datetime DEFAULT NULL COMMENT 借出时间, return_time datetime DEFAULT NULL COMMENT 归还时间, amount decimal(10,2) DEFAULT 0.00 COMMENT 消费金额, deposit decimal(10,2) DEFAULT 0.00 COMMENT 押金, status tinyint NOT NULL DEFAULT 0 COMMENT 0待取伞 1使用中 2待确认归还 3已完成 4已取消 5退款中, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), KEY idx_umbrella_id (umbrella_id) ) ENGINEInnoDB COMMENT订单表;order_no必须加唯一索引这是后面处理微信支付重复回调的第一道防线。status我用数字而不是字符串枚举是为了减少数据库存储开销但代码里要维护一个状态枚举类来保证可读性。伞具表和订单表的关联不要直接用umbrella_no做外键业务上有跨数据库扩容的可能外键约束反而拖累性能。2.3 状态机设计借、还、逾期、损坏的流转共享雨伞最容易被轻视的就是状态机。伞具的状态和订单的状态不是一个东西伞具“使用中”不代表订单一定处于计费状态有可能是用户还了但锁上报延迟。所以我在状态机里把伞具状态、订单状态分开管理。订单的状态流转设计成待取伞 → 使用中 → 待确认归还 → 已完成待取伞超时可以自动取消待确认归还是在用户还伞后到蓝牙锁确认前的一个中间态超过 30 分钟没确认就触发人工干预或者保护性结算。订单状态触发动作后续动作0 待取伞用户扫码下单超时未取自动取消1 使用中蓝牙锁开锁成功计时开始2 待确认归还用户点击还伞锁状态上报确认3 已完成确认归还并扣款押金退回4 已取消超时未取或者用户取消押金退回状态流转的判断全部放在 Service 层不用数据库触发器。原因有两个第一业务上要记录每次状态变更的操作人、时间和原因触发器不好扩展第二跨多表的状态变更需要事务控制Service 层可以统一管理事务边界。这一点是我在实际项目中踩过坑才坚持下来的初期用触发器省了代码后期排错完全对着黑匣子无从下手。3. 小程序端与接口联调登录鉴权、蓝牙开锁与经纬度纠偏3.1 微信登录与 token 签发的完整链路小程序端调用wx.login拿到临时code服务端拿这个code去微信接口换取openid和session_key。注意session_key只能在服务端用绝不能下发到小程序端否则任何人都可以拿着它破解用户数据。我习惯在自己的服务端再签一个业务 token把openid绑定到这个 token 上后续接口只认这个 token。原因很直接微信的session_key有过期时间而且你不能在小程序里感知它过期自己签 token 可以控制有效期、支持续期还能在服务端主动踢人。PostMapping(/login) public Result login(RequestBody LoginRequest req) { String code req.getCode(); MapString, String wxResp wxService.code2Session(code); String openid wxResp.get(openid); User user userService.findOrCreateByOpenid(openid); String token JwtUtil.generateToken(user.getId(), 7 * 24 * 3600 * 1000L); return Result.ok(new HashMapString, Object() {{ put(token, token); put(userInfo, user); }}); }这段登录接口做了三件事换openid、查或建用户、生成 token。code2Session是服务端通过 HTTP 调微信接口不是小程序直连这是安全红线。token 过期时间我设置成 7 天小程序端每次启动时用wx.checkSession判断微信会话是否过期过期就重新走一遍登录否则静默续期。3.2 伞锁控制蓝牙指令与临时开锁码共享雨伞的锁是蓝牙锁小程序通过蓝牙 BLE 连接设备后往指定特征值写入开锁指令。这里有个关键点开锁指令不能是固定字符串比如设备商签名或者自研的临时开锁码否则抓包之后谁都能开别人的伞。做法是用户下单后服务端生成一个一次性开锁码带上订单号和一个 5 分钟的时间戳再用本地密钥做签名。小程序收到这个开锁码后把它连同订单号一起通过蓝牙写给锁。锁端拿到之后如果它具备联网能力会去服务端校验如果不联网就用内置密钥校验签名和解码。public String generateUnlockCode(Long orderId, Long umbrellaId) { JSONObject payload new JSONObject(); payload.put(orderId, orderId); payload.put(umbrellaId, umbrellaId); payload.put(ts, System.currentTimeMillis()); String base payload.toJSONString(); String sign HMAC_SHA256(base, secretKey); return BASE64_URL_SAFE(base . sign); }secretKey每个项目独立配置不要写死在代码里。注意开锁码要设计成一次性服务端在 Redis 里存一个unlock_use:${orderId}消费前先检查有没有用过用过就拒绝防止同一个订单被重复开锁。3.3 经纬度上报与“附近伞点”查询小程序端wx.getLocation拿到的是 GCJ-02 坐标也就是“火星坐标”。很多初次做地图功能的人会直接把 GPS 原始坐标传上来接下来查询出来的伞点位置在地图上偏移几十米到几百米属于玄学问题里最常见的一种。我的做法是统一坐标系小程序端拿到坐标后不做任何转换原样传给服务端服务端数据库存的就是 GCJ-02。千万不要在小程序里先转成 WGS-84 再上传因为地图底图和定位接口的坐标系不一致时你会陷入“为什么这里差了 80 米”的排查循环。附近伞点查询初期不用引 Elasticsearch一个简化版的 SQL 就能撑住几千个伞点SELECT p.id, p.name, p.lng, p.lat, ROUND(6371 * 2 * ASIN(SQRT( POWER(SIN((#{lat} - p.lat) * PI() / 360), 2) COS(#{lat} * PI() / 180) * COS(p.lat * PI() / 180) * POWER(SIN((#{lng} - p.lng) * PI() / 360), 2)))) AS distance_km FROM t_umbrella_point p WHERE p.enabled 1 AND p.lat BETWEEN #{lat} - 0.5 AND #{lat} 0.5 AND p.lng BETWEEN #{lng} - 0.5 AND #{lng} 0.5 ORDER BY distance_km ASC LIMIT 20;这里的0.5大约对应 55 公里的范围先用矩形粗筛过滤掉明显远的伞点再对剩余记录计算球面距离。这个 SQL 在数据量超过一万条后扫描全表也还行如果伞点上了五万个建议加一个GeoHash前缀列先按前缀过滤再算精确距离。4. 订单与计费模块防并发扣款与押金退还的取舍4.1 计费规则按时长、阶梯价与免费期的取舍共享雨伞的计费规则不能写死在代码里否则改价一次要发一次版。我一般会设计一张t_charge_rule表存伞点能够使用的计费模式按分钟计费、按阶梯计费、封顶价格、免费时长。这套系统里默认的规则是免费 24 分钟超过后每 30 分钟 1 元24 小时内封顶 15 元。为什么设计免费 24 分钟而不是 30 分钟因为用户借伞后如果 30 分钟还刚好踩在免费门槛上平台赚不到钱24 分钟能保证绝大多数借用行为都会产生收入又让用户感觉真的有免费期。public BigDecimal calcAmount(LocalDateTime borrowTime, LocalDateTime returnTime, ChargeRule rule) { long minutes Duration.between(borrowTime, returnTime).toMinutes(); if (minutes rule.getFreeMinutes()) { return BigDecimal.ZERO; } long billableMinutes minutes - rule.getFreeMinutes(); long periods (billableMinutes rule.getPeriodMinutes() - 1) / rule.getPeriodMinutes(); BigDecimal raw rule.getPricePerPeriod().multiply(BigDecimal.valueOf(periods)); return raw.min(rule.getDailyCap()); }这个计算逻辑放在服务端前端只展示结果。有人会问为什么不在小程序算还能省一次请求。真实原因是前端计算可以被篡改而且游客用户可以改系统时间蹭免费时长服务端统一用服务器时间处理才靠谱。4.2 还伞扣款的并发控制乐观锁与分布式锁还伞这个动作是“先确认订单状态、再计算金额、再扣款、再退押金、再通知锁归位”一串操作。如果用户在确认还伞页面手抖双击或者 App 退到后台又重新回调同一个还伞请求可能在同一秒到达两次。不做并发控制的话第二次请求会拿到同一个订单并把已经改成“已完成”的单子再扣一遍钱。还伞的 Service 层用Transactional但事务只能保证原子性不能解决“两个请求读到了同一份数据”的问题。需要在订单状态更新上做乐观锁Transactional(rollbackFor Exception.class) public void returnUmbrella(String orderNo) { Order order orderMapper.selectByOrderNo(orderNo); if (order null || order.getStatus() ! 1) { throw new BizException(订单不存在或状态已变更); } int rows orderMapper.compareAndSetStatus( order.getId(), 1, 2, new Date()); if (rows 0) { throw new BizException(订单状态已变更请勿重复提交); } // 计算金额设置待确认归还状态 // 通知蓝牙锁上报状态异步等待锁状态确认 }compareAndSetStatus的 SQL 本质是UPDATE t_order SET status #{newStatus} WHERE id #{id} AND status #{oldStatus}。受影响行数是 0 就说明状态已经被别人改过直接抛异常。这个方案能挡住 99% 的重复提交场景。如果是跨服务的操作比如微信支付回调同时到达不仅要操作订单表还要操作押金流水表就需要再套一层 Redis 分布式锁锁的 key 用lock:order:${orderNo}设置 5 秒自动过期防止持锁进程挂掉导致死锁。分布式锁不能全项目乱用还伞这种场景下数据库乐观锁已经能挡住重复提交Redis 锁只在“同一个订单同时被业务主动还伞和支付回调修改”时才真正有必要。4.3 押金与退款流程设计押金的逻辑是“借伞时冻结还伞时结算”。小程序端用微信支付先付押金是真正扣款不是虚拟冻结。所以还伞结算时要把“消费金额从押金里扣掉剩余部分退回用户微信零钱”。这里有个容易踩坑的地方退款接口一旦发出它是个异步过程微信返回的结果只是“受理成功”不代表“退款到账”。所以订单状态要加一个“退款中”的中间状态服务端收到微信退款回调之后再把订单状态改成“已完成”。Component public class WxPayCallbackHandler { Transactional(rollbackFor Exception.class) public void handleRefundCallback(RefundNotifyDto dto) { int rows orderMapper.updateRefundStatus( dto.getOrderNo(), 5, 3, new Date()); if (rows 0) { log.warn(refund callback ignored for order {}, dto.getOrderNo()); } } }updateRefundStatus同样是一个带状态条件的更新SQL 层面保证同一条订单不会在“退款中”和“已完成”之间重复横跳。退款回调处理时直接把订单从“退款中”改成“已完成”消费金额与押金的差量在触发退款前算好不在回调里再算避免回调与主流程同时读数据。5. 避坑与常见问题从开发到上线的 5 个真实踩坑记录5.1 微信支付回调重复通知导致订单状态错乱现象同一笔支付订单微信回调连续到达两次订单金额被更新两次用户押金账户余额翻倍。原因微信支付回调本身有重试机制网络抖动时会在几秒内连发多条通知。回调处理没有做幂等每次回调都执行更新订单金额和增加余额的操作。解决在订单表增加唯一订单号并且回调处理里先查状态再更新用UPDATE ... WHERE status ?的方式第二次回调进来时受影响行数为 0直接忽略。5.2 蓝牙锁延迟上报导致已还伞仍显示“使用中”现象用户把伞插回桩位小程序端收到还伞成功提示但后台订单状态停在“使用中”用户继续被计费。原因蓝牙锁从触发还伞到上报服务端之间存在网络延迟极端情况小程序端没等到锁的上报确认就更新了本地 UI服务端却还没收到锁的状态消息。解决还伞时先更新订单到“待确认归还”状态停止前端计时显示服务端在收到蓝牙锁主动上报后才真正结算金额。同时在后台启动一个定时任务扫描超过 30 分钟仍处于“待确认归还”的订单触发主动向锁端查询状态锁端失联则走人工核实流程。5.3 经纬度漂移导致附近伞点定位偏差现象用户在小程序上看到 50 米外有伞点走过去实际距离快 150 米。原因比较典型的是坐标系不一致。小程序wx.getLocation返回 GCJ-02 坐标而地图组件或者后台存储时用了原生 GPS 的 WGS-84 坐标导致前后端数据在同一个地图上叠加出现偏移。还有一些第三方地图 SDK 默认输出 WGS-84不做转换就直接用也会出问题。解决整个链路统一使用 GCJ-02。小程序端拿到定位坐标后不做二次转换数据库表中经纬度字段也存 GCJ-02地图展示直接用同一个坐标系。测试的时候可以拿同一个坐标分别放到微信开发者工具和真机上看偏差排除是 API 行为差异还是坐标转换问题。5.4 数据库时间与本地时间差八小时导致计费异常现象用户下午借伞晚上还伞后台结算显示骑行时长多了 8 小时。原因服务器时区设置为 UTC数据库连接串没有加serverTimezoneAsia/ShanghaiJDBC 把 UTC 时间直接读成本地时间所有的时间计算都偏离 8 小时。解决时间统一存 UTC代码业务时间用LocalDateTime数据库连接参数显式指定时区。还伞结算时用服务器时间而不是数据库的CURRENT_TIMESTAMP这样即使数据库换实例也不会出现时间漂移。5.5 高并发下单重复创建同一把伞的订单现象多人同时扫码同一把空闲伞服务端收到流量后生成了多笔待取伞订单。原因伞具表只有一个状态字段查询时判断“空闲”之后没有加任何写锁两个请求同时读到空闲状态先后创建订单。解决取伞接口里先执行一个条件更新UPDATE t_umbrella SET status 1 WHERE id ? AND status 0受影响行数为 0 就说明被别人抢单了。这个更新本身带锁抢到更新的请求才允许创建订单抢不到的返回“该伞已被借出”。6. 从 Demo 到可交付压测、灰度与日志留痕的落地技巧6.1 用 JMeter 对还伞接口做并发压测还伞接口是最容易出并发问题的把它压透了基本就放心了。用 JMeter 建一个线程组200 个线程并发循环 5 次同时打还伞接口重点看两点compareAndSetStatus之后的“订单不存在或状态已变更”错误比例以及最终订单表里有没有出现状态跳变。压测脚本里预先生成一批订单然后每个线程随机拿一个orderNo请求还伞。结果里如果异常比例远超预期值就检查是不是乐观锁条件没命中或者事务里有没有嵌套调用其他接口导致锁时间过长。我一般会把压测对象的数据库单独拉一个测试库避免污染真实数据。6.2 按伞点分批上线新系统刚上线时不要一次把所有伞点都切过去。先把试点伞点的拿伞、还伞、结算跑顺再扩大到片区最后全量切流。每一步的验证标准包括订单异常率低于 0.1%、用户退款申诉率为 0、蓝牙锁失联次数归零。分批上线的同时要配一个灰度配置小程序端按后台下发的开关决定展示哪些伞点。这样一旦发现锁上报异常可以把流量切回旧逻辑不影响用户在没有覆盖到的伞点正常借还。6.3 日志规范与异步落库上线之后排障全靠日志。我在每个订单操作的关键节点都埋了一条结构化日志包含orderNo、userId、umbrellaId、action、timestamp并保证同一个请求链路使用同一个traceId。日志输出走异步 Appender不然高并发下单机磁盘 I/O 会成为瓶颈。Slf4j Service public class OrderTraceService { public void trace(String action, String orderNo, Long userId, String remark) { log.info(orderTrace|traceId{}|action{}|orderNo{}|userId{}|remark{}, TraceIdHolder.get(), action, orderNo, userId, remark); } }traceId在拦截器里生成跨 HTTP 调用时通过 Header 传递这样可以串起来“小程序端请求—服务端处理—微信回调”的完整链路。用logback的异步 Appender 保证打日志不会阻塞主流程同时单独给这种带action字段的日志配置一个独立文件方便检索时直接过滤。这套系统我前后完整跑过一遍最大的收获是“状态机里多设计一个中间态”能省掉大量线下沟通成本。从那以后我每次设计借还类业务都强制把用户操作和服务端最终确认之间隔一层中间状态给网络延迟和异常留出缓冲余地也希望这些拆解能帮到正打算上手共享雨伞系统的你。本文还有配套的精品资源点击获取