Files
CarLog/docs/ARCHITECTURE.md
T
wsh5485 9d6fb59fd1 docs: add Platform+Subsystem architecture proposal
12 节方案:
1. 目标
2. 顶层架构(Platform 在中间 / N 个 Subsystem 围着)
3. Subsystem Manifest 协议(6 个标准端点 + JSON Schema 设置)
4. SSO(JWT + 30min 短期 token)
5. 数据模型(Platform DB carplatform 5 张表 / 子系统各自独立 DB)
6. 集成方式对比(推荐 iframe 嵌入 + 独立部署 fallback)
7. 5 步改造路线图
8. 技术选型
9. 风险 & 缓解
10. Roadmap(W1~W8+)
11. 不做的事
12. 立即动手清单

决策依据:
- Q1: 10+ 子系统 → 完整的平台层
- Q2: 同构为主偶尔异构 → manifest + 6 端点语言无关
- Q3: 基座 DB + 子系统独立 DB
- Q4: 永远单用户 → 不要 RBAC / 多租户
2026-06-20 22:01:54 +08:00

365 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 平台化架构方案(Platform + Subsystem
## 1. 目标
把现在这套洗车管理系统(CarLog)从独立 app 升级成「生活操作系统」的子系统之一。整体目标:
- **平台层(Platform)**:一个薄壳。管身份 / 导航 / 子系统注册 / 总设置 / 跨子系统跳转
- **子系统(Subsystem**:每个子系统是独立 web app,跑自己业务。CarLog 是第一个,将来加「健身」「阅读」「理财」……
- **UI 保持不变**:CarLog 的界面原样,只是从平台跳进去(类似 Google Workspace 里 Drive → Docs 的体验)
- **同构优先**:主用 Vue + Express,但协议设计成语言无关,Python / Go / 静态站也能挂
- **永远单用户**:不需要复杂的权限模型,但保留将来扩展可能
## 2. 顶层架构
```
┌─────────────────────────────────────────────────────────────────┐
│ Platform (大基座) │
│ │
│ ┌─────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ │
│ │ 登录 │ │ 总设置 │ │ 子系统 │ │ 跨子系统 │ │
│ │ /login │ │ /settings│ │ 注册中心 │ │ 跳转 / SSO │ │
│ └─────────┘ └──────────┘ └──────────┘ └────────────┘ │
│ │
│ Platform DB (MySQL @ 162.14.110.130:33306/carplatform) │
│ - users / sessions │
│ - subsystems (注册表 + manifest 缓存) │
│ - platform_settings (UI 主题 / 语言 / 全局开关) │
│ - shortcuts (跨子系统跳转配置) │
└─────────────────────────────────────────────────────────────────┘
┌───────────────────────┼───────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ CarLog │ │ Fitness │ │ Reading │
│ Subsystem │ │ Subsystem │ │ Subsystem │
│ │ │ │ │ │
│ 独立部署 │ │ 独立部署 │ │ 独立部署 │
│ 独立 DB │ │ 独立 DB │ │ 独立 DB │
│ 自有 UI │ │ 自有 UI │ │ 自有 UI │
│ 自有设置 │ │ 自有设置 │ │ 自有设置 │
│ manifest.yaml │ │ manifest.yaml │ │ manifest.yaml │
└───────────────┘ └───────────────┘ └───────────────┘
localhost:8788 localhost:8789 localhost:8790
```
**集成方式**:平台用 iframe 嵌入子系统核心页面(保留子系统 UI 不变)。子系统也提供纯 API 模式,平台可以选择 iframe / 直接跳独立域名。
## 3. 子系统协议(Subsystem Manifest
每个子系统在 `/subsystem-manifest.yaml`(或 `.json`)声明身份。平台启动时拉取 + 缓存到 `subsystems` 表。
```yaml
# subsystems/carlog/subsystem-manifest.yaml
id: carlog # 唯一 ID(注册时校验)
name: 洗车管理系统
version: 2.8.0
description: 给自己的私家车记账
icon: 🚗 # emoji 或 URL
color: '#1B6EF3'
category: vehicle # vehicle / fitness / finance / reading / ...
# 平台需要调用的 6 个标准端点
endpoints:
health: /api/subsystem/health # GET, {ok, version, uptime}
manifest: /api/subsystem/manifest # GET, 完整 manifest
nav: /api/subsystem/nav # GET, {items: [{label, icon, path}]}
ssoVerify: /api/subsystem/sso/verify # POST {token}, {user_id, expires_at}
settings: /api/subsystem/settings # GET / PUT, 子系统自有设置 schema
proxy: /api/subsystem/proxy/* # 平台代理访问子系统 API(可选)
# 子系统定义自己的设置项(平台只展示,不解释)
settings_schema:
- key: default_city
label: 默认城市
type: select
options: [Beijing, Shanghai, Korla, ...]
default: Korla
- key: ai_provider
label: AI 识别 provider
type: select
options: [openai_compat, minimax_vl]
default: minimax_vl
# ... 任意 JSON Schema 字段
# 导航(在平台 Dashboard 上展示)
nav:
- label: 概览
path: /
icon: 🏠
- label: 洗车记录
path: /washes
icon: 🧽
- label: 加油
path: /refuels
icon:
# ...
# 子系统独立运行时的 URL(平台 iframe 嵌入)
entry_url: http://localhost:8788
# SSO 期望的 token 类型
auth:
type: jwt
issuer: platform
shared_secret: env.SUBSYSTEM_SSO_SECRET # 平台签发 JWT 用这个 secret
# 子系统专属权限 scope
scopes: [vehicles:read, vehicles:write, washes:write, ...]
```
## 4. SSO(单点登录)
**流程**
```
用户在平台登录
平台发 JWT: {sub: 'admin', iss: 'platform', exp: now+30min, scopes: [...]}
平台跳子系统(iframe / window.open):
URL: {entry_url}/auth/callback?token={JWT}
子系统验证 JWT(用 SUBSYSTEM_SSO_SECRET 校验签名)
子系统发自己的 session cookie(子系统自己管)
iframe 内正常操作
```
**关键约束**
- JWT 短期(30 分钟),过期前 iframe 内静默续期
- SUBSYSTEM_SSO_SECRET **不**写代码,每个子系统从 env 读
- 子系统不需要单独登录(直接 SSO);保留独立登录入口作为 fallback
## 5. 数据模型
### 5.1 Platform DB(基座)
复用现有的 MySQL,但**新建独立 database** `carplatform`(不污染现有 `carlog`)。
```sql
CREATE DATABASE IF NOT EXISTS carplatform
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE carplatform;
-- 用户(单用户,但保留 user_id 字段方便将来扩展)
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(64) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
display_name VARCHAR(100),
is_active TINYINT(1) DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_login_at DATETIME
) ENGINE=InnoDB;
-- Session(如果走 JWT 模式就不需要;但保留给 fallback)
CREATE TABLE sessions (
sid VARCHAR(64) PRIMARY KEY,
user_id INT NOT NULL,
expires_at DATETIME NOT NULL,
data JSON,
INDEX idx_expires (expires_at)
) ENGINE=InnoDB;
-- 子系统注册表
CREATE TABLE subsystems (
id VARCHAR(50) PRIMARY KEY, -- 'carlog' / 'fitness' / ...
name VARCHAR(100) NOT NULL,
version VARCHAR(20) NOT NULL,
description TEXT,
icon VARCHAR(20),
color VARCHAR(20),
category VARCHAR(50),
entry_url VARCHAR(255) NOT NULL, -- http://localhost:8788
manifest_url VARCHAR(255) NOT NULL, -- http://localhost:8788/subsystem-manifest.yaml
manifest_cache JSON, -- 平台启动时拉取缓存
enabled TINYINT(1) DEFAULT 1,
sort_order INT DEFAULT 0,
installed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
last_health_at DATETIME,
health_status VARCHAR(20) DEFAULT 'unknown' -- ok / warn / down / unknown
) ENGINE=InnoDB;
-- 平台级设置(UI 主题 / 语言 / 全局开关)
CREATE TABLE platform_settings (
key_name VARCHAR(100) PRIMARY KEY,
value TEXT,
description TEXT,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB;
-- 跨子系统跳转配置("看完车的保养,去看看日历里有没有预约")
CREATE TABLE shortcuts (
id INT AUTO_INCREMENT PRIMARY KEY,
from_system VARCHAR(50) NOT NULL,
from_path VARCHAR(255),
to_system VARCHAR(50) NOT NULL,
to_path VARCHAR(255),
label VARCHAR(100) NOT NULL,
icon VARCHAR(20),
sort_order INT DEFAULT 0
) ENGINE=InnoDB;
-- 初始数据
INSERT INTO users (username, password_hash, display_name) VALUES
('admin', '$2b$10$...', 'Admin'); -- 第一次启动创建
INSERT INTO platform_settings (key_name, value, description) VALUES
('ui.theme', 'auto', 'UI 主题:light / dark / auto'),
('ui.language', 'zh-CN', '界面语言'),
('subsystem.discovery', 'manual', '子系统发现方式:manual / auto');
```
### 5.2 子系统 DB
**完全不动**。CarLog 继续用现有的 `carlog` DB。其他子系统各自创建自己的 DB。
平台 DB 知道子系统存在 + 怎么访问,但**不**读写子系统业务数据。
## 6. 集成方式对比
| 方式 | 优点 | 缺点 | 适用 |
|---|---|---|---|
| iframe 嵌入 | 子系统 UI 100% 不变 / 独立 deploy | cookie 跨域 / iframe 通信麻烦 / UX 不连贯 | 短期最快出活 |
| Module Federation | 共享 chunk / SPA 体验一致 | Vue 生态 MF 支持弱 / 调试难 | 不推荐 |
| 子系统独立部署 + 平台跳转 | 简单 / 各自 deploy | 用户跳来跳去 | 现在没用 / 将来可作为 fallback |
| 平台写死子系统代码 | 部署最简 | 失去「独立子系统」的意义 | 不推荐 |
**推荐**:**iframe 嵌入为主,独立部署为辅**。CarLog 子系统自己跑(端口 8788),平台 Dashboard 用 iframe 嵌入 `/subsystems/carlog/index.html`
## 7. CarLog → 子系统改造步骤
### Phase 1:拆 CarLog 为独立 deploy(保留 UI 完全不变)
目标:把 CarLog 从根目录拆出来,但仍跑在原位置 + 原 URL,UI 不变。
```bash
# 在 git root
mkdir -p subsystems
mv client subsystems/carlog-client
mv server subsystems/carlog-server
# package.json / README / docs / migrations 等留在根目录
# subsystems/carlog/{client,server}/ 各自有 package.json
```
`subsystems/carlog/subsystem-manifest.yaml` + 6 个标准端点的 stub(先返 mock 数据)。
更新部署:CarLog 从 `127.0.0.1:8787` 改成 `127.0.0.1:8788`(端口错开,留给平台)。
### Phase 2:建 Platform 骨架
```bash
mkdir platform/{client,server}
```
Platform 极薄:
- `platform/server/`Express + 上面 5 张表的 CRUD + 子系统注册接口 + SSO token 签发
- `platform/client/`:Vue 3 壳子,左侧子系统菜单 + 右侧 iframe 嵌入当前子系统核心页
- 总设置页:UI 主题 / 语言 / 子系统启停
- 子系统注册页:填 URL → 平台 GET manifest → 校验 → 写 `subsystems`
### Phase 3:接 SSO
1. 平台登录成功 → 写平台 session + 发 JWT
2. Dashboard 加载 → 平台前端 GET `/api/subsystems?enabled=1` → 渲染左侧菜单
3. 用户点菜单项 → 平台前端把 JWT 写到 iframe 的 `src` URL 里:
```
http://localhost:8788/auth/callback?token=eyJhbGc...
```
4. 子系统 `/auth/callback` 端点:
- 校验 JWT 签名
- 在子系统自己的 session 里建 sessionuser_id = JWT.sub
- 重定向到 `?redirect=/` 或 manifest 里指定的路径
5. iframe 内子系统操作完成,回到平台(postMessage 或直接顶层跳转)
### Phase 4:通用子系统模板(scaffolding
为了「偶尔异构」(Python / Go / 静态站)也能接入,**manifest + 6 个端点** 是语言无关的。给个最小模板:
```bash
# Vue + Express
npx create-subsystem -t vue-express -n fitness
# Python + FastAPI
npx create-subsystem -t python-fastapi -n reading
# Static site
npx create-subsystem -t static -n blog
```
每个模板预置:
- `subsystem-manifest.yaml`(带 ID 替换占位符)
- 6 个端点的实现(health / manifest / nav / sso-verify / settings / proxy
- SSO callback 处理
- 一个示例页面(Hello from {subsystem_name}
- 部署 Dockerfile / 宝塔步骤
### Phase 5:跨子系统聚合(可选 v3.0)
Dashboard widget 系统:
- 平台前端提供 `<SubsystemWidget subsystem="carlog" type="recent_washes" />`
- widget 是个 Vue 组件,通过 postMessage 跟子系统 iframe 通信
- 子系统暴露 `window.parent.postMessage({type: 'widget_data', ...})` 响应
但**这个等 5+ 个子系统上线后再做**,过早抽象是万恶之源。
## 8. 关键技术选型
| 项 | 选型 | 理由 |
|---|---|---|
| 平台后端 | Express 4**和 CarLog 同构** | 复用现有工具链 / auth / csrf 中间件 |
| 平台前端 | Vue 3 + Vite**和 CarLog 同构** | 同上 |
| SSO token | JWTHS256 | 无状态 / 跨进程可验签 / 子系统不依赖平台 DB |
| JWT secret | env `PLATFORM_JWT_SECRET` | 不写代码 / 各子系统从 env 拿同一个 secret 即可验证 |
| 子系统发现 | 平台手动注册(Phase 1/ 平台 GET manifest URL 自动注册(Phase 2 | Phase 1 简单可控 / Phase 2 自动但需要重试机制 |
| 平台 DB | MySQL**复用现有连接** + 新 database `carplatform` | 同一个 MySQL server,不增加运维复杂度 |
| 子系统 DB | 各自独立 | 数据隔离 / 独立备份 |
| iframe 通信 | postMessage | 标准 API / 跨域安全 |
| 反向代理 | Nginx(**不动现有配置**,加平台子域名如 `app.img2img.com` | 子系统用子路径或子域名 |
## 9. 风险 & 缓解
| 风险 | 缓解 |
|---|---|
| cookie 跨域 | JWT 走 URL query,平台给每个子系统独立子域名,子系统不依赖 cookie 跨域 |
| 子系统挂掉平台跟着挂 | iframe 加载失败时平台 Dashboard 显示降级页面 + 「子系统不可用」提示 |
| JWT secret 泄漏 | 不写代码 / 部署时 env 注入 / 定期 rotate |
| 子系统版本兼容 | manifest 里 version 字段,平台校验最低支持版本 |
| 单点登录过多子系统 | JWT 30 分钟过期 + 静默续期;如果用户长时间不操作,重新登录 |
| 10+ 子系统的导航 UX | 按 category 分组(vehicle / fitness / finance / reading),折叠 / 搜索 |
| 平台 DB 变瓶颈 | 单用户场景下不可能;如果哪天变了,分表即可 |
## 10. Roadmap(按周排)
| 周 | 任务 | 验证 |
|---|---|---|
| W1 | Phase 1CarLog 拆目录 + 加 manifest stub + 改端口到 8788 | CarLog 独立跑 / UI 完全不变 |
| W2 | Phase 2 平台骨架:Express + 5 张表 + 子系统注册 API + Vue 壳 | 平台能启动 / 显示「暂无子系统」 |
| W3 | Phase 3 SSOJWT 签发 / iframe 嵌入 / 子系统 callback | 平台登录 → 点 CarLog → 看到 CarLog 内容 |
| W4 | Phase 4 scaffolding3 个模板(vue-express / python-fastapi / static | 用模板 1 分钟建出 fitness 子系统骨架 |
| W5+ | 加第二个真子系统(fitness 或 reading)+ 测试跨子系统跳转 | 第二个子系统跑通 / 验证 v2 的 sso 流程 |
| W8+ | Phase 5 跨子系统聚合 widget**等 5+ 子系统再开始** | 暂缓 |
## 11. 不做的事(明确边界)
- ❌ 多租户 / RBAC(永远单用户)
- ❌ 子系统 marketplace / 第三方接入(只自用)
- ❌ 复杂插件系统 / Hookmanifest + 6 端点已经够用)
- ❌ 实时协同 / WebSocket(单用户不需要)
- ❌ 移动端原生 app(PWA 够了)
- ❌ 服务端渲染 SSRVue SPA + PWA 已经够)
## 12. 立即动手清单(按优先级)
1. **本周**:决定 CarLog 拆目录的具体路径(是 `subsystems/carlog/` 还是独立 git repo
2. **本周**:在现有 MySQL 建 `carplatform` database
3. **本周**:写平台 server 的 5 张表 migration
4. **下周**:开始 Phase 1 拆 CarLog
---
> 实施过程中如果发现某块要复杂得多(比如 iframe 通信坑很多),可以**降级到独立部署 + 平台跳转**——架构方案是手段不是目的,能让自己日常用得爽才是。