docs: 多租户 SaaS 化设计(收敛版 SAAS_DESIGN.md)

前提:真·多租户商业化 + 桌面端仍是主产品(非 Web-first)。
对 GPT 重构方案的收敛替代:保留正确诊断(tenant_id前置/usage先落事件/subject规范/migration),
砍掉过度设计(Web重写/7服务拆分/Temporal/Remote MCP/独立Tool Policy)。
核心:保架构、穿一条多租户+计费脊柱、按需求拉动分阶段(P1地基tenant_id→P2用量→P3客户端租户化→P4enforcement→P5支付+硬化)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-06 16:47:11 +08:00
parent 53f7e172c3
commit 2f19e322e3
+156
View File
@@ -0,0 +1,156 @@
# sundynix-agentix · 多租户 SaaS 化设计(收敛版)
> 版本:v1 日期:2026-07-06
> 前提(已确认):**① 目标是真·多租户商业化 ② 桌面端仍是主产品**(不做 Web-first 重写)
> 基线:`ARCHITECTURE_DESIGN.md`(当前架构)+ `ARCHITECTURE_REVIEW.md`(评审)
> 立场:这是对 `sundynix-agentix-saas-refactor-plan.md`GPT 方案)的**收敛替代**——保留其正确诊断,
> 砍掉与本项目阶段/定位不符的过度设计。原则:**保架构、按需求拉动、每步可回退,不一次大重构。**
---
## 0. 一句话设计
**不改架构骨架,只往里穿一条"多租户 + 计费"的脊柱。** 桌面端保持主产品、变租户感知;加一个很薄的
Web 面做注册/计费/团队(桌面端不适合放这些)。后端按"地基→用量→enforcement→支付"分阶段长出 SaaS 能力,
**每一块都由验证过的需求拉动,而不是提前把 7 个服务的底座全建出来。**
---
## 1. 与 GPT 方案的取舍(明确保留 / 砍掉)
| GPT 方案主张 | 我的判断 | 理由 |
|---|---|---|
| **tenant_id 全链路前置** | ✅ **采纳,最高优先** | 现在便宜、以后要命;多租户地基 |
| **Usage Meter 先落事件(不接支付)** | ✅ **采纳** | 事件溯源式用量→再计费,顺序对;现 billing 太薄 |
| **NATS subject 规范化 + Envelope(带 tenant_id)** | ✅ **采纳(轻量)** | 做多租户本就要给消息带 tenant,顺手;但**不拆多根总线** |
| **AutoMigrate → migration 工具** | ✅ **采纳** | 生产必须、可控变更 |
| Web App 主产品化(重写前端) | ❌ **砍掉** | **你定了桌面端为主**;改为桌面端租户感知 + 薄 Web 面 |
| 拆 7 个物理服务(task-runtime/model-gateway/tool-policy 独立服务) | ⚠️ **降级为"包/边界",不拆服务** | 单团队扛不动 7 服务运维;先做成 dispatcher 内的清晰模块,真扛不住再拆 |
| Task Runtime 独立层 + Temporal | ❌ **砍掉(现阶段)** | 现有 FSM+JetStream+checkpoint 够用;加 cancel/retry 收口即可,别上 Temporal |
| Remote MCP GatewayOAuth/registry 整套) | ❌ **砍掉(现阶段)** | 远程 MCP 市场是远期产品赌注,无近期需求不建 |
| Tool Policy 强制拦每次工具调用 | ⚠️ **降级** | 内部工具先只做 tenant 作用域 + 用量计数;策略引擎等有不可信工具再上 |
**核心分歧**:GPT 把它当"推翻重来做成标准 SaaS 平台";我把它当"给现有平台穿一条多租户脊柱"。
---
## 2. 多租户数据模型(Phase 1 地基)
### 2.1 层级
```
Tenant(组织/账户,计费单位)
└── Workspace(工作区)
└── Project(项目)
└── Task / KB / Agent / ...
User ── (via TenantMember, 带 role) ── Tenant
```
桌面端主产品下,多数个人用户 = 一个单人 Tenant(免费档);团队/企业 = 多成员 Tenant。
### 2.2 新增表(雪花 id + BaseModel,沿用 `sundynix_` 前缀)
- `sundynix_tenant`(id, name, slug, plan, status)
- `sundynix_tenant_member`(tenant_id, user_id, role, status) — 唯一(tenant_id,user_id)
- `sundynix_workspace`(id, tenant_id, name, created_by)
- `sundynix_project`(id, tenant_id, workspace_id, name)
### 2.3 tenant_id 全链路(这是重点,也是最大工作量)
现有 owner_id 隔离 → 升级为 tenant 作用域。**所有核心表加 `tenant_id`**Task/Eval/KB/Doc/DocLink/Agent/Prompt/AuditLog/GuardrailEvent…)+ 索引。隔离规则:
| 层 | 现状 | 加 tenant |
|---|---|---|
| PostgreSQL | owner 过滤 | 全表加 tenant_id**所有查询默认带 tenant_id**(用 gorm scope 统一强制,防漏) |
| Redis key | 无前缀 | `sundynix:{tenant_id}:...`(限流/流/会话) |
| NATS envelope | 裸 payload | 统一 Envelope 带 tenant_id(见 §3 |
| MinIO path | 平铺 | `tenants/{tenant_id}/workspaces/{ws}/projects/{proj}/...` |
| Milvus | kb 字段 | collection 不变,加 tenant 到 metadata filter(或 partition |
| Neo4j | kb 属性 | 节点/关系加 `tenant_id` 属性 |
> ⚠️ 最易漏的是"查询默认带 tenant_id"。用一个**统一的 gorm scope + code review 清单**强制,别靠人肉记。
### 2.4 RBACPhase 1 同期)
角色:`owner / admin / member / viewer / billing_admin`。权限动作(示例):
`tenant.manage / billing.manage / workspace.create / project.create / task.run / task.approve / model.manage / tool.use / kb.write / audit.read`
实现:中间件从 JWT 取 user → 查 TenantMember 得 role → 按 role→permission 表判权。
`RequireAdmin` 升级为按 `tenant.manage` 权限判定(替代现在的开发期放行 + env 白名单)。
---
## 3. NATS Envelope + subjectPhase 1,轻量)
**不拆总线**(保持单 NATS,HA 是 T3)。只做两件卫生:
- 统一 `Envelope{id,type,version,tenant_id,workspace_id,user_id,task_id,trace_id,created_at,payload}` 包裹消息。
- subject 按语义规范前缀(现已半规范):`cmd.* / events.* / streams.* / tools.* / control.* / health.*`
命令/事件区分先做最小:任务提交=command,状态变更落 `task_events`(见 §4)。
---
## 4. 任务运行收口(Phase 1,轻量——不做独立服务)
不建 Task Runtime 服务、不上 Temporal。现有 FSM + JetStream + checkpoint 上补:
-`task_events`(tenant_id, task_id, run_id, event_type, payload, trace_id) 事件日志(可回放 + 计费溯源)。
-**cancel / retry**(现有 resume/超时已在)——在 dispatcher 消费侧加取消信号 + 幂等重试。
- 这些是 dispatcher 内的能力增强,不新增服务。
---
## 5. Usage & BillingPhase 2,事件优先、不急接支付)
### 5.1 先落用量事件
`sundynix_usage_event`(tenant_id, user_id, task_id, run_id, resource_type, quantity, unit, unit_price, cost, currency, created_at)。
计量点:**模型 tokenin/out/cached)· 工具调用次数 · 沙箱时长 · 解析页数 · 入库量 · 存储 · 任务时长**。
### 5.2 聚合
`sundynix_usage_daily_rollup`(tenant_id, date, resource_type, quantity, cost) —— 按天汇总,admin 展示成本。
### 5.3 分阶段
`①仅记 usage_event → ②按天 rollup + 后台成本可视 → ③套餐 quota + 超额限制 → ④接支付 → ⑤企业账单`
**现在只做 ①②**(把已有的 Redis token 计数升级为落库的 usage_event)。
---
## 6. Model GatewayPhase 4,扩展现有、不重建服务)
不拆独立服务。把现有 `llm.Pool`(已有 failover/熔断/缓存/热配置)**扩成 model runtime**
- 每次调用 emit `usage_event`token + 成本,按 tenant)。
- 按 tenant 套餐限制可用模型 + quotaBYOK(租户自带 key)。
- Provider Adapter 接口化(当前是 openai-compatible 单形态)便于接 vLLM/Ollama/未来 Claude。
真扛不住/要独立扩缩容再拆服务。**先在包边界内做清楚。**
---
## 7. Tool PolicyPhase 4,降级版)
不建独立 Policy 服务、不强制拦内部工具。分阶段:
- **现在**:工具调用带 tenant 作用域 + 计 usage_event(复用 §5)。
- **有不可信工具时**(远程 MCP / shell / 本地连接器):再上 allow/deny/approval/quota 的策略前置 + `tool_call_event` 审计。
---
## 8. 客户端(Phase 3,桌面端为主 + 薄 Web 面)
**不重写 Web App。**
- **桌面端(主产品)**:变**租户感知**——登录后带 tenant 上下文;所有数据 tenant 作用域;工作区/项目切换;团队成员看到共享资源。原生能力(文件/另存为)不变。
- **管理端(现有 React)**:长出**租户/成员/套餐/用量账单/审计**管理(运维 + 商业后台合一)。
- **新增薄 Web 面**(只做桌面端不适合的):注册/登录/组织创建、**订阅计费、团队邀请**——因为这些要在装桌面端**之前**就能用,且计费/组织管理放桌面端别扭。技术上可以是管理端的一部分,或一个极简独立页,**不是完整产品重写**。
---
## 9. 分阶段落地(我的收敛排序 vs GPT 的全家桶)
| 阶段 | 内容 | 为什么这个顺序 |
|---|---|---|
| **P1 地基**(先做,最关键) | tenant/workspace/project 表 + **tenant_id 全链路** + RBAC + 各存储隔离 + migration 工具 | 没这个,多租户什么都无从谈;schema 越小越便宜 |
| **P2 用量** | usage_event + daily rollup(模型/工具/存储计量),admin 成本可视 | 计费前必须先有可信用量;低风险 |
| **P3 客户端租户化** | 桌面端 tenant 上下文 + 管理端长出租户/账单页 + 薄 Web 注册/计费面 | 有了后端隔离才谈得上前端多租户;不重写 |
| **P4 enforcement** | 套餐 quota + model runtime 成本/quota/BYOK + 工具 tenant 作用域 | 有用量数据后才谈限额/套餐 |
| **P5 支付 + 硬化** | Stripe 接入 + NATS 集群 + DB HA + TLS= T3) | 有真实付费流量再付分布式硬化的税 |
**明确不做(现阶段)**:Web 前端重写、7 服务物理拆分、Temporal、Remote MCP Gateway、独立 Tool Policy 服务。
这些等"验证过的需求"拉动再上——不提前建地基。
---
## 10. 现在就能起手的第一刀
**P1 的 tenant_id 是整件事的地基,也是唯一"越早越省"的。建议第一步只做最小闭环:**
1.`tenant` + `tenant_member` 表;注册时给每个新用户建一个单人默认 tenant。
2. `contract` + 核心表加 `tenant_id`;migration 脚本给存量数据回填一个默认 tenant。
3. 一个统一的 gorm scope 强制所有查询带 tenant_id + 鉴权中间件注入 tenant 上下文。
4. 先只覆盖 PGRedis/MinIO/向量/图谱的 tenant 前缀随后一层层加),跑通"注册→建库→提交任务"在 tenant 作用域下端到端。
做完这一刀,多租户地基就立住了,后面 P2–P5 都能增量长上去。
---
*本设计对应「做什么」;对当前架构本身的评价见 `ARCHITECTURE_REVIEW.md`,当前架构描述见 `ARCHITECTURE_DESIGN.md`。*