perf: add rate limiting, validation, logging, and error handling

- Add express-rate-limit with tiered rate limits per endpoint
- Add express-validator for input validation on all API routes
- Add winston + morgan for structured logging
- Add node-cache for lottery result caching (5min TTL, 1000 max keys)
- Convert OCR to async task queue with max 2 concurrent jobs
- Add global error handler middleware
- Add React ErrorBoundary for client-side error handling
- Fix memory leaks in OCR task queue and lottery cache
- Add per-item error handling in batch ticket updates
- Fix SQL UPDATE to include user_id in WHERE clause

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Developer
2026-03-19 11:35:12 +08:00
co-authored by Claude Opus 4.6
parent 7b1073e23b
commit 335f0fb8dc
17 changed files with 927 additions and 148 deletions
+21 -13
View File
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import Login from './pages/Login';
import Dashboard from './pages/Dashboard';
import ErrorBoundary from './components/ErrorBoundary';
function App() {
const [user, setUser] = useState(null);
@@ -11,7 +12,12 @@ function App() {
const savedUser = localStorage.getItem('user');
const token = localStorage.getItem('token');
if (savedUser && token) {
setUser(JSON.parse(savedUser));
try {
setUser(JSON.parse(savedUser));
} catch (e) {
localStorage.removeItem('user');
localStorage.removeItem('token');
}
}
setLoading(false);
}, []);
@@ -33,18 +39,20 @@ function App() {
}
return (
<BrowserRouter>
<Routes>
<Route
path="/"
element={user ? <Navigate to="/dashboard" /> : <Login onLogin={handleLogin} />}
/>
<Route
path="/dashboard"
element={user ? <Dashboard user={user} onLogout={handleLogout} /> : <Navigate to="/" />}
/>
</Routes>
</BrowserRouter>
<ErrorBoundary>
<BrowserRouter>
<Routes>
<Route
path="/"
element={user ? <Navigate to="/dashboard" /> : <Login onLogin={handleLogin} />}
/>
<Route
path="/dashboard"
element={user ? <Dashboard user={user} onLogout={handleLogout} /> : <Navigate to="/" />}
/>
</Routes>
</BrowserRouter>
</ErrorBoundary>
);
}
+61
View File
@@ -0,0 +1,61 @@
import { Component } from 'react';
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('React ErrorBoundary caught error:', error, errorInfo);
}
handleRetry = () => {
this.setState({ hasError: false, error: null });
window.location.reload();
};
render() {
if (this.state.hasError) {
return (
<div style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '100vh',
padding: '20px',
textAlign: 'center',
backgroundColor: '#f5f5f5'
}}>
<h2 style={{ color: '#e74c3c', marginBottom: '10px' }}>出错了</h2>
<p style={{ color: '#666', marginBottom: '20px' }}>
{this.state.error?.message || '发生了未知错误'}
</p>
<button
onClick={this.handleRetry}
style={{
padding: '10px 20px',
backgroundColor: '#3498db',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
fontSize: '16px'
}}
>
刷新页面
</button>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
+30 -7
View File
@@ -2,6 +2,7 @@ import axios from 'axios';
const api = axios.create({
baseURL: '/api',
timeout: 30000,
});
// 请求拦截器 - 添加token
@@ -13,16 +14,37 @@ api.interceptors.request.use(config => {
return config;
});
// 响应拦截器 - 处理401
// 响应拦截器 - 处理错误
api.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.href = '/';
if (error.response) {
const { status, data } = error.response;
switch (status) {
case 401:
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.href = '/';
break;
case 429:
throw new Error(data.error || '请求过于频繁,请稍后再试');
case 400:
if (data.details) {
const firstError = data.details[0];
throw new Error(`${firstError.field}: ${firstError.message}`);
}
throw new Error(data.error || '请求参数错误');
case 500:
throw new Error(data.error || '服务器内部错误');
default:
throw new Error(data.error || '请求失败');
}
} else if (error.request) {
throw new Error('网络连接失败,请检查网络');
} else {
throw new Error('请求配置错误');
}
return Promise.reject(error);
}
);
@@ -56,7 +78,8 @@ export const ocrAPI = {
return api.post('/ocr/parse', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
}
},
getResult: (taskId) => api.get(`/ocr/result/${taskId}`),
};
export default api;