ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

SpringBoot+Vue+MyBatis企业级房屋租赁管理系统源码解析

SpringBoot+Vue+MyBatis企业级房屋租赁管理系统源码解析 做房屋租赁管理这类系统最怕的不是没有需求而是需求是一块一块长出来的。我前前后后接触过好几家做长租公寓和房屋中介的团队他们的日常是这样的几百间房源的租客信息、合同期限、收租记录全部摊在Excel里房租到期靠人肉翻日历空置房源挂在几个平台上同步月底做账能对到半夜。后来遇到这种情况我的标准答案不是去调研海外平台怎么设计而是直接给一套SpringBootVueMyBatisMySQL的完整系统源码让他们在自己的服务器上一跑所有租赁流程全部线上化。这套企业级房屋租赁管理系统前后端完整分离后端是SpringBootMyBatis数据库MySQL前端Vue3Element Plus。它不是一个只演示接口和管理员列表的教学Demo而是把房源、租客、合同、账单、收银、报修、角色权限都做成完整闭环的项目。正在做毕业设计或者想拿一个全栈项目练手的同学可以直接拿它当蓝本团队需要搭建自用租赁管理后台的也可以在这套源码上做二次开发。1. 项目背景与整体技术架构1.1 企业级租赁系统的核心需求拆解“企业级”三个字听上去很宏大但落到租赁业务其实很具体。核心是业务闭环和角色划分。我见过不少项目界面做得很漂亮但业务是断的合同签了房子状态还是空置租客退租了账单还在每个月自动生成月底统计应收和实收两边对不上。做这套系统时我一开始就做了需求拆解而不是直接写代码。拆解下来主要有四个点房源全生命周期从楼栋、房间建档开始经历空置、待租、签约、已租、到期、退租、维修每个状态都要有据可查谁操作的、什么时间操作的不能只是改个数字。合同与账单是闭环核心合同不是一张 PDF 存进去就完了它是租金的源头。合同生效要自动生成账单合同退租要把未缴账单结清账单逾期要有滞纳金规则。多角色协同管理员、财务、运营各自关注的东西不一样。财务要收银和对账运营要看空置和到期管理员要管账号和权限。企业级意味着不能所有账号进去都看到一模一样的菜单。统计分析房租收入、空置率、当月到期合同、收缴率月底能一键出报表。没有统计的租赁系统只能叫登记系统不能叫管理系统。需求拆解之后剩下的工作其实是把状态和链路设计好开发只是执行。1.2 为什么选SpringBootVueMyBatisMySQL这个技术栈组合老实说不是最时髦的但它是做这类业务系统最稳妥的方案。SpringBoot胜在生态成熟招人容易社区里能搜到的问题答案多。对企业项目来说可维护性比炫技重要。SpringBoot内置Tomcat打jar包就能跑部署成本低得可怜。这里要特别提醒一句如果团队环境还在用JDK8SpringBoot建议用2.7.x系列。Spring Boot 3以上至少要JDK17很多老机器和旧运维脚本不一定支持没必要为了追版本踩坑。MyBatis的选择理由也很直接。租赁业务里有大量对账统计SQL需要精确控制SQL的执行计划MyBatis的XML映射能把复杂SQL写得很直白也方便DBA直接review。JPA不是不好但遇到要手写统计SQL的场景JPA的抽象反而是一种阻碍。手写SQL虽然有工作量但性能可预期。Vue做后台管理系统的效率在国内几乎没有对手。Vue3Element Plus的组合表格、表单、日期选择器、弹窗、穿梭框都是现成的前端团队很容易上手开发速度非常快。MySQL在数据量没有到数百万级之前完全够用成本也低配合索引设计支撑几百套房源的租赁管理毫无压力。这套组合属于“下限很高、上限够用”的选择。1.3 项目结构总览源码仓库的组织方式直接影响二次开发的效率。我习惯把后端根包命名为com.rent按职责分层不按业务模块堆包controller接收请求、参数校验、返回统一响应service业务逻辑、事务控制、状态流转mapperMyBatis接口XML写在resources/mapper目录entity数据库实体dto请求参数模型vo视图模型返回给前端的数据结构config跨域配置、拦截器、定时任务配置等前端src目录也做了明确约定api按业务模块封装的接口方法views页面组件一个业务模块一个文件夹router路由配置storePinia状态管理utilsaxios实例、请求封装、日期工具constants状态字典、枚举值一个比较实用的约定是controller层只做参数接收和校验不写业务逻辑一个controller对应一个业务模块命名与前端路由对齐。这样前后端联调时两边说“房源列表”都知道去哪个文件改沟通成本很低。2. 数据库设计把业务状态理清楚2.1 核心数据表与关系数据库是这套系统的地基表与表之间的关系一旦定错了后面改起来极其痛苦。我设计的核心表大概有这些表名作用关键字段sys_user系统用户username、password、statussys_role / sys_user_role角色与用户关系role_code、remarksys_menu / sys_role_menu菜单与角色关系menu_name、path、permsbuilding_info楼栋/小区基础信息building_name、addresshouse_info房源/房间信息house_no、building_id、area、statustenant_info租客档案name、phone、id_cardlease_contract租赁合同contract_no、house_id、tenant_id、start_date、end_date、monthly_rentrent_bill租金账单contract_id、bill_month、amount、due_date、statuspayment_record缴费流水bill_id、amount、pay_method、operatorrepair_order报修工单house_id、content、statusoperation_log操作日志operator、action、detail这些表之间的关系并不复杂house_info一对多lease_contract因为同一个房源历史上会有多份合同lease_contract一对多rent_bill一份合同对应每个月的一张账单。tenant_info和lease_contract也是类似逻辑当前合同关联一个租客但历史合同也要保留。这里有个很容易忽略的细节合同表里要冗余房源快照字段。比如合同里存了当时的house_no、house_address、monthly_rent。这样做是因为如果后续房源调价、改门牌号历史合同不能被牵连。这类冗余在业务系统里叫“业务快照”比每次都去join查询更可靠也更符合审计要求。2.2 状态设计租赁业务最关键的地方租赁系统的状态设计比表关系更能体现系统的“企业级”水平。我给三类核心数据各设计了一套状态对象状态值含义房源 house_info.status1 / 2 / 3 / 4空置 / 已租 / 维护中 / 锁定合同 lease_contract.status0 / 1 / 2 / 3 / 4草稿 / 生效中 / 已退租 / 违约终止 / 到期结束账单 rent_bill.status0 / 1 / 2 / 3待缴 / 已缴 / 逾期 / 已作废状态流转是有固定路线的不能随意跳。比如签一个新合同流程必须是校验房源状态必须是“空置”创建合同状态置为“生效中”房源状态改为“已租”生成首期账单退租流程则是校验合同必须是“生效中”结算未缴账单合同状态改为“已退租”房源状态改回“空置”这些状态流转我在service层统一封装在代码里强制约束不依赖前端自觉。前端就算绕过按钮直接调接口后端也会拒绝非法状态变更。这里也解释一下为什么不用数据库外键企业级多人并发使用外键在插入和删除时容易造成锁等待报错信息也很难让运维理解。用业务字段加索引在service层统一控制关系更灵活也更可控。2.3 索引设计与关键查询租赁系统最常见的操作其实是几个固定场景按状态筛选房源、按合同扫描到期、按账单扫描逾期、按租客查历史合同。所以索引设计要围绕这些查询来。house_infoidx_house_status(status)、uk_house_no(house_no)、idx_building_id(building_id)lease_contractuk_contract_no(contract_no)、idx_house_id(house_id)、idx_tenant_id(tenant_id)、idx_end_time(end_time)rent_billuk_contract_month(contract_id, bill_month)、idx_status(status)、idx_due_date(due_date)tenant_infoidx_phone(phone)为什么给end_time和due_date建索引因为系统每天要用定时任务扫这些字段。如果没有索引房源几千条的时候感觉不出来等数据量到几万条定时任务会越跑越慢挤占业务数据库IO。加一个普通索引扫描成本直线下降。统计SQL也要提前想好。比如统计空置率SELECT SUM(CASE WHEN status 1 THEN 1 ELSE 0 END) AS empty_count, SUM(CASE WHEN status NOT IN (1, 4) THEN 1 ELSE 0 END) AS occupied_count, COUNT(*) AS total_house FROM house_info WHERE building_id #{buildingId};注意统计口径空置率的分母一般是“可租状态房源数”而不是“全部房源数”因为维护中和锁定的房源本来就不参与出租混在一起统计会把空置率算得虚低。这种细节一开始就要定清楚后面报表才可信。3. 后端核心模块实现细节3.1 统一响应与全局异常处理前后端分离之后接口规范是第一件事。我定了统一的返回结构{ code: 200, msg: success, data: {} }code200表示成功400表示业务失败比如房源已出租401表示未登录或登录过期403表示无权限500表示系统异常。为什么做这个统一因为前端每个接口都要处理成功、失败、弹提示、跳登录这几件事。如果十个接口有十种返回结构联调会变成灾难。统一之后前端axios拦截器只需要处理一套逻辑。全局异常处理用RestControllerAdvice实现。Service层抛出的BizException会被捕获转成code400和对应的提示信息其他未知异常打印完整堆栈后返回code500的友好提示。要注意的是不要把内部异常信息原样返回给前端比如堆栈里的服务器路径、SQL语句这些信息暴露出去是不安全的给个“系统繁忙请稍后重试”就够了详细日志留在后端文件里。3.2 MyBatis工程化配置与动态SQLMyBatis要配置好几个核心项最容易被新手漏掉的是驼峰映射mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.rent.entity configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case这个配置一开数据库的create_time字段会自动映射到实体类的createTime属性省掉一大堆手写resultMap。很多人查出来的实体字段全是null十有八九就是没开这个开关。动态SQL是MyBatis做条件筛选的王牌。房源列表的筛选查询就这么写select idselectHouseList resultTypeHouse select * from house_info where if teststatus ! null and status ! and status #{status} /if if testbuildingId ! null and building_id #{buildingId} /if if testkeyword ! null and keyword ! and (house_no like concat(%, #{keyword}, %) or house_name like concat(%, #{keyword}, %)) /if /where order by house_id desc /select有几个注意事项不要让前端把条件拼成SQL字符串传过来必须用#{}参数绑定防止SQL注入。${}不是不能用但使用时必须保证传入值在白名单里比如排序字段。用PageHelper分页时PageHelper.startPage()必须放在Mapper方法调用之前而且同一线程不要多次调用否则查询结果会错乱。不要在for循环里一条一条查数据库能用一条in查询解决就不要循环去查避免N1问题。3.3 签约退租一个完整的事务闭环签合同这段逻辑是整个系统里最需要谨慎的。我先给一段核心代码Transactional(rollbackFor Exception.class) public ContractVO createContract(ContractCreateRequest request) { House house houseMapper.selectByIdForUpdate(request.getHouseId()); if (house null || !1.equals(house.getStatus())) { throw new BizException(房源不存在或当前不可签约); } Contract contract buildContract(request); contractMapper.insert(contract); houseMapper.updateStatus(request.getHouseId(), 2); billService.generateFirstBill(contract.getId(), request.getFirstMonthRate()); logService.record(create_contract, 创建合同 contract.getContractNo()); return ContractVO.from(contract); }这段代码里最值得琢磨的是selectByIdForUpdate。为什么查询房源要加锁因为在两台服务器并发的情况下两个人同时看到这个房源待租一起点了签约。如果只是普通select两个请求都会通过状态校验都会插入合同房源就被租出去两次了。用select ... for update把这行锁住第二个请求会等第一个请求提交后再判断这时房源状态已经变成“已租”直接抛业务异常。Transactional(rollbackFor Exception.class)保证整个流程的原子性合同插入、房源状态更新、账单生成、日志记录要么全部成功要么全部回滚。如果不加事务就有可能出现“合同建了、房子还是空置”这种历史遗留问题。我在一线见过太多次几乎都是漏了事务或某个更新语句被吞了异常。退租逻辑同理结算未缴账单、合同状态改成已退租、房源状态改回空置这三个动作必须在一个事务里完成。退租完成后还要写一条退租记录包含最终的租金结算情况方便后续财务对账。3.4 定时账单与逾期滞纳金租金账单不能靠人工去录否则月底财务非疯了不可。我用Spring的Scheduled做定时任务月账单生成任务每月1号凌晨2点扫描所有状态为“生效中”的合同为每份合同生成当月账单。账单号有唯一约束(contract_id, bill_month)防止重复执行时产生重复账单。逾期扫描任务每天凌晨跑一次扫描所有due_date 今天且状态还是“待缴”的账单把状态改成“逾期”同时按规则计算滞纳金。滞纳金规则我建议用“固定比例上限”的方式。比如每天按应收金额的0.5‰计算但最高不超过应收金额的20%滞纳金 min(应收金额 * 0.0005 * 逾期天数, 应收金额 * 0.2)如果不设上限几年没缴的账单滞纳金会超过本金租客肯定不认运营也解释不清楚。这个上限要在系统说明文档里写清楚最好在租约合同模板里也体现。另外账单生成后要支持“作废”功能比如录入错误或者租客提前退租但作废操作一定要留日志谁作废的、为什么作废都要有记录。3.5 权限安全RBAC JWT企业级系统的安全比“能登录就行”要多两个层级角色权限和操作权限。我在这套系统里做了标准的RBAC模型用户关联角色角色关联菜单和按钮权限。登录成功后后端签发JWT token前端存起来每次请求在Header里带Authorization。后端用一个HandlerInterceptor拦截除了登录、菜单等白名单以外的所有接口校验token有效性和用户状态解析出用户ID放到ThreadLocal里业务层直接取。为什么不用Spring Security不是因为它不好而是对多数中小项目来说自定义拦截器RBAC表反而更容易理解出问题也好排查。Spring Security的过滤器链概念对新人来说门槛偏高而这里的需求就是“校验token、校验权限”一个拦截器就能覆盖。密码必须用BCrypt加密存储。绝对不能再用MD5MD5加盐也挡不住现代算力下的暴力破解。注册用户时用BCrypt生成哈希登录时校验哈希。数据库里即使泄露了密码字段也无法反推出明文。按钮权限我用一个自定义注解RequirePermission(rent:contract:create)标注在接口上拦截器里判断当前用户角色是否包含该权限点。没有权限直接返回403前端在按钮上也会根据权限集合决定要不要渲染。双端都控制安全性才完整。4. 前端实现Vue3的全流程开发4.1 工程初始化与项目依赖前端技术栈是Vue3 Vite Vue Router 4 Pinia Element Plus Axios dayjs。用Vite而不是Webpack最大的感受是开发时秒级热更新项目几十个页面也不会等半天编译。需要特别注意的一点依赖版本要固定写法在package.json里把版本号写死不要用^这种允许浮动的写法。Element Plus和Vue之间的兼容性问题我踩过不止一次锁定版本能少很多麻烦。环境变量文件要区分.env.developmentVITE_API_BASE_URL/api.env.productionVITE_API_BASE_URL/api前端代码里不写死后端地址统一走相对路径/api开发环境由Vite代理转发生产环境由Nginx转发。这样换环境只改配置文件不用改代码。4.2 登录态与权限路由登录成功后token和用户信息保存到Pinia同时持久化到localStorage。路由守卫统一处理未登录跳转router.beforeEach((to, from, next) { const store useUserStore() if (to.meta.public) return next() if (!store.token) return next(/login) return next() })to.meta.public用来标记登录页这类公开路由。菜单根据用户权限动态渲染后端返回当前用户能访问的菜单列表前端根据这个列表生成侧边栏而不是把所有菜单写死。按钮级别的权限比如“新增合同”“退租”这些操作按钮用权限集合判断是否渲染v-if。登录页有个小细节只存账号不存密码。浏览器记住密码是浏览器自己的功能应用层不要做“记住密码”这种把用户凭证暴露给本地存储的事。账号可以存localStorage密码不行。4.3 核心页面与业务交互几个核心页面的设计思路工作台Dashboard顶部四个统计卡片在租房源、空置房源、本月应收、本月实收中间是待办清单即将到期合同、逾期账单下面放近6个月收入趋势图。运营人员打开系统第一眼就能掌握全局。房源管理支持按楼栋、状态、关键字筛选。表格行内显示状态标签空置绿色、已租蓝色、维护中橙色。行内直接提供“签约”“详情”“报修”入口减少页面跳转。合同管理列表默认筛选“生效中”。合同创建页采用分步表单第一步选房源只显示空置房源第二步选租客第三步填租期和金额前端实时计算总租金。这里有个交互细节保存成功之后要刷新当前列表并保留筛选条件而不是跳回第一页否则用户操作几次就要重新翻页体验很差。账单收银支持按“全部/待缴/已缴/逾期”切换逾期账单红色高亮。收银时弹窗展示应收金额、滞纳金明细确认后调收款接口成功后刷新列表。前端表单校验不能只靠后端。Element Plus表单组件的校验规则实时提示比如手机号格式、身份证长度、日期先后顺序前端能拦住的问题就不要让用户提交了再等后端报错。4.4 Axios封装与前后端联调axios实例统一封装拦截器做两件事请求拦截从Pinia取token加到Authorization头响应拦截code 200直接返回datacode 401清除登录态并跳转登录页其他code用ElMessage.error(msg)统一弹错误提示开发环境跨域在Vite配置文件里做server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }生产环境跨域交给Nginx处理后面部署章节会提到。联调时最容易出问题的是时间和字段命名。后端返回的LocalDateTime默认序列化格式是数组前端拿到根本没法用。我的做法是后端在Jackson配置里统一格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8字段命名则要求VO全部使用驼峰接口返回JSON就是驼峰前端不要做二次映射。前后端约定好接口文档用Apifox或Swagger维护联调效率高很多。5. 部署上线与常见问题排查5.1 本地环境准备要跑起来这套源码本地环境准备如下JDK 8或11Maven 3.6Node.js 16MySQL 8.05.7也兼容开发工具IDEA DataGrip或Navicat启动步骤新建数据库导入项目根目录的rent_manage.sql脚本修改application.yml里的数据库地址、账号、密码启动后端确认http://localhost:8080能访问前端目录执行npm install再执行npm run dev浏览器打开Vite输出的地址先走一遍登录接口确认token流程通建议电脑内存16G以上IDEA同时开后端和前端开发再加两个数据库工具内存小了会卡到怀疑人生。5.2 生产部署方案后端打包mvn clean package -DskipTests产出jar包放到服务器上java -jar rent-manage.jar --spring.profiles.activeprod可以用systemd脚本或supervisor做进程守护避免进程意外退出。数据库导入项目提供的SQL脚本注意生产环境修改数据库账号密码不要用默认的弱口令。前端构建npm run build把dist目录整个传到服务器放到Nginx的HTML目录。Nginx配置两个关键locationserver { listen 80; root /usr/share/nginx/html; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080/; } location / { try_files $uri $uri/ /index.html; } }/api/转发到后端try_files是为了配合Vue Router的history模式否则刷新当前路径会404。还有一个很容易忽略的地方前端项目里如果有上传图片的需求静态资源目录也要规划好不能让Nginx和Tomcat抢同一个目录。MySQL建库时指定字符集CREATE DATABASE rent_manage DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;不指定utf8mb4的话后期一旦写入特殊符号就会报错而且改字符集要锁表代价很大。5.3 常见问题排查实录我整理了一份速查表基本都是实际踩过的坑症状原因解决办法分页总数不对PageHelper版本与MyBatis冲突或startPage位置不对使用pagehelper-spring-boot-starter1.4.xstartPage紧贴Mapper调用查询结果字段为null没开启驼峰映射配置map-underscore-to-camel-case: true登录后请求401token过期或拦截器白名单没配好检查拦截器路径规则和token过期时间前端跨域报错Vite代理没配或后端CORS没放开开发用proxy生产用Nginx proxy_pass时间格式展示成数组LocalDateTime默认序列化配置Jackson date-format和time-zone数据库中文乱码字符集不是utf8mb4建库时显式设置字符集和排序规则刷新页面404前端history路由没配try_filesNginx加try_files $uri $uri/ /index.html定时任务没跑主类没加EnableScheduling启动类加注解再补充几条避坑心得比上面的表格更重要不要用数据库外键强行做关联。真踩过这种坑后来导入数据和线上改表结构都异常痛苦。业务关联在service层控制状态由代码保证外键只保留索引级别。生产环境不要直接改数据库。要改合同状态、作废账单必须走系统功能这样才能在操作日志表里留下审计线索。直接改库出了问题没有任何人能回答“谁改的、为什么改”。合同历史的快照字段千万别忽略。我见过一个系统合同金额是联表实时查房源的后来房源调整了租金所有历史合同金额也跟着变了直接引发纠纷。这就是没有做业务快照的后果。页面上展示的一切统计数据都要能追溯明细。报表上显示“本月应收10万”点击去就要能看到是哪10个合同、哪50张账单组成的。不能只给一个汇总数字否则使用者不敢信。这套系统的源码核心逻辑偏实数据库结构、状态流转和事务操作彼此咬合得很紧你拿它去改最值得借鉴的就是这些咬合点。最后分享一个我个人的开发习惯拿到任何类似项目先别急着建表把状态流转表和核心业务链路图写在项目文档最前面让新加入团队的人一眼就能读懂。这个动作前期多花一小时后期能省来来回回解释的十个小时。这套租赁系统源码里我也保留了同样的文档按上面的步骤跑起来之后你可以很直观地看到业务状态是怎么一步步串起来的。
返回列表