更新外观模块
This commit is contained in:
@@ -301,6 +301,44 @@ class SimilarAsinTask(TaskBase):
|
|||||||
})
|
})
|
||||||
return normalized
|
return normalized
|
||||||
|
|
||||||
|
def fetch_parsed_payload(self, task_id, user_id=1):
|
||||||
|
url = f"{DELETE_BRAND_API_BASE}/api/similar-asin/tasks/{task_id}/parsed-payload"
|
||||||
|
response = requests.get(url, params={"user_id": user_id}, timeout=300, verify=False)
|
||||||
|
response.raise_for_status()
|
||||||
|
payload = response.json() if response.text else {}
|
||||||
|
if isinstance(payload, dict) and payload.get("success"):
|
||||||
|
return payload.get("data") or {}
|
||||||
|
raise RuntimeError(f"获取货源查询解析载荷失败: {payload}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _count_payload_rows(data):
|
||||||
|
rows = data.get("rows") or data.get("items") or []
|
||||||
|
if isinstance(rows, list) and rows:
|
||||||
|
return len(rows)
|
||||||
|
|
||||||
|
groups = data.get("groups") or []
|
||||||
|
if not isinstance(groups, list):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
total = 0
|
||||||
|
for group in groups:
|
||||||
|
if isinstance(group, dict) and isinstance(group.get("items"), list):
|
||||||
|
total += len(group.get("items"))
|
||||||
|
return total
|
||||||
|
|
||||||
|
def _merge_parsed_payload(self, data, parsed_payload):
|
||||||
|
payload_rows = parsed_payload.get("allItems") or parsed_payload.get("items") or []
|
||||||
|
merged = {
|
||||||
|
**data,
|
||||||
|
"groups": parsed_payload.get("groups") or [],
|
||||||
|
"rows": payload_rows,
|
||||||
|
}
|
||||||
|
if parsed_payload.get("aiPrompt"):
|
||||||
|
merged["prompt"] = parsed_payload.get("aiPrompt")
|
||||||
|
if parsed_payload.get("apiKey") and not merged.get("api_key"):
|
||||||
|
merged["api_key"] = parsed_payload.get("apiKey")
|
||||||
|
return merged
|
||||||
|
|
||||||
def process_task(self, task_data: dict):
|
def process_task(self, task_data: dict):
|
||||||
"""处理审批任务主入口
|
"""处理审批任务主入口
|
||||||
|
|
||||||
@@ -310,6 +348,21 @@ class SimilarAsinTask(TaskBase):
|
|||||||
try:
|
try:
|
||||||
data = task_data.get("data", {})
|
data = task_data.get("data", {})
|
||||||
task_id = data.get("taskId")
|
task_id = data.get("taskId")
|
||||||
|
if task_id:
|
||||||
|
queued_rows = self._count_payload_rows(data)
|
||||||
|
try:
|
||||||
|
parsed_payload = self.fetch_parsed_payload(task_id, data.get("user_id") or data.get("userId") or 1)
|
||||||
|
data = self._merge_parsed_payload(data, parsed_payload)
|
||||||
|
self.log(
|
||||||
|
f"similar asin task {task_id} loaded full parsed payload from Java, queuedRows={queued_rows}, fullRows={self._count_payload_rows(data)}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
if not queued_rows:
|
||||||
|
raise
|
||||||
|
self.log(
|
||||||
|
f"similar asin task {task_id} failed to load full parsed payload, fallback to queued rows={queued_rows}: {e}",
|
||||||
|
"WARNING"
|
||||||
|
)
|
||||||
groups = self.normalize_groups(data)
|
groups = self.normalize_groups(data)
|
||||||
|
|
||||||
if not task_id:
|
if not task_id:
|
||||||
@@ -447,7 +500,7 @@ class SimilarAsinTask(TaskBase):
|
|||||||
"""回传处理结果到API
|
"""回传处理结果到API
|
||||||
"""
|
"""
|
||||||
|
|
||||||
url = f"{DELETE_BRAND_API_BASE}/api/appearance-patent/tasks/{task_id}/result"
|
url = f"{DELETE_BRAND_API_BASE}/api/similar-asin/tasks/{task_id}/result"
|
||||||
|
|
||||||
payload ={
|
payload ={
|
||||||
"submissionId": f"{int(time.time())}",
|
"submissionId": f"{int(time.time())}",
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ public class TaskOperationLockConfig implements WebMvcConfigurer {
|
|||||||
private static final Pattern TASK_RESULT_PATH = Pattern.compile(".*/api/([^/]+)/tasks/(\\d+)/result/?$");
|
private static final Pattern TASK_RESULT_PATH = Pattern.compile(".*/api/([^/]+)/tasks/(\\d+)/result/?$");
|
||||||
private static final Set<String> MUTATING_METHODS = Set.of("POST", "PUT", "PATCH", "DELETE");
|
private static final Set<String> MUTATING_METHODS = Set.of("POST", "PUT", "PATCH", "DELETE");
|
||||||
private static final long RESULT_SUBMIT_WAIT_MILLIS = 10 * 60 * 1000L;
|
private static final long RESULT_SUBMIT_WAIT_MILLIS = 10 * 60 * 1000L;
|
||||||
|
private static final long DELETE_WAIT_MILLIS = 60 * 1000L;
|
||||||
private static final String LOCK_ATTRIBUTE = TaskOperationLockInterceptor.class.getName() + ".LOCK";
|
private static final String LOCK_ATTRIBUTE = TaskOperationLockInterceptor.class.getName() + ".LOCK";
|
||||||
|
|
||||||
private final TaskDistributedLockService taskDistributedLockService;
|
private final TaskDistributedLockService taskDistributedLockService;
|
||||||
@@ -71,6 +72,9 @@ public class TaskOperationLockConfig implements WebMvcConfigurer {
|
|||||||
if ("POST".equals(method) && TASK_RESULT_PATH.matcher(uri == null ? "" : uri).matches()) {
|
if ("POST".equals(method) && TASK_RESULT_PATH.matcher(uri == null ? "" : uri).matches()) {
|
||||||
return RESULT_SUBMIT_WAIT_MILLIS;
|
return RESULT_SUBMIT_WAIT_MILLIS;
|
||||||
}
|
}
|
||||||
|
if ("DELETE".equals(method)) {
|
||||||
|
return DELETE_WAIT_MILLIS;
|
||||||
|
}
|
||||||
return TaskDistributedLockService.DEFAULT_WAIT_MILLIS;
|
return TaskDistributedLockService.DEFAULT_WAIT_MILLIS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import com.nanri.aiimage.modules.shopmatch.service.ShopMatchTaskService;
|
|||||||
import com.nanri.aiimage.modules.task.mapper.FileTaskMapper;
|
import com.nanri.aiimage.modules.task.mapper.FileTaskMapper;
|
||||||
import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity;
|
import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity;
|
||||||
import com.nanri.aiimage.modules.task.service.TaskDistributedLockService;
|
import com.nanri.aiimage.modules.task.service.TaskDistributedLockService;
|
||||||
|
import com.nanri.aiimage.modules.task.service.TaskFileJobService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
@@ -60,6 +61,7 @@ public class DeleteBrandStaleTaskService {
|
|||||||
private final DeleteBrandProgressProperties deleteBrandProgressProperties;
|
private final DeleteBrandProgressProperties deleteBrandProgressProperties;
|
||||||
private final DistributedJobLockService distributedJobLockService;
|
private final DistributedJobLockService distributedJobLockService;
|
||||||
private final TaskDistributedLockService taskDistributedLockService;
|
private final TaskDistributedLockService taskDistributedLockService;
|
||||||
|
private final TaskFileJobService taskFileJobService;
|
||||||
|
|
||||||
@Value("${aiimage.temp-dir.retention-hours:24}")
|
@Value("${aiimage.temp-dir.retention-hours:24}")
|
||||||
private long tempDirRetentionHours;
|
private long tempDirRetentionHours;
|
||||||
@@ -231,6 +233,14 @@ public class DeleteBrandStaleTaskService {
|
|||||||
if (!hasStartedProgress) {
|
if (!hasStartedProgress) {
|
||||||
hasStartedProgress = productRiskTaskCacheService.hasAnyShopMergedPayload(task.getId());
|
hasStartedProgress = productRiskTaskCacheService.hasAnyShopMergedPayload(task.getId());
|
||||||
}
|
}
|
||||||
|
if (hasPendingAssembleJobs(task.getId(), MODULE_TYPE_PRODUCT_RISK)) {
|
||||||
|
stats.skippedTaskCount++;
|
||||||
|
log.info("[stale-check] product-risk skip pending-assemble-jobs taskId={} unfinishedJobs={} activeJobs={}",
|
||||||
|
task.getId(),
|
||||||
|
taskFileJobService.countUnfinishedAssembleJobs(task.getId(), MODULE_TYPE_PRODUCT_RISK),
|
||||||
|
taskFileJobService.countActiveAssembleJobs(task.getId(), MODULE_TYPE_PRODUCT_RISK));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!hasStartedProgress && isWithinInitialGrace(task, initialThreshold)) {
|
if (!hasStartedProgress && isWithinInitialGrace(task, initialThreshold)) {
|
||||||
stats.skippedTaskCount++;
|
stats.skippedTaskCount++;
|
||||||
log.info("[stale-check] product-risk skip initial-grace taskId={} createdAt={} initialThreshold={}",
|
log.info("[stale-check] product-risk skip initial-grace taskId={} createdAt={} initialThreshold={}",
|
||||||
@@ -305,6 +315,14 @@ public class DeleteBrandStaleTaskService {
|
|||||||
if (!hasStartedProgress) {
|
if (!hasStartedProgress) {
|
||||||
hasStartedProgress = priceTrackTaskCacheService.hasAnyShopMergedPayload(task.getId());
|
hasStartedProgress = priceTrackTaskCacheService.hasAnyShopMergedPayload(task.getId());
|
||||||
}
|
}
|
||||||
|
if (hasPendingAssembleJobs(task.getId(), MODULE_TYPE_PRICE_TRACK)) {
|
||||||
|
stats.skippedTaskCount++;
|
||||||
|
log.info("[stale-check] price-track skip pending-assemble-jobs taskId={} unfinishedJobs={} activeJobs={}",
|
||||||
|
task.getId(),
|
||||||
|
taskFileJobService.countUnfinishedAssembleJobs(task.getId(), MODULE_TYPE_PRICE_TRACK),
|
||||||
|
taskFileJobService.countActiveAssembleJobs(task.getId(), MODULE_TYPE_PRICE_TRACK));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!hasStartedProgress && isWithinInitialGrace(task, initialThreshold)) {
|
if (!hasStartedProgress && isWithinInitialGrace(task, initialThreshold)) {
|
||||||
stats.skippedTaskCount++;
|
stats.skippedTaskCount++;
|
||||||
continue;
|
continue;
|
||||||
@@ -380,6 +398,14 @@ public class DeleteBrandStaleTaskService {
|
|||||||
if (!hasStartedProgress) {
|
if (!hasStartedProgress) {
|
||||||
hasStartedProgress = shopMatchTaskCacheService.hasAnyShopMergedPayload(task.getId());
|
hasStartedProgress = shopMatchTaskCacheService.hasAnyShopMergedPayload(task.getId());
|
||||||
}
|
}
|
||||||
|
if (hasPendingAssembleJobs(task.getId(), MODULE_TYPE_SHOP_MATCH)) {
|
||||||
|
stats.skippedTaskCount++;
|
||||||
|
log.info("[stale-check] shop-match skip pending-assemble-jobs taskId={} unfinishedJobs={} activeJobs={}",
|
||||||
|
task.getId(),
|
||||||
|
taskFileJobService.countUnfinishedAssembleJobs(task.getId(), MODULE_TYPE_SHOP_MATCH),
|
||||||
|
taskFileJobService.countActiveAssembleJobs(task.getId(), MODULE_TYPE_SHOP_MATCH));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!hasStartedProgress && isWithinInitialGrace(task, initialThreshold)) {
|
if (!hasStartedProgress && isWithinInitialGrace(task, initialThreshold)) {
|
||||||
stats.skippedTaskCount++;
|
stats.skippedTaskCount++;
|
||||||
log.info("[stale-check] shop-match skip initial-grace taskId={} createdAt={} initialThreshold={}",
|
log.info("[stale-check] shop-match skip initial-grace taskId={} createdAt={} initialThreshold={}",
|
||||||
@@ -453,6 +479,14 @@ public class DeleteBrandStaleTaskService {
|
|||||||
if (!hasStartedProgress) {
|
if (!hasStartedProgress) {
|
||||||
hasStartedProgress = patrolDeleteTaskCacheService.hasAnyShopMergedPayload(task.getId());
|
hasStartedProgress = patrolDeleteTaskCacheService.hasAnyShopMergedPayload(task.getId());
|
||||||
}
|
}
|
||||||
|
if (hasPendingAssembleJobs(task.getId(), MODULE_TYPE_PATROL_DELETE)) {
|
||||||
|
stats.skippedTaskCount++;
|
||||||
|
log.info("[stale-check] patrol-delete skip pending-assemble-jobs taskId={} unfinishedJobs={} activeJobs={}",
|
||||||
|
task.getId(),
|
||||||
|
taskFileJobService.countUnfinishedAssembleJobs(task.getId(), MODULE_TYPE_PATROL_DELETE),
|
||||||
|
taskFileJobService.countActiveAssembleJobs(task.getId(), MODULE_TYPE_PATROL_DELETE));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!hasStartedProgress && isWithinInitialGrace(task, initialThreshold)) {
|
if (!hasStartedProgress && isWithinInitialGrace(task, initialThreshold)) {
|
||||||
stats.skippedTaskCount++;
|
stats.skippedTaskCount++;
|
||||||
continue;
|
continue;
|
||||||
@@ -612,6 +646,10 @@ public class DeleteBrandStaleTaskService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean hasPendingAssembleJobs(Long taskId, String moduleType) {
|
||||||
|
return taskFileJobService.countUnfinishedAssembleJobs(taskId, moduleType) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
private static final class ProductRiskStaleCheckStats {
|
private static final class ProductRiskStaleCheckStats {
|
||||||
private int scannedTaskCount;
|
private int scannedTaskCount;
|
||||||
private int finalizedTaskCount;
|
private int finalizedTaskCount;
|
||||||
|
|||||||
@@ -155,8 +155,10 @@ public class PatrolDeleteController {
|
|||||||
@Operation(summary = "查询任务记录", description = "返回当前用户在巡店删除模块中的当前任务和历史任务记录。")
|
@Operation(summary = "查询任务记录", description = "返回当前用户在巡店删除模块中的当前任务和历史任务记录。")
|
||||||
public ApiResponse<PatrolDeleteHistoryVo> history(
|
public ApiResponse<PatrolDeleteHistoryVo> history(
|
||||||
@Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1")
|
@Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1")
|
||||||
@RequestParam("user_id") Long userId) {
|
@RequestParam("user_id") Long userId,
|
||||||
return ApiResponse.success(patrolDeleteTaskService.listHistory(userId));
|
@Parameter(description = "history limit, default 30, max 100", example = "30")
|
||||||
|
@RequestParam(value = "limit", required = false) Integer limit) {
|
||||||
|
return ApiResponse.success(patrolDeleteTaskService.listHistory(userId, limit));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/tasks/progress/batch")
|
@PostMapping("/tasks/progress/batch")
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import com.nanri.aiimage.modules.task.mapper.FileTaskMapper;
|
|||||||
import com.nanri.aiimage.modules.task.model.entity.FileResultEntity;
|
import com.nanri.aiimage.modules.task.model.entity.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.service.TaskDistributedLockService;
|
||||||
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.TaskProgressSnapshotService;
|
||||||
import com.nanri.aiimage.modules.task.service.TaskResultItemService;
|
import com.nanri.aiimage.modules.task.service.TaskResultItemService;
|
||||||
@@ -68,6 +69,7 @@ public class PatrolDeleteTaskService {
|
|||||||
private final TaskResultItemService taskResultItemService;
|
private final TaskResultItemService taskResultItemService;
|
||||||
private final TaskProgressSnapshotService taskProgressSnapshotService;
|
private final TaskProgressSnapshotService taskProgressSnapshotService;
|
||||||
private final TaskScopePayloadStorageService taskScopePayloadStorageService;
|
private final TaskScopePayloadStorageService taskScopePayloadStorageService;
|
||||||
|
private final TaskDistributedLockService taskDistributedLockService;
|
||||||
|
|
||||||
private FileTaskEntity loadTaskForExecution(Long taskId) {
|
private FileTaskEntity loadTaskForExecution(Long taskId) {
|
||||||
Map<Long, FileTaskEntity> cachedTasks = taskCacheService.getTaskCacheBatch(List.of(taskId));
|
Map<Long, FileTaskEntity> cachedTasks = taskCacheService.getTaskCacheBatch(List.of(taskId));
|
||||||
@@ -137,8 +139,13 @@ public class PatrolDeleteTaskService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public PatrolDeleteHistoryVo listHistory(Long userId) {
|
public PatrolDeleteHistoryVo listHistory(Long userId) {
|
||||||
|
return listHistory(userId, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PatrolDeleteHistoryVo listHistory(Long userId, Integer limit) {
|
||||||
long startedAt = System.nanoTime();
|
long startedAt = System.nanoTime();
|
||||||
validateUserId(userId);
|
validateUserId(userId);
|
||||||
|
int normalizedLimit = normalizeHistoryLimit(limit);
|
||||||
PatrolDeleteHistoryVo vo = new PatrolDeleteHistoryVo();
|
PatrolDeleteHistoryVo vo = new PatrolDeleteHistoryVo();
|
||||||
List<FileResultEntity> entities = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
List<FileResultEntity> entities = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
||||||
.select(FileResultEntity::getId,
|
.select(FileResultEntity::getId,
|
||||||
@@ -155,7 +162,8 @@ public class PatrolDeleteTaskService {
|
|||||||
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
||||||
.eq(FileResultEntity::getUserId, userId)
|
.eq(FileResultEntity::getUserId, userId)
|
||||||
.orderByDesc(FileResultEntity::getCreatedAt)
|
.orderByDesc(FileResultEntity::getCreatedAt)
|
||||||
.last("limit 100"));
|
.orderByDesc(FileResultEntity::getId)
|
||||||
|
.last("limit " + normalizedLimit));
|
||||||
long resultRowsLoadedAt = System.nanoTime();
|
long resultRowsLoadedAt = System.nanoTime();
|
||||||
if (entities.isEmpty()) {
|
if (entities.isEmpty()) {
|
||||||
vo.setItems(List.of());
|
vo.setItems(List.of());
|
||||||
@@ -481,9 +489,20 @@ public class PatrolDeleteTaskService {
|
|||||||
throw new BusinessException("记录不存在");
|
throw new BusinessException("记录不存在");
|
||||||
}
|
}
|
||||||
Long taskId = entity.getTaskId();
|
Long taskId = entity.getTaskId();
|
||||||
fileResultMapper.deleteById(resultId);
|
if (taskId == null || taskId <= 0) {
|
||||||
cleanupResultAuxiliaryDataFast(taskId, resultId);
|
fileResultMapper.deleteById(resultId);
|
||||||
reconcileTaskAfterResultRemoval(taskId);
|
cleanupResultAuxiliaryDataFast(taskId, resultId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try (TaskDistributedLockService.LockHandle ignored = acquireTaskLockOrThrow(taskId)) {
|
||||||
|
FileResultEntity latestEntity = fileResultMapper.selectById(resultId);
|
||||||
|
if (latestEntity == null || !MODULE_TYPE.equals(latestEntity.getModuleType()) || !userId.equals(latestEntity.getUserId())) {
|
||||||
|
throw new BusinessException("record not found");
|
||||||
|
}
|
||||||
|
fileResultMapper.deleteById(resultId);
|
||||||
|
cleanupResultAuxiliaryDataFast(taskId, resultId);
|
||||||
|
reconcileTaskAfterResultRemoval(taskId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void reconcileTaskAfterResultRemoval(Long taskId) {
|
private void reconcileTaskAfterResultRemoval(Long taskId) {
|
||||||
@@ -560,6 +579,13 @@ public class PatrolDeleteTaskService {
|
|||||||
return payloadByShop;
|
return payloadByShop;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int normalizeHistoryLimit(Integer limit) {
|
||||||
|
if (limit == null || limit <= 0) {
|
||||||
|
return 30;
|
||||||
|
}
|
||||||
|
return Math.min(limit, 100);
|
||||||
|
}
|
||||||
|
|
||||||
private Map<Long, FileTaskEntity> loadTaskMap(List<FileResultEntity> entities) {
|
private Map<Long, FileTaskEntity> loadTaskMap(List<FileResultEntity> entities) {
|
||||||
List<Long> taskIds = entities.stream()
|
List<Long> taskIds = entities.stream()
|
||||||
.map(FileResultEntity::getTaskId)
|
.map(FileResultEntity::getTaskId)
|
||||||
@@ -760,6 +786,9 @@ public class PatrolDeleteTaskService {
|
|||||||
long successCount = rows.stream().filter(row -> Integer.valueOf(RESULT_SUCCESS).equals(row.getSuccess())).count();
|
long successCount = rows.stream().filter(row -> Integer.valueOf(RESULT_SUCCESS).equals(row.getSuccess())).count();
|
||||||
long failedCount = rows.stream().filter(row -> Integer.valueOf(RESULT_FAILED).equals(row.getSuccess())).count();
|
long failedCount = rows.stream().filter(row -> Integer.valueOf(RESULT_FAILED).equals(row.getSuccess())).count();
|
||||||
long pendingCount = rows.stream().filter(row -> !isResultFinished(row)).count();
|
long pendingCount = rows.stream().filter(row -> !isResultFinished(row)).count();
|
||||||
|
if (pendingCount == 0 && successCount > 0 && isTaskWorkbookPending(task, rows)) {
|
||||||
|
pendingCount = 1;
|
||||||
|
}
|
||||||
task.setSuccessFileCount((int) successCount);
|
task.setSuccessFileCount((int) successCount);
|
||||||
task.setFailedFileCount((int) failedCount);
|
task.setFailedFileCount((int) failedCount);
|
||||||
task.setUpdatedAt(LocalDateTime.now());
|
task.setUpdatedAt(LocalDateTime.now());
|
||||||
@@ -975,11 +1004,47 @@ public class PatrolDeleteTaskService {
|
|||||||
fileResultMapper.updateById(row);
|
fileResultMapper.updateById(row);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
updateTaskStatusFromRows(task, rows);
|
||||||
|
persistSnapshotJson(task, buildSnapshotFromDb(task, rows));
|
||||||
|
fileTaskMapper.updateById(task);
|
||||||
} finally {
|
} finally {
|
||||||
FileUtil.del(xlsx);
|
FileUtil.del(xlsx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isTaskWorkbookPending(FileTaskEntity task, List<FileResultEntity> rows) {
|
||||||
|
if (task == null || task.getId() == null || rows == null || rows.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
FileResultEntity firstSuccess = rows.stream()
|
||||||
|
.filter(row -> Integer.valueOf(RESULT_SUCCESS).equals(row.getSuccess()))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
if (firstSuccess == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!blank(firstSuccess.getResultFileUrl())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
TaskFileJobEntity job = taskFileJobService.findAssembleJob(task.getId(), MODULE_TYPE, firstSuccess.getId());
|
||||||
|
return job != null && !"SUCCESS".equals(job.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
private TaskDistributedLockService.LockHandle acquireTaskLockOrThrow(Long taskId) {
|
||||||
|
TaskDistributedLockService.LockHandle lockHandle = acquireTaskLock(taskId);
|
||||||
|
if (lockHandle == null) {
|
||||||
|
throw new BusinessException(40901, "浠诲姟姝e湪澶勭悊涓紝璇风◢鍚庡啀璇?");
|
||||||
|
}
|
||||||
|
return lockHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TaskDistributedLockService.LockHandle acquireTaskLock(Long taskId) {
|
||||||
|
if (taskId == null || taskId <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return taskDistributedLockService.acquire(MODULE_TYPE, taskId);
|
||||||
|
}
|
||||||
|
|
||||||
private void markResultSuccess(FileResultEntity row) {
|
private void markResultSuccess(FileResultEntity row) {
|
||||||
row.setSuccess(RESULT_SUCCESS);
|
row.setSuccess(RESULT_SUCCESS);
|
||||||
row.setErrorMessage(null);
|
row.setErrorMessage(null);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import com.nanri.aiimage.modules.task.mapper.FileTaskMapper;
|
|||||||
import com.nanri.aiimage.modules.task.model.entity.FileResultEntity;
|
import com.nanri.aiimage.modules.task.model.entity.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.service.TaskDistributedLockService;
|
||||||
import com.nanri.aiimage.modules.task.service.TaskFileJobService;
|
import com.nanri.aiimage.modules.task.service.TaskFileJobService;
|
||||||
import com.nanri.aiimage.modules.task.service.TaskResultPayloadService;
|
import com.nanri.aiimage.modules.task.service.TaskResultPayloadService;
|
||||||
import com.nanri.aiimage.modules.ziniao.service.ZiniaoShopSwitchService;
|
import com.nanri.aiimage.modules.ziniao.service.ZiniaoShopSwitchService;
|
||||||
@@ -71,6 +72,7 @@ public class PriceTrackTaskService {
|
|||||||
private final TaskPressureProperties taskPressureProperties;
|
private final TaskPressureProperties taskPressureProperties;
|
||||||
private final TaskResultPayloadService taskResultPayloadService;
|
private final TaskResultPayloadService taskResultPayloadService;
|
||||||
private final TaskFileJobService taskFileJobService;
|
private final TaskFileJobService taskFileJobService;
|
||||||
|
private final TaskDistributedLockService taskDistributedLockService;
|
||||||
|
|
||||||
private FileTaskEntity loadTaskForExecution(Long taskId) {
|
private FileTaskEntity loadTaskForExecution(Long taskId) {
|
||||||
Map<Long, FileTaskEntity> cachedTasks = priceTrackTaskCacheService.getTaskCacheBatch(List.of(taskId));
|
Map<Long, FileTaskEntity> cachedTasks = priceTrackTaskCacheService.getTaskCacheBatch(List.of(taskId));
|
||||||
@@ -239,8 +241,16 @@ public class PriceTrackTaskService {
|
|||||||
throw new BusinessException("记录不存在");
|
throw new BusinessException("记录不存在");
|
||||||
}
|
}
|
||||||
Long taskId = entity.getTaskId();
|
Long taskId = entity.getTaskId();
|
||||||
fileResultMapper.deleteById(resultId);
|
if (taskId == null || taskId <= 0) {
|
||||||
if (taskId != null && taskId > 0) {
|
fileResultMapper.deleteById(resultId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try (TaskDistributedLockService.LockHandle ignored = acquireTaskLockOrThrow(taskId)) {
|
||||||
|
FileResultEntity latestEntity = fileResultMapper.selectById(resultId);
|
||||||
|
if (latestEntity == null || !MODULE_TYPE.equals(latestEntity.getModuleType()) || !userId.equals(latestEntity.getUserId())) {
|
||||||
|
throw new BusinessException("record not found");
|
||||||
|
}
|
||||||
|
fileResultMapper.deleteById(resultId);
|
||||||
reconcileTaskAfterResultRemoval(taskId);
|
reconcileTaskAfterResultRemoval(taskId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -281,10 +291,18 @@ public class PriceTrackTaskService {
|
|||||||
FileTaskEntity task = loadTaskForExecution(fr.getTaskId());
|
FileTaskEntity task = loadTaskForExecution(fr.getTaskId());
|
||||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) continue;
|
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) continue;
|
||||||
if (!"RUNNING".equals(task.getStatus())) continue;
|
if (!"RUNNING".equals(task.getStatus())) continue;
|
||||||
fileResultMapper.deleteById(fr.getId());
|
try (TaskDistributedLockService.LockHandle ignored = acquireTaskLock(fr.getTaskId())) {
|
||||||
reconcileTaskAfterResultRemoval(fr.getTaskId());
|
if (ignored == null) continue;
|
||||||
vo.setRemoved(true);
|
FileTaskEntity lockedTask = loadTaskForExecution(fr.getTaskId());
|
||||||
return vo;
|
if (lockedTask == null || !MODULE_TYPE.equals(lockedTask.getModuleType()) || !userId.equals(lockedTask.getUserId())) continue;
|
||||||
|
if (!"RUNNING".equals(lockedTask.getStatus())) continue;
|
||||||
|
FileResultEntity lockedResult = fileResultMapper.selectById(fr.getId());
|
||||||
|
if (lockedResult == null || !MODULE_TYPE.equals(lockedResult.getModuleType())) continue;
|
||||||
|
fileResultMapper.deleteById(fr.getId());
|
||||||
|
reconcileTaskAfterResultRemoval(fr.getTaskId());
|
||||||
|
vo.setRemoved(true);
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
@@ -687,6 +705,15 @@ public class PriceTrackTaskService {
|
|||||||
throw new BusinessException("结果文件载荷不存在");
|
throw new BusinessException("结果文件载荷不存在");
|
||||||
}
|
}
|
||||||
assembleShopResult(result, job.getScopeKey(), payload);
|
assembleShopResult(result, job.getScopeKey(), payload);
|
||||||
|
FileTaskEntity task = loadTaskForExecution(job.getTaskId());
|
||||||
|
if (task != null && MODULE_TYPE.equals(task.getModuleType())) {
|
||||||
|
List<FileResultEntity> latest = fileResultMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<FileResultEntity>()
|
||||||
|
.eq(FileResultEntity::getTaskId, job.getTaskId())
|
||||||
|
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
||||||
|
.orderByAsc(FileResultEntity::getId));
|
||||||
|
updateTaskStatusFromLatestRows(task, latest);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void enqueueResultFileAssembly(FileResultEntity result,
|
private void enqueueResultFileAssembly(FileResultEntity result,
|
||||||
@@ -1280,10 +1307,20 @@ public class PriceTrackTaskService {
|
|||||||
int fail = 0;
|
int fail = 0;
|
||||||
List<String> allErrors = new ArrayList<>();
|
List<String> allErrors = new ArrayList<>();
|
||||||
boolean allDone = true;
|
boolean allDone = true;
|
||||||
|
Map<Long, TaskFileJobEntity> jobMap = taskFileJobService.findAssembleJobsByResultIds(
|
||||||
|
MODULE_TYPE,
|
||||||
|
latest.stream()
|
||||||
|
.map(FileResultEntity::getId)
|
||||||
|
.filter(id -> id != null && id > 0)
|
||||||
|
.toList());
|
||||||
for (FileResultEntity fr : latest) {
|
for (FileResultEntity fr : latest) {
|
||||||
boolean success = fr.getSuccess() != null && fr.getSuccess() == 1;
|
boolean success = fr.getSuccess() != null && fr.getSuccess() == 1;
|
||||||
boolean failed = fr.getErrorMessage() != null && !fr.getErrorMessage().isBlank();
|
boolean failed = fr.getErrorMessage() != null && !fr.getErrorMessage().isBlank();
|
||||||
if (success) {
|
if (success) {
|
||||||
|
if (isResultAwaitingFileAssembly(fr, jobMap.get(fr.getId()))) {
|
||||||
|
allDone = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
ok++;
|
ok++;
|
||||||
} else if (failed) {
|
} else if (failed) {
|
||||||
fail++;
|
fail++;
|
||||||
@@ -1327,6 +1364,34 @@ public class PriceTrackTaskService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isResultAwaitingFileAssembly(FileResultEntity result, TaskFileJobEntity job) {
|
||||||
|
if (result == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (result.getResultFileUrl() != null && !result.getResultFileUrl().isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (result.getResultFilename() == null || result.getResultFilename().isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return job == null || !"SUCCESS".equals(job.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
private TaskDistributedLockService.LockHandle acquireTaskLockOrThrow(Long taskId) {
|
||||||
|
TaskDistributedLockService.LockHandle lockHandle = acquireTaskLock(taskId);
|
||||||
|
if (lockHandle == null) {
|
||||||
|
throw new BusinessException(40901, "浠诲姟姝e湪澶勭悊涓紝璇风◢鍚庡啀璇?");
|
||||||
|
}
|
||||||
|
return lockHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TaskDistributedLockService.LockHandle acquireTaskLock(Long taskId) {
|
||||||
|
if (taskId == null || taskId <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return taskDistributedLockService.acquire(MODULE_TYPE, taskId);
|
||||||
|
}
|
||||||
|
|
||||||
private void cleanupTaskCacheIfTerminal(Long taskId, String taskStatus) {
|
private void cleanupTaskCacheIfTerminal(Long taskId, String taskStatus) {
|
||||||
if (!"SUCCESS".equals(taskStatus) && !"FAILED".equals(taskStatus) && !"DELETE_EMPTY".equals(taskStatus)) {
|
if (!"SUCCESS".equals(taskStatus) && !"FAILED".equals(taskStatus) && !"DELETE_EMPTY".equals(taskStatus)) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import com.nanri.aiimage.modules.task.mapper.FileTaskMapper;
|
|||||||
import com.nanri.aiimage.modules.task.model.entity.FileResultEntity;
|
import com.nanri.aiimage.modules.task.model.entity.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.service.TaskDistributedLockService;
|
||||||
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.TaskProgressSnapshotService;
|
||||||
import com.nanri.aiimage.modules.task.service.TaskResultItemService;
|
import com.nanri.aiimage.modules.task.service.TaskResultItemService;
|
||||||
@@ -68,6 +69,7 @@ public class ProductRiskTaskService {
|
|||||||
private final TaskFileJobService taskFileJobService;
|
private final TaskFileJobService taskFileJobService;
|
||||||
private final TaskResultItemService taskResultItemService;
|
private final TaskResultItemService taskResultItemService;
|
||||||
private final TaskProgressSnapshotService taskProgressSnapshotService;
|
private final TaskProgressSnapshotService taskProgressSnapshotService;
|
||||||
|
private final TaskDistributedLockService taskDistributedLockService;
|
||||||
|
|
||||||
private FileTaskEntity loadTaskForExecution(Long taskId) {
|
private FileTaskEntity loadTaskForExecution(Long taskId) {
|
||||||
Map<Long, FileTaskEntity> cachedTasks = productRiskTaskCacheService.getTaskCacheBatch(List.of(taskId));
|
Map<Long, FileTaskEntity> cachedTasks = productRiskTaskCacheService.getTaskCacheBatch(List.of(taskId));
|
||||||
@@ -243,8 +245,16 @@ public class ProductRiskTaskService {
|
|||||||
throw new BusinessException("记录不存在");
|
throw new BusinessException("记录不存在");
|
||||||
}
|
}
|
||||||
Long taskId = entity.getTaskId();
|
Long taskId = entity.getTaskId();
|
||||||
fileResultMapper.deleteById(resultId);
|
if (taskId == null || taskId <= 0) {
|
||||||
if (taskId != null && taskId > 0) {
|
fileResultMapper.deleteById(resultId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try (TaskDistributedLockService.LockHandle ignored = acquireTaskLockOrThrow(taskId)) {
|
||||||
|
FileResultEntity latestEntity = fileResultMapper.selectById(resultId);
|
||||||
|
if (latestEntity == null || !MODULE_TYPE.equals(latestEntity.getModuleType()) || !userId.equals(latestEntity.getUserId())) {
|
||||||
|
throw new BusinessException("record not found");
|
||||||
|
}
|
||||||
|
fileResultMapper.deleteById(resultId);
|
||||||
reconcileTaskAfterResultRemoval(taskId);
|
reconcileTaskAfterResultRemoval(taskId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -304,10 +314,26 @@ public class ProductRiskTaskService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Long tid = fr.getTaskId();
|
Long tid = fr.getTaskId();
|
||||||
fileResultMapper.deleteById(fr.getId());
|
try (TaskDistributedLockService.LockHandle ignored = acquireTaskLock(tid)) {
|
||||||
reconcileTaskAfterResultRemoval(tid);
|
if (ignored == null) {
|
||||||
vo.setRemoved(true);
|
continue;
|
||||||
return vo;
|
}
|
||||||
|
FileTaskEntity lockedTask = loadTaskForExecution(tid);
|
||||||
|
if (lockedTask == null || !MODULE_TYPE.equals(lockedTask.getModuleType()) || !userId.equals(lockedTask.getUserId())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!"RUNNING".equals(lockedTask.getStatus())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
FileResultEntity lockedResult = fileResultMapper.selectById(fr.getId());
|
||||||
|
if (lockedResult == null || !MODULE_TYPE.equals(lockedResult.getModuleType())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
fileResultMapper.deleteById(fr.getId());
|
||||||
|
reconcileTaskAfterResultRemoval(tid);
|
||||||
|
vo.setRemoved(true);
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
@@ -631,54 +657,10 @@ public class ProductRiskTaskService {
|
|||||||
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
||||||
.orderByAsc(FileResultEntity::getId));
|
.orderByAsc(FileResultEntity::getId));
|
||||||
|
|
||||||
int ok = 0;
|
updateTaskStatusFromLatestRows(task, latest, batchErrors);
|
||||||
int fail = 0;
|
log.warn("[product-risk] submitResult status evaluated taskId={} oldStatus={} newStatus={} matchedShopCount={} skippedUnmatchedCount={} waitingCount={} assembledCount={} fallbackMatchedCount={} batchErrors={}",
|
||||||
List<String> allErrors = new ArrayList<>();
|
taskId, oldStatus, task.getStatus(), matchedShopCount, skippedUnmatchedCount,
|
||||||
boolean allDone = true;
|
|
||||||
for (FileResultEntity fr : latest) {
|
|
||||||
boolean success = fr.getSuccess() != null && fr.getSuccess() == 1;
|
|
||||||
boolean failed = fr.getErrorMessage() != null && !fr.getErrorMessage().isBlank();
|
|
||||||
if (success) {
|
|
||||||
ok++;
|
|
||||||
} else if (failed) {
|
|
||||||
fail++;
|
|
||||||
allErrors.add(fr.getSourceFilename() + ": " + fr.getErrorMessage());
|
|
||||||
} else {
|
|
||||||
allDone = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
task.setSuccessFileCount(ok);
|
|
||||||
task.setFailedFileCount(fail);
|
|
||||||
task.setUpdatedAt(LocalDateTime.now());
|
|
||||||
|
|
||||||
if (!allDone) {
|
|
||||||
task.setStatus("RUNNING");
|
|
||||||
task.setErrorMessage(null);
|
|
||||||
task.setFinishedAt(null);
|
|
||||||
} else if (ok > 0 && fail == 0) {
|
|
||||||
task.setStatus("SUCCESS");
|
|
||||||
task.setErrorMessage(null);
|
|
||||||
task.setFinishedAt(LocalDateTime.now());
|
|
||||||
} else if (ok > 0) {
|
|
||||||
task.setStatus("SUCCESS");
|
|
||||||
task.setErrorMessage(String.join("; ", allErrors.isEmpty() ? batchErrors : allErrors));
|
|
||||||
task.setFinishedAt(LocalDateTime.now());
|
|
||||||
} else {
|
|
||||||
task.setStatus("FAILED");
|
|
||||||
task.setErrorMessage(allErrors.isEmpty() ? "全部店铺处理失败" : String.join("; ", allErrors));
|
|
||||||
task.setFinishedAt(LocalDateTime.now());
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
task.setResultJson(objectMapper.writeValueAsString(buildSnapshotFromDb(taskId, task.getStatus())));
|
|
||||||
} catch (Exception ex) {
|
|
||||||
log.warn("[product-risk] compact result json failed: {}", ex.getMessage());
|
|
||||||
}
|
|
||||||
fileTaskMapper.updateById(task);
|
|
||||||
log.warn("[product-risk] submitResult status evaluated taskId={} oldStatus={} newStatus={} ok={} fail={} allDone={} matchedShopCount={} skippedUnmatchedCount={} waitingCount={} assembledCount={} fallbackMatchedCount={} batchErrors={}",
|
|
||||||
taskId, oldStatus, task.getStatus(), ok, fail, allDone, matchedShopCount, skippedUnmatchedCount,
|
|
||||||
waitingCount, assembledCount, fallbackMatchedCount, batchErrors);
|
waitingCount, assembledCount, fallbackMatchedCount, batchErrors);
|
||||||
cleanupTaskCacheIfTerminal(taskId, task.getStatus());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean tryFinalizeTask(Long taskId, boolean fromCompensation) {
|
public boolean tryFinalizeTask(Long taskId, boolean fromCompensation) {
|
||||||
@@ -855,6 +837,14 @@ public class ProductRiskTaskService {
|
|||||||
}
|
}
|
||||||
File workRoot = FileUtil.mkdir(FileUtil.file(System.getProperty("java.io.tmpdir"), "product-risk-result", String.valueOf(job.getTaskId())));
|
File workRoot = FileUtil.mkdir(FileUtil.file(System.getProperty("java.io.tmpdir"), "product-risk-result", String.valueOf(job.getTaskId())));
|
||||||
assembleShopResult(result, job.getScopeKey(), payload, workRoot);
|
assembleShopResult(result, job.getScopeKey(), payload, workRoot);
|
||||||
|
FileTaskEntity task = loadTaskForExecution(job.getTaskId());
|
||||||
|
if (task != null && MODULE_TYPE.equals(task.getModuleType())) {
|
||||||
|
List<FileResultEntity> latest = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
||||||
|
.eq(FileResultEntity::getTaskId, job.getTaskId())
|
||||||
|
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
||||||
|
.orderByAsc(FileResultEntity::getId));
|
||||||
|
updateTaskStatusFromLatestRows(task, latest, List.of());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void enqueueResultFileAssembly(FileResultEntity result, String shopKey, ProductRiskShopPayloadDto payload) {
|
private void enqueueResultFileAssembly(FileResultEntity result, String shopKey, ProductRiskShopPayloadDto payload) {
|
||||||
@@ -888,10 +878,20 @@ public class ProductRiskTaskService {
|
|||||||
int fail = 0;
|
int fail = 0;
|
||||||
List<String> allErrors = new ArrayList<>();
|
List<String> allErrors = new ArrayList<>();
|
||||||
boolean allDone = true;
|
boolean allDone = true;
|
||||||
|
Map<Long, TaskFileJobEntity> jobMap = taskFileJobService.findAssembleJobsByResultIds(
|
||||||
|
MODULE_TYPE,
|
||||||
|
latest.stream()
|
||||||
|
.map(FileResultEntity::getId)
|
||||||
|
.filter(id -> id != null && id > 0)
|
||||||
|
.toList());
|
||||||
for (FileResultEntity fr : latest) {
|
for (FileResultEntity fr : latest) {
|
||||||
boolean success = fr.getSuccess() != null && fr.getSuccess() == 1;
|
boolean success = fr.getSuccess() != null && fr.getSuccess() == 1;
|
||||||
boolean failed = fr.getErrorMessage() != null && !fr.getErrorMessage().isBlank();
|
boolean failed = fr.getErrorMessage() != null && !fr.getErrorMessage().isBlank();
|
||||||
if (success) {
|
if (success) {
|
||||||
|
if (isResultAwaitingFileAssembly(fr, jobMap.get(fr.getId()))) {
|
||||||
|
allDone = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
ok++;
|
ok++;
|
||||||
} else if (failed) {
|
} else if (failed) {
|
||||||
fail++;
|
fail++;
|
||||||
@@ -947,6 +947,34 @@ public class ProductRiskTaskService {
|
|||||||
productRiskTaskCacheService.deleteTaskCache(taskId);
|
productRiskTaskCacheService.deleteTaskCache(taskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isResultAwaitingFileAssembly(FileResultEntity result, TaskFileJobEntity job) {
|
||||||
|
if (result == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (result.getResultFileUrl() != null && !result.getResultFileUrl().isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (result.getResultFilename() == null || result.getResultFilename().isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return job == null || !"SUCCESS".equals(job.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
private TaskDistributedLockService.LockHandle acquireTaskLockOrThrow(Long taskId) {
|
||||||
|
TaskDistributedLockService.LockHandle lockHandle = acquireTaskLock(taskId);
|
||||||
|
if (lockHandle == null) {
|
||||||
|
throw new BusinessException(40901, "浠诲姟姝e湪澶勭悊涓紝璇风◢鍚庡啀璇?");
|
||||||
|
}
|
||||||
|
return lockHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TaskDistributedLockService.LockHandle acquireTaskLock(Long taskId) {
|
||||||
|
if (taskId == null || taskId <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return taskDistributedLockService.acquire(MODULE_TYPE, taskId);
|
||||||
|
}
|
||||||
|
|
||||||
private ProductRiskTaskDetailVo buildTaskDetail(FileTaskEntity task) {
|
private ProductRiskTaskDetailVo buildTaskDetail(FileTaskEntity task) {
|
||||||
ProductRiskTaskDetailVo detail = new ProductRiskTaskDetailVo();
|
ProductRiskTaskDetailVo detail = new ProductRiskTaskDetailVo();
|
||||||
detail.setTask(toTaskItemVo(task));
|
detail.setTask(toTaskItemVo(task));
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import com.nanri.aiimage.modules.task.mapper.FileTaskMapper;
|
|||||||
import com.nanri.aiimage.modules.task.model.entity.FileResultEntity;
|
import com.nanri.aiimage.modules.task.model.entity.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.service.TaskDistributedLockService;
|
||||||
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.TaskProgressSnapshotService;
|
||||||
import com.nanri.aiimage.modules.task.service.TaskResultItemService;
|
import com.nanri.aiimage.modules.task.service.TaskResultItemService;
|
||||||
@@ -66,6 +67,7 @@ public class QueryAsinTaskService {
|
|||||||
private final TaskFileJobService taskFileJobService;
|
private final TaskFileJobService taskFileJobService;
|
||||||
private final TaskResultItemService taskResultItemService;
|
private final TaskResultItemService taskResultItemService;
|
||||||
private final TaskProgressSnapshotService taskProgressSnapshotService;
|
private final TaskProgressSnapshotService taskProgressSnapshotService;
|
||||||
|
private final TaskDistributedLockService taskDistributedLockService;
|
||||||
|
|
||||||
private FileTaskEntity loadTaskForExecution(Long taskId) {
|
private FileTaskEntity loadTaskForExecution(Long taskId) {
|
||||||
Map<Long, FileTaskEntity> cachedTasks = taskCacheService.getTaskCacheBatch(List.of(taskId));
|
Map<Long, FileTaskEntity> cachedTasks = taskCacheService.getTaskCacheBatch(List.of(taskId));
|
||||||
@@ -468,8 +470,18 @@ public class QueryAsinTaskService {
|
|||||||
throw new BusinessException("记录不存在");
|
throw new BusinessException("记录不存在");
|
||||||
}
|
}
|
||||||
Long taskId = entity.getTaskId();
|
Long taskId = entity.getTaskId();
|
||||||
fileResultMapper.deleteById(resultId);
|
if (taskId == null || taskId <= 0) {
|
||||||
reconcileTaskAfterResultRemoval(taskId);
|
fileResultMapper.deleteById(resultId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try (TaskDistributedLockService.LockHandle ignored = acquireTaskLockOrThrow(taskId)) {
|
||||||
|
FileResultEntity latestEntity = fileResultMapper.selectById(resultId);
|
||||||
|
if (latestEntity == null || !MODULE_TYPE.equals(latestEntity.getModuleType()) || !userId.equals(latestEntity.getUserId())) {
|
||||||
|
throw new BusinessException("record not found");
|
||||||
|
}
|
||||||
|
fileResultMapper.deleteById(resultId);
|
||||||
|
reconcileTaskAfterResultRemoval(taskId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void reconcileTaskAfterResultRemoval(Long taskId) {
|
private void reconcileTaskAfterResultRemoval(Long taskId) {
|
||||||
@@ -719,6 +731,9 @@ public class QueryAsinTaskService {
|
|||||||
long successCount = rows.stream().filter(row -> Integer.valueOf(RESULT_SUCCESS).equals(row.getSuccess())).count();
|
long successCount = rows.stream().filter(row -> Integer.valueOf(RESULT_SUCCESS).equals(row.getSuccess())).count();
|
||||||
long failedCount = rows.stream().filter(row -> Integer.valueOf(RESULT_FAILED).equals(row.getSuccess())).count();
|
long failedCount = rows.stream().filter(row -> Integer.valueOf(RESULT_FAILED).equals(row.getSuccess())).count();
|
||||||
long pendingCount = rows.stream().filter(row -> !isResultFinished(row)).count();
|
long pendingCount = rows.stream().filter(row -> !isResultFinished(row)).count();
|
||||||
|
if (pendingCount == 0 && successCount > 0 && isTaskWorkbookPending(task, rows)) {
|
||||||
|
pendingCount = 1;
|
||||||
|
}
|
||||||
task.setSuccessFileCount((int) successCount);
|
task.setSuccessFileCount((int) successCount);
|
||||||
task.setFailedFileCount((int) failedCount);
|
task.setFailedFileCount((int) failedCount);
|
||||||
task.setUpdatedAt(LocalDateTime.now());
|
task.setUpdatedAt(LocalDateTime.now());
|
||||||
@@ -948,11 +963,47 @@ public class QueryAsinTaskService {
|
|||||||
fileResultMapper.updateById(row);
|
fileResultMapper.updateById(row);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
updateTaskStatusFromRows(task, rows);
|
||||||
|
persistSnapshotJson(task, buildSnapshotFromDb(task, rows));
|
||||||
|
fileTaskMapper.updateById(task);
|
||||||
} finally {
|
} finally {
|
||||||
FileUtil.del(xlsx);
|
FileUtil.del(xlsx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isTaskWorkbookPending(FileTaskEntity task, List<FileResultEntity> rows) {
|
||||||
|
if (task == null || task.getId() == null || rows == null || rows.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
FileResultEntity firstSuccess = rows.stream()
|
||||||
|
.filter(row -> Integer.valueOf(RESULT_SUCCESS).equals(row.getSuccess()))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
if (firstSuccess == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!blank(firstSuccess.getResultFileUrl())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
TaskFileJobEntity job = taskFileJobService.findAssembleJob(task.getId(), MODULE_TYPE, firstSuccess.getId());
|
||||||
|
return job != null && !"SUCCESS".equals(job.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
private TaskDistributedLockService.LockHandle acquireTaskLockOrThrow(Long taskId) {
|
||||||
|
TaskDistributedLockService.LockHandle lockHandle = acquireTaskLock(taskId);
|
||||||
|
if (lockHandle == null) {
|
||||||
|
throw new BusinessException(40901, "浠诲姟姝e湪澶勭悊涓紝璇风◢鍚庡啀璇?");
|
||||||
|
}
|
||||||
|
return lockHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TaskDistributedLockService.LockHandle acquireTaskLock(Long taskId) {
|
||||||
|
if (taskId == null || taskId <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return taskDistributedLockService.acquire(MODULE_TYPE, taskId);
|
||||||
|
}
|
||||||
|
|
||||||
private void markResultSuccess(FileResultEntity row) {
|
private void markResultSuccess(FileResultEntity row) {
|
||||||
row.setSuccess(RESULT_SUCCESS);
|
row.setSuccess(RESULT_SUCCESS);
|
||||||
row.setErrorMessage(null);
|
row.setErrorMessage(null);
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import com.nanri.aiimage.modules.task.mapper.FileTaskMapper;
|
|||||||
import com.nanri.aiimage.modules.task.model.entity.FileResultEntity;
|
import com.nanri.aiimage.modules.task.model.entity.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.service.TaskDistributedLockService;
|
||||||
import com.nanri.aiimage.modules.task.service.TaskFileJobService;
|
import com.nanri.aiimage.modules.task.service.TaskFileJobService;
|
||||||
import com.nanri.aiimage.modules.task.service.TaskResultPayloadService;
|
import com.nanri.aiimage.modules.task.service.TaskResultPayloadService;
|
||||||
import com.nanri.aiimage.modules.ziniao.service.ZiniaoShopSwitchService;
|
import com.nanri.aiimage.modules.ziniao.service.ZiniaoShopSwitchService;
|
||||||
@@ -67,6 +68,7 @@ public class ShopMatchTaskService {
|
|||||||
private final TaskPressureProperties taskPressureProperties;
|
private final TaskPressureProperties taskPressureProperties;
|
||||||
private final TaskResultPayloadService taskResultPayloadService;
|
private final TaskResultPayloadService taskResultPayloadService;
|
||||||
private final TaskFileJobService taskFileJobService;
|
private final TaskFileJobService taskFileJobService;
|
||||||
|
private final TaskDistributedLockService taskDistributedLockService;
|
||||||
|
|
||||||
private FileTaskEntity loadTaskForExecution(Long taskId) {
|
private FileTaskEntity loadTaskForExecution(Long taskId) {
|
||||||
Map<Long, FileTaskEntity> cachedTasks = shopMatchTaskCacheService.getTaskCacheBatch(List.of(taskId));
|
Map<Long, FileTaskEntity> cachedTasks = shopMatchTaskCacheService.getTaskCacheBatch(List.of(taskId));
|
||||||
@@ -250,8 +252,16 @@ public class ShopMatchTaskService {
|
|||||||
throw new BusinessException("记录不存在");
|
throw new BusinessException("记录不存在");
|
||||||
}
|
}
|
||||||
Long taskId = entity.getTaskId();
|
Long taskId = entity.getTaskId();
|
||||||
fileResultMapper.deleteById(resultId);
|
if (taskId == null || taskId <= 0) {
|
||||||
if (taskId != null && taskId > 0) {
|
fileResultMapper.deleteById(resultId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try (TaskDistributedLockService.LockHandle ignored = acquireTaskLockOrThrow(taskId)) {
|
||||||
|
FileResultEntity latestEntity = fileResultMapper.selectById(resultId);
|
||||||
|
if (latestEntity == null || !MODULE_TYPE.equals(latestEntity.getModuleType()) || !userId.equals(latestEntity.getUserId())) {
|
||||||
|
throw new BusinessException("record not found");
|
||||||
|
}
|
||||||
|
fileResultMapper.deleteById(resultId);
|
||||||
reconcileTaskAfterResultRemoval(taskId);
|
reconcileTaskAfterResultRemoval(taskId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -727,6 +737,14 @@ public class ShopMatchTaskService {
|
|||||||
}
|
}
|
||||||
File workRoot = FileUtil.mkdir(FileUtil.file(System.getProperty("java.io.tmpdir"), "shop-match-result", String.valueOf(job.getTaskId())));
|
File workRoot = FileUtil.mkdir(FileUtil.file(System.getProperty("java.io.tmpdir"), "shop-match-result", String.valueOf(job.getTaskId())));
|
||||||
assembleShopResult(result, job.getScopeKey(), payload, workRoot);
|
assembleShopResult(result, job.getScopeKey(), payload, workRoot);
|
||||||
|
FileTaskEntity task = loadTaskForExecution(job.getTaskId());
|
||||||
|
if (task != null && MODULE_TYPE.equals(task.getModuleType())) {
|
||||||
|
List<FileResultEntity> latest = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
||||||
|
.eq(FileResultEntity::getTaskId, job.getTaskId())
|
||||||
|
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
||||||
|
.orderByAsc(FileResultEntity::getId));
|
||||||
|
updateTaskStatusFromLatestRows(task, latest, List.of());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void enqueueResultFileAssembly(FileResultEntity result, String shopKey, ShopMatchShopPayloadDto payload) {
|
private void enqueueResultFileAssembly(FileResultEntity result, String shopKey, ShopMatchShopPayloadDto payload) {
|
||||||
@@ -771,10 +789,20 @@ public class ShopMatchTaskService {
|
|||||||
int fail = 0;
|
int fail = 0;
|
||||||
List<String> errors = new ArrayList<>();
|
List<String> errors = new ArrayList<>();
|
||||||
boolean allDone = true;
|
boolean allDone = true;
|
||||||
|
Map<Long, TaskFileJobEntity> jobMap = taskFileJobService.findAssembleJobsByResultIds(
|
||||||
|
MODULE_TYPE,
|
||||||
|
latest.stream()
|
||||||
|
.map(FileResultEntity::getId)
|
||||||
|
.filter(id -> id != null && id > 0)
|
||||||
|
.toList());
|
||||||
for (FileResultEntity row : latest) {
|
for (FileResultEntity row : latest) {
|
||||||
boolean success = row.getSuccess() != null && row.getSuccess() == 1;
|
boolean success = row.getSuccess() != null && row.getSuccess() == 1;
|
||||||
boolean failed = row.getErrorMessage() != null && !row.getErrorMessage().isBlank();
|
boolean failed = row.getErrorMessage() != null && !row.getErrorMessage().isBlank();
|
||||||
if (success) {
|
if (success) {
|
||||||
|
if (isResultAwaitingFileAssembly(row, jobMap.get(row.getId()))) {
|
||||||
|
allDone = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
ok++;
|
ok++;
|
||||||
} else if (failed) {
|
} else if (failed) {
|
||||||
fail++;
|
fail++;
|
||||||
@@ -818,6 +846,34 @@ public class ShopMatchTaskService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isResultAwaitingFileAssembly(FileResultEntity result, TaskFileJobEntity job) {
|
||||||
|
if (result == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (result.getResultFileUrl() != null && !result.getResultFileUrl().isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (result.getResultFilename() == null || result.getResultFilename().isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return job == null || !"SUCCESS".equals(job.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
private TaskDistributedLockService.LockHandle acquireTaskLockOrThrow(Long taskId) {
|
||||||
|
TaskDistributedLockService.LockHandle lockHandle = acquireTaskLock(taskId);
|
||||||
|
if (lockHandle == null) {
|
||||||
|
throw new BusinessException(40901, "浠诲姟姝e湪澶勭悊涓紝璇风◢鍚庡啀璇?");
|
||||||
|
}
|
||||||
|
return lockHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TaskDistributedLockService.LockHandle acquireTaskLock(Long taskId) {
|
||||||
|
if (taskId == null || taskId <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return taskDistributedLockService.acquire(MODULE_TYPE, taskId);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean shouldRemainRunningUntilNextStage(FileTaskEntity task) {
|
private boolean shouldRemainRunningUntilNextStage(FileTaskEntity task) {
|
||||||
ShopMatchCreateTaskRequest request = parseTaskRequestSilently(task);
|
ShopMatchCreateTaskRequest request = parseTaskRequestSilently(task);
|
||||||
if (request == null || request.getScheduleTimes() == null || request.getScheduleTimes().isEmpty()) {
|
if (request == null || request.getScheduleTimes() == null || request.getScheduleTimes().isEmpty()) {
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ package com.nanri.aiimage.modules.similarasin.controller;
|
|||||||
|
|
||||||
import com.nanri.aiimage.common.api.ApiResponse;
|
import com.nanri.aiimage.common.api.ApiResponse;
|
||||||
import com.nanri.aiimage.common.util.DownloadHeaderUtil;
|
import com.nanri.aiimage.common.util.DownloadHeaderUtil;
|
||||||
|
import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinFilterConditionAddRequest;
|
||||||
import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinParseRequest;
|
import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinParseRequest;
|
||||||
|
import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinParsedPayloadDto;
|
||||||
import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinSubmitResultRequest;
|
import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinSubmitResultRequest;
|
||||||
import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinTaskBatchRequest;
|
import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinTaskBatchRequest;
|
||||||
import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinDashboardVo;
|
import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinDashboardVo;
|
||||||
|
import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinFilterConditionVo;
|
||||||
import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinHistoryVo;
|
import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinHistoryVo;
|
||||||
import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinParseVo;
|
import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinParseVo;
|
||||||
import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinTaskBatchVo;
|
import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinTaskBatchVo;
|
||||||
@@ -29,6 +32,7 @@ import org.springframework.web.server.ResponseStatusException;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -38,12 +42,48 @@ public class SimilarAsinController {
|
|||||||
|
|
||||||
private final SimilarAsinTaskService service;
|
private final SimilarAsinTaskService service;
|
||||||
|
|
||||||
|
@GetMapping("/filter-conditions")
|
||||||
|
@Operation(summary = "查询货源查询筛选条件")
|
||||||
|
public ApiResponse<List<SimilarAsinFilterConditionVo>> listFilterConditions(
|
||||||
|
@Parameter(description = "当前用户 ID", required = true, example = "1")
|
||||||
|
@RequestParam("user_id") Long userId) {
|
||||||
|
return ApiResponse.success(service.listFilterConditions(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/filter-conditions")
|
||||||
|
@Operation(summary = "保存货源查询筛选条件")
|
||||||
|
public ApiResponse<SimilarAsinFilterConditionVo> addFilterCondition(
|
||||||
|
@Valid @RequestBody SimilarAsinFilterConditionAddRequest request) {
|
||||||
|
return ApiResponse.success(service.addFilterCondition(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/filter-conditions/{id}")
|
||||||
|
@Operation(summary = "删除货源查询筛选条件")
|
||||||
|
public ApiResponse<Void> deleteFilterCondition(
|
||||||
|
@Parameter(description = "筛选条件主键", example = "10")
|
||||||
|
@PathVariable Long id,
|
||||||
|
@Parameter(description = "当前用户 ID", required = true, example = "1")
|
||||||
|
@RequestParam("user_id") Long userId) {
|
||||||
|
service.deleteFilterCondition(userId, id);
|
||||||
|
return ApiResponse.success(null);
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/parse")
|
@PostMapping("/parse")
|
||||||
@Operation(summary = "解析 Excel 并创建任务", description = "解析上传后的 Excel 文件,提取 id、ASIN、国家、URL、标题等字段。返回给前端的数据只包含整数 id 和 n_1 行;n_2、n_3 等子行会保存在 OSS 解析载荷中,用于最终结果补齐。创建后的任务状态为 PENDING,不会自动推送 Python。")
|
@Operation(summary = "解析 Excel 并创建任务", description = "解析上传后的 Excel 文件,提取 id、ASIN、国家、URL、标题等字段。返回给前端的数据只包含整数 id 和 n_1 行;n_2、n_3 等子行会保存在 OSS 解析载荷中,用于最终结果补齐。创建后的任务状态为 PENDING,不会自动推送 Python。")
|
||||||
public ApiResponse<SimilarAsinParseVo> parse(@Valid @RequestBody SimilarAsinParseRequest request) {
|
public ApiResponse<SimilarAsinParseVo> parse(@Valid @RequestBody SimilarAsinParseRequest request) {
|
||||||
return ApiResponse.success(service.parseAndCreateTask(request));
|
return ApiResponse.success(service.parseAndCreateTask(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/tasks/{taskId}/parsed-payload")
|
||||||
|
@Operation(summary = "获取货源查询完整解析载荷", description = "给 Python 队列消费端使用。parse 接口只返回轻量预览,大文件完整行数据通过该接口按 taskId 拉取。")
|
||||||
|
public ApiResponse<SimilarAsinParsedPayloadDto> parsedPayload(
|
||||||
|
@Parameter(description = "货源查询任务 ID", required = true, example = "7004")
|
||||||
|
@PathVariable Long taskId,
|
||||||
|
@Parameter(description = "当前用户 ID", required = true, example = "1")
|
||||||
|
@RequestParam("user_id") Long userId) {
|
||||||
|
return ApiResponse.success(service.parsedPayload(taskId, userId));
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/dashboard")
|
@GetMapping("/dashboard")
|
||||||
@Operation(summary = "查询相似ASIN检测总览", description = "查询当前用户的运行中、成功、失败和已结束任务数量。页面进入时请求一次即可,不需要持续轮询。")
|
@Operation(summary = "查询相似ASIN检测总览", description = "查询当前用户的运行中、成功、失败和已结束任务数量。页面进入时请求一次即可,不需要持续轮询。")
|
||||||
public ApiResponse<SimilarAsinDashboardVo> dashboard(
|
public ApiResponse<SimilarAsinDashboardVo> dashboard(
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.nanri.aiimage.modules.similarasin.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.nanri.aiimage.modules.similarasin.model.entity.SimilarAsinFilterConditionEntity;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface SimilarAsinFilterConditionMapper extends BaseMapper<SimilarAsinFilterConditionEntity> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.nanri.aiimage.modules.similarasin.model.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||||
|
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 SimilarAsinFilterConditionAddRequest {
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@JsonProperty("user_id")
|
||||||
|
@Schema(description = "当前用户 ID", example = "1", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
@JsonProperty("condition_text")
|
||||||
|
@JsonAlias({"conditionText", "filter_condition", "filterCondition"})
|
||||||
|
@Schema(description = "筛选条件文本", example = "只筛选有货且不带品牌 logo 的商品", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String conditionText;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.nanri.aiimage.modules.similarasin.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_similar_asin_filter_condition")
|
||||||
|
public class SimilarAsinFilterConditionEntity {
|
||||||
|
|
||||||
|
@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.similarasin.model.vo;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class SimilarAsinFilterConditionVo {
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@JsonProperty("conditionText")
|
||||||
|
private String conditionText;
|
||||||
|
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@@ -11,14 +11,18 @@ import com.nanri.aiimage.common.service.DistributedJobLockService;
|
|||||||
import com.nanri.aiimage.config.InstanceMetadata;
|
import com.nanri.aiimage.config.InstanceMetadata;
|
||||||
import com.nanri.aiimage.config.SimilarAsinProperties;
|
import com.nanri.aiimage.config.SimilarAsinProperties;
|
||||||
import com.nanri.aiimage.config.StorageProperties;
|
import com.nanri.aiimage.config.StorageProperties;
|
||||||
|
import com.nanri.aiimage.modules.similarasin.mapper.SimilarAsinFilterConditionMapper;
|
||||||
import com.nanri.aiimage.modules.similarasin.client.SimilarAsinCozeClient;
|
import com.nanri.aiimage.modules.similarasin.client.SimilarAsinCozeClient;
|
||||||
|
import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinFilterConditionAddRequest;
|
||||||
import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinParseRequest;
|
import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinParseRequest;
|
||||||
import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinParsedPayloadDto;
|
import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinParsedPayloadDto;
|
||||||
import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinResultGroupDto;
|
import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinResultGroupDto;
|
||||||
import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinResultRowDto;
|
import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinResultRowDto;
|
||||||
import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinSourceFileDto;
|
import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinSourceFileDto;
|
||||||
import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinSubmitResultRequest;
|
import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinSubmitResultRequest;
|
||||||
|
import com.nanri.aiimage.modules.similarasin.model.entity.SimilarAsinFilterConditionEntity;
|
||||||
import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinDashboardVo;
|
import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinDashboardVo;
|
||||||
|
import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinFilterConditionVo;
|
||||||
import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinHistoryItemVo;
|
import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinHistoryItemVo;
|
||||||
import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinHistoryVo;
|
import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinHistoryVo;
|
||||||
import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinParsedGroupVo;
|
import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinParsedGroupVo;
|
||||||
@@ -102,6 +106,7 @@ public class SimilarAsinTaskService {
|
|||||||
private static final long RESULT_ROWS_READ_RETRY_DELAY_MS = 500L;
|
private static final long RESULT_ROWS_READ_RETRY_DELAY_MS = 500L;
|
||||||
private static final Duration TASK_LOCK_TTL = Duration.ofMinutes(5);
|
private static final Duration TASK_LOCK_TTL = Duration.ofMinutes(5);
|
||||||
private static final long TASK_LOCK_WAIT_MILLIS = 10000L;
|
private static final long TASK_LOCK_WAIT_MILLIS = 10000L;
|
||||||
|
private static final int PARSE_RESPONSE_PREVIEW_LIMIT = 100;
|
||||||
private static final List<String> RESULT_HEADERS = List.of(
|
private static final List<String> RESULT_HEADERS = List.of(
|
||||||
"id",
|
"id",
|
||||||
"asin",
|
"asin",
|
||||||
@@ -118,6 +123,7 @@ public class SimilarAsinTaskService {
|
|||||||
private final FileResultMapper fileResultMapper;
|
private final FileResultMapper fileResultMapper;
|
||||||
private final TaskScopeStateMapper taskScopeStateMapper;
|
private final TaskScopeStateMapper taskScopeStateMapper;
|
||||||
private final TaskChunkMapper taskChunkMapper;
|
private final TaskChunkMapper taskChunkMapper;
|
||||||
|
private final SimilarAsinFilterConditionMapper filterConditionMapper;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final SimilarAsinCozeClient cozeClient;
|
private final SimilarAsinCozeClient cozeClient;
|
||||||
private final SimilarAsinTaskCacheService taskCacheService;
|
private final SimilarAsinTaskCacheService taskCacheService;
|
||||||
@@ -133,6 +139,55 @@ public class SimilarAsinTaskService {
|
|||||||
@Qualifier("cozeTaskExecutor")
|
@Qualifier("cozeTaskExecutor")
|
||||||
private TaskExecutor cozeTaskExecutor;
|
private TaskExecutor cozeTaskExecutor;
|
||||||
|
|
||||||
|
public List<SimilarAsinFilterConditionVo> listFilterConditions(Long userId) {
|
||||||
|
validateUserId(userId);
|
||||||
|
List<SimilarAsinFilterConditionEntity> rows = filterConditionMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<SimilarAsinFilterConditionEntity>()
|
||||||
|
.eq(SimilarAsinFilterConditionEntity::getUserId, userId)
|
||||||
|
.orderByDesc(SimilarAsinFilterConditionEntity::getId));
|
||||||
|
List<SimilarAsinFilterConditionVo> list = new ArrayList<>();
|
||||||
|
for (SimilarAsinFilterConditionEntity row : rows) {
|
||||||
|
list.add(toFilterConditionVo(row));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public SimilarAsinFilterConditionVo addFilterCondition(SimilarAsinFilterConditionAddRequest request) {
|
||||||
|
validateUserId(request.getUserId());
|
||||||
|
String text = normalize(request.getConditionText());
|
||||||
|
if (text.isBlank()) {
|
||||||
|
throw new BusinessException("筛选条件不能为空");
|
||||||
|
}
|
||||||
|
SimilarAsinFilterConditionEntity existing = filterConditionMapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<SimilarAsinFilterConditionEntity>()
|
||||||
|
.eq(SimilarAsinFilterConditionEntity::getUserId, request.getUserId())
|
||||||
|
.eq(SimilarAsinFilterConditionEntity::getConditionText, text)
|
||||||
|
.last("limit 1"));
|
||||||
|
if (existing != null) {
|
||||||
|
return toFilterConditionVo(existing);
|
||||||
|
}
|
||||||
|
SimilarAsinFilterConditionEntity entity = new SimilarAsinFilterConditionEntity();
|
||||||
|
entity.setUserId(request.getUserId());
|
||||||
|
entity.setConditionText(text);
|
||||||
|
entity.setCreatedAt(LocalDateTime.now());
|
||||||
|
filterConditionMapper.insert(entity);
|
||||||
|
return toFilterConditionVo(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void deleteFilterCondition(Long userId, Long id) {
|
||||||
|
validateUserId(userId);
|
||||||
|
if (id == null || id <= 0) {
|
||||||
|
throw new BusinessException("id 不合法");
|
||||||
|
}
|
||||||
|
SimilarAsinFilterConditionEntity row = filterConditionMapper.selectById(id);
|
||||||
|
if (row == null || !userId.equals(row.getUserId())) {
|
||||||
|
throw new BusinessException("记录不存在");
|
||||||
|
}
|
||||||
|
filterConditionMapper.deleteById(id);
|
||||||
|
}
|
||||||
|
|
||||||
public SimilarAsinParseVo parseAndCreateTask(SimilarAsinParseRequest request) {
|
public SimilarAsinParseVo parseAndCreateTask(SimilarAsinParseRequest request) {
|
||||||
long startedAt = System.nanoTime();
|
long startedAt = System.nanoTime();
|
||||||
if (request.getUserId() == null || request.getUserId() <= 0) {
|
if (request.getUserId() == null || request.getUserId() <= 0) {
|
||||||
@@ -240,8 +295,8 @@ public class SimilarAsinTaskService {
|
|||||||
vo.setDroppedRows(droppedRows);
|
vo.setDroppedRows(droppedRows);
|
||||||
vo.setGroupCount(groups.size());
|
vo.setGroupCount(groups.size());
|
||||||
vo.setAiPrompt(normalize(request.getAiPrompt()));
|
vo.setAiPrompt(normalize(request.getAiPrompt()));
|
||||||
vo.setItems(allRows);
|
vo.setItems(buildResponsePreviewRows(allRows));
|
||||||
vo.setGroups(groups);
|
vo.setGroups(List.of());
|
||||||
long finishedAt = System.nanoTime();
|
long finishedAt = System.nanoTime();
|
||||||
log.info("[similar-asin] parse timing taskId={} files={} rows={} groups={} totalMs={} parseMs={} groupMs={} taskInsertMs={} payloadJsonMs={} payloadStoreMs={} persistMs={} responseMs={}",
|
log.info("[similar-asin] parse timing taskId={} files={} rows={} groups={} totalMs={} parseMs={} groupMs={} taskInsertMs={} payloadJsonMs={} payloadStoreMs={} persistMs={} responseMs={}",
|
||||||
task.getId(),
|
task.getId(),
|
||||||
@@ -922,6 +977,36 @@ public class SimilarAsinTaskService {
|
|||||||
return groups;
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<SimilarAsinParsedRowVo> buildResponsePreviewRows(List<SimilarAsinParsedRowVo> rows) {
|
||||||
|
if (rows == null || rows.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
int limit = Math.min(PARSE_RESPONSE_PREVIEW_LIMIT, rows.size());
|
||||||
|
List<SimilarAsinParsedRowVo> preview = new ArrayList<>(limit);
|
||||||
|
for (int i = 0; i < limit; i++) {
|
||||||
|
preview.add(copyPreviewRow(rows.get(i)));
|
||||||
|
}
|
||||||
|
return preview;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SimilarAsinParsedRowVo copyPreviewRow(SimilarAsinParsedRowVo row) {
|
||||||
|
SimilarAsinParsedRowVo vo = new SimilarAsinParsedRowVo();
|
||||||
|
vo.setSourceFileKey(row.getSourceFileKey());
|
||||||
|
vo.setSourceFilename(row.getSourceFilename());
|
||||||
|
vo.setRowIndex(row.getRowIndex());
|
||||||
|
vo.setSourceId(row.getSourceId());
|
||||||
|
vo.setDisplayId(row.getDisplayId());
|
||||||
|
vo.setRowToken(row.getRowToken());
|
||||||
|
vo.setGroupKey(row.getGroupKey());
|
||||||
|
vo.setAsin(row.getAsin());
|
||||||
|
vo.setCountry(row.getCountry());
|
||||||
|
vo.setPrice(row.getPrice());
|
||||||
|
vo.setUrl(row.getUrl());
|
||||||
|
vo.setTitle(row.getTitle());
|
||||||
|
vo.setValues(new LinkedHashMap<>());
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
private Map<String, List<SimilarAsinParsedRowVo>> groupRowsByBaseId(List<SimilarAsinParsedRowVo> rows) {
|
private Map<String, List<SimilarAsinParsedRowVo>> groupRowsByBaseId(List<SimilarAsinParsedRowVo> rows) {
|
||||||
Map<String, List<SimilarAsinParsedRowVo>> result = new LinkedHashMap<>();
|
Map<String, List<SimilarAsinParsedRowVo>> result = new LinkedHashMap<>();
|
||||||
if (rows == null) {
|
if (rows == null) {
|
||||||
@@ -2228,6 +2313,28 @@ public class SimilarAsinTaskService {
|
|||||||
return preferred == null || preferred.isBlank() ? fallback : preferred.trim();
|
return preferred == null || preferred.isBlank() ? fallback : preferred.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void validateUserId(Long userId) {
|
||||||
|
if (userId == null || userId <= 0) {
|
||||||
|
throw new BusinessException("user_id 不合法");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SimilarAsinFilterConditionVo toFilterConditionVo(SimilarAsinFilterConditionEntity row) {
|
||||||
|
SimilarAsinFilterConditionVo vo = new SimilarAsinFilterConditionVo();
|
||||||
|
vo.setId(row.getId());
|
||||||
|
vo.setConditionText(row.getConditionText());
|
||||||
|
vo.setCreatedAt(row.getCreatedAt());
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SimilarAsinParsedPayloadDto parsedPayload(Long taskId, Long userId) {
|
||||||
|
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
||||||
|
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || userId != null && !Objects.equals(userId, task.getUserId())) {
|
||||||
|
throw new BusinessException("任务不存在");
|
||||||
|
}
|
||||||
|
return readParsedPayload(task);
|
||||||
|
}
|
||||||
|
|
||||||
private String normalize(String val) {
|
private String normalize(String val) {
|
||||||
return val == null ? "" : val.replace(String.valueOf((char) 0xFEFF), "").replace((char) 0x3000, ' ').trim().replaceAll("\\s+", " ");
|
return val == null ? "" : val.replace(String.valueOf((char) 0xFEFF), "").replace((char) 0x3000, ' ').trim().replaceAll("\\s+", " ");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
SET @schema_name = DATABASE();
|
||||||
|
|
||||||
|
SET @idx_file_result_patrol_history_desc_exists := (
|
||||||
|
SELECT COUNT(1)
|
||||||
|
FROM information_schema.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @schema_name
|
||||||
|
AND TABLE_NAME = 'biz_file_result'
|
||||||
|
AND INDEX_NAME = 'idx_file_result_patrol_history_desc'
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @sql_add_file_result_patrol_history_desc_idx := IF(
|
||||||
|
@idx_file_result_patrol_history_desc_exists = 0,
|
||||||
|
'ALTER TABLE biz_file_result ADD INDEX idx_file_result_patrol_history_desc (module_type, user_id, created_at DESC, id DESC)',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE stmt_add_file_result_patrol_history_desc_idx FROM @sql_add_file_result_patrol_history_desc_idx;
|
||||||
|
EXECUTE stmt_add_file_result_patrol_history_desc_idx;
|
||||||
|
DEALLOCATE PREPARE stmt_add_file_result_patrol_history_desc_idx;
|
||||||
|
|
||||||
|
SET @idx_task_file_job_patrol_history_lookup_exists := (
|
||||||
|
SELECT COUNT(1)
|
||||||
|
FROM information_schema.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @schema_name
|
||||||
|
AND TABLE_NAME = 'biz_task_file_job'
|
||||||
|
AND INDEX_NAME = 'idx_task_file_job_patrol_history_lookup'
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @sql_add_task_file_job_patrol_history_lookup_idx := IF(
|
||||||
|
@idx_task_file_job_patrol_history_lookup_exists = 0,
|
||||||
|
'ALTER TABLE biz_task_file_job ADD INDEX idx_task_file_job_patrol_history_lookup (module_type, job_type, result_id, status)',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE stmt_add_task_file_job_patrol_history_lookup_idx FROM @sql_add_task_file_job_patrol_history_lookup_idx;
|
||||||
|
EXECUTE stmt_add_task_file_job_patrol_history_lookup_idx;
|
||||||
|
DEALLOCATE PREPARE stmt_add_task_file_job_patrol_history_lookup_idx;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS biz_similar_asin_filter_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_similar_asin_filter_condition_user_text (user_id, condition_text),
|
||||||
|
KEY idx_similar_asin_filter_condition_user_id (user_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
UPDATE `columns`
|
||||||
|
SET `name` = 'source query',
|
||||||
|
`menu_type` = 'app',
|
||||||
|
`route_path` = 'similar-asin',
|
||||||
|
`sort_order` = 112
|
||||||
|
WHERE `column_key` = 'similar_asin';
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>相似ASIN检测</title>
|
<title>货源查询</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -0,0 +1,337 @@
|
|||||||
|
<template>
|
||||||
|
<div class="secret-settings-entry">
|
||||||
|
<button type="button" class="secret-settings-trigger" @click="dialogVisible = true">
|
||||||
|
<span class="secret-settings-icon" aria-hidden="true">◎</span>
|
||||||
|
<span>密钥设置</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<el-dialog v-model="dialogVisible" width="560px" class="secret-settings-dialog" :append-to-body="true">
|
||||||
|
<template #header>
|
||||||
|
<div class="dialog-header">
|
||||||
|
<div class="dialog-title">设置</div>
|
||||||
|
<div class="dialog-subtitle">按当前登录用户保存在本机,可分别管理外观专利和货源查询密钥。</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="secret-settings-body">
|
||||||
|
<section v-for="item in moduleStates" :key="item.moduleKey" class="secret-card">
|
||||||
|
<div class="secret-card-head">
|
||||||
|
<div>
|
||||||
|
<div class="secret-card-title">{{ item.title }}</div>
|
||||||
|
<div class="secret-card-desc">{{ item.description }}</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="item.exists"
|
||||||
|
type="button"
|
||||||
|
class="link-danger"
|
||||||
|
@click="clearSecret(item.moduleKey)"
|
||||||
|
>
|
||||||
|
清空
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
v-model="item.value"
|
||||||
|
class="secret-input"
|
||||||
|
type="password"
|
||||||
|
:placeholder="`请输入${item.title}密钥`"
|
||||||
|
autocomplete="off"
|
||||||
|
spellcheck="false"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="retention-block">
|
||||||
|
<div class="retention-label">保留时长</div>
|
||||||
|
<div class="retention-options">
|
||||||
|
<label v-for="option in retentionOptions" :key="option.value" class="retention-option">
|
||||||
|
<input v-model="item.retention" type="radio" :value="option.value" />
|
||||||
|
<span>{{ option.label }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="secret-meta">
|
||||||
|
<span v-if="item.exists">{{ formatRetentionText(item.retention, item.expiresAt) }}</span>
|
||||||
|
<span v-else>当前未保存</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<button type="button" class="footer-btn footer-btn-ghost" @click="dialogVisible = false">取消</button>
|
||||||
|
<button type="button" class="footer-btn footer-btn-primary" @click="saveAll">保存</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import {
|
||||||
|
clearStoredApiSecret,
|
||||||
|
getStoredApiSecretSnapshot,
|
||||||
|
saveStoredApiSecret,
|
||||||
|
type ApiSecretModuleKey,
|
||||||
|
type ApiSecretRetention,
|
||||||
|
} from '@/shared/utils/api-secret-store'
|
||||||
|
|
||||||
|
type ModuleState = {
|
||||||
|
moduleKey: ApiSecretModuleKey
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
value: string
|
||||||
|
retention: ApiSecretRetention
|
||||||
|
expiresAt: number | null
|
||||||
|
exists: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
|
||||||
|
const retentionOptions: Array<{ value: ApiSecretRetention; label: string }> = [
|
||||||
|
{ value: 'session', label: '仅本次打开有效' },
|
||||||
|
{ value: '1d', label: '1天' },
|
||||||
|
{ value: '7d', label: '7天' },
|
||||||
|
{ value: '30d', label: '30天' },
|
||||||
|
{ value: 'forever', label: '长期保留' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const moduleStates = ref<ModuleState[]>([])
|
||||||
|
|
||||||
|
function buildModuleState(
|
||||||
|
moduleKey: ApiSecretModuleKey,
|
||||||
|
title: string,
|
||||||
|
description: string,
|
||||||
|
): ModuleState {
|
||||||
|
const snapshot = getStoredApiSecretSnapshot(moduleKey)
|
||||||
|
return {
|
||||||
|
moduleKey,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
value: snapshot.value,
|
||||||
|
retention: snapshot.retention,
|
||||||
|
expiresAt: snapshot.expiresAt,
|
||||||
|
exists: snapshot.exists,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadStates() {
|
||||||
|
moduleStates.value = [
|
||||||
|
buildModuleState('appearance-patent', '外观专利', '用于外观专利检测的 Coze 接口密钥。'),
|
||||||
|
buildModuleState('similar-asin', '货源查询', '用于货源查询的 Coze 接口密钥。'),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRetentionText(retention: ApiSecretRetention, expiresAt: number | null) {
|
||||||
|
if (retention === 'session') return '关闭软件后自动清空'
|
||||||
|
if (retention === 'forever') return '长期保留,直到手动清空'
|
||||||
|
if (!expiresAt) return '已保存'
|
||||||
|
const date = new Date(expiresAt)
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const hour = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minute = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
return `有效期至 ${year}-${month}-${day} ${hour}:${minute}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSecret(moduleKey: ApiSecretModuleKey) {
|
||||||
|
clearStoredApiSecret(moduleKey)
|
||||||
|
loadStates()
|
||||||
|
ElMessage.success('已清空密钥')
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveAll() {
|
||||||
|
for (const item of moduleStates.value) {
|
||||||
|
saveStoredApiSecret(item.moduleKey, item.value, item.retention)
|
||||||
|
}
|
||||||
|
loadStates()
|
||||||
|
dialogVisible.value = false
|
||||||
|
ElMessage.success('密钥设置已保存')
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(dialogVisible, (visible) => {
|
||||||
|
if (visible) {
|
||||||
|
loadStates()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
loadStates()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.secret-settings-entry {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-settings-trigger {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
height: 38px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border: 1px solid #3c4a58;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: linear-gradient(180deg, #2b3239 0%, #242a31 100%);
|
||||||
|
color: #e7edf4;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-settings-trigger:hover {
|
||||||
|
border-color: #4c647d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-settings-icon {
|
||||||
|
color: #8dc4ff;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-title {
|
||||||
|
color: #f5f7fa;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-subtitle {
|
||||||
|
color: #8f9aa7;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-settings-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-card {
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid #2f363f;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #22272d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-card-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-card-title {
|
||||||
|
color: #eef4fb;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-card-desc {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #909ba8;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 42px;
|
||||||
|
padding: 0 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid #3b4652;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #1b2026;
|
||||||
|
color: #dce6f0;
|
||||||
|
font-size: 13px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-input:focus {
|
||||||
|
border-color: #5b96d6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retention-block {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retention-label {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #a3afbb;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retention-options {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retention-option {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #1b2026;
|
||||||
|
color: #dce4ec;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retention-option input {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-meta {
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #7f8a96;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-danger {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #ff9b9b;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-btn {
|
||||||
|
height: 38px;
|
||||||
|
padding: 0 18px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-btn-ghost {
|
||||||
|
border-color: #3a4653;
|
||||||
|
background: #232a31;
|
||||||
|
color: #d9e2eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-btn-primary {
|
||||||
|
background: #5aa2ea;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -24,11 +24,6 @@
|
|||||||
<div class="prompt-preview">{{ defaultAiPrompt }}</div>
|
<div class="prompt-preview">{{ defaultAiPrompt }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="secret-card">
|
|
||||||
<div class="section-title">密钥</div>
|
|
||||||
<input v-model="cozeApiKey" class="secret-input" type="password" autocomplete="off" spellcheck="false" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="run-row">
|
<div class="run-row">
|
||||||
<button type="button" class="btn-run" :disabled="parsing || !uploadedFiles.length" @click="parseFiles">
|
<button type="button" class="btn-run" :disabled="parsing || !uploadedFiles.length" @click="parseFiles">
|
||||||
{{ parsing ? '解析中...' : '解析并创建任务' }}
|
{{ parsing ? '解析中...' : '解析并创建任务' }}
|
||||||
@@ -170,6 +165,7 @@ import {
|
|||||||
import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared/api/brand'
|
import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared/api/brand'
|
||||||
import { getPywebviewApi } from '@/shared/bridges/pywebview'
|
import { getPywebviewApi } from '@/shared/bridges/pywebview'
|
||||||
import { getTaskPollIntervalMs } from '@/shared/task-progress-config'
|
import { getTaskPollIntervalMs } from '@/shared/task-progress-config'
|
||||||
|
import { getStoredApiSecret } from '@/shared/utils/api-secret-store'
|
||||||
import { createCategorizedTimers } from '@/shared/utils/categorized-timers'
|
import { createCategorizedTimers } from '@/shared/utils/categorized-timers'
|
||||||
|
|
||||||
const selectedFileNames = ref<string[]>([])
|
const selectedFileNames = ref<string[]>([])
|
||||||
@@ -185,7 +181,6 @@ const defaultAiPrompt = `请帮我排查以下亚马逊商品在英国及欧洲
|
|||||||
标题维度: 排查标题中是否包含大牌商标、敏感词或版权保护词汇。
|
标题维度: 排查标题中是否包含大牌商标、敏感词或版权保护词汇。
|
||||||
结论:XX`
|
结论:XX`
|
||||||
const aiPrompt = ref('')
|
const aiPrompt = ref('')
|
||||||
const cozeApiKey = ref('')
|
|
||||||
const parsing = ref(false)
|
const parsing = ref(false)
|
||||||
const pushing = ref(false)
|
const pushing = ref(false)
|
||||||
const queuePayloadText = ref('')
|
const queuePayloadText = ref('')
|
||||||
@@ -225,7 +220,7 @@ function effectiveAiPrompt() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function effectiveCozeApiKey() {
|
function effectiveCozeApiKey() {
|
||||||
return cozeApiKey.value.trim()
|
return getStoredApiSecret('appearance-patent').trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
function maskSecret(secret: string) {
|
function maskSecret(secret: string) {
|
||||||
@@ -352,7 +347,7 @@ async function parseFiles() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!effectiveCozeApiKey()) {
|
if (!effectiveCozeApiKey()) {
|
||||||
ElMessage.warning('请先填写密钥')
|
ElMessage.warning('请先在左上角设置中填写外观专利密钥')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
parsing.value = true
|
parsing.value = true
|
||||||
@@ -399,6 +394,10 @@ async function pushToPythonQueue() {
|
|||||||
ElMessage.error('当前环境未启用 pywebview enqueue_json')
|
ElMessage.error('当前环境未启用 pywebview enqueue_json')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!effectiveCozeApiKey()) {
|
||||||
|
ElMessage.warning('请先在左上角设置中填写外观专利密钥')
|
||||||
|
return
|
||||||
|
}
|
||||||
pushing.value = true
|
pushing.value = true
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
@@ -773,6 +772,7 @@ onUnmounted(() => {
|
|||||||
.selected-files { margin-top: 14px; color: #999; font-size: 12px; word-break: break-all; }
|
.selected-files { margin-top: 14px; color: #999; font-size: 12px; word-break: break-all; }
|
||||||
.selected-files span { display: block; margin: 4px 0; }
|
.selected-files span { display: block; margin: 4px 0; }
|
||||||
.prompt-card, .secret-card { margin-bottom: 18px; }
|
.prompt-card, .secret-card { margin-bottom: 18px; }
|
||||||
|
.secret-card { display: none; }
|
||||||
.prompt-input { width: 100%; box-sizing: border-box; resize: vertical; min-height: 180px; padding: 10px 12px; border: 1px solid #333; border-radius: 8px; background: #202020; color: #d8d8d8; font-size: 12px; line-height: 1.6; outline: none; }
|
.prompt-input { width: 100%; box-sizing: border-box; resize: vertical; min-height: 180px; padding: 10px 12px; border: 1px solid #333; border-radius: 8px; background: #202020; color: #d8d8d8; font-size: 12px; line-height: 1.6; outline: none; }
|
||||||
.secret-input { width: 100%; box-sizing: border-box; height: 38px; padding: 0 12px; border: 1px solid #333; border-radius: 8px; background: #202020; color: #d8d8d8; font-size: 12px; outline: none; }
|
.secret-input { width: 100%; box-sizing: border-box; height: 38px; padding: 0 12px; border: 1px solid #333; border-radius: 8px; background: #202020; color: #d8d8d8; font-size: 12px; outline: none; }
|
||||||
.prompt-input:focus, .secret-input:focus { border-color: #3498db; }
|
.prompt-input:focus, .secret-input:focus { border-color: #3498db; }
|
||||||
|
|||||||
@@ -18,15 +18,30 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="prompt-card">
|
<div class="prompt-card">
|
||||||
<div class="section-title">AI 提示词</div>
|
<div class="section-title">筛选条件</div>
|
||||||
<textarea v-model="aiPrompt" class="prompt-input" rows="8" placeholder="可选,留空时使用下方默认提示词" />
|
<textarea v-model="filterConditionInput" class="prompt-input" rows="4" placeholder="请输入筛选条件" />
|
||||||
<div class="prompt-default-label">留空时默认使用以下提示词</div>
|
<div class="condition-actions">
|
||||||
<div class="prompt-preview">{{ defaultAiPrompt }}</div>
|
<button type="button" class="opt-btn" :disabled="savingCondition" @click="saveFilterCondition">
|
||||||
</div>
|
{{ savingCondition ? '保存中...' : '保存' }}
|
||||||
|
</button>
|
||||||
<div class="secret-card">
|
</div>
|
||||||
<div class="section-title">密钥</div>
|
<div class="section-title condition-list-title">备选区</div>
|
||||||
<input v-model="cozeApiKey" class="secret-input" type="password" autocomplete="off" spellcheck="false" />
|
<div v-if="!filterConditions.length" class="empty-conditions">暂无筛选条件,输入后保存即可加入备选区</div>
|
||||||
|
<ul v-else class="condition-list">
|
||||||
|
<li v-for="condition in filterConditions" :key="condition.id" class="condition-item">
|
||||||
|
<label class="condition-check">
|
||||||
|
<input
|
||||||
|
v-model="selectedFilterConditionId"
|
||||||
|
type="radio"
|
||||||
|
name="similar-asin-filter-condition"
|
||||||
|
:value="condition.id"
|
||||||
|
@change="applySelectedFilterCondition(condition)"
|
||||||
|
/>
|
||||||
|
<span :title="condition.conditionText">{{ condition.conditionText }}</span>
|
||||||
|
</label>
|
||||||
|
<button type="button" class="link-danger" @click="removeFilterCondition(condition.id)">删除</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="run-row">
|
<div class="run-row">
|
||||||
@@ -47,7 +62,7 @@
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<section class="right-panel">
|
<section class="right-panel">
|
||||||
<div class="panel-header">相似ASIN检测</div>
|
<div class="panel-header">货源查询</div>
|
||||||
<div class="task-list-wrap">
|
<div class="task-list-wrap">
|
||||||
<div class="clean-result-summary">
|
<div class="clean-result-summary">
|
||||||
<div class="summary-card">
|
<div class="summary-card">
|
||||||
@@ -93,7 +108,7 @@
|
|||||||
<ul v-else class="task-list clean-result-list">
|
<ul v-else class="task-list clean-result-list">
|
||||||
<li v-for="item in currentItems" :key="`cur-${item.taskId}`" class="task-item">
|
<li v-for="item in currentItems" :key="`cur-${item.taskId}`" class="task-item">
|
||||||
<div class="left">
|
<div class="left">
|
||||||
<span class="id">{{ item.sourceFilename || '相似ASIN检测' }}</span>
|
<span class="id">{{ item.sourceFilename || '货源查询' }}</span>
|
||||||
<div class="files">任务 ID:{{ item.taskId }}</div>
|
<div class="files">任务 ID:{{ item.taskId }}</div>
|
||||||
<div class="files">行数:{{ item.rowCount ?? '-' }}</div>
|
<div class="files">行数:{{ item.rowCount ?? '-' }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,7 +128,7 @@
|
|||||||
<ul v-else class="task-list clean-result-list">
|
<ul v-else class="task-list clean-result-list">
|
||||||
<li v-for="item in historyOnlyItems" :key="`his-${item.resultId}-${item.taskId}`" class="task-item">
|
<li v-for="item in historyOnlyItems" :key="`his-${item.resultId}-${item.taskId}`" class="task-item">
|
||||||
<div class="left">
|
<div class="left">
|
||||||
<span class="id">{{ item.sourceFilename || '相似ASIN检测' }}</span>
|
<span class="id">{{ item.sourceFilename || '货源查询' }}</span>
|
||||||
<div class="files">任务 ID:{{ item.taskId ?? '-' }}</div>
|
<div class="files">任务 ID:{{ item.taskId ?? '-' }}</div>
|
||||||
<div v-if="item.resultFilename" class="files">
|
<div v-if="item.resultFilename" class="files">
|
||||||
{{ item.resultFilename || '下载结果' }}
|
{{ item.resultFilename || '下载结果' }}
|
||||||
@@ -153,13 +168,17 @@ import { ElMessage } from 'element-plus'
|
|||||||
import BrandTopBar from '@/pages/brand/components/BrandTopBar.vue'
|
import BrandTopBar from '@/pages/brand/components/BrandTopBar.vue'
|
||||||
import {
|
import {
|
||||||
activateSimilarAsinTask,
|
activateSimilarAsinTask,
|
||||||
|
addSimilarAsinFilterCondition,
|
||||||
|
deleteSimilarAsinFilterCondition,
|
||||||
deleteSimilarAsinHistory,
|
deleteSimilarAsinHistory,
|
||||||
deleteSimilarAsinTask,
|
deleteSimilarAsinTask,
|
||||||
getSimilarAsinDashboard,
|
getSimilarAsinDashboard,
|
||||||
getSimilarAsinHistory,
|
getSimilarAsinHistory,
|
||||||
getSimilarAsinResultDownloadUrl,
|
getSimilarAsinResultDownloadUrl,
|
||||||
getSimilarAsinTaskProgressBatch,
|
getSimilarAsinTaskProgressBatch,
|
||||||
|
listSimilarAsinFilterConditions,
|
||||||
parseSimilarAsin,
|
parseSimilarAsin,
|
||||||
|
type SimilarAsinFilterConditionVo,
|
||||||
type SimilarAsinParsedGroup,
|
type SimilarAsinParsedGroup,
|
||||||
type SimilarAsinDashboardVo,
|
type SimilarAsinDashboardVo,
|
||||||
type SimilarAsinHistoryItem,
|
type SimilarAsinHistoryItem,
|
||||||
@@ -171,6 +190,7 @@ import {
|
|||||||
import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared/api/brand'
|
import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared/api/brand'
|
||||||
import { getPywebviewApi } from '@/shared/bridges/pywebview'
|
import { getPywebviewApi } from '@/shared/bridges/pywebview'
|
||||||
import { getTaskPollIntervalMs } from '@/shared/task-progress-config'
|
import { getTaskPollIntervalMs } from '@/shared/task-progress-config'
|
||||||
|
import { getStoredApiSecret } from '@/shared/utils/api-secret-store'
|
||||||
import { createCategorizedTimers } from '@/shared/utils/categorized-timers'
|
import { createCategorizedTimers } from '@/shared/utils/categorized-timers'
|
||||||
|
|
||||||
const selectedFileNames = ref<string[]>([])
|
const selectedFileNames = ref<string[]>([])
|
||||||
@@ -179,14 +199,12 @@ const parseResult = ref<SimilarAsinParseVo | null>(null)
|
|||||||
const parsedGroups = ref<SimilarAsinParsedGroup[]>([])
|
const parsedGroups = ref<SimilarAsinParsedGroup[]>([])
|
||||||
type TaskSummary = Pick<SimilarAsinParseVo, 'taskId' | 'totalRows' | 'acceptedRows' | 'groupCount' | 'droppedRows'>
|
type TaskSummary = Pick<SimilarAsinParseVo, 'taskId' | 'totalRows' | 'acceptedRows' | 'groupCount' | 'droppedRows'>
|
||||||
const queuedTaskSummary = ref<TaskSummary | null>(null)
|
const queuedTaskSummary = ref<TaskSummary | null>(null)
|
||||||
const defaultAiPrompt = `1. 图案与形象限制:不含各国国旗及类似图案,无各国领导人画像;不含圣诞老人形象;排除迪士尼、漫威、三丽鸥等所有指定动漫IP的图案与关联物品,且不涉及知名油画作品图案。
|
const filterConditionInput = ref('')
|
||||||
2. 品类排除:非图书、刀具、服装;不含药品、带容器的粉末及液体;与吸烟行为无关,无医疗标志。
|
const filterConditions = ref<SimilarAsinFilterConditionVo[]>([])
|
||||||
3. 功能与配件要求:不可充电且不带电池,无磁吸功能;配件多的情况下需完全匹配。
|
const selectedFilterConditionId = ref<number | null>(null)
|
||||||
4. 规格与外观限制:不含体积大的家具类物品(如椅子、桌子、大镜子、柜子);不带任何品牌logo;无多种款式可选。`
|
|
||||||
const aiPrompt = ref('')
|
|
||||||
const cozeApiKey = ref('')
|
|
||||||
const parsing = ref(false)
|
const parsing = ref(false)
|
||||||
const pushing = ref(false)
|
const pushing = ref(false)
|
||||||
|
const savingCondition = ref(false)
|
||||||
const queuePayloadText = ref('')
|
const queuePayloadText = ref('')
|
||||||
const pollingTaskIds = ref<number[]>([])
|
const pollingTaskIds = ref<number[]>([])
|
||||||
const pendingFileTaskIds = ref<number[]>([])
|
const pendingFileTaskIds = ref<number[]>([])
|
||||||
@@ -206,11 +224,7 @@ const dashboard = ref<SimilarAsinDashboardVo>({
|
|||||||
})
|
})
|
||||||
const historyItems = ref<SimilarAsinHistoryItem[]>([])
|
const historyItems = ref<SimilarAsinHistoryItem[]>([])
|
||||||
|
|
||||||
const parsedRows = computed<SimilarAsinParsedRow[]>(() =>
|
const parsedRows = computed<SimilarAsinParsedRow[]>(() => parseResult.value?.items || [])
|
||||||
parseResult.value?.items?.length
|
|
||||||
? parseResult.value.items
|
|
||||||
: parsedGroups.value.flatMap((group) => group.items || []),
|
|
||||||
)
|
|
||||||
const previewRows = computed(() => parsedRows.value)
|
const previewRows = computed(() => parsedRows.value)
|
||||||
const visibleTaskSummary = computed(() => parseResult.value || queuedTaskSummary.value)
|
const visibleTaskSummary = computed(() => parseResult.value || queuedTaskSummary.value)
|
||||||
const currentItems = computed(() => historyItems.value.filter((i) => i.taskId != null && pollingTaskIds.value.includes(i.taskId)))
|
const currentItems = computed(() => historyItems.value.filter((i) => i.taskId != null && pollingTaskIds.value.includes(i.taskId)))
|
||||||
@@ -221,12 +235,18 @@ const historyOnlyItems = computed(() =>
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
function effectiveAiPrompt() {
|
function selectedFilterConditionText() {
|
||||||
return aiPrompt.value.trim() || defaultAiPrompt
|
const selectedId = selectedFilterConditionId.value
|
||||||
|
if (selectedId == null) return ''
|
||||||
|
return filterConditions.value.find((item) => item.id === selectedId)?.conditionText?.trim() || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function effectiveFilterCondition() {
|
||||||
|
return filterConditionInput.value.trim() || selectedFilterConditionText()
|
||||||
}
|
}
|
||||||
|
|
||||||
function effectiveCozeApiKey() {
|
function effectiveCozeApiKey() {
|
||||||
return cozeApiKey.value.trim()
|
return getStoredApiSecret('similar-asin').trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
function maskSecret(secret: string) {
|
function maskSecret(secret: string) {
|
||||||
@@ -347,13 +367,64 @@ async function selectFolder() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadFilterConditions() {
|
||||||
|
try {
|
||||||
|
filterConditions.value = await listSimilarAsinFilterConditions()
|
||||||
|
const ids = new Set(filterConditions.value.map((item) => item.id))
|
||||||
|
if (selectedFilterConditionId.value != null && !ids.has(selectedFilterConditionId.value)) {
|
||||||
|
selectedFilterConditionId.value = null
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e instanceof Error ? e.message : '加载筛选条件失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveFilterCondition() {
|
||||||
|
const text = filterConditionInput.value.trim()
|
||||||
|
if (!text) {
|
||||||
|
ElMessage.warning('请输入筛选条件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
savingCondition.value = true
|
||||||
|
try {
|
||||||
|
const saved = await addSimilarAsinFilterCondition(text)
|
||||||
|
await loadFilterConditions()
|
||||||
|
if (saved?.id) selectedFilterConditionId.value = saved.id
|
||||||
|
ElMessage.success('筛选条件已保存')
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e instanceof Error ? e.message : '保存筛选条件失败')
|
||||||
|
} finally {
|
||||||
|
savingCondition.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySelectedFilterCondition(condition: SimilarAsinFilterConditionVo) {
|
||||||
|
filterConditionInput.value = condition.conditionText || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeFilterCondition(id: number) {
|
||||||
|
try {
|
||||||
|
await deleteSimilarAsinFilterCondition(id)
|
||||||
|
if (selectedFilterConditionId.value === id) selectedFilterConditionId.value = null
|
||||||
|
await loadFilterConditions()
|
||||||
|
ElMessage.success('筛选条件已删除')
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e instanceof Error ? e.message : '删除筛选条件失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function parseFiles() {
|
async function parseFiles() {
|
||||||
if (!uploadedFiles.value.length) {
|
if (!uploadedFiles.value.length) {
|
||||||
ElMessage.warning('请先选择 Excel')
|
ElMessage.warning('请先选择 Excel')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const filterCondition = effectiveFilterCondition()
|
||||||
|
if (!filterCondition) {
|
||||||
|
ElMessage.warning('请输入筛选条件,或在备选区选择一个筛选条件')
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!effectiveCozeApiKey()) {
|
if (!effectiveCozeApiKey()) {
|
||||||
ElMessage.warning('请先填写密钥')
|
ElMessage.warning('请先在左上角设置中填写货源查询密钥')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
parsing.value = true
|
parsing.value = true
|
||||||
@@ -363,20 +434,10 @@ async function parseFiles() {
|
|||||||
originalFilename: f.originalFilename,
|
originalFilename: f.originalFilename,
|
||||||
relativePath: f.relativePath,
|
relativePath: f.relativePath,
|
||||||
}))
|
}))
|
||||||
const res = await parseSimilarAsin(files, effectiveAiPrompt(), effectiveCozeApiKey())
|
const res = await parseSimilarAsin(files, filterCondition, effectiveCozeApiKey())
|
||||||
parseResult.value = res
|
parseResult.value = res
|
||||||
queuedTaskSummary.value = null
|
queuedTaskSummary.value = null
|
||||||
parsedGroups.value = res.items?.length
|
parsedGroups.value = []
|
||||||
? [{
|
|
||||||
sourceFileKey: res.items[0]?.sourceFileKey,
|
|
||||||
sourceFilename: res.items[0]?.sourceFilename,
|
|
||||||
groupKey: `similar-asin:${res.taskId}`,
|
|
||||||
baseId: '',
|
|
||||||
displayId: res.items[0]?.displayId,
|
|
||||||
itemCount: res.items.length,
|
|
||||||
items: res.items,
|
|
||||||
}]
|
|
||||||
: []
|
|
||||||
queuePayloadText.value = ''
|
queuePayloadText.value = ''
|
||||||
ElMessage.success(`解析完成,共 ${res.acceptedRows} 条`)
|
ElMessage.success(`解析完成,共 ${res.acceptedRows} 条`)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -390,7 +451,7 @@ async function pushToPythonQueue() {
|
|||||||
const api = getPywebviewApi()
|
const api = getPywebviewApi()
|
||||||
const currentParseResult = parseResult.value
|
const currentParseResult = parseResult.value
|
||||||
const taskId = currentParseResult?.taskId
|
const taskId = currentParseResult?.taskId
|
||||||
if (!taskId || !parsedRows.value.length) {
|
if (!taskId) {
|
||||||
ElMessage.warning('请先解析文件')
|
ElMessage.warning('请先解析文件')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -399,7 +460,7 @@ async function pushToPythonQueue() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!effectiveCozeApiKey()) {
|
if (!effectiveCozeApiKey()) {
|
||||||
ElMessage.warning('请先填写密钥')
|
ElMessage.warning('请先在左上角设置中填写货源查询密钥')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pushing.value = true
|
pushing.value = true
|
||||||
@@ -409,41 +470,12 @@ async function pushToPythonQueue() {
|
|||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
data: {
|
data: {
|
||||||
taskId,
|
taskId,
|
||||||
prompt: currentParseResult.aiPrompt || effectiveAiPrompt(),
|
prompt: currentParseResult.aiPrompt || effectiveFilterCondition(),
|
||||||
api_key: effectiveCozeApiKey(),
|
api_key: effectiveCozeApiKey(),
|
||||||
groups: parsedGroups.value.map((group) => ({
|
sourceFileCount: currentParseResult.sourceFileCount || 0,
|
||||||
sourceFileKey: group.sourceFileKey || '',
|
totalRows: currentParseResult.totalRows || 0,
|
||||||
sourceFilename: group.sourceFilename || '',
|
acceptedRows: currentParseResult.acceptedRows || 0,
|
||||||
groupKey: group.groupKey || '',
|
groupCount: currentParseResult.groupCount || 0,
|
||||||
baseId: group.baseId || '',
|
|
||||||
displayId: group.displayId || '',
|
|
||||||
items: (group.items || []).map((row) => ({
|
|
||||||
sourceFileKey: row.sourceFileKey || '',
|
|
||||||
sourceFilename: row.sourceFilename || '',
|
|
||||||
rowToken: row.rowToken || '',
|
|
||||||
groupKey: row.groupKey || '',
|
|
||||||
id: row.displayId,
|
|
||||||
asin: row.asin,
|
|
||||||
country: row.country,
|
|
||||||
price: row.price || '',
|
|
||||||
url: row.url || '',
|
|
||||||
title: row.title || '',
|
|
||||||
target_urls: [],
|
|
||||||
})),
|
|
||||||
})),
|
|
||||||
rows: parsedRows.value.map((row) => ({
|
|
||||||
sourceFileKey: row.sourceFileKey || '',
|
|
||||||
sourceFilename: row.sourceFilename || '',
|
|
||||||
rowToken: row.rowToken || '',
|
|
||||||
groupKey: row.groupKey || '',
|
|
||||||
id: row.displayId,
|
|
||||||
asin: row.asin,
|
|
||||||
country: row.country,
|
|
||||||
price: row.price || '',
|
|
||||||
url: row.url || '',
|
|
||||||
title: row.title || '',
|
|
||||||
target_urls: [],
|
|
||||||
})),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
queuePayloadText.value = JSON.stringify(payloadForDisplay(payload), null, 2)
|
queuePayloadText.value = JSON.stringify(payloadForDisplay(payload), null, 2)
|
||||||
@@ -747,6 +779,7 @@ async function deleteTaskRecord(item: SimilarAsinHistoryItem) {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
loadPollingIds()
|
loadPollingIds()
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
loadFilterConditions().catch(() => undefined),
|
||||||
loadDashboard().catch(() => undefined),
|
loadDashboard().catch(() => undefined),
|
||||||
loadHistory().catch(() => undefined),
|
loadHistory().catch(() => undefined),
|
||||||
])
|
])
|
||||||
@@ -779,12 +812,20 @@ onUnmounted(() => {
|
|||||||
.btn-run:disabled { opacity: .55; cursor: not-allowed; }
|
.btn-run:disabled { opacity: .55; cursor: not-allowed; }
|
||||||
.selected-files { margin-top: 14px; color: #999; font-size: 12px; word-break: break-all; }
|
.selected-files { margin-top: 14px; color: #999; font-size: 12px; word-break: break-all; }
|
||||||
.selected-files span { display: block; margin: 4px 0; }
|
.selected-files span { display: block; margin: 4px 0; }
|
||||||
.prompt-card, .secret-card { margin-bottom: 18px; }
|
.prompt-card { margin-bottom: 14px; }
|
||||||
.prompt-input { width: 100%; box-sizing: border-box; resize: vertical; min-height: 180px; padding: 10px 12px; border: 1px solid #333; border-radius: 8px; background: #202020; color: #d8d8d8; font-size: 12px; line-height: 1.6; outline: none; }
|
.prompt-input { width: 100%; box-sizing: border-box; resize: vertical; min-height: 96px; max-height: 140px; padding: 8px 12px; border: 1px solid #333; border-radius: 8px; background: #202020; color: #d8d8d8; font-size: 12px; line-height: 1.5; outline: none; }
|
||||||
.secret-input { width: 100%; box-sizing: border-box; height: 38px; padding: 0 12px; border: 1px solid #333; border-radius: 8px; background: #202020; color: #d8d8d8; font-size: 12px; outline: none; }
|
.prompt-input:focus { border-color: #3498db; }
|
||||||
.prompt-input:focus, .secret-input:focus { border-color: #3498db; }
|
.condition-actions { display: flex; justify-content: flex-end; margin-top: 8px; }
|
||||||
.prompt-default-label { margin-top: 10px; color: #8d8d8d; font-size: 12px; }
|
.condition-actions .opt-btn { padding: 6px 14px; }
|
||||||
.prompt-preview { margin-top: 10px; padding: 12px; border: 1px solid #2a2a2a; border-radius: 8px; background: #202020; color: #9ea7b3; font-size: 12px; line-height: 1.6; white-space: pre-wrap; }
|
.condition-list-title { margin-top: 10px; }
|
||||||
|
.empty-conditions { color: #666; font-size: 12px; padding: 10px 4px; }
|
||||||
|
.condition-list { list-style: none; margin: 0; padding: 0; max-height: 96px; overflow: auto; display: flex; flex-direction: column; gap: 6px; }
|
||||||
|
.condition-item { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 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 { border: none; background: transparent; color: #ff8f8f; cursor: pointer; font-size: 12px; padding: 2px 0; white-space: nowrap; }
|
||||||
.parse-card, .queue-payload { margin-top: 14px; padding: 12px; border: 1px solid #2a2a2a; border-radius: 8px; background: #202020; color: #b8c1cc; font-size: 12px; }
|
.parse-card, .queue-payload { margin-top: 14px; padding: 12px; border: 1px solid #2a2a2a; border-radius: 8px; background: #202020; color: #b8c1cc; font-size: 12px; }
|
||||||
.queue-payload { max-height: 220px; overflow: auto; color: #8fd3ff; white-space: pre-wrap; }
|
.queue-payload { max-height: 220px; overflow: auto; color: #8fd3ff; white-space: pre-wrap; }
|
||||||
.panel-header { padding: 16px 20px; border-bottom: 1px solid #2a2a2a; font-size: 15px; font-weight: 600; color: #ddd; }
|
.panel-header { padding: 16px 20px; border-bottom: 1px solid #2a2a2a; font-size: 15px; font-weight: 600; color: #ddd; }
|
||||||
|
|||||||
@@ -34,13 +34,16 @@
|
|||||||
</section>
|
</section>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="top-right"></div>
|
<div class="top-right">
|
||||||
|
<BrandApiSecretSettingsButton />
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
import BrandApiSecretSettingsButton from '@/pages/brand/components/BrandApiSecretSettingsButton.vue'
|
||||||
import { getCurrentUserAppColumnKeys } from '@/shared/api/permission'
|
import { getCurrentUserAppColumnKeys } from '@/shared/api/permission'
|
||||||
|
|
||||||
type ActiveNavKey =
|
type ActiveNavKey =
|
||||||
@@ -85,7 +88,7 @@ const navGroups: ReadonlyArray<NavGroup> = [
|
|||||||
{ key: 'variant', label: '变体分析' },
|
{ key: 'variant', label: '变体分析' },
|
||||||
{ key: 'brand', label: '品牌检测', href: '/brand' },
|
{ key: 'brand', label: '品牌检测', href: '/brand' },
|
||||||
{ key: 'appearance-patent', label: '外观专利检测', href: '/new_web_source/appearance-patent.html' },
|
{ key: 'appearance-patent', label: '外观专利检测', href: '/new_web_source/appearance-patent.html' },
|
||||||
{ key: 'similar-asin', label: '相似ASIN检测', href: '/new_web_source/similar-asin.html' },
|
{ key: 'similar-asin', label: '货源查询', href: '/new_web_source/similar-asin.html' },
|
||||||
{ key: 'dedupe', label: '数据去重', href: '/new_web_source/dedupe.html' },
|
{ key: 'dedupe', label: '数据去重', href: '/new_web_source/dedupe.html' },
|
||||||
{ key: 'split', label: '数据拆分', href: '/new_web_source/split.html' },
|
{ key: 'split', label: '数据拆分', href: '/new_web_source/split.html' },
|
||||||
{ key: 'convert', label: '格式转换', href: '/new_web_source/convert.html' },
|
{ key: 'convert', label: '格式转换', href: '/new_web_source/convert.html' },
|
||||||
@@ -248,8 +251,11 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.top-right {
|
.top-right {
|
||||||
width: 60px;
|
width: 140px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
|
|||||||
@@ -1604,7 +1604,13 @@ export function getAppearancePatentResultDownloadUrl(resultId: number) {
|
|||||||
return getJavaDownloadUrl(`/appearance-patent/results/${resultId}/download`);
|
return getJavaDownloadUrl(`/appearance-patent/results/${resultId}/download`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 相似ASIN检测 ==========
|
// ========== 货源查询 ==========
|
||||||
|
|
||||||
|
export interface SimilarAsinFilterConditionVo {
|
||||||
|
id: number;
|
||||||
|
conditionText: string;
|
||||||
|
createdAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SimilarAsinParsedRow {
|
export interface SimilarAsinParsedRow {
|
||||||
rowIndex: number;
|
rowIndex: number;
|
||||||
@@ -1696,7 +1702,7 @@ export interface SimilarAsinTaskBatchVo {
|
|||||||
missingTaskIds?: number[];
|
missingTaskIds?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseSimilarAsin(files: UploadedFileRef[], aiPrompt: string, apiKey?: string) {
|
export function parseSimilarAsin(files: UploadedFileRef[], filterCondition: string, apiKey?: string) {
|
||||||
return unwrapJavaResponse(
|
return unwrapJavaResponse(
|
||||||
post<
|
post<
|
||||||
JavaApiResponse<SimilarAsinParseVo>,
|
JavaApiResponse<SimilarAsinParseVo>,
|
||||||
@@ -1704,12 +1710,41 @@ export function parseSimilarAsin(files: UploadedFileRef[], aiPrompt: string, api
|
|||||||
>(`${JAVA_API_PREFIX}/similar-asin/parse`, {
|
>(`${JAVA_API_PREFIX}/similar-asin/parse`, {
|
||||||
user_id: getCurrentUserId(),
|
user_id: getCurrentUserId(),
|
||||||
files,
|
files,
|
||||||
ai_prompt: aiPrompt,
|
ai_prompt: filterCondition,
|
||||||
api_key: apiKey,
|
api_key: apiKey,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function listSimilarAsinFilterConditions() {
|
||||||
|
return unwrapJavaResponse(
|
||||||
|
get<JavaApiResponse<SimilarAsinFilterConditionVo[]>>(
|
||||||
|
`${JAVA_API_PREFIX}/similar-asin/filter-conditions`,
|
||||||
|
{ params: { user_id: getCurrentUserId() } },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addSimilarAsinFilterCondition(conditionText: string) {
|
||||||
|
return unwrapJavaResponse(
|
||||||
|
post<
|
||||||
|
JavaApiResponse<SimilarAsinFilterConditionVo>,
|
||||||
|
{ user_id: number; condition_text: string }
|
||||||
|
>(`${JAVA_API_PREFIX}/similar-asin/filter-conditions`, {
|
||||||
|
user_id: getCurrentUserId(),
|
||||||
|
condition_text: conditionText,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteSimilarAsinFilterCondition(id: number) {
|
||||||
|
return unwrapJavaResponse(
|
||||||
|
del<JavaApiResponse<null>>(`${JAVA_API_PREFIX}/similar-asin/filter-conditions/${id}`, {
|
||||||
|
params: { user_id: getCurrentUserId() },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function getSimilarAsinDashboard() {
|
export function getSimilarAsinDashboard() {
|
||||||
return unwrapJavaResponse(
|
return unwrapJavaResponse(
|
||||||
get<JavaApiResponse<SimilarAsinDashboardVo>>(
|
get<JavaApiResponse<SimilarAsinDashboardVo>>(
|
||||||
|
|||||||
156
frontend-vue/src/shared/utils/api-secret-store.ts
Normal file
156
frontend-vue/src/shared/utils/api-secret-store.ts
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
export type ApiSecretModuleKey = 'appearance-patent' | 'similar-asin'
|
||||||
|
|
||||||
|
export type ApiSecretRetention = 'session' | '1d' | '7d' | '30d' | 'forever'
|
||||||
|
|
||||||
|
type ApiSecretRecord = {
|
||||||
|
value: string
|
||||||
|
retention: ApiSecretRetention
|
||||||
|
expiresAt: number | null
|
||||||
|
updatedAt: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiSecretSnapshot = {
|
||||||
|
value: string
|
||||||
|
retention: ApiSecretRetention
|
||||||
|
expiresAt: number | null
|
||||||
|
updatedAt: number | null
|
||||||
|
exists: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const STORAGE_PREFIX = 'brand:api-secret'
|
||||||
|
|
||||||
|
function currentUserStorageId() {
|
||||||
|
if (typeof window === 'undefined') return '0'
|
||||||
|
return window.localStorage.getItem('uid') || '0'
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildStorageKey(moduleKey: ApiSecretModuleKey) {
|
||||||
|
return `${STORAGE_PREFIX}:${currentUserStorageId()}:${moduleKey}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function readStorageRecord(storage: Storage, moduleKey: ApiSecretModuleKey): ApiSecretRecord | null {
|
||||||
|
const raw = storage.getItem(buildStorageKey(moduleKey))
|
||||||
|
if (!raw) return null
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw) as Partial<ApiSecretRecord>
|
||||||
|
if (typeof parsed.value !== 'string') return null
|
||||||
|
const retention = normalizeRetention(parsed.retention)
|
||||||
|
const expiresAt = typeof parsed.expiresAt === 'number' ? parsed.expiresAt : null
|
||||||
|
const updatedAt = typeof parsed.updatedAt === 'number' ? parsed.updatedAt : Date.now()
|
||||||
|
return {
|
||||||
|
value: parsed.value,
|
||||||
|
retention,
|
||||||
|
expiresAt,
|
||||||
|
updatedAt,
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRetention(value: unknown): ApiSecretRetention {
|
||||||
|
switch (value) {
|
||||||
|
case '1d':
|
||||||
|
case '7d':
|
||||||
|
case '30d':
|
||||||
|
case 'forever':
|
||||||
|
case 'session':
|
||||||
|
return value
|
||||||
|
default:
|
||||||
|
return 'session'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function retentionToExpiresAt(retention: ApiSecretRetention, now: number) {
|
||||||
|
switch (retention) {
|
||||||
|
case '1d':
|
||||||
|
return now + 24 * 60 * 60 * 1000
|
||||||
|
case '7d':
|
||||||
|
return now + 7 * 24 * 60 * 60 * 1000
|
||||||
|
case '30d':
|
||||||
|
return now + 30 * 24 * 60 * 60 * 1000
|
||||||
|
case 'forever':
|
||||||
|
return null
|
||||||
|
case 'session':
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExpired(record: ApiSecretRecord) {
|
||||||
|
return record.expiresAt != null && record.expiresAt <= Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearStorageRecord(storage: Storage, moduleKey: ApiSecretModuleKey) {
|
||||||
|
storage.removeItem(buildStorageKey(moduleKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLiveRecord(moduleKey: ApiSecretModuleKey): ApiSecretRecord | null {
|
||||||
|
if (typeof window === 'undefined') return null
|
||||||
|
|
||||||
|
const sessionRecord = readStorageRecord(window.sessionStorage, moduleKey)
|
||||||
|
if (sessionRecord) {
|
||||||
|
return sessionRecord
|
||||||
|
}
|
||||||
|
|
||||||
|
const localRecord = readStorageRecord(window.localStorage, moduleKey)
|
||||||
|
if (!localRecord) return null
|
||||||
|
if (isExpired(localRecord)) {
|
||||||
|
clearStorageRecord(window.localStorage, moduleKey)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return localRecord
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStoredApiSecret(moduleKey: ApiSecretModuleKey) {
|
||||||
|
return getLiveRecord(moduleKey)?.value || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStoredApiSecretSnapshot(moduleKey: ApiSecretModuleKey): ApiSecretSnapshot {
|
||||||
|
const record = getLiveRecord(moduleKey)
|
||||||
|
if (!record) {
|
||||||
|
return {
|
||||||
|
value: '',
|
||||||
|
retention: 'session',
|
||||||
|
expiresAt: null,
|
||||||
|
updatedAt: null,
|
||||||
|
exists: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
value: record.value,
|
||||||
|
retention: record.retention,
|
||||||
|
expiresAt: record.expiresAt,
|
||||||
|
updatedAt: record.updatedAt,
|
||||||
|
exists: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveStoredApiSecret(
|
||||||
|
moduleKey: ApiSecretModuleKey,
|
||||||
|
value: string,
|
||||||
|
retention: ApiSecretRetention,
|
||||||
|
) {
|
||||||
|
if (typeof window === 'undefined') return
|
||||||
|
|
||||||
|
const trimmedValue = value.trim()
|
||||||
|
clearStoredApiSecret(moduleKey)
|
||||||
|
if (!trimmedValue) return
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
const record: ApiSecretRecord = {
|
||||||
|
value: trimmedValue,
|
||||||
|
retention,
|
||||||
|
expiresAt: retentionToExpiresAt(retention, now),
|
||||||
|
updatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
const storage = retention === 'session' ? window.sessionStorage : window.localStorage
|
||||||
|
storage.setItem(buildStorageKey(moduleKey), JSON.stringify(record))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearStoredApiSecret(moduleKey: ApiSecretModuleKey) {
|
||||||
|
if (typeof window === 'undefined') return
|
||||||
|
clearStorageRecord(window.sessionStorage, moduleKey)
|
||||||
|
clearStorageRecord(window.localStorage, moduleKey)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user