getConfig(); } /** * 获取配置 * 优先读 pay_channel 表(和 AlinPay 一致的存储位置), * 回退到 config('pay_list.epay') */ private function getConfig() { $dbConfig = Db::name('pay_channel')->where('key', 'EPAY')->value('value'); if ($dbConfig) { $this->config = json_decode($dbConfig, true) ?: []; } else { $this->config = config('pay_list.epay') ?: []; } $this->apiUrl = $this->config['api_url'] ?? 'https://pay.g7g7.top'; } public function getConfigInfo() { return $this->config; } /** * 生成签名 * * @param array $params * @return string */ public function generateSign(array $params) { unset($params['sign'], $params['sign_type']); // 过滤空值(易支付官方要求) $params = array_filter($params, function ($val) { return $val !== '' && $val !== null; }); ksort($params); $parts = []; foreach ($params as $k => $v) { $parts[] = $k . '=' . $v; } $str = implode('&', $parts) . ($this->config['key'] ?? ''); return md5($str); } /** * 验证回调签名(时间安全比较) * * @param array $params * @return bool */ public function verifySign(array $params) { $received = $params['sign'] ?? ''; $calculated = $this->generateSign($params); return hash_equals($calculated, $received); } /** * 构造发起支付的 URL(跳转式 submit.php) * 前端收到该 URL 后直接 window.location 跳转即可 * * @param string $orderNo 商户订单号 * @param string $type 支付方式:alipay|wxpay|qqpay|bank|jdpay|usdt * @param string $amount 金额(元) * @param string $name 商品名称 * @param string $notifyUrl 异步通知地址 * @param string $returnUrl 同步跳转地址 * @param string $param 业务扩展参数(可选,原样回传) * @return string */ public function buildPayUrl($orderNo, $type, $amount, $name, $notifyUrl, $returnUrl = '', $param = '') { $params = [ 'pid' => strval($this->config['pid'] ?? ''), 'type' => $type, 'out_trade_no' => strval($orderNo), 'notify_url' => $notifyUrl, 'return_url' => $returnUrl, 'name' => $name, 'money' => strval($amount), 'param' => $param, 'sign_type' => 'MD5', ]; $params['sign'] = $this->generateSign($params); $params = array_filter($params, function ($v) { return $v !== '' && $v !== null; }); return rtrim($this->apiUrl, '/') . '/submit.php?' . http_build_query($params); } /** * API 模式下单(mapi.php) * * @param string $clientIp 下单用户的 IP(mapi.php 必填,否则报 "用户IP地址(clientip)不能为空") * @return array|null */ public function createOrderApi($orderNo, $type, $amount, $name, $notifyUrl, $returnUrl = '', $param = '', $clientIp = '') { if ($clientIp === '' || $clientIp === null) { $clientIp = \think\Request::instance()->ip(); } $params = [ 'pid' => strval($this->config['pid'] ?? ''), 'type' => $type, 'out_trade_no' => strval($orderNo), 'notify_url' => $notifyUrl, 'return_url' => $returnUrl, 'name' => $name, 'money' => strval($amount), 'param' => $param, 'clientip' => strval($clientIp), 'sign_type' => 'MD5', ]; $params['sign'] = $this->generateSign($params); $url = rtrim($this->apiUrl, '/') . '/mapi.php'; addLogToFile('epay API 下单请求:' . $url . ' params=' . json_encode($params, JSON_UNESCAPED_UNICODE), 'epay_request', 'epay'); $result = httpPost($url, $params); addLogToFile('epay API 下单返回:' . json_encode($result, JSON_UNESCAPED_UNICODE), 'epay_response', 'epay'); if (is_array($result)) return $result; if (is_string($result)) return json_decode($result, true); return null; } /** * 订单查询 */ public function queryOrder($orderNo) { $params = [ 'act' => 'order', 'pid' => strval($this->config['pid'] ?? ''), 'out_trade_no' => strval($orderNo), ]; $params['sign'] = $this->generateSign($params); $params['sign_type'] = 'MD5'; $url = rtrim($this->apiUrl, '/') . '/api.php?' . http_build_query($params); $resp = httpGet($url); return is_string($resp) ? json_decode($resp, true) : $resp; } /** * 处理异步回调 — 仅验签+解析状态,不操作余额 * 余额结算由 trait 内 epay_recharge_settle() 统一处理 * * @param array $data $_POST 或 $_GET * @param int $logId 日志 ID * @return array ['verified' => bool, 'success' => bool, 'data' => array] */ public function verifyNotifyCallback($data, $logId = 0) { if (!$this->verifySign($data)) { addLogToFile('epay 回调验签失败:' . json_encode($data, 320), 'epay_callback_error', 'epay'); return ['verified' => false, 'success' => false, 'data' => $data]; } $orderNo = $data['out_trade_no'] ?? ''; if ($logId && $orderNo) { Db::name('pay_callback_log')->where('id', $logId)->update(['unique_no' => $orderNo]); } $success = isset($data['trade_status']) && $data['trade_status'] === 'TRADE_SUCCESS'; addLogToFile('epay 回调验签通过:order=' . $orderNo . ' status=' . ($data['trade_status'] ?? ''), 'epay_callback', 'epay'); return ['verified' => true, 'success' => $success, 'data' => $data]; } }