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 次删除

查看文件

@@ -0,0 +1,150 @@
-- 汇付「延时分账待交易确认」与商城分账记录对账
-- 用法:在 MySQL 执行(表前缀 fa_ 请按实际修改)
-- 图1 汇付控台「商品描述=商城订单{order_sn}」可与 order_sn 直接对应
SELECT '=== 1. 当前分账配置(是否支付即分账)===' AS section;
SELECT name, value, description
FROM fa_shopro_split_config
WHERE name IN (
'split_enable', 'split_mode', 'split_provider', 'split_timing',
'split_amount_source', 'delay_acct_enable', 'platform_huifu_id'
);
-- 汇付控台截图中的 8 笔订单号(可按需增删)
SET @huifu_pending_sns = '202605495163454091004900,202609215157744091003800,202604274596990091003800,202606500438103091004500,202608341834105091004400,202607585392320091004300,202602490038750091003800,202602385010837091001400';
SELECT '=== 2. 汇付待确认列表 vs 商城分账/支付扩展(核心对账)===' AS section;
SELECT
o.id AS order_id,
o.order_sn,
o.status AS order_status,
o.pay_fee,
FROM_UNIXTIME(o.paytime) AS pay_time,
FROM_UNIXTIME(
NULLIF(CAST(JSON_UNQUOTE(JSON_EXTRACT(o.ext, '$.confirm_time')) AS UNSIGNED), 0)
) AS buyer_confirm_time,
pe.delay_acct_flag,
pe.confirm_status,
pe.confirm_hf_seq_id,
pe.hf_seq_id,
so.id AS split_order_id,
so.status AS split_status,
CASE so.status
WHEN 0 THEN '待分账'
WHEN 1 THEN '分账中'
WHEN 2 THEN '成功'
WHEN 3 THEN '部分成功'
WHEN 4 THEN '失败'
ELSE IF(so.id IS NULL, '无分账记录', so.status)
END AS split_status_label,
so.split_source,
so.split_amount,
LEFT(so.wx_result, 160) AS split_fail_hint,
CASE
WHEN pe.delay_acct_flag IS NULL OR pe.delay_acct_flag = '' THEN '⚠ 无 pay_ext 或未记录 delay 标记'
WHEN UPPER(pe.delay_acct_flag) <> 'Y' THEN '✗ 非延时分账单,控台待确认需单独核对'
WHEN pe.confirm_status = 'S' OR so.status = 2 THEN '✓ 已交易确认/分账成功'
WHEN so.id IS NULL THEN '✗ 从未生成分账记录(常见:split_timing≠当前订单状态,或支付时未开分账)'
WHEN so.status = 4 THEN '✗ 分账失败,未 confirm 成功(资金仍可能卡在汇付)'
WHEN so.status IN (0, 1) THEN '… 分账处理中'
ELSE '请人工核对'
END AS reconcile_hint
FROM fa_shopro_order o
LEFT JOIN fa_shopro_huifu_pay_ext pe ON pe.order_id = o.id
LEFT JOIN (
SELECT t.*
FROM fa_shopro_split_order t
INNER JOIN (
SELECT order_id, MAX(id) AS max_id
FROM fa_shopro_split_order
WHERE channel = 'huifu'
GROUP BY order_id
) x ON x.max_id = t.id
) so ON so.order_id = o.id
WHERE FIND_IN_SET(o.order_sn, REPLACE(@huifu_pending_sns, ' ', ''))
ORDER BY o.paytime DESC;
SELECT '=== 3. 近90天:延时分账(Y)但未 confirm 成功(与控台待释放资金相关)===' AS section;
SELECT
o.id AS order_id,
o.order_sn,
o.pay_fee,
FROM_UNIXTIME(o.paytime) AS pay_time,
pe.confirm_status,
pe.confirm_hf_seq_id,
IFNULL(so.status, -1) AS split_status,
LEFT(so.wx_result, 120) AS fail_msg
FROM fa_shopro_order o
INNER JOIN fa_shopro_huifu_pay_ext pe ON pe.order_id = o.id
LEFT JOIN (
SELECT order_id, MAX(status) AS status, MAX(wx_result) AS wx_result
FROM fa_shopro_split_order WHERE channel = 'huifu' GROUP BY order_id
) so ON so.order_id = o.id
WHERE o.pay_type = 'huifu_wechat'
AND o.paytime >= UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 90 DAY))
AND UPPER(IFNULL(pe.delay_acct_flag, '')) = 'Y'
AND IFNULL(pe.confirm_status, '') <> 'S'
AND IFNULL(so.status, 0) <> 2
ORDER BY o.paytime DESC
LIMIT 100;
SELECT '=== 4. 有延时分账标记但从未出现分账记录(高概率未触发 split)===' AS section;
SELECT
o.id,
o.order_sn,
FROM_UNIXTIME(o.paytime) AS pay_time,
o.status AS order_status,
pe.delay_acct_flag,
(SELECT value FROM fa_shopro_split_config WHERE name = 'split_timing' LIMIT 1) AS split_timing
FROM fa_shopro_order o
INNER JOIN fa_shopro_huifu_pay_ext pe ON pe.order_id = o.id
LEFT JOIN fa_shopro_split_order so ON so.order_id = o.id AND so.channel = 'huifu'
WHERE o.pay_type = 'huifu_wechat'
AND o.paytime >= UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 90 DAY))
AND UPPER(pe.delay_acct_flag) = 'Y'
AND so.id IS NULL
ORDER BY o.id DESC
LIMIT 50;
SELECT '=== 5. 分账失败(status=4) 汇付延时分账单 ===' AS section;
SELECT
so.order_id,
so.order_sn,
so.split_amount,
so.retry_count,
FROM_UNIXTIME(so.createtime) AS split_at,
LEFT(so.wx_result, 200) AS fail_reason
FROM fa_shopro_split_order so
INNER JOIN fa_shopro_order o ON o.id = so.order_id
INNER JOIN fa_shopro_huifu_pay_ext pe ON pe.order_id = o.id
WHERE so.channel = 'huifu'
AND so.status = 4
AND UPPER(IFNULL(pe.delay_acct_flag, '')) = 'Y'
ORDER BY so.id DESC
LIMIT 50;
SELECT '=== 6. 本地标记已 confirm 但可能未真正释放(需 CLI audit-delay --live 核实)===' AS section;
SELECT
o.order_sn,
o.pay_fee,
pe.confirm_status,
pe.confirm_hf_seq_id,
so.status AS split_status,
so.confirm_hf_seq_id AS split_confirm_seq,
FROM_UNIXTIME(so.createtime) AS split_at
FROM fa_shopro_order o
INNER JOIN fa_shopro_huifu_pay_ext pe ON pe.order_id = o.id
LEFT JOIN (
SELECT order_id, MAX(id) AS max_id FROM fa_shopro_split_order WHERE channel = 'huifu' GROUP BY order_id
) x ON x.order_id = o.id
LEFT JOIN fa_shopro_split_order so ON so.id = x.max_id
WHERE o.pay_type = 'huifu_wechat'
AND UPPER(pe.delay_acct_flag) = 'Y'
AND (pe.confirm_status = 'S' OR so.status = 2)
ORDER BY o.paytime DESC
LIMIT 50;

查看文件

@@ -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')