资讯动态

SpringBoot+Vue公寓报修管理系统:从数据库设计到部署全解析

发布时间:2026/10/14 5:18:27 来源:尧图企业网站定制
最近整理这套基于SpringBoot Vue MyBatis MySQL的公寓报修管理系统源码时不少朋友在后台问我能不能把项目的完整思路讲透而不是只丢一个压缩包。说实话这类系统在毕设选题和全栈项目练手里出现频率极高但网上能找到的要么是阉割版要么文档和代码对不上。我干脆把自己实际搭建、改 bug、部署上线的过程完整沉淀下来从业务模型到前后端实现再到真实环境里的坑一次性讲清楚。这套系统适合谁如果你是正在找毕设题目、想系统过一遍前后端分离开发流程、或者想了解一个标准管理类项目从零到部署全过程的开发者这篇内容应该能帮你省下大量试错时间。我会按真实开发顺序来讲先梳理业务再谈选型接着拆解数据库、后端、前端最后聊部署和排查。1. 报修管理这个业务到底在解决什么问题1.1 公寓报修的真实痛点不是做个表单这么简单很多人第一反应是报修系统不就是一个用户填单子、管理员看单子的 CRUD 吗如果你只在校园 demo 里跑过确实会这么觉得。但我实际走访过几家做公寓运营的团队也帮朋友改造过他们手上的报修流程真实场景比想象中复杂得多。一个典型的公寓报修流程大概是这样的住户发现水龙头漏水或空调不制冷先报给前台或管家管家记在本子上再打电话联系维修工。维修工做完活住户要验收签字管理员还要统计这个月哪些报修是重复发生的、哪些维修拖了很久、哪个维修工响应最慢。如果中间任何一个环节靠口头或纸面信息就断了。报修单丢失、维修工白跑一趟、住户投诉没有闭环这些问题的根源都不是没人干活而是流程没有系统化。所以这套公寓报修管理系统的本质是把报修流程变成一条可追踪、可统计、可控制的任务链路住户提交、管理员分配、维修工接单处理、住户确认完成每个环节都有状态记录和时间节点。这才是它值得被做成一个完整项目的原因也是我在设计数据表和接口时反复提醒自己的核心。1.2 三端角色的权限边界住户、维修工、管理员公寓报修系统最常见的角色划分是三端住户端、维修工端、管理端。这里要特别强调权限控制不是简单地区分谁能访问哪个页面而是要把每个角色能做的操作缩减到最小必要集合。我见过不少项目把维修工和管理员的权限混在一起维修工能改报修单状态甚至能删除记录结果数据一团糟。在我这套实现里三端职责边界是这样的角色核心操作不允许的操作住户提交报修、查看自己的报修进度、确认完成、评价不能看到其他住户的工单不能修改状态维修工查看分配给自己的工单、更新处理进度、填写维修结果不能分配工单给其他人不能修改费用管理员分配工单、查看所有工单、统计报表、管理用户和楼栋不能代替住户确认验收不能删操作日志这个边界看起来简单但在需求评审阶段就要确定否则开发到一半最痛苦的就是改权限逻辑因为权限一旦写进了每个接口再去把维修工能改状态的逻辑剥出来代价特别大。1.3 从零搭建时的功能清单划分如果你准备自己从零写一个公寓报修管理系统我建议把功能按优先级拆成三个批次而不是一上来就想做满所有功能。我自己的拆分是这样的第一批核心闭环住户端提交报修 查看进度管理端分配维修工维修工端处理报修管理员查看工单列表和状态流转记录。第二批提升体验报修单图片上传、维修费用登记、住户确认验收、消息通知站内信或邮件。第三批数据价值报修类型统计、维修响应时长报表、维修工工作量排行、住户满意度评价。按照这个顺序去排期即使时间紧张也能先交付一个能跑通流程的完整系统而不是做个半成品界面。反过来说如果你看一套源码时发现连第一批核心闭环都没做完那这套代码基本不值得作为蓝本参考。2. 技术选型为什么是 SpringBoot Vue MyBatis MySQL2.1 前后端分离的出发点不是跟风是维护成本很多老教程还在用 JSP Spring Boot 的做法页面直接渲染在后端里。不是说不能做而是对报修管理系统这种需要三端访问、状态实时变化的项目来说前后端分离的优势太大了前端可以独立部署、独立迭代后端只需要提供稳定的接口即可。我记得有一次要做一个小改动——住户端报修列表加一个筛选条件。在前后端分离的结构下前端同事改一个请求参数就行后端接口天然兼容。但如果用的是 JSP 模板渲染就得同时改 controller 里的模型数据和页面模板两边牵连很容易改出问题。所以当你看到有人用前后端分离做管理类系统不要觉得是小题大做这是维护成本和协作效率的最优解。2.2 SpringBoot 与 MyBatis 的组合逻辑选 SpringBoot 几乎没有争议它是当前 Java 后端最主流的基础框架内置 Tomcat、自动配置、打包即运行对新手极其友好。但 MyBatis 这个选择我听到过不同声音。有的人觉得 MyBatis-Plus 更省事有的人觉得 JPA 更简洁。我为什么在这套系统里坚持用 MyBatis核心原因是我们这类业务查询高度依赖定制化 SQL。报修管理系统的列表页很多不是简单查一张表比如查询某栋楼本月所有未完成的报修并按类型分组统计这种需求用 JPA 要写比较复杂的查询方法或者 Query用 MyBatis 直接写 XML 里的动态 SQL可控性高很多。而且 MyBatis 对性能敏感场景能精确控制 SQL 语句不会出现自动生成的 N1 查询问题。另外MyBatis 的 XML 文件把 SQL 和业务逻辑分开后期 DBA 要调索引、优化慢查询直接改 XML 就行不用动 Java 代码。在团队协作里这是很大的便利。2.3 Vue 版本和 UI 库的现实选择前端这块我要说一个 2025 年比较现实的情况现在新项目直接选 Vue 3 是合理的Composition API Vite 的开发体验确实比 Vue 2 的 Options API 顺手很多。但如果你的学习资料大量基于 Vue 2或者你的目标是把 demo 尽快跑起来Vue 2 Element UI 也完全没有问题。我这套源码用的是 Vue 3 Element Plus Vite。Element Plus 的组件交互比较完善表格、表单、对话框、分页这些管理系统的标配组件都有配合它的表单校验规则开发住户提交报修这类页面能省很多时间。对于还没有系统学过 Vue 的同学我有个小建议不要一头扎进源码里看所有文件先看懂路由配置、页面组件的挂载关系再找一个完整的业务页面比如报修单列表从上到下读一遍比漫无目的地读每个文件效率高得多。2.4 为什么没有引入工作流引擎你可能会问既然报修单有状态流转为什么不直接上 Activiti 或 Flowable 这类工作流引擎这是一个很好的问题我刚开始设计时也纠结过。后来想明白了报修流程虽然看着环节多但它本质是一条固定顺序的状态链没有复杂的会签、或签、条件分支。用工作流引擎会引入大量额外概念——流程定义、任务实例、监听器——对于 3 个角色、5 个状态的项目来说完全是过度设计。状态机用代码里的枚举 Service 层方法控制各状态的合法性一目了然排错也容易。记住一个原则工具复杂度必须匹配业务复杂度。管理类系统里 80% 的场景手写状态机比上工作流引擎更合适。3. 数据库设计报修单、用户、维修记录怎么建模3.1 核心表结构的设计思路我建表时遵循一个总体原则报修单是绝对核心用户表做角色外键维修记录表记录每一次处理动作楼栋和房间信息单独建表便于后续统计与权限隔离。具体的核心表如下-- 用户表 CREATE TABLE sys_user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录名, password varchar(100) NOT NULL COMMENT 加密后的密码, real_name varchar(50) DEFAULT NULL COMMENT 真实姓名, phone varchar(20) DEFAULT NULL, role tinyint(4) NOT NULL COMMENT 1住户 2维修工 3管理员, building_id bigint(20) DEFAULT NULL COMMENT 所属楼栋, room_number varchar(20) DEFAULT NULL COMMENT 房间号, status tinyint(4) DEFAULT 1 COMMENT 是否启用, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 报修单表 CREATE TABLE repair_order ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 报修单号, user_id bigint(20) NOT NULL COMMENT 报修住户id, repair_type varchar(30) NOT NULL COMMENT 水/电/暖/门窗/家电等, description text COMMENT 问题描述, image_urls varchar(500) DEFAULT NULL COMMENT 图片路径多个用逗号分隔, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 1待分配 2待维修 3维修中 4待验收 5已完成 6已取消, assignee_id bigint(20) DEFAULT NULL COMMENT 维修工id, repair_fee decimal(10,2) DEFAULT NULL COMMENT 维修费用, repair_result text COMMENT 维修结果说明, appoint_time datetime DEFAULT NULL COMMENT 预约时间, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_status (status), KEY idx_assignee (assignee_id), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 维修记录表 CREATE TABLE repair_record ( id bigint(20) NOT NULL AUTO_INCREMENT, order_id bigint(20) NOT NULL, operator_id bigint(20) NOT NULL COMMENT 操作人id, action varchar(30) NOT NULL COMMENT 如分配维修工、开始维修、提交结果、确认完成, remark varchar(255) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里面有几个我认为比较关键的设计点order_no一定要用业务单号而不要直接用自增ID。住户报修后要拍照发给管理员管理员反馈一句我看一下单号——如果没有一个独立的单号只靠 ID 沟通体验很差。我生成单号的方式是日期 随机数比如BX202506031001既简短又不会撞车。3.2 状态字段千万别用字符串裸写这是我在代码审查里反复强调的一点——状态字段一定要用有意义的枚举或常量类不要直接在业务代码里写魔法值。比如你把状态存成1、2、3、4、5看数据库根本不知道什么意思时间一长自己也忘。我一般这样处理public enum RepairStatus { WAIT_ASSIGN(1, 待分配), WAIT_REPAIR(2, 待维修), REPAIRING(3, 维修中), WAIT_ACCEPT(4, 待验收), COMPLETED(5, 已完成), CANCELED(6, 已取消); private final int code; private final String desc; RepairStatus(int code, String desc) { this.code code; this.desc desc; } public int getCode() { return code; } public String getDesc() { return desc; } public static RepairStatus of(int code) { for (RepairStatus status : values()) { if (status.code code) return status; } throw new IllegalArgumentException(无效的报修状态: code); } }然后 Service 层做状态流转判断时用枚举做防呆public RepairOrder assignRepairman(Long orderId, Long repairmanId) { RepairOrder order repairOrderMapper.selectById(orderId); // 只有待分配状态才能进行分配 if (order.getStatus() ! RepairStatus.WAIT_ASSIGN.getCode()) { throw new BusinessException(当前状态不允许分配维修工); } order.setAssigneeId(repairmanId); order.setStatus(RepairStatus.WAIT_REPAIR.getCode()); repairOrderMapper.updateById(order); // 记录一条操作轨迹 repairRecordMapper.insert(...); return order; }这样做的好处是以后加新状态比如待回访只需要在枚举里加一项然后把流转的合法性判断函数补上即可不会把整个项目翻个底朝天。3.3 索引怎么加先想查询场景再建索引索引不是越多越好一定要结合查询场景。我这个项目日常查询频率最高的是两类一是住户查我的报修单WHERE user_id ?二是管理员查某个状态的所有工单WHERE status ?三是维修工查分配给我的工单WHERE assignee_id ? AND status IN (...)。所以我在repair_order表上单独建了idx_user_id、idx_status、idx_assignee三个索引而不是建一个联合索引(status, assignee_id)去覆盖所有场景。原因很简单三个查询场景有不同的主导查询条件维护三个单列索引的成本不高而且每个查询都能走索引。如果你能确认某个场景一定是状态维修工联合筛选的比较高频也可以再加一个联合索引但一般这种管理系统的数据量不至于需要极度优化单列索引足够了。另外提醒一下description和repair_result这种 text 字段不要加普通索引如果以后要做全文搜索用 MySQL 的全文索引或另外接搜索引擎不要在普通查询里对 text 做 LIKE %xxx%。4. 后端实现状态机流转与权限控制的落地细节4.1 报修单状态机五个状态、六条合法路径状态机是我后端代码里最核心的部分。我在上面枚举里定义了 6 个状态但实际业务允许的合法流转路径只有这几条待分配 - 待维修管理员分配维修工待维修 - 维修中维修工开始处理维修中 - 待验收维修工提交结果和费用待验收 - 已完成住户确认完成待分配 - 已取消住户取消或管理员取消待维修 / 维修中 - 已取消管理员强制取消为了让状态流转逻辑能够集中检查而不是散落在各个 Service 方法里我写了一个RepairStatusTransition校验工具类public class RepairStatusTransition { private static final MapInteger, SetInteger ALLOWED_TRANSITIONS new HashMap(); static { ALLOWED_TRANSITIONS.put(RepairStatus.WAIT_ASSIGN.getCode(), new HashSet(Arrays.asList( RepairStatus.WAIT_REPAIR.getCode(), RepairStatus.CANCELED.getCode() ))); ALLOWED_TRANSITIONS.put(RepairStatus.WAIT_REPAIR.getCode(), new HashSet(Arrays.asList( RepairStatus.REPAIRING.getCode(), RepairStatus.CANCELED.getCode() ))); ALLOWED_TRANSITIONS.put(RepairStatus.REPAIRING.getCode(), new HashSet(Collections.singletonList( RepairStatus.WAIT_ACCEPT.getCode() ))); ALLOWED_TRANSITIONS.put(RepairStatus.WAIT_ACCEPT.getCode(), new HashSet(Collections.singletonList( RepairStatus.COMPLETED.getCode() ))); ALLOWED_TRANSITIONS.put(RepairStatus.COMPLETED.getCode(), new HashSet()); ALLOWED_TRANSITIONS.put(RepairStatus.CANCELED.getCode(), new HashSet()); } public static void check(Integer from, Integer to) { if (!ALLOWED_TRANSITIONS.getOrDefault(from, Collections.emptySet()).contains(to)) { throw new BusinessException( String.format(非法状态流转: %s - %s, RepairStatus.of(from).getDesc(), RepairStatus.of(to).getDesc())); } } }这个类的好处是任何 Service 方法在修改状态时先调用RepairStatusTransition.check(旧状态, 新状态)非法路径直接在入口处被拦截住。我最开始实现的时候状态判断分散在几个 Service 方法里后来加需求时发现有个接口漏了判断能直接从待分配跳到已完成导致统计报表数据全乱了。集中校验之后这类问题基本绝迹。4.2 基于拦截器和注解的权限控制Spring Security 是全家桶里很常见的方案但如果你只是想在项目里快速实现三端权限隔离可以用更轻量的做法拦截器 自定义注解。我实际项目里就是这种组合简单直接也足够安全。自定义注解RequireRoleTarget(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequireRole { RoleEnum[] value(); }拦截器里校验逻辑的核心Component public class RoleInterceptor implements HandlerInterceptor { Autowired private TokenService tokenService; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) return true; HandlerMethod handlerMethod (HandlerMethod) handler; RequireRole requireRole handlerMethod.getMethodAnnotation(RequireRole.class); if (requireRole null) return true; String userId tokenService.resolveUserId(request); if (userId null) { throw new BusinessException(未登录或登录过期); } int role userService.getRoleById(Long.parseLong(userId)); for (RoleEnum allowedRole : requireRole.value()) { if (allowedRole.getCode() role) return true; } throw new AccessDeniedException(当前角色无权访问该接口); } }然后 Controller 方法上标一下就行高层语义一目了然ApiOperation(分配维修工) PostMapping(/assign) RequireRole(RoleEnum.ADMIN) public Result assign(RequestBody AssignRequest request) { return Result.ok(orderService.assignRepairman(request.getOrderId(), request.getRepairmanId())); }有几个容易忽略的细节需要注意拦截器只校验路由和方法注解但数据级权限必须在 Service 层做判断。比如维修工只能查看分配给自己的工单不能因为拦截器放行了就一查所有表。我处理方式是 Service 层统一强制带上当前登录者的角色和 ID 作为查询条件不允许前端传 ID 来决定查询范围。4.3 MyBatis 动态 SQL 处理高筛选条件的列表页报修单列表页是管理系统里最典型的高筛选场景状态、楼栋、维修类型、时间范围、单号关键字组合起来动辄四五个条件。MyBatis 的where和if在这里发挥最大价值select idlistOrders resultTypecom.example.dto.RepairOrderVO SELECT o.*, u.real_name AS user_real_name, u.room_number, u.phone FROM repair_order o LEFT JOIN sys_user u ON o.user_id u.id where if teststatus ! null AND o.status #{status} /if if testbuildingId ! null AND u.building_id #{buildingId} /if if testrepairType ! null and repairType ! AND o.repair_type #{repairType} /if if teststartTime ! null AND o.create_time gt; #{startTime} /if if testendTime ! null AND o.create_time lt; #{endTime} /if if testorderNo ! null and orderNo ! AND o.order_no #{orderNo} /if if testownerId ! null AND o.user_id #{ownerId} /if if testassigneeId ! null AND o.assignee_id #{assigneeId} /if /where ORDER BY o.create_time DESC /select这段 XML 我用了大量if条件查询条件可以任意组合。比较重要的一点是默认排序必须稳定——我统一用create_time DESC否则管理端报表在分页场景下会出现同一页数据重复出现的尴尬现象。另外如果你要在这个列表里做分页建议用物理分页而不是逻辑分页MySQL 里就是LIMIT offset, size千万别 select 出全量数据再在 Java 里截断。4.4 报修进度通知消息表 定时任务的模拟实现报修系统的体验核心是让住户知道进度。我没有引入消息中间件而是用一张notify_message表配合定时任务扫描未读消息在实际项目里效果已经很好。消息表结构很简单id, user_id, title, content, is_read, create_time。在报修单状态发生变化时Service 层同步插入一条消息住户端登录后拉取未读消息列表已读状态提交时更新。如果以后要接入微信公众号通知或短信提醒只需要在消息插入处追加一个发送逻辑不需要改业务主流程。定时任务这块我用的是 Spring 自带的Scheduled写一个方法定期把已分配但没有维修工接单超过 24 小时的工单标记为提醒状态并给管理员也插入一条提示消息。这个功能对公寓运营方是刚需可以防止工单被人遗忘。5. 前端 Vue 实现工单流程页与管理看板的踩坑记录5.1 Vue 路由守卫和侧边栏权限控制前端这边的权限控制和后端思路一脉相承路由需要区分角色。我在router配置里给每个路由添加了meta: { roles: [tenant,repairman,admin] }字段然后通过全局前置守卫判断router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token) { if (to.path /login) return next() return next(/login) } const userRole store.state.user.role if (to.meta.roles !to.meta.roles.includes(userRole)) { return next(/403) } next() })这里有个容易犯的错只做前端路由守卫并不安全因为前端控制只能隐藏入口不能阻止网络请求。所以前端守卫是体验层面的真正防越权还是靠后端接口权限。两边都要有缺一不可。5.2 报修表单的校验细节住户提交报修的页面是整个前端交互里最需要打磨的表单。我手里这套实现里关键校验点有报修类型必选、问题描述长度不少于 10 个字鼓励用户写清楚、图片上传限制在 3 张以内且单张不超过 5MB、可选的预约时间不能早于当前时间。这里分享一下 Element Plus 里比较方便的校验写法const repairFormRef ref(null) const repairForm reactive({ repairType: , description: , imageUrls: [], appointTime: null }) const rules { repairType: [{ required: true, message: 请选择报修类型, trigger: change }], description: [ { required: true, message: 请描述问题情况, trigger: blur }, { min: 10, message: 描述至少10个字, trigger: blur } ] } const submitRepair async () { await repairFormRef.value.validate() // 不通过会直接抛业务提示 const res await repairApi.submit(repairForm) ElMessage.success(报修提交成功) router.push(/tenant/order-list) }实际使用中你会发现校验规则里trigger很关键下拉选择用change输入框用blur如果反过来用户还没点开下拉就会被错误提示轰炸体验很差。这个细节是很多入门前端容易忽略的。5.3 状态流转时的交互反馈和禁用判断列表页展示状态时按钮的显隐和禁用逻辑最能体现一个前端有没有认真处理业务。我的做法是状态字段直接映射按钮可用性比如待验收状态下只有住户角色能看到确认完成按钮待分配状态下只有管理员能看到分配维修工按钮。这不是后端返回什么就渲染什么而是前端要根据当前角色的权限和工单状态做二次过滤。另外接口调用失败时的提示文案也要写得具体。我见过不少项目只返回操作失败用户不知道是没权限还是状态不允许。我在后端异常里做了分类业务异常返回具体原因该工单已由其他维修工接单请刷新查看系统异常才返回通用提示。前端拿到错误信息后原样展示沟通效率会高很多。5.4 管理端看板统计图表不算加分项是基本要求管理端的核心价值除了操作工单还有掌握全局情况。我在首页放了一个看板今日新增报修数、待处理工单数、平均响应时长、本月完成率下方是近 7 天工单趋势折线图和维修工处理量排行榜。图表我用的是 ECharts组件封装成通用ChartPanel.vue数据来自/admin/dashboard接口。后端统计时注意几个常用 SQL-- 近7天每日新增工单数 SELECT DATE(create_time) AS day, COUNT(*) AS cnt FROM repair_order WHERE create_time DATE_SUB(CURDATE(), INTERVAL 6 DAY) GROUP BY DATE(create_time) ORDER BY day; -- 维修工处理量排行 SELECT assignee_id, COUNT(*) AS total_cnt FROM repair_order WHERE status IN (4, 5) AND assignee_id IS NOT NULL GROUP BY assignee_id ORDER BY total_cnt DESC LIMIT 10;做统计类接口时有一个注意点时间范围尽量选近 7 天、近 30 天这种固定窗口而不要一次性查全量。报表页数据量一旦大了前端渲染也会卡后端接口也会响应慢。我实际项目中就出现过管理端查全量工单做统计数据到 3 万条时接口耗时超 5 秒的情况。6. 部署上线与排错笔记真机环境里的常见坑6.1 前后端打包和部署方式后端我用的是 Maven 打包执行mvn clean package -DskipTests生成可执行 jar 包部署到服务器上用nohup java -jar跑起来即可。server 端配置里要注意application.yml中的数据库连接和环境变量生产环境不要把数据库密码写在配置文件里而是用环境变量占位spring: datasource: url: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: ${DB_USER} password: ${DB_PASSWORD}前端部署就简单了npm run build生成dist目录交给 Nginx 托管。关键是要把 Nginx 的接口反向代理配置好不然前端访问/api时会出现跨域问题。我常用的配置如下server { listen 80; server_name your-domain.com; root /www/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /uploads/ { alias /www/uploads/; } location / { try_files $uri $uri/ /index.html; } }这里有一个容易被忽视的问题前端路由是 history 模式时Nginx 必须配置try_files回退到index.html。否则用户手动刷新一个子路由页面比如http://domain/tenant/order-list时Nginx 找不到对应的文件会返回 404。这个坑我不止一次见过有人踩。6.2 部署后的常见报错和排查思路我把自己在这套系统部署过程中遇到的高频报错整理了一张表对照排查会快很多报错现象可能原因排查思路前端请求接口 502后端服务没启动或端口不对ps -ef接口报 400参数格式不对JSON 大小写不一致看后端日志里的参数名确认 DTO 字段和前端传参匹配中文乱码数据库连接 URL 没配characterEncodingutf8检查 MySQL URL 参数同时确认库表字符集为 utf8mb4图片上传失败uploads 目录不存在或没有写权限mkdir -p /www/uploads chown -R nginx:nginx /www/uploads刷新页面 404Nginx 缺少try_files配置参照上面的 Nginx 配置补上MySQL 连接超时数据库连接池空闲连接断开数据源配置加druid.testWhileIdletrue等连接保活参数6.3 几个值得注意的安全和性能小优化部署上线后我还有几个习惯性的优化操作在这里一并分享第一密码加密。用户密码绝不能明文存储我用的 BCrypt 加密Spring Security 自带的BCryptPasswordEncoder就行。验证时前端传明文密码后端加密比对。这个部分一定不能省。第二接口层加简单防刷。住户提交报修这种写操作可以在后端按用户 ID 做频率限制比如同一个用户 30 秒内只能提交一次。用 Guava 的RateLimiter或者简单的缓存计数器都能实现。不加限制的话一旦有用户写了个脚本反复提交系统就会出现大量垃圾工单。第三定期备份 MySQL 数据。我写过一行 crontab0 2 * * * mysqldump -u backup_user -ppass repair_db /backup/repair_db_$(date \%Y\%m\%d).sql别看这行简单发生一次误删数据后你就知道备份有多重要。7. 推荐的学习路线和使用源码的正确姿势7.1 拿到源码后第一周该做什么很多同学拿到一套源码第一反应是先把项目跑起来然后到处点按钮看效果。这样收获很小。我建议按下面的顺序来过这套公寓报修管理系统第一遍跑通之后画图。把项目里三张核心表的关系画出来把状态的流转路径画出来把前端路由和后端接口的对应关系整理成一份接口文档。这一步能帮你建立整体地图。第二遍挑一个完整业务链路读代码。比如住户提交报修 - 管理员分配 - 维修工处理 - 住户确认这条线从前端页面组件开始读到调用 API 的方法再读到后端的 Controller、Service、Mapper最后看到 SQL 和数据库字段。一条链路走完你基本就明白这个项目是怎么串起来的了。第三遍自己动手加一个小功能。比如给报修单加一个紧急程度字段数据库加列实体类加属性前端表单加一个单选按钮列表页加筛选条件。改完这一个功能你对整个开发流程的理解深度比读十遍代码都强。7.2 从毕设答辩角度准备几个常被问的问题如果你拿这套系统做毕设或课设展示有几个问题很常被问提前准备好回答是真的有用为什么报修单状态不用数据库表记录而用枚举答状态是有限的、确定的用枚举便于控制非法流转集中管理也更清晰。几张表之间是什么关系为什么这样设计答用户表和报修单是一对多维修工和报修单是一对多维修记录只做操作日志记录方便后续轨迹回放。并发场景下怎么保证数据正确答比如两个管理员同时分配同一个工单我在 Service 层做了状态前置校验并利用乐观锁/行锁控制更新确保只有状态为待分配的单子才能被分配。如果有大量工单数据怎么优化列表查询答索引已经建好然后分页查询如果数据量继续增长可以考虑按创建时间做冷热数据分离或者对列表页增加缓存。7.3 迭代方向从能跑到好用如果你打算把这套系统作为长期维护的项目我有几个迭代建议一是把图片上传从本地磁盘改成对象存储省去服务器磁盘扩容和 Nginx 配置的麻烦二是接入企业微信或短信通知让住户第一时间知道工单被接单了三是把所有统计查询单独抽取报表模块避免和业务查询互相影响性能四是引入操作日志审计所有状态修改都能追踪到人。这些迭代方向不难但每一个都能明显提升公寓运营方的使用体验。做项目最忌讳的是一直停留在能跑层面往好用方向打磨才是你拉开差距的地方。最后再分享一个小技巧我在启动后端项目时习惯先把application.yml里的日志级别改成debug跑通核心链路后再调回info。Spring Boot 的 debug 日志会打印 SQL 参数、拦截器匹配等关键信息排查问题时比断点调试直观得多。等你把代码玩熟了再慢慢体会项目里每个设计决策背后的原因这比单纯收藏源码有用得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑