feat: 新增MCP工具集合,包含用户、财务类业务接口

本次提交新增12个MCP工具,覆盖用户管理与财务统计场景:
- 用户类:用户标签列表、标签创建/删除/设置、分组设置、会员等级/时长赠送、用户资料/备注修改
- 财务类:充值记录/统计、余额流水、佣金流水、提现申请/统计、发票记录
所有工具均实现完整的入参校验、业务逻辑与脱敏处理,符合管理后台同链路操作逻辑。
This commit is contained in:
sugar1569 2026-09-24 11:58:20 +08:00
parent 3b5bd37bfb
commit a30565b3a0
20 changed files with 3348 additions and 0 deletions

View File

@ -0,0 +1,412 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace modules\service\mcp\tools;
use app\dao\product\CenterProductDao;
use app\dao\product\product\StoreCategoryDao;
use app\services\activity\coupon\StoreCouponIssueServices;
use crmeb\exceptions\AdminException;
use crmeb\interfaces\McpToolInterface;
use modules\service\mcp\McpException;
/**
* MCP 工具:添加优惠券(创建发行配置)。
*
* 与管理后台「添加优惠券」同链路:组装与 StoreCouponIssueController::saveCoupon
* 一致的数据结构后调用 StoreCouponIssueServices::saveCoupon;参数预检在工具层
* 完成(McpException 消息可透传),服务层业务校验不因 AI 调用而豁免。
*/
class CouponCreateTool implements McpToolInterface
{
/**
* 优惠券发行服务
* @var StoreCouponIssueServices
*/
private StoreCouponIssueServices $services;
/**
* 商品分类 DAO(品类券 ID 存在性校验)
* @var StoreCategoryDao
*/
private StoreCategoryDao $categoryDao;
/**
* 中台商品 DAO(商品券 ID 存在性校验)
* @var CenterProductDao
*/
private CenterProductDao $productDao;
/**
* 构造函数
* @param StoreCouponIssueServices $services 优惠券发行服务
* @param StoreCategoryDao $categoryDao 商品分类 DAO
* @param CenterProductDao $productDao 中台商品 DAO
*/
public function __construct(
StoreCouponIssueServices $services,
StoreCategoryDao $categoryDao,
CenterProductDao $productDao
) {
$this->services = $services;
$this->categoryDao = $categoryDao;
$this->productDao = $productDao;
}
/**
* 工具名称
* @return string
*/
public function name(): string
{
return 'crmeb_coupon_create';
}
/**
* 交互声明:创建优惠券会新增营销配置并可能被用户领取,需用户确认后才真实执行
* @return string execute=需确认后执行
*/
public function interaction(): string
{
return 'execute';
}
/**
* 工具描述
* @return string
*/
public function description(): string
{
return '创建一张新的优惠券发行配置,等同管理后台「添加优惠券」,成功返回新建的 coupon_id;'
. '不能编辑或删除已有优惠券,创建后的停用、开启、删除需在管理后台操作。'
. '必填参数:coupon_title 优惠券名称(18 字以内);coupon_price 面值(元,必须大于 0);'
. 'use_min_price 使用门槛(元,0=无门槛,默认 0)。'
. 'type 优惠券类型:0=通用券(默认)、1=品类券(必须传 category_id 品类 ID 数组)、2=商品券(必须传 product_id 商品 ID 数组);'
. '品类/商品 ID 必须真实存在(工具会校验),创建前先通过 crmeb_category_list 查品类、crmeb_product_list 查商品获取有效 ID。'
. '有效期二选一:coupon_time 领取后有效天数(正整数,默认 0),coupon_time=0 时必须传 start_use_time 与 end_use_time 时间段'
. '(格式 YYYY-MM-DD HH:mm:ss,开始时间不能早于今天且不能早于领取开始时间,结束时间不能早于开始时间;coupon_time 大于 0 时忽略时间段参数)。'
. 'user_type 适用用户:1=普通用户(默认)、2=付费会员专享(此时 receive_type 参数被忽略,服务端固定为会员领取)。'
. 'receive_type 发送方式:1=用户主动领取(默认)、3=系统赠送(固定不限量,忽略发布数量设置)。'
. '发布数量:is_permanent=1 不限量(默认);is_permanent=0 限量时 total_count 发布张数(≥1)必须 ≥ receive_limit 每人限领张数(默认 1)。'
. '领取时间可选:start_time 与 end_time 必须成对提供(YYYY-MM-DD HH:mm:ss,开始时间不能早于今天,结束时间不能早于开始时间),不传表示不限时;'
. '使用有效期开始时间不能早于领取开始时间,若两者同日请将使用开始时间设置为不早于领取开始时刻。'
. 'sort 排序值(默认 0);status:1=开启(默认)、0=关闭。'
. '创建后可通过 crmeb_coupon_list 查询配置,用户领取记录见 crmeb_coupon_claim_list。';
}
/**
* 工具中文名(管理后台展示)
* @return string
*/
public function label(): string
{
return '优惠券创建';
}
/**
* 工具分类(管理后台分组展示)
* @return string
*/
public function category(): string
{
return '营销';
}
/**
* 入参 Schema
* @return array
*/
public function inputSchema(): array
{
return [
'type' => 'object',
'required' => ['coupon_title', 'coupon_price'],
'properties' => [
'coupon_title' => ['type' => 'string', 'maxLength' => 18, 'description' => '优惠券名称,18 字以内'],
'coupon_price' => ['type' => 'number', 'exclusiveMinimum' => 0, 'description' => '优惠券面值(元),必须大于 0'],
'use_min_price' => ['type' => 'number', 'minimum' => 0, 'description' => '使用门槛(最低消费金额,元),0=无门槛,默认 0'],
'type' => ['type' => 'integer', 'enum' => [0, 1, 2], 'description' => '0=通用券(默认),1=品类券,2=商品券'],
'category_id' => ['type' => 'array', 'items' => ['type' => 'integer'], 'description' => '品类券适用的品类 ID 数组(任意层级均可),type=1 时必填,ID 需真实存在,可先通过 crmeb_category_list 查询'],
'product_id' => ['type' => 'array', 'items' => ['type' => 'integer'], 'description' => '商品券适用的中台商品 ID 数组,type=2 时必填,ID 需真实存在,可先通过 crmeb_product_list 查询'],
'coupon_time' => ['type' => 'integer', 'minimum' => 0, 'description' => '领取后有效天数;0=改用 start_use_time/end_use_time 时间段(默认 0),大于 0 时忽略时间段'],
'start_use_time' => ['type' => 'string', 'description' => '使用有效期开始时间,YYYY-MM-DD HH:mm:ss,coupon_time=0 时必填,不能早于今天及领取开始时间'],
'end_use_time' => ['type' => 'string', 'description' => '使用有效期结束时间,YYYY-MM-DD HH:mm:ss,coupon_time=0 时必填,不能早于开始时间'],
'user_type' => ['type' => 'integer', 'enum' => [1, 2], 'description' => '1=普通用户可领(默认),2=付费会员专享'],
'receive_type' => ['type' => 'integer', 'enum' => [1, 3], 'description' => '1=用户主动领取(默认),3=系统赠送;user_type=2 时被忽略'],
'is_permanent' => ['type' => 'integer', 'enum' => [0, 1], 'description' => '1=不限量(默认),0=限量发布'],
'total_count' => ['type' => 'integer', 'minimum' => 1, 'description' => '发布数量(张),is_permanent=0 时必须 ≥ receive_limit,默认 1'],
'receive_limit' => ['type' => 'integer', 'minimum' => 1, 'description' => '每人限领数量(张),默认 1'],
'start_time' => ['type' => 'string', 'description' => '领取开始时间,YYYY-MM-DD HH:mm:ss,与 end_time 成对提供,不能早于今天'],
'end_time' => ['type' => 'string', 'description' => '领取结束时间,YYYY-MM-DD HH:mm:ss,与 start_time 成对提供,不能早于领取开始时间'],
'sort' => ['type' => 'integer', 'description' => '排序值,默认 0'],
'status' => ['type' => 'integer', 'enum' => [0, 1], 'description' => '1=开启(默认),0=关闭'],
],
];
}
/**
* 执行工具
*
* @param array $args 优惠券配置参数
* @return array 新建优惠券 ID 与生效配置摘要
* @throws McpException 参数非法或服务层业务校验失败
*/
public function execute(array $args): array
{
$title = trim((string)($args['coupon_title'] ?? ''));
if ($title === '') {
throw new McpException('coupon_title 不能为空');
}
if (mb_strlen($title) > 18) {
throw new McpException('coupon_title 不能超过 18 个字');
}
$couponPrice = self::numberArg($args, 'coupon_price', 0.0);
if ($couponPrice <= 0) {
throw new McpException('coupon_price 必须大于 0');
}
$useMinPrice = self::numberArg($args, 'use_min_price', 0.0);
if ($useMinPrice < 0) {
throw new McpException('use_min_price 不能小于 0');
}
$type = self::integerArg($args, 'type', 0, 0, 2);
$categoryId = [];
$productId = [];
if ($type === 1) {
$categoryId = self::idArrayArg($args, 'category_id');
if (!$categoryId) {
throw new McpException('品类券(type=1)必须提供 category_id 品类 ID 数组');
}
$this->validateCategoryIds($categoryId);
} elseif ($type === 2) {
$productId = self::idArrayArg($args, 'product_id');
if (!$productId) {
throw new McpException('商品券(type=2)必须提供 product_id 商品 ID 数组');
}
$this->validateProductIds($productId);
}
// 领取时间可选,成对提供;user_type=2 时服务层强制会员领取,这里对齐后台传值
$userType = self::integerArg($args, 'user_type', 1, 1, 2);
$receiveType = $userType === 2 ? 1 : self::integerArg($args, 'receive_type', 1, 1, 3);
$startTime = self::dateTimeArg($args, 'start_time');
$endTime = self::dateTimeArg($args, 'end_time');
if (($startTime === '') !== ($endTime === '')) {
throw new McpException('start_time 与 end_time 必须同时提供');
}
if ($startTime !== '') {
if (strtotime($startTime) < strtotime(date('Y-m-d 00:00:00'))) {
throw new McpException('start_time 领取开始时间不能早于当前时间');
}
if (strtotime($endTime) < strtotime($startTime)) {
throw new McpException('end_time 领取结束时间不能早于领取开始时间');
}
}
// 使用有效期:整天数模式忽略时间段;时间段模式校验成对、不早于今天与先后顺序
$couponTime = self::integerArg($args, 'coupon_time', 0, 0);
$startUseTime = $endUseTime = '';
if ($couponTime === 0) {
$startUseTime = self::dateTimeArg($args, 'start_use_time');
$endUseTime = self::dateTimeArg($args, 'end_use_time');
if ($startUseTime === '' || $endUseTime === '') {
throw new McpException('coupon_time 为 0 时必须提供 start_use_time 与 end_use_time(YYYY-MM-DD HH:mm:ss)');
}
if (strtotime($startUseTime) < strtotime(date('Y-m-d 00:00:00'))) {
throw new McpException('start_use_time 开始使用时间不能早于当前时间');
}
if (strtotime($endUseTime) < strtotime($startUseTime)) {
throw new McpException('end_use_time 结束使用时间不能早于开始使用时间');
}
// 与服务层规则一致:使用开始不能早于领取开始
if ($startTime !== '' && strtotime($startUseTime) < strtotime($startTime)) {
throw new McpException('start_use_time 使用开始时间不能早于 start_time 领取开始时间');
}
}
// 数量规则:系统赠送固定不限量;限量时每人限领不得大于发布数量
$receiveLimit = self::integerArg($args, 'receive_limit', 1, 1);
$isPermanent = self::integerArg($args, 'is_permanent', 1, 0, 1);
if ($receiveType === 3) {
$isPermanent = 1;
}
$totalCount = self::integerArg($args, 'total_count', 1, 1);
if ($isPermanent === 1) {
$totalCount = 0;
} elseif ($receiveLimit > $totalCount) {
throw new McpException('receive_limit 每人限领数量不能大于 total_count 发布数量');
}
$status = self::integerArg($args, 'status', 1, 0, 1);
$sort = self::integerArg($args, 'sort', 0);
// 与 StoreCouponIssueController::saveCoupon 的 postMore 结构保持一致,由服务层完成入库
$data = [
'id' => 0,
'coupon_title' => $title,
'coupon_price' => $couponPrice,
'use_min_price' => $useMinPrice,
'coupon_time' => $couponTime,
'start_use_time' => $startUseTime === '' ? 0 : $startUseTime,
'end_use_time' => $endUseTime === '' ? 0 : $endUseTime,
'start_time' => $startTime === '' ? 0 : $startTime,
'end_time' => $endTime === '' ? 0 : $endTime,
'receive_type' => $receiveType,
'is_permanent' => $isPermanent,
'total_count' => $totalCount,
'product_id' => $productId ? implode(',', $productId) : '',
'category_id' => $categoryId,
'type' => $type,
'sort' => $sort,
'status' => $status,
'receive_limit' => $receiveLimit,
'user_type' => $userType,
];
try {
$couponId = $this->services->saveCoupon($data);
} catch (AdminException $e) {
// 服务层业务校验失败:转 McpException 让消息透传给 AI 客户端
throw new McpException($e->getMessage());
}
return [
'coupon_id' => (int)$couponId,
'coupon_title' => $title,
'coupon_price' => $couponPrice,
'use_min_price' => $useMinPrice,
'type' => $type,
'receive_type' => $userType === 2 ? 4 : $receiveType,
'is_permanent' => $isPermanent,
'total_count' => $totalCount,
'receive_limit' => $receiveLimit,
'status' => $status,
'message' => '优惠券创建成功,可通过 crmeb_coupon_list 查询',
];
}
/**
* @param array $args 工具参数
* @param string $name 参数名称
* @param float $default 默认值
* @return float 数值
* @throws McpException 非数字
*/
private static function numberArg(array $args, string $name, float $default): float
{
$value = $args[$name] ?? $default;
if (is_int($value) || is_float($value)) {
return (float)$value;
}
if (is_string($value) && $value !== '' && is_numeric($value)) {
return (float)$value;
}
throw new McpException($name . ' 必须是数字');
}
/**
* @param array $args 工具参数
* @param string $name 参数名称
* @param int $default 默认值
* @param int|null $min 最小值
* @param int|null $max 最大值
* @return int 整数
* @throws McpException 非整数或超出范围
*/
private static function integerArg(array $args, string $name, int $default, ?int $min = null, ?int $max = null): int
{
$value = $args[$name] ?? $default;
if (!is_int($value) && (!is_string($value) || !ctype_digit($value))) {
throw new McpException($name . ' 必须是整数');
}
$options = array_filter(['min_range' => $min, 'max_range' => $max], static fn($v) => $v !== null);
$number = filter_var($value, FILTER_VALIDATE_INT, ['options' => $options]);
if ($number === false) {
throw new McpException($name . ' 取值超出允许范围');
}
return $number;
}
/**
* @param array $args 工具参数
* @param string $name 参数名称
* @return int[] 去重后的正整数 ID 列表
* @throws McpException 不是 ID 数组
*/
private static function idArrayArg(array $args, string $name): array
{
$value = $args[$name] ?? [];
if (is_int($value) || (is_string($value) && $value !== '')) {
$value = [$value];
}
if (!is_array($value)) {
throw new McpException($name . ' 必须是 ID 数组');
}
$ids = [];
foreach ($value as $item) {
$valid = is_int($item) || (is_string($item) && ctype_digit($item));
if (!$valid || (int)$item <= 0) {
throw new McpException($name . ' 数组内必须全部是大于 0 的整数 ID');
}
$ids[] = (int)$item;
}
return array_values(array_unique($ids));
}
/**
* 校验品类 ID 均真实存在(服务层不校验,防止创建出永远匹配不到商品的无效品类券)
* @param int[] $categoryIds 品类 ID 列表
* @return void
* @throws McpException 存在不存在的品类 ID
*/
private function validateCategoryIds(array $categoryIds): void
{
$exist = (array)$this->categoryDao->getColumn([['id', 'in', $categoryIds]], 'id');
$missing = array_diff($categoryIds, array_map('intval', $exist));
if ($missing) {
throw new McpException('category_id 中以下品类 ID 不存在:' . implode('、', $missing) . ',请先通过 crmeb_category_list 查询有效品类 ID');
}
}
/**
* 校验商品 ID 均真实存在(服务层不校验,防止创建出无法关联商品的无效商品券)
* @param int[] $productIds 商品 ID 列表
* @return void
* @throws McpException 存在不存在的商品 ID
*/
private function validateProductIds(array $productIds): void
{
$exist = (array)$this->productDao->getColumn([['id', 'in', $productIds]], 'id');
$missing = array_diff($productIds, array_map('intval', $exist));
if ($missing) {
throw new McpException('product_id 中以下商品 ID 不存在:' . implode('、', $missing) . ',请先通过 crmeb_product_list 查询有效商品 ID');
}
}
/**
* @param array $args 工具参数
* @param string $name 参数名称
* @return string 可解析的时间字符串,未传返回空串
* @throws McpException 时间格式无效
*/
private static function dateTimeArg(array $args, string $name): string
{
$value = $args[$name] ?? '';
if ($value === '' || $value === 0 || $value === '0') {
return '';
}
if ((!is_string($value) && !is_int($value)) || strtotime((string)$value) === false) {
throw new McpException($name . ' 必须是有效的时间字符串,格式 YYYY-MM-DD HH:mm:ss');
}
return (string)$value;
}
}

View File

@ -0,0 +1,167 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace modules\service\mcp\tools;
use app\dao\user\UserExtractDao;
use crmeb\interfaces\McpToolInterface;
use modules\service\mcp\SensitiveData;
/**
* MCP 工具:提现申请列表
*
* 银行账号、支付宝账号、姓名、微信号等敏感信息脱敏后返回。
*/
class ExtractListTool implements McpToolInterface
{
/**
* 提现 DAO
* @var UserExtractDao
*/
private UserExtractDao $dao;
/**
* 构造函数
* @param UserExtractDao $dao 提现 DAO
*/
public function __construct(UserExtractDao $dao)
{
$this->dao = $dao;
}
/**
* 工具名称
* @return string
*/
public function name(): string
{
return 'crmeb_extract_list';
}
/**
* 工具描述
* @return string
*/
public function description(): string
{
return '查询用户佣金提现申请列表,支持按状态、提现方式、用户 UID 筛选;'
. 'status:0 审核中、1 已提现、-1 未通过(未通过原因见 fail_msg);'
. 'extract_type:bank 银行卡、alipay 支付宝、weixin 微信、balance 转余额;'
. 'balance 字段为提现前佣金余额。银行账号、支付宝账号、姓名、微信号已脱敏。';
}
/**
* 工具中文名(管理后台展示)
* @return string
*/
public function label(): string
{
return '提现申请列表';
}
/**
* 工具分类(管理后台分组展示)
* @return string
*/
public function category(): string
{
return '财务';
}
/**
* 入参 Schema
* @return array
*/
public function inputSchema(): array
{
return [
'type' => 'object',
'properties' => [
'page' => ['type' => 'number', 'description' => '页码'],
'limit' => ['type' => 'number', 'description' => '每页数量'],
'uid' => ['type' => 'number', 'description' => '用户 UID'],
'status' => ['type' => 'number', 'description' => '状态:0 审核中、1 已提现、-1 未通过'],
'extract_type' => ['type' => 'string', 'description' => '提现方式:bank/alipay/weixin/balance'],
],
];
}
/**
* 执行工具
*
* @param array $args 参数(page/limit/uid/status/extract_type)
* @return array 提现申请列表和总数
*/
public function execute(array $args): array
{
$page = max(1, (int)($args['page'] ?? 1));
$limit = min(100, max(1, (int)($args['limit'] ?? 10)));
$where = [];
if (!empty($args['uid'])) {
$where[] = ['uid', '=', (int)$args['uid']];
}
if (isset($args['status']) && $args['status'] !== '') {
$where[] = ['status', '=', (int)$args['status']];
}
if (isset($args['extract_type']) && $args['extract_type'] !== '') {
$where[] = ['extract_type', '=', (string)$args['extract_type']];
}
$list = $this->dao->search($where)
->field('id,uid,real_name,extract_type,bank_code,alipay_code,wechat,extract_price,extract_fee,balance,mark,status,fail_msg,add_time')
->order('id desc')
->page($page, $limit)
->select()
->toArray();
foreach ($list as &$item) {
$item['real_name'] = self::maskName((string)($item['real_name'] ?? ''));
$item['bank_code'] = self::maskAccount((string)($item['bank_code'] ?? ''));
$item['alipay_code'] = self::maskAccount((string)($item['alipay_code'] ?? ''));
$item['wechat'] = self::maskAccount((string)($item['wechat'] ?? ''));
}
unset($item);
$count = $this->dao->search($where)->count();
return ['list' => $list, 'count' => $count];
}
/**
* 姓名脱敏(仅保留首字符)
* @param string $value 姓名
* @return string
*/
private static function maskName(string $value): string
{
if ($value === '') {
return '';
}
return mb_substr($value, 0, 1) . str_repeat('*', max(0, mb_strlen($value) - 1));
}
/**
* 账号类信息脱敏(保留后 4 位)
* @param string $value 账号
* @return string
*/
private static function maskAccount(string $value): string
{
if ($value === '') {
return '';
}
if (mb_strlen($value) <= 4) {
return '******';
}
return str_repeat('*', mb_strlen($value) - 4) . mb_substr($value, -4);
}
}

View File

@ -0,0 +1,129 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace modules\service\mcp\tools;
use app\dao\user\UserExtractDao;
use crmeb\interfaces\McpToolInterface;
use modules\service\mcp\ReportPeriod;
/**
* MCP 工具:提现统计
*
* 按提现申请时间统计各状态笔数与金额。
*/
class ExtractStatisticsTool implements McpToolInterface
{
/**
* 提现 DAO
* @var UserExtractDao
*/
private UserExtractDao $dao;
/**
* 构造函数
* @param UserExtractDao $dao 提现 DAO
*/
public function __construct(UserExtractDao $dao)
{
$this->dao = $dao;
}
/**
* 工具名称
* @return string
*/
public function name(): string
{
return 'crmeb_extract_statistics';
}
/**
* 工具描述
* @return string
*/
public function description(): string
{
return '按提现申请时间(add_time)统计指定时间段内的提现情况:'
. '已提现(成功)笔数、金额与手续费合计,审核中笔数与金额,未通过笔数。'
. '不按审核/打款时间统计,金额为申请金额。默认近7天,最多31天。';
}
/**
* 工具中文名(管理后台展示)
* @return string
*/
public function label(): string
{
return '提现统计';
}
/**
* 工具分类(管理后台分组展示)
* @return string
*/
public function category(): string
{
return '财务';
}
/**
* 入参 Schema
* @return array
*/
public function inputSchema(): array
{
return [
'type' => 'object',
'properties' => [
'start_date' => ['type' => 'string', 'format' => 'date', 'description' => '开始日期 YYYY-MM-DD'],
'end_date' => ['type' => 'string', 'format' => 'date', 'description' => '结束日期 YYYY-MM-DD,包含结束日'],
],
];
}
/**
* 执行工具
*
* @param array $args 参数(start_date/end_date)
* @return array 提现统计结果
*/
public function execute(array $args): array
{
$period = ReportPeriod::fromArgs($args, '提现统计');
$range = [$period['start_time'], $period['end_time']];
$sumFields = ['COUNT(id) AS cnt', 'COALESCE(SUM(extract_price),0) AS amount', 'COALESCE(SUM(extract_fee),0) AS fee'];
$success = $this->dao->search([['status', '=', 1]])
->whereBetween('add_time', $range)->field($sumFields)->find();
$success = $success ? $success->toArray() : [];
$auditing = $this->dao->search([['status', '=', 0]])
->whereBetween('add_time', $range)->field($sumFields)->find();
$auditing = $auditing ? $auditing->toArray() : [];
$fail = $this->dao->search([['status', '=', -1]])
->whereBetween('add_time', $range)->field(['COUNT(id) AS cnt'])->find();
$fail = $fail ? $fail->toArray() : [];
return [
'start_date' => $period['start_date'],
'end_date' => $period['end_date'],
'metrics' => [
'success_count' => (int)($success['cnt'] ?? 0),
'success_amount' => (string)($success['amount'] ?? '0.00'),
'success_fee' => (string)($success['fee'] ?? '0.00'),
'auditing_count' => (int)($auditing['cnt'] ?? 0),
'auditing_amount' => (string)($auditing['amount'] ?? '0.00'),
'fail_count' => (int)($fail['cnt'] ?? 0),
],
];
}
}

View File

@ -0,0 +1,207 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace modules\service\mcp\tools;
use app\dao\user\UserBrokerageDao;
use app\dao\user\UserExtractDao;
use app\dao\user\UserMoneyDao;
use app\dao\user\UserRechargeDao;
use crmeb\interfaces\McpToolInterface;
use modules\service\mcp\ReportPeriod;
/**
* MCP 工具:财务总览统计
*
* 汇总指定时间段内平台核心资金数据:充值、提现、余额流水、佣金流水。
*/
class FinanceOverviewTool implements McpToolInterface
{
/**
* 充值 DAO
* @var UserRechargeDao
*/
private UserRechargeDao $rechargeDao;
/**
* 提现 DAO
* @var UserExtractDao
*/
private UserExtractDao $extractDao;
/**
* 余额流水 DAO
* @var UserMoneyDao
*/
private UserMoneyDao $moneyDao;
/**
* 佣金流水 DAO
* @var UserBrokerageDao
*/
private UserBrokerageDao $brokerageDao;
/**
* 构造函数
* @param UserRechargeDao $rechargeDao 充值 DAO
* @param UserExtractDao $extractDao 提现 DAO
* @param UserMoneyDao $moneyDao 余额流水 DAO
* @param UserBrokerageDao $brokerageDao 佣金流水 DAO
*/
public function __construct(
UserRechargeDao $rechargeDao,
UserExtractDao $extractDao,
UserMoneyDao $moneyDao,
UserBrokerageDao $brokerageDao
) {
$this->rechargeDao = $rechargeDao;
$this->extractDao = $extractDao;
$this->moneyDao = $moneyDao;
$this->brokerageDao = $brokerageDao;
}
/**
* 工具名称
* @return string
*/
public function name(): string
{
return 'crmeb_finance_overview';
}
/**
* 工具描述
* @return string
*/
public function description(): string
{
return '汇总统计指定时间段内平台核心资金数据:'
. '充值(仅已支付,按支付时间统计笔数、金额、赠送金额);'
. '提现(按申请时间统计已提现/审核中/未通过的笔数与金额及成功手续费);'
. '余额流水(按记账时间统计有效记录的收入与支出笔数、金额);'
. '佣金流水(按记账时间统计有效记录的收入与支出笔数、金额,收入含冻结中佣金)。'
. '不包含积分/经验变动,金额单位为元。默认近7天,最多31天。';
}
/**
* 工具中文名(管理后台展示)
* @return string
*/
public function label(): string
{
return '财务总览';
}
/**
* 工具分类(管理后台分组展示)
* @return string
*/
public function category(): string
{
return '财务';
}
/**
* 入参 Schema
* @return array
*/
public function inputSchema(): array
{
return [
'type' => 'object',
'properties' => [
'start_date' => ['type' => 'string', 'format' => 'date', 'description' => '开始日期 YYYY-MM-DD'],
'end_date' => ['type' => 'string', 'format' => 'date', 'description' => '结束日期 YYYY-MM-DD,包含结束日'],
],
];
}
/**
* 执行工具
*
* @param array $args 参数(start_date/end_date)
* @return array 财务总览统计结果
*/
public function execute(array $args): array
{
$period = ReportPeriod::fromArgs($args, '财务总览');
$range = [$period['start_time'], $period['end_time']];
// 充值:仅已支付,按支付时间
$recharge = $this->rechargeDao->search([['paid', '=', 1]])
->whereBetween('pay_time', $range)
->field(['COUNT(id) AS cnt', 'COALESCE(SUM(price),0) AS amount', 'COALESCE(SUM(give_price),0) AS give_amount'])
->find();
$recharge = $recharge ? $recharge->toArray() : [];
// 提现:按申请时间,分状态统计
$extractFields = ['COUNT(id) AS cnt', 'COALESCE(SUM(extract_price),0) AS amount', 'COALESCE(SUM(extract_fee),0) AS fee'];
$extractSuccess = $this->extractDao->search([['status', '=', 1]])
->whereBetween('add_time', $range)->field($extractFields)->find();
$extractSuccess = $extractSuccess ? $extractSuccess->toArray() : [];
$extractAudit = $this->extractDao->search([['status', '=', 0]])
->whereBetween('add_time', $range)->field($extractFields)->find();
$extractAudit = $extractAudit ? $extractAudit->toArray() : [];
$extractFail = $this->extractDao->search([['status', '=', -1]])
->whereBetween('add_time', $range)->field(['COUNT(id) AS cnt'])->find();
$extractFail = $extractFail ? $extractFail->toArray() : [];
// 余额流水:仅有效记录,按记账时间统计收/支
$flowFields = ['COUNT(id) AS cnt', 'COALESCE(SUM(number),0) AS amount'];
$moneyIn = $this->moneyDao->search([['pm', '=', 1], ['status', '=', 1]])
->whereBetween('add_time', $range)->field($flowFields)->find();
$moneyIn = $moneyIn ? $moneyIn->toArray() : [];
$moneyOut = $this->moneyDao->search([['pm', '=', 0], ['status', '=', 1]])
->whereBetween('add_time', $range)->field($flowFields)->find();
$moneyOut = $moneyOut ? $moneyOut->toArray() : [];
// 佣金流水:仅有效记录,按记账时间统计收/支(收入含冻结中佣金)
$brokerageIn = $this->brokerageDao->search([['pm', '=', 1], ['status', '=', 1]])
->whereBetween('add_time', $range)->field($flowFields)->find();
$brokerageIn = $brokerageIn ? $brokerageIn->toArray() : [];
$brokerageOut = $this->brokerageDao->search([['pm', '=', 0], ['status', '=', 1]])
->whereBetween('add_time', $range)->field($flowFields)->find();
$brokerageOut = $brokerageOut ? $brokerageOut->toArray() : [];
return [
'start_date' => $period['start_date'],
'end_date' => $period['end_date'],
'metrics' => [
'recharge' => [
'count' => (int)($recharge['cnt'] ?? 0),
'amount' => (string)($recharge['amount'] ?? '0.00'),
'give_amount' => (string)($recharge['give_amount'] ?? '0.00'),
],
'extract' => [
'success_count' => (int)($extractSuccess['cnt'] ?? 0),
'success_amount' => (string)($extractSuccess['amount'] ?? '0.00'),
'success_fee' => (string)($extractSuccess['fee'] ?? '0.00'),
'auditing_count' => (int)($extractAudit['cnt'] ?? 0),
'auditing_amount' => (string)($extractAudit['amount'] ?? '0.00'),
'fail_count' => (int)($extractFail['cnt'] ?? 0),
],
'user_money' => [
'in_count' => (int)($moneyIn['cnt'] ?? 0),
'in_amount' => (string)($moneyIn['amount'] ?? '0.00'),
'out_count' => (int)($moneyOut['cnt'] ?? 0),
'out_amount' => (string)($moneyOut['amount'] ?? '0.00'),
],
'user_brokerage' => [
'in_count' => (int)($brokerageIn['cnt'] ?? 0),
'in_amount' => (string)($brokerageIn['amount'] ?? '0.00'),
'out_count' => (int)($brokerageOut['cnt'] ?? 0),
'out_amount' => (string)($brokerageOut['amount'] ?? '0.00'),
],
],
];
}
}

View File

@ -0,0 +1,136 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace modules\service\mcp\tools;
use app\dao\order\OrderInvoiceDao;
use crmeb\interfaces\McpToolInterface;
use modules\service\mcp\SensitiveData;
/**
* MCP 工具:发票记录列表
*
* 查询订单开票记录,开票人手机号脱敏后返回。
*/
class InvoiceListTool implements McpToolInterface
{
/**
* 订单发票 DAO
* @var OrderInvoiceDao
*/
private OrderInvoiceDao $dao;
/**
* 构造函数
* @param OrderInvoiceDao $dao 订单发票 DAO
*/
public function __construct(OrderInvoiceDao $dao)
{
$this->dao = $dao;
}
/**
* 工具名称
* @return string
*/
public function name(): string
{
return 'crmeb_invoice_list';
}
/**
* 工具描述
* @return string
*/
public function description(): string
{
return '查询订单开票记录列表,支持按开票状态、订单 ID、用户 UID 筛选;'
. 'header_type 抬头类型:1 个人、2 企业;type 发票类型:1 普通发票、2 专用发票;'
. 'is_pay 是否支付、is_refund 订单是否退款、is_invoice 是否已开票;'
. 'invoice_number 为开票票号。开票人手机号与注册电话已脱敏。';
}
/**
* 工具中文名(管理后台展示)
* @return string
*/
public function label(): string
{
return '发票记录列表';
}
/**
* 工具分类(管理后台分组展示)
* @return string
*/
public function category(): string
{
return '财务';
}
/**
* 入参 Schema
* @return array
*/
public function inputSchema(): array
{
return [
'type' => 'object',
'properties' => [
'page' => ['type' => 'number', 'description' => '页码'],
'limit' => ['type' => 'number', 'description' => '每页数量'],
'uid' => ['type' => 'number', 'description' => '用户 UID'],
'order_id' => ['type' => 'number', 'description' => '订单 ID'],
'is_invoice' => ['type' => 'number', 'description' => '开票状态:1 已开票、0 未开票'],
],
];
}
/**
* 执行工具
*
* @param array $args 参数(page/limit/uid/order_id/is_invoice)
* @return array 发票记录列表和总数
*/
public function execute(array $args): array
{
$page = max(1, (int)($args['page'] ?? 1));
$limit = min(100, max(1, (int)($args['limit'] ?? 10)));
$where = [['is_del', '=', 0]];
if (!empty($args['uid'])) {
$where[] = ['uid', '=', (int)$args['uid']];
}
if (!empty($args['order_id'])) {
$where[] = ['order_id', '=', (int)$args['order_id']];
}
if (isset($args['is_invoice']) && $args['is_invoice'] !== '') {
$where[] = ['is_invoice', '=', (int)$args['is_invoice'] == 1 ? 1 : 0];
}
$list = $this->dao->search($where)
->field('id,uid,order_id,header_type,type,name,duty_number,drawer_phone,email,tell,is_pay,is_refund,is_invoice,invoice_number,invoice_time,add_time')
->order('id desc')
->page($page, $limit)
->select()
->toArray();
foreach ($list as &$item) {
$item['drawer_phone'] = SensitiveData::phone((string)($item['drawer_phone'] ?? ''));
$item['tell'] = SensitiveData::phone((string)($item['tell'] ?? ''));
}
unset($item);
$count = $this->dao->search($where)->count();
return ['list' => $list, 'count' => $count];
}
}

View File

@ -0,0 +1,128 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace modules\service\mcp\tools;
use app\dao\user\UserRechargeDao;
use crmeb\interfaces\McpToolInterface;
/**
* MCP 工具:充值记录列表
*
* 查询用户余额充值记录。
*/
class RechargeListTool implements McpToolInterface
{
/**
* 充值 DAO
* @var UserRechargeDao
*/
private UserRechargeDao $dao;
/**
* 构造函数
* @param UserRechargeDao $dao 充值 DAO
*/
public function __construct(UserRechargeDao $dao)
{
$this->dao = $dao;
}
/**
* 工具名称
* @return string
*/
public function name(): string
{
return 'crmeb_recharge_list';
}
/**
* 工具描述
* @return string
*/
public function description(): string
{
return '查询用户余额充值记录列表,支持按支付状态、充值(支付)方式、用户 UID 筛选,'
. '返回充值订单号、充值金额、赠送金额、退款金额与支付时间;'
. 'paid:1 已支付、0 未支付;recharge_type 为实际支付渠道:weixin 微信、alipay 支付宝、balance 余额。';
}
/**
* 工具中文名(管理后台展示)
* @return string
*/
public function label(): string
{
return '充值记录列表';
}
/**
* 工具分类(管理后台分组展示)
* @return string
*/
public function category(): string
{
return '财务';
}
/**
* 入参 Schema
* @return array
*/
public function inputSchema(): array
{
return [
'type' => 'object',
'properties' => [
'page' => ['type' => 'number', 'description' => '页码'],
'limit' => ['type' => 'number', 'description' => '每页数量'],
'uid' => ['type' => 'number', 'description' => '用户 UID'],
'paid' => ['type' => 'number', 'description' => '支付状态:1 已支付、0 未支付'],
'recharge_type' => ['type' => 'string', 'description' => '充值(支付)方式:weixin/alipay/balance'],
],
];
}
/**
* 执行工具
*
* @param array $args 参数(page/limit/uid/paid/recharge_type)
* @return array 充值记录列表和总数
*/
public function execute(array $args): array
{
$page = max(1, (int)($args['page'] ?? 1));
$limit = min(100, max(1, (int)($args['limit'] ?? 10)));
$where = [];
if (!empty($args['uid'])) {
$where[] = ['uid', '=', (int)$args['uid']];
}
if (isset($args['paid']) && $args['paid'] !== '') {
$where[] = ['paid', '=', (int)$args['paid'] == 1 ? 1 : 0];
}
if (isset($args['recharge_type']) && $args['recharge_type'] !== '') {
$where[] = ['recharge_type', '=', (string)$args['recharge_type']];
}
$list = $this->dao->search($where)
->field('id,uid,order_id,price,give_price,recharge_type,paid,pay_time,refund_price,channel_type,add_time')
->order('id desc')
->page($page, $limit)
->select()
->toArray();
$count = $this->dao->search($where)->count();
return ['list' => $list, 'count' => $count];
}
}

View File

@ -0,0 +1,129 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace modules\service\mcp\tools;
use app\dao\user\UserRechargeDao;
use crmeb\interfaces\McpToolInterface;
use modules\service\mcp\ReportPeriod;
/**
* MCP 工具:充值统计
*
* 按支付时间统计已支付充值,与后台财务统计口径一致。
*/
class RechargeStatisticsTool implements McpToolInterface
{
/**
* 充值 DAO
* @var UserRechargeDao
*/
private UserRechargeDao $dao;
/**
* 构造函数
* @param UserRechargeDao $dao 充值 DAO
*/
public function __construct(UserRechargeDao $dao)
{
$this->dao = $dao;
}
/**
* 工具名称
* @return string
*/
public function name(): string
{
return 'crmeb_recharge_statistics';
}
/**
* 工具描述
* @return string
*/
public function description(): string
{
return '按支付时间(pay_time)统计指定时间段内已支付(paid=1)的余额充值:'
. '充值笔数、充值金额、充值活动赠送金额,以及该时段内充值订单的累计退款金额。'
. '未支付记录不计入。默认近7天,最多31天。';
}
/**
* 工具中文名(管理后台展示)
* @return string
*/
public function label(): string
{
return '充值统计';
}
/**
* 工具分类(管理后台分组展示)
* @return string
*/
public function category(): string
{
return '财务';
}
/**
* 入参 Schema
* @return array
*/
public function inputSchema(): array
{
return [
'type' => 'object',
'properties' => [
'start_date' => ['type' => 'string', 'format' => 'date', 'description' => '开始日期 YYYY-MM-DD'],
'end_date' => ['type' => 'string', 'format' => 'date', 'description' => '结束日期 YYYY-MM-DD,包含结束日'],
],
];
}
/**
* 执行工具
*
* @param array $args 参数(start_date/end_date)
* @return array 充值统计结果
*/
public function execute(array $args): array
{
$period = ReportPeriod::fromArgs($args, '充值统计');
$range = [$period['start_time'], $period['end_time']];
$paid = $this->dao->search([['paid', '=', 1]])
->whereBetween('pay_time', $range)
->field(['COUNT(id) AS cnt', 'COALESCE(SUM(price),0) AS amount', 'COALESCE(SUM(give_price),0) AS give_amount'])
->find();
$paid = $paid ? $paid->toArray() : [];
$refunded = $this->dao->search([['paid', '=', 1], ['refund_price', '>', 0]])
->whereBetween('pay_time', $range)
->field(['COUNT(id) AS cnt', 'COALESCE(SUM(refund_price),0) AS amount'])
->find();
$refunded = $refunded ? $refunded->toArray() : [];
return [
'start_date' => $period['start_date'],
'end_date' => $period['end_date'],
'metrics' => [
'recharge_count' => (int)($paid['cnt'] ?? 0),
'recharge_amount' => (string)($paid['amount'] ?? '0.00'),
'give_amount' => (string)($paid['give_amount'] ?? '0.00'),
'refund_count' => (int)($refunded['cnt'] ?? 0),
'refund_amount' => (string)($refunded['amount'] ?? '0.00'),
],
];
}
}

View File

@ -0,0 +1,135 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace modules\service\mcp\tools;
use app\dao\user\UserBrokerageDao;
use crmeb\interfaces\McpToolInterface;
/**
* MCP 工具:佣金流水列表
*
* v8 起分销佣金变动记于独立账单表 user_brokerage。
*/
class UserBrokerageListTool implements McpToolInterface
{
/**
* 佣金流水 DAO
* @var UserBrokerageDao
*/
private UserBrokerageDao $dao;
/**
* 构造函数
* @param UserBrokerageDao $dao 佣金流水 DAO
*/
public function __construct(UserBrokerageDao $dao)
{
$this->dao = $dao;
}
/**
* 工具名称
* @return string
*/
public function name(): string
{
return 'crmeb_user_brokerage_list';
}
/**
* 工具描述
* @return string
*/
public function description(): string
{
return '查询用户分销佣金流水列表(v8 独立佣金账单表),支持按用户 UID、类型、收/支方向、状态筛选,'
. '返回变动金额、变动后佣金、冻结到期时间与备注;'
. 'type 常见值:self_brokerage 自购订单佣金、one_brokerage/two_brokerage 一/二级推广订单佣金、'
. 'brokerage_user 推广用户佣金、extract 佣金提现、extract_fail 提现失败退回、extract_money 提现到余额、'
. 'refund 退款退回佣金、pink_master_brokerage 拼团团长佣金;'
. 'pm:1 收入、0 支出;status:1 有效、0 待确定、-1 无效,冻结中的佣金以 frozen_time 大于当前时间判定。';
}
/**
* 工具中文名(管理后台展示)
* @return string
*/
public function label(): string
{
return '佣金流水列表';
}
/**
* 工具分类(管理后台分组展示)
* @return string
*/
public function category(): string
{
return '财务';
}
/**
* 入参 Schema
* @return array
*/
public function inputSchema(): array
{
return [
'type' => 'object',
'properties' => [
'page' => ['type' => 'number', 'description' => '页码'],
'limit' => ['type' => 'number', 'description' => '每页数量'],
'uid' => ['type' => 'number', 'description' => '用户 UID'],
'type' => ['type' => 'string', 'description' => '变动类型,如 one_brokerage/extract/refund'],
'pm' => ['type' => 'number', 'description' => '收/支方向:1 收入、0 支出'],
'status' => ['type' => 'number', 'description' => '状态:1 有效、0 待确定、-1 无效'],
],
];
}
/**
* 执行工具
*
* @param array $args 参数(page/limit/uid/type/pm/status)
* @return array 佣金流水列表和总数
*/
public function execute(array $args): array
{
$page = max(1, (int)($args['page'] ?? 1));
$limit = min(100, max(1, (int)($args['limit'] ?? 10)));
$where = [];
if (!empty($args['uid'])) {
$where[] = ['uid', '=', (int)$args['uid']];
}
if (isset($args['type']) && $args['type'] !== '') {
$where[] = ['type', '=', (string)$args['type']];
}
if (isset($args['pm']) && $args['pm'] !== '') {
$where[] = ['pm', '=', (int)$args['pm'] == 1 ? 1 : 0];
}
if (isset($args['status']) && $args['status'] !== '') {
$where[] = ['status', '=', (int)$args['status']];
}
$list = $this->dao->search($where)
->field('id,uid,link_id,type,title,number,balance,pm,mark,status,frozen_time,add_time')
->order('id desc')
->page($page, $limit)
->select()
->toArray();
$count = $this->dao->search($where)->count();
return ['list' => $list, 'count' => $count];
}
}

View File

@ -0,0 +1,170 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace modules\service\mcp\tools;
use app\services\user\UserLabelRelationServices;
use app\services\user\UserServices;
use modules\service\mcp\McpException;
/**
* 用户编辑数据组装辅助(MCP 内部共享,非工具类)
*
* UserServices::updateInfo 走基本信息分支时按全字段覆盖写库,并以 label_id
* 全量重设用户标签;本类负责先读用户当前值,合并工具显式传入的修改项后
* 构造 updateInfo 所需的完整 $data,保证「只改一项」不会清空其他资料,
* 与管理后台编辑表单(读出当前值 → 改一项 → 提交)链路一致。
*
* 文件名不以 Tool.php 结尾,不会被注册中心自动发现,仅供用户编辑类工具复用。
*/
final class UserEditPayload
{
/**
* 构造 UserServices::updateInfo 所需的全量编辑数据
*
* @param UserServices $services 用户服务(用于读取用户与手机号唯一性校验)
* @param UserLabelRelationServices $labelServices 用户标签关联服务(读取当前标签)
* @param int $uid 用户 ID
* @param array $overrides 显式要修改的字段(仅支持 real_name/phone/email/card_id/birthday/addres/mark/status/group_id/label_ids)
* @return array updateInfo 完整入参
* @throws McpException 用户不存在或字段校验失败
*/
public static function build(UserServices $services, UserLabelRelationServices $labelServices, int $uid, array $overrides): array
{
$user = $services->getUserInfo($uid);
if (!$user) {
throw new McpException('用户不存在');
}
$user = is_object($user) ? $user->toArray() : (array)$user;
// 真实姓名(对齐后台必填与 eb_user.real_name varchar(25))
$realName = trim((string)($overrides['real_name'] ?? $user['real_name'] ?? ''));
if ($realName === '') {
throw new McpException('real_name 真实姓名不能为空');
}
if (mb_strlen($realName) > 25) {
throw new McpException('real_name 真实姓名不能超过 25 个字');
}
// 手机号:必填、格式与全库唯一(对齐 UserController::update 校验)
$phone = trim((string)($overrides['phone'] ?? $user['phone'] ?? ''));
if ($phone === '') {
throw new McpException('phone 手机号不能为空');
}
if (!preg_match('/^1[3456789]\d{9}$/', $phone)) {
throw new McpException('phone 手机号格式错误');
}
if ($phone !== (string)($user['phone'] ?? '')
&& $services->count(['phone' => $phone, 'is_del' => 0, 'not_uid' => $uid])) {
throw new McpException('phone 手机号已被其他用户使用');
}
// 邮箱:可空,填了必须合法
$email = trim((string)($overrides['email'] ?? $user['email'] ?? ''));
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new McpException('email 邮箱格式错误');
}
// 身份证:可空,填了必须合法(全局辅助函数校验)
$cardId = trim((string)($overrides['card_id'] ?? $user['card_id'] ?? ''));
if ($cardId !== '' && !check_card($cardId)) {
throw new McpException('card_id 身份证号码不正确');
}
// 生日:传日期字符串(YYYY-MM-DD);未传时回填当前值(时间戳 0 回填纪元日,strtotime 后仍近似为空)
if (array_key_exists('birthday', $overrides)) {
$birthday = trim((string)$overrides['birthday']);
if ($birthday !== '' && strtotime($birthday) === false) {
throw new McpException('birthday 必须是有效日期,格式 YYYY-MM-DD');
}
} else {
$birthdayTimestamp = (int)($user['birthday'] ?? 0);
$birthday = $birthdayTimestamp > 0 ? date('Y-m-d H:i:s', $birthdayTimestamp) : '1970-01-01 08:00:00';
}
// 状态:1=开启,0=锁定
$status = (int)($overrides['status'] ?? $user['status'] ?? 1);
if (!in_array($status, [0, 1], true)) {
throw new McpException('status 只能为 0(锁定)或 1(开启)');
}
// 备注(eb_user.mark varchar(255))
$mark = trim((string)($overrides['mark'] ?? $user['mark'] ?? ''));
if (mb_strlen($mark) > 255) {
throw new McpException('mark 备注不能超过 255 个字');
}
// 详细地址
$addres = trim((string)($overrides['addres'] ?? $user['addres'] ?? ''));
if (mb_strlen($addres) > 255) {
throw new McpException('addres 详细地址不能超过 255 个字');
}
// 分组:0 表示未分组
$groupId = max(0, (int)($overrides['group_id'] ?? $user['group_id'] ?? 0));
// 标签:未显式传入时回填当前标签,避免 updateInfo 以空数组全量清空
if (array_key_exists('label_ids', $overrides)) {
$labelIds = self::idArray($overrides['label_ids'], 'label_ids');
} else {
$labelIds = array_map('intval', (array)$labelServices->getUserLabels($uid));
}
// updateInfo 基本信息分支要求的全量结构;等级/余额/积分不在编辑范围,
// 保持当前值避免触发分支逻辑,密码固定空串即不修改
return [
'money_status' => 0,
'money' => '0',
'integration_status' => 0,
'integration' => '0',
'is_other' => false,
'label_id' => $labelIds,
'pwd' => '',
'status' => $status,
'real_name' => $realName,
'card_id' => $cardId,
'birthday' => $birthday,
'mark' => $mark,
'level' => (int)($user['level'] ?? 0),
'phone' => $phone,
'addres' => $addres,
'email' => $email,
'group_id' => $groupId,
];
}
/**
* 校验并规范化正整数 ID 数组(去重)
*
* @param mixed $value 原始值
* @param string $name 参数名称(报错用)
* @return int[] ID 列表(允许为空数组)
* @throws McpException 不是合法 ID 数组
*/
public static function idArray(mixed $value, string $name): array
{
if (is_int($value) || (is_string($value) && $value !== '')) {
$value = [$value];
}
if (!is_array($value)) {
throw new McpException($name . ' 必须是 ID 数组');
}
$ids = [];
foreach ($value as $item) {
if (!is_int($item) && (!is_string($item) || !ctype_digit($item))) {
throw new McpException($name . ' 数组内必须全部是整数 ID');
}
$ids[] = (int)$item;
}
return array_values(array_unique($ids));
}
}

View File

@ -0,0 +1,141 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace modules\service\mcp\tools;
use app\services\user\UserServices;
use crmeb\exceptions\AdminException;
use crmeb\interfaces\McpToolInterface;
use modules\service\mcp\McpException;
/**
* MCP 工具:设置用户分组
*
* 与管理后台「设置分组」同链路:调用 UserServices::saveSetGroup 批量
* 将用户调整为指定分组;分组存在性与写入结果由服务层校验。
*/
class UserGroupSetTool implements McpToolInterface
{
/**
* 用户服务
* @var UserServices
*/
private UserServices $services;
/**
* 构造函数
* @param UserServices $services 用户服务
*/
public function __construct(UserServices $services)
{
$this->services = $services;
}
/**
* 工具名称
* @return string
*/
public function name(): string
{
return 'crmeb_user_group_set';
}
/**
* 交互声明:调整用户分组会变更用户数据,需用户确认后才真实执行
* @return string execute=需确认后执行
*/
public function interaction(): string
{
return 'execute';
}
/**
* 工具描述
* @return string
*/
public function description(): string
{
return '把一个或多个用户设置到指定用户分组,等同管理后台「设置分组」,成功返回本次调整的分组与用户数。'
. '必填参数:uids 用户ID数组(支持批量);group_id 分组ID(大于 0,必须是已存在的用户分组,分组清单在管理后台「用户 → 用户分组」维护,'
. '目前没有查询分组的 MCP 工具,ID 不确定时先向运营确认)。'
. '本工具是覆盖式设置:执行后所选用户的分组统一变为 group_id,原分组被替换。'
. '不能创建或删除分组,不能把用户移出分组(设为其他分组即是替换);设置用户标签用 crmeb_user_label_set。';
}
/**
* 工具中文名(管理后台展示)
* @return string
*/
public function label(): string
{
return '分组设置';
}
/**
* 工具分类(管理后台分组展示)
* @return string
*/
public function category(): string
{
return '用户';
}
/**
* 入参 Schema
* @return array
*/
public function inputSchema(): array
{
return [
'type' => 'object',
'required' => ['uids', 'group_id'],
'properties' => [
'uids' => ['type' => 'array', 'items' => ['type' => 'integer'], 'description' => '用户ID数组,支持批量设置'],
'group_id' => ['type' => 'integer', 'minimum' => 1, 'description' => '目标分组ID,必须是已存在的用户分组'],
],
];
}
/**
* 执行工具
*
* @param array $args 参数(uids、group_id)
* @return array 本次分组调整结果
* @throws McpException 参数非法或服务层业务校验失败
*/
public function execute(array $args): array
{
$uids = UserEditPayload::idArray($args['uids'] ?? [], 'uids');
$uids = array_values(array_filter($uids, static fn(int $id) => $id > 0));
if (!$uids) {
throw new McpException('uids 必须是至少一个大于 0 的用户ID数组');
}
$groupId = (int)($args['group_id'] ?? 0);
if ($groupId <= 0) {
throw new McpException('group_id 必须是大于 0 的分组ID');
}
try {
$this->services->saveSetGroup($uids, $groupId);
} catch (AdminException $e) {
// 「该分组不存在」「设置分组失败或无改动」等业务错误透传
throw new McpException($e->getMessage());
}
return [
'group_id' => $groupId,
'uid_count' => count($uids),
'uids' => $uids,
'message' => '已把 ' . count($uids) . ' 个用户设置到分组 ' . $groupId,
];
}
}

View File

@ -0,0 +1,182 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace modules\service\mcp\tools;
use app\services\user\UserLabelCateServices;
use app\services\user\UserLabelServices;
use crmeb\exceptions\AdminException;
use crmeb\interfaces\McpToolInterface;
use modules\service\mcp\McpException;
/**
* MCP 工具:创建用户标签
*
* 与管理后台「用户标签」同链路:调用 UserLabelServices::save 新建标签;
* 参数预检在工具层完成(McpException 消息可透传),服务层业务校验不因 AI 调用而豁免。
*/
class UserLabelCreateTool implements McpToolInterface
{
/**
* 用户标签服务
* @var UserLabelServices
*/
private UserLabelServices $services;
/**
* 标签分类服务
* @var UserLabelCateServices
*/
private UserLabelCateServices $cateServices;
/**
* 构造函数
* @param UserLabelServices $services 用户标签服务
* @param UserLabelCateServices $cateServices 标签分类服务
*/
public function __construct(UserLabelServices $services, UserLabelCateServices $cateServices)
{
$this->services = $services;
$this->cateServices = $cateServices;
}
/**
* 工具名称
* @return string
*/
public function name(): string
{
return 'crmeb_user_label_create';
}
/**
* 交互声明:创建标签会新增系统配置数据,需用户确认后才真实执行
* @return string execute=需确认后执行
*/
public function interaction(): string
{
return 'execute';
}
/**
* 工具描述
* @return string
*/
public function description(): string
{
return '创建一个用户标签(管理后台「用户标签」体系),成功后即可通过后台给用户打标;返回创建结果摘要;'
. '必填参数:label_name 标签名称(非空,50 字以内);label_cate 标签分类 ID(大于 0 的整数,'
. '必须是已存在的分类,可用分类见 crmeb_user_label_list 返回的 cates 字段);'
. '同一分类下不允许与既有标签重名;创建成功后可通过 crmeb_user_label_list 查询确认,'
. '修改已有标签用 crmeb_user_label_update,删除标签用 crmeb_user_label_delete。';
}
/**
* 工具中文名(管理后台展示)
* @return string
*/
public function label(): string
{
return '标签创建';
}
/**
* 工具分类(管理后台分组展示)
* @return string
*/
public function category(): string
{
return '用户';
}
/**
* 入参 Schema
* @return array
*/
public function inputSchema(): array
{
return [
'type' => 'object',
'required' => ['label_name', 'label_cate'],
'properties' => [
'label_name' => ['type' => 'string', 'maxLength' => 50, 'description' => '标签名称,非空,50 字以内'],
'label_cate' => ['type' => 'integer', 'minimum' => 1, 'description' => '标签分类 ID,必须是已存在的分类,可用分类见 crmeb_user_label_list 返回的 cates'],
],
];
}
/**
* 执行工具
*
* @param array $args 参数(label_name/label_cate)
* @return array 创建结果摘要
* @throws McpException 参数非法或服务层业务校验失败
*/
public function execute(array $args): array
{
$name = trim((string)($args['label_name'] ?? ''));
if ($name === '') {
throw new McpException('label_name 不能为空');
}
if (mb_strlen($name) > 50) {
throw new McpException('label_name 不能超过 50 个字');
}
$cateId = self::positiveIntegerArg($args, 'label_cate');
if (!$this->cateExists($cateId)) {
throw new McpException('label_cate 标签分类不存在,可用分类见 crmeb_user_label_list 返回的 cates');
}
// 与 UserLabelController::save 的 postMore 结构保持一致,由服务层完成入库与查重
try {
$this->services->save(0, ['id' => 0, 'label_cate' => $cateId, 'label_name' => $name]);
} catch (AdminException $e) {
// 服务层业务校验失败:转 McpException 让消息透传给 AI 客户端
throw new McpException($e->getMessage());
}
return [
'label_name' => $name,
'label_cate' => $cateId,
'message' => '用户标签创建成功,可通过 crmeb_user_label_list 查询确认',
];
}
/**
* 校验标签分类是否存在
* @param int $cateId 分类 ID
* @return bool 是否存在
*/
private function cateExists(int $cateId): bool
{
$cateIds = array_map('intval', array_column($this->cateServices->getLabelCateAll(), 'id'));
return in_array($cateId, $cateIds, true);
}
/**
* @param array $args 工具参数
* @param string $name 参数名称
* @return int 大于 0 的整数
* @throws McpException 非整数或小于等于 0
*/
private static function positiveIntegerArg(array $args, string $name): int
{
$value = $args[$name] ?? null;
if (!is_int($value) && (!is_string($value) || !ctype_digit($value))) {
throw new McpException($name . ' 必须是大于 0 的整数');
}
$number = (int)$value;
if ($number <= 0) {
throw new McpException($name . ' 必须是大于 0 的整数');
}
return $number;
}
}

View File

@ -0,0 +1,157 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace modules\service\mcp\tools;
use app\services\user\UserLabelServices;
use crmeb\exceptions\AdminException;
use crmeb\interfaces\McpToolInterface;
use modules\service\mcp\McpException;
/**
* MCP 工具:删除用户标签
*
* 与管理后台「用户标签」同链路:调用 UserLabelServices::delLabel 删除标签;
* 参数预检在工具层完成(McpException 消息可透传),服务层业务校验不因 AI 调用而豁免。
*/
class UserLabelDeleteTool implements McpToolInterface
{
/**
* 用户标签服务
* @var UserLabelServices
*/
private UserLabelServices $services;
/**
* 构造函数
* @param UserLabelServices $services 用户标签服务
*/
public function __construct(UserLabelServices $services)
{
$this->services = $services;
}
/**
* 工具名称
* @return string
*/
public function name(): string
{
return 'crmeb_user_label_delete';
}
/**
* 交互声明:删除标签会不可逆地移除系统配置数据,需用户确认后才真实执行;
* delete=删除类写操作,后台权限勾选与工具列表按「删」标注展示
* @return string delete=删除类,需确认后执行
*/
public function interaction(): string
{
return 'delete';
}
/**
* 工具描述
* @return string
*/
public function description(): string
{
return '删除一个用户标签,删除后该标签从标签列表消失且不可恢复;'
. '必填参数:id 标签 ID(可通过 crmeb_user_label_list 查询);'
. '仅删除标签本身,不清理用户与该标签的既有关联记录(与管理后台删除行为一致),'
. '删除前建议先用 crmeb_user_list 按 label_id 筛选确认受影响用户;'
. '创建标签用 crmeb_user_label_create,修改标签用 crmeb_user_label_update。';
}
/**
* 工具中文名(管理后台展示)
* @return string
*/
public function label(): string
{
return '标签删除';
}
/**
* 工具分类(管理后台分组展示)
* @return string
*/
public function category(): string
{
return '用户';
}
/**
* 入参 Schema
* @return array
*/
public function inputSchema(): array
{
return [
'type' => 'object',
'required' => ['id'],
'properties' => [
'id' => ['type' => 'integer', 'minimum' => 1, 'description' => '待删除的标签 ID,可通过 crmeb_user_label_list 查询'],
],
];
}
/**
* 执行工具
*
* @param array $args 参数(id)
* @return array 删除结果摘要
* @throws McpException 参数非法、标签不存在或服务层业务校验失败
*/
public function execute(array $args): array
{
$id = self::positiveIntegerArg($args, 'id');
$label = $this->services->getLable($id);
if (!$label) {
throw new McpException('标签不存在,可通过 crmeb_user_label_list 查询有效标签 ID');
}
$name = (string)$label->getData('label_name');
try {
$this->services->delLabel($id);
} catch (AdminException $e) {
// 服务层业务校验失败:转 McpException 让消息透传给 AI 客户端
throw new McpException($e->getMessage());
}
return [
'id' => $id,
'label_name' => $name,
'message' => '用户标签删除成功,可通过 crmeb_user_label_list 查询确认',
];
}
/**
* @param array $args 工具参数
* @param string $name 参数名称
* @return int 大于 0 的整数
* @throws McpException 非整数或小于等于 0
*/
private static function positiveIntegerArg(array $args, string $name): int
{
$value = $args[$name] ?? null;
if (!is_int($value) && (!is_string($value) || !ctype_digit($value))) {
throw new McpException($name . ' 必须是大于 0 的整数');
}
$number = (int)$value;
if ($number <= 0) {
throw new McpException($name . ' 必须是大于 0 的整数');
}
return $number;
}
}

View File

@ -0,0 +1,125 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace modules\service\mcp\tools;
use app\services\user\UserLabelCateServices;
use app\services\user\UserLabelServices;
use crmeb\interfaces\McpToolInterface;
/**
* MCP 工具:用户标签列表
*
* 标签字典查询:返回全部用户标签及所属分类,供 crmeb_user_list 按标签筛选时取标签 ID。
*/
class UserLabelListTool implements McpToolInterface
{
/**
* 用户标签服务
* @var UserLabelServices
*/
private UserLabelServices $services;
/**
* 标签分类服务
* @var UserLabelCateServices
*/
private UserLabelCateServices $cateServices;
/**
* 构造函数
* @param UserLabelServices $services 用户标签服务
* @param UserLabelCateServices $cateServices 标签分类服务
*/
public function __construct(UserLabelServices $services, UserLabelCateServices $cateServices)
{
$this->services = $services;
$this->cateServices = $cateServices;
}
/**
* 工具名称
* @return string
*/
public function name(): string
{
return 'crmeb_user_label_list';
}
/**
* 工具描述
* @return string
*/
public function description(): string
{
return '查询全部用户标签字典,返回标签 ID、标签名称、所属分类 ID 与分类名,并附全部标签分类列表(cates 字段);'
. 'label_cate 可选,按标签分类 ID 筛选标签,0 或不传返回全部分类下的标签;'
. '不分页全量返回,不包含各标签下的打标用户数;'
. '标签 ID 用于 crmeb_user_list 的 label_id 参数按标签筛选用户。';
}
/**
* 工具中文名(管理后台展示)
* @return string
*/
public function label(): string
{
return '标签列表';
}
/**
* 工具分类(管理后台分组展示)
* @return string
*/
public function category(): string
{
return '用户';
}
/**
* 入参 Schema
* @return array
*/
public function inputSchema(): array
{
return [
'type' => 'object',
'properties' => [
'label_cate' => ['type' => 'integer', 'minimum' => 0, 'description' => '标签分类 ID,按分类筛选标签;0 或不传返回全部'],
],
];
}
/**
* 执行工具
*
* @param array $args 参数(label_cate)
* @return array 标签列表、分类列表与总数
*/
public function execute(array $args): array
{
$where = [];
$cateId = (int)($args['label_cate'] ?? 0);
if ($cateId > 0) {
$where['label_cate'] = $cateId;
}
$list = $this->services->getLabelList($where, ['id', 'label_name', 'label_cate']);
$cates = [];
foreach ($this->cateServices->getLabelCateAll() as $cate) {
$cates[] = ['id' => (int)($cate['id'] ?? 0), 'name' => (string)($cate['name'] ?? '')];
}
return ['list' => $list, 'count' => count($list), 'cates' => $cates];
}
}

View File

@ -0,0 +1,165 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace modules\service\mcp\tools;
use app\services\user\UserServices;
use crmeb\exceptions\AdminException;
use crmeb\interfaces\McpToolInterface;
use modules\service\mcp\McpException;
/**
* MCP 工具:设置用户标签
*
* 与管理后台「设置标签」同链路:调用 UserServices::saveSetLabel 批量
* 覆盖 / 追加 / 移除用户标签;标签存在性与写入结果由服务层校验。
*/
class UserLabelSetTool implements McpToolInterface
{
/**
* 标签操作类型:清空原有后设置(覆盖)
*/
private const LABEL_TYPE_REPLACE = 0;
/**
* 标签操作类型:保留原有叠加(追加)
*/
private const LABEL_TYPE_APPEND = 1;
/**
* 标签操作类型:移除指定标签
*/
private const LABEL_TYPE_REMOVE = 2;
/**
* 用户服务
* @var UserServices
*/
private UserServices $services;
/**
* 构造函数
* @param UserServices $services 用户服务
*/
public function __construct(UserServices $services)
{
$this->services = $services;
}
/**
* 工具名称
* @return string
*/
public function name(): string
{
return 'crmeb_user_label_set';
}
/**
* 交互声明:调整用户标签会变更用户数据,需用户确认后才真实执行
* @return string execute=需确认后执行
*/
public function interaction(): string
{
return 'execute';
}
/**
* 工具描述
* @return string
*/
public function description(): string
{
return '为一个或多个用户设置用户标签,等同管理后台「设置标签」,支持覆盖、追加、移除三种方式,成功返回本次操作的标签与用户数。'
. '必填参数:uids 用户ID数组(支持批量);label_ids 标签ID数组。'
. 'label_type 操作方式:0=覆盖(默认,清空用户原有标签后设置 label_ids)、1=追加(保留原有标签叠加 label_ids)、'
. '2=移除(从用户身上移除 label_ids 指定的标签)。'
. 'label_ids 必须是已存在的用户标签,标签 ID 与分类清单可用 crmeb_user_label_list 查询(创建新标签用 crmeb_user_label_create),'
. '标签不存在时服务层会整体拒绝。'
. '不能创建或删除标签本身;改用户分组用 crmeb_user_group_set。';
}
/**
* 工具中文名(管理后台展示)
* @return string
*/
public function label(): string
{
return '标签设置';
}
/**
* 工具分类(管理后台分组展示)
* @return string
*/
public function category(): string
{
return '用户';
}
/**
* 入参 Schema
* @return array
*/
public function inputSchema(): array
{
return [
'type' => 'object',
'required' => ['uids', 'label_ids'],
'properties' => [
'uids' => ['type' => 'array', 'items' => ['type' => 'integer'], 'description' => '用户ID数组,支持批量设置'],
'label_ids' => ['type' => 'array', 'items' => ['type' => 'integer'], 'description' => '标签ID数组;label_type=2 时为要移除的标签'],
'label_type' => ['type' => 'integer', 'enum' => [0, 1, 2], 'description' => '0=覆盖原有标签(默认),1=追加标签,2=移除指定标签'],
],
];
}
/**
* 执行工具
*
* @param array $args 参数(uids、label_ids、label_type)
* @return array 本次标签操作结果
* @throws McpException 参数非法或服务层业务校验失败
*/
public function execute(array $args): array
{
$uids = UserEditPayload::idArray($args['uids'] ?? [], 'uids');
$uids = array_values(array_filter($uids, static fn(int $id) => $id > 0));
if (!$uids) {
throw new McpException('uids 必须是至少一个大于 0 的用户ID数组');
}
$labelIds = UserEditPayload::idArray($args['label_ids'] ?? [], 'label_ids');
if (!$labelIds) {
throw new McpException('label_ids 必须是至少一个标签ID');
}
$labelType = (int)($args['label_type'] ?? self::LABEL_TYPE_REPLACE);
if (!in_array($labelType, [self::LABEL_TYPE_REPLACE, self::LABEL_TYPE_APPEND, self::LABEL_TYPE_REMOVE], true)) {
throw new McpException('label_type 只能是 0(覆盖)、1(追加)或 2(移除)');
}
try {
$this->services->saveSetLabel($uids, $labelIds, $labelType);
} catch (AdminException $e) {
// 「有标签不存在或被删除」「设置标签失败」等业务错误透传
throw new McpException($e->getMessage());
}
$actionText = [self::LABEL_TYPE_REPLACE => '覆盖为', self::LABEL_TYPE_APPEND => '追加', self::LABEL_TYPE_REMOVE => '移除'][$labelType];
return [
'label_type' => $labelType,
'label_ids' => $labelIds,
'uid_count' => count($uids),
'uids' => $uids,
'message' => '已把 ' . count($uids) . ' 个用户的标签' . $actionText . ' ' . implode(',', $labelIds),
];
}
}

View File

@ -0,0 +1,199 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace modules\service\mcp\tools;
use app\services\user\UserLabelCateServices;
use app\services\user\UserLabelServices;
use crmeb\exceptions\AdminException;
use crmeb\interfaces\McpToolInterface;
use modules\service\mcp\McpException;
/**
* MCP 工具:修改用户标签
*
* 与管理后台「用户标签」同链路:调用 UserLabelServices::save 更新已有标签;
* 未提供的参数沿用原值;参数预检在工具层完成(McpException 消息可透传),
* 服务层业务校验不因 AI 调用而豁免。
*/
class UserLabelUpdateTool implements McpToolInterface
{
/**
* 用户标签服务
* @var UserLabelServices
*/
private UserLabelServices $services;
/**
* 标签分类服务
* @var UserLabelCateServices
*/
private UserLabelCateServices $cateServices;
/**
* 构造函数
* @param UserLabelServices $services 用户标签服务
* @param UserLabelCateServices $cateServices 标签分类服务
*/
public function __construct(UserLabelServices $services, UserLabelCateServices $cateServices)
{
$this->services = $services;
$this->cateServices = $cateServices;
}
/**
* 工具名称
* @return string
*/
public function name(): string
{
return 'crmeb_user_label_update';
}
/**
* 交互声明:修改标签会变更系统配置数据,需用户确认后才真实执行
* @return string execute=需确认后执行
*/
public function interaction(): string
{
return 'execute';
}
/**
* 工具描述
* @return string
*/
public function description(): string
{
return '修改一个已有用户标签的名称或所属分类,未提供的字段沿用原值,返回修改后的标签信息;'
. '必填参数:id 标签 ID(可通过 crmeb_user_label_list 查询);'
. '可选参数:label_name 新名称(非空,50 字以内)、label_cate 新分类 ID(必须是已存在的分类,'
. '可用分类见 crmeb_user_label_list 返回的 cates 字段),两者至少提供一个;'
. '同一分类下不允许与其他标签重名;创建标签用 crmeb_user_label_create,删除标签用 crmeb_user_label_delete。';
}
/**
* 工具中文名(管理后台展示)
* @return string
*/
public function label(): string
{
return '标签修改';
}
/**
* 工具分类(管理后台分组展示)
* @return string
*/
public function category(): string
{
return '用户';
}
/**
* 入参 Schema
* @return array
*/
public function inputSchema(): array
{
return [
'type' => 'object',
'required' => ['id'],
'properties' => [
'id' => ['type' => 'integer', 'minimum' => 1, 'description' => '标签 ID,可通过 crmeb_user_label_list 查询'],
'label_name' => ['type' => 'string', 'maxLength' => 50, 'description' => '新标签名称,非空,50 字以内;不传沿用原值'],
'label_cate' => ['type' => 'integer', 'minimum' => 1, 'description' => '新标签分类 ID,必须是已存在的分类;不传沿用原值'],
],
];
}
/**
* 执行工具
*
* @param array $args 参数(id,label_name/label_cate 至少其一)
* @return array 修改后的标签信息
* @throws McpException 参数非法、标签不存在或服务层业务校验失败
*/
public function execute(array $args): array
{
$id = self::positiveIntegerArg($args, 'id');
if (!isset($args['label_name']) && !isset($args['label_cate'])) {
throw new McpException('label_name 与 label_cate 至少提供一个');
}
$label = $this->services->getLable($id);
if (!$label) {
throw new McpException('标签不存在,可通过 crmeb_user_label_list 查询有效标签 ID');
}
$name = trim((string)($args['label_name'] ?? $label->getData('label_name')));
if ($name === '') {
throw new McpException('label_name 不能为空');
}
if (mb_strlen($name) > 50) {
throw new McpException('label_name 不能超过 50 个字');
}
$cateId = (int)$label->getData('label_cate');
if (isset($args['label_cate'])) {
$cateId = self::positiveIntegerArg($args, 'label_cate');
if (!$this->cateExists($cateId)) {
throw new McpException('label_cate 标签分类不存在,可用分类见 crmeb_user_label_list 返回的 cates');
}
}
// 与 UserLabelController::save 的 postMore 结构保持一致,由服务层完成入库与查重
try {
$this->services->save($id, ['id' => $id, 'label_cate' => $cateId, 'label_name' => $name]);
} catch (AdminException $e) {
// 服务层业务校验失败:转 McpException 让消息透传给 AI 客户端
throw new McpException($e->getMessage());
}
return [
'id' => $id,
'label_name' => $name,
'label_cate' => $cateId,
'message' => '用户标签修改成功,可通过 crmeb_user_label_list 查询确认',
];
}
/**
* 校验标签分类是否存在
* @param int $cateId 分类 ID
* @return bool 是否存在
*/
private function cateExists(int $cateId): bool
{
$cateIds = array_map('intval', array_column($this->cateServices->getLabelCateAll(), 'id'));
return in_array($cateId, $cateIds, true);
}
/**
* @param array $args 工具参数
* @param string $name 参数名称
* @return int 大于 0 的整数
* @throws McpException 非整数或小于等于 0
*/
private static function positiveIntegerArg(array $args, string $name): int
{
$value = $args[$name] ?? null;
if (!is_int($value) && (!is_string($value) || !ctype_digit($value))) {
throw new McpException($name . ' 必须是大于 0 的整数');
}
$number = (int)$value;
if ($number <= 0) {
throw new McpException($name . ' 必须是大于 0 的整数');
}
return $number;
}
}

View File

@ -0,0 +1,141 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace modules\service\mcp\tools;
use app\services\user\UserServices;
use crmeb\exceptions\AdminException;
use crmeb\interfaces\McpToolInterface;
use modules\service\mcp\McpException;
/**
* MCP 工具:赠送会员等级
*
* 与管理后台「赠送等级」同链路:调用 UserServices::saveGiveLevel 给用户
* 赠送系统会员等级配置中的一个等级;等级存在性、重复赠送由服务层校验,
* 赠送成功后用户的 level 字段同步为该等级 ID,原等级作废。
*/
class UserLevelGiveTool implements McpToolInterface
{
/**
* 用户服务
* @var UserServices
*/
private UserServices $services;
/**
* 构造函数
* @param UserServices $services 用户服务
*/
public function __construct(UserServices $services)
{
$this->services = $services;
}
/**
* 工具名称
* @return string
*/
public function name(): string
{
return 'crmeb_user_level_give';
}
/**
* 交互声明:赠送会员等级会变更用户权益数据,需用户确认后才真实执行
* @return string execute=需确认后执行
*/
public function interaction(): string
{
return 'execute';
}
/**
* 工具描述
* @return string
*/
public function description(): string
{
return '给单个用户赠送系统会员等级,等同管理后台「赠送等级」,成功返回赠送的等级信息。'
. '必填参数:uid 用户ID;level_id 等级ID(系统会员等级配置「会员 → 等级设置」的主键 ID,'
. '不是等级序数;目前没有查询等级配置的 MCP 工具,ID 不确定时先向运营确认)。'
. '行为口径:赠送后用户原会员等级记录被作废并替换为新等级,用户 level 字段同步为 level_id,'
. '等级附带经验差异会自动增减;用户已拥有该等级且未失效时赠送失败;只能赠等级,不能清除等级或修改经验。'
. '注意与付费会员是两套体系:延长付费会员到期时间用 crmeb_user_member_time_give。';
}
/**
* 工具中文名(管理后台展示)
* @return string
*/
public function label(): string
{
return '赠送等级';
}
/**
* 工具分类(管理后台分组展示)
* @return string
*/
public function category(): string
{
return '用户';
}
/**
* 入参 Schema
* @return array
*/
public function inputSchema(): array
{
return [
'type' => 'object',
'required' => ['uid', 'level_id'],
'properties' => [
'uid' => ['type' => 'number', 'description' => '用户ID'],
'level_id' => ['type' => 'integer', 'minimum' => 1, 'description' => '系统会员等级配置ID(会员等级设置列表的主键,非等级序数)'],
],
];
}
/**
* 执行工具
*
* @param array $args 参数(uid、level_id)
* @return array 赠送结果
* @throws McpException 参数非法或服务层业务校验失败
*/
public function execute(array $args): array
{
$uid = (int)($args['uid'] ?? 0);
if ($uid <= 0) {
throw new McpException('参数错误:缺少 uid 或格式不正确');
}
$levelId = (int)($args['level_id'] ?? 0);
if ($levelId <= 0) {
throw new McpException('level_id 必须是大于 0 的等级ID');
}
try {
$this->services->saveGiveLevel($uid, $levelId);
} catch (AdminException $e) {
// 「用户不存在」「等级不存在或被删除」「已有该等级」等业务错误透传
throw new McpException($e->getMessage());
}
return [
'uid' => $uid,
'level_id' => $levelId,
'message' => '会员等级赠送成功,用户当前等级已设置为 ' . $levelId . ',可通过 crmeb_user_detail 查看',
];
}
}

View File

@ -0,0 +1,150 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace modules\service\mcp\tools;
use app\services\user\UserLabelRelationServices;
use app\services\user\UserServices;
use crmeb\exceptions\AdminException;
use crmeb\interfaces\McpToolInterface;
use modules\service\mcp\McpException;
/**
* MCP 工具:设置用户备注
*
* 与管理后台「编辑用户」同链路:读取用户当前值合并新备注后调用
* UserServices::updateInfo 落库,只改 mark 一个字段,不影响其他资料。
*/
class UserMarkSetTool implements McpToolInterface
{
/**
* 用户服务
* @var UserServices
*/
private UserServices $services;
/**
* 用户标签关联服务(读取当前标签避免被清空)
* @var UserLabelRelationServices
*/
private UserLabelRelationServices $labelServices;
/**
* 构造函数
* @param UserServices $services 用户服务
* @param UserLabelRelationServices $labelServices 用户标签关联服务
*/
public function __construct(UserServices $services, UserLabelRelationServices $labelServices)
{
$this->services = $services;
$this->labelServices = $labelServices;
}
/**
* 工具名称
* @return string
*/
public function name(): string
{
return 'crmeb_user_mark_set';
}
/**
* 交互声明:写备注会变更用户数据,需用户确认后才真实执行
* @return string execute=需确认后执行
*/
public function interaction(): string
{
return 'execute';
}
/**
* 工具描述
* @return string
*/
public function description(): string
{
return '给单个用户写运营备注(mark 字段),等同管理后台「编辑用户」里的备注项;只修改备注,不影响昵称、手机号、分组、标签等其他资料,成功返回更新后的备注。'
. '必填参数:uid 用户ID;mark 备注内容(255 字以内,传空字符串即清空备注)。'
. '备注面向运营人员记录,用户端不可见;需要修改姓名、手机号等完整资料时用 crmeb_user_update。';
}
/**
* 工具中文名(管理后台展示)
* @return string
*/
public function label(): string
{
return '用户备注';
}
/**
* 工具分类(管理后台分组展示)
* @return string
*/
public function category(): string
{
return '用户';
}
/**
* 入参 Schema
* @return array
*/
public function inputSchema(): array
{
return [
'type' => 'object',
'required' => ['uid', 'mark'],
'properties' => [
'uid' => ['type' => 'number', 'description' => '用户ID'],
'mark' => ['type' => 'string', 'maxLength' => 255, 'description' => '备注内容,255 字以内,空字符串表示清空备注'],
],
];
}
/**
* 执行工具
*
* @param array $args 参数(uid、mark)
* @return array 更新后的备注
* @throws McpException 参数非法或服务层业务校验失败
*/
public function execute(array $args): array
{
$uid = (int)($args['uid'] ?? 0);
if ($uid <= 0) {
throw new McpException('参数错误:缺少 uid 或格式不正确');
}
if (!array_key_exists('mark', $args) || !is_string($args['mark'])) {
throw new McpException('参数错误:缺少 mark 或格式不正确');
}
$mark = trim($args['mark']);
if (mb_strlen($mark) > 255) {
throw new McpException('mark 备注不能超过 255 个字');
}
$data = UserEditPayload::build($this->services, $this->labelServices, $uid, ['mark' => $mark]);
try {
$this->services->updateInfo($uid, $data);
} catch (AdminException $e) {
throw new McpException($e->getMessage());
}
return [
'uid' => $uid,
'mark' => $mark,
'message' => $mark === '' ? '用户备注已清空' : '用户备注已更新,可通过 crmeb_user_detail 查看',
];
}
}

View File

@ -0,0 +1,173 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace modules\service\mcp\tools;
use app\services\user\UserServices;
use crmeb\exceptions\AdminException;
use crmeb\interfaces\McpToolInterface;
use modules\service\mcp\McpException;
/**
* MCP 工具:赠送付费会员时长
*
* 与管理后台「赠送付费会员时长」同链路:调用 UserServices::saveGiveLevelTime
* 给用户增加付费会员有效期(或设为永久);服务层会写入会员赠送订单并更新
* 会员到期时间(overdue_time)与永久会员标记(is_ever_level)。
*/
class UserMemberTimeGiveTool implements McpToolInterface
{
/**
* 天数取值:永久会员
*/
private const DAYS_FOREVER = -1;
/**
* 用户服务
* @var UserServices
*/
private UserServices $services;
/**
* 构造函数
* @param UserServices $services 用户服务
*/
public function __construct(UserServices $services)
{
$this->services = $services;
}
/**
* 工具名称
* @return string
*/
public function name(): string
{
return 'crmeb_user_member_time_give';
}
/**
* 交互声明:赠送付费会员会变更用户权益数据,需用户确认后才真实执行
* @return string execute=需确认后执行
*/
public function interaction(): string
{
return 'execute';
}
/**
* 工具描述
* @return string
*/
public function description(): string
{
return '给单个用户赠送付费会员时长(延长会员到期时间或设为永久会员),等同管理后台「赠送付费会员时长」,成功返回赠送后的会员到期时间。'
. '必填参数:uid 用户ID;days 赠送天数:-1=设为永久会员,正整数=在当前到期时间基础上增加天数(未开通过付费会员的用户从当前时间起算)。'
. 'days=0 或小于 -1 会被拒绝;已是永久会员时无需赠送,工具会直接返回提示。'
. '赠送会写入一条后台赠送的会员订单记录。注意与会员等级是两套体系:调整会员等级用 crmeb_user_level_give;'
. '本工具不能扣减时长(不支持负数天数)、不能关闭付费会员资格。';
}
/**
* 工具中文名(管理后台展示)
* @return string
*/
public function label(): string
{
return '赠送会员时长';
}
/**
* 工具分类(管理后台分组展示)
* @return string
*/
public function category(): string
{
return '用户';
}
/**
* 入参 Schema
* @return array
*/
public function inputSchema(): array
{
return [
'type' => 'object',
'required' => ['uid', 'days'],
'properties' => [
'uid' => ['type' => 'number', 'description' => '用户ID'],
'days' => ['type' => 'integer', 'description' => '赠送天数:-1=永久会员,正整数=增加的天数'],
],
];
}
/**
* 执行工具
*
* @param array $args 参数(uid、days)
* @return array 赠送后的会员状态
* @throws McpException 参数非法或服务层业务校验失败
*/
public function execute(array $args): array
{
$uid = (int)($args['uid'] ?? 0);
if ($uid <= 0) {
throw new McpException('参数错误:缺少 uid 或格式不正确');
}
$days = $args['days'] ?? null;
if (!is_int($days) && (!is_string($days) || !ctype_digit($days) && (string)$days !== '-1')) {
throw new McpException('days 必须是整数(-1 表示永久,正整数为天数)');
}
$days = (int)$days;
if ($days === 0 || $days < self::DAYS_FOREVER) {
throw new McpException('days 只能是 -1(永久)或正整数天数,不能为 0 或小于 -1');
}
// 服务层对永久会员直接跳过(返回 true 无动作),此处提前识别给出明确结果
$user = $this->services->getUserInfo($uid, 'uid,is_ever_level,overdue_time,is_money_level');
if (!$user) {
throw new McpException('用户不存在');
}
if ((int)$user['is_ever_level'] === 1) {
return [
'uid' => $uid,
'days' => $days,
'is_ever_level' => 1,
'overdue_time' => 0,
'message' => '该用户已是永久付费会员,无需赠送',
];
}
try {
$this->services->saveGiveLevelTime($uid, $days);
} catch (AdminException $e) {
// 「赠送天数不能为0」等业务错误透传
throw new McpException($e->getMessage());
}
// 重新读取会员状态,返回赠送后的到期时间
$user = $this->services->getUserInfo($uid, 'uid,is_ever_level,overdue_time');
$overdueTime = (int)($user['overdue_time'] ?? 0);
return [
'uid' => $uid,
'days' => $days,
'is_ever_level' => (int)($user['is_ever_level'] ?? 0),
'overdue_time' => $overdueTime,
'overdue_date' => $overdueTime > 0 ? date('Y-m-d H:i:s', $overdueTime) : '',
'message' => $days === self::DAYS_FOREVER
? '已把该用户设为永久付费会员'
: '已赠送 ' . $days . ' 天付费会员,到期时间 ' . ($overdueTime > 0 ? date('Y-m-d H:i:s', $overdueTime) : '未知'),
];
}
}

View File

@ -0,0 +1,131 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace modules\service\mcp\tools;
use app\dao\user\UserMoneyDao;
use crmeb\interfaces\McpToolInterface;
/**
* MCP 工具:余额流水列表
*
* v8 起用户余额变动记于独立账单表 user_money,历史数据可能仍存于旧账单表。
*/
class UserMoneyListTool implements McpToolInterface
{
/**
* 余额流水 DAO
* @var UserMoneyDao
*/
private UserMoneyDao $dao;
/**
* 构造函数
* @param UserMoneyDao $dao 余额流水 DAO
*/
public function __construct(UserMoneyDao $dao)
{
$this->dao = $dao;
}
/**
* 工具名称
* @return string
*/
public function name(): string
{
return 'crmeb_user_money_list';
}
/**
* 工具描述
* @return string
*/
public function description(): string
{
return '查询用户余额流水列表(v8 独立余额账单表),支持按用户 UID、变动类型、收/支方向筛选,'
. '返回变动金额、变动后余额、标题与备注;'
. 'type 常见值:recharge 充值入账、pay_product 余额支付购买商品、pay_member 余额购买会员、'
. 'offline_scan 线下收银、pay_product_refund 订单退款到余额、recharge_refund 充值退款、'
. 'extract 佣金提现到余额、lottery_add/lottery_use 抽奖、system_add/system_sub 系统调整、register_system_add 注册赠送;'
. 'pm:1 收入、0 支出;status:1 有效、0 待确定、-1 无效。';
}
/**
* 工具中文名(管理后台展示)
* @return string
*/
public function label(): string
{
return '余额流水列表';
}
/**
* 工具分类(管理后台分组展示)
* @return string
*/
public function category(): string
{
return '财务';
}
/**
* 入参 Schema
* @return array
*/
public function inputSchema(): array
{
return [
'type' => 'object',
'properties' => [
'page' => ['type' => 'number', 'description' => '页码'],
'limit' => ['type' => 'number', 'description' => '每页数量'],
'uid' => ['type' => 'number', 'description' => '用户 UID'],
'type' => ['type' => 'string', 'description' => '变动类型,如 recharge/pay_product/system_add'],
'pm' => ['type' => 'number', 'description' => '收/支方向:1 收入、0 支出'],
],
];
}
/**
* 执行工具
*
* @param array $args 参数(page/limit/uid/type/pm)
* @return array 余额流水列表和总数
*/
public function execute(array $args): array
{
$page = max(1, (int)($args['page'] ?? 1));
$limit = min(100, max(1, (int)($args['limit'] ?? 10)));
$where = [];
if (!empty($args['uid'])) {
$where[] = ['uid', '=', (int)$args['uid']];
}
if (isset($args['type']) && $args['type'] !== '') {
$where[] = ['type', '=', (string)$args['type']];
}
if (isset($args['pm']) && $args['pm'] !== '') {
$where[] = ['pm', '=', (int)$args['pm'] == 1 ? 1 : 0];
}
$list = $this->dao->search($where)
->field('id,uid,link_id,type,title,number,balance,pm,mark,status,add_time')
->order('id desc')
->page($page, $limit)
->select()
->toArray();
$count = $this->dao->search($where)->count();
return ['list' => $list, 'count' => $count];
}
}

View File

@ -0,0 +1,171 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace modules\service\mcp\tools;
use app\services\user\UserLabelRelationServices;
use app\services\user\UserServices;
use crmeb\exceptions\AdminException;
use crmeb\interfaces\McpToolInterface;
use modules\service\mcp\McpException;
/**
* MCP 工具:修改用户资料
*
* 与管理后台「编辑用户」同链路:先读用户当前值合并显式传入的修改项,
* 再调用 UserServices::updateInfo 全量落库;只改传入字段,不传的字段
* 保持原值,不会清空其他资料。服务层业务校验不因 AI 调用而豁免。
*/
class UserUpdateTool implements McpToolInterface
{
/**
* 用户服务
* @var UserServices
*/
private UserServices $services;
/**
* 用户标签关联服务(读取当前标签避免被清空)
* @var UserLabelRelationServices
*/
private UserLabelRelationServices $labelServices;
/**
* 构造函数
* @param UserServices $services 用户服务
* @param UserLabelRelationServices $labelServices 用户标签关联服务
*/
public function __construct(UserServices $services, UserLabelRelationServices $labelServices)
{
$this->services = $services;
$this->labelServices = $labelServices;
}
/**
* 工具名称
* @return string
*/
public function name(): string
{
return 'crmeb_user_update';
}
/**
* 交互声明:修改用户资料会直接变更用户数据,需用户确认后才真实执行
* @return string execute=需确认后执行
*/
public function interaction(): string
{
return 'execute';
}
/**
* 工具描述
* @return string
*/
public function description(): string
{
return '修改单个用户的基础资料,等同管理后台「编辑用户」;只修改显式传入的字段,未传字段保持原值,成功返回修改后的资料摘要。'
. '必填参数:uid 用户ID。'
. '可选字段:real_name 真实姓名(必填校验,不能为空、25 字以内)、phone 手机号(必填校验,11 位大陆手机号且全库唯一)、'
. 'email 邮箱(可空,填了须合法)、card_id 身份证号(可空,填了须合法)、birthday 生日(YYYY-MM-DD)、'
. 'addres 详细地址(255 字以内)、status 用户状态(1=开启、0=锁定)、group_id 用户分组 ID(0=未分组)、'
. 'label_ids 用户标签 ID 数组(覆盖语义:传入后用户标签被替换为该数组;只想改资料不要传此参数)、'
. 'mark 备注(255 字以内,仅改备注建议用 crmeb_user_mark_set)。'
. '至少传入一个可选字段。不能通过本工具修改余额、积分、会员等级与密码;改分组用 crmeb_user_group_set,'
. '改标签用 crmeb_user_label_set,赠送等级用 crmeb_user_level_give。';
}
/**
* 工具中文名(管理后台展示)
* @return string
*/
public function label(): string
{
return '资料修改';
}
/**
* 工具分类(管理后台分组展示)
* @return string
*/
public function category(): string
{
return '用户';
}
/**
* 入参 Schema
* @return array
*/
public function inputSchema(): array
{
return [
'type' => 'object',
'required' => ['uid'],
'properties' => [
'uid' => ['type' => 'number', 'description' => '用户ID'],
'real_name' => ['type' => 'string', 'maxLength' => 25, 'description' => '真实姓名,不能为空'],
'phone' => ['type' => 'string', 'description' => '手机号,11 位大陆手机号,全库唯一'],
'email' => ['type' => 'string', 'description' => '邮箱,可空'],
'card_id' => ['type' => 'string', 'description' => '身份证号码,可空'],
'birthday' => ['type' => 'string', 'description' => '生日,YYYY-MM-DD'],
'addres' => ['type' => 'string', 'maxLength' => 255, 'description' => '详细地址'],
'status' => ['type' => 'integer', 'enum' => [0, 1], 'description' => '用户状态:1=开启,0=锁定'],
'group_id' => ['type' => 'integer', 'minimum' => 0, 'description' => '用户分组 ID,0=未分组'],
'label_ids' => ['type' => 'array', 'items' => ['type' => 'integer'], 'description' => '用户标签 ID 数组,覆盖语义,传入后整体替换用户标签'],
'mark' => ['type' => 'string', 'maxLength' => 255, 'description' => '备注,255 字以内'],
],
];
}
/**
* 执行工具
*
* @param array $args 参数(uid + 至少一个修改字段)
* @return array 修改后的资料摘要
* @throws McpException 参数非法或服务层业务校验失败
*/
public function execute(array $args): array
{
$uid = (int)($args['uid'] ?? 0);
if ($uid <= 0) {
throw new McpException('参数错误:缺少 uid 或格式不正确');
}
$editable = ['real_name', 'phone', 'email', 'card_id', 'birthday', 'addres', 'mark', 'status', 'group_id', 'label_ids'];
$overrides = array_intersect_key($args, array_flip($editable));
if (!$overrides) {
throw new McpException('未提供任何要修改的字段,可选字段:real_name、phone、email、card_id、birthday、addres、status、group_id、label_ids、mark');
}
$data = UserEditPayload::build($this->services, $this->labelServices, $uid, $overrides);
try {
$this->services->updateInfo($uid, $data);
} catch (AdminException $e) {
// 服务层业务校验失败:转 McpException 让消息透传给 AI 客户端
throw new McpException($e->getMessage());
}
return [
'uid' => $uid,
'real_name' => $data['real_name'],
'phone' => $data['phone'],
'status' => $data['status'],
'group_id' => $data['group_id'],
'label_ids' => $data['label_id'],
'mark' => $data['mark'],
'message' => '用户资料修改成功,可通过 crmeb_user_detail 查看最新资料',
];
}
}