Files
FootPrint/DESIGN.md
T

287 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.
# 个人留痕系统(Footprints)设计方案
> ⚠️ 历史底稿:本文写于单人桌面阶段(v0.5),核心数据模型思想仍然有效;
> 现产品已演进为**云优先多用户架构**(见 README)。设计原则与本体模型可继续作为参考。
>
> 版本 v0.5 · 足迹本体是核心;分类、表单、统计、导出、评分全部用户可自定义
> 形态:单人使用 · Windows 桌面应用(MVP 已实现)· 预留多端同步
## 0. 设计原则
1. 足迹本体覆盖一个学生全部值得留痕的事,不局限于任何评分公式
2. **配置即数据**:所有枚举、分类轴、表单字段、统计口径、导出格式都是数据库里的配置,
程序只提供渲染与计算引擎;换学校、换习惯、换关注点 = 改配置,不改代码
3. 时间线完整:正面/负面记录、进行中、草稿都留痕
4. 单机桌面、数据全本地、附件与描述分离存储
## 1. 产品定位
个人成长档案馆:平时随手留痕,需要时按任意维度检索、聚合、量化、导出。
- 检索盘点:"大二上学期做过什么""拿过哪些省级奖""在宣传部做过哪些事" —— 秒级检索
- 材料输出:评奖评优/综测/简历/入党材料 → 选条目、汇总表、佐证包
- 成长量化:自定义维度与指标统计
- 时间线复盘:完整回顾
## 2. 足迹本体模型
```sql
footprint (
id INTEGER PRIMARY KEY,
type_id INTEGER NOT NULL, -- → footprint_type(含自定义类型)
title TEXT NOT NULL,
role TEXT, -- 来自 taxonomy(kind='role'),可自定义
level TEXT, -- 来自 taxonomy(kind='level'),可自定义
polarity TEXT, -- positive/negative
award_grade TEXT, rank INTEGER, -- 可选
organizer TEXT,
start_date DATE, end_date DATE,
status TEXT, -- draft/ongoing/submitted/finished
description TEXT, result TEXT,
metrics TEXT, -- JSON {"志愿时长":8} 任意键 → metric_def 注册后可统计
extras TEXT, -- JSON 按类型表单模板的动态字段
group_id TEXT, -- 事件归组
parent_id INTEGER, -- 晋升链/隶属
tags TEXT, -- JSON 自由标签
weight INTEGER,
created_at DATETIME, updated_at DATETIME
);
footprint_type (
id, name, icon, color, sort,
form_template JSON, -- 动态字段模板(见 §3.2)
is_builtin BOOL, closed BOOL -- 内置类型可改名/停用,不可删除
);
dimension (id, name, color, sort); -- 成长维度(自定义分类轴)
footprint_dimension (footprint_id, dimension_id);
attachment (id, owner_type, owner_id, kind, file_path, file_name, mime, size, taken_at);
semester (id, term UNIQUE, gpa, rank, total_students);
course_grade (id, semester_id, course_name, credit, score, grade_point);
footprint_fts: FTS5(title, description, result, tags, extras_text);
```
## 3. 用户自定义体系(本版重点)
### 3.1 十个可自定义层(从记录到输出)
| # | 对象 | 自定义内容 | 存储 |
|---|---|---|---|
| 1 | 足迹类型 | 新建类型(名称/图标/颜色);内置类型改名、停用 | footprint_type |
| 2 | 表单字段 | 每个类型的动态字段:增删改、必填、默认值、单位 | form_template JSON |
| 3 | 分类轴(维度) | 自定义正交分类(如 学业深造/职业发展/兴趣生活),一条足迹可挂多轴 | dimension + 多对多 |
| 4 | 标签 | 自由打标 + 标签管理(重命名/合并/用量统计) | tags JSON |
| 5 | 枚举字典 | 级别、角色等枚举可增改排序(如加"企业级""国际级") | taxonomy |
| 6 | 统计指标 | 把 metrics 的键注册为指标:求和/计数/去重计数 + 单位 | metric_def |
| 7 | 视图 | 保存筛选条件为一键视图(评优视图/学年视图/任一组合),可设默认 | saved_view |
| 8 | 导出模板 | 列选择、分组方式、文件命名规则 | export_template |
| 9 | 评分适配器 | 学校综测/加分类规则:预设 JSON 导入 + 界面微调分值 | scoring_preset |
| 10 | 外观 | 主题色/深浅模式/首页默认视图/时间线密度 | app_config |
### 3.2 动态表单模板(form_template)
每个类型一个字段数组,驱动录入表单、详情页、检索列三处渲染:
```json
[
{ "key": "team_members", "label": "队伍成员", "widget": "multiselect", "options": "从联系人选" },
{ "key": "my_work", "label": "我的分工", "widget": "text", "required": true },
{ "key": "deliverable", "label": "最终产品", "widget": "text" },
{ "key": "cert_level", "label": "证书等级", "widget": "select",
"options": ["从业资格", "初级", "中级", "高级"] },
{ "key": "hours", "label": "志愿时长", "widget": "number", "unit": "小时",
"metric": "volunteer_hours" }
]
```
- widget 类型:text / textarea / number / date / select / multiselect / link(关联其他足迹) / contact(联系人)
- 数据存 footprint.extras JSON;schema 永不因新字段而改
- widget=number 且声明 metric 时,录入即累计进统计
### 3.3 枚举字典(taxonomy)
```sql
taxonomy (id, kind TEXT, value TEXT, color TEXT, sort INT, is_builtin BOOL, UNIQUE(kind, value));
-- kind: level(级别) / role(角色) / attachment_kind / polarity ...
-- 预置: level = 国家级/省市级/区县级/校级/院级/none;
-- role = 主导/核心/参加 —— 均可增改,例如加"企业实习级"
```
### 3.4 统计指标与视图
```sql
metric_def (id, key TEXT UNIQUE, label TEXT, unit TEXT,
agg TEXT /*sum|count|distinct*/, section TEXT);
saved_view (id, name, filter JSON, sort JSON, is_default BOOL, updated_at);
export_template (id, name, columns JSON, group_by TEXT, filename_rule TEXT);
```
- 指标卡:看板上的"自定义指标卡"由 metric_def 驱动(例:volunteer_hours 求和=志愿总时长;
证书类型 distinct 计数;省级以上获奖 count)
- 视图:筛选器(类型×级别×角色×维度×学年×状态×标签×关键词)任意组合另存,
例:"综测佐证视图" = level∈{国家级,省市级} + polarity=positive + 有附件
### 3.5 评分适配器(第 9 层,V3)
预设 JSON 定义分类映射 + 查表/矩阵/公式 + 去重 + 封顶,对足迹投影综测得分单。
示例:rules/重庆城市科技学院-计网学院-2026-2027.json(参考实现,非系统依赖)。
## 4. 技术选型
Tauri 2 + React 18 + TypeScript + Vite + Tailwind + shadcn/ui + SQLite(rusqlite, FTS5) + ECharts
- 配置驱动 UI:动态表单/动态筛选器/动态看板全部由上述配置表渲染
- 备选:Electron + better-sqlite3(数据层一致可互换)
## 5. 输出
- 任意视图/筛选结果 → xlsx 汇总表(列与分组来自 export_template)
- 勾选条目 → 佐证材料包 zip(编号命名规则可配)
- 适配器激活时 → 综测得分单 + 加分明细对账单
- V3:评优事迹 Word、简历段生成
## 6. 迭代路线
- MVP:类型管理 + 动态表单 + footprint CRUD + 时间线 + FTS 检索 + 附件 + taxonomy + 标签
- V2:维度/指标/视图/导出模板、事件归组聚合、成绩导入、xlsx/佐证包
- V3:评分适配器、AI 字段抽取、评优 Word、同步备份
## 9. 账号系统与多端互通(v0.5 新增)
### 9.1 定位
单人系统中账号 = 同步身份(多端识别 + 云备份 + 未来协作扩展点),不是数据前提。
未登录 = 纯本地模式(现状完全不变),登录后多端打通。
### 9.2 路线选型
| | A 整库文件同步 | B 云优先 | C 本地优先+同步引擎(选定) |
|---|---|---|---|
| 离线 | 可用 | 差 | 完全可用 |
| 冲突 | 整库覆盖丢数据 | 服务端事务 | 行级合并可控 |
| 改造 | 极低 | 高 | 中(数据层+同步模块) |
| 附件 | 易碎 | 对象存储 | 内容寻址+对象存储 |
### 9.3 数据层改造(P1)
```sql
-- 业务表统一增加同步元数据
uuid TEXT; -- uuid v7(时间有序),替代自增 ID 的跨端身份
deleted_at TEXT; -- 软删除墓碑,删除也要同步
hlc TEXT; -- 混合逻辑时钟 "毫秒:计数:设备号",冲突仲裁依据
origin TEXT; -- 创建设备
-- 本地变更日志(推送队列)
CREATE TABLE sync_oplog (
id INTEGER PRIMARY KEY AUTOINCREMENT,
table_name TEXT NOT NULL,
row_uuid TEXT NOT NULL,
op TEXT NOT NULL, -- upsert / delete
hlc TEXT NOT NULL,
synced_at TEXT -- NULL = 待推送
);
```
- 自增 ID 换 UUID(v7):两台设备各自自增必然冲突
- HLC 而非本地时间:时钟回拨免疫,跨设备可比较、同设备单调
- 删除走墓碑:硬删除无法向其他设备传播
### 9.4 同步协议(P2)
- push:本地 oplog 未同步变更 → 服务端按 hlc LWW 仲裁 upsert
- pull:GET /sync?cursor=服务端hlc → 应用比自己新的行 → 推进游标
- 附件:SHA-256 内容寻址;上传前查重;本地按需缓存(同一佐证多处引用只存一份)
### 9.4.1 附件与图片存储布局
本地(桌面端):
```
应用数据目录/
├─ footprints.db ← 元数据(rel_path/sha256/size/mime + 同步元数据)
└─ attachments/{记录id}/{uuid}_{原文件名} ← 图片实体原样保存(EXIF 保留)
```
服务器(sync-server):
```
data/
├─ sync.db ← attachments 表 (user_id, sha256) 主键
└─ blobs/{user_id}/{sha256} ← 内容寻址,跨记录/跨设备去重
```
生命周期:添加 → 本地落盘+元数据入 oplog → push 元数据 → 服务器缺 blob 则补传(哈希校验)
→ 其他端 pull 元数据 → 按需下载。删除 → 元数据墓碑同步,文件延迟 GC(全端确认后回收)。
演进方向:缩略图(256px WebP,网格用缩略图)、大文件分片、可选上传压缩、对象存储适配、E2E 加密。
- 触发:启动 + 写操作后 debounce 3s + 手动同步
- 冲突:按行 LWW(单人多端最优);不做 CRDT
### 9.5 身份层
- 登录:邮箱魔法链接或手机验证码;本地设备 ID 注册
- 凭据:Tauri keyring(Windows Credential Manager)
- 设备管理:device_id ↔ 设备名,可远程下线
- 未登录:同步模块整体旁路,界面显示"本地模式"
### 9.6 服务端选型
| | BaaS(火山引擎 Supabase 版) | 自建轻量服务 |
|---|---|---|
| Auth/DB/存储/Realtime | 全部现成 + RLS 行级权限 | 自管 Postgres + MinIO/OSS |
| 同步实现 | 客户端直连表 + hlc 游标 | 单 /sync 模块 |
| 运维 | 免运维,起步免费 | 一台服务器 |
| 适用 | 快速落地(推荐) | 数据完全自持 |
### 9.7 安全分层
1. 基线:TLS + 服务端账号隔离(RLS)
2. 进阶:本地库 SQLCipher
3. 可选端到端:客户端加密后再上云(代价是服务端不可检索)
### 9.8 分阶段落地
| 阶段 | 内容 | 产出 | 状态 |
|---|---|---|---|
| P1 | UUID/软删除/HLC/oplog + 存量迁移(纯本地) | 同步地基,功能不变 | **已实现**(Node 与 Rust 双端一致) |
| P1.5 | 导出加密同步包手动导入合并 | 无后端的手动多端 | 待做(可选) |
| P2 | 自建同步服务:登录 + push/pull + 附件 hash 上传 | 真正多端互通 | **服务端已实现**(sync-server/,Docker 部署);客户端引擎待接 |
| P3 | Web 端复用 React 前端;移动端 PWA | 随时随地 | **前端响应式 + PWA 改造完成**(manifest/SW/抽屉导航),待服务部署后直连 |
| P4 | 端到端加密;多人协作(复用审核流设计) | 班级/学院版 | 待做 |
### 9.9 自建同步服务实现(sync-server/,v0.2 新增)
- 零 npm 依赖:node:http + node:sqlite,Docker 镜像 node:24-alpine,构建无供应链风险
- 存储模型:`current`(每行最新态,user+table+row_uuid 唯一)+ `changes`(全量历史,seq 全局单调即拉取游标)
- push:按 HLC 三元组(毫秒:计数:设备)逐级比较 LWW;旧推送计入 stale;服务端不解释业务字段(data 为不透明 JSON,业务表间引用由客户端以 uuid 表达)
- pull:`updated_seq > cursor` 增量,游标由服务端 seq 保证单调
- 附件:(user, sha256) 内容寻址去重,上传校验实际哈希,跨设备仅存一份
- 认证:scrypt 口令散列 + Bearer token(365 天)+ 设备登记(同名设备复用 device_id);REGISTER_ENABLED 环境变量控制注册开关
- 多用户:天然按 user_id 隔离(变更/附件/设备);INVITE_CODE 邀请码注册 + MAX_USERS 名额上限 + ADMIN_TOKEN 保护的 /admin/users 管理视图(仅元数据,档案内容管理员不可见)
- 客户端多账号(P2):登录/切换账号,本地档案按账号建独立 SQLite profile
- 部署:`cd sync-server && docker compose up -d --build`,数据落 ./data/;必须经 HTTPS 反代(Caddy 示例见 sync-server/README.md)
### 9.10 验证状态(诚实边界)
已验证(自动化测试):
- 服务端协议:注册/登录/多用户/邀请码/名额/管理视图/push(LWW)/pull(游标)/附件去重/账号隔离 —— 全链路通过
- Node 数据层:53 项回归(CRUD/守卫/FTS/墓碑/级联/oplog)—— 通过
- Rust 后端:9 项运行时测试(uuid v7/HLC 单调/时钟回拨/老库迁移/命令层往返/墓碑/级联/重名守卫/加权计算)—— 通过
测试曾抓出并已修复的关键缺陷:
- Rust exec() 返回"受影响行数"而非 lastInsertRowid,导致桌面端所有创建操作拿到 id=1(关键级)
- Rust footprint_list 的 BASE_SELECT 与 build_where 双 WHERE 拼接导致检索 SQL 报错
- 学期删除未级联墓碑课程,孤儿课程污染总览
尚未闭环(诚实声明):
- 客户端同步引擎(P2)未实现:桌面/手机目前均为纯本地,尚无数据真正流向服务器
- FK 的 uuid 映射约定已定稿(载荷中业务表间引用以 uuid 表达),代码随 P2 实现
- 真实双设备并发、附件 GC、备份恢复等长尾场景待真机验证
P1 实现说明(v0.2):
- schema v2:9 张业务表统一 uuid/deleted_at/origin/hlc 列 + sync_state + sync_oplog + uuid 唯一索引
- 老库迁移幂等:pre_migrate(补列,先于 schema 执行)→ schema → backfill(uuid/hlc/origin)
- 所有写路径接 oplog(Node 与 Rust 26 命令一致):变更先进本地队列,P2 推送直接消费
- 删除统一软删除(墓碑);FTS 内容表在删除时同步清理
- 附件新增 sha256 内容寻址列,为 P2 去重上传就绪
- 新增 sync_status 命令与侧边栏同步状态显示(本地模式 · 待同步 N)