1. 选型复盘ThinkPHP和Laravel在一套系统里怎么分工1.1 为什么不是“二选一”而是“各干各的”接手这个考试刷题及分析系统时我一开始也纠结了很久ThinkPHP和Laravel到底选哪个后来想明白一个道理——做项目不是比武大赛框架只是工具。真正的问题不是“哪个更好”而是“哪一段业务放在哪个框架里更省力”。我当时的判断依据很简单小程序端的主接口是大量题库读取、答题提交、错题记录这类高频CRUD特点是要快、要轻、要稳定ThinkPHP 6的模型和验证器在这类场景下非常顺手写起来效率高部署也简单。而后台管理端这边涉及成绩统计、学情分析、报表导出、定时汇总、审批流之类偏重业务流程的功能Laravel的Eloquent关系模型、队列任务、定时计划的生态更成熟尤其是统计报表和异步任务这一块Laravel几乎是开箱即用。所以最终架构定成了ThinkPHP扛小程序APILaravel做后台管理和数据分析中心。两个框架在一个项目里共存不是炫技而是各自负责自己最擅长的部分。1.2 两个框架如何协同工作session和token怎么打通两个框架共用一个MySQL数据库这是最核心的协同基础。小程序的答题记录、考试记录写入同一套表Laravel后台读同一套表做统计互不干扰。真正需要设计的是“用户身份”的打通。小程序端我用的是ThinkPHP签发的token登录时调用微信code2session拿到openid生成用户记录返回一个带过期时间的token。后续请求都带这个tokenThinkPHP侧用中间件校验。Laravel后台这边管理员登录走的是Laravel自带的session产出的统计数据通过内部API暴露给小程序。两边不直接共享登录态但共享数据层。这里有个容易被忽略的细节如果你让小程序直接调用Laravel的接口跨域、跨框架的鉴权会很麻烦。我是在Laravel侧做了一个API分组统一白名单IP或者内部签名校验只允许部署在同一台服务器上的ThinkPHP调用。这样小程序端永远只接触ThinkPHP一个入口Laravel后台的接口不直接暴露给外部。1.3 目录与仓库的落地结构我的项目根目录长这样exam-system/ ├── api-tp6/ # ThinkPHP 6 小程序API端 ├── admin-laravel/ # Laravel 后台管理端 ├── mini-program/ # 微信小程序前端 └── docs/ # 接口文档、数据库设计两个后端都部署在同一台服务器上api-tp6绑定api.xxx.comadmin-laravel绑定admin.xxx.com。部署时要注意ThinkPHP的运行目录必须指向public如果用的是小皮面板或宝塔站点设置里要把“运行目录”改成/public否则访问任何路径都会报错。这个坑我踩过后面部署章节再细说。2. 题库与刷题的数据模型提前把表设计清楚后面少改一半2.1 题目表的字段设计题库是刷题系统的地基表结构设计得不好后面做考试判分、学情分析都会难受。我最终的题目表核心字段如下字段名类型说明idint unsigned主键subject_idint科目ID比如科目一、科目二chapter_idint章节/考点ID用于按知识点统计question_typetinyint1单选 2多选 3判断contenttext题干支持富文本和图片optionsjson选项内容存JSON数组answervarchar正确答案单选存索引多选存数组analysistext答案解析difficultytinyint难度 1-5statustinyint0下架 1上架题目的选项这里我选型时有犹豫过到底单独建一个question_option表还是直接存JSON字段。实测下来刷题场景读多写少选项数量固定且不需要单独检索直接存JSON字段最省事。一次查询就把题目和选项全部拿出来不用连表接口响应速度明显比拆表快。判断题的选项其实可以不存后台录入时统一生成“正确/错误”两个选项即可。答案字段要注意多选的答案我存的是[A,B]这种JSON字符串判分时按集合比较和用户提交的答案做差集差集为空才算对。2.2 刷题进度与错题本的设计刷题系统的核心不是“出一张卷子”而是“连续刷、记录错、回头练”。所以除了题目表还有两张关键表第一张是答题记录表。每次用户提交一题就写一条记录user_answer_log - id - user_id - question_id - source_type // practice 练习 exam 考试 - user_answer // 用户提交的答案 - is_correct // 是否正确 - duration // 用时秒数 - exam_id // 考试记录ID练习可为空 - created_at这张表是学情分析的原始数据源所以索引很重要。我建了user_id source_type的联合索引统计“某人今天做了多少题”时直接走索引速度非常快。第二张是错题本表。刷题时答错自动写入错题本在错题本里重新做对一定次数后自动移出错题本。这里的逻辑我调整过好几次最后用的是“累计答对3次且错题后未再答错”才算掌握避免一次碰对就消失的误判。2.3 几个容易被忽略的边界情况选项顺序打乱练习模式下我建议对选项顺序做随机打乱防止用户背答案位置。但注意打乱后正确答案索引也要跟着变我是在后端用shuffle后重新生成映射关系前端拿到的永远是“当前选项顺序下的正确索引”。题目图片和公式题干里的图片建议传OSS或COS不要传服务器本地磁盘不然后期迁移很痛苦。理科题的公式直接用图片渲染前端rich-text组件可以正常展示。题目下架与历史记录一道题下架后历史答题记录的关联字段不要用外键强约束否则下架题目时会报错或级联删除数据。我统一用软关联统计时再过滤题目状态。3. 考试判分与学情分析从“刷了多少题”到“哪里不会”3.1 判分逻辑的设计全对与部分得分判分是整个系统最核心的业务逻辑我不建议在前端判分因为小程序端代码可以被反编译逻辑等于裸奔。我的做法是用户提交答案后后端统一判分。判分规则分几种单选题用户答案与标准答案完全一致得分。判断题同上只是选项固定为“正确/错误”。多选题默认全对才得分。如果产品想人性化一点也可以设置“少选得50%分、错选不得分”。我做的版本采用了只记正确/错误不搞部分得分因为部分得分会让学生纠结也会让成绩统计口径复杂化。核心判分代码很简短但要注意多选比较前先做排序否则[A,B]和[B,A]会判成不一致// ThinkPHP 判分伪代码 public function judge($question, $userAnswer) { if ($question[question_type] 2) { $std json_decode($question[answer], true); $usr $userAnswer; // 已排序的数组 sort($std); sort($usr); return $std $usr; } return strtoupper($userAnswer) strtoupper($question[answer]); }整套考试流程是创建试卷 - 从题库按章节难度抽题 - 用户交卷 - 后端逐题判分 - 写入考试记录 - 触发学情统计。抽查题时要注意同一份试卷不能重复抽到同一道题我用的是in_random_order取题后按题目ID去重而不是直接ORDER BY RAND()后者在几万道题时会严重拖慢数据库。3.2 学情分析都算哪些指标数据分析是这个系统区别于普通刷题App的卖点。刷题工具核心解决“练什么”分析系统要解决“还差什么”。我最终做了四个维度维度一正确率趋势。按天统计用户答题总数和正确数生成折线图能看到近7天或近30天的整体变化。维度二考点薄弱分布。按chapter_id聚合所有答题记录计算每个章节的正确率找出正确率低于60%的薄弱章节用柱状图展示。这是分析系统里最有价值的部分学生和家长一眼就能看出“数列部分不行函数部分还行”。维度三做题时长对比。统计每道题的平均耗时和用户自身耗时做对比。一道题如果正确但耗时是平均时长的两倍说明掌握得不够熟练这个信号单独列出来提醒。维度四错题重做率。定期把错题本里的题目重新组卷给用户做统计重做正确率。如果错题重做正确率在提升说明练习有效果。3.3 学情分析在Laravel侧的实现思路这部分业务放在Laravel里主要是因为统计任务适合异步做。小程序端的答题记录写入是高频操作用户每交一次卷就同步去跑全量统计数据库根本扛不住。我的做法是答题记录实时写入统计结果异步聚合。在Laravel里用一个定时任务每15分钟跑一次// Laravel Console Kernel 伪代码 protected function schedule(Schedule $schedule) { $schedule-command(stats:aggregate-user)-everyFifteenMinutes(); $schedule-command(stats:aggregate-chapter)-hourly(); $schedule-command(stats:rebuild-wrong-book)-daily(); }聚合结果落到一张user_study_stats表里小程序端查成绩报告时直接查这张汇总表不用临时跑大查询。这里有一个非常现实的坑如果用户量大15分钟的聚合任务可能跑不完。我的处理是队列化改造把每个用户的统计拆成独立任务扔进Redis队列并发处理Laravel的队列机制比ThinkPHP原生处理这个要顺手得多。后台管理端还做了一个审批流老师发布模拟考试需要管理员审核管理员在Laravel后台通过后考试才在小程序端显示。这类流程业务用Laravel的workflow或自己建审批状态表都很合适这也是我坚持把后台管理放进Laravel的另一个原因。4. 微信小程序端的关键实现与实测踩坑4.1 登录态与接口安全小程序端的登录链路不复杂wx.login()拿到code传给后端后端请求微信接口换取openid生成用户记录返回自签token。这里要注意的是不能说拿到了openid就完事了建议把token的过期时间设为7天以上并做自动续期否则用户隔几天打开小程序就要重新登录体验很差。接口安全方面我做了三层防护请求头带token后端中间件校验登录态。所有写接口做参数签名校验防止篡改。敏感接口加频率限制比如错题本重做接口每个用户每分钟最多请求30次。这里说句题外话小程序代码跑在用户手机里逻辑完全透明千万别把任何密钥、敏感判断丢在前端。判分、权限、答案比对这些必须全部放后端前端只负责展示。4.2 答题页面的交互与状态管理答题是小程序端最核心的交互页面。我的页面逻辑是一次加载20道题后端按顺序返回题目ID列表前端请求题目详情接口逐题显示用户答题后本地暂存答案全部完成后统一提交。这里有两个设计细节第一防止用户中途退出。用户答了15题突然退出进度就丢了这是刷题类小程序最影响留存的问题。我在答题页的onUnload里做了拦截判断如果当前有未提交的进度本地缓存作答状态下次进入时提示“继续上次答题”。热搜里提到的“返回拦截”能力小程序原生不支持完全阻断返回我是通过页面栈控制加wx.showModal提示来做到近似效果。第二避免重复提交。用户快速连点“交卷”按钮会在极短时间内触发两次提交请求。我用了前端锁定标志第一次点击后立即把按钮置灰后端也对同一份试卷记录做了唯一索引双保险防止重复。// 小程序端交卷防重复提交 submitExam() { if (this.data.submitting) return this.setData({ submitting: true }) wx.request({ url: ${config.baseUrl}/api/v1/exam/submit, method: POST, data: { exam_id: this.data.examId, answers: this.data.answers }, success: (res) { // 跳转成绩页 }, complete: () { this.setData({ submitting: false }) } }) }4.3 数据报告页的图表展示题库数据最终要以图表形式呈现给学生。我在小程序端对比过两种方案原生canvas绘制和第三方图表库F2。原生canvas的优点是体积小、不依赖第三方组件但手写柱状图、折线图的坐标轴、标签、Tooltip非常费时间而且canvas在真机上不同机型的尺寸适配也需要调。F2的优点是配置简单图表类型丰富社区案例多支持小程序端。缺点是要引入一份额外代码初期包体积会增加100多KB不过小程序有分包机制把图表页面放独立分包就行。我最后选了F2原因很简单分析报告页面的核心是数据准确、看着直观而不是重复造轮子。折线图展示正确率趋势柱状图展示章节薄弱分布用一个canvas组件装F2数据通过接口拉取后直接渲染。图表页还有两个适配细节容易踩坑一是canvas容器要给固定宽度和高度不要依赖百分比否则F2在部分安卓机型上计算宽度会出错二是在数据变化后要手动调用chart.changeData()更新图表光改setData不会自动刷新。4.4 那些容易让审核和真机翻车的小问题这部分是实战中一条一条趟出来的导航栏高度适配。自定义导航栏时不能写死导航栏高度。手机型号不一样状态栏高度和胶囊按钮位置都不同。正确做法是用wx.getWindowInfo()动态获取状态栏高度和胶囊按钮位置再算出导航栏总高度否则全面屏iPhone和安卓千元机上的页面布局会差出几十像素。iOS时间格式化兼容。后端返回的时间字符串2025-06-01 10:00:00在iOS上用new Date()解析是Invalid Date安卓正常。这个坑很经典原因是iOS不支持带横线的日期格式。统一方案是在后端返回时间戳前端用时间戳做格式化彻底绕开这个问题。“视频下载”类需求。现在不少题库会配视频解析热搜里也经常有人搜小程序视频下载。这里提醒一句视频内容在小程序里只能在线播放不能做下载功能内容类小程序做下载既违规又会触发审核拒绝。我用的方案是接入视频插件做在线播放不碰下载。虚拟支付审核。涉及会员订阅、题库付费的小程序审核规则非常严格。我的微信支付和会员逻辑是绑定在刷题服务里的这里有两条红线一是iOS端不能引导用户走虚拟支付要按平台规则隐藏支付入口或引导到其他合规途径二是不要尝试在小程序里做任何绕过平台审核的变相支付被处罚比损失功能严重得多。图片数量控制。题库里有大量含图片的题目我发现如果一屏内容纳的图片过多小程序在旧设备上会出现渲染卡顿甚至内存警告。处理方案是题目列表接口返回时对图片做?imageView2/2/w/750这种压缩参数限制宽度不超过750px遇到长图做裁剪只显示首屏区域点击再查看大图。5. 部署、性能优化与后续迭代5.1 服务器部署的简约方案部署这块我用的是小皮面板宝塔也可以整个部署流程可以一句话总结两个PHP站点加一个小程序前端包。ThinkPHP站点要注意设置运行目录为public。小皮面板创建站点后默认站点根目录指向项目根目录如果不改成/public访问任何路由都会在入口文件那里报错。伪静态必须配置为ThinkPHP的规则否则URL Rewrite会把/api/v1/question全部当成404。Laravel站点同样要指向public目录并且要手动执行php artisan config:cache和php artisan route:cache让路由和配置缓存生效否则第一次访问会非常慢。首次部署时经常遇到一个问题页面能打开但接口请求全部500。排查发现大部分是.env文件里的数据库配置没生效或者bootstrap/cache目录没有写权限。小程序端在微信开发者工具里上传代码后到微信公众平台提交审核即可。上传代码时要注意把request合法域名配置好开发阶段可以在开发者工具里勾选“不校验合法域名”真机预览时必须配置HTTPS域名而且域名要ICP备案否则真机请求直接失败。5.2 大数据量下的刷题性能优化题库刷到3万道题、同时在线的用户几百人时一些前期不明显的性能问题就会浮现出来。我实际遇到的三个主要瓶颈和对策如下瓶颈一随机抽题慢。后端组卷时直接用ORDER BY RAND()3万道题时这个查询耗时将近1秒。优化方式是先查出当前章节下符合条件的题目ID列表在PHP里用array_rand随机抽取再按ID查题目详情。题目详情有主键索引哪怕一次查50道题也很快。瓶颈二答题记录写入量大。用户每答一题写一条记录一个人考一场50题的考试要写50条。高峰期并发考试时MySQL的写压力一下子上来了。优化方式是一次提交的50条记录用insertAll批量写入而不是循环insert。实测批量写入比单条循环写入快了一个数量级。瓶颈三统计报表查询慢。之前学情分析接口直接查user_answer_log表按用户聚合200万条记录时接口超时是常事。优化方式就是前面说的交给Laravel定时任务异步聚合报表接口只查汇总表查询时间从5秒降到了100毫秒以内。另外纯读的题目列表接口我给热点章节的题目加了Redis缓存。题目内容基本不变缓存10分钟没有任何问题刷题高峰期时数据库读取压力一下就降下来了。要注意的是题目一旦修改要主动清理对应缓存否则用户会刷到旧题。我的做法是在后台编辑题的Laravel控制器里顺手删除该题ID对应的Redis key。5.3 从考试刷题工具到学习闭环系统上线稳定运行后我复盘时最大的感受是刷题工具最容易做的就是“下一题”最难的是让学生知道“我为什么错了、接下来该练什么”。分析系统这部分的价值是让整个产品从“题库答题器”升级成了“诊断练习”的闭环。具体到我实现的迭代考试报告页不再只展示总分而是会用雷达图展示各知识点的掌握度正确率低于阈值我设的是60%的章节自动推荐“专项突破”练习点进去就是该章节的题目列表。错题本增加“重做”功能重做正确后自动移出。后台管理端还可以按班级维度查看平均正确率老师能知道全班薄弱点在哪这在模拟考试和在线练习场景下很实用。未来如果继续扩展方向很明确一个是增加主观题和人工判分流程一个是接入语音读题和题目的视频解析再有就是配合题库运营做日更题目和每日一练的推送触达。这些功能的技术底座都是现在这套“ThinkPHP接口 Laravel后台统计 小程序端展示”的架构骨架已经在跑往后加模块会越来越顺手。回到开头那个问题——ThinkPHP和Laravel到底怎么选做这个项目之前我也觉得两个框架放在一套系统里是过度设计做完之后我的体会是当业务模块之间边界清晰、数据模型足够稳定时用多个后端框架各管一段不是加重负担反而是让每一段都跑得更舒服。这套经验不一定适合所有团队但如果你正在备考刷题类产品的技术方案希望能给你一个可以沿着走的参考路线。
