☰
基于SpringBoot+Vue的高校健康申报管理系统设计与实现
2026/10/14 6:41:54 网站建设 项目流程

1. 项目定位:这套管理系统到底解决了什么问题

1.1 需求背景与现实痛点

高校在日常管理里有一个非常典型的高频场景:在校师生每天需要完成健康状态上报,包括体温、当前所在地、身体有无异常、是否有接触史等等。以前很多学校靠微信群接龙或者手工填Excel表格来做这件事,一个辅导员带几百个学生,每天都要等学生逐个回复、手动汇总、再往上报,流程冗长、数据乱、漏报多。等到需要统计某个时间段、某个学院的整体情况时,还得从聊天记录里一点点翻,基本是噩梦。

我做这个项目的起因很简单:把这样一套“每日一报”业务从线下搬到线上。学生登录后只需要填一次表单,系统自动带上用户身份和日期;辅导员端能看到所带班级的填写情况;学院管理员能看全院数据;学校层面则能看到汇总看板、异常预警和趋势图。核心价值就是三个字:可追溯。每天的填报记录、异常记录、修改记录都落在数据库里,谁在什么时间提交了什么内容,一查就知道,不会出现“我记得填了但你没收到”的模糊地带。

这套系统并不算复杂,但它覆盖了一个真实业务系统该有的完整链路:用户认证、角色权限、核心业务表设计、复杂查询、数据统计、前端页面交互、打包部署。如果你正在学SpringBoot和Vue,想要找一个能把前后端所有环节串起来的练手项目,这个题目会非常合适。

1.2 技术选型背后的权衡

技术栈用的是SpringBoot + Vue + MyBatis + MySQL。很多人问我,为什么不直接用Spring Cloud微服务?为什么不选MyBatis-Plus而用原生MyBatis?这里的关键判断标准是:业务规模决定架构复杂度。

高校健康管理系统的用户量是几千到几万人,属于典型的中小型管理系统,单体后端完全扛得住,完全没有必要引入注册中心、网关、配置中心那套微服务体系。微服务解决的是大规模团队协作和独立扩展的问题,但同时会带来部署复杂、排障链路长、新手难以理解的代价。对这套系统来说,单体架构更合适。

每个组件的选择逻辑如下:

组件选择理由
后端框架SpringBoot 2.7自动配置能力强,起步依赖完善,社区资料多
ORM框架MyBatis手写SQL控制力强,统计查询、多表关联好调优
前端框架Vue 2 + Element UI后台管理组件成熟,表单、表格、弹窗开箱即用
数据库MySQL 5.7+学校机房和老服务器都能跑,生态稳定
认证方式JWT前后端分离下天然无状态,适合接口鉴权
统计图表ECharts折线图、柱状图、饼图配置简单,可视化效果成熟

选MyBatis而没有选MyBatis-Plus,有一个私人原因:做这类管理系统时,统计类SQL往往要自定义复杂聚合逻辑,原生MyBatis可以让我完全掌控SQL的书写过程,减少隐式行为带来的意外。实际开发中如果赶进度,用MyBatis-Plus作为增强底座也完全可以,逻辑上不冲突。

这套选型也有一个很现实的好处:市面上大量后台管理岗位的招聘要求就是这个组合。做完这个项目,你写简历时提到的技术栈和面试官想看的技术栈,重合度会非常高。

2. 数据库设计:先把表结构立住再谈业务

2.1 用户、角色、班级三张基础表的搭建

做管理系统,我个人的习惯是先把用户中心设计好,再做业务表。用户表不能只存一个用户名和密码,还要考虑后面的业务查询需求,比如按学院筛选、按班级筛选、按姓名模糊搜索。

用户表我最终拆成了三张表加一张中间表。t_user存放用户信息,t_role存放角色,t_class存放班级,t_user_role存放用户与角色的关联关系。班级和学院信息在学生表里通过class_id关联,学院信息冗余到了用户表里一个college字段,查询统计时可以少做一次联表。

CREATE TABLE t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录账号', password VARCHAR(100) NOT NULL COMMENT '加密后的密码', real_name VARCHAR(50) COMMENT '真实姓名', gender TINYINT DEFAULT 0 COMMENT '性别 0未知 1男 2女', phone VARCHAR(20) COMMENT '手机号', class_id BIGINT COMMENT '所属班级ID', college VARCHAR(100) COMMENT '所属学院', status TINYINT DEFAULT 1 COMMENT '1正常 0禁用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
CREATE TABLE t_class ( id BIGINT PRIMARY KEY AUTO_INCREMENT, class_name VARCHAR(100) NOT NULL COMMENT '班级名称', college VARCHAR(100) COMMENT '所属学院', grade_year VARCHAR(10) COMMENT '年级,如2023', UNIQUE KEY uk_class_name (class_name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='班级表';

角色表非常简单,只存角色编码和名称。实际代码里我控制权限的方式是在后端接口上用自定义注解加拦截器,前端菜单再按角色动态渲染。这样的做法比引入完整Spring Security的注解体系轻量不少,教学和中小项目里可读性好。

CREATE TABLE t_role ( id BIGINT PRIMARY KEY AUTO_INCREMENT, role_code VARCHAR(30) NOT NULL UNIQUE COMMENT '角色编码', role_name VARCHAR(30) NOT NULL COMMENT '角色名称' ); CREATE TABLE t_user_role ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, role_id BIGINT NOT NULL, UNIQUE KEY uk_user_role (user_id, role_id) ) COMMENT='用户角色关联表';

这里有一个设计取舍:如果业务明确要求一个用户只能有一个角色,那直接给t_user表加role_id字段更简单。我选择多对多中间表是因为后面要加“学院管理员”“校级管理员”“辅导员”等多角色场景时,不用频繁改动表结构。

2.2 健康申报表的具体建模过程

业务核心表是t_health_report,记录每次申报的具体内容。字段设计有几个要点:用户ID不能省,报表日期的类型用DATE而不用VARCHAR,温度字段用DECIMAL(3,1)表示范围在0.0到99.9之间,状态字段用TINYINT而不是字符串枚举。

CREATE TABLE t_health_report ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT '申报人ID', report_date DATE NOT NULL COMMENT '申报日期', temperature DECIMAL(3,1) COMMENT '体温', health_status TINYINT DEFAULT 1 COMMENT '1正常 0异常', symptoms VARCHAR(255) COMMENT '异常症状描述', location VARCHAR(255) COMMENT '当前所在地', contact_history TINYINT DEFAULT 0 COMMENT '是否有接触史 0无1有', remark VARCHAR(500) COMMENT '备注', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_date (user_id, report_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康申报表';

这张表的唯一键uk_user_date就是防止同一天重复申报的关键。不要小看这个约束,我在早期版本里没有加,结果测试时同一用户一天提交了几十次,后台统计图直接乱掉。后来在业务代码里先查询再插入,还是有问题:两个请求同时进来时,都查不到记录,于是都能插入成功。只有数据库层面加了唯一键,才能从根本上解决并发重复问题。

申报表的查询场景主要是按日期范围、学院、状态组合筛选,所以除了唯一索引外,report_date字段也适合建索引。如果数据量涨到百万行,可以考虑按月份做分区表,或者定时把旧数据归档到历史表。

2.3 公告表与通用字段的约定

系统还需要发通知。公告表的逻辑相对简单,但有一个细节:内容字段用TEXT类型,标题字段用VARCHAR(200)。如果想着以后可能要发富文本,就预留一个content_html字段,避免后面再改表结构。

CREATE TABLE t_notice ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL COMMENT '公告标题', content TEXT COMMENT '公告内容', publisher_id BIGINT COMMENT '发布人ID', is_top TINYINT DEFAULT 0 COMMENT '是否置顶 1是 0否', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='公告表';

我养成了一个习惯:每张业务表都加create_time和update_time两个通用字段。create_time用DEFAULT CURRENT_TIMESTAMP,update_time用ON UPDATE CURRENT_TIMESTAMP,这样记录被改过多少次数都能看到。虽然看起来只是小小的约定,但在线上排查问题时特别有用,尤其是当学生反馈“我的数据被改了”时,至少能看到最后的修改时间。

3. 后端核心实现:认证、上报与统计

3.1 JWT登录认证与访问控制的实现方式

登录模块我用的是JWT(JSON Web Token)。流程是:用户提交账号密码,后端校验成功后生成一个包含用户ID和用户名的token,返回给前端;前端每次请求都在Header里带上Authorization字段;后端拦截器校验token合法性并解析出当前用户。

不选用Spring Security的完整方案,是因为这个系统的角色数量可控、接口数量有限,手动实现能够更清楚地理解认证链路的每个环节。定义一个JWT工具类:

public class JwtUtils { private static final String SECRET = "school-system-jwt-secret-key"; private static final long EXPIRE = 1000L * 60 * 60 * 12; public static String createToken(Long userId, String username) { return Jwts.builder() .setSubject(username) .claim("userId", userId) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }

下面是拦截器,配置到SpringMVC的拦截器注册表里,除了登录接口和静态资源外全部拦截:

@Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (token == null || token.isEmpty()) { response.setStatus(401); return false; } try { Claims claims = JwtUtils.parseToken(token); request.setAttribute("userId", claims.get("userId")); request.setAttribute("username", claims.getSubject()); return true; } catch (Exception e) { response.setStatus(401); return false; } } }

关于SECRET的管理,写死在代码里只适合学习和内部项目。真正的生产环境要把密钥放到配置中心或者环境变量里,并且定期轮换。密钥不能太短,也不要使用容易被猜测的单词。

权限控制的第二个层面是角色鉴权。我在需要限制角色的接口上使用自定义注解:

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequireRole { String[] value(); }

每个接口在进入Controller方法前,先取出当前用户的角色列表,再和注解要求的角色做交集。这个方案比在代码里写一堆if判断清晰得多,新接口只要加一行注解就能控制访问范围。

3.2 健康申报的提交与分页查询

申报接口是整个系统逻辑最密集的部分。学生提交一条申报记录时,后端要做三件事:第一,校验当前日期是否已经申报过;第二,判断传入的数据是否合法;第三,插入数据库。整个流程都要放在一个事务里,避免中间出现异常导致数据不一致。

@Service public class HealthReportService { @Autowired private HealthReportMapper reportMapper; @Transactional(rollbackFor = Exception.class) public void submit(HealthReport report, Long userId) throws BizException { report.setUserId(userId); LocalDate today = LocalDate.now(); report.setReportDate(today); Integer count = reportMapper.countByUserAndDate(userId, today); if (count != null && count > 0) { throw new BizException("今日已申报,无需重复提交"); } if (report.getTemperature() != null && (report.getTemperature().doubleValue() <= 34 || report.getTemperature().doubleValue() >= 43)) { throw new BizException("体温数据不在正常范围"); } reportMapper.insert(report); } }

注意这里我在事务方法里先查后插,又依靠数据库唯一键兜底。为什么不去掉代码层的查询判断?因为遇到大量并发时,先查可以提前拦截绝大多数重复请求,降低数据库唯一键冲突出现的频率;真正撞上唯一键的极端请求则由数据库兜底。

分页查询用MyBatis手动写SQL。为了演示多条件组合查询,我使用了动态SQL标签:

<select id="selectReportPage" resultType="com.example.entity.HealthReport"> SELECT r.id, r.user_id, r.report_date, r.temperature, r.health_status, r.symptoms, r.location, r.contact_history, r.remark, u.real_name, u.college, c.class_name FROM t_health_report r LEFT JOIN t_user u ON r.user_id = u.id LEFT JOIN t_class c ON u.class_id = c.id <where> <if test="reportDate != null"> AND r.report_date = #{reportDate} </if> <if test="college != null and college != ''"> AND u.college = #{college} </if> <if test="healthStatus != null"> AND r.health_status = #{healthStatus} </if> <if test="keyword != null and keyword != ''"> AND (u.real_name LIKE CONCAT('%', #{keyword}, '%') OR u.username LIKE CONCAT('%', #{keyword}, '%')) </if> </where> ORDER BY r.create_time DESC </select>

分页这里我直接用PageHelper插件,配置一行分页拦截器就够,项目里不用自己在Controller里写LIMIT和COUNT语句。PageHelper使用时要特别注意:分页参数必须在查询方法之前设置,而且不能出现在多表查询里多个SQL的前后顺序搞错,否则会出现分页不生效或者查出错误总数的诡异问题。

3.3 统计接口:图表背后是怎么聚合的

统计模块是这类系统最有价值的部分。辅导员和学院管理员每天就看两个数据:今天有多少人报了,有没有异常数据。为了支撑前端的趋势图,我写了几个聚合查询。

按日期统计每天的申报总量和异常量:

SELECT report_date, COUNT(*) AS total_cnt, SUM(CASE WHEN health_status = 0 THEN 1 ELSE 0 END) AS abnormal_cnt FROM t_health_report WHERE report_date BETWEEN #{startDate} AND #{endDate} GROUP BY report_date ORDER BY report_date

按学院统计申报人数:

SELECT u.college, COUNT(DISTINCT r.user_id) AS report_user_cnt FROM t_health_report r LEFT JOIN t_user u ON r.user_id = u.id WHERE r.report_date = #{reportDate} GROUP BY u.college

这里的COUNT(DISTINCT user_id)很重要。如果不加DISTINCT,同一个学生一天内多条记录会被重复统计。虽然唯一键已经控制了重复,但统计SQL还是要养成加DISTINCT的习惯,防止别人在修改业务逻辑后数据结构变化导致统计数据异常。

统计接口还需要考虑返回体的一致性问题。我统一使用Result对象包装,结构为code、message、data三个字段。前端所有请求都期望这个格式,如果某个接口漏了包装,前端就会在解析时报错。统计类接口的data我大都使用Map或者List,这样前端的ECharts能直接对接。

4. 前端工程:从登录页到数据可视化

4.1 前端项目结构与请求封装

前端使用Vue2 + Element UI,构建工具用的Vue CLI。项目目录我习惯按功能模块拆,而不是按页面类型拆:

src/ api/ login.js report.js notice.js dashboard.js views/ Login.vue Dashboard.vue ReportForm.vue ReportList.vue NoticeList.vue router/ index.js store/ index.js components/ PageHeader.vue UploadExcel.vue

api目录下每个文件对应一个后端模块,里面只放接口定义。views放页面,components放可复用组件。这个拆法最大的好处是:后端加了一个接口,前端只需要在对应的api文件里加一行函数,不会动到其他文件。

axios请求封装是前端工程质量的关键。我直接抽出基础实例,加上请求拦截和响应拦截:

import axios from 'axios' const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API || '/api', timeout: 15000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = token } return config }) service.interceptors.response.use( response => { const res = response.data if (res.code === 401) { localStorage.removeItem('token') location.href = '/login' return Promise.reject(new Error('登录状态过期,请重新登录')) } if (res.code !== 200) { return Promise.reject(new Error(res.message || '请求失败')) } return res.data }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') location.href = '/login' } return Promise.reject(error) } ) export default service

在响应拦截器里统一处理401跳转,可以避免在每个页面里写重复代码。有一点值得提醒:code约定必须前后端一致。我在后端Result类里把成功码固定为200,失败码为500,未登录为401,前端所有逻辑只认这三个码。如果某天重新定义状态码,一定要同步修改前端拦截器,否则会出现页面没有报错但数据拿不到的问题。

4.2 管理员端核心页面实现要点

登录页面没什么好说的,就是一个表单加一个登录按钮。重点数据校验:

<el-form ref="loginForm" :model="form" :rules="rules"> <el-form-item label="账号" prop="username"> <el-input v-model="form.username" placeholder="请输入账号" /> </el-form-item> <el-form-item label="密码" prop="password"> <el-input v-model="form.password" type="password" placeholder="请输入密码" /> </el-form-item> </el-form>
submitLogin() { this.$refs.loginForm.validate(valid => { if (!valid) return loginApi({ ...this.form }).then(res => { localStorage.setItem('token', res.token) localStorage.setItem('userInfo', JSON.stringify(res.user)) this.$router.push('/dashboard') }) }) }

管理端最重要的页面是申报明细列表。这个页面要支持多个维度的筛选:日期范围、学院、状态、姓名关键词、分页。表格列包括学生姓名、学院、班级、上报日期、体温、症状、当前所在地、状态。操作列放“详情”和“导出”。

Element UI的表格在使用中容易碰到的问题是:当列特别多时要用fixed固定左侧列,但fixed和宽度设置不当会导致表格在屏幕过窄时挤压错位。我的建议是给固定列设置明确宽度,比如姓名列120px,班级列140px,其余列按内容自适应。

报表表单页面需要给日期默认值设为今天,并且提交成功后立即跳转到填报记录页。还有一个细节:学生在手机端使用的场景比较多,所以填报页面我加了viewport适配,减少了移动端输入框的放大抖动。

4.3 ECharts统计图表的落地与样式调优

统计看板是给学校管理层汇报用的,视觉上要直观。ECharts的折线图用来展示最近7天或30天的申报趋势,柱状图展示各学院填报率,饼图或环形图展示异常类型占比。

折线图的核心配置:

import * as echarts from 'echarts' mounted() { this.chart = echarts.init(this.$refs.trendChart) reportTrendApi({ days: 7 }).then(data => { this.chart.setOption({ tooltip: { trigger: 'axis' }, legend: { data: ['总申报数', '异常数'] }, xAxis: { type: 'category', data: data.map(item => item.reportDate) }, yAxis: { type: 'value' }, series: [ { name: '总申报数', type: 'line', smooth: true, data: data.map(item => item.totalCnt) }, { name: '异常数', type: 'line', data: data.map(item => item.abnormalCnt) } ] }) }) }

ECharts有一个高频坑:容器初始化时高度为0或隐藏状态,图表出来会是空白或畸形。解决方法是给图表容器设置固定高度,比如height: 400px;如果图表在切换tab后才展示,要调用chart.resize()。

另一个好用的细节是:ECharts实例在组件销毁时必须调用dispose(),否则会存在内存泄漏。我在Vue的beforeDestroy生命周期里统一处理。

5. 联调、打包与部署避坑清单

5.1 本地联调必踩的三个高频问题

前后端分离开发,本地联调阶段的问题比写代码阶段多得多。我总结出自己遇到且经常在别人项目里看到的三类问题。

第一类:跨域问题。前端开发服务器跑在8080端口,后端跑在8081端口,前端发请求会被浏览器拦截。解决方式有几种:后端加CORS配置、前端用构建代理、或者生产环境通过nginx反向代理。开发环境我更推荐vue.config.js里的devServer代理配置:

module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8081', changeOrigin: true } } } }

这样前端代码里请求路径写成/api/report/submit,开发时请求会被代理到后端,绕过了浏览器的同源策略。

第二类:MyBatis映射文件路径配置错误。XML文件放在java目录里时,springboot的打包会默认扫描不到xml文件,导致运行时报Invalid bound statement错误。解决方法是把xml文件放resources/mapper目录下,同时在application.yml配置:

mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.entity

第三类:日期时间格式不一致。后端返回的LocalDateTime默认序列化成数组或者字符串,前端截取显示会出错。我统一在后端字段上加格式注解:

{ "createTime": "2023-09-18 10:30:00" }
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime createTime;

同时前端解析日期时要用同一格式,避免出现时间相差8小时这类问题。timezone一定要写GMT+8,不然服务器时区设置不一样时,用户看到的时间会偏移。

5.2 服务器部署与Nginx配置

本地调试通过后,部署上线是另一个容易磨人的阶段。后端项目用Maven打包成jar包:

mvn clean package -DskipTests java -jar target/hospital-system.jar --server.port=8081

前端构建生成静态文件:

npm run build

构建产物在dist目录下,把它拷贝到服务器某个路径,比如/srv/www/school,再用nginx托管。关键配置片段如下:

server { listen 80; server_name your-domain.com; root /srv/www/school; index index.html; location /api { proxy_pass http://localhost:8081; 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 / { try_files $uri $uri/ /index.html; } }

try_files是Vue Router的history模式部署的关键。如果页面路由用了history模式而没配置这行,刷新页面时会报404,因为nginx不知道交给谁处理。这个坑非常经典,很多新人在本地没发现,一上服务器就露馅。

后端jar包在服务器上建议用systemd管理,方便开机自启和自动重启。写一个service单元文件,指定JAVA_HOME、启动命令、日志路径。这样进程挂了能被自动拉起,不需要手动盯着。

5.3 安全与性能优化建议

作为面向学校管理的系统,安全和性能同样重要。简单说几个我在这个项目里做的优化。

密码加密用BCrypt,不用MD5。MD5加盐虽然比纯MD5好,但算力提升后容易暴力破解;BCrypt的迭代成本设计和后端是同类技术,实现也简单。前端拿到密码后不要做任何加密传输,直接原样提交,因为BCrypt是在后端执行的。

SQL注入防御靠MyBatis的#{}语法就能完成,这个项目中凡是用户输入的查询条件都用了#{},使用${}的地方只有动态排序字段,而且白名单检查了字段名。

性能上,申报详情列表在上线后遇到过查询变慢的情况。主要原因是时间范围加学院筛选组合起来后,走了全表扫描。优化方案是在t_health_report表的user_id和report_date上建联合索引,并把学院信息冗余到申报表,减少不必要的left join。当数据增长到十万级,建议把统计查询改成定时任务预聚合,每天凌晨生成统计结果表,前端直接查统计结果表,响应速度会快很多。

还有一个很关键的体验优化:学生提交流程要快,不要让用户等待太久的校验和保存。提交接口不返回复杂统计信息,只返回“提交成功”,页面立刻跳转。后台的模糊查询、导出操作,使用异步线程池处理,避免请求卡住。

6. 项目结构复盘与个人体会

6.1 从零开始复现时,我建议的开发顺序

如果你打算照着这个思路从零写一遍,开发顺序很重要。不要先写前端,也不要先建报表。我的推荐顺序是:先建表结构,再写用户登录,再写健康申报的提交和列表,最后写统计看板。每个模块完成后再进入下一模块,每个接口写完就用Postman验证,不要攒到最后一起调。

踩过不少次坑之后,我总结出一个小原则:核心业务逻辑一定要先画清楚状态流转图再动手,别急着敲键盘。这个系统里最简单的“申报”状态也只有未提交、已提交两种,但如果前期没想清楚重复提交、撤销提交、补报昨日数据这些边缘场景,后期改动成本会非常大。

6.2 这套系统后续可以扩展的方向

基础的申报管理做完后,这个项目还有很多合理的扩展空间。比如增加定时提醒功能,每天早晨通过消息队列或者短信平台给未申报学生发提醒,提高申报率;比如导入导出功能,支持Excel批量导入学生名单、导出班级申报汇总;比如权限细化,把学院管理员再细分成多个维度,赋予不同菜单和按钮权限。

再往上走,可以做消息中心,把公告、异常提醒、审批结果都推送到统一收件箱。这些扩展方向都是一步一步叠加的,不会推倒原有架构。

6.3 个人收获与踩坑反思

最后说点实在的。做完这个项目最大的收获不是CRUD本身,而是对“前后端联调一致性”的理解。接口返回结构、状态码、日期格式、异常提示,每一个环节如果前后端各写各的,联调一定会出问题。所以现在做任何项目,我都会在动手前先写一份接口文档,定好统一返回结构,再开始两端的开发。

另外一个反思是数据库约束的优先级高于代码逻辑。很多初学者习惯把防重复、防非法数据这些校验全部写在业务逻辑里,忽略了数据库本身能提供的天然限制。加几个唯一键和约束字段,节省的排查时间远比写代码那一会儿多得多。

这套系统的思路和技术栈并不惊艳,但正是这种不惊艳,才让它非常适合作为学习SpringBoot+Vue全栈开发的样板项目。把每个模块老老实实写完、部署上线、解决几个真实报错,你对这套技术栈的掌控程度会有一个明显的提升。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询