简介:本资源为基于SpringBoot与Vue的社区维修平台毕业设计完整项目,面向计算机相关专业需要完成课程设计、毕业设计或期末大作业的学生。项目采用前后端分离架构,后端以SpringBoot(或SSM)搭建,数据库使用MySQL,开发环境涉及JDK、IDEA与Tomcat,适合具备一定Java与前端基础、希望直接部署运行并在此基础上二次开发的学习者。压缩包共808个文件,约23.76MB,涵盖122个Java后端源码、46个Vue组件、153个JavaScript脚本、44个CSS样式与43个HTML页面,另有SQL数据库脚本、项目说明文档及论文参考,并包含svg、png、jpg等界面素材与xml、yml等配置文件,结构完整便于按模块查阅。目前已有162人学习下载。资源提供可直接运行的源码与数据库脚本,读者能据此快速理解社区维修业务的表结构设计、接口分层与前端页面组织方式,也可参考论文梳理需求分析与系统实现思路,适合作为毕设模板或改造起点。
1. 社区维修平台为什么总在“接单”这一步翻车
做过社区维修类系统的人多半有同一个体会:需求文档里写得最热闹的是“智能派单”“地图定位”,真正上线后最先崩的却是最不起眼的接单状态流转。一个居民提交了“厨房水管漏水”,维修工点开列表、点了接单,结果两个人同时抢到同一单,或者接单后刷新页面状态又回到“待接单”。这类问题不是业务逻辑有多难,而是并发状态更新和前后端状态不同步这两件事没处理好。
这篇笔记围绕一个基于 SpringBoot + Vue 的社区维修平台展开,把这类系统从数据建模、接口设计、状态机落地到前后端联调的完整路径讲清楚。它适合正在做 Java 毕业设计、需要一套能跑通且经得起答辩追问的项目的同学,也适合想拿一个真实业务场景练 SpringBoot 分层和 Vue 组件通信的开发者。核心不是堆功能,而是让“报修—派单—接单—完工—评价”这条主链路稳定跑起来,顺带把权限、文件上传、分页这些绕不开的工程细节处理干净。
2. 社区维修平台的数据建模与状态机设计
动手写代码之前,先把实体和状态定死,否则后面每加一个功能都要回头改表。社区维修平台的业务对象其实不多,但关系要理清:用户分居民和维修工两种角色,报修单是核心,派单记录和评价是附属。
2.1 五张核心表与字段取舍
我一般会先落这几张表,字段够用就行,不要一上来就加一堆预留字段。
| 表名 | 关键字段 | 说明 |
|---|---|---|
| user | id, username, password, role, phone, avatar | role 区分 resident / worker / admin |
| repair_order | id, resident_id, worker_id, category, description, images, status, address, create_time, finish_time | 报修单主表 |
| order_log | id, order_id, operator_id, action, remark, create_time | 状态流转日志,答辩时很加分 |
| evaluation | id, order_id, score, content, create_time | 评价,一单一评 |
| category | id, name, sort | 维修分类,如水电、家电、门窗 |
repair_order里的status用整数而不是字符串,方便索引和比较。常见取值:0 待接单、1 已接单、2 维修中、3 已完成、4 已取消。images存图片路径的 JSON 数组字符串,别存逗号拼接,后期解析容易出错。
2.2 状态机:把流转规则写进代码而不是散落在 Service
状态流转如果靠 if-else 散在各个方法里,改一个规则要翻遍全项目。我习惯用一个枚举加一个校验方法集中管理。
public enum OrderStatus { PENDING(0, "待接单"), ACCEPTED(1, "已接单"), REPAIRING(2, "维修中"), FINISHED(3, "已完成"), CANCELED(4, "已取消"); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code = code; this.desc = desc; } // 定义允许的流转:当前状态 -> 可到达状态 public static boolean canTransfer(int from, int to) { if (from == PENDING.code) return to == ACCEPTED.code || to == CANCELED.code; if (from == ACCEPTED.code) return to == REPAIRING.code || to == CANCELED.code; if (from == REPAIRING.code) return to == FINISHED.code; return false; } public int getCode() { return code; } public String getDesc() { return desc; } }逻辑说明:canTransfer把合法流转集中在一个地方,Service 层只调用它,不自己写判断。参数说明:from是数据库里当前状态码,to是本次操作想改成的状态码。这样加新状态或改规则只动枚举,测试也好写。
2.3 接单接口的并发处理
接单是并发最集中的地方。两个维修工同时点接单,如果不加控制,后写的会覆盖先写的。常见做法是用数据库的乐观锁或条件更新。
UPDATE repair_order SET worker_id = #{workerId}, status = 1, accept_time = NOW() WHERE id = #{orderId} AND status = 0 AND worker_id IS NULL;这条 SQL 的关键在WHERE status = 0 AND worker_id IS NULL,只有当前还是待接单且没人接的单才会被更新。Service 层判断受影响行数,返回 0 就说明被别人抢了,前端提示“手慢了,该单已被接走”。这比先查再改的两步操作可靠得多,也不需要引入分布式锁这种重方案。
3. SpringBoot 后端分层与接口落地
后端这块,毕业设计最容易犯的错是把所有逻辑塞进 Controller,或者反过来每个表都套一套完整的 mapper-service-controller 却没有任何业务。合理的做法是按业务模块分,公共能力抽出来。
3.1 统一返回体与全局异常处理
前端要的是稳定结构,不是每个接口返回格式都不一样。先定一个统一返回体。
@Data public class Result<T> { private int code; private String msg; private T data; public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.code = 200; r.msg = "ok"; r.data = data; return r; } public static <T> Result<T> fail(String msg) { Result<T> r = new Result<>(); r.code = 500; r.msg = msg; return r; } }配合@RestControllerAdvice做全局异常捕获,业务异常统一返回 fail,系统异常记日志并返回通用提示。这样前端拦截器只需要判断 code 是否为 200,不用每个接口单独处理。
3.2 分页查询:别自己造轮子
报修单列表、维修工列表都要分页。用 MyBatis 的分页插件是最省事的做法,配置一次全局生效。
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }Service 里直接page(new Page<>(pageNum, pageSize), queryWrapper),返回的 Page 对象带 total 和 records,前端拿去做分页组件。参数说明:pageNum从 1 开始,pageSize建议限制上限比如 50,防止有人传 10000 把数据库拖垮。
3.3 文件上传与访问路径
报修单要传现场照片。上传接口存到本地磁盘或对象存储,数据库只存相对路径。
@PostMapping("/upload") public Result<String> upload(@RequestParam("file") MultipartFile file) { if (file.isEmpty()) return Result.fail("文件为空"); String original = file.getOriginalFilename(); String suffix = original.substring(original.lastIndexOf(".")); // 用 UUID 重命名,避免中文名和重名问题 String fileName = UUID.randomUUID().toString().replace("-", "") + suffix; Path dir = Paths.get(uploadPath); Files.createDirectories(dir); file.transferTo(dir.resolve(fileName).toFile()); return Result.success("/files/" + fileName); }逻辑说明:重命名是为了避免同名覆盖和中文路径在不同系统下的编码问题。参数说明:uploadPath从配置文件读,不要硬编码。返回相对路径,前端拼接域名访问。注意限制文件类型和大小,在配置里设spring.servlet.multipart.max-file-size。
4. Vue 前端的角色路由与状态同步
前端这块,社区维修平台有两个容易翻车的点:一是不同角色看到的菜单和页面不一样,二是接单后列表状态不刷新。
4.1 基于角色的动态路由
居民、维修工、管理员登录后应该看到不同的功能入口。常见做法是路由表里给每个路由加 meta.roles,登录后根据角色过滤。
const routes = [ { path: '/order/list', component: OrderList, meta: { roles: ['resident', 'admin'] } }, { path: '/order/hall', component: OrderHall, meta: { roles: ['worker'] } }, { path: '/order/my', component: MyOrders, meta: { roles: ['worker'] } } ]; // 路由守卫里校验 router.beforeEach((to, from, next) => { const role = store.state.user.role; if (to.meta.roles && !to.meta.roles.includes(role)) { next('/403'); } else { next(); } });逻辑说明:meta.roles 声明该页面允许的角色,守卫统一拦截。参数说明:store.state.user.role来自登录后存的状态,刷新页面要从 localStorage 恢复,否则守卫会误判。
4.2 接单后列表状态同步的两种做法
维修工在接单大厅点接单,成功后该单应该从大厅消失或变成已接状态。做法一是重新拉取列表,简单但体验一般;做法二是本地更新。
async function acceptOrder(orderId) { const res = await acceptApi(orderId); if (res.code === 200) { // 本地把该单从待接列表移除,避免整页刷新 this.orderList = this.orderList.filter(o => o.id !== orderId); this.$message.success('接单成功'); } else { // 被别人抢了,重新拉取保证数据准确 this.$message.warning(res.msg); this.loadOrders(); } }逻辑说明:成功时本地过滤,失败时重新拉取,兼顾体验和一致性。参数说明:orderId是当前操作的单号,acceptApi是封装好的请求方法。注意失败分支一定要重新拉取,否则页面显示的还是过期数据。
4.3 表单校验与提交防抖
报修提交按钮如果不做防抖,用户手快连点两次就会生成两条单。前端加一个 loading 状态或直接禁用按钮。
data() { return { submitting: false }; }, methods: { async submitOrder() { if (this.submitting) return; this.submitting = true; try { await createOrderApi(this.form); this.$message.success('提交成功'); this.$router.push('/order/list'); } finally { this.submitting = false; } } }逻辑说明:submitting作为闸门,请求期间重复点击直接返回。参数说明:放在 finally 里复位,保证请求失败后按钮还能用。后端也应该做幂等,但前端这层能挡掉大部分重复提交。
5. 联调阶段最容易踩的坑与排查
功能写完到能演示之间,往往还差一轮排错。下面几条是我在类似项目里反复遇到的。
5.1 跨域配置了还是报 CORS 错误
现象:前端请求后端接口,浏览器控制台报跨域,但后端明明加了@CrossOrigin。
原因:跨域配置加在了 Controller 上,但请求被拦截器或过滤器提前处理了,或者前端请求带了自定义 header 触发了预检,而预检请求没被放行。
解决:统一在配置类里配全局跨域,并确保拦截器放行 OPTIONS 请求。
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowCredentials(true) .maxAge(3600); } }注意allowedOriginPatterns和allowCredentials(true)同时用时不能用allowedOrigins("*"),否则会报错。
5.2 登录后刷新页面角色丢失
现象:登录后能正常跳转,但按 F5 刷新,路由守卫把用户踢回登录页。
原因:用户信息只存在 Vuex 里,刷新后内存状态清空,守卫读不到 role。
解决:登录成功后把 token 和用户信息写进 localStorage,应用初始化时从 localStorage 恢复到 store。守卫里判断 token 是否存在,存在就放行并异步拉取用户信息。
5.3 图片上传成功但页面显示裂图
现象:上传接口返回成功,数据库也有路径,但 img 标签显示不出来。
原因:后端把文件存到了项目运行目录,但静态资源映射没配,或者前端拼接的路径少了前缀。
解决:配置静态资源映射,把上传目录暴露成可访问路径。
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/files/**") .addResourceLocations("file:" + uploadPath); }注意file:后面跟的是绝对路径,末尾要带斜杠,Windows 下路径分隔符也要注意。
5.4 分页查询总数不对
现象:列表数据正确,但 total 始终等于当前页条数。
原因:分页插件没生效,或者手写了 count 查询但 SQL 写错。
解决:确认分页拦截器已注册,且查询用的是插件提供的 page 方法而不是自己 limit。如果自定义了 XML 查询,检查 count 语句的 where 条件是否和主查询一致。
5.5 接单接口偶发更新失败
现象:大部分时候正常,偶尔接单返回失败但单子确实被接了。
原因:并发下条件更新返回 0 行,但前端提示和后端实际状态不一致,或者事务边界没控制好。
解决:接单操作放在一个事务里,先条件更新,再写 order_log,两步都成功才提交。返回失败时前端重新拉取列表,不要只弹提示。
6. 让这套平台经得起答辩追问的两个进阶点
基础功能跑通之后,答辩老师最容易追问的是“你怎么保证数据一致性”和“如果量大了怎么办”。这两个问题不需要你真的做分布式,但要有清晰的回答思路。
第一个进阶点是状态流转的日志化。前面提到的 order_log 表,每次状态变更都记一条,包含操作人、动作、时间、备注。这样任何一个单子的完整生命周期都能查出来。实现上可以在 Service 的状态变更方法里统一写日志,而不是每个业务方法各写各的。
@Transactional public void transferStatus(Long orderId, int toStatus, Long operatorId, String remark) { RepairOrder order = orderMapper.selectById(orderId); if (!OrderStatus.canTransfer(order.getStatus(), toStatus)) { throw new BizException("非法的状态流转"); } int rows = orderMapper.updateStatus(orderId, order.getStatus(), toStatus, operatorId); if (rows == 0) { throw new BizException("状态已变更,请刷新重试"); } OrderLog log = new OrderLog(); log.setOrderId(orderId); log.setOperatorId(operatorId); log.setAction(toStatus); log.setRemark(remark); orderLogMapper.insert(log); }逻辑说明:先校验流转合法性,再条件更新,最后写日志,三步在一个事务里。参数说明:order.getStatus()是更新前的状态,作为条件传给 updateStatus,保证只有状态没被别人改过才更新成功。这样即使并发,也只有一个请求能成功,日志也不会重复。
第二个进阶点是列表查询的索引优化。报修单表随着数据增长,按状态和创建时间查询会变慢。给status和create_time建联合索引,或者按角色查询时给resident_id、worker_id单独建索引。用 explain 看一下查询计划,确保没走全表扫描。这个点答辩时说出来,比堆一堆没用的功能更有说服力。
验证方法很简单:用 JMeter 或写个简单的多线程测试,模拟 50 个并发接同一单,看是否只有一个成功。我一般会写个 main 方法起 20 个线程跑一遍,确认条件更新和事务都生效。这个习惯帮我省过好几次返工——有次就是事务注解加在了 private 方法上没生效,并发测试一跑就露馅了。
做这类平台,我的习惯是先把主链路的状态流转和并发控制做扎实,再去加评价、统计这些锦上添花的功能。答辩时老师问得最多的永远是核心链路,而不是你做了多少个页面。希望帮到你。
本文还有配套的精品资源,点击获取