spec.md 13 KB

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 messagerefactor: 客户 {名} 升档 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 移除 <module>,删除目录
  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<version> 保持与 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 宜搭应用表新增行时漏填 appKeycorpId(钉钉系)
  • 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.xmlversion 保持与上游同步(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-mcliB 模板mjava-proA 容器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 不允许新子模块合入但表内不登记