diff --git a/backend-java/pom.xml b/backend-java/pom.xml index 2fab57a..0c4fa85 100644 --- a/backend-java/pom.xml +++ b/backend-java/pom.xml @@ -25,8 +25,19 @@ 5.8.36 2.3.5 8.5.17 + 3.28.0-GA + + + + org.javassist + javassist + ${javassist.version} + + + + org.springframework.boot diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/client/AppearancePatentCozeClient.java b/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/client/AppearancePatentCozeClient.java index 12cd99d..bbc92db 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/client/AppearancePatentCozeClient.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/client/AppearancePatentCozeClient.java @@ -649,7 +649,7 @@ public class AppearancePatentCozeClient { } 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) { diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/controller/AppearancePatentController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/controller/AppearancePatentController.java index 8a7157a..0eddd72 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/controller/AppearancePatentController.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/controller/AppearancePatentController.java @@ -83,7 +83,10 @@ public class AppearancePatentController { public ApiResponse result( @Parameter(description = "外观专利检测任务 ID。任务必须处于 RUNNING 状态。", required = true, example = "3938") @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); return ApiResponse.success(null); } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskService.java index 9d751c2..2b6f85b 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskService.java @@ -121,7 +121,7 @@ public class AppearancePatentTaskService { .filter(Objects::nonNull) .toList(); if (sourceFiles.isEmpty()) { - throw new BusinessException("璇峰厛涓婁紶 Excel 鏂囦欢"); + throw new BusinessException("请先上传 Excel 文件"); } List allRows = new ArrayList<>(); List mergedHeaders = new ArrayList<>(); @@ -461,10 +461,10 @@ public class AppearancePatentTaskService { private SubmitContext persistSubmittedChunk(Long taskId, AppearancePatentSubmitResultRequest request) { FileTaskEntity task = fileTaskMapper.selectById(taskId); if (task == null || !MODULE_TYPE.equals(task.getModuleType())) { - throw new BusinessException("浠诲姟涓嶅瓨鍦?"); + throw new BusinessException("任务不存在"); } if (!STATUS_RUNNING.equals(task.getStatus())) { - throw new BusinessException("浠诲姟涓嶆槸杩愯涓姸鎬?"); + throw new BusinessException("任务不是运行中状态"); } int chunkIndex = request.getChunkIndex() == null ? 0 : request.getChunkIndex(); @@ -482,7 +482,7 @@ public class AppearancePatentTaskService { .last("limit 1")); if (existing == null) { List rawRows = flattenSubmittedRows(request); - String payloadJson = writeJson(rawRows, "缁撴灉搴忓垪鍖栧け璐?"); + String payloadJson = writeJson(rawRows, "结果序列化失败"); TaskChunkEntity chunk = new TaskChunkEntity(); chunk.setTaskId(taskId); @@ -514,7 +514,7 @@ public class AppearancePatentTaskService { private void completeSubmittedChunk(SubmitContext context) { FileTaskEntity task = fileTaskMapper.selectById(context.task().getId()); if (task == null || !MODULE_TYPE.equals(task.getModuleType())) { - throw new BusinessException("浠诲姟涓嶅瓨鍦?"); + throw new BusinessException("任务不存在"); } if (!STATUS_RUNNING.equals(task.getStatus())) { log.info("[appearance-patent] skip completion because task already finalized taskId={} status={}", @@ -1379,7 +1379,7 @@ public class AppearancePatentTaskService { } 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 sourceFiles, List headers, List groups, List allRows) { diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/brand/service/BrandTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/brand/service/BrandTaskService.java index 6361298..5354ceb 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/brand/service/BrandTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/brand/service/BrandTaskService.java @@ -881,8 +881,8 @@ public class BrandTaskService { if (value == null) { return ""; } - return value.replace("\ufeff", "") - .replace("\u3000", " ") + return value.replace(String.valueOf((char) 0xFEFF), "") + .replace((char) 0x3000, ' ') .replace("\r\n", " ") .replace("\r", " ") .replace("\n", " ") diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/convert/service/ConvertRunService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/convert/service/ConvertRunService.java index a8161ed..6d2bf4a 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/convert/service/ConvertRunService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/convert/service/ConvertRunService.java @@ -475,8 +475,8 @@ public class ConvertRunService { if (value == null) { return ""; } - return value.replace("\ufeff", "") - .replace("\u3000", " ") + return value.replace(String.valueOf((char) 0xFEFF), "") + .replace((char) 0x3000, ' ') .replace("\r\n", " ") .replace("\r", " ") .replace("\n", " ") diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/service/DedupeRunService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/service/DedupeRunService.java index 804130e..360a1c4 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/service/DedupeRunService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/service/DedupeRunService.java @@ -47,8 +47,8 @@ import java.util.zip.ZipOutputStream; @Slf4j 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_STOP_COLUMN = "\u7f29\u7565\u56fe\u5730\u57408"; + private static final String HEADER_SPLIT_MARKER = "idASIN国家状态价格变体数量"; + private static final String HEADER_STOP_COLUMN = "缩略图地址8"; private final FileTaskMapper fileTaskMapper; private final FileResultMapper fileResultMapper; @@ -59,7 +59,7 @@ public class DedupeRunService { public DedupeRunVo run(DedupeRunRequest request) { long runStartNs = System.nanoTime(); 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(); @@ -90,7 +90,7 @@ public class DedupeRunService { File inputFile = findLocalSourceFile(sourceFile.getFileKey()); long findFileNs = elapsedNs(fileStartNs); 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(); @@ -240,7 +240,7 @@ public class DedupeRunService { public void deleteHistory(Long resultId, Long userId) { FileResultEntity entity = fileResultMapper.selectById(resultId); 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); } @@ -248,11 +248,11 @@ public class DedupeRunService { public Resource getResultFile(Long resultId, Long userId) { FileResultEntity entity = fileResultMapper.selectById(resultId); 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()); if (!file.exists()) { - throw new BusinessException("\u7ed3\u679c\u6587\u4ef6\u4e0d\u5b58\u5728"); + throw new BusinessException("结果文件不存在"); } return new FileSystemResource(file); } @@ -342,18 +342,18 @@ public class DedupeRunService { Sheet sheet = workbook.getSheetAt(0); Row headerRow = sheet.getRow(0); if (headerRow == null) { - throw new BusinessException("Excel \u8868\u5934\u4e3a\u7a7a"); + throw new BusinessException("Excel 表头为空"); } Map headerMap = buildHeaderMap(headerRow, formatter); if (headerMap.isEmpty()) { - throw new BusinessException("Excel \u8868\u5934\u4e3a\u7a7a"); + throw new BusinessException("Excel 表头为空"); } List missing = selectedColumns.stream() .filter(column -> !headerMap.containsKey(column)) .toList(); if (!missing.isEmpty()) { - throw new BusinessException("\u7f3a\u5c11\u5217\uff1a" + String.join("\u3001", missing)); + throw new BusinessException("缺少列:" + String.join("、", missing)); } List selectedIndexes = new ArrayList<>(selectedColumns.size()); @@ -478,7 +478,7 @@ public class DedupeRunService { zos.closeEntry(); } } catch (Exception ex) { - throw new BusinessException("\u53bb\u91cd\u7ed3\u679c\u6253\u5305\u5931\u8d25\uff1a" + ex.getMessage()); + throw new BusinessException("去重结果打包失败:" + ex.getMessage()); } return zipFile; } @@ -602,7 +602,7 @@ public class DedupeRunService { boolean previousWhitespace = false; for (int i = start; i < end; i++) { char ch = value.charAt(i); - if (ch == '\uFEFF') { + if (ch == 0xFEFF) { if (builder == null) { builder = new StringBuilder(value.length()); builder.append(value, start, i); @@ -629,11 +629,11 @@ public class DedupeRunService { } 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) { - 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) { diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/file/service/LocalFileStorageService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/file/service/LocalFileStorageService.java index 1263a42..aec1237 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/file/service/LocalFileStorageService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/file/service/LocalFileStorageService.java @@ -109,8 +109,8 @@ public class LocalFileStorageService { if (value == null) { return ""; } - return value.replace("\ufeff", "") - .replace("\u3000", " ") + return value.replace(String.valueOf((char) 0xFEFF), "") + .replace((char) 0x3000, ' ') .replace("\r\n", " ") .replace("\r", " ") .replace("\n", " ") diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/patroldelete/service/PatrolDeleteTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/patroldelete/service/PatrolDeleteTaskService.java index 21298b0..8f6d54d 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/patroldelete/service/PatrolDeleteTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/patroldelete/service/PatrolDeleteTaskService.java @@ -281,7 +281,6 @@ public class PatrolDeleteTaskService { return vo; } - @Transactional public void submitResult(Long taskId, PatrolDeleteSubmitResultRequest request) { if (taskId == null || taskId <= 0) { throw new BusinessException("taskId 不合法"); @@ -334,7 +333,6 @@ public class PatrolDeleteTaskService { tryFinalizeTask(taskId, false); } - @Transactional public boolean tryFinalizeTask(Long taskId, boolean fromCompensation) { if (taskId == null || taskId <= 0) { return false; diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/dto/PriceTrackSubmitResultRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/dto/PriceTrackSubmitResultRequest.java index e0aeff8..f3810a5 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/dto/PriceTrackSubmitResultRequest.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/dto/PriceTrackSubmitResultRequest.java @@ -1,5 +1,6 @@ package com.nanri.aiimage.modules.pricetrack.model.dto; +import com.fasterxml.jackson.annotation.JsonAlias; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -88,6 +89,10 @@ public class PriceTrackSubmitResultRequest { @Schema(description = "购物车店铺名", example = "店铺A") private String cartShopName; + @JsonAlias({"changedPrice", "changePrice", "modifiedPrice", "newPrice", "targetPrice", "updatedPrice", "改价价格"}) + @Schema(description = "改价价格", example = "18.99") + private String priceChangePrice; + @Schema(description = "改价情况", example = "UPDATED") private String priceChangeStatus; diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/service/PriceTrackExcelAssemblyService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/service/PriceTrackExcelAssemblyService.java index 6d313a1..c042541 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/service/PriceTrackExcelAssemblyService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/service/PriceTrackExcelAssemblyService.java @@ -29,6 +29,7 @@ public class PriceTrackExcelAssemblyService { "第一名", "第二名", "购物车店铺名", + "改价价格", "改价情况", "修改次数", "状态" @@ -40,13 +41,14 @@ public class PriceTrackExcelAssemblyService { workbook.setCompressTempFiles(true); try (FileOutputStream fos = new FileOutputStream(outputXlsx)) { for (ProductRiskCountryCode code : ProductRiskCountryCode.values()) { - String sheetName = code.name(); + String countryCode = code.name(); + String sheetName = sheetNameOf(code); Sheet sheet = workbook.createSheet(sheetName); Row headerRow = sheet.createRow(0); for (int i = 0; i < RESULT_HEADER.length; i++) { headerRow.createCell(i).setCellValue(RESULT_HEADER[i]); } - List rows = safe.getOrDefault(sheetName, List.of()); + List rows = safe.getOrDefault(countryCode, List.of()); int rowIdx = 1; for (PriceTrackSubmitResultRequest.AsinResult item : rows) { if (item == null) { @@ -62,9 +64,10 @@ public class PriceTrackExcelAssemblyService { row.createCell(6).setCellValue(valueOf(item.getFirstPlace())); row.createCell(7).setCellValue(valueOf(item.getSecondPlace())); row.createCell(8).setCellValue(valueOf(item.getCartShopName())); - row.createCell(9).setCellValue(valueOf(item.getPriceChangeStatus())); - row.createCell(10).setCellValue(valueOf(item.getModifyCount())); - row.createCell(11).setCellValue(valueOf(item.getStatus())); + row.createCell(9).setCellValue(valueOf(item.getPriceChangePrice())); + row.createCell(10).setCellValue(valueOf(item.getPriceChangeStatus())); + row.createCell(11).setCellValue(valueOf(item.getModifyCount())); + row.createCell(12).setCellValue(valueOf(item.getStatus())); } applyDefaultColumnWidths(sheet); } @@ -113,8 +116,18 @@ public class PriceTrackExcelAssemblyService { 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) { - 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++) { sheet.setColumnWidth(i, widths[i] * 256); } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/service/PriceTrackTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/service/PriceTrackTaskService.java index e9ece5c..34043cb 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/service/PriceTrackTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/service/PriceTrackTaskService.java @@ -127,6 +127,21 @@ public class PriceTrackTaskService { return tasks; } + private List selectTaskStatusesByIdsInBatches(List taskIds) { + List 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() + .select(FileTaskEntity::getId, FileTaskEntity::getStatus) + .in(FileTaskEntity::getId, taskIds.subList(start, end)))); + } + return tasks; + } + public PriceTrackDashboardVo dashboard(Long userId) { if (userId == null || userId <= 0) throw new BusinessException("user_id 不合法"); PriceTrackDashboardVo vo = new PriceTrackDashboardVo(); @@ -166,7 +181,7 @@ public class PriceTrackTaskService { .distinct() .toList(); if (!taskIds.isEmpty()) { - List tasks = selectTasksByIdsInBatches(taskIds); + List tasks = selectTaskStatusesByIdsInBatches(taskIds); for (FileTaskEntity t : tasks) { if (t != null) statusByTaskId.put(t.getId(), t.getStatus()); } @@ -383,7 +398,6 @@ public class PriceTrackTaskService { return vo; } - @Transactional public void submitResult(Long taskId, PriceTrackSubmitResultRequest request) { if (taskId == null || taskId <= 0) throw new BusinessException("taskId 不合法"); if (request == null || request.getShops() == null || request.getShops().isEmpty()) { @@ -458,7 +472,6 @@ public class PriceTrackTaskService { updateTaskStatusFromLatestRows(task, latest); } - @Transactional public boolean tryFinalizeTask(Long taskId, boolean fromCompensation) { if (taskId == null || taskId <= 0) return false; FileTaskEntity task = loadTaskForExecution(taskId); @@ -1139,6 +1152,7 @@ public class PriceTrackTaskService { merged.setFirstPlace(firstNonBlank(incoming.getFirstPlace(), merged.getFirstPlace())); merged.setSecondPlace(firstNonBlank(incoming.getSecondPlace(), merged.getSecondPlace())); merged.setCartShopName(firstNonBlank(incoming.getCartShopName(), merged.getCartShopName())); + merged.setPriceChangePrice(firstNonBlank(incoming.getPriceChangePrice(), merged.getPriceChangePrice())); merged.setPriceChangeStatus(firstNonBlank(incoming.getPriceChangeStatus(), merged.getPriceChangeStatus())); merged.setModifyCount(firstNonBlank(incoming.getModifyCount(), merged.getModifyCount())); merged.setStatus(firstNonBlank(incoming.getStatus(), merged.getStatus())); @@ -1163,6 +1177,7 @@ public class PriceTrackTaskService { out.setFirstPlace(row.getFirstPlace()); out.setSecondPlace(row.getSecondPlace()); out.setCartShopName(row.getCartShopName()); + out.setPriceChangePrice(row.getPriceChangePrice()); out.setPriceChangeStatus(row.getPriceChangeStatus()); out.setModifyCount(row.getModifyCount()); out.setStatus(row.getStatus()); diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/service/ProductRiskTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/service/ProductRiskTaskService.java index 1744916..d0e55c8 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/service/ProductRiskTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/service/ProductRiskTaskService.java @@ -124,6 +124,21 @@ public class ProductRiskTaskService { return tasks; } + private List selectTaskStatusesByIdsInBatches(List taskIds) { + List 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() + .select(FileTaskEntity::getId, FileTaskEntity::getStatus) + .in(FileTaskEntity::getId, taskIds.subList(start, end)))); + } + return tasks; + } + public ProductRiskDashboardVo dashboard(Long userId) { if (userId == null || userId <= 0) { throw new BusinessException("user_id 不合法"); @@ -166,7 +181,7 @@ public class ProductRiskTaskService { .distinct() .toList(); if (!taskIds.isEmpty()) { - List tasks = selectTasksByIdsInBatches(taskIds); + List tasks = selectTaskStatusesByIdsInBatches(taskIds); for (FileTaskEntity t : tasks) { if (t != null) { statusByTaskId.put(t.getId(), t.getStatus()); @@ -465,7 +480,6 @@ public class ProductRiskTaskService { return vo; } - @Transactional public void submitResult(Long taskId, ProductRiskSubmitResultRequest request) { if (taskId == null || taskId <= 0) { throw new BusinessException("taskId 不合法"); @@ -630,7 +644,6 @@ public class ProductRiskTaskService { cleanupTaskCacheIfTerminal(taskId, task.getStatus()); } - @Transactional public boolean tryFinalizeTask(Long taskId, boolean fromCompensation) { if (taskId == null || taskId <= 0) { return false; diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/queryasin/service/QueryAsinTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/queryasin/service/QueryAsinTaskService.java index b266054..35c74a4 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/queryasin/service/QueryAsinTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/queryasin/service/QueryAsinTaskService.java @@ -287,7 +287,6 @@ public class QueryAsinTaskService { return vo; } - @Transactional public void submitResult(Long taskId, QueryAsinSubmitResultRequest request) { if (taskId == null || taskId <= 0) { throw new BusinessException("taskId 不合法"); @@ -340,7 +339,6 @@ public class QueryAsinTaskService { tryFinalizeTask(taskId, false); } - @Transactional public boolean tryFinalizeTask(Long taskId, boolean fromCompensation) { if (taskId == null || taskId <= 0) { return false; diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/mapper/ShopManageGroupMapper.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/mapper/ShopManageGroupMapper.java index fb44b6c..39858ea 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/mapper/ShopManageGroupMapper.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/mapper/ShopManageGroupMapper.java @@ -3,7 +3,68 @@ package com.nanri.aiimage.modules.shopkey.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.nanri.aiimage.modules.shopkey.model.entity.ShopManageGroupEntity; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; @Mapper public interface ShopManageGroupMapper extends BaseMapper { + + @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 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 selectAccessibleCreatorUserIds(@Param("operatorId") Long operatorId); } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/QueryAsinService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/QueryAsinService.java index 2e89945..8517d93 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/QueryAsinService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/QueryAsinService.java @@ -40,7 +40,7 @@ public class QueryAsinService { private static final List SUPPORTED_COUNTRIES = List.of("DE", "UK", "FR", "IT", "ES"); 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 ShopManageGroupService shopManageGroupService; @@ -79,7 +79,7 @@ public class QueryAsinService { List rows = queryAsinMapper .selectList(query.last("LIMIT " + ((safePage - 1) * safePageSize) + ", " + safePageSize)); - Map groupNameById = buildGroupNameMap(rows.stream() + Map groupNameById = shopManageGroupService.buildGroupNameMap(rows.stream() .map(QueryAsinEntity::getGroupId) .filter(id -> id != null && id > 0) .distinct() @@ -316,22 +316,6 @@ public class QueryAsinService { return vo; } - private Map buildGroupNameMap(List groupIds) { - if (groupIds == null || groupIds.isEmpty()) { - return Map.of(); - } - LinkedHashMap 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 normalizeCountries(List countries) { LinkedHashSet normalized = new LinkedHashSet<>(); if (countries != null) { @@ -384,11 +368,11 @@ public class QueryAsinService { return upper; } return switch (normalizeHeaderKey(normalized)) { - case "\u5fb7\u56fd", "\u5fb7", "de", "germany", "german", "deutschland" -> "DE"; - case "\u82f1\u56fd", "\u82f1", "uk", "gb", "greatbritain", "britain", "unitedkingdom" -> "UK"; - case "\u6cd5\u56fd", "\u6cd5", "fr", "france", "french" -> "FR"; - case "\u610f\u5927\u5229", "\u610f", "it", "italy", "italian" -> "IT"; - case "\u897f\u73ed\u7259", "\u897f", "es", "spain", "spanish", "espana" -> "ES"; + case "德国", "德", "de", "germany", "german", "deutschland" -> "DE"; + case "英国", "英", "uk", "gb", "greatbritain", "britain", "unitedkingdom" -> "UK"; + case "法国", "法", "fr", "france", "french" -> "FR"; + case "意大利", "意", "it", "italy", "italian" -> "IT"; + case "西班牙", "西", "es", "spain", "spanish", "espana" -> "ES"; default -> ""; }; } @@ -860,11 +844,11 @@ public class QueryAsinService { if (index != null) { return index; } - index = firstHeader("groupname", "\u5206\u7ec4id", "\u5206\u7ec4"); + index = firstHeader("groupname", "分组id", "分组"); if (index != null) { return index; } - return findHeaderByPredicate(key -> key.contains("\u5206\u7ec4") || key.contains("group")); + return findHeaderByPredicate(key -> key.contains("分组") || key.contains("group")); } private Integer resolveShopHeaderIndex() { @@ -873,13 +857,13 @@ public class QueryAsinService { return index; } index = firstHeader( - "\u5e97\u94fa\u540d", "\u5e97\u94fa\u540d\u79f0", "\u5e97\u94fa\u8d26\u53f7", "\u8d26\u53f7", + "店铺名", "店铺名称", "店铺账号", "账号", "account", "selleraccount", "storeaccount", "storename"); if (index != null) { return index; } 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("selleraccount") || key.contains("storeaccount")); @@ -891,32 +875,32 @@ public class QueryAsinService { return index; } return switch (country) { - case "DE" -> firstHeader("\u5fb7\u56fd", "\u5fb7", "\u5fb7\u56fdasin"); - case "UK" -> firstHeader("\u82f1\u56fd", "\u82f1", "\u82f1\u56fdasin"); - case "FR" -> firstHeader("\u6cd5\u56fd", "\u6cd5", "\u6cd5\u56fdasin"); - case "IT" -> firstHeader("\u610f\u5927\u5229", "\u610f", "\u610f\u5927\u5229asin"); - case "ES" -> firstHeader("\u897f\u73ed\u7259", "\u897f", "\u897f\u73ed\u7259asin"); + case "DE" -> firstHeader("德国", "德", "德国asin"); + case "UK" -> firstHeader("英国", "英", "英国asin"); + case "FR" -> firstHeader("法国", "法", "法国asin"); + case "IT" -> firstHeader("意大利", "意", "意大利asin"); + case "ES" -> firstHeader("西班牙", "西", "西班牙asin"); default -> null; }; } private Integer findSingleCountryIndex() { 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) { return index; } return findHeaderByPredicate(key -> - key.contains("\u56fd\u5bb6") - || key.contains("\u7ad9\u70b9") + key.contains("国家") + || key.contains("站点") || key.contains("country") || key.contains("marketplace") || key.equals("site")); } private Integer findSingleAsinIndex() { - return firstHeader("asin", "\u67e5\u8be2asin", "\u4ea7\u54c1asin", "\u94fe\u63a5asin"); + return firstHeader("asin", "查询asin", "产品asin", "链接asin"); } private boolean hasWideCountryColumns() { diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/ShopManageGroupService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/ShopManageGroupService.java index 204beea..73a8471 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/ShopManageGroupService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/ShopManageGroupService.java @@ -48,6 +48,7 @@ public class ShopManageGroupService { public List list(Long createdById) { return toVoList(groupMapper.selectList(new LambdaQueryWrapper() .eq(createdById != null && createdById > 0, ShopManageGroupEntity::getCreatedById, createdById) + .or(createdById != null && createdById > 0, wrapper -> wrapper.eq(ShopManageGroupEntity::getUserId, createdById)) .orderByAsc(ShopManageGroupEntity::getId))); } @@ -74,40 +75,44 @@ public class ShopManageGroupService { .collect(Collectors.toCollection(LinkedHashSet::new)); } Long normalizedOperatorId = normalizePositiveId(operatorId, "操作人不能为空"); - LinkedHashSet groupIds = groupMapper.selectList(new LambdaQueryWrapper() - .select(ShopManageGroupEntity::getId) - .eq(ShopManageGroupEntity::getCreatedById, normalizedOperatorId)) + return groupMapper.selectAccessibleGroupIds(normalizedOperatorId) .stream() - .map(ShopManageGroupEntity::getId) .filter(id -> id != null && id > 0) .collect(Collectors.toCollection(LinkedHashSet::new)); - List memberGroupIds = groupMemberMapper.selectList(new LambdaQueryWrapper() - .select(ShopManageGroupMemberEntity::getGroupId) - .eq(ShopManageGroupMemberEntity::getUserId, normalizedOperatorId)) - .stream() - .map(ShopManageGroupMemberEntity::getGroupId) - .filter(id -> id != null && id > 0) - .toList(); - groupIds.addAll(memberGroupIds); - if (!memberGroupIds.isEmpty()) { - LinkedHashSet leaderIds = groupMapper.selectList(new LambdaQueryWrapper() - .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() - .select(ShopManageGroupEntity::getId) - .in(ShopManageGroupEntity::getCreatedById, leaderIds)) - .stream() - .map(ShopManageGroupEntity::getId) - .filter(id -> id != null && id > 0) - .toList()); - } + } + + public Set listAccessibleCreatorUserIds(Long operatorId, boolean superAdmin) { + if (superAdmin) { + return Set.of(); } - return groupIds; + Long normalizedOperatorId = normalizePositiveId(operatorId, "操作人不能为空"); + return groupMapper.selectAccessibleCreatorUserIds(normalizedOperatorId) + .stream() + .filter(id -> id != null && id > 0) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + public Map buildGroupNameMap(List groupIds) { + if (groupIds == null || groupIds.isEmpty()) { + return Map.of(); + } + List normalizedGroupIds = groupIds.stream() + .filter(id -> id != null && id > 0) + .distinct() + .toList(); + if (normalizedGroupIds.isEmpty()) { + return Map.of(); + } + return groupMapper.selectList(new LambdaQueryWrapper() + .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 @@ -207,8 +212,9 @@ public class ShopManageGroupService { return; } Long normalizedOperatorId = normalizePositiveId(operatorId, "操作人不能为空"); - boolean createdBySelf = entity.getCreatedById() != null && entity.getCreatedById().equals(normalizedOperatorId); - if (!createdBySelf) { + boolean leaderBySelf = entity.getCreatedById() != null && entity.getCreatedById().equals(normalizedOperatorId); + boolean legacyLeaderBySelf = entity.getUserId() != null && entity.getUserId().equals(normalizedOperatorId); + if (!leaderBySelf && !legacyLeaderBySelf) { throw new BusinessException("只有组长才能维护分组"); } } @@ -332,8 +338,9 @@ public class ShopManageGroupService { LinkedHashSet userIds = new LinkedHashSet<>(); for (ShopManageGroupEntity group : groups) { - if (group.getCreatedById() != null && group.getCreatedById() > 0) { - userIds.add(group.getCreatedById()); + Long leaderUserId = resolveGroupLeaderUserId(group); + if (leaderUserId != null && leaderUserId > 0) { + userIds.add(leaderUserId); } } for (List members : membersByGroupId.values()) { @@ -355,8 +362,9 @@ public class ShopManageGroupService { ShopManageGroupItemVo vo = new ShopManageGroupItemVo(); vo.setId(entity.getId()); vo.setGroupName(entity.getGroupName()); - vo.setLeaderUserId(entity.getCreatedById()); - AdminUserEntity leaderUser = entity.getCreatedById() == null ? null : userById.get(entity.getCreatedById()); + Long leaderUserId = resolveGroupLeaderUserId(entity); + vo.setLeaderUserId(leaderUserId); + AdminUserEntity leaderUser = leaderUserId == null ? null : userById.get(leaderUserId); vo.setLeaderUsername(leaderUser == null ? "" : normalizeBlank(leaderUser.getUsername())); List memberEntities = membersByGroupId.getOrDefault(entity.getId(), Collections.emptyList()); @@ -380,4 +388,14 @@ public class ShopManageGroupService { } 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(); + } } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/ShopManageService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/ShopManageService.java index c994cde..5ab2b6f 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/ShopManageService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/ShopManageService.java @@ -15,7 +15,6 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -33,6 +32,7 @@ public class ShopManageService { long safePageSize = Math.min(Math.max(pageSize, 1), 100); String safeShopName = shopName == null ? "" : shopName.trim(); Set accessibleGroupIds = superAdmin ? Set.of() : shopManageGroupService.listAccessibleGroupIds(operatorId, false); + Set accessibleCreatorUserIds = superAdmin ? Set.of() : shopManageGroupService.listAccessibleCreatorUserIds(operatorId, false); LambdaQueryWrapper query = new LambdaQueryWrapper() .eq(groupId != null && groupId > 0, ShopManageEntity::getGroupId, groupId) @@ -40,20 +40,30 @@ public class ShopManageService { .orderByDesc(ShopManageEntity::getId); if (!superAdmin) { - Long normalizedOperatorId = normalizePositiveId(operatorId, "操作人不能为空"); - query.and(wrapper -> { - wrapper.eq(ShopManageEntity::getCreatedById, normalizedOperatorId); - if (!accessibleGroupIds.isEmpty()) { - wrapper.or().in(ShopManageEntity::getGroupId, accessibleGroupIds); - } - }); + normalizePositiveId(operatorId, "操作人不能为空"); + if (accessibleGroupIds.isEmpty() && accessibleCreatorUserIds.isEmpty()) { + query.eq(ShopManageEntity::getId, -1L); + } else { + 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); List rows = shopManageMapper .selectList(query.last("LIMIT " + ((safePage - 1) * safePageSize) + ", " + safePageSize)); - Map groupNameById = buildGroupNameMap(rows.stream() + Map groupNameById = shopManageGroupService.buildGroupNameMap(rows.stream() .map(ShopManageEntity::getGroupId) .filter(id -> id != null && id > 0) .distinct() @@ -190,22 +200,6 @@ public class ShopManageService { } } - private Map buildGroupNameMap(List groupIds) { - if (groupIds == null || groupIds.isEmpty()) { - return Map.of(); - } - LinkedHashMap 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) { ShopManageItemVo vo = new ShopManageItemVo(); vo.setId(entity.getId()); diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/SkipPriceAsinService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/SkipPriceAsinService.java index db38556..5499804 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/SkipPriceAsinService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/SkipPriceAsinService.java @@ -62,7 +62,7 @@ public class SkipPriceAsinService { List rows = skipPriceAsinMapper .selectList(query.last("LIMIT " + ((safePage - 1) * safePageSize) + ", " + safePageSize)); - Map groupNameById = buildGroupNameMap(rows.stream() + Map groupNameById = shopManageGroupService.buildGroupNameMap(rows.stream() .map(SkipPriceAsinEntity::getGroupId) .filter(id -> id != null && id > 0) .distinct() @@ -174,22 +174,6 @@ public class SkipPriceAsinService { return vo; } - private Map buildGroupNameMap(List groupIds) { - if (groupIds == null || groupIds.isEmpty()) { - return Map.of(); - } - LinkedHashMap 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 normalizeCountries(List countries) { LinkedHashSet normalized = new LinkedHashSet<>(); if (countries != null) { diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/service/ShopMatchTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/service/ShopMatchTaskService.java index 4f18f2d..17035e6 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/service/ShopMatchTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/service/ShopMatchTaskService.java @@ -132,6 +132,21 @@ public class ShopMatchTaskService { return tasks; } + private List selectTaskStatusesByIdsInBatches(List taskIds) { + List 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() + .select(FileTaskEntity::getId, FileTaskEntity::getStatus) + .in(FileTaskEntity::getId, taskIds.subList(start, end)))); + } + return tasks; + } + public ProductRiskDashboardVo dashboard(Long userId) { if (userId == null || userId <= 0) { throw new BusinessException("user_id 不合法"); @@ -174,7 +189,7 @@ public class ShopMatchTaskService { .distinct() .toList(); if (!taskIds.isEmpty()) { - for (FileTaskEntity task : selectTasksByIdsInBatches(taskIds)) { + for (FileTaskEntity task : selectTaskStatusesByIdsInBatches(taskIds)) { if (task != null) { statusByTaskId.put(task.getId(), task.getStatus()); } @@ -465,7 +480,6 @@ public class ShopMatchTaskService { } - @Transactional public void submitResult(Long taskId, ShopMatchSubmitResultRequest request) { if (taskId == null || taskId <= 0) { throw new BusinessException("taskId 不合法"); @@ -540,7 +554,6 @@ public class ShopMatchTaskService { updateTaskStatusFromLatestRows(task, latest); } - @Transactional public boolean tryFinalizeTask(Long taskId, boolean fromCompensation) { if (taskId == null || taskId <= 0) { return false; diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/split/service/SplitRunService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/split/service/SplitRunService.java index 474e36e..91630f6 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/split/service/SplitRunService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/split/service/SplitRunService.java @@ -456,8 +456,8 @@ public class SplitRunService { if (value == null) { return ""; } - return value.replace("\ufeff", "") - .replace("\u3000", " ") + return value.replace(String.valueOf((char) 0xFEFF), "") + .replace((char) 0x3000, ' ') .replace("\r\n", " ") .replace("\r", " ") .replace("\n", " ") diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskFileJobService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskFileJobService.java index 8166c2a..8365bf5 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskFileJobService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskFileJobService.java @@ -23,7 +23,6 @@ public class TaskFileJobService { private final TaskFileJobMapper taskFileJobMapper; private final ApplicationEventPublisher applicationEventPublisher; - @Transactional public TaskFileJobEntity enqueueAssembleResult(Long taskId, String moduleType, Long resultId, String scopeKey) { if (taskId == null || taskId <= 0 || moduleType == null || moduleType.isBlank() || resultId == null || resultId <= 0) { return null; @@ -146,7 +145,6 @@ public class TaskFileJobService { return reset; } - @Transactional public boolean markRunning(Long jobId) { if (jobId == null || jobId <= 0) { return false; @@ -158,7 +156,6 @@ public class TaskFileJobService { .set(TaskFileJobEntity::getUpdatedAt, LocalDateTime.now())) > 0; } - @Transactional public void touchRunning(Long jobId) { if (jobId == null || jobId <= 0) { return; @@ -169,7 +166,6 @@ public class TaskFileJobService { .set(TaskFileJobEntity::getUpdatedAt, LocalDateTime.now())); } - @Transactional public void markSuccess(TaskFileJobEntity job, String resultFileUrl) { taskFileJobMapper.update(null, new LambdaUpdateWrapper() .eq(TaskFileJobEntity::getId, job.getId()) @@ -180,7 +176,6 @@ public class TaskFileJobService { .set(TaskFileJobEntity::getFinishedAt, LocalDateTime.now())); } - @Transactional public void markFailed(TaskFileJobEntity job, String message) { int retryCount = job.getRetryCount() == null ? 0 : job.getRetryCount() + 1; taskFileJobMapper.update(null, new LambdaUpdateWrapper() diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskResultItemService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskResultItemService.java index d955967..66b9de9 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskResultItemService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskResultItemService.java @@ -25,7 +25,6 @@ public class TaskResultItemService { private final ObjectMapper objectMapper; private final TransientPayloadStorageService transientPayloadStorageService; - @Transactional 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) { return; @@ -35,7 +34,6 @@ public class TaskResultItemService { upsert(taskId, moduleType, resultId, normalizedScopeKey, itemKey, null, null, null, snapshot); } - @Transactional public void replaceTaskSnapshots(Long taskId, String moduleType, List snapshots, SnapshotKeyResolver resolver) { if (taskId == null || taskId <= 0 || isBlank(moduleType) || snapshots == null) { return; diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskResultPayloadService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskResultPayloadService.java index e6fdf36..d6e094a 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskResultPayloadService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskResultPayloadService.java @@ -25,7 +25,6 @@ public class TaskResultPayloadService { private final ObjectMapper objectMapper; private final TransientPayloadStorageService transientPayloadStorageService; - @Transactional public void saveLatest(Long taskId, String moduleType, String scopeKey, Object payload) { if (taskId == null || taskId <= 0 || isBlank(moduleType) || isBlank(scopeKey) || payload == null) { return; diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/service/ZiniaoAuthService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/service/ZiniaoAuthService.java index bc206da..f8c2a15 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/service/ZiniaoAuthService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/service/ZiniaoAuthService.java @@ -212,7 +212,7 @@ public boolean isIpWhitelistError(BusinessException ex) { if (value == null) { return ""; } - return value.replace("\u3000", " ") + return value.replace((char) 0x3000, ' ') .trim(); } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/service/ZiniaoShopIndexService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/service/ZiniaoShopIndexService.java index a446d9a..44959e5 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/service/ZiniaoShopIndexService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/service/ZiniaoShopIndexService.java @@ -357,7 +357,7 @@ public class ZiniaoShopIndexService { if (value == null) { return ""; } - return value.replace("\u3000", " ").trim(); + return value.replace((char) 0x3000, ' ').trim(); } private ZiniaoShopMatchResultVo pendingResult(String message) { diff --git a/backend-java/src/main/resources/application.yml b/backend-java/src/main/resources/application.yml index 8184634..f1f6719 100644 --- a/backend-java/src/main/resources/application.yml +++ b/backend-java/src/main/resources/application.yml @@ -19,6 +19,10 @@ spring: maximum-pool-size: ${AIIMAGE_DB_POOL_MAX_SIZE:30} minimum-idle: ${AIIMAGE_DB_POOL_MIN_IDLE:5} 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} data: redis: @@ -54,7 +58,7 @@ mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: map-underscore-to-camel-case: true - log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl springdoc: api-docs: diff --git a/backend-java/src/main/resources/db/V36__shopkey_list_query_indexes.sql b/backend-java/src/main/resources/db/V36__shopkey_list_query_indexes.sql new file mode 100644 index 0000000..747c3ea --- /dev/null +++ b/backend-java/src/main/resources/db/V36__shopkey_list_query_indexes.sql @@ -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; diff --git a/backend-java/src/main/resources/db/V37__permission_menu_query_indexes.sql b/backend-java/src/main/resources/db/V37__permission_menu_query_indexes.sql new file mode 100644 index 0000000..daa2db1 --- /dev/null +++ b/backend-java/src/main/resources/db/V37__permission_menu_query_indexes.sql @@ -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; diff --git a/backend-java/src/main/resources/db/V38__task_result_hot_path_indexes.sql b/backend-java/src/main/resources/db/V38__task_result_hot_path_indexes.sql new file mode 100644 index 0000000..01ac687 --- /dev/null +++ b/backend-java/src/main/resources/db/V38__task_result_hot_path_indexes.sql @@ -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; diff --git a/backend/__pycache__/config.cpython-312.pyc b/backend/__pycache__/config.cpython-312.pyc index 0afb1db..f3270ad 100644 Binary files a/backend/__pycache__/config.cpython-312.pyc and b/backend/__pycache__/config.cpython-312.pyc differ diff --git a/backend/blueprints/__pycache__/admin_api.cpython-312.pyc b/backend/blueprints/__pycache__/admin_api.cpython-312.pyc index e207bee..603f21e 100644 Binary files a/backend/blueprints/__pycache__/admin_api.cpython-312.pyc and b/backend/blueprints/__pycache__/admin_api.cpython-312.pyc differ diff --git a/backend/blueprints/admin_api.py b/backend/blueprints/admin_api.py index 1652c5c..ecda053 100644 --- a/backend/blueprints/admin_api.py +++ b/backend/blueprints/admin_api.py @@ -4,9 +4,11 @@ import json import os import re +import threading 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 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('/') 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 = { 'dedupe-total-data': { @@ -103,10 +108,22 @@ def _safe_version_key(version): 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): url = f"{backend_java_base_url}{path}" 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: return None, jsonify({'success': False, 'error': 'backend-java 服务不可用'}), 502 try: @@ -146,6 +163,28 @@ def _parse_optional_int(value): 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() try: with conn.cursor() as cur: @@ -299,6 +338,10 @@ def _get_current_admin_permission_sets(): user_id = _get_current_admin_id() if not user_id: 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() try: with conn.cursor() as cur: @@ -314,7 +357,9 @@ def _get_current_admin_permission_sets(): conn.close() 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()} - return key_set, route_set + cached = (key_set, route_set) + setattr(g, cache_key, cached) + return cached def _ensure_admin_menu_access(*menu_names): @@ -830,14 +875,6 @@ def list_columns(): if denied: return denied 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) items = [ { @@ -1484,32 +1521,7 @@ def _load_expanded_shop_manage_groups(role, current_row): if error_response is not None: return None, error_response, status - direct_groups = result.get('data') or [] - 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 + return result.get('data') or [], None, 200 @admin_api.route('/shop-manages') @@ -1523,59 +1535,6 @@ def list_shop_manages(): group_id_raw = (request.args.get('group_id') 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} if current_row and current_row.get('id'): params['operatorId'] = current_row.get('id') diff --git a/backend/config.py b/backend/config.py index 4e042f5..ccb1747 100644 --- a/backend/config.py +++ b/backend/config.py @@ -23,9 +23,9 @@ bucket_path = "nanri-image/" file_url_pre = f"https://{bucket}.oss-cn-hangzhou.aliyuncs.com/" 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://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_SECRET'] = accessKeySecret os.environ['SECRET_KEY'] = "ddffc7c1d02121d9554d7b080b2511b6" diff --git a/backend/utils/__pycache__/auth.cpython-312.pyc b/backend/utils/__pycache__/auth.cpython-312.pyc index f8172f6..4638aec 100644 Binary files a/backend/utils/__pycache__/auth.cpython-312.pyc and b/backend/utils/__pycache__/auth.cpython-312.pyc differ diff --git a/backend/utils/__pycache__/db.cpython-312.pyc b/backend/utils/__pycache__/db.cpython-312.pyc index 7b0e8ac..036fe34 100644 Binary files a/backend/utils/__pycache__/db.cpython-312.pyc and b/backend/utils/__pycache__/db.cpython-312.pyc differ diff --git a/backend/utils/auth.py b/backend/utils/auth.py index 772cd13..5b5c0a7 100644 --- a/backend/utils/auth.py +++ b/backend/utils/auth.py @@ -3,7 +3,7 @@ """ 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 @@ -13,15 +13,22 @@ def is_session_user_valid(): uid = session.get('user_id') if not uid: return False + cached_user = getattr(g, '_current_user_row', None) + if cached_user and cached_user.get('id') == uid: + return True try: conn = get_db() 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() conn.close() if not row: session.clear() return False + g._current_user_row = row return True except Exception: session.clear() @@ -34,16 +41,19 @@ def get_current_admin_role(): if not uid: return None, None try: - conn = get_db() - with conn.cursor() as cur: - cur.execute( - "SELECT id, username, is_admin, role, created_by_id FROM users WHERE id = %s", - (uid,) - ) - row = cur.fetchone() - conn.close() + row = getattr(g, '_current_user_row', None) + if not row or row.get('id') != uid: + conn = get_db() + with conn.cursor() as cur: + cur.execute( + "SELECT id, username, is_admin, role, created_by_id FROM users WHERE id = %s", + (uid,) + ) + row = cur.fetchone() + conn.close() if not row: return None, None + g._current_user_row = row role = (row.get('role') or '').strip().lower() if not role: role = 'super_admin' if row.get('is_admin') and row.get('created_by_id') is None else ( diff --git a/backend/utils/db.py b/backend/utils/db.py index edf13ef..5ec2849 100644 --- a/backend/utils/db.py +++ b/backend/utils/db.py @@ -6,12 +6,20 @@ import pymysql from werkzeug.security import generate_password_hash 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: - mysql_host = os.environ.get('MYSQL_HOST', 'localhost') - mysql_user = os.environ.get('MYSQL_USER', 'root') - mysql_password = os.environ.get('MYSQL_PASSWORD', '') - mysql_database = os.environ.get('MYSQL_DATABASE', 'maixiang_ai') + config_mysql_host = 'localhost' + config_mysql_user = 'root' + config_mysql_password = '' + 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(): diff --git a/backend/web_source/_tmp_prefix.js b/backend/web_source/_tmp_prefix.js deleted file mode 100644 index c578064..0000000 --- a/backend/web_source/_tmp_prefix.js +++ /dev/null @@ -1,2000 +0,0 @@ - - (function () { - // Tab 鍒囨崲 - var adminTabsEl = document.getElementById('adminTabs'); - if (adminTabsEl) adminTabsEl.innerHTML = ''; - var activeAdminTabName = ''; - var ADMIN_PANEL_MAP = { - 'users': 'panel-users', - 'columns': 'panel-columns', - 'dedupe-total-data': 'panel-dedupe-total-data', - 'shop-keys': 'panel-shop-keys', - 'shop-manage': 'panel-shop-manage', - 'skip-price-asin': 'panel-skip-price-asin', - 'history': 'panel-history', - 'version': 'panel-version' - }; - function runTabLoader(tabName) { - if (tabName === 'users') { loadUsers(1); loadColumnsForPermission(); } - else if (tabName === 'columns') loadColumns(); - else if (tabName === 'dedupe-total-data') loadDedupeTotalData(1); - else if (tabName === 'shop-keys') loadShopKeys(1); - else if (tabName === 'shop-manage') loadShopManage(1); - else if (tabName === 'skip-price-asin') loadSkipPriceAsin(1); - else if (tabName === 'history') loadHistory(1); - else if (tabName === 'version') loadVersions(); - } - function hideAllAdminPanels() { - document.querySelectorAll('.tab-panel').forEach(function (panel) { - panel.classList.remove('active'); - panel.style.display = 'none'; - }); - } - function getActiveAdminTabName() { - return activeAdminTabName; - } - function activateAdminTab(tabName) { - var panelId = ADMIN_PANEL_MAP[tabName]; - var panel = panelId ? document.getElementById(panelId) : null; - if (!panel) return; - activeAdminTabName = tabName; - document.querySelectorAll('#adminTabs .tab').forEach(function (tab) { - tab.classList.toggle('active', tab.dataset.tab === tabName); - }); - hideAllAdminPanels(); - panel.style.display = ''; - panel.classList.add('active'); - runTabLoader(tabName); - } - function renderAdminTabs(items) { - var knownItems = (items || []).filter(function (item) { - return !!ADMIN_PANEL_MAP[item.route_path]; - }); - if (!knownItems.length) { - activeAdminTabName = ''; - if (adminTabsEl) adminTabsEl.innerHTML = '
鏆傛棤鍙敤鑿滃崟
'; - hideAllAdminPanels(); - return; - } - if (adminTabsEl) { - adminTabsEl.innerHTML = knownItems.map(function (item) { - return '
' + (item.name || item.route_path) + '
'; - }).join(''); - } - document.querySelectorAll('#adminTabs .tab').forEach(function (tab) { - tab.onclick = function () { - activateAdminTab(tab.dataset.tab); - }; - }); - var fallbackTab = activeAdminTabName && knownItems.some(function (item) { return item.route_path === activeAdminTabName; }) - ? activeAdminTabName - : knownItems[0].route_path; - activateAdminTab(fallbackTab); - } - function loadAdminMenus(preferredTab) { - if (preferredTab) activeAdminTabName = preferredTab; - fetch('/api/admin/current-user/menus') - .then(function (r) { return r.json(); }) - .then(function (res) { - if (!res.success) { - if (adminTabsEl) adminTabsEl.innerHTML = '
鑿滃崟鍔犺浇澶辫触
'; - hideAllAdminPanels(); - return; - } - renderAdminTabs(res.items || []); - }) - .catch(function () { - if (adminTabsEl) adminTabsEl.innerHTML = '
鑿滃崟鍔犺浇澶辫触
'; - hideAllAdminPanels(); - }); - } - hideAllAdminPanels(); - - // ========== 鐢ㄦ埛绠$悊 ========== - var userPage = 1, userPageSize = 15; - var currentUserId = null; - var currentUserRole = 'admin'; - var currentUserUsername = ''; - var adminsList = []; - function roleLabel(role) { - if (role === 'super_admin') return '瓒呯骇绠$悊鍛?; - if (role === 'admin') return '绠$悊鍛?; - return '鏅€氳处鍙?; - } - function buildUserListQuery(page) { - var q = 'page=' + (page || 1) + '&page_size=' + userPageSize; - var kw = (document.getElementById('searchUsername').value || '').trim(); - if (kw) q += '&username=' + encodeURIComponent(kw); - var cby = document.getElementById('filterCreatedBy').value; - if (cby) q += '&created_by_id=' + encodeURIComponent(cby); - return q; - } - function loadUsers(page) { - userPage = page || 1; - fetch('/api/admin/users?' + buildUserListQuery(userPage)) - .then(function (r) { return r.json(); }) - .then(function (res) { - var tbody = document.getElementById('userListBody'); - if (!res.success) { - tbody.innerHTML = '鍔犺浇澶辫触: ' + (res.error || '') + ''; - return; - } - currentUserId = res.current_user_id || null; - currentUserRole = res.current_user_role || 'admin'; - currentUserUsername = (res.current_user_username || '').trim(); - adminsList = res.admins || []; - var items = res.items || []; - if (items.length === 0) { - tbody.innerHTML = '鏆傛棤鐢ㄦ埛'; - } else { - tbody.innerHTML = items.map(function (u) { - return '' + u.id + '' + (u.username || '') + '' + - roleLabel(u.role || 'normal') + '' + (u.creator_username || '-') + '' + (u.created_at || '') + '' + - ' ' + - '' + - ''; - }).join(''); - } - renderPagination('userPagination', res.total, res.page, res.page_size, loadUsers); - bindUserActions(); - updateCreateFormByRole(); - updateUserFilterByRole(); - }) - .catch(function () { - document.getElementById('userListBody').innerHTML = '璇锋眰澶辫触'; - }); - } - function updateUserFilterByRole() { - var grp = document.getElementById('filterCreatedByGroup'); - var sel = document.getElementById('filterCreatedBy'); - if (currentUserRole === 'super_admin') { - grp.style.display = 'block'; - var cur = sel.value; - sel.innerHTML = ''; - adminsList.forEach(function (a) { - var opt = document.createElement('option'); - opt.value = a.id; - opt.textContent = a.username; - sel.appendChild(opt); - }); - sel.value = cur || ''; - } else { - grp.style.display = 'none'; - } - } - - function updateDedupeTotalDataAccess() { - var tab = document.querySelector('.tab[data-tab="dedupe-total-data"]'); - var panel = document.getElementById('panel-dedupe-total-data'); - var canUse = currentUserRole === 'super_admin' || (currentUserRole === 'admin' && currentUserUsername === ''); - if (tab) tab.style.display = canUse ? '' : 'none'; - if (panel) panel.style.display = canUse ? '' : 'none'; - if (!canUse && tab && tab.classList.contains('active')) { - tab.classList.remove('active'); - if (panel) panel.classList.remove('active'); - var usersTab = document.querySelector('.tab[data-tab="users"]'); - var usersPanel = document.getElementById('panel-users'); - if (usersTab) usersTab.classList.add('active'); - if (usersPanel) usersPanel.classList.add('active'); - } - } - function updateShopManageAccess() { - var tab = document.querySelector('.tab[data-tab="shop-manage"]'); - var panel = document.getElementById('panel-shop-manage'); - if (!tab || !panel) return; - var canUse = currentUserRole === 'super_admin' || - !!currentUserAdminPermissionKeys['admin_shop_manage'] || - !!currentUserAdminPermissionRoutes['shop-manage']; - tab.style.display = canUse ? '' : 'none'; - panel.style.display = canUse ? '' : 'none'; - if (!canUse && tab.classList.contains('active')) { - tab.classList.remove('active'); - panel.classList.remove('active'); - var usersTab = document.querySelector('.tab[data-tab="users"]'); - var usersPanel = document.getElementById('panel-users'); - if (usersTab) usersTab.classList.add('active'); - if (usersPanel) usersPanel.classList.add('active'); - } - } - function loadCurrentUserAdminPermissions() { - currentUserAdminPermissionKeys = {}; - currentUserAdminPermissionRoutes = {}; - if (!currentUserId || currentUserRole === 'super_admin') { - updateShopManageAccess(); - return; - } - fetch('/api/admin/user/' + currentUserId + '/column-permissions?menu_type=admin') - .then(function (r) { return r.json(); }) - .then(function (res) { - if (!res.success) { - updateShopManageAccess(); - return; - } - (res.items || []).forEach(function (item) { - var columnKey = (item.column_key || '').trim(); - var routePath = (item.route_path || '').trim(); - if (columnKey) currentUserAdminPermissionKeys[columnKey] = true; - if (routePath) currentUserAdminPermissionRoutes[routePath] = true; - }); - updateShopManageAccess(); - }) - .catch(function () { - updateShopManageAccess(); - }); - } - function applyTabAccess(tabName, canUse) { - var tab = document.querySelector('.tab[data-tab="' + tabName + '"]'); - var panel = document.getElementById('panel-' + tabName); - if (tab) tab.style.display = canUse ? '' : 'none'; - if (panel) panel.style.display = canUse ? '' : 'none'; - if (!canUse && tab && tab.classList.contains('active')) { - tab.classList.remove('active'); - if (panel) panel.classList.remove('active'); - var usersTab = document.querySelector('.tab[data-tab="users"]'); - var usersPanel = document.getElementById('panel-users'); - if (usersTab) usersTab.classList.add('active'); - if (usersPanel) usersPanel.classList.add('active'); - } - } - function hasAdminTabAccess(tabName) { - var config = ADMIN_TAB_ACCESS_CONFIG[tabName]; - if (!config) return true; - if (currentUserRole === 'super_admin') return true; - if (config.superAdminOnly) return false; - return !!currentUserAdminPermissionKeys[config.columnKey] || - !!currentUserAdminPermissionRoutes[config.routePath]; - } - function refreshAdminTabAccess() { - Object.keys(ADMIN_TAB_ACCESS_CONFIG).forEach(function (tabName) { - applyTabAccess(tabName, hasAdminTabAccess(tabName)); - }); - } - function loadCurrentUserAdminPermissions() { - currentUserAdminPermissionKeys = {}; - currentUserAdminPermissionRoutes = {}; - if (!currentUserId || currentUserRole === 'super_admin') { - refreshAdminTabAccess(); - return; - } - fetch('/api/admin/user/' + currentUserId + '/column-permissions?menu_type=admin') - .then(function (r) { return r.json(); }) - .then(function (res) { - if (!res.success) { - refreshAdminTabAccess(); - return; - } - (res.items || []).forEach(function (item) { - var columnKey = (item.column_key || '').trim(); - var routePath = (item.route_path || '').trim(); - if (columnKey) currentUserAdminPermissionKeys[columnKey] = true; - if (routePath) currentUserAdminPermissionRoutes[routePath] = true; - }); - refreshAdminTabAccess(); - }) - .catch(function () { - refreshAdminTabAccess(); - }); - } - var allColumnsList = []; - function loadColumnsForPermission() { - fetch('/api/admin/columns?menu_type=admin') - .then(function (r) { return r.json(); }) - .then(function (res) { - if (!res.success) return; - allColumnsList = res.items || []; - renderColumnPermissionWrap('createColumnPermissionWrap'); - renderColumnPermissionWrap('editColumnPermissionWrap'); - var selCreate = document.getElementById('createColumnPermissionWrap'); - var selEdit = document.getElementById('editColumnPermissionWrap'); - if (selCreate && !selCreate._colCardsBound) { - selCreate._colCardsBound = true; - selCreate.onchange = function () { renderColumnCards('createColumnPermissionWrap'); }; - } - if (selEdit && !selEdit._colCardsBound) { - selEdit._colCardsBound = true; - selEdit.onchange = function () { renderColumnCards('editColumnPermissionWrap'); }; - } - }) - .catch(function () { }); - } - var columnCardsContainerMap = { createColumnPermissionWrap: 'createColumnCards', editColumnPermissionWrap: 'editColumnCards' }; - function renderColumnCards(selectId) { - var sel = document.getElementById(selectId); - var cardsId = columnCardsContainerMap[selectId]; - var cardsEl = cardsId ? document.getElementById(cardsId) : null; - if (!sel || sel.tagName !== 'SELECT' || !cardsEl) return; - cardsEl.innerHTML = ''; - for (var i = 0; i < sel.options.length; i++) { - var opt = sel.options[i]; - if (opt.disabled || !opt.selected) continue; - var card = document.createElement('span'); - card.className = 'column-permission-card'; - card.textContent = opt.textContent; - var btn = document.createElement('button'); - btn.type = 'button'; - btn.className = 'col-card-remove'; - btn.setAttribute('aria-label', '绉婚櫎'); - btn.textContent = '脳'; - (function (option, seldId) { - btn.onclick = function () { - option.selected = false; - renderColumnCards(seldId); - }; - })(opt, selectId); - card.appendChild(btn); - cardsEl.appendChild(card); - } - } - function renderColumnPermissionWrap(wrapId) { - var sel = document.getElementById(wrapId); - if (!sel || sel.tagName !== 'SELECT') return; - sel.innerHTML = ''; - allColumnsList.forEach(function (c) { - var opt = document.createElement('option'); - opt.value = c.id; - opt.textContent = c.name + ' (' + c.column_key + ')'; - sel.appendChild(opt); - }); - if (allColumnsList.length === 0) { - var opt = document.createElement('option'); - opt.disabled = true; - opt.textContent = '鏆傛棤鑿滃崟锛岃鍏堝湪鈥滄爮鐩潈闄愰厤缃€濅腑鏂板鑿滃崟'; - sel.appendChild(opt); - } - renderColumnCards(wrapId); - } - function getSelectedColumnIds(wrapId) { - var sel = document.getElementById(wrapId); - if (!sel || sel.tagName !== 'SELECT') return []; - var ids = []; - for (var i = 0; i < sel.options.length; i++) { - if (sel.options[i].selected) { - var v = parseInt(sel.options[i].value, 10); - if (!isNaN(v)) ids.push(v); - } - } - return ids; - } - function setColumnPermissionCheckboxes(wrapId, columnIds) { - var sel = document.getElementById(wrapId); - if (!sel || sel.tagName !== 'SELECT') return; - var set = {}; - (columnIds || []).forEach(function (id) { set[id] = true; }); - for (var i = 0; i < sel.options.length; i++) { - var opt = sel.options[i]; - if (opt.disabled) continue; - opt.selected = set[parseInt(opt.value, 10)] || false; - } - renderColumnCards(wrapId); - } - function updateCreateFormByRole() { - var roleSel = document.getElementById('createRole'); - var optAdmin = document.getElementById('optAdmin'); - var formCreatedBy = document.getElementById('formGroupCreatedBy'); - var selCreatedBy = document.getElementById('createCreatedBy'); - if (currentUserRole === 'super_admin') { - if (optAdmin) optAdmin.style.display = ''; - formCreatedBy.style.display = (roleSel.value === 'normal') ? 'block' : 'none'; - selCreatedBy.innerHTML = ''; - (res.items || []).forEach(function (u) { - var opt = document.createElement('option'); - opt.value = u.id; - opt.textContent = u.username + ' (' + roleLabel(u.role || 'normal') + ')'; - sel.appendChild(opt); - }); - sel.value = cur || ''; - }); - } - var shopManageGroupUsers = []; - function refreshShopManageGroupUserSelect(selectedUserId) { - var sel = document.getElementById('shopManageGroupUserSelect'); - if (!sel) return; - var opts = ['']; - shopManageGroupUsers.forEach(function (u) { - opts.push(''); - }); - sel.innerHTML = opts.join(''); - if (selectedUserId != null && selectedUserId !== '') { - sel.value = String(selectedUserId); - } - syncShopManageGroupNameWithUser(); - } - function syncShopManageGroupNameWithUser() { - var sel = document.getElementById('shopManageGroupUserSelect'); - var input = document.getElementById('shopManageGroupInput'); - if (!sel || !input) return; - var selectedUser = null; - shopManageGroupUsers.some(function (u) { - if (String(u.id) === String(sel.value || '')) { - selectedUser = u; - return true; - } - return false; - }); - input.value = selectedUser ? (selectedUser.username || '') : ''; - } - function loadUserOptions() { - return fetch('/api/admin/users?page=1&page_size=999') - .then(function (r) { return r.json(); }) - .then(function (res) { - var items = res.items || []; - shopManageGroupUsers = items.map(function (u) { - return { id: u.id, username: u.username || '', role: u.role || 'normal' }; - }); - refreshShopManageGroupUserSelect(document.getElementById('shopManageGroupUserSelect') ? document.getElementById('shopManageGroupUserSelect').value : ''); - var sel = document.getElementById('filterUser'); - var cur = sel.value; - sel.innerHTML = ''; - items.forEach(function (u) { - var opt = document.createElement('option'); - opt.value = u.id; - opt.textContent = u.username + ' (' + roleLabel(u.role || 'normal') + ')'; - sel.appendChild(opt); - }); - sel.value = cur || ''; - }); - } - document.getElementById('btnFilterHistory').onclick = function () { loadHistory(1); }; - - // ========== 鏁版嵁鍘婚噸鎬绘暟鎹?========== - var dedupeTotalDataPage = 1, dedupeTotalDataPageSize = 15; - function buildDedupeTotalDataQuery(page) { - var q = 'page=' + (page || 1) + '&page_size=' + dedupeTotalDataPageSize; - var keyword = (document.getElementById('searchDedupeTotalData').value || '').trim(); - if (keyword) q += '&keyword=' + encodeURIComponent(keyword); - return q; - } - function loadDedupeTotalData(page) { - dedupeTotalDataPage = page || 1; - fetch('/api/admin/dedupe-total-data?' + buildDedupeTotalDataQuery(dedupeTotalDataPage)) - .then(function (r) { return r.json(); }) - .then(function (res) { - var tbody = document.getElementById('dedupeTotalDataListBody'); - if (!res.success) { - tbody.innerHTML = '鍔犺浇澶辫触: ' + (res.error || '') + ''; - return; - } - var items = res.items || []; - if (items.length === 0) { - tbody.innerHTML = '鏆傛棤鎬绘暟鎹?/td>'; - } else { - tbody.innerHTML = items.map(function (item) { - return '' + item.id + '' + (item.data_value || '') + '' + (item.created_at || '') + '' + - ' ' + - '' + - ''; - }).join(''); - } - renderPagination('dedupeTotalDataPagination', res.total, res.page, res.page_size, loadDedupeTotalData); - bindDedupeTotalDataActions(); - }) - .catch(function () { - document.getElementById('dedupeTotalDataListBody').innerHTML = '璇锋眰澶辫触'; - }); - } - function bindDedupeTotalDataActions() { - document.querySelectorAll('[data-dedupe-total-edit]').forEach(function (btn) { - btn.onclick = function () { - document.getElementById('editDedupeTotalDataId').value = btn.dataset.dedupeTotalEdit || ''; - document.getElementById('editDedupeTotalDataValue').value = (btn.dataset.value || '').replace(/"/g, '"'); - document.getElementById('msgEditDedupeTotalData').textContent = ''; - document.getElementById('msgEditDedupeTotalData').className = 'msg'; - document.getElementById('editDedupeTotalDataModal').classList.add('show'); - }; - }); - document.querySelectorAll('[data-dedupe-total-delete]').forEach(function (btn) { - btn.onclick = function () { - var value = (btn.dataset.value || '').replace(/"/g, '"'); - if (!confirm('纭畾鍒犻櫎鎬绘暟鎹€? + value + '鈥濆悧锛?)) return; - fetch('/api/admin/dedupe-total-data/' + btn.dataset.dedupeTotalDelete, { method: 'DELETE' }) - .then(function (r) { return r.json(); }) - .then(function (res) { - if (res.success) { loadDedupeTotalData(dedupeTotalDataPage); } - else { alert(res.error || '鍒犻櫎澶辫触'); } - }); - }; - }); - } - document.getElementById('btnSearchDedupeTotalData').onclick = function () { loadDedupeTotalData(1); }; - var dedupeImportPollTimer = null; - var dedupeDeleteImportPollTimer = null; - function stopDedupeImportProgress() { - if (dedupeImportPollTimer) { - clearInterval(dedupeImportPollTimer); - dedupeImportPollTimer = null; - } - } - function stopDedupeDeleteImportProgress() { - if (dedupeDeleteImportPollTimer) { - clearInterval(dedupeDeleteImportPollTimer); - dedupeDeleteImportPollTimer = null; - } - } - function setDedupeImportProgress(percent, text) { - var wrap = document.getElementById('dedupeTotalDataProgressWrap'); - var fill = document.getElementById('dedupeTotalDataProgressFill'); - var textEl = document.getElementById('dedupeTotalDataProgressText'); - wrap.style.display = 'block'; - fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%'; - textEl.textContent = text || ''; - } - function setDedupeDeleteImportProgress(percent, text) { - var wrap = document.getElementById('dedupeTotalDataDeleteProgressWrap'); - var fill = document.getElementById('dedupeTotalDataDeleteProgressFill'); - var textEl = document.getElementById('dedupeTotalDataDeleteProgressText'); - wrap.style.display = 'block'; - fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%'; - textEl.textContent = text || ''; - } - function pollDedupeImport(importId) { - stopDedupeImportProgress(); - function tick() { - fetch('/api/admin/dedupe-total-data/import/' + encodeURIComponent(importId)) - .then(function (r) { return r.json(); }) - .then(function (res) { - if (!res.success) { - stopDedupeImportProgress(); - document.getElementById('msgDedupeTotalData').textContent = res.error || '鏌ヨ瀵煎叆杩涘害澶辫触'; - document.getElementById('msgDedupeTotalData').className = 'msg err'; - return; - } - var progress = res.progress || {}; - var totalRows = progress.total_rows || 0; - var processedRows = progress.processed_rows || 0; - var percent = totalRows > 0 ? Math.round(processedRows * 100 / totalRows) : 0; - setDedupeImportProgress(percent, '澶勭悊涓細宸插鐞?' + processedRows + ' / ' + totalRows + '锛屾柊澧?' + (progress.inserted_count || 0) + '锛岃烦杩?' + (progress.skipped_count || 0)); - if (progress.status === 'success') { - stopDedupeImportProgress(); - document.getElementById('msgDedupeTotalData').textContent = '瀵煎叆鎴愬姛锛氭€昏鏁?' + (progress.total_rows || 0) + '锛孉SIN 鏁伴噺 ' + (progress.asin_count || 0) + '锛屾柊澧?' + (progress.inserted_count || 0) + '锛岃烦杩?' + (progress.skipped_count || 0); - document.getElementById('msgDedupeTotalData').className = 'msg ok'; - loadDedupeTotalData(1); - } else if (progress.status === 'failed') { - stopDedupeImportProgress(); - document.getElementById('msgDedupeTotalData').textContent = progress.error_message || '瀵煎叆澶辫触'; - document.getElementById('msgDedupeTotalData').className = 'msg err'; - } - }) - .catch(function () { - stopDedupeImportProgress(); - document.getElementById('msgDedupeTotalData').textContent = '鏌ヨ瀵煎叆杩涘害澶辫触'; - document.getElementById('msgDedupeTotalData').className = 'msg err'; - }); - } - tick(); - dedupeImportPollTimer = setInterval(tick, 1000); - } - function pollDedupeDeleteImport(importId) { - stopDedupeDeleteImportProgress(); - function tick() { - fetch('/api/admin/dedupe-total-data/delete-import/' + encodeURIComponent(importId)) - .then(function (r) { return r.json(); }) - .then(function (res) { - if (!res.success) { - stopDedupeDeleteImportProgress(); - document.getElementById('msgDeleteDedupeTotalData').textContent = res.error || '鏌ヨ鍒犻櫎杩涘害澶辫触'; - document.getElementById('msgDeleteDedupeTotalData').className = 'msg err'; - return; - } - var progress = res.progress || {}; - var totalRows = progress.total_rows || 0; - var processedRows = progress.processed_rows || 0; - var percent = totalRows > 0 ? Math.round(processedRows * 100 / totalRows) : 0; - setDedupeDeleteImportProgress(percent, '澶勭悊涓細宸插鐞?' + processedRows + ' / ' + totalRows + '锛屽垹闄?' + (progress.deleted_count || 0) + '锛岃烦杩?' + (progress.skipped_count || 0)); - if (progress.status === 'success') { - stopDedupeDeleteImportProgress(); - document.getElementById('msgDeleteDedupeTotalData').textContent = '鍒犻櫎鎴愬姛锛氭€昏鏁?' + (progress.total_rows || 0) + '锛孉SIN 鏁伴噺 ' + (progress.asin_count || 0) + '锛屽垹闄?' + (progress.deleted_count || 0) + '锛岃烦杩?' + (progress.skipped_count || 0); - document.getElementById('msgDeleteDedupeTotalData').className = 'msg ok'; - loadDedupeTotalData(1); - } else if (progress.status === 'failed') { - stopDedupeDeleteImportProgress(); - document.getElementById('msgDeleteDedupeTotalData').textContent = progress.error_message || '鍒犻櫎澶辫触'; - document.getElementById('msgDeleteDedupeTotalData').className = 'msg err'; - } - }) - .catch(function () { - stopDedupeDeleteImportProgress(); - document.getElementById('msgDeleteDedupeTotalData').textContent = '鏌ヨ鍒犻櫎杩涘害澶辫触'; - document.getElementById('msgDeleteDedupeTotalData').className = 'msg err'; - }); - } - tick(); - dedupeDeleteImportPollTimer = setInterval(tick, 1000); - } - - document.getElementById('btnAddDedupeTotalData').onclick = function () { - var fileInput = document.getElementById('dedupeTotalDataFile'); - var msgEl = document.getElementById('msgDedupeTotalData'); - msgEl.textContent = ''; - msgEl.className = 'msg'; - stopDedupeImportProgress(); - document.getElementById('dedupeTotalDataProgressWrap').style.display = 'none'; - if (!fileInput.files || fileInput.files.length === 0) { - msgEl.textContent = '璇烽€夋嫨 Excel 鏂囦欢'; - msgEl.classList.add('err'); - return; - } - var file = fileInput.files[0]; - var lowerName = (file.name || '').toLowerCase(); - if (!(lowerName.endsWith('.xlsx') || lowerName.endsWith('.xls'))) { - msgEl.textContent = '浠呮敮鎸?.xlsx 鎴?.xls 鏂囦欢'; - msgEl.classList.add('err'); - return; - } - var formData = new FormData(); - formData.append('file', file); - var xhr = new XMLHttpRequest(); - xhr.open('POST', '/api/admin/dedupe-total-data/import', true); - xhr.upload.onprogress = function (event) { - if (event.lengthComputable) { - var percent = Math.round(event.loaded * 100 / event.total); - setDedupeImportProgress(percent, '涓婁紶涓細' + percent + '%'); - } - }; - xhr.onreadystatechange = function () { - if (xhr.readyState !== 4) return; - if (xhr.status < 200 || xhr.status >= 300) { - msgEl.textContent = '璇锋眰澶辫触'; - msgEl.className = 'msg err'; - return; - } - var res; - try { res = JSON.parse(xhr.responseText || '{}'); } catch (e) { res = { success: false, error: '杩斿洖鏍煎紡閿欒' }; } - if (!res.success) { - msgEl.textContent = res.error || '瀵煎叆澶辫触'; - msgEl.className = 'msg err'; - return; - } - setDedupeImportProgress(100, '涓婁紶瀹屾垚锛屽悗绔鐞嗕腑...'); - pollDedupeImport(res.import_id); - fileInput.value = ''; - }; - xhr.onerror = function () { - msgEl.textContent = '璇锋眰澶辫触'; - msgEl.className = 'msg err'; - }; - xhr.send(formData); - }; - document.getElementById('btnDeleteImportDedupeTotalData').onclick = function () { - var fileInput = document.getElementById('dedupeTotalDataDeleteFile'); - var msgEl = document.getElementById('msgDeleteDedupeTotalData'); - msgEl.textContent = ''; - msgEl.className = 'msg'; - stopDedupeDeleteImportProgress(); - document.getElementById('dedupeTotalDataDeleteProgressWrap').style.display = 'none'; - if (!fileInput.files || fileInput.files.length === 0) { - msgEl.textContent = '璇烽€夋嫨 Excel 鏂囦欢'; - msgEl.classList.add('err'); - return; - } - var file = fileInput.files[0]; - var lowerName = (file.name || '').toLowerCase(); - if (!(lowerName.endsWith('.xlsx') || lowerName.endsWith('.xls'))) { - msgEl.textContent = '浠呮敮鎸?.xlsx 鎴?.xls 鏂囦欢'; - msgEl.classList.add('err'); - return; - } - if (!confirm('纭畾鎸?Excel 涓殑 ASIN 鎵归噺鍒犻櫎鍖归厤鐨勬€绘暟鎹悧锛?)) { - return; - } - var formData = new FormData(); - formData.append('file', file); - var xhr = new XMLHttpRequest(); - xhr.open('POST', '/api/admin/dedupe-total-data/delete-import', true); - xhr.upload.onprogress = function (event) { - if (event.lengthComputable) { - var percent = Math.round(event.loaded * 100 / event.total); - setDedupeDeleteImportProgress(percent, '涓婁紶涓細' + percent + '%'); - } - }; - xhr.onreadystatechange = function () { - if (xhr.readyState !== 4) return; - if (xhr.status < 200 || xhr.status >= 300) { - msgEl.textContent = '璇锋眰澶辫触'; - msgEl.className = 'msg err'; - return; - } - var res; - try { res = JSON.parse(xhr.responseText || '{}'); } catch (e) { res = { success: false, error: '杩斿洖鏍煎紡閿欒' }; } - if (!res.success) { - msgEl.textContent = res.error || '鍒犻櫎澶辫触'; - msgEl.className = 'msg err'; - return; - } - setDedupeDeleteImportProgress(100, '涓婁紶瀹屾垚锛屽悗绔鐞嗕腑...'); - pollDedupeDeleteImport(res.import_id); - fileInput.value = ''; - }; - xhr.onerror = function () { - msgEl.textContent = '璇锋眰澶辫触'; - msgEl.className = 'msg err'; - }; - xhr.send(formData); - }; - document.getElementById('btnSaveDedupeTotalData').onclick = function () { - var itemId = document.getElementById('editDedupeTotalDataId').value; - var value = (document.getElementById('editDedupeTotalDataValue').value || '').trim(); - var msgEl = document.getElementById('msgEditDedupeTotalData'); - msgEl.textContent = ''; - msgEl.className = 'msg'; - if (!value) { - msgEl.textContent = '璇峰~鍐欐€绘暟鎹€?; - msgEl.classList.add('err'); - return; - } - fetch('/api/admin/dedupe-total-data/' + itemId, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ data_value: value }) - }) - .then(function (r) { return r.json(); }) - .then(function (res) { - if (res.success) { - document.getElementById('editDedupeTotalDataModal').classList.remove('show'); - loadDedupeTotalData(dedupeTotalDataPage); - } else { - msgEl.textContent = res.error || '淇濆瓨澶辫触'; - msgEl.classList.add('err'); - } - }); - }; - document.getElementById('btnCloseEditDedupeTotalData').onclick = function () { - document.getElementById('editDedupeTotalDataModal').classList.remove('show'); - }; - - // ========== 搴楅摵瀵嗛挜绠$悊 ========== - var shopKeyPage = 1, shopKeyPageSize = 15; - function buildShopKeyQuery(page) { - return 'page=' + (page || 1) + '&page_size=' + shopKeyPageSize; - } - function loadShopKeys(page) { - shopKeyPage = page || 1; - fetch('/api/admin/shop-keys?' + buildShopKeyQuery(shopKeyPage)) - .then(function (r) { return r.json(); }) - .then(function (res) { - var tbody = document.getElementById('shopKeyListBody'); - if (!res.success) { - tbody.innerHTML = '鍔犺浇澶辫触: ' + (res.error || '') + ''; - return; - } - var items = res.items || []; - if (items.length === 0) { - tbody.innerHTML = '鏆傛棤搴楅摵瀵嗛挜'; - } else { - tbody.innerHTML = items.map(function (item, index) { - var rowNo = (shopKeyPage - 1) * shopKeyPageSize + index + 1; - return '' + rowNo + '' + (item.ziniao_account_name || '') + '' + (item.ziniao_token || '') + '' + (item.created_at || '') + '' + (item.updated_at || '') + '' + - ' ' + - '' + - ''; - }).join(''); - } - renderPagination('shopKeyPagination', res.total, res.page, res.page_size, loadShopKeys); - bindShopKeyActions(); - }) - .catch(function () { - document.getElementById('shopKeyListBody').innerHTML = '璇锋眰澶辫触'; - }); - } - function bindShopKeyActions() { - document.querySelectorAll('[data-shop-key-edit]').forEach(function (btn) { - btn.onclick = function () { - var item = {}; - try { item = JSON.parse((btn.dataset.shopKey || '').replace(/"/g, '"')); } catch (e) { item = {}; } - document.getElementById('editShopKeyId').value = item.id || ''; - document.getElementById('editShopKeyZiniaoAccountName').value = item.ziniao_account_name || ''; - document.getElementById('editShopKeyZiniaoToken').value = item.ziniao_token || ''; - document.getElementById('msgEditShopKey').textContent = ''; - document.getElementById('msgEditShopKey').className = 'msg'; - document.getElementById('editShopKeyModal').classList.add('show'); - }; - }); - document.querySelectorAll('[data-shop-key-delete]').forEach(function (btn) { - btn.onclick = function () { - var name = (btn.dataset.ziniaoAccountName || '').replace(/"/g, '"'); - if (!confirm('纭畾鍒犻櫎搴楅摵瀵嗛挜鈥? + name + '鈥濆悧锛?)) return; - fetch('/api/admin/shop-key/' + btn.dataset.shopKeyDelete, { method: 'DELETE' }) - .then(function (r) { return r.json(); }) - .then(function (res) { - if (res.success) { loadShopKeys(shopKeyPage); } - else { alert(res.error || '鍒犻櫎澶辫触'); } - }); - }; - }); - } - document.getElementById('btnCreateShopKey').onclick = function () { - var ziniaoAccountName = (document.getElementById('shopKeyZiniaoAccountName').value || '').trim(); - var ziniaoToken = (document.getElementById('shopKeyZiniaoToken').value || '').trim(); - var msgEl = document.getElementById('msgShopKey'); - msgEl.textContent = ''; - msgEl.className = 'msg'; - if (!ziniaoAccountName || !ziniaoToken) { - msgEl.textContent = '璇峰畬鏁村~鍐欑传楦熻处鍙峰悕绉般€佺传楦熶护鐗?; - msgEl.classList.add('err'); - return; - } - fetch('/api/admin/shop-key', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - ziniao_account_name: ziniaoAccountName, - ziniao_token: ziniaoToken - }) - }) - .then(function (r) { return r.json(); }) - .then(function (res) { - if (res.success) { - document.getElementById('shopKeyZiniaoAccountName').value = ''; - document.getElementById('shopKeyZiniaoToken').value = ''; - msgEl.textContent = res.msg || '鍒涘缓鎴愬姛'; - msgEl.className = 'msg ok'; - loadShopKeys(1); - } else { - msgEl.textContent = res.error || '鍒涘缓澶辫触'; - msgEl.className = 'msg err'; - } - }) - .catch(function () { - msgEl.textContent = '璇锋眰澶辫触'; - msgEl.className = 'msg err'; - }); - }; - document.getElementById('btnSaveShopKey').onclick = function () { - var itemId = document.getElementById('editShopKeyId').value; - var ziniaoAccountName = (document.getElementById('editShopKeyZiniaoAccountName').value || '').trim(); - var ziniaoToken = (document.getElementById('editShopKeyZiniaoToken').value || '').trim(); - var msgEl = document.getElementById('msgEditShopKey'); - msgEl.textContent = ''; - msgEl.className = 'msg'; - if (!ziniaoAccountName || !ziniaoToken) { - msgEl.textContent = '璇峰畬鏁村~鍐欑传楦熻处鍙峰悕绉般€佺传楦熶护鐗?; - msgEl.classList.add('err'); - return; - } - fetch('/api/admin/shop-key/' + itemId, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - ziniao_account_name: ziniaoAccountName, - ziniao_token: ziniaoToken - }) - }) - .then(function (r) { return r.json(); }) - .then(function (res) { - if (res.success) { - document.getElementById('editShopKeyModal').classList.remove('show'); - loadShopKeys(shopKeyPage); - } else { - msgEl.textContent = res.error || '淇濆瓨澶辫触'; - msgEl.classList.add('err'); - } - }) - .catch(function () { - msgEl.textContent = '璇锋眰澶辫触'; - msgEl.classList.add('err'); - }); - }; - document.getElementById('btnCloseEditShopKey').onclick = function () { - document.getElementById('editShopKeyModal').classList.remove('show'); - }; - - // ========== 搴楅摵绠$悊 ========== - var shopManagePage = 1, shopManagePageSize = 15; - var shopManageGroups = []; - - function buildShopManageQuery(page) { - var query = 'page=' + (page || 1) + '&page_size=' + shopManagePageSize; - var groupId = (document.getElementById('shopManageFilterGroupId').value || '').trim(); - var shopName = (document.getElementById('shopManageFilterShopName').value || '').trim(); - if (groupId) query += '&group_id=' + encodeURIComponent(groupId); - if (shopName) query += '&shop_name=' + encodeURIComponent(shopName); - return query; - } - - function refreshShopGroupSelects(selectedCreateId, selectedEditId) { - var createSel = document.getElementById('shopManageGroupSelect'); - var editSel = document.getElementById('editShopManageGroupSelect'); - var filterSel = document.getElementById('shopManageFilterGroupId'); - var skipCreateSel = document.getElementById('skipPriceAsinGroupSelect'); - var skipFilterSel = document.getElementById('skipPriceAsinFilterGroupId'); - var chooseSkipShopSel = document.getElementById('chooseSkipPriceAsinShopGroupId'); - var selectedFilterId = filterSel ? filterSel.value : ''; - var selectedSkipCreateId = skipCreateSel ? skipCreateSel.value : ''; - var selectedSkipFilterId = skipFilterSel ? skipFilterSel.value : ''; - var selectedChooseSkipShopId = chooseSkipShopSel ? chooseSkipShopSel.value : ''; - var createOpts = ['']; - var filterOpts = ['']; - shopManageGroups.forEach(function (g) { - var option = ''; - createOpts.push(option); - filterOpts.push(option); - }); - createSel.innerHTML = createOpts.join(''); - editSel.innerHTML = createOpts.join(''); - if (skipCreateSel) skipCreateSel.innerHTML = createOpts.join(''); - if (filterSel) filterSel.innerHTML = filterOpts.join(''); - if (skipFilterSel) skipFilterSel.innerHTML = filterOpts.join(''); - if (chooseSkipShopSel) chooseSkipShopSel.innerHTML = filterOpts.join(''); - if (selectedCreateId != null) createSel.value = String(selectedCreateId); - if (selectedEditId != null) editSel.value = String(selectedEditId); - if (filterSel && selectedFilterId) filterSel.value = selectedFilterId; - if (skipCreateSel && selectedSkipCreateId) skipCreateSel.value = selectedSkipCreateId; - if (skipFilterSel && selectedSkipFilterId) skipFilterSel.value = selectedSkipFilterId; - if (chooseSkipShopSel && selectedChooseSkipShopId) chooseSkipShopSel.value = selectedChooseSkipShopId; - } - - function loadShopManageGroups(selectedCreateId, selectedEditId) { - return fetch('/api/admin/shop-manage-groups') - .then(function (r) { return r.json(); }) - .then(function (res) { - if (!res.success) throw new Error(res.error || '鍔犺浇鍒嗙粍澶辫触'); - shopManageGroups = res.items || []; - refreshShopGroupSelects(selectedCreateId, selectedEditId); - return shopManageGroups; - }) - .catch(function () { - shopManageGroups = []; - refreshShopGroupSelects(); - return []; - }); - } - - function openShopManageGroupModal() { - document.getElementById('shopManageGroupEditId').value = ''; - document.getElementById('shopManageGroupInput').value = ''; - document.getElementById('msgShopManageGroup').textContent = ''; - document.getElementById('msgShopManageGroup').className = 'msg'; - loadShopManageGroups().then(function () { - renderShopManageGroupRows(); - document.getElementById('shopManageGroupModal').classList.add('show'); - }); - } - - function renderShopManageGroupRows() { - var tbody = document.getElementById('shopManageGroupListBody'); - if (!shopManageGroups.length) { - tbody.innerHTML = '鏆傛棤鍒嗙粍'; - return; - } - tbody.innerHTML = shopManageGroups.map(function (item, index) { - return '' + (index + 1) + '' + (item.group_name || '') + '' + (item.created_at || '') + '' + (item.updated_at || '') + '' + - ' ' + - '' + - ''; - }).join(''); - - document.querySelectorAll('[data-shop-group-edit]').forEach(function (btn) { - btn.onclick = function () { - document.getElementById('shopManageGroupEditId').value = btn.dataset.shopGroupEdit; - document.getElementById('shopManageGroupInput').value = (btn.dataset.shopGroupName || '').replace(/"/g, '"'); - }; - }); - - document.querySelectorAll('[data-shop-group-delete]').forEach(function (btn) { - btn.onclick = function () { - var gid = btn.dataset.shopGroupDelete; - var gname = (btn.dataset.shopGroupName || '').replace(/"/g, '"'); - if (!confirm('纭畾鍒犻櫎鍒嗙粍鈥? + gname + '鈥濆悧锛?)) return; - fetch('/api/admin/shop-manage-group/' + gid, { method: 'DELETE' }) - .then(function (r) { return r.json(); }) - .then(function (res) { - if (!res.success) { - alert(res.error || '鍒犻櫎澶辫触'); - return; - } - loadShopManageGroups().then(function () { - renderShopManageGroupRows(); - loadShopManage(shopManagePage); - loadSkipPriceAsin(skipPriceAsinPage); - }); - }); - }; - }); - } - - function loadShopManage(page) { - shopManagePage = page || 1; - fetch('/api/admin/shop-manages?' + buildShopManageQuery(shopManagePage)) - .then(function (r) { return r.json(); }) - .then(function (res) { - var tbody = document.getElementById('shopManageListBody'); - if (!res.success) { - tbody.innerHTML = '鍔犺浇澶辫触: ' + (res.error || '') + ''; - return; - } - var items = res.items || []; - if (items.length === 0) { - tbody.innerHTML = '鏆傛棤搴楅摵'; - } else { - tbody.innerHTML = items.map(function (item, index) { - var rowNo = (shopManagePage - 1) * shopManagePageSize + index + 1; - return '' + rowNo + '' + (item.group_name || '') + '' + (item.shop_name || '') + '' + (item.mall_name || '') + '' + (item.account || '') + '' + (item.password || '') + '' + (item.created_at || '') + '' + (item.updated_at || '') + '' + - ' ' + - '' + - ''; - }).join(''); - } - renderPagination('shopManagePagination', res.total, res.page, res.page_size, loadShopManage); - bindShopManageActions(); - }) - .catch(function () { - document.getElementById('shopManageListBody').innerHTML = '璇锋眰澶辫触'; - }); - } - - function bindShopManageActions() { - document.querySelectorAll('[data-shop-manage-edit]').forEach(function (btn) { - btn.onclick = function () { - var item = {}; - try { item = JSON.parse((btn.dataset.shopManage || '').replace(/"/g, '"')); } catch (e) { item = {}; } - document.getElementById('editShopManageId').value = item.id || ''; - document.getElementById('editShopManageShopName').value = item.shop_name || ''; - document.getElementById('editShopManageMallName').value = item.mall_name || ''; - document.getElementById('editShopManageAccount').value = item.account || ''; - document.getElementById('editShopManagePassword').value = item.password || ''; - document.getElementById('msgEditShopManage').textContent = ''; - document.getElementById('msgEditShopManage').className = 'msg'; - loadShopManageGroups(null, item.group_id).then(function () { - document.getElementById('editShopManageModal').classList.add('show'); - }); - }; - }); - document.querySelectorAll('[data-shop-manage-delete]').forEach(function (btn) { - btn.onclick = function () { - var name = (btn.dataset.shopManageName || '').replace(/"/g, '"'); - if (!confirm('纭畾鍒犻櫎搴楅摵鈥? + name + '鈥濆悧锛?)) return; - fetch('/api/admin/shop-manage/' + btn.dataset.shopManageDelete, { method: 'DELETE' }) - .then(function (r) { return r.json(); }) - .then(function (res) { - if (res.success) { loadShopManage(shopManagePage); } - else { alert(res.error || '鍒犻櫎澶辫触'); } - }); - }; - }); - } - - document.getElementById('btnManageShopGroups').onclick = openShopManageGroupModal; - document.getElementById('btnManageShopGroupsFromEdit').onclick = openShopManageGroupModal; - document.getElementById('btnSearchShopManage').onclick = function () { - loadShopManage(1); - }; - document.getElementById('shopManageFilterShopName').addEventListener('keydown', function (e) { - if (e.key === 'Enter') { - e.preventDefault(); - loadShopManage(1); - } - }); - document.getElementById('btnCloseShopManageGroupModal').onclick = function () { - document.getElementById('shopManageGroupModal').classList.remove('show'); - }; - document.getElementById('btnCancelShopManageGroupEdit').onclick = function () { - document.getElementById('shopManageGroupEditId').value = ''; - document.getElementById('shopManageGroupInput').value = ''; - }; - document.getElementById('btnSaveShopManageGroup').onclick = function () { - var editId = (document.getElementById('shopManageGroupEditId').value || '').trim(); - var groupName = (document.getElementById('shopManageGroupInput').value || '').trim(); - var msgEl = document.getElementById('msgShopManageGroup'); - msgEl.textContent = ''; - msgEl.className = 'msg'; - if (!groupName) { - msgEl.textContent = '璇疯緭鍏ュ垎缁勫悕绉?; - msgEl.className = 'msg err'; - return; - } - var method = editId ? 'PUT' : 'POST'; - var url = editId ? ('/api/admin/shop-manage-group/' + editId) : '/api/admin/shop-manage-group'; - fetch(url, { - method: method, - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ group_name: groupName }) - }) - .then(function (r) { return r.json(); }) - .then(function (res) { - if (!res.success) { - msgEl.textContent = res.error || '淇濆瓨澶辫触'; - msgEl.className = 'msg err'; - return; - } - document.getElementById('shopManageGroupEditId').value = ''; - document.getElementById('shopManageGroupInput').value = ''; - msgEl.textContent = res.msg || '淇濆瓨鎴愬姛'; - msgEl.className = 'msg ok'; - loadShopManageGroups().then(function () { - renderShopManageGroupRows(); - loadShopManage(shopManagePage); - loadSkipPriceAsin(skipPriceAsinPage); - }); - }) - .catch(function () { - msgEl.textContent = '璇锋眰澶辫触'; - msgEl.className = 'msg err'; - }); - }; - - function refreshShopGroupSelects(selectedCreateId, selectedEditId) { - var createSel = document.getElementById('shopManageGroupSelect'); - var editSel = document.getElementById('editShopManageGroupSelect'); - var filterSel = document.getElementById('shopManageFilterGroupId'); - var skipCreateSel = document.getElementById('skipPriceAsinGroupSelect'); - var skipFilterSel = document.getElementById('skipPriceAsinFilterGroupId'); - var chooseSkipShopSel = document.getElementById('chooseSkipPriceAsinShopGroupId'); - var selectedFilterId = filterSel ? filterSel.value : ''; - var selectedSkipCreateId = skipCreateSel ? skipCreateSel.value : ''; - var selectedSkipFilterId = skipFilterSel ? skipFilterSel.value : ''; - var selectedChooseSkipShopId = chooseSkipShopSel ? chooseSkipShopSel.value : ''; - var createOpts = ['']; - var filterOpts = ['']; - shopManageGroups.forEach(function (g) { - var option = ''; - createOpts.push(option); - filterOpts.push(option); - }); - createSel.innerHTML = createOpts.join(''); - editSel.innerHTML = createOpts.join(''); - if (skipCreateSel) skipCreateSel.innerHTML = createOpts.join(''); - if (filterSel) filterSel.innerHTML = filterOpts.join(''); - if (skipFilterSel) skipFilterSel.innerHTML = filterOpts.join(''); - if (chooseSkipShopSel) chooseSkipShopSel.innerHTML = filterOpts.join(''); - if (selectedCreateId != null) createSel.value = String(selectedCreateId); - if (selectedEditId != null) editSel.value = String(selectedEditId); - if (filterSel && selectedFilterId) filterSel.value = selectedFilterId; - if (skipCreateSel && selectedSkipCreateId) skipCreateSel.value = selectedSkipCreateId; - if (skipFilterSel && selectedSkipFilterId) skipFilterSel.value = selectedSkipFilterId; - if (chooseSkipShopSel && selectedChooseSkipShopId) chooseSkipShopSel.value = selectedChooseSkipShopId; - } - - function loadShopManageGroups(selectedCreateId, selectedEditId) { - return fetch('/api/admin/shop-manage-groups') - .then(function (r) { return r.json(); }) - .then(function (res) { - if (!res.success) throw new Error(res.error || '鍔犺浇鍒嗙粍澶辫触'); - shopManageGroups = res.items || []; - refreshShopGroupSelects(selectedCreateId, selectedEditId); - return shopManageGroups; - }) - .catch(function () { - shopManageGroups = []; - refreshShopGroupSelects(); - return []; - }); - } - - function resetShopManageGroupForm() { - document.getElementById('shopManageGroupEditId').value = ''; - document.getElementById('shopManageGroupInput').value = ''; - document.getElementById('msgShopManageGroup').textContent = ''; - document.getElementById('msgShopManageGroup').className = 'msg'; - refreshShopManageGroupUserSelect(''); - } - - function openShopManageGroupModal() { - resetShopManageGroupForm(); - loadUserOptions() - .then(function () { return loadShopManageGroups(); }) - .then(function () { - renderShopManageGroupRows(); - document.getElementById('shopManageGroupModal').classList.add('show'); - }); - } - - function renderShopManageGroupRows() { - var tbody = document.getElementById('shopManageGroupListBody'); - if (!shopManageGroups.length) { - tbody.innerHTML = '鏆傛棤鍒嗙粍'; - return; - } - tbody.innerHTML = shopManageGroups.map(function (item, index) { - return '' + (index + 1) + '' + (item.group_name || '') + '' + (item.username || '') + '' + (item.created_at || '') + '' + (item.updated_at || '') + '' + - ' ' + - '' + - ''; - }).join(''); - - document.querySelectorAll('[data-shop-group-edit]').forEach(function (btn) { - btn.onclick = function () { - document.getElementById('shopManageGroupEditId').value = btn.dataset.shopGroupEdit; - refreshShopManageGroupUserSelect(btn.dataset.shopGroupUserId || ''); - }; - }); - - document.querySelectorAll('[data-shop-group-delete]').forEach(function (btn) { - btn.onclick = function () { - var gid = btn.dataset.shopGroupDelete; - var gname = (btn.dataset.shopGroupName || '').replace(/"/g, '"'); - if (!confirm('纭畾鍒犻櫎鍒嗙粍鈥? + gname + '鈥濆悧锛?)) return; - fetch('/api/admin/shop-manage-group/' + gid, { method: 'DELETE' }) - .then(function (r) { return r.json(); }) - .then(function (res) { - if (!res.success) { - alert(res.error || '鍒犻櫎澶辫触'); - return; - } - loadShopManageGroups().then(function () { - renderShopManageGroupRows(); - loadShopManage(shopManagePage); - loadSkipPriceAsin(skipPriceAsinPage); - }); - }); - }; - }); - } - - document.getElementById('shopManageGroupUserSelect').onchange = syncShopManageGroupNameWithUser; - document.getElementById('btnManageShopGroups').onclick = openShopManageGroupModal; - document.getElementById('btnManageShopGroupsFromEdit').onclick = openShopManageGroupModal; - document.getElementById('btnManageSkipPriceAsinGroups').onclick = openShopManageGroupModal; - document.getElementById('btnCloseShopManageGroupModal').onclick = function () { - document.getElementById('shopManageGroupModal').classList.remove('show'); - }; - document.getElementById('btnCancelShopManageGroupEdit').onclick = resetShopManageGroupForm; - document.getElementById('btnSaveShopManageGroup').onclick = function () { - var editId = (document.getElementById('shopManageGroupEditId').value || '').trim(); - var groupName = (document.getElementById('shopManageGroupInput').value || '').trim(); - var userId = (document.getElementById('shopManageGroupUserSelect').value || '').trim(); - var msgEl = document.getElementById('msgShopManageGroup'); - msgEl.textContent = ''; - msgEl.className = 'msg'; - if (!userId || !groupName) { - msgEl.textContent = '璇烽€夋嫨鍏宠仈鐢ㄦ埛'; - msgEl.className = 'msg err'; - return; - } - var method = editId ? 'PUT' : 'POST'; - var url = editId ? ('/api/admin/shop-manage-group/' + editId) : '/api/admin/shop-manage-group'; - fetch(url, { - method: method, - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ group_name: groupName, user_id: Number(userId) }) - }) - .then(function (r) { return r.json(); }) - .then(function (res) { - if (!res.success) { - msgEl.textContent = res.error || '淇濆瓨澶辫触'; - msgEl.className = 'msg err'; - return; - } - resetShopManageGroupForm(); - msgEl.textContent = res.msg || '淇濆瓨鎴愬姛'; - msgEl.className = 'msg ok'; - loadShopManageGroups().then(function () { - renderShopManageGroupRows(); - loadShopManage(shopManagePage); - loadSkipPriceAsin(skipPriceAsinPage); - }); - }) - .catch(function () { - msgEl.textContent = '璇锋眰澶辫触'; - msgEl.className = 'msg err'; - }); - }; - - document.getElementById('btnCreateShopManage').onclick = function () { - var groupId = (document.getElementById('shopManageGroupSelect').value || '').trim(); - var shopName = (document.getElementById('shopManageShopName').value || '').trim(); - var mallName = (document.getElementById('shopManageMallName').value || '').trim(); - var account = (document.getElementById('shopManageAccount').value || '').trim(); - var password = (document.getElementById('shopManagePassword').value || '').trim(); - var msgEl = document.getElementById('msgShopManage'); - msgEl.textContent = ''; - msgEl.className = 'msg'; - if (!groupId || !shopName || !mallName || !account || !password) { - msgEl.textContent = '璇峰畬鏁村~鍐欏垎缁勩€佸簵閾哄悕銆佸簵閾哄晢鍩庡悕銆佽处鍙枫€佸瘑鐮?; - msgEl.classList.add('err'); - return; - } - fetch('/api/admin/shop-manage', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ group_id: Number(groupId), shop_name: shopName, mall_name: mallName, account: account, password: password }) - }) - .then(function (r) { return r.json(); }) - .then(function (res) { - if (res.success) { - document.getElementById('shopManageGroupSelect').value = ''; - document.getElementById('shopManageShopName').value = ''; - document.getElementById('shopManageMallName').value = ''; - document.getElementById('shopManageAccount').value = ''; - document.getElementById('shopManagePassword').value = ''; - msgEl.textContent = res.msg || '鍒涘缓鎴愬姛'; - msgEl.className = 'msg ok'; - loadShopManage(1); - } else { - msgEl.textContent = res.error || '鍒涘缓澶辫触'; - msgEl.className = 'msg err'; - } - }) - .catch(function () { - msgEl.textContent = '璇锋眰澶辫触'; - msgEl.className = 'msg err'; - }); - }; - - document.getElementById('btnSaveShopManage').onclick = function () { - var itemId = document.getElementById('editShopManageId').value; - var groupId = (document.getElementById('editShopManageGroupSelect').value || '').trim(); - var shopName = (document.getElementById('editShopManageShopName').value || '').trim(); - var mallName = (document.getElementById('editShopManageMallName').value || '').trim(); - var account = (document.getElementById('editShopManageAccount').value || '').trim(); - var password = (document.getElementById('editShopManagePassword').value || '').trim(); - var msgEl = document.getElementById('msgEditShopManage'); - msgEl.textContent = ''; - msgEl.className = 'msg'; - if (!groupId || !shopName || !mallName || !account || !password) { - msgEl.textContent = '璇峰畬鏁村~鍐欏垎缁勩€佸簵閾哄悕銆佸簵閾哄晢鍩庡悕銆佽处鍙枫€佸瘑鐮?; - msgEl.classList.add('err'); - return; - } - fetch('/api/admin/shop-manage/' + itemId, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ group_id: Number(groupId), shop_name: shopName, mall_name: mallName, account: account, password: password }) - }) - .then(function (r) { return r.json(); }) - .then(function (res) { - if (res.success) { - document.getElementById('editShopManageModal').classList.remove('show'); - loadShopManage(shopManagePage); - } else { - msgEl.textContent = res.error || '淇濆瓨澶辫触'; - msgEl.classList.add('err'); - } - }) - .catch(function () { - msgEl.textContent = '璇锋眰澶辫触'; - msgEl.classList.add('err'); - }); - }; - document.getElementById('btnCloseEditShopManage').onclick = function () { - document.getElementById('editShopManageModal').classList.remove('show'); - }; - - // ========== 鐗堟湰绠$悊 ========== - // ========== 璺宠繃璺熶环 ASIN ========== - var skipPriceAsinPage = 1, skipPriceAsinPageSize = 15; - var chooseSkipPriceAsinShopPage = 1, chooseSkipPriceAsinShopPageSize = 10; - var skipPriceCountryColumns = [ - { code: 'DE', field: 'asin_de', label: '寰峰浗' }, - { code: 'UK', field: 'asin_uk', label: '鑻卞浗' }, - { code: 'FR', field: 'asin_fr', label: '娉曞浗' }, - { code: 'IT', field: 'asin_it', label: '鎰忓ぇ鍒? }, - { code: 'ES', field: 'asin_es', label: '瑗跨彮鐗? } - ]; - function buildChooseSkipPriceAsinShopQuery(page) { - var query = 'page=' + (page || 1) + '&page_size=' + chooseSkipPriceAsinShopPageSize; - var groupId = (document.getElementById('chooseSkipPriceAsinShopGroupId').value || '').trim(); - var shopName = (document.getElementById('chooseSkipPriceAsinShopKeyword').value || '').trim(); - if (groupId) query += '&group_id=' + encodeURIComponent(groupId); - if (shopName) query += '&shop_name=' + encodeURIComponent(shopName); - return query; - } - function loadChooseSkipPriceAsinShops(page) { - chooseSkipPriceAsinShopPage = page || 1; - fetch('/api/admin/shop-manages?' + buildChooseSkipPriceAsinShopQuery(chooseSkipPriceAsinShopPage)) - .then(function (r) { return r.json(); }) - .then(function (res) { - var tbody = document.getElementById('chooseSkipPriceAsinShopListBody'); - if (!res.success) { - tbody.innerHTML = '鍔犺浇澶辫触: ' + (res.error || '') + ''; - return; - } - var items = res.items || []; - if (!items.length) { - tbody.innerHTML = '鏆傛棤搴楅摵'; - } else { - tbody.innerHTML = items.map(function (item, index) { - var rowNo = (chooseSkipPriceAsinShopPage - 1) * chooseSkipPriceAsinShopPageSize + index + 1; - return '' + rowNo + '' + (item.group_name || '') + '' + (item.shop_name || '') + '' + (item.mall_name || '') + '' + (item.account || '') + '' + - '' + - ''; - }).join(''); - } - renderPagination('chooseSkipPriceAsinShopPagination', res.total, res.page, res.page_size, loadChooseSkipPriceAsinShops); - bindChooseSkipPriceAsinShopActions(); - }) - .catch(function () { - document.getElementById('chooseSkipPriceAsinShopListBody').innerHTML = '璇锋眰澶辫触'; - }); - } - function bindChooseSkipPriceAsinShopActions() { - document.querySelectorAll('[data-choose-skip-price-asin-shop]').forEach(function (btn) { - btn.onclick = function () { - document.getElementById('skipPriceAsinShopName').value = (btn.dataset.shopName || '').replace(/"/g, '"'); - if (!document.getElementById('skipPriceAsinGroupSelect').value && btn.dataset.groupId) { - document.getElementById('skipPriceAsinGroupSelect').value = btn.dataset.groupId; - } - document.getElementById('chooseSkipPriceAsinShopModal').classList.remove('show'); - }; - }); - } - function openChooseSkipPriceAsinShopModal() { - var currentGroupId = (document.getElementById('skipPriceAsinGroupSelect').value || '').trim(); - if (currentGroupId) { - document.getElementById('chooseSkipPriceAsinShopGroupId').value = currentGroupId; - } - document.getElementById('chooseSkipPriceAsinShopKeyword').value = (document.getElementById('skipPriceAsinShopName').value || '').trim(); - document.getElementById('chooseSkipPriceAsinShopModal').classList.add('show'); - loadChooseSkipPriceAsinShops(1); - } - function setupSkipPriceAsinShopPicker() { - var input = document.getElementById('skipPriceAsinShopName'); - var button = document.getElementById('btnChooseSkipPriceAsinShop'); - if (!input || !button) return; - var inputParent = input.parentNode; - if (inputParent && inputParent.style && inputParent.style.display === 'flex') return; - var legacyWrap = button.parentNode; - var wrapper = document.createElement('div'); - wrapper.style.display = 'flex'; - wrapper.style.gap = '8px'; - wrapper.style.alignItems = 'center'; - input.parentNode.insertBefore(wrapper, input); - wrapper.appendChild(input); - wrapper.appendChild(button); - button.style.whiteSpace = 'nowrap'; - button.style.marginTop = '0'; - if (legacyWrap && legacyWrap !== wrapper) { - legacyWrap.style.display = 'none'; - } - } - function getSelectedSkipPriceCountries() { - return Array.from(document.getElementById('skipPriceAsinCountries').selectedOptions).map(function (option) { - return option.value; - }); - } - function renderSkipPriceAsinInputs() { - var container = document.getElementById('skipPriceAsinInputs'); - var selectedCountries = getSelectedSkipPriceCountries(); - var existingValues = {}; - container.querySelectorAll('[data-skip-price-country-input]').forEach(function (input) { - existingValues[input.getAttribute('data-skip-price-country-input')] = input.value; - }); - if (!selectedCountries.length) { - container.innerHTML = '
璇烽€夋嫨鍥藉鍚庤緭鍏?ASIN
'; - return; - } - container.innerHTML = selectedCountries.map(function (countryCode) { - var country = skipPriceCountryColumns.find(function (item) { return item.code === countryCode; }); - var label = country ? country.label : countryCode; - var value = existingValues[countryCode] || ''; - return '
' + - '' + label + '' + - '' + - '
'; - }).join(''); - } - function collectSkipPriceAsinMappings(countries) { - var mappings = {}; - for (var i = 0; i < countries.length; i++) { - var countryCode = countries[i]; - var input = document.querySelector('[data-skip-price-country-input="' + countryCode + '"]'); - var asin = input ? (input.value || '').trim().toUpperCase() : ''; - if (!asin) { - var country = skipPriceCountryColumns.find(function (item) { return item.code === countryCode; }); - var label = country ? country.label : countryCode; - throw new Error(label + ' ASIN 涓嶈兘涓虹┖'); - } - mappings[countryCode] = asin; - } - return mappings; - } - function buildSkipPriceAsinQuery(page) { - var query = 'page=' + (page || 1) + '&page_size=' + skipPriceAsinPageSize; - var groupId = (document.getElementById('skipPriceAsinFilterGroupId').value || '').trim(); - var shopName = (document.getElementById('skipPriceAsinFilterShopName').value || '').trim(); - var asin = (document.getElementById('skipPriceAsinFilterAsin').value || '').trim(); - if (groupId) query += '&group_id=' + encodeURIComponent(groupId); - if (shopName) query += '&shop_name=' + encodeURIComponent(shopName); - if (asin) query += '&asin=' + encodeURIComponent(asin); - return query; - } - function renderSkipPriceAsinCell(item, country) { - var value = item[country.field] || ''; - if (!value) return '-'; - return '
' + - '' + value + '' + - '' + - '' + - '
'; - } - function bindSkipPriceAsinActions() { - document.querySelectorAll('[data-skip-price-asin-edit]').forEach(function (btn) { - btn.onclick = function () { - var shopName = (btn.dataset.shopName || '').replace(/"/g, '"'); - var countryCode = btn.dataset.country || ''; - var currentAsin = (btn.dataset.asin || '').replace(/"/g, '"'); - var nextAsin = prompt('璇疯緭鍏ュ簵閾衡€? + shopName + '鈥濆湪 ' + countryCode + ' 鐨?ASIN', currentAsin); - if (nextAsin === null) return; - nextAsin = (nextAsin || '').trim(); - if (!nextAsin) { - alert('ASIN 涓嶈兘涓虹┖'); - return; - } - fetch('/api/admin/skip-price-asin/' + btn.dataset.skipPriceAsinEdit + '/country/' + countryCode, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ asin: nextAsin }) - }) - .then(function (r) { return r.json(); }) - .then(function (res) { - if (res.success) loadSkipPriceAsin(skipPriceAsinPage); - else alert(res.error || '淇濆瓨澶辫触'); - }); - }; - }); - document.querySelectorAll('[data-skip-price-asin-delete]').forEach(function (btn) { - btn.onclick = function () { - var shopName = (btn.dataset.shopName || '').replace(/"/g, '"'); - var countryCode = btn.dataset.country || ''; - if (!confirm('纭畾鍒犻櫎搴楅摵鈥? + shopName + '鈥濆湪 ' + countryCode + ' 鐨?ASIN 鍚楋紵')) return; - fetch('/api/admin/skip-price-asin/' + btn.dataset.skipPriceAsinDelete + '/country/' + countryCode, { - method: 'DELETE' - }) - .then(function (r) { return r.json(); }) - .then(function (res) { - if (res.success) loadSkipPriceAsin(skipPriceAsinPage); - else alert(res.error || '鍒犻櫎澶辫触'); - }); - }; - }); - } - function loadSkipPriceAsin(page) { - skipPriceAsinPage = page || 1; - fetch('/api/admin/skip-price-asins?' + buildSkipPriceAsinQuery(skipPriceAsinPage)) - .then(function (r) { return r.json(); }) - .then(function (res) { - var tbody = document.getElementById('skipPriceAsinListBody'); - if (!res.success) { - tbody.innerHTML = '鍔犺浇澶辫触: ' + (res.error || '') + ''; - return; - } - var items = res.items || []; - if (items.length === 0) { - tbody.innerHTML = '鏆傛棤鏁版嵁'; - } else { - tbody.innerHTML = items.map(function (item, index) { - var rowNo = (skipPriceAsinPage - 1) * skipPriceAsinPageSize + index + 1; - return '' + rowNo + '' + (item.group_name || '') + '' + (item.shop_name || '') + '' + - skipPriceCountryColumns.map(function (country) { - return '' + renderSkipPriceAsinCell(item, country) + ''; - }).join('') + - ''; - }).join(''); - } - renderPagination('skipPriceAsinPagination', res.total, res.page, res.page_size, loadSkipPriceAsin); - bindSkipPriceAsinActions(); - }) - .catch(function () { - document.getElementById('skipPriceAsinListBody').innerHTML = '璇锋眰澶辫触'; - }); - } - document.getElementById('btnManageSkipPriceAsinGroups').onclick = openShopManageGroupModal; - document.getElementById('btnChooseSkipPriceAsinShop').onclick = openChooseSkipPriceAsinShopModal; - document.getElementById('btnSearchChooseSkipPriceAsinShop').onclick = function () { - loadChooseSkipPriceAsinShops(1); - }; - document.getElementById('chooseSkipPriceAsinShopGroupId').onchange = function () { - loadChooseSkipPriceAsinShops(1); - }; - document.getElementById('chooseSkipPriceAsinShopKeyword').addEventListener('keydown', function (e) { - if (e.key === 'Enter') { - e.preventDefault(); - loadChooseSkipPriceAsinShops(1); - } - }); - document.getElementById('btnCloseChooseSkipPriceAsinShopModal').onclick = function () { - document.getElementById('chooseSkipPriceAsinShopModal').classList.remove('show'); - }; - document.getElementById('skipPriceAsinCountries').addEventListener('change', renderSkipPriceAsinInputs); - document.getElementById('btnSearchSkipPriceAsin').onclick = function () { - loadSkipPriceAsin(1); - }; - document.getElementById('skipPriceAsinFilterShopName').addEventListener('keydown', function (e) { - if (e.key === 'Enter') { - e.preventDefault(); - loadSkipPriceAsin(1); - } - }); - document.getElementById('skipPriceAsinFilterAsin').addEventListener('keydown', function (e) { - if (e.key === 'Enter') { - e.preventDefault(); - loadSkipPriceAsin(1); - } - }); - document.getElementById('btnCreateSkipPriceAsin').onclick = function () { - var groupId = (document.getElementById('skipPriceAsinGroupSelect').value || '').trim(); - var shopName = (document.getElementById('skipPriceAsinShopName').value || '').trim(); - var countries = getSelectedSkipPriceCountries(); - var msgEl = document.getElementById('msgSkipPriceAsin'); - msgEl.textContent = ''; - msgEl.className = 'msg'; - if (!groupId || !shopName || !countries.length) { - msgEl.textContent = '璇峰畬鏁村~鍐欏垎缁勩€佸簵閾哄悕銆佸浗瀹跺拰 ASIN'; - msgEl.className = 'msg err'; - return; - } - var asinMappings = {}; - try { - asinMappings = collectSkipPriceAsinMappings(countries); - } catch (err) { - msgEl.textContent = err.message || '璇疯緭鍏?ASIN'; - msgEl.className = 'msg err'; - return; - } - var fallbackAsin = ''; - Object.keys(asinMappings).some(function (countryCode) { - fallbackAsin = asinMappings[countryCode] || ''; - return !!fallbackAsin; - }); - fetch('/api/admin/skip-price-asin', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - group_id: Number(groupId), - shop_name: shopName, - countries: countries, - asin: fallbackAsin, - asin_mappings: asinMappings - }) - }) - .then(function (r) { return r.json(); }) - .then(function (res) { - if (!res.success) { - msgEl.textContent = res.error || '淇濆瓨澶辫触'; - msgEl.className = 'msg err'; - return; - } - document.getElementById('skipPriceAsinGroupSelect').value = ''; - document.getElementById('skipPriceAsinShopName').value = ''; - Array.from(document.getElementById('skipPriceAsinCountries').options).forEach(function (option) { - option.selected = false; - }); - renderSkipPriceAsinInputs(); - msgEl.textContent = res.msg || '淇濆瓨鎴愬姛'; - msgEl.className = 'msg ok'; - loadSkipPriceAsin(1); - }) - .catch(function () { - msgEl.textContent = '璇锋眰澶辫触'; - msgEl.className = 'msg err'; - }); - }; - setupSkipPriceAsinShopPicker(); - renderSkipPriceAsinInputs(); - function loadVersions() { - fetch('/api/admin/versions') - .then(function (r) { return r.json(); }) - .then(function (res) { - var tbody = document.getElementById('versionListBody'); - if (!res.success) { - tbody.innerHTML = '鍔犺浇澶辫触: ' + (res.error || '') + ''; - return; - } - var items = res.items || []; - if (items.length === 0) { - tbody.innerHTML = '鏆傛棤鐗堟湰璁板綍'; - } else { - tbody.innerHTML = items.map(function (v) { - var url = (v.file_url || '').replace(/"/g, '"'); - return '' + (v.version || '') + '' + url + '' + (v.created_at || '') + '涓嬭浇'; - }).join(''); - } - }) - .catch(function () { - document.getElementById('versionListBody').innerHTML = '璇锋眰澶辫触'; - }); - } - document.getElementById('btnUploadVersion').onclick = function () { - var version = (document.getElementById('versionNumber').value || '').trim(); - var fileInput = document.getElementById('versionZip'); - var msgEl = document.getElementById('msgVersion'); - msgEl.textContent = ''; - msgEl.className = 'msg'; - if (!version) { - msgEl.textContent = '璇峰~鍐欑増鏈彿'; - msgEl.classList.add('err'); - return; - } - if (!fileInput.files || fileInput.files.length === 0) { - msgEl.textContent = '璇烽€夋嫨 zip 鍘嬬缉鍖?; - msgEl.classList.add('err'); - return; - } - var file = fileInput.files[0]; - if (!(file.name || '').toLowerCase().endsWith('.zip')) { - msgEl.textContent = '浠呮敮鎸?.zip 鏍煎紡'; - msgEl.classList.add('err'); - return; - } - var formData = new FormData(); - formData.append('version', version); - formData.append('file', file); - msgEl.textContent = '涓婁紶涓?..'; - msgEl.classList.remove('err', 'ok'); - fetch('/api/admin/version', { - method: 'POST', - body: formData - }) - .then(function (r) { return r.json(); }) - .then(function (res) { - if (res.success) { - msgEl.textContent = '鍙戝竷鎴愬姛銆傜増鏈細' + res.version + '锛岄摼鎺ワ細' + (res.file_url || ''); - msgEl.classList.add('ok'); - document.getElementById('versionNumber').value = ''; - fileInput.value = ''; - loadVersions(); - } else { - msgEl.textContent = res.error || '涓婁紶澶辫触'; - msgEl.classList.add('err'); - } - }) - .catch(function () { - msgEl.textContent = '璇锋眰澶辫触'; - msgEl.classList.add('err'); - }); - }; - - // ========== 鏍忕洰鏉冮檺閰嶇疆 ========== - function loadColumns() { - fetch('/api/admin/columns') - .then(function (r) { return r.json(); }) - .then(function (res) { - var tbody = document.getElementById('columnListBody'); - if (!res.success) { - tbody.innerHTML = '鍔犺浇澶辫触: ' + (res.error || '') + ''; - return; - } - allColumnsList = res.items || []; - if (allColumnsList.length === 0) { - tbody.innerHTML = '鏆傛棤鑿滃崟锛岃鍦ㄤ笂鏂规柊澧?/td>'; - } else { - tbody.innerHTML = allColumnsList.map(function (c) { - return '' + c.id + '' + (c.name || '') + '' + (c.column_key || '') + '' + (c.created_at || '') + '' + - ' ' + - ''; - }).join(''); - } - bindColumnActions(); - }) - .catch(function () { - document.getElementById('columnListBody').innerHTML = '璇锋眰澶辫触'; - }); - } - function bindColumnActions() { - document.querySelectorAll('[data-column-edit]').forEach(function (btn) { - btn.onclick = function () { - document.getElementById('editColumnId').value = btn.dataset.columnEdit || ''; - document.getElementById('editColumnName').value = (btn.dataset.name || '').replace(/"/g, '"'); - document.getElementById('editColumnKey').value = (btn.dataset.key || '').replace(/"/g, '"'); - document.getElementById('msgEditColumn').textContent = ''; - document.getElementById('editColumnModal').classList.add('show'); - }; - }); - document.querySelectorAll('[data-column-delete]').forEach(function (btn) { - btn.onclick = function () { - if (!confirm('纭畾鍒犻櫎鑿滃崟鈥? + (btn.dataset.name || '').replace(/"/g, '"') + '鈥濆悧锛?)) return; - fetch('/api/admin/column/' + btn.dataset.columnDelete, { method: 'DELETE' }) - .then(function (r) { return r.json(); }) - .then(function (res) { - if (res.success) { loadColumns(); loadColumnsForPermission(); loadAdminMenus(getActiveAdminTabName()); } - else { alert(res.error || '鍒犻櫎澶辫触'); } - }); - }; - }); - } - document.getElementById('btnAddColumn').onclick = function () { - var name = (document.getElementById('columnName').value || '').trim(); - var key = (document.getElementById('columnKey').value || '').trim(); - var msgEl = document.getElementById('msgColumn'); - msgEl.textContent = ''; - msgEl.className = 'msg'; - if (!name) { msgEl.textContent = '璇峰~鍐欒彍鍗曞悕绉?; msgEl.classList.add('err'); return; } - if (!key) { - msgEl.textContent = '璇峰~鍐欐爮鐩爣璇?; msgEl.classList.add('err'); return; - } - fetch('/api/admin/column', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name: name, column_key: key }) - }) - .then(function (r) { return r.json(); }) - .then(function (res) { - if (res.success) { - msgEl.textContent = res.msg || '鏂板鎴愬姛'; - msgEl.classList.add('ok'); - document.getElementById('columnName').value = ''; - document.getElementById('columnKey').value = ''; - loadColumns(); - loadColumnsForPermission(); - loadAdminMenus(getActiveAdminTabName()); - } else { diff --git a/backend/web_source/admin.html b/backend/web_source/admin.html index ccfb20b..04c7c99 100644 --- a/backend/web_source/admin.html +++ b/backend/web_source/admin.html @@ -336,6 +336,178 @@ 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 { display: flex; flex-wrap: wrap; @@ -347,6 +519,97 @@ 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 { display: inline-flex; align-items: center; @@ -762,7 +1025,7 @@ - @@ -840,7 +1103,7 @@
- @@ -1063,48 +1326,62 @@