☰
SpringBoot+Vue学生成绩管理系统前后端分离开发与部署实战
2026/10/1 4:08:37 网站建设 项目流程

简介:基于SpringBoot与Vue技术栈的学生成绩管理系统源码包,专为Java Web学习者、毕业设计及工程实践者准备。项目采用前后端分离架构,后端由SpringBoot实现RESTful API、数据库读写与业务逻辑,前端以Vue完成界面渲染、双向绑定和分页、排序等交互,完整覆盖成绩录入、查询、统计分析、按班级或课程筛选等常见需求。阅读源码可直观理解SpringBoot“约定优于配置”的自动化配置机制、分层开发模式以及Vue组件化复用思路,同时掌握前后端联调、跨域处理与打包部署等关键技能。资源为单个rar压缩包,大小仅1.74MB,便于下载与部署,目前已有4351人浏览学习,认可度较高。包内包含后端Java源码、resources配置目录、Vue前端工程及pom.xml构建文件等完整结构,并附有典型业务模块实现,可直接运行调试或二次扩展,是课程设计、毕业设计的高性价比参考模板。

1. 学生成绩管理系统:从 SpringBoot 到 Vue 的完整落地路径

SpringBoot+Vue学生成绩管理系统是这几年被翻出来做实际交付最多的选题之一,高校毕设、教务辅助、企业内部工具里都能看到同一套打法的影子。它要解决的事情并不复杂:让管理员维护学生和课程,让教师录入并修改成绩,让老师、学生按课程或学期查成绩单;难点不在单个功能,而在于前后端分离之后,接口、数据模型、页面状态和部署要能连成一条线。这篇笔记以一个可复现的成绩管理最小系统为主线,把 MySQL 表设计、SpringBoot 后端接口、Vue 前端页面和 Docker 部署串起来讲。适合正在做毕设、刚接触后端开发,或者准备接手类似内部系统的从业者。

2. 系统设计与技术选型:先把两个端口和三张表定下来

2.1 前后端分离的职责边界:两个端口、三张表

常见做法是把系统拆成三个独立进程:Vue 开发服务器跑在 5173 端口,负责渲染页面和收集用户操作;SpringBoot 后端跑在 8080 端口,只暴露 REST 接口;MySQL 跑在 3306,负责最终落盘。三者之间通过 HTTP + JSON 通信,前端不写 JDBC 连接,后端也不碰 JSP 模板,这就是 springboot vue 前后端分离的核心形态。

数据设计上,我一般不建议把成绩和选课信息塞进一张宽表。以这个系统为例,最经济的三张表就是 student、course、score:student 存学号、姓名、班级;course 存课程名、授课教师、学分;score 做主从关联,存 student_id、course_id、分数、学期。为什么是三张而不是两张?因为成绩一定是某个学生在某门课程下的行为,分数、学期这些字段只属于这一条关联关系。将来要扩展“补考成绩”“平时分占比”,也只需要在 score 表上追加字段,不需要动学生表和课程表。

2.2 技术栈确认:SpringBoot 2.7 + MyBatis-Plus + Vue 3 + Vite

基于这个标题做系统,选型不必追新。SpringBoot 2.7.18 是我用得比较顺手的版本,它还在维护期内,又不会像部分 3.x 一样把 javax 全换成 jakarta,导致一些老依赖报错;配合 JDK 8 或 11,多数开发机开箱即用。这里顺带提一句 springboot 自动装配原理:引入 spring-boot-starter-web 后,DispatcherServlet、内嵌 Tomcat 等组件会通过 xxxxAutoConfiguration 按条件装配,所以开发阶段几乎不用手写组件配置,这也是选 SpringBoot 而不是手工搭 SSM 框架的核心收益。

持久层选 MyBatis-Plus,它保留 MyBatis 手写 SQL 的灵活性,又提供 BaseMapper 的通用增删改查和分页插件,正好覆盖“mybatis 分页插件的用法 springboot”这类高频问题。前端选 Vue 3 + Vite,vue-router 4 做路由,axios 做接口请求,UI 直接用 Element Plus 省掉大量样式工作。Node 建议 16 以上,低于 14 的版本在 Vite 下容易出现依赖安装失败。

下面这份依赖清单可以直接抄进 backend 的 pom.xml:

<!-- backend/pom.xml --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> </dependencies>

逻辑说明:spring-boot-starter-parent 统一管理版本号,所以 mysql 和 jackson 相关依赖可以不写 version 标签;mybatis-plus 3.5.3 与 SpringBoot 2.7 兼容。starter-validation 用来做参数校验,成绩管理里主要是校验 score 不能为空、不能大于 100。

2.3 数据模型与初始化 SQL:字段类型决定前端显示

有了表结构之后,初始化 SQL 建议写进项目的 sql 目录,方便团队新人和 CI 环境一键重建。字段类型这里藏了不少坑:学号尽量用 varchar 而不是 int,避免前导零丢失;成绩字段用 decimal(4,1) 而不是 float,避免浮点显示成 89.9999。下面是这个系统的最小建表语句和样例数据:

-- sql/init.sql CREATE DATABASE IF NOT EXISTS score_system DEFAULT CHARACTER SET utf8mb4; USE score_system; CREATE TABLE student ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT '学号', name VARCHAR(50) NOT NULL, gender TINYINT DEFAULT 0 COMMENT '0-未知 1-男 2-女', class_name VARCHAR(50), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; CREATE TABLE course ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(100) NOT NULL, teacher_name VARCHAR(50), credit DECIMAL(3,1) ) ENGINE=InnoDB; CREATE TABLE score ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, course_id BIGINT NOT NULL, score DECIMAL(4,1) NOT NULL COMMENT '成绩,0-100', semester VARCHAR(20) COMMENT '2024-2025-1', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_student_course (student_id, course_id, semester) ) ENGINE=InnoDB; INSERT INTO student (student_no, name, gender, class_name) VALUES ('2023001', '张明', 1, '计科一班'), ('2023002', '李婷', 2, '计科一班'); INSERT INTO course (course_name, teacher_name, credit) VALUES ('Java程序设计', '王老师', 3.0), ('数据结构', '刘老师', 3.5); INSERT INTO score (student_id, course_id, score, semester) VALUES (1, 1, 92.5, '2024-2025-1'), (1, 2, 88.0, '2024-2025-1'), (2, 1, 76.5, '2024-2025-1');

逻辑说明:student 表用 AUTO_INCREMENT 主键,score 表用联合唯一键保证“同一学生同一课程同一学期”只有一条成绩,这是最容易在重复导入数据时触发 Duplicate Entry 的约束。gender 用 TINYINT 而不是字符串,代码里用枚举映射更省事,页面显示时再转中文。semester 字符串格式固定为四位数年份-四位数年份-学期数,UI 端筛选排序都依赖这个统一格式。

3. 用 SpringBoot 把后端接口搭起来:三层代码一条线

3.1 用 IDEA 初始化 SpringBoot 项目:关键目录和配置文件

在 IDEA 里新建 Spring Boot 项目时,我习惯先用 Spring Initializr 生成骨架,再调整 pom。创建后先打开 application.yml,把数据源和 MyBatis-Plus 配置写好:

# backend/src/main/resources/application.yml server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/score_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: id-type: auto

逻辑说明:前端传过来的时间字段经过 Jackson 默认序列化容易变成一串数字,所以这里必须显式指定 date-format 和 GMT+8,这是一个很典型的“后端数据看着对,前端显示却变成时间戳”的坑。map-underscore-to-camel-case 让 score 表里的 student_id 自动映射到实体类的 studentId,不用在每个字段上加 @TableField。log-impl 配成 StdOutImpl 后,SQL 会在控制台直接打印,排查接口问题时省去大量盲猜时间。

接着需要一个启动类,最省事的写法是直接在 Application 类上加 @MapperScan:

package com.example.score; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan("com.example.score.mapper") public class ScoreApplication { public static void main(String[] args) { SpringApplication.run(ScoreApplication.class, args); } }

逻辑说明:@MapperScan 指定 Mapper 接口所在的包,让 MyBatis-Plus 在启动时扫描并生成代理对象。少写这个注解,后面注入 Mapper 时会直接报 NoSuchBeanDefinitionException,新手通常绕一圈才发现问题出在扫描路径上。

3.2 实体类、通用返回结构和 Mapper:先让前后端约定好 JSON

前后端分离项目里,最怕各写各的返回格式。我习惯定义一个统一的 ApiResponse ,让所有接口都返回 code、message、data 三个字段:

// backend/src/main/java/com/example/score/common/ApiResponse.java package com.example.score.common; import lombok.Data; @Data public class ApiResponse<T> { private Integer code; private String message; private T data; public static <T> ApiResponse<T> ok(T data) { ApiResponse<T> resp = new ApiResponse<>(); resp.setCode(0); resp.setMessage("ok"); resp.setData(data); return resp; } public static <T> ApiResponse<T> error(String message) { ApiResponse<T> resp = new ApiResponse<>(); resp.setCode(500); resp.setMessage(message); return resp; } }

逻辑说明:这个结构是给 axios 拦截器和前端统一处理用的。把字段定为 code、message、data,前端就能在响应拦截器里先看 code,再决定是弹错误提示还是把 data 交给页面。code 用数字而不是字符串,是为了前端 switch 判断方便,也避免把异常消息直接暴露到页面上。

接着看 Score 实体类。数据库表里是下划线字段,Java 对象里用驼峰命名,配合前面配置的 map-underscore-to-camel-case,代码可以保持干净:

// backend/src/main/java/com/example/score/entity/Score.java package com.example.score.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.math.BigDecimal; @Data @TableName("score") public class Score { @TableId(type = IdType.AUTO) private Long id; private Long studentId; private Long courseId; private BigDecimal score; private String semester; }

参数说明:@TableName 指定表名,@TableId(type = IdType.AUTO) 表示数据库主键自增。如果这里写成 IdType.ASSIGN_ID,而数据库又是 AUTO_INCREMENT,就会出现主键重复或雪花 ID 被插入后又被数据库覆盖的奇怪现象,务必和建表语句保持一致。

3.3 服务层和控制器:分页查询和新增成绩的完整写法

核心接口方面,给一套可直接用的写法。列表页用 POST /api/score/page 做分页查询,新增成绩用 POST /api/score,修改用 PUT /api/score/{id},删除用 DELETE /api/score/{id}。下面这段 Controller 里加了按学期筛选条件:

// backend/src/main/java/com/example/score/controller/ScoreController.java package com.example.score.controller; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.example.score.common.ApiResponse; import com.example.score.entity.Score; import com.example.score.mapper.ScoreMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/score") public class ScoreController { @Autowired private ScoreMapper scoreMapper; @PostMapping("/page") public ApiResponse<Page<Score>> page(@RequestParam(defaultValue = "1") long current, @RequestParam(defaultValue = "10") long size, @RequestParam(required = false) String semester) { Page<Score> page = new Page<>(current, size); LambdaQueryWrapper<Score> wrapper = new LambdaQueryWrapper<>(); if (StringUtils.hasText(semester)) { wrapper.eq(Score::getSemester, semester); } wrapper.orderByDesc(Score::getId); return ApiResponse.ok(scoreMapper.selectPage(page, wrapper)); } @PostMapping public ApiResponse<Boolean> save(@RequestBody Score score) { return ApiResponse.ok(scoreMapper.insert(score) > 0); } @PutMapping("/{id}") public ApiResponse<Boolean> update(@PathVariable Long id, @RequestBody Score score) { score.setId(id); return ApiResponse.ok(scoreMapper.updateById(score) > 0); } @DeleteMapping("/{id}") public ApiResponse<Boolean> delete(@PathVariable Long id) { return ApiResponse.ok(scoreMapper.deleteById(id) > 0); } }

逻辑说明:selectPage 是 MyBatis-Plus 分页插件提供的能力,current 和 size 分别代表第几页、每页几条。LambdaQueryWrapper 的 eq 等于 SQL 里的 where 条件;orderByDesc 按主键倒序,保证新增的成绩排在最前。save、update、delete 走 BaseMapper 自带方法,省掉手写 SQL。controller 直接依赖 mapper,对小型成绩系统够用,业务变复杂后再抽 service 层。

需要注意的是,分页插件要在配置类里注册,否则 selectPage 返回的是全表数据,页面数量一多就会明显卡顿:

// backend/src/main/java/com/example/score/config/MybatisPlusConfig.java package com.example.score.config; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

参数说明:PaginationInnerInterceptor 指定数据库类型为 MySQL,MyBatis-Plus 才能生成正确的 limit 语句。不注册这个 Bean 时不会报错,只会静默地查全表再在内存里分页,这是分页功能“看着能用、数据一多就慢”的典型翻车点。

4. 用 Vue 3 把前端页面搭起来:路由、页面、接口对接

4.1 初始化 Vue 项目:Vite 创建与 axios 依赖安装

前端环境初始化,我习惯在项目根目录单独建一个 frontend 目录,不在 IDEA 的工程目录里混着 npm。终端命令如下:

npm create vite@latest frontend -- --template vue cd frontend npm install npm install vue-router@4 axios element-plus npm run dev

参数说明:npm create vite 是 Vite 官方脚手架,--template vue 生成标准 Vue 3 单页应用骨架。vue-router@4 对应 Vue 3,如果装成 vue-router@3 会直接报兼容错误,这是入门阶段最容易遇到的环境坑。element-plus 是 UI 组件库,开发调试阶段在 main.js 里全量引入即可,项目变大了再换按需加载。

Vite 开发服务器默认跑在 5173,后端跑在 8080,端口不一致会触发跨域。Vite 支持在 server 配置里做 proxy 转发,把 /api 开头的请求转到后端:

// frontend/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, } } } })

逻辑说明:proxy 配置是开发阶段的跨域解决方案。前端发到 /api/score/page 的请求,Vite 会改写成 http://localhost:8080/api/score/page,浏览器看到的还是同源请求,也就不会触发 CORS 拦截。生产环境不要依赖这个 proxy,要么用 Nginx 转发,要么把打包后的静态文件交给后端托管。

4.2 成绩列表页:el-table、筛选条件和分页组件

成绩列表页是主界面。下面这个 Vue 组件只保留三条核心逻辑:页面加载时请求第一页数据,点查询时带上学期参数,点分页时重新拉数据。

<!-- frontend/src/views/ScoreList.vue --> <script setup> import { ref, onMounted } from 'vue' import axios from 'axios' const tableData = ref([]) const total = ref(0) const semester = ref('') const current = ref(1) const size = ref(10) const fetchData = async () => { const { data } = await axios.post('/api/score/page', null, { params: { current: current.value, size: size.value, semester: semester.value } }) tableData.value = data.data.records total.value = data.data.total } onMounted(fetchData) </script> <template> <div> <el-input v-model="semester" placeholder="按学期筛选" style="width: 200px" /> <el-button type="primary" @click="current = 1; fetchData()">查询</el-button> <el-table :data="tableData" border> <el-table-column prop="id" label="ID" width="80" /> <el-table-column prop="studentId" label="学生ID" width="100" /> <el-table-column prop="courseId" label="课程ID" width="100" /> <el-table-column prop="score" label="成绩" width="120" /> <el-table-column prop="semester" label="学期" /> </el-table> <el-pagination v-model:current-page="current" :page-size="size" :total="total" layout="total, prev, pager, next" @current-change="fetchData" /> </div> </template>

逻辑说明:fetchData 把 axios 响应解构成 { data },第二个 data 是后端返回的 ApiResponse 里的 data,通过 data.data.records 取分页记录、data.data.total 取总数。查询按钮将当前页重置为 1,避免已经在第 5 页时带着大页码查询。el-pagination 的 current-change 事件在页码变化时触发 fetchData,实现分页刷新。

这段代码没有写样式,Element Plus 自带一套表格和按钮样式。在 main.js 里全量引入 Element Plus 后,el-table、el-input 这些标签不需要在组件里单独 import。后续做学生管理页、课程管理页时,结构基本一致,只是表格列和按钮文案不同。

4.3 路由与菜单:vue-router 的懒加载配置

页面写好后,需要在路由里注册。两个要点:一是用 createWebHistory,URL 不带 #,更接近真实站点;二是用懒加载语法 import(),页面多时不至于一个 js 包几十兆。路由配置如下:

// frontend/src/router/index.js import { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ history: createWebHistory(), routes: [ { path: '/', redirect: '/score' }, { path: '/score', name: 'ScoreList', component: () => import('../views/ScoreList.vue'), meta: { title: '成绩管理' } }, { path: '/student', name: 'StudentList', component: () => import('../views/StudentList.vue'), meta: { title: '学生管理' } } ] }) export default router

逻辑说明:component 的箭头函数会在路由被访问时才加载对应文件,浏览器首次打开只下载首屏页面,后续切到学生管理页才去拉 StudentList.vue 的 chunk。meta.title 可以配合路由守卫动态修改浏览器标签标题。这里将成绩管理作为默认路由,用户打开首页后直接重定向到 /score,避免进系统先白屏。开发时如果路由切换报 “Failed to fetch dynamically imported module”,大概率是 Vite 服务启动顺序问题,重启 npm run dev 就好。

5. 学生成绩管理系统的避坑清单:5 个能让你翻车的边界问题

5.1 现象:前端能打开页面,登录后一刷新就掉线

开发环境里,Vue 跑在 5173,后端跑在 8080,前端发请求时属于跨域。很多人已经给 Controller 加了 @CrossOrigin 注解,却发现带 Cookie 的 Session 请求反复失效。原因是 @CrossOrigin 默认不带 allowCredentials,或者没有把 allowedOrigins 写成精确的前端地址,导致浏览器虽然能拿到响应,却不会保存 Set-Cookie。解决方式是使用统一 CORS 配置:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("http://localhost:5173") .allowCredentials(true) .allowedMethods("GET", "POST", "PUT", "DELETE"); } }

注意,allowedOrigins 不能用 *,还要把 allowCredentials 设为 true。如果登录校验走的是 JWT、不依赖 Cookie,那这层配置对登录态的影响不大,但前端必须把 token 放在请求头里,两种方案不能混着用。

5.2 现象:POST 新增成绩返回 500,控制台报 Bad SQL Grammar

常见报错是 Unknown column 'student_id' in 'field list',但数据库明明建了 student_id 字段。原因多半是实体类属性名写成了 studentId,MyBatis-Plus 默认不会把驼峰自动转成下划线。我已经在 application.yml 里开了 map-underscore-to-camel-case: true,按理说不会出问题,但如果 Mapper XML 里手写了 SQL,手写 SQL 不会自动做驼峰转换,需要在 select 片段里显式写别名,或者在 SQL 里直接使用下划线字段名。

5.3 现象:后端返回的时间字段渲染成 1690000000000

前端表格里 create_time 列显示成一串数字,这是 LocalDateTime 的 JSON 序列化没有按格式输出导致的。只配置 spring.jackson.date-format 对 LocalDateTime 不生效,必须配合时间格式注解。最简单的处理方式是在字段上直接标注:

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime createTime;

SpringBoot 2.7 默认已经注册了 JavaTimeModule,所以加 @JsonFormat 就能覆盖全局配置。这个问题排查起来很玄学,因为后端控制台打印的实体对象是带字母 T 的 ISO 格式,前端拿到手却变成时间戳,根因就是 LocalDateTime 和 java.util.Date 走了两套序列化规则。

5.4 现象:更新成绩接口返回“成功”,数据库里数值没变

原因常见有两类。第一类是前端把 studentId 和 courseId 传成了字符串,后端用 @RequestBody Score 接收时类型转换失败,但异常被统一捕获吞掉,只返回 ok。第二类是 MyBatis-Plus 的 updateById 默认不会把 null 字段更新到数据库,如果前端故意清空某个字段,update 语句会直接跳过该列。解决方法是先做参数校验,把必填字段用 @NotNull 标出来,再在更新时用 UpdateWrapper 手动 set 需要清空的字段。

5.5 现象:Vite proxy 已经配好,接口还是 404

前后端路径对不上,最常见的是前端请求带了 context path,后端 @RequestMapping 又只写了 /api。比如前端调 /api/score/page,proxy 转给后端后会原样变成 /api/score/page,如果后端 controller 路径是 /api,但某个接口实际只写在 /score 下,就会 404。原则是:Vite 的 proxy 只做地址级转发,不改路径前缀,后端接口路径统一以 /api 开头,前端请求里也写死 /api。调试时先在后端控制台确认请求有没有进来,再谈路径匹配,这一步能过滤掉一半的 404。

6. 用 Docker Compose 跑一套最小生产环境:最后的验证手段

6.1 最小 Compose 配置

本地开发跑通不算完,把 SpringBoot 的 jar 和 Vue 的打包产物放到容器环境里,才能验证部署脚本是不是真的可靠。常见做法是把后端打进一个 openjdk 镜像,前端用 node 镜像构建后,再用 nginx 镜像托管静态资源。以下是一个可直接复用的最小编排文件:

version: '3.8' services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: score_system ports: - "3306:3306" backend: build: ./backend depends_on: - mysql environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/score_system?characterEncoding=utf8 ports: - "8080:8080" frontend: build: ./frontend depends_on: - backend ports: - "80:80"

逻辑说明:backend 容器里的数据库地址要写服务名 mysql,不能写 localhost;frontend 容器里没有 Java 环境,Vue 打包后都是静态文件,由 nginx 在 80 端口提供服务。前端 nginx 还应该加一条 location /api 的转发规则,把接口请求转给 backend 服务,否则页面能打开,但列表数据永远是空的。第一次启动顺序需要 mysql 先就绪,backend 再去连接。

6.2 上线自测清单:一条命令跑起来之后该怎么验证

启动后用 curl 验证接口是成本最低、最不容易漏的方式:

curl -X POST 'http://localhost:8080/api/score/page?current=1&size=10'

这条请求返回的 JSON 里,data.total 应该等于数据库里 score 表的行数。能查出来再打开浏览器访问 http://localhost,看到成绩列表页有数据,就说明前端、后端、数据库整条链路是通的。如果 curl 通、前端不通,问题基本都在 nginx 转发和打包路径上,而不是后端。

我做这类系统收尾时有个习惯:每次改完接口,先跑一遍 curl 再上页面,绝不用“浏览器看着没问题”代替接口验证。页面上的展示和操作会掩盖一些后端 bug,比如表格不报错只是因为接口数据刚好带过了。这条习惯帮我避开了不少交付现场的翻车,希望这些步骤经验也能帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询