完善多店铺开启多窗口、外观进度条等
This commit is contained in:
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,7 +1,8 @@
|
|||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import requests
|
import requests
|
||||||
from datetime import datetime
|
import threading
|
||||||
|
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
|
||||||
from config import JSON_TASK_QUEUE, runing_task, runing_shop, DELETE_BRAND_API_BASE, ZN_COMPANY, ZN_USERNAME, ZN_PASSWORD
|
from config import JSON_TASK_QUEUE, runing_task, runing_shop, DELETE_BRAND_API_BASE, ZN_COMPANY, ZN_USERNAME, ZN_PASSWORD
|
||||||
@@ -31,7 +32,10 @@ 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")
|
||||||
@@ -118,7 +122,17 @@ class TaskMonitor:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
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(
|
||||||
|
|||||||
@@ -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'])
|
||||||
|
|||||||
@@ -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)
|
||||||
|
);
|
||||||
@@ -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 =
|
finalStatus === "DELETED"
|
||||||
pendingQueue.value.length > 0
|
? "任务已删除"
|
||||||
? `任务 ${activeTaskId.value} ${finalStatus === "SUCCESS" ? "已完成" : "执行失败"},自动继续下一个(剩余 ${pendingQueue.value.length} 条)`
|
: `任务 ${activeTaskId.value} ${finalStatus === "SUCCESS" ? "已完成" : "执行失败"}`;
|
||||||
: `任务 ${activeTaskId.value} ${finalStatus === "SUCCESS" ? "已完成" : "执行失败"}`;
|
clearActiveQueueTask();
|
||||||
clearActiveQueueTask();
|
return;
|
||||||
continue;
|
}
|
||||||
|
|
||||||
|
const runnable = matchedRunnableItems.value;
|
||||||
|
if (!runnable.length) {
|
||||||
|
ElMessage.warning("请先匹配可用店铺");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextItem = pendingQueue.value.shift();
|
|
||||||
saveQueueState();
|
|
||||||
if (!nextItem) break;
|
|
||||||
|
|
||||||
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