新闻详情

新闻详情

首页 / 资讯中心 / 详情

前后端分离实战:SpringBoot+Vue+MyBatisPlus打造成绩管理系统全流程解析

发布时间:2026/9/30 7:29:14来源:尧图网络
前后端分离实战:SpringBoot+Vue+MyBatisPlus打造成绩管理系统全流程解析
简介一份基于Spring Boot、Vue.js与MyBatisPlus的前后端分离实战教程以学生成绩管理系统为案例适合具备Java基础、希望掌握全栈开发流程的开发者。资源为PDF格式共1个文件压缩包大小约2.8MB内容涵盖数据库设计、后端搭建与前端界面开发。数据库设计含am_user、am_student、am_score三张核心表并演示软删除字段is_deleted的运用。后端采用Spring Boot 2.7.17作为基础框架配合MyBatisPlus简化数据访问覆盖Mapper接口、实体类及RESTful API设计前端使用Vue.js组件化构建页面并通过axios与后端交互。教程从建表SQL讲到前后端联调结构清晰可直接对照学习已有448人学习下载适合入门与进阶参考。1. 前后端分离成绩管理系统为什么这套组合最值得照着重做学生成绩管理系统大概是前后端分离实战里被问得最多的一种工作里也常拿它当内部系统的练手原型。它的技术组合非常标准SpringBoot 出 REST 接口Vue 出页面MyBatisPlus 把单表 CRUD 和分页从手写 SQL 里解放出来。难点其实不在某个框架而在把三者串起来——建表时怎么防止成绩重复录入、联调时跨域怎么配、分页为什么翻到第二页就没数据。这篇文章按一个可复现的最小系统讲完整链路适合有 Java 和 JavaScript 基础、想独立走完一遍前后端分离项目的开发者哪怕你手里只有一份带水印的 PDF 截图照着下面的步骤也能把系统搭出来。2. 用 SpringBoot MyBatisPlus 落后端模块划分、建表与成绩 CRUD后端是整个系统最先动工的部分。用 IDEA 新建 SpringBoot 项目时很多人第一反应是选最新版本结果 SpringBoot 3.x 配 MyBatisPlus 3.5.x 的 starter 名称完全不同抄 2.x 时代的教程必然翻车。我一般固定用 SpringBoot 2.7.x JDK 8/11等这套骨架跑通了再考虑升级。2.1 依赖先选对SpringBoot 版本与 MyBatisPlus starter 的搭配MyBatisPlus 对 SpringBoot 的适配分成两条线SpringBoot 2.x 用mybatis-plus-boot-starterSpringBoot 3.x 必须用mybatis-plus-spring-boot3-starter后者要求 JDK 17 起步。版本越高连带 Lombok、MySQL 驱动的兼容问题越多这就是为什么很多老教程在 3.x 项目上跑不起来。依赖建议版本作用spring-boot-starter-parent2.7.x统一管理依赖版本避免 jar 冲突mybatis-plus-boot-starter3.5.x提供 BaseMapper、IService、分页插件mysql-connector-j8.0.xMySQL 驱动runtime 作用域即可lombok由 parent 管理减少 getter/setter 样板代码pom.xml 里最核心的依赖长这样parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies说明mybatis-plus-boot-starter的版本我写的是 3.5.3.1这只是我本地 Maven 仓库正在用的版本。实际以仓库里能拉到的最新 3.5.x 为准不建议上 3.4 以下的旧版分页插件和 LambdaQueryWrapper 在旧版上的表现差异很大。SpringBoot 的 parent 会把 Lombok 版本管好所以这里不写 version。选 MyBatisPlus 而不是原生 MyBatis理由是单表 CRUD 完全不用写 SQLBaseMapper内置了 insert、delete、selectBatchIds 等方法IService又包了一层批量操作和链式查询。成绩管理系统里 80% 的接口是单表查询加条件筛选MyBatisPlus 恰好把这一部分压缩成几行代码。2.2 建表先想清楚业务边界学生、课程、成绩表怎么设计成绩管理系统的基础表通常是三张学生表、课程表、成绩表。前两张是基础数据成绩表是关联核心。成绩表最容易犯的错误是不加唯一索引导致同一学生同一课程同一考试类型被录进去两条分数。CREATE TABLE student ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, class_id BIGINT NOT NULL COMMENT 班级ID, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_student_no (student_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生表; CREATE TABLE course ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 课程名 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT课程表; CREATE TABLE score ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, course_id BIGINT NOT NULL, score DECIMAL(5,1) NOT NULL COMMENT 分数保留一位小数, exam_type VARCHAR(20) NOT NULL DEFAULT 期中 COMMENT 期中/期末/补考, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_student_course_exam (student_id, course_id, exam_type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT成绩表;说明score字段用DECIMAL(5,1)最大值 999.9足够覆盖百分制同时避免浮点误差。成绩表的联合唯一索引uk_student_course_exam是业务层的底线防线程序里重复提交了数据库也会把数据挡在外面。实体类与表结构一一对应MyBatisPlus 的注解只写必要的部分TableName(score) Data public class Score { TableId(type IdType.AUTO) private Long id; private Long studentId; private Long courseId; private BigDecimal score; private String examType; TableField(fill FieldFill.INSERT) private LocalDateTime createdAt; }说明TableName把实体映射到 score 表TableId(type IdType.AUTO)表示主键自增插入后 MyBatisPlus 会把数据库生成的主键回填到 id 字段studentId字段不需要写TableFieldMyBatisPlus 默认开启驼峰转下划线studentId会自动映射到student_id。createdAt填了FieldFill.INSERT配合 MetaObjectHandler 可以在插入时自动写入当前时间不用每次手动 set。2.3 Service 层写查询LambdaQueryWrapper 与条件筛选Service 接口继承IServiceScore实现类继承ServiceImplScoreMapper, Score基础 CRUD 方法就全部具备了。真正的业务复杂度在查询条件的拼装上这里用 LambdaQueryWrapper 比 QueryWrapper 稳妥得多。Override public PageScore pageScores(ScoreQuery query) { LambdaQueryWrapperScore wrapper new LambdaQueryWrapper(); wrapper.eq(query.getCourseId() ! null, Score::getCourseId, query.getCourseId()) .ge(query.getMinScore() ! null, Score::getScore, query.getMinScore()) .le(query.getMaxScore() ! null, Score::getScore, query.getMaxScore()) .eq(StringUtils.hasText(query.getExamType()), Score::getExamType, query.getExamType()) .orderByDesc(Score::getId); return scoreMapper.selectPage(new Page(query.getPageNum(), query.getPageSize()), wrapper); }说明LambdaQueryWrapper 的每个条件方法第一个参数是 boolean 值条件不满足时整个条件自动跳过比如courseId为 null 时eq会被忽略这样就避免了写一堆if (xxx ! null)判断。Score::getCourseId是方法引用字段名写错会在编译期直接报错而 QueryWrapper 用的是字符串写错只能等运行时报 SQL 异常。Controller 层只做参数接收和结果包装不写业务逻辑RestController RequestMapping(/api/scores) public class ScoreController { Resource private ScoreService scoreService; GetMapping public ResultPageScore page(ScoreQuery query) { return Result.ok(scoreService.pageScores(query)); } PostMapping public ResultVoid save(RequestBody Score score) { scoreService.save(score); return Result.ok(); } }说明Controller 统一返回ResultT包装体结构是{ code, message, data }。前端 axios 拦截器按这个结构统一解包后端每个接口不需要各自处理异常。成绩录入接口这里不做重复校验因为数据库唯一索引已经兜底save抛出的 DuplicateKeyException 交给全局异常处理器转成友好提示即可。3. 用 Vue 搭前端Vite 工程、路由与成绩录入页面前端部分我用 Vue3 Vite Element Plus 这套组合。Vue2 的选项式 API 写小项目确实快但前后端分离项目实战最好一步到位用 Vue3 组合式 API业界新项目基本都在这条线上。3.1 vue 安装及环境配置先把 node 和 devtools 这两件事做对vue 安装及环境配置里最容易出问题的是 Node 版本。Vite 5 要求 Node 18 以上很多人的电脑还停在 16.x跑npm create vite直接报引擎不兼容。我习惯先确认版本再动手node -v npm -v npm create vitelatest score-web -- --template vue cd score-web npm install npm run dev说明npm create vitelatest不需要全局安装任何东西它会临时拉取 create-vite 脚手架。--template vue指定 Vue 模板生成的是 Vue3 单页应用骨架。如果npm install慢先检查是否配了国内镜像源npm config get registry应该返回一个可用的镜像地址这比反复删 node_modules 靠谱。前端调试工具方面Vue DevTools 插件在浏览器扩展商店下载时要注意版本Vue3 用的是独立的 DevTools 扩展Vue2 那套旧图标工具在新版浏览器里经常失灵。装好后打开页面Vue 应用会被识别组件树、Pinia 状态、路由跳转记录都能在面板里直接看这个工具能省掉一半的console.log。3.2 路由与页面结构按「学生-课程-成绩」拆三个视图页面结构按业务拆成三个视图学生管理、课程管理、成绩管理。路由用动态 import每个页面在访问时才加载对应 JS 包首屏体积小很多。// src/router/index.js import { createRouter, createWebHashHistory } from vue-router const routes [ { path: /, redirect: /scores }, { path: /students, component: () import(/views/StudentList.vue) }, { path: /courses, component: () import(/views/CourseList.vue) }, { path: /scores, component: () import(/views/ScoreList.vue) }, ] const router createRouter({ history: createWebHashHistory(), routes, }) export default router说明createWebHashHistory()是 hash 模式URL 里带#部署到任何静态服务器都不会出现刷新 404 的问题。如果改成createWebHistory()的 history 模式Tomcat 或 Nginx 必须配置 fallback 到 index.html否则用户手动刷新成绩页会直接白屏。实战项目里我默认用 hash 模式只有明确要求美观 URL 时才换 history。UI 组件库用 Element Plus在 main.js 里全量注册即可成绩管理这类后台页面不需要按需引入带来的那点体积收益npm install element-plus成绩录入页的核心是表格加分页。表格列用 el-table 渲染分页用 el-pagination数据通过 axios 从后端拉// src/views/ScoreList.vue 关键脚本 import { ref, reactive, onMounted } from vue import request from /utils/request const list ref([]) const total ref(0) const query reactive({ pageNum: 1, pageSize: 10, courseId: undefined }) async function loadScores() { const data await request.get(/scores, { params: query }) list.value data.records total.value data.total } onMounted(loadScores)说明reactive包裹的 query 对象页面里绑定到 el-pagination 的v-model:current-page和v-model:page-size用户翻页或改每页条数时 query 自动更新再调用 loadScores 即可。这里的data.records和data.total对应后端 MyBatisPlus 的PageT对象结构前后端字段名要对齐这也是分页联调第一个要确认的地方。3.3 axios 封装与接口对接统一响应体让页面少写一百个 ifaxios 不能直接用必须封装一层。后端返回的是{ code, message, data }结构如果每个页面都去判断 code 再解包几十个接口下来全是重复代码。拦截器统一处理整体逻辑// src/utils/request.js import axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: /api, timeout: 15000, }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) config.headers.Authorization Bearer ${token} return config }) request.interceptors.response.use( res { const body res.data if (body.code ! 200) { ElMessage.error(body.message || 请求失败) return Promise.reject(new Error(body.message)) } return body.data }, err { ElMessage.error(err.response?.status 401 ? 登录已过期 : 网络异常) return Promise.reject(err) } ) export default request说明响应拦截器直接返回body.data页面里const data await request.get(/scores)拿到的就是纯业务数据不需要再.data.data。401 状态统一跳登录页的逻辑也可以放在这里页面代码不用关心登录过期的问题。baseURL: /api在开发环境由 Vite 代理转发到后端上线后由 Nginx 反代或后端同域部署承载前端代码不需要为环境切换改任何请求路径。Vue 的响应式原理值得深入浅出地捋一遍ref和reactive的区别是新手最容易混的。ref适合包裹基本类型和需要重新赋值的对象reactive适合深层嵌套对象把reactive对象整体重新赋值会丢失响应式所以表格数据我用ref([])配合list.value data.records赋值而不是reactive({ list: [] })再整体替换。4. 前后端分离联调的关键几步跨域、鉴权与统计接口返回结构前后端分离项目联调时最大的摩擦集中在三件事跨域怎么配、登录状态怎么传、统计类接口返回什么结构。这三件在单页应用里都绕不开也是最容易被 localhost 环境掩盖、上线后才爆发的问题。4.1 跨域配置开发用代理上线用同域反代开发环境最省事的做法是让前端代理转发请求而不是在后端开 CORS。浏览器访问的是 localhost:5173前端把/api开头的请求代理到 localhost:8080浏览器视角里所有请求都是同源的根本不会触发跨域。// vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ), }, }, }, })说明rewrite把/api/scores改写成/scores再转发给后端所以后端的 Controller 实际路径不需要带/api前缀。changeOrigin: true让代理请求头里的 Host 变成 target 地址避免后端拿到错误的来源。如果后端接口路径本身设计为带/api把 rewrite 去掉即可。后端如果确实需要支持跨域调用配置要谨慎常见的错误是把allowedOrigins设为*又同时开启allowCredentials(true)浏览器会直接拒绝这种组合。Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOrigins(http://localhost:5173) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowCredentials(true) .maxAge(3600); } }说明maxAge(3600)是预检请求的有效期单位秒。前后端分离的浏览器跨域请求非简单请求会先发 OPTIONS 预检有效期设长一点能减少预检次数。生产环境如果前端资源和后端接口不在同一个域名优先用 Nginx 反向代理把/api转发到后端服务这种方案比后端 CORS 更干净。若依这类成熟的脚手架把前后端分离的代理和鉴权设计得很完整值得参考但新手不建议直接套整框架里面附带太多代码生成器的功能反而看不清自己这套链路的全貌。4.2 登录与鉴权JWT 在请求头里怎么流转学生成绩管理系统属于典型的内部管理系统登录鉴权最常用的方案是 JWT后端无状态用户信息签在 token 里横向扩容时不需要共享 Session。登录接口校验账号密码后签发 token前端存 localStorage之后每个请求在 axios 拦截器里自动带上。后端用一个过滤器统一校验请求头里的 token这是最直白的做法Component public class JwtFilter extends OncePerRequestFilter { public static final String HEADER Authorization; public static final String PREFIX Bearer ; Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String token request.getHeader(HEADER); if (StringUtils.hasText(token) token.startsWith(PREFIX)) { String userId JwtUtil.parseToken(token.substring(PREFIX.length())); if (userId ! null) { request.setAttribute(userId, userId); } else { response.setStatus(HttpStatus.UNAUTHORIZED.value()); return; } } chain.doFilter(request, response); } }说明过滤器只处理带 Authorization 头的请求token 解析失败直接返回 401解析成功把 userId 放进 request attribute后续 Controller 可以从 request 里取当前登录人。登录接口、验证码接口这类放行路径在过滤器里加一个白名单判断或者在 Spring Security 里统一配置。这套轻量方案比引入 Spring Security 全家桶好维护得多成绩系统用不到那么细的权限模型。前端在 3.3 节的 axios 拦截器里已经把 token 放进请求头登录成功后localStorage.setItem(token, token)之后所有请求自动携带。token 过期时间初版建议设 30 分钟到期后强制重新登录。刷新 token 机制属于二期优化不要在第一个版本里做否则联调周期会明显拉长。4.3 成绩统计接口班级均分与分数段分布怎么聚合成绩管理系统的统计需求一般有班级平均分、分数段人数分布。这类聚合查询用 LambdaQueryWrapper 硬拼非常别扭直接在 Mapper 里写 SQL 更直白。MyBatisPlus 不影响你用注解 SQL二者可以混用。Mapper public interface ScoreMapper extends BaseMapperScore { Select(SELECT c.name AS className, ROUND(AVG(s.score), 1) AS avgScore FROM score s JOIN student st ON s.student_id st.id JOIN course c ON st.class_id c.id WHERE s.course_id #{courseId} GROUP BY c.id) ListMapString, Object avgScoreByClass(Param(courseId) Long courseId); }说明返回类型用ListMapString, Object列名className和avgScore会被 MyBatisPlus 的驼峰映射自动处理。为什么不封装成 VO统计接口的列经常按前端图表需求变化Map 结构改动成本最低前端拿到直接渲染图表。数据库别名里用驼峰避免返回下划线命名再让前端转换一次。分数段分布统计用 CASE WHEN 一次查询算出所有段位SELECT SUM(CASE WHEN s.score 90 THEN 1 ELSE 0 END) AS excellent, SUM(CASE WHEN s.score 60 AND s.score 90 THEN 1 ELSE 0 END) AS pass, SUM(CASE WHEN s.score 60 THEN 1 ELSE 0 END) AS fail FROM score s WHERE s.course_id #{courseId}说明这一段 SQL 放在注解里或 XML 里都可以返回的 excellent、pass、fail 三个字段直接对应前端 ECharts 饼图的三个数据点。如果想按班级拆分在末尾加GROUP BY st.class_id即可。统计类 SQL 的执行计划最好用 EXPLAIN 看一眼score 表行数过万后建议在course_id、student_id上加索引。5. 避坑分页失效、字段映射错位与其他高频翻车点这一章写的是我在类似项目里真正踩过、也被反复问过的五个问题。每条按现象、原因、解决的顺序写按这套清单排查能省下大量联调时间。5.1 MyBatisPlus 分页失效total 一直是 0数据却是全表的现象selectPage返回的records是全表数据total却是 0翻到第二页返回的数据和第一页完全一样。原因MyBatisPlus 的分页插件没有注册。selectPage方法本身只是把Page对象当作参数往下传真正在 SQL 里拼LIMIT的是PaginationInnerInterceptor。没注册拦截器SQL 里就没有 LIMITPage 对象失去意义。这也是网上搜「MyBatisPlus 分页失效」出现频率最高的原因。解决在配置类里注册 MyBatisPlus 拦截器链。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }说明DbType.MYSQL必须和数据库类型一致PostgreSQL 或 Oracle 的分页语法不同写错会生成错误的方言 SQL。排查分页问题时最有效的办法是打开 SQL 日志看控制台打印的语句末尾有没有LIMIT。MyBatisPlus 的 SQL 日志配置项是mybatis-plus.configuration.log-impl: org.apache.ibatis.logging.stdout.StdOutImpl确认日志里出现 LIMIT分页才算真正生效。5.2 字段映射错位数据库下划线实体驼峰查出来却是 null现象数据库列名是student_id实体字段是studentIdLambdaQueryWrapper 查询条件正常但查询结果里studentId一直是 null。原因原生 MyBatis 默认mapUnderscoreToCamelCase是 falseMyBatisPlus 对该配置的默认值是 true。问题出在混用场景——Mapper 里只要手写了Results或resultMap自动映射就会被覆盖字段映射立刻回到默认状态。解决全局配置里显式打开驼峰映射。mybatis-plus: configuration: map-underscore-to-camel-case: true同时遵循一条原则不要在 Mapper 里写无意义的 resultMap让 MyBatisPlus 自动映射。如果自定义 SQL 确实需要映射给实体字段加TableField(student_id)直接把字段名钉死比在 resultMap 里逐列配置稳定得多。这个坑最磨人的地方在于用 BaseMapper 自带方法一切正常一写自定义 SQL 就出现 null 字段排查时先怀疑映射配置而不是数据本身。5.3 日期返回给前端变成数组JSON 序列化格式没配现象接口返回的createdAt字段是[2024, 5, 1, 10, 30, 0]这种数组结构前端表格里直接渲染成一行数字。原因Jackson 序列化LocalDateTime时默认输出为数组形式或者没有注册JavaTimeModule直接报InvalidDefinitionException。SpringBoot 2.x 默认依赖 Jackson但对 Java 8 时间类型的支持需要在配置里显式声明格式。解决在 application.yml 里统一配置时间格式。spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8说明date-format只对java.util.Date生效对LocalDateTime需要配合JsonFormat使用。实体字段上可以这样写JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private LocalDateTime createdAt;前后端传参的约定也建议统一前端传日期用字符串后端接收用DateTimeFormat(pattern yyyy-MM-dd)。最忌讳的是前端传 Date 对象、后端用时间戳接收这样联调十有八九要因为时区问题返工。5.4 「MyBatisPlus 单页 500 条限制」是个误会现象网上搜到「MyBatisPlus 单页 500 条限制」的说法自己项目里翻页超过 500 条就开始异常表格卡死甚至看不到后续数据。原因与解决MyBatisPlus 本身没有 500 条限制。这个说法大概率是把分页拦截器缺失导致的「分页失效、全表返回」记混了或者是把其他分页组件的默认行为安到了 MyBatisPlus 头上。我遇到一次真实情况是分页拦截器没生效接口把整张成绩表一次性返回前端渲染几千行数据浏览器明显卡顿体感上就像被限制在某一个量级。排查这类玄学问题第一件事永远是开 SQL 日志确认有没有 LIMIT。顺便说一句单页条数不要真的给到 500我一般把pageSize上限限制在 100超出就提示用户缩小查询范围。数据量大时走条件筛选而不是靠翻页硬扛。网上教程抄来抄去的说法太多排障时别被先入为主的结论带偏。5.5 逻辑删除与唯一索引撞车现象学生表student_no上有唯一索引。删除某个学生后再录入一个同号学生数据库报Duplicate entry for key uk_student_no。原因系统做的是逻辑删除删除只是把delete_flag置为 1数据行仍然留在表里。唯一索引依然占用着这个学号新插入的数据自然冲突。解决三类方案。第一成绩管理系统这类内部系统只要操作留痕物理删除加操作日志表完全够用删除逻辑移到归档表主表不保留已删除数据。第二如果坚持逻辑删除唯一索引要改成包含删除标记的联合索引比如(student_no, delete_flag)但删除时delete_flag要写入不同值避免多条已删数据互相冲突实现起来比较绕。第三彻底去掉数据库唯一索引靠应用层先查再插这种方式并发下存在竞态不推荐。说明逻辑删除不是银弹。选择方案前先问一句这些数据删了之后真的需要恢复吗成绩管理系统里的成绩记录、学生档案通常要求可追溯我的习惯是「物理删除主表数据 操作日志记录删除人和时间」比软删干净得多。别为了一个看起来优雅的功能给自己埋下索引冲突这种雷。6. 部署与验收单端口跑通前后端分离项目再谈成绩导出6.1 用 Tomcat 部署前后端分离项目最省事的一种落位方式前后端分离项目上线最标准的姿势是 Nginx 分开部署前端静态资源和后端接口。但内网小系统还有另一种更省事的方案把前端打包产物放进 SpringBoot 的静态资源目录打成 jar 包后由内嵌 Tomcat 一并承载整套系统只开一个端口。Tomcat 部署前后端分离项目如果不想维护两台服务这种做法最直接。npm run build rm -rf ../backend/src/main/resources/static/* cp -r dist/* ../backend/src/main/resources/static/ cd ../backend mvn clean package -DskipTests java -jar target/score-system.jar说明npm run build产出 dist 目录清空后端 static 再拷贝避免残留旧文件。打包后的 jar 自带全部前端页面和后端接口部署时只需java -jar一行命令。前端路由如果用的 hash 模式这个方案零额外配置如果用的 history 模式内嵌 Tomcat 需要额外配置 fallback 才能处理刷新 404。这也是我在 3.2 节建议用 hash 模式的原因。6.2 验收清单与成绩导出系统跑通后按下面的清单过一遍比只看后端接口通没通靠谱得多。验收项操作预期登录与登出错误密码提示正确密码跳首页无 500提示明确成绩重复录入同学生同课程同考试类型录入两次第二次被拦截并提示分页查询翻页、改每页条数URL 参数变化数据不重复统计图表按课程看班级均分与分数段数据与 SQL 手查一致部署刷新生产地址直接刷新页面不出现 404页面正常成绩导出是成绩管理系统最常见的进阶功能后端用 EasyExcel 写一个导出接口前端下载时注意把 axios 的responseType设为arraybuffer否则导出的文件会是一段乱码。系统里如果涉及上传 PDF 之类的附件全局过滤器要拦住文件名里的非法字符成绩管理系统最容易被攻击的入口就是导入导出哪怕只是内网系统这个过滤器也不能省。我做这类系统早期翻车最多的是分页和日期后来把分页拦截器、Jackson 配置、统一响应体、跨域配置全部固化成了项目模板换项目直接复制精力就都花在业务规则上了。建议你也先把这套骨架固化成自己的模板之后再写管理后台会顺手很多。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

LLM可观测性平台Hindsight:请求级追踪与异常诊断 2026/10/1 6:00:48

LLM可观测性平台Hindsight:请求级追踪与异常诊断

1. 项目概述:Hindsight 不是“事后诸葛亮”,而是一套面向 LLM 应用开发的可观测性基础设施Hindsight 这个名字乍看容易让人联想到“事后回顾”——英文里 hindsight 确实指“事后的洞察力”。但放在当前 LLM 工程实践语境下,它绝不是一句轻飘…

阅读更多 →
高斯贝叶斯图片分类器实战:从原理到批量分类与留一法验证 2026/10/1 6:00:48

高斯贝叶斯图片分类器实战:从原理到批量分类与留一法验证

简介:本资源是面向模式识别课程学习者与机器学习入门者的Python贝叶斯图像分类完整项目,对应课程大作业场景,帮助读者理解贝叶斯分类器在图片分类中的落地方式。项目同时提供控制台与GUI两种交互版本,涵盖图像预处理、特征提取、模…

阅读更多 →
Agent工程三层架构:Harness、Loop与Graph的生产级实践指南 2026/10/1 6:00:41

Agent工程三层架构:Harness、Loop与Graph的生产级实践指南

这话题我琢磨了很久,一直想找机会把 Agent 工程里那些看似玄乎、其实有章可循的东西整理出来。市面上聊 Agent 的文章不少,但大多数要么停在概念层,讲 ReAct、讲工具调用,要么一上来就甩一堆框架代码,让人看完更糊涂。…

阅读更多 →
LSTM图像描述源码实战:从环境搭建到推理生成全流程 2026/10/1 6:00:41

LSTM图像描述源码实战:从环境搭建到推理生成全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
VS Code运行Vue项目指南:环境配置、依赖安装与报错排查 2026/10/1 6:00:34

VS Code运行Vue项目指南:环境配置、依赖安装与报错排查

1. 用VS Code跑Vue项目,卡住你的往往是环境而非代码先讲个真实场景。你从GitHub上拉下来一个Vue项目,或者照着教程敲完了代码,打开VS Code,终端里输入npm run dev,满怀期待等浏览器弹出来——结果要么报错刷屏&#xf…

阅读更多 →
RFdiffusion+ProteinMPNN抗体从头设计实战:参数调优与避坑指南 2026/10/1 6:00:33

RFdiffusion+ProteinMPNN抗体从头设计实战:参数调优与避坑指南

1. 为什么“从头抗体设计”值得单独开一课抗体药物的研发长期依赖动物免疫和噬菌体展示这两条路径,前者周期动辄数月,后者虽然快一些,但库容量和筛选通量始终是瓶颈。更关键的是,这两条路都建立在“自然界已经存在的抗体序列”这个…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉