8.8 KiB
agent.user.list.query 代理角色用户查询 MCP 工具设计
- 需求来源:王恒 2026-10-12「找下小趣保洁用户管理接口,开发一个只查代理角色用户的 MCP 工具,所属权限组、部门位置信息都要显示」
- 老接口参照:
xiaoqu-franchiseePOST /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. 实现清单
PermissionService.applyAgentUserRowFilter(复用 visibleAgentIds)- 实体/Mapper 复用现有
entity/m0/User.java、Authority.java、Agent.java;新增entity/m0/DeptUser.java+DeptUserMapper;district 名称富化复用现有 DistrictMapper mcp/tool/AgentUserTools.java(@XqTool category=agent kind=read)- 单测
AgentUserToolsTest:参数校验、enterprise_type=1 强制过滤、代理身份行级收缩、agentId 越权拒绝、rows 截断 notice、敏感字段不出白名单、deptList 组装、空数据(纯 Mockito + TableInfoHelper 惯例) - 文档:本设计文档、
doc/agent-mcp-tools-reference.md新增章节(顺延至 §44)、README 工具表一行 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. 验收标准
- 总部身份不传条件可查全部 enterprise_type=1 用户,分页/排序正确。
- 代理商身份只见 visibleAgentIds 范围内用户;传范围外 agentId 拒绝。
- 返回含 authorityName、deptList、agentName;mobile 原样不脱敏;无 idcard/password/openid 字段,无位置字段。
- rows>100 截断并带 notice;全部新增单测通过,全量测试不回归。
- 生产冒烟:真实 AK tools/list 可见 + tools/call 返回结构正确。