接手这个项目的时候,我的第一反应是“这不就是套了个 Spring Boot + Vue 的皮,做点增删改查吗”。但真正动手把租房流程从前端页面一路打通到后端接口、再到数据库表设计之后,才发现店铺租赁这件事比普通商品交易麻烦得多。合同周期、押金结算、铺位状态流转、角色权限……每一项都藏着不少细节。这篇文章我就把这个毕设项目的完整实现思路、核心模块拆解、关键代码逻辑和一些平时文档里不会写的坑,一次性讲清楚。
这个项目能做什么?简单说,它是一个面向“店铺租赁”场景的管理平台,分为前台用户端和后台管理端。用户端可以浏览在租铺位、查看铺位详情、提交租赁申请、在线签约、缴纳租金;管理端则负责铺位信息维护、租赁合同审批、账单管理、数据统计等。技术栈就是标题里写的 Spring Boot + Vue,数据库用的 MySQL,ORM 用的是 MyBatis-Plus。
适合谁看?如果你正在做 Java 毕设,选题涉及“租赁类平台”,或者想在简历上写一个完整前后端分离项目,这篇内容应该能帮你省掉不少弯路。我会把业务模型怎么设计、权限怎么控制、合同状态怎么流转、前端权限路由怎么配,全部按实际开发顺序讲清楚。
1. 项目整体设计与思路拆解
1.1 为什么选 Spring Boot + Vue 这套组合
先说技术选型。Spring Boot + Vue 在目前毕设市场里几乎是“标准答案”,原因很实在:
- Spring Boot 自带 Tomcat、自动装配、Starter 机制,不用像 SSH 时代那样写一堆 XML 配置,对新手极度友好。尤其是
spring-boot-starter-web和spring-boot-starter-validation这两个起步依赖,能省掉大量基础配置时间。 - Vue 采用组件化开发,配合 Element UI 做后台管理界面,开发效率比传统 JSP + JQuery 高出一大截。而且 Vue 的响应式数据绑定天然适合表单交互密集的管理系统。
- 前后端完全分离,前端打包成静态资源后,后端只提供 RESTful API,这种架构在简历上写出来也更有说服力。
这个选择的核心逻辑是:毕设项目的分数高低,不在于用了多花哨的技术,而在于你能不能自圆其说,把每一个设计决策背后的理由讲清楚。Spring Boot + Vue 的资料多、社区活跃、报错能搜到答案,这是它作为毕设技术栈最大的隐性优势。
1.2 店铺租赁的业务模型设计
店铺租赁和普通商品交易最本质的区别在于:交易的不是所有权,而是使用权。这意味着系统需要额外管理合同时间轴、押金流转、铺位状态等多个维度。
我最终设计的核心业务模型是这样的:
- 租户(User):分为普通用户和管理员两类。普通用户可以提交租赁申请,管理员负责审核。
- 铺位(Shop):包含铺位编号、所在楼层、面积、月租金、押金比例、当前状态(待租、已租、维护中)等字段。
- 租赁申请(LeaseApply):用户在选定铺位后提交的申请记录,包含期望租赁周期、用途说明等。
- 合同(Contract):申请审核通过后生成的正式合同,包含起止日期、租期、金额明细、双方信息。
- 账单(Bill):按合同周期生成租金账单,支持在线支付或线下转账确认。
- 操作日志(OperateLog):记录所有关键操作,保证数据可追溯。
这里有一个值得展开的点:为什么把“租赁申请”和“合同”拆成两张表?因为从业务逻辑上说,申请是用户单方面的意向表达,合同是双方确认后的法律文件。用户提交申请、管理员审批通过、再生成合同,这是一个完整的审批流。如果把它合在一张表里,状态字段会变得非常混乱,比如“申请被驳回后重新提交”和“合同已经生效”这两个状态就很难优雅共存。
1.3 核心流程的状态机设计
店铺租赁平台最核心的一条主流程是这样的:
用户浏览铺位 -> 提交租赁申请 -> 管理员审核 -> 审核通过生成合同 -> 用户确认签约 -> 生成账单 -> 缴纳押金和首期租金 -> 合同生效 -> 到期退租退押金这个流程里每一步都涉及状态变更。我用的方式是定义一组明确的整型常量,而不是到处写魔法字符串:
public class ContractStatus { public static final int PENDING_SIGN = 0; // 待签约 public static final int PENDING_PAY = 1; // 待付款 public static final int ACTIVE = 2; // 生效中 public static final int EXPIRED = 3; // 已到期 public static final int TERMINATED = 4; // 已终止 }这个做法的好处是,在代码里你永远不需要猜测一个字符串"2"到底代表什么状态。而且 MyBatis-Plus 默认会把整型字段直接映射到数据库的TINYINT或INT,查询效率也更高。
2. 后端核心模块与数据库设计
2.1 数据库表结构设计
数据库一共设计了 8 张表,这里挑最重要的几张讲:
用户表(sys_user)
CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', username VARCHAR(50) UNIQUE NOT NULL COMMENT '登录用户名', password VARCHAR(100) NOT NULL COMMENT 'BCrypt加密后的密码', real_name VARCHAR(50) COMMENT '真实姓名', phone VARCHAR(20) COMMENT '手机号', role TINYINT DEFAULT 1 COMMENT '角色:0-管理员,1-普通用户', status TINYINT DEFAULT 1 COMMENT '状态:1-正常,0-禁用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ) COMMENT '系统用户表';铺位表(shop)
CREATE TABLE shop ( id BIGINT PRIMARY KEY AUTO_INCREMENT, shop_no VARCHAR(30) UNIQUE NOT NULL COMMENT '铺位编号', name VARCHAR(100) NOT NULL COMMENT '铺位名称', floor_no INT COMMENT '所在楼层', area DECIMAL(10, 2) COMMENT '面积(平方米)', rent_price DECIMAL(10, 2) NOT NULL COMMENT '月租金', deposit_months INT DEFAULT 2 COMMENT '押金月数', status TINYINT DEFAULT 0 COMMENT '状态:0-待租,1-已租,2-维护中', description TEXT COMMENT '铺位描述', image_url VARCHAR(255) COMMENT '铺位图片', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT '铺位表';这里需要注意一个细节:rent_price用的是DECIMAL(10, 2)而不是FLOAT或DOUBLE。原因很直接——涉及钱的字段,浮点数会有精度丢失。比如0.1 + 0.2在浮点数里不等于0.3,这在租金累计计算里会出大问题。
合同表(contract)
CREATE TABLE contract ( id BIGINT PRIMARY KEY AUTO_INCREMENT, contract_no VARCHAR(50) UNIQUE NOT NULL COMMENT '合同编号', user_id BIGINT NOT NULL COMMENT '租户ID', shop_id BIGINT NOT NULL COMMENT '铺位ID', start_date DATE NOT NULL COMMENT '开始日期', end_date DATE NOT NULL COMMENT '结束日期', monthly_rent DECIMAL(10, 2) NOT NULL COMMENT '月租金', deposit_amount DECIMAL(10, 2) NOT NULL COMMENT '押金金额', status TINYINT DEFAULT 0 COMMENT '状态:0-待签约,1-待付款,2-生效中,3-已到期,4-已终止', sign_time DATETIME COMMENT '签约时间', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT '租赁合同表';2.2 MyBatis-Plus 的使用技巧
MyBatis-Plus 对毕设项目来说是一个效率神器。它内置了BaseMapper,提供selectById、insert、updateById等基础 CRUD 方法,不需要你手写任何简单的 SQL 映射。
但它的真正威力在QueryWrapper和LambdaQueryWrapper上。比如查询“某个时间段内到期、且状态为生效中”的合同:
LambdaQueryWrapper<Contract> wrapper = Wrappers.lambdaQuery(); wrapper.eq(Contract::getStatus, ContractStatus.ACTIVE) .between(Contract::getEndDate, today, targetDate); List<Contract> contracts = contractMapper.selectList(wrapper);用 Lambda 方式写QueryWrapper的另一个好处是编译期就能检查字段名,不会出现字段名拼写错误导致运行时 SQL 报错的情况。
还有一个小技巧很多人不知道:在application.yml里开启 MyBatis-Plus 的日志输出,开发调试时会方便很多:
mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这样控制台会打印完整 SQL 语句,排查查询逻辑问题非常有用。
2.3 接口设计与统一返回格式
前后端分离项目的接口设计,最忌讳的是每个接口返回的数据结构都不一样。我封装了一个统一的返回对象:
@Data public class Result<T> { private Integer code; // 200成功,其他为失败 private String message; // 提示信息 private T data; // 业务数据 public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }这样前端在 axios 拦截器里就可以统一处理:
service.interceptors.response.use( response => { const res = response.data; if (res.code !== 200) { Message.error(res.message || '系统异常'); return Promise.reject(new Error(res.message)); } return res.data; }, error => { Message.error('网络请求异常'); return Promise.reject(error); } );前端所有接口拿到res的时候就已经是后端返回的data了,不需要每处都做判空和错误处理,代码会干净很多。
3. 权限控制与安全设计
3.1 JWT 登录认证
毕设项目用 Session 做登录是一个容易走的老路,但分页和跨域配置会变得很别扭。我用的是 JWT(JSON Web Token),流程如下:
- 用户提交用户名密码,后端校验成功后生成一个 token。
- 这个 token 里包含用户 ID 和角色信息,通过 JWT 签名算法保证内容不能被篡改。
- 前端把 token 存在 localStorage 里,每次请求在 axios 拦截器中放入
Authorization请求头。 - 后端用拦截器或 Spring Security 过滤器校验 token,解析出用户信息后放行。
这里我要单独说一句:虽然有现成的 Spring Security 框架,但对毕设项目来说,手写一个拦截器反而更可控。Spring Security 的过滤器链配置起来复杂,一旦出错报错信息又极其晦涩。用一个自定义 HandlerInterceptor 加 JwtUtil 工具类,二三十行代码就能实现同样的效果,而且整个链路你心里门儿清。
核心代码大致长这样:
public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { token = token.substring(7); } // 校验token,失败抛出未登录异常 Claims claims = JwtUtil.parseToken(token); // 将用户信息放入request,供后续controller使用 request.setAttribute("userId", claims.get("userId")); return true; } }3.2 BCrypt 密码加密
很多毕设项目的管理端密码都是明文存的,这放到简历上被问起来会很尴尬。我用了spring-security-crypto里的 BCrypt 加密,而不是简单的 MD5。为啥?
MD5 是哈希算法,撞库攻击下很容易被彩虹表破解。BCrypt 自带随机盐值,每次加密结果不一样,同样的密码存到数据库里都是不同的字符串。即使数据库被拖走,攻击者也没办法轻松反推出原始密码。
String encodedPassword = BCrypt.hashpw(rawPassword, BCrypt.gensalt()); // 校验时 boolean matched = BCrypt.checkpw(rawPassword, encodedPassword);3.3 前端路由权限控制
前端这一层,我用的方案是 Vue Router 的beforeEach导航守卫。路由表先只挂载公共页面,本地存储中拿到当前用户角色,然后根据角色动态添加可访问的路由。
router.beforeEach((to, from, next) => { const user = JSON.parse(localStorage.getItem('userInfo') || 'null'); if (to.path === '/login') { next(); return; } if (!user) { next('/login'); return; } // 动态添加角色对应的路由 if (user.role === 0 && !router.hasRoute('adminDashboard')) { router.addRoute(adminRoutes); } next(); });这样做的效果是:普通用户就算手动在地址栏输入后台管理页面的路径,也会被路由守卫拦截。虽然前端限制不是绝对安全(真正的安全边界在后端接口鉴权),但至少体验上是完整的。
4. 前端页面实现与 Vue 组件设计
4.1 页面整体布局
前台页面我设计了三块核心区域:铺位展示列表页、铺位详情页、个人中心(我的申请、我的合同、我的账单)。后台管理端则是经典布局:左侧菜单栏、顶部导航栏、中间内容区。
铺位列表页是这个项目的门面。每张卡片展示铺位图片、名称、面积和月租金,点击卡片跳转到详情页。
这里有一个交互细节值得提:铺位状态不同,卡片按钮文案和可用性也要跟着变。待租的铺位显示“提交租赁申请”,已租的显示“该铺位已被租赁”,维护中的显示“维护中不可申请”。这个逻辑放在前端用 Vue 的v-if判断是最直观的:
<el-button v-if="shop.status === 0" type="primary" @click="handleApply(shop.id)" >提交租赁申请</el-button> <el-button v-else type="info" disabled >{{ shop.status === 1 ? '已被租赁' : '维护中' }}</el-button>4.2 关键组件实现:申请租赁表单
租赁申请表单包括以下字段:期望租赁开始日期、期望租赁时长(按月选)、经营用途说明。后端校验通过后生成一条LeaseApply记录,状态为“待审核”。
这里一个容易踩的坑是日期格式的传递。前端<el-date-picker>组件默认返回的是 JavaScript 的Date对象,如果不配置value-format,传给后端的可能是"2025-06-30T16:00:00.000Z"这种带时区的格式,直接存数据库会多一天或少一天。
最稳妥的做法是在el-date-picker上显式指定格式:
<el-date-picker v-model="applyForm.startDate" type="date" value-format="YYYY-MM-DD" placeholder="选择开始日期" />这样前端传给后端的就是干净的"2025-07-01"字符串,后端用String接或LocalDate.parse()转都方便。
4.3 账单支付模拟逻辑
毕设项目里接真正的第三方支付平台比较麻烦(需要企业资质),所以支付环节我用的是“线下转账确认”模拟方案:
- 用户点击“去支付”,系统生成一笔账单。
- 用户选择“转账方式”(模拟),上传转账凭证截图。
- 管理员在后台看到支付凭证后,点击“确认收款”,账单状态变为已支付。
这套流程既完成了业务闭环,又避开支付平台的接入难题,答辩的时候也能解释清楚你的设计思路。
5. 遇到的坑与排查技巧
5.1 跨域问题导致前端请求失败
这是前后端分离项目第一个一定会碰到的问题。我碰上的是CORS policy: No 'Access-Control-Allow-Origin' header报错。
解决方案有两种:
第一种是后端配置全局 CORS,这是最简单干净的:
@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }第二种是前端开发环境用 Vite proxy 代理转发:
// vite.config.js export default { server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }我的建议是:开发阶段用 Vite 代理,后端不配 CORS;部署阶段后端配好 CORS 或直接由 Nginx 统一转发。这样两个阶段都少一些不必要的麻烦。
5.2 Spring Boot 版本太高导致的问题
这个项目开始搭建时我曾经用过 Spring Boot 3.x。Spring Boot 3 是一个大版本升级,底层 Java EE 规范换成了 Jakarta EE,原来javax.*包名全部改成jakarta.*。MyBatis-Plus 的旧版本如果不适配,启动就会直接报ClassNotFoundException。
这里有两个选择:
选择一(省事):用 Spring Boot 2.7.x。这个版本是目前毕设项目的最优解,大量适配文档和代码示例都基于它。
选择二(尝鲜):用 Spring Boot 3.x,同时确认 MyBatis-Plus 版本在 3.5.3 及以上,并且 Java 版本用 17+。
做毕设我强烈建议选第一种。你自己体会一下,当你花一整晚查一个ClassNotFoundException,最后发现只是版本不兼容时,那滋味相当不好受。
5.3 MyBatis-Plus 字段映射失败的坑
有一张表的字段名叫description,我在实体类里写了private String description;,结果查询后这个字段一直是null。排查下来的原因是 MySQL 的description在某些版本里是保留关键字。
这个问题的解法是在application.yml里开启驼峰映射,同时写 SQL 时给字段加反引号:
SELECT \`description\` FROM shop;最好还是在设计表结构时就避免使用这类保留字,直接把字段命名为shop_desc或details。
5.4 Vue 打包后刷新页面 404
这是前端路由使用 history 模式的老大难问题。开发环境一切正常,部署到服务器上,首页能打开,但一旦刷新子页面 URL,Nginx 直接返回 404。
原因很直接:前端路由是单页应用内部的虚拟路由,服务端并不知道/shop/1这个地址该返回哪个文件。解决办法是 Nginx 里加一个回退配置:
location / { try_files $uri $uri/ /index.html; }这样当服务器找不到对应文件时,就会回退到index.html,前端路由接管后重新渲染对应页面。
5.5 打包上线时前端资源 404 问题
另一个常见问题是前端构建完部署后,JS 和 CSS 资源加载不出来。十有八九是静态资源路径配错了。
Vite 的默认构建配置会使用/开头的绝对路径,如果你部署在子目录下,就会全部 404。解决办法是在vite.config.js里设置:
export default { base: './' }改成相对路径后,部署到哪个目录都不会出问题。
6. 项目部署与答辩准备
6.1 本地部署步骤
项目写完以后,实际部署踩一遍流程,对答辩“部署方案”这个必考题非常有帮助。完整步骤如下:
- 后端打包:
mvn clean package -DskipTests,生成Jar文件。 - 前端构建:在
store-front目录执行npm run build,生成dist目录。 - 将
dist下所有文件放到 Nginx 的html目录下。 - 后端 Jar 包通过
java -jar store-api.jar启动。 - Nginx 里配一个反向代理,把
/api路径转发到后端端口。
Nginx 配置可以参考这样一段:
server { listen 80; server_name your-domain.com; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }6.2 答辩前需要准备的问题
答辩时老师最喜欢问的就是“为什么”和“怎么做”。我把自己被问过的高频问题列个清单:
- 为什么用 MyBatis-Plus 而不是 JPA?因为 MyBatis-Plus 可以精确控制 SQL,复杂多表连接场景更灵活,而且中文资料多。
- 权限控制是怎么实现的?JWT 登录 + 前端路由守卫 + 后端拦截器三层防护,缺一不可。
- 如果同一铺位同时被两个人申请怎么办?数据库层面用状态字段 +
UPDATE ... WHERE status = 0原子更新,只能有一个申请生效。 - 押金退还是怎么处理的?合同到期时,管理员发起退押金操作,账单生成一条负向记录,资金流可查询。
这些问题如果能用自己的话讲清楚,说明你确实是对项目有深入理解的,而不是网上随便拷贝一份源码来应付。
7. 扩展思路:这个项目还能怎么玩?
如果时间充裕,想给项目加分,我有几个可以落地的扩展方向:
- 引入 Redis 缓存:把铺位列表、字典数据等热门只读数据缓存到 Redis,降低数据库压力。如果能顺便解释缓存穿透、缓存击穿的解决思路,那是妥妥的加分项。
- 做数据可视化:ECharts 加上商城空置率、月度租金收入趋势、铺位热度 Top10 统计图,管理端瞬间看起来高端不少。
- 定时任务做合同到期提醒:用 Spring 自带的
@Scheduled注解,每天扫描一次即将到期的合同,给管理员发站内消息提醒。 - 多角色管理:把管理员拆成招商经理和财务两个角色,分别分配不同菜单和操作权限,更贴近真实业务场景。
这些扩展不用全做,挑一个合适的点深入,答辩时能讲的东西就完全不一样了。
最后说一点个人的实际体会:做毕设和做真实商业项目最大的区别在于,毕设更看重的是你对整个软件开发流程的理解和把控能力,而不是代码量有多大。通常我用四步就能理清一个项目:拆业务、定表、画页面、串联接口。遇到不懂的就先换成最熟悉的方式实现,保证系统能跑起来,再去优化细节。如果你正在做这个项目,按照这篇博客的路子走一遍,我相信你会对整个前后端分离项目的全貌有更清晰的认识。祝顺利。