初始化
This commit is contained in:
parent
d836d7db1e
commit
7984fdc92f
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ClaudeCodeTabState">
|
||||
<option name="tabSessions">
|
||||
<map>
|
||||
<entry key="0">
|
||||
<value>
|
||||
<TabSessionState>
|
||||
<option name="provider" value="codex" />
|
||||
<option name="sessionId" value="019ef354-0d94-7a71-8df9-3e4860ac548a" />
|
||||
<option name="cwd" value="$PROJECT_DIR$" />
|
||||
<option name="model" value="gpt-5.5" />
|
||||
<option name="permissionMode" value="default" />
|
||||
<option name="reasoningEffort" value="high" />
|
||||
</TabSessionState>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -43,7 +43,7 @@
|
|||
<path value="$PROJECT_DIR$/vendor/guzzlehttp/guzzle" />
|
||||
</include_path>
|
||||
</component>
|
||||
<component name="PhpProjectSharedConfiguration" php_language_level="8.0">
|
||||
<component name="PhpProjectSharedConfiguration" php_language_level="8.2">
|
||||
<option name="suggestChangeDefaultLanguageLevel" value="false" />
|
||||
</component>
|
||||
<component name="PhpStanOptionsConfiguration">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"mcp/sdk": "^0.6.0",
|
||||
"guzzlehttp/guzzle": "^7.12"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "90853eb8e03275ba165d4875872d9169",
|
||||
"content-hash": "28614624427adcf2fb81f8d8c2311fff",
|
||||
"packages": [
|
||||
{
|
||||
"name": "doctrine/deprecations",
|
||||
|
|
@ -1868,7 +1868,9 @@
|
|||
"stability-flags": {},
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": {},
|
||||
"platform": {
|
||||
"php": "^8.2"
|
||||
},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
|
|
|||
139
server.php
139
server.php
|
|
@ -4,142 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use Mcp\Server;
|
||||
use App\Server\ServerFactory;
|
||||
use Mcp\Server\Transport\StdioTransport;
|
||||
|
||||
$token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwd2QiOiI2ODI5N2FjYzUwZDczYWQzZmNmMTNlM2Y4MWYxMWQyZSIsImlzcyI6InRwb2ludC5hZ3JpbWVkaWEuY24iLCJhdWQiOiJ0cG9pbnQuYWdyaW1lZGlhLmNuIiwiaWF0IjoxNzgwODk3Mjk3LCJuYmYiOjE3ODA4OTcyOTcsImV4cCI6MTc4MzQwMjg5NywianRpIjp7ImlkIjo0LCJ0eXBlIjoiYWRtaW4ifX0.G2yB38bYkf0b37kpAbQViDuXY4QHeRjm99WNXjSh4L0';
|
||||
|
||||
$server = Server::builder()
|
||||
->setServerInfo('Merchant MCP', '1.0.0')
|
||||
->addTool(
|
||||
handler: function (
|
||||
?string $date = null,
|
||||
?int $status = null,
|
||||
?string $keyword = null,
|
||||
?string $fieldKey = null,
|
||||
?string $payType = null,
|
||||
?string $type = null,
|
||||
?int $btcId = null,
|
||||
) use ($token): array {
|
||||
$client = new Client([
|
||||
'timeout' => 10,
|
||||
]);
|
||||
|
||||
// page、limit 由 MCP 内部固定,不开放给 AI 配置
|
||||
$query = [
|
||||
'status' => $status ?? -1,
|
||||
'field_key' => $fieldKey ?? 'all',
|
||||
'page' => 1,
|
||||
'limit' => 100,
|
||||
'time' => $date ?? date('Y/m/d') . '-' . date('Y/m/d'),
|
||||
];
|
||||
|
||||
// 仅在传入非空值时追加可选筛选条件
|
||||
// 注意:API 搜索关键词参数是 real_name,不是 data
|
||||
if ($keyword !== null && $keyword !== '') {
|
||||
$query['real_name'] = $keyword;
|
||||
}
|
||||
if ($payType !== null && $payType !== '') {
|
||||
$query['pay_type'] = $payType;
|
||||
}
|
||||
if ($type !== null && $type !== '') {
|
||||
$query['type'] = $type;
|
||||
}
|
||||
if ($btcId !== null && $btcId > 0) {
|
||||
$query['btc_id'] = $btcId;
|
||||
}
|
||||
|
||||
$response = $client->get(
|
||||
'https://tpoint.agrimedia.cn/adminapi/offline/list',
|
||||
[
|
||||
'headers' => [
|
||||
'accept' => 'application/json',
|
||||
'authori-zation' => 'Bearer ' . $token,
|
||||
],
|
||||
'query' => $query,
|
||||
]
|
||||
);
|
||||
|
||||
$data = json_decode(
|
||||
(string) $response->getBody(),
|
||||
true
|
||||
);
|
||||
|
||||
$orders = $data['data']['list'] ?? [];
|
||||
|
||||
$activeMerchants = [];
|
||||
$successOrderCount = 0;
|
||||
$totalOrderAmount = 0.0;
|
||||
$totalRealPay = 0.0;
|
||||
|
||||
foreach ($orders as $order) {
|
||||
$merchantId = (int) ($order['merchant_id'] ?? 0);
|
||||
|
||||
if ($merchantId > 0) {
|
||||
$activeMerchants[$merchantId] = true;
|
||||
}
|
||||
|
||||
/*
|
||||
* 这里暂时按 status = 1 作为成功订单。
|
||||
* 后面再确认你项目的真实状态含义。
|
||||
*/
|
||||
if ((int) ($order['status'] ?? -1) === 1) {
|
||||
$successOrderCount++;
|
||||
}
|
||||
|
||||
$totalOrderAmount += (float) ($order['order_price'] ?? 0);
|
||||
$totalRealPay += (float) ($order['real_pay'] ?? 0);
|
||||
}
|
||||
|
||||
return [
|
||||
'orderCount' => count($orders),
|
||||
'activeMerchantCount' => count($activeMerchants),
|
||||
'successOrderCount' => $successOrderCount,
|
||||
'totalOrderAmount' => round($totalOrderAmount, 2),
|
||||
'totalRealPay' => round($totalRealPay, 2),
|
||||
// 调试信息:实际发送给 API 的查询参数
|
||||
'_debug_query' => $query,
|
||||
];
|
||||
},
|
||||
name: 'get_order_summary',
|
||||
description: '获取订单总数、活跃商户数、成功订单数和金额统计。可按日期、状态、关键词、搜索字段、支付方式、订单类型、商户分类ID筛选。',
|
||||
inputSchema: [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'date' => [
|
||||
'type' => 'string',
|
||||
'description' => '查询日期,格式如 2026/06/23-2026/06/23(起止日期用 - 连接)。不传默认当天。',
|
||||
],
|
||||
'status' => [
|
||||
'type' => 'integer',
|
||||
'description' => '订单状态,-1 表示全部。不传默认 -1。',
|
||||
],
|
||||
'fieldKey' => [
|
||||
'type' => 'string',
|
||||
'description' => '搜索字段,对应接口的 field_key 参数。常用值:order_sn(按订单号搜索)、all(全部字段)。不传默认 all。注意:当 keyword 有值时,建议同时指定 fieldKey 以确保搜索生效。',
|
||||
],
|
||||
'keyword' => [
|
||||
'type' => 'string',
|
||||
'description' => '搜索关键词,对应接口的 data 参数。需配合 fieldKey 一起使用。',
|
||||
],
|
||||
'payType' => [
|
||||
'type' => 'string',
|
||||
'description' => '支付方式,对应接口的 pay_type 参数。',
|
||||
],
|
||||
'type' => [
|
||||
'type' => 'string',
|
||||
'description' => '订单类型,对应接口的 type 参数。',
|
||||
],
|
||||
'btcId' => [
|
||||
'type' => 'integer',
|
||||
'description' => '商户分类ID,对应接口的 btc_id 参数。',
|
||||
],
|
||||
],
|
||||
],
|
||||
)
|
||||
->build();
|
||||
|
||||
$transport = new StdioTransport();
|
||||
|
||||
$server->run($transport);
|
||||
ServerFactory::create()->run(new StdioTransport());
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Config;
|
||||
|
||||
final readonly class ApiConfig
|
||||
{
|
||||
public function __construct(
|
||||
public string $baseUri,
|
||||
public string $token,
|
||||
public int $timeout = 10,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromEnvironment(): self
|
||||
{
|
||||
$baseUri = getenv('MERCHANT_API_BASE_URI') ?: 'https://tpoint.agrimedia.cn';
|
||||
$token = getenv('MERCHANT_API_TOKEN') ?: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwd2QiOiI2ODI5N2FjYzUwZDczYWQzZmNmMTNlM2Y4MWYxMWQyZSIsImlzcyI6InRwb2ludC5hZ3JpbWVkaWEuY24iLCJhdWQiOiJ0cG9pbnQuYWdyaW1lZGlhLmNuIiwiaWF0IjoxNzgwODk3Mjk3LCJuYmYiOjE3ODA4OTcyOTcsImV4cCI6MTc4MzQwMjg5NywianRpIjp7ImlkIjo0LCJ0eXBlIjoiYWRtaW4ifX0.G2yB38bYkf0b37kpAbQViDuXY4QHeRjm99WNXjSh4L0';
|
||||
$timeout = (int) (getenv('MERCHANT_API_TIMEOUT') ?: 10);
|
||||
|
||||
if ($token === '') {
|
||||
throw new \RuntimeException('Missing MERCHANT_API_TOKEN environment variable.');
|
||||
}
|
||||
|
||||
return new self(
|
||||
baseUri: rtrim($baseUri, '/'),
|
||||
token: $token,
|
||||
timeout: $timeout > 0 ? $timeout : 10,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
use Mcp\Capability\Attribute\McpTool;
|
||||
|
||||
final class DemoTools
|
||||
{
|
||||
#[McpTool(
|
||||
name: 'hello',
|
||||
description: '返回一段测试文字'
|
||||
)]
|
||||
public function hello(string $name = '朋友'): string
|
||||
{
|
||||
return "你好,{$name},PHP MCP Server 运行成功!";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http;
|
||||
|
||||
use App\Config\ApiConfig;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
|
||||
final class MerchantApiClient
|
||||
{
|
||||
private Client $client;
|
||||
|
||||
public function __construct(private readonly ApiConfig $config)
|
||||
{
|
||||
$this->client = new Client([
|
||||
'base_uri' => $this->config->baseUri,
|
||||
'timeout' => $this->config->timeout,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $query
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getOfflineOrders(array $query): array
|
||||
{
|
||||
try {
|
||||
$response = $this->client->get('/adminapi/offline/list', [
|
||||
'headers' => [
|
||||
'accept' => 'application/json',
|
||||
'authori-zation' => 'Bearer ' . $this->config->token,
|
||||
],
|
||||
'query' => $query,
|
||||
]);
|
||||
} catch (GuzzleException $exception) {
|
||||
throw new \RuntimeException('Merchant API request failed: ' . $exception->getMessage(), 0, $exception);
|
||||
}
|
||||
|
||||
$data = json_decode((string) $response->getBody(), true);
|
||||
|
||||
if (!is_array($data)) {
|
||||
throw new \RuntimeException('Merchant API returned invalid JSON.');
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Http\MerchantApiClient;
|
||||
|
||||
final readonly class OfflineOrderRepository
|
||||
{
|
||||
public function __construct(private MerchantApiClient $apiClient)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $query
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function list(array $query): array
|
||||
{
|
||||
$data = $this->apiClient->getOfflineOrders($query);
|
||||
$orders = $data['data']['list'] ?? [];
|
||||
|
||||
if (!is_array($orders)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values(array_filter($orders, is_array(...)));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Server;
|
||||
|
||||
use App\Config\ApiConfig;
|
||||
use App\Http\MerchantApiClient;
|
||||
use App\Repositories\OfflineOrderRepository;
|
||||
use App\Tools\AnalyzeOfflineOrdersTool;
|
||||
use App\Tools\MerchantRankingTool;
|
||||
use App\Tools\OrderSummaryTool;
|
||||
use App\Tools\ToolRegistrar;
|
||||
use Mcp\Server;
|
||||
|
||||
final class ServerFactory
|
||||
{
|
||||
public static function create(): Server
|
||||
{
|
||||
$apiClient = new MerchantApiClient(ApiConfig::fromEnvironment());
|
||||
$orders = new OfflineOrderRepository($apiClient);
|
||||
|
||||
$registrar = new ToolRegistrar(
|
||||
new OrderSummaryTool($orders),
|
||||
new AnalyzeOfflineOrdersTool($orders),
|
||||
new MerchantRankingTool($orders),
|
||||
);
|
||||
|
||||
$builder = Server::builder()
|
||||
->setServerInfo('Merchant MCP', '1.0.0');
|
||||
|
||||
return $registrar->register($builder)->build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
final class OfflineOrderQuery
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function build(
|
||||
?string $date = null,
|
||||
?int $status = null,
|
||||
?string $keyword = null,
|
||||
?string $fieldKey = null,
|
||||
?string $payType = null,
|
||||
?string $type = null,
|
||||
?int $btcId = null,
|
||||
int $page = 1,
|
||||
int $limit = 100,
|
||||
): array {
|
||||
$query = [
|
||||
'status' => $status ?? -1,
|
||||
'field_key' => self::filled($fieldKey) ? $fieldKey : 'all',
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
'time' => self::filled($date) ? $date : date('Y/m/d') . '-' . date('Y/m/d'),
|
||||
];
|
||||
|
||||
if (self::filled($keyword)) {
|
||||
$query['real_name'] = $keyword;
|
||||
}
|
||||
|
||||
if (self::filled($payType)) {
|
||||
$query['pay_type'] = $payType;
|
||||
}
|
||||
|
||||
if (self::filled($type)) {
|
||||
$query['type'] = $type;
|
||||
}
|
||||
|
||||
if ($btcId !== null && $btcId > 0) {
|
||||
$query['btc_id'] = $btcId;
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
private static function filled(?string $value): bool
|
||||
{
|
||||
return $value !== null && trim($value) !== '';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tools;
|
||||
|
||||
use App\Repositories\OfflineOrderRepository;
|
||||
use App\Support\OfflineOrderQuery;
|
||||
|
||||
final readonly class AnalyzeOfflineOrdersTool
|
||||
{
|
||||
public const NAME = 'analyze_offline_orders';
|
||||
|
||||
public const DESCRIPTION = '分析线下订单的状态、支付方式、订单类型和金额分布。';
|
||||
|
||||
public function __construct(private OfflineOrderRepository $orders)
|
||||
{
|
||||
}
|
||||
|
||||
public function __invoke(
|
||||
?string $date = null,
|
||||
?int $status = null,
|
||||
?string $keyword = null,
|
||||
?string $fieldKey = null,
|
||||
?string $payType = null,
|
||||
?string $type = null,
|
||||
?int $btcId = null,
|
||||
): array {
|
||||
$query = OfflineOrderQuery::build($date, $status, $keyword, $fieldKey, $payType, $type, $btcId);
|
||||
$orders = $this->orders->list($query);
|
||||
|
||||
$statusDistribution = [];
|
||||
$payTypeDistribution = [];
|
||||
$typeDistribution = [];
|
||||
$totalOrderAmount = 0.0;
|
||||
$totalRealPay = 0.0;
|
||||
$maxOrderAmount = 0.0;
|
||||
|
||||
foreach ($orders as $order) {
|
||||
$statusKey = (string) ($order['status'] ?? 'unknown');
|
||||
$payTypeKey = (string) ($order['pay_type'] ?? 'unknown');
|
||||
$typeKey = (string) ($order['type'] ?? 'unknown');
|
||||
$orderAmount = (float) ($order['order_price'] ?? 0);
|
||||
$realPay = (float) ($order['real_pay'] ?? 0);
|
||||
|
||||
$statusDistribution[$statusKey] = ($statusDistribution[$statusKey] ?? 0) + 1;
|
||||
$payTypeDistribution[$payTypeKey] = ($payTypeDistribution[$payTypeKey] ?? 0) + 1;
|
||||
$typeDistribution[$typeKey] = ($typeDistribution[$typeKey] ?? 0) + 1;
|
||||
$totalOrderAmount += $orderAmount;
|
||||
$totalRealPay += $realPay;
|
||||
$maxOrderAmount = max($maxOrderAmount, $orderAmount);
|
||||
}
|
||||
|
||||
$orderCount = count($orders);
|
||||
|
||||
return [
|
||||
'orderCount' => $orderCount,
|
||||
'statusDistribution' => $statusDistribution,
|
||||
'payTypeDistribution' => $payTypeDistribution,
|
||||
'typeDistribution' => $typeDistribution,
|
||||
'totalOrderAmount' => round($totalOrderAmount, 2),
|
||||
'totalRealPay' => round($totalRealPay, 2),
|
||||
'averageOrderAmount' => $orderCount > 0 ? round($totalOrderAmount / $orderCount, 2) : 0,
|
||||
'averageRealPay' => $orderCount > 0 ? round($totalRealPay / $orderCount, 2) : 0,
|
||||
'maxOrderAmount' => round($maxOrderAmount, 2),
|
||||
'_debug_query' => $query,
|
||||
];
|
||||
}
|
||||
|
||||
public static function inputSchema(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'properties' => OrderSummaryTool::filterProperties(),
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tools;
|
||||
|
||||
use App\Repositories\OfflineOrderRepository;
|
||||
use App\Support\OfflineOrderQuery;
|
||||
|
||||
final readonly class MerchantRankingTool
|
||||
{
|
||||
public const NAME = 'get_merchant_ranking';
|
||||
|
||||
public const DESCRIPTION = '按订单数、订单金额或实付金额生成商户排行榜。';
|
||||
|
||||
public function __construct(private OfflineOrderRepository $orders)
|
||||
{
|
||||
}
|
||||
|
||||
public function __invoke(
|
||||
?string $date = null,
|
||||
?int $status = null,
|
||||
?string $keyword = null,
|
||||
?string $fieldKey = null,
|
||||
?string $payType = null,
|
||||
?string $type = null,
|
||||
?int $btcId = null,
|
||||
?string $sortBy = null,
|
||||
?int $limit = null,
|
||||
): array {
|
||||
$query = OfflineOrderQuery::build($date, $status, $keyword, $fieldKey, $payType, $type, $btcId);
|
||||
$orders = $this->orders->list($query);
|
||||
$ranking = [];
|
||||
|
||||
foreach ($orders as $order) {
|
||||
$merchantId = (int) ($order['merchant_id'] ?? 0);
|
||||
|
||||
if ($merchantId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($ranking[$merchantId])) {
|
||||
$ranking[$merchantId] = [
|
||||
'merchantId' => $merchantId,
|
||||
'merchantName' => (string) ($order['merchant_name'] ?? $order['mer_name'] ?? ''),
|
||||
'orderCount' => 0,
|
||||
'successOrderCount' => 0,
|
||||
'totalOrderAmount' => 0.0,
|
||||
'totalRealPay' => 0.0,
|
||||
];
|
||||
}
|
||||
|
||||
$ranking[$merchantId]['orderCount']++;
|
||||
$ranking[$merchantId]['totalOrderAmount'] += (float) ($order['order_price'] ?? 0);
|
||||
$ranking[$merchantId]['totalRealPay'] += (float) ($order['real_pay'] ?? 0);
|
||||
|
||||
if ((int) ($order['status'] ?? -1) === 1) {
|
||||
$ranking[$merchantId]['successOrderCount']++;
|
||||
}
|
||||
}
|
||||
|
||||
$items = array_values($ranking);
|
||||
$sortField = $this->normalizeSortField($sortBy);
|
||||
|
||||
usort($items, static function (array $left, array $right) use ($sortField): int {
|
||||
return $right[$sortField] <=> $left[$sortField];
|
||||
});
|
||||
|
||||
$maxItems = $limit !== null && $limit > 0 ? min($limit, 100) : 10;
|
||||
$items = array_slice($items, 0, $maxItems);
|
||||
|
||||
foreach ($items as $index => $item) {
|
||||
$items[$index]['totalOrderAmount'] = round((float) $item['totalOrderAmount'], 2);
|
||||
$items[$index]['totalRealPay'] = round((float) $item['totalRealPay'], 2);
|
||||
}
|
||||
|
||||
return [
|
||||
'sortBy' => $sortField,
|
||||
'ranking' => $items,
|
||||
'_debug_query' => $query,
|
||||
];
|
||||
}
|
||||
|
||||
public static function inputSchema(): array
|
||||
{
|
||||
$properties = OrderSummaryTool::filterProperties();
|
||||
$properties['sortBy'] = [
|
||||
'type' => 'string',
|
||||
'description' => '排序字段:orderCount、successOrderCount、totalOrderAmount、totalRealPay。不传默认 totalRealPay。',
|
||||
];
|
||||
$properties['limit'] = [
|
||||
'type' => 'integer',
|
||||
'description' => '返回商户数量,默认 10,最大 100。',
|
||||
];
|
||||
|
||||
return [
|
||||
'type' => 'object',
|
||||
'properties' => $properties,
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizeSortField(?string $sortBy): string
|
||||
{
|
||||
$allowed = ['orderCount', 'successOrderCount', 'totalOrderAmount', 'totalRealPay'];
|
||||
|
||||
if ($sortBy !== null && in_array($sortBy, $allowed, true)) {
|
||||
return $sortBy;
|
||||
}
|
||||
|
||||
return 'totalRealPay';
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tools;
|
||||
|
||||
use App\Repositories\OfflineOrderRepository;
|
||||
use App\Support\OfflineOrderQuery;
|
||||
|
||||
final readonly class OrderSummaryTool
|
||||
{
|
||||
public const NAME = 'get_order_summary';
|
||||
|
||||
public const DESCRIPTION = '获取订单总数、活跃商户数、成功订单数和金额统计。可按日期、状态、关键词、搜索字段、支付方式、订单类型、商户分类ID筛选。';
|
||||
|
||||
public function __construct(private OfflineOrderRepository $orders)
|
||||
{
|
||||
}
|
||||
|
||||
public function __invoke(
|
||||
?string $date = null,
|
||||
?int $status = null,
|
||||
?string $keyword = null,
|
||||
?string $fieldKey = null,
|
||||
?string $payType = null,
|
||||
?string $type = null,
|
||||
?int $btcId = null,
|
||||
): array {
|
||||
$query = OfflineOrderQuery::build($date, $status, $keyword, $fieldKey, $payType, $type, $btcId);
|
||||
$orders = $this->orders->list($query);
|
||||
|
||||
$activeMerchants = [];
|
||||
$successOrderCount = 0;
|
||||
$totalOrderAmount = 0.0;
|
||||
$totalRealPay = 0.0;
|
||||
|
||||
foreach ($orders as $order) {
|
||||
$merchantId = (int) ($order['merchant_id'] ?? 0);
|
||||
|
||||
if ($merchantId > 0) {
|
||||
$activeMerchants[$merchantId] = true;
|
||||
}
|
||||
|
||||
if ((int) ($order['status'] ?? -1) === 1) {
|
||||
$successOrderCount++;
|
||||
}
|
||||
|
||||
$totalOrderAmount += (float) ($order['order_price'] ?? 0);
|
||||
$totalRealPay += (float) ($order['real_pay'] ?? 0);
|
||||
}
|
||||
|
||||
return [
|
||||
'orderCount' => count($orders),
|
||||
'activeMerchantCount' => count($activeMerchants),
|
||||
'successOrderCount' => $successOrderCount,
|
||||
'totalOrderAmount' => round($totalOrderAmount, 2),
|
||||
'totalRealPay' => round($totalRealPay, 2),
|
||||
'_debug_query' => $query,
|
||||
];
|
||||
}
|
||||
|
||||
public static function inputSchema(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'properties' => self::filterProperties(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
public static function filterProperties(): array
|
||||
{
|
||||
return [
|
||||
'date' => [
|
||||
'type' => 'string',
|
||||
'description' => '查询日期,格式如 2026/06/23-2026/06/23(起止日期用 - 连接)。不传默认当天。',
|
||||
],
|
||||
'status' => [
|
||||
'type' => 'integer',
|
||||
'description' => '订单状态,-1 表示全部。不传默认 -1。',
|
||||
],
|
||||
'fieldKey' => [
|
||||
'type' => 'string',
|
||||
'description' => '搜索字段,对应接口的 field_key 参数。常用值:order_sn(按订单号搜索)、all(全部字段)。不传默认 all。',
|
||||
],
|
||||
'keyword' => [
|
||||
'type' => 'string',
|
||||
'description' => '搜索关键词。当前接口按 real_name 参数传递。',
|
||||
],
|
||||
'payType' => [
|
||||
'type' => 'string',
|
||||
'description' => '支付方式,对应接口的 pay_type 参数。',
|
||||
],
|
||||
'type' => [
|
||||
'type' => 'string',
|
||||
'description' => '订单类型,对应接口的 type 参数。',
|
||||
],
|
||||
'btcId' => [
|
||||
'type' => 'integer',
|
||||
'description' => '商户分类ID,对应接口的 btc_id 参数。',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tools;
|
||||
|
||||
use Mcp\Server\Builder;
|
||||
|
||||
final readonly class ToolRegistrar
|
||||
{
|
||||
public function __construct(
|
||||
private OrderSummaryTool $orderSummaryTool,
|
||||
private AnalyzeOfflineOrdersTool $analyzeOfflineOrdersTool,
|
||||
private MerchantRankingTool $merchantRankingTool,
|
||||
) {
|
||||
}
|
||||
|
||||
public function register(Builder $builder): Builder
|
||||
{
|
||||
return $builder
|
||||
->addTool(
|
||||
handler: $this->orderSummaryHandler(),
|
||||
name: OrderSummaryTool::NAME,
|
||||
description: OrderSummaryTool::DESCRIPTION,
|
||||
inputSchema: OrderSummaryTool::inputSchema(),
|
||||
)
|
||||
->addTool(
|
||||
handler: $this->analyzeOfflineOrdersHandler(),
|
||||
name: AnalyzeOfflineOrdersTool::NAME,
|
||||
description: AnalyzeOfflineOrdersTool::DESCRIPTION,
|
||||
inputSchema: AnalyzeOfflineOrdersTool::inputSchema(),
|
||||
)
|
||||
->addTool(
|
||||
handler: $this->merchantRankingHandler(),
|
||||
name: MerchantRankingTool::NAME,
|
||||
description: MerchantRankingTool::DESCRIPTION,
|
||||
inputSchema: MerchantRankingTool::inputSchema(),
|
||||
);
|
||||
}
|
||||
|
||||
private function orderSummaryHandler(): \Closure
|
||||
{
|
||||
return fn (
|
||||
?string $date = null,
|
||||
?int $status = null,
|
||||
?string $keyword = null,
|
||||
?string $fieldKey = null,
|
||||
?string $payType = null,
|
||||
?string $type = null,
|
||||
?int $btcId = null,
|
||||
): array => ($this->orderSummaryTool)($date, $status, $keyword, $fieldKey, $payType, $type, $btcId);
|
||||
}
|
||||
|
||||
private function analyzeOfflineOrdersHandler(): \Closure
|
||||
{
|
||||
return fn (
|
||||
?string $date = null,
|
||||
?int $status = null,
|
||||
?string $keyword = null,
|
||||
?string $fieldKey = null,
|
||||
?string $payType = null,
|
||||
?string $type = null,
|
||||
?int $btcId = null,
|
||||
): array => ($this->analyzeOfflineOrdersTool)($date, $status, $keyword, $fieldKey, $payType, $type, $btcId);
|
||||
}
|
||||
|
||||
private function merchantRankingHandler(): \Closure
|
||||
{
|
||||
return fn (
|
||||
?string $date = null,
|
||||
?int $status = null,
|
||||
?string $keyword = null,
|
||||
?string $fieldKey = null,
|
||||
?string $payType = null,
|
||||
?string $type = null,
|
||||
?int $btcId = null,
|
||||
?string $sortBy = null,
|
||||
?int $limit = null,
|
||||
): array => ($this->merchantRankingTool)($date, $status, $keyword, $fieldKey, $payType, $type, $btcId, $sortBy, $limit);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue