'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() { Log::info('支付结果异步通知'); $rawBody = file_get_contents('php://input'); Log::info('[SwiftPass][notify] raw: ' . $rawBody); $payload = $this->parseXmlOrRaw($rawBody); if (!is_array($payload) || empty($payload)) { Log::error('[SwiftPass][notify] parse fail'); echo 'fail'; return; } // 基础字段 $status = (string)($payload['status'] ?? ''); $resultCode = (string)($payload['result_code'] ?? ''); $outTradeNo = (string)($payload['out_trade_no'] ?? ''); $totalFee = (int)($payload['total_fee'] ?? 0); $mchIdNotify = (string)($payload['mch_id'] ?? ''); $signInNotify = (string)($payload['sign'] ?? ''); $signType = (string)($payload['sign_type'] ?? 'MD5'); if ($status !== '0' || $resultCode !== '0') { Log::warning('[SwiftPass][notify] business not success', $payload); echo 'fail'; return; } // 验签 $secret = Env::get('payment.key'); if (empty($secret)) { Log::error('[SwiftPass][notify] payment.key not configured'); echo 'fail'; return; } $verifyData = $payload; unset($verifyData['sign']); $calcSign = PaymentUtil::generateSign($verifyData, $secret, $signType ?: 'MD5'); if (strcasecmp($calcSign, $signInNotify) !== 0) { Log::error('[SwiftPass][notify] sign mismatch', [ 'calc' => $calcSign, 'recv' => $signInNotify, ]); echo 'fail'; return; } // 校验商户号 $mchIdConfig = Env::get('payment.mchId'); if (!empty($mchIdConfig) && $mchIdConfig !== $mchIdNotify) { Log::error('[SwiftPass][notify] mch_id mismatch', [ 'config' => $mchIdConfig, 'notify' => $mchIdNotify, ]); echo 'fail'; return; } // 业务处理:更新订单 Db::startTrans(); try { /** @var Order|null $order */ $order = Order::where('orderNo', $outTradeNo)->lock(true)->find(); if (!$order) { Db::rollback(); Log::error('[SwiftPass][notify] order not found', ['out_trade_no' => $outTradeNo]); echo 'fail'; return; } // 金额校验(单位:分) if ((int)$order['money'] !== $totalFee) { Db::rollback(); Log::error('[SwiftPass][notify] amount mismatch', [ 'order_money' => (int)$order['money'], 'notify_fee' => $totalFee, ]); echo 'fail'; return; } // 幂等:已支付直接返回成功 if ((int)$order['status'] === 1) { Db::commit(); echo 'success'; return; } $transactionId = (string)($payload['transaction_id'] ?? ''); $timeEndStr = (string)($payload['time_end'] ?? ''); $paidAt = $this->parsePayTime($timeEndStr) ?: time(); $order->save([ 'status' => 1, 'transactionId' => $transactionId, 'payTime' => $paidAt, 'updateTime' => time(), ]); Db::commit(); echo 'success'; return; } catch (\Throwable $e) { Db::rollback(); Log::error('[SwiftPass][notify] exception: ' . $e->getMessage()); echo 'fail'; return; } } /** * 解析威富通时间(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; } }