const express = require('express'); const cors = require('cors'); const path = require('path'); const multer = require('multer'); const jwt = require('jsonwebtoken'); const bcrypt = require('bcryptjs'); const db = require('./database'); const { extractFirstTwoPages } = require('./utils/pdfProcessor'); const fs = require('fs'); const app = express(); const PORT = 3000; const JWT_SECRET = 'super-secret-key-pdf-preview'; app.use(cors()); app.use(express.json()); app.use('/uploads/processed', express.static(path.join(__dirname, 'uploads/processed'))); // Serve the built frontend from the public folder app.use(express.static(path.join(__dirname, 'public'))); // Ensure upload directories exist const uploadDirOrig = path.join(__dirname, 'uploads/original'); const uploadDirProc = path.join(__dirname, 'uploads/processed'); if (!fs.existsSync(uploadDirOrig)) fs.mkdirSync(uploadDirOrig, { recursive: true }); if (!fs.existsSync(uploadDirProc)) fs.mkdirSync(uploadDirProc, { recursive: true }); const storage = multer.diskStorage({ destination: function (req, file, cb) { // 使用绝对路径,避免服务器 CWD 不在项目目录时写入失败 cb(null, path.join(__dirname, 'uploads/original/')); }, filename: function (req, file, cb) { const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); cb(null, uniqueSuffix + '.pdf'); } }); const upload = multer({ storage: storage }); // Middleware for auth const authenticateJWT = (req, res, next) => { const authHeader = req.headers.authorization; if (authHeader) { const token = authHeader.split(' ')[1]; jwt.verify(token, JWT_SECRET, (err, user) => { if (err) return res.sendStatus(403); req.user = user; next(); }); } else { res.sendStatus(401); } }; // Admin Login Route app.post('/api/admin/login', (req, res) => { const { username, password } = req.body; const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username); if (user && bcrypt.compareSync(password, user.password)) { const token = jwt.sign({ username: user.username }, JWT_SECRET, { expiresIn: '24h' }); res.json({ token }); } else { res.status(401).json({ message: 'Username or password incorrect' }); } }); // Upload PDF Route app.post('/api/admin/upload', authenticateJWT, upload.single('document'), async (req, res) => { if (!req.file) return res.status(400).json({ message: 'No file uploaded' }); const originalPath = req.file.path; const processedFilename = 'processed-' + req.file.filename; const processedPathRelative = 'uploads/processed/' + processedFilename; const processedPath = path.join(__dirname, processedPathRelative); const title = req.body.title || req.file.originalname; try { await extractFirstTwoPages(originalPath, processedPath); const stmt = db.prepare('INSERT INTO documents (title, original_path, processed_path) VALUES (?, ?, ?)'); const info = stmt.run(title, originalPath, processedPathRelative); res.json({ message: 'Upload successful', documentId: info.lastInsertRowid }); } catch (error) { // 输出完整堆栈到服务器日志,便于排查 console.error('Upload error details:', error.stack || error); res.status(500).json({ message: 'Error processing PDF', error: error.message || error.toString(), stack: process.env.NODE_ENV !== 'production' ? error.stack : undefined }); } }); // Delete PDF Route app.delete('/api/admin/documents/:id', authenticateJWT, (req, res) => { const doc = db.prepare('SELECT * FROM documents WHERE id = ?').get(req.params.id); if (!doc) return res.status(404).json({ message: 'Not found' }); try { if (fs.existsSync(path.join(__dirname, doc.original_path))) fs.unlinkSync(path.join(__dirname, doc.original_path)); if (fs.existsSync(path.join(__dirname, doc.processed_path))) fs.unlinkSync(path.join(__dirname, doc.processed_path)); } catch(e) { } db.prepare('DELETE FROM documents WHERE id = ?').run(req.params.id); res.json({ message: 'Deleted successfully' }); }); // Public List Documents Route app.get('/api/documents', (req, res) => { const docs = db.prepare('SELECT id, title, processed_path, created_at FROM documents ORDER BY created_at DESC').all(); // Map processed_path to a URL const responseData = docs.map(doc => ({ id: doc.id, title: doc.title, created_at: doc.created_at, url: `/${doc.processed_path}` })); res.json(responseData); }); // Public Get Single Document app.get('/api/documents/:id', (req, res) => { const doc = db.prepare('SELECT id, title, processed_path, created_at FROM documents WHERE id = ?').get(req.params.id); if (!doc) return res.status(404).json({ message: 'Document not found' }); res.json({ id: doc.id, title: doc.title, created_at: doc.created_at, url: `/${doc.processed_path}` }); }); app.listen(PORT, () => { console.log(`Backend server running on http://localhost:${PORT}`); }); // SPA fallback: must be after all API routes so Vue Router can handle client-side routing // Note: app.use() (not app.get('*')) is required for Express 5 compatibility app.use((req, res) => { const indexPath = path.join(__dirname, 'public', 'index.html'); if (fs.existsSync(indexPath)) { res.sendFile(indexPath); } else { res.status(404).send('Frontend not built yet. Run: cd frontend && npm run build'); } });