Files
i/docs/ARCHITECTURE.md
wsh5485 d7dab31f19 docs: add table prefix migration task (21 CarLog tables)
按用户决定「i系统内的carlog数据库表要加carlog_前缀」:
- 之前方案是「留给将来加第二个子系统时」, 现在提前做
- 新增 Task 2.8 表前缀迁移 (含 5 个子节 + review checklist)
- DEV-PLAN.md:
  - 关键决策表「本阶段不做」→「本阶段做」
  - 加 Task 2.8: 备份 + migration 020 + 改 server SQL + reset-all.js + 验证
  - 加 §6.2.11 表前缀迁移 review checklist (10 项)
  - 「不做的事」: 删掉表前缀
  - 「完成定义」: 加 Task 2.8
- ARCHITECTURE.md:
  - §3 决策表同步
  - §7 W2 改成完整 RENAME TABLE 21 张 + 备份警告
  - 删掉过时的 db.js tableName helper 建议
- README.md:
  - 决策表同步
  - 实施路线按 Phase 1 / 2 / 2.8 / 3 重新分组
  - 删掉「暂不做: CarLog 表前缀」

迁移策略:
- 21 张 CarLog 业务表加 carlog_ 前缀 (含 carlog_settings)
- 5 张共享表不动 (users / login_attempts / auth_locks / schema_migrations / 平台表)
- 改前必做 mysqldump 备份 (RENAME 是 DDL 不能事务回滚)
- 直接改 SQL (不加 helper 函数, 20 张表工作量可控)
- sed \\b word boundary 安全替换 (列了完整 21 张表清单, 不动共享表 4 张)
2026-06-20 23:06:12 +08:00

345 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 架构方案:单 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 schemaJSON 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_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
│ │ └── 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 helperbaseURL = /api/carlog
├── router/index.js
└── stores/
├── auth.js
└── platform.js # 总设置 / 子系统列表 / 当前激活子系统
```
### 6.3 子系统如何在平台菜单里显示
```vue
<!-- 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(() => {
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 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)必须**
- ✅ 用 `<AppLayout>` 包整个页面
- ✅ 颜色 / 字体 / 间距全部走 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.sqlsubsystems 表 + 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 时再说。