桌面应用(Tauri+React+SQLite):时间线/记录/学年学科成绩梯度/统计/类型与字段模板/附件内容寻址/详情视图/PWA 同步服务端(sync-server):多用户+邀请码/LWW推拉/seq游标/附件去重/Docker部署 多端同步地基:uuid v7/HLC混合逻辑时钟/软删除墓碑/sync_oplog,Node与Rust双端一致并通过自动化测试
284 lines
14 KiB
Markdown
284 lines
14 KiB
Markdown
# 个人留痕系统(Footprints)设计方案
|
||
|
||
> 版本 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)
|