这几年社区服务类网站又成了毕业设计的热门选择,尤其是用Java技术栈、SpringBoot框架来做前后端分离的智慧社区平台。我接触过不少类似项目,最近一套就是围绕“后疫情时代小区服务网站”这个方向展开的,功能覆盖了居民健康信息登记、异常上报处理、维修申报、生活物资代购、社区公告和志愿者管理,底层用SpringBoot + MyBatis-Plus + MySQL,前端配Vue3。整个项目跑下来,最大的感觉是:这类系统看似是个标准管理后台,但业务细节一旦没设计到位,很容易变成四不像。
这篇就以这个模拟项目X为例,把从需求拆解、表结构设计、核心接口实现,到部署避坑、答辩准备的全过程展开说一下。如果你正在准备Java方向的毕设,或者刚接触SpringBoot开发,想找个完整项目练手,这套思路可以直接参考。项目本身不复杂,真正考验人的地方在于:能不能把社区里的日常事务抽象成合理的表结构,能不能把角色权限边界讲清楚,能不能用少量代码把核心流程跑通。
1. 项目整体设计与需求梳理
1.1 为什么小区服务网站总被当成毕设热门
前几年一说到小区服务网站,很多人第一反应是“物业缴费加报修”,听起来太传统。但这两年的社区服务线上化节奏明显加快,居民登记信息、访客预约、公共区域报修、物资代购、志愿者招募这些需求集中出现,原来的“一句话需求”演变成了需要完整业务闭环的系统。所以这类项目早就不再是单纯的增删改查,而是带上了真实的业务逻辑、权限分层和流程状态流转。
站在毕设角度,这个选题有四点很现实的优势:
- 业务容易理解,不需要太多领域背景,答辩时能讲清楚;
- 模块粒度合适,一个人在一到两个月内能完成;
- 技术栈可深可浅,底层用SpringBoot加MyBatis,再配合JWT和拦截器,已经足够体现工程能力;
- 演示效果好,PC端管理后台加移动端界面,有对比就有完整度。
很多同学真正纠结的不是“做什么”,而是“怎么做才不像一个普通管理系统”。这个问题核心在于:有没有把业务问题想透,有没有在小细节上体现工程化处理。否则做出来就是一个给别人演示CRUD的工具。
1.2 需求分层与功能清单
我习惯把项目需求先分三层:必需功能、扩展功能、展示功能。必需功能是答辩底线,扩展功能决定项目完整度,展示功能用来体现设计思路。
这个社区服务平台里,必需功能至少要有:
- 用户注册、登录、个人信息维护;
- 居民房屋信息绑定,包括楼栋、单元、门牌号;
- 健康信息登记与异常状态上报;
- 社区公告发布与查看;
- 管理后台,包括用户审核、登记记录查看、异常报备汇总;
- 数据可视化看板,用ECharts展示每日登记人数、异常报备数量。
扩展功能可以加:
- 便民服务:生活物资代购、药品代购代送、快递代收提醒;
- 维修申报:居民发起维修单,物业处理,状态流转;
- 志愿者招募与任务认领;
- 访客预约与访客通行码。
再往上可以加展示功能,比如定时任务生成每日统计报表、导出Excel、短信通知提醒。这些不一定要全部做完,但要在答辩时展示“我知道怎么做”。
功能清单确定后,工作量估算才靠谱。我帮别人做规划时,一般把每个模块拆成“列表查询、新增、修改、状态变更、权限校验”五个动作去估时间。比如维修单模块,表面是一个页面,实际上包含提交、列表分页、接单、完成、评价五个接口,时间一下子就上去了。所以需求分层不是空话,它直接决定你的开发节奏。
1.3 技术路线与选型逻辑
现在毕设项目技术栈很容易长成一个样:SpringBoot + MyBatis-Plus + MySQL + Vue3 + Element Plus。这个组合当然没有问题,但答案里一定要能说清“为什么选它”,不能只说“大家都会”。
- SpringBoot:快速搭建独立Web应用,内嵌Tomcat,不用单独部署Servlet容器,这是Java后端的主流起步方式;
- MyBatis-Plus:比原生MyBatis少了很多重复CRUD代码,其中标准版官方文档清晰,适合时间紧张的毕设;更关键的是它底层还是MyBatis,不会被质疑“没用过数据库持久层框架”;
- MySQL:数据量不大、事务要求不极端,选择MySQL成本最低,资料最多;
- Vue3 + Element Plus:面向管理后台的组件很成熟,表格、表单、弹窗、标签页开箱即用,演示时视觉上就比纯JSP老项目有优势。
额外推荐的组件:JWT做接口无状态认证,Hutool处理日期、随机数、验证码,Lombok减少样板代码,ECharts做图表,EasyExcel做导出。消息队列、分布式锁、微服务这些不建议硬上,除非标题里明确写了“高并发”“微服务”,否则属于给自己挖坑。
2. 数据库设计:从业务抽象到表结构落地
2.1 实体关系与核心表
产品逻辑永远比代码逻辑重要。我见过不少同学第一版直接画前端页面,画完才开始建表,结果需求一变,表结构全推倒。正确顺序是先抽象实体,再梳理关系,最后落地成表。
这个项目里的核心实体有:用户、房屋、社区公告、健康登记记录、异常报备记录、维修单、代购订单、志愿者活动、活动报名记录、访客预约。
实体之间的关系并不复杂:
- 用户和房屋是绑定关系,一个房屋一般对应一位住户,也可以兼容家属;
- 健康登记记录和用户是多对一,一人每天可以有多条历史记录,但同一天只能有一条有效登记;
- 维修单关联用户和处理人,处理人可能是物业人员或楼栋管理员;
- 代购订单关联用户和接单人,接单人可能是志愿者;
- 志愿者活动和报名记录是多对多,主表保存活动信息,中间表保存报名状态。
我最看重的一张表是用户表。它不能只存账号密码,还要带上真实姓名、手机号、角色、房屋ID和账号状态。很多毕设项目把角色写死在代码里,用户表只有一个user_role字段,演示没问题,但一旦想让同一个账号既当居民又当楼栋管理员,就会非常尴尬。
如果项目时间比较紧,可以设计一张单角色用户表,角色字段用字符串存,用户注册时默认是居民角色,管理员指定之后变为楼栋管理员或社区管理员。这个设计实现最快,也足够应付答辩。
2.2 关键表与字段定义
我给出几张核心表的建表思路,不是直接复制就能跑的完整脚本,而是把设计逻辑说清楚。
用户表(sys_user)核心字段:
CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50), phone VARCHAR(20), role VARCHAR(30) NOT NULL DEFAULT 'ROLE_USER', house_id BIGINT, status TINYINT NOT NULL DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );密码字段一定要用密文,不要存明文。可以用BCrypt,也可以用Hutool封装的BCrypt工具。答辩时能讲清楚“登录时比对密文”就算合格。
健康登记表(health_record)核心字段:
CREATE TABLE health_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, temperature DECIMAL(4,1), health_status TINYINT COMMENT '1正常 2异常', symptom_description VARCHAR(500), remark VARCHAR(500), register_time DATETIME DEFAULT CURRENT_TIMESTAMP );这里有一个小细节:体温字段用DECIMAL,不要用FLOAT。FLOAT精确度问题虽然在这个场景不明显,但做后端的人对存储金额和精度敏感是加分项。
异常报备表(abnormal_report):
CREATE TABLE abnormal_report ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, health_record_id BIGINT, report_content VARCHAR(500), handler_id BIGINT, handle_time DATETIME, handle_result VARCHAR(500), status TINYINT DEFAULT 0 COMMENT '0待处理 1已处理' );社区公告表、维修单表、代购订单表也都是类似套路:主键ID、业务字段、状态字段、关联用户ID、时间字段。唯一提醒是时间字段要区分创建时间和处理时间,不要一个create_time从头用到尾。
2.3 权限模型与索引设计
这个项目我建议做三级角色:社区居民、楼栋管理员、社区管理员。不用引入Spring Security完整RBAC那一套,但最好实现一个可扩展的角色枚举。
- 社区居民:只能操作自己的数据,比如填自己的健康登记、报自己的维修单、查自己的代购订单;
- 楼栋管理员:能看到本楼栋范围内登记记录、报备列表,处理本楼栋的便民服务工单;
- 社区管理员:全量数据可见,能发布公告、导出报表、管理志愿活动。
如果只用is_admin布尔字段,看起来简单,但答辩时老师问“楼栋管理员和社区管理员怎么区分”就答不上来。角色用字符串存,接口里加一个校验工具类就够。
数据权限这块有一点必须提前想清楚:楼栋管理员看数据时,不能只在前端隐藏按钮,后端接口必须过滤数据范围。我的做法是先从房屋表查出楼栋管理员负责的楼栋编号集合,再关联用户ID集合,最后用IN条件查询登记记录。这个逻辑不复杂,但体现的是系统安全性,答辩时值得重点讲。
索引设计建议:用户表username建唯一索引,健康登记表user_id加普通索引,维修单表status加普通索引,代购订单表user_id和status建联合索引。基本查询场景都能照顾到。物理外键尽量不要用,用逻辑外键加应用层校验,避免删除住户时把历史单据一起删掉。这也是一个可以讲的设计取舍。
3. 核心功能实现与关键代码解析
3.1 登录认证与JWT拦截器
这个项目的前后端是分离的,所以登录认证用JWT比用Session更顺。整体逻辑是:用户提交用户名密码,后端校验通过后签发Token,前端把Token存到本地,每次请求在Header里带Authorization,后端写一个Interceptor统一解析Token。
登录接口的核心代码:
@PostMapping("/login") public R login(@RequestBody LoginDTO dto) { User user = userService.findByUsername(dto.getUsername()); if (user == null || !BCrypt.checkpw(dto.getPassword(), user.getPassword())) { return R.error("用户名或密码错误"); } if (user.getStatus() == 0) { return R.error("账号已被禁用"); } String token = JwtUtil.createToken(user.getId(), user.getUsername(), user.getRole()); return R.ok().put("token", token).put("user", user); }注意两个细节。
第一,登录接口必须放行。很多同学写完拦截器后发现登录也跳401,怎么调都不对,原因是Interceptor把/login也拦截了。注册拦截器时用excludePathPatterns把/login、/register和静态资源路径排除掉即可。
第二,密码校验不能用“password.equals()”,因为数据库存的是BCrypt密文。BCrypt的校验方法是每次生成一个新盐值对比,所以要用checkpw。这个点如果做对了,在答辩时反而会成为亮点,能证明你理解密码存储的基本要求。
拦截器解析Token的代码:
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String auth = request.getHeader("Authorization"); if (StringUtils.hasText(auth) && auth.startsWith("Bearer ")) { String token = auth.substring(7); Claims claims = JwtUtil.parseToken(token); request.setAttribute("userId", claims.get("userId")); return true; } response.setStatus(401); return false; }这段代码还不完整,正常还要处理Token过期、签名错误等异常情况。但核心套路就是这样。
3.2 居民健康登记与异常上报
这个模块是项目的业务重心。技术上不复杂,难的是流程闭环:居民提交健康登记,如果状态异常,自动生成一条异常报备记录,楼栋管理员看到待处理列表,确认后填处理结果,状态变成已处理。
保存健康登记的核心代码:
@PostMapping("/health/register") public R register(@RequestBody HealthRecordDTO dto) { Long userId = (Long) request.getAttribute("userId"); HealthRecord record = new HealthRecord(); record.setUserId(userId); record.setTemperature(dto.getTemperature()); record.setHealthStatus(dto.getHealthStatus()); record.setSymptomDescription(dto.getSymptomDescription()); record.setRegisterTime(new Date()); healthRecordMapper.insert(record); // 状态异常时,自动生成报备记录 if (dto.getHealthStatus() != null && dto.getHealthStatus() == 2) { AbnormalReport report = new AbnormalReport(); report.setUserId(userId); report.setHealthRecordId(record.getId()); report.setReportContent(dto.getSymptomDescription()); report.setStatus(0); abnormalReportMapper.insert(report); } return R.ok("登记成功"); }这里还有一个隐藏需求:每人每天只能登记一次。最简单的实现是Service层先查询当天是否已有记录,有就直接返回“今日已登记”。代码不复杂,但能体现边界意识。
LocalDate today = LocalDate.now(); LocalDateTime start = today.atStartOfDay(); LocalDateTime end = today.plusDays(1).atStartOfDay(); LambdaQueryWrapper<HealthRecord> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(HealthRecord::getUserId, userId) .ge(HealthRecord::getRegisterTime, start) .lt(HealthRecord::getRegisterTime, end); Long count = healthRecordMapper.selectCount(wrapper);如果时间充裕,还可以在数据库层做一个组合唯一约束。但MySQL对日期函数的索引支持有限,所以应用层校验更适合毕设。
除了居民端提交,管理端还要有一个“今日登记情况”列表,楼栋管理员查看时只能看到本楼栋的用户记录。查询时先查房屋表拿到该楼栋用户ID集合,再查健康记录表。
3.3 便民服务:维修申报与代购下单
便民服务模块是做项目完整度的关键。维修申报和代购下单逻辑类似,核心是状态流转。
维修单状态可以这样定义:
- 1 待处理:居民提交后;
- 2 处理中:物业或楼栋管理员接单;
- 3 已完成:处理人填写处理结果、关闭工单;
- 4 已取消:居民提交后自行取消。
状态流转不能用前端页面控制,后端接口要校验当前状态是否允许目标状态存在。比如已完成的任务不能再次被接单。实现方式不复杂,Service层先查单,判断状态,再更新。
代购订单的状态稍微多一点:
- 1 待接单
- 2 配送中
- 3 已完成
- 4 已取消
居民下单时选择物资类型、数量、期望送达时间,管理员或志愿者接单,然后填配送完成情况。这里要注意金额字段的精度问题,用BigDecimal,不要用Double。Double在金额计算时会出现0.1+0.2不等于0.3的小数问题,现场演示很尴尬。
以维修单接单接口为例:
@PostMapping("/repair/accept") public R accept(@RequestParam Long repairId) { Long userId = (Long) request.getAttribute("userId"); RepairOrder order = repairOrderMapper.selectById(repairId); if (order == null) { return R.error("工单不存在"); } if (order.getStatus() != 1) { return R.error("当前工单状态不允许接单"); } order.setStatus(2); order.setHandlerId(userId); order.setHandleTime(new Date()); repairOrderMapper.updateById(order); return R.ok("接单成功"); }这段代码的价值不在CRUD本身,而在于“状态校验”。答辩时老师问到并发问题,你可以说:“通常一个人点击接单不会同时发生竞态,但如果要做严谨处理,可以加上乐观锁版本号。”这个回答比直接说“我没考虑”要好很多。
3.4 统计看板与通知提醒
统计看板是整场演示最容易出效果的地方。我的做法是放两个图:近7天健康登记人数折线图、异常报备来源饼图。ECharts对这两个图支持很好,后端只要返回数据数组,前端直接setOption。
查询近7天登记人数的SQL:
SELECT DATE(register_time) AS stat_date, COUNT(*) AS total FROM health_record WHERE register_time >= DATE_SUB(CURDATE(), INTERVAL 6 DAY) GROUP BY DATE(register_time) ORDER BY stat_date;需要注意的是,如果某天没有登记数据,这条SQL不会返回那天的记录,前端折线图会出现断点。简单办法是后端生成一个近7天日期数组,没有数据的日期补0。这个细节做得越细,演示越顺。
通知提醒可以用两种方式实现。一种是站内消息表,另一种是定时扫描异常表。项目里我建议先做站内信,降低复杂度。用户登录后,通过右上角红点提示“有待处理通知”。实现方式就是在用户表旁边加一个message表,关联用户ID和业务ID,前端定时拉取未读数量。
定时任务可以用SpringBoot自带的@Scheduled注解实现。例如每天凌晨把过期未处理的代购订单自动取消,同时给用户发站内信。代码量不大,但能让系统看起来更像真实产品。
4. 项目启动、部署与验收避坑
4.1 环境准备与启动流程
一套干净可复现的本地环境很重要,我建议按下面的顺序来准备。
JDK和SpringBoot版本要注意匹配。JDK8推荐用SpringBoot 2.7.x,JDK17推荐用SpringBoot 3.0以上。很多从旧教程抄配置的同学,用JDK17跑SpringBoot 2.6,会碰到IllegalArgumentException或者依赖版本冲突,排查起来非常烦躁。
MySQL建议5.7或8.0。连接串里加上serverTimezone=Asia/Shanghai,否则日期字段可能相差8小时。字符集用utf8mb4,别用utf8,否则数据库里存emoji会报错。
启动流程拆成几步:
- 打开IDEA,选择Maven项目,等待依赖下载完;
- 修改application.yml里的数据库账号密码;
- 执行项目resource目录下的sql脚本,初始化数据库;
- 运行主类,看到SpringBoot启动日志;
- 本地访问配置好的端口,看到Swagger页面或者前端页面,说明后端起来了。
这里有个建议:不要把端口固定成8080。很多同学同时开着其他服务,8080经常被占用。在application.yml里改成8081或者8090,能省下不少尴尬。
前端跑起来也不难,Vite项目执行npm install、npm run dev,访问Vite给的地址。如果访问时报跨域错误,说明前后端分离的CORS配置没做好,下一步排错就围绕这个来。
4.2 典型运行问题排查
我整理了几个毕设项目里最高频的问题,每一个都见过多次。
第一,端口被占用。启动日志里直接说Port already in use,最快的办法不是关掉占用程序,而是修改yaml里的端口,一劳永逸。
第二,前端跨域。Vite开发服务器在5173端口,后端在8080,浏览器会拦截跨域请求。常见做法是后端的WebMvcConfigurer里加CORS映射,或者前端Vite配置proxy代理。演示时我一般建议直接部署成同域,最省心。
第三,数据库连不上。报Communications link failure时先用客户端工具连接MySQL,确认账号密码正确。很多坑是密码里有特殊字符,写在yaml里没加引号。
第四,刷新页面404。前端用了History路由,部署后刷新会出现404。解决方法是nginx配置try_files,把请求回退到index.html。
第五,上传图片获取不到。建议用本地虚拟路径映射,在配置类里把某个本地目录映射为/upload/**,这样图片URL不会变成绝对路径,换环境只改配置就行。
下面表格是速查版本:
| 问题现象 | 典型原因 | 处理建议 |
|---|---|---|
| 启动端口冲突 | 8080被占用 | 修改server.port |
| 页面请求跨域 | 前后端不同源 | 配置CORS或代理 |
| 数据库连不上 | 账号密码/时区配置错误 | 检查yaml和连接串 |
| 刷新404 | History路由无兜底 | nginx配置try_files |
| 图片加载失败 | 上传路径不匹配 | 配置虚拟路径映射 |
4.3 答辩前建议准备的四件事
技术项目最怕只会写代码不会讲。我建议在答辩前留出三到五天,专门准备四件事。
第一,接口文档。哪怕只是用Swagger自动生成一页接口列表,都能让整个系统显得工程化。用Knife4j增强一下UI,演示时打开接口文档页面,比临时点十几个按钮更有说服力。
第二,导出功能。社区管理员一键导出本周登记汇总Excel,用EasyExcel十几行代码搞定。但演示效果很强,老师看到能导数据,会默认你的系统“可以落地”。
第三,测试数据。演示数据一定要提前造好,不要现场填。比如健康登记表里要有近7天连续数据,维修单要有不同状态的数据,代购订单要有待接单、配送中、已完成三类。图表才不是空的,状态流转才有演示素材。
第四,角色切换演示。最理想的情况是准备三个账号:普通居民、楼栋管理员、社区管理员。演示时先用居民账号提单,再切管理员账号处理,最后回居民账号看状态变化。这个闭环比只展示列表漂亮得多。
5. 实战体会与最后的小建议
这类项目真正的工作量往往不在编码,而在把需求边界想清楚、把数据关系理清楚。很多人一拿到选题就开始敲页面,一天写三个Controller,看起来很努力,但跑到最后发现逻辑混乱、越写越没底。正确节奏是先画页面原型,再定表结构,最后动手写接口,接口跑通后再回头打磨页面交互。
我特别建议把楼栋管理员的数据权限这条线做完整。因为能这样做,说明你理解了“不同角色看到的数据范围不同”,这正好是社区服务平台区别于普通增删改查系统的核心。
最后再分享一个实用小技巧:所有查询列表都加上分页参数,哪怕目前数据只有几十条。用了MyBatis-Plus的分页插件后,前后端交互会更标准,答辩时再顺手提一句“系统列表统一采用分页查询”,答题深度完全不一样。这类社区服务项目后续还能扩展很多方向,比如活动投票、邻里互助帖、积分商城,底层表结构不用大改,这也是我比较推荐拿它当毕设的主要原因。