简介面向悬赏任务类平台开发者的仿“悬赏猫/牛帮”任务平台源码定位为可直接部署运营的完整项目适合需要快速上线悬赏任务、兼职任务并支持APP封装业务的团队或个人。压缩包共2000个文件整体约263MB主体为995个PHP脚本、360个JS文件、283个HTML页面与209个CSS样式分别对应后端业务逻辑、前端交互、页面结构与界面布局同时包含配置类文件、函数库、说明文档及SQL文件便于根据注释和目录快速定位数据库、后台地址等关键配置。项目已在宝塔面板Apache2.4PHP5.6MySQL5.6环境亲测可用压缩包内提供了数据库配置文件路径、前台测试账号、后台管理员账号和密码并预留腾讯验证码关闭选项能降低环境搭建与测试阶段的踩坑成本。当前已有249人浏览学习适合熟悉PHP基础、希望基于现成代码二次开发或直接运营悬赏任务平台的中级开发者参考。1. 悬赏任务平台的核心不是任务列表是资金和风控做悬赏任务平台的人大多数把精力放在任务列表 UI、领单按钮和提现页上真正上线后摔跟头的全是钱的问题用户完成任务却迟迟拿不到奖励平台被同一设备批量注册刷穿渠道代理的分佣对不上账。标题里说的“完美运营”落到源码层面就是三条链路必须闭环任务验收有记录、资金结算不走样、设备风控能拦截。这套系统本质上由任务流、资金托管、渠道分发三层组成适合准备拿源码二开做自营平台的技术团队也适合正在评估“从 Web 站点封装成 APP”落地成本的产品和开发。下面按数据模型、结算链路、风控、分佣与封装一路讲下去所有表和参数都可以直接抄到自己的项目里。2. 任务状态机与多角色数据模型把验收拆成三段才撑得住运营一套悬赏任务源码能不能长期跑先看任务状态的穷举是否完整。状态机设计得细后面的审核、结算、驳回退款才有据可依设计得粗运营每天都要手工改数据库。2.1 任务主流程用五态别用“待接-已完成”两态很多入门版本把任务表设计成 status 只有 0 和 10 代表没人领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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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 status4 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 小时新用户做任务后延迟到账抗撸关键参数提现到账方式T1小额可以秒到大额走人工审核参数表要放进后端配置接口而不是写死在常量里。运营调整抽佣比例后新任务立即生效老任务仍按发布时的费率结算——所以 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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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_idP20250101001 AND biz_typerecharge;第一次推送完成加账第二次推送应该被幂等键拦截只在通知日志里多一条记录account_log 仍然只有一行。这里要注意 sign 只是测试占位真实环境必须校验签名和金额防止伪造回调。6.3 上线前冒烟清单九项必须过验证项可以直接做成一张上线检查表避免反复手工回归检查项验证动作预期结果重复领取同一账号连续领取同一任务第二次被拒绝设备注册限制同一设备信息连报 5 次第 6 次触发风控任务驳回解冻审核驳回后查发布方余额冻结余额自动减少重复回调两次 curl 模拟支付入账流水仅一行渠道返佣新用户完成任务后查渠道账户到账金额与费率一致图片上传壳内点击拍照上传弹系统相机可返回回显打包后刷新H5 路由刷新页面不掉回登录页APP 字体缩放系统字体调最大后打开应用页面不破版支付回跳支付完成返回壳内立即显示支付结果把这张表跑完再谈上架比上线后靠客诉发现问题要省心得多。悬赏任务平台的工程量不在界面上而在这些看不见的资金与风控细节里验证得越充分运营期的意外就越少。本文还有配套的精品资源点击获取