1. 项目概述:儿童性教育网站的定位与技术底座
儿童性教育这个话题,在当下的家庭教育和社会教育里越来越被正视。很多学校、社区机构、公益组织都在尝试用数字化手段做科普,但真正落地的时候往往卡在几个问题上:内容怎么分级、家长怎么参与、数据怎么沉淀、系统怎么维护。我做的这套管理系统,核心目标就是解决这些问题——它不是一个简单的展示型官网,而是一套完整的“内容管理+用户管理+学习记录+数据分析”的企业级后台,前端用Vue做交互,后端用SpringBoot扛业务逻辑,MyBatis负责数据持久化,MySQL存所有业务数据,四件套组合在一起,是当前Java Web开发里最主流也最稳妥的方案。
这套系统的服务对象主要有三类:第一类是教育机构,需要给自己的学员和家长提供一个科学、安全的性教育知识库;第二类是公益组织,需要管理志愿者、开展线上课程并记录学习效果;第三类是技术学习者,尤其是做Java课程设计或毕业设计的同学,这套系统的架构思路、权限模型、数据库设计都有很强的参考价值。无论你是哪种角色,这套源码拆解下来,都能直接复用或者二次开发。
从技术角度看,这套系统最值得说的不是某个单一功能有多炫,而是它把“企业级”这三个字落实到了实处:基于RBAC的权限控制、统一异常处理、MyBatis的动态SQL与分页插件、Vue的路由守卫与状态管理、MySQL的事务与索引优化,每一层都有明确的职责边界。下面我从整体设计、核心模块、实操部署、问题排查四个维度,把这套系统从头到尾拆一遍。
2. 整体设计与技术选型:为什么是SpringBoot+Vue+MyBatis+MySQL
2.1 前后端分离架构的取舍逻辑
这套系统采用的是前后端分离架构,前端Vue项目通过Axios向后端SpringBoot项目发请求,后端只提供JSON格式的RESTful API,不关心页面怎么渲染。这种设计在团队协作里优势非常明显:前端工程师和后端工程师可以并行开发,只要提前约定好接口文档,谁也不用等谁。
那为什么不选传统的JSP+Servlet或者Thymeleaf模板渲染?我的判断是,这套系统的功能形态决定了它必须前后端分离。儿童性教育网站的管理端有大量的表格、表单、弹窗、树形菜单交互,如果依赖服务端渲染,每次页面跳转都要刷新整个文档,用户体验会很割裂。而Vue的双向数据绑定和组件化开发,可以让管理后台的操作像本地应用一样流畅。另外,前后端分离之后,后端API可以被多个端复用——Web端、小程序端、甚至未来的App端,扩展成本几乎为零。
2.2 SpringBoot在业务落地中的核心价值
SpringBoot在Java后端生态里的地位不用多说,它就是用来解决Spring配置繁琐、依赖冲突、部署复杂这些老问题的。这个项目里我用到了SpringBoot的几个关键特性,逐个说。
自动配置是省事的关键。引入spring-boot-starter-web之后,内嵌的Tomcat就准备好了,引入spring-boot-starter-jdbc和对应的MySQL驱动,数据源也会自动装配。我只需要在application.yml里写清楚数据库连接信息,SpringBoot就能自己把DataSource、SqlSessionFactory这些基础设施搭好,不需要再写一堆XML配置文件。
统一异常处理这块,我习惯用@RestControllerAdvice配合@ExceptionHandler做全局异常捕获。系统里不管是参数校验失败、业务逻辑错误还是数据库异常,最终返回给前端的JSON结构都是统一的:{code: 0, msg: "成功", data: ...}。前端拿到这个结构,只需要根据code判断是弹出错误提示还是正常渲染数据,逻辑非常清晰。
事务管理方面,SpringBoot通过@Transactional注解就能搞定。比如后台管理员批量导入课程内容时,可能需要同时插入课程主表、章节表、知识点表,任何一张表插入失败都应该回滚整个操作,否则数据就脏了。我只需要在Service层的方法上加上@Transactional(rollbackFor = Exception.class),Spring的AOP机制就会自动处理事务提交和回滚,省去了大量手写事务管理的代码。
2.3 MyBatis为什么比JPA更合适
MyBatis和JPA之争是老生常谈了,这个项目我坚定选择MyBatis,原因就一条:这个系统的数据查询规则太灵活了。
性教育网站的内容是多维度的,课程可能按年龄段分类,也可能按主题分类,还可能按形式(图文、视频、音频)分类。后台管理端的列表页通常会提供各种筛选条件:按标题模糊查询、按状态筛选、按创建时间范围查询、按年龄段筛选,这些条件组合起来数量非常多。如果用JPA,这种动态条件查询就得靠Specification或者QueryDSL,写起来既绕又难维护。而MyBatis的XML映射文件里写动态SQL,用<where>、<if>、<choose>标签组合条件,逻辑一目了然。
再就是SQL的可控性。这个系统的数据报表模块需要做多表联查和聚合统计,比如统计每个年龄段用户的学习完成率、统计每周新增注册用户数等。用MyBatis我可以在XML里直接写优化过的SQL语句,甚至可以根据MySQL的执行计划手动调整索引策略。数据库层面的事情,就应该在SQL层面解决,这是MyBatis的哲学。
至于MyBatis的缓存机制,我单独说一下。MyBatis默认开启一级缓存,也就是SqlSession级别的缓存,同一个会话里相同查询不会重复查库。二级缓存是跨会话的,我在这个项目里没有开启,原因很简单:系统的数据更新频率不低,课程内容、用户学习记录都在实时变化,开二级缓存容易出现脏读问题,而且要处理缓存失效策略反而增加复杂度。对于这类业务系统,MyBatis自带的一级缓存完全够用,性能瓶颈通常不在查询量上,而在慢SQL和缺索引上。
2.4 MySQL存储引擎与字符集选型参考
MySQL版本我用的是8.0,存储引擎统一InnoDB,字符集用utf8mb4。这里有个很多人会踩的坑:MySQL 8.0之前的版本默认utf8其实是utf8mb3,只能存基本多语言平面的字符,像emoji表情、部分生僻字就存不进去。儿童性教育网站的问答区、留言板功能,用户完全可能发emoji,所以字符集必须用utf8mb4,对应的排序规则utf8mb4_general_ci就够了。
InnoDB相对MyISAM的核心优势在事务支持和行级锁。这个系统里有用户充值购买课程、管理员审核内容、学习进度实时记录等场景,并发写入频率不低,行级锁能最大限度减少锁冲突,事务机制保证数据一致性。选型这块没什么好犹豫的。
3. 数据库设计与权限模型:系统稳定运行的基石
3.1 核心业务表的字段设计与关系梳理
这套系统的数据库我一共设计了12张核心表,我按业务域拆开来列一下。
用户域三张表:sys_user(系统用户表,存管理员、教师、运营人员账号)、member(C端用户表,存注册的家长、学生)、member_role_relation(用户-角色关联表)。这里我把系统用户和C端用户分开建表,主要考虑是两个群体的字段差异太大:系统用户需要关联部门、岗位、登录IP,而C端用户需要记录年龄、性别、家长绑定关系等。混在一张表里会出现大量冗余字段,分开建表更清爽。
内容域四张表:course(课程表)、course_chapter(章节表)、knowledge_point(知识点表)、article(图文资讯表)。课程表的核心字段包括课程标题、所属年龄段、课程类型(视频/图文/音频)、封面图URL、上下架状态、创建人ID、创建时间。章节表通过course_id外键关联课程,用sort_order字段控制排序。知识点表是这套系统比较有特色的设计,它把课程里的关键知识点单独拆出来,方便后台运营人员做标签化管理,比如“身体界限”“青春期变化”“自我保护”这些主题标签,前端可以按标签聚合推荐内容。
学习域三张表:study_record(学习记录表,记录用户每次学习课程的行为)、study_progress(学习进度表,记录每个用户对每门课程学到哪一章)、exam_record(随堂测验记录表)。这三个表是数据报表模块的数据来源,也是这个系统区别于普通CMS的关键——它不只是内容展示,还能追踪学习效果。
运营域两张表:feedback(反馈表,收集用户对内容的意见)、banner(轮播图表,管理首页推荐位)。辅助表两张:sys_menu(菜单权限表)、sys_role_menu(角色-菜单关联表),这两张表支撑整个RBAC权限模型。
3.2 RBAC权限模型的具体落地方式
儿童性教育网站的内容敏感度比一般网站高得多,权限控制必须精细。我采用的是经典RBAC模型:用户-角色-菜单三层关系。sys_user存用户基本信息,sys_role表存角色(我预置了超级管理员、内容编辑、审核员、普通运营四种角色),sys_menu表存所有可访问的菜单和按钮级别的操作项,然后通过sys_user_role和sys_role_menu两张关联表把三者串起来。
具体到实现层面,后端在用户登录成功后,会查询该用户拥有的所有权限标识(比如course:add、user:delete),把这些权限标识塞进JWT令牌里返回。前端拿到JWT后存储起来,在路由配置里通过meta.permission字段声明每个路由需要的权限,配合beforeEach路由守卫做前置校验:没有权限的直接跳转403页面。后端接口层面,我通过自定义注解@RequirePermission("course:add")配合Spring AOP拦截器做二次校验,防止有人绕过前端直接调接口。双层校验,基本堵死了越权操作的可能。
3.3 索引设计与SQL优化笔记
这套系统的查询场景有几个高频入口:用户登录时按用户名查用户、后台课程列表按条件分页查询、前台按年龄段和主题查课程。针对这三个场景,我分别建了索引:uk_username唯一索引(用户表)、idx_course_age_status组合索引(课程表的年龄段+上下架状态)、idx_study_record_user_id(学习记录表的用户ID)。
实际操作里有一个很重要的细节:组合索引的字段顺序要遵循“最左前缀原则”。比如idx_course_age_status(age_group, status)这个索引,可以支持按age_group单独查询,也可以支持按age_group + status组合查询,但如果只按status查询,这个索引就完全失效了。我之前见过不少团队把组合索引的顺序建反,导致慢查询一直没解决,排查半天找不到原因。所以索引设计不要想当然,EXPLAIN一下,看到key那一列实际命中了哪个索引,心里才有底。
4. 核心功能模块拆解:从登录鉴权到学习记录
4.1 基于JWT的登录鉴权与Token刷新策略
登录模块是整套系统的安全入口。用户输入账号密码后,后端先用BCrypt算法校验密码哈希(BCrypt的加盐机制让彩虹表攻击基本失效),校验通过后生成JWT令牌返回前端。JWT里我放了三个核心字段:用户ID、用户名、权限标识列表。为什么不把角色名放进去?因为角色和菜单的映射关系可能随时调整,JWT又无法在服务端主动失效,如果权限信息全塞在JWT里,管理员改了角色权限后,用户要等Token过期才能生效。所以JWT里只放相对稳定的用户ID,前端每次请求时中间件会查一遍用户的实时权限做鉴权,虽然多一次数据库查询,但换来了权限变更的即时生效,这笔开销是值得的。
Token有效期我设置了2小时,同时在前端维护了一个“记住我”的逻辑:如果用户在登录页勾选了记住我,后端会额外生成一个refresh_token,有效期7天,存在HttpOnly的Cookie里。当access_token过期时,前端拦截到401响应,自动调用refresh接口换取新的access_token。用户感知就是“一直登录着,不会突然被踢出去”。当然,HttpOnly Cookie也带来了CSRF防护的需求,我在后端配置了CORS白名单,只允许前端域名跨域访问,同时在每次请求头里要求携带自定义的X-Token字段,双重保障。
4.2 课程管理模块:动态表单与富文本内容设计
后台课程管理是整个系统的核心业务模块。管理员创建课程时,需要填写课程标题、封面图、适用年龄段、课程简介、课程标签,然后为课程添加若干章节,每个章节又关联具体的知识点内容。
前端这块我用的是Vue的动态表单方案:课程基本信息是一个表单,章节列表用一个可增删的组件数组,点击“添加章节”就动态push一个章节对象,每个章节里还能点击“添加知识点”继续嵌套。这个层层嵌套的表单结构,如果不用Vue的响应式数据模型,实现起来会非常痛苦。我用reactive定义整个表单数据对象,新增、删除、修改都直接操作对象,视图自动更新,这就是Vue最直观的爽点。
后端接收时,我用了一个复合DTO来接收整个课程对象:课程主信息+章节列表+知识点列表。Service层的处理逻辑是:先插入课程主记录拿到自增ID,再遍历章节列表插入,最后遍历知识点插入。整个操作加上事务控制,任何一个环节失败都整体回滚。富文本编辑器我用的是wangEditor,它在Vue组件里用v-model就能绑定内容,比较简单。这里有个安全细节必须注意:富文本内容提交到后端必须做XSS过滤。我写了一个全局的过滤器,拦截请求体里的HTML字段,用Jsoup库清理<script>标签、onerror事件这类危险内容,防止存储型XSS攻击。这个点很多开发者会忽略,但在这个场景下必须做扎实,因为内容可能会被未成年人浏览。
4.3 学习进度跟踪:前端拦截路由+后端幂等写入
前台用户学习课程的流程是:进入课程详情页,查看章节列表,点击某个章节开始学习。这个系统在学习进度这块做了两个层面的记录。
第一个层面是章节完成度。用户在视频播放页,当播放进度超过80%时,前端会调用POST /api/study/progress接口,把这个章节标记为已完成。这里我用了一个防抖逻辑:不是实时上报进度,而是每15秒上报一次,并且只有当进度百分比比上次上报的值大时才发送请求,大大减少了无效请求量。
第二个层面是课程完成度。当用户完成一门课程的所有章节,前端根据章节完成数计算完成百分比,展示一个“结业”按钮。后端接口做了幂等处理:同一个用户对同一个章节的完成记录,如果重复提交,不会插入新记录,而是更新完成时间和耗时。这是通过(user_id, chapter_id)的唯一索引加上INSERT ... ON DUPLICATE KEY UPDATE实现的,既保证数据不重复,又避免并发请求下的异常。
前后端在进度数据上的数据流是这样的:用户学习时前端实时记录本地学习状态,同时异步同步到后端;用户离开页面时,前端主动调用一次保底上报接口,把该章节的最终状态同步过去。这样即使网络中断过,重新连接后也能把记录补上。
4.4 数据报表模块:MyBatis联表统计与Excel导出
报表模块是面向管理人员的功能,主要呈现三类数据:注册用户增长趋势、课程学习热度排行、年龄段学习完成率对比。我用ECharts做图表展示,前端只需要通过接口拿聚合好的数据,ECharts配置对应的折线图、柱状图、饼图即可。
后端实现报表SQL的时候,重点用到了MyBatis的几个高级特性。第一个是<foreach>标签处理动态参数,比如按多个年龄段筛选;第二个是<choose>标签处理不同的分组维度,前端传groupType=age就按年龄段分组,传groupType=week就按周分组;第三个是<sql>标签抽取公共SQL片段,避免多处重复编写相同的查询条件。
报表接口如果每次都实时全量统计,数据量大时容易慢。我在中间加了一层Redis缓存,缓存key按照业务维度拼接,缓存有效期5分钟。管理员在报表页面刷新的时候,5秒内基本能拿到数据,体验比直接裸查数据库好很多。Excel导出用的是阿里巴巴的EasyExcel,它的SAX模式解析和写入对内存友好,几十万条数据的导出也不会OOM。我封装了一个通用导出工具类,传入的List对象和目标Excel类的Class,方法内部通过反射完成字段映射,一行代码就可以把查询结果导出为.xlsx文件。
5. 前端Vue工程化的实操要点
5.1 Vue项目初始化与依赖安装的版本避坑
前端项目我用Vue CLI创建,Vue版本选的2.6,Element UI版本选的2.15。这里有个控制依赖版本的经验:如果项目要稳定产出,创建时直接指定版本号,不要无脑装最新版。比如Element UI在2.15.14之后基本停止维护了,而Vue 2生态在2023年底已停止官方维护,仍然有大量团队在用,就是因为存量系统稳定,没有必要追新。如果你是课程设计、毕业设计,用Vue 2全家桶+Element UI,网络上的案例和踩坑记录最丰富,遇到问题最容易找到解决方案。
Vue Router的安装和配置也是容易出问题的地方。Vue 2对应Vue Router 3.x,Vue 3对应Vue Router 4.x,版本不匹配会直接报错。我见过太多人从网上复制一段命令然后报错,一查发现是版本不对。npm install vue-router@3和npm install vue-router@4,这个花不了几秒钟,但能省几个小时。同样,Vuex版本也分:Vue 2对应Vuex 3,Vue 3对应Vuex 4。
5.2 路由守卫拦权限的完整代码方案
管理后台的路由我分成了两部分:静态路由(登录页、404页)和动态路由(所有功能页面)。用户登录后,前端根据返回的权限标识列表,动态注册路由。
// 权限路由核心逻辑 const permission = { path: '/permission', component: Layout, children: [ { path: 'user', name: 'UserManage', component: () => import('@/views/user/index.vue'), meta: { permission: ['user:list'] } }, { path: 'course', name: 'CourseManage', component: () => import('@/views/course/index.vue'), meta: { permission: ['course:list'] } } ] } // 全局前置守卫 router.beforeEach(async (to, from, next) => { const token = localStorage.getItem('access_token') if (!token) { if (to.path === '/login') next() else next('/login') return } const userStore = useUserStore() if (!userStore.userInfo) { await userStore.fetchUserInfo() } if (to.meta.permission && !userStore.hasPermission(to.meta.permission)) { next('/403') return } next() })这个方案的注意点是:next('/403')之前要确保403页面在静态路由里已经注册,否则会死循环跳转。另外,动态路由的方案适合菜单权限需要从后端下发的场景;如果你的系统菜单是写死在前端的,用静态路由+路由守卫就足够了,不需要额外的复杂度。
5.3 Axios封装:请求拦截、响应处理与错误提示
Axios封装是Vue项目里最基础也最重要的公共代码。我统一创建了一个request.js文件,里面配置了baseURL环境变量、请求超时时间,request拦截器负责从localStorage里取出Token并设置到请求头,response拦截器负责统一处理返回码。
service.interceptors.response.use( (response) => { const res = response.data if (res.code === 0) return res.data if (res.code === 401) { // Token过期,尝试refresh return refreshToken().then(() => { const config = response.config config.headers['Authorization'] = 'Bearer ' + localStorage.getItem('access_token') return service(config) }) } Message.error(res.msg || '请求失败') return Promise.reject(new Error(res.msg)) }, (error) => { if (error.response && error.response.status === 403) { Message.error('没有权限执行该操作') router.push('/403') } // 处理文件下载等特殊响应 return Promise.reject(error) } )关于文件导出,有一个非常容易踩的坑:导出Excel接口如果直接返回二进制流,响应拦截器如果统一按JSON处理就会解析失败。所以我的导出接口请求单独走了一个不经过拦截器的实例,收到response后直接处理Blob对象。这个是实战里特别常见的隐蔽Bug,排错的时候容易一头雾水,最终反应过来是拦截器把二进制流吞了。
6. 项目的完整落地实操:环境搭建、配置与部署
6.1 本地开发环境的搭建步骤
我这台机器是本机Windows 11,开发环境一步步装。
第一步,JDK用1.8。SpringBoot 2.x版本对JDK 8支持非常成熟。去Oracle官网下载JDK 8,配置JAVA_HOME环境变量,命令行里输入java -version能输出版本号就说明环境好了。
第二步,Maven用3.6.3。不一定要去官网单独下,我自己习惯用IDEA自带的Maven,配置好settings.xml里的阿里云镜像仓库,下载依赖会快很多。这个镜像配置很关键:不配置的话,从中央仓库拉取SpringBoot全家桶的依赖可能要等十几分钟,配了镜像基本一分钟内完成。
第三步,MySQL安装8.0。安装过程中选对字符集(utf8mb4),设置好root密码。Windows上MySQL安装完默认没有启动,我手动通过net start mysql启动服务。连接测试用命令行或Navicat都能测,能连上就进入下一步。
第四步,Redis安装。Windows下的Redis用tporadowski维护的版本,直接解压运行redis-server.exe就行。项目里缓存和Token存储都依赖Redis,启动后命令行里redis-cli ping返回PONG就说明正常运行。
第五步,Node.js环境。Vue项目依赖npm,Node.js版本我用的14.21.3。这个版本对应npm 6.x,配合Vue CLI 4.x项目很稳定。装好后npm config set registry https://registry.npmmirror.com,把npm源切到国内镜像,安装依赖的速度会有质的提升。
6.2 配置文件与应用启动的完整流程
后端项目的关键配置集中在application.yml里。数据源、Redis连接、MyBatis配置、JWT密钥都在这里维护。给出一个基础配置参考:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/sex_edu?useUnicode=true&characterEncoding=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai username: root password: root123456 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 database: 0 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.edusystem.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl特别强调连接串里的serverTimezone=Asia/Shanghai。MySQL 8.0的时区默认是UTC,如果不指定时区,应用连上数据库后所有时间字段都会差8个小时,排查起来特别容易让人崩溃。useSSL=false是因为本地开发的MySQL一般没有配置SSL证书,开启反而可能报警告。
MyBatis的map-underscore-to-camel-case配置建议打开,这样数据库字段user_name可以直接映射到实体类的userName属性,省去大量resultMap手写字段映射的工作量。
启动后端项目的步骤是:IDEA里导入项目,等待Maven下载完依赖,直接运行主类EducationApplication,控制台日志出现Started EducationApplication就启动成功。前端项目的启动步骤是:命令行进入项目目录,先npm install安装依赖,再npm run serve启动开发服务器,默认监听8080端口。前端项目和后端项目使用不同的端口,开发环境下需要通过Vue的代理配置把API请求转发到后端:
// vue.config.js devServer: { port: 3000, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }配置了代理之后,前端页面请求/api/xxx,实际上是发到http://localhost:8080/api/xxx,避免了跨域问题。注意后端接口的配合方式:如果后端配置了server.servlet.context-path=/api,那代理路径就需要对应调整。项目里我让后端控制器类直接以/api开头定义路径,前端代理不做路径重写,逻辑最简洁。
6.3 权限数据的初始化脚本说明
系统初始化需要一个SQL脚本,里面包含了建库建表、插入初始角色、插入管理员账号、插入菜单数据四部分内容。管理员账号我预置的是admin/admin123,密码经过BCrypt加密存入数据库,这也是为什么直接插入数据库是看不到明文密码的。
菜单数据这块,我插入的是权限树的初始数据,包含父级菜单(系统管理、内容管理、用户管理、报表统计)和子级操作,每个菜单项有menu_id、parent_id、menu_name、permission_code、route_path这些字段。前端路由的meta配置需要和后端菜单的permission_code保持一致,这个对应关系靠开发时人工维护,没有自动化工具。项目规模小还好,如果菜单多了,建议做个后端同步菜单到Redis缓存的小工具,后面权限变更刷新缓存就可以生效。
7. 常见问题与排错实录:我踩过的那些坑
7.1 SpringBoot与MySQL版本兼容性问题
这个坑出现的频率极高:SpringBoot 2.x配MySQL 8.0,启动时数据库连接报错Public Key Retrieval is not allowed。原因是MySQL 8.0默认使用caching_sha2_password插件管理用户密码,而连接URL里如果没有指定allowPublicKeyRetrieval=true,JDBC驱动在首次连接时会拒绝获取公钥。解决方式是在JDBC连接串里追加allowPublicKeyRetrieval=true&useSSL=false。网上很多老教程用的还是MySQL 5.x的配置方式,遇到这种问题容易卡住很久。
还有一种情况是Maven引入了过旧的MySQL驱动,比如mysql-connector-java5.1.x版本配MySQL 8.0,会出现Unknown system variable 'query_cache_size'错误。因为MySQL 8.0移除了查询缓存功能,旧驱动连上来会查询这个系统变量,直接报错。解决方案就是统一用MySQL官方推荐的8.0系列驱动依赖。
7.2 MyBatis分页插件不生效的排查思路
分页插件我用的是PageHelper。这个插件用起来简单,PageHelper.startPage(pageNum, pageSize)写在查询方法前一行,紧接着执行查询就能拿到分页结果。但实际操作中有一个高频错误:startPage后面跟的查询语句不是插件要拦截的那个SQL,分页就会失效,查出来的是全量数据。
这种问题常见出现在Mapper方法内部做了多次SQL查询的场景,比如先查了一次主表,又查了一次关联表,PageHelper只对第一次查询生效。排查的办法是打开MyBatis日志,观察控制台是否打印了SELECT count(0)——分页插件正常生效的标志就是会先执行一条count查询。如果只看到一条业务查询SQL而没有count,那基本可以确定startPage和Mapper方法之间隔了别的数据库操作。
另外,PageHelper的reasonable参数建议设置为true,这样页码越界时不会报错,而是自动修正到边界页码,提升接口的健壮性。
7.3 前端npm安装依赖的典型异常处理
npm install报错是前端开发最让人头大的问题之一。第一种常见异常就是权限相关的EACCES: permission denied,Windows上不常见,但macOS/Linux上频繁。解决方式就是不要用sudo装全局包,要给项目目录正确授权。第二种是网络相关,报ETIMEDOUT或者ECONNREFUSED,基本是默认npm源太慢或连不上,切到npmmirror镜像源立刻好。第三种是依赖版本冲突,报peerDependencies相关错误,解决方式是锁定核心依赖的版本号,不要用^范围符号。
我个人的经验是,遇到依赖安装问题先清缓存:npm cache clean --force,然后删掉node_modules目录和package-lock.json,重新npm install。一次不行就两次,大部分问题都能解决。
7.4 Token过期后前端白屏的集中排查
部署的时候遇到一个诡异问题:用户登录后长时间操作,突然刷新页面就白屏了。排查链路是:刷新页面后,前端路由守卫检查Token是否过期,发现过期了就调用refresh接口,refresh接口返回401,路由跳到了登录页。但问题在于,如果用户当前访问的是一个动态路由页面,动态路由的重新注册逻辑没有在Token刷新后执行,路由表里没有对应路由,就渲染出了空白页。
解决方式是:在fetchUserInfo成功重新注册动态路由后,通过next({ ...to, replace: true })手动导航一次,强制触发路由匹配重新执行。这个坑的核心思路是“Token过期后重新登录或续期,必须重新走一遍路由初始化”,很多前端新手不知道Vue Router的路由表是可以动态变化的,踩了坑才明白。
8. 系统扩展思路与二次开发建议
8.1 从单体到微服务的渐进式演进
这套系统的架构是基于单体应用设计的,单体在中小规模下没有任何问题。但如果业务量真的大起来,比如课程并发学习人数达到几千人,可以先做前后端彻底分离部署,前端静态文件扔Nginx,后端服务扛在Tomcat上,数据库单独一台机器。再往后可以按业务域拆分服务,比如会员服务、课程服务、报表服务独立部署,服务之间通过Feign或RPC通信。这套系统的代码结构里,包名按controller、service、mapper、entity分层,后续拆分微服务时可以按照这些包边界直接提取,迁移成本可控。
8.2 内容审核与安全加固建议
儿童性教育网站内容安全是重中之中,这部分建议在现有基础上继续加固。第一,图片和视频内容,应该接入第三方内容审核API,或者自建审核队列,让审核员人工复核后才能上线。第二,用户生成内容(比如留言、问答),建议先进入待审核状态,审核通过后再公开展示,防止出现违规内容。第三,后台操作日志应该全面记录,哪个管理员在什么时间改了哪条数据,通过AOP切面统一记录到日志表,给审计提供依据。
我在系统里预留了content_status字段,内容默认是草稿状态,编辑完成后提交审核,审核通过才设置成已发布。这个流程如果后续需要,可以把审核动作也做成菜单权限的一部分,审批流程落地后这套系统的内容安全级别会更上一个台阶。
8.3 微信小程序端的对接思路
系统后端的接口全部是RESTful JSON格式,天然适合小程序端复用。要做小程序对接,需要在后端增加微信登录逻辑:小程序端通过wx.login拿到code,后端拿着code调用微信接口换取openid,然后绑定或创建注册用户。这个系统里目前C端用户是手机号+密码登录,后续如果要接微信登录,加一个openid字段和对应的登录接口就能实现。
前端的小程序端页面可以直接复用管理后台的课程浏览、章节学习这些前端逻辑,只是UI组件库换成了微信原生组件或者Vant Weapp。学习记录和学习进度这两个接口设计的都是兼容多端的,因为上报的只是用户ID、章节ID、进度百分比,不关心请求来自哪个客户端。
9. 我在实际开发中的一些体会
这套系统从零写到部署运行,前后大概花了一个多月的时间。回过头来看,我最大的感受是:技术选型没有绝对的优劣,只有是否适合当前的场景。SpringBoot+Vue+MyBatis+MySQL这套组合,可能不是每个方面都是最强的,但它在团队协作、开发效率、社区生态、招聘匹配度上达到了一个很好的平衡。
我最想提醒后来者的一点是:不要只顾着写代码,数据库设计才是这类系统真正的灵魂。课程、章节、知识点、用户、学习记录这些表的设计如果做得扎实,后面的功能开发会顺利很多;反过来如果表结构设计随意,后期改表带来的工作量可能要超过前期所有开发量的总和。建表之前多画几张ER图,多想想每个字段在业务上意味着什么,这套时间花得非常值。
另外,前后端联调是项目推进中最影响心态的环节。我建议端到端接口联调之前,后端先通过Swagger把所有接口的请求参数和响应结构文档化,前端严格按照文档开发,有出入的地方开会一次对齐,别靠微信来回传话。协议对了,联调就快了。
最后再说一个运营层面的细节。这套系统的内容,我在录入的时候特别注意措辞——儿童性教育不是成人性知识,它的核心是帮助孩子认识身体、理解边界、学会保护自己,所以内容审核一定不能松。系统功能做得再好,内容本身不过关,整个项目的价值就归零了。这也是我在后台把审核功能和内容状态管理做得比较重的原因。如果你准备基于这套系统做二次开发,一定要把内容安全放在第一位。