feat: 足迹 Footprints 个人留痕系统

桌面应用(Tauri+React+SQLite):时间线/记录/学年学科成绩梯度/统计/类型与字段模板/附件内容寻址/详情视图/PWA

同步服务端(sync-server):多用户+邀请码/LWW推拉/seq游标/附件去重/Docker部署

多端同步地基:uuid v7/HLC混合逻辑时钟/软删除墓碑/sync_oplog,Node与Rust双端一致并通过自动化测试
This commit is contained in:
root
2026-10-16 13:53:15 +08:00
commit 42ca57abfd
107 changed files with 234676 additions and 0 deletions
+91
View File
@@ -0,0 +1,91 @@
// 同步服务存储层:SQLite(node:sqlite 内置,零依赖)
import { DatabaseSync } from 'node:sqlite';
import fs from 'node:fs';
import path from 'node:path';
export const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'data');
export const BLOB_DIR = path.join(DATA_DIR, 'blobs');
const DB_PATH = path.join(DATA_DIR, 'sync.db');
let db = null;
export function getDb() {
if (db) return db;
fs.mkdirSync(BLOB_DIR, { recursive: true });
db = new DatabaseSync(DB_PATH);
db.exec('PRAGMA journal_mode = WAL;');
db.exec('PRAGMA foreign_keys = ON;');
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
salt TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
);
CREATE TABLE IF NOT EXISTS devices (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
name TEXT NOT NULL,
last_seen_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
UNIQUE(user_id, name)
);
CREATE TABLE IF NOT EXISTS tokens (
token TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
device_id TEXT NOT NULL REFERENCES devices(id),
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
expires_at TEXT NOT NULL
);
-- 每行最新状态(按 user + table + row_uuid 唯一,LWW 胜者)
CREATE TABLE IF NOT EXISTS current (
user_id TEXT NOT NULL,
table_name TEXT NOT NULL,
row_uuid TEXT NOT NULL,
op TEXT NOT NULL,
hlc TEXT NOT NULL,
ms INTEGER NOT NULL,
cnt INTEGER NOT NULL,
dev TEXT NOT NULL,
data TEXT,
updated_seq INTEGER,
PRIMARY KEY (user_id, table_name, row_uuid)
);
-- 全量变更历史(seq 单调递增,作为拉取游标基准)
CREATE TABLE IF NOT EXISTS changes (
seq INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
table_name TEXT NOT NULL,
row_uuid TEXT NOT NULL,
op TEXT NOT NULL,
hlc TEXT NOT NULL,
data TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
);
CREATE INDEX IF NOT EXISTS idx_current_seq ON current(user_id, updated_seq);
CREATE INDEX IF NOT EXISTS idx_changes_user ON changes(user_id, seq);
-- 附件:内容寻址去重
CREATE TABLE IF NOT EXISTS attachments (
user_id TEXT NOT NULL,
sha256 TEXT NOT NULL,
size INTEGER NOT NULL,
mime TEXT,
file_name TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
PRIMARY KEY (user_id, sha256)
);
`);
return db;
}
// 允许同步的表(与客户端 schema 对应;footprint_dimension 以 uuid 对作普通行同步)
export const SYNC_TABLES = new Set([
'footprint', 'footprint_type', 'taxonomy', 'dimension', 'attachment',
'semester', 'course_grade', 'metric_def', 'saved_view', 'footprint_dimension'
]);