feat(payment): tighten gateways and implement Epay
Keep only balance, Alipay, WeChat, PayPal, Stripe, Epay, and card-pass channels; remove placeholder aggregators. Add dual-channel Epay gateway with MD5 notify/return, register cashier methods, and gate card-pass redeem behind the payment channel switch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
aa4bdfb5c4
commit
723dd180e4
11 files changed
+470
-264
No files matched your search
@@ -111,6 +111,30 @@ class ChargeCard {
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function get_public_options(): array {
|
||||
$labels = [
|
||||
'title' => __('卡密兑换', 'aksrt'),
|
||||
'password' => __('卡密', 'aksrt'),
|
||||
'card' => __('卡号', 'aksrt'),
|
||||
'submit_balance' => __('兑换余额', 'aksrt'),
|
||||
'submit_vip' => __('兑换会员', 'aksrt'),
|
||||
'submit_credits' => __('兑换积分', 'aksrt'),
|
||||
'placeholder_pw' => __('请输入卡密', 'aksrt'),
|
||||
'placeholder_card' => __('请输入卡号', 'aksrt'),
|
||||
];
|
||||
|
||||
// Master kill-switch on 收款接口 → 卡密充值.
|
||||
$channel_on = !class_exists('\AKSRT\Settings\PaymentRuntime')
|
||||
|| \AKSRT\Settings\PaymentRuntime::is_gateway_enabled('card_pass');
|
||||
if (!$channel_on) {
|
||||
$off = ['enabled' => false, 'desc' => '', 'only_password' => true];
|
||||
return [
|
||||
'balance' => $off,
|
||||
'vip' => $off,
|
||||
'credits' => $off,
|
||||
'labels' => $labels,
|
||||
];
|
||||
}
|
||||
|
||||
$balance = ['enabled' => false, 'desc' => '', 'only_password' => true];
|
||||
$vip = ['enabled' => false, 'desc' => '', 'only_password' => true];
|
||||
|
||||
@@ -154,16 +178,7 @@ class ChargeCard {
|
||||
'balance' => $balance,
|
||||
'vip' => $vip,
|
||||
'credits' => $credits,
|
||||
'labels' => [
|
||||
'title' => __('卡密兑换', 'aksrt'),
|
||||
'password' => __('卡密', 'aksrt'),
|
||||
'card' => __('卡号', 'aksrt'),
|
||||
'submit_balance' => __('兑换余额', 'aksrt'),
|
||||
'submit_vip' => __('兑换会员', 'aksrt'),
|
||||
'submit_credits' => __('兑换积分', 'aksrt'),
|
||||
'placeholder_pw' => __('请输入卡密', 'aksrt'),
|
||||
'placeholder_card' => __('请输入卡号', 'aksrt'),
|
||||
],
|
||||
'labels' => $labels,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -128,8 +128,11 @@ class Gateway {
|
||||
if ($gateway_id === 'wechatpay' && method_exists($gateway, 'output_callback_response')) {
|
||||
$gateway->output_callback_response(['success' => $processed]); // exit
|
||||
}
|
||||
if ($gateway_id === 'alipay') {
|
||||
// 支付宝异步通知要求返回纯文本 success,否则会重复通知
|
||||
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;
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
<?php
|
||||
/**
|
||||
* Compatible Epay (彩虹易支付) gateway — alipay / wxpay channels.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Payment\Gateways;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class EpayGateway implements \AKSRT\Payment\GatewayInterface {
|
||||
|
||||
/** @var string alipay|wxpay */
|
||||
private string $channel;
|
||||
|
||||
public function __construct(string $channel = 'alipay') {
|
||||
$channel = sanitize_key($channel);
|
||||
$this->channel = in_array($channel, ['alipay', 'wxpay'], true) ? $channel : 'alipay';
|
||||
}
|
||||
|
||||
public function get_id(): string {
|
||||
return $this->channel === 'wxpay' ? 'epay_wxpay' : 'epay_alipay';
|
||||
}
|
||||
|
||||
public function get_name(): string {
|
||||
return $this->channel === 'wxpay'
|
||||
? __('易支付·微信', 'aksrt')
|
||||
: __('易支付·支付宝', 'aksrt');
|
||||
}
|
||||
|
||||
public function is_configured(): bool {
|
||||
if (class_exists('\AKSRT\Settings\PaymentRuntime')
|
||||
&& !\AKSRT\Settings\PaymentRuntime::is_gateway_enabled($this->get_id())) {
|
||||
return false;
|
||||
}
|
||||
$c = $this->get_config();
|
||||
return $c['apiurl'] !== '' && $c['pid'] !== '' && $c['key'] !== '';
|
||||
}
|
||||
|
||||
public function create_payment(array $order): array {
|
||||
if (!$this->is_configured()) {
|
||||
return ['success' => false, 'message' => __('易支付未配置', 'aksrt')];
|
||||
}
|
||||
|
||||
$order_number = (string) ($order['order_number'] ?? '');
|
||||
$amount = (float) ($order['amount'] ?? 0);
|
||||
$subject = (string) ($order['subject'] ?? __('订单支付', 'aksrt'));
|
||||
if ($order_number === '' || $amount <= 0) {
|
||||
return ['success' => false, 'message' => __('订单参数无效', 'aksrt')];
|
||||
}
|
||||
|
||||
$config = $this->get_config();
|
||||
$params = [
|
||||
'pid' => $config['pid'],
|
||||
'type' => $this->channel,
|
||||
'out_trade_no' => $order_number,
|
||||
'notify_url' => rest_url('aksrt/v1/payment/' . $this->get_id() . '/notify'),
|
||||
'return_url' => rest_url('aksrt/v1/payment/' . $this->get_id() . '/return'),
|
||||
'name' => mb_substr(wp_strip_all_tags($subject), 0, 127),
|
||||
'money' => number_format($amount, 2, '.', ''),
|
||||
];
|
||||
$params['sign'] = $this->sign($params, $config['key']);
|
||||
$params['sign_type'] = 'MD5';
|
||||
|
||||
$use_mobile = $this->prefer_mobile($config['device']);
|
||||
if ($use_mobile) {
|
||||
// mapi.php returns JSON with payurl / qrcode / urlscheme.
|
||||
$api = $this->endpoint($config['apiurl'], 'mapi.php');
|
||||
$response = wp_remote_post($api, [
|
||||
'timeout' => 30,
|
||||
'body' => $params,
|
||||
'headers' => ['Content-Type' => 'application/x-www-form-urlencoded; charset=UTF-8'],
|
||||
]);
|
||||
if (is_wp_error($response)) {
|
||||
return ['success' => false, 'message' => $response->get_error_message()];
|
||||
}
|
||||
$body = json_decode((string) wp_remote_retrieve_body($response), true);
|
||||
if (!is_array($body)) {
|
||||
// Fall back to browser submit if mapi is unavailable.
|
||||
return $this->browser_submit_result($config['apiurl'], $params, $order_number);
|
||||
}
|
||||
$code = (int) ($body['code'] ?? 0);
|
||||
if ($code !== 1) {
|
||||
$msg = (string) ($body['msg'] ?? $body['message'] ?? __('易支付下单失败', 'aksrt'));
|
||||
return ['success' => false, 'message' => $msg];
|
||||
}
|
||||
$pay_url = (string) ($body['payurl'] ?? $body['urlscheme'] ?? '');
|
||||
$qrcode = (string) ($body['qrcode'] ?? '');
|
||||
if ($pay_url === '' && $qrcode === '') {
|
||||
return ['success' => false, 'message' => __('易支付未返回支付链接', 'aksrt')];
|
||||
}
|
||||
$result = [
|
||||
'success' => true,
|
||||
'payment_data' => [
|
||||
'order_number' => $order_number,
|
||||
'code_url' => $qrcode,
|
||||
'channel' => $this->channel,
|
||||
],
|
||||
'message' => __('易支付订单已创建', 'aksrt'),
|
||||
];
|
||||
if ($pay_url !== '') {
|
||||
$result['payment_url'] = $pay_url;
|
||||
} elseif ($qrcode !== '') {
|
||||
$result['payment_url'] = $qrcode;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
return $this->browser_submit_result($config['apiurl'], $params, $order_number);
|
||||
}
|
||||
|
||||
public function verify_callback(\WP_REST_Request $request): array {
|
||||
if (!$this->is_configured()) {
|
||||
return ['success' => false, 'message' => __('易支付未配置', 'aksrt')];
|
||||
}
|
||||
|
||||
$params = strtoupper($request->get_method()) === 'POST'
|
||||
? $request->get_body_params()
|
||||
: $request->get_query_params();
|
||||
if (!is_array($params) || $params === []) {
|
||||
// Some panels POST raw query string.
|
||||
$params = $request->get_params();
|
||||
}
|
||||
if (!is_array($params)) {
|
||||
$params = [];
|
||||
}
|
||||
unset($params['gateway'], $params['rest_route']);
|
||||
|
||||
$config = $this->get_config();
|
||||
$sign = (string) ($params['sign'] ?? '');
|
||||
if ($sign === '' || !$this->verify_sign($params, $config['key'], $sign)) {
|
||||
return ['success' => false, 'message' => __('易支付签名校验失败', 'aksrt')];
|
||||
}
|
||||
|
||||
$pid = (string) ($params['pid'] ?? '');
|
||||
if ($pid !== '' && !hash_equals($config['pid'], $pid)) {
|
||||
return ['success' => false, 'message' => __('易支付商户号不匹配', 'aksrt')];
|
||||
}
|
||||
|
||||
$order_number = sanitize_text_field((string) ($params['out_trade_no'] ?? ''));
|
||||
$transaction_id = sanitize_text_field((string) ($params['trade_no'] ?? ''));
|
||||
$amount = (float) ($params['money'] ?? 0);
|
||||
$trade_status = (string) ($params['trade_status'] ?? '');
|
||||
$type = sanitize_key((string) ($params['type'] ?? ''));
|
||||
|
||||
// When type is present, require channel match.
|
||||
if ($type !== '' && $type !== $this->channel) {
|
||||
return ['success' => false, 'message' => __('易支付通道类型不匹配', 'aksrt')];
|
||||
}
|
||||
|
||||
$paid = ($trade_status === '' || $trade_status === 'TRADE_SUCCESS');
|
||||
if ($order_number === '' || ($paid && ($transaction_id === '' || $amount <= 0))) {
|
||||
return ['success' => false, 'message' => __('易支付回调数据无效', 'aksrt')];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'order_number' => $order_number,
|
||||
'amount' => $amount,
|
||||
'transaction_id' => $transaction_id,
|
||||
'status' => $paid ? 'paid' : 'pending',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Epay expects plain-text success/fail (handled also by Gateway for epay_* ids).
|
||||
*/
|
||||
public function output_callback_response(array $result): void {
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
echo !empty($result['success']) ? 'success' : 'fail';
|
||||
exit;
|
||||
}
|
||||
|
||||
public function query_status(string $order_number): array {
|
||||
if (!$this->is_configured() || $order_number === '') {
|
||||
return ['success' => false, 'message' => __('易支付未配置', 'aksrt')];
|
||||
}
|
||||
|
||||
$config = $this->get_config();
|
||||
// Common compatible query: api.php?act=order&pid&key&out_trade_no
|
||||
$url = add_query_arg([
|
||||
'act' => 'order',
|
||||
'pid' => $config['pid'],
|
||||
'key' => $config['key'],
|
||||
'out_trade_no' => $order_number,
|
||||
], $this->endpoint($config['apiurl'], 'api.php'));
|
||||
|
||||
$response = wp_remote_get($url, ['timeout' => 20]);
|
||||
if (is_wp_error($response)) {
|
||||
return ['success' => false, 'message' => $response->get_error_message()];
|
||||
}
|
||||
$body = json_decode((string) wp_remote_retrieve_body($response), true);
|
||||
if (!is_array($body)) {
|
||||
return ['success' => false, 'message' => __('易支付查询响应无效', 'aksrt')];
|
||||
}
|
||||
|
||||
// code=1 success in most forks; status 1 = paid.
|
||||
$code = (int) ($body['code'] ?? 0);
|
||||
$status = (int) ($body['status'] ?? $body['trade_status'] ?? 0);
|
||||
$paid = $code === 1 && ((string) $status === '1' || (string) ($body['trade_status'] ?? '') === 'TRADE_SUCCESS');
|
||||
|
||||
$amount = (float) ($body['money'] ?? $body['amount'] ?? 0);
|
||||
$tx = (string) ($body['trade_no'] ?? $body['api_trade_no'] ?? '');
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'order_number' => (string) ($body['out_trade_no'] ?? $order_number),
|
||||
'amount' => $amount,
|
||||
'transaction_id' => $tx,
|
||||
'status' => $paid ? 'paid' : 'pending',
|
||||
];
|
||||
}
|
||||
|
||||
public function refund(string $order_number, float $amount, string $request_id = ''): array {
|
||||
return [
|
||||
'success' => false,
|
||||
'status' => 'failed',
|
||||
'message' => __('易支付退款请在商户后台人工处理', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{apiurl:string,pid:string,key:string,device:string}
|
||||
*/
|
||||
private function get_config(): array {
|
||||
$all = class_exists('\AKSRT\Settings\Settings')
|
||||
? \AKSRT\Settings\Settings::get_all()
|
||||
: [];
|
||||
$device = sanitize_key((string) ($all['epay_device'] ?? 'auto'));
|
||||
if (!in_array($device, ['auto', 'pc', 'mobile'], true)) {
|
||||
$device = 'auto';
|
||||
}
|
||||
$apiurl = (string) ($all['epay_apiurl'] ?? '');
|
||||
$apiurl = rtrim(trim($apiurl), '/') . '/';
|
||||
if ($apiurl === '/') {
|
||||
$apiurl = '';
|
||||
}
|
||||
return [
|
||||
'apiurl' => $apiurl,
|
||||
'pid' => (string) ($all['epay_pid'] ?? ''),
|
||||
'key' => (string) ($all['epay_key'] ?? ''),
|
||||
'device' => $device,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $params
|
||||
*/
|
||||
private function sign(array $params, string $key): string {
|
||||
$filtered = [];
|
||||
foreach ($params as $k => $v) {
|
||||
if ($k === 'sign' || $k === 'sign_type') {
|
||||
continue;
|
||||
}
|
||||
if ($v === '' || $v === null) {
|
||||
continue;
|
||||
}
|
||||
$filtered[(string) $k] = is_scalar($v) ? (string) $v : '';
|
||||
}
|
||||
ksort($filtered);
|
||||
$parts = [];
|
||||
foreach ($filtered as $k => $v) {
|
||||
$parts[] = $k . '=' . $v;
|
||||
}
|
||||
return md5(implode('&', $parts) . $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $params
|
||||
*/
|
||||
private function verify_sign(array $params, string $key, string $sign): bool {
|
||||
$expected = $this->sign($params, $key);
|
||||
return hash_equals(strtolower($expected), strtolower($sign));
|
||||
}
|
||||
|
||||
private function endpoint(string $apiurl, string $file): string {
|
||||
return rtrim($apiurl, '/') . '/' . ltrim($file, '/');
|
||||
}
|
||||
|
||||
private function prefer_mobile(string $device): bool {
|
||||
if ($device === 'mobile') {
|
||||
return true;
|
||||
}
|
||||
if ($device === 'pc') {
|
||||
return false;
|
||||
}
|
||||
// auto
|
||||
if (!empty($_SERVER['HTTP_USER_AGENT'])) {
|
||||
$ua = (string) $_SERVER['HTTP_USER_AGENT'];
|
||||
return (bool) preg_match('/Mobile|Android|iPhone|iPad|iPod|MicroMessenger/i', $ua);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,string> $params
|
||||
* @return array{success:bool,payment_url?:string,payment_data?:array,message?:string}
|
||||
*/
|
||||
private function browser_submit_result(string $apiurl, array $params, string $order_number): array {
|
||||
$url = add_query_arg($params, $this->endpoint($apiurl, 'submit.php'));
|
||||
return [
|
||||
'success' => true,
|
||||
'payment_url' => $url,
|
||||
'payment_data' => [
|
||||
'order_number' => $order_number,
|
||||
'channel' => $this->channel,
|
||||
],
|
||||
'message' => __('易支付支付链接已生成', 'aksrt'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -504,13 +504,15 @@ class UserOrders {
|
||||
private function payment_method_label(string $method): string {
|
||||
$method = sanitize_key($method);
|
||||
$map = [
|
||||
'balance' => __('站内余额', 'aksrt'),
|
||||
'points' => __('积分支付', 'aksrt'),
|
||||
'alipay' => __('支付宝', 'aksrt'),
|
||||
'wechatpay' => __('微信支付', 'aksrt'),
|
||||
'wechat' => __('微信支付', 'aksrt'),
|
||||
'paypal' => 'PayPal',
|
||||
'stripe' => 'Stripe',
|
||||
'balance' => __('站内余额', 'aksrt'),
|
||||
'points' => __('积分支付', 'aksrt'),
|
||||
'alipay' => __('支付宝', 'aksrt'),
|
||||
'wechatpay' => __('微信支付', 'aksrt'),
|
||||
'wechat' => __('微信支付', 'aksrt'),
|
||||
'paypal' => 'PayPal',
|
||||
'stripe' => 'Stripe',
|
||||
'epay_alipay' => __('易支付·支付宝', 'aksrt'),
|
||||
'epay_wxpay' => __('易支付·微信', 'aksrt'),
|
||||
];
|
||||
return $map[$method] ?? ($method !== '' ? $method : __('—', 'aksrt'));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user