feat: Web 版上线 footpr.linyux.cn —— 登录页 + 按用户隔离的命令 API + 静态托管
This commit is contained in:
+57
-32
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user