-- 【数据修复,不是 DDL】人工把两笔卡死的退款标记为失败、释放在途闸门(2026-10-06) -- 手工执行:psql -f 本文件,或用任意客户端逐条跑。执行由用户做,读数请回贴。 -- -- 【本脚本尚未执行;执行前先把下面「执行前提」读完】 -- -- ── 这是什么 ───────────────────────────────────────────────────────── -- 代码注释里写明的**第 ③ 条出口「人工改库」**:退款行停在 REFUNDING 时 -- (对账腿不收敛、回调又不来),由人工核对上游后手动落 FAIL,释放 -- uk_lsy_refund_one_pending 与在途闸门 ⇒ 该订单恢复可退款。 -- -- ── ★★ 执行前提(缺一不可)────────────────────────────────────────── -- 1) **必须先去上游(乐收赢/汇安融商户后台,或直接问上游)确认这两笔退款没有成功出金。** -- 本脚本的全部依据只有:同步应答说 PAY_FAIL(余额不足)+ 对账腿不收敛 -- (2026-10-06 11:40 实测:两行仍是 REFUNDING,游标新鲜 —— updated_time + 60s = -- next_query_time ⇒ 腿在跑;问到 PAY_FAIL/PAY_SUCCESS 都会当场落库 ⇒ 还停着只能是 -- 「每轮答复都不终局(上游回在途/查无)」或「查询调用每轮失败(日志里有 WARN/ERROR)」)。 -- 而 R11/R-1 这套「不落终态」的设计前提恰恰是「上游可能误报」——如果钱其实退出去了, -- 写 FAIL 会放闸 ⇒ 同一张单可以再退一次 ⇒ **双重出金**。这是本脚本唯一的风险面, -- 也是「为什么必须先人工核对」的全部理由。核对不确定 ⇒ 不跑本脚本。 -- 2) 若打算马上重退:先确认上游商户**可用余额已补**(否则重退会被同一理由再拒一次)。 -- 3) 跑之前先在订单页/退款页刷新一次,记下这两笔当前状态(下面 Step 3/4 要对照)。 -- -- ── 已知副作用(本 SQL 绕过应用层,照实列)──────────────────────────── -- 与正常终态通道(LsyRefundService.applyNotifyResult)相比,本 SQL **不做**两件事: -- a) 不发 SSE —— 前端订单实时刷新不会自动动;手动刷新照常看到「退款失败」; -- b) **不推下游退款回调** —— notify_status 留在 'PENDING',租户若配置了退款回调地址, -- 这两笔**不会**收到「退款失败」回调。正常通道会推(pushRefundCallbackIfTerminal)。 -- 这两笔是 2026-10-06 11:15 的批量退款测试单;若确实需要补一条回调, -- 先别跑本脚本,另行讨论。 -- 此外不动任何业务字段:upstream_result_code / upstream_result_msg 保留上游原话 -- ('PAY_FAIL' / 业务执行失败[…可用余额不足])。 -- -- ── 判据(三格取交集,逐格说清)────────────────────────────────────── -- refund_trace_no IN (...):**全局唯一**(uk_lsy_refund_trace_no)⇒ 单格即可钉死一行。 -- 两个值取自 2026-10-06 11:15:34 线上日志原文。 -- out_refund_no IN (...) :人眼可读的一致性核对(租户级唯一)。 -- status IN ('CREATED','REFUNDING'):守卫。已终态的行**一个字节都不动** -- (对账腿/回调若抢先收敛,这里命中 0 行)⇒ 本脚本幂等,重跑是空操作。 -- -- ── 写下去的值与谁对齐 ─────────────────────────────────────────────── -- status='FAIL' + finish_time=now() + updated_time=now(): -- 与 settleFromQuery → applyNotifyResult(..., "FAIL") 在终态流转时写的字段集一致 -- (applyNotifyResult 写 status / upstream_result_code / upstream_result_msg / -- finish_time / updated_time;这里不重写 code/msg,它们已是上游同步应答原文)。 -- -- ── 期望读数 ───────────────────────────────────────────────────────── -- Step 1 输出 2 行;UPDATE 报 **2**;Step 3 两行 status='FAIL'、finish_time 非空; -- Step 4 的 pending_refunds = 0。 -- ⚠ 若 UPDATE 报 0 或 1 行:**停下、别重跑**,把四段输出一起回贴 —— -- 说明行已被对账/回调抢先收敛,或行数不是预期的两笔,任一情况都要先核对再定。 -- Step 1(只读):先看命中哪些行,确认与上面两个单号一致 SELECT out_trade_no, out_refund_no, refund_trace_no, status, next_query_time, updated_time, finish_time FROM pay_lsy_refund WHERE refund_trace_no IN ('302d459a176e4f9f91de51c2111d1d3b', 'ac23251a4832497b9a7692e6ac53d97b') AND out_refund_no IN ('RF2107308753332903936', 'RF2107308757233606656') AND status IN ('CREATED', 'REFUNDING'); -- Step 2:标记失败(期望 2 行) UPDATE pay_lsy_refund SET status = 'FAIL', finish_time = now(), updated_time = now() WHERE refund_trace_no IN ('302d459a176e4f9f91de51c2111d1d3b', 'ac23251a4832497b9a7692e6ac53d97b') AND out_refund_no IN ('RF2107308753332903936', 'RF2107308757233606656') AND status IN ('CREATED', 'REFUNDING'); -- Step 3(只读):核对 —— 两行应为 FAIL、finish_time 非空 SELECT out_refund_no, status, upstream_result_code, upstream_result_msg, finish_time, updated_time FROM pay_lsy_refund WHERE out_refund_no IN ('RF2107308753332903936', 'RF2107308757233606656') ORDER BY created_time; -- Step 4(只读):订单侧应恢复可退(pending_refunds 期望 0) SELECT o.out_trade_no, o.status AS order_status, o.refunded_amount, (SELECT count(*) FROM pay_lsy_refund r WHERE r.order_id = o.id AND r.status IN ('CREATED', 'REFUNDING')) AS pending_refunds FROM pay_lsy_order o WHERE o.id IN (SELECT order_id FROM pay_lsy_refund WHERE out_refund_no IN ('RF2107308753332903936', 'RF2107308757233606656')); -- ── 回滚(正常情况下不要用)────────────────────────────────────────── -- 只有「执行后才发现上游其实还在处理/其实退成功了」才谈回滚,且必须先人工把事实核清。 -- 形式上就是把 status 从 'FAIL' 写回,但**不要**照抄一句简单的 UPDATE —— -- 那时状态已是终态、回调/对账都已被关在门外,回写要连同上游事实一起重新裁定。