1. 项目启动前的需求梳理与技术选型1.1 这个系统到底要解决什么问题做房屋租赁管理系统这个项目前后折腾了将近一个多月。当时接手这个需求业务方是一家小型公寓运营团队手里十几套房源分布在两三个小区租客流动频繁。他们最头疼的地方在于房源信息零零散散存在不同同事的Excel表里合同签完就塞进文件柜每个月靠人手统计房租收没收到、水电费怎么摊、合同哪天到期。经常出现一个很尴尬的场面——某套房明明已经租出去了另一个同事还在群里转发招租信息或者租客合同到期没人提醒房子空了两周才发现。所以这个系统的核心诉求非常直接房源信息集中管理、租客合同线上记录、账单和缴费情况清晰可查、到期提醒自动触发。往深一层讲业务方真正想要的是一套信息不丢、账目不乱、谁都能上手的管理工具而不是一个功能堆砌的花架子。想清楚这一点再动手设计后面每一步都不会跑偏。1.2 为什么选SpringBootVue这套组合技术选型这件事我向来遵循一个原则在满足需求的前提下选自己最熟悉、社区方案最成熟、出了问题最好查的技术栈。SpringBoot Vue MySQL MyBatis恰好落在这个组合里。后端用SpringBoot是因为它对第三方组件的集成足够省心。Spring MVC处理HTTP请求、Spring事务管理数据一致性、拦截器做权限控制很多原本要手写配置的东西都自动化了能省下大量Bean装配的时间。前端用Vue是因为这种管理后台类系统Vue的组件化开发、前端路由切换、以及和Element UI这类组件库的结合体验非常顺开发效率高代码结构也清楚。MySQL和MyBatis没什么争议。MySQL稳定免费资料多MyBatis上手成本低SQL写在XML里自己完全可控遇到多条件组合查询、动态SQL这类场景比JPA更直观。整套组合适合个人开发者独立完成也适合作为毕业设计或者简历项目——能讲清楚原理能跑通完整流程比强行追新技术栈重要得多。1.3 三类用户角色划分与权限边界这个系统在设计上分了三个角色管理员、房东、租客。这里的房东你也可以理解成业务员或运营人员本质一样。权限边界我一开始就划得很清楚管理员能看全站数据和统计报表能管理所有用户、房源和合同房东能管理自己名下的房源录入房源信息、上下架房源、查看自己房源的租约租客只能查看自己的合同、账单、个人资料以及浏览公开的房源。权限粒度不需要做到按钮级那么细但绝不能不做。如果所有接口都不校验角色任何登录用户都能调管理接口上线之后一定会出事。2. 系统架构与功能模块全景拆解2.1 前后端分离架构下的请求流转整个系统采用前后端分离架构。前端是一套独立的Vue工程开发环境跑在自己的端口上生产环境打包后由Nginx托管静态文件后端SpringBoot应用跑在另一个端口对外提供JSON接口。一次完整的请求流转是这样的用户在浏览器点击添加房源→ 前端路由跳转到对应页面 → 页面组件调用API → Axios拦截器自动带上Token → 后端拦截器校验Token和角色权限 → Controller接收参数 → Service处理业务逻辑 → Mapper执行SQL → 数据逐层返回 → 前端拿到JSON渲染DOM。这套流程在本地联调时绕不开跨域问题。前后端端口不同浏览器的同源策略会直接拦截请求。我在开发阶段选择在后端统一配置CORS生产环境则通过Nginx反向代理统一入口从根上规避跨域。两种方案都实践过后面在排坑章节会详细展开。2.2 功能模块怎么划分才清晰功能拆成六个模块登录认证、房源管理、租客与合同、账单缴费、系统管理、数据统计。登录认证账号密码登录、Token生成与校验、当前用户信息获取房源管理房源新增、编辑、上下架、条件搜索、图片上传租客与合同租客信息登记、租赁合同创建、续签、到期处理、到期提醒列表账单缴费账单按月生成、缴费登记、逾期状态展示系统管理用户管理、角色管理、公告发布数据统计房源出租率、月度租金收入汇总图表模块边界清楚之后接口命名也尽量统一。房源相关接口以/api/house开头合同相关以/api/contract开头账单相关以/api/bill开头。这样不用翻文档看一眼URL就知道接口职责后面扩展也顺手。2.3 用Vue Router而不是iframe集成有个朋友问我为什么不把各个功能页面做成iframe嵌进来那样更简单。我明确不建议。iframe嵌套最大的问题是状态隔离——子页面要单独维护登录状态URL无法深链刷新一下页面就回到原始状态给用户的感觉就是这系统怎么总是跳来跳去。Vue Router天然支持单页应用的路由跳转、历史记录、参数传递配合路由前置守卫做登录和角色校验体验和可维护性都远好于iframe。这也是当前主流的做法。凡是看到多页面iframe集成后台的方案除非是历史遗留系统否则都建议重构。3. 数据库设计房屋租赁系统的核心表结构3.1 建表之前先梳理业务实体数据库是整套系统的基础表结构设计直接决定后面开发是顺畅还是痛苦。我的习惯是动手建表之前先把业务里涉及的实体列出来理清它们之间的关系再落成表。这个系统的核心实体有用户、房源、房型、房源图片、租约合同、账单、缴费记录、公告。其中房型单独拆了一张表有人一开始觉得没必要认为在房源表里加个字段存一室一厅就行。但实际开发中发现房型会参与筛选、统计、按户型分析出租率单独建表灵活得多。我的经验是凡是后续可能做分类统计和条件筛选的字段都值得考虑单独建表。3.2 核心表字段详解用户表sys_user设计比较常规id、username、password、real_name、phone、role用数字区分1管理员、2房东、3租客、status、create_time。密码字段存的是加密后的密文绝不允许明文入库。房源表house是信息量最大的一张表核心字段列出来可以参考字段名类型说明idbigint主键titlevarchar(100)房源标题addressvarchar(255)详细地址areadecimal(10,2)面积㎡floorint所在楼层total_floorint总楼层orientationvarchar(10)朝向decorationvarchar(20)装修情况rent_pricedecimal(10,2)月租金deposit_typetinyint押付方式如押一付三house_type_idbigint关联房型表owner_idbigint所属房东IDstatustinyint状态0未上架、1已上架、2已租出descriptiontext房源描述create_timedatetime创建时间这里重点说下status字段。房源状态最初只设计了上架/下架后来发现不够用——一间已经租出去的房子如果还挂在已上架状态用户浏览时会误以为还能租实际打电话过去却碰壁。所以最终加了已租出状态并且约定只要合同处于执行中房源状态就自动变为已租出。这个逻辑放在Service层统一处理。租约合同表contract关键字段id、contract_no合同编号、house_id、tenant_id、owner_id、start_date、end_date、monthly_rent、deposit、status、create_time。status用数字枚举1执行中、2已结束、3已违约。合同生效时自动联动房源状态、按月生成账单到期前30天触发提醒。这些业务联动都放在Service层而不是散落在Controller里。账单表bill和缴费记录表payment_record我分开建。账单记录的是该收多少缴费记录的是实际收了什么。这样做的价值在于如果租客部分缴费、跨期缴费、或者有优惠减免每一笔实际收款都能单独追溯而原始账单数据不会被破坏。bill表字段包括id、contract_id、bill_month、amount、status未缴/已缴/逾期、due_date、pay_date。3.3 数据库设计的三个心得第一个心得时间字段统一用datetime不要混用时间戳。后面做到期提醒和月度统计的时候SQL直接拼日期字符串就能算写起来顺手很多。第二个心得金额字段用decimal(10,2)不要用float或double。房租、押金、水电费都是钱float和double在累加统计时会暴露精度问题轻则对不上账重则业务方直接怀疑系统可靠性。decimal虽然占用空间大一点但存金额是唯一靠谱的选择。第三个心得MySQL层面不要滥用外键。建表我没建物理外键表间关系在应用层维护。这样插入更新时的约束检查和锁开销更少性能更好而且删历史数据要不要保留关联记录这类业务规则用代码控制更灵活。逻辑外键完全够用物理外键反而在后续改表结构时束手束脚。4. 后端核心实现SpringBoot MyBatis落地细节4.1 项目结构怎么组织才不乱SpringBoot项目按职责分包这是最基础也最重要的规范。源码目录大致如下com.example.rent ├── controller │ ├── AuthController.java │ ├── HouseController.java │ ├── ContractController.java │ └── BillController.java ├── service │ ├── HouseService.java │ ├── ContractService.java │ └── ... ├── mapper │ ├── HouseMapper.java │ ├── ContractMapper.java │ └── ... ├── entity │ ├── User.java │ ├── House.java │ └── ... ├── config │ ├── WebConfig.java │ └── ... ├── common │ ├── Result.java │ └── PageResult.java ├── interceptor │ └── AuthInterceptor.java └── util └── JwtUtil.javaentity映射数据库表mapper负责数据访问service承载业务逻辑controller只做参数接收和结果返回。我一直主张controller要薄、service要厚。业务逻辑变化最频繁集中在service层之后单测、复用、维护都会轻松很多。有个细节容易被忽略统一返回类Result。每个接口返回我都用Result.success(data)或Result.error(msg)包装不直接返回裸数据。前端拿到的是一个固定结构——code、msg、data不管成功失败都是同一套解析逻辑。联调阶段这个习惯能省下大量口水。4.2 登录认证与Token方案认证方案我选了JWT没有用传统Session。原因有两个前后端分离架构下Session要处理跨域Cookie问题比较麻烦JWT是自包含令牌服务端不需要存会话状态过期时间、用户ID、角色都放在Token里后端验签即可确认身份完美契合无状态接口风格。集成流程用户提交用户名密码到/api/auth/login后端校验通过后生成Token返回前端前端存在localStorage每次请求通过Axios请求拦截器加上Authorization: Bearer token头。后端写一个AuthInterceptor拦截所有/api/**请求登录接口放行其他接口必须通过Token校验。拦截器核心逻辑是这样public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { throw new ApiException(401, 未登录或登录已过期); } String realToken token.substring(7); Claims claims JwtUtil.parseToken(realToken); request.setAttribute(userId, claims.get(userId)); // 角色权限判断 return true; }这里有个实操经验拦截器校验完Token后后续Controller要用当前登录用户信息直接从request attribute取不要再去数据库查一次。用户ID和角色都在Token里解析出来放进request简单又高效。4.3 房源列表的分页与多条件查询房源列表是系统里最常用的功能既要做分页又要支持关键词、租金区间、房型、状态等多条件筛选。我写了一个统一的查询方法接收查询对象HouseQuery包含pageNum、pageSize和各个筛选条件由MyBatis动态SQL拼条件。MyBatis动态SQL是全项目里最有价值的部分。多条件组合查询的核心是where和if标签配合select idselectHouseList resultTypecom.example.rent.entity.House select * from house where if teststatus ! null and status #{status} /if if testminPrice ! null and rent_price gt; #{minPrice} /if if testmaxPrice ! null and rent_price lt; #{maxPrice} /if if testhouseTypeId ! null and house_type_id #{houseTypeId} /if if testkeyword ! null and keyword ! and (title like concat(%, #{keyword}, %) or address like concat(%, #{keyword}, %)) /if /where order by create_time desc limit #{offset}, #{pageSize} /select注意三个细节模糊查询的百分号用concat拼接参数用#{}占位规避SQL注入分页用limit不要全量查出再内存分页查询条件里的大于小于号要用XML实体转义否则XML解析直接报错。4.4 MyBatis使用中的三个易错细节第一个驼峰映射。数据库字段是下划线命名create_time实体类是驼峰命名createTime如果不在配置里开启map-underscore-to-camel-case: true查询回来的字段全是null。这是出现频率最高的问题每次有人问我为什么查出来一堆null八成都是这个原因。第二个批量插入。房源图片一张张插没关系但账单按月生成是批量的。用foreach标签拼批量insert比循环单条插入少很多次数据库往返。一份合同可能生成几十条账单循环单插要几十次网络往返批量一次搞定性能差了一个量级。第三个事务管理。合同创建涉及多个表操作——插入contract、更新house状态、生成账单、变更租客状态。任何一步失败都会造成数据不一致所以创建方法上必须加Transactional。注意事务只对RuntimeException默认回滚如果方法内部catch住了异常又没往外抛事务就失效了这个坑很隐蔽排查时要格外留神。5. 前端Vue实现从页面搭建到接口联调5.1 前端工程初始化与目录结构前端我用Vue CLI创建项目装好Vue Router和AxiosUI组件库选Element UI。目录保持标准习惯vue-rent ├── src │ ├── api # 接口请求模块 │ │ ├── auth.js │ │ ├── house.js │ │ └── contract.js │ ├── router # 路由配置 │ │ └── index.js │ ├── store # 状态管理 │ ├── utils │ │ ├── request.js # axios封装 │ │ └── auth.js │ └── views # 页面组件 │ ├── login/Login.vue │ ├── house/HouseList.vue │ ├── house/HouseDetail.vue │ ├── contract/ContractList.vue │ └── bill/BillList.vue页面组件按业务模块分文件夹放每个页面一个或多个Vue文件。这样新增功能的时候不需要到处找文件模块归属一目了然。5.2 Axios封装与Token注入Axios封装是每次都会认真做的基础工作。直接在页面里写axios.get(http://localhost:8080/api/house/list)短期能跑后期维护成本极高。我封装request.js做了三件事统一baseURL、请求拦截器注入Token、响应拦截器统一处理业务码和HTTP错误。import axios from axios import { Message } from element-ui import router from ../router const service axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { if (res.code 401) { localStorage.removeItem(token) router.push(/login) } Message.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) } return res }, error { Message.error(error.message || 网络异常) return Promise.reject(error) } )这个封装的收益在联调阶段体现得最明显后端返回401前端自动跳登录页后端返回业务错误前端统一弹提示。所有页面共用一套错误处理逻辑不用每个页面重复写判断代码量直接砍掉一截。5.3 路由守卫实现页面级权限控制前端权限控制分两级路由级访问控制和按钮级操作控制。路由守卫在router/index.js里通过beforeEach实现。判断逻辑是先看路由meta里有没有requiresAuth没有就直接放行比如登录页有则检查本地有没有Token没Token跳登录页并在query里记录redirect登录完跳回原页面。有Token后检查角色是否满足meta里的roles不满足跳403页面。router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) return } if (to.meta.roles) { const role Number(localStorage.getItem(role)) if (!to.meta.roles.includes(role)) { next(/403) return } } next() })还有一个细节管理员、房东、租客登录后看到的侧边栏菜单是不一样的。我是登录成功后根据角色从后端拉取菜单配置再动态注册路由。这样菜单权限由后端控制前端路由配置保持相对干净权限扩展时后端加个字段就行。5.4 核心页面实现列表、表单与图片上传房源列表页工作量最大由四个部分组成顶部搜索栏关键词、租金区间、房型下拉、状态筛选、工具栏新增、刷新、数据表格状态用tag展示不同颜色、底部分页器。列表数据加载逻辑很简单定义一个loadData()方法参数用this.searchForm调用列表接口拿到分页数据赋值给data。这中间有个细节必须处理搜索条件变化时要把pageNum重置为1再加载。不然用户在第5页点了搜索返回的数据可能只有一页页面还停在5看起来就像没搜索一样。表单页我用Dialog弹窗嵌套Form组件实现新增和编辑共用同一个表单。Element UI的表单校验负责必填和格式检查比如房源标题、租金必填租金还要通过pattern规则限定只能填数字。这个做法比在提交时手动写一堆if判断干净很多校验规则集中在配置里可读性也更好。图片上传是当时花时间最多的部分。前端用el-upload组件action指向后端的/api/house/upload接口on-success回调里拿到返回的文件URLPush进表单的图片列表。后端接收MultipartFile后用UUID重命名文件保存到服务器的upload目录再把能访问的URL返回给前端。详情页展示时直接img :srcurl就能显示。6. 环境配置与部署上线实操6.1 本地开发环境准备开发环境四件套JDK 8、Maven 3.6、MySQL 5.7及以上、Node.js 12。JDK我用的1.8SpringBoot 2.x配JDK 8最稳资料也最多。MySQL 5.7数据库默认字符集设成utf8mb4这样能正确存放emoji和生僻字。拿到项目之后先执行SQL脚本初始化表结构和测试数据。脚本在开发前要仔细检查索引house.status、contract.end_date、bill.status这些高频查询字段都建了索引。列表筛选和到期提醒会反复用到这些字段没有索引数据量上来之后查询会明显变慢。6.2 application.yml核心配置解析server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/rent?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl jwt: secret: your-secret-key-please-change-in-production expire: 7200三个配置细节必须说清楚第一连接URL里characterEncodingutf8少写了中文可能乱码第二serverTimezoneAsia/Shanghai必须加MySQL 8.0之后对时区敏感不加要么连不上要么时间差8小时第三MyBatis的log-impl开发环境开启方便看SQL排查问题生产环境务必关掉避免日志泄露用户相关数据和SQL细节。6.3 前端打包与Nginx部署前端开发完不能直接把源码丢到服务器。用npm run build打包Vue CLI会把页面编译成静态资源输出到dist目录。部署时将整个dist目录放到Nginx站点根目录再配置/api请求反向代理到后端server { listen 80; server_name your-domain.com; root /var/www/rent/dist; index 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 /var/www/rent/upload/; } location / { try_files $uri $uri/ /index.html; } }三个location各有用途。/api/反代解决跨域这是生产环境最推荐的方式/upload/直接把图片目录暴露出去图片URL就是http://域名/upload/xxx.jpg最后try_files配置是为了解决Vue Router history模式的刷新404问题。少配任何一个前端都可能出现首次打开正常刷新就白屏的尴尬状况。6.4 打包部署踩过的具体坑部署过程印象最深的坑是SpringBoot打包。IDE里运行一切正常一打包成jar就报No MyBatis mapper was found。查了半天发现是mapper的XML文件没被打进jar。原因很简单MyBatis XML放在src/main/resources/mapper下Maven打包时资源过滤配置不对XML就漏了。解决方式是在pom.xml显式配置resources路径。这个坑在做项目打包时非常典型几乎每次换机器重打包都可能踩一次。另一个坑是Tomcat临时目录导致图片上传失败。项目部署到服务器后上传图片一直报磁盘空间不足的错但磁盘明明还有大量空间。后来发现是Tomcat临时目录满了重启Tomcat清理临时文件后恢复。根因是系统/tmp目录被其他进程挤占。处理方式是修改spring.servlet.multipart.location配置指向项目独立的upload临时目录不依赖系统默认临时目录。7. 实战排坑六个典型问题与排查思路7.1 问题速查表把开发过程中遇到的典型问题整理成了一张速查表建议收藏现象可能原因排查思路登录接口一直报401Token校验失败或漏传Token检查请求头是否带Authorization检查JWT密钥是否一致检查拦截器放行路径数据库中文乱码连接字符集没配置检查连接URL是否带characterEncodingutf8检查表字符集查询结果全是nullMyBatis驼峰映射未开启检查map-underscore-to-camel-case配置上传图片404上传目录未映射或路径错误检查Nginx /upload/的alias路径检查后端存储路径页面刷新后404Vue Router history模式未配置配置Nginx try_files回退到index.html接口跨域报错前后端端口不一致开发环境后端开启CORS生产环境用Nginx反代7.2 跨域问题最容易被忽略跨域在前后端分离项目里几乎必踩。一开始我用Vue CLI的代理方式解决在vue.config.js里配置devServer.proxy把请求转发到后端前端代码里统一用相对路径/api。这样浏览器看到的是同源请求不会有跨域。后来我改成了后端直接开启CORS。因为代理方式只对开发环境生效生产环境依赖Nginx或者后端支持。思来想去后端配置CORS才是通用方案。registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600);注意一个坑这里用的是allowedOriginPatterns而不是allowedOrigins。前者支持通配符且能和allowCredentials(true)共存后者不支持通配符混用会直接报错。7.3 时间差8小时的问题上线后业务方反馈账单日期显示不对一查差了8个小时。原因就是MySQL连接串没配serverTimezoneAsia/Shanghai默认取了UTC时间。加上之后问题消失。但这个坑还有变种如果数据库服务器系统时区本身是UTC光在连接串指定还不够需要同步修改MySQL的global time_zone参数。国内环境把两处都设成东八区基本就稳了。7.4 排错顺序的通用习惯经验不深的时候遇到bug第一反应是翻后端日志这个习惯没毛病但有更省时间的做法先看浏览器F12的Network请求。很多时候问题根本不在后端而是前端请求本身就没发对——参数名不对、请求方式不对、漏了Token、URL拼错。先在Network里确认请求体和响应体再决定要不要翻后端日志至少能省一半排查时间。后端排查顺序也一样先看是不是参数校验失败返回了错误信息再看SQL执行是否报错最后才怀疑业务逻辑逻辑。大部分问题靠日志里的SQL和参数就能一眼定位不需要反复打断点调试。8. 项目做完之后的几点实际体会做这个房屋租赁管理系统的过程让我对设计一个能用、好用的系统有了更具体的理解。首先表设计阶段多花一天后面能省一周。实体关系理清楚、状态字段类型和含义约定明白开发时就不会一边写代码一边返工。状态枚举要在项目启动前就想清楚房源状态、合同状态、账单状态都用数字或固定字符串常量不要随手存是/否上架/下架这类不统一的描述。其次接口返回结构统一、前端请求封装规范这两件事带来效率提升被很多人低估。看起来多写了一些样板代码但在联调、排错、扩展的时候收益是几何级的。再一个体会是权限设计要前置。角色划分、接口拦截、路由守卫这些工作在项目初期确定下来后面加新功能时只需要按照既定模式添加即可不会出现功能都写完了才发现权限漏洞要返工的情况。最后说一个扩展方向。如果想继续做可以增加移动端H5版本复用的还是这一套后端接口也可以把房源图片迁移到对象存储把到期提醒做成定时任务推送消息。不过这些都是后话先把当前系统跑稳、核心逻辑做对比什么都重要。
阅读完成 · 觉得有帮助?