From 7984fdc92f51bf7a1b1cac42da4ec23fb57ccf8d Mon Sep 17 00:00:00 2001 From: "1173117610@qq.com" Date: Tue, 23 Jun 2026 15:57:06 +0800 Subject: [PATCH] =?UTF-8?q?=20=E5=88=9D=E5=A7=8B=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/claudeCodeTabState.xml | 21 +++ .idea/php.xml | 2 +- composer.json | 1 + composer.lock | 6 +- server.php | 139 +------------------- src/Config/ApiConfig.php | 32 +++++ src/DemoTools.php | 19 --- src/Http/MerchantApiClient.php | 49 +++++++ src/Repositories/OfflineOrderRepository.php | 30 +++++ src/Server/ServerFactory.php | 34 +++++ src/Support/OfflineOrderQuery.php | 54 ++++++++ src/Tools/AnalyzeOfflineOrdersTool.php | 78 +++++++++++ src/Tools/MerchantRankingTool.php | 113 ++++++++++++++++ src/Tools/OrderSummaryTool.php | 107 +++++++++++++++ src/Tools/ToolRegistrar.php | 81 ++++++++++++ 15 files changed, 607 insertions(+), 159 deletions(-) create mode 100644 .idea/claudeCodeTabState.xml create mode 100644 src/Config/ApiConfig.php delete mode 100644 src/DemoTools.php create mode 100644 src/Http/MerchantApiClient.php create mode 100644 src/Repositories/OfflineOrderRepository.php create mode 100644 src/Server/ServerFactory.php create mode 100644 src/Support/OfflineOrderQuery.php create mode 100644 src/Tools/AnalyzeOfflineOrdersTool.php create mode 100644 src/Tools/MerchantRankingTool.php create mode 100644 src/Tools/OrderSummaryTool.php create mode 100644 src/Tools/ToolRegistrar.php diff --git a/.idea/claudeCodeTabState.xml b/.idea/claudeCodeTabState.xml new file mode 100644 index 0000000..be38166 --- /dev/null +++ b/.idea/claudeCodeTabState.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml index 4c1f8cf..3e486c0 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -43,7 +43,7 @@ - + diff --git a/composer.json b/composer.json index cbf4714..50ae81a 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,6 @@ { "require": { + "php": "^8.2", "mcp/sdk": "^0.6.0", "guzzlehttp/guzzle": "^7.12" }, diff --git a/composer.lock b/composer.lock index d22e5cb..37817c7 100644 --- a/composer.lock +++ b/composer.lock @@ -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" } diff --git a/server.php b/server.php index df7168f..7e793ec 100644 --- a/server.php +++ b/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); \ No newline at end of file +ServerFactory::create()->run(new StdioTransport()); diff --git a/src/Config/ApiConfig.php b/src/Config/ApiConfig.php new file mode 100644 index 0000000..7241d8b --- /dev/null +++ b/src/Config/ApiConfig.php @@ -0,0 +1,32 @@ + 0 ? $timeout : 10, + ); + } +} diff --git a/src/DemoTools.php b/src/DemoTools.php deleted file mode 100644 index 1267a0e..0000000 --- a/src/DemoTools.php +++ /dev/null @@ -1,19 +0,0 @@ -client = new Client([ + 'base_uri' => $this->config->baseUri, + 'timeout' => $this->config->timeout, + ]); + } + + /** + * @param array $query + * @return array + */ + 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; + } +} diff --git a/src/Repositories/OfflineOrderRepository.php b/src/Repositories/OfflineOrderRepository.php new file mode 100644 index 0000000..894181f --- /dev/null +++ b/src/Repositories/OfflineOrderRepository.php @@ -0,0 +1,30 @@ + $query + * @return list> + */ + 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(...))); + } +} diff --git a/src/Server/ServerFactory.php b/src/Server/ServerFactory.php new file mode 100644 index 0000000..25c902b --- /dev/null +++ b/src/Server/ServerFactory.php @@ -0,0 +1,34 @@ +setServerInfo('Merchant MCP', '1.0.0'); + + return $registrar->register($builder)->build(); + } +} diff --git a/src/Support/OfflineOrderQuery.php b/src/Support/OfflineOrderQuery.php new file mode 100644 index 0000000..ad1c1d3 --- /dev/null +++ b/src/Support/OfflineOrderQuery.php @@ -0,0 +1,54 @@ + + */ + 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) !== ''; + } +} diff --git a/src/Tools/AnalyzeOfflineOrdersTool.php b/src/Tools/AnalyzeOfflineOrdersTool.php new file mode 100644 index 0000000..cb5b95f --- /dev/null +++ b/src/Tools/AnalyzeOfflineOrdersTool.php @@ -0,0 +1,78 @@ +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(), + ]; + } + +} diff --git a/src/Tools/MerchantRankingTool.php b/src/Tools/MerchantRankingTool.php new file mode 100644 index 0000000..a843a29 --- /dev/null +++ b/src/Tools/MerchantRankingTool.php @@ -0,0 +1,113 @@ +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'; + } + +} diff --git a/src/Tools/OrderSummaryTool.php b/src/Tools/OrderSummaryTool.php new file mode 100644 index 0000000..a6fa53e --- /dev/null +++ b/src/Tools/OrderSummaryTool.php @@ -0,0 +1,107 @@ +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> + */ + 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 参数。', + ], + ]; + } + +} diff --git a/src/Tools/ToolRegistrar.php b/src/Tools/ToolRegistrar.php new file mode 100644 index 0000000..599672b --- /dev/null +++ b/src/Tools/ToolRegistrar.php @@ -0,0 +1,81 @@ +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); + } +}