From 6d5191b5e867541d3bec3eb1799e18342239b300 Mon Sep 17 00:00:00 2001 From: "1173117610@qq.com" Date: Mon, 29 Jun 2026 13:51:33 +0800 Subject: [PATCH] =?UTF-8?q?=20=E6=B7=BB=E5=8A=A0=E5=85=B6=E4=BB=96?= =?UTF-8?q?=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 5 +++ .gitignore | 3 ++ README.md | 8 +++- docs/AI_INTEGRATION_GUIDE.md | 10 +++++ public/index.php | 7 +++- src/Api/ApiFactory.php | 4 +- src/Config/ApiConfig.php | 8 ++-- src/Config/Env.php | 43 ++++++++++++++++++++ src/Http/HttpRequest.php | 44 ++++++++++++++++++++- src/Repositories/OfflineOrderRepository.php | 13 ++++-- 10 files changed, 132 insertions(+), 13 deletions(-) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 src/Config/Env.php diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..bf7b370 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +MERCHANT_API_BASE_URI=https://tpoint.agrimedia.cn +MERCHANT_API_TOKEN=your-admin-api-token +MERCHANT_API_TIMEOUT=60 + +OFFLINE_ORDER_PAGE_LIMIT=100 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6549667 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.env +vendor/ +.idea/ diff --git a/README.md b/README.md index aa590cc..5eb5666 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,19 @@ PHP 8.2 JSON API service for merchant order statistics, offline order analysis, ## Environment +Copy `.env.example` to `.env`, then fill your real token: + ```bash MERCHANT_API_TOKEN=your-admin-api-token MERCHANT_API_BASE_URI=https://tpoint.agrimedia.cn -MERCHANT_API_TIMEOUT=10 +MERCHANT_API_TIMEOUT=60 +OFFLINE_ORDER_PAGE_LIMIT=100 ``` `MERCHANT_API_TOKEN` is required. `MERCHANT_API_BASE_URI` and `MERCHANT_API_TIMEOUT` are optional. +`OFFLINE_ORDER_PAGE_LIMIT` controls each upstream pagination request size. + +If the request contains `Authorization: Bearer `, that token is used for the upstream merchant API request first. If the header is missing, `MERCHANT_API_TOKEN` from `.env` is used. ## Start Locally diff --git a/docs/AI_INTEGRATION_GUIDE.md b/docs/AI_INTEGRATION_GUIDE.md index 2e259d5..e519d82 100644 --- a/docs/AI_INTEGRATION_GUIDE.md +++ b/docs/AI_INTEGRATION_GUIDE.md @@ -80,6 +80,16 @@ POST /api/merchant-ranking Content-Type: application/json ``` +如果调用方请求头中传入: + +```http +Authorization: Bearer <后台订单接口 token> +``` + +本服务会优先使用该 token 请求后台订单接口;如果没有传 `Authorization`,则使用服务器 `.env` 中的 `MERCHANT_API_TOKEN`。 + +注意:只有可信后端服务可以传该 header,不建议在浏览器前端或公开客户端暴露后台 token。 + 统一请求示例: ```json diff --git a/public/index.php b/public/index.php index 45c6748..143b035 100644 --- a/public/index.php +++ b/public/index.php @@ -5,6 +5,11 @@ declare(strict_types=1); require dirname(__DIR__) . '/vendor/autoload.php'; use App\Api\ApiFactory; +use App\Config\Env; use App\Http\HttpRequest; -ApiFactory::create()->handle(HttpRequest::fromGlobals()); +Env::load(dirname(__DIR__) . '/.env'); + +$request = HttpRequest::fromGlobals(); + +ApiFactory::create($request->bearerToken())->handle($request); diff --git a/src/Api/ApiFactory.php b/src/Api/ApiFactory.php index be1cc65..d9bbf20 100644 --- a/src/Api/ApiFactory.php +++ b/src/Api/ApiFactory.php @@ -17,9 +17,9 @@ use App\Tools\OrderSummaryTool; final class ApiFactory { - public static function create(): ApiKernel + public static function create(?string $tokenOverride = null): ApiKernel { - $apiClient = new MerchantApiClient(ApiConfig::fromEnvironment()); + $apiClient = new MerchantApiClient(ApiConfig::fromEnvironment($tokenOverride)); $orders = new OfflineOrderRepository($apiClient); $statsService = new StatsService( new OrderSummaryTool($orders), diff --git a/src/Config/ApiConfig.php b/src/Config/ApiConfig.php index 7241d8b..80773b4 100644 --- a/src/Config/ApiConfig.php +++ b/src/Config/ApiConfig.php @@ -9,15 +9,15 @@ final readonly class ApiConfig public function __construct( public string $baseUri, public string $token, - public int $timeout = 10, + public int $timeout = 60, ) { } - public static function fromEnvironment(): self + public static function fromEnvironment(?string $tokenOverride = null): self { $baseUri = getenv('MERCHANT_API_BASE_URI') ?: 'https://tpoint.agrimedia.cn'; - $token = getenv('MERCHANT_API_TOKEN') ?: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwd2QiOiI2ODI5N2FjYzUwZDczYWQzZmNmMTNlM2Y4MWYxMWQyZSIsImlzcyI6InRwb2ludC5hZ3JpbWVkaWEuY24iLCJhdWQiOiJ0cG9pbnQuYWdyaW1lZGlhLmNuIiwiaWF0IjoxNzgwODk3Mjk3LCJuYmYiOjE3ODA4OTcyOTcsImV4cCI6MTc4MzQwMjg5NywianRpIjp7ImlkIjo0LCJ0eXBlIjoiYWRtaW4ifX0.G2yB38bYkf0b37kpAbQViDuXY4QHeRjm99WNXjSh4L0'; - $timeout = (int) (getenv('MERCHANT_API_TIMEOUT') ?: 10); + $token = $tokenOverride ?: (getenv('MERCHANT_API_TOKEN') ?: ''); + $timeout = (int) (getenv('MERCHANT_API_TIMEOUT') ?: 60); if ($token === '') { throw new \RuntimeException('Missing MERCHANT_API_TOKEN environment variable.'); diff --git a/src/Config/Env.php b/src/Config/Env.php new file mode 100644 index 0000000..c23b9e0 --- /dev/null +++ b/src/Config/Env.php @@ -0,0 +1,43 @@ + $input + * @param array $headers */ public function __construct( public string $method, public string $path, public array $input, + public array $headers = [], ) { } @@ -41,7 +43,20 @@ final readonly class HttpRequest } } - return new self($method, $path, $input); + return new self($method, $path, $input, self::headersFromGlobals()); + } + + public function bearerToken(): ?string + { + $authorization = $this->headers['authorization'] ?? ''; + + if (preg_match('/^Bearer\s+(.+)$/i', $authorization, $matches) !== 1) { + return null; + } + + $token = trim($matches[1]); + + return $token !== '' ? $token : null; } public function string(string $key): ?string @@ -73,4 +88,31 @@ final readonly class HttpRequest return (int) $value; } + + /** + * @return array + */ + private static function headersFromGlobals(): array + { + $headers = []; + + foreach ($_SERVER as $key => $value) { + if (!is_string($value) || !str_starts_with($key, 'HTTP_')) { + continue; + } + + $name = strtolower(str_replace('_', '-', substr($key, 5))); + $headers[$name] = $value; + } + + if (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION']) && is_string($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) { + $headers['authorization'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION']; + } + + if (isset($_SERVER['HTTP_AUTHORIZATION']) && is_string($_SERVER['HTTP_AUTHORIZATION'])) { + $headers['authorization'] = $_SERVER['HTTP_AUTHORIZATION']; + } + + return $headers; + } } diff --git a/src/Repositories/OfflineOrderRepository.php b/src/Repositories/OfflineOrderRepository.php index 1c326c8..afe71fe 100644 --- a/src/Repositories/OfflineOrderRepository.php +++ b/src/Repositories/OfflineOrderRepository.php @@ -8,8 +8,6 @@ use App\Http\MerchantApiClient; final readonly class OfflineOrderRepository { - private const PAGE_LIMIT = 100; - public function __construct(private MerchantApiClient $apiClient) { } @@ -27,7 +25,7 @@ final readonly class OfflineOrderRepository do { $data = $this->apiClient->getOfflineOrders($query + [ 'page' => $page, - 'limit' => self::PAGE_LIMIT, + 'limit' => $this->pageLimit(), ]); $pageOrders = $data['data']['list'] ?? []; @@ -56,6 +54,13 @@ final readonly class OfflineOrderRepository return count($orders) < $totalCount; } - return is_array($pageOrders) && count($pageOrders) >= self::PAGE_LIMIT; + return is_array($pageOrders) && count($pageOrders) >= $this->pageLimit(); + } + + private function pageLimit(): int + { + $limit = (int) (getenv('OFFLINE_ORDER_PAGE_LIMIT') ?: 100); + + return $limit > 0 ? $limit : 100; } }