Spring Boot 3汉服租赁系统实战:档期冲突检测与并发控制

发布时间:2026/10/21 21:48:16
Spring Boot 3汉服租赁系统实战:档期冲突检测与并发控制
做毕设或者接外包项目这几年我经手过不少“电商/租赁/预约”类的管理系统。但前阵子做的一个“Spring Boot 3 汉服租赁系统”让我印象很深原因很简单汉服租赁这种业务跟普通商品交易完全不是一回事。它要管档期、管尺码库存、管押金、管归还验收甚至要管“这件衣服在某个时间段是否已经被别人订走”。普通商城那套“有货就卖”的逻辑直接搬到汉服租赁上第一天运营就得乱套。这篇文章就把这个系统的设计与实现完整拆开讲。我会从业务痛点、技术选型、数据库设计、核心功能实现到开发中踩过的坑、部署上线的注意事项全部捋一遍。内容适合正在做类似选题的毕业生、准备接租赁类项目的开发者以及想把自己的汉服馆从Excel排期里解放出来的运营者参考。这套设计思路不局限于汉服西装租赁、婚纱租赁、摄影道具租赁都能直接用。1. 为什么汉服租赁不是“套个商城模板”就能搞定的事先说个我调研时的真实场景。有一家汉服体验馆衣服挂在墙上很好看但后台用的是共享Excel表格排期。每件衣服一个sheet顾客订了哪件、哪天穿、哪天还全靠人工填。旺季的时候两个店员同时填一张表经常出现同一件齐胸襦裙在同一天被订给两组客人的情况。即便后来换了个通用商城系统也没解决根子上的问题——因为通用系统根本不知道“租借”和“购买”是两种完全不同的业务模型。1.1 租赁业务和普通电商的本质差异普通商品交易下单即扣库存卖完就完事订单生命周期很短。租赁业务不一样它有完整的闭环选衣 → 下单 → 支付押金 → 到店取衣 → 穿着使用 → 到期归还 → 验收无损 → 退还押金。这个链条里的每一步都对应一套独立的状态和操作。更要紧的是“库存”这个概念。普通商品库存是个数字——剩10件就卖10件。汉服租赁库存是个“时间表”——同一件衣服可以今天租给A、明天租给B、后天空着。也就是说库存维度从“数量”变成了“数量 x 时间段”。一件衣服每天可以被不同的人租但不能在同一时间段被两个人同时租。这是整个系统设计的核心命题后面数据库设计那一章我会详细讲怎么处理。1.2 系统要解决的核心问题清单把需求抽象之后这套系统要覆盖的无非这几件事会员管理顾客注册、登录、个人信息、历史租赁记录、信用状态。服装管理汉服的分类齐胸、齐腰、明制、宋制等、尺码、款式图、租金单价、押金金额、护理状态。档期与库存每件衣服的可租时间段管理租出后对应时间段被锁死归还后释放。订单管理选择服装和时间段、下单、支付租金与押金、提醒到期、登记归还。归还与验收归还时检查衣物是否有污渍、破损按损毁程度决定是否扣押金。统计报表哪件衣服最受欢迎、哪个时间段出租率最高、月度流水多少。这些功能听上去不少但落到Spring Boot 3项目里真正花时间的是业务规则设计而不是写CRUD接口。2. 技术选型为什么我坚持用Spring Boot 3配套组件怎么选这个项目标题点名了“Spring Boot 3”不是顺手写的版本号它确实影响到了实现方式。我就把选型时的考量和实际使用体验一起说。2.1 Spring Boot 3 相比 Boot 2 的几个实际差异Spring Boot 3.0 最大的变化是基础框架升级到 Spring Framework 6并且强制要求 Java 17 及以上版本。这意味着项目一开始就要用新的jakarta.*命名空间替代旧的javax.*。如果你之前写过 Boot 2 的老项目像javax.servlet、javax.persistence这类导入全部要改成jakarta.servlet、jakarta.persistence。刚上手时容易在导入报错上卡一会儿但本质上是替换前缀不算难。Boot 3 默认使用 Jakarta EE 9同时 Spring Security 6 的配置方式和 5 也有不少变化尤其是 SecurityFilterChain 的 Bean 定义方式老写法很多不兼容。我这次做权限模块时已经用 Spring Security 6 的新链式写法避免了把旧代码搬过来再改一遍的麻烦。另外Boot 3 对可观测性做了增强配合 Spring Boot Actuator 可以直接接 Micrometer Tracing。虽然这套系统的规模用不到分布式链路追踪但在本地开发时看一眼健康检查和接口调用指标还是很方便的。2.2 持久层、鉴权、文件与前端方案持久层我选了 MyBatis-Plus理由很实在Boot 3 对应的mybatis-plus-spring-boot3-starter已经发布了新版本分页、条件构造器、逻辑删除都有现成封装能省掉不少样板代码。如果你个人更喜欢 Spring Data JPA也没问题只是针对“多条件筛选服装 时间段查询订单”这类需求MyBatis-Plus 的 XML 自定义 SQL 写起来更直观一点。鉴权方面当时评估了两条路线一套是 Spring Security JWT另一套是 Sa-Token。考虑到后面可能还要扩展小程序登录而且想尽量少写配置我最后用了 Sa-Token。它的登录、注销、权限认证、踢人下线API都直接提供很适合同一个系统里要有用户端和管理端两种角色的场景。不过如果你是为了深入学习 Spring 生态用 Spring Security JWT 仍然是最主流的选择这个看个人取舍。文件存储没有引入 MinIO 或云OSS而是先把图片路径存在本机磁盘数据库里只放 URL 地址。这样在课程设计或小型商单项目里部署成本最低等真上了量再迁对象存储也不难——因为所有代码里只跟 URL 打交道换存储只需要重写上传接口。前端用的是 Vue 3 Element Plus管理后台用现成模板改用户端是自己写的页面通过 axios 调后端接口。实际上租赁前台更常见的是小程序但如果不想多维护一套响应式 H5 页面也能达到同样的效果。2.3 项目整体工程结构工程的包结构我习惯按业务域拆分而不是按 controller/service/entity 这种技术层拆。这样后续加功能时相关代码都集中在一个包下找起来快得多。模拟项目里大致是这样com.hanfu.rent ├── common # 公共返回结果、异常处理、工具类 ├── config # Sa-Token、MyBatis-Plus、跨域等配置 ├── module │ ├── member # 会员模块 │ ├── costume # 服装模块 │ ├── schedule # 档期模块 │ ├── order # 订单模块 │ └── dashboard # 统计模块 └── HanfuRentApplication.java3. 数据库设计房租档期和库存是两张最关键的“王牌”数据库是这类系统的灵魂。我设计的时候把“服装-档期-订单”这三者的关联关系放在最优先级因为一旦表结构建错后续所有接口都会写得别扭。3.1 核心数据表怎么划分系统一共建了这些主要表表名说明关键字段member会员表id, nickname, phone, password, credit_scorecostume服装表id, name, category, size, rent_price, deposit, statuscostume_image服装图片表id, costume_id, image_url, sortschedule档期表id, costume_id, start_date, end_date, statusrent_order租赁订单表id, order_no, member_id, costume_id, schedule_id, rent_fee, deposit, statusreturn_record归还记录表id, order_id, damage_desc, deduct_amount, actual_return_time多说一句把schedule单独建表而不是在订单表里直接写起止时间是有讲究的。一个档期代表“某件衣服在某段时间被占用了”它可以先被创建比如运营预排也可以被订单引用。这样做的好处是后续如果要支持“锁定档期后临时取消再释放给下一个客户”只需要改档期状态不动订单主结构。3.2 档期和库存的核心设计思路时间冲突检测这是全系统最值得细讲的地方。汉服租赁的库存本质是时间。假设某件齐胸襦裙的 ID 是 C001顾客要租 2025-06-01 到 2025-06-03。系统要判断的是C001 的档期里有没有一个时间段和 6月1日到6月3日重叠。如果用 SQL 表达这个冲突查询可以写得很精简SELECT COUNT(*) FROM schedule WHERE costume_id #{costumeId} AND status 1 AND start_date #{endDate} AND end_date #{startDate}这个查询非常关键start_date 查询结束日并且end_date 查询开始日一旦计数大于0就说明存在档期重叠。对于“多件同款服装可同时租借”的场景查询条件里加个size或group_code字段把同款不同件看成独立库存单元即可。我用了一个生活化的类比来解释这个查询给客户听把档期看成是酒店的房间预订记录每个订单入住日期和退房日期不能跟已有预订重叠查重的方式一致。汉服租赁只是把“房间”换成了“一件衣服”。3.3 订单状态机的设计订单生命周期如果定义不清楚代码就会到处是散落的 if 判断。我在开发前先用状态机把租借流程定死了待支付 → 已支付/待取衣 → 租赁中 → 待归还 → 已完成(归还验收通过) ↓ 已取消(取消后释放档期)每个状态只允许固定的流转路径比如“租赁中”可以直接跳到“已完成”但不允许从“待支付”跳到“已完成”。代码里我用了一个简单的状态枚举类来约束避免魔法数字散落在各处。这也是我比较推荐的做法——宁可多写几个枚举也别用 0/1/2 整数到处传。另外我设计了一个字段叫lock_version用在订单表上这就是乐观锁。后面并发章节会再展开说为什么必须加。4. 核心功能实现从注册登录到租赁订单闭环接下来是把设计落成代码的过程。我不贴整段源码重点讲每个模块的实现逻辑以及为什么要这么写。4.1 登录鉴权与双角色处理系统分两个角色顾客和管理员。登录接口只有一个后端根据手机号去查用户表再判断角色。Sa-Token 的写法比较简洁PostMapping(/login) public Result login(RequestBody LoginDTO dto) { Member member memberService.login(dto.getPhone(), dto.getPassword()); if (member null) { return Result.error(账号或密码错误); } StpUtil.login(member.getId()); // 写入角色标识方便后续鉴权 StpUtil.getSession().set(role, member.getRole()); return Result.ok(StpUtil.getTokenInfo()); }管理员删除服装、修改价格这类操作加一个SaCheckRole(admin)注解就能拦住。相比 Spring Security 的 Filter 链配置这个方式的入脑门槛低很多。顾客在用户端只能查看自己的订单不能操作其他用户的订单。这里有个细节查询订单列表时强制从当前登录态里取用户 ID 作为查询条件永远不要信任前端传过来的用户 ID。这是我见过很多新手项目都会犯的错——前端把 userId 放在请求 body 里后端直接拿来做查询结果一个顾客改一下参数就能看到别人的订单。4.2 租赁下单的完整业务序列下单接口是整套系统逻辑最密集的地方伪代码流程如下校验用户登录状态。根据costumeId和前端选择的startDate、endDate查询订单表确认该时段无冲突。开启事务创建订单状态为待支付创建档期记录状态为锁定。计算费用租金 单价 × 租赁天数押金 服装押金字段值。返回订单号和应付总额。很多人会问第二步查一下第四步插入一条中间会不会有并发问题如果不做任何处理确实会。两个用户同时下单同一件衣服都查到“无冲突”然后各自插入一条档期记录数据库里就出现了重叠。我在这个接口上做了三个层面的防护数据库侧给档期表加了一个联合唯一索引或者用悲观锁查询确保硬层面不出现重叠。代码侧查询冲突记录时使用SELECT ... FOR UPDATE锁行串行化同一件服装的档期创建操作。业务侧下单到支付之间有时效超过15分钟订单自动取消同时释放档期。4.3 押金退还与租期费用计算押金逻辑不复杂但有一个体验上的坑用户归还衣服后押金是“秒退”还是“审核后退”如果直接秒退衣服有污渍扣款就变成事后追讨非常被动。我最终实现成归还登记创建return_record管理员或店员录入衣物检查结果如果没有任何问题押金原路退回订单变成“已完成”如果有损坏从押金里扣除相应赔偿剩余退回。费用计算还要处理跨月、续租这种边界情况。比如 6月30日租到7月2日如果按“结束日期减开始日期”的毫秒数除以一天的毫秒数来计算天数时区和夏令时问题时会差一天。稳妥的做法是直接用ChronoUnit.DAYS.between(startDate, endDate)注意结果如果是0天代表当天租当天还这种单日租赁很多店家会要求按2小时计费不能套用按天公式。具体计费规则要跟实际商家约定清楚我这边默认就是按整天计算同时支持配置首日优惠避免运营规则僵化。5. 开发中最容易踩的坑并发扣库存、租期边界、图片处理这部分是我最想分享的内容。很多设计看着没问题一上线就被极端场景打脸。5.1 并发扣库存和档期冲突的实战问题我在自测阶段就用并发工具模拟了“同一个人同时点两次下单”“两个人同时抢同一件衣服”的情况。果然第一次测就出现了重复档期。原因就是我在写代码时只做了普通查询没有走锁。修复思路是这样的把档期创建的核心操作单独抽一个方法加上事务传播机制在查询和插入之间加锁。数据库锁最简单的方式是-- 伪代码查询该服装当前最大结束日期 SELECT MAX(end_date) FROM schedule WHERE costume_id #{costumeId} FOR UPDATE这个FOR UPDATE会把命中的行锁住直到事务提交。这样同一件衣服的并发下单被串行化了第二个请求只能等第一个事务提交后才能查到最新数据自然就能发现冲突。如果你用 MyBatis-Plus分页查询模型再套这种锁会比较绕我建议这种关键 SQL 直接写在 XML 里控制起来最清楚。5.2 租期计算与服务费规则边界租期计算还有一个容易忽略的地方还衣时间。用户下单时选的是“2025-06-01 到 2025-06-03”指的是 6月1日12:00取衣6月3日18:00前还。如果用户 6月3日 晚上10点才还算不算逾期要不要收逾期费这个问题如果不提前定义代码里没法自动判断。我的做法是档期表存了start_date和end_date两个日期字段但在订单外加了一个可空字段expect_return_time专门记录“最晚归还时间”。归还登记时若实际归还时间晚于期望时间则按小时计算过期租赁费。上线之后这一块对商家的价值非常大因为线下门店最怕口头协商没凭据。再有就是“租金按天算还是按件算”。汉服租赁通常是按天算但同一件衣服不同尺码价格一致如果加了配饰头冠、披帛、团扇最好在订单明细表里加一行行项目而不是堆在备注字段里。5.3 图片上传和服装展示的实现细节图片模块看似简单实际有不少坑。首先是存储路径不能写死成绝对路径否则换个环境部署图片就全挂了。我统一用相对路径存放在upload/目录数据库里只放/upload/costume/xxx.jpg然后通过一个配置项把访问前缀拼出来比如hanfu: upload-path: ./upload/ access-prefix: /api/files/**其次图片是“多图”的需求一件汉服至少前端展示图、细节图、模特上身图三张。所以我没有在 costume 表里搞image_url1、image_url2这种字段而是独立成表。这样无论加几张图都自由而且排序字段也好控制。最后是图片校验。只校验扩展名是远远不够的我见过有人直接把一个 JSP 文件改名成 .jpg 传上去然后触发了远程代码执行。所以后端校验要双管齐下扩展名白名单 文件头字节校验。Spring Boot 里可以用Tika或者简单读文件前几个字节来判断真实类型折腾不多但安全收益很高。6. 项目部署与上线从本地跑通到正式营业的考虑事项系统开发完不等于能用部署环节才能真正暴露问题。我把这个项目从 IDEA 里跑通到装在服务商服务器上的全过程踩坑点也一并写了。6.1 本地环境准备细节本机开发环境需要 JDK 17、Maven 3.8、MySQL 8.0。这里有个坑Spring Boot 3 项目创建时如果用 IDEA 默认的 Spring InitializrJava 版本要手动确认选 17否则直接按 8/11 拉骨架项目一启动就报UnsupportedClassVersionError。数据库连接配置上MySQL 8 的驱动类名是com.mysql.cj.jdbc.Driver同时要加上时区参数spring: datasource: url: jdbc:mysql://localhost:3306/hanfu_rent?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: ******如果密码里有特殊字符比如或者#一定要试试直接写在 yml 里会不会被解析出问题。我这次踩过密码是Hf2025yml 里没加引号时启动直接报连接串解析错误加上单引号就好了。这就是个很小的细节但排查起来很耗时间。6.2 打包部署时容易忽略的配置差异开发环境和生产环境最大的差异在于静态资源路径、跨域配置和端口。我用了多环境配置application-dev.yml和application-prod.yml打包时通过spring.profiles.active指定。上线阶段主要做了这几件事后端打成 jar 包用nohup java -jar hanfu-rent.jar --spring.profiles.activeprod启动。反向代理层把/和/api/分别指向前端打包后的静态目录和后端服务避免前后端跨域问题也省了在代码里去配CORS。这一步是重点能不在代码里开跨域就别开交给网关层处理最干净。图片上传目录映射成持久化磁盘目录用 nginx 直接静态目录访问不给 Spring 容器增加压力。定时任务我写了一个每 15 分钟扫描一次待支付订单的清理任务超时的订单自动取消并释放档期。部署时必须保证这个任务只在一个节点上跑否则定时任务重复执行会产生脏数据。6.3 系统上线后的监控与数据备份用 Spring Boot Actuator 暴露了健康检查接口配合一个简单的 shell 脚本定时检查服务是否存活。备份则是每天晚上导出 MySQL 数据库同时把上传的图片目录增量同步到另一个磁盘。对一个小型租赁系统来说这个级别的备份策略完全足够。不要觉得这种系统数据量小就不用备份订单和押金记录一旦丢失商家信誉直接崩掉。另外我还建议商家后台加一个“经营看板”展示今日新增订单、待归还数量、本月流水。统计接口的方式很简单用 SQL 按日期分组做聚合查询就行。但如果订单量涨上去了这种实时 count 会拖慢数据库届时可以再用定时汇总表或者缓存优化目前这个规模完全没必要提前引入 Redis。最后再分享一个我自己强烈推荐的“小功能”订单到期前 24 小时给用户发一条短信或模板消息提醒归还。这条提醒看似不起眼实际能把逾期率降下一大截。实现也容易——定时任务查一下expect_return_time在 24 小时内的订单调一个短信服务商的接口即可。我做完这个功能之后商家反馈是“终于不用天天翻手机催客人还衣服了”这句话比任何技术指标都更能说明系统的价值。如果你也正在动手写这样一套租赁系统建议先别急着写代码把本文第三节的档期冲突查询和第五节的并发问题想透。这两块是租赁类项目区别于普通管理系统的分水岭想通了剩下的都是常规操作。