简介这套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 目录反推框架版本拿到源码包后先不要急着配环境。直接看目录结构基本能判断这个项目的技术栈和框架版本。源码中包含vendor、extend、app、public、runtime、thinkphp这几个关键目录其中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/listsApache 和 Nginx 都有对应的 rewrite 规则。以任务列表为例浏览器执行一次 GET 请求后框架内部经历了这样几个阶段index.php加载thinkphp/start.php注册自动加载机制。路由解析s参数得到模块index、控制器task、方法lists。实例化app\index\controller\Task调用lists()方法。控制器里调用模型TaskModel查询数据库把结果赋值给视图模板。视图渲染后返回 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里至少应该包含task、user、user_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_num和remain_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/registerinvite . $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再写一条分销日志。多级分销则要递归查pid的pid每一级的分佣比例不同。我在实际项目中一般会控制最多两级因为第二级佣金比例通常只有 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-setmysql -u root -p your_database_name \ --default-character-setutf8mb4 weikerenwu.sql这里有两点值得强调第一utf8mb4是 MySQL 中真正完整的 UTF-8 实现支持 emoji 四字节字符而utf8在 MySQL 里只支持三字节表情包存不进去第二命令行直接导入比用 phpMyAdmin 导入更适合大文件几百 MB 的 SQL 用浏览器导入很可能会超时中断但生产环境建议分批次处理而不是一次性导入。导入完成后用下面这条命令验证核心表是否存在SHOW TABLES LIKE %task%; SHOW TABLES LIKE %user%;如果出现task、user、user_task、distribution_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里配置了PREFIXtp_那么模型查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 运行的用户名不同环境可能是nginx或apache可以用ps aux | grep nginx查看确认。另外public/uploads目录如果权限不足裂变海报和用户上传的任务凭证也保存不了同样需要检查属主和写权限。4.4 Linux 一键环境下的 PHP 扩展依赖清单ThinPHP 5.x 运行需要gd、pdo_mysql、curl、mbstring这几个 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.1ThinkPHP5.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可以作为后端被攻破后的第二道防线。坦白讲任何限流方案都不是绝对安全的最后兜底的一定是账单核对脚本定期检查每个用户的分销佣金总额与任务完成记录是否匹配确认无误后再批准提现这样刷子就算绕过了应用层防护也无法在人工审核环节蒙混过关。本文还有配套的精品资源点击获取