'unified.trade.native', 'sign_type' => PaymentUtil::SIGN_TYPE_MD5, 'mch_id' => Env::get('payment.mchId'), 'out_trade_no' => $order['orderNo'], 'body' => $order['goodsName'] ?? '', 'total_fee' => $order['money'] ?? 0, 'mch_create_ip' => Request::ip(), 'notify_url' => Env::get('payment.notify_url', '127.0.0.1'), 'nonce_str' => PaymentUtil::generateNonceStr(), ]; Db::startTrans(); try { // 过滤空值签名 $secret = Env::get('payment.key'); $params['sign_type'] = 'MD5'; $params['sign'] = PaymentUtil::generateSign($params, $secret, 'MD5'); $url = Env::get('payment.url'); if (empty($url)) { throw new \Exception('支付网关地址未配置'); } //创建订单 Order::create([ 'mchId' => $params['mch_id'], 'companyId' => $order['companyId'], 'userId' => $order['userId'], 'orderType' => $order['orderType'] ?? 1, 'status' => 0, 'goodsId' => $order['goodsId'], 'goodsName' => $order['goodsName'], 'money' => $order['money'], 'orderNo' => $order['orderNo'], 'ip' => Request::ip(), 'nonceStr' => $params['nonce_str'], 'createTime' => time(), ]); // XML POST 请求 $xmlBody = $this->arrayToXml($params); $response = $this->postXml($url, $xmlBody); $parsed = $this->parseXmlOrRaw($response); if ($parsed['status'] == 0 && $parsed['result_code'] == 0) { Db::commit(); return json(['code' => 200, 'msg' => '订单创建成功', 'data' => $parsed['code_url']]); } else { Db::rollback(); return json(['code' => 500, 'msg' => '订单创建失败:' . $parsed['err_msg']]); } } catch (\Exception $e) { Db::rollback(); return json(['code' => 500, 'msg' => '订单创建失败:' . $e->getMessage()]); } } /** * POST 请求(x-www-form-urlencoded) */ protected function httpPost(string $url, array $params, array $headers = []) { if (!function_exists('requestCurl')) { throw new \RuntimeException('requestCurl 未定义'); } return requestCurl($url, $params, 'POST', $headers, 'dataBuild'); } /** * 解析响应 */ protected function parseResponse($response) { if ($response === '' || $response === null) { return ''; } $decoded = json_decode($response, true); if (json_last_error() === JSON_ERROR_NONE) { return $decoded; } if (strpos($response, '=') !== false && strpos($response, '&') !== false) { $arr = []; foreach (explode('&', $response) as $pair) { if ($pair === '') continue; $kv = explode('=', $pair, 2); $arr[$kv[0]] = $kv[1] ?? ''; } return $arr; } return $response; } /** * 以 XML 方式 POST(text/xml) */ protected function postXml(string $url, string $xml) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_TIMEOUT, 30); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: text/xml; charset=UTF-8' ]); curl_setopt($ch, CURLOPT_POSTFIELDS, $xml); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); $res = curl_exec($ch); curl_close($ch); return $res; } /** * 数组转 XML(按 ASCII 升序,字符串走 CDATA) */ protected function arrayToXml(array $data): string { // 过滤空值 $filtered = []; foreach ($data as $k => $v) { if ($v === '' || $v === null) continue; $filtered[$k] = $v; } ksort($filtered, SORT_STRING); $xml = ''; foreach ($filtered as $key => $value) { if (is_numeric($value)) { $xml .= "<{$key}>{$value}"; } else { $xml .= "<{$key}>"; } } $xml .= ''; return $xml; } /** * 解析 XML 响应 */ protected function parseXmlOrRaw($response) { if (!is_string($response) || $response === '') { return $response; } libxml_use_internal_errors(true); $xml = simplexml_load_string($response, 'SimpleXMLElement', LIBXML_NOCDATA); if ($xml !== false) { $json = json_encode($xml, JSON_UNESCAPED_UNICODE); return json_decode($json, true); } return $response; } /** * 支付结果异步通知 * - 威富通回调为 XML;需校验签名与业务字段并更新订单 * - 回应:成功回"success",失败回"fail" * @return void */ public function notify() { $rawBody = file_get_contents('php://input'); $payload = $this->parseXmlOrRaw($rawBody); if (!is_array($payload) || empty($payload)) { return json_encode(['code' => 500, 'msg' => 'XML解析错误']); } if ($payload['status'] != 0 || $payload['result_code'] != 0) { $errMsg = (isset($payload['err_msg']) ? $payload['err_msg'] : isset($payload['err_msg'])) ? $payload['err_msg'] : '未知错误'; return json_encode(['code' => 500, 'msg' => $errMsg]); } // 业务处理:更新订单 Db::startTrans(); try { $outTradeNo = $payload['out_trade_no']; $pay_result = $payload['pay_result']; $time_end = $payload['time_end']; $order = Order::where('orderNo', $outTradeNo)->find(); if (!$order) { Db::rollback(); return json_encode(['code' => 500, 'msg' => '该订单不存在']); } if ($pay_result != 0) { $order->payInfo = $payload['pay_info']; $order->payType = $payload['trade_type'] == 'pay.wechat.jspay' ? 1 : 2; $order->status = 3; $order->save(); Db::commit(); return json_encode(['code' => 500, 'msg' => $payload['pay_info']]); } $order->status = 1; $order->payTime = $this->parsePayTime($time_end); $order->save(); Db::commit(); return json_encode(['code' => 200, 'msg' => '付款成功']); } catch (\Exception $e) { Db::rollback(); return json_encode(['code' => 500, 'msg' => '付款失败' . $e->getMessage()]); } } /** * 解析威富通时间(yyyyMMddHHmmss)为时间戳 */ protected function parsePayTime(string $timeEnd) { if ($timeEnd === '') { return 0; } // 期望格式:20250102153045 if (preg_match('/^\\d{14}$/', $timeEnd) !== 1) { return 0; } $dt = \DateTime::createFromFormat('YmdHis', $timeEnd, new \DateTimeZone('Asia/Shanghai')); return $dt ? $dt->getTimestamp() : 0; } /** * 查询订单(威富通 unified.trade.query) * - 入参:商户订单号或平台交易号 * - 出参:统一 JSON 格式,包含交易状态与关键信息 * @param array $query * - out_trade_no: string 商户订单号(与 transaction_id 二选一) * - transaction_id: string 平台交易号(与 out_trade_no 二选一) * @return \think\response\Json */ public function queryOrder(array $query) { $outTradeNo = $query['out_trade_no'] ?? ($query['orderNo'] ?? ''); $transactionId = $query['transaction_id'] ?? ''; if ($outTradeNo === '' && $transactionId === '') { return json(['code' => 422, 'msg' => '缺少查询参数:out_trade_no 或 transaction_id']); } $params = [ 'service' => 'unified.trade.query', 'mch_id' => Env::get('payment.mchId'), 'out_trade_no' => $outTradeNo ?: null, 'nonce_str' => PaymentUtil::generateNonceStr(), 'sign_type' => 'MD5', ]; // 过滤空值后签名 $secret = Env::get('payment.key'); if (empty($secret)) { return json(['code' => 500, 'msg' => '支付密钥未配置']); } $filtered = []; foreach ($params as $k => $v) { if ($v === '' || $v === null) continue; $filtered[$k] = $v; } $filtered['sign'] = PaymentUtil::generateSign($filtered, $secret, $filtered['sign_type']); $url = Env::get('payment.url'); if (empty($url)) { return json(['code' => 500, 'msg' => '支付网关地址未配置']); } // 请求网关 $xmlBody = $this->arrayToXml($filtered); $response = $this->postXml($url, $xmlBody); $parsed = $this->parseXmlOrRaw($response); if (!is_array($parsed)) { return json(['code' => 500, 'msg' => '响应解析失败', 'data' => $response]); } if (($parsed['status'] ?? '') !== '0') { return json(['code' => 500, 'msg' => '通信失败:' . ($parsed['message'] ?? 'unknown')]); } if (($parsed['result_code'] ?? '') !== '0') { return json(['code' => 200, 'msg' => '业务失败', 'data' => [ 'err_code' => $parsed['err_code'] ?? '', 'err_msg' => $parsed['err_msg'] ?? '', ]]); } $tradeState = $parsed['trade_state'] ?? ''; $resp = [ 'trade_state' => $tradeState, 'trade_state_desc' => $parsed['trade_state_desc'] ?? '', 'transaction_id' => $parsed['transaction_id'] ?? '', 'out_trade_no' => $parsed['out_trade_no'] ?? $outTradeNo, 'total_fee' => isset($parsed['total_fee']) ? (int)$parsed['total_fee'] : null, 'time_end' => $parsed['time_end'] ?? '', 'buyer_logon_id' => $parsed['buyer_logon_id'] ?? '', 'bank_type' => $parsed['bank_type'] ?? '', ]; // 若已支付,同步本地订单 if ($tradeState === 'SUCCESS' && ($resp['out_trade_no'] ?? '') !== '') { Db::startTrans(); try { /** @var Order|null $order */ $order = Order::where('orderNo', $resp['out_trade_no'])->lock(true)->find(); if ($order) { $paidAt = $this->parsePayTime($resp['time_end'] ?? '') ?: time(); if ((int)$order['status'] !== 1) { $order->save([ 'status' => 1, 'transactionId' => $resp['transaction_id'] ?? '', 'payTime' => $paidAt, 'updateTime' => time(), ]); } } Db::commit(); } catch (\Throwable $e) { Db::rollback(); Log::error('[SwiftPass][query] update order exception: ' . $e->getMessage()); } } return json(['code' => 200, 'msg' => '查询成功', 'data' => $resp]); } }