Files
wordpress-theme-aksrt/inc/classes/class-aksrt-payment-gateway.php
T
kate522 c93b303453 feat(payment): finish Zibll key contract and wire checkout gateways
Drop dual-read payment runtime fallbacks, align PHP schema to Vue keys,
read gateway credentials only from aksrt_settings with enable toggles,
expose configured adapters at checkout, fix credits pay signatures, and
apply order expiry settings. Wire orphan order/card-pass/withdrawal
schema pages; delete unused Payment* wrappers.
2026-07-11 17:42:35 +08:00

448 lines
15 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* AKSRT Payment Gateway — 支付网关适配器层
*
* 定义支付网关统一接口和基础实现。
* 具体网关(支付宝、微信支付等)通过子类实现。
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
/**
* 支付网关统一接口
*/
interface AKSRT_Payment_Gateway_Interface {
/**
* 创建支付单
*
* @param array $order 订单数据
* @return array{success: bool, payment_url?: string, payment_data?: array, message?: string}
*/
public function create_payment(array $order): array;
/**
* 验证回调
*
* @param WP_REST_Request $request 回调请求
* @return array{success: bool, order_number?: string, amount?: float, transaction_id?: string, message?: string}
*/
public function verify_callback(WP_REST_Request $request): array;
/**
* 查询支付状态
*
* @param string $order_number 订单号
* @return array{success: bool, status?: string, transaction_id?: string, message?: string}
*/
public function query_status(string $order_number): array;
/**
* 退款
*
* @param string $order_number 订单号
* @param float $amount 退款金额
* @return array{success: bool, refund_id?: string, message?: string}
*/
public function refund(string $order_number, float $amount): array;
/**
* 获取网关显示名称
*/
public function get_name(): string;
/**
* 获取网关标识
*/
public function get_id(): string;
/**
* 网关是否已配置
*/
public function is_configured(): bool;
}
/**
* 支付网关管理器
*/
class AKSRT_Payment_Gateway {
private static $instance = null;
private $gateways = [];
public static function get_instance(): self {
if (null === self::$instance) self::$instance = new self();
return self::$instance;
}
/**
* 注册网关
*/
public function register(string $id, AKSRT_Payment_Gateway_Interface $gateway): void {
$this->gateways[$id] = $gateway;
}
/**
* 获取网关
*/
public function get_gateway(string $id): ?AKSRT_Payment_Gateway_Interface {
return $this->gateways[$id] ?? null;
}
/**
* 获取所有已注册网关
*
* @return AKSRT_Payment_Gateway_Interface[]
*/
public function get_all(): array {
return $this->gateways;
}
/**
* 获取可用网关列表(已配置的)
*/
public function get_available(): array {
$available = [];
foreach ($this->gateways as $id => $gateway) {
if ($gateway->is_configured()) {
$available[$id] = $gateway;
}
}
return $available;
}
/**
* 获取网关状态摘要(用于后台展示)
*/
public function get_status_summary(): array {
$summary = [];
foreach ($this->gateways as $id => $gateway) {
$summary[] = [
'id' => $id,
'name' => $gateway->get_name(),
'configured' => $gateway->is_configured(),
'available' => $gateway->is_configured(),
];
}
return $summary;
}
/**
* 绑定支付回调路由。必须在 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<gateway>[a-z0-9_]+)/notify', array_merge($args, [
'callback' => [$this, 'handle_notify'],
]));
register_rest_route('aksrt/v1', '/payment/(?P<gateway>[a-z0-9_]+)/return', array_merge($args, [
'callback' => [$this, 'handle_return'],
]));
register_rest_route('aksrt/v1', '/payment/(?P<gateway>[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';
if ($paid) {
$this->complete_order($gateway_id, $verify);
}
// 网关各自要求的 ACK 格式
if ($gateway_id === 'wechatpay' && method_exists($gateway, 'output_callback_response')) {
$gateway->output_callback_response(['success' => $verified]); // exit
}
if ($gateway_id === 'alipay') {
// 支付宝异步通知要求返回纯文本 success,否则会重复通知
header('Content-Type: text/plain; charset=utf-8');
echo $verified ? 'success' : 'fail';
exit;
}
// Stripe / PayPal webhook:200 表示已接收;验签失败返回 400 触发重试
return new WP_REST_Response(['received' => $verified], $verified ? 200 : 400);
}
/**
* 用户浏览器同步返回。尽力确认到账(兜底异步通知延迟),再跳回订单中心。
*/
public function handle_return(WP_REST_Request $request) {
$gateway_id = sanitize_key((string) $request->get_param('gateway'));
$order_number = sanitize_text_field((string) ($request->get_param('order')
?: $request->get_param('out_trade_no')
?: ''));
$gateway = $this->get_gateway($gateway_id);
$status = 'pending';
if ($gateway) {
if ($gateway_id === 'alipay') {
// 支付宝同步返回带签名参数,可直接验签作为到账兜底
$verify = $gateway->verify_callback($request);
if (!empty($verify['success'])) {
$order_number = $order_number ?: sanitize_text_field((string) ($verify['order_number'] ?? ''));
if (($verify['status'] ?? '') === 'paid' && $this->complete_order($gateway_id, $verify)) {
$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') {
$order_number = $order_number ?: sanitize_text_field((string) ($cap['order_number'] ?? ''));
$cap['order_number'] = $order_number;
if ($this->complete_order($gateway_id, $cap)) {
$status = 'success';
}
}
}
} elseif ($order_number !== '') {
// 其余网关:主动查询订单状态兜底
$q = $gateway->query_status($order_number);
if (!empty($q['success']) && ($q['status'] ?? '') === 'paid') {
$done = $this->complete_order($gateway_id, [
'order_number' => $order_number,
'amount' => 0, // 由网关 API 已确认到账,金额校验交给 notify 路径
'transaction_id' => $q['transaction_id'] ?? '',
'status' => 'paid',
]);
if ($done) {
$status = 'success';
}
}
}
}
$this->redirect_to_orders($order_number, $status);
return null;
}
/**
* 用户取消支付,跳回订单中心。
*/
public function handle_cancel(WP_REST_Request $request) {
$order_number = sanitize_text_field((string) ($request->get_param('order')
?: $request->get_param('out_trade_no')
?: ''));
$this->redirect_to_orders($order_number, 'cancelled');
return null;
}
/**
* 幂等、带金额校验的订单完成。仅由验签通过的回调调用。
*
* @param string $gateway_id 网关 ID
* @param array $verify 网关 verify_callback / query 结果
* @return bool
*/
public function complete_order(string $gateway_id, array $verify): bool {
$order_number = sanitize_text_field((string) ($verify['order_number'] ?? ''));
if ($order_number === '' || !class_exists('AKSRT_Order_Repository')) {
return false;
}
$repo = AKSRT_Order_Repository::get_instance();
$order = $repo->get($order_number);
if (!$order) {
return false;
}
$current = (string) ($order['status'] ?? '');
// 幂等:已支付/已交付/已完成视为成功,不重复处理(回调可能多次到达)
if (in_array($current, ['paid', 'delivered', 'completed'], true)) {
return true;
}
// 只允许从未支付态完成
if (!in_array($current, ['created', 'pending'], true)) {
return false;
}
// 金额校验,防止前端/回调金额被篡改
$expected = (float) ($order['amount'] ?? 0);
$paid_amount = (float) ($verify['amount'] ?? 0);
if ($expected > 0 && $paid_amount > 0 && abs($expected - $paid_amount) > 0.01) {
if (defined('WP_DEBUG') && WP_DEBUG) {
error_log(sprintf('[AKSRT Payment] amount mismatch order=%s expected=%.2f paid=%.2f gateway=%s', $order_number, $expected, $paid_amount, $gateway_id));
}
return false;
}
return $repo->update_status($order_number, AKSRT_Order::STATUS_PAID, [
'payment_method' => $gateway_id,
'payment_transaction_id' => sanitize_text_field((string) ($verify['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 {
$pages = get_posts([
'post_type' => 'page',
'post_status' => 'publish',
'posts_per_page' => 1,
'meta_key' => '_wp_page_template',
'meta_value' => 'templates/pages/page-orders.php',
'fields' => 'ids',
'no_found_rows' => true,
]);
if (!empty($pages)) {
return (string) get_permalink((int) $pages[0]);
}
return home_url('/orders/');
}
}
/**
* 站内余额支付网关(内置实现)
*
* 使用积分/余额进行支付,无需第三方。
*/
class AKSRT_Balance_Gateway implements AKSRT_Payment_Gateway_Interface {
public function get_id(): string {
return 'balance';
}
public function get_name(): string {
return __('站内余额', 'aksrt');
}
public function is_configured(): bool {
if (!class_exists('AKSRT_Credits')) {
return false;
}
if (class_exists('AKSRT_Settings_Payment_Runtime')) {
return AKSRT_Settings_Payment_Runtime::is_gateway_enabled('balance');
}
return true;
}
public function create_payment(array $order): array {
if (!class_exists('AKSRT_Credits')) {
return ['success' => false, 'message' => __('积分系统未启用', 'aksrt')];
}
$user_id = (int) ($order['user_id'] ?? 0);
$amount = (float) ($order['amount'] ?? 0);
$credits_cost = (int) ceil($amount * 100); // 1 元 = 100 积分
$order_number = (string) ($order['order_number'] ?? '');
$balance = AKSRT_Credits::balance($user_id);
if ($balance < $credits_cost) {
return ['success' => false, 'message' => __('余额不足', 'aksrt')];
}
$result = AKSRT_Credits::deduct(
$user_id,
$credits_cost,
'order_payment',
['order' => $order_number],
sprintf(__('订单 %s 支付', 'aksrt'), $order_number)
);
if (is_wp_error($result)) {
return ['success' => false, 'message' => $result->get_error_message() ?: __('扣款失败', 'aksrt')];
}
return [
'success' => true,
'transaction_id' => 'BAL_' . ($order_number !== '' ? $order_number : (string) time()),
'message' => __('余额支付成功', 'aksrt'),
];
}
public function verify_callback(WP_REST_Request $request): array {
// Balance payment is synchronous — no callback needed
return ['success' => false, 'message' => __('余额支付无需回调验证', 'aksrt')];
}
public function query_status(string $order_number): array {
$order_repo = AKSRT_Order_Repository::get_instance();
$order = $order_repo->get($order_number);
if (!$order) {
return ['success' => false, 'message' => __('订单不存在', 'aksrt')];
}
return [
'success' => true,
'status' => $order['status'],
'transaction_id' => $order['payment_transaction_id'] ?? '',
];
}
public function refund(string $order_number, float $amount): array {
if (!class_exists('AKSRT_Credits')) {
return ['success' => false, 'message' => __('积分系统未启用', 'aksrt')];
}
$order_repo = AKSRT_Order_Repository::get_instance();
$order = $order_repo->get($order_number);
if (!$order) {
return ['success' => false, 'message' => __('订单不存在', 'aksrt')];
}
$user_id = (int) $order['user_id'];
$credits_refund = (int) ceil($amount * 100);
$result = AKSRT_Credits::add(
$user_id,
$credits_refund,
'order_refund',
['order' => $order_number],
sprintf(__('订单 %s 退款', 'aksrt'), $order_number)
);
if (is_wp_error($result)) {
return ['success' => false, 'message' => $result->get_error_message() ?: __('退款失败', 'aksrt')];
}
return ['success' => true, 'refund_id' => 'BAL_REF_' . $order_number, 'message' => __('退款成功', 'aksrt')];
}
}