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

122 lines
8.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 返回结构正确。