添加其他字段
This commit is contained in:
parent
b5c68be1a6
commit
6d5191b5e8
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
.env
|
||||||
|
vendor/
|
||||||
|
.idea/
|
||||||
|
|
@ -4,13 +4,19 @@ PHP 8.2 JSON API service for merchant order statistics, offline order analysis,
|
||||||
|
|
||||||
## Environment
|
## Environment
|
||||||
|
|
||||||
|
Copy `.env.example` to `.env`, then fill your real token:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
MERCHANT_API_TOKEN=your-admin-api-token
|
MERCHANT_API_TOKEN=your-admin-api-token
|
||||||
MERCHANT_API_BASE_URI=https://tpoint.agrimedia.cn
|
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.
|
`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
|
## Start Locally
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,16 @@ POST /api/merchant-ranking
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
如果调用方请求头中传入:
|
||||||
|
|
||||||
|
```http
|
||||||
|
Authorization: Bearer <后台订单接口 token>
|
||||||
|
```
|
||||||
|
|
||||||
|
本服务会优先使用该 token 请求后台订单接口;如果没有传 `Authorization`,则使用服务器 `.env` 中的 `MERCHANT_API_TOKEN`。
|
||||||
|
|
||||||
|
注意:只有可信后端服务可以传该 header,不建议在浏览器前端或公开客户端暴露后台 token。
|
||||||
|
|
||||||
统一请求示例:
|
统一请求示例:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,11 @@ declare(strict_types=1);
|
||||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||||
|
|
||||||
use App\Api\ApiFactory;
|
use App\Api\ApiFactory;
|
||||||
|
use App\Config\Env;
|
||||||
use App\Http\HttpRequest;
|
use App\Http\HttpRequest;
|
||||||
|
|
||||||
ApiFactory::create()->handle(HttpRequest::fromGlobals());
|
Env::load(dirname(__DIR__) . '/.env');
|
||||||
|
|
||||||
|
$request = HttpRequest::fromGlobals();
|
||||||
|
|
||||||
|
ApiFactory::create($request->bearerToken())->handle($request);
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@ use App\Tools\OrderSummaryTool;
|
||||||
|
|
||||||
final class ApiFactory
|
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);
|
$orders = new OfflineOrderRepository($apiClient);
|
||||||
$statsService = new StatsService(
|
$statsService = new StatsService(
|
||||||
new OrderSummaryTool($orders),
|
new OrderSummaryTool($orders),
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,15 @@ final readonly class ApiConfig
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public string $baseUri,
|
public string $baseUri,
|
||||||
public string $token,
|
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';
|
$baseUri = getenv('MERCHANT_API_BASE_URI') ?: 'https://tpoint.agrimedia.cn';
|
||||||
$token = getenv('MERCHANT_API_TOKEN') ?: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwd2QiOiI2ODI5N2FjYzUwZDczYWQzZmNmMTNlM2Y4MWYxMWQyZSIsImlzcyI6InRwb2ludC5hZ3JpbWVkaWEuY24iLCJhdWQiOiJ0cG9pbnQuYWdyaW1lZGlhLmNuIiwiaWF0IjoxNzgwODk3Mjk3LCJuYmYiOjE3ODA4OTcyOTcsImV4cCI6MTc4MzQwMjg5NywianRpIjp7ImlkIjo0LCJ0eXBlIjoiYWRtaW4ifX0.G2yB38bYkf0b37kpAbQViDuXY4QHeRjm99WNXjSh4L0';
|
$token = $tokenOverride ?: (getenv('MERCHANT_API_TOKEN') ?: '');
|
||||||
$timeout = (int) (getenv('MERCHANT_API_TIMEOUT') ?: 10);
|
$timeout = (int) (getenv('MERCHANT_API_TIMEOUT') ?: 60);
|
||||||
|
|
||||||
if ($token === '') {
|
if ($token === '') {
|
||||||
throw new \RuntimeException('Missing MERCHANT_API_TOKEN environment variable.');
|
throw new \RuntimeException('Missing MERCHANT_API_TOKEN environment variable.');
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,11 +8,13 @@ final readonly class HttpRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $input
|
* @param array<string, mixed> $input
|
||||||
|
* @param array<string, string> $headers
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public string $method,
|
public string $method,
|
||||||
public string $path,
|
public string $path,
|
||||||
public array $input,
|
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
|
public function string(string $key): ?string
|
||||||
|
|
@ -73,4 +88,31 @@ final readonly class HttpRequest
|
||||||
|
|
||||||
return (int) $value;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,6 @@ use App\Http\MerchantApiClient;
|
||||||
|
|
||||||
final readonly class OfflineOrderRepository
|
final readonly class OfflineOrderRepository
|
||||||
{
|
{
|
||||||
private const PAGE_LIMIT = 100;
|
|
||||||
|
|
||||||
public function __construct(private MerchantApiClient $apiClient)
|
public function __construct(private MerchantApiClient $apiClient)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
@ -27,7 +25,7 @@ final readonly class OfflineOrderRepository
|
||||||
do {
|
do {
|
||||||
$data = $this->apiClient->getOfflineOrders($query + [
|
$data = $this->apiClient->getOfflineOrders($query + [
|
||||||
'page' => $page,
|
'page' => $page,
|
||||||
'limit' => self::PAGE_LIMIT,
|
'limit' => $this->pageLimit(),
|
||||||
]);
|
]);
|
||||||
$pageOrders = $data['data']['list'] ?? [];
|
$pageOrders = $data['data']['list'] ?? [];
|
||||||
|
|
||||||
|
|
@ -56,6 +54,13 @@ final readonly class OfflineOrderRepository
|
||||||
return count($orders) < $totalCount;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue