这两年帮不少学弟学妹参谋毕业设计,发现“基于SpringBoot的XX管理系统”几乎成了默认选项,而露营装备租赁这个方向尤其多。你可能看过类似标题:计算机毕业设计springboot露营装备租赁系统、基于SpringBoot的户外露营装备共享租赁平台、基于SpringBoot的野营器材在线租借管理系统——说到底,大家都是同一套业务内核,换了个包装。
这个系统解什么问题?一句话:把原来靠Excel记台账、靠微信聊租借的线下露营装备生意,搬到一个有装备展示、在线下单、库存管理、押金结算、订单跟踪的Web平台上。它适合谁做?一是确实拿它当毕设题目、需要快速搭建并讲清楚技术亮点的本科生;二是想自己练手、完整走一遍从需求到部署的Java后端开发者。无论哪种身份,这类系统的技术骨架都值得拆开看一遍:SpringBoot负责业务接口,MyBatis-Plus负责数据库操作,Vue负责前端交互,MySQL存业务数据,Redis做缓存和会话,再挂一个对象存储放装备图片。
直接说结论:如果你能把下面这套设计讲明白、写出来,答辩老师基本挑不出大毛病,因为你在做的不是“增删改查堆功能”,而是一套有状态、有库存约束、有资金流程的真实业务系统。
1. 系统定位与核心需求拆解
1.1 用户到底想要什么
先别急着写代码。做毕设最大的坑,是一上来就建表、写接口,结果做到一半发现业务逻辑自相矛盾。露营装备租赁不是普通的商品买卖,它有几个特殊点,必须在设计阶段想清楚:
第一,装备有“租期”概念。用户选的不只是“买一个帐篷”,而是“租某件帐篷3天”。租金怎么算?按天计费还是按时间段?超时怎么补费?这些直接影响订单表怎么设计。
第二,装备库存是“可占用的”。同一顶帐篷在某个时间段被租走,那这个时间段内它就不能再被别人租。但普通的库存数量字段没法表达“时间维度上的占用”,所以必须引入“库存扣减 + 订单状态 + 时间区间校验”这套组合逻辑。
第三,涉及押金。户外装备单价高,帐篷、天幕、炉具、睡袋动辄几百上千,平台一般会收押金,用户归还后原路退回。押金字段、退款状态、异常扣款,都是需求里必须明确的点。
第四,角色天然分三类:普通用户(游客/注册会员)、管理员(后台维护)、运营人员(处理订单和归还)。不同角色看到的界面和能调用的接口完全不同。
我用一个表格把核心业务需求整理出来,你在开题报告里也可以直接参考:
| 角色 | 核心诉求 | 对应功能模块 |
|---|---|---|
| 游客 | 浏览装备、了解租金 | 装备列表、装备详情、价格日历 |
| 注册用户 | 下单租赁、在线支付押金 | 装备下单、订单查询、押金支付、归还申请 |
| 管理员 | 维护装备信息、监控库存 | 装备CRUD、分类管理、库存管理、数据统计 |
| 运营人员 | 处理归还、核验装备 | 归还审核、订单状态变更、异常登记 |
1.2 功能模块怎么划分才不显得“水”
很多毕设喜欢把模块切得很碎,比如“用户管理模块”“订单管理模块”“评价管理模块”,看着功能多,实际彼此之间没有数据关联。比较好的划分方式是按“业务流程”切:
- 基础数据模块:用户、装备分类、装备规格、装备图片。
- 租赁交易模块:下单、订单详情、租期计算、订单状态流转。
- 库存与调度模块:库存占用、释放、可用数量查询。
- 押金与结算模块:押金收取、冻结、退还。
- 营销辅助模块:公告、 Banner、装备评价(有剩余精力就做,没有就砍掉)。
我当时带人做这个项目时,遵循一个原则:先做主干,再做枝叶。主干就是“用户能租到装备,管理员能管住库存”,枝叶才是评价、收藏、优惠券、积分。很多人死在枝叶上,主干反而粗糙。
1.3 这个系统的技术亮点在哪里
同样是SpringBoot毕设,为什么有人答辩能拿优秀,有人被评价“像大作业”?差别在于你有没有“有说服力的技术思考”。
露营装备租赁系统的技术亮点,不在于用了多新的框架,而在于你处理了几个真实难点:
- 租期重叠校验。同一装备在同一时间区间内不能重复被租,这需要数据库层的并发控制。
- 库存扣减的原子性。用户提交订单时,库存扣减和订单创建必须在一个事务里完成,不能出现“订单建了库存没扣”或“库存扣了订单没建”的情况。
- 订单状态机设计。从“待支付”到“已支付/待发货”到“租赁中”到“待归还”到“已完成”或“已取消”,每一步都有前置条件和后置动作。
- 押金退款的安全性。用户归还装备后,押金不能人工随便退,要走状态流,确保只有“归还审核通过”才能触发退款。
这些不是花架子,是你写在论文“技术难点”章节里的实打实内容。后面我会逐一把实现方式讲透。
2. 技术选型与开发环境准备
2.1 为什么是SpringBoot,而不是SSH或SSM
很多同学纠结技术栈。如果用SSH(Struts2 + Spring + Hibernate),年代感太强,答辩老师一看就知道是过时项目。如果用纯SSM,也不是不行,但配置麻烦,而且面试时“SpringBoot自动装配”这个问题你都接不住。
SpringBoot的本质是“约定大于配置”,内嵌Tomcat,自动配置数据源、MyBatis、Redis,让你把精力放在业务实现上。对毕设来说,它最大的价值是在论文里能讲出一套完整的“自动装配原理:@SpringBootApplication → @EnableAutoConfiguration → spring.factories / AutoConfiguration.imports → 条件注解”,这一串讲下来,答辩环节基本就稳了。
技术栈推荐如下(这也是目前毕设最主流的组合):
- JDK 1.8 或 11。别一上来就用 JDK 17/21,有些老依赖会出兼容问题。
- SpringBoot 2.7.x。比3.x稳定,javax.servlet 和 javax.validation 的坑少,网上资料也最多。
- MyBatis-Plus 3.5.x。单表CRUD不用写SQL,复杂查询用LambdaQueryWrapper,比JPA更直观。
- MySQL 5.7 或 8.0。二选一都行,注意驱动版本差异。
- Redis 6.x。做登录Token缓存、装备热点数据缓存。
- Vue 2 或 Vue 3 + Element UI / Element Plus。前端不必玩花活,能配合联调就行。
- MinIO 或本地文件存储。存装备图,能讲清楚对象存储的概念。
2.2 项目结构规划
不要把所有类堆在几个包里。合理的包结构,会让你的代码量看起来多一倍且更专业。我常用的结构:
com.camping.rent ├── common // 通用返回结果、异常处理、工具类 ├── config // RedisConfig、MybatisPlusConfig、WebConfig、MinioConfig ├── controller // 接口层,只做参数接收和返回 ├── service // 业务层,核心逻辑写在这里 │ └── impl ├── mapper // MyBatis-Plus 的 Mapper 接口 ├── entity // 数据库实体 ├── dto // 前端传参对象 ├── vo // 返回给前端的视图对象 └── utils // JWT工具、日期工具、租金计算工具强调一下:把 DTO 和 VO 分开,是毕设从“会写CRUD”到“像工程代码”的关键一步。不要直接把 Entity 丢给前端,否则表字段一改,前端就崩,而且会有把敏感字段(如密码)暴露的风险。
2.3 动手前的环境清单
这一步没什么捷径,照着装就行。但有几个容易踩的坑我提前说明:
- 数据库编码必须设置为 utf8mb4,不然存表情符号或特殊字符会报错。
- application.yml 里配置时区:serverTimezone=Asia/Shanghai,否则日期字段会差8小时。
- Lombok 插件必须安装到 IDEA,否则 @Data 注解编译报错。
- 如果你的SpringBoot版本是2.7.x,MyBatis-Plus 用 3.5.3 以上版本,规避分页插件和 baomidou 版本的兼容问题。
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/camping_rent?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 database: 0 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl3. 数据库设计与核心表结构
3.1 表设计的总原则
租赁系统的核心不是订单表本身,而是“库存账 + 资金账 + 状态账”三套数据怎么串起来。我先说几个设计原则,你可以直接写进论文里:
第一,订单表必须有一个全局唯一的“订单编号”,不要用自增主键给用户看。原因很简单:自增ID会暴露平台单量,而且容易被恶意猜测。我用雪花算法生成订单号,格式类似 1712345678901234567,但给用户展示时可以加前缀,比如 ZL + 时间戳 + 随机数。
第二,金额字段用 decimal(10,2),绝对不要用 float/double。租金、押金都是钱,精度不能丢。比如rental_price decimal(10,2)。
第三,所有“状态”字段建议用 tinyint 数字表示,而不是直接存中文。为什么?存数字可以让后端做枚举映射,前端根据数字渲染标签,数据库层面也能做索引。比如订单状态 0待支付 1已支付 2租赁中 3待归还 4已完成 5已取消 6退款中。
第四,每个表都要有 create_time、update_time、deleted 字段。尤其 deleted 这个逻辑删除字段,MyBatis-Plus 有 @TableLogic 注解直接支持,这样你删除装备时数据不会真丢,在论文里还能写一句“采用逻辑删除保证数据可追溯”。
3.2 核心表清单
我把核心表列出来,并标注关键字段。为了避免篇幅过长,我这里只展开最重要的四张表。
用户表user:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| username | varchar(50) | 用户名,唯一 |
| password | varchar(100) | BCrypt加密存储 |
| phone | varchar(20) | 手机号 |
| real_name | varchar(50) | 真实姓名 |
| id_card | varchar(18) | 身份证号(租装备常需要实名) |
| role | tinyint | 0普通用户 1管理员 2运营 |
| status | tinyint | 0禁用 1正常 |
| deleted | tinyint | 逻辑删除 |
| create_time | datetime | 注册时间 |
装备表equipment:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| category_id | bigint | 分类ID |
| name | varchar(100) | 装备名称 |
| description | text | 装备描述 |
| cover_image | varchar(255) | 封面图URL |
| images | text | 轮播图JSON数组 |
| total_stock | int | 总库存 |
| day_price | decimal(10,2) | 日租金 |
| deposit | decimal(10,2) | 押金金额 |
| status | tinyint | 0下架 1上架 |
| damage_level | varchar(20) | 成色/损耗程度 |
订单表rental_order:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| order_no | varchar(32) | 订单号 |
| user_id | bigint | 用户ID |
| equipment_id | bigint | 装备ID |
| rent_start_date | date | 租借开始日期 |
| rent_end_date | date | 租借结束日期 |
| rent_days | int | 租期天数 |
| total_amount | decimal(10,2) | 租金总额 |
| deposit_amount | decimal(10,2) | 押金 |
| status | tinyint | 状态 |
| pay_time | datetime | 支付时间 |
| return_time | datetime | 归还时间 |
| cancel_reason | varchar(255) | 取消原因 |
库存表equipment_stock或直接用“库存占用记录表”stock_occupied:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| equipment_id | bigint | 装备ID |
| order_id | bigint | 订单ID |
| start_date | date | 开始占用日期 |
| end_date | date | 结束占用日期(含) |
| quantity | int | 占用数量 |
| status | tinyint | 0占用中 1已释放 |
你发现没有,我特意引入了stock_occupied而不是在equipment表里存一个“剩余库存”。原因我在开头提过:库存是随时间变化的,如果只存剩余数量,同一时间段内的多笔订单就会互相覆盖。正确做法是“总库存 - 指定时间段内未释放的占用数量 = 可用库存”。
3.3 SQL建表语句参考
给你一段全核心的建表SQL,注意我加了索引和默认值,这是很多新手容易漏掉的细节。
CREATE TABLE `rental_order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `order_no` varchar(32) NOT NULL COMMENT '订单号', `user_id` bigint(20) NOT NULL COMMENT '用户ID', `equipment_id` bigint(20) NOT NULL COMMENT '装备ID', `rent_start_date` date DEFAULT NULL COMMENT '租借开始日期', `rent_end_date` date DEFAULT NULL COMMENT '租借结束日期', `rent_days` int(11) DEFAULT NULL COMMENT '租期天数', `total_amount` decimal(10,2) DEFAULT NULL COMMENT '租金总额', `deposit_amount` decimal(10,2) DEFAULT NULL COMMENT '押金', `status` tinyint(4) DEFAULT '0' COMMENT '状态 0待支付 1已支付 2租赁中 3待归还 4已完成 5已取消 6退款中', `pay_time` datetime DEFAULT NULL COMMENT '支付时间', `return_time` datetime DEFAULT NULL COMMENT '归还时间', `cancel_reason` varchar(255) DEFAULT NULL COMMENT '取消原因', `deleted` tinyint(1) DEFAULT '0' COMMENT '逻辑删除', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_equipment_id` (`equipment_id`), KEY `idx_status` (`status`), KEY `idx_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租赁订单表';4. 后端核心模块实现与关键逻辑
4.1 登录与权限控制:从JWT到拦截器
登录模块是毕设答辩必问的环节。最简单的做法是 SpringBoot 整合 JWT:用户登录成功,后端生成一个 Token 返回给前端,前端每次请求带上 Authorization 头,后端通过拦截器校验 Token 有效性和用户角色权限。
技术选型上,JWT 对比传统 Session 方案的优势在于:无状态、适合前后端分离、天然支持多端。毕设里用这个方案不丢人。
关键代码大致长这样,我先给一个简要的 JWT 工具类:
@Component public class JwtUtils { @Value("${jwt.secret}") private String secret; @Value("${jwt.expire}") private long expire; // 毫秒 public String generateToken(Long userId, String username, Integer role) { Date now = new Date(); Date expireDate = new Date(now.getTime() + expire); return Jwts.builder() .setHeaderParam("typ", "JWT") .claim("userId", userId) .claim("username", username) .claim("role", role) .setIssuedAt(now) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } }有了工具类,再写一个拦截器。我这里建议你用 HandlerInterceptor + WebMvcConfigurer 注册,而不是用 OncePerRequestFilter,因为拦截器能拿到 HandlerMethod,方便做“判断这个方法上有没有 @RequireRole 注解”这种细粒度控制。
拦截器逻辑不复杂:所有接口都先检查 Token 是否存在,存在就解析出用户信息放入 ThreadLocal;接着看Controller方法上有没有标注 @RequireRole,有的话再比对当前用户的 role 是否匹配。
这里有一个很多教程不会提的细节:JWT 无状态意味着服务端无法主动让Token失效。用户被禁用后,他手上的Token依然有效。解决方案很简单,把Token存一份到Redis,过期时间和JWT保持一致,每次请求拦截器里先查Redis是否存在,不存在就直接拒绝。这就是“可控的无状态认证”,也是你在答辩时可以讲的优化点。
4.2 装备展示与租期校验接口
装备列表接口比较简单,MyBatis-Plus 分页查询就行:
public PageResult<EquipmentVO> pageEquipment(int page, int size, Long categoryId, String keyword) { Page<Equipment> p = new Page<>(page, size); LambdaQueryWrapper<Equipment> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(categoryId != null, Equipment::getCategoryId, categoryId) .like(StringUtils.hasText(keyword), Equipment::getName, keyword) .eq(Equipment::getStatus, 1) .orderByDesc(Equipment::getCreateTime); equipmentMapper.selectPage(p, wrapper); // 转VO,夹带“可租状态”和“可租数量” }麻烦的查询是“某件装备在某段时间内可租数量”。这类接口的高频场景是:用户点进详情页,选开始日期和结束日期,页面实时显示“该时段可租x件”。
实现方式也不难,基于stock_occupied表做聚合:
SELECT COALESCE(SUM(quantity), 0) AS occupied FROM stock_occupied WHERE equipment_id = #{equipmentId} AND deleted = 0 AND status = 0 AND start_date <= #{endDate} AND end_date >= #{startDate}然后可用库存 = 总库存 - 占用数量。这里要注意日期重叠的边界条件:只要start_date <= 结束日期且end_date >= 开始日期,就说明有重叠,需要计算进去。
我当时第一次写这个条件时想当然用了“只判断区间在范围内”,结果漏掉了跨区间订单,最后靠画时间轴才想明白。这个SQL条件建议你背下来,答辩被问到“同一装备在任意时间段内可用数量怎么算”时,直接一条SQL演示就很有说服力。
4.3 下单与库存扣减:一定要上事务和锁
下单是整个系统最核心、最容易出问题的接口,也是论文里能写“并发控制与数据一致性”的重要素材。我先把理想流程写出来:
- 校验用户登录状态、装备上架状态。
- 校验租期天数大于0。
- 校验指定时间段可用库存 ≥ 1。
- 锁定库存:向
stock_occupied插入占用记录,数量为1。 - 创建订单,状态为待支付。
- 计算租金总额 = 日租金 × 租期天数。
- 返回订单号和待支付金额。
关键在于第3步和第4步之间有并发窗口:两个用户同时下单,都查到了可用库存为1,然后同时插入占用记录,就会超卖。解决办法是给“校验+插入”操作加锁。
最简单的工程实现是 MySQL 悲观锁:查询时带FOR UPDATE。
EquipmentStock stock = stockMapper.selectForUpdate(equipmentId);需要注意的是,FOR UPDATE必须用在事务中,而且只对 InnoDB 表、且查询命中了索引才有效。更推荐的做法是直接为装备维度加分布式锁。用 Redis 实现一个简易锁:
String lockKey = "stock:lock:" + equipmentId; String lockValue = UUID.randomUUID().toString(); Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, Duration.ofSeconds(5)); if (!locked) { throw new BizException("当前下单人数过多,请稍后重试"); } try { // 校验库存、扣减、创建订单 } finally { if (lockValue.equals(redisTemplate.opsForValue().get(lockKey))) { redisTemplate.delete(lockKey); } }这里有一个很关键的经验:锁必须有超时时间,而且释放锁时必须校验 value 是否是自己设置的。不然如果业务执行超过锁超时时间,锁自动释放,另一个线程拿到锁,而前一个线程结束时又把锁删了,会造成锁误删。
Redis 锁不是万能的,但在毕设场景中已经足够。你甚至可以在论文里加一个小标题“基于Redis实现分布式锁防止库存超卖”,这个题目答辩老师很爱听。
4.4 状态机的设计与实现
订单状态流转我强烈建议单独写一个枚举类,不要散落一堆 if/else。
public enum OrderStatus { PENDING_PAY(0, "待支付"), PAID(1, "已支付"), RENTING(2, "租赁中"), PENDING_RETURN(3, "待归还"), COMPLETED(4, "已完成"), CANCELED(5, "已取消"), REFUNDING(6, "退款中"); private final int code; private final String desc; }核心状态流转我再画一个表,方便你对照:
| 当前状态 | 触发动作 | 目标状态 |
|---|---|---|
| 待支付 | 用户支付成功 | 已支付 |
| 待支付 | 用户取消订单 | 已取消 |
| 已支付 | 平台发货/用户到店自提 | 租赁中 |
| 租赁中 | 用户寄回/到店归还 | 待归还 |
| 待归还 | 运营人员核验无损 | 已完成 |
| 待归还 | 运营人员核验有损/超时 | 已完成(并记录赔偿金额) |
| 已完成 | 押金退款成功 | 退款中 |
| 退款中 | 退款回调成功 | 已完成(最终态) |
这里必须强调:每笔订单的状态流转要配上操作时间记录,因此我还会加一张order_status_log表,每变化一次就插入一条。这在论文里叫“订单操作日志审计”,属于一个加分项。
4.5 定时任务处理超时未支付订单
下单后用户一直不支付,库存被白白占用怎么办?常规方案是:引入定时任务,每分钟扫描一次超过15分钟未支付且未取消的订单,自动将其取消,并释放对应的stock_occupied记录。
SpringBoot 里用@Scheduled注解就能实现。注意两点:
第一,记得在主类或配置类上加上@EnableScheduling。
第二,定时任务虽然每个节点都执行,但实际生产环境会存在多实例重复执行问题。毕设中单机无所谓,但论文里可以写一句“为避免多实例重复执行,可引入分布式定时任务调度框架(如XXL-JOB)”。只提一句就够,不用真实现。
4.6 押金退还与金额计算
押金退还的接口逻辑简单,但容易被忽略一点:只有订单状态是“待归还”并且运营人员确认装备无损后,才能把押金金额置入“退款中”,然后调用第三方模拟支付接口完成退款。不要直接用UPDATE order SET status = 4一把梭,这会导致论文里完全没有资金安全的控制逻辑。
另外,租金计算涉及超时场景。比如用户应还日期是7月10日,实际7月12日才归还,超时2天。合理做法是系统支持“续租申请”,用户在到期前可以申请延长时间;如果未申请直接超时,则在退款押金时自动扣减超时租金。
这类业务逻辑可以写成一个工具类,方便调用:
public BigDecimal calculateOverdueFee(BigDecimal dayPrice, long overdueDays) { return dayPrice.multiply(BigDecimal.valueOf(overdueDays)) .multiply(new BigDecimal("1.5")); }我这里超时费率设为1.5倍,类似服务行业的超时违约金逻辑。答辩时你可以解释:租期即将到期前,平台提前24小时短信提醒用户;超时后按日租金1.5倍收取占用补偿。这比干巴巴说“超时加钱”更有说服力。
5. 前端联调与关键交互逻辑
5.1 前端技术栈选择
毕设前端不建议花太多时间做炫酷特效,用 Vue + Element UI 全家桶就够了。如果你用 Vue 3,就配 Element Plus。我建议用 Vue 3 + Vite + Pinia + Axios + Element Plus,这套组合在 GitHub 上有大量现成模板,三分钟就能起一个管理端框架。
前端目录结构建议:
src ├── api // 按模块拆分的接口请求文件 ├── assets ├── components ├── router ├── store ├── views │ ├── admin │ ├── order │ ├── equipment │ └── user └── utils // request封装、时间格式化5.2 Axios 请求封装
前端最关键的封装是 Axios 拦截器。统一处理:附带Token、统一处理HTTP错误码、处理后端返回的 code 字段、401 跳转登录页。
import axios from 'axios' import { ElMessage } from 'element-plus' import { useUserStore } from '../store/user' const service = axios.create({ baseURL: '/api', timeout: 10000 }) service.interceptors.request.use(config => { const userStore = useUserStore() if (userStore.token) { config.headers['Authorization'] = 'Bearer ' + userStore.token } return config }) service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '系统异常') return Promise.reject(new Error(res.message)) } return res }, error => { if (error.response && error.response.status === 401) { ElMessage.error('登录已过期,请重新登录') // 跳转登录页 } return Promise.reject(error) } )这里要提醒:后端返回结构建议统一为{ code, message, data },不要一会儿直接返回对象,一会儿返回数组。前后端联调时这个规范能省下一大半的沟通成本。
5.3 租期选择与实时价格计算
前端在租赁详情页做一个日期范围选择器,选中日期后,调用后端“可用库存查询”接口,同时前端实时计算预估金额:
const computedAmount = () => { const days = (endDate.value - startDate.value) / (1000 * 60 * 60 * 24) return (equipment.dayPrice * days).toFixed(2) }这里要处理一个边界:用户选了今天,租期不能是负值;用户选了跨月的日期,计算天数要按真实日历天数,不能直接减时间戳除以86400000再四舍五入,因为夏令时和时区差异可能导致算错一天。更好的做法是用dayjs的diff方法:
const days = dayjs(endDate).diff(dayjs(startDate), 'day')接着把开始日期、结束日期传给后端,后端再算一次金额,防止前端改参数钻空子。这个“后端二次校验金额”的行为,面试时也可以提。
5.4 后台管理界面的核心页面
后台管理端一般至少需要这几个页面:
- 装备管理:列表、新增/编辑弹窗、上下架按钮、图片拖拽上传。
- 分类管理:树形或扁平列表都行。
- 订单管理:按状态Tab筛选,支持查看详情、修改订单状态(发货/确认归还)、记录异常。
- 用户管理:列表、启用/禁用。
- 数据统计:使用 ECharts 展示近7日订单量、热租装备Top10、收益曲线。
数据统计这块,你不一定真做复杂的报表,但至少给管理员一个“今天应收、在租订单、待归还数量”的概览数字。这是让系统看起来“完整”的捷径。
6. 部署、测试与常见问题排查
6.1 本地部署流程
毕设项目到交付阶段,最稳妥的部署方式是:本地打包SpringBoot的jar包 + 前端dist目录放到Nginx,或者更简单一点,前端打包后放到SpringBoot的static目录下,实现单端口访问。第二种方式对答辩演示更安全,因为不用单独启动Nginx和前端项目。
具体操作:
- 前端执行
npm run build,把生成的dist目录下的内容复制到 SpringBoot 的src/main/resources/static。 - 后端执行
mvn clean package -DskipTests,生成target/camping-rent-0.0.1-SNAPSHOT.jar。 - 服务器执行
java -jar camping-rent-0.0.1-SNAPSHOT.jar。 - 浏览器访问
http://localhost:8080即可看到完整系统。
这种部署方式的好处是:资源文件和服务在同一端口下,不存在跨域问题。缺点是不适合高并发,但毕设答辩完全够用。
如果你用 Docker 部署,我给你一个最简的 Dockerfile:
FROM openjdk:8-jre-alpine COPY target/camping-rent-0.0.1-SNAPSHOT.jar /app/app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "/app/app.jar"]构建命令:docker build -t camping-rent .,运行命令:docker run -d -p 8080:8080 --name camping-rent camping-rent。
这里有个小坑:容器里访问宿主机MySQL,不能用localhost,要用宿主机局域网IP或者用--network=host。很多人第一次跑Docker项目都栽在这。
6.2 高频问题排查实录
我在陪跑过程中整理了这8个问题,基本覆盖了毕设项目90%的“跑不起来”原因:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
启动报Access denied for user | 数据库账号密码/权限错误 | 检查application.yml,确认用户有读写权限 |
启动报Unknown database | 数据库没建 | 先执行CREATE DATABASE语句 |
| 分页接口不生效 | MyBatis-Plus分页插件未注册 | 配置MybatisPlusInterceptor并添加PaginationInnerInterceptor |
| 日期字段差8小时 | JDBC连接未设置时区 | URL加serverTimezone=Asia/Shanghai |
| 上传图片后无法访问 | 静态资源映射未配置 | 配置ResourceHandlerRegistry映射本地路径 |
| Token一直在登录页循环 | 前端/后端Token过期时间不一致 | 统一JWT有效期和Redis缓存有效期 |
| 跨域报错 | 前端独立端口,后端8080 | 后端加CorsRegistry或部署时统一端口 |
| 本地启动正常,Docker启动连不上MySQL | 容器内不能访问localhost | 改用宿主机IP或加入同一个Docker网络 |
第三个问题特别常见,很多人导入了 MyBatis-Plus 依赖,却忘了加配置类,导致分页不生效,查出来的数据永远是全部。配置方法我放在这里:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }6.3 答辩前的检查清单
最后把我的答辩经验完整分享出来。首先,演示流程要设计一条主线:注册/登录一个用户账号 → 浏览装备 → 选择租期 → 下单支付 → 模拟发货 → 模拟归还 → 押金原路退回。全程控制在3分钟内。这条链路能证明系统业务流程完整。然后,开启MySQL慢查询日志或后端SQL日志放在后台,演示时如果一个接口或SQL报错,你至少能看到报错信息,不至于当场懵。还有一个重要的点:把所有接口统一返回结构,答辩时老师会看Swagger或接口文档,建议集成 Knife4j(Swagger增强版),它能在浏览器里直接调试接口,这比贴一堆截图更有说服力。最后,准备一个“遇到的问题”清单。老师最爱问的是“你项目里遇到最大的困难是什么”。不要说“没遇到”,也不要说“都很顺利”。我当时会这么讲:用户在并发下同时租赁同一装备时,库存可能超卖,因此我使用了Redis分布式锁+事务保证数据一致;后面又发现锁超时和误删问题,于是增加了唯一随机值校验。这个回答有具体问题、解决思路、优化过程,老师一听就知道项目是你自己写的。
6.4 扩展思路:让系统更有亮点
如果你的基础不错,想在答辩时再拉开一点差距,可以在以下方向里选一到两个做深:
- 引入 RabbitMQ 或 RocketMQ,在用户下单成功后发送短信通知、超时未支付延迟取消订单。所谓延迟取消,本质上是 RabbitMQ 的延迟队列。
- 引入 Elasticsearch,代替 MySQL 的 like 查询做装备搜索。当然毕设中用不上,但你把原理写进论文“系统优化与展望”章节没问题。
- 引入 Redisson 的看门狗机制,解决 Redis 分布式锁超时释放问题。这个点非常能体现你读过真实生产场景的方案。
- 引入微信支付模拟沙箱,把押金支付流程做得更真实。
但我要劝一句:不要全上。毕设的核心是“讲清楚一件事”,你做一个完整闭环比十个半成品更符合预期。把基础打好,再加上一两个亮点就足够了。
这套系统做完之后,我最大的感受是:它不是一个普通的 CRUD 练习,而是一个带状态、带资源约束、带资金流的真实业务模型。你只要顺着“用户下单—库存占用—支付押金—归还核验—押金退还”这条业务主线,每一步用对应的技术方案回答“怎么保证数据不出错”,整个项目的逻辑自洽度就会远超同题目的其他作品。希望这份拆解能让你少走点弯路,顺利把系统跑起来。