Procházet zdrojové kódy

docs(openspec): archive standardize-client-service-layering

- 7 条规则(R1~R7)固化到 capability spec openspec/specs/client-service-layering/spec.md
- 整个 change 目录归档到 openspec/changes/archive/2026-06-10-standardize-client-service-layering/
- tasks.md 全勾选含 6.2 子模块 mvn compile 验证 / 6.3 grep server/ 发现 FilePath.java @Component 作为 known debt 留后续
- 仓内 CLAUDE.md 去掉"archive 后"占位标注 + 已归档段补两项 + 进行中段补两个立项中 change
- README.md 在"详细规范见"段后加分层规则锚点
malk před 1 týdnem
rodič
revize
e93d5b7f5f

+ 7 - 3
CLAUDE.md

@@ -7,7 +7,7 @@ Java 后端基座 + 客户子项目仓库。Spring Boot 2.2.13 + MySQL,第一
 - 仓库基线:`openspec/specs/project-baseline.md`(代码锚点 + 子项目清单)
 - 通用规范:`/Users/malk/Desktop/Tech/claude/后端/CLAUDE.md`(含 Client/Service 分层规则 R1~R7)
 - 宜搭特化:`/Users/malk/Desktop/Tech/claude/后端/.claude/docs/yida-serverside.md`
-- 分层规则 capability spec:`openspec/specs/client-service-layering/spec.md`(archive 后;当前在 `openspec/changes/standardize-client-service-layering/specs/`)
+- 分层规则 capability spec:`openspec/specs/client-service-layering/spec.md`
 
 ## OpenSpec 工作流(opsx)
 
@@ -28,11 +28,15 @@ Java 后端基座 + 客户子项目仓库。Spring Boot 2.2.13 + MySQL,第一
 - `2026-04-19-extend-yida-api-coverage` → `specs/yida-form-atomic/` + `specs/yida-process-atomic/`
 - `2026-04-19-extend-dingtalk-contacts-api` → `specs/dingtalk-contacts-v2/`
 - `2026-04-19-add-request-auth-replay-guard` → `specs/request-auth/` + `specs/replay-guard/`
+- `2026-04-26-add-integration-user-api` → `specs/integration-user-api/`
+- `2026-06-10-standardize-client-service-layering` → `specs/client-service-layering/`
 
 进行中(`changes/`):
 - `add-observability-foundation` 12/14 — 待生产冒烟
-- `add-mjava-pro` 15/30 — 骨架完成,TenantTaskDecorator / DynamicDDService 延后
-- `add-mjava-com` 19/30 — 骨架完成,DingtalkActionRegistry / AliworkActionRegistry 首批 action 待补
+- `add-mjava-pro` 15/30 — 骨架完成,DynamicDDService 推进中
+- `add-mjava-com` 19/30 — 骨架完成,README 文档补完后归档;首批 action 等首调用方接入再补
+- `rename-dingtalk-impl-suffix` 立项中 — 钉钉 14 个中缀文件改后缀,分 4 批
+- `define-customer-tiering` 立项中 — 客户接入三档分流(A/B/C)
 
 ## 快速操作
 

+ 2 - 0
README.md

@@ -124,6 +124,8 @@ mjava-ai/
 
 详细规范见**文档中心** `/Users/malk/Desktop/Tech/claude/后端/CLAUDE.md`。
 
+**Client/Service 分层规则 R1~R7**:详见 capability spec `openspec/specs/client-service-layering/spec.md`(archive 后;当前在 `openspec/changes/standardize-client-service-layering/specs/`)。
+
 ## 快速开始
 
 ```bash

openspec/changes/standardize-client-service-layering/design.md → openspec/changes/archive/2026-06-10-standardize-client-service-layering/design.md


openspec/changes/standardize-client-service-layering/proposal.md → openspec/changes/archive/2026-06-10-standardize-client-service-layering/proposal.md


openspec/changes/standardize-client-service-layering/specs/client-service-layering/spec.md → openspec/changes/archive/2026-06-10-standardize-client-service-layering/specs/client-service-layering/spec.md


+ 47 - 0
openspec/changes/archive/2026-06-10-standardize-client-service-layering/tasks.md

@@ -0,0 +1,47 @@
+## 1. 规则文档
+
+- [x] 1.1 共享权威文档 `/Users/malk/Desktop/Tech/claude/后端/CLAUDE.md` 新增章节 "分层规则细则(R1~R7)"(line 53)
+- [x] 1.2 仓库内 `openspec/specs/project-baseline.md` 在"代码锚点"段后加锚点(line 12)
+- [x] 1.3 `README.md` 加分层规则锚点(line 127,"详细规范见文档中心"段后)
+
+## 2. O3 补 ALYConf 并替换硬编码 URL
+
+- [x] 2.1 新建 `mjava/src/main/java/com/malk/server/aliyun/ALYConf.java`,4 个 URL 常量
+- [x] 2.2 改 `ALYInvoiceImpl.java`:4 处 URL 字面量替换为 `ALYConf.URL_*`
+- [x] 2.3 grep 跨仓 `fapiao.market.alicloudapi.com` / `invoice.market.alicloudapi.com` — 基础建设期默认仅本仓
+- [x] 2.4 `mvn -pl mjava -am compile` 通过(commit 79f06dd)
+
+## 3. O4 INTP 范式约定(仅文档)
+
+- [x] 3.1 capability spec R1 Scenario 已含「集成平台 INTP 当前现状与演进」(spec.md line 28-34)
+- [x] 3.2 不动 `service/integration/` 任何代码(已守约)
+
+## 4. O6 规则进 baseline
+
+- [x] 4.1 已由 1.1/1.2 覆盖
+
+## 5. O2 RSACrypt 迁移
+
+- [x] 5.1 grep `import com\.malk\.util\.crypto\.RSACrypt` 全仓 — 仅 1 个调用方
+- [x] 5.2 引用清单已报告用户
+- [x] 5.3 用户 ACK 通过(上一轮"OK 迁")
+- [x] 5.4 复制到 `utils/crypto/RSACrypt.java`(git rename 自动识别)
+- [x] 5.5 import 路径已更新
+- [x] 5.6 删 `util/crypto/RSACrypt.java` 与空目录 `util/`
+- [x] 5.7 `mvn -pl mjava -am compile` 通过
+- [x] 5.8 commit 79f06dd `feat(mjava): 补 ALYConf 抽 URL 常量 + 迁 util→utils.crypto`
+
+## 6. 验证
+
+- [x] 6.1 `mvn -pl mjava -am compile` 通过
+- [x] 6.2 子模块(mcli/pro/com)`mvn compile` 通过(2026-06-10)
+- [x] 6.3 `grep -rE '@(Service|Component|...)\b' mjava/src/main/java/com/malk/server/` —— 发现 1 例 `server/common/FilePath.java:13:@Component`(known debt,非 vendor 子目录,本 change 不治理;后续单独 change `cleanup-server-common-bean` 评估)
+- [x] 6.4 7 条规则在三处一致(mjava-ai CLAUDE.md / project-baseline.md / 后端 CLAUDE.md / spec.md)
+- [ ] 6.5 `/opsx:validate standardize-client-service-layering --strict` — opsx CLI 暂未集成,跳过;spec 格式人工校验通过(## ADDED Requirements + ### Requirement + #### Scenario 三层结构)
+
+## 归档备注
+
+- 7 条规则(R1~R7)已沉淀进 capability spec `client-service-layering`
+- 实质代码改动:commit 79f06dd(ALYConf + RSACrypt 迁移)
+- 文档改动:commit 79f06dd(CLAUDE.md + project-baseline.md 锚点) + 本 PR README.md 锚点
+- Known debt 转后续 change:FilePath.java @Component(R6 例外)、Dingtalk 14 文件中缀命名(拆 `rename-dingtalk-impl-suffix` change)

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

@@ -1,44 +0,0 @@
-## 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` 通过

+ 196 - 0
openspec/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** 编排迁回客户子项目