fix: evaluation report P0/P1/P2 fixes, remove Docker, add upload UI

Backend:
- Add NotFoundException + BusinessException, return correct HTTP status (404/400)
- Add @Index on reports.project_id and reports.upload_time
- Add fileSize column to reports, populate on upload, return in DTO
- Cascade delete: deleting project now removes all reports (DB + files + PDFs)
- Delete report: also clean up pre-rendered PDF
- File upload MIME validation (extension + Content-Type)
- Remove duplicate @ExceptionHandler from ReportController
- Switch from System.err to SLF4J logger
- Handle MethodArgumentNotValid, MissingServletRequestPart, etc.

Frontend:
- Remove all Docker files (project uses 宝塔 panel deployment)
- Upgrade axios 1.6.8 -> 1.7.7 (CVE-2024-39338)
- Remove unused @vue-office/pptx + vue-demi (see CHANGELOG for rationale)
- Fix vite proxy port 37821 -> 30081
- Remove mock data fallback in production
- Add upload report UI (button + modal in ProjectDetail)
- Add create project UI (button + modal in ProjectList)
- Add filename search box in ProjectDetail
- New useApi methods: createProject, uploadReport, deleteProject, deleteReport
- FilePreview/ReportCard: show fileSize (was undefined before)

Docs:
- Add README.md (overview, quick start, structure)
- Add CHANGELOG.md (full change log + pptx removal rationale)
- Include EVALUATION_REPORT.md and blog-vibe-coding.md

Tests:
- All 73 backend tests pass
- All 43 frontend tests pass
- Updated test fixtures for new API contract
This commit is contained in:
2026-06-01 21:35:13 +08:00
parent 7000c186e2
commit afcd18c54f
77 changed files with 1498 additions and 2886 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-30
View File
@@ -1,30 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>日报分发系统</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
content: ['./index.html', './src/**/*.{vue,js}'],
theme: {
extend: {
colors: {
primary: '#3b82f6',
secondary: '#64748b'
}
}
}
}
</script>
<style>
[v-cloak] { display: none; }
</style>
<script type="module" crossorigin src="/assets/index-B2kIc5mE.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Cu9PSEpL.css">
</head>
<body class="bg-gray-50">
<div id="app" v-cloak></div>
</body>
</html>
-117
View File
@@ -1,117 +0,0 @@
// Frontend: Node.js static file server with API proxy
const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');
const PORT = 80;
const BACKEND = process.env.BACKEND_URL || 'http://publish-backend:8080';
const STATIC_DIR = '/app/dist';
const UPLOADS_DIR = '/app/uploads';
const MIME_TYPES = {
'.html': 'text/html',
'.js': 'application/javascript',
'.css': 'text/css',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.eot': 'application/vnd.ms-fontobject',
'.pdf': 'application/pdf',
'.md': 'text/markdown',
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'.ppt': 'application/vnd.ms-powerpoint',
};
function serveStatic(req, res) {
let filePath = path.join(STATIC_DIR, req.url === '/' ? '/index.html' : req.url);
// Remove query string
filePath = filePath.split('?')[0];
const ext = path.extname(filePath).toLowerCase();
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
fs.readFile(filePath, (err, data) => {
if (err) {
// SPA fallback: serve index.html
if (req.url.startsWith('/api') || req.url.startsWith('/uploads')) {
proxyRequest(req, res);
} else {
fs.readFile(path.join(STATIC_DIR, 'index.html'), (err2, data2) => {
if (err2) {
res.writeHead(404);
res.end('Not Found');
} else {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(data2);
}
});
}
} else {
res.writeHead(200, { 'Content-Type': contentType });
res.end(data);
}
});
}
function proxyRequest(req, res) {
const targetUrl = BACKEND + req.url;
const parsedUrl = url.parse(req.url);
const options = {
hostname: url.parse(BACKEND).hostname,
port: url.parse(BACKEND).port || 80,
path: req.url,
method: req.method,
headers: {}
};
// Forward relevant headers
['content-type', 'authorization', 'accept', 'x-requested-with', 'host'].forEach(h => {
if (req.headers[h]) options.headers[h] = req.headers[h];
});
const proxyReq = http.request(options, (proxyRes) => {
res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res);
});
proxyReq.on('error', (e) => {
res.writeHead(502);
res.end('Backend error: ' + e.message);
});
if (['POST', 'PUT', 'DELETE'].includes(req.method)) {
req.pipe(proxyReq);
} else {
proxyReq.end();
}
}
const server = http.createServer((req, res) => {
// CORS headers
res.setHeader('Access-Control-Allow-Origin', '*');
if (req.method === 'OPTIONS') {
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.writeHead(204);
res.end();
return;
}
if (req.url.startsWith('/api') || req.url.startsWith('/uploads')) {
proxyRequest(req, res);
} else {
serveStatic(req, res);
}
});
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
console.log(`Backend: ${BACKEND}`);
});