feat: 足迹 Footprints 个人留痕系统
桌面应用(Tauri+React+SQLite):时间线/记录/学年学科成绩梯度/统计/类型与字段模板/附件内容寻址/详情视图/PWA 同步服务端(sync-server):多用户+邀请码/LWW推拉/seq游标/附件去重/Docker部署 多端同步地基:uuid v7/HLC混合逻辑时钟/软删除墓碑/sync_oplog,Node与Rust双端一致并通过自动化测试
This commit is contained in:
@@ -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'
|
||||
]);
|
||||
Reference in New Issue
Block a user