按用户反馈重写 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% 场景。
16 KiB
平台化架构方案(单库 + 表前缀 + 单 Vue)
1. 目标
把现在这套洗车管理系统(CarLog)升级成「生活操作系统」的第一个子系统,但不引入分布式复杂度:
- 一个 MySQL 数据库(复用现有
carlog) - 一个 Express 进程(所有子系统路由一起跑)
- 一个 Vue SPA(同一个壳子,左侧菜单按子系统分组)
- 表前缀隔离:
carlog_*/fitness_*/reading_*/ ... - 永远单用户:管什么 RBAC / 多租户
理由:你个人用,数据量小;分库分进程反而增加运维复杂度而没收益。
2. 顶层架构
┌────────────────────────────────────────────────────────┐
│ 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 │
└────────────────────────────────────────────────────────┘
3. 物理隔离靠什么
| 隔离维度 | 做法 |
|---|---|
| 数据 | 表前缀 {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 分组渲染左侧导航 |
没有 JWT / SSO / iframe / 6 端点协议 / 独立 DB — 那些都是过度设计。
4. Subsystem 注册表
平台层只管一张元数据表:
CREATE TABLE subsystems (
id VARCHAR(50) PRIMARY KEY, -- 'carlog' / 'fitness' / 'reading'
name VARCHAR(100) NOT NULL, -- 显示名:「洗车管理系统」
description TEXT,
icon VARCHAR(20), -- emoji 或文件名
color VARCHAR(20),
category VARCHAR(50), -- vehicle / fitness / finance / reading
version VARCHAR(20),
enabled TINYINT(1) DEFAULT 1,
sort_order INT DEFAULT 0,
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;
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":"🧽"}]'
);
注意:subsystem 表里不存业务数据,只存「我是谁 / 我有什么设置 / 我在菜单里长啥样」。
5. 总设置 vs 子系统设置
5.1 总设置(平台级)
存 platform_settings 表,不带前缀:
| key | value | 说明 |
|---|---|---|
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):
// 通用渲染器伪代码
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 子系统如何在平台菜单里显示
<!-- AppLayout.vue 左侧导航 -->
<template v-for="cat in groupedCategories" :key="cat">
<div class="nav-category">{{ cat.label }}</div>
<router-link v-for="item in cat.items" :key="item.path" :to="item.path">
{{ item.icon }} {{ item.label }}
</router-link>
</template>
<script setup>
import { computed } from 'vue';
import { usePlatformStore } from '../stores/platform';
const platform = usePlatformStore();
const groupedCategories = computed(() => {
// platform.subsystems 按 category 分组
const groups = {};
for (const sub of platform.subsystems.filter(s => s.enabled)) {
if (!groups[sub.category]) groups[sub.category] = { label: sub.category, items: [] };
for (const nav of (sub.nav_items || [])) {
groups[sub.category].items.push({ ...nav, subsystem: sub.id });
}
}
return Object.values(groups);
});
</script>
7. 实施步骤
W1: 平台骨架(极简版)
- 加
subsystems+platform_settings两张表(migration 0019_platform.sql) - CarLog 自己注册一条记录到
subsystems表(migration 里 INSERT) - 加
server/src/routes/platform/{settings,subsystems,dashboard}.js三个文件,每个 50-100 行 - 在
index.js把这三个 router mount 到/api/platform - 加
client/src/views/Platform/GlobalSettings.vue(总设置 UI) - 加
client/src/views/Platform/SubsystemSettings.vue(通用子系统设置渲染器) - AppLayout.vue 改成读
platform.subsystems动态渲染导航 - 测试:登录 → 看到菜单 + 总设置 + 子系统设置 → 全部正常
预计改动量:~500 行新代码 + ~200 行改代码。
W2: 表前缀迁移(CarLog)
把所有 CarLog 表加 carlog_ 前缀:
-- 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:
// 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+: 加第二个子系统(健身)
- 写
subsystems.fitness子系统:3-5 张表 + 3-5 个路由 + 5-10 个前端 view - 注册到
subsystems表 - 平台菜单自动出现 Fitness 入口(因为
subsystems.nav_items字段) - 通用设置渲染器自动支持 Fitness 的 settings
- 验证:能从平台菜单点进 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. 风险 & 缓解
| 风险 | 缓解 |
|---|---|
| 表前缀迁移漏表 | 写一个 Python 脚本扫描所有 SQL,从 FROM / JOIN / UPDATE / INSERT INTO 提取表名,自动检查遗漏 |
| 子系统相互依赖(A 写 B 的表) | 代码 review / 加一个 lint 规则:禁止跨 subsystem_ 前缀的 SQL |
| 表越来越多 DB 卡 | 单用户场景下不可能;真到瓶颈(几万条)再分库 |
| 总设置 vs 子系统设置混淆 | key 命名约定:总设置无前缀,子系统设置 {subsystem}.{key} |
10. 立即动手清单
- 本周写 migration 0019_platform.sql(subsystems 表 + platform_settings 表 + seed CarLog)
- 本周写平台路由:settings / subsystems / dashboard 三个文件
- 本周写平台前端:GlobalSettings + SubsystemSettings + 改 AppLayout 动态菜单
- 下下周做表前缀迁移 0020_carlog_prefix.sql + 改 server SQL
- 下下周跑完整测试套件 + 手动 E2E
简单胜过复杂。元数据驱动(manifest / settings_schema / nav_items 三个 JSON 字段)覆盖 80% 场景,剩下的真要 iframe / SSO 时再说。