尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

PHP后端+uniapp小程序:古诗词学习挑战系统开发实录

发布时间:2026/9/29 17:22:34

资讯中心
01
ARTICLE

PHP后端+uniapp小程序:古诗词学习挑战系统开发实录

PHP后端+uniapp小程序:古诗词学习挑战系统开发实录
古诗词学习挑战系统开发实录PHP后端与uniapp小程序的全流程复盘去年接了这么一个项目需求方想做一个面向中小学生的古诗词学习小程序主打“学习挑战”双模式。前端选了uniapp一套代码同时编译到微信小程序和H5后端用PHP提供接口整套系统从需求梳理、数据库设计、接口开发到小程序上架前后花了三个月。目前线上稳定运行了大半年累计注册用户过万日活稳定在两千左右我觉得这套技术组合和功能设计有一定参考价值今天把整个项目的架构思路、核心玩法实现、以及那些文档里查不到的坑从头到尾捋一遍给正准备做同类项目的朋友当个参考。这个系统解决的核心问题其实很明确古诗文类App市面上不少但大多是“文库式”的孩子打开就失去兴趣。我们真正要做的是把学习变成一场带有即时反馈的对战。所以整个系统的灵魂在“挑战”PHP负责题库、判分、排行这些后端逻辑uniapp负责把题面、倒计时、动画反馈这些体验做出来。如果你是独立开发者、教育行业的产品经理或者正在选型做小程序的项目负责人这篇文章应该能帮你少走不少弯路。1. 项目梳理与技术选型思路1.1 为什么选择phpuniapp这对组合技术选型是这个项目最先定下来的事。需求方预算有限服务器就是个2核4G的云主机后端用PHP完全没有问题——它的部署成本极低一个Nginx加PHP-FPM就能跑得很舒服。开发效率上PHP配合ThinkPHP框架写一个带鉴权的接口通常十分钟就能搞定对于这种没有高并发压力的教育类小程序PHP的短板根本暴露不出来。uniapp这边理由更直接需求方明确说“以后可能要出App版本还要兼顾H5分享页面”。如果用原生微信小程序开发后面要再做一个App等于整个前端重写一遍。uniapp基于Vue语法一次开发可以同时产出微信小程序、支付宝小程序、H5和Android/iOS的App包后面真要拓展端前端不用动或者只做少量适配就能跑起来。这里有个很多新手容易忽略的点uniapp不是简单的“一套代码到处跑”它编译到不同端时是有差异的。比如H5端有完整的DOM和BOM小程序端则完全屏蔽了这些。所以我们在项目中约定了一个铁律业务代码一律通过uni.request和uni.setStorage等uniapp的API操作绝不允许直接操作window或document。这个约定在项目后期拓展H5端时帮了大忙。1.2 系统整体架构设计整个系统前后端分离前端uniapp以Vue3为基础编译后生成微信小程序包上传到微信公众平台后端PHP以ThinkPHP 8作为框架对外提供纯JSON格式的RESTful接口。数据存储选型上主库用MySQL 8.0Redis用来做用户登录态、排行榜实时数据和答题限流的缓存。有人可能会问用户量不大为什么要上Redis这里不是装是为了续榜实时性和每日一诗这类需要定时更新的功能做的准备。排行榜如果每次都查MySQL页面会卡用Redis的有序集合zSet做积分排行读写都非常快。项目目录结构大致是这样的frontend/ # uniapp项目目录 ├── pages/ # 页面首页、挑战赛、排行榜、我的 ├── components/ # 通用组件倒计时、诗词卡片、答题进度条 ├── api/ # 接口请求封装 ├── utils/ # 工具函数、鉴权模块 └── static/ # 静态资源 backend/ # PHP后端项目目录 ├── app/ # 应用代码 │ ├── controller/ # 控制器User、Poem、Challenge、Rank │ ├── model/ # 数据模型 │ ├── service/ # 业务逻辑层答题判分、积分结算 │ └── middleware/ # 中间件登录鉴权、接口限流 ├── config/ # 数据库、Redis等配置 └── route/ # 路由定义这种前后端分离的好处是小程序、H5、App共用一套后端接口后面不管新增什么端前端团队只需专注写界面后端接口完全不用动。业务逻辑我全部下沉到service层控制器只负责参数接收和结果返回这样接口的复用性和测试友好度都会高很多。2. 核心功能模块拆解与实现要点2.1 古诗词题库的建模与数据接口设计题库是整个系统最基础也最核心的部分。古诗文类内容和普通业务数据不一样它包含的信息字段非常多。我给诗词表设计的核心字段如下字段类型说明idint主键titlevarchar诗词标题如《静夜思》authorvarchar作者如李白dynastyvarchar朝代如唐contenttext正文含标点换行用\n分隔translationtext译文notestext注释JSON数组包含字词解释appreciationtext赏析内容difficultytinyint难度等级1-31基础、2进阶、3挑战categoryvarchar分类如咏物、抒情、边塞、哲理created_atdatetime创建时间正文为什么用text而不是一行一行的数组一开始我打算把每句诗存成JSON数组后端判分时对比容易。后来想想不对前端页面要展示整首诗的排版效果用\n分隔的纯文本反而更灵活H5和小程序都能直接解析而且接入mp-html富文本渲染时也更简单。接口设计上我遵循一个原则数据交给前端时结构要一步到位不要让小程序再做二次组装。比如获取诗词详情的接口直接返回一个包含上下五篇的导航信息用户滑动阅读时不需要再次请求接口。{ code: 0, msg: ok, data: { id: 101, title: 静夜思, author: 李白, dynasty: 唐, content: 床前明月光\\n疑是地上霜\\n举头望明月\\n低头思故乡, translation: 明亮的月光洒在床前..., prev_id: 100, next_id: 102 } }题库的冷启动是个不容忽视的问题。我们不可能上线第一天就有几千首带赏析的诗词所以我对内容团队提出了“三批入库”策略第一批150首小学必背第二批250首中学必背第三批再扩展到唐诗宋词三百首的完整库。每批数据都经过专人校对重点检查朝代、作者和标点符号的错误。市面上的诗词数据源鱼龙混杂错字漏字很常见这个钱不能省。2.2 挑战玩法设计与答题判分机制挑战模式是整个系统的留存抓手。设计的时候我研究过不少答题类产品最终把玩法定为每轮挑战回答10道题题型涵盖上下句填空、作者朝代判断、诗意理解和字词释义每题限时20秒答对加分连续答对有额外连击加成。每轮结束后显示得分和正确率根据积分划分段位从“诗词新秀”一直到“诗词状元”。答题判分的逻辑放在后端做前端只负责展示题目和收集用户选项。为什么不在前端判分因为小程序的代码逻辑是可以被反编译查看的如果判分逻辑放在前端用户改个变量就能刷满分后端接口接到的分数完全不可信。所以前端提交的是“用户答案”后端拿着正确答案做对比再算出本轮得分写回数据库。判分接口的PHP实现要点是这样的public function submit() { $user_id $this-request-userId; $round_id $this-request-post(round_id); $answers $this-request-post(answers); // [[question_id1,option_id2],...] // 用事务保证积分和答题记录的一致性 Db::startTrans(); try { $round RoundModel::find($round_id); if ($round-user_id ! $user_id || $round-status ! 0) { throw new \Exception(轮次状态异常); } $total_score 0; $correct_count 0; $combo 0; $max_combo 0; foreach ($answers as $item) { $question QuestionModel::find($item[question_id]); $is_correct ($question-correct_option_id $item[option_id]) ? 1 : 0; if ($is_correct) { $combo; $bonus min($combo - 1, 5) * 2; // 连击加成最多额外加10分 $total_score $question-score $bonus; $correct_count; } else { $combo 0; } $max_combo max($max_combo, $combo); // 记录每道题的答题详情 AnswerRecordModel::create([ user_id $user_id, round_id $round_id, question_id $item[question_id], option_id $item[option_id], is_correct $is_correct ]); } $round-score $total_score; $round-correct_count $correct_count; $round-max_combo $max_combo; $round-status 1; $round-save(); // 更新用户总积分和段位 UserModel::where(id, $user_id)-inc(total_score, $total_score)-update(); Db::commit(); return json([code 0, data [score $total_score]]); } catch (\Exception $e) { Db::rollback(); return json([code 500, msg 提交失败]); } }连击加分这个细节我解释一下。纯粹蒙题也能得分但连击机制能让“背诗连贯”这件事获得正向激励这也是游戏化学习的关键所在。我设置连击加成上限为5段也就是最多额外加10分防止玩家和高手之间的分差被拉得太悬殊。答题倒计时怎么处理前端拿题目的时候后端同时返回一个expire_time时间戳前端根据这个时间戳做倒计时。用户答题超时提交时后端校验时间戳发现已过期这道题无论选什么都判定为错误。这就是前后端双重校验前端控制体验后端保证公平。2.3 学习模式与打卡激励机制挑战模式负责拉活跃学习模式负责沉淀知识。学习模块包含三个子功能每日一诗、诗词卡片收藏、知识点测验。每日一诗的逻辑很简单就是每天全站用户看到同一首诗由后端每天凌晨零点从题库里轮换。这需要Redis加一个定时缓存机制// 每天凌晨刷新当日诗词缓存 public function refreshDailyPoem() { $today date(Y-m-d); $poem PoemModel::where(date, $today)-find(); if (!$poem) { // 随机从推荐池里选一首未推荐的 $poem PoemModel::where(recommend, 1) -where(last_recommend_date, , date(Y-m-d, strtotime(-30 days))) -orderRaw(RAND())-find(); $poem-date $today; $poem-last_recommend_date $today; $poem-save(); } Redis::set(daily_poem: . $today, json_encode($poem-toArray())); }这里我踩过一个坑刚开始直接用orderRaw(RAND())随机取诗结果用户反馈连续几天出现同一首诗。排查后发现是条件里只判断了last_recommend_date如果一首诗在一个月前被推荐过它依然可能再次被选中。后来我改成先从最近30天未推荐的诗里随机取如果池子空了则放宽到15天保证不会出现短期重复。打卡功能我是和挑战赛绑在一起的。用户只要每天完成至少一轮挑战就自动打卡成功连续打卡7天、21天、60天分别有不同等级的徽章。这一招的效果非常明显用户为了保住连续打卡记录每天打开小程序的意愿会强很多小程序本身有了“社交压力”留存就立起来了。3. 关键开发环节实操从登录鉴权到答题闭环3.1 微信小程序登录与用户体系打通小程序的登录体系和传统Web完全不同。用户在微信里打开小程序前端调用uni.login()拿到一个临时code这个code只能使用一次并且有效期只有五分钟。前端把code传给后端后端拿着code加上小程序的AppID和AppSecret向微信接口服务器换取openid和session_key。这里有个新手经常踩的坑uni.login()必须在用户点击按钮之后调用不能在小程序onLoad生命周期里静默调用。微信官方限制了这个API必须由用户手势触发否则会报invalid code错误。我一开始就在首页onLoad里调用结果真机上时不时出现登录失败的情况后来改成一个“微信一键登录”按钮点击后才发起login请求问题就消失了。换取openid之后后端流程是public function login() { $code $this-request-post(code); $appid 你的小程序AppID; $secret 你的小程序AppSecret; $res file_get_contents(https://api.weixin.qq.com/sns/jscode2session?appid{$appid}secret{$secret}js_code{$code}grant_typeauthorization_code); $res json_decode($res, true); if (!isset($res[openid])) { return json([code 401, msg 登录失败]); } // 根据openid查找或创建用户 $user UserModel::where(openid, $res[openid])-find(); if (!$user) { $user UserModel::create([ openid $res[openid], nickname 诗友 . mt_rand(10000, 99999), avatar , created_at date(Y-m-d H:i:s) ]); } // 生成自定义token存入Redis有效期7天 $token md5($res[openid] . time() . mt_rand(1000, 9999)); Redis::setex(user_token: . $token, 7 * 86400, $user-id); return json([code 0, data [token $token, user $user]]); }这里我不建议直接用微信的session_key做鉴权因为session_key是微信侧的会话凭证我们自己系统的接口如果也用它后面扩展App端、H5端时身份验证就很别扭。我选择自己生成token存Redis相当于做了一层“统一登录态”这样不管用户从哪个端进来只要token有效就能访问所有接口。token有效期7天客户端每次请求时在Header里带Authorization: Bearer token后端中间件统一解析。3.2 答题对战前端状态机设计答题页面前端是整个项目里交互最复杂的部分它至少要经历四个状态待开始、答题中、提交中、结算展示。我用Vue3的reactive管理了一个状态机对象const state reactive({ phase: loading, // loading | ready | answering | submitting | result currentIndex: 0, // 当前题号 questions: [], answers: [], remainTime: 20, timer: null });进入页面后先请求后端接口获取本轮10道题phase变成ready用户点击“开始挑战”后第一题显示倒计时启动phase进入answering。每答完一题答案push进数组下一题无缝切换。倒计时组件我单独抽了一层因为答题页和结算页都要用而且小程序端的定时器需要特别注意页面切到后台时uniapp的定时器会暂停回到前台后如果还显示旧时间会产生体验问题。我的处理方式是每次倒计时都基于后端返回的expire_time来计算剩余时间而不仅仅是一个本地递减的计数器// 倒计时计算基于后端时间戳而非本地自减 function getRemainTime() { const diff Math.floor((state.expireTime - Date.now()) / 1000); return Math.max(0, diff); }这样哪怕用户切后台再回来倒计时依然准确也不会出现本地时间被修改导致的作弊问题。提交答案用了防重复提交机制用户在结算页停留时前端会不断轮询后端查询本轮得分轮询接口做了幂等处理同一个round_id只返回一次结算数据。如果用户在结算完成前直接退出小程序下次进入时还能在“挑战记录”里看到本轮的最终成绩不会丢数据。3.3 古文内容排版与富文本解析古诗词的文风排版与普通文章不同正文要求居中、大字号、特殊间距注释和译文则要层次清晰。小程序原生组件不支持复杂的富文本HTML渲染所以前端我用了一款成熟的开源组件mp-html来渲染诗词详情页。mp-html的使用非常简单它是作为一个自定义组件嵌入pages的mp-html :contentpoem.content :tag-styletagStyle /安装和注册过程几句话带过关键是tag-style这步。我通过tag-style给p标签设置了行高、给标题设置了居中样式这样服务端返回的内容不用嵌入内联样式保持数据侧干净。举个例子我需要让正文每句居中且留白明显就配置如下const tagStyle { p: line-height: 1.8; margin-bottom: 12px; text-align: center; font-size: 18px; letter-spacing: 2px;, h1: font-size: 22px; font-weight: bold; text-align: center; margin-bottom: 8px;, blockquote: border-left: 3px solid #b08b5a; padding-left: 12px; color: #888; };用mp-html而不是直接用rich-text组件原因有两点一是mp-html支持代码高亮、表格渲染和图片懒加载纯rich-text遇到复杂结构会出错二是mp-html提供了onReady事件可以在渲染完成后动态调整页面布局。不过也要注意mp-html的包体积不小小程序主包有2MB限制如果项目里组件太多要考虑分包加载。3.4 自定义分享与小程序参数传递为了提升传播率我把挑战结果做成了可分享的卡片。用户挑战结束后点击“炫耀一下”调用uni.share弹起微信好友分享面板。这里有个细节小程序分享时通过path参数携带用户ID和本轮得分信息uni.share({ provider: weixin, scene: WXSceneSession, title: 我在古诗词挑战赛得了850分你能超过我吗, path: /pages/index/index?inviter userId score850, imageUrl: cardImagePath, success: () {} });被分享的人打开小程序后首页会读取options.inviter参数如果发现邀请人ID就调用后端接口记录邀请关系。这样设计的好处是我们不需要额外制作H5分享页分享回流直接落到了小程序本身转化路径更短。这个功能上线后分享率从最初的3%提升到了11%邀请裂变带来的新用户占了总用户的近三成属于投入产出比非常高的功能。4. 上线前后踩过的坑与排查清单4.1 uniapp打包安卓应用市场的适配问题项目上线三个月后需求方果然提出了做App的需求。uniapp的打包流程是在HBuilderX里配置好manifest.json云端打包生成apk文件。打包过程本身不复杂但有两个坑必须提醒第一安卓App的推送功能需要集成个推而个推在uniapp里的配置需要申请对应的appkey这个appkey跟微信小程序的AppID完全不是一回事很多人在这里卡住。第二安卓10及以上版本要求应用声明权限如果manifest.json里的权限配置和实际代码用到的API不一致华为等应用市场可能直接驳回。4.2 manifest配置与多端差异处理manifest.json是uniapp项目里最重要的配置文件微信小程序的AppID、App名称、图标、权限声明都在这里维护。我第一次配置时漏掉了mp-weixin里的appid字段导致本地调试正常但上传到微信公众平台时一直提示“AppID不匹配”排查了很久才发现是manifest里写的是测试号。多端差异处理上最大的问题出在CSS兼容性。比如苹果的刘海屏小程序顶部导航栏高度在不同机型上不一致我用uni.getSystemInfoSync()获取状态栏高度后动态设置占位块的高度才算完美适配。4.3 接口安全与数据防作弊教育类产品的数据安全和游戏一样重要。用户刷分、恶意请求、批量爬题库这些攻击手段我全都遇到过。首先是接口鉴权我自定义的token中间件对所有接口统一校验排除登录接口本身。token过期后返回401前端拦截响应并跳转登录页。这里要注意小程序端不能默认跨域问题H5端则必须配置跨域我在ThinkPHP的中间件里做了跨域Headers设置H5和App测试才通了。其次是限流防止有人用脚本狂刷接口。我在中间件里对同一token每秒钟的请求数做了限制超过阈值直接返回429。实战里发现有些刷子用的是IP轮换策略token都不同所以除了token限流外我还加了一道全局限流超过系统整体QPS阈值就触发保护。最后是题库防爬。诗词数据本身不算特别机密但被人完整扒走也不舒服。我在接口层面做了两个防爬手段一是详情接口没有开放全部列表只提供单首诗词的详情页二是对访问频率异常高的IP做了封禁处理。技术上没有用太重的风控但对小体量项目够用了。安全问题表现解决方案刷分作弊高频率提交答案后端判分、前端不可信机制接口暴力破解短时间大量请求token限流 IP限流题库爬取频繁调用详情页访问频率检测 封禁异常IP4.4 小程序审核与上架经验小程序审核是另一个让人头秃的环节。我们第一次提交审核被拒理由是“类目选择不正确”。古诗词学习属于“教育-教育信息服务”而不是普通的“工具-信息查询”。提交前一定要仔细核对小程序类目并确保有对应的资质文件不然白白浪费一个星期的审核周期。第二次被拒是因为“用户隐私协议不完整”。小程序目前强制要求配置用户隐私保护指引包括收集的信息类型、用途和第三方SDK列表都必须明确写明。建议上线前就准备好隐私政策页面并在小程序后台后台完整填写。4.5 性能优化与首屏加载小程序的冷启动速度直接影响用户留存。我做了三方面优化第一启用分包加载把挑战赛、排行榜这类低频页面放进分包首包只保留首页、登录页和诗词列表第二静态图片全部走CDN唐诗配图和头像图片不占小程序包体积第三首页接口做了缓存用户进入首页时先显示上次缓存的内容后台静默刷新。优化后的效果很直观首屏加载时间从2.8秒降到了1.2秒以内小程序的体验分也从72提升到了90以上。5. 数据统计与运营增长实践5.1 挑战完成率的数据分析与策略优化上线后我发现一个有意思的数据挑战赛的完成率只有58%这意味着每两轮挑战里就有一轮用户中途退出。分析数据后发现大部分退出发生在第7、8题此时倒计时压力很大用户连续答错两三道就容易放弃。针对这个问题我做了两处调整。第一降低前期题目的难度前3道题全部从“基础”题库里出确保用户开局不崩第二在第5题之后插入一次“锦囊”道具答错时可以选择跳过但锦囊每轮只有一次用完就没有了。调整之后完成率提升到了82%用户的次日留存也随之涨了7个百分点。5.2 排行榜实时更新与社交激励排行榜对用户活跃度的拉动非常明显。我用Redis的有序集合存每天的实时积分排行前100名展示在排行榜页。用户挑战结束那一刻积分会立即写入Redis并刷新排名给用户即时的竞争感。排行数据虽然存在Redis但七天总榜和历史榜单依然要落库不然Redis重启数据就全丢了。所以我的逻辑是Redis负责读写频繁的当天榜单MySQL负责持久化历史榜单每天凌晨定时任务把Redis数据同步回MySQL。5.3 uniapp中的图表展示与数据可视化排行榜页面我还接入了用户积分趋势的可视化图表。uniapp官方没有图表组件我用了echarts配合renderjs实现。当时在做H5端时图表正常但小程序端一直白屏排查很久发现是echarts的DOM操作在小程序环境里被禁用了。解决办法是使用微信小程序的canvas渲染方式。我引入了echarts-for-weixin这个适配方案它通过canvas 2d接口渲染不需要DOM。Vue3项目中配置起来稍微麻烦一点需要把echarts的初始化放在onReady生命周期里执行不能放在created里因为那一刻canvas还不存在。这套方案不仅兼容微信小程序H5和App端也能正常显示。6. 服务器部署与运维实用手册6.1 环境搭建与LNMP部署后端部署我用的是宝塔面板这算是PHP项目最省心的方案了。服务器配置了Nginx PHP 8.1 MySQL 8.0 Redis部署步骤其实就几步上传代码到服务器目录导入数据库修改.env配置文件再设置Nginx伪静态规则指向public目录。ThinkPHP 8项目有一个关键配置点伪静态规则。如果Nginx没有配置好访问接口会直接报404。我用的规则是location / { if (!-e $request_filename){ rewrite ^(.*)$ /index.php?s$1 last; } }6.2 定时任务与数据备份策略系统的每日诗词推荐、每日排行快照都需要定时任务。我用Crontab调用PHP的CLI模式执行0 0 * * * php /www/wwwroot/poem/think DailyPoem 0 1 * * * php /www/wwwroot/poem/think RankSnapshot数据备份我设置了每天凌晨全量备份MySQL保留最近15天备份文件自动上传到OSS对象存储。上线三个月后服务器被某个恶意扫描扫到并尝试暴力破解数据库密码。虽然密码足够复杂没有攻破但从此我强制修改了MySQL默认端口并限制了远程访问IP白名单这类攻击基本就杜绝了。6.3 日志分析与线上问题排查PHP项目的日志分析和抢修我总结了一条快速路径先看Nginx访问日志再看PHP-FPM慢日志最后查异常日志表。为了方便排查我在接口返回结构里统一加了request_id字段日志里记录了这个ID和对应的完整参数一旦用户反馈问题就能通过request_id快速定位到那一笔请求的完整链路。小程序端的排查则依赖微信开发者工具的Network面板看每个请求的状态码和返回数据。小程序与H5表现不一致时多半是API不兼容或者CSS渲染差异我会让前端开发者在两种环境分别console输出关键变量来对比。环境核心排查工具常见问题微信小程序微信开发者工具域名未配置合法API不兼容H5浏览器DevToolsCORS跨域、路由模式问题AppHBuilderX真机调试权限声明、推送配置、包体积写在最后这套系统的可复制经验如果让我把这些项目的经验浓缩成几句话我想说技术选型上PHPuniapp非常适合预算有限、需要快速上线、后续可能多端拓展的团队这套组合的性价比确实是目前市面上比较高的产品设计上内容型工具一定要做游戏化激励打卡、段位、排行榜、分享裂变这四件套是教育类小程序的标配运营层面数据复盘要比功能开发本身更重要一个基于完成率做的小调整收益远大于多加一个新功能。从我个人实际操作的体会来看古诗词学习挑战系统最难的其实不是技术而是内容质量的打磨和持续运营。技术方案再成熟也要靠内容去吸引用户、靠活动去留住用户。如果你也想做类似的垂直内容小程序建议先把题库、素材这些内容基础打好再动手写代码这样产品做出来才不会显得空。后续如果有条件这个项目还可以往“诗词地图打卡”、“小队对战PK”、“AI诗词创作点评”这些方向去扩展每一块都有很多可挖的空间。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。