初始化

This commit is contained in:
1173117610@qq.com 2026-06-23 16:26:06 +08:00
parent 3b4674ed92
commit 8484851bd9
11 changed files with 197 additions and 76 deletions

View File

@ -2,4 +2,4 @@ RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ server.php [QSA,L] RewriteRule ^ public/index.php [QSA,L]

View File

@ -15,7 +15,7 @@ MERCHANT_API_TIMEOUT=10
## Start Locally ## Start Locally
```bash ```bash
php -S 127.0.0.1:8080 server.php php -S 127.0.0.1:8080 -t public
``` ```
## Response Format ## Response Format
@ -44,19 +44,23 @@ Error:
## Endpoints ## Endpoints
### `GET|POST /tools` ### `GET /health`
Returns service health status.
### `GET /tools`
Returns available API tools and input schemas. 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. 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. 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`. Returns merchant ranking by `orderCount`, `successOrderCount`, `totalOrderAmount`, or `totalRealPay`.

10
public/index.php Normal file
View File

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

View File

@ -2,9 +2,4 @@
declare(strict_types=1); declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php'; require __DIR__ . '/public/index.php';
use App\Api\ApiFactory;
use App\Http\HttpRequest;
ApiFactory::create()->handle(HttpRequest::fromGlobals());

View File

@ -5,8 +5,11 @@ declare(strict_types=1);
namespace App\Api; namespace App\Api;
use App\Config\ApiConfig; use App\Config\ApiConfig;
use App\Controller\HealthController;
use App\Controller\StatsController;
use App\Http\MerchantApiClient; use App\Http\MerchantApiClient;
use App\Repositories\OfflineOrderRepository; use App\Repositories\OfflineOrderRepository;
use App\Service\StatsService;
use App\Tools\AnalyzeOfflineOrdersTool; use App\Tools\AnalyzeOfflineOrdersTool;
use App\Tools\MerchantRankingTool; use App\Tools\MerchantRankingTool;
use App\Tools\OrderSummaryTool; use App\Tools\OrderSummaryTool;
@ -17,11 +20,15 @@ final class ApiFactory
{ {
$apiClient = new MerchantApiClient(ApiConfig::fromEnvironment()); $apiClient = new MerchantApiClient(ApiConfig::fromEnvironment());
$orders = new OfflineOrderRepository($apiClient); $orders = new OfflineOrderRepository($apiClient);
$statsService = new StatsService(
return new ApiKernel(
new OrderSummaryTool($orders), new OrderSummaryTool($orders),
new AnalyzeOfflineOrdersTool($orders), new AnalyzeOfflineOrdersTool($orders),
new MerchantRankingTool($orders), new MerchantRankingTool($orders),
); );
return new ApiKernel(
new HealthController(),
new StatsController($statsService),
);
} }
} }

View File

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace App\Api; namespace App\Api;
use App\Controller\HealthController;
use App\Controller\StatsController;
use App\Http\HttpRequest; use App\Http\HttpRequest;
use App\Http\JsonResponse; use App\Http\JsonResponse;
use App\Tools\AnalyzeOfflineOrdersTool; use App\Tools\AnalyzeOfflineOrdersTool;
@ -13,9 +15,8 @@ use App\Tools\OrderSummaryTool;
final readonly class ApiKernel final readonly class ApiKernel
{ {
public function __construct( public function __construct(
private OrderSummaryTool $orderSummaryTool, private HealthController $healthController,
private AnalyzeOfflineOrdersTool $analyzeOfflineOrdersTool, private StatsController $statsController,
private MerchantRankingTool $merchantRankingTool,
) { ) {
} }
@ -23,11 +24,11 @@ final readonly class ApiKernel
{ {
try { try {
match ($request->path) { match ($request->path) {
'/', '/health' => $this->ok(['status' => 'ok', 'service' => 'merchant-ai-api']), '/', '/health' => $this->ok($this->healthController->show()),
'/tools' => $this->ok($this->tools()), '/tools' => $this->ok($this->tools()),
'/api/order-summary' => $this->ok($this->orderSummary($request)), '/api/order-summary' => $this->post($request, fn (): array => $this->statsController->orderSummary($request)),
'/api/offline-orders/analyze' => $this->ok($this->analyzeOfflineOrders($request)), '/api/offline-stats' => $this->post($request, fn (): array => $this->statsController->offlineStats($request)),
'/api/merchant-ranking' => $this->ok($this->merchantRanking($request)), '/api/merchant-ranking' => $this->post($request, fn (): array => $this->statsController->merchantRanking($request)),
default => JsonResponse::send([ default => JsonResponse::send([
'success' => false, 'success' => false,
'error' => [ '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 * @param array<string, mixed> $data
*/ */
@ -68,75 +89,25 @@ final readonly class ApiKernel
[ [
'name' => OrderSummaryTool::NAME, 'name' => OrderSummaryTool::NAME,
'description' => OrderSummaryTool::DESCRIPTION, 'description' => OrderSummaryTool::DESCRIPTION,
'method' => 'GET|POST', 'method' => 'POST',
'path' => '/api/order-summary', 'path' => '/api/order-summary',
'inputSchema' => OrderSummaryTool::inputSchema(), 'inputSchema' => OrderSummaryTool::inputSchema(),
], ],
[ [
'name' => AnalyzeOfflineOrdersTool::NAME, 'name' => AnalyzeOfflineOrdersTool::NAME,
'description' => AnalyzeOfflineOrdersTool::DESCRIPTION, 'description' => AnalyzeOfflineOrdersTool::DESCRIPTION,
'method' => 'GET|POST', 'method' => 'POST',
'path' => '/api/offline-orders/analyze', 'path' => '/api/offline-stats',
'inputSchema' => AnalyzeOfflineOrdersTool::inputSchema(), 'inputSchema' => AnalyzeOfflineOrdersTool::inputSchema(),
], ],
[ [
'name' => MerchantRankingTool::NAME, 'name' => MerchantRankingTool::NAME,
'description' => MerchantRankingTool::DESCRIPTION, 'description' => MerchantRankingTool::DESCRIPTION,
'method' => 'GET|POST', 'method' => 'POST',
'path' => '/api/merchant-ranking', 'path' => '/api/merchant-ranking',
'inputSchema' => MerchantRankingTool::inputSchema(), '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'),
);
}
} }

View File

@ -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',
];
}
}

View File

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

View File

@ -21,6 +21,12 @@ final readonly class HttpRequest
$method = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')); $method = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET'));
$uri = (string) ($_SERVER['REQUEST_URI'] ?? '/'); $uri = (string) ($_SERVER['REQUEST_URI'] ?? '/');
$path = parse_url($uri, PHP_URL_PATH) ?: '/'; $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; $input = $_GET;
if (in_array($method, ['POST', 'PUT', 'PATCH'], true)) { if (in_array($method, ['POST', 'PUT', 'PATCH'], true)) {

View File

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

View File

@ -9,7 +9,7 @@ use App\Support\OfflineOrderQuery;
final readonly class AnalyzeOfflineOrdersTool final readonly class AnalyzeOfflineOrdersTool
{ {
public const NAME = 'analyze_offline_orders'; public const NAME = 'get_offline_stats';
public const DESCRIPTION = '分析线下订单的状态、支付方式、订单类型和金额分布。'; public const DESCRIPTION = '分析线下订单的状态、支付方式、订单类型和金额分布。';