初始化
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" />
|
<path value="$PROJECT_DIR$/vendor/guzzlehttp/guzzle" />
|
||||||
</include_path>
|
</include_path>
|
||||||
</component>
|
</component>
|
||||||
<component name="PhpProjectSharedConfiguration" php_language_level="8.0">
|
<component name="PhpProjectSharedConfiguration" php_language_level="8.2">
|
||||||
<option name="suggestChangeDefaultLanguageLevel" value="false" />
|
<option name="suggestChangeDefaultLanguageLevel" value="false" />
|
||||||
</component>
|
</component>
|
||||||
<component name="PhpStanOptionsConfiguration">
|
<component name="PhpStanOptionsConfiguration">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"require": {
|
"require": {
|
||||||
|
"php": "^8.2",
|
||||||
"mcp/sdk": "^0.6.0",
|
"mcp/sdk": "^0.6.0",
|
||||||
"guzzlehttp/guzzle": "^7.12"
|
"guzzlehttp/guzzle": "^7.12"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "90853eb8e03275ba165d4875872d9169",
|
"content-hash": "28614624427adcf2fb81f8d8c2311fff",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "doctrine/deprecations",
|
"name": "doctrine/deprecations",
|
||||||
|
|
@ -1868,7 +1868,9 @@
|
||||||
"stability-flags": {},
|
"stability-flags": {},
|
||||||
"prefer-stable": false,
|
"prefer-stable": false,
|
||||||
"prefer-lowest": false,
|
"prefer-lowest": false,
|
||||||
"platform": {},
|
"platform": {
|
||||||
|
"php": "^8.2"
|
||||||
|
},
|
||||||
"platform-dev": {},
|
"platform-dev": {},
|
||||||
"plugin-api-version": "2.6.0"
|
"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';
|
require __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
use GuzzleHttp\Client;
|
use App\Server\ServerFactory;
|
||||||
use Mcp\Server;
|
|
||||||
use Mcp\Server\Transport\StdioTransport;
|
use Mcp\Server\Transport\StdioTransport;
|
||||||
|
|
||||||
$token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwd2QiOiI2ODI5N2FjYzUwZDczYWQzZmNmMTNlM2Y4MWYxMWQyZSIsImlzcyI6InRwb2ludC5hZ3JpbWVkaWEuY24iLCJhdWQiOiJ0cG9pbnQuYWdyaW1lZGlhLmNuIiwiaWF0IjoxNzgwODk3Mjk3LCJuYmYiOjE3ODA4OTcyOTcsImV4cCI6MTc4MzQwMjg5NywianRpIjp7ImlkIjo0LCJ0eXBlIjoiYWRtaW4ifX0.G2yB38bYkf0b37kpAbQViDuXY4QHeRjm99WNXjSh4L0';
|
ServerFactory::create()->run(new StdioTransport());
|
||||||
|
|
||||||
$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);
|
|
||||||
|
|
|
||||||
|
|
@ -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