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
@@ -13,3 +13,9 @@ node_modules/
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
|
||||
# Application logs
|
||||
logs/
|
||||
|
||||
@@ -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) {
|
||||
try {
|
||||
setUser(JSON.parse(savedUser));
|
||||
} catch (e) {
|
||||
localStorage.removeItem('user');
|
||||
localStorage.removeItem('token');
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
}, []);
|
||||
@@ -33,6 +39,7 @@ function App() {
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route
|
||||
@@ -45,6 +52,7 @@ function App() {
|
||||
/>
|
||||
</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) {
|
||||
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;
|
||||
|
||||
+26
-2
@@ -1,6 +1,14 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
const morgan = require('morgan');
|
||||
const fs = require('fs');
|
||||
|
||||
// 初始化日志目录
|
||||
const logsDir = path.join(__dirname, 'logs');
|
||||
if (!fs.existsSync(logsDir)) {
|
||||
fs.mkdirSync(logsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 初始化数据库
|
||||
require('./db/init');
|
||||
@@ -9,15 +17,28 @@ const authRoutes = require('./routes/auth');
|
||||
const ticketRoutes = require('./routes/tickets');
|
||||
const lotteryRoutes = require('./routes/lottery');
|
||||
const ocrRoutes = require('./routes/ocr');
|
||||
const { standardLimiter } = require('./middleware/rateLimiter');
|
||||
const errorHandler = require('./middleware/errorHandler');
|
||||
const logger = require('./services/logger');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 4444;
|
||||
|
||||
// 创建 morgan stream 用于日志
|
||||
const morganStream = {
|
||||
write: (message) => logger.http(message.trim())
|
||||
};
|
||||
|
||||
// 中间件
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
app.use(morgan('combined', { stream: morganStream }));
|
||||
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
|
||||
|
||||
// 全局限流 (应用于所有API)
|
||||
app.use('/api', standardLimiter);
|
||||
|
||||
// API 路由
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/tickets', ticketRoutes);
|
||||
@@ -38,6 +59,9 @@ app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(clientDist, 'index.html'));
|
||||
});
|
||||
|
||||
// 全局错误处理 (必须放在最后)
|
||||
app.use(errorHandler);
|
||||
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`服务器运行在 http://0.0.0.0:${PORT}`);
|
||||
logger.info(`服务器运行在 http://0.0.0.0:${PORT}`);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
const logger = require('../services/logger');
|
||||
|
||||
function errorHandler(err, req, res, next) {
|
||||
logger.error('Unhandled error', {
|
||||
error: err.message,
|
||||
stack: err.stack,
|
||||
path: req.path,
|
||||
method: req.method,
|
||||
ip: req.ip
|
||||
});
|
||||
|
||||
if (err.type === 'entity.parse.failed') {
|
||||
return res.status(400).json({ error: '无效的JSON格式' });
|
||||
}
|
||||
|
||||
// express-validator 验证失败时直接返回400,不会抛到这里
|
||||
// 此检查保留作为其他验证库的安全网
|
||||
|
||||
res.status(err.status || 500).json({
|
||||
error: process.env.NODE_ENV === 'production' ? '服务器内部错误' : err.message
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = errorHandler;
|
||||
@@ -0,0 +1,39 @@
|
||||
const rateLimit = require('express-rate-limit');
|
||||
|
||||
// 通用限流器 - 100请求/分钟
|
||||
const standardLimiter = rateLimit({
|
||||
windowMs: 60 * 1000,
|
||||
max: 100,
|
||||
message: { error: '请求过于频繁,请稍后再试' },
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
|
||||
// 认证限流器 - 10请求/分钟 (防止暴力攻击)
|
||||
const authLimiter = rateLimit({
|
||||
windowMs: 60 * 1000,
|
||||
max: 10,
|
||||
message: { error: '登录尝试过于频繁,请稍后再试' },
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
|
||||
// OCR限流器 - 5请求/分钟 (OCR是重操作)
|
||||
const ocrLimiter = rateLimit({
|
||||
windowMs: 60 * 1000,
|
||||
max: 5,
|
||||
message: { error: 'OCR处理请求过于频繁,请稍后再试' },
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
|
||||
// 彩票刷新限流器 - 10请求/分钟
|
||||
const refreshLimiter = rateLimit({
|
||||
windowMs: 60 * 1000,
|
||||
max: 10,
|
||||
message: { error: '刷新请求过于频繁,请稍后再试' },
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
|
||||
module.exports = { standardLimiter, authLimiter, ocrLimiter, refreshLimiter };
|
||||
@@ -0,0 +1,64 @@
|
||||
const { body, query, param, validationResult } = require('express-validator');
|
||||
|
||||
// 验证结果处理
|
||||
function validate(req, res, next) {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
error: '输入验证失败',
|
||||
details: errors.array().map(e => ({ field: e.path, message: e.msg }))
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
// 注册验证
|
||||
const registerRules = [
|
||||
body('username')
|
||||
.trim()
|
||||
.isLength({ min: 3, max: 20 }).withMessage('用户名3-20个字符')
|
||||
.matches(/^[a-zA-Z0-9_]+$/).withMessage('用户名只能包含字母、数字、下划线'),
|
||||
body('password')
|
||||
.isLength({ min: 6 }).withMessage('密码至少6个字符'),
|
||||
];
|
||||
|
||||
// 登录验证
|
||||
const loginRules = [
|
||||
body('username').trim().notEmpty().withMessage('用户名不能为空'),
|
||||
body('password').notEmpty().withMessage('密码不能为空'),
|
||||
];
|
||||
|
||||
// 彩票创建验证
|
||||
const ticketCreateRules = [
|
||||
body('type').isIn(['welfare', 'sports']).withMessage('无效的彩票类型'),
|
||||
body('game').isString().notEmpty().withMessage('游戏类型不能为空'),
|
||||
body('cost').isFloat({ min: 0 }).withMessage('金额必须为正数'),
|
||||
body('issue').optional().matches(/^\d{5,}$/).withMessage('期号格式不正确'),
|
||||
body('numbers').optional().isArray().withMessage('号码必须是数组'),
|
||||
body('bet_count').optional().isInt({ min: 1 }).withMessage('注数必须大于0'),
|
||||
body('multiple').optional().isInt({ min: 1, max: 99 }).withMessage('倍数必须在1-99之间'),
|
||||
];
|
||||
|
||||
// 彩票更新验证
|
||||
const ticketUpdateRules = [
|
||||
body('type').optional().isIn(['welfare', 'sports']).withMessage('无效的彩票类型'),
|
||||
body('game').optional().isString().notEmpty().withMessage('游戏类型不能为空'),
|
||||
body('cost').optional().isFloat({ min: 0 }).withMessage('金额必须为正数'),
|
||||
body('issue').optional().matches(/^\d{5,}$/).withMessage('期号格式不正确'),
|
||||
body('bet_count').optional().isInt({ min: 1 }).withMessage('注数必须大于0'),
|
||||
body('multiple').optional().isInt({ min: 1, max: 99 }).withMessage('倍数必须在1-99之间'),
|
||||
body('status').optional().isIn(['pending', 'won', 'lost']).withMessage('状态无效'),
|
||||
];
|
||||
|
||||
// 开奖查询验证
|
||||
const lotteryQueryRules = [
|
||||
query('game').isString().notEmpty().withMessage('彩票类型不能为空'),
|
||||
query('issue').matches(/^\d{5,}$/).withMessage('期号格式不正确'),
|
||||
];
|
||||
|
||||
// ID参数验证
|
||||
const idParamRule = [
|
||||
param('id').isInt({ min: 1 }).withMessage('无效的ID'),
|
||||
];
|
||||
|
||||
module.exports = { validate, registerRules, loginRules, ticketCreateRules, ticketUpdateRules, lotteryQueryRules, idParamRule };
|
||||
Generated
+378
-1
@@ -14,11 +14,52 @@
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"express-validator": "^7.0.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"tesseract.js": "^5.0.4"
|
||||
"node-cache": "^5.1.2",
|
||||
"tesseract.js": "^5.0.4",
|
||||
"winston": "^3.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@colors/colors": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
|
||||
"integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.1.90"
|
||||
}
|
||||
},
|
||||
"node_modules/@dabh/diagnostics": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz",
|
||||
"integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@so-ric/colorspace": "^1.1.6",
|
||||
"enabled": "2.0.x",
|
||||
"kuler": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@so-ric/colorspace": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz",
|
||||
"integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color": "^5.0.2",
|
||||
"text-hex": "1.0.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/triple-beam": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
|
||||
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
@@ -44,6 +85,12 @@
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
@@ -81,6 +128,24 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/basic-auth": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
|
||||
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "5.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/basic-auth/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bcryptjs": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
|
||||
@@ -313,6 +378,61 @@
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/clone": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
||||
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/color": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz",
|
||||
"integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^3.1.3",
|
||||
"color-string": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz",
|
||||
"integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.6"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz",
|
||||
"integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
}
|
||||
},
|
||||
"node_modules/color-string": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz",
|
||||
"integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
@@ -581,6 +701,12 @@
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/enabled": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
|
||||
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
@@ -739,6 +865,40 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "7.5.1",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz",
|
||||
"integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/express-rate-limit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": ">= 4.11"
|
||||
}
|
||||
},
|
||||
"node_modules/express-validator": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.1.tgz",
|
||||
"integrity": "sha512-IGenaSf+DnWc69lKuqlRE9/i/2t5/16VpH5bXoqdxWz1aCpRvEdrBuu1y95i/iL5QP8ZYVATiwLFhwk3EDl5vg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21",
|
||||
"validator": "~13.15.23"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fecha": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
|
||||
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
@@ -763,6 +923,12 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/fn.name": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
|
||||
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
@@ -1042,6 +1208,18 @@
|
||||
"integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-stream": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-url": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
|
||||
@@ -1103,6 +1281,18 @@
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/kuler": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
|
||||
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
@@ -1145,6 +1335,29 @@
|
||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/logform": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz",
|
||||
"integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@colors/colors": "1.6.0",
|
||||
"@types/triple-beam": "^1.3.2",
|
||||
"fecha": "^4.2.0",
|
||||
"ms": "^2.1.1",
|
||||
"safe-stable-stringify": "^2.3.1",
|
||||
"triple-beam": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/logform/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -1253,6 +1466,34 @@
|
||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/morgan": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
|
||||
"integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"basic-auth": "~2.0.1",
|
||||
"debug": "2.6.9",
|
||||
"depd": "~2.0.0",
|
||||
"on-finished": "~2.3.0",
|
||||
"on-headers": "~1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/morgan/node_modules/on-finished": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||
"integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
@@ -1305,6 +1546,18 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/node-cache": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz",
|
||||
"integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clone": "2.x"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
@@ -1370,6 +1623,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/on-headers": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
|
||||
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
@@ -1379,6 +1641,15 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/one-time": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
|
||||
"integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fn.name": "1.x.x"
|
||||
}
|
||||
},
|
||||
"node_modules/opencollective-postinstall": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz",
|
||||
@@ -1626,6 +1897,15 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safe-stable-stringify": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
|
||||
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
@@ -1812,6 +2092,15 @@
|
||||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stack-trace": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
|
||||
"integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
@@ -1920,6 +2209,12 @@
|
||||
"integrity": "sha512-KX3bYSU5iGcO1XJa+QGPbi+Zjo2qq6eBhNjSGR5E5q0JtzkoipJKOUQD7ph8kFyteCEfEQ0maWLu8MCXtvX5uQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/text-hex": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
|
||||
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
@@ -1935,6 +2230,15 @@
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/triple-beam": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz",
|
||||
"integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
@@ -1999,6 +2303,15 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/validator": {
|
||||
"version": "13.15.26",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz",
|
||||
"integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
@@ -2052,6 +2365,70 @@
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/winston": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz",
|
||||
"integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@colors/colors": "^1.6.0",
|
||||
"@dabh/diagnostics": "^2.0.8",
|
||||
"async": "^3.2.3",
|
||||
"is-stream": "^2.0.0",
|
||||
"logform": "^2.7.0",
|
||||
"one-time": "^1.0.0",
|
||||
"readable-stream": "^3.4.0",
|
||||
"safe-stable-stringify": "^2.3.1",
|
||||
"stack-trace": "0.0.x",
|
||||
"triple-beam": "^1.3.0",
|
||||
"winston-transport": "^4.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/winston-transport": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz",
|
||||
"integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"logform": "^2.7.0",
|
||||
"readable-stream": "^3.6.2",
|
||||
"triple-beam": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/winston-transport/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/winston/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
|
||||
+6
-1
@@ -15,6 +15,11 @@
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"tesseract.js": "^5.0.4",
|
||||
"axios": "^1.6.7",
|
||||
"cheerio": "^1.0.0-rc.12"
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"express-validator": "^7.0.1",
|
||||
"node-cache": "^5.1.2",
|
||||
"winston": "^3.11.0",
|
||||
"morgan": "^1.10.0"
|
||||
}
|
||||
}
|
||||
|
||||
+9
-10
@@ -2,17 +2,16 @@ const express = require('express');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const db = require('../db/init');
|
||||
const { generateToken } = require('../middleware/auth');
|
||||
const { authLimiter } = require('../middleware/rateLimiter');
|
||||
const { validate, registerRules, loginRules } = require('../middleware/validate');
|
||||
const logger = require('../services/logger');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// 注册
|
||||
router.post('/register', (req, res) => {
|
||||
router.post('/register', authLimiter, registerRules, validate, (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ error: '用户名和密码不能为空' });
|
||||
}
|
||||
|
||||
try {
|
||||
const hashedPassword = bcrypt.hashSync(password, 10);
|
||||
const stmt = db.prepare('INSERT INTO users (username, password) VALUES (?, ?)');
|
||||
@@ -21,31 +20,31 @@ router.post('/register', (req, res) => {
|
||||
const user = { id: result.lastInsertRowid, username };
|
||||
const token = generateToken(user);
|
||||
|
||||
logger.info(`用户注册成功: ${username}`);
|
||||
res.json({ user, token });
|
||||
} catch (err) {
|
||||
if (err.code === 'SQLITE_CONSTRAINT_UNIQUE') {
|
||||
return res.status(400).json({ error: '用户名已存在' });
|
||||
}
|
||||
logger.error('注册失败', { error: err.message });
|
||||
res.status(500).json({ error: '注册失败' });
|
||||
}
|
||||
});
|
||||
|
||||
// 登录
|
||||
router.post('/login', (req, res) => {
|
||||
router.post('/login', authLimiter, loginRules, validate, (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ error: '用户名和密码不能为空' });
|
||||
}
|
||||
|
||||
const stmt = db.prepare('SELECT * FROM users WHERE username = ?');
|
||||
const user = stmt.get(username);
|
||||
|
||||
if (!user || !bcrypt.compareSync(password, user.password)) {
|
||||
logger.warn(`登录失败: ${username}`);
|
||||
return res.status(401).json({ error: '用户名或密码错误' });
|
||||
}
|
||||
|
||||
const token = generateToken(user);
|
||||
logger.info(`用户登录成功: ${username}`);
|
||||
res.json({ user: { id: user.id, username: user.username }, token });
|
||||
});
|
||||
|
||||
|
||||
@@ -2,25 +2,23 @@ const express = require('express');
|
||||
const db = require('../db/init');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
const { fetchLotteryResult, updateTicketsStatus, getEstimatedDrawTime } = require('../services/lottery');
|
||||
const { refreshLimiter, standardLimiter } = require('../middleware/rateLimiter');
|
||||
const { validate, lotteryQueryRules } = require('../middleware/validate');
|
||||
const logger = require('../services/logger');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
// 查询开奖结果
|
||||
router.get('/result', async (req, res) => {
|
||||
router.get('/result', lotteryQueryRules, validate, async (req, res) => {
|
||||
const { game, issue } = req.query;
|
||||
|
||||
if (!game || !issue) {
|
||||
return res.status(400).json({ error: '请提供彩票类型和期号' });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await fetchLotteryResult(game, issue);
|
||||
if (result) {
|
||||
res.json(result);
|
||||
} else {
|
||||
// 未开奖时返回预计开奖时间
|
||||
const estimatedTime = getEstimatedDrawTime(game, issue);
|
||||
res.json({
|
||||
message: '暂未开奖或未找到数据',
|
||||
@@ -29,22 +27,25 @@ router.get('/result', async (req, res) => {
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('查询开奖结果失败', { game, issue, error: err.message });
|
||||
res.status(500).json({ error: '查询失败' });
|
||||
}
|
||||
});
|
||||
|
||||
// 刷新用户所有待开奖彩票状态
|
||||
router.post('/refresh', async (req, res) => {
|
||||
router.post('/refresh', refreshLimiter, async (req, res) => {
|
||||
try {
|
||||
await updateTicketsStatus(req.user.id);
|
||||
logger.info(`刷新开奖: userId=${req.user.id}`);
|
||||
res.json({ message: '刷新成功' });
|
||||
} catch (err) {
|
||||
logger.error('刷新开奖失败', { error: err.message });
|
||||
res.status(500).json({ error: '刷新失败' });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取最近开奖记录
|
||||
router.get('/recent', async (req, res) => {
|
||||
router.get('/recent', standardLimiter, (req, res) => {
|
||||
const { game, limit = 10 } = req.query;
|
||||
|
||||
let query = 'SELECT * FROM lottery_results';
|
||||
@@ -58,8 +59,13 @@ router.get('/recent', async (req, res) => {
|
||||
query += ' ORDER BY draw_date DESC LIMIT ?';
|
||||
params.push(Number(limit));
|
||||
|
||||
try {
|
||||
const results = db.prepare(query).all(...params);
|
||||
res.json(results.map(r => ({ ...r, numbers: JSON.parse(r.numbers) })));
|
||||
} catch (err) {
|
||||
logger.error('获取最近开奖记录失败', { error: err.message });
|
||||
res.status(500).json({ error: '查询失败' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
+40
-8
@@ -3,7 +3,9 @@ const multer = require('multer');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
const { parseTicketImage } = require('../services/ocr');
|
||||
const { parseTicketImage, getOCRResult } = require('../services/ocr');
|
||||
const { ocrLimiter } = require('../middleware/rateLimiter');
|
||||
const logger = require('../services/logger');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -25,7 +27,6 @@ const upload = multer({
|
||||
storage,
|
||||
limits: { fileSize: 10 * 1024 * 1024 },
|
||||
fileFilter: (req, file, cb) => {
|
||||
// 放宽图片类型限制
|
||||
if (file.mimetype.startsWith('image/')) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
@@ -36,18 +37,49 @@ const upload = multer({
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
// 上传并解析彩票图片
|
||||
router.post('/parse', upload.single('image'), async (req, res) => {
|
||||
// 上传并解析彩票图片 (改为异步)
|
||||
router.post('/parse', ocrLimiter, upload.single('image'), (req, res) => {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: '请上传图片' });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await parseTicketImage(req.file.path);
|
||||
result.image_path = `/uploads/${req.file.filename}`;
|
||||
res.json(result);
|
||||
const { taskId } = parseTicketImage(req.file.path);
|
||||
logger.info(`OCR任务已提交: ${taskId}`);
|
||||
res.json({ taskId, status: 'processing', message: '图片已提交处理,请稍后查询结果' });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || '解析失败' });
|
||||
logger.error('OCR提交失败', { error: err.message });
|
||||
res.status(500).json({ error: err.message || '提交失败' });
|
||||
}
|
||||
});
|
||||
|
||||
// 查询OCR结果
|
||||
router.get('/result/:taskId', (req, res) => {
|
||||
const { taskId } = req.params;
|
||||
|
||||
if (!taskId || !taskId.startsWith('ocr_')) {
|
||||
return res.status(400).json({ error: '无效的任务ID' });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = getOCRResult(taskId);
|
||||
|
||||
if (result.completed) {
|
||||
if (result.success) {
|
||||
res.json({
|
||||
status: 'completed',
|
||||
...result.data
|
||||
// image_path 在提交时由客户端记录,此处无需返回
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({ status: 'failed', error: result.error });
|
||||
}
|
||||
} else {
|
||||
res.json({ status: result.processing ? 'processing' : 'pending', taskId });
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('OCR结果查询失败', { taskId, error: err.message });
|
||||
res.status(500).json({ error: '查询失败' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+14
-20
@@ -2,6 +2,8 @@ const express = require('express');
|
||||
const db = require('../db/init');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
const { getEstimatedDrawTime } = require('../services/lottery');
|
||||
const { validate, ticketCreateRules, ticketUpdateRules, idParamRule } = require('../middleware/validate');
|
||||
const logger = require('../services/logger');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -16,7 +18,6 @@ router.get('/', (req, res) => {
|
||||
SELECT * FROM tickets WHERE user_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?
|
||||
`).all(req.user.id, limit, offset);
|
||||
|
||||
// 为未开奖彩票添加预计开奖时间
|
||||
const ticketsWithDrawTime = tickets.map(ticket => {
|
||||
if (ticket.status === 'pending' && ticket.game && ticket.issue) {
|
||||
return {
|
||||
@@ -57,13 +58,9 @@ router.get('/stats', (req, res) => {
|
||||
});
|
||||
|
||||
// 添加彩票
|
||||
router.post('/', (req, res) => {
|
||||
router.post('/', ticketCreateRules, validate, (req, res) => {
|
||||
const { type, game, issue, numbers, bet_count, multiple, is_additional, cost, image_path } = req.body;
|
||||
|
||||
if (!type || !game || !cost) {
|
||||
return res.status(400).json({ error: '缺少必要字段' });
|
||||
}
|
||||
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO tickets (user_id, type, game, issue, numbers, bet_count, multiple, is_additional, cost, image_path)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
@@ -75,11 +72,12 @@ router.post('/', (req, res) => {
|
||||
bet_count || 1, multiple || 1, is_additional ? 1 : 0, cost, image_path || null
|
||||
);
|
||||
|
||||
logger.info(`添加彩票: userId=${req.user.id}, game=${game}, cost=${cost}`);
|
||||
res.json({ id: result.lastInsertRowid, message: '添加成功' });
|
||||
});
|
||||
|
||||
// 更新彩票
|
||||
router.put('/:id', (req, res) => {
|
||||
router.put('/:id', idParamRule, validate, (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { type, game, issue, numbers, bet_count, multiple, is_additional, cost, prize, status } = req.body;
|
||||
|
||||
@@ -90,7 +88,7 @@ router.put('/:id', (req, res) => {
|
||||
|
||||
const stmt = db.prepare(`
|
||||
UPDATE tickets SET type = ?, game = ?, issue = ?, numbers = ?, bet_count = ?, multiple = ?, is_additional = ?, cost = ?, prize = ?, status = ?
|
||||
WHERE id = ?
|
||||
WHERE id = ? AND user_id = ?
|
||||
`);
|
||||
stmt.run(
|
||||
type ?? ticket.type,
|
||||
@@ -103,35 +101,31 @@ router.put('/:id', (req, res) => {
|
||||
cost ?? ticket.cost,
|
||||
prize ?? ticket.prize,
|
||||
status ?? ticket.status,
|
||||
id
|
||||
id,
|
||||
req.user.id
|
||||
);
|
||||
|
||||
logger.info(`更新彩票: id=${id}`);
|
||||
res.json({ message: '更新成功' });
|
||||
});
|
||||
|
||||
// 删除彩票
|
||||
router.delete('/:id', (req, res) => {
|
||||
router.delete('/:id', idParamRule, validate, (req, res) => {
|
||||
const { id } = req.params;
|
||||
const ticketId = Number(id);
|
||||
|
||||
console.log('删除请求:', { id, ticketId, userId: req.user.id });
|
||||
|
||||
if (isNaN(ticketId)) {
|
||||
return res.status(400).json({ error: '无效的ID' });
|
||||
}
|
||||
const ticketId = Number(id); // idParamRule 已验证 id 为正整数
|
||||
|
||||
try {
|
||||
const result = db.prepare('DELETE FROM tickets WHERE id = ? AND user_id = ?').run(ticketId, req.user.id);
|
||||
console.log('删除结果:', result);
|
||||
|
||||
if (result.changes === 0) {
|
||||
return res.status(404).json({ error: '彩票不存在' });
|
||||
}
|
||||
|
||||
logger.info(`删除彩票: id=${ticketId}`);
|
||||
res.json({ message: '删除成功' });
|
||||
} catch (err) {
|
||||
console.error('删除错误:', err);
|
||||
res.status(500).json({ error: '删除失败: ' + err.message });
|
||||
logger.error('删除彩票失败', { error: err.message });
|
||||
res.status(500).json({ error: '删除失败' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
const winston = require('winston');
|
||||
const path = require('path');
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.json()
|
||||
),
|
||||
defaultMeta: { service: 'lottery-api' },
|
||||
transports: [
|
||||
new winston.transports.File({ filename: path.join(__dirname, '../logs/error.log'), level: 'error' }),
|
||||
new winston.transports.File({ filename: path.join(__dirname, '../logs/combined.log') }),
|
||||
new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.printf(({ timestamp, level, message, ...meta }) => {
|
||||
const metaStr = Object.keys(meta).length ? JSON.stringify(meta) : '';
|
||||
return `${timestamp} [${level}]: ${message} ${metaStr}`;
|
||||
})
|
||||
)
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
module.exports = logger;
|
||||
@@ -1,7 +1,12 @@
|
||||
const axios = require('axios');
|
||||
const cheerio = require('cheerio');
|
||||
const NodeCache = require('node-cache');
|
||||
const logger = require('./logger');
|
||||
const db = require('../db/init');
|
||||
|
||||
// 内存缓存 - 5分钟TTL, 最多1000个键防止内存溢出
|
||||
const cache = new NodeCache({ stdTTL: 300, checkperiod: 60, maxKeys: 1000 });
|
||||
|
||||
// 获取预计开奖时间
|
||||
function getEstimatedDrawTime(game, issue) {
|
||||
const now = new Date();
|
||||
@@ -51,10 +56,21 @@ function getEstimatedDrawTime(game, issue) {
|
||||
|
||||
// 获取开奖结果
|
||||
async function fetchLotteryResult(game, issue) {
|
||||
// 先查本地缓存
|
||||
const cached = db.prepare('SELECT * FROM lottery_results WHERE game = ? AND issue = ?').get(game, issue);
|
||||
const cacheKey = `lottery:${game}:${issue}`;
|
||||
|
||||
// 先查内存缓存
|
||||
const cached = cache.get(cacheKey);
|
||||
if (cached) {
|
||||
return { ...cached, numbers: JSON.parse(cached.numbers) };
|
||||
logger.debug(`内存缓存命中: ${cacheKey}`);
|
||||
return cached;
|
||||
}
|
||||
|
||||
// 查本地数据库
|
||||
const dbCached = db.prepare('SELECT * FROM lottery_results WHERE game = ? AND issue = ?').get(game, issue);
|
||||
if (dbCached) {
|
||||
const result = { ...dbCached, numbers: JSON.parse(dbCached.numbers) };
|
||||
cache.set(cacheKey, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 从网络获取
|
||||
@@ -66,26 +82,41 @@ async function fetchLotteryResult(game, issue) {
|
||||
INSERT OR REPLACE INTO lottery_results (game, issue, numbers, draw_date)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`).run(game, issue, JSON.stringify(result.numbers), result.draw_date);
|
||||
cache.set(cacheKey, result);
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
console.error('获取开奖数据失败:', err.message);
|
||||
logger.error('获取开奖数据失败:', err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 从500.com获取数据
|
||||
async function fetchFromWeb(game, issue) {
|
||||
// 检查是否在冷却期 (防止频繁请求)
|
||||
const cooldownKey = `cooldown:${game}`;
|
||||
if (cache.get(cooldownKey)) {
|
||||
logger.warn(`请求冷却中: ${game}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
let result = null;
|
||||
try {
|
||||
if (game === '双色球') {
|
||||
return await fetchSSQ(issue);
|
||||
result = await fetchSSQ(issue);
|
||||
} else if (game === '大乐透') {
|
||||
return await fetchDLT(issue);
|
||||
result = await fetchDLT(issue);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`获取${game}开奖数据失败:`, err.message);
|
||||
logger.error(`获取${game}开奖数据失败:`, err.message);
|
||||
}
|
||||
return null;
|
||||
|
||||
// 设置请求冷却 (5分钟)
|
||||
if (!result) {
|
||||
cache.set(cooldownKey, true, 300);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 获取双色球开奖数据
|
||||
@@ -224,6 +255,7 @@ async function updateTicketsStatus(userId) {
|
||||
`).all(userId);
|
||||
|
||||
for (const ticket of pendingTickets) {
|
||||
try {
|
||||
const result = await fetchLotteryResult(ticket.game, ticket.issue);
|
||||
if (result) {
|
||||
const ticketNumbers = ticket.numbers ? JSON.parse(ticket.numbers) : null;
|
||||
@@ -233,7 +265,6 @@ async function updateTicketsStatus(userId) {
|
||||
const winning = checkWinning(ticket.game, num, result.numbers);
|
||||
if (winning.won) {
|
||||
totalPrize += winning.prize * ticket.multiple;
|
||||
// 大乐透只有1等奖和2等奖追加才有80%浮动奖金,其他等级没有
|
||||
if (ticket.is_additional && winning.level >= 1 && winning.level <= 2) {
|
||||
totalPrize += Math.floor(winning.prize * 0.8) * ticket.multiple;
|
||||
}
|
||||
@@ -244,6 +275,10 @@ async function updateTicketsStatus(userId) {
|
||||
.run(totalPrize, totalPrize > 0 ? 'won' : 'lost', ticket.id);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`更新彩票 ${ticket.id} 状态失败`, { error: err.message });
|
||||
// 继续处理其他彩票,不因单个失败中止
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+103
-48
@@ -1,53 +1,119 @@
|
||||
const Tesseract = require('tesseract.js');
|
||||
const path = require('path');
|
||||
const logger = require('./logger');
|
||||
|
||||
// 彩票类型识别规则
|
||||
// 彩票类型识别规则 (模块级别常量,避免每次调用时重建)
|
||||
const LOTTERY_PATTERNS = {
|
||||
// 福利彩票
|
||||
ssq: { name: '双色球', type: 'welfare', pattern: /双\s*色\s*球|SSQ/i, price: 2 },
|
||||
fc3d: { name: '福彩3D', type: 'welfare', pattern: /福彩\s*3D|3D/i, price: 2 },
|
||||
fc3d: { name: '福彩3D', type: 'welfare', pattern: /福\s*彩\s*3D|3D/i, price: 2 },
|
||||
qlc: { name: '七乐彩', type: 'welfare', pattern: /七\s*乐\s*彩/i, price: 2 },
|
||||
// 体育彩票
|
||||
dlt: { name: '大乐透', type: 'sports', pattern: /大\s*乐\s*透|超级\s*大\s*乐\s*透/i, price: 2 },
|
||||
pl3: { name: '排列3', type: 'sports', pattern: /排列\s*3|排列\s*三/i, price: 2 },
|
||||
pl5: { name: '排列5', type: 'sports', pattern: /排列\s*5|排列\s*五/i, price: 2 },
|
||||
qxc: { name: '七星彩', type: 'sports', pattern: /七\s*星\s*彩/i, price: 2 },
|
||||
};
|
||||
|
||||
// 简单的内存任务队列 (有界队列防止内存溢出)
|
||||
const MAX_QUEUE_SIZE = 100;
|
||||
const taskQueue = [];
|
||||
const processingTasks = new Set();
|
||||
const completedTasks = new Map(); // taskId -> result
|
||||
const TASK_TIMEOUT = 5 * 60 * 1000; // 5分钟超时
|
||||
|
||||
// OCR处理函数
|
||||
async function processOCR(task) {
|
||||
const { taskId, imagePath } = task;
|
||||
|
||||
try {
|
||||
logger.info(`OCR任务开始: ${taskId}`);
|
||||
const text = await parseWithLocalTesseract(imagePath);
|
||||
const result = parseTicketText(text);
|
||||
|
||||
completedTasks.set(taskId, { success: true, data: result });
|
||||
// 5分钟后清理已完成的任务 (只设置一次)
|
||||
setTimeout(() => {
|
||||
completedTasks.delete(taskId);
|
||||
logger.debug(`OCR任务已清理: ${taskId}`);
|
||||
}, 5 * 60 * 1000);
|
||||
logger.info(`OCR任务完成: ${taskId}`);
|
||||
} catch (err) {
|
||||
logger.error(`OCR任务失败: ${taskId}`, { error: err.message });
|
||||
completedTasks.set(taskId, { success: false, error: err.message });
|
||||
setTimeout(() => {
|
||||
completedTasks.delete(taskId);
|
||||
}, 5 * 60 * 1000);
|
||||
} finally {
|
||||
processingTasks.delete(taskId);
|
||||
processNextTask();
|
||||
}
|
||||
}
|
||||
|
||||
// 处理下一个任务
|
||||
function processNextTask() {
|
||||
if (processingTasks.size >= 2) return; // 最多同时处理2个
|
||||
|
||||
const nextTask = taskQueue.shift();
|
||||
if (nextTask) {
|
||||
processingTasks.add(nextTask.taskId);
|
||||
processOCR(nextTask);
|
||||
}
|
||||
}
|
||||
|
||||
// 提交OCR任务 (同步返回taskId)
|
||||
function submitOCRTask(imagePath) {
|
||||
if (taskQueue.length >= MAX_QUEUE_SIZE) {
|
||||
throw new Error('任务队列已满,请稍后再试');
|
||||
}
|
||||
|
||||
const taskId = `ocr_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
||||
taskQueue.push({ taskId, imagePath, submittedAt: Date.now() });
|
||||
logger.info(`OCR任务已提交: ${taskId}, 队列长度: ${taskQueue.length}`);
|
||||
|
||||
processNextTask();
|
||||
|
||||
return taskId;
|
||||
}
|
||||
|
||||
// 查询任务状态
|
||||
function getOCRTaskStatus(taskId) {
|
||||
if (completedTasks.has(taskId)) {
|
||||
return { completed: true, ...completedTasks.get(taskId) };
|
||||
}
|
||||
|
||||
// 检查是否超时 - O(n) scan (可接受,队列通常很小)
|
||||
const task = taskQueue.find(t => t.taskId === taskId);
|
||||
if (task && Date.now() - task.submittedAt > TASK_TIMEOUT) {
|
||||
return { completed: true, success: false, error: '任务处理超时' };
|
||||
}
|
||||
|
||||
if (processingTasks.has(taskId)) {
|
||||
return { completed: false, processing: true };
|
||||
}
|
||||
|
||||
return { completed: false };
|
||||
}
|
||||
|
||||
// 使用本地tesseract.js-core进行OCR识别
|
||||
async function parseWithLocalTesseract(imagePath) {
|
||||
try {
|
||||
// 使用本地core文件
|
||||
const corePath = path.join(__dirname, '../node_modules/tesseract.js-core');
|
||||
|
||||
const { data } = await Tesseract.recognize(imagePath, 'chi_sim+eng', {
|
||||
corePath: corePath,
|
||||
logger: m => console.log('OCR:', m.status, m.progress)
|
||||
logger: m => logger.debug(`OCR: ${m.status} ${m.progress || ''}`)
|
||||
});
|
||||
|
||||
return data.text;
|
||||
} catch (err) {
|
||||
console.error('tesseract.js识别失败:', err.message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// 解析彩票图片
|
||||
async function parseTicketImage(imagePath) {
|
||||
let text = '';
|
||||
|
||||
try {
|
||||
text = await parseWithLocalTesseract(imagePath);
|
||||
console.log('OCR识别完成, 文本长度:', text.length);
|
||||
} catch (err) {
|
||||
console.error('OCR识别失败:', err);
|
||||
throw new Error('图片识别失败');
|
||||
// 解析彩票图片 (改为异步提交)
|
||||
function parseTicketImage(imagePath) {
|
||||
const taskId = submitOCRTask(imagePath);
|
||||
return { taskId, status: 'pending' };
|
||||
}
|
||||
|
||||
// 调试:打印识别的文本
|
||||
console.log('识别的文本:', text.substring(0, 500));
|
||||
|
||||
return parseTicketText(text);
|
||||
// 查询OCR结果 (轮询)
|
||||
function getOCRResult(taskId) {
|
||||
return getOCRTaskStatus(taskId);
|
||||
}
|
||||
|
||||
// 解析彩票文本
|
||||
@@ -64,7 +130,7 @@ function parseTicketText(text) {
|
||||
raw_text: text
|
||||
};
|
||||
|
||||
// 识别彩票类型
|
||||
// 识别彩票类型 (使用模块级别的 LOTTERY_PATTERNS)
|
||||
for (const [key, lottery] of Object.entries(LOTTERY_PATTERNS)) {
|
||||
if (lottery.pattern.test(text)) {
|
||||
result.type = lottery.type;
|
||||
@@ -73,7 +139,7 @@ function parseTicketText(text) {
|
||||
}
|
||||
}
|
||||
|
||||
// 提取期号 - 优先匹配"期号:xxx"格式
|
||||
// 提取期号
|
||||
const issueMatch = text.match(/期\s*号\s*[::]\s*(\d{5,})/);
|
||||
if (issueMatch) {
|
||||
result.issue = issueMatch[1];
|
||||
@@ -99,22 +165,20 @@ function parseTicketText(text) {
|
||||
// 检测是否追加
|
||||
result.is_additional = /追加|追加投注/.test(text);
|
||||
|
||||
// 提取金额 - 支持多种格式
|
||||
// 提取金额
|
||||
const costMatch = text.match(/金额\s*[::]\s*(\d+(?:[.,]\d+)?)\s*元|合计[::]\s*(\d+(?:\.\d+)?)\s*元|(\d+(?:\.\d+)?)\s*元/);
|
||||
if (costMatch) {
|
||||
const costStr = (costMatch[1] || costMatch[2] || costMatch[3]).replace(',', '.');
|
||||
result.cost = parseFloat(costStr);
|
||||
}
|
||||
|
||||
// 提取号码 - 根据不同彩票类型
|
||||
// 提取号码
|
||||
result.numbers = extractNumbers(text, result.game);
|
||||
|
||||
// 根据识别到的号码数量更新注数
|
||||
if (result.numbers.length > 0) {
|
||||
result.bet_count = result.numbers.length;
|
||||
}
|
||||
|
||||
// 如果没有识别到金额,根据注数和倍数计算
|
||||
if (!result.cost && result.game && result.numbers.length > 0) {
|
||||
const lottery = Object.values(LOTTERY_PATTERNS).find(l => l.name === result.game);
|
||||
if (lottery) {
|
||||
@@ -131,32 +195,27 @@ function parseTicketText(text) {
|
||||
function extractNumbers(text, game) {
|
||||
const numbers = [];
|
||||
|
||||
// 预处理文本:修复常见OCR错误
|
||||
let cleanText = text
|
||||
.replace(/[oO]/g, '0') // o和O经常被误识别为0
|
||||
.replace(/[D]/g, '0') // D经常被误识别
|
||||
.replace(/[lI|]/g, '1') // l和I和|经常被误识别为1
|
||||
.replace(/[S\$§]/g, '5') // S和$和§经常被误识别为5
|
||||
.replace(/[@®»«]/g, '') // 去掉这些特殊字符
|
||||
.replace(/[::]/g, ' ') // 冒号替换为空格
|
||||
.replace(/[g]/g, '9'); // g经常被误识别为9
|
||||
.replace(/[oO]/g, '0')
|
||||
.replace(/[D]/g, '0')
|
||||
.replace(/[lI|]/g, '1')
|
||||
.replace(/[S$§]/g, '5')
|
||||
.replace(/[@®»«]/g, '')
|
||||
.replace(/[::]/g, ' ')
|
||||
.replace(/[g]/g, '9');
|
||||
|
||||
// 处理连在一起的数字:将6位以上连续数字按每2位分割
|
||||
cleanText = cleanText.replace(/\b(\d{6,})\b/g, (match) => {
|
||||
return match.match(/.{1,2}/g).join(' ');
|
||||
});
|
||||
// 处理4位连续数字
|
||||
cleanText = cleanText.replace(/\b(\d{4})\b/g, (match) => {
|
||||
return match.slice(0, 2) + ' ' + match.slice(2);
|
||||
});
|
||||
|
||||
// 移除开奖号码行(包含"开奖号码"的行)
|
||||
cleanText = cleanText.split('\n')
|
||||
.filter(line => !line.includes('开奖') && !line.includes('号码'))
|
||||
.join('\n');
|
||||
|
||||
if (game === '双色球') {
|
||||
// 双色球: 6红+1蓝,支持多种格式
|
||||
const patterns = [
|
||||
/(\d{2})\s+(\d{2})\s+(\d{2})\s+(\d{2})\s+(\d{2})\s+(\d{2})[-+](\d{2})/g,
|
||||
/(\d{2})\s+(\d{2})\s+(\d{2})\s+(\d{2})\s+(\d{2})\s+(\d{2})\s*[+|]\s*(\d{2})/g,
|
||||
@@ -173,12 +232,9 @@ function extractNumbers(text, game) {
|
||||
if (numbers.length > 0) break;
|
||||
}
|
||||
} else if (game === '大乐透') {
|
||||
// 大乐透: 5前区+2后区
|
||||
const patterns = [
|
||||
// 带空格的标准格式
|
||||
/(\d{2})\s+(\d{2})\s+(\d{2})\s+(\d{2})\s+(\d{2})[-+](\d{2})\s+(\d{2})/g,
|
||||
/(\d{2})\s+(\d{2})\s+(\d{2})\s+(\d{2})\s+(\d{2})\s*[+|]\s*(\d{2})\s+(\d{2})/g,
|
||||
// 连在一起无空格的格式(第一注可能这样)
|
||||
/(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})[-+](\d{2})(\d{2})/g,
|
||||
/(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})[+|](\d{2})(\d{2})/g,
|
||||
];
|
||||
@@ -194,7 +250,6 @@ function extractNumbers(text, game) {
|
||||
if (numbers.length > 0) break;
|
||||
}
|
||||
} else if (game === '福彩3D' || game === '排列3') {
|
||||
// 3位数
|
||||
const matches = cleanText.matchAll(/[^\d](\d)\s*(\d)\s*(\d)[^\d]/g);
|
||||
for (const match of matches) {
|
||||
numbers.push([match[1], match[2], match[3]]);
|
||||
@@ -204,4 +259,4 @@ function extractNumbers(text, game) {
|
||||
return numbers;
|
||||
}
|
||||
|
||||
module.exports = { parseTicketImage, parseTicketText };
|
||||
module.exports = { parseTicketImage, parseTicketText, getOCRResult };
|
||||
|
||||
Reference in New Issue
Block a user