效果演示
源码下载:
链接:https://pan.xunlei.com/s/VOh3tXuI4D8SqYtdNMfUUwv6A1?pwd=kchg# 复制这段内容后打开「手机迅雷 App」即可获取。无需下载在线查看,视频原画享倍速播放
功能演示效果:




一、技术栈选型逻辑:轻量适配+易扩展优先
选型的核心原则是「稳定优先、降低二开成本」,毕竟后续可能需要根据用户需求新增题型、扩展社交功能。
后端选用PHP 7.4+Laravel 8.x,核心原因是Laravel的生态足够成熟:Eloquent ORM简化数据库操作,路由和中间件机制能快速实现接口权限控制,Service层封装便于后续功能迭代;前端坚持原生微信小程序(WXML+WXSS+JS)+ WeUI组件库,既能完美适配微信生态的交互规范,避免第三方框架过度封装导致的兼容性问题,又能保证页面渲染性能,尤其是答题过程中不会出现卡顿、延迟。
辅助技术栈仅保留刚需:Redis 6.0用于缓存高频访问数据(如热门题库、用户积分、排行榜),减少数据库压力;腾讯云COS存储题库图片、用户头像等静态资源,适配微信小程序的就近访问策略,提升资源加载速度;EasyWeChat封装微信官方API,简化登录、用户信息获取等功能的对接流程,减少重复开发。
二、核心功能实现:全链路技术拆解
1. 答题核心流程:从题库加载到结果校验的闭环
答题流程是小程序的核心,重点要解决「题库高效加载」「答题状态同步」「结果实时校验」三个问题。
前端侧:用户选择答题分类后,前端发起接口请求,携带分类ID和用户ID;为了提升体验,做「预加载机制」------当前答题组答到最后3题时,自动预加载下一组题目,避免用户答题间隙出现加载等待;答题过程中,实时本地缓存用户的答题选择(用wx.setStorageSync),防止意外退出导致答题进度丢失。
前端核心代码(答题加载与缓存):
// 加载题目列表
loadQuestions(categoryId) {
wx.showLoading({ title: '加载题目中...' });
wx.request({
url: `${app.globalData.baseUrl}/api/question/list`,
method: 'POST',
data: {
category_id: categoryId,
user_id: app.globalData.userId // 全局存储的用户ID
},
success: (res) => {
wx.hideLoading();
if (res.data.code === 200) {
this.setData({
questions: res.data.data,
currentQuestionIndex: 0 // 重置当前答题索引
});
// 预加载下一组题目(当前组剩余3题时触发)
this.preloadNextQuestions(categoryId, res.data.data.length);
} else {
wx.showToast({ title: res.data.msg, icon: 'none' });
}
},
fail: () => {
wx.hideLoading();
wx.showToast({ title: '题目加载失败', icon: 'none' });
}
});
},
// 预加载下一组题目
preloadNextQuestions(categoryId, currentLength) {
this.setData({
preloadTrigger: (currentLength - this.data.currentQuestionIndex) <= 3
});
if (this.data.preloadTrigger && !this.data.isPreloading) {
this.setData({ isPreloading: true });
wx.request({
url: `${app.globalData.baseUrl}/api/question/list`,
method: 'POST',
data: { category_id: categoryId, user_id: app.globalData.userId, page: this.data.page + 1 },
success: (res) => {
if (res.data.code === 200) {
this.setData({
nextQuestions: res.data.data,
page: this.data.page + 1,
isPreloading: false
});
}
}
});
}
},
// 存储用户答题选择
saveAnswer(questionId, answer) {
let userAnswers = wx.getStorageSync('userAnswers') || {};
userAnswers[questionId] = answer;
wx.setStorageSync('userAnswers', userAnswers); // 本地缓存答题记录
}
后端侧(PHP核心):接收请求后,先从Redis缓存中查询对应分类的题库,若缓存失效,再从数据库查询,查询后同步存入Redis(设置1小时过期时间,平衡实时性和性能);题库返回时做「乱序处理」------用Laravel的collection打乱题目顺序,同时保证选项随机(避免用户记答案作弊);用户提交答题结果后,后端用Service层封装校验逻辑,遍历用户答题列表,与正确答案比对,计算得分和正确率,同时更新用户积分,整个校验过程用事务包裹,确保数据一致性。
后端核心代码(题库加载与答题校验):
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Services\QuestionService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redis;
class QuestionController extends Controller
{
protected $questionService;
public function __construct(QuestionService $questionService)
{
$this->questionService = $questionService;
}
// 加载题目列表(含Redis缓存)
public function getList(Request $request)
{
$categoryId = $request->input('category_id');
$userId = $request->input('user_id');
$page = $request->input('page', 1);
$cacheKey = "question:list:{$categoryId}:{$page}";
// 先查Redis缓存
$cacheData = Redis::get($cacheKey);
if ($cacheData) {
return $this->success(json_decode($cacheData, true));
}
// 缓存失效,查询数据库并乱序处理
$questions = $this->questionService->getQuestionList($categoryId, $page);
// 题目乱序(避免固定顺序作弊)
$questions = $questions->shuffle()->values()->toArray();
// 选项乱序(单选/多选题)
foreach ($questions as &$question) {
$options = json_decode($question['options'], true);
shuffle($options);
$question['options'] = json_encode($options);
}
// 存入Redis,设置1小时过期
Redis::setex($cacheKey, 3600, json_encode($questions));
return $this->success($questions);
}
// 答题结果校验
public function checkAnswer(Request $request)
{
$userId = $request->input('user_id');
$answerList = $request->input('answer_list'); // 前端传的答题列表:[{question_id:1,answer:"A"},...]
// 事务包裹,确保数据一致性
\DB::beginTransaction();
try {
$result = $this->questionService->checkAnswer($userId, $answerList);
\DB::commit();
return $this->success($result);
} catch (\Exception $e) {
\DB::rollBack();
return $this->error($e->getMessage());
}
}
}
// QuestionService核心方法(校验逻辑)
namespace App\Services;
use App\Models\Question;
use App\Models\UserScore;
class QuestionService
{
public function checkAnswer($userId, $answerList)
{
$totalScore = 0;
$totalCount = count($answerList);
$correctCount = 0;
foreach ($answerList as $item) {
$question = Question::find($item['question_id']);
if (!$question) continue;
$correctAnswer = $question->answer;
$userAnswer = $item['answer'];
// 多选题答案排序后比对(避免用户选择顺序影响结果)
if ($question->type == 2) {
$correctArr = explode(',', $correctAnswer);
sort($correctArr);
$userArr = explode(',', $userAnswer);
sort($userArr);
$isCorrect = $correctArr === $userArr;
} else {
$isCorrect = $correctAnswer === $userAnswer;
}
if ($isCorrect) {
$correctCount++;
$totalScore += $question->score;
}
}
// 更新用户积分
UserScore::updateOrCreate(
['user_id' => $userId],
['total_score' => \DB::raw("total_score + {$totalScore}"), 'update_time' => now()]
);
return [
'total_count' => $totalCount,
'correct_count' => $correctCount,
'total_score' => $totalScore,
'accuracy' => $totalCount > 0 ? round(($correctCount/$totalCount)*100, 2) : 0
];
}
}
特殊场景处理:针对多选题的复杂校验,后端用数组交集逻辑判断用户选择是否与正确答案完全匹配;对于超时未提交的答题,前端设置倒计时,超时后自动提交,后端同步接收并按未答处理,积分计0。
2. 自定义题库模块:系统+用户双模式的灵活实现
核心需求是支持「系统自带题库直接使用」和「用户自主添加/批量导入」,且两者逻辑隔离、互不干扰。
前端侧:设计两种添加入口------单题添加用表单组件,做严格的格式校验(如题目不能为空、选项数量匹配题型:单选2-4个选项,多选至少2个选项、答案需与选项对应);批量导入对接PHPExcel插件,前端先解析Excel文件,将题目、选项、答案等信息转换成统一的JSON格式,先在前端完成格式校验(如排除空行、校验答案格式),再批量提交后端,减少无效请求。
后端侧:接收前端数据后,用Laravel的事务机制处理批量导入,确保要么全部成功,要么全部失败,避免部分题目插入异常导致的题库混乱;添加「重复题去重」逻辑------对题目内容做MD5加密,与数据库中已有题目比对,相同题目直接过滤,避免冗余;通过「is_system」标识区分系统题库和用户自定义题库,查询时通过条件筛选:用户端仅能查看「系统题库+当前用户自定义题库」,管理端可查看全量题库,便于审核和管理。
3. 用户激励体系:登录礼包+积分+碎片合成的留存设计
激励体系的核心是提升用户活跃度和留存率,技术实现重点是「规则明确、数据准确、避免作弊」。
登录礼包:用「Redis缓存+数据库记录」的组合实现。用户登录时,前端调用登录接口,后端先查询Redis中「用户今日登录标识」(key为user:login:用户ID,value为1),若不存在,则触发礼包发放逻辑,给用户增加积分奖励,同时将标识存入Redis,设置过期时间为当天24点,确保每日仅能领取一次;数据库同步记录登录日志,包含登录时间、领取礼包状态,便于后续数据统计。
后端核心代码(登录礼包实现):
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\UserLoginLog;
use App\Models\UserScore;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redis;
use EasyWeChat\Factory;
class AuthController extends Controller
{
// 微信小程序登录+登录礼包发放
public function login(Request $request)
{
$code = $request->input('code');
$app = Factory::miniProgram(config('wechat.mini_program.default'));
// 调用微信接口获取openid
$res = $app->auth->session($code);
if (isset($res['errcode'])) {
return $this->error('登录失败:' . $res['errmsg']);
}
$openid = $res['openid'];
// 查询或创建用户
$user = User::firstOrCreate(['openid' => $openid]);
$userId = $user->id;
// 登录礼包发放逻辑
$cacheKey = "user:login:{$userId}";
$hasLoginToday = Redis::get($cacheKey);
$giftScore = 10; // 登录礼包积分
$isGetGift = false;
if (!$hasLoginToday) {
// 发放积分奖励
UserScore::updateOrCreate(
['user_id' => $userId],
['total_score' => \DB::raw("total_score + {$giftScore}"), 'update_time' => now()]
);
// 记录登录标识,设置当天24点过期
$expireTime = strtotime(date('Y-m-d 23:59:59')) - time();
Redis::setex($cacheKey, $expireTime, 1);
// 记录登录日志
UserLoginLog::create([
'user_id' => $userId,
'login_time' => now(),
'is_get_gift' => 1,
'gift_score' => $giftScore
]);
$isGetGift = true;
} else {
// 已登录过,仅记录日志
UserLoginLog::create([
'user_id' => $userId,
'login_time' => now(),
'is_get_gift' => 0
]);
}
return $this->success([
'user_id' => $userId,
'nickname' => $user->nickname,
'is_get_gift' => $isGetGift,
'gift_score' => $isGetGift ? $giftScore : 0
]);
}
}
积分体系:后端用Service层封装积分增减逻辑,明确积分获取渠道(答题得分、每日登录、完成任务)和消耗场景(兑换奖励、参与特殊答题活动),所有积分变动都记录日志,包含变动类型、数量、时间、操作人,便于追溯;前端实时同步展示用户当前积分和积分明细,提升透明度。
碎片合成:给奖励设置「碎片属性」(如10个碎片可合成对应奖励),前端展示碎片数量、合成进度和所需碎片数;后端封装碎片合成逻辑,用户发起合成请求后,先校验碎片数量是否满足要求,满足则扣减对应碎片数量,生成奖励记录,更新用户账户信息;用数据库事务保证碎片扣减和奖励生成的原子性,避免出现碎片扣了但奖励未到账的问题。
4. 社交互动功能:好友招募+排行榜的落地逻辑
社交功能是提升用户粘性的关键,核心实现逻辑围绕「关系绑定」和「数据排序」展开。
好友招募:生成专属邀请码(规则:用户ID+6位随机字符串),用户分享邀请链接或邀请码给好友,好友通过邀请码注册并完成首次答题后,前端触发邀请成功回调,后端检测到邀请关系(通过邀请码解析出邀请人ID),给邀请人增加积分奖励;用Redis的Set结构存储邀请关系,key为invite:邀请人ID,value为被邀请人ID列表,方便统计邀请人数和发放对应奖励,同时数据库记录邀请日志,包含邀请时间、被邀请人信息、奖励发放状态。
排行榜:按用户累计积分或答题正确率排序,用Redis的ZSet结构实现高效排序。用户答题得分后,后端调用Redis的ZADD命令更新用户积分排序;查询排行榜时,用ZREVRANGE命令获取前100名用户信息(包含用户ID、昵称、头像、积分),设置5分钟缓存更新周期,避免实时排序消耗过多服务器资源;前端展示排行榜Top10,同时显示当前用户的排名,增加用户的攀比心理,提升答题积极性;为了避免排名波动过大,可设置「每日重置」或「每周重置」规则,后端用定时任务(Laravel Scheduler)实现排行榜数据重置和历史排名记录。
核心代码(Redis ZSet实现排行榜):
<?php
namespace App\Services;
use App\Models\User;
use App\Models\UserScore;
use Illuminate\Support\Facades\Redis;
class RankService
{
// 排行榜缓存key(按日重置,后缀为日期)
private function getRankKey()
{
return "rank:score:" . date('Ymd');
}
// 更新用户排行榜积分
public function updateUserRank($userId, $score)
{
$rankKey = $this->getRankKey();
// ZADD:将用户ID加入有序集合,分数为总积分
Redis::zadd($rankKey, $score, $userId);
// 只保留前1000名,减少内存占用
Redis::zremrangebyrank($rankKey, 0, -1001);
}
// 获取排行榜列表(前100名)
public function getRankList()
{
$rankKey = $this->getRankKey();
$cacheKey = "rank:list:cache";
// 先查缓存(5分钟过期)
$cacheData = Redis::get($cacheKey);
if ($cacheData) {
return json_decode($cacheData, true);
}
// ZREVRANGE:按分数倒序取前100名,withscores获取分数
$rankData = Redis::zrevrange($rankKey, 0, 99, 'withscores');
$rankList = [];
$rank = 1;
foreach ($rankData as $userId => $score) {
$user = User::find($userId, ['id', 'nickname', 'avatar']);
if ($user) {
$rankList[] = [
'rank' => $rank,
'user_id' => $userId,
'nickname' => $user->nickname,
'avatar' => $user->avatar,
'score' => (int)$score
];
$rank++;
}
}
// 存入缓存
Redis::setex($cacheKey, 300, json_encode($rankList));
return $rankList;
}
// 获取当前用户排名
public function getUserRank($userId)
{
$rankKey = $this->getRankKey();
// ZREVRANK:获取用户倒序排名(从0开始,+1为实际排名)
$rank = Redis::zrevrank($rankKey, $userId);
return $rank !== false ? $rank + 1 : '未上榜';
}
}
// 定时任务:每日重置排行榜(Laravel Scheduler)
// app/Console/Kernel.php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
protected function schedule(Schedule $schedule)
{
// 每日0点重置排行榜(删除前一天的有序集合)
$schedule->call(function () {
$yesterdayKey = "rank:score:" . date('Ymd', strtotime('-1 day'));
Redis::del($yesterdayKey);
})->dailyAt('00:00:00');
}
}
前端核心代码(排行榜展示):
// 获取排行榜数据
getRankList() {
wx.showLoading({ title: '加载排行榜中...' });
wx.request({
url: `${app.globalData.baseUrl}/api/rank/list`,
success: (res) => {
wx.hideLoading();
if (res.data.code === 200) {
this.setData({ rankList: res.data.data });
// 获取当前用户排名
this.getUserRank();
} else {
wx.showToast({ title: res.data.msg, icon: 'none' });
}
}
});
},
// 获取当前用户排名
getUserRank() {
wx.request({
url: `${app.globalData.baseUrl}/api/rank/user`,
data: { user_id: app.globalData.userId },
success: (res) => {
if (res.data.code === 200) {
this.setData({ myRank: res.data.data });
}
}
});
}
5. 自定义化功能:第三方引流+底部菜单的灵活配置
这类功能的核心是提升小程序的扩展性,方便后续根据需求调整,无需频繁修改源码重新提交审核。
第三方小程序引流:后端设计配置表,存储第三方小程序的appid、跳转路径、展示文案、图标等信息;前端从接口拉取配置数据,动态渲染引流入口(如首页banner、答题完成后弹窗);用户点击时,前端调用微信小程序的wx.navigateToMiniProgram接口跳转,后端同步记录跳转日志(包含跳转时间、用户ID、目标小程序信息),便于统计引流效果;配置表支持后台动态修改,无需改动前后端代码。
自定义底部菜单:后端设计菜单配置表,存储菜单名称、图标路径、跳转页面路径、排序序号、是否显示等信息;前端启动时拉取配置数据,用微信小程序的wx.setTabBarItem接口动态渲染底部tabBar;支持后台修改菜单配置(如新增菜单、调整顺序、隐藏菜单),前端实时同步更新,无需重新发布小程序。
核心代码(自定义底部菜单):
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\TabBarConfig;
class TabBarController extends Controller
{
// 获取底部菜单配置
public function getConfig()
{
// 查询启用的菜单,按排序序号排序
$config = TabBarConfig::where('is_show', 1)
->orderBy('sort', 'asc')
->get(['name', 'icon_path', 'selected_icon_path', 'page_path'])
->toArray();
return $this->success($config);
}
}
// 小程序启动时加载自定义底部菜单
onLaunch() {
this.loadTabBarConfig();
},
// 加载底部菜单配置并动态渲染
loadTabBarConfig() {
wx.request({
url: `${app.globalData.baseUrl}/api/tabbar/config`,
success: (res) => {
if (res.data.code === 200) {
const tabBarList = res.data.data;
// 设置tabBar整体配置
wx.setTabBarStyle({
color: '#666',
selectedColor: '#ff4444',
backgroundColor: '#fff'
});
// 动态设置每个tabBarItem
tabBarList.forEach((item, index) => {
wx.setTabBarItem({
index: index,
text: item.name,
iconPath: item.icon_path,
selectedIconPath: item.selected_icon_path,
pagePath: item.page_path
});
});
// 存储菜单配置到全局
app.globalData.tabBarList = tabBarList;
}
}
});
}
三、二开优化与性能保障要点
1. 二开友好性设计
将核心业务逻辑封装成独立的Service类(如QuestionService、RewardService、SocialService),每个Service对应一个功能模块,后续修改功能只需调整对应Service,无需改动路由和控制器;接口采用RESTful规范,统一返回JSON格式,约定错误码体系(如1001=参数错误,1002=权限不足,1003=答题超时),前端对接更清晰;数据库设计预留扩展字段,如用户表增加ext_info字段(JSON格式),用于存储后续新增的用户属性,避免频繁修改表结构。
2. 性能优化策略
除了Redis缓存高频数据,还可给高频查询接口(如排行榜、题库列表)添加Laravel的缓存中间件,设置合理的缓存过期时间;静态资源(如题库图片、菜单图标)放腾讯云COS并开启CDN加速,减少资源加载时间;数据库层面,给高频查询字段(如题库分类ID、用户ID)建立索引,优化查询SQL,避免全表扫描;针对流量高峰,可配置Nginx负载均衡,分散服务器压力,确保小程序稳定运行。
核心代码(Laravel缓存中间件使用):
<?php
// app/Http/Kernel.php 注册缓存中间件
protected $routeMiddleware = [
// 自定义缓存中间件
'api.cache' => \App\Http\Middleware\ApiCacheMiddleware::class,
];
// 自定义缓存中间件:app/Http/Middleware/ApiCacheMiddleware.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Redis;
class ApiCacheMiddleware
{
public function handle($request, Closure $next, $expire = 300)
{
// 只对GET请求缓存
if ($request->method() !== 'GET') {
return $next($request);
}
// 生成唯一缓存key(请求地址+参数)
$cacheKey = 'api:cache:' . md5($request->fullUrl());
$cacheData = Redis::get($cacheKey);
if ($cacheData) {
// 缓存存在,直接返回
return response()->json(json_decode($cacheData, true));
}
// 缓存不存在,执行后续逻辑
$response = $next($request);
$responseData = $response->original;
// 只缓存成功的响应
if ($responseData['code'] === 200) {
Redis::setex($cacheKey, $expire, json_encode($responseData));
}
return $response;
}
}
// 路由中使用缓存中间件(例:排行榜接口缓存5分钟)
// routes/api.php
Route::get('/rank/list', [RankController::class, 'getList'])->middleware('api.cache:300');