| | |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | |
| | | /** |
| | | * Second confirmation path for payments whose callback did not arrive or whose outcome was unknown. |
| | |
| | | * 这次尝试停在 {@code CLOSING} 等后台重试,并记一条问题记录给人工。</li> |
| | | * <li>关单处理中:这次尝试停在新状态 {@code CLOSING}(关单中)——它既不能当已关闭(通道还可能 |
| | | * 收钱),也不该是终点,做成可等待、可重试的中间态。</li> |
| | | * <li>原交易已经终结({@code 23000000}/{@code 10000016}):不是「现在不让关」,而是「它已经 |
| | | * 结束了」——那张预支付单不可能再被支付,落成已关闭,不再交回客户端。</li> |
| | | * <li>其余拒绝(未满一分钟不允许关单、原交易不支持关单、关单请求本身失败):允许取消,但流水记为 |
| | | * {@code CANCEL_UNCONFIRMED} 而不是已关闭——对账任务仍会盯着这笔请求,万一用户后来真的付了, |
| | | * 会留下问题记录让人处理,而不是无声地变成一笔没有订单的悬账。</li> |
| | |
| | | markClosing(transaction, close.code(), close.message()); |
| | | return new PendingPaymentConfirmation(PendingPaymentState.UNCONFIRMED, paymentNo); |
| | | } |
| | | if (close.originalTradeAlreadyTerminal()) { |
| | | // 关单回执说的不是「现在不让关」,而是「原交易已经终结」。此时再关只会一直拿到同一句拒绝 |
| | | // (旧实现就把它当成了「关不掉=可能还能付」),而一张死掉的预支付单被交回客户端时,微信 |
| | | // 收银台会直接报「订单状态异常,支付请求无效」;订单也会挂成一个既付不了又取消不了的僵尸。 |
| | | // 这里落成已关闭:取消那条路与关单补偿都是先查过单、通道说过这笔钱没收下才走到关单的, |
| | | // 「放弃支付」那条路则是用户刚关掉收银台——钱真收下了会有异步通知把它结算掉(迟到的成功 |
| | | // 记成「取消后才收到钱」,交给悬账退款与人工),不会无声地消失。 |
| | | markCancelledTransaction(paymentNo, "CLOSED", close.code(), close.message()); |
| | | issues.resolve("PAYMENT_CLOSE_UNCONFIRMED", paymentNo, "system"); |
| | | return new PendingPaymentConfirmation(PendingPaymentState.NONE, paymentNo); |
| | | } |
| | | if (close.processing()) { |
| | | issues.issue("PAYMENT_CLOSE_UNCONFIRMED", paymentNo, orderNo, "closed at provider", "close processing", amount, |
| | | "Provider is still closing the payment"); |
| | |
| | | * |
| | | * <p>到上限时通道既没说关掉、也没说钱收下了,而这张预支付单还可能被支付。此时订单不能继续挂着 |
| | | * 「待支付」——那样它既付不了(不允许在同一订单上再建一张预支付单)也永远不会被自动取消,变成一笔 |
| | | * 僵尸订单。所以这里把订单落成已取消,并把这次尝试留成 {@code CANCEL_UNCONFIRMED}: |
| | | * 订单状态不由自动取消的排除表管理,而是由这次明确的「不再重试」结论结束; |
| | | * 预支付单继续被对账盯着,万一用户后来真的付了,会记成「取消后才收到钱」的问题记录, |
| | | * 僵尸订单:同一件商品再也下不了单。所以这里把订单落成已取消,并把这次尝试留成 |
| | | * {@code CANCEL_UNCONFIRMED}:订单状态不由自动取消的排除表管理,而是由这次明确的「不再重试」结论 |
| | | * 结束;预支付单继续被对账盯着,万一用户后来真的付了,会记成「取消后才收到钱」的问题记录, |
| | | * 交给悬账自动退款与人工处理,而不是无声地留下没有出口的悬账。 |
| | | * |
| | | * <p>这个终止条件按「这次尝试」而不是按它的当前状态来下:流水可能从没经过 {@code CLOSING} |
| | | * (关单第一次就被拒,直接落在 {@code CANCEL_UNCONFIRMED}),只认 {@code CLOSING} 的话这类订单 |
| | | * 永远结束不了。 |
| | | */ |
| | | private void giveUpClosing(Map<String, Object> transaction) { |
| | | String paymentNo = text(transaction.get("paymentNo")); |
| | |
| | | issues.issue("PAYMENT_CLOSE_UNCONFIRMED", paymentNo, orderNo, "closed at provider", "close attempts exhausted", amount, |
| | | "Payment could not be closed after " + MAX_CLOSE_ATTEMPTS + " attempts; the prepay stays watchable"); |
| | | jdbc.update(""" |
| | | update benefit_order set order_status = 2 |
| | | where order_no = ? and deleted = 0 and order_status = 0 |
| | | update benefit_order o set order_status = 2 |
| | | where o.order_no = ? and o.deleted = 0 and o.order_status = 0 |
| | | and not exists ( |
| | | select 1 from payment_transaction p |
| | | where p.order_id = o.id and p.status = 'SUCCEEDED' |
| | | ) |
| | | """, orderNo); |
| | | } |
| | | |
| | |
| | | """, status, code, message, paymentNo); |
| | | } |
| | | |
| | | /** 通道对关单请求的答复。只有「关掉了」和「原交易已成功」是明确结论,其余一律当作没确认下来。 */ |
| | | /** 通道对关单请求的答复。只要还没说「关掉了/原交易已成功/原交易已终结」,就当作没确认下来。 */ |
| | | private record CloseResult(String code, String message, String status, String originalStatus) { |
| | | /** 汇付「原订单已为终态」的两个返回码:原交易已终结(已关单或已完成),无法再关单。 */ |
| | | private static final Set<String> TERMINAL_CODES = Set.of("23000000", "10000016"); |
| | | |
| | | private static CloseResult from(Map<String, Object> response) { |
| | | Map<String, Object> data = response == null ? Map.of() |
| | | : (response.get("data") instanceof Map<?, ?> map ? (Map<String, Object>) map : response); |
| | |
| | | |
| | | boolean originalTradeSucceeded() { |
| | | return "S".equalsIgnoreCase(originalStatus); |
| | | } |
| | | |
| | | /** |
| | | * 通道拒绝关单,理由是原交易已经终结。 |
| | | * |
| | | * <p>这与「未满一分钟不允许关单」这类拒绝不是一回事:后者说的是「现在不让关」,预支付单还活着; |
| | | * 这一句说的是「它已经结束了」,那张预支付单不可能再被支付。 |
| | | */ |
| | | boolean originalTradeAlreadyTerminal() { |
| | | return TERMINAL_CODES.contains(code); |
| | | } |
| | | |
| | | boolean processing() { |
| | |
| | | private void retryClose(Map<String, Object> transaction) { |
| | | String paymentNo = text(transaction.get("paymentNo")); |
| | | if (number(transaction.get("closeAttempts")) >= MAX_CLOSE_ATTEMPTS) { |
| | | // 到上限:先给还停在「关单中」的尝试一个终止条件(落成已取消并结束订单),之后只查不关。 |
| | | if (CLOSING_STATUS.equals(text(transaction.get("status")))) giveUpClosing(transaction); |
| | | else watchOnly(transaction); |
| | | // 到上限:关单已经问不出更多结论,再打通道只是重复。先查一次(钱真的来了要先落成已支付, |
| | | // 那一支会把订单翻成已支付,下面的收尾就不会再取消它),再给这次尝试一个终止条件。 |
| | | watchOnly(transaction); |
| | | giveUpClosing(transaction); |
| | | return; |
| | | } |
| | | issues.resolve("PAYMENT_CLOSE_UNCONFIRMED", paymentNo, "system"); |