From 0528333d5e8b8e0651512a50d2b30844985b7032 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 17 Apr 2026 15:40:50 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=89=A9=E6=B5=81=E4=B8=AD?= =?UTF-8?q?=E4=B8=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/GancaoCallbackController.php | 90 ++++++++++++++----- server/config/gancao_scm.php | 3 + 2 files changed, 70 insertions(+), 23 deletions(-) diff --git a/server/app/api/controller/GancaoCallbackController.php b/server/app/api/controller/GancaoCallbackController.php index e85a86e4..6be2db20 100644 --- a/server/app/api/controller/GancaoCallbackController.php +++ b/server/app/api/controller/GancaoCallbackController.php @@ -56,23 +56,25 @@ class GancaoCallbackController extends BaseApiController */ public function orderStatus(): Response { + $rawBody = (string) file_get_contents('php://input'); + $headers = $this->request->header(); + + $accessAppkey = (string) $this->pickHeader($headers, ['access-appkey', 'accessappkey', 'x-access-appkey']); + $accessNonce = (string) $this->pickHeader($headers, ['access-nonce', 'accessnonce', 'x-access-nonce']); + $accessTimestamp = (string) $this->pickHeader($headers, ['access-timestamp', 'accesstimestamp', 'x-access-timestamp']); + $accessSign = (string) $this->pickHeader($headers, ['access-sign', 'accesssign', 'x-access-sign']); + + Log::info(sprintf( + 'Gancao callback received | appkey=%s | nonce=%s | ts=%s | sign=%s | body=%s | headers=%s', + $accessAppkey !== '' ? $accessAppkey : '(empty)', + $accessNonce !== '' ? $accessNonce : '(empty)', + $accessTimestamp !== '' ? $accessTimestamp : '(empty)', + $accessSign !== '' ? $accessSign : '(empty)', + $rawBody, + json_encode($headers, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) + )); + try { - $rawBody = file_get_contents('php://input'); - - $headers = $this->request->header(); - $accessAppkey = (string) ($headers['access-appkey'] ?? ''); - $accessNonce = (string) ($headers['access-nonce'] ?? ''); - $accessTimestamp = (string) ($headers['access-timestamp'] ?? ''); - $accessSign = (string) ($headers['access-sign'] ?? ''); - - Log::info('Gancao callback received', [ - 'appkey' => $accessAppkey, - 'nonce' => $accessNonce, - 'ts' => $accessTimestamp, - 'sign' => $accessSign, - 'body' => $rawBody, - ]); - if (!$this->verifySign($accessAppkey, $accessNonce, $accessTimestamp, $accessSign, $rawBody)) { Log::warning('Gancao callback sign verification failed'); return $this->ok(); @@ -100,27 +102,69 @@ class GancaoCallbackController extends BaseApiController /* 签名验证 */ /* ------------------------------------------------------------------ */ + /** + * 兼容多种 header key 命名(ThinkPHP 默认都会统一成小写-连字符,但不同反向代理/php-fpm 下可能变体) + * + * @param array> $headers + * @param array $candidates 按优先级排列的 header key + */ + private function pickHeader(array $headers, array $candidates): string + { + foreach ($candidates as $key) { + if (!isset($headers[$key])) { + continue; + } + $v = $headers[$key]; + if (is_array($v)) { + $v = reset($v); + } + $v = trim((string) $v); + if ($v !== '') { + return $v; + } + } + return ''; + } + /** * md5(access-appkey + secret-key + access-nonce + access-timestamp + $sBody) + * + * 注意:回调签名使用的是「回调通知账号」—— callback_appkey / callback_secret, + * 与下单使用的 biz_ak / biz_sk 是不同的两套凭证。 */ private function verifySign(string $appkey, string $nonce, string $timestamp, string $sign, string $body): bool { - if ($sign === '' || $appkey === '') { + $config = Config::get('gancao_scm', []); + $cfgAppkey = (string) ($config['callback_appkey'] ?? ''); + $secretKey = (string) ($config['callback_secret'] ?? ''); + + if ($appkey === '' || $sign === '') { + Log::warning(sprintf( + 'Gancao callback missing header | appkey=%s | sign=%s', + $appkey !== '' ? $appkey : '(empty)', + $sign !== '' ? $sign : '(empty)' + )); return false; } - $config = Config::get('gancao_scm', []); - $cfgAppkey = (string) ($config['biz_ak'] ?? ''); - $secretKey = (string) ($config['biz_sk'] ?? ''); - if ($appkey !== $cfgAppkey) { - Log::warning('Gancao callback appkey mismatch', compact('appkey', 'cfgAppkey')); + Log::warning(sprintf( + 'Gancao callback appkey mismatch | received=%s | expected(config.callback_appkey)=%s', + $appkey, + $cfgAppkey !== '' ? $cfgAppkey : '(empty, check GANCAO_SCM_CALLBACK_APPKEY in .env)' + )); return false; } $expected = md5($appkey . $secretKey . $nonce . $timestamp . $body); if (!hash_equals($expected, $sign)) { - Log::warning('Gancao callback sign mismatch', compact('sign', 'expected')); + Log::warning(sprintf( + 'Gancao callback sign mismatch | received=%s | expected=%s | nonce=%s | ts=%s', + $sign, + $expected, + $nonce, + $timestamp + )); return false; } diff --git a/server/config/gancao_scm.php b/server/config/gancao_scm.php index 95dbff3e..ec896ee2 100644 --- a/server/config/gancao_scm.php +++ b/server/config/gancao_scm.php @@ -64,6 +64,9 @@ return [ 'biz_ak' => $bizAk, 'biz_sk' => $bizSk, 'callback_url' => (string) zyt_gancao_scm_env('CALLBACK_URL', ''), + // 回调签名凭证:与下单业务账号 biz_ak/biz_sk 不同,由甘草控制台「订单流转回调通知」配置 + 'callback_appkey' => (string) zyt_gancao_scm_env('CALLBACK_APPKEY', ''), + 'callback_secret' => (string) zyt_gancao_scm_env('CALLBACK_SECRET', ''), 'cradle_store' => (string) zyt_gancao_scm_env('CRADLE_STORE', '甄养堂互联网医院'), 'df_id' => (int) zyt_gancao_scm_env('DF_ID', 102), 'express_type' => (string) zyt_gancao_scm_env('EXPRESS_TYPE', 'general'),