# 个人留痕系统(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)