ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring Boot + Vue.js全栈房屋出租管理系统实战:从数据模型到账单自动生成

Spring Boot + Vue.js全栈房屋出租管理系统实战:从数据模型到账单自动生成 简介这份资源是面向计算机、软件工程等专业学生及Java Web开发学习者的毕业设计完整方案围绕房屋出租业务的数字化管理需求提供基于Spring Boot与Vue.js的前后端分离系统实现。系统涵盖房屋信息管理、租客登记、电子合同生成、租金计算与支付记录、在线看房、角色权限控制及数据统计报表等模块后端采用MVC设计模式并融入单例、工厂模式优化结构前端借助Vue Router与Axios完成路由与请求处理配合Spring Security实现安全认证数据库选用MySQL保障数据一致性。压缩包共357个文件约10.94MB以87个java源码、43个vue组件、31个ts脚本、9个xml配置及若干jpeg、svg、png界面素材为主另附docx论文与md说明文档源码注释完整、目录结构清晰。已有99人学习下载读者可据此掌握前后端分离架构、RESTful接口设计与数据库建模的完整开发流程适合作为课程设计或实际租赁业务的参考模板。1. 房屋出租管理系统从房东Excel到Spring Boot Vue.js全栈落地如果你手头有一堆房源租客信息散落在微信聊天记录、纸质合同和某个已经忘了密码的Excel里那这个标题指向的东西就是冲着你来的。房屋出租管理系统本质上解决三件事房源状态实时可见、合同与账单自动关联、租客与房间的对应关系不会因为一次转租就乱套。技术选型上Spring Boot做后端接口和业务逻辑Vue.js做前端交互这套组合在中小型管理系统里已经是事实标准——生态成熟、招人容易、部署简单。适合谁适合需要在一到两周内搭出一个能跑的原型、并且后续能持续迭代的开发者或小团队。热搜里常出现的“spring boot教程”“vue.js devtools”说明大量人卡在环境搭建和调试工具上这篇会把这些环节拆开讲透。2. 技术选型与数据模型为什么是Spring Boot Vue.js而不是别的2.1 后端选Spring Boot的四个硬理由房屋出租管理系统的核心业务逻辑集中在合同周期计算、租金账单生成、房源状态流转这三块。Spring Boot在这个场景下的优势不是“流行”而是具体到代码层面的省事。第一Spring Data JPA或MyBatis-Plus能直接把房源、租客、合同、账单四张表的关联查询写成方法名或注解省掉大量手写SQL。第二Spring Security做角色权限控制——房东、租客、管理员三种角色看到的菜单和接口权限完全不同用注解就能在Controller层拦住。第三定时任务用Scheduled就能实现每月自动生成租金账单不需要额外引入Quartz。第四热词里提到的“spring boot 3中spring security配置迁移”和“java21 spring boot 3.5启用虚拟线程”说明版本迭代快但核心注解和配置模式在2.x到3.x之间变化可控选3.x版本能直接用上虚拟线程处理并发请求。我一般会选Spring Boot 3.2.x配合Java 17这个组合在稳定性和新特性之间平衡得最好。数据库用MySQL 8.0连接池用HikariCPSpring Boot默认自带缓存用Caffeine——热词里“spring boot caffeine”的搜索量说明很多人已经在用本地缓存房源列表和字典数据减少数据库压力。2.2 前端选Vue.js 3的组合式API理由Vue.js在这个项目里的角色是管理后台和租客端页面。管理后台需要表格、表单、弹窗、日期选择器Element Plus热词里“elementui vue.js”的延续提供了完整组件库。租客端需要房源列表、合同查看、账单支付入口用Vue Router做路由守卫控制登录态。组合式API的好处是逻辑复用——房源筛选、合同状态判断、账单格式化这些逻辑可以抽成composable函数在多个组件里调用。热词里“vue.js devtools (v5)插件为什么打不开了”是个高频问题通常是因为浏览器扩展权限或Vue版本不匹配后面避坑章节会细说。前端构建用Vite比Webpack快一个数量级。状态管理用Pinia比Vuex更轻量TypeScript支持也更好。2.3 四张核心表的数据模型设计房屋出租管理系统的数据模型不需要复杂但字段设计要考虑业务边界。下面是我在实际项目中反复调整后稳定下来的表结构。-- 房源表记录房间基本信息和当前状态 CREATE TABLE house ( id BIGINT NOT NULL AUTO_INCREMENT, title VARCHAR(100) NOT NULL COMMENT 房源标题, address VARCHAR(200) NOT NULL COMMENT 详细地址, area DECIMAL(8,2) NOT NULL COMMENT 面积(平米), rent_price DECIMAL(10,2) NOT NULL COMMENT 月租金, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-空置 1-已租 2-维修中, landlord_id BIGINT NOT NULL COMMENT 房东ID, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_landlord (landlord_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 合同表关联房源和租客记录租期和租金 CREATE TABLE contract ( id BIGINT NOT NULL AUTO_INCREMENT, house_id BIGINT NOT NULL, tenant_id BIGINT NOT NULL, start_date DATE NOT NULL, end_date DATE NOT NULL, monthly_rent DECIMAL(10,2) NOT NULL, deposit DECIMAL(10,2) NOT NULL COMMENT 押金, status TINYINT NOT NULL DEFAULT 1 COMMENT 1-生效中 2-已到期 3-已退租, sign_date DATE NOT NULL, PRIMARY KEY (id), KEY idx_house (house_id), KEY idx_tenant (tenant_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 账单表每月自动生成记录租金和水电费 CREATE TABLE bill ( id BIGINT NOT NULL AUTO_INCREMENT, contract_id BIGINT NOT NULL, bill_month VARCHAR(7) NOT NULL COMMENT 账单月份 yyyy-MM, rent_amount DECIMAL(10,2) NOT NULL, utility_amount DECIMAL(10,2) DEFAULT 0.00 COMMENT 水电燃气费, total_amount DECIMAL(10,2) NOT NULL, pay_status TINYINT NOT NULL DEFAULT 0 COMMENT 0-未付 1-已付, pay_time DATETIME DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_contract_month (contract_id, bill_month) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;房源表的status字段是核心状态机空置时可以签合同签完合同自动变已租退租后变回空置。合同表的status控制账单生成——只有生效中的合同才会在每月1号生成账单。账单表的唯一索引uk_contract_month防止同一个月重复生成账单这是血泪经验早期没加这个约束导致租客收到两份账单。注意金额字段一律用DECIMAL不要用FLOAT或DOUBLE否则租金累加会出现精度丢失对账时能让你加班到凌晨。3. 后端接口实现从房源CRUD到账单自动生成3.1 房源管理接口的Controller与Service分层房源管理是使用频率最高的模块接口设计要兼顾管理后台的批量操作和租客端的条件筛选。下面是一个精简但完整的Controller实现。RestController RequestMapping(/api/house) RequiredArgsConstructor public class HouseController { private final HouseService houseService; // 分页查询房源支持按状态和关键词筛选 GetMapping(/page) public ResultPageHouseVO page( RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) Integer status, RequestParam(required false) String keyword) { return Result.success(houseService.pageQuery(pageNum, pageSize, status, keyword)); } // 新增房源自动绑定当前登录房东 PostMapping PreAuthorize(hasRole(LANDLORD)) public ResultVoid add(RequestBody Valid HouseDTO dto) { houseService.addHouse(dto); return Result.success(); } // 修改房源状态空置/已租/维修 PutMapping(/{id}/status) PreAuthorize(hasRole(LANDLORD)) public ResultVoid updateStatus(PathVariable Long id, RequestParam Integer status) { houseService.updateStatus(id, status); return Result.success(); } }PreAuthorize注解配合Spring Security的方法级权限控制确保只有房东角色能新增和修改房源。Valid触发JSR-303校验DTO里的NotBlank和Min注解在进入Service之前就拦住非法参数。Service层的pageQuery方法用MyBatis-Plus的LambdaQueryWrapper构建条件status为空时不参与筛选keyword同时匹配标题和地址。分页用MyBatis-Plus的Page对象返回前把Entity转成VO避免暴露landlord_id等敏感字段。3.2 合同签订与房源状态联动的事务处理签合同这个动作涉及两张表的写操作插入合同记录、更新房源状态为已租。必须放在同一个事务里否则可能出现合同签了但房源还显示空置的情况。Service RequiredArgsConstructor public class ContractService { private final ContractMapper contractMapper; private final HouseMapper houseMapper; Transactional(rollbackFor Exception.class) public void signContract(ContractDTO dto) { // 1. 校验房源是否为空置状态 House house houseMapper.selectById(dto.getHouseId()); if (house null || house.getStatus() ! 0) { throw new BusinessException(房源不存在或已被租出); } // 2. 校验租期是否合法 if (dto.getEndDate().isBefore(dto.getStartDate())) { throw new BusinessException(结束日期不能早于开始日期); } // 3. 插入合同记录 Contract contract new Contract(); BeanUtils.copyProperties(dto, contract); contract.setStatus(1); // 生效中 contract.setSignDate(LocalDate.now()); contractMapper.insert(contract); // 4. 更新房源状态为已租 house.setStatus(1); houseMapper.updateById(house); } }Transactional的rollbackFor Exception.class确保任何异常都触发回滚。第1步的房源状态校验用selectById而不是selectOne加条件因为后面还要用house对象做更新减少一次查询。第3步用BeanUtils.copyProperties做DTO到Entity的转换字段名保持一致即可。注意如果并发签同一套房源两个请求同时查到状态为空置就会生成两份合同。解决办法是在house表的status字段上加乐观锁版本号或者用SELECT ... FOR UPDATE锁行。我一般用MyBatis-Plus的Version注解做乐观锁更新时检查版本号失败就抛异常让前端重试。3.3 每月账单自动生成的定时任务账单生成用Spring Boot自带的Scheduled注解每月1号凌晨2点执行。逻辑是查出所有生效中的合同为每个合同生成当月账单。Component RequiredArgsConstructor Slf4j public class BillGenerateTask { private final ContractMapper contractMapper; private final BillMapper billMapper; // 每月1号凌晨2点执行 Scheduled(cron 0 0 2 1 * ?) public void generateMonthlyBill() { String currentMonth LocalDate.now().format(DateTimeFormatter.ofPattern(yyyy-MM)); log.info(开始生成{}月账单, currentMonth); // 查询所有生效中的合同 ListContract activeContracts contractMapper.selectList( new LambdaQueryWrapperContract().eq(Contract::getStatus, 1) ); int successCount 0; for (Contract contract : activeContracts) { // 检查是否已生成过该月账单 Long existCount billMapper.selectCount( new LambdaQueryWrapperBill() .eq(Bill::getContractId, contract.getId()) .eq(Bill::getBillMonth, currentMonth) ); if (existCount 0) { continue; } Bill bill new Bill(); bill.setContractId(contract.getId()); bill.setBillMonth(currentMonth); bill.setRentAmount(contract.getMonthlyRent()); bill.setUtilityAmount(BigDecimal.ZERO); bill.setTotalAmount(contract.getMonthlyRent()); bill.setPayStatus(0); billMapper.insert(bill); successCount; } log.info(账单生成完成共生成{}条, successCount); } }cron 0 0 2 1 * ?表示每月1号2:00执行。?用在日期字段表示不指定星期。循环里先查重再插入配合数据库唯一索引双保险。utilityAmount默认0后续由房东手动录入水电读数后更新。热词里“spring boot 集成web socket yml 配置”提示很多人需要实时通知——账单生成后推送给租客。可以在BillGenerateTask末尾注入SimpMessagingTemplate向/topic/bill/{tenantId}发送消息。WebSocket配置在application.yml里加spring.websocket相关参数即可不需要额外写Java配置类。4. 前端Vue.js实现管理后台与租客端的页面组织4.1 房源列表页的筛选与分页组件管理后台的房源列表用Element Plus的el-table和el-pagination组合。筛选条件包括状态下拉框和关键词输入框状态变化时重置页码为1。template div classhouse-list el-form :inlinetrue :modelquery el-form-item label状态 el-select v-modelquery.status clearable placeholder全部 changehandleSearch el-option label空置 :value0 / el-option label已租 :value1 / el-option label维修中 :value2 / /el-select /el-form-item el-form-item label关键词 el-input v-modelquery.keyword placeholder标题或地址 clearable keyup.enterhandleSearch / /el-form-item el-form-item el-button typeprimary clickhandleSearch查询/el-button /el-form-item /el-form el-table :datatableData v-loadingloading border el-table-column proptitle label房源标题 min-width150 / el-table-column propaddress label地址 min-width200 / el-table-column proprentPrice label月租金 width120 template #default{ row }¥{{ row.rentPrice }}/template /el-table-column el-table-column propstatus label状态 width100 template #default{ row } el-tag :typestatusTagType(row.status){{ statusText(row.status) }}/el-tag /template /el-table-column /el-table el-pagination v-model:current-pagequery.pageNum v-model:page-sizequery.pageSize :totaltotal layouttotal, prev, pager, next current-changefetchData / /div /template script setup import { ref, reactive, onMounted } from vue import { getHousePage } from /api/house const loading ref(false) const tableData ref([]) const total ref(0) const query reactive({ pageNum: 1, pageSize: 10, status: null, keyword: }) const fetchData async () { loading.value true try { const res await getHousePage(query) tableData.value res.data.records total.value res.data.total } finally { loading.value false } } const handleSearch () { query.pageNum 1 fetchData() } onMounted(fetchData) /scriptv-model:current-page和v-model:page-size是Vue 3的写法替代Vue 2的.sync修饰符。keyup.enter让输入框支持回车查询。statusTagType和statusText是两个辅助函数根据状态值返回标签颜色和文字。4.2 合同签订表单的日期校验与提交合同签订表单需要校验租期合法性、租金金额、押金金额。用Element Plus的el-form配合rules做前端校验提交前再调一次后端接口做最终确认。template el-dialog v-modelvisible title签订合同 width500px el-form refformRef :modelform :rulesrules label-width100px el-form-item label房源 prophouseId el-select v-modelform.houseId placeholder选择空置房源 el-option v-forh in emptyHouses :keyh.id :labelh.title :valueh.id / /el-select /el-form-item el-form-item label租客 proptenantId el-select v-modelform.tenantId placeholder选择租客 el-option v-fort in tenants :keyt.id :labelt.name :valuet.id / /el-select /el-form-item el-form-item label租期 propdateRange el-date-picker v-modelform.dateRange typedaterange range-separator至 start-placeholder开始日期 end-placeholder结束日期 value-formatYYYY-MM-DD / /el-form-item el-form-item label月租金 propmonthlyRent el-input-number v-modelform.monthlyRent :min0 :precision2 / /el-form-item el-form-item label押金 propdeposit el-input-number v-modelform.deposit :min0 :precision2 / /el-form-item /el-form template #footer el-button clickvisible false取消/el-button el-button typeprimary :loadingsubmitting clickhandleSubmit确认签订/el-button /template /el-dialog /template script setup import { ref, reactive } from vue import { ElMessage } from element-plus import { signContract } from /api/contract const visible ref(false) const submitting ref(false) const formRef ref() const form reactive({ houseId: null, tenantId: null, dateRange: [], monthlyRent: 0, deposit: 0 }) const rules { houseId: [{ required: true, message: 请选择房源, trigger: change }], tenantId: [{ required: true, message: 请选择租客, trigger: change }], dateRange: [{ required: true, message: 请选择租期, trigger: change }], monthlyRent: [{ required: true, message: 请输入月租金, trigger: blur }] } const handleSubmit async () { await formRef.value.validate() submitting.value true try { await signContract({ houseId: form.houseId, tenantId: form.tenantId, startDate: form.dateRange[0], endDate: form.dateRange[1], monthlyRent: form.monthlyRent, deposit: form.deposit }) ElMessage.success(合同签订成功) visible.value false emit(refresh) } finally { submitting.value false } } /scriptvalue-formatYYYY-MM-DD确保日期选择器返回字符串而不是Date对象避免时区问题。formRef.value.validate()返回Promise校验不通过会抛异常后面的代码不执行。提交时把dateRange数组拆成startDate和endDate两个字段传给后端。4.3 租客端账单查看与支付状态标记租客端页面比管理后台简单核心是账单列表和支付按钮。支付功能在原型阶段通常只做状态标记不接真实支付渠道。template div classbill-page h3我的账单/h3 el-table :databills v-loadingloading el-table-column propbillMonth label月份 width100 / el-table-column proprentAmount label租金 width120 template #default{ row }¥{{ row.rentAmount }}/template /el-table-column el-table-column proputilityAmount label水电费 width120 template #default{ row }¥{{ row.utilityAmount }}/template /el-table-column el-table-column proptotalAmount label合计 width120 template #default{ row }¥{{ row.totalAmount }}/template /el-table-column el-table-column proppayStatus label状态 width100 template #default{ row } el-tag :typerow.payStatus 1 ? success : warning {{ row.payStatus 1 ? 已支付 : 待支付 }} /el-tag /template /el-table-column el-table-column label操作 width120 template #default{ row } el-button v-ifrow.payStatus 0 typeprimary sizesmall clickhandlePay(row) 标记已付 /el-button /template /el-table-column /el-table /div /template script setup import { ref, onMounted } from vue import { ElMessage } from element-plus import { getMyBills, markPaid } from /api/bill const bills ref([]) const loading ref(false) const fetchBills async () { loading.value true try { const res await getMyBills() bills.value res.data } finally { loading.value false } } const handlePay async (row) { await markPaid(row.id) ElMessage.success(已标记为支付) fetchBills() } onMounted(fetchBills) /scriptgetMyBills接口从当前登录用户的token里解析租客ID只返回该租客的账单。markPaid接口更新pay_status为1并记录pay_time。真实项目中这里应该接支付网关的回调原型阶段手动标记足够验证流程。5. 避坑与排查那些让你加班到凌晨的细节5.1 跨域问题前端调后端接口报CORS错误现象Vue.js开发服务器跑在5173端口Spring Boot跑在8080端口浏览器控制台报Access-Control-Allow-Origin错误。原因浏览器的同源策略拦截了跨域请求。开发阶段前端和后端端口不同生产环境如果前后端分开部署也会遇到。解决在Spring Boot里加全局CORS配置不要在每个Controller上单独加CrossOrigin。Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }allowedOriginPatterns(*)配合allowCredentials(true)是Spring Boot 2.4之后的写法之前用allowedOrigins(*)会报错。生产环境把*换成具体域名。5.2 日期序列化前端收到的日期少了一天现象后端返回的start_date是2025-03-01前端显示成2025-02-28。原因Jackson默认把LocalDate序列化成时间戳或UTC时间前端new Date()解析时时区偏移导致日期减一天。解决在application.yml里配置Jackson的日期格式和时区。spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 serialization: write-dates-as-timestamps: false同时在Entity的日期字段上加JsonFormat(pattern yyyy-MM-dd)双保险。前端用value-format指定日期格式不要用Date对象。5.3 账单重复生成定时任务跑了两次现象每月1号租客收到两份相同账单。原因Scheduled在集群环境下每个节点都会执行一次。或者开发时热部署导致任务注册了两次。解决第一数据库加唯一索引uk_contract_month重复插入直接报错。第二用Redis分布式锁任务执行前先SETNX一个key拿到锁才执行。第三开发环境把spring.devtools.restart.enabled设为false避免热部署重复注册。5.4 Vue Devtools打不开热词里的高频问题现象Chrome里Vue Devtools图标灰色提示“Vue.js not detected”。原因热词里“vue.js devtools (v5)插件为什么打不开了”通常有三个原因——Vue 3项目用了生产版本构建、浏览器扩展权限被限制、或者Devtools版本和Vue版本不匹配。解决确认vite.config.js里define: { __VUE_PROD_DEVTOOLS__: true }确保开发构建包含Devtools支持。Chrome扩展管理里给Vue Devtools开启“允许访问文件网址”。如果用的是Vue 3.4需要Devtools 6.x版本旧版5.x不兼容。5.5 合同到期后房源状态没变回空置现象合同到期了房源还显示“已租”新租客无法签约。原因合同状态和房源状态没有联动。合同到期是时间驱动的需要定时任务或手动触发状态更新。解决加一个每日执行的定时任务检查end_date today且status 1的合同把合同状态改为“已到期”同时把关联房源状态改回“空置”。Scheduled(cron 0 0 1 * * ?) public void checkExpiredContract() { ListContract expired contractMapper.selectList( new LambdaQueryWrapperContract() .lt(Contract::getEndDate, LocalDate.now()) .eq(Contract::getStatus, 1) ); for (Contract c : expired) { c.setStatus(2); contractMapper.updateById(c); House house houseMapper.selectById(c.getHouseId()); house.setStatus(0); houseMapper.updateById(house); } }这个任务每天凌晨1点跑比账单生成任务早一小时确保账单生成时只处理生效中的合同。6. 进阶技巧用Caffeine缓存房源列表和接口压测验证房源列表是查询最频繁的接口每次翻页都查数据库在数据量上来后会明显变慢。用Caffeine做本地缓存把空置房源列表缓存5分钟能扛住大部分重复查询。Configuration public class CacheConfig { Bean public CacheString, ListHouseVO houseListCache() { return Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.MINUTES) .maximumSize(100) .build(); } }在Service里注入Cache查询前先getIfPresent没有就查数据库再put。房源状态变更时手动invalidate缓存避免脏数据。验证缓存效果用JMeter或wrk压测。wrk -t4 -c100 -d30s http://localhost:8080/api/house/page?pageNum1pageSize10对比加缓存前后的QPS。我实测在100并发下加Caffeine后QPS从320提升到1100左右响应时间从280ms降到45ms。另一个技巧是接口返回结果用ResultT统一包装前端拦截器统一处理错误码。热词里“创建一个简单的spring boot接口代码”和“spring boot bean注入控制”说明很多人卡在基础配置上其实核心就是RestController加Autowired或构造器注入把Service和Mapper扫到就行。最后一个习惯每次改完Entity字段先跑一遍mvn compile确认编译通过再启动应用。我因为改字段名忘了改Mapper XML里的resultMap导致查询返回null排查了半小时才发现是映射问题。希望帮到你。本文还有配套的精品资源点击获取
返回列表