宿舍报修这件事,在高校里几乎是每天都会发生的场景。灯管坏了、水龙头漏水、空调不制冷、门锁损坏,以前靠宿管阿姨手写登记,再电话联系维修师傅,信息一多就容易漏单、错单,学生等不到维修进度反馈,后勤部门也无法统计维修效率。我做了一套基于SpringBoot+Vue的宿舍维修管理系统,用Java+MySQL+MyBatis这套非常经典的组合,把报修、派单、维修、验收、评价的完整业务闭环搬到了线上。这篇文章就把这套系统的从零搭建过程、核心代码设计、数据库结构、踩坑经验全部写出来,适合正在做毕业设计、课设,或者想入门全栈开发的同学直接照着复现。
之所以选SpringBoot+Vue+MyBatis这套技术栈,不是因为它新潮,而是因为它足够务实。SpringBoot解决了传统SSH、SSM时代大量繁琐的XML配置问题,内嵌Tomcat让项目可以一键启动;MyBatis把SQL控制权完全交给开发者,多表关联查询、动态SQL都很好写,这一点在维修工单这种需要频繁按条件筛选、统计的业务里尤其重要;Vue做前端页面的组件化开发非常顺手,和Element Plus配合,不需要太多时间就能把后台管理界面的交互做得像模像样。下面我会按照功能设计、数据库建模、后端实现、前端实现、环境部署、问题排查这个顺序,把整个项目完整拆解一遍。
1. 功能设计与技术选型思路
做管理系统,最忌讳一上来就闷头写代码。我在动手之前先明确了这套系统要服务的三类人和他们的核心诉求:学生要能快速提交报修单并看到处理进度,维修师傅要能接单、反馈处理结果,管理员要能分配工单、管理用户和宿舍楼并统计维修数据。围绕这三个角色,系统的功能模块就非常清晰了。
1.1 角色权限模型怎么设计
宿舍维修系统天然是三个角色的业务场景,权限设计上我用的是非常经典的RBAC模型。用户表(sys_user)里用role字段区分三种身份:ROLE_ADMIN、ROLE_STUDENT、ROLE_WORKER。前端路由守卫里根据角色动态生成菜单和可访问页面,后端接口用SpringBoot的拦截器基于JWT令牌解析用户身份,在没有引入Spring Security这种重框架的前提下,用拦截器处理权限已经足够干净实用。
后台管理的核心页面包括:系统首页的大屏统计数据看板、报修工单列表、工单分配、维修进度管理、宿舍楼栋管理、报修类型维护、用户管理、系统日志。学生端有:我要报修、我的报修单、维修评价、个人信息维护、站内通知。维修师傅端有:我的工单列表、接单与状态更新、工单详情与费用录入。
这里有个设计细节值得单独说:状态机流转。我在设计工单状态时定义了一个非常清晰的流转路径:待分配 → 已分配 → 维修中 → 待验收 → 已完成 → 已评价。任何角色都只能按照这个路径推进状态,不能随意往回跳或者跨级跳。这个设计在数据库里就是一张int类型的status字段,但前端对状态的控制完全靠Vue的计算属性和按钮权限控制来实现,后端再加一道校验,防止脏数据。
1.2 为什么选用SpringBoot+MyBatis而不是JPA
很多同学在选型时会纠结SpringBoot到底配Spring Data JPA还是MyBatis。我的建议是:凡是涉及复杂SQL统计、多表联查、字段较多的业务模型,直接用MyBatis,不要犹豫。宿舍维修系统里最典型的统计报表是"按宿舍楼统计维修完成率"和"按报修类型统计维修耗时",这些SQL语句用MyBatis的XML映射文件写起来非常直观,你可以直接看到SQL优化空间。而JPA的底层是Hibernate,自动生成的SQL往往不是最优的,遇到慢查询时排查问题的难度会高很多。
SpringBoot框架我选的版本是2.7.x,这个版本非常成熟稳定,网上资料丰富,对JDK8的支持也最友好。有些同学图新鲜用了SpringBoot 3.x,结果JDK17的模块系统、Jakarta命名空间迁移、第三方starter兼容问题一个接一个,开发效率严重受影响。反正做毕设或课设不是搞技术尝鲜,稳定压倒一切。
前端技术栈方面,如果你有一定Vue基础,可以直接上Vue3+Vite+Element Plus的组合,这也是目前的主流搭配。Vite作为构建工具比Webpack快太多,HMR热更新几乎是秒级响应,开发体验很好。Element Plus的表格、表单、弹窗、日期选择器组件非常完善,按需引入之后项目体积也不会太大。不过有一点要注意,Vue3和Vue2差别极大,组件通信、插槽语法、响应式原理都不一样,如果你以前只学过Vue2,建议先花半天过一遍Vue3组合式API(Composition API)的语法再动手写代码。
1.3 项目整体目录结构
项目采用前后端完全分离的结构。后端是一个标准Maven多模块项目,前端是独立的Vite工程。我会先把后端目录结构设计好:
code复制dormitory-repair-backend/
├── pom.xml
├── src/main/java/com/campus/
│ ├── DormitoryRepairApplication.java
│ ├── config/ // 跨域配置、MyBatis分页插件、拦截器注册
│ ├── controller/ // 控制层,接收前端请求
│ ├── service/ // 业务接口与实现类
│ ├── mapper/ // MyBatis数据访问接口
│ ├── entity/ // 实体类
│ ├── common/ // 统一返回结果、枚举、异常处理、工具类
│ └── util/ // JWT工具类、密码加密工具类
└── src/main/resources/
├── application.yml
└── mapper/ // MyBatis的XML映射文件
前端目录结构:
code复制dormitory-repair-frontend/
├── src/
│ ├── api/ // axios请求封装和所有接口定义
│ ├── assets/
│ ├── components/ // 公共组件
│ ├── router/ // 路由配置与守卫
│ ├── store/ // Pinia状态管理
│ ├── views/ // 页面组件
│ │ ├── login/
│ │ ├── admin/
│ │ ├── student/
│ │ └── worker/
│ ├── App.vue
│ └── main.js
├── vite.config.js
└── package.json
目录结构清晰的最大好处是:前后端并行开发的时候不需要反复沟通文件位置,每个人都能根据路径快速找到需要修改的代码。这一点在多人协作和答辩演示的时候都特别重要。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计与核心表结构
宿舍维修系统的数据模型并不复杂,但表与表之间的关联关系需要精心设计。我花了大量时间在设计数据库表结构上,因为一旦表结构设计不合理,后面写Mapper层SQL的时候就是灾难。
2.1 核心数据表清单
系统一共设计了7张核心业务表,每张表在设计时都遵循了一个原则:不冗余存储可推导的数据,但必要的冗余字段(比如冗余报修人姓名、手机号)一定要留,这样可以减少高频率业务场景下的联表查询次数。
| 表名称 | 表用途 | 关键字段 |
|---|---|---|
| sys_user | 系统用户表 | id, username, password, real_name, phone, role, avatar, status |
| dorm_building | 宿舍楼栋表 | id, building_name, address, manager_id |
| dorm_room | 宿舍房间表 | id, building_id, room_no, floor, capacity |
| repair_order | 报修工单表 | id, order_no, user_id, room_id, type_id, title, description, images, status, create_time, assign_time, finish_time |
| repair_type | 报修类型表 | id, type_name, description, sort |
| repair_evaluation | 维修评价表 | id, order_id, user_id, rating, content, create_time |
| sys_log | 操作日志表 | id, user_id, action, detail, ip, create_time |
核心业务表是repair_order报修工单表。我在这个表上设计了一个order_no字段,格式是"BX" + 年月日时分秒 + 四位随机数,比如BX202403151630120001。工单编号做成业务主键展示给用户看,数据库自增id只做内部逻辑处理,这种双主键设计在业务系统里非常常见,用户看到单号就知道是什么时候提交的报修。
2.2 报修工单表SQL详解
工单表的建表DDL是整个系统最核心的,我贴出来详细解释几个关键设计点:
sql复制CREATE TABLE `repair_order` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`order_no` varchar(32) NOT NULL COMMENT '工单编号',
`user_id` bigint(20) NOT NULL COMMENT '报修人ID',
`room_id` bigint(20) NOT NULL COMMENT '宿舍房间ID',
`type_id` bigint(20) DEFAULT NULL COMMENT '报修类型ID',
`title` varchar(100) NOT NULL COMMENT '报修标题',
`description` text COMMENT '报修详细描述',
`images` varchar(1000) DEFAULT NULL COMMENT '图片地址,多个用逗号分隔',
`status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态:0待分配 1已分配 2维修中 3待验收 4已完成 5已评价',
`priority` tinyint(4) DEFAULT '1' COMMENT '优先级:1普通 2紧急 3非常紧急',
`assigned_user_id` bigint(20) DEFAULT NULL COMMENT '分配维修师傅ID',
`assign_time` datetime DEFAULT NULL COMMENT '派单时间',
`accept_time` datetime DEFAULT NULL COMMENT '接单时间',
`finish_time` datetime DEFAULT NULL COMMENT '完成时间',
`repair_result` varchar(500) DEFAULT NULL COMMENT '维修结果描述',
`cost` decimal(10,2) DEFAULT NULL COMMENT '维修费用',
`delete_flag` tinyint(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除标记',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_room_id` (`room_id`),
KEY `idx_status` (`status`),
KEY `idx_assigned_user_id` (`assigned_user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='报修工单表';
几个关键设计的思考如下:images字段用逗号分隔图片URL。很多人喜欢另建一张附件表来存多图,但对于报修单这种最多传三五张照片的场景,一个varchar字段足够,省掉了联合查询的麻烦。如果后续要做PDF导出、图片压缩,也完全可以在业务层控制。status字段是系统最核心的状态机字段,用tinyint类型,每个数字的含义在代码中定义成常量或枚举,避免魔法数字满天飞。delete_flag是逻辑删除标记。业务表的信息原则上不允许物理删除,防止误操作丢了重要数据,也方便审计追踪。索引设计上,针对高频查询条件user_id、status、assigned_user_id都建立了索引,分页列表查询在数据量大时也能保证性能。
2.3 用户表与宿舍楼表的关联设计
用户表相对简单,但有一个坑要提醒大家:password字段的存储。我用的不是MD5而是BCrypt加密,同一条密码每次加密的结果都不一样,这样即使数据库泄露了,攻击者也很难通过彩虹表反查密码。Spring Security的BCryptPasswordEncoder可以直接拿来做工具类使用,不需要引入整套Security框架。
宿舍楼表和房间表是标准的父子结构。房间表的room_no字段并不是简单的"101"这种编号,我存的是string类型,因为有些学校楼栋房间号可能带AB座后缀这种格式。房间表还冗余了一个building_id外键,查询学生报修单时,通过一次JOIN就能同时拿到房间号和楼栋名。为了让前端表格展示更直观,我在报修单表里还会冗余存储room_no和building_name这两个展示字段,代价是一点点存储空间,换来的是列表查询不用做复杂的多表连接。这种取舍在真实业务中是值得的。
3. 后端SpringBoot核心实现
后端是整个系统的逻辑核心,包含JWT认证体系、工单状态流转、MyBatis多条件动态查询、文件上传服务、数据统计接口等多个关键模块。下面挑几个含金量最高、最容易出错的部分详细展开。
3.1 统一返回结果与全局异常处理
前后端分离开发中最重要的一层约定就是统一返回结果格式。如果没有统一约定,前端每个人拿到的响应结构可能都不一样,联调时完全无法推进。我的统一返回结构是:
json复制{
"code": 200,
"message": "操作成功",
"data": { }
}
后端专门写了一个Result类来封装这个结构,所有Controller接口的返回类型都必须是Result的实例。查询成功用Result.success(data),操作失败用Result.error("参数不能为空")。
配合全局异常处理,后端代码会干净很多。比如文件上传超限、参数校验失败、业务异常(工单状态不允许跳转)、服务器内部错误,这些异常会被拦截,统一处理成符合格式的JSON响应。前端axios响应拦截器里只需要判断response.data.code是否等于200,不是200就统一弹出错误提示,完全不用每个接口单独处理错误逻辑。
3.2 JWT身份认证与登录拦截
JWT(JSON Web Token)是目前前后端分离项目最主流的认证方案。它的核心思路是:用户登录成功后,服务端签发一个包含用户身份信息的加密令牌返回给前端;前端每次请求时把令牌放在HTTP头部的Authorization字段中;后端拦截器解析令牌,验证合法性,携带的用户信息写入当前请求上下文。
登录逻辑我是这么写的:
java复制public Result login(String username, String password) {
SysUser user = userMapper.findByUsername(username);
if (user == null || !passwordEncoder.matches(password, user.getPassword())) {
return Result.error("用户名或密码错误");
}
if (user.getStatus() == 0) {
return Result.error("该账号已被禁用,请联系管理员");
}
String token = JwtUtil.generateToken(user.getId(), user.getUsername(), user.getRole());
return Result.success(new LoginVO(token, user.getRealName(), user.getRole()));
}
JWT工具类生成令牌时,我在payload中放入了用户id、用户名、角色这三个关键信息,有效期设为24小时。拦截器解析令牌时会调用JwtUtil.parseToken方法,如果令牌过期或签名错误就抛出异常。这里特别强调一点:绝对不要在上传图片、分页查询这类高频接口中做数据库查询来校验令牌,直接从JWT中取出用户id即可,否则HTTP请求的耗时会被无谓的数据库访问拖慢。
注册拦截器时要注意放行路径的处理。登录接口、验证码接口、文件资源访问路径必须放行,其余接口统一拦截。实现WebMvcConfigurer接口时,要用registry.addInterceptor(authenticationInterceptor()).addPathPatterns("/").excludePathPatterns("/api/login", "/api/register", "/files/")这种方式明确配置。
3.3 MyBatis动态SQL工单多条件查询
维修工单列表是系统最核心的查询场景。管理员要按状态筛选、按宿舍楼筛选、按报修类型筛选、按时间段筛选;维修师傅要只看分给自己的工单;学生要只看自己的工单。我基于MyBatis的XML映射文件用动态SQL解决了这个痛点。
核心mapper文件内容如下:
xml复制<select id="selectRepairOrderList" resultMap="RepairOrderResultMap">
SELECT
o.*,
u.real_name AS userRealName,
u.phone AS userPhone,
r.room_no AS roomNo,
b.building_name AS buildingName,
t.type_name AS typeName,
w.real_name AS assignedUserName
FROM repair_order o
LEFT JOIN sys_user u ON o.user_id = u.id
LEFT JOIN dorm_room r ON o.room_id = r.id
LEFT JOIN dorm_building b ON r.building_id = b.id
LEFT JOIN repair_type t ON o.type_id = t.id
LEFT JOIN sys_user w ON o.assigned_user_id = w.id
<where>
o.delete_flag = 0
<if test="status != null">
AND o.status = #{status}
</if>
<if test="buildingId != null and buildingId != 0">
AND b.id = #{buildingId}
</if>
<if test="typeId != null and typeId != 0">
AND o.type_id = #{typeId}
</if>
<if test="userId != null">
AND o.user_id = #{userId}
</if>
<if test="assignedUserId != null">
AND o.assigned_user_id = #{assignedUserId}
</if>
<if test="keyword != null and keyword != ''">
AND (o.title LIKE CONCAT('%', #{keyword}, '%')
OR o.order_no LIKE CONCAT('%', #{keyword}, '%')
OR u.real_name LIKE CONCAT('%', #{keyword}, '%'))
</if>
<if test="startTime != null">
AND DATE(o.create_time) >= #{startTime}
</if>
<if test="endTime != null">
AND DATE(o.create_time) <= #{endTime}
</if>
</where>
ORDER BY o.create_time DESC
</select>
这段SQL的动态判断处理了所有可能的查询条件组合。用LEFT JOIN而不是INNER JOIN,是因为有些工单可能还没有分配维修师傅,assigned_user_id为NULL,这是合法状态。动态SQL里的每一条if判断都有明确含义,参数是从前端传过来的对象属性。
3.4 工单状态流转与核心业务流程
工单状态流转是整个后端业务逻辑中最容易写乱的地方。我的处理方式是:把每个角色允许的操作封装成独立方法,在方法入口统一校验当前状态与目标状态是否合法。以管理员派单接口为例:
java复制public Result assignOrder(Long orderId, Long workerId) {
RepairOrder order = orderMapper.selectById(orderId);
if (order == null) {
return Result.error("工单不存在");
}
if (order.getStatus() != 0) {
return Result.error("只有待分配的工单才能进行派单操作");
}
SysUser worker = userMapper.selectById(workerId);
if (worker == null || !"ROLE_WORKER".equals(worker.getRole())) {
return Result.error("请选择有效的维修师傅");
}
order.setStatus(1);
order.setAssignedUserId(workerId);
order.setAssignTime(new Date());
repairOrderMapper.updateById(order);
return Result.success(null);
}
这段代码的逻辑非常直白:先校验工单存在,再校验当前状态,再校验维修师傅身份有效,最后更新数据。维修师傅接单方法类似,但校验条件是status要等于1且assignedUserId等于当前登录用户。提交完成方法校验status等于2,管理员验收方法校验status等于3,学生评价方法校验status等于4。每一环都被严格约束,业务流程就非常清晰了。
3.5 数据统计报表接口
系统的首页看板需要展示三种核心统计数据:总工单数、待处理工单、本月完成率,以及按类型展示的维修占比。这些统计全部用一条SQL聚合查询搞定。
sql复制SELECT
COUNT(*) AS totalCount,
SUM(CASE WHEN status = 0 OR status = 1 THEN 1 ELSE 0 END) AS pendingCount,
SUM(CASE WHEN status = 4 OR status = 5 THEN 1 ELSE 0 END) AS finishedCount,
ROUND(
SUM(CASE WHEN status = 4 OR status = 5 THEN 1 ELSE 0 END) * 100.0 / COUNT(*),
2
) AS finishRate
FROM repair_order
WHERE delete_flag = 0
SUM(CASE WHEN...)这种写法效率很高,一次扫描就能统计多个维度的数据,比写多条SQL再合并再Java里算要好得多。首页另外还需要"各类型报修工单占比"的统计,对应的SQL是GROUP BY type_id然后JOIN类型表查出类型名称,再按数量倒序排列。前端用ECharts渲染成饼图。
4. 前端Vue核心页面设计
后端接口设计完成之后,前端开发就是把这些数据用美观、可用的方式呈现给用户。Vue3+Element Plus+Vite的组合在这一步发挥出了很好的生产力。
4.1 前端路由与动态菜单权限
路由是前端框架的核心骨架。登录成功后,前端会拿到当前用户角色,然后根据角色生成可访问的菜单路由。我用的是动态添加路由的方式:基础路由只有登录页和404页,所有业务页面全部通过router.addRoute动态注入。
javascript复制// 角色对应的路由配置
const dynamicRoutes = {
ROLE_ADMIN: [
{ path: '/index', component: Layout, children: [
{ path: 'dashboard', component: () => import('@/views/admin/Dashboard.vue'), meta: { title: '数据看板' } },
{ path: 'order/list', component: () => import('@/views/admin/OrderList.vue'), meta: { title: '工单管理' } },
{ path: 'order/assign', component: () => import('@/views/admin/OrderAssign.vue'), meta: { title: '工单派单' } },
{ path: 'building', component: () => import('@/views/admin/BuildingManage.vue'), meta: { title: '宿舍楼管理' } },
{ path: 'user', component: () => import('@/views/admin/UserManage.vue'), meta: { title: '用户管理' } }
]}
],
ROLE_STUDENT: [
{ path: '/index', component: Layout, children: [
{ path: 'my/order', component: () => import('@/views/student/MyOrder.vue'), meta: { title: '我的报修' } },
{ path: 'my/order/add', component: () => import('@/views/student/AddOrder.vue'), meta: { title: '提交报修' } }
]}
],
ROLE_WORKER: [
{ path: '/index', component: Layout, children: [
{ path: 'my/task', component: () => import('@/views/worker/MyTask.vue'), meta: { title: '我的工单' } }
]}
]
}
配合路由守卫,每次跳转时检查本地是否存在token,没有就强制跳到登录页。刷新页面时从本地存储重新解析用户信息和路由配置。这套动态菜单方案在答辩时是非常好的加分项,评委看到不同角色登录后的界面和权限完全不同,会认可系统的完整度。
4.2 axios请求封装与拦截
前端与后端所有通信都通过axios完成。我对axios做了一层统一封装,重点处理了三个问题:请求头自动附加JWT令牌、响应数据自动解包、401状态统一跳转登录页。
javascript复制// 封装axios实例
const service = axios.create({
baseURL: '/api',
timeout: 10000
})
// 请求拦截器
service.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers['Authorization'] = 'Bearer ' + token
}
return config
})
// 响应拦截器
service.interceptors.response.use(
response => {
const res = response.data
if (res.code !== 200) {
ElMessage.error(res.message || '请求失败')
return Promise.reject(new Error(res.message))
}
return res.data
},
error => {
if (error.response && error.response.status === 401) {
localStorage.removeItem('token')
router.push('/login')
}
ElMessage.error(error.message || '网络异常')
return Promise.reject(error)
}
)
这个封装最直接的收益是:每个具体页面调用接口时只需要关心业务数据,完全不需要处理重复的模板代码。比如学生提交报修时,页面调用submitOrder(orderForm)方法,拿到返回的工单编号直接跳转列表页。维护性和可读性都有很大提升。
4.3 报修工单列表页面的核心交互
工单列表是整个系统最重要的前端页面,管理员和维修师傅每天面对的就是这张表。我使用ElTable组件展示数据,分页用ElPagination组件,布局上给筛选区、表格区、分页区做了清晰分区。表格里的状态字段不做数字展示,而是通过Vue的计算属性映射成带颜色的el-tag标签,比如待分配是灰色"待分配"按钮、维修中是蓝色"维修中"、已完成是绿色"已完成"。这个细节会让系统看起来专业很多。
操作列根据当前用户角色和工单状态动态渲染按钮。管理员看到待分配状态的工单会显示"派单"按钮;维修师傅看到已分配给自己且未接单的工单会显示"接单"按钮;状态为维修中时显示"提交完成"按钮;状态为待验收时管理员看到"验收"按钮;状态为已完成且当前用户是报修人时,学生看到"评价"按钮。每个按钮都对应一个独立的弹窗,比如派单弹窗里是维修师傅的下拉选择,维修结果弹窗里是文本域和费用填写。
在弹窗数据交互中要注意一个问题:提交成功后必须刷新当前页面数据,同时清空弹窗里残留的表单数据。如果不清理,下一次打开弹窗会显示上一次填的内容,这是一个很容易被忽略但影响体验的bug。
4.4 图片上传组件的实现
报修单需要支持学生上传故障现场照片。前端用的是Element Plus的ElUpload组件,配置action属性指向后端文件上传接口,上传完成后把返回的图片路径推入表单字段。这个组件默认上传行为是选择文件后立刻上传,收到响应后再把URL保存。后端配合设置文件存储目录、允许的拓展名校验和单文件大小限制。
我在开发初期遇到过一个问题:SpringBoot默认只允许单个文件最大1MB,多文件总大小最大10MB,而手机拍出来的照片动辄3-5MB,上传直接报错。需要在application.yml里设置spring.servlet.multipart.max-file-size为20MB、max-request-size为50MB,并且因为后端是放在云服务器上的,还要保证上传目录有足够的磁盘空间。另外,上传接口返回的URL建议用相对路径,比如/files/xxxx.jpg,前端用网站根路径拼接访问,这样以后迁移服务器换域名时不用改数据库里的图片地址。
5. 环境准备与项目启动全流程
很多人在写代码之前就被环境准备绊倒了。JDK版本不对、Maven依赖下载失败、Node版本和Vite不兼容、MySQL字符集乱码,这些千奇百怪的环境问题会让人崩溃。我把自己整理的环境要求放在这里,照着配基本不会出错。
5.1 后端环境安装配置
我用的版本是:JDK 1.8(严格说是8u202后的版本)、Maven 3.8.x、MySQL 8.0+、Idea 2023版本以上。数据库连接配置在application.yml中如下:
yaml复制server:
port: 8080
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/dormitory_repair?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
servlet:
multipart:
max-file-size: 20MB
max-request-size: 50MB
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.campus.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 自定义配置
jwt:
secret: your-secret-key-change-me
expire-hours: 24
file:
upload-dir: /data/dormitory-repair/files/
map-underscore-to-camel-case这个配置非常重要。数据库字段是create_time这种下划线风格,Java实体类字段是createTime这种驼峰风格,开启这个配置后MyBatis会自动完成映射转换,不用写一堆@TableField注解。否则你在XML的resultMap里要逐个字段映射,工作量翻倍。
5.2 前端环境与Vite配置
前端需要安装Node.js 16.0以上版本,建议用最新的LTS版本(20.x或22.x)。Vite创建项目、安装依赖后,最关键的配置是开发环境的跨域代理。
javascript复制// vite.config.js
export default defineConfig({
plugins: [vue()],
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
},
'/files': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
},
build: {
outDir: 'dist'
}
})
开发时前端跑在3000端口,后端跑在8080端口,属于跨域请求。配置代理后前端把请求转发到后端,完美规避了跨域问题,cookie和认证信息都能正常传递。生产环境则完全不需要代理,因为前端打包后的静态文件直接放在SpringBoot的static目录里,同源访问天然没有跨域问题。
5.3 项目启动顺序与测试
项目启动的标准化流程是:先启动MySQL服务并执行数据库初始化SQL脚本 → 启动后端SpringBoot项目,看到"Started DormitoryRepairApplication"日志就说明启动成功 → 在前端项目目录执行npm run dev启动开发服务器 → 浏览器访问http://localhost:3000进行测试。
后端启动如果报端口占用错误,可以在Idea的Run/Debug Configurations里给环境变量添加server.port=8081参数来快速规避,或者直接用命令netstat -ano | findstr 8080找出占用进程并结束它。这时候千万不要去改application.yml的端口来妥协,否则前端代理配置也要跟着改,很容易留下隐患。
6. 生产部署与常见问题排查
项目开发完成之后,部署上线是最后一个重要环节。我当初第一次把所有代码部署到服务器的时候踩了不少坑,我把这些经验整理出来,能让你少走很多弯路。
6.1 前后端打包与部署
后端打包有几种方式:Idea的Maven面板双击package命令可以生成可执行jar包;也可以用命令行mvn clean package -DskipTests在项目根目录执行。打包成功后target目录下会生成一个dormitory-repair-backend-1.0.0.jar文件。同时把前端项目的dist目录里的所有静态文件拷贝到后端项目的src/main/resources/static目录下重新打包,这样整个Web应用就只有一个jar包。
服务器上只需要安装JDK和MySQL,然后执行:
bash复制nohup java -jar dormitory-repair-backend-1.0.0.jar --spring.profiles.active=prod > app.log 2>&1 &
加--spring.profiles.active=prod参数可以切换到生产环境配置,实际是把数据库连接、文件上传目录这些配置放到application-prod.yml里,避免开发环境的配置泄露。用nohup和重定向符号把日志写进app.log文件,方便用tail -f app.log实时查看日志排查问题。
Linux服务器上文件上传目录要注意权限问题。如果上传目录是/data/dormitory-repair/files/,要确保运行jar包的账号对该目录有写权限,否则上传图片会报FileNotFoundException。我吃过这个亏,后来在启动脚本里加了一段自动创建目录并赋予权限的shell代码:
bash复制mkdir -p /data/dormitory-repair/files
chmod -R 755 /data/dormitory-repair/files
6.2 MyBatis查询常见问题排查
开发过程中最容易遇到的一类问题就是MyBatis查询结果异常。第一个高频坑是:数据库字段能查到值,但Java实体类属性却是null。原因绝大多数是map-underscore-to-camel-case没配置,或者XML里的resultType和resultMap混用了。resultType是MyBatis自动按驼峰规则映射,resultMap是你手动指定映射规则,两者冲突时会以resultMap的配置为准,字段没写的就映射不上。
第二个高频坑是大于号小于号在XML文件里的转义。SQL里写status > 0时,大于号必须写成>,小于号写成<,否则XML解析直接报错。很多初学者在这里卡半天,错误提示是The content of elements must consist of well-formed character data。应对方案有三个:转义符、CDATA区、或者用MyBatis的包裹,我最推荐CDATA,SQL一眼就能看清不用猜。
第三个坑是分页插件的使用方式。我的做法是引入PageHelper依赖后,在Mapper接口查询方法前加一行PageHelper.startPage(pageNum, pageSize),然后立即调用查询方法,返回的结果会被自动包装成PageInfo对象。这个插件有个使用注意事项:startPage方法必须在查询方法之前立即调用,中间不能夹任何其他SQL操作,否则分页会作用到错误的执行SQL上。
6.3 前端部署浏览器兼容问题
前端项目打包后的dist目录里的js、css文件名默认带哈希值(如index-abc123.js),这是Vite的静态资源缓存策略。部署后如果改了前端代码,浏览器访问时可能还会命中旧的缓存文件。解决方案有两个:一是在nginx配置里关闭静态资源缓存,或设置短缓存时间;二是每次都更新dist目录文件名(哈希值变了,浏览器自然重新加载)。如果你用的是SpringBoot内嵌Tomcat部署,修改了前端代码重新打包时,建议清空浏览器缓存或用无痕窗口验证。
还有一个在部署时常遇到的问题:刷新页面404。这个问题是vue-router用了history模式导致的。部署在Tomcat或Nginx上时,需要配置fallback规则让所有请求都指向index.html。如果在SpringBoot里用static目录放前端文件,还需自定义一个ErrorPage配置,或者直接改用hash模式创建路由。对我这种偶尔图省事的人来说,hash模式在小型管理系统里几乎无可挑剔,但如果你想体验更优雅的URL或者需要在多台设备和路由之间共享链接,history模式就必须配合正确的服务端配置。
6.4 高频异常速查表
我在开发过程中整理了这么一张经验表,遇到问题先对照这个表排查,能解决80%的困惑:
| 异常现象 | 可能原因 | 解决方案 |
|---|---|---|
| 启动报"Invalid bound statement (not found)" | Mapper接口与XML映射文件未绑定 | 检查application.yml中mapper-locations路径是否正确,XML文件的namespace是否等于Mapper接口全限定名 |
| SQL查询中文显示问号 | 数据库连接串缺失characterEncoding | 连接URL中加入characterEncoding=utf8 |
| 接口返回401但token有效 | 拦截器未放行OPTIONS预检请求 | 拦截器中放行OPTIONS方法,并配置跨域允许请求头 |
| 前端菜单切换报404 | 动态路由注册时机不对 | 在路由守卫中完成token校验后再注册动态路由,并调用next({...to, replace: true}) |
| 上传图片访问404 | 静态资源映射未配置 | SpringBoot中需要配置resources映射到file路径,或者把上传目录放在static下 |
| 前端调用后端接口偶尔超时 | MySQL连接池耗尽,大查询或慢查询阻塞 | 优化SQL索引,配置更高连接池上限,开启慢查询日志定位瓶颈 |
7. 经验心得与项目延展方向
这个系统整体开发下来,前后端加起来大概花了三周左右的业余时间。回过头来看,我认为最有价值的不是代码量,而是设计思路和落地能力。先说几个我过程中的感悟:第一,设计数据库表的时候多花一小时,后面写代码能省三天,字段冗余和索引设计一定要在写业务代码前想清楚;第二,不要盲目追求新技术,SpringBoot 2.7 + Vue3 + MyBatis这套组合已经完全够用,稳定性和资料丰富性才是学生项目最需要的东西;第三,拷贝代码不等于会写代码,尤其分页、全局异常处理、拦截器、动态权限路由这些核心部分一定要自己手写一遍,才能理解各个组件之间的联系。
在后期扩展方面,这套系统还有不少可以升级的空间:权限模块引入Spring Security或Sa-Token,做成细粒度的操作权限控制;报修流程接入WebSocket实时通知,工单状态变化时学生端能立刻收到提醒;报表模块接入ECharts制作更丰富的数据大屏,展示维修响应时长趋势图、楼栋维修费用排行;甚至可以对接企业微信或钉钉机器人,把工单派发消息推送到维修师傅手机上。这些方向每一个都是很好的毕设加分项,也是真实生产环境中后勤管理系统会考虑的进阶需求。
宿舍维修管理系统这类项目极具代表性,它麻雀虽小五脏俱全:用户角色权限、事务性操作、文件上传、状态机流转、数据统计分析、前后端分离部署,几乎覆盖了企业级Web开发的全部基础知识点。把这一套完整做完并吃透,SpringBoot和Vue的开发能力会有非常明显的提升,后续不管是找工作面试还是做更复杂的系统,都会扎实很多。
