做智慧社区这种全栈项目,我估计你已经看过不下几十个“xx管理系统源码”了。但说实话,市面上大量项目要么是SSM老古董套个半残前端,要么就是后端塞满业务但前端用JSP硬撑,真正能做到SpringBoot+Vue3+MyBatis前后端分离、能直接跑起来还能写进简历的,真不多。这篇就以这个“智慧社区”项目为主线,把技术选型的逻辑、数据库怎么设计、接口怎么落、前端怎么接、最后怎么打包部署,一条线全部拆开讲清楚。
先交代一下适用人群:这个项目最适合两类人,一是准备毕业设计的计算机专业学生,二是想找Java后端或全栈岗位、手里缺一个能讲清楚的项目经验的初学者。你如果已经能把SpringBoot的自动配置说个大概,知道Vue3的组合式API和Pinia是干嘛的,那看这篇会非常顺畅。如果还在起步阶段,文中每个关键点我也会补上“为什么这么做”的底层逻辑,按步骤走也能把项目搭出来。
1. 技术选型与整体架构拆解
1.1 为什么是SpringBoot+Vue3+MyBatis+MySQL这套组合
先说结论:这套组合在2026年的Java全栈项目里,依然是性价比最高、容错率最低的选择,没有之一。
后端用SpringBoot,看中的不是它“快”,而是它的生态完整度和岗位匹配度。现在你去搜Java相关的工作要求,十个里有八个写着“熟悉SpringBoot”,它已经是Java服务端的事实标准。SpringBoot的自动配置把过去SpringMVC那套繁琐的XML配置全部吞掉了,你只需要在application.yml里写几个关键参数,一个能跑的Web服务就起来了。这对做毕设或者个人项目的人来说,省下来的时间可以全部砸在业务逻辑上。
前端这块,Vue3已经是绝对的主流。Vue2在2023年底就停止了官方维护,新项目再上Vue2属于给自己挖坑。Vue3的组合式API(Composition API)带来的好处是逻辑复用变得非常干净,同一个功能相关的ref、computed、watch、方法可以放在一起,而不是像Vue2的选项式API那样按data、methods、computed分开摊大饼。配合<script setup>语法糖,写起来的手感已经非常接近React的函数组件,但模板上手门槛又比JSX低得多。
MyBatis作为持久层框架,优点就两个字:灵活。它不像JPA/Hibernate那样给你套一层厚厚的对象关系映射,而是把SQL控制权完全交回到你手里。智慧社区这种项目里有大量多表关联查询、条件动态拼装(比如按楼栋+单元+状态组合筛选业主列表),用MyBatis的动态SQL写起来极其顺手。而且国内绝大多数Java岗位面试都会问MyBatis的缓存机制、#{}和${}的区别,你项目里用了它,面试时就有话可说。
MySQL则是毫无争议的默认选项,开源、免费、社区资料多到泛滥,装个5.7或者8.0都行。智慧社区的数据量级撑死也就百万级,MySQL配合合适的索引和分页完全没压力。
1.2 前后端分离到底“分离”了什么
很多新手对前后端分离的理解就是“前端一个文件夹,后端一个文件夹”,这是最大的误解。前后端分离的核心是职责分离 + 接口契约分离:
- 前端只负责页面渲染、用户交互、状态管理,通过HTTP请求调用后端接口获取数据。
- 后端只负责业务逻辑、数据持久化、权限校验,返回JSON数据,完全不关心数据最终长成什么HTML。
- 两边通过一份接口文档(或者Swagger页面)约定请求路径、请求参数、响应格式,各自独立开发、独立部署。
这样做的好处,往小了说是开发效率高——前端不用等你后端写完才能动工,你可以先用Mock数据把页面全部搭好,后端接口写完直接替换baseURL就能联调。往大了说,前端和后端可以分别部署在不同的服务器上,前端用Nginx托管静态文件,后端跑在独立的Tomcat/内嵌容器里,流量压力可以分开处理。如果以后要做App,同一套后端接口可以直接复用给移动端——我见过不少公司就是这么干的,后端接口一套,PC网页、H5、小程序三端共用。
1.3 智慧社区项目的模块边界怎么划
拿到“智慧社区”这个需求,第一步不是写代码,而是把业务模块拆出来。拆模块的标准是:每个模块要能回答“谁在用、解决什么问题、需要哪些数据”。
结合我做过的小区物业类项目,核心模块通常这样划分:
| 模块 | 使用角色 | 核心功能 |
|---|---|---|
| 业主管理 | 物业管理员 | 业主信息的增删改查、按楼栋/单元筛选、入住状态管理 |
| 房屋管理 | 物业管理员 | 楼栋、单元、房号三级结构维护,房屋与业主关联 |
| 报修管理 | 业主、物业 | 业主提交报修单、上传图片、物业派单处理、状态流转 |
| 缴费管理 | 业主、物业 | 物业费/停车费账单生成、在线缴费记录、欠费提醒 |
| 公告通知 | 物业管理员 | 公告发布、置顶、按小区推送 |
| 车位管理 | 物业管理员 | 车位信息维护、车位与业主绑定关系 |
| 访客登记 | 安保人员 | 访客信息登记、进出记录 |
| 系统管理 | 超级管理员 | 用户管理、角色管理、菜单权限分配 |
这套模块划分的逻辑是:以“人—房—钱—事”为主线。业主和房屋是基础数据,缴费是资金流,报修和访客是日常事务流,公告是信息流,系统管理是所有系统的底座。你做的时候不需要一次性全做,挑四五个核心模块就能支撑起一个完整的演示流程——我后面给出的表结构也优先围绕“业主—房屋—账单—报修”这条链路来设计。
2. 后端工程落地:SpringBoot+MyBatis的骨架搭建
2.1 项目初始化和分层规范
后端工程建议直接用Spring Initializr生成(start.spring.io),或者用IDEA自带的Spring Initializr。关键依赖勾选:Spring Web、MyBatis Framework、MySQL Driver、Lombok。版本上不用刻意追新,SpringBoot 2.7.x或者3.x都可以,但要注意如果你的JDK是8,就别选SpringBoot 3.x了——3.0起强制要求JDK 17+,这是个很常见的坑。
工程建好之后,分包结构建议这样:
com.example.community ├── controller // 接口层:接收请求、参数校验、返回结果 ├── service // 业务层:核心逻辑处理(接口+实现类) ├── mapper // 数据访问层:MyBatis的Mapper接口 ├── entity // 实体类:对应数据库表 ├── dto // 数据传输对象:接收前端参数、返回前端结果 ├── config // 配置类:跨域、拦截器、Knife4j等 ├── common // 公共类:统一返回结果、异常处理、工具类 ├── interceptor // 拦截器:登录鉴权这个分层方式是基于实际业务反复打磨后的结果。核心原则就一条:Controller不写业务逻辑,Service不写SQL,Mapper只做数据访问。很多新手图省事,直接在Controller里写userMapper.selectById(),短期内是快,但一旦项目变复杂,你会发现自己改一个逻辑要翻遍所有Controller,那感觉就是灾难。规范分层看起来多写了几个类,但每个类的职责单一清晰,后面扩展功能、排查问题都会快得多。
2.2 统一返回结果和全局异常处理必须做
前后端分离项目里,前后端必须约定一个统一的接口返回格式。我推荐用最经典的:
{ "code": 200, "message": "操作成功", "data": { } }前端对所有接口都按这个结构解析:code为200时取data渲染页面,非200时弹出message提示。这比直接返回裸数据要舒服得多——一旦后端报错,前端能拿到明确的错误信息,而不是解析出一个undefined然后页面白屏。
对应的后端代码很简单,定义一个泛型类:
@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }全局异常处理用@RestControllerAdvice,把校验异常、业务异常、数据库异常统一拦截,转成上面的Result格式返回。这里有个我踩过的坑:如果异常不统一处理,SpringBoot默认会把异常堆栈直接抛给前端,不仅信息冗余、不友好,还会暴露内部实现细节(比如SQL错误里带着表名和字段名),做项目或者答辩的时候都很扣分。
2.3 MyBatis的XML映射与动态SQL实战
这套项目里我强烈建议用XML方式写SQL,而不是用注解。原因有两个:第一,智慧社区这种业务有大量动态条件查询,XML里<if>标签拼条件比注解里的@Select拼字符串强太多了;第二,面试官问到MyBatis,十有八九会问你动态SQL和XML映射的细节,你在项目里用过XML,回答起来底气完全不一样。
举一个业主管理的典型查询:按业主姓名、手机号、楼栋号、入住状态筛选,同时分页。Mapper接口定义:
public interface OwnerMapper { List<OwnerVO> selectOwnerPage(@Param("name") String name, @Param("phone") String phone, @Param("building") String building, @Param("status") Integer status, @Param("offset") int offset, @Param("size") int size); long countOwner(@Param("name") String name, @Param("phone") String phone, @Param("building") String building, @Param("status") Integer status); }对应的XML:
<select id="selectOwnerPage" resultType="com.example.community.vo.OwnerVO"> SELECT o.id, o.name, o.phone, o.id_card, o.status, h.building_no, h.unit_no, h.room_no FROM owner o LEFT JOIN house h ON o.house_id = h.id <where> <if test="name != null and name != ''"> AND o.name LIKE CONCAT('%', #{name}, '%') </if> <if test="phone != null and phone != ''"> AND o.phone LIKE CONCAT('%', #{phone}, '%') </if> <if test="building != null and building != ''"> AND h.building_no = #{building} </if> <if test="status != null"> AND o.status = #{status} </if> </where> ORDER BY o.create_time DESC LIMIT #{offset}, #{size} </select>注意几个细节:<where>标签会自动去掉第一个多余的AND,比手动写WHERE 1=1干净得多;模糊查询别用'%${name}%',会有SQL注入风险,要用CONCAT('%', #{name}, '%');分页参数offset和size在接口方法上用@Param指定名字,XML里才能用#{offset}直接引用。表名和字段名都是下划线命名,返回对象如果希望直接自动映射到驼峰格式(比如building_no对应buildingNo),需要在application.yml里加一行配置:
mybatis: configuration: map-underscore-to-camel-case: true这个配置省掉大量resultMap手写工作,强烈建议一开始就加上。
3. 数据库设计:智慧社区的核心表结构
3.1 表结构设计总览与设计准则
数据库设计是这类项目的根基,表建得烂,后面每一个查询都在还债。设计原则就一条:按业务实体建模,而不是按页面建模。别因为页面上需要一个“业主列表带房屋信息”,就把业主字段和房屋字段揉进一张表。正确做法是业主一张表、房屋一张表,查询时关联。这样以后房屋换业主、业主买多套房,数据模型都不用动。
核心表建议这么设计(我给出建表SQL的关键部分):
-- 业主表 CREATE TABLE `owner` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL COMMENT '业主姓名', `phone` varchar(20) NOT NULL COMMENT '手机号', `id_card` varchar(18) DEFAULT NULL COMMENT '身份证号', `house_id` bigint DEFAULT NULL COMMENT '关联房屋ID', `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:0迁出 1入住 2未入住', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_phone` (`phone`), KEY `idx_house_id` (`house_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='业主信息表'; -- 房屋表 CREATE TABLE `house` ( `id` bigint NOT NULL AUTO_INCREMENT, `building_no` varchar(10) NOT NULL COMMENT '楼栋号', `unit_no` varchar(10) DEFAULT NULL COMMENT '单元号', `room_no` varchar(20) NOT NULL COMMENT '房号', `area` decimal(10,2) DEFAULT NULL COMMENT '建筑面积', `owner_id` bigint DEFAULT NULL COMMENT '当前业主ID', PRIMARY KEY (`id`), UNIQUE KEY `uk_building_unit_room` (`building_no`, `unit_no`, `room_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='房屋信息表'; -- 账单表 CREATE TABLE `bill` ( `id` bigint NOT NULL AUTO_INCREMENT, `owner_id` bigint NOT NULL COMMENT '业主ID', `house_id` bigint NOT NULL COMMENT '房屋ID', `bill_type` varchar(20) NOT NULL COMMENT '账单类型:物业费/停车费/水费', `amount` decimal(10,2) NOT NULL COMMENT '金额', `period` varchar(20) DEFAULT NULL COMMENT '账期:2026-01', `status` tinyint NOT NULL DEFAULT '0' COMMENT '状态:0未缴 1已缴', `pay_time` datetime DEFAULT NULL COMMENT '缴费时间', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_owner_id` (`owner_id`), KEY `idx_house_id` (`house_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='缴费账单表'; -- 报修表 CREATE TABLE `repair` ( `id` bigint NOT NULL AUTO_INCREMENT, `owner_id` bigint NOT NULL COMMENT '报修业主ID', `content` varchar(500) NOT NULL COMMENT '报修内容', `images` varchar(1000) DEFAULT NULL COMMENT '图片路径,逗号分隔', `status` tinyint NOT NULL DEFAULT '0' COMMENT '状态:0待处理 1处理中 2已完成 3已取消', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `handle_time` datetime DEFAULT NULL COMMENT '处理时间', PRIMARY KEY (`id`), KEY `idx_owner_id` (`owner_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报修工单表';3.2 几个关键的字段设计细节
第一,统一的ID策略。直接用数据库自增bigint就好。别在毕设项目里引入雪花算法或者MyBatis-Plus的ASSIGN_ID,那是分布式场景下才需要的东西,单库单表自增ID性能完全够用,而且肉眼排查数据时自增ID最直观。
第二,状态字段用tinyint而不是varchar。比如业主状态0/1/2、账单状态0/1,存数字的好处是查询快、存储小、便于做条件筛选。前端展示时再通过枚举或者字典翻译成“已入住”“未入住”这种文案。如果你前后端都在自己手里,甚至可以约定好后端直接返回翻译好的状态名,省得前端每个字段都做映射。
第三,时间字段统一datetime,并设置默认值。create_time用DEFAULT CURRENT_TIMESTAMP、update_time加ON UPDATE CURRENT_TIMESTAMP,这样插入和更新时你连代码都不用写,数据库自动维护,少写很多重复代码。
第四,逻辑外键,但物理上不建外键约束。表里保留house_id、owner_id这种关联字段,但不要真去建FOREIGN KEY约束。原因很实际:智慧社区这种管理系统删除操作比较频繁(比如测试数据要清理),物理外键会挡住删除,而且影响写入性能。关联完整性靠代码和接口层保证就够了。
3.3 初始化数据的准备技巧
项目演示的时候,最尴尬的场面就是数据库里没数据,页面空空如也。所以建完表之后,一定要准备一份初始化SQL,造一些看起来真实的数据。
造数有几个技巧:名字就用“张三丰”“李秀英”这种常用名,手机号用13x开头的虚拟号段,楼栋就1栋、2栋、3栋各造几户。报修数据把状态分布开,既有待处理的,也有处理中的和完成的,这样演示时所有页面都有内容可看。账单数据要造一些已缴的、一些未缴的,而且未缴的要有最近的账期(比如当前月份),这样你演示“欠费催缴”功能时才有素材。一份好的初始化数据,能让你的项目答辩效果提升一个档次,这句话我说在前头。
4. 前后端接口联动:从JWT鉴权到Vue3页面
4.1 登录鉴权方案:JWT还是Session
智慧社区项目里有业主端和物业端,必须做登录鉴权。现在主流方案就两个:Session和JWT。我的意见很明确:用JWT,但只是简单的JWT,不引入Spring Security。
为什么不引入Spring Security?因为它的概念体系对初学者太重了——过滤器链、UserDetailsService、SecurityContext、各种放行配置,你光搞明白这套东西就要好几天,而且它的版本配置在SpringBoot 2.7和3.x之间又不一致,一套配置写错就是全线报错。智慧社区的权限粒度做到“登录才能访问接口,管理员才能访问管理接口”这个级别,自己写个拦截器也就十几行代码。
JWT流程如下:
- 用户提交用户名密码,后端校验通过后生成JWT返回前端。
- JWT里只放用户ID、用户名、角色这些非敏感信息,不要放密码。
- 前端拿到JWT存在
localStorage,后续每次请求在Authorization请求头带上Bearer <token>。 - 后端拦截器校验JWT签名和有效期,通过则放行并把用户信息放入请求上下文。
后端生成JWT用jjwt库,引入依赖后核心代码大致长这样:
// 生成token String token = Jwts.builder() .setSubject(String.valueOf(user.getId())) .claim("username", user.getUsername()) .claim("role", user.getRole()) .setExpiration(new Date(System.currentTimeMillis() + 86400000)) // 24小时有效期 .signWith(SignatureAlgorithm.HS256, secretKey) .compact();拦截器里解析并校验:
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String header = request.getHeader("Authorization"); if (header == null || !header.startsWith("Bearer ")) { response.setStatus(401); return false; } String token = header.substring(7); try { Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody(); request.setAttribute("userId", claims.getSubject()); request.setAttribute("role", claims.get("role")); return true; } catch (Exception e) { response.setStatus(401); return false; } }这里有个实操细节:secretKey要用足够长的字符串,至少32个字符。不然jjwt在解析时会因为密钥强度不够直接抛异常,这个错误信息写得比较隐晦,我第一次用的时候调了半天才反应过来。
4.2 跨域问题与接口联调配置
前后端分离开发模式下,前端跑在localhost:5173(Vite默认端口),后端跑在localhost:8080,端口不同必然产生跨域。Vite自带的代理功能是开发环境最优解,不需要后端动任何代码。在vite.config.js里配置:
export default defineConfig({ server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })这样前端请求/api/owner/list时,Vite会把它转发到http://localhost:8080/api/owner/list,浏览器看到的请求是同源的,就不会有跨域报错。生产部署时,如果前端用Nginx托管、后端是独立服务,也同样用Nginx的location /api做反向代理,思路完全一致。
另一个方案是后端加全局跨域配置,用@CrossOrigin或者CorsFilter。但这个方法只适合开发阶段临时用,生产环境不建议轻易开放跨域——API暴露给任何域名,等于关掉了浏览器同源策略这道防线。优先用代理方案,这是行业里的基本共识。
4.3 Vue3前端工程搭建和页面骨架
前端工程用Vite创建,命令就一句:
npm create vite@latest community-web -- --template vue项目创建后,先装路由、状态管理和UI组件库:
npm install vue-router@4 pinia element-plus axios sassElement Plus是这套项目的UI基座,它的表格、表单、弹窗、菜单组件都很成熟,做管理后台可以省一大半样式工作。装完之后在main.js里全局注册组件——注意Element Plus的图标是按需引入的,第一次用全量引入@element-plus/icons-vue可能体积大一点,但对开发体验更友好,打包时可以做优化,先跑起来要紧。
路由设计需要用到嵌套路由,后台管理页面的典型结构是一个左侧菜单+右侧内容区,菜单对应父路由,不同页面是子路由:
const routes = [ { path: '/login', component: Login }, { path: '/layout', component: Layout, redirect: '/dashboard', children: [ { path: 'dashboard', component: Dashboard }, { path: 'owner', component: OwnerList }, { path: 'house', component: HouseList }, { path: 'bill', component: BillList }, { path: 'repair', component: RepairList } ] } ]路由守卫用beforeEach,判断有没有token、要去的页面是不是登录页,逻辑很简单但很关键:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.path !== '/login' && !token) { next('/login') } else { next() } })4.4 Axios封装:统一请求头和错误拦截
前端调用后端接口,必须要封装一个Axios实例。如果每个页面都直接axios.get('/api/xxx'),你后续要加token、要统一处理401超时,就得改几十个文件。封装之后就一个文件要动,这就是工程化思维。
import axios from 'axios' const request = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器:自动带上token request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) // 响应拦截器:统一处理业务code和HTTP错误 request.interceptors.response.use( response => { const res = response.data if (res.code === 200) { return res } ElMessage.error(res.message) return Promise.reject(new Error(res.message)) }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') ElMessage.error('登录已过期,请重新登录') } else { ElMessage.error('网络请求异常') } return Promise.reject(error) } )封装好之后,页面里就能非常简洁地调用:
const listData = ref([]) const loading = ref(false) const fetchList = async () => { loading.value = true try { const res = await request.get('/owner/list', { params: queryParams }) listData.value = res.data.records total.value = res.data.total } finally { loading.value = false } }4.5 页面落地:一个完整的业主管理模块
业主管理模块是最能体现前后端联动的模块,建议第一个做。页面结构是:顶部条件搜索区(姓名、手机号、楼栋号)+ 数据表格 + 分页器 + 新增/编辑弹窗。
用<script setup>组织逻辑时,把“搜索条件”定义成一个响应式对象,查询、重置、分页变化都复用同一个fetchList方法:
const queryParams = reactive({ name: '', phone: '', building: '', pageNum: 1, pageSize: 10 }) const handleQuery = () => { queryParams.pageNum = 1 fetchList() } const handleReset = () => { queryParams.name = '' queryParams.phone = '' queryParams.building = '' queryParams.pageNum = 1 fetchList() }表格列渲染时要注意状态字段的处理。后端返回的status是数字,直接展示出来用户看不懂,要么在表格列里用formatter转换,要么用标签组件配合枚举对象:
const statusMap = { 0: { label: '迁出', type: 'info' }, 1: { label: '入住', type: 'success' }, 2: { label: '未入住', type: 'warning' } }然后在表格列里这样绑定::formatter="(row) => statusMap[row.status]?.label || '未知'"。
新增和编辑共用一个弹窗组件,关键点就是区分isEdit状态:新增时表单留空,编辑时用当前行数据回填。提交成功后关弹窗+重新拉列表+提示成功,三步一气呵成。这块代码逻辑不难,但它是所有管理模块的模板,做顺了这个,房屋、账单、报修都是复制粘贴微调的节奏。
5. 部署上线:从开发环境到生产环境
5.1 前端打包与SpringBoot结合的两种方式
项目做完了要演示、要交给导师检查,这时候面临部署问题。前后端分离的部署方式无非两种:分开部署和合并部署。
分开部署是标准生产模式:前端打包后丢给Nginx托管,后端打成Jar包直接java -jar跑。这种模式的好处是前后端完全独立,扩容互不影响,但需要你有一台服务器或者演示时开两个进程端口。
合并部署是毕设演示的省事方案:Vue项目构建后生成的dist目录,直接把里面的文件复制到SpringBoot的src/main/resources/static目录下,重新打包后一个Jar包全搞定。访问时后端同时承担静态文件服务和接口服务。这样你只需要跑一个进程,无论拷到哪里给别人演示都方便。但有个细节要注意:开发时Vite代理的/api前缀,生产部署时如果前端静态资源和后端接口在同一个应用中,前端里的请求路径必须保持/api前缀,并且后端Controller的RequestMapping也要带/api前缀,这样请求/api/xxx才能正确路由到接口,而不会和静态文件冲突。
如果走Nginx部署,配置参考:
server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; # 前端路由是history模式,必须加这句 } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这句try_files $uri $uri/ /index.html;是Vue Router用history模式的前提,少了它刷新页面就会404。
5.2 打包命令与构建配置
前端打包命令:
npm run build构建产物在dist目录下。如果你要把前端合并进SpringBoot,别忘了检查vite.config.js里的base选项——如果部署在根路径,保持默认就行;如果部署在某个子路径(比如/admin),就得设置base: '/admin/',否则资源路径会全部404。
后端打包前,把application.yml里数据库连接、JWT密钥等环境相关配置再核对一遍。多环境配置建议用spring.profiles.active机制拆成application-dev.yml和application-prod.yml,开发环境连本地数据库,生产环境连接服务器数据库,切换只需改一个激活项。打包命令:
mvn clean package -DskipTests启动:
java -jar target/community-server.jar --spring.profiles.active=prod5.3 文档和演示脚本准备
这个建议很少人提,但实战价值很高。项目做完后,写一份演示脚本(对,和写剧本一样),把系统核心操作流程串起来:
- 管理员登录,展示首页数据看板。
- 进入业主管理,搜索“张三”,展示查询结果。
- 新增一个业主+关联房屋,演示弹窗表单。
- 进入缴费管理,选一个欠费账单标记已缴。
- 以业主身份登录,提交一个报修单,再切回管理员处理工单。
把这份脚本练熟,是答辩或面试时展示项目的关键。很多人口头描述项目时逻辑混乱,但跟着脚本一步步操作,语言自然清晰。这份脚本我建议写到项目的README里,既方便自己,也方便导师或面试官快速了解系统。
6. 高频报错排查与避坑清单
6.1 联调阶段的经典报错
前后端联调是问题最多的阶段,我把最常见的问题按出现频率排个序:
数据库连接报错。常见两种:一是Access denied for user 'root'@'localhost',说明用户名密码错了,检查application.yml里的username和password;二是Public Key Retrieval is not allowed,这是MySQL 8.0的加密插件导致,JDBC连接串上加allowPublicKeyRetrieval=true&useSSL=false就解决。
端口被占用。SpringBoot默认8080端口被其他进程占了,启动直接失败。命令行执行netstat -ano | findstr 8080找到PID,然后taskkill /F /PID <pid>杀掉,或者临时换端口server.port=8081。
前端请求404。后端接口能直接访问但前端请求404时,先看请求URL对不对、/api路径有没有对上。还有一个隐藏坑:SpringBoot的@RequestMapping路径如果和前端baseURL拼接后多了一个斜杠或少了一个斜杠,都可能404,排查时直接把完整的请求URL复制到浏览器或者Postman里测试最快。
跨域(CORS)报错。浏览器控制台出现Access to XMLHttpRequest at ... has been blocked by CORS policy,开发阶段用Vite代理解决,别在后端乱加@CrossOrigin,理由前面说过。
日期格式问题。后端返回2026-01-01T10:30:00这种带T的格式,前端表格渲染出来不太好看。统一在application.yml配置:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8这里要特别注意时区配置。如果不设置time-zone: GMT+8,中国用户看到的时间会差8小时,这在缴费账单这种对时间敏感的业务里是非常容易暴露的错误。
6.2 业务逻辑层的隐藏坑
删除数据时外键关联是本项目最大的坑。比如要删除一条房屋记录,但该房屋下还有业主和账单,直接删会破坏数据完整性。业务上必须做校验:删除前先查关联表有没有数据,有就提示“该房屋下存在业主/账单,无法删除”。这才是真实系统该有的行为,也值得写进你的项目亮点里。
分页查询的总数问题。很多人只写了分页列表查询,忘了写count查询,导致前端分页组件显示的总条数永远是0。MyBatis里必须配套一个count方法,并且where条件要和列表查询完全一致。如果你用PageHelper,注意它会在本地线程里持有分页参数,用完之后要及时清理,否则下一个查询可能会被错误分页。
并发和数据一致性问题。比如两个管理员同时处理同一张报修单,后提交的覆盖了先提交的。智慧社区项目里最常用的处理方式:表里加一个version字段,更新时SET status = #{newStatus} WHERE id = #{id} AND version = #{oldVersion},受影响行数为0说明被其他人改过了,提示用户刷新重试。这个优化虽然简单,但面试时提到乐观锁控制并发,分量一下子就起来了。
6.3 前端页面的常见问题
表格数据不显示。优先检查接口返回的数据结构。我之前遇到过前端代码写res.data.records,但后端返回的是res.data.list,字段对不上页面渲染不出任何内容。这类问题用浏览器的Network面板看响应JSON一眼就能定位,比在页面代码里瞎猜快得多。
刷新404。Vue Router用了history模式,前端部署后直接刷新子页面(比如刷新/owner)出现404。这就是Nginx配置里try_files的问题,按前面的配置加好就解决。如果你把前端合并进SpringBoot,则要确保后端没有拦截/owner这种无/api前缀的路径,否则会被JWT拦截器挡住。
Element Plus表格操作列宽度不够。表格里放了编辑、删除、详情三个按钮时,操作列宽度至少要设140px,否则按钮会换行或者溢出。这算个小细节,但演示时按钮挤在一起确实很拉低观感。
7. 这套源码改造成简历项目的思路
项目跑通只是第一步,能把它讲出彩更重要。我建议你在熟悉代码之后,往这几个方向做一点深挖,面试时能讲的东西会完全不同:
第一,数据权限。现在是所有物业管理员都能看到所有业主,能不能改成“A管理员只能看A号楼的数据”?这就涉及在查询SQL里加数据范围过滤,是一个很典型的数据权限设计点。
第二,缓存优化。业主列表、楼栋信息这些变化不频繁的数据,能不能用@Cacheable做Redis缓存?引入Redis能聊的话题立刻变多:缓存穿透、缓存击穿、缓存雪崩,每个都是Java岗位的高频面试题。
第三,消息通知。报修状态变化时,怎么通知业主?可以引入WebSocket实时推送,或者用RabbitMQ做异步消息。这个点一加,项目从“CRUD管理系统”直接升级成“有事件驱动架构的系统”。
第四,文件上传改造。报修图片现在存在本地磁盘,能不能上传到对象存储服务?改造后业务逻辑不变,但扩展性和面试话题又是质的提升。
我个人在实际操作中的体会是,一个项目能走多远,不取决于它用了多新的技术,而取决于你是否能把每一层逻辑讲清楚。智慧社区这套项目最大的价值,就是它麻雀虽小五脏俱全,从数据库设计到权限控制,从联调调试到打包部署,完整覆盖了一个真实Web系统从零到一的全过程。你把它吃透一遍之后举一反三,再做任何管理系统类项目,都会发现骨架是完全一样的,只不过换了一套业务皮。
最后再分享一个小技巧:做这类项目时,坚持每天在项目仓库里提交代码,并且提交信息写清楚(比如feat: 完成业主列表分页查询)。这不仅是给自己留后路,而且面试时GitHub主页上的提交记录本身就是一种“持续开发和代码规范意识”的证明。很多技术过硬的候选人,都是栽在这类不起眼的工程习惯上。