优化后台业务
This commit is contained in:
@@ -25,8 +25,19 @@
|
|||||||
<hutool.version>5.8.36</hutool.version>
|
<hutool.version>5.8.36</hutool.version>
|
||||||
<rocketmq-spring.version>2.3.5</rocketmq-spring.version>
|
<rocketmq-spring.version>2.3.5</rocketmq-spring.version>
|
||||||
<minio.version>8.5.17</minio.version>
|
<minio.version>8.5.17</minio.version>
|
||||||
|
<javassist.version>3.28.0-GA</javassist.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.javassist</groupId>
|
||||||
|
<artifactId>javassist</artifactId>
|
||||||
|
<version>${javassist.version}</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
|||||||
@@ -649,7 +649,7 @@ public class AppearancePatentCozeClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String normalize(String value) {
|
private String normalize(String value) {
|
||||||
return value == null ? "" : value.replace("\ufeff", "").replace("\u3000", " ").trim();
|
return value == null ? "" : value.replace(String.valueOf((char) 0xFEFF), "").replace((char) 0x3000, ' ').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String rowKey(AppearancePatentResultRowDto row) {
|
private String rowKey(AppearancePatentResultRowDto row) {
|
||||||
|
|||||||
@@ -83,7 +83,10 @@ public class AppearancePatentController {
|
|||||||
public ApiResponse<Void> result(
|
public ApiResponse<Void> result(
|
||||||
@Parameter(description = "外观专利检测任务 ID。任务必须处于 RUNNING 状态。", required = true, example = "3938")
|
@Parameter(description = "外观专利检测任务 ID。任务必须处于 RUNNING 状态。", required = true, example = "3938")
|
||||||
@PathVariable Long taskId,
|
@PathVariable Long taskId,
|
||||||
@Valid @RequestBody AppearancePatentSubmitResultRequest request) {
|
@Valid @RequestBody AppearancePatentSubmitResultRequest request,
|
||||||
|
jakarta.servlet.http.HttpServletResponse response) {
|
||||||
|
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||||
|
response.setContentType("application/json;charset=UTF-8");
|
||||||
service.submitResult(taskId, request);
|
service.submitResult(taskId, request);
|
||||||
return ApiResponse.success(null);
|
return ApiResponse.success(null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ public class AppearancePatentTaskService {
|
|||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
.toList();
|
.toList();
|
||||||
if (sourceFiles.isEmpty()) {
|
if (sourceFiles.isEmpty()) {
|
||||||
throw new BusinessException("璇峰厛涓婁紶 Excel 鏂囦欢");
|
throw new BusinessException("请先上传 Excel 文件");
|
||||||
}
|
}
|
||||||
List<AppearancePatentParsedRowVo> allRows = new ArrayList<>();
|
List<AppearancePatentParsedRowVo> allRows = new ArrayList<>();
|
||||||
List<String> mergedHeaders = new ArrayList<>();
|
List<String> mergedHeaders = new ArrayList<>();
|
||||||
@@ -461,10 +461,10 @@ public class AppearancePatentTaskService {
|
|||||||
private SubmitContext persistSubmittedChunk(Long taskId, AppearancePatentSubmitResultRequest request) {
|
private SubmitContext persistSubmittedChunk(Long taskId, AppearancePatentSubmitResultRequest request) {
|
||||||
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
||||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
||||||
throw new BusinessException("浠诲姟涓嶅瓨鍦?");
|
throw new BusinessException("任务不存在");
|
||||||
}
|
}
|
||||||
if (!STATUS_RUNNING.equals(task.getStatus())) {
|
if (!STATUS_RUNNING.equals(task.getStatus())) {
|
||||||
throw new BusinessException("浠诲姟涓嶆槸杩愯涓姸鎬?");
|
throw new BusinessException("任务不是运行中状态");
|
||||||
}
|
}
|
||||||
|
|
||||||
int chunkIndex = request.getChunkIndex() == null ? 0 : request.getChunkIndex();
|
int chunkIndex = request.getChunkIndex() == null ? 0 : request.getChunkIndex();
|
||||||
@@ -482,7 +482,7 @@ public class AppearancePatentTaskService {
|
|||||||
.last("limit 1"));
|
.last("limit 1"));
|
||||||
if (existing == null) {
|
if (existing == null) {
|
||||||
List<AppearancePatentResultRowDto> rawRows = flattenSubmittedRows(request);
|
List<AppearancePatentResultRowDto> rawRows = flattenSubmittedRows(request);
|
||||||
String payloadJson = writeJson(rawRows, "缁撴灉搴忓垪鍖栧け璐?");
|
String payloadJson = writeJson(rawRows, "结果序列化失败");
|
||||||
|
|
||||||
TaskChunkEntity chunk = new TaskChunkEntity();
|
TaskChunkEntity chunk = new TaskChunkEntity();
|
||||||
chunk.setTaskId(taskId);
|
chunk.setTaskId(taskId);
|
||||||
@@ -514,7 +514,7 @@ public class AppearancePatentTaskService {
|
|||||||
private void completeSubmittedChunk(SubmitContext context) {
|
private void completeSubmittedChunk(SubmitContext context) {
|
||||||
FileTaskEntity task = fileTaskMapper.selectById(context.task().getId());
|
FileTaskEntity task = fileTaskMapper.selectById(context.task().getId());
|
||||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
||||||
throw new BusinessException("浠诲姟涓嶅瓨鍦?");
|
throw new BusinessException("任务不存在");
|
||||||
}
|
}
|
||||||
if (!STATUS_RUNNING.equals(task.getStatus())) {
|
if (!STATUS_RUNNING.equals(task.getStatus())) {
|
||||||
log.info("[appearance-patent] skip completion because task already finalized taskId={} status={}",
|
log.info("[appearance-patent] skip completion because task already finalized taskId={} status={}",
|
||||||
@@ -1379,7 +1379,7 @@ public class AppearancePatentTaskService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String normalize(String val) {
|
private String normalize(String val) {
|
||||||
return val == null ? "" : val.replace("\ufeff", "").replace("\u3000", " ").trim().replaceAll("\\s+", " ");
|
return val == null ? "" : val.replace(String.valueOf((char) 0xFEFF), "").replace((char) 0x3000, ' ').trim().replaceAll("\\s+", " ");
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildParsedPayloadJson(String aiPrompt, List<AppearancePatentSourceFileDto> sourceFiles, List<String> headers, List<AppearancePatentParsedGroupVo> groups, List<AppearancePatentParsedRowVo> allRows) {
|
private String buildParsedPayloadJson(String aiPrompt, List<AppearancePatentSourceFileDto> sourceFiles, List<String> headers, List<AppearancePatentParsedGroupVo> groups, List<AppearancePatentParsedRowVo> allRows) {
|
||||||
|
|||||||
@@ -881,8 +881,8 @@ public class BrandTaskService {
|
|||||||
if (value == null) {
|
if (value == null) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
return value.replace("\ufeff", "")
|
return value.replace(String.valueOf((char) 0xFEFF), "")
|
||||||
.replace("\u3000", " ")
|
.replace((char) 0x3000, ' ')
|
||||||
.replace("\r\n", " ")
|
.replace("\r\n", " ")
|
||||||
.replace("\r", " ")
|
.replace("\r", " ")
|
||||||
.replace("\n", " ")
|
.replace("\n", " ")
|
||||||
|
|||||||
@@ -475,8 +475,8 @@ public class ConvertRunService {
|
|||||||
if (value == null) {
|
if (value == null) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
return value.replace("\ufeff", "")
|
return value.replace(String.valueOf((char) 0xFEFF), "")
|
||||||
.replace("\u3000", " ")
|
.replace((char) 0x3000, ' ')
|
||||||
.replace("\r\n", " ")
|
.replace("\r\n", " ")
|
||||||
.replace("\r", " ")
|
.replace("\r", " ")
|
||||||
.replace("\n", " ")
|
.replace("\n", " ")
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ import java.util.zip.ZipOutputStream;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class DedupeRunService {
|
public class DedupeRunService {
|
||||||
|
|
||||||
private static final String HEADER_SPLIT_MARKER = "idASIN\u56fd\u5bb6\u72b6\u6001\u4ef7\u683c\u53d8\u4f53\u6570\u91cf";
|
private static final String HEADER_SPLIT_MARKER = "idASIN国家状态价格变体数量";
|
||||||
private static final String HEADER_STOP_COLUMN = "\u7f29\u7565\u56fe\u5730\u57408";
|
private static final String HEADER_STOP_COLUMN = "缩略图地址8";
|
||||||
|
|
||||||
private final FileTaskMapper fileTaskMapper;
|
private final FileTaskMapper fileTaskMapper;
|
||||||
private final FileResultMapper fileResultMapper;
|
private final FileResultMapper fileResultMapper;
|
||||||
@@ -59,7 +59,7 @@ public class DedupeRunService {
|
|||||||
public DedupeRunVo run(DedupeRunRequest request) {
|
public DedupeRunVo run(DedupeRunRequest request) {
|
||||||
long runStartNs = System.nanoTime();
|
long runStartNs = System.nanoTime();
|
||||||
if (!request.isKeepIntegerIds() && !request.isKeepUnderscoreIds() && !request.isKeepIntegerMainIdsWhenNoSubIds()) {
|
if (!request.isKeepIntegerIds() && !request.isKeepUnderscoreIds() && !request.isKeepIntegerMainIdsWhenNoSubIds()) {
|
||||||
throw new BusinessException("\u8bf7\u81f3\u5c11\u9009\u62e9\u4e00\u79cd ID \u4fdd\u7559\u89c4\u5219");
|
throw new BusinessException("请至少选择一种 ID 保留规则");
|
||||||
}
|
}
|
||||||
|
|
||||||
FileTaskEntity task = new FileTaskEntity();
|
FileTaskEntity task = new FileTaskEntity();
|
||||||
@@ -90,7 +90,7 @@ public class DedupeRunService {
|
|||||||
File inputFile = findLocalSourceFile(sourceFile.getFileKey());
|
File inputFile = findLocalSourceFile(sourceFile.getFileKey());
|
||||||
long findFileNs = elapsedNs(fileStartNs);
|
long findFileNs = elapsedNs(fileStartNs);
|
||||||
if (inputFile == null || !inputFile.exists()) {
|
if (inputFile == null || !inputFile.exists()) {
|
||||||
throw new BusinessException("\u4e0a\u4f20\u6587\u4ef6\u4e0d\u5b58\u5728\uff0c\u8bf7\u91cd\u65b0\u4e0a\u4f20");
|
throw new BusinessException("上传文件不存在,请重新上传");
|
||||||
}
|
}
|
||||||
|
|
||||||
String inputName = sourceFile.getOriginalFilename() == null ? inputFile.getName() : sourceFile.getOriginalFilename();
|
String inputName = sourceFile.getOriginalFilename() == null ? inputFile.getName() : sourceFile.getOriginalFilename();
|
||||||
@@ -240,7 +240,7 @@ public class DedupeRunService {
|
|||||||
public void deleteHistory(Long resultId, Long userId) {
|
public void deleteHistory(Long resultId, Long userId) {
|
||||||
FileResultEntity entity = fileResultMapper.selectById(resultId);
|
FileResultEntity entity = fileResultMapper.selectById(resultId);
|
||||||
if (entity == null || !"DEDUPE".equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {
|
if (entity == null || !"DEDUPE".equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {
|
||||||
throw new BusinessException("\u8bb0\u5f55\u4e0d\u5b58\u5728");
|
throw new BusinessException("记录不存在");
|
||||||
}
|
}
|
||||||
fileResultMapper.deleteById(resultId);
|
fileResultMapper.deleteById(resultId);
|
||||||
}
|
}
|
||||||
@@ -248,11 +248,11 @@ public class DedupeRunService {
|
|||||||
public Resource getResultFile(Long resultId, Long userId) {
|
public Resource getResultFile(Long resultId, Long userId) {
|
||||||
FileResultEntity entity = fileResultMapper.selectById(resultId);
|
FileResultEntity entity = fileResultMapper.selectById(resultId);
|
||||||
if (entity == null || entity.getResultFileUrl() == null || !"DEDUPE".equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {
|
if (entity == null || entity.getResultFileUrl() == null || !"DEDUPE".equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {
|
||||||
throw new BusinessException("\u7ed3\u679c\u6587\u4ef6\u4e0d\u5b58\u5728");
|
throw new BusinessException("结果文件不存在");
|
||||||
}
|
}
|
||||||
File file = new File(entity.getResultFileUrl());
|
File file = new File(entity.getResultFileUrl());
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
throw new BusinessException("\u7ed3\u679c\u6587\u4ef6\u4e0d\u5b58\u5728");
|
throw new BusinessException("结果文件不存在");
|
||||||
}
|
}
|
||||||
return new FileSystemResource(file);
|
return new FileSystemResource(file);
|
||||||
}
|
}
|
||||||
@@ -342,18 +342,18 @@ public class DedupeRunService {
|
|||||||
Sheet sheet = workbook.getSheetAt(0);
|
Sheet sheet = workbook.getSheetAt(0);
|
||||||
Row headerRow = sheet.getRow(0);
|
Row headerRow = sheet.getRow(0);
|
||||||
if (headerRow == null) {
|
if (headerRow == null) {
|
||||||
throw new BusinessException("Excel \u8868\u5934\u4e3a\u7a7a");
|
throw new BusinessException("Excel 表头为空");
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, Integer> headerMap = buildHeaderMap(headerRow, formatter);
|
Map<String, Integer> headerMap = buildHeaderMap(headerRow, formatter);
|
||||||
if (headerMap.isEmpty()) {
|
if (headerMap.isEmpty()) {
|
||||||
throw new BusinessException("Excel \u8868\u5934\u4e3a\u7a7a");
|
throw new BusinessException("Excel 表头为空");
|
||||||
}
|
}
|
||||||
List<String> missing = selectedColumns.stream()
|
List<String> missing = selectedColumns.stream()
|
||||||
.filter(column -> !headerMap.containsKey(column))
|
.filter(column -> !headerMap.containsKey(column))
|
||||||
.toList();
|
.toList();
|
||||||
if (!missing.isEmpty()) {
|
if (!missing.isEmpty()) {
|
||||||
throw new BusinessException("\u7f3a\u5c11\u5217\uff1a" + String.join("\u3001", missing));
|
throw new BusinessException("缺少列:" + String.join("、", missing));
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Integer> selectedIndexes = new ArrayList<>(selectedColumns.size());
|
List<Integer> selectedIndexes = new ArrayList<>(selectedColumns.size());
|
||||||
@@ -478,7 +478,7 @@ public class DedupeRunService {
|
|||||||
zos.closeEntry();
|
zos.closeEntry();
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
throw new BusinessException("\u53bb\u91cd\u7ed3\u679c\u6253\u5305\u5931\u8d25\uff1a" + ex.getMessage());
|
throw new BusinessException("去重结果打包失败:" + ex.getMessage());
|
||||||
}
|
}
|
||||||
return zipFile;
|
return zipFile;
|
||||||
}
|
}
|
||||||
@@ -602,7 +602,7 @@ public class DedupeRunService {
|
|||||||
boolean previousWhitespace = false;
|
boolean previousWhitespace = false;
|
||||||
for (int i = start; i < end; i++) {
|
for (int i = start; i < end; i++) {
|
||||||
char ch = value.charAt(i);
|
char ch = value.charAt(i);
|
||||||
if (ch == '\uFEFF') {
|
if (ch == 0xFEFF) {
|
||||||
if (builder == null) {
|
if (builder == null) {
|
||||||
builder = new StringBuilder(value.length());
|
builder = new StringBuilder(value.length());
|
||||||
builder.append(value, start, i);
|
builder.append(value, start, i);
|
||||||
@@ -629,11 +629,11 @@ public class DedupeRunService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isTrimmedWhitespace(char ch) {
|
private boolean isTrimmedWhitespace(char ch) {
|
||||||
return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' || ch == '\u3000' || Character.isWhitespace(ch);
|
return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' || ch == 0x3000 || Character.isWhitespace(ch);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isNormalizedWhitespace(char ch) {
|
private boolean isNormalizedWhitespace(char ch) {
|
||||||
return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' || ch == '\u3000' || Character.isWhitespace(ch);
|
return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' || ch == 0x3000 || Character.isWhitespace(ch);
|
||||||
}
|
}
|
||||||
|
|
||||||
private record DedupeArchiveEntry(String relativePath, String inputName, File outputFile) {
|
private record DedupeArchiveEntry(String relativePath, String inputName, File outputFile) {
|
||||||
|
|||||||
@@ -109,8 +109,8 @@ public class LocalFileStorageService {
|
|||||||
if (value == null) {
|
if (value == null) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
return value.replace("\ufeff", "")
|
return value.replace(String.valueOf((char) 0xFEFF), "")
|
||||||
.replace("\u3000", " ")
|
.replace((char) 0x3000, ' ')
|
||||||
.replace("\r\n", " ")
|
.replace("\r\n", " ")
|
||||||
.replace("\r", " ")
|
.replace("\r", " ")
|
||||||
.replace("\n", " ")
|
.replace("\n", " ")
|
||||||
|
|||||||
@@ -281,7 +281,6 @@ public class PatrolDeleteTaskService {
|
|||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public void submitResult(Long taskId, PatrolDeleteSubmitResultRequest request) {
|
public void submitResult(Long taskId, PatrolDeleteSubmitResultRequest request) {
|
||||||
if (taskId == null || taskId <= 0) {
|
if (taskId == null || taskId <= 0) {
|
||||||
throw new BusinessException("taskId 不合法");
|
throw new BusinessException("taskId 不合法");
|
||||||
@@ -334,7 +333,6 @@ public class PatrolDeleteTaskService {
|
|||||||
tryFinalizeTask(taskId, false);
|
tryFinalizeTask(taskId, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public boolean tryFinalizeTask(Long taskId, boolean fromCompensation) {
|
public boolean tryFinalizeTask(Long taskId, boolean fromCompensation) {
|
||||||
if (taskId == null || taskId <= 0) {
|
if (taskId == null || taskId <= 0) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.nanri.aiimage.modules.pricetrack.model.dto;
|
package com.nanri.aiimage.modules.pricetrack.model.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@@ -88,6 +89,10 @@ public class PriceTrackSubmitResultRequest {
|
|||||||
@Schema(description = "购物车店铺名", example = "店铺A")
|
@Schema(description = "购物车店铺名", example = "店铺A")
|
||||||
private String cartShopName;
|
private String cartShopName;
|
||||||
|
|
||||||
|
@JsonAlias({"changedPrice", "changePrice", "modifiedPrice", "newPrice", "targetPrice", "updatedPrice", "改价价格"})
|
||||||
|
@Schema(description = "改价价格", example = "18.99")
|
||||||
|
private String priceChangePrice;
|
||||||
|
|
||||||
@Schema(description = "改价情况", example = "UPDATED")
|
@Schema(description = "改价情况", example = "UPDATED")
|
||||||
private String priceChangeStatus;
|
private String priceChangeStatus;
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ public class PriceTrackExcelAssemblyService {
|
|||||||
"第一名",
|
"第一名",
|
||||||
"第二名",
|
"第二名",
|
||||||
"购物车店铺名",
|
"购物车店铺名",
|
||||||
|
"改价价格",
|
||||||
"改价情况",
|
"改价情况",
|
||||||
"修改次数",
|
"修改次数",
|
||||||
"状态"
|
"状态"
|
||||||
@@ -40,13 +41,14 @@ public class PriceTrackExcelAssemblyService {
|
|||||||
workbook.setCompressTempFiles(true);
|
workbook.setCompressTempFiles(true);
|
||||||
try (FileOutputStream fos = new FileOutputStream(outputXlsx)) {
|
try (FileOutputStream fos = new FileOutputStream(outputXlsx)) {
|
||||||
for (ProductRiskCountryCode code : ProductRiskCountryCode.values()) {
|
for (ProductRiskCountryCode code : ProductRiskCountryCode.values()) {
|
||||||
String sheetName = code.name();
|
String countryCode = code.name();
|
||||||
|
String sheetName = sheetNameOf(code);
|
||||||
Sheet sheet = workbook.createSheet(sheetName);
|
Sheet sheet = workbook.createSheet(sheetName);
|
||||||
Row headerRow = sheet.createRow(0);
|
Row headerRow = sheet.createRow(0);
|
||||||
for (int i = 0; i < RESULT_HEADER.length; i++) {
|
for (int i = 0; i < RESULT_HEADER.length; i++) {
|
||||||
headerRow.createCell(i).setCellValue(RESULT_HEADER[i]);
|
headerRow.createCell(i).setCellValue(RESULT_HEADER[i]);
|
||||||
}
|
}
|
||||||
List<PriceTrackSubmitResultRequest.AsinResult> rows = safe.getOrDefault(sheetName, List.of());
|
List<PriceTrackSubmitResultRequest.AsinResult> rows = safe.getOrDefault(countryCode, List.of());
|
||||||
int rowIdx = 1;
|
int rowIdx = 1;
|
||||||
for (PriceTrackSubmitResultRequest.AsinResult item : rows) {
|
for (PriceTrackSubmitResultRequest.AsinResult item : rows) {
|
||||||
if (item == null) {
|
if (item == null) {
|
||||||
@@ -62,9 +64,10 @@ public class PriceTrackExcelAssemblyService {
|
|||||||
row.createCell(6).setCellValue(valueOf(item.getFirstPlace()));
|
row.createCell(6).setCellValue(valueOf(item.getFirstPlace()));
|
||||||
row.createCell(7).setCellValue(valueOf(item.getSecondPlace()));
|
row.createCell(7).setCellValue(valueOf(item.getSecondPlace()));
|
||||||
row.createCell(8).setCellValue(valueOf(item.getCartShopName()));
|
row.createCell(8).setCellValue(valueOf(item.getCartShopName()));
|
||||||
row.createCell(9).setCellValue(valueOf(item.getPriceChangeStatus()));
|
row.createCell(9).setCellValue(valueOf(item.getPriceChangePrice()));
|
||||||
row.createCell(10).setCellValue(valueOf(item.getModifyCount()));
|
row.createCell(10).setCellValue(valueOf(item.getPriceChangeStatus()));
|
||||||
row.createCell(11).setCellValue(valueOf(item.getStatus()));
|
row.createCell(11).setCellValue(valueOf(item.getModifyCount()));
|
||||||
|
row.createCell(12).setCellValue(valueOf(item.getStatus()));
|
||||||
}
|
}
|
||||||
applyDefaultColumnWidths(sheet);
|
applyDefaultColumnWidths(sheet);
|
||||||
}
|
}
|
||||||
@@ -113,8 +116,18 @@ public class PriceTrackExcelAssemblyService {
|
|||||||
return value == null ? "" : value;
|
return value == null ? "" : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String sheetNameOf(ProductRiskCountryCode code) {
|
||||||
|
return switch (code) {
|
||||||
|
case DE -> "德国";
|
||||||
|
case FR -> "法国";
|
||||||
|
case ES -> "西班牙";
|
||||||
|
case IT -> "意大利";
|
||||||
|
case UK -> "英国";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private void applyDefaultColumnWidths(Sheet sheet) {
|
private void applyDefaultColumnWidths(Sheet sheet) {
|
||||||
int[] widths = {22, 18, 12, 12, 12, 12, 20, 20, 22, 18, 12, 12};
|
int[] widths = {22, 18, 12, 12, 12, 12, 20, 20, 22, 14, 18, 12, 12};
|
||||||
for (int i = 0; i < widths.length; i++) {
|
for (int i = 0; i < widths.length; i++) {
|
||||||
sheet.setColumnWidth(i, widths[i] * 256);
|
sheet.setColumnWidth(i, widths[i] * 256);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,6 +127,21 @@ public class PriceTrackTaskService {
|
|||||||
return tasks;
|
return tasks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<FileTaskEntity> selectTaskStatusesByIdsInBatches(List<Long> taskIds) {
|
||||||
|
List<FileTaskEntity> tasks = new ArrayList<>();
|
||||||
|
if (taskIds == null || taskIds.isEmpty()) {
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
int batchSize = Math.max(1, taskPressureProperties.getDbSelectBatchSize());
|
||||||
|
for (int start = 0; start < taskIds.size(); start += batchSize) {
|
||||||
|
int end = Math.min(start + batchSize, taskIds.size());
|
||||||
|
tasks.addAll(fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
|
||||||
|
.select(FileTaskEntity::getId, FileTaskEntity::getStatus)
|
||||||
|
.in(FileTaskEntity::getId, taskIds.subList(start, end))));
|
||||||
|
}
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
|
||||||
public PriceTrackDashboardVo dashboard(Long userId) {
|
public PriceTrackDashboardVo dashboard(Long userId) {
|
||||||
if (userId == null || userId <= 0) throw new BusinessException("user_id 不合法");
|
if (userId == null || userId <= 0) throw new BusinessException("user_id 不合法");
|
||||||
PriceTrackDashboardVo vo = new PriceTrackDashboardVo();
|
PriceTrackDashboardVo vo = new PriceTrackDashboardVo();
|
||||||
@@ -166,7 +181,7 @@ public class PriceTrackTaskService {
|
|||||||
.distinct()
|
.distinct()
|
||||||
.toList();
|
.toList();
|
||||||
if (!taskIds.isEmpty()) {
|
if (!taskIds.isEmpty()) {
|
||||||
List<FileTaskEntity> tasks = selectTasksByIdsInBatches(taskIds);
|
List<FileTaskEntity> tasks = selectTaskStatusesByIdsInBatches(taskIds);
|
||||||
for (FileTaskEntity t : tasks) {
|
for (FileTaskEntity t : tasks) {
|
||||||
if (t != null) statusByTaskId.put(t.getId(), t.getStatus());
|
if (t != null) statusByTaskId.put(t.getId(), t.getStatus());
|
||||||
}
|
}
|
||||||
@@ -383,7 +398,6 @@ public class PriceTrackTaskService {
|
|||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public void submitResult(Long taskId, PriceTrackSubmitResultRequest request) {
|
public void submitResult(Long taskId, PriceTrackSubmitResultRequest request) {
|
||||||
if (taskId == null || taskId <= 0) throw new BusinessException("taskId 不合法");
|
if (taskId == null || taskId <= 0) throw new BusinessException("taskId 不合法");
|
||||||
if (request == null || request.getShops() == null || request.getShops().isEmpty()) {
|
if (request == null || request.getShops() == null || request.getShops().isEmpty()) {
|
||||||
@@ -458,7 +472,6 @@ public class PriceTrackTaskService {
|
|||||||
updateTaskStatusFromLatestRows(task, latest);
|
updateTaskStatusFromLatestRows(task, latest);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public boolean tryFinalizeTask(Long taskId, boolean fromCompensation) {
|
public boolean tryFinalizeTask(Long taskId, boolean fromCompensation) {
|
||||||
if (taskId == null || taskId <= 0) return false;
|
if (taskId == null || taskId <= 0) return false;
|
||||||
FileTaskEntity task = loadTaskForExecution(taskId);
|
FileTaskEntity task = loadTaskForExecution(taskId);
|
||||||
@@ -1139,6 +1152,7 @@ public class PriceTrackTaskService {
|
|||||||
merged.setFirstPlace(firstNonBlank(incoming.getFirstPlace(), merged.getFirstPlace()));
|
merged.setFirstPlace(firstNonBlank(incoming.getFirstPlace(), merged.getFirstPlace()));
|
||||||
merged.setSecondPlace(firstNonBlank(incoming.getSecondPlace(), merged.getSecondPlace()));
|
merged.setSecondPlace(firstNonBlank(incoming.getSecondPlace(), merged.getSecondPlace()));
|
||||||
merged.setCartShopName(firstNonBlank(incoming.getCartShopName(), merged.getCartShopName()));
|
merged.setCartShopName(firstNonBlank(incoming.getCartShopName(), merged.getCartShopName()));
|
||||||
|
merged.setPriceChangePrice(firstNonBlank(incoming.getPriceChangePrice(), merged.getPriceChangePrice()));
|
||||||
merged.setPriceChangeStatus(firstNonBlank(incoming.getPriceChangeStatus(), merged.getPriceChangeStatus()));
|
merged.setPriceChangeStatus(firstNonBlank(incoming.getPriceChangeStatus(), merged.getPriceChangeStatus()));
|
||||||
merged.setModifyCount(firstNonBlank(incoming.getModifyCount(), merged.getModifyCount()));
|
merged.setModifyCount(firstNonBlank(incoming.getModifyCount(), merged.getModifyCount()));
|
||||||
merged.setStatus(firstNonBlank(incoming.getStatus(), merged.getStatus()));
|
merged.setStatus(firstNonBlank(incoming.getStatus(), merged.getStatus()));
|
||||||
@@ -1163,6 +1177,7 @@ public class PriceTrackTaskService {
|
|||||||
out.setFirstPlace(row.getFirstPlace());
|
out.setFirstPlace(row.getFirstPlace());
|
||||||
out.setSecondPlace(row.getSecondPlace());
|
out.setSecondPlace(row.getSecondPlace());
|
||||||
out.setCartShopName(row.getCartShopName());
|
out.setCartShopName(row.getCartShopName());
|
||||||
|
out.setPriceChangePrice(row.getPriceChangePrice());
|
||||||
out.setPriceChangeStatus(row.getPriceChangeStatus());
|
out.setPriceChangeStatus(row.getPriceChangeStatus());
|
||||||
out.setModifyCount(row.getModifyCount());
|
out.setModifyCount(row.getModifyCount());
|
||||||
out.setStatus(row.getStatus());
|
out.setStatus(row.getStatus());
|
||||||
|
|||||||
@@ -124,6 +124,21 @@ public class ProductRiskTaskService {
|
|||||||
return tasks;
|
return tasks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<FileTaskEntity> selectTaskStatusesByIdsInBatches(List<Long> taskIds) {
|
||||||
|
List<FileTaskEntity> tasks = new ArrayList<>();
|
||||||
|
if (taskIds == null || taskIds.isEmpty()) {
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
int batchSize = Math.max(1, taskPressureProperties.getDbSelectBatchSize());
|
||||||
|
for (int start = 0; start < taskIds.size(); start += batchSize) {
|
||||||
|
int end = Math.min(start + batchSize, taskIds.size());
|
||||||
|
tasks.addAll(fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
|
||||||
|
.select(FileTaskEntity::getId, FileTaskEntity::getStatus)
|
||||||
|
.in(FileTaskEntity::getId, taskIds.subList(start, end))));
|
||||||
|
}
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
|
||||||
public ProductRiskDashboardVo dashboard(Long userId) {
|
public ProductRiskDashboardVo dashboard(Long userId) {
|
||||||
if (userId == null || userId <= 0) {
|
if (userId == null || userId <= 0) {
|
||||||
throw new BusinessException("user_id 不合法");
|
throw new BusinessException("user_id 不合法");
|
||||||
@@ -166,7 +181,7 @@ public class ProductRiskTaskService {
|
|||||||
.distinct()
|
.distinct()
|
||||||
.toList();
|
.toList();
|
||||||
if (!taskIds.isEmpty()) {
|
if (!taskIds.isEmpty()) {
|
||||||
List<FileTaskEntity> tasks = selectTasksByIdsInBatches(taskIds);
|
List<FileTaskEntity> tasks = selectTaskStatusesByIdsInBatches(taskIds);
|
||||||
for (FileTaskEntity t : tasks) {
|
for (FileTaskEntity t : tasks) {
|
||||||
if (t != null) {
|
if (t != null) {
|
||||||
statusByTaskId.put(t.getId(), t.getStatus());
|
statusByTaskId.put(t.getId(), t.getStatus());
|
||||||
@@ -465,7 +480,6 @@ public class ProductRiskTaskService {
|
|||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public void submitResult(Long taskId, ProductRiskSubmitResultRequest request) {
|
public void submitResult(Long taskId, ProductRiskSubmitResultRequest request) {
|
||||||
if (taskId == null || taskId <= 0) {
|
if (taskId == null || taskId <= 0) {
|
||||||
throw new BusinessException("taskId 不合法");
|
throw new BusinessException("taskId 不合法");
|
||||||
@@ -630,7 +644,6 @@ public class ProductRiskTaskService {
|
|||||||
cleanupTaskCacheIfTerminal(taskId, task.getStatus());
|
cleanupTaskCacheIfTerminal(taskId, task.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public boolean tryFinalizeTask(Long taskId, boolean fromCompensation) {
|
public boolean tryFinalizeTask(Long taskId, boolean fromCompensation) {
|
||||||
if (taskId == null || taskId <= 0) {
|
if (taskId == null || taskId <= 0) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -287,7 +287,6 @@ public class QueryAsinTaskService {
|
|||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public void submitResult(Long taskId, QueryAsinSubmitResultRequest request) {
|
public void submitResult(Long taskId, QueryAsinSubmitResultRequest request) {
|
||||||
if (taskId == null || taskId <= 0) {
|
if (taskId == null || taskId <= 0) {
|
||||||
throw new BusinessException("taskId 不合法");
|
throw new BusinessException("taskId 不合法");
|
||||||
@@ -340,7 +339,6 @@ public class QueryAsinTaskService {
|
|||||||
tryFinalizeTask(taskId, false);
|
tryFinalizeTask(taskId, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public boolean tryFinalizeTask(Long taskId, boolean fromCompensation) {
|
public boolean tryFinalizeTask(Long taskId, boolean fromCompensation) {
|
||||||
if (taskId == null || taskId <= 0) {
|
if (taskId == null || taskId <= 0) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -3,7 +3,68 @@ package com.nanri.aiimage.modules.shopkey.mapper;
|
|||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
import com.nanri.aiimage.modules.shopkey.model.entity.ShopManageGroupEntity;
|
import com.nanri.aiimage.modules.shopkey.model.entity.ShopManageGroupEntity;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface ShopManageGroupMapper extends BaseMapper<ShopManageGroupEntity> {
|
public interface ShopManageGroupMapper extends BaseMapper<ShopManageGroupEntity> {
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT DISTINCT accessible_group_id
|
||||||
|
FROM (
|
||||||
|
SELECT g.id AS accessible_group_id
|
||||||
|
FROM biz_shop_manage_group g
|
||||||
|
WHERE g.created_by_id = #{operatorId}
|
||||||
|
OR g.user_id = #{operatorId}
|
||||||
|
UNION
|
||||||
|
SELECT gm.group_id AS accessible_group_id
|
||||||
|
FROM biz_shop_manage_group_member gm
|
||||||
|
WHERE gm.user_id = #{operatorId}
|
||||||
|
UNION
|
||||||
|
SELECT g2.id AS accessible_group_id
|
||||||
|
FROM biz_shop_manage_group_member gm
|
||||||
|
INNER JOIN biz_shop_manage_group g1 ON g1.id = gm.group_id
|
||||||
|
INNER JOIN biz_shop_manage_group g2
|
||||||
|
ON g2.created_by_id = COALESCE(g1.created_by_id, g1.user_id)
|
||||||
|
OR g2.user_id = COALESCE(g1.created_by_id, g1.user_id)
|
||||||
|
WHERE gm.user_id = #{operatorId}
|
||||||
|
AND COALESCE(g1.created_by_id, g1.user_id) IS NOT NULL
|
||||||
|
UNION
|
||||||
|
SELECT member_groups.id AS accessible_group_id
|
||||||
|
FROM biz_shop_manage_group leader_group
|
||||||
|
INNER JOIN biz_shop_manage_group_member direct_member ON direct_member.group_id = leader_group.id
|
||||||
|
INNER JOIN biz_shop_manage_group member_groups
|
||||||
|
ON member_groups.created_by_id = direct_member.user_id
|
||||||
|
OR member_groups.user_id = direct_member.user_id
|
||||||
|
WHERE leader_group.created_by_id = #{operatorId}
|
||||||
|
OR leader_group.user_id = #{operatorId}
|
||||||
|
) accessible_groups
|
||||||
|
WHERE accessible_group_id IS NOT NULL
|
||||||
|
ORDER BY accessible_group_id ASC
|
||||||
|
""")
|
||||||
|
List<Long> selectAccessibleGroupIds(@Param("operatorId") Long operatorId);
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT DISTINCT visible_user_id
|
||||||
|
FROM (
|
||||||
|
SELECT #{operatorId} AS visible_user_id
|
||||||
|
UNION
|
||||||
|
SELECT direct_member.user_id AS visible_user_id
|
||||||
|
FROM biz_shop_manage_group leader_group
|
||||||
|
INNER JOIN biz_shop_manage_group_member direct_member ON direct_member.group_id = leader_group.id
|
||||||
|
WHERE leader_group.created_by_id = #{operatorId}
|
||||||
|
OR leader_group.user_id = #{operatorId}
|
||||||
|
UNION
|
||||||
|
SELECT COALESCE(direct_group.created_by_id, direct_group.user_id) AS visible_user_id
|
||||||
|
FROM biz_shop_manage_group_member membership
|
||||||
|
INNER JOIN biz_shop_manage_group direct_group ON direct_group.id = membership.group_id
|
||||||
|
WHERE membership.user_id = #{operatorId}
|
||||||
|
AND COALESCE(direct_group.created_by_id, direct_group.user_id) IS NOT NULL
|
||||||
|
) visible_users
|
||||||
|
WHERE visible_user_id IS NOT NULL
|
||||||
|
ORDER BY visible_user_id ASC
|
||||||
|
""")
|
||||||
|
List<Long> selectAccessibleCreatorUserIds(@Param("operatorId") Long operatorId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public class QueryAsinService {
|
|||||||
|
|
||||||
private static final List<String> SUPPORTED_COUNTRIES = List.of("DE", "UK", "FR", "IT", "ES");
|
private static final List<String> SUPPORTED_COUNTRIES = List.of("DE", "UK", "FR", "IT", "ES");
|
||||||
private static final String KEY_SEPARATOR = Character.toString((char) 1);
|
private static final String KEY_SEPARATOR = Character.toString((char) 1);
|
||||||
private static final String UTF8_BOM = Character.toString(0xFEFF);
|
private static final String UTF8_BOM = String.valueOf((char) 0xFEFF);
|
||||||
|
|
||||||
private final QueryAsinMapper queryAsinMapper;
|
private final QueryAsinMapper queryAsinMapper;
|
||||||
private final ShopManageGroupService shopManageGroupService;
|
private final ShopManageGroupService shopManageGroupService;
|
||||||
@@ -79,7 +79,7 @@ public class QueryAsinService {
|
|||||||
List<QueryAsinEntity> rows = queryAsinMapper
|
List<QueryAsinEntity> rows = queryAsinMapper
|
||||||
.selectList(query.last("LIMIT " + ((safePage - 1) * safePageSize) + ", " + safePageSize));
|
.selectList(query.last("LIMIT " + ((safePage - 1) * safePageSize) + ", " + safePageSize));
|
||||||
|
|
||||||
Map<Long, String> groupNameById = buildGroupNameMap(rows.stream()
|
Map<Long, String> groupNameById = shopManageGroupService.buildGroupNameMap(rows.stream()
|
||||||
.map(QueryAsinEntity::getGroupId)
|
.map(QueryAsinEntity::getGroupId)
|
||||||
.filter(id -> id != null && id > 0)
|
.filter(id -> id != null && id > 0)
|
||||||
.distinct()
|
.distinct()
|
||||||
@@ -316,22 +316,6 @@ public class QueryAsinService {
|
|||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<Long, String> buildGroupNameMap(List<Long> groupIds) {
|
|
||||||
if (groupIds == null || groupIds.isEmpty()) {
|
|
||||||
return Map.of();
|
|
||||||
}
|
|
||||||
LinkedHashMap<Long, String> map = new LinkedHashMap<>();
|
|
||||||
for (Long groupId : groupIds) {
|
|
||||||
try {
|
|
||||||
ShopManageGroupEntity group = shopManageGroupService.getById(groupId);
|
|
||||||
map.put(groupId, group.getGroupName());
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
map.put(groupId, "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Set<String> normalizeCountries(List<String> countries) {
|
private Set<String> normalizeCountries(List<String> countries) {
|
||||||
LinkedHashSet<String> normalized = new LinkedHashSet<>();
|
LinkedHashSet<String> normalized = new LinkedHashSet<>();
|
||||||
if (countries != null) {
|
if (countries != null) {
|
||||||
@@ -384,11 +368,11 @@ public class QueryAsinService {
|
|||||||
return upper;
|
return upper;
|
||||||
}
|
}
|
||||||
return switch (normalizeHeaderKey(normalized)) {
|
return switch (normalizeHeaderKey(normalized)) {
|
||||||
case "\u5fb7\u56fd", "\u5fb7", "de", "germany", "german", "deutschland" -> "DE";
|
case "德国", "德", "de", "germany", "german", "deutschland" -> "DE";
|
||||||
case "\u82f1\u56fd", "\u82f1", "uk", "gb", "greatbritain", "britain", "unitedkingdom" -> "UK";
|
case "英国", "英", "uk", "gb", "greatbritain", "britain", "unitedkingdom" -> "UK";
|
||||||
case "\u6cd5\u56fd", "\u6cd5", "fr", "france", "french" -> "FR";
|
case "法国", "法", "fr", "france", "french" -> "FR";
|
||||||
case "\u610f\u5927\u5229", "\u610f", "it", "italy", "italian" -> "IT";
|
case "意大利", "意", "it", "italy", "italian" -> "IT";
|
||||||
case "\u897f\u73ed\u7259", "\u897f", "es", "spain", "spanish", "espana" -> "ES";
|
case "西班牙", "西", "es", "spain", "spanish", "espana" -> "ES";
|
||||||
default -> "";
|
default -> "";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -860,11 +844,11 @@ public class QueryAsinService {
|
|||||||
if (index != null) {
|
if (index != null) {
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
index = firstHeader("groupname", "\u5206\u7ec4id", "\u5206\u7ec4");
|
index = firstHeader("groupname", "分组id", "分组");
|
||||||
if (index != null) {
|
if (index != null) {
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
return findHeaderByPredicate(key -> key.contains("\u5206\u7ec4") || key.contains("group"));
|
return findHeaderByPredicate(key -> key.contains("分组") || key.contains("group"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Integer resolveShopHeaderIndex() {
|
private Integer resolveShopHeaderIndex() {
|
||||||
@@ -873,13 +857,13 @@ public class QueryAsinService {
|
|||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
index = firstHeader(
|
index = firstHeader(
|
||||||
"\u5e97\u94fa\u540d", "\u5e97\u94fa\u540d\u79f0", "\u5e97\u94fa\u8d26\u53f7", "\u8d26\u53f7",
|
"店铺名", "店铺名称", "店铺账号", "账号",
|
||||||
"account", "selleraccount", "storeaccount", "storename");
|
"account", "selleraccount", "storeaccount", "storename");
|
||||||
if (index != null) {
|
if (index != null) {
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
return findHeaderByPredicate(key ->
|
return findHeaderByPredicate(key ->
|
||||||
(key.contains("\u5e97\u94fa") && (key.contains("\u540d") || key.contains("\u8d26\u53f7")))
|
(key.contains("店铺") && (key.contains("名") || key.contains("账号")))
|
||||||
|| (key.contains("shop") && (key.contains("name") || key.contains("account")))
|
|| (key.contains("shop") && (key.contains("name") || key.contains("account")))
|
||||||
|| key.contains("selleraccount")
|
|| key.contains("selleraccount")
|
||||||
|| key.contains("storeaccount"));
|
|| key.contains("storeaccount"));
|
||||||
@@ -891,32 +875,32 @@ public class QueryAsinService {
|
|||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
return switch (country) {
|
return switch (country) {
|
||||||
case "DE" -> firstHeader("\u5fb7\u56fd", "\u5fb7", "\u5fb7\u56fdasin");
|
case "DE" -> firstHeader("德国", "德", "德国asin");
|
||||||
case "UK" -> firstHeader("\u82f1\u56fd", "\u82f1", "\u82f1\u56fdasin");
|
case "UK" -> firstHeader("英国", "英", "英国asin");
|
||||||
case "FR" -> firstHeader("\u6cd5\u56fd", "\u6cd5", "\u6cd5\u56fdasin");
|
case "FR" -> firstHeader("法国", "法", "法国asin");
|
||||||
case "IT" -> firstHeader("\u610f\u5927\u5229", "\u610f", "\u610f\u5927\u5229asin");
|
case "IT" -> firstHeader("意大利", "意", "意大利asin");
|
||||||
case "ES" -> firstHeader("\u897f\u73ed\u7259", "\u897f", "\u897f\u73ed\u7259asin");
|
case "ES" -> firstHeader("西班牙", "西", "西班牙asin");
|
||||||
default -> null;
|
default -> null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private Integer findSingleCountryIndex() {
|
private Integer findSingleCountryIndex() {
|
||||||
Integer index = firstHeader(
|
Integer index = firstHeader(
|
||||||
"\u4e2d\u6587\u56fd\u5bb6\u540d", "\u56fd\u5bb6", "\u56fd\u5bb6\u540d", "\u56fd\u5bb6\u7ad9\u70b9",
|
"中文国家名", "国家", "国家名", "国家站点",
|
||||||
"\u7ad9\u70b9", "country", "countryname", "marketplace", "site");
|
"站点", "country", "countryname", "marketplace", "site");
|
||||||
if (index != null) {
|
if (index != null) {
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
return findHeaderByPredicate(key ->
|
return findHeaderByPredicate(key ->
|
||||||
key.contains("\u56fd\u5bb6")
|
key.contains("国家")
|
||||||
|| key.contains("\u7ad9\u70b9")
|
|| key.contains("站点")
|
||||||
|| key.contains("country")
|
|| key.contains("country")
|
||||||
|| key.contains("marketplace")
|
|| key.contains("marketplace")
|
||||||
|| key.equals("site"));
|
|| key.equals("site"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Integer findSingleAsinIndex() {
|
private Integer findSingleAsinIndex() {
|
||||||
return firstHeader("asin", "\u67e5\u8be2asin", "\u4ea7\u54c1asin", "\u94fe\u63a5asin");
|
return firstHeader("asin", "查询asin", "产品asin", "链接asin");
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasWideCountryColumns() {
|
private boolean hasWideCountryColumns() {
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ public class ShopManageGroupService {
|
|||||||
public List<ShopManageGroupItemVo> list(Long createdById) {
|
public List<ShopManageGroupItemVo> list(Long createdById) {
|
||||||
return toVoList(groupMapper.selectList(new LambdaQueryWrapper<ShopManageGroupEntity>()
|
return toVoList(groupMapper.selectList(new LambdaQueryWrapper<ShopManageGroupEntity>()
|
||||||
.eq(createdById != null && createdById > 0, ShopManageGroupEntity::getCreatedById, createdById)
|
.eq(createdById != null && createdById > 0, ShopManageGroupEntity::getCreatedById, createdById)
|
||||||
|
.or(createdById != null && createdById > 0, wrapper -> wrapper.eq(ShopManageGroupEntity::getUserId, createdById))
|
||||||
.orderByAsc(ShopManageGroupEntity::getId)));
|
.orderByAsc(ShopManageGroupEntity::getId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,40 +75,44 @@ public class ShopManageGroupService {
|
|||||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||||
}
|
}
|
||||||
Long normalizedOperatorId = normalizePositiveId(operatorId, "操作人不能为空");
|
Long normalizedOperatorId = normalizePositiveId(operatorId, "操作人不能为空");
|
||||||
LinkedHashSet<Long> groupIds = groupMapper.selectList(new LambdaQueryWrapper<ShopManageGroupEntity>()
|
return groupMapper.selectAccessibleGroupIds(normalizedOperatorId)
|
||||||
.select(ShopManageGroupEntity::getId)
|
|
||||||
.eq(ShopManageGroupEntity::getCreatedById, normalizedOperatorId))
|
|
||||||
.stream()
|
.stream()
|
||||||
.map(ShopManageGroupEntity::getId)
|
|
||||||
.filter(id -> id != null && id > 0)
|
.filter(id -> id != null && id > 0)
|
||||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||||
List<Long> memberGroupIds = groupMemberMapper.selectList(new LambdaQueryWrapper<ShopManageGroupMemberEntity>()
|
}
|
||||||
.select(ShopManageGroupMemberEntity::getGroupId)
|
|
||||||
.eq(ShopManageGroupMemberEntity::getUserId, normalizedOperatorId))
|
public Set<Long> listAccessibleCreatorUserIds(Long operatorId, boolean superAdmin) {
|
||||||
.stream()
|
if (superAdmin) {
|
||||||
.map(ShopManageGroupMemberEntity::getGroupId)
|
return Set.of();
|
||||||
.filter(id -> id != null && id > 0)
|
|
||||||
.toList();
|
|
||||||
groupIds.addAll(memberGroupIds);
|
|
||||||
if (!memberGroupIds.isEmpty()) {
|
|
||||||
LinkedHashSet<Long> leaderIds = groupMapper.selectList(new LambdaQueryWrapper<ShopManageGroupEntity>()
|
|
||||||
.select(ShopManageGroupEntity::getCreatedById)
|
|
||||||
.in(ShopManageGroupEntity::getId, memberGroupIds))
|
|
||||||
.stream()
|
|
||||||
.map(ShopManageGroupEntity::getCreatedById)
|
|
||||||
.filter(id -> id != null && id > 0)
|
|
||||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
|
||||||
if (!leaderIds.isEmpty()) {
|
|
||||||
groupIds.addAll(groupMapper.selectList(new LambdaQueryWrapper<ShopManageGroupEntity>()
|
|
||||||
.select(ShopManageGroupEntity::getId)
|
|
||||||
.in(ShopManageGroupEntity::getCreatedById, leaderIds))
|
|
||||||
.stream()
|
|
||||||
.map(ShopManageGroupEntity::getId)
|
|
||||||
.filter(id -> id != null && id > 0)
|
|
||||||
.toList());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return groupIds;
|
Long normalizedOperatorId = normalizePositiveId(operatorId, "操作人不能为空");
|
||||||
|
return groupMapper.selectAccessibleCreatorUserIds(normalizedOperatorId)
|
||||||
|
.stream()
|
||||||
|
.filter(id -> id != null && id > 0)
|
||||||
|
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<Long, String> buildGroupNameMap(List<Long> groupIds) {
|
||||||
|
if (groupIds == null || groupIds.isEmpty()) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
List<Long> normalizedGroupIds = groupIds.stream()
|
||||||
|
.filter(id -> id != null && id > 0)
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
if (normalizedGroupIds.isEmpty()) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
return groupMapper.selectList(new LambdaQueryWrapper<ShopManageGroupEntity>()
|
||||||
|
.select(ShopManageGroupEntity::getId, ShopManageGroupEntity::getGroupName)
|
||||||
|
.in(ShopManageGroupEntity::getId, normalizedGroupIds))
|
||||||
|
.stream()
|
||||||
|
.filter(group -> group.getId() != null)
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
ShopManageGroupEntity::getId,
|
||||||
|
group -> normalizeBlank(group.getGroupName()),
|
||||||
|
(left, right) -> left,
|
||||||
|
LinkedHashMap::new));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -207,8 +212,9 @@ public class ShopManageGroupService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Long normalizedOperatorId = normalizePositiveId(operatorId, "操作人不能为空");
|
Long normalizedOperatorId = normalizePositiveId(operatorId, "操作人不能为空");
|
||||||
boolean createdBySelf = entity.getCreatedById() != null && entity.getCreatedById().equals(normalizedOperatorId);
|
boolean leaderBySelf = entity.getCreatedById() != null && entity.getCreatedById().equals(normalizedOperatorId);
|
||||||
if (!createdBySelf) {
|
boolean legacyLeaderBySelf = entity.getUserId() != null && entity.getUserId().equals(normalizedOperatorId);
|
||||||
|
if (!leaderBySelf && !legacyLeaderBySelf) {
|
||||||
throw new BusinessException("只有组长才能维护分组");
|
throw new BusinessException("只有组长才能维护分组");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -332,8 +338,9 @@ public class ShopManageGroupService {
|
|||||||
|
|
||||||
LinkedHashSet<Long> userIds = new LinkedHashSet<>();
|
LinkedHashSet<Long> userIds = new LinkedHashSet<>();
|
||||||
for (ShopManageGroupEntity group : groups) {
|
for (ShopManageGroupEntity group : groups) {
|
||||||
if (group.getCreatedById() != null && group.getCreatedById() > 0) {
|
Long leaderUserId = resolveGroupLeaderUserId(group);
|
||||||
userIds.add(group.getCreatedById());
|
if (leaderUserId != null && leaderUserId > 0) {
|
||||||
|
userIds.add(leaderUserId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (List<ShopManageGroupMemberEntity> members : membersByGroupId.values()) {
|
for (List<ShopManageGroupMemberEntity> members : membersByGroupId.values()) {
|
||||||
@@ -355,8 +362,9 @@ public class ShopManageGroupService {
|
|||||||
ShopManageGroupItemVo vo = new ShopManageGroupItemVo();
|
ShopManageGroupItemVo vo = new ShopManageGroupItemVo();
|
||||||
vo.setId(entity.getId());
|
vo.setId(entity.getId());
|
||||||
vo.setGroupName(entity.getGroupName());
|
vo.setGroupName(entity.getGroupName());
|
||||||
vo.setLeaderUserId(entity.getCreatedById());
|
Long leaderUserId = resolveGroupLeaderUserId(entity);
|
||||||
AdminUserEntity leaderUser = entity.getCreatedById() == null ? null : userById.get(entity.getCreatedById());
|
vo.setLeaderUserId(leaderUserId);
|
||||||
|
AdminUserEntity leaderUser = leaderUserId == null ? null : userById.get(leaderUserId);
|
||||||
vo.setLeaderUsername(leaderUser == null ? "" : normalizeBlank(leaderUser.getUsername()));
|
vo.setLeaderUsername(leaderUser == null ? "" : normalizeBlank(leaderUser.getUsername()));
|
||||||
|
|
||||||
List<ShopManageGroupMemberEntity> memberEntities = membersByGroupId.getOrDefault(entity.getId(), Collections.emptyList());
|
List<ShopManageGroupMemberEntity> memberEntities = membersByGroupId.getOrDefault(entity.getId(), Collections.emptyList());
|
||||||
@@ -380,4 +388,14 @@ public class ShopManageGroupService {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Long resolveGroupLeaderUserId(ShopManageGroupEntity entity) {
|
||||||
|
if (entity == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (entity.getCreatedById() != null && entity.getCreatedById() > 0) {
|
||||||
|
return entity.getCreatedById();
|
||||||
|
}
|
||||||
|
return entity.getUserId();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -33,6 +32,7 @@ public class ShopManageService {
|
|||||||
long safePageSize = Math.min(Math.max(pageSize, 1), 100);
|
long safePageSize = Math.min(Math.max(pageSize, 1), 100);
|
||||||
String safeShopName = shopName == null ? "" : shopName.trim();
|
String safeShopName = shopName == null ? "" : shopName.trim();
|
||||||
Set<Long> accessibleGroupIds = superAdmin ? Set.of() : shopManageGroupService.listAccessibleGroupIds(operatorId, false);
|
Set<Long> accessibleGroupIds = superAdmin ? Set.of() : shopManageGroupService.listAccessibleGroupIds(operatorId, false);
|
||||||
|
Set<Long> accessibleCreatorUserIds = superAdmin ? Set.of() : shopManageGroupService.listAccessibleCreatorUserIds(operatorId, false);
|
||||||
|
|
||||||
LambdaQueryWrapper<ShopManageEntity> query = new LambdaQueryWrapper<ShopManageEntity>()
|
LambdaQueryWrapper<ShopManageEntity> query = new LambdaQueryWrapper<ShopManageEntity>()
|
||||||
.eq(groupId != null && groupId > 0, ShopManageEntity::getGroupId, groupId)
|
.eq(groupId != null && groupId > 0, ShopManageEntity::getGroupId, groupId)
|
||||||
@@ -40,20 +40,30 @@ public class ShopManageService {
|
|||||||
.orderByDesc(ShopManageEntity::getId);
|
.orderByDesc(ShopManageEntity::getId);
|
||||||
|
|
||||||
if (!superAdmin) {
|
if (!superAdmin) {
|
||||||
Long normalizedOperatorId = normalizePositiveId(operatorId, "操作人不能为空");
|
normalizePositiveId(operatorId, "操作人不能为空");
|
||||||
query.and(wrapper -> {
|
if (accessibleGroupIds.isEmpty() && accessibleCreatorUserIds.isEmpty()) {
|
||||||
wrapper.eq(ShopManageEntity::getCreatedById, normalizedOperatorId);
|
query.eq(ShopManageEntity::getId, -1L);
|
||||||
if (!accessibleGroupIds.isEmpty()) {
|
} else {
|
||||||
wrapper.or().in(ShopManageEntity::getGroupId, accessibleGroupIds);
|
query.and(wrapper -> {
|
||||||
}
|
boolean hasCreatorFilter = !accessibleCreatorUserIds.isEmpty();
|
||||||
});
|
if (hasCreatorFilter) {
|
||||||
|
wrapper.in(ShopManageEntity::getCreatedById, accessibleCreatorUserIds);
|
||||||
|
}
|
||||||
|
if (!accessibleGroupIds.isEmpty()) {
|
||||||
|
if (hasCreatorFilter) {
|
||||||
|
wrapper.or();
|
||||||
|
}
|
||||||
|
wrapper.in(ShopManageEntity::getGroupId, accessibleGroupIds);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Long total = shopManageMapper.selectCount(query);
|
Long total = shopManageMapper.selectCount(query);
|
||||||
List<ShopManageEntity> rows = shopManageMapper
|
List<ShopManageEntity> rows = shopManageMapper
|
||||||
.selectList(query.last("LIMIT " + ((safePage - 1) * safePageSize) + ", " + safePageSize));
|
.selectList(query.last("LIMIT " + ((safePage - 1) * safePageSize) + ", " + safePageSize));
|
||||||
|
|
||||||
Map<Long, String> groupNameById = buildGroupNameMap(rows.stream()
|
Map<Long, String> groupNameById = shopManageGroupService.buildGroupNameMap(rows.stream()
|
||||||
.map(ShopManageEntity::getGroupId)
|
.map(ShopManageEntity::getGroupId)
|
||||||
.filter(id -> id != null && id > 0)
|
.filter(id -> id != null && id > 0)
|
||||||
.distinct()
|
.distinct()
|
||||||
@@ -190,22 +200,6 @@ public class ShopManageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<Long, String> buildGroupNameMap(List<Long> groupIds) {
|
|
||||||
if (groupIds == null || groupIds.isEmpty()) {
|
|
||||||
return Map.of();
|
|
||||||
}
|
|
||||||
LinkedHashMap<Long, String> map = new LinkedHashMap<>();
|
|
||||||
for (Long groupId : groupIds) {
|
|
||||||
try {
|
|
||||||
ShopManageGroupEntity group = shopManageGroupService.getById(groupId);
|
|
||||||
map.put(groupId, group.getGroupName());
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
map.put(groupId, "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ShopManageItemVo toItemVo(ShopManageEntity entity, String groupName) {
|
private ShopManageItemVo toItemVo(ShopManageEntity entity, String groupName) {
|
||||||
ShopManageItemVo vo = new ShopManageItemVo();
|
ShopManageItemVo vo = new ShopManageItemVo();
|
||||||
vo.setId(entity.getId());
|
vo.setId(entity.getId());
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ public class SkipPriceAsinService {
|
|||||||
List<SkipPriceAsinEntity> rows = skipPriceAsinMapper
|
List<SkipPriceAsinEntity> rows = skipPriceAsinMapper
|
||||||
.selectList(query.last("LIMIT " + ((safePage - 1) * safePageSize) + ", " + safePageSize));
|
.selectList(query.last("LIMIT " + ((safePage - 1) * safePageSize) + ", " + safePageSize));
|
||||||
|
|
||||||
Map<Long, String> groupNameById = buildGroupNameMap(rows.stream()
|
Map<Long, String> groupNameById = shopManageGroupService.buildGroupNameMap(rows.stream()
|
||||||
.map(SkipPriceAsinEntity::getGroupId)
|
.map(SkipPriceAsinEntity::getGroupId)
|
||||||
.filter(id -> id != null && id > 0)
|
.filter(id -> id != null && id > 0)
|
||||||
.distinct()
|
.distinct()
|
||||||
@@ -174,22 +174,6 @@ public class SkipPriceAsinService {
|
|||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<Long, String> buildGroupNameMap(List<Long> groupIds) {
|
|
||||||
if (groupIds == null || groupIds.isEmpty()) {
|
|
||||||
return Map.of();
|
|
||||||
}
|
|
||||||
LinkedHashMap<Long, String> map = new LinkedHashMap<>();
|
|
||||||
for (Long groupId : groupIds) {
|
|
||||||
try {
|
|
||||||
ShopManageGroupEntity group = shopManageGroupService.getById(groupId);
|
|
||||||
map.put(groupId, group.getGroupName());
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
map.put(groupId, "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Set<String> normalizeCountries(List<String> countries) {
|
private Set<String> normalizeCountries(List<String> countries) {
|
||||||
LinkedHashSet<String> normalized = new LinkedHashSet<>();
|
LinkedHashSet<String> normalized = new LinkedHashSet<>();
|
||||||
if (countries != null) {
|
if (countries != null) {
|
||||||
|
|||||||
@@ -132,6 +132,21 @@ public class ShopMatchTaskService {
|
|||||||
return tasks;
|
return tasks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<FileTaskEntity> selectTaskStatusesByIdsInBatches(List<Long> taskIds) {
|
||||||
|
List<FileTaskEntity> tasks = new ArrayList<>();
|
||||||
|
if (taskIds == null || taskIds.isEmpty()) {
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
int batchSize = Math.max(1, taskPressureProperties.getDbSelectBatchSize());
|
||||||
|
for (int start = 0; start < taskIds.size(); start += batchSize) {
|
||||||
|
int end = Math.min(start + batchSize, taskIds.size());
|
||||||
|
tasks.addAll(fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
|
||||||
|
.select(FileTaskEntity::getId, FileTaskEntity::getStatus)
|
||||||
|
.in(FileTaskEntity::getId, taskIds.subList(start, end))));
|
||||||
|
}
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
|
||||||
public ProductRiskDashboardVo dashboard(Long userId) {
|
public ProductRiskDashboardVo dashboard(Long userId) {
|
||||||
if (userId == null || userId <= 0) {
|
if (userId == null || userId <= 0) {
|
||||||
throw new BusinessException("user_id 不合法");
|
throw new BusinessException("user_id 不合法");
|
||||||
@@ -174,7 +189,7 @@ public class ShopMatchTaskService {
|
|||||||
.distinct()
|
.distinct()
|
||||||
.toList();
|
.toList();
|
||||||
if (!taskIds.isEmpty()) {
|
if (!taskIds.isEmpty()) {
|
||||||
for (FileTaskEntity task : selectTasksByIdsInBatches(taskIds)) {
|
for (FileTaskEntity task : selectTaskStatusesByIdsInBatches(taskIds)) {
|
||||||
if (task != null) {
|
if (task != null) {
|
||||||
statusByTaskId.put(task.getId(), task.getStatus());
|
statusByTaskId.put(task.getId(), task.getStatus());
|
||||||
}
|
}
|
||||||
@@ -465,7 +480,6 @@ public class ShopMatchTaskService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public void submitResult(Long taskId, ShopMatchSubmitResultRequest request) {
|
public void submitResult(Long taskId, ShopMatchSubmitResultRequest request) {
|
||||||
if (taskId == null || taskId <= 0) {
|
if (taskId == null || taskId <= 0) {
|
||||||
throw new BusinessException("taskId 不合法");
|
throw new BusinessException("taskId 不合法");
|
||||||
@@ -540,7 +554,6 @@ public class ShopMatchTaskService {
|
|||||||
updateTaskStatusFromLatestRows(task, latest);
|
updateTaskStatusFromLatestRows(task, latest);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public boolean tryFinalizeTask(Long taskId, boolean fromCompensation) {
|
public boolean tryFinalizeTask(Long taskId, boolean fromCompensation) {
|
||||||
if (taskId == null || taskId <= 0) {
|
if (taskId == null || taskId <= 0) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -456,8 +456,8 @@ public class SplitRunService {
|
|||||||
if (value == null) {
|
if (value == null) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
return value.replace("\ufeff", "")
|
return value.replace(String.valueOf((char) 0xFEFF), "")
|
||||||
.replace("\u3000", " ")
|
.replace((char) 0x3000, ' ')
|
||||||
.replace("\r\n", " ")
|
.replace("\r\n", " ")
|
||||||
.replace("\r", " ")
|
.replace("\r", " ")
|
||||||
.replace("\n", " ")
|
.replace("\n", " ")
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ public class TaskFileJobService {
|
|||||||
private final TaskFileJobMapper taskFileJobMapper;
|
private final TaskFileJobMapper taskFileJobMapper;
|
||||||
private final ApplicationEventPublisher applicationEventPublisher;
|
private final ApplicationEventPublisher applicationEventPublisher;
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public TaskFileJobEntity enqueueAssembleResult(Long taskId, String moduleType, Long resultId, String scopeKey) {
|
public TaskFileJobEntity enqueueAssembleResult(Long taskId, String moduleType, Long resultId, String scopeKey) {
|
||||||
if (taskId == null || taskId <= 0 || moduleType == null || moduleType.isBlank() || resultId == null || resultId <= 0) {
|
if (taskId == null || taskId <= 0 || moduleType == null || moduleType.isBlank() || resultId == null || resultId <= 0) {
|
||||||
return null;
|
return null;
|
||||||
@@ -146,7 +145,6 @@ public class TaskFileJobService {
|
|||||||
return reset;
|
return reset;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public boolean markRunning(Long jobId) {
|
public boolean markRunning(Long jobId) {
|
||||||
if (jobId == null || jobId <= 0) {
|
if (jobId == null || jobId <= 0) {
|
||||||
return false;
|
return false;
|
||||||
@@ -158,7 +156,6 @@ public class TaskFileJobService {
|
|||||||
.set(TaskFileJobEntity::getUpdatedAt, LocalDateTime.now())) > 0;
|
.set(TaskFileJobEntity::getUpdatedAt, LocalDateTime.now())) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public void touchRunning(Long jobId) {
|
public void touchRunning(Long jobId) {
|
||||||
if (jobId == null || jobId <= 0) {
|
if (jobId == null || jobId <= 0) {
|
||||||
return;
|
return;
|
||||||
@@ -169,7 +166,6 @@ public class TaskFileJobService {
|
|||||||
.set(TaskFileJobEntity::getUpdatedAt, LocalDateTime.now()));
|
.set(TaskFileJobEntity::getUpdatedAt, LocalDateTime.now()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public void markSuccess(TaskFileJobEntity job, String resultFileUrl) {
|
public void markSuccess(TaskFileJobEntity job, String resultFileUrl) {
|
||||||
taskFileJobMapper.update(null, new LambdaUpdateWrapper<TaskFileJobEntity>()
|
taskFileJobMapper.update(null, new LambdaUpdateWrapper<TaskFileJobEntity>()
|
||||||
.eq(TaskFileJobEntity::getId, job.getId())
|
.eq(TaskFileJobEntity::getId, job.getId())
|
||||||
@@ -180,7 +176,6 @@ public class TaskFileJobService {
|
|||||||
.set(TaskFileJobEntity::getFinishedAt, LocalDateTime.now()));
|
.set(TaskFileJobEntity::getFinishedAt, LocalDateTime.now()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public void markFailed(TaskFileJobEntity job, String message) {
|
public void markFailed(TaskFileJobEntity job, String message) {
|
||||||
int retryCount = job.getRetryCount() == null ? 0 : job.getRetryCount() + 1;
|
int retryCount = job.getRetryCount() == null ? 0 : job.getRetryCount() + 1;
|
||||||
taskFileJobMapper.update(null, new LambdaUpdateWrapper<TaskFileJobEntity>()
|
taskFileJobMapper.update(null, new LambdaUpdateWrapper<TaskFileJobEntity>()
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ public class TaskResultItemService {
|
|||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final TransientPayloadStorageService transientPayloadStorageService;
|
private final TransientPayloadStorageService transientPayloadStorageService;
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public void replaceResultSnapshot(Long taskId, String moduleType, Long resultId, String scopeKey, Object snapshot) {
|
public void replaceResultSnapshot(Long taskId, String moduleType, Long resultId, String scopeKey, Object snapshot) {
|
||||||
if (taskId == null || taskId <= 0 || isBlank(moduleType) || resultId == null || resultId <= 0 || snapshot == null) {
|
if (taskId == null || taskId <= 0 || isBlank(moduleType) || resultId == null || resultId <= 0 || snapshot == null) {
|
||||||
return;
|
return;
|
||||||
@@ -35,7 +34,6 @@ public class TaskResultItemService {
|
|||||||
upsert(taskId, moduleType, resultId, normalizedScopeKey, itemKey, null, null, null, snapshot);
|
upsert(taskId, moduleType, resultId, normalizedScopeKey, itemKey, null, null, null, snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public void replaceTaskSnapshots(Long taskId, String moduleType, List<? extends Object> snapshots, SnapshotKeyResolver resolver) {
|
public void replaceTaskSnapshots(Long taskId, String moduleType, List<? extends Object> snapshots, SnapshotKeyResolver resolver) {
|
||||||
if (taskId == null || taskId <= 0 || isBlank(moduleType) || snapshots == null) {
|
if (taskId == null || taskId <= 0 || isBlank(moduleType) || snapshots == null) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ public class TaskResultPayloadService {
|
|||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final TransientPayloadStorageService transientPayloadStorageService;
|
private final TransientPayloadStorageService transientPayloadStorageService;
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public void saveLatest(Long taskId, String moduleType, String scopeKey, Object payload) {
|
public void saveLatest(Long taskId, String moduleType, String scopeKey, Object payload) {
|
||||||
if (taskId == null || taskId <= 0 || isBlank(moduleType) || isBlank(scopeKey) || payload == null) {
|
if (taskId == null || taskId <= 0 || isBlank(moduleType) || isBlank(scopeKey) || payload == null) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ public boolean isIpWhitelistError(BusinessException ex) {
|
|||||||
if (value == null) {
|
if (value == null) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
return value.replace("\u3000", " ")
|
return value.replace((char) 0x3000, ' ')
|
||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -357,7 +357,7 @@ public class ZiniaoShopIndexService {
|
|||||||
if (value == null) {
|
if (value == null) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
return value.replace("\u3000", " ").trim();
|
return value.replace((char) 0x3000, ' ').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
private ZiniaoShopMatchResultVo pendingResult(String message) {
|
private ZiniaoShopMatchResultVo pendingResult(String message) {
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ spring:
|
|||||||
maximum-pool-size: ${AIIMAGE_DB_POOL_MAX_SIZE:30}
|
maximum-pool-size: ${AIIMAGE_DB_POOL_MAX_SIZE:30}
|
||||||
minimum-idle: ${AIIMAGE_DB_POOL_MIN_IDLE:5}
|
minimum-idle: ${AIIMAGE_DB_POOL_MIN_IDLE:5}
|
||||||
connection-timeout: ${AIIMAGE_DB_POOL_CONNECTION_TIMEOUT_MS:30000}
|
connection-timeout: ${AIIMAGE_DB_POOL_CONNECTION_TIMEOUT_MS:30000}
|
||||||
|
validation-timeout: ${AIIMAGE_DB_POOL_VALIDATION_TIMEOUT_MS:5000}
|
||||||
|
idle-timeout: ${AIIMAGE_DB_POOL_IDLE_TIMEOUT_MS:600000}
|
||||||
|
max-lifetime: ${AIIMAGE_DB_POOL_MAX_LIFETIME_MS:1500000}
|
||||||
|
keepalive-time: ${AIIMAGE_DB_POOL_KEEPALIVE_TIME_MS:120000}
|
||||||
leak-detection-threshold: ${AIIMAGE_DB_POOL_LEAK_DETECT_MS:0}
|
leak-detection-threshold: ${AIIMAGE_DB_POOL_LEAK_DETECT_MS:0}
|
||||||
data:
|
data:
|
||||||
redis:
|
redis:
|
||||||
@@ -54,7 +58,7 @@ mybatis-plus:
|
|||||||
mapper-locations: classpath*:mapper/**/*.xml
|
mapper-locations: classpath*:mapper/**/*.xml
|
||||||
configuration:
|
configuration:
|
||||||
map-underscore-to-camel-case: true
|
map-underscore-to-camel-case: true
|
||||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
|
||||||
|
|
||||||
springdoc:
|
springdoc:
|
||||||
api-docs:
|
api-docs:
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
SET @db_name = DATABASE();
|
||||||
|
|
||||||
|
SET @idx_exists := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @db_name
|
||||||
|
AND TABLE_NAME = 'biz_shop_manage'
|
||||||
|
AND INDEX_NAME = 'idx_group_id_id'
|
||||||
|
);
|
||||||
|
SET @sql := IF(@idx_exists = 0,
|
||||||
|
'ALTER TABLE biz_shop_manage ADD INDEX idx_group_id_id (group_id, id)',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @idx_exists := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @db_name
|
||||||
|
AND TABLE_NAME = 'biz_shop_manage'
|
||||||
|
AND INDEX_NAME = 'idx_created_by_id_id'
|
||||||
|
);
|
||||||
|
SET @sql := IF(@idx_exists = 0,
|
||||||
|
'ALTER TABLE biz_shop_manage ADD INDEX idx_created_by_id_id (created_by_id, id)',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @idx_exists := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @db_name
|
||||||
|
AND TABLE_NAME = 'biz_skip_price_asin'
|
||||||
|
AND INDEX_NAME = 'idx_group_id_id'
|
||||||
|
);
|
||||||
|
SET @sql := IF(@idx_exists = 0,
|
||||||
|
'ALTER TABLE biz_skip_price_asin ADD INDEX idx_group_id_id (group_id, id)',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @idx_exists := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @db_name
|
||||||
|
AND TABLE_NAME = 'biz_query_asin'
|
||||||
|
AND INDEX_NAME = 'idx_group_id_id'
|
||||||
|
);
|
||||||
|
SET @sql := IF(@idx_exists = 0,
|
||||||
|
'ALTER TABLE biz_query_asin ADD INDEX idx_group_id_id (group_id, id)',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
SET @db_name = DATABASE();
|
||||||
|
|
||||||
|
SET @idx_exists := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @db_name
|
||||||
|
AND TABLE_NAME = 'columns'
|
||||||
|
AND INDEX_NAME = 'idx_menu_type_sort_id'
|
||||||
|
);
|
||||||
|
SET @sql := IF(@idx_exists = 0,
|
||||||
|
'ALTER TABLE columns ADD INDEX idx_menu_type_sort_id (menu_type, sort_order, id)',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @idx_exists := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @db_name
|
||||||
|
AND TABLE_NAME = 'user_column_permission'
|
||||||
|
AND INDEX_NAME = 'idx_user_column'
|
||||||
|
);
|
||||||
|
SET @sql := IF(@idx_exists = 0,
|
||||||
|
'ALTER TABLE user_column_permission ADD INDEX idx_user_column (user_id, column_id)',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
SET @db_name = DATABASE();
|
||||||
|
|
||||||
|
SET @idx_exists := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @db_name
|
||||||
|
AND TABLE_NAME = 'biz_file_result'
|
||||||
|
AND INDEX_NAME = 'idx_file_result_task_module_id'
|
||||||
|
);
|
||||||
|
SET @sql := IF(@idx_exists = 0,
|
||||||
|
'ALTER TABLE biz_file_result ADD INDEX idx_file_result_task_module_id (task_id, module_type, id)',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @idx_exists := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @db_name
|
||||||
|
AND TABLE_NAME = 'biz_task_file_job'
|
||||||
|
AND INDEX_NAME = 'idx_file_job_status_retry_updated'
|
||||||
|
);
|
||||||
|
SET @sql := IF(@idx_exists = 0,
|
||||||
|
'ALTER TABLE biz_task_file_job ADD INDEX idx_file_job_status_retry_updated (status, retry_count, updated_at)',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
Binary file not shown.
Binary file not shown.
@@ -4,9 +4,11 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import threading
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from flask import Blueprint, request, jsonify, session, current_app
|
from requests.adapters import HTTPAdapter
|
||||||
|
from flask import Blueprint, request, jsonify, session, current_app, g
|
||||||
|
|
||||||
import pymysql
|
import pymysql
|
||||||
from werkzeug.security import generate_password_hash
|
from werkzeug.security import generate_password_hash
|
||||||
@@ -43,6 +45,9 @@ except ImportError:
|
|||||||
backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://127.0.0.1:18080').rstrip('/')
|
backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://127.0.0.1:18080').rstrip('/')
|
||||||
|
|
||||||
admin_api = Blueprint('admin_api', __name__, url_prefix='/api/admin')
|
admin_api = Blueprint('admin_api', __name__, url_prefix='/api/admin')
|
||||||
|
_backend_java_session_local = threading.local()
|
||||||
|
_column_sort_schema_checked = False
|
||||||
|
_column_sort_schema_lock = threading.Lock()
|
||||||
|
|
||||||
ADMIN_MENU_ACCESS_CONFIG = {
|
ADMIN_MENU_ACCESS_CONFIG = {
|
||||||
'dedupe-total-data': {
|
'dedupe-total-data': {
|
||||||
@@ -103,10 +108,22 @@ def _safe_version_key(version):
|
|||||||
return s or 'unknown'
|
return s or 'unknown'
|
||||||
|
|
||||||
|
|
||||||
|
def _get_backend_java_session():
|
||||||
|
http_session = getattr(_backend_java_session_local, 'session', None)
|
||||||
|
if http_session is None:
|
||||||
|
http_session = requests.Session()
|
||||||
|
adapter = HTTPAdapter(pool_connections=8, pool_maxsize=32, max_retries=0)
|
||||||
|
http_session.mount('http://', adapter)
|
||||||
|
http_session.mount('https://', adapter)
|
||||||
|
_backend_java_session_local.session = http_session
|
||||||
|
return http_session
|
||||||
|
|
||||||
|
|
||||||
def _proxy_backend_java(method, path, *, params=None, json_data=None, files=None, data=None, timeout=10):
|
def _proxy_backend_java(method, path, *, params=None, json_data=None, files=None, data=None, timeout=10):
|
||||||
url = f"{backend_java_base_url}{path}"
|
url = f"{backend_java_base_url}{path}"
|
||||||
try:
|
try:
|
||||||
resp = requests.request(method=method, url=url, params=params, json=json_data, files=files, data=data, timeout=timeout)
|
requester = requests if files else _get_backend_java_session()
|
||||||
|
resp = requester.request(method=method, url=url, params=params, json=json_data, files=files, data=data, timeout=timeout)
|
||||||
except requests.RequestException:
|
except requests.RequestException:
|
||||||
return None, jsonify({'success': False, 'error': 'backend-java 服务不可用'}), 502
|
return None, jsonify({'success': False, 'error': 'backend-java 服务不可用'}), 502
|
||||||
try:
|
try:
|
||||||
@@ -146,6 +163,28 @@ def _parse_optional_int(value):
|
|||||||
|
|
||||||
|
|
||||||
def _ensure_column_sort_schema():
|
def _ensure_column_sort_schema():
|
||||||
|
global _column_sort_schema_checked
|
||||||
|
if _column_sort_schema_checked:
|
||||||
|
return
|
||||||
|
with _column_sort_schema_lock:
|
||||||
|
if _column_sort_schema_checked:
|
||||||
|
return
|
||||||
|
conn = get_db()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
try:
|
||||||
|
cur.execute("ALTER TABLE columns ADD COLUMN sort_order INT NOT NULL DEFAULT 0 COMMENT '鑿滃崟鎺掑簭' AFTER route_path")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
cur.execute("UPDATE columns SET sort_order = id WHERE sort_order IS NULL OR sort_order = 0")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
conn.commit()
|
||||||
|
_column_sort_schema_checked = True
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
return
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
try:
|
try:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
@@ -299,6 +338,10 @@ def _get_current_admin_permission_sets():
|
|||||||
user_id = _get_current_admin_id()
|
user_id = _get_current_admin_id()
|
||||||
if not user_id:
|
if not user_id:
|
||||||
return set(), set()
|
return set(), set()
|
||||||
|
cache_key = f'_admin_permission_sets_{user_id}'
|
||||||
|
cached = getattr(g, cache_key, None)
|
||||||
|
if cached is not None:
|
||||||
|
return cached
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
try:
|
try:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
@@ -314,7 +357,9 @@ def _get_current_admin_permission_sets():
|
|||||||
conn.close()
|
conn.close()
|
||||||
key_set = {(row.get('column_key') or '').strip() for row in rows if (row.get('column_key') or '').strip()}
|
key_set = {(row.get('column_key') or '').strip() for row in rows if (row.get('column_key') or '').strip()}
|
||||||
route_set = {(row.get('route_path') or '').strip() for row in rows if (row.get('route_path') or '').strip()}
|
route_set = {(row.get('route_path') or '').strip() for row in rows if (row.get('route_path') or '').strip()}
|
||||||
return key_set, route_set
|
cached = (key_set, route_set)
|
||||||
|
setattr(g, cache_key, cached)
|
||||||
|
return cached
|
||||||
|
|
||||||
|
|
||||||
def _ensure_admin_menu_access(*menu_names):
|
def _ensure_admin_menu_access(*menu_names):
|
||||||
@@ -830,14 +875,6 @@ def list_columns():
|
|||||||
if denied:
|
if denied:
|
||||||
return denied
|
return denied
|
||||||
menu_type = (request.args.get('menu_type') or '').strip()
|
menu_type = (request.args.get('menu_type') or '').strip()
|
||||||
params = {'menuType': menu_type} if menu_type else None
|
|
||||||
result, error_response, status = _proxy_backend_java(
|
|
||||||
'GET',
|
|
||||||
'/api/admin/permission-menus',
|
|
||||||
params=params,
|
|
||||||
)
|
|
||||||
if error_response is None:
|
|
||||||
_sync_local_columns_from_backend([_format_permission_item(item) for item in (result.get('data') or [])])
|
|
||||||
local_rows = _load_local_columns(menu_type or None)
|
local_rows = _load_local_columns(menu_type or None)
|
||||||
items = [
|
items = [
|
||||||
{
|
{
|
||||||
@@ -1484,32 +1521,7 @@ def _load_expanded_shop_manage_groups(role, current_row):
|
|||||||
if error_response is not None:
|
if error_response is not None:
|
||||||
return None, error_response, status
|
return None, error_response, status
|
||||||
|
|
||||||
direct_groups = result.get('data') or []
|
return result.get('data') or [], None, 200
|
||||||
if role == 'super_admin':
|
|
||||||
return direct_groups, None, 200
|
|
||||||
|
|
||||||
direct_ids = {item.get('id') for item in direct_groups if item.get('id')}
|
|
||||||
leader_ids = {item.get('leaderUserId') for item in direct_groups if item.get('leaderUserId')}
|
|
||||||
if not leader_ids:
|
|
||||||
return direct_groups, None, 200
|
|
||||||
|
|
||||||
all_result, error_response, status = _proxy_backend_java(
|
|
||||||
'GET',
|
|
||||||
'/api/admin/shop-manages/groups',
|
|
||||||
params={'superAdmin': 'true'},
|
|
||||||
)
|
|
||||||
if error_response is not None:
|
|
||||||
return None, error_response, status
|
|
||||||
|
|
||||||
merged = []
|
|
||||||
seen_ids = set()
|
|
||||||
for item in all_result.get('data') or []:
|
|
||||||
item_id = item.get('id')
|
|
||||||
if item_id in direct_ids or item.get('leaderUserId') in leader_ids:
|
|
||||||
if item_id not in seen_ids:
|
|
||||||
merged.append(item)
|
|
||||||
seen_ids.add(item_id)
|
|
||||||
return merged, None, 200
|
|
||||||
|
|
||||||
|
|
||||||
@admin_api.route('/shop-manages')
|
@admin_api.route('/shop-manages')
|
||||||
@@ -1523,59 +1535,6 @@ def list_shop_manages():
|
|||||||
group_id_raw = (request.args.get('group_id') or '').strip()
|
group_id_raw = (request.args.get('group_id') or '').strip()
|
||||||
shop_name = (request.args.get('shop_name') or '').strip()
|
shop_name = (request.args.get('shop_name') or '').strip()
|
||||||
|
|
||||||
if role != 'super_admin':
|
|
||||||
groups, error_response, status = _load_expanded_shop_manage_groups(role, current_row)
|
|
||||||
if error_response is not None:
|
|
||||||
return error_response, status
|
|
||||||
accessible_group_ids = [item.get('id') for item in (groups or []) if item.get('id')]
|
|
||||||
selected_group_id = None
|
|
||||||
if group_id_raw:
|
|
||||||
try:
|
|
||||||
selected_group_id = int(group_id_raw)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
selected_group_id = None
|
|
||||||
if selected_group_id and selected_group_id not in accessible_group_ids:
|
|
||||||
return jsonify({'success': True, 'items': [], 'total': 0, 'page': page, 'page_size': page_size})
|
|
||||||
query_group_ids = [selected_group_id] if selected_group_id else accessible_group_ids
|
|
||||||
all_items = []
|
|
||||||
for group_id in query_group_ids:
|
|
||||||
group_page = 1
|
|
||||||
while True:
|
|
||||||
group_params = {
|
|
||||||
'page': group_page,
|
|
||||||
'pageSize': 100,
|
|
||||||
'groupId': group_id,
|
|
||||||
'superAdmin': 'true',
|
|
||||||
}
|
|
||||||
if current_row and current_row.get('id'):
|
|
||||||
group_params['operatorId'] = current_row.get('id')
|
|
||||||
if shop_name:
|
|
||||||
group_params['shopName'] = shop_name
|
|
||||||
result, error_response, status = _proxy_backend_java(
|
|
||||||
'GET',
|
|
||||||
'/api/admin/shop-manages',
|
|
||||||
params=group_params,
|
|
||||||
)
|
|
||||||
if error_response is not None:
|
|
||||||
return error_response, status
|
|
||||||
payload = result.get('data') or {}
|
|
||||||
rows = payload.get('items') or []
|
|
||||||
all_items.extend(rows)
|
|
||||||
total = payload.get('total') or 0
|
|
||||||
if group_page * 100 >= total or not rows:
|
|
||||||
break
|
|
||||||
group_page += 1
|
|
||||||
all_items.sort(key=lambda item: item.get('id') or 0, reverse=True)
|
|
||||||
total = len(all_items)
|
|
||||||
offset = (page - 1) * page_size
|
|
||||||
return jsonify({
|
|
||||||
'success': True,
|
|
||||||
'items': [_format_shop_manage_item(item) for item in all_items[offset:offset + page_size]],
|
|
||||||
'total': total,
|
|
||||||
'page': page,
|
|
||||||
'page_size': page_size,
|
|
||||||
})
|
|
||||||
|
|
||||||
params = {'page': page, 'pageSize': page_size}
|
params = {'page': page, 'pageSize': page_size}
|
||||||
if current_row and current_row.get('id'):
|
if current_row and current_row.get('id'):
|
||||||
params['operatorId'] = current_row.get('id')
|
params['operatorId'] = current_row.get('id')
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ bucket_path = "nanri-image/"
|
|||||||
file_url_pre = f"https://{bucket}.oss-cn-hangzhou.aliyuncs.com/"
|
file_url_pre = f"https://{bucket}.oss-cn-hangzhou.aliyuncs.com/"
|
||||||
|
|
||||||
import os
|
import os
|
||||||
# backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://127.0.0.1:18080').rstrip('/')
|
backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://127.0.0.1:18080').rstrip('/')
|
||||||
# backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://8.136.19.173:18080').rstrip('/')
|
# backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://8.136.19.173:18080').rstrip('/')
|
||||||
backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://47.111.163.154:18080').rstrip('/')
|
# backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://47.111.163.154:18080').rstrip('/')
|
||||||
os.environ['OSS_ACCESS_KEY_ID'] = accessKeyId
|
os.environ['OSS_ACCESS_KEY_ID'] = accessKeyId
|
||||||
os.environ['OSS_ACCESS_KEY_SECRET'] = accessKeySecret
|
os.environ['OSS_ACCESS_KEY_SECRET'] = accessKeySecret
|
||||||
os.environ['SECRET_KEY'] = "ddffc7c1d02121d9554d7b080b2511b6"
|
os.environ['SECRET_KEY'] = "ddffc7c1d02121d9554d7b080b2511b6"
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -3,7 +3,7 @@
|
|||||||
"""
|
"""
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from flask import request, redirect, url_for, session, jsonify
|
from flask import request, redirect, url_for, session, jsonify, g
|
||||||
|
|
||||||
from utils.db import get_db
|
from utils.db import get_db
|
||||||
|
|
||||||
@@ -13,15 +13,22 @@ def is_session_user_valid():
|
|||||||
uid = session.get('user_id')
|
uid = session.get('user_id')
|
||||||
if not uid:
|
if not uid:
|
||||||
return False
|
return False
|
||||||
|
cached_user = getattr(g, '_current_user_row', None)
|
||||||
|
if cached_user and cached_user.get('id') == uid:
|
||||||
|
return True
|
||||||
try:
|
try:
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute("SELECT id FROM users WHERE id = %s", (uid,))
|
cur.execute(
|
||||||
|
"SELECT id, username, is_admin, role, created_by_id FROM users WHERE id = %s",
|
||||||
|
(uid,)
|
||||||
|
)
|
||||||
row = cur.fetchone()
|
row = cur.fetchone()
|
||||||
conn.close()
|
conn.close()
|
||||||
if not row:
|
if not row:
|
||||||
session.clear()
|
session.clear()
|
||||||
return False
|
return False
|
||||||
|
g._current_user_row = row
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
session.clear()
|
session.clear()
|
||||||
@@ -34,16 +41,19 @@ def get_current_admin_role():
|
|||||||
if not uid:
|
if not uid:
|
||||||
return None, None
|
return None, None
|
||||||
try:
|
try:
|
||||||
conn = get_db()
|
row = getattr(g, '_current_user_row', None)
|
||||||
with conn.cursor() as cur:
|
if not row or row.get('id') != uid:
|
||||||
cur.execute(
|
conn = get_db()
|
||||||
"SELECT id, username, is_admin, role, created_by_id FROM users WHERE id = %s",
|
with conn.cursor() as cur:
|
||||||
(uid,)
|
cur.execute(
|
||||||
)
|
"SELECT id, username, is_admin, role, created_by_id FROM users WHERE id = %s",
|
||||||
row = cur.fetchone()
|
(uid,)
|
||||||
conn.close()
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
conn.close()
|
||||||
if not row:
|
if not row:
|
||||||
return None, None
|
return None, None
|
||||||
|
g._current_user_row = row
|
||||||
role = (row.get('role') or '').strip().lower()
|
role = (row.get('role') or '').strip().lower()
|
||||||
if not role:
|
if not role:
|
||||||
role = 'super_admin' if row.get('is_admin') and row.get('created_by_id') is None else (
|
role = 'super_admin' if row.get('is_admin') and row.get('created_by_id') is None else (
|
||||||
|
|||||||
@@ -6,12 +6,20 @@ import pymysql
|
|||||||
from werkzeug.security import generate_password_hash
|
from werkzeug.security import generate_password_hash
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from config import mysql_host, mysql_user, mysql_password, mysql_database
|
from config import mysql_host as config_mysql_host
|
||||||
|
from config import mysql_user as config_mysql_user
|
||||||
|
from config import mysql_password as config_mysql_password
|
||||||
|
from config import mysql_database as config_mysql_database
|
||||||
except ImportError:
|
except ImportError:
|
||||||
mysql_host = os.environ.get('MYSQL_HOST', 'localhost')
|
config_mysql_host = 'localhost'
|
||||||
mysql_user = os.environ.get('MYSQL_USER', 'root')
|
config_mysql_user = 'root'
|
||||||
mysql_password = os.environ.get('MYSQL_PASSWORD', '')
|
config_mysql_password = ''
|
||||||
mysql_database = os.environ.get('MYSQL_DATABASE', 'maixiang_ai')
|
config_mysql_database = 'maixiang_ai'
|
||||||
|
|
||||||
|
mysql_host = os.environ.get('MYSQL_HOST', config_mysql_host)
|
||||||
|
mysql_user = os.environ.get('MYSQL_USER', config_mysql_user)
|
||||||
|
mysql_password = os.environ.get('MYSQL_PASSWORD', config_mysql_password)
|
||||||
|
mysql_database = os.environ.get('MYSQL_DATABASE', config_mysql_database)
|
||||||
|
|
||||||
|
|
||||||
def get_db():
|
def get_db():
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -336,6 +336,178 @@
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#shopManageGroupModal .shop-group-modal {
|
||||||
|
width: min(1180px, calc(100vw - 48px));
|
||||||
|
max-width: none;
|
||||||
|
max-height: calc(100vh - 48px);
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-group-modal-header {
|
||||||
|
padding: 20px 24px 14px;
|
||||||
|
border-bottom: 1px solid #edf0f5;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-group-modal-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-group-editor {
|
||||||
|
padding: 18px 24px 14px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(180px, 240px) minmax(240px, 320px) minmax(360px, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
align-items: start;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-group-editor .form-group {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-group-member-select {
|
||||||
|
min-height: 128px;
|
||||||
|
max-height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-group-help {
|
||||||
|
color: #777;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-group-action-bar {
|
||||||
|
padding: 0 24px 12px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#msgShopManageGroup {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 24px 10px;
|
||||||
|
min-height: 18px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-group-table-wrap {
|
||||||
|
margin: 0 24px;
|
||||||
|
border: 1px solid #edf0f5;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: auto;
|
||||||
|
min-height: 220px;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-group-table {
|
||||||
|
min-width: 1040px;
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-group-table th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-group-table th,
|
||||||
|
.shop-group-table td {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-group-table .col-index {
|
||||||
|
width: 58px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-group-table .col-name {
|
||||||
|
width: 130px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-group-table .col-leader {
|
||||||
|
width: 110px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-group-table .col-count {
|
||||||
|
width: 86px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-group-table .col-members {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-group-table .col-time {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-group-table .col-action {
|
||||||
|
width: 128px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-group-time-cell,
|
||||||
|
.shop-group-action-cell {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-group-name-cell,
|
||||||
|
.shop-group-leader-cell {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-group-members {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
max-height: 96px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-group-member-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 150px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #eef1ff;
|
||||||
|
color: #4f63d8;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-group-modal-footer {
|
||||||
|
padding: 14px 24px 18px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
border-top: 1px solid #edf0f5;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
#shopManageGroupModal .shop-group-modal {
|
||||||
|
width: calc(100vw - 24px);
|
||||||
|
max-height: calc(100vh - 24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-group-editor {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-group-table-wrap {
|
||||||
|
margin: 0 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.column-permission-cards {
|
.column-permission-cards {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -347,6 +519,97 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.multi-select-native {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
width: 1px !important;
|
||||||
|
height: 1px !important;
|
||||||
|
min-height: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
border: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-select-dropdown {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-select-trigger {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 42px;
|
||||||
|
padding: 10px 34px 10px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fff;
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-select-trigger::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
right: 12px;
|
||||||
|
top: 50%;
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-right: 1.5px solid #666;
|
||||||
|
border-bottom: 1.5px solid #666;
|
||||||
|
transform: translateY(-65%) rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-select-dropdown.open .multi-select-trigger {
|
||||||
|
border-color: #667eea;
|
||||||
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-select-panel {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: calc(100% + 6px);
|
||||||
|
z-index: 30;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.14);
|
||||||
|
padding: 6px;
|
||||||
|
max-height: 220px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-select-dropdown.open .multi-select-panel {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-select-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 34px;
|
||||||
|
padding: 7px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-select-option:hover {
|
||||||
|
background: #f4f6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-select-option input {
|
||||||
|
width: auto;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.column-permission-card {
|
.column-permission-card {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -762,7 +1025,7 @@
|
|||||||
<label> </label>
|
<label> </label>
|
||||||
<button class="btn btn-secondary" id="btnChooseSkipPriceAsinShop" type="button">选择店铺</button>
|
<button class="btn btn-secondary" id="btnChooseSkipPriceAsinShop" type="button">选择店铺</button>
|
||||||
</div>
|
</div>
|
||||||
<select id="skipPriceAsinCountries" multiple size="5">
|
<select id="skipPriceAsinCountries" class="js-dropdown-multi" multiple>
|
||||||
<option value="DE">德国</option>
|
<option value="DE">德国</option>
|
||||||
<option value="UK">英国</option>
|
<option value="UK">英国</option>
|
||||||
<option value="FR">法国</option>
|
<option value="FR">法国</option>
|
||||||
@@ -840,7 +1103,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="min-width:220px;">
|
<div class="form-group" style="min-width:220px;">
|
||||||
<label>国家</label>
|
<label>国家</label>
|
||||||
<select id="queryAsinCountries" multiple size="5" style="min-height:98px;">
|
<select id="queryAsinCountries" class="js-dropdown-multi" multiple>
|
||||||
<option value="DE">德国</option>
|
<option value="DE">德国</option>
|
||||||
<option value="UK">英国</option>
|
<option value="UK">英国</option>
|
||||||
<option value="FR">法国</option>
|
<option value="FR">法国</option>
|
||||||
@@ -1063,48 +1326,62 @@
|
|||||||
|
|
||||||
<!-- 分组管理弹窗 -->
|
<!-- 分组管理弹窗 -->
|
||||||
<div class="modal-mask" id="shopManageGroupModal">
|
<div class="modal-mask" id="shopManageGroupModal">
|
||||||
<div class="modal" style="max-width:760px;">
|
<div class="modal shop-group-modal">
|
||||||
<h3>分组管理</h3>
|
<div class="shop-group-modal-header">
|
||||||
<div class="form-row">
|
<h3>分组管理</h3>
|
||||||
|
</div>
|
||||||
|
<div class="shop-group-editor">
|
||||||
<input type="hidden" id="shopManageGroupEditId">
|
<input type="hidden" id="shopManageGroupEditId">
|
||||||
<input type="hidden" id="shopManageGroupLeaderUserId">
|
<input type="hidden" id="shopManageGroupLeaderUserId">
|
||||||
<div class="form-group" style="min-width:220px;">
|
<div class="form-group">
|
||||||
<label>组长</label>
|
<label>组长</label>
|
||||||
<input type="text" id="shopManageGroupLeaderName" placeholder="当前登录用户" readonly
|
<input type="text" id="shopManageGroupLeaderName" placeholder="当前登录用户" readonly
|
||||||
style="background:#f5f5f5;">
|
style="background:#f5f5f5;">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="min-width:260px;">
|
<div class="form-group">
|
||||||
<label>分组名称</label>
|
<label>分组名称</label>
|
||||||
<input type="text" id="shopManageGroupInput" placeholder="请输入分组名称">
|
<input type="text" id="shopManageGroupInput" placeholder="请输入分组名称">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="min-width:280px;flex:1;">
|
<div class="form-group">
|
||||||
<label>组员</label>
|
<label>组员</label>
|
||||||
<select id="shopManageGroupMemberSelect" multiple size="6" style="min-height:140px;">
|
<select id="shopManageGroupMemberSelect" class="shop-group-member-select" multiple size="6">
|
||||||
</select>
|
</select>
|
||||||
<div id="shopManageGroupMemberHelp" style="color:#888;font-size:12px;margin-top:6px;">可添加当前组长创建的普通员工账号,按住 Ctrl 或 Command 可多选。</div>
|
<div id="shopManageGroupMemberHelp" class="shop-group-help">可添加当前组长创建的普通员工账号,按住 Ctrl 或 Command 可多选。</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;gap:8px;justify-content:flex-end;margin-bottom:12px;">
|
<div class="shop-group-action-bar">
|
||||||
<button class="btn" id="btnSaveShopManageGroup" type="button">保存分组</button>
|
<button class="btn" id="btnSaveShopManageGroup" type="button">保存分组</button>
|
||||||
<button class="btn btn-secondary" id="btnCancelShopManageGroupEdit" type="button">取消编辑</button>
|
<button class="btn btn-secondary" id="btnCancelShopManageGroupEdit" type="button">取消编辑</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="msg" id="msgShopManageGroup"></p>
|
<p class="msg" id="msgShopManageGroup"></p>
|
||||||
<table>
|
<div class="shop-group-table-wrap">
|
||||||
<thead>
|
<table class="shop-group-table">
|
||||||
<tr>
|
<colgroup>
|
||||||
<th>序号</th>
|
<col class="col-index">
|
||||||
<th>分组名称</th>
|
<col class="col-name">
|
||||||
<th>组长</th>
|
<col class="col-leader">
|
||||||
<th>组员数量</th>
|
<col class="col-count">
|
||||||
<th>组员</th>
|
<col class="col-members">
|
||||||
<th>创建时间</th>
|
<col class="col-time">
|
||||||
<th>修改时间</th>
|
<col class="col-time">
|
||||||
<th>操作</th>
|
<col class="col-action">
|
||||||
</tr>
|
</colgroup>
|
||||||
</thead>
|
<thead>
|
||||||
<tbody id="shopManageGroupListBody"></tbody>
|
<tr>
|
||||||
</table>
|
<th>序号</th>
|
||||||
<div style="margin-top:16px;display:flex;gap:8px;justify-content:flex-end;">
|
<th>分组名称</th>
|
||||||
|
<th>组长</th>
|
||||||
|
<th>组员数量</th>
|
||||||
|
<th>组员</th>
|
||||||
|
<th>创建时间</th>
|
||||||
|
<th>修改时间</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="shopManageGroupListBody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="shop-group-modal-footer">
|
||||||
<button class="btn btn-secondary" id="btnCloseShopManageGroupModal" type="button">关闭</button>
|
<button class="btn btn-secondary" id="btnCloseShopManageGroupModal" type="button">关闭</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1865,6 +2142,111 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
var shopManageAllUsers = [];
|
var shopManageAllUsers = [];
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value == null ? '' : value)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
var dropdownMultiSelects = {};
|
||||||
|
function initDropdownMultiSelect(selectId, placeholder) {
|
||||||
|
var select = document.getElementById(selectId);
|
||||||
|
if (!select || dropdownMultiSelects[selectId]) return;
|
||||||
|
|
||||||
|
select.classList.add('multi-select-native');
|
||||||
|
var wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'multi-select-dropdown';
|
||||||
|
wrapper.setAttribute('data-multi-select-id', selectId);
|
||||||
|
|
||||||
|
var trigger = document.createElement('button');
|
||||||
|
trigger.type = 'button';
|
||||||
|
trigger.className = 'multi-select-trigger';
|
||||||
|
trigger.textContent = placeholder || '请选择';
|
||||||
|
|
||||||
|
var panel = document.createElement('div');
|
||||||
|
panel.className = 'multi-select-panel';
|
||||||
|
|
||||||
|
select.parentNode.insertBefore(wrapper, select);
|
||||||
|
wrapper.appendChild(trigger);
|
||||||
|
wrapper.appendChild(panel);
|
||||||
|
wrapper.appendChild(select);
|
||||||
|
|
||||||
|
function selectedOptions() {
|
||||||
|
return Array.from(select.options).filter(function (option) { return option.selected; });
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSummary() {
|
||||||
|
var selected = selectedOptions();
|
||||||
|
if (!selected.length) {
|
||||||
|
trigger.textContent = placeholder || '请选择';
|
||||||
|
trigger.title = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var labels = selected.map(function (option) { return option.textContent || option.value; });
|
||||||
|
trigger.textContent = labels.join('、');
|
||||||
|
trigger.title = labels.join('、');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderOptions() {
|
||||||
|
panel.innerHTML = Array.from(select.options).map(function (option, index) {
|
||||||
|
var optionId = selectId + '_multi_' + index;
|
||||||
|
return '<label class="multi-select-option" for="' + escapeHtml(optionId) + '">' +
|
||||||
|
'<input type="checkbox" id="' + escapeHtml(optionId) + '" data-multi-option-index="' + index + '"' + (option.selected ? ' checked' : '') + '>' +
|
||||||
|
'<span>' + escapeHtml(option.textContent || option.value) + '</span>' +
|
||||||
|
'</label>';
|
||||||
|
}).join('');
|
||||||
|
panel.querySelectorAll('[data-multi-option-index]').forEach(function (checkbox) {
|
||||||
|
checkbox.onchange = function () {
|
||||||
|
var option = select.options[Number(checkbox.getAttribute('data-multi-option-index'))];
|
||||||
|
if (!option) return;
|
||||||
|
option.selected = checkbox.checked;
|
||||||
|
updateSummary();
|
||||||
|
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
trigger.onclick = function (event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
document.querySelectorAll('.multi-select-dropdown.open').forEach(function (item) {
|
||||||
|
if (item !== wrapper) item.classList.remove('open');
|
||||||
|
});
|
||||||
|
wrapper.classList.toggle('open');
|
||||||
|
};
|
||||||
|
panel.onclick = function (event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
};
|
||||||
|
select.addEventListener('change', function () {
|
||||||
|
panel.querySelectorAll('[data-multi-option-index]').forEach(function (checkbox) {
|
||||||
|
var option = select.options[Number(checkbox.getAttribute('data-multi-option-index'))];
|
||||||
|
checkbox.checked = !!(option && option.selected);
|
||||||
|
});
|
||||||
|
updateSummary();
|
||||||
|
});
|
||||||
|
|
||||||
|
renderOptions();
|
||||||
|
updateSummary();
|
||||||
|
dropdownMultiSelects[selectId] = {
|
||||||
|
refresh: function () {
|
||||||
|
renderOptions();
|
||||||
|
updateSummary();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshDropdownMultiSelect(selectId) {
|
||||||
|
if (dropdownMultiSelects[selectId]) {
|
||||||
|
dropdownMultiSelects[selectId].refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', function () {
|
||||||
|
document.querySelectorAll('.multi-select-dropdown.open').forEach(function (item) {
|
||||||
|
item.classList.remove('open');
|
||||||
|
});
|
||||||
|
});
|
||||||
function getEligibleShopManageGroupUsers(leaderUserId) {
|
function getEligibleShopManageGroupUsers(leaderUserId) {
|
||||||
var canViewAllMembers = currentUserRole === 'super_admin';
|
var canViewAllMembers = currentUserRole === 'super_admin';
|
||||||
return shopManageAllUsers.filter(function (u) {
|
return shopManageAllUsers.filter(function (u) {
|
||||||
@@ -1884,7 +2266,7 @@
|
|||||||
});
|
});
|
||||||
var eligibleUsers = getEligibleShopManageGroupUsers(leaderUserId);
|
var eligibleUsers = getEligibleShopManageGroupUsers(leaderUserId);
|
||||||
var options = eligibleUsers.map(function (u) {
|
var options = eligibleUsers.map(function (u) {
|
||||||
return '<option value="' + u.id + '"' + (selectedMap[String(u.id)] ? ' selected' : '') + '>' + (u.username || '') + '</option>';
|
return '<option value="' + escapeHtml(u.id) + '"' + (selectedMap[String(u.id)] ? ' selected' : '') + '>' + escapeHtml(u.username || '') + '</option>';
|
||||||
});
|
});
|
||||||
if (options.length) {
|
if (options.length) {
|
||||||
sel.innerHTML = options.join('');
|
sel.innerHTML = options.join('');
|
||||||
@@ -2405,6 +2787,7 @@
|
|||||||
// ========== 店铺管理 ==========
|
// ========== 店铺管理 ==========
|
||||||
var shopManagePage = 1, shopManagePageSize = 15;
|
var shopManagePage = 1, shopManagePageSize = 15;
|
||||||
var shopManageGroups = [];
|
var shopManageGroups = [];
|
||||||
|
var shopManageGroupsLoadedAt = 0;
|
||||||
var currentShopManageGroupGrantRoutes = [];
|
var currentShopManageGroupGrantRoutes = [];
|
||||||
|
|
||||||
function buildShopManageQuery(page) {
|
function buildShopManageQuery(page) {
|
||||||
@@ -2521,17 +2904,23 @@
|
|||||||
if (chooseQueryShopSel && selectedChooseQueryShopId) chooseQueryShopSel.value = selectedChooseQueryShopId;
|
if (chooseQueryShopSel && selectedChooseQueryShopId) chooseQueryShopSel.value = selectedChooseQueryShopId;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadShopManageGroups(selectedCreateId, selectedEditId) {
|
function loadShopManageGroups(selectedCreateId, selectedEditId, force) {
|
||||||
|
if (!force && shopManageGroups.length && Date.now() - shopManageGroupsLoadedAt < 30000) {
|
||||||
|
refreshShopGroupSelects(selectedCreateId, selectedEditId);
|
||||||
|
return Promise.resolve(shopManageGroups);
|
||||||
|
}
|
||||||
return fetch('/api/admin/shop-manage-groups')
|
return fetch('/api/admin/shop-manage-groups')
|
||||||
.then(function (r) { return r.json(); })
|
.then(function (r) { return r.json(); })
|
||||||
.then(function (res) {
|
.then(function (res) {
|
||||||
if (!res.success) throw new Error(res.error || '加载分组失败');
|
if (!res.success) throw new Error(res.error || '加载分组失败');
|
||||||
shopManageGroups = res.items || [];
|
shopManageGroups = res.items || [];
|
||||||
|
shopManageGroupsLoadedAt = Date.now();
|
||||||
refreshShopGroupSelects(selectedCreateId, selectedEditId);
|
refreshShopGroupSelects(selectedCreateId, selectedEditId);
|
||||||
return shopManageGroups;
|
return shopManageGroups;
|
||||||
})
|
})
|
||||||
.catch(function () {
|
.catch(function () {
|
||||||
shopManageGroups = [];
|
shopManageGroups = [];
|
||||||
|
shopManageGroupsLoadedAt = 0;
|
||||||
refreshShopGroupSelects();
|
refreshShopGroupSelects();
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
@@ -2576,14 +2965,28 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
tbody.innerHTML = shopManageGroups.map(function (item, index) {
|
tbody.innerHTML = shopManageGroups.map(function (item, index) {
|
||||||
var memberNames = Array.isArray(item.member_usernames) ? item.member_usernames.join('、') : '';
|
var memberNames = Array.isArray(item.member_usernames) ? item.member_usernames : [];
|
||||||
|
var memberHtml = memberNames.length
|
||||||
|
? '<div class="shop-group-members" title="' + escapeHtml(memberNames.join('、')) + '">' +
|
||||||
|
memberNames.map(function (name) {
|
||||||
|
return '<span class="shop-group-member-chip">' + escapeHtml(name || '') + '</span>';
|
||||||
|
}).join('') + '</div>'
|
||||||
|
: '<span style="color:#999;">-</span>';
|
||||||
var canEdit = currentUserRole === 'super_admin' || String(item.leader_user_id || '') === String(currentUserId || '');
|
var canEdit = currentUserRole === 'super_admin' || String(item.leader_user_id || '') === String(currentUserId || '');
|
||||||
var actionHtml = canEdit
|
var actionHtml = canEdit
|
||||||
? ('<button class="btn btn-sm" data-shop-group-edit="' + item.id + '">编辑</button> ' +
|
? ('<button class="btn btn-sm" data-shop-group-edit="' + escapeHtml(item.id) + '">编辑</button> ' +
|
||||||
'<button class="btn btn-sm btn-danger" data-shop-group-delete="' + item.id + '" data-shop-group-name="' + (item.group_name || '').replace(/"/g, '"') + '">删除</button>')
|
'<button class="btn btn-sm btn-danger" data-shop-group-delete="' + escapeHtml(item.id) + '" data-shop-group-name="' + escapeHtml(item.group_name || '') + '">删除</button>')
|
||||||
: '<span style="color:#999;">-</span>';
|
: '<span style="color:#999;">-</span>';
|
||||||
return '<tr><td>' + (index + 1) + '</td><td>' + (item.group_name || '') + '</td><td>' + (item.leader_username || '') + '</td><td>' + (item.member_count || 0) + '</td><td>' + (memberNames || '-') + '</td><td>' + (item.created_at || '') + '</td><td>' + (item.updated_at || '') + '</td><td>' +
|
return '<tr>' +
|
||||||
actionHtml + '</td></tr>';
|
'<td>' + (index + 1) + '</td>' +
|
||||||
|
'<td class="shop-group-name-cell">' + escapeHtml(item.group_name || '') + '</td>' +
|
||||||
|
'<td class="shop-group-leader-cell">' + escapeHtml(item.leader_username || '') + '</td>' +
|
||||||
|
'<td>' + escapeHtml(item.member_count || 0) + '</td>' +
|
||||||
|
'<td>' + memberHtml + '</td>' +
|
||||||
|
'<td class="shop-group-time-cell">' + escapeHtml(item.created_at || '') + '</td>' +
|
||||||
|
'<td class="shop-group-time-cell">' + escapeHtml(item.updated_at || '') + '</td>' +
|
||||||
|
'<td class="shop-group-action-cell">' + actionHtml + '</td>' +
|
||||||
|
'</tr>';
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
document.querySelectorAll('[data-shop-group-edit]').forEach(function (btn) {
|
document.querySelectorAll('[data-shop-group-edit]').forEach(function (btn) {
|
||||||
@@ -2617,7 +3020,7 @@
|
|||||||
alert(res.error || '删除失败');
|
alert(res.error || '删除失败');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
loadShopManageGroups().then(function () {
|
loadShopManageGroups(null, null, true).then(function () {
|
||||||
renderShopManageGroupRows();
|
renderShopManageGroupRows();
|
||||||
loadShopManage(shopManagePage);
|
loadShopManage(shopManagePage);
|
||||||
loadSkipPriceAsin(skipPriceAsinPage);
|
loadSkipPriceAsin(skipPriceAsinPage);
|
||||||
@@ -2689,7 +3092,7 @@
|
|||||||
resetShopManageGroupForm();
|
resetShopManageGroupForm();
|
||||||
msgEl.textContent = res.msg || '保存成功';
|
msgEl.textContent = res.msg || '保存成功';
|
||||||
msgEl.className = 'msg ok';
|
msgEl.className = 'msg ok';
|
||||||
loadShopManageGroups().then(function () {
|
loadShopManageGroups(null, null, true).then(function () {
|
||||||
renderShopManageGroupRows();
|
renderShopManageGroupRows();
|
||||||
loadShopManage(shopManagePage);
|
loadShopManage(shopManagePage);
|
||||||
loadSkipPriceAsin(skipPriceAsinPage);
|
loadSkipPriceAsin(skipPriceAsinPage);
|
||||||
@@ -3174,6 +3577,7 @@
|
|||||||
Array.from(document.getElementById('skipPriceAsinCountries').options).forEach(function (option) {
|
Array.from(document.getElementById('skipPriceAsinCountries').options).forEach(function (option) {
|
||||||
option.selected = false;
|
option.selected = false;
|
||||||
});
|
});
|
||||||
|
refreshDropdownMultiSelect('skipPriceAsinCountries');
|
||||||
renderSkipPriceAsinInputs();
|
renderSkipPriceAsinInputs();
|
||||||
msgEl.textContent = res.msg || '保存成功';
|
msgEl.textContent = res.msg || '保存成功';
|
||||||
msgEl.className = 'msg ok';
|
msgEl.className = 'msg ok';
|
||||||
@@ -3185,6 +3589,7 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
setupSkipPriceAsinShopPicker();
|
setupSkipPriceAsinShopPicker();
|
||||||
|
initDropdownMultiSelect('skipPriceAsinCountries', '请选择国家');
|
||||||
renderSkipPriceAsinInputs();
|
renderSkipPriceAsinInputs();
|
||||||
|
|
||||||
// ========== 查询 ASIN ==========
|
// ========== 查询 ASIN ==========
|
||||||
@@ -3677,6 +4082,7 @@
|
|||||||
Array.from(document.getElementById('queryAsinCountries').options).forEach(function (option) {
|
Array.from(document.getElementById('queryAsinCountries').options).forEach(function (option) {
|
||||||
option.selected = false;
|
option.selected = false;
|
||||||
});
|
});
|
||||||
|
refreshDropdownMultiSelect('queryAsinCountries');
|
||||||
renderQueryAsinInputs();
|
renderQueryAsinInputs();
|
||||||
msgEl.textContent = res.msg || '保存成功';
|
msgEl.textContent = res.msg || '保存成功';
|
||||||
msgEl.className = 'msg ok';
|
msgEl.className = 'msg ok';
|
||||||
@@ -3687,6 +4093,7 @@
|
|||||||
msgEl.className = 'msg err';
|
msgEl.className = 'msg err';
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
initDropdownMultiSelect('queryAsinCountries', '请选择国家');
|
||||||
document.getElementById('btnImportQueryAsin').onclick = function () {
|
document.getElementById('btnImportQueryAsin').onclick = function () {
|
||||||
uploadQueryAsinImport(false);
|
uploadQueryAsinImport(false);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -153,6 +153,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 { createCategorizedTimers } from '@/shared/utils/categorized-timers'
|
||||||
|
|
||||||
const selectedFileNames = ref<string[]>([])
|
const selectedFileNames = ref<string[]>([])
|
||||||
const uploadedFiles = ref<UploadFileVo[]>([])
|
const uploadedFiles = ref<UploadFileVo[]>([])
|
||||||
@@ -171,6 +172,8 @@ const queuePayloadText = ref('')
|
|||||||
const pollingTaskIds = ref<number[]>([])
|
const pollingTaskIds = ref<number[]>([])
|
||||||
const pollTimer = ref<number | null>(null)
|
const pollTimer = ref<number | null>(null)
|
||||||
const pollingInFlight = ref(false)
|
const pollingInFlight = ref(false)
|
||||||
|
let disposed = false
|
||||||
|
const timers = createCategorizedTimers('appearance-patent')
|
||||||
|
|
||||||
const dashboard = ref<AppearancePatentDashboardVo>({
|
const dashboard = ref<AppearancePatentDashboardVo>({
|
||||||
pendingTaskCount: 0,
|
pendingTaskCount: 0,
|
||||||
@@ -389,33 +392,36 @@ function removePollingTask(taskId: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ensurePolling() {
|
function ensurePolling() {
|
||||||
|
if (disposed) return
|
||||||
if (pollTimer.value != null) return
|
if (pollTimer.value != null) return
|
||||||
scheduleNextPoll(true)
|
scheduleNextPoll(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopPolling() {
|
function stopPolling() {
|
||||||
if (pollTimer.value != null) {
|
if (pollTimer.value != null) {
|
||||||
window.clearTimeout(pollTimer.value)
|
timers.clearTimer('task-poll', pollTimer.value)
|
||||||
pollTimer.value = null
|
pollTimer.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleNextPoll(immediate = false) {
|
function scheduleNextPoll(immediate = false) {
|
||||||
|
if (disposed) return
|
||||||
if (pollTimer.value != null) {
|
if (pollTimer.value != null) {
|
||||||
if (!immediate) return
|
if (!immediate) return
|
||||||
window.clearTimeout(pollTimer.value)
|
timers.clearTimer('task-poll', pollTimer.value)
|
||||||
pollTimer.value = null
|
pollTimer.value = null
|
||||||
}
|
}
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
pollTimer.value = null
|
pollTimer.value = null
|
||||||
|
if (disposed) return
|
||||||
if (!pollingTaskIds.value.length) return
|
if (!pollingTaskIds.value.length) return
|
||||||
await refreshTaskProgress()
|
await refreshTaskProgress()
|
||||||
if (pollingTaskIds.value.length) {
|
if (!disposed && pollingTaskIds.value.length) {
|
||||||
pollTimer.value = window.setTimeout(run, getTaskPollIntervalMs())
|
pollTimer.value = timers.setTimeout('task-poll', run, getTaskPollIntervalMs())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (immediate) void run()
|
if (immediate) void run()
|
||||||
else pollTimer.value = window.setTimeout(run, getTaskPollIntervalMs())
|
else pollTimer.value = timers.setTimeout('task-poll', run, getTaskPollIntervalMs())
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshTaskProgress() {
|
async function refreshTaskProgress() {
|
||||||
@@ -547,7 +553,11 @@ onMounted(async () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => stopPolling())
|
onUnmounted(() => {
|
||||||
|
disposed = true
|
||||||
|
stopPolling()
|
||||||
|
timers.clearScope()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -226,6 +226,7 @@ import {
|
|||||||
} from '@/shared/api/java-modules'
|
} from '@/shared/api/java-modules'
|
||||||
import { getPywebviewApi, type UploadedJavaFile } from '@/shared/bridges/pywebview'
|
import { getPywebviewApi, type UploadedJavaFile } from '@/shared/bridges/pywebview'
|
||||||
import { getTaskPollIntervalMs } from '@/shared/task-progress-config'
|
import { getTaskPollIntervalMs } from '@/shared/task-progress-config'
|
||||||
|
import { createCategorizedTimers } from '@/shared/utils/categorized-timers'
|
||||||
|
|
||||||
interface SessionDeleteBrandItem extends DeleteBrandResultItem {
|
interface SessionDeleteBrandItem extends DeleteBrandResultItem {
|
||||||
_pushed?: boolean
|
_pushed?: boolean
|
||||||
@@ -263,6 +264,8 @@ const activeItemKey = ref('')
|
|||||||
const autoAdvancing = ref(false)
|
const autoAdvancing = ref(false)
|
||||||
const autoRetryTimer = ref<number | null>(null)
|
const autoRetryTimer = ref<number | null>(null)
|
||||||
const waitingForBackendRecovery = ref(false)
|
const waitingForBackendRecovery = ref(false)
|
||||||
|
let disposed = false
|
||||||
|
const timers = createCategorizedTimers('delete-brand')
|
||||||
const displayPaths = computed(() => selectedPaths.value.slice(0, 10))
|
const displayPaths = computed(() => selectedPaths.value.slice(0, 10))
|
||||||
|
|
||||||
const currentSectionItems = computed(() => {
|
const currentSectionItems = computed(() => {
|
||||||
@@ -892,14 +895,16 @@ function getPollIntervalMs() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function scheduleNextPoll(immediate = false) {
|
function scheduleNextPoll(immediate = false) {
|
||||||
|
if (disposed) return
|
||||||
if (pollTimer.value) {
|
if (pollTimer.value) {
|
||||||
if (!immediate) return
|
if (!immediate) return
|
||||||
clearTimeout(pollTimer.value)
|
timers.clearTimer('task-poll', pollTimer.value)
|
||||||
pollTimer.value = null
|
pollTimer.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const executePoll = async () => {
|
const executePoll = async () => {
|
||||||
pollTimer.value = null
|
pollTimer.value = null
|
||||||
|
if (disposed) return
|
||||||
if (pollingInFlight.value) {
|
if (pollingInFlight.value) {
|
||||||
scheduleNextPoll()
|
scheduleNextPoll()
|
||||||
return
|
return
|
||||||
@@ -933,7 +938,7 @@ function scheduleNextPoll(immediate = false) {
|
|||||||
syncResultState()
|
syncResultState()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getPollingTaskIds().length > 0) {
|
if (!disposed && getPollingTaskIds().length > 0) {
|
||||||
scheduleNextPoll()
|
scheduleNextPoll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -941,7 +946,7 @@ function scheduleNextPoll(immediate = false) {
|
|||||||
if (immediate) {
|
if (immediate) {
|
||||||
executePoll()
|
executePoll()
|
||||||
} else {
|
} else {
|
||||||
pollTimer.value = window.setTimeout(executePoll, getPollIntervalMs())
|
pollTimer.value = timers.setTimeout('task-poll', executePoll, getPollIntervalMs())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -969,7 +974,7 @@ function getTaskStatusInfo(item: DeleteBrandResultItem) {
|
|||||||
|
|
||||||
function stopPolling() {
|
function stopPolling() {
|
||||||
if (pollTimer.value) {
|
if (pollTimer.value) {
|
||||||
window.clearTimeout(pollTimer.value)
|
timers.clearTimer('task-poll', pollTimer.value)
|
||||||
pollTimer.value = null
|
pollTimer.value = null
|
||||||
}
|
}
|
||||||
clearAutoRetryTimer()
|
clearAutoRetryTimer()
|
||||||
@@ -1178,16 +1183,17 @@ function debugAutoAdvance(step: string, payload?: Record<string, unknown>) {
|
|||||||
|
|
||||||
function clearAutoRetryTimer() {
|
function clearAutoRetryTimer() {
|
||||||
if (autoRetryTimer.value) {
|
if (autoRetryTimer.value) {
|
||||||
window.clearTimeout(autoRetryTimer.value)
|
timers.clearTimer('auto-retry', autoRetryTimer.value)
|
||||||
autoRetryTimer.value = null
|
autoRetryTimer.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleAutoAdvanceRetry(delayMs = 3000) {
|
function scheduleAutoAdvanceRetry(delayMs = 3000) {
|
||||||
|
if (disposed) return
|
||||||
clearAutoRetryTimer()
|
clearAutoRetryTimer()
|
||||||
autoRetryTimer.value = window.setTimeout(() => {
|
autoRetryTimer.value = timers.setTimeout('auto-retry', () => {
|
||||||
autoRetryTimer.value = null
|
autoRetryTimer.value = null
|
||||||
maybeAutoAdvance().catch(() => undefined)
|
if (!disposed) maybeAutoAdvance().catch(() => undefined)
|
||||||
}, delayMs)
|
}, delayMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1428,7 +1434,9 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
disposed = true
|
||||||
stopPolling()
|
stopPolling()
|
||||||
|
timers.clearScope()
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -334,6 +334,7 @@ import {
|
|||||||
} from "@/shared/api/java-modules";
|
} from "@/shared/api/java-modules";
|
||||||
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 { createCategorizedTimers } from "@/shared/utils/categorized-timers";
|
||||||
|
|
||||||
const COUNTRY_TEMPLATE = ["德国", "英国", "法国", "意大利", "西班牙"] as const;
|
const COUNTRY_TEMPLATE = ["德国", "英国", "法国", "意大利", "西班牙"] as const;
|
||||||
const MAX_TRANSIENT_ERRORS = 30;
|
const MAX_TRANSIENT_ERRORS = 30;
|
||||||
@@ -360,6 +361,7 @@ const activeQueueItem = ref<PatrolDeleteShopQueueItem | null>(null);
|
|||||||
const queueWorkerRunning = ref(false);
|
const queueWorkerRunning = ref(false);
|
||||||
const autoQueueEnabled = ref(false);
|
const autoQueueEnabled = ref(false);
|
||||||
let historyPollTimer: number | null = null;
|
let historyPollTimer: number | null = null;
|
||||||
|
const timers = createCategorizedTimers("patrol-delete");
|
||||||
|
|
||||||
const matchedRunnableItems = computed(() =>
|
const matchedRunnableItems = computed(() =>
|
||||||
matchedItems.value.filter((item) => item.matched),
|
matchedItems.value.filter((item) => item.matched),
|
||||||
@@ -399,8 +401,15 @@ function historyItemKey(item: PatrolDeleteHistoryItem) {
|
|||||||
return `${item.taskId ?? 0}:${item.resultId ?? 0}`;
|
return `${item.taskId ?? 0}:${item.resultId ?? 0}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let disposed = false;
|
||||||
|
|
||||||
function sleep(ms: number) {
|
function sleep(ms: number) {
|
||||||
return new Promise((resolve) => window.setTimeout(resolve, ms));
|
if (disposed) return Promise.resolve();
|
||||||
|
return timers.sleep("queue-wait", ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSleepTimers() {
|
||||||
|
timers.clearCategory("queue-wait");
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTransientBackendError(error: unknown) {
|
function isTransientBackendError(error: unknown) {
|
||||||
@@ -425,6 +434,7 @@ async function withTransientRetry<T>(
|
|||||||
) {
|
) {
|
||||||
let attempt = 0;
|
let attempt = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
|
if (disposed) throw new Error("component disposed");
|
||||||
try {
|
try {
|
||||||
return await action();
|
return await action();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -716,21 +726,21 @@ async function refreshActiveTaskProgress(taskIds?: number[]) {
|
|||||||
|
|
||||||
function startHistoryPolling() {
|
function startHistoryPolling() {
|
||||||
stopHistoryPolling();
|
stopHistoryPolling();
|
||||||
if (!hasQueueWork.value) return;
|
if (disposed || !hasQueueWork.value) return;
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
historyPollTimer = null;
|
historyPollTimer = null;
|
||||||
if (!hasQueueWork.value) return;
|
if (disposed || !hasQueueWork.value) return;
|
||||||
await refreshActiveTaskProgress();
|
await refreshActiveTaskProgress();
|
||||||
if (hasQueueWork.value) {
|
if (!disposed && hasQueueWork.value) {
|
||||||
historyPollTimer = window.setTimeout(run, getTaskPollIntervalMs() * 2);
|
historyPollTimer = timers.setTimeout("history-poll", run, getTaskPollIntervalMs() * 2);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
historyPollTimer = window.setTimeout(run, getTaskPollIntervalMs() * 2);
|
historyPollTimer = timers.setTimeout("history-poll", run, getTaskPollIntervalMs() * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopHistoryPolling() {
|
function stopHistoryPolling() {
|
||||||
if (historyPollTimer) {
|
if (historyPollTimer) {
|
||||||
window.clearTimeout(historyPollTimer);
|
timers.clearTimer("history-poll", historyPollTimer);
|
||||||
historyPollTimer = null;
|
historyPollTimer = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -865,6 +875,7 @@ function findHistoryItemByTaskId(taskId: number) {
|
|||||||
async function waitForTaskTerminal(taskId: number) {
|
async function waitForTaskTerminal(taskId: number) {
|
||||||
let transientErrorCount = 0;
|
let transientErrorCount = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
|
if (disposed) return "STOPPED";
|
||||||
try {
|
try {
|
||||||
await refreshActiveTaskProgress([taskId]);
|
await refreshActiveTaskProgress([taskId]);
|
||||||
if (transientErrorCount > 0) {
|
if (transientErrorCount > 0) {
|
||||||
@@ -893,7 +904,7 @@ async function waitForTaskTerminal(taskId: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function processQueue() {
|
async function processQueue() {
|
||||||
if (queueWorkerRunning.value) return;
|
if (disposed || queueWorkerRunning.value) return;
|
||||||
queueWorkerRunning.value = true;
|
queueWorkerRunning.value = true;
|
||||||
pushing.value = true;
|
pushing.value = true;
|
||||||
startHistoryPolling();
|
startHistoryPolling();
|
||||||
@@ -904,7 +915,7 @@ async function processQueue() {
|
|||||||
throw new Error("当前客户端未提供 enqueue_json");
|
throw new Error("当前客户端未提供 enqueue_json");
|
||||||
}
|
}
|
||||||
|
|
||||||
while (activeTaskId.value || pendingQueue.value.length) {
|
while (!disposed && (activeTaskId.value || pendingQueue.value.length)) {
|
||||||
if (activeTaskId.value) {
|
if (activeTaskId.value) {
|
||||||
const finalStatus = await waitForTaskTerminal(activeTaskId.value);
|
const finalStatus = await waitForTaskTerminal(activeTaskId.value);
|
||||||
queuePushResult.value =
|
queuePushResult.value =
|
||||||
@@ -975,6 +986,7 @@ async function processQueue() {
|
|||||||
await refreshTaskViews();
|
await refreshTaskViews();
|
||||||
ElMessage.success("巡店删除队列已按顺序执行完成");
|
ElMessage.success("巡店删除队列已按顺序执行完成");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (disposed) return;
|
||||||
const message = error instanceof Error ? error.message : "队列执行失败";
|
const message = error instanceof Error ? error.message : "队列执行失败";
|
||||||
queuePushResult.value = message;
|
queuePushResult.value = message;
|
||||||
ElMessage.error(message);
|
ElMessage.error(message);
|
||||||
@@ -1061,7 +1073,10 @@ onMounted(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
disposed = true;
|
||||||
stopHistoryPolling();
|
stopHistoryPolling();
|
||||||
|
clearSleepTimers();
|
||||||
|
timers.clearScope();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -265,6 +265,7 @@ import { LISTING_FILTER_OPTIONS, type ListingFilterValue } from '@/pages/brand/c
|
|||||||
import { expandBrandFolderRecursive } from '@/shared/api/brand'
|
import { expandBrandFolderRecursive } from '@/shared/api/brand'
|
||||||
import { getPywebviewApi, type UploadedJavaFile } from '@/shared/bridges/pywebview'
|
import { getPywebviewApi, type UploadedJavaFile } from '@/shared/bridges/pywebview'
|
||||||
import { getTaskPollIntervalMs } from '@/shared/task-progress-config'
|
import { getTaskPollIntervalMs } from '@/shared/task-progress-config'
|
||||||
|
import { createCategorizedTimers } from '@/shared/utils/categorized-timers'
|
||||||
import {
|
import {
|
||||||
addPriceTrackCandidate,
|
addPriceTrackCandidate,
|
||||||
completePriceTrackLoopChild,
|
completePriceTrackLoopChild,
|
||||||
@@ -369,6 +370,17 @@ const taskDetails = ref<Record<number, string>>({})
|
|||||||
const taskSnapshots = ref<Record<number, PriceTrackTaskDetailVo>>({})
|
const taskSnapshots = ref<Record<number, PriceTrackTaskDetailVo>>({})
|
||||||
const pollTimer = ref<number | null>(null)
|
const pollTimer = ref<number | null>(null)
|
||||||
const pollingInFlight = ref(false)
|
const pollingInFlight = ref(false)
|
||||||
|
let disposed = false
|
||||||
|
const timers = createCategorizedTimers('price-track')
|
||||||
|
|
||||||
|
function sleep(ms: number) {
|
||||||
|
if (disposed) return Promise.resolve()
|
||||||
|
return timers.sleep('queue-wait', ms)
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSleepTimers() {
|
||||||
|
timers.clearCategory('queue-wait')
|
||||||
|
}
|
||||||
|
|
||||||
function uidForStorage() {
|
function uidForStorage() {
|
||||||
return typeof window !== 'undefined' ? window.localStorage.getItem('uid') || '0' : '0'
|
return typeof window !== 'undefined' ? window.localStorage.getItem('uid') || '0' : '0'
|
||||||
@@ -649,8 +661,8 @@ function onCountryDrop(toIndex: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function scheduleSaveCountryPref() {
|
function scheduleSaveCountryPref() {
|
||||||
if (countryPrefSaveTimer) clearTimeout(countryPrefSaveTimer)
|
if (countryPrefSaveTimer) timers.clearTimer('preference-save', countryPrefSaveTimer)
|
||||||
countryPrefSaveTimer = setTimeout(() => {
|
countryPrefSaveTimer = timers.setTimeout('preference-save', () => {
|
||||||
countryPrefSaveTimer = null
|
countryPrefSaveTimer = null
|
||||||
void persistCountryPref()
|
void persistCountryPref()
|
||||||
}, 450)
|
}, 450)
|
||||||
@@ -1043,6 +1055,7 @@ async function waitForTaskTerminal(taskId: number) {
|
|||||||
let transientErrorCount = 0
|
let transientErrorCount = 0
|
||||||
const maxTransientErrors = 30
|
const maxTransientErrors = 30
|
||||||
while (true) {
|
while (true) {
|
||||||
|
if (disposed) return 'STOPPED'
|
||||||
try {
|
try {
|
||||||
const batch = await getPriceTrackTaskProgressBatch([taskId])
|
const batch = await getPriceTrackTaskProgressBatch([taskId])
|
||||||
if (transientErrorCount > 0) {
|
if (transientErrorCount > 0) {
|
||||||
@@ -1086,10 +1099,10 @@ async function waitForTaskTerminal(taskId: number) {
|
|||||||
if (transientErrorCount === 1 || transientErrorCount % 5 === 0) {
|
if (transientErrorCount === 1 || transientErrorCount % 5 === 0) {
|
||||||
ElMessage.warning(`任务 ${taskId} 运行中,后端服务暂时不可用,正在自动重试`)
|
ElMessage.warning(`任务 ${taskId} 运行中,后端服务暂时不可用,正在自动重试`)
|
||||||
}
|
}
|
||||||
await new Promise<void>((resolve) => { window.setTimeout(() => resolve(), getPollIntervalMs()) })
|
await sleep(getPollIntervalMs())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
await new Promise<void>((resolve) => { window.setTimeout(() => resolve(), getPollIntervalMs()) })
|
await sleep(getPollIntervalMs())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1103,6 +1116,7 @@ function nextMatchedQueueItem() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function processMatchedQueue() {
|
async function processMatchedQueue() {
|
||||||
|
if (disposed) return
|
||||||
if (queueWorkerRunning.value) {
|
if (queueWorkerRunning.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1131,7 +1145,7 @@ async function processMatchedQueue() {
|
|||||||
let failedCount = 0
|
let failedCount = 0
|
||||||
try {
|
try {
|
||||||
let index = 0
|
let index = 0
|
||||||
while (autoQueueEnabled.value) {
|
while (!disposed && autoQueueEnabled.value) {
|
||||||
const row = nextMatchedQueueItem()
|
const row = nextMatchedQueueItem()
|
||||||
if (!row) {
|
if (!row) {
|
||||||
break
|
break
|
||||||
@@ -1141,6 +1155,7 @@ async function processMatchedQueue() {
|
|||||||
let attempt = 0
|
let attempt = 0
|
||||||
const taskVo = await (async () => {
|
const taskVo = await (async () => {
|
||||||
while (true) {
|
while (true) {
|
||||||
|
if (disposed) throw new Error('component disposed')
|
||||||
try {
|
try {
|
||||||
return await createPriceTrackTask({
|
return await createPriceTrackTask({
|
||||||
userId: 0,
|
userId: 0,
|
||||||
@@ -1156,7 +1171,7 @@ async function processMatchedQueue() {
|
|||||||
attempt += 1
|
attempt += 1
|
||||||
if (!transient || attempt >= 10) throw error
|
if (!transient || attempt >= 10) throw error
|
||||||
queuePushResult.value = '后端服务暂时不可用,正在重试创建任务(' + attempt + '/10)...'
|
queuePushResult.value = '后端服务暂时不可用,正在重试创建任务(' + attempt + '/10)...'
|
||||||
await new Promise<void>((resolve) => { window.setTimeout(() => resolve(), getPollIntervalMs()) })
|
await sleep(getPollIntervalMs())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
@@ -1197,6 +1212,7 @@ async function processMatchedQueue() {
|
|||||||
successCount += 1
|
successCount += 1
|
||||||
queuePushResult.value = '任务 ' + taskVo.taskId + ' 已完成'
|
queuePushResult.value = '任务 ' + taskVo.taskId + ' 已完成'
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (disposed) break
|
||||||
failedCount += 1
|
failedCount += 1
|
||||||
queuePushResult.value = '第 ' + index + ' 条任务处理失败,已自动继续下一条:' + (error instanceof Error ? error.message : '未知错误')
|
queuePushResult.value = '第 ' + index + ' 条任务处理失败,已自动继续下一条:' + (error instanceof Error ? error.message : '未知错误')
|
||||||
ElMessage.error(queuePushResult.value)
|
ElMessage.error(queuePushResult.value)
|
||||||
@@ -1208,6 +1224,7 @@ async function processMatchedQueue() {
|
|||||||
ElMessage.success('店铺任务推送已完成:成功 ' + successCount + ' 条,失败 ' + failedCount + ' 条')
|
ElMessage.success('店铺任务推送已完成:成功 ' + successCount + ' 条,失败 ' + failedCount + ' 条')
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (disposed) return
|
||||||
queuePushResult.value = e instanceof Error ? e.message : '推送异常'
|
queuePushResult.value = e instanceof Error ? e.message : '推送异常'
|
||||||
ElMessage.error(queuePushResult.value)
|
ElMessage.error(queuePushResult.value)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1282,6 +1299,7 @@ async function stopLoopExecution() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function runLoopExecution(loopId?: number) {
|
async function runLoopExecution(loopId?: number) {
|
||||||
|
if (disposed) return
|
||||||
if (loopDispatching.value) return
|
if (loopDispatching.value) return
|
||||||
if (loopId && activeLoopRunId.value !== loopId) {
|
if (loopId && activeLoopRunId.value !== loopId) {
|
||||||
activeLoopRunId.value = loopId
|
activeLoopRunId.value = loopId
|
||||||
@@ -1294,7 +1312,7 @@ async function runLoopExecution(loopId?: number) {
|
|||||||
}
|
}
|
||||||
loopDispatching.value = true
|
loopDispatching.value = true
|
||||||
try {
|
try {
|
||||||
while (activeLoopRunId.value) {
|
while (!disposed && activeLoopRunId.value) {
|
||||||
const loop = await syncActiveLoopRun()
|
const loop = await syncActiveLoopRun()
|
||||||
if (!loop || loop.status === 'SUCCESS' || loop.status === 'FAILED' || loop.status === 'STOPPED') break
|
if (!loop || loop.status === 'SUCCESS' || loop.status === 'FAILED' || loop.status === 'STOPPED') break
|
||||||
|
|
||||||
@@ -1306,6 +1324,7 @@ async function runLoopExecution(loopId?: number) {
|
|||||||
let completeAttempt = 0
|
let completeAttempt = 0
|
||||||
const updatedLoop = await (async () => {
|
const updatedLoop = await (async () => {
|
||||||
while (true) {
|
while (true) {
|
||||||
|
if (disposed) throw new Error('component disposed')
|
||||||
try {
|
try {
|
||||||
return await completePriceTrackLoopChild(loop.id, activeTaskId)
|
return await completePriceTrackLoopChild(loop.id, activeTaskId)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1314,7 +1333,7 @@ async function runLoopExecution(loopId?: number) {
|
|||||||
completeAttempt += 1
|
completeAttempt += 1
|
||||||
if (!transient || completeAttempt >= 10) throw error
|
if (!transient || completeAttempt >= 10) throw error
|
||||||
queuePushResult.value = `循环任务 ${loop.id} 等待后端恢复后确认子任务完成(${completeAttempt}/10)...`
|
queuePushResult.value = `循环任务 ${loop.id} 等待后端恢复后确认子任务完成(${completeAttempt}/10)...`
|
||||||
await new Promise<void>((resolve) => { window.setTimeout(() => resolve(), getPollIntervalMs()) })
|
await sleep(getPollIntervalMs())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
@@ -1332,6 +1351,7 @@ async function runLoopExecution(loopId?: number) {
|
|||||||
let dispatchAttempt = 0
|
let dispatchAttempt = 0
|
||||||
const dispatch = await (async () => {
|
const dispatch = await (async () => {
|
||||||
while (true) {
|
while (true) {
|
||||||
|
if (disposed) throw new Error('component disposed')
|
||||||
try {
|
try {
|
||||||
return await dispatchNextPriceTrackLoopRun(loop.id)
|
return await dispatchNextPriceTrackLoopRun(loop.id)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1340,7 +1360,7 @@ async function runLoopExecution(loopId?: number) {
|
|||||||
dispatchAttempt += 1
|
dispatchAttempt += 1
|
||||||
if (!transient || dispatchAttempt >= 10) throw error
|
if (!transient || dispatchAttempt >= 10) throw error
|
||||||
queuePushResult.value = `循环任务 ${loop.id} 等待后端恢复后继续派发(${dispatchAttempt}/10)...`
|
queuePushResult.value = `循环任务 ${loop.id} 等待后端恢复后继续派发(${dispatchAttempt}/10)...`
|
||||||
await new Promise<void>((resolve) => { window.setTimeout(() => resolve(), getPollIntervalMs()) })
|
await sleep(getPollIntervalMs())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
@@ -1355,6 +1375,7 @@ async function runLoopExecution(loopId?: number) {
|
|||||||
let createAttempt = 0
|
let createAttempt = 0
|
||||||
const taskVo = await (async () => {
|
const taskVo = await (async () => {
|
||||||
while (true) {
|
while (true) {
|
||||||
|
if (disposed) throw new Error('component disposed')
|
||||||
try {
|
try {
|
||||||
return await createPriceTrackTask(childTaskRequest)
|
return await createPriceTrackTask(childTaskRequest)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1363,7 +1384,7 @@ async function runLoopExecution(loopId?: number) {
|
|||||||
createAttempt += 1
|
createAttempt += 1
|
||||||
if (!transient || createAttempt >= 10) throw error
|
if (!transient || createAttempt >= 10) throw error
|
||||||
queuePushResult.value = `循环任务 ${loop.id} 等待后端恢复后重试创建子任务(${createAttempt}/10)...`
|
queuePushResult.value = `循环任务 ${loop.id} 等待后端恢复后重试创建子任务(${createAttempt}/10)...`
|
||||||
await new Promise<void>((resolve) => { window.setTimeout(() => resolve(), getPollIntervalMs()) })
|
await sleep(getPollIntervalMs())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
@@ -1483,25 +1504,27 @@ async function refreshTaskBatch() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function scheduleNextPoll(immediate = false) {
|
function scheduleNextPoll(immediate = false) {
|
||||||
|
if (disposed) return
|
||||||
if (pollTimer.value) {
|
if (pollTimer.value) {
|
||||||
if (!immediate) return
|
if (!immediate) return
|
||||||
window.clearTimeout(pollTimer.value)
|
timers.clearTimer('task-poll', pollTimer.value)
|
||||||
pollTimer.value = null
|
pollTimer.value = null
|
||||||
}
|
}
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
pollTimer.value = null
|
pollTimer.value = null
|
||||||
|
if (disposed) return
|
||||||
if (pollingInFlight.value) { scheduleNextPoll(); return }
|
if (pollingInFlight.value) { scheduleNextPoll(); return }
|
||||||
if (!pollingTaskIds.value.length) return
|
if (!pollingTaskIds.value.length) return
|
||||||
pollingInFlight.value = true
|
pollingInFlight.value = true
|
||||||
try { await refreshTaskBatch() } finally { pollingInFlight.value = false }
|
try { await refreshTaskBatch() } finally { pollingInFlight.value = false }
|
||||||
if (pollingTaskIds.value.length > 0) pollTimer.value = window.setTimeout(run, getPollIntervalMs())
|
if (!disposed && pollingTaskIds.value.length > 0) pollTimer.value = timers.setTimeout('task-poll', run, getPollIntervalMs())
|
||||||
}
|
}
|
||||||
if (immediate) void run()
|
if (immediate) void run()
|
||||||
else pollTimer.value = window.setTimeout(run, getPollIntervalMs())
|
else pollTimer.value = timers.setTimeout('task-poll', run, getPollIntervalMs())
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopPolling() {
|
function stopPolling() {
|
||||||
if (pollTimer.value) { window.clearTimeout(pollTimer.value); pollTimer.value = null }
|
if (pollTimer.value) { timers.clearTimer('task-poll', pollTimer.value); pollTimer.value = null }
|
||||||
}
|
}
|
||||||
|
|
||||||
function addPollingTask(taskId: number) {
|
function addPollingTask(taskId: number) {
|
||||||
@@ -1658,8 +1681,11 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
disposed = true
|
||||||
stopPolling()
|
stopPolling()
|
||||||
if (countryPrefSaveTimer) clearTimeout(countryPrefSaveTimer)
|
clearSleepTimers()
|
||||||
|
timers.clearScope()
|
||||||
|
if (countryPrefSaveTimer) timers.clearTimer('preference-save', countryPrefSaveTimer)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ import {
|
|||||||
} from '@/shared/api/java-modules'
|
} from '@/shared/api/java-modules'
|
||||||
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 { createCategorizedTimers } from '@/shared/utils/categorized-timers'
|
||||||
|
|
||||||
const shopInput = ref('')
|
const shopInput = ref('')
|
||||||
const candidates = ref<ProductRiskCandidateVo[]>([])
|
const candidates = ref<ProductRiskCandidateVo[]>([])
|
||||||
@@ -327,8 +328,8 @@ function onCountryDrop(toIndex: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function scheduleSaveCountryPref() {
|
function scheduleSaveCountryPref() {
|
||||||
if (countryPrefSaveTimer) clearTimeout(countryPrefSaveTimer)
|
if (countryPrefSaveTimer) timers.clearTimer('preference-save', countryPrefSaveTimer)
|
||||||
countryPrefSaveTimer = setTimeout(() => {
|
countryPrefSaveTimer = timers.setTimeout('preference-save', () => {
|
||||||
countryPrefSaveTimer = null
|
countryPrefSaveTimer = null
|
||||||
void persistCountryPref()
|
void persistCountryPref()
|
||||||
}, 450)
|
}, 450)
|
||||||
@@ -374,6 +375,8 @@ const taskDetails = ref<Record<number, string>>({})
|
|||||||
const taskSnapshots = ref<Record<number, ProductRiskTaskDetailVo>>({})
|
const taskSnapshots = ref<Record<number, ProductRiskTaskDetailVo>>({})
|
||||||
const pollTimer = ref<number | null>(null)
|
const pollTimer = ref<number | null>(null)
|
||||||
const pollingInFlight = ref(false)
|
const pollingInFlight = ref(false)
|
||||||
|
let disposed = false
|
||||||
|
const timers = createCategorizedTimers('product-risk')
|
||||||
|
|
||||||
function uidForStorage() {
|
function uidForStorage() {
|
||||||
return typeof window !== 'undefined' ? window.localStorage.getItem('uid') || '0' : '0'
|
return typeof window !== 'undefined' ? window.localStorage.getItem('uid') || '0' : '0'
|
||||||
@@ -684,9 +687,12 @@ const TRANSIENT_BACKEND_ERROR_PATTERNS = [
|
|||||||
] as const
|
] as const
|
||||||
|
|
||||||
function sleep(ms: number) {
|
function sleep(ms: number) {
|
||||||
return new Promise<void>((resolve) => {
|
if (disposed) return Promise.resolve()
|
||||||
window.setTimeout(() => resolve(), ms)
|
return timers.sleep('queue-wait', ms)
|
||||||
})
|
}
|
||||||
|
|
||||||
|
function clearSleepTimers() {
|
||||||
|
timers.clearCategory('queue-wait')
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTransientBackendError(error: unknown) {
|
function isTransientBackendError(error: unknown) {
|
||||||
@@ -697,6 +703,7 @@ function isTransientBackendError(error: unknown) {
|
|||||||
async function createProductRiskTaskWithRetry(items: ProductRiskShopQueueItem[], maxAttempts = 10) {
|
async function createProductRiskTaskWithRetry(items: ProductRiskShopQueueItem[], maxAttempts = 10) {
|
||||||
let attempt = 0
|
let attempt = 0
|
||||||
while (true) {
|
while (true) {
|
||||||
|
if (disposed) throw new Error('component disposed')
|
||||||
try {
|
try {
|
||||||
return await createProductRiskTask(items)
|
return await createProductRiskTask(items)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -751,12 +758,13 @@ function getPollIntervalMs() {
|
|||||||
function scheduleNextPoll(immediate = false) {
|
function scheduleNextPoll(immediate = false) {
|
||||||
if (pollTimer.value) {
|
if (pollTimer.value) {
|
||||||
if (!immediate) return
|
if (!immediate) return
|
||||||
window.clearTimeout(pollTimer.value)
|
timers.clearTimer('task-poll', pollTimer.value)
|
||||||
pollTimer.value = null
|
pollTimer.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
pollTimer.value = null
|
pollTimer.value = null
|
||||||
|
if (disposed) return
|
||||||
if (pollingInFlight.value) {
|
if (pollingInFlight.value) {
|
||||||
scheduleNextPoll()
|
scheduleNextPoll()
|
||||||
return
|
return
|
||||||
@@ -768,13 +776,14 @@ function scheduleNextPoll(immediate = false) {
|
|||||||
} finally {
|
} finally {
|
||||||
pollingInFlight.value = false
|
pollingInFlight.value = false
|
||||||
}
|
}
|
||||||
if (pollingTaskIds.value.length > 0) {
|
if (!disposed && pollingTaskIds.value.length > 0) {
|
||||||
pollTimer.value = window.setTimeout(run, getPollIntervalMs())
|
pollTimer.value = timers.setTimeout('task-poll', run, getPollIntervalMs())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (disposed) return
|
||||||
if (immediate) void run()
|
if (immediate) void run()
|
||||||
else pollTimer.value = window.setTimeout(run, getPollIntervalMs())
|
else pollTimer.value = timers.setTimeout('task-poll', run, getPollIntervalMs())
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensurePolling(immediate = false) {
|
function ensurePolling(immediate = false) {
|
||||||
@@ -784,7 +793,7 @@ function ensurePolling(immediate = false) {
|
|||||||
|
|
||||||
function stopPolling() {
|
function stopPolling() {
|
||||||
if (pollTimer.value) {
|
if (pollTimer.value) {
|
||||||
window.clearTimeout(pollTimer.value)
|
timers.clearTimer('task-poll', pollTimer.value)
|
||||||
pollTimer.value = null
|
pollTimer.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -793,6 +802,7 @@ async function waitForTaskTerminal(taskId: number) {
|
|||||||
let transientErrorCount = 0
|
let transientErrorCount = 0
|
||||||
const maxTransientErrors = 30
|
const maxTransientErrors = 30
|
||||||
while (true) {
|
while (true) {
|
||||||
|
if (disposed) return 'STOPPED'
|
||||||
try {
|
try {
|
||||||
const batch = await getProductRiskTaskProgressBatch([taskId])
|
const batch = await getProductRiskTaskProgressBatch([taskId])
|
||||||
if (transientErrorCount > 0) {
|
if (transientErrorCount > 0) {
|
||||||
@@ -1027,7 +1037,7 @@ function nextMatchedQueueItem() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function processMatchedQueue() {
|
async function processMatchedQueue() {
|
||||||
if (queueWorkerRunning.value) {
|
if (disposed || queueWorkerRunning.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!matchedItems.value.length) {
|
if (!matchedItems.value.length) {
|
||||||
@@ -1051,7 +1061,7 @@ async function processMatchedQueue() {
|
|||||||
let failedCount = 0
|
let failedCount = 0
|
||||||
try {
|
try {
|
||||||
let index = 0
|
let index = 0
|
||||||
while (autoQueueEnabled.value) {
|
while (!disposed && autoQueueEnabled.value) {
|
||||||
const item = nextMatchedQueueItem()
|
const item = nextMatchedQueueItem()
|
||||||
if (!item) {
|
if (!item) {
|
||||||
break
|
break
|
||||||
@@ -1102,6 +1112,7 @@ async function processMatchedQueue() {
|
|||||||
successCount += 1
|
successCount += 1
|
||||||
queuePushResult.value = '任务 ' + taskId + ' 已完成'
|
queuePushResult.value = '任务 ' + taskId + ' 已完成'
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (disposed) break
|
||||||
failedCount += 1
|
failedCount += 1
|
||||||
queuePushResult.value = '第 ' + index + ' 条任务处理失败,已自动继续下一条:' + (error instanceof Error ? error.message : '未知错误')
|
queuePushResult.value = '第 ' + index + ' 条任务处理失败,已自动继续下一条:' + (error instanceof Error ? error.message : '未知错误')
|
||||||
ElMessage.error(queuePushResult.value)
|
ElMessage.error(queuePushResult.value)
|
||||||
@@ -1115,6 +1126,7 @@ async function processMatchedQueue() {
|
|||||||
await loadHistory()
|
await loadHistory()
|
||||||
ensurePolling(true)
|
ensurePolling(true)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (disposed) return
|
||||||
queuePushResult.value = e instanceof Error ? e.message : '推送异常'
|
queuePushResult.value = e instanceof Error ? e.message : '推送异常'
|
||||||
ElMessage.error(queuePushResult.value)
|
ElMessage.error(queuePushResult.value)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1141,8 +1153,11 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
disposed = true
|
||||||
stopPolling()
|
stopPolling()
|
||||||
if (countryPrefSaveTimer) clearTimeout(countryPrefSaveTimer)
|
clearSleepTimers()
|
||||||
|
timers.clearScope()
|
||||||
|
if (countryPrefSaveTimer) timers.clearTimer('preference-save', countryPrefSaveTimer)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -333,6 +333,7 @@ import {
|
|||||||
} from "@/shared/api/java-modules";
|
} from "@/shared/api/java-modules";
|
||||||
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 { createCategorizedTimers } from "@/shared/utils/categorized-timers";
|
||||||
|
|
||||||
const MAX_TRANSIENT_ERRORS = 30;
|
const MAX_TRANSIENT_ERRORS = 30;
|
||||||
|
|
||||||
@@ -358,6 +359,7 @@ const activeQueueItem = ref<QueryAsinShopQueueItem | null>(null);
|
|||||||
const queueWorkerRunning = ref(false);
|
const queueWorkerRunning = ref(false);
|
||||||
const autoQueueEnabled = ref(false);
|
const autoQueueEnabled = ref(false);
|
||||||
let historyPollTimer: number | null = null;
|
let historyPollTimer: number | null = null;
|
||||||
|
const timers = createCategorizedTimers("query-asin");
|
||||||
|
|
||||||
const matchedRunnableItems = computed(() =>
|
const matchedRunnableItems = computed(() =>
|
||||||
matchedItems.value.filter((item) => item.matched && (item.queryAsins || []).some((row) => row.asins?.length)),
|
matchedItems.value.filter((item) => item.matched && (item.queryAsins || []).some((row) => row.asins?.length)),
|
||||||
@@ -398,8 +400,15 @@ function historyItemKey(item: QueryAsinHistoryItem) {
|
|||||||
return `${item.taskId ?? 0}:${item.resultId ?? 0}`;
|
return `${item.taskId ?? 0}:${item.resultId ?? 0}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let disposed = false;
|
||||||
|
|
||||||
function sleep(ms: number) {
|
function sleep(ms: number) {
|
||||||
return new Promise((resolve) => window.setTimeout(resolve, ms));
|
if (disposed) return Promise.resolve();
|
||||||
|
return timers.sleep("queue-wait", ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSleepTimers() {
|
||||||
|
timers.clearCategory("queue-wait");
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTransientBackendError(error: unknown) {
|
function isTransientBackendError(error: unknown) {
|
||||||
@@ -424,6 +433,7 @@ async function withTransientRetry<T>(
|
|||||||
) {
|
) {
|
||||||
let attempt = 0;
|
let attempt = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
|
if (disposed) throw new Error("component disposed");
|
||||||
try {
|
try {
|
||||||
return await action();
|
return await action();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -747,21 +757,21 @@ async function refreshActiveTaskProgress(taskIds?: number[]) {
|
|||||||
|
|
||||||
function startHistoryPolling() {
|
function startHistoryPolling() {
|
||||||
stopHistoryPolling();
|
stopHistoryPolling();
|
||||||
if (!hasQueueWork.value) return;
|
if (disposed || !hasQueueWork.value) return;
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
historyPollTimer = null;
|
historyPollTimer = null;
|
||||||
if (!hasQueueWork.value) return;
|
if (disposed || !hasQueueWork.value) return;
|
||||||
await refreshActiveTaskProgress();
|
await refreshActiveTaskProgress();
|
||||||
if (hasQueueWork.value) {
|
if (!disposed && hasQueueWork.value) {
|
||||||
historyPollTimer = window.setTimeout(run, getTaskPollIntervalMs() * 2);
|
historyPollTimer = timers.setTimeout("history-poll", run, getTaskPollIntervalMs() * 2);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
historyPollTimer = window.setTimeout(run, getTaskPollIntervalMs() * 2);
|
historyPollTimer = timers.setTimeout("history-poll", run, getTaskPollIntervalMs() * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopHistoryPolling() {
|
function stopHistoryPolling() {
|
||||||
if (historyPollTimer) {
|
if (historyPollTimer) {
|
||||||
window.clearTimeout(historyPollTimer);
|
timers.clearTimer("history-poll", historyPollTimer);
|
||||||
historyPollTimer = null;
|
historyPollTimer = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -893,6 +903,7 @@ function findHistoryItemByTaskId(taskId: number) {
|
|||||||
async function waitForTaskTerminal(taskId: number) {
|
async function waitForTaskTerminal(taskId: number) {
|
||||||
let transientErrorCount = 0;
|
let transientErrorCount = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
|
if (disposed) return "STOPPED";
|
||||||
try {
|
try {
|
||||||
await refreshActiveTaskProgress([taskId]);
|
await refreshActiveTaskProgress([taskId]);
|
||||||
if (activeTaskId.value !== taskId) {
|
if (activeTaskId.value !== taskId) {
|
||||||
@@ -924,7 +935,7 @@ async function waitForTaskTerminal(taskId: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function processQueue() {
|
async function processQueue() {
|
||||||
if (queueWorkerRunning.value) return;
|
if (disposed || queueWorkerRunning.value) return;
|
||||||
queueWorkerRunning.value = true;
|
queueWorkerRunning.value = true;
|
||||||
pushing.value = true;
|
pushing.value = true;
|
||||||
startHistoryPolling();
|
startHistoryPolling();
|
||||||
@@ -935,7 +946,7 @@ async function processQueue() {
|
|||||||
throw new Error("当前客户端未提供 enqueue_json");
|
throw new Error("当前客户端未提供 enqueue_json");
|
||||||
}
|
}
|
||||||
|
|
||||||
while (activeTaskId.value || pendingQueue.value.length) {
|
while (!disposed && (activeTaskId.value || pendingQueue.value.length)) {
|
||||||
if (activeTaskId.value) {
|
if (activeTaskId.value) {
|
||||||
const finalStatus = await waitForTaskTerminal(activeTaskId.value);
|
const finalStatus = await waitForTaskTerminal(activeTaskId.value);
|
||||||
queuePushResult.value =
|
queuePushResult.value =
|
||||||
@@ -1002,6 +1013,7 @@ async function processQueue() {
|
|||||||
await refreshTaskViews();
|
await refreshTaskViews();
|
||||||
ElMessage.success("查询ASIN队列已按顺序执行完成");
|
ElMessage.success("查询ASIN队列已按顺序执行完成");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (disposed) return;
|
||||||
const message = error instanceof Error ? error.message : "队列执行失败";
|
const message = error instanceof Error ? error.message : "队列执行失败";
|
||||||
queuePushResult.value = message;
|
queuePushResult.value = message;
|
||||||
ElMessage.error(message);
|
ElMessage.error(message);
|
||||||
@@ -1092,7 +1104,10 @@ onMounted(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
disposed = true;
|
||||||
stopHistoryPolling();
|
stopHistoryPolling();
|
||||||
|
clearSleepTimers();
|
||||||
|
timers.clearScope();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ import { LISTING_FILTER_OPTIONS, type ListingFilterValue } from '@/pages/brand/c
|
|||||||
import { activateShopMatchTask, addShopMatchCandidate, createShopMatchTask, deleteShopMatchCandidate, deleteShopMatchHistory, deleteShopMatchTask, getShopMatchCountryPreference, getShopMatchDashboard, getShopMatchHistory, getShopMatchResultDownloadUrl, getShopMatchTaskProgressBatch, getShopMatchTasksBatch, listShopMatchCandidates, matchShopMatchShops, putShopMatchCountryPreference, type ShopMatchCandidateVo, type ShopMatchDashboardVo, type ShopMatchHistoryItem, type ShopMatchShopQueueItem, type ShopMatchTaskDetailVo } from '@/shared/api/java-modules'
|
import { activateShopMatchTask, addShopMatchCandidate, createShopMatchTask, deleteShopMatchCandidate, deleteShopMatchHistory, deleteShopMatchTask, getShopMatchCountryPreference, getShopMatchDashboard, getShopMatchHistory, getShopMatchResultDownloadUrl, getShopMatchTaskProgressBatch, getShopMatchTasksBatch, listShopMatchCandidates, matchShopMatchShops, putShopMatchCountryPreference, type ShopMatchCandidateVo, type ShopMatchDashboardVo, type ShopMatchHistoryItem, type ShopMatchShopQueueItem, type ShopMatchTaskDetailVo } from '@/shared/api/java-modules'
|
||||||
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 { createCategorizedTimers } from '@/shared/utils/categorized-timers'
|
||||||
|
|
||||||
const COUNTRY_OPTIONS = [{ code: 'DE', label: '德国' }, { code: 'UK', label: '英国' }, { code: 'FR', label: '法国' }, { code: 'IT', label: '意大利' }, { code: 'ES', label: '西班牙' }] as const
|
const COUNTRY_OPTIONS = [{ code: 'DE', label: '德国' }, { code: 'UK', label: '英国' }, { code: 'FR', label: '法国' }, { code: 'IT', label: '意大利' }, { code: 'ES', label: '西班牙' }] as const
|
||||||
const shopInput = ref('')
|
const shopInput = ref('')
|
||||||
@@ -144,6 +145,8 @@ const scheduledDispatchInFlight = ref(false)
|
|||||||
let scheduleHeartbeatTimer: number | null = null
|
let scheduleHeartbeatTimer: number | null = null
|
||||||
let lastScheduledErrorAt = 0
|
let lastScheduledErrorAt = 0
|
||||||
let lastScheduledErrorKey = ''
|
let lastScheduledErrorKey = ''
|
||||||
|
let disposed = false
|
||||||
|
const timers = createCategorizedTimers('shop-match')
|
||||||
const currentSectionItems = computed(() => uniqueTaskRows(historyItems.value.filter((item) => { const taskId = normalizeTaskId(item.taskId); return !!taskId && !isTaskTerminalItem(item) }), taskSnapshots.value, 'current'))
|
const currentSectionItems = computed(() => uniqueTaskRows(historyItems.value.filter((item) => { const taskId = normalizeTaskId(item.taskId); return !!taskId && !isTaskTerminalItem(item) }), taskSnapshots.value, 'current'))
|
||||||
const historySectionItems = computed(() => uniqueTaskRows(historyItems.value.filter((item) => { const taskId = normalizeTaskId(item.taskId); return !taskId || isTaskTerminalItem(item) }), taskSnapshots.value, 'history'))
|
const historySectionItems = computed(() => uniqueTaskRows(historyItems.value.filter((item) => { const taskId = normalizeTaskId(item.taskId); return !taskId || isTaskTerminalItem(item) }), taskSnapshots.value, 'history'))
|
||||||
const hasVisibleList = computed(() => currentSectionItems.value.length > 0 || historySectionItems.value.length > 0)
|
const hasVisibleList = computed(() => currentSectionItems.value.length > 0 || historySectionItems.value.length > 0)
|
||||||
@@ -194,7 +197,7 @@ function saveMatchedItemsToStorage() { setStorageJson(matchedItemsStorageKey(),
|
|||||||
function isTaskMissingError(error: unknown) { const message = error instanceof Error ? error.message : String(error || ''); return /记录不存在|任务不存在|不存在|已删除|not\s*found|404/i.test(message) }
|
function isTaskMissingError(error: unknown) { const message = error instanceof Error ? error.message : String(error || ''); return /记录不存在|任务不存在|不存在|已删除|not\s*found|404/i.test(message) }
|
||||||
function syncPollingIdsWithTaskState() { const nextIds = pollingTaskIds.value.filter((taskId) => (taskDetails.value[taskId] || taskSnapshots.value[taskId]?.task?.status) === 'RUNNING'); if (nextIds.length === pollingTaskIds.value.length) return; pollingTaskIds.value = nextIds; savePollingIds() }
|
function syncPollingIdsWithTaskState() { const nextIds = pollingTaskIds.value.filter((taskId) => (taskDetails.value[taskId] || taskSnapshots.value[taskId]?.task?.status) === 'RUNNING'); if (nextIds.length === pollingTaskIds.value.length) return; pollingTaskIds.value = nextIds; savePollingIds() }
|
||||||
function addPollingTask(taskId: number) { if (!pollingTaskIds.value.includes(taskId)) { pollingTaskIds.value = [...pollingTaskIds.value, taskId]; savePollingIds() } }
|
function addPollingTask(taskId: number) { if (!pollingTaskIds.value.includes(taskId)) { pollingTaskIds.value = [...pollingTaskIds.value, taskId]; savePollingIds() } }
|
||||||
function clearDispatchTimer(taskId: number) { const timer = dispatchTimers.get(taskId); if (timer) { window.clearTimeout(timer); dispatchTimers.delete(taskId) } }
|
function clearDispatchTimer(taskId: number) { const timer = dispatchTimers.get(taskId); if (timer) { timers.clearTimer('scheduled-dispatch', timer); dispatchTimers.delete(taskId); if (!dispatchTimers.size) stopScheduleHeartbeat() } }
|
||||||
function removePollingTask(taskId: number) {
|
function removePollingTask(taskId: number) {
|
||||||
clearDispatchTimer(taskId)
|
clearDispatchTimer(taskId)
|
||||||
pollingTaskIds.value = pollingTaskIds.value.filter((id) => id !== taskId)
|
pollingTaskIds.value = pollingTaskIds.value.filter((id) => id !== taskId)
|
||||||
@@ -226,7 +229,7 @@ async function loadDashboard() { dashboard.value = await getShopMatchDashboard()
|
|||||||
async function loadHistory() { const data = await getShopMatchHistory(); historyItems.value = data.items || [] }
|
async function loadHistory() { const data = await getShopMatchHistory(); historyItems.value = data.items || [] }
|
||||||
async function loadCountryPreference() { try { const data = await getShopMatchCountryPreference(); if (countryPrefUserTouched.value) return; const codes = (data.country_codes || []).filter((item) => COUNTRY_OPTIONS.some((option) => option.code === item)); if (codes.length) orderedCountryCodes.value = codes } catch {} }
|
async function loadCountryPreference() { try { const data = await getShopMatchCountryPreference(); if (countryPrefUserTouched.value) return; const codes = (data.country_codes || []).filter((item) => COUNTRY_OPTIONS.some((option) => option.code === item)); if (codes.length) orderedCountryCodes.value = codes } catch {} }
|
||||||
async function refreshTaskViewsBestEffort() { await Promise.allSettled([loadHistory(), loadDashboard()]) }
|
async function refreshTaskViewsBestEffort() { await Promise.allSettled([loadHistory(), loadDashboard()]) }
|
||||||
function scheduleSaveCountryPreference() { if (countryPrefSaveTimer) clearTimeout(countryPrefSaveTimer); countryPrefSaveTimer = setTimeout(() => { countryPrefSaveTimer = null; void saveCountryPreference() }, 450) }
|
function scheduleSaveCountryPreference() { if (countryPrefSaveTimer) timers.clearTimer('preference-save', countryPrefSaveTimer); countryPrefSaveTimer = timers.setTimeout('preference-save', () => { countryPrefSaveTimer = null; void saveCountryPreference() }, 450) }
|
||||||
async function saveCountryPreference() { if (!orderedCountryCodes.value.length) return; countryPrefSaving.value = true; try { await putShopMatchCountryPreference(orderedCountryCodes.value) } catch (error) { ElMessage.error(error instanceof Error ? error.message : '保存国家配置失败') } finally { countryPrefSaving.value = false } }
|
async function saveCountryPreference() { if (!orderedCountryCodes.value.length) return; countryPrefSaving.value = true; try { await putShopMatchCountryPreference(orderedCountryCodes.value) } catch (error) { ElMessage.error(error instanceof Error ? error.message : '保存国家配置失败') } finally { countryPrefSaving.value = false } }
|
||||||
function onSelectionChange(rows: ShopMatchCandidateVo[]) { selectedCandidates.value = rows }
|
function onSelectionChange(rows: ShopMatchCandidateVo[]) { selectedCandidates.value = rows }
|
||||||
async function confirmAdd() { const name = shopInput.value.trim(); if (!name) { ElMessage.warning('请输入店铺名'); return } adding.value = true; try { await addShopMatchCandidate(name); shopInput.value = ''; await loadCandidates(); await loadDashboard() } catch (error) { ElMessage.error(error instanceof Error ? error.message : '添加失败') } finally { adding.value = false } }
|
async function confirmAdd() { const name = shopInput.value.trim(); if (!name) { ElMessage.warning('请输入店铺名'); return } adding.value = true; try { await addShopMatchCandidate(name); shopInput.value = ''; await loadCandidates(); await loadDashboard() } catch (error) { ElMessage.error(error instanceof Error ? error.message : '添加失败') } finally { adding.value = false } }
|
||||||
@@ -320,25 +323,27 @@ async function pumpScheduledQueue() {
|
|||||||
restoreScheduledDispatches()
|
restoreScheduledDispatches()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function scheduleDispatch(taskId: number, scheduledAt?: string) { if (!scheduledAt) return; clearDispatchTimer(taskId); const time = scheduledTimestamp(scheduledAt); if (!Number.isFinite(time)) return; const delay = Math.max(0, time - Date.now()); const timer = window.setTimeout(() => { dispatchTimers.delete(taskId); void pumpScheduledQueue() }, delay); dispatchTimers.set(taskId, timer) }
|
function scheduleDispatch(taskId: number, scheduledAt?: string) { if (disposed || !scheduledAt) return; clearDispatchTimer(taskId); const time = scheduledTimestamp(scheduledAt); if (!Number.isFinite(time)) return; const delay = Math.max(0, time - Date.now()); const timer = timers.setTimeout('scheduled-dispatch', () => { dispatchTimers.delete(taskId); if (!dispatchTimers.size) stopScheduleHeartbeat(); if (!disposed) void pumpScheduledQueue() }, delay); dispatchTimers.set(taskId, timer); startScheduleHeartbeat() }
|
||||||
function restoreScheduledDispatches() { const taskIds = new Set<number>(pollingTaskIds.value); for (const key of Object.keys(taskSnapshots.value)) { const id = normalizeTaskId(key); if (id > 0) taskIds.add(id) } for (const item of historyItems.value) { const id = normalizeTaskId(item.taskId); if (id > 0) taskIds.add(id) } for (const taskId of taskIds) { const snapshot = taskSnapshots.value[taskId]; const taskStatus = snapshot?.task?.status || taskDetails.value[taskId]; const stage = nextScheduleStage(taskId); if (taskStatus === 'SCHEDULED' && stage?.scheduledAt) scheduleDispatch(taskId, stage.scheduledAt); else clearDispatchTimer(taskId) } void pumpScheduledQueue() }
|
function restoreScheduledDispatches() { if (disposed) return; const taskIds = new Set<number>(pollingTaskIds.value); for (const key of Object.keys(taskSnapshots.value)) { const id = normalizeTaskId(key); if (id > 0) taskIds.add(id) } for (const item of historyItems.value) { const id = normalizeTaskId(item.taskId); if (id > 0) taskIds.add(id) } for (const taskId of taskIds) { const snapshot = taskSnapshots.value[taskId]; const taskStatus = snapshot?.task?.status || taskDetails.value[taskId]; const stage = nextScheduleStage(taskId); if (taskStatus === 'SCHEDULED' && stage?.scheduledAt) scheduleDispatch(taskId, stage.scheduledAt); else clearDispatchTimer(taskId) } if (dispatchTimers.size) startScheduleHeartbeat(); else stopScheduleHeartbeat(); void pumpScheduledQueue() }
|
||||||
function startScheduleHeartbeat() { if (scheduleHeartbeatTimer) return; scheduleHeartbeatTimer = window.setInterval(() => { void pumpScheduledQueue() }, 1000) }
|
function startScheduleHeartbeat() { if (disposed || scheduleHeartbeatTimer || !dispatchTimers.size) return; scheduleHeartbeatTimer = timers.setInterval('schedule-heartbeat', () => { if (!disposed) void pumpScheduledQueue() }, 30000) }
|
||||||
function stopScheduleHeartbeat() { if (!scheduleHeartbeatTimer) return; window.clearInterval(scheduleHeartbeatTimer); scheduleHeartbeatTimer = null }
|
function stopScheduleHeartbeat() { if (!scheduleHeartbeatTimer) return; timers.clearTimer('schedule-heartbeat', scheduleHeartbeatTimer); scheduleHeartbeatTimer = null }
|
||||||
function handleScheduleRecovery() { restoreScheduledDispatches(); if (pollingTaskIds.value.length) ensurePolling(true) }
|
function handleScheduleRecovery() { restoreScheduledDispatches(); if (pollingTaskIds.value.length) ensurePolling(true) }
|
||||||
const TRANSIENT_BACKEND_ERROR_PATTERNS = ['无法连接到后端服务', 'bad gateway', 'gateway timeout', 'network error', 'timeout', '502', '503', '504'] as const
|
const TRANSIENT_BACKEND_ERROR_PATTERNS = ['无法连接到后端服务', 'bad gateway', 'gateway timeout', 'network error', 'timeout', '502', '503', '504'] as const
|
||||||
function sleep(ms: number) { return new Promise<void>((resolve) => { window.setTimeout(() => resolve(), ms) }) }
|
function sleep(ms: number) { if (disposed) return Promise.resolve(); return timers.sleep('queue-wait', ms) }
|
||||||
|
function clearSleepTimers() { timers.clearCategory('queue-wait') }
|
||||||
function isTransientBackendError(error: unknown) { const message = (error instanceof Error ? error.message : String(error || '')).toLowerCase(); return TRANSIENT_BACKEND_ERROR_PATTERNS.some((pattern) => message.includes(pattern.toLowerCase())) }
|
function isTransientBackendError(error: unknown) { const message = (error instanceof Error ? error.message : String(error || '')).toLowerCase(); return TRANSIENT_BACKEND_ERROR_PATTERNS.some((pattern) => message.includes(pattern.toLowerCase())) }
|
||||||
async function withTransientRetry<T>(action: () => Promise<T>, onRetry: (attempt: number, maxAttempts: number) => void, maxAttempts = 10) { let attempt = 0; while (true) { try { return await action() } catch (error) { attempt += 1; if (!isTransientBackendError(error) || attempt >= maxAttempts) throw error; onRetry(attempt, maxAttempts); await sleep(getPollIntervalMs()) } } }
|
async function withTransientRetry<T>(action: () => Promise<T>, onRetry: (attempt: number, maxAttempts: number) => void, maxAttempts = 10) { let attempt = 0; while (true) { if (disposed) throw new Error('component disposed'); try { return await action() } catch (error) { attempt += 1; if (!isTransientBackendError(error) || attempt >= maxAttempts) throw error; onRetry(attempt, maxAttempts); await sleep(getPollIntervalMs()) } } }
|
||||||
async function refreshTaskBatch() { if (!pollingTaskIds.value.length) return { settledTaskIds: [] as number[] }; try { const batch = await getShopMatchTaskProgressBatch(pollingTaskIds.value); const nextSnapshots = { ...taskSnapshots.value }; const settledTaskIds = new Set<number>(); for (const missingId of batch.missingTaskIds || []) { removeTaskLocally(missingId); delete nextSnapshots[missingId]; settledTaskIds.add(missingId) } for (const detail of batch.items || []) { const taskId = detail.task?.id; if (!taskId) continue; const prev = nextSnapshots[taskId]; nextSnapshots[taskId] = prev ? { ...prev, task: { ...(prev.task || {}), ...(detail.task || {}) } } : detail; const status = detail.task?.status || ''; if (!status) continue; taskDetails.value[taskId] = status; if (isTaskTerminalStatus(status)) { removePollingTask(taskId); settledTaskIds.add(taskId); continue } if (status === 'SCHEDULED') { stopPollingTask(taskId); settledTaskIds.add(taskId) } } taskSnapshots.value = nextSnapshots; saveTaskSnapshotsToStorage(); saveTaskDetailsToStorage(); restoreScheduledDispatches(); return { settledTaskIds: Array.from(settledTaskIds) } } catch { return { settledTaskIds: [] as number[] } } }
|
async function refreshTaskBatch() { if (!pollingTaskIds.value.length) return { settledTaskIds: [] as number[] }; try { const batch = await getShopMatchTaskProgressBatch(pollingTaskIds.value); const nextSnapshots = { ...taskSnapshots.value }; const settledTaskIds = new Set<number>(); for (const missingId of batch.missingTaskIds || []) { removeTaskLocally(missingId); delete nextSnapshots[missingId]; settledTaskIds.add(missingId) } for (const detail of batch.items || []) { const taskId = detail.task?.id; if (!taskId) continue; const prev = nextSnapshots[taskId]; nextSnapshots[taskId] = prev ? { ...prev, task: { ...(prev.task || {}), ...(detail.task || {}) } } : detail; const status = detail.task?.status || ''; if (!status) continue; taskDetails.value[taskId] = status; if (isTaskTerminalStatus(status)) { removePollingTask(taskId); settledTaskIds.add(taskId); continue } if (status === 'SCHEDULED') { stopPollingTask(taskId); settledTaskIds.add(taskId) } } taskSnapshots.value = nextSnapshots; saveTaskSnapshotsToStorage(); saveTaskDetailsToStorage(); restoreScheduledDispatches(); return { settledTaskIds: Array.from(settledTaskIds) } } catch { return { settledTaskIds: [] as number[] } } }
|
||||||
function getPollIntervalMs() { return getTaskPollIntervalMs() }
|
function getPollIntervalMs() { return getTaskPollIntervalMs() }
|
||||||
function scheduleNextPoll(immediate = false) { if (pollTimer.value) { if (!immediate) return; window.clearTimeout(pollTimer.value); pollTimer.value = null } const run = async () => { pollTimer.value = null; if (pollingInFlight.value || !pollingTaskIds.value.length) { if (pollingTaskIds.value.length) scheduleNextPoll(); return } pollingInFlight.value = true; try { const { settledTaskIds } = await refreshTaskBatch(); if (settledTaskIds.length) await Promise.allSettled([loadHistory(), loadDashboard()]) } finally { pollingInFlight.value = false } if (pollingTaskIds.value.length) pollTimer.value = window.setTimeout(run, getPollIntervalMs()) }; if (immediate) void run(); else pollTimer.value = window.setTimeout(run, getPollIntervalMs()) }
|
function scheduleNextPoll(immediate = false) { if (disposed) return; if (pollTimer.value) { if (!immediate) return; timers.clearTimer('task-poll', pollTimer.value); pollTimer.value = null } const run = async () => { pollTimer.value = null; if (disposed) return; if (pollingInFlight.value || !pollingTaskIds.value.length) { if (pollingTaskIds.value.length) scheduleNextPoll(); return } pollingInFlight.value = true; try { const { settledTaskIds } = await refreshTaskBatch(); if (settledTaskIds.length) await Promise.allSettled([loadHistory(), loadDashboard()]) } finally { pollingInFlight.value = false } if (!disposed && pollingTaskIds.value.length) pollTimer.value = timers.setTimeout('task-poll', run, getPollIntervalMs()) }; if (immediate) void run(); else pollTimer.value = timers.setTimeout('task-poll', run, getPollIntervalMs()) }
|
||||||
function ensurePolling(immediate = false) { scheduleNextPoll(immediate) }
|
function ensurePolling(immediate = false) { scheduleNextPoll(immediate) }
|
||||||
function stopPolling() { if (pollTimer.value) { window.clearTimeout(pollTimer.value); pollTimer.value = null } }
|
function stopPolling() { if (pollTimer.value) { timers.clearTimer('task-poll', pollTimer.value); pollTimer.value = null } }
|
||||||
async function waitForTaskTerminal(taskId: number) { let transientErrorCount = 0; const maxTransientErrors = 30; while (true) { try { const batch = await getShopMatchTaskProgressBatch([taskId]); if (transientErrorCount > 0) queuePushResult.value = `任务 ${taskId} 后端已恢复,继续等待执行结果...`; transientErrorCount = 0; if ((batch.missingTaskIds || []).includes(taskId)) { removeTaskLocally(taskId); await refreshTaskViewsBestEffort(); restoreScheduledDispatches(); return 'FAILED' } const detail = (batch.items || []).find((item) => item.task?.id === taskId); const status = detail?.task?.status || ''; if (detail) { const prev = taskSnapshots.value[taskId]; taskSnapshots.value = { ...taskSnapshots.value, [taskId]: prev ? { ...prev, task: { ...(prev.task || {}), ...(detail.task || {}) } } : detail }; saveTaskSnapshotsToStorage() } if (status) { taskDetails.value[taskId] = status; saveTaskDetailsToStorage() } if (isTaskTerminalStatus(status)) { removePollingTask(taskId); await refreshTaskViewsBestEffort(); restoreScheduledDispatches(); return status } } catch (error) { if (!isTransientBackendError(error)) throw error; transientErrorCount += 1; if (transientErrorCount >= maxTransientErrors) throw new Error(`任务 ${taskId} 等待后端恢复超时,请稍后手动刷新查看状态`); queuePushResult.value = `任务 ${taskId} 运行中,正在等待后端服务恢复(${transientErrorCount}/${maxTransientErrors})...`; if (transientErrorCount === 1 || transientErrorCount % 5 === 0) ElMessage.warning(`任务 ${taskId} 运行中,后端服务暂时不可用,正在自动重试`); await sleep(getPollIntervalMs()); continue } await sleep(getPollIntervalMs()) } }
|
async function waitForTaskTerminal(taskId: number) { let transientErrorCount = 0; const maxTransientErrors = 30; while (true) { if (disposed) return 'STOPPED'; try { const batch = await getShopMatchTaskProgressBatch([taskId]); if (transientErrorCount > 0) queuePushResult.value = `任务 ${taskId} 后端已恢复,继续等待执行结果...`; transientErrorCount = 0; if ((batch.missingTaskIds || []).includes(taskId)) { removeTaskLocally(taskId); await refreshTaskViewsBestEffort(); restoreScheduledDispatches(); return 'FAILED' } const detail = (batch.items || []).find((item) => item.task?.id === taskId); const status = detail?.task?.status || ''; if (detail) { const prev = taskSnapshots.value[taskId]; taskSnapshots.value = { ...taskSnapshots.value, [taskId]: prev ? { ...prev, task: { ...(prev.task || {}), ...(detail.task || {}) } } : detail }; saveTaskSnapshotsToStorage() } if (status) { taskDetails.value[taskId] = status; saveTaskDetailsToStorage() } if (isTaskTerminalStatus(status)) { removePollingTask(taskId); await refreshTaskViewsBestEffort(); restoreScheduledDispatches(); return status } } catch (error) { if (!isTransientBackendError(error)) throw error; transientErrorCount += 1; if (transientErrorCount >= maxTransientErrors) throw new Error(`任务 ${taskId} 等待后端恢复超时,请稍后手动刷新查看状态`); queuePushResult.value = `任务 ${taskId} 运行中,正在等待后端服务恢复(${transientErrorCount}/${maxTransientErrors})...`; if (transientErrorCount === 1 || transientErrorCount % 5 === 0) ElMessage.warning(`任务 ${taskId} 运行中,后端服务暂时不可用,正在自动重试`); await sleep(getPollIntervalMs()); continue } await sleep(getPollIntervalMs()) } }
|
||||||
async function waitForScheduledTaskStageExit(taskId: number) {
|
async function waitForScheduledTaskStageExit(taskId: number) {
|
||||||
let transientErrorCount = 0
|
let transientErrorCount = 0
|
||||||
const maxTransientErrors = 30
|
const maxTransientErrors = 30
|
||||||
while (true) {
|
while (true) {
|
||||||
|
if (disposed) return 'STOPPED'
|
||||||
try {
|
try {
|
||||||
const batch = await getShopMatchTaskProgressBatch([taskId])
|
const batch = await getShopMatchTaskProgressBatch([taskId])
|
||||||
if (transientErrorCount > 0) queuePushResult.value = `任务 ${taskId} 后端已恢复,继续等待当前轮次结束...`
|
if (transientErrorCount > 0) queuePushResult.value = `任务 ${taskId} 后端已恢复,继续等待当前轮次结束...`
|
||||||
@@ -429,9 +434,9 @@ function formatMatchStatus(status?: string) { const value = (status || '').trim(
|
|||||||
function formatMatchRemark(row: ShopMatchShopQueueItem) { const message = (row.matchMessage || '').trim(); if (message) return message; if (row.matched && row.matchStatus === 'MATCHED') return '紫鸟索引已命中,可推送队列'; if (row.matched) return '已关联索引,请结合状态列查看是否可推送'; return '未命中或未就绪,请检查店铺名与索引刷新' }
|
function formatMatchRemark(row: ShopMatchShopQueueItem) { const message = (row.matchMessage || '').trim(); if (message) return message; if (row.matched && row.matchStatus === 'MATCHED') return '紫鸟索引已命中,可推送队列'; if (row.matched) return '已关联索引,请结合状态列查看是否可推送'; return '未命中或未就绪,请检查店铺名与索引刷新' }
|
||||||
async function pushToPythonQueue() { autoQueueEnabled.value = true; await processMatchedQueue() }
|
async function pushToPythonQueue() { autoQueueEnabled.value = true; await processMatchedQueue() }
|
||||||
function nextMatchedQueueItem() { return matchedItems.value.find((item) => item.matched) }
|
function nextMatchedQueueItem() { return matchedItems.value.find((item) => item.matched) }
|
||||||
async function processMatchedQueue() { if (queueWorkerRunning.value) return; const api = getPywebviewApi(); if (!api?.enqueue_json) { ElMessage.error('当前客户端未提供 enqueue_json'); return } const matched = matchedItems.value.filter((item) => item.matched); if (!matched.length) { ElMessage.warning('请先匹配可用店铺'); return } if (!orderedCountryCodes.value.length) { ElMessage.warning('请至少勾选一个国家'); return } let scheduleValues: string[] | undefined; try { scheduleValues = parseScheduleValues() } catch (error) { ElMessage.error(error instanceof Error ? error.message : '定时配置无效'); return } queueWorkerRunning.value = true; pushing.value = true; queuePayloadText.value = ''; let successCount = 0; let failedCount = 0; let index = 0; try { while (autoQueueEnabled.value) { const item = nextMatchedQueueItem(); if (!item) break; index += 1; try { const created = await withTransientRetry(() => createShopMatchTask([item], orderedCountryCodes.value, scheduleValues), (attempt, maxAttempts) => { queuePushResult.value = `后端服务暂时不可用,正在重试创建任务(${attempt}/${maxAttempts})...` }); const taskId = created.taskId; const initialStatus = scheduleValues?.length ? 'SCHEDULED' : 'RUNNING'; const snapshot: ShopMatchTaskDetailVo = { task: { id: taskId, status: initialStatus, scheduledAt: scheduleValues?.[0], countryCodes: [...orderedCountryCodes.value], currentStageIndex: scheduleValues?.length ? 0 : undefined, activeStageIndex: undefined, scheduleStages: (scheduleValues || []).map((value, stageIndex) => ({ stageIndex, scheduledAt: value, status: stageIndex === 0 ? 'SCHEDULED' : 'PENDING' })) }, items: created.items }; taskSnapshots.value = { ...taskSnapshots.value, [taskId]: snapshot }; taskDetails.value = { ...taskDetails.value, [taskId]: initialStatus }; saveTaskSnapshotsToStorage(); saveTaskDetailsToStorage(); const createdItem = created.items?.[0]; if (!createdItem) { removeMatchedRowsLocally([item]); continue } if (scheduleValues?.length) { successCount += 1; restoreScheduledDispatches(); queuePushResult.value = `任务 ${taskId} 已创建,共 ${scheduleValues.length} 个执行时间点`; removeMatchedRowsLocally([item]); continue } const payload = buildQueuePayload(taskId, item, orderedCountryCodes.value); queuePayloadText.value = JSON.stringify(payload, null, 2); const result = await api.enqueue_json(payload); if (!result?.success) { failedCount += 1; queuePushResult.value = `任务 ${taskId} 推送失败,已自动继续下一条:${result?.error || '未知错误'}`; ElMessage.error(queuePushResult.value); removeMatchedRowsLocally([item]); continue } addPollingTask(taskId); ensurePolling(true); removeMatchedRowsLocally([item]); queuePushResult.value = `任务 ${taskId} 已入队,等待执行完成`; const finalStatus = await waitForTaskTerminal(taskId); if (finalStatus !== 'SUCCESS') { failedCount += 1; queuePushResult.value = `任务 ${taskId} 执行失败,已自动继续下一条`; ElMessage.error(queuePushResult.value); continue } successCount += 1; queuePushResult.value = `任务 ${taskId} 已完成` } catch (error) { failedCount += 1; queuePushResult.value = `第 ${index} 条任务处理失败,已自动继续下一条:${error instanceof Error ? error.message : '未知错误'}`; ElMessage.error(queuePushResult.value); removeMatchedRowsLocally([item]); continue } } restoreScheduledDispatches(); if (!scheduleValues?.length) ensurePolling(true); if (successCount > 0 || failedCount > 0) ElMessage.success(scheduleValues?.length ? `定时任务已创建:成功 ${successCount} 条,失败 ${failedCount} 条` : `店铺推送已完成:成功 ${successCount} 条,失败 ${failedCount} 条`) } catch (error) { const message = error instanceof Error ? error.message : '推送失败'; queuePushResult.value = message; ElMessage.error(message) } finally { queueWorkerRunning.value = false; pushing.value = false } }
|
async function processMatchedQueue() { if (disposed || queueWorkerRunning.value) return; const api = getPywebviewApi(); if (!api?.enqueue_json) { ElMessage.error('当前客户端未提供 enqueue_json'); return } const matched = matchedItems.value.filter((item) => item.matched); if (!matched.length) { ElMessage.warning('请先匹配可用店铺'); return } if (!orderedCountryCodes.value.length) { ElMessage.warning('请至少勾选一个国家'); return } let scheduleValues: string[] | undefined; try { scheduleValues = parseScheduleValues() } catch (error) { ElMessage.error(error instanceof Error ? error.message : '定时配置无效'); return } queueWorkerRunning.value = true; pushing.value = true; queuePayloadText.value = ''; let successCount = 0; let failedCount = 0; let index = 0; try { while (!disposed && autoQueueEnabled.value) { const item = nextMatchedQueueItem(); if (!item) break; index += 1; try { const created = await withTransientRetry(() => createShopMatchTask([item], orderedCountryCodes.value, scheduleValues), (attempt, maxAttempts) => { queuePushResult.value = `后端服务暂时不可用,正在重试创建任务(${attempt}/${maxAttempts})...` }); const taskId = created.taskId; const initialStatus = scheduleValues?.length ? 'SCHEDULED' : 'RUNNING'; const snapshot: ShopMatchTaskDetailVo = { task: { id: taskId, status: initialStatus, scheduledAt: scheduleValues?.[0], countryCodes: [...orderedCountryCodes.value], currentStageIndex: scheduleValues?.length ? 0 : undefined, activeStageIndex: undefined, scheduleStages: (scheduleValues || []).map((value, stageIndex) => ({ stageIndex, scheduledAt: value, status: stageIndex === 0 ? 'SCHEDULED' : 'PENDING' })) }, items: created.items }; taskSnapshots.value = { ...taskSnapshots.value, [taskId]: snapshot }; taskDetails.value = { ...taskDetails.value, [taskId]: initialStatus }; saveTaskSnapshotsToStorage(); saveTaskDetailsToStorage(); const createdItem = created.items?.[0]; if (!createdItem) { removeMatchedRowsLocally([item]); continue } if (scheduleValues?.length) { successCount += 1; restoreScheduledDispatches(); queuePushResult.value = `任务 ${taskId} 已创建,共 ${scheduleValues.length} 个执行时间点`; removeMatchedRowsLocally([item]); continue } const payload = buildQueuePayload(taskId, item, orderedCountryCodes.value); queuePayloadText.value = JSON.stringify(payload, null, 2); const result = await api.enqueue_json(payload); if (!result?.success) { failedCount += 1; queuePushResult.value = `任务 ${taskId} 推送失败,已自动继续下一条:${result?.error || '未知错误'}`; ElMessage.error(queuePushResult.value); removeMatchedRowsLocally([item]); continue } addPollingTask(taskId); ensurePolling(true); removeMatchedRowsLocally([item]); queuePushResult.value = `任务 ${taskId} 已入队,等待执行完成`; const finalStatus = await waitForTaskTerminal(taskId); if (finalStatus !== 'SUCCESS') { failedCount += 1; queuePushResult.value = `任务 ${taskId} 执行失败,已自动继续下一条`; ElMessage.error(queuePushResult.value); continue } successCount += 1; queuePushResult.value = `任务 ${taskId} 已完成` } catch (error) { if (disposed) break; failedCount += 1; queuePushResult.value = `第 ${index} 条任务处理失败,已自动继续下一条:${error instanceof Error ? error.message : '未知错误'}`; ElMessage.error(queuePushResult.value); removeMatchedRowsLocally([item]); continue } } restoreScheduledDispatches(); if (!scheduleValues?.length) ensurePolling(true); if (!disposed && (successCount > 0 || failedCount > 0)) ElMessage.success(scheduleValues?.length ? `定时任务已创建:成功 ${successCount} 条,失败 ${failedCount} 条` : `店铺推送已完成:成功 ${successCount} 条,失败 ${failedCount} 条`) } catch (error) { if (disposed) return; const message = error instanceof Error ? error.message : '推送失败'; queuePushResult.value = message; ElMessage.error(message) } finally { queueWorkerRunning.value = false; pushing.value = false } }
|
||||||
onMounted(async () => { loadPollingIdsFromStorage(); loadTaskDetailsFromStorage(); loadTaskSnapshotsFromStorage(); loadMatchedItemsFromStorage(); schedulePickerValues.value = sanitizeSchedulePickerValues(schedulePickerValues.value, scheduleEnabled.value); syncPollingIdsWithTaskState(); await Promise.allSettled([loadCandidates(), loadCountryPreference(), loadHistory(), loadDashboard()]); startScheduleHeartbeat(); window.addEventListener('focus', handleScheduleRecovery); document.addEventListener('visibilitychange', handleScheduleRecovery); restoreScheduledDispatches(); if (pollingTaskIds.value.length) ensurePolling(true) })
|
onMounted(async () => { loadPollingIdsFromStorage(); loadTaskDetailsFromStorage(); loadTaskSnapshotsFromStorage(); loadMatchedItemsFromStorage(); schedulePickerValues.value = sanitizeSchedulePickerValues(schedulePickerValues.value, scheduleEnabled.value); syncPollingIdsWithTaskState(); await Promise.allSettled([loadCandidates(), loadCountryPreference(), loadHistory(), loadDashboard()]); window.addEventListener('focus', handleScheduleRecovery); document.addEventListener('visibilitychange', handleScheduleRecovery); restoreScheduledDispatches(); if (pollingTaskIds.value.length) ensurePolling(true) })
|
||||||
onUnmounted(() => { stopPolling(); stopScheduleHeartbeat(); window.removeEventListener('focus', handleScheduleRecovery); document.removeEventListener('visibilitychange', handleScheduleRecovery); if (countryPrefSaveTimer) clearTimeout(countryPrefSaveTimer); for (const timer of dispatchTimers.values()) window.clearTimeout(timer); dispatchTimers.clear() })
|
onUnmounted(() => { disposed = true; stopPolling(); stopScheduleHeartbeat(); clearSleepTimers(); window.removeEventListener('focus', handleScheduleRecovery); document.removeEventListener('visibilitychange', handleScheduleRecovery); if (countryPrefSaveTimer) timers.clearTimer('preference-save', countryPrefSaveTimer); for (const timer of dispatchTimers.values()) timers.clearTimer('scheduled-dispatch', timer); dispatchTimers.clear(); timers.clearScope() })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
113
frontend-vue/src/shared/utils/categorized-timers.ts
Normal file
113
frontend-vue/src/shared/utils/categorized-timers.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
type TimerKind = 'timeout' | 'interval'
|
||||||
|
type TimerCancel = () => void
|
||||||
|
|
||||||
|
interface TimerEntry {
|
||||||
|
id: number
|
||||||
|
kind: TimerKind
|
||||||
|
category: string
|
||||||
|
cancel?: TimerCancel
|
||||||
|
}
|
||||||
|
|
||||||
|
const timerBuckets = new Map<string, Map<number, TimerEntry>>()
|
||||||
|
|
||||||
|
function bucketOf(category: string) {
|
||||||
|
let bucket = timerBuckets.get(category)
|
||||||
|
if (!bucket) {
|
||||||
|
bucket = new Map<number, TimerEntry>()
|
||||||
|
timerBuckets.set(category, bucket)
|
||||||
|
}
|
||||||
|
return bucket
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeTimer(category: string, id: number) {
|
||||||
|
const bucket = timerBuckets.get(category)
|
||||||
|
if (!bucket) return
|
||||||
|
bucket.delete(id)
|
||||||
|
if (!bucket.size) timerBuckets.delete(category)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setCategorizedTimeout(category: string, handler: () => void, delayMs: number) {
|
||||||
|
const id = window.setTimeout(() => {
|
||||||
|
removeTimer(category, id)
|
||||||
|
handler()
|
||||||
|
}, delayMs)
|
||||||
|
bucketOf(category).set(id, { id, kind: 'timeout', category })
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setCategorizedInterval(category: string, handler: () => void, delayMs: number) {
|
||||||
|
const id = window.setInterval(handler, delayMs)
|
||||||
|
bucketOf(category).set(id, { id, kind: 'interval', category })
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearCategorizedTimer(category: string, id: number | null | undefined) {
|
||||||
|
if (id == null) return
|
||||||
|
const bucket = timerBuckets.get(category)
|
||||||
|
const entry = bucket?.get(id)
|
||||||
|
if (entry?.kind === 'interval') window.clearInterval(id)
|
||||||
|
else window.clearTimeout(id)
|
||||||
|
entry?.cancel?.()
|
||||||
|
removeTimer(category, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearCategorizedTimers(category: string) {
|
||||||
|
const bucket = timerBuckets.get(category)
|
||||||
|
if (!bucket) return
|
||||||
|
for (const entry of bucket.values()) {
|
||||||
|
if (entry.kind === 'interval') window.clearInterval(entry.id)
|
||||||
|
else window.clearTimeout(entry.id)
|
||||||
|
entry.cancel?.()
|
||||||
|
}
|
||||||
|
timerBuckets.delete(category)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sleepWithCategory(category: string, delayMs: number) {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
const id = window.setTimeout(() => {
|
||||||
|
removeTimer(category, id)
|
||||||
|
resolve()
|
||||||
|
}, delayMs)
|
||||||
|
bucketOf(category).set(id, { id, kind: 'timeout', category, cancel: resolve })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearTimerScope(scope: string) {
|
||||||
|
const prefix = `${scope}:`
|
||||||
|
for (const category of Array.from(timerBuckets.keys())) {
|
||||||
|
if (category === scope || category.startsWith(prefix)) {
|
||||||
|
clearCategorizedTimers(category)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCategorizedTimers(scope: string) {
|
||||||
|
const key = (category: string) => `${scope}:${category}`
|
||||||
|
return {
|
||||||
|
setTimeout(category: string, handler: () => void, delayMs: number) {
|
||||||
|
return setCategorizedTimeout(key(category), handler, delayMs)
|
||||||
|
},
|
||||||
|
setInterval(category: string, handler: () => void, delayMs: number) {
|
||||||
|
return setCategorizedInterval(key(category), handler, delayMs)
|
||||||
|
},
|
||||||
|
clearTimer(category: string, id: number | null | undefined) {
|
||||||
|
clearCategorizedTimer(key(category), id)
|
||||||
|
},
|
||||||
|
clearCategory(category: string) {
|
||||||
|
clearCategorizedTimers(key(category))
|
||||||
|
},
|
||||||
|
clearScope() {
|
||||||
|
clearTimerScope(scope)
|
||||||
|
},
|
||||||
|
sleep(category: string, delayMs: number) {
|
||||||
|
return sleepWithCategory(key(category), delayMs)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCategorizedTimerStats() {
|
||||||
|
return Array.from(timerBuckets.entries()).map(([category, bucket]) => ({
|
||||||
|
category,
|
||||||
|
count: bucket.size,
|
||||||
|
}))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user