ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

从需求到上线:Vue3+PHP(ThinkPHP+Laravel)课外阅读记录平台实战

2026/9/19 4:15:24 拓冰建站 浏览量
从需求到上线:Vue3+PHP(ThinkPHP+Laravel)课外阅读记录平台实战 做这个中小学生课外阅读记录平台最初是因为一位当小学老师的朋友吐槽班上几十个孩子课外阅读情况全靠家长在群里发照片接龙老师还得手动抄到Excel里统计月底想评个“阅读之星”简直要翻半天聊天记录。我当时正好在折腾 Vue3 和 PHP 后端就想着能不能做一个让老师、学生、家长都能用的系统。项目前后花了不到三个月前端用的是 Vue3后端在 ThinkPHP 和 Laravel 之间来回折腾最后两个框架都派上了用场。这篇文章不聊高大上的架构就讲讲这个项目从需求到上线的完整思路、踩坑记录以及一些可以直接抄走的代码片段。如果你正准备做教育类管理系统或者想看看 Vue3 PHP 这套组合怎么落地应该会有收获。1. 项目背景与核心目标1.1 这个系统到底要解决什么问题先说痛点。过去小学生的课外阅读记录最常见的方式是“家长签字 纸质登记表”孩子读完书在表上写个书名、页数家长签个字就算完成。老师想检查只能把一堆登记表收上来翻想统计班级阅读量还得手动录入电脑。这种模式有几个问题第一数据是静态的没法实时看到每个孩子的阅读进度第二记录内容简单看不出孩子是真读了还是随便填的第三缺少激励和反馈孩子坚持几周可能就疲了。我们做的这个平台核心就是解决“记录”和“反馈”两件事。学生可以快速记下今天读了多少页、读了多久、感受是什么系统自动计算阅读时长和积分老师能看到班级整体的阅读趋势、每个人的完成情况还能发布阅读计划和测评家长通过绑定的账号就能看到自己孩子的阅读日历不需要再在群里打卡。一句话概括把原本靠人工整理的阅读数据变成一套自动化的记录、分析和激励系统。1.2 核心功能点与用户角色平台有四种角色管理员、教师、学生、家长。管理员管书籍和账号教师管计划和任务学生做记录和测评家长只看报告。首版功能我拆成了六个模块模块说明主要使用者阅读记录打卡页登记书籍、页码、时长、心情和照片学生书单管理管理员维护全校共读书目教师可以添加班级书单管理员、教师阅读计划教师创建周期计划学生领取任务提交后教师审核教师、学生测评中心基于书目的选择题、判断题学生在线答题学生数据看板班级/个人阅读统计、排行榜、趋势图教师、家长通知中心任务提醒、审核结果、积分到账消息所有角色第一版不需要做太重的东西我甚至没做在线聊天和复杂社交就是把阅读记录和统计闭环打通。等老师和学生用习惯了再慢慢加东西。1.3 项目适合谁来参考我给这个项目的定位是“可运行的教育管理系统样板”。所以无论你是马上要做类似平台的开发者还是学校的信息化老师想搭个内部工具甚至只是想把 Vue3 和 Laravel/ThinkPHP 串起来练手都可以参考这篇文章。我会把框架选型、数据库设计、核心模块实现和部署细节都过一遍重点说那些文档里不写、但实操一定会遇到的坑。2. 技术选型Vue3 ThinkPHP Laravel 的选择2.1 前端为什么是Vue3前端部分我几乎没有犹豫就选了 Vue3。原因很实在第一这项目有大量表单交互和实时刷新场景Vue3 的组合式 API 可以让逻辑复用比 Vue2 的 Options API 舒服得多。比如阅读打卡的表单校验、图片上传、位置选择这些逻辑抽成一个useReadingLog组合函数页面代码非常干净。第二Vite 带来的开发体验是 Webpack 时代没法比的文件保存后浏览器几乎秒级更新调试效率高很多。当然Vue2 和 Vue3 的差异还是要心里有数。最明显的是全局 API 变成了实例方法filter被移除$on/$off也没了很多老项目里用Vue.prototype.$http的习惯得改成app.config.globalProperties。另外v-model的用法和多根节点组件在 Vue3 里也更灵活。如果是从 Vue2 项目迁移建议先用官方迁移工具扫一遍重点看插件是否兼容。环境搭建方面Windows 开发机上我用的 Node 18 pnpm创建项目直接跑pnpm create vite reading-platform --template vue-ts cd reading-platform pnpm install pnpm add element-plus axios vue-router4 pinia这样安装出来就是一个 Vue3 TypeScript Vite 的骨架后续加 Element Plus 做后台管理界面非常顺手。如果你在 Windows 上安装依赖遇到 node-sass 之类的问题建议直接改用 sassdart-sass编译更快也不容易报 Python 版本错误。2.2 ThinkPHP 和 Laravel我全都要一开始我只打算用 ThinkPHP因为它在国内资料多、中文文档友好而且配合小皮面板部署几乎零门槛。但做到阅读计划审批和通知推送的时候我发现 ThinkPHP 的队列和事件组件用起来还是差点意思每次都要自己写脚本或者引第三方包。后来试了 LaravelEloquent ORM 的关联模型写起来比 TP 的 Db 类顺手太多队列、任务调度、事件监听都是开箱即用“审批流”那套状态流转也能用枚举和事件做得非常清晰。于是项目就演变成了一个“双后端”结构主业务 API 用 Laravel 跑包括用户认证、打卡、测评、计划审批另外一些统计脚本和旧版接口用 ThinkPHP 跑原因是它部署轻服务器上一条php think run就能拉起来适合做定时任务和报表服务。两个服务共享同一个 MySQL 数据库但用不同的表前缀区分通过 Redis 做缓存和临时数据交换。标题里“Thinkphp-Laravel框架”其实就是这么来的——不是二选一而是按模块分而治之。我画一张部署逻辑图不用工具文字描述浏览器请求 Nginx静态资源直接走 Vue3 打包后的 dist/api/*反代到 Laravel 的public/index.php/report/*反代到 ThinkPHP 的public/index.php两者都连同一个 MySQL 和 Redis。定时任务由 ThinkPHP 每隔几分钟跑一次汇总阅读数据写入统计表Laravel 只负责业务读写。对比一下我实际用下来的感受对比点ThinkPHPLaravel开发文档中文友好上手快英文为主生态大ORMDb 类直白但关联处理繁琐Eloquent 关联非常顺畅队列/任务调度需要自己集成内置 Queue Scheduler权限认证用中间件手写JWT Policy 很标准部署门槛低小皮面板一键稍高需要配置 storage 权限适用场景轻量独立服务、报表核心业务、复杂关联、审批流如果你只是做一个小项目没必要像我一样搞两套选一个顺手就行。但如果你计划长期迭代把 Laravel 做主服务、用 ThinkPHP 做辅助统计脚本这种组合在中小团队里挺实用的。2.3 基础设施MySQL、Redis、对象存储存储我选的是 MySQL 8.0 Redis 6。MySQL 用来存业务数据Redis 主要放排行榜、验证码、限流计数以及两个框架之间共享的临时数据。图片文件上传后放到对象存储本地开发用 MinIO线上用阿里云 OSS 或者腾讯云 COS接口只保存 URL不落磁盘。这样迁移服务器也不用担心图片丢失。选择这些组件的原因很简单团队熟悉、维护成本低、学生访问量一般不会大到需要上微服务的程度。不要把系统设计得过于复杂够用就好。3. 系统整体架构与数据库设计3.1 前后端分离架构架构上我走的是标准前后端分离部署时 Vue3 打完包直接给 Nginx 托管后端 PHP 服务只提供 JSON API。这样有个好处以后如果想把前端换成 uni-app 或者做小程序后端接口可以完全复用数据层不用动。Vue3 端用的是 Vue Router 做单页应用路由Pinia 做全局状态axios 统一处理请求。登录后把 JWT 存在 localStorage 里axios 拦截器自动带Authorization头收到 401 就跳转登录页。这种模式很主流踩坑率低。后端在 Laravel 里用tymon/jwt-auth做认证中间件按角色过滤。ThinkPHP 服务则只接受内部请求通过一个自定义的X-Internal-Token校验避免外部直接调用报表接口。3.2 数据库核心表设计我把核心表做成了这样精简字段表名主要字段用途说明usersid, name, role, class_id, parent_id所有用户角色区分学生/教师/家长booksid, isbn, title, author, cover, category图书库管理员预先录入reading_logsid, student_id, book_id, page_start, page_end, duration, mood, content, record_date学生每日阅读打卡记录reading_plansid, teacher_id, title, start_date, end_date, status阅读计划主表plan_tasksid, plan_id, student_id, book_id, progress, status学生认领的阅读任务assessment_questionsid, book_id, type, question, options, answer, score测评题库assessment_resultsid, student_id, plan_id, score, level, detail测评结果points_logsid, student_id, points, reason, created_at积分流水这里有两个值得强调的地方。第一reading_logs里冗余了book_id和record_date但没有直接存书名字段因为查询时会关联 books 表而书名和作者在管理员修改后需要同步冗余存储容易不一致。第二我加了points_logs表因为积分是激励的核心每次打卡、测评都要记一笔流水后面统计“阅读之星”直接查这个表就好不用去复杂计算。3.3 API权限与安全权限设计上我在 Laravel 里用了中间件 Policy 的组合。路由示例Route::middleware([auth:api, role:student])-group(function () { Route::post(/reading-log, [ReadingLogController::class, store]); });role:student中间件判断 JWT 里的角色字段如果是学生才能记录打卡。教师在 Laravel 这边只能访问计划管理相关接口家长只能查看自己孩子的阅读报告。安全方面做了三件事所有输入都通过 Laravel 的FormRequest校验SQL 统一走 Eloquent避免字符串拼接注入前端 Vue3 里对富文本内容做v-html白名单过滤防止 XSS接口统一加了throttle:60,1限流防止恶意请求刷爆题库接口。教育系统里会有未成年人数据再怎么强调安全都不为过。4. 核心功能模块实操拆解4.1 阅读记录打卡模块这个模块是整个系统使用频率最高的功能学生每天打开就是记录。前端用 Element Plus 的el-form做表单字段包括书籍、开始页、结束页、阅读时长分钟、心情emoji 选择和一张照片。图片上传用el-upload提交时通过FormData一并发送到后端后端先存 OSS再写数据库。这里有几个设计细节后端每天去重同一个学生同一天对同一本书只能有一条记录用student_id book_id record_date建唯一索引然后写代码时捕获唯一索引冲突返回“今天已经打过卡了”的提示。自动计算页数差前端只提交 start 和 end后端计算page_count end - start 1避免前端篡改。时长校验时长必须大于 10 分钟否则认为无效记录防止乱填。Laravel 后端关键代码$validated $request-validate([ book_id required|exists:books,id, page_start required|integer|min:1, page_end required|integer|gt:page_start, duration required|integer|min:10|max:600, content nullable|string|max:500, image nullable|image|max:2048, ]);如果你做移动端适配页面布局要注意小屏底下弹窗键盘遮挡问题Element Plus 的自适应做得不错但我在24小时分段选择这种自定义控件上还是踩过坑后面在常见问题里细说。4.2 阅读计划与教师审核Laravel审批流实战阅读计划功能是这样教师创建一个计划选定时间段从书单里挑一堆书提交给教研组长管理员审批。审批通过后计划自动发布给学生学生可以认领任务。这个流程看起来简单但状态一多就容易乱。我用了最简单的状态机pending - approved或pending - rejected状态字段存计划表配合 Laravel 的事件监听去通知相关人员。审批接口代码public function approve(ReadingPlan $plan) { $this-authorize(approve, $plan); $plan-update([status approved]); event(new PlanApproved($plan)); return response()-json([message 已通过]); }PlanApproved事件里我通过通知机制给学生发站内信并更新学生的任务列表。实际使用中Laravel 的队列和事件配合很丝滑不需要自己撸 worker。后来我还加了一个小功能教师可以驳回计划并填写原因前端在“审核记录”里展示审批历史。这块虽然简单但让流程显得很正规家长和老师都觉得系统“靠谱”。4.3 阅读能力测评模块测评模块是为了回答“读得多不等于读得好”这个问题。每本书配置一套题题型包括单选、多选、判断题目直接挂在书下面。学生读完一本书后进入“完本测评”答题后系统按正确率给出等级并根据阅读时长、书目难度综合计算一个“阅读能力值”。实现上我还是用了 Laravel 队列。学生提交答案后接口只校验格式然后丢到队列里异步计算分数计算完把结果保存并推送通知。这样做的原因是如果几十个学生同时交卷同步计算会拖慢响应异步处理后接口毫秒级返回体验好很多。计算能力的逻辑基础分 正确题数 / 总题数 × 100时长加分 累计阅读时长分钟× 0.1封顶 20 分平均页数加分 每本书平均阅读页数 × 0.2封顶 30 分综合分数后映射成 5 个等级入门、熟练、进阶、优秀、阅读达人。学生每次测评完都能看到雷达图分别展示理解力、速度、积累量三个维度。这个设计让老师能一眼看出哪个学生是“读得快但理解不够”。4.4 数据可视化大屏Vue3 ECharts数据看板是整个系统最漂亮的部分也是老师最喜欢的功能。我用 Vue3 ECharts 做了两个页面一个是班级总览包含阅读趋势折线图、书籍类别饼图、学生排行柱状图另一个是个人报告包含每日任务打卡热力图和测评雷达图。封装 ECharts 的组合式函数特别适合复用import * as echarts from echarts import { onMounted, onUnmounted, ref } from vue export function useECharts(containerRef: RefHTMLElement | undefined) { let chart: echarts.ECharts | null null onMounted(() { if (containerRef.value) { chart echarts.init(containerRef.value) } }) function setOption(option: echarts.EChartsOption) { chart?.setOption(option) } function resize() { chart?.resize() } onUnmounted(() { chart?.dispose() }) return { setOption, resize } }使用的时候只需要把容器的 ref 传进去然后给 setOption 传配置。这里要特别注意一个问题如果项目里用了pxtorem做移动端适配ECharts 的图表容器会被影响导致图表宽高异常、字体错乱。我在项目里就没有全局 pxtorem而是只对普通元素做 rem图表容器用固定的像素单位。如果确实要全局适配记得在 resize 里手动更新图表。大屏页还做了一个动态线段背景类似“登录页点线动态背景”的效果。这个用 Canvas 实现其实很简单在背景上随机生成一些点然后绘制连线鼠标划过时高亮最近的节点。学生看到这个界面会感觉科技感很足也让整个平台更像“系统”。5. 开发环境搭建与前后端联调实录5.1 本地环境准备小皮面板和运行目录本地开发用的是小皮面板PHPStudyWindows 上装一个就同时搞定了 Nginx、MySQL、PHP省去自己安装的环境变量折腾。唯一要注意的是ThinkPHP 和 Laravel 的入口文件都在public目录里如果你把项目根目录直接作为网站根目录会暴露.env和源码文件非常危险。正确做法是在小皮面板创建网站时把“运行目录”指定到public。具体步骤小皮面板 → 网站 → 添加站点 → 填域名和路径 → 运行目录选择/public然后保存。伪静态选择ThinkPHPLaravel 也可以选这个或者laravel内置规则。这样访问http://localhost就会直接进入入口文件路径上没有index.php。Laravel 还需要给storage和bootstrap/cache目录写权限Windows 下一般没问题Linux 后台上线时记得chmod -R 775。5.2 Vue3前端初始化和API封装Vue3 项目初始化其实就是开头那几条命令这里重点说 API 封装。我习惯把所有请求收敛到一个 src/utils/request.ts 里统一处理 baseURL 和拦截器。import axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || /api, timeout: 10000, }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response response.data, error { if (error.response?.status 401) { router.push(/login) } else { ElMessage.error(error.response?.data?.message || 请求失败) } return Promise.reject(error) } ) export default request联调阶段最容易出现的两个问题一个是vue3怎么连接后端——其实就是配置好 baseURL 和跨域代理另一个是“路由跳转组件内容渲染不显示”多半是 router-view 放错了层级或者没在根组件引入 RouterView。把这两个问题解决掉前后端就通了大半。5.3 前后端联调常见问题我列了个表格很多问题都是新手必踩的现象原因解决方案axios 请求 404baseURL 没配好或 Nginx 没转发检查 Vite proxy 和 Nginx location登录成功后刷新页面白屏路由模式是 history服务端没配置 fallbackNginx 加try_files $uri $uri/ /index.html上传图片接口 CORS 报错后端域名与前端不一致配置 Laravel 的 CORS 中间件组件内拿不到路由参数使用了router-view同名组件复用给 router-view 添加:key$route.fullPath浏览器缓存旧接口数据接口有 GET 缓存请求头加Cache-Control: no-cache6. 部署上线与运维经验6.1 二级域名、伪静态和Nginx配置正式部署我用了三个二级域名前台www.example.com放 Vue3 静态页面API 用api.example.com转发到 Laravel报表后端用report.example.com转发到 ThinkPHP。设置二级域名时要注意PHP 后台里要绑定对应域名ThinkPHP 还要在.env或路由里设置好app_domain否则生成资源链接时会带上localhost。Nginx 配置示例server { listen 80; server_name api.example.com; root /var/www/laravel/public; index index.php; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { include fastcgi_params; fastcgi_pass 127.0.0.1:9000; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } }如果用的是 ThinkPHP伪静态规则和 Laravel 基本一样底层都是把请求交给index.php。6.2 定时任务、备份和日志我写了一个 ThinkPHP 的定时脚本每天凌晨同步读取 reading_logs 表计算出每个学生过去 7 天的阅读量写入到统计缓存这样大屏页面打开时直接读 Redis不用每次跑复杂 SQL。这个脚本通过服务器 crontab 每分钟执行一次* * * * * cd /var/www/report php think sync-read-statLaravel 那边用任务调度器做“未打卡提醒”每天晚上 8 点通过队列发送提醒通知给学生家长。Laravel 调度配置里只需一条 cron* * * * * cd /var/www/laravel php artisan schedule:run /dev/null 21数据库备份我用的是 mysqldump每天备份到另一个磁盘写脚本保留近 7 天并同步到对象存储。教育系统最怕数据丢了备份不能偷懒。6.3 安全与性能优化除了之前说的 SQL 注入和 XSS正式上线前一定要做以下几点开启 PHP OpCache性能提升明显。Redis 缓存访问量高的接口比如排行榜和书单列表设置 5 分钟过期。图片走 CDN不要直接在后端服务上提供下载。Nginx 层启用 Gzip 压缩Vue3 打包文件明显小一些。MySQL 慢查询日志打开每周看一遍慢 SQL优化索引。实际用下来这套配置在 2000 个学生同时注册的情况下也能扛住页面响应基本在 200ms 以内。7. 常见问题速查表与避坑指南7.1 前端高频问题实录问题原因解决办法修改 tabs 标签页样式不生效Element Plus 组件内部样式优先级高使用:deep()深度选择器props 赋值给 data 后组件不更新data 是初始值props 变化不会被赋值用watch监听 props 再赋值或直接计算属性图片和文字合并上传不能直接 multipart 混合用 FormData 先 append 字段再 append 文件ECharts 在弹窗/隐藏容器中宽度为 0容器不可见时初始化无法计算尺寸nextTick后初始化并在容器显示后调用 resize24 小时分段选择无法实现没有合适现成组件基于el-time-picker二次封装输出范围数组登录页面动态点线背景卡顿同时运行了多个 requestAnimationFrame用单动画循环控制在 60 帧内这些坑我在开发的过程中几乎全踩过一遍。比如 props 赋值问题当时学生详情页从列表传一个 id结果页面打开时永远显示上一本书的数据只有刷新后才正常排查了很久才发现是 data 初始化顺序问题。7.2 后端与部署问题实录问题原因解决办法Laravel 审批流状态回滚不一致业务逻辑没包事务DB::transaction包裹状态更新和事件处理同一个学生并发重复打卡前端多次点击唯一索引student_idbook_idrecord_date兜底ThinkPHP 二级域名下资源路径错误未设置app_domain配置app_domain或使用根相对路径Vue Router history 刷新 404服务器未支持 fallbackNginx 加try_files规则上传文件大小超限PHPupload_max_filesize默认 2M修改 php.ini 并重启 PHP 服务这里重点说一下“并发重复打卡”。我在测试时用脚本模拟 100 个请求同时提交结果表里插了很多重复记录后来加了唯一索引和 Redis 锁才堵住。这个案例做技术分享时特别有说服力也说明实际业务中不能只靠前端控制按钮。7.3 我的避坑心得做完整个项目我最大的体会是技术选型不要有执念。一开始我纠结 ThinkPHP 还是 Laravel浪费了一周时间看对比文章最后发现“两个都用”才是最适合我的。另外教育类系统的难点在于“数据好看”而不是“功能花哨”。老师要的不是一堆配置项而是打开就看到班级阅读曲线、谁没打卡。所以我在开发后期把重心都放在报表和大屏上这套东西一出来老师们都愿意用了。如果你也想做类似项目我建议第一版砍掉一切社交属性先跑通“打卡—统计—激励”闭环。上线后再加书评、讨论区、AI 荐书都不迟。系统的价值最终靠持续使用来体现而不是功能的堆叠。希望这篇记录能给你一些参考少走点弯路。