feat: update appraisal ordering and payment flows
This commit is contained in:
285
server-api/app/support/ShouqianbaClient.php
Normal file
285
server-api/app/support/ShouqianbaClient.php
Normal file
@@ -0,0 +1,285 @@
|
||||
<?php
|
||||
|
||||
namespace app\support;
|
||||
|
||||
class ShouqianbaClient
|
||||
{
|
||||
private const VERSION = '1.0.0';
|
||||
private const SIGN_TYPE = 'SHA256';
|
||||
private ?ShouqianbaConfigService $configService;
|
||||
|
||||
public function __construct(?ShouqianbaConfigService $configService = null)
|
||||
{
|
||||
$this->configService = $configService ?: new ShouqianbaConfigService();
|
||||
}
|
||||
|
||||
public function purchase(array $body): array
|
||||
{
|
||||
return $this->post('/api/lite-pos/v1/sales/purchase', $body);
|
||||
}
|
||||
|
||||
public function query(array $body): array
|
||||
{
|
||||
return $this->post('/api/lite-pos/v1/sales/query', $body);
|
||||
}
|
||||
|
||||
public function void(array $body): array
|
||||
{
|
||||
return $this->post('/api/lite-pos/v1/sales/void', $body);
|
||||
}
|
||||
|
||||
public function decodeNotification(string $rawBody): array
|
||||
{
|
||||
$payload = json_decode($rawBody, true);
|
||||
if (!is_array($payload)) {
|
||||
throw new \RuntimeException('收钱吧通知请求体格式错误');
|
||||
}
|
||||
|
||||
if (!$this->verifyEnvelopeRaw($rawBody, $payload, 'request') && !$this->verifyEnvelope($payload, 'request')) {
|
||||
throw new \RuntimeException('收钱吧通知验签失败');
|
||||
}
|
||||
|
||||
$body = $payload['request']['body'] ?? null;
|
||||
if (!is_array($body)) {
|
||||
throw new \RuntimeException('收钱吧通知业务参数为空');
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
public function signedResponse(string $resultCode = '200', string $bizResultCode = '200'): array
|
||||
{
|
||||
$config = $this->configService->assertReady();
|
||||
$response = [
|
||||
'head' => [
|
||||
'version' => self::VERSION,
|
||||
'sign_type' => self::SIGN_TYPE,
|
||||
'appid' => $config['appid'],
|
||||
'response_time' => $this->isoTime(),
|
||||
],
|
||||
'body' => [
|
||||
'result_code' => $resultCode,
|
||||
'biz_response' => [
|
||||
'result_code' => $bizResultCode,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
return [
|
||||
'response' => $response,
|
||||
'signature' => $this->sign($response, $config['merchant_private_key']),
|
||||
];
|
||||
}
|
||||
|
||||
private function post(string $path, array $body): array
|
||||
{
|
||||
$config = $this->configService->assertReady(true);
|
||||
$request = [
|
||||
'head' => [
|
||||
'version' => self::VERSION,
|
||||
'sign_type' => self::SIGN_TYPE,
|
||||
'appid' => $config['appid'],
|
||||
'request_time' => $this->isoTime(),
|
||||
],
|
||||
'body' => $body,
|
||||
];
|
||||
$payload = [
|
||||
'request' => $request,
|
||||
'signature' => $this->sign($request, $config['merchant_private_key']),
|
||||
];
|
||||
|
||||
$rawResponse = $this->postJson($config['api_domain'] . $path, $payload);
|
||||
$decoded = json_decode($rawResponse, true);
|
||||
if (!is_array($decoded)) {
|
||||
throw new \RuntimeException('收钱吧接口返回格式异常');
|
||||
}
|
||||
|
||||
if (!$this->verifyEnvelopeRaw($rawResponse, $decoded, 'response') && !$this->verifyEnvelope($decoded, 'response')) {
|
||||
throw new \RuntimeException('收钱吧接口返回验签失败');
|
||||
}
|
||||
|
||||
$responseBody = $decoded['response']['body'] ?? [];
|
||||
if (!is_array($responseBody)) {
|
||||
throw new \RuntimeException('收钱吧接口返回业务体异常');
|
||||
}
|
||||
|
||||
if ((string)($responseBody['result_code'] ?? '') !== '200') {
|
||||
throw new \RuntimeException($responseBody['error_message'] ?? '收钱吧接口通信失败');
|
||||
}
|
||||
|
||||
$bizResponse = $responseBody['biz_response'] ?? [];
|
||||
if (!is_array($bizResponse) || (string)($bizResponse['result_code'] ?? '') !== '200') {
|
||||
throw new \RuntimeException($bizResponse['error_message'] ?? '收钱吧接口业务处理失败');
|
||||
}
|
||||
|
||||
$data = $bizResponse['data'] ?? [];
|
||||
return [
|
||||
'request' => $request,
|
||||
'response' => $decoded,
|
||||
'data' => is_array($data) ? $data : [],
|
||||
];
|
||||
}
|
||||
|
||||
private function sign(array $payload, string $privateKey): string
|
||||
{
|
||||
$content = $this->encodeJson($payload);
|
||||
$key = openssl_pkey_get_private($privateKey);
|
||||
if ($key === false) {
|
||||
throw new \RuntimeException('收钱吧商户 RSA 私钥不可用');
|
||||
}
|
||||
|
||||
$signature = '';
|
||||
$ok = openssl_sign($content, $signature, $key, OPENSSL_ALGO_SHA256);
|
||||
if (!$ok) {
|
||||
throw new \RuntimeException('收钱吧请求签名失败');
|
||||
}
|
||||
|
||||
return base64_encode($signature);
|
||||
}
|
||||
|
||||
private function verifyEnvelope(array $payload, string $field): bool
|
||||
{
|
||||
$content = $payload[$field] ?? null;
|
||||
$signature = (string)($payload['signature'] ?? '');
|
||||
if (!is_array($content) || $signature === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->verifyContent($this->encodeJson($content), $signature);
|
||||
}
|
||||
|
||||
private function verifyEnvelopeRaw(string $rawPayload, array $payload, string $field): bool
|
||||
{
|
||||
$signature = (string)($payload['signature'] ?? '');
|
||||
if ($signature === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$content = $this->extractJsonFieldRaw($rawPayload, $field);
|
||||
if ($content === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->verifyContent($content, $signature);
|
||||
}
|
||||
|
||||
private function verifyContent(string $content, string $signature): bool
|
||||
{
|
||||
$config = $this->configService->assertReady(true);
|
||||
$publicKey = openssl_pkey_get_public($config['shouqianba_public_key']);
|
||||
if ($publicKey === false) {
|
||||
throw new \RuntimeException('收钱吧 RSA 公钥不可用');
|
||||
}
|
||||
|
||||
$result = openssl_verify(
|
||||
$content,
|
||||
base64_decode($signature, true) ?: '',
|
||||
$publicKey,
|
||||
OPENSSL_ALGO_SHA256
|
||||
);
|
||||
|
||||
return $result === 1;
|
||||
}
|
||||
|
||||
private function extractJsonFieldRaw(string $json, string $field): string
|
||||
{
|
||||
if (!preg_match('/"' . preg_quote($field, '/') . '"\s*:/', $json, $matches, PREG_OFFSET_CAPTURE)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$offset = $matches[0][1] + strlen($matches[0][0]);
|
||||
$length = strlen($json);
|
||||
while ($offset < $length && ctype_space($json[$offset])) {
|
||||
$offset++;
|
||||
}
|
||||
if ($offset >= $length || !in_array($json[$offset], ['{', '['], true)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$start = $offset;
|
||||
$depth = 0;
|
||||
$inString = false;
|
||||
$escaped = false;
|
||||
for ($i = $offset; $i < $length; $i++) {
|
||||
$char = $json[$i];
|
||||
if ($inString) {
|
||||
if ($escaped) {
|
||||
$escaped = false;
|
||||
continue;
|
||||
}
|
||||
if ($char === '\\') {
|
||||
$escaped = true;
|
||||
continue;
|
||||
}
|
||||
if ($char === '"') {
|
||||
$inString = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($char === '"') {
|
||||
$inString = true;
|
||||
continue;
|
||||
}
|
||||
if ($char === '{' || $char === '[') {
|
||||
$depth++;
|
||||
continue;
|
||||
}
|
||||
if ($char === '}' || $char === ']') {
|
||||
$depth--;
|
||||
if ($depth === 0) {
|
||||
return substr($json, $start, $i - $start + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private function postJson(string $url, array $payload): string
|
||||
{
|
||||
$body = $this->encodeJson($payload);
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $body,
|
||||
CURLOPT_TIMEOUT => 12,
|
||||
CURLOPT_CONNECTTIMEOUT => 5,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'Accept: application/json',
|
||||
],
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$errno = curl_errno($ch);
|
||||
$error = curl_error($ch);
|
||||
$httpStatus = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($errno) {
|
||||
throw new \RuntimeException('收钱吧请求失败:' . $error);
|
||||
}
|
||||
if ($httpStatus < 200 || $httpStatus >= 300) {
|
||||
throw new \RuntimeException('收钱吧请求 HTTP 状态异常:' . $httpStatus);
|
||||
}
|
||||
|
||||
return is_string($response) ? $response : '';
|
||||
}
|
||||
|
||||
private function encodeJson(array $payload): string
|
||||
{
|
||||
$json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if (!is_string($json)) {
|
||||
throw new \RuntimeException('收钱吧 JSON 编码失败');
|
||||
}
|
||||
|
||||
return $json;
|
||||
}
|
||||
|
||||
private function isoTime(?int $timestamp = null): string
|
||||
{
|
||||
return date('c', $timestamp ?? time());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user