
简介一款面向短视频运营场景的抖音点赞任务系统源码附带大转盘抽奖机器人模块站长实测可正常部署运行。系统基于ThinkPHP框架开发适配宝塔面板、Nginx 1.18、PHP 7.0、MySQL 5.6环境内置后台管理、前台登录、任务发布与抽奖激励流程适合自媒体运营者、站长以及PHP开发者快速搭建或二次开发。压缩包约330.56MB以PHP源码、数据库配置文件/Application/Common/db.php及部署说明为主解压后按ThinkPHP伪静态规则配置即可访问后台可使用资源附带的超管及前台测试账号体验完整功能资源还包含全新UI与大转盘机器人模块可衔接区块链积分激励玩法扩展为抽奖型短视频任务平台。目前已有527人学习/下载项目结构清晰适合作为抖音引流类系统的开发参考。1. 这个标题背后是两套系统点赞任务派发 直播间抽奖机器人这个 zip 名字里其实装了三个东西一套能发单、派单、审核、结算的抖音点赞任务系统一台进直播间读弹幕、定时开奖的大转盘机器人以及一套声称重新设计过的 UI 前端。任务系统解决的是“任务怎么发布、谁做了、怎么证明、钱怎么算”机器人解决的是“直播间热度怎么维持、奖品发给谁”两者共用同一份 MySQL 和 Redis用任务 ID 和直播间房间号把业务串起来。适合直播公会、MCN 运营团队自建也适合接私活时当源码交付物。2. 任务系统的数据模型任务状态机、扣量并发与结算流水2.1 点赞任务系统的核心表设计任务系统的本质是“发单—接单—交单—审核—结算”五步闭环。用户端是 H5 页面和抖音 App 内嵌页管理端是 PC 后台所以数据库要把任务实例和领取记录拆开任务实例存可领取份数领取记录存每一次真实领取。常见做法是biz_task主表 biz_task_order流水表CREATE TABLE biz_task ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, task_title VARCHAR(100) NOT NULL DEFAULT COMMENT 任务标题如给指定视频点赞, target_url VARCHAR(500) NOT NULL DEFAULT COMMENT 目标作品链接或口令, total_num INT NOT NULL DEFAULT 0 COMMENT 总任务份数, processed_num INT NOT NULL DEFAULT 0 COMMENT 已领取份数, success_num INT NOT NULL DEFAULT 0 COMMENT 已审核通过份数, reward DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 完成单价, start_time DATETIME NOT NULL COMMENT 开始领取时间, end_time DATETIME NOT NULL COMMENT 任务结束时间, status TINYINT NOT NULL DEFAULT 0 COMMENT 0草稿 1投放中 2已满 3已关闭 4已结算, created_at DATETIME NOT NULL, PRIMARY KEY (id), KEY idx_status_time (status, start_time, end_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT任务主表; CREATE TABLE biz_task_order ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, order_sn VARCHAR(32) NOT NULL COMMENT 订单号唯一, task_id INT UNSIGNED NOT NULL, uid INT UNSIGNED NOT NULL COMMENT 接单用户ID, dy_account VARCHAR(100) NOT NULL DEFAULT COMMENT 用户填写的抖音号, screenshot_url VARCHAR(500) NOT NULL DEFAULT COMMENT 点赞完成截图地址, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待提交 1待审核 2通过 3驳回, audit_msg VARCHAR(200) NOT NULL DEFAULT COMMENT 驳回原因, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_sn (order_sn), KEY idx_task_status (task_id, status), KEY idx_uid (uid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT接单流水表;逻辑说明processed_num表示已经发出多少份success_num表示审核通过多少份用户能领取的前提是processed_num total_num且当前时间落在start_time和end_time之间。order_sn用唯一索引后续写余额流水、提现单都带着它这是对账的锚点。参数说明target_url建议同时存两种值能直接打开的https://v.douyin.com/xxxx/短链以及用户复制出来的文字口令。短链给“去点赞”按钮跳转口令给手动操作的兜底路径字段留 500 长度是因为部分带参数的分享链接会超过 255。2.2 领取任务必须做原子扣减避免超卖领取任务时最常见的错误是先 SELECT 再 UPDATE。两个人同时读到processed_num99、total_num100两个都执行 UPDATE最后变成 101 或者覆盖丢失第 100 单之后还能继续领。用一条带条件的 UPDATE 做原子扣减public function grabTask(int $taskId, int $uid): array { $now date(Y-m-d H:i:s); $this-db-beginTransaction(); try { // 原子扣减只有当前 processed_num total_num 时才允许 1 $affected $this-db-execute( UPDATE biz_task SET processed_num processed_num 1 WHERE id ? AND status 1 AND processed_num total_num AND start_time ? AND end_time ?, [$taskId, $now, $now] ); if ($affected ! 1) { throw new RuntimeException(任务已满或已结束); } // 写领取流水order_sn 用日期 uid 随机串生成 $orderSn date(YmdHis) . sprintf(%06d, $uid) . mt_rand(100000, 999999); $this-db-insert(biz_task_order, [ order_sn $orderSn, task_id $taskId, uid $uid, status 0, created_at $now, updated_at $now, ]); $this-db-commit(); return [order_sn $orderSn]; } catch (Throwable $e) { $this-db-rollBack(); return [error $e-getMessage()]; } }逻辑说明UPDATE ... WHERE processed_num total_num在 InnoDB 下会锁住这一行任务直到事务提交。第二个用户再进来时读到的processed_num已经被加过等于total_num时条件不成立affected0直接判定“已满”。这种方式比先查 Redis 再落库省掉一层一致性问题。参数说明mt_rand(100000, 999999)只是让order_sn更难猜真正防重靠uk_order_sn唯一索引万一撞索引catch 里回滚后重新生成一个单号再试即可不要在前台直接抛 500。任务主表行锁在秒杀场景下会串行化但对每天几千单的任务系统完全够用。2.3 审核通过和提现结算要有状态机审核不是把 status 改成“通过”这样一个动作。用户提交截图后要把订单状态从 1 改成 2给用户余额加钱再把success_num加一。三个动作不放在一个事务里就会出现“钱加了但成功数没加”“任务显示还有富余其实实际已经满了”的脏数据。状态流转要收敛在一个方法里public function auditOrder(int $orderId, bool $pass, string $msg ): bool { $order $this-db-queryRow( SELECT task_id, uid, status FROM biz_task_order WHERE id ? FOR UPDATE, [$orderId] ); if (!$order || (int)$order[status] ! 1) { return false; // 只有待审核状态才允许流转 } if (!$pass) { $this-db-execute( UPDATE biz_task_order SET status 3, audit_msg ? WHERE id ? AND status 1, [$msg, $orderId] ); return true; } $this-db-execute( UPDATE biz_task_order SET status 2, updated_at NOW() WHERE id ? AND status 1, [$orderId] ); $this-db-execute( UPDATE biz_task SET success_num success_num 1 WHERE id ?, [$order[task_id]] ); $this-db-execute( UPDATE biz_user SET balance balance ? WHERE uid ?, [$this-getTaskReward($order[task_id]), $order[uid]] ); return true; }逻辑说明SELECT ... FOR UPDATE锁定订单行再校验状态避免后台双击或两个管理员同时审同一单。一次事务里完成改订单、加成功数、加余额任何一个失败都要回滚否则后台对账时会对不上。参数说明余额字段用 DECIMAL(10,2)不要用 FLOAT。如果平台有小数奖励更推荐以“分”为单位存 INT展示层再除以 100能直接省掉浮点误差带来的对账困扰。提示任务状态机从发布到结算只保留草稿、投放中、已满、已关闭、已结算五个状态。已满是领完自然结束已关闭是运营手动停发已结算之后任何字段不允许修改所有运营调整都走单独流水表留痕。3. 全新 UI 的落地选型Vant 4 任务大厅 抖音内嵌 WebView 兼容3.1 为什么用户端选 Vant 4后台用 vxe-table这类源码标题里的“全新 UI”落地时要覆盖三层移动端 H5、PC 管理后台、抖音 App 内打开 H5 的显示效果。用户端我一般选 Vant 4 Vue 3 ViteVant 对表单、列表、弹窗、步进器都内置了按需引入后首屏 JS 能压到 150KB 以内对抖音 WebView 的加载速度很友好。管理后台则用 vxe-table。任务系统后台的任务列表动辄几千条记录普通 el-table 在无分页渲染时会有明显 UI 卡顿vxe-table 的虚拟滚动配合服务端分页2000 行数据滚动也不会掉帧。这里需要提醒Vant 4 只支持 Vue 3如果拿到手的源码还是 Vue 2 工程先看List、Field、Popup这几个组件的 API升级成本主要在组件属性名上而不是依赖版本本身。3.2 任务大厅列表的懒加载与虚拟滚动任务大厅是用户进来的第一屏每张任务卡都要展示视频封面、标题、单价、剩余份数、结束时间。一次渲染 100 张卡片图片请求和 DOM 节点会把中低端安卓机直接拖垮。用 Vant 的 List 组件做分页配合v-lazy做图片懒加载script setup import { ref, onMounted } from vue import { showToast } from vant import request from /utils/request const list ref([]) const loading ref(false) const finished ref(false) const page ref(1) const onLoad async () { if (loading.value) return loading.value true try { const res await request.get(/api/task/list, { params: { page: page.value, page_size: 20 } }) const records res.data.records || [] list.value.push(...records) page.value 1 if (list.value.length res.data.total) { finished.value true } } finally { loading.value false } } onMounted(() onLoad()) /script template van-list v-model:loadingloading :finishedfinished finished-text没有更多任务了 loadonLoad div v-foritem in list :keyitem.id classtask-card img classtask-cover v-lazyitem.cover_url loadinglazy / div classtask-title{{ item.task_title }}/div div classtask-meta span剩余 {{ item.total_num - item.processed_num }} 份/span span{{ item.reward }} 元/单/span /div /div /van-list /template逻辑说明van-list的load在滚动到底部时自动触发finished置为 true 后不再触发。loading和finished必须由接口数据驱动否则会出现“上一次还没返回又触发一次”的重复请求。图片用v-lazy懒加载加载失败时给一张默认图避免破图占位。参数说明page_size固定 10 到 20移动端单屏大概显示 4 到 5 张卡片20 条的 DOM 量完全可控。任务卡上的“剩余份数”是total_num - processed_num实时算出来的不需要额外在 Redis 里维护一份副本避免两处数据不一致。3.3 抖音内打开 H5 的兼容处理抖音 App 里的 WebView 和系统 WebView 不完全一样常见三个问题localStorage偶发写入失败、分享链接域名被拦截、复制口令后回到 App 页面状态丢失。第一个要靠降级到内存存储第二个要在后台配置可访问域名白名单第三个要监听visibilitychange刷新数据document.addEventListener(visibilitychange, () { if (document.visibilityState visible) { // 从抖音回到当前页面重新拉取任务剩余数量 fetch(/api/task/detail?id${currentTaskId}) .then((res) res.json()) .then((data) { const remainEl document.querySelector(.task-remain) remainEl.textContent 剩余 ${data.processed_num}/${data.total_num} 份 }) } })逻辑说明用户在抖音里点“去点赞”切到抖音视频页再切回来时页面可能已被系统回收过部分状态。visibilitychange在 WebView 回到前台时一定会触发用它重新拉一次最新数据能避免用户看到过期的剩余份数产生误操作。参数说明接口 URL 写相对路径/api/...不要写死域名换服务器不用改前端打包产物同时后端接口响应头加Cache-Control: no-cache否则部分安卓 WebView 会把接口结果缓存住刷新无效。提示抖音 App 内 WebView 对window.open支持不稳定跳转“去点赞”不要新开窗口直接location.href当前页跳转返回时用history.back()并配合pageshow重新拉取任务状态。4. 大转盘机器人的实现muduo 连接层、去重、抽奖算法与定时任务4.1 机器人在整个系统里的位置大转盘机器人独立于任务系统存在但它和任务系统能联动直播间里粉丝完成关注、点赞、评论后机器人从弹幕里抽中一位或几位用户把中奖记录回写到任务后台。这样直播间热度由机器人带动奖品发放又和任务系统的积分结算挂钩。机器人分两层端侧运行在手机或模拟器上负责登录直播间、采集弹幕、发送开奖指令服务端接收弹幕、判定参与条件、执行抽奖算法、回调任务系统。端侧自动化框架在抖音端策略变更很频繁不建议作为主链路依赖更合适当开发期自测工具正式运营时弹幕采集和指令下发要和服务端解耦坏了只坏采集端不影响抽奖逻辑。4.2 用 muduo 组织弹幕长连接服务端要同时维持几百个直播间的长连接C 的事件驱动模型比多线程模型省内存muduo 是这块常见选型。弹幕网关把直播间协议转成内部 TCP 帧muduo 的TcpServer正好处理这一层#include muduo/net/TcpServer.h #include muduo/net/EventLoop.h using namespace muduo; using namespace muduo::net; class DanmakuServer { public: DanmakuServer(EventLoop* loop, const InetAddress addr) : server_(loop, addr, danmaku-server) { server_.setConnectionCallback( std::bind(DanmakuServer::onConnection, this, _1)); server_.setMessageCallback( std::bind(DanmakuServer::onMessage, this, _1, _2, _3)); } void start() { server_.start(); } private: void onConnection(const TcpConnectionPtr conn) { if (conn-connected()) { // 新直播间连接注册分配一个 room_id conn-setContext(assignRoomId()); } else { // 断开后清理该直播间的参与者集合 cleanupRoom(conn); } } void onMessage(const TcpConnectionPtr conn, Buffer* buf, Timestamp time) { std::string msg buf-retrieveAllAsString(); // 协议帧解析从 msg 中提取弹幕内容、用户 ID、发弹幕时间 auto frame parseFrame(msg); if (frame.type kDanmaku) { handleDanmaku(conn, frame); } else if (frame.type kStartLottery) { startLottery(frame.roomId, frame.extra); } } void startLottery(int roomId, const std::string rule) { // 将开奖任务投递到抽奖执行器不阻塞 IO 线程 lotteryExecutor_.submit(roomId, rule); } TcpServer server_; };逻辑说明这里把连接管理和弹幕业务分成两层。onMessage只负责解析协议帧并派发不直接做去重和抽奖弹幕量大时不会阻塞 IO 线程去重、抽奖放到独立执行器里用队列解耦。参数说明连接断开时cleanupRoom(conn)必须做否则用户已经离开直播间参与抽奖的集合里还有他后面开奖会抽到离场的人。muduo 的conn-setContext()适合放连接级状态房间信息放这里比全局 map 查找更快。4.3 抽奖去重用 Redis 集合参与条件实时判定直播间弹幕里真正参与抽奖的只有一批人参与条件至少满足关注了主播、发过指定口令、不在黑名单里。关注状态由端侧定时上报弹幕只作为触发条件不作为参与资格。用 Redis 的 SADD 天然可以解决用户刷屏问题# Ruby 伪代码示意实际项目常用 PHP/Go 调用 Redis def on_danmaku(room_id, user_id, content) key lottery:join:#{room_id}:#{current_round} if content.include?(抽奖) client.sismember(lottery:follow:#{room_id}, user_id) client.sadd(key, user_id) end end def draw_winner(room_id, round_id, count) key lottery:join:#{room_id}:#{round_id} members client.smembers(key) return [] if members.empty? winners members.sample(count) # sample 不是原子操作正式实现用 Lua 脚本 winners.each do |uid| notify_task_system(room_id, uid, round_id) end winners end逻辑说明用集合而不是列表去重刷屏再多用户只在集合里出现一次。sample在小数据量下够用但正式环境要用 Lua 脚本在 Redis 里完成“取成员 随机抽取 删除已中奖者”否则并发抽奖可能抽到同一个人。参数说明current_round是开奖轮次号每次抽完奖给上一轮 key 设置 30 秒过期时间不要手动删参与集合要限制大小比如三分钟的开奖轮次可能有两万人参与集合保留到本场直播结束再清理。4.4 开奖定时与公平性验证开奖由两种方式触发主播在直播间发“开始抽奖”口令机器人立刻开奖或者运营在后台指定“每整点开奖”机器人用 muduo 的runAfter注册定时器。定时器执行时做三件事拉取参与集合、计算开奖数量、把结果推给端侧显示。公平性不能只看“随机”要验证算法没有偏向某个用户。离线跑模拟是最直接的办法import random import collections users [fu{i} for i in range(2000)] rounds 10000 counters collections.Counter() for _ in range(rounds): winners random.sample(users, 10) for w in winners: counters[w] 1 # 理论每个用户中奖次数期望10 / 2000 * 10000 50 次 expected 10 / len(users) * rounds max_deviation max(abs(c - expected) for c in counters.values()) / expected print(f最大偏差 {max_deviation:.4f})逻辑说明跑一万轮统计每个用户被抽中的次数和理论期望比较相对偏差偏差在 5% 以内说明算法没有明显偏向。发现固定某几个用户中奖异常高先检查sample的实现和参与集合是否被污染而不是怀疑随机数种子。参数说明真实环境还要关注“开奖后是否通知到人”。中奖名单要写进biz_lottery_record表前台公告和私信通知都从这张表读不要直接读 Redis 快照否则公告和实际抽奖结果会对不上。5. 任务系统部署排错要点LNMP、定时任务、日志与常见卡单5.1 最小可运行环境与目录结构任务系统和机器人服务端解耦部署任务系统是 PHP MySQL Redis机器人服务端是 C可以共用一台 2C4G 服务器。操作系统选 CentOS 7 或 Ubuntu 22.04PHP 7.4 以上MySQL 5.7Nginx 1.20 以上Redis 5.0 以上。目录结构建议固定成标准布局方便交付和排障/data/www/task-system/ ├── public/ # Web 根目录入口 index.php ├── runtime/ # 日志、缓存需可写 ├── config/ # 数据库、Redis 配置 ├── app/ # 控制器、模型、服务 ├── robot/ # C 机器人源码及编译产物 └── crontab/ # 定时任务脚本Nginx 的root指向public其余目录全部禁止对外访问。伪静态规则按框架区分写错会出现首页能开、列表 404server { listen 80; server_name task.example.com; root /data/www/task-system/public; index index.php index.html; location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s$1 last; } } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } location ~* ^/(runtime|robot)/ { deny all; } }逻辑说明runtime和robot暴露到公网日志文件会被下载、机器人二进制会被逆向。location ~*优先匹配 URL 里的路径命中就 deny这套规则对所有 PHP 项目都通用。参数说明上面的 rewrite 是 ThinkPHP 5 和 6 的写法如果源码是 Laravel要换成try_files $uri $uri/ /index.php?$query_string;两个框架的伪静态不能混用混用会出现所有非根路径 404。5.2 定时任务必须覆盖的三件事部署完先加三组 crontab否则跑几天就会出现任务时间到了还显示可领取、提现申请没人处理、机器人掉线没人管的问题# 每 5 分钟关停已到 end_time 的任务 */5 * * * * php /data/www/task-system/cli/task_close_expired.php # 每 1 分钟把提交超过 24 小时未审核的订单提醒管理员 * * * * * php /data/www/task-system/cli/audit_remind.php # 每 10 分钟检查机器人心跳连续 3 个周期没心跳则重启机器人 */10 * * * * bash /data/www/task-system/robot/check_heartbeat.sh逻辑说明任务关闭如果用“用户领取时校验 end_time”做兜底那只是被动关闭列表页还是会显示主动关闭脚本把 status 从 1 改成 3前台就刷掉了。审核提醒是防止运营漏单告警通道直接对接钉钉或企业微信机器人即可。参数说明机器人心跳检查要防“假活”进程在、但弹幕连接已经断开。check_heartbeat.sh应该检查 Redis 里的心跳时间戳而不是ps -ef | grep robot进程存在不等于连接正常。5.3 常见卡单排查顺序第一用户提交截图后一直停在“待审核”。排查顺序是看biz_task_order.status是否有 1 的数据 → 看审核接口有没有 500 日志 → 看runtime/log/里的 SQL 报错。一半情况是screenshot_url太长被截断MySQL 插入失败但前端没感知到事务回滚。第二列表显示“剩余 0 份”但任务状态还是投放中。这是processed_num total_num后没有触发状态切换。不要只靠定时任务查询时用表达式实时算状态SELECT *, CASE WHEN status 1 AND processed_num total_num THEN 2 WHEN status 1 AND end_time NOW() THEN 3 ELSE status END AS real_status FROM biz_task WHERE id ?;逻辑说明real_status是查询时实时算出来的不写回数据库定时脚本扫描到这个任务时再统一落库。这样即使定时任务延迟几分钟用户也不会看到“已满”和“投放中”并存的怪状态。参数说明管理后台展示“已满”用real_status编辑时判断能否编辑用落库值两个值分开用不容易误操作。第三部分安卓手机上传截图失败。大多数是前端把 base64 直接传接口2MB 的图片 base64 膨胀到 2.7MBNginx 默认 1MB 直接返回 413。前端先压缩再上传后端限制体积和格式// 前端已经压缩过这里只做服务端校验 if ($_FILES[screenshot][size] 1048576) { throw new Exception(图片不能超过 1MB); } $img imagecreatefromstring(file_get_contents($_FILES[screenshot][tmp_name])); if (!$img) { throw new Exception(不是有效的图片文件); }逻辑说明imagecreatefromstring检查的是文件内容而不是扩展名能挡掉大部分伪图片。真正的“截图 P 图”问题靠审核员放大看头像和 ID源码层面一般只做格式与体积校验。参数说明client_max_body_size要同时改 Nginx 的 http、server、location 三层只改一层在部分配置里不生效php.ini的upload_max_filesize和post_max_size要一起调到 2M。6. 上线前怎么验证压测、抽奖均衡性、幂等对账6.1 用 ab 压测并发领取验证任务系统能不能扛住一波流量用 ab 打接口就够了重点是观察有没有超卖ab -n 500 -c 100 -p /tmp/grab.json -T application/json \ https://task.example.com/api/task/grab打完之后查领取数和实际流水数是否一致SELECT t.id, t.total_num, t.processed_num, COUNT(o.id) AS order_count FROM biz_task t LEFT JOIN biz_task_order o ON o.task_id t.id GROUP BY t.id HAVING order_count ! t.processed_num OR t.processed_num t.total_num;processed_num和order_count必须相等不等说明有回滚失败或旁路插入。压测前清空测试任务的流水不要拿线上数据跑。6.2 抽奖结果与任务系统对账机器人抽完奖任务系统要生成中奖记录。对账脚本保证“机器人说抽了 10 个系统里正好 10 条流水”php /data/www/task-system/cli/lottery_reconcile.php /tmp/lottery_reconcile.log 21处理逻辑是以round_id room_id分组 count和机器人回调上报的数量对比不一致就标记need_manual1。抽奖回调是异步的回调丢失只能靠对账发现。对出来的差异推给人工处理不要自动补单自动补单会掩盖消息丢失问题。biz_lottery_record的round_id uid要加唯一索引从数据库层兜底同一个人不能在一轮里中两次。6.3 提现幂等同一个提现单不能被处理两次提现最容易出事的地方是用户提交、运营点击打款、支付回调重试导致重复打款。处理方式是以提现单号为幂等键处理前先锁单UPDATE biz_withdraw SET status 1 WHERE withdraw_sn :sn AND status 0 AND locked_at IS NULL;受影响行数为 1 才继续打款否则直接返回“重复提交”。打款成功后再把 status 改成 2失败则改回 0 并清空locked_at。压箱底的验证是上线第一周把runtime/log里的 error 日志按天归档每周扫一遍SQLSTATE出现次数最多的三类错误通常能在用户投诉之前发现字段截断和索引失效。任务系统稳定运行的标志不是没有错误日志而是错误种类收敛连续一周没有新增错误类型。本文还有配套的精品资源点击获取