config/services.php(env) */ class WangpuPayService { /** 网关应答成功码 */ public const string CODE_SUCCESS = '0000'; /** 通知应答码:成功(网关收到 {"code":"00"} 才认为通知成功) */ public const string NOTIFY_ACK_OK = '00'; /** 订单状态:支付成功(交易查询 / 后台通知的 order_status) */ public const int ORDER_STATUS_PAID = 1; /** * 统一下单B-JSAPI(微信公众号/小程序、支付宝服务窗/生活号、银联二维码) * * @param array $params 业务参数(mer_order_id/order_amt/open_id/notifyurl 等) * @return array 网关 data 节点(含 order_id/trade_no/调起支付参数) * @throws RepositoryException 网关应答失败 */ public function createOrder(array $params): array { return $this->request('/industrial/payment/order', array_merge([ 'mer_code' => $this->config('mer_code'), 'term_code' => $this->config('term_code'), 'payway_code' => $this->config('payway_code'), ], $params)); } /** * 交易查询(单笔订单支付状态同步) * * @param string $merOrderId 商户唯一订单号(本系统 payment_no) * @param string $orderId 旺铺订单号(可选,与 mer_order_id 同时上送时网关以 order_id 为准) * @return array 网关 data 节点 * @throws RepositoryException 网关应答失败 */ public function queryOrder(string $merOrderId, string $orderId = ''): array { $params = ['mer_order_id' => $merOrderId]; if ($orderId !== '') { $params['order_id'] = $orderId; } return $this->request('/industrial/query/order', $params); } /** * 校验后台通知签名(支付成功/退款成功通知通用) * * @param array $params 通知报文全量参数(含 sign) */ public function verifyNotifySign(array $params): bool { $sign = (string) ($params['sign'] ?? ''); if ($sign === '') { return false; } return hash_equals($this->sign($params), strtoupper($sign)); } /** * MD5 加签:非空参数(sign 除外)按参数名 ASCII 升序 key=value 以 & 拼接, * 末尾拼接 &key={SignKey} 后 MD5 转大写 * * @param array $params */ public function sign(array $params): string { unset($params['sign']); $pairs = []; foreach ($params as $key => $value) { if ($value === null || $value === '') { continue; } if (is_array($value) || is_object($value)) { $value = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } $pairs[(string) $key] = (string) $key . '=' . (string) $value; } ksort($pairs, SORT_STRING); $signStr = implode('&', $pairs) . '&key=' . $this->config('sign_key'); return strtoupper(md5($signStr)); } /** * 支付结果后台通知地址(统一下单时上送的 notifyurl) */ public function notifyUrl(): string { return rtrim((string) config('app.url'), '/') . '/mini/payment/notify'; } /** * 网关 POST 请求(表单方式):公共参数 + 加签 → 发送 → 校验应答码 * * @param array $params 接口业务参数 * @return array 应答 data 节点 * @throws RepositoryException 通讯失败或应答码非成功 */ protected function request(string $path, array $params): array { $baseUrl = rtrim($this->config('base_url'), '/'); if ($baseUrl === '') { throw new RepositoryException('旺铺支付未配置网关地址,请联系管理员'); } $body = array_merge([ 'organiz_no' => $this->config('organiz_no'), 'mer_no' => $this->config('mer_no'), ], $params); // 空值参数不下送(与网关签名口径一致) $body = array_filter($body, static fn ($value): bool => $value !== null && $value !== ''); $body['sign'] = $this->sign($body); $response = Http::asForm()->timeout(15)->post($baseUrl . $path, $body); $result = $response->json(); if (! is_array($result)) { Log::error('旺铺网关应答异常', ['path' => $path, 'status' => $response->status(), 'body' => $response->body()]); throw new RepositoryException('支付网关通讯异常,请稍后重试'); } if (($result['code'] ?? '') !== self::CODE_SUCCESS) { Log::warning('旺铺网关应答失败', ['path' => $path, 'response' => $result]); throw new RepositoryException('支付网关下单失败:' . ($result['msg'] ?? '未知错误')); } $data = $result['data'] ?? []; return is_array($data) ? $data : []; } /** * 读取网关配置:后台站点配置优先,为空回退 config/services.php(env) */ protected function config(string $key): string { $value = site_config('pay.wangpu_' . $key, ''); if ($value === null || $value === '') { $value = config('services.wangpu.' . $key, ''); } return trim((string) $value); } }