独立版答题小程序源码拆解:PHP+MySQL后端架构与商业化功能实现
简介这是一套可直接部署的“在线答题/答题竞猜”微信小程序独立源码面向需要快速上线答题互动小程序的开发者、运营者或个人站长。功能覆盖自定义题库、激励视频领取在线奖励、登录礼包、奖励兑换、排行榜PK、答题卡与复活卡等并加入全新音效增强体验兼顾趣味性与运营留存。源码基于PHP 7.2与MySQL 5.6环境随包附带详细搭建教程可快速完成环境配置与部署调试。资源包共2002个文件主要包含1086个JS逻辑脚本、420个HTML页面、327个CSS样式、90个JSON数据配置等整体大小约46.92MB目录结构完整前后端分层清晰便于二次开发与功能扩展也适合作为小程序开发的学习样例。当前已有191人学习下载若正需要一套功能完整、拓展灵活的答题/竞猜源码这份资源值得入手参考。1. 独立版答题小程序不只有答题一份能跑通的微信小程序源码拆解答题类微信小程序源码在 GitHub 上一抓一大把但绝大多数是“伪独立版”前端套模板、后端依赖云开发或者题库写死在 JS 里换套题就要发版审核。这套独立版源码不一样的地方在于它把 PHP 7.2 MySQL 5.6 作为完整后端小程序端只管展示和交互题库、奖励、排行榜全走接口——这意味着你可以真正“接管”数据而不是被平台锁死。它的功能点也明显冲着商业化去自定义题库、激励视频领奖励、登录礼包、奖励兑换、答题卡、复活卡、排行榜每一项都对应真实运营场景。适合三类人想快速上线答题类小程序的开发者需要给现有 App 或公众号做增长活动的运营以及想研究小程序支付、广告回调、排行榜并发问题的后端工程师。下面从数据库设计开始逐步拆到上线避坑。2. 服务端与小程序端架构PHP 7.2 MySQL 5.6 的选型与数据表设计2.1 为什么独立版用 PHP MySQL 而不是小程序云开发很多 2024 年之后的新项目倾向用微信云开发免运维、免域名备案但云开发有个硬伤你无法完全掌控用户数据和奖励流水。尤其是答题竞猜这类涉及奖励兑换、广告收入分账的项目财务审计需要导出的数据表结构必须是自己的。而 PHP MySQL 的经典组合在今天依然有不可替代的优势——部署门槛低、任意虚拟主机都能跑国内云厂商的轻量服务器几百元一年就能撑起中小规模答题活动。这套源码选择 PHP 7.2 和 MySQL 5.6 也是现实考量很多开发者手里还留着老服务器PHP 7.2 对 mysqli 和 PDO 的支持很成熟MySQL 5.6 的 InnoDB 事务能力足以处理奖励流水。项目自带 CSS 文件bootstrap、AdminLTE、layui说明它还包括一个 Web 管理后台管理员可以直接在网页上维护题库、查看用户数据。这种“小程序端 Web 管理端 单一后端”的架构比纯云开发更容易让开发者理解和修改。2.2 核心数据表结构题库、用户答题记录、奖励流水答题系统的核心资产是题库和成绩。打开源码的 SQL 文件你会看到几张关键表的设计。我简化掉冗余字段保留最重要的结构-- 题库表 CREATE TABLE question_bank ( id int(11) NOT NULL AUTO_INCREMENT, question text NOT NULL COMMENT 题目内容, option_a varchar(255) DEFAULT NULL, option_b varchar(255) DEFAULT NULL, option_c varchar(255) DEFAULT NULL, option_d varchar(255) DEFAULT NULL, answer char(1) NOT NULL COMMENT 正确答案 A/B/C/D, category_id smallint(6) DEFAULT 1 COMMENT 分类对应答题模式, difficulty tinyint(1) DEFAULT 1 COMMENT 难度 1-3, status tinyint(1) DEFAULT 1 COMMENT 1启用 0停用, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT题库表; -- 用户答题记录表 CREATE TABLE answer_log ( id bigint(20) NOT NULL AUTO_INCREMENT, uid int(11) NOT NULL COMMENT 用户ID, paper_id int(11) NOT NULL COMMENT 本次答题会话ID, question_id int(11) NOT NULL, user_answer char(1) DEFAULT NULL, is_correct tinyint(1) DEFAULT 0, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_uid_paper (uid, paper_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 奖励流水表 CREATE TABLE reward_log ( id bigint(20) NOT NULL AUTO_INCREMENT, uid int(11) NOT NULL, type tinyint(1) DEFAULT 0 COMMENT 1登录礼包 2激励视频奖励 3兑换扣减 4管理员发放, amount decimal(10,2) NOT NULL DEFAULT 0.00, balance decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT 变动后余额, remark varchar(255) DEFAULT , create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_uid_time (uid, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT奖励流水腾讯云函数或定时任务可核对;三个表的设计要点answer_log通过paper_id区分“第几轮答题”而不是一条记录存全部答案这样方便实现答题卡断点续答和回溯分析。reward_log中的balance字段记录变动后的余额而不是让前端传总额防止接口被刷导致金额不一致。如果后续并发量上来可以把reward_log改成用数据库事务配合SELECT ... FOR UPDATE或者引入 Redis 做余额原子扣减。2.3 后端接口设计与鉴权方式后端统一走index.php?actionxxx或者 ThinkPHP 风格的路由。核心接口包括login微信登录换取 openid、get_questions拉取题目、submit_answer提交答题结果、get_balance查询余额、exchange_reward兑换奖励、get_rank排行榜。看一个典型的获取题目接口实现public function get_questions() { $uid $this-checkLogin(); // 校验token并返回uid $categoryId intval($_GET[category_id] ?? 0); if ($categoryId 0) { $this-fail(参数错误); } // 从缓存读取减少数据库压力 $cacheKey questions:{$categoryId}; $list redis()-get($cacheKey); if (!$list) { $sql SELECT id, question, option_a, option_b, option_c, option_d FROM question_bank WHERE category_id ? AND status 1 ORDER BY RAND() LIMIT 10; $list Db::query($sql, [$categoryId]); redis()-setex($cacheKey, 3600, json_encode($list)); } // 注意永远不要把 answer 字段返回给前端 return json_encode([code 0, data $list]); }这里有个关键安全点get_questions返回的 SQL 查询中没有查询answer字段。正确答案只在前端本地暂存提交答案时后端再根据question_id从数据库取答案比对。如果直接把answer放进返回包抓包工具比如 Charles 或 Burp Suite 抓 PC 端微信小程序数据包可以直接看到正确答案题库瞬间失去价值。这个接口中我加了 Redis 缓存因为高频请求题目时 MySQL 容易成为瓶颈微信小程序冷启动时会并发拉题目缓存能明显降低响应时间。3. 核心功能实现自定义题库、答题卡与复活卡机制3.1 自定义题库的导入与存储策略这套源码“支持用户自定义题库”不是一个概念而是有实际导入通道。Web 管理后台里你会发现「题库管理」菜单支持两种方式单题添加和 Excel 批量导入。批量导入的 Excel 列顺序必须与数据库字段一一对应题干、选项A、选项B、选项C、选项D、正确答案、分类、难度。我一般会在导入前先跑一个校验脚本# pip install pandas openpyxl import pandas as pd df pd.read_excel(questions.xlsx) required_cols [题目, 选项A, 选项B, 选项C, 选项D, 答案] for col in required_cols: if col not in df.columns: raise SystemExit(f缺少列: {col}) for i, row in df.iterrows(): ans str(row[答案]).strip().upper() if ans not in [A, B, C, D]: print(f第{i2}行答案格式错误: {row[答案]})导入阶段最容易出问题的不是格式而是编码。从网上下载的题库大多是 GBK 编码导入前要转成 UTF-8。如果管理后台是 AdminLTE layui 做的它的上传接口只处理了 UTF-8直接扔 GBK 文件进去你会看到乱码题目。常见做法是先用 Python 脚本统一转码再走后台导入。另外题库总量建议控制在 1 万条以内MySQL 单表查ORDER BY RAND()在几千条数据时没问题上了几万条后会明显变慢。如果题库量级大建议改成先SELECT id FROM question_bank WHERE category_id? ORDER BY RAND() LIMIT 10再按 id 取题目或者用TABLESAMPLE之类的近似随机算法。3.2 答题卡状态机与计分规则答题卡不是一张静态 UI它代表一次答题会话的完整生命周期。这张卡需要记录已答题目、未答题目、当前得分、剩余复活卡数。在小程序端我用一个本地状态对象管理// pages/quiz/quiz.js 中的核心状态 const quizState { paperId: null, // 本次答题会话ID total: 10, // 总题数 currentIndex: 0, // 当前题号 answers: [], // 记录用户选择 [A, null, C, ...] status: pending, // pending | active | paused | finished revivals: 1, // 本次可用复活卡数 score: 0 }; // 点击选项时更新状态 function selectOption(optionKey) { if (quizState.status ! active) return; const idx quizState.currentIndex; quizState.answers[idx] optionKey; // 记录选择 // 此页面暂不提交等点击下一题时统一提交 this.setData({ canGoNext: true, selectedOption: optionKey }); } // 下一题时提交当前题答案 function goNext() { const currentAnswer quizState.answers[quizState.currentIndex]; wx.request({ url: ${app.globalData.baseUrl}/submit_answer, data: { paper_id: quizState.paperId, question_id: questionList[quizState.currentIndex].id, user_answer: currentAnswer }, success: (res) { if (res.data.code 0) { // 后端返回该题是否正确 if (res.data.is_correct) quizState.score; } } }); quizState.currentIndex; }这套逻辑的要点是答案不是“当前答题进度”的整体提交而是每答一题就向后端发送一次后端立刻判分并返回。这样有两个好处第一用户意外退出小程序时已答的题目结果不会丢重新进入可以通过paper_id恢复答题卡第二后端可以做每道题的正确率统计为后续智能化错题重练做准备。注意submit_answer接口需要做防重复提交——在answer_log里对(uid, paper_id, question_id)加唯一索引否则用户连点两次下一题同一道题会被记两次分数。3.3 复活卡与激励视频的联动复活卡是这个源码里最有商业潜力的设计用户答错题后如果还有复活卡可以选择“用复活卡复活”相当于跳过这道错题继续答下一题。复活卡的来源有两个新用户登录礼包赠送以及观看激励视频领取。每次复活卡的消耗小程序端需要调用后端接口public function use_revival() { $uid $this-checkLogin(); // 先查用户复活卡数量 $cardNum Db::query(SELECT revival_count FROM users WHERE uid ?, [$uid]); if ($cardNum[0][revival_count] 0) { $this-fail(复活卡不足); } // 原子扣减 Db::transaction(function() use ($uid) { Db::execute(UPDATE users SET revival_count revival_count - 1 WHERE uid ?, [$uid]); Db::execute(INSERT INTO revival_log (uid, type, create_time) VALUES (?, use, NOW()), [$uid]); }); return json_encode([code 0, message 复活成功]); }这里不能先查再更新因为在并发场景下两个请求同时查到revival_count 1然后各自扣减结果就变成 -1。正确做法是用UPDATE users SET revival_count revival_count - 1 WHERE uid ? AND revival_count 0这条 SQL 自带原子性不必开事务。激励视频领取复活卡的流程则是小程序端先播放广告广告回调成功后由服务端生成一个video_reward_token前端拿这个 token 去换复活卡。不能在小程序端直接调wx.createRewardedVideoAd后就立刻减少库存必须等服务端校验。4. 奖励体系与排行榜从激励视频到兑换闭环4.1 在线奖励与登录礼包发放在线奖励是最容易被人忽略的坑。题目描述“支持在线奖励激励视频领取在线奖励”实际实现时你不能让小程序端自己判断“用户在线了 30 秒”然后调接口发钱。常见的做法是后端记录用户进入答题页面的时间起点离开时计算时长或者由前端在答题完成后上报一个“本次答题耗时”。后端判断耗时时长达到阈值比如 60 秒才发放在线奖励。代码逻辑如下public function online_reward() { $uid $this-checkLogin(); $duration intval($_POST[duration]); $todayStart date(Y-m-d 00:00:00); $todayEnd date(Y-m-d 23:59:59); // 检查今天是否已经领取过 $hasGot Db::query(SELECT id FROM reward_log WHERE uid? AND type1 AND create_time BETWEEN ? AND ?, [$uid, $todayStart, $todayEnd]); if ($hasGot) { $this-fail(今天已领取); } if ($duration 60) { $this-fail(在线时长不足); } // 发放奖励并写流水 Db::transaction(function() use ($uid, $duration) { Db::execute(UPDATE users SET balance balance ? WHERE uid ?, [rand(10,50), $uid]); Db::execute(INSERT INTO reward_log (uid, type, amount, balance, remark) SELECT ?, 1, ?, balance, 在线奖励 FROM users WHERE uid ?, [$uid, $amount, $uid]); }); return json_encode([code 0, message 领取成功]); }这段代码有几个细节前端传入的duration绝不能完全信任恶意用户可以通过修改请求包伪造任意时长。所以我在后端做了一个兜底——从answer_log表里查询该用户今天最近一次答题的paper_id用create_time和当前时间差来校验真实在线时长。更严格的做法是前端每 10 秒上报一次心跳后端只累计心跳时长。另外每日领取限制需要防重上面用“查询今天是否已有记录”再加“reward_log 当天唯一索引”双保险。4.2 激励视频服务端回调验签这套源码接激励视频的方式有两种一种是用微信原生的wx.createRewardedVideoAd另一种是通过穿山甲等广告聚合平台。这里以微信原生激励视频为例后端不能信任前端发的“播放完成”回调要拿到微信服务器下发的videoAdId和transId去验签。微信官方推荐的做法是前端在onClose回调里判断res.isEnded但isEnded同样可以伪造。对于有真实奖励价值的场景应该使用服务端回调校验public function video_callback() { // 微信服务端会带上用户的 openid 和广告事件需要反查微信接口验证 $openid $_POST[openid] ?? ; $transId $_POST[transId] ?? ; $adUnitId $_POST[adUnitId] ?? ; // 请求微信校验接口实际应使用微信API安全校验 $verifyUrl https://api.weixin.qq.com/wxa/verify_video?access_token{$accessToken}; $resp http_post($verifyUrl, [ transId $transId, openid $openid, ad_unit_id $adUnitId ]); $result json_decode($resp, true); if ($result[errcode] 0 $result[valid]) { // 确认有效后发放奖励同样需要防止重复发放 $key video_reward:{$transId}; if (redis()-setnx($key, 1)) { Db::execute(UPDATE users SET balance balance 100 WHERE openid ?, [$openid]); Db::execute(INSERT INTO reward_log (uid, type, amount, remark) VALUES (?, 2, 100, 视频奖励), [$uid]); } } }这里最关键的是transId唯一性每个激励视频回调都携带一个全局唯一的transId后端用SETNX锁住它同一transId重复回调时直接忽略。如果你的项目没有用到 Redis可以在数据库给reward_log增加trans_id唯一索引。很多开发者在测试时漏掉这个验签结果上线后被人用抓包软件反复重放接口余额被刷爆。4.3 排行榜实时更新与缓存排行榜的需求很简单按用户总得分排序显示前 100 名。但实现上如果每次点开排行榜都SELECT * FROM users ORDER BY score DESC LIMIT 100数据库会上来就是慢查询尤其当用户数过万后。常见做法是用 Redis 的有序集合ZSET维护实时分# 每次用户答完题后端更新其得分 redis-cli ZADD quiz_rank 1250 user_12345 # 取出前100名 redis-cli ZREVRANGE quiz_rank 0 99 WITHSCORESZREVRANGE的时间复杂度是 O(log(N)M)性能远胜数据库排序。这套源码的排行榜如果没做缓存你自己加上这一层很值得。需要注意的是排行榜上的分数应当只记录“有效答题得分”即排除了复活卡跳过的题目。如果你直接把users表里的积分字段拿来排序那些用复活卡逃避错题的用户反而排名更高这会让真实答题用户失去竞争动力。我通常的做法是建一张rank_score字段只有后端判题为“正确”的题目才增加这个字段复活和使用道具都不影响。排行榜数据同时会出现在小程序首页和管理后台。管理后台的排行榜因为数据量不大可以直接查 MySQL小程序端必须走缓存接口。缓存更新策略可以在每次答题提交时同步更新 Redis也可以接受 5 分钟延迟用定时任务从answer_log聚合后全量重建 ZSET。后者实现更简单也不容易出错。5. 部署与二次开发从 Nginx 配置到小程序发布避坑5.1 环境初始化与目录权限部署这套源码我强烈建议直接用宝塔面板或 LNMP 一键包省去编译环境的痛苦。你需要 PHP 7.2推荐 7.4经过两年验证最稳定、MySQL 5.6推荐 5.7、Nginx 1.18。上传源码后首先确认几个关键目录的写权限chmod -R 755 /www/wwwroot/quiz/ chown -R www:www /www/wwwroot/quiz/storage chown -R www:www /www/wwwroot/quiz/upload chmod -R 644 /www/wwwroot/quiz/application/config.php源码里的install目录在安装完成后必须删除否则别人访问你的域名/install就能重装系统把你的数据库配置清空。另外PHP 需要开启pdo_mysql、curl、fileinfo扩展这两个是微信接口交互的刚需。如果管理后台图形验证码加载不出来检查gd扩展是否安装。5.2 Nginx 伪静态与 PHP 常量调整小程序端请求https://api.你的域名.com/index.php?actionxxx这种写法虽然能用但不够优雅。建议在 Nginx 里配置伪静态让 URL 更简短server { listen 443 ssl; server_name api.你的域名.com; root /www/wwwroot/quiz/public; index index.php; location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s$1 last; } } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } # 上传文件大小限制题库Excel可能较大 client_max_body_size 20m; }client_max_body_size一定要设置不然导入稍大的 Excel 题库时会直接返回 413。另外要在 PHP 的php.ini中调整upload_max_filesize和post_max_size都为 20M同时确保memory_limit不小于 128M否则解析题库时内存溢出。5.3 小程序端域名白名单与上线检查微信公众平台后台的「开发管理 - 开发设置 - 服务器域名」中所有 request 合法域名必须为 HTTPS 且已备案。如果后端用的 IP 访问测试记得在开发者工具中勾选「不校验合法域名」。但上线前务必改成正式域名否则真机预览时所有请求都会失败。另外这套源码如果是基于非 uniapp 的原生微信小程序开发代码里直接调用wx.request如果你拿到的是uniapp微信小程序版本请求方式要改成uni.request而且底层的 URL 拼接逻辑略有不同。这里很容易踩坑原生小程序的wx.request默认没有重试机制网络慢时用户会看到加载失败提示。上线前建议用微信开发者工具自带的「体验评分」和「代码质量」检查关注一下wx:key是否有设置、页面层级是否过深。答题页面如果依赖setData频繁更新当前题号每次setData传的数据量要控制不要把整张题库列表循环渲染只渲染当前题目和答题卡缩略图。5.4 常见坑时间字段、并发扣减、激励视频回调这三块是开发者实际运营中问得最多的。时间字段上MySQL 5.6 默认的datetime范围只有到 2038 年如果做周年庆活动要预测两年后建议直接改成timestamp或bigint存毫秒。并发扣减我已经在前面提过核心是永远不要“先查后写”要写UPDATE ... WHERE 条件带限制。激励视频回调这块很多人把 access_token 的获取逻辑写死在每次回调里结果访问太频繁触发微信接口频率限制。正确做法是把 access_token 缓存起来用定时任务或请求时判断过期时间function getAccessToken() { $cacheFile /tmp/wechat_access_token.log; $ttl 7000; // 提前200秒过期留足余量 if (file_exists($cacheFile) time()-filemtime($cacheFile) $ttl) { return file_get_contents($cacheFile); } $url https://api.weixin.qq.com/cgi-bin/token?grant_typeclient_credentialappid{$appid}secret{$secret}; $res json_decode(file_get_contents($url), true); file_put_contents($cacheFile, $res[access_token]); return $res[access_token]; }这道题的面试官如果问“access_token 怎么管理”上面的代码就是标准答案。如果你是抓包调试微信小程序用 Charles 或 Burp Suite 抓 PC 端微信小程序请求时注意微信小程序的请求默认走HTTP/2Charles 需要安装根证书并开启 SSL Proxying否则只能看到 CONNECT 请求看不到实际内容。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →