拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot+Vue3前后端分离教学资源库系统开发实战

SpringBoot+Vue3前后端分离教学资源库系统开发实战

最近把一套教学资源库系统的源码重新翻了出来,打算把这次从零搭建的全过程整理成文字。整套系统采用前后端分离架构,后端用 Java SpringBoot 配合 MyBatis 做持久层,前端用 Vue3 写单页应用,数据库选用 MySQL,三个部分各司其职,比较适合作为课程设计、毕业设计或者公司内部资料库的底子。

为什么值得聊聊?因为这类系统看起来是“增删改查”,真正做起来要处理的细节非常多:文件上传、分类检索、权限控制、跨域联调、部署上线,每一条都能单独拎出来写一篇。如果你想学 SpringBoot 和 Vue3 的整合,或者准备面试时讲一个拿得出手的项目,这篇内容应该能给你一个比较完整的参考。我会从项目整体设计讲起,接着是后端实现、前端工程化、数据库与部署,最后把开发中遇到的问题和排查思路整理成一份速查表,内容按我真实的开发顺序来,不是教科书式讲解。

1. 项目整体设计与模块拆解

1.1 为什么选前后端分离架构

资源库系统最典型的特点就是页面交互重、资源形态多。用户要浏览资源列表、切换分类、上传大文件、在线预览,还要处理收藏、评价、后台审核这些操作。如果继续用传统的服务端渲染模板,所有逻辑都堆在 Controller 里,前端稍微改交互,后端就得跟着改页面,两边耦合严重,后期维护成本很高。

前后端分离意味着前端 Vue3 负责路由、渲染、交互,后端 SpringBoot 只提供 JSON 接口。这样有一个很直接的好处:后端可以完全脱离浏览器进行测试,Postman 直接打接口就行;前端也可以在 Mock 数据下先把页面画完,等接口好了再对接。团队协作时,前端和后端只需要约定好接口文档,互不阻塞。

这个选择还带了一个隐藏价值:接口化后,以后如果要做 App、小程序,或者给第三方开放查询下载接口,后端几乎不用改,直接复用现有 API。我做这套系统时曾经被问到“以后怎么扩展”,答案就是 API 化之后,接入层只是一个壳的事。这也是我在面试里讲这个项目时的加分项之一。

当然,前后端分离不是银弹,跨域、Token 鉴权、接口联调的成本会明显增加。项目准备阶段一定要把统一返回结构、错误码规范、命名约定定好,不然联调阶段前端抱怨“字段名对不上”,后端抱怨“参数传不进来”的场面几乎每天都会发生。我用过一个笨但有效的办法:后端先把所有接口的返回 JSON 样例写进接口文档,前端照着样例做类型定义,这样字段名偏差能提前暴露。

1.2 核心模块与四张核心表设计

功能模块上,我拆分成用户模块、资源模块、互动模块和管理模块四块。用户模块负责注册登录和角色区分,资源模块负责上传、分类检索与下载,互动模块负责收藏与评价,管理模块负责资源审核、分类管理和用户状态管理。四个模块之间通过一张资源主表串联,职责边界比较清晰。

说到表结构,我没有设计太多花哨的表,核心就这几张:

  • sys_user:用户基本信息,包括用户名、密码密文、角色、禁用状态。
  • resource_category:资源分类,树形结构,通过 parent_id 表示层级。
  • resource_info:资源主表,存标题、摘要、文件类型、存储地址、下载量、审核状态。
  • user_favorite:用户收藏表,记录用户和资源的收藏关系。

sys_user的密码字段我使用 BCrypt 加密后存储,这个没得商量。很多初学者把密码明文直接入库,一旦数据库泄露,用户在其他平台的高危密码也会被尝试撞库。BCrypt 自带盐值,每次加密结果不同,即使数据库丢了,破解成本也高得多。

resource_info是我花时间最多的一张表。除了常规的标题、分类、上传人、文件地址外,我特意加了file_type和status两个字段。file_type是为了后续按类型渲染不同的预览组件,图片走图片预览,视频走播放器,PDF 走嵌入式预览。status则是审核状态,资源上传后先进入待审核,后台管理员审核通过才公开,避免上传完就直接暴露到前端列表里。

有个设计细节容易被忽略:外键。我在这套系统里没有建立任何物理外键约束。开发时用 JOIN 查询完全够用,物理外键会在插入、删除时增加额外的锁开销,而且资源表、收藏表的删除顺序经常要变动,物理外键一旦建错,改起来非常痛苦。我的做法是:字段名约定user_id、category_id,查询时通过索引关联,逻辑外键关系靠代码保证。

2. 后端核心实现:SpringBoot + MyBatis 的落地细节

2.1 项目结构、统一响应结构

后端我用的是 SpringBoot 2.7 + MyBatis 3.x 的组合。为什么不直接用 MyBatis-Plus?因为这套系统的查询条件比较杂,很多地方需要手写动态 SQL,原生 MyBatis 的动态 SQL 能力足够强,而且面试时 MyBatis-Plus 的自动化操作容易让候选人忽略对 SQL 的理解。

项目分包我坚持四个方面:

  • controller:接收参数、调用 service、返回结果,不写业务逻辑。
  • service:业务处理,包括事务控制、状态流转。
  • mapper:数据访问接口,对应 XML 文件。
  • common:统一返回结构、异常处理、常量、工具类。

controller层只做参数校验和响应封装,我想强调这一点。很多人喜欢在 controller 里直接操作多个 service 或 mapper,短期看省代码,后期一旦要加缓存、加幂等、加审计日志,就会改得很别扭。把业务放在 service 层,事务边界也容易控制。

统一返回结构我用的是Result<T>。代码很简单:

@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }

为什么前端喜欢这种统一体?因为 axios 拦截器里只需要判断code === 200,就可以直接取到业务数据,异常信息统一弹一个提示即可。如果后端每个接口返回结构不一致,前端每个页面都要写不同的错误分支,那是灾难。接口开发前第一件事,就是先把 Result 定义好,前后端同时遵守。

我有一次就是栽在这个地方:某个管理接口直接返回了Map<String, Object>,前端取data字段时发现结构完全不一样,排查了半天才发现这个接口没有走通用返回封装,而是返回了一个字典。后来我增加了一条规范:所有接口返回类型必须统一使用Result<T>,不允许直接返回对象或集合。

2.2 MyBatis XML 动态 SQL 与自定义 TypeHandler

MyBatis 的核心能力在 XML 映射文件里体现得最明显。拿资源列表的查询来说,教学资源库的查询条件一般包括:标题模糊搜索、分类筛选、类型筛选、审核状态、时间范围,这些条件是可选的。用注解写 SQL 会拼得很难看,XML 里的<where>标签配上<if>可以很优雅地解决:

<select id="selectResourcePage" resultType="com.example.entity.ResourceInfo"> SELECT * FROM resource_info <where> <if test="title != null and title != ''"> AND title LIKE CONCAT('%', #{title}, '%') </if> <if test="categoryId != null"> AND category_id = #{categoryId} </if> <if test="fileType != null"> AND file_type = #{fileType} </if> <if test="status != null"> AND status = #{status} </if> <if test="startTime != null"> AND create_time &gt;= #{startTime} </if> <if test="endTime != null"> AND create_time &lt;= #{endTime} </if> </where> ORDER BY create_time DESC </select>

<where>标签会自动去掉多余的 AND 或 OR,这个特性极其好用,我刚开始手写 SQL 时经常因为第一个条件不满足导致残留 AND,用了<where>后这类问题基本消失。需要提醒的是,<if>的判断要写完整,空字符串和 null 都要处理,否则用户只输入空格时会出现误过滤。

再讲讲 TypeHandler。这是 MyBatis 面试中经常被问到的点,也正好和我的实际需求对上。我在资源表里存了一个资源标签字段tags,数据库用JSON类型存储,Java 实体里对应的是List<String>。默认情况下 MyBatis 不知道如何把List<String>写进 JSON 字段,也不能把 JSON 自动读成列表,这时就需要自定义 TypeHandler。

TypeHandler 的工作流程其实很清晰:

  1. MyBatis 解析 mapper 时,根据配置或注解注册 TypeHandler。
  2. 执行 SQL 前,ParameterHandler会调用 TypeHandler 的setParameter,把 Java 对象转为 JDBC 对应的类型。
  3. 执行 SQL 后,ResultSetHandler拿到数据库字段,调用 TypeHandler 的getResult,把 JDBC 类型转为 Java 对象。
  4. 整个过程对业务代码透明,你只需要实现BaseTypeHandler<T>接口。

要实现 JSON 与 List 互转,就两步:

@MappedTypes(List.class) public class ListTypeHandler extends BaseTypeHandler<List<String>> { private final ObjectMapper objectMapper = new ObjectMapper(); @Override public void setNonNullParameter(PreparedStatement ps, int i, List<String> parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, toJson(parameter)); } @Override public List<String> getNullableResult(ResultSet rs, String columnName) throws SQLException { return toList(rs.getString(columnName)); } @Override public List<String> getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return toList(rs.getString(columnIndex)); } @Override public List<String> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return toList(cs.getString(columnIndex)); } private String toJson(List<String> list) { try { return objectMapper.writeValueAsString(list); } catch (JsonProcessingException e) { throw new RuntimeException(e); } } private List<String> toList(String json) { if (json == null || json.isEmpty()) { return Collections.emptyList(); } try { return objectMapper.readValue(json, new TypeReference<List<String>>() {}); } catch (JsonProcessingException e) { throw new RuntimeException(e); } } }

使用方式是在 XML 中指定typeHandler:

<insert id="insertResource"> INSERT INTO resource_info(title, tags) VALUES (#{title}, #{tags, typeHandler=com.example.handler.ListTypeHandler}) </insert>

这个方式让资源存储显得非常灵活。需要注意的是,TypeHandler 必须针对每个使用场景手动指定,否则 MyBatis 不会自动识别。如果你用的是 MyBatis-Plus,会有更简单的类型处理方案,但原生 MyBatis 里这套自定义逻辑是必须掌握的。

2.3 缓存机制与本次的取舍

热词里很多人搜 MyBatis 缓存,我在这个项目里也专门研究过。MyBatis 的缓存分两级:一级缓存是SqlSession级别,默认开启;二级缓存是namespace级别,需要配置开启。一级缓存最大的坑在于,Spring 容器管理的 Mapper 每次通过SqlSessionTemplate获得的 SqlSession 可能是不同的代理对象,在没有事务时,一次 Mapper 方法调用就创建并关闭一个 SqlSession,一级缓存的值实际上被限制了。很多人以为缓存生效,但查出来的数据每次都在刷新,就是因为没理解这一点。

二级缓存我用了一段实践后选择了关闭。原因很简单:教学资源库的资源列表数据变化频繁,用户上传、后台审核、删除资源都会影响列表。开启二级缓存后,一旦某条数据变更,所有该 namespace 下的查询都可能拿到脏数据,而且刷新时机不好控制。为了性能去承担数据一致性的风险,在资源库这种场景下得不偿失。

如果一定要做缓存,我更推荐用 Spring Cache 配合 Caffeine 来缓存分类列表、数据字典这类几乎不变的数据。把缓存从持久层抽离到业务层,缓存粒度、失效策略都更可控。这样,MyBatis 保持直查数据库,业务层对热点数据做短时缓存,既安全又高效。

我在项目中实际做的一个小优化是:资源的分类列表和导航菜单数据基本不变,我在 service 层用 Caffeine 缓存了十分钟。效果很明显,首页加载从几十毫秒降到个位数毫秒,而且不会出现脏数据问题。如果你在面试中讲到缓存,能把这个取舍讲清楚,比背一堆缓存概念强很多。

3. 前端 Vue3 工程化与页面实现

3.1 Vite + 组合式 API 搭建项目

前端部分我用了 Vue3 + Vite + Element Plus + Pinia + Axios 这套组合。构建工具选择 Vite 而不是 Vue CLI,主要原因是 Vite 基于 ES Module 的冷启动速度确实快,项目大一点之后优势尤其明显。用 Vite 创建项目很简单:

npm create vite@latest resource-web -- --template vue

创建完把依赖装上,vue-router、pinia、axios、element-plus、sass一个都不能少。注意,如果要在 Vue3 项目里用 scss,需要先安装sass这个依赖,并且在 Vite 配置里做全局变量注入,否则每个组件里想用$primary-color这种变量都要重复@import。

// vite.config.ts export default defineConfig({ css: { preprocessorOptions: { scss: { additionalData: `@use "@/styles/variables.scss" as *;` } } } })

这个配置是我实测过的最省心的方案,项目里所有组件都能直接用 scss 变量,不用再手动 import,写样式时舒服很多。

Vue3 组件的写法我统一用的<script setup>组合式 API。以资源列表页为例,把所有状态和逻辑集中在 setup 里组织,搜索条件、列表数据、加载状态、分页参数放在一起,watch监听搜索条件变化,条件一变就自动重新请求后端接口。相比 Vue2 的 options api,代码的阅读顺序就是数据变化的顺序,维护起来很清楚。

这里要提一个体验上的点:资源列表页的搜索表单我用reactive定义,但因为 Element Plus 表单校验要求表单对象不能直接用普通对象解构,否则响应式会丢失,这也是很多新手常踩的坑。在提交表单时,如果校验状态一直没有变化,先检查是不是把 reactive 对象整个解构了。

3.2 接口封装、JWT 鉴权与路由守卫

前端调用后端接口,不可能每个页面都直接写 axios.get,容易导致代码重复而且维护困难。我在src/api目录下面按模块建文件,比如auth.js、resource.js,每个文件导出对应的请求函数。创建 axios 实例时统一设置baseURL和超时时间,然后加请求拦截器和响应拦截器。

请求拦截器的核心工作是带 token。登录成功后,后端签发 JWT,前端把 token 存在localStorage里,每次请求都从里面取出来放到Authorization请求头:

http.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config })

响应拦截器里做两件事:第一,判断后端返回的code是否为 200,是就返回data,否则统一弹 ElMessage 提示;第二,遇到 HTTP 401 时清除本地 token 并跳回登录页。这样全局处理之后,业务页面里只需要处理成功数据,错误提示不需要每个接口都写。

路由守卫则需要配合角色做动态控制。我在用户登录后从后端接口拿到角色信息,存在 Pinia 里。全局前置守卫beforeEach中判断:没有 token 则强制跳转到登录页;有 token 但当前要访问管理员页面且角色不是管理员,则直接提示无权限。动态路由的做法是在守卫里根据角色添加对应路由,而不是把所有路由一次性注册。

表单校验也值得一提,比如发布资源时,如果用户填写了“资源有效期”,开始时间就不能晚于结束时间。Element Plus 的 Form rules 里可以写自定义校验函数:

const rules = { dateRange: [ { validator: (_rule, value, callback) => { if (value && value[0] > value[1]) { callback(new Error('开始日期不能晚于结束日期')) } else { callback() } }, trigger: 'change' } ] }

这种自定义校验在 Vue3 的表单场景下非常常用,写起来也很直观。我建议每个参考这个项目的人都把 Element 的校验规则完整过一遍,面试时聊这个能体现出细节思考。

3.3 文件上传、预览与 MinIO 集成

资源库系统的核心是资源文件,文件上传必然绕不开。前端我用的 Element Plus 的el-upload,但没有用默认的 action 方式,而是通过http-request自定义上传逻辑,因为默认的 action 会把文件直接 POST 到某个 URL,无法和后端的统一 Token 机制完美配合。自定义上传函数里可以手动加入Authorization头,也可以在做大文件分片时控制发送逻辑。

上传文件的时机我建议放在表单提交时统一处理,而不是用户选择文件后立刻上传。用户可能选错了文件、又取消了表单提交,提前上传会产生大量孤儿文件,白白消耗存储空间。实际上,我在这里踩过一个坑:之前用户一选完文件就传到服务器,结果有人上传了几个 G 的视频又取消了发布,服务器磁盘被一堆没有关联资源的文件占满。

文件存储方面,如果只做本地磁盘存储,简单但扩展性差。在团队内部使用场景下,我建议直接集成 MinIO,它是一个兼容 S3 协议的对象存储系统,Docker 一行命令就能拉起服务。SpringBoot 集成 MinIO 的要点是:引入minio依赖,配置 endpoint、accessKey、secretKey、bucket 名称,然后封装一个上传工具类。上传完成后拿到文件访问地址,存到resource_info表的file_url字段。

MinIO 的访问地址要注意一点:如果图片或视频需要直接展示在网页上,bucket 的访问权限需要设置成可读。在内网环境可以直接公开读,生产环境建议通过后端做鉴权后再重定向到文件地址,避免资源外泄。

预览方面,PDF 和图片可以直接用浏览器展示,视频则用 video 标签播放。至于 Word、PPT 这类办公文档,线上预览是最麻烦的,我实际做的时候用了一个取巧方案:后端加了一个转换接口,用 LibreOffice 把文档转成 PDF,然后前端展示 PDF。这个方案对服务器资源有一定要求,如果不是硬性需求,我建议在最初的产品设计里就限制可上传的文件类型,能省掉大量精力。

4. 数据库设计与部署调优

4.1 索引设计、排序与分页优化

教学资源库的查询场景,集中在资源列表页。用户进入页面默认看到最新的资源,可以选择按分类筛选,也可以按下载量排序,后台还会按状态筛选。这些操作都落在resource_info表上,表的索引设计直接决定接口性能。

我最常用的查询条件是status、category_id、create_time。所以建了一个联合索引:

ALTER TABLE resource_info ADD INDEX idx_status_category_time (status, category_id, create_time);

查询时先过滤 status,再过滤 category,最后按时间排序。注意索引的最左前缀原则,如果只查分类而不带状态,这个联合索引就用不上。所以实际开发时,我还会根据查询模式的统计来调整索引顺序,不要只知道建索引,不知道索引为什么命中不了。

模糊搜索是一个常见陷阱。WHERE title LIKE '%关键词%'因为通配符在字符串前面,MySQL 无法走索引,数据量一上去就是全表扫描。对于教学资源库这种规模,我选择保持现状,因为资源量通常不会达到百万级;如果真要优化,可以改成前缀匹配,或者引入全文索引。面试时别背“加索引就行”,能说出这个区别,会更专业。

分页问题是另一个高频坑。LIMIT 100000, 20意味着 MySQL 要先扫描前 10 万行再丢弃,越到后面越慢。我在资源列表的接口里,对深翻页做了处理,改用游标分页:传入最后一条记录的 ID,用WHERE id < #{lastId} ORDER BY id DESC LIMIT 20这种方式代替页码。前端只需要维护一个“加载更多”按钮,不需要跳页,体验也自然。

排序字段上,按下载量排序要用download_count DESC,但要注意下载量更新频繁,这个字段的索引维护成本偏高。我实际没有给它加索引,因为教学资源库的下载量排序很少被访问,等数据大到排序慢的时候再做异步统计也不迟。优化要针对真实访问路径,不要提前给所有字段都加索引。

4.2 MySQL 连接配置与前后端一体化部署

很多初学者在使用这套系统时,第一步就卡在 MySQL 安装配置上。我不打算重复完整的安装教程,只提醒和项目运行相关的三个点:字符集、时区、账号权限。数据库建库时默认用utf8mb4,避免中文和 emoji 乱码;连接串要显式设置时区;root 账号如果没有特殊需求,建议新建一个专用账号并只授权当前库,防止误操作殃及整个实例。

开发时,SpringBoot 连接 MySQL 的配置最容易踩两个坑:时区和 SSL。MySQL 8 默认启用了 SSL,但本地开发时自签名证书往往会导致连接报错,所以连接串里要加上useSSL=false。时区问题体现为时间字段比北京时间差 8 小时,原因是驱动和数据库时区不一致,连接串里带上serverTimezone=Asia/Shanghai就会正常。

我的application.yml关键配置长这样:

spring: datasource: url: jdbc:mysql://localhost:3306/resource_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true username: root password: 123456 servlet: multipart: max-file-size: 200MB max-request-size: 500MB

allowMultiQueries=true允许一条语句带多个分号,批量插入和某些复杂 SQL 需要它,但生产环境如果不需要,可以不加,多一份安全。

部署方案,很多小项目只有一台服务器,我给出一套最简单实用的:前端执行npm run build,生成dist目录,把目录里的文件整体复制到 SpringBoot 的src/main/resources/static下,然后重新打包 Jar。启动后,静态资源由 SpringBoot 托管,API 也由同一个端口提供,不用单独安装 Nginx。

这种打包方式很适合我这种“一个 Jar 跑一个项目”的场景。优点是部署简单,缺点是前端每次更新都要重新打包后端,而且静态资源和 API 混在一起,并发能力有限。如果访问量上来,再把dist放到 Nginx,并用 Nginx 反向代理/api到后端端口,前端不用改任何代码。

要注意的是,Vue3 默认路由是 history 模式,SpringBoot 需要把非 API 的路径都转发到index.html,否则刷新当前路由时会 404。配置也很简单:

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/{spring:[^\\.]*}") .setViewName("forward:/index.html"); } }

这个细节如果漏了,打包部署后用户从首页点进详情再刷新,立刻就会遇到白屏。

5. 常见问题排查与面试复盘

5.1 高频启动与运行问题速查

下面这个表格是我在开发和联调阶段反复遇到的典型问题,整理出来供参考。

现象可能的根因解决方案
项目启动报NoSuchMethodErrorMaven 依赖版本冲突,SpringBoot 版本过高用mvn dependency:tree查冲突,统一版本;检查 Java 版本是否支持该 SpringBoot 版本
接口提示跨域前后端端口不同且后端未配置 CORS配置全局 CorsFilter;如果用了 Spring Security,需在安全配置中放行预检请求
登录后请求仍返回 401JWT 解析失败或拦截器排除了部分路径检查 token 传递方式、拦截器 exclude 列表
数据库连接超时MySQL 连接池耗尽或错误使用 SSL调整连接池参数,连接串加useSSL=false
时间字段相差 8 小时数据库时区和 JDBC 时区不一致连接串加serverTimezone=Asia/Shanghai
上传大文件报错后端 multipart 限制太小调整max-file-size和max-request-size
Vue 打包后刷新 404history 路由模式未配置转发后端配置/index.htmlforward 规则

所有这些我都实际遇到过。比如 SpringBoot 版本太高的问题,当时用的是 3.1 的预览版,底层 Java 版本要求 17,而项目还跑在 JDK 8 上,启动时报的错特别隐晦。后来我老老实实回到 SpringBoot 2.7 + JDK 8 的组合,稳如老狗。技术选型时不要一味追求最新,先确认环境兼容性。

跨域问题也很容易让人头疼。开发环境我用 Vite 的 proxy 代理/api到后端,基本绕开了跨域。但如果直接跨域调用后端接口,就一定要在后端配 CORS,而且要注意如果还引入了 spring security,CORS 配置必须在安全过滤器链中生效,否则会被安全拦截器拦截。

5.2 Java 后端和数据库面试常见问题复盘

做项目不只是为了跑通功能,更要学会把它讲清楚,尤其是面试场景。这套教学资源库系统的源码在面试里,我会按下面这些角度准备:

第一,MyBatis 相关。面试官问“MyBatis 中 TypeHandler 的工作流程”时,我会结合自己写的 JSON TypeHandler 讲:参数处理时如何 setParameter,结果集处理时如何 getResult,以及注册方式。问缓存时,讲清楚一级缓存和二级缓存的作用域、Spring 环境下的实际表现,以及为什么我选择了关闭二级缓存,用 Caffeine 做业务层缓存。

第二,SpringBoot 相关。比如“SpringBoot 自动配置原理”“事务失效的场景”。事务失效在资源上传场景特别有讨论价值:文件上传已建立事务,但更新资源记录时,同一个类内部调用事务方法,会导致事务失效,因为 Spring 事务基于代理,this 调用不会经过代理对象。我在代码里就专门注意了这个坑。

第三,MySQL 相关。比较常问的有“索引失效的场景”“深分页怎么优化”“怎么保证数据一致性”。数据一致性这里我提一下资源状态流转:用户上传资源后,文件先写到 MinIO,成功后再写数据库并提交事务。万一数据库写入失败,我会启动一个定时任务,定期扫描未在数据库中登记的文件并清理,保证文件和数据库最终一致。这种“缓存队列+补偿”的思路比单纯的事务更贴合对象存储场景。

第四,前端相关。Vue3 的响应式原理、组合式 API 相比选项式的优势、路由守卫权限控制。我会用权限控制和动态路由的例子来佐证,而不是空讲概念。

我遇到过不少候选人,项目经历写得花团锦簇,但一细问就露怯。真正有效的方法是,把项目里每个“坑”都当成面试题来准备,向面试官展示你是怎么发现问题和解决问题的。这套资源库系统虽然不是高并发项目,但它涵盖了从表设计到部署的完整链路,每一个环节都能延展出不少问题。

最后再分享一个小技巧:在本地开发时,我会把 Vite 的 proxy target 指向http://localhost:8080,并设置changeOrigin: true。这样前端代码里所有的请求路径都只写/api/xxx,切换部署环境时只需改动一个 proxy 配置,不用全局搜索替换接口地址。这个小习惯让我在联调和部署阶段省下了大量时间,也推荐你建立一个与自己开发习惯匹配的固定接口前缀规范。

返回列表