```
feat(shopro): 添加汇付延时分账对账功能 - 新增 huifu_delay_reconcile.sql 对账脚本,包含多个维度的分账核对查询 - 在 split_commission_verify.php 中添加 audit-delay 命令,支持实时查询汇付未确认金额 - 实现递归解析汇付查单响应的功能,支持嵌套JSON数据提取 - 提供延时分账订单审计功能,可对比本地状态与汇付侧 unconfirm_amt - 支持批量诊断延时分账待确认订单,便于排查资金卡顿问题 ```
这个提交包含在:
@@ -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')
|
||||
|
||||
在新工单中引用
屏蔽一个用户