$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). * * The authoritative mapping is the order's own meta (provider_out_trade_no / * provider_out_trade_nos); the prefix heuristic is only a fallback and must * never be the sole resolver, otherwise a truncated attempt number would * strand a real payment. */ public function resolve_by_out_trade_no(string $out_trade_no): ?array { global $wpdb; $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; } // Authoritative: the attempt number was recorded in the order note JSON. $needle = '"provider_out_trade_no":"' . $out_trade_no . '"'; $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$this->table()} WHERE note LIKE %s ORDER BY id DESC LIMIT 1", '%' . $wpdb->esc_like($needle) . '%' ), ARRAY_A ); if (is_array($row)) { return $this->expire_order_if_due($this->normalize_row($row)); } // Superseded attempts stay in the history array. $history_needle = '"' . $out_trade_no . '"'; $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$this->table()} WHERE note LIKE %s AND note LIKE %s ORDER BY id DESC LIMIT 1", '%' . $wpdb->esc_like('"provider_out_trade_nos"') . '%', '%' . $wpdb->esc_like($history_needle) . '%' ), ARRAY_A ); if (is_array($row)) { return $this->expire_order_if_due($this->normalize_row($row)); } // Last resort: {order_number}P{n} prefix heuristic. if (preg_match('/^(.+)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 $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, ]; } }