Compare commits

..

10 Commits

Author SHA1 Message Date
1173117610@qq.com 6d5191b5e8 添加其他字段 2026-06-29 13:51:33 +08:00
1173117610@qq.com b5c68be1a6 添加其他字段 2026-06-29 13:14:34 +08:00
1173117610@qq.com c3ed03c92f 添加其他字段 2026-06-26 16:34:34 +08:00
1173117610@qq.com 511fffe26a 初始化 2026-06-26 15:38:07 +08:00
1173117610@qq.com 1019d0b4fc 初始化 2026-06-24 16:14:15 +08:00
1173117610@qq.com 404260f9d7 初始化 2026-06-24 14:35:56 +08:00
1173117610@qq.com 8484851bd9 初始化 2026-06-23 16:26:06 +08:00
1173117610@qq.com 3b4674ed92 初始化 2026-06-23 16:08:46 +08:00
1173117610@qq.com 7984fdc92f 初始化 2026-06-23 15:57:06 +08:00
1173117610@qq.com d836d7db1e 2026-06-23 15:03:54 +08:00
55 changed files with 2202 additions and 2862 deletions

5
.env.example Normal file
View File

@ -0,0 +1,5 @@
MERCHANT_API_BASE_URI=https://tpoint.agrimedia.cn
MERCHANT_API_TOKEN=your-admin-api-token
MERCHANT_API_TIMEOUT=60
OFFLINE_ORDER_PAGE_LIMIT=100

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.env
vendor/
.idea/

5
.htaccess Normal file
View File

@ -0,0 +1,5 @@
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ public/index.php [QSA,L]

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

@ -4,34 +4,15 @@
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" packagePrefix="App\" />
<excludeFolder url="file://$MODULE_DIR$/vendor/composer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/deprecations" />
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/guzzle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/promises" />
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/psr7" />
<excludeFolder url="file://$MODULE_DIR$/vendor/mcp/sdk" />
<excludeFolder url="file://$MODULE_DIR$/vendor/opis/json-schema" />
<excludeFolder url="file://$MODULE_DIR$/vendor/opis/string" />
<excludeFolder url="file://$MODULE_DIR$/vendor/opis/uri" />
<excludeFolder url="file://$MODULE_DIR$/vendor/php-http/discovery" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpdocumentor/reflection-common" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpdocumentor/reflection-docblock" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpdocumentor/type-resolver" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpstan/phpdoc-parser" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/clock" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/container" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/event-dispatcher" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-client" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-factory" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-message" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-server-handler" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-server-middleware" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/log" />
<excludeFolder url="file://$MODULE_DIR$/vendor/ralouphie/getallheaders" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/deprecation-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php80" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-uuid" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/uid" />
<excludeFolder url="file://$MODULE_DIR$/vendor/webmozart/assert" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />

View File

@ -12,29 +12,10 @@
</component>
<component name="PhpIncludePathManager">
<include_path>
<path value="$PROJECT_DIR$/vendor/psr/clock" />
<path value="$PROJECT_DIR$/vendor/mcp/sdk" />
<path value="$PROJECT_DIR$/vendor/psr/http-factory" />
<path value="$PROJECT_DIR$/vendor/psr/http-server-handler" />
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-common" />
<path value="$PROJECT_DIR$/vendor/php-http/discovery" />
<path value="$PROJECT_DIR$/vendor/psr/log" />
<path value="$PROJECT_DIR$/vendor/opis/uri" />
<path value="$PROJECT_DIR$/vendor/psr/http-server-middleware" />
<path value="$PROJECT_DIR$/vendor/opis/string" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-uuid" />
<path value="$PROJECT_DIR$/vendor/composer" />
<path value="$PROJECT_DIR$/vendor/opis/json-schema" />
<path value="$PROJECT_DIR$/vendor/psr/http-client" />
<path value="$PROJECT_DIR$/vendor/psr/container" />
<path value="$PROJECT_DIR$/vendor/psr/event-dispatcher" />
<path value="$PROJECT_DIR$/vendor/psr/http-message" />
<path value="$PROJECT_DIR$/vendor/phpstan/phpdoc-parser" />
<path value="$PROJECT_DIR$/vendor/symfony/uid" />
<path value="$PROJECT_DIR$/vendor/doctrine/deprecations" />
<path value="$PROJECT_DIR$/vendor/webmozart/assert" />
<path value="$PROJECT_DIR$/vendor/phpdocumentor/type-resolver" />
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-docblock" />
<path value="$PROJECT_DIR$/vendor/ralouphie/getallheaders" />
<path value="$PROJECT_DIR$/vendor/guzzlehttp/promises" />
<path value="$PROJECT_DIR$/vendor/guzzlehttp/psr7" />
@ -43,7 +24,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,4 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings" defaultProject="true" />
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

97
README.md Normal file
View File

@ -0,0 +1,97 @@
# Merchant AI HTTP API
PHP 8.2 JSON API service for merchant order statistics, offline order analysis, and merchant ranking.
## Environment
Copy `.env.example` to `.env`, then fill your real token:
```bash
MERCHANT_API_TOKEN=your-admin-api-token
MERCHANT_API_BASE_URI=https://tpoint.agrimedia.cn
MERCHANT_API_TIMEOUT=60
OFFLINE_ORDER_PAGE_LIMIT=100
```
`MERCHANT_API_TOKEN` is required. `MERCHANT_API_BASE_URI` and `MERCHANT_API_TIMEOUT` are optional.
`OFFLINE_ORDER_PAGE_LIMIT` controls each upstream pagination request size.
If the request contains `Authorization: Bearer <token>`, that token is used for the upstream merchant API request first. If the header is missing, `MERCHANT_API_TOKEN` from `.env` is used.
## Start Locally
```bash
php -S 127.0.0.1:8080 -t public
```
## Response Format
Success:
```json
{
"success": true,
"data": {}
}
```
Error:
```json
{
"success": false,
"error": {
"code": "server_error",
"message": "error message"
}
}
```
## Endpoints
### `GET /health`
Returns service health status.
### `GET /tools`
Returns available API tools and input schemas.
### `POST /api/order-summary`
Returns order count, active merchant count, successful order count, and amount totals.
### `POST /api/offline-stats`
Returns status, payment type, order type, and amount distribution analysis.
### `POST /api/merchant-ranking`
Returns merchant ranking by `orderCount`, `successOrderCount`, `totalOrderAmount`, or `totalRealPay`.
## Common Parameters
- `timePreset`: preferred AI time intent. Supported values: `today`, `yesterday`, `last_7_days`, `last_30_days`, `this_week`, `last_week`, `this_month`, `last_month`, `custom`. Defaults to `today`.
- `startDate`: custom start date, required when `timePreset=custom`, format `YYYY-MM-DD`.
- `endDate`: custom end date, required when `timePreset=custom`, format `YYYY-MM-DD`.
- `date`: legacy compatible date range, for example `2026/06/23-2026/06/23`; if provided, it is sent to the upstream order API as-is.
- `status`: order status; defaults to `-1` (all). Values: `0` = pending payment, `1` = completed, `2` = cancelled, `3` = pending verification/use, `4` = refunded.
- `keyword`: search keyword, sent to upstream API as `real_name`.
- `fieldKey`: upstream `field_key`; defaults to `all`.
- `payType`: upstream `pay_type`.
- `type`: upstream order `type`.
- `btcId`: upstream merchant category ID.
`/api/merchant-ranking` also supports:
- `sortBy`: `orderCount`, `successOrderCount`, `totalOrderAmount`, or `totalRealPay`; defaults to `totalRealPay`.
- `limit`: number of merchants to return; defaults to `10`, max `100`.
## POST Example
```bash
curl -X POST http://127.0.0.1:8080/api/merchant-ranking \
-H "Content-Type: application/json" \
-d '{"timePreset":"today","sortBy":"totalRealPay","limit":10}'
```

View File

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

1171
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,443 @@
# Merchant AI HTTP API 调用说明
本文档用于喂给外部“解读 AI”或其他服务平台让 AI 在对话中正确调用本项目的 HTTP JSON API 进行订单统计、线下订单分析和商户排行分析。
## 1. 服务定位
本项目是一个中间层 HTTP JSON API 服务。AI 不直接请求后台订单系统,也不直接拼接后台订单接口参数。
本项目负责:
- 接收 AI 的结构化分析请求
- 解析时间意图
- 调用后台订单接口
- 复用本项目内部统计逻辑
- 返回适合 AI 解读的 JSON 结果
## 2. 启动方式
本地开发启动:
```bash
php -S 127.0.0.1:8080 -t public public/index.php
```
健康检查:
```http
GET http://127.0.0.1:8080/health
```
正常返回:
```json
{
"success": true,
"data": {
"status": "ok",
"service": "merchant-ai-api"
}
}
```
## 3. 接口列表
### 3.1 订单汇总
```http
POST /api/order-summary
```
对应工具:`get_order_summary`
用途:订单总数、活跃商户数、成功订单数、订单金额合计、实付金额合计。
### 3.2 线下订单分析
```http
POST /api/offline-stats
```
对应工具:`get_offline_stats`
用途:订单状态分布、支付方式分布、订单类型分布、营销中台分布、退款状态分布、抵扣金额、手续费、`diamond` 字段代币值汇总、平均订单金额、平均实付金额、最大订单金额、活跃商户数、实付金额 Top 商户。
### 3.3 商户排行榜
```http
POST /api/merchant-ranking
```
对应工具:`get_merchant_ranking`
用途:按订单数、成功订单数、订单金额或实付金额生成商户排行。
## 4. 请求格式
所有统计接口都使用:
```http
Content-Type: application/json
```
如果调用方请求头中传入:
```http
Authorization: Bearer <后台订单接口 token>
```
本服务会优先使用该 token 请求后台订单接口;如果没有传 `Authorization`,则使用服务器 `.env` 中的 `MERCHANT_API_TOKEN`
注意:只有可信后端服务可以传该 header不建议在浏览器前端或公开客户端暴露后台 token。
统一请求示例:
```json
{
"timePreset": "today",
"status": -1,
"keyword": "",
"fieldKey": "all",
"payType": "",
"type": "",
"btcId": null
}
```
## 5. 时间参数规则
AI 应优先传 `timePreset`,不要直接传自然语言时间。
推荐:
```json
{
"timePreset": "today"
}
```
支持的 `timePreset`
| 值 | 含义 |
| --- | --- |
| `today` | 今天 |
| `yesterday` | 昨天 |
| `last_7_days` | 最近 7 天,包含今天 |
| `last_30_days` | 最近 30 天,包含今天 |
| `this_week` | 本周,周一到今天 |
| `last_week` | 上周,周一到周日 |
| `this_month` | 本月1 号到今天 |
| `last_month` | 上月1 号到月末 |
| `custom` | 自定义时间范围 |
自定义时间:
```json
{
"timePreset": "custom",
"startDate": "2026-06-01",
"endDate": "2026-06-23"
}
```
自定义时间约束:
- `startDate` 格式必须是 `YYYY-MM-DD`
- `endDate` 格式必须是 `YYYY-MM-DD`
- `startDate` 不能晚于 `endDate`
- 不要传 `2026/06/01`,自定义日期统一使用 `2026-06-01`
兼容旧字段:
```json
{
"date": "2026/06/01-2026/06/23"
}
```
AI 默认不要使用 `date`,除非调用平台只能传固定后台日期格式。
## 6. AI 对话到接口参数映射
用户说“今天订单怎么样?”时调用:
```json
{
"timePreset": "today"
}
```
推荐接口:`POST /api/order-summary`
用户说“分析一下昨天线下订单”时调用:
```json
{
"timePreset": "yesterday"
}
```
推荐接口:`POST /api/offline-stats`
用户说“看本月商户排行”时调用:
```json
{
"timePreset": "this_month",
"sortBy": "totalRealPay",
"limit": 10
}
```
推荐接口:`POST /api/merchant-ranking`
用户说“查 6 月 1 日到 6 月 23 日订单统计”时调用:
```json
{
"timePreset": "custom",
"startDate": "2026-06-01",
"endDate": "2026-06-23"
}
```
推荐接口:`POST /api/order-summary`
## 7. 常用筛选参数
| 参数 | 类型 | 说明 |
| --- | --- | --- |
| `status` | integer | 订单状态,默认 `-1` 表示全部 |
| `keyword` | string | 搜索关键词,当前会传给后台接口的 `real_name` |
| `fieldKey` | string | 搜索字段,默认 `all` |
| `payType` | string | 支付方式,对应后台 `pay_type` |
| `type` | string | 订单类型,对应后台 `type` |
| `btcId` | integer | 商户分类 ID对应后台 `btc_id` |
### 7.1 `diamond` 字段与 `btc_id` 的关系
后台订单里存在 `diamond` 字段。该字段不是固定只表示“钻石”,需要结合 `btc_id` 判断业务含义:
| btc_id | `diamond` 字段含义 |
| --- | --- |
| `1` | 钻石值 |
| `3` | 绿宝石值 |
| `4` | 算力值 |
`/api/offline-stats` 会返回:
| 字段 | 含义 |
| --- | --- |
| `totalTokenValue` | 所有订单 `diamond` 字段本身的合计 |
| `tokenValueSummary` | 按 `btc_id` 解释后的 `diamond` 字段汇总,例如钻石值、绿宝石值、算力值 |
注意:
- `diamond` 表示本单关联/附赠/记录的代币值。
- `diamond_ded` 表示消费券/钻石抵扣金额。
- 两者不是同一个字段,不要混淆。
### 7.2 订单状态 `status` 取值
| status | 含义 | 用户常见说法 |
| --- | --- | --- |
| `-1` | 全部订单 | 全部、所有订单、不限状态 |
| `0` | 待支付 | 待支付、未付款、未支付订单 |
| `1` | 已完成/成功 | 已完成、成功订单、已支付完成 |
| `2` | 已取消 | 已取消、取消订单 |
| `3` | 待核销/待使用 | 待核销、待使用、未使用 |
| `4` | 已退款 | 已退款、退款订单 |
AI 选择规则:
- 用户没有提到订单状态时,必须传 `status=-1`
- 用户问“成功订单”“已完成订单”时,传 `status=1`
- 用户问“待支付订单”“未付款订单”时,传 `status=0`
- 用户问“取消订单”时,传 `status=2`
- 用户问“待核销”“待使用”时,传 `status=3`
- 用户问“退款订单”时,传 `status=4`
- 如果用户说“订单汇总”“今日数据”“最近7天数据”但没有限定状态`status=-1`
示例:
用户说“今天成功订单有多少?”时调用:
```json
{
"timePreset": "today",
"status": 1
}
```
用户说“最近7天退款订单金额是多少”时调用
```json
{
"timePreset": "last_7_days",
"status": 4
}
```
用户说“本月所有订单汇总”时调用:
```json
{
"timePreset": "this_month",
"status": -1
}
```
## 8. 商户排行参数
`/api/merchant-ranking` 额外支持:
| 参数 | 类型 | 说明 |
| --- | --- | --- |
| `sortBy` | string | 排序字段 |
| `limit` | integer | 返回商户数量,默认 `10`,最大 `100` |
`sortBy` 可选值:
| 值 | 含义 |
| --- | --- |
| `orderCount` | 按订单数排序 |
| `successOrderCount` | 按成功订单数排序 |
| `totalOrderAmount` | 按订单金额排序 |
| `totalRealPay` | 按实付金额排序,默认值 |
## 9. 响应格式
成功响应:
```json
{
"success": true,
"data": {}
}
```
失败响应:
```json
{
"success": false,
"error": {
"code": "server_error",
"message": "错误信息"
}
}
```
常见错误:
| code | 含义 | 处理方式 |
| --- | --- | --- |
| `not_found` | 路由不存在 | 检查 URL |
| `method_not_allowed` | 请求方法错误 | 统计接口必须使用 POST |
| `server_error` | 服务端或后台接口异常 | 根据 message 排查 |
## 10. 返回数据解读规则
AI 必须严格按照接口返回的 JSON 数据解读,不允许编造、放大、补全或推测不存在的数据。
例如接口返回:
```json
{
"success": true,
"data": {
"orderCount": 100,
"activeMerchantCount": 5,
"successOrderCount": 47,
"totalOrderAmount": 882.89,
"totalRealPay": 662.39,
"_debug_query": {
"time": "2026/06/18-2026/06/24"
}
}
}
```
AI 只能解读为:
- 查询时间范围:`2026/06/18-2026/06/24`
- 当前接口返回订单数:`100`
- 活跃商户数:`5`
- 成功订单数:`47`
- 订单总金额:`882.89`
- 实付总金额:`662.39`
AI 可以基于已返回字段做简单计算,但必须说明是“根据当前返回数据计算”:
- 成功率:`47 / 100 = 47%`
- 实付占比:`662.39 / 882.89 ≈ 75.03%`
- 平均订单金额:`882.89 / 100 ≈ 8.83`
- 平均实付金额:`662.39 / 100 ≈ 6.62`
AI 禁止输出以下内容,除非接口明确返回:
- 禁止把 `100` 改写成 `3850`
- 禁止编造 `3427` 个成功订单
- 禁止编造 `186` 家商户
- 禁止编造 `日均订单 550 单`
- 禁止编造 `日均流水 38.5 万元`
- 禁止编造“整体交易状况良好”等过度结论
- 禁止把未返回的后台总数当作事实
注意:当前接口会根据后台返回的 `data.count` 自动分页拉取订单列表,直到取完当前筛选条件下的订单数据后再统计。`_debug_query` 只表示筛选条件,不再表示只统计第 1 页。
## 11. AI 调用约束
AI 必须遵守以下规则:
1. 统计接口必须使用 `POST`
2. 不要用浏览器 `GET` 方式访问统计接口。
3. 时间优先使用 `timePreset`
4. 只有用户明确给出起止日期时,才使用 `timePreset=custom`
5. 自定义日期必须使用 `YYYY-MM-DD`
6. 不要传 `今天`、`昨天`、`本月` 这类中文自然语言给接口。
7. 不要伪造不存在的筛选字段。
8. 不要把后台系统 token 暴露给用户。
9. 不要直接调用后台订单接口,只调用本项目 HTTP API。
10. 如果用户问题不包含时间,默认使用 `timePreset=today`
11. 解读结果时只能使用接口返回的字段,不允许编造不存在的订单数、金额、商户数、日均值或趋势结论。
12. 如果要计算成功率、平均值、占比,必须基于接口返回字段计算,并说明“根据当前返回数据计算”。
13. 如果接口返回 `_debug_query.time`,可以把它作为本次查询时间范围。
## 12. PowerShell 调用示例
订单汇总:
```powershell
Invoke-RestMethod `
-Uri "http://127.0.0.1:8080/api/order-summary" `
-Method POST `
-ContentType "application/json" `
-Body '{"timePreset":"today"}'
```
线下订单分析:
```powershell
Invoke-RestMethod `
-Uri "http://127.0.0.1:8080/api/offline-stats" `
-Method POST `
-ContentType "application/json" `
-Body '{"timePreset":"yesterday"}'
```
商户排行:
```powershell
Invoke-RestMethod `
-Uri "http://127.0.0.1:8080/api/merchant-ranking" `
-Method POST `
-ContentType "application/json" `
-Body '{"timePreset":"this_month","sortBy":"totalRealPay","limit":10}'
```
## 13. 推荐给 AI 的简短系统提示词
```text
你需要通过 Merchant AI HTTP API 分析订单数据。统计接口只能使用 POST。不要直接请求后台订单接口不要暴露 token。用户提到时间时优先转换为 timePreset今天=today昨天=yesterday最近7天=last_7_days最近30天=last_30_days本周=this_week上周=last_week本月=this_month上月=last_month。只有明确起止日期时使用 custom并传 startDate/endDate格式 YYYY-MM-DD。不要把“今天”“昨天”“本月”等自然语言直接作为参数传给接口。如果用户没说时间默认 timePreset=today。订单状态 status全部=-1待支付=0已完成/成功=1已取消=2待核销/待使用=3已退款=4如果用户没有限定状态默认 status=-1。解读接口返回时必须只使用 JSON 中真实存在的字段和数值,不允许编造订单数、成功订单数、商户数、金额、日均值、趋势或业务结论。可以基于返回字段做简单计算,但必须说明“根据当前返回数据计算”。如果返回 _debug_query.time可以作为查询时间范围。当前接口会自动分页拉取当前筛选条件下的订单数据后再统计_debug_query 只表示筛选条件。
```

View File

@ -0,0 +1,390 @@
# Merchant AI API 返回 JSON 解读说明
本文档用于喂给“结果解读 AI”。该 AI 的任务不是调用接口,而是接收已经真实调用接口后得到的 JSON并把 JSON 解读成用户能看懂的中文结果。
## 1. 核心原则
AI 必须严格遵守:
1. 只能基于输入 JSON 中真实存在的字段和数值回答。
2. 不允许编造、猜测、放大、补全任何订单数、金额、商户数、排行、趋势或业务结论。
3. 如果 JSON 中没有某个字段,就不能当作事实输出。
4. 可以做简单计算,但必须基于 JSON 已有字段,并说明“根据当前返回数据计算”。
5. 如果接口返回失败,必须先说明失败原因,不要继续分析业务数据。
6. 如果 JSON 中存在 `_debug_query.time`,可以把它作为本次查询时间范围。
7. 当前接口会根据后台 `data.count` 自动分页拉取当前筛选条件下的订单列表,再进行统计。`_debug_query` 只表示筛选条件。
## 2. 输入数据格式
成功响应格式:
```json
{
"success": true,
"data": {}
}
```
失败响应格式:
```json
{
"success": false,
"error": {
"code": "server_error",
"message": "错误信息"
}
}
```
## 3. 失败响应解读规则
如果 `success=false`AI 只能说明接口调用失败。
示例输入:
```json
{
"success": false,
"error": {
"code": "method_not_allowed",
"message": "This endpoint only supports POST."
}
}
```
推荐输出:
```text
接口调用失败原因是请求方法不正确This endpoint only supports POST。请使用 POST 方法重新请求该统计接口。
```
禁止输出订单统计、金额分析或商户排行。
## 4. 订单汇总 JSON 字段说明
接口:`POST /api/order-summary`
常见返回:
```json
{
"success": true,
"data": {
"orderCount": 100,
"activeMerchantCount": 5,
"successOrderCount": 47,
"totalOrderAmount": 882.89,
"totalRealPay": 662.39,
"_debug_query": {
"status": -1,
"field_key": "all",
"page": 1,
"limit": 100,
"time": "2026/06/18-2026/06/24"
}
}
}
```
字段含义:
| 字段 | 含义 |
| --- | --- |
| `orderCount` | 当前筛选条件下参与统计的订单数量 |
| `activeMerchantCount` | 当前筛选条件下的活跃商户数量 |
| `successOrderCount` | 当前筛选条件下的成功订单数量 |
| `totalOrderAmount` | 当前筛选条件下的订单总金额 |
| `totalRealPay` | 当前筛选条件下的实付总金额 |
| `_debug_query.time` | 本次查询时间范围 |
推荐解读模板:
```text
本次查询时间范围:{_debug_query.time}
订单汇总如下:
- 订单数:{orderCount} 单
- 活跃商户数:{activeMerchantCount} 家
- 成功订单数:{successOrderCount} 单
- 订单总金额:{totalOrderAmount}
- 实付总金额:{totalRealPay}
根据当前接口统计结果计算:
- 成功率:{successOrderCount / orderCount}
- 实付占比:{totalRealPay / totalOrderAmount}
注意:以上结果基于接口按当前筛选条件自动分页获取后的数据统计。
```
`orderCount=0` 时,不要计算成功率和平均值,应该说明当前范围内没有返回订单数据。
## 5. 线下订单分析 JSON 字段说明
接口:`POST /api/offline-stats`
常见返回:
```json
{
"success": true,
"data": {
"orderCount": 100,
"statusDistribution": {
"1 已完成": 47,
"2 已取消": 10
},
"payTypeDistribution": {
"7 蜜传微信支付": 50,
"2 微信支付": 20
},
"typeDistribution": {
"0 扫码订单": 70,
"5 视频团单": 30
},
"btcDistribution": {
"3 绿宝石中台(绿宝石)": 80,
"4 算力中台(算力)": 20
},
"refundDistribution": {
"0 未退款": 95,
"1 已退款": 5
},
"totalOrderAmount": 882.89,
"totalRealPay": 662.39,
"totalCoinDeduction": 100.00,
"totalBrokerDeduction": 10.00,
"totalDiamondDeduction": 20.00,
"totalTokenValue": 123.456789,
"tokenValueSummary": {
"diamond 钻石值": 10.000000,
"diamond 绿宝石值": 100.000000,
"diamond 算力值": 13.456789
},
"totalPreferentialAmount": 5.00,
"totalMerchantRatePrice": 2.50,
"totalDeductionAmount": 135.00,
"averageOrderAmount": 8.83,
"averageRealPay": 6.62,
"maxOrderAmount": 99.99,
"activeMerchantCount": 5,
"topMerchantsByRealPay": [
{
"merchantId": 110,
"merchantName": "示例商户",
"orderCount": 20,
"totalOrderAmount": 200.00,
"totalRealPay": 180.00
}
],
"_debug_query": {
"time": "2026/06/18-2026/06/24"
}
}
}
```
字段含义:
| 字段 | 含义 |
| --- | --- |
| `orderCount` | 当前筛选条件下参与统计的订单数量 |
| `statusDistribution` | 订单状态分布 |
| `payTypeDistribution` | 支付方式分布 |
| `typeDistribution` | 订单类型分布 |
| `btcDistribution` | 营销代币中台分布,如积分、钻石、绿宝石、算力 |
| `refundDistribution` | 退款状态分布 |
| `totalOrderAmount` | 订单总金额 |
| `totalRealPay` | 实付总金额 |
| `totalCoinDeduction` | 抵用券/积分抵扣金额合计,对应 `coin_ded` |
| `totalBrokerDeduction` | 佣金抵扣金额合计,对应 `broker_ded` |
| `totalDiamondDeduction` | 消费券/钻石抵扣金额合计,对应 `diamond_ded` |
| `totalTokenValue` | `diamond` 字段本身的总值,不是抵扣金额 |
| `tokenValueSummary` | 按 `btc_id` 解释后的 `diamond` 字段汇总 |
| `totalPreferentialAmount` | 平台让利抵扣券金额合计,对应 `pr_amount` |
| `totalMerchantRatePrice` | 商户承担通道手续费合计,对应 `merchant_rate_price` |
| `totalDeductionAmount` | 抵扣类金额合计,包含抵用券、佣金、消费券、平台让利抵扣券 |
| `averageOrderAmount` | 平均订单金额 |
| `averageRealPay` | 平均实付金额 |
| `maxOrderAmount` | 最大订单金额 |
| `activeMerchantCount` | 当前筛选条件下的活跃商户数量 |
| `topMerchantsByRealPay` | 按实付金额排序的前 10 个商户 |
状态值参考:
| 状态值 | 含义 |
| --- | --- |
| `-1` | 全部,仅用于请求筛选 |
| `0` | 待支付 |
| `1` | 已完成或成功 |
| `2` | 已取消 |
| `3` | 待核销或待使用 |
| `4` | 已退款 |
如果分布中的 key 无法确认含义,保持原值输出,不要强行翻译。
推荐解读模板:
```text
本次查询时间范围:{_debug_query.time}
线下订单分析如下:
- 订单数:{orderCount} 单
- 订单总金额:{totalOrderAmount}
- 实付总金额:{totalRealPay}
- 抵用券/积分抵扣:{totalCoinDeduction}
- 佣金抵扣:{totalBrokerDeduction}
- 消费券/钻石抵扣:{totalDiamondDeduction}
- diamond 字段总值:{totalTokenValue}
- diamond 字段分中台汇总:{tokenValueSummary 中逐项列出}
- 平台让利抵扣券:{totalPreferentialAmount}
- 通道手续费:{totalMerchantRatePrice}
- 抵扣合计:{totalDeductionAmount}
- 平均订单金额:{averageOrderAmount}
- 平均实付金额:{averageRealPay}
- 最大订单金额:{maxOrderAmount}
- 活跃商户数:{activeMerchantCount}
状态分布:
- {statusDistribution 中逐项列出}
支付方式分布:
- {payTypeDistribution 中逐项列出}
订单类型分布:
- {typeDistribution 中逐项列出}
营销中台分布:
- {btcDistribution 中逐项列出}
退款状态分布:
- {refundDistribution 中逐项列出}
实付金额 Top 商户:
- {topMerchantsByRealPay 中逐项列出}
注意:以上结果基于接口按当前筛选条件自动分页获取后的数据统计。
```
`diamond` 字段业务解释规则:
| btc_id | `diamond` 字段含义 |
| --- | --- |
| `1` | 钻石值 |
| `3` | 绿宝石值 |
| `4` | 算力值 |
注意:`diamond` 和 `diamond_ded` 不是同一个含义。
- `diamond`:本单关联/附赠/记录的代币值,需结合 `btc_id` 判断是钻石、绿宝石还是算力。
- `diamond_ded`:消费券/钻石抵扣金额,属于抵扣金额。
## 6. 商户排行榜 JSON 字段说明
接口:`POST /api/merchant-ranking`
常见返回:
```json
{
"success": true,
"data": {
"sortBy": "totalRealPay",
"ranking": [
{
"merchantId": 1,
"merchantName": "示例商户",
"orderCount": 20,
"successOrderCount": 10,
"totalOrderAmount": 200.00,
"totalRealPay": 180.00
}
],
"_debug_query": {
"time": "2026/06/18-2026/06/24"
}
}
}
```
字段含义:
| 字段 | 含义 |
| --- | --- |
| `sortBy` | 当前排行榜排序字段 |
| `ranking` | 商户排行列表 |
| `merchantId` | 商户 ID |
| `merchantName` | 商户名称,可能为空 |
| `orderCount` | 该商户在当前筛选条件下的订单数 |
| `successOrderCount` | 该商户在当前筛选条件下的成功订单数 |
| `totalOrderAmount` | 该商户在当前筛选条件下的订单金额 |
| `totalRealPay` | 该商户在当前筛选条件下的实付金额 |
推荐解读模板:
```text
本次查询时间范围:{_debug_query.time}
商户排行榜如下,排序字段:{sortBy}
1. {merchantName 或 merchantId}
- 订单数:{orderCount}
- 成功订单数:{successOrderCount}
- 订单金额:{totalOrderAmount}
- 实付金额:{totalRealPay}
注意:以上排行基于接口按当前筛选条件自动分页获取后的数据统计。
```
如果 `ranking` 为空,输出:
```text
当前查询条件下没有返回可排行的商户数据。
```
## 7. 允许的计算
AI 可以进行以下简单计算:
| 指标 | 计算方式 |
| --- | --- |
| 成功率 | `successOrderCount / orderCount * 100%` |
| 实付占比 | `totalRealPay / totalOrderAmount * 100%` |
| 平均订单金额 | `totalOrderAmount / orderCount` |
| 平均实付金额 | `totalRealPay / orderCount` |
计算规则:
- 分母为 `0` 时不要计算,说明无法计算。
- 百分比建议保留 2 位小数。
- 金额建议保留 2 位小数。
- 必须说明计算基于“当前返回数据”。
## 8. 禁止行为
AI 禁止:
- 禁止说“我已经调用接口”,除非系统确实提供了真实接口返回 JSON。
- 禁止把示例 JSON 当成真实数据。
- 禁止模拟、假设、补全数据。
- 禁止把 `orderCount=100` 写成其他数值。
- 禁止编造日均订单、日均流水、增长率、环比、同比。
- 禁止编造“业务表现良好”“交易状况良好”等没有数据支撑的结论。
- 禁止脱离接口返回 JSON 编造后台总量。
- 禁止输出 JSON 中不存在的商户、订单状态或支付方式。
## 9. 没有真实 JSON 时的回答
如果没有收到真实接口返回 JSONAI 必须回答:
```text
我目前还没有拿到真实接口返回数据,不能生成具体订单统计结果。请先调用对应 API并把返回 JSON 提供给我,我会基于真实数据进行解读。
```
不要输出示例统计结果。
## 10. 推荐系统提示词
```text
你是 Merchant AI API 的结果解读助手。你只负责解读已经真实调用接口后返回的 JSON。你不能自己调用接口也不能声称已经调用接口除非系统消息中提供了真实 JSON。你必须严格使用 JSON 中存在的字段和数值,不得编造、模拟、放大或补全任何订单数、金额、商户数、日均值、增长率、排行、趋势或业务结论。如果没有真实 JSON必须说明尚未获取真实接口数据。可以基于 JSON 字段做简单计算,例如成功率、实付占比、平均金额,但必须说明“根据当前接口统计结果计算”。如果存在 _debug_query.time将其作为查询时间范围。当前接口会自动分页拉取当前筛选条件下的数据后再统计_debug_query 只表示筛选条件。
```

15
public/index.php Normal file
View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
use App\Api\ApiFactory;
use App\Config\Env;
use App\Http\HttpRequest;
Env::load(dirname(__DIR__) . '/.env');
$request = HttpRequest::fromGlobals();
ApiFactory::create($request->bearerToken())->handle($request);

View File

@ -2,83 +2,4 @@
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use GuzzleHttp\Client;
use Mcp\Server;
use Mcp\Server\Transport\StdioTransport;
$token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwd2QiOiI2ODI5N2FjYzUwZDczYWQzZmNmMTNlM2Y4MWYxMWQyZSIsImlzcyI6InRwb2ludC5hZ3JpbWVkaWEuY24iLCJhdWQiOiJ0cG9pbnQuYWdyaW1lZGlhLmNuIiwiaWF0IjoxNzgwODk3Mjk3LCJuYmYiOjE3ODA4OTcyOTcsImV4cCI6MTc4MzQwMjg5NywianRpIjp7ImlkIjo0LCJ0eXBlIjoiYWRtaW4ifX0.G2yB38bYkf0b37kpAbQViDuXY4QHeRjm99WNXjSh4L0';
$server = Server::builder()
->setServerInfo('Merchant MCP', '1.0.0')
->addTool(
handler: function () use ($token): array {
$client = new Client([
'timeout' => 10,
]);
$response = $client->get(
'https://tpoint.agrimedia.cn/adminapi/offline/list',
[
'headers' => [
'accept' => 'application/json',
'authori-zation' => 'Bearer ' . $token,
],
'query' => [
'status' => -1,
'field_key' => 'all',
'page' => 1,
'limit' => 100,
'time' => '2026/06/23-2026/06/23',
],
]
);
$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),
];
},
name: 'get_order_summary',
description: '获取订单总数、活跃商户数、成功订单数和金额统计'
)
->build();
$transport = new StdioTransport();
$server->run($transport);
require __DIR__ . '/public/index.php';

36
src/Api/ApiFactory.php Normal file
View File

@ -0,0 +1,36 @@
<?php
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\Service\TimeRangeResolver;
use App\Tools\AnalyzeOfflineOrdersTool;
use App\Tools\MerchantRankingTool;
use App\Tools\OrderSummaryTool;
final class ApiFactory
{
public static function create(?string $tokenOverride = null): ApiKernel
{
$apiClient = new MerchantApiClient(ApiConfig::fromEnvironment($tokenOverride));
$orders = new OfflineOrderRepository($apiClient);
$statsService = new StatsService(
new OrderSummaryTool($orders),
new AnalyzeOfflineOrdersTool($orders),
new MerchantRankingTool($orders),
new TimeRangeResolver(),
);
return new ApiKernel(
new HealthController(),
new StatsController($statsService),
);
}
}

113
src/Api/ApiKernel.php Normal file
View File

@ -0,0 +1,113 @@
<?php
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;
use App\Tools\MerchantRankingTool;
use App\Tools\OrderSummaryTool;
final readonly class ApiKernel
{
public function __construct(
private HealthController $healthController,
private StatsController $statsController,
) {
}
public function handle(HttpRequest $request): void
{
try {
match ($request->path) {
'/', '/health' => $this->ok($this->healthController->show()),
'/tools' => $this->ok($this->tools()),
'/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' => [
'code' => 'not_found',
'message' => 'API route not found.',
],
], 404),
};
} catch (\Throwable $exception) {
JsonResponse::send([
'success' => false,
'error' => [
'code' => 'server_error',
'message' => $exception->getMessage(),
],
], 500);
}
}
/**
* @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
*/
private function ok(array $data): void
{
JsonResponse::send([
'success' => true,
'data' => $data,
]);
}
/**
* @return array<string, mixed>
*/
private function tools(): array
{
return [
'tools' => [
[
'name' => OrderSummaryTool::NAME,
'description' => OrderSummaryTool::DESCRIPTION,
'method' => 'POST',
'path' => '/api/order-summary',
'inputSchema' => OrderSummaryTool::inputSchema(),
],
[
'name' => AnalyzeOfflineOrdersTool::NAME,
'description' => AnalyzeOfflineOrdersTool::DESCRIPTION,
'method' => 'POST',
'path' => '/api/offline-stats',
'inputSchema' => AnalyzeOfflineOrdersTool::inputSchema(),
],
[
'name' => MerchantRankingTool::NAME,
'description' => MerchantRankingTool::DESCRIPTION,
'method' => 'POST',
'path' => '/api/merchant-ranking',
'inputSchema' => MerchantRankingTool::inputSchema(),
],
],
];
}
}

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 = 60,
) {
}
public static function fromEnvironment(?string $tokenOverride = null): self
{
$baseUri = getenv('MERCHANT_API_BASE_URI') ?: 'https://tpoint.agrimedia.cn';
$token = $tokenOverride ?: (getenv('MERCHANT_API_TOKEN') ?: '');
$timeout = (int) (getenv('MERCHANT_API_TIMEOUT') ?: 60);
if ($token === '') {
throw new \RuntimeException('Missing MERCHANT_API_TOKEN environment variable.');
}
return new self(
baseUri: rtrim($baseUri, '/'),
token: $token,
timeout: $timeout > 0 ? $timeout : 10,
);
}
}

43
src/Config/Env.php Normal file
View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Config;
final class Env
{
public static function load(string $path): void
{
if (!is_file($path)) {
return;
}
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if ($lines === false) {
return;
}
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || str_starts_with($line, '#') || !str_contains($line, '=')) {
continue;
}
[$key, $value] = explode('=', $line, 2);
$key = trim($key);
$value = trim($value);
if ($key === '' || getenv($key) !== false) {
continue;
}
$value = trim($value, "\"'");
putenv($key . '=' . $value);
$_ENV[$key] = $value;
$_SERVER[$key] = $value;
}
}
}

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

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

118
src/Http/HttpRequest.php Normal file
View File

@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace App\Http;
final readonly class HttpRequest
{
/**
* @param array<string, mixed> $input
* @param array<string, string> $headers
*/
public function __construct(
public string $method,
public string $path,
public array $input,
public array $headers = [],
) {
}
public static function fromGlobals(): self
{
$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)) {
$body = trim((string) file_get_contents('php://input'));
if ($body !== '') {
$decoded = json_decode($body, true);
if (is_array($decoded)) {
$input = array_merge($input, $decoded);
}
}
}
return new self($method, $path, $input, self::headersFromGlobals());
}
public function bearerToken(): ?string
{
$authorization = $this->headers['authorization'] ?? '';
if (preg_match('/^Bearer\s+(.+)$/i', $authorization, $matches) !== 1) {
return null;
}
$token = trim($matches[1]);
return $token !== '' ? $token : null;
}
public function string(string $key): ?string
{
if (!array_key_exists($key, $this->input)) {
return null;
}
$value = $this->input[$key];
if ($value === null || is_array($value)) {
return null;
}
return (string) $value;
}
public function int(string $key): ?int
{
if (!array_key_exists($key, $this->input)) {
return null;
}
$value = $this->input[$key];
if ($value === null || $value === '' || is_array($value)) {
return null;
}
return (int) $value;
}
/**
* @return array<string, string>
*/
private static function headersFromGlobals(): array
{
$headers = [];
foreach ($_SERVER as $key => $value) {
if (!is_string($value) || !str_starts_with($key, 'HTTP_')) {
continue;
}
$name = strtolower(str_replace('_', '-', substr($key, 5)));
$headers[$name] = $value;
}
if (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION']) && is_string($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
$headers['authorization'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
}
if (isset($_SERVER['HTTP_AUTHORIZATION']) && is_string($_SERVER['HTTP_AUTHORIZATION'])) {
$headers['authorization'] = $_SERVER['HTTP_AUTHORIZATION'];
}
return $headers;
}
}

19
src/Http/JsonResponse.php Normal file
View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace App\Http;
final class JsonResponse
{
/**
* @param array<string, mixed> $payload
*/
public static function send(array $payload, int $statusCode = 200): void
{
http_response_code($statusCode);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
}

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,66 @@
<?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
{
$page = 1;
$orders = [];
$totalCount = null;
do {
$data = $this->apiClient->getOfflineOrders($query + [
'page' => $page,
'limit' => $this->pageLimit(),
]);
$pageOrders = $data['data']['list'] ?? [];
if (!is_array($pageOrders) || $pageOrders === []) {
break;
}
if ($totalCount === null) {
$totalCount = isset($data['data']['count']) ? (int) $data['data']['count'] : null;
}
$orders = array_merge($orders, array_values(array_filter($pageOrders, is_array(...))));
$page++;
} while ($this->shouldFetchNextPage($orders, $pageOrders, $totalCount));
return $orders;
}
/**
* @param list<array<string, mixed>> $orders
* @param mixed $pageOrders
*/
private function shouldFetchNextPage(array $orders, mixed $pageOrders, ?int $totalCount): bool
{
if ($totalCount !== null) {
return count($orders) < $totalCount;
}
return is_array($pageOrders) && count($pageOrders) >= $this->pageLimit();
}
private function pageLimit(): int
{
$limit = (int) (getenv('OFFLINE_ORDER_PAGE_LIMIT') ?: 100);
return $limit > 0 ? $limit : 100;
}
}

View File

@ -0,0 +1,81 @@
<?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,
private TimeRangeResolver $timeRangeResolver,
) {
}
/**
* @return array<string, mixed>
*/
public function getOrderSummary(HttpRequest $request): array
{
return ($this->orderSummaryTool)(
$this->date($request),
$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)(
$this->date($request),
$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)(
$this->date($request),
$request->int('status'),
$request->string('keyword'),
$request->string('fieldKey'),
$request->string('payType'),
$request->string('type'),
$request->int('btcId'),
$request->string('sortBy'),
$request->int('limit'),
);
}
private function date(HttpRequest $request): string
{
return $this->timeRangeResolver->resolve(
$request->string('date'),
$request->string('timePreset'),
$request->string('startDate'),
$request->string('endDate'),
);
}
}

View File

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace App\Service;
final class TimeRangeResolver
{
private const DATE_FORMAT = 'Y/m/d';
public function __construct(private readonly string $timezone = 'Asia/Shanghai')
{
}
public function resolve(?string $date, ?string $timePreset, ?string $startDate, ?string $endDate): string
{
if ($this->filled($date)) {
return $date;
}
$now = new \DateTimeImmutable('now', new \DateTimeZone($this->timezone));
$preset = $this->filled($timePreset) ? $timePreset : 'today';
[$start, $end] = match ($preset) {
'today' => [$now, $now],
'yesterday' => [$now->modify('-1 day'), $now->modify('-1 day')],
'last_7_days' => [$now->modify('-6 days'), $now],
'last_30_days' => [$now->modify('-29 days'), $now],
'this_week' => [$now->modify('monday this week'), $now],
'last_week' => [$now->modify('monday last week'), $now->modify('sunday last week')],
'this_month' => [$now->modify('first day of this month'), $now],
'last_month' => [$now->modify('first day of last month'), $now->modify('last day of last month')],
'custom' => [$this->parseDate($startDate), $this->parseDate($endDate)],
default => throw new \InvalidArgumentException('Unsupported timePreset: ' . $preset),
};
if ($start > $end) {
throw new \InvalidArgumentException('startDate cannot be later than endDate.');
}
return $start->format(self::DATE_FORMAT) . '-' . $end->format(self::DATE_FORMAT);
}
private function parseDate(?string $date): \DateTimeImmutable
{
if (!$this->filled($date)) {
throw new \InvalidArgumentException('startDate and endDate are required when timePreset is custom.');
}
$normalized = str_replace('/', '-', $date);
$parsed = \DateTimeImmutable::createFromFormat('!Y-m-d', $normalized, new \DateTimeZone($this->timezone));
if (!$parsed instanceof \DateTimeImmutable) {
throw new \InvalidArgumentException('Invalid date format, expected YYYY-MM-DD.');
}
return $parsed;
}
private function filled(?string $value): bool
{
return $value !== null && trim($value) !== '';
}
}

View File

@ -0,0 +1,50 @@
<?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,
): array {
$query = [
'status' => $status ?? -1,
'field_key' => self::filled($fieldKey) ? $fieldKey : 'all',
'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,242 @@
<?php
declare(strict_types=1);
namespace App\Tools;
use App\Repositories\OfflineOrderRepository;
use App\Support\OfflineOrderQuery;
final readonly class AnalyzeOfflineOrdersTool
{
public const NAME = 'get_offline_stats';
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 = [];
$btcDistribution = [];
$refundDistribution = [];
$merchantStats = [];
$totalOrderAmount = 0.0;
$totalRealPay = 0.0;
$totalCoinDeduction = 0.0;
$totalBrokerDeduction = 0.0;
$totalDiamondDeduction = 0.0;
$totalTokenValue = 0.0;
$tokenValueSummary = [];
$totalPreferentialAmount = 0.0;
$totalMerchantRatePrice = 0.0;
$maxOrderAmount = 0.0;
foreach ($orders as $order) {
$statusKey = $this->statusLabel($order);
$payTypeKey = $this->payTypeLabel($order['pay_type'] ?? null);
$typeKey = $this->orderTypeLabel($order['type'] ?? null, $order['is_backend'] ?? null);
$btcKey = $this->btcLabel($order['btc_id'] ?? null, $order['btc_type'] ?? null);
$refundKey = $this->refundLabel($order['refund_status'] ?? null);
$orderAmount = (float) ($order['order_price'] ?? 0);
$realPay = (float) ($order['real_pay'] ?? 0);
$coinDeduction = (float) ($order['coin_ded'] ?? 0);
$brokerDeduction = (float) ($order['broker_ded'] ?? 0);
$diamondDeduction = (float) ($order['diamond_ded'] ?? 0);
$tokenValue = (float) ($order['diamond'] ?? 0);
$preferentialAmount = (float) ($order['pr_amount'] ?? 0);
$merchantRatePrice = (float) ($order['merchant_rate_price'] ?? 0);
$merchantId = (int) ($order['merchant_id'] ?? 0);
$tokenValueKey = $this->tokenValueLabel($order['btc_id'] ?? null);
$statusDistribution[$statusKey] = ($statusDistribution[$statusKey] ?? 0) + 1;
$payTypeDistribution[$payTypeKey] = ($payTypeDistribution[$payTypeKey] ?? 0) + 1;
$typeDistribution[$typeKey] = ($typeDistribution[$typeKey] ?? 0) + 1;
$btcDistribution[$btcKey] = ($btcDistribution[$btcKey] ?? 0) + 1;
$refundDistribution[$refundKey] = ($refundDistribution[$refundKey] ?? 0) + 1;
$totalOrderAmount += $orderAmount;
$totalRealPay += $realPay;
$totalCoinDeduction += $coinDeduction;
$totalBrokerDeduction += $brokerDeduction;
$totalDiamondDeduction += $diamondDeduction;
$totalTokenValue += $tokenValue;
$tokenValueSummary[$tokenValueKey] = round(($tokenValueSummary[$tokenValueKey] ?? 0) + $tokenValue, 6);
$totalPreferentialAmount += $preferentialAmount;
$totalMerchantRatePrice += $merchantRatePrice;
$maxOrderAmount = max($maxOrderAmount, $orderAmount);
if ($merchantId > 0) {
if (!isset($merchantStats[$merchantId])) {
$merchantStats[$merchantId] = [
'merchantId' => $merchantId,
'merchantName' => (string) ($order['merchant_name'] ?? ''),
'orderCount' => 0,
'totalOrderAmount' => 0.0,
'totalRealPay' => 0.0,
];
}
$merchantStats[$merchantId]['orderCount']++;
$merchantStats[$merchantId]['totalOrderAmount'] += $orderAmount;
$merchantStats[$merchantId]['totalRealPay'] += $realPay;
}
}
$orderCount = count($orders);
$topMerchants = array_values($merchantStats);
usort($topMerchants, static fn (array $left, array $right): int => $right['totalRealPay'] <=> $left['totalRealPay']);
$topMerchants = array_slice($topMerchants, 0, 10);
foreach ($topMerchants as $index => $merchant) {
$topMerchants[$index]['totalOrderAmount'] = round((float) $merchant['totalOrderAmount'], 2);
$topMerchants[$index]['totalRealPay'] = round((float) $merchant['totalRealPay'], 2);
}
return [
'orderCount' => $orderCount,
'statusDistribution' => $statusDistribution,
'payTypeDistribution' => $payTypeDistribution,
'typeDistribution' => $typeDistribution,
'btcDistribution' => $btcDistribution,
'refundDistribution' => $refundDistribution,
'totalOrderAmount' => round($totalOrderAmount, 2),
'totalRealPay' => round($totalRealPay, 2),
'totalCoinDeduction' => round($totalCoinDeduction, 2),
'totalBrokerDeduction' => round($totalBrokerDeduction, 2),
'totalDiamondDeduction' => round($totalDiamondDeduction, 2),
'totalTokenValue' => round($totalTokenValue, 6),
'tokenValueSummary' => $tokenValueSummary,
'totalPreferentialAmount' => round($totalPreferentialAmount, 2),
'totalMerchantRatePrice' => round($totalMerchantRatePrice, 2),
'totalDeductionAmount' => round($totalCoinDeduction + $totalBrokerDeduction + $totalDiamondDeduction + $totalPreferentialAmount, 2),
'averageOrderAmount' => $orderCount > 0 ? round($totalOrderAmount / $orderCount, 2) : 0,
'averageRealPay' => $orderCount > 0 ? round($totalRealPay / $orderCount, 2) : 0,
'maxOrderAmount' => round($maxOrderAmount, 2),
'activeMerchantCount' => count($merchantStats),
'topMerchantsByRealPay' => $topMerchants,
'_debug_query' => $query,
];
}
public static function inputSchema(): array
{
return [
'type' => 'object',
'properties' => OrderSummaryTool::filterProperties(),
];
}
/**
* @param array<string, mixed> $order
*/
private function statusLabel(array $order): string
{
$status = (int) ($order['status'] ?? -999);
$refundStatus = (int) ($order['refund_status'] ?? 0);
if ($status === 1 && $refundStatus === 1) {
return '1 已完成(已退款)';
}
if ($status === 1 && $refundStatus > 1) {
return '1 已完成(部分退款)';
}
return match ($status) {
0 => '0 待付款',
1 => '1 已完成',
2 => '2 已取消',
3 => '3 待使用/待核销',
4 => '4 已退款',
default => 'unknown 未知状态',
};
}
private function payTypeLabel(mixed $payType): string
{
return match ((int) $payType) {
0 => '0 中台微信',
1 => '1 中台支付宝',
2 => '2 微信支付',
3 => '3 公众号支付',
4 => '4 中台公众号',
7 => '7 蜜传微信支付',
default => 'unknown 未知支付渠道',
};
}
private function orderTypeLabel(mixed $type, mixed $isBackend): string
{
$orderType = (int) $type;
if ($orderType === 0 && (int) $isBackend === 1) {
return '0 后台扫码订单';
}
if ($orderType === 0) {
return '0 扫码订单';
}
return match ($orderType) {
1 => '1 点餐订单',
2 => '2 团购订单',
3 => '3 收银系统订单',
4 => '4 曹操出行订单',
5 => '5 视频团单',
default => 'unknown 未知订单类型',
};
}
private function btcLabel(mixed $btcId, mixed $btcType): string
{
$label = match ((int) $btcId) {
0 => '0 积分中台',
1 => '1 钻石中台',
3 => '3 绿宝石中台',
4 => '4 算力中台',
default => 'unknown 未知中台',
};
if ($btcType !== null && trim((string) $btcType) !== '') {
return $label . '' . $btcType . '';
}
return $label;
}
private function tokenValueLabel(mixed $btcId): string
{
return match ((int) $btcId) {
1 => 'diamond 钻石值',
3 => 'diamond 绿宝石值',
4 => 'diamond 算力值',
default => 'diamond 其他中台值',
};
}
private function refundLabel(mixed $refundStatus): string
{
return match ((int) $refundStatus) {
0 => '0 未退款',
1 => '1 已退款',
2 => '2 部分退款',
default => 'unknown 未知退款状态',
};
}
}

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,120 @@
<?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。不建议 AI 优先传此字段;建议传 timePreset 或 custom + startDate/endDate。',
],
'timePreset' => [
'type' => 'string',
'description' => '推荐给 AI 使用的时间意图。支持today今天、yesterday昨天、last_7_days最近7天、last_30_days最近30天、this_week本周、last_week上周、this_month本月、last_month上月、custom自定义。不传默认 today。',
'enum' => ['today', 'yesterday', 'last_7_days', 'last_30_days', 'this_week', 'last_week', 'this_month', 'last_month', 'custom'],
],
'startDate' => [
'type' => 'string',
'description' => '自定义开始日期,仅 timePreset=custom 时需要,格式 YYYY-MM-DD例如 2026-06-01。',
],
'endDate' => [
'type' => 'string',
'description' => '自定义结束日期,仅 timePreset=custom 时需要,格式 YYYY-MM-DD例如 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

@ -9,6 +9,5 @@ return array(
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
'09f6b20656683369174dd6fa83b7e5fb' => $vendorDir . '/symfony/polyfill-uuid/bootstrap.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
);

View File

@ -6,27 +6,11 @@ $vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'phpDocumentor\\Reflection\\' => array($vendorDir . '/phpdocumentor/reflection-docblock/src', $vendorDir . '/phpdocumentor/type-resolver/src', $vendorDir . '/phpdocumentor/reflection-common/src'),
'Webmozart\\Assert\\' => array($vendorDir . '/webmozart/assert/src'),
'Symfony\\Polyfill\\Uuid\\' => array($vendorDir . '/symfony/polyfill-uuid'),
'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
'Symfony\\Component\\Uid\\' => array($vendorDir . '/symfony/uid'),
'Psr\\Log\\' => array($vendorDir . '/psr/log/src'),
'Psr\\Http\\Server\\' => array($vendorDir . '/psr/http-server-middleware/src', $vendorDir . '/psr/http-server-handler/src'),
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'),
'Psr\\Http\\Client\\' => array($vendorDir . '/psr/http-client/src'),
'Psr\\EventDispatcher\\' => array($vendorDir . '/psr/event-dispatcher/src'),
'Psr\\Container\\' => array($vendorDir . '/psr/container/src'),
'Psr\\Clock\\' => array($vendorDir . '/psr/clock/src'),
'PHPStan\\PhpDocParser\\' => array($vendorDir . '/phpstan/phpdoc-parser/src'),
'Opis\\Uri\\' => array($vendorDir . '/opis/uri/src'),
'Opis\\String\\' => array($vendorDir . '/opis/string/src'),
'Opis\\JsonSchema\\' => array($vendorDir . '/opis/json-schema/src'),
'Mcp\\' => array($vendorDir . '/mcp/sdk/src'),
'Http\\Discovery\\' => array($vendorDir . '/php-http/discovery/src'),
'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'),
'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'),
'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'),
'Doctrine\\Deprecations\\' => array($vendorDir . '/doctrine/deprecations/src'),
'App\\' => array($baseDir . '/src'),
);

View File

@ -10,49 +10,18 @@ class ComposerStaticInit842ab640c1f6a67d129686b79420231b
'6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
'7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
'09f6b20656683369174dd6fa83b7e5fb' => __DIR__ . '/..' . '/symfony/polyfill-uuid/bootstrap.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
);
public static $prefixLengthsPsr4 = array (
'p' =>
array (
'phpDocumentor\\Reflection\\' => 25,
),
'W' =>
array (
'Webmozart\\Assert\\' => 17,
),
'S' =>
array (
'Symfony\\Polyfill\\Uuid\\' => 22,
'Symfony\\Polyfill\\Php80\\' => 23,
'Symfony\\Component\\Uid\\' => 22,
),
'P' =>
array (
'Psr\\Log\\' => 8,
'Psr\\Http\\Server\\' => 16,
'Psr\\Http\\Message\\' => 17,
'Psr\\Http\\Client\\' => 16,
'Psr\\EventDispatcher\\' => 20,
'Psr\\Container\\' => 14,
'Psr\\Clock\\' => 10,
'PHPStan\\PhpDocParser\\' => 21,
),
'O' =>
array (
'Opis\\Uri\\' => 9,
'Opis\\String\\' => 12,
'Opis\\JsonSchema\\' => 16,
),
'M' =>
array (
'Mcp\\' => 4,
),
'H' =>
array (
'Http\\Discovery\\' => 15,
),
'G' =>
array (
@ -60,10 +29,6 @@ class ComposerStaticInit842ab640c1f6a67d129686b79420231b
'GuzzleHttp\\Promise\\' => 19,
'GuzzleHttp\\' => 11,
),
'D' =>
array (
'Doctrine\\Deprecations\\' => 22,
),
'A' =>
array (
'App\\' => 4,
@ -71,37 +36,10 @@ class ComposerStaticInit842ab640c1f6a67d129686b79420231b
);
public static $prefixDirsPsr4 = array (
'phpDocumentor\\Reflection\\' =>
array (
0 => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src',
1 => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src',
2 => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src',
),
'Webmozart\\Assert\\' =>
array (
0 => __DIR__ . '/..' . '/webmozart/assert/src',
),
'Symfony\\Polyfill\\Uuid\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-uuid',
),
'Symfony\\Polyfill\\Php80\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
),
'Symfony\\Component\\Uid\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/uid',
),
'Psr\\Log\\' =>
array (
0 => __DIR__ . '/..' . '/psr/log/src',
),
'Psr\\Http\\Server\\' =>
array (
0 => __DIR__ . '/..' . '/psr/http-server-middleware/src',
1 => __DIR__ . '/..' . '/psr/http-server-handler/src',
),
'Psr\\Http\\Message\\' =>
array (
0 => __DIR__ . '/..' . '/psr/http-factory/src',
@ -111,42 +49,6 @@ class ComposerStaticInit842ab640c1f6a67d129686b79420231b
array (
0 => __DIR__ . '/..' . '/psr/http-client/src',
),
'Psr\\EventDispatcher\\' =>
array (
0 => __DIR__ . '/..' . '/psr/event-dispatcher/src',
),
'Psr\\Container\\' =>
array (
0 => __DIR__ . '/..' . '/psr/container/src',
),
'Psr\\Clock\\' =>
array (
0 => __DIR__ . '/..' . '/psr/clock/src',
),
'PHPStan\\PhpDocParser\\' =>
array (
0 => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src',
),
'Opis\\Uri\\' =>
array (
0 => __DIR__ . '/..' . '/opis/uri/src',
),
'Opis\\String\\' =>
array (
0 => __DIR__ . '/..' . '/opis/string/src',
),
'Opis\\JsonSchema\\' =>
array (
0 => __DIR__ . '/..' . '/opis/json-schema/src',
),
'Mcp\\' =>
array (
0 => __DIR__ . '/..' . '/mcp/sdk/src',
),
'Http\\Discovery\\' =>
array (
0 => __DIR__ . '/..' . '/php-http/discovery/src',
),
'GuzzleHttp\\Psr7\\' =>
array (
0 => __DIR__ . '/..' . '/guzzlehttp/psr7/src',
@ -159,10 +61,6 @@ class ComposerStaticInit842ab640c1f6a67d129686b79420231b
array (
0 => __DIR__ . '/..' . '/guzzlehttp/guzzle/src',
),
'Doctrine\\Deprecations\\' =>
array (
0 => __DIR__ . '/..' . '/doctrine/deprecations/src',
),
'App\\' =>
array (
0 => __DIR__ . '/../..' . '/src',

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,9 @@
<?php return array(
'root' => array(
'name' => '__root__',
'pretty_version' => '1.0.0+no-version-set',
'version' => '1.0.0.0',
'reference' => null,
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '7984fdc92f51bf7a1b1cac42da4ec23fb57ccf8d',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@ -11,23 +11,14 @@
),
'versions' => array(
'__root__' => array(
'pretty_version' => '1.0.0+no-version-set',
'version' => '1.0.0.0',
'reference' => null,
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '7984fdc92f51bf7a1b1cac42da4ec23fb57ccf8d',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
'doctrine/deprecations' => array(
'pretty_version' => '1.1.6',
'version' => '1.1.6.0',
'reference' => 'd4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca',
'type' => 'library',
'install_path' => __DIR__ . '/../doctrine/deprecations',
'aliases' => array(),
'dev_requirement' => false,
),
'guzzlehttp/guzzle' => array(
'pretty_version' => '7.12.1',
'version' => '7.12.1.0',
@ -55,126 +46,6 @@
'aliases' => array(),
'dev_requirement' => false,
),
'mcp/sdk' => array(
'pretty_version' => 'v0.6.0',
'version' => '0.6.0.0',
'reference' => '433c84b58af346dd32f15f9909679e96a46ebe23',
'type' => 'library',
'install_path' => __DIR__ . '/../mcp/sdk',
'aliases' => array(),
'dev_requirement' => false,
),
'opis/json-schema' => array(
'pretty_version' => '2.6.0',
'version' => '2.6.0.0',
'reference' => '8458763e0dd0b6baa310e04f1829fc73da4e8c8a',
'type' => 'library',
'install_path' => __DIR__ . '/../opis/json-schema',
'aliases' => array(),
'dev_requirement' => false,
),
'opis/string' => array(
'pretty_version' => '2.1.0',
'version' => '2.1.0.0',
'reference' => '3e4d2aaff518ac518530b89bb26ed40f4503635e',
'type' => 'library',
'install_path' => __DIR__ . '/../opis/string',
'aliases' => array(),
'dev_requirement' => false,
),
'opis/uri' => array(
'pretty_version' => '1.1.0',
'version' => '1.1.0.0',
'reference' => '0f3ca49ab1a5e4a6681c286e0b2cc081b93a7d5a',
'type' => 'library',
'install_path' => __DIR__ . '/../opis/uri',
'aliases' => array(),
'dev_requirement' => false,
),
'php-http/async-client-implementation' => array(
'dev_requirement' => false,
'provided' => array(
0 => '*',
),
),
'php-http/client-implementation' => array(
'dev_requirement' => false,
'provided' => array(
0 => '*',
),
),
'php-http/discovery' => array(
'pretty_version' => '1.20.0',
'version' => '1.20.0.0',
'reference' => '82fe4c73ef3363caed49ff8dd1539ba06044910d',
'type' => 'composer-plugin',
'install_path' => __DIR__ . '/../php-http/discovery',
'aliases' => array(),
'dev_requirement' => false,
),
'phpdocumentor/reflection-common' => array(
'pretty_version' => '2.2.0',
'version' => '2.2.0.0',
'reference' => '1d01c49d4ed62f25aa84a747ad35d5a16924662b',
'type' => 'library',
'install_path' => __DIR__ . '/../phpdocumentor/reflection-common',
'aliases' => array(),
'dev_requirement' => false,
),
'phpdocumentor/reflection-docblock' => array(
'pretty_version' => '6.0.3',
'version' => '6.0.3.0',
'reference' => '7bae67520aa9f5ecc506d646810bd40d9da54582',
'type' => 'library',
'install_path' => __DIR__ . '/../phpdocumentor/reflection-docblock',
'aliases' => array(),
'dev_requirement' => false,
),
'phpdocumentor/type-resolver' => array(
'pretty_version' => '2.0.0',
'version' => '2.0.0.0',
'reference' => '327a05bbee54120d4786a0dc67aad30226ad4cf9',
'type' => 'library',
'install_path' => __DIR__ . '/../phpdocumentor/type-resolver',
'aliases' => array(),
'dev_requirement' => false,
),
'phpstan/phpdoc-parser' => array(
'pretty_version' => '2.3.2',
'version' => '2.3.2.0',
'reference' => 'a004701b11273a26cd7955a61d67a7f1e525a45a',
'type' => 'library',
'install_path' => __DIR__ . '/../phpstan/phpdoc-parser',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/clock' => array(
'pretty_version' => '1.0.0',
'version' => '1.0.0.0',
'reference' => 'e41a24703d4560fd0acb709162f73b8adfc3aa0d',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/clock',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/container' => array(
'pretty_version' => '2.0.2',
'version' => '2.0.2.0',
'reference' => 'c71ecc56dfe541dbd90c5360474fbc405f8d5963',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/container',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/event-dispatcher' => array(
'pretty_version' => '1.0.0',
'version' => '1.0.0.0',
'reference' => 'dbefd12671e8a14ec7f180cab83036ed26714bb0',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/event-dispatcher',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/http-client' => array(
'pretty_version' => '1.0.3',
'version' => '1.0.3.0',
@ -187,8 +58,7 @@
'psr/http-client-implementation' => array(
'dev_requirement' => false,
'provided' => array(
0 => '*',
1 => '1.0',
0 => '1.0',
),
),
'psr/http-factory' => array(
@ -203,8 +73,7 @@
'psr/http-factory-implementation' => array(
'dev_requirement' => false,
'provided' => array(
0 => '*',
1 => '1.0',
0 => '1.0',
),
),
'psr/http-message' => array(
@ -219,37 +88,9 @@
'psr/http-message-implementation' => array(
'dev_requirement' => false,
'provided' => array(
0 => '*',
1 => '1.0',
0 => '1.0',
),
),
'psr/http-server-handler' => array(
'pretty_version' => '1.0.2',
'version' => '1.0.2.0',
'reference' => '84c4fb66179be4caaf8e97bd239203245302e7d4',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/http-server-handler',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/http-server-middleware' => array(
'pretty_version' => '1.0.2',
'version' => '1.0.2.0',
'reference' => 'c1481f747daaa6a0782775cd6a8c26a1bf4a3829',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/http-server-middleware',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/log' => array(
'pretty_version' => '3.0.2',
'version' => '3.0.2.0',
'reference' => 'f16e1d5863e37f8d8c2a01719f5b34baa2b714d3',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/log',
'aliases' => array(),
'dev_requirement' => false,
),
'ralouphie/getallheaders' => array(
'pretty_version' => '3.0.3',
'version' => '3.0.3.0',
@ -277,32 +118,5 @@
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-uuid' => array(
'pretty_version' => 'v1.37.0',
'version' => '1.37.0.0',
'reference' => '26dfec253c4cf3e51b541b52ddf7e42cb0908e94',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-uuid',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/uid' => array(
'pretty_version' => 'v7.4.9',
'version' => '7.4.9.0',
'reference' => '2676b524340abcfe4d6151ec698463cebafee439',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/uid',
'aliases' => array(),
'dev_requirement' => false,
),
'webmozart/assert' => array(
'pretty_version' => '2.4.1',
'version' => '2.4.1.0',
'reference' => '2ccb7c2e821038c03a3e6e1700c570c158c55f70',
'type' => 'library',
'install_path' => __DIR__ . '/../webmozart/assert',
'aliases' => array(),
'dev_requirement' => false,
),
),
);

@ -1 +0,0 @@
Subproject commit d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca

1
vendor/mcp/sdk vendored

@ -1 +0,0 @@
Subproject commit 433c84b58af346dd32f15f9909679e96a46ebe23

@ -1 +0,0 @@
Subproject commit 8458763e0dd0b6baa310e04f1829fc73da4e8c8a

1
vendor/opis/string vendored

@ -1 +0,0 @@
Subproject commit 3e4d2aaff518ac518530b89bb26ed40f4503635e

1
vendor/opis/uri vendored

@ -1 +0,0 @@
Subproject commit 0f3ca49ab1a5e4a6681c286e0b2cc081b93a7d5a

@ -1 +0,0 @@
Subproject commit 82fe4c73ef3363caed49ff8dd1539ba06044910d

@ -1 +0,0 @@
Subproject commit 1d01c49d4ed62f25aa84a747ad35d5a16924662b

@ -1 +0,0 @@
Subproject commit 7bae67520aa9f5ecc506d646810bd40d9da54582

@ -1 +0,0 @@
Subproject commit 327a05bbee54120d4786a0dc67aad30226ad4cf9

@ -1 +0,0 @@
Subproject commit a004701b11273a26cd7955a61d67a7f1e525a45a

1
vendor/psr/clock vendored

@ -1 +0,0 @@
Subproject commit e41a24703d4560fd0acb709162f73b8adfc3aa0d

@ -1 +0,0 @@
Subproject commit c71ecc56dfe541dbd90c5360474fbc405f8d5963

@ -1 +0,0 @@
Subproject commit dbefd12671e8a14ec7f180cab83036ed26714bb0

@ -1 +0,0 @@
Subproject commit 84c4fb66179be4caaf8e97bd239203245302e7d4

@ -1 +0,0 @@
Subproject commit c1481f747daaa6a0782775cd6a8c26a1bf4a3829

1
vendor/psr/log vendored

@ -1 +0,0 @@
Subproject commit f16e1d5863e37f8d8c2a01719f5b34baa2b714d3

@ -1 +0,0 @@
Subproject commit 26dfec253c4cf3e51b541b52ddf7e42cb0908e94

1
vendor/symfony/uid vendored

@ -1 +0,0 @@
Subproject commit 2676b524340abcfe4d6151ec698463cebafee439

@ -1 +0,0 @@
Subproject commit 2ccb7c2e821038c03a3e6e1700c570c158c55f70