做Java Web毕设,最怕的不是代码写不出来,而是题目选得太大,最后东拼西凑自己都讲不清楚。我最后定的题目是“SpringBoot+Vue高校汉服租赁网站平台”,前后端分离、带完整SQL脚本和接口文档,从选题、建表、写接口到前端页面,陆续做了四个多星期。这篇文章把我整个做项目的思路、数据库设计、核心模块和常见的坑都梳理一遍,适合正在选毕业设计题目、或者想快速跑通一个前后端分离项目的学生参考。
1. 项目从选题到落地的整体思路拆解
1.1 为什么选“汉服租赁”这个业务场景
毕设选题最重要的标准是“业务复杂度刚好”。选纯电商卖衣服,流程太常见,评委看一眼就猜到是CRUD;选二手交易平台,又要涉及支付、担保交易、聊天沟通,复杂度很容易失控。汉服租赁恰好卡在中间。
汉服这个品类的特点是单价高、使用频率低,学生想参加校园活动、拍写真但购买成本太高,租赁需求天然存在。更重要的是,租赁业务的规则比普通买卖多一层:
- 租期按天计算,押金在下单时就要收取;
- 归还时要检查汉服是否损坏,损坏要扣一定费用;
- 逾期归还要另外计算逾期费用;
- 同一件汉服在规定日期内不能被重复预订。
这样一来,系统建模就比单纯的“商品+订单”多了不少需要设计的地方。库存不再是一个单纯的数字,还要考虑时间段占用;订单金额也不再是简单的单价乘数量,而是租金加押金再减去各种扣款的组合。这些业务规则写进论文和答辩PPT里都有内容可讲,评委也不会觉得项目太单薄。
1.2 技术栈选型:SpringBoot+Vue的前后端分离方案
最开始我也纠结过要不要用传统的JSP+SSM,主要是网上教程多、学校以前的项目也大多是这种。但仔细算账后发现,SSM的配置成本太高,SpringMVC里光spring-web.xml、spring-service.xml、mybatis-config.xml就要写一大堆,出问题排查半天。SpringBoot把自动配置和内嵌Tomcat都做好了,一个Application类就能跑起来,省下的时间可以全部放在业务逻辑上。
前端我用的是Vue2 + Element UI,后面如果你时间比较充裕,也可以换成Vue3 + Element Plus,业务逻辑基本一样,只是个别组件用法有差异。之所以不继续用服务端渲染的JSP,是因为前后端分离之后优势很明显:
- 后端只提供JSON接口,不关心页面长什么样;
- 前端可以单独调试,配合Vue CLI的代理转发很方便;
- 部署时后端打一个jar包,前端build出静态文件,结构非常清晰。
数据库用MySQL 5.7或8.0,ORM选MyBatis-Plus而不是原生MyBatis。MyBatis-Plus对单表CRUD提供了现成方法,像分页、条件查询基本不用手写SQL,能把精力省下来放在租赁业务本身。这组技术栈在Java Web毕设里非常主流,遇到问题几乎都能搜到现成的解决办法。
1.3 功能模块全景图
整个系统只分两大类角色:管理员和普通用户(学生)。
用户端:
- 注册、登录、个人信息维护;
- 浏览汉服列表,按朝代形制、价格区间筛选,支持关键词搜索;
- 汉服详情页:多图展示、尺码库存、押金和日租金、用户评价;
- 购物车:加购、修改数量、删除;
- 下单租赁:选择租期、计算总费用、模拟支付;
- 订单中心:待支付、待取货、租赁中、已归还、已取消;
- 归还后可以填写评价。
管理员端:
- 汉服分类管理、汉服信息维护,包括封面图、多图、库存、价格;
- 订单管理:查看所有订单、标记取货、确认归还、处理逾期;
- 用户管理:禁用或启用账号;
- 公告管理:发布站内公告;
- 首页统计:总用户数、总订单额、各分类租赁占比。
模块划分清楚之后,数据库设计和接口设计都跟着这套结构走,后边写代码基本就是按图索骥,不会出现一边写一边改功能的混乱局面。
2. 从零开始的数据库设计
2.1 核心表结构与业务关系
数据库是毕设项目的根基,库表设计得好,后面接口写起来会非常顺。我设计的表结构并不复杂,核心就是把商品、订单、归还、评价这几条线拆清楚。
用户表(user)
- id、username、password、nickname、phone、avatar、role、status、create_time、update_time
汉服分类表(hanfu_category)
- id、name、sort、status
汉服表(hanfu)
- id、category_id、name、dynasty、style、size、color、description、daily_price、deposit、stock、cover_image、images、status、deleted、create_time
购物车表(cart)
- id、user_id、hanfu_id、count、create_time
订单表(orders)
- id、order_no、user_id、total_price、deposit、status、rent_start_date、rent_end_date、rental_days、create_time、pay_time、fetch_time、return_time、cancel_reason
订单明细表(order_item)
- id、order_id、hanfu_id、hanfu_name、daily_price、deposit、count、rental_days
归还记录表(return_record)
- id、order_id、return_time、is_damaged、damage_desc、damage_fee、overdue_days、overdue_fee、refund_amount、operator
评价表(review)
- id、user_id、order_id、hanfu_id、content、score、create_time
公告表(notice)
- id、title、content、create_time、update_time
用户和订单是一对多,订单和订单明细是一对多,因为一个订单可能同时租多件汉服。归还记录和订单一对一,评价和订单也是一对一,避免同一个订单被重复评价。
2.2 SQL脚本中的关键设计细节
我在SQL脚本里做了几个比较重要的决定,写下来供参考。
第一,所有表都用utf8mb4字符集。汉服详情和评价里可能出现生僻汉字或特殊符号,utf8mb4才能完整存下,直接用默认latin1或utf8很容易在导入数据时报错或出现乱码。
第二,金额字段全部用DECIMAL(10,2),不要用FLOAT或DOUBLE。租金、押金、逾期费进数据库后要参与加减运算,浮点数精度问题在金额场景下非常致命。展示的时候注意保留两位小数就够了。
第三,逻辑删除字段deleted和通用时间字段create_time、update_time在业务表里都留一份。逻辑删除的好处是数据不会真丢,管理员误删了汉服还能恢复;时间字段用MyBatis-Plus的自动填充,插入和更新时不用手工维护。
第四,订单号order_no要加唯一索引。订单号格式是“yyyyMMddHHmmss + 4位随机数”,虽然撞号概率极低,但唯一索引是数据库层面的最后一道防线,万一重复就直接抛业务异常重新生成。
第五,初始化数据里放一个管理员账号。密码必须存BCrypt哈希,不能存明文。BCrypt算法对同一个密码每次生成的结果都不同,所以管理员初始密码写死在SQL里时,要先用代码生成一次哈希再塞进去,别以为写个明文也能登录成功。
2.3 数据初始化与测试数据准备
这个部分很多同学会忽略。项目能跑起来和演示效果好是两码事,演示的时候首页只有5条汉服、订单列表空荡荡的,评委几分钟就能看出准备不充分。我后来专门花了一个下午补测试数据。
- 分类至少5个:唐制、宋制、明制、晋制、现代改良;
- 汉服至少30条,每个分类下6条左右,覆盖不同价格档位,封面图统一处理成jpg或webp;
- 用户至少10个,密码统一用同一个哈希值,方便演示时切换账号;
- 订单至少50条,状态要覆盖待支付、租赁中、已归还、已逾期等各个阶段,时间分布在不同月份,这样统计图才有内容;
- 评价每条汉服2到3条,内容尽量贴近真实体验。
测试数据可以直接在SQL脚本里INSERT,也可以在后台一条条录入。我建议直接写INSERT语句放进SQL脚本,因为这样任何导入这个库的人都能马上看到完整效果,也方便答辩前重置数据。
3. 后端核心模块实现
3.1 用户认证与权限控制
用户认证我用的方案是JWT加拦截器,没有引入完整的Spring Security。对毕设来说Spring Security配置繁琐,各种过滤器顺序很容易把人绕晕,与其把精力花在框架配置上,不如把业务逻辑做得更扎实。
登录接口流程是这样的:
- 用户提交username和password;
- 查询用户,用BCrypt比对密码;
- 通过后生成一个JWT,里面放userId、username、role,有效期设成7天;
- 前端每次请求都在请求头的Authorization字段带上token。
拦截器里要做两件事:从请求头取出token并解析,解析失败直接返回401;解析成功后把userId和role放进ThreadLocal,后面的Controller和Service随时能拿到当前登录用户。
这里有个坑:线程池和异步处理会丢失ThreadLocal数据。我的项目中归还记录和逾期检查用的定时任务是自己查询用户信息的,不走ThreadLocal,所以没有踩雷。如果你在业务线程里需要用户信息,最好在进入线程之前就把它取出来传参,不要等进入线程内部再读ThreadLocal。
权限控制就是角色判断。管理员接口上加一个自定义注解,用拦截器检查当前用户的role是否为admin,权限不够直接拒绝。因为是毕设项目,这个简单方案完全够用,也容易跟评委解释。
3.2 租赁订单流转的核心逻辑
订单流转是项目中业务最重的部分,也是最容易写乱的地方。我的做法是先定一个状态机,再按状态写接口,严格禁止订单状态随意跳变。
订单状态定义:
- 0 待支付
- 1 已支付,待取货
- 2 租赁中,已取货
- 3 已归还,押金已处理
- 4 已取消
- 5 已逾期
核心流程拆开来讲:
下单接口:
- 用户从购物车选择商品,提交租期start_date和end_date;
- 后端校验:租期不能早于今天、结束日期必须大于开始日期;
- 计算每件汉服的租赁天数,结束日减开始日,按自然日计算;
- 租金等于日租金乘以租赁天数;
- 检查每一件汉服在这个时间段内是否被占用,查询该汉服所有“已支付、租赁中”状态的订单,判断是否存在租期重叠;
- 库存充足则扣减库存,生成订单和订单明细,状态为待支付。
有一点特别重要:库存扣减和订单生成必须在同一个事务里执行。我用@Transactional包住整个下单逻辑,任何一步失败都会回滚,不然会出现订单生成了但库存没减的脏数据。
支付接口是模拟实现:
- 订单状态必须为待支付才能调支付;
- 支付成功后状态置为已支付,记录支付时间。
取货接口由管理员操作:
- 状态为已支付时才能取货;
- 取货后状态置为租赁中,记录取货时间。
归还接口也由管理员操作:
- 状态为租赁中才能归还;
- 先算实际归还时间是否超过租期结束日,超出天数按逾期费单价计算;
- 检查汉服是否有损坏,有损坏则记录损坏费;
- 应退押金等于原押金减去损坏费再减去逾期费,不够扣就提示客户补缴;
- 生成归还记录,订单状态置为已归还。
这些校验逻辑如果漏掉任何一个,就会出现“待支付订单直接变成已归还”“逾期没人管”这种答辩时一追问就露馅的问题。建议把状态校验抽成一个独立方法,每次状态变更前先做一次断言。
3.3 库存、并发与数据一致性
答辩老师特别喜欢问并发问题:两个用户同时租同一件汉服怎么办?我当时做了两层处理。
第一层,下单时用乐观锁的思路。扣减库存的update语句可以写成:
update hanfu set stock = stock - 1 where id = #{id} and stock > 0通过受影响行数判断是否扣减成功,如果行数为0说明库存不足,直接抛业务异常。这样即使并发请求同时进来,数据库在行锁层面也会把这个update串行化,不会出现库存扣成负数的问题。
第二层,订单租期重叠的校验不能只靠应用层的for循环去查,就算查到了也可能被并发窗口绕过。我在hanfu表上额外维护了一个不可租状态字段,当某个时段被订单占用时直接标记,下单时先检查这个字段。不过毕设场景并发量很小,第一层的update语句加上事务,实际已经能挡住绝大多数问题。
还有一处数据一致性容易忽略:库存的初始值。我被自己坑过一次,导入SQL脚本后汉服表库存是0,前端页面显示有商品,但一点“立即租赁”就提示库存不足。后来我在初始化脚本里把所有汉服库存统一写成5到10,并把下单扣库存的逻辑写得足够直观,才算解决了演示时的尴尬。
3.4 统一返回体与全局异常处理
前后端接口约好统一的返回结构非常重要,否则前端每个接口都要单独处理错误分支。我的Result对象很简单:
{ "code": 200, "message": "操作成功", "data": {} }code固定几个值:200成功、400参数错误、401未登录、500系统异常。业务异常统一抛BizException,全局异常处理器捕获后返回对应的code和message,前端在响应拦截器里拿到非200就把message弹出来给用户看。
参数校验我用了@Validated,配合javax.validation的@NotNull、@NotBlank、@Min注解,省去大量手写if判断。Controller层的代码看起来干净很多,评委翻源码的时候印象也会更好。全局异常处理器里还要单独处理MethodArgumentNotValidException,把具体的校验失败信息返回给前端,不然用户看到的永远是笼统的“系统异常”,体验会差很多。
4. 前端Vue实现与页面交互
4.1 前端项目搭建与目录结构
我用vue-cli创建的前端项目,目录结构分成几块:
- src/api:每个模块的接口请求函数
- src/router:路由表和路由守卫
- src/store:用户状态、购物车状态
- src/views:页面组件
- src/components:公共组件,比如验证码、图片上传、分页
- src/utils:axios封装、日期工具、金额格式化
axios封装是最先要做的事情。请求拦截器里把token从localStorage取出来放到请求头,响应拦截器里判断result.code,不是200就调用Element UI的Message提示,碰到401就清掉本地token并跳转登录页。这样业务组件里只需要写this.$axios.get('/api/hanfu/list')然后直接拿data使用,不需要每个页面重复处理错误逻辑。
路由守卫用的beforeEach:进入需要登录的页面时判断store里有没有用户信息,没有就跳登录页并带上redirect参数,登录成功后跳回原页面。这个小交互做出来很加分,评委演示时能直观看到登录态控制的效果。
4.2 核心页面拆解
首页是门面,我把它分成顶部导航、轮播图、分类tab、汉服卡片网格、底部公告几块。顶部导航放搜索框和购物车入口,购物车角标数量用Vuex里的状态驱动,登录后从后端接口拉取购物车数量。
汉服列表页支持分类筛选和价格排序,我用的是Element UI的Tabs组件和下拉选择器,筛选条件变化后调后端分页查询接口。需要注意后端分页参数pageNum和pageSize要和Element UI的Pagination组件对应好,页码从1开始,不要混用。
详情页信息量比较大:左侧多图轮播,右侧显示名称、朝代形制、日租金、押金、尺码、库存和可租日期,下方是评价列表。用户选择开始日期和结束日期后,前端把两个日期传给后端,后端返回该时间段的总费用,点击“立即租赁”或“加入购物车”调用对应接口。
订单确认页要展示每个商品明细、租金小计、押金合计,并且明确标明“押金在归还后扣除相关费用原路退回”。这是租赁场景和普通电商最大的区别,这块业务表达清楚,整个项目的专业度就体现出来了。
后台管理端我用的布局是左侧菜单加右侧内容区。汉服管理页面核心是表格加弹窗表单,表格列包括封面缩略图、名称、分类、价格、库存、状态;表单里图片上传组件把文件传到后端,返回图片URL再回填到表单。
4.3 与后端联调的关键点
前后端联调最容易出问题的地方在跨域和字段格式上。
跨域问题:开发环境下最简单的方式是vue.config.js里配置devServer.proxy,把/api前缀的请求全部代理到后端地址。线上部署时让nginx代理后端接口,或者后端直接加CorsFilter允许跨域。我写了一个CorsConfig配置类,开发环境和线上都能跑,省心很多。
日期格式:后端返回的LocalDate默认格式是yyyy-MM-dd,但LocalDateTime默认带T,比如2025-01-01T10:20:30,前端展示肯定不想要这个T。统一在application.yml里配置spring.jackson.date-format,或者对时间字段单独加@JsonFormat注解。
金额格式:后端返回的是BigDecimal,前端直接用就行,但计算合计时要小心浮点精度。我都是先把字符串转成Number再算,最后用toFixed(2)保留两位小数显示。购物车里几条数据加起来金额不对,多半就是浮点精度在捣乱。
图片上传:上传接口返回的URL要区分绝对路径和相对路径。我把图片统一放到了后端的static/upload目录下,返回的URL直接是相对路径,前端拼接自己的请求地址访问。注意Windows和Linux的路径分隔符不同,拼接文件路径时要处理一下,不然部署到服务器上图片全挂。
5. 接口文档编写与自测
5.1 一份能用于毕设的接口文档应该包含什么
接口文档不只是给答辩看的,它同时也是开发期前后端沟通的契约。这份文档最后交付时包括下面几块内容:
- 项目概述和环境约定:BaseURL、数据格式、鉴权方式;
- 通用错误码表;
- 用户模块:注册、登录、获取个人信息、修改个人资料;
- 汉服模块:分页查询、详情、分类列表、搜索;
- 购物车模块:列表、加入、修改数量、删除;
- 订单模块:创建订单、支付、取消、详情、我的订单列表、管理员订单列表、取货、归还;
- 归还模块:归还记录查询、押金退回明细;
- 公告和评价模块。
每个接口的格式统一为:请求URL、请求方式、请求参数表格、成功返回JSON示例、可能的错误码。参数表格里要标注是否必填、类型、长度限制,返回示例里别省略字段,把真实数据贴进去。比如汉服详情接口,返回示例就写成完整的一条记录,包含id、名称、分类、价格、库存、图片URL、描述等字段,这样评阅人一眼就能看懂接口的用途,不需要再翻代码。
除了手写的文档,我还在项目里集成了Knife4j自动生成在线接口文档。SpringBoot加一个依赖,写几个注解就能在浏览器里打开接口调试页面。手写文档用作纸质交付,Knife4j在线文档用于答辩现场演示,两边都覆盖,算是比较稳妥的做法。
5.2 用Apifox从零自测一套订单流程
自测是整个开发过程中最容易被拖延的环节。我的习惯是每写完一个模块就跑一遍全套接口,而不是拖到最后一次性联调,几十个接口堆在一起出问题时根本定位不了。
推荐用Apifox或Postman建一个环境变量,把token存成变量,登录接口返回的token自动写入变量,后续所有需要登录的接口直接引用。这样测试一套流程可以连续跑下来,不用每次重新登录。
完整流程建议按业务顺序走:
- 注册新用户;
- 登录拿token;
- 查询汉服列表,选一个有库存的商品;
- 加入购物车;
- 创建订单,日期选未来三天;
- 支付订单;
- 管理员登录,查询待取货订单;
- 管理员执行取货;
- 管理员执行归还,分别测无损坏和损坏两种情况;
- 查看应退押金金额和归还记录。
跑这套流程的同时,故意把日期改成已经过期的,验证逾期费计算对不对;再改成和已有订单重叠的日期,看库存占用能不能被拦住。异常路径测试完,项目才算真正稳了。纸上写十个接口文档都不如实测一遍暴露的问题多,这是我当时最深的感受。
6. 部署运行与避坑实录
6.1 本地从零跑通的完整步骤
拿到这套源码后,最快跑通的步骤如下:
- 用Navicat或命令行执行SQL脚本,创建数据库和表,注意SQL脚本文件头部会指定数据库名,跑之前先确认有创建数据库的权限;
- 用IDEA打开后端项目,等待Maven依赖下载完成;
- 修改application.yml里的数据库地址、账号、密码,时区配置成serverTimezone=Asia/Shanghai,否则连MySQL 8会报时区错误;
- 启动后端Application类,看到“Started Application in x seconds”就说明成功了;
- 进入前端目录,先npm install,再npm run serve,浏览器打开前端地址;
- 管理员账号和普通用户账号在SQL脚本里都有说明,初始密码统一。
后端默认端口配的8080,前端开发服务器端口配的8081,这样前后端不会冲突。如果不小心撞了端口,改一下配置文件就能解决。
6.2 常见坑与避坑经验
我踩过且印象比较深的几个坑,汇总一下:
- MySQL 8.0的驱动名为com.mysql.cj.jdbc.Driver,MySQL 5.7用com.mysql.jdbc.Driver,版本别混用,混了启动直接报ClassNotFoundException;
- 导入SQL脚本时如果报“unknown command”或者中文乱码,检查文件编码是不是UTF-8,以及连接数据库时是否指定了useUnicode=true和characterEncoding=utf8;
- 前端npm install慢,可以用国内镜像源,但换源后可能出现依赖版本缓存问题,遇到奇怪报错先清理npm缓存;
- 后端图片URL在浏览器打不开,多半是静态资源映射没配置,SpringBoot里要写一个WebMvcConfigurer,手动addResourceHandlers;
- Vue的axios请求如果baseURL配置错误会出现404,建议先单独测试登录接口通不通,再写页面;
- 定时任务扫描逾期订单时依赖服务器时间,答辩演示如果临时改了服务器时间,扫描结果可能不符合预期,最好做一个小接口手动触发扫描,演示更可控。
6.3 答辩与演示的几个实用心得
最后说点实际操作中的体会。答辩时演示环节比PPT更关键,宁可少讲优点,也要把一条完整用户流程顺下来。我当时准备了两套数据:一套是模拟数据,用于正常演示;一套是空库直接导入SQL,用于展示“从零到跑通”的能力。
常见问题和答复思路我也准备了几个:问为什么前后端分离,就从维护性和部署便利性讲;问订单状态如何处理,就画状态机讲清楚每一跳的触发条件;问并发怎么防超卖,就讲库存扣减的update条件语句。这几个问题答好了,整个项目的可信度就上来了。
如果你也想做这个题目,或者正在做类似的前后端分离毕设,我建议先把数据库和接口文档定下来再动手写代码,磨刀不误砍柴工。中途改表结构是最痛苦的,不仅后端代码要跟着改,前端字段也要一起调,那一晚上我是记忆犹新。这套项目源码和SQL脚本现在整理得很完整,按照上面步骤跑一遍,基本半天内就能看到完整效果。