初始化

This commit is contained in:
1173117610@qq.com 2026-06-23 15:57:06 +08:00
parent d836d7db1e
commit 7984fdc92f
15 changed files with 607 additions and 159 deletions

View File

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

View File

@ -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">

View File

@ -1,5 +1,6 @@
{
"require": {
"php": "^8.2",
"mcp/sdk": "^0.6.0",
"guzzlehttp/guzzle": "^7.12"
},

6
composer.lock generated
View File

@ -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"
}

View File

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

32
src/Config/ApiConfig.php Normal file
View File

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

View File

@ -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 运行成功!";
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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