企油通「运单油费下发」(7.7) // +---------------------------------------------------------------------- // | OilCard::share 创建 yoshop_partners_order 后 push 'partners_queue',本命令消费: // | 1. 每个 order_id 用 RedisLock 串行化,锁内重读订单状态 // | 2. 生成企油通全平台唯一编号(确定性派生,可安全重试) // | 3. 调 7.7 createAndSendQuota;支付宝企业码通道下「受理成功即完成发放」 // | 4. 发放成功 -> oil_status=1 / oil_status_zsy=2,投 notify_queue 回调下单平台 // | // | 并发/原子性: // | · oil_status_zsy 只在 7.7 结果确定后离开 0;空闲时 reconcile() 补投丢失的队列消息 // | · 状态跃迁为条件 UPDATE(... WHERE oil_status_zsy=0),affected=1 才回调,天然幂等 // | · batch_no 确定性派生 -> 崩溃重试用同一值,依赖企油通按 batch_no 幂等去重 // | 启动:php think partners:queue(可多进程;RedisLock 保证同一订单串行) // +---------------------------------------------------------------------- namespace app\console\command; use app\common\lock\RedisLock; use app\common\model\partners\Order as OrderModel; use app\common\service\PartnersBalance; use app\common\service\QueueService; use app\common\service\QytOil; use think\console\Command; use think\console\Input; use think\console\Output; use think\facade\Db; class PartnersQueue extends Command { /** @var QueueService */ protected $queueService; /** @var int 上次兜底巡检时间 */ protected $lastReconcile = 0; protected function configure() { $this->setName('partners:queue') ->setDescription('下单平台订单 -> 企油通运单油费下发(7.7)'); } protected function execute(Input $input, Output $output) { $this->queueService = new QueueService(); while (true) { $message = $this->queueService->pop('partners_queue'); if ($message) { $this->processMessage($message); } else { $this->reconcile(); sleep(1); } } } protected function processMessage($message) { $qyt = new QytOil(); $data = is_array($message) ? $message : json_decode($message, true); $orderId = $data['order_id'] ?? null; if (!$orderId) { $qyt->saveLog('partners_queue 消息缺少 order_id:' . json_encode($message, 320)); return; } // 每个订单串行处理;锁 60s 自动过期,崩溃不会死锁 $lock = new RedisLock('qyt:send:' . $orderId, 60); if (!$lock->acquireLock()) { $qyt->saveLog("partners_queue 订单处理中,跳过 order={$orderId}"); return; } try { $order = OrderModel::detail($orderId); if (!$order) { $qyt->saveLog('partners_queue 未找到订单:' . $orderId); return; } if ($order['order_status'] != 10 || $order['oil_status'] != 0 || (int) $order['oil_status_zsy'] !== 0) { $qyt->saveLog("partners_queue 订单状态不满足下发条件 order={$orderId}"); return; } // 1. 确定性派生编号(含企业前缀),重试时保持一致 $seed = $order['partners_order_no'] ?: (string) $order['order_id']; $batchNo = QytOil::genNo('B', $seed); $waybillNo = QytOil::genNo('W', $seed); $waybillNoCode = QytOil::genNo('C', $seed); $name = trim((string) $order['driver_name']); if ($name === '' || $order['mobile'] === '') { $failed = Db::name('partners_order')->where('order_id', $orderId)->where('oil_status_zsy', 0) ->update(['oil_status_zsy' => 3, 'oil_text' => '缺司机姓名/手机号,无法发放', 'update_time' => time()]); $failed && $this->refundBalance($order); return; } // 2. 先落编号(状态不动,仍为 0) Db::name('partners_order')->where('order_id', $orderId)->update([ 'batch_no' => $batchNo, 'waybill_no' => $waybillNo, 'waybill_no_code' => $waybillNoCode, 'oil_text' => '发放中', 'update_time' => time(), ]); // 3. 7.7 运单油费下发 $res = $qyt->createAndSendQuota([ 'batch_no' => $batchNo, 'waybill_no' => $waybillNo, 'waybill_no_code' => $waybillNoCode, 'mobile' => $order['mobile'], 'name' => $name, 'amount' => $order['total_price'], ]); // 失败(且非 batch 已存在) if ($res['code'] !== '00' && empty($res['dup_batch'])) { $failed = Db::name('partners_order')->where('order_id', $orderId)->where('oil_status_zsy', 0)->update([ 'oil_status_zsy' => 3, 'oil_text' => '下发失败:' . $res['message'], 'update_time' => time(), ]); $failed && $this->refundBalance($order); $qyt->saveLog("7.7 下发失败 order={$orderId}:" . $res['message']); return; } // data.status: 1 成功 / 2 失败(dup_batch 视为已受理成功) $status = empty($res['dup_batch']) ? (string) ($res['data']['status'] ?? '1') : '1'; if ($status === '2') { $failed = Db::name('partners_order')->where('order_id', $orderId)->where('oil_status_zsy', 0)->update([ 'oil_status_zsy' => 3, 'oil_text' => $res['data']['msg'] ?? '下发失败', 'update_time' => time(), ]); $failed && $this->refundBalance($order); return; } // 4. 受理成功 = 发放成功(支付宝企业码通道);条件跃迁 0 -> 2,胜出者回调下单平台 $affected = Db::name('partners_order')->where('order_id', $orderId)->where('oil_status_zsy', 0)->update([ 'oil_status' => 1, 'oil_status_zsy' => 2, 'oil_time' => time(), 'shareNo' => $waybillNoCode, 'oil_text' => $res['data']['msg'] ?? '发放成功', 'update_time' => time(), ]); if ($affected) { $this->queueService->push('notify_queue', json_encode([ 'order_id' => $orderId, 'type' => 'add', 'optType' => 'share_oil', 'retry_count' => 0, ], 320)); $qyt->saveLog("7.7 发放成功 order={$orderId} batch_no={$batchNo}" . (empty($res['dup_batch']) ? '' : '(batch 已存在,按已发放处理)')); } } catch (\Throwable $e) { // 不改状态(仍为 0),交由 reconcile 兜底重投 $qyt->saveLog('partners_queue 异常 order=' . $orderId . ':' . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine()); } finally { $lock->releaseLock(); } } /** * 发放失败 -> 回补合作商余额(仅在状态跃迁胜出时调用,天然幂等) */ protected function refundBalance($order) { PartnersBalance::refund( (int) $order['partners_id'], (float) $order['total_price'], (string) $order['partners_order_no'], (int) $order['order_id'], (int) $order['store_id'] ); (new QytOil())->saveLog("发放失败已回补合作商余额 order={$order['order_id']} amount={$order['total_price']}"); } /** * 兜底巡检(限频 60s):补投丢失的 partners_queue 消息。 * 因下发为条件跃迁,重复投递安全。 */ protected function reconcile() { if (time() - $this->lastReconcile < 60) { return; } $this->lastReconcile = time(); $now = time(); try { $ids = Db::name('partners_order') ->where('order_status', 10) ->where('oil_status_zsy', 0) ->where('oilCardType', 'zsy_ticket') ->where('create_time', '<', $now - 120) ->where('create_time', '>', $now - 86400) ->limit(200)->column('order_id'); foreach ($ids as $oid) { $this->queueService->push('partners_queue', json_encode(['order_id' => $oid], 320)); } if ($ids) { (new QytOil())->saveLog('partners_queue reconcile 补投 ' . count($ids) . ' 单'); } } catch (\Throwable $e) { (new QytOil())->saveLog('partners_queue reconcile 异常:' . $e->getMessage()); } } }