前阵子帮朋友检查一套“党员教育和管理系统”的完整源码,项目技术栈是Java SpringBoot + Vue3 + MyBatis + MySQL,前后端分离,典型的后台管理型系统。说实话,这类系统从业务上看并不复杂——党员信息管理、学习资料发布、在线学习记录、考核考试、组织关系维护,核心就是围绕“人”和“学习记录”做增删改查。但真正把它做得顺手、能上线、后续好维护,里面还是有不少细节值得掰开揉碎讲一讲。
如果你正准备做类似的内部管理系统,或者打算拿这套体系作为毕业设计、二次开发的基础,这篇文章会非常对你胃口。我会从需求拆解、技术选型、数据库设计、后端核心实现、前端工程化、部署踩坑等几个方面,完整复盘这套系统的落地过程,也会把我在实际走查和改造中遇到的问题整理成一份可直接参考的避坑清单。
1. 需求拆解:这套系统的真实业务边界
不少新手拿到“党员教育和管理系统”这个标题,第一反应就是:这不就是一个信息管理系统吗?用户管理加课程管理,搞定。但真去把需求梳理一遍,会发现它比普通的企业内部管理系统多出许多隐含要求,尤其是“组织架构”和“考核闭环”这两条线。
1.1 角色与权限:一套系统先理清谁在用
这类系统里最典型的角色可以分成四类:
- 系统管理员:负责用户维护、菜单权限、系统参数、日志查看,属于“管系统的人”。
- 组织管理员:一般为党委、总支或支部级别的管理员,负责维护本组织下的党员信息、发布学习任务、查看学习进度和考试成绩。
- 普通党员用户:大部分系统使用者的角色,登录后看学习资料、参加考试、查看个人学习档案。
- 监督/统计人员:可能是分管领导或上级组织工作人员,需要跨组织查看汇总数据、导出统计报表。
这四类角色对菜单和操作权限的要求完全不同。管理员能进管理后台,普通用户只能看自己的学习中心,监督人员只能看数据看板和报表,不能改业务数据。因此权限模型不能只用一个简单的“role_id”字段打天下,至少需要“用户-角色-菜单”三层结构,必要时还要加数据权限,比如某支部管理员只能看到本支部的党员数据,不能跨组织越权查看。
1.2 核心业务链路:从党员信息到学习考核
这套系统的业务主链路可以概括成一条线:组织维护 → 党员建档 → 发布学习资料 → 党员在线学习 → 组织考试/测验 → 成绩归档 → 统计报表。
很多人做这类系统时会把注意力全放在“学习资料管理”这个模块上,觉得把课程表、视频表建好就行。但真正串起来以后会发现,最难处理的其实是“学习记录”和“考试记录”的关联查询。你不仅要记录某个党员学了哪个课程,还要记录学习时长、学习进度、是否完成,甚至要能区分是“首次学习”还是“复习”。组织管理员经常需要知道“哪些人还没学”,这又要求我们能在列表页快速过滤出未完成记录。业务一旦延伸到这种程度,简单的单表CRUD就不够用了,必须在设计阶段就把关联查询的字段冗余和索引策略想清楚。
1.3 功能模块与优先级的取舍
我梳理了一下,这套系统通常需要包含以下模块,按优先级排列:
| 模块 | 核心功能 | 难度等级 |
|---|---|---|
| 系统管理 | 用户、角色、菜单、字典、操作日志 | 中 |
| 组织管理 | 党组织树、组织成员维护 | 中高 |
| 党员信息管理 | 党员建档、信息维护、关系转接 | 中 |
| 学习资料管理 | 图文、视频、附件上传与发布 | 中 |
| 在线学习中心 | 个人学习列表、学习进度记录 | 高 |
| 考试考核管理 | 题库、试卷、在线答题、自动判分 | 高 |
| 数据统计报表 | 学习完成率、考试合格率、活跃度 | 中高 |
| 公告通知 | 站内信、通知发布 | 低 |
如果项目时间紧,公告通知最可以先做成简单的表单加列表;但“学习记录”和“考试记录”绝对不能省,因为这是整个业务闭环的核心证据。一套教育管理系统如果没有可靠的学习过程数据,管理员就没法做考核,系统价值会大打折扣。
2. 技术选型:SpringBoot+Vue3+MyBatis+MySQL为什么是黄金组合
这个项目用的是SpringBoot做后端、Vue3做前端、MyBatis做持久层、MySQL做存储,四件套非常经典。我见过太多人一开始想用更重的方案,比如Spring Cloud微服务、多数据库、复杂缓存中间件,结果人力和时间根本撑不住。这类业务规模的管理系统,单体应用反而是最优解。
2.1 后端选型逻辑
SpringBoot的核心价值在于“约定大于配置”。以前用SSH(Struts+Spring+Hibernate)时代光是配置文件就要写一天,现在SpringBoot只需要一个启动类加几个注解就能把Web容器、数据源、事务管理全部集成好。对于管理类系统,Spring Boot自带的嵌入式Tomcat、Spring MVC、以及spring-boot-starter-validation等组件,足够覆盖90%以上的场景。
MyBatis在这套组合里承担的是SQL持久层。很多人纠结用MyBatis还是JPA,我的建议是:如果团队里有人对SQL比较熟,或者业务中有大量报表和动态查询,就直接选MyBatis。管理系统的查询条件经常动态变化,比如“按姓名模糊查询、按组织过滤、按时间范围筛选”,MyBatis的动态SQL比JPA的派生查询和方法规范更直观。另外MyBatis对复杂查询结果集的映射也更可控,后期调优SQL比较容易。
2.2 前端Vue3的价值
Vue3对比Vue2最大的提升在于组合式API和更好的响应式机制。管理系统页面结构相似度高,我们完全可以抽出大量自定义Composable来复用业务逻辑。比如useUserStore管理登录状态,useTable封装列表页的分页、搜索、选择、刷新逻辑,useDict管理字典下拉数据。
更重要的是Vite带来的开发体验提升。Vue3 + Vite的冷启动速度非常快,保存代码后的热更新基本是毫秒级反馈,这对频繁调整页面表单、表格列的后台管理系统开发来说,日常幸福感提升不是一点半点。
Element Plus是目前Vue3后台管理系统最常用的组件库,表格、表单、弹窗、树控件都齐全,能覆盖这类系统90%的页面需求。组件库本身不是越花哨越好,稳定、文档多、团队熟悉才是关键。
2.3 项目目录与依赖配置
我这里给出一个典型的后端项目目录结构,方便你对照自己的工程:
src/main/java/com/example/partyedu ├── config // 配置类:MyBatis分页、CORS、拦截器、异常处理 ├── controller // 接口层 ├── service // 业务逻辑层 │ └── impl ├── mapper // MyBatis Mapper接口 ├── entity // 数据库实体 ├── dto // 请求/响应对象 ├── vo // 视图对象 ├── common // 通用返回结果、常量、工具类 └── security // 登录认证与权限相关对应的pom.xml核心依赖如下:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.2</version> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.7</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>这里我特意没把Spring Security放进来,因为初期可以先用手写的拦截器加JWT,简单直接。Spring Security功能强大但学习曲线陡峭,对这类业务不复杂的系统而言,过度设计反而增加理解成本。
3. 数据库设计:给业务打地基
数据库设计是整个系统最不能赶工的部分。很多后续的重复开发、查询慢、数据不一致问题,都源于表结构没设计好。这套系统里我建议把表分成四组:系统权限组、组织用户组、学习业务组、考试业务组。
3.1 用户和组织机构模型
用户表不能只存一个用户名密码,因为教育管理系统需要承载真实身份信息。参考设计如下:
CREATE TABLE `sys_user` ( `id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '登录账号', `password` varchar(100) NOT NULL COMMENT 'BCrypt加密后的密码', `real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名', `phone` varchar(20) DEFAULT NULL, `org_id` bigint DEFAULT NULL COMMENT '所属组织ID', `status` tinyint DEFAULT '1' COMMENT '状态:1启用,0禁用', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`), KEY `idx_org_id` (`org_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';组织表建议使用经典的parent_id树形结构,不要用单独的闭包表,除非你对树查询性能有极高要求。管理系统里组织层级最多三四层,用MySQL 8.0的递归CTE就能很优雅地查出某个节点下的所有子组织。
CREATE TABLE `party_org` ( `id` bigint NOT NULL AUTO_INCREMENT, `org_name` varchar(100) NOT NULL, `parent_id` bigint DEFAULT '0', `org_level` tinyint DEFAULT '1' COMMENT '层级', `sort` int DEFAULT '0', PRIMARY KEY (`id`), KEY `idx_parent_id` (`parent_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='党组织机构表';3.2 学习与考试业务表设计
学习资料表相对简单,关键字段是title、type、content_url、cover_url、publish_status。麻烦的是学习记录表,它需要回答三个问题:谁学的、学的是哪个资料、学到什么程度。所以字段至少要包含user_id、course_id、study_duration、total_duration、progress、status、last_study_time。
考试模块是另一个重灾区。建议至少拆成四张表:试卷表、题目表、试卷试题关联表、考试记录表。如果你把题目直接以JSON字符串塞进试卷表里,后面做成绩查询和统计分析会非常痛苦。我的经验是:题目单独建表,试卷通过关联表引用题目,考试记录表再保存每道题的作答详情,做成考试答题子表。
CREATE TABLE `edu_exam_record` ( `id` bigint NOT NULL AUTO_INCREMENT, `exam_id` bigint NOT NULL, `user_id` bigint NOT NULL, `score` decimal(5,2) DEFAULT '0.00', `total_score` decimal(5,2) DEFAULT '0.00', `status` tinyint DEFAULT '0' COMMENT '0未交卷,1已交卷,2已批阅', `start_time` datetime DEFAULT NULL, `submit_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_user_exam` (`user_id`, `exam_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='考试记录表';3.3 索引与数据库优化建议
管理系统的数据量通常不会特别大,但会出现“单表数据不多,查起来却很慢”的情况。原因往往是查询条件没走索引,或者用了大量LIKE '%keyword%'这种前模糊查询。
几个实操建议:
- 所有业务表必须有主键,推荐用自增bigint,方便MyBatis批量操作和分页查询。
- user_id、exam_id、course_id这些外键逻辑字段必须建索引,它们会频繁出现在JOIN和WHERE条件里。
- 时间字段如果要按天统计,可以建普通索引;如果数据量超过几十万,再考虑按月分表或者加汇总表。
- 状态字段区分度很低,单独建索引意义不大,通常要和org_id、create_time等字段做组合索引。
比如统计“某组织下党员的学习完成率”时,常见的查询是“先JOIN用户表拿到org_id,再统计每个用户的学习记录”。这种情况下,学习记录表上的(user_id, status, course_id)组合索引就非常重要了。
4. 后端关键实现:认证、动态SQL、分页与缓存
4.1 登录认证与权限拦截
这套系统我采用的方案是JWT + 自定义拦截器。登录成功后,后端生成一个token,包含用户ID、用户名、过期时间等信息,前端保存到localStorage,并在每次请求时放入Authorization请求头。
拦截器里只做两件事:校验token是否有效、把用户信息放入ThreadLocal或Request上下文。权限校验则在进入Controller之前通过自定义注解或按角色ID判断。
@Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (request.getMethod().equals("OPTIONS")) { return true; } String token = request.getHeader("Authorization"); if (token == null || token.isEmpty()) { throw new BizException(401, "未登录或登录已过期"); } // 解析token,验证签名和过期时间 Claims claims = JwtUtil.parseToken(token); request.setAttribute("userId", claims.get("userId")); return true; } }注意:JWT的签名密钥绝对不能硬编码在业务代码里,也不要提交到Git仓库。即便是内网系统,仍然建议放在配置中心或环境变量里。密钥一旦泄露,所有token都能被伪造。
4.2 MyBatis动态SQL与Mapper层写法
管理系统里最常用的就是条件查询。拿党员列表查询举例,可能要同时根据姓名、组织、学历、状态等字段过滤,有些字段可空。用MyBatis的动态标签实现非常方便:
<select id="selectUserList" resultType="com.example.vo.UserVO"> SELECT u.id, u.username, u.real_name, u.phone, o.org_name FROM sys_user u LEFT JOIN party_org o ON u.org_id = o.id <where> <if test="realName != null and realName != ''"> AND u.real_name LIKE CONCAT('%', #{realName}, '%') </if> <if test="orgId != null"> AND u.org_id = #{orgId} </if> <if test="status != null"> AND u.status = #{status} </if> </where> ORDER BY u.create_time DESC </select>这里有一个容易踩的坑:当你需要按组织树筛选时,直接写u.org_id = #{orgId}只能查到当前组织的人,查不到下级组织。正确做法是先查出当前组织及所有子组织的ID列表,再通过IN去查用户表。我一般会把“取子组织ID集合”的逻辑写在Service层,避免在SQL里用递归,逻辑可读性更好。
4.3 PageHelper分页插件正确姿势
MyBatis的分页插件PageHelper确实好用,但网上很多文章只讲了基础用法,没把坑讲清楚。我最常踩的坑就是“分页失效”,原因多半是分页插件拦截到了不需要分页的查询,或者在同一个线程中重复使用PageHelper导致分页条件被下一个查询继承。
我的标准写法如下:
public PageResult<UserVO> queryUserPage(UserQueryDTO query) { PageHelper.startPage(query.getPageNum(), query.getPageSize()); List<UserVO> list = userMapper.selectUserList(query); PageInfo<UserVO> pageInfo = new PageInfo<>(list); PageResult<UserVO> result = new PageResult<>(); result.setTotal(pageInfo.getTotal()); result.setList(pageInfo.getList()); return result; }需要注意两点:第一,PageHelper.startPage后面必须紧跟着第一条要分页的SQL查询,中间不能夹杂其他查询语句;第二,查询结果的泛型建议直接用VO,不要用Entity再手动转,因为PageInfo里的List类型转换容易写出很绕的代码。
4.4 缓存到底该不该加
看到热搜里有“mybatis缓存”,我多说一句。管理系统中使用最频繁、数据最不易变化的是“字典数据”和“组织树”。这两个非常适合缓存,但不建议直接使用MyBatis自带的二级缓存。MyBatis二级缓存在同一个Mapper命名空间下有效,一旦你用了多表关联查询,比如LEFT JOIN party_org,缓存刷新策略会让你非常头疼,稍有不慎就会出现脏数据。
我的做法是:用Spring Cache抽象,配合Caffeine做本地缓存,只缓存字典项、系统参数、组织树这类变化频率极低的数据。对学习记录、考试记录这类写频繁的数据,完全不加缓存。如果真的要缓存用户基本信息,建议等系统流量上来以后再加Redis,前期本地缓存就足够了。
@Service public class DictService { @Cacheable(cacheNames = "dict", key = "#type") public List<DictItem> listByType(String type) { return dictMapper.selectByType(type); } }缓存过期时间不要设太长,字典类建议最多30分钟。如果管理员修改了字典,应当在修改方法上加@CacheEvict主动清掉相关缓存。
5. 前端Vue3实现:从工程化到业务页
5.1 Vite项目初始化与配置
前端项目我习惯用Vite创建,而不是Vue CLI,因为Vite的速度优势太明显了。创建命令:
npm create vite@latest frontend -- --template vue随后安装核心依赖:
npm install vue-router@4 pinia axios element-plus在vite.config.js里配置路径别名和代理:
import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import path from 'path' export default defineConfig({ plugins: [vue()], resolve: { alias: { '@': path.resolve(__dirname, 'src') } }, server: { port: 3000, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })这里用代理解决本地开发跨域问题,比在浏览器里安装CORS插件、或者在后端强制开启所有跨域都更稳妥。实际部署后由Nginx反向代理,也不需要后端开启CORS。
5.2 Axios封装与Token处理
管理系统的所有请求都需要携带token,并且要统一处理错误码。我一般会抽出utils/request.js:
import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' const request = axios.create({ baseURL: '/api', timeout: 15000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = token } return config }) request.interceptors.response.use( response => { const res = response.data if (res.code === 401) { localStorage.removeItem('token') router.push('/login') return Promise.reject(new Error('未登录')) } if (res.code !== 200) { ElMessage.error(res.message || '系统错误') return Promise.reject(new Error(res.message)) } return res.data }, error => { ElMessage.error(error.message || '网络异常') return Promise.reject(error) } ) export default request有一点要强调:token不要存到sessionStorage,因为如果用户在多个标签页打开系统,sessionStorage不共享,会导致某一个页面突然“掉线”。localStorage明显更适合后台管理系统。
5.3 典型CRUD页面写法与组件划分
一个标准的列表页,我通常分成三部分:搜索区、表格区、弹窗表单区。Vue3组合式API之后,可以把整套逻辑封装成一个useTable组合式函数,让页面代码非常薄。
以党员列表页为例:
<template> <div class="app-container"> <el-form inline :model="queryParams"> <el-form-item label="姓名"> <el-input v-model="queryParams.realName" placeholder="请输入姓名" clearable /> </el-form-item> <el-form-item> <el-button type="primary" @click="fetchData">搜索</el-button> </el-form-item> </el-form> <el-table v-loading="loading" :data="tableData"> <el-table-column prop="realName" label="姓名" /> <el-table-column prop="orgName" label="所属组织" /> <el-table-column prop="createTime" label="创建时间" /> </el-table> <el-pagination v-model:current-page="queryParams.pageNum" v-model:page-size="queryParams.pageSize" :total="total" @current-change="fetchData" /> </div> </template>脚本中只需要维护queryParams、tableData、total,方法交给useTable统一处理。这样后面把同样的模式复制到课程管理、考试管理页面时,开发速度会成倍提升。
6. 前后端联调、构建与部署
6.1 CORS与本地联调环境
前后端分离开发时,最烦的就是跨域问题。我的建议是:开发阶段,Vite的proxy已经能解决大部分问题,后端完全不需要写CORS配置。如果必须开启CORS,注意不要把allowedOrigins设成*,因为带上本地调试域名反而报错的情况很多。
如果出现请求能到达后端但拿不到响应,或者登录成功后下一次请求又失败,先检查是不是token传递问题,再检查是不是OPTIONS预检请求被拦截器拦截了。拦截器里一定要放行OPTIONS请求,否则前端会报“CORS header missing”一类的错。
6.2 前后端分离部署方案
生产环境我推荐最简单的方案:后端打成jar包,前端构建成静态文件,用Nginx统一提供入口。
server { listen 80; server_name your-domain.com; location / { root /opt/frontend/dist; index index.html; try_files $uri $uri/ /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; } }try_files这一行必须要写,否则Vue Router的history模式在刷新子路由页面时会404。有些朋友为了省事改成hash模式,虽然也行,但我不推荐,因为URL里带一个#既不好看又不利于分享。
6.3 application.yml多环境配置
源码里一定要有多环境配置。最简单的做法是分application-dev.yml和application-prod.yml,用启动参数指定profile。注意数据库密码、Redis密码、JWT密钥这类敏感信息,不要在dev配置里和prod配置里写同一个值,也不要都写在文件里。可以结合环境变量覆盖,比如:
spring: datasource: url: ${DB_URL:jdbc:mysql://localhost:3306/party_edu} username: ${DB_USERNAME:root} password: ${DB_PASSWORD:}这样本地默认有一套值,生产环境通过DB_URL等环境变量注入,避免把生产密码泄露在代码库中。
7. 实操中踩过的坑:问题清单与排查思路
7.1 MyBatis二级缓存引发的脏数据问题
有网友问到MyBatis缓存,我也实际碰到过一个很典型的问题:启用了MyBatis的二级缓存后,列表页第一次查询正常,但新增一条记录后,列表竟然还是旧数据,而且其他模块的数据也出现了串号现象。
排查后发现,问题出在两个Mapper接口存在关联表更新,但只清了一个Mapper的缓存。比如PartyOrgMapper缓存了组织树,但SysUserMapper.updateUser()改了用户所属组织后没有清PartyOrgMapper的缓存,导致树形下拉还是旧数据。
这种坑一旦出现,排查成本很高。所以我在开发阶段直接把MyBatis的cache-enabled设为false,避免开发时被缓存问题干扰。等到确实有性能瓶颈,再考虑用更可控的缓存方案。
mybatis: configuration: cache-enabled: false7.2 时区问题导致日期错乱
管理系统里经常会做“今天、本周、本月”之类的统计,我也踩过时区导致统计不准确的坑。数据库连接串里如果没有指定serverTimezone,MySQL驱动默认会用JVM时区,导致查询出来的日期和本地时间差了8小时。
正确做法是在JDBC连接串里明确指定时区:
url: jdbc:mysql://localhost:3306/party_edu?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true同时MySQL服务器自身的时区也要检查,可以用show variables like '%time_zone%'查看。如果显示的是SYSTEM,最好改成+08:00,否则到了夏令时或者服务器时区切换时,业务统计又会异常。
7.3 Vue3响应式丢失与列表刷新
这个坑主要来自对reactive的误用。比如:
const form = reactive({}) // 某处重新赋值 form = res.data // 错误!这样会丢失响应式正确写法是用Object.assign(form, res.data),或者定义表单时直接拆成多个ref。如果是在下拉选择后动态添加表单字段,也建议用form.value = { ...form.value, field: value },避免直接替换整个对象。
另一个管理系统中常见的现象是:路由跳转后再次回到列表页,数据还是上一次的状态。原因是页面没有被销毁,列表组件没有重新请求数据。最简单的解决办法是在onActivated里重新fetchData(),或者用provide/inject维护一个“列表刷新标识”,每次新增、修改、删除成功后递增标识值,相关列表监听标识变化后自动刷新。
7.4 Excel导出与文件上传
很多教育管理系统都有导出学习记录、党员花名册的需求。使用Apache POI时,恰好搜索里有“java poi word能生成图表吗”,这里一起回答了:POI的XWPFDocument可以生成Word表格,但图表支持很弱;如果是Word图表,更靠谱的是用EasyPOI或者模板方式,预置一个Word模板,用poi-tl这类第三方库填充数据。对管理系统的Excel导出,我用的是阿里EasyExcel,内存占用小,写起来也简单。
导出大文件的坑是“超时”和“内存溢出”。千万不要在Controller里一边查全量数据一边同步生成Excel然后直接输出到浏览器。正确做法是:查询时分页或游标,分批写入Excel,所有数据写完后一次性返回文件流。如果数据量很大,更推荐先导出到服务器临时目录,再异步通知用户下载。
8. 我的一些经验总结与建议
做这类管理系统,我最大的感受是:不要把精力浪费在追求“新潮技术”上,先把手里的CRUD做得足够顺手。用SpringBoot + Vue3 + MyBatis + MySQL这套组合,本身就是为了快速交付和维护稳定。很多同学上来就想搞微服务拆分、引入各种中间件,结果连最基础的事务回滚都没处理好。业务管理系统拼的不是架构复杂度,而是业务理解的细腻程度和数据模型的合理性。
如果你准备基于这套源码二次开发,我的建议是先花一天时间把数据库表结构和接口文档理清楚,尤其是学习和考试模块的关联关系。然后把权限体系摸透,搞清楚不同角色能访问哪些菜单和数据。最后再去动页面和代码,否则很容易改一个点崩另一个点。
另外,源码之外一定要有配套的初始化SQL脚本和部署文档。很多项目代码不错,但接手的人不知道怎么建库、怎么初始化菜单、怎么配置Nginx,结果整套系统就无法落地。我通常在项目中放一个docs目录,里面写清楚:数据库版本要求、初始化SQL执行顺序、默认账号密码、前端构建命令、后端启动参数。这些内容看着不起眼,但关键时刻能省下大量沟通成本。
最后分享一个小习惯:每次修改完MyBatis Mapper XML,我都会顺手在本地执行一次涉及到的SQL,把执行计划和索引使用情况过一遍。尤其是列表页的分页查询,确认没有出现全表扫描和文件排序。这比事后上线再排查性能问题要省力太多。掌握好这些基础功夫,你离一个靠谱的Java全栈开发就真的不远了。