gateways[$id] = $gateway; } /** * 获取网关 */ public function get_gateway(string $id): ?GatewayInterface { return $this->gateways[$id] ?? null; } /** * 获取所有已注册网关 * * @return GatewayInterface[] */ public function get_all(): array { return $this->gateways; } /** * 绑定支付回调路由。必须在 functions.php 中初始化。 * * 此前网关 create_payment() 把 notify_url/return_url 指向 * /wp-json/aksrt/v1/payment/{gateway}/{notify|return|cancel},但这些路由 * 从未注册,导致所有跳转型网关(支付宝/微信/PayPal/Stripe)无法完成支付。 */ public function init(): void { add_action('rest_api_init', [$this, 'register_routes']); } public function register_routes(): void { $args = [ 'methods' => ['GET', 'POST'], 'permission_callback' => '__return_true', // 真实性由各网关签名校验保证 'args' => [ 'gateway' => ['sanitize_callback' => 'sanitize_key'], ], ]; register_rest_route('aksrt/v1', '/payment/(?P[a-z0-9_]+)/notify', array_merge($args, [ 'callback' => [$this, 'handle_notify'], ])); register_rest_route('aksrt/v1', '/payment/(?P[a-z0-9_]+)/return', array_merge($args, [ 'callback' => [$this, 'handle_return'], ])); register_rest_route('aksrt/v1', '/payment/(?P[a-z0-9_]+)/cancel', array_merge($args, [ 'callback' => [$this, 'handle_cancel'], ])); } /** * 异步通知(服务器到服务器)。验签 → 到账 → 输出网关期望的 ACK。 */ public function handle_notify(\WP_REST_Request $request) { $gateway_id = sanitize_key((string) $request->get_param('gateway')); $gateway = $this->get_gateway($gateway_id); if (!$gateway) { return new \WP_REST_Response('gateway not found', 404); } $verify = $gateway->verify_callback($request); $verified = !empty($verify['success']); $paid = $verified && ($verify['status'] ?? '') === 'paid'; $processed = $verified; if ($paid) { // Only ACK paid notifications after the order was actually synchronized. $processed = $this->complete_order($gateway_id, $verify); } // 网关各自要求的 ACK 格式 if ($gateway_id === 'wechatpay' && method_exists($gateway, 'output_callback_response')) { $gateway->output_callback_response(['success' => $processed]); // exit } if ($gateway_id === 'alipay' || str_starts_with($gateway_id, 'epay')) { // 支付宝 / 易支付异步通知要求返回纯文本 success,否则会重复通知 if (method_exists($gateway, 'output_callback_response')) { $gateway->output_callback_response(['success' => $processed]); // exit } header('Content-Type: text/plain; charset=utf-8'); echo $processed ? 'success' : 'fail'; exit; } // Stripe / PayPal webhook:200 表示已接收;处理失败返回 400 触发重试 return new \WP_REST_Response(['received' => $processed], $processed ? 200 : 400); } /** * 用户浏览器同步返回。尽力确认到账(兜底异步通知延迟),再跳回订单中心。 */ public function handle_return(\WP_REST_Request $request) { $gateway_id = sanitize_key((string) $request->get_param('gateway')); // Providers echo back whatever number we submitted, which for a re-pay is an // attempt-suffixed out_trade_no. Map it to the local order for query + redirect. $provider_ref = sanitize_text_field((string) ($request->get_param('order') ?: $request->get_param('out_trade_no') ?: '')); $order_number = $this->local_order_number($provider_ref); $gateway = $this->get_gateway($gateway_id); $status = 'pending'; if ($gateway) { if ($gateway_id === 'alipay' || str_starts_with($gateway_id, 'epay')) { // 支付宝 / 易支付同步返回带签名参数,可直接验签作为到账兜底 $verify = $gateway->verify_callback($request); if (!empty($verify['success'])) { if ($order_number === '') { $order_number = $this->local_order_number( sanitize_text_field((string) ($verify['order_number'] ?? '')) ); } if (($verify['status'] ?? '') === 'paid' && $this->complete_order($gateway_id, $verify)) { $status = 'success'; } elseif ($order_number !== '') { // Synchronous returns generally omit trade_status; query after signature verification. $query = $gateway->query_status($provider_ref !== '' ? $provider_ref : $order_number); if (!empty($query['success']) && ($query['status'] ?? '') === 'paid') { if (empty($query['order_number'])) { $query['order_number'] = $order_number; } if ($this->complete_order($gateway_id, $query)) { $status = 'success'; } } } } } elseif ($gateway_id === 'paypal' && method_exists($gateway, 'capture_order')) { // PayPal v2:用户批准后在 return 时捕获(token = PayPal 订单号) $paypal_order_id = sanitize_text_field((string) ($request->get_param('token') ?: '')); if ($paypal_order_id !== '') { $cap = $gateway->capture_order($paypal_order_id); if (!empty($cap['success']) && ($cap['status'] ?? '') === 'paid') { $captured_ref = sanitize_text_field((string) ($cap['order_number'] ?? '')); $captured_order = $this->local_order_number($captured_ref); if ($captured_order === '' || ($order_number !== '' && !hash_equals($order_number, $captured_order))) { $this->redirect_to_orders($order_number, 'pending'); return null; } $order_number = $captured_order; if ($this->complete_order($gateway_id, $cap)) { $status = 'success'; } } } } elseif ($order_number !== '') { // 其余网关:用返回时的 provider ref 查询该次尝试;local number 仅用于跳转。 $q = $gateway->query_status($provider_ref !== '' ? $provider_ref : $order_number); if (!empty($q['success']) && ($q['status'] ?? '') === 'paid') { if (empty($q['order_number'])) { $q['order_number'] = $order_number; } if ($this->complete_order($gateway_id, $q)) { $status = 'success'; } } } } $this->redirect_to_orders($order_number, $status); return null; } /** * 用户取消支付,跳回订单中心。 */ public function handle_cancel(\WP_REST_Request $request) { $provider_ref = sanitize_text_field((string) ($request->get_param('order') ?: $request->get_param('out_trade_no') ?: '')); $this->redirect_to_orders($this->local_order_number($provider_ref), Order::STATUS_CLOSED); return null; } /** * Map a provider out_trade_no back to the local order number (identity when unknown). */ private function local_order_number(string $provider_ref): string { if ($provider_ref === '' || !class_exists('AKSRT\\Payment\\OrderRepository')) { return $provider_ref; } $order = OrderRepository::get_instance()->resolve_by_out_trade_no($provider_ref); if (!$order) { return $provider_ref; } return sanitize_text_field((string) ($order['order_number'] ?? $provider_ref)); } /** * Whether a pending order can be reconciled against an external provider. * * This is the single gate used by the order view and the actual reconcile * operation, so ordinary detail reads never trigger provider I/O. * * @param array|null $order */ public function needs_reconciliation(?array $order): bool { return null !== $this->reconciliation_gateway($order); } /** * Reconcile a pending external order against the configured provider. * * @param array|null $order */ public function reconcile_order(string $order_number, ?array $order = null): ?array { if ($order_number === '' || !class_exists('AKSRT\\Payment\\OrderRepository')) { return $order; } $repo = OrderRepository::get_instance(); $order = is_array($order) ? $order : $repo->get($order_number); $gateway = $this->reconciliation_gateway($order); if (!$gateway) { return $order; } $gateway_id = sanitize_key((string) ($order['payment_method'] ?? '')); $throttle_key = 'aksrt_reconcile_' . hash('sha256', $order_number); if (get_transient($throttle_key)) { return $order; } set_transient($throttle_key, 1, 5); $lock = PaymentAttemptLock::acquire('reconcile_' . $order_number); if ($lock === false) { return $repo->get($order_number) ?: $order; } try { $provider_ref = OrderRepository::provider_out_trade_no($order); $query = $gateway->query_status($provider_ref); if (!empty($query['success']) && ($query['status'] ?? '') === 'paid') { if (empty($query['order_number'])) { $query['order_number'] = $provider_ref; } $this->complete_order($gateway_id, $query); } } catch (\Throwable $e) { if (defined('WP_DEBUG') && WP_DEBUG) { error_log(sprintf('[AKSRT Payment] reconcile failed order=%s gateway=%s: %s', $order_number, $gateway_id, $e->getMessage())); } } finally { PaymentAttemptLock::release('reconcile_' . $order_number, $lock); } return $repo->get($order_number) ?: $order; } /** * @param array|null $order */ private function reconciliation_gateway(?array $order): ?GatewayInterface { if (!is_array($order) || (string) ($order['status'] ?? '') !== Order::STATUS_PENDING) { return null; } $gateway_id = sanitize_key((string) ($order['payment_method'] ?? '')); if ($gateway_id === '' || in_array($gateway_id, ['balance', 'points'], true)) { return null; } $gateway = $this->get_gateway($gateway_id); return $gateway && $gateway->is_configured() ? $gateway : null; } /** * 幂等、带金额校验的订单完成。仅由验签通过的回调调用。 * * @param string $gateway_id 网关 ID * @param array $verify 网关 verify_callback / query 结果 * @return bool */ public function complete_order(string $gateway_id, array $verify): bool { $provider_out_trade_no = sanitize_text_field((string) ($verify['order_number'] ?? '')); if ($provider_out_trade_no === '' || !class_exists('AKSRT\\Payment\\OrderRepository')) { return false; } $repo = OrderRepository::get_instance(); // Callbacks may carry attempt-suffixed out_trade_no (…P2); map back to local order. $order = $repo->resolve_by_out_trade_no($provider_out_trade_no); if (!$order) { return false; } $order_number = sanitize_text_field((string) ($order['order_number'] ?? '')); if ($order_number === '') { return false; } $stored_gateway = sanitize_key((string) ($order['payment_method'] ?? '')); if ($stored_gateway === '' || !hash_equals($stored_gateway, sanitize_key($gateway_id))) { return false; } // Provider-confirmed external payments must include an exact amount and transaction ID. $expected_currency = strtoupper((string) ($order['currency'] ?? '')); $paid_currency = strtoupper(sanitize_key((string) ($verify['currency'] ?? ''))); try { $expected = \AKSRT\Ledger\AssetAmount::balance(is_string($order['amount'] ?? null) ? $order['amount'] : ''); $paid_amount = \AKSRT\Ledger\AssetAmount::balance(is_string($verify['amount'] ?? null) ? $verify['amount'] : ''); } catch (\InvalidArgumentException $e) { return false; } $transaction_id = sanitize_text_field((string) ($verify['transaction_id'] ?? '')); if ($expected->minor() <= 0 || $paid_amount->minor() <= 0 || $transaction_id === '' || $expected_currency === '' || $paid_currency === '' || !hash_equals($expected_currency, $paid_currency) || $expected->minor() !== $paid_amount->minor()) { if (defined('WP_DEBUG') && WP_DEBUG) { error_log(sprintf( '[AKSRT Payment] invalid completion order=%s out_trade_no=%s expected=%s %s paid=%s %s transaction=%s gateway=%s', $order_number, $provider_out_trade_no, $expected->decimal(), $expected_currency, $paid_amount->decimal(), $paid_currency, $transaction_id !== '' ? 'present' : 'missing', $gateway_id )); } return false; } $current = (string) ($order['status'] ?? ''); // 幂等:仅同一交易号的重复回调可视为成功。不同交易号意味着 // 用户可能重复付款,不能静默 ACK 为已正确同步。 if ($current === Order::STATUS_PAID) { $stored_transaction = sanitize_text_field((string) ($order['payment_transaction_id'] ?? '')); if (sanitize_key((string) ($order['product_type'] ?? '')) === \AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE) { if ($stored_transaction !== '' && !hash_equals($stored_transaction, $transaction_id)) { return false; } return $repo->update_status($order_number, Order::STATUS_PAID, [ 'payment_method' => $gateway_id, 'payment_transaction_id' => $transaction_id, 'note' => __('支付网关回调补充确认到账。', 'aksrt'), ]); } return $stored_transaction !== '' && hash_equals($stored_transaction, $transaction_id); } // 只允许从未支付态完成 if ($current !== Order::STATUS_PENDING) { return false; } return $repo->update_status($order_number, Order::STATUS_PAID, [ 'payment_method' => $gateway_id, 'payment_transaction_id' => $transaction_id, 'note' => __('支付网关回调确认到账。', 'aksrt'), ]); } private function redirect_to_orders(string $order_number, string $status): void { $url = add_query_arg(array_filter([ 'payment' => sanitize_key($status), 'order' => $order_number, ]), $this->orders_page_url()); nocache_headers(); wp_safe_redirect($url); exit; } private function orders_page_url(): string { return function_exists('aksrt_get_user_center_tab_url') ? aksrt_get_user_center_tab_url('orders') : home_url('/user/orders/'); } }