最近帮朋友救急,接手了一个Java Web方向的项目:高校汉服租赁网站系统,技术栈定的是SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0,要求前后端分离,配套开发文档。项目本身不算特别重,但把汉服租赁这种带"衣物租赁周期+尺码库存+押金管理"的业务走通一遍,踩了不少值得记录的坑,正好整理出来,给准备做毕设、课程设计,或者想抄一个校园租赁类项目作业的同学当参考。
这篇我会从技术选型逻辑、项目目录规划、数据库设计、租赁业务核心流程、前后端实现,以及部署时MySQL8.0那几个经典问题这几个角度展开,全程按实际开发顺序讲,代码思路多于完整代码,毕竟每个学校的课程设计要求不一样,直接抄代码意义不大,搞清楚为什么这么设计才值钱。
1. 为什么定这套技术栈:SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0
1.1 从需求反推选型,而不是先选型再套需求
做毕业设计最忌讳的事情就是一上来堆技术名词。你去看那些评分比较高的项目,技术栈不一定最新,但每一样都能说清楚解决了什么问题。这个汉服租赁系统,核心需求其实特别实在:
- 学生用户能注册登录、浏览汉服列表、按分类筛选、查看详情、加入购物车、下单租赁、支付押金和租金。
- 管理员能维护服装信息(名称、分类、尺码、日租金、押金、图片)、处理订单状态、处理归还和续租、查看统计。
- 整个流程要符合高校社团实际使用场景,也就是社团统一采购一批汉服,向校内有活动需求的学生出租,订单有明确起止日期,不存在"无限库存"。
基于这个需求,SpringBoot2足够稳。SpringBoot3虽然已经发布一段时间,但很多教程资源、第三方集成方案还停留在SpringBoot2的惯性里,对毕设来说,稳定、资料多、踩坑少才是硬指标。Vue3作为前端框架则纯粹是趋势问题,现在新开的Vue项目没有必要再回到Vue2的Options API老路上去,Composition API在处理复杂的表单与状态联动时明显更顺手。MyBatis-Plus解决了单表CRUD的大量重复劳动,尤其后台管理的增删改查页面,几乎没有手写SQL的必要。MySQL8.0则是当前主流版本,处理utf8mb4中文排序、窗口函数都比5.7省心。
1.2 为什么没有用Spring Data JPA
很多同学会纠结MyBatis-Plus和Spring Data JPA到底选哪个。我这次选MyBatis-Plus,核心原因就一个:租赁系统的订单查询往往有复杂的多条件动态拼装。比如"查询某个时间段内、某个分类下、尺码为M、且尚未被预订的汉服",这种需求用JPA的Specification写起来会绕一大圈,而MyBatis-Plus的QueryWrapper + LambdaQueryWrapper几乎是一行代码的事情,配合MP自带的分页插件PaginationInnerInterceptor,后台分页列表开发效率碾压。
对比一下大概是这样:
| 对比维度 | MyBatis-Plus | Spring Data JPA |
|---|---|---|
| 单表CRUD | 继承BaseMapper即完成,非常快 | 继承JpaRepository即完成,也快 |
| 多条件动态查询 | QueryWrapper/LambdaQueryWrapper,简洁直观 | Specification/Criteria,写法繁琐 |
| 分页 | 分页插件,一行配置,物理分页 | Pageable自带,但复杂查询需要改造 |
| 复杂SQL | XML写SQL,灵活可控 | JPQL或原生SQL,稍显别扭 |
| 学习成本 | 低 | 中 |
| 与MyBatis生态衔接 | 原生兼容 | 需要额外适配 |
从实际执行效率上看,JPA的@ManyToOne、懒加载这些问题,处理不好反而容易出现N+1查询,对毕设答辩来说是个隐患。Mp在这一点上更直白,查出来的就是DTO或Map,不需要考虑实体关联关系。
1.3 前端为什么是Vue3 + Vite,而不是Vue2 + Webpack
标题里写的是Vue3,这里的细节需要注意的是初始化工具。如果有人还在用vue create命令创建Vue2项目,就明显落后了。当前主流是用Vite初始化Vue3工程,命令是npm create vite@latest。Vite依赖原生ESM,本地开发启动速度快到几乎没有等待感,和Webpack那种动辄十来秒的冷启动完全不是一个体验。
在Vue3内部,组合式API配合<script setup>语法,让组件逻辑复用变得非常直接。以汉服列表页的筛选功能为例,分类、价格区间、尺码、上架时间这几个筛选条件,以前Vue2里要写在data、computed、watch三个地方,Vue3里只用ref和computed就能完成闭环,逻辑全部收敛在一个<script setup>块里,读代码的人一眼就能看懂筛选逻辑是怎么流转的。
2. 项目目录结构怎么设计:从Maven后端到Vite前端
2.1 后端标准目录结构与包拆分逻辑
一个规范的Java Web项目,目录结构本身就是评分点。Maven约定的结构是:
hanfu-rental/ ├── src/main/java/com/example/hanfu/ │ ├── controller/ # 控制层,只做参数接收与结果封装 │ ├── service/ # 业务层,核心逻辑全部在这里 │ │ └── impl/ # Service实现类 │ ├── mapper/ # MyBatis-Plus的Mapper接口 │ ├── entity/ # 数据库实体类 │ ├── dto/ # 接收前端参数的对象 │ ├── vo/ # 返回前端的视图对象 │ ├── config/ # 配置类(MyBatis-Plus分页、CORS、拦截器等) │ ├── common/ # 通用返回结果、异常处理、常量 │ ├── utils/ # JWT工具、日期工具等 │ └── HanfuApplication.java # 启动类 ├── src/main/resources/ │ ├── mapper/ # MyBatis XML文件 │ ├── application.yml │ └── static/ # 本地存储上传文件的目录 └── pom.xml这里最想强调的一点是:Controller一定要薄。很多毕设项目把业务逻辑写在Controller里,看起来代码没少写,但以后每次改动都可能牵一发动全身。租赁系统的核心业务比如"下单预占库存""归还时计算延期费用",这些逻辑必须放在Service层,Controller只负责确认用户登录状态、接收参数、调用Service、返回结果。
DTO和VO的区分也很重要,直接拿数据库实体返回给前端会造成两个麻烦:一是多余的字段暴露给前端,比如密码哈希值;二是数据库表结构调整时,前端接口也跟着变,耦合太重。我的习惯是Entity绝不直接出参,至少套一层VO。
2.2 前端目录结构与路由设计
前端部分我用Vite初始化,目录也做了分层:
hanfu-web/ ├── src/ │ ├── api/ # 所有axios请求封装 │ ├── components/ # 公共组件 │ ├── router/ # 路由配置 │ ├── store/ # Pinia状态管理 │ ├── views/ # 页面组件 │ │ ├── home/ # 首页 │ │ ├── hall/ # 汉服列表页 │ │ ├── detail/ # 详情页 │ │ ├── cart/ # 购物车 │ │ ├── order/ # 订单确认/我的订单 │ │ ├── user/ # 个人中心/登录注册 │ │ └── admin/ # 后台管理 │ ├── utils/ # 请求拦截器、日期工具 │ ├── App.vue │ └── main.js └── package.json路由设计上要注意一个点:前台商城和后台管理虽然都在同一个工程里,但应拆成两套路由嵌套。前台是普通用户浏览使用,后台需要管理员权限,因此在路由守卫router.beforeEach里,需要先判断目标路由是否带有meta.requiresAdmin标记,如果有,就检查Pinia里保存的userInfo.role是否为1(管理员)。之所以强调这个,是因为很多毕设直接用一个路由表,不做权限区分,答辩时老师一问"你怎么控制管理员页面访问",就答不上来。
2.3 需要一份"含文档"的项目,文档里应该写什么
这个项目标题特别注明了"含文档",实际指的是完整的开发文档,包括需求分析、数据库设计说明书、接口文档、部署说明。文档不见得要几十页,但一定要覆盖这几个点:
- 需求分析:画清楚角色图谱。这个系统至少有游客、学生用户、管理员三种角色,分别能做什么。
- 用例图:把注册、登录、浏览、下单、支付、管理服装、订单处理这些用例列出来。
- 数据库表结构说明:每张表的字段、类型、注释、关联关系。
- 接口文档:建议用Swagger/knife4j自动生成,不用手写Markdown,但要保证注释写得规范。
- 部署文档:如何初始化数据库、修改配置文件、启动后端、启动前端。别觉得这些简单就不写,评分老师最看重的恰恰是这个。
3. 数据库设计:汉服租赁比普通商品交易多出来的那些坑
3.1 核心数据表
汉服租赁本质上是一个"有库存、有租期"的租赁交易系统,和普通电商 buying 一次性商品最大的区别在于库存的占用是按时间区间计算的。表结构我按模块拆分成用户、服装、订单、内容管理、日志五个部分,以下是最核心的几张表:
用户表sys_user:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键自增 |
| username | varchar(50) | 登录名,唯一 |
| password | varchar(100) | BCrypt加密后的密码 |
| nickname | varchar(50) | 昵称/姓名 |
| phone | varchar(20) | 联系方式 |
| avatar | varchar(255) | 头像图片地址 |
| role | tinyint | 0普通用户 1管理员 |
| status | tinyint | 0正常 1禁用 |
| create_time | datetime | 注册时间 |
汉服表t_clothing,这个表的重点是要体现"租赁属性"和"尺码属性":
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| name | varchar(100) | 服装名称,例如"齐胸襦裙-淡紫" |
| category_id | bigint | 关联分类表 |
| cover | varchar(255) | 封面图 |
| images | text | 详情图,多张图片用逗号分隔 |
| sizes | varchar(50) | 可租尺码,例如"S,M,L" |
| total_stock | int | 总库存件数 |
| daily_price | decimal(10,2) | 日租金 |
| deposit | decimal(10,2) | 押金 |
| status | tinyint | 1上架 0下架 |
| description | text | 服装描述,材质、适合身高、适用场合 |
| create_time | datetime | 上架时间 |
订单表t_order和订单明细表t_order_item其实是租赁系统最核心的部分。一个订单可以包含多件汉服,每一件在明细里记录租赁开始日期和结束日期:
订单表t_order:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 订单ID |
| order_no | varchar(32) | 订单编号,时间戳+随机数生成 |
| user_id | bigint | 下单用户 |
| total_amount | decimal(10,2) | 租金总额 |
| deposit_amount | decimal(10,2) | 押金总额 |
| status | tinyint | 0待支付 1已支付 2租赁中 3待归还 4已完成 5已取消 |
| create_time | datetime | 下单时间 |
| pay_time | datetime | 支付时间 |
| return_time | datetime | 实际归还时间 |
订单明细表t_order_item:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| order_id | bigint | 关联订单ID |
| clothing_id | bigint | 关联汉服ID |
| clothing_name | varchar(100) | 服装名称快照 |
| size | varchar(10) | 租赁尺码 |
| daily_price | decimal(10,2) | 当时日租金快照 |
| days | int | 租赁天数 |
| start_date | date | 开始日期 |
| end_date | date | 结束日期 |
| item_amount | decimal(10,2) | 单项金额 |
3.2 为什么订单明细里要冗余服装名称和价格快照
这个设计很多人第一次做会忽略。一条订单明细关联了clothing_id,按第三范式来说确实不应该冗余clothing_name和daily_price。但租赁场景里存在一个实际痛点:服装信息每天都在变化,管理员可能修改名称、调价、下架。如果订单明细只是指向服装表,那用户在"我的订单"里看历史订单时,服装名称和价格可能已经不是下单时的样子了,要是管理员刚好把服装下架了,前端查不到关联数据,还会出现空指针或订单页面数据缺失。
所以订单明细保留"快照"字段,既不是偷懒,也不是不遵守范式,而是对业务场景的真实妥协。同理,也在订单里冗余了user_id对应的用户昵称,避免后台订单列表每次都要连表查询用户表。
3.3 尺码与库存怎么建模
网上不少二手项目把尺码直接拼成一个字符串"XL,XXL,L"存在一个字段里,这样确实省了事,但一旦需要按尺码精确查询库存就会很痛苦。更合理的做法是单独建一张库存表t_clothing_stock:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| clothing_id | bigint | 关联汉服 |
| size | varchar(10) | 尺码 |
| stock | int | 这个尺码的库存件数 |
为什么需要拆这张表?因为汉服不同尺码的备货数量往往不对等,比如社团采购时M码买了10件,XXL码可能只有2件。每次下单占用的是特定尺码的库存,不是笼统的"整件服装减少1"。库存表拆出来后,查询"某件汉服当前哪些尺码还有货"就变成了一条简单的SQL:WHERE clothing_id = ? AND stock > 0。
查询"某个时间段是否有货"就复杂一点,需要排除已经被订单占用的时间区间,这一块也是租赁系统最核心的冲突检测逻辑,下一节详细说。
4. 租赁业务核心逻辑:预占、冲突检测与延期归还
4.1 下单时怎么做租期冲突检测
普通电商下单只需要判断库存数量是否足够,因为商品不存在"被借走"的状态。租赁系统不一样,一件汉服在5月1日至5月3日被A同学预订了,那B同学就算在4月30日下单,也不能选择5月1日至5月3日这个区间。
冲突检测的SQL设计逻辑如下,以clothing_id = 10、尺码M、新订单时间区间为start = 2024-05-01、end = 2024-05-03为例,需要查出所有与该区间有重叠的已支付或租赁中的订单:
SELECT COUNT(*) FROM t_order_item oi INNER JOIN t_order o ON oi.order_id = o.id WHERE oi.clothing_id = 10 AND oi.size = 'M' AND o.status IN (1, 2, 3) -- 已支付、租赁中、待归还均视为占用 AND oi.start_date <= '2024-05-03' AND oi.end_date >= '2024-05-01'这段SQL的精髓在于重叠区间判断条件:已有订单开始日期 <= 新订单结束日期且已有订单结束日期 >= 新订单开始日期,两个条件同时满足即代表时间区间存在重叠。这是区间重叠判断的经典公式,比用"是否在某两个时间点之间"这种直觉写法要严谨得多。
4.2 库存扣减与防超卖
冲突检测通过之后,还需要对库存进行防并发操作。这里有两个常见方案:
方案一:先查库存,再在代码里判断是否大于0,最后UPDATE减少库存。这种方式在高并发下会出现超卖,因为两个线程同时查到stock=1,然后都执行了扣减,最终库存变成-1。
方案二:把判断写进SQL语句,利用MySQL的行锁保证原子性:
UPDATE t_clothing_stock SET stock = stock - 1 WHERE clothing_id = 10 AND size = 'M' AND stock > 0如果UPDATE影响行数为1,说明扣减成功;如果影响行数为0,说明库存不足或尺码不存在。这是一个非常实用的防超卖写法,不需要手写SELECT FOR UPDATE,也不需要引入Redis分布式锁,在毕设和小型项目的并发量级下完全够用。
需要注意一点:库存扣减必须和创建订单在同一个事务里执行。如果订单创建成功但库存扣减失败,事务回滚,两个操作要么都成功,要么都失败。Spring的@Transactional注解就是干这个的。
4.3 归还、延期与押金退还流程
租赁结束,用户归还汉服后,管理员在后台点击"确认归还"。这个动作要做的事情比表面看起来多:
- 修改订单状态为已完成(status=4)。
- 回补库存:根据订单明细里的服装ID和尺码,执行
stock = stock + 数量。 - 计算是否需要补交费用:如果实际归还日期晚于订单结束日期,按超出的天数乘以日租金计算延期费。
- 更新押金状态:全款退还、扣减延期费后退还、或者因服装损坏扣除赔偿金后把剩余押金退还。
延期费的计算在Service层里就是个纯函数逻辑:
long diffDays = ChronoUnit.DAYS.between(orderItem.getEndDate(), actualReturnDate); if (diffDays > 0) { BigDecimal lateFee = dailyPrice.multiply(BigDecimal.valueOf(diffDays)); // 记录到订单的额外费用字段 }这里也是用ChronoUnit.DAYS.between而不是(endDate.getTime() - startDate.getTime()) / 86400000,后者在夏令时、闰秒等极端情况下容易出现一天误差,虽然中国没有夏令时了,但养成好习惯没坏处。
另外一个业务细节值得在答辩时主动提出来:学生社团的租赁大多数不涉及物流配送,所以整套系统没有做收货地址和运费模块,订单都默认线下自取。这个决策一定要在需求分析里写清楚,否则评委老师会问"为什么不考虑运费"。
5. 后端接口设计:从登录鉴权到订单管理的实现思路
5.1 JWT登录鉴权与拦截器配置
整个系统的接口多数需要登录后才能访问,但列表页、详情页可以让游客浏览。我采用JWT做无状态鉴权,流程是:
- 用户提交用户名密码,调用
/api/auth/login。 - 后端用BCrypt验证密码,验证通过后生成JWT,放入
claims中保存userId和role,返回前端。 - 前端把token存在
localStorage,在axios请求拦截器里统一加入Authorization: Bearer <token>请求头。 - 后端在SpringBoot中注册拦截器,拦截所有
/api/**请求,排除掉登录、注册、商品列表等白名单地址。校验token通过后,解析出用户信息放入ThreadLocal或RequestContext。
这里特别提醒一个坑:JWT的SECRET_KEY一定不要写死在代码里,至少要放在application.yml的外部配置里。另外HS256算法足够用,不用为了炫技去搞RS256,你那点用户量根本不存在证书分发问题。
5.2 统一返回结果与全局异常处理
前后端分离项目最忌讳每个接口返回结构不一致。我的习惯是定义一个通用返回体Result<T>:
public class Result<T> { private Integer code; // 200成功 401未登录 500服务器错误 private String message; private T data; }同时用@RestControllerAdvice做全局异常捕获。修改一个特别容易踩的坑:全局异常处理器一定要区分业务异常和系统异常。业务异常比如"该时间段已被预订"应该返回code=200但message提示,还是直接返回code=500?我建议业务异常返回code=200但携带业务错误码,因为HTTP状态码用于标识请求是否完成,而不是业务是否成功。这在前端axios拦截器里处理起来最顺滑,只需要判断res.data.code,不需要关心HTTP 4xx/5xx的语义问题。
5.3 MyBatis-Plus分页配置
后台管理页面几乎每个列表都需要分页,MyBatis-Plus自带的分页插件配置起来很简单,但要小心版本差异。SpringBoot2 + MyBatis-Plus 3.5.x的配置是:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }这个配置加完,Service层里直接:
Page<ClothingVO> page = clothingMapper.selectPage(new Page<>(pageNum, pageSize), new LambdaQueryWrapper<Clothing>() .eq(StringUtils.hasText(categoryId), Clothing::getCategoryId, categoryId) .like(StringUtils.hasText(keyword), Clothing::getName, keyword) .eq(status != null, Clothing::getStatus, status));LambdaQueryWrapper的条件里第一个参数是boolean,为true时才拼接该条件,这种写法可以完全避免字符串拼接SQL的麻烦。这也是MyBatis-Plus相对原生MyBatis最大的价值所在。
5.4 图片上传的本地存储方案
汉服的展示图片是刚需,每件衣服至少一张封面图,详情页还需要多张展示图。考虑到毕设项目没有条件买OSS,我用本地存储方案:
- 配置虚拟路径映射,把
/upload/**映射到本机磁盘目录。 - 上传接口接收
MultipartFile,校验图片类型和大小,存储到配置的目录。 - 返回给前端时,拼接成完整的可访问URL。
配置虚拟路径映射的方法是:
@Configuration public class WebConfig implements WebMvcConfigurer { @Value("${upload.path}") private String uploadPath; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceHandler("file:" + uploadPath); } }如果没做这个映射,前端访问图片会404,部署时尤其容易忘记,记一下。
6. Vue3前端的关键实现:从首页到后台管理
6.1 首页与列表页的渲染逻辑
前端首页主要展示轮播图、分类入口、热销汉服推荐。轮播图数据可以做成后台可管理,也可以先写死在接口里,我建议做成banner表,管理员可以修改轮播图,前后端联调更完整。
列表页是重头戏,筛选条件包括分类、尺码、日租金区间、是否可租。这个页面我用Vue3的computed来处理"当前筛选条件下有哪些服装":
<script setup> import { ref, computed, onMounted } from 'vue' const clothesList = ref([]) const filter = ref({ categoryId: '', size: '', maxPrice: null }) const filteredList = computed(() => { return clothesList.value.filter(item => { if (filter.value.categoryId && item.categoryId !== filter.value.categoryId) return false if (filter.value.size && !item.sizes.includes(filter.value.size)) return false if (filter.value.maxPrice && item.dailyPrice > filter.value.maxPrice) return false return true }) }) </script>这里的核心逻辑是:页面渲染时始终使用filteredList,而不是直接使用原始列表clothesList。这样无论用户怎么改筛选条件,视图层都会自动重新计算,不需要写一堆事件监听和手动赋值代码。
6.2 购物车与本地状态管理
购物车数据我用Pinia管理,状态定义为一个数组,每个元素包含clothingId、name、cover、dailyPrice、size、startDate、endDate、days、itemAmount。为什么购物车里要记录起止日期?因为汉服租赁的租金计算依赖租赁天数,把日期放进购物车项里,下单时就能直接算出总金额,购物车页面也可以展示"小计:日租金 × 天数"。
用Pinia的好处是在首页、列表页、详情页都可以很方便地往购物车里添加商品,而不需要组件间事件层层传递。我在状态里封装了一个addItem(payload)方法,内部自动计算日期差并设置itemAmount,这也是租赁系统与普通电商购物车最大的差异点。
6.3 订单确认页与支付模拟
订单确认页需要展示购物车里的全部商品,确认租赁起止日期,并展示租金合计、押金合计。然后用户点击"提交订单",后端完成库存预占和订单创建。支付这块因为毕设不接真实支付,通常做成模拟支付:创建订单后,前端调用/api/order/pay/{orderId},后端直接把订单状态从待支付改为已支付。
有一点要注意:"已支付"状态和"租赁中"并不一样。已支付表示用户付了钱,但租期还没开始;租赁中表示租期已经开始。如果用户的租期是明天开始,那么今天订单状态就是"已支付",而系统在后台仍然要把它显示为"即将开始"。很多毕设项目把这两个状态混为一谈,导致时间判断逻辑出错。状态机设计建议这样走:
- 0待支付:下单后未支付,超时可取消。
- 1已支付:已付款,租期未开始。
- 2租赁中:当前日期在Start到End之间。
- 3待归还:End日期已过但未确认归还。
- 4已完成:已确认归还。
- 5已取消:用户取消或超时系统取消。
在实际开发中,"租赁中"和"待归还"之间不需要定时任务去扫描转换,可以在查询列表时根据start_date、end_date与today的比较动态计算出来,避免写复杂的定时器。
6.4 后台管理系统:用Element Plus快速搭建
后台管理页面使用Element Plus,布局就是经典的侧边栏+顶部+主内容区。核心页面包括:仪表盘(订单量、营收、租赁中的订单数)、服装管理(增删改查)、分类管理、订单管理(列表、查看详情、确认归还)、用户管理(列表、禁用启用)、公告管理。
管理员的订单列表是"含金量"最高的页面,因为这里需要同时展示用户信息、服装信息、租赁时间、订单金额、押金状态、订单状态。设计表格时建议用el-table嵌套多个列,在"操作"栏根据订单状态动态显示按钮:已支付显示"确认出借",待归还显示"确认归还",已完成后只读。
6.5 Vue3中用Axios封装请求的写法
所有接口请求统一走src/utils/request.js,在里面封装axios实例、请求拦截器和响应拦截器。响应拦截器要统一处理401跳转登录和业务错误码:
service.interceptors.response.use( (response) => { const res = response.data if (res.code === 200) { return res } if (res.code === 401) { localStorage.removeItem('token') router.push('/login') } ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message || '请求失败')) }, (error) => { ElMessage.error(error.message || '网络异常') return Promise.reject(error) } )这样在页面里调用时,只需要关心成功分支返回res.data,失败分支由拦截器统一处理,页面代码干净很多。
7. MySQL8.0部署与配置:最容易被卡住的那几个点
7.1 安装与初始化的基本流程
MySQL8.0和5.7的最大变化之一是默认认证插件从mysql_native_password改成了caching_sha2_password。这个变化带来的直接后果是:老版本JDBC驱动、老版本Navicat(12以下)连接MySQL8.0时会报Authentication plugin 'caching_sha2_password' cannot be loaded错误。解决办法有两个:
一是用最新版本JDBC驱动和最新版Navicat。SpringBoot2的mysql-connector-java依赖要注意版本,我使用的依赖坐标是:
<dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>SpringBoot2.7.x管理该依赖的版本默认是8.0.x,一般不需要显式指定版本号,但如果是特别旧的SpringBoot版本,需要手动加上版本号。
二是如果实在要用老客户端,可以建用户时指定老认证插件:
CREATE USER 'hanfu'@'%' IDENTIFIED WITH mysql_native_password BY 'yourpassword'; GRANT ALL PRIVILEGES ON hanfu_db.* TO 'hanfu'@'%'; FLUSH PRIVILEGES;但不推荐长期这么干,MySQL官方已经明确mysql_native_password在后续版本会被移除。
7.2 时区问题与URL参数
MySQL8.0连接时最经典的一个错误是The server time zone value '???ú???????' is unrecognized or represents more than one time zone。这是因为MySQL服务器时区没有设置,JDBC无法解析。解决方式是JDBC URL带上serverTimezone参数:
spring: datasource: url: jdbc:mysql://localhost:3306/hanfu_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.DriverallowPublicKeyRetrieval=true也是MySQL8.0下很容易踩的坑,不加这个参数,首次连接时如果认证方式是caching_sha2_password,JDBC可能因为无法获取公钥而报错Public Key Retrieval is not allowed。开发环境建议直接加上。
7.3 Docker部署MySQL8.0的细节
不少同学的MySQL是直接用Docker启动的,命令通常长这样:
docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=123456 \ -e TZ=Asia/Shanghai \ -v /opt/mysql/data:/var/lib/mysql \ -v /opt/mysql/conf:/etc/mysql/conf.d \ mysql:8.0这里要提醒的是:TZ=Asia/Shanghai这个环境变量虽然设置了,但MySQL内部的time_zone系统变量不一定被同步设置。建议在容器启动后执行:
docker exec -it mysql8 mysql -uroot -p123456进入MySQL后执行SELECT NOW();,如果返回的时间是UTC而不是北京时间,就要手动设置全局时区:
SET GLOBAL time_zone = '+08:00'; SET time_zone = '+08:00';更彻底的做法是在MySQL配置文件my.cnf的[mysqld]段里写上default-time-zone = '+08:00'。毕设项目里时间字段比较多,时区不一致会导致订单的创建时间、租赁日期全部差8个小时,排查起来非常痛苦。
7.4 导入SQL文件时中文乱码
MySQL8.0默认字符集是utf8mb4,按理说不该乱码,但如果你用命令导入SQL文件,可能会因为SQL文件本身的编码格式与数据库连接编码不一致而出现乱码。解决方案是在导入时显式指定字符集:
mysql -uroot -p123456 --default-character-set=utf8mb4 hanfu_db < hanfu.sql如果是从Navicat里导入的,导入前先看右下角连接属性里的编码设置是否选择了utf8mb4。同时检查建表语句里是否显式指定了DEFAULT CHARSET=utf8mb4,如果建库语句漏了这个,表默认可能是latin1。
另外,application.yml里的连接URL已经带了characterEncoding=utf8,这里的utf8实际上在MySQL里会被理解为utf8mb3,对于绝大多数汉字存储没有任何问题。但为了追求规范的字符集配置,建议在MySQL端的库、表、JDBC URL统一使用utf8mb4。
8. 开发中实际碰到的问题与排查思路
8.1 MyBatis-Plus的Mapper XML路径问题
如果用MyBatis-Plus写复杂SQL,XML文件放在src/main/resources/mapper/目录下,但mapper接口在com.example.hanfu.mapper包中,不配置的话项目启动时会提示Invalid bound statement (not found)。
配置点有两个。第一个是在application.yml里声明mapper XML的位置:
mybatis-plus: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.hanfu.entity第二个是在启动类或配置类上加@MapperScan("com.example.hanfu.mapper"),让Spring能扫描到Mapper接口。这两个配置缺一个都不行,前者找不到XML,后者注册不了Bean。
网上经常有人问"Mapper接口和XML放在同一个文件夹下应该怎么配置",那是指把mapper包也建在src/main/java下,然后配置mapper-locations: classpath:com/example/hanfu/mapper/*.xml。我建议老老实实把XML放resources/mapper,不要搞那些花活,省得构建时被Maven过滤掉。
8.2 跨域请求CORS配置
前端Vite开发服务在http://localhost:5173,后端SpringBoot在http://localhost:8080,两者端口不同必然触发跨域。开发阶段的解决方式是后端配置CORS:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }注意这里用的是allowedOriginPatterns("*")而不是allowedOrigins("*"),因为如果设置了allowCredentials(true),allowedOrigins(" * ")在较新的Spring版本会被拒绝,而allowedOriginPatterns没有这个问题。
如果已经配置了JWT拦截器,还需要注意一个细节:预检请求(OPTIONS方法)不应被拦截。拦截器里要加判断:
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; }否则前端浏览器发起的预检请求过不了拦截器,实际业务请求发不出去,表现就是前端报CORS错误但后端日志里根本没有业务日志,排查半天找不到原因。
8.3 日期参数传递的格式问题
前端传递客户选择的租赁日期,通常是2024-05-01这种字符串,后端如果用Date类型接收,需要配置全局日期格式。推荐的做法是前端直接传字符串或LocalDate,后端实体字段用LocalDate接收,配合@DateTimeFormat(pattern = "yyyy-MM-dd"):
public Result<?> createOrder(@RequestBody OrderCreateDTO dto) { // dto中的 startDate 类型为 LocalDate,可直接使用 }LocalDate和LocalDateTime在Jackson反序列化时的处理与Date不同,需要依赖jackson-datatype-jsr310,SpringBoot2默认引入了该依赖,无需额外配置。但如果前端传到后端的是时间戳或带时区的ISO字符串,就要额外写@JsonFormat注解指定格式。
8.4 数据库字段名与Java属性的映射
MySQL的字段习惯是下划线风格create_time,Java属性是驼峰createTime。MyBatis-Plus默认开启了下划线转驼峰映射,理论上不需要额外配置。但如果踩到createTime查询出来是null,先看实体类有没有加@TableField("create_time"),再检查application.yml里是否不小心写错:
mybatis-plus: configuration: map-underscore-to-camel-case: true这个值默认就是true,真正的坑往往出在实体类字段用了Integer或BigDecimal等包装类型却初始化为null,或者数据库字段和实体类字段对不上但没报错只显示null,排查思路要放到字段名对不上这个方向。
9. 验收前最后一轮自查:让项目能经得住答辩的几个细节
9.1 演示数据要"厚"
毕设项目最尴尬的一件事是演示时数据库里只有两条测试数据,老师一点"下一页"就到头了。我做这个项目时,前期就往每张表里灌了足够的演示数据:服装表至少20条,覆盖齐胸襦裙、齐腰襦裙、圆领袍、褙子、马面裙、斗篷等热门汉服品类,尺码覆盖S到XL,价格从每天20元到120元不等。订单表也造了十几单,状态覆盖待支付、已支付、租赁中、待归还、已完成、已取消六种情况。这样演示时不管点哪个列表页,展示效果都是满的。
9.2 权限控制要能现场演示
答辩老师可能会做两件事:第一,用普通用户登录后直接输入后台管理地址,看看能不能访问;第二,直接调用后台接口,看看能不能拿到数据。针对这两点,我的处理方式是:路由守卫拦截页面访问,同时后端接口层用拦截器校验/api/admin/**下的所有接口必须具有管理员角色。这样即使前端绕过了路由,后端也会拒绝返回数据。
后端权限校验可以做一个简单的@RequireAdmin注解配合AOP实现,也可以在拦截器里根据URL前缀做判断。我建议做成注解的方式,这样后续如果有新加的后台接口,只需要在方法上加注解即可:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequireAdmin { }AOP切面里从RequestContext取当前用户,判断role是否等于1,不等于就抛出无权限异常。
9.3 接口文档必须能实时生成
"含文档"这个卖点在验收时必须体现出来。我给项目集成了knife4j,它是Swagger的增强UI,效果比原生Swagger好看很多,展示接口列表、参数说明、在线调试都方便。SpringBoot2集成knife4j的依赖是:
<dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-openapi2-spring-boot-starter</artifactId> <version>4.4.0</version> </dependency>然后写一个Knife4jConfig配置类,定义文档标题为"高校汉服租赁系统接口文档",版本号写上1.0.0。启动项目后访问http://localhost:8080/doc.html,就能看到所有接口。这个细节在答辩时非常加分,因为老师只要打开这个页面就能看到项目的全部接口和参数定义,不需要去翻代码。
9.4 优化答辩时的演示路径
我建议准备两条演示路径。第一条是普通用户路径:注册账号 -> 登录 -> 浏览汉服 -> 筛选 -> 查看详情 -> 加入购物车 -> 提交订单 -> 模拟支付 -> 查看我的订单。第二条是管理员路径:管理员登录 -> 后台首页看仪表盘 -> 查看订单列表 -> 找到待归还订单 -> 确认归还 -> 去服装管理修改一条服装信息 -> 去用户管理禁用一个用户。这两条路径走完,项目的核心功能几乎全覆盖,不会被追问"购物车呢""订单管理呢"这种尴尬问题。
我记得我第一次演示这个项目时,最紧张的不是技术点讲不明白,而是演示过程中突然发现某个列表页加载不出图片。后来排查是图片上传后存在本地目录,但前端访问时路径拼接少了一层/upload前缀。这类细节问题在答辩前一定要自己反复走几遍完整流程,尤其是图片、日期、金额这种容易出问题的地方。
10. 整体项目落地过程中最值得记住的三个经验
第一,开发顺序上,先做数据库设计,再定接口,最后写页面。我见过太多项目先写页面再改表结构,结果一张订单表的字段改了五六次,前端页面跟着返工。数据库表结构一旦定下来,就是整个项目的"地基",后续任何模块的扩展都要在这个地基上盖楼。建议花至少半天到一天时间,把表结构、字段注释、关联关系全部评审一遍再动手写代码。
第二,不要追求"全项目都用MyBatis-Plus"。MyBatis-Plus确实方便,但遇到多表关联的统计查询,比如"按月统计租赁收入",还是写原生SQL更清晰。把这个经验刻在脑子里:MyBatis-Plus负责80%的简单CRUD,XML负责20%的复杂统计,这个小组合能覆盖几乎所有毕设需求。
第三,日志要留在关键节点。订单创建、库存扣减、归还确认这些操作,一定要在Service层加上log.info日志,记录关键参数和操作结果。这样一旦线上出问题,排查比一脸懵地看前端报错高效得多。我一路开发下来,有几个Bug就是靠日志快速定位的,比如有一次订单状态一直卡在"待支付",就是因为回调方法里参数没传到,日志一打出来立刻发现是前端传参少了一个字段。
这个技术栈的组合在毕业设计市场已经很成熟了,SpringBoot2负责稳定兜底,Vue3保证前端体验不落伍,MyBatis-Plus把开发效率拉满,MySQL8.0解决中文与事务问题。把这套流程完整走一遍,你得到的不仅是一个能通过答辩的项目,更是对租赁类业务系统的深度理解,以后再接到类似的校园物品租赁、图书借阅、场地预约项目,核心业务逻辑都能直接迁移复用。