Files
CarLog/docs/ARCHITECTURE.md
T
wsh5485 1dfa442ad1 docs: simplify architecture — single SPA + single DB + table prefix
按用户反馈重写 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% 场景。
2026-06-20 22:11:13 +08:00

16 KiB
Raw Blame History

平台化架构方案(单库 + 表前缀 + 单 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 schemaJSON 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_settingsCarLog 自己的设置继续用 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: 平台骨架(极简版)

  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_ 前缀:

-- 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+: 加第二个子系统(健身)

  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. 风险 & 缓解

风险 缓解
表前缀迁移漏表 写一个 Python 脚本扫描所有 SQL,从 FROM / JOIN / UPDATE / INSERT INTO 提取表名,自动检查遗漏
子系统相互依赖(A 写 B 的表) 代码 review / 加一个 lint 规则:禁止跨 subsystem_ 前缀的 SQL
表越来越多 DB 卡 单用户场景下不可能;真到瓶颈(几万条)再分库
总设置 vs 子系统设置混淆 key 命名约定:总设置无前缀,子系统设置 {subsystem}.{key}

10. 立即动手清单

  1. 本周写 migration 0019_platform.sqlsubsystems 表 + platform_settings 表 + seed CarLog
  2. 本周写平台路由:settings / subsystems / dashboard 三个文件
  3. 本周写平台前端:GlobalSettings + SubsystemSettings + 改 AppLayout 动态菜单
  4. 下下周做表前缀迁移 0020_carlog_prefix.sql + 改 server SQL
  5. 下下周跑完整测试套件 + 手动 E2E

简单胜过复杂。元数据驱动(manifest / settings_schema / nav_items 三个 JSON 字段)覆盖 80% 场景,剩下的真要 iframe / SSO 时再说。