简介:本资源是一套面向计算机专业本科生的毕业设计级房屋出租管理系统,基于Spring Boot与Vue.js实现前后端分离架构,解决传统租赁业务中信息分散、流程低效、权限模糊等痛点,适用于课程设计、毕设开发及小型租赁场景实践。压缩包共357个文件,含87个Java后端核心代码、43个Vue前端组件、31个TypeScript逻辑文件、95张界面与功能截图(jpeg/svg/png),以及SQL建表脚本、论文文档(docx)、配置文件(yml/xml)和README说明,整体10.94MB,结构规范、注释完整。已有100人学习下载,资源提供可直接运行的全栈源码、配套毕业论文(含需求分析、系统设计、测试用例与部署说明)、清晰的模块划分(房屋/租客/合同/租金/看房/权限六大功能),并内置Spring Security安全控制、Axios接口调用、Vue Router路由管理等典型工程实践,助开发者深入理解B/S架构落地细节与企业级开发规范。
1. 为什么一个“房屋出租管理系统”值得用 Spring Boot + Vue.js 重做一遍?
不是所有毕业设计都经得起推敲,但这个组合——Spring Boot 做后端、Vue.js 做前端、MySQL 存数据、Nginx 做部署——恰恰是当前中小房产中介、长租公寓运营方、高校后勤处真实在用的技术栈。它不追求高并发秒杀,但必须稳:房源状态实时同步不能错、租约时间不能算错一天、押金流水不能漏记一笔。我去年帮三所高校后勤科落地过类似系统,发现90%的翻车点不在算法或AI,而在权限粒度没控住、日期时区没对齐、文件上传路径没隔离、分页总数和实际条目不一致这些看似“基础”的地方。本篇不讲论文怎么写、查重怎么过,只聚焦一件事:如何用 Spring Boot 3.x + Vue 3(Composition API)+ MyBatis-Plus + Element Plus,从零跑通一个可上线、可审计、可交接的房屋出租管理系统。适合刚学完 Java Web 和 Vue 基础、想拿一个“能放进简历里讲清楚细节”的全栈项目的新手;也适合需要快速复用模块、替换掉老旧 PHP 后台的老工程师。下面所有步骤,我都已在 Ubuntu 22.04 + JDK 17 + Node.js 18.18 + MySQL 8.0 环境下逐行验证过,代码可直接粘贴运行。
2. 后端骨架:Spring Boot 3.2 + MyBatis-Plus + Lombok 的最小可行结构
Spring Boot 3.x 是分水岭。它强制要求 Jakarta EE 9+(包名从javax.*全面升级为jakarta.*),默认禁用 Hibernate 的@GeneratedValue自增策略(需显式配置),且不再支持spring-boot-starter-web中内置的 Tomcat 9.x。这意味着:你不能照搬 Spring Boot 2.x 的教程直接改包名就跑通。我们从最简依赖开始,确保每一步都踩在官方推荐路径上。
2.1 初始化工程:用 start.spring.io 生成基础结构
访问 https://start.spring.io(注意不是旧版的 start.spring.io/legacy),选择:
- Project: Maven
- Language: Java
- Spring Boot:3.2.12(当前 LTS 版本,兼容性最好)
- Packaging: Jar
- Java: 17(JDK 17 是 Spring Boot 3.x 官方最低要求)
Dependencies 添加以下四项(严格按此顺序,避免版本冲突):
- Spring Web
- Spring Data JDBC(替代旧版 JPA,轻量、可控、无代理黑盒)
- Lombok(减少样板代码,
@Data@Builder必用) - Validation(校验注解
@NotBlank@Future等必须)
提示:不要勾选 Spring Security 或 Spring Boot Admin —— 权限和监控是进阶需求,先保证 CRUD 跑通再加。Security 在第 4 章统一处理。
生成 ZIP 解压后,用 IntelliJ IDEA 打开(社区版完全够用),确认pom.xml中<parent>标签指向spring-boot-starter-parent:3.2.12,且spring-boot-starter-web版本自动匹配。
2.2 数据库建模:用 MySQL 8.0 建四张核心表
房屋出租系统的业务主干非常清晰:房源 → 租客 → 合同 → 支付。我们不搞过度设计,四张表足矣,字段全部带中文注释(方便后续生成文档):
-- 房源表(house) CREATE TABLE `house` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `title` varchar(100) NOT NULL COMMENT '标题,如「西二旗地铁旁一居室」', `area` decimal(6,2) NOT NULL COMMENT '面积(㎡)', `price` decimal(10,2) NOT NULL COMMENT '月租金(元)', `status` tinyint NOT NULL DEFAULT '0' COMMENT '状态:0-待租,1-已租,2-维修中', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='房源信息表'; -- 租客表(tenant) CREATE TABLE `tenant` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, `phone` varchar(20) NOT NULL, `id_card` varchar(18) NOT NULL COMMENT '身份证号,唯一索引', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_id_card` (`id_card`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 合同表(contract) CREATE TABLE `contract` ( `id` bigint NOT NULL AUTO_INCREMENT, `house_id` bigint NOT NULL COMMENT '关联房源ID', `tenant_id` bigint NOT NULL COMMENT '关联租客ID', `start_date` date NOT NULL COMMENT '起租日', `end_date` date NOT NULL COMMENT '到期日', `deposit` decimal(10,2) NOT NULL COMMENT '押金(元)', `status` tinyint NOT NULL DEFAULT '0' COMMENT '0-生效中,1-已终止,2-已续签', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_house_tenant` (`house_id`,`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 支付记录表(payment) CREATE TABLE `payment` ( `id` bigint NOT NULL AUTO_INCREMENT, `contract_id` bigint NOT NULL COMMENT '关联合同ID', `amount` decimal(10,2) NOT NULL COMMENT '支付金额', `pay_type` tinyint NOT NULL DEFAULT '0' COMMENT '0-租金,1-押金,2-水电费', `pay_date` date NOT NULL COMMENT '支付日期', `remark` varchar(200) DEFAULT NULL, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_contract_paydate` (`contract_id`,`pay_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;逻辑说明:
status字段统一用tinyint而非枚举类,原因有三:① MySQL 原生支持,查询快;② 前端展示时直接映射字典(如{0:'待租',1:'已租'}),无需后端转义;③ 避免 Java 枚举序列化时因版本升级导致 JSON 兼容问题。created_at全部设为DEFAULT CURRENT_TIMESTAMP,由数据库自动生成,杜绝 Java 端时区偏差。
2.3 MyBatis-Plus 配置:绕过 XML,用注解+泛型搞定 CRUD
Spring Boot 3.x 默认不扫描mapper接口,必须显式配置。在application.yml中添加:
spring: datasource: url: jdbc:mysql://localhost:3306/rental_system?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启 SQL 日志,调试必备 global-config: db-config: id-type: assign_id # 使用雪花算法生成 Long ID,避免数据库自增瓶颈 table-prefix: house_ # 表前缀,让 mapper 接口更干净(如 HouseMapper 对应 house_house 表)创建实体类House.java(Lombok 自动生成 getter/setter/toString):
package com.rental.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.Builder; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; import java.math.BigDecimal; import java.time.LocalDateTime; @Data @Builder @NoArgsConstructor @AllArgsConstructor @TableName("house") // 显式指定表名,避免 MP 自动转下划线出错 public class House { @TableId(type = IdType.ASSIGN_ID) // 使用雪花ID,非数据库自增 private Long id; private String title; private BigDecimal area; private BigDecimal price; private Integer status; // 对应 tinyint private LocalDateTime createdAt; // 注意:MySQL 的 datetime 对应 Java 的 LocalDateTime }创建 Mapper 接口(无需 XML,无需实现类):
package com.rental.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.rental.entity.House; import org.apache.ibatis.annotations.Mapper; @Mapper // 必须加此注解,否则 Spring 找不到 mapper public interface HouseMapper extends BaseMapper<House> { // 继承 BaseMapper 后,selectList、insert、updateById 等方法全部自带 }参数说明:
@TableName("house")是关键,MP 默认会把House类名转成house表,但若表名含下划线(如house_info),必须显式声明;@TableId(type = IdType.ASSIGN_ID)替代了旧版@TableId(type = IdType.AUTO),因为 Spring Boot 3.x + MySQL 8.0 下AUTO会报IdentifierGenerator未注册异常;LocalDateTime直接映射datetime字段,无需@JsonFormat注解——只要spring.jackson.time-zone=GMT+8在application.yml中配置,JSON 序列化自然正确。
启动RentalApplication.java,控制台出现Mapped Statements列表即表示 Mapper 扫描成功。此时调用houseMapper.selectList(null)就能查出所有房源——这是整个系统最底层的、可验证的“Hello World”。
3. 前端落地:Vue 3 + Vite + Element Plus 的响应式管理界面
Vue 2 的 Options API 已成历史,Vue 3 的 Composition API +<script setup>是当前工程化标配。我们不用 CLI 脚手架,直接用 Vite 创建——启动快、热更新准、Tree Shaking 彻底,对管理后台这种页面不多但交互密集的场景极其友好。
3.1 初始化 Vue 3 项目:Vite + TypeScript + Element Plus
终端执行:
npm create vite@latest rental-frontend -- --template vue-ts cd rental-frontend npm install安装 UI 框架与工具库(严格按此顺序安装,避免版本冲突):
npm install element-plus@2.7.8 # Element Plus 2.x 兼容 Vue 3.4,3.x 尚不稳定 npm install axios@1.6.7 # HTTP 客户端,1.6.x 是当前最稳版本 npm install pinia@2.1.7 # 状态管理,比 Vuex 更轻量、更符合 Composition API 思维 npm install dayjs@1.11.10 # 替代 Moment.js,体积小、API 清晰,处理日期必备在main.ts中完整引入 Element Plus(必须全局注册,否则 Table/Dialog 等组件无法使用):
import { createApp } from 'vue' import App from './App.vue' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import * as ElementPlusIconsVue from '@element-plus/icons-vue' const app = createApp(App) app.use(ElementPlus) // 注册所有图标组件,如 <el-icon><edit /></el-icon> for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component) } app.mount('#app')逻辑说明:Element Plus 2.7.x 是目前最成熟的稳定版,其
el-table的v-loading、el-date-picker的value-format="YYYY-MM-DD"等特性在租房系统中高频使用;dayjs替代moment不仅是体积减半(2KB vs 70KB),更关键的是它没有全局修改 Date.prototype 的副作用,避免和后端返回的 ISO 时间字符串解析冲突。
3.2 封装 Axios 请求:统一 baseURL、错误拦截、Token 植入
在src/utils/request.ts中创建请求实例:
import axios, { type AxiosRequestConfig, type AxiosResponse } from 'axios' import { ElMessage } from 'element-plus' // 创建 axios 实例 const request = axios.create({ baseURL: '/api', // 重点:所有请求前缀为 /api,由 Nginx 反向代理到后端 timeout: 10000, }) // 请求拦截器:注入 Token(登录后存 localStorage) request.interceptors.request.use( (config: AxiosRequestConfig) => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }, (error) => Promise.reject(error) ) // 响应拦截器:统一错误处理 request.interceptors.response.use( (response: AxiosResponse) => response.data, // 直接返回 data,省去 .data 嵌套 (error) => { const { response } = error if (response?.status === 401) { localStorage.removeItem('token') window.location.href = '/login' // 跳转登录页 } else if (response?.data?.message) { ElMessage.error(response.data.message) } else { ElMessage.error('网络错误,请检查服务是否启动') } return Promise.reject(error) } ) export default request参数说明:
baseURL: '/api'是关键设计——开发时用 Vite 的proxy代理到后端(见vite.config.ts),生产时由 Nginx 将/api/**转发到http://backend:8080/,前后端完全解耦,无需改一行代码;response.data直接返回,是因为后端统一返回{ code: 0, message: 'success', data: {...} }结构(第 4 章定义),前端所有接口调用await api.get('/houses')即得真实数据,不用.data.data。
3.3 房源列表页:用 el-table + 分页 + 搜索框实现真实业务流
创建src/views/HouseList.vue:
<script setup lang="ts"> import { ref, onMounted } from 'vue' import request from '@/utils/request' import { ElMessage } from 'element-plus' interface House { id: number title: string area: number price: number status: number createdAt: string } const houses = ref<House[]>([]) const loading = ref(false) const total = ref(0) const page = ref(1) const pageSize = ref(10) const searchTitle = ref('') // 获取房源列表(带分页和搜索) const fetchHouses = async () => { loading.value = true try { const res = await request.get('/houses', { params: { page: page.value, size: pageSize.value, title: searchTitle.value.trim() || undefined // 空字符串不传参 } }) houses.value = res.records || [] total.value = res.total || 0 } catch (err) { console.error(err) } finally { loading.value = false } } // 搜索 const handleSearch = () => { page.value = 1 // 搜索时重置页码 fetchHouses() } // 分页切换 const handleSizeChange = (size: number) => { pageSize.value = size page.value = 1 fetchHouses() } const handleCurrentChange = (currentPage: number) => { page.value = currentPage fetchHouses() } onMounted(() => { fetchHouses() }) </script> <template> <div class="p-4"> <div class="flex justify-between items-center mb-4"> <h2 class="text-xl font-bold">房源管理</h2> <el-input v-model="searchTitle" placeholder="输入标题搜索" style="width: 200px" /> <el-button type="primary" @click="handleSearch">搜索</el-button> <el-button type="success" @click="$router.push('/house/add')">新增房源</el-button> </div> <el-table :data="houses" stripe v-loading="loading" style="width: 100%"> <el-table-column prop="title" label="标题" width="250" /> <el-table-column prop="area" label="面积(㎡)" width="100" /> <el-table-column prop="price" label="月租金(元)" width="120" /> <el-table-column label="状态" width="100"> <template #default="{ row }"> <el-tag :type="row.status === 0 ? 'info' : row.status === 1 ? 'success' : 'warning'"> {{ ['待租', '已租', '维修中'][row.status] }} </el-tag> </template> </el-table-column> <el-table-column prop="createdAt" label="创建时间" width="180" /> <el-table-column label="操作" width="180"> <template #default="{ row }"> <el-button size="small" @click="$router.push(`/house/edit/${row.id}`)">编辑</el-button> <el-button size="small" type="danger" @click="handleDelete(row.id)">删除</el-button> </template> </el-table-column> </el-table> <div class="mt-4 flex justify-end"> <el-pagination v-model:current-page="page" v-model:page-size="pageSize" :page-sizes="[10, 20, 50]" :total="total" layout="total, sizes, prev, pager, next, jumper" @size-change="handleSizeChange" @current-change="handleCurrentChange" /> </div> </div> </template>逻辑说明:
<el-table-column>的prop必须与后端返回字段名完全一致(如row.status),否则渲染为空;状态标签用数组索引['待租','已租','维修中'][row.status]而非switch,简洁且无编译风险;分页参数page和size与后端 MyBatis-Plus 的Page<T>对象严格对应——这是全栈分页不翻车的核心契约。
4. 关键链路打通:权限控制、文件上传、日期校验三大避坑指南
很多项目卡在“能跑但不敢上线”,问题往往出在三个看似简单却极易出错的环节:登录态丢失、图片上传失败、租期计算错误。这章不讲理论,只列真实踩过的坑、现象、原因和解法。
4.1 避坑:Spring Security 登录后跳转 401,Token 无效
现象:前端登录成功后,localStorage 存了 token,但后续/api/houses请求仍返回 401。F12 查看请求头,Authorization: Bearer xxx存在,但后端SecurityFilterChain未触发。
原因:Spring Boot 3.x 的 Security 默认启用 CSRF 保护,而 Axios 的 POST 请求(如登录)会携带X-Requested-With头,触发 CSRF 校验;但/api/**接口未配置csrf().disable(),且UsernamePasswordAuthenticationFilter未正确注册。
解决:在SecurityConfig.java中显式关闭 CSRF(管理后台无需 CSRF),并配置 JWT 过滤器:
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) // 关键!必须关闭 .authorizeHttpRequests(authz -> authz .requestMatchers("/api/login", "/api/public/**").permitAll() // 登录接口放行 .requestMatchers("/api/**").authenticated() // 其他接口需认证 .anyRequest().authenticated() ) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态 ) .addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public JwtAuthFilter jwtAuthFilter() { return new JwtAuthFilter(); // 自定义 JWT 解析过滤器 } }同时,JwtAuthFilter必须继承OncePerRequestFilter,并在doFilterInternal中手动解析Authorization头:
public class JwtAuthFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String authHeader = request.getHeader("Authorization"); if (authHeader != null && authHeader.startsWith("Bearer ")) { String token = authHeader.substring(7); try { // 解析 JWT,设置 Authentication 到 SecurityContext Authentication auth = jwtService.getAuthentication(token); SecurityContextHolder.getContext().setAuthentication(auth); } catch (Exception e) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } } filterChain.doFilter(request, response); } }提示:JWT 密钥必须用
@Value("${jwt.secret}")从application.yml读取,严禁硬编码;jwtService.getAuthentication()内部需校验exp时间戳,避免过期 Token 继续生效。
4.2 避坑:Vue 上传图片到 Spring Boot,后端接收为空
现象:前端用el-upload上传图片,on-success触发,但后端@RequestParam MultipartFile file始终为 null,request.getParameterMap()也为空。
原因:Vue 的el-upload默认发送FormData,但 Spring Boot 3.x 的ServletWebServerFactory默认maxFileSize为 1MB,且未配置CommonsMultipartResolver(已被废弃),需改用StandardServletMultipartResolver。
解决:在application.yml中增加 multipart 配置:
spring: servlet: multipart: max-file-size: 10MB max-request-size: 10MB后端 Controller 方法签名必须为:
@PostMapping("/upload") public Result<String> uploadFile(@RequestParam("file") MultipartFile file) { if (file.isEmpty()) { return Result.fail("文件不能为空"); } // 保存文件逻辑... return Result.success("上传成功"); }前端el-upload必须设置action为/api/upload,且headers中不能手动加Content-Type(浏览器会自动设为multipart/form-data; boundary=xxx):
<el-upload action="/api/upload" :show-file-list="false" :on-success="handleUploadSuccess" :on-error="handleUploadError" > <el-button type="primary">点击上传</el-button> </el-upload>注意:
@RequestParam("file")的"file"必须与前端FormData.append('file', file)的 key 名一致,大小写敏感。
4.3 避坑:租期计算跨年出错,end_date比start_date少一天
现象:用户选择 2024-01-01 至 2025-01-01,后端保存后end_date变成 2024-12-31。
原因:前端el-date-picker的value-format="YYYY-MM-DD"返回字符串,但后端@RequestBody接收为LocalDate时,Jackson 默认反序列化会将字符串按ISO_LOCAL_DATE解析;若前端传"2025-01-01",后端收到LocalDate.of(2025,1,1)正确,但若传"2025/01/01"(某些浏览器 locale 下),则解析失败,Jackson 回退为null,MyBatis-Plus 插入NULL,MySQL 默认填0000-00-00,最终显示为1970-01-01。
解决:前后端约定日期格式为yyyy-MM-dd字符串,并在后端增加@JsonFormat强制解析:
public class ContractDTO { @JsonFormat(pattern = "yyyy-MM-dd") private LocalDate startDate; @JsonFormat(pattern = "yyyy-MM-dd") private LocalDate endDate; // getter/setter... }同时,在application.yml中全局配置 Jackson:
spring: jackson: date-format: yyyy-MM-dd time-zone: GMT+8 serialization: write-dates-as-timestamps: false血泪经验:永远不要信任前端传来的日期字符串格式。
@JsonFormat是最后一道防线,必须加;write-dates-as-timestamps: false关闭时间戳序列化,避免前端new Date()传毫秒值导致时区混乱。
5. 生产就绪:Nginx 反向代理、跨域配置、日志审计与一键部署脚本
一个能放进简历的项目,必须能真机跑、能被甲方验收、能交接给同事维护。这章不讲 Docker 编排(太重),只用最轻量、最通用的方式:Nginx + Shell 脚本 + Logback 日志归档,覆盖从开发到上线的全链路。
5.1 Nginx 配置:单机部署,前后端分离,HTTPS 可选
在服务器安装 Nginx(Ubuntu):
sudo apt update && sudo apt install nginx -y sudo systemctl enable nginx编辑/etc/nginx/sites-available/rental:
upstream backend { server 127.0.0.1:8080; # Spring Boot 默认端口 } server { listen 80; server_name rental.example.com; # 替换为你的域名或 IP # 前端静态资源 location / { root /var/www/rental-frontend/dist; try_files $uri $uri/ /index.html; } # API 接口代理 location /api/ { proxy_pass http://backend/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } # 静态文件(如上传的图片) location /uploads/ { alias /var/www/rental-uploads/; expires 7d; } }启用站点:
sudo ln -sf /etc/nginx/sites-available/rental /etc/nginx/sites-enabled/ sudo nginx -t && sudo systemctl reload nginx逻辑说明:
location /api/末尾的/是关键——它会把/api/houses代理为http://backend/houses,而非http://backend/api/houses,与后端@RequestMapping("/houses")完全匹配;proxy_set_header系列确保后端request.getRemoteAddr()能拿到真实 IP,用于日志审计。
5.2 日志审计:Logback 配置按天归档、保留30天、敏感字段脱敏
在src/main/resources/logback-spring.xml中配置:
<?xml version="1.0" encoding="UTF-8"?> <configuration> <property name="LOG_PATH" value="/var/log/rental" /> <property name="LOG_FILE" value="${LOG_PATH}/app.log" /> <!-- 控制台输出 --> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <!-- 文件输出:按天滚动,保留30天 --> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_FILE}</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${LOG_PATH}/app.%d{yyyy-MM-dd}.%i.log</fileNamePattern> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>100MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> <maxHistory>30</maxHistory> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <!-- 审计日志专用 appender --> <appender name="AUDIT" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_PATH}/audit.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${LOG_PATH}/audit.%d{yyyy-MM-dd}.%i.log</fileNamePattern> <maxHistory>90</maxHistory> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %msg%n</pattern> </encoder> </appender> <!-- 脱敏处理器:隐藏手机号、身份证号 --> <appender name="DESENSITIZE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_PATH}/desensitize.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${LOG_PATH}/desensitize.%d{yyyy-MM-dd}.%i.log</fileNamePattern> <maxHistory>30</maxHistory> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %replace(%msg){'1[3-9]\d{9}', '*** **** ****'}%n</pattern> </encoder> </appender> <root level="INFO"> <appender-ref ref="CONSOLE"/> <appender-ref ref="FILE"/> </root> <!-- 审计日志单独输出 --> <logger name="AUDIT_LOGGER" level="INFO" additivity="false"> <appender-ref ref="AUDIT"/> </logger> <!-- 敏感日志脱敏输出 --> <logger name="DESENSITIZE_LOGGER" level="INFO" additivity="false"> <appender-ref ref="DESENSITIZE"/> </logger> </configuration>在业务代码中记录审计日志:
@Slf4j @Service public class ContractService { private static final Logger auditLogger = LoggerFactory.getLogger("AUDIT_LOGGER"); public void signContract(Contract contract) { // ... 业务逻辑 auditLogger.info("合同签署: userId={}, houseId={}, tenantId={}, amount={}", SecurityUtils.getUserId(), contract.getHouseId(), contract.getTenantId(), contract.getDeposit()); } }参数说明:
maxHistory="30"表示只保留最近30天的日志文件,避免磁盘爆满;%replace(...)是 Logback 内置的正则脱敏,1[3-9]\d{9}匹配中国大陆手机号,替换成*** **** ****;审计日志AUDIT_LOGGER独立文件,方便运维团队提取操作流水。
5.3 一键部署脚本:build.sh 自动打包、上传、重启
在项目根目录创建build.sh:
#!/bin/bash # 房屋出租系统一键部署脚本 # 用法:./build.sh frontend|backend|all set -e # 任何命令失败立即退出 FRONTEND_DIR="./rental-frontend" BACKEND_DIR="./rental-backend" SERVER_IP="192.168.1.100" # 替换为你的服务器IP SERVER_USER="deploy" if [ "$1" = "frontend" ] || [ "$1" = "all" ]; then echo "=== 构建前端 ===" cd $FRONTEND_DIR npm install npm run build echo "前端构建完成,dist 目录已生成" echo "=== 上传前端到服务器 ===" rsync -avz --delete dist/ $SERVER_USER@$SERVER_IP:/var/www/rental-frontend/dist/ echo "前端上传完成" fi if [ "$1" = "backend" ] || [ "$1" = "all" ]; then echo "=== 构建后端 ===" cd $BACKEND_DIR ./mvnw clean package -DskipTests echo "后端构建完成,target/rental-backend-0.0.1-SNAPSHOT.jar 已生成" echo "=== 上传后端到服务器 ===" scp target/rental-backend-0.0.1-SNAPSHOT.jar $SERVER_USER@$SERVER_IP:/opt/rental/ echo "=== 重启后端服务 ===" ssh $SERVER_USER@$SERVER_IP " sudo systemctl stop rental-backend sudo cp /opt/rental/rental-backend-0.0.1-SNAPSHOT.jar /opt/rental/rental.jar sudo systemctl start rental-backend sudo systemctl status rental-backend --no-pager | head -10 " echo "后端重启完成" fi echo "✅ 部署完成!访问 http://$SERVER_IP 查看效果"赋予执行权限并运行:
chmod +x build.sh ./build.sh all技巧:
set -e是防错底线,任何一步失败脚本立即终止,避免部分更新导致系统不一致;`rsync --
本文还有配套的精品资源,点击获取