get_config(); return $c['mch_id'] !== '' && $c['appid'] !== '' && $c['api_v3_key'] !== '' && $c['private_key'] !== '' && $c['cert_path'] !== ''; } public function create_payment(array $order): array { if (!$this->is_configured()) { return ['success' => false, 'message' => __('微信支付未配置', 'aksrt')]; } $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; } $subject = (string) ($order['subject'] ?? __('订单支付', 'aksrt')); $payment_type = $this->get_config()['payment_type']; try { $total_fen = \AKSRT\Ledger\AssetAmount::balance(is_string($order['amount'] ?? null) ? trim($order['amount']) : '')->minor(); } catch (\InvalidArgumentException $e) { $total_fen = 0; } if ($order_number === '' || $out_trade_no === '' || $total_fen <= 0) { return ['success' => false, 'message' => __('订单参数无效', 'aksrt')]; } try { YansongdaPayFactory::boot_wechat(); $payload = [ 'out_trade_no' => $out_trade_no, 'description' => mb_substr($subject, 0, 127), 'amount' => [ 'total' => $total_fen, 'currency' => 'CNY', ], ]; switch ($payment_type) { case 'h5': $payload['scene_info'] = [ 'payer_client_ip' => (string) ($_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'), 'h5_info' => ['type' => 'Wap'], ]; $result = Pay::wechat()->h5($payload); break; case 'native': default: $result = Pay::wechat()->scan($payload); break; } $data = YansongdaPayFactory::to_array($result); $payment_data = array_merge($data, [ 'order_number' => $order_number, 'out_trade_no' => $out_trade_no, ]); $result_data = [ 'success' => true, 'payment_data' => $payment_data, '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()]; } } public function verify_callback(\WP_REST_Request $request): array { try { YansongdaPayFactory::boot_wechat(); // Pass raw body + headers so callback works under WP REST (php://input may be drained). $raw = $request->get_body(); $headers = [ 'Content-Type' => (string) ($request->get_header('content-type') ?: 'application/json'), 'Wechatpay-Serial' => (string) $request->get_header('wechatpay-serial'), 'Wechatpay-Timestamp' => (string) $request->get_header('wechatpay-timestamp'), 'Wechatpay-Nonce' => (string) $request->get_header('wechatpay-nonce'), 'Wechatpay-Signature' => (string) $request->get_header('wechatpay-signature'), ]; $callback = Pay::wechat()->callback([ 'body' => $raw !== '' ? $raw : file_get_contents('php://input'), 'headers' => $headers, ]); $data = YansongdaPayFactory::to_array($callback); // Decrypted resource is nested under resource (array) after CallbackPlugin. $resource = []; if (isset($data['resource']) && is_array($data['resource'])) { $resource = $data['resource']; // ciphertext decrypt may leave plaintext fields at resource root or under ciphertext key. if (isset($resource['ciphertext']) && is_array($resource['ciphertext'])) { $resource = $resource['ciphertext']; } } $order_number = (string) ($resource['out_trade_no'] ?? $data['out_trade_no'] ?? ''); $transaction_id = (string) ($resource['transaction_id'] ?? $data['transaction_id'] ?? ''); $trade_state = (string) ($resource['trade_state'] ?? $data['trade_state'] ?? ''); $event_type = (string) ($data['event_type'] ?? ''); $total_fen = (int) YansongdaPayFactory::dig($resource, 'amount.total', 0); if ($total_fen <= 0) { $total_fen = (int) YansongdaPayFactory::dig($data, 'amount.total', 0); } $paid = ($trade_state === 'SUCCESS') || ($event_type === 'TRANSACTION.SUCCESS'); // When event is pay success, resource has trade_state SUCCESS. if ($event_type !== '' && $event_type !== 'TRANSACTION.SUCCESS' && $trade_state !== 'SUCCESS') { $paid = false; } $config = $this->get_config(); $mch_id = (string) ($resource['mchid'] ?? $data['mchid'] ?? ''); $app_id = (string) ($resource['appid'] ?? $data['appid'] ?? ''); $currency = (string) YansongdaPayFactory::dig($resource, 'amount.currency', ''); if ($order_number === '' || ($paid && ( $mch_id === '' || $app_id === '' || $currency === '' || !hash_equals($config['mch_id'], $mch_id) || !hash_equals($config['appid'], $app_id) || $currency !== 'CNY' || $transaction_id === '' || $total_fen <= 0 )) || (!$paid && ( ($mch_id !== '' && !hash_equals($config['mch_id'], $mch_id)) || ($app_id !== '' && !hash_equals($config['appid'], $app_id)) || ($currency !== '' && $currency !== 'CNY') ))) { return ['success' => false, 'message' => __('微信支付回调身份或订单数据无效', 'aksrt')]; } return [ 'success' => true, 'order_number' => $order_number, 'amount' => $this->fen_decimal($total_fen), 'currency' => strtoupper((string) YansongdaPayFactory::dig($data, 'amount.currency', 'CNY')), 'transaction_id' => $transaction_id, 'status' => $paid ? 'paid' : 'pending', ]; } catch (\Throwable $e) { return ['success' => false, 'message' => $e->getMessage()]; } } /** * Output WeChat v3 notify ACK (JSON). */ public function output_callback_response(array $result): void { try { YansongdaPayFactory::boot_wechat(); if (!empty($result['success'])) { YansongdaPayFactory::emit_response(Pay::wechat()->success(), '{"code":"SUCCESS","message":"成功"}'); } } catch (\Throwable $e) { // fall through to fail body } if (!headers_sent()) { http_response_code(500); header('Content-Type: application/json; charset=utf-8'); } echo wp_json_encode([ 'code' => 'FAIL', 'message' => (string) ($result['message'] ?? 'FAIL'), ]); exit; } public function query_status(string $order_number): array { if (!$this->is_configured()) { return ['success' => false, 'message' => __('微信支付未配置', 'aksrt')]; } $out_trade_no = $this->resolve_out_trade_no($order_number); try { YansongdaPayFactory::boot_wechat(); $action = $this->query_action(); $result = Pay::wechat()->query([ 'out_trade_no' => $out_trade_no, '_action' => $action, ]); $data = YansongdaPayFactory::to_array($result); $trade_state = (string) ($data['trade_state'] ?? ''); $status = 'pending'; if ($trade_state === 'SUCCESS') { $status = 'paid'; } elseif (in_array($trade_state, ['CLOSED', 'REVOKED'], true)) { $status = 'cancelled'; } elseif ($trade_state === 'REFUND') { $status = 'refunded'; } return [ 'success' => true, // 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' => $this->fen_decimal((int) YansongdaPayFactory::dig($data, 'amount.total', 0)), 'currency' => strtoupper((string) YansongdaPayFactory::dig($data, 'amount.currency', 'CNY')), 'transaction_id' => (string) ($data['transaction_id'] ?? ''), 'status' => $status, ]; } catch (\Throwable $e) { return ['success' => false, 'message' => $e->getMessage()]; } } /** * Best-effort close of a previous provider trade before re-pay. */ public function close_trade(string $out_trade_no): void { $out_trade_no = sanitize_text_field($out_trade_no); if ($out_trade_no === '' || !$this->is_configured()) { return; } try { YansongdaPayFactory::boot_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, string $amount, string $request_id = ''): array { try { $refund_amount = \AKSRT\Ledger\AssetAmount::balance($amount); } catch (\InvalidArgumentException $e) { return ['success' => false, 'status' => 'failed', 'message' => __('退款金额无效', 'aksrt')]; } if ($refund_amount->minor() <= 0) { return ['success' => false, 'status' => 'failed', 'message' => __('退款金额无效', 'aksrt')]; } if (!$this->is_configured()) { return ['success' => false, 'message' => __('微信支付未配置', 'aksrt')]; } $out_trade_no = $this->resolve_out_trade_no($order_number); try { YansongdaPayFactory::boot_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); $refund_fen = $refund_amount->minor(); if ($total_fen <= 0) { return ['success' => false, 'status' => 'failed', 'message' => __('无法确认原订单金额', 'aksrt')]; } if ($refund_fen <= 0 || $refund_fen > $total_fen) { return ['success' => false, 'status' => 'failed', 'message' => __('退款金额无效', 'aksrt')]; } $refund_request_id = (string) preg_replace( '/[^A-Za-z0-9_-]/', '', $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'), 'amount' => [ 'refund' => $refund_fen, 'total' => $total_fen, 'currency' => 'CNY', ], ]); $data = YansongdaPayFactory::to_array($result); $status = strtoupper((string) ($data['status'] ?? '')); $normalized = $status === 'SUCCESS' ? 'succeeded' : (in_array($status, ['CLOSED', 'ABNORMAL'], true) ? 'failed' : 'pending'); return [ 'success' => true, 'status' => $normalized, 'refund_id' => (string) ($data['refund_id'] ?? $data['out_refund_no'] ?? ''), 'request_id' => $refund_request_id, 'message' => $normalized === 'succeeded' ? __('退款成功', 'aksrt') : __('退款请求已受理', 'aksrt'), ]; } catch (\Throwable $e) { return ['success' => false, 'message' => $e->getMessage()]; } } public function query_refund(string $request_id, string $order_number = ''): array { if (!$this->is_configured() || $request_id === '') { return ['success' => false, 'status' => 'failed', 'message' => __('微信退款查询参数无效', 'aksrt')]; } try { YansongdaPayFactory::boot_wechat(); $result = Pay::wechat()->query([ 'out_refund_no' => $request_id, '_action' => 'refund_' . $this->query_action(), ]); $data = YansongdaPayFactory::to_array($result); $status = strtoupper((string) ($data['status'] ?? '')); $normalized = $status === 'SUCCESS' ? 'succeeded' : (in_array($status, ['CLOSED', 'ABNORMAL'], true) ? 'failed' : 'pending'); return [ 'success' => true, 'status' => $normalized, 'refund_id' => (string) ($data['refund_id'] ?? $data['out_refund_no'] ?? ''), ]; } catch (\Throwable $e) { return ['success' => false, 'status' => 'unknown', 'message' => $e->getMessage()]; } } /** * @return array{mch_id:string,appid:string,api_v3_key:string,private_key:string,cert_path:string,payment_type:string} */ private function get_config(): array { $all = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : []; $type = sanitize_key((string) ($all['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, ]; } 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, max(0, $amount) )->decimal(); } /** * Map local order_number → provider out_trade_no (supports resume attempts). */ private function resolve_out_trade_no(string $order_number): string { if (class_exists('\AKSRT\Payment\OrderRepository')) { $repo = \AKSRT\Payment\OrderRepository::get_instance(); if ($repo->resolve_by_out_trade_no($order_number) && !$repo->get($order_number)) { return $order_number; } $order = $repo->get($order_number); if ($order) { return \AKSRT\Payment\OrderRepository::provider_out_trade_no($order); } } return $order_number; } }