Files
xiaoqu_mcp_service/xiaoqu-agent-service/doc/agent-user-list-mcp-tool-design.md
T

8.8 KiB
Raw Blame History

agent.user.list.query 代理角色用户查询 MCP 工具设计

  • 需求来源:王恒 2026-10-12「找下小趣保洁用户管理接口,开发一个只查代理角色用户的 MCP 工具,所属权限组、部门位置信息都要显示」
  • 老接口参照:xiaoqu-franchisee POST /user/listByPage(UserController.java:266)
  • 状态:已评审拍板(2026-10-13 王恒拍板 6 个决策点,见第 7 节),进入实现
  • 修订记录:v2 2026-10-12 经王恒指正,判定口径由 authority.isagent=1 更正为 user.enterprise_type=1;v3 2026-10-13 拍板结论回写(mobile 不脱敏、去掉位置字段)

1. 背景与老接口语义

老系统用户管理主页面接口 /user/listByPage(UserController.java:266-339):

  • 入参 UserPageInput 继承 PageInput(rows/page/sidx/sord),查询条件经 WrapperUtils.beanToWrapper 反射构造:String→like、Integer→eq、List→in。
  • 返回 UserOutput 继承 User 全字段,额外富化 agentName/propertyName/districtName/authorityName/createName/isManager/postEntityList,名称全部从 Redis 取(name:Agent:{id} 等,UserController.java:296-303)。
  • 行级过滤(:278-282):非管理员不能看代理商管理员;代理身份登录人走 WrapperUtils.getByIsAgent 收缩 agent_id 范围。
  • 字段级裁剪:field_webpage_authority 配置驱动 select 列。

"代理角色用户"的权威判定(2026-10-12 经王恒指正修订):用户表字段 user.enterprise_type=1(1=代理;2=物业,User.java:82-84)。依据:代理商创建运维人员时硬编码 setEnterpriseType(Constants.enterprise_type_1)(UserController.java:243);全系统查代理用户均为 wrapper.eq("enterprise_type", 1)(AddressController.java:94/142、DropDownController.java:146/243/460/489、WarehouseOutController.java:4134、InstallOrderController.java:199);入参 UserParamInput2/UserPageInput2 默认 enterpriseType=1。

⚠️ 初版误判:authority.isagent=1 是登录人行级权限的判定(checkAuthorityIsagent 用于收缩调用者可见 agent 范围),不是被查询用户的角色分类依据,已废弃该口径。辅助字段 user.approle(6=代理,User.java:86-88)为 APP 端角色,仅作返回展示,不作判定依据。

2. 老接口的坑(本工具全部规避)

# 坑 证据 本工具对策
1 名称靠 Redis,缓存缺失返回 null 名称 UserController.java:296-303, 112-114 直接查库批量富化
2 N+1:每行 Redis×3 + getById + deptUser list :305-332 按页批量 IN 查询
3 接口级权限为空(PermissionInterceptor return true) PermissionInterceptor.java:13-15 MCP 侧 AuthContext + 行级过滤
4 字段裁剪由配置驱动,可能漏字段/多查 password :285-287 固定白名单字段
5 sidx 排序无白名单注入面 WrapperUtils.java:313-324 固定排序 id DESC
6 mobile/idcard/password 原样返回 UserOutput 继承全字段 白名单:mobile 原样返回(v3 拍板不脱敏),idcard/password/openid 不出
7 dept_user/dept_name 为冗余快照,可能与 dept 表不一致 DeptUser.java 以 dept_user 关联记录为准返回,名称取自关联表快照(与老页面一致)
8 dept/dept_user 仅 Z/F 渠道代理强制维护,D 渠道代理用户大概率无部门数据 UserController.java:454-458 deptList 允许为空,不视为异常

3. 工具契约

agent.user.list.query(category=agent,kind=read)——员工查询(代理人员查询):分页查询代理角色用户(enterprise_type=1)列表,返回所属权限组、部门岗位明细。(v3 拍板:不返回位置信息;v4 拍板:工具描述定位为"员工查询/代理人员查询")

入参

参数 类型 必填 说明
agentId int 否 所属代理商 id,精确匹配,且必须在当前身份可见范围内
name string 否 用户姓名,模糊匹配
mobile string 否 手机号,模糊匹配
no string 否 员工编号/工号,模糊匹配
authorityId int 否 权限组 id,精确匹配(仍强制叠加 enterprise_type=1)
deptId int 否 部门 id,精确匹配(查 dept_user 关联)
workStatus int 否 在职状态,精确匹配
page int 否 默认 1,<1 归 1
rows int 否 默认 20,上限 100,超出截断 + notice

固定排序:id DESC。软删过滤 soft_delete=0。

返回

固定键序 total / page / rows / [notice] / records。record 字段:

字段 说明
id / name / no / loginAccount 基本信息
mobile 手机号原样返回不脱敏(v3 王恒拍板)
agentId / agentName 所属代理商
authorityId / authorityName 所属权限组
enterpriseType / enterpriseTypeText 1代理 2物业
approle / approleText 1运维 2保洁 3富士康 4老化 5物业 6代理
roleType / roleTypeText 0管理 1普通
workStatus / workStatusText 在职状态
deptId / deptName / postName 主岗部门(user 冗余字段)
deptList 部门岗位明细(dept_user 全量关联):deptId/deptName/postId/postName/isManager/isKey
districtId / districtName 保洁当前站点(可为空)
lastLoginTime / createTime 时间

数据源与查询

全部 m0 库:user(主表,强制 enterprise_type=1)+ authority(权限组名称富化)+ agent(代理商名称富化)+ dept_user(部门岗位明细,soft_delete=0)+ district(站点名)。

实现:user.enterprise_type=1 AND soft_delete=0 + 查询条件 + 行级过滤,selectCount + wrapper.last("LIMIT offset,size") 物理分页(项目未注册 MP 分页拦截器)→ 按页批量富化 agent/authority/district/dept_user。

4. 权限设计(新增 PermissionService.applyAgentUserRowFilter)

m0.user 表目前无行级过滤先例(users.changes.list 走 SYNC scope 白名单不过滤)。本工具口径:

  • 总部(isagent=0):可见全部代理角色用户,可传任意 agentId。
  • 代理商(isagent=1):user.agent_id IN visibleAgentIds(user)(自己 + 直属下级 + warehouse_agent_auth 代管公司,复用 PermissionService.java:146-161);传 agentId 时校验必须在可见集合内,否则抛 IllegalStateException("无权访问该代理商")。
  • 服务主体(tenant_service):按惯例不放行在线查询工具(本工具不绑 sync scope)。
  • 权限组不存在/已删/缺配置:fail closed 抛异常(对齐现有 isAgentAuthority 语义)。

不叠加老接口"非管理员不能看代理商管理员"的收缩——MCP 场景是系统对接,该规则是页面层防误操作语义。

5. 敏感字段处理

  • mobile 原样返回不脱敏(2026-10-13 王恒拍板);idcard、password、weixin_openid、weixin_name、ding_user_id 不进入返回白名单。
  • 查询条件支持 mobile 模糊匹配。

6. 实现清单

  1. PermissionService.applyAgentUserRowFilter(复用 visibleAgentIds)
  2. 实体/Mapper 复用现有 entity/m0/User.java、Authority.java、Agent.java;新增 entity/m0/DeptUser.java + DeptUserMapper;district 名称富化复用现有 DistrictMapper
  3. mcp/tool/AgentUserTools.java(@XqTool category=agent kind=read)
  4. 单测 AgentUserToolsTest:参数校验、enterprise_type=1 强制过滤、代理身份行级收缩、agentId 越权拒绝、rows 截断 notice、敏感字段不出白名单、deptList 组装、空数据(纯 Mockito + TableInfoHelper 惯例)
  5. 文档:本设计文档、doc/agent-mcp-tools-reference.md 新增章节(顺延至 §44)、README 工具表一行
  6. sql/seed-dev.sql 幂等 scope 绑定(建议绑 prod scope 1,见决策点 4)

7. 评审决策点(2026-10-13 王恒全部拍板)

# 决策点 拍板结论
1 行级权限口径 visibleAgentIds(自己+直属下级+代管),与 agent.list.query 等现有工具一致
2 mobile 脱敏 不脱敏,原样返回
3 位置信息 不需要取位置,返回不含省市区/地址字段
4 scope 绑定 只绑 prod scope 1,用户数据敏感从严
5 分类归属 agent 类
6 配套 detail 工具 不需要,列表字段已含全量信息

8. 验收标准

  1. 总部身份不传条件可查全部 enterprise_type=1 用户,分页/排序正确。
  2. 代理商身份只见 visibleAgentIds 范围内用户;传范围外 agentId 拒绝。
  3. 返回含 authorityName、deptList、agentName;mobile 原样不脱敏;无 idcard/password/openid 字段,无位置字段。
  4. rows>100 截断并带 notice;全部新增单测通过,全量测试不回归。
  5. 生产冒烟:真实 AK tools/list 可见 + tools/call 返回结构正确。