## ADDED Requirements ### Requirement: R1 三档分档矩阵 mjava-ai 接入新客户时按以下三档分流,三档**互斥**(一个客户同时只属一档): | 档 | 名称 | 物理落点 | 部署形态 | 命名 | |---|---|---|---|---| | **A** | 公共托管(多租户) | `mjava-pro/`(mjava-ai 仓内共享 jar) | 一个 jar 跑 N 客户,端口 9010 | 仅在宜搭应用表加一行 `tenantId` | | **B** | 独立子模块 | `mjava-ai/mjava-{客户}/`(仓内独立子模块,复制 `mjava-mcli`) | 一客户一 jar,独立端口 | `mjava-{客户拼音/英文短名}`(如 `mjava-akds`) | | **C** | 独立 git 仓 | `cur/mjava-{客户}/`(不在 mjava-ai 仓内) | 客户自管部署 | `mjava-{客户}`(仓 root artifactId) | 历史先例:`cur/mjava-guangming/` 与 akds 为 C 档;当前无落地 A/B 档客户实例。 #### Scenario: 新客户接入第一步 - **WHEN** 有新客户接入需求 - **THEN** 必须先按 R2 决策流程选档 - **AND** 在客户接入会议纪要 / OpenSpec change proposal / commit message 中显式记录所选档位 - **AND** 不允许跳过分档判定直接动手建子模块或拉新仓 #### Scenario: 单客户跨档归属 - **WHEN** 一个客户的不同业务模块似乎需要不同档位(如某客户审批流走 A 档共享、某独立报表服务走 B 档) - **THEN** 整个客户按**最高档**归属(取 B),不允许跨档混编 - **AND** 若坚持拆分,必须视为「客户名下两个客户实体」分别走 R2 --- ### Requirement: R2 决策流程(按硬指标升档) 判定按「**触发即升档**」原则,逐档自上而下匹配,匹配到最高档位停止: 1. **从 A 起评**:默认所有新客户先评估 A 档 2. **A→B 触发条件**(任一即触发升档): - (a) 客户需要写自定义 Java Controller / Service / Entity - (b) 客户需要私有数据库 schema(不能共享 mjava-pro 的 DB) - (c) 客户需要独立部署节点以隔离故障域 3. **B→C 触发条件**(任一即触发升档): - (a) 客户提供自己的部署环境(私网 / 客户机房 / 客户 K8s) - (b) 客户要求源代码进客户 git 仓 - (c) 客户要求 mjava 基座做大幅分叉(≥3 个文件改基座) 升档**单向**:A→B→C 不可逆。原因:升档后物理资产已经独立(独立 jar / 独立 git 仓),回退需要数据迁移 + 部署变更 + 历史 git 不可回收,成本远高于继续维护。 #### Scenario: 客户初次接入按硬指标走判定 - **WHEN** 评估新客户「思库文化」时,确认其无需写自定义 Java 代码、共享数据库即可、可部署在我们机房 - **THEN** A→B 三个触发条件均不命中 - **AND** 选 A 档:宜搭应用表加一行 `tenantId=sikuwh`,无新增 Java 代码 #### Scenario: 客户初次接入命中 B 档 - **WHEN** 评估「阿科德斯」时,发现需要写自定义工时审批 Controller(命中 A→B(a)) - **THEN** 升 B 档:在 mjava-ai 仓内 `cp -r mjava-mcli mjava-akds`,独立 artifactId / 端口 - **AND** 部署仍在我们机房(B→C 不触发) #### Scenario: 客户初次接入命中 C 档 - **WHEN** 评估「光明集团」时,客户要求源代码进客户 git 仓(命中 B→C(b)) - **THEN** 升 C 档:拉独立 git 仓 `cur/mjava-guangming/`,fork mjava 基座到客户仓内 - **AND** 客户仓与 mjava-ai 解耦演进 #### Scenario: 误选低档需升档 - **WHEN** 已落地 A 档的客户业务长大,开始要求私有 schema(命中 A→B(b)) - **THEN** 走 R3 升档迁移路径,**不**继续凑合在 A 档 - **AND** 升档迁移期间双轨运行(A 档读写转只读,B 档新实例并行启动),数据迁移完成后下线 A 档租户配置 #### Scenario: 试图降档 - **WHEN** 已升 B 档的客户业务收缩,看似可退回 A 档 - **THEN** 不降档;继续以 B 档运行 - **AND** 若长期闲置(≥6 个月无业务变更),评估是否归档下线整个子模块(属另一类决策,不属降档) --- ### Requirement: R3 升档迁移路径 升档触发后按以下步骤执行迁移: **A → B 迁移步骤**: 1. **新建 B 档子模块**:`cp -r mjava-mcli mjava-{客户}`,改 artifactId / port / context-path 2. **配置迁移**:把宜搭应用表该 tenantId 的凭据复制到 `application-{profile}.yml` 3. **数据迁移**:把 mjava-pro DB 内该 tenantId 的行级数据导出 → 导入 B 档独立 schema(脚本一次性,归档备份) 4. **双轨期**(1~2 周):A 档租户配置 `enabled=false`(拒新请求),B 档接管;监控异常 5. **下线 A 档配置**:宜搭应用表删除该 tenantId 行 6. **commit message**:`refactor: 客户 {名} 升档 A → B(trigger: {条件})` **B → C 迁移步骤**: 1. **新建独立 git 仓**:`cur/mjava-{客户}/`,初始化为多模块 pom 结构 2. **fork 基座**:把 mjava-ai 当前 `mjava/` 目录全量复制进客户仓 `mjava/`,版本号保持上游一致 3. **迁移 B 档子模块**:把 `mjava-ai/mjava-{客户}/` 全量复制进客户仓 4. **删除 mjava-ai 仓内的 B 档子模块**:根 `pom.xml` 移除 ``,删除目录 5. **客户仓 README**:写明分叉自 mjava-ai 哪个 commit 6. **commit message**:mjava-ai 侧 `refactor: 客户 {名} 升档 B → C(迁出独立仓 {仓库 URL})` #### Scenario: A → B 双轨期出错回退 - **WHEN** B 档启动后发现严重 bug,3 天内无法修复 - **THEN** 临时把 A 档租户配置 `enabled=true` 恢复 A 档接管,B 档进入修复期 - **AND** 不视为「降档」(B 档子模块继续保留,等待修复后再切) #### Scenario: B → C 客户仓 fork 基座的版本号 - **WHEN** 把 mjava-ai 的 `mjava/` fork 到客户仓 - **THEN** `mjava/pom.xml` 的 `` **保持与 mjava-ai 当前一致**(如 0.0.3) - **AND** 客户仓内对基座的后续分叉改动**不**改版本号,通过 git 历史追溯 - **AND** 客户仓 README 写明 fork 来源 commit hash --- ### Requirement: R4 A 档(mjava-pro)入驻清单 客户走 A 档时,宜搭应用表(`mjava-pro.yml` 配置的 formUuid)必填字段: | 字段 | 含义 | 必填 | 备注 | |---|---|---|---| | `tenantId` | 业务唯一标识 | ✅ | 全局唯一,禁用中文 / 特殊字符 | | `tenantName` | 中文展示名 | ✅ | 仅用于日志 / 监控可读性 | | `vendor` | 第三方平台 | ✅ | `dingtalk` / `aliwork` / `feishu` 等枚举 | | `appKey` | 产品方 appKey | ✅ | — | | `appSecret` | 产品方 appSecret | ✅ | 应在宜搭加密字段存储 | | `corpId` | 钉钉系 corp ID | 钉钉系必填 | — | | `extraJson` | vendor 特定额外参数 | 可选 | JSON 字符串,由 vendor 适配器解析 | | `enabled` | 启用标志 | ✅ | `false` 时拒绝该 tenant 请求 | **隔离边界**: - **DB**:A 档客户共享 mjava-pro 的 DB schema;所有业务表必须含 `tenant_id` 列;查询条件强制带 `tenant_id`(由 JPA Interceptor 注入,不可绕过) - **缓存**:`UtilToken` key 命名空间扩为 `{tenantId}:{vendor}:{appKey}`(由 add-mjava-pro 的 `tenant-registry` capability 实现) - **日志**:MDC 自动注入 `tenantId`,traceId 日志格式追加 `[T:{tenantId}]` **入驻 SOP**: 1. 校验:确认 R2 决策中 A 档判定通过(不命中任一 A→B 条件) 2. 宜搭应用表新增一行 3. 凭据热验证:调 mjava-pro `/api/pro/_diag/tenant/{tenantId}` 探活 4. 邮件通告运维 + 客户对接人 #### Scenario: A 档入驻缺必填字段 - **WHEN** 宜搭应用表新增行时漏填 `appKey` 或 `corpId`(钉钉系) - **THEN** mjava-pro 启动该 tenant 路由时必须显式拒绝,返回明确错误(如 `400 TENANT_CONFIG_INCOMPLETE: appKey is required`) - **AND** 不允许「字段缺失但允许跑」的兼容兜底 #### Scenario: A 档客户业务表漏 tenant_id - **WHEN** A 档客户的 Java Repository 查询 SQL 缺 `tenant_id` 条件 - **THEN** JPA Interceptor 应强制注入 `WHERE tenant_id = :currentTenant` - **AND** 若 Repository 故意绕过(如原生 SQL 字符串),code review 必须打回 --- ### Requirement: R5 mjava-com 对外暴露白名单规则 mjava-com 作为对外 BaaS 网关,**不**自动暴露所有 mjava Client/Service 方法。暴露遵循: **白名单制**: - 在 `mjava-com/application.yml` 显式声明 `gateway.expose: [{vendor}.{action}, ...]` - 未列入即返 404(不返 405,避免泄露内部能力清单) **危险动作默认禁开**: - 凡 `action` 名含 `delete` / `remove` / `drop` / `truncate` 的方法 - 凡 `action` 名含 `batch` + 写动作(`update` / `create` / `delete`)的方法 - 凡涉及租户管理(`tenant.create` / `tenant.delete` / `tenant.update`)的方法 上述方法即使配置在 `gateway.expose` 也必须由主仓维护者在 PR 中显式 ACK 才能合并。 **限流默认值**: - 每调用方默认 60 QPS(本地 `RateLimiter`,进程级,不跨实例) - 在 `gateway.rateLimit.{callerId}: {qps}` 按调用方覆盖 **审计强制**: - 每次调用打全字段审计日志:`{callerId, vendor, action, paramSummary, resultSummary, latencyMs, success}` - 审计日志不可关闭,不可降级到 DEBUG 级别 **暴露审批流程**: - 新增白名单条目 → PR + 主仓维护者 review - commit message 标 `feat(com): 开放 {vendor}.{action} 暴露` - 危险动作额外要求 PR 描述写明业务场景 + 调用方授权范围 #### Scenario: 配置中含危险动作 - **WHEN** PR 把 `dingtalk.user.delete` 加入 `gateway.expose` - **THEN** code review 必须打回,要求拆开作为「危险动作专项 PR」 - **AND** 专项 PR 必须含业务场景说明 + 主仓维护者显式 ACK #### Scenario: 调用方未配限流 - **WHEN** 新调用方 `callerId=xyz` 首次调用 mjava-com - **THEN** 默认走 60 QPS 限流 - **AND** 若实际业务需要更高 QPS,走 PR 改 `gateway.rateLimit.xyz` --- ### Requirement: R6 C 档独立仓与 mjava-ai 解耦边界 C 档客户仓**不**通过 maven 依赖引入 mjava-ai 发布的 jar,而是 fork 基座源码到客户仓内(如 `cur/mjava-guangming/mjava/`)。 **fork 基座规则**: - 客户仓 `mjava/pom.xml` 的 `version` 保持与上游同步(fork 时刻的上游版本号),后续分叉改动**不**改版本号 - 客户仓 README 写明 fork 来源 commit hash + 上游仓库 URL - 客户仓 mjava 包结构保持 `com.malk.*`(不改 package),便于 git diff 跨仓比较 **演进解耦**: - 上游 mjava-ai 的基座演进**不自动**同步到 C 档客户仓 - C 档客户仓的基座分叉**不自动**回流到上游 - 双向同步由各仓工程师按需手动 cherry-pick **R4(接口变更确认)跨仓 grep 范围**: - mjava-ai 仓内 R4 grep 范围 = mjava-ai 本仓(基础建设期) - C 档客户仓内 R4 grep 范围 = 客户仓本仓 - **两侧不互相联动**:mjava-ai 改基座接口签名时,不需要扫 C 档客户仓;C 档客户仓改自己基座副本时,不需要扫 mjava-ai **回流通道(可选,不强制)**: - C 档客户仓的工程师若认为某改动有上游回流价值(修 bug / 加产品方接口),鼓励提交 PR 回 mjava-ai - 回流走标准 OpenSpec change 流程(不绕开) #### Scenario: C 档客户仓 fork 时刻 - **WHEN** 升档 B → C 时把 mjava-ai 的 `mjava/` 复制进客户仓 - **THEN** 客户仓 `mjava/pom.xml` 保留上游版本号(如 0.0.3) - **AND** 客户仓 README 记录 fork 来源 commit hash - **AND** 不在客户仓建「上游同步」cron / CI #### Scenario: 上游 mjava-ai 改 DDClient 接口签名 - **WHEN** mjava-ai 内 `DDClient.foo(String)` 改签名为 `DDClient.foo(String, Boolean)` - **THEN** R4 grep 仅扫 mjava-ai 本仓 - **AND** **不**扫 `cur/mjava-guangming/` 或 akds 等 C 档客户仓 - **AND** C 档客户仓的同名接口(若已 fork)由客户仓工程师按需自主升级,不阻塞上游 #### Scenario: C 档客户仓回流改动 - **WHEN** `cur/mjava-guangming/mjava/` 内修了一个上游也存在的 bug - **THEN** 鼓励但不强制提 PR 回 mjava-ai - **AND** 回流走 OpenSpec change 流程,不绕开 --- ### Requirement: R7 README / CLAUDE.md 同步更新 本 change archive 后,mjava-ai 仓内文档必须同步: **README.md**: - 「子项目速览」表追加「档位」列:`mjava-mcli` 标 `B 模板`、`mjava-pro` 标 `A 容器`、`mjava-com` 标 `—` - 「新客户接入」段改写为「先按 customer-tiering 决策树选档 → 按档执行 SOP」,详细判定不再展开(避免与 spec 重复) **CLAUDE.md**: - 「快速操作」加一行:「**新客户接入**:先查 `openspec/specs/customer-tiering/spec.md` R2 决策树选档 → 按档执行 SOP」 **共享后端规范**(`/Users/malk/Desktop/Tech/claude/后端/CLAUDE.md`): - 在「子项目接入流程」段补 customer-tiering 锚点(与本 spec R2/R3 互锚) #### Scenario: README 子项目速览表更新 - **WHEN** 本 change archive - **THEN** README.md 「子项目速览」表必须含「档位」列 - **AND** 各模块标记与本 spec R1 一致 #### Scenario: 后续新增 B 档子模块 - **WHEN** 任何 B 档客户子模块(如 `mjava-akds`)合入 mjava-ai - **THEN** README.md 「子项目速览」表必须追加该子模块行,「档位」列标 `B` - **AND** 不允许新子模块合入但表内不登记