feat(shopro): 添加汇付延时分账对账功能

- 新增 huifu_delay_reconcile.sql 对账脚本,包含多个维度的分账核对查询
- 在 split_commission_verify.php 中添加 audit-delay 命令,支持实时查询汇付未确认金额
- 实现递归解析汇付查单响应的功能,支持嵌套JSON数据提取
- 提供延时分账订单审计功能,可对比本地状态与汇付侧 unconfirm_amt
- 支持批量诊断延时分账待确认订单,便于排查资金卡顿问题
```
这个提交包含在:
可乐加冰
2026-10-23 15:12:47 +08:00
父节点 f1e2be8dc4
当前提交 0d1256a679
共有 2 个文件被更改,包括 288 次插入 和 1 次删除

查看文件

@@ -10,6 +10,7 @@
* php addons/shopro/tests/split_commission_verify.php diagnose-missing [limit=20]
* php addons/shopro/tests/split_commission_verify.php try-split {order_sn} --execute # 补跑汇付分账(慎用)
* php addons/shopro/tests/split_commission_verify.php try-split {order_sn} --confirm-only # 仅延时分账确认、不分账(排查22000002)
* php addons/shopro/tests/split_commission_verify.php audit-delay [limit=30] [--live] # 延迟户对账:本地+可选汇付查单 unconfirm_amt
*/
if (PHP_SAPI !== 'cli') {
@@ -62,9 +63,14 @@ switch ($command) {
$confirmOnly = in_array('--confirm-only', $argv, true);
trySplitOrder($sn, $execute, $confirmOnly);
break;
case 'audit-delay':
$limit = max(1, min(200, intval($argv[2] ?? 30)));
$live = in_array('--live', $argv, true);
auditDelayUnconfirm($limit, $live);
break;
default:
echo "未知命令: {$command}\n";
echo "可用: summary | anomalies | order {order_sn} | recent-success [limit] | diagnose-missing [limit] | try-split {order_sn} [--execute|--confirm-only]\n";
echo "可用: summary | anomalies | order {order_sn} | recent-success [limit] | diagnose-missing [limit] | try-split {order_sn} [--execute|--confirm-only] | audit-delay [limit] [--live]\n";
exit(1);
}
@@ -718,6 +724,137 @@ function printOrderDetail($orderSn)
}
}
/**
* 延迟专户余额排查:列出 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')