122 lines
8.8 KiB
Markdown
122 lines
8.8 KiB
Markdown
# 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 返回结构正确。
|