2025-09-18

商城接口
This commit is contained in:
2025-09-19 16:46:00 +08:00
parent 3710f23dc3
commit 1b55af47de
9 changed files with 1557 additions and 4 deletions

28
app.js
View File

@@ -11,6 +11,9 @@ var commonRouter = require('./routes/common');
var captchaRouter = require('./routes/captcha');
var authRouter = require('./routes/auth');
var smsRouter = require('./routes/sms');
var mallRouter = require('./routes/mall');
var programRouter = require('./routes/program');
var uploadRouter = require('./routes/upload')
// endregion
var app = express();
@@ -34,6 +37,28 @@ app.use(cors({
app.use(bodyParser.json({ limit: '10mb' }));
app.use(bodyParser.urlencoded({ extended: true, limit: '10mb' }));
// 静态文件服务 - 必须在API路由之前
// 为uploads路径配置CORS和静态文件服务
app.use('/uploads', express.static(path.join(__dirname, 'uploads'), {
setHeaders: (res, filePath) => {
// 设置CORS头部
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
// 设置缓存和内容类型
if (filePath.endsWith('.jpg') || filePath.endsWith('.jpeg')) {
res.setHeader('Content-Type', 'image/jpeg');
} else if (filePath.endsWith('.png')) {
res.setHeader('Content-Type', 'image/png');
} else if (filePath.endsWith('.gif')) {
res.setHeader('Content-Type', 'image/gif');
} else if (filePath.endsWith('.webp')) {
res.setHeader('Content-Type', 'image/webp');
}
res.setHeader('Cache-Control', 'public, max-age=86400'); // 1天缓存
}
}));
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs)); // swagger
@@ -44,10 +69,13 @@ app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
// region api配置
app.use('/api/upload', uploadRouter)
app.use('/api/common', commonRouter)
app.use('/api/captcha', captchaRouter)
app.use('/api/auth', authRouter)
app.use('/api/sms', smsRouter)
app.use('/api/mall', mallRouter)
app.use('/api/program', programRouter)
// endregion
module.exports = app;

110
middleware/auth.js Normal file
View File

@@ -0,0 +1,110 @@
const jwt = require('jsonwebtoken');
const { getDB } = require('../database');
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key'; // 在生产环境中应该使用环境变量
/**
* 用户认证中间件
* 验证JWT令牌并检查用户状态包括是否被拉黑
*/
const auth = async (req, res, next) => {
try {
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ success: false, message: '未提供认证令牌' });
}
const decoded = jwt.verify(token, JWT_SECRET);
const db = getDB();
const [users] = await db.execute('SELECT * FROM users WHERE id = ?', [decoded.userId]);
if (users.length === 0) {
return res.status(401).json({ success: false, message: '用户不存在' });
}
const user = users[0];
// 检查用户是否被拉黑
if (user.is_blacklisted) {
return res.status(403).json({
success: false,
message: '账户已被拉黑,请联系管理员',
code: 'USER_BLACKLISTED'
});
}
// 检查支付状态(管理员除外)
if (user.role !== 'admin' && user.payment_status === 'unpaid') {
return res.status(403).json({
success: false,
message: '您的账户尚未激活,请完成支付后再使用',
code: 'PAYMENT_REQUIRED',
needPayment: true,
userId: user.id
});
}
req.user = user;
next();
} catch (error) {
res.status(401).json({ success: false, message: '无效的认证令牌' });
}
};
// 管理员认证中间件
const adminAuth = (req, res, next) => {
if (req.user.role !== 'admin') {
return res.status(403).json({ success: false, message: '需要管理员权限' });
}
next();
};
/**
* 支付认证中间件
* 只验证JWT令牌和用户状态不检查支付状态
* 用于支付相关接口,允许未支付用户创建支付订单
*/
const paymentAuth = async (req, res, next) => {
try {
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ success: false, message: '未提供认证令牌' });
}
const decoded = jwt.verify(token, JWT_SECRET);
const db = getDB();
const [users] = await db.execute('SELECT * FROM users WHERE id = ?', [decoded.userId]);
if (users.length === 0) {
return res.status(401).json({ success: false, message: '用户不存在' });
}
const user = users[0];
// 检查用户是否被拉黑
if (user.is_blacklisted) {
return res.status(403).json({
success: false,
message: '账户已被拉黑,请联系管理员',
code: 'USER_BLACKLISTED'
});
}
// 注意:这里不检查支付状态,允许未支付用户创建支付订单
req.user = user;
next();
} catch (error) {
console.error('支付认证失败:', error);
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({ success: false, message: '无效的认证令牌' });
}
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ success: false, message: '认证令牌已过期' });
}
return res.status(500).json({ success: false, message: '认证失败' });
}
};
module.exports = { auth, adminAuth, paymentAuth, JWT_SECRET };

585
package-lock.json generated
View File

@@ -15,9 +15,12 @@
"cookie-parser": "~1.4.4",
"cors": "^2.8.5",
"debug": "~2.6.9",
"dotenv": "^17.2.1",
"express": "~4.16.1",
"jsonwebtoken": "^9.0.2",
"minio": "^8.0.5",
"morgan": "~1.9.1",
"multer": "^1.4.5-lts.1",
"mysql2": "^3.15.0",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1"
@@ -284,6 +287,12 @@
"@types/node": "*"
}
},
"node_modules/@zxing/text-encoding": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
"integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==",
"optional": true
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -296,6 +305,11 @@
"node": ">= 0.6"
}
},
"node_modules/append-field": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw=="
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -306,6 +320,25 @@
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
},
"node_modules/async": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
"integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
"dependencies": {
"possible-typed-array-names": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/aws-ssl-profiles": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
@@ -335,6 +368,27 @@
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ=="
},
"node_modules/block-stream2": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/block-stream2/-/block-stream2-2.1.0.tgz",
"integrity": "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==",
"dependencies": {
"readable-stream": "^3.4.0"
}
},
"node_modules/block-stream2/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==",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/body-parser": {
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
@@ -442,11 +496,40 @@
"concat-map": "0.0.1"
}
},
"node_modules/browser-or-node": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-2.1.1.tgz",
"integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg=="
},
"node_modules/buffer-crc32": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz",
"integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -455,6 +538,23 @@
"node": ">= 0.8"
}
},
"node_modules/call-bind": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.0",
"es-define-property": "^1.0.0",
"get-intrinsic": "^1.2.4",
"set-function-length": "^1.2.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@@ -500,6 +600,20 @@
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"node_modules/concat-stream": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
"engines": [
"node >= 0.8"
],
"dependencies": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^2.2.2",
"typedarray": "^0.0.6"
}
},
"node_modules/content-disposition": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
@@ -541,6 +655,11 @@
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
@@ -561,6 +680,30 @@
"ms": "2.0.0"
}
},
"node_modules/decode-uri-component": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
"integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
"engines": {
"node": ">=0.10"
}
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@@ -593,6 +736,17 @@
"node": ">=6.0.0"
}
},
"node_modules/dotenv": {
"version": "17.2.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz",
"integrity": "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -675,6 +829,11 @@
"node": ">= 0.6"
}
},
"node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
},
"node_modules/express": {
"version": "4.16.4",
"resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz",
@@ -776,6 +935,31 @@
"node": ">= 0.8"
}
},
"node_modules/fast-xml-parser": {
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz",
"integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"dependencies": {
"strnum": "^1.1.1"
},
"bin": {
"fxparser": "src/cli/cli.js"
}
},
"node_modules/filter-obj": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz",
"integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/finalhandler": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
@@ -793,6 +977,20 @@
"node": ">= 0.8"
}
},
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
"integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
"dependencies": {
"is-callable": "^1.2.7"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -896,6 +1094,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dependencies": {
"es-define-property": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@@ -907,6 +1116,20 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -1009,11 +1232,90 @@
"node": ">= 0.10"
}
},
"node_modules/is-arguments": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
"integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
"dependencies": {
"call-bound": "^1.0.2",
"has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-callable": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-generator-function": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz",
"integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==",
"dependencies": {
"call-bound": "^1.0.3",
"get-proto": "^1.0.0",
"has-tostringtag": "^1.0.2",
"safe-regex-test": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-property": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="
},
"node_modules/is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
"dependencies": {
"call-bound": "^1.0.2",
"gopd": "^1.2.0",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-typed-array": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
"integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
"dependencies": {
"which-typed-array": "^1.1.16"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@@ -1237,6 +1539,57 @@
"node": "*"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/minio": {
"version": "8.0.6",
"resolved": "https://registry.npmjs.org/minio/-/minio-8.0.6.tgz",
"integrity": "sha512-sOeh2/b/XprRmEtYsnNRFtOqNRTPDvYtMWh+spWlfsuCV/+IdxNeKVUMKLqI7b5Dr07ZqCPuaRGU/rB9pZYVdQ==",
"dependencies": {
"async": "^3.2.4",
"block-stream2": "^2.1.0",
"browser-or-node": "^2.1.1",
"buffer-crc32": "^1.0.0",
"eventemitter3": "^5.0.1",
"fast-xml-parser": "^4.4.1",
"ipaddr.js": "^2.0.1",
"lodash": "^4.17.21",
"mime-types": "^2.1.35",
"query-string": "^7.1.3",
"stream-json": "^1.8.0",
"through2": "^4.0.2",
"web-encoding": "^1.1.5",
"xml2js": "^0.5.0 || ^0.6.2"
},
"engines": {
"node": "^16 || ^18 || >=20"
}
},
"node_modules/minio/node_modules/ipaddr.js": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
"integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==",
"engines": {
"node": ">= 10"
}
},
"node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/moment": {
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
@@ -1276,6 +1629,24 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/multer": {
"version": "1.4.5-lts.2",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz",
"integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==",
"deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.",
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^1.0.0",
"concat-stream": "^1.5.2",
"mkdirp": "^0.5.4",
"object-assign": "^4.1.1",
"type-is": "^1.6.4",
"xtend": "^4.0.0"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/mysql2": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.0.tgz",
@@ -1402,6 +1773,19 @@
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
},
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
"integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -1422,6 +1806,23 @@
"node": ">=0.6"
}
},
"node_modules/query-string": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz",
"integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==",
"dependencies": {
"decode-uri-component": "^0.2.2",
"filter-obj": "^1.1.0",
"split-on-first": "^1.0.0",
"strict-uri-encode": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -1485,11 +1886,41 @@
"node": ">= 0.8"
}
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"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=="
},
"node_modules/safe-regex-test": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
"integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"is-regex": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -1553,6 +1984,22 @@
"node": ">= 0.8.0"
}
},
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"dependencies": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/setprototypeof": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
@@ -1631,6 +2078,14 @@
"resolved": "https://registry.npmjs.org/sm3/-/sm3-1.0.3.tgz",
"integrity": "sha512-KyFkIfr8QBlFG3uc3NaljaXdYcsbRy1KrSfc4tsQV8jW68jAktGeOcifu530Vx/5LC+PULHT0Rv8LiI8Gw+c1g=="
},
"node_modules/split-on-first": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
"integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==",
"engines": {
"node": ">=6"
}
},
"node_modules/sqlstring": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
@@ -1647,6 +2102,54 @@
"node": ">= 0.6"
}
},
"node_modules/stream-chain": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz",
"integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA=="
},
"node_modules/stream-json": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz",
"integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==",
"dependencies": {
"stream-chain": "^2.2.5"
}
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/strict-uri-encode": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
"integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==",
"engines": {
"node": ">=4"
}
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/strnum": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz",
"integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
]
},
"node_modules/swagger-jsdoc": {
"version": "6.2.8",
"resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz",
@@ -1699,6 +2202,27 @@
"express": ">=4.0.0 || >=5.0.0-beta"
}
},
"node_modules/through2": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz",
"integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==",
"dependencies": {
"readable-stream": "3"
}
},
"node_modules/through2/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==",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@@ -1719,6 +2243,11 @@
"node": ">= 0.6"
}
},
"node_modules/typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
@@ -1732,6 +2261,23 @@
"node": ">= 0.8"
}
},
"node_modules/util": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
"integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
"dependencies": {
"inherits": "^2.0.3",
"is-arguments": "^1.0.4",
"is-generator-function": "^1.0.7",
"is-typed-array": "^1.1.3",
"which-typed-array": "^1.1.2"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -1756,6 +2302,37 @@
"node": ">= 0.8"
}
},
"node_modules/web-encoding": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/web-encoding/-/web-encoding-1.1.5.tgz",
"integrity": "sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==",
"dependencies": {
"util": "^0.12.3"
},
"optionalDependencies": {
"@zxing/text-encoding": "0.9.0"
}
},
"node_modules/which-typed-array": {
"version": "1.1.19",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
"integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
"dependencies": {
"available-typed-arrays": "^1.0.7",
"call-bind": "^1.0.8",
"call-bound": "^1.0.4",
"for-each": "^0.3.5",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -1781,6 +2358,14 @@
"node": ">=4.0"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"engines": {
"node": ">=0.4"
}
},
"node_modules/yaml": {
"version": "2.0.0-1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz",

View File

@@ -8,10 +8,13 @@
"dependencies": {
"@alicloud/dysmsapi20170525": "^4.1.1",
"@alicloud/openapi-client": "^0.4.15",
"dotenv": "^17.2.1",
"bcryptjs": "^2.4.3",
"body-parser": "^1.20.2",
"minio": "^8.0.5",
"cookie-parser": "~1.4.4",
"cors": "^2.8.5",
"multer": "^1.4.5-lts.1",
"debug": "~2.6.9",
"express": "~4.16.1",
"jsonwebtoken": "^9.0.2",

View File

@@ -2,15 +2,36 @@ const express = require('express')
const router = express.Router()
const {getDB} = require('../database')
/**
* @swagger
* tags:
* name: Common
* description: 公共接口
*/
/**
* @swagger
* /api/common/provinces:
* get:
* summary: 获取省列表
* description: 获取省份列表
* get:
* summary: 获取省市区列表
* tags: [Common]
* responses:
* '200':
* 200:
* description: 成功获取分类列表
* content:
* application/json:
* schema:
* type: array
* items:
* properties:
* code:
* type: string
* description: 区域编码
* example: 110000
* label:
* type: string
* description: 区域名称
* example: 北京市
*/
router.get('/provinces', async (req, res) => {
try {

73
routes/mall.js Normal file
View File

@@ -0,0 +1,73 @@
const express = require('express');
const router = express.Router();
const {getDB} = require('../database');
/**
* @swagger
* tags:
* name: Mall
* description: 商城模块
*/
router.get('/', async (req, res) => {
try {
const conn = await getDB()
const {
page = '1',
size = '10',
category = '',
keyword = '',
} = req.query;
const pageNum = Math.max(1, parseInt(page) || 1);
const pageSize = Math.max(1, Math.min(100, parseInt(size) || 10)); // 限制最大100条
const offset = Math.max(0, (pageNum - 1) * pageSize);
console.log("分页参数:", {pageNum, pageSize, offset, category, keyword})
let whereClause = " and status = 'active'";
if (category) {
whereClause += ` and category = '${category}'`;
}
if (keyword) {
whereClause += ` and name like '%${keyword}%'`;
}
// 获取商品总数
const [countResult] = await conn.execute(`
select count(1) as total
from products
where '1 = 1' ${whereClause}
`)
const total = countResult[0].total;
// 获取商品列表
const queryMallListSql = `
select *
from products
where '1 = 1' ${whereClause}
order by created_at desc
limit ${pageSize} offset ${offset}
`
const [products] = await conn.execute(queryMallListSql)
res.json({
success: true,
data: {
products,
pagination: {
page: pageNum,
size: pageSize,
total,
pages: Math.ceil(total / pageSize)
}
}
})
} catch (error) {
console.error('获取商品列表错误:', error);
res.status(500).json({success: false, message: '获取商品列表失败'});
}
})
module.exports = router

19
routes/program.js Normal file
View File

@@ -0,0 +1,19 @@
const express = require('express')
const router = express.Router()
const db = require('../database')
/**
* @swagger
* tags:
* name: Program
* description: 项目模块
*/
router.get('/', async (req, res) => {
})
module.exports = router

421
routes/upload.js Normal file
View File

@@ -0,0 +1,421 @@
const express = require('express');
const multer = require('multer');
const path = require('path');
const { auth } = require('../middleware/auth');
const { authenticateToken } = require('./auth');
const minioService = require('../services/minioService');
const { initializeBuckets } = require('../config/minio');
const router = express.Router();
// 初始化MinIO存储桶
// initializeBuckets().catch(console.error);
/**
* @swagger
* tags:
* name: Upload
* description: 文件上传API
*/
// 配置multer内存存储用于MinIO上传
const storage = multer.memoryStorage();
// 文件过滤器 - 支持图片和视频
const fileFilter = (req, file, cb) => {
// 允许图片和视频文件
if (file.mimetype.startsWith('image/') || file.mimetype.startsWith('video/')) {
cb(null, true);
} else {
cb(new Error('只能上传图片或视频文件'), false);
}
};
// 单文件上传配置
const upload = multer({
storage: storage,
fileFilter: fileFilter,
limits: {
fileSize: 5 * 1024 * 1024, // 5MB
files: 1 // 一次只能上传一个文件
}
});
// 多文件上传配置
const multiUpload = multer({
storage: storage,
fileFilter: fileFilter,
limits: {
fileSize: 10 * 1024 * 1024, // 10MB (视频文件更大)
files: 10 // 最多10个文件
}
});
/**
* @swagger
* /upload/image:
* post:
* summary: 上传图片
* tags: [Upload]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* multipart/form-data:
* schema:
* type: object
* properties:
* file:
* type: string
* format: binary
* description: 要上传的图片文件
* type:
* type: string
* enum: [avatar, product, document]
* default: document
* description: 上传文件类型
* responses:
* 200:
* description: 图片上传成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* url:
* type: string
* description: 上传后的文件URL
* filename:
* type: string
* description: 上传后的文件名
* 400:
* description: 请求参数错误
* 401:
* description: 未授权
* 500:
* description: 服务器错误
*/
router.post('/image', authenticateToken, (req, res) => {
upload.single('file')(req, res, async (err) => {
if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({
success: false,
message: '文件大小不能超过 5MB'
});
}
if (err.code === 'LIMIT_FILE_COUNT') {
return res.status(400).json({
success: false,
message: '一次只能上传一个文件'
});
}
return res.status(400).json({
success: false,
message: '文件上传失败:' + err.message
});
} else if (err) {
return res.status(400).json({
success: false,
message: err.message
});
}
if (!req.file) {
return res.status(400).json({
success: false,
message: '请选择要上传的文件'
});
}
try {
// 使用MinIO服务上传文件
const type = req.body.type || 'document';
const result = await minioService.uploadFile(
req.file.buffer,
req.file.originalname,
req.file.mimetype,
type
);
res.json({
success: true,
message: '文件上传成功',
data: result.data
});
} catch (error) {
console.error('文件上传到MinIO失败:', error);
res.status(500).json({
success: false,
message: error.message || '文件上传失败'
});
}
});
});
/**
* @swagger
* /upload:
* post:
* summary: 多文件上传接口 (支持MediaUpload组件)
* tags: [Upload]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* multipart/form-data:
* schema:
* type: object
* properties:
* files:
* type: array
* items:
* type: string
* format: binary
* description: 要上传的文件列表
* type:
* type: string
* enum: [avatar, product, document]
* default: document
* description: 上传文件类型
* responses:
* 200:
* description: 文件上传成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* message:
* type: string
* example: 文件上传成功
* data:
* type: array
* items:
* type: object
* properties:
* filename:
* type: string
* originalname:
* type: string
* mimetype:
* type: string
* size:
* type: integer
* path:
* type: string
* url:
* type: string
* 400:
* description: 请求参数错误
* 401:
* description: 未授权
* 500:
* description: 服务器错误
* @access Private
*/
router.post('/', authenticateToken, (req, res) => {
multiUpload.array('file', 10)(req, res, async (err) => {
if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({
success: false,
message: '文件大小不能超过 10MB'
});
}
if (err.code === 'LIMIT_FILE_COUNT') {
return res.status(400).json({
success: false,
message: '一次最多只能上传10个文件'
});
}
return res.status(400).json({
success: false,
message: '文件上传失败:' + err.message
});
} else if (err) {
return res.status(400).json({
success: false,
message: err.message
});
}
if (!req.files || req.files.length === 0) {
return res.status(400).json({
success: false,
message: '请选择要上传的文件'
});
}
try {
// 使用MinIO服务上传多个文件
const type = req.body.type || 'document';
const files = req.files.map(file => ({
buffer: file.buffer,
originalName: file.originalname,
mimeType: file.mimetype
}));
const result = await minioService.uploadMultipleFiles(files, type);
// 如果只上传了一个文件,返回单文件格式以保持兼容性
if (result.data.files.length === 1) {
result.data.files.forEach(element => {
element.path = '/' + element.path
});
res.json({
success: true,
message: '文件上传成功',
data: {
...result.data.files[0],
urls: result.data.urls // 同时提供urls数组格式
}
});
} else {
// 多文件返回数组格式
res.json({
success: true,
message: `成功上传${result.data.files.length}个文件`,
data: result.data
});
}
} catch (error) {
console.error('文件上传到MinIO失败:', error);
res.status(500).json({
success: false,
message: error.message || '文件上传失败'
});
}
});
});
/**
* @swagger
* /upload/single:
* post:
* summary: 单文件上传接口(兼容性接口)
* tags: [Upload]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* multipart/form-data:
* schema:
* type: object
* properties:
* file:
* type: string
* format: binary
* description: 要上传的文件
* type:
* type: string
* enum: [avatar, product, document]
* default: document
* description: 上传文件类型
* responses:
* 200:
* description: 文件上传成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* message:
* type: string
* example: 文件上传成功
* url:
* type: string
* description: 上传后的文件URL
* filename:
* type: string
* description: 上传后的文件名
* originalname:
* type: string
* description: 原始文件名
* size:
* type: integer
* description: 文件大小
* 400:
* description: 请求参数错误
* 401:
* description: 未授权
* 500:
* description: 服务器错误
*/
router.post('/single', auth, (req, res) => {
upload.single('file')(req, res, async (err) => {
if (err instanceof multer.MulterError) {
return res.status(400).json({
success: false,
message: '文件上传失败:' + err.message
});
} else if (err) {
return res.status(400).json({
success: false,
message: err.message
});
}
if (!req.file) {
return res.status(400).json({ success: false, message: '没有上传文件' });
}
try {
// 使用MinIO服务上传文件
const type = req.body.type || 'document';
const result = await minioService.uploadFile(
req.file.buffer,
req.file.originalname,
req.file.mimetype,
type
);
res.json({
success: true,
message: '文件上传成功',
url: result.data.url,
filename: result.data.filename,
originalname: result.data.originalname,
size: result.data.size
});
} catch (error) {
console.error('文件上传到MinIO失败:', error);
res.status(500).json({
success: false,
message: error.message || '文件上传失败'
});
}
});
});
// 错误处理中间件
router.use((error, req, res, next) => {
if (error instanceof multer.MulterError) {
if (error.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({ success: false, message: '文件大小不能超过10MB' });
}
if (error.code === 'LIMIT_FILE_COUNT') {
return res.status(400).json({ success: false, message: '一次最多只能上传10个文件' });
}
}
if (error.message === '只能上传图片或视频文件') {
return res.status(400).json({ success: false, message: error.message });
}
res.status(500).json({ success: false, message: '上传失败' });
});
module.exports = router;

293
services/minioService.js Normal file
View File

@@ -0,0 +1,293 @@
const { createMinioClient, minioConfig, getPublicUrl } = require('../config/minio');
const path = require('path');
const crypto = require('crypto');
/**
* MinIO 文件服务
* 提供文件上传、删除、获取等功能
*/
class MinioService {
constructor() {
this.client = createMinioClient();
}
/**
* 生成唯一文件名
* @param {string} originalName - 原始文件名
* @returns {string} 唯一文件名
*/
generateUniqueFileName(originalName) {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const timestamp = Date.now();
const randomString = crypto.randomBytes(8).toString('hex');
const ext = path.extname(originalName);
return `${year}/${month}/${day}/${timestamp}_${randomString}${ext}`;
}
/**
* 根据文件类型获取存储桶名称
* @param {string} type - 文件类型 (avatar, product, document)
* @returns {string} 存储桶名称
*/
getBucketName(type = 'document') {
const bucketMap = {
'avatar': minioConfig.buckets.avatars,
'product': minioConfig.buckets.products,
'document': minioConfig.buckets.documents
};
return bucketMap[type] || minioConfig.buckets.documents;
}
/**
* 上传单个文件
* @param {Buffer} fileBuffer - 文件缓冲区
* @param {string} originalName - 原始文件名
* @param {string} mimeType - 文件MIME类型
* @param {string} type - 文件类型
* @returns {Promise<Object>} 上传结果
*/
async uploadFile(fileBuffer, originalName, mimeType, type = 'document') {
try {
const bucketName = this.getBucketName(type);
const fileName = this.generateUniqueFileName(originalName);
// 设置文件元数据
const metaData = {
'Content-Type': mimeType,
'Original-Name': encodeURIComponent(originalName),
'Upload-Time': new Date().toISOString()
};
// 上传文件到MinIO
await this.client.putObject(bucketName, fileName, fileBuffer, fileBuffer.length, metaData);
// 生成访问URL
const url = getPublicUrl(bucketName, fileName);
return {
success: true,
data: {
filename: fileName,
originalname: originalName,
mimetype: mimeType,
size: fileBuffer.length,
bucket: bucketName,
path: `${bucketName}/${fileName}`,
url: url
}
};
} catch (error) {
console.error('MinIO文件上传失败:', error);
throw new Error(`文件上传失败: ${error.message}`);
}
}
/**
* 迁移专用:上传文件到指定存储桶和路径
* @param {string} bucketName - 存储桶名称
* @param {string} filePath - 文件路径
* @param {Buffer} fileBuffer - 文件缓冲区
* @param {string} mimeType - 文件MIME类型
* @returns {Promise<Object>} 上传结果
*/
async uploadFileForMigration(bucketName, filePath, fileBuffer, mimeType) {
try {
// 设置文件元数据
const metaData = {
'Content-Type': mimeType,
'Upload-Time': new Date().toISOString()
};
// 上传文件到MinIO
await this.client.putObject(bucketName, filePath, fileBuffer, fileBuffer.length, metaData);
// 生成访问URL
const url = getPublicUrl(bucketName, filePath);
return {
success: true,
data: {
filename: filePath,
mimetype: mimeType,
size: fileBuffer.length,
bucket: bucketName,
path: `${bucketName}/${filePath}`,
url: url
}
};
} catch (error) {
console.error('MinIO文件迁移上传失败:', error);
throw new Error(`文件迁移上传失败: ${error.message}`);
}
}
/**
* 上传多个文件
* @param {Array} files - 文件数组,每个文件包含 {buffer, originalName, mimeType}
* @param {string} type - 文件类型
* @returns {Promise<Array>} 上传结果数组
*/
async uploadMultipleFiles(files, type = 'document') {
try {
const uploadPromises = files.map(file =>
this.uploadFile(file.buffer, file.originalName, file.mimeType, type)
);
const results = await Promise.all(uploadPromises);
const uploadedFiles = results.map(result => result.data);
return {
success: true,
data: {
files: uploadedFiles,
urls: uploadedFiles.map(file => file.url),
count: uploadedFiles.length
}
};
} catch (error) {
console.error('MinIO多文件上传失败:', error);
throw new Error(`多文件上传失败: ${error.message}`);
}
}
/**
* 删除文件
* @param {string} bucketName - 存储桶名称
* @param {string} fileName - 文件名
* @returns {Promise<boolean>} 删除结果
*/
async deleteFile(bucketName, fileName) {
try {
await this.client.removeObject(bucketName, fileName);
console.log(`✅ 文件删除成功: ${bucketName}/${fileName}`);
return true;
} catch (error) {
console.error('MinIO文件删除失败:', error);
throw new Error(`文件删除失败: ${error.message}`);
}
}
/**
* 批量删除文件
* @param {string} bucketName - 存储桶名称
* @param {Array<string>} fileNames - 文件名数组
* @returns {Promise<Object>} 删除结果
*/
async deleteMultipleFiles(bucketName, fileNames) {
try {
const deletePromises = fileNames.map(fileName =>
this.deleteFile(bucketName, fileName)
);
await Promise.all(deletePromises);
return {
success: true,
deletedCount: fileNames.length,
message: `成功删除${fileNames.length}个文件`
};
} catch (error) {
console.error('MinIO批量删除失败:', error);
throw new Error(`批量删除失败: ${error.message}`);
}
}
/**
* 检查文件是否存在
* @param {string} bucketName - 存储桶名称
* @param {string} fileName - 文件名
* @returns {Promise<boolean>} 文件是否存在
*/
async fileExists(bucketName, fileName) {
try {
await this.client.statObject(bucketName, fileName);
return true;
} catch (error) {
if (error.code === 'NotFound') {
return false;
}
throw error;
}
}
/**
* 获取文件信息
* @param {string} bucketName - 存储桶名称
* @param {string} fileName - 文件名
* @returns {Promise<Object>} 文件信息
*/
async getFileInfo(bucketName, fileName) {
try {
const stat = await this.client.statObject(bucketName, fileName);
return {
size: stat.size,
lastModified: stat.lastModified,
etag: stat.etag,
contentType: stat.metaData['content-type'],
originalName: decodeURIComponent(stat.metaData['original-name'] || fileName)
};
} catch (error) {
console.error('获取文件信息失败:', error);
throw new Error(`获取文件信息失败: ${error.message}`);
}
}
/**
* 生成预签名URL用于临时访问
* @param {string} bucketName - 存储桶名称
* @param {string} fileName - 文件名
* @param {number} expiry - 过期时间默认7天
* @returns {Promise<string>} 预签名URL
*/
async getPresignedUrl(bucketName, fileName, expiry = 7 * 24 * 60 * 60) {
try {
const url = await this.client.presignedGetObject(bucketName, fileName, expiry);
return url;
} catch (error) {
console.error('生成预签名URL失败:', error);
throw new Error(`生成预签名URL失败: ${error.message}`);
}
}
/**
* 列出存储桶中的文件
* @param {string} bucketName - 存储桶名称
* @param {string} prefix - 文件前缀
* @param {number} limit - 限制数量
* @returns {Promise<Array>} 文件列表
*/
async listFiles(bucketName, prefix = '', limit = 100) {
try {
const files = [];
const stream = this.client.listObjects(bucketName, prefix, true);
return new Promise((resolve, reject) => {
stream.on('data', (obj) => {
if (files.length < limit) {
files.push({
name: obj.name,
size: obj.size,
lastModified: obj.lastModified,
etag: obj.etag,
url: getPublicUrl(bucketName, obj.name)
});
}
});
stream.on('end', () => resolve(files));
stream.on('error', reject);
});
} catch (error) {
console.error('列出文件失败:', error);
throw new Error(`列出文件失败: ${error.message}`);
}
}
}
// 创建单例实例
const minioService = new MinioService();
module.exports = minioService;