Przeglądaj źródła

docs(openspec): 立项 client/Service 分层规则与钉钉 impl 改名两个 change

standardize-client-service-layering: 固化 R1~R7 七条分层规则
作为新 capability spec(原子接口分层 / 板块拆 client / 调用优先级 /
变更确认 / 命名一致 / server 层定位 / Service 准入);R4 跨仓 grep
范围明确为基础建设期默认仅本仓,客户接入后扩。同步覆盖 O3 ALYConf /
O4 INTP 范式约定 / O6 规则进 baseline 三个落地优化的 tasks。

rename-dingtalk-impl-suffix: R5 命名一致对钉钉 14 个存量
DDImplClient_X 中缀文件改名为 DDClient_XImpl 后缀的占位提案,
分 4 批落地,排期门控等 standardize change archive 后开始。
malk 1 tydzień temu
rodzic
commit
f69d90ed04

+ 84 - 0
openspec/changes/rename-dingtalk-impl-suffix/design.md

@@ -0,0 +1,84 @@
+# Design
+
+## 关键决策
+
+### 1. 为什么分批 4 批
+
+14 文件一把改的成本:
+
+- grep 引用清单 ≥ 50 行(钉钉是基座最重的接入),用户审阅压力大
+- 一次 PR 影响所有钉钉调用方,回滚必须全量
+- 任一引用方漏改 → 编译爆炸 → 整个仓库阻塞
+
+分批 4 批的好处:
+
+- 每批 3~4 文件,grep 清单 ≤ 15 行
+- 每批独立 commit,回滚成本 = 1 批
+- 用户可在批次间叫停("批次 1 出问题,先观察一周再继续")
+- 失败半径限定在该批关联调用方
+
+### 2. 批次顺序为什么这样排
+
+| 批次 | 文件 | 排序理由 |
+|---|---|---|
+| 1 | `DDImplClient` / `DDImplService` / `DDImplClient_Attendance` | 主流量入口,先打通主路径验证迁移可行性 |
+| 2 | `DDImplClient_Contacts` / `_Group` / `_Notice` | 通讯通知,高频但相对独立 |
+| 3 | `DDImplClient_Personnel` / `_Report` / `_Schedule` | 人事日历,与考勤强关联 |
+| 4 | `DDImplClient_Dedicated` / `_Event` / `_Extension` / `_Storage` / `_Workflow` | 长尾,引用方少,最后清扫 |
+
+### 3. Spring Bean 名 by-name 兼容性
+
+Spring 默认 Bean 名是类名首字母小写。改名后:
+
+| 旧 Bean 名 | 新 Bean 名 |
+|---|---|
+| `dDImplClient` | `dDClientImpl` |
+| `dDImplClient_Attendance` | `dDClient_AttendanceImpl` |
+| ... | ... |
+
+调用方影响:
+- `@Autowired DDClient ddClient`(by 接口类型)— **不受影响**,Spring 仍能找到唯一实现
+- `@Autowired @Qualifier("dDImplClient") DDClient client`(by-name)— **受影响**,必须改
+- `@Resource(name = "dDImplClient_Attendance")` — **受影响**,必须改
+
+每批 R4 grep 时必须加扫 `@Qualifier` `@Resource(name=` 字面量。
+
+### 4. 没用 `@Component("dDImplClient_Attendance")` 别名兼容
+
+可以给新类标 `@Component("dDImplClient_Attendance")` 保留旧 Bean 名,让调用方零修改。**拒绝**,原因:
+
+- 旧 Bean 名继续存在会让代码长期维持「类名后缀 + Bean 名中缀」的语义裂缝,违反 R5 精神
+- 别名是临时栈,迟早要拆 → 拆别名时还得做一遍引用扫,反复做 2 次
+- 一次性切换的总成本 ≤ 双轨过渡
+
+### 5. 跨仓 grep 范围
+
+- mjava-ai:必扫
+- 光明独立仓 `/Users/malk/server/cur/mjava-guangming/`:含 mjava-guangming/mjava-shunfeng 模块 — 必扫
+- akds 项目:路径在用户 memory 中(独立维护),ACK 时由用户确认是否扫
+- 其他客户独立仓:用户 ACK 时主动补充清单
+
+grep 命令模板(每批执行):
+
+```bash
+# 该批每个旧类名扫:
+grep -rE "(import\s+.+\.DDImplClient_Attendance|DDImplClient_Attendance\.class|@Qualifier\(\"dDImplClient_Attendance\"|@Resource\(name\s*=\s*\"dDImplClient_Attendance)" \
+  --include="*.java" \
+  /Users/malk/server/cur/mjava-ai/ \
+  /Users/malk/server/cur/mjava-guangming/ \
+  {其他客户仓}
+```
+
+### 6. 验证流程(每批)
+
+1. 改名 + import 替换
+2. `mvn -pl mjava -am clean compile`(基座)
+3. `mvn -pl mjava-mcli,mjava-pro,mjava-com -am clean compile`(子项目)
+4. 跨仓客户项目验证(用户在 ACK 时指定)
+5. 抽样运行:钉钉考勤/通讯录最常用接口的集成测试
+
+## 拒绝的方案
+
+- **一次 commit 14 文件** — 拒绝。爆炸半径过大
+- **同时改 `DDClient` 接口名** — 拒绝。R5 只约束 Impl 命名;接口名变化触发跨仓所有 import 变化,超出本 change 范围
+- **保留旧 Bean 名作为别名** — 拒绝(见 §4)

+ 73 - 0
openspec/changes/rename-dingtalk-impl-suffix/proposal.md

@@ -0,0 +1,73 @@
+> 状态(2026-06-10 立项):**占位提案,未排期**
+> 优先级:中。依赖 `standardize-client-service-layering` 先 archive 入 R5;本 change 是 R5 对存量钉钉 14 文件的整改。
+
+## Why
+
+R5(命名一致,`Impl` 后缀)落地后,钉钉模块仍有 14 个存量中缀文件违反规则:
+
+```
+service/dingtalk/impl/DDImplClient.java               → DDClientImpl.java
+service/dingtalk/impl/DDImplService.java              → DDServiceImpl.java
+service/dingtalk/impl/DDImplClient_Attendance.java    → DDClient_AttendanceImpl.java
+service/dingtalk/impl/DDImplClient_Contacts.java      → DDClient_ContactsImpl.java
+service/dingtalk/impl/DDImplClient_Dedicated.java     → DDClient_DedicatedImpl.java
+service/dingtalk/impl/DDImplClient_Event.java         → DDClient_EventImpl.java
+service/dingtalk/impl/DDImplClient_Extension.java     → DDClient_ExtensionImpl.java
+service/dingtalk/impl/DDImplClient_Group.java         → DDClient_GroupImpl.java
+service/dingtalk/impl/DDImplClient_Notice.java        → DDClient_NoticeImpl.java
+service/dingtalk/impl/DDImplClient_Personnel.java     → DDClient_PersonnelImpl.java
+service/dingtalk/impl/DDImplClient_Report.java        → DDClient_ReportImpl.java
+service/dingtalk/impl/DDImplClient_Schedule.java      → DDClient_ScheduleImpl.java
+service/dingtalk/impl/DDImplClient_Storage.java       → DDClient_StorageImpl.java
+service/dingtalk/impl/DDImplClient_Workflow.java      → DDClient_WorkflowImpl.java
+```
+
+新接入子 client 时强行避开(R5 强制后缀)会让钉钉模块产生混搭风格,更难看,必须整改。
+
+但本 change 拆出 `standardize-client-service-layering` 独立处理,原因:
+
+- 14 文件改名 + Bean 名变化 + 跨仓 `@Qualifier` / `@Autowired by-name` 字面引用扫,爆炸半径大
+- R4 要求改前 grep 报告 + 用户 ACK;本 change 需要分批扫描
+- 失败回滚成本:一个文件改坏可能拖累钉钉栈所有调用方
+
+## What Changes
+
+按 R5 把 14 个中缀文件改为后缀风格。Spring DI by 接口的调用方不受影响(DDClient/DDClient_X 接口名不变);by-name 注入(如 `@Qualifier("dDImplClient_Attendance")`)需逐一找出替换。
+
+执行策略:**分批,每批 3~4 文件**,每批一个独立 commit:
+
+1. 批次 1:3 个 `DDImplClient` + `DDImplService` + `DDImplClient_Attendance`(主流量入口)
+2. 批次 2:通讯/通知 — `DDImplClient_Contacts` / `DDImplClient_Group` / `DDImplClient_Notice`
+3. 批次 3:考勤/人事 — `DDImplClient_Personnel` / `DDImplClient_Report` / `DDImplClient_Schedule`
+4. 批次 4:扩展 — `DDImplClient_Dedicated` / `DDImplClient_Event` / `DDImplClient_Extension` / `DDImplClient_Storage` / `DDImplClient_Workflow`
+
+每批落地前按 R4 流程跨仓 grep + 用户 ACK。
+
+## Capabilities
+
+### Modified Capabilities
+
+- `client-service-layering`:R5 从「新增代码强制」演进为「全仓覆盖」(钉钉存量符合后)
+
+### New Capabilities
+
+<!-- 无新能力,纯重构 -->
+
+## Impact
+
+- **重命名**:14 个 .java 文件
+- **改 import**:所有引用这 14 个具体 impl 类的代码(预计绝大多数是 interface 引用,少数 by-name)
+- **Spring Bean 名变化**:默认 Bean 名首字母小写,`dDImplClient_Attendance` → `dDClient_AttendanceImpl`;by-name 调用方需同步改
+- **跨仓影响**:
+  - mjava-ai 仓:自扫
+  - akds 项目:路径待用户确认;R4 grep
+  - 光明独立仓 `/Users/malk/server/cur/mjava-guangming/`:含 mjava-guangming/mjava-shunfeng 模块,可能引用
+  - 其他客户独立仓:用户在 ACK 时主动补
+- **运行时风险**:纯重命名,无逻辑变更;编译期能捕获所有问题
+
+## Non-Goals
+
+- ❌ 不改 `DDClient` / `DDClient_X` 接口名(仅改 Impl)
+- ❌ 不改方法签名(R4 触发的实质性变更不在本 change 范围)
+- ❌ 不顺手改其他模块(aliwork/beisen 已经是后缀风格,本 change 只动钉钉)
+- ❌ 不一次 commit 14 文件(必须分批,每批可独立回滚)

+ 44 - 0
openspec/changes/rename-dingtalk-impl-suffix/specs/client-service-layering/spec.md

@@ -0,0 +1,44 @@
+## MODIFIED Requirements
+
+### Requirement: R5 命名一致(Impl 后缀)
+
+> archive 后并入 `specs/client-service-layering/spec.md` 的 R5 段
+
+R5 演进:从「**新增代码强制后缀,存量钉钉中缀为待整改技术债**」演进为「**全仓所有实现类一律 `XxxImpl` 后缀**」。
+
+钉钉模块 14 个存量中缀文件已按本 change 分 4 批整改为后缀风格:
+
+| 旧 | 新 |
+|---|---|
+| `DDImplClient` | `DDClientImpl` |
+| `DDImplService` | `DDServiceImpl` |
+| `DDImplClient_Attendance` | `DDClient_AttendanceImpl` |
+| `DDImplClient_Contacts` | `DDClient_ContactsImpl` |
+| `DDImplClient_Dedicated` | `DDClient_DedicatedImpl` |
+| `DDImplClient_Event` | `DDClient_EventImpl` |
+| `DDImplClient_Extension` | `DDClient_ExtensionImpl` |
+| `DDImplClient_Group` | `DDClient_GroupImpl` |
+| `DDImplClient_Notice` | `DDClient_NoticeImpl` |
+| `DDImplClient_Personnel` | `DDClient_PersonnelImpl` |
+| `DDImplClient_Report` | `DDClient_ReportImpl` |
+| `DDImplClient_Schedule` | `DDClient_ScheduleImpl` |
+| `DDImplClient_Storage` | `DDClient_StorageImpl` |
+| `DDImplClient_Workflow` | `DDClient_WorkflowImpl` |
+
+R5 全文(更新后):
+
+所有实现类命名风格统一为 `XxxImpl` **后缀**:
+
+- 正:`DDClientImpl` / `DDClient_AttendanceImpl` / `DDServiceImpl` / `YDClient_FormImpl` / `BSServiceImpl`
+- 负:`DDImplClient` / `DDImplClient_Attendance` / `DDImplService`(中缀风格,全仓禁止)
+
+#### Scenario: 新增实现类(同 archive 前)
+
+- **WHEN** 给某 Client 接口新增实现
+- **THEN** 文件名必须用 `XxxImpl` 后缀,禁用 `XxxImplXxx` 中缀
+
+#### Scenario: 钉钉新增子 client 实现(更新)
+
+- **WHEN** 给 `service/dingtalk/impl/` 新增子 client 实现
+- **THEN** 必须用 `DDClient_XImpl` 后缀
+- **AND** 周围所有存量文件均已是后缀风格,无回退诱因

+ 52 - 0
openspec/changes/rename-dingtalk-impl-suffix/tasks.md

@@ -0,0 +1,52 @@
+## 0. 排期门控
+
+- [ ] 0.1 等 `standardize-client-service-layering` archive 入 specs/(R5 入规则)
+- [ ] 0.2 用户排期确认开始本 change
+
+## 1. 批次 1:主流量入口(3 文件)
+
+- [ ] 1.1 R4 grep 跨仓引用扫描(mjava-ai + 光明独立仓 + 用户指定的其他客户仓):
+  - `DDImplClient`(类名 / import / `@Qualifier` / `@Resource(name=)`)
+  - `DDImplService`
+  - `DDImplClient_Attendance`
+- [ ] 1.2 引用清单提交给用户,等 ACK
+- [ ] 1.3 改名:`DDImplClient.java` → `DDClientImpl.java`,class 声明同步
+- [ ] 1.4 改名:`DDImplService.java` → `DDServiceImpl.java`,class 声明同步
+- [ ] 1.5 改名:`DDImplClient_Attendance.java` → `DDClient_AttendanceImpl.java`,class 声明同步
+- [ ] 1.6 全仓改 import + `@Qualifier` + `@Resource(name=)` 引用
+- [ ] 1.7 `mvn -pl mjava -am clean compile` 通过
+- [ ] 1.8 `mvn -pl mjava-mcli,mjava-pro,mjava-com -am clean compile` 通过
+- [ ] 1.9 跨仓客户项目编译通过(用户指定)
+- [ ] 1.10 commit:`refactor(service/dingtalk): rename DDImpl* → DDImpl suffix [batch 1/4]`
+- [ ] 1.11 观察 1 周(或用户确认)→ 进入批次 2
+
+## 2. 批次 2:通讯通知(3 文件)
+
+- [ ] 2.1 R4 grep(Contacts / Group / Notice)
+- [ ] 2.2 ACK
+- [ ] 2.3 三个改名 + import 替换
+- [ ] 2.4 编译 + 跨仓验证
+- [ ] 2.5 commit `[batch 2/4]`
+
+## 3. 批次 3:人事日历(3 文件)
+
+- [ ] 3.1 R4 grep(Personnel / Report / Schedule)
+- [ ] 3.2 ACK
+- [ ] 3.3 三个改名 + import 替换
+- [ ] 3.4 编译 + 跨仓验证
+- [ ] 3.5 commit `[batch 3/4]`
+
+## 4. 批次 4:长尾(5 文件)
+
+- [ ] 4.1 R4 grep(Dedicated / Event / Extension / Storage / Workflow)
+- [ ] 4.2 ACK
+- [ ] 4.3 五个改名 + import 替换
+- [ ] 4.4 编译 + 跨仓验证
+- [ ] 4.5 commit `[batch 4/4]`
+
+## 5. 验证 & archive
+
+- [ ] 5.1 `grep -rE "DDImpl(Client|Service)" --include=\"*.java\"` 全仓零命中
+- [ ] 5.2 `/opsx:validate rename-dingtalk-impl-suffix --strict` 通过
+- [ ] 5.3 archive 到 `openspec/changes/archive/2026-XX-XX-rename-dingtalk-impl-suffix/`
+- [ ] 5.4 R5 在 specs/client-service-layering/spec.md 演进为「全仓覆盖」(删除"钉钉存量待整改"备注)

+ 91 - 0
openspec/changes/standardize-client-service-layering/design.md

@@ -0,0 +1,91 @@
+# Design
+
+## 关键决策
+
+### 1. 为什么用 capability spec 而不是只改 baseline
+
+`project-baseline.md` 现状是「指路 + 锚点 + 子项目清单」,纯结构指引。R1~R7 是**代码组织约束**(行为规则),属于 capability 层面。新建 `client-service-layering` capability spec 后:
+
+- baseline 只加一行锚点指向新 spec
+- 权威正文进共享 `mjava-baseline.md`(跨仓库引用)
+- 仓库内 spec 用 `## ADDED Requirements` 段落对齐 OpenSpec 模式
+
+这样 baseline 保持「薄」,规则改动走 capability spec 的 propose/archive 流程,演进路径清晰。
+
+### 2. R5 命名:为什么选 `Impl` 后缀
+
+仓内现状:
+- 后缀:宜搭 3 文件(YDClient_FormImpl/YDClient_ProcessImpl/YDClientImpl/YDServiceImpl)
+- 后缀:北森 4 文件(BSClient_AttendanceImpl/BSClient_EmployeeImpl/BSClientImpl/BSServiceImpl)
+- 后缀:其他 6 个产品所有 impl(EKB/FXK/CY/VK/XBB/TB/INTP/ALY 共 ~10 文件)
+- 中缀:钉钉 14 文件(DDImplClient + DDImplClient_X×12 + DDImplService)
+
+**14 文件中缀 vs ~17 文件后缀** —— 后缀是事实多数派。R5 选后缀,钉钉作为少数派改名,影响面已知(钉钉 14 文件 + `@Qualifier` 字面引用)。
+
+### 3. R7 准入条件:为什么是「≥2 个客户子项目复用」
+
+mjava 基座的价值是「**多客户复用的二次封装**」。单客户编排放基座会污染共享层:
+
+- 客户 A 的特殊错误码翻译进 `XxxService` → 客户 B 调用时被强加上
+- 客户 A 的特定字段映射进 Service → 客户 B 的字段被误转
+
+阈值定 2 是最低门槛:**第 2 个复用方出现 = 抽象有市场**。第 1 个复用方需要时,先在自己客户子项目里写编排;第 2 个想用时再上提到基座 Service。
+
+> 实操:第 1 个复用方写代码时,可以在子项目放在 `service/{product}/` 下用同样命名,后续上提时一份代码两边引用。
+
+### 4. R4 变更确认:grep 范围
+
+**当前阶段(基础建设期)**:mjava-ai 是纯基础建设项目,无实际客户子项目依赖本仓基座 jar,grep 范围 = **mjava-ai 本仓**。
+
+**客户接入后**:当某客户独立仓(如未来 akds / 光明独立仓 / 新客户)开始依赖 mjava-ai 发布的基座 jar 时,由用户在 ACK 流程中显式补充该仓库路径,扫描范围随之扩展。
+
+> 历史背景:光明独立仓 `/Users/malk/server/cur/mjava-guangming/` 自维护了一份 mjava 基座的分叉副本(独立 git 仓),不依赖 mjava-ai;akds 同样独立仓。这些客户仓的 R4 grep 由各自仓内自行执行,不与 mjava-ai 的 R4 流程联动。
+
+grep 模板:
+```bash
+# 接口签名
+grep -rE "\.{methodName}\(" --include="*.java" {repo}
+
+# 类名引用(迁移/重命名)
+grep -rE "(import|extends|implements)\s+.*\.{ClassName}" --include="*.java" {repo}
+```
+
+报告格式:
+```
+方法/类: com.malk.service.dingtalk.DDClient.foo
+mjava-ai: 3 处
+  - mjava-mcli/src/.../Boot.java:42
+  - mjava-pro/src/.../Xxx.java:88
+  - mjava/src/.../impl/DDImplClient.java:156(自定义,不计)
+akds: 1 处
+  - apps/web/src/.../Yyy.java:33
+光明独立仓: 0 处
+```
+
+### 5. O3 ALYConf 内容
+
+`ALYInvoiceImpl` 内 4 个 URL 字面量:
+
+```
+https://fapiao.market.alicloudapi.com/v2/invoice/pdf      → URL_INVOICE_PDF
+https://fapiao.market.alicloudapi.com/v2/invoice/query    → URL_INVOICE_QUERY
+https://fapiao.market.alicloudapi.com/v2/invoice/qrcode   → URL_INVOICE_QRCODE
+https://invoice.market.alicloudapi.com/v2/invoice/ocr     → URL_INVOICE_OCR
+```
+
+`ALYConf` 不用 `@ConfigurationProperties`(appcode 是调用方传参,不进 yml),只放 `public static final String` 常量。
+
+### 6. 为什么 O1 钉钉改名拆独立 change
+
+- 文件改动量大(14 文件 + Bean 名 + Qualifier 字面引用扫)
+- 跨仓引用扫范围至少 4 个仓库(mjava-ai/akds/光明/其他客户)
+- 失败回滚成本:1 个文件改坏可能拖累整个钉钉栈
+
+独立 change `rename-dingtalk-impl-suffix` 让改名风险隔离,可按文件批次推进。
+
+## 拒绝的方案
+
+- **直接进 mjava-baseline.md 不建仓库 spec** — 拒绝。OpenSpec 是仓内单一事实源,跨仓库共享文档需仓内 spec 作锚点
+- **R7 阈值定 1**(只要有 1 个复用就上提)— 拒绝。会鼓励过早抽象
+- **R5 选中缀** — 拒绝。事实多数派是后缀
+- **同时改名钉钉 12 文件** — 拒绝。爆炸半径过大,单独 change 隔离

+ 60 - 0
openspec/changes/standardize-client-service-layering/proposal.md

@@ -0,0 +1,60 @@
+> 状态(2026-06-10 立项):**提案阶段,规则文档 + O3/O4/O6 待 apply,O2 待 ACK**
+> 优先级:中。固化 7 条分层规则,配套 4 个轻量优化(O2/O3/O4/O6);O1(钉钉 impl 改名)拆到独立 change `rename-dingtalk-impl-suffix`。
+
+## Why
+
+`mjava` 基座现有 11 个产品接入板块(dingtalk / aliwork / beisen / aliyun / ekuaibao / fxiaoke / h3yun / integration / teambition / vika / xbongbong),整体已按产品分目录,但有 4 个不一致:
+
+1. **子 client 拆分粒度不均**:钉钉拆 12 子 client(按板块),宜搭/北森仅 2 子;EKB/FXK/CY/VK/XBB 单 Client 文件未拆 — 缺规则约定
+2. **`Impl` 命名风格分裂**:钉钉用中缀 `DDImplClient_X`,宜搭/北森用后缀 `XxxImpl` — 新人困惑
+3. **`util/` 与 `utils/` 并行**:2026-04-18 的 crypto 抽取留下中间态,`util/crypto/RSACrypt.java` 单独平行于 `utils/` 复数目录
+4. **`server/aliyun/` 缺 `ALYConf`**:`ALYInvoiceImpl` 内 4 个 URL 硬编码 — 与其他产品 server/ 结构不对齐
+5. **Service 层覆盖准入无规则**:5 个产品没 Service 层只暴露 Client,是否要补、什么时候补,没标准 → 容易写空壳
+
+更核心的痛点:**已有 Client/Service 接口签名改动时,跨仓库(mjava-ai / akds / 光明独立仓 / 其他客户仓)引用关系不可见**,改了下游一片报错。需要把"改前先扫引用 + 等用户 ACK"沉淀为强制规则。
+
+## What Changes
+
+新增 1 个 capability spec `client-service-layering`,固化 7 条规则:
+
+- **R1 原子接口分层**:`server/{product}/` 放产品方数据契约(POJO/Conf/枚举),`service/{product}/{Prod}Client*.java` 1:1 对应产品官方接口,`{Prod}Service.java` 做通用业务编排
+- **R2 板块拆 client**:单 Client > ~15 方法 或 跨 2+ 产品板块时,按板块拆 `{Prod}Client_{Module}.java`
+- **R3 调用优先级**:子项目调产品方接口必须先查 mjava `service/{product}/`;没有 → 按 R1/R2 新增;禁子项目自建 HTTP 直调
+- **R4 变更确认(核心)**:改 / 删 / 重命名任何 `*Client*.java` 或 `*Service*.java` 的**接口签名**前,必须 grep 全仓引用清单 → 报告 → 等用户 ACK;仅扩展(新方法/新子 client)不需要确认
+- **R5 命名一致**:所有实现类用 `XxxImpl` **后缀**风格(统一全仓,钉钉现有 12 个 `DDImplClient_X` 中缀产物拆到独立 change `rename-dingtalk-impl-suffix` 推进)
+- **R6 `server/` 层定位**:只放数据契约(POJO/Conf/枚举/常量),禁 `@Service` / `@Component`,禁注入,禁调 HTTP
+- **R7 Service 层准入**:仅当 ≥2 个客户子项目复用同一段二次编排时才建 `XxxService`;单客户编排留在客户子项目
+
+同步做 4 个轻量优化:
+
+- **O3** 补 `mjava/server/aliyun/ALYConf.java`,抽 `ALYInvoiceImpl` 内 4 个硬编码 URL 为常量(纯新增 + 一次性 import 替换,仅本仓引用,影响小)
+- **O4** INTP(集成平台)范式约定**仅写入文档**:未来扩多域时按 R1/R2 补 `INTPClient` 主入口与 `INTPService`(不做空壳预建 — 服从 R7)
+- **O6** 7 条规则进**权威**共享文档 `/Users/malk/Desktop/Tech/claude/后端/mjava-baseline.md`;仓库内 `openspec/specs/project-baseline.md` 加锚点
+- **O2** `util/crypto/RSACrypt.java` 迁到 `utils/crypto/RSACrypt.java`,删 `util/` 目录 — 按 R4 流程:本 change 仅做 grep 报告,**等用户 ACK 后再独立动**
+
+## Capabilities
+
+### New Capabilities
+
+- `client-service-layering`:mjava 仓 server/ + service/ 两层组织规则与变更流程
+
+### Modified Capabilities
+
+<!-- project-baseline.md 加锚点,不改语义 -->
+
+## Impact
+
+- **新增 spec**:`openspec/specs/client-service-layering/`(archive 后)
+- **新增代码**:`mjava/src/main/java/com/malk/server/aliyun/ALYConf.java`(一个常量类,~20 行)
+- **修改代码**:`mjava/src/main/java/com/malk/service/aliyun/impl/ALYInvoiceImpl.java` 4 处 URL 字面量改为 `ALYConf.URL_*` 引用 — 触发 R4 但作者即审核(本仓 + 本 change 范围内),grep 确认无下游 import URL 字面量后落地
+- **新增文档**:`/Users/malk/Desktop/Tech/claude/后端/mjava-baseline.md` 新章节"client/Service 分层规则"
+- **修改文档**:`openspec/specs/project-baseline.md` 加锚点;`README.md` 子项目速览表更新(如需)
+- **现有 Client/Service 代码**:零修改(除 ALYInvoiceImpl 4 行 URL 替换)
+- **客户子项目(mcli/pro/com 及独立仓 akds/光明)**:零影响
+
+## Non-Goals
+
+- ❌ 不改名钉钉 12 个 `DDImplClient_X`(拆到独立 change `rename-dingtalk-impl-suffix`,R5 仅约束新增代码)
+- ❌ 不立即迁 `util/crypto/RSACrypt.java`(O2 本 change 只 grep 报告,等 ACK 单独执行)
+- ❌ 不补 5 个产品(EKB/FXK/CY/VK/XBB)的 Service 层(R7 准入未达成)
+- ❌ 不强制现有 Client 单文件超过 15 方法立即拆分(R2 只约束新增;现有大文件作为技术债等下次触碰时顺手拆)

+ 196 - 0
openspec/changes/standardize-client-service-layering/specs/client-service-layering/spec.md

@@ -0,0 +1,196 @@
+## ADDED Requirements
+
+### Requirement: R1 原子接口分层(server / service 两层)
+
+mjava 基座对每个产品方接入按两层组织:
+
+- **`com.malk.server.{product}/`**:产品方原始数据契约。允许放 POJO(如 `*R` 响应、`*Param` 入参、`*Dto` 字段映射)、`{Prod}Conf` 配置类、枚举、常量、签名/加密工具类。禁业务逻辑。
+- **`com.malk.service.{product}/{Prod}Client*.java`**:1:1 对应产品官方接口文档,**一接口一方法**,不做组合,不做错误码翻译,仅做参数填充 + HTTP 调用 + 反序列化。
+- **`com.malk.service.{product}/{Prod}Service.java`**:可选。仅当存在跨客户共用的二次封装(自动续 token、分页拼装、错误码翻译、跨接口编排)时建立。准入条件见 R7。
+
+实现类一律落在 `com.malk.service.{product}.impl/` 子包。
+
+#### Scenario: 新增一个产品方接入
+
+- **WHEN** 需要在 mjava 基座新增对某产品方(如 "腾讯会议")的接入
+- **THEN** 必须建 `server/txmeeting/` 与 `service/txmeeting/` 两个目录
+- **AND** `server/txmeeting/` 下至少有 `TXMConf` 或 `TXMR` 至少一个数据契约类
+- **AND** `service/txmeeting/TXMClient.java` 接口 + `impl/TXMClientImpl.java` 实现各一个文件
+- **AND** 如无跨客户复用场景,**禁建** `TXMService.java` 空壳
+
+#### Scenario: Client 方法定义
+
+- **WHEN** 实现 `{Prod}Client*.java` 接口的方法
+- **THEN** 方法签名应与产品方官方接口文档 1:1 对应(每个 OpenAPI 一个 Java 方法)
+- **AND** 方法体只做:构造 headers / params / body → 调 `UtilHttp` → 反序列化 → 返回;**不调其他 Client 方法、不写 if-else 业务分支**
+
+#### Scenario: 集成平台 INTP 当前现状与演进
+
+- **WHEN** 当前 `service/integration/` 仅 `INTPClient_User`(单一域)
+- **THEN** **不**预建空壳 `INTPClient` 主入口或 `INTPService`(服从 R7 防空壳准入)
+- **AND** 未来扩多域时(例:补 `INTPClient_Org` / `INTPClient_Permission` 等)按 R2 触发:
+  - 若总方法数 > 15 或跨 2+ 域,必须建 `INTPClient` 主入口聚合
+  - 若 ≥2 客户子项目复用相同的二次编排(如统一鉴权续约),按 R7 准入条件补 `INTPService`
+
+---
+
+### Requirement: R2 板块拆 client
+
+当单一 `{Prod}Client.java` 接口方法数 > 15,或跨 2+ 产品板块(例:钉钉的「通讯录」+「考勤」),必须按板块拆分为 `{Prod}Client_{Module}.java`。
+
+参照范式:
+- 钉钉 12 子 client:`DDClient_Attendance` / `DDClient_Contacts` / `DDClient_Group` 等
+- 宜搭 2 子 client:`YDClient_Form` / `YDClient_Process`
+- 北森 2 子 client:`BSClient_Attendance` / `BSClient_Employee`
+
+主 `{Prod}Client.java` 保留作为总入口或聚合(含 token 获取、通用工具方法),不消失。
+
+#### Scenario: Client 方法数超阈值
+
+- **WHEN** 给某 Client 增加新方法导致总数 > 15
+- **THEN** 必须在同次 PR 中拆出至少 1 个 `{Prod}Client_{Module}.java` 子文件,按业务板块归类
+- **OR** 如无法立即拆分,必须在 PR 描述里明确「**技术债**:超阈值 N 方法,跟踪 issue/change #xxx」
+
+#### Scenario: 跨板块新方法
+
+- **WHEN** 新方法所属产品板块(如「考勤」)与现有 Client 主体(如「通讯录」)不同
+- **THEN** 必须建新的 `{Prod}Client_{Module}.java` 而不是塞进现有 Client
+
+---
+
+### Requirement: R3 调用优先级(子项目复用 mjava)
+
+所有 mjava-ai 仓内子项目(mcli / pro / com / 未来新建客户模块)和**外部独立客户仓**(akds / 光明独立仓 / 其他客户仓)调产品方接口时遵循优先级:
+
+1. **优先调** `mjava.service.{product}.{Prod}Client*` / `{Prod}Service`
+2. mjava 未提供但**确属产品方原子接口** → 按 R1/R2 在 mjava 新增 Client → 再被子项目调
+3. mjava 未提供且**仅为该客户独有业务编排**(非产品方原子接口)→ 留在客户子项目本地
+
+子项目**禁止**自建 HTTP 直调(绕过 `UtilHttp`)、禁止自建产品方鉴权/Token 逻辑(绕过 `UtilToken`)、禁止引入产品方三方 SDK。
+
+#### Scenario: 子项目发现 mjava 缺接口
+
+- **WHEN** 客户子项目需要某产品方接口,mjava 当前未提供
+- **THEN** 先判断该接口是否为「产品方原子能力」
+- **AND** 是 → 起 mjava 侧 change 新增 Client → mjava 发版 → 子项目升级 mjava 版本调用
+- **AND** 否(客户独有编排)→ 在子项目本地写,不进 mjava
+
+#### Scenario: 子项目存量 HTTP 直调
+
+- **WHEN** 巡检发现客户子项目内有 `UtilHttp.doXxx` 直接调产品方域名
+- **THEN** 标记为待迁移技术债
+- **AND** 下次触碰该代码段时按 R3 上提到 mjava Client
+
+---
+
+### Requirement: R4 变更确认(核心)
+
+对 `com.malk.service.{product}.{Prod}Client*.java` 或 `{Prod}Service.java` 的**接口签名**做以下任一变更前,必须执行变更确认流程:
+
+- 删除方法
+- 改名方法
+- 改方法参数列表(增/减/换类型/换顺序)
+- 改方法返回类型
+- 删除/改名接口本身
+- 删除/改名 `server/` 层 POJO 类、字段、方法
+
+变更确认流程:
+
+1. **grep 引用扫描**:
+   - **基础建设阶段(当前默认)**:扫 mjava-ai 本仓全量
+   - **有客户子项目依赖时**:用户在 ACK 流程中显式列出需扩扫的下游仓库路径(如未来 akds / 光明独立仓 / 新客户仓)
+2. **生成引用清单**:file:line + 调用上下文
+3. **报告给用户**:影响面 + 拟改动方案 + 兼容路径(如有)
+4. **等待用户 ACK**:用户回复"OK / 同意 / 改吧"等明确肯定后方可执行
+5. **变更后回写**:commit message 注明 `BREAKING: {Class}.{method} 签名变更`,受影响调用方列表写入 commit body
+
+**仅扩展**变更(新方法 / 新重载 / 新子 client 文件 / 新 server 层 POJO)**不需要**确认,但 commit message 注明 `feat(service): 新增 {Class}.{method}`。
+
+#### Scenario: 改 Client 方法签名
+
+- **WHEN** 计划修改 `DDClient_Contacts.getUserDetail(String userid)` 改为 `getUserDetail(String userid, Boolean includeExt)`
+- **THEN** 必须先执行 grep `\.getUserDetail\(` 跨仓扫描
+- **AND** 生成引用清单提交给用户
+- **AND** 在用户 ACK 前**不修改源代码**
+- **AND** ACK 后才动手,commit message 标 `BREAKING`
+
+#### Scenario: 仅扩展(加新方法)
+
+- **WHEN** 给 `DDClient_Contacts` 新增 `listDepartmentTree()` 方法
+- **THEN** 直接实现,无需扫引用
+- **AND** commit message 写 `feat(service/dingtalk): DDClient_Contacts 新增 listDepartmentTree`
+
+---
+
+### Requirement: R5 命名一致(Impl 后缀)
+
+所有实现类命名风格统一为 `XxxImpl` **后缀**:
+
+- 正:`DDClientImpl` / `DDClient_AttendanceImpl` / `DDServiceImpl` / `YDClient_FormImpl` / `BSServiceImpl`
+- 负:`DDImplClient` / `DDImplClient_Attendance` / `DDImplService`(中缀风格,禁止新增)
+
+R5 对**新增代码**强制生效。钉钉现有 14 个中缀文件(`DDImplClient` + `DDImplClient_X×12` + `DDImplService`)作为存量技术债,拆到独立 change `rename-dingtalk-impl-suffix` 推进,本 change 不动。
+
+#### Scenario: 新增实现类
+
+- **WHEN** 给某 Client 接口新增实现
+- **THEN** 文件名必须用 `XxxImpl` 后缀,禁用 `XxxImplXxx` 中缀
+
+#### Scenario: 钉钉新增子 client 实现
+
+- **WHEN** 给 `service/dingtalk/impl/` 新增子 client 实现
+- **THEN** 即使周围 12 个文件是 `DDImplClient_X` 中缀风格,新文件必须用 `DDClient_XImpl` 后缀
+- **AND** 不为「对齐周围风格」回退 R5
+
+---
+
+### Requirement: R6 server/ 层定位(数据契约纯净性)
+
+`com.malk.server.{product}/` 包内的类必须满足:
+
+- **允许**:POJO(含 `*R` `*Param` `*Dto` `*Conf`)、枚举(`enum`)、常量类(`public static final`)、产品方签名/加密工具类(如 `DingCallbackCrypto`、`DigestUtil`)
+- **禁止**:
+  - `@Service` / `@Component` / `@Repository` / `@RestController` 等 Bean 注解
+  - `@Autowired` / `@Resource` / 构造器注入其他 Bean
+  - 直接调 `UtilHttp` / `UtilToken` 发起网络请求
+  - 含业务编排逻辑(if-else 业务分支、跨产品组合)
+
+`{Prod}Conf` 是 `@ConfigurationProperties` 的允许例外(标了 `@Component` 注解但仅为配置绑定,不参与业务)。
+
+#### Scenario: 误把业务类放进 server/
+
+- **WHEN** 代码评审发现 `server/dingtalk/` 下出现 `@Service` 注解的类
+- **THEN** 必须移到 `service/dingtalk/impl/` 或客户子项目对应位置
+
+#### Scenario: server/ 类的依赖
+
+- **WHEN** 写 `server/{product}/` 下的 POJO 类
+- **THEN** import 范围限定在:JDK / Lombok / fastjson / `com.malk.server.common.*` / 同包内类
+- **AND** 不得 import `com.malk.service.*` / `com.malk.utils.*`(除常量工具如 `UtilMap` 静态方法)
+
+---
+
+### Requirement: R7 Service 层准入(防空壳)
+
+`{Prod}Service.java` 仅当满足以下任一条件时才建:
+
+- 当前有 ≥2 个客户子项目(包括 mjava-ai 内 mcli/pro/com 与外部独立客户仓)使用相同二次编排逻辑
+- 该编排涉及非业务的通用横切(自动续 token、统一分页拼装、产品方错误码 → `McException` 翻译)
+- 该编排为新接入产品的「试水期占位」,提案中明确承诺 3 个月内补复用方,否则归零
+
+不满足上述条件时,二次编排留在客户子项目内 `com.malk.{customer}.service.{product}.{Prod}LocalService.java`,命名带 `Local` 前缀以示区分。
+
+未满足条件强行预建 Service 文件(空 interface / 仅一个 default 方法 / 仅一个未被任何客户调用的方法)视为违反 R7,code review 必须打回。
+
+#### Scenario: 新接入产品评估 Service 层
+
+- **WHEN** 在 mjava 接入新产品(如「腾讯会议」)
+- **THEN** 默认**只建 Client**,不建 Service
+- **AND** 第一个客户的二次编排放在客户子项目本地
+- **AND** 第二个客户来要复用时再上提
+
+#### Scenario: 误建空壳 Service
+
+- **WHEN** PR 中出现 `XxxService.java` 接口仅有 1~2 个未被任何子项目调用的方法
+- **THEN** code review 必须打回
+- **AND** 编排迁回客户子项目

+ 44 - 0
openspec/changes/standardize-client-service-layering/tasks.md

@@ -0,0 +1,44 @@
+## 1. 规则文档
+
+- [ ] 1.1 共享权威文档 `/Users/malk/Desktop/Tech/claude/后端/mjava-baseline.md` 新增章节 "client/Service 分层规则",正文为 R1~R7 七条
+- [ ] 1.2 仓库内 `openspec/specs/project-baseline.md` 在"代码锚点"段后加锚点:`> 分层规则见 capability spec [client-service-layering](changes/standardize-client-service-layering/specs/client-service-layering/spec.md)(archive 后路径变更)`
+- [ ] 1.3 `README.md` 子项目速览或基座章节加一行:分层规则锚点(同上)
+
+## 2. O3 补 ALYConf 并替换硬编码 URL
+
+- [ ] 2.1 新建 `mjava/src/main/java/com/malk/server/aliyun/ALYConf.java`,4 个 `public static final String` 常量:
+  - `URL_INVOICE_PDF = "https://fapiao.market.alicloudapi.com/v2/invoice/pdf"`
+  - `URL_INVOICE_QUERY = "https://fapiao.market.alicloudapi.com/v2/invoice/query"`
+  - `URL_INVOICE_QRCODE = "https://fapiao.market.alicloudapi.com/v2/invoice/qrcode"`
+  - `URL_INVOICE_OCR = "https://invoice.market.alicloudapi.com/v2/invoice/ocr"`
+- [ ] 2.2 改 `mjava/src/main/java/com/malk/service/aliyun/impl/ALYInvoiceImpl.java`:4 处 URL 字面量替换为 `ALYConf.URL_*` 引用
+- [ ] 2.3 grep 跨仓 `fapiao.market.alicloudapi.com` 和 `invoice.market.alicloudapi.com` 字面量,确认无其他副本(按 R4 流程)
+- [ ] 2.4 `mvn -pl mjava -am compile` 通过
+
+## 3. O4 INTP 范式约定(仅文档)
+
+- [ ] 3.1 在 capability spec `client-service-layering` 的 R7 Scenario 区,明确「集成平台 INTP 当前仅 `INTPClient_User`,未来扩多域时按 R1/R2 补 `INTPClient` 主入口;按 R7 评估后再补 `INTPService`」
+- [ ] 3.2 不动 `service/integration/` 任何代码
+
+## 4. O6 规则进 baseline(与 1.1/1.2 合并执行)
+
+- [ ] 4.1 已在 1.1/1.2 覆盖,本节为占位标记,验收 1.1/1.2 完成即可
+
+## 5. O2 RSACrypt 迁移(仅 grep 报告,等 ACK)
+
+- [ ] 5.1 grep `import com\.malk\.util\.crypto\.RSACrypt` 全仓 + 跨仓(mjava-ai / akds / 光明独立仓 / 其他用户在 ACK 时主动补的仓)
+- [ ] 5.2 输出引用清单(file:line + 调用上下文)给用户
+- [ ] 5.3 **等待用户 ACK**:用户回复"OK 迁"才执行下一步;否则本任务长期挂起
+- [ ] 5.4 ACK 后:复制 `util/crypto/RSACrypt.java` 到 `utils/crypto/RSACrypt.java`
+- [ ] 5.5 全仓改 import `com.malk.util.crypto.RSACrypt` → `com.malk.utils.crypto.RSACrypt`
+- [ ] 5.6 删 `util/crypto/RSACrypt.java` 和空目录 `util/`
+- [ ] 5.7 `mvn -pl mjava -am compile` 通过
+- [ ] 5.8 commit message 标 `refactor(util→utils): RSACrypt 迁移`,body 列受影响 import
+
+## 6. 验证
+
+- [ ] 6.1 `mvn -pl mjava -am compile` 全部通过
+- [ ] 6.2 子项目(mcli/pro/com)`mvn compile` 全部通过
+- [ ] 6.3 grep 验证:`server/` 下无 `@Service` `@Component`(R6 除 `*Conf`)
+- [ ] 6.4 grep 验证:spec 7 条规则在 mjava-baseline.md / project-baseline.md / spec.md 三处一致
+- [ ] 6.5 `/opsx:validate standardize-client-service-layering --strict` 通过