Files
xin-procurement/app/Services/WangpuPayService.php
T
2026-09-01 11:07:00 +08:00

171 lines
6.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Services;
use App\Exceptions\RepositoryException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
/**
* 旺铺支付网关服务(统一下单B-JSAPI / 交易查询 / 后台通知验签)
*
* 加签规则(请求与回调一致):
* 除 sign 外所有非空参数按参数名 ASCII 码升序排列,以 key=value 形式用 & 拼接成 Str
* 在 Str 后拼接 &key={SignKey} 得到 SignStr,对其做 MD5(utf-8)并转大写即为签名值。
*
* 配置优先级:后台「系统设置 → 支付配置」(site_config pay.wangpu_*) > config/services.phpenv
*/
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<string, mixed> $params 业务参数(mer_order_id/order_amt/open_id/notifyurl 等)
* @return array<string, mixed> 网关 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<string, mixed> 网关 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<string, mixed> $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<string, mixed> $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<string, mixed> $params 接口业务参数
* @return array<string, mixed> 应答 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()->withOptions([
'verify' => false, // 禁用 SSL 证书验证
])->timeout(15)->post($baseUrl . $path, $body);
$result = $response->json();
if (! is_array($result)) {
Log::driver('pay')->error('旺铺网关应答异常', [
'path' => $path,
'status' => $response->status(),
'body' => $response->body(),
'request' => $body,
]);
throw new RepositoryException('支付网关通讯异常,请稍后重试');
}
if (($result['code'] ?? '') !== self::CODE_SUCCESS) {
Log::driver('pay')->warning('旺铺网关应答失败', [
'path' => $path,
'response' => $result,
'request' => $body,
]);
throw new RepositoryException('支付网关下单失败:' . ($result['msg'] ?? '未知错误'));
}
$data = $result['data'] ?? [];
return is_array($data) ? $data : [];
}
/**
* 读取网关配置:后台站点配置优先,为空回退 config/services.phpenv
*/
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);
}
}