Files
wordpress-theme-aksrt/src/Payment/Gateways/AlipayGateway.php
T

330 lines
11 KiB
PHP

<?php
/**
* Alipay gateway via yansongda/pay (cert mode).
*
* @package AKSRT
*/
namespace AKSRT\Payment\Gateways;
defined('ABSPATH') || exit;
use Yansongda\Pay\Pay;
class AlipayGateway implements \AKSRT\Payment\GatewayInterface {
public function get_id(): string {
return 'alipay';
}
public function get_name(): string {
return __('支付宝', 'aksrt');
}
public function is_configured(): bool {
if (class_exists('\AKSRT\Settings\PaymentRuntime')
&& !\AKSRT\Settings\PaymentRuntime::is_gateway_enabled('alipay')) {
return false;
}
$c = $this->get_config();
return $c['app_id'] !== ''
&& $c['private_key'] !== ''
&& $c['app_public_cert'] !== ''
&& $c['alipay_public_cert'] !== ''
&& $c['alipay_root_cert'] !== '';
}
public function create_payment(array $order): array {
if (!$this->is_configured()) {
return ['success' => false, 'message' => __('支付宝未配置', 'aksrt')];
}
$order_number = (string) ($order['order_number'] ?? '');
$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 = $this->amount_string($order['amount'] ?? null);
$subject = (string) ($order['subject'] ?? __('订单支付', 'aksrt'));
$payment_type = $this->get_config()['payment_type'];
if ($order_number === '' || $out_trade_no === '' || $amount === '') {
return ['success' => false, 'message' => __('订单参数无效', 'aksrt')];
}
try {
YansongdaPayFactory::boot_alipay();
$payload = [
'_method' => 'GET',
'out_trade_no' => $out_trade_no,
'total_amount' => $amount,
'subject' => mb_substr($subject, 0, 256),
];
$result = $payment_type === 'wap'
? Pay::alipay()->h5($payload)
: Pay::alipay()->web($payload);
$payment_url = YansongdaPayFactory::extract_payment_url($result);
if ($payment_url === '') {
return ['success' => false, 'message' => __('支付宝支付链接生成失败', 'aksrt')];
}
return [
'success' => true,
'payment_url' => $payment_url,
'payment_data' => [
'order_number' => $order_number,
'out_trade_no' => $out_trade_no,
],
'message' => __('支付宝支付链接已生成', 'aksrt'),
];
} catch (\Throwable $e) {
return ['success' => false, 'message' => $e->getMessage()];
}
}
public function verify_callback(\WP_REST_Request $request): array {
try {
YansongdaPayFactory::boot_alipay();
// Use the unsanitized form/query payload; route params must not enter the signature base.
$callback_input = strtoupper($request->get_method()) === 'POST'
? $request->get_body_params()
: $request->get_query_params();
if (!is_array($callback_input)) {
$callback_input = [];
}
unset($callback_input['gateway']);
$data_obj = Pay::alipay()->callback($callback_input);
$data = YansongdaPayFactory::to_array($data_obj);
$config = $this->get_config();
$trade_status = (string) ($data['trade_status'] ?? '');
$paid = in_array($trade_status, ['TRADE_SUCCESS', 'TRADE_FINISHED'], true);
$order_number = (string) ($data['out_trade_no'] ?? '');
$trade_no = (string) ($data['trade_no'] ?? '');
$amount = $this->amount_string($data['total_amount'] ?? null);
if (($data['app_id'] ?? '') !== $config['app_id']
|| $order_number === ''
|| ($paid && ($trade_no === '' || $amount === ''))) {
return ['success' => false, 'message' => __('支付宝回调身份或订单数据无效', 'aksrt')];
}
return [
'success' => true,
'order_number' => $order_number,
'amount' => $amount,
'currency' => 'CNY',
'transaction_id' => $trade_no,
'status' => $paid ? 'paid' : 'pending',
];
} catch (\Throwable $e) {
return ['success' => false, 'message' => $e->getMessage()];
}
}
/**
* Alipay async notify expects plain text success/fail (Gateway also handles this).
*/
public function output_callback_response(array $result): void {
try {
if (!empty($result['success'])) {
YansongdaPayFactory::boot_alipay();
YansongdaPayFactory::emit_response(Pay::alipay()->success(), 'success');
}
} catch (\Throwable $e) {
// fall through
}
header('Content-Type: text/plain; charset=utf-8');
echo 'fail';
exit;
}
public function query_status(string $order_number): array {
if (!$this->is_configured()) {
return ['success' => false, 'message' => __('支付宝未配置', 'aksrt')];
}
$out_trade_no = $this->resolve_out_trade_no($order_number);
try {
YansongdaPayFactory::boot_alipay();
$result = Pay::alipay()->query([
'out_trade_no' => $out_trade_no,
]);
$data = YansongdaPayFactory::to_array($result);
$trade_status = (string) ($data['trade_status'] ?? '');
$status = 'pending';
if (in_array($trade_status, ['TRADE_SUCCESS', 'TRADE_FINISHED'], true)) {
$status = 'paid';
} elseif ($trade_status === 'TRADE_CLOSED') {
$status = 'cancelled';
}
return [
'success' => true,
// Keep provider out_trade_no for Gateway::complete_order resolve.
'order_number' => (string) ($data['out_trade_no'] ?? $out_trade_no),
'amount' => $this->amount_string($data['total_amount'] ?? null),
'currency' => 'CNY',
'transaction_id' => (string) ($data['trade_no'] ?? ''),
'status' => $status,
];
} catch (\Throwable $e) {
return ['success' => false, 'message' => $e->getMessage()];
}
}
/**
* Best-effort close of a previous provider trade before re-pay.
*/
public function close_trade(string $out_trade_no): void {
$out_trade_no = sanitize_text_field($out_trade_no);
if ($out_trade_no === '' || !$this->is_configured()) {
return;
}
try {
YansongdaPayFactory::boot_alipay();
// alipay.trade.close — supported by yansongda as close().
Pay::alipay()->close([
'out_trade_no' => $out_trade_no,
]);
} catch (\Throwable $e) {
// ignore
}
}
public function refund(string $order_number, string $amount, string $request_id = ''): array {
try {
$refund_amount = \AKSRT\Ledger\AssetAmount::balance($amount);
} catch (\InvalidArgumentException $e) {
return ['success' => false, 'status' => 'failed', 'message' => __('退款金额无效', 'aksrt')];
}
if ($refund_amount->minor() <= 0) {
return ['success' => false, 'status' => 'failed', 'message' => __('退款金额无效', 'aksrt')];
}
if (!$this->is_configured()) {
return ['success' => false, 'message' => __('支付宝未配置', 'aksrt')];
}
$out_trade_no = $this->resolve_out_trade_no($order_number);
try {
YansongdaPayFactory::boot_alipay();
$refund_request_id = (string) preg_replace(
'/[^A-Za-z0-9_-]/',
'',
$request_id !== '' ? $request_id : $order_number . '_refund'
);
$result = Pay::alipay()->refund([
'out_trade_no' => $out_trade_no,
'refund_amount' => $refund_amount->decimal(),
'refund_reason' => __('用户申请退款', 'aksrt'),
'out_request_no' => $refund_request_id,
]);
$data = YansongdaPayFactory::to_array($result);
$fund_change = strtoupper((string) ($data['fund_change'] ?? ''));
$status = ($fund_change === 'Y' || $this->amount_string($data['refund_fee'] ?? null) !== '')
? 'succeeded'
: 'unknown';
return [
'success' => true,
'status' => $status,
'refund_id' => (string) ($data['trade_no'] ?? $data['out_trade_no'] ?? ''),
'request_id' => $refund_request_id,
'message' => $status === 'succeeded' ? __('退款成功', 'aksrt') : __('退款结果待确认', 'aksrt'),
];
} catch (\Throwable $e) {
return ['success' => false, 'message' => $e->getMessage()];
}
}
public function query_refund(string $request_id, string $order_number = ''): array {
if (!$this->is_configured() || $request_id === '') {
return ['success' => false, 'status' => 'failed', 'message' => __('支付宝退款查询参数无效', 'aksrt')];
}
try {
YansongdaPayFactory::boot_alipay();
$params = ['out_request_no' => $request_id, '_action' => 'refund'];
if ($order_number !== '') {
$params['out_trade_no'] = $this->resolve_out_trade_no($order_number);
}
$result = Pay::alipay()->query($params);
$data = YansongdaPayFactory::to_array($result);
$refund_status = strtoupper((string) ($data['refund_status'] ?? ''));
$refund_amount = $this->amount_string($data['refund_amount'] ?? null);
$status = ($refund_status === 'REFUND_SUCCESS' || $refund_amount !== '')
? 'succeeded'
: ($refund_status === 'REFUND_FAIL' ? 'failed' : 'pending');
return [
'success' => true,
'status' => $status,
'refund_id' => (string) ($data['trade_no'] ?? $data['out_request_no'] ?? ''),
];
} catch (\Throwable $e) {
return ['success' => false, 'status' => 'unknown', 'message' => $e->getMessage()];
}
}
/**
* @return array{app_id:string,private_key:string,app_public_cert:string,alipay_public_cert:string,alipay_root_cert:string,payment_type:string}
*/
private function get_config(): array {
$all = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_all()
: [];
$type = sanitize_key((string) ($all['alipay_payment_type'] ?? 'page'));
if (!in_array($type, ['page', 'wap'], true)) {
$type = 'page';
}
return [
'app_id' => (string) ($all['alipay_appid'] ?? ''),
'private_key' => (string) ($all['alipay_privatekey'] ?? ''),
'app_public_cert' => (string) ($all['alipay_app_public_cert_path'] ?? ''),
'alipay_public_cert' => (string) ($all['alipay_public_cert_path'] ?? ''),
'alipay_root_cert' => (string) ($all['alipay_root_cert_path'] ?? ''),
'payment_type' => $type,
];
}
private function amount_string(mixed $value): string {
if (!is_string($value)) {
return '';
}
try {
$amount = \AKSRT\Ledger\AssetAmount::balance(trim($value));
return $amount->minor() > 0 ? $amount->decimal() : '';
} catch (\InvalidArgumentException $e) {
return '';
}
}
/**
* Map local order_number → provider out_trade_no (supports resume attempts).
*/
private function resolve_out_trade_no(string $order_number): string {
if (class_exists('\AKSRT\Payment\OrderRepository')) {
$repo = \AKSRT\Payment\OrderRepository::get_instance();
// A provider attempt id must be queried verbatim, including superseded Pn.
if ($repo->resolve_by_out_trade_no($order_number) && !$repo->get($order_number)) {
return $order_number;
}
$order = $repo->get($order_number);
if ($order) {
return \AKSRT\Payment\OrderRepository::provider_out_trade_no($order);
}
}
return $order_number;
}
}