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'] ?? ''); $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); $subject = (string) ($order['subject'] ?? __('订单支付', 'aksrt')); if ($order_number === '' || $out_trade_no === '' || $amount <= 0) { return ['success' => false, 'message' => __('订单参数无效', 'aksrt')]; } $config = $this->get_config(); $params = [ 'pid' => $config['pid'], 'type' => $this->channel, 'out_trade_no' => $out_trade_no, '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, $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; } return $this->browser_submit_result($config['apiurl'], $params, $order_number, $out_trade_no); } 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')]; } $out_trade_no = $this->resolve_out_trade_no($order_number); $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' => $out_trade_no, ], $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'] ?? $out_trade_no), '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 $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 $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 $params * @return array{success:bool,payment_url?:string,payment_data?:array,message?:string} */ /** * @param array $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). */ 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; } }