浏览代码

docs(openspec): 立项 extend-yida-api-coverage + extend-dingtalk-contacts-api

- 归档 extract-dingtalk-standard-api → archive/2026-04-18-extract-dingtalk-standard-api/
- 新增 crypto-utils 稳态 spec(由归档自动提升)
- 新增 changes/extend-yida-api-coverage/:YDClient_Form + YDClient_Process 拆分规则与覆盖度矩阵(14+12 个 endpoint)
- 新增 changes/extend-dingtalk-contacts-api/:按用户/部门/角色/字段四个子模块对齐规则(~25 个方法清单,~17 新增 + 8 v2 对齐)
- openspec validate --strict 两个 change 均通过
- CLAUDE.md + BACKLOG.md 更新 Phase B.1 范围,标注高优先级宜搭优先

用户指令:改造先不做,完成规则定义后再启动 /opsx:apply。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
malk 2 周之前
父节点
当前提交
90419e9036

+ 6 - 4
CLAUDE.md

@@ -20,11 +20,13 @@ Java 后端基座 + 客户子项目仓库。Spring Boot 2.2.13 + MySQL,第一
 | `/opsx:archive` | 完成后归档到 `openspec/changes/archive/` |
 
 现有 change 状态:
+- `changes/archive/2026-04-18-extract-dingtalk-standard-api/` — 已归档(crypto-utils 稳态 spec 合并)
 - `changes/add-observability-foundation/` — 代码已实施,待生产冒烟
-- `changes/extract-dingtalk-standard-api/` — 已完成,待归档
-- `changes/init-project-baseline/` — 文档提案(基线沉淀)
-- `changes/add-mjava-pro/` — 专项,**高优先级**:多客户单部署(宜搭应用表授权)
-- `changes/add-mjava-com/` — 专项,**高优先级**:通用能力 BaaS(宜搭权限表单授权)
+- `changes/init-project-baseline/` — 基线沉淀,仅 `mvn compile` 验证阻塞在 Maven 未装
+- `changes/extend-yida-api-coverage/` — **Phase B.1 最高优先级**:宜搭表单+流程 API 对齐
+- `changes/extend-dingtalk-contacts-api/` — Phase B.1:钉钉通讯录 API 对齐
+- `changes/add-mjava-pro/` — Phase C:多客户单部署(待 B 完成后)
+- `changes/add-mjava-com/` — Phase C:通用能力 BaaS(待 B 完成后)
 
 ## 快速操作
 

+ 12 - 11
openspec/BACKLOG.md

@@ -26,22 +26,23 @@
 | A7 | 敏感文件 .gitignore 防护 + .example 模板 | ✅ 完成 |
 | A8 | mjava-pro / mjava-com 专项提案四件套 | ✅ 完成 |
 | A9 | init-project-baseline tasks 5.1-5.3 验证 | ✅ 完成 |
-| A10 | **执行 `/opsx:archive extract-dingtalk-standard-api`**(全部 tasks 已 `[x]`) | ⏳ 等用户触发 |
+| A10 | 归档 `extract-dingtalk-standard-api` | ✅ 完成,`changes/archive/2026-04-18-extract-dingtalk-standard-api/` |
 | A11 | `brew install maven` 后补做 init-baseline 任务 5.4(编译冒烟) | ⏳ 阻塞在本机未装 Maven |
-| A12 | `/Users/malk/Desktop/Tech/claude/` 是否 git init(规范文档版本化)? | ❓ 待用户决定 |
+| A12 | `/Users/malk/Desktop/Tech/claude/` git init(规范文档版本化) | ✅ 完成(白名单策略,只收规范 md) |
+| A13 | 扩展 `mjava-baseline.md §3.4` — Client API 对齐详细规则(§3.4.1-§3.4.5)| ✅ 完成 |
 
 ## 阶段 B.1 — 补全 mjava 模块 API 能力
 
-现状观察:`mjava/service/{vendor}/` 下每个 vendor 已有 `Client` + `Client_{Domain}` 的骨架,但官方新 API 不断增加,部分 endpoint 可能缺失
+按用户优先级指令(2026-04-18):**先宜搭,后钉钉通讯录,其余 vendor 暂不做**。改造行为由 `/opsx:apply` 触发,本阶段**先完成规则定义**
 
-| ID | 任务 | 备注 |
-|----|------|------|
-| B1.1 | 盘点钉钉 Client 覆盖度(对照钉钉开放平台官方 API 列表) | 输出缺失 endpoint 清单 |
-| B1.2 | 盘点 YDClient 覆盖度 | 对照宜搭 API 文档 |
-| B1.3 | 盘点 BSClient(北森)覆盖度 | 对照北森 API 文档 |
-| B1.4 | 盘点其余 vendor(teambition / fxiaoke / h3yun / vika / xbongbong / feishu / ekuaibao)覆盖度 | 低优先级,按需补 |
-| B1.5 | 按 §3.4 规范补缺失 endpoint:**方法签名必须兼容官方全部参数,禁止为图省事删减字段** | 每个 vendor 一个 PR |
-| B1.6 | Client 新增方法的 Service 层编排(如需批量 / 分页聚合) | 按需 |
+| ID | 任务 | 归属 change | 状态 |
+|----|------|------------|------|
+| B1.1 | **宜搭**表单 + 流程 API 对齐规则与覆盖度矩阵 | `changes/extend-yida-api-coverage/` | ✅ 规则就绪(proposal/design/spec/tasks 四件套 valid) |
+| B1.2 | **钉钉通讯录** API 对齐规则与覆盖度矩阵 | `changes/extend-dingtalk-contacts-api/` | ✅ 规则就绪 |
+| B1.3 | 钉钉其余模块(考勤/审批/消息/群聊/...)| 未立项 | ⏸ 暂缓,待用户指令 |
+| B1.4 | 其他 vendor(北森/teambition/fxiaoke/h3yun/vika/xbongbong/feishu/ekuaibao)| 未立项 | ⏸ 低优先级,按需补 |
+| B1.5 | **实施**:宜搭原子 Client 落地(`/opsx:apply extend-yida-api-coverage`) | — | ⏳ 待用户触发 |
+| B1.6 | **实施**:钉钉通讯录原子 Client 落地(`/opsx:apply extend-dingtalk-contacts-api`) | — | ⏳ 待用户触发 |
 
 ## 阶段 B.2 — 待办功能添加(基座增强)
 

openspec/changes/extract-dingtalk-standard-api/.openspec.yaml → openspec/changes/archive/2026-04-18-extract-dingtalk-standard-api/.openspec.yaml


openspec/changes/extract-dingtalk-standard-api/design.md → openspec/changes/archive/2026-04-18-extract-dingtalk-standard-api/design.md


openspec/changes/extract-dingtalk-standard-api/proposal.md → openspec/changes/archive/2026-04-18-extract-dingtalk-standard-api/proposal.md


openspec/changes/extract-dingtalk-standard-api/specs/crypto-utils/spec.md → openspec/changes/archive/2026-04-18-extract-dingtalk-standard-api/specs/crypto-utils/spec.md


openspec/changes/extract-dingtalk-standard-api/tasks.md → openspec/changes/archive/2026-04-18-extract-dingtalk-standard-api/tasks.md


+ 119 - 0
openspec/changes/extend-dingtalk-contacts-api/design.md

@@ -0,0 +1,119 @@
+## 目标结构
+
+`DDClient_Contacts` 内部按子模块分段,同一文件内保持阅读连贯性(不拆成 `_Contacts_User` / `_Contacts_Dept`):
+
+```java
+public interface DDClient_Contacts {
+    // ========== 用户管理 User ==========
+    Map createUser(...);          // 旧, 保留
+    Map createUser_v2(...);       // 新对齐
+    Map updateUser(...);          // 新增(旧无此方法)
+    ...
+
+    // ========== 部门管理 Department ==========
+    Map createDepartment(...);
+    Map createDepartment_v2(...);
+    ...
+
+    // ========== 角色管理 Role ==========
+    Map addRole(...);             // 新增
+    Map listRoles(...);
+    ...
+
+    // ========== 员工字段管理 EmployeeField ==========
+    Map hideEmployeeField(...);
+    ...
+}
+```
+
+## v2 命名约定
+
+同一个 endpoint 若**旧方法签名不规范**(参数缺漏、命名不统一),新方法用 `_v2` 后缀:
+
+- `createUser` (旧) 不删
+- `createUser_v2` (新) 按 §3.4.2 规范,所有 body_ext 字段 javadoc 列全
+
+若**旧方法不存在**(真正新增),直接用正常名字 `updateUser`,无需 `_v2`。
+
+## 覆盖度矩阵(任务 0 填写模板)
+
+| 官方 endpoint | 当前方法 | 对齐行动 |
+|--------------|---------|---------|
+| POST /topapi/v2/user/create | `createUser` | 新增 `createUser_v2` |
+| POST /topapi/v2/user/update | ❌ 缺 | 新增 `updateUser` |
+| POST /topapi/v2/user/delete | `deleteUser` | 不动(签名已规范) |
+| POST /topapi/v2/user/get | `getUserInfoById` | 新增 `getUser_v2`(命名统一) |
+| POST /topapi/v2/user/getbymobile | `getUserInfoByMobile` | 新增 `getUserByMobile_v2` |
+| POST /topapi/user/listsimple | ❌ 缺 | 新增 `listUsersSimple` |
+| POST /topapi/v2/user/list | 部分(`listSubDepartmentDetail`?)| 新增 `listDeptUserDetail_v2` |
+| GET /topapi/user/listid | `listDepartmentUserId` | 不动 |
+| POST /topapi/inactive/user/v2/get | ❌ 缺 | 新增 `listInactiveUsers` |
+| POST /topapi/user/getbyunionid | ❌ 缺 | 新增 `getUserByUnionId` |
+| POST /topapi/user/get_admin | ❌ 缺 | 新增 `listAdmins` |
+| POST /topapi/smartwork/hrm/employee/listdimission | `getLeaveEmployeeRecords` | 新增 `listDimissionEmployees_v2` |
+| POST /topapi/v2/department/create | `createDepartment` | 新增 `createDepartment_v2` |
+| POST /topapi/v2/department/update | ❌ 缺 | 新增 `updateDepartment` |
+| POST /topapi/v2/department/delete | ❌ 缺 | 新增 `deleteDepartment` |
+| POST /topapi/v2/department/get | `getDepartmentInfo` | 新增 `getDepartment_v2` |
+| POST /topapi/v2/department/listsubid | `listSubDepartmentId` | 不动 |
+| POST /topapi/v2/department/listparentbyuser | `listParentByUser` | 不动 |
+| POST /topapi/v2/department/listparentbydept | ❌ 缺 | 新增 `listParentByDept` |
+| POST /topapi/v2/department/listsub | `listSubDepartmentDetail` | 新增 `listSubDepartments_v2` |
+| POST /topapi/v2/role/add_role | ❌ 缺 | 新增 `addRole` |
+| POST /topapi/v2/role/addrolesforemps | ❌ 缺 | 新增 `addRolesForEmps` |
+| POST /topapi/v2/role/removerolesforemps | ❌ 缺 | 新增 `removeRolesForEmps` |
+| POST /topapi/v2/role/update_role | ❌ 缺 | 新增 `updateRole` |
+| POST /topapi/v2/role/list | ❌ 缺 | 新增 `listRoles` |
+| POST /topapi/v2/role/getrole | ❌ 缺 | 新增 `getRole` |
+| POST /topapi/v2/role/simplelist | ❌ 缺 | 新增 `listRoleEmployees` |
+| POST /topapi/hide_field/add_or_update | ❌ 缺 | 新增 `upsertHideField` |
+| POST /topapi/hide_field/remove | ❌ 缺 | 新增 `removeHideField` |
+| POST /topapi/hide_field/query | ❌ 缺 | 新增 `listHideFields` |
+
+总计:**~30 个方法**,其中 **~17 个新增**,**~8 个 v2 对齐**,**~5 个保持不动**。
+
+## 方法签名模板
+
+```java
+/**
+ * 创建用户(v2 对齐版)
+ * @apiNote https://open.dingtalk.com/document/orgapp/user-information-creation
+ *
+ * @param access_token  钉钉 accessToken(非企业内部应用按文档约定)
+ * @param name          员工姓名(必填)
+ * @param mobile        手机号(必填,同一企业内不重复)
+ * @param dept_id_list  所属部门 ID 列表(必填,至少 1 个)
+ * @param body_ext 可选参数:
+ *   - userid (String): 自定义 userid,不传则自动生成
+ *   - hired_date (Long): 入职时间毫秒时间戳
+ *   - job_number (String): 工号
+ *   - title (String): 职位
+ *   - work_place (String): 办公地点
+ *   - remark (String): 备注
+ *   - email (String): 邮箱(应用需有通讯录读写权限)
+ *   - org_email (String): 企业邮箱
+ *   - org_email_type (String): profession / premium
+ *   - extension (String): 扩展属性 JSON 字符串
+ *   - senior_mode (Boolean): 是否高管模式
+ *   - hide_mobile (Boolean): 是否隐藏手机号
+ *   - telephone (String): 分机号
+ *   - dept_order_list (List<Map>): 部门内排序
+ *   - dept_title_list (List<Map>): 部门内职位
+ *   - login_email (String): 企业账号登录邮箱
+ *   - exclusive_account (Boolean): 是否专属账号
+ *   - exclusive_account_type (String): sso/custom
+ *   - exclusive_mobile (String): 专属账号手机号
+ *   - ...(完整清单见官方文档,最后同步日期 2026-04-18)
+ * @return Map 包含 userid、isLeaderInDepts 等
+ */
+Map createUser_v2(String access_token, String name, String mobile,
+                  List<String> dept_id_list, Map<String, Object> body_ext);
+```
+
+## 风险与缓解
+
+| 风险 | 缓解 |
+|------|------|
+| body_ext javadoc 随官方文档变动脱节 | javadoc 末尾强制写"最后同步日期",下次对齐时更新 |
+| 新旧方法并存造成调用方选择困惑 | README 加明确指引"新项目用 _v2 / 无后缀版本;历史项目原地不动" |
+| 新增方法数量大(~17 个)导致 PR 过重 | 按用户 / 部门 / 角色 / 员工字段四个子模块拆 4 个 PR |

+ 44 - 0
openspec/changes/extend-dingtalk-contacts-api/proposal.md

@@ -0,0 +1,44 @@
+> 状态(2026-04-18 立项):**规则定义阶段,不做代码改造**
+> 优先级:高(Phase B.1 第二个专项)
+> 改造等宜搭专项规则走通后启动
+
+## Why
+
+`DDClient_Contacts` 当前有 ~18 个方法,对齐钉钉通讯录的部分 endpoint,但:
+
+- **参数不完整**:`createUser(access_token, name, mobile, dept_id_list, body_ext)` 只显式暴露了 3 个必填参数,其他如 `userid` / `hired_date` / `job_number` / `email` / `title` 全塞在 `body_ext` 里,**javadoc 未枚举**,调用方要查官方文档才知道能传什么。
+- **缺失 endpoint**:钉钉通讯录目前官方提供 30+ 个 endpoint,现有实现只覆盖 18 个,缺 `updateUser` / `listsimpleUser` / `getUserByUnionId` / `getAdminList` / `listInactive` / `createDepartment_v2` / `updateDepartment` / `listParentByDept` / 角色管理(addRole/delRole/listRoles/...)/ 员工字段隐藏设置 等。
+- **命名不一致**:`getUserInfoById` vs `getDepartmentInfo` 动词选择混乱,新方法应统一 `get{Resource}` / `list{Resource}` / `create{Resource}` 三模式。
+
+## What Changes
+
+**保留 `DDClient_Contacts` 现有方法**(已上线客户依赖),按 `mjava-baseline §3.4.4` **新增对齐方法**:
+
+- 补齐缺失 endpoint(参照钉钉官方通讯录 API 列表)
+- 对已有方法做**签名对齐新版本**(方法名加后缀 `_v2` 或重载),`body_ext` 字段在 javadoc 完整枚举
+- 按"用户管理 / 部门管理 / 角色管理 / 员工字段管理"四个子模块在同一接口内分段组织
+- 后续专项统一把旧方法标 `@Deprecated`(不在本 change 做)
+
+涉及范围**仅钉钉通讯录**;考勤 / OA 审批 / 消息 / 群聊等其他 DDClient_{Domain} 模块不在此次提案。
+
+## Capabilities
+
+### New Capabilities
+- `dingtalk-contacts-v2`:对齐后的通讯录原子接口集(含用户 / 部门 / 角色 / 员工字段)
+
+### Modified Capabilities
+- `dingtalk-contacts-legacy`(现有):标记为后续弃用,保留原有接口
+
+## Impact
+
+- **修改文件**:`DDClient_Contacts.java` 追加方法;`impl/DDImplClient_Contacts.java` 追加实现
+- **不改动**:旧方法签名
+- **API 消费者**:旧方法继续可用;新方法可按需切换
+- **三个生产客户**:零影响
+
+## Non-Goals
+
+- ❌ 不删或改旧方法
+- ❌ 不做考勤 / OA 审批 / 消息 / 群聊等模块(每模块独立 change)
+- ❌ 不做企业外部联系人、专属账号的完整覆盖(现有 `DDClient_Dedicated` 已部分覆盖,按需再评估)
+- ❌ 不做新旧 API 自动路由(由调用方显式选择 v2 方法)

+ 95 - 0
openspec/changes/extend-dingtalk-contacts-api/specs/dingtalk-contacts-v2/spec.md

@@ -0,0 +1,95 @@
+## ADDED Requirements
+
+### Requirement: 用户管理完整覆盖
+
+`DDClient_Contacts` SHALL 提供对钉钉官方通讯录用户管理 endpoint 的完整对齐。所有方法 MUST 遵守 `mjava-baseline §3.4.2` 的签名规则。
+
+#### Scenario: 创建用户(完整字段)
+
+- **WHEN** 调用 `createUser_v2(access_token, name, mobile, dept_id_list, body_ext)`
+- **THEN** `body_ext` 必须支持官方文档列出的所有可选字段(userid / hired_date / job_number / title / email / senior_mode / extension / ...)
+- **AND** javadoc 必须枚举这些字段并注明类型
+
+#### Scenario: 更新用户
+
+- **WHEN** 调用 `updateUser(access_token, userid, body_ext)`
+- **THEN** `userid` 必填,其他字段全部通过 `body_ext` 传入
+- **AND** 不传的字段保持钉钉侧原值不变
+
+#### Scenario: 按 unionId 查询
+
+- **WHEN** 调用 `getUserByUnionId(access_token, union_id)`
+- **THEN** 返回 userid(二次调用 `getUser_v2` 获取详情)
+
+#### Scenario: 查询管理员
+
+- **WHEN** 调用 `listAdmins(access_token)`
+- **THEN** 返回管理员 userid 列表,含主管理员与子管理员区分字段
+
+#### Scenario: 查询未激活员工
+
+- **WHEN** 调用 `listInactiveUsers(access_token, is_active, offset, size, body_ext)`
+- **THEN** 支持按激活状态过滤
+
+### Requirement: 部门管理完整覆盖
+
+`DDClient_Contacts` SHALL 提供部门 CRUD 与层级查询的完整对齐。所有方法 MUST 对应钉钉官方部门管理 endpoint。
+
+#### Scenario: 部门 CRUD
+
+- **WHEN** 依次调用 `createDepartment_v2` / `updateDepartment` / `deleteDepartment` / `getDepartment_v2`
+- **THEN** 每个方法参数完整对齐官方文档
+- **AND** `body_ext` 支持 `hide_dept` / `dept_permits` / `user_permits` / `outer_dept` / `source_identifier` 等全部可选项
+
+#### Scenario: 父部门链查询
+
+- **WHEN** 调用 `listParentByDept(access_token, dept_id)`
+- **THEN** 返回目标部门到根部门的父链
+
+### Requirement: 角色管理
+
+`DDClient_Contacts` SHALL 提供角色的增删改查与批量员工授权能力。
+
+#### Scenario: 新增角色
+
+- **WHEN** 调用 `addRole(access_token, roleName, groupId)`
+- **THEN** 返回 roleId
+
+#### Scenario: 批量分配角色
+
+- **WHEN** 调用 `addRolesForEmps(access_token, roleIds, userIds)`
+- **THEN** 一次调用同时为多员工授权多角色
+
+#### Scenario: 查询角色成员
+
+- **WHEN** 调用 `listRoleEmployees(access_token, role_id, size, offset)`
+- **THEN** 分页返回角色下员工列表
+
+### Requirement: 员工字段可见性
+
+`DDClient_Contacts` SHALL 提供员工档案字段隐藏规则的管理能力。
+
+#### Scenario: 隐藏字段设置
+
+- **WHEN** 调用 `upsertHideField(access_token, name, field, userIds, deptIds)`
+- **THEN** 对指定员工或部门隐藏指定员工档案字段
+
+#### Scenario: 查询隐藏字段
+
+- **WHEN** 调用 `listHideFields(access_token, size, offset)`
+- **THEN** 返回企业当前生效的字段隐藏规则列表
+
+### Requirement: 参数透传约束
+
+本 capability 所有方法 MUST 满足官方可选参数完整透传与文档化要求,不得因 Java 侧建模省事而过滤字段。
+
+#### Scenario: body_ext 不得过滤官方可选参数
+
+- **WHEN** 调用方通过 `body_ext` 传入官方文档支持的任意可选字段
+- **THEN** 实现必须原样透传到 HTTP 请求体,不得删除或改写
+- **AND** 方法 javadoc 必须列出所有已知 body_ext key 并注明类型
+
+#### Scenario: 方法必须有 apiNote
+
+- **WHEN** 新增任一 Client 方法
+- **THEN** javadoc 必须含 `@apiNote` 链到该 endpoint 的钉钉官方文档页

+ 71 - 0
openspec/changes/extend-dingtalk-contacts-api/tasks.md

@@ -0,0 +1,71 @@
+## 0. 覆盖度矩阵(前置,开发者必须对照官方文档最新版填写)
+
+> 见 `design.md` 表格;当前盘点截至 2026-04-18,约 17 项新增 + 8 项 v2 对齐。
+
+## 1. 前置
+
+- [ ] 1.1 对照钉钉官方通讯录文档**当前版本**,逐行复核 `design.md` 覆盖度矩阵,补齐 apiNote 链接
+- [ ] 1.2 确认 `DDConf` 是否已含完整的 `corpId` / `appKey` / `appSecret`,按需扩展
+
+## 2. 用户管理(User)
+
+- [ ] 2.1 `createUser_v2`(完整 body_ext javadoc)
+- [ ] 2.2 `updateUser`
+- [ ] 2.3 `getUser_v2`(取代 `getUserInfoById` 作为新规范入口)
+- [ ] 2.4 `getUserByMobile_v2`
+- [ ] 2.5 `listUsersSimple`
+- [ ] 2.6 `listDeptUserDetail_v2`(分页含详情,取代现有零散 `listSubDepartmentDetail`)
+- [ ] 2.7 `listInactiveUsers`
+- [ ] 2.8 `getUserByUnionId`
+- [ ] 2.9 `listAdmins`
+- [ ] 2.10 `listDimissionEmployees_v2`
+
+## 3. 部门管理(Department)
+
+- [ ] 3.1 `createDepartment_v2`
+- [ ] 3.2 `updateDepartment`
+- [ ] 3.3 `deleteDepartment`
+- [ ] 3.4 `getDepartment_v2`
+- [ ] 3.5 `listSubDepartments_v2`
+- [ ] 3.6 `listParentByDept`
+
+## 4. 角色管理(Role)
+
+- [ ] 4.1 `addRole`
+- [ ] 4.2 `updateRole`
+- [ ] 4.3 `deleteRole`(若官方有)
+- [ ] 4.4 `listRoles`
+- [ ] 4.5 `getRole`
+- [ ] 4.6 `listRoleEmployees`
+- [ ] 4.7 `addRolesForEmps`
+- [ ] 4.8 `removeRolesForEmps`
+
+## 5. 员工字段管理(EmployeeField)
+
+- [ ] 5.1 `upsertHideField`
+- [ ] 5.2 `removeHideField`
+- [ ] 5.3 `listHideFields`
+
+## 6. 实现
+
+- [ ] 6.1 `DDClient_Contacts.java` 接口追加 ~25 个方法声明(按子模块分段注释清晰)
+- [ ] 6.2 `impl/DDImplClient_Contacts.java` 实现全部新方法
+- [ ] 6.3 所有实现走 `UtilHttp`(禁 SDK)
+- [ ] 6.4 每个方法的 javadoc 严格遵守 §3.4.2 模板(必填注释 + body_ext 枚举 + apiNote)
+
+## 7. 文档
+
+- [ ] 7.1 在 mjava-baseline.md §3.4 表格中追加"dingtalk-contacts-v2 已对齐"状态
+- [ ] 7.2 PR 描述含官方文档对照矩阵截图
+
+## 8. 验证
+
+- [ ] 8.1 单元测试:每个方法的参数透传(mock UtilHttp 验证 body 完整)
+- [ ] 8.2 集成测试:选 5 个关键 endpoint(createUser_v2 / updateUser / listAdmins / addRole / upsertHideField)跑真实钉钉
+- [ ] 8.3 `openspec validate extend-dingtalk-contacts-api --strict` 通过
+- [ ] 8.4 审视 mjava-guangming / mjava-shunfeng / mjava-mcli 是否需要迁移到新 v2 方法(本次**不强制迁**,只评估)
+
+## 9. 交付
+
+- [ ] 9.1 PR 按子模块拆分(User / Dept / Role / Field 四个)
+- [ ] 9.2 走 `/opsx:archive` 归档

+ 92 - 0
openspec/changes/extend-yida-api-coverage/design.md

@@ -0,0 +1,92 @@
+## 目标架构
+
+```
+┌─────────────────────────────────────────────────────┐
+│ 业务 Controller (mcli / shunfeng / guangming / pro) │
+└─────────────────────┬───────────────────────────────┘
+                      │
+          ┌───────────▼────────────┐
+          │  YDService (组合)      │  公开签名不变
+          └───────────┬────────────┘
+                      │ 内部改调原子
+     ┌────────────────┼────────────────┐
+     │                │                │
+┌────▼────┐    ┌──────▼──────┐   ┌─────▼──────┐
+│YDClient │    │YDClient_Form│   │YDClient_Proc│
+│(deprecated)│  │ (新增原子)  │   │ (新增原子) │
+└─────────┘    └─────────────┘   └─────────────┘
+                      │                │
+                      └────┬───────────┘
+                           ▼
+                      UtilHttp (审计日志 §3.5)
+                      UtilToken (tenantId:yida:appType)
+```
+
+## 模块拆分映射
+
+### YDClient_Form(表单数据)
+
+按宜搭官方文档模块映射:
+
+| 子模块 | 方法前缀 | 备注 |
+|--------|---------|------|
+| 表单实例 CRUD | `saveForm` / `updateForm` / `deleteForm` / `getForm` | 单实例 |
+| 查询 | `searchForm` / `listFormIds` / `listFormsAll` | 分页 / ID 列表 / 全量含子表 |
+| 批量 | `batchSaveForm` / `batchUpsertForm` | 批量接口 |
+| 组件值 | `listComponentValues` | 指定字段取值 |
+| 操作日志 | `listFormOperations` | 审计日志 |
+| 按条件删除 | `deleteFormByCondition` | 条件批量删 |
+
+### YDClient_Process(流程审批)
+
+| 子模块 | 方法前缀 | 备注 |
+|--------|---------|------|
+| 流程实例 | `startProcess` / `terminateProcess` / `revokeProcess` / `getProcess` / `searchProcesses` | 发起 / 终止 / 撤回 / 查 |
+| 任务 | `agreeTask` / `disagreeTask` / `redirectTask` / `ccTask` / `commentTask` / `searchTasks` | 审批动作 |
+| 节点 | `redirectProcess` / `removeNode` | 跳转节点 |
+
+## 方法签名模板(强约束)
+
+```java
+/**
+ * 新增表单实例
+ * @apiNote https://open.dingtalk.com/document/orgapp/add-or-update-form-instances
+ *
+ * @param conf            宜搭应用鉴权(appType + systemToken + userId)
+ * @param formUuid        表单 UUID(必填)
+ * @param formDataJson    表单数据 JSON 字符串(必填,字段名使用表单 fieldId)
+ * @param body_ext 可选字段:
+ *   - noExecuteExpression (Boolean): 是否不执行公式字段计算
+ *   - language (String): 语言(zh_CN/en_US)
+ *   - ...(完整清单见官方文档)
+ * @return formInstanceId 新增成功的表单实例 ID
+ */
+String saveForm(YDConf conf, String formUuid, String formDataJson, Map<String, Object> body_ext);
+```
+
+**强约束**:
+1. 第 1 参永远是 `YDConf`(封装 appType / systemToken / userId)
+2. 必填参数严格按官方文档顺序
+3. 可选参数 `body_ext` 即使当前业务用不到也必须保留
+4. javadoc 必须有 `@apiNote` 官方文档链接
+5. javadoc 必须列出 body_ext 支持的 key(每个 key 标明类型)
+
+## 兼容策略
+
+- `YDClient` 接口文件不动,旧方法签名保持
+- 旧方法在下个 `remove-yida-legacy-aggregate` change 里统一加 `@Deprecated` 并标注迁移目标
+- `YDService` 可以内部悄悄切换到新方法,**只要公开签名不变**
+
+## Non-Goals
+
+- 不重写 `YDParam` 建造者的契约
+- 不做"连接器应用"、"附件管理"、"表单设计器" 模块(作为后续 change 扩展)
+- 不做反射式调度(每个方法显式实现)
+
+## 风险
+
+| 风险 | 缓解 |
+|------|------|
+| 旧 operateData 某些边角 case 在新方法里漏 | 迁移前保留旧路径,新方法就绪后做等价测试再切 YDService |
+| 字段过多,`body_ext` javadoc 难维护 | 文档化**日期与当前官方版本**,后续按需更新;鼓励 PR review 时对照 |
+| 客户子项目里有人绕过 YDService 直接调 YDClient 旧方法 | grep 审计 + 后续 Deprecated + 编译告警推动 |

+ 55 - 0
openspec/changes/extend-yida-api-coverage/proposal.md

@@ -0,0 +1,55 @@
+> 状态(2026-04-18 立项):**规则定义阶段,不做代码改造**
+> 优先级:**最高**(Phase B.1 首个专项)
+> 改造执行等规则通过后,走 `/opsx:apply extend-yida-api-coverage`
+
+## Why
+
+`YDClient` 当前只有两个聚合方法 `operateData(param, FORM_OPERATION)` 与 `queryData(param, FORM_QUERY)`,通过枚举内部路由到不同宜搭 API。**违反** `mjava-baseline §3.4` 的"Client 原子接口 1:1 对应官方 endpoint"规范:
+
+- 一个方法承载多种语义,调用方必须理解 `FORM_OPERATION` 枚举才能用;
+- 参数通过 `YDParam` 建造者承载,**无法从方法签名直接看出哪些字段必填**;
+- 官方新增 endpoint 时没有明确的落地位置,容易漏或错位;
+- Service 层无法对单一 endpoint 做精准缓存或审计(审计颗粒度只能到 `operateData`)。
+
+现有三个客户模块(mcli / shunfeng / guangming)依赖旧 `operateData` / `queryData`,**不能直接删**。采用"新增对齐方法 + 保留旧方法 @Deprecated"的并行策略(mjava-baseline §3.4.4)。
+
+## What Changes
+
+按宜搭开放平台官方文档,把 `YDClient` 拆分为:
+
+```
+service/aliwork/
+├── YDClient.java              # 保留旧 operateData/queryData, 标 @Deprecated
+├── YDClient_Form.java         # 表单数据 API 原子接口(新增)
+├── YDClient_Process.java      # 流程审批 API 原子接口(新增)
+└── YDService.java             # 组合服务(保留,内部改调 Form/Process 原子方法)
+```
+
+- 新增 `YDClient_Form`:覆盖表单实例增删改查、批量、按条件删除、查询组件值等
+- 新增 `YDClient_Process`:覆盖发起/同意/拒绝/转交/跳转/撤回/终止/任务评论等
+- 方法签名严格按 `mjava-baseline §3.4.2` 规则:必填参数显式、可选参数放 `Map body_ext` 并在 javadoc 中**穷举**
+- 每个方法 `@apiNote` 链到宜搭官方文档对应页面
+
+## Capabilities
+
+### New Capabilities
+- `yida-form-atomic`:宜搭表单原子接口集
+- `yida-process-atomic`:宜搭流程原子接口集
+
+### Modified Capabilities
+- `yida-legacy-aggregate`(现有):标记为 deprecated,保持现有行为
+
+## Impact
+
+- **新增文件**:`YDClient_Form.java` + `YDClient_Process.java` + `impl/` 两份实现 + 可能的 `YDParam` 补字段
+- **不动**:`YDClient.operateData` / `queryData` 方法签名与行为
+- **YDService 改造**(可选):内部调用从 `YDClient.operateData` 迁到新原子方法,保持公开签名不变,让调用方透明受益
+- **mcli / shunfeng / guangming 三个客户**:零影响
+- **审计日志**:每个原子方法都能被 `UtilHttp` 单独打点,颗粒度从"操作/查询"提升到具体 endpoint
+
+## Non-Goals
+
+- ❌ 不删旧 `operateData` / `queryData`(等所有新方法稳定 + 全部调用方迁移完成,再单独走一个 `remove-yida-legacy-aggregate` change)
+- ❌ 不封装 SDK 层(依然走 `UtilHttp.doRequest`)
+- ❌ 本提案不做"连接器"、"附件"、"流程设计器"模块(作为后续扩展 change)
+- ❌ 不改 `YDParam` 建造者的对外契约(内部扩字段 OK)

+ 68 - 0
openspec/changes/extend-yida-api-coverage/specs/yida-form-atomic/spec.md

@@ -0,0 +1,68 @@
+## ADDED Requirements
+
+### Requirement: 表单实例 CRUD 原子接口
+
+`YDClient_Form` SHALL 提供与宜搭官方 1:1 对应的表单实例增删改查原子方法。方法签名 MUST 严格遵守 `mjava-baseline §3.4.2`(必填参数显式 + `body_ext` 承接可选参数 + javadoc 枚举全部 body_ext key + `@apiNote` 链到官方文档)。
+
+#### Scenario: 新增表单实例
+
+- **WHEN** 调用 `YDClient_Form.saveForm(conf, formUuid, formDataJson, body_ext)`
+- **THEN** 发起 `POST /v1.0/yida/forms/instances`(或旧版 `/dingtalk/yida/processes/saveFormData`)
+- **AND** `body_ext` 所有可选参数必须透传(不删、不填默认)
+- **AND** 失败时抛 `McException` 带宜搭原始错误码
+
+#### Scenario: 更新表单实例
+
+- **WHEN** 调用 `YDClient_Form.updateForm(conf, formInstanceId, updateFormDataJson, body_ext)`
+- **THEN** 必须支持 `useLatestVersion` / `ignoreEmpty` 选项(通过 body_ext 透传)
+- **AND** 默认**不改**这两个参数的宜搭侧默认行为
+
+#### Scenario: 删除表单实例
+
+- **WHEN** 调用 `YDClient_Form.deleteForm(conf, formInstanceId, body_ext)`
+- **THEN** 单条删除走 `DELETE` 接口;批量条件删除走 `deleteFormByCondition`
+
+### Requirement: 表单查询原子接口
+
+`YDClient_Form` SHALL 提供分页、ID 列表、全量含子表三种查询粒度,查询方法 MUST 分别对应宜搭官方的不同 endpoint(不混用一个方法承担多种语义)。
+
+#### Scenario: 分页查询
+
+- **WHEN** `searchForm(conf, formUuid, searchFieldJson, currentPage, pageSize, body_ext)`
+- **THEN** 对应 `POST /v1.0/yida/forms/instances/search`
+- **AND** `pageSize` 超过 100 时抛 `McException`(宜搭侧强制上限)
+
+#### Scenario: 全量查询含子表
+
+- **WHEN** `listFormsAll(conf, formUuid, currentPage, pageSize, body_ext)`
+- **THEN** 对应 `retrieve_list_all`(含子表数据),与 `searchForm`(不含子表)行为区分清楚
+
+#### Scenario: 查询组件值
+
+- **WHEN** `listComponentValues(conf, formUuid, fieldId, body_ext)`
+- **THEN** 可获取指定字段的可选项列表
+
+### Requirement: 批量操作
+
+`YDClient_Form` SHALL 提供批量新增与批量 upsert 两个方法,MUST 遵守宜搭侧每批 ≤ 100 条的限制。
+
+#### Scenario: 批量新增
+
+- **WHEN** `batchSaveForm(conf, formUuid, formDataListJson, body_ext)`
+- **THEN** 一次调用批量创建多条记录
+- **AND** 超过 100 条时必须由调用方自行分片(或方法内自动分片,策略见 Service 层)
+
+#### Scenario: 批量 upsert
+
+- **WHEN** `batchUpsertForm(conf, formUuid, searchConditionListJson, dataListJson, body_ext)`
+- **THEN** 对应批量 upsert
+- **AND** `searchCondition` 里日期字段 MUST 用字符串数组格式(规避 `selectListException`)
+
+### Requirement: 操作日志查询
+
+`YDClient_Form` SHALL 提供表单实例的操作历史查询方法。
+
+#### Scenario: 查询实例变更历史
+
+- **WHEN** `listFormOperations(conf, formInstanceId, body_ext)`
+- **THEN** 返回该实例的全部变更历史记录(含操作人 / 操作时间 / 操作类型)

+ 75 - 0
openspec/changes/extend-yida-api-coverage/specs/yida-process-atomic/spec.md

@@ -0,0 +1,75 @@
+## ADDED Requirements
+
+### Requirement: 流程实例生命周期
+
+`YDClient_Process` SHALL 覆盖流程发起、终止、撤回、跳转四个关键动作。每个方法 MUST 对应宜搭官方的一个流程实例 endpoint。
+
+#### Scenario: 发起流程
+
+- **WHEN** `startProcess(conf, processCode, formUuid, formDataJson, body_ext)`
+- **THEN** 对应 `POST /v1.0/yida/processes/instances/start`
+- **AND** 返回 `processInstanceId`
+
+#### Scenario: 终止流程
+
+- **WHEN** `terminateProcess(conf, processInstanceId, body_ext)`
+- **AND** `body_ext` 中支持 `operator` / `noExecuteExpression` 等
+
+#### Scenario: 撤回流程
+
+- **WHEN** `revokeProcess(conf, processInstanceId, body_ext)`
+- **THEN** 发起人撤回已提交但未审批完成的流程
+
+### Requirement: 审批任务动作
+
+`YDClient_Process` SHALL 覆盖同意 / 拒绝 / 转交 / 抄送 / 评论五类审批任务动作。每个方法 MUST 与宜搭官方 task 类 endpoint 1:1 对应。
+
+#### Scenario: 同意任务
+
+- **WHEN** `agreeTask(conf, processInstanceId, taskId, comment, body_ext)`
+- **THEN** 对应 `POST /v1.0/yida/processes/tasks/agree`
+
+#### Scenario: 拒绝任务
+
+- **WHEN** `disagreeTask(conf, processInstanceId, taskId, comment, body_ext)`
+- **AND** `body_ext` 支持 `nextOperatorUserIds`(拒绝后指定下一个审批人,部分流程开启此功能)
+
+#### Scenario: 转交任务
+
+- **WHEN** `redirectTask(conf, processInstanceId, taskId, toUserId, comment, body_ext)`
+
+#### Scenario: 抄送
+
+- **WHEN** `ccTask(conf, processInstanceId, taskId, toUserIds, comment, body_ext)`
+
+#### Scenario: 添加评论
+
+- **WHEN** `commentTask(conf, processInstanceId, taskId, comment, body_ext)`
+
+### Requirement: 流程与任务查询
+
+`YDClient_Process` SHALL 提供流程详情、流程列表、任务列表三类查询能力。
+
+#### Scenario: 查单个流程详情
+
+- **WHEN** `getProcess(conf, processInstanceId, body_ext)`
+- **THEN** 返回流程全量信息含已走过的所有节点
+
+#### Scenario: 分页查流程
+
+- **WHEN** `searchProcesses(conf, appType, formUuid, processCode, searchCriteria, body_ext)`
+- **THEN** 支持按发起人 / 状态 / 时间范围过滤
+
+#### Scenario: 查任务列表
+
+- **WHEN** `searchTasks(conf, userId, statuses, body_ext)`
+- **THEN** 返回某用户当前的待办 / 已办任务
+
+### Requirement: 节点跳转
+
+`YDClient_Process` SHALL 提供流程节点跳转能力,允许运维干预流程路径。
+
+#### Scenario: 跳转到指定节点
+
+- **WHEN** `redirectProcess(conf, processInstanceId, targetActivityId, body_ext)`
+- **THEN** 流程跳转到指定节点,支持前跳与后跳

+ 86 - 0
openspec/changes/extend-yida-api-coverage/tasks.md

@@ -0,0 +1,86 @@
+## 0. 覆盖度矩阵(按官方文档对齐前置工作)
+
+> 本节在实施前必须由开发者对照**当前**宜搭开放平台文档逐条填写。已知大致清单如下,具体参数以官方文档为准。
+
+### YDClient_Form 覆盖度
+
+| 官方 endpoint | HTTP | 方法名(建议) | 已对齐 | apiNote 链接 |
+|--------------|------|---------------|-------|-------------|
+| 新增表单实例 | POST | `saveForm` | ❌ | `/document/orgapp/add-or-update-form-instances` |
+| 更新表单实例 | PUT | `updateForm` | ❌ | 待补 |
+| 修改指定字段值 | PUT | `updateFormComponents` | ❌ | 待补 |
+| 删除表单实例 | DELETE | `deleteForm` | ❌ | 待补 |
+| 按条件删除 | POST | `deleteFormByCondition` | ❌ | 待补 |
+| 查单条 | GET | `getForm` | ❌ | 待补 |
+| 分页查询 | POST | `searchForm` | ❌ | `/document/orgapp/retrieve-search-form-information` |
+| 全量查询含子表 | POST | `listFormsAll` | ❌ | `/document/orgapp/retrieve-list-all` |
+| 查询 ID 列表 | POST | `listFormIds` | ❌ | 待补 |
+| 组件值查询 | POST | `listComponentValues` | ❌ | 待补 |
+| 批量新增 | POST | `batchSaveForm` | ❌ | 待补 |
+| 批量 upsert | POST | `batchUpsertForm` | ❌ | 待补 |
+| 查操作日志 | POST | `listFormOperations` | ❌ | 待补 |
+| 附件临时 URL | POST | `convertTempUrl` | ✅(旧 `convertTemporaryUrl`)| 迁移 |
+
+### YDClient_Process 覆盖度
+
+| 官方 endpoint | HTTP | 方法名 | 已对齐 | apiNote |
+|--------------|------|-------|-------|---------|
+| 发起流程 | POST | `startProcess` | ❌ | `/document/orgapp/initiate-a-process` |
+| 同意任务 | POST | `agreeTask` | ❌ | 待补 |
+| 拒绝任务 | POST | `disagreeTask` | ❌ | 待补 |
+| 转交任务 | POST | `redirectTask` | ❌ | 待补 |
+| 抄送 | POST | `ccTask` | ❌ | 待补 |
+| 评论任务 | POST | `commentTask` | ❌ | 待补 |
+| 终止流程 | POST | `terminateProcess` | ❌ | 待补 |
+| 撤回流程 | POST | `revokeProcess` | ❌ | 待补 |
+| 跳转节点 | POST | `redirectProcess` | ❌ | 待补 |
+| 查流程详情 | GET | `getProcess` | ❌ | 待补 |
+| 分页查流程 | POST | `searchProcesses` | ❌ | 待补 |
+| 查任务列表 | POST | `searchTasks` | ❌ | 待补 |
+
+## 1. 前置准备
+
+- [ ] 1.1 对照宜搭开放平台**当前版本**文档更新上述覆盖度矩阵,补齐 apiNote 链接
+- [ ] 1.2 在 `com.malk.server.aliwork` 下建 `YDConf` 封装 `appType + systemToken + userId`(若已有则复用)
+- [ ] 1.3 审视现有 `YDParam` 建造者,识别可沿用字段避免重复
+
+## 2. YDClient_Form 接口
+
+- [ ] 2.1 新建 `mjava/src/main/java/com/malk/service/aliwork/YDClient_Form.java` 接口
+- [ ] 2.2 按覆盖度矩阵声明全部 14 个方法,signature 严格按 §3.4.2 规则
+- [ ] 2.3 每个方法 javadoc 写全 `@apiNote` + `body_ext` 字段枚举
+
+## 3. YDClient_Form 实现
+
+- [ ] 3.1 新建 `impl/YDClient_FormImpl.java`
+- [ ] 3.2 每个方法通过 `UtilHttp.doPost/doGet/doPut/doDelete` 实现
+- [ ] 3.3 返回值统一 `Map` 或 `List<Map>`(特殊 DTO 情况另议)
+- [ ] 3.4 错误处理:宜搭侧 `code != 0` 抛 `McException`
+
+## 4. YDClient_Process 接口与实现
+
+- [ ] 4.1 新建 `YDClient_Process.java` 接口
+- [ ] 4.2 按覆盖度矩阵声明全部 12 个方法
+- [ ] 4.3 新建 `impl/YDClient_ProcessImpl.java`
+
+## 5. YDService 切换
+
+- [ ] 5.1 `YDService` 公开签名不变,内部将 `operateData` / `queryData` 分支调用改为调原子方法
+- [ ] 5.2 保留旧 `YDClient.operateData` / `queryData` 原样
+- [ ] 5.3 回归测试:mcli / shunfeng / guangming 三个客户不回归
+
+## 6. 文档与审计
+
+- [ ] 6.1 审计日志(`UtilHttp` §3.5)在新原子方法生效时验证字段齐全
+- [ ] 6.2 更新 `/Users/malk/Desktop/Tech/claude/后端/yida-serverside.md`,把"YDClient 用法"替换为新原子方法示例
+
+## 7. 验证
+
+- [ ] 7.1 单元测试覆盖所有 26 个方法的参数透传(mock `UtilHttp` 校验 body 含所有传入字段)
+- [ ] 7.2 集成测试:至少对 3 个关键 endpoint(saveForm / startProcess / agreeTask)跑真实宜搭调用
+- [ ] 7.3 `openspec validate extend-yida-api-coverage --strict` 通过
+
+## 8. 交付
+
+- [ ] 8.1 PR 附覆盖度矩阵对照截图
+- [ ] 8.2 走 `/opsx:archive` 归档

+ 22 - 0
openspec/specs/crypto-utils/spec.md

@@ -0,0 +1,22 @@
+# crypto-utils Specification
+
+## Purpose
+TBD - created by archiving change extract-dingtalk-standard-api. Update Purpose after archive.
+## Requirements
+### Requirement: RSACrypt 下沉到基座
+
+系统 SHALL 把现有 `com.malk.guangming.util.RSACrypt` 完整复制到基座 `com.malk.util.crypto.RSACrypt`,保持方法签名与行为完全一致(`encrypt(str, publicKey)` + `toUrlSafe(base64)`)。本次**不新增**任何方法。
+
+#### Scenario: 基座新位置可用
+- **WHEN** 任一业务模块 import `com.malk.util.crypto.RSACrypt`
+- **THEN** 调用 `encrypt`、`toUrlSafe` 结果与旧 `com.malk.guangming.util.RSACrypt` 逐字节一致
+
+### Requirement: 旧位置保留 Deprecated 壳
+
+系统 SHALL 在 `com.malk.guangming.util.RSACrypt` 保留类名与方法签名,内部委托到新类并加 `@Deprecated`,避免破坏已编译的业务代码。
+
+#### Scenario: 旧代码继续编译运行
+- **WHEN** 业务代码仍写 `com.malk.guangming.util.RSACrypt.encrypt(...)`
+- **THEN** 编译通过;运行结果与调用新类完全一致
+- **AND** 首次调用打印一次 WARN 日志引导迁移(可选,不阻塞功能)
+