ARTICLE DETAIL

资讯详情

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

SpringBoot2+Vue3+MyBatis-Plus教学资源库系统开发实战

SpringBoot2+Vue3+MyBatis-Plus教学资源库系统开发实战 在高校里做教学资源管理最头疼的问题就是资源散落在各个老师的U盘、网盘和课程群里。直到我基于SpringBoot2Vue3MyBatis-PlusMySQL8.0这套组合重构了一版教学资源库系统才真正把课件、实验指导书、视频、习题这些资产统一管了起来。这套源码我从项目初始化到最终部署踩了不少坑也沉淀了一套可以复用的方法论今天把技术选型的逻辑、前后端的关键实现、数据库设计的细节以及排错过程都整理出来希望对正在折腾类似项目的同学有帮助。1. 项目整体设计与技术选型思路1.1 为什么选择前后端完全分离的架构刚开始做这个教学资源库的时候我其实犹豫过到底是用传统的JSPServlet还是直接上前后端分离。后来想清楚了这个系统的用户角色比较多——管理员、教师、学生、还有可能来检查的教务处人员每种角色看到的界面和能执行的操作都不一样。如果继续用服务端渲染每次需求调整都要动后端模板改起来太痛苦。前后端分离最直接的好处就是前端专注于交互和展示后端专注于提供API。Vue3负责管理端的复杂交互比如资源上传时的进度反馈、按学科分类的树形筛选、以及资源审核的状态流转这些如果放到服务端渲染里前端代码会乱得没法维护。后端的SpringBoot2只管接收请求、校验权限、处理业务、返回JSON分工非常干净。开发效率上这套组合也明显占优。Vue3的单文件组件把模板、脚本、样式放在一起组件复用率很高比如上传组件我封装了一次在教师端和管理端都能直接用。SpringBoot2的自动配置机制帮我省掉了大量XML配置配合Lombok连实体类的getter和setter都不用手写。实测从零开始搭骨架到跑通登录接口前后端加起来不到一天。1.2 核心依赖与版本选型的坑技术选型最怕的不是技术不好用而是版本之间互相不兼容。我这个项目在启动阶段就遇到过SpringBoot2.4和MyBatis-Plus的兼容性问题后来锁定了稳定组合才顺利推进。下面是我最终确定的依赖清单parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.14/version relativePath/ /parent dependencies dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.11.5/version scoperuntime/scope /dependency /dependencies强调一下MySQL驱动的坑。MySQL8.0的驱动类名已经从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.DriverURL后面还必须加上时区参数否则直接报时间区域错误。我项目里的jdbc配置是这样的spring.datasource.urljdbc:mysql://localhost:3306/edu_resource?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue spring.datasource.usernameroot spring.datasource.password你的密码 spring.datasource.driver-class-namecom.mysql.cj.jdbc.DriverallowPublicKeyRetrievaltrue这个参数特别重要MySQL8.0默认使用caching_sha2_password认证插件如果不开公钥检索第一次连接的时候会报Public Key Retrieval is not allowed的错误。前端方面Vue3我用了3.4.x的版本配套Vite5构建工具比Webpack快太多。Element Plus作为UI组件库版本选择2.6.x。状态管理用Pinia路由用Vue Router4HTTP库用Axios。这套组合是当前比较成熟的方案网上资料多遇到问题好搜。2. 后端核心实现SpringBoot2与MyBatis-Plus的实战要点2.1 实体类设计与MyBatis-Plus的省力之处教学资源库的实体类围绕资源展开核心表包括用户表、角色表、资源表、资源分类表、下载记录表、审核记录表。用MyBatis-Plus之后最明显的感受就是单表CRUD完全不用写SQL内置的BaseMapper接口已经提供了insert、deleteById、selectById、updateById这些常用方法而且还有强大的条件构造器QueryWrapper和LambdaQueryWrapper。以资源实体为例我当时的设计是这样的Data EqualsAndHashCode(callSuper false) TableName(t_resource) public class Resource { TableId(type IdType.ASSIGN_ID) private Long id; private String title; private String description; private String categoryCode; private String fileUrl; private String coverUrl; private Long fileSize; private String fileType; private Integer downloadCount; private Integer viewCount; private Integer status; private Long uploadUserId; private LocalDateTime createTime; private LocalDateTime updateTime; }这里有两个很有用的配置值得说一下。TableId(type IdType.ASSIGN_ID)是MyBatis-Plus的雪花算法主键分布式环境下不会重复比数据库的自增主键更稳。TableName(t_resource)是显式指定表名防止类名Resource和数据库关键字冲突。MyBatis-Plus的自动填充功能非常适合处理createTime和updateTime这种字段。我实现了一个MetaObjectHandler接口Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }然后在实体字段上加上TableField(fill FieldFill.INSERT)和TableField(fill FieldFill.INSERT_UPDATE)注解插入和更新的时候时间字段就会自动填上不用手动setData省了不少重复代码。2.2 逻辑删除与唯一索引的冲突处理教学资源库里的数据删除需要谨慎老师误删课件的情况很常见。我采用的方案是逻辑删除也就是在实体上加了TableLogic注解TableLogic private Integer deleted;这样MyBatis-Plus在做delete操作时会自动转成update语句把deleted字段置为1查询的时候自动加上deleted0的条件。这个设计让误删数据可以快速恢复后台管理界面上加一个回收站功能就能搞定。但这里藏着一个大坑逻辑删除会让数据库里的唯一索引失效。比如资源表里如果对categoryCode和title建了联合唯一索引第一次删除后deleted1第二次再插入相同标题的资源时因为唯一索引把deleted1的记录也算进去了就会导致插入失败。解决办法是不要直接在业务表上建包含逻辑删除字段的唯一索引而是用代码层面做重复检查或者在唯一索引里把deleted字段也加进去但要注意deleted字段需要有一个固定的非0标识。2.3 多条件分页查询接口的设计教学资源库的列表页是最常被访问的页面学生要按课程、类型、关键词筛选资源这个接口我用MyBatis-Plus的分页插件来实现。分页插件的配置方式Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }Service层的实现用了LambdaQueryWrapper既安全又简洁Override public IPageResourceVO queryResourcePage(ResourceQueryDTO dto) { PageResource page new Page(dto.getPageNum(), dto.getPageSize()); LambdaQueryWrapperResource wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.isNotBlank(dto.getTitle()), Resource::getTitle, dto.getTitle()) .eq(StringUtils.isNotBlank(dto.getCategoryCode()), Resource::getCategoryCode, dto.getCategoryCode()) .eq(dto.getStatus() ! null, Resource::getStatus, dto.getStatus()) .orderByAsc(Resource::getCategoryCode) .orderByDesc(Resource::getCreateTime); PageResource result this.page(page, wrapper); return convertToVO(result); }like方法第一个参数是boolean条件可以动态决定是否拼接这个查询条件这样前端传什么参数就查什么参数参数为空就跳过既灵活又不容易拼错SQL。2.4 JWT认证与权限控制方案资源库系统有严格的权限边界——学生只能下载公开资源教师能上传和编辑本人资源管理员有审核和删除权限。我用了JWT拦截器来实现认证和授权没有引入Spring Security因为在这个项目里简单的角色判断就够了Security的过滤器链配置反而增加复杂度。登录成功的接口返回一个JWT令牌public String generateToken(User user) { Date now new Date(); Date expiryDate new Date(now.getTime() 3600000); // 1小时过期 return Jwts.builder() .setSubject(user.getId().toString()) .claim(role, user.getRoleCode()) .claim(username, user.getUsername()) .setIssuedAt(now) .setExpiration(expiryDate) .signWith(secretKey, SignatureAlgorithm.HS256) .compact(); }然后写一个拦截器在进入Controller之前解析token并把用户信息放入ThreadLocal。这里重点提醒一下JWT不像Session那样可以服务端主动失效所以遇到用户修改密码或者被管理员禁用的情况JWT在过期之前仍然有效。解决办法是在用户表里加一个token_version字段修改密码时version1JWT中带上这个版本号拦截器比对版本号不一致就直接拒绝。3. 前端核心实现Vue3组合式API与权限路由3.1 基于Vite的项目初始化和Axios封装创建Vue3项目我直接用官方推荐的Vite方式npm create vitelatest edu-resource-frontend -- --template vue cd edu-resource-frontend npm install npm install element-plus axios pinia vue-routerVite的开发服务器启动速度快热更新是毫秒级的开发体验比Webpack好太多。Axios封装这块我单独建了一个request.js统一处理baseURL、token注入和响应拦截。核心代码如下import axios from axios import { ElMessage } from element-plus import router from /router import { useUserStore } from /stores/user const service axios.create({ baseURL: /api, timeout: 15000 }) service.interceptors.request.use(config { const userStore useUserStore() if (userStore.token) { config.headers[Authorization] Bearer userStore.token } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { if (error.response error.response.status 401) { const userStore useUserStore() userStore.clearToken() router.push(/login) ElMessage.error(登录已过期请重新登录) } else { ElMessage.error(error.message || 网络错误) } return Promise.reject(error) } )这样做的好处是所有API调用都不需要关心token的传递和错误处理每个接口调用的代码非常干净。响应拦截器统一处理401跳转登录前端就不需要在每个页面里重复写token失效的判断逻辑。3.2 Pinia状态管理与登录态持久化Vue3项目中Pinia是状态管理的首选方案。我的用户store如下import { defineStore } from pinia export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(edu_token) || , userInfo: JSON.parse(localStorage.getItem(edu_user) || {}) }), getters: { isLoggedIn: (state) !!state.token, role: (state) state.userInfo.roleCode || }, actions: { setLoginInfo(data) { this.token data.token this.userInfo data.user localStorage.setItem(edu_token, data.token) localStorage.setItem(edu_user, JSON.stringify(data.user)) }, logout() { this.token this.userInfo {} localStorage.removeItem(edu_token) localStorage.removeItem(edu_user) } } })这里有个经验分享token和用户信息存储到localStorage刷新页面后状态还能保留这样Vuex或Pinia内存里的数据就不会因为F5丢失。但要注意localStorage有XSS风险所以前端要做好输入框的内容转义不要用v-html直接渲染用户提交的内容。3.3 动态路由与菜单权限控制不同角色登录系统的可见菜单不一样我采用的是路由动态生成方案。基础路由只包含login、dashboard、403、404这几个页面业务路由在用户登录成功后再根据角色动态添加。核心逻辑是登录返回的用户信息里带上权限标识列表permissions前端定义一个路由映射表把权限标识和组件对应起来然后通过router.addRoute动态注册。菜单渲染根据Vue Router当前的routes配置生成。这个方案的好处是前端代码不会把管理员的审核页面暴露给学生虽然不安全的用户可以手动修改路由强行访问但后端接口有JWT和角色校验兜底前端隐藏只是为了更友好的交互体验。3.4 组合式API与资源上传组件的实践Vue3的Composition API让逻辑复用变得非常方便。我封装了一个上传组件集成了Element Plus的el-upload组件template el-upload :actionuploadUrl :headersuploadHeaders :before-uploadbeforeUpload :on-successhandleSuccess :on-errorhandleError drag el-iconUploadFilled //el-icon div classel-upload__text拖拽文件到此处或em点击上传/em/div /el-upload /template script setup import { computed } from vue import { ElMessage } from element-plus import { useUserStore } from /stores/user const userStore useUserStore() const uploadUrl /api/resource/upload const uploadHeaders computed(() ({ Authorization: Bearer userStore.token })) function beforeUpload(file) { const maxSize 500 * 1024 * 1024 if (file.size maxSize) { ElMessage.error(文件大小不能超过500MB) return false } return true } function handleSuccess(response) { if (response.code 200) { ElMessage.success(上传成功) } else { ElMessage.error(response.message) } } function handleError() { ElMessage.error(上传失败) } /scriptscript setup这种写法是Vue3推荐的语法糖组件的逻辑都直接写在setup作用域里代码比Options API简短很多。结合computed计算上传header每次请求都会自动带上当前最新的token。4. MySQL8.0在项目中的配置与设计细节4.1 数据库初始化与字符集设置创建教学资源库的数据库时字符集一定要用utf8mb4因为utf8mb4支持完整的Unicode包括emoji表情以及一些生僻汉字。有些课程资料里可能会包含特殊符号如果用utf8这些字符在存储时会变成乱码或直接插入失败。初始化脚本关键部分如下CREATE DATABASE IF NOT EXISTS edu_resource DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE edu_resource; CREATE TABLE t_user ( id BIGINT PRIMARY KEY COMMENT 主键ID雪花算法生成, username VARCHAR(50) NOT NULL COMMENT 用户名, password VARCHAR(100) NOT NULL COMMENT 密码BCrypt加密, real_name VARCHAR(50) COMMENT 真实姓名, role_code VARCHAR(20) NOT NULL COMMENT 角色编码ADMIN/TEACHER/STUDENT, avatar_url VARCHAR(255) COMMENT 头像地址, status TINYINT DEFAULT 1 COMMENT 状态1正常 0禁用, create_time DATETIME NOT NULL COMMENT 创建时间, update_time DATETIME NOT NULL COMMENT 更新时间, deleted TINYINT DEFAULT 0 COMMENT 逻辑删除0未删除 1已删除, UNIQUE KEY uk_username (username) ) ENGINEInnoDB COMMENT用户表;4.2 大字段与索引设计资源表t_resource中描述字段description用TEXT类型文件大小file_size用BIGINT类型。首页列表页经常用category_code和status作为查询条件所以索引设计为KEY idx_category_status (category_code, status), KEY idx_upload_user (upload_user_id), KEY idx_create_time (create_time)联合索引idx_category_status能同时支撑按分类查、按状态查、以及按分类加状态查的场景。下载记录表按资源ID和时间查询频繁也建了idx_resource_id。这里想提一个思路对于下载量、浏览量这类的统计分析不要直接在原表里做count统计而是每一条资源记录都维护一个download_count字段在用户下载时通过update语句把这个字段加一列表页直接读字段不走聚合查询大量节省了数据库IO。4.3 MySQL8.0用户权限与密码认证的坑MySQL8.0默认的认证插件是caching_sha2_password相比老的mysql_native_password更安全但某些旧版本的可视化工具连不上。如果你用的是Navicat旧版或某些版本DB可视化工具连接会报Authentication plugin caching_sha2_password cannot be loaded的错误。解决方案有两种。一是升级工具到新版本新版本都支持caching_sha2_password。二是把用户的认证方式改回mysql_native_passwordALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;但是要注意mysql_native_password在MySQL8.4里已经被移除了所以如果可能还是要升级工具而不是改认证方式。5. 资源上传存储方案与安全细节5.1 本地存储与Nginx静态映射教学资源库的文件存储在一开始数据量不大时可以先用本地磁盘。上传的文件统一放到一个指定目录比如D:/edu-resource/files/在SpringBoot的配置文件中定义好路径然后通过Nginx做静态资源的映射。Nginx配置里加一个locationserver { listen 8088; server_name localhost; location /files/ { alias D:/edu-resource/files/; expires 7d; add_header Cache-Control public; } }这样文件的访问URL就是http://localhost:8088/files/xxx.pdf同时Nginx层就处理了静态文件的缓存不占用SpringBoot应用服务器的线程。5.2 上传文件名的规范与防重复上传文件的第一件事就是重命名。如果直接用用户上传的原始文件名会带来两个问题一是中文文件名和空格在URL里需要转义二是重名文件会相互覆盖。我采用的策略是UUID原始扩展名String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String storedName UUID.randomUUID().toString().replace(-, ) ext;UUID生成的文件名保证了唯一性扩展名保留则是为了让用户下载时能正常识别文件类型。数据库里同时保存file_url存储路径和original_name原始文件名用户下载时通过Content-Disposition响应头恢复原始文件名。5.3 文件类型校验的双层防护文件安全不能只信任前端的选择框。前端限制只能选文档、视频、图片等类型但后端必须再校验一次。我的做法是校验MIME类型和后缀名双重验证MIME类型通过文件内容自动检测而不是看ContentType字段。这里遇到了一个特殊情况有次上传了一个.pdf文件前端检测到application/pdf但是后端用Tika检测出来却是application/octet-stream原因是那个PDF文件实际是加密的压缩包改后缀名伪装的。这种文件直接拒绝上传避免了后续用户下载后打不开或者打开后是病毒的风险。6. 常见问题排查与实战经验沉淀6.1 MyBatis-Plus查询条件不生效有段时间列表页筛选分类没反应调试发现是前端传的参数名和后端DTO字段名不一致。前端传的是categoryId后端DTO里定义的是categoryId但LambdaQueryWrapper中用的是Resource实体的categoryCode。两边字段名对不上SQL里自然多了一个恒真条件。排查方法很简单打开MyBatis-Plus的日志输出mybatis-plus.configuration.log-implorg.apache.ibatis.logging.stdout.StdOutImpl日志里直接打印SQL能看出来条件是否拼接成功。之后我养成了一个习惯写查询接口时先看日志里的SQL再验证返回结果。这个小习惯帮我提前发现了很多参数传递的问题。6.2 Vue3中的响应式数据失去响应在写前端时遇到过一个诡异的问题用reactive声明了一个对象给对象动态添加新属性后界面没有更新。原因是Vue3的reactive基于Proxy实现响应式但只能拦截对象已有属性的访问新增属性需要调用Object.assign合并或者直接用ref或者用this.$set的替代方案。// 错误写法 const form reactive({}) form.categoryName 数学 // 正确写法 const form ref({}) form.value.categoryName 数学Vue3中用ref对象外层包一层.value内部新增属性也具备响应式。这个知识点看起来基础但在关键场合会卡住很久。6.3 跨域问题与Vite代理配置开发环境下前后端分离一定会遇到跨域问题。Vite的代理配置如下// vite.config.js server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端请求/api/resource/list在开发环境中会自动转发到后端的8080端口。生产环境则直接用Nginx反代把/api和后端的SpringBoot端口绑在一起。这里我踩过一个坑Vite配置了代理但前端请求的baseURL直接写成了http://localhost:8080绕过了代理导致浏览器直接向后端发请求CORS又没在后端配置浏览器报跨域错误。所以Axios的baseURL要写成/api让代理生效而不是写完整的URL。6.4 分页查询返回数据格式不一致有次前端页码显示正确但后端返回的total是全表的数量不是筛选条件后的数量。这个问题出在自定义SQL上。我在某个查询里写了多表关联的SQL没有走MyBatis-Plus内置的分页而是用注解SQL自定义的分页条件然后忘了查总数时也带上筛选条件。解决思路是使用MyBatis-Plus的分页插件时自定义SQL也要写在特定格式的Mapper方法里让插件自动生成带条件的count语句。如果确实需要非常复杂的连表统计可以直接手写两条SQL一条查数据一条查计数参数保持一致。6.5 常见错误速查表错误信息原因分析解决方案Public Key Retrieval is not allowedMySQL8.0连接串缺少参数jdbc URL加allowPublicKeyRetrievaltrueUnknown database edu_resource数据库没建或连接串库名错误检查初始化SQL是否执行成功Table xxx doesnt exist表名与实体类名不匹配添加TableName注解指定表名Column create_time cannot be null自动填充没生效检查MetaObjectHandler是否注册Invalid bound statement (not found)Mapper接口和XML映射不匹配检查Mapper文件路径和namespaceCannot resolve symbol LambdaQueryWrapperMyBatis-Plus依赖未引入pom.xml添加mybatis-plus-boot-starterERR_CONNECTION_REFUSED前后端端口不一致或后端未启动检查Vite代理目标端口和后端启动日志[Vue warn]: Property xxx was accessed during render but is not defined响应式数据中访问了不存在的属性在reactive或ref中声明默认值7. 部署上线的关键步骤7.1 Jar包多环境打包教学资源库系统的部署我是用Maven打包成可执行的Jar包配合外部配置文件来区分开发、测试、生产环境。SpringBoot的profile配置mvn clean package -DskipTests java -jar edu-resource-1.0.0.jar --spring.profiles.activeprod生产环境的application-prod.yml里放真实的数据库连接、Redis地址、文件存储路径这些不因为打包带入代码仓库。这个实践的好处是不同环境之间只需要替换配置文件不需要重新编译代码。7.2 Nginx反向代理与前端静态文件前端项目构建之后npm run builddist目录就是纯静态资源拷贝到服务器上然后Nginx配置指向这个目录server { listen 80; server_name edu.resource.com; root /var/www/edu-resource/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /files/ { alias /data/edu-resource/files/; } location / { try_files $uri $uri/ /index.html; } }try_files那行特别关键Vue Router如果使用history模式刷新页面时请求的路径是前端路由路径比如/resource/detail/123Nginx要在磁盘上找不到这个文件时回退到index.html让Vue Router自己解析出对应页面。7.3 MySQL8.0生产环境的内存与连接数调优生产环境数据库的my.cnf里我调整了几个关键参数。教学资源库读多写少innodb_buffer_pool_size设置为主机内存的60%到70%比如8G内存的机器设5G。max_connections根据实际并发调整默认151太低系统上线后高峰会报Too many connections我调到500。另外开启慢查询日志日志时间阈值设置为2秒后期分析SQL性能非常有用。上线第一周我就通过慢查询日志发现分类统计接口有个全表扫描优化成索引覆盖查询后响应时间从3秒降到了0.2秒。8. 项目扩展方向与维护建议8.1 引入Redis做缓存与在线访问统计如果后续并发量上来建议引入Redis。资源排行榜、热门下载这种实时性要求不高的数据放到Redis里给数据库减负。在线用户数也可以基于Redis的键过期机制实现用户登录后写入一个带过期时间的key每次请求时刷新过期时间key还在就在线过期了就自动下线。8.2 文件存储迁移到对象存储本地文件存储只适用于中小规模如果资源库积累到几百GB甚至TB级别强烈建议迁移到对象存储服务比如MinIO或者云厂商的对象存储。SpringBoot集成MinIO的SDK并不复杂上传接口改成预签名URL直传可以减轻应用服务器的带宽压力。数据库表里加一个bucket_name和object_key字段和文件元数据分开存灵活性更高。如果是云环境对象存储自带CDN加速学生下载资源时体验会好很多。后端只需要生成一个临时访问凭证返回给前端前端拿着凭证直接上传或下载。这个方案还能顺便解决多节点部署时本地磁盘不一致的问题。8.3 教学资源库的功能扩展路线图做这套系统最深的体会是教学资源库的核心不只是存储更重要的是流转和沉淀。现阶段我做完了基础的上传、检索、审核、统计。下一步计划加入资源评价功能学生对下载的课件打分评论帮助老师迭代课件质量。再往后可以接入在线预览文档转PDF后用pdf.js在浏览器直接打开视频用HLS流媒体切片不用下载就能看。如果学校有统一身份认证还可以对接CAS或OAuth2免去学生单独注册账号的麻烦。我个人在维护这套系统的过程中最大的感悟是技术栈选型真的没必要追求新把SpringBoot2和Vue3用扎实了照样能开发出稳定易用、让师生都觉得好用的教学资源管理系统。初期花了一些时间梳理表结构和权限模型后面开发顺畅了很多。代码层面多写通用组件和封装少复制粘贴项目后期的可维护性会好很多。如果你是拿这套代码入门前后端分离项目建议先从后端接口开始跑通再慢慢啃Vue3的权限路由踩过几个坑之后整个体系就串起来了。
返回列表