ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

PHP自动抢单系统源码:卡单连单管理与佣金设置实现

PHP自动抢单系统源码:卡单连单管理与佣金设置实现 简介此源码包为赚多多自动抢单系统最新版完整代码面向需要部署自动抢单或派单业务场景的开发者和站长解决传统抢单平台匹配效率低、佣金计算不灵活的问题。系统基于第三方匹配平台自动分配订单简化了程序流程并新增派单设置、连单管理以及卡单连单佣金设置功能使订单流转更高效同时支持针对不同商品灵活配置派单佣金比例。佣金计算支持按商品单价百分比动态设定例如单价十元、佣金比例零点三、数量二时订单佣金为十乘以零点三乘以二共六元运营方可据此灵活配置不同商品的收益规则满足多样化运营需求。压缩包大小约为四十七点四五兆内含完整项目源码及安装说明文档解压后按步骤部署即可适合有一定服务端开发基础的中高级开发者深入理解抢单系统的整体架构、核心接口与业务逻辑。目前已有千余人学习下载其中涵盖了派单规则配置、连单管理流程、卡单处理机制及佣金模块实现等关键内容可帮助读者快速掌握平台运转原理也为后续二次开发与功能增强提供了直接可用的代码参考。1. 抢单系统难的不是抢是“抢完怎么处理”“赚多多V10自动抢单系统源码”这个标题里真正值钱的不是“自动抢”三个字而是后面那半句卡单连单管理、新增佣金设置。做过接单业务的人都知道自动抢单本身并不复杂——订单池、循环扫描、命中条件就提交半小时能跑通。复杂的是抢到之后的归属确认、重复订单防抖、多订单的串行处理以及钱怎么分。卡单解决的是“抢到手但还没确认接”连单解决的是“一次活动来了好几个订单”佣金设置解决的是“平台、推广员、接单方三方分账”。这套逻辑在任何跑腿、外卖、同城配送场景里都一样。适合读这篇的人不是想找一个现成源码包直接跑的而是手里已经有一两套接单业务想自己把控源码、把单量高峰扛住的人。2. 自动抢单的底层原理订单池、锁、回调三件事2.1 为什么订单要用队列池而不是直接查数据库多数人第一次写自动抢单习惯让脚本每隔几秒SELECT * FROM orders WHERE status0。单量小的时候没问题订单量一上来数据库的 CPU 会先被打满。原因很简单轮询查询是“每次把整个候选集捞出来”而实际上每次只需要最早那一条。见过实际项目的做法是把订单进入系统的那一刻就推到内存队列里脚本只消费队列头部。结合标题里“V10”这个版本概念可以理解成前几个版本就是栽在订单池设计上后面几版全部围绕“池”在优化。Redis 的 ZSET 是很好的订单池载体。订单号作为成员订单产生时间作为分数天然支持按时间排序。入池的脚本长这样// 外部订单推送进来时写入 Redis 有序集合 $redis-zAdd(order_pool, time(), $orderId);这样入池时就已经排好序了。抢单脚本要拿最早一笔用zPopMin一行就能取出并移除避免同一笔订单被两个进程同时读到。2.2 抢单动作的本质在一个 key 上做 SET NX订单从池里取出来之后接下来是“抢”这个动作。抢的本质不是查而是锁。同一时间可能有多个抢单进程都在处理同一笔订单谁先抢到谁就在 Redis 里建一个带过期时间的锁。这里有个关键锁的过期时间就是卡单时长。V10 里的“卡单管理”本质是在管理这个过期时间。用 Lua 脚本保证原子性local ok redis.call(SET, KEYS[1], ARGV[1], NX, EX, ARGV[2]) if ok then return 1 else return 0 endPHP 侧调用$lua LUA local ok redis.call(SET, KEYS[1], ARGV[1], NX, EX, ARGV[2]) if ok then return 1 else return 0 end LUA; $locked $redis-eval($lua, [ lock:order: . $orderId, $userId, 5 // 卡单锁定 5 秒 ], 1); if ($locked) { // 拿到锁进入订单校验流程 }NX保证同一个 key 只能被一个进程 SET 成功EX控制锁自动过期。这里 5 秒的意思是抢到之后 5 秒内必须完成校验、确认接单否则锁自动释放订单重新回到可抢状态。2.3 卡单、连单、挂单背后的状态流转订单池里的订单有四个状态可抢、已锁定卡单中、已确认接单、已完成确认。卡单指的是“锁已拿到但还没确认接单”。这个状态下订单不能被别人抢走但也还没真正归属到某人账下。连单则是指一次活动里产生多笔订单接单方需要批量拿下而不是一单一单去扫。状态机的另外一层含义是超时处理。锁一旦到期无论当前进程在做什么Redis 都会把锁释放。这里常见做法是一个独立的挂单扫描进程每隔 1 秒去扫那些已经过期但没确认的锁把订单重新放回池子里。3. 实现卡单、连单和挂单从订单池到确认接单3.1 卡单的 5 秒窗口拿锁之后再校验为什么先锁再校验而不是校验完再锁原因是校验动作本身有网络成本比如需要请求外部接口确认订单是否有效。如果先校验再锁两个进程可能同时校验同一笔订单校验都通过后锁只有一个能拿到另一个就是白忙。V10 的“卡单”逻辑就是先把锁拿到锁里面带短超时时间在超时时间内校验校验不通过就静默释放锁通过就更新订单状态。// 卡单确认逻辑 if ($verifyPassed) { $redis-del(lock:order: . $orderId); $redis-sAdd(my_orders, $orderId); $order-status confirmed; $order-grab_user_id $userId; $order-save(); } else { // 校验失败释放锁订单回池 $redis-del(lock:order: . $orderId); $redis-zAdd(order_pool, time(), $orderId); }这里注意区分两个 Redis 操作del是释放锁sAdd是把订单写入“我抢到的订单”集合里作为后续结算的依据。很多误把这两个操作顺序写反先sAdd再校验这样订单六一校验就变成不可回滚。3.2 连单批量锁定的两种典型形态连单场景下一个用户可能一次性下单多笔。常见做法有两种一是把同一批次订单合并成一个组抢单时锁一个 group key而不是一个个锁;二是逐单加锁但锁的过期时间设短全部锁完再统一确认。如果一次性锁 20 个订单每个订单都请求一次 Redis网络开销会比较大而且可能锁到第 15 个时第 1 个已经超时了。业界更常见的做法是用 pipeline 批量提交$orderIds $this-getBatchOrderIds($groupId); $pipe $redis-multi(Redis::PIPELINE); foreach ($orderIds as $id) { // 每个订单尝试加锁锁 8 秒给批量操作留足时间 $pipe-set(lock:order:$id, $userId, [NX, EX 8]); } $results $pipe-exec();pipeline 的好处是 Redis 一次处理所有 SET 请求网络 Round Trip 从 N 次降为 1 次。失败率仍然存在比如某个订单已经被别人锁了pipeline 不会因为这一个失败而中断其他订单等exec返回后再逐个检查结果即可。3.3 挂单扫描谁处理那些被释放的订单挂在池子里没人接的订单需要一个常态的守护进程来处理。这个守护进程做的事情很简单把池子里最早的订单拿出来看符不符合当前用户的接单条件符合就尝试加锁加锁成功就进入卡单流程。# crontab 里每分钟跑一次即可或者直接作为常驻进程 * * * * * php /path/to/grab_daemon.php /tmp/grab_daemon.log 21grab_daemon.php 内部是一个循环while (true) { $order $redis-zPopMin(order_pool); if (!$order) { sleep(1); // 池子空了就歇 1 秒 continue; } if ($this-matchCondition($order)) { $this-grabOne($order); // 进入加锁流程 } }重点在于那个sleep(1)。如果不加这个等待空池时 CPU 会空转打满。3.4 卡单和连单相关的表结构怎么设计一路写下来数据库层面至少要承载订单轨迹、卡单记录、接单归属。推荐五张表订单表、卡单锁定表、连单批次表、接单日志表、结算表。字段规划如下表所示。表名关键字段用途ordersorder_no, status, batch_id, grab_user_id, amount订单主表状态变化记录在这里hold_recordsorder_id, user_id, lock_key, expire_time, status卡单记录每锁一次记一行grab_batchesbatch_no, total_count, locked_count, status连单批次记录批量锁单进度grab_logsorder_id, user_id, action, created_at抢单日志用于排错和对账commission_logsorder_id, user_id, level, amount, status佣金结算流水见第 4 章卡单记录表不删数据每次锁写一行释放时更新状态。留着这些记录有两个目的排查“为什么这单我没有抢到”、跟渠道方对账时能拿出证据。4. 佣金设置V10 新增的功能怎么做才不出错4.1 佣金规则先建好模型再写界面新增佣金设置本质上不是一个表单而是一套规则引擎。一个清晰的分佣配置应该支持三种维度按订单金额百分比比如每笔订单金额的 10%按固定金额比如每单固定 2 元按阶梯比如当月完成 100 单之后每单佣金上浮 5%在 V10 场景里涉及的通常是两级分佣推广员拿一级佣金推广员的推荐人拿二级佣金。“新增佣金设置”意味着这些参数要可配置、可即时生效不用改代码。数据库里要有一张规则表典型结构包括 min_amount、max_amount、rate、fixed_amount、level 这几个字段。// 获取规则时按金额区间匹配 $rule $db-query( SELECT * FROM commission_rules WHERE $amount BETWEEN min_amount AND max_amount AND level 1 AND status 1 ORDER BY updated_at DESC LIMIT 1 );设计上要注意一点区间不要重叠否则一条订单可能匹配到两条规则。建议在写接口时用排他区间min_amount 包含、max_amount 不包含。4.2 结算逻辑必须放在事务里先入流水再改余额佣金结算的触发时机是订单确认完成之后。常见做法是在订单完成回调里调用一个独立的结算服务。这个服务做的事按顺序分四步读规则、算金额、记流水、加余额。这四步任何一步失败都不能出现“钱已经加了但流水没有”的情况。$pdo-beginTransaction(); try { // 1. 锁定订单防止重复结算 $affected $pdo-exec( UPDATE orders SET settle_status 1 WHERE order_no {$orderNo} AND settle_status 0 ); if ($affected 0) { throw new Exception(订单已结算或不存在); } // 2. 计算一级佣金 $commission $orderAmount * $rule[rate] / 100; // 3. 写佣金流水 $pdo-exec( INSERT INTO commission_logs (order_id, user_id, level, amount, status, created_at) VALUES ({$orderId}, {$userId}, 1, {$commission}, 1, NOW()) ); // 4. 更新用户余额 $pdo-exec( UPDATE users SET balance balance {$commission} WHERE id {$userId} ); $pdo-commit(); } catch (Exception $e) { $pdo-rollBack(); // 记录日志并触发告警 }关键点在第 1 步用 update 影响行数判断是否已经结算过。如果两个回调同时进来只有第一个 update 能命中 1 行第二个影响 0 行直接抛异常。这比先 select 再 update 再判断并发更安全。4.3 佣金设置的隐藏坑整数、精度和退款场景金额计算的坑比业务逻辑坑多。PHP 里0.1 0.2 ! 0.3的问题处理佣金时会真实发生。建议所有金额字段在数据库里存分为单位的整数比如 10 元存 1000 分。计算佣金时1000 * 10 / 100得到整数 100 分全程不出现小数。还有一个很多人会忽略的退款场景订单完成了、佣金也发了、然后用户退款这时得把已经结算的佣金扣除。更常见的变通做法是佣金不立刻结算而是进入待结算状态等过了退款期再确认。回扣逻辑复杂这个做法最省心。5. 最后的验证技巧日志先行、参数可调、降级有底5.1 用一行的文件日志验证整个抢单闭环抢单系统是个低延迟系统排查问题最快的方式不是开调试器而是记录每次动作的一行日志。脚本里把每个关键动作都写进独立日志文件格式统一方便 grep。file_put_contents( /data/logs/snatch.log, date(H:i:s) . order:{$orderId} user:{$userId} action:{$action} spent:{$ms}ms\n, FILE_APPEND );压测时跑一轮然后用grep action:锁定 /data/logs/snatch.log | tail -50看耗时分布。如果spent稳定在 100ms 以下说明性能可以接受;如果超过 300ms先查网络再查锁竞争。5.2 三个值得调优的参数和一组压测命令第一个是锁的过期时间也就是卡单窗口。5 秒是典型值如果发现大量订单因为校验慢而超时释放就调大到 8 秒。第二个是挂单守护进程的 sleep 间隔池子空的时候 1 秒合适池子经常有货可以调到 0.1 秒。第三个是 pull 订单的批量大小连单时一次处理 10 笔和 50 笔对内存和 Redis 的压力完全不同。用 ab 模拟并发抢单时请求拉满到 200 并发观察耗时曲线ab -n 10000 -c 200 -p post_data.json -T application/json http://127.0.0.1:8080/grab关注两个指标失败请求数占总请求的比例、平均响应时间。失败率超过 2%优先看 Redis 连接数是否被打满。5.3 Redis 挂掉的降级路径数据库悲观锁兜底自动抢单强依赖 RedisRedis 一旦不可用整个系统直接哑掉。我一般会用数据库悲观锁作为降级路径SELECT ... FOR UPDATE锁住订单行然后再走正常结算流程。并发量会从每秒几千掉到每秒几百但至少订单不会丢、不会重。验证方式也很简单手动把 Redis 停掉跑一轮抢单脚本如果全部订单最终都能落库降级就是通的。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进