Quellcode durchsuchen

chore(config): integration yml.example 占位段 + openspec apply 进度同步

- 4 份 application-prod.yml.example 加 integration: 段(baseUrl/clientId/clientSecret 占位)
- spec/design 命名修正:IntpClient → INTPClient_User,与 DD/YD 全大写惯例一致
- design D3 调整:getAccessToken 进 Client 接口(5 个方法),与 dingtalk DDService 模式对齐
- design D4 调整:响应直接走 INTPR.assertSuccess 抛 McException,不返回 McR
- tasks.md 12/14 勾选;BACKLOG B1.7 标 active 12/14

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
malk vor 1 Monat
Ursprung
Commit
1a64580aab

+ 6 - 0
mjava-guangming/src/main/resources/application-prod.yml.example

@@ -31,3 +31,9 @@ guangming:
   mailSso:
     ssoUrl: ${MAIL_SSO_URL}
     publicKey: ${MAIL_SSO_PUBLIC_KEY}
+
+# integration(IAM 集成平台)
+integration:
+  baseUrl: ${INTP_BASE_URL}        # 不含 /iam 路径前缀
+  clientId: ${INTP_CLIENT_ID}
+  clientSecret: ${INTP_CLIENT_SECRET}

+ 6 - 0
mjava-mcli/src/main/resources/application-prod.yml.example

@@ -35,3 +35,9 @@ dingtalk:
   aesKey: ${DINGTALK_AES_KEY}
   token: ${DINGTALK_TOKEN}
   operator: ${DINGTALK_OPERATOR}
+
+# integration(IAM 集成平台)
+integration:
+  baseUrl: ${INTP_BASE_URL}        # 不含 /iam 路径前缀
+  clientId: ${INTP_CLIENT_ID}
+  clientSecret: ${INTP_CLIENT_SECRET}

+ 6 - 0
mjava-shunfeng/src/main/resources/application-prod.yml.example

@@ -46,3 +46,9 @@ dingtalk:
   aesKey: ${DINGTALK_AES_KEY}
   token: ${DINGTALK_TOKEN}
   operator: ${DINGTALK_OPERATOR}
+
+# integration(IAM 集成平台)
+integration:
+  baseUrl: ${INTP_BASE_URL}        # 不含 /iam 路径前缀
+  clientId: ${INTP_CLIENT_ID}
+  clientSecret: ${INTP_CLIENT_SECRET}

+ 6 - 0
mjava/src/main/resources/application-prod.yml.example

@@ -45,6 +45,12 @@ aliwork:
   appType: ${ALIWORK_APP_TYPE}
   systemToken: ${ALIWORK_SYSTEM_TOKEN}
 
+# integration(IAM 集成平台)
+integration:
+  baseUrl: ${INTP_BASE_URL}        # 不含 /iam 路径前缀
+  clientId: ${INTP_CLIENT_ID}
+  clientSecret: ${INTP_CLIENT_SECRET}
+
 # h3yun
 h3yun:
   engineCode: ${H3YUN_ENGINE_CODE}

+ 1 - 1
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 0/14 — 2026-04-26 立项,待 apply |
+| 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.5 | **实施**:宜搭原子 Client 落地(`/opsx:apply extend-yida-api-coverage`) | — | ⏳ 待用户触发 |
 | B1.6 | **实施**:钉钉通讯录原子 Client 落地(`/opsx:apply extend-dingtalk-contacts-api`) | — | ⏳ 待用户触发 |
 

+ 14 - 14
openspec/changes/add-integration-user-api/design.md

@@ -35,27 +35,27 @@
 - 必填字段显式入参(`username`、`password`、`usernames`)
 - 可选字段统一打入 `Map<String, Object> body_ext`,javadoc 完整枚举字段名 + 类型 + 含义
 
-具体签名:
+具体签名(`INTPClient_User`)
 
 ```java
-McR getAccessToken();                                                           // 内部用,不进 Client 接口
-McR createUser(String accessToken, String username, String password,
-               Map<String, Object> body_ext);                                   // body_ext: name/email/phone_number/user_job_number/nick_name/picture/org_ids/address/title/hired_date/tag/group_positions
-McR updateUser(String accessToken, String username,
-               Map<String, Object> body_ext);                                   // username 走 Query Param,body 全是可选字段
-McR deleteUsers(String accessToken, List<String> usernames);                    // body: { "usernames": [...] }
-McR queryUsers(String accessToken, Map<String, Object> query);                  // query: q/org_id/page/size/attrs/return_users_in_sub_org
+String getAccessToken();                                            // 自带缓存,调用方第一步先取
+Map createUser(String access_token, String username, String password,
+               Map body_ext);    // body_ext: name/email/phone_number/user_job_number/nick_name/picture/org_ids/address/title/hired_date/tag/group_positions
+Boolean updateUser(String access_token, String username,
+               Map body_ext);    // username 走 Query Param,body 全是可选字段
+Boolean deleteUsers(String access_token, List<String> usernames);   // body: { "usernames": [...] }
+Map queryUsers(String access_token, Map query);                     // query: q/org_id/page/size/attrs/return_users_in_sub_org
 ```
 
-### D4. 响应 unwrap 策略
+返回类型沿用 mjava 现有 Client 风格(如 `DDClient_Group.createGroup` 返回 `Map`、`addGroupUser` 返回 `Boolean`)。`getAccessToken()` 进接口,调用方先取再调 4 个 CRUD 方法,与 dingtalk `DDService.getAccessToken()` 模式对齐。
 
-集成平台统一响应:`{ result, error, error_description, data? }`。
+### D4. 响应 unwrap 策略
 
-- `result=true` → `McR.success(data)`
-- `result=false` → `McR.error(error, error_description)`
-- HTTP 非 2xx → `McException.throwBy(...)` 由 `CatchException` 统一拦截
+集成平台统一响应:`{ result, error, error_description, data? }`。沿用 `DDR` / `YDR` 模式新建 `INTPR extends VenR`:
 
-不在 Client 层抛业务异常(保留 `McR` 让调用方决定如何处理"用户不存在/已删除"等边界)。
+- `INTPR.assertSuccess()` 在 `result=false` 时抛 `McException(error, error_description, "integration")`
+- 由全局 `CatchException` 统一拦截转 HTTP 4xx/5xx
+- Client 方法直接返回 `(Map) intpR.getData()` 或 `intpR.isResult()`(参考 `DDR.doPost(...).isSuccess()` 用法)
 
 ### D5. 配置段命名
 

+ 10 - 10
openspec/changes/add-integration-user-api/proposal.md

@@ -6,7 +6,7 @@
 
 后续多个客户项目要调用「集成平台」(IAM / 身份治理产品)的用户管理接口,做"钉钉用户 → 集成平台用户"的同步与对接。集成平台官方接口为 RESTful + OAuth2 client_credentials 模式,与 mjava 现有 vendor(dingtalk/aliwork/...)模式不同,需要:
 
-- 独立的 `service/integration/` 包承载;类前缀 `IntpClient`
+- 独立的 `service/integration/` 包承载;类前缀 `INTPClient_*` / `INTPConf` / `INTPR`(与 dingtalk DD* / aliwork YD* 全大写惯例一致)
 - 独立的 `IntpConf`(baseUrl / clientId / clientSecret),配置段 `integration.*` 与 dingtalk/aliwork 平级
 - token 走 `UtilToken` 缓存,按"过期前刷"策略(OAuth2 返回 `expires_in`)
 
@@ -14,16 +14,16 @@
 
 ## What Changes
 
-新增 `service/integration/` 包,含
+新增 vendor 包
 
-- `IntpConf`:配置类(baseUrl / clientId / clientSecret)
-- `IntpClient`(接口):5 个原子方法
-  - `getAccessToken()` → 内部调用,不暴露给业务方
-  - `createUser(accessToken, username, password, body_ext)`
-  - `updateUser(accessToken, username, body_ext)`
-  - `deleteUsers(accessToken, usernames)`
-  - `queryUsers(accessToken, query)` —— 顺手对齐已有 GET 接口,便于校验
-- `IntpClientImpl`:基于 `UtilHttp` + `UtilToken` 的实现
+- `server/integration/INTPConf.java`:配置类(baseUrl / clientId / clientSecret)
+- `server/integration/INTPR.java`:响应包装(继承 `VenR`),字段 `result/error/error_description/data` + `assertSuccess`
+- `service/integration/INTPClient_User.java`(接口):4 个对外原子方法
+  - `createUser(access_token, username, password, body_ext)`
+  - `updateUser(access_token, username, body_ext)`
+  - `deleteUsers(access_token, usernames)`
+  - `queryUsers(access_token, query)` —— 顺手对齐 GET 接口,便于联调校验
+- `service/integration/impl/INTPImplClient_User.java`:基于 `INTPR.doPost/doGet/doPatch` + `UtilToken` 缓存 access_token 的实现(`getAccessToken()` 为 private 方法,不进 Client 接口)
 - 三个客户子模块的 `application-{dev,prod}.yml.example` 加 `integration:` 段占位
 
 不涉及:

+ 6 - 6
openspec/changes/add-integration-user-api/specs/integration-user-api/spec.md

@@ -2,7 +2,7 @@
 
 ### Requirement: 集成平台 OAuth2 鉴权与 token 缓存
 
-`IntpClientImpl` SHALL 通过 `client_credentials` 流程获取 access_token,并使用 `UtilToken` 缓存。所有对外原子方法 MUST 接收 `accessToken` 作为第一个参数(按 baseline §3.4.2)
+`INTPImplClient_User#getAccessToken()` SHALL 通过 `client_credentials` 流程获取 access_token,并使用 `UtilToken` 缓存。CRUD 方法 MUST 接收 `access_token` 作为第一个参数,调用方先调 `getAccessToken()` 再传入
 
 #### Scenario: 首次拉取 access_token
 
@@ -23,7 +23,7 @@
 
 ### Requirement: 用户创建
 
-`IntpClient.createUser` SHALL 对齐 apifox 接口 `POST /iam/api/users`。必填字段必须显式入参;可选字段通过 `body_ext` Map 注入。
+`INTPClient_User.createUser` SHALL 对齐 apifox 接口 `POST /iam/api/users`。必填字段必须显式入参;可选字段通过 `body_ext` Map 注入。
 
 #### Scenario: 创建用户(最小集)
 
@@ -45,7 +45,7 @@
 
 ### Requirement: 用户修改
 
-`IntpClient.updateUser` SHALL 对齐 apifox 接口 `PATCH /iam/api/user`。`username` 通过 Query Param 传递(**非 Path**),其它字段全部走 body。
+`INTPClient_User.updateUser` SHALL 对齐 apifox 接口 `PATCH /iam/api/user`。`username` 通过 Query Param 传递(**非 Path**),其它字段全部走 body。
 
 #### Scenario: 修改单字段
 
@@ -56,7 +56,7 @@
 
 ### Requirement: 用户删除(批量)
 
-`IntpClient.deleteUsers` SHALL 对齐 apifox 接口 `POST /iam/api/users/delete`。**HTTP 方法是 POST**,因 body 需带 username 数组。
+`INTPClient_User.deleteUsers` SHALL 对齐 apifox 接口 `POST /iam/api/users/delete`。**HTTP 方法是 POST**,因 body 需带 username 数组。
 
 #### Scenario: 批量按登录名删除
 
@@ -66,7 +66,7 @@
 
 ### Requirement: 用户查询
 
-`IntpClient.queryUsers` SHALL 对齐 apifox 接口 `GET /iam/api/users`,支持分页与多维过滤。
+`INTPClient_User.queryUsers` SHALL 对齐 apifox 接口 `GET /iam/api/users`,支持分页与多维过滤。
 
 #### Scenario: 关键字搜索
 
@@ -76,7 +76,7 @@
 
 ### Requirement: 配置段独立命名
 
-`IntpConf` SHALL 通过 Spring `@ConfigurationProperties("integration")` 加载配置;`baseUrl` 不含 `/iam` 前缀。
+`INTPConf` SHALL 通过 Spring `@ConfigurationProperties("integration")` 加载配置;`baseUrl` 不含 `/iam` 前缀。
 
 #### Scenario: 三个客户子模块的 yml 模板
 

+ 17 - 19
openspec/changes/add-integration-user-api/tasks.md

@@ -3,35 +3,33 @@
 > 已完成:apifox 公开站抓 5 个接口字段详情,结果固化在 `design.md` 覆盖度矩阵 + 风险章节。
 > 风险:apifox 文档可能与生产 API 漂移,实施冒烟时需联调验证。
 
-## 1. 配置类 yml 段
+## 1. 配置类 + 响应包装 + yml 段
 
-- [ ] 1.1 新建 `mjava/src/main/java/com/malk/service/integration/IntpConf.java`,`@ConfigurationProperties("integration")`,字段 `baseUrl` / `clientId` / `clientSecret`
-- [ ] 1.2 `mjava/src/main/resources/application-prod.yml.example` 加 `integration:` 段
-- [ ] 1.3 `mjava-mcli` / `mjava-shunfeng` / `mjava-guangming` 三个 `application-prod.yml.example` 加 `integration:` 段
-- [ ] 1.4 三个客户已有 `application-dev.yml`(不是 .example)按需自行加段,本 change 不动真实 yml
+- [x] 1.1 新建 `mjava/src/main/java/com/malk/server/integration/INTPConf.java`,`@ConfigurationProperties("integration")`,字段 `baseUrl` / `clientId` / `clientSecret` + 4 个 path 常量 + 缓存 key — 2026-04-26
+- [x] 1.2 新建 `mjava/src/main/java/com/malk/server/integration/INTPR.java`,extends `VenR`,字段 `result` / `error` / `error_description` / `data` + `access_token`/`expires_in`(兼容 token endpoint)+ `assertSuccess` 抛 `McException` + 静态 `doPost/doGet` — 2026-04-26
+- [x] 1.3 `VenR` 加 `RC_INTP = "com.malk.server.integration.INTPR"` 常量 — 2026-04-26
+- [x] 1.4 `mjava/src/main/resources/application-prod.yml.example` 加 `integration:` 段 — 2026-04-26
+- [x] 1.5 `mjava-mcli` / `mjava-shunfeng` / `mjava-guangming` 三个 `application-prod.yml.example` 加 `integration:` 段 — 2026-04-26
+- [x] 1.6 真实 dev/prod yml 不动(敏感凭据由用户自填)
 
 ## 2. Client 接口与实现
 
-- [ ] 2.1 新建 `service/integration/IntpClient.java`(接口),方法:
-      - `McR createUser(String accessToken, String username, String password, Map<String,Object> body_ext)`
-      - `McR updateUser(String accessToken, String username, Map<String,Object> body_ext)`
-      - `McR deleteUsers(String accessToken, List<String> usernames)`
-      - `McR queryUsers(String accessToken, Map<String,Object> query)`
-- [ ] 2.2 javadoc 完整枚举 `body_ext` 与 `query` 的可选字段(见 design.md / spec.md)
-- [ ] 2.3 新建 `service/integration/impl/IntpClientImpl.java`,依赖 `UtilHttp` + `UtilToken` + `IntpConf`
-- [ ] 2.4 实现内部 `getAccessToken()`:缓存命中直接返回;miss 调 `POST {baseUrl}/iam/token` 走 form body 拿 token,TTL=`expires_in - 60`
-- [ ] 2.5 4 个对外方法实现:拼 URL + Header `Authorization: Bearer {accessToken}` + 调 `UtilHttp.doPost/doPatch/doGet` + unwrap `{result, error, error_description, data}` → `McR`
+- [x] 2.1 新建 `service/integration/INTPClient_User.java`(接口),5 方法(含 `getAccessToken()`) — 2026-04-26
+- [x] 2.2 javadoc 完整枚举 `body_ext` 与 `query` 的可选字段(含子对象字段如 tag/group_positions) — 2026-04-26
+- [x] 2.3 新建 `service/integration/impl/INTPImplClient_User.java`,依赖 `INTPConf` — 2026-04-26
+- [x] 2.4 `getAccessToken()` 公开方法:缓存命中返回;miss 调 `POST {baseUrl}/iam/token` 走 form body 拿 token,TTL=`(expires_in - 60) * 1000` 毫秒入 `UtilToken.put` — 2026-04-26
+- [x] 2.5 4 个 CRUD 方法实现:拼 URL + Header `Authorization: Bearer` + `INTPR.doPost/doGet` 或 `UtilHttp.doPatch(... INTPR.class)` + `INTPR.assertSuccess` 自动抛业务异常 — 2026-04-26
 
 ## 3. 验证
 
-- [ ] 3.1 `mvn -pl mjava -am clean compile` 通过
-- [ ] 3.2 `mvn -pl mjava-mcli -am clean package -Dmaven.test.skip=true` 通过(确保配置类不会让客户子模块启动失败)
-- [ ] 3.3 联调冒烟(任选一个客户环境):
+- [x] 3.1 `mvn -pl mjava -am clean compile` 通过(2.5s,仅历史 warning) — 2026-04-26
+- [x] 3.2 `mvn clean package -DskipTests` 全 reactor 7 模块 BUILD SUCCESS(5.5s) — 2026-04-26
+- [ ] 3.3 联调冒烟(任选一个客户环境,待真实凭据):
       - 拿 clientId/clientSecret 配进 dev yml
-      - 写一个临时 Controller 调 `createUser("test_user", "Pwd@123", null)`
+      - 写一个临时 Controller 调 `getAccessToken()` + `createUser(token, "test_user", "Pwd@123", null)`
       - 验证服务端真实创建一条记录 + 返回数据正确
       - 重复调用验证 token 缓存命中(不再发 `/iam/token` 请求)
-- [ ] 3.4 `openspec validate add-integration-user-api --strict` 通过
+- [ ] 3.4 `openspec validate add-integration-user-api --strict` 通过(待 openspec CLI 可用时跑)
 
 ## 4. 归档