Merge pull request 'Worktree tighten payment gateways' (#307) from worktree-tighten-payment-gateways into main
Reviewed-on: CYL/wordpress-theme-aksrt#307
This commit is contained in:
commit
158b0d0e61
7 files changed
+323
-25
No files matched your search
@@ -227,16 +227,21 @@ class Gateway {
|
|||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function complete_order(string $gateway_id, array $verify): bool {
|
public function complete_order(string $gateway_id, array $verify): bool {
|
||||||
$order_number = sanitize_text_field((string) ($verify['order_number'] ?? ''));
|
$provider_out_trade_no = sanitize_text_field((string) ($verify['order_number'] ?? ''));
|
||||||
if ($order_number === '' || !class_exists('AKSRT\\Payment\\OrderRepository')) {
|
if ($provider_out_trade_no === '' || !class_exists('AKSRT\\Payment\\OrderRepository')) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$repo = OrderRepository::get_instance();
|
$repo = OrderRepository::get_instance();
|
||||||
$order = $repo->get($order_number);
|
// Callbacks may carry attempt-suffixed out_trade_no (…P2); map back to local order.
|
||||||
|
$order = $repo->resolve_by_out_trade_no($provider_out_trade_no);
|
||||||
if (!$order) {
|
if (!$order) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
$order_number = sanitize_text_field((string) ($order['order_number'] ?? ''));
|
||||||
|
if ($order_number === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
$stored_gateway = sanitize_key((string) ($order['payment_method'] ?? ''));
|
$stored_gateway = sanitize_key((string) ($order['payment_method'] ?? ''));
|
||||||
if ($stored_gateway === '' || !hash_equals($stored_gateway, sanitize_key($gateway_id))) {
|
if ($stored_gateway === '' || !hash_equals($stored_gateway, sanitize_key($gateway_id))) {
|
||||||
@@ -253,8 +258,9 @@ class Gateway {
|
|||||||
|| abs($expected - $paid_amount) > 0.01) {
|
|| abs($expected - $paid_amount) > 0.01) {
|
||||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||||
error_log(sprintf(
|
error_log(sprintf(
|
||||||
'[AKSRT Payment] invalid completion order=%s expected=%.2f paid=%.2f transaction=%s gateway=%s',
|
'[AKSRT Payment] invalid completion order=%s out_trade_no=%s expected=%.2f paid=%.2f transaction=%s gateway=%s',
|
||||||
$order_number,
|
$order_number,
|
||||||
|
$provider_out_trade_no,
|
||||||
$expected,
|
$expected,
|
||||||
$paid_amount,
|
$paid_amount,
|
||||||
$transaction_id !== '' ? 'present' : 'missing',
|
$transaction_id !== '' ? 'present' : 'missing',
|
||||||
|
|||||||
@@ -40,11 +40,17 @@ class AlipayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$order_number = (string) ($order['order_number'] ?? '');
|
$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 = (float) ($order['amount'] ?? 0);
|
$amount = (float) ($order['amount'] ?? 0);
|
||||||
$subject = (string) ($order['subject'] ?? __('订单支付', 'aksrt'));
|
$subject = (string) ($order['subject'] ?? __('订单支付', 'aksrt'));
|
||||||
$payment_type = $this->get_config()['payment_type'];
|
$payment_type = $this->get_config()['payment_type'];
|
||||||
|
|
||||||
if ($order_number === '' || $amount <= 0) {
|
if ($order_number === '' || $out_trade_no === '' || $amount <= 0) {
|
||||||
return ['success' => false, 'message' => __('订单参数无效', 'aksrt')];
|
return ['success' => false, 'message' => __('订单参数无效', 'aksrt')];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +59,7 @@ class AlipayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
'_method' => 'GET',
|
'_method' => 'GET',
|
||||||
'out_trade_no' => $order_number,
|
'out_trade_no' => $out_trade_no,
|
||||||
'total_amount' => number_format($amount, 2, '.', ''),
|
'total_amount' => number_format($amount, 2, '.', ''),
|
||||||
'subject' => mb_substr($subject, 0, 256),
|
'subject' => mb_substr($subject, 0, 256),
|
||||||
];
|
];
|
||||||
@@ -70,7 +76,10 @@ class AlipayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
return [
|
return [
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'payment_url' => $payment_url,
|
'payment_url' => $payment_url,
|
||||||
'payment_data' => ['order_number' => $order_number],
|
'payment_data' => [
|
||||||
|
'order_number' => $order_number,
|
||||||
|
'out_trade_no' => $out_trade_no,
|
||||||
|
],
|
||||||
'message' => __('支付宝支付链接已生成', 'aksrt'),
|
'message' => __('支付宝支付链接已生成', 'aksrt'),
|
||||||
];
|
];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
@@ -140,10 +149,12 @@ class AlipayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
return ['success' => false, 'message' => __('支付宝未配置', 'aksrt')];
|
return ['success' => false, 'message' => __('支付宝未配置', 'aksrt')];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$out_trade_no = $this->resolve_out_trade_no($order_number);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
YansongdaPayFactory::boot_alipay();
|
YansongdaPayFactory::boot_alipay();
|
||||||
$result = Pay::alipay()->query([
|
$result = Pay::alipay()->query([
|
||||||
'out_trade_no' => $order_number,
|
'out_trade_no' => $out_trade_no,
|
||||||
]);
|
]);
|
||||||
$data = YansongdaPayFactory::to_array($result);
|
$data = YansongdaPayFactory::to_array($result);
|
||||||
|
|
||||||
@@ -157,7 +168,8 @@ class AlipayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'order_number' => (string) ($data['out_trade_no'] ?? $order_number),
|
// Keep provider out_trade_no for Gateway::complete_order resolve.
|
||||||
|
'order_number' => (string) ($data['out_trade_no'] ?? $out_trade_no),
|
||||||
'amount' => (float) ($data['total_amount'] ?? 0),
|
'amount' => (float) ($data['total_amount'] ?? 0),
|
||||||
'transaction_id' => (string) ($data['trade_no'] ?? ''),
|
'transaction_id' => (string) ($data['trade_no'] ?? ''),
|
||||||
'status' => $status,
|
'status' => $status,
|
||||||
@@ -167,6 +179,25 @@ class AlipayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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, float $amount, string $request_id = ''): array {
|
public function refund(string $order_number, float $amount, string $request_id = ''): array {
|
||||||
if ($amount <= 0) {
|
if ($amount <= 0) {
|
||||||
return ['success' => false, 'status' => 'failed', 'message' => __('退款金额无效', 'aksrt')];
|
return ['success' => false, 'status' => 'failed', 'message' => __('退款金额无效', 'aksrt')];
|
||||||
@@ -175,6 +206,8 @@ class AlipayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
return ['success' => false, 'message' => __('支付宝未配置', 'aksrt')];
|
return ['success' => false, 'message' => __('支付宝未配置', 'aksrt')];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$out_trade_no = $this->resolve_out_trade_no($order_number);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
YansongdaPayFactory::boot_alipay();
|
YansongdaPayFactory::boot_alipay();
|
||||||
$refund_request_id = (string) preg_replace(
|
$refund_request_id = (string) preg_replace(
|
||||||
@@ -183,7 +216,7 @@ class AlipayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
$request_id !== '' ? $request_id : $order_number . '_refund'
|
$request_id !== '' ? $request_id : $order_number . '_refund'
|
||||||
);
|
);
|
||||||
$result = Pay::alipay()->refund([
|
$result = Pay::alipay()->refund([
|
||||||
'out_trade_no' => $order_number,
|
'out_trade_no' => $out_trade_no,
|
||||||
'refund_amount' => number_format($amount, 2, '.', ''),
|
'refund_amount' => number_format($amount, 2, '.', ''),
|
||||||
'refund_reason' => __('用户申请退款', 'aksrt'),
|
'refund_reason' => __('用户申请退款', 'aksrt'),
|
||||||
'out_request_no' => $refund_request_id,
|
'out_request_no' => $refund_request_id,
|
||||||
@@ -216,7 +249,7 @@ class AlipayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
YansongdaPayFactory::boot_alipay();
|
YansongdaPayFactory::boot_alipay();
|
||||||
$params = ['out_request_no' => $request_id, '_action' => 'refund'];
|
$params = ['out_request_no' => $request_id, '_action' => 'refund'];
|
||||||
if ($order_number !== '') {
|
if ($order_number !== '') {
|
||||||
$params['out_trade_no'] = $order_number;
|
$params['out_trade_no'] = $this->resolve_out_trade_no($order_number);
|
||||||
}
|
}
|
||||||
$result = Pay::alipay()->query($params);
|
$result = Pay::alipay()->query($params);
|
||||||
$data = YansongdaPayFactory::to_array($result);
|
$data = YansongdaPayFactory::to_array($result);
|
||||||
@@ -255,4 +288,17 @@ class AlipayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
'payment_type' => $type,
|
'payment_type' => $type,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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')) {
|
||||||
|
$order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number);
|
||||||
|
if ($order) {
|
||||||
|
return \AKSRT\Payment\OrderRepository::provider_out_trade_no($order);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $order_number;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -44,9 +44,15 @@ class EpayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$order_number = (string) ($order['order_number'] ?? '');
|
$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 = (float) ($order['amount'] ?? 0);
|
$amount = (float) ($order['amount'] ?? 0);
|
||||||
$subject = (string) ($order['subject'] ?? __('订单支付', 'aksrt'));
|
$subject = (string) ($order['subject'] ?? __('订单支付', 'aksrt'));
|
||||||
if ($order_number === '' || $amount <= 0) {
|
if ($order_number === '' || $out_trade_no === '' || $amount <= 0) {
|
||||||
return ['success' => false, 'message' => __('订单参数无效', 'aksrt')];
|
return ['success' => false, 'message' => __('订单参数无效', 'aksrt')];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +60,7 @@ class EpayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
$params = [
|
$params = [
|
||||||
'pid' => $config['pid'],
|
'pid' => $config['pid'],
|
||||||
'type' => $this->channel,
|
'type' => $this->channel,
|
||||||
'out_trade_no' => $order_number,
|
'out_trade_no' => $out_trade_no,
|
||||||
'notify_url' => rest_url('aksrt/v1/payment/' . $this->get_id() . '/notify'),
|
'notify_url' => rest_url('aksrt/v1/payment/' . $this->get_id() . '/notify'),
|
||||||
'return_url' => rest_url('aksrt/v1/payment/' . $this->get_id() . '/return'),
|
'return_url' => rest_url('aksrt/v1/payment/' . $this->get_id() . '/return'),
|
||||||
'name' => mb_substr(wp_strip_all_tags($subject), 0, 127),
|
'name' => mb_substr(wp_strip_all_tags($subject), 0, 127),
|
||||||
@@ -78,7 +84,7 @@ class EpayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
$body = json_decode((string) wp_remote_retrieve_body($response), true);
|
$body = json_decode((string) wp_remote_retrieve_body($response), true);
|
||||||
if (!is_array($body)) {
|
if (!is_array($body)) {
|
||||||
// Fall back to browser submit if mapi is unavailable.
|
// Fall back to browser submit if mapi is unavailable.
|
||||||
return $this->browser_submit_result($config['apiurl'], $params, $order_number);
|
return $this->browser_submit_result($config['apiurl'], $params, $order_number, $out_trade_no);
|
||||||
}
|
}
|
||||||
$code = (int) ($body['code'] ?? 0);
|
$code = (int) ($body['code'] ?? 0);
|
||||||
if ($code !== 1) {
|
if ($code !== 1) {
|
||||||
@@ -94,6 +100,7 @@ class EpayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
'success' => true,
|
'success' => true,
|
||||||
'payment_data' => [
|
'payment_data' => [
|
||||||
'order_number' => $order_number,
|
'order_number' => $order_number,
|
||||||
|
'out_trade_no' => $out_trade_no,
|
||||||
'code_url' => $qrcode,
|
'code_url' => $qrcode,
|
||||||
'channel' => $this->channel,
|
'channel' => $this->channel,
|
||||||
],
|
],
|
||||||
@@ -107,7 +114,7 @@ class EpayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->browser_submit_result($config['apiurl'], $params, $order_number);
|
return $this->browser_submit_result($config['apiurl'], $params, $order_number, $out_trade_no);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function verify_callback(\WP_REST_Request $request): array {
|
public function verify_callback(\WP_REST_Request $request): array {
|
||||||
@@ -177,13 +184,14 @@ class EpayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
return ['success' => false, 'message' => __('易支付未配置', 'aksrt')];
|
return ['success' => false, 'message' => __('易支付未配置', 'aksrt')];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$out_trade_no = $this->resolve_out_trade_no($order_number);
|
||||||
$config = $this->get_config();
|
$config = $this->get_config();
|
||||||
// Common compatible query: api.php?act=order&pid&key&out_trade_no
|
// Common compatible query: api.php?act=order&pid&key&out_trade_no
|
||||||
$url = add_query_arg([
|
$url = add_query_arg([
|
||||||
'act' => 'order',
|
'act' => 'order',
|
||||||
'pid' => $config['pid'],
|
'pid' => $config['pid'],
|
||||||
'key' => $config['key'],
|
'key' => $config['key'],
|
||||||
'out_trade_no' => $order_number,
|
'out_trade_no' => $out_trade_no,
|
||||||
], $this->endpoint($config['apiurl'], 'api.php'));
|
], $this->endpoint($config['apiurl'], 'api.php'));
|
||||||
|
|
||||||
$response = wp_remote_get($url, ['timeout' => 20]);
|
$response = wp_remote_get($url, ['timeout' => 20]);
|
||||||
@@ -205,7 +213,7 @@ class EpayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'order_number' => (string) ($body['out_trade_no'] ?? $order_number),
|
'order_number' => (string) ($body['out_trade_no'] ?? $out_trade_no),
|
||||||
'amount' => $amount,
|
'amount' => $amount,
|
||||||
'transaction_id' => $tx,
|
'transaction_id' => $tx,
|
||||||
'status' => $paid ? 'paid' : 'pending',
|
'status' => $paid ? 'paid' : 'pending',
|
||||||
@@ -297,16 +305,34 @@ class EpayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
* @param array<string,string> $params
|
* @param array<string,string> $params
|
||||||
* @return array{success:bool,payment_url?:string,payment_data?:array,message?:string}
|
* @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 {
|
/**
|
||||||
|
* @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'));
|
$url = add_query_arg($params, $this->endpoint($apiurl, 'submit.php'));
|
||||||
return [
|
return [
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'payment_url' => $url,
|
'payment_url' => $url,
|
||||||
'payment_data' => [
|
'payment_data' => [
|
||||||
'order_number' => $order_number,
|
'order_number' => $order_number,
|
||||||
|
'out_trade_no' => $out_trade_no !== '' ? $out_trade_no : $order_number,
|
||||||
'channel' => $this->channel,
|
'channel' => $this->channel,
|
||||||
],
|
],
|
||||||
'message' => __('易支付支付链接已生成', 'aksrt'),
|
'message' => __('易支付支付链接已生成', 'aksrt'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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')) {
|
||||||
|
$order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number);
|
||||||
|
if ($order) {
|
||||||
|
return \AKSRT\Payment\OrderRepository::provider_out_trade_no($order);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $order_number;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -40,12 +40,19 @@ class WechatPayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$order_number = (string) ($order['order_number'] ?? '');
|
$order_number = (string) ($order['order_number'] ?? '');
|
||||||
|
// Prefer attempt-specific out_trade_no so re-pay does not reuse a frozen provider order.
|
||||||
|
$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 = (float) ($order['amount'] ?? 0);
|
$amount = (float) ($order['amount'] ?? 0);
|
||||||
$subject = (string) ($order['subject'] ?? __('订单支付', 'aksrt'));
|
$subject = (string) ($order['subject'] ?? __('订单支付', 'aksrt'));
|
||||||
$payment_type = $this->get_config()['payment_type'];
|
$payment_type = $this->get_config()['payment_type'];
|
||||||
$total_fen = (int) round($amount * 100);
|
$total_fen = (int) round($amount * 100);
|
||||||
|
|
||||||
if ($order_number === '' || $total_fen <= 0) {
|
if ($order_number === '' || $out_trade_no === '' || $total_fen <= 0) {
|
||||||
return ['success' => false, 'message' => __('订单参数无效', 'aksrt')];
|
return ['success' => false, 'message' => __('订单参数无效', 'aksrt')];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +60,7 @@ class WechatPayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
YansongdaPayFactory::boot_wechat();
|
YansongdaPayFactory::boot_wechat();
|
||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
'out_trade_no' => $order_number,
|
'out_trade_no' => $out_trade_no,
|
||||||
'description' => mb_substr($subject, 0, 127),
|
'description' => mb_substr($subject, 0, 127),
|
||||||
'amount' => [
|
'amount' => [
|
||||||
'total' => $total_fen,
|
'total' => $total_fen,
|
||||||
@@ -76,7 +83,10 @@ class WechatPayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$data = YansongdaPayFactory::to_array($result);
|
$data = YansongdaPayFactory::to_array($result);
|
||||||
$payment_data = array_merge($data, ['order_number' => $order_number]);
|
$payment_data = array_merge($data, [
|
||||||
|
'order_number' => $order_number,
|
||||||
|
'out_trade_no' => $out_trade_no,
|
||||||
|
]);
|
||||||
|
|
||||||
$result_data = [
|
$result_data = [
|
||||||
'success' => true,
|
'success' => true,
|
||||||
@@ -206,11 +216,13 @@ class WechatPayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
return ['success' => false, 'message' => __('微信支付未配置', 'aksrt')];
|
return ['success' => false, 'message' => __('微信支付未配置', 'aksrt')];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$out_trade_no = $this->resolve_out_trade_no($order_number);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
YansongdaPayFactory::boot_wechat();
|
YansongdaPayFactory::boot_wechat();
|
||||||
$action = $this->query_action();
|
$action = $this->query_action();
|
||||||
$result = Pay::wechat()->query([
|
$result = Pay::wechat()->query([
|
||||||
'out_trade_no' => $order_number,
|
'out_trade_no' => $out_trade_no,
|
||||||
'_action' => $action,
|
'_action' => $action,
|
||||||
]);
|
]);
|
||||||
$data = YansongdaPayFactory::to_array($result);
|
$data = YansongdaPayFactory::to_array($result);
|
||||||
@@ -227,7 +239,8 @@ class WechatPayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'order_number' => (string) ($data['out_trade_no'] ?? $order_number),
|
// Keep provider out_trade_no so Gateway::complete_order can resolve the local order.
|
||||||
|
'order_number' => (string) ($data['out_trade_no'] ?? $out_trade_no),
|
||||||
'amount' => ((int) YansongdaPayFactory::dig($data, 'amount.total', 0)) / 100,
|
'amount' => ((int) YansongdaPayFactory::dig($data, 'amount.total', 0)) / 100,
|
||||||
'transaction_id' => (string) ($data['transaction_id'] ?? ''),
|
'transaction_id' => (string) ($data['transaction_id'] ?? ''),
|
||||||
'status' => $status,
|
'status' => $status,
|
||||||
@@ -237,17 +250,38 @@ class WechatPayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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_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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function refund(string $order_number, float $amount, string $request_id = ''): array {
|
public function refund(string $order_number, float $amount, string $request_id = ''): array {
|
||||||
if (!$this->is_configured()) {
|
if (!$this->is_configured()) {
|
||||||
return ['success' => false, 'message' => __('微信支付未配置', 'aksrt')];
|
return ['success' => false, 'message' => __('微信支付未配置', 'aksrt')];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$out_trade_no = $this->resolve_out_trade_no($order_number);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
YansongdaPayFactory::boot_wechat();
|
YansongdaPayFactory::boot_wechat();
|
||||||
$action = $this->query_action();
|
$action = $this->query_action();
|
||||||
|
|
||||||
$query = Pay::wechat()->query([
|
$query = Pay::wechat()->query([
|
||||||
'out_trade_no' => $order_number,
|
'out_trade_no' => $out_trade_no,
|
||||||
'_action' => $action,
|
'_action' => $action,
|
||||||
]);
|
]);
|
||||||
$q = YansongdaPayFactory::to_array($query);
|
$q = YansongdaPayFactory::to_array($query);
|
||||||
@@ -267,7 +301,7 @@ class WechatPayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
);
|
);
|
||||||
$result = Pay::wechat()->refund([
|
$result = Pay::wechat()->refund([
|
||||||
'_action' => 'refund_' . $action,
|
'_action' => 'refund_' . $action,
|
||||||
'out_trade_no' => $order_number,
|
'out_trade_no' => $out_trade_no,
|
||||||
'out_refund_no' => $refund_request_id,
|
'out_refund_no' => $refund_request_id,
|
||||||
'reason' => __('用户申请退款', 'aksrt'),
|
'reason' => __('用户申请退款', 'aksrt'),
|
||||||
'amount' => [
|
'amount' => [
|
||||||
@@ -350,4 +384,17 @@ class WechatPayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
$type = $this->get_config()['payment_type'];
|
$type = $this->get_config()['payment_type'];
|
||||||
return $map[$type] ?? 'native';
|
return $map[$type] ?? 'native';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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')) {
|
||||||
|
$order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number);
|
||||||
|
if ($order) {
|
||||||
|
return \AKSRT\Payment\OrderRepository::provider_out_trade_no($order);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $order_number;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -510,4 +510,135 @@ trait OrderRepositoryCore {
|
|||||||
return 'AKS' . date('YmdHis') . str_pad((string) random_int(0, 9999), 4, '0', STR_PAD_LEFT);
|
return 'AKS' . date('YmdHis') . str_pad((string) random_int(0, 9999), 4, '0', STR_PAD_LEFT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge keys into the order note JSON payload (meta_payload).
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $patch
|
||||||
|
*/
|
||||||
|
public function merge_meta(string $order_number, array $patch): bool {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$order = $this->get($order_number);
|
||||||
|
if (!$order) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : [];
|
||||||
|
foreach ($patch as $key => $value) {
|
||||||
|
$meta[(string) $key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$encoded = wp_json_encode($meta, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
if (!is_string($encoded)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$updated = $wpdb->update(
|
||||||
|
$this->table(),
|
||||||
|
['note' => $encoded],
|
||||||
|
['order_number' => $order_number],
|
||||||
|
['%s'],
|
||||||
|
['%s']
|
||||||
|
);
|
||||||
|
|
||||||
|
return $updated !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a local order from a provider out_trade_no (base or attempt-suffixed).
|
||||||
|
*/
|
||||||
|
public function resolve_by_out_trade_no(string $out_trade_no): ?array {
|
||||||
|
$out_trade_no = sanitize_text_field($out_trade_no);
|
||||||
|
if ($out_trade_no === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$direct = $this->get($out_trade_no);
|
||||||
|
if ($direct) {
|
||||||
|
return $direct;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resume attempts: {order_number}P{n}
|
||||||
|
if (preg_match('/^(AKS[0-9]{10,})P([0-9]{1,3})$/', $out_trade_no, $m)) {
|
||||||
|
$base = $this->get($m[1]);
|
||||||
|
if ($base) {
|
||||||
|
return $base;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider out_trade_no currently bound to this order (falls back to order_number).
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $order
|
||||||
|
*/
|
||||||
|
public static function provider_out_trade_no(array $order): string {
|
||||||
|
$meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : [];
|
||||||
|
$out = sanitize_text_field((string) ($meta['provider_out_trade_no'] ?? ''));
|
||||||
|
if ($out !== '') {
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
return sanitize_text_field((string) ($order['order_number'] ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allocate a new provider out_trade_no for a payment attempt.
|
||||||
|
* First attempt uses bare order_number; later attempts append Pn (WeChat limit 32).
|
||||||
|
*
|
||||||
|
* @return array{out_trade_no:string,attempt:int,previous:string}|null
|
||||||
|
*/
|
||||||
|
public function allocate_payment_attempt(string $order_number): ?array {
|
||||||
|
$order = $this->get($order_number);
|
||||||
|
if (!$order) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : [];
|
||||||
|
$previous = sanitize_text_field((string) ($meta['provider_out_trade_no'] ?? ''));
|
||||||
|
$history = is_array($meta['provider_out_trade_nos'] ?? null) ? $meta['provider_out_trade_nos'] : [];
|
||||||
|
|
||||||
|
// First provider submission keeps the bare order number.
|
||||||
|
// Re-pay (provider_out_trade_no already set) always mints a new suffix so
|
||||||
|
// WeChat/Alipay/Epay accept the order when amount/subject/notify changed.
|
||||||
|
if ($previous === '') {
|
||||||
|
$attempt = 1;
|
||||||
|
$out = $order_number;
|
||||||
|
} else {
|
||||||
|
$attempt = max(1, (int) ($meta['pay_attempt'] ?? 1)) + 1;
|
||||||
|
$suffix = 'P' . $attempt;
|
||||||
|
$base = $order_number;
|
||||||
|
if (strlen($base) + strlen($suffix) > 32) {
|
||||||
|
$base = substr($base, 0, 32 - strlen($suffix));
|
||||||
|
}
|
||||||
|
$out = $base . $suffix;
|
||||||
|
if ($previous !== '' && !in_array($previous, $history, true)) {
|
||||||
|
$history[] = $previous;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!in_array($out, $history, true)) {
|
||||||
|
$history[] = $out;
|
||||||
|
}
|
||||||
|
if (count($history) > 20) {
|
||||||
|
$history = array_slice($history, -20);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ok = $this->merge_meta($order_number, [
|
||||||
|
'pay_attempt' => $attempt,
|
||||||
|
'provider_out_trade_no' => $out,
|
||||||
|
'provider_out_trade_nos' => array_values($history),
|
||||||
|
]);
|
||||||
|
if (!$ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'out_trade_no' => $out,
|
||||||
|
'attempt' => $attempt,
|
||||||
|
'previous' => $previous,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -337,6 +337,26 @@ class UserOrders {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$order['subject'] = $order['subject'] ?? sprintf(__('订单 %s', 'aksrt'), $order_number);
|
$order['subject'] = $order['subject'] ?? sprintf(__('订单 %s', 'aksrt'), $order_number);
|
||||||
|
|
||||||
|
$needs_attempt = !in_array($payment_method, ['balance', 'points'], true);
|
||||||
|
if ($needs_attempt) {
|
||||||
|
$attempt = $repo->allocate_payment_attempt($order_number);
|
||||||
|
if ($attempt === null) {
|
||||||
|
return ['success' => false, 'message' => __('无法生成支付单号,请稍后重试。', 'aksrt')];
|
||||||
|
}
|
||||||
|
$order = $repo->get_user_order($user_id, $order_number) ?: $order;
|
||||||
|
$order['out_trade_no'] = (string) $attempt['out_trade_no'];
|
||||||
|
if (!empty($attempt['previous'])
|
||||||
|
&& $attempt['previous'] !== $attempt['out_trade_no']
|
||||||
|
&& method_exists($gateway, 'close_trade')) {
|
||||||
|
try {
|
||||||
|
$gateway->close_trade((string) $attempt['previous']);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// best-effort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$payment_result = $gateway->create_payment($order);
|
$payment_result = $gateway->create_payment($order);
|
||||||
if (empty($payment_result['success'])) {
|
if (empty($payment_result['success'])) {
|
||||||
return ['success' => false, 'message' => $payment_result['message'] ?? __('支付失败。', 'aksrt')];
|
return ['success' => false, 'message' => $payment_result['message'] ?? __('支付失败。', 'aksrt')];
|
||||||
|
|||||||
@@ -226,6 +226,28 @@ class CheckoutService {
|
|||||||
return $this->pay_with_points($user_id, $order_number, $order);
|
return $this->pay_with_points($user_id, $order_number, $order);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// External gateways: allocate a fresh out_trade_no so re-pay works when
|
||||||
|
// the provider rejects "same order number with changed params".
|
||||||
|
$needs_attempt = !in_array($payment_method, ['balance', 'points'], true);
|
||||||
|
if ($needs_attempt && class_exists('\AKSRT\Payment\OrderRepository')) {
|
||||||
|
$attempt = $repo->allocate_payment_attempt($order_number);
|
||||||
|
if ($attempt === null) {
|
||||||
|
return ['success' => false, 'message' => __('无法生成支付单号,请稍后重试。', 'aksrt')];
|
||||||
|
}
|
||||||
|
$order = $repo->get_user_order($user_id, $order_number) ?: $order;
|
||||||
|
$order['out_trade_no'] = (string) $attempt['out_trade_no'];
|
||||||
|
// Close previous provider trade when possible (WeChat/Alipay) so the new attempt is clean.
|
||||||
|
if (!empty($attempt['previous'])
|
||||||
|
&& $attempt['previous'] !== $attempt['out_trade_no']
|
||||||
|
&& method_exists($gateway, 'close_trade')) {
|
||||||
|
try {
|
||||||
|
$gateway->close_trade((string) $attempt['previous']);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// best-effort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$payment_result = $gateway->create_payment($order);
|
$payment_result = $gateway->create_payment($order);
|
||||||
if (empty($payment_result['success'])) {
|
if (empty($payment_result['success'])) {
|
||||||
return ['success' => false, 'message' => $payment_result['message'] ?? __('支付失败。', 'aksrt')];
|
return ['success' => false, 'message' => $payment_result['message'] ?? __('支付失败。', 'aksrt')];
|
||||||
|
|||||||
Reference in new issue
Block a user