FEATURED · 精选文章

Spring Boot相机租赁与销售平台:双模式库存锁定与状态机设计案例

发布时间 / 2026/9/15 4:03:23
来源 / 创域科博编辑部
栏目 / 资讯中心
Spring Boot相机租赁与销售平台:双模式库存锁定与状态机设计案例 想说的第一句话是这个题目不是让你做一个“相机商城”而是让你做一个能把“租”和“卖”两条业务线同时跑通的平台。纯商城类毕设答辩时容易陷入“这不就是个增删改查吗”的尴尬但一旦加上租赁周期的库存锁定、逾期归还计算、押金和赔偿金处理业务深度立刻就上来了这也是当时我把毕设题目定为“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 3MyBatis-Plus3.5.x单表CRUD基本不写SQL代码量少很多MySQL8.05.7也能跑但8.0对JSON、窗口函数支持更好Redis任意稳定版做分布式锁、缓存、验证码存储Sa-Token1.34.0比Spring Security上手快适合毕设演示Lombok标准版简化实体类getter/setterHutool5.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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;判断某相机在某段时间是否可租不能只做“库存数量 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_time、end_time、rent_days、unit_price、rent_amount、deposit_amount、insurance_amount、total_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() { ListRentOrder list rentOrderMapper.selectList( new LambdaQueryWrapperRentOrder() .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 本地快速跑通的步骤准备环境JDK8、Maven 3.6、MySQL 8.0、Redis、Node.js 16。创建数据库并导入doc/sql/camera_rent.sql里面包含表结构和基础的初始化数据。修改后端application.yml把数据库账号密码和Redis地址改成你自己的。用IDEA打开后端工程等待Maven下载依赖然后启动CameraRentApplication主类。前端目录下执行npm install依赖装好后执行npm run dev。访问用户端和管理端地址。默认管理员账号在初始化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的理解、数据库设计能力和排查问题的能力都会有质的提升。希望这篇复盘能帮你把项目真正吃透答辩时自信地说出每一行设计背后的理由。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻