简介:面向悬赏任务类平台开发者的仿“悬赏猫/牛帮”任务平台源码,定位为可直接部署运营的完整项目,适合需要快速上线悬赏任务、兼职任务并支持APP封装业务的团队或个人。压缩包共2000个文件,整体约263MB,主体为995个PHP脚本、360个JS文件、283个HTML页面与209个CSS样式,分别对应后端业务逻辑、前端交互、页面结构与界面布局;同时包含配置类文件、函数库、说明文档及SQL文件,便于根据注释和目录快速定位数据库、后台地址等关键配置。项目已在宝塔面板+Apache2.4+PHP5.6+MySQL5.6环境亲测可用,压缩包内提供了数据库配置文件路径、前台测试账号、后台管理员账号和密码,并预留腾讯验证码关闭选项,能降低环境搭建与测试阶段的踩坑成本。当前已有249人浏览学习,适合熟悉PHP基础、希望基于现成代码二次开发或直接运营悬赏任务平台的中级开发者参考。
1. 悬赏任务平台的核心不是任务列表,是资金和风控
做悬赏任务平台的人,大多数把精力放在任务列表 UI、领单按钮和提现页上,真正上线后摔跟头的全是钱的问题:用户完成任务却迟迟拿不到奖励,平台被同一设备批量注册刷穿,渠道代理的分佣对不上账。标题里说的“完美运营”,落到源码层面就是三条链路必须闭环:任务验收有记录、资金结算不走样、设备风控能拦截。这套系统本质上由任务流、资金托管、渠道分发三层组成,适合准备拿源码二开做自营平台的技术团队,也适合正在评估“从 Web 站点封装成 APP”落地成本的产品和开发。下面按数据模型、结算链路、风控、分佣与封装一路讲下去,所有表和参数都可以直接抄到自己的项目里。
2. 任务状态机与多角色数据模型:把验收拆成三段才撑得住运营
一套悬赏任务源码能不能长期跑,先看任务状态的穷举是否完整。状态机设计得细,后面的审核、结算、驳回退款才有据可依;设计得粗,运营每天都要手工改数据库。
2.1 任务主流程用五态,别用“待接-已完成”两态
很多入门版本把任务表设计成 status 只有 0 和 1:0 代表没人领,1 代表已完成。这在演示项目里没问题,一旦任务需要“提交凭证后由人工或机审验收”,你就发现缺少中间态,无法回答“任务被谁领了、提交了什么、谁审核的、为什么驳回”这四个问题。
我一般会把任务状态做成五个核心态,再给每个状态配余额动作:
| 状态值 | 状态名 | 谁触发 | 余额动作 |
|---|---|---|---|
| 1 | 待领取 | 发布方发布 | 冻结任务奖励总额 |
| 2 | 进行中 | 用户领取 | 名额占用,不重复扣款 |
| 3 | 待验收 | 用户提交凭证 | 无 |
| 4 | 待结算 | 管理员/机审通过 | 触发拆分结算 |
| 5 | 已结束 | 结算消费者执行 | 奖励发放、手续费入账 |
状态 4 是我特意保留的中间态。验收通过不代表钱马上到账,先落到“待结算”,由异步任务执行资金拆分,避免用户刷新页面时余额和流水还没写完。驳回场景单独用任务流记录表保存,不在 task 主表里反复改写历史。
配套的任务表核心字段和状态流日志表如下:
CREATE TABLE task ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '任务 ID', title VARCHAR(120) NOT NULL COMMENT '任务标题', reward BIGINT NOT NULL COMMENT '单个任务奖励,单位:分', total_quota INT NOT NULL DEFAULT 0 COMMENT '总名额', claimed_quota INT NOT NULL DEFAULT 0 COMMENT '已领取名额', status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1待领取 2进行中 3待验收 4待结算 5已结束', deadline_at DATETIME NOT NULL COMMENT '任务截止时间', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_status (status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='悬赏任务主表'; CREATE TABLE task_flow ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '任务流 ID', task_id BIGINT NOT NULL COMMENT '任务 ID', operator_uid BIGINT NOT NULL COMMENT '操作人用户 ID', from_status TINYINT NOT NULL COMMENT '迁移前状态', to_status TINYINT NOT NULL COMMENT '迁移后状态', reason VARCHAR(500) DEFAULT '' COMMENT '驳回或关闭原因', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_task_id (task_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='任务状态流转日志';上面两张表把业务状态和操作日志拆开。task 表只关心当前状态,task_flow 负责留下每一次变更痕迹。实际运营中“接单用户提交了截图但审核员误点了通过”,要查是谁在什么时间改的状态,全靠 task_flow,否则只能翻后端日志,效率极低。
2.1.1 五态迁移的边界条件
状态迁移不是随手改字段。我一般会在 service 层写一个状态机校验,比如“待领取只能迁移到进行中”“待验收只能迁移到待结算或驳回”,迁移失败直接抛业务异常,不让 UPDATE 语句跨状态乱跳。你还需要注意超时任务:截止时间到后仍处于待领取或进行中的,由定时任务统一收回冻结金额。
2.2 用户四角色与余额字段分层,不靠一张表塞所有人
悬赏平台里至少有四类人:发布方、接单用户、渠道代理、平台管理员。很多源码把所有角色塞进一张 member 表,靠 type 字段区分,这没问题,但余额字段不能共用。发布方账户和接单账户的语义完全不同——发布方的钱要“冻结”,接单用户的钱要“可提现”,混在一张表里最后会失去审计能力。
CREATE TABLE user_account ( uid BIGINT NOT NULL PRIMARY KEY COMMENT '用户 ID,号段区分角色', usable BIGINT NOT NULL DEFAULT 0 COMMENT '可用余额,单位分', frozen BIGINT NOT NULL DEFAULT 0 COMMENT '冻结余额,发布任务时占用', total_income BIGINT NOT NULL DEFAULT 0 COMMENT '累计收入,用于等级门槛', updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户资金账户';我把 usable、frozen、total_income 分开存,原因有两点:一是提现只能从 usable 出,冻结部分在提现 SQL 里直接WHERE usable >= amount,不会误扣;二是统计用户等级时直接读 total_income,不用把订单流水全部 sum 一遍。角色区分用 uid 号段或者独立 role 表都可以,关键是账户表必须独立。
2.3 任务快照表:审核凭证要冗余成日志
任务提交的凭证不能只更新 task 主表里的evidence_url字段。接单用户提交截图、系统自动截图、用户填写的回执链接,这些都是审核依据,需要按提交批次落库。我会加一张 task_submit 表,记录每次提交的附件、IP、设备指纹、浏览器 UA、提交时间,审核通过后再把最终使用的凭证状态置为有效。
这样做的直接好处是,当运营质疑“这张截图是不是 P 的”,你能看到用户提交了三次、每次间隔多少秒、来自什么设备指纹。没有快照表的话,第二次提交会覆盖第一次,审核和客诉就完全失去抓手。
3. 资金托管与结算:先让结算闭环,再谈平台抽成
任务平台的商业模式本质是“发布方预付、完成者后取、平台按比例抽成”。如果资金链路不先在源码层闭环,哪怕 UI 做得再华丽,提现环节也会被手续费和负数余额问题打爆。
3.1 账户三户分离:可用、冻结、结算中
前文 user_account 表已经把可用余额和冻结余额分开,这里要补充的是“结算中”的资金不能直接在 usable 上加减。任务验收通过后,接单用户看到的是“已结算”流水,但余额加账采用异步事务完成,临界期间数据要放在待结算任务表里作为唯一凭证。
实际操作上,我用一张 settlement_task 表记录“待结算任务”,字段包括 task_id、user_id、reward、fee、status;结算消费者扫描这张表,成功加账后把状态改为 done。这张表同时解决了重复结算问题——同一任务只允许有一条未完成记录。
3.2 结算链路:从任务验收通过到余额到账的五个步骤
结算的核心函数我一般写成这样:
def settle_task(task_id: int) -> bool: # 1. 同一个事务里先锁定待结算任务,避免并发重复结算 with db.transaction(): task = db.fetchone( "SELECT id, reward, publisher_uid, worker_uid, status " "FROM task WHERE id=%s AND status=4 FOR UPDATE", (task_id,) ) if task is None: raise BizError("任务不存在或不在待结算状态") # 2. 计算平台抽成和用户到手金额,单位:分 platform_rate = get_config("settle.platform_rate", 0.2) worker_gain = int(task["reward"] * (1 - platform_rate)) platform_fee = task["reward"] - worker_gain # 3. 发布方冻结额核减,接单用户可用余额增加 db.execute( "UPDATE user_account SET frozen = frozen - %s WHERE uid = %s", (task["reward"], task["publisher_uid"]) ) db.execute( "UPDATE user_account SET usable = usable + %s, total_income = total_income + %s " "WHERE uid = %s", (worker_gain, worker_gain, task["worker_uid"]) ) # 4. 写账务流水,更新任务状态为已结束 db.execute( "INSERT INTO account_log (uid, amount, biz_type, ref_id) " "VALUES (%s, %s, 'task_income', %s)", (task["worker_uid"], worker_gain, task_id) ) db.execute( "INSERT INTO platform_income (amount, source_task_id, fee) " "VALUES (%s, %s, %s)", (platform_fee, task_id, platform_fee) ) db.execute("UPDATE task SET status = 5 WHERE id = %s", (task_id,)) return True这段代码的关键在第一步 SELECT FOR UPDATE。多台结算消费者同时抢同一任务时,只有拿到行锁的进程能继续,其余进程看到 status 已经不是 4 会直接返回,天然幂等。平台抽成率通过配置中心下发,改动不用发版。第二步用平台费率计算到手金额,优先保证“用户收益可预期”,抽成率再高也不至于让余额变负数。
还需要注意,这里没有直接改 settlement_task 的状态,而是靠 task 的状态位判断是否已结算。二开时如果保留两张表,记得把 task 状态更新和加账放进同一个数据库事务,否则会出现“钱到了但任务还挂着待结算”的对账差异。
3.3 结算参数表:手续费、提现门槛、单笔限额怎么设
运营参数决定了平台毛利和用户体验的平衡点。下表是我常用的一套初始参数,二开时直接写进参数配置表即可:
| 参数名 | 推荐初始值 | 说明 |
|---|---|---|
| 平台抽佣比例 | 20% | 奖励越大比例可下调,防止大额任务没人接 |
| 提现手续费 | 1% | 低于支付通道成本时平台会亏,别设 0 |
| 最低提现金额 | 1 元 | 低于成本,建议 3 元以上 |
| 单日提现次数 | 3 次 | 超过后引导次日再提,降低通道压力 |
| 首单奖励结算延迟 | 24 小时 | 新用户做任务后延迟到账,抗撸关键参数 |
| 提现到账方式 | T+1 | 小额可以秒到,大额走人工审核 |
参数表要放进后端配置接口而不是写死在常量里。运营调整抽佣比例后,新任务立即生效,老任务仍按发布时的费率结算——所以 task 表里最好冗余一个settle_rate字段,快照当时的费率,避免历史单结算时比例已经变化。
4. 风控与防刷:悬赏任务平台能持续运营的硬门槛
悬赏任务平台天然吸引“薅羊毛”用户,他们用批量脚本注册账号、领取任务、提交假截图。这一章不讨论怎么根治刷单,只讲源码里必须具备的三道基本防线。
4.1 设备维度风控:同一台手机能注册多少个号
最常见的刷单方式是“一机多号”。注册接口和设备指纹必须绑定,前端在注册时上报设备信息,后端做归一化识别:
{ "udid": "a1b2c3d4e5f60718293a", "platform": "android", "device_model": "Pixel 6", "screen": "1080x2400", "ua": "Mozilla/5.0 (Linux; Android 13) ...", "ip": "203.0.113.10" }udid 不能只信前端传的值,因为模拟器和改机工具能伪造字符串。服务端要把 ip、ua、device_model、screen 拼接之后再算一次特征值,和 udid 一起写入设备指纹表。两个条件里任何一个命中了已有设备,就认为同一台设备。实际操作中,我可以接受一定误杀,比如公司 WiFi 下多台同型号手机可能共用出口 IP,所以 ip 权重降低,udid 与 screen 的组合权重提高。
4.2 任务限领与黑名单联动:Redis 滑动窗口防重
同一设备在短时间内的注册、领任务、提现行为,用 Redis ZSet 做滑动窗口非常合适。以下 Lua 脚本限制“同一设备指纹 1 小时内最多注册 5 个账号”:
-- KEYS[1] 设备指纹对应的注册记录 key -- ARGV[1] 当前时间戳(秒) -- ARGV[2] 窗口大小(3600 秒) -- ARGV[3] 窗口内最大次数(5 次) local key = KEYS[1] local now = tonumber(ARGV[1]) local window = tonumber(ARGV[2]) local max_count = tonumber(ARGV[3]) redis.call('ZREMRANGEBYSCORE', key, 0, now - window) local count = redis.call('ZCARD', key) if count >= max_count then return 0 end redis.call('ZADD', key, now, now) redis.call('EXPIRE', key, window) return 1脚本执行结果是 1 才放行注册请求,0 则提示“操作过于频繁”。把 max_count 调成任务领取限制,同一条逻辑就可以复用到“同一用户 10 分钟内只能领 3 个同类型任务”上。同时,命中黑名单时要把 uid 和设备指纹都加入black:uid与black:device两个 Key,任务领取接口每次都先查这两个 Key。
4.3 提交文件二次校验:截图相册时间、文件类型与接口层鉴权的坑
接单用户提交的截图,不能只看文件后缀。.png可以改名成.jpg,服务端要读二进制头判断真实格式;更实用的校验是“用户提交三张截图之间的时间间隔”,间隔小于 500 毫秒的批量提交基本可以判定为脚本操作。时间戳校验要在服务端做,客户端传的时间不可信。如果任务要求用户在 APP 外完成某个动作,APP 端可以在任务开始和结束时各上报一次行为轨迹,源码里至少留出task_submit.behavior_data字段存 JSON 轨迹,上线后再决定要不要接入人工风控审核。
5. 渠道分佣与封装 APP:源码复用后最值钱的两步
标题里“可做任何悬赏任务平台”落到工程上,靠的是渠道分佣可配置和前端壳可复用。这两步做好,同一套后端源码就能对接不同行业、不同渠道的任务方。
5.1 invite_code 与渠道分佣的最小设计
每个推广渠道分配一个独立邀请码,用户通过渠道链接注册后,在 user 表里冗余invite_channel_id。渠道分佣在任务结算事件之后异步执行,不能放在第 3 章那个结算事务里,否则渠道返佣失败会导致任务结算回滚。
CREATE TABLE channel ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '渠道 ID', name VARCHAR(64) NOT NULL COMMENT '渠道名称', invite_code VARCHAR(32) NOT NULL UNIQUE COMMENT '用户填写的邀请码', rebate_rate DECIMAL(5,4) NOT NULL DEFAULT 0.1000 COMMENT '分佣比例,按平台手续费计算', status TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0停用', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='渠道代理表';rebate_rate 我一般设 0.1 到 0.3,表示平台抽佣部分的 10% 到 30% 返给渠道。比如任务奖励 100 元,平台抽佣 20 元,渠道比例 0.2,则渠道获得 4 元。结算消费者在platform_income写好后,再发一条channel_commission消息,由独立消费者给渠道账户加钱并记录流水。
5.2 封装 APP 的常见路径:H5 壳而不是原生重写
多数悬赏源码前端是 H5 站点,二次开发反而别重写原生 APP。常见做法是保留 H5 全部页面,用壳工程打包成可安装应用,既能上架也能作为企业签名包分发。工程上我一般用 manifest.json 描述壳配置:
{ "name": "TaskPlatformShell", "app-plus": { "distribute": { "android": { "minSdkVersion": 21, "targetSdkVersion": 30, "permissions": [ "<uses-permission android:name=\"android.permission.CAMERA\"/>", "<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>", "<uses-permission android:name=\"android.permission.INTERNET\"/>" ] } } }, "h5": { "router": { "mode": "hash" } } }minSdkVersion 21 覆盖了绝大多数存量 Android 设备;targetSdkVersion 30 意味着应用适配分区存储,直接访问外部相册需要申请 READ_EXTERNAL_STORAGE。h5.router 用 hash 模式是为了保证壳内刷新页面不会 404——history 模式需要服务端做路由回退,二开时经常忘记配。如果你手头就只有纯网页,也可以直接用 WebView 壳加载线上域名,但支付和上传图片时要注意权限回调没接好,常见问题就是“能打开网页但不能拍照上传”。
5.3 打包后要处理的登录态、权限与支付回调
壳工程不是套个 WebView 就完事。登录态必须从 H5 的 Cookie/Token 换成 APP 本地存储再注入 WebView,否则每次冷启动都要重新登录。相册和相机权限要在壳层申请,H5 内部的 input file 在部分 WebView 里弹不出文件选择器。支付回调则建议走 notify_url 服务端通知,壳内只做结果页轮询,避免用户在 WebView 里关掉支付页导致状态不一致。
6. 封装后先别急着上架:抓包与回调幂等验证做一遍
壳工程打包完,第一件事不是申请上架,而是把关键链路在测试环境下完整验证一遍。这里给三个我每次都会做的验证项。
6.1 解决 APP 抓包失败:证书信任与代理配置
很多团队反馈“APP 抓包失败”,十有八九是 Android 7.0 以上默认不信任用户证书。用 Charles 或 mitmproxy 调试时,必须先把抓包工具的 CA 证书装成系统证书,并在 manifest 里把网络配置改成信任用户证书。更省事的验证方式是先抓 Web 端接口,确认后端逻辑没问题,再看 APP 壳是否按预期把请求打到了同一套 API。另外注意部分壳工程会自动禁用 WebView 随系统字体缩放,如果发现 H5 页面字体异常偏大,优先查壳配置里的字体缩放开关,而不是改页面 rem。
6.2 支付回调重复推送:用两次 curl 验证幂等
支付网关为了确保通知送达,会对同一笔订单推送多次。用测试环境模拟同一笔回调推两次,观察入账流水只增加一次:
curl -X POST https://api.example.com/pay/notify \ -H "Content-Type: application/json" \ -d '{"order_no":"P20250101001","txn_id":"T999","amount":10000,"sign":"test_sign"}' curl -X POST https://api.example.com/pay/notify \ -H "Content-Type: application/json" \ -d '{"order_no":"P20250101001","txn_id":"T999","amount":10000,"sign":"test_sign"}' mysql -e "SELECT COUNT(*) FROM account_log WHERE ref_id='P20250101001' AND biz_type='recharge';"第一次推送完成加账,第二次推送应该被幂等键拦截,只在通知日志里多一条记录,account_log 仍然只有一行。这里要注意 sign 只是测试占位,真实环境必须校验签名和金额,防止伪造回调。
6.3 上线前冒烟清单:九项必须过
验证项可以直接做成一张上线检查表,避免反复手工回归:
| 检查项 | 验证动作 | 预期结果 |
|---|---|---|
| 重复领取 | 同一账号连续领取同一任务 | 第二次被拒绝 |
| 设备注册限制 | 同一设备信息连报 5 次 | 第 6 次触发风控 |
| 任务驳回解冻 | 审核驳回后查发布方余额 | 冻结余额自动减少 |
| 重复回调 | 两次 curl 模拟支付 | 入账流水仅一行 |
| 渠道返佣 | 新用户完成任务后查渠道账户 | 到账金额与费率一致 |
| 图片上传 | 壳内点击拍照上传 | 弹系统相机可返回回显 |
| 打包后刷新 | H5 路由刷新页面 | 不掉回登录页 |
| APP 字体缩放 | 系统字体调最大后打开应用 | 页面不破版 |
| 支付回跳 | 支付完成返回壳内 | 立即显示支付结果 |
把这张表跑完再谈上架,比上线后靠客诉发现问题要省心得多。悬赏任务平台的工程量不在界面上,而在这些看不见的资金与风控细节里,验证得越充分,运营期的意外就越少。
本文还有配套的精品资源,点击获取