## ADDED Requirements ### Requirement: 集成平台 OAuth2 鉴权与 token 缓存 `INTPImplClient_User#getAccessToken()` SHALL 通过 `client_credentials` 流程获取 access_token,并使用 `UtilToken` 缓存。CRUD 方法 MUST 接收 `access_token` 作为第一个参数,调用方先调 `getAccessToken()` 再传入。 #### Scenario: 首次拉取 access_token - **WHEN** `UtilToken` 缓存中无 `integration:accessToken` - **THEN** 向 `{baseUrl}/iam/token` 发 POST,body 为 `application/x-www-form-urlencoded`,字段:`grant_type=client_credentials` / `client_id={IntpConf.clientId}` / `client_secret={IntpConf.clientSecret}` - **AND** 响应 `access_token` 写入缓存,TTL = `expires_in - 60` 秒 #### Scenario: 缓存命中 - **WHEN** `UtilToken` 缓存中已有未过期的 `integration:accessToken` - **THEN** 直接复用,不发 HTTP 请求 #### Scenario: token 失效 - **WHEN** 服务端 revoke 或过期 - **THEN** 调用方收到 HTTP 401 + `error/error_description` - **AND** 本期不做"401 自动重拿",由调用方决定重试策略 ### Requirement: 用户创建 `INTPClient_User.createUser` SHALL 对齐 apifox 接口 `POST /iam/api/users`。必填字段必须显式入参;可选字段通过 `body_ext` Map 注入。 #### Scenario: 创建用户(最小集) - **WHEN** 调用 `createUser(accessToken, "alice", "Pwd@123", null)` - **THEN** 向 `{baseUrl}/iam/api/users` 发 POST `application/json` - **AND** Header `Authorization: Bearer {accessToken}` - **AND** body 为 `{"username":"alice","password":"Pwd@123"}` #### Scenario: 创建用户(完整字段) - **WHEN** `body_ext` 含 `name` / `email` / `phone_number` / `user_job_number` / `nick_name` / `picture` / `org_ids` / `address` / `title` / `hired_date` / `tag` / `group_positions` - **THEN** body 合并 `username` / `password` 与 `body_ext` 全部字段 - **AND** javadoc MUST 枚举上述字段名 + 类型 + 含义 #### Scenario: 创建失败 - **WHEN** 服务端返回 `{"result":false,"error":"USER_EXIST","error_description":"用户已存在"}` - **THEN** 返回 `McR.error("USER_EXIST", "用户已存在")` ### Requirement: 用户修改 `INTPClient_User.updateUser` SHALL 对齐 apifox 接口 `PATCH /iam/api/user`。`username` 通过 Query Param 传递(**非 Path**),其它字段全部走 body。 #### Scenario: 修改单字段 - **WHEN** 调用 `updateUser(accessToken, "alice", Map.of("email", "alice@new.com"))` - **THEN** 向 `{baseUrl}/iam/api/user?username=alice` 发 PATCH - **AND** body 为 `{"email":"alice@new.com"}` - **AND** 不传的字段保持服务端原值不变 ### Requirement: 用户删除(批量) `INTPClient_User.deleteUsers` SHALL 对齐 apifox 接口 `POST /iam/api/users/delete`。**HTTP 方法是 POST**,因 body 需带 username 数组。 #### Scenario: 批量按登录名删除 - **WHEN** 调用 `deleteUsers(accessToken, List.of("alice","bob"))` - **THEN** 向 `{baseUrl}/iam/api/users/delete` 发 POST `application/json` - **AND** body 为 `{"usernames":["alice","bob"]}` ### Requirement: 用户查询 `INTPClient_User.queryUsers` SHALL 对齐 apifox 接口 `GET /iam/api/users`,支持分页与多维过滤。 #### Scenario: 关键字搜索 - **WHEN** 调用 `queryUsers(accessToken, Map.of("q","alice","page",1,"size",20))` - **THEN** 向 `{baseUrl}/iam/api/users?q=alice&page=1&size=20` 发 GET - **AND** 解析响应 `data.items[]` 返回 `McR.success(data)` ### Requirement: 配置段独立命名 `INTPConf` SHALL 通过 Spring `@ConfigurationProperties("integration")` 加载配置;`baseUrl` 不含 `/iam` 前缀。 #### Scenario: 三个客户子模块的 yml 模板 - **WHEN** 浏览 `mjava-mcli` / `mjava-shunfeng` / `mjava-guangming` 的 `application-{dev,prod}.yml.example` - **THEN** 每个模板 MUST 含 `integration:` 段示例,字段:`baseUrl` / `clientId` / `clientSecret` - **AND** 真实凭据从 `${INTP_CLIENT_ID}` / `${INTP_CLIENT_SECRET}` 环境变量注入