初始化

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} !-d
RewriteRule ^ server.php [QSA,L]
RewriteRule ^ public/index.php [QSA,L]

View File

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

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);
require __DIR__ . '/vendor/autoload.php';
use App\Api\ApiFactory;
use App\Http\HttpRequest;
ApiFactory::create()->handle(HttpRequest::fromGlobals());
require __DIR__ . '/public/index.php';

View File

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

View File

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

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'));
$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)) {

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
{
public const NAME = 'analyze_offline_orders';
public const NAME = 'get_offline_stats';
public const DESCRIPTION = '分析线下订单的状态、支付方式、订单类型和金额分布。';