ThinkPHP任务平台源码解析:裂变海报与分销佣金系统实战
2026/9/15 15:47:52 网站建设 项目流程

简介:这套PHP任务发布平台源码基于ThinkPHP框架构建,面向需要快速搭建任务分发、悬赏推广类网站的开发者或企业,可覆盖任务发布、接单管理、裂变海报推广与分销佣金结算等完整业务闭环。压缩包共5921个文件,大小约64.5MB,以1432个PHP文件为核心,同时包含png/gif/svg图片素材、js/css前端资源、dat/json数据文件以及SQL数据库脚本,便于前端展示、交互逻辑与数据初始化。目前已有1183人下载学习,适合电商、兼职、众包类项目参考。平台内置裂变海报生成、分销返佣等实用功能,并附安装说明与清晰的目录划分(app、public、runtime、vendor等),开发者可快速部署体验,也可基于ThinkPHP二次开发,深入理解任务平台的后台管理、用户分佣和推广追踪设计。

1. 为什么任务平台离不开裂变和分销这两板斧

拿到的这套 PHP 任务发布平台源码,不是普通的待办事项列表,也不是企业内部用的工单系统,而是一套带裂变海报和分销结算逻辑的完整 Web 应用。它的核心场景是:有人发布任务,有人接受任务并执行,执行完成后系统按规则给佣金,同时通过海报分享把新用户拉进来,形成二次传播。适合的对象很明确:做威客站、外包众包平台、微任务分发系统的人,以及想研究 ThinkPHP 实战项目的开发者。

这套源码的价值在于它把两个常被割裂的能力——任务流和分销流——放在了同一个系统里。任务侧涉及发布、审核、接受、提交、验收,分销侧涉及推广关系绑定、佣金比例、结算状态查询。两者通过用户表、任务表、推广记录表关联起来,业务复杂度比一般 CRUD 高不少。如果你正打算做类似平台,这篇就按这套源码的结构,把从建表到跑通的路径拆开来讲。

2. ThinkPHP 目录结构、请求生命周期与任务模块选型

2.1 从 vendor 和 app 目录反推框架版本

拿到源码包后,先不要急着配环境。直接看目录结构,基本能判断这个项目的技术栈和框架版本。源码中包含vendorextendapppublicruntimethinkphp这几个关键目录,其中thinkphp目录的存在说明项目用的是 ThinkPHP 框架,而不是 Laravel 或原生 PHP。public是 Web 根目录,app是应用代码目录,runtime存放运行时缓存和日志,vendor由 Composer 管理。

project_root/ ├── app/ # 应用目录:controller、model、service 等 ├── extend/ # 自定义扩展类库 ├── public/ # 入口文件 index.php、静态资源 ├── runtime/ # 运行时缓存、日志、模板编译 ├── thinkphp/ # 框架核心目录 ├── vendor/ # Composer 依赖库 ├── weikerenwu.sql # 数据库结构和初始数据 └── 安装说明.txt # 部署步骤

ThinkPHP 5.x 和 6.x 的目录结构差异不算太大,但app目录下的组织方式略有不同。5.1 默认是单应用模式,控制器放在app/index/controller下;6.0 开始模块的概念弱化,更推荐使用多应用模式但目录层级不变。看这个项目的目录里有extend目录,这是一个强信号——ThinkPHP 5.1 时代自定义类库放extend很常见,6.0 里也可以用但 Composer 加载优先级更高。我倾向于判断这是基于 ThinkPHP 5.1 或 5.0 的架构,因为weikerenwu.sql这种命名方式也比较符合那个时期的项目习惯。

2.2 请求从 URL 到控制器的完整链路

在 ThinkPHP 中,所有请求都经过public/index.php入口文件。这个文件负责引入框架启动文件,然后由路由组件解析 URL,找到对应的控制器方法执行。如果启用了伪静态,URL 形如https://example.com/index.php?s=/index/task/lists,Apache 和 Nginx 都有对应的 rewrite 规则。

以任务列表为例,浏览器执行一次 GET 请求后,框架内部经历了这样几个阶段:

  1. index.php加载thinkphp/start.php,注册自动加载机制。
  2. 路由解析s参数,得到模块index、控制器task、方法lists
  3. 实例化app\index\controller\Task,调用lists()方法。
  4. 控制器里调用模型TaskModel查询数据库,把结果赋值给视图模板。
  5. 视图渲染后返回 HTML,响应给浏览器。

下面是一段简化的Task控制器代码,保留了这个过程的核心结构:

<?php namespace app\index\controller; use app\common\model\TaskModel; use think\Request; class Task { // 注入请求对象 protected $request; public function __construct(Request $request) { $this->request = $request; } // 任务列表:分页查询 + 状态过滤 public function lists() { $status = $this->request->get('status', 0, 'intval'); $page = $this->request->get('page', 1, 'intval'); $where = []; if ($status > 0) { $where['status'] = $status; // 1待接单 2进行中 3待验收 4已完成 } $list = TaskModel::where($where) ->order('id', 'desc') ->paginate(10, false, ['page' => $page]); return json(['code' => 0, 'data' => $list]); } }

这段代码里paginate(10, false, ['page' => $page])是 ThinkPHP 自带的分页方法,第一个参数是每页条数,第二个参数表示是否简单分页,第三个参数传入当前页码。实际项目里这里往往直接渲染模板而不是返回 JSON,但接口化的写法便于后面对接小程序或 App。

2.3 任务模块的状态机和数据表设计

任务发布平台最核心的部分是状态机设计。一套任务从创建到结束,通常经历待接单 -> 进行中 -> 待验收 -> 已结束这四个稳定状态,另外还要考虑已取消申诉中已失败等边界状态。源码中的weikerenwu.sql里至少应该包含taskuseruser_task(或task_accept)、distribution_log(分销日志)这几张表。

表名关键字段用途说明
userid, username, pid(推广人ID), status用户表,pid 记录该用户是被谁推广来的
taskid, title, reward, total_num, remain_num, status任务表,reward为单品佣金,total为总份数
user_taskid, task_id, user_id, status, submit_time任务领取记录,status标记执行情况
distribution_logid, task_id, from_user_id, to_user_id, amount分销佣金流水,记录每一笔推广收益

task表里比较关键的几个字段是total_numremain_num。发布人指定总共可领取多少份,每被领取一次remain_num减一,减到零任务自动下架。这个逻辑在并发量上来后会有超卖问题,所以更新库存时要用条件更新,而不是先查后改。

UPDATE task SET remain_num = remain_num - 1 WHERE id = ? AND remain_num > 0;

上面这条 SQL 的原子性依赖的是WHERE remain_num > 0这个条件,在并发下,数据库会锁住这一行直到更新完成,从而避免把剩余量减成负数。如果在代码里写SELECT出来判断再UPDATE,并发请求会同时读到剩余量,导致超发。这是任务类系统最常见的坑。

3. 裂变海报的实现路径与分销追踪逻辑

3.1 海报生成不是抠图,是用二维码把用户关系串起来

裂变海报在技术上并不复杂,核心只有两件事:生成带参数的专属二维码,然后把它拼到一张背景图上。真正麻烦的是,别人扫了这个码之后,系统怎么知道是谁带来的?这就需要在二维码里编码一个用户标识。

源码里extend目录下如果存在海报生成类库,一般是用phpqrcode或类似的库生成二维码,再用GD库把二维码贴到背景图上。正常流程是这样的:

use think\facade\Cache; // 生成专属推广码参数 $token = md5($userId . time() . uniqid()); Cache::set('invite_token_' . $token, $userId, 86400); // 24小时有效 // 拼接带参数的推广URL $url = "https://yourdomain.com/index.php?s=/index/register&invite=" . $token; // 调用二维码类生成图片,再用GD库合成海报 $qrFile = ROOT_PATH . 'public/uploads/qrcodes/' . $token . '.png'; \QrCode::png($url, $qrFile, QR_ECLEVEL_L, 8); // 创建画布把背景图和二维码合在一起 $bg = imagecreatefromjpeg(ROOT_PATH . 'public/uploads/poster_bg.jpg'); $qr = imagecreatefromstring(file_get_contents($qrFile)); imagecopyresampled($bg, $qr, 450, 900, 0, 0, 300, 300, 300, 300); imagejpeg($bg, ROOT_PATH . 'public/uploads/poster_' . $userId . '.jpg', 90);

这里我用了一个token而不是直接把userId明文放在 URL 里,主要考虑是防止被别人伪造参数。md5($userId . time() . uniqid())生成的随机串无法反推用户 ID,只有服务端通过Cache::get('invite_token_' . $token)才能找到对应的用户。24 小时过期时间可以根据业务调整,如果希望推广码永久有效,可以把映射存到数据库而不是缓存。

3.2 分销关系绑定:注册时的 pid 写入与多级佣金计算

分销逻辑的起点是注册。用户点击别人的推广链接进入注册页面,表单里会携带invite参数。注册成功后,这个参数对应的用户 ID 被写入当前用户的pid字段。这个字段就是你的上级,也就是推广人。

<?php namespace app\index\controller; use think\facade\Cache; use app\common\model\UserModel; class Register { public function index() { $inviteToken = input('get.invite', '', 'trim'); $pid = 0; if ($inviteToken) { // 根据token反查推广人 $pid = Cache::get('invite_token_' . $inviteToken, 0); } // 后续在写入user表时带上pid $data = [ 'username' => input('post.username'), 'password' => password_hash(input('post.password'), PASSWORD_DEFAULT), 'pid' => $pid, 'create_time' => time() ]; $userId = UserModel::insertGetId($data); return json(['code' => 0, 'msg' => '注册成功', 'user_id' => $userId]); } }

关于多级分销的计算,主流做法是只算一级或者两级,三级以上在合规性和代码复杂度上都会遇到问题。一级分销逻辑最简单:任务发布者设置一个推广佣金比例,比如任务金额的 10%,当被推广人完成一单任务后,推广人获得reward * 0.10的佣金。代码上只需要在任务验收通过时,去user_task表里找到这个执行人的pid,再写一条分销日志。

多级分销则要递归查pidpid,每一级的分佣比例不同。我在实际项目中一般会控制最多两级,因为第二级佣金比例通常只有 5% 左右,再往深层级,金额小到没有激励效果,反而增加结算时的计算复杂度,还容易触碰到合规红线。下面贴一段二级分销佣金的结算参考写法:

use app\common\model\UserTaskModel; use app\common\model\UserModel; use app\common\model\DistributionLogModel; /** * 任务验收通过后,计算两级推荐佣金 * @param int $taskId 任务ID * @param int $execUserId 完成任务的用户ID * @param float $reward 任务单价 */ function settleDistribution($taskId, $execUserId, $reward) { // 查执行人的上级(一级推荐人) $execUser = UserModel::find($execUserId); if (empty($execUser['pid'])) { return; // 没有推荐人,不结算 } $level1User = UserModel::find($execUser['pid']); $level1Amount = round($reward * 0.10, 2); // 一级佣金10% if ($level1Amount > 0) { DistributionLogModel::insert([ 'task_id' => $taskId, 'from_user_id' => $execUserId, 'to_user_id' => $level1User['id'], 'amount' => $level1Amount, 'level' => 1, 'create_time' => time() ]); UserModel::where('id', $level1User['id'])->setInc('balance', $level1Amount); } // 查一级推荐人的上级(二级推荐人) if (!empty($level1User['pid'])) { $level2User = UserModel::find($level1User['pid']); $level2Amount = round($reward * 0.05, 2); // 二级佣金5% if ($level2Amount > 0) { DistributionLogModel::insert([ 'task_id' => $taskId, 'from_user_id' => $execUserId, 'to_user_id' => $level2User['id'], 'amount' => $level2Amount, 'level' => 2, 'create_time' => time() ]); UserModel::where('id', $level2User['id'])->setInc('balance', $level2Amount); } } }

这套逻辑里最关键的是setInc方法,它是 ThinkPHP 的原子自增操作,生成的 SQL 是UPDATE user SET balance = balance + 金额 WHERE id = ?,不会因为并发导致余额覆盖。每次结算都往distribution_log里写流水,账目对得上。

3.3 佣金出现负数或重复到账的排查思路

分销模块上线后最容易遇到的问题是重复打款。常见的根因有两个方向:一是任务验收通过后,没有对同一任务的结算做幂等控制,管理员重复点击验收按钮,就会执行两次结算方法;二是用户重复提交任务凭证,比如同一张截图传了两次,每次提交都触发一次结算逻辑。

要解决这类问题,最直接的办法是在distribution_log表中建立唯一索引,防止同一个执行人对同一个任务重复结算:

ALTER TABLE `distribution_log` ADD UNIQUE KEY `uk_task_user` (`task_id`, `from_user_id`);

加了这层约束后,即使代码逻辑出现漏洞,第二笔写入也会被数据库层直接拦截,不会造成资金损失。另一个经验是佣金金额上的校验,任务单价 10 元的任务,佣金比例 10%,算出来不可能超过 1 元,如果在日志里看到异常金额,比如几千块,那一定是类型转换出了问题,round没有给到第二参数,或字符串拼接导致数值错位。

4. 从 SQL 导入到 Nginx 配置的完整部署流程

4.1 导入 weikerenwu.sql 时的顺序坑与字符集问题

拿到源码后第一步是创建数据库并导入 SQL。weikerenwu.sql里包含了建表语句和初始管理账号,导入时最容易踩的坑有两个:一个是字符集,一个是 SQL 文件编码格式。

如果 SQL 文件里有中文注释或中文字符串,在命令行导入时经常会报Incorrect string value错误。这是因为 SQL 文件本身是 UTF-8 编码,但数据库连接缺省字符集不是 UTF-8 导致的。建议使用下面的命令行导入方式,并显式指定--default-character-set

mysql -u root -p your_database_name \ --default-character-set=utf8mb4 < weikerenwu.sql

这里有两点值得强调:第一,utf8mb4是 MySQL 中真正完整的 UTF-8 实现,支持 emoji 四字节字符,而utf8在 MySQL 里只支持三字节,表情包存不进去;第二,命令行直接导入比用 phpMyAdmin 导入更适合大文件,几百 MB 的 SQL 用浏览器导入很可能会超时中断,但生产环境建议分批次处理而不是一次性导入。导入完成后,用下面这条命令验证核心表是否存在:

SHOW TABLES LIKE '%task%'; SHOW TABLES LIKE '%user%';

如果出现taskuseruser_taskdistribution_log这几张表,说明建表结构没问题。接着要确认管理员的初始账号是否在 SQL 里。如果源码包里没有管理后台,一般是通过直接改数据库字段或app目录里的配置文件来指定默认管理员 ID。

4.2 ThinkPHP 的 env 配置与数据源切换

ThinkPHP 5.1 之后的版本支持.env环境配置文件,数据库账号密码、调试开关都写在里面。生产环境建议把.example.env复制成.env,然后把数据库配置改成你自己的:

APP_DEBUG = true APP_TRACE = false [DATABASE] TYPE = mysql HOSTNAME = 127.0.0.1 DATABASE = weikerenwu USERNAME = your_db_user PASSWORD = your_db_pass HOSTPORT = 3306 CHARSET = utf8mb4 PREFIX = tp_

特别注意PREFIX这个字段。ThinkPHP 模型层默认在表名前面加前缀,如果 SQL 文件里建的表是task_xxx,但.env里配置了PREFIX=tp_,那么模型查TaskModel::where(...)实际会找tp_task这张表,直接报 1146 表不存在。我在帮别人排查这类问题时,十个里有八个是前缀对不上。改完.env后再看config/database.php,确认它读取了.env里的值而不是写死配置:

return [ 'type' => 'mysql', 'hostname' => env('database.hostname', '127.0.0.1'), 'database' => env('database.database', ''), 'username' => env('database.username', 'root'), 'password' => env('database.password', ''), 'hostport' => env('database.hostport', '3306'), 'prefix' => env('database.prefix', 'tp_'), 'charset' => env('database.charset', 'utf8mb4'), ];

4.3 Nginx 伪静态配置与 runtime 目录权限

PHP 项目在 Nginx 下运行,必须配置对public目录的转发,并设置index.php为入口文件,同时处理统一路由。下面是一段可以直接用的虚拟主机配置,摘录了站点必须的三个关键 location 块:

server { listen 80; server_name demo.yourdomain.com; root /data/wwwroot/weikerenwu/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_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } location ~ /\.(?!well-known).* { deny all; } }

root必须指向public目录,不能指到项目根目录;如果把root指到项目根目录,浏览器就能直接访问到app目录里的 PHP 源码,这是一个很常见的低级安全隐患。location ~ \.php$中,fastcgi_param SCRIPT_FILENAME使用$document_root$fastcgi_script_name是为了避免$request_filename在某些 PHP-FPM 版本下路径解析不一致的问题。

runtime目录权限是另一个常见故障点。ThinkPHP 在运行时会向runtime目录写入缓存和日志文件,如果该目录不是www用户所有,页面会报目录不可写错误:

chown -R www:www /data/wwwroot/weikerenwu/runtime chmod -R 755 /data/wwwroot/weikerenwu/runtime

这里www是 Nginx 运行的用户名,不同环境可能是nginxapache,可以用ps aux | grep nginx查看确认。另外,public/uploads目录如果权限不足,裂变海报和用户上传的任务凭证也保存不了,同样需要检查属主和写权限。

4.4 Linux 一键环境下的 PHP 扩展依赖清单

ThinPHP 5.x 运行需要gdpdo_mysqlcurlmbstring这几个 PHP 扩展。如果用的是宝塔面板,安装完 PHP 之后在「PHP 管理」里可以检查扩展是否齐全。Linux 命令行环境下的安装方式如下:

# Debian / Ubuntu 系列 apt-get install -y php-gd php-mbstring php-curl php-mysql # CentOS / RHEL 系列 yum install -y php-gd php-mbstring php-curl php-mysqlnd

装完后用php -m检查模块是否被正确加载。如果没有gd扩展,海报合成功能会直接报Call to undefined function imagecreatefromjpeg(),而且这个错误是在运行时才暴露,代码检查根本发现不了。

4.5 环境验证清单:从安装说明到功能自测

部署完成后不要急着对外使用,先按下面的顺序走一遍自测,避免上线才发现账号登录不进去、海报生成不出来:

检测项操作方式预期结果
PHP 版本php -vPHP >= 7.1(ThinkPHP5.1 要求)
扩展检查php -m包含 pdo_mysql、gd、curl
数据库连接访问首页并登录无数据库连接报错
伪静态规则访问index.php?s=/index/task/lists和不带 index.php 的 URL两种 URL 均能正常访问
海报生成在有推广权限的账号下生成专属海报海报图片能保存到 uploads 目录
佣金结算用两个测试账号模拟推广关系并完成任务distribution_log 中产生佣金流水

如果登录时报session相关问题,检查runtime/session目录的写权限,同时确认config/app.php里的session配置没有指向不可写的位置。

5. 基于 URL 重写的海报防刷策略

裂变海报上线后最大的威胁不是羊毛党,而是刷子。刷子的典型行为是:短时间内生成大量海报、通过程序模拟用户注册、批量领取任务。这套源码如果没有二次验证,在公网裸奔大概率会被刷穿。

我一般会在生成海报的入口加三样东西:请求频率限制、验证码校验、IP 黑名单。请求频率限制最轻量的实现是使用 PHP 的session或缓存,在控制器里判断两次请求的时间间隔:

use think\facade\Cache; // 每分钟生成海报上限:每个用户最多生成5张 $key = 'poster_limit_' . $userId; $count = Cache::get($key, 0); if ($count >= 5) { return json(['code' => 1, 'msg' => '操作过于频繁,请稍后再试']); } Cache::set($key, $count + 1, 60); // 60秒过期

Cache::set第三个参数是过期时间,60 秒内超过 5 次就拒绝服务。这里用的是 ThinkPHP 缓存门面,底层可以无缝切换到 Redis 或 Memcached,在高并发下比文件缓存稳定得多。

另一个容易被忽略的点是,海报生成接口一定要校验用户登录状态。因为在接口层面,生成海报和注册新用户是两回事,如果生成海报的接口没有鉴权,别人可以直接用登录态的 Cookie 或 Token 来伪造请求,拿到其他人的推广码,把佣金挂到自己名下。这个问题的本质是接口鉴权不完整,用 ThinkPHP 的中间件解决:

public function handle($request, \Closure $next) { $token = $request->header('Authorization', ''); if (!$token || !Cache::has('user_token_' . $token)) { return json(['code' => 401, 'msg' => '未登录或登录已过期']); } $request->userId = Cache::get('user_token_' . $token); return $next($request); }

Nginx 层面也可以加一层拦截,比如对海报生成和注册接口做 IP 级别的限流,但这个要小心误伤局域网用户。考虑到成本,还是建议先在 PHP 代码层处理限流,Nginx 层的limit_req可以作为后端被攻破后的第二道防线。坦白讲,任何限流方案都不是绝对安全的,最后兜底的一定是账单核对脚本,定期检查每个用户的分销佣金总额与任务完成记录是否匹配,确认无误后再批准提现,这样刷子就算绕过了应用层防护,也无法在人工审核环节蒙混过关。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询