添加其他字段

This commit is contained in:
1173117610@qq.com 2026-06-29 13:51:33 +08:00
parent b5c68be1a6
commit 6d5191b5e8
10 changed files with 132 additions and 13 deletions

5
.env.example Normal file
View File

@ -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

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.env
vendor/
.idea/

View File

@ -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 <token>`, 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

View File

@ -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

View File

@ -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);

View File

@ -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),

View File

@ -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.');

43
src/Config/Env.php Normal file
View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Config;
final class Env
{
public static function load(string $path): void
{
if (!is_file($path)) {
return;
}
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if ($lines === false) {
return;
}
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || str_starts_with($line, '#') || !str_contains($line, '=')) {
continue;
}
[$key, $value] = explode('=', $line, 2);
$key = trim($key);
$value = trim($value);
if ($key === '' || getenv($key) !== false) {
continue;
}
$value = trim($value, "\"'");
putenv($key . '=' . $value);
$_ENV[$key] = $value;
$_SERVER[$key] = $value;
}
}
}

View File

@ -8,11 +8,13 @@ final readonly class HttpRequest
{
/**
* @param array<string, mixed> $input
* @param array<string, string> $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<string, string>
*/
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;
}
}

View File

@ -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;
}
}