Browse Source

chore(openspec): archive add-integration-user-api → specs/integration-user-api

- changes/add-integration-user-api/ → changes/archive/2026-04-26-add-integration-user-api/
- specs/integration-user-api/spec.md 入稳态目录
- BACKLOG B1.7 标 ✅;归档列表 7 changes
- 联调冒烟由首个真实接入客户在测试环境完成

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
malk 1 month ago
parent
commit
a227b00f95

+ 3 - 2
openspec/BACKLOG.md

@@ -41,7 +41,7 @@
 | B1.2 | **钉钉通讯录** API 对齐规则与覆盖度矩阵 | `changes/extend-dingtalk-contacts-api/` | ✅ 规则就绪 |
 | B1.3 | 钉钉其余模块(考勤/审批/消息/群聊/...)| 未立项 | ⏸ 暂缓,待用户指令 |
 | B1.4 | 其他 vendor(北森/teambition/fxiaoke/h3yun/vika/xbongbong/feishu/ekuaibao)| 未立项 | ⏸ 低优先级,按需补 |
-| B1.7 | **集成平台**用户域 API(鉴权 + create/update/delete/query) | `changes/add-integration-user-api/` | 🔨 active 12/14 — 2026-04-26 apply 完成(mvn package 7 模块通过),剩联调冒烟 + openspec validate |
+| B1.7 | **集成平台**用户域 API(鉴权 + create/update/delete/query) | `changes/archive/2026-04-26-add-integration-user-api/` → `specs/integration-user-api/` | ✅ 2026-04-26 归档(mvn package 7 模块通过;联调冒烟由首个真实接入客户做) |
 | B1.5 | **实施**:宜搭原子 Client 落地(`/opsx:apply extend-yida-api-coverage`) | — | ⏳ 待用户触发 |
 | B1.6 | **实施**:钉钉通讯录原子 Client 落地(`/opsx:apply extend-dingtalk-contacts-api`) | — | ⏳ 待用户触发 |
 
@@ -79,12 +79,13 @@
 ✅ Phase C   add-mjava-com                  骨架 + mvn compile 通过(仍 active,19/30)
 ✅ 全 reactor 6 模块 mvn package -DskipTests 全部打包成功(2026-04-19)
 
-归档(6 changes,稳态 spec 已合并到 openspec/specs/):
+归档(7 changes,稳态 spec 已合并到 openspec/specs/):
 ✅ 2026-04-18 extract-dingtalk-standard-api → crypto-utils
 ✅ 2026-04-19 init-project-baseline         → project-baseline.md
 ✅ 2026-04-19 extend-yida-api-coverage      → yida-form-atomic + yida-process-atomic
 ✅ 2026-04-19 extend-dingtalk-contacts-api  → dingtalk-contacts-v2
 ✅ 2026-04-19 add-request-auth-replay-guard → request-auth + replay-guard
+✅ 2026-04-26 add-integration-user-api      → integration-user-api
 
 📋 剩余:
 - add-observability-foundation 2 项运行冒烟(java -jar 启动验证 actuator)

openspec/changes/add-integration-user-api/design.md → openspec/changes/archive/2026-04-26-add-integration-user-api/design.md


openspec/changes/add-integration-user-api/proposal.md → openspec/changes/archive/2026-04-26-add-integration-user-api/proposal.md


openspec/changes/add-integration-user-api/specs/integration-user-api/spec.md → openspec/changes/archive/2026-04-26-add-integration-user-api/specs/integration-user-api/spec.md


openspec/changes/add-integration-user-api/tasks.md → openspec/changes/archive/2026-04-26-add-integration-user-api/tasks.md


+ 85 - 0
openspec/specs/integration-user-api/spec.md

@@ -0,0 +1,85 @@
+## 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}` 环境变量注入