fix: harden cloud storage security
This commit is contained in:
@@ -40,6 +40,23 @@ const db = new Database(dbPath);
|
||||
const DEFAULT_LOCAL_STORAGE_QUOTA_BYTES = 1024 * 1024 * 1024; // 1GB
|
||||
const DEFAULT_OSS_STORAGE_QUOTA_BYTES = 1024 * 1024 * 1024; // 1GB
|
||||
|
||||
function normalizeStoredDownloadTrafficQuota(rawQuota) {
|
||||
if (rawQuota === null || rawQuota === undefined || rawQuota === '') {
|
||||
return -1; // 默认不限下载
|
||||
}
|
||||
|
||||
const parsedQuota = Number(rawQuota);
|
||||
if (!Number.isFinite(parsedQuota)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (parsedQuota < 0) {
|
||||
return -1; // -1 表示不限流量
|
||||
}
|
||||
|
||||
return Math.max(0, Math.floor(parsedQuota)); // 0 表示禁止下载
|
||||
}
|
||||
|
||||
// ===== 性能优化配置(P0 优先级修复) =====
|
||||
|
||||
// 1. 启用 WAL 模式(Write-Ahead Logging)
|
||||
@@ -507,6 +524,26 @@ function initDatabase() {
|
||||
)
|
||||
`);
|
||||
|
||||
// OSS 直传临时对象登记表:直传必须完成服务端确认后才进入用户目录
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS oss_upload_reservations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
reservation_token TEXT NOT NULL UNIQUE,
|
||||
user_id INTEGER NOT NULL,
|
||||
final_object_key TEXT NOT NULL,
|
||||
temp_object_key TEXT NOT NULL,
|
||||
expected_size INTEGER NOT NULL DEFAULT 0,
|
||||
previous_size INTEGER NOT NULL DEFAULT 0,
|
||||
file_hash TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'pending', -- pending/completed/expired/cancelled
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at DATETIME NOT NULL,
|
||||
completed_at DATETIME,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
// 在线设备会话(用于设备管理和强制下线)
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS user_device_sessions (
|
||||
@@ -575,6 +612,12 @@ function initDatabase() {
|
||||
CREATE INDEX IF NOT EXISTS idx_upload_sessions_hash
|
||||
ON upload_sessions(user_id, file_hash, file_size);
|
||||
|
||||
-- OSS 直传临时对象索引
|
||||
CREATE INDEX IF NOT EXISTS idx_oss_upload_reservations_status_expires
|
||||
ON oss_upload_reservations(status, expires_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_oss_upload_reservations_user_status
|
||||
ON oss_upload_reservations(user_id, status, expires_at);
|
||||
|
||||
-- 在线设备会话索引
|
||||
CREATE INDEX IF NOT EXISTS idx_user_device_sessions_user_active
|
||||
ON user_device_sessions(user_id, revoked_at, expires_at, last_active_at);
|
||||
@@ -615,14 +658,22 @@ function createDefaultAdmin() {
|
||||
// 从环境变量读取管理员账号密码,如果没有则使用默认值
|
||||
const adminUsername = process.env.ADMIN_USERNAME || 'admin';
|
||||
const adminPassword = process.env.ADMIN_PASSWORD || 'admin123';
|
||||
const defaultAdminPasswords = new Set(['admin123', 'password', '123456', '12345678', 'change-this-admin-password']);
|
||||
|
||||
if (process.env.NODE_ENV === 'production' && defaultAdminPasswords.has(String(adminPassword).trim())) {
|
||||
console.error('[安全] 生产环境禁止使用默认管理员密码,请设置 ADMIN_PASSWORD');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const hashedPassword = bcrypt.hashSync(adminPassword, 10);
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO users (
|
||||
username, email, password,
|
||||
is_admin, is_active, has_oss_config, is_verified
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
is_admin, is_active, has_oss_config, is_verified,
|
||||
download_traffic_quota, download_traffic_used,
|
||||
download_traffic_reset_cycle, download_traffic_last_reset_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
adminUsername,
|
||||
`${adminUsername}@example.com`,
|
||||
@@ -630,7 +681,11 @@ function createDefaultAdmin() {
|
||||
1,
|
||||
1,
|
||||
0, // 管理员不需要OSS配置
|
||||
1 // 管理员默认已验证
|
||||
1, // 管理员默认已验证
|
||||
-1, // 默认不限下载
|
||||
0,
|
||||
'none',
|
||||
null
|
||||
);
|
||||
|
||||
console.log('默认管理员账号已创建');
|
||||
@@ -658,10 +713,15 @@ const UserDB = {
|
||||
username, email, password,
|
||||
oss_provider, oss_region, oss_access_key_id, oss_access_key_secret, oss_bucket, oss_endpoint,
|
||||
has_oss_config,
|
||||
is_verified, verification_token, verification_expires_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
is_verified, verification_token, verification_expires_at,
|
||||
storage_permission, current_storage_type,
|
||||
download_traffic_quota, download_traffic_used,
|
||||
download_traffic_reset_cycle, download_traffic_last_reset_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const downloadTrafficQuota = normalizeStoredDownloadTrafficQuota(userData.download_traffic_quota);
|
||||
|
||||
const result = stmt.run(
|
||||
userData.username,
|
||||
userData.email,
|
||||
@@ -675,7 +735,13 @@ const UserDB = {
|
||||
hasOssConfig,
|
||||
userData.is_verified !== undefined ? userData.is_verified : 0,
|
||||
hashedVerificationToken,
|
||||
userData.verification_expires_at || null
|
||||
userData.verification_expires_at || null,
|
||||
userData.storage_permission || 'oss_only',
|
||||
userData.current_storage_type || 'oss',
|
||||
downloadTrafficQuota,
|
||||
0,
|
||||
'none',
|
||||
null
|
||||
);
|
||||
|
||||
return result.lastInsertRowid;
|
||||
@@ -751,9 +817,8 @@ const UserDB = {
|
||||
'theme_preference': 'string',
|
||||
|
||||
// 数值类型字段
|
||||
'is_admin': 'number',
|
||||
'is_active': 'number',
|
||||
'is_banned': 'is_banned',
|
||||
'is_banned': 'number',
|
||||
'has_oss_config': 'number',
|
||||
'is_verified': 'number',
|
||||
'local_storage_quota': 'number',
|
||||
@@ -808,7 +873,6 @@ const UserDB = {
|
||||
|
||||
// API 密钥和权限字段
|
||||
'upload_api_key': 'upload_api_key',
|
||||
'is_admin': 'is_admin',
|
||||
'is_active': 'is_active',
|
||||
'is_banned': 'is_banned',
|
||||
'has_oss_config': 'has_oss_config',
|
||||
@@ -852,7 +916,7 @@ const UserDB = {
|
||||
|
||||
// 检查数据库中是否有 FIELD_MAP 未定义的字段(可选)
|
||||
for (const dbField of dbFields) {
|
||||
if (!mappedFields.has(dbField) && !['id', 'created_at', 'updated_at'].includes(dbField)) {
|
||||
if (!mappedFields.has(dbField) && !['id', 'created_at', 'updated_at', 'is_admin'].includes(dbField)) {
|
||||
extraFields.push(dbField);
|
||||
}
|
||||
}
|
||||
@@ -894,7 +958,6 @@ const UserDB = {
|
||||
|
||||
// API 密钥和权限字段
|
||||
'upload_api_key': 'upload_api_key',
|
||||
'is_admin': 'is_admin',
|
||||
'is_active': 'is_active',
|
||||
'is_banned': 'is_banned',
|
||||
'has_oss_config': 'has_oss_config',
|
||||
@@ -968,7 +1031,7 @@ const UserDB = {
|
||||
'download_traffic_quota_expires_at': 'string',
|
||||
'download_traffic_reset_cycle': 'string',
|
||||
'download_traffic_last_reset_at': 'string',
|
||||
'is_admin': 'number', 'is_active': 'number', 'is_banned': 'number',
|
||||
'is_active': 'number', 'is_banned': 'number',
|
||||
'has_oss_config': 'number', 'is_verified': 'number',
|
||||
'local_storage_quota': 'number', 'local_storage_used': 'number',
|
||||
'oss_storage_quota': 'number', 'download_traffic_quota': 'number',
|
||||
@@ -1014,6 +1077,43 @@ const UserDB = {
|
||||
return result;
|
||||
},
|
||||
|
||||
adjustLocalStorageUsed(id, delta) {
|
||||
const amount = Number(delta);
|
||||
if (!Number.isFinite(amount) || amount === 0) {
|
||||
return this.findById(id);
|
||||
}
|
||||
|
||||
db.prepare(`
|
||||
UPDATE users
|
||||
SET local_storage_used = MAX(COALESCE(local_storage_used, 0) + ?, 0),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`).run(Math.trunc(amount), id);
|
||||
|
||||
return this.findById(id);
|
||||
},
|
||||
|
||||
reserveLocalStorageSpace(id, additionalSize) {
|
||||
const amount = Math.max(0, Math.trunc(Number(additionalSize) || 0));
|
||||
if (amount === 0) {
|
||||
return this.findById(id);
|
||||
}
|
||||
|
||||
const stmt = db.prepare(`
|
||||
UPDATE users
|
||||
SET local_storage_used = COALESCE(local_storage_used, 0) + ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
AND COALESCE(local_storage_used, 0) + ? <= COALESCE(local_storage_quota, 0)
|
||||
`);
|
||||
const result = stmt.run(amount, id, amount);
|
||||
if (result.changes === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.findById(id);
|
||||
},
|
||||
|
||||
// 获取所有用户
|
||||
getAll(filters = {}) {
|
||||
let query = 'SELECT * FROM users WHERE 1=1';
|
||||
@@ -1278,7 +1378,7 @@ const ShareDB = {
|
||||
if (attempts > 10) {
|
||||
shareCode = this.generateShareCode(10); // 增加长度
|
||||
}
|
||||
} while (this.findByCode(shareCode) && attempts < 20);
|
||||
} while (this.findAnyByCode(shareCode) && attempts < 20);
|
||||
|
||||
// 计算过期时间
|
||||
let expiresAt = null;
|
||||
@@ -1376,6 +1476,10 @@ const ShareDB = {
|
||||
return result;
|
||||
},
|
||||
|
||||
findAnyByCode(shareCode) {
|
||||
return db.prepare('SELECT * FROM shares WHERE share_code = ?').get(shareCode);
|
||||
},
|
||||
|
||||
// 根据ID查找
|
||||
findById(id) {
|
||||
return db.prepare('SELECT * FROM shares WHERE id = ?').get(id);
|
||||
@@ -1396,6 +1500,7 @@ const ShareDB = {
|
||||
AND share_type = ?
|
||||
AND share_path = ?
|
||||
AND COALESCE(storage_type, 'oss') = ?
|
||||
AND (expires_at IS NULL OR expires_at > datetime('now', 'localtime'))
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
`).get(
|
||||
@@ -1557,6 +1662,7 @@ const DirectLinkDB = {
|
||||
WHERE user_id = ?
|
||||
AND file_path = ?
|
||||
AND COALESCE(storage_type, 'oss') = ?
|
||||
AND (expires_at IS NULL OR expires_at > datetime('now', 'localtime'))
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
`).get(
|
||||
@@ -1691,7 +1797,17 @@ const SettingsDB = {
|
||||
* 删除统一的 OSS 配置
|
||||
*/
|
||||
clearUnifiedOssConfig() {
|
||||
db.prepare('DELETE FROM system_settings WHERE key LIKE "oss_%"').run();
|
||||
db.prepare(`
|
||||
DELETE FROM system_settings
|
||||
WHERE key IN (
|
||||
'oss_provider',
|
||||
'oss_region',
|
||||
'oss_access_key_id',
|
||||
'oss_access_key_secret',
|
||||
'oss_bucket',
|
||||
'oss_endpoint'
|
||||
)
|
||||
`).run();
|
||||
console.log('[系统设置] 统一 OSS 配置已清除');
|
||||
},
|
||||
|
||||
@@ -1721,12 +1837,13 @@ const VerificationDB = {
|
||||
const row = db.prepare(`
|
||||
SELECT * FROM users
|
||||
WHERE verification_token = ?
|
||||
AND (
|
||||
verification_expires_at IS NULL
|
||||
OR verification_expires_at = ''
|
||||
OR verification_expires_at > strftime('%s','now')*1000 -- 数值时间戳(ms)
|
||||
OR verification_expires_at > CURRENT_TIMESTAMP -- 兼容旧的字符串时间
|
||||
)
|
||||
AND verification_expires_at IS NOT NULL
|
||||
AND verification_expires_at != ''
|
||||
AND CASE
|
||||
WHEN typeof(verification_expires_at) IN ('integer', 'real')
|
||||
THEN verification_expires_at > strftime('%s','now') * 1000
|
||||
ELSE datetime(verification_expires_at) > datetime('now','localtime')
|
||||
END
|
||||
AND is_verified = 0
|
||||
`).get(hashedToken);
|
||||
if (!row) return null;
|
||||
@@ -1757,10 +1874,14 @@ const PasswordResetTokenDB = {
|
||||
const hashedToken = crypto.createHash('sha256').update(token).digest('hex');
|
||||
const row = db.prepare(`
|
||||
SELECT * FROM password_reset_tokens
|
||||
WHERE token = ? AND used = 0 AND (
|
||||
expires_at > strftime('%s','now')*1000 -- 数值时间戳
|
||||
OR expires_at > CURRENT_TIMESTAMP -- 兼容旧的字符串时间
|
||||
)
|
||||
WHERE token = ? AND used = 0
|
||||
AND expires_at IS NOT NULL
|
||||
AND expires_at != ''
|
||||
AND CASE
|
||||
WHEN typeof(expires_at) IN ('integer', 'real')
|
||||
THEN expires_at > strftime('%s','now') * 1000
|
||||
ELSE datetime(expires_at) > datetime('now','localtime')
|
||||
END
|
||||
`).get(hashedToken);
|
||||
if (!row) return null;
|
||||
// 立即标记为已使用(防止重复使用)
|
||||
@@ -1960,7 +2081,7 @@ function migrateDownloadTrafficFields() {
|
||||
|
||||
if (!hasDownloadTrafficQuota) {
|
||||
console.log('[数据库迁移] 添加 download_traffic_quota 字段...');
|
||||
db.exec('ALTER TABLE users ADD COLUMN download_traffic_quota INTEGER DEFAULT 0');
|
||||
db.exec('ALTER TABLE users ADD COLUMN download_traffic_quota INTEGER DEFAULT -1');
|
||||
console.log('[数据库迁移] ✓ download_traffic_quota 字段已添加');
|
||||
}
|
||||
|
||||
@@ -1988,10 +2109,10 @@ function migrateDownloadTrafficFields() {
|
||||
console.log('[数据库迁移] ✓ download_traffic_last_reset_at 字段已添加');
|
||||
}
|
||||
|
||||
// 统一策略:download_traffic_quota 为空时回填为 0(0 表示禁止下载,-1 表示不限流量)
|
||||
// 未设置下载配额的旧记录默认回填为不限流量,避免升级后默认无法下载
|
||||
const quotaBackfillResult = db.prepare(`
|
||||
UPDATE users
|
||||
SET download_traffic_quota = 0
|
||||
SET download_traffic_quota = -1
|
||||
WHERE download_traffic_quota IS NULL
|
||||
`).run();
|
||||
|
||||
@@ -2009,6 +2130,53 @@ function migrateDownloadTrafficFields() {
|
||||
console.log(`[数据库迁移] ✓ 不限流量配额已标准化为 -1: ${quotaUnlimitedNormalizeResult.changes} 条记录`);
|
||||
}
|
||||
|
||||
const legacyRepairMarkerKey = 'download_traffic_zero_default_repaired_v1';
|
||||
if (SettingsDB.get(legacyRepairMarkerKey) !== 'true') {
|
||||
const legacyRepairCandidates = db.prepare(`
|
||||
SELECT u.id, u.username
|
||||
FROM users u
|
||||
WHERE COALESCE(u.download_traffic_quota, 0) = 0
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM system_logs l
|
||||
WHERE l.action = 'update_user_storage_and_traffic'
|
||||
AND l.details LIKE '%"targetUserId":' || u.id || ',%'
|
||||
)
|
||||
`).all();
|
||||
|
||||
if (legacyRepairCandidates.length > 0) {
|
||||
const repairLegacyZeroQuotaDefaults = db.transaction((candidates) => {
|
||||
const stmt = db.prepare(`
|
||||
UPDATE users
|
||||
SET download_traffic_quota = -1
|
||||
WHERE id = ? AND download_traffic_quota = 0
|
||||
`);
|
||||
|
||||
let repaired = 0;
|
||||
for (const candidate of candidates) {
|
||||
const result = stmt.run(candidate.id);
|
||||
repaired += Number(result?.changes || 0);
|
||||
}
|
||||
return repaired;
|
||||
});
|
||||
|
||||
const repairedCount = repairLegacyZeroQuotaDefaults(legacyRepairCandidates);
|
||||
if (repairedCount > 0) {
|
||||
const repairedUsers = legacyRepairCandidates
|
||||
.slice(0, 8)
|
||||
.map(item => `${item.username}(#${item.id})`)
|
||||
.join(', ');
|
||||
const suffix = legacyRepairCandidates.length > 8 ? ' ...' : '';
|
||||
console.log(
|
||||
`[数据库迁移] ✓ 已修复旧版默认下载配额为 0 的遗留数据: ${repairedCount} 条记录` +
|
||||
` (${repairedUsers}${suffix})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SettingsDB.set(legacyRepairMarkerKey, 'true');
|
||||
}
|
||||
|
||||
const usedBackfillResult = db.prepare(`
|
||||
UPDATE users
|
||||
SET download_traffic_used = 0
|
||||
@@ -2645,6 +2813,120 @@ const UploadSessionDB = {
|
||||
}
|
||||
};
|
||||
|
||||
const OssUploadReservationDB = {
|
||||
create({
|
||||
reservationToken,
|
||||
userId,
|
||||
finalObjectKey,
|
||||
tempObjectKey,
|
||||
expectedSize,
|
||||
previousSize = 0,
|
||||
fileHash = null,
|
||||
expiresAt
|
||||
}) {
|
||||
const token = typeof reservationToken === 'string' ? reservationToken.trim() : '';
|
||||
const uid = Number(userId);
|
||||
const expected = Math.max(0, Math.floor(Number(expectedSize) || 0));
|
||||
const previous = Math.max(0, Math.floor(Number(previousSize) || 0));
|
||||
const finalKey = typeof finalObjectKey === 'string' ? finalObjectKey.trim() : '';
|
||||
const tempKey = typeof tempObjectKey === 'string' ? tempObjectKey.trim() : '';
|
||||
const hash = typeof fileHash === 'string' && fileHash.trim() ? fileHash.trim() : null;
|
||||
const expiresAtValue = typeof expiresAt === 'string' ? expiresAt : null;
|
||||
|
||||
if (!token || !Number.isFinite(uid) || uid <= 0 || !finalKey || !tempKey || expected <= 0 || !expiresAtValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = db.prepare(`
|
||||
INSERT INTO oss_upload_reservations (
|
||||
reservation_token, user_id, final_object_key, temp_object_key,
|
||||
expected_size, previous_size, file_hash, status, expires_at,
|
||||
created_at, updated_at
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, 'pending', ?,
|
||||
datetime('now', 'localtime'), datetime('now', 'localtime')
|
||||
)
|
||||
`).run(token, Math.floor(uid), finalKey, tempKey, expected, previous, hash, expiresAtValue);
|
||||
|
||||
return db.prepare('SELECT * FROM oss_upload_reservations WHERE id = ?').get(result.lastInsertRowid);
|
||||
},
|
||||
|
||||
findPendingByToken(reservationToken) {
|
||||
const token = typeof reservationToken === 'string' ? reservationToken.trim() : '';
|
||||
if (!token) return null;
|
||||
|
||||
return db.prepare(`
|
||||
SELECT *
|
||||
FROM oss_upload_reservations
|
||||
WHERE reservation_token = ?
|
||||
AND status = 'pending'
|
||||
AND expires_at > datetime('now', 'localtime')
|
||||
LIMIT 1
|
||||
`).get(token);
|
||||
},
|
||||
|
||||
complete(reservationToken) {
|
||||
const token = typeof reservationToken === 'string' ? reservationToken.trim() : '';
|
||||
if (!token) return { changes: 0 };
|
||||
|
||||
return db.prepare(`
|
||||
UPDATE oss_upload_reservations
|
||||
SET status = 'completed',
|
||||
completed_at = datetime('now', 'localtime'),
|
||||
updated_at = datetime('now', 'localtime')
|
||||
WHERE reservation_token = ?
|
||||
AND status = 'pending'
|
||||
`).run(token);
|
||||
},
|
||||
|
||||
cancel(reservationToken) {
|
||||
const token = typeof reservationToken === 'string' ? reservationToken.trim() : '';
|
||||
if (!token) return { changes: 0 };
|
||||
|
||||
return db.prepare(`
|
||||
UPDATE oss_upload_reservations
|
||||
SET status = 'cancelled',
|
||||
updated_at = datetime('now', 'localtime')
|
||||
WHERE reservation_token = ?
|
||||
AND status = 'pending'
|
||||
`).run(token);
|
||||
},
|
||||
|
||||
listExpiredPending(limit = 100) {
|
||||
const safeLimit = Math.min(500, Math.max(1, Math.floor(Number(limit) || 100)));
|
||||
return db.prepare(`
|
||||
SELECT *
|
||||
FROM oss_upload_reservations
|
||||
WHERE status = 'pending'
|
||||
AND expires_at <= datetime('now', 'localtime')
|
||||
ORDER BY expires_at ASC, id ASC
|
||||
LIMIT ?
|
||||
`).all(safeLimit);
|
||||
},
|
||||
|
||||
expire(reservationToken) {
|
||||
const token = typeof reservationToken === 'string' ? reservationToken.trim() : '';
|
||||
if (!token) return { changes: 0 };
|
||||
|
||||
return db.prepare(`
|
||||
UPDATE oss_upload_reservations
|
||||
SET status = 'expired',
|
||||
updated_at = datetime('now', 'localtime')
|
||||
WHERE reservation_token = ?
|
||||
AND status = 'pending'
|
||||
`).run(token);
|
||||
},
|
||||
|
||||
cleanupFinalizedHistory(keepDays = 7) {
|
||||
const days = Math.min(365, Math.max(1, Math.floor(Number(keepDays) || 7)));
|
||||
return db.prepare(`
|
||||
DELETE FROM oss_upload_reservations
|
||||
WHERE status IN ('completed', 'expired', 'cancelled')
|
||||
AND updated_at < datetime('now', 'localtime', '-' || ? || ' days')
|
||||
`).run(days);
|
||||
}
|
||||
};
|
||||
|
||||
const DeviceSessionDB = {
|
||||
_normalizeSessionId(sessionId) {
|
||||
return typeof sessionId === 'string' ? sessionId.trim() : '';
|
||||
@@ -3190,13 +3472,13 @@ const TransactionDB = {
|
||||
|
||||
// 初始化数据库
|
||||
initDatabase();
|
||||
createDefaultAdmin();
|
||||
initDefaultSettings();
|
||||
migrateToV2(); // 执行数据库迁移
|
||||
migrateThemePreference(); // 主题偏好迁移
|
||||
migrateToOss(); // SFTP → OSS 迁移
|
||||
migrateOssQuotaField(); // OSS 配额字段迁移
|
||||
migrateDownloadTrafficFields(); // 下载流量字段迁移
|
||||
createDefaultAdmin();
|
||||
|
||||
module.exports = {
|
||||
db,
|
||||
@@ -3209,6 +3491,7 @@ module.exports = {
|
||||
DownloadTrafficReportDB,
|
||||
DownloadTrafficReservationDB,
|
||||
UploadSessionDB,
|
||||
OssUploadReservationDB,
|
||||
DeviceSessionDB,
|
||||
FileHashIndexDB,
|
||||
DownloadTrafficIngestDB,
|
||||
|
||||
Reference in New Issue
Block a user