答题流量主小程序源码+后台题库管理系统源码

效果演示

源码下载:

链接: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');
相关推荐
陈思杰系统思考Jason1 天前
系统思考:团队学习的误区
百度·微信·微信公众平台·新浪微博·微信开放平台
个微管理5 天前
告别多手机切换烦恼,无需下载安装软件的CRM管理系统
微信·智能手机·自动化·微信开放平台
陈思杰系统思考Jason5 天前
系统思考与科学决策
百度·微信·微信公众平台·新浪微博·微信开放平台
chao1898447 天前
基于C# WinForm实现的仿微信打飞机游戏
游戏·微信·c#
陈思杰系统思考Jason8 天前
系统思考:基本功在快速变化中的重要性
百度·微信·微信公众平台·新浪微博·微信开放平台
陈思杰系统思考Jason9 天前
系统思考:结构性重复
百度·微信·微信公众平台·新浪微博·微信开放平台
陈思杰系统思考Jason10 天前
企业经营误区:被动应激与主动经营
百度·微信·微信公众平台·新浪微博·微信开放平台
wan55cn@126.com11 天前
人类文明可通过技术手段(如加强航天器防护、改进电网设计)缓解地球两极反转带来的影响
人工智能·笔记·搜索引擎·百度·微信