2025-09-18
app后端搭建
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/node_modules/
|
||||||
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# 默认忽略的文件
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 基于编辑器的 HTTP 客户端请求
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
12
.idea/dataSources.xml
generated
Normal file
12
.idea/dataSources.xml
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
|
<data-source source="LOCAL" name="test_mao@114.55.111.44" uuid="ed6f561f-4021-4cc9-a78a-71ff11b68d9e">
|
||||||
|
<driver-ref>mysql.8</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
|
||||||
|
<jdbc-url>jdbc:mysql://114.55.111.44:3306/test_mao</jdbc-url>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
</data-source>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/data_source_mapping.xml
generated
Normal file
6
.idea/data_source_mapping.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourcePerFileMappings">
|
||||||
|
<file url="file://$PROJECT_DIR$/routes/auth.js" value="ed6f561f-4021-4cc9-a78a-71ff11b68d9e" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/jsLibraryMappings.xml
generated
Normal file
6
.idea/jsLibraryMappings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="JavaScriptLibraryMappings">
|
||||||
|
<includedPredefinedLibrary name="Node.js Core" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
12
.idea/middle_office_system.iml
generated
Normal file
12
.idea/middle_office_system.iml
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/middle_office_system.iml" filepath="$PROJECT_DIR$/.idea/middle_office_system.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
11
.idea/runConfigurations/bin_www.xml
generated
Normal file
11
.idea/runConfigurations/bin_www.xml
generated
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="bin\www" type="NodeJSConfigurationType" path-to-js-file="bin/www" working-dir="$PROJECT_DIR$">
|
||||||
|
<envs>
|
||||||
|
<env name="DEBUG" value="middle-office-system:*" />
|
||||||
|
</envs>
|
||||||
|
<EXTENSION ID="com.intellij.lang.javascript.buildTools.npm.rc.StartBrowserRunConfigurationExtension">
|
||||||
|
<browser url="http://localhost:3000/" />
|
||||||
|
</EXTENSION>
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
13
.idea/sqldialects.xml
generated
Normal file
13
.idea/sqldialects.xml
generated
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="SqlDialectMappings">
|
||||||
|
<file url="file://$PROJECT_DIR$/../jurong_circle_black" dialect="MySQL" />
|
||||||
|
<file url="file://$PROJECT_DIR$" dialect="MySQL" />
|
||||||
|
<file url="PROJECT" dialect="MySQL" />
|
||||||
|
</component>
|
||||||
|
<component name="SqlResolveMappings">
|
||||||
|
<file url="file://$PROJECT_DIR$/../jurong_circle_black" scope="{"node":{ "@negative":"1", "group":{ "@kind":"root", "node":{ "name":{ "@qname":"ed6f561f-4021-4cc9-a78a-71ff11b68d9e" }, "group":{ "@kind":"schema", "node":{ "name":{ "@qname":"test_mao" } } } } } }}" />
|
||||||
|
<file url="file://$PROJECT_DIR$" scope="{"node":{ "@negative":"1", "group":{ "@kind":"root", "node":{ "name":{ "@qname":"ed6f561f-4021-4cc9-a78a-71ff11b68d9e" }, "group":{ "@kind":"schema", "node":{ "name":{ "@qname":"test_mao" } } } } } }}" />
|
||||||
|
<file url="PROJECT" scope="{"node":{ "@negative":"1", "group":{ "@kind":"root", "node":{ "name":{ "@qname":"ed6f561f-4021-4cc9-a78a-71ff11b68d9e" }, "group":{ "@kind":"schema", "node":{ "name":{ "@qname":"test_mao" } } } } } }}" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
51
app.js
Normal file
51
app.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
var express = require('express');
|
||||||
|
var path = require('path');
|
||||||
|
var cookieParser = require('cookie-parser');
|
||||||
|
var logger = require('morgan');
|
||||||
|
const {specs, swaggerUi} = require('./config/swagger'); // 引入 swagger 配置
|
||||||
|
const cors = require('cors');
|
||||||
|
const bodyParser = require('body-parser');
|
||||||
|
|
||||||
|
var indexRouter = require('./routes/index');
|
||||||
|
var usersRouter = require('./routes/users');
|
||||||
|
var commonRouter = require('./routes/common');
|
||||||
|
var captchaRouter = require('./routes/captcha');
|
||||||
|
var authRouter = require('./routes/auth');
|
||||||
|
|
||||||
|
var app = express();
|
||||||
|
|
||||||
|
// 中间件配置
|
||||||
|
// CORS配置 - 允许前端访问静态资源
|
||||||
|
app.use(cors({
|
||||||
|
origin: [
|
||||||
|
'http://localhost:5173',
|
||||||
|
'http://localhost:5176',
|
||||||
|
'http://localhost:5175',
|
||||||
|
'http://localhost:5174',
|
||||||
|
'http://localhost:3001',
|
||||||
|
'https://www.zrbjr.com',
|
||||||
|
'https://zrbjr.com'
|
||||||
|
],
|
||||||
|
credentials: true,
|
||||||
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||||
|
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With']
|
||||||
|
}));
|
||||||
|
app.use(bodyParser.json({ limit: '10mb' }));
|
||||||
|
app.use(bodyParser.urlencoded({ extended: true, limit: '10mb' }));
|
||||||
|
|
||||||
|
|
||||||
|
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs)); // swagger
|
||||||
|
|
||||||
|
app.use(logger('dev'));
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded({extended: false}));
|
||||||
|
app.use(cookieParser());
|
||||||
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
|
||||||
|
app.use('/users', usersRouter);
|
||||||
|
app.use('/api/common', commonRouter)
|
||||||
|
app.use('/api/captcha', captchaRouter)
|
||||||
|
app.use('/api/auth', authRouter)
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = app;
|
||||||
96
bin/www
Normal file
96
bin/www
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module dependencies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var app = require('../app');
|
||||||
|
var debug = require('debug')('middle-office-system:server');
|
||||||
|
var http = require('http');
|
||||||
|
|
||||||
|
// 引入数据库初始化模块
|
||||||
|
const {initDatabase} = require('../config/database-init');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get port from environment and store in Express.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var port = normalizePort(process.env.PORT || '5001');
|
||||||
|
app.set('port', port);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create HTTP server.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var server = http.createServer(app);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listen on provided port, on all network interfaces.
|
||||||
|
*/
|
||||||
|
|
||||||
|
server.listen(port, async () => {
|
||||||
|
await initDatabase();
|
||||||
|
global.captchaStore = new Map();
|
||||||
|
});
|
||||||
|
server.on('error', onError);
|
||||||
|
server.on('listening', onListening);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a port into a number, string, or false.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function normalizePort(val) {
|
||||||
|
var port = parseInt(val, 10);
|
||||||
|
|
||||||
|
if (isNaN(port)) {
|
||||||
|
// named pipe
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (port >= 0) {
|
||||||
|
// port number
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event listener for HTTP server "error" event.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function onError(error) {
|
||||||
|
if (error.syscall !== 'listen') {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bind = typeof port === 'string'
|
||||||
|
? 'Pipe ' + port
|
||||||
|
: 'Port ' + port;
|
||||||
|
|
||||||
|
// handle specific listen errors with friendly messages
|
||||||
|
switch (error.code) {
|
||||||
|
case 'EACCES':
|
||||||
|
console.error(bind + ' requires elevated privileges');
|
||||||
|
process.exit(1);
|
||||||
|
break;
|
||||||
|
case 'EADDRINUSE':
|
||||||
|
console.error(bind + ' is already in use');
|
||||||
|
process.exit(1);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event listener for HTTP server "listening" event.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function onListening() {
|
||||||
|
var addr = server.address();
|
||||||
|
var bind = typeof addr === 'string'
|
||||||
|
? 'pipe ' + addr
|
||||||
|
: 'port ' + addr.port;
|
||||||
|
debug('Listening on ' + bind);
|
||||||
|
}
|
||||||
17
config/config.js
Normal file
17
config/config.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
const mysql = require('mysql2')
|
||||||
|
|
||||||
|
const sql = {
|
||||||
|
createConnection() {
|
||||||
|
return mysql.createPool({
|
||||||
|
connectionLimit: 10,
|
||||||
|
host: '114.55.111.44',
|
||||||
|
user: 'test_mao',
|
||||||
|
password: 'nK2mPbWriBp25BRd',
|
||||||
|
database: 'test_mao',
|
||||||
|
charset: 'utf8mb4',
|
||||||
|
multipleStatements: true
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = sql
|
||||||
70
config/constants.js
Normal file
70
config/constants.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
// 系统常量配置
|
||||||
|
module.exports = {
|
||||||
|
// 转账类型
|
||||||
|
TRANSFER_TYPES: {
|
||||||
|
USER_TO_USER: 'user_to_user',
|
||||||
|
SYSTEM_TO_USER: 'system_to_user',
|
||||||
|
USER_TO_SYSTEM: 'user_to_system'
|
||||||
|
},
|
||||||
|
|
||||||
|
// 转账状态
|
||||||
|
TRANSFER_STATUS: {
|
||||||
|
PENDING: 'pending',
|
||||||
|
CONFIRMED: 'confirmed',
|
||||||
|
RECEIVED: 'received',
|
||||||
|
REJECTED: 'rejected',
|
||||||
|
CANCELLED: 'cancelled',
|
||||||
|
NOT_RECEIVED: 'not_received',
|
||||||
|
FAILED: 'failed'
|
||||||
|
},
|
||||||
|
|
||||||
|
// 用户角色
|
||||||
|
USER_ROLES: {
|
||||||
|
ADMIN: 'admin',
|
||||||
|
USER: 'user'
|
||||||
|
},
|
||||||
|
|
||||||
|
// 订单状态
|
||||||
|
ORDER_STATUS: {
|
||||||
|
PENDING: 'pending',
|
||||||
|
PAID: 'paid',
|
||||||
|
SHIPPED: 'shipped',
|
||||||
|
DELIVERED: 'delivered',
|
||||||
|
CANCELLED: 'cancelled'
|
||||||
|
},
|
||||||
|
|
||||||
|
// 错误代码
|
||||||
|
ERROR_CODES: {
|
||||||
|
VALIDATION_ERROR: 'VALIDATION_ERROR',
|
||||||
|
AUTHENTICATION_ERROR: 'AUTHENTICATION_ERROR',
|
||||||
|
AUTHORIZATION_ERROR: 'AUTHORIZATION_ERROR',
|
||||||
|
NOT_FOUND: 'NOT_FOUND',
|
||||||
|
DUPLICATE_ENTRY: 'DUPLICATE_ENTRY',
|
||||||
|
DATABASE_ERROR: 'DATABASE_ERROR',
|
||||||
|
INTERNAL_ERROR: 'INTERNAL_ERROR'
|
||||||
|
},
|
||||||
|
|
||||||
|
// HTTP状态码
|
||||||
|
HTTP_STATUS: {
|
||||||
|
OK: 200,
|
||||||
|
CREATED: 201,
|
||||||
|
BAD_REQUEST: 400,
|
||||||
|
UNAUTHORIZED: 401,
|
||||||
|
FORBIDDEN: 403,
|
||||||
|
NOT_FOUND: 404,
|
||||||
|
CONFLICT: 409,
|
||||||
|
INTERNAL_SERVER_ERROR: 500
|
||||||
|
},
|
||||||
|
|
||||||
|
// 分页默认值
|
||||||
|
PAGINATION: {
|
||||||
|
DEFAULT_PAGE: 1,
|
||||||
|
DEFAULT_LIMIT: 10,
|
||||||
|
MAX_LIMIT: 100
|
||||||
|
},
|
||||||
|
|
||||||
|
// JWT配置
|
||||||
|
JWT: {
|
||||||
|
EXPIRES_IN: '24h'
|
||||||
|
}
|
||||||
|
};
|
||||||
38
config/database-init.js
Normal file
38
config/database-init.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
const mysql = require('mysql2/promise');
|
||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
const { initDB, getDB, dbConfig } = require('../database');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据库初始化函数
|
||||||
|
* 创建所有必要的表结构和初始数据
|
||||||
|
*/
|
||||||
|
async function initDatabase() {
|
||||||
|
try {
|
||||||
|
|
||||||
|
|
||||||
|
// 初始化数据库连接池
|
||||||
|
await initDB();
|
||||||
|
console.log('数据库连接池初始化成功');
|
||||||
|
|
||||||
|
// 创建所有表
|
||||||
|
// await createTables();
|
||||||
|
|
||||||
|
// 添加字段(处理表结构升级)
|
||||||
|
// await addMissingFields();
|
||||||
|
|
||||||
|
// 创建默认数据
|
||||||
|
// await createDefaultData();
|
||||||
|
|
||||||
|
console.log('数据库初始化完成');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('数据库初始化失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
initDatabase
|
||||||
|
};
|
||||||
363
config/dbv2.js
Normal file
363
config/dbv2.js
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
class QueryBuilder {
|
||||||
|
constructor() {
|
||||||
|
this.conditions = {};
|
||||||
|
this.limit = null;
|
||||||
|
this.offset = null;
|
||||||
|
this.groupBy = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
where(condition, ...params) {
|
||||||
|
this.conditions[condition] = params;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLimit(limit) {
|
||||||
|
this.limit = limit;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
setOffset(offset) {
|
||||||
|
this.offset = offset;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
setGroupBy(groupBy) {
|
||||||
|
this.groupBy = groupBy;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqdata(sql, params) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
global.sqlReq.query(sql, params, (err, result) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
resolve(result);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getParams() {
|
||||||
|
return Object.values(this.conditions).flat();
|
||||||
|
}
|
||||||
|
|
||||||
|
buildConditions() {
|
||||||
|
return Object.keys(this.conditions).map(condition => `${condition}`).join(' AND ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SelectBuilder extends QueryBuilder {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.selectFields = [];
|
||||||
|
this.tables = [];
|
||||||
|
this.orderByField = '';
|
||||||
|
this.orderByDirection = 'ASC';
|
||||||
|
this.subQueries = []; // 用于存储子查询
|
||||||
|
this.unions = []; // 存储UNION查询
|
||||||
|
}
|
||||||
|
// 添加UNION查询
|
||||||
|
union(queryBuilder, type = 'UNION') {
|
||||||
|
this.unions.push({queryBuilder, type});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加UNION ALL查询
|
||||||
|
unionAll(queryBuilder) {
|
||||||
|
this.union(queryBuilder, 'UNION ALL');
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建主查询部分(不含ORDER BY/LIMIT/OFFSET)
|
||||||
|
buildMainQuery() {
|
||||||
|
const subQuerySQL = this.subQueries.map(({alias, subQuery}) => `(${subQuery}) AS ${alias}`);
|
||||||
|
const selectClause = this.selectFields.concat(subQuerySQL).join(', ');
|
||||||
|
|
||||||
|
let sql = `SELECT ${selectClause}
|
||||||
|
FROM ${this.tables.join(' ')}`;
|
||||||
|
|
||||||
|
const conditionClauses = this.buildConditions();
|
||||||
|
if (conditionClauses) {
|
||||||
|
sql += ` WHERE ${conditionClauses}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.groupBy) {
|
||||||
|
sql += ` GROUP BY ${this.groupBy}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = this.getParams();
|
||||||
|
return {sql, params};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 供UNION查询调用的构建方法
|
||||||
|
buildForUnion() {
|
||||||
|
return this.buildMainQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
select(fields) {
|
||||||
|
this.selectFields = fields.split(',').map(field => field.trim());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加子查询
|
||||||
|
addSubQuery(alias, subQuery) {
|
||||||
|
this.subQueries.push({alias, subQuery});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
whereLike(fields, keyword) {
|
||||||
|
const likeConditions = fields.map(field => `${field} LIKE ?`).join(' OR ');
|
||||||
|
this.conditions[likeConditions] = fields.map(() => `%${keyword}%`);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
from(table) {
|
||||||
|
this.tables.push(table);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
leftJoin(table, condition) {
|
||||||
|
this.tables.push(`LEFT JOIN ${table} ON ${condition}`);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
orderBy(field, direction = 'ASC') {
|
||||||
|
this.orderByField = field;
|
||||||
|
this.orderByDirection = direction.toUpperCase();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
paginate(page, pageSize) {
|
||||||
|
if (page <= 0 || pageSize <= 0) {
|
||||||
|
throw new Error('分页参数必须大于0');
|
||||||
|
}
|
||||||
|
this.limit = pageSize;
|
||||||
|
this.offset = (page - 1) * pageSize;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async chidBuild() {
|
||||||
|
|
||||||
|
let sql = `SELECT ${this.selectFields.join(', ')}
|
||||||
|
FROM ${this.tables.join(' ')}`;
|
||||||
|
let conditionClauses = this.buildConditions();
|
||||||
|
if (conditionClauses) {
|
||||||
|
sql += ` WHERE ${conditionClauses}`;
|
||||||
|
}
|
||||||
|
if (this.orderByField) {
|
||||||
|
sql += ` ORDER BY ${this.orderByField} ${this.orderByDirection}`;
|
||||||
|
}
|
||||||
|
if (this.limit !== null) {
|
||||||
|
sql += ` LIMIT ${this.limit}`;
|
||||||
|
}
|
||||||
|
if (this.offset !== null) {
|
||||||
|
sql += ` OFFSET ${this.offset}`;
|
||||||
|
}
|
||||||
|
return sql;
|
||||||
|
}
|
||||||
|
|
||||||
|
async build() {
|
||||||
|
const main = this.buildMainQuery();
|
||||||
|
let fullSql = `(${main.sql})`;
|
||||||
|
const allParams = [...main.params];
|
||||||
|
|
||||||
|
// 处理UNION部分
|
||||||
|
for (const union of this.unions) {
|
||||||
|
const unionBuilder = union.queryBuilder;
|
||||||
|
if (!(unionBuilder instanceof SelectBuilder)) {
|
||||||
|
throw new Error('UNION query must be a SelectBuilder instance');
|
||||||
|
}
|
||||||
|
const unionResult = unionBuilder.buildForUnion();
|
||||||
|
fullSql += ` ${union.type} (${unionResult.sql})`;
|
||||||
|
allParams.push(...unionResult.params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加ORDER BY、LIMIT、OFFSET
|
||||||
|
if (this.orderByField) {
|
||||||
|
fullSql += ` ORDER BY ${this.orderByField} ${this.orderByDirection}`;
|
||||||
|
}
|
||||||
|
if (this.limit !== null) {
|
||||||
|
fullSql += ` LIMIT ${this.limit}`;
|
||||||
|
}
|
||||||
|
if (this.offset !== null) {
|
||||||
|
fullSql += ` OFFSET ${this.offset}`;
|
||||||
|
}
|
||||||
|
console.log(fullSql,allParams);
|
||||||
|
return await this.sqdata(fullSql, allParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateBuilder extends QueryBuilder {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.table = '';
|
||||||
|
this.updateFields = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
update(table) {
|
||||||
|
this.table = table;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(field, value) {
|
||||||
|
if (value && value.increment && typeof value === 'object' ) {
|
||||||
|
this.updateFields[field] = {increment: value.increment};
|
||||||
|
} else {
|
||||||
|
this.updateFields[field] = value;
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async build() {
|
||||||
|
let sql = `UPDATE ${this.table}
|
||||||
|
SET `;
|
||||||
|
let updateClauses = Object.keys(this.updateFields).map(field => {
|
||||||
|
const value = this.updateFields[field];
|
||||||
|
if (value && value.increment && typeof value === 'object' ) {
|
||||||
|
return `${field} = ${field} + ?`;
|
||||||
|
}
|
||||||
|
return `${field} = ?`;
|
||||||
|
}).join(', ');
|
||||||
|
|
||||||
|
sql += updateClauses;
|
||||||
|
|
||||||
|
let conditionClauses = this.buildConditions();
|
||||||
|
if (conditionClauses) {
|
||||||
|
sql += ` WHERE ${conditionClauses}`;
|
||||||
|
}
|
||||||
|
// 处理参数,确保自增字段也传入增量值
|
||||||
|
const params = [
|
||||||
|
...Object.values(this.updateFields).map(value =>
|
||||||
|
(value && value.increment && typeof value === 'object' ) ? value.increment : value
|
||||||
|
),
|
||||||
|
...this.getParams()
|
||||||
|
];
|
||||||
|
return await this.sqdata(sql, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InsertBuilder extends QueryBuilder {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.table = '';
|
||||||
|
this.insertValues = [];
|
||||||
|
this.updateValues = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
insertInto(table) {
|
||||||
|
this.table = table;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 仍然保留单条记录的插入
|
||||||
|
values(values) {
|
||||||
|
if (Array.isArray(values)) {
|
||||||
|
this.insertValues = values;
|
||||||
|
} else {
|
||||||
|
this.insertValues = [values]; // 将单条记录包装成数组
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增方法,支持一次插入多条记录
|
||||||
|
valuesMultiple(records) {
|
||||||
|
if (!Array.isArray(records) || records.length === 0) {
|
||||||
|
throw new Error('Values must be a non-empty array');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保每一条记录都是对象
|
||||||
|
records.forEach(record => {
|
||||||
|
if (typeof record !== 'object') {
|
||||||
|
throw new Error('Each record must be an object');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.insertValues = records;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增 upsert 方法,支持更新或插入
|
||||||
|
upsert(values, updateFields) {
|
||||||
|
// values: 要插入的记录
|
||||||
|
// updateFields: 如果记录存在时,需要更新的字段
|
||||||
|
if (!Array.isArray(values) || values.length === 0) {
|
||||||
|
throw new Error('Values must be a non-empty array');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查每条记录是否是对象
|
||||||
|
values.forEach(record => {
|
||||||
|
if (typeof record !== 'object') {
|
||||||
|
throw new Error('Each record must be an object');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.insertValues = values;
|
||||||
|
this.updateValues = updateFields || {};
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async build() {
|
||||||
|
if (this.insertValues.length === 0) {
|
||||||
|
throw new Error("No values to insert");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取表单列名,假设所有记录有相同的字段
|
||||||
|
const columns = Object.keys(this.insertValues[0]);
|
||||||
|
|
||||||
|
// 构建 VALUES 子句,支持批量插入
|
||||||
|
const valuePlaceholders = this.insertValues.map(() =>
|
||||||
|
`(${columns.map(() => '?').join(', ')})`
|
||||||
|
).join(', ');
|
||||||
|
|
||||||
|
// 展平所有的插入值
|
||||||
|
const params = this.insertValues.flatMap(record =>
|
||||||
|
columns.map(column => record[column])
|
||||||
|
);
|
||||||
|
|
||||||
|
// 如果有 updateFields,构建 ON DUPLICATE KEY UPDATE 子句
|
||||||
|
let updateClause = '';
|
||||||
|
if (Object.keys(this.updateValues).length > 0) {
|
||||||
|
updateClause = ' ON DUPLICATE KEY UPDATE ' +
|
||||||
|
Object.keys(this.updateValues).map(field => {
|
||||||
|
return `${field} = VALUES(${field})`;
|
||||||
|
}).join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成 SQL 语句
|
||||||
|
const sql = `INSERT INTO ${this.table} (${columns.join(', ')})
|
||||||
|
VALUES ${valuePlaceholders} ${updateClause}`;
|
||||||
|
// 执行查询
|
||||||
|
return await this.sqdata(sql, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteBuilder extends QueryBuilder {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.table = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteFrom(table) {
|
||||||
|
this.table = table;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async build() {
|
||||||
|
let sql = `DELETE
|
||||||
|
FROM ${this.table}`;
|
||||||
|
let conditionClauses = this.buildConditions();
|
||||||
|
if (conditionClauses) {
|
||||||
|
sql += ` WHERE ${conditionClauses}`;
|
||||||
|
}
|
||||||
|
return await this.sqdata(sql, this.getParams());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
SelectBuilder,
|
||||||
|
UpdateBuilder,
|
||||||
|
InsertBuilder,
|
||||||
|
DeleteBuilder,
|
||||||
|
};
|
||||||
73
config/logger.js
Normal file
73
config/logger.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
const winston = require('winston');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// 创建日志目录
|
||||||
|
const logDir = path.join(__dirname, '../logs');
|
||||||
|
|
||||||
|
// 日志格式配置
|
||||||
|
const logFormat = winston.format.combine(
|
||||||
|
winston.format.timestamp({
|
||||||
|
format: 'YYYY-MM-DD HH:mm:ss'
|
||||||
|
}),
|
||||||
|
winston.format.errors({ stack: true }),
|
||||||
|
winston.format.json()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 控制台日志格式
|
||||||
|
const consoleFormat = winston.format.combine(
|
||||||
|
winston.format.colorize(),
|
||||||
|
winston.format.timestamp({
|
||||||
|
format: 'YYYY-MM-DD HH:mm:ss'
|
||||||
|
}),
|
||||||
|
winston.format.printf(({ timestamp, level, message, ...meta }) => {
|
||||||
|
return `${timestamp} [${level}]: ${message} ${Object.keys(meta).length ? JSON.stringify(meta, null, 2) : ''}`;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 创建logger实例
|
||||||
|
const logger = winston.createLogger({
|
||||||
|
level: process.env.LOG_LEVEL || 'info',
|
||||||
|
format: logFormat,
|
||||||
|
defaultMeta: { service: 'integrated-system' },
|
||||||
|
transports: [
|
||||||
|
// 错误日志文件
|
||||||
|
new winston.transports.File({
|
||||||
|
filename: path.join(logDir, 'error.log'),
|
||||||
|
level: 'error',
|
||||||
|
maxsize: 5242880, // 5MB
|
||||||
|
maxFiles: 5
|
||||||
|
}),
|
||||||
|
// 所有日志文件
|
||||||
|
new winston.transports.File({
|
||||||
|
filename: path.join(logDir, 'combined.log'),
|
||||||
|
maxsize: 5242880, // 5MB
|
||||||
|
maxFiles: 5
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// 开发环境添加控制台输出
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
logger.add(new winston.transports.Console({
|
||||||
|
format: consoleFormat
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 审计日志记录器
|
||||||
|
const auditLogger = winston.createLogger({
|
||||||
|
level: 'info',
|
||||||
|
format: logFormat,
|
||||||
|
defaultMeta: { service: 'audit' },
|
||||||
|
transports: [
|
||||||
|
new winston.transports.File({
|
||||||
|
filename: path.join(logDir, 'audit.log'),
|
||||||
|
maxsize: 5242880, // 5MB
|
||||||
|
maxFiles: 10
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
logger,
|
||||||
|
auditLogger
|
||||||
|
};
|
||||||
97
config/minio.js
Normal file
97
config/minio.js
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
const Minio = require('minio');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MinIO 配置
|
||||||
|
* 用于对象存储服务配置
|
||||||
|
*/
|
||||||
|
const minioConfig = {
|
||||||
|
// MinIO 服务器配置
|
||||||
|
endPoint: process.env.MINIO_ENDPOINT || 'localhost',
|
||||||
|
port: parseInt(process.env.MINIO_PORT) || 9000,
|
||||||
|
useSSL: process.env.MINIO_USE_SSL === 'true' || false,
|
||||||
|
accessKey: process.env.MINIO_ACCESS_KEY || 'minioadmin',
|
||||||
|
secretKey: process.env.MINIO_SECRET_KEY || 'minioadmin',
|
||||||
|
|
||||||
|
// 存储桶配置
|
||||||
|
buckets: {
|
||||||
|
uploads: process.env.MINIO_BUCKET_UPLOADS || 'uploads',
|
||||||
|
avatars: process.env.MINIO_BUCKET_AVATARS || 'avatars',
|
||||||
|
products: process.env.MINIO_BUCKET_PRODUCTS || 'products',
|
||||||
|
documents: process.env.MINIO_BUCKET_DOCUMENTS || 'documents'
|
||||||
|
},
|
||||||
|
|
||||||
|
// 文件访问配置
|
||||||
|
publicUrl: process.env.MINIO_PUBLIC_URL || `http://localhost:9000`
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 MinIO 客户端实例
|
||||||
|
*/
|
||||||
|
const createMinioClient = () => {
|
||||||
|
return new Minio.Client({
|
||||||
|
endPoint: minioConfig.endPoint,
|
||||||
|
port: minioConfig.port,
|
||||||
|
useSSL: minioConfig.useSSL,
|
||||||
|
accessKey: minioConfig.accessKey,
|
||||||
|
secretKey: minioConfig.secretKey
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化存储桶
|
||||||
|
* 确保所有需要的存储桶都存在
|
||||||
|
*/
|
||||||
|
const initializeBuckets = async () => {
|
||||||
|
const minioClient = createMinioClient();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 检查并创建存储桶
|
||||||
|
for (const [key, bucketName] of Object.entries(minioConfig.buckets)) {
|
||||||
|
const exists = await minioClient.bucketExists(bucketName);
|
||||||
|
if (!exists) {
|
||||||
|
await minioClient.makeBucket(bucketName, 'us-east-1');
|
||||||
|
console.log(`✅ 存储桶 '${bucketName}' 创建成功`);
|
||||||
|
|
||||||
|
// 设置存储桶策略为公开读取(可选)
|
||||||
|
const policy = {
|
||||||
|
Version: '2012-10-17',
|
||||||
|
Statement: [
|
||||||
|
{
|
||||||
|
Effect: 'Allow',
|
||||||
|
Principal: { AWS: ['*'] },
|
||||||
|
Action: ['s3:GetObject'],
|
||||||
|
Resource: [`arn:aws:s3:::${bucketName}/*`]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await minioClient.setBucketPolicy(bucketName, JSON.stringify(policy));
|
||||||
|
console.log(`✅ 存储桶 '${bucketName}' 策略设置成功`);
|
||||||
|
} catch (policyError) {
|
||||||
|
console.warn(`⚠️ 存储桶 '${bucketName}' 策略设置失败:`, policyError.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`✅ 存储桶 '${bucketName}' 已存在`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 初始化存储桶失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件的公开访问URL
|
||||||
|
*/
|
||||||
|
const getPublicUrl = (bucketName, objectName) => {
|
||||||
|
return `${minioConfig.publicUrl}/${bucketName}/${objectName}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
minioConfig,
|
||||||
|
createMinioClient,
|
||||||
|
initializeBuckets,
|
||||||
|
getPublicUrl
|
||||||
|
};
|
||||||
43
config/swagger.js
Normal file
43
config/swagger.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
const swaggerUi = require('swagger-ui-express');
|
||||||
|
|
||||||
|
const swaggerJsdoc = require('swagger-jsdoc');
|
||||||
|
|
||||||
|
// Swagger定义
|
||||||
|
const options = {
|
||||||
|
definition: {
|
||||||
|
openapi: '3.0.0',
|
||||||
|
info: {
|
||||||
|
title: '融豆商城 API',
|
||||||
|
version: '1.0.0',
|
||||||
|
description: '融豆商城后端API文档',
|
||||||
|
contact: {
|
||||||
|
name: '技术支持',
|
||||||
|
email: 'support@example.com'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
servers: [
|
||||||
|
{
|
||||||
|
url: '/api',
|
||||||
|
description: 'API服务器'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
components: {
|
||||||
|
securitySchemes: {
|
||||||
|
bearerAuth: {
|
||||||
|
type: 'http',
|
||||||
|
scheme: 'bearer',
|
||||||
|
bearerFormat: 'JWT'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
security: [{
|
||||||
|
bearerAuth: []
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
// API文档扫描路径
|
||||||
|
apis: ['./docs/schemas/*.js', './docs/apis/*.js', './routes/*.js', './admin/routes/*.js'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const specs = swaggerJsdoc(options);
|
||||||
|
|
||||||
|
module.exports = {specs,swaggerUi};
|
||||||
159
database.js
Normal file
159
database.js
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
const mysql = require('mysql2/promise');
|
||||||
|
|
||||||
|
// 数据库配置
|
||||||
|
const dbConfig = {
|
||||||
|
// host: process.env.DB_HOST || '114.55.111.44',
|
||||||
|
// user: process.env.DB_USER || 'maov2',
|
||||||
|
// password: process.env.DB_PASSWORD || '5fYhw8z6T62b7heS',
|
||||||
|
// database: process.env.DB_NAME || 'maov2',
|
||||||
|
host: '114.55.111.44',
|
||||||
|
user: 'test_mao',
|
||||||
|
password: 'nK2mPbWriBp25BRd',
|
||||||
|
database: 'test_mao',
|
||||||
|
charset: 'utf8mb4',
|
||||||
|
dateStrings: true,
|
||||||
|
// 连接池配置
|
||||||
|
connectionLimit: 20, // 连接池最大连接数
|
||||||
|
queueLimit: 0, // 排队等待连接的最大数量,0表示无限制
|
||||||
|
// 连接超时配置
|
||||||
|
// acquireTimeout: 60000, // 获取连接超时时间 60秒
|
||||||
|
// timeout: 60000, // 查询超时时间 60秒
|
||||||
|
// reconnect: true, // 自动重连
|
||||||
|
// 连接保活配置
|
||||||
|
// multipleStatements: true,
|
||||||
|
// 空闲连接超时配置
|
||||||
|
// idleTimeout: 300000, // 5分钟空闲超时
|
||||||
|
// maxLifetime: 1800000, // 30分钟最大生命周期
|
||||||
|
// 连接保活设置
|
||||||
|
// keepAliveInitialDelay: 0, // 开始保活探测前的延迟时间
|
||||||
|
// enableKeepAlive: true, // 启用TCP保活
|
||||||
|
// 添加类型转换配置
|
||||||
|
typeCast: function (field, next) {
|
||||||
|
if (field.type === 'TINY' && field.length === 1) {
|
||||||
|
return (field.string() === '1'); // 1 = true, 0 = false
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
},
|
||||||
|
// 确保参数正确处理
|
||||||
|
supportBigNumbers: true,
|
||||||
|
bigNumberStrings: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建数据库连接池
|
||||||
|
let pool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化数据库连接池
|
||||||
|
* @returns {Promise<mysql.Pool>} 数据库连接池
|
||||||
|
*/
|
||||||
|
async function initDB() {
|
||||||
|
if (!pool) {
|
||||||
|
try {
|
||||||
|
pool = mysql.createPool(dbConfig);
|
||||||
|
|
||||||
|
// 添加连接池事件监听
|
||||||
|
pool.on('connection', function (connection) {
|
||||||
|
console.log('新的数据库连接建立:', connection.threadId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注释掉频繁的连接获取和释放日志,避免日志过多
|
||||||
|
// pool.on('acquire', function (connection) {
|
||||||
|
// console.log('连接池获取连接:', connection.threadId);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// pool.on('release', function (connection) {
|
||||||
|
// console.log('连接池释放连接:', connection.threadId);
|
||||||
|
// });
|
||||||
|
|
||||||
|
pool.on('error', function (err) {
|
||||||
|
console.error('数据库连接池错误:', err);
|
||||||
|
if (err.code === 'PROTOCOL_CONNECTION_LOST') {
|
||||||
|
console.log('数据库连接丢失,尝试重新连接...');
|
||||||
|
} else if (err.code === 'ECONNRESET') {
|
||||||
|
console.log('数据库连接被重置,尝试重新连接...');
|
||||||
|
} else if (err.code === 'ETIMEDOUT') {
|
||||||
|
console.log('数据库连接超时,尝试重新连接...');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试连接
|
||||||
|
const connection = await pool.getConnection();
|
||||||
|
console.log('数据库连接池初始化成功');
|
||||||
|
connection.release();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('数据库连接池初始化失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取数据库连接池
|
||||||
|
* @returns {mysql.Pool} 数据库连接池
|
||||||
|
*/
|
||||||
|
function getDB() {
|
||||||
|
if (!pool) {
|
||||||
|
throw new Error('数据库连接池未初始化,请先调用 initDB()');
|
||||||
|
}
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行数据库查询(带重试机制)
|
||||||
|
* @param {string} sql SQL查询语句
|
||||||
|
* @param {Array} params 查询参数
|
||||||
|
* @param {number} retries 重试次数
|
||||||
|
* @returns {Promise<Array>} 查询结果
|
||||||
|
*/
|
||||||
|
async function executeQuery(sql, params = [], retries = 3) {
|
||||||
|
for (let i = 0; i < retries; i++) {
|
||||||
|
try {
|
||||||
|
const connection = await pool.getConnection();
|
||||||
|
try {
|
||||||
|
const [results] = await connection.execute(sql, params);
|
||||||
|
connection.release();
|
||||||
|
return results;
|
||||||
|
} catch (error) {
|
||||||
|
connection.release();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`数据库查询失败 (尝试 ${i + 1}/${retries}):`, error.message);
|
||||||
|
|
||||||
|
if (i === retries - 1) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是连接相关错误,等待后重试
|
||||||
|
if (error.code === 'PROTOCOL_CONNECTION_LOST' ||
|
||||||
|
error.code === 'ECONNRESET' ||
|
||||||
|
error.code === 'ETIMEDOUT') {
|
||||||
|
console.log(`等待 ${(i + 1) * 1000}ms 后重试...`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, (i + 1) * 1000));
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭数据库连接池
|
||||||
|
*/
|
||||||
|
async function closeDB() {
|
||||||
|
if (pool) {
|
||||||
|
await pool.end();
|
||||||
|
pool = null;
|
||||||
|
console.log('数据库连接池已关闭');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
initDB,
|
||||||
|
getDB,
|
||||||
|
closeDB,
|
||||||
|
executeQuery,
|
||||||
|
dbConfig
|
||||||
|
};
|
||||||
1559
package-lock.json
generated
Normal file
1559
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
package.json
Normal file
21
package.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "middle-office-system",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"start": "node ./bin/www"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
|
"body-parser": "^2.2.0",
|
||||||
|
"cookie-parser": "~1.4.4",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"debug": "~2.6.9",
|
||||||
|
"express": "~4.16.1",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"morgan": "~1.9.1",
|
||||||
|
"mysql2": "^3.15.0",
|
||||||
|
"swagger-jsdoc": "^6.2.8",
|
||||||
|
"swagger-ui-express": "^5.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
13
public/index.html
Normal file
13
public/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Express</title>
|
||||||
|
<link rel="stylesheet" href="/stylesheets/style.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>Express</h1>
|
||||||
|
<p>Welcome to Express</p>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
8
public/stylesheets/style.css
Normal file
8
public/stylesheets/style.css
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
body {
|
||||||
|
padding: 50px;
|
||||||
|
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #00B7FF;
|
||||||
|
}
|
||||||
346
routes/auth.js
Normal file
346
routes/auth.js
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const {getDB} = require('../database');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
|
||||||
|
router.post('/register', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const db = getDB();
|
||||||
|
await db.query('START TRANSACTION');
|
||||||
|
|
||||||
|
const {
|
||||||
|
username,
|
||||||
|
phone,
|
||||||
|
password,
|
||||||
|
city,
|
||||||
|
district_id: district,
|
||||||
|
province,
|
||||||
|
inviter = null,
|
||||||
|
captchaId,
|
||||||
|
captchaText,
|
||||||
|
smsCode, // 短信验证码
|
||||||
|
role = 'user'
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
if (!username || !phone || !password || !city || !district || !province) {
|
||||||
|
return res.status(400).json({success: false, message: '用户名、手机号、密码、城市和区域不能为空'});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!captchaId || !captchaText) {
|
||||||
|
return res.status(400).json({success: false, message: '图形验证码不能为空'});
|
||||||
|
}
|
||||||
|
const storedCaptcha = global.captchaStore.get(captchaId);
|
||||||
|
console.log(storedCaptcha);
|
||||||
|
|
||||||
|
if (!storedCaptcha) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '验证码不存在或已过期'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否过期
|
||||||
|
if (Date.now() > storedCaptcha.expires) {
|
||||||
|
global.captchaStore.delete(captchaId);
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '验证码已过期'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证验证码(不区分大小写)
|
||||||
|
const isValid = storedCaptcha.text === captchaText.toLowerCase();
|
||||||
|
|
||||||
|
// 删除已验证的验证码
|
||||||
|
global.captchaStore.delete(captchaId);
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '验证码错误'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!smsCode) {
|
||||||
|
return res.status(400).json({success: false, message: '短信验证码不能为空'});
|
||||||
|
}
|
||||||
|
// 验证短信验证码
|
||||||
|
const smsAPI = require('./sms');
|
||||||
|
const smsValid = smsAPI.verifySMSCode(phone, smsCode);
|
||||||
|
if (!smsValid) {
|
||||||
|
return res.status(400).json({success: false, message: '短信验证码错误或已过期'});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证手机号格式
|
||||||
|
const phoneRegex = /^1[3-9]\d{9}$/;
|
||||||
|
if (!phoneRegex.test(phone)) {
|
||||||
|
return res.status(400).json({success: false, message: '手机号格式不正确'});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 检查用户是否已存在
|
||||||
|
const [existingUsers] = await db.execute(
|
||||||
|
'SELECT id, payment_status FROM users WHERE username = ? OR phone = ?',
|
||||||
|
[username, phone]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingUsers.length > 0) {
|
||||||
|
return res.status(400).json({success: false, message: '用户名或手机号已存在'});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加密密码
|
||||||
|
const hashedPassword = await bcrypt.hash(password, 10);
|
||||||
|
|
||||||
|
// 创建用户(初始状态为未支付)
|
||||||
|
const [result] = await db.execute(
|
||||||
|
'INSERT INTO users (username, phone, password, role, points, audit_status, city, district_id, payment_status, province, inviter) VALUES (?, ?, ?, ?, ?, ?, ?, ?, "unpaid", ?, ?)',
|
||||||
|
[username, phone, hashedPassword, role, 0, 'pending', city, district, province, inviter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const userId = result.insertId;
|
||||||
|
await db.query('COMMIT');
|
||||||
|
|
||||||
|
// 生成JWT token(用于支付流程)
|
||||||
|
const token = jwt.sign(
|
||||||
|
{userId: userId, username, role},
|
||||||
|
JWT_SECRET,
|
||||||
|
{expiresIn: '24h'}
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
message: '用户信息创建成功,请完成支付以激活账户',
|
||||||
|
token,
|
||||||
|
user: {
|
||||||
|
id: userId,
|
||||||
|
username,
|
||||||
|
phone,
|
||||||
|
role,
|
||||||
|
points: 0,
|
||||||
|
audit_status: 'pending',
|
||||||
|
city,
|
||||||
|
district,
|
||||||
|
paymentStatus: 'unpaid'
|
||||||
|
},
|
||||||
|
needPayment: true
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
try {
|
||||||
|
// await getDB().query('ROLLBACK');
|
||||||
|
} catch (rollbackError) {
|
||||||
|
console.error('回滚错误:', rollbackError);
|
||||||
|
}
|
||||||
|
console.error('注册错误详情:', error);
|
||||||
|
console.error('错误堆栈:', error.stack);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '注册失败',
|
||||||
|
error: process.env.NODE_ENV === 'development' ? error.message : undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
router.post('/login', async (req, res) => {
|
||||||
|
console.log(123456)
|
||||||
|
try {
|
||||||
|
const db = getDB();
|
||||||
|
const {username, password, captchaId, captchaText} = req.body;
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
return res.status(400).json({success: false, message: '用户名和密码不能为空'});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!captchaId || !captchaText) {
|
||||||
|
return res.status(400).json({success: false, message: '验证码不能为空'});
|
||||||
|
}
|
||||||
|
// 获取存储的验证码
|
||||||
|
const storedCaptcha = global.captchaStore.get(captchaId);
|
||||||
|
console.log(storedCaptcha);
|
||||||
|
|
||||||
|
if (!storedCaptcha) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '验证码不存在或已过期'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否过期
|
||||||
|
if (Date.now() > storedCaptcha.expires) {
|
||||||
|
global.captchaStore.delete(captchaId);
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '验证码已过期'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证验证码(不区分大小写)
|
||||||
|
const isValid = storedCaptcha.text === captchaText.toLowerCase();
|
||||||
|
|
||||||
|
// 删除已验证的验证码
|
||||||
|
global.captchaStore.delete(captchaId);
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '验证码错误'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注意:验证码已在前端通过 /captcha/verify 接口验证过,这里不再重复验证
|
||||||
|
|
||||||
|
// 查找用户(包含支付状态)
|
||||||
|
console.log('登录尝试 - 用户名:', username);
|
||||||
|
const [users] = await db.execute(
|
||||||
|
'SELECT * FROM users WHERE username = ?',
|
||||||
|
[username]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('查找到的用户数量:', users.length);
|
||||||
|
if (users.length === 0) {
|
||||||
|
console.log('用户不存在:', username);
|
||||||
|
return res.status(401).json({success: false, message: '用户名或密码错误'});
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = users[0];
|
||||||
|
console.log('找到用户:', user.username, '密码长度:', user.password ? user.password.length : 'null');
|
||||||
|
|
||||||
|
// 验证密码
|
||||||
|
console.log('验证密码 - 输入密码:', password, '数据库密码前10位:', user.password ? user.password.substring(0, 10) : 'null');
|
||||||
|
const isValidPassword = await bcrypt.compare(password, user.password);
|
||||||
|
console.log('密码验证结果:', isValidPassword);
|
||||||
|
|
||||||
|
if (!isValidPassword) {
|
||||||
|
console.log('密码验证失败');
|
||||||
|
return res.status(401).json({success: false, message: '用户名或密码错误'});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查支付状态(管理员除外)
|
||||||
|
if (user.role !== 'admin' && user.payment_status === 'unpaid') {
|
||||||
|
const token = jwt.sign(
|
||||||
|
{userId: user.id, username: user.username, role: user.role},
|
||||||
|
JWT_SECRET,
|
||||||
|
{expiresIn: '5m'}
|
||||||
|
);
|
||||||
|
return res.status(200).json({
|
||||||
|
success: false,
|
||||||
|
message: '您的账户尚未激活,请完成支付后再登录',
|
||||||
|
needPayment: true,
|
||||||
|
user: user[0],
|
||||||
|
token
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户审核状态(管理员除外,只阻止被拒绝的用户)
|
||||||
|
if (user.role !== 'admin' && user.audit_status === 'rejected') {
|
||||||
|
return res.status(403).json({success: false, message: '您的账户审核未通过,请联系管理员'});
|
||||||
|
}
|
||||||
|
// 待审核用户可以正常登录使用系统,但匹配功能会有限制
|
||||||
|
|
||||||
|
// 生成JWT token
|
||||||
|
const token = jwt.sign(
|
||||||
|
{userId: user.id, username: user.username, role: user.role},
|
||||||
|
JWT_SECRET,
|
||||||
|
{expiresIn: '24h'}
|
||||||
|
);
|
||||||
|
const [is_distribution] = await db.execute(`
|
||||||
|
SELECT *
|
||||||
|
FROM distribution
|
||||||
|
WHERE user_id = ?`, [user.id]);
|
||||||
|
user.distribution = is_distribution.length > 0 ? true : false;
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: '登录成功',
|
||||||
|
token,
|
||||||
|
user
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('登录错误:', error);
|
||||||
|
res.status(500).json({success: false, message: '登录失败'});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 验证token中间件
|
||||||
|
const authenticateToken = (req, res, next) => {
|
||||||
|
const authHeader = req.headers['authorization'];
|
||||||
|
const token = authHeader && authHeader.split(' ')[1];
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return res.status(401).json({success: false, message: '访问令牌缺失'});
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt.verify(token, JWT_SECRET, (err, user) => {
|
||||||
|
if (err) {
|
||||||
|
return res.status(403).json({success: false, message: '访问令牌无效'});
|
||||||
|
}
|
||||||
|
req.user = user;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取当前用户信息
|
||||||
|
router.get('/me', authenticateToken, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const db = getDB();
|
||||||
|
const [users] = await db.execute(
|
||||||
|
'SELECT id, username, role, avatar, points, created_at FROM users WHERE id = ?',
|
||||||
|
[req.user.userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (users.length === 0) {
|
||||||
|
return res.status(404).json({success: false, message: '用户不存在'});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({success: true, user: users[0]});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户信息错误:', error);
|
||||||
|
res.status(500).json({success: false, message: '获取用户信息失败'});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 修改密码
|
||||||
|
router.put('/change-password', authenticateToken, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const db = getDB();
|
||||||
|
const {currentPassword, newPassword} = req.body;
|
||||||
|
|
||||||
|
if (!currentPassword || !newPassword) {
|
||||||
|
return res.status(400).json({success: false, message: '旧密码和新密码不能为空'});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户当前密码
|
||||||
|
const [users] = await db.execute(
|
||||||
|
'SELECT password FROM users WHERE id = ?',
|
||||||
|
[req.user.userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (users.length === 0) {
|
||||||
|
return res.status(404).json({success: false, message: '用户不存在'});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证旧密码
|
||||||
|
const isValidPassword = await bcrypt.compare(currentPassword, users[0].password);
|
||||||
|
|
||||||
|
if (!isValidPassword) {
|
||||||
|
return res.status(400).json({success: false, message: '旧密码错误'});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加密新密码
|
||||||
|
const hashedNewPassword = await bcrypt.hash(newPassword, 10);
|
||||||
|
|
||||||
|
// 更新密码
|
||||||
|
await db.execute(
|
||||||
|
'UPDATE users SET password = ? WHERE id = ?',
|
||||||
|
[hashedNewPassword, req.user.userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({success: true, message: '密码修改成功'});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('修改密码错误:', error);
|
||||||
|
res.status(500).json({success: false, message: '修改密码失败'});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
module.exports.authenticateToken = authenticateToken;
|
||||||
220
routes/captcha.js
Normal file
220
routes/captcha.js
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 内存存储验证码(生产环境建议使用Redis)
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成随机验证码字符串
|
||||||
|
* @param {number} length 验证码长度
|
||||||
|
* @returns {string} 验证码字符串
|
||||||
|
*/
|
||||||
|
function generateCaptchaText(length = 4) {
|
||||||
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成SVG验证码图片
|
||||||
|
* @param {string} text 验证码文本
|
||||||
|
* @returns {string} SVG字符串
|
||||||
|
*/
|
||||||
|
function generateCaptchaSVG(text) {
|
||||||
|
const width = 120;
|
||||||
|
const height = 40;
|
||||||
|
const fontSize = 18;
|
||||||
|
|
||||||
|
// 生成随机颜色
|
||||||
|
const getRandomColor = () => {
|
||||||
|
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8'];
|
||||||
|
return colors[Math.floor(Math.random() * colors.length)];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成干扰线
|
||||||
|
const generateNoise = () => {
|
||||||
|
let noise = '';
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const x1 = Math.random() * width;
|
||||||
|
const y1 = Math.random() * height;
|
||||||
|
const x2 = Math.random() * width;
|
||||||
|
const y2 = Math.random() * height;
|
||||||
|
noise += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${getRandomColor()}" stroke-width="1" opacity="0.3"/>`;
|
||||||
|
}
|
||||||
|
return noise;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成干扰点
|
||||||
|
const generateDots = () => {
|
||||||
|
let dots = '';
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
const x = Math.random() * width;
|
||||||
|
const y = Math.random() * height;
|
||||||
|
const r = Math.random() * 2 + 1;
|
||||||
|
dots += `<circle cx="${x}" cy="${y}" r="${r}" fill="${getRandomColor()}" opacity="0.4"/>`;
|
||||||
|
}
|
||||||
|
return dots;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成文字
|
||||||
|
let textElements = '';
|
||||||
|
const charWidth = width / text.length;
|
||||||
|
|
||||||
|
for (let i = 0; i < text.length; i++) {
|
||||||
|
const char = text[i];
|
||||||
|
const x = charWidth * i + charWidth / 2;
|
||||||
|
const y = height / 2 + fontSize / 3;
|
||||||
|
const rotation = (Math.random() - 0.5) * 30; // 随机旋转角度
|
||||||
|
const color = getRandomColor();
|
||||||
|
|
||||||
|
textElements += `
|
||||||
|
<text x="${x}" y="${y}"
|
||||||
|
font-family="Arial, sans-serif"
|
||||||
|
font-size="${fontSize}"
|
||||||
|
font-weight="bold"
|
||||||
|
fill="${color}"
|
||||||
|
text-anchor="middle"
|
||||||
|
transform="rotate(${rotation} ${x} ${y})">
|
||||||
|
${char}
|
||||||
|
</text>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const svg = `
|
||||||
|
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#f8f9fa;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#e9ecef;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="${width}" height="${height}" fill="url(#bg)" stroke="#dee2e6" stroke-width="1"/>
|
||||||
|
${generateNoise()}
|
||||||
|
${generateDots()}
|
||||||
|
${textElements}
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
return svg;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
router.get('/generate', (req, res) => {
|
||||||
|
try {
|
||||||
|
// 生成验证码文本
|
||||||
|
const captchaText = generateCaptchaText();
|
||||||
|
|
||||||
|
// 生成唯一ID
|
||||||
|
const captchaId = crypto.randomUUID();
|
||||||
|
|
||||||
|
// 存储验证码(5分钟过期)
|
||||||
|
global.captchaStore.set(captchaId, {
|
||||||
|
text: captchaText.toLowerCase(), // 存储小写用于比较
|
||||||
|
expires: Date.now() + 5 * 60 * 1000 // 5分钟过期
|
||||||
|
});
|
||||||
|
|
||||||
|
// 生成SVG图片
|
||||||
|
const svgImage = generateCaptchaSVG(captchaText);
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
captchaId,
|
||||||
|
image: `data:image/svg+xml;base64,${Buffer.from(svgImage).toString('base64')}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('生成验证码失败:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '生成验证码失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
router.post('/verify', (req, res) => {
|
||||||
|
try {
|
||||||
|
const { captchaId, captchaText } = req.body;
|
||||||
|
|
||||||
|
if (!captchaId || !captchaText) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '验证码ID和验证码不能为空'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取存储的验证码
|
||||||
|
const storedCaptcha = global.captchaStore.get(captchaId);
|
||||||
|
|
||||||
|
if (!storedCaptcha) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '验证码不存在或已过期'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否过期
|
||||||
|
if (Date.now() > storedCaptcha.expires) {
|
||||||
|
global.captchaStore.delete(captchaId);
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '验证码已过期'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证验证码(不区分大小写)
|
||||||
|
const isValid = storedCaptcha.text === captchaText.toLowerCase();
|
||||||
|
|
||||||
|
// 验证后删除验证码(无论成功失败)
|
||||||
|
global.captchaStore.delete(captchaId);
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: '验证码验证成功'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '验证码错误'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('验证验证码失败:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '验证验证码失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 清理过期验证码的定时任务
|
||||||
|
setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
for (const [id, captcha] of global.captchaStore.entries()) {
|
||||||
|
if (now > captcha.expires) {
|
||||||
|
global.captchaStore.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 60 * 1000); // 每分钟清理一次
|
||||||
|
|
||||||
|
// 导出验证函数供其他模块使用
|
||||||
|
module.exports = router;
|
||||||
|
module.exports.verifyCaptcha = (captchaId, captchaText) => {
|
||||||
|
const captcha = global.captchaStore.get(captchaId);
|
||||||
|
if (!captcha) {
|
||||||
|
return false; // 验证码不存在或已过期
|
||||||
|
}
|
||||||
|
|
||||||
|
if (captcha.text.toLowerCase() !== captchaText.toLowerCase()) {
|
||||||
|
return false; // 验证码错误
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证成功后删除验证码(一次性使用)
|
||||||
|
global.captchaStore.delete(captchaId);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
77
routes/common.js
Normal file
77
routes/common.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
const express = require('express')
|
||||||
|
const router = express.Router()
|
||||||
|
const {getDB} = require('../database')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/common/provinces:
|
||||||
|
* get:
|
||||||
|
* summary: 获取省份列表
|
||||||
|
* description: 获取省份列表
|
||||||
|
* responses:
|
||||||
|
* '200':
|
||||||
|
* description: 成功获取分类列表
|
||||||
|
*/
|
||||||
|
router.get('/provinces', async (req, res) => {
|
||||||
|
try {
|
||||||
|
// 按level分组数据
|
||||||
|
const regionsByLevel = {
|
||||||
|
1: [], // 省份
|
||||||
|
2: [], // 城市
|
||||||
|
3: [] // 区县
|
||||||
|
};
|
||||||
|
if (!global.provinces) {
|
||||||
|
// 一次性获取所有区域数据(省、市、区县)
|
||||||
|
const [allRegions] = await getDB().execute(
|
||||||
|
`SELECT code, name as label, level, parent_code
|
||||||
|
FROM china_regions
|
||||||
|
WHERE level <= 3
|
||||||
|
ORDER BY level, code`
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
// 创建code到region的映射,便于快速查找
|
||||||
|
const regionMap = {};
|
||||||
|
|
||||||
|
// 分组并建立映射
|
||||||
|
allRegions.forEach(region => {
|
||||||
|
region.children = []; // 初始化children数组
|
||||||
|
regionsByLevel[region.level].push(region);
|
||||||
|
regionMap[region.code] = region;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 构建层级关系:先处理区县到城市的关系
|
||||||
|
regionsByLevel[3].forEach(district => {
|
||||||
|
const parentCity = regionMap[district.parent_code];
|
||||||
|
if (parentCity) {
|
||||||
|
parentCity.children.push(district);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 再处理城市到省份的关系
|
||||||
|
regionsByLevel[2].forEach(city => {
|
||||||
|
const parentProvince = regionMap[city.parent_code];
|
||||||
|
if (parentProvince) {
|
||||||
|
parentProvince.children.push(city);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
global.provinces = regionsByLevel[1];
|
||||||
|
} else {
|
||||||
|
console.log('1111')
|
||||||
|
regionsByLevel[1] = global.provinces;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 返回省份数据(已包含完整的层级结构)
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: regionsByLevel[1]
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取省份列表错误:', error);
|
||||||
|
res.status(500).json({message: '获取省份列表失败'});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = router
|
||||||
6
routes/index.js
Normal file
6
routes/index.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
var express = require('express');
|
||||||
|
var router = express.Router();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
9
routes/users.js
Normal file
9
routes/users.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
var express = require('express');
|
||||||
|
var router = express.Router();
|
||||||
|
|
||||||
|
/* GET users listing. */
|
||||||
|
router.get('/',function(req, res, next) {
|
||||||
|
res.send('respond with a resource');
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
Reference in New Issue
Block a user