初始化
This commit is contained in:
parent
3b4674ed92
commit
8484851bd9
|
|
@ -2,4 +2,4 @@ RewriteEngine On
|
|||
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule ^ server.php [QSA,L]
|
||||
RewriteRule ^ public/index.php [QSA,L]
|
||||
|
|
|
|||
14
README.md
14
README.md
|
|
@ -15,7 +15,7 @@ MERCHANT_API_TIMEOUT=10
|
|||
## Start Locally
|
||||
|
||||
```bash
|
||||
php -S 127.0.0.1:8080 server.php
|
||||
php -S 127.0.0.1:8080 -t public
|
||||
```
|
||||
|
||||
## Response Format
|
||||
|
|
@ -44,19 +44,23 @@ Error:
|
|||
## Endpoints
|
||||
|
||||
|
||||
### `GET|POST /tools`
|
||||
### `GET /health`
|
||||
|
||||
Returns service health status.
|
||||
|
||||
### `GET /tools`
|
||||
|
||||
Returns available API tools and input schemas.
|
||||
|
||||
### `GET|POST /api/order-summary`
|
||||
### `POST /api/order-summary`
|
||||
|
||||
Returns order count, active merchant count, successful order count, and amount totals.
|
||||
|
||||
### `GET|POST /api/offline-orders/analyze`
|
||||
### `POST /api/offline-stats`
|
||||
|
||||
Returns status, payment type, order type, and amount distribution analysis.
|
||||
|
||||
### `GET|POST /api/merchant-ranking`
|
||||
### `POST /api/merchant-ranking`
|
||||
|
||||
Returns merchant ranking by `orderCount`, `successOrderCount`, `totalOrderAmount`, or `totalRealPay`.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
use App\Api\ApiFactory;
|
||||
use App\Http\HttpRequest;
|
||||
|
||||
ApiFactory::create()->handle(HttpRequest::fromGlobals());
|
||||
|
|
@ -2,9 +2,4 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
use App\Api\ApiFactory;
|
||||
use App\Http\HttpRequest;
|
||||
|
||||
ApiFactory::create()->handle(HttpRequest::fromGlobals());
|
||||
require __DIR__ . '/public/index.php';
|
||||
|
|
|
|||
|
|
@ -5,8 +5,11 @@ declare(strict_types=1);
|
|||
namespace App\Api;
|
||||
|
||||
use App\Config\ApiConfig;
|
||||
use App\Controller\HealthController;
|
||||
use App\Controller\StatsController;
|
||||
use App\Http\MerchantApiClient;
|
||||
use App\Repositories\OfflineOrderRepository;
|
||||
use App\Service\StatsService;
|
||||
use App\Tools\AnalyzeOfflineOrdersTool;
|
||||
use App\Tools\MerchantRankingTool;
|
||||
use App\Tools\OrderSummaryTool;
|
||||
|
|
@ -17,11 +20,15 @@ final class ApiFactory
|
|||
{
|
||||
$apiClient = new MerchantApiClient(ApiConfig::fromEnvironment());
|
||||
$orders = new OfflineOrderRepository($apiClient);
|
||||
|
||||
return new ApiKernel(
|
||||
$statsService = new StatsService(
|
||||
new OrderSummaryTool($orders),
|
||||
new AnalyzeOfflineOrdersTool($orders),
|
||||
new MerchantRankingTool($orders),
|
||||
);
|
||||
|
||||
return new ApiKernel(
|
||||
new HealthController(),
|
||||
new StatsController($statsService),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Api;
|
||||
|
||||
use App\Controller\HealthController;
|
||||
use App\Controller\StatsController;
|
||||
use App\Http\HttpRequest;
|
||||
use App\Http\JsonResponse;
|
||||
use App\Tools\AnalyzeOfflineOrdersTool;
|
||||
|
|
@ -13,9 +15,8 @@ use App\Tools\OrderSummaryTool;
|
|||
final readonly class ApiKernel
|
||||
{
|
||||
public function __construct(
|
||||
private OrderSummaryTool $orderSummaryTool,
|
||||
private AnalyzeOfflineOrdersTool $analyzeOfflineOrdersTool,
|
||||
private MerchantRankingTool $merchantRankingTool,
|
||||
private HealthController $healthController,
|
||||
private StatsController $statsController,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -23,11 +24,11 @@ final readonly class ApiKernel
|
|||
{
|
||||
try {
|
||||
match ($request->path) {
|
||||
'/', '/health' => $this->ok(['status' => 'ok', 'service' => 'merchant-ai-api']),
|
||||
'/', '/health' => $this->ok($this->healthController->show()),
|
||||
'/tools' => $this->ok($this->tools()),
|
||||
'/api/order-summary' => $this->ok($this->orderSummary($request)),
|
||||
'/api/offline-orders/analyze' => $this->ok($this->analyzeOfflineOrders($request)),
|
||||
'/api/merchant-ranking' => $this->ok($this->merchantRanking($request)),
|
||||
'/api/order-summary' => $this->post($request, fn (): array => $this->statsController->orderSummary($request)),
|
||||
'/api/offline-stats' => $this->post($request, fn (): array => $this->statsController->offlineStats($request)),
|
||||
'/api/merchant-ranking' => $this->post($request, fn (): array => $this->statsController->merchantRanking($request)),
|
||||
default => JsonResponse::send([
|
||||
'success' => false,
|
||||
'error' => [
|
||||
|
|
@ -47,6 +48,26 @@ final readonly class ApiKernel
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Closure(): array<string, mixed> $handler
|
||||
*/
|
||||
private function post(HttpRequest $request, \Closure $handler): void
|
||||
{
|
||||
if ($request->method !== 'POST') {
|
||||
JsonResponse::send([
|
||||
'success' => false,
|
||||
'error' => [
|
||||
'code' => 'method_not_allowed',
|
||||
'message' => 'This endpoint only supports POST.',
|
||||
],
|
||||
], 405);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->ok($handler());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
|
|
@ -68,75 +89,25 @@ final readonly class ApiKernel
|
|||
[
|
||||
'name' => OrderSummaryTool::NAME,
|
||||
'description' => OrderSummaryTool::DESCRIPTION,
|
||||
'method' => 'GET|POST',
|
||||
'method' => 'POST',
|
||||
'path' => '/api/order-summary',
|
||||
'inputSchema' => OrderSummaryTool::inputSchema(),
|
||||
],
|
||||
[
|
||||
'name' => AnalyzeOfflineOrdersTool::NAME,
|
||||
'description' => AnalyzeOfflineOrdersTool::DESCRIPTION,
|
||||
'method' => 'GET|POST',
|
||||
'path' => '/api/offline-orders/analyze',
|
||||
'method' => 'POST',
|
||||
'path' => '/api/offline-stats',
|
||||
'inputSchema' => AnalyzeOfflineOrdersTool::inputSchema(),
|
||||
],
|
||||
[
|
||||
'name' => MerchantRankingTool::NAME,
|
||||
'description' => MerchantRankingTool::DESCRIPTION,
|
||||
'method' => 'GET|POST',
|
||||
'method' => 'POST',
|
||||
'path' => '/api/merchant-ranking',
|
||||
'inputSchema' => MerchantRankingTool::inputSchema(),
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function orderSummary(HttpRequest $request): array
|
||||
{
|
||||
return ($this->orderSummaryTool)(
|
||||
$request->string('date'),
|
||||
$request->int('status'),
|
||||
$request->string('keyword'),
|
||||
$request->string('fieldKey'),
|
||||
$request->string('payType'),
|
||||
$request->string('type'),
|
||||
$request->int('btcId'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function analyzeOfflineOrders(HttpRequest $request): array
|
||||
{
|
||||
return ($this->analyzeOfflineOrdersTool)(
|
||||
$request->string('date'),
|
||||
$request->int('status'),
|
||||
$request->string('keyword'),
|
||||
$request->string('fieldKey'),
|
||||
$request->string('payType'),
|
||||
$request->string('type'),
|
||||
$request->int('btcId'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function merchantRanking(HttpRequest $request): array
|
||||
{
|
||||
return ($this->merchantRankingTool)(
|
||||
$request->string('date'),
|
||||
$request->int('status'),
|
||||
$request->string('keyword'),
|
||||
$request->string('fieldKey'),
|
||||
$request->string('payType'),
|
||||
$request->string('type'),
|
||||
$request->int('btcId'),
|
||||
$request->string('sortBy'),
|
||||
$request->int('limit'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
final class HealthController
|
||||
{
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function show(): array
|
||||
{
|
||||
return [
|
||||
'status' => 'ok',
|
||||
'service' => 'merchant-ai-api',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Http\HttpRequest;
|
||||
use App\Service\StatsService;
|
||||
|
||||
final readonly class StatsController
|
||||
{
|
||||
public function __construct(private StatsService $statsService)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function orderSummary(HttpRequest $request): array
|
||||
{
|
||||
return $this->statsService->getOrderSummary($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function offlineStats(HttpRequest $request): array
|
||||
{
|
||||
return $this->statsService->getOfflineStats($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function merchantRanking(HttpRequest $request): array
|
||||
{
|
||||
return $this->statsService->getMerchantRanking($request);
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,12 @@ final readonly class HttpRequest
|
|||
$method = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET'));
|
||||
$uri = (string) ($_SERVER['REQUEST_URI'] ?? '/');
|
||||
$path = parse_url($uri, PHP_URL_PATH) ?: '/';
|
||||
$scriptName = (string) ($_SERVER['SCRIPT_NAME'] ?? '');
|
||||
|
||||
if ($scriptName !== '' && $scriptName !== '/index.php' && str_starts_with($path, dirname($scriptName))) {
|
||||
$path = substr($path, strlen(dirname($scriptName))) ?: '/';
|
||||
}
|
||||
|
||||
$input = $_GET;
|
||||
|
||||
if (in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Http\HttpRequest;
|
||||
use App\Tools\AnalyzeOfflineOrdersTool;
|
||||
use App\Tools\MerchantRankingTool;
|
||||
use App\Tools\OrderSummaryTool;
|
||||
|
||||
final readonly class StatsService
|
||||
{
|
||||
public function __construct(
|
||||
private OrderSummaryTool $orderSummaryTool,
|
||||
private AnalyzeOfflineOrdersTool $offlineStatsTool,
|
||||
private MerchantRankingTool $merchantRankingTool,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getOrderSummary(HttpRequest $request): array
|
||||
{
|
||||
return ($this->orderSummaryTool)(
|
||||
$request->string('date'),
|
||||
$request->int('status'),
|
||||
$request->string('keyword'),
|
||||
$request->string('fieldKey'),
|
||||
$request->string('payType'),
|
||||
$request->string('type'),
|
||||
$request->int('btcId'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getOfflineStats(HttpRequest $request): array
|
||||
{
|
||||
return ($this->offlineStatsTool)(
|
||||
$request->string('date'),
|
||||
$request->int('status'),
|
||||
$request->string('keyword'),
|
||||
$request->string('fieldKey'),
|
||||
$request->string('payType'),
|
||||
$request->string('type'),
|
||||
$request->int('btcId'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getMerchantRanking(HttpRequest $request): array
|
||||
{
|
||||
return ($this->merchantRankingTool)(
|
||||
$request->string('date'),
|
||||
$request->int('status'),
|
||||
$request->string('keyword'),
|
||||
$request->string('fieldKey'),
|
||||
$request->string('payType'),
|
||||
$request->string('type'),
|
||||
$request->int('btcId'),
|
||||
$request->string('sortBy'),
|
||||
$request->int('limit'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ use App\Support\OfflineOrderQuery;
|
|||
|
||||
final readonly class AnalyzeOfflineOrdersTool
|
||||
{
|
||||
public const NAME = 'analyze_offline_orders';
|
||||
public const NAME = 'get_offline_stats';
|
||||
|
||||
public const DESCRIPTION = '分析线下订单的状态、支付方式、订单类型和金额分布。';
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue