后端优化,增加新需求
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package com.nanri.aiimage.modules.dedupe.controller;
|
package com.nanri.aiimage.modules.dedupe.controller;
|
||||||
|
|
||||||
import com.nanri.aiimage.common.api.ApiResponse;
|
import com.nanri.aiimage.common.api.ApiResponse;
|
||||||
|
import com.nanri.aiimage.common.util.DownloadHeaderUtil;
|
||||||
import com.nanri.aiimage.modules.dedupe.model.dto.DedupeTotalDataCreateRequest;
|
import com.nanri.aiimage.modules.dedupe.model.dto.DedupeTotalDataCreateRequest;
|
||||||
import com.nanri.aiimage.modules.dedupe.model.dto.DedupeTotalDataUpdateRequest;
|
import com.nanri.aiimage.modules.dedupe.model.dto.DedupeTotalDataUpdateRequest;
|
||||||
import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataImportProgressVo;
|
import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataImportProgressVo;
|
||||||
@@ -17,6 +18,10 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
@@ -28,12 +33,18 @@ import org.springframework.web.bind.annotation.RequestParam;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@RequestMapping("/api/admin/dedupe-total-data")
|
@RequestMapping("/api/admin/dedupe-total-data")
|
||||||
@Tag(name = "数据去重总数据", description = "维护数据去重模块的总数据列表,支持增删改查。")
|
@Tag(name = "数据去重总数据", description = "维护数据去重模块的总数据列表,支持增删改查。")
|
||||||
public class DedupeTotalDataController {
|
public class DedupeTotalDataController {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter EXPORT_FILENAME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
|
||||||
|
|
||||||
private final DedupeTotalDataService dedupeTotalDataService;
|
private final DedupeTotalDataService dedupeTotalDataService;
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@@ -50,6 +61,24 @@ public class DedupeTotalDataController {
|
|||||||
return ApiResponse.success(dedupeTotalDataService.page(page, pageSize, keyword, username, operatorId));
|
return ApiResponse.success(dedupeTotalDataService.page(page, pageSize, keyword, username, operatorId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/export")
|
||||||
|
@Operation(summary = "导出总数据", description = "按上传用户名和创建日期导出当前用户可访问的总数据。")
|
||||||
|
public ResponseEntity<byte[]> export(
|
||||||
|
@Parameter(description = "用户名模糊搜索关键字") @RequestParam(required = false) String username,
|
||||||
|
@Parameter(description = "开始日期(包含)")
|
||||||
|
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
|
||||||
|
@Parameter(description = "结束日期(包含)")
|
||||||
|
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate,
|
||||||
|
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId) {
|
||||||
|
byte[] bytes = dedupeTotalDataService.export(username, startDate, endDate, operatorId);
|
||||||
|
String filename = "dedupe-total-data-" + LocalDateTime.now().format(EXPORT_FILENAME_FORMATTER) + ".xlsx";
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.header(HttpHeaders.CONTENT_DISPOSITION, DownloadHeaderUtil.contentDisposition(filename))
|
||||||
|
.contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))
|
||||||
|
.contentLength(bytes.length)
|
||||||
|
.body(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@Operation(summary = "新增总数据", description = "新增一条数据去重总数据。")
|
@Operation(summary = "新增总数据", description = "新增一条数据去重总数据。")
|
||||||
@ApiResponses({
|
@ApiResponses({
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import org.apache.poi.ss.usermodel.DataFormatter;
|
|||||||
import org.apache.poi.ss.usermodel.Row;
|
import org.apache.poi.ss.usermodel.Row;
|
||||||
import org.apache.poi.ss.usermodel.Sheet;
|
import org.apache.poi.ss.usermodel.Sheet;
|
||||||
import org.apache.poi.ss.usermodel.Workbook;
|
import org.apache.poi.ss.usermodel.Workbook;
|
||||||
|
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.PlatformTransactionManager;
|
import org.springframework.transaction.PlatformTransactionManager;
|
||||||
import org.springframework.transaction.TransactionDefinition;
|
import org.springframework.transaction.TransactionDefinition;
|
||||||
@@ -28,11 +29,16 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
import org.springframework.transaction.support.TransactionTemplate;
|
import org.springframework.transaction.support.TransactionTemplate;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
@@ -48,6 +54,8 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
public class DedupeTotalDataService {
|
public class DedupeTotalDataService {
|
||||||
|
|
||||||
private static final int COMPARE_BATCH_SIZE = 5000;
|
private static final int COMPARE_BATCH_SIZE = 5000;
|
||||||
|
private static final long COMPLETED_PROGRESS_RETENTION_MILLIS = 60 * 60 * 1000L;
|
||||||
|
private static final DateTimeFormatter EXPORT_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
private final DedupeTotalDataMapper dedupeTotalDataMapper;
|
private final DedupeTotalDataMapper dedupeTotalDataMapper;
|
||||||
private final AdminUserMapper adminUserMapper;
|
private final AdminUserMapper adminUserMapper;
|
||||||
@@ -57,6 +65,8 @@ public class DedupeTotalDataService {
|
|||||||
private final Map<String, DedupeTotalDataImportProgressVo> deleteImportProgressMap = new ConcurrentHashMap<>();
|
private final Map<String, DedupeTotalDataImportProgressVo> deleteImportProgressMap = new ConcurrentHashMap<>();
|
||||||
private final Map<String, Long> importOwnerMap = new ConcurrentHashMap<>();
|
private final Map<String, Long> importOwnerMap = new ConcurrentHashMap<>();
|
||||||
private final Map<String, Long> deleteImportOwnerMap = new ConcurrentHashMap<>();
|
private final Map<String, Long> deleteImportOwnerMap = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, Long> importCompletedAtMap = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, Long> deleteImportCompletedAtMap = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private TransactionTemplate newRequiresNewTemplate() {
|
private TransactionTemplate newRequiresNewTemplate() {
|
||||||
TransactionTemplate template = new TransactionTemplate(transactionManager);
|
TransactionTemplate template = new TransactionTemplate(transactionManager);
|
||||||
@@ -70,16 +80,10 @@ public class DedupeTotalDataService {
|
|||||||
String safeKeyword = keyword == null ? "" : keyword.trim();
|
String safeKeyword = keyword == null ? "" : keyword.trim();
|
||||||
String safeUsername = username == null ? "" : username.trim();
|
String safeUsername = username == null ? "" : username.trim();
|
||||||
AccessScope scope = resolveAccessScope(operatorId);
|
AccessScope scope = resolveAccessScope(operatorId);
|
||||||
Set<Long> usernameUserIds = safeUsername.isEmpty()
|
|
||||||
? Set.of()
|
|
||||||
: findUserIdsByUsername(safeUsername, scope);
|
|
||||||
if (!safeUsername.isEmpty() && usernameUserIds.isEmpty()) {
|
|
||||||
return emptyPage(safePage, safePageSize);
|
|
||||||
}
|
|
||||||
LambdaQueryWrapper<DedupeTotalDataEntity> query = new LambdaQueryWrapper<DedupeTotalDataEntity>()
|
LambdaQueryWrapper<DedupeTotalDataEntity> query = new LambdaQueryWrapper<DedupeTotalDataEntity>()
|
||||||
.like(!safeKeyword.isEmpty(), DedupeTotalDataEntity::getDataValue, safeKeyword)
|
.like(!safeKeyword.isEmpty(), DedupeTotalDataEntity::getDataValue, safeKeyword)
|
||||||
.in(!scope.allUsers(), DedupeTotalDataEntity::getUploaderUserId, scope.userIds())
|
.in(!scope.allUsers(), DedupeTotalDataEntity::getUploaderUserId, scope.userIds())
|
||||||
.in(!safeUsername.isEmpty(), DedupeTotalDataEntity::getUploaderUserId, usernameUserIds)
|
.like(!safeUsername.isEmpty(), DedupeTotalDataEntity::getUploaderUsername, safeUsername)
|
||||||
.orderByDesc(DedupeTotalDataEntity::getId);
|
.orderByDesc(DedupeTotalDataEntity::getId);
|
||||||
Long total = dedupeTotalDataMapper.selectCount(query);
|
Long total = dedupeTotalDataMapper.selectCount(query);
|
||||||
List<DedupeTotalDataItemVo> items = dedupeTotalDataMapper.selectList(query.last("LIMIT " + ((safePage - 1) * safePageSize) + ", " + safePageSize))
|
List<DedupeTotalDataItemVo> items = dedupeTotalDataMapper.selectList(query.last("LIMIT " + ((safePage - 1) * safePageSize) + ", " + safePageSize))
|
||||||
@@ -94,6 +98,57 @@ public class DedupeTotalDataService {
|
|||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] export(String username, LocalDate startDate, LocalDate endDate, Long operatorId) {
|
||||||
|
if (startDate != null && endDate != null && startDate.isAfter(endDate)) {
|
||||||
|
throw new BusinessException("开始日期不能晚于结束日期");
|
||||||
|
}
|
||||||
|
String safeUsername = username == null ? "" : username.trim();
|
||||||
|
AccessScope scope = resolveAccessScope(operatorId);
|
||||||
|
LambdaQueryWrapper<DedupeTotalDataEntity> query = new LambdaQueryWrapper<DedupeTotalDataEntity>()
|
||||||
|
.in(!scope.allUsers(), DedupeTotalDataEntity::getUploaderUserId, scope.userIds())
|
||||||
|
.like(!safeUsername.isEmpty(), DedupeTotalDataEntity::getUploaderUsername, safeUsername)
|
||||||
|
.ge(startDate != null, DedupeTotalDataEntity::getCreatedAt,
|
||||||
|
startDate == null ? null : startDate.atStartOfDay())
|
||||||
|
.lt(endDate != null, DedupeTotalDataEntity::getCreatedAt,
|
||||||
|
endDate == null ? null : endDate.plusDays(1).atStartOfDay())
|
||||||
|
.orderByDesc(DedupeTotalDataEntity::getId);
|
||||||
|
List<DedupeTotalDataEntity> rows = dedupeTotalDataMapper.selectList(query);
|
||||||
|
return buildExportWorkbook(rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] buildExportWorkbook(List<DedupeTotalDataEntity> rows) {
|
||||||
|
try (SXSSFWorkbook workbook = new SXSSFWorkbook(100);
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||||
|
Sheet sheet = workbook.createSheet("DedupeTotalData");
|
||||||
|
Row header = sheet.createRow(0);
|
||||||
|
header.createCell(0).setCellValue("ID");
|
||||||
|
header.createCell(1).setCellValue("ASIN值");
|
||||||
|
header.createCell(2).setCellValue("用户名");
|
||||||
|
header.createCell(3).setCellValue("创建时间");
|
||||||
|
for (int index = 0; index < rows.size(); index++) {
|
||||||
|
DedupeTotalDataEntity entity = rows.get(index);
|
||||||
|
Row row = sheet.createRow(index + 1);
|
||||||
|
row.createCell(0).setCellValue(entity.getId() == null ? "" : String.valueOf(entity.getId()));
|
||||||
|
row.createCell(1).setCellValue(entity.getDataValue() == null ? "" : entity.getDataValue());
|
||||||
|
row.createCell(2).setCellValue(entity.getUploaderUsername() == null ? "" : entity.getUploaderUsername());
|
||||||
|
row.createCell(3).setCellValue(formatExportTime(entity.getCreatedAt()));
|
||||||
|
}
|
||||||
|
sheet.setColumnWidth(0, 3600);
|
||||||
|
sheet.setColumnWidth(1, 5200);
|
||||||
|
sheet.setColumnWidth(2, 5200);
|
||||||
|
sheet.setColumnWidth(3, 5600);
|
||||||
|
workbook.write(outputStream);
|
||||||
|
workbook.dispose();
|
||||||
|
return outputStream.toByteArray();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new BusinessException("导出数据去重总数据失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatExportTime(LocalDateTime value) {
|
||||||
|
return value == null ? "" : value.format(EXPORT_TIME_FORMATTER);
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public DedupeTotalDataItemVo create(DedupeTotalDataCreateRequest request, Long operatorId) {
|
public DedupeTotalDataItemVo create(DedupeTotalDataCreateRequest request, Long operatorId) {
|
||||||
AdminUserEntity uploader = getOperator(operatorId);
|
AdminUserEntity uploader = getOperator(operatorId);
|
||||||
@@ -129,12 +184,19 @@ public class DedupeTotalDataService {
|
|||||||
Set<String> existingValues = new HashSet<>();
|
Set<String> existingValues = new HashSet<>();
|
||||||
for (int start = 0; start < normalizedValues.size(); start += COMPARE_BATCH_SIZE) {
|
for (int start = 0; start < normalizedValues.size(); start += COMPARE_BATCH_SIZE) {
|
||||||
int end = Math.min(start + COMPARE_BATCH_SIZE, normalizedValues.size());
|
int end = Math.min(start + COMPARE_BATCH_SIZE, normalizedValues.size());
|
||||||
existingValues.addAll(dedupeTotalDataMapper.selectExistingDataValues(normalizedValues.subList(start, end)));
|
List<String> batch = dedupeTotalDataMapper.selectExistingDataValues(normalizedValues.subList(start, end));
|
||||||
|
if (batch != null) {
|
||||||
|
batch.stream()
|
||||||
|
.map(this::normalizeComparableValueOrBlank)
|
||||||
|
.filter(value -> !value.isEmpty())
|
||||||
|
.forEach(existingValues::add);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return existingValues;
|
return existingValues;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DedupeTotalDataImportStartVo startImport(MultipartFile file, Long operatorId) {
|
public DedupeTotalDataImportStartVo startImport(MultipartFile file, Long operatorId) {
|
||||||
|
cleanupExpiredProgress();
|
||||||
if (file == null || file.isEmpty()) {
|
if (file == null || file.isEmpty()) {
|
||||||
throw new BusinessException("请上传 xlsx 文件");
|
throw new BusinessException("请上传 xlsx 文件");
|
||||||
}
|
}
|
||||||
@@ -167,6 +229,7 @@ public class DedupeTotalDataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public DedupeTotalDataImportProgressVo getImportProgress(String importId, Long operatorId) {
|
public DedupeTotalDataImportProgressVo getImportProgress(String importId, Long operatorId) {
|
||||||
|
cleanupExpiredProgress();
|
||||||
DedupeTotalDataImportProgressVo progress = importProgressMap.get(importId);
|
DedupeTotalDataImportProgressVo progress = importProgressMap.get(importId);
|
||||||
Long ownerId = importOwnerMap.get(importId);
|
Long ownerId = importOwnerMap.get(importId);
|
||||||
if (progress == null || ownerId == null) {
|
if (progress == null || ownerId == null) {
|
||||||
@@ -177,6 +240,7 @@ public class DedupeTotalDataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public DedupeTotalDataImportStartVo startDeleteImport(MultipartFile file, Long operatorId) {
|
public DedupeTotalDataImportStartVo startDeleteImport(MultipartFile file, Long operatorId) {
|
||||||
|
cleanupExpiredProgress();
|
||||||
if (file == null || file.isEmpty()) {
|
if (file == null || file.isEmpty()) {
|
||||||
throw new BusinessException("请上传 xlsx 文件");
|
throw new BusinessException("请上传 xlsx 文件");
|
||||||
}
|
}
|
||||||
@@ -208,6 +272,7 @@ public class DedupeTotalDataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public DedupeTotalDataImportProgressVo getDeleteImportProgress(String importId, Long operatorId) {
|
public DedupeTotalDataImportProgressVo getDeleteImportProgress(String importId, Long operatorId) {
|
||||||
|
cleanupExpiredProgress();
|
||||||
DedupeTotalDataImportProgressVo progress = deleteImportProgressMap.get(importId);
|
DedupeTotalDataImportProgressVo progress = deleteImportProgressMap.get(importId);
|
||||||
Long ownerId = deleteImportOwnerMap.get(importId);
|
Long ownerId = deleteImportOwnerMap.get(importId);
|
||||||
if (progress == null || ownerId == null) {
|
if (progress == null || ownerId == null) {
|
||||||
@@ -237,6 +302,7 @@ public class DedupeTotalDataService {
|
|||||||
progress.setErrorMessage(e instanceof BusinessException ? e.getMessage() : "删除 Excel 匹配数据失败");
|
progress.setErrorMessage(e instanceof BusinessException ? e.getMessage() : "删除 Excel 匹配数据失败");
|
||||||
} finally {
|
} finally {
|
||||||
deleteQuietly(tempFile);
|
deleteQuietly(tempFile);
|
||||||
|
deleteImportCompletedAtMap.put(importId, System.currentTimeMillis());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,6 +327,7 @@ public class DedupeTotalDataService {
|
|||||||
progress.setErrorMessage(e instanceof BusinessException ? e.getMessage() : "导入 Excel 失败");
|
progress.setErrorMessage(e instanceof BusinessException ? e.getMessage() : "导入 Excel 失败");
|
||||||
} finally {
|
} finally {
|
||||||
deleteQuietly(tempFile);
|
deleteQuietly(tempFile);
|
||||||
|
importCompletedAtMap.put(importId, System.currentTimeMillis());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,17 +474,22 @@ public class DedupeTotalDataService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
final String pendingDataValue = dataValue;
|
final String pendingDataValue = dataValue;
|
||||||
Boolean inserted = newRequiresNewTemplate().execute(status -> {
|
Boolean inserted;
|
||||||
if (existsDataValue(pendingDataValue)) {
|
try {
|
||||||
return false;
|
inserted = newRequiresNewTemplate().execute(status -> {
|
||||||
}
|
if (existsDataValue(pendingDataValue)) {
|
||||||
DedupeTotalDataEntity entity = new DedupeTotalDataEntity();
|
return false;
|
||||||
entity.setDataValue(pendingDataValue);
|
}
|
||||||
entity.setUploaderUserId(uploaderUserId);
|
DedupeTotalDataEntity entity = new DedupeTotalDataEntity();
|
||||||
entity.setUploaderUsername(uploaderUsername);
|
entity.setDataValue(pendingDataValue);
|
||||||
dedupeTotalDataMapper.insert(entity);
|
entity.setUploaderUserId(uploaderUserId);
|
||||||
return true;
|
entity.setUploaderUsername(uploaderUsername);
|
||||||
});
|
dedupeTotalDataMapper.insert(entity);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
} catch (DuplicateKeyException ignored) {
|
||||||
|
inserted = false;
|
||||||
|
}
|
||||||
if (Boolean.TRUE.equals(inserted)) {
|
if (Boolean.TRUE.equals(inserted)) {
|
||||||
insertedCount++;
|
insertedCount++;
|
||||||
} else {
|
} else {
|
||||||
@@ -590,7 +662,7 @@ public class DedupeTotalDataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String normalizeComparableValueOrBlank(String value) {
|
public String normalizeComparableValueOrBlank(String value) {
|
||||||
return normalizeExcelText(value);
|
return normalizeExcelText(value).toUpperCase(Locale.ROOT);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ensureUnique(String dataValue, Long excludeId) {
|
private void ensureUnique(String dataValue, Long excludeId) {
|
||||||
@@ -649,31 +721,29 @@ public class DedupeTotalDataService {
|
|||||||
|| (role.isEmpty() && Integer.valueOf(1).equals(user.getIsAdmin()) && user.getCreatedById() == null);
|
|| (role.isEmpty() && Integer.valueOf(1).equals(user.getIsAdmin()) && user.getCreatedById() == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Set<Long> findUserIdsByUsername(String username, AccessScope scope) {
|
|
||||||
Set<Long> matched = adminUserMapper.selectIdsByUsernameLike(username)
|
|
||||||
.stream()
|
|
||||||
.filter(id -> id != null && id > 0)
|
|
||||||
.collect(java.util.stream.Collectors.toSet());
|
|
||||||
if (scope.allUsers()) {
|
|
||||||
return matched;
|
|
||||||
}
|
|
||||||
matched.retainAll(scope.userIds());
|
|
||||||
return matched;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ensureUploaderAccess(Long uploaderUserId, AccessScope scope) {
|
private void ensureUploaderAccess(Long uploaderUserId, AccessScope scope) {
|
||||||
if (!scope.allUsers() && (uploaderUserId == null || !scope.userIds().contains(uploaderUserId))) {
|
if (!scope.allUsers() && (uploaderUserId == null || !scope.userIds().contains(uploaderUserId))) {
|
||||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "无权操作该总数据");
|
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "无权操作该总数据");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private DedupeTotalDataPageVo emptyPage(long page, long pageSize) {
|
private void cleanupExpiredProgress() {
|
||||||
DedupeTotalDataPageVo vo = new DedupeTotalDataPageVo();
|
long cutoff = System.currentTimeMillis() - COMPLETED_PROGRESS_RETENTION_MILLIS;
|
||||||
vo.setItems(List.of());
|
cleanupExpiredProgressEntries(importCompletedAtMap, importProgressMap, importOwnerMap, cutoff);
|
||||||
vo.setTotal(0L);
|
cleanupExpiredProgressEntries(deleteImportCompletedAtMap, deleteImportProgressMap, deleteImportOwnerMap, cutoff);
|
||||||
vo.setPage(page);
|
}
|
||||||
vo.setPageSize(pageSize);
|
|
||||||
return vo;
|
private void cleanupExpiredProgressEntries(
|
||||||
|
Map<String, Long> completedAtMap,
|
||||||
|
Map<String, DedupeTotalDataImportProgressVo> progressMap,
|
||||||
|
Map<String, Long> ownerMap,
|
||||||
|
long cutoff) {
|
||||||
|
completedAtMap.forEach((id, completedAt) -> {
|
||||||
|
if (completedAt != null && completedAt < cutoff && completedAtMap.remove(id, completedAt)) {
|
||||||
|
progressMap.remove(id);
|
||||||
|
ownerMap.remove(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private String normalizeExcelText(String value) {
|
private String normalizeExcelText(String value) {
|
||||||
|
|||||||
@@ -3,14 +3,7 @@ package com.nanri.aiimage.modules.permission.mapper;
|
|||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
|
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
import org.apache.ibatis.annotations.Param;
|
|
||||||
import org.apache.ibatis.annotations.Select;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface AdminUserMapper extends BaseMapper<AdminUserEntity> {
|
public interface AdminUserMapper extends BaseMapper<AdminUserEntity> {
|
||||||
|
|
||||||
@Select("SELECT id FROM users WHERE username LIKE CONCAT('%', #{username}, '%') ORDER BY id ASC")
|
|
||||||
List<Long> selectIdsByUsernameLike(@Param("username") String username);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ import lombok.Data;
|
|||||||
@Schema(description = "店铺密钥新增请求")
|
@Schema(description = "店铺密钥新增请求")
|
||||||
public class ShopKeyCreateRequest {
|
public class ShopKeyCreateRequest {
|
||||||
|
|
||||||
|
@Size(max = 128, message = "备注名长度不能超过128个字符")
|
||||||
|
@Schema(description = "备注名")
|
||||||
|
private String remarkName;
|
||||||
|
|
||||||
@NotBlank(message = "紫鸟账号名称不能为空")
|
@NotBlank(message = "紫鸟账号名称不能为空")
|
||||||
@Size(max = 128, message = "紫鸟账号名称长度不能超过128个字符")
|
@Size(max = 128, message = "紫鸟账号名称长度不能超过128个字符")
|
||||||
@Schema(description = "紫鸟账号名称", requiredMode = Schema.RequiredMode.REQUIRED)
|
@Schema(description = "紫鸟账号名称", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ import lombok.Data;
|
|||||||
@Schema(description = "店铺密钥更新请求")
|
@Schema(description = "店铺密钥更新请求")
|
||||||
public class ShopKeyUpdateRequest {
|
public class ShopKeyUpdateRequest {
|
||||||
|
|
||||||
|
@Size(max = 128, message = "备注名长度不能超过128个字符")
|
||||||
|
@Schema(description = "备注名")
|
||||||
|
private String remarkName;
|
||||||
|
|
||||||
@NotBlank(message = "紫鸟账号名称不能为空")
|
@NotBlank(message = "紫鸟账号名称不能为空")
|
||||||
@Size(max = 128, message = "紫鸟账号名称长度不能超过128个字符")
|
@Size(max = 128, message = "紫鸟账号名称长度不能超过128个字符")
|
||||||
@Schema(description = "紫鸟账号名称", requiredMode = Schema.RequiredMode.REQUIRED)
|
@Schema(description = "紫鸟账号名称", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ public class ShopKeyEntity {
|
|||||||
|
|
||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
private String remarkName;
|
||||||
private String ziniaoAccountName;
|
private String ziniaoAccountName;
|
||||||
private String ziniaoToken;
|
private String ziniaoToken;
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ public class ShopKeyItemVo {
|
|||||||
@Schema(description = "主键ID")
|
@Schema(description = "主键ID")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "备注名")
|
||||||
|
private String remarkName;
|
||||||
|
|
||||||
@Schema(description = "紫鸟账号名称")
|
@Schema(description = "紫鸟账号名称")
|
||||||
private String ziniaoAccountName;
|
private String ziniaoAccountName;
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ public class ShopKeyService {
|
|||||||
String ziniaoAccountName = normalizeRequired(request.getZiniaoAccountName(), "紫鸟账号名称不能为空");
|
String ziniaoAccountName = normalizeRequired(request.getZiniaoAccountName(), "紫鸟账号名称不能为空");
|
||||||
String ziniaoToken = normalizeRequired(request.getZiniaoToken(), "紫鸟令牌不能为空");
|
String ziniaoToken = normalizeRequired(request.getZiniaoToken(), "紫鸟令牌不能为空");
|
||||||
ShopKeyEntity entity = new ShopKeyEntity();
|
ShopKeyEntity entity = new ShopKeyEntity();
|
||||||
|
entity.setRemarkName(normalizeOptional(request.getRemarkName()));
|
||||||
entity.setZiniaoAccountName(ziniaoAccountName);
|
entity.setZiniaoAccountName(ziniaoAccountName);
|
||||||
entity.setZiniaoToken(ziniaoToken);
|
entity.setZiniaoToken(ziniaoToken);
|
||||||
shopKeyMapper.insert(entity);
|
shopKeyMapper.insert(entity);
|
||||||
@@ -59,6 +60,7 @@ public class ShopKeyService {
|
|||||||
ShopKeyEntity entity = getById(id);
|
ShopKeyEntity entity = getById(id);
|
||||||
String ziniaoAccountName = normalizeRequired(request.getZiniaoAccountName(), "紫鸟账号名称不能为空");
|
String ziniaoAccountName = normalizeRequired(request.getZiniaoAccountName(), "紫鸟账号名称不能为空");
|
||||||
String ziniaoToken = normalizeRequired(request.getZiniaoToken(), "紫鸟令牌不能为空");
|
String ziniaoToken = normalizeRequired(request.getZiniaoToken(), "紫鸟令牌不能为空");
|
||||||
|
entity.setRemarkName(normalizeOptional(request.getRemarkName()));
|
||||||
entity.setZiniaoAccountName(ziniaoAccountName);
|
entity.setZiniaoAccountName(ziniaoAccountName);
|
||||||
entity.setZiniaoToken(ziniaoToken);
|
entity.setZiniaoToken(ziniaoToken);
|
||||||
shopKeyMapper.updateById(entity);
|
shopKeyMapper.updateById(entity);
|
||||||
@@ -89,9 +91,14 @@ public class ShopKeyService {
|
|||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String normalizeOptional(String value) {
|
||||||
|
return value == null ? "" : value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
private ShopKeyItemVo toItemVo(ShopKeyEntity entity) {
|
private ShopKeyItemVo toItemVo(ShopKeyEntity entity) {
|
||||||
ShopKeyItemVo vo = new ShopKeyItemVo();
|
ShopKeyItemVo vo = new ShopKeyItemVo();
|
||||||
vo.setId(entity.getId());
|
vo.setId(entity.getId());
|
||||||
|
vo.setRemarkName(entity.getRemarkName());
|
||||||
vo.setZiniaoAccountName(entity.getZiniaoAccountName());
|
vo.setZiniaoAccountName(entity.getZiniaoAccountName());
|
||||||
vo.setZiniaoToken(entity.getZiniaoToken());
|
vo.setZiniaoToken(entity.getZiniaoToken());
|
||||||
vo.setCreatedAt(entity.getCreatedAt());
|
vo.setCreatedAt(entity.getCreatedAt());
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ public class TaskResultFileJobWorker {
|
|||||||
|
|
||||||
private void processInternal(TaskFileJobEntity job) {
|
private void processInternal(TaskFileJobEntity job) {
|
||||||
long startedAt = System.currentTimeMillis();
|
long startedAt = System.currentTimeMillis();
|
||||||
|
boolean finalizeWithdraw = false;
|
||||||
try {
|
try {
|
||||||
TaskFileJobEntity latest = taskFileJobService.findById(job.getId());
|
TaskFileJobEntity latest = taskFileJobService.findById(job.getId());
|
||||||
if (latest != null && "SUCCESS".equals(latest.getStatus())) {
|
if (latest != null && "SUCCESS".equals(latest.getStatus())) {
|
||||||
@@ -162,10 +163,14 @@ public class TaskResultFileJobWorker {
|
|||||||
String resultFileUrl = resolveResultFileUrl(job);
|
String resultFileUrl = resolveResultFileUrl(job);
|
||||||
taskFileJobService.markSuccess(job, resultFileUrl);
|
taskFileJobService.markSuccess(job, resultFileUrl);
|
||||||
cleanupAfterSuccess(job);
|
cleanupAfterSuccess(job);
|
||||||
|
finalizeWithdraw = "WITHDRAW".equals(job.getModuleType());
|
||||||
log.info("[task-file-job] process success jobId={} taskId={} moduleType={} resultId={} elapsedMs={} resultFileUrl={}",
|
log.info("[task-file-job] process success jobId={} taskId={} moduleType={} resultId={} elapsedMs={} resultFileUrl={}",
|
||||||
job.getId(), job.getTaskId(), job.getModuleType(), job.getResultId(),
|
job.getId(), job.getTaskId(), job.getModuleType(), job.getResultId(),
|
||||||
System.currentTimeMillis() - startedAt, resultFileUrl);
|
System.currentTimeMillis() - startedAt, resultFileUrl);
|
||||||
}
|
}
|
||||||
|
if (finalizeWithdraw) {
|
||||||
|
withdrawTaskService.tryFinalizeTask(job.getTaskId(), false);
|
||||||
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
TaskFileJobEntity latest = taskFileJobService.findById(job.getId());
|
TaskFileJobEntity latest = taskFileJobService.findById(job.getId());
|
||||||
if (latest != null && "SUCCESS".equals(latest.getStatus())) {
|
if (latest != null && "SUCCESS".equals(latest.getStatus())) {
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE biz_shop_key
|
||||||
|
ADD COLUMN remark_name VARCHAR(128) NOT NULL DEFAULT '' COMMENT '备注名' AFTER id;
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
package com.nanri.aiimage.modules.dedupe.service;
|
package com.nanri.aiimage.modules.dedupe.service;
|
||||||
|
|
||||||
|
import com.nanri.aiimage.common.exception.BusinessException;
|
||||||
import com.nanri.aiimage.modules.dedupe.mapper.DedupeTotalDataMapper;
|
import com.nanri.aiimage.modules.dedupe.mapper.DedupeTotalDataMapper;
|
||||||
import com.nanri.aiimage.modules.dedupe.model.dto.DedupeTotalDataCreateRequest;
|
import com.nanri.aiimage.modules.dedupe.model.dto.DedupeTotalDataCreateRequest;
|
||||||
import com.nanri.aiimage.modules.dedupe.model.entity.DedupeTotalDataEntity;
|
import com.nanri.aiimage.modules.dedupe.model.entity.DedupeTotalDataEntity;
|
||||||
|
import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataImportProgressVo;
|
||||||
import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataItemVo;
|
import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataItemVo;
|
||||||
import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataPageVo;
|
import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataPageVo;
|
||||||
import com.nanri.aiimage.modules.permission.mapper.AdminUserMapper;
|
import com.nanri.aiimage.modules.permission.mapper.AdminUserMapper;
|
||||||
@@ -15,15 +17,23 @@ import org.mockito.InjectMocks;
|
|||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||||
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
import org.springframework.transaction.PlatformTransactionManager;
|
import org.springframework.transaction.PlatformTransactionManager;
|
||||||
|
import org.springframework.transaction.TransactionStatus;
|
||||||
import org.springframework.mock.web.MockMultipartFile;
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
@@ -42,6 +52,8 @@ class DedupeTotalDataServiceTest {
|
|||||||
private ShopManageGroupMapper shopManageGroupMapper;
|
private ShopManageGroupMapper shopManageGroupMapper;
|
||||||
@Mock
|
@Mock
|
||||||
private PlatformTransactionManager transactionManager;
|
private PlatformTransactionManager transactionManager;
|
||||||
|
@Mock
|
||||||
|
private TransactionStatus transactionStatus;
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private DedupeTotalDataService service;
|
private DedupeTotalDataService service;
|
||||||
|
|
||||||
@@ -65,13 +77,14 @@ class DedupeTotalDataServiceTest {
|
|||||||
});
|
});
|
||||||
|
|
||||||
DedupeTotalDataCreateRequest request = new DedupeTotalDataCreateRequest();
|
DedupeTotalDataCreateRequest request = new DedupeTotalDataCreateRequest();
|
||||||
request.setDataValue(" B012345678 ");
|
request.setDataValue(" b012345678 ");
|
||||||
DedupeTotalDataItemVo item = service.create(request, 23L);
|
DedupeTotalDataItemVo item = service.create(request, 23L);
|
||||||
|
|
||||||
ArgumentCaptor<DedupeTotalDataEntity> captor = ArgumentCaptor.forClass(DedupeTotalDataEntity.class);
|
ArgumentCaptor<DedupeTotalDataEntity> captor = ArgumentCaptor.forClass(DedupeTotalDataEntity.class);
|
||||||
verify(dedupeTotalDataMapper).insert(captor.capture());
|
verify(dedupeTotalDataMapper).insert(captor.capture());
|
||||||
assertEquals(23L, captor.getValue().getUploaderUserId());
|
assertEquals(23L, captor.getValue().getUploaderUserId());
|
||||||
assertEquals("member-a", captor.getValue().getUploaderUsername());
|
assertEquals("member-a", captor.getValue().getUploaderUsername());
|
||||||
|
assertEquals("B012345678", captor.getValue().getDataValue());
|
||||||
assertEquals("member-a", item.getUsername());
|
assertEquals("member-a", item.getUsername());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,31 +138,106 @@ class DedupeTotalDataServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void usernameSearchCannotExpandLeaderScope() {
|
void concurrentDuplicateDuringImportIsSkipped() throws Exception {
|
||||||
|
when(adminUserMapper.selectById(23L)).thenReturn(user(23L, "normal", "member-a"));
|
||||||
|
when(dedupeTotalDataMapper.selectOne(any())).thenReturn(null);
|
||||||
|
when(transactionManager.getTransaction(any())).thenReturn(transactionStatus);
|
||||||
|
when(dedupeTotalDataMapper.insert(any(DedupeTotalDataEntity.class)))
|
||||||
|
.thenThrow(new DuplicateKeyException("duplicate"));
|
||||||
|
|
||||||
|
var result = service.importFromExcel(asinWorkbook("b012345678"), 23L);
|
||||||
|
|
||||||
|
assertEquals(0, result.getInsertedCount());
|
||||||
|
assertEquals(1, result.getSkippedCount());
|
||||||
|
verify(transactionManager).rollback(transactionStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void usernameSearchKeepsLeaderScope() {
|
||||||
when(adminUserMapper.selectById(10L)).thenReturn(user(10L, "admin", "leader"));
|
when(adminUserMapper.selectById(10L)).thenReturn(user(10L, "admin", "leader"));
|
||||||
when(shopManageGroupMapper.selectManagedMemberUserIds(10L)).thenReturn(List.of(23L));
|
when(shopManageGroupMapper.selectManagedMemberUserIds(10L)).thenReturn(List.of(23L));
|
||||||
when(adminUserMapper.selectIdsByUsernameLike("other")).thenReturn(List.of(99L));
|
when(dedupeTotalDataMapper.selectCount(any())).thenReturn(0L);
|
||||||
|
when(dedupeTotalDataMapper.selectList(any())).thenReturn(List.of());
|
||||||
|
|
||||||
DedupeTotalDataPageVo page = service.page(1, 15, "", "other", 10L);
|
DedupeTotalDataPageVo page = service.page(1, 15, "", "other", 10L);
|
||||||
|
|
||||||
assertEquals(0L, page.getTotal());
|
assertEquals(0L, page.getTotal());
|
||||||
assertTrue(page.getItems().isEmpty());
|
assertTrue(page.getItems().isEmpty());
|
||||||
verify(dedupeTotalDataMapper, never()).selectCount(any());
|
verify(shopManageGroupMapper).selectManagedMemberUserIds(10L);
|
||||||
verify(dedupeTotalDataMapper, never()).selectList(any());
|
verify(dedupeTotalDataMapper).selectCount(any());
|
||||||
|
verify(dedupeTotalDataMapper).selectList(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void comparableValueLookupRemainsGlobal() {
|
void comparableValueLookupRemainsGlobal() {
|
||||||
when(dedupeTotalDataMapper.selectExistingDataValues(List.of("B012345678")))
|
when(dedupeTotalDataMapper.selectExistingDataValues(List.of("B012345678")))
|
||||||
.thenReturn(List.of("B012345678"));
|
.thenReturn(List.of("b012345678"));
|
||||||
|
|
||||||
Set<String> values = service.findExistingComparableValues(List.of(" B012345678 "));
|
Set<String> values = service.findExistingComparableValues(List.of(" b012345678 "));
|
||||||
|
|
||||||
assertEquals(Set.of("B012345678"), values);
|
assertEquals(Set.of("B012345678"), values);
|
||||||
verify(adminUserMapper, never()).selectById(any(Long.class));
|
verify(adminUserMapper, never()).selectById(any(Long.class));
|
||||||
verify(shopManageGroupMapper, never()).selectManagedMemberUserIds(any(Long.class));
|
verify(shopManageGroupMapper, never()).selectManagedMemberUserIds(any(Long.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void exportUsesScopedUsernameAndProducesWorkbook() throws Exception {
|
||||||
|
when(adminUserMapper.selectById(10L)).thenReturn(user(10L, "admin", "leader"));
|
||||||
|
when(shopManageGroupMapper.selectManagedMemberUserIds(10L)).thenReturn(List.of(23L));
|
||||||
|
DedupeTotalDataEntity entity = data(91L, 23L);
|
||||||
|
entity.setUploaderUsername("member-a");
|
||||||
|
entity.setCreatedAt(LocalDateTime.of(2026, 7, 20, 12, 30));
|
||||||
|
when(dedupeTotalDataMapper.selectList(any())).thenReturn(List.of(entity));
|
||||||
|
|
||||||
|
byte[] bytes = service.export(
|
||||||
|
"member",
|
||||||
|
LocalDate.of(2026, 7, 1),
|
||||||
|
LocalDate.of(2026, 7, 20),
|
||||||
|
10L);
|
||||||
|
|
||||||
|
try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(bytes))) {
|
||||||
|
var sheet = workbook.getSheetAt(0);
|
||||||
|
assertEquals("ASIN值", sheet.getRow(0).getCell(1).getStringCellValue());
|
||||||
|
assertEquals("B012345678", sheet.getRow(1).getCell(1).getStringCellValue());
|
||||||
|
assertEquals("member-a", sheet.getRow(1).getCell(2).getStringCellValue());
|
||||||
|
assertEquals("2026-07-20 12:30:00", sheet.getRow(1).getCell(3).getStringCellValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(shopManageGroupMapper).selectManagedMemberUserIds(10L);
|
||||||
|
verify(dedupeTotalDataMapper).selectList(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void exportRejectsReversedDateRange() {
|
||||||
|
assertThrows(BusinessException.class, () -> service.export(
|
||||||
|
"",
|
||||||
|
LocalDate.of(2026, 7, 20),
|
||||||
|
LocalDate.of(2026, 7, 1),
|
||||||
|
10L));
|
||||||
|
verify(dedupeTotalDataMapper, never()).selectList(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
void expiredCompletedProgressIsRemovedOnNextLookup() {
|
||||||
|
Map<String, DedupeTotalDataImportProgressVo> progressMap =
|
||||||
|
(Map<String, DedupeTotalDataImportProgressVo>) ReflectionTestUtils.getField(service, "importProgressMap");
|
||||||
|
Map<String, Long> ownerMap = (Map<String, Long>) ReflectionTestUtils.getField(service, "importOwnerMap");
|
||||||
|
Map<String, Long> completedAtMap =
|
||||||
|
(Map<String, Long>) ReflectionTestUtils.getField(service, "importCompletedAtMap");
|
||||||
|
DedupeTotalDataImportProgressVo progress = new DedupeTotalDataImportProgressVo();
|
||||||
|
progress.setStatus("success");
|
||||||
|
progressMap.put("expired", progress);
|
||||||
|
ownerMap.put("expired", 23L);
|
||||||
|
completedAtMap.put("expired", System.currentTimeMillis() - (2 * 60 * 60 * 1000L));
|
||||||
|
|
||||||
|
assertThrows(BusinessException.class, () -> service.getImportProgress("expired", 23L));
|
||||||
|
|
||||||
|
assertFalse(progressMap.containsKey("expired"));
|
||||||
|
assertFalse(ownerMap.containsKey("expired"));
|
||||||
|
assertFalse(completedAtMap.containsKey("expired"));
|
||||||
|
}
|
||||||
|
|
||||||
private AdminUserEntity user(Long id, String role, String username) {
|
private AdminUserEntity user(Long id, String role, String username) {
|
||||||
AdminUserEntity user = new AdminUserEntity();
|
AdminUserEntity user = new AdminUserEntity();
|
||||||
user.setId(id);
|
user.setId(id);
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package com.nanri.aiimage.modules.task.service;
|
||||||
|
|
||||||
|
import com.nanri.aiimage.config.InstanceMetadata;
|
||||||
|
import com.nanri.aiimage.modules.appearancepatent.service.AppearancePatentTaskService;
|
||||||
|
import com.nanri.aiimage.modules.brand.service.BrandTaskService;
|
||||||
|
import com.nanri.aiimage.modules.collectdata.service.CollectDataService;
|
||||||
|
import com.nanri.aiimage.modules.deletebrand.service.DeleteBrandRunService;
|
||||||
|
import com.nanri.aiimage.modules.patroldelete.service.PatrolDeleteTaskService;
|
||||||
|
import com.nanri.aiimage.modules.pricetrack.service.PriceTrackTaskService;
|
||||||
|
import com.nanri.aiimage.modules.productrisk.service.ProductRiskTaskService;
|
||||||
|
import com.nanri.aiimage.modules.queryasin.service.QueryAsinTaskService;
|
||||||
|
import com.nanri.aiimage.modules.shopmatch.service.ShopMatchTaskService;
|
||||||
|
import com.nanri.aiimage.modules.similarasin.service.SimilarAsinTaskService;
|
||||||
|
import com.nanri.aiimage.modules.task.mapper.FileResultMapper;
|
||||||
|
import com.nanri.aiimage.modules.task.model.entity.FileResultEntity;
|
||||||
|
import com.nanri.aiimage.modules.task.model.entity.TaskFileJobEntity;
|
||||||
|
import com.nanri.aiimage.modules.withdraw.service.WithdrawTaskService;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InOrder;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.inOrder;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class TaskResultFileJobWorkerTest {
|
||||||
|
|
||||||
|
@Mock private TaskFileJobService taskFileJobService;
|
||||||
|
@Mock private TaskDistributedLockService taskDistributedLockService;
|
||||||
|
@Mock private TaskResultPayloadService taskResultPayloadService;
|
||||||
|
@Mock private FileResultMapper fileResultMapper;
|
||||||
|
@Mock private TaskFileJobLocalDispatcher taskFileJobLocalDispatcher;
|
||||||
|
@Mock private InstanceMetadata instanceMetadata;
|
||||||
|
@Mock private ShopMatchTaskService shopMatchTaskService;
|
||||||
|
@Mock private PriceTrackTaskService priceTrackTaskService;
|
||||||
|
@Mock private ProductRiskTaskService productRiskTaskService;
|
||||||
|
@Mock private QueryAsinTaskService queryAsinTaskService;
|
||||||
|
@Mock private WithdrawTaskService withdrawTaskService;
|
||||||
|
@Mock private PatrolDeleteTaskService patrolDeleteTaskService;
|
||||||
|
@Mock private AppearancePatentTaskService appearancePatentTaskService;
|
||||||
|
@Mock private SimilarAsinTaskService similarAsinTaskService;
|
||||||
|
@Mock private DeleteBrandRunService deleteBrandRunService;
|
||||||
|
@Mock private BrandTaskService brandTaskService;
|
||||||
|
@Mock private CollectDataService collectDataService;
|
||||||
|
|
||||||
|
@InjectMocks private TaskResultFileJobWorker worker;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void withdrawFileSuccessFinalizesTaskAfterReleasingLock() {
|
||||||
|
long jobId = 13640L;
|
||||||
|
long taskId = 20140L;
|
||||||
|
long resultId = 22928L;
|
||||||
|
TaskFileJobEntity job = new TaskFileJobEntity();
|
||||||
|
job.setId(jobId);
|
||||||
|
job.setTaskId(taskId);
|
||||||
|
job.setResultId(resultId);
|
||||||
|
job.setModuleType("WITHDRAW");
|
||||||
|
job.setScopeKey("withdraw:20140");
|
||||||
|
|
||||||
|
FileResultEntity result = new FileResultEntity();
|
||||||
|
result.setResultFileUrl("result/withdraw/20140.xlsx");
|
||||||
|
TaskDistributedLockService.LockHandle lock = mock(TaskDistributedLockService.LockHandle.class);
|
||||||
|
|
||||||
|
when(taskFileJobService.markRunning(jobId)).thenReturn(true);
|
||||||
|
when(taskDistributedLockService.acquire("WITHDRAW", taskId, TaskDistributedLockService.DEFAULT_WAIT_MILLIS))
|
||||||
|
.thenReturn(lock);
|
||||||
|
when(fileResultMapper.selectById(resultId)).thenReturn(result);
|
||||||
|
|
||||||
|
worker.process(job);
|
||||||
|
|
||||||
|
verify(taskFileJobService).markSuccess(job, "result/withdraw/20140.xlsx");
|
||||||
|
verify(taskResultPayloadService).deleteLatest(taskId, "WITHDRAW", "withdraw:20140");
|
||||||
|
InOrder order = inOrder(lock, withdrawTaskService);
|
||||||
|
order.verify(lock).close();
|
||||||
|
order.verify(withdrawTaskService).tryFinalizeTask(taskId, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2202,6 +2202,7 @@ def list_shop_keys():
|
|||||||
items = [
|
items = [
|
||||||
{
|
{
|
||||||
'id': item.get('id'),
|
'id': item.get('id'),
|
||||||
|
'remark_name': item.get('remarkName') or '',
|
||||||
'ziniao_account_name': item.get('ziniaoAccountName') or '',
|
'ziniao_account_name': item.get('ziniaoAccountName') or '',
|
||||||
'ziniao_token': item.get('ziniaoToken') or '',
|
'ziniao_token': item.get('ziniaoToken') or '',
|
||||||
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
|
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
|
||||||
@@ -2226,6 +2227,7 @@ def create_shop_key():
|
|||||||
return denied
|
return denied
|
||||||
data = request.get_json() or {}
|
data = request.get_json() or {}
|
||||||
payload = {
|
payload = {
|
||||||
|
'remarkName': (data.get('remark_name') or '').strip(),
|
||||||
'ziniaoAccountName': (data.get('ziniao_account_name') or '').strip(),
|
'ziniaoAccountName': (data.get('ziniao_account_name') or '').strip(),
|
||||||
'ziniaoToken': (data.get('ziniao_token') or '').strip(),
|
'ziniaoToken': (data.get('ziniao_token') or '').strip(),
|
||||||
}
|
}
|
||||||
@@ -2242,6 +2244,7 @@ def create_shop_key():
|
|||||||
'msg': result.get('message') or '创建成功',
|
'msg': result.get('message') or '创建成功',
|
||||||
'item': {
|
'item': {
|
||||||
'id': item.get('id'),
|
'id': item.get('id'),
|
||||||
|
'remark_name': item.get('remarkName') or '',
|
||||||
'ziniao_account_name': item.get('ziniaoAccountName') or '',
|
'ziniao_account_name': item.get('ziniaoAccountName') or '',
|
||||||
'ziniao_token': item.get('ziniaoToken') or '',
|
'ziniao_token': item.get('ziniaoToken') or '',
|
||||||
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
|
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
|
||||||
@@ -2258,6 +2261,7 @@ def update_shop_key(item_id):
|
|||||||
return denied
|
return denied
|
||||||
data = request.get_json() or {}
|
data = request.get_json() or {}
|
||||||
payload = {
|
payload = {
|
||||||
|
'remarkName': (data.get('remark_name') or '').strip(),
|
||||||
'ziniaoAccountName': (data.get('ziniao_account_name') or '').strip(),
|
'ziniaoAccountName': (data.get('ziniao_account_name') or '').strip(),
|
||||||
'ziniaoToken': (data.get('ziniao_token') or '').strip(),
|
'ziniaoToken': (data.get('ziniao_token') or '').strip(),
|
||||||
}
|
}
|
||||||
@@ -2274,6 +2278,7 @@ def update_shop_key(item_id):
|
|||||||
'msg': result.get('message') or '更新成功',
|
'msg': result.get('message') or '更新成功',
|
||||||
'item': {
|
'item': {
|
||||||
'id': item.get('id'),
|
'id': item.get('id'),
|
||||||
|
'remark_name': item.get('remarkName') or '',
|
||||||
'ziniao_account_name': item.get('ziniaoAccountName') or '',
|
'ziniao_account_name': item.get('ziniaoAccountName') or '',
|
||||||
'ziniao_token': item.get('ziniaoToken') or '',
|
'ziniao_token': item.get('ziniaoToken') or '',
|
||||||
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
|
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
|
||||||
@@ -2345,6 +2350,51 @@ def list_dedupe_total_data():
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@admin_api.route('/dedupe-total-data/export')
|
||||||
|
@login_required
|
||||||
|
def export_dedupe_total_data():
|
||||||
|
_, current_row, denied = _ensure_dedupe_total_data_access()
|
||||||
|
if denied:
|
||||||
|
return denied
|
||||||
|
params = {'operatorId': current_row.get('id')}
|
||||||
|
username = (request.args.get('username') or '').strip()
|
||||||
|
start_date = (request.args.get('start_date') or request.args.get('startDate') or '').strip()
|
||||||
|
end_date = (request.args.get('end_date') or request.args.get('endDate') or '').strip()
|
||||||
|
if username:
|
||||||
|
params['username'] = username
|
||||||
|
if start_date:
|
||||||
|
params['startDate'] = start_date
|
||||||
|
if end_date:
|
||||||
|
params['endDate'] = end_date
|
||||||
|
|
||||||
|
url = f"{backend_java_base_url}/api/admin/dedupe-total-data/export"
|
||||||
|
try:
|
||||||
|
resp = _get_backend_java_session().get(url, params=params, timeout=60)
|
||||||
|
except requests.RequestException:
|
||||||
|
return jsonify({'success': False, 'error': 'backend-java 服务不可用'}), 502
|
||||||
|
if resp.status_code >= 400:
|
||||||
|
try:
|
||||||
|
data = resp.json()
|
||||||
|
error = data.get('message') or data.get('error') or '导出失败'
|
||||||
|
except ValueError:
|
||||||
|
error = '导出失败'
|
||||||
|
return jsonify({'success': False, 'error': error}), resp.status_code
|
||||||
|
|
||||||
|
headers = {}
|
||||||
|
disposition = resp.headers.get('Content-Disposition')
|
||||||
|
if disposition:
|
||||||
|
headers['Content-Disposition'] = disposition
|
||||||
|
return Response(
|
||||||
|
resp.content,
|
||||||
|
status=resp.status_code,
|
||||||
|
headers=headers,
|
||||||
|
content_type=resp.headers.get(
|
||||||
|
'Content-Type',
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin_api.route('/dedupe-total-data/import/<import_id>')
|
@admin_api.route('/dedupe-total-data/import/<import_id>')
|
||||||
@login_required
|
@login_required
|
||||||
def dedupe_total_data_import_progress(import_id):
|
def dedupe_total_data_import_progress(import_id):
|
||||||
|
|||||||
@@ -1467,6 +1467,43 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
document.getElementById('btnSearchDedupeTotalData').onclick = function () { loadDedupeTotalData(1); };
|
document.getElementById('btnSearchDedupeTotalData').onclick = function () { loadDedupeTotalData(1); };
|
||||||
|
document.getElementById('btnExportDedupeTotalData').onclick = function () {
|
||||||
|
var username = (document.getElementById('searchDedupeTotalDataUsername').value || '').trim();
|
||||||
|
var startDate = document.getElementById('exportDedupeTotalDataStartDate').value || '';
|
||||||
|
var endDate = document.getElementById('exportDedupeTotalDataEndDate').value || '';
|
||||||
|
if (startDate && endDate && startDate > endDate) {
|
||||||
|
alert('开始日期不能晚于结束日期');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var params = [];
|
||||||
|
if (username) params.push('username=' + encodeURIComponent(username));
|
||||||
|
if (startDate) params.push('start_date=' + encodeURIComponent(startDate));
|
||||||
|
if (endDate) params.push('end_date=' + encodeURIComponent(endDate));
|
||||||
|
fetch('/api/admin/dedupe-total-data/export' + (params.length ? ('?' + params.join('&')) : ''))
|
||||||
|
.then(function (response) {
|
||||||
|
var contentType = response.headers.get('content-type') || '';
|
||||||
|
if (!response.ok || contentType.indexOf('application/json') >= 0) {
|
||||||
|
return response.json().then(function (res) {
|
||||||
|
throw new Error((res && (res.error || res.msg)) || '导出失败');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return response.blob().then(function (blob) {
|
||||||
|
return {
|
||||||
|
blob: blob,
|
||||||
|
filename: extractDownloadFilename(
|
||||||
|
response.headers.get('content-disposition'),
|
||||||
|
'dedupe-total-data.xlsx'
|
||||||
|
)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(function (payload) {
|
||||||
|
triggerBrowserDownload(payload.blob, payload.filename);
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
alert((err && err.message) || '导出失败');
|
||||||
|
});
|
||||||
|
};
|
||||||
var dedupeImportPollTimer = null;
|
var dedupeImportPollTimer = null;
|
||||||
var dedupeDeleteImportPollTimer = null;
|
var dedupeDeleteImportPollTimer = null;
|
||||||
function stopDedupeImportProgress() {
|
function stopDedupeImportProgress() {
|
||||||
@@ -1858,11 +1895,11 @@
|
|||||||
}
|
}
|
||||||
var items = res.items || [];
|
var items = res.items || [];
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
tbody.innerHTML = '<tr><td colspan="6" class="empty-tip">暂无店铺密钥</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="7" class="empty-tip">暂无店铺密钥</td></tr>';
|
||||||
} else {
|
} else {
|
||||||
tbody.innerHTML = items.map(function (item, index) {
|
tbody.innerHTML = items.map(function (item, index) {
|
||||||
var rowNo = (shopKeyPage - 1) * shopKeyPageSize + index + 1;
|
var rowNo = (shopKeyPage - 1) * shopKeyPageSize + index + 1;
|
||||||
return '<tr><td>' + rowNo + '</td><td>' + (item.ziniao_account_name || '') + '</td><td>' + (item.ziniao_token || '') + '</td><td>' + (item.created_at || '') + '</td><td>' + (item.updated_at || '') + '</td><td>' +
|
return '<tr><td>' + rowNo + '</td><td>' + (item.remark_name || '') + '</td><td>' + (item.ziniao_account_name || '') + '</td><td>' + (item.ziniao_token || '') + '</td><td>' + (item.created_at || '') + '</td><td>' + (item.updated_at || '') + '</td><td>' +
|
||||||
'<button class="btn btn-sm" data-shop-key-edit="' + item.id + '" data-shop-key="' + (JSON.stringify(item).replace(/"/g, '"')) + '">编辑</button> ' +
|
'<button class="btn btn-sm" data-shop-key-edit="' + item.id + '" data-shop-key="' + (JSON.stringify(item).replace(/"/g, '"')) + '">编辑</button> ' +
|
||||||
'<button class="btn btn-sm btn-danger" data-shop-key-delete="' + item.id + '" data-ziniao-account-name="' + (item.ziniao_account_name || '').replace(/"/g, '"') + '">删除</button>' +
|
'<button class="btn btn-sm btn-danger" data-shop-key-delete="' + item.id + '" data-ziniao-account-name="' + (item.ziniao_account_name || '').replace(/"/g, '"') + '">删除</button>' +
|
||||||
'</td></tr>';
|
'</td></tr>';
|
||||||
@@ -1881,6 +1918,7 @@
|
|||||||
var item = {};
|
var item = {};
|
||||||
try { item = JSON.parse((btn.dataset.shopKey || '').replace(/"/g, '"')); } catch (e) { item = {}; }
|
try { item = JSON.parse((btn.dataset.shopKey || '').replace(/"/g, '"')); } catch (e) { item = {}; }
|
||||||
document.getElementById('editShopKeyId').value = item.id || '';
|
document.getElementById('editShopKeyId').value = item.id || '';
|
||||||
|
document.getElementById('editShopKeyRemarkName').value = item.remark_name || '';
|
||||||
document.getElementById('editShopKeyZiniaoAccountName').value = item.ziniao_account_name || '';
|
document.getElementById('editShopKeyZiniaoAccountName').value = item.ziniao_account_name || '';
|
||||||
document.getElementById('editShopKeyZiniaoToken').value = item.ziniao_token || '';
|
document.getElementById('editShopKeyZiniaoToken').value = item.ziniao_token || '';
|
||||||
document.getElementById('msgEditShopKey').textContent = '';
|
document.getElementById('msgEditShopKey').textContent = '';
|
||||||
@@ -1902,6 +1940,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
document.getElementById('btnCreateShopKey').onclick = function () {
|
document.getElementById('btnCreateShopKey').onclick = function () {
|
||||||
|
var remarkName = (document.getElementById('shopKeyRemarkName').value || '').trim();
|
||||||
var ziniaoAccountName = (document.getElementById('shopKeyZiniaoAccountName').value || '').trim();
|
var ziniaoAccountName = (document.getElementById('shopKeyZiniaoAccountName').value || '').trim();
|
||||||
var ziniaoToken = (document.getElementById('shopKeyZiniaoToken').value || '').trim();
|
var ziniaoToken = (document.getElementById('shopKeyZiniaoToken').value || '').trim();
|
||||||
var msgEl = document.getElementById('msgShopKey');
|
var msgEl = document.getElementById('msgShopKey');
|
||||||
@@ -1916,6 +1955,7 @@
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
remark_name: remarkName,
|
||||||
ziniao_account_name: ziniaoAccountName,
|
ziniao_account_name: ziniaoAccountName,
|
||||||
ziniao_token: ziniaoToken
|
ziniao_token: ziniaoToken
|
||||||
})
|
})
|
||||||
@@ -1923,6 +1963,7 @@
|
|||||||
.then(function (r) { return r.json(); })
|
.then(function (r) { return r.json(); })
|
||||||
.then(function (res) {
|
.then(function (res) {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
|
document.getElementById('shopKeyRemarkName').value = '';
|
||||||
document.getElementById('shopKeyZiniaoAccountName').value = '';
|
document.getElementById('shopKeyZiniaoAccountName').value = '';
|
||||||
document.getElementById('shopKeyZiniaoToken').value = '';
|
document.getElementById('shopKeyZiniaoToken').value = '';
|
||||||
msgEl.textContent = res.msg || '创建成功';
|
msgEl.textContent = res.msg || '创建成功';
|
||||||
@@ -1940,6 +1981,7 @@
|
|||||||
};
|
};
|
||||||
document.getElementById('btnSaveShopKey').onclick = function () {
|
document.getElementById('btnSaveShopKey').onclick = function () {
|
||||||
var itemId = document.getElementById('editShopKeyId').value;
|
var itemId = document.getElementById('editShopKeyId').value;
|
||||||
|
var remarkName = (document.getElementById('editShopKeyRemarkName').value || '').trim();
|
||||||
var ziniaoAccountName = (document.getElementById('editShopKeyZiniaoAccountName').value || '').trim();
|
var ziniaoAccountName = (document.getElementById('editShopKeyZiniaoAccountName').value || '').trim();
|
||||||
var ziniaoToken = (document.getElementById('editShopKeyZiniaoToken').value || '').trim();
|
var ziniaoToken = (document.getElementById('editShopKeyZiniaoToken').value || '').trim();
|
||||||
var msgEl = document.getElementById('msgEditShopKey');
|
var msgEl = document.getElementById('msgEditShopKey');
|
||||||
@@ -1954,6 +1996,7 @@
|
|||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
remark_name: remarkName,
|
||||||
ziniao_account_name: ziniaoAccountName,
|
ziniao_account_name: ziniaoAccountName,
|
||||||
ziniao_token: ziniaoToken
|
ziniao_token: ziniaoToken
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import io
|
import io
|
||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
from flask import jsonify
|
from flask import jsonify
|
||||||
|
|
||||||
@@ -72,6 +72,36 @@ class DedupeTotalDataAdminTest(unittest.TestCase):
|
|||||||
self.assertEqual('import-1', response.get_json()['import_id'])
|
self.assertEqual('import-1', response.get_json()['import_id'])
|
||||||
self.assertEqual({'operatorId': 31}, proxy.call_args.kwargs['data'])
|
self.assertEqual({'operatorId': 31}, proxy.call_args.kwargs['data'])
|
||||||
|
|
||||||
|
def test_export_forwards_filters_and_session_operator(self):
|
||||||
|
self._login(31)
|
||||||
|
java_response = Mock(
|
||||||
|
status_code=200,
|
||||||
|
content=b'xlsx-data',
|
||||||
|
headers={
|
||||||
|
'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
'Content-Disposition': 'attachment; filename="dedupe-total-data.xlsx"',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
java_session = Mock()
|
||||||
|
java_session.get.return_value = java_response
|
||||||
|
with patch('utils.auth.is_session_user_valid', return_value=True), \
|
||||||
|
patch.object(admin_module, '_ensure_dedupe_total_data_access',
|
||||||
|
return_value=('normal', {'id': 31, 'username': 'member-b'}, None)), \
|
||||||
|
patch.object(admin_module, '_get_backend_java_session', return_value=java_session):
|
||||||
|
response = self.client.get(
|
||||||
|
'/api/admin/dedupe-total-data/export'
|
||||||
|
'?username=member&start_date=2026-07-01&end_date=2026-07-20')
|
||||||
|
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
self.assertEqual(b'xlsx-data', response.data)
|
||||||
|
self.assertEqual({
|
||||||
|
'operatorId': 31,
|
||||||
|
'username': 'member',
|
||||||
|
'startDate': '2026-07-01',
|
||||||
|
'endDate': '2026-07-20',
|
||||||
|
}, java_session.get.call_args.kwargs['params'])
|
||||||
|
self.assertEqual(60, java_session.get.call_args.kwargs['timeout'])
|
||||||
|
|
||||||
def test_delete_ignores_client_operator_and_uses_session_user(self):
|
def test_delete_ignores_client_operator_and_uses_session_user(self):
|
||||||
self._login(41)
|
self._login(41)
|
||||||
java_response = {'success': True, 'message': '删除成功', 'data': None}
|
java_response = {'success': True, 'message': '删除成功', 'data': None}
|
||||||
|
|||||||
@@ -162,6 +162,26 @@
|
|||||||
min-width: 120px;
|
min-width: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dedupe-filter-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(160px, 1fr)) auto;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: end;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dedupe-filter-row .form-group {
|
||||||
|
min-width: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dedupe-filter-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
background: #667eea;
|
background: #667eea;
|
||||||
@@ -651,6 +671,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
|
.dedupe-filter-row {
|
||||||
|
grid-template-columns: repeat(2, minmax(180px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.dedupe-filter-actions {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
.dedupe-group-access {
|
.dedupe-group-access {
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -1282,6 +1310,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
|
.dedupe-filter-row {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dedupe-filter-actions {
|
||||||
|
grid-column: auto;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-user-box {
|
.admin-user-box {
|
||||||
position: static !important;
|
position: static !important;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
@@ -1608,16 +1645,27 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="panel-box">
|
<div class="panel-box">
|
||||||
<h3 style="margin-bottom:16px;font-size:15px;">ASIN列表</h3>
|
<h3 style="margin-bottom:16px;font-size:15px;">ASIN列表</h3>
|
||||||
<div class="form-row" style="margin-bottom:16px;">
|
<div class="dedupe-filter-row">
|
||||||
<div class="form-group" style="min-width:220px;">
|
<div class="form-group">
|
||||||
<label>数据值(模糊搜索)</label>
|
<label>数据值(模糊搜索)</label>
|
||||||
<input type="text" id="searchDedupeTotalData" placeholder="输入关键字">
|
<input type="text" id="searchDedupeTotalData" placeholder="输入关键字">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="min-width:220px;">
|
<div class="form-group">
|
||||||
<label>用户名(模糊搜索)</label>
|
<label>用户名(模糊搜索)</label>
|
||||||
<input type="text" id="searchDedupeTotalDataUsername" placeholder="输入用户名">
|
<input type="text" id="searchDedupeTotalDataUsername" placeholder="输入用户名">
|
||||||
</div>
|
</div>
|
||||||
<button class=" btn" id="btnSearchDedupeTotalData">查询</button>
|
<div class="form-group">
|
||||||
|
<label>导出开始日期</label>
|
||||||
|
<input type="date" id="exportDedupeTotalDataStartDate">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>导出结束日期</label>
|
||||||
|
<input type="date" id="exportDedupeTotalDataEndDate">
|
||||||
|
</div>
|
||||||
|
<div class="dedupe-filter-actions">
|
||||||
|
<button class="btn" id="btnSearchDedupeTotalData">查询</button>
|
||||||
|
<button class="btn btn-secondary" id="btnExportDedupeTotalData" type="button">导出 XLSX</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -1682,6 +1730,10 @@
|
|||||||
<div class="form-box">
|
<div class="form-box">
|
||||||
<h3 style="margin-bottom:16px;font-size:15px;">新增店铺密钥</h3>
|
<h3 style="margin-bottom:16px;font-size:15px;">新增店铺密钥</h3>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
|
<div class="form-group" style="min-width:180px;">
|
||||||
|
<label>备注名</label>
|
||||||
|
<input type="text" id="shopKeyRemarkName" maxlength="128" placeholder="请输入备注名(可选)">
|
||||||
|
</div>
|
||||||
<div class="form-group" style="min-width:220px;">
|
<div class="form-group" style="min-width:220px;">
|
||||||
<label>紫鸟账号名称</label>
|
<label>紫鸟账号名称</label>
|
||||||
<input type="text" id="shopKeyZiniaoAccountName" placeholder="请输入紫鸟账号名称">
|
<input type="text" id="shopKeyZiniaoAccountName" placeholder="请输入紫鸟账号名称">
|
||||||
@@ -1700,6 +1752,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>序号</th>
|
<th>序号</th>
|
||||||
|
<th>备注名</th>
|
||||||
<th>紫鸟账号名称</th>
|
<th>紫鸟账号名称</th>
|
||||||
<th>紫鸟令牌</th>
|
<th>紫鸟令牌</th>
|
||||||
<th>创建时间</th>
|
<th>创建时间</th>
|
||||||
@@ -2503,6 +2556,10 @@
|
|||||||
<div class="modal">
|
<div class="modal">
|
||||||
<h3>编辑店铺密钥</h3>
|
<h3>编辑店铺密钥</h3>
|
||||||
<input type="hidden" id="editShopKeyId">
|
<input type="hidden" id="editShopKeyId">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>备注名</label>
|
||||||
|
<input type="text" id="editShopKeyRemarkName" maxlength="128" placeholder="备注名(可选)">
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>紫鸟账号名称</label>
|
<label>紫鸟账号名称</label>
|
||||||
<input type="text" id="editShopKeyZiniaoAccountName" placeholder="紫鸟账号名称">
|
<input type="text" id="editShopKeyZiniaoAccountName" placeholder="紫鸟账号名称">
|
||||||
@@ -2550,7 +2607,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/admin.js?v=dedupe-groups-1"></script>
|
<script src="/static/admin.js?v=shop-key-remark-1"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user