这两年高校扩招和毕业生租房需求一起涨大学生找房源基本还是靠中介、学长学姐转租、各种群里的零散信息信息不透明、房源真实性没保障、签约过程不正规的问题一直存在。所以当有人让我做一套大学生在线租房平台的时候我第一反应是这不单纯是个“发布房源打电话”的展示站得有完整的业务闭环——从房源发布、审核、浏览、预约看房到订单生成、合同记录、支付跟踪、评价管理再到后台的数据统计和权限控制。我最终用 SpringBoot Vue MyBatis MySQL 把这套企业级管理系统完整落地了这篇文章就把我的设计思路、核心实现、数据库结构、部署细节以及我踩过的几个坑全部拆开来讲。适合正在做毕业设计、想入行 Java 全栈、或者准备接手类似租房类管理系统的同学参考。1. 项目定位与整体设计思路1.1 大学生租房场景到底特殊在哪普通租房平台和大学生租房平台最大的区别不在功能层面而在“信任链路”和“管控流程”。普通平台只要房东发房源、租客打电话联系就结束了但校内场景必须解决三个问题第一房源信息的真实性——很多学生没时间实地看房图片和描述一旦失真浪费的是整个找房周期第二签约流程的规范化——学生群体法律意识相对薄弱押金、违约金、水电费分摊这些关键条款如果线上没有任何留痕记录后期扯皮成本极高第三管理侧的可视化——学校周边的房源往往集中在几个核心小区管理员需要知道哪些房源在租、哪些待审核、哪些即将到期这比普通平台要求更高。所以我在需求设计阶段就把系统拆成三条业务线房源全生命周期管理发布→审核→上架→带看→成交→下架、订单与合同管理预约→确认→生成合同→支付押金→入住→退租、以及面向管理员的运营视图用户审核、房源审核、订单监控、数据统计。只做单端展示的“伪项目”没法覆盖这些这也是为什么必须上前后端分离架构——前端负责交互体验后端负责业务规则和数据一致性两边可以独立演进。1.2 角色权限模型的确定这个系统一共三种角色学生租客、房东房源提供方、管理员平台运营方。很多同学做权限设计喜欢一上来就引入 Spring Security 做细粒度 RBAC但对这个体量的系统来说有点重。我的做法是后端统一用 JWT 做身份认证用一个自定义拦截器解析 token、取出用户角色再通过一个简单的角色常量判断接口访问范围。核心权限规则集中在后端接口层前端只是配合做路由守卫和按钮级显示真正的安全性必须依赖后端校验这个原则我在代码注释里也写得很清楚。具体到权限矩阵功能学生房东管理员浏览房源允许允许允许收藏房源允许拒绝拒绝发布/编辑房源拒绝允许拒绝预约看房允许拒绝拒绝审核房源拒绝拒绝允许数据统计拒绝拒绝允许用户管理拒绝拒绝允许这块是后续所有业务功能的地基先想清楚再做比写完代码再回头补权限要省力得多。2. 技术选型为什么是这套“全家桶”组合2.1 后端为什么选 SpringBoot MyBatisSpringBoot 在这个场景里的优势其实不是“快”而是“可维护性”。租房平台的业务规则不算复杂但接口数量不少——用户、房源、订单、合同、评论、收藏、统计每个模块都有一组 CRUD 加自定义业务操作。用 SpringBoot 的自动配置和 starter 机制十几分钟就能拉起一个结构清晰的项目Controller、Service、Mapper 三层的职责边界非常明确后期找人接盘也不至于看不懂。MyBatis 的选择可能有人会问为什么不直接用 JPA我的理由是租房平台最核心的高频操作是“房源条件查询”关键词、区域、价格区间、户型、朝向、排序方式这些都是动态拼接的。MyBatis 的 XML 或注解方式可以精确控制每一条 SQL动态 SQL 标签在应对这种多条件组合筛选时非常自然而且 MySQL 的 SQL 优化手段比如分页参数下推、索引提示都能直接写在 mapper 里不需要绕一层 JPQL。对这套系统来说SQL 的可控性比实体关系映射的自动性重要得多。2.2 前端为什么是 Vue 而不是其他框架选 Vue 3 组合 Vue Router Pinia Element Plus三个理由第一房源列表、搜索筛选、订单状态切换这些都是典型的响应式交互Vue 的双向绑定和虚拟 DOM 在这个场景下开发效率极高第二Element Plus 提供了一套完整的中后台组件库表格、表单、分页、弹窗、步骤条都是现成的尤其适合管理端页面的快速搭建第三Vue 生态对中文社区最友好遇到问题搜解决方案几乎不费劲。前端项目用 Vite 构建开发阶段的热更新体验很好生产构建产物用 Nginx 托管再通过反向代理把 /api 路径转发到后端服务这样前端部署不涉及跨域问题。这是我在实践里比较推荐的部署方式后面第三节详细展开。2.3 MySQL 够不够用很多人一听“企业级”就想上 PG 或者 Oracle但说实话这个体量的业务用 MySQL 完全足够而且运维成本低、云数据库支持好、周边工具链成熟。我的选择是 MySQL 8.0字符集 utf8mb4InnoDB 引擎事务隔离级别用默认的 REPEATABLE READ。核心表全部设计 InnoDB利用行级锁保证并发下单时订单表不会出问题——比如两个学生同时预约同一套房源后提交的那一个必须能感知到已经被预约这个事务和锁机制后面细讲。3. 核心功能模块设计与落地实现3.1 用户认证与 JWT 登录流程登录模块是整个系统最容易被人忽略、但最影响安全性的部分。我这里没有用传统的 Session Cookie而是 JWT 无状态认证。流程是用户提交账号密码 → 后端用 BCrypt 校验密码哈希 → 校验通过后生成一个包含 userId 和 role 的 token → 前端把 token 存到 localStorage 并在每次请求时放进 Authorization 头。后端自定义了一个拦截器对所有需要登录的接口统一校验。核心代码示意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 )) { throw new BusinessException(401, 未登录或token已失效); } LoginUser user JwtUtil.parseToken(token.replace(Bearer , )); UserContext.set(user); return true; } }需要特别提醒的是JWT 的密钥千万不要写死在代码里要放到配置文件的加密配置项中生产环境还可以通过环境变量注入。token 的过期时间我设置为 2 小时前端在 axios 响应拦截器里对 401 状态做统一跳转回登录页。另外前端存储 token 用 localStorage 可以但要了解它存在 XSS 风险所以我在前端项目里对用户输入做了统一的文本转义处理防止恶意脚本通过房源描述等字段注入。3.2 房源发布与多条件搜索的 SQL 设计房源发布是房东端的核心操作。字段比较多标题、描述、户型、面积、租金、押金、租期、所在小区、朝向、楼层、配套设施这是一个逗号分隔的标签、房源图片多图上传、以及经纬度方便地图展示。我设计成一张 house 主表加一张 house_image 图片表一对多关系避免图片字段用 JSON 存导致无法走数据库索引的问题。多条件搜索是性能敏感点。搜索参数可能有keyWords匹配标题和描述、minPrice/maxPrice、district、houseType、orientation、hasElevator、sortField 等。我用 MyBatis 的where标签动态拼接并强制使用参数化查询防止 SQL 注入。核心示意select idsearchHouses resultTypecom.xxx.entity.HouseVO SELECT h.*, u.nickname AS ownerName FROM house h LEFT JOIN user u ON h.owner_id u.id where h.status 1 if testparam.keyWords ! null and param.keyWords ! AND (h.title LIKE CONCAT(%, #{param.keyWords}, %) OR h.description LIKE CONCAT(%, #{param.keyWords}, %)) /if if testparam.minPrice ! null AND h.price gt; #{param.minPrice} /if if testparam.maxPrice ! null AND h.price lt; #{param.maxPrice} /if if testparam.houseType ! null and param.houseType ! AND h.house_type #{param.houseType} /if /where ORDER BY ${param.sortField} ${param.sortOrder} LIMIT #{param.offset}, #{param.pageSize} /select这里踩过一个坑ORDER BY ${param.sortField}用${}拼接是容易 SQL 注入的所以我在前端传参时对 sortField 做了白名单校验只允许传入预设的几个字段名price, create_time, area否则就用默认的 create_time这是个必须处理的安全细节别忽略。3.3 预约、订单与合同状态的流转学生看到房源后可以发起“预约看房”这条预约记录会通知房东房东确认后预约状态变成“已确认”学生可以进一步选择“签约入住”此时系统自动生成一个订单把房源状态改成“已出租”入住后到退租再触发退租流程更新房源状态为“已下架”。这里最关键的是并发控制和事务管理。比如同一房源如果同时被两个人预约成功就会出现一房二租。我的处理方式是在 order 表增加一个房源维度的事务锁定生成订单前先SELECT * FROM house WHERE id ? FOR UPDATE锁住房源行记录确认当前 status 还是可租状态后再插入订单并更新房源状态为已出租整个过程包在一个Transactional方法里。这个做法在面试里也很容易成为一个亮点建议理解透。合同这一块我做了简化但保留核心字段合同编号用日期随机数生成、租客 id、房东 id、房源 id、起止日期、月租金、押金、违约金比例、合同状态。正式落地时可以接入电子签但作为这套管理系统先把数据留痕做好比追求花哨功能更重要。退租时系统会自动计算押金应退金额的初值如果有欠费扣款可以人工修改保证流程可追溯。3.4 管理后台的数据统计与审批流后端管理员端主要做三件事用户审核房东实名认证、房源审核新发布的房源必须管理员确认后才能上架、以及统计看板。统计看板用 ECharts 前端图表组件展示几个核心指标每日新增用户数、房源发布趋势、各小区房源占比、订单成交量走势。统计接口我用了三条 SQL 聚合查询没有引入额外的大数据组件因为数据量级不大-- 近30天订单成交量 SELECT DATE(create_time) AS day, COUNT(*) AS cnt FROM rent_order WHERE create_time DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY DATE(create_time) ORDER BY day;前端获取到这些分组数据后直接喂给 ECharts 的折线图和饼图性能完全够。有同学可能会问为什么“企业级”不大动干戈上定时任务和消息队列我的回答是架构要匹配业务复杂度当前量的预约通知用系统内置消息表就够了房东登录后看到“待处理预约”的红点提醒比引入 RabbitMQ 更务实——先把这个完整跑通将来量大了再平滑扩展。4. 数据库设计这些表结构和索引值得抄4.1 核心表结构全景整个系统一共设计了 9 张核心表用户表区分角色和认证信息、房源表房源核心属性、房源图片表一对多、收藏表学生和房源的关联、预约看房表、订单表签约入住、合同表订单的补充信息、消息通知表、管理员操作日志表。我挑三张最关键的表给出建议字段设计其它表可以顺着同样的思路补全。第一张是用户表字段名类型说明idbigint主键usernamevarchar(50)登录账号唯一索引passwordvarchar(100)BCrypt 哈希roletinyint0学生 1房东 2管理员nicknamevarchar(50)昵称phonevarchar(20)手机号登录/联系用student_novarchar(30)学生证编号可选statustinyint0禁用 1正常create_timedatetime注册时间第二张是房源表。注意几个字段facility 用 varchar 存逗号分隔的配套标签比如“空调,独卫,阳台”查询时用 FIND_IN_SET 或 LIKE 都能处理数据量上来以后如果标签查询频繁建议拆成关联表但当前阶段够用status 字段要设计成可扩展的枚举0待审核、1上架中、2已出租、3已下架一个字段表达整个生命周期。第三张是订单表。核心字段包括 order_no唯一业务单号、user_id、house_id、owner_id冗余房东 id方便房东端查询、status0待付款、1已付款、2已入住、3已退租、4已取消、amount租金总额、deposit押金、start_date、end_date、create_time。4.2 索引设计的关键经验这一块我单独说因为很多同学建表时随手把字段都加上索引反而导致写入慢、占用空间大。我的索引设计原则是这样的登录查询高频user 表的 username 建唯一索引。房源搜索高频组合house 表的 status create_time 建联合索引因为列表页默认按时间排序且只展示上架房源price 建普通索引支持价格排序district house_type 这个组合在筛选时经常同时出现也建了联合索引。订单查询高频order 表的 user_id 和 owner_id 分别建索引因为学生端和房东端都经常查自己的订单列表。不做无意义的索引比如描述字段 desccription 内容长绝不建索引性别的区分度太低不建索引。在建索引这件事上被坑过一次一开始我直接在 create_time 上单独建索引但列表查询条件是 status1 AND create_time DESC单独走 create_time 索引回表后还要过滤 status导致性能比预想的差。改成 (status, create_time) 联合索引后查询直接走索引覆盖效率提升非常明显。4.3 事务与并发下单的数据库原语前面提到的SELECT ... FOR UPDATE锁我要再展开说说。在 MySQL InnoDB 下这个语句会给命中的行记录加排他锁直到事务提交或回滚才释放。所以“下单”这个操作的事务顺序很重要Transactional(rollbackFor Exception.class) public Order createOrder(Long houseId, Long userId) { House house houseMapper.selectByIdForUpdate(houseId); // 进入这个方法后这条 house 行已被锁住 if (house null || house.getStatus() ! 1) { throw new BusinessException(房源不存在或已不可租); } Order order buildOrder(house, userId); orderMapper.insert(order); houseMapper.updateStatus(houseId, 2); // 标记为已出租 return order; }注意这里的锁一定要在事务开始后第一时间获得如果中间再穿插别的查询锁的持有时间变长并发能力会下降。同时这个写法要求houseMapper.selectByIdForUpdate这个 SQL 必须在事务方法内执行否则会自动提交、锁直接失效。另外事务内的所有操作走同一个数据库连接这是 Spring 的事务管理保证的你不需要自己关心连接问题但要知道它的前提是基于线程绑定的连接传递。5. 前端工程与前后端联调实操记录5.1 Vue 项目的目录结构与路由守卫前端工程我按模块划分了目录api所有接口请求封装、router路由表、store状态管理、views页面组件、components通用组件、utilsaxios 实例和工具函数。页面分成门户端和管理端两个 layout门户端包含首页、房源列表、房源详情、个人中心、订单页管理端有用户管理、房源审核、订单管理、数据统计。路由守卫是前端权限的第一道门。我在全局前置守卫中读取本地 store 里的用户角色然后判断目标路由的 meta.roles 是否包含当前角色不匹配就重定向到登录页。示例router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.meta.requiresAuth !token) { next(/login); return; } const role store.state.user.role; if (to.meta.roles !to.meta.roles.includes(role)) { next(/403); return; } next(); });注意前端守卫只是提升用户体验真正防越权还得靠后端接口的角色校验否则别人换个接口地址就能访问不该看的数据了。这是全套系统里我认为最需要反复强调的安全认知。5.2 Axios 封装与统一异常处理axios 实例我统一设置了 baseURL 为/api超时时间 10 秒。请求拦截器里带上 token响应拦截器里做了三层处理正常返回 data、业务错误提示后端返回的 message、HTTP 401 跳转登录页。后端统一使用一个 Result 包装类结构是{ code, message, data }其中 code 200 表示成功业务异常通过全局RestControllerAdvice捕获并返回对应 code。这样前后端联调时异常形态是统一的排查问题非常简单不会出现“这里返回了空对象、那里抛了 500”的混乱情况。代码示意service.interceptors.response.use( response { const res response.data; if (res.code ! 200) { ElMessage.error(res.message); return Promise.reject(new Error(res.message)); } return res.data; }, error { if (error.response error.response.status 401) { router.push(/login); } return Promise.reject(error); } );5.3 图片上传与文件存储方案房源图片上传我最初想直接用 base64 塞进 MySQL测了几张就放弃了——数据库体积膨胀快、接口响应慢完全不划算。后来改成前端把图片文件以 FormData 方式 POST 到后端的/api/upload接口后端用MultipartFile接收保存到服务器本地一个 upload 目录文件名用 UUID 重命名避免冲突然后返回访问 URL。生产环境可以考虑接云存储但本地文件存储方案在系统演示和中小规模部署完全够用。上传接口还有个细节必须限制文件类型和大小。我同时做了两端校验——前端在上传组件里只允许 jpg/png/webp 且不超过 5MB后端再根据扩展名和大小做二次校验防止绕过前端直接构造请求把脚本文件传上去。别嫌麻烦这个环节省掉以后是要吃大亏的。6. 部署上线全流程与 Nginx 配置6.1 后端打包与启动后端项目的打包用 Maven执行mvn clean package -DskipTests产物是target/xxx.jar。启动时我用外部配置优先的方式把数据库连接、JWT 密钥、上传路径全部放到 application-prod.yml 中启动命令java -Xms256m -Xmx512m -jar rent-platform.jar --spring.profiles.activeprod生产环境内存没必要给太大租房平台这种并发量级 512M 绰绰有余。如果想让进程在后台稳定跑可以用 nohup 或者配 systemd 服务这个根据你自己服务器的习惯来就行。6.2 前端构建与 Nginx 反向代理前端执行npm run build后生成 dist 目录把这个目录放到 Nginx 的 html 目录下。我的 Nginx 配置核心片段如下server { listen 80; server_name your-domain.com; root /usr/share/nginx/html/rent-platform; 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; } location /upload/ { alias /data/rent-platform/upload/; } }两个关键点第一try_files是为了支持 Vue Router 的 history 模式前端路由刷新时不会 404第二location /api/的proxy_pass末尾带斜杠和不带斜杠行为不同带斜杠表示把 URL 中 /api 前缀去掉再转发到后端我这里后端接口设计就带 /api 前缀所以按这个写法转发。如果你后端接口没有 /api 前缀就需要把 proxy_pass 写成http://127.0.0.1:8080/api/这个细节经常让人卡半天贴出来提醒一下。6.3 数据库初始化与迁移数据库脚本我分成三个文件schema.sql建表、data.sql初始管理员账号和演示数据、index.sql索引。首次部署时按顺序执行。日常开发用本地的 MySQL 5.7 或 Docker 起一个实例都行。这里建议在 schema.sql 里写清楚每个表的注释和字段注释后面接手的同学会感谢你的——代码写注释的习惯比任何文档都管用。7. 常见问题与排查技巧实录7.1 跨域问题前端请求后端报 403/404 却看不到具体错误这是个高频问题尤其是前后端分离项目。如果你开发时用 Vite 本身的代理在 vite.config.ts 里配了 proxy那么前端请求是/api/...相对路径走的是 Vite 代理转发不会跨域但如果你直接在 axios 里写了http://localhost:8080这种绝对地址就会出现跨域。我的建议是开发环境统一用 Vite 代理生产环境统一用 Nginx 反向代理两端都不要在后端代码里加CrossOrigin注解也别用全局 CORS 配置容忍所有来源——那等于把跨域安全限制关掉了。7.2 MyBatis 报 “Invalid bound statement (not found)”这个报错几乎每个用 MyBatis 的新手都遇到过。原因是 Mapper 接口和 XML 文件的绑定失败。最常见的情形是XML 文件没有放在 mapper 接口相同的包路径下或者 application.yml 里没有配置 mapper-locations。我这里的配置是mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.xxx.entity另外提醒一句如果 XML 文件放在 src/main/java 下Maven 默认不会把它打进 classpath需要在 pom.xml 的 build 里加 resources 配置把**/*.xml包含进去。这个坑特别隐蔽代码在 IDE 里跑得好好的一打包就报 Statement not found原因就在这里。7.3 前端表格日期显示成了时间戳后端实体里的 LocalDateTime 经过 JSON 序列化后默认是一串数组或时间戳前端表格直接显示会很难看。解决方式是在 application.yml 配置全局的 Jackson 日期格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8这里 time-zone 必须设置成 GMT8否则你本地看着正常部署到服务器上日期会差 8 小时——因为服务器默认时区可能是 UTC这个坑我也踩过。7.4 并发预约同一房源导致一房二单前面讲的FOR UPDATE锁我实际测试时排查过一个问题如果 selectByIdForUpdate 所在的方法没有被 Spring 的事务切面管理比如同类内部调用导致代理失效事务就不会生效锁也会立刻释放结果两个并发请求都能拿到同一个房源的“可租”状态。所以遇到一房二单的时候先检查调用的入口是不是走了代理的 public 方法再确认锁语句在事务内。这个定位思路比反复看 SQL 快得多。7.5 Nginx 刷新页面 404 问题速查表现象原因解法首页能打开/house/1 刷新就 404Vue Router history 模式未配置 try_files在 location / 加上 try_files $uri $uri/ /index.html接口能通首页和详情页面资源 404dist 部署路径不匹配引用的资源路径是绝对路径vite.config.ts 里设置 base: ./上传图片不显示Nginx 的 /upload/ 代理路径不对或目录权限不足确认 alias 路径存在且 nginx 进程有读取权限7.6 密码明文存储的安全风险最后说一个最基础但最容易犯的错误用户表密码千万不要明文存。我这里用 BCrypt 加盐哈希Spring Security 的BCryptPasswordEncoder可以直接用每次生成哈希值不同但matches方法可以校验。哪怕你只是做一个 demo也请把这一步做对了因为租房平台涉及用户手机号、身份证号学生认证时可能涉及等敏感信息密码如果明文存库一旦数据库泄露影响的不只是演示项目而是真实用户的数据安全。我觉得这是整个系统开发里最不能妥协的一条底线。我最后分享一个个人体会这个项目从需求分析到完整落地消耗时间最多的其实不是写代码而是想清楚“业务状态怎么流转”“哪些地方要防并发”“哪些接口要控权限”。把这些设计层面的问题做扎实代码实现反而很快。如果你正在复刻类似系统建议先把角色权限矩阵和订单状态机画清楚再动手指写 SpringBoot 和 Vue你会明显感觉整个过程顺很多。将来扩展时可以在这个基础上逐步加电子合同签章、地图找房、支付对接、消息推送这些模块架构上不用做大的调整业务能力却会上一个台阶。
阅读完成 · 觉得有帮助?