Files
vue-driven-cloud-storage/backend/database.js
237899745 4350113979 fix: 修复配额说明重复和undefined问题
- 在editStorageForm中初始化oss_storage_quota_value和oss_quota_unit
- 删除重复的旧配额说明块,保留新的当前配额设置显示

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 19:39:53 +08:00

1465 lines
48 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 加载环境变量(确保在 server.js 之前也能读取)
require('dotenv').config();
const Database = require('better-sqlite3');
const bcrypt = require('bcryptjs');
const path = require('path');
const fs = require('fs');
const crypto = require('crypto');
// 引入加密工具(用于敏感数据加密存储)
const { encryptSecret, decryptSecret, validateEncryption } = require('./utils/encryption');
// 验证加密系统在启动时正常工作
try {
validateEncryption();
} catch (error) {
console.error('[安全] 加密系统验证失败,服务无法启动');
console.error('[安全] 请检查 ENCRYPTION_KEY 配置');
process.exit(1);
}
// 数据库路径配置
// 优先使用环境变量 DATABASE_PATH默认为 ./data/database.db
const DEFAULT_DB_PATH = path.join(__dirname, 'data', 'database.db');
const dbPath = process.env.DATABASE_PATH
? path.resolve(__dirname, process.env.DATABASE_PATH)
: DEFAULT_DB_PATH;
// 确保数据库目录存在
const dbDir = path.dirname(dbPath);
if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir, { recursive: true });
console.log(`[数据库] 创建目录: ${dbDir}`);
}
console.log(`[数据库] 路径: ${dbPath}`);
// 创建或连接数据库
const db = new Database(dbPath);
// ===== 性能优化配置P0 优先级修复) =====
// 1. 启用 WAL 模式Write-Ahead Logging
// 优势:支持并发读写,大幅提升数据库性能
db.pragma('journal_mode = WAL');
// 2. 配置同步模式为 NORMAL
// 性能提升:在安全性和性能之间取得平衡,比 FULL 模式快很多
db.pragma('synchronous = NORMAL');
// 3. 增加缓存大小到 64MB
// 性能提升:减少磁盘 I/O缓存更多数据页和索引页
// 负值表示 KB-64000 = 64MB
db.pragma('cache_size = -64000');
// 4. 临时表存储在内存中
// 性能提升:避免临时表写入磁盘,加速排序和分组操作
db.pragma('temp_store = MEMORY');
// 5. 启用外键约束
db.pragma('foreign_keys = ON');
console.log('[数据库性能优化] ✓ WAL 模式已启用');
console.log('[数据库性能优化] ✓ 同步模式: NORMAL');
console.log('[数据库性能优化] ✓ 缓存大小: 64MB');
console.log('[数据库性能优化] ✓ 临时表存储: 内存');
// ===== 第二轮修复WAL 文件定期清理机制 =====
/**
* 执行数据库检查点Checkpoint
* 将 WAL 文件中的内容写入主数据库文件,并清理 WAL
* @param {Database} database - 数据库实例
* @returns {boolean} 是否成功执行
*/
function performCheckpoint(database = db) {
try {
// 执行 checkpoint将 WAL 内容合并到主数据库)
database.pragma('wal_checkpoint(PASSIVE)');
// 获取 WAL 文件大小信息
const walInfo = database.pragma('wal_checkpoint(TRUNCATE)', { simple: true });
console.log('[WAL清理] ✓ 检查点完成');
return true;
} catch (error) {
console.error('[WAL清理] ✗ 检查点失败:', error.message);
return false;
}
}
/**
* 获取 WAL 文件大小
* @param {Database} database - 数据库实例
* @returns {number} WAL 文件大小(字节)
*/
function getWalFileSize(database = db) {
try {
const dbPath = database.name;
const walPath = `${dbPath}-wal`;
if (fs.existsSync(walPath)) {
const stats = fs.statSync(walPath);
return stats.size;
}
return 0;
} catch (error) {
console.error('[WAL清理] 获取 WAL 文件大小失败:', error.message);
return 0;
}
}
/**
* 启动时检查 WAL 文件大小,如果超过阈值则执行清理
* @param {number} threshold - 阈值(字节),默认 100MB
*/
function checkWalOnStartup(threshold = 100 * 1024 * 1024) {
try {
const walSize = getWalFileSize();
if (walSize > threshold) {
console.warn(`[WAL清理] ⚠ 启动时检测到 WAL 文件过大: ${(walSize / 1024 / 1024).toFixed(2)}MB`);
console.log('[WAL清理] 正在执行自动清理...');
const success = performCheckpoint();
if (success) {
const newSize = getWalFileSize();
console.log(`[WAL清理] ✓ 清理完成: ${walSize}${newSize} 字节`);
}
} else {
console.log(`[WAL清理] ✓ WAL 文件大小正常: ${(walSize / 1024 / 1024).toFixed(2)}MB`);
}
} catch (error) {
console.error('[WAL清理] 启动检查失败:', error.message);
}
}
/**
* 设置定期 WAL 检查点
* 每隔指定时间自动执行一次检查点,防止 WAL 文件无限增长
* @param {number} intervalHours - 间隔时间(小时),默认 24 小时
* @returns {NodeJS.Timeout} 定时器 ID可用于取消
*/
function schedulePeriodicCheckpoint(intervalHours = 24) {
const intervalMs = intervalHours * 60 * 60 * 1000;
const timerId = setInterval(() => {
const walSize = getWalFileSize();
console.log(`[WAL清理] 定期检查点执行中... (当前 WAL: ${(walSize / 1024 / 1024).toFixed(2)}MB)`);
performCheckpoint();
}, intervalMs);
console.log(`[WAL清理] ✓ 定期检查点已启用: 每 ${intervalHours} 小时执行一次`);
return timerId;
}
// 立即执行启动时检查
checkWalOnStartup(100 * 1024 * 1024); // 100MB 阈值
// 启动定期检查点24 小时)
let walCheckpointTimer = null;
if (process.env.WAL_CHECKPOINT_ENABLED !== 'false') {
const interval = parseInt(process.env.WAL_CHECKPOINT_INTERVAL_HOURS || '24', 10);
walCheckpointTimer = schedulePeriodicCheckpoint(interval);
} else {
console.log('[WAL清理] 定期检查点已禁用WAL_CHECKPOINT_ENABLED=false');
}
// 导出 WAL 管理函数
const WalManager = {
performCheckpoint,
getWalFileSize,
checkWalOnStartup,
schedulePeriodicCheckpoint
};
// 初始化数据库表
function initDatabase() {
// 用户表
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
-- OSS配置可选
oss_provider TEXT,
oss_region TEXT,
oss_access_key_id TEXT,
oss_access_key_secret TEXT,
oss_bucket TEXT,
oss_endpoint TEXT,
-- 上传工具API密钥
upload_api_key TEXT,
-- 用户状态
is_admin INTEGER DEFAULT 0,
is_active INTEGER DEFAULT 1,
is_banned INTEGER DEFAULT 0,
has_oss_config INTEGER DEFAULT 0,
-- 时间戳
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// 分享链接表
db.exec(`
CREATE TABLE IF NOT EXISTS shares (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
share_code TEXT UNIQUE NOT NULL,
share_path TEXT NOT NULL,
share_type TEXT DEFAULT 'file',
share_password TEXT,
-- 分享统计
view_count INTEGER DEFAULT 0,
download_count INTEGER DEFAULT 0,
-- 时间戳
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
expires_at DATETIME,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)
`);
// 系统设置表
db.exec(`
CREATE TABLE IF NOT EXISTS system_settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// 创建索引
db.exec(`
-- 基础索引
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_users_upload_api_key ON users(upload_api_key);
CREATE INDEX IF NOT EXISTS idx_shares_code ON shares(share_code);
CREATE INDEX IF NOT EXISTS idx_shares_user ON shares(user_id);
CREATE INDEX IF NOT EXISTS idx_shares_expires ON shares(expires_at);
-- ===== 性能优化复合索引P0 优先级修复) =====
-- 1. 分享链接复合索引share_code + expires_at
-- 优势:加速分享码查询(最常见的操作),同时过滤过期链接
-- 使用场景ShareDB.findByCode, 分享访问验证
CREATE INDEX IF NOT EXISTS idx_shares_code_expires ON shares(share_code, expires_at);
-- 注意system_logs 表的复合索引在表创建后创建第372行之后
-- 2. 活动日志复合索引user_id + created_at
-- 优势:快速查询用户最近的活动记录,支持时间范围过滤
-- 使用场景:用户活动历史、审计日志查询
-- CREATE INDEX IF NOT EXISTS idx_logs_user_created ON system_logs(user_id, created_at);
-- 3. 文件复合索引user_id + parent_path
-- 注意:当前系统使用 OSS不直接存储文件元数据到数据库
-- 如果未来需要文件系统功能,此索引将优化目录浏览性能
-- CREATE INDEX IF NOT EXISTS idx_files_user_parent ON files(user_id, parent_path);
`);
console.log('[数据库性能优化] ✓ 基础索引已创建');
console.log(' - idx_shares_code_expires: 分享码+过期时间');
// 数据库迁移添加upload_api_key字段如果不存在
try {
const columns = db.prepare("PRAGMA table_info(users)").all();
const hasUploadApiKey = columns.some(col => col.name === 'upload_api_key');
if (!hasUploadApiKey) {
db.exec(`ALTER TABLE users ADD COLUMN upload_api_key TEXT`);
console.log('数据库迁移添加upload_api_key字段完成');
}
} catch (error) {
console.error('数据库迁移失败:', error);
}
// 数据库迁移添加share_type字段如果不存在
try {
const shareColumns = db.prepare("PRAGMA table_info(shares)").all();
const hasShareType = shareColumns.some(col => col.name === 'share_type');
if (!hasShareType) {
db.exec(`ALTER TABLE shares ADD COLUMN share_type TEXT DEFAULT 'file'`);
console.log('数据库迁移添加share_type字段完成');
}
} catch (error) {
console.error('数据库迁移share_type失败:', error);
}
// 数据库迁移:邮箱验证字段
try {
const columns = db.prepare("PRAGMA table_info(users)").all();
const hasVerified = columns.some(col => col.name === 'is_verified');
const hasVerifyToken = columns.some(col => col.name === 'verification_token');
const hasVerifyExpires = columns.some(col => col.name === 'verification_expires_at');
if (!hasVerified) {
db.exec(`ALTER TABLE users ADD COLUMN is_verified INTEGER DEFAULT 0`);
}
if (!hasVerifyToken) {
db.exec(`ALTER TABLE users ADD COLUMN verification_token TEXT`);
}
if (!hasVerifyExpires) {
db.exec(`ALTER TABLE users ADD COLUMN verification_expires_at DATETIME`);
}
// 注意:不再自动将未验证用户设为已验证
// 仅修复 is_verified 为 NULL 的旧数据(添加字段前创建的用户)
// 这些用户没有 verification_token说明是在邮箱验证功能上线前注册的
db.exec(`UPDATE users SET is_verified = 1 WHERE is_verified IS NULL AND verification_token IS NULL`);
} catch (error) {
console.error('数据库迁移(邮箱验证)失败:', error);
}
// 数据库迁移密码重置Token表
try {
db.exec(`
CREATE TABLE IF NOT EXISTS password_reset_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
token TEXT UNIQUE NOT NULL,
expires_at DATETIME NOT NULL,
used INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)
`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_reset_tokens_token ON password_reset_tokens(token);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_reset_tokens_user ON password_reset_tokens(user_id);`);
} catch (error) {
console.error('数据库迁移密码重置Token失败:', error);
}
// 系统日志表
db.exec(`
CREATE TABLE IF NOT EXISTS system_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
level TEXT NOT NULL DEFAULT 'info',
category TEXT NOT NULL,
action TEXT NOT NULL,
message TEXT NOT NULL,
user_id INTEGER,
username TEXT,
ip_address TEXT,
user_agent TEXT,
details TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL
)
`);
// 日志表索引
db.exec(`
CREATE INDEX IF NOT EXISTS idx_logs_created_at ON system_logs(created_at);
CREATE INDEX IF NOT EXISTS idx_logs_category ON system_logs(category);
CREATE INDEX IF NOT EXISTS idx_logs_level ON system_logs(level);
CREATE INDEX IF NOT EXISTS idx_logs_user_id ON system_logs(user_id);
-- ===== 性能优化复合索引P0 优先级修复) =====
-- 活动日志复合索引user_id + created_at
-- 优势:快速查询用户最近的活动记录,支持时间范围过滤
-- 使用场景:用户活动历史、审计日志查询
CREATE INDEX IF NOT EXISTS idx_logs_user_created ON system_logs(user_id, created_at);
`);
console.log('[数据库性能优化] ✓ 日志表复合索引已创建');
console.log(' - idx_logs_user_created: 用户+创建时间');
// 数据库迁移:添加 storage_used 字段P0 性能优化)
try {
const columns = db.prepare("PRAGMA table_info(users)").all();
const hasStorageUsed = columns.some(col => col.name === 'storage_used');
if (!hasStorageUsed) {
db.exec(`ALTER TABLE users ADD COLUMN storage_used INTEGER DEFAULT 0`);
console.log('[数据库迁移] ✓ storage_used 字段已添加');
}
} catch (error) {
console.error('[数据库迁移] storage_used 字段添加失败:', error);
}
console.log('数据库初始化完成');
}
// 创建默认管理员账号
function createDefaultAdmin() {
const adminExists = db.prepare('SELECT id FROM users WHERE is_admin = 1').get();
if (!adminExists) {
// 从环境变量读取管理员账号密码,如果没有则使用默认值
const adminUsername = process.env.ADMIN_USERNAME || 'admin';
const adminPassword = process.env.ADMIN_PASSWORD || 'admin123';
const hashedPassword = bcrypt.hashSync(adminPassword, 10);
db.prepare(`
INSERT INTO users (
username, email, password,
is_admin, is_active, has_oss_config, is_verified
) VALUES (?, ?, ?, ?, ?, ?, ?)
`).run(
adminUsername,
`${adminUsername}@example.com`,
hashedPassword,
1,
1,
0, // 管理员不需要OSS配置
1 // 管理员默认已验证
);
console.log('默认管理员账号已创建');
console.log('用户名:', adminUsername);
console.log('密码: ********');
console.log('⚠️ 请登录后立即修改密码!');
}
}
// 用户相关操作
const UserDB = {
// 创建用户
create(userData) {
const hashedPassword = bcrypt.hashSync(userData.password, 10);
const hasOssConfig = userData.oss_provider && userData.oss_access_key_id && userData.oss_access_key_secret && userData.oss_bucket ? 1 : 0;
// 对验证令牌进行哈希存储(与 VerificationDB.setVerification 保持一致)
const hashedVerificationToken = userData.verification_token
? crypto.createHash('sha256').update(userData.verification_token).digest('hex')
: null;
const stmt = db.prepare(`
INSERT INTO users (
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const result = stmt.run(
userData.username,
userData.email,
hashedPassword,
userData.oss_provider || null,
userData.oss_region || null,
userData.oss_access_key_id || null,
userData.oss_access_key_secret || null,
userData.oss_bucket || null,
userData.oss_endpoint || null,
hasOssConfig,
userData.is_verified !== undefined ? userData.is_verified : 0,
hashedVerificationToken,
userData.verification_expires_at || null
);
return result.lastInsertRowid;
},
// 根据用户名查找
findByUsername(username) {
return db.prepare('SELECT * FROM users WHERE username = ?').get(username);
},
// 根据邮箱查找
findByEmail(email) {
return db.prepare('SELECT * FROM users WHERE email = ?').get(email);
},
// 根据ID查找
findById(id) {
return db.prepare('SELECT * FROM users WHERE id = ?').get(id);
},
// 验证密码
verifyPassword(plainPassword, hashedPassword) {
return bcrypt.compareSync(plainPassword, hashedPassword);
},
/**
* 字段类型验证函数
* 确保所有字段值类型符合数据库要求
* @param {string} fieldName - 字段名
* @param {*} value - 字段值
* @returns {boolean} 是否有效
* @private
*/
_validateFieldValue(fieldName, value) {
// 字段类型白名单(根据数据库表结构定义)
const FIELD_TYPES = {
// 文本类型字段
'username': 'string',
'email': 'string',
'password': 'string',
'oss_provider': 'string',
'oss_region': 'string',
'oss_access_key_id': 'string',
'oss_access_key_secret': 'string',
'oss_bucket': 'string',
'oss_endpoint': 'string',
'upload_api_key': 'string',
'verification_token': 'string',
'verification_expires_at': 'string',
'storage_permission': 'string',
'current_storage_type': 'string',
'theme_preference': 'string',
// 数值类型字段
'is_admin': 'number',
'is_active': 'number',
'is_banned': 'is_banned',
'has_oss_config': 'number',
'is_verified': 'number',
'local_storage_quota': 'number',
'local_storage_used': 'number'
};
const expectedType = FIELD_TYPES[fieldName];
// 如果字段不在类型定义中,允许通过(向后兼容)
if (!expectedType) {
return true;
}
// 检查类型匹配
if (expectedType === 'string') {
return typeof value === 'string';
} else if (expectedType === 'number') {
// 允许数值或可转换为数值的字符串
return typeof value === 'number' || (typeof value === 'string' && !isNaN(Number(value)));
}
return true;
},
/**
* 验证字段映射完整性
* 确保 FIELD_MAP 中定义的所有字段都在数据库表中存在
* @returns {Object} 验证结果 { valid: boolean, missing: string[], extra: string[] }
* @private
*/
_validateFieldMapping() {
// 字段映射白名单:防止别名攻击(如 toString、valueOf 等原型方法)
const FIELD_MAP = {
// 基础字段
'username': 'username',
'email': 'email',
'password': 'password',
// OSS 配置字段
'oss_provider': 'oss_provider',
'oss_region': 'oss_region',
'oss_access_key_id': 'oss_access_key_id',
'oss_access_key_secret': 'oss_access_key_secret',
'oss_bucket': 'oss_bucket',
'oss_endpoint': 'oss_endpoint',
// 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',
// 验证字段
'is_verified': 'is_verified',
'verification_token': 'verification_token',
'verification_expires_at': 'verification_expires_at',
// 存储配置字段
'storage_permission': 'storage_permission',
'current_storage_type': 'current_storage_type',
'local_storage_quota': 'local_storage_quota',
'local_storage_used': 'local_storage_used',
// 偏好设置
'theme_preference': 'theme_preference'
};
try {
// 获取数据库表的实际列信息
const columns = db.prepare("PRAGMA table_info(users)").all();
const dbFields = new Set(columns.map(col => col.name));
// 检查 FIELD_MAP 中的字段是否都在数据库中存在
const mappedFields = new Set(Object.values(FIELD_MAP));
const missingFields = [];
const extraFields = [];
for (const field of mappedFields) {
if (!dbFields.has(field)) {
missingFields.push(field);
}
}
// 检查数据库中是否有 FIELD_MAP 未定义的字段(可选)
for (const dbField of dbFields) {
if (!mappedFields.has(dbField) && !['id', 'created_at', 'updated_at'].includes(dbField)) {
extraFields.push(dbField);
}
}
const isValid = missingFields.length === 0;
if (!isValid) {
console.error(`[数据库错误] 字段映射验证失败,缺失字段: ${missingFields.join(', ')}`);
}
if (extraFields.length > 0) {
console.warn(`[数据库警告] 数据库存在 FIELD_MAP 未定义的字段: ${extraFields.join(', ')}`);
}
return { valid: isValid, missing: missingFields, extra: extraFields };
} catch (error) {
console.error(`[数据库错误] 字段映射验证失败: ${error.message}`);
return { valid: false, missing: [], extra: [], error: error.message };
}
},
// 更新用户
// 安全修复:使用字段映射白名单,防止 SQL 注入和原型污染攻击
update(id, updates) {
// 字段映射白名单:防止别名攻击(如 toString、valueOf 等原型方法)
const FIELD_MAP = {
// 基础字段
'username': 'username',
'email': 'email',
'password': 'password',
// OSS 配置字段
'oss_provider': 'oss_provider',
'oss_region': 'oss_region',
'oss_access_key_id': 'oss_access_key_id',
'oss_access_key_secret': 'oss_access_key_secret',
'oss_bucket': 'oss_bucket',
'oss_endpoint': 'oss_endpoint',
// 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',
// 验证字段
'is_verified': 'is_verified',
'verification_token': 'verification_token',
'verification_expires_at': 'verification_expires_at',
// 存储配置字段
'storage_permission': 'storage_permission',
'current_storage_type': 'current_storage_type',
'local_storage_quota': 'local_storage_quota',
'local_storage_used': 'local_storage_used',
// 偏好设置
'theme_preference': 'theme_preference'
};
const fields = [];
const values = [];
const rejectedFields = []; // 记录被拒绝的字段(类型不符)
for (const [key, value] of Object.entries(updates)) {
// 安全检查 1确保是对象自身的属性防止原型污染
// 使用 Object.prototype.hasOwnProperty.call() 避免原型链污染
if (!Object.prototype.hasOwnProperty.call(updates, key)) {
console.warn(`[安全警告] 跳过非自身属性: ${key} (类型: ${typeof key})`);
continue;
}
// 安全检查 2字段名必须是字符串类型
if (typeof key !== 'string' || key.trim() === '') {
console.warn(`[安全警告] 跳过无效字段名: ${key} (类型: ${typeof key})`);
rejectedFields.push({ field: key, reason: '字段名不是有效字符串' });
continue;
}
// 安全检查 3验证字段映射防止别名攻击
const mappedField = FIELD_MAP[key];
if (!mappedField) {
console.warn(`[安全警告] 尝试更新非法字段: ${key}`);
rejectedFields.push({ field: key, reason: '字段不在白名单中' });
continue;
}
// 安全检查 4确保字段名不包含特殊字符或 SQL 关键字
// 只允许字母、数字和下划线
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(mappedField)) {
console.warn(`[安全警告] 字段名包含非法字符: ${mappedField}`);
rejectedFields.push({ field: key, reason: '字段名包含非法字符' });
continue;
}
// 安全检查 5验证字段值类型第二轮修复
if (!this._validateFieldValue(key, value)) {
const expectedType = {
'username': 'string', 'email': 'string', 'password': 'string',
'oss_provider': 'string', 'oss_region': 'string',
'oss_access_key_id': 'string', 'oss_access_key_secret': 'string',
'oss_bucket': 'string', 'oss_endpoint': 'string',
'upload_api_key': 'string', 'verification_token': 'string',
'verification_expires_at': 'string', 'storage_permission': 'string',
'current_storage_type': 'string', 'theme_preference': 'string',
'is_admin': 'number', 'is_active': 'number', 'is_banned': 'number',
'has_oss_config': 'number', 'is_verified': 'number',
'local_storage_quota': 'number', 'local_storage_used': 'number'
}[key];
console.warn(`[类型检查] 字段 ${key} 值类型不符: 期望 ${expectedType}, 实际 ${typeof value}, 值: ${JSON.stringify(value)}`);
rejectedFields.push({ field: key, reason: `值类型不符 (期望: ${expectedType}, 实际: ${typeof value})` });
continue;
}
// 特殊处理密码字段(需要哈希)
if (key === 'password') {
fields.push(`${mappedField} = ?`);
values.push(bcrypt.hashSync(value, 10));
} else {
fields.push(`${mappedField} = ?`);
values.push(value);
}
}
// 记录被拒绝的字段(用于调试)
if (rejectedFields.length > 0) {
console.log(`[类型检查] 用户 ${id} 更新请求拒绝了 ${rejectedFields.length} 个字段:`, rejectedFields);
}
// 如果没有有效字段,返回空结果
if (fields.length === 0) {
console.warn(`[安全警告] 没有有效字段可更新用户ID: ${id}`);
return { changes: 0, rejectedFields };
}
// 添加 updated_at 时间戳
fields.push('updated_at = CURRENT_TIMESTAMP');
values.push(id);
// 使用参数化查询执行更新(防止 SQL 注入)
const stmt = db.prepare(`UPDATE users SET ${fields.join(', ')} WHERE id = ?`);
const result = stmt.run(...values);
// 附加被拒绝字段信息到返回结果
result.rejectedFields = rejectedFields;
return result;
},
// 获取所有用户
getAll(filters = {}) {
let query = 'SELECT * FROM users WHERE 1=1';
const params = [];
if (filters.is_admin !== undefined) {
query += ' AND is_admin = ?';
params.push(filters.is_admin);
}
if (filters.is_banned !== undefined) {
query += ' AND is_banned = ?';
params.push(filters.is_banned);
}
query += ' ORDER BY created_at DESC';
return db.prepare(query).all(...params);
},
// 删除用户
delete(id) {
return db.prepare('DELETE FROM users WHERE id = ?').run(id);
},
// 封禁/解封用户
setBanStatus(id, isBanned) {
return db.prepare('UPDATE users SET is_banned = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
.run(isBanned ? 1 : 0, id);
}
};
// 分享链接相关操作
const ShareDB = {
// 生成随机分享码
generateShareCode(length = 8) {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
const bytes = crypto.randomBytes(length);
let code = '';
for (let i = 0; i < length; i++) {
code += chars[bytes[i] % chars.length];
}
return code;
},
// 创建分享链接
// 创建分享链接
create(userId, options = {}) {
const {
share_type = 'file',
file_path = '',
file_name = '',
password = null,
expiry_days = null
} = options;
let shareCode;
let attempts = 0;
// 尝试生成唯一的分享码
do {
shareCode = this.generateShareCode();
attempts++;
if (attempts > 10) {
shareCode = this.generateShareCode(10); // 增加长度
}
} while (this.findByCode(shareCode) && attempts < 20);
// 计算过期时间
let expiresAt = null;
if (expiry_days) {
const expireDate = new Date();
expireDate.setDate(expireDate.getDate() + parseInt(expiry_days));
// 使用本地时区时间,而不是UTC时间
// 这样前端解析时会正确显示为本地时间
const year = expireDate.getFullYear();
const month = String(expireDate.getMonth() + 1).padStart(2, '0');
const day = String(expireDate.getDate()).padStart(2, '0');
const hours = String(expireDate.getHours()).padStart(2, '0');
const minutes = String(expireDate.getMinutes()).padStart(2, '0');
const seconds = String(expireDate.getSeconds()).padStart(2, '0');
expiresAt = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
const stmt = db.prepare(`
INSERT INTO shares (user_id, share_code, share_path, share_type, share_password, expires_at)
VALUES (?, ?, ?, ?, ?, ?)
`);
const hashedPassword = password ? bcrypt.hashSync(password, 10) : null;
// 修复:正确处理不同类型的分享路径
let sharePath;
if (share_type === 'file') {
// 单文件分享:使用完整文件路径
sharePath = file_path;
} else if (share_type === 'directory') {
// 文件夹分享:使用文件夹路径
sharePath = file_path;
} else {
// all类型分享根目录
sharePath = '/';
}
const result = stmt.run(
userId,
shareCode,
sharePath,
share_type,
hashedPassword,
expiresAt
);
return {
id: result.lastInsertRowid,
share_code: shareCode,
share_type: share_type,
expires_at: expiresAt,
};
},
// 根据分享码查找
// 增强: 检查分享者是否被封禁(被封禁用户的分享不可访问)
// ===== 性能优化P0 优先级修复):只查询必要字段,避免 N+1 查询 =====
// 移除了敏感字段oss_access_key_id, oss_access_key_secret不需要传递给分享访问者
findByCode(shareCode) {
const result = db.prepare(`
SELECT
s.id, s.user_id, s.share_code, s.share_path, s.share_type,
s.view_count, s.download_count, s.created_at, s.expires_at,
u.username,
-- OSS 配置(访问分享文件所需)
u.oss_provider, u.oss_region, u.oss_bucket, u.oss_endpoint,
-- 用户偏好(主题)
u.theme_preference,
-- 安全检查
u.is_banned
FROM shares s
JOIN users u ON s.user_id = u.id
WHERE s.share_code = ?
AND (s.expires_at IS NULL OR s.expires_at > datetime('now', 'localtime'))
AND u.is_banned = 0
`).get(shareCode);
return result;
},
// 根据ID查找
findById(id) {
return db.prepare('SELECT * FROM shares WHERE id = ?').get(id);
},
// 验证分享密码
verifyPassword(plainPassword, hashedPassword) {
return bcrypt.compareSync(plainPassword, hashedPassword);
},
// 获取用户的所有分享
getUserShares(userId) {
return db.prepare(`
SELECT * FROM shares
WHERE user_id = ?
ORDER BY created_at DESC
`).all(userId);
},
// 增加查看次数
incrementViewCount(shareCode) {
return db.prepare(`
UPDATE shares
SET view_count = view_count + 1
WHERE share_code = ?
`).run(shareCode);
},
// 增加下载次数
incrementDownloadCount(shareCode) {
return db.prepare(`
UPDATE shares
SET download_count = download_count + 1
WHERE share_code = ?
`).run(shareCode);
},
// 删除分享
delete(id, userId = null) {
if (userId) {
return db.prepare('DELETE FROM shares WHERE id = ? AND user_id = ?').run(id, userId);
}
return db.prepare('DELETE FROM shares WHERE id = ?').run(id);
},
// 获取所有分享(管理员)
getAll() {
return db.prepare(`
SELECT s.*, u.username
FROM shares s
JOIN users u ON s.user_id = u.id
ORDER BY s.created_at DESC
`).all();
}
};
// 系统设置管理
const SettingsDB = {
// 获取设置
get(key) {
const row = db.prepare('SELECT value FROM system_settings WHERE key = ?').get(key);
return row ? row.value : null;
},
// 设置值
set(key, value) {
db.prepare(`
INSERT INTO system_settings (key, value, updated_at)
VALUES (?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(key) DO UPDATE SET
value = excluded.value,
updated_at = CURRENT_TIMESTAMP
`).run(key, value);
},
// 获取所有设置
getAll() {
return db.prepare('SELECT key, value FROM system_settings').all();
},
// ===== 统一 OSS 配置管理(管理员配置,所有用户共享) =====
/**
* 获取统一的 OSS 配置
* @returns {Object|null} OSS 配置对象,如果未配置则返回 null
*/
getUnifiedOssConfig() {
const config = {
provider: this.get('oss_provider'),
region: this.get('oss_region'),
access_key_id: this.get('oss_access_key_id'),
access_key_secret: this.get('oss_access_key_secret'),
bucket: this.get('oss_bucket'),
endpoint: this.get('oss_endpoint')
};
// 检查是否所有必需字段都已配置
if (!config.provider || !config.access_key_id || !config.access_key_secret || !config.bucket) {
return null;
}
// 安全修复:解密 OSS Access Key Secret
try {
if (config.access_key_secret) {
config.access_key_secret = decryptSecret(config.access_key_secret);
}
} catch (error) {
console.error('[安全] 解密统一 OSS 配置失败:', error.message);
return null;
}
return config;
},
/**
* 设置统一的 OSS 配置
* @param {Object} ossConfig - OSS 配置对象
* @param {string} ossConfig.provider - 服务商aliyun/tencent/aws
* @param {string} ossConfig.region - 区域
* @param {string} ossConfig.access_key_id - Access Key ID
* @param {string} ossConfig.access_key_secret - Access Key Secret
* @param {string} ossConfig.bucket - 存储桶名称
* @param {string} [ossConfig.endpoint] - 自定义 Endpoint可选
*/
setUnifiedOssConfig(ossConfig) {
this.set('oss_provider', ossConfig.provider);
this.set('oss_region', ossConfig.region);
this.set('oss_access_key_id', ossConfig.access_key_id);
// 安全修复:加密存储 OSS Access Key Secret
try {
const encryptedSecret = encryptSecret(ossConfig.access_key_secret);
this.set('oss_access_key_secret', encryptedSecret);
} catch (error) {
console.error('[安全] 加密统一 OSS 配置失败:', error.message);
throw new Error('保存 OSS 配置失败:加密错误');
}
this.set('oss_bucket', ossConfig.bucket);
this.set('oss_endpoint', ossConfig.endpoint || '');
console.log('[系统设置] 统一 OSS 配置已更新(已加密)');
},
/**
* 删除统一的 OSS 配置
*/
clearUnifiedOssConfig() {
db.prepare('DELETE FROM system_settings WHERE key LIKE "oss_%"').run();
console.log('[系统设置] 统一 OSS 配置已清除');
},
/**
* 检查是否已配置统一的 OSS
* @returns {boolean}
*/
hasUnifiedOssConfig() {
return this.getUnifiedOssConfig() !== null;
}
};
// 邮箱验证管理(增强安全:哈希存储)
const VerificationDB = {
setVerification(userId, token, expiresAtMs) {
// 对令牌进行哈希后存储
const hashedToken = crypto.createHash('sha256').update(token).digest('hex');
db.prepare(`
UPDATE users
SET verification_token = ?, verification_expires_at = ?, is_verified = 0, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`).run(hashedToken, expiresAtMs, userId);
},
consumeVerificationToken(token) {
// 对用户提供的令牌进行哈希
const hashedToken = crypto.createHash('sha256').update(token).digest('hex');
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 is_verified = 0
`).get(hashedToken);
if (!row) return null;
db.prepare(`
UPDATE users
SET is_verified = 1, verification_token = NULL, verification_expires_at = NULL, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`).run(row.id);
return row;
}
};
// 密码重置 Token 管理(增强安全:哈希存储)
const PasswordResetTokenDB = {
// 创建令牌时存储哈希值
create(userId, token, expiresAtMs) {
// 对令牌进行哈希后存储(防止数据库泄露时令牌被直接使用)
const hashedToken = crypto.createHash('sha256').update(token).digest('hex');
db.prepare(`
INSERT INTO password_reset_tokens (user_id, token, expires_at, used)
VALUES (?, ?, ?, 0)
`).run(userId, hashedToken, expiresAtMs);
},
// 验证令牌时先哈希再比较
use(token) {
// 对用户提供的令牌进行哈希
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 -- 兼容旧的字符串时间
)
`).get(hashedToken);
if (!row) return null;
// 立即标记为已使用(防止重复使用)
db.prepare(`UPDATE password_reset_tokens SET used = 1 WHERE id = ?`).run(row.id);
return row;
}
};
// 初始化默认设置
function initDefaultSettings() {
// 默认上传限制为10GB
if (!SettingsDB.get('max_upload_size')) {
SettingsDB.set('max_upload_size', '10737418240'); // 10GB in bytes
}
// 默认全局主题为暗色
if (!SettingsDB.get('global_theme')) {
SettingsDB.set('global_theme', 'dark');
}
}
// 数据库迁移 - 主题偏好字段
function migrateThemePreference() {
try {
const columns = db.prepare("PRAGMA table_info(users)").all();
const hasThemePreference = columns.some(col => col.name === 'theme_preference');
if (!hasThemePreference) {
console.log('[数据库迁移] 添加主题偏好字段...');
db.exec(`ALTER TABLE users ADD COLUMN theme_preference TEXT DEFAULT NULL`);
console.log('[数据库迁移] ✓ 主题偏好字段已添加');
}
} catch (error) {
console.error('[数据库迁移] 主题偏好迁移失败:', error);
}
}
// 数据库版本迁移 - v2.0 本地存储功能
function migrateToV2() {
try {
const columns = db.prepare("PRAGMA table_info(users)").all();
const hasStoragePermission = columns.some(col => col.name === 'storage_permission');
if (!hasStoragePermission) {
console.log('[数据库迁移] 检测到旧版本数据库,开始升级到 v2.0...');
// 添加本地存储相关字段
db.exec(`
ALTER TABLE users ADD COLUMN storage_permission TEXT DEFAULT 'sftp_only';
ALTER TABLE users ADD COLUMN current_storage_type TEXT DEFAULT 'sftp';
ALTER TABLE users ADD COLUMN local_storage_quota INTEGER DEFAULT 1073741824;
ALTER TABLE users ADD COLUMN local_storage_used INTEGER DEFAULT 0;
`);
console.log('[数据库迁移] ✓ 用户表已升级');
// 为分享表添加存储类型字段
const shareColumns = db.prepare("PRAGMA table_info(shares)").all();
const hasShareStorageType = shareColumns.some(col => col.name === 'storage_type');
if (!hasShareStorageType) {
db.exec(`ALTER TABLE shares ADD COLUMN storage_type TEXT DEFAULT 'sftp';`);
console.log('[数据库迁移] ✓ 分享表已升级');
}
console.log('[数据库迁移] ✅ 数据库升级到 v2.0 完成!本地存储功能已启用');
}
} catch (error) {
console.error('[数据库迁移] 迁移失败:', error);
throw error;
}
}
// 数据库版本迁移 - v3.0 SFTP → OSS
function migrateToOss() {
try {
const columns = db.prepare("PRAGMA table_info(users)").all();
const hasOssProvider = columns.some(col => col.name === 'oss_provider');
if (!hasOssProvider) {
console.log('[数据库迁移] 检测到 SFTP 版本,开始升级到 v3.0 OSS...');
// 添加 OSS 相关字段
db.exec(`
ALTER TABLE users ADD COLUMN oss_provider TEXT DEFAULT NULL;
ALTER TABLE users ADD COLUMN oss_region TEXT DEFAULT NULL;
ALTER TABLE users ADD COLUMN oss_access_key_id TEXT DEFAULT NULL;
ALTER TABLE users ADD COLUMN oss_access_key_secret TEXT DEFAULT NULL;
ALTER TABLE users ADD COLUMN oss_bucket TEXT DEFAULT NULL;
ALTER TABLE users ADD COLUMN oss_endpoint TEXT DEFAULT NULL;
ALTER TABLE users ADD COLUMN has_oss_config INTEGER DEFAULT 0;
`);
console.log('[数据库迁移] ✓ OSS 字段已添加');
}
// 修复:无论 OSS 字段是否刚添加,都要确保更新现有的 sftp 数据
// 检查是否有用户仍使用 sftp 类型
const sftpUsers = db.prepare("SELECT COUNT(*) as count FROM users WHERE storage_permission = 'sftp_only' OR current_storage_type = 'sftp'").get();
if (sftpUsers.count > 0) {
console.log(`[数据库迁移] 检测到 ${sftpUsers.count} 个用户仍使用 sftp 类型,正在更新...`);
// 更新存储权限枚举值sftp_only → oss_only
db.exec(`UPDATE users SET storage_permission = 'oss_only' WHERE storage_permission = 'sftp_only'`);
console.log('[数据库迁移] ✓ 存储权限枚举值已更新');
// 更新存储类型sftp → oss
db.exec(`UPDATE users SET current_storage_type = 'oss' WHERE current_storage_type = 'sftp'`);
console.log('[数据库迁移] ✓ 存储类型已更新');
// 更新分享表的存储类型
const shareColumns = db.prepare("PRAGMA table_info(shares)").all();
const hasStorageType = shareColumns.some(col => col.name === 'storage_type');
if (hasStorageType) {
db.exec(`UPDATE shares SET storage_type = 'oss' WHERE storage_type = 'sftp'`);
console.log('[数据库迁移] ✓ 分享表存储类型已更新');
}
console.log('[数据库迁移] ✅ SFTP → OSS 数据更新完成!');
}
} catch (error) {
console.error('[数据库迁移] OSS 迁移失败:', error);
// 不抛出错误,允许服务继续启动
}
}
// 数据库版本迁移 - 添加 OSS 配额字段
function migrateAddOssQuota() {
try {
const columns = db.prepare("PRAGMA table_info(users)").all();
const hasOssQuota = columns.some(col => col.name === 'oss_storage_quota');
if (!hasOssQuota) {
console.log('[数据库迁移] 添加 OSS 配额字段...');
db.exec();
console.log('[数据库迁移] ✅ OSS 配额字段已添加 (默认 0 表示无限制)');
}
} catch (error) {
console.error('[数据库迁移] OSS 配额迁移失败:', error);
}
}
// 系统日志操作
const SystemLogDB = {
// 日志级别常量
LEVELS: {
DEBUG: 'debug',
INFO: 'info',
WARN: 'warn',
ERROR: 'error'
},
// 日志分类常量
CATEGORIES: {
AUTH: 'auth', // 认证相关(登录、登出、注册)
USER: 'user', // 用户管理(创建、修改、删除、封禁)
FILE: 'file', // 文件操作(上传、下载、删除、重命名)
SHARE: 'share', // 分享操作(创建、删除、访问)
SYSTEM: 'system', // 系统操作(设置修改、服务启动)
SECURITY: 'security' // 安全事件(登录失败、暴力破解、异常访问)
},
// 写入日志
log({ level = 'info', category, action, message, userId = null, username = null, ipAddress = null, userAgent = null, details = null }) {
try {
const stmt = db.prepare(`
INSERT INTO system_logs (level, category, action, message, user_id, username, ip_address, user_agent, details, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now', 'localtime'))
`);
const detailsStr = details ? (typeof details === 'string' ? details : JSON.stringify(details)) : null;
stmt.run(level, category, action, message, userId, username, ipAddress, userAgent, detailsStr);
} catch (error) {
console.error('写入日志失败:', error);
}
},
// 查询日志(支持分页和筛选)
query({ page = 1, pageSize = 50, level = null, category = null, userId = null, startDate = null, endDate = null, keyword = null }) {
let sql = 'SELECT * FROM system_logs WHERE 1=1';
let countSql = 'SELECT COUNT(*) as total FROM system_logs WHERE 1=1';
const params = [];
if (level) {
sql += ' AND level = ?';
countSql += ' AND level = ?';
params.push(level);
}
if (category) {
sql += ' AND category = ?';
countSql += ' AND category = ?';
params.push(category);
}
if (userId) {
sql += ' AND user_id = ?';
countSql += ' AND user_id = ?';
params.push(userId);
}
if (startDate) {
sql += ' AND created_at >= ?';
countSql += ' AND created_at >= ?';
params.push(startDate);
}
if (endDate) {
sql += ' AND created_at <= ?';
countSql += ' AND created_at <= ?';
params.push(endDate);
}
if (keyword) {
sql += ' AND (message LIKE ? OR username LIKE ? OR action LIKE ?)';
countSql += ' AND (message LIKE ? OR username LIKE ? OR action LIKE ?)';
const kw = `%${keyword}%`;
params.push(kw, kw, kw);
}
// 获取总数
const totalResult = db.prepare(countSql).get(...params);
const total = totalResult ? totalResult.total : 0;
// 分页查询
sql += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
const offset = (page - 1) * pageSize;
const logs = db.prepare(sql).all(...params, pageSize, offset);
return {
logs,
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize)
};
},
// 获取最近的日志
getRecent(limit = 100) {
return db.prepare('SELECT * FROM system_logs ORDER BY created_at DESC LIMIT ?').all(limit);
},
// 按分类统计
getStatsByCategory() {
return db.prepare(`
SELECT category, COUNT(*) as count
FROM system_logs
GROUP BY category
ORDER BY count DESC
`).all();
},
// 按日期统计最近7天
getStatsByDate(days = 7) {
return db.prepare(`
SELECT DATE(created_at) as date, COUNT(*) as count
FROM system_logs
WHERE created_at >= datetime('now', 'localtime', '-' || ? || ' days')
GROUP BY DATE(created_at)
ORDER BY date DESC
`).all(days);
},
// 清理旧日志(保留指定天数)
cleanup(keepDays = 90) {
const result = db.prepare(`
DELETE FROM system_logs
WHERE created_at < datetime('now', 'localtime', '-' || ? || ' days')
`).run(keepDays);
return result.changes;
}
};
// 事务工具函数
const TransactionDB = {
/**
* 在事务中执行操作
* @param {Function} fn - 要执行的函数,接收 db 作为参数
* @returns {*} 函数返回值
* @throws {Error} 如果事务失败则抛出错误
*/
run(fn) {
const transaction = db.transaction((callback) => {
return callback(db);
});
return transaction(fn);
},
/**
* 删除用户及其所有相关数据(使用事务)
* @param {number} userId - 用户ID
* @returns {object} 删除结果
*/
deleteUserWithData(userId) {
return this.run(() => {
// 1. 删除用户的所有分享
const sharesDeleted = db.prepare('DELETE FROM shares WHERE user_id = ?').run(userId);
// 2. 删除密码重置令牌
const tokensDeleted = db.prepare('DELETE FROM password_reset_tokens WHERE user_id = ?').run(userId);
// 3. 更新日志中的用户引用(设为 NULL保留日志记录
db.prepare('UPDATE system_logs SET user_id = NULL WHERE user_id = ?').run(userId);
// 4. 删除用户记录
const userDeleted = db.prepare('DELETE FROM users WHERE id = ?').run(userId);
return {
sharesDeleted: sharesDeleted.changes,
tokensDeleted: tokensDeleted.changes,
userDeleted: userDeleted.changes
};
});
}
};
// 初始化数据库
initDatabase();
createDefaultAdmin();
initDefaultSettings();
migrateToV2(); // 执行数据库迁移
migrateThemePreference(); // 主题偏好迁移
migrateToOss(); // SFTP → OSS 迁移
migrateAddOssQuota(); // 添加 OSS 配额字段
module.exports = {
db,
UserDB,
ShareDB,
SettingsDB,
VerificationDB,
PasswordResetTokenDB,
SystemLogDB,
TransactionDB,
WalManager
};