From 1b55af47de09ee8af62c462b03dd2746e73a1619 Mon Sep 17 00:00:00 2001 From: Sun_sun <469361609@qq.com> Date: Fri, 19 Sep 2025 16:46:00 +0800 Subject: [PATCH] =?UTF-8?q?2025-09-18=20=E5=95=86=E5=9F=8E=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 28 ++ middleware/auth.js | 110 ++++++++ package-lock.json | 585 +++++++++++++++++++++++++++++++++++++++ package.json | 3 + routes/common.js | 29 +- routes/mall.js | 73 +++++ routes/program.js | 19 ++ routes/upload.js | 421 ++++++++++++++++++++++++++++ services/minioService.js | 293 ++++++++++++++++++++ 9 files changed, 1557 insertions(+), 4 deletions(-) create mode 100644 middleware/auth.js create mode 100644 routes/mall.js create mode 100644 routes/program.js create mode 100644 routes/upload.js create mode 100644 services/minioService.js diff --git a/app.js b/app.js index 4c84532..d8474bd 100644 --- a/app.js +++ b/app.js @@ -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; diff --git a/middleware/auth.js b/middleware/auth.js new file mode 100644 index 0000000..ddb8180 --- /dev/null +++ b/middleware/auth.js @@ -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 }; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index fef7c63..ab5324d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 5452848..4981b10 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/routes/common.js b/routes/common.js index d71ca27..608d6d1 100644 --- a/routes/common.js +++ b/routes/common.js @@ -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 { diff --git a/routes/mall.js b/routes/mall.js new file mode 100644 index 0000000..144e5a6 --- /dev/null +++ b/routes/mall.js @@ -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 \ No newline at end of file diff --git a/routes/program.js b/routes/program.js new file mode 100644 index 0000000..dfbae4a --- /dev/null +++ b/routes/program.js @@ -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 \ No newline at end of file diff --git a/routes/upload.js b/routes/upload.js new file mode 100644 index 0000000..2222e0b --- /dev/null +++ b/routes/upload.js @@ -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; \ No newline at end of file diff --git a/services/minioService.js b/services/minioService.js new file mode 100644 index 0000000..9355f5a --- /dev/null +++ b/services/minioService.js @@ -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} 上传结果 + */ + 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} 上传结果 + */ + 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} 上传结果数组 + */ + 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} 删除结果 + */ + 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} fileNames - 文件名数组 + * @returns {Promise} 删除结果 + */ + 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} 文件是否存在 + */ + 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} 文件信息 + */ + 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} 预签名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} 文件列表 + */ + 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; \ No newline at end of file