Fix/ecommerce 502 bug #1
Generated
+20
@@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"alipay-sdk": "^4.14.0",
|
"alipay-sdk": "^4.14.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
"busboy": "^1.6.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.21.0",
|
"express": "^4.21.0",
|
||||||
@@ -159,6 +160,17 @@
|
|||||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"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": {
|
"node_modules/bytes": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
@@ -1527,6 +1539,14 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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/superagent": {
|
"node_modules/superagent": {
|
||||||
"version": "8.0.6",
|
"version": "8.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.6.tgz",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"alipay-sdk": "^4.14.0",
|
"alipay-sdk": "^4.14.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
"busboy": "^1.6.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.21.0",
|
"express": "^4.21.0",
|
||||||
|
|||||||
+71
-1
@@ -1,6 +1,7 @@
|
|||||||
const crypto = require("node:crypto");
|
const crypto = require("node:crypto");
|
||||||
const dns = require("node:dns");
|
const dns = require("node:dns");
|
||||||
const { requireAuth } = require("./context");
|
const { requireAuth } = require("./context");
|
||||||
|
const Busboy = require("busboy");
|
||||||
const { putObject, isOssConfigured, createSignedReadUrl } = require("../ossClient");
|
const { putObject, isOssConfigured, createSignedReadUrl } = require("../ossClient");
|
||||||
|
|
||||||
const DATA_URL_PATTERN = /^data:([^;,]+);base64,([A-Za-z0-9+/=\s]+)$/;
|
const DATA_URL_PATTERN = /^data:([^;,]+);base64,([A-Za-z0-9+/=\s]+)$/;
|
||||||
@@ -61,6 +62,9 @@ const MIME_EXTENSIONS = {
|
|||||||
"image/jpeg": "jpg",
|
"image/jpeg": "jpg",
|
||||||
"image/png": "png",
|
"image/png": "png",
|
||||||
"image/webp": "webp",
|
"image/webp": "webp",
|
||||||
|
"image/avif": "avif",
|
||||||
|
"image/heic": "heic",
|
||||||
|
"image/heif": "heif",
|
||||||
"image/gif": "gif",
|
"image/gif": "gif",
|
||||||
"video/mp4": "mp4",
|
"video/mp4": "mp4",
|
||||||
"video/webm": "webm",
|
"video/webm": "webm",
|
||||||
@@ -166,7 +170,7 @@ function parseUploadPayload(body) {
|
|||||||
const rawData = String(body?.dataUrl || body?.data || "");
|
const rawData = String(body?.dataUrl || body?.data || "");
|
||||||
const dataUrlMatch = rawData.match(DATA_URL_PATTERN);
|
const dataUrlMatch = rawData.match(DATA_URL_PATTERN);
|
||||||
const mimeType = normalizeMimeType(body?.mimeType || dataUrlMatch?.[1]);
|
const mimeType = normalizeMimeType(body?.mimeType || dataUrlMatch?.[1]);
|
||||||
const base64 = (dataUrlMatch?.[2] || rawData).replace(/\s+/g, "");
|
const base64 = (dataUrlMatch?.[2] || rawData.replace(/^data:[^;,]+;base64,/, "")).replace(/\s+/g, "");
|
||||||
if (!base64) {
|
if (!base64) {
|
||||||
const error = new Error("Missing upload data");
|
const error = new Error("Missing upload data");
|
||||||
error.status = 400;
|
error.status = 400;
|
||||||
@@ -317,6 +321,72 @@ function registerOssRoutes(router) {
|
|||||||
if (!res.headersSent) res.status(502).json({ error: "Proxy failed" });
|
if (!res.headersSent) res.status(502).json({ error: "Proxy failed" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
router.post("/oss/upload-binary", requireAuth, (req, res) => {
|
||||||
|
if (!isOssConfigured()) {
|
||||||
|
return res.status(501).json({ error: "OSS 未配置" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const busboy = Busboy({ headers: req.headers, limits: { fileSize: 10 * 1024 * 1024 } });
|
||||||
|
let fileBuffer = null;
|
||||||
|
let fileMimeType = "application/octet-stream";
|
||||||
|
let fileName = "upload";
|
||||||
|
let scope = "";
|
||||||
|
|
||||||
|
busboy.on("file", (fieldname, stream, info) => {
|
||||||
|
if (fieldname !== "file") {
|
||||||
|
stream.resume();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fileName = info.filename || "upload";
|
||||||
|
fileMimeType = info.mimeType || "application/octet-stream";
|
||||||
|
const chunks = [];
|
||||||
|
stream.on("data", (chunk) => chunks.push(chunk));
|
||||||
|
stream.on("end", () => {
|
||||||
|
fileBuffer = Buffer.concat(chunks);
|
||||||
|
});
|
||||||
|
stream.on("error", (err) => {
|
||||||
|
console.error("[oss/upload-binary] stream error:", err.message);
|
||||||
|
res.status(500).json({ error: "文件读取失败" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
busboy.on("field", (fieldname, val) => {
|
||||||
|
if (fieldname === "scope") scope = String(val).trim();
|
||||||
|
if (fieldname === "mimeType") fileMimeType = String(val).trim();
|
||||||
|
});
|
||||||
|
|
||||||
|
busboy.on("finish", async () => {
|
||||||
|
try {
|
||||||
|
if (!fileBuffer) {
|
||||||
|
return res.status(400).json({ error: "未收到文件" });
|
||||||
|
}
|
||||||
|
const normalizedMimeType = normalizeMimeType(fileMimeType);
|
||||||
|
const ext = MIME_EXTENSIONS[normalizedMimeType] || "bin";
|
||||||
|
const assetDir = getAssetDirectory(normalizedMimeType);
|
||||||
|
const safeUserId = String(req.user.id).replace(/[^a-zA-Z0-9_-]/g, "");
|
||||||
|
const objectKey =
|
||||||
|
getProfileObjectKey(scope, req.user.id, ext, normalizedMimeType) ||
|
||||||
|
getCommunityObjectKey(scope, req.user.id, ext, normalizedMimeType) ||
|
||||||
|
`tmp/${safeUserId}/generation-inputs/${assetDir}/${Date.now()}_${crypto.randomUUID()}.${ext}`;
|
||||||
|
await putObject(objectKey, fileBuffer, normalizedMimeType, { "x-oss-object-acl": "public-read" });
|
||||||
|
const url = buildOssPublicUrl(objectKey);
|
||||||
|
const signedUrl = typeof createSignedReadUrl === "function" ? createSignedReadUrl(objectKey) : url;
|
||||||
|
console.info("[oss/upload-binary] mimeType:", normalizedMimeType, "size:", fileBuffer.length, "userId:", req.user.id);
|
||||||
|
res.status(201).json({ ossKey: objectKey, url, signedUrl });
|
||||||
|
} catch (err) {
|
||||||
|
const status = err.status || 500;
|
||||||
|
console.error("[oss/upload-binary] failed:", err.message);
|
||||||
|
res.status(status).json({ error: err.message || "上传素材失败" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
busboy.on("error", (err) => {
|
||||||
|
console.error("[oss/upload-binary] busboy error:", err.message);
|
||||||
|
res.status(500).json({ error: "文件解析失败" });
|
||||||
|
});
|
||||||
|
|
||||||
|
req.pipe(busboy);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
Reference in New Issue
Block a user