资讯动态

SpringBoot+Vue选题管理系统:从数据库到部署的全栈实战解析

发布时间:2026/9/10 7:32:52 来源:尧图企业网站定制
每年到了课程设计选题季我身边总有学弟学妹为了选个好题目、避开雷区老师、凑齐靠谱队友发愁——教务处发一个Excel模板老师把题目贴在共享文档里学生在微信群里接龙抢题最后统计时还要人工核对哪些题满了、哪些人没选上。这个选题管理系统的初衷就是把这些混乱流程彻底线上化学生登录后可以浏览题目、提交申请教师能看到申请名单、一键通过或拒绝管理员则负责审核题目、控制选题开放状态、管理所有用户。技术栈用 SpringBoot Vue MyBatis MySQL前端负责交互展示后端提供Restful API数据库存全量数据是一个典型的全栈企业级课程设计项目。这套系统最适合正在做课程设计或毕业设计的人拿来二次开发也适合想系统学习前后端分离项目如何落地的人。你拿到的不只是一堆源码而是一整套从数据库设计、后端接口、前端页面到部署上线的完整实现方案。这篇文章我会把项目的核心设计思路、数据库表结构、关键接口、前端交互逻辑和部署方式全部拆开讲一遍同时把我在开发过程中踩过的坑一并写出来。我会尽量讲得具体、讲得能直接照着操作。1. 选题管理这个场景为什么值得做成一个完整项目1.1 传统选题方式的核心痛点在动手写代码之前我先把业务问题想清楚了。传统选题方式最大的问题不是没有系统而是信息不同步。老师在Excel里更新了一版题目学生手里还是老版本某道题已经有3个人选了但另一个学生仍然能看到它并提交申请好不容易等老师审核完还得人工在群里喊哪位同学还没选上。这些问题放在系统里本质上就是三个需求题目要有状态是待审核、已发布还是已截止学生只能看到已发布范围内的题。选题要有审核流学生申请后教师确认是否接收而不是谁手快谁就抢到。名额要有硬约束已经满员的题目不能再让人申请这个约束必须放在数据库层面控制不能只靠前端按钮置灰。这三个需求任何一个用Excel做都别扭但用数据库表加状态字段就能干净地解决。这也是我为什么说选题管理是一个非常适合做全栈项目的场景——业务清楚、流程完整、角色的权限边界又很分明。1.2 技术选型为什么是SpringBoot Vue MyBatis MySQL现在课程设计项目的主流技术栈基本就是这套组合而且这个组合确实是经过验证的。SpringBoot负责后端服务的快速搭建。它内置了Tomcat省去了繁琐的XML配置一个注解就能启动Web服务学习成本低资料也最多。Vue负责前端页面组件化的方式很适合把学生端、教师端、管理端拆成独立模块维护。MyBatis作为持久层框架比JPA更容易控制SQL尤其在多条件动态查询和复杂更新语句上MyBatis的XML映射文件写起来非常直观。MySQL则负责稳定保存所有业务数据。这个选型还有一个现实优势招人、找参考、遇到问题搜解决方案都容易。你遇到一个MyBatis的报错网上能搜到成百上千个案例你遇到Vue的官方文档也足够详细。对课程设计来说技术栈太冷门等于给自己挖坑。1.3 三个角色的功能边界系统的角色我分成三类管理员、教师、学生。它们的功能边界一开始就要划清因为背后对应的接口和页面完全不同。角色主要功能学生注册登录、浏览已发布题目、按关键词/分类筛选、提交选题申请、查看审核结果、取消申请教师发布题目、编辑自己的题目、查看申请列表、通过或拒绝学生申请、查看自己名下选题结果管理员审核教师发布的题目、管理用户账号启用/禁用、控制选题开放/关闭时间、发布系统公告这三个角色在数据库里通过一个role字段区分登录后根据角色进入不同的首页。后端接口也按角色做了权限控制学生不能调教师的审核接口教师在未开放选题时也不能给学生分配名额。这些规则看起来简单但在代码里要把每个接口的访问控制做好否则系统就变成了一个只有展示功能的空壳。2. 数据库设计四张核心表如何撑起选题全流程2.1 表关系梳理先从业务逻辑入手我设计数据库的习惯是先画业务关系图再落表结构。这个系统的核心关系其实就三组一个教师可以发布多个题目所以用户表和题目表是一对多关系。一个学生可以申请多个题目一个题目可以被多个学生申请所以学生和题目是多对多关系。多对多关系在关系型数据库里必须拆成一张中间表也就是选题记录表。管理员发布的公告是一张独立的表和用户表只存在发布人这个外键关联。最终我设计了四张表用户表、题目表、选题记录表、公告表。下面这是建表SQL字段命名、注释我尽量写得完整方便你直接导入MySQL使用。-- 用户表 CREATE TABLE t_user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录账号, password varchar(100) NOT NULL COMMENT BCrypt加密后的密码, real_name varchar(50) NOT NULL COMMENT 真实姓名, role tinyint(4) NOT NULL COMMENT 角色1管理员 2教师 3学生, student_no varchar(20) DEFAULT NULL COMMENT 学号学生角色使用, teacher_no varchar(20) DEFAULT NULL COMMENT 工号教师角色使用, department varchar(100) DEFAULT NULL COMMENT 院系/专业, phone varchar(20) DEFAULT NULL, email varchar(100) DEFAULT NULL, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 1启用 0禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 题目表 CREATE TABLE t_subject ( id bigint(20) NOT NULL AUTO_INCREMENT, title varchar(200) NOT NULL COMMENT 题目名称, description text COMMENT 选题描述, requirements text COMMENT 技术要求/成果要求, category varchar(50) DEFAULT NULL COMMENT 分类前端/后端/算法/数据库等, tech_stack varchar(200) DEFAULT NULL COMMENT 技术栈关键词, teacher_id bigint(20) NOT NULL COMMENT 发布教师ID, max_students int(11) NOT NULL DEFAULT 1 COMMENT 可容纳人数, selected_count int(11) NOT NULL DEFAULT 0 COMMENT 已选人数, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0待审核 1已发布 2已截止 3已下架, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_teacher (teacher_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT题目表; -- 选题记录表 CREATE TABLE t_selection ( id bigint(20) NOT NULL AUTO_INCREMENT, subject_id bigint(20) NOT NULL COMMENT 题目ID, student_id bigint(20) NOT NULL COMMENT 学生ID, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0待审核 1已通过 2已拒绝 3已取消, apply_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 申请时间, audit_time datetime DEFAULT NULL COMMENT 审核时间, audit_comment varchar(255) DEFAULT NULL COMMENT 审核意见, PRIMARY KEY (id), UNIQUE KEY uk_subject_student (subject_id,student_id), KEY idx_student (student_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT选题记录表; -- 公告表 CREATE TABLE t_announcement ( id bigint(20) NOT NULL AUTO_INCREMENT, title varchar(200) NOT NULL, content text, publisher_id bigint(20) NOT NULL COMMENT 发布人ID, is_top tinyint(4) NOT NULL DEFAULT 0 COMMENT 1置顶, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT公告表;2.2 几个关键的字段设计理由很多人建表时容易忽略状态字段和冗余字段的设计但在这个系统里这两类字段恰恰是核心。t_subject.status不是简单的上架/下架而是一条完整的题目生命周期。教师刚创建时是0待审核管理员审核通过后变为1已发布被选择超时或管理员手动关闭后变为2已截止教师自己也可以下架题目变为3。一个int字段就能表达这个过程查询时只用where status 1就能筛出所有可选题目非常方便。selected_count是一个冗余字段这个字段我强烈建议保留。如果不冗余每次查题目列表都要算一下这个题已经通过了几个人意味着每条题目都要做一次count查询列表页会非常卡。冗余字段会让列表查询变成一次简单的主表查询性能好很多。当然冗余的前提是必须在事务里维护它的一致性——每插入一条通过的选题记录就立刻更新这个字段。t_selection为什么要有单独的状态而不是直接存一个student_id到题目表因为一个学生的选题过程是有中间状态的。他提交了申请但老师还没审核这时候如果题目表只存一个已选学生ID那这个学生的记录到底算不算数就说不清了。所以中间表用status来承载已申请、已通过、已拒绝、已取消这四种状态业务才能完整流转。2.3 外键和索引的取舍我建表时没有写物理外键FOREIGN KEY全部用逻辑外键也就是只保存关联ID不建立数据库级的约束。原因很现实课程设计项目里的数据删除、批量操作如果被物理外键约束着非常容易报错排查起来也麻烦。逻辑外键在代码层面维护关系足够满足需求。索引方面username要加唯一索引因为它是登录账号t_selection上我建了(suject_id, student_id)的联合唯一索引作用是防止学生重复申请同一道题。这个唯一索引是兜底手段就算代码里忘记判断数据库也会拒绝插入重复记录。3. 后端实现SpringBoot和MyBatis如何把流程变成API3.1 工程结构按业务模块拆分还是按技术分层后端工程的包结构我推荐按技术分层为主、业务模块为辅的做法。因为这是一个课程设计项目代码量不大过度模块化反而增加理解成本。经典的三层结构清晰易懂com.example.subject ├── SubjectApplication.java // SpringBoot启动类 ├── common // 通用类 │ ├── Result.java // 统一返回体 │ ├── BizException.java // 自定义业务异常 │ └── GlobalExceptionHandler.java // 全局异常处理 ├── config // 配置类 │ ├── CorsConfig.java // 跨域配置 │ └── WebConfig.java // 拦截器注册 ├── controller // 控制层 │ ├── AuthController.java // 登录注册 │ ├── SubjectController.java // 题目接口 │ ├── SelectionController.java // 选题接口 │ ├── UserController.java // 用户管理接口 │ └── AnnouncementController.java // 公告接口 ├── entity // 实体类 ├── mapper // MyBatis Mapper接口 ├── service // 业务层 │ ├── impl └── util // 工具类 └── JwtUtil.java // JWT生成与解析3.2 登录认证与角色权限控制认证我用的是JWT方案没有引入Spring Security——对于课程设计项目来说Spring Security的配置门槛有点高用JWT 拦截器是更轻量、也更容易讲清楚的做法。具体流程是用户登录成功后后端把用户ID和角色装进Token里返回给前端前端把Token存在localStorage每次请求在Header里带上Authorization: Bearer token后端用一个拦截器拦截所有需要登录的接口从Token中解析出用户信息放进ThreadLocal方便后续代码取用。角色权限的校验我做了两个层面。第一层是在拦截器里统一校验是否登录第二层是在Controller方法上通过自定义注解标记所需角色再用另一个拦截器校验。比如PostMapping(/apply) RequireRole(role RoleConstant.STUDENT) public Result apply(RequestBody ApplyDTO dto) { Long studentId CurrentUser.get(); return selectionService.apply(studentId, dto.getSubjectId()); }这样学生就永远调不到教师的审核接口权限边界清楚面试答辩时也容易解释。3.3 核心接口从发布题目到完成选题的完整链路接口设计上我列一张表所有核心接口一目了然接口功能请求路径角色说明登录POST /api/auth/login所有角色返回JWT Token注册POST /api/auth/register学生注册后默认启用发布题目POST /api/teacher/subject教师保存后状态为待审核题目列表GET /api/student/subjects学生仅返回已发布题目提交选题申请POST /api/student/selection/apply学生学生申请选题取消申请POST /api/student/selection/cancel学生待审核状态可取消审核选题POST /api/teacher/selection/audit教师通过/拒绝学生申请审核题目POST /api/admin/subject/audit管理员通过教师发布的题目开放/关闭选题POST /api/admin/config/switch管理员全局控制选题阶段以提交选题申请为例这个接口是系统最核心的一段业务逻辑完整代码大概是这样Override Transactional(rollbackFor Exception.class) public boolean apply(Long studentId, Long subjectId) { // 1. 判断题目是否存在且状态为已发布 Subject subject subjectMapper.selectById(subjectId); if (subject null || subject.getStatus() ! 1) { throw new BizException(题目不存在或未发布); } // 2. 检查学生是否已有通过的选题一个学生最多只能通过一个选题 Integer passedCount selectionMapper.countByStudentAndStatus(studentId, 1); if (passedCount 0) { throw new BizException(你已经有一个通过的选题不能重复申请); } // 3. 已选人数加1这里用带条件的UPDATE解决并发抢题问题 int updated subjectMapper.increaseSelectedCount(subjectId); if (updated 0) { throw new BizException(该题目名额已满); } // 4. 插入选题记录 Selection selection new Selection(); selection.setSubjectId(subjectId); selection.setStudentId(studentId); selection.setStatus(0); return selectionMapper.insert(selection) 0; }对应的Mapper SQL是update idincreaseSelectedCount UPDATE t_subject SET selected_count selected_count 1 WHERE id #{subjectId} AND status 1 AND selected_count max_students /update这个写法是并发场景的关键。很多初学写法的同学喜欢先select看一下当前人数然后判断是否小于max_students再update。但这样在多线程下会出问题——两个请求同时读到还剩1个名额然后同时通过校验结果超录。把判断条件直接写进UPDATE语句数据库的锁机制会自动保证只有一个事务能成功执行。这既是一行精彩的条件更新也是答辩时能展示你是否理解并发控制的证据。3.4 MyBatis动态SQL多条件筛选题目的实现学生端的题目列表大概率需要支持按关键词、按分类、按技术栈筛选。这种多条件动态查询正是MyBatis XML的强项select idqueryByCondition resultTypecom.example.subject.entity.Subject SELECT * FROM t_subject where if testkeyword ! null and keyword ! AND (title LIKE CONCAT(%, #{keyword}, %) OR tech_stack LIKE CONCAT(%, #{keyword}, %)) /if if testcategory ! null and category ! AND category #{category} /if if teststatus ! null AND status #{status} /if if testteacherId ! null AND teacher_id #{teacherId} /if /where ORDER BY create_time DESC /selectwhere标签会自动处理首条AND的问题写起来舒服也避免了拼接SQL时的常见错误。分页我直接用PageHelper拦截器插件一行代码搞定PageHelper.startPage(pageNum, pageSize); ListSubject list subjectMapper.queryByCondition(condition); PageInfoSubject pageInfo new PageInfo(list);PageHelper 的原理是把分页参数绑定到ThreadLocal拦截器在查询前自动拼上limit语句。不过用的时候要注意PageHelper.startPage后面必须紧跟第一条查询语句中间不能插入其他SQL否则分页会失效。4. 前端实现Vue怎么把复杂流程变成清爽页面4.1 前端工程结构和基础封装前端我用的Vue 2 Element UI如果你用Vue 3改成Vue 3 Element Plus即可交互思路完全一样。工程结构按功能拆不是按页面拆src ├── api // 接口定义 │ ├── auth.js │ ├── subject.js │ └── selection.js ├── assets ├── components // 通用组件 ├── router // 路由 ├── store // Vuex ├── utils │ ├── request.js // axios封装 │ └── auth.js // token存取 └── views ├── login ├── student │ ├── SubjectList.vue // 题目大厅 │ └── MySelection.vue // 我的选题 ├── teacher │ ├── MySubjects.vue // 我的题目 │ └── AuditSelection.vue // 审核申请 └── admin ├── UserManage.vue // 用户管理 └── SubjectAudit.vue // 题目审核axios请求封装我几乎是每个项目必做的统一处理BaseURL、Token注入、错误提示避免每写一个接口都重复一遍逻辑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 401) { localStorage.clear() router.push(/login) return Promise.reject(new Error(未登录或登录已过期)) } if (res.code ! 200) { Message.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { Message.error(error.message || 网络异常) return Promise.reject(error) } ) export default request4.2 路由守卫与角色分流这个系统有一个特点三种角色登录后进入的首页完全不同。我用路由守卫在这个环节做了统一处理router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path /login) { next() return } if (!token) { next(/login) return } // 动态判断用户角色然后允许访问对应目录 const role localStorage.getItem(role) if (to.path.startsWith(/student) role ! student) { next(/student/subjects) return } if (to.path.startsWith(/teacher) role ! teacher) { next(/teacher/my-subjects) return } if (to.path.startsWith(/admin) role ! admin) { next(/admin/users) return } next() })这样做的核心价值不是防破解而是避免用户手输URL进入没有权限的页面。后端接口层面必须再做一次权限校验前端路由守卫只是体验层面的保障。4.3 题目列表页状态驱动的交互页面交互里最有代表性的就是学生端的题目大厅。一道题的完整状态影响三个东西按钮是否可点、按钮文案是什么、卡片是否置灰。题目状态为已发布当前用户未申请过按钮显示申请可点击。题目状态为已发布当前用户申请待审核按钮显示审核中不可点击。当前用户已通过该题按钮显示已通过整张卡片高亮。题目已满员按钮显示已满员不可点击。这些状态不用前端自己算后端接口在返回题目列表时会带上当前用户对这个题目的申请状态字段applyStatus。前端只需要根据这个字段做条件渲染。交互上有个细节要注意申请成功后列表数据必须重新拉取不能只改当前按钮的文案否则刷新后状态又回退了。我通常的做法是申请接口成功后在回调里重新load一次当前列表。教师审核页的交互也很直观。教师点进申请列表看到的是学生信息、申请时间、当前状态。操作区只有两个按钮通过、拒绝。拒绝时弹窗要求填写原因这个原因会呈现给学生端方便学生知道自己被拒的原因这是很多课程设计项目会忽略的细节。管理员的全局控制页面核心是一个开关选题阶段是否开放。开放时学生可以提交新申请关闭时申请接口直接返回选题已截止。这个开关可以存到系统配置表里用一个常量接口读取。课程设计阶段可以不做多配置项存到一个简单的config表里即可。5. 跑通整套系统从环境配置到打包部署5.1 环境版本清单在开始安装之前先说一个版本兼容性的关键结论这个坑我踩过一次。SpringBoot 3.x 要求JDK 17以上而很多课程设计环境还在用JDK 8。如果你本机是JDK 8就老老实实用SpringBoot 2.7.x并且对应的MyBatis starter版本也要匹配。SpringBoot 2.7.18 mybatis-spring-boot-starter 2.3.2这套组合是我实测过最稳的。Vue那边Vue 2.6 Element UI 2.15 和 Vue 3 Element Plus 都行如果电脑配置不高建议Vue 2构建更快。Node版本也很关键。Vue 2 项目如果用了node-sassNode 17以上直接安装失败因为node-sass不走纯JavaScript需要编译原生模块。我现在统一用sassdart-sass替代node-sass兼容性好很多。依赖推荐版本说明JDK1.8 或 17与SpringBoot版本匹配Maven3.6打包构建SpringBoot2.7.x稳定、资料多MyBatis Starter2.3.x与SpringBoot 2.x匹配MySQL5.7 或 8.0建议8.0注意时区配置Node.js14 或 16Vue 2项目建议Vue2.6.x搭配Element UIElement UI2.15.x老牌组件库5.2 后端启动配置与应用配置数据库建好后第一步是修改后端的application.yml。这里有几处必须写对否则项目起不来server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/subject_manage?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 你的密码 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.subject.entity configuration: map-underscore-to-camel-case: true logging: level: com.example.subject.mapper: debugmap-underscore-to-camel-case: true这行很重要。数据库字段是real_name实体类是realName没有这个配置你查询出来的实体属性全部是null。logging.level配置成debug可以在控制台打印MyBatis的SQL日志开发排查问题时非常有用我建议开发阶段一定要开。启动方式也很简单mvn clean package -DskipTests java -jar target/subject-manage-1.0.0.jar5.3 前端开发环境代理与打包部署前端开发时最大的问题是跨域。前后端分离项目前端跑在8080端口后端跑在8080端口如果前端直接用axios.get(http://localhost:8080/api/...)浏览器会报跨域错误。解决办法是在vue.config.js里配置代理module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }这样前端请求/api开头的路径时开发服务器会转发给8080端口前端代码里不需要写完整的后端地址。生产部署时用Nginx做反向代理同样转发/api请求即可server { listen 80; server_name localhost; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://localhost:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }注意try_files $uri $uri/ /index.html;这一行是保障Vue Router的history模式刷新不404的关键。没有这行用户在某个路由下刷新页面Nginx会去找对应路径的真实文件找不到就返回404。生产环境打包命令npm install npm run build构建完成后生成dist目录把dist目录里的所有文件上传到服务器再加上上面那段Nginx配置整个系统就能通过公网访问了。6. 开发过程中真实踩过的几个坑6.1 环境类问题版本不匹配和时区错乱SpringBoot 3.x 配JDK 8启动直接报错UnsupportedClassVersionError。这个问题看似低级但很多人在创建新项目时不注意版本匹配等代码写完了才回来改环境代价很大。我的建议是创建项目前先定好版本清单统一用SpringBoot 2.7.x JDK 8资源占用小兼容性最好。MySQL 8.0的时区问题也很经典。连接数据库时如果URL里不写serverTimezoneAsia/Shanghai启动项目时会报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。这个错误第一眼看上去像乱码其实是MySQL和JDBC驱动之间的时区协商失败。配置里加上时区参数就彻底解决了。6.2 MyBatis的隐藏坑字符串比较和批量插入MyBatis的XML里if teststatus ! null and status 1这种写法会报错或者查不出数据。原因是MyBatis的OGNL表达式里单引号内的内容被当成字符类型和String类型比较时可能出问题。正确的写法是if teststatus 1 AND status 1 /if外层用单引号内层用双引号这样OGNL才会把1识别成字符串。如果status字段是int类型直接写status 1就行。批量插入时很多人直接在Mapper接口里写Insert(script INSERT INTO t_selection(subject_id, student_id, status) VALUES foreach collectionlist itemitem separator, (#{item.subjectId}, #{item.studentId}, #{item.status}) /foreach /script)这种写法在MySQL里没问题但要注意单次批量插入的数据量超过1000条时建议分批否则MySQL可能因为单条SQL过大而报错或性能下降。普通课程设计项目里单次不会超过几十条问题不大。6.3 前后端联调的坑代理路径和Token失效联调时最常见的现象是前端页面打开了请求也发出了但控制台network里一片红。排查步骤就三步第一步看请求URL是不是http://localhost:8081/api/...这种相对路径。如果直接写了http://localhost:8080/api/...说明绕过了代理会触发跨域。第二步看代理配置vue.config.js里/api的target是不是后端真实地址。第三步看后端日志如果请求其实到了后端但返回401大概率是Token失效重新登录一下就行。我把这三步走了一遍80%的联调问题都能解决。还有一个前端容易忽略的地方JWT里有过期时间但前端并不知道什么时候过期。如果用户长时间停留在一个页面不操作突然点申请按钮请求发出去被后端拦截器拦截返回401但前端页面并没什么直观感知。我的处理方式是在axios响应拦截器里统一处理401——清空本地存储、跳转登录页、提示登录已过期请重新登录。这个统一处理要放在全局而不是在每个接口单独判断。6.4 业务逻辑上的坑状态流转和删除保护最后一个坑是业务逻辑层面的。刚开始实现时我把学生取消申请的逻辑写成只要选题记录状态是待审核就把记录删掉。但后来发现这样有问题——教师已经通过之后学生不能反悔系统里必须保留这条通过的记录。所以取消操作只能作用于待审核状态并且不能物理删除而是把状态改成已取消保留历史轨迹。类似的还有删除保护。管理员删除题目时如果这个题目已经有人申请了直接删掉会丢失申请记录。我的做法是有申请记录的题目不能物理删除只能下架管理员看到的删除按钮实际上调用的是下架接口。这个细节能避免很多数据不一致的问题。我建议所有状态变化都用新增状态值而不是物理删除来承载因为业务数据是有审计价值的。虽然在课程设计里你不需要完整审计日志但保留记录会让你的项目看起来更规范——答辩时这也是一个可以主动讲解的亮点。经过这几个版本迭代这套选题管理系统已经在我自己手里跑通了完整流程教师注册发布题目、管理员审核上架、学生申请、教师录取、管理员统计。如果你拿到源码后想在此基础上扩展我还建议你按这个顺序做三件事第一步增加选题结果的Excel导出教师和管理员都能导出名单这是最实用的功能第二步给公告模块加上已读标记让学生能看到未读公告的数量第三步把选题结果页做成按教师维度统计的报表方便学院管理者查看。这三个功能任何一个做出来都会让项目在答辩时的完整度和实用度明显提升。

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

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

免费获取报价