电动车租赁会员管理系统源码实战:从库表设计到订单状态机
简介这份电动车租赁会员管理系统资源包面向计算机相关专业的高校学生、教师及行业开发者尤其适合需要完成毕业设计、课程设计或项目立项演示的人群。项目基于C#开发代码完整且经过测试可正常运行涵盖会员管理、车辆租赁等核心业务逻辑既能直接借鉴学习也支持在此基础上二次开发扩展功能。压缩包共381个文件约52.61MB以176个cs源码文件为主体辅以resources与resx资源文件、dll动态库、exe可执行程序、png与jpg界面素材以及sql数据库脚本、docx项目说明与用户手册、sln解决方案等结构清晰便于按模块查阅。目前已有43人学习关注。随包附带的数据库设计、项目说明和用户手册能帮助读者快速理解系统架构与数据表关系掌握从建库到部署运行的完整流程遇到配置问题还可获得远程教学支持适合作为学习进阶与实战参考的完整案例。1. 电动车租赁会员管理系统从一张会员卡到一套能跑通的业务闭环很多做毕设或接私活的开发者第一次拿到“电动车租赁会员管理系统”这个题目时脑子里蹦出来的往往是“不就是个 CRUD 吗”。真动手才发现会员卡类型、押金规则、计费策略、车辆状态流转、订单超时释放这几件事搅在一起比想象中难缠得多。我见过一个模拟项目会员按次计费和按天计费混用结果一辆车被两个订单同时占用最后靠人工改数据库才收场。这套系统要解决的核心就是把“人—卡—车—单—钱”五者之间的状态约束用数据库和业务代码锁死。它适合正在做课程设计的学生、想练手全栈的初级开发者以及需要一套可复用租赁业务骨架的独立开发者。下面我按实际落地顺序把源码结构、库表设计、关键接口和踩过的坑一次讲清。2. 先想清楚业务模型会员、车辆、订单三张核心表怎么定2.1 为什么不能一上来就写 Controller新手最容易犯的错是打开 IDE 先建 Controller边写边想字段。租赁业务的状态耦合度很高会员有余额和等级车辆有可用/租出/维修三种状态订单有待支付/进行中/已完成/已取消四种状态。任何一个状态变更都要联动另外两张表。如果表结构没定死写到一半发现“会员押金”没地方放回头改表会连带改十几处代码。我的习惯是先在纸上画状态流转图再落成 SQL。常见做法是会员表管身份和钱车辆表管资产和位置订单表管一次交易的完整生命周期三张表通过外键和状态字段互相约束。2.2 会员表与车辆表的关键字段取舍会员表不要只存用户名密码。租赁场景下balance余额、deposit押金、member_level等级、status正常/冻结这四个字段决定了后续所有计费和权限逻辑。押金单独存而不是混在余额里是因为退押金和消费退款是两条审计线。车辆表里plate_no车牌/编号要唯一索引status用枚举而不是布尔值因为“维修中”和“已租出”是两种不可用但原因不同的状态。下面是我一般会用的建表片段-- 会员表钱和身份分开管理 CREATE TABLE member ( id BIGINT PRIMARY KEY AUTO_INCREMENT, phone VARCHAR(20) NOT NULL UNIQUE COMMENT 手机号登录用, password VARCHAR(128) NOT NULL COMMENT 加盐哈希后的密码, balance DECIMAL(10,2) DEFAULT 0.00 COMMENT 可消费余额, deposit DECIMAL(10,2) DEFAULT 0.00 COMMENT 押金独立字段便于退款审计, member_level TINYINT DEFAULT 1 COMMENT 1普通 2银卡 3金卡, status TINYINT DEFAULT 1 COMMENT 1正常 0冻结, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 车辆表状态用枚举不用布尔 CREATE TABLE vehicle ( id BIGINT PRIMARY KEY AUTO_INCREMENT, plate_no VARCHAR(32) NOT NULL UNIQUE COMMENT 车辆编号, model VARCHAR(64) COMMENT 车型, battery INT DEFAULT 100 COMMENT 电量百分比, status TINYINT DEFAULT 1 COMMENT 1可用 2租出 3维修, price_per_hour DECIMAL(8,2) NOT NULL COMMENT 小时单价, current_station VARCHAR(64) COMMENT 当前站点 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明member表把balance和deposit拆开是为了在退款时只动押金不动余额避免对账混乱。vehicle.status用 1/2/3 而不是 0/1是因为维修中的车不能被下单接口查到但运营后台要能看到。参数上金额统一用DECIMAL(10,2)不要用FLOAT否则计费累加会出现 0.30000000000000004 这种玄学尾差。手机号加唯一索引防止同一手机号注册出两个会员导致押金对不上。2.3 订单表一次租赁的完整生命周期载体订单表是整个系统的中枢。它要记录谁租了哪辆车、什么时候开始、什么时候结束、按什么规则计费、最终扣了多少钱。关键字段包括member_id、vehicle_id、start_time、end_time、status、total_fee。其中status的流转必须由业务代码严格控制不能允许从“已完成”跳回“进行中”。我一般还会加一个pay_status字段区分“订单状态”和“支付状态”因为存在订单已结束但支付失败需要补扣的情况。CREATE TABLE rent_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 业务订单号, member_id BIGINT NOT NULL, vehicle_id BIGINT NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, status TINYINT DEFAULT 1 COMMENT 1待支付 2进行中 3已完成 4已取消, pay_status TINYINT DEFAULT 0 COMMENT 0未支付 1已支付 2已退款, total_fee DECIMAL(10,2) DEFAULT 0.00, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_member (member_id), INDEX idx_vehicle (vehicle_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明order_no单独生成而不是用自增 id是为了对外暴露时不泄露业务量。start_time在创建订单时就写入end_time在还车时更新两者之差乘以单价就是费用。索引加在member_id和vehicle_id上因为“查我的订单”和“查这辆车的历史订单”是最频繁的两个查询。注意status和pay_status必须分开否则“已取消但已支付”这种状态无处安放退款逻辑会写得很别扭。3. 把源码跑起来环境、配置与最小启动路径3.1 技术栈选型与目录结构这套系统的常见实现是 Spring Boot MyBatis MySQL前端用 Vue 或 Thymeleaf 都行。选 Spring Boot 不是因为时髦而是它的事务管理和定时任务能直接解决租赁业务里两个硬需求下单时扣余额和改车辆状态必须在一个事务里超时未支付的订单要自动取消并释放车辆。目录结构我一般这样分controller只做参数校验和路由service写业务状态流转mapper管数据库entity对应表config放拦截器和定时任务。不要把计费逻辑写在 Controller 里否则单元测试没法写。3.2 数据库连接与关键配置项配置文件里最容易翻车的是时区和连接池。租赁系统对时间敏感serverTimezone必须和数据库服务器一致否则start_time和end_time会差 8 小时计费直接算错。连接池我一般用 HikariCPmaximum-pool-size设 10 到 20 就够设太大反而拖慢数据库。下面是一个能直接抄的application.yml片段spring: datasource: url: jdbc:mysql://localhost:3306/ebike_rental?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 15 minimum-idle: 5 connection-timeout: 30000 jackson: time-zone: Asia/Shanghai date-format: yyyy-MM-dd HH:mm:ss逻辑说明serverTimezoneAsia/Shanghai和jackson.time-zone两处必须同时设只设一处会导致接口返回的时间和数据库存的时间对不上。maximum-pool-size设 15 是经验值学生项目并发不高设 100 反而会因为线程切换拖慢响应。connection-timeout设 30 秒避免数据库卡死时请求一直挂着。3.3 启动顺序与初始化数据启动前先执行建表 SQL再插入几条测试数据一个会员、三辆车分别处于可用、租出、维修状态、一条进行中的订单。这样启动后直接能测“还车”和“下单”两个核心流程。启动命令用 Maven 或 Gradle 都行关键是看日志里有没有Started Application in X seconds。如果卡在HikariPool初始化八成是数据库没启动或密码错了。初始化数据我一般写在一个data.sql里随项目一起执行避免每次手动插。INSERT INTO member (phone, password, balance, deposit, member_level) VALUES (13800000000, hashed_pwd_here, 100.00, 200.00, 1); INSERT INTO vehicle (plate_no, model, battery, status, price_per_hour, current_station) VALUES (EB-001, 标准款, 95, 1, 5.00, A站), (EB-002, 长续航, 80, 2, 8.00, A站), (EB-003, 标准款, 60, 3, 5.00, B站);逻辑说明测试数据要覆盖三种车辆状态才能验证“下单接口只返回 status1 的车”。会员余额和押金分开给值方便测试扣款时只动余额不动押金。password字段存的是哈希值实际插入时要用代码里的加密工具生成不要直接写明文。4. 核心接口实现下单、还车与计费的代码级拆解4.1 下单接口事务里必须同时做三件事下单不是简单插一条订单记录。它要检查会员状态和余额、检查车辆是否可用、把车辆状态改成租出、插入订单。这四步必须在一个事务里否则会出现“订单插入了但车辆还是可用”的脏数据。我一般用Transactional注解并在方法里按顺序执行。下面是一个简化后的 Service 方法Transactional(rollbackFor Exception.class) public OrderVO createOrder(Long memberId, Long vehicleId) { // 1. 查会员校验状态和余额 Member member memberMapper.selectById(memberId); if (member null || member.getStatus() ! 1) { throw new BizException(会员状态异常); } if (member.getBalance().compareTo(new BigDecimal(10)) 0) { throw new BizException(余额不足最低需10元); } // 2. 查车辆必须处于可用状态 Vehicle vehicle vehicleMapper.selectById(vehicleId); if (vehicle null || vehicle.getStatus() ! 1) { throw new BizException(车辆不可租); } // 3. 乐观锁更新车辆状态防止并发抢车 int updated vehicleMapper.updateStatus(vehicleId, 1, 2); if (updated 0) { throw new BizException(车辆已被他人租走); } // 4. 插入订单 RentOrder order new RentOrder(); order.setOrderNo(generateOrderNo()); order.setMemberId(memberId); order.setVehicleId(vehicleId); order.setStartTime(new Date()); order.setStatus(1); orderMapper.insert(order); return convertToVO(order); }逻辑说明第 3 步的updateStatus(vehicleId, 1, 2)是带条件的更新SQL 类似UPDATE vehicle SET status2 WHERE id? AND status1。返回影响行数为 0 说明车已经被别人抢了直接抛异常回滚。这就是乐观锁比SELECT ... FOR UPDATE轻量适合并发不高的学生项目。参数上最低余额校验我设了 10 元实际可以根据单价调整。rollbackFor Exception.class确保任何异常都回滚不会留下半截数据。4.2 还车与计费时间差怎么算才不扯皮还车接口要做三件事更新订单的end_time和total_fee、把车辆状态改回可用、从会员余额扣钱。计费公式是(end_time - start_time) 的小时数 × 单价不足一小时按一小时算。这里有个坑如果直接用小时间隔的浮点数乘单价会出现 1.9999999 小时被算成 1 小时的情况。我的做法是先算分钟差再向上取整到小时。Transactional(rollbackFor Exception.class) public BigDecimal returnVehicle(Long orderId) { RentOrder order orderMapper.selectById(orderId); if (order null || order.getStatus() ! 2) { throw new BizException(订单状态不允许还车); } Vehicle vehicle vehicleMapper.selectById(order.getVehicleId()); Date endTime new Date(); // 分钟差转小时向上取整 long minutes (endTime.getTime() - order.getStartTime().getTime()) / (1000 * 60); long hours (long) Math.ceil(minutes / 60.0); if (hours 0) hours 1; // 最低计1小时 BigDecimal fee vehicle.getPricePerHour().multiply(new BigDecimal(hours)); // 扣余额 Member member memberMapper.selectById(order.getMemberId()); if (member.getBalance().compareTo(fee) 0) { throw new BizException(余额不足请先充值); } memberMapper.deductBalance(member.getId(), fee); // 更新订单 order.setEndTime(endTime); order.setTotalFee(fee); order.setStatus(3); order.setPayStatus(1); orderMapper.updateById(order); // 释放车辆 vehicleMapper.updateStatus(vehicle.getId(), 2, 1); return fee; }逻辑说明Math.ceil(minutes / 60.0)保证 61 分钟算 2 小时避免用户卡 59 分钟还车导致平台亏钱。最低计 1 小时是行业常见做法防止短租刷单。扣余额用deductBalance而不是先查再更新是为了在 SQL 层用UPDATE member SET balance balance - ? WHERE id ? AND balance ?保证原子性。参数上hours用long不用int虽然实际不会超过 int 范围但习惯上时间相关都用 long。4.3 超时未支付订单的自动取消用户下单后如果一直不支付车辆会被一直占用。常见做法是用 Spring 的Scheduled定时任务每分钟扫一次status1且创建超过 15 分钟的订单把它们改成已取消并释放车辆。这个逻辑必须和下单接口用同一套状态判断否则会出现“定时任务取消了订单但用户同时支付了”的并发问题。我一般会在取消时也加乐观锁条件。Scheduled(fixedRate 60000) Transactional(rollbackFor Exception.class) public void cancelTimeoutOrders() { Date deadline new Date(System.currentTimeMillis() - 15 * 60 * 1000); ListRentOrder timeoutOrders orderMapper.selectTimeoutOrders(deadline); for (RentOrder order : timeoutOrders) { // 带状态条件更新防止并发支付 int updated orderMapper.cancelIfPending(order.getId()); if (updated 0) { vehicleMapper.updateStatus(order.getVehicleId(), 2, 1); } } }逻辑说明cancelIfPending的 SQL 是UPDATE rent_order SET status4 WHERE id? AND status1只有影响行数大于 0 才释放车辆。这样即使支付接口同时把状态改成 2定时任务也不会误释放。fixedRate 60000表示每分钟执行一次学生项目够用。参数上15 分钟的超时时间可以根据业务调整但不要设太短否则用户刚下单就被取消体验很差。5. 避坑与排查那些让系统“看起来能跑”的隐藏问题5.1 现象下单成功但车辆列表里还能看到这辆车原因车辆状态更新和订单插入不在同一个事务里或者更新时没加状态条件。更隐蔽的情况是前端缓存了车辆列表下单后没刷新。解决先确认 Service 方法上有Transactional再检查更新 SQL 是否带了AND status1。前端方面下单成功后强制刷新列表或局部更新该车辆的状态。我一般会在下单接口返回里带上最新的车辆状态前端直接用返回值更新不重新查列表。5.2 现象还车时扣了钱但订单状态没变原因扣余额和更新订单在两个不同的数据库连接里执行扣款成功了但更新订单时抛异常回滚了扣款不对如果同一个事务扣款也会回滚。真正的原因是扣款用了独立的Transactional方法或者 MyBatis 的二级缓存导致更新没生效。解决把扣款和更新订单放在同一个 Service 方法里确保Transactional生效。检查是否在同类内部调用了带事务的方法Spring 的代理在这种情况下会失效。参数上可以在配置文件里打开 MyBatis 的 SQL 日志看更新语句到底有没有执行。5.3 现象计费金额出现 0.01 元的尾差原因用FLOAT或DOUBLE存金额累加时精度丢失。解决所有金额字段用DECIMAL(10,2)Java 里用BigDecimal并且BigDecimal的除法必须指定精度和舍入模式。我一般会在计费最后一步用setScale(2, RoundingMode.HALF_UP)统一保留两位。注意BigDecimal的equals和compareTo行为不同比较金额大小用compareTo。5.4 现象定时任务把正在支付的订单取消了原因取消订单时没有检查支付状态或者检查了但支付接口更新状态在取消之后。解决取消时用UPDATE ... WHERE status1的乐观锁支付接口也用UPDATE ... WHERE status1谁先执行谁生效。另一个办法是给订单加一个version字段做乐观锁。参数上定时任务的扫描间隔不要小于支付接口的平均响应时间否则并发窗口太大。5.5 现象会员余额扣成负数原因扣款 SQL 没有加AND balance ?条件或者加了但事务隔离级别是读未提交。解决扣款 SQL 必须写成UPDATE member SET balance balance - ? WHERE id ? AND balance ?并在 Service 里检查影响行数。如果返回 0说明余额不足抛异常回滚。隔离级别用默认的REPEATABLE READ就够不要为了“性能”改成读未提交。6. 进阶技巧用状态机把订单流转管死以及一套可复用的验证方法订单状态流转是这套系统里最容易写乱的地方。我见过一个模拟项目订单状态在 Controller、Service、Mapper 里各改各的最后出现“已取消的订单还能还车”。后来我强制把所有状态变更收口到一个OrderStateMachine类里每个状态只允许特定的操作触发其他一律抛异常。这个类不需要多复杂一个Map加几个判断就够。public class OrderStateMachine { // 定义允许的流转key是当前状态value是允许的下一个状态集合 private static final MapInteger, SetInteger ALLOWED new HashMap(); static { ALLOWED.put(1, Set.of(2, 4)); // 待支付 - 进行中 或 已取消 ALLOWED.put(2, Set.of(3)); // 进行中 - 已完成 ALLOWED.put(3, Set.of()); // 已完成 - 无 ALLOWED.put(4, Set.of()); // 已取消 - 无 } public static void check(int from, int to) { SetInteger allowed ALLOWED.getOrDefault(from, Set.of()); if (!allowed.contains(to)) { throw new BizException(非法状态流转: from - to); } } }逻辑说明ALLOWED里只写了合法的流转路径任何不在里面的组合都会抛异常。这样即使以后加了新状态也只需要改这个 Map不用满项目找setStatus。参数上状态值用 1/2/3/4 而不是枚举是为了和数据库的TINYINT直接对应减少转换代码。调用时在每次setStatus之前先OrderStateMachine.check(order.getStatus(), newStatus)。验证这套系统是否真的跑通我一般用三个动作第一用两个浏览器同时下单同一辆车看是否只有一个成功第二下单后等 16 分钟不支付看订单是否自动取消且车辆释放第三还车后查会员余额和订单金额是否一致车辆状态是否回到可用。这三个动作能覆盖并发、定时任务和计费三个最容易出问题的点。如果都过了这套系统拿去答辩或交付基本没问题。我自己踩过最深的坑是早期没把押金和余额分开结果用户退押金时把消费的钱也退了对账对了整整一个下午。从那以后凡是涉及钱的字段我一定拆开存、分开审计。希望帮到你。本文还有配套的精品资源点击获取