diff --git a/.crmeb/AGENTS.md b/.crmeb/AGENTS.md index 94c66c64b..20bbbff5b 100644 --- a/.crmeb/AGENTS.md +++ b/.crmeb/AGENTS.md @@ -14,7 +14,7 @@ CRMEB v8 电商系统,三端一体: | 区域 | 目录 | 技术栈 | | --- | --- | --- | -| 后端 | `backend/` | PHP 8.2-8.4 + ThinkPHP 8 + MySQL 8 + Redis,多应用入口 `adminapi` / `api` / `outapi` | +| 后端 | `backend/` | PHP 8.2-8.4 + ThinkPHP 8 + MySQL 8 + Redis,多应用入口 `adminapi` / `api` | | 管理后台 | `frontend/admin/` | Vue 3.5 + Element Plus 2.9 + Pinia 2 + vue-router 4 + Vite 5(纯 JS) | | 移动端 | `frontend/uni-app/` | uni-app 官方 CLI(Vite 5)+ Vue 3 + Pinia + Sass | @@ -79,8 +79,8 @@ CRMEB v8 电商系统,三端一体: | `crmeb-admin-dev` | 管理后台页面/组件/接口/路由/插件页面开发 | | `crmeb-admin-ui` | 管理端 UI 规范:设计令牌、布局、页面骨架、组件与交互、深色模式、评审清单 | | `crmeb-uniapp-dev` | 移动端 `frontend/uni-app` 页面、API 封装、条件编译、分包、动态渲染 | -| `crmeb-order-dev` | 订单中台全生命周期开发手册:交易模式、产品类型订单链路、订单事件、状态流转、售后退款 | | `crmeb-dev` | 二开总览与流程导航(含 `references/` 分类子文档,如创建插件/驱动/产品类型/用户端页面) | +| `project-standard-bootstrap` | 项目统一工程规范引导:审计/初始化/修复 `.crmeb` 规则骨架、工具入口、目录契约与文档体系(模板类技能,仅按需用于规范化治理) | 「项目规范化」「工程规范审计」「新项目初始化」 | ## 子智能体索引(`.crmeb/agents/`) diff --git a/.crmeb/README.md b/.crmeb/README.md index f475f46da..13e71c262 100644 --- a/.crmeb/README.md +++ b/.crmeb/README.md @@ -8,7 +8,7 @@ | --- | --- | --- | | 项目约定与任务路由 | [AGENTS.md](AGENTS.md) | 决定本次任务需要读取哪些规则和技能 | | 开发规则 | [规则索引](AGENTS.md#规则索引crmebrules) | 架构、代码、数据库、安全、接口等强制约束 | -| 操作手册 | [技能索引](AGENTS.md#技能索引crmebskills) | 7 个专项技能,详细教程在各自 `references/` 中 | +| 操作手册 | [技能索引](AGENTS.md#技能索引crmebskills) | 8 个专项技能,详细教程在各自 `references/` 中 | | 专项角色 | [角色索引](AGENTS.md#子智能体索引crmebagents) | 架构检查、模块构建、路由校验的执行流程 | | 各端补充约定 | [管理端](scopes/admin.md)、[移动端](scopes/uni-app.md) | 只在对应端任务中读取 | | 项目说明 | [文档索引](../help/docs/文档索引.md) | 项目、架构、模块与运行说明,不代替当前规则或执行授权 | @@ -20,7 +20,7 @@ AGENTS.md AI 开发入口与统一规则索引 README.md 人工查阅入口 rules/ 当前开发规则 - skills/ 7 个技能及按需加载的参考手册 + skills/ 8 个技能及按需加载的参考手册 agents/ 3 个专项角色说明 scopes/ 管理端、移动端补充约定 ``` diff --git a/.crmeb/rules/事件规范.md b/.crmeb/rules/事件规范.md index af5499c2f..d5e5f4425 100644 --- a/.crmeb/rules/事件规范.md +++ b/.crmeb/rules/事件规范.md @@ -99,18 +99,11 @@ v8.1.xu 采用三层架构,事件监听器分布在核心层和模块层: |------|---------------|------| | product/normal | [event.php](../../backend/modules/product/normal/event.php) | 实物商品订单生命周期 + 售后 + 扩展数据 | | product/virtual | [event.php](../../backend/modules/product/virtual/event.php) | 虚拟商品订单生命周期 + 售后 | -| marketing/recharge | [event.php](../../backend/modules/marketing/recharge/event.php) | 充值营销功能 + 服务代理 | -| trade/integral | [event.php](../../backend/modules/trade/integral/event.php) | 积分商城订单 + 退款 | -| trade/combination | [event.php](../../backend/modules/trade/combination/event.php) | 拼团生命周期 + 活动数据代理 | -| trade/bargain | [event.php](../../backend/modules/trade/bargain/event.php) | 砍价生命周期 + 活动数据代理 | -| trade/seckill | [event.php](../../backend/modules/trade/seckill/event.php) | 秒杀活动状态查询 | -| distribution | [event.php](../../backend/modules/distribution/event.php) | 分销佣金 + 用户扩展 + 服务代理 | -| marketing/lottery | [event.php](../../backend/modules/marketing/lottery/event.php) | 抽奖次数缓存 + 营销代理 | | marketing/signin | [event.php](../../backend/modules/marketing/signin/event.php) | 签到提醒 + 营销代理 | -| marketing/newuser | [event.php](../../backend/modules/marketing/newuser/event.php) | 新人注册奖励 | -| channel/wechat_mp | [event.php](../../backend/modules/channel/wechat_mp/event.php) | 公众号渠道 + 物流跟踪 + 粉丝同步 | | channel/wechat_mini | [event.php](../../backend/modules/channel/wechat_mini/event.php) | 小程序渠道 + 直播 API + 小程序码 | +> 其余已安装模块(如 `aiagent`)当前 `event.php` 为空监听,未注册事件时不入上表;新增监听后同步登记。 + --- ## 四、核心事件清单 diff --git a/.crmeb/rules/前端规范.md b/.crmeb/rules/前端规范.md index aa42c5d22..aefb8be85 100644 --- a/.crmeb/rules/前端规范.md +++ b/.crmeb/rules/前端规范.md @@ -16,6 +16,7 @@ description: 前端强制红线、接口契约变更同步清单与管理端 UI 6. **事件总线**:`@/utils/bus` 的 `off` 必须传与 `on` 相同的具名函数引用。 7. **编辑纪律**:同一文件多处修改必须逐处顺序编辑,禁止同批并行编辑同一文件(会互相覆盖静默丢失改动)。 8. **版权头**:新写文件不添加 CRMEB 版权头。 +9. **SchemaPage 组件是跨系统契约,禁止随手改写**:`frontend/admin/src/components/SchemaPage/components/` 下的组件(尤其 SchemaForm 注册的 form-create 自定义组件 SchemaEditor / SchemaCoupon / SchemaCheckboxGroup)被后端 `backend/crmeb/utils/FormBuilder.php` 规则生成、schema 驱动页面与已编译插件 UMD(内含独立打包副本)多方引用,为单一业务需求改写通用组件即违规。能力扩展必须向后兼容:新增可选 props、默认行为与既有 props 语义不变、提交数据结构不变;同一提交同步 FormBuilder 对应方法的 props 文档,并回归全部既有调用方。业务个性化需求优先在业务侧封装组件或经后端规则 props 表达,确需改契约组件时先在方案中列明影响面。 ## 二、移动端红线(frontend/uni-app) diff --git a/.crmeb/rules/升级包规范.md b/.crmeb/rules/升级包规范.md index 0fde966e4..646ea0522 100644 --- a/.crmeb/rules/升级包规范.md +++ b/.crmeb/rules/升级包规范.md @@ -34,7 +34,7 @@ description: 二开改动归档升级程序包规范——首个正式版本发 ## 3. 版本增量脚本(首发后包的骨架,必做) -- 位置:`backend/upgrade/versions/` 目录下,文件名按版本号 `vX.Y.Z.php`;返回数组结构以 [v6.0.2.php](../../backend/upgrade/versions/v6.0.2.php) 为模板:`version` / `code` / `description` / `update_sql` / `update_file`。 +- 位置:`backend/upgrade/versions/` 目录下,文件名按版本号 `vX.Y.Z.php`;脚本返回数组,键为 `version` / `code` / `description` / `update_sql` / `update_file`。 - 版本号:`code = major*100 + minor*10 + patch`(如 v8.0.1 → 801),与文件名严格一致,**只增不减**。 - `update_sql` 条目:`type` 语义(1 建表、2 删表、3 加字段、4 改字段、5 删字段、6 加数据、7 改数据、8 删数据、-1 直接执行);SQL 中表名一律用 `` `@table` `` 占位符,同时在 `table` 字段给出去前缀表名;**禁止硬编码 `eb_` 前缀**。 - **SQL 必须幂等**:加字段/索引前先判存在;插数据用 `REPLACE INTO` 或先查后插;UPDATE/DELETE 必须带明确 WHERE。幂等的原因:执行方按 `upgrade_log` 跳过已执行版本,但同一脚本在别的站点可能因手工二开已部分落库。 diff --git a/.crmeb/rules/目录与文档规范.md b/.crmeb/rules/目录与文档规范.md index 468915ae1..ffa7d057c 100644 --- a/.crmeb/rules/目录与文档规范.md +++ b/.crmeb/rules/目录与文档规范.md @@ -58,7 +58,7 @@ description: 目录与文档规范(全仓库唯一真源)- 三类知识目 **2. Git 合并冲突(文档并发修改)** - Markdown 冲突禁止机械整取(`--ours` / `--theirs` 一键覆盖),须按语义合并双方改动。 -- 索引表(`.crmeb/AGENTS.md`、`tests/README.md` 等)冲突必须**合并双方新增行**后去重,禁止丢弃任何一方的登记。 +- 索引表(`.crmeb/AGENTS.md`、`help/docs/文档索引.md` 等)冲突必须**合并双方新增行**后去重,禁止丢弃任何一方的登记。 - 冲突解决后必须全量重跑 `php think docs:check`,error 清零方可提交(同第五节)。 **3. 本地副本与缓存冲突** diff --git a/.crmeb/skills/crmeb-dev/references/03-系统商品说明/product-type.md b/.crmeb/skills/crmeb-dev/references/03-系统商品说明/product-type.md index 55b0439df..d019d4003 100644 --- a/.crmeb/skills/crmeb-dev/references/03-系统商品说明/product-type.md +++ b/.crmeb/skills/crmeb-dev/references/03-系统商品说明/product-type.md @@ -13,9 +13,8 @@ 已有实现参考: - [backend/modules/product/normal](../../../../../backend/modules/product/normal) — 实物商品 - [backend/modules/product/virtual](../../../../../backend/modules/product/virtual) — 虚拟商品 -- [backend/modules/product/cardkey](../../../../../backend/modules/product/cardkey) — 卡密商品 -> `coupon`(卡券)产品类型尚未实现,目录不存在。 +> `cardkey`(卡密)与 `coupon`(卡券)产品类型尚未实现,目录不存在。 ## 二、前置约束(必须遵守) diff --git a/.crmeb/skills/crmeb-dev/references/05-用户端开发/user-product-detail.md b/.crmeb/skills/crmeb-dev/references/05-用户端开发/user-product-detail.md index 546875f33..3ecc84555 100644 --- a/.crmeb/skills/crmeb-dev/references/05-用户端开发/user-product-detail.md +++ b/.crmeb/skills/crmeb-dev/references/05-用户端开发/user-product-detail.md @@ -546,7 +546,7 @@ event('ProductDetailExtend', $eventData); |---------|-----------|-----------------|---------------------| | normal | [normal/.../ProductDetailExtendListener.php](../../../../../backend/modules/product/normal/listener/ProductDetailExtendListener.php) | custom_form/params_list/label_list/protection_list/type_info/fulfillment_preview | spec(matrix) + fulfillment + store + presale | | virtual | [virtual/.../ProductDetailExtendListener.php](../../../../../backend/modules/product/virtual/listener/ProductDetailExtendListener.php) | custom_form/params_list/label_list/protection_list/type_info/fulfillment_preview | spec(matrix) + fulfillment | -| cardkey | [cardkey/.../ProductDetailExtendListener.php](../../../../../backend/modules/product/cardkey/listener/ProductDetailExtendListener.php) | custom_form/params_list/label_list/protection_list/type_info/fulfillment_preview | spec(chips) + fulfillment(自动发放说明) | +| cardkey | `modules/product/cardkey/listener/ProductDetailExtendListener.php`(未随当前版本提供) | custom_form/params_list/label_list/protection_list/type_info/fulfillment_preview | spec(chips) + fulfillment(自动发放说明) | ### 7.3 监听器实现规范 diff --git a/.crmeb/skills/crmeb-dev/references/06-安装部署/install-deploy.md b/.crmeb/skills/crmeb-dev/references/06-安装部署/install-deploy.md index 60ffca2e5..6ed1ec6ab 100644 --- a/.crmeb/skills/crmeb-dev/references/06-安装部署/install-deploy.md +++ b/.crmeb/skills/crmeb-dev/references/06-安装部署/install-deploy.md @@ -73,7 +73,7 @@ SELECT = 0 `backend/public/install.lock` 存在时跳过安装向导(删除后重新进入安装页)。 -> 全新安装由安装向导导入 `backend/public/install/crmeb_table.sql` 和 `backend/public/install/crmeb_data.sql`,再逐项执行必需模块、默认模块和默认驱动 Installer;不需要手动补跑历史升级 SQL。 +> 全新安装由安装向导导入 `backend/public/install/crmeb_table.sql` 和 `backend/public/install/data/` 基础数据,再逐项执行必需模块、默认模块和默认驱动 Installer;不需要手动补跑历史升级 SQL。 ## 四、数据库 / Redis 连接信息速查 @@ -84,7 +84,7 @@ SELECT = 0 ## 五、首版数据库变更规范(二开必备) -- 核心表结构直接同步到 `backend/public/install/crmeb_table.sql`,核心初始数据同步到 `backend/public/install/crmeb_data.sql`;模块自有结构和数据由对应 `Installer.php` 管理 +- 核心表结构直接同步到 `backend/public/install/crmeb_table.sql`,核心初始数据同步到 `backend/public/install/data/`;模块自有结构和数据由对应 `Installer.php` 管理 - 当前首版不新增版本升级脚本;未来明确进入发布后维护阶段时,再建立增量升级链 - SQL 文件必须保存为 **UTF-8 无 BOM**;导入时连接必须指定 `utf8mb4`,否则中文注释(表/列注释)会存成乱码 `?` - 手动验证 SQL 时的导入命令: diff --git a/.crmeb/skills/crmeb-dev/references/10-消息通知/notice.md b/.crmeb/skills/crmeb-dev/references/10-消息通知/notice.md index b669abfee..dc40141bc 100644 --- a/.crmeb/skills/crmeb-dev/references/10-消息通知/notice.md +++ b/.crmeb/skills/crmeb-dev/references/10-消息通知/notice.md @@ -557,7 +557,7 @@ Notice::to($uid)->sendMsg('my_new_event', '我的新通知', [ ### 10.2 实际 payload 与默认数据核对 -`variable` 仍用 `{key}描述` 格式供后台插入,但 key 必须与监听器 `sendMsg()` 的实际数组一致;模板 `{key}` 和微信 `key_list` 不得引用不存在的业务字段。`to(0)` 为平台 type=2,`to($uid)` 为用户 type=1,同 mark 双侧分别声明。动态发货两个 mark 要显式登记。短信旧编号只属于一号通(`sms_driver_key=yihaotong`),微信 `tempkey` 是短编号,实际商户 `tempid` 留空并由后台配置。重复启用只补空字段,不覆盖商户修改。同键空归属记录由所属 Installer 条件认领并补齐空字段,不重置已有模板、状态或商户凭据;已有其他插件归属的同键记录仍拒绝抢占。通知种子回归脚本属于本地测试资产,当前可用入口必须登记在根 `tests/README.md` 后再作为验收依据。 +`variable` 仍用 `{key}描述` 格式供后台插入,但 key 必须与监听器 `sendMsg()` 的实际数组一致;模板 `{key}` 和微信 `key_list` 不得引用不存在的业务字段。`to(0)` 为平台 type=2,`to($uid)` 为用户 type=1,同 mark 双侧分别声明。动态发货两个 mark 要显式登记。短信旧编号只属于一号通(`sms_driver_key=yihaotong`),微信 `tempkey` 是短编号,实际商户 `tempid` 留空并由后台配置。重复启用只补空字段,不覆盖商户修改。同键空归属记录由所属 Installer 条件认领并补齐空字段,不重置已有模板、状态或商户凭据;已有其他插件归属的同键记录仍拒绝抢占。通知种子回归脚本属于本地测试资产(仓库根 `tests` 目录为本地登记处,可缺失),登记后才能作为验收依据。 ### 10.3 后台配置模板 diff --git a/.crmeb/skills/crmeb-dev/references/11-业务领域/fenxiao.md b/.crmeb/skills/crmeb-dev/references/11-业务领域/fenxiao.md index 83eb636bd..609b3323c 100644 --- a/.crmeb/skills/crmeb-dev/references/11-业务领域/fenxiao.md +++ b/.crmeb/skills/crmeb-dev/references/11-业务领域/fenxiao.md @@ -2,7 +2,7 @@ alwaysApply: false description: 分销返佣业务参考 - 返佣层级结构与计算规则说明;当前返佣链路由 OrderPaySuccess 等事件与 distribution 模块监听器接入,历史流程仅供迁移定位 --- -> **⚠️ 版本说明**:当前返佣链路已中台化——订单支付成功由 `app/services/order/OrderSuccessServices.php` 触发 `OrderPaySuccess` 事件(基于 `eb_order_center` 中台表),`modules/distribution/listener/` 下监听器负责订单快照、佣金发放和用户扩展。本文保留的旧流程只供迁移定位,当前字段与行为以代码为准。 +> **⚠️ 版本说明**:当前返佣链路已中台化——订单支付成功由 `app/services/order/OrderSuccessServices.php` 触发 `OrderPaySuccess` 事件(基于 `eb_order_center` 中台表);`modules/distribution/listener/` 下监听器负责订单快照、佣金发放和用户扩展,但 **distribution 模块未随当前版本提供**,表中相关路径为规划结构,实际以自身二开实现为准。本文保留的旧流程只供迁移定位,当前字段与行为以代码为准。 ## 二、核心架构 @@ -27,9 +27,9 @@ description: 分销返佣业务参考 - 返佣层级结构与计算规则说明 | 服务文件 | 职责 | 关键方法 | |---------|------|---------| | [UserBrokerageServices.php](../../../../../backend/app/services/user/UserBrokerageServices.php) | 用户佣金管理 | `brokerageIncome()`、`orderRefundBrokerageBack()` | -| [OrderCreateSnapshotListener.php](../../../../../backend/modules/distribution/listener/OrderCreateSnapshotListener.php) | 订单创建后写入分销快照 | `OrderCreateAfter` | -| [OrderPaidListener.php](../../../../../backend/modules/distribution/listener/OrderPaidListener.php) | 支付成功后的分销处理 | `OrderPaySuccess` | -| [OrderSpreadBrokerageListener.php](../../../../../backend/modules/distribution/listener/OrderSpreadBrokerageListener.php) | 返佣规则与佣金分配 | `OrderSpreadBrokerage` | +| `modules/distribution/listener/OrderCreateSnapshotListener.php`(未随版本提供) | 订单创建后写入分销快照 | `OrderCreateAfter` | +| `modules/distribution/listener/OrderPaidListener.php`(未随版本提供) | 支付成功后的分销处理 | `OrderPaySuccess` | +| `modules/distribution/listener/OrderSpreadBrokerageListener.php`(未随版本提供) | 返佣规则与佣金分配 | `OrderSpreadBrokerage` | --- diff --git a/.crmeb/skills/crmeb-dev/references/11-业务领域/integral.md b/.crmeb/skills/crmeb-dev/references/11-业务领域/integral.md index f0520801a..cbc2cdc7b 100644 --- a/.crmeb/skills/crmeb-dev/references/11-业务领域/integral.md +++ b/.crmeb/skills/crmeb-dev/references/11-业务领域/integral.md @@ -93,7 +93,7 @@ public function bcDec($key, string $incField, string $inc, string $keyField = nu | 管理员增加积分 | setUserInfo() | [UserServices.php](../../../../../backend/app/services/user/UserServices.php) | system_add | | 后台调整积分 | setUserInfo() | [UserServices.php](../../../../../backend/app/services/user/UserServices.php) | system_add | | 下单赠送积分 | paySuccess() | [OrderSuccessServices.php](../../../../../backend/app/services/order/OrderSuccessServices.php)(约 L106 触发 `OrderPaySuccess` 事件) | gain/product_gain | -| 抽奖中奖 | sendPrize() | [LuckLotteryRecordServices.php](../../../../../backend/modules/marketing/lottery/service/LuckLotteryRecordServices.php)(抽奖模块已迁移至 modules/marketing/lottery/,`app/services/activity/` 下无 lottery/) | lottery_add | +| 抽奖中奖 | sendPrize() | `modules/marketing/lottery/service/LuckLotteryRecordServices.php`(抽奖模块未随当前版本提供,`app/services/activity/` 下无 lottery/) | lottery_add | | 订单退款返还 | execute() | [RefundExecutorServices.php](../../../../../backend/app/services/order/RefundExecutorServices.php) | pay_product_integral_back | ### 4.2 积分减少场景 diff --git a/.crmeb/skills/crmeb-dev/references/13-工具类/basic-tools.md b/.crmeb/skills/crmeb-dev/references/13-工具类/basic-tools.md index 9815004ac..56b51f096 100644 --- a/.crmeb/skills/crmeb-dev/references/13-工具类/basic-tools.md +++ b/.crmeb/skills/crmeb-dev/references/13-工具类/basic-tools.md @@ -113,7 +113,7 @@ $token = $jwtAuth->getToken($uid, 'user'); // 仅签发( $jwtAuth->verifyToken(); // 验签(leeway 60 秒) ``` -真实先例:登录签发 `backend/app/services/user/LoginServices.php:58`;校验基础 `backend/crmeb/auth/AuthTokenVerifier.php:57`;开放接口鉴权 `backend/modules/service/outapi/service/OutAccountServices.php:95`。 +真实先例:登录签发 `backend/app/services/user/LoginServices.php:58`;校验基础 `backend/crmeb/auth/AuthTokenVerifier.php:57`。 --- diff --git a/.crmeb/skills/crmeb-dev/references/13-工具类/form-builder.md b/.crmeb/skills/crmeb-dev/references/13-工具类/form-builder.md index 7d815336b..d87cf17e6 100644 --- a/.crmeb/skills/crmeb-dev/references/13-工具类/form-builder.md +++ b/.crmeb/skills/crmeb-dev/references/13-工具类/form-builder.md @@ -82,7 +82,7 @@ Form::checkbox($field, $title, array $value = [])->options([...]); ``` - `options()` 传二维数组也可:`[['value' => 1, 'label' => '开启'], ...]` -- **条件联动**(真实示例 `backend/modules/service/kefu/service/service/StoreServiceAutoReplyServices.php:104`): +- **条件联动**: ```php Form::radio('type', '回复类型', $data['type'] ?? 'text') diff --git a/.crmeb/skills/crmeb-dev/references/13-工具类/http-tools.md b/.crmeb/skills/crmeb-dev/references/13-工具类/http-tools.md index efd315d76..b4eead448 100644 --- a/.crmeb/skills/crmeb-dev/references/13-工具类/http-tools.md +++ b/.crmeb/skills/crmeb-dev/references/13-工具类/http-tools.md @@ -92,7 +92,7 @@ foreach ($goodsList as &$goods) { 源码:`backend/crmeb/utils/Canvas.php`。基于 GD 库把多张图片 + 多段文字合成一张图(邀请海报、线下收款码),保存到 uploads 并返回相对路径。`__call` 魔法提供全部 `set*` 配置方法。 -### 3.1 使用流程(真实调用 `backend/modules/channel/wechat_mp/service/WechatServices.php:201`) +### 3.1 使用流程 ```php use crmeb\utils\Canvas; diff --git a/.crmeb/skills/crmeb-dev/references/14-定时任务/crontab.md b/.crmeb/skills/crmeb-dev/references/14-定时任务/crontab.md index 663a05f4a..ae02a3e12 100644 --- a/.crmeb/skills/crmeb-dev/references/14-定时任务/crontab.md +++ b/.crmeb/skills/crmeb-dev/references/14-定时任务/crontab.md @@ -99,7 +99,7 @@ mark 在调度时经 `Str::camel()` 归一,**任务标识一律使用驼峰命 ## 五、数据表 eb_system_crontab -建表见 `backend/public/install/crmeb_table.sql`(「eb_system_crontab:定时器」),种子见 `backend/public/install/crmeb_data.sql`(同名注释段);旧表 `eb_system_timer` 已更名为 `eb_system_crontab`。 +建表见 `backend/public/install/crmeb_table.sql`(「eb_system_crontab:定时器」),种子见 `backend/public/install/data/`(同名注释段);旧表 `eb_system_timer` 已更名为 `eb_system_crontab`。 关键字段与调度行为的关系: diff --git a/.crmeb/skills/crmeb-order-dev/references/order-aftersale.md b/.crmeb/skills/crmeb-order-dev/references/order-aftersale.md index 419469b62..75ed30b33 100644 --- a/.crmeb/skills/crmeb-order-dev/references/order-aftersale.md +++ b/.crmeb/skills/crmeb-order-dev/references/order-aftersale.md @@ -34,7 +34,7 @@ AfterSaleRuleResolver::resolve()(合并,app/services/order/rule/) | 退款方案 | `buildRefundPlan / beforeRefundSubmit / applyRefundEffects` | 定制退款金额构成与资金副作用;**副作用必须经 `RefundEffectContextInterface::once()` 登记**保证幂等 | | 表单与动作 | `getFormConfig / getUserActions` | 申请表单、用户侧可用动作 | -参考实现:实物 [NormalAfterSaleHandler](../../../../backend/modules/product/normal/handler/NormalAfterSaleHandler.php)(标准 7 天)、卡密 [CardkeyAfterSaleHandler](../../../../backend/modules/product/cardkey/handler/CardkeyAfterSaleHandler.php)(1 小时 + 退款回收卡密)。 +参考实现:实物 [NormalAfterSaleHandler](../../../../backend/modules/product/normal/handler/NormalAfterSaleHandler.php)(标准 7 天);卡密 `modules/product/cardkey/handler/CardkeyAfterSaleHandler.php`(1 小时 + 退款回收卡密,卡密模块未随当前版本提供)。 ## 三、售后单流程 diff --git a/.crmeb/skills/crmeb-order-dev/references/order-lifecycle.md b/.crmeb/skills/crmeb-order-dev/references/order-lifecycle.md index 409187ea1..e826d3c00 100644 --- a/.crmeb/skills/crmeb-order-dev/references/order-lifecycle.md +++ b/.crmeb/skills/crmeb-order-dev/references/order-lifecycle.md @@ -85,7 +85,7 @@ public function advance(string $orderNo, string $extStatus, array $extra = []): | `OrderCenterListExtendListener` | 按需 | 列表注入扩展字段 | | `OrderTakeListener / OrderDeliveryListener / OrderCommentListener` | 实物类必须 | 收货/发货/评价推进(纯虚拟类通常无这些节点) | -监听器内的通用防御写法:先查中台记录校验 `product_type`/`source_type` 防误触发,再用扩展表当前状态做幂等短路,最后 `advance()`。示例见 [modules/product/cardkey/listener/OrderPaidListener](../../../../backend/modules/product/cardkey/listener/OrderPaidListener.php)。 +监听器内的通用防御写法:先查中台记录校验 `product_type`/`source_type` 防误触发,再用扩展表当前状态做幂等短路,最后 `advance()`。示例参考 `modules/product/cardkey/listener/OrderPaidListener.php`(卡密模块未随当前版本提供,路径为规划参考)。 ## 六、状态异常排查指引 diff --git a/.crmeb/skills/crmeb-order-dev/references/product-type.md b/.crmeb/skills/crmeb-order-dev/references/product-type.md index 7159d3f4e..53bb4afa9 100644 --- a/.crmeb/skills/crmeb-order-dev/references/product-type.md +++ b/.crmeb/skills/crmeb-order-dev/references/product-type.md @@ -33,7 +33,7 @@ | --- | --- | --- | --- | --- | --- | | 实物 normal | [modules/product/normal](../../../../backend/modules/product/normal/) | `normal_order` | `eb_pt_normal_order` | 手动发货 → 物流 → 收货(可选安装) | 是 | | 虚拟 virtual | [modules/product/virtual](../../../../backend/modules/product/virtual/) | `virtual_order` | `eb_pt_virtual_order` | 支付后自动发放网盘链接+提取码 | 否 | -| 卡密 cardkey | [modules/product/cardkey](../../../../backend/modules/product/cardkey/) | `cardkey_order` | `eb_pt_cardkey_order` | 支付后自动发卡,退款自动回收卡密 | 否 | +| 卡密 cardkey | `modules/product/cardkey/`(未随当前版本提供) | `cardkey_order` | `eb_pt_cardkey_order` | 支付后自动发卡,退款自动回收卡密 | 否 | > `coupon`(卡券)类型规划中未实现。活动订单(秒杀/拼团/砍价)不建独立 source_type,复用底层产品类型的订单链路,活动维度经 `activity_type` 关联,见 [交易模式](trade-mode.md#3-3-交易模式与订单表的关系)。 @@ -77,7 +77,7 @@ | `OrderCenterListExtend` 事件 | 向订单列表注入扩展字段(listener 直接改 `$event['list']` 引用) | | `OrderCenterDetailExtend` 事件 | 向详情注入扩展数据 + 自声明渲染 section(`extend` + `extend_sections`,中台不硬编码插件 section) | -监听器示例:[modules/product/normal/listener/OrderCenterDetailExtendListener.php](../../../../backend/modules/product/normal/listener/OrderCenterDetailExtendListener.php)、卡密详情注入发卡数据 [modules/product/cardkey/listener/OrderCenterDetailExtendListener.php](../../../../backend/modules/product/cardkey/listener/OrderCenterDetailExtendListener.php)。 +监听器示例:[modules/product/normal/listener/OrderCenterDetailExtendListener.php](../../../../backend/modules/product/normal/listener/OrderCenterDetailExtendListener.php);卡密详情注入发卡数据见 `modules/product/cardkey/listener/OrderCenterDetailExtendListener.php`(卡密模块未随当前版本提供,路径为规划参考)。 ## 四、新产品类型接入订单链路最小清单 @@ -98,5 +98,5 @@ - Handler 契约:[crmeb/trade/product/ProductTypeHandlerInterface.php](../../../../backend/crmeb/trade/product/ProductTypeHandlerInterface.php)、基类 [BaseProductTypeHandler.php](../../../../backend/crmeb/trade/product/BaseProductTypeHandler.php) - OrderAdapter 契约:[crmeb/trade/order/type/OrderTypeAdapterInterface.php](../../../../backend/crmeb/trade/order/type/OrderTypeAdapterInterface.php)、基类 [BaseOrderTypeAdapter.php](../../../../backend/crmeb/trade/order/type/BaseOrderTypeAdapter.php) -- 标准实现:实物 [modules/product/normal/](../../../../backend/modules/product/normal/)(全流程最全)、卡密 [modules/product/cardkey/](../../../../backend/modules/product/cardkey/)(自动履约 + 退款回收) +- 标准实现:实物 [modules/product/normal/](../../../../backend/modules/product/normal/)(全流程最全);卡密 `modules/product/cardkey/`(自动履约 + 退款回收,未随当前版本提供) - 完整创建教程:[crmeb-dev/03-系统商品说明](../../crmeb-dev/references/03-系统商品说明/product-type.md) diff --git a/.crmeb/skills/crmeb-order-dev/references/trade-mode.md b/.crmeb/skills/crmeb-order-dev/references/trade-mode.md index 4ed289349..490fd41a2 100644 --- a/.crmeb/skills/crmeb-order-dev/references/trade-mode.md +++ b/.crmeb/skills/crmeb-order-dev/references/trade-mode.md @@ -99,10 +99,12 @@ | --- | --- | --- | --- | --- | --- | | `normal` | 普通购买 | `DefaultTradeModeHandler` | [app/AppService.php](../../../../backend/app/AppService.php)(boot 中注册) | 0 | 否 | | `member` | 会员购买 | `MemberTradeModeHandler` | [app/AppService.php](../../../../backend/app/AppService.php)(boot 中注册) | 0 | 否 | -| `seckill` | 秒杀 | `SeckillActivityHandler` | [modules/trade/seckill/App.php](../../../../backend/modules/trade/seckill/App.php) | 1 | 是 | -| `bargain` | 砍价 | `BargainActivityHandler` | [modules/trade/bargain/App.php](../../../../backend/modules/trade/bargain/App.php) | 2 | 是 | -| `combination` | 拼团 | `CombinationActivityHandler` | [modules/trade/combination/App.php](../../../../backend/modules/trade/combination/App.php) | 3 | 是 | -| `integral` | 积分兑换 | `IntegralActivityHandler` | [modules/trade/integral/App.php](../../../../backend/modules/trade/integral/App.php) | — | 是 | +| `seckill` | 秒杀 | `SeckillActivityHandler` | `modules/trade/seckill/App.php`(未随当前版本提供) | 1 | 是 | +| `bargain` | 砍价 | `BargainActivityHandler` | `modules/trade/bargain/App.php`(未随当前版本提供) | 2 | 是 | +| `combination` | 拼团 | `CombinationActivityHandler` | `modules/trade/combination/App.php`(未随当前版本提供) | 3 | 是 | +| `integral` | 积分兑换 | `IntegralActivityHandler` | `modules/trade/integral/App.php`(未随当前版本提供) | — | 是 | + +> 上表营销类模式(秒杀/砍价/拼团/积分兑换)为规划扩展,模块目录未随当前版本提供;接入方式按本节注册写法实现。 注册写法(模块 `App.php::register()`): @@ -155,6 +157,6 @@ $app->make(TradeModeRegistry::class)->register('seckill', SeckillActivityHandler - 注册器:[backend/crmeb/trade/mode/TradeModeRegistry.php](../../../../backend/crmeb/trade/mode/TradeModeRegistry.php) - 契约:[TradeModeHandlerInterface.php](../../../../backend/crmeb/trade/mode/TradeModeHandlerInterface.php)、基类 [BaseTradeModeHandler.php](../../../../backend/crmeb/trade/mode/BaseTradeModeHandler.php)、兜底 [DefaultTradeModeHandler.php](../../../../backend/crmeb/trade/mode/DefaultTradeModeHandler.php) -- 完整示例:拼团 [modules/trade/combination/](../../../../backend/modules/trade/combination/)(钩子覆盖最全)、积分 [modules/trade/integral/](../../../../backend/modules/trade/integral/)(免支付 + 独立订单表) +- 完整示例:拼团 `modules/trade/combination/`(钩子覆盖最全)、积分 `modules/trade/integral/`(免支付 + 独立订单表)——两者均未随当前版本提供,路径为规划参考 - 调用方参考:下单 [CenterOrderController.php](../../../../backend/app/api/controller/v1/order/CenterOrderController.php)、购物车校验 [StoreCartServices.php](../../../../backend/app/services/order/StoreCartServices.php) - 注册中心体检命令:[app/command/RegistryCheck.php](../../../../backend/app/command/RegistryCheck.php) diff --git a/.crmeb/skills/crmeb-plugin-dev/references/mcp-tools.md b/.crmeb/skills/crmeb-plugin-dev/references/mcp-tools.md index 0ebe8d5d1..39bb915c9 100644 --- a/.crmeb/skills/crmeb-plugin-dev/references/mcp-tools.md +++ b/.crmeb/skills/crmeb-plugin-dev/references/mcp-tools.md @@ -1,6 +1,6 @@ --- name: mcp-tools -description: 插件向 MCP 服务贡献工具的完整流程:实现 crmeb\interfaces\McpToolInterface 契约、监听 McpToolsRegister 事件注册工具、工具命名规范、description 与参数 Schema 编写规范、写工具确认声明、异常通道现状与 curl 自测。插件需要让 AI 客户端通过 /api/mcp 调用本插件能力时阅读。 +description: 插件向 MCP 服务贡献工具的完整流程:实现 crmeb\interfaces\McpToolInterface 契约、监听 McpToolsRegister 事件注册工具、工具命名规范、description 与参数 Schema 编写规范、写工具确认声明、label/category 后台展示可选方法、异常通道现状与 curl 自测。插件需要让 AI 客户端通过 /api/mcp 调用本插件能力时阅读。 --- # 插件扩展 MCP 工具 @@ -60,6 +60,11 @@ class CardKeyOrderListTool implements McpToolInterface return '卡密订单'; } + public function category(): string + { + return '订单'; + } + public function inputSchema(): array { return [ @@ -81,8 +86,9 @@ class CardKeyOrderListTool implements McpToolInterface 要点: - 构造器注入 `app/` 层 DAO/Service(容器解析,依赖方向 `modules → app` 合法)。 -- 写工具额外实现可选方法 `public function interaction(): string`,返回 `'execute'` 声明"修改系统数据、需客户端确认";不实现或返回空串视为只读。采用可选方法而非接口成员,保证已注册工具无需随核心改动。 +- 写工具额外实现可选方法 `public function interaction(): string`,返回 `'execute'` 声明"修改系统数据、需客户端确认";删除类写操作(不可逆移除数据)返回 `'delete'`,后台按「(删)」标注展示、AI 客户端同样需确认;不实现或返回空串视为只读。采用可选方法而非接口成员,保证已注册工具无需随核心改动。 - 建议实现可选方法 `public function label(): string` 返回简短中文名称(2~6 字,如「卡密订单」):后台工具列表与账号权限勾选优先展示中文名,`description()` 面向 AI 模型、常为长段说明,不适合做勾选文案;不实现或返回空串时回退展示 `description()`。 +- 建议实现可选方法 `public function category(): string` 返回分类名(2~6 字中文):后台工具列表与账号权限勾选按分类分组展示。内置分类名与展示顺序由 `McpToolRegistry::CATEGORY_ORDER` 约定(用户、商品、订单、售后退款、营销、系统设置、消息通知、开发支持),按业务领域选最贴近的一个;插件也可返回自定义分类名,后台按出现顺序追加为新分组。不实现或返回空串时归入「其他」分组排在最后。 - 同类工具集中放插件 `mcp/` 目录,文件名随意(自动发现约定只对模块内置 `tools/` 目录生效,插件工具一律走事件注册)。 ### 2. 创建监听器 @@ -144,9 +150,9 @@ return [ 补充要点: -- `label()` 与 `description()` 分工:`label()` 是给人看的短中文名(2~6 字,后台工具列表与账号权限勾选展示),`description()` 是给模型看的说明书,两者不要互相替代。 +- `label()` 与 `description()` 分工:`label()` 是给人看的短中文名(2~6 字,后台工具列表与账号权限勾选展示),`description()` 是给模型看的说明书,两者不要互相替代;`category()` 只承担后台分组,不写入 `description()`。 - `description()` 用中文撰写即可,模型理解无障碍;工具名 `name()` 保持 `crmeb_` 前缀 ASCII 标识,这是调用契约,不允许中文。 -- 管理端展示链路:`label`/`description` 随 `tools/list` 下发,AI Agent「同步工具」后写入工具缓存,在 MCP 管理页与对话工具清单中展示;**修改描述后需在内置 MCP 上重新同步工具才对 AI Agent 生效**(服务端 `/api/mcp` 即时生效,无缓存)。 +- 管理端展示链路:`label`/`category`/`description` 随 `tools/list` 下发,AI Agent「同步工具」后写入工具缓存,在 MCP 管理页与对话工具清单中展示;**修改描述后需在内置 MCP 上重新同步工具才对 AI Agent 生效**(服务端 `/api/mcp` 即时生效,无缓存)。 ## 强制约定 @@ -154,7 +160,7 @@ return [ | --- | --- | | 工具命名 | `crmeb_{插件标识}_{动作}`(如 `crmeb_cardkey_order_list`)。同名注册会覆盖既有工具,命名必须带插件标识 | | 参数 Schema | 字段 `description` 必须与 `execute()` 实现严格一致:声明的筛选能力、格式、枚举值实现里必须真的存在(如实现在就匹配订单号/商品标题,不得写"匹配收件人");通配符转义等校验在实现内兜底 | -| 写工具 | 必须声明 `interaction(): 'execute'`,客户端据该声明在执行前取得用户确认。写操作的业务校验与授权不得因 AI 调用而豁免(见 [安全红线.md](../../../rules/安全红线.md)) | +| 写工具 | 必须声明 `interaction(): 'execute'`(删除类不可逆操作返回 `'delete'`,后台按「删」标注),客户端据该声明在执行前取得用户确认。写操作的业务校验与授权不得因 AI 调用而豁免(见 [安全红线.md](../../../rules/安全红线.md)) | | 数据访问 | 读系统表走自有 DAO 只读;写数据只能调用对应核心 Service 公开方法,禁止直写系统表(红线见 [插件开发规范.md](../../../rules/插件开发规范.md) 第三节) | | 生命周期 | 插件禁用/卸载后 `event.php` 不再加载,工具自动失效,Installer 无需任何清理逻辑 | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bcf96c892..93926cecd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,5 +53,5 @@ php think make:module demo --label=演示模块 --type=service - [ ] `php think event:check` 无新增孤儿事件 - [ ] 涉及插件清单变更时 `php think plugin:manifest-check` 通过 - [ ] 接口契约变更已同步两端 `api` 目录、菜单权限与安装 SQL(核对清单见 [.crmeb/rules/前端规范.md](.crmeb/rules/前端规范.md)) -- [ ] 数据表变更已同步 `backend/public/install/crmeb_table.sql` / `crmeb_data.sql` +- [ ] 数据表变更已同步 `backend/public/install/crmeb_table.sql`(基础数据见 `backend/public/install/data/`) - [ ] 提交信息符合上述格式、按主题分批 diff --git a/README.md b/README.md index b6efc7bb2..fc2839577 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ Dev Containers 配置(`.devcontainer/`)不在仓库内,已在 `.gitignore` ## 首次安装配置 -1. 创建数据库,依次导入 `backend/public/install/crmeb_table.sql`(表结构)与 `crmeb_data.sql`(基础数据) +1. 创建数据库,导入 `backend/public/install/crmeb_table.sql`(表结构)与 `backend/public/install/data/` 下的基础数据(推荐直接访问安装向导自动完成) 2. 复制 `backend/.env.example` 为 `backend/.env`,按实际环境修改(文件内有逐项注释) 3. 设置 `backend/runtime`、`backend/public` 目录权限为 777 4. 访问 `http://域名/admin`,默认账号 `admin` / 密码 `crmeb.com` @@ -173,7 +173,6 @@ php think workerman restart | [模块与驱动](help/docs/模块与驱动.md) | 默认安装清单与各业务模块功能说明 | | [管理端 API](help/docs/api/管理端接口.md) | AdminAPI 接口文档 | | [用户端 API](help/docs/api/用户端接口.md) | 用户端 API 接口文档 | -| [对外接口](help/docs/api/对外接口.md) | OutAPI REST 接口文档(第三方接入) | | [MCP 接口](help/docs/api/MCP接口.md) | 独立 MCP 服务的接入、认证、工具与排障说明 | | [数据库设计](help/docs/数据库设计.md) | 数据库表结构设计 | | [开发指南](help/docs/开发指南.md) | 开发环境、规范、流程 | @@ -191,8 +190,8 @@ URL 前缀自动路由到对应应用: | 前缀 | 应用 | 说明 | |------|------|------| | `/admin` | 管理端静态页面 | Vue 管理后台入口,不是 API 前缀 | -| `/adminapi` | `app/adminapi/` + 模块 `route/admin.php` | 管理后台 API;客服工作台位于 `/adminapi/kefu/*` | -| `/api` | `app/api/` + 模块 `route/user.php` | 商城用户 API、模块用户端接口及 `/api/outapi` 开放平台 | +| `/adminapi` | `app/adminapi/` + 模块 `route/admin.php` | 管理后台 API | +| `/api` | `app/api/` + 模块 `route/user.php` | 商城用户 API、模块用户端接口 | --- @@ -234,7 +233,7 @@ URL 前缀自动路由到对应应用: | Claude Code | 自动读取 [CLAUDE.md](CLAUDE.md) | | 支持 llms.txt 的工具 / 网页抓取类 AI | 读取 [llms.txt](llms.txt) | -所有入口最终指向 [.crmeb/AGENTS.md](.crmeb/AGENTS.md):其中定义了项目硬约束(依赖方向、分层、插件零侵入红线)、13 条开发规则、7 个开发技能与 3 个专项子智能体,AI 按索引按需加载即可获得与内部开发者一致的约定。 +所有入口最终指向 [.crmeb/AGENTS.md](.crmeb/AGENTS.md):其中定义了项目硬约束(依赖方向、分层、插件零侵入红线)、16 条开发规则、8 个开发技能与 3 个专项子智能体,AI 按索引按需加载即可获得与内部开发者一致的约定。 ### 连接本项目的 MCP 开发工具 diff --git a/README_ZH.md b/README_ZH.md index f6fab0e5f..2012b9474 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -106,7 +106,7 @@ v8 全新升级为中台化电商架构:**商品中台**统一管理多产品 1.系统安全:JWT认证、角色权限校验、操作日志、系统生产日志、文件校验、数据备份; 2.高性能:Redis缓存、think-queue消息队列、Workerman长连接网关、多种云存储、支持集群部署; 3.多语言:管理端与用户端均支持多语言(vue-i18n),自动识别浏览器语言; -4.驱动扩展:支付、上传、短信、邮件、物流、打印、机器人、采集、发票十大类驱动,同一能力多家供应商自由切换; +4.驱动扩展:支付、上传、短信、邮件、物流、电子面单、打印、机器人、采集、发票十大类驱动,同一能力多家供应商自由切换; 5.实时通信:统一 WebSocket 网关(/ws),支撑后台实时通知等场景; 6.一号通:通用第三方扩展,支持短信、物流查询、电子面单、电子发票、商品采集。 ~~~ diff --git a/backend/app/command/DocsCheck.php b/backend/app/command/DocsCheck.php index d727f2d37..aef765ce2 100644 --- a/backend/app/command/DocsCheck.php +++ b/backend/app/command/DocsCheck.php @@ -42,10 +42,11 @@ class DocsCheck extends Command 'backend/runtime', 'backend/public/uploads', 'backend/public/install.lock', 'docker/mysql/data', 'docker/mysql/log', 'docker/nginx/log', 'frontend/admin/dist', 'frontend/uni-app/dist', + 'tests/', 'backend/tests/', ]; /** 行内路径豁免的目录前缀:过程文档与可分发模板描述的是计划/待办路径,存在性随实施变化 */ - protected array $inlinePathExemptDirs = ['tests/plans/', 'tests/reports/', 'tests/skills/']; + protected array $inlinePathExemptDirs = ['tests/plans/', 'tests/reports/', 'tests/skills/', '.crmeb/skills/project-standard-bootstrap/']; /** @var string 仓库根目录(运行时定位) */ protected string $repoRoot = ''; diff --git a/backend/app/dao/activity/coupon/StoreCouponIssueDao.php b/backend/app/dao/activity/coupon/StoreCouponIssueDao.php index 88a107616..263e8c4e9 100644 --- a/backend/app/dao/activity/coupon/StoreCouponIssueDao.php +++ b/backend/app/dao/activity/coupon/StoreCouponIssueDao.php @@ -269,6 +269,22 @@ class StoreCouponIssueDao extends BaseDao })->select()->toArray(); } + /** + * 按增量原子调整剩余可领取数量 + * @param int $id + * @param int $incCount 总量增量(负数表示缩减发布数量) + * @return int 影响行数 + */ + public function incRemainCount(int $id, int $incCount): int + { + $query = $this->getModel()->where('id', $id); + if ($incCount < 0) { + // 并发兜底:校验后有新领取导致剩余量不足以缩减时拒绝 + $query->where('remain_count', '>=', -$incCount); + } + return $query->inc('remain_count', $incCount)->update(); + } + /** * 获取一条优惠券信息 * @param int $id diff --git a/backend/app/model/product/product/StoreProductLog.php b/backend/app/model/product/product/StoreProductLog.php index 737a2131a..623f1de0a 100644 --- a/backend/app/model/product/product/StoreProductLog.php +++ b/backend/app/model/product/product/StoreProductLog.php @@ -52,11 +52,12 @@ class StoreProductLog extends BaseModel public function storeName() { return $this->hasOne(\app\model\product\CenterProductModel::class, 'id', 'product_id')->bind([ - 'store_name', + // 中台商品表 eb_product_center 无 store_name 列,名称存于 name,按别名绑定 + 'store_name' => 'name', 'image', - 'product_price'=>'price', + 'product_price' => 'price', 'stock', - 'is_show' + 'is_show', ]); } diff --git a/backend/app/services/activity/coupon/StoreCouponIssueServices.php b/backend/app/services/activity/coupon/StoreCouponIssueServices.php index b8454608b..a8da9889a 100644 --- a/backend/app/services/activity/coupon/StoreCouponIssueServices.php +++ b/backend/app/services/activity/coupon/StoreCouponIssueServices.php @@ -93,16 +93,40 @@ class StoreCouponIssueServices extends BaseService */ public function saveCoupon($data) { - if ($data['id']) { - $res = $this->dao->update($data['id'], [ - 'coupon_title' => $data['coupon_title'], - 'title' => $data['coupon_title'], - 'total_count' => $data['total_count'], - 'remain_count' => $data['total_count'], - 'receive_limit' => $data['receive_limit'], - 'status' => $data['status'], - ]); - if (!$res) throw new AdminException('修改失败'); + if (!empty($data['id'])) { + $issueInfo = $this->dao->get((int)$data['id']); + if (!$issueInfo) throw new AdminException('优惠券不存在'); + + $totalCount = (int)$data['total_count']; + if ($issueInfo['is_permanent'] != 1) { + // 与新增口径一致:每人限领不得超过发布数量 + if ((int)$data['receive_limit'] > $totalCount) { + throw new AdminException('用户领取数量不能大于发布数量'); + } + // 已领取数量 = 原总量 - 原剩余量;发布数量不得低于已领取数量 + $issuedCount = (int)$issueInfo['total_count'] - (int)$issueInfo['remain_count']; + if ($totalCount < $issuedCount) { + throw new AdminException('发布数量不能小于已领取数量'); + } + } else { + // 无限张券数量字段不参与(固定为 0) + $totalCount = 0; + } + + // 剩余量按总量增量原子增减,避免覆盖并发领取的扣减 + $this->transaction(function () use ($issueInfo, $totalCount, $data) { + $incCount = $totalCount - (int)$issueInfo['total_count']; + if ($incCount != 0 && !$this->dao->incRemainCount((int)$issueInfo['id'], $incCount)) { + throw new AdminException('发布数量不能小于已领取数量'); + } + $this->dao->update((int)$issueInfo['id'], [ + 'coupon_title' => $data['coupon_title'], + 'title' => $data['coupon_title'], + 'total_count' => $totalCount, + 'receive_limit' => $data['receive_limit'], + 'status' => $data['status'], + ]); + }); return (int)$data['id']; } @@ -134,10 +158,10 @@ class StoreCouponIssueServices extends BaseService if ($data['start_time'] && $data['start_use_time']) { - if ($data['start_time'] < date('Y-m-d 00:00:00')) { + if ($data['start_time'] < strtotime(date('Y-m-d 00:00:00'))) { throw new AdminException('开始领取时间不能小于当前时间'); } - if ($data['start_use_time'] < date('Y-m-d 00:00:00')) { + if ($data['start_use_time'] < strtotime(date('Y-m-d 00:00:00'))) { throw new AdminException('开始使用时间不能小于当前时间'); } if ($data['start_use_time'] < $data['start_time']) { diff --git a/backend/crmeb/interfaces/McpToolInterface.php b/backend/crmeb/interfaces/McpToolInterface.php index 7668af4fa..23a925c7b 100644 --- a/backend/crmeb/interfaces/McpToolInterface.php +++ b/backend/crmeb/interfaces/McpToolInterface.php @@ -28,6 +28,13 @@ namespace crmeb\interfaces; * (2~6 字,如「订单列表」),供管理后台工具列表与账号权限勾选展示; * 不实现或返回空串时后台回退展示 description()。同样采用可选方法而非 * 接口成员,保证第三方已注册工具无需改动。 + * + * 可选约定:工具类可额外实现 `category(): string` 方法返回分类名 + * (2~6 字中文,如「订单」「系统设置」),供管理后台按分类分组展示工具 + * 列表与账号权限勾选项;不实现或返回空串时后台归入「其他」分组。内置 + * 工具使用 McpToolRegistry::CATEGORY_ORDER 约定的分类名,第三方插件工具 + * 可返回自定义分类名,后台按出现顺序追加为新分组。同样采用可选方法而非 + * 接口成员,保证第三方已注册工具无需改动。 */ interface McpToolInterface { diff --git a/backend/crmeb/services/schema/SchemaPageProvider.php b/backend/crmeb/services/schema/SchemaPageProvider.php index da6ddcee4..49629cc17 100644 --- a/backend/crmeb/services/schema/SchemaPageProvider.php +++ b/backend/crmeb/services/schema/SchemaPageProvider.php @@ -52,6 +52,8 @@ interface SchemaPageProvider * @param int $limit 每页条数 * @param array $order 排序(如 ['add_time' => 'desc']),空时使用插件默认排序 * @return array ['list' => array 行数据, 'count' => int 总条数] + * 行数据必须携带唯一 id:前端行动作(详情/表单/动作)固定取 row.id 回查, + * 数据库记录用主键,内存拼装的列表用稳定唯一标识(排序/筛选不得改变 id 语义) */ public function list(array $where, int $page, int $limit, array $order = []): array; diff --git a/backend/crmeb/utils/FormBuilder.php b/backend/crmeb/utils/FormBuilder.php index d39941001..55fabf2dd 100644 --- a/backend/crmeb/utils/FormBuilder.php +++ b/backend/crmeb/utils/FormBuilder.php @@ -90,17 +90,22 @@ class FormBuilder extends Form } /** - * 多列勾选组(form-create 自定义组件) + * 多列勾选组(form-create 自定义组件 · 契约组件) * - * 前端 SchemaForm 已注册 type=SchemaCheckboxGroup 的自定义组件: - * 网格布局多列展示勾选项,支持全选联动与选项悬停提示(option.title, - * 支持多行文本)。 + * 前端 SchemaForm 已注册 type=SchemaCheckboxGroup 的自定义组件。 + * ⚠️ 契约组件:前端实现被 schema 驱动页面与已编译插件 UMD 多方引用, + * 改前端组件只能向后兼容扩展(新增可选 props、默认行为不变,红线见 + * .crmeb/rules/前端规范.md 第一节),并同步本方法注释。已支持能力: + * - 网格布局多列展示(columns),支持全选联动与半选态; + * - 选项 title 悬停多行提示;选项 group 归并分组渲染(分组标题全选/清空 + 已选计数); + * - 选项 disabled=true 单项禁用;props 传 disabled=true 整组禁用; + * - props 传 max=N 限制最多勾选 N 项(默认 0 不限)。 * * @param string $field 字段名 * @param string $title 字段标题 * @param array $value 已勾选值数组 - * @param array $options 选项列表 [{label, value[, title]}] - * @param array $props 组件 props 覆盖:columns 每行列数(默认 2)、selectAll 是否显示全选(默认 true) + * @param array $options 选项列表 [{label, value[, group][, title][, disabled]}] + * @param array $props 组件 props 覆盖:columns 每行列数(默认 2)、selectAll 是否显示全选(默认 true)、disabled 整组禁用、max 最多可勾选数(默认 0 不限) * @return \FormBuilder\Driver\CustomComponent */ public static function checkboxGroup($field, $title, array $value = [], array $options = [], array $props = []) diff --git a/backend/modules/aiagent/.crmeb/AGENTS.md b/backend/modules/aiagent/.crmeb/AGENTS.md index d0035b7b8..da99bb63e 100644 --- a/backend/modules/aiagent/.crmeb/AGENTS.md +++ b/backend/modules/aiagent/.crmeb/AGENTS.md @@ -28,6 +28,10 @@ - 工具返回空数据时直接说明未查到,不编造记录 - 只调用本次对话挂载的工具清单中列出的工具,不要调用未列出的工具 - 需要调用工具时,按系统提示词约定的 JSON 格式输出工具调用;拿到工具结果后再继续回答或调用下一个工具 +- 调用工具返回「无权访问工具 / 没有该工具权限」时,说明该 MCP 账号未勾选此工具,按顺序引导管理员两步处理,缺一不可;处理完成前不要反复重试,也不要换其他方式绕过: + 1. 打开 MCP 权限设置 [MCP 配置](/admin/plugin/mcp/config),找到对应账号点「编辑权限」,勾选该工具并保存; + 2. 再到 AI 对话页 [/admin/aiagent/index](/admin/aiagent/index) 左上角「MCP 管理」,对内置 MCP 执行「同步工具」,新勾选的工具与权限才会进入 AI 助手的工具缓存; + 3. 同步完成后引导用户重试原操作 ## 四、操作确认与用户交互机制 diff --git a/backend/modules/aiagent/.crmeb/skills/crmeb-assistant/SKILL.md b/backend/modules/aiagent/.crmeb/skills/crmeb-assistant/SKILL.md index 8e1c4d0f5..74340c5bd 100644 --- a/backend/modules/aiagent/.crmeb/skills/crmeb-assistant/SKILL.md +++ b/backend/modules/aiagent/.crmeb/skills/crmeb-assistant/SKILL.md @@ -1,10 +1,10 @@ --- -name: CRMEB 业务助手 -description: 用当前已授权的 CRMEB MCP 工具查询商品、订单、真实售后、优惠券与用户数据,核对统计口径后给出业务答复。 +name: 业务助手 +description: 工具查询商品、订单、真实售后、优惠券与用户数据,核对统计口径后给出业务答复。 status: 1 --- -# CRMEB 业务助手 +# 业务助手 处理实时业务问题时,先确定对象、筛选条件、时间范围和指标,再从**本次下发的工具清单**选择工具。内置 MCP 工具对话名称带 `builtin.` 前缀;`crmeb_ask_user` 等对话虚拟工具不带此前缀。未挂载或未授权的工具不可调用,也不可把旧查询结果说成当前数据。 diff --git a/backend/modules/aiagent/README.md b/backend/modules/aiagent/README.md index 5401ff820..968e6d0c7 100644 --- a/backend/modules/aiagent/README.md +++ b/backend/modules/aiagent/README.md @@ -57,7 +57,7 @@ aiagent/ | `eb_ai_agent_task_run` | 任务运行记录(状态、触发方式、摘要、错误、工具次数、用量、起止时间) | | `eb_ai_mcp_server` | MCP Server 注册表(url、api_key、headers、timeout、tools 同步缓存、server_key 唯一) | -依赖核心表 `eb_ai_model`(模型管理,**非本模块自有表**):本模块升级时幂等补充 `full_url` 列,卸载不删除该列。 +依赖核心表 `eb_ai_model`(模型管理,**非本模块自有表**):模型管理为系统级能力,`full_url` 等字段由核心维护,本模块安装/卸载不改动该表结构。 ## 内置资产(.crmeb/) @@ -68,13 +68,13 @@ aiagent/ | `skills/{key}/*.md` | 附属资料,由模型经 `crmeb_skill_load` 按需加载 | 同上 | | `skills/{key}/EXAMPLES.md` | 维护者回归用例,**不进入模型上下文** | 同上(保留名) | -内置 Skill:`crmeb-assistant`(业务助手)、`systemsetting`(系统设置)、`doc`(功能说明)、`emailformat`(邮件格式)、`crontab`(系统定时任务)。 +内置 Skill:`crmeb-assistant`(业务助手)、`user`(用户管理)、`coupon`(优惠券管理)、`systemsetting`(系统设置)、`doc`(功能说明)、`emailformat`(邮件格式)、`crontab`(系统定时任务)。编写要求见 [help/docs/AI Agent 模块.md](../../../help/docs/AI%20Agent%20模块.md)「Skill 体系」。 ## 与 service_mcp 的关系 本模块**不直接引用** `service_mcp` 模块的类(避免跨模块互引),一律经 `RemoteMcpClient` 以 HTTP 调用其 `/api/mcp` 端点。因此: -- 业务工具(商品/订单/售后/优惠券/用户/配置等 33 个)由 `service_mcp` 提供,本模块只负责发现、授权与转发。 +- 业务工具(商品/订单/售后/优惠券/用户/配置等,当前清单见 [help/docs/api/MCP接口.md](../../../../help/docs/api/MCP接口.md))由 `service_mcp` 提供,本模块只负责发现、授权与转发。 - 内置 MCP 可用性要求 `service_mcp` 已启用 + `mcp_enabled=1`(安装默认开启)+ `mcp_appkey` 非空(`McpToolService::available()`)。 - 内置服务工具缓存为空时读取会自动补同步一次(失败 10 分钟冷却),新装站点开箱即用;也可在 MCP 管理页手动「同步工具」。 - 工具的确认交互声明(`interaction`)随「同步工具」写入缓存,`service_mcp` 侧声明变更后需重新同步内置 MCP 才生效。 @@ -85,19 +85,15 @@ aiagent/ - Skill 路由已显式声明 `Route::pattern(['key' => '[A-Za-z0-9_-]+'])`,因全局路由变量规则不含连字符。 - Skill 附属资料的文件名经查询参数 `file` 传递(文件名可能含中文与点号),不走路由变量。 - Web 运行用户需对 `modules/aiagent/.crmeb/skills/` 具备**写权限**。 -- 任务指令与 Skill 正文都会进入模型上下文;改动涉及安全边界的提示词时,同步检查 `tests/aiagent/` 的回归断言。 +- 任务指令与 Skill 正文都会进入模型上下文;改动涉及安全边界的提示词时,人工回归对话工具循环、确认卡与无人值守邮件白名单流程。 ## 定向回归 -不依赖数据库,从 `backend/` 目录执行(仓库无全局 PHPUnit 配置): +模块无随仓库分发的自动化测试脚本(`backend/tests/` 为本地可选目录,缺失属正常)。涉及提示词、工具白名单或任务调度的改动,建议在本地环境人工回归: -```bash -php tests/aiagent/AgentChatFlowTest.php -php tests/aiagent/BuiltinSkillInstructionsTest.php -php tests/mcp/McpContractTest.php -``` - -aiagent 的两个脚本**成功时无输出、退出码 0**,失败抛异常并打印原因。 +1. 对话页流式对话一次含工具调用的问答,确认 `tool` 事件与最终答复正常; +2. 触发一个写工具(如系统配置更新),确认确认卡弹出、拒绝与批准两条路径均正确; +3. 建一个每分钟任务跑一次,确认运行记录、摘要与通知投递符合预期。 ## 注意事项 diff --git a/backend/modules/service/mcp/McpToolRegistry.php b/backend/modules/service/mcp/McpToolRegistry.php index 2ab396423..c5a5b7f94 100644 --- a/backend/modules/service/mcp/McpToolRegistry.php +++ b/backend/modules/service/mcp/McpToolRegistry.php @@ -38,6 +38,14 @@ class McpToolRegistry */ private const TOOL_NAMESPACE = 'modules\\service\\mcp\\tools\\'; + /** + * 内置工具分类展示顺序(与各工具 category() 返回值对应) + * + * 仅约束展示顺序:未知分类(第三方插件工具自定义)按出现顺序追加在后, + * 未声明分类的工具由后台归入「其他」分组排在最后。 + */ + public const CATEGORY_ORDER = ['用户', '商品', '订单', '售后退款', '财务', '营销', '系统设置', '消息通知', '开发支持']; + /** * 自动发现内置工具(扫描 tools/ 目录) * @@ -181,11 +189,43 @@ class McpToolRegistry $definition['label'] = $label; } } + // 可选中文分类:仅实现了 category() 且非空的工具携带该字段,供后台分组展示 + if (method_exists($tool, 'category')) { + $category = trim((string)$tool->category()); + if ($category !== '') { + $definition['category'] = $category; + } + } $definitions[] = $definition; } return $definitions; } + /** + * 按约定顺序整理分类清单(去重) + * + * CATEGORY_ORDER 中的已知分类按约定顺序在前,其余分类(第三方插件 + * 自定义)按出现顺序追加在后;未声明分类(空串)不返回,由调用方 + * 归入「其他」分组。 + * + * @param array $definitions 工具定义列表(allDefinitions() 返回结构) + * @return array 分类名列表 + */ + public static function orderedCategories(array $definitions): array + { + $seen = []; + foreach ($definitions as $definition) { + $category = trim((string)($definition['category'] ?? '')); + if ($category !== '' && !in_array($category, $seen, true)) { + $seen[] = $category; + } + } + return array_merge( + array_values(array_intersect(self::CATEGORY_ORDER, $seen)), + array_values(array_diff($seen, self::CATEGORY_ORDER)) + ); + } + /** * 调用工具(tools/call) * diff --git a/backend/modules/service/mcp/schema/AccountListPage.php b/backend/modules/service/mcp/schema/AccountListPage.php index 8c615505f..45f27e51b 100644 --- a/backend/modules/service/mcp/schema/AccountListPage.php +++ b/backend/modules/service/mcp/schema/AccountListPage.php @@ -174,8 +174,8 @@ class AccountListPage extends BaseSchemaPageProvider // 多列勾选组:两列展示 + 全选联动 + 英文名/描述悬停提示(option.title) $field[] = Form::checkboxGroup('tools', '授权工具', $checked, $this->toolOptions()) ->info($isSystem - ? '系统账号供 AI Agent 内置工具源调用,收紧白名单即收紧 AI Agent 可见范围(调整后需在 AI Agent 的 MCP 管理页重新「同步工具」);标注(写)的为写入/发送类工具' - : '账号仅能调用勾选的工具;标注(写)的为写入/发送类工具') + ? '系统账号供 AI Agent 内置工具源调用,收紧白名单即收紧 AI Agent 可见范围(调整后需在 AI Agent 的 MCP 管理页重新「同步工具」);标注(写)的为写入/发送类工具,标注(删)的为删除类工具' + : '账号仅能调用勾选的工具;标注(写)的为写入/发送类工具,标注(删)的为删除类工具') ->validate([ ['required' => true, 'message' => '请至少勾选一个工具', 'type' => 'array', 'trigger' => 'change'], ]); @@ -289,7 +289,8 @@ class AccountListPage extends BaseSchemaPageProvider * 工具名 → 展示文案映射(当前已注册的全部工具:内置 + 插件经事件追加) * * 文案优先取工具中文名(label()),未提供时回退功能描述,最后回退 - * 工具名;写工具在文案后标注(写),便于管理员识别写入/发送类工具。 + * 工具名;写工具在文案后标注(写)、删除类工具标注(删),便于管理员 + * 识别写入/发送与删除类工具。 * * @return array 工具名 => 展示文案 */ @@ -304,7 +305,9 @@ class AccountListPage extends BaseSchemaPageProvider if ($label === '') { $label = (string)$definition['name']; } - if ((string)($definition['interaction'] ?? '') === 'execute') { + if ((string)($definition['interaction'] ?? '') === 'delete') { + $label .= '(删)'; + } elseif ((string)($definition['interaction'] ?? '') === 'execute') { $label .= '(写)'; } $map[(string)$definition['name']] = $label; @@ -313,18 +316,24 @@ class AccountListPage extends BaseSchemaPageProvider } /** - * 工具勾选选项(文案取自 toolLabelMap()) + * 工具勾选选项(文案取自 toolLabelMap(),按分类分组排序) * + * 选项按注册中心约定分类顺序排列(插件自定义分类按出现顺序追加, + * 未声明分类的归入「其他」),group 字段供前端渲染分组标题与分组全选; * title 携带英文名 + 描述作为悬停提示:form-create 渲染 checkbox 时会把 * 选项的额外字段透传给 el-checkbox 根节点,原生 title 随之生效。 * - * @return array {label, value, title} 选项列表(按工具名排序) + * @return array {label, value, group, title} 选项列表 */ private function toolOptions(): array { $labelMap = $this->toolLabelMap(); $definitions = McpToolRegistry::booted()->allDefinitions(); - usort($definitions, fn(array $a, array $b) => strcmp((string)$a['name'], (string)$b['name'])); + // 分组排序:约定分类在前,插件自定义分类次之,「其他」兜底;组内按工具名排序 + $order = array_merge(McpToolRegistry::orderedCategories($definitions), ['其他']); + usort($definitions, fn(array $a, array $b) => array_search($this->categoryOf($a), $order, true) + <=> array_search($this->categoryOf($b), $order, true) + ?: strcmp((string)$a['name'], (string)$b['name'])); $options = []; foreach ($definitions as $definition) { $name = (string)$definition['name']; @@ -333,11 +342,28 @@ class AccountListPage extends BaseSchemaPageProvider if ($description !== '') { $title .= "\n" . $description; } - $options[] = ['label' => $labelMap[$name], 'value' => $name, 'title' => $title]; + $options[] = [ + 'label' => $labelMap[$name], + 'value' => $name, + 'group' => $this->categoryOf($definition), + 'title' => $title, + ]; } return $options; } + /** + * 工具所属分类(未声明分类的工具归入「其他」) + * + * @param array $definition 工具定义(allDefinitions() 返回结构) + * @return string 分类名 + */ + private function categoryOf(array $definition): string + { + $category = trim((string)($definition['category'] ?? '')); + return $category !== '' ? $category : '其他'; + } + /** * 排序白名单化(支持 id/add_time 升降序,默认 id 倒序) * diff --git a/backend/modules/service/mcp/schema/ToolListPage.php b/backend/modules/service/mcp/schema/ToolListPage.php index 9c4df6145..2e1dfa6ce 100644 --- a/backend/modules/service/mcp/schema/ToolListPage.php +++ b/backend/modules/service/mcp/schema/ToolListPage.php @@ -46,16 +46,29 @@ class ToolListPage extends BaseSchemaPageProvider 'title' => 'MCP 工具列表', 'search' => [ ['field' => 'keyword', 'label' => '关键词', 'type' => 'input', 'placeholder' => '中文名/工具名称/功能描述'], + ['field' => 'category', 'label' => '分类', 'type' => 'select', 'placeholder' => '全部分类', 'options' => $this->categoryOptions()], ], 'columns' => [ ['prop' => 'index', 'label' => '序号', 'type' => 'text', 'width' => 70], ['prop' => 'name', 'label' => '工具名称', 'type' => 'text', 'width' => 220, 'sortable' => true], ['prop' => 'label', 'label' => '中文名称', 'type' => 'text', 'width' => 130], + ['prop' => 'category', 'label' => '分类', 'type' => 'tag', 'width' => 100, + 'map' => [ + '用户' => ['theme' => 'primary'], + '商品' => ['theme' => 'success'], + '订单' => ['theme' => 'warning'], + '售后退款' => ['theme' => 'danger'], + '营销' => ['theme' => 'warning'], + '系统设置' => ['theme' => 'info'], + '消息通知' => ['theme' => 'primary'], + '开发支持' => ['theme' => 'success'], + ]], ['prop' => 'description', 'label' => '功能描述', 'type' => 'ellipsis'], ['prop' => 'params_count', 'label' => '参数数', 'type' => 'number', 'width' => 90], ['prop' => 'interaction', 'label' => '执行方式', 'type' => 'tag', 'width' => 110, 'map' => [ - 'execute' => ['label' => '需写入授权', 'theme' => 'warning'], + 'execute' => ['label' => '写入需确认', 'theme' => 'warning'], + 'delete' => ['label' => '删除需确认', 'theme' => 'danger'], '' => ['label' => '直接执行', 'theme' => 'success'], ]], ], @@ -86,6 +99,11 @@ class ToolListPage extends BaseSchemaPageProvider || str_contains($item['description'], $keyword); })); } + // 分类筛选:选项与行数据的 category 同源(未声明分类归入「其他」) + $category = trim((string)($where['category'] ?? '')); + if ($category !== '') { + $all = array_values(array_filter($all, fn(array $item) => (string)$item['category'] === $category)); + } // 列排序(当前支持 name 列升/降序,保持分页结果稳定;默认注册顺序) $nameOrder = (string)($order['name'] ?? ''); @@ -116,10 +134,12 @@ class ToolListPage extends BaseSchemaPageProvider $fields = [ ['label' => '工具名称', 'value' => $tool['name']], ['label' => '中文名称', 'value' => $tool['label']], + ['label' => '分类', 'value' => $tool['category']], ['label' => '功能描述', 'value' => $tool['description']], ['label' => '执行方式', 'value' => $tool['interaction'], 'type' => 'tag', 'map' => [ - 'execute' => ['label' => '需写入授权', 'theme' => 'warning'], + 'execute' => ['label' => '写入需确认', 'theme' => 'warning'], + 'delete' => ['label' => '删除需确认', 'theme' => 'danger'], '' => ['label' => '直接执行', 'theme' => 'success'], ]], ]; @@ -132,7 +152,7 @@ class ToolListPage extends BaseSchemaPageProvider /** * 加载全部工具定义(内置工具注册到注册中心后取全量定义,幂等) * - * @return array 行数据列表(index/name/label/description/params_count/interaction/inputSchema) + * @return array 行数据列表(id=注册顺序下标/index/name/label/category/description/params_count/interaction/inputSchema) */ protected function loadDefinitions(): array { @@ -140,10 +160,16 @@ class ToolListPage extends BaseSchemaPageProvider $rows = []; foreach ($registry->allDefinitions() as $index => $definition) { + // 未声明分类(含第三方插件工具)归入「其他」,与筛选选项、权限勾选分组口径一致 + $category = trim((string)($definition['category'] ?? '')); $rows[] = [ + // 行 id = 注册顺序下标(同一部署内注册顺序确定,详情按下标回查; + // 前端行动作固定取 row.id,漏传会导致详情恒落在第 0 条) + 'id' => $index, 'index' => $index, 'name' => (string)($definition['name'] ?? ''), 'label' => (string)($definition['label'] ?? ''), + 'category' => $category !== '' ? $category : '其他', 'description' => (string)($definition['description'] ?? ''), 'params_count' => count((array)($definition['inputSchema']['properties'] ?? [])), 'interaction' => (string)($definition['interaction'] ?? ''), @@ -152,4 +178,18 @@ class ToolListPage extends BaseSchemaPageProvider } return $rows; } + + /** + * 分类筛选选项(注册中心约定顺序 + 「其他」兜底) + * + * @return array {label, value} 选项列表 + */ + private function categoryOptions(): array + { + $categories = array_merge( + McpToolRegistry::orderedCategories(McpToolRegistry::booted()->allDefinitions()), + ['其他'] + ); + return array_map(fn(string $category) => ['label' => $category, 'value' => $category], $categories); + } } diff --git a/backend/modules/service/mcp/tools/AfterSaleListTool.php b/backend/modules/service/mcp/tools/AfterSaleListTool.php index 9f85fa936..6f3998b98 100644 --- a/backend/modules/service/mcp/tools/AfterSaleListTool.php +++ b/backend/modules/service/mcp/tools/AfterSaleListTool.php @@ -31,6 +31,12 @@ class AfterSaleListTool implements McpToolInterface { return '售后记录查询'; } + + /** @return string 工具分类(管理后台分组展示) */ + public function category(): string + { + return '售后退款'; + } /** @return array 入参 JSON Schema */ public function inputSchema(): array { diff --git a/backend/modules/service/mcp/tools/AfterSaleStatisticsTool.php b/backend/modules/service/mcp/tools/AfterSaleStatisticsTool.php index 94c81f105..2ced47d23 100644 --- a/backend/modules/service/mcp/tools/AfterSaleStatisticsTool.php +++ b/backend/modules/service/mcp/tools/AfterSaleStatisticsTool.php @@ -31,6 +31,12 @@ class AfterSaleStatisticsTool implements McpToolInterface { return '售后统计'; } + + /** @return string 工具分类(管理后台分组展示) */ + public function category(): string + { + return '售后退款'; + } /** @return array 入参 JSON Schema */ public function inputSchema(): array { diff --git a/backend/modules/service/mcp/tools/CategoryDetailTool.php b/backend/modules/service/mcp/tools/CategoryDetailTool.php index 1aac768f4..50bf88c3d 100644 --- a/backend/modules/service/mcp/tools/CategoryDetailTool.php +++ b/backend/modules/service/mcp/tools/CategoryDetailTool.php @@ -64,6 +64,15 @@ class CategoryDetailTool implements McpToolInterface return '分类详情'; } + /** + * 工具分类(管理后台分组展示) + * @return string + */ + public function category(): string + { + return '商品'; + } + /** * 入参 Schema * @return array diff --git a/backend/modules/service/mcp/tools/CategoryListTool.php b/backend/modules/service/mcp/tools/CategoryListTool.php index d92a9f099..1a06ac5c9 100644 --- a/backend/modules/service/mcp/tools/CategoryListTool.php +++ b/backend/modules/service/mcp/tools/CategoryListTool.php @@ -65,6 +65,15 @@ class CategoryListTool implements McpToolInterface return '分类列表'; } + /** + * 工具分类(管理后台分组展示) + * @return string + */ + public function category(): string + { + return '商品'; + } + /** * 入参 Schema * @return array diff --git a/backend/modules/service/mcp/tools/ConfigTabListTool.php b/backend/modules/service/mcp/tools/ConfigTabListTool.php index af9efe2ad..1c3d3e645 100644 --- a/backend/modules/service/mcp/tools/ConfigTabListTool.php +++ b/backend/modules/service/mcp/tools/ConfigTabListTool.php @@ -79,6 +79,15 @@ class ConfigTabListTool implements McpToolInterface return '配置分组目录'; } + /** + * 工具分类(管理后台分组展示) + * @return string + */ + public function category(): string + { + return '系统设置'; + } + /** * 入参 Schema * @return array diff --git a/backend/modules/service/mcp/tools/CouponClaimListTool.php b/backend/modules/service/mcp/tools/CouponClaimListTool.php index 554eb307d..1c7a67db6 100644 --- a/backend/modules/service/mcp/tools/CouponClaimListTool.php +++ b/backend/modules/service/mcp/tools/CouponClaimListTool.php @@ -39,6 +39,12 @@ class CouponClaimListTool implements McpToolInterface { return '用户券记录'; } + + /** @return string 工具分类(管理后台分组展示) */ + public function category(): string + { + return '营销'; + } /** @return array 入参 JSON Schema */ public function inputSchema(): array { diff --git a/backend/modules/service/mcp/tools/CouponClaimStatisticsTool.php b/backend/modules/service/mcp/tools/CouponClaimStatisticsTool.php index d5ac3cd2b..0a19802c8 100644 --- a/backend/modules/service/mcp/tools/CouponClaimStatisticsTool.php +++ b/backend/modules/service/mcp/tools/CouponClaimStatisticsTool.php @@ -39,6 +39,12 @@ class CouponClaimStatisticsTool implements McpToolInterface { return '发券统计'; } + + /** @return string 工具分类(管理后台分组展示) */ + public function category(): string + { + return '营销'; + } /** @return array 入参 JSON Schema */ public function inputSchema(): array { diff --git a/backend/modules/service/mcp/tools/CouponHoldingStatisticsTool.php b/backend/modules/service/mcp/tools/CouponHoldingStatisticsTool.php index 6d106d721..049a70579 100644 --- a/backend/modules/service/mcp/tools/CouponHoldingStatisticsTool.php +++ b/backend/modules/service/mcp/tools/CouponHoldingStatisticsTool.php @@ -51,6 +51,12 @@ class CouponHoldingStatisticsTool implements McpToolInterface return '持券统计'; } + /** @return string 工具分类(管理后台分组展示) */ + public function category(): string + { + return '营销'; + } + /** @return array MCP 入参 JSON Schema */ public function inputSchema(): array { diff --git a/backend/modules/service/mcp/tools/CouponListTool.php b/backend/modules/service/mcp/tools/CouponListTool.php index a6f76a9b9..fc6206761 100644 --- a/backend/modules/service/mcp/tools/CouponListTool.php +++ b/backend/modules/service/mcp/tools/CouponListTool.php @@ -65,6 +65,15 @@ class CouponListTool implements McpToolInterface return '优惠券配置'; } + /** + * 工具分类(管理后台分组展示) + * @return string + */ + public function category(): string + { + return '营销'; + } + /** * 入参 Schema * @return array diff --git a/backend/modules/service/mcp/tools/CouponUsageStatisticsTool.php b/backend/modules/service/mcp/tools/CouponUsageStatisticsTool.php index 0c344783c..ba08dadae 100644 --- a/backend/modules/service/mcp/tools/CouponUsageStatisticsTool.php +++ b/backend/modules/service/mcp/tools/CouponUsageStatisticsTool.php @@ -52,6 +52,12 @@ class CouponUsageStatisticsTool implements McpToolInterface return '用券统计'; } + /** @return string 工具分类(管理后台分组展示) */ + public function category(): string + { + return '营销'; + } + /** @return array MCP 入参 JSON Schema */ public function inputSchema(): array { diff --git a/backend/modules/service/mcp/tools/DevDocsSearchTool.php b/backend/modules/service/mcp/tools/DevDocsSearchTool.php index b44d8cd8d..5f1666684 100644 --- a/backend/modules/service/mcp/tools/DevDocsSearchTool.php +++ b/backend/modules/service/mcp/tools/DevDocsSearchTool.php @@ -106,6 +106,15 @@ class DevDocsSearchTool implements McpToolInterface return '开发文档检索'; } + /** + * 工具分类(管理后台分组展示) + * @return string + */ + public function category(): string + { + return '开发支持'; + } + /** * 入参 Schema * @return array diff --git a/backend/modules/service/mcp/tools/DevRouteSearchTool.php b/backend/modules/service/mcp/tools/DevRouteSearchTool.php index fa67f1f3d..bf7caa0b8 100644 --- a/backend/modules/service/mcp/tools/DevRouteSearchTool.php +++ b/backend/modules/service/mcp/tools/DevRouteSearchTool.php @@ -94,6 +94,15 @@ class DevRouteSearchTool implements McpToolInterface return '路由定义检索'; } + /** + * 工具分类(管理后台分组展示) + * @return string + */ + public function category(): string + { + return '开发支持'; + } + /** * 入参 Schema * @return array diff --git a/backend/modules/service/mcp/tools/DevTableSchemaTool.php b/backend/modules/service/mcp/tools/DevTableSchemaTool.php index 3220000d1..34b85c42e 100644 --- a/backend/modules/service/mcp/tools/DevTableSchemaTool.php +++ b/backend/modules/service/mcp/tools/DevTableSchemaTool.php @@ -67,6 +67,15 @@ class DevTableSchemaTool implements McpToolInterface return '表结构查询'; } + /** + * 工具分类(管理后台分组展示) + * @return string + */ + public function category(): string + { + return '开发支持'; + } + /** * 入参 Schema * @return array diff --git a/backend/modules/service/mcp/tools/DriverConfigUpdateTool.php b/backend/modules/service/mcp/tools/DriverConfigUpdateTool.php index f30648b1b..7923c51dd 100644 --- a/backend/modules/service/mcp/tools/DriverConfigUpdateTool.php +++ b/backend/modules/service/mcp/tools/DriverConfigUpdateTool.php @@ -88,6 +88,15 @@ class DriverConfigUpdateTool implements McpToolInterface return '驱动配置修改'; } + /** + * 工具分类(管理后台分组展示) + * @return string + */ + public function category(): string + { + return '系统设置'; + } + /** * 入参 Schema * @return array diff --git a/backend/modules/service/mcp/tools/DriverListTool.php b/backend/modules/service/mcp/tools/DriverListTool.php index 3a7f6316d..5d10c4684 100644 --- a/backend/modules/service/mcp/tools/DriverListTool.php +++ b/backend/modules/service/mcp/tools/DriverListTool.php @@ -80,6 +80,15 @@ class DriverListTool implements McpToolInterface return '驱动列表'; } + /** + * 工具分类(管理后台分组展示) + * @return string + */ + public function category(): string + { + return '系统设置'; + } + /** * 入参 Schema * @return array diff --git a/backend/modules/service/mcp/tools/EmailSendTool.php b/backend/modules/service/mcp/tools/EmailSendTool.php index 0009bb842..399c4c762 100644 --- a/backend/modules/service/mcp/tools/EmailSendTool.php +++ b/backend/modules/service/mcp/tools/EmailSendTool.php @@ -68,6 +68,15 @@ class EmailSendTool implements McpToolInterface return '发送邮件'; } + /** + * 工具分类(管理后台分组展示) + * @return string + */ + public function category(): string + { + return '消息通知'; + } + /** * 入参 Schema * @return array diff --git a/backend/modules/service/mcp/tools/EntWechatSendTool.php b/backend/modules/service/mcp/tools/EntWechatSendTool.php index 3a715c34b..8c8e83d46 100644 --- a/backend/modules/service/mcp/tools/EntWechatSendTool.php +++ b/backend/modules/service/mcp/tools/EntWechatSendTool.php @@ -70,6 +70,15 @@ class EntWechatSendTool implements McpToolInterface return '企微机器人推送'; } + /** + * 工具分类(管理后台分组展示) + * @return string + */ + public function category(): string + { + return '消息通知'; + } + /** * 入参 Schema * @return array diff --git a/backend/modules/service/mcp/tools/ExpressListTool.php b/backend/modules/service/mcp/tools/ExpressListTool.php index 4c4bb4bc4..583275a12 100644 --- a/backend/modules/service/mcp/tools/ExpressListTool.php +++ b/backend/modules/service/mcp/tools/ExpressListTool.php @@ -64,6 +64,15 @@ class ExpressListTool implements McpToolInterface return '物流公司列表'; } + /** + * 工具分类(管理后台分组展示) + * @return string + */ + public function category(): string + { + return '订单'; + } + /** * 入参 Schema * @return array diff --git a/backend/modules/service/mcp/tools/InventoryAlertTool.php b/backend/modules/service/mcp/tools/InventoryAlertTool.php index 90d04f030..fa6807f0b 100644 --- a/backend/modules/service/mcp/tools/InventoryAlertTool.php +++ b/backend/modules/service/mcp/tools/InventoryAlertTool.php @@ -42,6 +42,12 @@ class InventoryAlertTool implements McpToolInterface return '库存预警'; } + /** @return string 工具分类(管理后台分组展示) */ + public function category(): string + { + return '商品'; + } + /** @return array 入参 JSON Schema */ public function inputSchema(): array { diff --git a/backend/modules/service/mcp/tools/MenuListTool.php b/backend/modules/service/mcp/tools/MenuListTool.php index ed206b0fd..1e48d88a9 100644 --- a/backend/modules/service/mcp/tools/MenuListTool.php +++ b/backend/modules/service/mcp/tools/MenuListTool.php @@ -66,6 +66,15 @@ class MenuListTool implements McpToolInterface return '后台菜单查询'; } + /** + * 工具分类(管理后台分组展示) + * @return string + */ + public function category(): string + { + return '系统设置'; + } + /** * 入参 Schema * @return array diff --git a/backend/modules/service/mcp/tools/ModuleListTool.php b/backend/modules/service/mcp/tools/ModuleListTool.php index fd3295631..0d74384f1 100644 --- a/backend/modules/service/mcp/tools/ModuleListTool.php +++ b/backend/modules/service/mcp/tools/ModuleListTool.php @@ -51,6 +51,15 @@ class ModuleListTool implements McpToolInterface return '模块清单查询'; } + /** + * 工具分类(管理后台分组展示) + * @return string + */ + public function category(): string + { + return '系统设置'; + } + /** * 入参 Schema * @return array diff --git a/backend/modules/service/mcp/tools/OrderDetailTool.php b/backend/modules/service/mcp/tools/OrderDetailTool.php index 4d166503f..106f44232 100644 --- a/backend/modules/service/mcp/tools/OrderDetailTool.php +++ b/backend/modules/service/mcp/tools/OrderDetailTool.php @@ -66,6 +66,15 @@ class OrderDetailTool implements McpToolInterface return '订单详情'; } + /** + * 工具分类(管理后台分组展示) + * @return string + */ + public function category(): string + { + return '订单'; + } + /** * 入参 Schema * @return array diff --git a/backend/modules/service/mcp/tools/OrderListTool.php b/backend/modules/service/mcp/tools/OrderListTool.php index ea1bdc80a..874431bbd 100644 --- a/backend/modules/service/mcp/tools/OrderListTool.php +++ b/backend/modules/service/mcp/tools/OrderListTool.php @@ -67,6 +67,15 @@ class OrderListTool implements McpToolInterface return '订单列表'; } + /** + * 工具分类(管理后台分组展示) + * @return string + */ + public function category(): string + { + return '订单'; + } + /** * 入参 Schema * @return array @@ -99,20 +108,36 @@ class OrderListTool implements McpToolInterface if (isset($args['status']) && $args['status'] !== '') { $where[] = ['status_code', '=', $args['status']]; } - // 关键词搜索:转义通配符防止注入(中台表无收件人字段,匹配订单号/商品标题) + + // 关键词搜索(订单号/商品标题):管道字段写法(order_no|title)会被 + // BaseDao::filterWhere 当作不存在列静默丢弃,必须用闭包显式 OR,不进 search 条件数组 + // 转义通配符防止注入;中台表无收件人字段 + $keyword = ''; if (!empty($args['keyword'])) { $keyword = addcslashes($args['keyword'], '%_'); - $where[] = ['order_no|title', 'like', '%' . $keyword . '%']; + } + $applyKeyword = static function (\think\db\Query $query) use ($keyword): void { + $query->where(function (\think\db\Query $q) use ($keyword) { + $q->whereLike('order_no', '%' . $keyword . '%') + ->whereOr('title', 'like', '%' . $keyword . '%'); + }); + }; + + $listQuery = $this->dao->search($where); + $countQuery = $this->dao->search($where); + if ($keyword !== '') { + $applyKeyword($listQuery); + $applyKeyword($countQuery); } - $list = $this->dao->search($where) + $list = $listQuery ->field('id,order_no,uid,source_type,total_price,pay_price,pay_status,status_code,refund_status,shipping_type,add_time') ->order('id desc') ->page($page, $limit) ->select() ->toArray(); - $count = $this->dao->search($where)->count(); + $count = $countQuery->count(); return ['list' => $list, 'count' => $count]; } } diff --git a/backend/modules/service/mcp/tools/OrderLogisticsTool.php b/backend/modules/service/mcp/tools/OrderLogisticsTool.php index 963dab690..eac0dca79 100644 --- a/backend/modules/service/mcp/tools/OrderLogisticsTool.php +++ b/backend/modules/service/mcp/tools/OrderLogisticsTool.php @@ -38,6 +38,12 @@ class OrderLogisticsTool implements McpToolInterface { return '订单物流'; } + + /** @return string 工具分类(管理后台分组展示) */ + public function category(): string + { + return '订单'; + } /** @return array 入参 JSON Schema */ public function inputSchema(): array { diff --git a/backend/modules/service/mcp/tools/OrderStatisticsTool.php b/backend/modules/service/mcp/tools/OrderStatisticsTool.php index 8ce2954ed..34599c03c 100644 --- a/backend/modules/service/mcp/tools/OrderStatisticsTool.php +++ b/backend/modules/service/mcp/tools/OrderStatisticsTool.php @@ -35,6 +35,12 @@ class OrderStatisticsTool implements McpToolInterface return '订单统计'; } + /** @return string 工具分类(管理后台分组展示) */ + public function category(): string + { + return '订单'; + } + /** @return array 入参 JSON Schema */ public function inputSchema(): array { diff --git a/backend/modules/service/mcp/tools/ProductDetailTool.php b/backend/modules/service/mcp/tools/ProductDetailTool.php index 8cab88546..e948853e2 100644 --- a/backend/modules/service/mcp/tools/ProductDetailTool.php +++ b/backend/modules/service/mcp/tools/ProductDetailTool.php @@ -67,6 +67,15 @@ class ProductDetailTool implements McpToolInterface return '商品详情'; } + /** + * 工具分类(管理后台分组展示) + * @return string + */ + public function category(): string + { + return '商品'; + } + /** * 入参 Schema * @return array diff --git a/backend/modules/service/mcp/tools/ProductListTool.php b/backend/modules/service/mcp/tools/ProductListTool.php index 6d19c7035..724111a84 100644 --- a/backend/modules/service/mcp/tools/ProductListTool.php +++ b/backend/modules/service/mcp/tools/ProductListTool.php @@ -88,6 +88,15 @@ class ProductListTool implements McpToolInterface return '商品列表'; } + /** + * 工具分类(管理后台分组展示) + * @return string + */ + public function category(): string + { + return '商品'; + } + /** * 入参 Schema * @return array diff --git a/backend/modules/service/mcp/tools/ProductRankingTool.php b/backend/modules/service/mcp/tools/ProductRankingTool.php index 61c84643f..59bad98c3 100644 --- a/backend/modules/service/mcp/tools/ProductRankingTool.php +++ b/backend/modules/service/mcp/tools/ProductRankingTool.php @@ -46,6 +46,12 @@ class ProductRankingTool implements McpToolInterface return '商品排行'; } + /** @return string 工具分类(管理后台分组展示) */ + public function category(): string + { + return '商品'; + } + /** @return array MCP 入参 JSON Schema */ public function inputSchema(): array { diff --git a/backend/modules/service/mcp/tools/ProductStatisticsTool.php b/backend/modules/service/mcp/tools/ProductStatisticsTool.php index 675a53cce..b95954b85 100644 --- a/backend/modules/service/mcp/tools/ProductStatisticsTool.php +++ b/backend/modules/service/mcp/tools/ProductStatisticsTool.php @@ -51,6 +51,12 @@ class ProductStatisticsTool implements McpToolInterface return '商品统计'; } + /** @return string 工具分类(管理后台分组展示) */ + public function category(): string + { + return '商品'; + } + /** @return array MCP 入参 JSON Schema */ public function inputSchema(): array { diff --git a/backend/modules/service/mcp/tools/RefundDetailTool.php b/backend/modules/service/mcp/tools/RefundDetailTool.php index d757c05b6..c47a9f539 100644 --- a/backend/modules/service/mcp/tools/RefundDetailTool.php +++ b/backend/modules/service/mcp/tools/RefundDetailTool.php @@ -65,6 +65,15 @@ class RefundDetailTool implements McpToolInterface return '退款标记详情'; } + /** + * 工具分类(管理后台分组展示) + * @return string + */ + public function category(): string + { + return '售后退款'; + } + /** * 入参 Schema * @return array diff --git a/backend/modules/service/mcp/tools/RefundListTool.php b/backend/modules/service/mcp/tools/RefundListTool.php index 9f2854b7f..db6c7c50e 100644 --- a/backend/modules/service/mcp/tools/RefundListTool.php +++ b/backend/modules/service/mcp/tools/RefundListTool.php @@ -64,6 +64,15 @@ class RefundListTool implements McpToolInterface return '退款订单列表'; } + /** + * 工具分类(管理后台分组展示) + * @return string + */ + public function category(): string + { + return '售后退款'; + } + /** * 入参 Schema * @return array diff --git a/backend/modules/service/mcp/tools/SystemConfigListTool.php b/backend/modules/service/mcp/tools/SystemConfigListTool.php index de1e653c2..3c35abda4 100644 --- a/backend/modules/service/mcp/tools/SystemConfigListTool.php +++ b/backend/modules/service/mcp/tools/SystemConfigListTool.php @@ -67,6 +67,15 @@ class SystemConfigListTool implements McpToolInterface return '系统配置查询'; } + /** + * 工具分类(管理后台分组展示) + * @return string + */ + public function category(): string + { + return '系统设置'; + } + /** * 入参 Schema * @return array diff --git a/backend/modules/service/mcp/tools/SystemConfigUpdateTool.php b/backend/modules/service/mcp/tools/SystemConfigUpdateTool.php index 14185c31d..460472a98 100644 --- a/backend/modules/service/mcp/tools/SystemConfigUpdateTool.php +++ b/backend/modules/service/mcp/tools/SystemConfigUpdateTool.php @@ -84,6 +84,15 @@ class SystemConfigUpdateTool implements McpToolInterface return '系统配置修改'; } + /** + * 工具分类(管理后台分组展示) + * @return string + */ + public function category(): string + { + return '系统设置'; + } + /** * 入参 Schema * @return array diff --git a/backend/modules/service/mcp/tools/UserDetailTool.php b/backend/modules/service/mcp/tools/UserDetailTool.php index 559aa9ff9..809580ce0 100644 --- a/backend/modules/service/mcp/tools/UserDetailTool.php +++ b/backend/modules/service/mcp/tools/UserDetailTool.php @@ -67,6 +67,15 @@ class UserDetailTool implements McpToolInterface return '用户详情'; } + /** + * 工具分类(管理后台分组展示) + * @return string + */ + public function category(): string + { + return '用户'; + } + /** * 入参 Schema * @return array diff --git a/backend/modules/service/mcp/tools/UserListTool.php b/backend/modules/service/mcp/tools/UserListTool.php index ba28e86c0..ba892c085 100644 --- a/backend/modules/service/mcp/tools/UserListTool.php +++ b/backend/modules/service/mcp/tools/UserListTool.php @@ -53,7 +53,8 @@ class UserListTool implements McpToolInterface */ public function description(): string { - return '查询用户列表,支持按昵称/手机号关键词筛选,返回 UID、昵称、余额、积分、等级、注册时间等字段;' + return '查询用户列表,支持按昵称/手机号关键词筛选,支持按用户标签筛选(label_id 传标签 ID 数组,满足任一标签即命中,' + . '标签 ID 可通过 crmeb_user_label_list 查询),返回 UID、昵称、余额、积分、等级、注册时间等字段;' . '手机号已脱敏(中间四位掩码),不含用户收货地址与订单/消费记录,' . '查询单个用户完整信息请用 crmeb_user_detail。'; } @@ -67,6 +68,15 @@ class UserListTool implements McpToolInterface return '用户列表'; } + /** + * 工具分类(管理后台分组展示) + * @return string + */ + public function category(): string + { + return '用户'; + } + /** * 入参 Schema * @return array @@ -79,6 +89,11 @@ class UserListTool implements McpToolInterface 'page' => ['type' => 'number', 'description' => '页码'], 'limit' => ['type' => 'number', 'description' => '每页数量'], 'keyword' => ['type' => 'string', 'description' => '搜索关键词,匹配昵称/手机号'], + 'label_id' => [ + 'type' => 'array', + 'items' => ['type' => 'integer'], + 'description' => '按用户标签筛选,标签 ID 数组(满足任一标签即命中),标签 ID 可通过 crmeb_user_label_list 查询', + ], ], ]; } @@ -86,7 +101,7 @@ class UserListTool implements McpToolInterface /** * 执行工具 * - * @param array $args 参数(page/limit/keyword) + * @param array $args 参数(page/limit/keyword/label_id) * @return array 用户列表和总数 */ public function execute(array $args): array @@ -101,18 +116,43 @@ class UserListTool implements McpToolInterface $where[] = ['nickname|phone', 'like', '%' . $keyword . '%']; } - $list = $this->dao->search($where) + // 标签筛选:兼容单个 ID 或 ID 数组,空值跳过 + $labelIds = []; + if (!empty($args['label_id'])) { + $labelIds = is_array($args['label_id']) ? $args['label_id'] : [$args['label_id']]; + $labelIds = array_values(array_filter(array_map('intval', $labelIds))); + } + + $list = $this->searchQuery($where, $labelIds) ->field('uid,nickname,avatar,phone,now_money,integral,level,add_time') ->order('uid desc') ->page($page, $limit) ->select() ->toArray(); - $count = $this->dao->search($where)->count(); + $count = $this->searchQuery($where, $labelIds)->count(); foreach ($list as &$item) { $item['phone'] = SensitiveData::phone((string)($item['phone'] ?? '')); } unset($item); return ['list' => $list, 'count' => $count]; } + + /** + * 构建查询:基础条件 + 标签子查询(用户拥有任一所选标签即命中) + * + * @param array $where 基础查询条件 + * @param array $labelIds 标签 ID 数组 + * @return \think\db\Query + */ + private function searchQuery(array $where, array $labelIds) + { + $query = $this->dao->search($where); + if ($labelIds) { + $query->whereIn('uid', function ($subQuery) use ($labelIds) { + $subQuery->name('user_label_relation')->whereIn('label_id', $labelIds)->field('uid')->select(); + }); + } + return $query; + } } diff --git a/backend/modules/service/mcp/tools/UserStatisticsTool.php b/backend/modules/service/mcp/tools/UserStatisticsTool.php index e311fd020..eaac6eb8e 100644 --- a/backend/modules/service/mcp/tools/UserStatisticsTool.php +++ b/backend/modules/service/mcp/tools/UserStatisticsTool.php @@ -42,6 +42,12 @@ class UserStatisticsTool implements McpToolInterface return '用户统计'; } + /** @return string 工具分类(管理后台分组展示) */ + public function category(): string + { + return '用户'; + } + /** @return array 入参 JSON Schema */ public function inputSchema(): array { diff --git a/frontend/admin/src/components/SchemaPage/components/SchemaCheckboxGroup.vue b/frontend/admin/src/components/SchemaPage/components/SchemaCheckboxGroup.vue index 14c5a2322..c1b6b07b5 100644 --- a/frontend/admin/src/components/SchemaPage/components/SchemaCheckboxGroup.vue +++ b/frontend/admin/src/components/SchemaPage/components/SchemaCheckboxGroup.vue @@ -6,22 +6,70 @@ class="schema-checkbox-group__all" :model-value="allChecked" :indeterminate="indeterminate" + :disabled="disabled" @change="toggleAll" > 全选 + + + - - + - {{ opt.label }} + {{ opt.label }} @@ -29,12 +77,24 @@