From 8484851bd9e07b49adc79f91d583a8eb0b04d325 Mon Sep 17 00:00:00 2001 From: "1173117610@qq.com" Date: Tue, 23 Jun 2026 16:26:06 +0800 Subject: [PATCH] =?UTF-8?q?=20=E5=88=9D=E5=A7=8B=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .htaccess | 2 +- README.md | 14 ++-- public/index.php | 10 +++ server.php | 7 +- src/Api/ApiFactory.php | 11 ++- src/Api/ApiKernel.php | 93 +++++++++----------------- src/Controller/HealthController.php | 19 ++++++ src/Controller/StatsController.php | 39 +++++++++++ src/Http/HttpRequest.php | 6 ++ src/Service/StatsService.php | 70 +++++++++++++++++++ src/Tools/AnalyzeOfflineOrdersTool.php | 2 +- 11 files changed, 197 insertions(+), 76 deletions(-) create mode 100644 public/index.php create mode 100644 src/Controller/HealthController.php create mode 100644 src/Controller/StatsController.php create mode 100644 src/Service/StatsService.php diff --git a/.htaccess b/.htaccess index cbef204..686b74a 100644 --- a/.htaccess +++ b/.htaccess @@ -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] diff --git a/README.md b/README.md index 80a65cc..e741b90 100644 --- a/README.md +++ b/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`. diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..45c6748 --- /dev/null +++ b/public/index.php @@ -0,0 +1,10 @@ +handle(HttpRequest::fromGlobals()); diff --git a/server.php b/server.php index 870d1ee..488a2e5 100644 --- a/server.php +++ b/server.php @@ -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'; diff --git a/src/Api/ApiFactory.php b/src/Api/ApiFactory.php index b29f90f..7d9a366 100644 --- a/src/Api/ApiFactory.php +++ b/src/Api/ApiFactory.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), + ); } } diff --git a/src/Api/ApiKernel.php b/src/Api/ApiKernel.php index 36c91c6..f9beaed 100644 --- a/src/Api/ApiKernel.php +++ b/src/Api/ApiKernel.php @@ -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 $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 $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 - */ - 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 - */ - 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 - */ - 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'), - ); - } } diff --git a/src/Controller/HealthController.php b/src/Controller/HealthController.php new file mode 100644 index 0000000..17a9af5 --- /dev/null +++ b/src/Controller/HealthController.php @@ -0,0 +1,19 @@ + + */ + public function show(): array + { + return [ + 'status' => 'ok', + 'service' => 'merchant-ai-api', + ]; + } +} diff --git a/src/Controller/StatsController.php b/src/Controller/StatsController.php new file mode 100644 index 0000000..00f4b31 --- /dev/null +++ b/src/Controller/StatsController.php @@ -0,0 +1,39 @@ + + */ + public function orderSummary(HttpRequest $request): array + { + return $this->statsService->getOrderSummary($request); + } + + /** + * @return array + */ + public function offlineStats(HttpRequest $request): array + { + return $this->statsService->getOfflineStats($request); + } + + /** + * @return array + */ + public function merchantRanking(HttpRequest $request): array + { + return $this->statsService->getMerchantRanking($request); + } +} diff --git a/src/Http/HttpRequest.php b/src/Http/HttpRequest.php index 5c39bac..262e182 100644 --- a/src/Http/HttpRequest.php +++ b/src/Http/HttpRequest.php @@ -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)) { diff --git a/src/Service/StatsService.php b/src/Service/StatsService.php new file mode 100644 index 0000000..67f0923 --- /dev/null +++ b/src/Service/StatsService.php @@ -0,0 +1,70 @@ + + */ + 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 + */ + 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 + */ + 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'), + ); + } +} diff --git a/src/Tools/AnalyzeOfflineOrdersTool.php b/src/Tools/AnalyzeOfflineOrdersTool.php index cb5b95f..98096bd 100644 --- a/src/Tools/AnalyzeOfflineOrdersTool.php +++ b/src/Tools/AnalyzeOfflineOrdersTool.php @@ -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 = '分析线下订单的状态、支付方式、订单类型和金额分布。';