# 架构方案:单 SPA + 单库 + 表前缀 ## 1. 目标 i 平台是一个「生活操作系统」:单 Vue SPA + 单 Express 进程 + 单 MySQL,按表前缀分多子系统,永远单用户。 - **不引入分布式复杂度**:不分库、不分进程、不分部署 - **数据隔离靠表前缀**:`carlog_*` / `fitness_*` / `reading_*` / ... - **元数据驱动 UI**:subsystem 注册到 `subsystems` 表,平台前端通用渲染设置页和导航 - **永远单用户**:管什么 RBAC / 多租户 理由:个人用,数据量小;分库分进程反而增加运维复杂度而没收益。 ## 2. 顶层架构 ``` ┌────────────────────────────────────────────────────────┐ │ Vue SPA (一个壳子) │ │ │ │ ┌──────────────────────────────────────────────────────┐│ │ │ 总设置 / 子系统管理 / Dashboard (平台层) ││ │ └──────────────────────────────────────────────────────┘│ │ │ │ ┌──────────────────────────────────────────────────────┐│ │ │ 🚗 CarLog | 💪 Fitness | 📚 Reading ││ │ │ 概览/洗车/加油 | 训练/打卡/计划 | 书/笔记/进度 ││ │ └──────────────────────────────────────────────────────┘│ │ │ └────────────────────────────────────────────────────────┘ │ axios + cookie session ▼ ┌────────────────────────────────────────────────────────┐ │ Express (一个进程) │ │ ├── /api/platform/* (总设置 / 子系统管理 / Dashboard) │ │ ├── /api/carlog/* (CarLog 子系统) │ │ └── /api/{future}/* (将来加的子系统) │ └────────────────────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────┐ │ 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 内;CarLog 已加 `carlog_` 前缀,将来加 fitness/reading 直接走 `fitness_*` / `reading_*`) | | 路由 | 子系统自己的路径空间(`/api/carlog/*` 现有;将来 `/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 注册表 平台层只管一张元数据表: ```sql 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): ```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 │ │ └── dashboard.js # GET /api/platform/dashboard (跨子系统聚合) │ └── subsystems/ │ └── carlog/ │ ├── index.js # 聚合导出 13 个 CarLog router │ └── routes/ # 13 个 CarLog 路由文件 └── ... ``` ### 6.2 前端 ``` client/src/ ├── AppLayout.vue # 左侧菜单按 category 分组 ├── views/ │ ├── Platform/ # 平台层 │ │ ├── GlobalSettings.vue │ │ ├── SubsystemSettings.vue # 通用渲染器 │ │ └── Subsystems.vue # 启停 + 注册 │ ├── Login.vue # i 平台统一登录 │ ├── Home.vue # Dashboard, 读 /api/platform/dashboard │ └── subsystems/ │ └── carlog/ # 20 个 CarLog view ├── api/ │ ├── client.js # 底层 axios 实例(auth interceptor + 解包) │ └── subsystems.js # carlogApi helper(baseURL = /api/carlog) ├── router/index.js └── stores/ ├── auth.js └── platform.js # 总设置 / 子系统列表 / 当前激活子系统 ``` ### 6.3 子系统如何在平台菜单里显示 ```vue ``` ## 7. 实施步骤 ### W1: 平台骨架(极简版) 1. 加 `subsystems` + `platform_settings` 两张表(migration 001_platform.sql) 2. CarLog 自己注册一条记录到 `subsystems` 表(migration 里 INSERT) 3. 加 `server/src/routes/platform/{settings,subsystems,dashboard}.js` 三个文件 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` 动态渲染导航 ### W2: 表前缀迁移(CarLog) 把所有 21 张 CarLog 业务表加 `carlog_` 前缀(共享表 `users / login_attempts / auth_locks / schema_migrations` 不动): ```sql -- migration 020_carlog_prefix.sql DROP TABLE IF EXISTS _weather_snapshots_new; -- 清理 0013 临时表 RENAME TABLE vehicles TO carlog_vehicles, wash_records TO carlog_wash_records, chemicals TO carlog_chemicals, insurance_records TO carlog_insurance_records, chemical_usage TO carlog_chemical_usage, refuel_records TO carlog_refuel_records, settings TO carlog_settings, maintenance_records TO carlog_maintenance_records, charging_records TO carlog_charging_records, weather_snapshots TO carlog_weather_snapshots, operation_logs TO carlog_operation_logs, record_tags TO carlog_record_tags, notifications TO carlog_notifications, wash_photos TO carlog_wash_photos, tags TO carlog_tags, user_achievements TO carlog_user_achievements, notification_prefs TO carlog_notification_prefs, grocy_sync_logs TO carlog_grocy_sync_logs, category_mappings TO carlog_category_mappings, chemical_inventory_log TO carlog_chemical_inventory_log; ``` **改 server SQL**:13 个路由文件 + reset-all.js 全部加前缀(用 sed word boundary `\b` 替换,**`users`/`login_attempts`/`auth_locks`/`schema_migrations` 不要替换**)。 > ⚠️ **改前先 mysqldump 备份**!RENAME 是 DDL 不能事务回滚。 **注意**: 直接改 SQL,**不加 helper 函数**(helper 让代码难读,20 张表工作量可控)。 ``` ### W3+(暂不规划) 用户还没决定下一子系统做什么。本阶段只做: 1. ✅ 平台基座(subsystems 表 + platform_settings + 3 个平台路由 + 3 个平台前端 + 元数据驱动菜单) 2. ✅ CarLog 子系统化(目录迁移 + 加 `/api/carlog/` 前缀 + 注册到 subsystems 表) 将来加新子系统时按这个流程: 1. 写子系统的表(带 `{subsystem}_` 前缀)+ 路由 + 前端 2. 注册到 `subsystems` 表 3. 平台菜单自动出现新子系统入口 4. 通用设置渲染器自动支持新子系统的 settings 5. 验证端到端流程 ## 7.5 UI 设计原则 **平台层 UI 100% 复用 CarLog 现有 UI,不另起设计。** CarLog 已经有完整设计系统: - 设计令牌:`client/src/style.css` 顶部 `:root`(颜色 / 圆角 / 字体 / 阴影 / 响应式断点) - 工具类:60+ 个(`.btn` / `.card` / `.input` / `.pill` / `.flex` / `.mt-3` / `.mobile-only` ...) - 组件:`AppLayout` / `AppHeader` / `StatCard` / `ChartBlock` / `MobileCardList` / `ConfirmDangerDialog` **平台层新页面(GlobalSettings / SubsystemSettings / Subsystems / Dashboard)必须**: - ✅ 用 `` 包整个页面 - ✅ 颜色 / 字体 / 间距全部走 CSS 变量(`var(--xxx)`)或工具类 - ✅ 复用现有组件(`StatCard` / `MobileCardList` / `ConfirmDangerDialog`) - ✅ 跟现有 `Settings.vue` 视觉一致(卡片 + form + 按钮 + 消息) **禁止**: - ❌ 引入新 UI 库(ant-design / element-plus / naive-ui / vuetify) - ❌ 写新 CSS 变量(除非有明确理由) - ❌ inline `style="color: ..."` - ❌ 自定义 button / card 样式 - ❌ 另起设计语言 完整规范见 [docs/UI-STYLE.md](UI-STYLE.md)(设计令牌 + 工具类速查 + 4 个平台 view 完整模板 + Mavis review 验收点)。 ## 8. 不做的事 - ❌ JWT / SSO / 跨域 auth - ❌ iframe 嵌入子系统 - ❌ 子系统独立部署 / 独立 DB - ❌ RBAC / 多用户 / 多租户 - ❌ manifest + 6 端点协议 - ❌ SSR / 原生 app - ❌ Marketplace / 插件化 - ❌ 跨子系统 widget(等 5+ 子系统再做) ## 9. 风险 & 缓解 | 风险 | 缓解 | |---|---| | 表前缀迁移漏表 | 写 Python 脚本扫描所有 SQL 提取表名检查 | | 子系统相互依赖(A 写 B 的表) | 代码 review + lint 规则禁止跨前缀 SQL | | 表越来越多 DB 卡 | 单用户不可能;几万条再分库 | | 总设置 vs 子系统设置混淆 | key 命名约定:总设置无前缀,子系统 `{sub}.{key}` | ## 10. 立即动手清单 1. **本周**写 migration 001_platform.sql(subsystems 表 + platform_settings 表 + seed CarLog) 2. **本周**写平台路由:settings / subsystems / dashboard 三个文件 3. **本周**写平台前端:GlobalSettings + SubsystemSettings + 改 AppLayout 动态菜单 4. **下下周**做表前缀迁移 + 改 server SQL 5. **下下周**跑完整测试套件 + 手动 E2E --- > 简单胜过复杂。元数据驱动(settings_schema / nav_items 两个 JSON 字段)覆盖 80% 场景,剩下的真要 iframe / SSO 时再说。