前后端分离的大学生创新创业训练项目管理系统,技术栈是SpringBoot + Vue + MyBatis + MySQL。这种组合在校园项目、毕设、实验室管理系统里极其常见,但常见不代表简单:项目要管申报、评审、中期检查、结题验收,还有学生、老师、评审专家、管理员多种角色,权限逻辑和业务状态流很容易写成一团乱麻。这篇文章就把我做这套系统的完整思路、踩过的坑和部署过程全盘写出来,从数据库设计到前后端代码,再到服务器上线,一步一步说清楚,不管是拿来交作业还是应付真实业务场景都够用。
1. 大创项目管理系统到底要管什么:需求复盘与方案选型
1.1 核心需求拆解
我说的大学生创新创业训练项目管理系统,指的是高校里最常见的“大创”业务:学生团队发起项目申报,指导老师审核,学院组织专家评审,评审通过后立项,接着是中期检查和结题验收。整个过程还涉及经费分配、成果登记、结题材料上传。
听上去简单,但只要真坐在电脑前写这套系统,就会发现业务逻辑比想象中要拧巴。最大难点不是某一个页面多复杂,而是状态流转和角色权限交织在一起。举个例子:一个项目在“申报中”时,学生能改表单;一旦进入“评审中”,学生就应该只能看不能动;评审专家只能看到分配给自己评审的项目;管理员要能随时调整项目阶段。这些规则如果没有在设计阶段想清楚,后面写接口就是一坨改来改去的补丁。
所以我在做需求复盘时,先把用户分成了四类:学生、指导老师、评审专家、系统管理员。学生做申报和进度维护;老师审批自己指导的项目;专家负责评审打分和填写意见;管理员负责账号管理、项目分配、阶段切换和数据统计。整个系统围绕这四个角色去设计菜单、路由和接口权限,后面开发会省心很多。
1.2 为什么选择前后端分离 + SpringBoot + Vue
这个选择我不是拍脑袋定的。早期很多同类系统是JSP+Servlet那套,页面和后端代码混在一起,改个按钮都要重新编译部署,更别提多人协作时前后端互相阻塞。前后端分离的核心好处在于,前端和后端各干各的,接口定义清楚后可以并行开发,上线部署也能分开处理。
再聊技术选型。
- SpringBoot就是拿来快速搭后端的,内置Tomcat,打包成jar直接运行,不用装外置容器,这对写管理系统来说太舒服了。它的starter机制让依赖管理也变得非常简单,加个Web、加个MyBatis、加个MySQL驱动,几行配置就完事。
- Vue用来写前端页面,组件化的思维特别适合页面复用。像项目列表、用户表单、评审弹窗这种东西,写成组件后在不同页面里直接用,代码量能压缩很大一截。
- MyBatis半自动化ORM,SQL自己写,控制力很强。大创系统里有很多多表联查和动态条件筛选,比如按项目状态、按年份、按专业方向过滤,MyBatis的
<where>和<if>动态SQL写起来非常顺手。 - MySQL不用多说,免费、资料多,学校服务器随便装,适合这种读多写少、事务不复杂的管理系统。
1.3 完整功能模块清单
我把自己最终实现的功能整理成了一个表,这样你核对需求时候有对照:
| 模块名称 | 主要功能 | 涉及角色 |
|---|---|---|
| 登录认证 | 账号密码登录、JWT签发、退出登录 | 全部 |
| 用户管理 | 用户信息维护、角色分配、账号启用禁用 | 管理员 |
| 项目管理 | 项目申报、修改、提交审核 | 学生 |
| 项目审核 | 指导老师审核、退回修改 | 指导老师 |
| 评审管理 | 评审专家分配、在线打分、填写意见、评审汇总 | 管理员、评审专家 |
| 过程管理 | 中期检查材料提交、检查结果登记 | 学生、指导老师 |
| 结题管理 | 结题材料上传、结题审核 | 学生、指导老师 |
| 公告通知 | 公告发布、列表查看 | 管理员、全部 |
| 数据统计 | 项目状态统计、按学院分布统计 | 管理员 |
这套功能设计参考的是我自己接触过的实际业务流程。如果你的项目另有要求,比如想加“经费报销”“成果转化”,在这个框架上继续扩展接口和页面就行,不会伤筋动骨。
2. 数据库设计与核心表结构
2.1 数据库设计原则
数据库是一切业务的地基。大创系统这种业务,我的建议是先画状态机,再建表。为什么?因为项目管理最核心的就是状态:草稿、已提交、待老师审核、老师审核通过、待专家评审、评审通过(立项)、中期检查、待结题、已结题、已终止。每个状态对应哪些角色能做什么操作,都应该是确定的。
状态机画清楚之后再建表,每张表会非常自然。我画了一张简单的项目状态流转图(这里用文字描述,方便你在文档里画): 草稿 → 提交审核 → 指导老师审核(不通过则退回草稿)→ 学院推荐 → 专家评审(不通过则终止)→ 立项 → 中期检查 → 结题申请 → 已结题。
基于这个流程,核心表就浮现出来了:用户表、角色表、项目表、项目成员表、评审记录表、中期检查表、结题申请表、附件表、公告表。接着还要考虑一对多关系:一个项目有多名学生成员,一个专家评审多个项目,一个项目有多次阶段记录。
2.2 用户表和角色表
用户表不用设计太复杂,大创系统里的用户字段无非是学号/工号、姓名、密码、邮箱、电话、所属学院、类型。密码存的是BCrypt加密后的密文,不要用MD5明文,打死也不要。
角色这块我当时没有单独弄一套复杂的RBAC权限表(用户-角色-菜单-权限4张表),而是精简成了用户表中的user_type字段。为什么?因为大创系统的角色是固定的,不像企业系统需要动态配置权限点。用user_type = 1/2/3/4分别表示学生、指导老师、评审专家、管理员,拦截器里判断角色代码即可,代码简单,维护成本低。
如果你希望角色能灵活扩展,也可以把user_role、role_permission做成标准RBAC结构,但那种复杂度对这个项目来说有点过度设计。用到真实企业级多部门场景时再考虑。
2.3 项目主表和成员表
项目主表是系统的核心,字段大致如下:
CREATE TABLE project ( id BIGINT PRIMARY KEY AUTO_INCREMENT, project_code VARCHAR(32) COMMENT '项目编号', project_name VARCHAR(200) COMMENT '项目名称', project_year YEAR COMMENT '立项年度', category VARCHAR(50) COMMENT '项目类别,如创新训练、创业训练', level VARCHAR(20) COMMENT '国家级/省级/校级', status TINYINT COMMENT '项目状态 0草稿 1待审核 2审核通过 3评审中 4已立项 5中期检查 6待结题 7已结题 8已终止', student_id BIGINT COMMENT '项目负责人学生ID', teacher_id BIGINT COMMENT '指导教师ID', budget DECIMAL(10,2) COMMENT '预算金额', intro TEXT COMMENT '项目简介', create_time DATETIME, update_time DATETIME );项目成员单独一张project_member表,因为一个项目可能有3到5个学生,不能在一个字段里逗号分隔存ID,后面查成员、统计人数都会非常痛苦。成员表里带一个is_leader字段,用来区分谁是大创系统的项目负责人。
2.4 评审表和阶段表
评审表要和项目表关联,设计时有个建议:一份项目在多个专家之间评审,所以评审表主体是expert_id + project_id + review_status + score + opinion + review_time,一个专家对同一项目只保留一条评审记录,后面加一个唯一索引(project_id, expert_id)防止重复提交。
中期和结题我合并思路做成了一张阶段表:project_stage_log,里面有stage_type区分中期/结题/变更,content存内容提交说明,file_ids存关联附件,status记录该阶段是否通过。这个设计的好处是未来新增“年度进展报告”之类的阶段,不需要再加一张表,改枚举值就行。
附件我单独抽出来一张file_info表,存文件名、存储路径、上传人、关联业务类型和业务ID。这样项目申报材料、中期报告、结题报告都能复用同一个上传下载逻辑,不用每个模块都写一套文件管理。
3. 后端实现细节:SpringBoot + MyBatis 怎么搭才顺手
3.1 后端工程结构与依赖
我的后端工程结构大致如下,分包清晰,排查问题时顺着controller-service-mapper一条线找下去非常快:
src/main/java/com/xxx/innovate ├── controller # 接口层,只做参数接收和结果返回 ├── service # 业务逻辑层,事务都写在这层 ├── mapper # MyBatis数据访问层 ├── entity # 数据库实体 ├── dto # 前端交互用的数据对象 ├── vo # 视图对象,比如项目详情返回给前端展示 ├── config # 配置类,比如拦截器、跨域配置、CORS ├── common # 统一返回结果、常量、异常处理 └── util # 工具类,比如JWT工具、文件上传工具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.1</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>4.4.0</version> </dependency>JWT我是用的java-jwt这个库,比较轻量。你也可以用jjwt,都行。密码加密用spring-security-crypto里的BCrypt工具,但注意不要引入整套Spring Security,否则拦截器配置和你的原始方式冲突,处理不好会变成灾难。
3.2 统一响应与全局异常处理
前后端分离的项目,接口返回格式必须统一。我定义了一个Result<T>类,里面就三个字段:code(0成功,非0失败)、message、data。所有controller一律返回这个对象。
public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.code = 0; r.message = "success"; r.data = data; return r; } public static <T> Result<T> error(String message) { Result<T> r = new Result<>(); r.code = 500; r.message = message; return r; } }全局异常处理用@RestControllerAdvice,把业务异常、参数校验异常、未知异常分开处理。比如参数校验失败返回400提示,权限不足返回403提示,避免直接把一堆堆栈信息丢给前端。
3.3 登录认证与角色权限控制
这个系统最容易被忽略又最重要的就是权限。我用的方案是JWT + 拦截器,流程:
- 用户登录成功后,签发一个JWT,把userId和userType写进token里。
- 前端把token存到localStorage,之后每个请求都在请求头带上
Authorization: Bearer <token>。 - 后端写一个拦截器,拦截除登录接口外的所有请求,解析token并校验有效期,把用户信息放到ThreadLocal或Request attribute里供后续代码使用。
- 在需要角色判断的接口里,从用户信息里取出userType判断有没有权限。
拦截器核心代码:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行OPTIONS预检请求,否则前端跨域调用会被卡 if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } String authHeader = request.getHeader("Authorization"); if (authHeader == null || !authHeader.startsWith("Bearer ")) { throw new BusinessException("未登录或token缺失"); } String token = authHeader.substring(7); // 校验并解析token,失败会抛出异常由全局异常处理器兜底 LoginUser user = JwtUtil.parseToken(token); request.setAttribute("loginUser", user); return true; } }在WebMvcConfig里注册拦截器,并排除登录接口、静态资源等不需要认证的路径。
角色权限判断我写得比较简单:在service层先getLoginUser(),然后if判断。比如评审专家提交评审结果时,先查出该项目是不是当前评审人的,不是就抛异常。
// 只有专家角色能调用 if (loginUser.getUserType() != 3) { throw new BusinessException("无评审权限"); } ReviewRecord record = reviewMapper.selectByProjectAndExpert(projectId, loginUser.getUserId()); if (record == null) { throw new BusinessException("该项目未分配给你评审"); }3.4 项目申报接口的完整实现链
拿最核心的“学生申报项目”接口举个例子。前端传来的不是单张表数据,而是一个项目的基本信息、成员列表和上传的文件ID列表,需要在一个事务里同时操作三张表。
Controller层接收一个ProjectApplyDTO:
@PostMapping("/project/apply") public Result<?> apply(@RequestBody ProjectApplyDTO dto, HttpServletRequest request) { LoginUser user = (LoginUser) request.getAttribute("loginUser"); projectService.applyProject(dto, user); return Result.success(null); }Service层就是业务逻辑重点:
@Transactional(rollbackFor = Exception.class) public void applyProject(ProjectApplyDTO dto, LoginUser user) { // 1. 校验当前用户是学生 if (user.getUserType() != 1) { throw new BusinessException("仅学生可申报项目"); } // 2. 插入项目主表,状态初始为草稿或待提交 Project project = new Project(); BeanUtils.copyProperties(dto, project); project.setStudentId(user.getUserId()); project.setStatus(0); project.setCreateTime(new Date()); projectMapper.insert(project); // 3. 插入项目成员 if (dto.getMembers() != null) { for (MemberDTO member : dto.getMembers()) { ProjectMember pm = new ProjectMember(); pm.setProjectId(project.getId()); pm.setStudentName(member.getStudentName()); pm.setStudentNo(member.getStudentNo()); projectMemberMapper.insert(pm); } } // 4. 关联上传的文件,更新文件表的业务ID字段 if (dto.getFileIds() != null) { for (Long fileId : dto.getFileIds()) { fileMapper.updateBiz(fileId, "PROJECT", project.getId()); } } }注意@Transactional(rollbackFor = Exception.class),很多新手写事务时不指定rollbackFor,导致自定义业务异常不会触发回滚,项目主表插入了,成员表插入失败后留下半截数据,就很尴尬。
状态流转的update操作就更要小心。比如“提交审核”这个操作,updateProjectStatus要带条件where id = ? and status = 0,防止前端连续点击导致状态错乱。
UPDATE project SET status = 1, submit_time = NOW() WHERE id = #{projectId} AND status = 0如果影响行数为0,就说明状态已经不是草稿,服务端直接报“当前状态不允许提交”。
3.5 文件上传与下载
文件上传我用的SpringBoot自带的MultipartFile,存储路径放在服务器某个目录,而不是存进数据库的blob字段。生产环境文件一旦多起来,数据库会非常臃肿。FileService里面把文件保存到本地磁盘或OSS,数据库只存元数据。上传接口接收MultipartFile,返回文件的ID和访问地址,前端再把ID关联到项目表单里。
这里有个容易被坑的点:如果你使用Nginx部署前端并在后端接口路径用/api做前缀,文件上传后要保证Nginx也能正确代理到后端,跨域和路径重写都要提前测好。
4. 前端实现细节:Vue + Element UI 怎么组织不失控
4.1 前端项目结构与接口封装
前端我用Vue CLI创建的项目,配合Element UI做组件库。代码组织分了views(页面)、components(通用组件)、router(路由)、store(状态管理)、api(接口封装)、utils(工具)。
API封装是前端工程化的重点。我把所有接口请求统一封装成一个模块,axios实例带上baseURL和拦截器。请求拦截器里自动把localStorage的token塞进header;响应拦截器里统一处理业务code,非0则弹出错误提示,401则跳转登录页。这样页面代码里就不会到处写token获取逻辑了。
// api/request.js import axios from 'axios' import { Message } from 'element-ui' import router from '@/router' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }) request.interceptors.response.use( response => { const res = response.data if (res.code !== 0) { Message.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } else { Message.error('网络异常') } return Promise.reject(error) } ) export default requestbaseURL用/api而不是写死后端地址,是为了配合开发环境的proxy和上线后的Nginx统一代理。开发时在vue.config.js里配置代理到localhost:8080,上线时Nginx把/api转发到后端服务,这样前端代码不需要改动。
4.2 路由与前端登录守卫
路由表按角色拆分是这套系统的关键。管理员能看到用户管理、项目分配菜单;学生看到申报和我的项目;专家看到评审任务。我把路由分为公共路由、学生路由、教师路由、专家路由、管理员路由,然后在router.beforeEach里根据登录用户角色动态拼接。
一个简单的路由守卫实现:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.path === '/login') { next() return } if (!token) { next('/login') return } const user = JSON.parse(localStorage.getItem('userInfo') || '{}') // 校验路由meta中是否包含当前用户角色 if (to.meta && to.meta.roles && to.meta.roles.indexOf(user.userType) === -1) { next('/403') return } next() })路由守卫只做页面层的权限遮挡,真正的数据安全还是后端拦截器。前端隐藏菜单只是为了体验,不能信任前端权限,这是我一直强调的原则。
4.3 核心页面实现:申报表单、评审打分、进度看板
项目申报表单,我用的动态表单加表单校验。项目成员数量不固定,用一个v-for循环渲染成员输入行,用户可以增加和删除行。表单校验规则通过rules配置。Element UI的el-form配合el-form-item,校验错误信息会自动展示在输入框下方,交互体验比较标准。
评审打分页是评审专家最重要的操作界面。专家从待评审列表点击项目名,弹出一个详情抽屉,里面展示项目资料和成员信息,底部是评分星星组件和意见textarea。提交评审时调接口,后端校验项目是否分配给该专家,评分范围0到100分。
进度看板是给学生和老师看的,我把项目状态阶段做成一个步骤条el-steps,根据status字段显示当前在第几步。同时旁边展示每个阶段的材料提交情况,如果还没提交中期材料,就显示红色的待办标签。这种可视化非常受用,指导老师一打开就能看出哪些学生项目卡在哪个环节。
4.4 前后端接口约定
前后端联调时,接口约定最好文档化。我常用的格式是列表:
GET /api/project/my我的项目列表,返回分页数据:list + totalPOST /api/project/apply提交项目申报POST /api/project/{id}/submit提交审核GET /api/review/tasks获取我的评审任务列表POST /api/review/do提交评审结果PUT /api/user/{id}更新用户信息
所有分页接口约定返回{ code:0, data: { list: [], total: 0 } }结构。所有日期时间返回字符串格式yyyy-MM-dd HH:mm:ss,避免时间格式解析的差异。数字状态用数字而非字符串做枚举,前端映射表维护在常量文件里。
5. 完整部署教程:从零到能跑的每一步
5.1 环境准备清单
部署这套系统,你需要以下基础环境:
- JDK 1.8或11(建议JDK 8,兼容性最稳)
- Maven 3.6以上
- Node.js 14或16(Vue CLI5要求Node高版本,但过高的Node20可能遇兼容问题,建议14/16)
- MySQL 5.7或8.0
- Nginx 1.20以上(放前端静态资源并充当反向代理)
- 一个能访问到服务器的Linux环境(CentOS7或Ubuntu20.04都行,我这边用的是CentOS7)
开发阶段你也可以在Windows本机跑:后端用IDEA启动,前端用VSCode启动devServer,数据库用Navicat管理。上线部署可以参考下一小节的Linux流程。
5.2 数据库初始化
把项目里的sql文件上传到服务器MySQL,或者直接本地导入后导出,线上导入:
mysql -uroot -p < innovate_db.sql导入成功后确认所有表已经创建。然后修改后端配置文件application.yml:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/innovate_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true jwt: secret: 一串足够长的随机字符串 expire-days: 75.3 后端打包与启动
后端项目在服务器上编译打包。如果没有源码构建环境,可以在本地用IDEA打包成jar再上传;如果服务器装了Maven和JDK,直接执行:
mvn clean package -DskipTests打包后target目录下会生成一个innovate-server-1.0.0.jar。上传到服务器,建议放到/opt/innovate/目录,然后启动:
nohup java -jar innovate-server-1.0.0.jar --spring.profiles.active=prod > server.log 2>&1 &用tail -f server.log查看启动日志,看到Started ... in ... seconds就是成功了。检查端口:
netstat -ntlp | grep 80805.4 前端构建与Nginx配置
前端本地执行依赖安装和构建:
npm install --registry=https://registry.npmmirror.com npm run build构建产物在dist/目录。上传dist目录到服务器,比如放到/opt/innovate/dist。然后修改Nginx配置,我是这样写的:
server { listen 80; server_name 你的服务器IP或域名; # 前端静态文件根目录 root /opt/innovate/dist; index index.html; # 解决vue router刷新404问题 location / { 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; } # 上传文件访问路径 location /files/ { alias /opt/innovate/upload/; } }注意location /api/里的proxy_pass写法,末尾带不带斜杠差别很大。proxy_pass http://127.0.0.1:8080/;会把/api/project/list重写为/project/list转发给后端,所以后端的controller不需要写/api前缀,前端请求时带/api即可。
改完配置执行nginx -t检查语法,然后nginx -s reload生效。访问服务器IP,如果看到登录页,恭喜,部署成功了。
5.5 部署后验证流程
我的习惯是部署完按一条链路走通:
- 访问登录页,用管理员账号登录。
- 创建一个测试学生账号,退出管理员,用测试学生登录,提交一个项目。
- 切管理员账号,把项目分配一个评审专家。
- 登录评审专家账号,进入评审页面打分。
- 检查附件上传下载是否正常。
- 查看数据库里项目状态是否跟着变化。
这几步全部通过,说明前后端连通、数据库读写、JWT认证、权限控制、文件存储都没有问题。
6. 常见问题与避坑记录
6.1 后端常见故障
问题一:数据库连接时区报错。MySQL 8.x的驱动对时区敏感,如果连接串不加上serverTimezone=Asia/Shanghai,启动时会报SQLException: The server time zone value ...。另一个隐藏问题是MySQL连接时区设置与JVM时区不一致,导致接口返回的时间差8小时。统一在JDBC配置里加参数解决。
问题二:MyBatis的Mapped Statement绑定找不到。十有八九是以下几点:XML文件的namespace和Mapper接口全套类名不一致;接口方法名和XML的id不一致;mapper-locations路径写错。排查时打开SpringBoot的debug日志,确认加载了哪些XML文件。
问题三:jar包启动后端口被占。先netstat -ntlp | grep java看清楚端口PID,再kill -9杀掉旧进程。注意用了nohup启动的进程,如果直接关终端窗口进程也可能被hang up,正确应使用nohup方式启动,这样才能后台持续运行。
6.2 前端常见故障
问题一:跨域请求失败。开发环境用vue.config.js的proxy,线上环境用Nginx的proxy_pass,只要统一请求前缀到/api,就不会有跨域问题。如果直接让前端请求后端8090端口,后端就要写CorsFilter。我这边选择的是Nginx代理,理由是不需要在代码里开放跨域,线上更安全。
问题二:打包部署后刷新404。Nginx配置里必须加try_files $uri $uri/ /index.html;,因为Vue是History路由,刷新时会向服务器请求真实路径,服务器没有这个路径就返回404了。不加这段话,用户点浏览器刷新按钮就会白屏。
问题三:build出来的资源路径不对。Vite或Vue CLI默认资源引用是绝对路径/js/xxx.js,部署在域名子路径下就会404。需要在vue.config.js里设置publicPath: './'或部署时配置对应的base。
6.3 业务逻辑层的隐藏问题
并发重复提交。用户双击“提交审核”按钮,前端往往来不及禁用按钮,就可能触发两次请求。解决方法是后端update语句里带状态条件,或者利用数据库唯一索引兜底。我前后端都做了:按钮loading禁用 + 后端状态条件更新。
事务失效。这是最常见但是最隐蔽的坑。@Transactional默认只对RuntimeException回滚,如果你在事务方法里catch了异常没往外抛,事务也不会回滚。还有一点,事务方法和被调用方法不能在同一个类里用内部调用,否则动态代理不生效,事务等于摆设。
SQL注入。MyBatis使用${}拼接参数会存在注入风险,能用#{}就用#{}。动态排序字段、动态表名这种特殊场景非要使用${}时,必须在前端传入前做白名单校验。我在项目里排序字段做了一张允许排序的白名单Map,非法字段直接忽略。
7. 扩展思路与我的实际心得
这套系统交付到后面,其实已经不只是“能跑就行”。我把它的能力继续扩展了几处,这里分享出来给你作参考。
第一,消息通知机制。大创系统里最有价值的体验提升就是状态变化要主动通知。学生在项目被退回时如果没人提醒,拖上一周才看到就是事故。我给项目加了一张message表,状态变更时除了更新状态,同时插入一条待办消息。前端顶部导航栏显示未读消息红点,点击进入消息中心。实现成本不大,但对用户好感提升非常巨大。
第二,导出Excel。管理员经常要导出项目汇总表给学院存档。我用EasyExcel写了一个导出接口,把项目列表、负责人、指导老师、状态导出成一个Excel文件,前端通过window.location跳转下载接口即可。这个功能毕设答辩时非常能加分。
第三,评审汇总自动化。多个专家评同一项目时,后台需要一个项目评审总分汇总功能。我写了一个SQL:按项目ID分组求平均分。注意这里不要用AVG对NULL进行错误处理,评审状态不等于已评审的专家记录要过滤掉,否则汇总会被空记录拉低。
最后说一下我最深的体会:写大创系统这种业务系统,60%的时间都在处理“边界情况”。数据库边界(状态非法值、外键关联)、权限边界(越权操作)、接口边界(参数为空、格式错误)、部署边界(路径不对、端口被占)。真正把边界全处理干净,系统才谈得上可用。如果你是从零开始复现这套项目,我是真心建议先把项目状态机画清楚、把角色权限矩阵列明白,哪怕多花半天时间,后端的实现速度会快一两倍。
我后来把这套系统的源码、SQL脚本和部署文档整理成了一个完整包。如果你想直接拿来改改当课设或者毕设,重点不是跑起来,而是把每个模块背后的设计原因讲清楚,答辩的时候这反而是最亮的加分点。