refactor: flatten src/AKSRT → src (drop theme-identifier dir)

Move all domain folders from src/AKSRT/{Domain} to src/{Domain}; remap
Composer PSR-4 "AKSRT\\": "src/"; update audit script + README + ADR
paths; remove dead tools/build-psr4-map.php (scanned deleted inc/classes).
This commit is contained in:
kate522 committed 2026-08-02 22:06:06 +08:00
1 parent 56f1a8175f
commit 8c5b1555c9
285 files changed
+26 -3600

No files matched your search

+275
View File
@@ -0,0 +1,275 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Payment\Gateways;
defined('ABSPATH') || exit;
class PayPalGateway implements \AKSRT\Payment\GatewayInterface {
public function get_id(): string {
return 'paypal';
}
public function get_name(): string {
return __('PayPal', 'aksrt');
}
public function is_configured(): bool {
if (class_exists('\AKSRT\Settings\PaymentRuntime')
&& !\AKSRT\Settings\PaymentRuntime::is_gateway_enabled('paypal')) {
return false;
}
$config = $this->get_config();
return !empty($config['client_id']) && !empty($config['client_secret']);
}
public function create_payment(array $order): array {
if (!$this->is_configured()) {
return ['success' => false, 'message' => __('PayPal 未配置', 'aksrt')];
}
$config = $this->get_config();
$order_number = (string) ($order['order_number'] ?? '');
// Re-pay attempts carry a fresh provider number; the callback resolves it back.
$out_trade_no = (string) ($order['out_trade_no'] ?? '');
if ($out_trade_no === '') {
$out_trade_no = class_exists('\AKSRT\Payment\OrderRepository')
? \AKSRT\Payment\OrderRepository::provider_out_trade_no($order)
: $order_number;
}
$amount = (float) ($order['amount'] ?? 0);
$subject = $order['subject'] ?? __('订单支付', 'aksrt');
$order_currency = strtoupper((string) ($order['currency'] ?? ''));
$currency = strtoupper((string) ($config['currency'] ?? 'USD'));
if ($order_currency === '' || !hash_equals($order_currency, $currency)) {
return ['success' => false, 'message' => __('PayPal 币种与订单币种不一致', 'aksrt')];
}
try {
$response = $this->api_request('POST', '/v2/checkout/orders', [
'intent' => 'CAPTURE',
'purchase_units' => [[
'reference_id' => $out_trade_no,
'description' => $subject,
'amount' => [
'currency_code' => $currency,
'value' => number_format($amount, 2, '.', ''),
],
]],
'application_context' => [
'return_url' => home_url('/wp-json/aksrt/v1/payment/paypal/return?order=' . rawurlencode($out_trade_no)),
'cancel_url' => home_url('/wp-json/aksrt/v1/payment/paypal/cancel?order=' . rawurlencode($out_trade_no)),
'brand_name' => get_bloginfo('name'),
'user_action' => 'PAY_NOW',
],
]);
if ($response['status'] === 201) {
$data = $response['body'];
$approveUrl = '';
foreach ($data['links'] ?? [] as $link) {
if ($link['rel'] === 'approve') {
$approveUrl = $link['href'];
break;
}
}
if ($order_number !== '' && class_exists('\AKSRT\Payment\OrderRepository')) {
$persisted = \AKSRT\Payment\OrderRepository::get_instance()->merge_meta($order_number, [
'paypal_order_id' => (string) ($data['id'] ?? ''),
]);
if (!$persisted) {
return ['success' => false, 'message' => __('PayPal 支付单保存失败,请重试', 'aksrt')];
}
}
return [
'success' => true,
'payment_url' => $approveUrl,
'payment_data' => [
'order_number' => $order_number,
'out_trade_no' => $out_trade_no,
'paypal_id' => $data['id'] ?? '',
],
'message' => __('PayPal 订单已创建', 'aksrt'),
];
}
return ['success' => false, 'message' => $response['body']['message'] ?? __('PayPal 创建失败', 'aksrt')];
} catch (\Exception $e) {
return ['success' => false, 'message' => $e->getMessage()];
}
}
public function verify_callback(\WP_REST_Request $request): array {
// PayPal orders are finalized only by the authenticated capture call in handle_return().
// Never treat an unsigned CHECKOUT.ORDER.APPROVED webhook as payment completion.
return [
'success' => false,
'status' => 'pending',
'message' => __('PayPal 支付需在返回流程中完成捕获确认', 'aksrt'),
];
}
public function query_status(string $order_number): array {
if (!$this->is_configured() || !class_exists('\AKSRT\Payment\OrderRepository')) {
return ['success' => false, 'status' => 'pending', 'message' => __('PayPal 未配置', 'aksrt')];
}
$order = \AKSRT\Payment\OrderRepository::get_instance()->resolve_by_out_trade_no($order_number);
$meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : [];
$paypal_order_id = sanitize_text_field((string) ($meta['paypal_order_id'] ?? ''));
if ($paypal_order_id === '') {
return ['success' => true, 'status' => 'pending', 'message' => __('PayPal 支付尚未建立', 'aksrt')];
}
try {
$response = $this->api_request('GET', '/v2/checkout/orders/' . rawurlencode($paypal_order_id));
$body = $response['body'];
if ($response['status'] !== 200 || !is_array($body)) {
return ['success' => false, 'status' => 'pending', 'message' => __('PayPal 状态查询失败', 'aksrt')];
}
if (($body['status'] ?? '') === 'APPROVED') {
return $this->capture_order($paypal_order_id);
}
if (($body['status'] ?? '') === 'COMPLETED') {
return $this->completed_order_evidence($body);
}
return ['success' => true, 'status' => 'pending', 'message' => __('PayPal 支付处理中', 'aksrt')];
} catch (\Throwable $e) {
return ['success' => false, 'status' => 'pending', 'message' => $e->getMessage()];
}
}
public function refund(string $order_number, float $amount, string $request_id = ''): array {
return [
'success' => false,
'message' => __('PayPal 退款需要通过后台手动处理', 'aksrt'),
];
}
private function get_config(): array {
if (!class_exists('\AKSRT\Settings\Settings')) {
return [];
}
$all = \AKSRT\Settings\Settings::get_all();
return [
'client_id' => (string) ($all['paypal_client_id'] ?? ''),
'client_secret' => (string) ($all['paypal_client_secret'] ?? ''),
'currency' => (string) ($all['paypal_currency'] ?? 'USD'),
'mode' => !empty($all['paypal_sandbox']) ? 'sandbox' : 'live',
];
}
/**
* 捕获已批准的 PayPal 订单(v2 CAPTURE 必需步骤)。
* 用户在 return_url 返回后调用,确认真正扣款。
*
* @param string $paypal_order_id PayPal 订单 ID(create 时返回的 id)
* @return array{success:bool, order_number?:string, amount?:float, transaction_id?:string, status?:string, message?:string}
*/
public function capture_order(string $paypal_order_id): array {
if ($paypal_order_id === '') {
return ['success' => false, 'message' => __('缺少 PayPal 订单号', 'aksrt')];
}
try {
$response = $this->api_request('POST', '/v2/checkout/orders/' . rawurlencode($paypal_order_id) . '/capture');
$body = $response['body'];
if (in_array($response['status'], [200, 201], true) && ($body['status'] ?? '') === 'COMPLETED') {
return $this->completed_order_evidence($body);
}
return ['success' => false, 'message' => $body['message'] ?? __('PayPal 捕获失败', 'aksrt')];
} catch (\Exception $e) {
return ['success' => false, 'message' => $e->getMessage()];
}
}
/** @param array<string,mixed> $body */
private function completed_order_evidence(array $body): array {
$unit = $body['purchase_units'][0] ?? [];
$capture = $unit['payments']['captures'][0] ?? [];
return [
'success' => true,
'order_number' => (string) ($unit['reference_id'] ?? ''),
'amount' => (float) ($capture['amount']['value'] ?? 0),
'currency' => strtoupper((string) ($capture['amount']['currency_code'] ?? '')),
'transaction_id' => (string) ($capture['id'] ?? ($body['id'] ?? '')),
'status' => 'paid',
];
}
/**
* 获取 OAuth2 access token(v2 REST 必需)。
*/
private function get_access_token(): string {
$config = $this->get_config();
$isSandbox = ($config['mode'] ?? 'live') === 'sandbox';
$baseUrl = $isSandbox ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com';
$response = wp_remote_post($baseUrl . '/v1/oauth2/token', [
'headers' => [
'Authorization' => 'Basic ' . base64_encode(($config['client_id'] ?? '') . ':' . ($config['client_secret'] ?? '')),
'Content-Type' => 'application/x-www-form-urlencoded',
],
'body' => 'grant_type=client_credentials',
'timeout' => 30,
]);
if (is_wp_error($response)) {
throw new \Exception($response->get_error_message());
}
$body = json_decode(wp_remote_retrieve_body($response), true);
$token = $body['access_token'] ?? '';
if ($token === '') {
throw new \Exception($body['error_description'] ?? __('PayPal 鉴权失败', 'aksrt'));
}
return (string) $token;
}
/**
* 调用 PayPal REST API(带 OAuth2 Bearer token)
*/
private function api_request(string $method, string $endpoint, array $data = []): array {
$config = $this->get_config();
$isSandbox = ($config['mode'] ?? 'live') === 'sandbox';
$baseUrl = $isSandbox
? 'https://api-m.sandbox.paypal.com'
: 'https://api-m.paypal.com';
$url = $baseUrl . $endpoint;
$headers = [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $this->get_access_token(),
];
$args = [
'method' => $method,
'headers' => $headers,
'timeout' => 30,
];
if (!empty($data)) {
$args['body'] = wp_json_encode($data);
}
$response = wp_remote_request($url, $args);
if (is_wp_error($response)) {
throw new \Exception($response->get_error_message());
}
$status = wp_remote_retrieve_response_code($response);
$body = json_decode(wp_remote_retrieve_body($response), true);
return [
'status' => $status,
'body' => $body ?: [],
];
}
}