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或2status房屋状态用TINYINT0空置、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-01amount应缴金额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过期我统一返回code401前端响应拦截器捕获到401后跳转登录页并给出登录已过期请重新登录的提示这个体验细节要跟上。动态权限这块考虑到系统不算大我用的是基于注解的权限校验RequiresRole(admin)标注在需要管理员权限的controller方法上。实现方式是在拦截器里解析注解如果当前用户角色不匹配直接返回403。这个设计比用Spring Security全家桶轻量得多——毕竟租管系统真的需要区分角色的操作也就十来个接口引入整套权限框架反而是杀鸡用牛刀。3.3 MyBatis查询与复杂业务实现MyBatis这块是整个后端实现里最体现功底的部分。先说一个最容易踩坑的点实体类的属性命名和数据库字段命名不一致导致映射失败。常见的坑是数据库字段叫house_typeJava属性叫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 idselectHouseList resultTypeHouse select * from house where if testhouseType ! null and houseType ! and house_type #{houseType} /if if teststatus ! null and status #{status} /if if testminRent ! null and monthly_rent gt; #{minRent} /if if testmaxRent ! null and monthly_rent lt; #{maxRent} /if /where order by create_time desc /select这样前端传什么条件就拼什么SQL不传就查全量。这里的gt;和lt;是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() { ListContract 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里做了两件事第一是请求拦截器自动加tokenservice.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config })第二是响应拦截器统一处理后端返回的codeservice.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 本地开发环境准备部署这套系统前先确认本机的环境工具箱是否齐全JDK1.8或11都可以推荐1.8稳定。命令行执行java -version确认。Maven3.6版本以上执行mvn -v确认。Node.js14以上版本执行node -v确认。MySQL5.7或8.0本教程以8.0为例执行mysql -uroot -p进入命令行确认。NginxWindows版或Linux版按系统对应安装。推荐直接用IDEA作为后端IDEVSCode或IDEA写前端都行。环境变量配置这块Windows用户把JDK和Maven的bin目录加进PATHmacOS和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?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/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里serverTimezoneAsia/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 21 注意日志重定向到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、状态码基本一眼就能定位。中文乱码。入参中文乱码通常是前端没有设置charsetUTF-8axios请求头补上Content-Type: application/json; charsetutf-8即可。查询结果中文乱码则是MySQL连接时characterEncoding设置不正确检查URL里useUnicodetruecharacterEncodingutf8是否齐全。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比对。即使数据库泄露攻击者拿到的是一串不可逆的哈希而不是明文密码爆破成本极高。我见过有人图省事甚至把密码直接存在表里这是对自己作品和用户数据严重不负责任的做法。做任何系统这块底线都不能破。最后再提一个看起来小但实际很影响使用感的坑操作日志的埋点数量。租管系统涉及资金操作收租、退押金、合同变更等重要动作一定要写进操作日志表记录操作人、时间、操作类型、关键参数。一方面这是安全审计的硬需求另一方面一旦数据出问题能靠日志快速定位是谁、在什么时候、做了哪个操作。我一开始只给核心写操作打了日志后来运营方反馈不知道某个合同是谁改的这才补全了整个日志体系。做管理系统日志从设计阶段就要当一等公民对待不要等出事了才想起来补。这个项目的源码和部署流程写到这该讲的细节基本都覆盖到了。如果你拿到这套代码准备自己搭一遍我的建议是别急着改功能先把录入房源 - 签合同 - 生成账单 - 收租 - 退租这条主链路完整跑通再考虑加需求。我最初开发这套系统时也是先完成主链路再逐步补充权限、报表、操作日志这些外围能力整个过程相对从容遇到问题也能快速定位。希望这篇拆解能帮你避开我踩过的那些坑。