ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue3智能学习平台全栈实战:从数据库设计到部署上线

SpringBoot+Vue3智能学习平台全栈实战:从数据库设计到部署上线 写这篇博文之前我先交代一下背景前前后后折腾了两周把一个从零开始搭的“智能学习平台”系统做到了能跑、能看、能用的状态。技术栈就是标题里那套——Java SpringBoot Vue3 MyBatis MySQL前后端完全分离源码级复现。这篇文章不打算讲空泛的概念直接把我踩过的坑、设计时候的取舍、关键代码长什么样全部摊开写清楚。不管你是刚学完 SpringBoot 想找完整项目的在校生还是工作中需要快速搭一套管理后台的 Java 工程师这篇文章都能给你省下不少查资料的功夫。1. 项目全貌与技术选型思路1.1 智能学习平台到底在做什么先把这个项目说清楚。它不是一个简单的“在线课程播放器”而是一个带学习行为跟踪的学习管理平台。核心用户是学生和老师两类角色学生登录后可以浏览课程、报名学习、记录学习进度、完成课后练习老师可以发布课程、上传章节内容、查看学生的学习情况。管理员则负责用户管理、课程审核和数据统计。说白了这套系统要解决三件事把课程内容组织起来、把学习过程记录下来、把学习效果反馈出来。其中“学习过程记录”是这个项目的灵魂也是和市面上大部分 CRUD 管理后台拉开差距的地方——学习时长、学习进度百分比、章节完成状态、练习得分这些数据都需要有专门的数据模型去承接。我选择从零搭建而不是直接用开源脚手架是因为这个项目后续要扩展“智能推荐”功能也就是根据学生的学习记录和历史行为推荐合适的课程。如果一开始表结构设计得不够灵活后面加推荐模块会非常痛苦。1.2 技术栈选型的几条硬道理这套技术栈不是我随便拍的每一项都有明确理由SpringBootJava 后端的事实标准。内嵌 Tomcat、自动配置、Starter 机制让项目搭建成本大幅降低不用像 SSM 时代那样写一堆 XML 配置文件。版本我用的 2.7.x这个版本踩坑最少、网上资料最全。Vue3前端用了组合式 APIComposition API配合script setup语法代码比 Vue2 的 Options API 清爽太多了。特别是课程列表、学习进度这种需要大量响应式数据交互的场景ref和reactive用起来非常顺手。MyBatis很多新项目喜欢用 MyBatis-Plus但我这里用了原生 MyBatis。原因有两个一是 SQL 可控性最强学习记录、进度统计这类复杂查询写在 XML 里能精确掌控每一行 SQL二是面试和工作中常被问到底层原理用原生 MyBatis 能把 SqlSession、Mapper 代理这些机制吃透。MySQL5.7 版本稳定、够用、资料多。8.0 虽然有窗口函数等新特性但 5.7 在兼容性和部署便利性上更稳妥。字符集统一用 utf8mb4别问问就是 Emoji 和生僻字。前后端分离的架构核心是让前端和后端各自独立开发、独立部署。前端只需要通过 HTTP 接口拿数据后端只负责业务逻辑和数据持久化。这个模式在团队协作里优势非常明显——前端开发不需要装 JDK 和 MySQL后端开发也不需要关心浏览器兼容性只要把接口文档约定好就行。2. 数据库设计与核心表结构2.1 用户与权限体系设计数据库设计是所有功能的地基。我先从用户体系开始拆。智能学习平台有三类角色学生student、教师teacher、管理员admin我用的方案是“用户表 角色字段”而不是单独的 RBAC 权限表。CREATE TABLE sys_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) DEFAULT NULL COMMENT 真实姓名, role tinyint(4) NOT NULL DEFAULT 1 COMMENT 1学生 2教师 3管理员, avatar varchar(255) DEFAULT NULL, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 1正常 0禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT系统用户表;这张表设计时有几个细节我要强调。第一密码绝不存明文必须用 BCrypt 加密Spring Security 自带的BCryptPasswordEncoder直接可用第二status字段必须预留用户被禁用后前端要能识别并踢下线第三角色用tinyint而不是字符串节省存储空间查询效率也更高。2.2 课程内容与学习进度建模课程模块是学习平台的核心业务我拆成了三张表课程表course、章节表chapter、学习记录表study_record。CREATE TABLE course ( id bigint(20) NOT NULL AUTO_INCREMENT, title varchar(100) NOT NULL COMMENT 课程名称, cover varchar(255) DEFAULT NULL COMMENT 封面图片地址, teacher_id bigint(20) DEFAULT NULL COMMENT 发布教师ID, description text COMMENT 课程简介, category varchar(30) DEFAULT NULL COMMENT 分类Java/前端/数据库等, difficulty tinyint(4) DEFAULT 1 COMMENT 难度1入门 2进阶 3高级, status tinyint(4) DEFAULT 0 COMMENT 0待审核 1已发布 2下架, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT课程表;课程表里的status字段很关键它支持了“发布审核”流程——教师创建的课程先进入待审核状态管理员审核通过后才对学生可见。这个设计在真实业务中几乎必用一开始如果省略掉后期再加审核逻辑要改一堆接口。学习记录表是整张库设计里最需要花心思的CREATE TABLE study_record ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL COMMENT 学生ID, course_id bigint(20) NOT NULL COMMENT 课程ID, chapter_id bigint(20) NOT NULL COMMENT 章节ID, study_duration int(11) DEFAULT 0 COMMENT 学习时长秒, progress tinyint(4) DEFAULT 0 COMMENT 本章节进度百分比 0-100, is_completed tinyint(4) DEFAULT 0 COMMENT 是否完成 0否 1是, last_study_time datetime DEFAULT NULL COMMENT 最后学习时间, PRIMARY KEY (id), UNIQUE KEY uk_user_chapter (user_id,chapter_id), KEY idx_user_course (user_id,course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学习记录表;这里加了联合唯一索引uk_user_chapter保证同一个学生对同一个章节只有一条学习记录避免了重复数据。查询时通过user_id course_id的联合索引快速定位某个学生在一门课下的所有章节进度课程详情页的“学习进度条”就是靠这个查询撑起来的。2.3 学习记录与智能推荐的数据支撑智能推荐模块我做了简化版但数据支撑是完整的。推荐逻辑的核心是“统计学生的学习偏好找到同分类下未学习的课程”。所需的 SQL 查询用到了聚合函数和子查询SELECT c.id, c.title, c.difficulty, COUNT(sr.id) AS study_count FROM course c LEFT JOIN study_record sr ON c.id sr.course_id AND sr.is_completed 1 WHERE c.status 1 AND c.category ( SELECT category FROM course WHERE id IN ( SELECT course_id FROM study_record WHERE user_id #{userId} ) GROUP BY category ORDER BY COUNT(*) DESC LIMIT 1 ) AND c.id NOT IN ( SELECT course_id FROM study_record WHERE user_id #{userId} ) GROUP BY c.id ORDER BY study_count DESC LIMIT 6;这条 SQL 的意图是先找到该学生学得最多的课程分类再从同分类中找出他没学过的课程按学习人数排序。虽然是简化版的“协同过滤”但已经具备推荐系统的雏形了。我在实际开发中发现这种统计类查询在 MyBatis XML 里写比用 LambdaQueryWrapper 拼要直观得多也方便 DBA 直接拿去优化。3. 后端核心实现与关键代码3.1 SpringBoot 项目结构与分层后端项目的包结构我严格遵守了经典的分层架构这个习惯在多人协作和后续维护中作用极大com.learn.platform ├── controller # 接口层只做参数接收和结果封装 ├── service # 业务层核心逻辑全部在这里 ├── mapper # MyBatis Mapper接口 ├── entity # 数据库实体类 ├── dto # 数据传输对象 ├── vo # 视图对象返回给前端的数据 ├── config # 配置类跨域、拦截器、WebMvc等 ├── common # 通用类统一返回结果、异常处理、常量 └── utils # 工具类JWT、文件上传等分层最关键的原则是“单向依赖”Controller 依赖 ServiceService 依赖 Mapper不允许反向调用。Controller 层要做到“瘦”只做三件事——接收参数、调用 Service、包装返回值。我在项目里定义了一个统一的返回类型ResultTpublic class ResultT { private Integer code; // 200成功 500失败 401未登录 private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } }前端拿到这个统一结构后只需要在 axios 拦截器里判断code是否为 200就能统一处理成功和异常不需要每个接口单独判断。这个约定在前后端分离项目中非常重要接口风格统一了联调效率能提升一大截。3.2 MyBatis 的合理使用与缓存坑MyBatis 的使用主要在两个地方实体类对应的基础 SQL 写在注解里复杂查询写在 XML 里。比如用户表的 MapperMapper public interface SysUserMapper { Select(SELECT * FROM sys_user WHERE username #{username}) SysUser selectByUsername(String username); Select(SELECT * FROM sys_user WHERE id #{id}) SysUser selectById(Long id); }注解方式适合这种单表简单查询写起来快。但学习记录列表、课程分页这种带动态条件分类筛选、难度筛选、关键字搜索的查询必须用 XMLselect idselectCoursePage resultTypecom.learn.platform.entity.Course SELECT * FROM course where if testcategory ! null and category ! AND category #{category} /if if testkeyword ! null and keyword ! AND title LIKE CONCAT(%, #{keyword}, %) /if AND status 1 /where ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} /selectwhere标签会自动去掉多余的 ANDif实现动态拼接。这里有一个新手极其容易踩的坑LIKE 查询不要用${keyword}直接拼接一定要用CONCAT(%, #{keyword}, %)${}是字符串替换存在 SQL 注入风险而#{}是预编译参数安全得多。再来说 MyBatis 缓存。MyBatis 有一级缓存和二级缓存一级缓存是 SqlSession 级别的默认开启二级缓存是 namespace 级别的默认关闭。我在开发中就吃过一级缓存的亏同一个 SqlSession 里先查询用户信息再更新用户状态如果中间没有清缓存可能读到脏数据。好在 Spring 管理的 SqlSession 默认每次操作都会新建和关闭这个问题在单体应用里很少触发但如果你后续改成手动管理 SqlSession一定要小心。二级缓存我直接没开。对于学习平台这种读多写少的系统加二级缓存看似能提升查询性能但实际上带来了缓存一致性的大麻烦——任何一张关联表的更新都要手动清理相关 namespace 的缓存一旦漏了用户看到的就是过期数据。我的建议是项目初期不要开二级缓存先把 Redis 缓存方案想清楚了再上。性能优化要建立在正确性之上这个顺序不能反。3.3 认证授权与接口设计登录认证我用了 JWTJSON Web Token方案。流程是用户登录成功后后端签发一个 token 返回给前端前端存储到 localStorage每次请求在请求头Authorization里带上后端通过拦截器校验 token 的有效性。Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().contains(/auth/login)) { return true; } String token request.getHeader(Authorization); if (token null || token.isEmpty()) { throw new BusinessException(401, 未登录或登录已过期); } // 解析token校验签名和有效期 Claims claims JwtUtil.parseToken(token.replace(Bearer , )); request.setAttribute(userId, claims.get(userId)); return true; } }这里有一个设计细节值得分享JWT 默认是无状态的服务端不保存会话所以用户被禁用后 token 仍然可能有效直到过期。我在设计里加了方案——用户表里维护一个token_version字段签发 JWT 时把这个版本号写进 token 里拦截器每次都从数据库读取用户当前版本号对比。版本号不一致说明用户被强制下线了。这个改造有效解决了 JWT 的“无法主动失效”问题。接口设计上我遵循了 RESTful 风格GET 用于查询POST 用于新增PUT 用于更新DELETE 用于删除。例如GET /api/course/page?pageNum1pageSize10分页查询课程POST /api/course教师创建课程POST /api/study/record上报学习进度GET /api/study/progress/{courseId}查询某课程的进度学习进度上报这个接口用了时间戳策略前端每 15 秒上报一次当前章节的学习时长增量后端累加到study_duration字段。这里防了一个问题用户直接关闭浏览器导致数据丢失。所以我在前端做了心跳机制页面关闭时使用navigator.sendBeacon()发送最后一条上报数据这个方法在页面卸载时依然能可靠送达。这个细节虽然不起眼但直接决定了学习时长统计的准确性。4. 前端 Vue3 实现与前后端联调4.1 Vue3 工程搭建与目录组织前端工程我用 Vite 搭建没有用 Vue CLI。Vite 基于 ES Module冷启动速度秒开开发体验比 Webpack 时代的 CRA 好太多。项目目录我做了这样的划分learn-frontend ├── src │ ├── api # 接口请求封装 │ ├── assets # 静态资源 │ ├── components # 公共组件 │ ├── router # 路由配置 │ ├── stores # Pinia状态管理 │ ├── views # 页面组件 │ ├── utils # 工具函数 │ ├── App.vue │ └── main.js路由配置使用了 Vue Router 4和 Vue3 配套。这里要注意的是路由守卫的写法变化Vue3 中使用router.beforeEach做登录拦截判断 localStorage 里有没有 token没有就跳转到登录页。用script setup的写法组件代码会非常简洁比如课程卡片组件script setup import { useRouter } from vue-router; const props defineProps({ course: { type: Object, required: true } }); const router useRouter(); function goDetail() { router.push({ path: /course/${props.course.id} }); } /script template div classcourse-card clickgoDetail img :srccourse.cover :altcourse.title / h3{{ course.title }}/h3 span{{ course.category }}/span /div /template4.2 数据交互与状态管理前端和后端的数据交互统一走 axios。我封装了一个 request 实例配置了 baseURL、请求超时时间和拦截器import axios from axios; import { ElMessage } from element-plus; const request axios.create({ baseURL: /api, timeout: 10000 }); // 请求拦截器携带token 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 ! 200) { ElMessage.error(res.message || 请求失败); if (res.code 401) { localStorage.removeItem(token); window.location.href /login; } return Promise.reject(new Error(res.message)); } return res.data; }, error { ElMessage.error(网络异常请稍后重试); return Promise.reject(error); } ); export default request;状态管理我用了 Pinia相比 VuexPinia 去掉了 mutations直接在 actions 里同步修改 state心智负担小很多。登录状态我存在 Pinia 的 userStore 里页面刷新后从 localStorage 恢复import { defineStore } from pinia; export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , userInfo: JSON.parse(localStorage.getItem(userInfo) || null) }), actions: { setLoginInfo(token, userInfo) { this.token token; this.userInfo userInfo; localStorage.setItem(token, token); localStorage.setItem(userInfo, JSON.stringify(userInfo)); }, logout() { this.token ; this.userInfo null; localStorage.removeItem(token); localStorage.removeItem(userInfo); } } });4.3 跨域与会话保持前后端分离必然面对跨域问题。本地开发时前端跑在 5173 端口后端跑在 8080 端口浏览器默认会阻止跨域请求。我用了两种方式解决开发环境用 Vite 的代理生产环境用 Nginx 反向代理。Vite 配置代理很简单在vite.config.js里export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } });这样前端的请求路径写成/api/course/pageVite 开发服务器会把它转发到后端的http://localhost:8080/api/course/page同源策略被完美绕过。生产环境我在 Nginx 里做了同样的事location /api/ { proxy_pass http://localhost:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }需要特别注意的是JWT 模式下不需要依赖 Cookie 做会话保持所以不需要配置withCredentials: true这反而简化了跨域处理。但如果你后续改用 Session 方案就必须同时配置withCredentials和后端的allowedOrigins否则 Cookie 带不过去登录状态保持就会出问题。5. 常见问题排查与性能优化5.1 数据库连接与慢查询项目跑起来后遇到的第一个大问题是数据库连接泄漏。表现是系统运行一两个小时接口突然全部超时控制台报Connection is not available, request timed out。排查过程很有代表性——先看 MySQL 的max_connections默认 151 个按理说一个学习平台系统并发不会打满但连接数就是持续涨到上限。最终定位到原因是某个查询在事务里抛了异常事务没有正确回滚连接一直没释放。我在写课程导入功能时用了Transactional注解但方法内部 catch 了异常并吞掉导致事务边界感知不到异常连接被占住。教训是Transactional方法内不要自己 try-catch 吞异常要么在 catch 里手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()要么直接抛出让 Spring 统一处理。慢查询排查我用的是 MySQL 的慢查询日志。开启方式SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1;打开后超过 1 秒的 SQL 都会被记录下来。我发现学习进度统计接口有一条 SQL 扫描了全表因为在study_record表上没建好复合索引。加了(user_id, course_id, is_completed)联合索引后查询时间从 800ms 降到 30ms 左右。这个优化收益是立竿见影的索引设计一定要结合业务查询模式不是越多越好而是要覆盖高频查询的 WHERE 条件和排序字段。5.2 MyBatis 缓存与脏读问题前面提到我关闭了 MyBatis 二级缓存但一级缓存带来的问题在集群部署时也要防。虽然 Spring 管理的 SqlSession 每次用完就关但如果你在同一个方法里先查再改再查一级缓存可能会命中旧数据。我实际遇到的一个场景是修改用户角色后立即查询用户列表结果角色没变。排查发现是 Service 层同一个方法里先调用了selectById查了一次然后updateRole更新数据库接着又查列表——因为一级缓存是 SqlSession 级别的第二个查询直接命中了缓存没有重新查库。解决方案很简单在更新操作对应的 Mapper 方法上不用管但 Service 层逻辑上要保证“先改后查有延迟”或者干脆拆成两个独立事务方法。更稳妥的做法是在需要强制刷新数据的地方用SqlSession.clearCache()或者在 XML 的更新语句里配置flushCachetrue这是默认行为但列出来提醒大家知道有这回事。核心经验别让缓存问题的排查浪费你半天时间——读写频率不对等的场景优先考虑直接在更新后重新查询而不是依赖缓存失效。5.3 前后端联调中的典型坑联调阶段的问题比后端还要多。我遇到的第一个问题是前端 Request Payload 是 JSON 格式后端 Controller 用RequestBody接收但字段名对不上。前端传的是courseId后端实体类字段叫course_id因为 MyBatis 开启了下划线转驼峰但 Jackson 反序列化的时候没有把 JSON 里的下划线字段映射到驼峰属性上。解决方式是在全局配置里开启 Jackson 的下划线转驼峰spring: jackson: property-naming-strategy: SNAKE_CASE或者更推荐的做法前端统一下划线命名或者后端 DTO 用JsonProperty(courseId)注解显式声明映射关系避免歧义。还有一个常见的坑是日期格式。MySQL 的datetime字段默认返回格式是2024-01-15T10:30:00.00000:00前端显示的时候如果没有格式化用户看到的就是一长串字母 T 的 UTC 时间。我在后端的考试时间字段用JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8)做了格式化同时前端再用 Day.js 做二次格式化兜底双保险这个坑就彻底排掉了。另外就是把 Vue3 打包后的dist目录放进 SpringBoot 的static目录作为单机部署方案时要注意前端路由是 history 模式直接访问/course/1会 404。解决方案是后端加一个控制器把非/api开头的请求都转发到index.html。Controller public class PageForwardController { RequestMapping(value {/, /course/**, /profile/**, /admin/**}) public String forward() { return forward:/index.html; } }这个方案适合不想单独部署 Nginx 的场景实测单机部署非常稳。6. 部署上线与环境配置要点6.1 后端打包与运行后端打的是可执行 JAR 包。打包前确认三件事application-prod.yml里的数据库连接指向生产库、JWT 密钥换成强随机值、日志级别设置为INFO。mvn clean package -DskipTests打出来的 JAR 用java -jar learn-platform-1.0.0.jar --spring.profiles.activeprod启动。我在服务器上用 systemd 做了守护进程进程崩溃后会自动拉起这个配置在生产环境几乎是必须的[Unit] DescriptionLearn Platform Server Afternetwork.target [Service] Userappuser ExecStart/usr/bin/java -Xms512m -Xmx512m -jar /opt/learn-platform/learn-platform.jar Restartalways RestartSec5 [Install] WantedBymulti-user.target内存配置这里我吃了亏。学习平台并发量不大但 MyBatis 加上 Jackson 序列化512MB 堆内存足够如果开得过大服务器内存不够会频繁触发 GC反而更慢。Xms和Xmx设成一样避免 JVM 运行时动态扩容导致的性能抖动。6.2 前端上线与避坑细节前端部署我用 Nginx 托管静态文件。打包命令是npm run build产物在dist目录。Nginx 的关键配置除了前面说的/api代理还有两个细节server { listen 80; server_name learn.example.com; root /opt/learn-frontend/dist; index index.html; # 解决history路由404 location / { try_files $uri $uri/ /index.html; } gzip on; gzip_types text/plain text/css application/json application/javascript; }try_files指令是 history 路由模式的生命线——用户手动刷新/course/3页面时Nginx 先找有没有对应的静态文件找不到就把请求重写到index.html让前端路由接管。开启 gzip 压缩后打包出来 100KB 的 JS 文件体积能压到 30KB 左右首屏加载速度快了一个量级。这个优化成本几乎为零收益却非常明显强烈建议每个前端项目都开。我在实际部署中还发现了一个很容易忽略的问题前端请求路径被 Nginx 代理后如果前端采用了按需引入的本地图片资源图片路径没有经过代理也可以直接访问但接口请求路径必须和 Nginx 代理前缀严格一致否则会出现请求到了 Nginx、Nginx 转不到后端的诡异问题。排查这种问题最快的方式是直接打开浏览器的 Network 面板看请求的实际 URL再对比 Nginx 的location规则十有八九是代理前缀匹配问题。7. 性能优化与扩展方向学习平台这类系统性能优化的高优先级清单里第一个是数据库索引第二个是缓存策略。前面已经讲了索引和缓存这里补充一个我压测中发现的问题课程列表页每次请求都对study_record表做COUNT(*)统计学习人数课程多、学习记录多之后这个COUNT成了瓶颈。我的优化方案是给课程表加一个冗余字段study_count每次有学习记录写入时顺手更新课程表的计数。读多写少的场景里牺牲一点点写入性能换取查询性能这笔账非常划算。MySQL 的UPDATE course SET study_count study_count 1 WHERE id ?是行级原子操作并发安全不用额外加锁。系统后续扩展我准备的方向有两个一是引入 Redis 做热门课程排行榜利用 ZSET 的INCRBY操作实现实时热度排名二是把智能推荐从“同分类推荐”升级为基于用户行为权重的策略比如浏览行为权重 0.3、完成章节权重 0.5、练习得分权重 0.2用加权评分做个性化排序。这些扩展在当前表结构上都能直接落地不需要大改数据库设计这也是前面说的“表结构设计要为未来留余地”的意义。另外提一个很多人忽略的细节日志系统一定要早点规范好。我用 logback 按天滚动错误日志单独存储appender nameERROR_FILE classch.qos.logback.core.rolling.RollingFileAppender filelogs/error.log/file filter classch.qos.logback.classic.filter.LevelFilter levelERROR/level onMatchACCEPT/onMatch onMismatchDENY/onMismatch /filter rollingPolicy classch.qos.logback.core.rolling.TimeBasedRollingPolicy fileNamePatternlogs/error.%d{yyyy-MM-dd}.log/fileNamePattern maxHistory30/maxHistory /rollingPolicy /appender排查线上问题的时候如果日志里错误信息混在业务打印里能让你找半天分开之后一条grep ERROR error.log就能定位问题这个习惯越早养成越好。最后分享一个我个人的小体会做这样的全栈项目最大的收获不是“学会了多少新框架 API”而是把从前端到后端、从数据库设计到部署上线的整条链路打通了。你写 Vue3 组件的时候要考虑接口返回什么结构写后端接口的时候要考虑前端拿到什么数据最方便写数据库的时候要考虑查询怎么写才高效——这种全局视角是只做单端开发很难获得的。项目本身可复现代码都在我自己的仓库里照着这个思路从零搭一遍踩过几个坑之后你对这套技术栈的理解会比看十遍教程都深刻。
返回列表