完善多店铺开启多窗口、外观进度条等
This commit is contained in:
17535
2026_04_30.log
17535
2026_04_30.log
File diff suppressed because one or more lines are too long
1
app/.env
1
app/.env
@@ -14,5 +14,6 @@ client_name=ShuFuAI
|
|||||||
# java_api_base=http://47.111.163.154:18080
|
# java_api_base=http://47.111.163.154:18080
|
||||||
java_api_base=http://127.0.0.1:18080
|
java_api_base=http://127.0.0.1:18080
|
||||||
# java_api_base=http://8.136.19.173:18080
|
# java_api_base=http://8.136.19.173:18080
|
||||||
|
# java_api_base=http://121.196.149.225:18080
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import requests
|
import requests
|
||||||
|
import threading
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, Any, List
|
from typing import Dict, Any, List
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
@@ -32,6 +33,9 @@ class TaskMonitor:
|
|||||||
self.chunk_index = 1 # 当前处理的分块索引
|
self.chunk_index = 1 # 当前处理的分块索引
|
||||||
self.max_workers = 5 # 线程池最大线程数
|
self.max_workers = 5 # 线程池最大线程数
|
||||||
self.executor = None # 线程池执行器
|
self.executor = None # 线程池执行器
|
||||||
|
self.serial_task_locks = {
|
||||||
|
"product-risk-resolve-run": threading.Lock(),
|
||||||
|
}
|
||||||
|
|
||||||
# 在提交新任务前杀掉旧进程(确保环境干净)
|
# 在提交新任务前杀掉旧进程(确保环境干净)
|
||||||
kill_process("v6")
|
kill_process("v6")
|
||||||
@@ -119,6 +123,16 @@ class TaskMonitor:
|
|||||||
self.log(f"线程 {id(task_data)} 删除品牌任务处理异常: {traceback.format_exc()}", "ERROR")
|
self.log(f"线程 {id(task_data)} 删除品牌任务处理异常: {traceback.format_exc()}", "ERROR")
|
||||||
|
|
||||||
def _process_approve_task_wrapper(self, task_data: Dict[str, Any],TASK_TYPE:str):
|
def _process_approve_task_wrapper(self, task_data: Dict[str, Any],TASK_TYPE:str):
|
||||||
|
task_type = task_data.get("type", "")
|
||||||
|
task_lock = self.serial_task_locks.get(task_type)
|
||||||
|
if task_lock is not None:
|
||||||
|
self.log(f"线程 {id(task_data)} 等待 {task_type} 串行锁...")
|
||||||
|
with task_lock:
|
||||||
|
self.log(f"线程 {id(task_data)} 获取 {task_type} 串行锁,开始处理...")
|
||||||
|
return self._process_approve_task_unlocked(task_data, TASK_TYPE)
|
||||||
|
return self._process_approve_task_unlocked(task_data, TASK_TYPE)
|
||||||
|
|
||||||
|
def _process_approve_task_unlocked(self, task_data: Dict[str, Any],TASK_TYPE:str):
|
||||||
"""审批任务处理包装器(用于线程池调用)
|
"""审批任务处理包装器(用于线程池调用)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
@@ -234,12 +234,14 @@ class InventoryManage(AmamzonBase):
|
|||||||
country: str,
|
country: str,
|
||||||
max_delete_operations: Optional[int] = None,
|
max_delete_operations: Optional[int] = None,
|
||||||
trace_id: str = "",
|
trace_id: str = "",
|
||||||
|
delete_conditions: Optional[List[Dict[str, Any]]] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""循环删除非白名单分类商品,直到当前国家没有可删项。"""
|
"""循环删除非白名单分类商品,直到当前国家没有可删项。"""
|
||||||
trace_id = trace_id or _new_trace_id("del")
|
trace_id = trace_id or _new_trace_id("del")
|
||||||
logger.info(
|
logger.info(
|
||||||
f"trace_id={trace_id} 开始批量删除非白名单商品: "
|
f"trace_id={trace_id} 开始批量删除非白名单商品: "
|
||||||
f"shop={shop_name}, country={country}, max_delete_operations={max_delete_operations}"
|
f"shop={shop_name}, country={country}, max_delete_operations={max_delete_operations}, "
|
||||||
|
f"delete_conditions={self._format_delete_conditions(delete_conditions)}"
|
||||||
)
|
)
|
||||||
all_results: List[Dict[str, Any]] = []
|
all_results: List[Dict[str, Any]] = []
|
||||||
delete_counts: Dict[str, int] = {}
|
delete_counts: Dict[str, int] = {}
|
||||||
@@ -249,7 +251,10 @@ class InventoryManage(AmamzonBase):
|
|||||||
stop_reason = "no-candidates"
|
stop_reason = "no-candidates"
|
||||||
|
|
||||||
latest_status_rows = self._read_listing_status_rows_for_delete(shop_name, country, trace_id=trace_id)
|
latest_status_rows = self._read_listing_status_rows_for_delete(shop_name, country, trace_id=trace_id)
|
||||||
latest_delete_candidates = self._build_deletable_status_rows(latest_status_rows)
|
latest_delete_candidates = self._build_deletable_status_rows(
|
||||||
|
latest_status_rows,
|
||||||
|
delete_conditions=delete_conditions,
|
||||||
|
)
|
||||||
if not latest_delete_candidates:
|
if not latest_delete_candidates:
|
||||||
logger.info(f"trace_id={trace_id} 未找到可删除商品分类: shop={shop_name}, country={country}")
|
logger.info(f"trace_id={trace_id} 未找到可删除商品分类: shop={shop_name}, country={country}")
|
||||||
final_status_rows = latest_status_rows
|
final_status_rows = latest_status_rows
|
||||||
@@ -783,7 +788,12 @@ class InventoryManage(AmamzonBase):
|
|||||||
]
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _build_deletable_status_rows(cls, rows: Iterable[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
def _build_deletable_status_rows(
|
||||||
|
cls,
|
||||||
|
rows: Iterable[Dict[str, Any]],
|
||||||
|
delete_conditions: Optional[List[Dict[str, Any]]] = None,
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
condition_texts = cls._normalize_delete_condition_texts(delete_conditions)
|
||||||
candidates = []
|
candidates = []
|
||||||
for row in rows:
|
for row in rows:
|
||||||
status = cls._normalize_option_text(row.get("status") or "")
|
status = cls._normalize_option_text(row.get("status") or "")
|
||||||
@@ -795,6 +805,8 @@ class InventoryManage(AmamzonBase):
|
|||||||
canonical_status = cls._canonical_listing_status(status)
|
canonical_status = cls._canonical_listing_status(status)
|
||||||
if not quantity_text or quantity <= 0 or canonical_status in LISTING_STATUS_DELETE_WHITELIST:
|
if not quantity_text or quantity <= 0 or canonical_status in LISTING_STATUS_DELETE_WHITELIST:
|
||||||
continue
|
continue
|
||||||
|
if condition_texts and not cls._matches_delete_conditions(status, condition_texts):
|
||||||
|
continue
|
||||||
|
|
||||||
candidates.append(
|
candidates.append(
|
||||||
{
|
{
|
||||||
@@ -805,6 +817,39 @@ class InventoryManage(AmamzonBase):
|
|||||||
)
|
)
|
||||||
return candidates
|
return candidates
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _normalize_delete_condition_texts(
|
||||||
|
cls,
|
||||||
|
delete_conditions: Optional[List[Dict[str, Any]]],
|
||||||
|
) -> List[str]:
|
||||||
|
texts = []
|
||||||
|
for condition in delete_conditions or []:
|
||||||
|
if isinstance(condition, dict):
|
||||||
|
raw_text = condition.get("conditionText") or condition.get("condition_text") or condition.get("text")
|
||||||
|
else:
|
||||||
|
raw_text = condition
|
||||||
|
text = cls._normalize_option_text(str(raw_text or "")).lower()
|
||||||
|
if text:
|
||||||
|
texts.append(text)
|
||||||
|
return list(dict.fromkeys(texts))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _matches_delete_conditions(cls, status: str, condition_texts: List[str]) -> bool:
|
||||||
|
normalized_status = cls._normalize_option_text(status).lower()
|
||||||
|
canonical_status = cls._canonical_listing_status(status).lower()
|
||||||
|
return any(
|
||||||
|
condition in normalized_status
|
||||||
|
or condition in canonical_status
|
||||||
|
or normalized_status in condition
|
||||||
|
or canonical_status in condition
|
||||||
|
for condition in condition_texts
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _format_delete_conditions(cls, delete_conditions: Optional[List[Dict[str, Any]]]) -> str:
|
||||||
|
texts = cls._normalize_delete_condition_texts(delete_conditions)
|
||||||
|
return ", ".join(texts) if texts else "default"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _format_deletable_candidates(candidates: Iterable[Dict[str, Any]]) -> str:
|
def _format_deletable_candidates(candidates: Iterable[Dict[str, Any]]) -> str:
|
||||||
summary = []
|
summary = []
|
||||||
@@ -1299,6 +1344,7 @@ class PatrolDeleteTask:
|
|||||||
items = data.get("items", [])
|
items = data.get("items", [])
|
||||||
country_sections = data.get("country_sections", [])
|
country_sections = data.get("country_sections", [])
|
||||||
cart_ratios = data.get("cart_ratios", [])
|
cart_ratios = data.get("cart_ratios", [])
|
||||||
|
delete_conditions = data.get("delete_conditions") or data.get("deleteConditions") or []
|
||||||
|
|
||||||
if not task_id:
|
if not task_id:
|
||||||
self.log("任务ID为空,跳过", "WARNING")
|
self.log("任务ID为空,跳过", "WARNING")
|
||||||
@@ -1321,10 +1367,14 @@ class PatrolDeleteTask:
|
|||||||
"failed_count": 0,
|
"failed_count": 0,
|
||||||
"failed_countries": 0,
|
"failed_countries": 0,
|
||||||
"deleted_listings_count": 0,
|
"deleted_listings_count": 0,
|
||||||
|
"delete_conditions": delete_conditions,
|
||||||
"stop_requested": False,
|
"stop_requested": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
self.log(f"开始处理巡店删除任务 {task_id},共 {len(items)} 个店铺,{total_countries} 个国家")
|
self.log(
|
||||||
|
f"开始处理巡店删除任务 {task_id},共 {len(items)} 个店铺,{total_countries} 个国家,"
|
||||||
|
f"删除条件 {len(delete_conditions)} 条"
|
||||||
|
)
|
||||||
for index, shop_item in enumerate(items, 1):
|
for index, shop_item in enumerate(items, 1):
|
||||||
if runing_task[task_id].get("stop_requested", False):
|
if runing_task[task_id].get("stop_requested", False):
|
||||||
self.log(f"检测到任务 {task_id} 的暂停请求,停止处理", "WARNING")
|
self.log(f"检测到任务 {task_id} 的暂停请求,停止处理", "WARNING")
|
||||||
@@ -1333,7 +1383,7 @@ class PatrolDeleteTask:
|
|||||||
|
|
||||||
shop_name = shop_item.get("shopName", "未知店铺")
|
shop_name = shop_item.get("shopName", "未知店铺")
|
||||||
self.log(f"[{index}/{len(items)}] 开始处理店铺: {shop_name}")
|
self.log(f"[{index}/{len(items)}] 开始处理店铺: {shop_name}")
|
||||||
self.process_shop(task_id, shop_item, country_sections, cart_ratios)
|
self.process_shop(task_id, shop_item, country_sections, cart_ratios, delete_conditions)
|
||||||
runing_task[task_id]["processed_shops"] += 1
|
runing_task[task_id]["processed_shops"] += 1
|
||||||
|
|
||||||
if runing_task[task_id].get("stop_requested", False):
|
if runing_task[task_id].get("stop_requested", False):
|
||||||
@@ -1359,6 +1409,7 @@ class PatrolDeleteTask:
|
|||||||
shop_item: Dict[str, Any],
|
shop_item: Dict[str, Any],
|
||||||
country_sections: List[Dict[str, Any]],
|
country_sections: List[Dict[str, Any]],
|
||||||
cart_ratios: List[Dict[str, Any]],
|
cart_ratios: List[Dict[str, Any]],
|
||||||
|
delete_conditions: Optional[List[Dict[str, Any]]] = None,
|
||||||
):
|
):
|
||||||
"""处理单个店铺。"""
|
"""处理单个店铺。"""
|
||||||
from config import runing_shop, runing_task
|
from config import runing_shop, runing_task
|
||||||
@@ -1412,6 +1463,7 @@ class PatrolDeleteTask:
|
|||||||
task_id=task_id,
|
task_id=task_id,
|
||||||
shop_name=shop_name,
|
shop_name=shop_name,
|
||||||
country_name=country_name,
|
country_name=country_name,
|
||||||
|
delete_conditions=delete_conditions,
|
||||||
)
|
)
|
||||||
country_results.append(country_result)
|
country_results.append(country_result)
|
||||||
aggregated_sections[country_name] = country_result["countrySection"]
|
aggregated_sections[country_name] = country_result["countrySection"]
|
||||||
@@ -1522,6 +1574,7 @@ class PatrolDeleteTask:
|
|||||||
task_id: int,
|
task_id: int,
|
||||||
shop_name: str,
|
shop_name: str,
|
||||||
country_name: str,
|
country_name: str,
|
||||||
|
delete_conditions: Optional[List[Dict[str, Any]]] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
from config import runing_task
|
from config import runing_task
|
||||||
|
|
||||||
@@ -1543,6 +1596,7 @@ class PatrolDeleteTask:
|
|||||||
shop_name,
|
shop_name,
|
||||||
country_name,
|
country_name,
|
||||||
trace_id=trace_id,
|
trace_id=trace_id,
|
||||||
|
delete_conditions=delete_conditions,
|
||||||
)
|
)
|
||||||
latest_status_rows = delete_result.get("statusRows") or []
|
latest_status_rows = delete_result.get("statusRows") or []
|
||||||
self.log(
|
self.log(
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -168,6 +168,7 @@ def serve_assets(filename):
|
|||||||
|
|
||||||
|
|
||||||
@main_bp.route('/new_web_source/<path:filename>')
|
@main_bp.route('/new_web_source/<path:filename>')
|
||||||
|
@login_required
|
||||||
def serve_new_web_source(filename):
|
def serve_new_web_source(filename):
|
||||||
"""提供 static 目录及子目录下的静态文件访问。"""
|
"""提供 static 目录及子目录下的静态文件访问。"""
|
||||||
filepath = os.path.normpath(os.path.join("new_web_source", filename))
|
filepath = os.path.normpath(os.path.join("new_web_source", filename))
|
||||||
@@ -175,6 +176,21 @@ def serve_new_web_source(filename):
|
|||||||
file_abs = os.path.abspath(filepath)
|
file_abs = os.path.abspath(filepath)
|
||||||
if not file_abs.startswith(static_abs) or not os.path.isfile(file_abs):
|
if not file_abs.startswith(static_abs) or not os.path.isfile(file_abs):
|
||||||
return '', 404
|
return '', 404
|
||||||
|
if filename.lower().endswith('.html'):
|
||||||
|
with open(file_abs, 'rb') as f:
|
||||||
|
content = f.read().decode('utf-8', errors='replace')
|
||||||
|
raw_uid = session.get('user_id')
|
||||||
|
uid_value = str(int(raw_uid)) if str(raw_uid or '').isdigit() else '""'
|
||||||
|
uid_script = (
|
||||||
|
"\n<script>"
|
||||||
|
f"window.localStorage.setItem('uid', {uid_value});"
|
||||||
|
"</script>\n"
|
||||||
|
)
|
||||||
|
if '</head>' in content:
|
||||||
|
content = content.replace('</head>', uid_script + '</head>', 1)
|
||||||
|
else:
|
||||||
|
content = uid_script + content
|
||||||
|
return render_template_string(content)
|
||||||
return send_file(file_abs, as_attachment=False)
|
return send_file(file_abs, as_attachment=False)
|
||||||
|
|
||||||
@main_bp.route('/logo.jpg', methods=['GET'])
|
@main_bp.route('/logo.jpg', methods=['GET'])
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>格式转换 - 数富AI</title>
|
<title>格式转换 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/convert.js"></script>
|
<script type="module" crossorigin src="/assets/convert.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-ruApoCdL.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-BtdhG28Q.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/brand-qwC923-0.js">
|
<link rel="modulepreload" crossorigin href="/assets/brand-Lp7YWffz.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-Dp5dN8OO.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-Dp5dN8OO.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/convert-BKSNvX8i.css">
|
<link rel="stylesheet" crossorigin href="/assets/convert-BKSNvX8i.css">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>数据去重 - 数富AI</title>
|
<title>数据去重 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/dedupe.js"></script>
|
<script type="module" crossorigin src="/assets/dedupe.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-ruApoCdL.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-BtdhG28Q.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/brand-qwC923-0.js">
|
<link rel="modulepreload" crossorigin href="/assets/brand-Lp7YWffz.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-Dp5dN8OO.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-Dp5dN8OO.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/dedupe-DNlVfFj-.css">
|
<link rel="stylesheet" crossorigin href="/assets/dedupe-DNlVfFj-.css">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>删除品牌 - 数富AI</title>
|
<title>删除品牌 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/delete-brand.js"></script>
|
<script type="module" crossorigin src="/assets/delete-brand.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-ruApoCdL.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-BtdhG28Q.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/brand-qwC923-0.js">
|
<link rel="modulepreload" crossorigin href="/assets/brand-Lp7YWffz.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-Dp5dN8OO.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-Dp5dN8OO.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/delete-brand-BfMLVSQU.css">
|
<link rel="stylesheet" crossorigin href="/assets/delete-brand-BfMLVSQU.css">
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>数据拆分 - 数富AI</title>
|
<title>数据拆分 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/split.js"></script>
|
<script type="module" crossorigin src="/assets/split.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-ruApoCdL.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-BtdhG28Q.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/brand-qwC923-0.js">
|
<link rel="modulepreload" crossorigin href="/assets/brand-Lp7YWffz.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-Dp5dN8OO.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-Dp5dN8OO.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/split-BoBVrLdC.css">
|
<link rel="stylesheet" crossorigin href="/assets/split-BoBVrLdC.css">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ public class AppearancePatentHistoryItemVo {
|
|||||||
private String fileStatus;
|
private String fileStatus;
|
||||||
private String fileError;
|
private String fileError;
|
||||||
private Boolean fileReady;
|
private Boolean fileReady;
|
||||||
|
private Integer fileProgressPercent;
|
||||||
|
private Integer fileProgressCurrent;
|
||||||
|
private Integer fileProgressTotal;
|
||||||
|
private String fileProgressMessage;
|
||||||
@Schema(description = "任务状态:PENDING=已解析待推送,RUNNING=执行中,SUCCESS=成功,FAILED=失败。", example = "SUCCESS")
|
@Schema(description = "任务状态:PENDING=已解析待推送,RUNNING=执行中,SUCCESS=成功,FAILED=失败。", example = "SUCCESS")
|
||||||
private String taskStatus;
|
private String taskStatus;
|
||||||
@Schema(description = "结果是否成功。true 表示任务完成并生成结果文件;false 表示失败或未完成。", example = "true")
|
@Schema(description = "结果是否成功。true 表示任务完成并生成结果文件;false 表示失败或未完成。", example = "true")
|
||||||
|
|||||||
@@ -34,8 +34,10 @@ import com.nanri.aiimage.modules.task.model.entity.FileResultEntity;
|
|||||||
import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity;
|
import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity;
|
||||||
import com.nanri.aiimage.modules.task.model.entity.TaskFileJobEntity;
|
import com.nanri.aiimage.modules.task.model.entity.TaskFileJobEntity;
|
||||||
import com.nanri.aiimage.modules.task.model.entity.TaskChunkEntity;
|
import com.nanri.aiimage.modules.task.model.entity.TaskChunkEntity;
|
||||||
|
import com.nanri.aiimage.modules.task.model.entity.TaskProgressSnapshotEntity;
|
||||||
import com.nanri.aiimage.modules.task.model.entity.TaskScopeStateEntity;
|
import com.nanri.aiimage.modules.task.model.entity.TaskScopeStateEntity;
|
||||||
import com.nanri.aiimage.modules.task.service.TaskFileJobService;
|
import com.nanri.aiimage.modules.task.service.TaskFileJobService;
|
||||||
|
import com.nanri.aiimage.modules.task.service.TaskProgressSnapshotService;
|
||||||
import com.nanri.aiimage.modules.task.service.TransientPayloadStorageService;
|
import com.nanri.aiimage.modules.task.service.TransientPayloadStorageService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -59,6 +61,7 @@ import org.springframework.transaction.support.TransactionTemplate;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
@@ -106,6 +109,7 @@ public class AppearancePatentTaskService {
|
|||||||
private final AppearancePatentTaskCacheService taskCacheService;
|
private final AppearancePatentTaskCacheService taskCacheService;
|
||||||
private final AppearancePatentProperties properties;
|
private final AppearancePatentProperties properties;
|
||||||
private final TaskFileJobService taskFileJobService;
|
private final TaskFileJobService taskFileJobService;
|
||||||
|
private final TaskProgressSnapshotService taskProgressSnapshotService;
|
||||||
private final TransientPayloadStorageService transientPayloadStorageService;
|
private final TransientPayloadStorageService transientPayloadStorageService;
|
||||||
private final PlatformTransactionManager transactionManager;
|
private final PlatformTransactionManager transactionManager;
|
||||||
|
|
||||||
@@ -678,10 +682,10 @@ public class AppearancePatentTaskService {
|
|||||||
int batchSize = Math.max(1, properties.getCozeBatchSize());
|
int batchSize = Math.max(1, properties.getCozeBatchSize());
|
||||||
List<AppearancePatentResultRowDto> result = new ArrayList<>();
|
List<AppearancePatentResultRowDto> result = new ArrayList<>();
|
||||||
for (int i = 0; i < items.size(); i += batchSize) {
|
for (int i = 0; i < items.size(); i += batchSize) {
|
||||||
|
result.addAll(cozeClient.inspect(items.subList(i, Math.min(i + batchSize, items.size())), prompt));
|
||||||
if (progressHook != null) {
|
if (progressHook != null) {
|
||||||
progressHook.run();
|
progressHook.run();
|
||||||
}
|
}
|
||||||
result.addAll(cozeClient.inspect(items.subList(i, Math.min(i + batchSize, items.size())), prompt));
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -697,9 +701,6 @@ public class AppearancePatentTaskService {
|
|||||||
.orderByAsc(TaskChunkEntity::getChunkIndex));
|
.orderByAsc(TaskChunkEntity::getChunkIndex));
|
||||||
log.info("[appearance-patent] async coze start taskId={} chunks={}", task.getId(), chunks.size());
|
log.info("[appearance-patent] async coze start taskId={} chunks={}", task.getId(), chunks.size());
|
||||||
for (TaskChunkEntity chunk : chunks) {
|
for (TaskChunkEntity chunk : chunks) {
|
||||||
if (progressHook != null) {
|
|
||||||
progressHook.run();
|
|
||||||
}
|
|
||||||
Map<String, AppearancePatentResultRowDto> persistedRows = readChunkRows(chunk);
|
Map<String, AppearancePatentResultRowDto> persistedRows = readChunkRows(chunk);
|
||||||
if (persistedRows.isEmpty()) {
|
if (persistedRows.isEmpty()) {
|
||||||
continue;
|
continue;
|
||||||
@@ -1021,12 +1022,68 @@ public class AppearancePatentTaskService {
|
|||||||
if (result == null || !MODULE_TYPE.equals(result.getModuleType())) {
|
if (result == null || !MODULE_TYPE.equals(result.getModuleType())) {
|
||||||
throw new BusinessException("结果记录不存在");
|
throw new BusinessException("结果记录不存在");
|
||||||
}
|
}
|
||||||
Runnable progressHook = () -> taskFileJobService.touchRunning(job.getId());
|
List<TaskChunkEntity> chunks = taskChunkMapper.selectList(new LambdaQueryWrapper<TaskChunkEntity>()
|
||||||
|
.eq(TaskChunkEntity::getTaskId, task.getId())
|
||||||
|
.eq(TaskChunkEntity::getModuleType, MODULE_TYPE)
|
||||||
|
.orderByAsc(TaskChunkEntity::getChunkIndex));
|
||||||
|
int cozeWorkUnits = countCozeWorkUnits(chunks, Math.max(1, properties.getCozeBatchSize()));
|
||||||
|
int totalProgressUnits = Math.max(3, cozeWorkUnits + 3);
|
||||||
|
int[] completedProgressUnits = {0};
|
||||||
|
saveFileBuildProgress(task, job, totalProgressUnits, completedProgressUnits[0], "正在请求 Coze");
|
||||||
|
Runnable progressHook = () -> {
|
||||||
|
taskFileJobService.touchRunning(job.getId());
|
||||||
|
completedProgressUnits[0] = Math.min(totalProgressUnits - 2, completedProgressUnits[0] + 1);
|
||||||
|
saveFileBuildProgress(task, job, totalProgressUnits, completedProgressUnits[0], "正在请求 Coze");
|
||||||
|
};
|
||||||
applyCozeToPersistedChunks(task, progressHook);
|
applyCozeToPersistedChunks(task, progressHook);
|
||||||
progressHook.run();
|
completedProgressUnits[0] = Math.max(completedProgressUnits[0], totalProgressUnits - 2);
|
||||||
|
saveFileBuildProgress(task, job, totalProgressUnits, completedProgressUnits[0], "正在组装 xlsx");
|
||||||
assembleResultWorkbook(task, result);
|
assembleResultWorkbook(task, result);
|
||||||
progressHook.run();
|
completedProgressUnits[0] = totalProgressUnits - 1;
|
||||||
|
saveFileBuildProgress(task, job, totalProgressUnits, completedProgressUnits[0], "正在上传结果文件");
|
||||||
fileResultMapper.updateById(result);
|
fileResultMapper.updateById(result);
|
||||||
|
saveFileBuildProgress(task, job, totalProgressUnits, totalProgressUnits, "结果文件已生成");
|
||||||
|
}
|
||||||
|
|
||||||
|
private int countCozeWorkUnits(List<TaskChunkEntity> chunks, int batchSize) {
|
||||||
|
if (chunks == null || chunks.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int total = 0;
|
||||||
|
for (TaskChunkEntity chunk : chunks) {
|
||||||
|
Map<String, AppearancePatentResultRowDto> persistedRows = readChunkRows(chunk);
|
||||||
|
if (persistedRows.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int unresolved = pickGroupRepresentativesForCoze(persistedRows.values()).size();
|
||||||
|
if (unresolved > 0) {
|
||||||
|
total += Math.max(1, (unresolved + batchSize - 1) / batchSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveFileBuildProgress(FileTaskEntity task,
|
||||||
|
TaskFileJobEntity job,
|
||||||
|
int total,
|
||||||
|
int completed,
|
||||||
|
String message) {
|
||||||
|
if (task == null || task.getId() == null || job == null || job.getId() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int safeTotal = Math.max(1, total);
|
||||||
|
int safeCompleted = Math.max(0, Math.min(completed, safeTotal));
|
||||||
|
taskProgressSnapshotService.save(
|
||||||
|
task.getId(),
|
||||||
|
MODULE_TYPE,
|
||||||
|
STATUS_RUNNING,
|
||||||
|
safeTotal,
|
||||||
|
safeCompleted,
|
||||||
|
0,
|
||||||
|
job.getScopeKey(),
|
||||||
|
message,
|
||||||
|
Map.of("phase", "RESULT_FILE", "jobId", job.getId())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cleanupResultFileJob(TaskFileJobEntity job) {
|
public void cleanupResultFileJob(TaskFileJobEntity job) {
|
||||||
@@ -1045,6 +1102,14 @@ public class AppearancePatentTaskService {
|
|||||||
private void assembleResultWorkbook(FileTaskEntity task, FileResultEntity result) {
|
private void assembleResultWorkbook(FileTaskEntity task, FileResultEntity result) {
|
||||||
AppearancePatentParsedPayloadDto parsed = readParsedPayload(task);
|
AppearancePatentParsedPayloadDto parsed = readParsedPayload(task);
|
||||||
Map<String, AppearancePatentResultRowDto> resultMap = loadPersistedResultRows(task.getId());
|
Map<String, AppearancePatentResultRowDto> resultMap = loadPersistedResultRows(task.getId());
|
||||||
|
long resolvedRows = parsed.getAllItems().stream()
|
||||||
|
.filter(row -> findResultRow(row, resultMap) != null || findResultRowByAsin(row.getAsin(), resultMap) != null)
|
||||||
|
.count();
|
||||||
|
long reasonRows = resultMap.values().stream()
|
||||||
|
.filter(this::hasReasonFields)
|
||||||
|
.count();
|
||||||
|
log.info("[appearance-patent] assemble workbook taskId={} parsedRows={} resultRows={} resolvedRows={} reasonRows={}",
|
||||||
|
task.getId(), parsed.getAllItems().size(), resultMap.size(), resolvedRows, reasonRows);
|
||||||
File outputDir = new File(storageProperties.getLocalTempDir(), "appearance-patent-result");
|
File outputDir = new File(storageProperties.getLocalTempDir(), "appearance-patent-result");
|
||||||
if (!outputDir.exists() && !outputDir.mkdirs()) {
|
if (!outputDir.exists() && !outputDir.mkdirs()) {
|
||||||
throw new BusinessException("创建结果目录失败");
|
throw new BusinessException("创建结果目录失败");
|
||||||
@@ -1087,8 +1152,8 @@ public class AppearancePatentTaskService {
|
|||||||
headerStyle.setFont(font);
|
headerStyle.setFont(font);
|
||||||
|
|
||||||
List<String> resultHeaders = new ArrayList<>(RESULT_HEADERS);
|
List<String> resultHeaders = new ArrayList<>(RESULT_HEADERS);
|
||||||
resultHeaders.add(4, "title");
|
resultHeaders.add(4, "标题");
|
||||||
resultHeaders.add(5, "url");
|
resultHeaders.add(5, "图片链接");
|
||||||
Row header = sheet.createRow(0);
|
Row header = sheet.createRow(0);
|
||||||
for (int i = 0; i < resultHeaders.size(); i++) {
|
for (int i = 0; i < resultHeaders.size(); i++) {
|
||||||
Cell cell = header.createCell(i);
|
Cell cell = header.createCell(i);
|
||||||
@@ -1099,6 +1164,9 @@ public class AppearancePatentTaskService {
|
|||||||
int rowIndex = 1;
|
int rowIndex = 1;
|
||||||
for (AppearancePatentParsedRowVo parsedRow : parsed.getAllItems()) {
|
for (AppearancePatentParsedRowVo parsedRow : parsed.getAllItems()) {
|
||||||
AppearancePatentResultRowDto resultRow = findResultRow(parsedRow, resultMap);
|
AppearancePatentResultRowDto resultRow = findResultRow(parsedRow, resultMap);
|
||||||
|
if (resultRow == null) {
|
||||||
|
resultRow = findResultRowByAsin(parsedRow.getAsin(), resultMap);
|
||||||
|
}
|
||||||
String missingReason = "";
|
String missingReason = "";
|
||||||
if (resultRow == null) {
|
if (resultRow == null) {
|
||||||
missingReason = hasPromptFields(parsedRow) ? "未匹配到检测结果" : "未送检:缺少标题或图片";
|
missingReason = hasPromptFields(parsedRow) ? "未匹配到检测结果" : "未送检:缺少标题或图片";
|
||||||
@@ -1162,12 +1230,28 @@ public class AppearancePatentTaskService {
|
|||||||
if (normalizedAsin.isBlank() || resultMap == null || resultMap.isEmpty()) {
|
if (normalizedAsin.isBlank() || resultMap == null || resultMap.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
AppearancePatentResultRowDto fallback = null;
|
||||||
for (AppearancePatentResultRowDto row : resultMap.values()) {
|
for (AppearancePatentResultRowDto row : resultMap.values()) {
|
||||||
if (row != null && normalizedAsin.equals(normalize(row.getAsin()).toUpperCase(Locale.ROOT))) {
|
if (row == null || !normalizedAsin.equals(normalize(row.getAsin()).toUpperCase(Locale.ROOT))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (hasResolvedCozeFields(row) || hasReasonFields(row)) {
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
if (fallback == null) {
|
||||||
|
fallback = row;
|
||||||
}
|
}
|
||||||
return null;
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasReasonFields(AppearancePatentResultRowDto row) {
|
||||||
|
if (row == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !normalize(row.getAppearanceReason()).isBlank()
|
||||||
|
|| !normalize(row.getPatentReason()).isBlank()
|
||||||
|
|| !normalize(row.getTitleReason()).isBlank();
|
||||||
}
|
}
|
||||||
|
|
||||||
private ParsedWorkbook parseWorkbook(File input, AppearancePatentSourceFileDto source) {
|
private ParsedWorkbook parseWorkbook(File input, AppearancePatentSourceFileDto source) {
|
||||||
@@ -1468,11 +1552,50 @@ public class AppearancePatentTaskService {
|
|||||||
vo.setFileReady(row.getResultFileUrl() != null && !row.getResultFileUrl().isBlank());
|
vo.setFileReady(row.getResultFileUrl() != null && !row.getResultFileUrl().isBlank());
|
||||||
if (job == null) {
|
if (job == null) {
|
||||||
vo.setFileStatus(Boolean.TRUE.equals(vo.getFileReady()) ? "SUCCESS" : null);
|
vo.setFileStatus(Boolean.TRUE.equals(vo.getFileReady()) ? "SUCCESS" : null);
|
||||||
|
attachFileProgress(vo, row, job);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
vo.setFileJobId(job.getId());
|
vo.setFileJobId(job.getId());
|
||||||
vo.setFileStatus(job.getStatus());
|
vo.setFileStatus(job.getStatus());
|
||||||
vo.setFileError(job.getErrorMessage());
|
vo.setFileError(job.getErrorMessage());
|
||||||
|
attachFileProgress(vo, row, job);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void attachFileProgress(AppearancePatentHistoryItemVo vo, FileResultEntity row, TaskFileJobEntity job) {
|
||||||
|
if (row == null || row.getTaskId() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Boolean.TRUE.equals(vo.getFileReady())) {
|
||||||
|
vo.setFileProgressCurrent(1);
|
||||||
|
vo.setFileProgressTotal(1);
|
||||||
|
vo.setFileProgressPercent(100);
|
||||||
|
vo.setFileProgressMessage("结果文件已生成");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TaskProgressSnapshotEntity snapshot = taskProgressSnapshotService.find(row.getTaskId(), MODULE_TYPE);
|
||||||
|
if (snapshot == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int total = snapshot.getTotalCount() == null ? 0 : snapshot.getTotalCount();
|
||||||
|
int current = snapshot.getSuccessCount() == null ? 0 : snapshot.getSuccessCount();
|
||||||
|
if (total <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
current = Math.max(0, Math.min(current, total));
|
||||||
|
int percent = Math.max(1, Math.min(99, (int) Math.floor(current * 100.0 / total)));
|
||||||
|
if (job != null && STATUS_RUNNING.equals(job.getStatus())) {
|
||||||
|
LocalDateTime baseTime = snapshot.getUpdatedAt() != null ? snapshot.getUpdatedAt() : job.getUpdatedAt();
|
||||||
|
long elapsedSeconds = baseTime == null ? 0 : Math.max(0, Duration.between(baseTime, LocalDateTime.now()).getSeconds());
|
||||||
|
if (current <= 0) {
|
||||||
|
percent = Math.max(percent, Math.min(35, 8 + (int) (elapsedSeconds / 6)));
|
||||||
|
} else if (current < total) {
|
||||||
|
percent = Math.max(percent, Math.min(92, percent + (int) (elapsedSeconds / 10)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vo.setFileProgressCurrent(current);
|
||||||
|
vo.setFileProgressTotal(total);
|
||||||
|
vo.setFileProgressPercent(percent);
|
||||||
|
vo.setFileProgressMessage(snapshot.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
private String fmt(LocalDateTime t) {
|
private String fmt(LocalDateTime t) {
|
||||||
|
|||||||
@@ -279,16 +279,21 @@ public class BrandTaskService {
|
|||||||
throw new BusinessException("totalLines 超过原始文件行数: " + fileUrl);
|
throw new BusinessException("totalLines 超过原始文件行数: " + fileUrl);
|
||||||
}
|
}
|
||||||
BrandTaskStorageService.ChunkStoreResult storeResult = brandTaskStorageService.storeChunk(taskId, resultFile);
|
BrandTaskStorageService.ChunkStoreResult storeResult = brandTaskStorageService.storeChunk(taskId, resultFile);
|
||||||
|
BrandTaskStorageService.ChunkStoreResult refreshResult = brandTaskStorageService.refreshFileAggregate(taskId, fileUrl);
|
||||||
|
BrandFileAggregateCacheDto aggregate = refreshResult.aggregate() == null ? storeResult.aggregate() : refreshResult.aggregate();
|
||||||
finishedCount = Math.max(finishedCount, storeResult.finishedFiles());
|
finishedCount = Math.max(finishedCount, storeResult.finishedFiles());
|
||||||
log.info("[brand-submit] taskId={} fileUrl={} chunk={}/{} stored={} newlyCompletedFile={} finishedFiles={}",
|
finishedCount = Math.max(finishedCount, refreshResult.finishedFiles());
|
||||||
|
log.info("[brand-submit] taskId={} fileUrl={} chunk={}/{} stored={} newlyCompletedFile={} finishedFiles={} receivedChunks={} processedLines={} totalLines={}",
|
||||||
taskId,
|
taskId,
|
||||||
fileUrl,
|
fileUrl,
|
||||||
resultFile.getChunkIndex(),
|
resultFile.getChunkIndex(),
|
||||||
resultFile.getChunkTotal(),
|
resultFile.getChunkTotal(),
|
||||||
storeResult.stored(),
|
storeResult.stored(),
|
||||||
storeResult.newlyCompletedFile(),
|
storeResult.newlyCompletedFile() || refreshResult.newlyCompletedFile(),
|
||||||
finishedCount);
|
finishedCount,
|
||||||
BrandFileAggregateCacheDto aggregate = storeResult.aggregate();
|
aggregate == null ? 0 : defaultInteger(aggregate.getReceivedChunkCount()),
|
||||||
|
aggregate == null ? 0 : defaultInteger(aggregate.getProcessedLineCount()),
|
||||||
|
aggregate == null ? 0 : defaultInteger(aggregate.getTotalLines()));
|
||||||
String fileName = blankToDefault(sourceFile.getOriginalFilename(), sourceFile.getFileUrl());
|
String fileName = blankToDefault(sourceFile.getOriginalFilename(), sourceFile.getFileUrl());
|
||||||
int fileIndex = sourceIndexByUrl.getOrDefault(fileUrl, 0);
|
int fileIndex = sourceIndexByUrl.getOrDefault(fileUrl, 0);
|
||||||
int currentLine = aggregate == null ? 0 : defaultInteger(aggregate.getProcessedLineCount());
|
int currentLine = aggregate == null ? 0 : defaultInteger(aggregate.getProcessedLineCount());
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ public class BrandTaskStorageService {
|
|||||||
if (existingChunk != null) {
|
if (existingChunk != null) {
|
||||||
if (payloadHash.equals(existingChunk.getPayloadHash())) {
|
if (payloadHash.equals(existingChunk.getPayloadHash())) {
|
||||||
BrandFileAggregateCacheDto aggregate = rebuildAggregate(taskId, scopeKey, scopeHash);
|
BrandFileAggregateCacheDto aggregate = rebuildAggregate(taskId, scopeKey, scopeHash);
|
||||||
|
saveAggregate(taskId, scopeKey, scopeHash, aggregate, now);
|
||||||
return new ChunkStoreResult(false, false, countCompletedFiles(taskId), aggregate);
|
return new ChunkStoreResult(false, false, countCompletedFiles(taskId), aggregate);
|
||||||
}
|
}
|
||||||
throw new BusinessException("Brand task chunk conflict: " + scopeKey + "#" + file.getChunkIndex());
|
throw new BusinessException("Brand task chunk conflict: " + scopeKey + "#" + file.getChunkIndex());
|
||||||
@@ -155,6 +156,7 @@ public class BrandTaskStorageService {
|
|||||||
if (concurrentChunk != null && payloadHash.equals(concurrentChunk.getPayloadHash())) {
|
if (concurrentChunk != null && payloadHash.equals(concurrentChunk.getPayloadHash())) {
|
||||||
transientPayloadStorageService.deletePayloadIfPresent(storedPayload);
|
transientPayloadStorageService.deletePayloadIfPresent(storedPayload);
|
||||||
BrandFileAggregateCacheDto aggregate = rebuildAggregate(taskId, scopeKey, scopeHash);
|
BrandFileAggregateCacheDto aggregate = rebuildAggregate(taskId, scopeKey, scopeHash);
|
||||||
|
saveAggregate(taskId, scopeKey, scopeHash, aggregate, now);
|
||||||
return new ChunkStoreResult(false, false, countCompletedFiles(taskId), aggregate);
|
return new ChunkStoreResult(false, false, countCompletedFiles(taskId), aggregate);
|
||||||
}
|
}
|
||||||
transientPayloadStorageService.deletePayloadIfPresent(storedPayload);
|
transientPayloadStorageService.deletePayloadIfPresent(storedPayload);
|
||||||
@@ -218,6 +220,22 @@ public class BrandTaskStorageService {
|
|||||||
return count == null ? 0 : count.intValue();
|
return count == null ? 0 : count.intValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public ChunkStoreResult refreshFileAggregate(Long taskId, String fileUrl) {
|
||||||
|
if (taskId == null || taskId <= 0 || isBlank(fileUrl)) {
|
||||||
|
return new ChunkStoreResult(false, false, countCompletedFiles(taskId), null);
|
||||||
|
}
|
||||||
|
String scopeKey = normalize(fileUrl);
|
||||||
|
String scopeHash = hash(scopeKey);
|
||||||
|
BrandFileAggregateCacheDto aggregate = rebuildAggregate(taskId, scopeKey, scopeHash);
|
||||||
|
if (aggregate == null) {
|
||||||
|
return new ChunkStoreResult(false, false, countCompletedFiles(taskId), null);
|
||||||
|
}
|
||||||
|
boolean completed = Boolean.TRUE.equals(aggregate.getCompleted());
|
||||||
|
saveAggregate(taskId, scopeKey, scopeHash, aggregate, LocalDateTime.now());
|
||||||
|
return new ChunkStoreResult(false, completed, countCompletedFiles(taskId), aggregate);
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void deleteTaskData(Long taskId) {
|
public void deleteTaskData(Long taskId) {
|
||||||
if (taskId == null || taskId <= 0) {
|
if (taskId == null || taskId <= 0) {
|
||||||
|
|||||||
@@ -14,9 +14,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.core.io.Resource;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
@@ -27,6 +25,8 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@RequestMapping("/api/dedupe")
|
@RequestMapping("/api/dedupe")
|
||||||
@@ -94,14 +94,14 @@ public class DedupeRunController {
|
|||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "返回结果文件流"),
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "返回结果文件流"),
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "结果文件不存在")
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "结果文件不存在")
|
||||||
})
|
})
|
||||||
public ResponseEntity<Resource> download(
|
public ResponseEntity<Void> download(
|
||||||
@Parameter(description = "去重结果记录 ID", required = true) @PathVariable Long resultId,
|
@Parameter(description = "去重结果记录 ID", required = true) @PathVariable Long resultId,
|
||||||
@Parameter(name = "user_id", description = "当前登录用户 ID", required = true, in = ParameterIn.QUERY)
|
@Parameter(name = "user_id", description = "当前登录用户 ID", required = true, in = ParameterIn.QUERY)
|
||||||
@RequestParam("user_id") Long userId) {
|
@RequestParam("user_id") Long userId) {
|
||||||
Resource resource = dedupeRunService.getResultFile(resultId, userId);
|
String downloadUrl = dedupeRunService.getResultDownloadUrl(resultId, userId);
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.status(302)
|
||||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
.location(URI.create(downloadUrl))
|
||||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment")
|
.header(HttpHeaders.CACHE_CONTROL, "no-store")
|
||||||
.body(resource);
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ import org.apache.poi.ss.usermodel.Sheet;
|
|||||||
import org.apache.poi.ss.usermodel.Workbook;
|
import org.apache.poi.ss.usermodel.Workbook;
|
||||||
import org.apache.poi.ss.util.WorkbookUtil;
|
import org.apache.poi.ss.util.WorkbookUtil;
|
||||||
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
|
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
|
||||||
import org.springframework.core.io.FileSystemResource;
|
|
||||||
import org.springframework.core.io.Resource;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -229,7 +227,7 @@ public class DedupeRunService {
|
|||||||
vo.setResultId(entity.getId());
|
vo.setResultId(entity.getId());
|
||||||
vo.setSourceFilename(entity.getSourceFilename());
|
vo.setSourceFilename(entity.getSourceFilename());
|
||||||
vo.setOutputFilename(entity.getResultFilename());
|
vo.setOutputFilename(entity.getResultFilename());
|
||||||
vo.setDownloadUrl(null);
|
vo.setDownloadUrl(entity.getResultFileUrl() == null ? null : ossStorageService.generateFreshDownloadUrl(entity.getResultFileUrl()));
|
||||||
vo.setSuccess(entity.getSuccess() != null && entity.getSuccess() == 1);
|
vo.setSuccess(entity.getSuccess() != null && entity.getSuccess() == 1);
|
||||||
vo.setError(entity.getErrorMessage());
|
vo.setError(entity.getErrorMessage());
|
||||||
return vo;
|
return vo;
|
||||||
@@ -245,16 +243,12 @@ public class DedupeRunService {
|
|||||||
fileResultMapper.deleteById(resultId);
|
fileResultMapper.deleteById(resultId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Resource getResultFile(Long resultId, Long userId) {
|
public String getResultDownloadUrl(Long resultId, Long userId) {
|
||||||
FileResultEntity entity = fileResultMapper.selectById(resultId);
|
FileResultEntity entity = fileResultMapper.selectById(resultId);
|
||||||
if (entity == null || entity.getResultFileUrl() == null || !"DEDUPE".equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {
|
if (entity == null || entity.getResultFileUrl() == null || !"DEDUPE".equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {
|
||||||
throw new BusinessException("结果文件不存在");
|
throw new BusinessException("结果文件不存在");
|
||||||
}
|
}
|
||||||
File file = new File(entity.getResultFileUrl());
|
return ossStorageService.generateFreshDownloadUrl(entity.getResultFileUrl());
|
||||||
if (!file.exists()) {
|
|
||||||
throw new BusinessException("结果文件不存在");
|
|
||||||
}
|
|
||||||
return new FileSystemResource(file);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void cleanExcelByLegacyRules(File inputFile, File outputFile, List<String> selectedColumns,
|
private void cleanExcelByLegacyRules(File inputFile, File outputFile, List<String> selectedColumns,
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package com.nanri.aiimage.modules.patroldelete.controller;
|
package com.nanri.aiimage.modules.patroldelete.controller;
|
||||||
|
|
||||||
import com.nanri.aiimage.common.api.ApiResponse;
|
import com.nanri.aiimage.common.api.ApiResponse;
|
||||||
|
import com.nanri.aiimage.modules.patroldelete.model.dto.PatrolDeleteConditionAddRequest;
|
||||||
import com.nanri.aiimage.modules.patroldelete.model.dto.PatrolDeleteCreateTaskRequest;
|
import com.nanri.aiimage.modules.patroldelete.model.dto.PatrolDeleteCreateTaskRequest;
|
||||||
import com.nanri.aiimage.modules.patroldelete.model.dto.PatrolDeleteSubmitResultRequest;
|
import com.nanri.aiimage.modules.patroldelete.model.dto.PatrolDeleteSubmitResultRequest;
|
||||||
import com.nanri.aiimage.modules.patroldelete.model.dto.PatrolDeleteTaskBatchRequest;
|
import com.nanri.aiimage.modules.patroldelete.model.dto.PatrolDeleteTaskBatchRequest;
|
||||||
import com.nanri.aiimage.modules.patroldelete.model.vo.PatrolDeleteCreateTaskVo;
|
import com.nanri.aiimage.modules.patroldelete.model.vo.PatrolDeleteCreateTaskVo;
|
||||||
|
import com.nanri.aiimage.modules.patroldelete.model.vo.PatrolDeleteConditionVo;
|
||||||
import com.nanri.aiimage.modules.patroldelete.model.vo.PatrolDeleteHistoryVo;
|
import com.nanri.aiimage.modules.patroldelete.model.vo.PatrolDeleteHistoryVo;
|
||||||
import com.nanri.aiimage.modules.patroldelete.model.vo.PatrolDeleteTaskBatchVo;
|
import com.nanri.aiimage.modules.patroldelete.model.vo.PatrolDeleteTaskBatchVo;
|
||||||
import com.nanri.aiimage.modules.patroldelete.service.PatrolDeleteResolveService;
|
import com.nanri.aiimage.modules.patroldelete.service.PatrolDeleteResolveService;
|
||||||
@@ -92,6 +94,32 @@ public class PatrolDeleteController {
|
|||||||
return ApiResponse.success(null);
|
return ApiResponse.success(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/conditions")
|
||||||
|
@Operation(summary = "查询删除条件", description = "返回当前用户保存的巡店删除条件。")
|
||||||
|
public ApiResponse<List<PatrolDeleteConditionVo>> listConditions(
|
||||||
|
@Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1")
|
||||||
|
@RequestParam("user_id") Long userId) {
|
||||||
|
return ApiResponse.success(patrolDeleteResolveService.listConditions(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/conditions")
|
||||||
|
@Operation(summary = "新增删除条件", description = "保存一条当前用户可复用的巡店删除条件。")
|
||||||
|
public ApiResponse<PatrolDeleteConditionVo> addCondition(
|
||||||
|
@Valid @RequestBody PatrolDeleteConditionAddRequest request) {
|
||||||
|
return ApiResponse.success(patrolDeleteResolveService.addCondition(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/conditions/{id}")
|
||||||
|
@Operation(summary = "删除删除条件", description = "删除当前用户名下的一条巡店删除条件。")
|
||||||
|
public ApiResponse<Void> deleteCondition(
|
||||||
|
@Parameter(description = "删除条件主键", example = "10")
|
||||||
|
@PathVariable Long id,
|
||||||
|
@Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1")
|
||||||
|
@RequestParam("user_id") Long userId) {
|
||||||
|
patrolDeleteResolveService.deleteCondition(userId, id);
|
||||||
|
return ApiResponse.success(null);
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/match-shops")
|
@PostMapping("/match-shops")
|
||||||
@Operation(summary = "批量匹配店铺", description = "根据店铺名批量匹配紫鸟店铺索引,返回是否命中、店铺 ID、平台、公司和匹配状态。")
|
@Operation(summary = "批量匹配店铺", description = "根据店铺名批量匹配紫鸟店铺索引,返回是否命中、店铺 ID、平台、公司和匹配状态。")
|
||||||
public ApiResponse<ProductRiskMatchShopsVo> matchShops(
|
public ApiResponse<ProductRiskMatchShopsVo> matchShops(
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.nanri.aiimage.modules.patroldelete.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.nanri.aiimage.modules.patroldelete.model.entity.PatrolDeleteConditionEntity;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface PatrolDeleteConditionMapper extends BaseMapper<PatrolDeleteConditionEntity> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.nanri.aiimage.modules.patroldelete.model.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "巡店删除条件新增请求")
|
||||||
|
public class PatrolDeleteConditionAddRequest {
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@JsonProperty("user_id")
|
||||||
|
@Schema(description = "当前用户 ID", example = "1", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
@JsonProperty("condition_text")
|
||||||
|
@Schema(description = "删除条件文本", example = "删除商品信息草稿", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String conditionText;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.nanri.aiimage.modules.patroldelete.model.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName("biz_patrol_delete_condition")
|
||||||
|
public class PatrolDeleteConditionEntity {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
private Long userId;
|
||||||
|
private String conditionText;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.nanri.aiimage.modules.patroldelete.model.vo;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class PatrolDeleteConditionVo {
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@JsonProperty("conditionText")
|
||||||
|
private String conditionText;
|
||||||
|
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@@ -2,8 +2,12 @@ package com.nanri.aiimage.modules.patroldelete.service;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.nanri.aiimage.common.exception.BusinessException;
|
import com.nanri.aiimage.common.exception.BusinessException;
|
||||||
|
import com.nanri.aiimage.modules.patroldelete.mapper.PatrolDeleteConditionMapper;
|
||||||
import com.nanri.aiimage.modules.patroldelete.mapper.PatrolDeleteShopCandidateMapper;
|
import com.nanri.aiimage.modules.patroldelete.mapper.PatrolDeleteShopCandidateMapper;
|
||||||
|
import com.nanri.aiimage.modules.patroldelete.model.dto.PatrolDeleteConditionAddRequest;
|
||||||
|
import com.nanri.aiimage.modules.patroldelete.model.entity.PatrolDeleteConditionEntity;
|
||||||
import com.nanri.aiimage.modules.patroldelete.model.entity.PatrolDeleteShopCandidateEntity;
|
import com.nanri.aiimage.modules.patroldelete.model.entity.PatrolDeleteShopCandidateEntity;
|
||||||
|
import com.nanri.aiimage.modules.patroldelete.model.vo.PatrolDeleteConditionVo;
|
||||||
import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskCandidateAddRequest;
|
import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskCandidateAddRequest;
|
||||||
import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskMatchShopsRequest;
|
import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskMatchShopsRequest;
|
||||||
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskCandidateVo;
|
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskCandidateVo;
|
||||||
@@ -27,6 +31,7 @@ import java.util.Objects;
|
|||||||
public class PatrolDeleteResolveService {
|
public class PatrolDeleteResolveService {
|
||||||
|
|
||||||
private final PatrolDeleteShopCandidateMapper candidateMapper;
|
private final PatrolDeleteShopCandidateMapper candidateMapper;
|
||||||
|
private final PatrolDeleteConditionMapper conditionMapper;
|
||||||
private final ZiniaoShopSwitchService ziniaoShopSwitchService;
|
private final ZiniaoShopSwitchService ziniaoShopSwitchService;
|
||||||
|
|
||||||
public List<ProductRiskCandidateVo> listCandidates(Long userId) {
|
public List<ProductRiskCandidateVo> listCandidates(Long userId) {
|
||||||
@@ -127,6 +132,67 @@ public class PatrolDeleteResolveService {
|
|||||||
return count == null ? 0L : count;
|
return count == null ? 0L : count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<PatrolDeleteConditionVo> listConditions(Long userId) {
|
||||||
|
validateUserId(userId);
|
||||||
|
List<PatrolDeleteConditionEntity> rows = conditionMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<PatrolDeleteConditionEntity>()
|
||||||
|
.eq(PatrolDeleteConditionEntity::getUserId, userId)
|
||||||
|
.orderByDesc(PatrolDeleteConditionEntity::getId));
|
||||||
|
List<PatrolDeleteConditionVo> list = new ArrayList<>();
|
||||||
|
for (PatrolDeleteConditionEntity row : rows) {
|
||||||
|
list.add(toConditionVo(row));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public PatrolDeleteConditionVo addCondition(PatrolDeleteConditionAddRequest request) {
|
||||||
|
validateUserId(request.getUserId());
|
||||||
|
String text = normalizeConditionText(request.getConditionText());
|
||||||
|
if (text.isBlank()) {
|
||||||
|
throw new BusinessException("删除条件不能为空");
|
||||||
|
}
|
||||||
|
PatrolDeleteConditionEntity existing = conditionMapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<PatrolDeleteConditionEntity>()
|
||||||
|
.eq(PatrolDeleteConditionEntity::getUserId, request.getUserId())
|
||||||
|
.eq(PatrolDeleteConditionEntity::getConditionText, text)
|
||||||
|
.last("limit 1"));
|
||||||
|
if (existing != null) {
|
||||||
|
return toConditionVo(existing);
|
||||||
|
}
|
||||||
|
PatrolDeleteConditionEntity entity = new PatrolDeleteConditionEntity();
|
||||||
|
entity.setUserId(request.getUserId());
|
||||||
|
entity.setConditionText(text);
|
||||||
|
entity.setCreatedAt(LocalDateTime.now());
|
||||||
|
conditionMapper.insert(entity);
|
||||||
|
return toConditionVo(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void deleteCondition(Long userId, Long id) {
|
||||||
|
validateUserId(userId);
|
||||||
|
if (id == null || id <= 0) {
|
||||||
|
throw new BusinessException("id 不合法");
|
||||||
|
}
|
||||||
|
PatrolDeleteConditionEntity row = conditionMapper.selectById(id);
|
||||||
|
if (row == null || !userId.equals(row.getUserId())) {
|
||||||
|
throw new BusinessException("记录不存在");
|
||||||
|
}
|
||||||
|
conditionMapper.deleteById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PatrolDeleteConditionVo toConditionVo(PatrolDeleteConditionEntity row) {
|
||||||
|
PatrolDeleteConditionVo vo = new PatrolDeleteConditionVo();
|
||||||
|
vo.setId(row.getId());
|
||||||
|
vo.setConditionText(row.getConditionText());
|
||||||
|
vo.setCreatedAt(row.getCreatedAt());
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeConditionText(String value) {
|
||||||
|
return value == null ? "" : value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
private ProductRiskShopQueueItemVo matchOneShop(String shopName) {
|
private ProductRiskShopQueueItemVo matchOneShop(String shopName) {
|
||||||
ProductRiskShopQueueItemVo item = new ProductRiskShopQueueItemVo();
|
ProductRiskShopQueueItemVo item = new ProductRiskShopQueueItemVo();
|
||||||
item.setShopName(shopName);
|
item.setShopName(shopName);
|
||||||
|
|||||||
@@ -149,11 +149,16 @@ public class PatrolDeleteTaskService {
|
|||||||
|
|
||||||
Map<Long, FileTaskEntity> taskMap = loadTaskMap(entities);
|
Map<Long, FileTaskEntity> taskMap = loadTaskMap(entities);
|
||||||
Map<Long, Map<Long, PatrolDeleteResultItemVo>> snapshotMap = buildSnapshotMap(taskMap);
|
Map<Long, Map<Long, PatrolDeleteResultItemVo>> snapshotMap = buildSnapshotMap(taskMap);
|
||||||
|
Map<Long, TaskFileJobEntity> jobMap = taskFileJobService.findAssembleJobsByResultIds(MODULE_TYPE, entities.stream()
|
||||||
|
.map(FileResultEntity::getId)
|
||||||
|
.filter(id -> id != null && id > 0)
|
||||||
|
.distinct()
|
||||||
|
.toList());
|
||||||
List<PatrolDeleteResultItemVo> items = new ArrayList<>();
|
List<PatrolDeleteResultItemVo> items = new ArrayList<>();
|
||||||
for (FileResultEntity entity : entities) {
|
for (FileResultEntity entity : entities) {
|
||||||
FileTaskEntity task = taskMap.get(entity.getTaskId());
|
FileTaskEntity task = taskMap.get(entity.getTaskId());
|
||||||
PatrolDeleteResultItemVo snapshot = snapshotMap.getOrDefault(entity.getTaskId(), Map.of()).get(entity.getId());
|
PatrolDeleteResultItemVo snapshot = snapshotMap.getOrDefault(entity.getTaskId(), Map.of()).get(entity.getId());
|
||||||
items.add(toHistoryItem(entity, task, snapshot));
|
items.add(toHistoryItem(entity, task, snapshot, jobMap.get(entity.getId())));
|
||||||
}
|
}
|
||||||
vo.setItems(items);
|
vo.setItems(items);
|
||||||
return vo;
|
return vo;
|
||||||
@@ -183,6 +188,11 @@ public class PatrolDeleteTaskService {
|
|||||||
for (FileResultEntity row : rows) {
|
for (FileResultEntity row : rows) {
|
||||||
rowsByTaskId.computeIfAbsent(row.getTaskId(), ignored -> new ArrayList<>()).add(row);
|
rowsByTaskId.computeIfAbsent(row.getTaskId(), ignored -> new ArrayList<>()).add(row);
|
||||||
}
|
}
|
||||||
|
Map<Long, TaskFileJobEntity> jobMap = taskFileJobService.findAssembleJobsByResultIds(MODULE_TYPE, rows.stream()
|
||||||
|
.map(FileResultEntity::getId)
|
||||||
|
.filter(id -> id != null && id > 0)
|
||||||
|
.distinct()
|
||||||
|
.toList());
|
||||||
|
|
||||||
for (Long taskId : normalizedTaskIds) {
|
for (Long taskId : normalizedTaskIds) {
|
||||||
FileTaskEntity task = taskMap.get(taskId);
|
FileTaskEntity task = taskMap.get(taskId);
|
||||||
@@ -195,7 +205,7 @@ public class PatrolDeleteTaskService {
|
|||||||
batch.getMissingTaskIds().add(taskId);
|
batch.getMissingTaskIds().add(taskId);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
batch.getItems().addAll(buildProgressItems(task, taskRows));
|
batch.getItems().addAll(buildProgressItems(task, taskRows, jobMap));
|
||||||
}
|
}
|
||||||
return batch;
|
return batch;
|
||||||
}
|
}
|
||||||
@@ -252,8 +262,6 @@ public class PatrolDeleteTaskService {
|
|||||||
task.setCreatedAt(now);
|
task.setCreatedAt(now);
|
||||||
task.setUpdatedAt(now);
|
task.setUpdatedAt(now);
|
||||||
fileTaskMapper.insert(task);
|
fileTaskMapper.insert(task);
|
||||||
taskCacheService.saveTaskCache(task);
|
|
||||||
taskCacheService.touchTaskHeartbeat(task.getId());
|
|
||||||
|
|
||||||
List<PatrolDeleteResultItemVo> snapshots = new ArrayList<>();
|
List<PatrolDeleteResultItemVo> snapshots = new ArrayList<>();
|
||||||
for (PatrolDeleteTaskItemDto item : uniqueItems) {
|
for (PatrolDeleteTaskItemDto item : uniqueItems) {
|
||||||
@@ -272,9 +280,6 @@ public class PatrolDeleteTaskService {
|
|||||||
fileResultMapper.insert(result);
|
fileResultMapper.insert(result);
|
||||||
snapshots.add(toSnapshotVo(result, item, task.getStatus(), null));
|
snapshots.add(toSnapshotVo(result, item, task.getStatus(), null));
|
||||||
}
|
}
|
||||||
persistTaskJson(task, uniqueItems, snapshots);
|
|
||||||
taskCacheService.saveTaskCache(task);
|
|
||||||
|
|
||||||
PatrolDeleteCreateTaskVo vo = new PatrolDeleteCreateTaskVo();
|
PatrolDeleteCreateTaskVo vo = new PatrolDeleteCreateTaskVo();
|
||||||
vo.setTaskId(task.getId());
|
vo.setTaskId(task.getId());
|
||||||
vo.setItems(snapshots);
|
vo.setItems(snapshots);
|
||||||
@@ -291,7 +296,8 @@ public class PatrolDeleteTaskService {
|
|||||||
|
|
||||||
FileTaskEntity task = loadTaskForExecution(taskId);
|
FileTaskEntity task = loadTaskForExecution(taskId);
|
||||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
||||||
throw new BusinessException("任务不存在");
|
log.info("[patrol-delete] ignore result callback for deleted task taskId={}", taskId);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) {
|
if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) {
|
||||||
throw new BusinessException("任务已结束,拒绝重复提交");
|
throw new BusinessException("任务已结束,拒绝重复提交");
|
||||||
@@ -419,6 +425,7 @@ public class PatrolDeleteTaskService {
|
|||||||
fileResultMapper.delete(new LambdaQueryWrapper<FileResultEntity>()
|
fileResultMapper.delete(new LambdaQueryWrapper<FileResultEntity>()
|
||||||
.eq(FileResultEntity::getTaskId, taskId)
|
.eq(FileResultEntity::getTaskId, taskId)
|
||||||
.eq(FileResultEntity::getModuleType, MODULE_TYPE));
|
.eq(FileResultEntity::getModuleType, MODULE_TYPE));
|
||||||
|
cleanupTaskAuxiliaryData(taskId);
|
||||||
fileTaskMapper.deleteById(taskId);
|
fileTaskMapper.deleteById(taskId);
|
||||||
taskCacheService.deleteTaskCache(taskId);
|
taskCacheService.deleteTaskCache(taskId);
|
||||||
}
|
}
|
||||||
@@ -432,6 +439,7 @@ public class PatrolDeleteTaskService {
|
|||||||
}
|
}
|
||||||
Long taskId = entity.getTaskId();
|
Long taskId = entity.getTaskId();
|
||||||
fileResultMapper.deleteById(resultId);
|
fileResultMapper.deleteById(resultId);
|
||||||
|
cleanupResultAuxiliaryData(taskId, resultId);
|
||||||
reconcileTaskAfterResultRemoval(taskId);
|
reconcileTaskAfterResultRemoval(taskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -445,6 +453,7 @@ public class PatrolDeleteTaskService {
|
|||||||
}
|
}
|
||||||
List<FileResultEntity> rows = listTaskRows(taskId);
|
List<FileResultEntity> rows = listTaskRows(taskId);
|
||||||
if (rows.isEmpty()) {
|
if (rows.isEmpty()) {
|
||||||
|
cleanupTaskAuxiliaryData(taskId);
|
||||||
fileTaskMapper.deleteById(taskId);
|
fileTaskMapper.deleteById(taskId);
|
||||||
taskCacheService.deleteTaskCache(taskId);
|
taskCacheService.deleteTaskCache(taskId);
|
||||||
return;
|
return;
|
||||||
@@ -455,6 +464,17 @@ public class PatrolDeleteTaskService {
|
|||||||
taskCacheService.saveTaskCache(task);
|
taskCacheService.saveTaskCache(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void cleanupTaskAuxiliaryData(Long taskId) {
|
||||||
|
taskResultItemService.deleteTaskItems(taskId, MODULE_TYPE);
|
||||||
|
taskProgressSnapshotService.delete(taskId, MODULE_TYPE);
|
||||||
|
taskFileJobService.deleteTaskJobs(taskId, MODULE_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanupResultAuxiliaryData(Long taskId, Long resultId) {
|
||||||
|
taskResultItemService.deleteResultItem(taskId, MODULE_TYPE, resultId);
|
||||||
|
taskFileJobService.deleteResultJobs(taskId, MODULE_TYPE, resultId);
|
||||||
|
}
|
||||||
|
|
||||||
private long countTasks(Long userId, List<String> statuses) {
|
private long countTasks(Long userId, List<String> statuses) {
|
||||||
Long count = fileTaskMapper.selectCount(new LambdaQueryWrapper<FileTaskEntity>()
|
Long count = fileTaskMapper.selectCount(new LambdaQueryWrapper<FileTaskEntity>()
|
||||||
.eq(FileTaskEntity::getModuleType, MODULE_TYPE)
|
.eq(FileTaskEntity::getModuleType, MODULE_TYPE)
|
||||||
@@ -517,6 +537,10 @@ public class PatrolDeleteTaskService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private PatrolDeleteResultItemVo toHistoryItem(FileResultEntity entity, FileTaskEntity task, PatrolDeleteResultItemVo snapshot) {
|
private PatrolDeleteResultItemVo toHistoryItem(FileResultEntity entity, FileTaskEntity task, PatrolDeleteResultItemVo snapshot) {
|
||||||
|
return toHistoryItem(entity, task, snapshot, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PatrolDeleteResultItemVo toHistoryItem(FileResultEntity entity, FileTaskEntity task, PatrolDeleteResultItemVo snapshot, TaskFileJobEntity job) {
|
||||||
PatrolDeleteResultItemVo item = snapshot != null ? snapshot : new PatrolDeleteResultItemVo();
|
PatrolDeleteResultItemVo item = snapshot != null ? snapshot : new PatrolDeleteResultItemVo();
|
||||||
item.setResultId(entity.getId());
|
item.setResultId(entity.getId());
|
||||||
item.setTaskId(entity.getTaskId());
|
item.setTaskId(entity.getTaskId());
|
||||||
@@ -529,7 +553,7 @@ public class PatrolDeleteTaskService {
|
|||||||
item.setFinishedAt(task != null ? task.getFinishedAt() : item.getFinishedAt());
|
item.setFinishedAt(task != null ? task.getFinishedAt() : item.getFinishedAt());
|
||||||
item.setOutputFilename(firstNonBlank(item.getOutputFilename(), entity.getResultFilename()));
|
item.setOutputFilename(firstNonBlank(item.getOutputFilename(), entity.getResultFilename()));
|
||||||
item.setDownloadUrl(null);
|
item.setDownloadUrl(null);
|
||||||
attachFileJobState(item, entity);
|
attachFileJobState(item, entity, job);
|
||||||
if (item.getCountrySections() == null) {
|
if (item.getCountrySections() == null) {
|
||||||
item.setCountrySections(new ArrayList<>());
|
item.setCountrySections(new ArrayList<>());
|
||||||
}
|
}
|
||||||
@@ -540,7 +564,10 @@ public class PatrolDeleteTaskService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void attachFileJobState(PatrolDeleteResultItemVo item, FileResultEntity entity) {
|
private void attachFileJobState(PatrolDeleteResultItemVo item, FileResultEntity entity) {
|
||||||
TaskFileJobEntity job = taskFileJobService.findAssembleJob(entity.getTaskId(), MODULE_TYPE, entity.getId());
|
attachFileJobState(item, entity, taskFileJobService.findAssembleJob(entity.getTaskId(), MODULE_TYPE, entity.getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void attachFileJobState(PatrolDeleteResultItemVo item, FileResultEntity entity, TaskFileJobEntity job) {
|
||||||
item.setFileReady(!blank(entity.getResultFileUrl()));
|
item.setFileReady(!blank(entity.getResultFileUrl()));
|
||||||
if (job == null) {
|
if (job == null) {
|
||||||
item.setFileStatus(Boolean.TRUE.equals(item.getFileReady()) ? "SUCCESS" : null);
|
item.setFileStatus(Boolean.TRUE.equals(item.getFileReady()) ? "SUCCESS" : null);
|
||||||
@@ -612,7 +639,7 @@ public class PatrolDeleteTaskService {
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<PatrolDeleteResultItemVo> buildProgressItems(FileTaskEntity task, List<FileResultEntity> rows) {
|
private List<PatrolDeleteResultItemVo> buildProgressItems(FileTaskEntity task, List<FileResultEntity> rows, Map<Long, TaskFileJobEntity> jobMap) {
|
||||||
List<PatrolDeleteResultItemVo> list = new ArrayList<>();
|
List<PatrolDeleteResultItemVo> list = new ArrayList<>();
|
||||||
for (FileResultEntity row : rows) {
|
for (FileResultEntity row : rows) {
|
||||||
PatrolDeleteResultItemVo item = new PatrolDeleteResultItemVo();
|
PatrolDeleteResultItemVo item = new PatrolDeleteResultItemVo();
|
||||||
@@ -627,7 +654,7 @@ public class PatrolDeleteTaskService {
|
|||||||
item.setFinishedAt(task == null ? null : task.getFinishedAt());
|
item.setFinishedAt(task == null ? null : task.getFinishedAt());
|
||||||
item.setOutputFilename(row.getResultFilename());
|
item.setOutputFilename(row.getResultFilename());
|
||||||
item.setDownloadUrl(null);
|
item.setDownloadUrl(null);
|
||||||
attachFileJobState(item, row);
|
attachFileJobState(item, row, jobMap == null ? null : jobMap.get(row.getId()));
|
||||||
list.add(item);
|
list.add(item);
|
||||||
}
|
}
|
||||||
return list;
|
return list;
|
||||||
|
|||||||
@@ -225,6 +225,27 @@ public class TaskFileJobService {
|
|||||||
return count == null ? 0L : count;
|
return count == null ? 0L : count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void deleteTaskJobs(Long taskId, String moduleType) {
|
||||||
|
if (taskId == null || taskId <= 0 || moduleType == null || moduleType.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
taskFileJobMapper.delete(new LambdaQueryWrapper<TaskFileJobEntity>()
|
||||||
|
.eq(TaskFileJobEntity::getTaskId, taskId)
|
||||||
|
.eq(TaskFileJobEntity::getModuleType, moduleType));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void deleteResultJobs(Long taskId, String moduleType, Long resultId) {
|
||||||
|
if (taskId == null || taskId <= 0 || moduleType == null || moduleType.isBlank() || resultId == null || resultId <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
taskFileJobMapper.delete(new LambdaQueryWrapper<TaskFileJobEntity>()
|
||||||
|
.eq(TaskFileJobEntity::getTaskId, taskId)
|
||||||
|
.eq(TaskFileJobEntity::getModuleType, moduleType)
|
||||||
|
.eq(TaskFileJobEntity::getResultId, resultId));
|
||||||
|
}
|
||||||
|
|
||||||
private TaskFileJobEntity findJob(Long taskId, String moduleType, Long resultId, String jobType) {
|
private TaskFileJobEntity findJob(Long taskId, String moduleType, Long resultId, String jobType) {
|
||||||
if (taskId == null || moduleType == null || moduleType.isBlank() || resultId == null || jobType == null || jobType.isBlank()) {
|
if (taskId == null || moduleType == null || moduleType.isBlank() || resultId == null || jobType == null || jobType.isBlank()) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -84,6 +84,16 @@ public class TaskProgressSnapshotService {
|
|||||||
.last("limit 1"));
|
.last("limit 1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void delete(Long taskId, String moduleType) {
|
||||||
|
if (taskId == null || taskId <= 0 || isBlank(moduleType)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
taskProgressSnapshotMapper.delete(new LambdaQueryWrapper<TaskProgressSnapshotEntity>()
|
||||||
|
.eq(TaskProgressSnapshotEntity::getTaskId, taskId)
|
||||||
|
.eq(TaskProgressSnapshotEntity::getModuleType, moduleType));
|
||||||
|
}
|
||||||
|
|
||||||
private String writeJson(Object value) {
|
private String writeJson(Object value) {
|
||||||
try {
|
try {
|
||||||
return objectMapper.writeValueAsString(value);
|
return objectMapper.writeValueAsString(value);
|
||||||
|
|||||||
@@ -94,6 +94,27 @@ public class TaskResultItemService {
|
|||||||
return count == null ? 0L : count;
|
return count == null ? 0L : count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void deleteResultItem(Long taskId, String moduleType, Long resultId) {
|
||||||
|
if (taskId == null || taskId <= 0 || isBlank(moduleType) || resultId == null || resultId <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<TaskResultItemEntity> rows = taskResultItemMapper.selectList(new LambdaQueryWrapper<TaskResultItemEntity>()
|
||||||
|
.select(TaskResultItemEntity::getId, TaskResultItemEntity::getPayloadJson)
|
||||||
|
.eq(TaskResultItemEntity::getTaskId, taskId)
|
||||||
|
.eq(TaskResultItemEntity::getModuleType, moduleType)
|
||||||
|
.eq(TaskResultItemEntity::getResultId, resultId));
|
||||||
|
if (rows != null) {
|
||||||
|
for (TaskResultItemEntity row : rows) {
|
||||||
|
transientPayloadStorageService.deletePayloadIfPresent(row.getPayloadJson());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
taskResultItemMapper.delete(new LambdaQueryWrapper<TaskResultItemEntity>()
|
||||||
|
.eq(TaskResultItemEntity::getTaskId, taskId)
|
||||||
|
.eq(TaskResultItemEntity::getModuleType, moduleType)
|
||||||
|
.eq(TaskResultItemEntity::getResultId, resultId));
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void deleteTaskItems(Long taskId, String moduleType) {
|
public void deleteTaskItems(Long taskId, String moduleType) {
|
||||||
if (taskId == null || taskId <= 0 || isBlank(moduleType)) {
|
if (taskId == null || taskId <= 0 || isBlank(moduleType)) {
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS biz_patrol_delete_condition (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
condition_text VARCHAR(500) NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY uk_patrol_delete_condition_user_text (user_id, condition_text),
|
||||||
|
KEY idx_patrol_delete_condition_user_id (user_id)
|
||||||
|
);
|
||||||
BIN
frontend-vue/new_web_source.zip
Normal file
BIN
frontend-vue/new_web_source.zip
Normal file
Binary file not shown.
@@ -113,6 +113,18 @@
|
|||||||
{{ item.resultFilename || '下载结果' }}
|
{{ item.resultFilename || '下载结果' }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="pendingResultHint(item)" class="files result-hint">{{ pendingResultHint(item) }}</div>
|
<div v-if="pendingResultHint(item)" class="files result-hint">{{ pendingResultHint(item) }}</div>
|
||||||
|
<div v-if="showFileProgress(item)" class="file-progress">
|
||||||
|
<div class="file-progress-meta">
|
||||||
|
<span>{{ item.fileProgressMessage || '结果生成中' }}</span>
|
||||||
|
<span>{{ fileProgressPercent(item) }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="file-progress-track">
|
||||||
|
<div class="file-progress-bar" :style="{ width: `${fileProgressPercent(item)}%` }"></div>
|
||||||
|
</div>
|
||||||
|
<div class="file-progress-count">
|
||||||
|
{{ item.fileProgressCurrent || 0 }}/{{ item.fileProgressTotal || 0 }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-if="item.error" class="files">错误:{{ item.error }}</div>
|
<div v-if="item.error" class="files">错误:{{ item.error }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="task-right">
|
<div class="task-right">
|
||||||
@@ -633,6 +645,16 @@ function canDownload(item: AppearancePatentHistoryItem) {
|
|||||||
return Boolean(item.resultId && (item.fileReady || item.downloadUrl))
|
return Boolean(item.resultId && (item.fileReady || item.downloadUrl))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fileProgressPercent(item: AppearancePatentHistoryItem) {
|
||||||
|
const percent = Number(item.fileProgressPercent || 0)
|
||||||
|
if (!Number.isFinite(percent)) return 0
|
||||||
|
return Math.max(0, Math.min(100, Math.round(percent)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function showFileProgress(item: AppearancePatentHistoryItem) {
|
||||||
|
return isResultPreparing(item) && (item.fileProgressTotal || 0) > 0
|
||||||
|
}
|
||||||
|
|
||||||
async function downloadResult(item: AppearancePatentHistoryItem) {
|
async function downloadResult(item: AppearancePatentHistoryItem) {
|
||||||
if (!item.resultId) return
|
if (!item.resultId) return
|
||||||
const api = getPywebviewApi()
|
const api = getPywebviewApi()
|
||||||
@@ -729,6 +751,11 @@ onUnmounted(() => {
|
|||||||
.status.running { background: rgba(52, 152, 219, .18); color: #3498db; }
|
.status.running { background: rgba(52, 152, 219, .18); color: #3498db; }
|
||||||
.status.pending { background: rgba(149, 165, 166, .18); color: #bdc3c7; }
|
.status.pending { background: rgba(149, 165, 166, .18); color: #bdc3c7; }
|
||||||
.result-hint { margin-top: 6px; color: #e0b96d; }
|
.result-hint { margin-top: 6px; color: #e0b96d; }
|
||||||
|
.file-progress { margin-top: 8px; max-width: 520px; }
|
||||||
|
.file-progress-meta { display: flex; justify-content: space-between; gap: 12px; color: #d8c278; font-size: 12px; }
|
||||||
|
.file-progress-track { margin-top: 5px; height: 8px; border-radius: 999px; overflow: hidden; background: #303030; border: 1px solid #3b3b3b; }
|
||||||
|
.file-progress-bar { height: 100%; border-radius: inherit; background: linear-gradient(90deg, #4aa3ff, #f0c75e); transition: width .25s ease; }
|
||||||
|
.file-progress-count { margin-top: 4px; color: #858585; font-size: 11px; }
|
||||||
.download { padding: 6px 10px; color: #d6ecff; background: rgba(52, 152, 219, .18); }
|
.download { padding: 6px 10px; color: #d6ecff; background: rgba(52, 152, 219, .18); }
|
||||||
.btn-delete { padding: 6px 10px; color: #ff8f8f; background: rgba(231, 76, 60, .12); }
|
.btn-delete { padding: 6px 10px; color: #ff8f8f; background: rgba(231, 76, 60, .12); }
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<div class="section-title">店铺输入</div>
|
<div class="section-title">店铺输入</div>
|
||||||
<div class="input-zone">
|
<div class="input-zone">
|
||||||
<div class="hint">
|
<div class="hint">
|
||||||
左侧负责录入店铺并加入备选区,确认命中后推送到 Python 队列。巡店删除按店铺串行执行:上一个任务完成后会自动开始下一个,失败也会继续下一个。
|
左侧负责录入店铺并加入备选区,点击“匹配店铺”只会生成匹配结果;确认后需手动点击“推送到 Python 队列”才会创建并执行任务。
|
||||||
</div>
|
</div>
|
||||||
<div class="input-row">
|
<div class="input-row">
|
||||||
<el-input
|
<el-input
|
||||||
@@ -60,6 +60,54 @@
|
|||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="section-title">删除条件</div>
|
||||||
|
<div class="condition-panel">
|
||||||
|
<div class="condition-input-row">
|
||||||
|
<el-input
|
||||||
|
v-model="conditionInput"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入删除条件"
|
||||||
|
@keyup.enter="confirmAddCondition"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="opt-btn"
|
||||||
|
:disabled="addingCondition"
|
||||||
|
@click="confirmAddCondition"
|
||||||
|
>
|
||||||
|
{{ addingCondition ? "保存中..." : "保存" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="!deleteConditions.length" class="empty-conditions">
|
||||||
|
暂无删除条件,保存后可在推送任务时多选携带。
|
||||||
|
</div>
|
||||||
|
<ul v-else class="condition-list">
|
||||||
|
<li
|
||||||
|
v-for="condition in deleteConditions"
|
||||||
|
:key="condition.id"
|
||||||
|
class="condition-item"
|
||||||
|
>
|
||||||
|
<label class="condition-check">
|
||||||
|
<input
|
||||||
|
v-model="selectedConditionIds"
|
||||||
|
type="checkbox"
|
||||||
|
:value="condition.id"
|
||||||
|
/>
|
||||||
|
<span :title="condition.conditionText">
|
||||||
|
{{ condition.conditionText }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="link-danger"
|
||||||
|
@click="removeCondition(condition.id)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="run-row">
|
<div class="run-row">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -75,12 +123,12 @@
|
|||||||
:disabled="pushing || !matchedRunnableItems.length"
|
:disabled="pushing || !matchedRunnableItems.length"
|
||||||
@click="pushToPythonQueue"
|
@click="pushToPythonQueue"
|
||||||
>
|
>
|
||||||
{{ pushing ? "串行执行中..." : "推送到 Python 队列" }}
|
{{ pushing ? "任务执行中..." : "推送到 Python 队列" }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="loading-msg">
|
<p class="loading-msg">
|
||||||
当前页面会按店铺串行推送任务,并在任务成功或失败后自动接续下一个任务。服务端临时重启时会自动重试查询状态,恢复后继续后续店铺。
|
当前页面会把匹配结果合并为一个巡店删除任务推送;多个店铺会在同一个任务和同一个结果文件中汇总。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div v-if="queuePushResult" class="queue-debug-card">
|
<div v-if="queuePushResult" class="queue-debug-card">
|
||||||
@@ -192,19 +240,19 @@
|
|||||||
<ul class="task-list">
|
<ul class="task-list">
|
||||||
<li
|
<li
|
||||||
v-for="item in currentSectionItems"
|
v-for="item in currentSectionItems"
|
||||||
:key="`${item.resultId}-${item.taskId}`"
|
:key="taskGroupKey(item)"
|
||||||
class="task-item"
|
class="task-item"
|
||||||
>
|
>
|
||||||
<div class="left split-result-main">
|
<div class="left split-result-main">
|
||||||
<span class="id" :title="item.shopName || ''">
|
<span class="id" :title="formatTaskGroupShopNames(item)">
|
||||||
{{ item.shopName || "-" }}
|
{{ formatTaskGroupShopNames(item) }}
|
||||||
</span>
|
</span>
|
||||||
<div class="files">任务 ID: {{ item.taskId ?? "-" }}</div>
|
<div class="files">任务 ID: {{ item.taskId ?? "-" }}</div>
|
||||||
<div v-if="item.resultId" class="files">
|
<div v-if="formatTaskGroupResultIds(item)" class="files">
|
||||||
结果 ID: {{ item.resultId }}
|
结果 ID: {{ formatTaskGroupResultIds(item) }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="item.shopId" class="files">
|
<div v-if="formatTaskGroupShopIds(item)" class="files">
|
||||||
店铺 ID: {{ item.shopId }}
|
店铺 ID: {{ formatTaskGroupShopIds(item) }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="item.platform" class="files">
|
<div v-if="item.platform" class="files">
|
||||||
平台: {{ item.platform }}
|
平台: {{ item.platform }}
|
||||||
@@ -213,10 +261,10 @@
|
|||||||
创建时间: {{ formatDateTime(item.createdAt) }}
|
创建时间: {{ formatDateTime(item.createdAt) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="files">
|
<div class="files">
|
||||||
模板结构: {{ formatTemplateSummary(item) }}
|
模板: {{ formatTemplateSummary(item) }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="item.error" class="files">
|
<div v-if="formatTaskGroupErrors(item)" class="files">
|
||||||
错误: {{ item.error }}
|
错误: {{ formatTaskGroupErrors(item) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="task-right">
|
<div class="task-right">
|
||||||
@@ -243,19 +291,19 @@
|
|||||||
<ul class="task-list">
|
<ul class="task-list">
|
||||||
<li
|
<li
|
||||||
v-for="item in historySectionItems"
|
v-for="item in historySectionItems"
|
||||||
:key="`${item.resultId}-${item.taskId}`"
|
:key="taskGroupKey(item)"
|
||||||
class="task-item"
|
class="task-item"
|
||||||
>
|
>
|
||||||
<div class="left split-result-main">
|
<div class="left split-result-main">
|
||||||
<span class="id" :title="item.shopName || ''">
|
<span class="id" :title="formatTaskGroupShopNames(item)">
|
||||||
{{ item.shopName || "-" }}
|
{{ formatTaskGroupShopNames(item) }}
|
||||||
</span>
|
</span>
|
||||||
<div class="files">任务 ID: {{ item.taskId ?? "-" }}</div>
|
<div class="files">任务 ID: {{ item.taskId ?? "-" }}</div>
|
||||||
<div v-if="item.resultId" class="files">
|
<div v-if="formatTaskGroupResultIds(item)" class="files">
|
||||||
结果 ID: {{ item.resultId }}
|
结果 ID: {{ formatTaskGroupResultIds(item) }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="item.shopId" class="files">
|
<div v-if="formatTaskGroupShopIds(item)" class="files">
|
||||||
店铺 ID: {{ item.shopId }}
|
店铺 ID: {{ formatTaskGroupShopIds(item) }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="item.platform" class="files">
|
<div v-if="item.platform" class="files">
|
||||||
平台: {{ item.platform }}
|
平台: {{ item.platform }}
|
||||||
@@ -267,10 +315,10 @@
|
|||||||
完成时间: {{ formatDateTime(item.finishedAt) }}
|
完成时间: {{ formatDateTime(item.finishedAt) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="files">
|
<div class="files">
|
||||||
模板结构: {{ formatTemplateSummary(item) }}
|
模板: {{ formatTemplateSummary(item) }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="item.error" class="files">
|
<div v-if="formatTaskGroupErrors(item)" class="files">
|
||||||
错误: {{ item.error }}
|
错误: {{ formatTaskGroupErrors(item) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="task-right">
|
<div class="task-right">
|
||||||
@@ -313,18 +361,22 @@ import { ElMessage } from "element-plus";
|
|||||||
import BrandTopBar from "@/pages/brand/components/BrandTopBar.vue";
|
import BrandTopBar from "@/pages/brand/components/BrandTopBar.vue";
|
||||||
import {
|
import {
|
||||||
addPatrolDeleteCandidate,
|
addPatrolDeleteCandidate,
|
||||||
|
addPatrolDeleteCondition,
|
||||||
createPatrolDeleteTask,
|
createPatrolDeleteTask,
|
||||||
deletePatrolDeleteCandidate,
|
deletePatrolDeleteCandidate,
|
||||||
|
deletePatrolDeleteCondition,
|
||||||
deletePatrolDeleteHistory,
|
deletePatrolDeleteHistory,
|
||||||
deletePatrolDeleteTask,
|
deletePatrolDeleteTask,
|
||||||
getPatrolDeleteDashboard,
|
getPatrolDeleteDashboard,
|
||||||
getPatrolDeleteHistory,
|
getPatrolDeleteHistory,
|
||||||
getPatrolDeleteTaskProgressBatch,
|
getPatrolDeleteTaskProgressBatch,
|
||||||
getPatrolDeleteResultDownloadUrl,
|
getPatrolDeleteResultDownloadUrl,
|
||||||
|
listPatrolDeleteConditions,
|
||||||
listPatrolDeleteCandidates,
|
listPatrolDeleteCandidates,
|
||||||
matchPatrolDeleteShops,
|
matchPatrolDeleteShops,
|
||||||
submitPatrolDeleteTaskResult,
|
submitPatrolDeleteTaskResult,
|
||||||
type PatrolDeleteCandidateVo,
|
type PatrolDeleteCandidateVo,
|
||||||
|
type PatrolDeleteConditionVo,
|
||||||
type PatrolDeleteCartRatio,
|
type PatrolDeleteCartRatio,
|
||||||
type PatrolDeleteCountrySection,
|
type PatrolDeleteCountrySection,
|
||||||
type PatrolDeleteDashboardVo,
|
type PatrolDeleteDashboardVo,
|
||||||
@@ -340,8 +392,11 @@ const COUNTRY_TEMPLATE = ["德国", "英国", "法国", "意大利", "西班牙"
|
|||||||
const MAX_TRANSIENT_ERRORS = 30;
|
const MAX_TRANSIENT_ERRORS = 30;
|
||||||
|
|
||||||
const shopInput = ref("");
|
const shopInput = ref("");
|
||||||
|
const conditionInput = ref("");
|
||||||
const candidates = ref<PatrolDeleteCandidateVo[]>([]);
|
const candidates = ref<PatrolDeleteCandidateVo[]>([]);
|
||||||
const selectedCandidates = ref<PatrolDeleteCandidateVo[]>([]);
|
const selectedCandidates = ref<PatrolDeleteCandidateVo[]>([]);
|
||||||
|
const deleteConditions = ref<PatrolDeleteConditionVo[]>([]);
|
||||||
|
const selectedConditionIds = ref<number[]>([]);
|
||||||
const matchedItems = ref<PatrolDeleteShopQueueItem[]>([]);
|
const matchedItems = ref<PatrolDeleteShopQueueItem[]>([]);
|
||||||
const historyItems = ref<PatrolDeleteHistoryItem[]>([]);
|
const historyItems = ref<PatrolDeleteHistoryItem[]>([]);
|
||||||
const dashboard = ref<PatrolDeleteDashboardVo>({
|
const dashboard = ref<PatrolDeleteDashboardVo>({
|
||||||
@@ -351,32 +406,30 @@ const dashboard = ref<PatrolDeleteDashboardVo>({
|
|||||||
failedTaskCount: 0,
|
failedTaskCount: 0,
|
||||||
});
|
});
|
||||||
const adding = ref(false);
|
const adding = ref(false);
|
||||||
|
const addingCondition = ref(false);
|
||||||
const matching = ref(false);
|
const matching = ref(false);
|
||||||
const pushing = ref(false);
|
const pushing = ref(false);
|
||||||
const queuePushResult = ref("");
|
const queuePushResult = ref("");
|
||||||
const queuePayloadText = ref("");
|
const queuePayloadText = ref("");
|
||||||
const pendingQueue = ref<PatrolDeleteShopQueueItem[]>([]);
|
|
||||||
const activeTaskId = ref<number | null>(null);
|
const activeTaskId = ref<number | null>(null);
|
||||||
const activeQueueItem = ref<PatrolDeleteShopQueueItem | null>(null);
|
|
||||||
const queueWorkerRunning = ref(false);
|
const queueWorkerRunning = ref(false);
|
||||||
const autoQueueEnabled = ref(false);
|
|
||||||
let historyPollTimer: number | null = null;
|
let historyPollTimer: number | null = null;
|
||||||
const timers = createCategorizedTimers("patrol-delete");
|
const timers = createCategorizedTimers("patrol-delete");
|
||||||
|
|
||||||
const matchedRunnableItems = computed(() =>
|
const matchedRunnableItems = computed(() =>
|
||||||
matchedItems.value.filter((item) => item.matched),
|
matchedItems.value.filter((item) => item.matched),
|
||||||
);
|
);
|
||||||
|
const taskRecordItems = computed(() => groupHistoryItemsByTask(historyItems.value));
|
||||||
const currentSectionItems = computed(() =>
|
const currentSectionItems = computed(() =>
|
||||||
historyItems.value.filter((item) => !isTaskTerminal(item.taskStatus)),
|
taskRecordItems.value.filter((item) => !isTaskTerminal(item.taskStatus)),
|
||||||
);
|
);
|
||||||
const historySectionItems = computed(() =>
|
const historySectionItems = computed(() =>
|
||||||
historyItems.value.filter((item) => isTaskTerminal(item.taskStatus)),
|
taskRecordItems.value.filter((item) => isTaskTerminal(item.taskStatus)),
|
||||||
);
|
);
|
||||||
const hasQueueWork = computed(
|
const hasQueueWork = computed(
|
||||||
() =>
|
() =>
|
||||||
queueWorkerRunning.value ||
|
queueWorkerRunning.value ||
|
||||||
!!activeTaskId.value ||
|
!!activeTaskId.value,
|
||||||
pendingQueue.value.length > 0,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
function uidForStorage() {
|
function uidForStorage() {
|
||||||
@@ -401,6 +454,10 @@ function historyItemKey(item: PatrolDeleteHistoryItem) {
|
|||||||
return `${item.taskId ?? 0}:${item.resultId ?? 0}`;
|
return `${item.taskId ?? 0}:${item.resultId ?? 0}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function taskGroupKey(item: PatrolDeleteHistoryItem) {
|
||||||
|
return `task:${item.taskId ?? item.resultId ?? 0}`;
|
||||||
|
}
|
||||||
|
|
||||||
let disposed = false;
|
let disposed = false;
|
||||||
|
|
||||||
function sleep(ms: number) {
|
function sleep(ms: number) {
|
||||||
@@ -546,8 +603,65 @@ function formatTemplateSummary(
|
|||||||
"shopName" | "countrySections" | "cartRatios"
|
"shopName" | "countrySections" | "cartRatios"
|
||||||
>,
|
>,
|
||||||
) {
|
) {
|
||||||
const rows = flattenTemplateRows(record);
|
const countries = new Set([
|
||||||
return `${rows.length} 行状态数据,首行含 ${record.cartRatios?.length || 0} 个购物车比例列`;
|
...(record.countrySections || []).map((item) => item.country).filter(Boolean),
|
||||||
|
...(record.cartRatios || []).map((item) => item.country).filter(Boolean),
|
||||||
|
]);
|
||||||
|
return `${countries.size || 0} 个国家,含状态数据和购物车比例`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function uniqueJoined(values: Array<string | number | undefined | null>) {
|
||||||
|
return Array.from(
|
||||||
|
new Set(
|
||||||
|
values
|
||||||
|
.map((value) => (value == null ? "" : String(value).trim()))
|
||||||
|
.filter(Boolean),
|
||||||
|
),
|
||||||
|
).join("、");
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupHistoryItemsByTask(items: PatrolDeleteHistoryItem[]) {
|
||||||
|
const groups = new Map<string, PatrolDeleteHistoryItem[]>();
|
||||||
|
for (const item of items || []) {
|
||||||
|
const key = String(item.taskId ?? `result:${item.resultId ?? Math.random()}`);
|
||||||
|
const group = groups.get(key) || [];
|
||||||
|
group.push(item);
|
||||||
|
groups.set(key, group);
|
||||||
|
}
|
||||||
|
return Array.from(groups.values()).map((group) => {
|
||||||
|
const base =
|
||||||
|
group.find((item) => canDownload(item)) ||
|
||||||
|
group.find((item) => item.taskStatus) ||
|
||||||
|
group[0];
|
||||||
|
const errors = uniqueJoined(group.map((item) => item.error));
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
shopName: uniqueJoined(group.map((item) => item.shopName)) || base.shopName,
|
||||||
|
shopId: uniqueJoined(group.map((item) => item.shopId)) || base.shopId,
|
||||||
|
error: errors || base.error,
|
||||||
|
__groupItems: group,
|
||||||
|
} as PatrolDeleteHistoryItem & { __groupItems?: PatrolDeleteHistoryItem[] };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function taskGroupItems(item: PatrolDeleteHistoryItem) {
|
||||||
|
return ((item as PatrolDeleteHistoryItem & { __groupItems?: PatrolDeleteHistoryItem[] }).__groupItems || [item]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTaskGroupShopNames(item: PatrolDeleteHistoryItem) {
|
||||||
|
return uniqueJoined(taskGroupItems(item).map((row) => row.shopName)) || "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTaskGroupResultIds(item: PatrolDeleteHistoryItem) {
|
||||||
|
return uniqueJoined(taskGroupItems(item).map((row) => row.resultId));
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTaskGroupShopIds(item: PatrolDeleteHistoryItem) {
|
||||||
|
return uniqueJoined(taskGroupItems(item).map((row) => row.shopId));
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTaskGroupErrors(item: PatrolDeleteHistoryItem) {
|
||||||
|
return uniqueJoined(taskGroupItems(item).map((row) => row.error));
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTaskTerminal(status?: string) {
|
function isTaskTerminal(status?: string) {
|
||||||
@@ -569,7 +683,9 @@ function statusClass(status?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function canDownload(item: PatrolDeleteHistoryItem) {
|
function canDownload(item: PatrolDeleteHistoryItem) {
|
||||||
return Boolean(item.resultId && (item.fileReady || item.downloadUrl));
|
return taskGroupItems(item).some((row) =>
|
||||||
|
Boolean(row.resultId && (row.fileReady || row.downloadUrl)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveMatchedItems() {
|
function saveMatchedItems() {
|
||||||
@@ -595,9 +711,7 @@ function loadMatchedItems() {
|
|||||||
function saveQueueState() {
|
function saveQueueState() {
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
const payload = {
|
const payload = {
|
||||||
pendingQueue: pendingQueue.value,
|
|
||||||
activeTaskId: activeTaskId.value,
|
activeTaskId: activeTaskId.value,
|
||||||
activeQueueItem: activeQueueItem.value,
|
|
||||||
};
|
};
|
||||||
window.localStorage.setItem(queueStateStorageKey(), JSON.stringify(payload));
|
window.localStorage.setItem(queueStateStorageKey(), JSON.stringify(payload));
|
||||||
}
|
}
|
||||||
@@ -610,23 +724,16 @@ function loadQueueState() {
|
|||||||
: null;
|
: null;
|
||||||
if (!raw) return;
|
if (!raw) return;
|
||||||
const parsed = JSON.parse(raw) as {
|
const parsed = JSON.parse(raw) as {
|
||||||
pendingQueue?: PatrolDeleteShopQueueItem[];
|
|
||||||
activeTaskId?: number | null;
|
activeTaskId?: number | null;
|
||||||
activeQueueItem?: PatrolDeleteShopQueueItem | null;
|
|
||||||
};
|
};
|
||||||
pendingQueue.value = parsed.pendingQueue || [];
|
|
||||||
activeTaskId.value = parsed.activeTaskId ?? null;
|
activeTaskId.value = parsed.activeTaskId ?? null;
|
||||||
activeQueueItem.value = parsed.activeQueueItem ?? null;
|
|
||||||
} catch {
|
} catch {
|
||||||
pendingQueue.value = [];
|
|
||||||
activeTaskId.value = null;
|
activeTaskId.value = null;
|
||||||
activeQueueItem.value = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearActiveQueueTask() {
|
function clearActiveQueueTask() {
|
||||||
activeTaskId.value = null;
|
activeTaskId.value = null;
|
||||||
activeQueueItem.value = null;
|
|
||||||
saveQueueState();
|
saveQueueState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -636,9 +743,8 @@ function isRecordMissingError(error: unknown) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removeMatchedRowLocally(row: PatrolDeleteShopQueueItem) {
|
function removeMatchedRowLocally(row: PatrolDeleteShopQueueItem) {
|
||||||
matchedItems.value = matchedItems.value.filter(
|
const key = rowKeyForMatch(row);
|
||||||
(item) => rowKeyForMatch(item) !== rowKeyForMatch(row),
|
matchedItems.value = matchedItems.value.filter((item) => rowKeyForMatch(item) !== key);
|
||||||
);
|
|
||||||
saveMatchedItems();
|
saveMatchedItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -650,20 +756,18 @@ function removeHistoryItemLocally(item: PatrolDeleteHistoryItem) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeQueueItems(
|
|
||||||
base: PatrolDeleteShopQueueItem[],
|
|
||||||
incoming: PatrolDeleteShopQueueItem[],
|
|
||||||
) {
|
|
||||||
const map = new Map<string, PatrolDeleteShopQueueItem>();
|
|
||||||
for (const item of base) map.set(rowKeyForMatch(item), item);
|
|
||||||
for (const item of incoming) map.set(rowKeyForMatch(item), item);
|
|
||||||
return Array.from(map.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadCandidates() {
|
async function loadCandidates() {
|
||||||
candidates.value = await listPatrolDeleteCandidates();
|
candidates.value = await listPatrolDeleteCandidates();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadConditions() {
|
||||||
|
deleteConditions.value = await listPatrolDeleteConditions();
|
||||||
|
const existingIds = new Set(deleteConditions.value.map((item) => item.id));
|
||||||
|
selectedConditionIds.value = selectedConditionIds.value.filter((id) =>
|
||||||
|
existingIds.has(id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function loadDashboard() {
|
async function loadDashboard() {
|
||||||
dashboard.value = await getPatrolDeleteDashboard();
|
dashboard.value = await getPatrolDeleteDashboard();
|
||||||
}
|
}
|
||||||
@@ -705,6 +809,10 @@ function mergeHistoryProgressItems(incoming: PatrolDeleteHistoryItem[]) {
|
|||||||
historyItems.value = merged;
|
historyItems.value = merged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function upsertHistoryItems(incoming: PatrolDeleteHistoryItem[]) {
|
||||||
|
mergeHistoryProgressItems(incoming);
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshActiveTaskProgress(taskIds?: number[]) {
|
async function refreshActiveTaskProgress(taskIds?: number[]) {
|
||||||
const ids = Array.from(
|
const ids = Array.from(
|
||||||
new Set(
|
new Set(
|
||||||
@@ -768,6 +876,41 @@ async function confirmAdd() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function confirmAddCondition() {
|
||||||
|
const text = conditionInput.value.trim();
|
||||||
|
if (!text) {
|
||||||
|
ElMessage.warning("请输入删除条件");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addingCondition.value = true;
|
||||||
|
try {
|
||||||
|
const saved = await addPatrolDeleteCondition(text);
|
||||||
|
conditionInput.value = "";
|
||||||
|
await loadConditions();
|
||||||
|
if (saved?.id && !selectedConditionIds.value.includes(saved.id)) {
|
||||||
|
selectedConditionIds.value = [...selectedConditionIds.value, saved.id];
|
||||||
|
}
|
||||||
|
ElMessage.success("删除条件已保存");
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error instanceof Error ? error.message : "保存失败");
|
||||||
|
} finally {
|
||||||
|
addingCondition.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeCondition(id: number) {
|
||||||
|
try {
|
||||||
|
await deletePatrolDeleteCondition(id);
|
||||||
|
selectedConditionIds.value = selectedConditionIds.value.filter(
|
||||||
|
(item) => item !== id,
|
||||||
|
);
|
||||||
|
await loadConditions();
|
||||||
|
ElMessage.success("删除条件已删除");
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error instanceof Error ? error.message : "删除失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function removeCandidate(id: number) {
|
async function removeCandidate(id: number) {
|
||||||
try {
|
try {
|
||||||
await deletePatrolDeleteCandidate(id);
|
await deletePatrolDeleteCandidate(id);
|
||||||
@@ -804,11 +947,6 @@ async function runMatch() {
|
|||||||
const data = await matchPatrolDeleteShops(names);
|
const data = await matchPatrolDeleteShops(names);
|
||||||
matchedItems.value = mergeMatchedItems(matchedItems.value, data.items || []);
|
matchedItems.value = mergeMatchedItems(matchedItems.value, data.items || []);
|
||||||
saveMatchedItems();
|
saveMatchedItems();
|
||||||
if (autoQueueEnabled.value) {
|
|
||||||
pendingQueue.value = mergeQueueItems(pendingQueue.value, matchedRunnableItems.value);
|
|
||||||
saveQueueState();
|
|
||||||
void processQueue();
|
|
||||||
}
|
|
||||||
ElMessage.success(`本次返回 ${data.items?.length || 0} 条匹配结果`);
|
ElMessage.success(`本次返回 ${data.items?.length || 0} 条匹配结果`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(error instanceof Error ? error.message : "匹配失败");
|
ElMessage.error(error instanceof Error ? error.message : "匹配失败");
|
||||||
@@ -818,14 +956,8 @@ async function runMatch() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removeMatchedRow(row: PatrolDeleteShopQueueItem) {
|
function removeMatchedRow(row: PatrolDeleteShopQueueItem) {
|
||||||
if (hasQueueWork.value) {
|
removeMatchedRowLocally(row);
|
||||||
ElMessage.warning("队列执行中,请等待当前串行任务结束后再调整");
|
ElMessage.success("已从匹配结果移除");
|
||||||
return;
|
|
||||||
}
|
|
||||||
matchedItems.value = matchedItems.value.filter(
|
|
||||||
(item) => rowKeyForMatch(item) !== rowKeyForMatch(row),
|
|
||||||
);
|
|
||||||
saveMatchedItems();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildTaskItem(item: PatrolDeleteShopQueueItem): PatrolDeleteTaskItem {
|
function buildTaskItem(item: PatrolDeleteShopQueueItem): PatrolDeleteTaskItem {
|
||||||
@@ -842,7 +974,19 @@ function buildTaskItem(item: PatrolDeleteShopQueueItem): PatrolDeleteTaskItem {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildQueuePayload(taskId: number, item: PatrolDeleteHistoryItem) {
|
function selectedDeleteConditions() {
|
||||||
|
const selected = new Set(selectedConditionIds.value);
|
||||||
|
return deleteConditions.value
|
||||||
|
.filter((condition) => selected.has(condition.id))
|
||||||
|
.map((condition) => ({
|
||||||
|
id: condition.id,
|
||||||
|
conditionText: condition.conditionText,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildQueuePayload(taskId: number, items: PatrolDeleteHistoryItem[]) {
|
||||||
|
const firstItem = items[0];
|
||||||
|
const deleteConditionsForTask = selectedDeleteConditions();
|
||||||
return {
|
return {
|
||||||
type: "patrol-delete-run",
|
type: "patrol-delete-run",
|
||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
@@ -850,8 +994,9 @@ function buildQueuePayload(taskId: number, item: PatrolDeleteHistoryItem) {
|
|||||||
taskId,
|
taskId,
|
||||||
user_id: Number(uidForStorage()) || 0,
|
user_id: Number(uidForStorage()) || 0,
|
||||||
source: "frontend-vue-patrol-delete",
|
source: "frontend-vue-patrol-delete",
|
||||||
items: [
|
delete_conditions: deleteConditionsForTask,
|
||||||
{
|
deleteConditions: deleteConditionsForTask,
|
||||||
|
items: items.map((item) => ({
|
||||||
shopName: item.shopName,
|
shopName: item.shopName,
|
||||||
shopId: item.shopId,
|
shopId: item.shopId,
|
||||||
platform: item.platform,
|
platform: item.platform,
|
||||||
@@ -859,11 +1004,17 @@ function buildQueuePayload(taskId: number, item: PatrolDeleteHistoryItem) {
|
|||||||
matched: item.matched,
|
matched: item.matched,
|
||||||
matchStatus: item.matchStatus,
|
matchStatus: item.matchStatus,
|
||||||
matchMessage: item.matchMessage,
|
matchMessage: item.matchMessage,
|
||||||
},
|
countrySections: item.countrySections || [],
|
||||||
],
|
cartRatios: item.cartRatios || [],
|
||||||
template_rows: flattenTemplateRows(item),
|
})),
|
||||||
country_sections: item.countrySections || [],
|
template_rows: firstItem ? flattenTemplateRows(firstItem) : [],
|
||||||
cart_ratios: item.cartRatios || [],
|
shop_template_rows: items.map((item) => ({
|
||||||
|
shopName: item.shopName,
|
||||||
|
shopId: item.shopId,
|
||||||
|
templateRows: flattenTemplateRows(item),
|
||||||
|
})),
|
||||||
|
country_sections: firstItem?.countrySections || [],
|
||||||
|
cart_ratios: firstItem?.cartRatios || [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -876,8 +1027,10 @@ async function waitForTaskTerminal(taskId: number) {
|
|||||||
let transientErrorCount = 0;
|
let transientErrorCount = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
if (disposed) return "STOPPED";
|
if (disposed) return "STOPPED";
|
||||||
|
if (activeTaskId.value !== taskId) return "DELETED";
|
||||||
try {
|
try {
|
||||||
await refreshActiveTaskProgress([taskId]);
|
await refreshActiveTaskProgress([taskId]);
|
||||||
|
if (activeTaskId.value !== taskId) return "DELETED";
|
||||||
if (transientErrorCount > 0) {
|
if (transientErrorCount > 0) {
|
||||||
queuePushResult.value = `任务 ${taskId} 后端已恢复,继续等待执行结果...`;
|
queuePushResult.value = `任务 ${taskId} 后端已恢复,继续等待执行结果...`;
|
||||||
}
|
}
|
||||||
@@ -915,76 +1068,72 @@ async function processQueue() {
|
|||||||
throw new Error("当前客户端未提供 enqueue_json");
|
throw new Error("当前客户端未提供 enqueue_json");
|
||||||
}
|
}
|
||||||
|
|
||||||
while (!disposed && (activeTaskId.value || pendingQueue.value.length)) {
|
|
||||||
if (activeTaskId.value) {
|
if (activeTaskId.value) {
|
||||||
const finalStatus = await waitForTaskTerminal(activeTaskId.value);
|
const finalStatus = await waitForTaskTerminal(activeTaskId.value);
|
||||||
queuePushResult.value =
|
queuePushResult.value =
|
||||||
pendingQueue.value.length > 0
|
finalStatus === "DELETED"
|
||||||
? `任务 ${activeTaskId.value} ${finalStatus === "SUCCESS" ? "已完成" : "执行失败"},自动继续下一个(剩余 ${pendingQueue.value.length} 条)`
|
? "任务已删除"
|
||||||
: `任务 ${activeTaskId.value} ${finalStatus === "SUCCESS" ? "已完成" : "执行失败"}`;
|
: `任务 ${activeTaskId.value} ${finalStatus === "SUCCESS" ? "已完成" : "执行失败"}`;
|
||||||
clearActiveQueueTask();
|
clearActiveQueueTask();
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextItem = pendingQueue.value.shift();
|
const runnable = matchedRunnableItems.value;
|
||||||
saveQueueState();
|
if (!runnable.length) {
|
||||||
if (!nextItem) break;
|
ElMessage.warning("请先匹配可用店铺");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const created = await withTransientRetry(
|
const created = await withTransientRetry(
|
||||||
() => createPatrolDeleteTask([buildTaskItem(nextItem)]),
|
() => createPatrolDeleteTask(runnable.map(buildTaskItem)),
|
||||||
(attempt, maxAttempts) => {
|
(attempt, maxAttempts) => {
|
||||||
queuePushResult.value = `后端服务暂时不可用,正在重试创建任务(${attempt}/${maxAttempts})...`;
|
queuePushResult.value = `后端服务暂时不可用,正在重试创建任务(${attempt}/${maxAttempts})...`;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const createdItem = created.items?.[0];
|
const createdItems = created.items || [];
|
||||||
if (!createdItem?.taskId || !createdItem.resultId) {
|
if (!created.taskId || !createdItems.length || createdItems.some((item) => !item.taskId || !item.resultId)) {
|
||||||
throw new Error("后端未返回有效任务标识");
|
throw new Error("后端未返回有效任务标识");
|
||||||
}
|
}
|
||||||
|
|
||||||
activeTaskId.value = created.taskId;
|
activeTaskId.value = created.taskId;
|
||||||
activeQueueItem.value = nextItem;
|
|
||||||
saveQueueState();
|
saveQueueState();
|
||||||
await refreshTaskViews();
|
upsertHistoryItems(createdItems);
|
||||||
|
void loadDashboard();
|
||||||
|
|
||||||
const payload = buildQueuePayload(created.taskId, createdItem);
|
const payload = buildQueuePayload(created.taskId, createdItems);
|
||||||
queuePayloadText.value = JSON.stringify(payload, null, 2);
|
queuePayloadText.value = JSON.stringify(payload, null, 2);
|
||||||
|
|
||||||
const pushResult = await api.enqueue_json(payload);
|
const pushResult = await api.enqueue_json(payload);
|
||||||
if (!pushResult?.success) {
|
if (!pushResult?.success) {
|
||||||
await submitPatrolDeleteTaskResult(created.taskId, {
|
await submitPatrolDeleteTaskResult(created.taskId, {
|
||||||
shops: [
|
shops: createdItems.map((createdItem) => ({
|
||||||
{
|
shopName: createdItem.shopName || "",
|
||||||
shopName: createdItem.shopName || nextItem.shopName || "",
|
|
||||||
error: pushResult?.error || `任务 ${created.taskId} 推送失败`,
|
error: pushResult?.error || `任务 ${created.taskId} 推送失败`,
|
||||||
countrySections: createdItem.countrySections || [],
|
countrySections: createdItem.countrySections || [],
|
||||||
cartRatios: createdItem.cartRatios || [],
|
cartRatios: createdItem.cartRatios || [],
|
||||||
shopDone: true,
|
shopDone: true,
|
||||||
submissionId: createSubmissionId(
|
submissionId: createSubmissionId(
|
||||||
created.taskId,
|
created.taskId,
|
||||||
createdItem.shopName || nextItem.shopName,
|
createdItem.shopName,
|
||||||
),
|
),
|
||||||
chunkIndex: 1,
|
chunkIndex: 1,
|
||||||
chunkTotal: 1,
|
chunkTotal: 1,
|
||||||
},
|
})),
|
||||||
],
|
|
||||||
});
|
});
|
||||||
removeMatchedRowLocally(nextItem);
|
for (const item of runnable) removeMatchedRowLocally(item);
|
||||||
await refreshTaskViews();
|
await refreshTaskViews();
|
||||||
queuePushResult.value = `任务 ${created.taskId} 推送失败,已自动继续下一个任务`;
|
queuePushResult.value = `任务 ${created.taskId} 推送失败`;
|
||||||
clearActiveQueueTask();
|
clearActiveQueueTask();
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
removeMatchedRowLocally(nextItem);
|
for (const item of runnable) removeMatchedRowLocally(item);
|
||||||
queuePushResult.value =
|
queuePushResult.value = `任务 ${created.taskId} 已入队,共 ${createdItems.length} 个店铺,等待执行完成`;
|
||||||
pendingQueue.value.length > 0
|
|
||||||
? `任务 ${created.taskId} 已入队,等待完成后自动继续下一个(剩余 ${pendingQueue.value.length} 条)`
|
|
||||||
: `任务 ${created.taskId} 已入队,等待执行完成`;
|
|
||||||
}
|
|
||||||
|
|
||||||
queuePushResult.value = "串行队列已执行完成";
|
const finalStatus = await waitForTaskTerminal(created.taskId);
|
||||||
|
queuePushResult.value = `任务 ${created.taskId} ${finalStatus === "SUCCESS" ? "已完成" : finalStatus === "DELETED" ? "已删除" : "执行失败"}`;
|
||||||
|
clearActiveQueueTask();
|
||||||
await refreshTaskViews();
|
await refreshTaskViews();
|
||||||
ElMessage.success("巡店删除队列已按顺序执行完成");
|
ElMessage.success("巡店删除任务已完成");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (disposed) return;
|
if (disposed) return;
|
||||||
const message = error instanceof Error ? error.message : "队列执行失败";
|
const message = error instanceof Error ? error.message : "队列执行失败";
|
||||||
@@ -1000,27 +1149,28 @@ async function processQueue() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function pushToPythonQueue() {
|
async function pushToPythonQueue() {
|
||||||
autoQueueEnabled.value = true;
|
|
||||||
const runnable = matchedRunnableItems.value;
|
const runnable = matchedRunnableItems.value;
|
||||||
if (!runnable.length) {
|
if (!runnable.length) {
|
||||||
ElMessage.warning("请先匹配可用店铺");
|
ElMessage.warning("请先匹配可用店铺");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pendingQueue.value = mergeQueueItems(pendingQueue.value, runnable);
|
queuePushResult.value = `开始创建任务,共 ${runnable.length} 个店铺`;
|
||||||
saveQueueState();
|
|
||||||
queuePushResult.value = `已加入 ${runnable.length} 条店铺,开始串行执行`;
|
|
||||||
await processQueue();
|
await processQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadResult(item: PatrolDeleteHistoryItem) {
|
async function downloadResult(item: PatrolDeleteHistoryItem) {
|
||||||
if (!item.resultId) return;
|
const downloadable = taskGroupItems(item).find((row) =>
|
||||||
|
Boolean(row.resultId && (row.fileReady || row.downloadUrl)),
|
||||||
|
);
|
||||||
|
if (!downloadable?.resultId) return;
|
||||||
const api = getPywebviewApi();
|
const api = getPywebviewApi();
|
||||||
if (!api?.save_file_from_url_new) {
|
if (!api?.save_file_from_url_new) {
|
||||||
ElMessage.error("当前客户端未提供下载能力");
|
ElMessage.error("当前客户端未提供下载能力");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const url = getPatrolDeleteResultDownloadUrl(item.resultId);
|
const url = getPatrolDeleteResultDownloadUrl(downloadable.resultId);
|
||||||
const filename = item.outputFilename || `${item.shopName || "result"}.xlsx`;
|
const filename =
|
||||||
|
downloadable.outputFilename || `${item.shopName || "patrol-delete-result"}.xlsx`;
|
||||||
const result = await api.save_file_from_url_new(url, filename);
|
const result = await api.save_file_from_url_new(url, filename);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
ElMessage.success(`已保存: ${result.path || filename}`);
|
ElMessage.success(`已保存: ${result.path || filename}`);
|
||||||
@@ -1041,7 +1191,8 @@ async function deleteTaskRecord(item: PatrolDeleteHistoryItem) {
|
|||||||
} else {
|
} else {
|
||||||
throw new Error("缺少可删除的任务标识");
|
throw new Error("缺少可删除的任务标识");
|
||||||
}
|
}
|
||||||
await Promise.all([loadCandidates(), refreshTaskViews()]);
|
removeHistoryItemLocally(item);
|
||||||
|
await Promise.allSettled([loadCandidates(), refreshTaskViews()]);
|
||||||
ElMessage.success("已删除");
|
ElMessage.success("已删除");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isRecordMissingError(error)) {
|
if (isRecordMissingError(error)) {
|
||||||
@@ -1060,14 +1211,13 @@ async function deleteTaskRecord(item: PatrolDeleteHistoryItem) {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
loadMatchedItems();
|
loadMatchedItems();
|
||||||
loadQueueState();
|
loadQueueState();
|
||||||
await Promise.all([loadCandidates(), loadDashboard(), loadHistory()]);
|
await Promise.all([loadCandidates(), loadConditions(), loadDashboard(), loadHistory()]);
|
||||||
|
|
||||||
if (activeTaskId.value || pendingQueue.value.length) {
|
if (activeTaskId.value) {
|
||||||
autoQueueEnabled.value = true;
|
|
||||||
queuePushResult.value =
|
queuePushResult.value =
|
||||||
activeTaskId.value != null
|
activeTaskId.value != null
|
||||||
? `检测到未完成队列,继续等待任务 ${activeTaskId.value} 完成并自动接续后续店铺`
|
? `检测到未完成任务,继续等待任务 ${activeTaskId.value} 完成`
|
||||||
: `检测到未完成队列,继续执行剩余 ${pendingQueue.value.length} 条店铺`;
|
: "";
|
||||||
void processQueue();
|
void processQueue();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1096,6 +1246,16 @@ onUnmounted(() => {
|
|||||||
.empty-candidates { color: #666; font-size: 13px; padding: 16px; border: 1px dashed #333; border-radius: 8px; margin-bottom: 16px; }
|
.empty-candidates { color: #666; font-size: 13px; padding: 16px; border: 1px dashed #333; border-radius: 8px; margin-bottom: 16px; }
|
||||||
.candidate-table-scroll { max-height: 320px; overflow: auto; margin-bottom: 18px; border-radius: 8px; border: 1px solid #2a2a2a; }
|
.candidate-table-scroll { max-height: 320px; overflow: auto; margin-bottom: 18px; border-radius: 8px; border: 1px solid #2a2a2a; }
|
||||||
.candidate-table { --el-table-bg-color: #252525; --el-table-tr-bg-color: #252525; --el-table-header-bg-color: #2a2a2a; --el-table-text-color: #ccc; --el-table-border-color: #333; }
|
.candidate-table { --el-table-bg-color: #252525; --el-table-tr-bg-color: #252525; --el-table-header-bg-color: #2a2a2a; --el-table-text-color: #ccc; --el-table-border-color: #333; }
|
||||||
|
.condition-panel { margin-bottom: 18px; border: 1px solid #2a2a2a; border-radius: 8px; background: #222; padding: 12px; }
|
||||||
|
.condition-input-row { display: flex; gap: 10px; align-items: center; margin-bottom: 10px; }
|
||||||
|
.condition-input-row :deep(.el-input) { flex: 1; }
|
||||||
|
.empty-conditions { color: #666; font-size: 12px; padding: 10px 4px; }
|
||||||
|
.condition-list { list-style: none; margin: 0; padding: 0; max-height: 150px; overflow: auto; display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
.condition-item { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 6px; border-bottom: 1px solid #303030; }
|
||||||
|
.condition-item:last-child { border-bottom: none; }
|
||||||
|
.condition-check { display: flex; align-items: center; gap: 8px; min-width: 0; color: #cfd6df; font-size: 12px; cursor: pointer; }
|
||||||
|
.condition-check input { width: 16px; height: 16px; margin: 0; flex: 0 0 auto; }
|
||||||
|
.condition-check span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
.link-danger { background: none; border: none; color: #e74c3c; cursor: pointer; font-size: 12px; padding: 0; }
|
.link-danger { background: none; border: none; color: #e74c3c; cursor: pointer; font-size: 12px; padding: 0; }
|
||||||
.link-danger:hover { text-decoration: underline; }
|
.link-danger:hover { text-decoration: underline; }
|
||||||
.run-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 8px; }
|
.run-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 8px; }
|
||||||
|
|||||||
@@ -73,8 +73,7 @@
|
|||||||
{{ pushing ? '推送中…' : '推送到 Python 队列' }}
|
{{ pushing ? '推送中…' : '推送到 Python 队列' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="loading-msg">与删除 ASIN 品牌相同:先创建任务,再按店铺<strong>逐条</strong>入队(每条 <code>data.items</code>
|
<p class="loading-msg">在搜索结果中禁止显示、详情页面已删除会把匹配店铺合并为一个任务入队,避免同时打开多个紫鸟窗口;其它筛选仍按店铺逐条入队。Python
|
||||||
仅含一个店铺)。队列串行执行;Python
|
|
||||||
每处理完一店可 POST <code>/api/product-risk-resolve/tasks/{taskId}/result</code>(可多次,每次可只含已完成的店铺);全部完成后任务结束。前端轮询任务状态。
|
每处理完一店可 POST <code>/api/product-risk-resolve/tasks/{taskId}/result</code>(可多次,每次可只含已完成的店铺);全部完成后任务结束。前端轮询任务状态。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -1033,6 +1032,53 @@ function nextMatchedQueueItem() {
|
|||||||
return matchedItems.value.find((item) => item.matched)
|
return matchedItems.value.find((item) => item.matched)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldBatchProductRiskQueue() {
|
||||||
|
return productRiskListingFilter.value === 'SearchSuppressed' || productRiskListingFilter.value === 'DetailPageRemoved'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processMatchedBatchQueue(toPush: ProductRiskShopQueueItem[]) {
|
||||||
|
const api = getPywebviewApi()
|
||||||
|
if (!api?.enqueue_json) {
|
||||||
|
ElMessage.error('当前环境未启用 pywebview enqueue_json')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const created = await createProductRiskTaskWithRetry(toPush)
|
||||||
|
const taskId = created.taskId
|
||||||
|
taskSnapshots.value = {
|
||||||
|
...taskSnapshots.value,
|
||||||
|
[taskId]: {
|
||||||
|
task: { id: taskId, status: 'RUNNING' },
|
||||||
|
items: created.items,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
saveTaskSnapshotsToStorage()
|
||||||
|
addPollingTask(taskId)
|
||||||
|
ensurePolling(true)
|
||||||
|
const payload = {
|
||||||
|
type: 'product-risk-resolve-run',
|
||||||
|
ts: Date.now(),
|
||||||
|
data: {
|
||||||
|
taskId,
|
||||||
|
items: toPush,
|
||||||
|
country_codes: [...orderedCountryCodes.value],
|
||||||
|
risk_listing_filter: productRiskListingFilter.value,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
queuePayloadText.value = JSON.stringify(payload, null, 2)
|
||||||
|
const pushResult = await api.enqueue_json(payload)
|
||||||
|
if (!pushResult?.success) {
|
||||||
|
removePollingTask(taskId)
|
||||||
|
throw new Error('任务 ' + taskId + ' 推送失败:' + (pushResult?.error || '未知错误'))
|
||||||
|
}
|
||||||
|
removeMatchedRowsLocally(toPush)
|
||||||
|
queuePushResult.value = '任务 ' + taskId + ' 已入队,共 ' + toPush.length + ' 个店铺,等待执行完成...'
|
||||||
|
const finalStatus = await waitForTaskTerminal(taskId)
|
||||||
|
queuePushResult.value = '任务 ' + taskId + (finalStatus === 'SUCCESS' ? ' 已完成' : ' 执行失败')
|
||||||
|
if (finalStatus !== 'SUCCESS') {
|
||||||
|
throw new Error(queuePushResult.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function processMatchedQueue() {
|
async function processMatchedQueue() {
|
||||||
if (disposed || queueWorkerRunning.value) {
|
if (disposed || queueWorkerRunning.value) {
|
||||||
return
|
return
|
||||||
@@ -1057,6 +1103,14 @@ async function processMatchedQueue() {
|
|||||||
let successCount = 0
|
let successCount = 0
|
||||||
let failedCount = 0
|
let failedCount = 0
|
||||||
try {
|
try {
|
||||||
|
if (shouldBatchProductRiskQueue()) {
|
||||||
|
await processMatchedBatchQueue(toPush)
|
||||||
|
ElMessage.success('店铺推送已完成:成功 ' + toPush.length + ' 条,失败 0 条')
|
||||||
|
await loadHistory()
|
||||||
|
ensurePolling(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let index = 0
|
let index = 0
|
||||||
while (!disposed && autoQueueEnabled.value) {
|
while (!disposed && autoQueueEnabled.value) {
|
||||||
const item = nextMatchedQueueItem()
|
const item = nextMatchedQueueItem()
|
||||||
|
|||||||
@@ -1008,6 +1008,12 @@ export type PatrolDeleteCandidateVo = ProductRiskCandidateVo;
|
|||||||
export type PatrolDeleteDashboardVo = ProductRiskDashboardVo;
|
export type PatrolDeleteDashboardVo = ProductRiskDashboardVo;
|
||||||
export type PatrolDeleteShopQueueItem = ProductRiskShopQueueItem;
|
export type PatrolDeleteShopQueueItem = ProductRiskShopQueueItem;
|
||||||
|
|
||||||
|
export interface PatrolDeleteConditionVo {
|
||||||
|
id: number;
|
||||||
|
conditionText: string;
|
||||||
|
createdAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PatrolDeleteCountryMetricRow {
|
export interface PatrolDeleteCountryMetricRow {
|
||||||
status: string;
|
status: string;
|
||||||
quantity: string;
|
quantity: string;
|
||||||
@@ -1110,6 +1116,40 @@ export function deletePatrolDeleteCandidate(id: number) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function listPatrolDeleteConditions() {
|
||||||
|
return unwrapJavaResponse(
|
||||||
|
get<JavaApiResponse<PatrolDeleteConditionVo[]>>(
|
||||||
|
`${JAVA_API_PREFIX}/patrol-delete/conditions`,
|
||||||
|
{
|
||||||
|
params: { user_id: getCurrentUserId() },
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addPatrolDeleteCondition(conditionText: string) {
|
||||||
|
return unwrapJavaResponse(
|
||||||
|
post<
|
||||||
|
JavaApiResponse<PatrolDeleteConditionVo>,
|
||||||
|
{ user_id: number; condition_text: string }
|
||||||
|
>(`${JAVA_API_PREFIX}/patrol-delete/conditions`, {
|
||||||
|
user_id: getCurrentUserId(),
|
||||||
|
condition_text: conditionText,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deletePatrolDeleteCondition(id: number) {
|
||||||
|
return unwrapJavaResponse(
|
||||||
|
del<JavaApiResponse<null>>(
|
||||||
|
`${JAVA_API_PREFIX}/patrol-delete/conditions/${id}`,
|
||||||
|
{
|
||||||
|
params: { user_id: getCurrentUserId() },
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function matchPatrolDeleteShops(shopNames: string[]) {
|
export function matchPatrolDeleteShops(shopNames: string[]) {
|
||||||
return unwrapJavaResponse(
|
return unwrapJavaResponse(
|
||||||
post<
|
post<
|
||||||
@@ -1451,6 +1491,10 @@ export interface AppearancePatentHistoryItem {
|
|||||||
fileStatus?: string;
|
fileStatus?: string;
|
||||||
fileError?: string;
|
fileError?: string;
|
||||||
fileReady?: boolean;
|
fileReady?: boolean;
|
||||||
|
fileProgressPercent?: number;
|
||||||
|
fileProgressCurrent?: number;
|
||||||
|
fileProgressTotal?: number;
|
||||||
|
fileProgressMessage?: string;
|
||||||
taskStatus?: string;
|
taskStatus?: string;
|
||||||
success?: boolean;
|
success?: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user