Files
i/docs/ARCHITECTURE.md
T
wsh5485 a365c8be10 docs: clarify /api/carlog/* prefix + drop W3+ fitness plan
按用户反馈调整:

1. 路由前缀改成 /api/carlog/* (不再是 /api/vehicles 等)
   - DEV-PLAN.md Task 2.2 mount 全部加前缀
   - 新增 Task 2.3.5: 前端 carlogApi helper
   - ARCHITECTURE.md 架构图 + 6.1/6.2 代码结构更新
   - README.md 同步

2. 删除 W3+ 健身子系统计划 (用户还没决定下一子系统)
   - ARCHITECTURE.md W3+ 章节改成 "暂不规划"
   - README.md 实施路线只保留当前阶段
   - DEV-PLAN.md "不做的事" 显式列出来

3. 加 Mavis review checklist (6.2/6.3/6.4)
   - 10 项检查清单 (DB/后端/前端/API baseURL/E2E/数据完整性)
   - 10 个常见坑 (JSON parse / TINYINT boolean / mount 顺序 ...)
   - review 通过标准
2026-06-20 22:43:43 +08:00

307 lines
14 KiB
Markdown
Raw 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 还没加前缀,留给将来加第二个子系统时) |
| 路由 | 子系统自己的路径空间(`/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
把所有 CarLog 表加 `carlog_` 前缀:
```sql
RENAME TABLE vehicles TO carlog_vehicles;
RENAME TABLE wash_records TO carlog_wash_records;
RENAME TABLE refuel_records TO carlog_refuels;
RENAME TABLE charging_records TO carlog_chargings;
-- ... 其他表
```
`db.js` 加 helper:
```js
const TABLE_PREFIX = {
vehicles: 'carlog_vehicles',
wash_records: 'carlog_wash_records',
};
export function tableName(name) {
return TABLE_PREFIX[name] || name;
}
```
### W3+(暂不规划)
用户还没决定下一子系统做什么。本阶段只做:
1. ✅ 平台基座(subsystems 表 + platform_settings + 3 个平台路由 + 3 个平台前端 + 元数据驱动菜单)
2. ✅ CarLog 子系统化(目录迁移 + 加 `/api/carlog/` 前缀 + 注册到 subsystems 表)
将来加新子系统时按这个流程:
1. 写子系统的表(带 `{subsystem}_` 前缀)+ 路由 + 前端
2. 注册到 `subsystems`
3. 平台菜单自动出现新子系统入口
4. 通用设置渲染器自动支持新子系统的 settings
5. 验证端到端流程
## 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 时再说。