Spring Boot相机租赁与销售平台:双模式库存锁定与状态机设计案例
2026/9/15 4:02:45 网站建设 项目流程

想说的第一句话是:这个题目,不是让你做一个“相机商城”,而是让你做一个能把“租”和“卖”两条业务线同时跑通的平台。纯商城类毕设答辩时容易陷入“这不就是个增删改查吗”的尴尬,但一旦加上租赁周期的库存锁定、逾期归还计算、押金和赔偿金处理,业务深度立刻就上来了,这也是当时我把毕设题目定为“Spring Boot相机租赁及销售平台”(项目编号92374的源码工程)的核心原因。

下面是我基于这套源码做完整复盘的全过程,涵盖了从需求分析、数据库设计、并发处理到答辩准备的所有关键节点,希望能给正在做Java毕设、Spring Boot项目,或者想拿一个“有点东西”的简历项目的同学一些参考。

1. 这个选题“难”在哪儿:租赁与销售叠加,不是简单拼两个模块

很多同学听到“相机租赁及销售平台”的第一反应是:一个商品表、一个订单表、一个用户表,和电商系统差不多。这是把这个题目做浅了。租赁和销售并行,真正的难点在于库存的时间维度业务状态机的复杂度。这两点想清楚,后面从设计到编码都会顺很多。

1.1 相机租赁的业务闭环和普通商品交易的区别

普通商城是“下单 -> 支付 -> 发货 -> 收货 -> 完成”的单向链路,商品数量只要大于0就能卖。相机租赁不一样,它多了一条“时间”轴:

  • 一台相机在某段时间内只能租给一个人,比如7月1日到7月5日被A租了,B想在7月3日到7月4日取用,系统应该直接判定不可租。
  • 相机的损耗是可变的,押金、保险、损坏赔偿、逾期费用,每一笔都可能跟原订单金额无关。
  • 取机、还机两个节点都需要线下/线上确认,归还时还要填写相机外观状态、配件是否齐全。

这个业务模型天然比普通商城多了一层“资源调度”的含义。你可以把它理解成一个小型酒店预订系统:房间就是相机,入住时间就是租期,退房就是归还验收,只不过酒店卖的是住宿服务,平台卖的是相机使用权。

1.2 出租+出售双模式的库存如何不打架

同一种商品既要“可租”又要“可售”,这是这个题目最容易翻车的地方。比如库存里有5台索尼A7M4,租出去3台,那剩下的2台可以卖掉;但如果新增一笔销售订单把2台都卖了,此时又有用户来租,系统该显示有货还是无货?需要有一个全局的库存逻辑。

两种设计思路:

  • 方案A:库存池统一,租赁占用和销售占用共享总数。租赁中的相机不可再售,已售出的相机不可再租。
  • 方案B:租赁库存和销售库存分开,各管各的池子。

我最终选了方案A。理由是现实中实体店的相机总数是固定的,如果分成两个池子,很容易出现“库存表显示能租,但实际店里已经被卖掉”这种和现实脱节的情况。方案A的代价是租赁和销售都需要经过统一的“占用校验”,但代码上多写一个库存占用接口,换来的是业务上的强一致性。

2. 技术选型:Spring Boot版本、ORM、鉴权组件怎么搭配才不出幺蛾子

这套源码整体的技术栈是:Spring Boot + MyBatis-Plus + MySQL + Redis + Vue 3前后端分离。下面说一下我为什么这么选,以及几个版本搭配上的坑。

2.1 后端核心选型与版本坑

后端以Spring Boot为主框架,这是Java毕设的绝对主流,也是网上资源最丰富、面试最容易问出深度的框架。

我选的具体版本和组件:

组件版本/方案说明
Spring Boot2.7.18稳定,兼容JDK8,绝大多数教程适用
JDK8 或 11不要一上来就JDK17,除非你确定要上Spring Boot 3
MyBatis-Plus3.5.x单表CRUD基本不写SQL,代码量少很多
MySQL8.05.7也能跑,但8.0对JSON、窗口函数支持更好
Redis任意稳定版做分布式锁、缓存、验证码存储
Sa-Token1.34.0比Spring Security上手快,适合毕设演示
Lombok标准版简化实体类getter/setter
Hutool5.8.x日期处理、随机数、Excel导出工具

这里特别提醒一个很多人踩过的坑:Spring Boot 3.x要求JDK17及以上。你如果电脑上装的是JDK8,选了3.x,项目根本起不来,报各种“UnsupportedClassVersionError”。Spring Boot 2.7.x + JDK8是最稳妥的组合,除非你有明确的学习目的要用Spring Boot 3的新特性。

另外,MyBatis-Plus 3.5.x需要和Spring Boot 2.x匹配。有同学在pom里引入了最新的MyBatis-Plus 3.5.5,同时又用了Spring Boot 3,结果Mapper扫描一直报错,后来排查半天才发现是版本兼容问题。所以强烈建议建项目时直接参考我上面这张表,不要盲目追新。

2.2 前端和部署环境的选择

前端我用了Vue 3 + Element Plus + Axios,管理端和用户端是两套独立页面。很多同学问“毕设要不要做前后端分离”,我的观点是:如果你的论文里需要展示架构设计能力,就做分离;如果时间只剩两周,建议直接用Thymeleaf模板,把精力留给后端逻辑。

前后端分离带来的好处是,你可以在答辩PPT里画出一张“Spring Boot后端服务 + Vue前端应用 + Redis缓存 + MySQL持久化”的架构图,让老师一眼看到你对分层架构的理解。同时这项技术栈也和目前企业主流保持一致,面试聊到项目时不至于露怯。

部署上本地演示没必要上Docker,直接在IDEA里跑后端,VSCode里跑前端即可。数据库脚本和初始化数据放到项目的doc/sql目录,方便评委老师自己搭环境复现。

3. 功能模块拆解:从用户浏览到归还验收的完整链路

功能设计不能只列“用户管理、商品管理、订单管理”这种空话。下面按用户端和管理端两条线拆开,核心是把租赁业务闭环里的特殊节点补上。

3.1 用户端的“租、买、还、评”四个主流程

租(租赁相机)

  • 用户在商品详情页看到相机的基础信息,包括品牌、型号、画幅、传感器参数、日租金、押金、可租库存。
  • 选择租赁开始时间和结束时间,系统自动计算租用天数和租金总额。
  • 可选“智能设备保险服务”,保险费用按日租金一定比例收取,逾期时保险可以抵扣部分违约金。
  • 提交订单后需要支付“租金+押金+保险费”三部分费用。我做了模拟支付,环境允许的话也可以对接支付宝沙箱。

买(购买相机)

  • 销售流程和普通商城一致:加入购物车、填写收货地址、生成销售订单、支付、发货、确认收货。
  • 关键点是库存要统一校验:如果当前可用库存已经被租赁订单占用,销售订单就不能再下单。

还(归还申请与验收)

  • 用户租期到达前后,可以在“我的租赁订单”中发起“归还申请”。
  • 上传相机实拍照片和配件照片,作为管理员验收的参考。
  • 管理员在后台确认实际归还时间、检查相机损耗情况,根据损坏程度录入赔偿费用。
  • 如果用户一直没有发起归还,系统定时任务会在租期结束后自动把订单标记为“逾期”,并按天累计逾期费。

评(评价)

  • 订单完成后用户可以对相机本身、租赁体验进行文字评价,评分会展示在商品详情页。
  • 这个模块虽然简单,但建议保留,因为答辩时老师常问“用户反馈从哪里看”“商家如何做改进”,有评价闭环才好回答。

3.2 管理端的“商品、订单、验收、统计”四个后台操作

商品管理

  • 维护相机品牌、型号、参数、租赁价、销售价、押金、封面图、轮播图。
  • 支持上下架操作,下架后用户端不可见,但历史订单不受影响。

租赁订单管理

  • 查看所有租赁订单,按状态筛选(待支付、待取货、租赁中、待归还、逾期、已完成、已取消)。
  • 确认取货后,订单进入“租赁中”状态,同时商品库存被真正锁定。
  • 处理用户归还申请,录入损坏赔偿、遗失赔偿等金额,确认后订单变为完成,押金根据结算结果退还。

销售订单管理

  • 处理销售订单的发货、退款、取消操作。
  • 发货后需要录入物流单号,用户端可以查看物流信息。

数据统计

  • 用ECharts展示近7日交易额、租赁订单数量、热门租赁相机排行。
  • 我额外做了一个“租期日历”,在后台以日历形式展示每台相机未来30天的被租状态,管理者安排库存非常直观。这个功能展示效果好,答辩时是加分项。

4. 数据库设计中的几个关键决策

数据库是整个项目的底层,也是最容易被答辩老师深挖的部分。我在设计表结构时做了几个关键决策,下面展开说明。

4.1 库存模型:库存记录与库存锁定

最核心的一条:不要在相机主表里只存一个整数库存字段就完事。对于租赁业务,库存是跟“时间段”绑定的。我设计了两张表配合:

  • 相机商品表camera:存储相机基础信息和数量总量。
  • 库存锁定表stock_lock:每一行代表某台相机在某时间段内被某个租赁订单占用。

stock_lock核心字段:

CREATE TABLE `stock_lock` ( `id` bigint NOT NULL AUTO_INCREMENT, `camera_id` bigint NOT NULL COMMENT '相机ID', `rent_order_id` bigint NOT NULL COMMENT '租赁订单ID', `lock_start_time` datetime NOT NULL COMMENT '占用开始时间', `lock_end_time` datetime NOT NULL COMMENT '占用结束时间', `status` tinyint DEFAULT '1' COMMENT '1占用中 0已释放', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

判断某相机在某段时间是否可租,不能只做“库存数量 > 0”的检查,必须查时间段冲突:

SELECT COUNT(*) FROM stock_lock WHERE camera_id = ? AND status = 1 AND lock_start_time < #{endTime} AND lock_end_time > #{startTime}

当前时间段已有占用的数量,加上待发货和已售未发货的占用数量,如果小于等于总库存,才允许下单。

这种设计的好处是,一个相机可以被连续租给不同用户,只要时间不重叠即可,且不需要为每个相机生成复杂的“日历表”。本质上就是用数据库的区间查询来模拟时间段冲突,逻辑清晰又好写SQL。

4.2 租赁订单表的状态机与租期重叠检查

租赁订单表我故意把很多字段冗余进去了,比如start_timeend_timerent_daysunit_pricerent_amountdeposit_amountinsurance_amounttotal_amount。为什么冗余?因为订单是一种“快照”,用户在支付那一刻看到的金额就是最终应结算金额,后续价格调整(比如改日租金)不能影响已经生成的订单。

租赁订单状态变更:

状态值含义触发动作
0待支付用户提交订单后生成
1待取货支付完成,等待线下/线上确认取机
2租赁中管理员点击“确认取货”
3待归还用户提交归还申请,管理员未验收
4已完成管理员验收通过,结算押金
5已取消支付前取消或超时未支付
6逾期中租期结束用户未归还

这里最容易忽略的是超时未支付状态。我在订单里增加了expire_time字段,创建订单15分钟后未支付,定时任务自动把订单状态改为已取消,同时释放库存锁定记录。否则一个不付款的订单会把库存锁死,其他用户完全无法下单。

4.3 销售订单与租赁订单是不是要分开

分开。租赁订单和销售订单差异太大:一个要计算天数、押金、保险、归还时间,另一个要处理地址、物流、发货状态。合并到同一张订单表,会有大量字段在某一种订单下永远为空,数据库字段冗余不说,代码里还得做各种if (type == 1)判断,维护成本很高。

分开设计后,两张表各自独立,查询时用order_type区分,统计营收时再联合查询。答辩时如果老师问“为什么不做成一张表”,回答“租赁订单和销售订单是两种不同生命周期和结算模型,分开设计符合单一职责原则”,这就是一个很标准的架构决策答案。

5. 落地实现中必须解决的核心代码问题

功能模块可以靠增删改查堆出来,但真正体现水平的是下面这几个点。我把当时编码过程中最花心思的几段逻辑整理出来。

5.1 两个用户同时下单租同一台相机怎么办

这是一个典型的并发问题。假设库存只有1台相机,用户A和B同时看到了“可租”,同时点击下单。如果代码写的是先查询、判断、再插入,那么两个请求可能都通过了判断,最后产生超卖。

我的做法是:使用Redis分布式锁,锁的粒度是“相机ID”。当用户对某台相机发起租赁下单时,先尝试获取这把锁,拿到锁之后再做时间段冲突判断、库存校验和订单创建,最后释放锁。

@Transactional public Result createRentOrder(RentOrderCreateDTO dto) { String lockKey = "camera:rent:" + dto.getCameraId(); boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, "1", Duration.ofSeconds(10)); if (!locked) { return Result.error("当前操作人数较多,请稍后重试"); } try { // 1. 检查时间段冲突 int conflictCount = stockLockMapper.selectConflictCount( dto.getCameraId(), dto.getStartTime(), dto.getEndTime()); Camera camera = cameraMapper.selectById(dto.getCameraId()); if (conflictCount >= camera.getTotalStock()) { return Result.error("该时间段相机已被租出"); } // 2. 创建租赁订单 // 3. 写入库存锁定记录 // 4. 返回支付参数 } finally { redisTemplate.delete(lockKey); } }

这里锁的超时时间设成10秒,是为了防止代码异常导致锁无法释放,形成死锁。正常情况下下单接口不会执行10秒,所以这个兜底是够用的。

除了Redis锁,数据库层面我在stock_lock表增加了一个唯一索引(camera_id, rent_order_id),确保同一租赁订单不会被重复锁定。这样哪怕锁因为极端情况失效,数据库也会兜底报错,不至于出现严重的数据错乱。

5.2 超时未归还和逾期费自动计算

租赁业务里一定要有“到了归还日还没归还”的后续处理机制。我写了一个定时任务,每10分钟扫描一次所有“租赁中”和“待归还”的订单,如果当前时间已经超过了订单的end_time,就把状态更新为“逾期”,同时计算逾期天数。

@Scheduled(cron = "0 */10 * * * ?") public void checkOverdueOrders() { List<RentOrder> list = rentOrderMapper.selectList( new LambdaQueryWrapper<RentOrder>() .in(RentOrder::getStatus, Arrays.asList(2, 3)) ); for (RentOrder order : list) { if (order.getEndTime().before(new Date())) { long diffMs = System.currentTimeMillis() - order.getEndTime().getTime(); int overdueDays = (int) Math.ceil(diffMs / (24.0 * 3600 * 1000)); if (order.getStatus() != 6) { order.setStatus(6); order.setOverdueDays(overdueDays); order.setOverdueFee( rentOrderService.calcOverdueFee(order, overdueDays)); rentOrderMapper.updateById(order); } } } }

逾期费的计算规则,我在系统里做了可配置:基础逾期费=日租金*0.5,如果用户购买了保险,每天额外有30元上限抵扣。这个规则写在配置表里,而不是写死在代码中,答辩时可以提一下“可配置化”的设计理念。

5.3 后端校验租期冲突的SQL写法

除了并发场景,普通的校验查询也需要写对。核心就是区间重叠判断,SQL条件:

lock_start_time < #{endTime} AND lock_end_time > #{startTime}

简单解释一下:两个时间区间[start1, end1][start2, end2]存在重叠,当且仅当start1 < end2 AND end1 > start2。这个条件很多人会写成start1 <= end2 AND end1 >= start2,其实边界情况要看业务定义:比如相机还机当日可以继续租给下一个人,那就不该包含等于,而应该用严格小于/大于。这个细节在答辩时如果被问到,就能体现你对边界情况的考虑。

5.4 上传图片与静态文件映射

商品图片、用户归还凭证、评价图片都需要上传功能。我做了本地存储方案,上传目录通过配置文件指定:

spring: web: resources: static-locations: file:D:/upload/

上传接口逻辑:

@PostMapping("/upload") public Result upload(@RequestParam("file") MultipartFile file) throws IOException { if (file.isEmpty()) { return Result.error("文件不能为空"); } String originalFilename = file.getOriginalFilename(); String ext = StringUtils.getFilenameExtension(originalFilename); String fileName = UUID.randomUUID() + "." + ext; File dest = new File(uploadPath, fileName); file.transferTo(dest); return Result.ok("/upload/" + fileName); }

前端拿到返回的图片路径后,直接拼在img标签的src上即可。本地演示阶段不要上OSS,没必要,代码里留着这个接口,以后要接云存储也方便改。

6. 部署、论文写作、答辩准备的实战经验

最后这部分可能是很多同学最想直接“抄作业”的。我把从启动项目到答辩的所有关键点尽量浓缩在这里。

6.1 本地快速跑通的步骤

  1. 准备环境:JDK8、Maven 3.6+、MySQL 8.0、Redis、Node.js 16+。
  2. 创建数据库并导入doc/sql/camera_rent.sql,里面包含表结构和基础的初始化数据。
  3. 修改后端application.yml,把数据库账号密码和Redis地址改成你自己的。
  4. 用IDEA打开后端工程,等待Maven下载依赖,然后启动CameraRentApplication主类。
  5. 前端目录下执行npm install,依赖装好后执行npm run dev
  6. 访问用户端和管理端地址。默认管理员账号在初始化SQL里已经插入,登录后台即可。

整个流程如果顺利,只用三步:导入SQL、改配置、启动。关键在于不要跳过初始化数据,管理端账号、分类数据、示例商品都在里面。

6.2 答辩常被问的几个“送命题”怎么回答

我总结了自己和周围同学被问得最多的几个问题:

1. “你这个库存到底是怎么扣的?”

答:不是简单的减库存。租赁订单会生成库存锁定记录,销售订单会占用销售数量,每次下单前都先做时间段冲突判断和数量校验。并发场景使用Redis分布式锁把同一台相机的下单请求串行化执行,数据库唯一索引兜底。

2. “Spring Boot自动装配原理是什么?”

答:@SpringBootApplication里包含了@EnableAutoConfiguration,它通过导入AutoConfigurationImportSelector,读取META-INF下记录的自动配置类,通过条件注解(如@ConditionalOnClass@ConditionalOnMissingBean)按需创建Bean。回答时先讲入口、再讲SPI机制、最后讲条件装配,层次清晰,基本不会被追问更细。

3. “为什么选择MyBatis-Plus?”

答:解决单表CRUD的重复编码问题,内置分页插件、逻辑删除、自动填充,LambdaQueryWrapper可以在编译期检查字段名。同时SQL仍然可以通过自定义Mapper方法控制,保留了灵活性。

4. “如果数据量大了,这个系统怎么做优化?”

答:可以分方向回答:MySQL查询层面加索引、用Redis缓存热门相机信息和库存状态、订单表按时间归档、引入消息队列削峰。重点不是你真的做了,而是能说出来方案和理由。

6.3 给这套源码做二次扩展的方向

如果你做完之后还有余力,我强烈建议做下面几个方向之一,既是简历上的亮点,也能避免和同学的题目重复:

  • 对接支付宝沙箱支付:把模拟支付替换成真实支付回调,支付结果通过异步通知处理,订单状态流转会更完整。
  • 小程序端:相机租赁本身非常适合移动端场景,微信小程序的“拍摄取机、上传归还照片”体验比PC端好很多,而且前端框架Vue3可以配合uni-app快速迁移。
  • 消息通知:在归还日前一天通过短信或邮件提醒用户,减少逾期率。
  • 推荐系统:根据用户历史和浏览行为,推荐相似的相机,可以用简单的基于标签匹配的算法实现。

我在实际做这套项目时,最深的体会是:毕设题目的价值不在标题有多花哨,而在业务模型够不够复杂、代码处理够不够严谨。相机租赁这个场景正好卡在一个绝佳的位置——有电商的基础,又有租赁特有的时间维度和并发问题。做完它,你对Spring Boot的理解、数据库设计能力和排查问题的能力都会有质的提升。希望这篇复盘能帮你把项目真正吃透,答辩时自信地说出每一行设计背后的理由。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询