orderIdsWithCloseLog($orderIds); } function sqlExcludeClosedOrdersSubquery($orderAlias = 'o') { $closedStatus = \addons\shopro\library\split\SplitCloseService::STATUS_CLOSED; $pfx = config('database.prefix'); if (!tableHasColumn('shopro_split_manual_log', 'action')) { return "NOT EXISTS ( SELECT 1 FROM {$pfx}shopro_split_order so_c WHERE so_c.order_id = {$orderAlias}.id AND so_c.channel = 'huifu' AND so_c.status = {$closedStatus} )"; } return "NOT EXISTS ( SELECT 1 FROM {$pfx}shopro_split_order so_c WHERE so_c.order_id = {$orderAlias}.id AND so_c.channel = 'huifu' AND so_c.status = {$closedStatus} ) AND NOT EXISTS ( SELECT 1 FROM {$pfx}shopro_split_manual_log ml WHERE ml.order_id = {$orderAlias}.id AND ml.action = 'close' )"; } function runMarkClosedSingle($orderSn, $reason) { if ($orderSn === '') { fwrite(STDERR, "用法: mark-closed {order_sn} [--reason=...]\n"); exit(1); } try { $result = splitCloseService()->closeByOrderSn($orderSn, $reason, 0, 'CLI'); } catch (\Throwable $e) { fwrite(STDERR, '结案失败: ' . $e->getMessage() . "\n"); exit(1); } if (!empty($result['already'])) { echo "订单 id={$result['order_id']} 此前已结案\n"; return; } echo "已结案 order_id={$result['order_id']}"; if (!empty($result['split_order_id'])) { echo " split_order_id={$result['split_order_id']}"; } echo "\n"; } function runMarkClosedBatch($reason, $file, $fromAnomalies, $yes) { $sns = []; if ($file !== '') { if (!is_readable($file)) { fwrite(STDERR, "无法读取文件: {$file}\n"); exit(1); } $lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); foreach ($lines as $line) { $line = trim($line); if ($line === '' || $line[0] === '#') { continue; } $sns[] = preg_split('/[\s,,]+/', $line)[0]; } } elseif ($fromAnomalies) { $sns = collectAnomalyOrderSns(90); } else { fwrite(STDERR, "请指定 --file=orders.txt 或 --from-anomalies\n"); exit(1); } $sns = array_values(array_unique(array_filter(array_map('trim', $sns)))); if (!$sns) { echo "无订单号可处理\n"; return; } echo "将结案 " . count($sns) . " 笔订单,原因: {$reason}\n"; foreach ($sns as $sn) { echo " · {$sn}\n"; } if (!$yes) { echo "\n加 --yes 确认执行\n"; return; } $ok = 0; $skip = 0; $fail = 0; $svc = splitCloseService(); foreach ($sns as $sn) { try { $result = $svc->closeByOrderSn($sn, $reason, 0, 'CLI-batch'); if (!empty($result['already'])) { $skip++; } else { $ok++; } } catch (\Throwable $e) { $fail++; echo "[FAIL] {$sn}: " . $e->getMessage() . "\n"; } } echo "完成:新结案 {$ok},已结案跳过 {$skip},失败 {$fail}\n"; } /** @return string[] */ function collectAnomalyOrderSns($days = 90) { $since = time() - intval($days) * 86400; $pfx = config('database.prefix'); $closed = \addons\shopro\library\split\SplitCloseService::STATUS_CLOSED; $sns = []; $double = Db::query(" SELECT so.order_sn FROM {$pfx}shopro_split_order so INNER JOIN {$pfx}shopro_order o ON o.id = so.order_id INNER JOIN ( SELECT order_id, SUM(wallet_sum) AS wallet_sum FROM ( SELECT cr.order_id, SUM(wl.wallet) AS wallet_sum FROM {$pfx}shopro_commission_reward cr INNER JOIN {$pfx}shopro_user_wallet_log wl ON wl.item_id = cr.id AND wl.type = 'commission_income' AND wl.wallet_type = 'money' WHERE cr.status = 1 GROUP BY cr.order_id UNION ALL SELECT rr.order_id, SUM(wl.wallet) FROM {$pfx}shopro_regional_agent_reward rr INNER JOIN {$pfx}shopro_user_wallet_log wl ON wl.item_id = rr.id AND wl.type = 'regional_agent_income' AND wl.wallet_type = 'money' WHERE rr.status = 1 GROUP BY rr.order_id ) t GROUP BY order_id ) wl ON wl.order_id = so.order_id WHERE so.status IN (2,3) AND so.split_amount > 0 AND wl.wallet_sum > 0 AND so.status <> {$closed} AND o.paytime >= ? ORDER BY so.id DESC LIMIT 100 ", [$since]); foreach ($double as $r) { $sns[] = $r['order_sn']; } $failed = Db::name('shopro_split_order')->alias('so') ->join('shopro_order o', 'o.id = so.order_id') ->where('so.channel', 'huifu') ->where('so.status', 4) ->where('o.paytime', '>=', $since) ->column('so.order_sn'); foreach ($failed as $sn) { $sns[] = $sn; } foreach (fetchMissingSplitOrders($since, 100) as $row) { $sns[] = $row['order_sn']; } return array_values(array_unique(array_filter($sns))); } function tableHasColumn($table, $column) { static $cache = []; $key = $table . '.' . $column; if (array_key_exists($key, $cache)) { return $cache[$key]; } $full = config('database.prefix') . $table; try { $col = str_replace(['`', "'"], '', $column); $rows = Db::query("SHOW COLUMNS FROM `{$full}` LIKE '{$col}'"); } catch (\Exception $e) { $cache[$key] = false; return false; } $cache[$key] = !empty($rows); return $cache[$key]; } /** 按订单查区域佣金(兼容无 order_id 字段的旧表结构) */ function selectRegionalRewardsByOrderId($orderId) { if (tableHasColumn('shopro_regional_agent_reward', 'order_id')) { return Db::name('shopro_regional_agent_reward')->where('order_id', $orderId)->select(); } return Db::name('shopro_regional_agent_reward')->alias('rr') ->join('shopro_regional_agent_order ro', 'ro.id = rr.regional_order_id') ->where('ro.order_id', $orderId) ->field('rr.*') ->select(); } /** 订单是否存在分销/区域佣金(用于筛选) */ function sqlOrderHasAnyReward($orderIdExpr = 'o.id') { $pfx = config('database.prefix'); $dist = "EXISTS (SELECT 1 FROM {$pfx}shopro_commission_reward cr WHERE cr.order_id = {$orderIdExpr} AND cr.status >= 0)"; if (tableHasColumn('shopro_regional_agent_reward', 'order_id')) { $reg = "EXISTS (SELECT 1 FROM {$pfx}shopro_regional_agent_reward rr WHERE rr.order_id = {$orderIdExpr} AND rr.status >= 0)"; } else { $reg = "EXISTS (SELECT 1 FROM {$pfx}shopro_regional_agent_reward rr " . "INNER JOIN {$pfx}shopro_regional_agent_order ro ON ro.id = rr.regional_order_id " . "WHERE ro.order_id = {$orderIdExpr} AND rr.status >= 0)"; } return "({$dist} OR {$reg})"; } function splitConfigMap() { return Db::name('shopro_split_config')->column('value', 'name'); } function huifuConfigMap() { if (!Db::query("SHOW TABLES LIKE '" . config('database.prefix') . "shopro_huifu_config'")) { return []; } return Db::name('shopro_huifu_config')->column('value', 'name'); } function inferSplitPath(array $sc, array $hf) { $enable = ($sc['split_enable'] ?? '0') === '1'; if (!$enable) { return '未走支付分账(split_enable=0)'; } $mode = $sc['split_mode'] ?? 'wechat'; if ($mode === 'wallet') { return '未走支付分账(split_mode=wallet 仅钱包)'; } $channel = $hf['miniprogram_channel'] ?? 'yansongda'; $hfOn = ($hf['status'] ?? '0') === '1'; $provider = $sc['split_provider'] ?? 'auto'; if ($hfOn && $channel === 'huifu') { if (in_array($mode, ['huifu', 'both'], true) || in_array($provider, ['auto', 'huifu'], true)) { return '汇付延时分账(huifu_wechat 订单,确认时 acct_split_bunch)'; } } if (in_array($mode, ['wechat', 'both'], true)) { return '微信分账(pay_type=wechat 订单)'; } return '分账已开,请结合 pay_type 与 split_mode/provider 人工确认'; } function printSummary() { $sc = splitConfigMap(); $hf = huifuConfigMap(); $commEvent = Db::name('shopro_commission_config')->where('name', 'commission_event')->value('value'); $regEvent = Db::name('shopro_regional_agent_config')->where('name', 'commission_event')->value('value'); echo "========================================\n"; echo " 分账 & 佣金核验 · 摘要\n"; echo "========================================\n\n"; echo "【分账配置】\n"; foreach (['split_enable', 'split_mode', 'split_provider', 'split_timing', 'split_amount_source', 'split_max_rate', 'delay_acct_enable', 'platform_huifu_id'] as $k) { echo " {$k}: " . ($sc[$k] ?? '(未配置)') . "\n"; } echo "\n【汇付】\n"; foreach (['status', 'miniprogram_channel', 'huifu_id', 'delay_acct_enable'] as $k) { echo " {$k}: " . ($hf[$k] ?? '(无表/未配置)') . "\n"; } echo "\n【佣金结算事件】\n"; echo " 分销 commission_event: " . ($commEvent ?: 'confirm') . "\n"; echo " 区域 regional commission_event: " . ($regEvent ?: 'confirm') . "\n"; echo " 分账 split_timing: " . ($sc['split_timing'] ?? 'payed') . "\n"; if (($sc['split_timing'] ?? 'payed') !== ($commEvent ?: 'confirm')) { echo " ⚠ 分账时机与分销结算事件不一致,可能出现「先网关分账、后钱包入账」\n"; } echo "\n【推断】\n"; echo " " . inferSplitPath($sc, $hf) . "\n"; echo "\n【汇付延时分账支付标记(delay_acct_flag)】\n"; $delayEnable = ($sc['delay_acct_enable'] ?? '0') === '1'; $delayAtPay = $delayEnable && ( in_array($sc['split_mode'] ?? '', ['huifu', 'both'], true) || ($sc['split_provider'] ?? 'auto') === 'huifu' ); echo " delay_acct_enable(分账配置): " . ($delayEnable ? '1' : '0') . "\n"; echo " 下单时 delay_acct_flag=Y: " . ($delayAtPay ? '是(HuifuConfig::shouldDelayAcct)' : '否') . "\n"; if (($sc['split_enable'] ?? '0') === '1' && ($hf['miniprogram_channel'] ?? '') === 'huifu' && !$delayAtPay) { echo " ⚠ 小程序走汇付但支付未开延时分账:延时分账确认(acct_split_bunch)可能无法成功\n"; echo " 建议: delay_acct_enable=1,且 split_mode=huifu/both 或 split_provider=huifu\n"; } echo "\n【代码行为说明】\n"; echo " · 网关分账金额来自 commission/regional reward 或 split_rule\n"; echo " · 汇付:推广员/区域 huifu_id 收 div_amt,余款进 platform_huifu_id 或主商户 huifu_id\n"; echo " · 业务佣金结算仍写 shopro_user_wallet_log(commission_income / regional_agent_income)\n"; echo " · 汇付/微信分账成功:佣金标记已结算且不入 user.money;分账失败则回退入钱包\n"; $since = time() - 90 * 86400; $stats = Db::name('shopro_split_order')->alias('so') ->join('shopro_order o', 'o.id = so.order_id') ->where('o.paytime', '>=', $since) ->field('so.channel, so.status, COUNT(*) cnt, SUM(so.split_amount) split_sum') ->group('so.channel, so.status') ->select(); echo "\n【近90天分账订单】\n"; if (empty($stats)) { echo " (无记录)\n"; } else { foreach ($stats as $row) { echo " channel={$row['channel']} status={$row['status']} cnt={$row['cnt']} split_sum={$row['split_sum']}\n"; } } } function printAnomalies() { $since = time() - 90 * 86400; echo "========================================\n"; echo " 异常检测(近90天)\n"; echo "========================================\n\n"; $pfx = config('database.prefix'); $double = Db::query(" SELECT so.order_sn, so.split_amount, wl.wallet_sum FROM {$pfx}shopro_split_order so INNER JOIN {$pfx}shopro_order o ON o.id = so.order_id INNER JOIN ( SELECT order_id, SUM(wallet_sum) AS wallet_sum FROM ( SELECT cr.order_id, SUM(wl.wallet) AS wallet_sum FROM {$pfx}shopro_commission_reward cr INNER JOIN {$pfx}shopro_user_wallet_log wl ON wl.item_id = cr.id AND wl.type = 'commission_income' AND wl.wallet_type = 'money' WHERE cr.status = 1 GROUP BY cr.order_id UNION ALL SELECT rr.order_id, SUM(wl.wallet) FROM {$pfx}shopro_regional_agent_reward rr INNER JOIN {$pfx}shopro_user_wallet_log wl ON wl.item_id = rr.id AND wl.type = 'regional_agent_income' AND wl.wallet_type = 'money' WHERE rr.status = 1 GROUP BY rr.order_id ) t GROUP BY order_id ) wl ON wl.order_id = so.order_id WHERE so.status IN (2,3) AND so.split_amount > 0 AND wl.wallet_sum > 0 AND o.paytime >= ? AND NOT EXISTS ( SELECT 1 FROM {$pfx}shopro_split_manual_log ml WHERE ml.order_id = so.order_id AND ml.action = 'close' ) ORDER BY so.id DESC LIMIT 30 ", [$since]); echo "【可能双重发放】网关分账成功且钱包有佣金流水(最多30条)\n"; if (empty($double)) { echo " ✓ 未发现\n\n"; } else { foreach ($double as $r) { echo " {$r['order_sn']} split={$r['split_amount']} wallet={$r['wallet_sum']}\n"; } echo "\n"; } $failed = Db::name('shopro_split_order')->where('status', 4)->order('id desc')->limit(15)->select(); echo "【分账失败 status=4】\n"; if (empty($failed)) { echo " ✓ 无\n\n"; } else { foreach ($failed as $r) { $msg = mb_substr($r['wx_result'] ?? '', 0, 80); echo " {$r['order_sn']} channel={$r['channel']} retry={$r['retry_count']} {$msg}\n"; } echo "\n"; } $missing = Db::name('shopro_order')->alias('o') ->where('o.pay_type', 'huifu_wechat') ->where('o.paytime', '>=', $since) ->whereNotExists(function ($q) { $q->name('shopro_split_order')->alias('so') ->whereRaw('so.order_id = o.id') ->where('so.channel', 'huifu') ->where('so.status', 'in', [1, 2, 3]); }) ->whereRaw(sqlExcludeClosedOrdersSubquery('o')) ->whereExists(function ($q) { $q->name('shopro_commission_reward')->alias('cr') ->whereRaw('cr.order_id = o.id') ->where('cr.status', '>=', 0); }) ->field('o.id, o.order_sn, o.pay_fee') ->limit(20) ->select(); echo "【汇付单有分销佣金但无成功/进行中分账记录】\n"; if (empty($missing)) { echo " ✓ 未发现(或分账未开启/无佣金)\n"; } else { foreach ($missing as $r) { echo " {$r['order_sn']} pay_fee={$r['pay_fee']}\n"; } echo "\n → 执行 diagnose-missing 查看每笔未分账原因\n"; } } /** * @return array> */ function fetchMissingSplitOrders($since, $limit = 20) { $closedStatus = \addons\shopro\library\split\SplitCloseService::STATUS_CLOSED; return Db::name('shopro_order')->alias('o') ->where('o.pay_type', 'huifu_wechat') ->where('o.paytime', '>=', $since) ->whereNotExists(function ($q) { $q->name('shopro_split_order')->alias('so') ->whereRaw('so.order_id = o.id') ->where('so.channel', 'huifu') ->where('so.status', 'in', [1, 2, 3]); }) ->whereNotExists(function ($q) use ($closedStatus) { $q->name('shopro_split_order')->alias('so5') ->whereRaw('so5.order_id = o.id') ->where('so5.channel', 'huifu') ->where('so5.status', $closedStatus); }) ->whereRaw(sqlExcludeClosedOrdersSubquery('o')) ->whereRaw(sqlOrderHasAnyReward('o.id')) ->field('o.id, o.order_sn, o.pay_fee, o.pay_type, o.paytime, o.transaction_id') ->order('o.id desc') ->limit($limit) ->select(); } function diagnoseOrderSplitBlockers($orderId) { $reasons = []; $order = Db::name('shopro_order')->where('id', $orderId)->find(); if (!$order) { return ['订单不存在']; } if (class_exists('\\addons\\shopro\\library\\split\\SplitCloseService') && (new \addons\shopro\library\split\SplitCloseService())->isClosed($orderId)) { $reasons[] = '已标记线下结案,不再重试汇付分账'; } if ($order['pay_type'] !== 'huifu_wechat') { $reasons[] = "pay_type={$order['pay_type']},非 huifu_wechat,汇付分账不会执行"; } $sc = splitConfigMap(); if (($sc['split_enable'] ?? '0') !== '1') { $reasons[] = '当前 split_enable=0(若支付时也为关,则当时不会分账)'; } $ext = Db::name('shopro_huifu_pay_ext')->where('order_id', $orderId)->find(); if (!$ext) { $reasons[] = '无 shopro_huifu_pay_ext 记录,可能非汇付支付或扩展未写入'; } elseif (($ext['delay_acct_flag'] ?? 'N') !== 'Y') { $reasons[] = '支付时 delay_acct_flag≠Y(历史配置未开延时分账),补跑 confirm 可能被汇付拒绝'; } $calcItems = []; if (class_exists('\\addons\\shopro\\library\\split\\SplitCalculator')) { $calcItems = (new \addons\shopro\library\split\SplitCalculator())->calculate($orderId); } if (empty($calcItems)) { $reasons[] = 'SplitCalculator 结果为空(最常见:推广员/区域无 channel=huifu 的分账接收方)'; $rewards = Db::name('shopro_commission_reward') ->where('order_id', $orderId) ->where('status', '>=', 0) ->field('id, commission, agent_id') ->select(); foreach ($rewards as $rw) { // shopro_commission_agent 主键为 user_id,reward.agent_id 即分销商 user_id $uid = intval($rw['agent_id']); $recv = Db::name('shopro_split_receiver') ->where('role_type', 'promoter') ->where('role_id', $uid) ->where('channel', 'huifu') ->where('status', 'normal') ->find(); $onboard = Db::name('shopro_huifu_onboard') ->where('user_id', $uid) ->where('role_type', 'promoter') ->order('id desc') ->find(); $ob = $onboard ? ($onboard['onboard_status'] . '/' . ($onboard['huifu_id'] ?: '-')) : '无进件'; $rc = $recv ? $recv['receiver_account'] : '无 receiver'; $reasons[] = " 推广员 user_id={$uid} 佣金={$rw['commission']} 进件={$ob} split_receiver={$rc}"; } $regRewards = selectRegionalRewardsByOrderId($orderId); $regRewards = array_filter($regRewards, function ($rw) { return intval($rw['status']) >= 0; }); foreach ($regRewards as $rw) { $uid = $rw['agent_user_id']; $recv = Db::name('shopro_split_receiver') ->where('role_type', 'regional_agent') ->where('role_id', $uid) ->where('channel', 'huifu') ->where('status', 'normal') ->find(); $onboard = Db::name('shopro_huifu_onboard') ->where('user_id', $uid) ->where('role_type', 'regional_agent') ->order('id desc') ->find(); $ob = $onboard ? ($onboard['onboard_status'] . '/' . ($onboard['huifu_id'] ?: '-')) : '无进件'; $rc = $recv ? $recv['receiver_account'] : '无 receiver'; $reasons[] = " 区域 user_id={$uid} 佣金={$rw['commission']} 进件={$ob} split_receiver={$rc}"; } } else { $sum = array_sum(array_column($calcItems, 'amount')); $maxRate = floatval($sc['split_max_rate'] ?? 30); $reasons[] = '当前可算出分账明细 ' . count($calcItems) . ' 条,合计 ' . $sum . ' 元(受 split_max_rate=' . $maxRate . '% 与 pay_fee 上限约束)'; foreach ($calcItems as $item) { $uid = intval($item['role_id'] ?? 0); $acct = $item['receiver_account'] ?? '-'; $onboardHint = ''; if ($uid > 0 && ($item['role_type'] ?? '') === 'promoter') { $onboard = Db::name('shopro_huifu_onboard') ->where('user_id', $uid) ->where('role_type', 'promoter') ->order('id desc') ->find(); if ($onboard) { $onboardHint = ' 进件=' . ($onboard['onboard_status'] ?? '') . '/' . ($onboard['huifu_id'] ?: '-'); if (($onboard['huifu_id'] ?? '') !== '' && $onboard['huifu_id'] !== $acct) { $onboardHint .= ' ⚠与 receiver 不一致'; } } else { $onboardHint = ' 进件=无'; } } $reasons[] = sprintf( ' → %s user_id=%s huifu_id=%s 金额=%s%s', $item['role_type'] ?? '-', $uid ?: '-', $acct, number_format((float) ($item['amount'] ?? 0), 2, '.', ''), $onboardHint ); } $payFee = number_format((float) $order['pay_fee'], 2, '.', ''); $payHuifuId = trim((string) Db::name('shopro_huifu_config')->where('name', 'huifu_id')->value('value')); $platformId = trim((string) ($sc['platform_huifu_id'] ?? '')); if ($platformId === '') { $platformId = $payHuifuId; } $remain = bcsub($payFee, number_format($sum, 2, '.', ''), 2); if (bccomp($remain, '0.00', 2) > 0) { if ($platformId !== '' && $platformId === $payHuifuId) { $reasons[] = ' → 主商户留存(不写 acct_infos)huifu_id=' . $payHuifuId . ' 金额=' . $remain; } else { $reasons[] = ' → 平台分账 huifu_id=' . $platformId . ' 金额=' . $remain; } } } $walletCnt = 0; $crIds = Db::name('shopro_commission_reward')->where('order_id', $orderId)->where('status', 1)->column('id'); $rrIds = array_column(array_filter(selectRegionalRewardsByOrderId($orderId), function ($r) { return intval($r['status']) === 1; }), 'id'); $allIds = array_merge($crIds ?: [], $rrIds ?: []); if ($allIds) { $walletCnt = Db::name('shopro_user_wallet_log') ->where('item_id', 'in', $allIds) ->where('wallet_type', 'money') ->where('type', 'in', ['commission_income', 'regional_agent_income']) ->count(); } if ($walletCnt > 0) { $reasons[] = "佣金已入钱包({$walletCnt} 条流水)→ 资金目前在用户余额,非汇付分账账户"; } return $reasons; } function printTrySplitChannelHints(array $order) { if (!class_exists('\\addons\\shopro_huifu\\library\\HuifuQueryService')) { return; } echo "\n【汇付查单】\n"; try { $query = (new \addons\shopro_huifu\library\HuifuQueryService())->queryByOrderSn($order['order_sn']); $keys = ['trans_stat', 'delay_acct_flag', 'unconfirm_amt', 'fund_freeze_stat', 'unfreeze_time']; foreach ($keys as $key) { if (isset($query[$key]) && $query[$key] !== '' && $query[$key] !== null) { echo " · {$key}=" . $query[$key] . "\n"; } } $freeze = strtoupper(trim((string) ($query['fund_freeze_stat'] ?? ''))); if ($freeze === 'FREEZE') { echo " → 仍 FREEZE:请先启用 shopro_wechat_trade 并完成发货/微信确认收货,勿 --execute\n"; } } catch (\Throwable $e) { echo ' · 查单失败: ' . $e->getMessage() . "\n"; } $latest = Db::name('shopro_split_order') ->where('order_id', $order['id']) ->where('channel', 'huifu') ->order('id desc') ->find(); if ($latest && intval($latest['status']) === 4 && !empty($latest['wx_result'])) { $raw = is_string($latest['wx_result']) ? $latest['wx_result'] : json_encode($latest['wx_result'], JSON_UNESCAPED_UNICODE); if (strpos($raw, '22000002') !== false || strpos($raw, '分账配置') !== false) { echo "\n【最近失败】id={$latest['id']} → 22000002:若已部署「主商户不写 acct_infos」仍失败,请在控台绑定子商户分账关系\n"; } elseif (strpos($raw, 'FREEZE') !== false) { echo "\n【最近失败】id={$latest['id']} → 资金冻结,见上查单\n"; } } $payHuifuId = Db::name('shopro_huifu_config')->where('name', 'huifu_id')->value('value'); if ($payHuifuId) { echo " · 插件支付 huifu_id={$payHuifuId}(应与 platform_huifu_id / 控台主商户一致)\n"; } printHuifuAcctInfosPreview($order); } function printHuifuAcctInfosPreview(array $order) { if (!class_exists('\\addons\\shopro\\library\\split\\SplitCalculator') || !class_exists('\\addons\\shopro_huifu\\library\\HuifuSplitService')) { return; } $items = (new \addons\shopro\library\split\SplitCalculator())->calculate($order['id']); if (empty($items)) { return; } $service = new \addons\shopro_huifu\library\HuifuSplitService(); $payFee = number_format((float) $order['pay_fee'], 2, '.', ''); $confirmBase = $service->previewConfirmPayFee($order['order_sn'], $payFee); $ref = new \ReflectionClass($service); $method = $ref->getMethod('buildAcctInfos'); $method->setAccessible(true); try { $acctInfos = $method->invoke($service, $items, $confirmBase); } catch (\Throwable $e) { echo "\n【acct_infos 预览】组装失败: " . $e->getMessage() . "\n"; return; } echo "\n【将提交汇付 acct_infos】confirm_base={$confirmBase}\n"; foreach ($acctInfos as $row) { echo ' · huifu_id=' . ($row['huifu_id'] ?? '-') . ' div_amt=' . ($row['div_amt'] ?? '-') . "\n"; } $splitSum = '0.00'; foreach ($acctInfos as $row) { $splitSum = bcadd($splitSum, number_format((float) ($row['div_amt'] ?? 0), 2, '.', ''), 2); } $stay = bcsub($confirmBase, $splitSum, 2); if (bccomp($stay, '0.00', 2) > 0) { echo " · (其余 {$stay} 元留在收款主商户,不写入分账串)\n"; } } function diagnoseMissingSplitOrders($limit) { $since = time() - 90 * 86400; $rows = fetchMissingSplitOrders($since, $limit); echo "========================================\n"; echo " 未分账订单诊断(近90天,最多 {$limit} 笔)\n"; echo "========================================\n\n"; if (empty($rows)) { echo "无匹配订单。\n"; return; } foreach ($rows as $row) { echo str_repeat('-', 60) . "\n"; echo "订单: {$row['order_sn']} id={$row['id']} pay_fee={$row['pay_fee']}\n"; echo '支付: ' . date('Y-m-d H:i:s', $row['paytime']) . " pay_type={$row['pay_type']}\n"; foreach (diagnoseOrderSplitBlockers($row['id']) as $line) { echo " · {$line}\n"; } echo "\n"; } echo "补跑(需 delay_acct_flag=Y 且接收方齐全):\n"; echo " php addons/shopro/tests/split_commission_verify.php try-split {order_sn} --execute\n"; } function trySplitOrder($orderSn, $execute, $confirmOnly = false) { if ($orderSn === '') { fwrite(STDERR, "请指定订单号\n"); exit(1); } $order = Db::name('shopro_order')->where('order_sn', $orderSn)->find(); if (!$order) { fwrite(STDERR, "订单不存在\n"); exit(1); } if (class_exists('\\addons\\shopro\\library\\split\\SplitCloseService') && (new \addons\shopro\library\split\SplitCloseService())->isClosed($order['id'])) { echo "该订单已标记线下结案,禁止 try-split --execute / --confirm-only\n"; exit($execute || $confirmOnly ? 1 : 0); } echo "诊断:\n"; foreach (diagnoseOrderSplitBlockers($order['id']) as $line) { echo " · {$line}\n"; } printTrySplitChannelHints($order); if (!class_exists('\\addons\\shopro_huifu\\library\\HuifuSplitService')) { fwrite(STDERR, "未安装 shopro_huifu\n"); exit(1); } $service = new \addons\shopro_huifu\library\HuifuSplitService(); $preview = $service->previewDelaytransConfirmExtend($order['id']); if ($preview) { echo "\n【confirm 请求预览(写入日志 + 下方 JSON)】\n"; echo json_encode($preview, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n"; } if (!$execute && !$confirmOnly) { echo "\n未执行分账。确认汇付控台分账关系 + 非 FREEZE 后再加 --execute\n"; echo "排查 22000002 可先: try-split {$orderSn} --confirm-only(仅确认、不分账)\n"; echo "(本单若佣金已入钱包,--execute 成功可能造成测试单双轨,正式单请先对齐 split_timing 与分销结算)\n"; return; } $payFee = number_format((float) $order['pay_fee'], 2, '.', ''); $confirmBase = $service->previewConfirmPayFee($order['order_sn'], $payFee); echo "\n汇付确认基数: pay_fee={$payFee} → confirm_base={$confirmBase}"; if (bccomp($confirmBase, $payFee, 2) < 0) { echo "(已扣手续费/待确认金额)"; } echo "\n"; try { $query = (new \addons\shopro_huifu\library\HuifuQueryService())->queryByOrderSn($order['order_sn']); $snippet = [ 'trans_amt' => $query['trans_amt'] ?? null, 'unconfirm_amt' => $query['unconfirm_amt'] ?? null, 'settlement_amt' => $query['settlement_amt'] ?? null, 'delay_acct_flag' => $query['delay_acct_flag'] ?? null, 'fund_freeze_stat' => $query['fund_freeze_stat'] ?? null, 'freeze_time' => $query['freeze_time'] ?? null, 'unfreeze_time' => $query['unfreeze_time'] ?? null, 'trans_stat' => $query['trans_stat'] ?? null, ]; echo '查单摘要: ' . json_encode($snippet, JSON_UNESCAPED_UNICODE) . "\n"; } catch (\Throwable $e) { echo '查单摘要: 失败 ' . $e->getMessage() . "\n"; } if ($confirmOnly) { echo "\n>>> 执行 --confirm-only(不传 acct_split_bunch)\n"; try { $result = $service->executeConfirmOnlyByOrderId($order['id']); echo "仅确认返回: " . json_encode($result, JSON_UNESCAPED_UNICODE) . "\n"; echo "若成功:说明延迟户/解冻正常,22000002 来自分账接收方 6666000245630872 未在控台绑定或 rule_origin≠01\n"; } catch (\Throwable $e) { echo "仅确认失败: " . $e->getMessage() . "\n"; } return; } $ok = $service->executeByOrderId($order['id']); if ($ok) { echo "\n分账执行返回: 成功\n"; return; } $latest = Db::name('shopro_split_order') ->where('order_id', $order['id']) ->where('channel', 'huifu') ->order('id', 'desc') ->find(); echo "\n分账执行返回: 失败\n"; if ($latest) { echo "最新分账记录 id={$latest['id']} status={$latest['status']} split_amount={$latest['split_amount']}\n"; if (!empty($latest['wx_result'])) { echo "wx_result: {$latest['wx_result']}\n"; } } else { echo "未写入 shopro_split_order(可能明细为空或 pay_type 非 huifu_wechat)\n"; } } function printOrderDetail($orderSn) { $order = Db::name('shopro_order')->where('order_sn', $orderSn)->find(); if (!$order) { echo "订单不存在: {$orderSn}\n"; exit(1); } $oid = $order['id']; echo "订单 {$orderSn} id={$oid} pay_type={$order['pay_type']} pay_fee={$order['pay_fee']}\n\n"; $splits = Db::name('shopro_split_order')->where('order_id', $oid)->select(); echo "【分账主表】\n"; print_r($splits); if ($splits) { $ids = array_column($splits, 'id'); $lines = Db::name('shopro_split_order_receiver')->alias('sor') ->join('shopro_split_receiver sr', 'sr.id = sor.receiver_id', 'LEFT') ->where('sor.split_order_id', 'in', $ids) ->field('sor.*, sr.receiver_account, sr.channel, sr.receiver_type') ->select(); echo "\n【分账明细 → 接收方账户(网关去向)】\n"; print_r($lines); } $cr = Db::name('shopro_commission_reward')->where('order_id', $oid)->select(); $rr = selectRegionalRewardsByOrderId($oid); echo "\n【分销/区域佣金记录(业务层)】\n"; echo "commission_reward: " . count($cr) . " 条\n"; echo "regional_reward: " . count($rr) . " 条\n"; $rewardIds = array_merge( array_column($cr, 'id'), array_column($rr, 'id') ); $wl = []; if ($rewardIds) { $wl = Db::name('shopro_user_wallet_log') ->where('item_id', 'in', $rewardIds) ->where('wallet_type', 'money') ->where('type', 'in', ['commission_income', 'regional_agent_income', 'commission_back', 'regional_agent_back']) ->select(); } echo "\n【钱包流水(用户可提现余额来源)】\n"; print_r($wl); $ext = Db::name('shopro_huifu_pay_ext')->where('order_id', $oid)->find(); if ($ext) { echo "\n【汇付支付扩展】\n"; print_r($ext); } echo "\n【分账阻塞诊断】\n"; foreach (diagnoseOrderSplitBlockers($oid) as $line) { echo " · {$line}\n"; } } /** * 延迟专户余额排查:列出 delay_acct=Y 订单,可选 live 查汇付 unconfirm_amt */ /** 从汇付查单响应(可能嵌套 JSON 字符串)取字段 */ function huifuQueryFieldRecursive($data, $key) { if (is_string($data)) { $trim = trim($data); if ($trim !== '' && ($trim[0] === '{' || $trim[0] === '[')) { $decoded = json_decode($trim, true); if (is_array($decoded)) { return huifuQueryFieldRecursive($decoded, $key); } } return null; } if (!is_array($data)) { return null; } if (array_key_exists($key, $data)) { return $data[$key]; } foreach ($data as $v) { $found = huifuQueryFieldRecursive($v, $key); if ($found !== null && $found !== '') { return $found; } } return null; } function auditDelayUnconfirm($limit, $live) { $since = time() - 180 * 86400; $peFields = 'o.id, o.order_sn, o.pay_fee, o.paytime, o.status, pe.confirm_status, pe.confirm_hf_seq_id'; if (tableHasColumn('shopro_huifu_pay_ext', 'split_timing_at_pay')) { $peFields .= ', pe.split_timing_at_pay'; } $rows = Db::name('shopro_order')->alias('o') ->join('shopro_huifu_pay_ext pe', 'pe.order_id = o.id') ->where('o.pay_type', 'huifu_wechat') ->where('o.paytime', '>=', $since) ->whereRaw("UPPER(IFNULL(pe.delay_acct_flag, '')) = 'Y'") ->field($peFields) ->order('o.paytime', 'desc') ->limit($limit) ->select(); echo "========================================\n"; echo " 延时分账订单审计(最多 {$limit} 笔,近180天 delay_acct=Y)\n"; echo " --live 会调汇付支付查单,以 unconfirm_amt 为准\n"; echo "========================================\n\n"; $querySvc = null; if ($live && class_exists('\\addons\\shopro_huifu\\library\\HuifuQueryService')) { $querySvc = new \addons\shopro_huifu\library\HuifuQueryService(); } elseif ($live) { echo "⚠ 未安装 shopro_huifu,忽略 --live\n\n"; } $sumUnconfirm = '0.00'; $pendingCount = 0; foreach ($rows as $row) { $oid = intval($row['id']); $split = Db::name('shopro_split_order') ->where('order_id', $oid) ->where('channel', 'huifu') ->order('id', 'desc') ->find(); $splitStatus = $split ? intval($split['status']) : -1; $splitLabel = $split ? (['0' => '待', '1' => '中', '2' => '成功', '3' => '部分', '4' => '失败'][$splitStatus] ?? $splitStatus) : '无记录'; $unconfirm = null; $remoteDelay = null; $freeze = null; $confirmResp = null; if ($querySvc) { try { $q = $querySvc->queryByOrderSn($row['order_sn']); $unconfirm = huifuQueryFieldRecursive($q, 'unconfirm_amt'); $remoteDelay = huifuQueryFieldRecursive($q, 'delay_acct_flag'); $freeze = huifuQueryFieldRecursive($q, 'fund_freeze_stat'); if ($unconfirm !== null && $unconfirm !== '' && bccomp((string) $unconfirm, '0.00', 2) > 0) { $sumUnconfirm = bcadd($sumUnconfirm, number_format((float) $unconfirm, 2, '.', ''), 2); $pendingCount++; } } catch (\Throwable $e) { $confirmResp = '查单失败: ' . $e->getMessage(); } } $localClaimOk = ($row['confirm_status'] === 'S' || $splitStatus === 2); $flagMismatch = false; if ($unconfirm !== null && $unconfirm !== '' && bccomp((string) $unconfirm, '0.00', 2) > 0 && $localClaimOk) { $flagMismatch = true; } echo str_repeat('-', 72) . "\n"; echo "order_sn: {$row['order_sn']} id={$oid} pay_fee={$row['pay_fee']}\n"; $timingAtPay = isset($row['split_timing_at_pay']) ? $row['split_timing_at_pay'] : '-'; echo 'pay_time: ' . date('Y-m-d H:i:s', $row['paytime']) . " order_status={$row['status']} split_timing_at_pay={$timingAtPay}\n"; echo "本地: confirm_status=" . ($row['confirm_status'] ?: 'NULL') . " confirm_hf_seq_id=" . ($row['confirm_hf_seq_id'] ?: '-') . "\n"; echo "本地分账: {$splitLabel}" . ($split ? " split_amount={$split['split_amount']} id={$split['id']}" : '') . "\n"; if ($querySvc && $confirmResp === null) { echo "汇付查单: delay_acct_flag={$remoteDelay} unconfirm_amt={$unconfirm} fund_freeze_stat={$freeze}\n"; } elseif ($confirmResp) { echo "{$confirmResp}\n"; } if ($flagMismatch) { echo ">>> ⚠ 本地显示已确认/分账成功,但汇付仍有 unconfirm_amt(符合汇付说的「返回链路/终态未对齐」)\n"; } elseif ($splitLabel === '无记录') { echo ">>> ⚠ 从未分账:多为 split_timing=confirm 且未确认收货,或当时未触发分账\n"; } elseif ($splitStatus === 4) { echo ">>> ⚠ 分账失败,资金可能仍在延迟户\n"; } elseif ($unconfirm !== null && bccomp((string) $unconfirm, '0.00', 2) > 0) { echo ">>> ⚠ 汇付侧仍有待确认金额\n"; } } echo "\n========================================\n"; if ($querySvc) { echo "汇付查单汇总: 仍有 unconfirm_amt>0 的订单 {$pendingCount} 笔,合计约 ¥{$sumUnconfirm}\n"; echo "(可与控台「延迟户」余额对照;内扣手续费后单笔待确认略小于 pay_fee)\n"; } else { echo "加 --live 调汇付查单 unconfirm_amt,才能精确对应延迟户余额。\n"; } echo "处理: 无分账记录 → 确认收货或 try-split --execute;仅释款 → try-split --confirm-only\n"; } function printRecentSuccess($limit) { $rows = Db::name('shopro_split_order')->alias('so') ->join('shopro_order o', 'o.id = so.order_id') ->where('so.status', 'in', [2, 3]) ->field('so.order_sn, so.channel, so.total_amount, so.split_amount, so.createtime, o.pay_type') ->order('so.id desc') ->limit($limit) ->select(); echo "最近 {$limit} 笔分账成功订单:\n"; foreach ($rows as $r) { $remain = round($r['total_amount'] - $r['split_amount'], 2); echo date('Y-m-d H:i', $r['createtime']) . " {$r['order_sn']} {$r['channel']} pay={$r['total_amount']} split={$r['split_amount']} platform_remain≈{$remain}\n"; } }