From 1dfa442ad1f06444521448fb6a715d856f1f8046 Mon Sep 17 00:00:00 2001 From: wsh5485 Date: Sat, 20 Jun 2026 22:11:13 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20simplify=20architecture=20=E2=80=94=20s?= =?UTF-8?q?ingle=20SPA=20+=20single=20DB=20+=20table=20prefix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按用户反馈重写 ARCHITECTURE.md: - 砍掉 JWT/SSO/iframe 嵌入/6 端点协议/独立 DB - 改成一个 Vue SPA + 一个 Express 进程 + 一个 MySQL DB - 表前缀隔离 carlog_/fitness_/reading_ - 平台层极薄:subsystems 元数据表 + settings_schema + nav_items - 通用子系统设置渲染器(JSON Schema 驱动) 实施步骤简化到 W1-W3+: - W1: 平台骨架(subsystems 表 + 3 个路由 + GlobalSettings UI) - W2: CarLog 表前缀迁移 - W3+: 加第二个真子系统验证流程 理由:单用户 + 个人场景不需要分布式复杂度,元数据驱动 UI 覆盖 80% 场景。 --- docs/ARCHITECTURE.md | 611 ++++++++++++++++++++----------------------- 1 file changed, 286 insertions(+), 325 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index d67bddb..e7cc0d9 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,365 +1,326 @@ -# 平台化架构方案(Platform + Subsystem) +# 平台化架构方案(单库 + 表前缀 + 单 Vue) ## 1. 目标 -把现在这套洗车管理系统(CarLog)从独立 app 升级成「生活操作系统」的子系统之一。整体目标: +把现在这套洗车管理系统(CarLog)升级成「生活操作系统」的第一个子系统,但**不引入分布式复杂度**: -- **平台层(Platform)**:一个薄壳。管身份 / 导航 / 子系统注册 / 总设置 / 跨子系统跳转 -- **子系统(Subsystem)**:每个子系统是独立 web app,跑自己业务。CarLog 是第一个,将来加「健身」「阅读」「理财」…… -- **UI 保持不变**:CarLog 的界面原样,只是从平台跳进去(类似 Google Workspace 里 Drive → Docs 的体验) -- **同构优先**:主用 Vue + Express,但协议设计成语言无关,Python / Go / 静态站也能挂 -- **永远单用户**:不需要复杂的权限模型,但保留将来扩展可能 +- **一个 MySQL 数据库**(复用现有 `carlog`) +- **一个 Express 进程**(所有子系统路由一起跑) +- **一个 Vue SPA**(同一个壳子,左侧菜单按子系统分组) +- **表前缀隔离**:`carlog_*` / `fitness_*` / `reading_*` / ... +- **永远单用户**:管什么 RBAC / 多租户 + +理由:你个人用,数据量小;分库分进程反而增加运维复杂度而没收益。 ## 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 +┌────────────────────────────────────────────────────────┐ +│ Vue SPA (一个壳子) │ +│ │ +│ ┌──────────────────────────────────────────────────────┐│ +│ │ 总设置 / 子系统管理 / Dashboard (平台层) ││ +│ └──────────────────────────────────────────────────────┘│ +│ │ +│ ┌──────────────────────────────────────────────────────┐│ +│ │ 🚗 CarLog | 💪 Fitness | 📚 Reading ││ +│ │ 概览/洗车/加油 | 训练/打卡/计划 | 书/笔记/进度 ││ +│ └──────────────────────────────────────────────────────┘│ +│ │ +└────────────────────────────────────────────────────────┘ + │ axios + cookie session + ▼ +┌────────────────────────────────────────────────────────┐ +│ Express (一个进程) │ +│ ├── /api/platform/* (总设置 / 子系统管理 / Dashboard) │ +│ ├── /api/vehicles (CarLog 路由, 保持现状) │ +│ ├── /api/washes (CarLog) │ +│ ├── /api/fitness/* (将来加的 Fitness 子系统) │ +│ └── /api/reading/* (将来加的 Reading 子系统) │ +└────────────────────────────────────────────────────────┘ + │ + ▼ +┌────────────────────────────────────────────────────────┐ +│ MySQL @ 162.14.110.130:33306 / carlog │ +│ │ +│ 平台表 (无前缀): │ +│ users / sessions / subsystems / platform_settings │ +│ │ +│ CarLog 表: │ +│ carlog_vehicles / carlog_wash_records / carlog_refuels │ +│ carlog_charging_records / carlog_maintenance_records │ +│ carlog_insurance_records / carlog_chemicals │ +│ carlog_weather_snapshots / carlog_operation_logs │ +│ carlog_tags / carlog_achievements / carlog_notifications│ +│ ... │ +│ │ +│ Fitness 表 (将来): │ +│ fitness_workouts / fitness_plans / fitness_measurements│ +│ │ +│ Reading 表 (将来): │ +│ reading_books / reading_notes / reading_progress │ +└────────────────────────────────────────────────────────┘ ``` -**集成方式**:平台用 iframe 嵌入子系统核心页面(保留子系统 UI 不变)。子系统也提供纯 API 模式,平台可以选择 iframe / 直接跳独立域名。 +## 3. 物理隔离靠什么 -## 3. 子系统协议(Subsystem Manifest) +| 隔离维度 | 做法 | +|---|---| +| 数据 | 表前缀 `{subsystem}_*`(同一 DB 内) | +| 路由 | 子系统自己的路径空间(`/api/{resource}` 现有;将来 `/api/fitness/*`) | +| 代码 | 子系统独立目录(`server/src/subsystems/{name}/`、`client/src/views/subsystems/{name}/`) | +| 设置 | 每个子系统有自己的 settings schema(JSON Schema,存 `platform_settings` 表,key 前缀 `{name}.*`) | +| 菜单 | 每个子系统在 `subsystems` 表注册,平台层根据 `category` 分组渲染左侧导航 | -每个子系统在 `/subsystem-manifest.yaml`(或 `.json`)声明身份。平台启动时拉取 + 缓存到 `subsystems` 表。 +**没有** JWT / SSO / iframe / 6 端点协议 / 独立 DB — 那些都是过度设计。 -```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 / ... +## 4. Subsystem 注册表 -# 平台需要调用的 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, + id VARCHAR(50) PRIMARY KEY, -- 'carlog' / 'fitness' / 'reading' + name VARCHAR(100) NOT NULL, -- 显示名:「洗车管理系统」 description TEXT, - icon VARCHAR(20), + icon VARCHAR(20), -- emoji 或文件名 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, -- 平台启动时拉取缓存 + category VARCHAR(50), -- vehicle / fitness / finance / reading + version VARCHAR(20), 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 + settings_schema JSON, -- JSON Schema 描述这个子系统的设置项 + nav_items JSON, -- [{label, icon, path}] + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) 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'); +INSERT INTO subsystems (id, name, icon, color, category, version, settings_schema, nav_items) VALUES +('carlog', '洗车管理系统', '🚗', '#1B6EF3', 'vehicle', '2.8.0', +'{"fields":[ + {"key":"weather.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"} +]}', +'[{"label":"概览","path":"/","icon":"🏠"},{"label":"洗车记录","path":"/washes","icon":"🧽"}]' +); ``` -### 5.2 子系统 DB +**注意**:subsystem 表里**不存业务数据**,只存「我是谁 / 我有什么设置 / 我在菜单里长啥样」。 -**完全不动**。CarLog 继续用现有的 `carlog` DB。其他子系统各自创建自己的 DB。 +## 5. 总设置 vs 子系统设置 -平台 DB 知道子系统存在 + 怎么访问,但**不**读写子系统业务数据。 +### 5.1 总设置(平台级) -## 6. 集成方式对比 +存 `platform_settings` 表,**不带前缀**: -| 方式 | 优点 | 缺点 | 适用 | -|---|---|---|---| -| 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 里建 session(user_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 系统: -- 平台前端提供 `` -- widget 是个 Vue 组件,通过 postMessage 跟子系统 iframe 通信 -- 子系统暴露 `window.parent.postMessage({type: 'widget_data', ...})` 响应 - -但**这个等 5+ 个子系统上线后再做**,过早抽象是万恶之源。 - -## 8. 关键技术选型 - -| 项 | 选型 | 理由 | +| key | value | 说明 | |---|---|---| -| 平台后端 | Express 4(**和 CarLog 同构**) | 复用现有工具链 / auth / csrf 中间件 | -| 平台前端 | Vue 3 + Vite(**和 CarLog 同构**) | 同上 | -| SSO token | JWT(HS256) | 无状态 / 跨进程可验签 / 子系统不依赖平台 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`) | 子系统用子路径或子域名 | +| `ui.theme` | auto / light / dark | UI 主题 | +| `ui.language` | zh-CN / en | 界面语言 | +| `dashboard.layout` | default / compact | Dashboard 布局 | +| `backup.enabled` | 1 / 0 | 自动备份开关 | +| `backup.path` | /path/ | 备份路径 | + +UI 在 `/settings/global`,平台前端渲染。 + +### 5.2 子系统设置 + +存 `platform_settings` 表,**带 `{subsystem}.` 前缀**: + +| key | 说明 | +|---|---| +| `carlog.weather.default_city` | 来自 CarLog manifest 的 settings_schema | +| `carlog.ai.provider` | 同上 | +| `carlog.grocy.url` | Grocy 实例 URL(将来从 settings 表迁过来) | +| `fitness.units.metric` | Fitness 子系统的「公制单位」开关 | +| `reading.sort.default` | Reading 子系统的默认排序 | + +UI 在 `/settings/{subsystem}`,平台前端**通用渲染器**(按 settings_schema 的 type 渲染 input / select / textarea): + +```js +// 通用渲染器伪代码 +async function loadSubsystemSettings(subsystemId) { + const sub = await api.get(`/platform/subsystems/${subsystemId}`); + const values = await api.get(`/platform/settings?prefix=${subsystemId}.`); + return { schema: sub.settings_schema, values }; +} +``` + +**好处**:加新子系统不用改平台前端代码,只写 settings_schema 就完事。 + +### 5.3 兼容现有 settings 表 + +CarLog 现在已经有 `settings` 表(21 个键),存了 AI / Grocy / 登录锁定等配置。**两种方案**: + +- (A) **保留 `settings` 表**,平台层加 `platform_settings` 表,新子系统的设置走 platform_settings,CarLog 自己的设置继续用 `settings` 表 +- (B) **迁移到 `platform_settings`**,所有子系统统一,CarLog 的 key 加 `carlog.` 前缀 + +**推荐 (A)**:迁移工作量小,CarLog 的现有逻辑不用改。platform_settings 主要管「平台级 UI 设置 + 将来新子系统的设置」。 + +## 6. 路由 / 代码目录重组 + +### 6.1 服务端 + +``` +server/src/ +├── index.js # 统一入口(不变) +├── routes/ +│ ├── platform/ # 新增:总设置 / 子系统管理 / Dashboard 聚合 +│ │ ├── settings.js # GET/PUT /api/platform/settings +│ │ ├── subsystems.js # GET /api/platform/subsystems, POST /api/platform/subsystems +│ │ └── dashboard.js # GET /api/platform/dashboard (聚合多子系统) +│ ├── vehicles.js # 现有(不改路径) +│ ├── washes.js # 现有 +│ ├── ... # 现有 +│ └── subsystems/ # 新增:将来每个子系统独立目录(可选) +│ └── (空 — CarLog 暂时还留在 routes/ 根) +└── ... +``` + +**第一阶段不搬**,路由保持现状。CarLog 现在叫 `/api/vehicles`,将来新子系统叫 `/api/fitness/workouts`,靠路径区分。 + +### 6.2 前端 + +``` +client/src/ +├── App.vue # 改:左侧菜单按 category 分组 +├── views/ +│ ├── Platform/ # 新增 +│ │ ├── GlobalSettings.vue # /settings/global +│ │ ├── SubsystemSettings.vue # /settings/:subsystem (通用渲染) +│ │ └── Subsystems.vue # /admin/subsystems (启停 + 注册) +│ ├── Home.vue # 改成读 /api/platform/dashboard +│ ├── WashesList.vue # 现有(不改路径) +│ └── ... # 现有 +├── router/ +│ └── index.js # 加 /settings/global, /settings/:subsystem, /admin/subsystems +└── stores/ + ├── auth.js # 现有 + └── platform.js # 新增:总设置 / 子系统列表 / 当前激活子系统 +``` + +### 6.3 子系统如何在平台菜单里显示 + +```vue + + + + +``` + +## 7. 实施步骤 + +### W1: 平台骨架(极简版) + +1. 加 `subsystems` + `platform_settings` 两张表(migration 0019_platform.sql) +2. CarLog 自己注册一条记录到 `subsystems` 表(migration 里 INSERT) +3. 加 `server/src/routes/platform/{settings,subsystems,dashboard}.js` 三个文件,每个 50-100 行 +4. 在 `index.js` 把这三个 router mount 到 `/api/platform` +5. 加 `client/src/views/Platform/GlobalSettings.vue`(总设置 UI) +6. 加 `client/src/views/Platform/SubsystemSettings.vue`(通用子系统设置渲染器) +7. AppLayout.vue 改成读 `platform.subsystems` 动态渲染导航 +8. 测试:登录 → 看到菜单 + 总设置 + 子系统设置 → 全部正常 + +预计改动量:~500 行新代码 + ~200 行改代码。 + +### W2: 表前缀迁移(CarLog) + +把所有 CarLog 表加 `carlog_` 前缀: + +```sql +-- migration 0020_carlog_prefix.sql +RENAME TABLE vehicles TO carlog_vehicles; +RENAME TABLE wash_records TO carlog_wash_records; +RENAME TABLE refuel_records TO carlog_refuels; -- 注意 refuels 改名了 +RENAME TABLE charging_records TO carlog_chargings; +-- ... 其他 11 张表 + +-- settings / users / sessions / operation_logs 不动(共享表) +``` + +**server 代码改动**:每个 SQL 把表名加上前缀。可以在 db.js 里加个 wrapper: + +```js +// db.js 加一个 helper +const TABLE_PREFIX = { + vehicles: 'carlog_vehicles', + wash_records: 'carlog_wash_records', + // ... +}; + +export function tableName(name) { + return TABLE_PREFIX[name] || name; +} +``` + +然后所有 route 文件 `db().all('SELECT * FROM ' + tableName('vehicles'))`。 + +**测试**:跑 101 个测试,看有没有崩;前端 E2E(手动测一遍)。 + +### W3+: 加第二个子系统(健身) + +1. 写 `subsystems.fitness` 子系统:3-5 张表 + 3-5 个路由 + 5-10 个前端 view +2. 注册到 `subsystems` 表 +3. 平台菜单自动出现 Fitness 入口(因为 `subsystems.nav_items` 字段) +4. 通用设置渲染器自动支持 Fitness 的 settings +5. 验证:能从平台菜单点进 Fitness,操作后回平台 + +预计改动量:~1000 行新代码(健身子系统本身)+ ~50 行平台代码(如果有需要)。 + +### 持续 + +每加一个子系统 ~1000 行。平台层不再增长(除非要加通用功能比如跨子系统聚合 widget)。 + +## 8. 跟之前方案的差异 + +| 维度 | 之前(v1 方案) | 现在(v2 方案) | +|---|---|---| +| 子系统部署 | 独立 web app + 独立进程 | 同一进程 + 路由前缀 | +| 数据隔离 | 独立 DB | 同一 DB + 表前缀 | +| UI 嵌入 | iframe 嵌入 | 同一个 Vue SPA(菜单聚合) | +| 认证 | JWT + SSO 30min 过期 | 同一个 cookie session(已存在的) | +| 子系统协议 | manifest + 6 端点 | `subsystems` 表 + `nav_items` + `settings_schema` 3 个 JSON 字段 | +| 复杂度 | 高(SSO / JWT / manifest 端点) | 低(就是元数据驱动的菜单渲染) | + +**为什么这个更好**:单用户 + 个人场景下,所有「分布式」方案都是为了将来多用户 / 多团队设计的,但对当前需求是过度工程。元数据驱动 + 物理目录隔离 + 表前缀,足以应对 10+ 个子系统。 ## 9. 风险 & 缓解 | 风险 | 缓解 | |---|---| -| cookie 跨域 | JWT 走 URL query,平台给每个子系统独立子域名,子系统不依赖 cookie 跨域 | -| 子系统挂掉平台跟着挂 | iframe 加载失败时平台 Dashboard 显示降级页面 + 「子系统不可用」提示 | -| JWT secret 泄漏 | 不写代码 / 部署时 env 注入 / 定期 rotate | -| 子系统版本兼容 | manifest 里 version 字段,平台校验最低支持版本 | -| 单点登录过多子系统 | JWT 30 分钟过期 + 静默续期;如果用户长时间不操作,重新登录 | -| 10+ 子系统的导航 UX | 按 category 分组(vehicle / fitness / finance / reading),折叠 / 搜索 | -| 平台 DB 变瓶颈 | 单用户场景下不可能;如果哪天变了,分表即可 | +| 表前缀迁移漏表 | 写一个 Python 脚本扫描所有 SQL,从 `FROM / JOIN / UPDATE / INSERT INTO` 提取表名,自动检查遗漏 | +| 子系统相互依赖(A 写 B 的表) | 代码 review / 加一个 lint 规则:禁止跨 subsystem_ 前缀的 SQL | +| 表越来越多 DB 卡 | 单用户场景下不可能;真到瓶颈(几万条)再分库 | +| 总设置 vs 子系统设置混淆 | key 命名约定:总设置无前缀,子系统设置 `{subsystem}.{key}` | -## 10. Roadmap(按周排) +## 10. 立即动手清单 -| 周 | 任务 | 验证 | -|---|---|---| -| W1 | Phase 1:CarLog 拆目录 + 加 manifest stub + 改端口到 8788 | CarLog 独立跑 / UI 完全不变 | -| W2 | Phase 2 平台骨架:Express + 5 张表 + 子系统注册 API + Vue 壳 | 平台能启动 / 显示「暂无子系统」 | -| W3 | Phase 3 SSO:JWT 签发 / iframe 嵌入 / 子系统 callback | 平台登录 → 点 CarLog → 看到 CarLog 内容 | -| W4 | Phase 4 scaffolding:3 个模板(vue-express / python-fastapi / static) | 用模板 1 分钟建出 fitness 子系统骨架 | -| W5+ | 加第二个真子系统(fitness 或 reading)+ 测试跨子系统跳转 | 第二个子系统跑通 / 验证 v2 的 sso 流程 | -| W8+ | Phase 5 跨子系统聚合 widget(**等 5+ 子系统再开始**) | 暂缓 | - -## 11. 不做的事(明确边界) - -- ❌ 多租户 / RBAC(永远单用户) -- ❌ 子系统 marketplace / 第三方接入(只自用) -- ❌ 复杂插件系统 / Hook(manifest + 6 端点已经够用) -- ❌ 实时协同 / WebSocket(单用户不需要) -- ❌ 移动端原生 app(PWA 够了) -- ❌ 服务端渲染 SSR(Vue SPA + PWA 已经够) - -## 12. 立即动手清单(按优先级) - -1. **本周**:决定 CarLog 拆目录的具体路径(是 `subsystems/carlog/` 还是独立 git repo) -2. **本周**:在现有 MySQL 建 `carplatform` database -3. **本周**:写平台 server 的 5 张表 migration -4. **下周**:开始 Phase 1 拆 CarLog +1. **本周**写 migration 0019_platform.sql(subsystems 表 + platform_settings 表 + seed CarLog) +2. **本周**写平台路由:settings / subsystems / dashboard 三个文件 +3. **本周**写平台前端:GlobalSettings + SubsystemSettings + 改 AppLayout 动态菜单 +4. **下下周**做表前缀迁移 0020_carlog_prefix.sql + 改 server SQL +5. **下下周**跑完整测试套件 + 手动 E2E --- -> 实施过程中如果发现某块要复杂得多(比如 iframe 通信坑很多),可以**降级到独立部署 + 平台跳转**——架构方案是手段不是目的,能让自己日常用得爽才是。 \ No newline at end of file +> 简单胜过复杂。元数据驱动(manifest / settings_schema / nav_items 三个 JSON 字段)覆盖 80% 场景,剩下的真要 iframe / SSO 时再说。 \ No newline at end of file