feat(pay): 统一收银台四种模式(站内二维码/当前页跳转/表单提交/JSAPI)
- PaymentOrchestrator 统一返回 mode + code_url/payment_url/form_html/jsapi_params
- 支付宝:当面付二维码 / 电脑网站支付 / H5;微信:Native / H5 / JSAPI(WechatOpenid 静默授权)
- 易支付:PC mapi 二维码,移动端自动提交表单
- 删除旧键 alipay_payment_type / wechat_payment_type / epay_device 与 redirect/qr_content 字段
- 新增 pay-cashier.js,移除 window.open,订单状态接口改为 /pay/order/{n}
- 拆分 credits.js(affiliate / withdraw-history)至 1000 行以内
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b340b77d5a
commit
1df2822f01
28 files changed
+1255
-836
No files matched your search
@@ -486,25 +486,24 @@ class SchemaPayment {
|
||||
], 'help' => '收银台支付方式显示顺序;仅显示已开启的接口'],
|
||||
]],
|
||||
['key' => 'alipay', 'title' => '支付宝官方', 'collapsible' => true, 'defaultCollapsed' => true, 'dependsOn' => ['key' => 'pay_alipay_sdk_options', 'value' => 'official_alipay'], 'fields' => [
|
||||
['key' => 'alipay_notice', 'label' => '', 'type' => 'content', 'ui_only' => true, 'content' => '<ul><li>支付宝公钥为必填项目(公钥错误可以成功付款,但会回调失败)</li><li>回调地址固定为 /wp-json/aksrt/v1/payment/alipay/{notify|return}</li><li>同时填写了企业支付以及当面付参数,则优先使用当面付</li></ul>'],
|
||||
['key' => 'alipay_notice', 'label' => '', 'type' => 'content', 'ui_only' => true, 'content' => '<ul><li>支付宝公钥为必填项目(公钥错误可以成功付款,但会回调失败)</li><li>回调地址固定为 /wp-json/aksrt/v1/payment/alipay/{notify|return}</li><li>电脑端按「电脑端收款」选择站内二维码或跳转;手机端开启 H5 时跳转,否则显示二维码</li></ul>'],
|
||||
['key' => 'alipay_appid', 'label' => '当面付:APPID', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'alipay_privatekey', 'label' => '当面付:应用私钥', 'type' => 'textarea', 'sanitize_type' => 'code', 'default' => '', 'rows' => 4, 'secret' => true],
|
||||
['key' => 'alipay_payment_type', 'label' => '支付方式', 'type' => 'select', 'default' => 'page', 'options' => [
|
||||
['label' => '电脑网站支付(web)', 'value' => 'page'],
|
||||
['label' => '手机网站支付(wap)', 'value' => 'wap'],
|
||||
]],
|
||||
['key' => 'alipay_pc_mode', 'label' => '电脑端收款', 'type' => 'select', 'default' => 'facepay', 'options' => [
|
||||
['label' => '当面付(站内二维码)', 'value' => 'facepay'],
|
||||
['label' => '电脑网站支付(跳转支付宝)', 'value' => 'web'],
|
||||
], 'help' => '当面付在站内弹出二维码;电脑网站支付在当前页跳转到支付宝收银台'],
|
||||
['key' => 'alipay_h5', 'label' => '移动端 H5 跳转', 'type' => 'switch', 'default' => false, 'help' => '需签约手机网站支付。开启后手机浏览器跳转支付宝 App/收银台付款;关闭则手机端也显示二维码(附唤起链接)'],
|
||||
['key' => 'alipay_app_public_cert_path', 'label' => '应用公钥证书路径', 'type' => 'input', 'default' => '', 'help' => 'appCertPublicKey.crt 绝对路径。'],
|
||||
['key' => 'alipay_public_cert_path', 'label' => '支付宝公钥证书路径', 'type' => 'input', 'default' => '', 'help' => 'alipayCertPublicKey_RSA2.crt 绝对路径。'],
|
||||
['key' => 'alipay_root_cert_path', 'label' => '支付宝根证书路径', 'type' => 'input', 'default' => '', 'help' => 'alipayRootCert.crt 绝对路径。'],
|
||||
]],
|
||||
['key' => 'wechat', 'title' => '微信官方', 'collapsible' => true, 'defaultCollapsed' => true, 'dependsOn' => ['key' => 'pay_wechat_sdk_options', 'value' => 'official_wechat'], 'fields' => [
|
||||
['key' => 'wechat_notice', 'label' => '', 'type' => 'content', 'ui_only' => true, 'content' => '<ul><li>微信企业支付:官方接口,商家可申请,有年费,申请较为复杂</li><li>回调地址固定为 /wp-json/aksrt/v1/payment/wechatpay/{notify|return},请配置 Native 支付回调链接</li></ul>'],
|
||||
['key' => 'wechat_notice', 'label' => '', 'type' => 'content', 'ui_only' => true, 'content' => '<ul><li>微信企业支付:官方接口,商家可申请,有年费,申请较为复杂</li><li>回调地址固定为 /wp-json/aksrt/v1/payment/wechatpay/{notify|return},请配置 Native 支付回调链接</li><li>电脑端显示站内二维码;手机端按下方开关选择 H5 跳转或微信内 JSAPI,未开启时同样显示二维码</li></ul>'],
|
||||
['key' => 'wechat_merchantid', 'label' => '商户号 PartnerID', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'wechat_appid', 'label' => '授权绑定的AppID', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'wechat_payment_type', 'label' => '支付方式', 'type' => 'select', 'default' => 'native', 'options' => [
|
||||
['label' => '扫码支付(Native)', 'value' => 'native'],
|
||||
['label' => 'H5支付', 'value' => 'h5'],
|
||||
]],
|
||||
['key' => 'wechat_h5', 'label' => '移动端 H5 跳转', 'type' => 'switch', 'default' => false, 'help' => '需开通 H5 支付。开启后手机浏览器(非微信内)跳转微信付款;关闭则显示站内二维码'],
|
||||
['key' => 'wechat_jsapi', 'label' => '微信内 JSAPI 支付', 'type' => 'switch', 'default' => false, 'help' => '需开通 JSAPI 支付并在商户平台配置支付授权目录。微信内打开时直接调起支付;用户 openid 通过「用户→微信公众号」的 AppID/AppSecret 静默授权获取,公众号 AppID 必须与上方 AppID 一致'],
|
||||
['key' => 'wechat_api_v3_key', 'label' => '支付APIv3密钥', 'type' => 'input', 'inputType' => 'password', 'secret' => true, 'default' => '', 'help' => '微信商户平台设置的 APIv3 密钥(32位)。'],
|
||||
['key' => 'wechat_private_key_path', 'label' => '商户私钥路径', 'type' => 'input', 'default' => '', 'help' => 'apiclient_key.pem 绝对路径,用于签名。'],
|
||||
['key' => 'wechat_cert_path', 'label' => '商户API证书路径', 'type' => 'input', 'default' => '', 'help' => 'apiclient_cert.pem 绝对路径。'],
|
||||
@@ -534,11 +533,7 @@ class SchemaPayment {
|
||||
['key' => 'epay_apiurl', 'label' => 'API接口网址', 'type' => 'input', 'default' => '', 'help' => '请填写接口地址,例如:https://pay.xxxxxxx.cn/'],
|
||||
['key' => 'epay_pid', 'label' => '商户号', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'epay_key', 'label' => '商户秘钥', 'type' => 'input', 'inputType' => 'password', 'secret' => true, 'default' => '', 'help' => 'MD5 通信密钥。'],
|
||||
['key' => 'epay_device', 'label' => '下单终端', 'type' => 'select', 'default' => 'auto', 'options' => [
|
||||
['label' => '自动(按 UA)', 'value' => 'auto'],
|
||||
['label' => '电脑端 submit', 'value' => 'pc'],
|
||||
['label' => '移动端 mapi', 'value' => 'mobile'],
|
||||
]],
|
||||
['key' => 'epay_qrcode', 'label' => '电脑端站内二维码', 'type' => 'switch', 'default' => true, 'help' => '开启后电脑端通过 mapi 接口获取二维码在站内展示(接口未返回二维码时跳转支付页);关闭则跳转易支付收银台。移动端始终跳转'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
|
||||
@@ -544,21 +544,18 @@ trait PayOrders {
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($payment_result['redirect'])) {
|
||||
$cashier = \AKSRT\Payment\PaymentOrchestrator::cashier_payload($payment_result);
|
||||
if ($cashier !== []) {
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'redirect' => true,
|
||||
'payment_url' => (string) ($payment_result['payment_url'] ?? ''),
|
||||
'qr_content' => (string) ($payment_result['qr_content'] ?? ''),
|
||||
'data' => array_merge($cashier, [
|
||||
'order_number' => $order_number,
|
||||
'amount' => $amount_decimal,
|
||||
'pay_modo' => '0',
|
||||
'message' => $payment_result['message'],
|
||||
'reload' => false,
|
||||
'poll' => true,
|
||||
'replayed' => false,
|
||||
],
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ trait PayRest {
|
||||
],
|
||||
]);
|
||||
|
||||
register_rest_route('aksrt/v1', '/pay/content/order/(?P<order_number>[A-Za-z0-9_-]+)', [
|
||||
register_rest_route('aksrt/v1', '/pay/order/(?P<order_number>[A-Za-z0-9_-]+)', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_order_status'],
|
||||
'permission_callback' => [$this, 'check_rest_nonce'],
|
||||
@@ -164,7 +164,7 @@ trait PayRest {
|
||||
|
||||
|
||||
/**
|
||||
* GET /pay/content/order/{order_number}
|
||||
* GET /pay/order/{order_number} (all order types; guests limited to content orders)
|
||||
*/
|
||||
public function rest_order_status(\WP_REST_Request $request) {
|
||||
$uid = get_current_user_id();
|
||||
|
||||
@@ -488,20 +488,17 @@ trait VipServicePurchase {
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($payment_result['redirect'])) {
|
||||
$cashier = \AKSRT\Payment\PaymentOrchestrator::cashier_payload($payment_result);
|
||||
if ($cashier !== []) {
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'redirect' => true,
|
||||
'payment_url' => (string) ($payment_result['payment_url'] ?? ''),
|
||||
'qr_content' => (string) ($payment_result['qr_content'] ?? ''),
|
||||
'data' => array_merge($cashier, [
|
||||
'order_number' => $order_number,
|
||||
'amount' => $amount_decimal,
|
||||
'message' => $payment_result['message'],
|
||||
'reload' => false,
|
||||
'poll' => true,
|
||||
'replayed' => false,
|
||||
],
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
+33
-18
@@ -93,6 +93,24 @@ trait ScriptsPart1b {
|
||||
&& class_exists('\AKSRT\Credits\Signin')
|
||||
&& class_exists('\AKSRT\Settings\Settings')
|
||||
&& !empty(\AKSRT\Settings\Settings::get_signin_reward_settings()['enabled']);
|
||||
// 统一收银台(站内二维码 / 当前页跳转 / 表单提交 / JSAPI):按需作为依赖入队。
|
||||
$qrcode_js_path = get_template_directory() . '/assets/js/vendor/qrcode.min.js';
|
||||
wp_register_script(
|
||||
'aksrt-qrcode',
|
||||
$base_uri . '/assets/js/vendor/qrcode.min.js',
|
||||
[],
|
||||
file_exists($qrcode_js_path) ? (string) filemtime($qrcode_js_path) : $version,
|
||||
true
|
||||
);
|
||||
$pay_cashier_js = get_template_directory() . '/assets/js/pay-cashier.js';
|
||||
wp_register_script(
|
||||
'aksrt-pay-cashier',
|
||||
$base_uri . '/assets/js/pay-cashier.js',
|
||||
['loader_js', 'aksrt-qrcode'],
|
||||
file_exists($pay_cashier_js) ? (string) filemtime($pay_cashier_js) : $version,
|
||||
true
|
||||
);
|
||||
|
||||
$vip_coordinator = get_template_directory() . '/assets/js/vip-purchase-coordinator.js';
|
||||
if ((($vip_on && is_user_logged_in()) || $credits_on || $affiliate_on) && file_exists($vip_coordinator)) {
|
||||
wp_enqueue_script(
|
||||
@@ -120,7 +138,7 @@ trait ScriptsPart1b {
|
||||
wp_enqueue_script(
|
||||
'aksrt-pay-vip',
|
||||
$base_uri . '/assets/js/pay-vip.js',
|
||||
['loader_js', 'aksrt-vip-coordinator'],
|
||||
['loader_js', 'aksrt-vip-coordinator', 'aksrt-pay-cashier'],
|
||||
(string) filemtime($vip_js),
|
||||
true
|
||||
);
|
||||
@@ -407,7 +425,7 @@ trait ScriptsPart1b {
|
||||
wp_enqueue_script(
|
||||
'aksrt-order-center',
|
||||
$base_uri . '/assets/js/order-center.js',
|
||||
['loader_js'],
|
||||
['loader_js', 'aksrt-pay-cashier'],
|
||||
(string) filemtime($order_center_js),
|
||||
true
|
||||
);
|
||||
@@ -667,7 +685,7 @@ trait ScriptsPart1b {
|
||||
wp_enqueue_script(
|
||||
'aksrt-cms-download',
|
||||
$base_uri . '/assets/js/cms-download.js',
|
||||
['loader_js'],
|
||||
['loader_js', 'aksrt-pay-cashier'],
|
||||
file_exists($cms_download_js_path) ? (string) filemtime($cms_download_js_path) : $version,
|
||||
true
|
||||
);
|
||||
@@ -692,28 +710,25 @@ trait ScriptsPart1b {
|
||||
|
||||
// profile 页:加载 credits.js(签到 + 邀请 + 提现交互)
|
||||
if ($is_profile_page) {
|
||||
$affiliate_deps = [];
|
||||
if ($affiliate_on) {
|
||||
// 推广二维码生成库(本地打包,无外部 CDN)。
|
||||
$qrcode_js_path = get_template_directory() . '/assets/js/vendor/qrcode.min.js';
|
||||
wp_enqueue_script(
|
||||
'aksrt-qrcode',
|
||||
$base_uri . '/assets/js/vendor/qrcode.min.js',
|
||||
[],
|
||||
file_exists($qrcode_js_path) ? (string) filemtime($qrcode_js_path) : $version,
|
||||
true
|
||||
);
|
||||
$affiliate_deps[] = 'aksrt-qrcode';
|
||||
}
|
||||
// aksrt-pay-cashier 依赖 aksrt-qrcode(推广海报二维码同用)。
|
||||
$credits_js_path = get_template_directory() . '/assets/js/credits.js';
|
||||
wp_enqueue_script(
|
||||
'aksrt-credits',
|
||||
$base_uri . '/assets/js/credits.js',
|
||||
array_merge(['loader_js', 'aksrt-vip-coordinator'], $affiliate_deps),
|
||||
['loader_js', 'aksrt-vip-coordinator', 'aksrt-pay-cashier'],
|
||||
file_exists($credits_js_path) ? (string) filemtime($credits_js_path) : $version,
|
||||
true
|
||||
);
|
||||
|
||||
foreach (['credits-withdraw-history' => ['loader_js']] + ($affiliate_on ? ['credits-affiliate' => ['loader_js', 'aksrt-qrcode']] : []) as $credits_part => $credits_part_deps) {
|
||||
$credits_part_path = get_template_directory() . '/assets/js/' . $credits_part . '.js';
|
||||
wp_enqueue_script(
|
||||
'aksrt-' . $credits_part,
|
||||
$base_uri . '/assets/js/' . $credits_part . '.js',
|
||||
$credits_part_deps,
|
||||
file_exists($credits_part_path) ? (string) filemtime($credits_part_path) : $version,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -389,7 +389,6 @@ trait BalanceServiceCharge {
|
||||
}
|
||||
if (is_array($existing) && (string) ($order['status'] ?? '') === \AKSRT\Payment\Order::STATUS_PAID) {
|
||||
return [
|
||||
'redirect' => false,
|
||||
'order_number' => $order_number,
|
||||
'amount' => $pay_decimal,
|
||||
'credit_amount' => $credit_decimal,
|
||||
@@ -425,26 +424,21 @@ trait BalanceServiceCharge {
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($payment_result['redirect'])) {
|
||||
return [
|
||||
'redirect' => true,
|
||||
'payment_url' => (string) ($payment_result['payment_url'] ?? ''),
|
||||
'qr_content' => (string) ($payment_result['qr_content'] ?? ''),
|
||||
$cashier = \AKSRT\Payment\PaymentOrchestrator::cashier_payload($payment_result);
|
||||
if ($cashier !== []) {
|
||||
return array_merge($cashier, [
|
||||
'order_number' => $order_number,
|
||||
'amount' => $pay_decimal,
|
||||
'credit_amount' => $credit_decimal,
|
||||
'message' => $payment_result['message'],
|
||||
'reload' => false,
|
||||
'poll' => true,
|
||||
'transaction_id' => 0,
|
||||
'replayed' => false,
|
||||
'wallet' => \AKSRT\Ledger\LedgerService::get_instance()->balance_decimal($user_id),
|
||||
];
|
||||
]);
|
||||
}
|
||||
|
||||
// The synchronous gateway has already committed the recharge and order state.
|
||||
return [
|
||||
'redirect' => false,
|
||||
'order_number' => $order_number,
|
||||
'amount' => $pay_decimal,
|
||||
'credit_amount' => $credit_decimal,
|
||||
|
||||
@@ -73,6 +73,12 @@ class Gateway {
|
||||
register_rest_route('aksrt/v1', '/payment/(?P<gateway>[a-z0-9_]+)/cancel', array_merge($args, [
|
||||
'callback' => [$this, 'handle_cancel'],
|
||||
]));
|
||||
// WeChat JSAPI snsapi_base callback: one-time state transient guards it.
|
||||
register_rest_route('aksrt/v1', '/payment/wechatpay/openid', [
|
||||
'methods' => 'GET',
|
||||
'permission_callback' => '__return_true',
|
||||
'callback' => ['\AKSRT\Payment\Gateways\WechatOpenid', 'handle_callback'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,7 +15,10 @@ interface GatewayInterface {
|
||||
* 创建支付单
|
||||
*
|
||||
* @param array $order 订单数据
|
||||
* @return array{success: bool, payment_url?: string, payment_data?: array, message?: string}
|
||||
* 外部网关返回 mode(qrcode|redirect|form|jsapi)及对应的
|
||||
* code_url / payment_url / form_html / jsapi_params;同步网关(余额/积分)不返回 mode。
|
||||
*
|
||||
* @return array{success: bool, mode?: string, code_url?: string, payment_url?: string, form_html?: string, jsapi_params?: array<string,string>, message?: string}
|
||||
*/
|
||||
public function create_payment(array $order): array;
|
||||
|
||||
|
||||
@@ -46,41 +46,52 @@ class AlipayGateway implements \AKSRT\Payment\GatewayInterface {
|
||||
? \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'];
|
||||
$amount = $this->amount_string($order['amount'] ?? null);
|
||||
$subject = (string) ($order['subject'] ?? __('订单支付', 'aksrt'));
|
||||
$config = $this->get_config();
|
||||
|
||||
if ($order_number === '' || $out_trade_no === '' || $amount === '') {
|
||||
return ['success' => false, 'message' => __('订单参数无效', 'aksrt')];
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'out_trade_no' => $out_trade_no,
|
||||
'total_amount' => $amount,
|
||||
'subject' => mb_substr($subject, 0, 256),
|
||||
];
|
||||
$mobile = wp_is_mobile();
|
||||
|
||||
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')];
|
||||
// Off-site cashier: mobile H5 when signed, desktop web when chosen.
|
||||
if (($mobile && $config['h5']) || (!$mobile && $config['pc_mode'] === 'web')) {
|
||||
$payload['_method'] = 'GET';
|
||||
$result = $mobile ? 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,
|
||||
'mode' => \AKSRT\Payment\PaymentOrchestrator::MODE_REDIRECT,
|
||||
'payment_url' => $payment_url,
|
||||
'message' => __('正在跳转支付宝…', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
// Face-to-face precreate: on-site QR code (deep link on mobile).
|
||||
$data = YansongdaPayFactory::to_array(Pay::alipay()->scan($payload));
|
||||
$qr_code = (string) ($data['qr_code'] ?? '');
|
||||
if ($qr_code === '') {
|
||||
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'),
|
||||
'success' => true,
|
||||
'mode' => \AKSRT\Payment\PaymentOrchestrator::MODE_QRCODE,
|
||||
'code_url' => $qr_code,
|
||||
'payment_url' => $mobile ? $qr_code : '',
|
||||
'message' => __('请使用支付宝扫码支付', 'aksrt'),
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
return ['success' => false, 'message' => $e->getMessage()];
|
||||
@@ -277,15 +288,15 @@ class AlipayGateway implements \AKSRT\Payment\GatewayInterface {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{app_id:string,private_key:string,app_public_cert:string,alipay_public_cert:string,alipay_root_cert:string,payment_type:string}
|
||||
* @return array{app_id:string,private_key:string,app_public_cert:string,alipay_public_cert:string,alipay_root_cert:string,pc_mode:string,h5:bool}
|
||||
*/
|
||||
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';
|
||||
$pc_mode = sanitize_key((string) ($all['alipay_pc_mode'] ?? 'facepay'));
|
||||
if (!in_array($pc_mode, ['facepay', 'web'], true)) {
|
||||
$pc_mode = 'facepay';
|
||||
}
|
||||
return [
|
||||
'app_id' => (string) ($all['alipay_appid'] ?? ''),
|
||||
@@ -293,7 +304,8 @@ class AlipayGateway implements \AKSRT\Payment\GatewayInterface {
|
||||
'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,
|
||||
'pc_mode' => $pc_mode,
|
||||
'h5' => !empty($all['alipay_h5']),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -65,56 +65,74 @@ class EpayGateway implements \AKSRT\Payment\GatewayInterface {
|
||||
'return_url' => rest_url('aksrt/v1/payment/' . $this->get_id() . '/return'),
|
||||
'name' => mb_substr(wp_strip_all_tags($subject), 0, 127),
|
||||
'money' => $amount,
|
||||
'sitename' => mb_substr(wp_strip_all_tags((string) get_bloginfo('name')), 0, 64),
|
||||
];
|
||||
$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, $out_trade_no);
|
||||
}
|
||||
$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,
|
||||
'out_trade_no' => $out_trade_no,
|
||||
'code_url' => $qrcode,
|
||||
'channel' => $this->channel,
|
||||
],
|
||||
'message' => __('易支付订单已创建', 'aksrt'),
|
||||
];
|
||||
if ($pay_url !== '') {
|
||||
$result['payment_url'] = $pay_url;
|
||||
} elseif ($qrcode !== '') {
|
||||
$result['payment_url'] = $qrcode;
|
||||
}
|
||||
return $result;
|
||||
// Mobile, or desktop with the on-site QR switched off: hand off to the
|
||||
// Epay cashier via a self-submitting form (current tab).
|
||||
if (wp_is_mobile() || !$config['qrcode']) {
|
||||
return $this->submit_form_result($config, $params);
|
||||
}
|
||||
|
||||
return $this->browser_submit_result($config['apiurl'], $params, $order_number, $out_trade_no);
|
||||
// Desktop on-site QR: mapi.php returns qrcode / payurl as JSON.
|
||||
$params['clientip'] = function_exists('aksrt_get_client_ip') ? aksrt_get_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? '');
|
||||
$params['device'] = 'pc';
|
||||
$params['sign'] = $this->sign($params, $config['key']);
|
||||
$params['sign_type'] = 'MD5';
|
||||
$response = wp_remote_post($this->endpoint($config['apiurl'], 'mapi.php'), [
|
||||
'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) || (int) ($body['code'] ?? 0) !== 1) {
|
||||
$msg = is_array($body) ? (string) ($body['msg'] ?? $body['message'] ?? '') : '';
|
||||
return ['success' => false, 'message' => $msg !== '' ? $msg : __('易支付下单失败', 'aksrt')];
|
||||
}
|
||||
$qrcode = (string) ($body['qrcode'] ?? $body['code_url'] ?? '');
|
||||
$pay_url = (string) ($body['payurl'] ?? '');
|
||||
if ($qrcode !== '') {
|
||||
return [
|
||||
'success' => true,
|
||||
'mode' => \AKSRT\Payment\PaymentOrchestrator::MODE_QRCODE,
|
||||
'code_url' => $qrcode,
|
||||
'message' => $this->channel === 'wxpay' ? __('请使用微信扫码支付', 'aksrt') : __('请使用支付宝扫码支付', 'aksrt'),
|
||||
];
|
||||
}
|
||||
if ($pay_url !== '') {
|
||||
return [
|
||||
'success' => true,
|
||||
'mode' => \AKSRT\Payment\PaymentOrchestrator::MODE_REDIRECT,
|
||||
'payment_url' => $pay_url,
|
||||
'message' => __('正在跳转支付页面…', 'aksrt'),
|
||||
];
|
||||
}
|
||||
return ['success' => false, 'message' => __('易支付未返回支付链接', 'aksrt')];
|
||||
}
|
||||
|
||||
/**
|
||||
* Self-submitting POST form to submit.php (signed params as hidden inputs).
|
||||
*
|
||||
* @param array{apiurl:string,key:string} $config
|
||||
* @param array<string,string> $params
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function submit_form_result(array $config, array $params): array {
|
||||
$params['sign'] = $this->sign($params, $config['key']);
|
||||
$params['sign_type'] = 'MD5';
|
||||
$inputs = '';
|
||||
foreach ($params as $name => $value) {
|
||||
$inputs .= '<input type="hidden" name="' . esc_attr((string) $name) . '" value="' . esc_attr((string) $value) . '">';
|
||||
}
|
||||
return [
|
||||
'success' => true,
|
||||
'mode' => \AKSRT\Payment\PaymentOrchestrator::MODE_FORM,
|
||||
'form_html' => '<form method="post" action="' . esc_url($this->endpoint($config['apiurl'], 'submit.php')) . '" accept-charset="utf-8">' . $inputs . '</form>',
|
||||
'message' => __('正在跳转支付页面…', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
public function verify_callback(\WP_REST_Request $request): array {
|
||||
@@ -231,16 +249,12 @@ class EpayGateway implements \AKSRT\Payment\GatewayInterface {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{apiurl:string,pid:string,key:string,device:string}
|
||||
* @return array{apiurl:string,pid:string,key:string,qrcode:bool}
|
||||
*/
|
||||
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 === '/') {
|
||||
@@ -250,7 +264,7 @@ class EpayGateway implements \AKSRT\Payment\GatewayInterface {
|
||||
'apiurl' => $apiurl,
|
||||
'pid' => (string) ($all['epay_pid'] ?? ''),
|
||||
'key' => (string) ($all['epay_key'] ?? ''),
|
||||
'device' => $device,
|
||||
'qrcode' => !array_key_exists('epay_qrcode', $all) || !empty($all['epay_qrcode']),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -300,43 +314,6 @@ class EpayGateway implements \AKSRT\Payment\GatewayInterface {
|
||||
}
|
||||
}
|
||||
|
||||
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}
|
||||
*/
|
||||
/**
|
||||
* @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, string $out_trade_no = ''): array {
|
||||
$url = add_query_arg($params, $this->endpoint($apiurl, 'submit.php'));
|
||||
return [
|
||||
'success' => true,
|
||||
'payment_url' => $url,
|
||||
'payment_data' => [
|
||||
'order_number' => $order_number,
|
||||
'out_trade_no' => $out_trade_no !== '' ? $out_trade_no : $order_number,
|
||||
'channel' => $this->channel,
|
||||
],
|
||||
'message' => __('易支付支付链接已生成', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Map local order_number → provider out_trade_no (supports resume attempts).
|
||||
*/
|
||||
|
||||
@@ -90,14 +90,10 @@ class PayPalGateway implements \AKSRT\Payment\GatewayInterface {
|
||||
}
|
||||
|
||||
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'),
|
||||
'success' => true,
|
||||
'mode' => \AKSRT\Payment\PaymentOrchestrator::MODE_REDIRECT,
|
||||
'payment_url' => $approveUrl,
|
||||
'message' => __('正在跳转 PayPal…', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -110,14 +110,10 @@ class StripeGateway implements \AKSRT\Payment\GatewayInterface {
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'payment_url' => $session->url,
|
||||
'payment_data' => [
|
||||
'order_number' => $order_number,
|
||||
'out_trade_no' => $out_trade_no,
|
||||
'session_id' => $session->id,
|
||||
],
|
||||
'message' => __('Stripe 支付页面已创建', 'aksrt'),
|
||||
'success' => true,
|
||||
'mode' => \AKSRT\Payment\PaymentOrchestrator::MODE_REDIRECT,
|
||||
'payment_url' => (string) $session->url,
|
||||
'message' => __('正在跳转 Stripe…', 'aksrt'),
|
||||
];
|
||||
} catch (\Stripe\Exception\ApiErrorException $e) {
|
||||
return ['success' => false, 'message' => $e->getMessage()];
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
/**
|
||||
* Payer openid for WeChat JSAPI payments (official-account silent OAuth).
|
||||
*
|
||||
* Sources, in order:
|
||||
* 1. the logged-in user's bound official-account openid (same AppID only)
|
||||
* 2. a signed short-lived cookie set by the snsapi_base callback below
|
||||
* When neither exists the gateway sends the browser through authorize_url();
|
||||
* the callback exchanges the code, stores the cookie and returns to the page
|
||||
* so the user can tap pay again.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Payment\Gateways;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class WechatOpenid {
|
||||
|
||||
private const COOKIE = 'aksrt_wxpay_openid';
|
||||
private const STATE_KEY = 'aksrt_wxpay_oauth_';
|
||||
private const STATE_TTL = 10 * MINUTE_IN_SECONDS;
|
||||
private const COOKIE_TTL = 30 * DAY_IN_SECONDS;
|
||||
|
||||
public static function in_wechat(): bool {
|
||||
$ua = (string) ($_SERVER['HTTP_USER_AGENT'] ?? '');
|
||||
return $ua !== '' && stripos($ua, 'MicroMessenger') !== false;
|
||||
}
|
||||
|
||||
public static function resolve(string $pay_appid): string {
|
||||
if ($pay_appid === '' || !self::gzh_matches($pay_appid)) {
|
||||
return '';
|
||||
}
|
||||
$user_id = get_current_user_id();
|
||||
if ($user_id > 0) {
|
||||
$bound = sanitize_text_field((string) get_user_meta($user_id, 'aksrt_weixingzh_id', true));
|
||||
if ($bound !== '') {
|
||||
return $bound;
|
||||
}
|
||||
}
|
||||
return self::read_cookie($pay_appid);
|
||||
}
|
||||
|
||||
/**
|
||||
* snsapi_base authorize URL that comes back to the current page. Empty when
|
||||
* the official account is not configured with the pay AppID.
|
||||
*/
|
||||
public static function authorize_url(string $pay_appid): string {
|
||||
if ($pay_appid === '' || !self::gzh_matches($pay_appid) || self::gzh()['appsecret'] === '') {
|
||||
return '';
|
||||
}
|
||||
$back = wp_validate_redirect((string) wp_get_referer(), home_url('/'));
|
||||
$state = wp_generate_password(24, false, false);
|
||||
set_transient(self::STATE_KEY . $state, ['appid' => $pay_appid, 'back' => $back], self::STATE_TTL);
|
||||
|
||||
return 'https://open.weixin.qq.com/connect/oauth2/authorize?' . http_build_query([
|
||||
'appid' => $pay_appid,
|
||||
'redirect_uri' => rest_url('aksrt/v1/payment/wechatpay/openid'),
|
||||
'response_type' => 'code',
|
||||
'scope' => 'snsapi_base',
|
||||
'state' => $state,
|
||||
], '', '&', PHP_QUERY_RFC3986) . '#wechat_redirect';
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /payment/wechatpay/openid?code&state — one-time state, then redirect back.
|
||||
*/
|
||||
public static function handle_callback(\WP_REST_Request $request) {
|
||||
$state = preg_replace('/[^A-Za-z0-9]/', '', (string) $request->get_param('state'));
|
||||
$code = sanitize_text_field((string) $request->get_param('code'));
|
||||
$saved = $state !== '' ? get_transient(self::STATE_KEY . $state) : false;
|
||||
if ($state !== '') {
|
||||
delete_transient(self::STATE_KEY . $state);
|
||||
}
|
||||
if (!is_array($saved) || $code === '') {
|
||||
return new \WP_REST_Response(['message' => __('微信授权已失效,请返回重新支付', 'aksrt')], 400);
|
||||
}
|
||||
|
||||
$appid = (string) ($saved['appid'] ?? '');
|
||||
$back = wp_validate_redirect((string) ($saved['back'] ?? ''), home_url('/'));
|
||||
$gzh = self::gzh();
|
||||
if ($appid === '' || !self::gzh_matches($appid) || $gzh['appsecret'] === '') {
|
||||
return new \WP_REST_Response(['message' => __('微信支付授权配置无效', 'aksrt')], 400);
|
||||
}
|
||||
|
||||
$response = wp_remote_get(add_query_arg([
|
||||
'appid' => rawurlencode($appid),
|
||||
'secret' => rawurlencode($gzh['appsecret']),
|
||||
'code' => rawurlencode($code),
|
||||
'grant_type' => 'authorization_code',
|
||||
], 'https://api.weixin.qq.com/sns/oauth2/access_token'), ['timeout' => 10]);
|
||||
$body = is_wp_error($response) ? null : json_decode((string) wp_remote_retrieve_body($response), true);
|
||||
$openid = is_array($body) ? sanitize_text_field((string) ($body['openid'] ?? '')) : '';
|
||||
if ($openid !== '') {
|
||||
self::write_cookie($appid, $openid);
|
||||
}
|
||||
|
||||
wp_safe_redirect($back);
|
||||
exit;
|
||||
}
|
||||
|
||||
/** @return array{appid:string,appsecret:string} */
|
||||
private static function gzh(): array {
|
||||
$gzh = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_weixingzh_settings() : [];
|
||||
return [
|
||||
'appid' => trim((string) ($gzh['appid'] ?? '')),
|
||||
'appsecret' => trim((string) ($gzh['appsecret'] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
private static function gzh_matches(string $pay_appid): bool {
|
||||
$appid = self::gzh()['appid'];
|
||||
return $appid !== '' && hash_equals($appid, $pay_appid);
|
||||
}
|
||||
|
||||
private static function signature(string $appid, string $openid): string {
|
||||
return hash_hmac('sha256', $appid . '|' . $openid, wp_salt('auth'));
|
||||
}
|
||||
|
||||
private static function read_cookie(string $appid): string {
|
||||
$raw = isset($_COOKIE[self::COOKIE]) ? sanitize_text_field(wp_unslash((string) $_COOKIE[self::COOKIE])) : '';
|
||||
$parts = explode('.', $raw, 2);
|
||||
if (count($parts) !== 2 || $parts[0] === '') {
|
||||
return '';
|
||||
}
|
||||
return hash_equals(self::signature($appid, $parts[0]), $parts[1]) ? $parts[0] : '';
|
||||
}
|
||||
|
||||
private static function write_cookie(string $appid, string $openid): void {
|
||||
if (headers_sent()) {
|
||||
return;
|
||||
}
|
||||
setcookie(self::COOKIE, $openid . '.' . self::signature($appid, $openid), [
|
||||
'expires' => time() + self::COOKIE_TTL,
|
||||
'path' => COOKIEPATH ?: '/',
|
||||
'domain' => COOKIE_DOMAIN ?: '',
|
||||
'secure' => is_ssl(),
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ class WechatPayGateway implements \AKSRT\Payment\GatewayInterface {
|
||||
: $order_number;
|
||||
}
|
||||
$subject = (string) ($order['subject'] ?? __('订单支付', 'aksrt'));
|
||||
$payment_type = $this->get_config()['payment_type'];
|
||||
$config = $this->get_config();
|
||||
try {
|
||||
$total_fen = \AKSRT\Ledger\AssetAmount::balance(is_string($order['amount'] ?? null) ? trim($order['amount']) : '')->minor();
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
@@ -71,39 +71,73 @@ class WechatPayGateway implements \AKSRT\Payment\GatewayInterface {
|
||||
],
|
||||
];
|
||||
|
||||
switch ($payment_type) {
|
||||
case 'h5':
|
||||
$payload['scene_info'] = [
|
||||
'payer_client_ip' => (string) ($_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'),
|
||||
'h5_info' => ['type' => 'Wap'],
|
||||
$in_wechat = WechatOpenid::in_wechat();
|
||||
|
||||
// Inside WeChat: JSAPI with the payer's official-account openid.
|
||||
if ($in_wechat && $config['jsapi']) {
|
||||
$openid = WechatOpenid::resolve($config['appid']);
|
||||
if ($openid !== '') {
|
||||
$payload['payer'] = ['openid' => $openid];
|
||||
$invoke = YansongdaPayFactory::to_array(Pay::wechat()->mp($payload));
|
||||
$params = [];
|
||||
foreach (['appId', 'timeStamp', 'nonceStr', 'package', 'signType', 'paySign'] as $key) {
|
||||
$params[$key] = (string) ($invoke[$key] ?? '');
|
||||
}
|
||||
if (in_array('', $params, true)) {
|
||||
return ['success' => false, 'message' => __('微信支付调起参数生成失败', 'aksrt')];
|
||||
}
|
||||
return [
|
||||
'success' => true,
|
||||
'mode' => \AKSRT\Payment\PaymentOrchestrator::MODE_JSAPI,
|
||||
'jsapi_params' => $params,
|
||||
'message' => __('正在调起微信支付…', 'aksrt'),
|
||||
];
|
||||
$result = Pay::wechat()->h5($payload);
|
||||
break;
|
||||
case 'native':
|
||||
default:
|
||||
$result = Pay::wechat()->scan($payload);
|
||||
break;
|
||||
}
|
||||
$authorize = WechatOpenid::authorize_url($config['appid']);
|
||||
if ($authorize !== '') {
|
||||
return [
|
||||
'success' => true,
|
||||
'mode' => \AKSRT\Payment\PaymentOrchestrator::MODE_REDIRECT,
|
||||
'payment_url' => $authorize,
|
||||
'message' => __('正在获取微信授权,返回后请重新支付', 'aksrt'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$data = YansongdaPayFactory::to_array($result);
|
||||
$payment_data = array_merge($data, [
|
||||
'order_number' => $order_number,
|
||||
'out_trade_no' => $out_trade_no,
|
||||
]);
|
||||
// Mobile browser outside WeChat: H5 hand-off in the current tab.
|
||||
if (!$in_wechat && $config['h5'] && wp_is_mobile()) {
|
||||
$payload['scene_info'] = [
|
||||
'payer_client_ip' => function_exists('aksrt_get_client_ip')
|
||||
? aksrt_get_client_ip()
|
||||
: (string) ($_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'),
|
||||
'h5_info' => ['type' => 'Wap'],
|
||||
];
|
||||
$data = YansongdaPayFactory::to_array(Pay::wechat()->h5($payload));
|
||||
$h5_url = (string) ($data['h5_url'] ?? '');
|
||||
if ($h5_url === '') {
|
||||
return ['success' => false, 'message' => __('微信 H5 支付链接生成失败', 'aksrt')];
|
||||
}
|
||||
$back = add_query_arg('out_trade_no', $out_trade_no, rest_url('aksrt/v1/payment/wechatpay/return'));
|
||||
return [
|
||||
'success' => true,
|
||||
'mode' => \AKSRT\Payment\PaymentOrchestrator::MODE_REDIRECT,
|
||||
'payment_url' => $h5_url . (str_contains($h5_url, '?') ? '&' : '?') . 'redirect_url=' . rawurlencode($back),
|
||||
'message' => __('正在跳转微信支付…', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
$result_data = [
|
||||
'success' => true,
|
||||
'payment_data' => $payment_data,
|
||||
'message' => __('微信支付订单已创建', 'aksrt'),
|
||||
// Default: Native on-site QR code.
|
||||
$data = YansongdaPayFactory::to_array(Pay::wechat()->scan($payload));
|
||||
$code_url = (string) ($data['code_url'] ?? '');
|
||||
if ($code_url === '') {
|
||||
return ['success' => false, 'message' => __('微信支付二维码生成失败', 'aksrt')];
|
||||
}
|
||||
return [
|
||||
'success' => true,
|
||||
'mode' => \AKSRT\Payment\PaymentOrchestrator::MODE_QRCODE,
|
||||
'code_url' => $code_url,
|
||||
'message' => __('请使用微信扫码支付', 'aksrt'),
|
||||
];
|
||||
|
||||
if ($payment_type === 'native' && !empty($data['code_url'])) {
|
||||
$result_data['payment_url'] = (string) $data['code_url'];
|
||||
} elseif ($payment_type === 'h5' && !empty($data['h5_url'])) {
|
||||
$result_data['payment_url'] = (string) $data['h5_url'];
|
||||
}
|
||||
|
||||
return $result_data;
|
||||
} catch (\Throwable $e) {
|
||||
return ['success' => false, 'message' => $e->getMessage()];
|
||||
}
|
||||
@@ -224,10 +258,9 @@ class WechatPayGateway implements \AKSRT\Payment\GatewayInterface {
|
||||
|
||||
try {
|
||||
YansongdaPayFactory::boot_wechat();
|
||||
$action = $this->query_action();
|
||||
// The default (jsapi) query plugin hits the same out-trade-no endpoint for every pay type.
|
||||
$result = Pay::wechat()->query([
|
||||
'out_trade_no' => $out_trade_no,
|
||||
'_action' => $action,
|
||||
]);
|
||||
$data = YansongdaPayFactory::to_array($result);
|
||||
|
||||
@@ -267,7 +300,6 @@ class WechatPayGateway implements \AKSRT\Payment\GatewayInterface {
|
||||
YansongdaPayFactory::boot_wechat();
|
||||
Pay::wechat()->close([
|
||||
'out_trade_no' => $out_trade_no,
|
||||
'_action' => $this->query_action(),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
// ignore — new attempt still uses a fresh out_trade_no
|
||||
@@ -291,11 +323,8 @@ class WechatPayGateway implements \AKSRT\Payment\GatewayInterface {
|
||||
|
||||
try {
|
||||
YansongdaPayFactory::boot_wechat();
|
||||
$action = $this->query_action();
|
||||
|
||||
$query = Pay::wechat()->query([
|
||||
'out_trade_no' => $out_trade_no,
|
||||
'_action' => $action,
|
||||
]);
|
||||
$q = YansongdaPayFactory::to_array($query);
|
||||
$total_fen = (int) YansongdaPayFactory::dig($q, 'amount.total', 0);
|
||||
@@ -313,7 +342,6 @@ class WechatPayGateway implements \AKSRT\Payment\GatewayInterface {
|
||||
$request_id !== '' ? $request_id : 'REF_' . $order_number
|
||||
);
|
||||
$result = Pay::wechat()->refund([
|
||||
'_action' => 'refund_' . $action,
|
||||
'out_trade_no' => $out_trade_no,
|
||||
'out_refund_no' => $refund_request_id,
|
||||
'reason' => __('用户申请退款', 'aksrt'),
|
||||
@@ -351,7 +379,7 @@ class WechatPayGateway implements \AKSRT\Payment\GatewayInterface {
|
||||
YansongdaPayFactory::boot_wechat();
|
||||
$result = Pay::wechat()->query([
|
||||
'out_refund_no' => $request_id,
|
||||
'_action' => 'refund_' . $this->query_action(),
|
||||
'_action' => 'refund',
|
||||
]);
|
||||
$data = YansongdaPayFactory::to_array($result);
|
||||
$status = strtoupper((string) ($data['status'] ?? ''));
|
||||
@@ -369,35 +397,23 @@ class WechatPayGateway implements \AKSRT\Payment\GatewayInterface {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{mch_id:string,appid:string,api_v3_key:string,private_key:string,cert_path:string,payment_type:string}
|
||||
* @return array{mch_id:string,appid:string,api_v3_key:string,private_key:string,cert_path:string,h5:bool,jsapi:bool}
|
||||
*/
|
||||
private function get_config(): array {
|
||||
$all = class_exists('\AKSRT\Settings\Settings')
|
||||
? \AKSRT\Settings\Settings::get_all()
|
||||
: [];
|
||||
$type = sanitize_key((string) ($all['wechat_payment_type'] ?? 'native'));
|
||||
if (!in_array($type, ['native', 'h5'], true)) {
|
||||
$type = 'native';
|
||||
}
|
||||
return [
|
||||
'mch_id' => (string) ($all['wechat_merchantid'] ?? ''),
|
||||
'appid' => (string) ($all['wechat_appid'] ?? ''),
|
||||
'api_v3_key' => (string) ($all['wechat_api_v3_key'] ?? ''),
|
||||
'private_key' => (string) ($all['wechat_private_key_path'] ?? ''),
|
||||
'cert_path' => (string) ($all['wechat_cert_path'] ?? ''),
|
||||
'payment_type' => $type,
|
||||
'mch_id' => (string) ($all['wechat_merchantid'] ?? ''),
|
||||
'appid' => (string) ($all['wechat_appid'] ?? ''),
|
||||
'api_v3_key' => (string) ($all['wechat_api_v3_key'] ?? ''),
|
||||
'private_key' => (string) ($all['wechat_private_key_path'] ?? ''),
|
||||
'cert_path' => (string) ($all['wechat_cert_path'] ?? ''),
|
||||
'h5' => !empty($all['wechat_h5']),
|
||||
'jsapi' => !empty($all['wechat_jsapi']),
|
||||
];
|
||||
}
|
||||
|
||||
private function query_action(): string {
|
||||
$map = [
|
||||
'native' => 'native',
|
||||
'h5' => 'h5',
|
||||
];
|
||||
$type = $this->get_config()['payment_type'];
|
||||
return $map[$type] ?? 'native';
|
||||
}
|
||||
|
||||
private function fen_decimal(int $amount): string {
|
||||
return \AKSRT\Ledger\AssetAmount::from_minor(
|
||||
\AKSRT\Ledger\AssetType::BALANCE,
|
||||
|
||||
@@ -187,21 +187,6 @@ class YansongdaPayFactory {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML body from alipay Response (form auto-submit) when no Location header.
|
||||
*
|
||||
* @param mixed $result
|
||||
*/
|
||||
public static function extract_html_body($result): string {
|
||||
if ($result instanceof \Psr\Http\Message\ResponseInterface) {
|
||||
if ($result->getHeaderLine('Location') !== '') {
|
||||
return '';
|
||||
}
|
||||
return (string) $result->getBody();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit SDK success()/fail() ResponseInterface (or string) and exit.
|
||||
*
|
||||
|
||||
@@ -21,17 +21,29 @@ class PaymentOrchestrator {
|
||||
/** Methods settled synchronously in-process (no provider round trip). */
|
||||
private const SYNC_METHODS = ['balance', 'points'];
|
||||
|
||||
/** Cashier modes a provider gateway may return (see cashier_fields()). */
|
||||
public const MODE_QRCODE = 'qrcode';
|
||||
public const MODE_REDIRECT = 'redirect';
|
||||
public const MODE_FORM = 'form';
|
||||
public const MODE_JSAPI = 'jsapi';
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $order Order row plus subject/amount overrides.
|
||||
* @param array<string,mixed> $opts paid_note: string, order_context: array
|
||||
*
|
||||
* Provider gateways answer with `mode` (qrcode|redirect|form|jsapi) plus the
|
||||
* matching code_url / payment_url / form_html / jsapi_params; synchronous
|
||||
* gateways settle in-process and carry no `mode`.
|
||||
*
|
||||
* @return array{
|
||||
* success:bool,
|
||||
* code?:string,
|
||||
* message:string,
|
||||
* redirect?:bool,
|
||||
* mode?:string,
|
||||
* code_url?:string,
|
||||
* payment_url?:string,
|
||||
* qr_content?:string,
|
||||
* form_html?:string,
|
||||
* jsapi_params?:array<string,string>,
|
||||
* transaction_id?:string,
|
||||
* out_trade_no?:string
|
||||
* }
|
||||
@@ -108,19 +120,18 @@ class PaymentOrchestrator {
|
||||
);
|
||||
}
|
||||
|
||||
$payment_url = (string) ($result['payment_url'] ?? '');
|
||||
$qr_content = (string) ($result['payment_data']['code_url'] ?? '');
|
||||
if ($payment_url !== '' || $qr_content !== '') {
|
||||
// Redirect/scan gateways: the order stays pending until the signed
|
||||
$cashier = self::cashier_fields($result);
|
||||
if ($cashier !== []) {
|
||||
// Provider gateways: the order stays pending until the signed
|
||||
// notify/return callback confirms it. Never pre-mark as paid here.
|
||||
return [
|
||||
return array_merge($cashier, [
|
||||
'success' => true,
|
||||
'redirect' => true,
|
||||
'payment_url' => $payment_url,
|
||||
'qr_content' => $qr_content,
|
||||
'out_trade_no' => (string) ($order['out_trade_no'] ?? $order_number),
|
||||
'message' => (string) ($result['message'] ?? __('请在新页面完成支付。', 'aksrt')),
|
||||
];
|
||||
'message' => (string) ($result['message'] ?? __('请完成支付。', 'aksrt')),
|
||||
]);
|
||||
}
|
||||
if (!in_array($method, self::SYNC_METHODS, true)) {
|
||||
return self::fail('aksrt_pay_failed', __('支付网关未返回收银信息。', 'aksrt'));
|
||||
}
|
||||
|
||||
// Synchronous asset gateways commit the debit and paid transition together.
|
||||
@@ -128,7 +139,6 @@ class PaymentOrchestrator {
|
||||
if (!empty($result['order_committed'])) {
|
||||
return [
|
||||
'success' => true,
|
||||
'redirect' => false,
|
||||
'transaction_id' => $transaction_id,
|
||||
'replayed' => !empty($result['replayed']),
|
||||
'message' => (string) ($result['message'] ?? __('支付成功。', 'aksrt')),
|
||||
@@ -149,7 +159,6 @@ class PaymentOrchestrator {
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'redirect' => false,
|
||||
'transaction_id' => $transaction_id,
|
||||
'message' => (string) ($result['message'] ?? __('支付成功。', 'aksrt')),
|
||||
];
|
||||
@@ -177,6 +186,67 @@ class PaymentOrchestrator {
|
||||
return ['gateway' => $gateway];
|
||||
}
|
||||
|
||||
/**
|
||||
* Cashier keys of a pay() result, for REST builders to pass through verbatim.
|
||||
* Empty for synchronous settlements, so `!empty(...)` doubles as "awaiting provider".
|
||||
*
|
||||
* @param array<string,mixed> $pay_result
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public static function cashier_payload(array $pay_result): array {
|
||||
if (empty($pay_result['mode'])) {
|
||||
return [];
|
||||
}
|
||||
return array_intersect_key($pay_result, array_flip(['mode', 'code_url', 'payment_url', 'form_html', 'jsapi_params']));
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a provider create_payment() result into the cashier contract
|
||||
* shared by every front-end cashier. Empty when the gateway is synchronous.
|
||||
*
|
||||
* @param array<string,mixed> $result
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public static function cashier_fields(array $result): array {
|
||||
$mode = sanitize_key((string) ($result['mode'] ?? ''));
|
||||
switch ($mode) {
|
||||
case self::MODE_QRCODE:
|
||||
$code_url = (string) ($result['code_url'] ?? '');
|
||||
return $code_url === '' ? [] : [
|
||||
'mode' => $mode,
|
||||
'code_url' => $code_url,
|
||||
// Optional mobile deep link shown beside the QR code.
|
||||
'payment_url' => (string) ($result['payment_url'] ?? ''),
|
||||
];
|
||||
case self::MODE_REDIRECT:
|
||||
$url = (string) ($result['payment_url'] ?? '');
|
||||
return $url === '' ? [] : ['mode' => $mode, 'payment_url' => $url];
|
||||
case self::MODE_FORM:
|
||||
$html = (string) ($result['form_html'] ?? '');
|
||||
return $html === '' ? [] : ['mode' => $mode, 'form_html' => $html];
|
||||
case self::MODE_JSAPI:
|
||||
$params = $result['jsapi_params'] ?? null;
|
||||
return is_array($params) && $params !== [] ? ['mode' => $mode, 'jsapi_params' => $params] : [];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fields every cashier builder forwards to the browser for a pending provider payment.
|
||||
*
|
||||
* @param array<string,mixed> $payment_result PaymentOrchestrator::pay() success result.
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public static function public_cashier(array $payment_result): array {
|
||||
$out = ['mode' => (string) ($payment_result['mode'] ?? '')];
|
||||
foreach (['code_url', 'payment_url', 'form_html', 'jsapi_params'] as $key) {
|
||||
if (isset($payment_result[$key]) && $payment_result[$key] !== '') {
|
||||
$out[$key] = $payment_result[$key];
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
private static function failure_code(string $method, string $message): string {
|
||||
if ($method === 'balance' && (str_contains($message, '余额') || str_contains($message, '不足'))) {
|
||||
return 'aksrt_balance_insufficient';
|
||||
|
||||
@@ -422,8 +422,6 @@ class UserOrders {
|
||||
}
|
||||
if ($this->status_group((string) ($order['status'] ?? '')) === OrderCenterQuery::STATUS_PAID) {
|
||||
return ['success' => true, 'data' => [
|
||||
'redirect' => false,
|
||||
'payment_url' => '',
|
||||
'order_number' => $order_number,
|
||||
'order' => $this->get_order_detail_view($user_id, $order_number),
|
||||
'replayed' => true,
|
||||
@@ -455,14 +453,13 @@ class UserOrders {
|
||||
'message' => (string) ($result['message'] ?? __('支付失败。', 'aksrt')),
|
||||
];
|
||||
}
|
||||
return ['success' => true, 'data' => [
|
||||
'redirect' => !empty($result['redirect']),
|
||||
'payment_url' => (string) ($result['payment_url'] ?? ''),
|
||||
$cashier = PaymentOrchestrator::cashier_payload($result);
|
||||
return ['success' => true, 'data' => array_merge($cashier, [
|
||||
'order_number' => $order_number,
|
||||
'order' => empty($result['redirect']) ? $this->get_order_detail_view($user_id, $order_number) : null,
|
||||
'order' => $cashier === [] ? $this->get_order_detail_view($user_id, $order_number) : null,
|
||||
'replayed' => !empty($result['replayed']),
|
||||
'balance' => $payment_method === 'balance' ? \AKSRT\Ledger\LedgerService::get_instance()->balance_decimal($user_id) : null,
|
||||
'message' => (string) ($result['message'] ?? ''),
|
||||
]];
|
||||
])];
|
||||
}
|
||||
}
|
||||
@@ -410,7 +410,7 @@ class PaymentRuntime {
|
||||
'apiurl' => esc_url_raw((string) ($settings['epay_apiurl'] ?? '')),
|
||||
'pid' => sanitize_text_field((string) ($settings['epay_pid'] ?? '')),
|
||||
'key' => sanitize_text_field((string) ($settings['epay_key'] ?? '')),
|
||||
'device' => sanitize_key((string) ($settings['epay_device'] ?? 'auto')),
|
||||
'qrcode' => !array_key_exists('epay_qrcode', $settings) || !empty($settings['epay_qrcode']),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user