Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
electron-printer
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
1
Merge Requests
1
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
zhuzhequan
electron-printer
Commits
26c55257
Commit
26c55257
authored
Jul 09, 2026
by
linjinhong
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat:修改定时任务方法
parent
98eefb15
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
434 additions
and
468 deletions
+434
-468
src/background.js
+19
-56
src/server/entity/function.js
+1
-1
src/utils/download-queue.js
+338
-347
src/utils/request.js
+42
-0
src/views/dtf-batch-detail/components/detailHead.vue
+13
-6
src/views/dtf-list/index.vue
+21
-58
No files found.
src/background.js
View file @
26c55257
...
...
@@ -5,7 +5,7 @@ import { createProtocol } from "vue-cli-plugin-electron-builder/lib";
import
{
createServer
}
from
"@/server/index.js"
;
import
{
autoUpdater
}
from
"electron-updater"
;
const
{
createBatchDownloadQueue
}
=
require
(
"@/utils/download-queue"
)
;
import
downloadQueue
from
"@/utils/download-queue"
;
const
fs
=
require
(
"fs"
);
import
path
from
"path"
;
// 引入 path 模块
...
...
@@ -30,36 +30,6 @@ const winURL =
?
"http://localhost:8050"
:
`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
()
{
win
=
new
BrowserWindow
({
width
:
1500
,
...
...
@@ -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
)
{
await
win
.
loadURL
(
process
.
env
.
WEBPACK_DEV_SERVER_URL
);
if
(
!
process
.
env
.
IS_TEST
)
win
.
webContents
.
openDevTools
();
...
...
@@ -508,17 +487,7 @@ async function createWindow() {
win
.
on
(
"resize"
,
()
=>
{
const
{
width
,
height
}
=
win
.
getBounds
();
win
.
webContents
.
send
(
"window-size"
,
{
width
,
height
});
});
// 监听窗口的 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
:
"已处理"
};
console
.
log
(
888
);
});
ipcMain
.
on
(
"get-install-path"
,
event
=>
{
...
...
@@ -542,18 +511,9 @@ async function createWindow() {
}
});
ipcMain
.
on
(
"download:get-status"
,
event
=>
{
try
{
// 无论正常异常都保证回复,避免前端挂起
event
.
reply
(
"download:status-reply"
,
{
isRunning
:
downloadQueue
.
isRunning
});
}
catch
(
err
)
{
event
.
reply
(
"download:status-reply"
,
{
isRunning
:
false
,
error
:
err
.
message
});
}
// ========== 新增3:窗口关闭时停止定时任务 ==========
win
.
on
(
"closed"
,
()
=>
{
downloadQueue
.
stopAutoDownloadTask
();
});
}
...
...
@@ -587,8 +547,11 @@ app.on("will-quit", () => {
globalShortcut
.
unregister
(
"F5"
);
globalShortcut
.
unregister
(
"CommandOrControl+R"
);
});
app
.
on
(
"before-quit"
,
()
=>
{
downloadQueue
.
destroy
();
app
.
on
(
"window-all-closed"
,
()
=>
{
downloadQueue
.
stopAutoDownloadTask
();
if
(
process
.
platform
!==
"darwin"
)
{
app
.
quit
();
}
});
if
(
isDevelopment
)
{
...
...
src/server/entity/function.js
View file @
26c55257
...
...
@@ -75,7 +75,7 @@ function readEnv() {
fileEnv
=
config
.
fileApiUrl
;
env
=
config
.
apiApiHost
;
visionUrl
=
config
.
visionUrl
;
console
.
log
(
"配置加载完成:"
,
{
fileEnv
,
env
,
visionUrl
});
//
console.log("配置加载完成:", { fileEnv, env, visionUrl });
}
catch
(
err
)
{
console
.
error
(
"读取配置文件失败:"
,
err
);
...
...
src/utils/download-queue.js
View file @
26c55257
const
fs
=
require
(
"fs"
);
// ===================== 1. 依赖引入(最顶层,按依赖顺序) =====================
const
path
=
require
(
"path"
);
const
fs
=
require
(
"fs"
);
const
http
=
require
(
"http"
);
const
https
=
require
(
"https"
);
const
{
URL
}
=
require
(
"url"
);
const
{
getDTFSetting
}
=
require
(
"@/server/utils/store"
);
export
function
createBatchDownloadQueue
(
options
=
{})
{
// ========== 1. 配置项 ==========
const
config
=
Object
.
assign
(
{
concurrency
:
3
,
retryTimes
:
2
,
onSuccess
:
()
=>
{},
// 单文件下载成功
onError
:
()
=>
{},
// 单文件下载失败
onBatchInvalid
:
()
=>
{},
// 批次校验失败回调(返回异常批次列表+原因)
onIdle
:
()
=>
{}
// 全部任务完成
},
options
);
// ========== 2. 内部状态 ==========
let
pendingQueue
=
[];
let
runningCount
=
0
;
const
successSet
=
new
Set
();
const
runningSet
=
new
Set
();
let
isDestroyed
=
false
;
const
{
pathMap
}
=
require
(
"@/config/index.js"
);
const
{
getDTFSetting
,
getHostApi
}
=
require
(
"@/server/utils/store"
);
import
api
from
"./request"
;
// ===================== 2. 全局状态与可配置参数 =====================
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,生产环境安全稳定
};
// ===================== 3. 基础工具函数 =====================
// ========== 3. 通用工具方法 ==========
const
getRequestModule
=
url
=>
{
try
{
return
new
URL
(
url
).
protocol
===
"https:"
?
https
:
http
;
}
catch
(
e
)
{
return
https
;
}
};
/**
* 统一错误日志格式化输出
* @param {string} type 错误类型
* @param {string} batchNo 批次号
* @param {string} url 出错的图片链接
* @param {string} message 错误详情
*/
function
logDownloadError
(
type
,
batchNo
,
url
,
message
)
{
console
.
error
(
`[自动下载失败][
${
type
}
] 批次号:
${
batchNo
||
"未知"
}
| 链接:
${
url
||
"-"
}
| 详情:
${
message
}
`
);
}
const
isPngUrl
=
url
=>
{
try
{
const
urlObj
=
new
URL
(
url
);
return
urlObj
.
pathname
.
toLowerCase
().
endsWith
(
".png"
);
}
catch
(
e
)
{
return
false
;
/**
* 简易并发限制器(无第三方依赖,兼容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
();
// 任务完成后自动启动下一个
});
}
};
const
getPngFileName
=
url
=>
{
try
{
const
urlObj
=
new
URL
(
url
);
const
name
=
path
.
basename
(
urlObj
.
pathname
);
return
name
.
toLowerCase
().
endsWith
(
".png"
)
?
name
:
`
${
name
||
`img_
${
Date
.
now
()}
`
}
.png`
;
}
catch
(
e
)
{
return
`img_
${
Date
.
now
()}
.png`
;
}
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
;
}
}
const
fileExists
=
targetPath
=>
fs
.
existsSync
(
targetPath
);
const
cleanBadFile
=
targetPath
=>
fileExists
(
targetPath
)
&&
fs
.
unlinkSync
(
targetPath
);
// ===================== 4. 核心下载能力 =====================
// ========== 4. 流式下载核心 ==========
const
streamDownload
=
(
url
,
targetPath
)
=>
new
Promise
((
resolve
,
reject
)
=>
{
const
requestModule
=
getRequestModule
(
url
);
const
writeStream
=
fs
.
createWriteStream
(
targetPath
);
const
request
=
requestModule
.
get
(
url
,
res
=>
{
/**
* 单文件流式下载(全错误捕获 + 重定向控制)
* 流式写入:边下载边写盘,内存占用极低,不阻塞主线程
* @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
))
{
writeStream
.
close
();
streamDownload
(
res
.
headers
.
location
,
targetPath
)
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
)
{
writeStream
.
close
();
cleanBadFile
(
targetPath
);
reject
(
new
Error
(
`响应状态码
${
res
.
statusCode
}
`
));
res
.
resume
();
reject
(
new
Error
(
`HTTP状态码异常:
${
res
.
statusCode
}
`
));
return
;
}
// 创建写入流,通过管道将网络响应写入文件
const
writeStream
=
fs
.
createWriteStream
(
savePath
);
res
.
pipe
(
writeStream
);
});
writeStream
.
on
(
"finish"
,
()
=>
{
writeStream
.
close
();
resolve
();
});
writeStream
.
on
(
"error"
,
err
=>
{
writeStream
.
close
();
cleanBadFile
(
targetPath
);
reject
(
err
);
});
request
.
on
(
"error"
,
err
=>
{
writeStream
.
close
();
cleanBadFile
(
targetPath
);
reject
(
err
);
});
request
.
setTimeout
(
30000
,
()
=>
{
request
.
destroy
();
writeStream
.
close
();
cleanBadFile
(
targetPath
);
reject
(
new
Error
(
"下载超时"
));
// 写入完成
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
}
`
));
});
});
});
// ========== 5. 队列消费逻辑 ==========
const
consume
=
()
=>
{
if
(
isDestroyed
)
return
;
if
(
runningCount
>=
config
.
concurrency
||
pendingQueue
.
length
===
0
)
{
runningCount
===
0
&&
config
.
onIdle
();
return
;
// 请求级错误(网络不通、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
}
`
));
}
});
}
const
task
=
pendingQueue
.
shift
();
runningCount
++
;
(
async
()
=>
{
try
{
const
targetDir
=
path
.
dirname
(
task
.
targetPath
);
if
(
!
fileExists
(
targetDir
))
{
fs
.
mkdirSync
(
targetDir
,
{
recursive
:
true
});
}
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
;
/**
* 处理单个批次的图片下载
* 单张图片失败仅跳过当前文件,不影响同批次其他图片
* @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字段为空或非字符串格式"
);
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
;
});
// 第一步:筛选 layoutStatus = 3 或 5 的批次
const
targetBatch
=
list
.
filter
(
item
=>
[
3
,
5
].
includes
(
item
.
layoutStatus
)
);
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
;
}
if
(
urlList
.
length
===
0
)
{
logDownloadError
(
"无有效链接"
,
batchArrangeNum
,
""
,
"拆分后无可用图片链接"
);
return
;
}
// 校验2:URL 为空字符串/纯空格
if
(
batch
.
url
.
trim
()
===
""
)
{
invalidBatchList
.
push
({
batchArrangeNum
:
batchNo
,
reason
:
"下载链接为空"
});
return
;
}
// ---------- 3. 并发下载批次内所有图片 ----------
const
imageLimit
=
pLimit
(
taskState
.
maxImageConcurrency
);
const
fileEnv
=
getHostApi
().
fileApiUrl
;
const
downloadPromises
=
urlList
.
map
(
url
=>
imageLimit
(
async
()
=>
{
const
newUrl
=
`
${
fileEnv
}${
url
}
`
;
// 第三步:拆分多URL,过滤空值,校验PNG格式
const
urlList
=
batch
.
url
.
split
(
","
)
.
map
(
u
=>
u
.
trim
())
.
filter
(
Boolean
);
const
validPngUrls
=
urlList
.
filter
(
url
=>
isPngUrl
(
url
));
// 校验3:拆分后无有效PNG链接
if
(
validPngUrls
.
length
===
0
)
{
invalidBatchList
.
push
({
batchArrangeNum
:
batchNo
,
reason
:
"无有效PNG格式下载链接"
});
try
{
const
fileName
=
getPngFileName
(
url
);
const
savePath
=
path
.
join
(
saveDir
,
fileName
);
// 文件已存在则直接跳过,不重复下载
const
exists
=
await
isFileExists
(
savePath
);
if
(
exists
)
{
console
.
log
(
`[跳过下载] 文件已存在 | 批次:
${
batchArrangeNum
}
| 文件:
${
fileName
}
`
);
return
;
}
// 第四步:生成合法下载任务,去重过滤
const
dir
=
path
.
join
(
getDTFSetting
().
downloadDir
,
`批次号:
${
batchNo
}
`
,
"自动排版素材"
// 执行下载
await
downloadFile
(
newUrl
,
savePath
);
console
.
log
(
`[下载成功] 批次:
${
batchArrangeNum
}
| 文件:
${
fileName
}
`
);
}
catch
(
downloadErr
)
{
// 单张下载失败:记录日志,跳过当前文件
logDownloadError
(
"文件下载失败"
,
batchArrangeNum
,
newUrl
,
downloadErr
.
message
);
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
});
}
});
});
// 有异常批次,通过回调抛出
if
(
invalidBatchList
.
length
>
0
)
{
config
.
onBatchInvalid
(
invalidBatchList
);
}
})
);
// 启动下载队列
if
(
fileTasks
.
length
>
0
)
{
fileTasks
.
forEach
(
task
=>
{
runningSet
.
add
(
task
.
targetPath
);
pendingQueue
.
push
(
task
);
});
consume
();
}
},
get
isRunning
()
{
return
runningCount
>
0
||
pendingQueue
.
length
>
0
;
},
destroy
()
{
isDestroyed
=
true
;
pendingQueue
=
[];
runningCount
=
0
;
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
=
{})
{
const
config
=
{
concurrency
:
2
,
timeout
:
30000
,
retryTimes
:
1
,
...
options
};
async
function
runTask
()
{
// 互斥锁:上一轮任务未完成则直接跳过本轮
if
(
taskState
.
isDownloading
)
{
console
.
log
(
"当前有下载任务进行中,跳过本轮定时任务"
);
return
;
}
return
new
Promise
(
resolve
=>
{
// 参数合法性校验
if
(
!
Array
.
isArray
(
urls
)
||
urls
.
length
===
0
)
{
resolve
({
success
:
[],
failed
:
[{
url
:
""
,
error
:
"下载链接数组为空"
}]
});
try
{
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
)
{
console
.
log
(
"不自动排版,跳过本次下载"
);
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
;
// 步骤3:调用排版数据接口,拉取当前页数据
const
layoutRes
=
await
api
.
post
(
pathMap
[
"productionSoftware"
],
{
currentPage
:
taskState
.
currentPage
,
pageSize
:
taskState
.
pageSize
,
layoutStatus
:
taskState
.
layoutStatus
});
console
.
log
(
layoutRes
);
// 从URL中提取PNG文件名,无后缀自动补.png
const
getPngFileName
=
url
=>
{
try
{
const
urlObj
=
new
URL
(
url
);
const
name
=
path
.
basename
(
urlObj
.
pathname
);
return
name
.
toLowerCase
().
endsWith
(
".png"
)
?
name
:
`
${
name
}
.png`
;
}
catch
(
e
)
{
return
`img_
${
Date
.
now
()}
_
${
Math
.
random
()
.
toString
(
36
)
.
slice
(
2
,
6
)}
.png`
;
}
};
// 单文件流式下载核心
const
streamDownload
=
(
url
,
filePath
)
=>
{
return
new
Promise
((
resolve
,
reject
)
=>
{
const
requestModule
=
url
.
startsWith
(
"https:"
)
?
https
:
http
;
const
writeStream
=
fs
.
createWriteStream
(
filePath
);
const
request
=
requestModule
.
get
(
url
,
res
=>
{
// 自动处理301/302/307/308重定向
if
([
301
,
302
,
307
,
308
].
includes
(
res
.
statusCode
))
{
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
;
}
res
.
pipe
(
writeStream
);
});
// return;
const
list
=
layoutRes
.
data
.
records
||
[];
const
total
=
layoutRes
.
data
.
total
||
0
;
taskState
.
total
=
total
;
writeStream
.
on
(
"finish"
,
()
=>
{
console
.
log
(
"downloadPngList---finish"
);
if
(
list
.
length
===
0
)
{
console
.
log
(
"当前页无数据,本轮任务结束"
);
return
;
}
writeStream
.
close
();
resolve
();
});
writeStream
.
on
(
"error"
,
err
=>
{
writeStream
.
close
();
fs
.
existsSync
(
filePath
)
&&
fs
.
unlinkSync
(
filePath
);
reject
(
err
);
});
request
.
on
(
"error"
,
err
=>
{
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
(
"下载超时"
));
});
});
};
// 步骤4:多批次并行下载
const
batchLimit
=
pLimit
(
taskState
.
maxBatchConcurrency
);
const
batchPromises
=
list
.
map
(
item
=>
batchLimit
(()
=>
processBatchItem
(
item
))
);
await
Promise
.
all
(
batchPromises
);
// 带重试的单任务执行
const
runTask
=
async
url
=>
{
const
fileName
=
getPngFileName
(
url
);
const
filePath
=
path
.
join
(
targetDir
,
fileName
);
console
.
log
(
`=== 第
${
taskState
.
currentPage
}
页全部处理完成 ===`
);
// 本地文件已存在,直接跳过
if
(
fs
.
existsSync
(
filePath
))
{
successList
.
push
(
filePath
);
return
;
}
console
.
log
(
`开始下载:
${
fileName
}
`
);
let
remainRetry
=
config
.
retryTimes
;
// 步骤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
(
"本轮任务结束,锁已释放"
);
}
}
try
{
await
streamDownload
(
url
,
filePath
);
console
.
log
(
`下载成功:
${
fileName
}
`
);
successList
.
push
(
filePath
);
}
catch
(
err
)
{
if
(
remainRetry
>
0
)
{
remainRetry
--
;
}
else
{
failedList
.
push
({
url
,
error
:
err
.
message
});
}
}
};
// 并发调度器
const
next
=
()
=>
{
// 并发数未满且还有未处理任务,继续启动
while
(
runningCount
<
config
.
concurrency
&&
currentIndex
<
total
)
{
runningCount
++
;
const
url
=
urls
[
currentIndex
].
trim
();
currentIndex
++
;
runTask
(
url
).
finally
(()
=>
{
runningCount
--
;
next
();
});
}
// ===================== 7. 对外暴露的启停方法 =====================
// 所有任务执行完毕,返回结果
if
(
runningCount
===
0
&&
currentIndex
>=
total
)
{
console
.
log
(
`全部任务执行完成,成功:
${
successList
.
length
}
个,失败:
${
failedList
.
length
}
个`
);
resolve
({
success
:
successList
,
failed
:
failedList
});
}
};
/**
* 启动自动下载定时任务
* 调用后立即启动定时器,5分钟后首次执行任务
*/
function
startAutoDownloadTask
()
{
// 防止重复启动
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
};
src/utils/request.js
0 → 100644
View file @
26c55257
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
;
src/views/dtf-batch-detail/components/detailHead.vue
View file @
26c55257
...
...
@@ -179,10 +179,7 @@ export default {
<div
class=
"page-header"
>
<el-form
style=
"display: flex;flex-wrap: nowrap;"
inline
>
<el-form-item>
<el-button
icon=
"el-icon-arrow-left"
@
click=
"goBack"
style=
"margin-right:12px;"
<el-button
icon=
"el-icon-arrow-left"
@
click=
"goBack"
>
返回
</el-button
></el-form-item
>
...
...
@@ -194,7 +191,12 @@ export default {
></el-input
></el-form-item>
<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
v-for=
"(value, label) in statusMap"
:key=
"value"
...
...
@@ -203,7 +205,12 @@ export default {
></el-option>
</el-select
></el-form-item>
<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=
"false"
>
</el-option>
</el-select
></el-form-item>
...
...
src/views/dtf-list/index.vue
View file @
26c55257
<
script
>
import
BaseTable
from
"../../components/BaseTable.vue"
;
const
{
remote
,
ipcRenderer
}
=
require
(
"electron"
);
const
{
getDTFSetting
}
=
require
(
"@/server/utils/store"
);
const
{
getDTFSetting
,
setDTFSetting
}
=
require
(
"@/server/utils/store"
);
const
{
Menu
}
=
remote
;
const
path
=
require
(
"path"
);
const
{
pathMap
}
=
require
(
"@/config/index.js"
);
...
...
@@ -26,7 +26,7 @@ export default {
searchForm
:
{},
radio
:
""
,
templateBatchArrangeNum
:
""
,
pollTimer
:
null
,
dtfSetting
:
getDTFSetting
(),
isFetching
:
false
,
pageInfo
:
{
...
...
@@ -107,63 +107,25 @@ export default {
};
},
mounted
()
{
this
.
loadLocalConfig
();
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
:
{
getDownloadStatus
()
{
return
new
Promise
(
resolve
=>
{
// 只监听一次回复,避免重复绑定
ipcRenderer
.
once
(
"download:status-reply"
,
(
_
,
status
)
=>
{
resolve
(
status
);
}
);
// 发送请求
ipcRenderer
.
send
(
"download:get-status"
);
}
);
async
loadLocalConfig
()
{
try
{
if
(
!
getDTFSetting
().
downloadDir
)
{
const
path
=
ipcRenderer
.
sendSync
(
"get-install-path"
);
setDTFSetting
({
downloadDir
:
path
}
);
}
}
catch
(
error
)
{
console
.
log
(
error
);
}
},
//定时任务
async
fetchBatchList
(
value
)
{
// 先判断:有下载任务在跑,本轮直接跳过
// console.log("this.isFetching", this.isFetching);
if
(
this
.
isFetching
)
return
;
this
.
isFetching
=
true
;
try
{
const
status
=
await
this
.
getDownloadStatus
();
// console.log("status", status);
if
(
status
.
isRunning
)
return
;
let
params
=
{
currentPage
:
this
.
pageInfo
.
currentPage
,
pageSize
:
this
.
pageInfo
.
pageSize
...
...
@@ -179,12 +141,9 @@ export default {
this
.
tableList
=
res
.
data
.
records
||
[];
this
.
pageInfo
.
total
=
res
.
data
.
total
;
ipcRenderer
.
sendSync
(
"download:submit-batch-list"
,
this
.
tableList
);
}
}
catch
(
err
)
{
console
.
error
(
"批次列表拉取失败"
,
err
);
}
finally
{
this
.
isFetching
=
false
;
}
},
...
...
@@ -195,7 +154,7 @@ export default {
handleSizeChange
(
size
)
{
this
.
pageInfo
.
pageSize
=
size
;
this
.
pageInfo
.
currentPage
=
1
;
this
.
loadData
();
this
.
settingRes
();
},
handleCurrentChange
(
page
)
{
this
.
pageInfo
.
currentPage
=
page
;
...
...
@@ -245,6 +204,7 @@ export default {
[
3
,
5
].
includes
(
value
.
layoutStatus
)
)
{
this
.
dialogVisible
=
true
;
this
.
radio
=
""
;
this
.
templateBatchArrangeNum
=
value
.
batchArrangeNum
;
}
else
{
this
.
$message
(
...
...
@@ -305,15 +265,18 @@ export default {
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"
,
{
urls
:
data
.
u
rls
,
urls
:
newU
rls
,
targetPath
:
path
.
join
(
getDTFSetting
().
downloadDir
,
`批次号:
${
value
}
`
,
"原素材"
)
});
console
.
log
(
"listMaterialsByBatch-"
,
data
);
//
console.log("listMaterialsByBatch-", data);
console
.
log
(
"downloadMaterial"
,
res
);
this
.
$message
(
"下载进行中,下载完成的素材将在文件夹原素材中显示"
);
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment