Commit 90fd3ed3 by linjinhong

feat:修改下载素材 下载排版方法

parent 26c55257
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -39,6 +39,7 @@
"express": "^4.17.1",
"fabric": "^6.7.0",
"html2canvas": "^1.4.1",
"lodash": "^4.18.1",
"lodash-id": "^0.14.0",
"log4js": "^6.9.1",
"moment": "^2.30.1",
......
......@@ -4,8 +4,11 @@ import { app, protocol, BrowserWindow, globalShortcut } from "electron";
import { createProtocol } from "vue-cli-plugin-electron-builder/lib";
import { createServer } from "@/server/index.js";
import { autoUpdater } from "electron-updater";
const registerDownloadIpc = require("@/utils/download-file.js");
import downloadQueue from "@/utils/download-queue";
const {
registerTypesettingDownloadIpc
} = require("@/utils/download-typesetting");
const fs = require("fs");
import path from "path"; // 引入 path 模块
......@@ -30,6 +33,8 @@ const winURL =
? "http://localhost:8050"
: `file://${__dirname}/index.html`;
registerDownloadIpc();
registerTypesettingDownloadIpc();
async function createWindow() {
win = new BrowserWindow({
width: 1500,
......
......@@ -52,7 +52,9 @@ export const pathMap = {
listOperationByBatch: "factory/podOrderBatchDownload/listOperationByBatch",
//排版
replenishmentComposingDesignImages:
"factory/podOrderOperation/replenishmentComposingDesignImages"
"factory/podOrderOperation/replenishmentComposingDesignImages",
//获取单个批次号信息
podOrderBatchDownload: "factory/podOrderBatchDownload/get"
};
// 操作单状态枚举
export const statusMap = {
......
......@@ -7,7 +7,7 @@ import {
createBatchFolder,
downloadDTFFiles
} from "@/server/utils";
const { downloadPngList } = require("@/utils/download-queue");
const {
cropImageTransparentEdges,
cropTransparentEdges,
......@@ -564,7 +564,8 @@ export default {
//获取国家名称及代码
getAllCountry: async (req, res) => {
const token = req.headers["jwt-token"];
let url = "https://factory.jomalls.com/api/logisticsAddress/getAllCountry";
env = getHostApi().apiApiHost;
let url = `${env}/logisticsAddress/getAllCountry`;
try {
let { data } = await axios.get(url, { headers: { "jwt-token": token } });
......@@ -671,16 +672,7 @@ export default {
res.json({ code: 500, msg: error });
}
},
downloadMaterial: async (req, res) => {
try {
const { urls, targetPath } = req.body;
const data = await downloadPngList(urls, targetPath);
res.send(data);
} catch (error) {
console.log("downloadMaterial", error);
res.json({ code: 500, msg: error });
}
},
//获取生产配置
productionConfig: async (req, res) => {
env = getHostApi().apiApiHost;
......@@ -816,5 +808,22 @@ export default {
console.log("checkInventory", error);
res.json({ code: 500, msg: error });
}
},
podOrderBatchDownload: async (req, res) => {
env = getHostApi().apiApiHost;
const token = req.headers["jwt-token"];
const { id } = req.body;
let url = pathMap["podOrderBatchDownload"];
try {
let { data } = await axios.get(`${env}/${url}?id=${id}`, {
headers: { "jwt-token": token }
});
res.json({ code: 200, data });
} catch (error) {
console.log("checkInventory", error);
res.json({ code: 500, msg: error });
}
}
};
......@@ -54,9 +54,6 @@ router.post("/saveImgByUrl", fn.saveImgByUrl);
//获取目标路径文件夹下所以文件
router.post("/createBatchFolder", fn.createBatchFolder);
//获下载原素材
router.post("/downloadMaterial", fn.downloadMaterial);
//复制文件夹下文件
router.post("/copySingleImageFn", fn.copySingleImageFn);
......@@ -74,12 +71,14 @@ autoRegisterRouter(
"psReComposingDesignImages",
fn.psReComposingDesignImages
);
//重新合成排版
//排版
autoRegisterRouter(
router,
"replenishmentComposingDesignImages",
fn.replenishmentComposingDesignImages
);
//排版
autoRegisterRouter(router, "podOrderBatchDownload", fn.podOrderBatchDownload);
//根据批次分页查询操作单
autoRegisterRouter(router, "listOperationByBatch", fn.listOperationByBatch);
......
......@@ -58,9 +58,9 @@ service.interceptors.response.use(
showClose: true,
message: res.msg || res.message || "Error"
});
setTimeout(() => {
location.reload();
}, 500);
// setTimeout(() => {
// location.reload();
// }, 500);
return Promise.reject(new Error(res.msg || res.message || "Error"));
}
......
const path = require("path");
const fs = require("fs");
const http = require("http");
const https = require("https");
const url = require("url");
/* ===================== 基础工具函数 ===================== */
/**
* 从URL提取文件名
*/
function getFileName(urlStr) {
const formatUrl = urlStr.replace(/\\/g, "/");
return path.basename(formatUrl);
}
/**
* 判断是否为绝对URL
*/
function isAbsoluteUrl(urlStr) {
return /^https?:\/\//i.test(urlStr);
}
/**
* 补全相对路径为完整URL
*/
function resolveUrl(urlStr, baseUrl) {
if (isAbsoluteUrl(urlStr)) return urlStr;
return `${baseUrl.replace(/\/$/, "")}/${urlStr.replace(/^\//, "")}`;
}
/**
* 递归创建目录
*/
async function ensureDir(dirPath) {
await fs.promises.mkdir(dirPath, { recursive: true });
}
/**
* 判断文件是否存在
*/
async function isFileExists(filePath) {
try {
await fs.promises.access(filePath, fs.constants.F_OK);
return true;
} catch (e) {
return false;
}
}
/**
* 并发限制器
*/
function pLimit(concurrency) {
let activeCount = 0;
const queue = [];
const next = () => {
if (queue.length > 0 && activeCount < concurrency) {
activeCount++;
const task = queue.shift();
task().finally(() => {
activeCount--;
next();
});
}
};
return fn => {
return new Promise((resolve, reject) => {
queue.push(() =>
fn()
.then(resolve)
.catch(reject)
);
next();
});
};
}
/* ===================== 核心下载能力 ===================== */
/**
* 单文件流式下载(支持重定向、超时、自动清理残文件)
* @param {string} fileUrl 完整下载链接
* @param {string} filePath 本地保存路径
* @param {number} timeout 超时时间ms
* @param {number} redirectCount 已重定向次数
*/
function streamDownload(fileUrl, filePath, timeout = 30000, redirectCount = 0) {
return new Promise((resolve, reject) => {
try {
const requestModule = fileUrl.startsWith("https") ? https : http;
const req = requestModule.get(fileUrl, { timeout }, res => {
// 重定向处理,最多3次防死循环
if ([301, 302, 307, 308].includes(res.statusCode)) {
if (redirectCount >= 3) {
res.resume();
reject(new Error("重定向次数超过3次"));
return;
}
const redirectUrl = url.resolve(fileUrl, res.headers.location);
streamDownload(redirectUrl, filePath, timeout, redirectCount + 1)
.then(resolve)
.catch(reject);
return;
}
if (res.statusCode !== 200) {
res.resume();
reject(new Error(`HTTP状态码异常: ${res.statusCode}`));
return;
}
let writeStream;
try {
writeStream = fs.createWriteStream(filePath);
} catch (err) {
res.resume();
reject(new Error(`路径创建失败:${err.message}`));
return;
}
res.pipe(writeStream);
writeStream.on("finish", () => {
writeStream.close(() => resolve(filePath));
});
writeStream.on("error", err => {
writeStream.destroy();
fs.promises.unlink(filePath).catch(() => {});
reject(err);
});
res.on("error", err => {
writeStream.destroy();
fs.promises.unlink(filePath).catch(() => {});
reject(new Error(`网络响应异常: ${err.message}`));
});
});
req.on("error", err => reject(new Error(`网络请求失败: ${err.message}`)));
req.on("timeout", () => {
req.destroy();
reject(new Error("下载超时"));
});
} catch (syncErr) {
reject(new Error(`请求初始化失败: ${syncErr.message}`));
}
});
}
/**
* 带失败重试的单文件下载
*/
async function downloadWithRetry(
fileUrl,
filePath,
retryTimes = 0,
timeout = 30000
) {
const attempt = async remain => {
try {
return await streamDownload(fileUrl, filePath, timeout);
} catch (err) {
if (remain > 0) {
console.log(`[下载重试] 剩余 ${remain} 次:${path.basename(filePath)}`);
return attempt(remain - 1);
}
throw err;
}
};
return attempt(retryTimes);
}
/* ===================== 批量下载封装 ===================== */
/**
* 批量下载(URL数组版本)
* @param {string[]} urlList URL数组
* @param {string} targetDir 目标目录
* @param {Object} options 配置
* @returns {Promise<{success: string[], failed: Array<{url:string, error:string}>}>}
*/
async function batchDownloadByList(urlList, targetDir, options = {}) {
const {
concurrency = 5,
timeout = 30000,
retryTimes = 0,
baseUrl = "",
skipExists = true,
fileNameFormatter = null // 新增:自定义文件名回调
} = options;
const success = [];
const failed = [];
// 1. 创建目标目录
try {
await ensureDir(targetDir);
} catch (dirErr) {
const errMsg = `目标目录创建失败:${dirErr.message}`;
console.error(`[批量下载][路径错误] ${errMsg} | 目录: ${targetDir}`);
return {
success: [],
failed: [{ url: "", error: errMsg }]
};
}
// 2. 并发执行下载
const limit = pLimit(concurrency);
const tasks = urlList.map(urlStr =>
limit(async () => {
const fullUrl = baseUrl ? resolveUrl(urlStr, baseUrl) : urlStr;
const fileName = fileNameFormatter
? fileNameFormatter(urlStr, baseUrl)
: getFileName(urlStr);
const savePath = path.join(targetDir, fileName);
try {
// 已存在则跳过
if (skipExists && (await isFileExists(savePath))) {
console.log(`[批量下载][跳过] 文件已存在: ${fileName}`);
success.push(savePath);
return;
}
await downloadWithRetry(fullUrl, savePath, retryTimes, timeout);
console.log(`[批量下载][成功] ${fileName}`);
success.push(savePath);
} catch (err) {
// 识别路径类错误
const isPathError =
["ENOENT", "EINVAL", "ENAMETOOLONG", "EPERM", "EACCES"].includes(
err.code
) || /路径|path|file/i.test(err.message);
const errorType = isPathError ? "路径错误" : "下载失败";
console.error(
`[批量下载][${errorType}] 跳过文件: ${fileName} | 链接: ${fullUrl} | 原因: ${err.message}`
);
failed.push({ url: fullUrl, error: err.message });
}
})
);
await Promise.all(tasks);
console.log(`批量下载完成,成功: ${success.length},失败: ${failed.length}`);
return { success, failed };
}
/**
* 批量下载(逗号分隔URL字符串版本)
* @param {string} urlStr 逗号分隔的URL字符串
* @param {string} targetDir 目标目录
* @param {Object} options 配置
*/
async function batchDownloadByStr(urlStr, targetDir, options = {}) {
// 参数校验
if (!urlStr || typeof urlStr !== "string") {
console.error("[批量下载][参数错误] 传入的URL字符串为空或格式无效");
return { success: [], failed: [{ url: "", error: "URL字符串无效" }] };
}
// 拆分URL
const urlList = urlStr
.split(",")
.map(u => u.trim())
.filter(u => u);
if (urlList.length === 0) {
console.warn("[批量下载][无有效链接] 拆分后无可用URL,直接跳过");
return { success: [], failed: [] };
}
return batchDownloadByList(urlList, targetDir, options);
}
module.exports = {
// 工具函数
getFileName,
isAbsoluteUrl,
resolveUrl,
ensureDir,
isFileExists,
pLimit,
// 核心下载
streamDownload,
downloadWithRetry,
// 批量下载
batchDownloadByList,
batchDownloadByStr
};
const { ipcMain, BrowserWindow } = require("electron");
const { getHostApi } = require("@/server/utils/store");
const { batchDownloadByList } = require("./download-core");
/* ========== 任务管理模块(全局互斥锁 + 状态广播) ========== */
const taskState = {
isRunning: false,
currentBatchNo: "",
latestResult: { success: [], failed: [] }
};
function broadcastStatus() {
const allWindows = BrowserWindow.getAllWindows();
allWindows.forEach(win => {
if (!win.isDestroyed()) {
win.webContents.send("download:status", {
isRunning: taskState.isRunning,
currentBatchNo: taskState.currentBatchNo,
result: taskState.latestResult
});
}
});
}
function submitTask(params) {
const { urls, targetPath, batchArrangeNum } = params;
if (taskState.isRunning) {
return {
code: 1,
msg: `当前正在下载批次【${taskState.currentBatchNo}】,请等待完成后再操作`,
data: { isRunning: true }
};
}
if (!Array.isArray(urls) || urls.length === 0) {
return { code: 1, msg: "下载链接数组为空" };
}
taskState.isRunning = true;
taskState.currentBatchNo = batchArrangeNum || "未知批次";
taskState.latestResult = { success: [], failed: [] };
broadcastStatus();
// 调用核心下载能力
batchDownloadByList(urls, targetPath, {
concurrency: 2,
timeout: 30000,
retryTimes: 1,
baseUrl: getHostApi().fileApiUrl
})
.then(result => {
taskState.latestResult = result;
})
.catch(err => {
taskState.latestResult = {
success: [],
failed: [{ url: "", error: err.message || "下载任务异常中断" }]
};
})
.finally(() => {
taskState.isRunning = false;
taskState.currentBatchNo = "";
broadcastStatus();
});
return {
code: 0,
msg: "下载任务已提交,后台处理中",
data: { isRunning: true }
};
}
function getStatus() {
return {
code: 0,
data: {
isRunning: taskState.isRunning,
currentBatchNo: taskState.currentBatchNo,
result: taskState.latestResult
}
};
}
/* ========== IPC 注册 ========== */
function registerDownloadIpc() {
ipcMain.on("download:submit", (event, params) => {
const result = submitTask(params);
event.sender.send("download:submit-reply", result);
});
ipcMain.on("download:getStatus", event => {
const result = getStatus();
event.sender.send("download:getStatus-reply", result);
});
}
module.exports = registerDownloadIpc;
// ===================== 1. 依赖引入(最顶层,按依赖顺序) =====================
const path = require("path");
const fs = require("fs");
const http = require("http");
const https = require("https");
const { pathMap } = require("@/config/index.js");
const { getDTFSetting, getHostApi } = require("@/server/utils/store");
import api from "./request";
// ===================== 2. 全局状态与可配置参数 =====================
const { batchDownloadByStr } = require("./download-core.js");
/* ========== 全局状态 ========== */
const taskState = {
currentPage: 1, // 当前拉取页码,从第1页开始
pageSize: 100, // 每页拉取100条数据,与后端约定一致
layoutStatus: "3,5", // 部分成功,排版成功
isDownloading: false, // 任务互斥锁:防止5分钟到点后重复执行
timer: null, // 定时器实例,用于启停控制
total: 0, // 数据总条数,从接口实时同步
interval: 5 * 60 * 1000, // 定时周期:5分钟执行一次
maxBatchConcurrency: 3, // 同时处理的最大批次数量(总并发维度1)
maxImageConcurrency: 10 // 单个批次内同时下载的最大图片数(总并发维度2)
// 总并发峰值 = 3 × 10 = 30,生产环境安全稳定
currentPage: 1,
pageSize: 100,
layoutStatus: "3,5",
isDownloading: false,
timer: null,
total: 0,
interval: 5 * 60 * 1000,
maxBatchConcurrency: 3,
maxImageConcurrency: 10
};
// ===================== 3. 基础工具函数 =====================
/**
* 统一错误日志格式化输出
* @param {string} type 错误类型
* @param {string} batchNo 批次号
* @param {string} url 出错的图片链接
* @param {string} message 错误详情
*/
/* ========== 工具函数(业务专属,保留) ========== */
function logDownloadError(type, batchNo, url, message) {
console.error(
`[自动下载失败][${type}] 批次号: ${batchNo || "未知"} | 链接: ${url ||
......@@ -37,162 +25,13 @@ function logDownloadError(type, batchNo, url, message) {
);
}
/**
* 简易并发限制器(无第三方依赖,兼容Node 12)
* 作用:保证同时运行的异步任务不超过指定数量,完成一个自动补上下一个
* @param {number} concurrency 最大并发数
* @returns {Function} 接收异步函数的包装器
*/
function pLimit(concurrency) {
let activeCount = 0; // 当前正在执行的任务数
const queue = []; // 等待执行的任务队列
// 尝试启动下一个任务
const next = () => {
if (queue.length > 0 && activeCount < concurrency) {
activeCount++;
const task = queue.shift();
task().finally(() => {
activeCount--;
next(); // 任务完成后自动启动下一个
});
}
};
return fn => {
return new Promise((resolve, reject) => {
// 加入等待队列
queue.push(() =>
fn()
.then(resolve)
.catch(reject)
);
next();
});
};
}
/**
* 递归创建目录(异步,不阻塞主线程)
* @param {string} dirPath 目录路径
*/
async function ensureDir(dirPath) {
await fs.promises.mkdir(dirPath, { recursive: true });
}
/**
* 从URL中提取原始文件名(完全沿用你指定的逻辑)
* @param {string} url 图片链接
* @returns {string} 原始文件名
*/
const getPngFileName = url => {
let filePath = url.replace(/\\/g, "/"); // 统一反斜杠为正斜杠
const originalFileName = path.basename(filePath);
return originalFileName;
};
/**
* 异步判断文件是否已存在
* @param {string} filePath 文件完整路径
* @returns {boolean} true=存在,false=不存在
*/
async function isFileExists(filePath) {
try {
await fs.promises.access(filePath, fs.constants.F_OK);
return true;
} catch (e) {
return false;
}
}
// ===================== 4. 核心下载能力 =====================
/**
* 单文件流式下载(全错误捕获 + 重定向控制)
* 流式写入:边下载边写盘,内存占用极低,不阻塞主线程
* @param {string} url 图片下载链接
* @param {string} savePath 本地保存完整路径
* @param {number} redirectCount 已重定向次数(内部递归用,外部不传)
* @returns {Promise<string>} 保存成功的文件路径
*/
function downloadFile(url, savePath, redirectCount = 0) {
return new Promise((resolve, reject) => {
try {
// 根据链接协议自动选择 http/https 模块
const request = url.startsWith("https") ? https : http;
const req = request.get(url, { timeout: 30000 }, res => {
// 处理重定向:最多3次,防止死循环
if ([301, 302, 307, 308].includes(res.statusCode)) {
if (redirectCount >= 3) {
res.resume(); // 消耗响应体,释放连接
reject(new Error("重定向次数超过3次,终止下载"));
return;
}
// 递归执行下载,重定向次数+1
downloadFile(res.headers.location, savePath, redirectCount + 1)
.then(resolve)
.catch(reject);
return;
}
// HTTP状态码非200直接报错
if (res.statusCode !== 200) {
res.resume();
reject(new Error(`HTTP状态码异常: ${res.statusCode}`));
return;
}
// 创建写入流,通过管道将网络响应写入文件
const writeStream = fs.createWriteStream(savePath);
res.pipe(writeStream);
// 写入完成
writeStream.on("finish", () => {
writeStream.close(() => resolve(savePath));
});
// 写入失败:销毁流并清理损坏文件
writeStream.on("error", err => {
writeStream.destroy();
fs.promises.unlink(savePath).catch(() => {});
reject(new Error(`文件写入失败: ${err.message}`));
});
// 响应流异常
res.on("error", err => {
writeStream.destroy();
fs.promises.unlink(savePath).catch(() => {});
reject(new Error(`网络响应异常: ${err.message}`));
});
});
// 请求级错误(网络不通、DNS失败等)
req.on("error", err => reject(new Error(`网络请求失败: ${err.message}`)));
// 请求超时
req.on("timeout", () => {
req.destroy();
reject(new Error("下载超时(30s)"));
});
} catch (syncErr) {
// 捕获URL格式错误等同步异常
reject(new Error(`请求初始化失败: ${syncErr.message}`));
}
});
}
// ===================== 6. 核心业务逻辑 =====================
// 复用core的并发控制器,也可以直接用core导出的pLimit
const { pLimit } = require("./download-core");
/**
* 处理单个批次的图片下载
* 单张图片失败仅跳过当前文件,不影响同批次其他图片
* @param {object} item 单条排版数据(含batchArrangeNum、url字段)
*/
/* ========== 业务逻辑 ========== */
async function processBatchItem(item) {
const { batchArrangeNum, url: urlStr } = item;
// ---------- 基础参数校验 ----------
if (!batchArrangeNum) {
logDownloadError(
"参数无效",
......@@ -212,87 +51,30 @@ async function processBatchItem(item) {
return;
}
// ---------- 1. 创建批次保存目录 ----------
// 构建保存目录
let saveDir;
try {
const downloadRoot = getDTFSetting().downloadDir;
if (!downloadRoot) throw new Error("下载根目录downloadDir配置为空");
// 目录结构:根目录/批次号:xxx/自动排版素材
saveDir = path.join(
downloadRoot,
`批次号:${batchArrangeNum}`,
"自动排版素材"
);
await ensureDir(saveDir);
} catch (dirErr) {
// 目录创建失败:整个批次跳过
logDownloadError("目录创建失败", batchArrangeNum, "", dirErr.message);
return;
}
// ---------- 2. 拆分图片链接 ----------
// 支持逗号分隔的多图字符串,过滤空值与无效值
const urlList = urlStr
.split(",")
.map(u => u.trim())
.filter(u => {
if (!u || typeof u !== "string") {
logDownloadError("链接无效", batchArrangeNum, u, "空链接或非字符串");
return false;
}
return true;
// 调用核心批量下载
await batchDownloadByStr(urlStr, saveDir, {
concurrency: taskState.maxImageConcurrency,
timeout: 30000,
baseUrl: getHostApi().fileApiUrl
});
if (urlList.length === 0) {
logDownloadError("无有效链接", batchArrangeNum, "", "拆分后无可用图片链接");
return;
}
// ---------- 3. 并发下载批次内所有图片 ----------
const imageLimit = pLimit(taskState.maxImageConcurrency);
const fileEnv = getHostApi().fileApiUrl;
const downloadPromises = urlList.map(url =>
imageLimit(async () => {
const newUrl = `${fileEnv}${url}`;
try {
const fileName = getPngFileName(url);
const savePath = path.join(saveDir, fileName);
// 文件已存在则直接跳过,不重复下载
const exists = await isFileExists(savePath);
if (exists) {
console.log(
`[跳过下载] 文件已存在 | 批次: ${batchArrangeNum} | 文件: ${fileName}`
);
return;
}
// 执行下载
await downloadFile(newUrl, savePath);
console.log(`[下载成功] 批次: ${batchArrangeNum} | 文件: ${fileName}`);
} catch (downloadErr) {
// 单张下载失败:记录日志,跳过当前文件
logDownloadError(
"文件下载失败",
batchArrangeNum,
newUrl,
downloadErr.message
);
}
})
);
// 等待当前批次所有图片处理完成
await Promise.all(downloadPromises);
}
/**
* 执行一次完整的定时任务
* 流程:获取设置 → 判断策略 → 拉取列表 → 批量下载 → 更新页码
*/
async function runTask() {
// 互斥锁:上一轮任务未完成则直接跳过本轮
if (taskState.isDownloading) {
console.log("当前有下载任务进行中,跳过本轮定时任务");
return;
......@@ -302,36 +84,29 @@ async function runTask() {
taskState.isDownloading = true;
console.log(`=== 开始执行定时任务,当前页码:${taskState.currentPage} ===`);
// 步骤1:调用获取设置接口
// 获取排版策略
const settingRes = await api.post(pathMap["productionConfig"]);
const autoLayoutStrategy = settingRes.data.autoLayoutStrategy;
// 步骤2:策略判断:不等于3才继续下载
if (autoLayoutStrategy === 3) {
if (settingRes.data.autoLayoutStrategy === 3) {
console.log("不自动排版,跳过本次下载");
return;
}
// 步骤3:调用排版数据接口,拉取当前页数据
// 拉取排版数据
const layoutRes = await api.post(pathMap["productionSoftware"], {
currentPage: taskState.currentPage,
pageSize: taskState.pageSize,
layoutStatus: taskState.layoutStatus
});
console.log(layoutRes);
// return;
const list = layoutRes.data.records || [];
const total = layoutRes.data.total || 0;
taskState.total = total;
taskState.total = layoutRes.data.total || 0;
if (list.length === 0) {
console.log("当前页无数据,本轮任务结束");
return;
}
// 步骤4:多批次并行下载
// 多批次并行下载
const batchLimit = pLimit(taskState.maxBatchConcurrency);
const batchPromises = list.map(item =>
batchLimit(() => processBatchItem(item))
......@@ -340,40 +115,27 @@ async function runTask() {
console.log(`=== 第 ${taskState.currentPage} 页全部处理完成 ===`);
// 步骤5:更新页码,超过总数则从第1页循环
// 更新页码
taskState.currentPage++;
if ((taskState.currentPage - 1) * taskState.pageSize >= taskState.total) {
taskState.currentPage = 1;
console.log("已遍历全部数据,下轮任务从第1页重新开始");
}
} catch (err) {
// 整体任务异常(接口挂了等),记录日志
console.error("定时任务执行异常:", err.message);
} finally {
// 无论成功失败,最终释放锁,避免死锁
taskState.isDownloading = false;
console.log("本轮任务结束,锁已释放");
}
}
// ===================== 7. 对外暴露的启停方法 =====================
/**
* 启动自动下载定时任务
* 调用后立即启动定时器,5分钟后首次执行任务
*/
/* ========== 对外启停方法 ========== */
function startAutoDownloadTask() {
// 防止重复启动
if (taskState.timer) return;
console.log("自动下载定时任务已启动,5分钟后首次执行");
taskState.timer = setInterval(runTask, taskState.interval);
}
/**
* 停止自动下载定时任务
* 窗口关闭、应用退出时调用,清理定时器与状态
*/
function stopAutoDownloadTask() {
if (taskState.timer) {
clearInterval(taskState.timer);
......@@ -383,7 +145,6 @@ function stopAutoDownloadTask() {
}
}
// ===================== 8. 模块导出 =====================
export default {
startAutoDownloadTask,
stopAutoDownloadTask
......
const { ipcMain, BrowserWindow } = require("electron");
const path = require("path");
const { getHostApi } = require("@/server/utils/store");
const { batchDownloadByStr } = require("./utils/download-core");
/* ===================== 原有命名逻辑(完全保留) ===================== */
function formatTime() {
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 hour = String(now.getHours()).padStart(2, "0");
const minute = String(now.getMinutes()).padStart(2, "0");
const second = String(now.getSeconds()).padStart(2, "0");
return `${year}-${month}-${day}_${hour}.${minute}.${second}`;
}
function customFileNameFormatter(urlStr) {
let cleanPath = urlStr;
try {
cleanPath = decodeURIComponent(urlStr);
} catch (e) {
cleanPath = urlStr;
}
cleanPath = cleanPath.replace(/\\/g, "/");
const originalFileName = path.basename(cleanPath);
const timePrefix = formatTime();
return originalFileName
? `${timePrefix} ${originalFileName}`
: `${timePrefix}.png`;
}
/**
* 原有纯函数调用方式(保留,兼容现有代码)
*/
async function batchDownloadUrls(urlStr, targetDir, options = {}) {
return batchDownloadByStr(urlStr, targetDir, {
baseUrl: getHostApi().fileApiUrl,
fileNameFormatter: customFileNameFormatter,
...options
});
}
/* ===================== 新增:IPC 任务管理模块(独立锁,与原素材隔离) ===================== */
const taskState = {
isRunning: false,
latestResult: { success: [], failed: [] }
};
/**
* 向所有窗口广播状态
*/
function broadcastStatus() {
const allWindows = BrowserWindow.getAllWindows();
allWindows.forEach(win => {
if (!win.isDestroyed()) {
win.webContents.send("download-typesetting:status", {
isRunning: taskState.isRunning,
result: taskState.latestResult
});
}
});
}
/**
* 提交下载任务(异步执行,立即返回)
*/
function submitTask(params) {
const { urlStr, targetPath } = params;
// 全局互斥:有任务在跑直接拒绝
if (taskState.isRunning) {
return {
code: 1,
msg: "当前正在下载手动排版素材,请等待完成后再操作",
data: { isRunning: true }
};
}
if (!urlStr || typeof urlStr !== "string") {
return { code: 1, msg: "下载链接为空或格式无效" };
}
// 加锁
taskState.isRunning = true;
taskState.latestResult = { success: [], failed: [] };
broadcastStatus();
// 后台异步执行下载,不阻塞 IPC 返回
batchDownloadUrls(urlStr, targetPath)
.then(result => {
taskState.latestResult = result;
})
.catch(err => {
taskState.latestResult = {
success: [],
failed: [{ url: "", error: err.message || "下载任务异常中断" }]
};
})
.finally(() => {
// 释放锁
taskState.isRunning = false;
broadcastStatus();
});
return {
code: 0,
msg: "手动排版素材下载任务已提交,后台处理中",
data: { isRunning: true }
};
}
/**
* 查询当前下载状态
*/
function getStatus() {
return {
code: 0,
data: {
isRunning: taskState.isRunning,
result: taskState.latestResult
}
};
}
/* ===================== 新增:IPC 事件注册 ===================== */
/**
* 注册手动排版下载 IPC 事件
* 在主进程入口调用一次即可
*/
function registerTypesettingDownloadIpc() {
// 提交下载任务
ipcMain.on("download-typesetting:submit", (event, params) => {
const result = submitTask(params);
event.sender.send("download-typesetting:submit-reply", result);
});
// 查询下载状态
ipcMain.on("download-typesetting:getStatus", event => {
const result = getStatus();
event.sender.send("download-typesetting:getStatus-reply", result);
});
}
/* ===================== 导出(兼容两种使用方式) ===================== */
module.exports = {
// 原有纯函数,可直接在主进程内调用
batchDownloadUrls,
// IPC 注册函数,主进程入口调用
registerTypesettingDownloadIpc
};
const { ipcRenderer } = window.require("electron");
// ========== 原素材下载 API(原有保留) ==========
/**
* 提交下载任务
* @param {Object} params { urls, targetPath, batchArrangeNum }
* @returns {Promise<Object>}
*/
export function submitDownload(params) {
return new Promise(resolve => {
ipcRenderer.once("download:submit-reply", (_, res) => resolve(res));
ipcRenderer.send("download:submit", params);
});
}
/**
* 获取当前下载状态
* @returns {Promise<Object>}
*/
export function getDownloadStatus() {
return new Promise(resolve => {
ipcRenderer.once("download:getStatus-reply", (_, res) => resolve(res));
ipcRenderer.send("download:getStatus");
});
}
/**
* 监听下载状态变更
* @param {Function} callback
* @returns {Function} 取消监听函数
*/
export function onDownloadStatusChange(callback) {
const handler = (_, data) => callback(data);
ipcRenderer.on("download:status", handler);
return () => ipcRenderer.removeListener("download:status", handler);
}
// ========== 新增:手动排版下载 API ==========
/**
* 提交手动排版下载任务
* @param {Object} params { urlStr: 逗号分隔URL字符串, targetPath: 保存目录 }
* @returns {Promise<Object>}
*/
export function submitTypesettingDownload(params) {
return new Promise(resolve => {
ipcRenderer.once("download-typesetting:submit-reply", (_, res) =>
resolve(res)
);
ipcRenderer.send("download-typesetting:submit", params);
});
}
/**
* 查询手动排版下载状态
* @returns {Promise<Object>}
*/
export function getTypesettingDownloadStatus() {
return new Promise(resolve => {
ipcRenderer.once("download-typesetting:getStatus-reply", (_, res) =>
resolve(res)
);
ipcRenderer.send("download-typesetting:getStatus");
});
}
/**
* 监听手动排版下载状态变更
* @param {Function} callback
* @returns {Function} 取消监听函数
*/
export function onTypesettingStatusChange(callback) {
const handler = (_, data) => callback(data);
ipcRenderer.on("download-typesetting:status", handler);
return () =>
ipcRenderer.removeListener("download-typesetting:status", handler);
}
......@@ -62,7 +62,7 @@ export const cutArea = (canvasEl, mousewheel) => {
boundaryLeft,
boundaryTop,
boundaryWidth,
boundaryHeight,
boundaryHeight
} = getRealBoundary(canvasEl, mousewheel);
//存储正方形边界
......@@ -70,7 +70,7 @@ export const cutArea = (canvasEl, mousewheel) => {
boundaryLeft,
boundaryTop,
boundaryWidth,
boundaryHeight,
boundaryHeight
});
cutCtx.clearRect(boundaryLeft, boundaryTop, boundaryWidth, boundaryHeight);
......@@ -92,7 +92,7 @@ export const cutArea = (canvasEl, mousewheel) => {
canvasEl.set({
cutWidth: width,
cutHeight: height,
cutHeight: height
});
//设置蒙版图片
......@@ -119,7 +119,7 @@ export const cutArea = (canvasEl, mousewheel) => {
canvasEl.maskDiv.appendChild(mask_img);
insertAfter(
canvasEl.maskDiv,
document.getElementById(canvasEl.cutCanvas.id),
document.getElementById(canvasEl.cutCanvas.id)
);
}
......@@ -177,7 +177,7 @@ const getRealBoundary = function(canvasEl, mousewheel) {
if (!canvasEl.oldCutWidth) {
canvasEl.set({
oldCutWidth: cutWidth,
oldCutHeight: cutHeight,
oldCutHeight: cutHeight
});
}
}
......@@ -218,7 +218,7 @@ const getRealBoundary = function(canvasEl, mousewheel) {
boundaryLeft,
boundaryTop,
boundaryWidth,
boundaryHeight,
boundaryHeight
};
};
......
......@@ -6,6 +6,17 @@ const { Menu } = remote;
const path = require("path");
const { pathMap } = require("@/config/index.js");
import DTFhead from "./components/head.vue";
import debounce from "lodash/debounce";
import {
submitDownload,
getDownloadStatus,
onDownloadStatusChange,
submitTypesettingDownload,
getTypesettingDownloadStatus,
onTypesettingStatusChange
} from "@/api/download.api";
export default {
name: "design-dtf",
components: { BaseTable, DTFhead },
......@@ -26,9 +37,12 @@ export default {
searchForm: {},
radio: "",
templateBatchArrangeNum: "",
templateId: "",
isDownloading: false,
resTypesettingisDownloading: false,
dtfSetting: getDTFSetting(),
isFetching: false,
removeStatusListener: null, // 保存取消监听函数
pageInfo: {
currentPage: 1,
pageSize: 10,
......@@ -106,11 +120,24 @@ export default {
}
};
},
mounted() {
async mounted() {
this.loadLocalConfig();
this.fetchBatchList();
this.removeStatusListener = onDownloadStatusChange(status => {
this.handleDownloadStatus("material", status);
});
this.removeListener = onTypesettingStatusChange(status => {
this.handleDownloadStatus("typesetting", status);
});
// 初始化同步状态
await this.syncStatus();
},
beforeDestroy() {
if (this.removeStatusListener) this.removeStatusListener();
if (this.removeListener) this.removeListener();
// 取消待执行的防抖任务,避免销毁后执行报错
this.downloadFile.cancel?.();
},
methods: {
async loadLocalConfig() {
try {
......@@ -154,11 +181,11 @@ export default {
handleSizeChange(size) {
this.pageInfo.pageSize = size;
this.pageInfo.currentPage = 1;
this.settingRes();
this.fetchBatchList(this.searchForm);
},
handleCurrentChange(page) {
this.pageInfo.currentPage = page;
this.loadData();
this.fetchBatchList(this.searchForm);
},
//右键选择器
......@@ -206,6 +233,7 @@ export default {
this.dialogVisible = true;
this.radio = "";
this.templateBatchArrangeNum = value.batchArrangeNum;
this.templateId = value.id;
} else {
this.$message(
"排版进行中,请查看处理状态,排版完成的素材至文件夹排版素材查看"
......@@ -215,10 +243,15 @@ export default {
//提交手动排版
async submit() {
if (this.resTypesettingisDownloading) {
this.$message.warning("其他排版下载中,请等待完成后再操作");
return;
}
if (!this.radio) {
return this.$message.error("请选择排版宽度");
}
try {
this.resTypesettingisDownloading = true;
if (this.dtfSetting.widthStrategy == 3 && this.radio == 60) {
this.$confirm(
"由于排版设置中选择“按更大规格排版”仅适用于40+2,选60时,默认超60的规格不排版",
......@@ -232,21 +265,36 @@ export default {
batchArrangeNum: this.templateBatchArrangeNum,
templateWidth: this.radio
});
this.$message(
"排版进行中,请查看处理状态,排版完成的素材至文件夹排版素材查看"
);
} else {
await this.$api.post(pathMap["psReComposingDesignImages"], {
batchArrangeNum: this.templateBatchArrangeNum,
templateWidth: this.radio
});
}
const { data } = await this.$api.post(
pathMap["podOrderBatchDownload"],
{
id: this.templateId
}
);
await submitTypesettingDownload({
urlStr: data.data.url,
targetPath: path.join(
getDTFSetting().downloadDir,
`批次号:${this.templateBatchArrangeNum}`,
"手动排版素材"
)
});
this.$message(
"排版进行中,请查看处理状态,排版完成的素材至文件夹排版素材查看"
);
}
this.dialogVisible = false;
this.fetchBatchList(this.searchForm);
} catch (error) {
console.log(error);
this.resTypesettingisDownloading = false;
}
},
//跳转批次详情页
......@@ -258,30 +306,84 @@ export default {
}
});
},
downloadFile: debounce(
function(value) {
this._downloadFile(value);
},
300,
{
leading: true,
trailing: false
}
),
//下载批次原素材
async downloadFile(value) {
async _downloadFile(value) {
if (this.isDownloading) {
this.$message.warning("正在下载中,请等待完成后再操作");
return;
}
try {
this.isDownloading = true;
const { data } = await this.$api.post(pathMap["listMaterialsByBatch"], {
batchArrangeNum: value
});
const newUrls = data.flatMap(el => el.urls);
// console.log(311, newUrls);
// return;
const res = await this.$api.post("/downloadMaterial", {
if (newUrls.length === 0) {
this.$message.warning("该批次暂无素材可下载");
this.isDownloading = false;
return;
}
await submitDownload({
urls: newUrls,
targetPath: path.join(
getDTFSetting().downloadDir,
this.dtfSetting.downloadDir,
`批次号:${value}`,
"原素材"
)
),
batchArrangeNum: value
});
// console.log("listMaterialsByBatch-", data);
console.log("downloadMaterial", res);
this.$message("下载进行中,下载完成的素材将在文件夹原素材中显示");
// 提交成功后,锁由 onDownloadStatusChange 事件维护,不再手动修改
} catch (error) {
console.log(error);
console.error("下载提交失败", error);
this.$message.error("下载提交失败,请重试");
// 仅提交失败时释放锁
this.isDownloading = false;
}
},
/**
* 状态变更回调
*/
handleDownloadStatus(type, status) {
const statusKey =
type === "material" ? "isDownloading" : "resTypesettingisDownloading";
this[statusKey] = status.isRunning;
if (!status.isRunning && status.result) {
const { success, failed } = status.result;
console.log(
`下载完成:成功 ${success.length} 个,失败 ${failed.length} 个`
);
if (failed.length > 0) {
console.warn("下载失败明细:", failed);
}
}
},
/**
* 同步当前状态
*/
async syncStatus() {
try {
const res = await getDownloadStatus();
const resTypesetting = await getTypesettingDownloadStatus();
this.isDownloading = res.data.isRunning;
this.resTypesettingisDownloading = resTypesetting.data.isRunning;
} catch (err) {
console.error("同步下载状态失败", err);
}
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment