Commit 26c55257 by linjinhong

feat:修改定时任务方法

parent 98eefb15
...@@ -5,7 +5,7 @@ import { createProtocol } from "vue-cli-plugin-electron-builder/lib"; ...@@ -5,7 +5,7 @@ import { createProtocol } from "vue-cli-plugin-electron-builder/lib";
import { createServer } from "@/server/index.js"; import { createServer } from "@/server/index.js";
import { autoUpdater } from "electron-updater"; import { autoUpdater } from "electron-updater";
const { createBatchDownloadQueue } = require("@/utils/download-queue"); import downloadQueue from "@/utils/download-queue";
const fs = require("fs"); const fs = require("fs");
import path from "path"; // 引入 path 模块 import path from "path"; // 引入 path 模块
...@@ -30,36 +30,6 @@ const winURL = ...@@ -30,36 +30,6 @@ const winURL =
? "http://localhost:8050" ? "http://localhost:8050"
: `file://${__dirname}/index.html`; : `file://${__dirname}/index.html`;
// 全局唯一下载队列,服务启动时初始化一次
const downloadQueue = createBatchDownloadQueue({
concurrency: 3,
retryTimes: 2,
basePath: "",
// 单文件下载成功
onSuccess: task => {
win.webContents.send("download:file-success", {
batchArrangeNum: task.batchArrangeNum,
fileName: task.fileName
});
},
// 单文件下载失败
onError: (task, err) => {
win.webContents.send("download:file-error", {
batchArrangeNum: task.batchArrangeNum,
fileName: task.fileName,
msg: err.message
});
},
// 批次校验失败(新增:返回所有异常批次)
onBatchInvalid: invalidList => {
win.webContents.send("download:batch-invalid", invalidList);
},
// 全部任务完成
onIdle: () => {
win.webContents.send("download:all-finish");
}
});
async function createWindow() { async function createWindow() {
win = new BrowserWindow({ win = new BrowserWindow({
width: 1500, width: 1500,
...@@ -408,6 +378,15 @@ async function createWindow() { ...@@ -408,6 +378,15 @@ async function createWindow() {
); );
}); });
// 监听窗口的 reload 事件
win.webContents.on("did-finish-load", () => {
const { width, height } = win.getBounds();
win.webContents.send("window-size", { width, height });
console.log(99999);
// downloadQueue.startAutoDownloadTask();
});
if (process.env.WEBPACK_DEV_SERVER_URL) { if (process.env.WEBPACK_DEV_SERVER_URL) {
await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL); await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL);
if (!process.env.IS_TEST) win.webContents.openDevTools(); if (!process.env.IS_TEST) win.webContents.openDevTools();
...@@ -508,17 +487,7 @@ async function createWindow() { ...@@ -508,17 +487,7 @@ async function createWindow() {
win.on("resize", () => { win.on("resize", () => {
const { width, height } = win.getBounds(); const { width, height } = win.getBounds();
win.webContents.send("window-size", { width, height }); win.webContents.send("window-size", { width, height });
}); console.log(888);
// 监听窗口的 reload 事件
win.webContents.on("did-finish-load", () => {
const { width, height } = win.getBounds();
win.webContents.send("window-size", { width, height });
});
// IPC 通信
ipcMain.on("download:submit-batch-list", (event, list) => {
downloadQueue.addBatchList(list);
event.returnValue = { code: 200, msg: "已处理" };
}); });
ipcMain.on("get-install-path", event => { ipcMain.on("get-install-path", event => {
...@@ -542,18 +511,9 @@ async function createWindow() { ...@@ -542,18 +511,9 @@ async function createWindow() {
} }
}); });
ipcMain.on("download:get-status", event => { // ========== 新增3:窗口关闭时停止定时任务 ==========
try { win.on("closed", () => {
// 无论正常异常都保证回复,避免前端挂起 downloadQueue.stopAutoDownloadTask();
event.reply("download:status-reply", {
isRunning: downloadQueue.isRunning
});
} catch (err) {
event.reply("download:status-reply", {
isRunning: false,
error: err.message
});
}
}); });
} }
...@@ -587,8 +547,11 @@ app.on("will-quit", () => { ...@@ -587,8 +547,11 @@ app.on("will-quit", () => {
globalShortcut.unregister("F5"); globalShortcut.unregister("F5");
globalShortcut.unregister("CommandOrControl+R"); globalShortcut.unregister("CommandOrControl+R");
}); });
app.on("before-quit", () => { app.on("window-all-closed", () => {
downloadQueue.destroy(); downloadQueue.stopAutoDownloadTask();
if (process.platform !== "darwin") {
app.quit();
}
}); });
if (isDevelopment) { if (isDevelopment) {
......
...@@ -75,7 +75,7 @@ function readEnv() { ...@@ -75,7 +75,7 @@ function readEnv() {
fileEnv = config.fileApiUrl; fileEnv = config.fileApiUrl;
env = config.apiApiHost; env = config.apiApiHost;
visionUrl = config.visionUrl; visionUrl = config.visionUrl;
console.log("配置加载完成:", { fileEnv, env, visionUrl }); // console.log("配置加载完成:", { fileEnv, env, visionUrl });
} catch (err) { } catch (err) {
console.error("读取配置文件失败:", err); console.error("读取配置文件失败:", err);
......
const fs = require("fs"); // ===================== 1. 依赖引入(最顶层,按依赖顺序) =====================
const path = require("path"); const path = require("path");
const fs = require("fs");
const http = require("http"); const http = require("http");
const https = require("https"); const https = require("https");
const { URL } = require("url");
const { getDTFSetting } = require("@/server/utils/store"); const { pathMap } = require("@/config/index.js");
const { getDTFSetting, getHostApi } = require("@/server/utils/store");
export function createBatchDownloadQueue(options = {}) { import api from "./request";
// ========== 1. 配置项 ========== // ===================== 2. 全局状态与可配置参数 =====================
const config = Object.assign( const taskState = {
{ currentPage: 1, // 当前拉取页码,从第1页开始
concurrency: 3, pageSize: 100, // 每页拉取100条数据,与后端约定一致
retryTimes: 2, layoutStatus: "3,5", // 部分成功,排版成功
onSuccess: () => {}, // 单文件下载成功 isDownloading: false, // 任务互斥锁:防止5分钟到点后重复执行
onError: () => {}, // 单文件下载失败 timer: null, // 定时器实例,用于启停控制
onBatchInvalid: () => {}, // 批次校验失败回调(返回异常批次列表+原因) total: 0, // 数据总条数,从接口实时同步
onIdle: () => {} // 全部任务完成 interval: 5 * 60 * 1000, // 定时周期:5分钟执行一次
}, maxBatchConcurrency: 3, // 同时处理的最大批次数量(总并发维度1)
options maxImageConcurrency: 10 // 单个批次内同时下载的最大图片数(总并发维度2)
// 总并发峰值 = 3 × 10 = 30,生产环境安全稳定
};
// ===================== 3. 基础工具函数 =====================
/**
* 统一错误日志格式化输出
* @param {string} type 错误类型
* @param {string} batchNo 批次号
* @param {string} url 出错的图片链接
* @param {string} message 错误详情
*/
function logDownloadError(type, batchNo, url, message) {
console.error(
`[自动下载失败][${type}] 批次号: ${batchNo || "未知"} | 链接: ${url ||
"-"} | 详情: ${message}`
); );
}
// ========== 2. 内部状态 ========== /**
let pendingQueue = []; * 简易并发限制器(无第三方依赖,兼容Node 12)
let runningCount = 0; * 作用:保证同时运行的异步任务不超过指定数量,完成一个自动补上下一个
const successSet = new Set(); * @param {number} concurrency 最大并发数
const runningSet = new Set(); * @returns {Function} 接收异步函数的包装器
let isDestroyed = false; */
function pLimit(concurrency) {
let activeCount = 0; // 当前正在执行的任务数
const queue = []; // 等待执行的任务队列
// ========== 3. 通用工具方法 ========== // 尝试启动下一个任务
const getRequestModule = url => { const next = () => {
try { if (queue.length > 0 && activeCount < concurrency) {
return new URL(url).protocol === "https:" ? https : http; activeCount++;
} catch (e) { const task = queue.shift();
return https; task().finally(() => {
activeCount--;
next(); // 任务完成后自动启动下一个
});
} }
}; };
const isPngUrl = url => { return fn => {
try { return new Promise((resolve, reject) => {
const urlObj = new URL(url); // 加入等待队列
return urlObj.pathname.toLowerCase().endsWith(".png"); queue.push(() =>
} catch (e) { fn()
return false; .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;
};
const getPngFileName = url => { /**
* 异步判断文件是否已存在
* @param {string} filePath 文件完整路径
* @returns {boolean} true=存在,false=不存在
*/
async function isFileExists(filePath) {
try { try {
const urlObj = new URL(url); await fs.promises.access(filePath, fs.constants.F_OK);
const name = path.basename(urlObj.pathname); return true;
return name.toLowerCase().endsWith(".png")
? name
: `${name || `img_${Date.now()}`}.png`;
} catch (e) { } catch (e) {
return `img_${Date.now()}.png`; return false;
} }
}; }
const fileExists = targetPath => fs.existsSync(targetPath); // ===================== 4. 核心下载能力 =====================
const cleanBadFile = targetPath =>
fileExists(targetPath) && fs.unlinkSync(targetPath); /**
* 单文件流式下载(全错误捕获 + 重定向控制)
* 流式写入:边下载边写盘,内存占用极低,不阻塞主线程
* @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;
// ========== 4. 流式下载核心 ========== const req = request.get(url, { timeout: 30000 }, res => {
const streamDownload = (url, targetPath) => // 处理重定向:最多3次,防止死循环
new Promise((resolve, reject) => {
const requestModule = getRequestModule(url);
const writeStream = fs.createWriteStream(targetPath);
const request = requestModule.get(url, res => {
if ([301, 302, 307, 308].includes(res.statusCode)) { if ([301, 302, 307, 308].includes(res.statusCode)) {
writeStream.close(); if (redirectCount >= 3) {
streamDownload(res.headers.location, targetPath) res.resume(); // 消耗响应体,释放连接
reject(new Error("重定向次数超过3次,终止下载"));
return;
}
// 递归执行下载,重定向次数+1
downloadFile(res.headers.location, savePath, redirectCount + 1)
.then(resolve) .then(resolve)
.catch(reject); .catch(reject);
return; return;
} }
// HTTP状态码非200直接报错
if (res.statusCode !== 200) { if (res.statusCode !== 200) {
writeStream.close(); res.resume();
cleanBadFile(targetPath); reject(new Error(`HTTP状态码异常: ${res.statusCode}`));
reject(new Error(`响应状态码 ${res.statusCode}`));
return; return;
} }
// 创建写入流,通过管道将网络响应写入文件
const writeStream = fs.createWriteStream(savePath);
res.pipe(writeStream); res.pipe(writeStream);
});
// 写入完成
writeStream.on("finish", () => { writeStream.on("finish", () => {
writeStream.close(); writeStream.close(() => resolve(savePath));
resolve();
}); });
// 写入失败:销毁流并清理损坏文件
writeStream.on("error", err => { writeStream.on("error", err => {
writeStream.close(); writeStream.destroy();
cleanBadFile(targetPath); fs.promises.unlink(savePath).catch(() => {});
reject(err); reject(new Error(`文件写入失败: ${err.message}`));
});
request.on("error", err => {
writeStream.close();
cleanBadFile(targetPath);
reject(err);
}); });
request.setTimeout(30000, () => {
request.destroy(); // 响应流异常
writeStream.close(); res.on("error", err => {
cleanBadFile(targetPath); writeStream.destroy();
reject(new Error("下载超时")); fs.promises.unlink(savePath).catch(() => {});
reject(new Error(`网络响应异常: ${err.message}`));
}); });
}); });
// ========== 5. 队列消费逻辑 ========== // 请求级错误(网络不通、DNS失败等)
const consume = () => { req.on("error", err => reject(new Error(`网络请求失败: ${err.message}`)));
if (isDestroyed) return;
if (runningCount >= config.concurrency || pendingQueue.length === 0) {
runningCount === 0 && config.onIdle();
return;
}
const task = pendingQueue.shift(); // 请求超时
runningCount++; req.on("timeout", () => {
(async () => { req.destroy();
try { reject(new Error("下载超时(30s)"));
const targetDir = path.dirname(task.targetPath); });
if (!fileExists(targetDir)) { } catch (syncErr) {
fs.mkdirSync(targetDir, { recursive: true }); // 捕获URL格式错误等同步异常
} reject(new Error(`请求初始化失败: ${syncErr.message}`));
await streamDownload(task.downloadUrl, task.targetPath);
successSet.add(task.targetPath);
config.onSuccess(task);
} catch (err) {
if (task.remainRetry > 0) {
task.remainRetry--;
pendingQueue.unshift(task);
} else {
config.onError(task, err);
}
} finally {
runningCount--;
runningSet.delete(task.targetPath);
consume();
} }
})(); });
}; }
// ========== 6. 对外暴露方法 ========== // ===================== 6. 核心业务逻辑 =====================
return {
addBatchList(list = []) {
if (isDestroyed || !Array.isArray(list)) return;
// 第一步:筛选 layoutStatus = 3 或 5 的批次 /**
const targetBatch = list.filter(item => * 处理单个批次的图片下载
[3, 5].includes(item.layoutStatus) * 单张图片失败仅跳过当前文件,不影响同批次其他图片
* @param {object} item 单条排版数据(含batchArrangeNum、url字段)
*/
async function processBatchItem(item) {
const { batchArrangeNum, url: urlStr } = item;
// ---------- 基础参数校验 ----------
if (!batchArrangeNum) {
logDownloadError(
"参数无效",
batchArrangeNum,
"",
"缺少批次号batchArrangeNum"
);
return;
}
if (!urlStr || typeof urlStr !== "string") {
logDownloadError(
"参数无效",
batchArrangeNum,
urlStr,
"url字段为空或非字符串格式"
); );
const invalidBatchList = []; // 收集异常批次
const fileTasks = [];
// 第二步:逐个校验批次URL合法性
targetBatch.forEach(batch => {
const batchNo = batch.batchArrangeNum || "未知批次";
// 校验1:URL 不是字符串类型
if (typeof batch.url !== "string") {
invalidBatchList.push({
batchArrangeNum: batchNo,
reason: "下载链接格式错误,非字符串类型"
});
return; return;
} }
// 校验2:URL 为空字符串/纯空格 // ---------- 1. 创建批次保存目录 ----------
if (batch.url.trim() === "") { let saveDir;
invalidBatchList.push({ try {
batchArrangeNum: batchNo, const downloadRoot = getDTFSetting().downloadDir;
reason: "下载链接为空" if (!downloadRoot) throw new Error("下载根目录downloadDir配置为空");
});
// 目录结构:根目录/批次号:xxx/自动排版素材
saveDir = path.join(
downloadRoot,
`批次号:${batchArrangeNum}`,
"自动排版素材"
);
await ensureDir(saveDir);
} catch (dirErr) {
// 目录创建失败:整个批次跳过
logDownloadError("目录创建失败", batchArrangeNum, "", dirErr.message);
return; return;
} }
// 第三步:拆分多URL,过滤空值,校验PNG格式 // ---------- 2. 拆分图片链接 ----------
const urlList = batch.url // 支持逗号分隔的多图字符串,过滤空值与无效值
const urlList = urlStr
.split(",") .split(",")
.map(u => u.trim()) .map(u => u.trim())
.filter(Boolean); .filter(u => {
const validPngUrls = urlList.filter(url => isPngUrl(url)); if (!u || typeof u !== "string") {
logDownloadError("链接无效", batchArrangeNum, u, "空链接或非字符串");
// 校验3:拆分后无有效PNG链接 return false;
if (validPngUrls.length === 0) { }
invalidBatchList.push({ return true;
batchArrangeNum: batchNo,
reason: "无有效PNG格式下载链接"
}); });
if (urlList.length === 0) {
logDownloadError("无有效链接", batchArrangeNum, "", "拆分后无可用图片链接");
return; return;
} }
// 第四步:生成合法下载任务,去重过滤 // ---------- 3. 并发下载批次内所有图片 ----------
const dir = path.join( const imageLimit = pLimit(taskState.maxImageConcurrency);
getDTFSetting().downloadDir, const fileEnv = getHostApi().fileApiUrl;
`批次号:${batchNo}`, const downloadPromises = urlList.map(url =>
"自动排版素材" imageLimit(async () => {
); const newUrl = `${fileEnv}${url}`;
validPngUrls.forEach(url => {
const fileName = getPngFileName(url);
const targetPath = path.join(dir, fileName);
if (
!fileExists(targetPath) &&
!successSet.has(targetPath) &&
!runningSet.has(targetPath)
) {
fileTasks.push({
batchArrangeNum: batchNo,
downloadUrl: `https://factory.jomalls.com${url}`,
targetPath,
fileName,
remainRetry: config.retryTimes
});
}
});
});
// 有异常批次,通过回调抛出 try {
if (invalidBatchList.length > 0) { const fileName = getPngFileName(url);
config.onBatchInvalid(invalidBatchList); const savePath = path.join(saveDir, fileName);
}
// 启动下载队列 // 文件已存在则直接跳过,不重复下载
if (fileTasks.length > 0) { const exists = await isFileExists(savePath);
fileTasks.forEach(task => { if (exists) {
runningSet.add(task.targetPath); console.log(
pendingQueue.push(task); `[跳过下载] 文件已存在 | 批次: ${batchArrangeNum} | 文件: ${fileName}`
}); );
consume(); return;
} }
}, // 执行下载
await downloadFile(newUrl, savePath);
get isRunning() { console.log(`[下载成功] 批次: ${batchArrangeNum} | 文件: ${fileName}`);
return runningCount > 0 || pendingQueue.length > 0; } catch (downloadErr) {
}, // 单张下载失败:记录日志,跳过当前文件
logDownloadError(
destroy() { "文件下载失败",
isDestroyed = true; batchArrangeNum,
pendingQueue = []; newUrl,
runningCount = 0; downloadErr.message
successSet.clear(); );
runningSet.clear();
} }
}; })
);
// 等待当前批次所有图片处理完成
await Promise.all(downloadPromises);
} }
/** /**
* 批量下载PNG图片到指定文件夹 * 执行一次完整的定时任务
* @param {string[]} urls - PNG图片链接数组 * 流程:获取设置 → 判断策略 → 拉取列表 → 批量下载 → 更新页码
* @param {string} targetDir - 目标文件夹绝对路径
* @param {Object} [options] - 可选配置
* @param {number} [options.concurrency=2] - 同时下载数量
* @param {number} [options.timeout=30000] - 单文件超时时间(ms)
* @param {number} [options.retryTimes=1] - 单文件失败重试次数
* @returns {Promise<{success: string[], failed: {url: string, error: string}[]}>}
*/ */
export function downloadPngList(urls, targetDir, options = {}) { async function runTask() {
const config = { // 互斥锁:上一轮任务未完成则直接跳过本轮
concurrency: 2, if (taskState.isDownloading) {
timeout: 30000, console.log("当前有下载任务进行中,跳过本轮定时任务");
retryTimes: 1,
...options
};
return new Promise(resolve => {
// 参数合法性校验
if (!Array.isArray(urls) || urls.length === 0) {
resolve({
success: [],
failed: [{ url: "", error: "下载链接数组为空" }]
});
return; return;
} }
console.log(
`开始批量下载,共 ${urls.length} 个链接,目标目录:${targetDir}`
);
// 自动创建目标目录(多级递归创建,不需要可注释掉)
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
const successList = []; // 下载成功的文件路径
const failedList = []; // 下载失败的链接+原因
let currentIndex = 0; // 当前处理到第几个链接
let runningCount = 0; // 当前正在下载的数量
const total = urls.length;
// 从URL中提取PNG文件名,无后缀自动补.png
const getPngFileName = url => {
try { try {
const urlObj = new URL(url); taskState.isDownloading = true;
const name = path.basename(urlObj.pathname); console.log(`=== 开始执行定时任务,当前页码:${taskState.currentPage} ===`);
return name.toLowerCase().endsWith(".png") ? name : `${name}.png`;
} catch (e) {
return `img_${Date.now()}_${Math.random()
.toString(36)
.slice(2, 6)}.png`;
}
};
// 单文件流式下载核心 // 步骤1:调用获取设置接口
const streamDownload = (url, filePath) => { const settingRes = await api.post(pathMap["productionConfig"]);
return new Promise((resolve, reject) => {
const requestModule = url.startsWith("https:") ? https : http; const autoLayoutStrategy = settingRes.data.autoLayoutStrategy;
const writeStream = fs.createWriteStream(filePath);
const request = requestModule.get(url, res => { // 步骤2:策略判断:不等于3才继续下载
// 自动处理301/302/307/308重定向 if (autoLayoutStrategy === 3) {
if ([301, 302, 307, 308].includes(res.statusCode)) { console.log("不自动排版,跳过本次下载");
writeStream.close();
streamDownload(res.headers.location, filePath)
.then(resolve)
.catch(reject);
return;
}
if (res.statusCode !== 200) {
writeStream.close();
fs.existsSync(filePath) && fs.unlinkSync(filePath);
reject(new Error(`HTTP状态码 ${res.statusCode}`));
return; return;
} }
res.pipe(writeStream);
});
writeStream.on("finish", () => {
console.log("downloadPngList---finish");
writeStream.close(); // 步骤3:调用排版数据接口,拉取当前页数据
resolve(); const layoutRes = await api.post(pathMap["productionSoftware"], {
}); currentPage: taskState.currentPage,
writeStream.on("error", err => { pageSize: taskState.pageSize,
writeStream.close(); layoutStatus: taskState.layoutStatus
fs.existsSync(filePath) && fs.unlinkSync(filePath);
reject(err);
}); });
request.on("error", err => { console.log(layoutRes);
writeStream.close();
fs.existsSync(filePath) && fs.unlinkSync(filePath);
reject(err);
});
// 超时兜底
request.setTimeout(config.timeout, () => {
request.destroy();
writeStream.close();
fs.existsSync(filePath) && fs.unlinkSync(filePath);
reject(new Error("下载超时"));
});
});
};
// 带重试的单任务执行 // return;
const runTask = async url => { const list = layoutRes.data.records || [];
const fileName = getPngFileName(url); const total = layoutRes.data.total || 0;
const filePath = path.join(targetDir, fileName); taskState.total = total;
// 本地文件已存在,直接跳过 if (list.length === 0) {
if (fs.existsSync(filePath)) { console.log("当前页无数据,本轮任务结束");
successList.push(filePath);
return; return;
} }
console.log(`开始下载:${fileName}`);
let remainRetry = config.retryTimes;
try { // 步骤4:多批次并行下载
await streamDownload(url, filePath); const batchLimit = pLimit(taskState.maxBatchConcurrency);
console.log(`下载成功:${fileName}`); const batchPromises = list.map(item =>
successList.push(filePath); batchLimit(() => processBatchItem(item))
} catch (err) { );
if (remainRetry > 0) { await Promise.all(batchPromises);
remainRetry--;
} else { console.log(`=== 第 ${taskState.currentPage} 页全部处理完成 ===`);
failedList.push({ url, error: err.message });
// 步骤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. 对外暴露的启停方法 =====================
const next = () => {
// 并发数未满且还有未处理任务,继续启动
while (runningCount < config.concurrency && currentIndex < total) {
runningCount++;
const url = urls[currentIndex].trim();
currentIndex++;
runTask(url).finally(() => {
runningCount--;
next();
});
}
// 所有任务执行完毕,返回结果 /**
if (runningCount === 0 && currentIndex >= total) { * 启动自动下载定时任务
console.log( * 调用后立即启动定时器,5分钟后首次执行任务
`全部任务执行完成,成功:${successList.length} 个,失败:${failedList.length} 个` */
); function startAutoDownloadTask() {
resolve({ success: successList, failed: failedList }); // 防止重复启动
} if (taskState.timer) return;
};
next(); console.log("自动下载定时任务已启动,5分钟后首次执行");
}); taskState.timer = setInterval(runTask, taskState.interval);
} }
/**
* 停止自动下载定时任务
* 窗口关闭、应用退出时调用,清理定时器与状态
*/
function stopAutoDownloadTask() {
if (taskState.timer) {
clearInterval(taskState.timer);
taskState.timer = null;
taskState.isDownloading = false;
console.log("自动下载定时任务已停止");
}
}
// ===================== 8. 模块导出 =====================
export default {
startAutoDownloadTask,
stopAutoDownloadTask
};
import axios from "axios";
import Store from "electron-store"; // 主进程用 electron-store 存取用户信息,和渲染进程的 $dataStore 数据互通
const dataStore = new Store();
// 纯请求实例:无任何 UI 依赖,仅处理 token 注入和错误格式化
const service = axios.create({
baseURL: "http://localhost:3000",
timeout: 12600000
});
// 请求拦截:仅注入 token
service.interceptors.request.use(
config => {
const user = dataStore.get("user");
if (user.token) {
config.headers["jwt-token"] = user.token;
}
return config;
},
error => Promise.reject(error)
);
// 响应拦截:仅处理数据和错误信息,不做任何 UI 操作
service.interceptors.response.use(
response => response.data,
error => {
let errMsg = "请求失败,请稍后重试";
if (error.response.data) {
const data = error.response.data;
errMsg =
data.msg || data.message || `请求错误(${error.response.status})`;
} else if (error.message) {
errMsg = error.message.includes("timeout")
? "请求超时,请检查网络"
: error.message;
}
return Promise.reject(new Error(errMsg));
}
);
export default service;
...@@ -179,10 +179,7 @@ export default { ...@@ -179,10 +179,7 @@ export default {
<div class="page-header"> <div class="page-header">
<el-form style="display: flex;flex-wrap: nowrap;" inline> <el-form style="display: flex;flex-wrap: nowrap;" inline>
<el-form-item> <el-form-item>
<el-button <el-button icon="el-icon-arrow-left" @click="goBack"
icon="el-icon-arrow-left"
@click="goBack"
style="margin-right:12px;"
>返回</el-button >返回</el-button
></el-form-item ></el-form-item
> >
...@@ -194,7 +191,12 @@ export default { ...@@ -194,7 +191,12 @@ export default {
></el-input ></el-input
></el-form-item> ></el-form-item>
<el-form-item label="操作单状态"> <el-form-item label="操作单状态">
<el-select v-model="searchForm.status" placeholder="请选择" clearable> <el-select
style="width: 120px;"
v-model="searchForm.status"
placeholder="请选择"
clearable
>
<el-option <el-option
v-for="(value, label) in statusMap" v-for="(value, label) in statusMap"
:key="value" :key="value"
...@@ -203,7 +205,12 @@ export default { ...@@ -203,7 +205,12 @@ export default {
></el-option> </el-select ></el-option> </el-select
></el-form-item> ></el-form-item>
<el-form-item label="操作单排版状态"> <el-form-item label="操作单排版状态">
<el-select v-model="searchForm.layout" placeholder="请选择" clearable> <el-select
v-model="searchForm.layout"
placeholder="请选择"
clearable
style="width: 120px;"
>
<el-option label="排版成功" :value="true"></el-option> <el-option label="排版成功" :value="true"></el-option>
<el-option label="排版失败" :value="false"> </el-option> </el-select <el-option label="排版失败" :value="false"> </el-option> </el-select
></el-form-item> ></el-form-item>
......
<script> <script>
import BaseTable from "../../components/BaseTable.vue"; import BaseTable from "../../components/BaseTable.vue";
const { remote, ipcRenderer } = require("electron"); const { remote, ipcRenderer } = require("electron");
const { getDTFSetting } = require("@/server/utils/store"); const { getDTFSetting, setDTFSetting } = require("@/server/utils/store");
const { Menu } = remote; const { Menu } = remote;
const path = require("path"); const path = require("path");
const { pathMap } = require("@/config/index.js"); const { pathMap } = require("@/config/index.js");
...@@ -26,7 +26,7 @@ export default { ...@@ -26,7 +26,7 @@ export default {
searchForm: {}, searchForm: {},
radio: "", radio: "",
templateBatchArrangeNum: "", templateBatchArrangeNum: "",
pollTimer: null,
dtfSetting: getDTFSetting(), dtfSetting: getDTFSetting(),
isFetching: false, isFetching: false,
pageInfo: { pageInfo: {
...@@ -107,63 +107,25 @@ export default { ...@@ -107,63 +107,25 @@ export default {
}; };
}, },
mounted() { mounted() {
this.loadLocalConfig();
this.fetchBatchList(); this.fetchBatchList();
this.pollTimer = setInterval(() => this.fetchBatchList(), 300000);
// 单文件下载成功
ipcRenderer.on("download:file-success", (_, data) => {
console.log(`批次 ${data.batchArrangeNum}${data.fileName} 下载完成`);
});
// 单文件下载失败
ipcRenderer.on("download:file-error", (_, data) => {
this.$message.error(
`批次 ${data.batchArrangeNum}${data.fileName} 下载失败,${data.msg}`
);
});
// 新增:批次校验失败,批量提示
ipcRenderer.on("download:batch-invalid", (_, invalidList) => {
const msg = invalidList
.map(item => `【${item.batchArrangeNum}${item.reason}`)
.join(";");
this.$message.warning(`存在异常批次:${msg}`);
});
},
beforeDestroy() {
if (this.pollTimer) {
clearInterval(this.pollTimer);
this.pollTimer = null;
}
ipcRenderer.removeAllListeners("download:status-reply");
ipcRenderer.removeAllListeners("download:file-success");
ipcRenderer.removeAllListeners("download:file-error");
ipcRenderer.removeAllListeners("download:batch-invalid");
ipcRenderer.removeAllListeners("download:all-finish");
}, },
methods: { methods: {
getDownloadStatus() { async loadLocalConfig() {
return new Promise(resolve => { try {
// 只监听一次回复,避免重复绑定 if (!getDTFSetting().downloadDir) {
ipcRenderer.once("download:status-reply", (_, status) => { const path = ipcRenderer.sendSync("get-install-path");
resolve(status); setDTFSetting({ downloadDir: path });
}); }
// 发送请求 } catch (error) {
ipcRenderer.send("download:get-status"); console.log(error);
}); }
}, },
//定时任务 //定时任务
async fetchBatchList(value) { async fetchBatchList(value) {
// 先判断:有下载任务在跑,本轮直接跳过
// console.log("this.isFetching", this.isFetching);
if (this.isFetching) return;
this.isFetching = true;
try { try {
const status = await this.getDownloadStatus();
// console.log("status", status);
if (status.isRunning) return;
let params = { let params = {
currentPage: this.pageInfo.currentPage, currentPage: this.pageInfo.currentPage,
pageSize: this.pageInfo.pageSize pageSize: this.pageInfo.pageSize
...@@ -179,12 +141,9 @@ export default { ...@@ -179,12 +141,9 @@ export default {
this.tableList = res.data.records || []; this.tableList = res.data.records || [];
this.pageInfo.total = res.data.total; this.pageInfo.total = res.data.total;
ipcRenderer.sendSync("download:submit-batch-list", this.tableList);
} }
} catch (err) { } catch (err) {
console.error("批次列表拉取失败", err); console.error("批次列表拉取失败", err);
} finally {
this.isFetching = false;
} }
}, },
...@@ -195,7 +154,7 @@ export default { ...@@ -195,7 +154,7 @@ export default {
handleSizeChange(size) { handleSizeChange(size) {
this.pageInfo.pageSize = size; this.pageInfo.pageSize = size;
this.pageInfo.currentPage = 1; this.pageInfo.currentPage = 1;
this.loadData(); this.settingRes();
}, },
handleCurrentChange(page) { handleCurrentChange(page) {
this.pageInfo.currentPage = page; this.pageInfo.currentPage = page;
...@@ -245,6 +204,7 @@ export default { ...@@ -245,6 +204,7 @@ export default {
[3, 5].includes(value.layoutStatus) [3, 5].includes(value.layoutStatus)
) { ) {
this.dialogVisible = true; this.dialogVisible = true;
this.radio = "";
this.templateBatchArrangeNum = value.batchArrangeNum; this.templateBatchArrangeNum = value.batchArrangeNum;
} else { } else {
this.$message( this.$message(
...@@ -305,15 +265,18 @@ export default { ...@@ -305,15 +265,18 @@ export default {
const { data } = await this.$api.post(pathMap["listMaterialsByBatch"], { const { data } = await this.$api.post(pathMap["listMaterialsByBatch"], {
batchArrangeNum: value batchArrangeNum: value
}); });
const newUrls = data.flatMap(el => el.urls);
// console.log(311, newUrls);
// return;
const res = await this.$api.post("/downloadMaterial", { const res = await this.$api.post("/downloadMaterial", {
urls: data.urls, urls: newUrls,
targetPath: path.join( targetPath: path.join(
getDTFSetting().downloadDir, getDTFSetting().downloadDir,
`批次号:${value}`, `批次号:${value}`,
"原素材" "原素材"
) )
}); });
console.log("listMaterialsByBatch-", data); // console.log("listMaterialsByBatch-", data);
console.log("downloadMaterial", res); console.log("downloadMaterial", res);
this.$message("下载进行中,下载完成的素材将在文件夹原素材中显示"); this.$message("下载进行中,下载完成的素材将在文件夹原素材中显示");
......
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