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:
co-authored by
Claude Opus 4.6
parent
7b1073e23b
commit
335f0fb8dc
+21
-13
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user