소스 검색

docs(openspec): add mjava-pro / mjava-com 专项提案 + CLAUDE.md 索引

- 新增 openspec/changes/add-mjava-pro/ 四件套:多客户单部署子项目,从宜搭应用表动态加载租户配置
- 新增 openspec/changes/add-mjava-com/ 四件套:通用能力 BaaS 网关,宜搭权限表单管理调用方白名单
- CLAUDE.md 更新现有 change 列表,标记两个专项为高优先级

本次只建提案不实施代码,保障 mcli/shunfeng/guangming 生产服务可用。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
malk 2 주 전
부모
커밋
a822ed64f4

+ 3 - 1
CLAUDE.md

@@ -22,7 +22,9 @@ Java 后端基座 + 客户子项目仓库。Spring Boot 2.2.13 + MySQL,第一
 现有 change 状态:
 - `changes/add-observability-foundation/` — 代码已实施,待生产冒烟
 - `changes/extract-dingtalk-standard-api/` — 已完成,待归档
-- `changes/init-project-baseline/` — 文档提案(本次初始化)
+- `changes/init-project-baseline/` — 文档提案(基线沉淀)
+- `changes/add-mjava-pro/` — 专项,**高优先级**:多客户单部署(宜搭应用表授权)
+- `changes/add-mjava-com/` — 专项,**高优先级**:通用能力 BaaS(宜搭权限表单授权)
 
 ## 快速操作
 

+ 87 - 0
openspec/changes/add-mjava-com/design.md

@@ -0,0 +1,87 @@
+## 架构要点
+
+### 请求流程
+
+```
+External System → POST /api/com/{vendor}/{action}
+                  Header: X-Caller-Id: {callerId}
+                          X-Signature: {HMAC-SHA256(callerSecret, body+timestamp)}
+                          X-Timestamp: {unix ms}
+                  Body: { ...params aligned with base Client method args }
+
+     ↓
+CallerAuthInterceptor
+  ├── 校验 X-Timestamp 5 分钟内
+  ├── CallerRegistryService.get(callerId) → callerSecret
+  ├── 计算 HMAC 对比
+  └── 通过 → 继续;失败 → 401
+     ↓
+GatewayController
+  ├── 查 ActionWhitelist:{vendor}.{action} 是否开放
+  ├── 用反射或注册的 handler 调对应基座 Client 方法
+  └── 包 McR 返回
+```
+
+### 调用方注册(宜搭权限表单)
+
+字段约定:
+
+| 宜搭字段 | 含义 |
+|---------|------|
+| `textField_callerId` | 调用方唯一 ID(签发时生成,如 `caller-YDCBC-001`) |
+| `textField_callerSecret` | HMAC 密钥(生成时返回给调用方,服务端也存,需加密字段) |
+| `textField_callerName` | 可读名称(便于审计) |
+| `textareaField_allowedActions` | JSON 数组:`["dingtalk.user.get", "aliwork.form.save"]`,精确白名单 |
+| `numberField_rateLimit` | 每秒限流上限 |
+| `radioField_enabled` | `on` / `off` |
+| `dateField_expireAt` | 密钥过期时间(可选) |
+
+### Vendor Action 路由
+
+为避免反射开销和控制边界,每个开放的 action 在代码里显式注册:
+
+```java
+@Component
+public class DingtalkActionRegistry {
+    @PostConstruct
+    public void register(ActionRegistry reg) {
+        reg.register("dingtalk.user.get", (ctx, body) -> {
+            DDConf conf = resolveConf(ctx);  // 可选:按 caller 配置不同 dingtalk 应用
+            return ddClient.getUserDetail(conf, body.getString("userid"));
+        });
+    }
+}
+```
+
+`application.yml` 只维护"哪些 action 默认开放"的白名单;细粒度调用方权限通过宜搭权限表单的 `allowedActions` 限制。
+
+### 限流
+
+- 本地 Guava `RateLimiter` 按 callerId 维度
+- 单实例场景足够;若未来多实例部署再评估 Redis / 分布式限流
+- 超限返回 `429 { code: "RATE_LIMITED" }`
+
+### 审计
+
+继承 `mjava-baseline §3.5` 审计规范,追加字段:
+
+| 字段 | 说明 |
+|------|------|
+| `callerId` | 调用方 ID |
+| `vendorAction` | `{vendor}.{action}` |
+| `signatureValid` | 签名校验结果 |
+
+日志输出 `./log/{日期}/com-{callerId}.log`(按调用方分片)。
+
+## 安全要点
+
+- `callerSecret` 存宜搭"加密字段"类型;内存访问后**不得**回传前端或记录到日志
+- HMAC 防重放:`X-Timestamp` 5 分钟外拒绝 + 可选 Nonce(Phase 1 不做,5 分钟窗口足够内部场景)
+- 每次调用尝试失败都打 `WARN`,连续 5 次同 callerId 失败触发 `ERROR` 告警(`ExceptionNotice` 已有通道)
+
+## Non-Goals(设计边界)
+
+- 不做 REST 规范化(不强制 HATEOAS / JSON:API)
+- 不做 WebSocket / 长连接
+- 不做异步任务回调(请求同步完成,调用方需要异步能力自行轮询或用 mjava-pro)
+- 不做请求转发到非 mjava vendor(本质是 mjava Client 方法的 HTTP 门面)

+ 53 - 0
openspec/changes/add-mjava-com/proposal.md

@@ -0,0 +1,53 @@
+> 状态(2026-04-18 立项):**提案阶段,未实施**
+> 优先级:高。与 `add-mjava-pro` 并行。
+
+## Why
+
+`mjava` 基座沉淀了大量通用能力:HTTP 请求封装(`UtilHttp`)、Token 缓存(`UtilToken`)、JPA + QueryDSL、统一响应体 `McR`、多数据源、TraceId 链路 等。目前这些能力只能被"引入 mjava 依赖"的 Java 子项目(mcli/shunfeng/guangming)使用。
+
+但实际业务中常遇到:
+
+- **非 Java 系统**(Node.js / Python / PHP 宜搭连接器 / 低代码平台)需要调用钉钉/宜搭 API,每个系统重复实现鉴权 + HTTP + token 缓存,浪费且容易出错
+- **前端侧业务**(宜搭自定义页面 JSX)也希望复用后端对接能力,不想在前端暴露 appSecret
+- **实习/新人项目**没必要为了调用一次钉钉 API 就起一个完整的 mjava 子项目
+
+需要把 mjava 基座能力"服务化"——通过 `mjava-com` 子模块对外开放 REST API,外部系统只要通过鉴权就能直接调用钉钉/宜搭/飞书等底层接口,**无需引入 mjava 代码**。
+
+## What Changes
+
+新增子模块 `mjava-com/`,具备:
+
+- **统一入口 API**:`/api/com/{vendor}/{action}`(如 `POST /api/com/dingtalk/user.get`)转发到基座 Client 方法
+- **调用方鉴权**:从宜搭"权限表单"(formUuid 配置在 `application.yml`)查询被授权的调用方白名单,按 apiKey + signature 验签
+- **审计日志**:每次调用打完整审计(调用方 ID + vendor + action + latency + success)
+- **限流/熔断**:按调用方维度做基础限流(避免某个外部系统打爆第三方配额);Phase 1 用本地 `RateLimiter`,不引入 Sentinel/Resilience4j
+- **Vendor 暴露白名单**:不是所有 Client 方法都自动开放,需要在 `application.yml` 显式声明哪些 `{vendor}.{action}` 可被 com 调用
+
+## Capabilities
+
+### New Capabilities
+- `baas-gateway`:BaaS 风格的后端即服务网关,把 mjava Client 能力以 REST 开放
+- `caller-registry`:外部调用方注册中心(宜搭权限表单)
+
+### Modified Capabilities
+<!-- 基座 Client 层保持不变,com 作为 facade 层向外暴露 -->
+
+## Impact
+
+- **新增模块**:`mjava-com/`(独立 jar,默认端口 9020,context-path `/api/com`)
+- **新增代码**(预计):
+  - `CallerAuthInterceptor.java`(apiKey + signature 验签)
+  - `CallerRegistryService.java`(查宜搭权限表单)
+  - `GatewayController.java`(路由 `/{vendor}/{action}` 到基座 Client)
+  - `ActionWhitelistConfig.java`(vendor.action 白名单)
+  - 简单本地限流 `CallerRateLimiter.java`
+- **基座影响**:零(com 作为 facade 调基座现有 Client)
+- **外部调用方**:简化 — 只需 apiKey + signature + JSON body 发 POST 即可
+
+## Non-Goals
+
+- ❌ 不做 OpenAPI / Swagger 规范(第一阶段人工维护接口文档;若需要再单独提案)
+- ❌ 不做计费 / 配额套餐(Phase 1 固定限流即可)
+- ❌ 不做 OAuth2 授权(apiKey + HMAC 签名足够内部场景;跨组织再评估)
+- ❌ 不引入 API Gateway 中间件(Kong / APISIX / Spring Cloud Gateway 等)
+- ❌ 不做请求录制回放(可测性工作,独立专项)

+ 67 - 0
openspec/changes/add-mjava-com/spec.md

@@ -0,0 +1,67 @@
+## baas-gateway
+
+### REQ-GW-001 统一入口
+
+- 路由规则:`POST /api/com/{vendor}/{action}`
+- `vendor` ∈ {`dingtalk`, `aliwork`, `beisen`, `teambition`, `feishu`, `fxiaoke`, `h3yun`, ...}(以 `mjava/service/` 下 vendor 包实际存在为准)
+- `action` 为显式注册的 key,形如 `user.get` / `form.save` / `attendance.listRecord`(命名对应官方 API 文档语义)
+- 请求体 JSON;字段名**必须**与基座 Client 对应方法参数一致
+
+### REQ-GW-002 签名校验
+
+- Header 必含 `X-Caller-Id` `X-Signature` `X-Timestamp`
+- 校验顺序:timestamp 窗口 5 分钟 → callerId 查注册表 → HMAC-SHA256 比对 → 调用方启用状态
+- 任一失败:`401 { code: "AUTH_FAILED", reason: "..." }`(reason 不暴露具体环节,避免用户猜测)
+
+### REQ-GW-003 动作白名单
+
+- action 必须先在 `ActionRegistry` 代码显式注册(反射调基座 Client 方法)
+- 同时必须在 `application.yml` 的 `com.actions.enabled` 列表启用
+- 调用方在宜搭权限表单的 `allowedActions` 包含该 action 才允许访问
+- 三层校验任一不过:`403 { code: "ACTION_FORBIDDEN" }`
+
+### REQ-GW-004 响应格式
+
+- 返回 `McR<T>`(基座已有):`{ success, code, message, data }`
+- 第三方原始响应放在 `data` 字段,保留全部原始结构(不做字段转义)
+- 第三方错误统一映射为 `McR.fail()`,`data` 含 vendor 原始错误码
+
+### REQ-GW-005 限流
+
+- 按 callerId 本地 `RateLimiter`,阈值取宜搭权限表单 `rateLimit` 字段
+- 超限返回 `429 { code: "RATE_LIMITED", retryAfter: 1 }`
+
+### REQ-GW-006 审计扩展
+
+- 日志字段在 `mjava-baseline §3.5` 基础上追加 `callerId` / `vendorAction` / `signatureValid`
+- 日志文件按 callerId 分片
+
+## caller-registry
+
+### REQ-CR-001 注册源
+
+- 调用方白名单维护于宜搭"权限表单"(formUuid 配置在 `com.caller.registry.formUuid`)
+- 字段约定见 `design.md`
+
+### REQ-CR-002 加载与刷新
+
+- 启动时全量拉取 `enabled=on` 的调用方到内存 `Map<callerId, CallerProfile>`
+- 默认 TTL 300 秒,可配 `com.caller.registry.ttlSeconds`
+- 失效后 stale-while-revalidate
+
+### REQ-CR-003 密钥轮换
+
+- 调用方申请新密钥 → 运营在宜搭表单更新 `callerSecret` → 下次拉取后生效
+- 宜搭表单不保留历史版本(如需双密钥轮换,未来提案支持)
+
+### REQ-CR-004 调用方停用
+
+- `enabled=off` 后,注册表下次刷新移除
+- 停用期间返回 `401 CALLER_DISABLED`
+
+## 非目标明示
+
+- 不做签名算法可插拔(固定 HMAC-SHA256;将来如需支持国密再提案)
+- 不做 mTLS / API Gateway 前置
+- 不做请求体加密(HTTPS + HMAC 完整性足够)
+- 不做响应缓存(每次都直调基座 Client;幂等缓存由调用方自行做)

+ 53 - 0
openspec/changes/add-mjava-com/tasks.md

@@ -0,0 +1,53 @@
+## 1. 模块脚手架
+
+- [ ] 1.1 复制 `mjava-mcli/` 为 `mjava-com/`
+- [ ] 1.2 改 `pom.xml` artifactId 为 `mjava-com`
+- [ ] 1.3 改 `Boot.java` package 为 `com.malk.com_`(注意避开 java 保留字 `com`,用 `com_` 或 `gw`)
+- [ ] 1.4 改 `application.yml` port=9020 + context-path=/api/com
+- [ ] 1.5 根 pom `<modules>` 追加 `<module>mjava-com</module>`
+- [ ] 1.6 编译冒烟通过
+
+## 2. 调用方鉴权
+
+- [ ] 2.1 `com.malk.com_.auth.CallerProfile`(数据类)
+- [ ] 2.2 `CallerRegistryService`(查宜搭权限表单,缓存 + 刷新)
+- [ ] 2.3 `CallerAuthInterceptor`(时间窗 → callerId → HMAC-SHA256 → enabled)
+- [ ] 2.4 注册拦截器到 WebMvcConfigurer,排除 `/health` 等公共端点
+
+## 3. 动作注册
+
+- [ ] 3.1 `ActionRegistry`(`Map<String, ActionHandler>`)
+- [ ] 3.2 `ActionHandler` 接口:`McR handle(CallerProfile caller, JSONObject body)`
+- [ ] 3.3 `DingtalkActionRegistry`:在 `@PostConstruct` 注册至少 3 个 action(user.get / dept.list / workflow.start)作为首批
+- [ ] 3.4 `AliworkActionRegistry`:首批 3 action(form.save / form.list / form.update)
+- [ ] 3.5 `ActionWhitelistConfig`(读 `application.yml` `com.actions.enabled`)
+
+## 4. 网关 Controller
+
+- [ ] 4.1 `GatewayController` 路由 `POST /{vendor}/{action}`
+- [ ] 4.2 三层校验(代码 registry / yml whitelist / caller.allowedActions)
+- [ ] 4.3 统一异常返回 McR(AUTH_FAILED / ACTION_FORBIDDEN / RATE_LIMITED / VENDOR_ERROR)
+
+## 5. 限流
+
+- [ ] 5.1 `CallerRateLimiter`(Guava RateLimiter per callerId,LRU 容量 500)
+- [ ] 5.2 拦截器链路:Auth → RateLimit → Gateway
+
+## 6. 审计日志
+
+- [ ] 6.1 在基座 `UtilHttp` 审计基础上,com 侧拦截器补写入 MDC(callerId / vendorAction)
+- [ ] 6.2 logback-spring.xml 为 mjava-com 输出 `./log/{日期}/com-%X{callerId}.log`
+
+## 7. 配置与文档
+
+- [ ] 7.1 `application-dev.yml.example`(com.caller.registry.* + com.actions.enabled 示例)
+- [ ] 7.2 `application-prod.yml.example`(同上占位)
+- [ ] 7.3 `README.md`:外部系统对接步骤、signature 算法示例(Python / Node.js 样例代码)
+- [ ] 7.4 更新 `mjava-baseline.md` 表格里 mjava-com 状态为"已实施"
+
+## 8. 验证
+
+- [ ] 8.1 单元测试:`HmacSignatureTest` / `CallerRegistryTest`
+- [ ] 8.2 集成冒烟:模拟外部 Python 客户端签名 → 调 `/dingtalk/user.get` → 验证成功 & 失败(签名错、过期、限流、未启用 action)
+- [ ] 8.3 压测:单个 caller 限流准确性(RateLimiter 容差)
+- [ ] 8.4 `/opsx:validate add-mjava-com --strict` 通过

+ 75 - 0
openspec/changes/add-mjava-pro/design.md

@@ -0,0 +1,75 @@
+## 架构要点
+
+### 租户识别
+
+```
+Request → TenantInterceptor (读 X-Tenant-Id) → TenantContext.set(tenantId) → Controller → Service → Client
+                                                                                        ↓
+                                                                       Client 从 TenantContext 拿 appKey/appSecret
+```
+
+`TenantContext` 用 `ThreadLocal<TenantProfile>`;`TenantProfile` 包含:
+- `tenantId`(宜搭应用表主键)
+- `vendorCredentials: Map<String, VendorCredential>`(钉钉/aliwork 等)
+- `extraJson`(tenant 级自定义配置)
+
+### 租户配置加载
+
+**数据源**:宜搭"应用表"(formUuid 配置)。字段约定:
+
+| 宜搭字段 | 含义 |
+|---------|------|
+| `textField_tenantId` | 租户唯一标识(建议用客户代号如 `guangming`) |
+| `textField_vendor` | 第三方平台 key(`dingtalk` / `aliwork` / ...) |
+| `textField_appKey` | 该 vendor 的 appKey |
+| `textField_appSecret` | 该 vendor 的 appSecret(写入时走宜搭加密字段) |
+| `textField_corpId` | 可选(钉钉需要) |
+| `textareaField_extraJson` | JSON 字符串,扩展参数 |
+| `radioField_enabled` | `on` / `off` |
+
+**加载策略**:
+- 启动时全量拉取 → 内存 `Map<tenantId, TenantProfile>`
+- TTL 10 分钟(可配 `tenant.registry.ttlSeconds`)过期后按需刷新
+- 手动 `/pro/_admin/reloadTenant?tenantId=xxx`(仅 dev profile 开放)做热刷
+
+### Client 透传机制
+
+基座 `DDClient` 签名不变(仍接受 appKey/appSecret 参数),`mjava-pro` 提供一层 `DynamicDDService` 组合:
+
+```java
+// Service 层内部
+DDConf conf = TenantContext.current().credential("dingtalk").toDDConf();
+ddClient.listDepartmentUserDetail(conf, deptId, cursor, size);
+```
+
+**不做** AOP 反射修改签名 — 保持显式、可读、可调试。
+
+### Token 缓存隔离
+
+`UtilToken` 当前 key 是裸 `{vendor}:{appKey}`。多租户环境下两个客户可能共用同一 appKey(极少,但要防撞)。建议基座支持 namespace:
+
+```java
+UtilToken.put(tenantId, "dingtalk:" + appKey, token, expireSec);
+UtilToken.get(tenantId, "dingtalk:" + appKey);
+```
+
+若基座改动太大,**回退方案**:`mjava-pro` 约束 key 为 `{tenantId}:{vendor}:{appKey}`,不改基座。
+
+### 错误处理
+
+- tenantId 缺失 → `401 TENANT_REQUIRED`
+- tenantId 不存在或 disabled → `403 TENANT_NOT_FOUND`
+- vendor 配置缺失 → `500 TENANT_VENDOR_MISCONFIGURED` + 审计日志
+
+## 技术约束
+
+- 沿用 `mjava-baseline.md` 全部规范
+- `application-prod.yml` 只配宜搭应用表 formUuid / systemToken(访问宜搭自身的凭据仍是静态),不配任何客户凭据
+- 请求监听日志必须含 `tenantId` 字段(在 §3.5 审计字段基础上扩展)
+
+## Non-Goals(设计边界)
+
+- 不考虑租户级数据库隔离(所有租户共享同一 MySQL)
+- 不考虑租户级流量限额(QPS 限流等;有需求再单独提案)
+- 不考虑跨租户工作流(暂时假设每次请求单租户上下文)
+- 不考虑 Redis 分布式缓存(单实例 TimedCache 足够 Phase 1)

+ 57 - 0
openspec/changes/add-mjava-pro/proposal.md

@@ -0,0 +1,57 @@
+> 状态(2026-04-18 立项):**提案阶段,未实施**
+> 优先级:高。与 `add-mjava-com` 并行,共同承担"多用户/多客户使用支持"大方向。
+
+## Why
+
+现有 `mjava-mcli` / `mjava-shunfeng` / `mjava-guangming` 三个客户子项目采用"一客户一 jar"模式,每次加新客户都要:
+
+1. 复制 mcli 模板 → 改 artifactId / port / context-path
+2. 在 `application-{profile}.yml` 硬编码该客户的 appKey/appSecret/corpId
+3. 单独部署一个 JVM 实例
+
+这种方式在客户数量少(≤ 5)时尚可,但问题:
+
+- 新客户上线 = 发布新版本 + 运维新增部署节点,交付周期慢
+- 一客户一实例,资源利用率低
+- appKey/appSecret 静态写在 yml,切换客户需要重启 + 重新发包
+- 实例间没有共享缓存,每客户独立维护自己的 token 生命周期
+
+需要一个支持"**一个 jar 同时服务多个客户**"的子项目模板 `mjava-pro`,按请求上下文动态加载客户配置、token 按租户隔离缓存,**加客户 = 新增一条宜搭应用表记录**,无需改代码和重启。
+
+## What Changes
+
+新增子模块 `mjava-pro/`,具备以下能力:
+
+- **租户解析**:请求入口识别 `X-Tenant-Id` header(或 subdomain / path prefix,实现时选定),未识别到返回 401
+- **应用配置动态加载**:从宜搭"应用表"(formUuid 配置在 `application.yml`)按 tenantId 查询 appKey / appSecret / corpId / 其他授权参数;命中后缓存到内存(TTL 可配,默认 10 分钟)
+- **Token 隔离**:`UtilToken` key 扩展为 `{tenant}:{vendor}:{appKey}`,各租户独立缓存不互相覆盖
+- **Client 透传**:基座 `DDClient` / `YDClient` 等保持原签名不变;`mjava-pro` 内部通过 `TenantContext`(ThreadLocal)传 appKey/appSecret 进去
+- **配置表结构约定**:宜搭应用表字段规范固定在 `spec.md` 中(tenantId / vendor / appKey / appSecret / extraJson / enabled / ...)
+
+## Capabilities
+
+### New Capabilities
+- `multi-tenant-runtime`:单部署多租户运行时,按请求上下文路由到对应客户配置
+- `tenant-registry`:从宜搭应用表加载租户配置的注册中心抽象
+
+### Modified Capabilities
+<!-- 基座 Client 层签名保持不变,tenant context 作为 cross-cutting concern 注入 -->
+
+## Impact
+
+- **新增模块**:`mjava-pro/`(独立 jar,默认端口 9010,context-path `/api/pro`)
+- **新增代码**(预计):
+  - `TenantContext.java` + `TenantInterceptor.java`(ThreadLocal 管理)
+  - `TenantRegistryService.java`(查宜搭应用表)
+  - `DynamicDDClientProxy.java`(包装基座 DDClient,按 tenant 注入凭据)
+  - `application.yml` + `application-{dev,prod}.yml.example`
+- **基座影响**:可能需要 `UtilToken` 支持 key 命名空间;评估后若需要再开子提案
+- **现有客户模块**:零影响,mcli/shunfeng/guangming 继续运行
+- **部署**:单 pro jar 可接管 N 个客户,运维新增客户只在宜搭应用表加一行记录即可
+
+## Non-Goals
+
+- ❌ 不替代现有 mjava-mcli/shunfeng/guangming(已交付客户保持原架构)
+- ❌ 不引入 Redis(tenant 配置缓存仍走 `TimedCache`,单实例足够;若将来多实例再评估)
+- ❌ 不做跨租户数据聚合 / 租户级计费 / 多租户数据库分离
+- ❌ 本提案不涵盖前端租户切换 UI(前端按现有方式各自管理)

+ 55 - 0
openspec/changes/add-mjava-pro/spec.md

@@ -0,0 +1,55 @@
+## multi-tenant-runtime
+
+### REQ-MT-001 租户识别
+
+- Controller 入口通过 `TenantInterceptor` 读 HTTP Header `X-Tenant-Id`
+- Header 缺失:返回 `401 { code: "TENANT_REQUIRED" }`
+- Header 存在但租户不在注册表:返回 `403 { code: "TENANT_NOT_FOUND" }`
+- 识别成功:写入 `TenantContext.set(profile)`;请求结束时 `TenantContext.clear()`(务必在 finally)
+
+### REQ-MT-002 异步任务传递
+
+- `@Async` 线程池必须通过 `TaskDecorator` 复制 TenantContext 到子线程(参考现有 `MdcTaskDecorator`)
+- `CompletableFuture` 手动切换线程时需显式 `TenantContext.propagate(ctx, () -> ...)`
+
+### REQ-MT-003 Token 隔离
+
+- `UtilToken` key 格式统一为 `{tenantId}:{vendor}:{appKey}`
+- `mjava-pro` 不得使用无 tenant 前缀的 UtilToken 接口
+- 无租户上下文(如定时任务)显式传 tenantId,或声明为 `SYSTEM` 伪租户
+
+### REQ-MT-004 审计日志扩展
+
+- `UtilHttp` 审计日志在 `mjava-baseline §3.5` 字段基础上追加 `tenantId`
+- 日志输出目标 `./log/{日期}/pro-{tenantId}.log`(按租户分片,方便定责)
+
+## tenant-registry
+
+### REQ-TR-001 注册源
+
+- 租户配置存放于宜搭"应用表"(formUuid 配置在 `tenant.registry.formUuid`)
+- 字段约定见 `design.md` 表格
+- 访问宜搭本身使用 `application-{profile}.yml` 配置的 `aliwork.appType` / `aliwork.systemToken`(这是 mjava-pro 自身的入口凭据,非租户凭据)
+
+### REQ-TR-002 加载与缓存
+
+- 启动时全量拉取一次,写入 `Map<String, TenantProfile>`
+- TTL:默认 600 秒(`tenant.registry.ttlSeconds` 可覆盖)
+- 过期后首次访问触发异步刷新,旧值继续可用直到新值就绪(stale-while-revalidate)
+- 热刷入口:`POST /api/pro/_admin/reloadTenant`(仅 dev profile)
+
+### REQ-TR-003 敏感字段处理
+
+- appSecret / systemToken 等字段在日志输出时必须脱敏(`***`)
+- 内存中允许保留明文(TenantProfile 字段直接 String,不引入加密存储以保持简单)
+
+### REQ-TR-004 失效与禁用
+
+- 宜搭应用表 `radioField_enabled=off` → 注册表移除该 tenantId
+- 请求携带已禁用 tenantId → 返回 `403 TENANT_DISABLED`
+
+## 非目标明示
+
+- 不定义 tenantId 命名规范(客户代号 / UUID / 数字 ID 均可,由运营决定)
+- 不定义 tenant-level feature flag(有需要另外提案)
+- 不定义多 tenant 同库的 row-level security(共享库方案下由业务 service 自行按 tenantId 过滤)

+ 50 - 0
openspec/changes/add-mjava-pro/tasks.md

@@ -0,0 +1,50 @@
+## 1. 模块脚手架
+
+- [ ] 1.1 复制 `mjava-mcli/` 为 `mjava-pro/`
+- [ ] 1.2 改 `pom.xml` artifactId 为 `mjava-pro`
+- [ ] 1.3 改 `Boot.java` package 为 `com.malk.pro`,`scanBasePackages = {"com.malk"}` 保持
+- [ ] 1.4 改 `application.yml` server.port=9010 + context-path=/api/pro;spel.multiSource=false
+- [ ] 1.5 根 `pom.xml` `<modules>` 追加 `<module>mjava-pro</module>`
+- [ ] 1.6 `mvn -pl mjava-pro -am compile` 通过
+
+## 2. TenantContext 核心
+
+- [ ] 2.1 新建 `mjava-pro/src/main/java/com/malk/pro/tenant/TenantProfile.java`(数据类:tenantId + vendorCredentials + extraJson)
+- [ ] 2.2 新建 `com.malk.pro.tenant.TenantContext`(`ThreadLocal<TenantProfile>` + `current()/set()/clear()/propagate()`)
+- [ ] 2.3 新建 `TenantInterceptor`(`HandlerInterceptor`)解析 `X-Tenant-Id` → 查注册表 → set;afterCompletion clear
+- [ ] 2.4 在 `WebMvcConfigurer` 注册拦截器,排除 `/_admin/**` 的公共端点
+- [ ] 2.5 扩展 `MdcTaskDecorator` 或新建 `TenantTaskDecorator`,挂到 `AsyncConfig` 两个线程池
+
+## 3. TenantRegistry
+
+- [ ] 3.1 新建 `com.malk.pro.tenant.TenantRegistryService`(依赖 `YDClient`)
+- [ ] 3.2 实现 `loadAll()`:按 `tenant.registry.formUuid` 分页查宜搭应用表,转 `TenantProfile`
+- [ ] 3.3 实现 `get(tenantId)`:内存 Map 查;缺失触发 `loadOne()` 单条拉取
+- [ ] 3.4 `@PostConstruct loadAll()`;`@Scheduled(fixedDelay = ttl)` 异步刷新
+- [ ] 3.5 新建 `AdminController`(仅 dev profile 通过 `@Profile("dev")` 生效)暴露 `/reloadTenant`
+
+## 4. Client / Service 透传
+
+- [ ] 4.1 新建 `com.malk.pro.service.dingtalk.DynamicDDService`:从 `TenantContext` 读凭据 → 构造 `DDConf` → 调基座 `DDClient`
+- [ ] 4.2 同法建 `DynamicYDService`(对接宜搭)、其他 vendor 按需
+- [ ] 4.3 `UtilToken` 使用规范:key 统一 `{tenantId}:{vendor}:{appKey}`(若基座未扩展 namespace,就在调用侧拼)
+
+## 5. 审计日志
+
+- [ ] 5.1 `UtilHttp` 审计日志扩展字段 `tenantId`(从 MDC 读);改基座或在 pro 侧包一层
+- [ ] 5.2 logback-spring.xml 为 mjava-pro 独立输出 `./log/{日期}/pro-%X{tenantId}.log`
+- [ ] 5.3 确保异步线程能读到 MDC(配合 Task 2.5)
+
+## 6. 配置与文档
+
+- [ ] 6.1 `application-dev.yml.example`(含 tenant.registry.* 占位)
+- [ ] 6.2 `application-prod.yml.example`(含 tenant.registry.* 占位)
+- [ ] 6.3 `README.md`(在 mjava-pro 目录下,说明新增租户操作步骤)
+- [ ] 6.4 更新 `/Users/malk/Desktop/Tech/claude/后端/mjava-baseline.md` 表格里 mjava-pro 状态改为"已实施"
+
+## 7. 验证
+
+- [ ] 7.1 单元测试:`TenantContextTest`(set/clear/异步传播)
+- [ ] 7.2 集成冒烟:两个租户配置 → 用不同 `X-Tenant-Id` 调同一接口 → 验证 token 隔离 + 审计日志分片
+- [ ] 7.3 压测:100 QPS 下 TenantContext 无泄漏(ThreadLocal clear 正确)
+- [ ] 7.4 `/opsx:validate add-mjava-pro --strict` 通过