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

16 KiB
Raw Blame History

平台化架构方案(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 表。

# 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)。

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 不变。

# 在 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 骨架

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 个端点 是语言无关的。给个最小模板:

# 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(单用户不需要)
  • 移动端原生 appPWA 够了)
  • 服务端渲染 SSRVue SPA + PWA 已经够)

12. 立即动手清单(按优先级)

  1. 本周:决定 CarLog 拆目录的具体路径(是 subsystems/carlog/ 还是独立 git repo
  2. 本周:在现有 MySQL 建 carplatform database
  3. 本周:写平台 server 的 5 张表 migration
  4. 下周:开始 Phase 1 拆 CarLog

实施过程中如果发现某块要复杂得多(比如 iframe 通信坑很多),可以降级到独立部署 + 平台跳转——架构方案是手段不是目的,能让自己日常用得爽才是。