1. 项目整体设计与技术选型思路
1.1 为什么坚持做前后端分离
这两年带过的做毕设和练手项目的学生里,越来越多的人选择房屋租赁管理系统作为实战课题,原因也简单:业务模型贴近真实世界,功能量级适中,既不像商城系统那样牵涉支付、物流一堆外围服务,也不像纯CRUD的管理后台那样单薄,撑不起一个完整的项目展示。而在具体落地时,只要条件允许,我基本都会建议走前后端分离的路线。
前后端分离的好处,用大白话说就是把"页面怎么显示"和"业务怎么处理"分开成两个独立工程。传统单体写法用JSP或者Thymeleaf,后端Java代码里嵌HTML,前端工程师改个样式要动后端工程,后端改个接口返回结构前端页面可能直接崩。两拨人协作时天天得为"这个字段谁负责"吵架。而分离之后,前端Vue工程只关心数据和交互,后端SpringBoot工程只提供JSON接口,两者通过HTTP协议沟通,等于把"装修"和"承重结构"彻底拆分——装修队不用等楼盖完才能进场,承重墙出问题也不会牵连整个装修方案返工。
另外一个现实的好处是部署和维护的灵活性。房租管理系统往往要跑在很多场景里:一个中介门店的内部管理系统、一个公寓运营方的小型SaaS、或者一个高校实训项目的演示环境。分离后,前端构建产物是一堆静态文件,随便扔在Nginx或者对象存储上就能跑;后端是一个独立Java进程,升级后端不影响前端缓存,更新页面功能也不用重新部署服务端。这对小团队的迭代节奏来说非常友好。
1.2 为什么是SpringBoot + Vue + MyBatis + MySQL这套组合
技术选型这件事,我一直坚持一个原则:不是选最潮的,而是选最成熟、文档最全、坑最少的组合。这套SpringBoot + Vue + MyBatis + MySQL的搭配,在中小型管理系统领域可以说是"标准答案"级别。
先看SpringBoot。它本质上把Spring繁重的XML配置做了大刀阔斧的简化,内嵌Tomcat让项目从写完到跑起来只需要一条命令。对于房屋租赁这种需要快速迭代的管理系统,SpringBoot的自动配置和Starter生态能省掉大量重复劳动——比如引入spring-boot-starter-web就自带了MVC和JSON序列化,引入mybatis-spring-boot-starter就自动帮你配置好SqlSessionFactory。不需要在环境搭建上浪费时间。
再看Vue。为什么不用React?不是React不好,而是Vue对中小型项目更"亲民"。它的模板语法贴近传统HTML写法,指令系统(v-if、v-for、v-model)上手门槛低,一个Java后端开发即使没系统学过前端,花几天看一遍官方文档就能写出像样的页面。而且Vue生态的Element UI / Element Plus组件库,表格、表单、弹窗、分页、日期选择器这些管理系统的高频组件都是现成的,搭出后台管理界面跟拼积木差不多。
MyBatis和JPA之争,在管理类系统里我站MyBatis。核心理由是SQL可控性。房屋租赁系统里有大量报表查询、多条件筛选、月度账单生成这类复杂逻辑,用JPA的自动SQL很难精细控制,排错也麻烦。MyBatis把SQL写在XML里,写完之后能复制出来在数据库客户端单独执行验证,出了问题一眼就能看到SQL本身写得对不对。团队里如果既有高手又有新手,"SQL可见即所得"的容错性明显更好。
MySQL就更不用说了,稳定、免费、资料多、会的人也多。一台普通服务器跑这种量级的系统绰绰有余。这套组合总结下来就四个字:稳、快、省、熟。
1.3 系统功能规划
做管理系统最忌讳一上来就猛怼功能,然后做着做着发现控制不住复杂度。我的习惯是先画清楚"谁来用、用哪些功能、流程怎么走"。
这个房屋租赁管理系统,我规划了两端角色:管理员端给房管员使用,租客端给签约租客使用。管理员端核心功能包括:
- 房源管理:楼栋、楼层、房号、户型、面积、朝向、租金单价、押金规则、房屋状态(空置/已租/预定/维护中)。房源是系统的"地基"资产,所有其他业务都围绕它转。
- 租客管理:租客基本信息、证件信息、联系方式、历史租住记录,支持从Excel批量导入初始数据。
- 合同管理:签约、续签、变更备案、退租登记。合同关联房屋和租客,是"人"与"房"的绑定凭证。
- 账单管理:月度账单自动生成、租金催缴、费用调整、实收登记。这是整个系统资金流的"命脉",也是业务上最容易出错的地方。
- 退租管理:退租申请、房间验收、押金抵扣计算、退款登记。
- 统计报表:出租率、租金收入按月趋势、到期合同预警、欠费清单。这些数据给经营决策用。
- 系统管理:管理员账号管理、角色权限配置、操作日志审计、数据字典维护。
租客端功能相对轻量:查看自己名下合同、查看账单并在线登记缴费状态、发起退租申请、提交维修工单、查看公告。
这个功能矩阵覆盖了房屋租赁业务的主链路:房源录入 -> 租客建档 -> 签约入住 -> 按月收费 -> 到期续签或退租。每一步都是前后端联动的完整闭环,也方便在简历里讲清楚自己做了哪些事。
2. 数据库设计与核心表结构
2.1 核心实体梳理
说句实在话,数据库设计如果一开始没想清楚,后面写代码时每写一步都是在给自己挖坑。我这次在正式写代码前,先花了整整一个下午梳理实体关系和字段规划。不夸张地说,这个环节占整个项目20%的时间,但它决定了后面80%的顺畅度。
核心实体我列了这几张:房屋信息表(house)、租客表(tenant)、租赁合同表(contract)、月度账单表(bill)、缴费记录表(bill_record)、维修工单表(repair_order)、退租交接表(checkout)、系统用户表(sys_user)、角色表(sys_role)、用户角色关联表(sys_user_role)、操作日志表(sys_log)。
这几张表之间的关系是:房屋和租客是多对多通过合同绑定的——一个房屋在不同时间可以被不同租客租住,一个租客在不同时间也可以租不同房屋。合同和房屋是多对一,合同和租客是多对一。一张合同在有效期内对应每个月份产生一张账单,账单和缴费记录是一对多。退租记录关联合同,维修工单关联合同和房屋。整个模型就是围绕"房屋-合同-账单"这条主干展开的。
用生活化的话理解:房屋是"货架上的商品",租客是"顾客",合同是"购买凭证",而账单是"每月寄到家里的水电缴费单"。商品可以换顾客,顾客可以买不同商品,这些都通过凭证来约束,账单则承载了资金往来的细节。
2.2 房屋与合同表设计细节
先看房屋表(house),我设计的核心字段如下:
- house_id:主键,自增
- house_no:房号,比如 3-2-501 表示三栋二单元501,作为业务标识加唯一索引
- building_no:栋号(冗余存储,方便按栋筛选)
- unit_no:单元号
- floor:楼层
- house_type:户型,如"两室一厅"
- area:面积,DECIMAL(6,2),单位平方米
- orientation:朝向,如南/北/东西
- monthly_rent:月租金,DECIMAL(10,2)
- deposit_months:押金月数,通常1或2
- status:房屋状态,用TINYINT,0空置、1已租、2预定、3维护中
- decoration:装修情况(简装/精装/豪华)
- create_time、update_time,保留审计字段
这里两个细节需要强调:第一,house_no 加唯一索引非常关键,这是业务上定位房屋的天然业务主键。系统里后续合同、账单、维修工单都频繁通过房号关联,如果只靠自增ID,实际使用场景里查数据要先记住ID,很别扭。第二,status字段用TINYINT而非字符串,省空间是一方面,更重要的是避免字符串拼写错误导致的数据脏乱,配合代码里的枚举映射,可读性和稳定性都能兼顾。
合同表(contract)的设计则是整个系统的业务核心,字段我定成这样:
- contract_id:主键
- contract_no:合同编号,格式如HT202501001,唯一索引
- house_id:外键,关联house表
- tenant_id:外键,关联tenant表
- sign_date:签约日期
- start_date:租期开始日期
- end_date:租期结束日期
- monthly_rent:合同月租(与house表当前月租可能不同,比如中途改价,以合同截存的为准)
- deposit_amount:押金金额
- rent_day:每月交租日,默认1号或5号
- fee_type:计费类型,如月付/季付/年付
- late_fee_rate:违约金百分比(逾期每日万分之五之类)
- status:合同状态,1生效中、2已到期、3已退租、4已作废
- remark:备注字段
合同表里我把月租金和押金金额都作为冗余字段存储,而不是动态关联房屋表。这个设计是为了防止"历史合同跟着房屋调价一起变"的问题——合同一旦签署,租金金额就以合同上的数字为准,哪怕后续房屋租金调整,老合同依然按旧价执行。这一点做管理系统时很多人容易忽略,结果做好后回查历史账目对不上,非常头疼。
2.3 账单与流程类表设计
账单表(bill)是资金流的核心,设计时花的心思最多。核心字段:
- bill_id:主键
- contract_id:关联合同
- bill_month:账单归属月份,格式"2025-01"
- amount:应缴金额,DECIMAL(10,2)
- paid_amount:实缴金额
- status:账单状态,0待缴、1部分缴纳、2已缴清、3逾期
- due_date:最晚缴纳日期,通常是当月25日
- generate_time:生成时间
- finish_time:结清时间
关键设计点有两个:第一是唯一索引 (contract_id, bill_month),这保证了一份合同每个月份只能生成一条账单。后面的定时任务生成账单,必须依赖这个唯一约束来防止重复数据——不然定时任务一旦重跑,账单爆出一堆重复记录,整个资金流全乱了。第二是状态字段和实缴金额拆开,因为"已缴清"和"部分缴纳"是完全不同的资金状态,必须能精细表达。很多初学设计的同学只设计一个"是否缴纳"布尔字段,季度付这种模式一旦出现就会数据失真。
退租表(checkout)核心字段:checkout_id、contract_id、checkout_date、check_deposit_return(应退押金)、deduct_amount(扣款金额)、deduct_reason(扣款原因,如墙面破损、水电欠费)、status(待审批/已完成)。这张表把退租这个"动作"沉淀为可审计的"数据",方便追溯每个退租事件的资金处理过程。维修工单表(repair_order)则比较简单:repair_id、house_id、tenant_id、repair_type、description、status(待处理/处理中/已完成)、handler_name、handle_time。
这套表结构设计完成后,我在纸上画了一下主流程的数据流转:房源新增后在house表出现一条空置记录;签约时house状态改成已租,contract表新增生效合同;每月1号定时任务扫描生效合同生成当月bill;租客缴费后bill_record表新增缴费记录,bill状态同步更新;到期后走退租流程,checkout表登记。整条链路逻辑闭合,后面编码的时候基本没有推倒重来过。
3. 后端核心实现与关键细节
3.1 SpringBoot项目结构与分层
后端工程我用的是标准的分层结构,包名按功能模块组织,而不是简单地按技术栈。具体是这样:
com.example.rental ├── common # 公共模块:统一返回体、全局异常处理、常量定义 ├── config # 配置类:拦截器注册、跨域配置、定时任务开关 ├── controller # 控制层:接收请求、参数校验、调用service ├── service # 业务层:核心业务逻辑,service + impl实现 ├── mapper # 数据访问层:MyBatis接口定义 ├── entity # 数据库实体 ├── dto # 请求/响应数据对象 └── utils # 工具类:JWT工具、加密工具、Excel工具这种分层的核心价值是职责单一:controller只做"翻译"——把HTTP请求参数转成对象,把service的返回值封装成统一响应;service只做"业务"——租约合法性校验、账单状态流转、金额计算;mapper只做"数据"——根据条件查库、写库。我自己见过不少初始项目把业务逻辑写在controller里的,短平快一时爽,等后面加权限、加校验、加审计的时候,改起来痛苦得想删库重写。
统一返回体我设计的结构是{ "code": 200, "message": "success", "data": ... },所有接口的返回都走这个壳,前端axios拦截器统一处理,极大简化了错误分支的判断。code约定为:200成功、400参数错误、401未登录/登录过期、403无权限、500系统异常。这里注意业务上"登录失败"用500还是用自定义业务码?我建议非2xx都用自定义业务码,而不是HTTP状态码——因为HTTP状态码在Nginx和后端网关层容易混淆,而JSON里的code是前后端约定,语义清晰、排查方便。
3.2 登录鉴权与权限控制
房屋租赁系统的权限模型不复杂,我用的是JWT + 拦截器的经典方案。
流程梳理一下:用户提交账号密码,后端校验成功后用HMAC算法签发一个JWT,里面放入userId、username、role三个核心claim,设置过期时间24小时。前端拿到token存到localStorage,每次axios请求在请求拦截器里往head加Authorization: Bearer xxx。后端注册一个拦截器,放行白名单接口(登录、注册、静态资源),其余接口统一校验token。
这里有几个实现细节值得重点说。第一,token校验不能只检查"能解析出来"。很多初学的人写完解析就完事,但某人的token被另一个普通用户拿去,后端也放行,这就是权限漏洞。我的拦截器里校验完token后,会把当前用户user_id和角色塞进ThreadLocal,后续service层如果需要校验"这个操作只能由管理员执行",直接从ThreadLocal取角色判断即可,不用每次从数据库查,性能好也干净。第二,密码加密用BCrypt而不是MD5。MD5加盐在现在看来已经不够安全,而BCrypt是自适应算法,能自动抵抗暴力拆解。Spring Security里自带BCryptPasswordEncoder,可以单独拿来做密码的哈希工具。第三,被拦截器校验的接口里,如果token过期,我统一返回code=401,前端响应拦截器捕获到401后跳转登录页,并给出"登录已过期,请重新登录"的提示,这个体验细节要跟上。
动态权限这块,考虑到系统不算大,我用的是基于注解的权限校验:@RequiresRole("admin")标注在需要管理员权限的controller方法上。实现方式是在拦截器里解析注解,如果当前用户角色不匹配,直接返回403。这个设计比用Spring Security全家桶轻量得多——毕竟租管系统真的需要区分角色的操作也就十来个接口,引入整套权限框架反而是杀鸡用牛刀。
3.3 MyBatis查询与复杂业务实现
MyBatis这块是整个后端实现里最体现"功底"的部分。先说一个最容易踩坑的点:实体类的属性命名和数据库字段命名不一致导致映射失败。常见的坑是数据库字段叫house_type,Java属性叫houseType,默认映射对不上,查出来全是null。解决方案是在application.yml里开启驼峰映射:mybatis.configuration.map-underscore-to-camel-case: true,这一行配置就能解决90%的映射问题。
房源列表的分页查询,我用了PageHelper插件,它最大的好处是用法简单——查询前调用PageHelper.startPage(pageNum, pageSize),紧跟的Mapper查询自动变成分页SQL,完美对接前端传来的页码和每页条数。但注意一个非常隐蔽的问题:PageHelper必须在紧跟着的Mapper方法上使用,中间不能插入其他查询。我见过有人这么写:
PageHelper.startPage(pageNum, pageSize); someOtherMapper.querySomething(); // 这行会导致PageHelper被"截胡" houseMapper.queryHouseList(param);这会导致分页信息取错,页面数据总数和列表内容对不上。排查起来非常隐蔽,我当初也被坑了小半天才回过神来。
多条件筛选的SQL我用了MyBatis的动态<where>标签,写法是这样:
<select id="selectHouseList" resultType="House"> select * from house <where> <if test="houseType != null and houseType != ''"> and house_type = #{houseType} </if> <if test="status != null"> and status = #{status} </if> <if test="minRent != null"> and monthly_rent >= #{minRent} </if> <if test="maxRent != null"> and monthly_rent <= #{maxRent} </if> </where> order by create_time desc </select>这样前端传什么条件就拼什么SQL,不传就查全量。这里的>和<是XML转义,小于号和大于号在XML里必须转义,这是新手写MyBatis XML最容易报错的地方。
合同到期提醒的SQL,用DATE_SUB函数直接查30天内到期的合同:
select contract_id, contract_no, tenant_id, end_date from contract where status = 1 and end_date between CURDATE() and DATE_SUB(CURDATE(), INTERVAL -30 DAY)前端合同列表里高亮剩余天数不足30天的记录,运营人员每天打开系统就能看到哪些合同下个月需要联系续签。这个功能虽然实现起来只有一行SQL,但对业务的实用价值非常明显。
统计报表的SQL是重头戏。月度租金收入按合同表的生效时间和金额聚合:
select DATE_FORMAT(start_date, '%Y-%m') as month, sum(monthly_rent) as total_rent from contract where status = 1 group by DATE_FORMAT(start_date, '%Y-%m') order by month出租率则是实时计算已租房屋数除以总房源数,这个用两个count查出来在Java里做除法就行,不必强行用一条SQL拼。
3.4 定时任务生成月度账单
月度账单的自动生成是整个后端最核心的"自动化"逻辑,我单独讲一下。
实现方式是Spring的@Scheduled注解,配置cron表达式在每个自然月1号凌晨2点执行:
@Scheduled(cron = "0 0 2 1 * ?") public void generateMonthlyBills() { List<Contract> activeContracts = contractMapper.selectByStatus(1); for (Contract contract : activeContracts) { Bill bill = new Bill(); bill.setContractId(contract.getContractId()); bill.setBillMonth(previousMonth()); // 上个月 bill.setAmount(contract.getMonthlyRent()); bill.setStatus(0); bill.setDueDate(calculateDueDate()); try { billMapper.insert(bill); } catch (DuplicateKeyException e) { log.warn("账单已存在,合同ID: {}", contract.getContractId()); } } }这个实现里有个关键心得:金额不直接用currentDate算,必须取"上月"作为账单归属月份。因为每月1号生成的是上个月的费用——比如2月1号跑定时任务,应该生成1月份的账单。用LocalDate.now().minusMonths(1)拿到归属月份,再配合合同里约定的计费方式,逻辑才是自洽的。
幂等性是这里的重头戏。定时任务如果因为部署重启、人为手动触发等原因重复执行,重复插入的账单就变成了资金数据的"脏账"。我的方案就是在bill表上建了(contract_id, bill_month)唯一索引,插入时如果撞了唯一索引就捕获DuplicateKeyException,跳过这条合同,输出告警日志。这样重复执行最多造成几条无效警告日志,不会污染数据。这个幂等方案虽然不及引入消息队列的事件驱动方案高大上,但在单体应用里是性价比最高、最好排查的解法。实际线上跑了几个月的验证,效果很稳。
4. 前端Vue实现与前后端联调
4.1 前端项目结构与工程化
前端我用Vue CLI创建的Vue 2项目,搭配Element UI组件库。说实在的,Vue 3 + Vite + Element Plus当然是更新的方案,但考虑到当前后端开发者最常见的熟悉度,以及组件库资料成熟度,Vue 2 + Element UI在现网管理系统的存量仍然巨大,学起来也不吃亏。这里做个技术取舍,如果你从零开始学,直接用Vue 3 + Element Plus也行,语法基本互通,差异不大。
目录结构我这样组织:
src ├── api # 接口请求封装,一个模块一个文件 ├── assets # 静态资源 ├── components # 公共组件 ├── router # 路由配置 ├── store # Vuex状态管理 ├── utils # 工具函数,request.js放axios封装 └── views # 页面组件,按模块分文件夹路由用的是懒加载 + 嵌套路由。外层布局组件套一层侧边栏和头部导航,内层是各个功能页。这样做的体验好处是刷新页面时不会白屏闪一下,首屏加载速度也快。懒加载写法:
const HouseManage = () => import('@/views/house/HouseManage.vue')4.2 Axios封装与接口对接
axios封装是整个前端联调的"心脏",设计得好能省下无数重复代码。我在utils/request.js里做了两件事:
第一是请求拦截器,自动加token:
service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config })第二是响应拦截器,统一处理后端返回的code:
service.interceptors.response.use( response => { const res = response.data if (res.code === 200) { return res } else if (res.code === 401) { localStorage.removeItem('token') router.push('/login') return Promise.reject(new Error(res.message)) } else { Message.error(res.message || '操作失败') return Promise.reject(new Error(res.message)) } }, error => { Message.error('网络异常,请稍后重试') return Promise.reject(error) } )这样前端页面里调用接口只需要关心成功分支的data数据,错误和异常全部被拦截器兜住。开发效率和代码整洁度都提升一个档次。另外我在开发环境配置了Vue CLI的devServer.proxy,把/api前缀的请求代理到本地后端的8080端口,这样本地调试时浏览器里不存在跨域问题。说到这个,跨域问题在校验部署时也是一大考点,后面部署章节细讲。
4.3 页面核心实现思路
房源管理页是系统的主页面,整体是"表格 + 搜索表单 + 分页 + 新增/编辑弹窗"的经典结构。表格里我重点处理了两个细节:房屋状态字段用el-tag渲染成不同颜色——空置是浅灰色标签、已租绿色、预定橙色、维护红色,一眼就能看出房源状态分布,比纯文本字段直观得多。另外操作列里根据不同状态动态显示不同按钮:空置房显示"签约"按钮,已租房显示"查看合同""退租提醒"按钮。
合同管理页我花了最多心思。新增合同功能是一个大弹窗,里面分三个区块:第一步选房屋(用带分页的表格,只展示空置房源并锁定状态)、第二步选租客(支持按姓名或手机号搜索)、第三步填合同信息(起止日期、月租、押金月数、交租日)。第三步里日期选择器用了el-date-picker带快捷面板,这样操作人能快速选定一个月后、三个月后这类常用周期,实际房管员录入合同时效率提升明显。
账单管理页的核心是"结账操作"和"导出Excel"。结账操作本质上把账单状态从待缴改为已缴清,同时插入一条缴费记录,前端用Dialog收集本次实缴金额和缴费时间,校验金额不超过应缴金额。导出Excel我用了前端库xlsx,做法是把表格数据通过XLSX.utils.json_to_sheet转换成工作簿,再调用XLSX.writeFile下载。这套方案不用后端介入,纯前端轻量实现,对账单量不大的场景完全够用。
统计报表页用ECharts图表组件。我在views/statistics/Statistics.vue里,通过一个折线图展示近6个月租金收入趋势,一个柱状图展示各户型出租率,外加几个统计卡片显示当前总房源数、在租房源数、本月应收金额、本月逾期金额。数据是从后端的统计接口拿的,ECharts只要拿到字段映射进option就能出图。这里一个小技巧:折线图X轴月份连续时,如果某个月份数据为0,后端返回的JSON不能只返回有数据的月份列表,要返回完整12个月补0,不然图表会出现断点,不美观也不真实。
4.4 权限控制与菜单动态渲染
前端权限控制我用的是路由守卫配合角色字段。router.beforeEach里做三件事:
第一,判断目标路由是否需要登录(通过meta.requiresAuth),如果需要且没有token,直接redirect到登录页并带上redirect参数。第二,用户已经登录但还没拉取用户信息时,先调用getUserInfo接口拿回用户角色,存到Vuex里。第三,根据角色过滤菜单:管理员展示全部菜单,普通操作员隐藏系统管理模块的路由和菜单项。
菜单这块我用动态渲染:侧边栏菜单的配置数据放路由meta里,登录后根据角色过滤meta.permission,然后v-for渲染到el-menu。这样实现的好处是权限逻辑集中在一处,后续增加新菜单只需要在路由配置上标注需要的角色,代码零改动。
这里要提一个我在实际开发里常被新手问到的点:前端做了路由守卫和菜单过滤,后端是不是可以不做权限校验?答案是绝对不行。前端控制权限只是为了用户体验——隐藏掉没权限的入口;真正的安全边界必须由后端每个接口做校验。只要后端校验做好,即使有人绕过前端直接调接口也拿不到数据。这个观念希望读者在看这篇文章时就牢牢记下来,很多初学项目被认为"不安全"就是栽在这一环。
5. 完整部署教程(从零到跑通)
5.1 本地开发环境准备
部署这套系统前,先确认本机的环境工具箱是否齐全:
- JDK:1.8或11都可以,推荐1.8,稳定。命令行执行
java -version确认。 - Maven:3.6版本以上,执行
mvn -v确认。 - Node.js:14以上版本,执行
node -v确认。 - MySQL:5.7或8.0,本教程以8.0为例,执行
mysql -uroot -p进入命令行确认。 - Nginx:Windows版或Linux版按系统对应安装。
推荐直接用IDEA作为后端IDE,VSCode或IDEA写前端都行。环境变量配置这块,Windows用户把JDK和Maven的bin目录加进PATH;macOS和Linux建议装个sdkman或者直接用brew安装,版本管理方便。这里我不详细展开每种系统的环境变量设置,网上资料多得是,遇到报错就报错信息去查,基本都能解决。
5.2 后端配置与启动
第一步把项目源码里sql目录下的rental_db.sql导入MySQL。用Navicat、DataGrip或者命令行都行,确保建出来的库名字是rental_db,字符集用utf8mb4,排序规则用utf8mb4_general_ci或utf8mb4_unicode_ci都可以。数据初始化后可以用下面命令快速验证:
mysql -uroot -p rental_db < rental_db.sql mysql -uroot -p -e "use rental_db; show tables;"如果看到十几张表都在,说明导入成功。
第二步修改application.yml里的数据源配置:
spring: datasource: url: jdbc:mysql://localhost:3306/rental_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的数据库密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true注意URL里serverTimezone=Asia/Shanghai这一项,MySQL 8.0默认时区是UTC,如果缺了这项,启动项目就会报The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized这类时区错误。这是新手频繁踩的坑。
第三步打后端jar包并启动。在项目根目录执行:
mvn clean package -DskipTests构建完成后在target目录下生成rental-server-1.0.0.jar。本地调试可以直接在IDEA里运行Application类,生产或演示环境则推荐后台运行:
nohup java -jar rental-server-1.0.0.jar > app.log 2>&1 &注意日志重定向到app.log,方便出问题排查日志。启动后访问http://localhost:8080/api/system/health,返回{"code":200,"message":"success","data":"ok"}就代表后端已经跑通。
5.3 前端构建与部署
前端部署前先安装依赖:
cd rental-web npm install如果npm install因为网络原因很慢甚至失败,可以换成国内镜像源,执行npm config set registry https://registry.npmmirror.com后再装一次。npm这块的坑就是网络和版本,装完依赖后启动开发模式调试,npm run serve本地起一个开发服务器。调试确认功能无误后开始构建生产包:
npm run build构建输出在dist目录下,一堆静态文件。前端部署我推荐用Nginx托管,同时让Nginx把/api前缀的请求反向代理到后端Java进程,这样前端和后端对外是同一个域名和端口,不存在跨域问题,也符合实际生产环境的标准做法。Nginx关键配置如下:
server { listen 80; server_name localhost; # 前端静态资源 location / { root /opt/rental-web/dist; index index.html; try_files $uri $uri/ /index.html; # 解决前端路由刷新404问题 } # 后端接口反向代理 location /api/ { proxy_pass http://localhost:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这个配置里有几个关键点值得重点说明。第一,try_files $uri $uri/ /index.html;这行,对Vue单页应用必不可少。前端路由在刷新页面时会请求/house/manage这类路径,如果没有这个配置,Nginx直接在dist目录里找house/manage.html,找不到就返回404,刷新页面就白屏。加上try_files后,所有路径都回退到index.html,由前端路由接管。这个坑我见过太多次了,部署上线后用户一刷新就白屏,十有八九是缺了这行配置。第二,proxy_pass的URI结尾带不带/是有讲究的。写http://localhost:8080/api/;,Nginx转发时会把原始URI完整拼上去,也就是/api/xxx会原封不动传给后端,前端请求的/api前缀在后端接口层已经兼容了,所以这样最省事。第三,修改Nginx配置后要执行nginx -s reload让配置生效,而不是重启进程,避免正在处理的请求断开。
5.4 全流程冒烟自测清单
部署完成,我会按下面清单快速验证一遍,确认所有功能链路OK:
- 打开浏览器访问
http://your-server-ip/,能看到登录页,输入管理员账号登录成功。 - 系统首页仪表盘能正确显示总房源数、在租数、本月应收金额。
- 房源管理页打开列表,分页和筛选正常,新增一条房源后状态正确。
- 合同管理页新建一条合同,选房、选人、填日期,保存成功后房源状态自动变为已租。
- 账单管理页看到合同对应月份的账单自动生成(模拟测试时也可以手动触发一次生成接口)。
- 退租流程走一遍,押金计算和扣款逻辑没有报错。
- 统计报表的图表有数据,日期维度切换正常。
- 用普通操作员账号登录,系统管理菜单不可见;直接访问后台系统管理URL也会被后端拦截返回403。
所有项通过,整套系统就算部署完毕,交到使用方手里可以放心跑了。
6. 常见问题与排查技巧实录
6.1 前后端联调经典坑
联调阶段跑的坑,我列几个排得上号的,每个都是实战中真实踩过:
跨域问题。本地开发时前端和后端不同端口,如果不配置代理,浏览器直接发请求会报No 'Access-Control-Allow-Origin' header is present on the requested resource。我对开发环境的解法是优先用webpack devServer proxy,而不是在后端加CORS配置。原因在于CORS配置一旦处理不当会引发更隐蔽的问题(如预检请求失败、带cookie时跨域限制等)。但如果是前后端完全分离部署在不同域名下,那就必须后端配合CORS了,这时可以写一个WebMvcConfigurer里配置addCorsMappings,允许来自你的前端域名即可。
路径404或者405。前端调的接口路径和后端@RequestMapping里的路径不一致,最常见的错误是漏写/api前缀,或者方法类型不对(前端POST、后端GET)。排查方式是打开浏览器DevTools的NetWork面板,看请求的URL、Method、状态码,基本一眼就能定位。
中文乱码。入参中文乱码通常是前端没有设置charset=UTF-8,axios请求头补上Content-Type: application/json; charset=utf-8即可。查询结果中文乱码则是MySQL连接时characterEncoding设置不正确,检查URL里useUnicode=true&characterEncoding=utf8是否齐全。
6.2 数据库与时区问题
MySQL 8.0的时区坑,在前面部署章节已经提过。另一个容易踩的坑是SQL的ONLY_FULL_GROUP_BY问题。MySQL 5.7及以上版本默认开启了ONLY_FULL_GROUP_BY模式,SELECT中出现的字段必须都在GROUP BY子句中或者被聚合函数包裹。我做统计SQL时写了个select house_type, count(*) from house group by house_type,这是合法的;但如果写了select house_id, house_type, count(*) from house group by house_type就会报错,因为house_id不在group by里。这种报错初看莫名其妙,实际一查就明白是SQL模式约束。解决办法有两种:要么严格遵守SQL规范改写语句;要么在mysql配置里SET sql_mode=''关掉严格模式(不推荐,治标不治本且可能掩盖真正错误)。
6.3 权限与安全常见问题
登录鉴权常遇到的一个坑:token过期后页面没有及时跳回登录页。前端响应拦截器已经有了401跳转逻辑,但注意后端拦截器校验token时返回的状态码必须是JSON里的"code": 401,而不是HTTP的401状态码——因为前端响应拦截器里拿到的error.response.status如果是401,会走catch分支而不是response的code分支。这里前后端约定一定要一致,否则拦截逻辑永远不生效。我的做法是后端token过期时既返回HTTP 401,又在JSON body里带"code": 401,前端就可以在响应拦截器里同时兼容两种情况,万无一失。
另一个安全实践细节:密码绝对不能明文存储。这个系统使用的BCrypt哈希方案,注册时encode一次,登录校验时用matches比对。即使数据库泄露,攻击者拿到的是一串不可逆的哈希,而不是明文密码,爆破成本极高。我见过有人图省事甚至把密码直接存在表里,这是对自己作品和用户数据严重不负责任的做法。做任何系统,这块底线都不能破。
最后再提一个看起来小但实际很影响使用感的坑:操作日志的埋点数量。租管系统涉及资金操作(收租、退押金)、合同变更等重要动作,一定要写进操作日志表,记录操作人、时间、操作类型、关键参数。一方面这是安全审计的硬需求,另一方面一旦数据出问题,能靠日志快速定位是谁、在什么时候、做了哪个操作。我一开始只给核心写操作打了日志,后来运营方反馈"不知道某个合同是谁改的",这才补全了整个日志体系。做管理系统,日志从设计阶段就要当一等公民对待,不要等出事了才想起来补。
这个项目的源码和部署流程写到这,该讲的细节基本都覆盖到了。如果你拿到这套代码准备自己搭一遍,我的建议是别急着改功能,先把"录入房源 -> 签合同 -> 生成账单 -> 收租 -> 退租"这条主链路完整跑通,再考虑加需求。我最初开发这套系统时也是先完成主链路,再逐步补充权限、报表、操作日志这些外围能力,整个过程相对从容,遇到问题也能快速定位。希望这篇拆解能帮你避开我踩过的那些坑。