330 lines
11 KiB
PHP
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;
|
|
}
|
|
}
|