feat: Web 版上线 footpr.linyux.cn —— 登录页 + 按用户隔离的命令 API + 静态托管

This commit is contained in:
root
2026-10-16 15:00:58 +08:00
parent 99358b834a
commit 547157aaa1
31 changed files with 967 additions and 60 deletions
+57 -32
View File
@@ -11,33 +11,6 @@ export const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, '..', '.dat
export const ATT_DIR = path.join(DATA_DIR, 'attachments');
export const DB_PATH = path.join(DATA_DIR, 'footprints.db');
let db = null;
export function getDb() {
if (db) return db;
fs.mkdirSync(ATT_DIR, { recursive: true });
db = new DatabaseSync(DB_PATH);
db.exec('PRAGMA foreign_keys = ON;');
// 先补列(老库无 uuid/hlc 等列时,schema 中的索引会引用失败),再执行 schema,最后回填
preMigrateColumns(db);
db.exec(fs.readFileSync(path.join(__dirname, '..', 'schema.sql'), 'utf8'));
migrateSync(db);
ensureBuiltin(db);
return db;
}
export function buildSearchText(title, description, result, tags, extras) {
const extraFlat = Object.entries(extras || {}).map(([k, v]) => {
if (v == null) return '';
if (typeof v === 'object') return Object.values(v).filter(Boolean).join(' ');
return String(v);
}).filter(Boolean);
return [title, description || '', result || '', (tags || []).join(' '), ...extraFlat]
.filter(Boolean).join('\n');
}
// ---------- 同步基础设施 ----------
function getMeta(d, key) {
const r = d.prepare('SELECT value FROM sync_state WHERE key=?').get(key);
return r ? r.value : null;
@@ -98,6 +71,15 @@ function getUuid(d, table, id) {
return r ? r.uuid : null;
}
export function buildSearchText(title, description, result, tags, extras) {
const extraFlat = Object.entries(extras || {}).map(([k, v]) => {
if (v == null) return '';
if (typeof v === 'object') return Object.values(v).filter(Boolean).join(' ');
return String(v);
}).filter(Boolean);
return [title, description || '', result || '', (tags || []).join(' '), ...extraFlat]
.filter(Boolean).join('\n');
}
// 老库迁移:补列 + 回填 uuid/hlc/origin(幂等)
const SYNC_TABLES = ['footprint', 'footprint_type', 'taxonomy', 'dimension', 'attachment',
'semester', 'course_grade', 'metric_def', 'saved_view', 'grade_band'];
@@ -250,6 +232,31 @@ function ensureBuiltin(db) {
}
// ---------- 查询构建 ----------
function openDb(dbPath) {
fs.mkdirSync(path.join(path.dirname(dbPath), 'attachments'), { recursive: true });
const d = new DatabaseSync(dbPath);
d.exec('PRAGMA foreign_keys = ON;');
// 先补列(老库无 uuid/hlc 等列时,schema 中的索引会引用失败),再执行 schema,最后回填
preMigrateColumns(d);
d.exec(fs.readFileSync(path.join(__dirname, '..', 'schema.sql'), 'utf8'));
migrateSync(d);
ensureBuiltin(d);
return d;
}
// 按数据库路径创建一套应用命令(多用户/服务器模式:每个用户一个独立档案库)
export function createAppCommands(dbPath) {
const db = openDb(dbPath);
const DATA_DIR = path.dirname(dbPath); // 工厂内遮蔽顶层默认值:附件按用户目录
function getDb() {
return db;
}
// ---------- 同步基础设施 ----------
const SORTS = {
date_desc: "COALESCE(NULLIF(fp.start_date,''), substr(fp.created_at,1,10)) DESC, fp.id DESC",
date_asc: "COALESCE(NULLIF(fp.start_date,''), substr(fp.created_at,1,10)) ASC, fp.id ASC",
@@ -337,8 +344,7 @@ function mapRow(r) {
};
}
function listFootprints(query) {
const d = getDb();
function listFootprints(d, query) {
const page = Math.max(1, query.page || 1);
const pageSize = Math.min(2000, Math.max(1, query.page_size || 20));
const { joins, conds, params } = buildWhere(query);
@@ -359,7 +365,7 @@ function listFootprints(query) {
}
// ---------- 命令处理 ----------
export function handleCommand(cmd, payload) {
function handleCommand(cmd, payload) {
const d = getDb();
switch (cmd) {
@@ -496,7 +502,7 @@ export function handleCommand(cmd, payload) {
}
case 'footprint_list':
return listFootprints(payload.query || {});
return listFootprints(d, payload.query || {});
case 'footprint_get': {
const row = d.prepare(BASE_SELECT + ' AND fp.id=?').get(payload.id);
@@ -563,7 +569,7 @@ export function handleCommand(cmd, payload) {
}
case 'timeline': {
const res = listFootprints({ ...(payload.query || {}), page_size: 2000, sort: 'date_desc' });
const res = listFootprints(d, { ...(payload.query || {}), page_size: 2000, sort: 'date_desc' });
const groups = [];
const byYear = new Map();
for (const row of res.items) {
@@ -908,3 +914,22 @@ export function handleCommand(cmd, payload) {
throw new Error('未知命令:' + cmd);
}
}
// 工厂闭合
return { handleCommand, getDb };
}
// ---------- 默认单例(开发桥接 / seed 兼容) ----------
let defaultCommands = null;
function getDefault() {
if (!defaultCommands) defaultCommands = createAppCommands(DB_PATH);
return defaultCommands;
}
export function getDb() {
return getDefault().getDb();
}
export function handleCommand(cmd, payload) {
return getDefault().handleCommand(cmd, payload);
}