const express = require('express'); const multer = require('multer'); const { nanoid } = require('nanoid'); const QRCode = require('qrcode'); const path = require('path'); const fs = require('fs'); const os = require('os'); const session = require('express-session'); const crypto = require('crypto'); const Database = require('better-sqlite3'); const svgCaptcha = require('svg-captcha'); const Iconv = require('iconv-lite'); const bcrypt = require('bcrypt'); const rateLimit = require('express-rate-limit'); const app = express(); const PORT = process.env.PORT || 3344; const BASE_URL = process.env.BASE_URL || `http://${getLocalIP()}:${PORT}`; const UPLOAD_DIR = path.join(__dirname, 'uploads'); const DB_PATH = process.env.DB_PATH || path.join(__dirname, 'data', 'data.db'); // === 安全配置 === const BCRYPT_ROUNDS = 12; const UPLOAD_EXTENSIONS = new Set([ // 文档类 '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt', // 图片类 '.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', // 压缩类 '.zip', '.rar', '.7z', '.tar', '.gz' ]); // === 初始化 SQLite === const db = new Database(DB_PATH); db.pragma('journal_mode = WAL'); db.pragma('foreign_keys = ON'); // 用户表 db.exec(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'admin', created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ) `); // 文件表 db.exec(` CREATE TABLE IF NOT EXISTS files ( id INTEGER PRIMARY KEY AUTOINCREMENT, code TEXT NOT NULL UNIQUE, original_name TEXT NOT NULL, saved_name TEXT NOT NULL, size INTEGER NOT NULL, mime_type TEXT, uploaded_at INTEGER NOT NULL, downloads INTEGER NOT NULL DEFAULT 0, uploaded_by TEXT ) `); // 索引 db.exec(`CREATE INDEX IF NOT EXISTS idx_files_code ON files(code)`); // === 初始化默认用户 === const SALT = process.env.SALT || 'docmgr-salt-2026'; // 密码哈希函数(使用 bcrypt) async function hashPassword(pwd) { return await bcrypt.hash(pwd, BCRYPT_ROUNDS); } function verifyPassword(pwd, hash) { return bcrypt.compareSync(pwd, hash); } // 默认管理员(超级管理员,拥有所有权限) const DEFAULT_ADMIN_USER = 'gwalrusadmin'; const DEFAULT_ADMIN_PASS = process.env.ADMIN_PASSWORD || 'admin123'; const USERS = parseUsers(process.env.USERS || `${DEFAULT_ADMIN_USER}:${DEFAULT_ADMIN_PASS}`); function parseUsers(str) { const map = {}; str.split(',').forEach(pair => { const [u, p] = pair.split(':'); if (u && p) map[u.trim()] = p.trim(); }); return map; } async function initUsers() { const now = Date.now(); const insert = db.prepare( 'INSERT OR IGNORE INTO users (username, password_hash, role, created_at, updated_at) VALUES (?, ?, ?, ?, ?)' ); const updateRole = db.prepare( 'UPDATE users SET role = ?, updated_at = ? WHERE username = ?' ); const updatePassword = db.prepare( 'UPDATE users SET password_hash = ?, updated_at = ? WHERE username = ?' ); // 确保超级管理员存在 const adminExists = db.prepare('SELECT id FROM users WHERE username = ?').get(DEFAULT_ADMIN_USER); if (!adminExists) { const hash = await hashPassword(DEFAULT_ADMIN_PASS); insert.run(DEFAULT_ADMIN_USER, hash, 'super_admin', now, now); } else { // 确保超级管理员的角色正确 updateRole.run('super_admin', now, DEFAULT_ADMIN_USER); } // 初始化其他用户 for (const [username, password] of Object.entries(USERS)) { if (username === DEFAULT_ADMIN_USER) continue; const existing = db.prepare('SELECT id, password_hash FROM users WHERE username = ?').get(username); if (!existing) { const hash = await hashPassword(password); insert.run(username, hash, 'admin', now, now); } } } initUsers().then(() => { console.log('用户初始化完成'); }).catch(err => { console.error('用户初始化失败:', err); process.exit(1); }); // 确保目录存在 if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true }); const DB_DIR = path.dirname(DB_PATH); if (!fs.existsSync(DB_DIR)) fs.mkdirSync(DB_DIR, { recursive: true }); // 获取本机局域网 IP function getLocalIP() { const nets = os.networkInterfaces(); for (const name of Object.keys(nets)) { for (const net of nets[name]) { if (net.family === 'IPv4' && !net.internal) return net.address; } } return 'localhost'; } // Session 配置 app.use(session({ secret: process.env.SESSION_SECRET || crypto.randomBytes(32).toString('hex'), resave: false, saveUninitialized: false, name: 'docmgr.sid', cookie: { httpOnly: true, secure: false, // 生产环境配合 nginx 应设为 true sameSite: 'lax', maxAge: 24 * 60 * 60 * 1000 } })); // === 安全响应头 === app.use((req, res, next) => { res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('X-Frame-Options', 'DENY'); res.setHeader('X-XSS-Protection', '1; mode=block'); res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; font-src 'self' https://cdn.jsdelivr.net; img-src 'self' data: blob:;"); res.setHeader('Referrer-Policy', 'no-referrer'); res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()'); res.removeHeader('X-Powered-By'); // 隐藏技术栈 next(); }); // === 请求体解析 === app.use(express.urlencoded({ extended: true })); app.use(express.json()); // 登录锁定记录(内存) const LOGIN_MAX_ATTEMPTS = 3; const LOGIN_LOCK_MINUTES = 10; const loginAttempts = new Map(); function checkLoginLock(username, ip) { const key = `${username}:${ip}`; const record = loginAttempts.get(key); if (!record) return null; const elapsed = Date.now() - record.lockedAt; const remaining = LOGIN_LOCK_MINUTES * 60 * 1000 - elapsed; if (remaining > 0) { return Math.ceil(remaining / 1000 / 60); } loginAttempts.delete(key); return null; } function recordLoginAttempt(username, ip, success) { const key = `${username}:${ip}`; if (success) { loginAttempts.delete(key); return; } const record = loginAttempts.get(key) || { count: 0, lockedAt: 0 }; record.count++; if (record.count >= LOGIN_MAX_ATTEMPTS) { record.lockedAt = Date.now(); } loginAttempts.set(key, record); } // Multer 配置 const storage = multer.diskStorage({ destination: (req, file, cb) => cb(null, UPLOAD_DIR), filename: (req, file, cb) => { const code = nanoid(10); const ext = path.extname(file.originalname).toLowerCase(); cb(null, code + ext); } }); const upload = multer({ storage, limits: { fileSize: 500 * 1024 * 1024 }, fileFilter: (req, file, cb) => { const ext = path.extname(file.originalname).toLowerCase(); if (!UPLOAD_EXTENSIONS.has(ext)) { return cb(new Error(`不支持的文件类型:${ext},允许的类型:${Array.from(UPLOAD_EXTENSIONS).join(', ')}`)); } cb(null, true); } }); // EJS 模板引擎 app.set('view engine', 'ejs'); app.set('views', path.join(__dirname, 'views')); // 模板辅助函数 app.locals.formatSize = (bytes) => { if (!bytes) return '0 B'; const units = ['B', 'KB', 'MB', 'GB']; let i = 0; let size = bytes; while (size >= 1024 && i < units.length - 1) { size /= 1024; i++; } return size.toFixed(i > 0 ? 1 : 0) + ' ' + units[i]; }; app.locals.formatDate = (ts) => { const d = new Date(ts); return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0') + ' ' + String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0'); }; app.locals.getFileIconClass = (name) => { const ext = path.extname(name).toLowerCase(); if (['.zip', '.rar', '.7z', '.tar', '.gz'].includes(ext)) return 'archive'; if (['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'].includes(ext)) return 'image'; if (['.pdf'].includes(ext)) return 'pdf'; if (['.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt'].includes(ext)) return 'doc'; return 'other'; }; app.locals.getFileIcon = (name) => { const ext = path.extname(name).toLowerCase(); if (['.zip', '.rar', '.7z', '.tar', '.gz'].includes(ext)) return 'fa-regular fa-file-zipper'; if (['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'].includes(ext)) return 'fa-regular fa-file-image'; if (['.pdf'].includes(ext)) return 'fa-regular fa-file-pdf'; if (['.doc', '.docx'].includes(ext)) return 'fa-regular fa-file-word'; if (['.xls', '.xlsx'].includes(ext)) return 'fa-regular fa-file-excel'; if (['.ppt', '.pptx'].includes(ext)) return 'fa-regular fa-file-powerpoint'; if (['.txt'].includes(ext)) return 'fa-regular fa-file-lines'; if (['.mp4', '.avi', '.mov', '.mkv'].includes(ext)) return 'fa-regular fa-file-video'; if (['.mp3', '.wav', '.flac'].includes(ext)) return 'fa-regular fa-file-audio'; return 'fa-regular fa-file'; }; // 静态文件(防止目录遍历攻击) app.use('/uploads', (req, res, next) => { // 只允许访问文件,禁止目录浏览 if (req.path.includes('..')) { return res.status(403).send('禁止访问'); } next(); }, express.static(UPLOAD_DIR, { dotfiles: 'ignore', index: false // 禁止列出目录 })); app.use('/static', express.static(path.join(__dirname, 'public'))); // ========== 登录中间件 ========== function requireAuth(req, res, next) { if (req.session && req.session.user) return next(); if (req.xhr || req.headers.accept?.includes('json')) { return res.status(401).json({ error: '未登录', loginUrl: '/login' }); } res.redirect('/login'); } // 超级管理员权限中间件(只有 gwalrusadmin 可访问) function requireSuperAdmin(req, res, next) { if (!req.session || !req.session.user) { return res.status(401).json({ error: '未登录' }); } if (req.session.user.username !== 'gwalrusadmin') { return res.status(403).json({ error: '权限不足:仅超级管理员可访问' }); } next(); } // === 速率限制配置 === const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 分钟 max: 10, // 最多 10 次请求 message: { error: '操作太频繁,请稍后再试' }, standardHeaders: true, legacyHeaders: false }); const apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 100, // API 最多 100 次请求 message: { error: '操作太频繁,请稍后再试' }, standardHeaders: true, legacyHeaders: false }); // ========== 生成验证码 ========== app.get('/captcha', (req, res) => { const captcha = svgCaptcha.create({ size: 4, noise: 2, color: true, background: '#f0f4ff', width: 120, height: 40 }); req.session.captcha = captcha.text.toLowerCase(); res.type('svg'); res.send(captcha.data); }); // ========== 登录 ========== app.get('/login', (req, res) => { if (req.session && req.session.user) return res.redirect('/'); const clientIp = req.ip || req.connection.remoteAddress || ''; const lockRemaining = checkLoginLock(req.query.u || '', clientIp); res.render('login', { error: req.query.error || null, username: req.query.u || '', password: req.query.p || '', lockedUsername: req.query.u || '', lockRemaining, baseUrl: BASE_URL }); }); app.post('/login', loginLimiter, async (req, res) => { const { username, password, captcha } = req.body; const clientIp = req.ip || req.connection.remoteAddress || ''; // 检查是否被锁定 const lockRemaining = checkLoginLock(username, clientIp); if (lockRemaining !== null) { return res.redirect('/login?error=' + encodeURIComponent('账户已锁定,请稍后再试') + '&u=' + encodeURIComponent(username)); } if (!username || !password) { return res.redirect('/login?error=' + encodeURIComponent('请输入用户名和密码') + '&u=' + encodeURIComponent(username || '')); } // 验证验证码 const sessionCaptcha = req.session.captcha || ''; delete req.session.captcha; if (!captcha || captcha.toLowerCase() !== sessionCaptcha) { return res.redirect('/login?error=' + encodeURIComponent('验证码错误') + '&u=' + encodeURIComponent(username) + '&p=' + encodeURIComponent(password || '')); } const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username); if (!user || !verifyPassword(password, user.password_hash)) { recordLoginAttempt(username, clientIp, false); return res.redirect('/login?error=' + encodeURIComponent('用户名或密码错误') + '&u=' + encodeURIComponent(username) + '&p=' + encodeURIComponent(password || '')); } // 登录成功,重新生成 Session ID(防止 Session 固定攻击) recordLoginAttempt(username, clientIp, true); return new Promise((resolve) => { req.session.regenerate((err) => { if (err) { console.error('Session 重新生成失败:', err); return res.redirect('/login?error=' + encodeURIComponent('登录失败,请重试')); } req.session.user = { username: user.username, role: user.role }; res.redirect(req.session.returnTo || '/'); resolve(); }); }); }); app.get('/logout', (req, res) => { req.session.destroy(() => res.redirect('/login')); }); // ========== 管理后台首页 ========== app.get('/', requireAuth, (req, res) => { const files = db.prepare('SELECT * FROM files ORDER BY uploaded_at DESC').all(); const list = files.map(f => ({ code: f.code, originalName: f.original_name, savedName: f.saved_name, size: f.size, mimeType: f.mime_type, uploadedAt: f.uploaded_at, downloads: f.downloads, uploadedBy: f.uploaded_by, downloadUrl: `${BASE_URL}/d/${f.code}`, qrUrl: `${BASE_URL}/qr/${f.code}` })); res.render('index', { files: list, baseUrl: BASE_URL, user: req.session.user }); }); // ========== 账号管理页面 ========== app.get('/users', requireAuth, (req, res) => { if (req.session.user.username !== 'gwalrusadmin') { return res.status(403).send('权限不足:仅超级管理员可访问'); } res.render('users', { user: req.session.user, baseUrl: BASE_URL }); }); // ========== 上传文件 ========== app.post('/api/upload', requireAuth, upload.single('file'), (req, res) => { if (!req.file) return res.status(400).json({ error: '请选择文件' }); const code = path.parse(req.file.filename).name; const now = Date.now(); // 修复 multer 接收的中文文件名乱码 // 浏览器 multipart/form-data 中文件名是 UTF-8 编码 // 但有时被错误地按 latin1 解析,导致中文显示为 中 这类乱码 let originalName = req.file.originalname; const latin1Buf = Buffer.from(originalName, 'latin1'); const utf8Candidate = Iconv.decode(latin1Buf, 'utf-8'); // 如果 latin1→utf8 解码后有明显的 CJK 字符(一-鿿),就用它 if (/[\u4e00-\u9fff]/.test(utf8Candidate)) { originalName = utf8Candidate; } db.prepare(` INSERT INTO files (code, original_name, saved_name, size, mime_type, uploaded_at, downloads, uploaded_by) VALUES (?, ?, ?, ?, ?, ?, 0, ?) `).run(code, originalName, req.file.filename, req.file.size, req.file.mimetype, now, req.session.user.username); req.file.originalname = originalName; res.json({ code, downloadUrl: `${BASE_URL}/d/${code}`, qrUrl: `${BASE_URL}/qr/${code}`, originalName: req.file.originalname }); }); // ========== 删除文件 ========== app.post('/api/delete/:code', requireAuth, (req, res) => { const file = db.prepare('SELECT * FROM files WHERE code = ?').get(req.params.code); if (!file) return res.status(404).json({ error: '文件不存在' }); const filePath = path.join(UPLOAD_DIR, file.saved_name); if (fs.existsSync(filePath)) fs.unlinkSync(filePath); db.prepare('DELETE FROM files WHERE code = ?').run(req.params.code); res.json({ success: true }); }); // ========== 获取文件列表(API) ========== app.get('/api/files', requireAuth, (req, res) => { const files = db.prepare('SELECT * FROM files ORDER BY uploaded_at DESC').all(); res.json(files.map(f => ({ code: f.code, originalName: f.original_name, savedName: f.saved_name, size: f.size, mimeType: f.mime_type, uploadedAt: f.uploaded_at, downloads: f.downloads, uploadedBy: f.uploaded_by, downloadUrl: `${BASE_URL}/d/${f.code}`, qrUrl: `${BASE_URL}/qr/${f.code}` }))); }); // ========== 二维码数据 API ========== app.get('/api/qr/:code', requireAuth, async (req, res) => { const file = db.prepare('SELECT * FROM files WHERE code = ?').get(req.params.code); if (!file) return res.status(404).json({ error: '文件不存在' }); const downloadUrl = `${BASE_URL}/d/${req.params.code}`; try { const qrDataUrl = await QRCode.toDataURL(downloadUrl, { width: 400, margin: 2 }); res.json({ downloadUrl, qrDataUrl }); } catch (err) { res.status(500).json({ error: '生成二维码失败' }); } }); // ========== 修改密码 ========== app.get('/settings', requireAuth, (req, res) => { res.render('settings', { success: req.query.success || null, error: req.query.error || null, user: req.session.user, baseUrl: BASE_URL }); }); app.post('/api/change-password', requireAuth, async (req, res) => { const { oldPassword, newPassword, confirmPassword } = req.body; const username = req.session.user.username; if (!oldPassword || !newPassword || !confirmPassword) { return res.redirect('/settings?error=' + encodeURIComponent('请填写所有字段')); } if (newPassword.length < 6) { return res.redirect('/settings?error=' + encodeURIComponent('新密码至少 6 位')); } if (newPassword !== confirmPassword) { return res.redirect('/settings?error=' + encodeURIComponent('两次密码输入不一致')); } const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username); if (!user || !verifyPassword(oldPassword, user.password_hash)) { return res.redirect('/settings?error=' + encodeURIComponent('原密码错误')); } const newHash = await hashPassword(newPassword); db.prepare('UPDATE users SET password_hash = ?, updated_at = ? WHERE username = ?') .run(newHash, Date.now(), username); res.redirect('/settings?success=' + encodeURIComponent('密码修改成功')); }); // ========== 检查登录状态 ========== app.get('/api/me', (req, res) => { if (req.session && req.session.user) { return res.json({ loggedIn: true, user: req.session.user }); } res.json({ loggedIn: false }); }); // ========== 账号管理 API(仅 gwalrusadmin) ========== // 获取用户列表 app.get('/api/users', requireAuth, requireSuperAdmin, apiLimiter, (req, res) => { const users = db.prepare('SELECT id, username, role, created_at, updated_at FROM users ORDER BY created_at DESC').all(); res.json({ success: true, users }); }); // 创建用户 app.post('/api/users', requireAuth, requireSuperAdmin, apiLimiter, async (req, res) => { const { username, password, role } = req.body; if (!username || !password) { return res.status(400).json({ error: '用户名和密码不能为空' }); } if (username.length < 3 || username.length > 30) { return res.status(400).json({ error: '用户名长度必须在 3-30 个字符之间' }); } if (!/^[a-zA-Z0-9_]+$/.test(username)) { return res.status(400).json({ error: '用户名只能包含字母、数字和下划线' }); } if (password.length < 6) { return res.status(400).json({ error: '密码长度至少 6 位' }); } if (!['admin', 'viewer'].includes(role)) { return res.status(400).json({ error: '角色必须是 admin 或 viewer' }); } const existing = db.prepare('SELECT id FROM users WHERE username = ?').get(username); if (existing) { return res.status(409).json({ error: '用户名已存在' }); } const hash = await hashPassword(password); const now = Date.now(); db.prepare('INSERT INTO users (username, password_hash, role, created_at, updated_at) VALUES (?, ?, ?, ?, ?)') .run(username, hash, role, now, now); res.json({ success: true, message: '用户创建成功' }); }); // 更新用户 app.put('/api/users/:id', requireAuth, requireSuperAdmin, apiLimiter, async (req, res) => { const userId = parseInt(req.params.id); const { password, role } = req.body; const user = db.prepare('SELECT id, username, role FROM users WHERE id = ?').get(userId); if (!user) { return res.status(404).json({ error: '用户不存在' }); } // 不允许修改超级管理员 if (user.username === 'gwalrusadmin') { return res.status(403).json({ error: '不能修改超级管理员账号' }); } const updates = []; const params = []; if (password) { if (password.length < 6) { return res.status(400).json({ error: '密码长度至少 6 位' }); } const hash = await hashPassword(password); updates.push('password_hash = ?'); params.push(hash); } if (role) { if (!['admin', 'viewer'].includes(role)) { return res.status(400).json({ error: '角色必须是 admin 或 viewer' }); } updates.push('role = ?'); params.push(role); } if (updates.length === 0) { return res.status(400).json({ error: '没有要更新的字段' }); } updates.push('updated_at = ?'); params.push(Date.now()); params.push(userId); db.prepare(`UPDATE users SET ${updates.join(', ')} WHERE id = ?`).run(...params); res.json({ success: true, message: '用户更新成功' }); }); // 删除用户 app.delete('/api/users/:id', requireAuth, requireSuperAdmin, apiLimiter, (req, res) => { const userId = parseInt(req.params.id); const user = db.prepare('SELECT id, username FROM users WHERE id = ?').get(userId); if (!user) { return res.status(404).json({ error: '用户不存在' }); } // 不允许删除超级管理员 if (user.username === 'gwalrusadmin') { return res.status(403).json({ error: '不能删除超级管理员账号' }); } // 不允许删除自己 if (user.username === req.session.user.username) { return res.status(403).json({ error: '不能删除当前登录的账号' }); } db.prepare('DELETE FROM users WHERE id = ?').run(userId); res.json({ success: true, message: '用户删除成功' }); }); // ========== 公开下载 / 二维码 ========== app.get('/d/:code', (req, res) => { const file = db.prepare('SELECT * FROM files WHERE code = ?').get(req.params.code); if (!file) return res.status(404).send('文件不存在或已过期'); const filePath = path.join(UPLOAD_DIR, file.saved_name); if (!fs.existsSync(filePath)) { db.prepare('DELETE FROM files WHERE code = ?').run(req.params.code); return res.status(404).send('文件已被删除'); } db.prepare('UPDATE files SET downloads = downloads + 1 WHERE code = ?').run(req.params.code); // 中文文件名编码处理 const encodedName = encodeURIComponent(file.original_name); res.setHeader('Content-Disposition', `attachment; filename="${encodedName}"; filename*=UTF-8''${encodedName}`); // 设置正确的 Content-Type 和 Content-Length const stat = fs.statSync(filePath); res.setHeader('Content-Length', stat.size); res.setHeader('Content-Type', file.mime_type || 'application/octet-stream'); // 使用流式传输,添加完整的错误处理 const fileStream = fs.createReadStream(filePath); fileStream.on('error', (err) => { console.error(`下载文件时出错: ${err.message}`); if (!res.headersSent) { res.status(500).send('文件读取失败'); } }); fileStream.on('open', () => { fileStream.pipe(res); }); }); app.get('/qr/:code', async (req, res) => { const file = db.prepare('SELECT * FROM files WHERE code = ?').get(req.params.code); if (!file) return res.status(404).send('文件不存在'); const downloadUrl = `${BASE_URL}/d/${req.params.code}`; try { const qrDataUrl = await QRCode.toDataURL(downloadUrl, { width: 400, margin: 2 }); const base64 = qrDataUrl.replace(/^data:image\/png;base64,/, ''); const img = Buffer.from(base64, 'base64'); res.writeHead(200, { 'Content-Type': 'image/png', 'Content-Length': img.length }); res.end(img); } catch (err) { res.status(500).send('生成二维码失败'); } }); // ========== 启动 ========== app.listen(PORT, '0.0.0.0', () => { console.log(`📄 文档管理系统启动成功!`); console.log(`🌐 管理后台: ${BASE_URL}`); console.log(`📂 上传目录: ${UPLOAD_DIR}`); console.log(`🗄️ 数据库: ${DB_PATH} (SQLite)`); console.log(`👤 默认超级管理员: gwalrusadmin / ${process.env.ADMIN_PASSWORD || 'admin123'}`); console.log(`🔒 安全加固:bcrypt 密码加密 + 速率限制 + 文件类型白名单`); });