完善删除品牌asin
This commit is contained in:
86005
aiimage-backend.log
86005
aiimage-backend.log
File diff suppressed because it is too large
Load Diff
4
app/.env
4
app/.env
@@ -7,7 +7,7 @@ proxy_url=https://api.jikip.com/ip-get?num=1&minute=1&format=json&area=all&proto
|
||||
proxy_mode=2
|
||||
|
||||
client_name=NanriAI
|
||||
java_api_base=http://8.136.19.173:18080
|
||||
# java_api_base=http://127.0.0.1:18080
|
||||
# java_api_base=http://8.136.19.173:18080
|
||||
java_api_base=http://127.0.0.1:18080
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -18,8 +18,8 @@ proxy_url = os.getenv("proxy_url")
|
||||
proxy_mode = int(os.getenv("proxy_mode",1))
|
||||
|
||||
client_name=os.getenv("client_name") + ".exe"
|
||||
JAVA_API_BASE = os.getenv("java_api_base", "http://8.136.19.173:18080")
|
||||
# JAVA_API_BASE = os.getenv("java_api_base", "http://127.0.0.1:18080")
|
||||
# JAVA_API_BASE = os.getenv("java_api_base", "http://8.136.19.173:18080")
|
||||
JAVA_API_BASE = os.getenv("java_api_base", "http://127.0.0.1:18080")
|
||||
|
||||
cache_path = "./user_data"
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<script type="module" crossorigin src="/assets/delete-brand.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-C8Bik-Sc.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-MkjZQlBu.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/delete-brand-CWSVtCqj.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/delete-brand-BRIDBaoM.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>亚马逊 - 南日AI</title>
|
||||
<title>亚马逊 - 数富AI</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: "Microsoft YaHei", "Noto Serif SC", "SimSun", "Songti SC", "Times New Roman", serif;
|
||||
background: rgba(13, 13, 13, 1);
|
||||
@@ -20,7 +14,6 @@
|
||||
height: 100vh;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
height: 56px;
|
||||
display: flex;
|
||||
@@ -29,19 +22,12 @@
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid #2a2a2a;
|
||||
}
|
||||
|
||||
.logo-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.top-bar .app-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.top-bar .app-name { font-size: 18px; font-weight: 600; color: #fff; }
|
||||
.btn-home {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
@@ -50,11 +36,7 @@
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-home:hover {
|
||||
color: #fff;
|
||||
background: #2a2a2a;
|
||||
}
|
||||
.btn-home:hover { color: #fff; background: #2a2a2a; }
|
||||
|
||||
.nav-tabs {
|
||||
display: flex;
|
||||
@@ -149,7 +131,6 @@
|
||||
color: #3498db;
|
||||
background: rgba(52, 152, 219, 0.2);
|
||||
}
|
||||
|
||||
.top-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -160,7 +141,6 @@
|
||||
display: flex;
|
||||
height: calc(100vh - 56px);
|
||||
}
|
||||
|
||||
/* 栏目内容容器:每个栏目一个 .tab-panel,通过 data-panel 与导航对应 */
|
||||
.tab-panel {
|
||||
display: none;
|
||||
@@ -168,11 +148,9 @@
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.tab-panel.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
width: 380px;
|
||||
background: #1e1e1e;
|
||||
@@ -180,7 +158,6 @@
|
||||
overflow-y: auto;
|
||||
border-right: 1px solid #2a2a2a;
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -188,13 +165,11 @@
|
||||
min-width: 0;
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
color: #bbb;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.upload-zone {
|
||||
border: 1px dashed #3a3a3a;
|
||||
border-radius: 10px;
|
||||
@@ -204,25 +179,9 @@
|
||||
margin-bottom: 20px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.upload-zone:hover {
|
||||
border-color: #3498db;
|
||||
background: #2a2a2a;
|
||||
}
|
||||
|
||||
.upload-zone .hint {
|
||||
color: #888;
|
||||
font-size: 13px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.upload-zone .btns {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.upload-zone:hover { border-color: #3498db; background: #2a2a2a; }
|
||||
.upload-zone .hint { color: #888; font-size: 13px; margin-bottom: 12px; }
|
||||
.upload-zone .btns { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
|
||||
.opt-btn {
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
@@ -233,14 +192,7 @@
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.opt-btn:hover {
|
||||
color: #fff;
|
||||
background: #333;
|
||||
border-color: #3498db;
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
.opt-btn:hover { color: #fff; background: #333; border-color: #3498db; color: #3498db; }
|
||||
.selected-files {
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
@@ -248,17 +200,8 @@
|
||||
max-height: 72px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.selected-files span {
|
||||
display: block;
|
||||
margin: 4px 0;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.option-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.selected-files span { display: block; margin: 4px 0; word-break: break-all; }
|
||||
.option-group { margin-bottom: 20px; }
|
||||
.radio-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@@ -270,35 +213,16 @@
|
||||
border: 1px solid #2a2a2a;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.radio-item:hover {
|
||||
background: #2a2a2a;
|
||||
}
|
||||
|
||||
.radio-item input {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.radio-item .label {
|
||||
font-weight: 500;
|
||||
color: #e0e0e0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.radio-item .desc {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
margin-top: 2px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.radio-item:hover { background: #2a2a2a; }
|
||||
.radio-item input { margin-top: 3px; }
|
||||
.radio-item .label { font-weight: 500; color: #e0e0e0; font-size: 13px; }
|
||||
.radio-item .desc { font-size: 12px; color: #888; margin-top: 2px; font-weight: normal; }
|
||||
.run-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.btn-run {
|
||||
padding: 10px 22px;
|
||||
background: #3498db;
|
||||
@@ -309,21 +233,9 @@
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-run:hover {
|
||||
background: #2980b9;
|
||||
}
|
||||
|
||||
.btn-run:disabled {
|
||||
background: #555;
|
||||
cursor: not-allowed;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.loading-msg {
|
||||
color: #3498db;
|
||||
font-size: 13px;
|
||||
}
|
||||
.btn-run:hover { background: #2980b9; }
|
||||
.btn-run:disabled { background: #555; cursor: not-allowed; color: #999; }
|
||||
.loading-msg { color: #3498db; font-size: 13px; }
|
||||
|
||||
.template-download-row {
|
||||
margin-top: 16px;
|
||||
@@ -337,22 +249,16 @@
|
||||
transition: all 0.25s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.template-download-row:hover {
|
||||
border-color: #2e7d32;
|
||||
background: linear-gradient(135deg, #2a2a2a 0%, #243324 100%);
|
||||
box-shadow: 0 4px 12px rgba(46, 125, 50, 0.15);
|
||||
}
|
||||
|
||||
.template-download-row:hover .template-download-text .title {
|
||||
color: #2ecc71;
|
||||
}
|
||||
|
||||
.template-download-row:hover .template-download-text .title { color: #2ecc71; }
|
||||
.template-download-row:hover .template-download-icon {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 6px 16px rgba(46, 125, 50, 0.45), inset 0 1px 0 rgba(255,255,255,0.15);
|
||||
}
|
||||
|
||||
.template-download-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
@@ -365,25 +271,12 @@
|
||||
box-shadow: 0 4px 12px rgba(46, 125, 50, 0.35), inset 0 1px 0 rgba(255,255,255,0.1);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.template-download-icon svg {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.template-download-text .title {
|
||||
font-size: 14px;
|
||||
color: #e0e0e0;
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.template-download-text .hint {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
margin-top: 2px;
|
||||
font-weight: normal;
|
||||
}
|
||||
.template-download-text .title { font-size: 14px; color: #e0e0e0; font-weight: 600; display: block; }
|
||||
.template-download-text .hint { font-size: 12px; color: #888; margin-top: 2px; font-weight: normal; }
|
||||
|
||||
.panel-header {
|
||||
padding: 16px 20px;
|
||||
@@ -396,11 +289,9 @@
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.panel-header-title {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.btn-refresh-tasks {
|
||||
padding: 6px 14px;
|
||||
font-size: 12px;
|
||||
@@ -412,23 +303,17 @@
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-refresh-tasks:hover {
|
||||
background: #333;
|
||||
border-color: #3498db;
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
.task-list-wrap {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.task-list { list-style: none; }
|
||||
.task-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -439,155 +324,38 @@
|
||||
margin-bottom: 10px;
|
||||
background: #1e1e1e;
|
||||
}
|
||||
|
||||
.task-item .left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-item .id {
|
||||
font-weight: 600;
|
||||
color: #e0e0e0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.task-item .files {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.task-item .time {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.task-item .left { flex: 1; min-width: 0; }
|
||||
.task-item .id { font-weight: 600; color: #e0e0e0; font-size: 13px; }
|
||||
.task-item .files { font-size: 12px; color: #888; margin-top: 4px; }
|
||||
.task-item .time { font-size: 12px; color: #666; margin-top: 2px; }
|
||||
.task-item .status {
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.task-item .status.pending {
|
||||
background: rgba(255, 193, 7, 0.2);
|
||||
color: #d4a500;
|
||||
}
|
||||
|
||||
.task-item .status.running {
|
||||
background: rgba(52, 152, 219, 0.2);
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
.task-item .status.success {
|
||||
background: rgba(46, 204, 113, 0.2);
|
||||
color: #2ecc71;
|
||||
}
|
||||
|
||||
.task-item .status.failed {
|
||||
background: rgba(231, 76, 60, 0.2);
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.task-item .status.cancelled {
|
||||
background: rgba(149, 165, 166, 0.2);
|
||||
color: #95a5a6;
|
||||
}
|
||||
|
||||
.task-item .progress-wrap {
|
||||
margin-top: 8px;
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0;
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.task-item .progress-bar-bg {
|
||||
height: 6px;
|
||||
background: #2a2a2a;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.task-item .progress-bar-fill {
|
||||
height: 100%;
|
||||
background: #3498db;
|
||||
border-radius: 3px;
|
||||
transition: width 0.2s;
|
||||
}
|
||||
|
||||
.task-item .btn-cancel,
|
||||
.task-item .btn-delete {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
margin-left: 6px;
|
||||
.task-item .status.pending { background: rgba(255,193,7,0.2); color: #d4a500; }
|
||||
.task-item .status.running { background: rgba(52,152,219,0.2); color: #3498db; }
|
||||
.task-item .status.success { background: rgba(46,204,113,0.2); color: #2ecc71; }
|
||||
.task-item .status.failed { background: rgba(231,76,60,0.2); color: #e74c3c; }
|
||||
.task-item .status.cancelled { background: rgba(149,165,166,0.2); color: #95a5a6; }
|
||||
.task-item .progress-wrap { margin-top: 8px; margin-right: 8px; flex-shrink: 0; width: 120px; }
|
||||
.task-item .progress-bar-bg { height: 6px; background: #2a2a2a; border-radius: 3px; overflow: hidden; }
|
||||
.task-item .progress-bar-fill { height: 100%; background: #3498db; border-radius: 3px; transition: width 0.2s; }
|
||||
.task-item .btn-cancel, .task-item .btn-delete {
|
||||
padding: 6px 12px; font-size: 12px; border-radius: 6px; cursor: pointer; border: none; margin-left: 6px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.task-item .btn-cancel {
|
||||
background: #e67e22;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.task-item .btn-cancel:hover {
|
||||
background: #d35400;
|
||||
}
|
||||
|
||||
.task-item .btn-delete {
|
||||
background: #444;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.task-item .btn-delete:hover {
|
||||
background: #e74c3c;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.task-item .task-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.run-row .progress-wrap {
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.run-row .progress-bar-bg {
|
||||
height: 8px;
|
||||
background: #2a2a2a;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.run-row .progress-bar-fill {
|
||||
height: 100%;
|
||||
background: #3498db;
|
||||
border-radius: 4px;
|
||||
transition: width 0.2s;
|
||||
}
|
||||
|
||||
.run-row .btn-cancel-run {
|
||||
margin-left: 12px;
|
||||
padding: 8px 16px;
|
||||
background: #e67e22;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.run-row .btn-cancel-run:hover {
|
||||
background: #d35400;
|
||||
}
|
||||
|
||||
.task-item .btn-cancel { background: #e67e22; color: #fff; }
|
||||
.task-item .btn-cancel:hover { background: #d35400; }
|
||||
.task-item .btn-delete { background: #444; color: #ccc; }
|
||||
.task-item .btn-delete:hover { background: #e74c3c; color: #fff; }
|
||||
.task-item .task-right { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.run-row .progress-wrap { margin-top: 10px; width: 100%; max-width: 280px; }
|
||||
.run-row .progress-bar-bg { height: 8px; background: #2a2a2a; border-radius: 4px; overflow: hidden; }
|
||||
.run-row .progress-bar-fill { height: 100%; background: #3498db; border-radius: 4px; transition: width 0.2s; }
|
||||
.run-row .btn-cancel-run { margin-left: 12px; padding: 8px 16px; background: #e67e22; color: #fff; border: none; border-radius: 6px; font-size: 13px; cursor: pointer; }
|
||||
.run-row .btn-cancel-run:hover { background: #d35400; }
|
||||
.task-item .download {
|
||||
padding: 6px 14px;
|
||||
background: #3498db;
|
||||
@@ -600,23 +368,9 @@
|
||||
white-space: nowrap;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.task-item .download:hover {
|
||||
background: #2980b9;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.task-item .download.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.empty-tasks {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
padding: 32px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.task-item .download:hover { background: #2980b9; color: #fff; }
|
||||
.task-item .download.hide { display: none; }
|
||||
.empty-tasks { text-align: center; color: #666; padding: 32px; font-size: 13px; }
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 32px;
|
||||
@@ -632,13 +386,9 @@
|
||||
transition: opacity 0.3s;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.toast.show {
|
||||
opacity: 1;
|
||||
}
|
||||
.toast.show { opacity: 1; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="top-bar">
|
||||
<div class="logo-area">
|
||||
@@ -726,33 +476,23 @@
|
||||
<button type="button" class="btn-run" id="btnRun">立即运行</button>
|
||||
<span class="loading-msg" id="loadingMsg" style="display:none;">生成中,请稍候…</span>
|
||||
<div class="progress-wrap" id="runProgressWrap" style="display:none;">
|
||||
<div class="progress-bar-bg">
|
||||
<div class="progress-bar-fill" id="runProgressFill" style="width:0%;"></div>
|
||||
</div>
|
||||
<div class="progress-bar-bg"><div class="progress-bar-fill" id="runProgressFill" style="width:0%;"></div></div>
|
||||
<span class="hint" style="font-size:12px;color:#888;" id="runProgressText">0 / 0</span>
|
||||
</div>
|
||||
<!-- 当前文件行级进度(不经数据库中转,只读内存接口) -->
|
||||
<div class="progress-wrap" id="runLineProgressWrap" style="display:none;">
|
||||
<div class="progress-bar-bg">
|
||||
<div class="progress-bar-fill" id="runLineProgressFill" style="width:0%;"></div>
|
||||
</div>
|
||||
<div class="progress-bar-bg"><div class="progress-bar-fill" id="runLineProgressFill" style="width:0%;"></div></div>
|
||||
<span class="hint" style="font-size:12px;color:#888;" id="runLineProgressText">0 / 0</span>
|
||||
</div>
|
||||
<button type="button" class="btn-cancel-run" id="btnCancelRun"
|
||||
style="display:none;">取消生成</button>
|
||||
<button type="button" class="btn-cancel-run" id="btnCancelRun" style="display:none;">取消生成</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="template-download-row" id="templateDownloadRow" role="button" tabindex="0"
|
||||
title="下载品牌文档格式模板(选择保存位置)">
|
||||
<div class="template-download-row" id="templateDownloadRow" role="button" tabindex="0" title="下载品牌文档格式模板(选择保存位置)">
|
||||
<span class="template-download-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6z"
|
||||
stroke="rgba(255,255,255,0.9)" stroke-width="1.8" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path d="M14 2v6h6" stroke="rgba(255,255,255,0.9)" stroke-width="1.8" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path d="M8 13h8M8 17h5" stroke="rgba(255,255,255,0.85)" stroke-width="1.5"
|
||||
stroke-linecap="round" />
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6z" stroke="rgba(255,255,255,0.9)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14 2v6h6" stroke="rgba(255,255,255,0.9)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8 13h8M8 17h5" stroke="rgba(255,255,255,0.85)" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="template-download-text">
|
||||
@@ -760,17 +500,12 @@
|
||||
<span class="hint">品牌文档格式_模板.xlsx · 点击选择保存位置</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="template-download-row" id="templateDownloadRow2" role="button" tabindex="0"
|
||||
title="下载模板2(以文件夹方式上传)">
|
||||
<div class="template-download-row" id="templateDownloadRow2" role="button" tabindex="0" title="下载模板2(以文件夹方式上传)">
|
||||
<span class="template-download-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6z"
|
||||
stroke="rgba(255,255,255,0.9)" stroke-width="1.8" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path d="M14 2v6h6" stroke="rgba(255,255,255,0.9)" stroke-width="1.8" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path d="M3 7v10h4v-4h2v4h8v-4h2v4h2V7H3z" stroke="rgba(255,255,255,0.85)"
|
||||
stroke-width="1.5" stroke-linecap="round" />
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6z" stroke="rgba(255,255,255,0.9)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14 2v6h6" stroke="rgba(255,255,255,0.9)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3 7v10h4v-4h2v4h8v-4h2v4h2V7H3z" stroke="rgba(255,255,255,0.85)" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="template-download-text">
|
||||
@@ -801,6 +536,16 @@
|
||||
var cachedTasks = [];
|
||||
var api = window.pywebview && window.pywebview.api;
|
||||
|
||||
function getUid() {
|
||||
return localStorage.getItem('uid') || '';
|
||||
}
|
||||
|
||||
function withUidHeaders(headers) {
|
||||
headers = headers || {};
|
||||
headers.uid = getUid();
|
||||
return headers;
|
||||
}
|
||||
|
||||
// 顶部栏目切换:点击当前页菜单项时显示对应 data-panel 的 tab-panel
|
||||
document.querySelectorAll('.nav-dropdown-item[data-panel]').forEach(function(tab) {
|
||||
tab.addEventListener('click', function() {
|
||||
@@ -906,8 +651,8 @@
|
||||
if (!folder) return;
|
||||
fetch('/api/brand/expand-folder', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
|
||||
credentials: 'same-origin',
|
||||
headers: withUidHeaders({ 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ folder: folder })
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
@@ -938,14 +683,14 @@
|
||||
|
||||
var pollTaskId = null;
|
||||
var pollTimer = null;
|
||||
var runTaskEventSource = null;
|
||||
var runTaskEventAbortController = null;
|
||||
|
||||
function stopPollAndResetRunUI() {
|
||||
pollTaskId = null;
|
||||
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
|
||||
if (runTaskEventSource) {
|
||||
runTaskEventSource.close();
|
||||
runTaskEventSource = null;
|
||||
if (runTaskEventAbortController) {
|
||||
try { runTaskEventAbortController.abort(); } catch (e) {}
|
||||
runTaskEventAbortController = null;
|
||||
}
|
||||
var btn = document.getElementById('btnRun');
|
||||
var loadingMsg = document.getElementById('loadingMsg');
|
||||
@@ -979,7 +724,7 @@
|
||||
runProgressWrap.style.display = 'block';
|
||||
btnCancelRun.style.display = 'inline-block';
|
||||
btnCancelRun.onclick = function() {
|
||||
fetch('/api/brand/tasks/' + taskId + '/cancel', { method: 'POST', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||
fetch('/api/brand/tasks/' + taskId + '/cancel', { method: 'POST', credentials: 'include', headers: withUidHeaders({ 'X-Requested-With': 'XMLHttpRequest' }) })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
if (res.success) onRunTaskFinished('cancelled');
|
||||
@@ -987,28 +732,64 @@
|
||||
});
|
||||
};
|
||||
|
||||
// 通过 SSE 订阅任务完成事件,完成后由后端主动推送,前端收到后立即隐藏进度条和取消按钮
|
||||
// 通过事件流订阅任务完成事件(使用 fetch 以便携带 uid 请求头)
|
||||
var eventsUrl = '/api/brand/tasks/' + taskId + '/events';
|
||||
runTaskEventSource = new EventSource(eventsUrl);
|
||||
runTaskEventSource.onmessage = function (e) {
|
||||
runTaskEventAbortController = new AbortController();
|
||||
(async function() {
|
||||
try {
|
||||
var data = JSON.parse(e.data);
|
||||
var resp = await fetch(eventsUrl, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: withUidHeaders({ 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'text/event-stream' }),
|
||||
signal: runTaskEventAbortController.signal
|
||||
});
|
||||
if (!resp.ok || !resp.body) return;
|
||||
|
||||
var reader = resp.body.getReader();
|
||||
var decoder = new TextDecoder('utf-8');
|
||||
var buffer = '';
|
||||
|
||||
while (true) {
|
||||
var r = await reader.read();
|
||||
if (!r || r.done) break;
|
||||
buffer += decoder.decode(r.value, { stream: true });
|
||||
|
||||
var parts = buffer.split(/\n\n/);
|
||||
buffer = parts.pop();
|
||||
|
||||
var terminalStatus = null;
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
var part = parts[i];
|
||||
if (!part) continue;
|
||||
|
||||
var dataLine = part.split('\n').find(function(l) { return l.indexOf('data:') === 0; });
|
||||
if (!dataLine) continue;
|
||||
|
||||
var dataStr = dataLine.slice(5).trim();
|
||||
if (!dataStr) continue;
|
||||
|
||||
try {
|
||||
var data = JSON.parse(dataStr);
|
||||
if (data && (data.status === 'success' || data.status === 'failed' || data.status === 'cancelled')) {
|
||||
runTaskEventSource.close();
|
||||
runTaskEventSource = null;
|
||||
// tick();
|
||||
onRunTaskFinished(data.status);
|
||||
terminalStatus = data.status;
|
||||
break;
|
||||
}
|
||||
} catch (err) {}
|
||||
};
|
||||
runTaskEventSource.onerror = function () {
|
||||
runTaskEventSource.close();
|
||||
runTaskEventSource = null;
|
||||
};
|
||||
}
|
||||
|
||||
if (terminalStatus) {
|
||||
try { runTaskEventAbortController.abort(); } catch (e2) {}
|
||||
runTaskEventAbortController = null;
|
||||
onRunTaskFinished(terminalStatus);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (err) {}
|
||||
})();
|
||||
|
||||
function tick() {
|
||||
if (!pollTaskId) return;
|
||||
fetch('/api/brand/tasks/' + taskId, { credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||
fetch('/api/brand/tasks/' + taskId, { credentials: 'include', headers: withUidHeaders({ 'X-Requested-With': 'XMLHttpRequest' }) })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
if (!res.success || !res.task) return;
|
||||
@@ -1035,8 +816,8 @@
|
||||
var st2 = (t.status || '').toLowerCase();
|
||||
if (st2 === 'running') {
|
||||
fetch('/api/brand/tasks/' + taskId + '/line-progress', {
|
||||
credentials: 'same-origin',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
credentials: 'include',
|
||||
headers: withUidHeaders({ 'X-Requested-With': 'XMLHttpRequest' })
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res2) {
|
||||
@@ -1064,7 +845,7 @@
|
||||
});
|
||||
}
|
||||
tick();
|
||||
pollTimer = setInterval(tick, 5000);
|
||||
pollTimer = setInterval(tick, 10000);
|
||||
}
|
||||
|
||||
document.getElementById('btnRun').onclick = function() {
|
||||
@@ -1082,8 +863,8 @@
|
||||
loadingMsg.style.display = 'inline';
|
||||
fetch('/api/brand/run', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
|
||||
credentials: 'same-origin',
|
||||
headers: withUidHeaders({ 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ paths: selectedPaths, strategy: strategy, task_type: 1 })
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
@@ -1103,8 +884,8 @@
|
||||
} else {
|
||||
fetch('/api/brand/tasks', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
|
||||
credentials: 'same-origin',
|
||||
headers: withUidHeaders({ 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ paths: selectedPaths, strategy: strategy, task_type: 2 })
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
@@ -1121,7 +902,7 @@
|
||||
};
|
||||
|
||||
function loadTasks() {
|
||||
fetch('/api/brand/tasks', { credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||
fetch('/api/brand/tasks', { credentials: 'include', headers: withUidHeaders({ 'X-Requested-With': 'XMLHttpRequest' }) })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
var list = document.getElementById('taskList');
|
||||
@@ -1188,7 +969,7 @@
|
||||
if (target.classList && (target.classList.contains('js-cancel-task') || target.closest('.js-cancel-task'))) {
|
||||
var btn = target.classList.contains('js-cancel-task') ? target : target.closest('.js-cancel-task');
|
||||
if (!btn) return;
|
||||
fetch('/api/brand/tasks/' + taskId + '/cancel', { method: 'POST', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||
fetch('/api/brand/tasks/' + taskId + '/cancel', { method: 'POST', credentials: 'include', headers: withUidHeaders({ 'X-Requested-With': 'XMLHttpRequest' }) })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
if (res.success) { showToast('已取消'); loadTasks(); } else { showToast(res.error || '取消失败'); }
|
||||
@@ -1198,7 +979,7 @@
|
||||
if (target.classList && (target.classList.contains('js-delete-task') || target.closest('.js-delete-task'))) {
|
||||
var btn = target.classList.contains('js-delete-task') ? target : target.closest('.js-delete-task');
|
||||
if (!btn) return;
|
||||
fetch('/api/brand/tasks/' + taskId, { method: 'DELETE', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||
fetch('/api/brand/tasks/' + taskId, { method: 'DELETE', credentials: 'include', headers: withUidHeaders({ 'X-Requested-With': 'XMLHttpRequest' }) })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
if (res.success) { showToast('已删除'); loadTasks(); } else { showToast(res.error || '删除失败'); }
|
||||
@@ -1236,7 +1017,40 @@
|
||||
a.style.pointerEvents = '';
|
||||
}
|
||||
} else {
|
||||
window.open('/api/brand/download/' + taskId, '_blank');
|
||||
fetch('/api/brand/download/' + taskId, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: withUidHeaders({ 'X-Requested-With': 'XMLHttpRequest' }),
|
||||
redirect: 'manual'
|
||||
})
|
||||
.then(function(res) {
|
||||
if (res.status === 301 || res.status === 302) {
|
||||
var loc = res.headers.get('Location');
|
||||
if (loc) {
|
||||
window.open(loc, '_blank');
|
||||
return;
|
||||
}
|
||||
}
|
||||
var ct = (res.headers && res.headers.get && res.headers.get('content-type')) ? res.headers.get('content-type') : '';
|
||||
if (ct && ct.indexOf('application/json') >= 0) {
|
||||
return res.json().then(function(errObj) {
|
||||
throw new Error((errObj && (errObj.error || errObj.msg)) || '下载失败');
|
||||
});
|
||||
}
|
||||
return res.blob().then(function(blob) {
|
||||
var url = URL.createObjectURL(blob);
|
||||
var a2 = document.createElement('a');
|
||||
a2.href = url;
|
||||
a2.download = 'brand_task_' + taskId + '.zip';
|
||||
document.body.appendChild(a2);
|
||||
a2.click();
|
||||
a2.remove();
|
||||
setTimeout(function() { URL.revokeObjectURL(url); }, 1000);
|
||||
});
|
||||
})
|
||||
.catch(function(err) {
|
||||
showToast('下载失败:' + (err && err.message ? err.message : err));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1252,5 +1066,4 @@
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -62,4 +62,7 @@ public class DeleteBrandResultItemVo {
|
||||
|
||||
@Schema(description = "错误信息")
|
||||
private String error;
|
||||
|
||||
@Schema(description = "由于列表来自 history 接口,增加所属任务的状态(RUNNING/SUCCESS/FAILED),协助前端准确定位执行中任务")
|
||||
private String taskStatus;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import com.nanri.aiimage.modules.task.mapper.FileTaskMapper;
|
||||
import com.nanri.aiimage.modules.task.model.entity.FileResultEntity;
|
||||
import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.DataFormatter;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
@@ -55,6 +56,7 @@ import java.util.Set;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class DeleteBrandRunService {
|
||||
|
||||
private static final String MODULE_TYPE = "DELETE_BRAND";
|
||||
@@ -86,6 +88,7 @@ public class DeleteBrandRunService {
|
||||
task.setCreatedAt(LocalDateTime.now());
|
||||
task.setUpdatedAt(LocalDateTime.now());
|
||||
fileTaskMapper.insert(task);
|
||||
deleteBrandTaskCacheService.saveTaskCache(task);
|
||||
|
||||
List<DeleteBrandResultItemVo> items = new ArrayList<>();
|
||||
int parsedSuccessCount = 0;
|
||||
@@ -128,7 +131,8 @@ public class DeleteBrandRunService {
|
||||
if (message != null && (message.contains("code=40004")
|
||||
|| message.contains("userId存在无效的参数值")
|
||||
|| message.contains("无权限")
|
||||
|| message.contains("没有权限"))) {
|
||||
|| message.contains("没有权限")) && !message.contains("白名单")) {
|
||||
log.warn("[ziniao-match] suppressed upstream permission error for {}, msg: {}", item.getShopName(), message);
|
||||
item.setMatched(false);
|
||||
} else {
|
||||
throw ex;
|
||||
@@ -193,6 +197,9 @@ public class DeleteBrandRunService {
|
||||
task.setFinishedAt(LocalDateTime.now());
|
||||
}
|
||||
fileTaskMapper.updateById(task);
|
||||
if ("RUNNING".equals(task.getStatus())) {
|
||||
deleteBrandTaskCacheService.saveTaskCache(task);
|
||||
}
|
||||
|
||||
if (!parsedPayloadByFileIdentity.isEmpty()) {
|
||||
deleteBrandTaskCacheService.saveParsedPayload(task.getId(), parsedPayloadByFileIdentity);
|
||||
@@ -202,7 +209,7 @@ public class DeleteBrandRunService {
|
||||
vo.setTotal(request.getFiles().size());
|
||||
vo.setSuccessCount(parsedSuccessCount);
|
||||
vo.setFailedCount(parsedFailedCount);
|
||||
vo.setItems(items);
|
||||
vo.setItems(items.stream().peek(item -> item.setTaskStatus("RUNNING")).toList());
|
||||
return vo;
|
||||
}
|
||||
|
||||
@@ -214,8 +221,9 @@ public class DeleteBrandRunService {
|
||||
.orderByDesc(FileResultEntity::getCreatedAt)
|
||||
.last("limit 100"));
|
||||
|
||||
// 补充从 task.resultJson 还原匹配信息/真实失败原因(历史表本身不存这些字段)
|
||||
// 补充信息
|
||||
Map<Long, List<DeleteBrandResultItemVo>> itemsByTaskId = new LinkedHashMap<>();
|
||||
Map<Long, String> statusByTaskId = new LinkedHashMap<>();
|
||||
List<Long> taskIds = entities.stream()
|
||||
.map(FileResultEntity::getTaskId)
|
||||
.filter(id -> id != null && id > 0)
|
||||
@@ -224,13 +232,13 @@ public class DeleteBrandRunService {
|
||||
if (!taskIds.isEmpty()) {
|
||||
List<FileTaskEntity> tasks = fileTaskMapper.selectBatchIds(taskIds);
|
||||
for (FileTaskEntity task : tasks) {
|
||||
if (task == null || task.getResultJson() == null || task.getResultJson().isBlank()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
statusByTaskId.put(task.getId(), task.getStatus());
|
||||
if (task.getResultJson() != null && !task.getResultJson().isBlank()) {
|
||||
List<DeleteBrandResultItemVo> taskItems = objectMapper.readValue(task.getResultJson(), new TypeReference<List<DeleteBrandResultItemVo>>() {
|
||||
});
|
||||
itemsByTaskId.put(task.getId(), taskItems);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
@@ -250,7 +258,9 @@ public class DeleteBrandRunService {
|
||||
item.setError(entity.getErrorMessage());
|
||||
|
||||
// 以 task.resultJson 中的项为准补全 matched/shopId/platform/openStoreUrl/错误信息
|
||||
// 真实补充
|
||||
if (entity.getTaskId() != null) {
|
||||
item.setTaskStatus(statusByTaskId.get(entity.getTaskId()));
|
||||
List<DeleteBrandResultItemVo> taskItems = itemsByTaskId.get(entity.getTaskId());
|
||||
if (taskItems != null && entity.getSourceFilename() != null && !entity.getSourceFilename().isBlank()) {
|
||||
for (DeleteBrandResultItemVo candidate : taskItems) {
|
||||
@@ -456,9 +466,27 @@ public class DeleteBrandRunService {
|
||||
return vo;
|
||||
}
|
||||
|
||||
java.util.Map<Long, FileTaskEntity> taskById = fileTaskMapper.selectBatchIds(normalizedTaskIds).stream()
|
||||
java.util.Map<Long, FileTaskEntity> cachedTasks = deleteBrandTaskCacheService.getTaskCacheBatch(normalizedTaskIds);
|
||||
java.util.List<Long> missingTaskIds = new java.util.ArrayList<>();
|
||||
for (Long taskId : normalizedTaskIds) {
|
||||
if (!cachedTasks.containsKey(taskId)) {
|
||||
missingTaskIds.add(taskId);
|
||||
}
|
||||
}
|
||||
|
||||
java.util.Map<Long, FileTaskEntity> taskById = new java.util.LinkedHashMap<>(cachedTasks);
|
||||
if (!missingTaskIds.isEmpty()) {
|
||||
java.util.List<FileTaskEntity> dbTasks = fileTaskMapper.selectBatchIds(missingTaskIds).stream()
|
||||
.filter(task -> task != null && MODULE_TYPE.equals(task.getModuleType()))
|
||||
.collect(java.util.stream.Collectors.toMap(FileTaskEntity::getId, task -> task, (left, right) -> left, java.util.LinkedHashMap::new));
|
||||
.toList();
|
||||
for (FileTaskEntity dbTask : dbTasks) {
|
||||
taskById.put(dbTask.getId(), dbTask);
|
||||
// Save back to cache
|
||||
if ("RUNNING".equals(dbTask.getStatus())) {
|
||||
deleteBrandTaskCacheService.saveTaskCache(dbTask);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
java.util.Map<Long, java.util.Map<Object, Object>> progressByTaskId = deleteBrandTaskCacheService.getProgressBatch(normalizedTaskIds);
|
||||
|
||||
@@ -858,6 +886,7 @@ public class DeleteBrandRunService {
|
||||
task.setUpdatedAt(LocalDateTime.now());
|
||||
task.setFinishedAt(LocalDateTime.now());
|
||||
fileTaskMapper.updateById(task);
|
||||
deleteBrandTaskCacheService.saveTaskCache(task);
|
||||
|
||||
deleteBrandTaskCacheService.saveProgress(task.getId(), Map.of(
|
||||
"phase", "success",
|
||||
@@ -872,6 +901,7 @@ public class DeleteBrandRunService {
|
||||
task.setUpdatedAt(LocalDateTime.now());
|
||||
task.setFinishedAt(LocalDateTime.now());
|
||||
fileTaskMapper.updateById(task);
|
||||
deleteBrandTaskCacheService.saveTaskCache(task);
|
||||
deleteBrandTaskCacheService.saveProgress(task.getId(), Map.of(
|
||||
"phase", DeleteBrandTaskCacheService.PHASE_FAILED,
|
||||
"finished_files", String.valueOf(task.getSuccessFileCount() == null ? 0 : task.getSuccessFileCount()),
|
||||
|
||||
@@ -37,22 +37,30 @@ public class DeleteBrandStaleTaskService {
|
||||
|
||||
for (FileTaskEntity task : runningTasks) {
|
||||
Map<Object, Object> progress = deleteBrandTaskCacheService.getProgress(task.getId());
|
||||
if (progress == null || progress.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
boolean hasProgress = progress != null && !progress.isEmpty() && progress.containsKey("last_heartbeat_at");
|
||||
|
||||
long lastHeartbeatAt = 0L;
|
||||
if (hasProgress) {
|
||||
try {
|
||||
lastHeartbeatAt = Long.parseLong(String.valueOf(progress.getOrDefault("last_heartbeat_at", "0")));
|
||||
lastHeartbeatAt = Long.parseLong(String.valueOf(progress.get("last_heartbeat_at")));
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
if (lastHeartbeatAt <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
LocalDateTime lastHeartbeat = LocalDateTime.ofInstant(Instant.ofEpochMilli(lastHeartbeatAt), ZoneId.systemDefault());
|
||||
if (lastHeartbeat.isAfter(threshold)) {
|
||||
continue;
|
||||
LocalDateTime lastActivityTime;
|
||||
if (lastHeartbeatAt > 0) {
|
||||
// 有心跳,用最后心跳时间对比 15 分钟
|
||||
lastActivityTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(lastHeartbeatAt), ZoneId.systemDefault());
|
||||
if (lastActivityTime.isAfter(threshold)) {
|
||||
continue; // 没超时
|
||||
}
|
||||
} else {
|
||||
// 没有心跳,说明任务一直没被 Python 取出来执行(处在排队状态中)
|
||||
// 排队的宽限期我们设为 12 个小时,如果建了 12 个小时都没执行,再杀死
|
||||
LocalDateTime queuedThreshold = LocalDateTime.now().minusHours(12);
|
||||
if (task.getCreatedAt() != null && task.getCreatedAt().isAfter(queuedThreshold)) {
|
||||
continue; // 排队中,不要杀它!
|
||||
}
|
||||
}
|
||||
|
||||
int updated = fileTaskMapper.update(null, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<FileTaskEntity>()
|
||||
@@ -65,10 +73,7 @@ public class DeleteBrandStaleTaskService {
|
||||
.set(FileTaskEntity::getFinishedAt, LocalDateTime.now()));
|
||||
|
||||
if (updated > 0) {
|
||||
deleteBrandTaskCacheService.saveProgress(task.getId(), java.util.Map.of(
|
||||
"phase", DeleteBrandTaskCacheService.PHASE_FAILED,
|
||||
"updated_at", String.valueOf(System.currentTimeMillis())
|
||||
));
|
||||
deleteBrandTaskCacheService.delete(task.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,6 +181,42 @@ public class DeleteBrandTaskCacheService {
|
||||
stringRedisTemplate.delete(buildPayloadKey(taskId));
|
||||
stringRedisTemplate.delete(buildProgressKey(taskId));
|
||||
stringRedisTemplate.delete(buildResultChunksKey(taskId));
|
||||
stringRedisTemplate.delete(buildTaskEntityKey(taskId));
|
||||
}
|
||||
|
||||
public void saveTaskCache(com.nanri.aiimage.modules.task.model.entity.FileTaskEntity task) {
|
||||
if (task == null || task.getId() == null) return;
|
||||
try {
|
||||
stringRedisTemplate.opsForValue().set(buildTaskEntityKey(task.getId()), objectMapper.writeValueAsString(task), Duration.ofHours(PAYLOAD_TTL_HOURS));
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public java.util.Map<Long, com.nanri.aiimage.modules.task.model.entity.FileTaskEntity> getTaskCacheBatch(java.util.List<Long> taskIds) {
|
||||
java.util.Map<Long, com.nanri.aiimage.modules.task.model.entity.FileTaskEntity> result = new java.util.LinkedHashMap<>();
|
||||
if (taskIds == null || taskIds.isEmpty()) return result;
|
||||
|
||||
java.util.List<Long> normalized = taskIds.stream().filter(id -> id != null && id > 0).distinct().toList();
|
||||
if (normalized.isEmpty()) return result;
|
||||
|
||||
java.util.List<String> keys = normalized.stream().map(this::buildTaskEntityKey).toList();
|
||||
java.util.List<String> values = stringRedisTemplate.opsForValue().multiGet(keys);
|
||||
|
||||
for (int i = 0; i < normalized.size(); i++) {
|
||||
Long taskId = normalized.get(i);
|
||||
String val = values != null && i < values.size() ? values.get(i) : null;
|
||||
if (val != null && !val.isBlank()) {
|
||||
try {
|
||||
result.put(taskId, objectMapper.readValue(val, com.nanri.aiimage.modules.task.model.entity.FileTaskEntity.class));
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String buildTaskEntityKey(Long taskId) {
|
||||
return "delete-brand:task:entity:" + taskId;
|
||||
}
|
||||
|
||||
private record LocalParsedPayloadCacheEntry(long cachedAtMillis, Object value) {
|
||||
|
||||
@@ -472,6 +472,9 @@ public class ZiniaoAuthService {
|
||||
if (message == null || message.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
if (message.contains("白名单") || message.contains("IP") || message.contains("whitelist")) {
|
||||
return false;
|
||||
}
|
||||
return message.contains("isv.invalid-api-key")
|
||||
|| message.contains("无效的apiKey参数")
|
||||
|| message.contains("非法的参数");
|
||||
|
||||
Binary file not shown.
@@ -128,8 +128,8 @@
|
||||
</div>
|
||||
|
||||
<div class="task-right split-result-actions">
|
||||
<span class="status" :class="item.success ? 'success' : 'failed'">
|
||||
{{ item.success ? '已完成' : '失败' }}
|
||||
<span class="status" :class="getTaskStatusInfo(item).className">
|
||||
{{ getTaskStatusInfo(item).text }}
|
||||
</span>
|
||||
<button
|
||||
v-if="item.openStoreUrl"
|
||||
@@ -192,8 +192,8 @@
|
||||
</div>
|
||||
|
||||
<div class="task-right split-result-actions">
|
||||
<span class="status" :class="item.success ? 'success' : 'failed'">
|
||||
{{ item.success ? '已完成' : '失败' }}
|
||||
<span class="status" :class="getTaskStatusInfo(item).className">
|
||||
{{ getTaskStatusInfo(item).text }}
|
||||
</span>
|
||||
<button
|
||||
v-if="item.openStoreUrl"
|
||||
@@ -262,17 +262,41 @@ const taskDetails = ref<Record<number, DeleteBrandTaskDetailVo>>({})
|
||||
const pollTimer = ref<number | null>(null)
|
||||
const pollingInFlight = ref(false)
|
||||
const displayPaths = computed(() => selectedPaths.value.slice(0, 10))
|
||||
const currentSectionItems = computed(() => currentRunItems.value)
|
||||
const runningTaskIdsInHistory = computed(() => {
|
||||
return historyItems.value
|
||||
.filter(item => {
|
||||
const status = item.taskId ? taskDetails.value[item.taskId]?.task?.status : null;
|
||||
// 优先认后端的实时状态
|
||||
if (status) return status === 'RUNNING';
|
||||
// 如果还没拿到底层详情,看 history 接口带回来的全局任务状态
|
||||
if (item.taskStatus) return item.taskStatus === 'RUNNING';
|
||||
// 最后降级到根据 success/error 猜测(向下兼容)
|
||||
return (item.taskId || 0) > 0 && item.success === false && !item.error;
|
||||
})
|
||||
.map(item => item.taskId);
|
||||
});
|
||||
|
||||
const currentSectionItems = computed(() => {
|
||||
// 当前运行的任务 = 用户手动触发的 + 历史记录里还在跑的
|
||||
const allCurrent = [...currentRunItems.value];
|
||||
|
||||
historyItems.value.forEach(hItem => {
|
||||
if (hItem.taskId && runningTaskIdsInHistory.value.includes(hItem.taskId)) {
|
||||
// 避免重复
|
||||
if (!allCurrent.some(c => c.taskId === hItem.taskId)) {
|
||||
allCurrent.push(hItem);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return allCurrent;
|
||||
});
|
||||
|
||||
const historySectionItems = computed(() =>
|
||||
historyItems.value.filter(
|
||||
(item) =>
|
||||
!currentRunItems.value.some(
|
||||
(current) =>
|
||||
(current.resultId || current.taskId || current.fileKey || current.sourceFilename) ===
|
||||
(item.resultId || item.taskId || item.fileKey || item.sourceFilename),
|
||||
(item) => !currentSectionItems.value.some(c => c.taskId === item.taskId || (c.resultId && c.resultId === item.resultId))
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
const hasVisibleItems = computed(() => currentSectionItems.value.length > 0 || historySectionItems.value.length > 0)
|
||||
|
||||
function getTaskStatus(taskId?: number) {
|
||||
@@ -289,8 +313,9 @@ function getDisplayError(item: DeleteBrandResultItem) {
|
||||
|
||||
function shouldShowProgress(item: DeleteBrandResultItem) {
|
||||
const taskId = item.taskId
|
||||
if (!taskId || item.matched !== true) return false
|
||||
if (!taskId) return false
|
||||
const status = getTaskStatus(taskId)
|
||||
// 只要状态是运行中就显示进度条,不管 matched 了(matched 不对的状态应该在后端就标记为 FAILED)
|
||||
return status === 'RUNNING'
|
||||
}
|
||||
|
||||
@@ -301,7 +326,6 @@ function normalizeDeleteBrandItems(items: DeleteBrandResultItem[]) {
|
||||
...item,
|
||||
success: false,
|
||||
error: item.error || '未匹配到紫鸟店铺',
|
||||
taskId: undefined,
|
||||
openStoreUrl: undefined,
|
||||
}
|
||||
}
|
||||
@@ -344,10 +368,10 @@ function formatProgress(taskId: number) {
|
||||
if (info.phase) parts.push(`阶段:${info.phase}`)
|
||||
if (info.file_index && info.file_total) parts.push(`文件:${info.file_index}/${info.file_total}`)
|
||||
if (info.file_name) parts.push(`当前文件:${info.file_name}`)
|
||||
if (info.current_line !== undefined && info.total_lines !== undefined) parts.push(`进度:${info.current_line}/${info.total_lines}`)
|
||||
if (info.current_line != null && info.total_lines != null) parts.push(`进度:${info.current_line}/${info.total_lines}`)
|
||||
if (info.current_country) parts.push(`国家:${info.current_country}`)
|
||||
if (info.current_asin) parts.push(`ASIN:${info.current_asin}`)
|
||||
if (info.finished_files !== undefined) parts.push(`已完成文件:${info.finished_files}`)
|
||||
if (info.finished_files != null) parts.push(`已完成文件:${info.finished_files}`)
|
||||
return parts.join('|')
|
||||
}
|
||||
|
||||
@@ -356,10 +380,10 @@ function formatProgressPercent(taskId: number) {
|
||||
const info = taskDetails.value[taskId]?.line_progress?.info
|
||||
if (!info) return 0
|
||||
|
||||
if (info.current_line !== undefined && info.total_lines && info.total_lines > 0) {
|
||||
if (info.current_line != null && info.total_lines && info.total_lines > 0) {
|
||||
return Math.max(0, Math.min(100, Math.round((info.current_line / info.total_lines) * 100)))
|
||||
}
|
||||
if (info.finished_files !== undefined && info.file_total && info.file_total > 0) {
|
||||
if (info.finished_files != null && info.file_total && info.file_total > 0) {
|
||||
return Math.max(0, Math.min(100, Math.round((info.finished_files / info.file_total) * 100)))
|
||||
}
|
||||
const status = taskDetails.value[taskId]?.task?.status
|
||||
@@ -377,14 +401,34 @@ function isTaskTerminal(taskId: number) {
|
||||
}
|
||||
|
||||
function getPollingTaskIds() {
|
||||
return Array.from(
|
||||
new Set(
|
||||
currentRunItems.value
|
||||
.filter((item) => item.matched && (item.taskId || 0) > 0)
|
||||
.map(getItemTaskId)
|
||||
.filter((id) => id > 0 && !isTaskTerminal(id)),
|
||||
),
|
||||
)
|
||||
const ids = new Set<number>()
|
||||
|
||||
// 1. 检查本会话触发的任务
|
||||
currentRunItems.value.forEach((item) => {
|
||||
if (item.taskId && !isTaskTerminal(item.taskId)) {
|
||||
ids.add(item.taskId)
|
||||
}
|
||||
})
|
||||
|
||||
// 2. 检查历史加载的任务
|
||||
historyItems.value.forEach((item) => {
|
||||
if (item.taskId) {
|
||||
if (taskDetails.value[item.taskId]) {
|
||||
// 如果已经拿到了详情,以后端详情为准
|
||||
if (!isTaskTerminal(item.taskId)) {
|
||||
ids.add(item.taskId)
|
||||
}
|
||||
} else {
|
||||
// 还没拿到详情,看 history 接口中的 taskStatus 或 success 标记
|
||||
const isRunning = item.taskStatus === 'RUNNING' || (!item.taskStatus && !item.success && !item.error);
|
||||
if (isRunning) {
|
||||
ids.add(item.taskId)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return Array.from(ids)
|
||||
}
|
||||
|
||||
function canDownloadTaskResult(item: DeleteBrandResultItem) {
|
||||
@@ -415,9 +459,14 @@ function getPollIntervalMs() {
|
||||
return document.visibilityState === 'visible' ? 4500 : 10000
|
||||
}
|
||||
|
||||
function scheduleNextPoll() {
|
||||
if (pollTimer.value) return
|
||||
pollTimer.value = window.setTimeout(async () => {
|
||||
function scheduleNextPoll(immediate = false) {
|
||||
if (pollTimer.value) {
|
||||
if (!immediate) return
|
||||
clearTimeout(pollTimer.value)
|
||||
pollTimer.value = null
|
||||
}
|
||||
|
||||
const executePoll = async () => {
|
||||
pollTimer.value = null
|
||||
if (pollingInFlight.value) {
|
||||
scheduleNextPoll()
|
||||
@@ -438,12 +487,35 @@ function scheduleNextPoll() {
|
||||
if (stillRunning) {
|
||||
scheduleNextPoll()
|
||||
}
|
||||
}, getPollIntervalMs())
|
||||
}
|
||||
|
||||
function ensurePolling() {
|
||||
if (pollTimer.value) return
|
||||
scheduleNextPoll()
|
||||
if (immediate) {
|
||||
executePoll()
|
||||
} else {
|
||||
pollTimer.value = window.setTimeout(executePoll, getPollIntervalMs())
|
||||
}
|
||||
}
|
||||
|
||||
function ensurePolling(immediate = false) {
|
||||
if (pollTimer.value && !immediate) return
|
||||
scheduleNextPoll(immediate)
|
||||
}
|
||||
|
||||
function getTaskStatusInfo(item: DeleteBrandResultItem) {
|
||||
// 1. 优先查实时详情中的状态
|
||||
const status = item.taskId ? taskDetails.value[item.taskId]?.task?.status : null
|
||||
if (status === 'SUCCESS') return { className: 'success', text: '已完成' }
|
||||
if (status === 'FAILED') return { className: 'failed', text: '失败' }
|
||||
if (status === 'RUNNING') return { className: 'running', text: '执行中' }
|
||||
|
||||
// 2. 其次看 history 接口带回来的后端任务状态
|
||||
if (item.taskStatus === 'SUCCESS') return { className: 'success', text: '已完成' }
|
||||
if (item.taskStatus === 'FAILED') return { className: 'failed', text: '失败' }
|
||||
if (item.taskStatus === 'RUNNING') return { className: 'running', text: '执行中' }
|
||||
|
||||
// 3. 最后根据 item 本身的 success 标识兜底
|
||||
if (item.success) return { className: 'success', text: '已完成' }
|
||||
return { className: 'failed', text: '失败' }
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
@@ -597,8 +669,12 @@ async function loadHistory() {
|
||||
const response = await getDeleteBrandHistory()
|
||||
historyItems.value = normalizeDeleteBrandItems(response.items || [])
|
||||
syncResultState()
|
||||
} catch {
|
||||
// ignore history load errors
|
||||
|
||||
// 关键:页面加载历史后,立刻拉取详情触发状态机
|
||||
await refreshTaskDetails()
|
||||
ensurePolling(true)
|
||||
} catch (err) {
|
||||
console.error('loadHistory error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -712,6 +788,7 @@ onUnmounted(() => {
|
||||
.status { padding: 4px 10px; border-radius: 6px; font-size: 12px; white-space: nowrap; }
|
||||
.status.success { background: rgba(46, 204, 113, 0.18); color: #2ecc71; }
|
||||
.status.failed { background: rgba(231, 76, 60, 0.18); color: #ff6b6b; }
|
||||
.status.running { background: rgba(52, 152, 219, 0.18); color: #3498db; }
|
||||
.download { padding: 6px 10px; border-radius: 6px; font-size: 12px; background: rgba(52, 152, 219, 0.18); color: #69b6ff; }
|
||||
.download:hover { background: rgba(52, 152, 219, 0.28); }
|
||||
.btn-delete { padding: 6px 10px; border-radius: 6px; font-size: 12px; background: rgba(231, 76, 60, 0.12); color: #ff8f8f; }
|
||||
|
||||
@@ -229,6 +229,7 @@ export interface DeleteBrandResultItem {
|
||||
previewRows?: DeleteBrandPreviewRow[]
|
||||
success: boolean
|
||||
error?: string
|
||||
taskStatus?: string
|
||||
}
|
||||
|
||||
export interface DeleteBrandRunVo {
|
||||
|
||||
Reference in New Issue
Block a user