fix(payment): re-pay with fresh provider out_trade_no
WeChat/Alipay/Epay reject reusing the same merchant order number when params change. Allocate attempt-suffixed out_trade_no on resume pay, map callbacks back to the local order, and best-effort close prior trades. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -510,4 +510,135 @@ trait OrderRepositoryCore {
|
||||
return 'AKS' . date('YmdHis') . str_pad((string) random_int(0, 9999), 4, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge keys into the order note JSON payload (meta_payload).
|
||||
*
|
||||
* @param array<string,mixed> $patch
|
||||
*/
|
||||
public function merge_meta(string $order_number, array $patch): bool {
|
||||
global $wpdb;
|
||||
|
||||
$order = $this->get($order_number);
|
||||
if (!$order) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : [];
|
||||
foreach ($patch as $key => $value) {
|
||||
$meta[(string) $key] = $value;
|
||||
}
|
||||
|
||||
$encoded = wp_json_encode($meta, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if (!is_string($encoded)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$updated = $wpdb->update(
|
||||
$this->table(),
|
||||
['note' => $encoded],
|
||||
['order_number' => $order_number],
|
||||
['%s'],
|
||||
['%s']
|
||||
);
|
||||
|
||||
return $updated !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a local order from a provider out_trade_no (base or attempt-suffixed).
|
||||
*/
|
||||
public function resolve_by_out_trade_no(string $out_trade_no): ?array {
|
||||
$out_trade_no = sanitize_text_field($out_trade_no);
|
||||
if ($out_trade_no === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$direct = $this->get($out_trade_no);
|
||||
if ($direct) {
|
||||
return $direct;
|
||||
}
|
||||
|
||||
// Resume attempts: {order_number}P{n}
|
||||
if (preg_match('/^(AKS[0-9]{10,})P([0-9]{1,3})$/', $out_trade_no, $m)) {
|
||||
$base = $this->get($m[1]);
|
||||
if ($base) {
|
||||
return $base;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider out_trade_no currently bound to this order (falls back to order_number).
|
||||
*
|
||||
* @param array<string,mixed> $order
|
||||
*/
|
||||
public static function provider_out_trade_no(array $order): string {
|
||||
$meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : [];
|
||||
$out = sanitize_text_field((string) ($meta['provider_out_trade_no'] ?? ''));
|
||||
if ($out !== '') {
|
||||
return $out;
|
||||
}
|
||||
return sanitize_text_field((string) ($order['order_number'] ?? ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocate a new provider out_trade_no for a payment attempt.
|
||||
* First attempt uses bare order_number; later attempts append Pn (WeChat limit 32).
|
||||
*
|
||||
* @return array{out_trade_no:string,attempt:int,previous:string}|null
|
||||
*/
|
||||
public function allocate_payment_attempt(string $order_number): ?array {
|
||||
$order = $this->get($order_number);
|
||||
if (!$order) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : [];
|
||||
$previous = sanitize_text_field((string) ($meta['provider_out_trade_no'] ?? ''));
|
||||
$history = is_array($meta['provider_out_trade_nos'] ?? null) ? $meta['provider_out_trade_nos'] : [];
|
||||
|
||||
// First provider submission keeps the bare order number.
|
||||
// Re-pay (provider_out_trade_no already set) always mints a new suffix so
|
||||
// WeChat/Alipay/Epay accept the order when amount/subject/notify changed.
|
||||
if ($previous === '') {
|
||||
$attempt = 1;
|
||||
$out = $order_number;
|
||||
} else {
|
||||
$attempt = max(1, (int) ($meta['pay_attempt'] ?? 1)) + 1;
|
||||
$suffix = 'P' . $attempt;
|
||||
$base = $order_number;
|
||||
if (strlen($base) + strlen($suffix) > 32) {
|
||||
$base = substr($base, 0, 32 - strlen($suffix));
|
||||
}
|
||||
$out = $base . $suffix;
|
||||
if ($previous !== '' && !in_array($previous, $history, true)) {
|
||||
$history[] = $previous;
|
||||
}
|
||||
}
|
||||
|
||||
if (!in_array($out, $history, true)) {
|
||||
$history[] = $out;
|
||||
}
|
||||
if (count($history) > 20) {
|
||||
$history = array_slice($history, -20);
|
||||
}
|
||||
|
||||
$ok = $this->merge_meta($order_number, [
|
||||
'pay_attempt' => $attempt,
|
||||
'provider_out_trade_no' => $out,
|
||||
'provider_out_trade_nos' => array_values($history),
|
||||
]);
|
||||
if (!$ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'out_trade_no' => $out,
|
||||
'attempt' => $attempt,
|
||||
'previous' => $previous,
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user