后台去重数据新增国家

This commit is contained in:
2026-08-23 22:50:58 +08:00
parent 5a750be066
commit 8712df2645
12 changed files with 294 additions and 37 deletions
@@ -87,10 +87,11 @@ public class DedupeTotalDataController {
@Parameter(description = "结束日期(包含)") @Parameter(description = "结束日期(包含)")
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate,
@Parameter(description = "分组 ID") @RequestParam(required = false) Long groupId, @Parameter(description = "分组 ID") @RequestParam(required = false) Long groupId,
@Parameter(description = "国家代码(如 DE、UK") @RequestParam(required = false) String country,
HttpServletRequest request) { HttpServletRequest request) {
RequestOperator operator = requireDedupeTotalDataAccess(request); RequestOperator operator = requireDedupeTotalDataAccess(request);
return ApiResponse.success(dedupeTotalDataService.page( return ApiResponse.success(dedupeTotalDataService.page(
page, pageSize, keyword, username, startDate, endDate, groupId, operator.id())); page, pageSize, keyword, username, startDate, endDate, groupId, country, operator.id()));
} }
@GetMapping("/export") @GetMapping("/export")
@@ -102,6 +103,7 @@ public class DedupeTotalDataController {
@Parameter(description = "结束日期(包含)") @Parameter(description = "结束日期(包含)")
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate,
@Parameter(description = "分组 ID") @RequestParam(required = false) Long groupId, @Parameter(description = "分组 ID") @RequestParam(required = false) Long groupId,
@Parameter(description = "国家代码(如 DE、UK") @RequestParam(required = false) String country,
HttpServletRequest request) { HttpServletRequest request) {
if (startDate != null && endDate != null && startDate.isAfter(endDate)) { if (startDate != null && endDate != null && startDate.isAfter(endDate)) {
throw new BusinessException(400, "invalid export date range"); throw new BusinessException(400, "invalid export date range");
@@ -112,9 +114,9 @@ public class DedupeTotalDataController {
+ (monthlyZip ? ".zip" : ".xlsx"); + (monthlyZip ? ".zip" : ".xlsx");
StreamingResponseBody body = monthlyZip StreamingResponseBody body = monthlyZip
? outputStream -> dedupeTotalDataService.writeMonthlyZipExport( ? outputStream -> dedupeTotalDataService.writeMonthlyZipExport(
outputStream, username, startDate, endDate, groupId, operator.id()) outputStream, username, startDate, endDate, groupId, country, operator.id())
: outputStream -> dedupeTotalDataService.writeExport( : outputStream -> dedupeTotalDataService.writeExport(
outputStream, username, startDate, endDate, groupId, operator.id()); outputStream, username, startDate, endDate, groupId, country, operator.id());
return ResponseEntity.ok() return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, DownloadHeaderUtil.contentDisposition(filename)) .header(HttpHeaders.CONTENT_DISPOSITION, DownloadHeaderUtil.contentDisposition(filename))
.contentType(MediaType.parseMediaType(monthlyZip ? ZIP_CONTENT_TYPE : XLSX_CONTENT_TYPE)) .contentType(MediaType.parseMediaType(monthlyZip ? ZIP_CONTENT_TYPE : XLSX_CONTENT_TYPE))
@@ -28,10 +28,10 @@ public interface DedupeTotalDataMapper extends BaseMapper<DedupeTotalDataEntity>
@Insert(""" @Insert("""
<script> <script>
INSERT IGNORE INTO biz_dedupe_total_data INSERT IGNORE INTO biz_dedupe_total_data
(data_value, group_id, uploader_user_id, uploader_username) (data_value, country, group_id, uploader_user_id, uploader_username)
VALUES VALUES
<foreach collection="rows" item="row" separator=","> <foreach collection="rows" item="row" separator=",">
(#{row.dataValue}, #{row.groupId}, #{row.uploaderUserId}, #{row.uploaderUsername}) (#{row.dataValue}, #{row.country}, #{row.groupId}, #{row.uploaderUserId}, #{row.uploaderUsername})
</foreach> </foreach>
</script> </script>
""") """)
@@ -14,6 +14,7 @@ public class DedupeTotalDataEntity {
@TableId(type = IdType.AUTO) @TableId(type = IdType.AUTO)
private Long id; private Long id;
private String dataValue; private String dataValue;
private String country;
private Long groupId; private Long groupId;
private Long uploaderUserId; private Long uploaderUserId;
private String uploaderUsername; private String uploaderUsername;
@@ -15,6 +15,9 @@ public class DedupeTotalDataItemVo {
@Schema(description = "总数据值") @Schema(description = "总数据值")
private String dataValue; private String dataValue;
@Schema(description = "国家代码,逗号分隔,如 DE,UK")
private String country;
@Schema(description = "分组 ID") @Schema(description = "分组 ID")
private Long groupId; private Long groupId;
@@ -94,6 +94,11 @@ public class DedupeTotalDataService {
public DedupeTotalDataPageVo page(long page, long pageSize, String keyword, String username, public DedupeTotalDataPageVo page(long page, long pageSize, String keyword, String username,
LocalDate startDate, LocalDate endDate, Long groupId, Long operatorId) { LocalDate startDate, LocalDate endDate, Long groupId, Long operatorId) {
return page(page, pageSize, keyword, username, startDate, endDate, groupId, null, operatorId);
}
public DedupeTotalDataPageVo page(long page, long pageSize, String keyword, String username,
LocalDate startDate, LocalDate endDate, Long groupId, String country, Long operatorId) {
if (startDate != null && endDate != null && startDate.isAfter(endDate)) { if (startDate != null && endDate != null && startDate.isAfter(endDate)) {
throw new BusinessException("开始日期不能晚于结束日期"); throw new BusinessException("开始日期不能晚于结束日期");
} }
@@ -101,10 +106,14 @@ public class DedupeTotalDataService {
long safePageSize = Math.min(Math.max(pageSize, 1), 100); long safePageSize = Math.min(Math.max(pageSize, 1), 100);
String safeKeyword = keyword == null ? "" : keyword.trim(); String safeKeyword = keyword == null ? "" : keyword.trim();
String safeUsername = username == null ? "" : username.trim(); String safeUsername = username == null ? "" : username.trim();
String safeCountry = country == null ? "" : country.trim();
AccessScope scope = resolveAccessScope(operatorId); AccessScope scope = resolveAccessScope(operatorId);
LambdaQueryWrapper<DedupeTotalDataEntity> query = new LambdaQueryWrapper<DedupeTotalDataEntity>() LambdaQueryWrapper<DedupeTotalDataEntity> query = new LambdaQueryWrapper<DedupeTotalDataEntity>()
.like(!safeKeyword.isEmpty(), DedupeTotalDataEntity::getDataValue, safeKeyword) .like(!safeKeyword.isEmpty(), DedupeTotalDataEntity::getDataValue, safeKeyword)
.like(!safeUsername.isEmpty(), DedupeTotalDataEntity::getUploaderUsername, safeUsername) .like(!safeUsername.isEmpty(), DedupeTotalDataEntity::getUploaderUsername, safeUsername)
.apply(!safeCountry.isEmpty(),
"FIND_IN_SET({0}, IFNULL(country, '')) > 0",
safeCountry)
.ge(startDate != null, DedupeTotalDataEntity::getCreatedAt, .ge(startDate != null, DedupeTotalDataEntity::getCreatedAt,
startDate == null ? null : startDate.atStartOfDay()) startDate == null ? null : startDate.atStartOfDay())
.lt(endDate != null, DedupeTotalDataEntity::getCreatedAt, .lt(endDate != null, DedupeTotalDataEntity::getCreatedAt,
@@ -134,13 +143,18 @@ public class DedupeTotalDataService {
public byte[] export(String username, LocalDate startDate, LocalDate endDate, public byte[] export(String username, LocalDate startDate, LocalDate endDate,
Long groupId, Long operatorId) { Long groupId, Long operatorId) {
return export(username, startDate, endDate, groupId, null, operatorId);
}
public byte[] export(String username, LocalDate startDate, LocalDate endDate,
Long groupId, String country, Long operatorId) {
if (startDate != null && endDate != null && startDate.isAfter(endDate)) { if (startDate != null && endDate != null && startDate.isAfter(endDate)) {
throw new BusinessException("开始日期不能晚于结束日期"); throw new BusinessException("开始日期不能晚于结束日期");
} }
String safeUsername = username == null ? "" : username.trim(); String safeUsername = username == null ? "" : username.trim();
AccessScope scope = resolveAccessScope(operatorId); AccessScope scope = resolveAccessScope(operatorId);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
buildExportWorkbook(outputStream, safeUsername, startDate, endDate, groupId, scope); buildExportWorkbook(outputStream, safeUsername, startDate, endDate, groupId, country, scope);
return outputStream.toByteArray(); return outputStream.toByteArray();
} }
@@ -150,6 +164,16 @@ public class DedupeTotalDataService {
LocalDate endDate, LocalDate endDate,
Long groupId, Long groupId,
Long operatorId) { Long operatorId) {
writeExport(outputStream, username, startDate, endDate, groupId, null, operatorId);
}
public void writeExport(OutputStream outputStream,
String username,
LocalDate startDate,
LocalDate endDate,
Long groupId,
String country,
Long operatorId) {
if (outputStream == null) { if (outputStream == null) {
throw new BusinessException("导出输出流不能为空"); throw new BusinessException("导出输出流不能为空");
} }
@@ -158,7 +182,7 @@ public class DedupeTotalDataService {
} }
String safeUsername = username == null ? "" : username.trim(); String safeUsername = username == null ? "" : username.trim();
AccessScope scope = resolveAccessScope(operatorId); AccessScope scope = resolveAccessScope(operatorId);
buildExportWorkbook(outputStream, safeUsername, startDate, endDate, groupId, scope); buildExportWorkbook(outputStream, safeUsername, startDate, endDate, groupId, country, scope);
} }
public void writeMonthlyZipExport(OutputStream outputStream, public void writeMonthlyZipExport(OutputStream outputStream,
@@ -167,17 +191,28 @@ public class DedupeTotalDataService {
LocalDate endDate, LocalDate endDate,
Long groupId, Long groupId,
Long operatorId) { Long operatorId) {
writeMonthlyZipExport(outputStream, username, startDate, endDate, groupId, null, operatorId);
}
public void writeMonthlyZipExport(OutputStream outputStream,
String username,
LocalDate startDate,
LocalDate endDate,
Long groupId,
String country,
Long operatorId) {
if (outputStream == null) { if (outputStream == null) {
throw new BusinessException("export output stream cannot be null"); throw new BusinessException("export output stream cannot be null");
} }
if (startDate == null || endDate == null || YearMonth.from(startDate).equals(YearMonth.from(endDate))) { if (startDate == null || endDate == null || YearMonth.from(startDate).equals(YearMonth.from(endDate))) {
writeExport(outputStream, username, startDate, endDate, groupId, operatorId); writeExport(outputStream, username, startDate, endDate, groupId, country, operatorId);
return; return;
} }
if (startDate.isAfter(endDate)) { if (startDate.isAfter(endDate)) {
throw new BusinessException("invalid export date range"); throw new BusinessException("invalid export date range");
} }
String safeUsername = username == null ? "" : username.trim(); String safeUsername = username == null ? "" : username.trim();
String safeCountry = country == null ? "" : country.trim();
AccessScope scope = resolveAccessScope(operatorId); AccessScope scope = resolveAccessScope(operatorId);
boolean groupPrevalidated = groupId != null && groupId > 0; boolean groupPrevalidated = groupId != null && groupId > 0;
if (groupPrevalidated) { if (groupPrevalidated) {
@@ -194,7 +229,7 @@ public class DedupeTotalDataService {
LocalDate entryEndDate = endDate.isBefore(monthEnd) ? endDate : monthEnd; LocalDate entryEndDate = endDate.isBefore(monthEnd) ? endDate : monthEnd;
zipOutputStream.putNextEntry(new ZipEntry(monthlyExportEntryName(currentMonth))); zipOutputStream.putNextEntry(new ZipEntry(monthlyExportEntryName(currentMonth)));
buildExportWorkbook(zipOutputStream, safeUsername, entryStartDate, entryEndDate, buildExportWorkbook(zipOutputStream, safeUsername, entryStartDate, entryEndDate,
groupId, scope, groupPrevalidated); groupId, safeCountry, scope, groupPrevalidated);
zipOutputStream.closeEntry(); zipOutputStream.closeEntry();
currentMonth = currentMonth.plusMonths(1); currentMonth = currentMonth.plusMonths(1);
} }
@@ -211,8 +246,9 @@ public class DedupeTotalDataService {
LocalDate startDate, LocalDate startDate,
LocalDate endDate, LocalDate endDate,
Long groupId, Long groupId,
String country,
AccessScope scope) { AccessScope scope) {
buildExportWorkbook(outputStream, username, startDate, endDate, groupId, scope, false); buildExportWorkbook(outputStream, username, startDate, endDate, groupId, country, scope, false);
} }
private void buildExportWorkbook(OutputStream outputStream, private void buildExportWorkbook(OutputStream outputStream,
@@ -220,6 +256,7 @@ public class DedupeTotalDataService {
LocalDate startDate, LocalDate startDate,
LocalDate endDate, LocalDate endDate,
Long groupId, Long groupId,
String country,
AccessScope scope, AccessScope scope,
boolean groupPrevalidated) { boolean groupPrevalidated) {
try (SXSSFWorkbook workbook = new SXSSFWorkbook(100)) { try (SXSSFWorkbook workbook = new SXSSFWorkbook(100)) {
@@ -227,14 +264,15 @@ public class DedupeTotalDataService {
Row header = sheet.createRow(0); Row header = sheet.createRow(0);
header.createCell(0).setCellValue("ID"); header.createCell(0).setCellValue("ID");
header.createCell(1).setCellValue("ASIN值"); header.createCell(1).setCellValue("ASIN值");
header.createCell(2).setCellValue("用户名"); header.createCell(2).setCellValue("国家");
header.createCell(3).setCellValue("分组"); header.createCell(3).setCellValue("用户名");
header.createCell(4).setCellValue("创建时间"); header.createCell(4).setCellValue("分组");
header.createCell(5).setCellValue("创建时间");
Long lastId = null; Long lastId = null;
int rowIndex = 1; int rowIndex = 1;
while (true) { while (true) {
LambdaQueryWrapper<DedupeTotalDataEntity> pageQuery = buildExportQuery( LambdaQueryWrapper<DedupeTotalDataEntity> pageQuery = buildExportQuery(
username, startDate, endDate, groupId, scope, groupPrevalidated); username, startDate, endDate, groupId, country, scope, groupPrevalidated);
if (lastId != null) { if (lastId != null) {
pageQuery.lt(DedupeTotalDataEntity::getId, lastId); pageQuery.lt(DedupeTotalDataEntity::getId, lastId);
} }
@@ -248,11 +286,12 @@ public class DedupeTotalDataService {
Row row = sheet.createRow(rowIndex++); Row row = sheet.createRow(rowIndex++);
row.createCell(0).setCellValue(entity.getId() == null ? "" : String.valueOf(entity.getId())); row.createCell(0).setCellValue(entity.getId() == null ? "" : String.valueOf(entity.getId()));
row.createCell(1).setCellValue(entity.getDataValue() == null ? "" : entity.getDataValue()); row.createCell(1).setCellValue(entity.getDataValue() == null ? "" : entity.getDataValue());
row.createCell(2).setCellValue(entity.getUploaderUsername() == null ? "" : entity.getUploaderUsername()); row.createCell(2).setCellValue(entity.getCountry() == null ? "" : entity.getCountry());
row.createCell(3).setCellValue(entity.getGroupId() == null row.createCell(3).setCellValue(entity.getUploaderUsername() == null ? "" : entity.getUploaderUsername());
row.createCell(4).setCellValue(entity.getGroupId() == null
? "" ? ""
: groupNames.getOrDefault(entity.getGroupId(), "")); : groupNames.getOrDefault(entity.getGroupId(), ""));
row.createCell(4).setCellValue(formatExportTime(entity.getCreatedAt())); row.createCell(5).setCellValue(formatExportTime(entity.getCreatedAt()));
} }
DedupeTotalDataEntity lastRow = rows.getLast(); DedupeTotalDataEntity lastRow = rows.getLast();
lastId = lastRow == null ? null : lastRow.getId(); lastId = lastRow == null ? null : lastRow.getId();
@@ -265,9 +304,10 @@ public class DedupeTotalDataService {
} }
sheet.setColumnWidth(0, 3600); sheet.setColumnWidth(0, 3600);
sheet.setColumnWidth(1, 5200); sheet.setColumnWidth(1, 5200);
sheet.setColumnWidth(2, 5200); sheet.setColumnWidth(2, 3200);
sheet.setColumnWidth(3, 5200); sheet.setColumnWidth(3, 5200);
sheet.setColumnWidth(4, 5600); sheet.setColumnWidth(4, 5200);
sheet.setColumnWidth(5, 5600);
workbook.write(outputStream); workbook.write(outputStream);
workbook.dispose(); workbook.dispose();
} catch (Exception ex) { } catch (Exception ex) {
@@ -280,18 +320,23 @@ public class DedupeTotalDataService {
LocalDate endDate, LocalDate endDate,
Long groupId, Long groupId,
AccessScope scope) { AccessScope scope) {
return buildExportQuery(username, startDate, endDate, groupId, scope, false); return buildExportQuery(username, startDate, endDate, groupId, null, scope, false);
} }
private LambdaQueryWrapper<DedupeTotalDataEntity> buildExportQuery(String username, private LambdaQueryWrapper<DedupeTotalDataEntity> buildExportQuery(String username,
LocalDate startDate, LocalDate startDate,
LocalDate endDate, LocalDate endDate,
Long groupId, Long groupId,
String country,
AccessScope scope, AccessScope scope,
boolean groupPrevalidated) { boolean groupPrevalidated) {
String safeUsername = username == null ? "" : username.trim(); String safeUsername = username == null ? "" : username.trim();
String safeCountry = country == null ? "" : country.trim();
LambdaQueryWrapper<DedupeTotalDataEntity> query = new LambdaQueryWrapper<DedupeTotalDataEntity>() LambdaQueryWrapper<DedupeTotalDataEntity> query = new LambdaQueryWrapper<DedupeTotalDataEntity>()
.like(!safeUsername.isEmpty(), DedupeTotalDataEntity::getUploaderUsername, safeUsername) .like(!safeUsername.isEmpty(), DedupeTotalDataEntity::getUploaderUsername, safeUsername)
.apply(!safeCountry.isEmpty(),
"FIND_IN_SET({0}, IFNULL(country, '')) > 0",
safeCountry)
.ge(startDate != null, DedupeTotalDataEntity::getCreatedAt, .ge(startDate != null, DedupeTotalDataEntity::getCreatedAt,
startDate == null ? null : startDate.atStartOfDay()) startDate == null ? null : startDate.atStartOfDay())
.lt(endDate != null, DedupeTotalDataEntity::getCreatedAt, .lt(endDate != null, DedupeTotalDataEntity::getCreatedAt,
@@ -608,9 +653,11 @@ public class DedupeTotalDataService {
if (asinIndex == null) { if (asinIndex == null) {
throw new BusinessException("缺少 ASIN 列"); throw new BusinessException("缺少 ASIN 列");
} }
Integer countryIndex = headerMap.get("国家");
Set<String> seenInFile = new HashSet<>(); Set<String> seenInFile = new HashSet<>();
List<String> pendingValues = new ArrayList<>(IMPORT_BATCH_SIZE); List<String> pendingValues = new ArrayList<>(IMPORT_BATCH_SIZE);
Map<String, String> pendingCountries = new HashMap<>();
int totalRows = Math.max(sheet.getLastRowNum(), 0); int totalRows = Math.max(sheet.getLastRowNum(), 0);
int asinCount = 0; int asinCount = 0;
int insertedCount = 0; int insertedCount = 0;
@@ -644,21 +691,26 @@ public class DedupeTotalDataService {
continue; continue;
} }
pendingValues.add(asin); pendingValues.add(asin);
if (countryIndex != null) {
pendingCountries.put(asin, parseCountries(formatter.formatCellValue(row.getCell(countryIndex))));
}
if (pendingValues.size() >= IMPORT_BATCH_SIZE) { if (pendingValues.size() >= IMPORT_BATCH_SIZE) {
ImportBatchResult batch = insertImportBatch( ImportBatchResult batch = insertImportBatch(
pendingValues, groupId, uploaderUserId, uploaderUsername); pendingValues, pendingCountries, groupId, uploaderUserId, uploaderUsername);
insertedCount += batch.insertedCount(); insertedCount += batch.insertedCount();
skippedCount += batch.skippedCount(); skippedCount += batch.skippedCount();
pendingValues.clear(); pendingValues.clear();
pendingCountries.clear();
} }
updateImportProgress(progress, rowNum, asinCount, insertedCount, skippedCount); updateImportProgress(progress, rowNum, asinCount, insertedCount, skippedCount);
} }
if (!pendingValues.isEmpty()) { if (!pendingValues.isEmpty()) {
ImportBatchResult batch = insertImportBatch( ImportBatchResult batch = insertImportBatch(
pendingValues, groupId, uploaderUserId, uploaderUsername); pendingValues, pendingCountries, groupId, uploaderUserId, uploaderUsername);
insertedCount += batch.insertedCount(); insertedCount += batch.insertedCount();
skippedCount += batch.skippedCount(); skippedCount += batch.skippedCount();
pendingValues.clear(); pendingValues.clear();
pendingCountries.clear();
} }
updateImportProgress(progress, totalRows, asinCount, insertedCount, skippedCount); updateImportProgress(progress, totalRows, asinCount, insertedCount, skippedCount);
@@ -676,6 +728,7 @@ public class DedupeTotalDataService {
} }
private ImportBatchResult insertImportBatch(List<String> values, private ImportBatchResult insertImportBatch(List<String> values,
Map<String, String> countriesByValue,
Long groupId, Long groupId,
Long uploaderUserId, Long uploaderUserId,
String uploaderUsername) { String uploaderUsername) {
@@ -690,6 +743,7 @@ public class DedupeTotalDataService {
} }
DedupeTotalDataEntity entity = new DedupeTotalDataEntity(); DedupeTotalDataEntity entity = new DedupeTotalDataEntity();
entity.setDataValue(value); entity.setDataValue(value);
entity.setCountry(countriesByValue == null ? null : countriesByValue.get(value));
entity.setGroupId(groupId); entity.setGroupId(groupId);
entity.setUploaderUserId(uploaderUserId); entity.setUploaderUserId(uploaderUserId);
entity.setUploaderUsername(uploaderUsername); entity.setUploaderUsername(uploaderUsername);
@@ -1076,6 +1130,49 @@ public class DedupeTotalDataService {
.replaceAll("\\s+", " "); .replaceAll("\\s+", " ");
} }
private static final List<String> COUNTRY_CODE_MAP = List.of(
"DE", "UK", "FR", "IT", "ES", "US",
"美国", "德国", "英国", "法国", "意大利", "西班牙");
private String normalizeCountryCode(String raw) {
String upper = normalizeExcelText(raw).toUpperCase(Locale.ROOT);
return upper
.replace("GERMANY", "DE")
.replace("UNITED KINGDOM", "UK")
.replace("UNITEDKINGDOM", "UK")
.replace("ENGLAND", "UK")
.replace("FRANCE", "FR")
.replace("ITALY", "IT")
.replace("SPAIN", "ES")
.replace("USA", "US")
.replace("UNITED STATES", "US")
.replace("UNITEDSTATES", "US")
.replace("AMERICA", "US")
.replace("德国", "DE")
.replace("英国", "UK")
.replace("法国", "FR")
.replace("意大利", "IT")
.replace("西班牙", "ES")
.replace("美国", "US");
}
/** 将国家列单元格内容解析为标准国家代码(去重后按出现顺序),无有效国家返回 null。 */
private String parseCountries(String raw) {
if (raw == null || raw.isBlank()) {
return null;
}
// 先对整个单元格做别名归一(含多词国家名如 "United Kingdom"),再按分隔符/空格拆分。
String normalized = normalizeCountryCode(raw);
LinkedHashSet<String> codes = new LinkedHashSet<>();
String[] tokens = normalized.split("[,;;、/\\\\|\\s]+");
for (String token : tokens) {
if (COUNTRY_CODE_MAP.contains(token)) {
codes.add(token);
}
}
return codes.isEmpty() ? null : String.join(",", codes);
}
private Map<Long, String> loadGroupNames(List<DedupeTotalDataEntity> rows) { private Map<Long, String> loadGroupNames(List<DedupeTotalDataEntity> rows) {
List<Long> groupIds = rows.stream() List<Long> groupIds = rows.stream()
.map(DedupeTotalDataEntity::getGroupId) .map(DedupeTotalDataEntity::getGroupId)
@@ -1098,6 +1195,7 @@ public class DedupeTotalDataService {
DedupeTotalDataItemVo vo = new DedupeTotalDataItemVo(); DedupeTotalDataItemVo vo = new DedupeTotalDataItemVo();
vo.setId(entity.getId()); vo.setId(entity.getId());
vo.setDataValue(entity.getDataValue()); vo.setDataValue(entity.getDataValue());
vo.setCountry(entity.getCountry());
vo.setGroupId(entity.getGroupId()); vo.setGroupId(entity.getGroupId());
vo.setGroupName(groupName == null ? "" : groupName); vo.setGroupName(groupName == null ? "" : groupName);
vo.setUploaderUserId(entity.getUploaderUserId()); vo.setUploaderUserId(entity.getUploaderUserId());
@@ -0,0 +1,33 @@
SET @country_col_exists := (
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'biz_dedupe_total_data'
AND COLUMN_NAME = 'country'
);
SET @sql_add_country := IF(
@country_col_exists = 0,
'ALTER TABLE biz_dedupe_total_data ADD COLUMN country VARCHAR(64) DEFAULT NULL COMMENT ''国家代码,逗号分隔,如 DE,UK'' AFTER data_value',
'SELECT 1'
);
PREPARE stmt_add_country FROM @sql_add_country;
EXECUTE stmt_add_country;
DEALLOCATE PREPARE stmt_add_country;
SET @country_idx_exists := (
SELECT COUNT(*)
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'biz_dedupe_total_data'
AND INDEX_NAME = 'idx_country'
);
SET @sql_add_country_idx := IF(
@country_idx_exists = 0,
'ALTER TABLE biz_dedupe_total_data ADD INDEX idx_country (country)',
'SELECT 1'
);
PREPARE stmt_add_country_idx FROM @sql_add_country_idx;
EXECUTE stmt_add_country_idx;
DEALLOCATE PREPARE stmt_add_country_idx;
@@ -56,6 +56,7 @@ class DedupeTotalDataControllerTest {
eq(LocalDate.of(2026, 7, 1)), eq(LocalDate.of(2026, 7, 1)),
eq(LocalDate.of(2026, 7, 31)), eq(LocalDate.of(2026, 7, 31)),
eq(3L), eq(3L),
isNull(),
eq(8L)); eq(8L));
ResponseEntity<StreamingResponseBody> response = controller.export( ResponseEntity<StreamingResponseBody> response = controller.export(
@@ -63,6 +64,7 @@ class DedupeTotalDataControllerTest {
LocalDate.of(2026, 7, 1), LocalDate.of(2026, 7, 1),
LocalDate.of(2026, 7, 31), LocalDate.of(2026, 7, 31),
3L, 3L,
null,
request); request);
assertNotNull(response.getBody()); assertNotNull(response.getBody());
@@ -84,9 +86,10 @@ class DedupeTotalDataControllerTest {
eq(LocalDate.of(2026, 7, 1)), eq(LocalDate.of(2026, 7, 1)),
eq(LocalDate.of(2026, 7, 31)), eq(LocalDate.of(2026, 7, 31)),
eq(3L), eq(3L),
isNull(),
eq(8L)); eq(8L));
verify(service, never()).writeMonthlyZipExport( verify(service, never()).writeMonthlyZipExport(
any(OutputStream.class), any(), any(), any(), any(), any()); any(OutputStream.class), any(), any(), any(), any(), any(), any());
verifyNoInteractions(permissionMenuService); verifyNoInteractions(permissionMenuService);
} }
@@ -109,6 +112,7 @@ class DedupeTotalDataControllerTest {
null, null,
LocalDate.of(2026, 8, 2), LocalDate.of(2026, 8, 2),
3L, 3L,
null,
request); request);
assertEquals(MediaType.parseMediaType( assertEquals(MediaType.parseMediaType(
@@ -125,9 +129,10 @@ class DedupeTotalDataControllerTest {
isNull(), isNull(),
eq(LocalDate.of(2026, 8, 2)), eq(LocalDate.of(2026, 8, 2)),
eq(3L), eq(3L),
isNull(),
eq(8L)); eq(8L));
verify(service, never()).writeMonthlyZipExport( verify(service, never()).writeMonthlyZipExport(
any(OutputStream.class), any(), any(), any(), any(), any()); any(OutputStream.class), any(), any(), any(), any(), any(), any());
} }
@Test @Test
@@ -153,6 +158,7 @@ class DedupeTotalDataControllerTest {
eq(LocalDate.of(2026, 7, 15)), eq(LocalDate.of(2026, 7, 15)),
eq(LocalDate.of(2026, 8, 2)), eq(LocalDate.of(2026, 8, 2)),
eq(3L), eq(3L),
isNull(),
eq(8L)); eq(8L));
ResponseEntity<StreamingResponseBody> response = controller.export( ResponseEntity<StreamingResponseBody> response = controller.export(
@@ -160,6 +166,7 @@ class DedupeTotalDataControllerTest {
LocalDate.of(2026, 7, 15), LocalDate.of(2026, 7, 15),
LocalDate.of(2026, 8, 2), LocalDate.of(2026, 8, 2),
3L, 3L,
null,
request); request);
assertEquals(MediaType.parseMediaType("application/zip"), response.getHeaders().getContentType()); assertEquals(MediaType.parseMediaType("application/zip"), response.getHeaders().getContentType());
@@ -176,8 +183,9 @@ class DedupeTotalDataControllerTest {
eq(LocalDate.of(2026, 7, 15)), eq(LocalDate.of(2026, 7, 15)),
eq(LocalDate.of(2026, 8, 2)), eq(LocalDate.of(2026, 8, 2)),
eq(3L), eq(3L),
isNull(),
eq(8L)); eq(8L));
verify(service, never()).writeExport(any(OutputStream.class), any(), any(), any(), any(), any()); verify(service, never()).writeExport(any(OutputStream.class), any(), any(), any(), any(), any(), any());
} }
@Test @Test
@@ -194,6 +202,7 @@ class DedupeTotalDataControllerTest {
LocalDate.of(2026, 8, 2), LocalDate.of(2026, 8, 2),
LocalDate.of(2026, 7, 15), LocalDate.of(2026, 7, 15),
3L, 3L,
null,
request)); request));
assertEquals(400, exception.getCode()); assertEquals(400, exception.getCode());
@@ -21,6 +21,7 @@ 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.ibatis.builder.MapperBuilderAssistant; import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.DuplicateKeyException;
import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.util.ReflectionTestUtils;
@@ -188,6 +189,58 @@ class DedupeTotalDataServiceTest {
verify(dedupeTotalDataMapper, never()).insert(any(DedupeTotalDataEntity.class)); verify(dedupeTotalDataMapper, never()).insert(any(DedupeTotalDataEntity.class));
} }
@Test
void importReadsAndNormalizesCountryColumn() throws Exception {
when(adminUserMapper.selectById(23L)).thenReturn(user(23L, "normal", "member-a"));
stubWritableGroup(23L, 7L);
when(dedupeTotalDataMapper.selectExistingDataValues(List.of("B012345678"))).thenReturn(List.of());
when(dedupeTotalDataMapper.insertBatchIgnore(any())).thenReturn(1);
MockMultipartFile file = asinCountryWorkbook("B012345678", "德国, US,United Kingdom");
var result = service.importFromExcel(file, 7L, 23L);
assertEquals(1, result.getInsertedCount());
ArgumentCaptor<List<DedupeTotalDataEntity>> captor = ArgumentCaptor.forClass(List.class);
verify(dedupeTotalDataMapper).insertBatchIgnore(captor.capture());
assertEquals("DE,US,UK", captor.getValue().getFirst().getCountry());
}
@Test
void importKeepsCountryBlankWhenHeaderMissing() throws Exception {
when(adminUserMapper.selectById(23L)).thenReturn(user(23L, "normal", "member-a"));
stubWritableGroup(23L, 7L);
when(dedupeTotalDataMapper.selectExistingDataValues(List.of("B012345678"))).thenReturn(List.of());
when(dedupeTotalDataMapper.insertBatchIgnore(any())).thenReturn(1);
var result = service.importFromExcel(asinWorkbook("B012345678"), 7L, 23L);
assertEquals(1, result.getInsertedCount());
ArgumentCaptor<List<DedupeTotalDataEntity>> captor = ArgumentCaptor.forClass(List.class);
verify(dedupeTotalDataMapper).insertBatchIgnore(captor.capture());
assertEquals(null, captor.getValue().getFirst().getCountry());
}
@Test
@SuppressWarnings({"rawtypes", "unchecked"})
void pageFiltersByCountryWithFindInSet() {
TableInfoHelper.initTableInfo(
new MapperBuilderAssistant(new MybatisConfiguration(), ""),
DedupeTotalDataEntity.class);
when(adminUserMapper.selectById(1L)).thenReturn(user(1L, "super_admin", "root"));
when(dedupeTotalDataMapper.selectCount(any())).thenReturn(0L);
when(dedupeTotalDataMapper.selectList(any())).thenReturn(List.of());
service.page(1, 15, "", "", null, null, null, "UK", 1L);
ArgumentCaptor<LambdaQueryWrapper<DedupeTotalDataEntity>> queryCaptor =
ArgumentCaptor.forClass((Class) LambdaQueryWrapper.class);
verify(dedupeTotalDataMapper).selectCount(queryCaptor.capture());
LambdaQueryWrapper<DedupeTotalDataEntity> query = queryCaptor.getValue();
query.getSqlSegment();
assertTrue(query.getParamNameValuePairs().containsValue("UK"));
assertTrue(query.getCustomSqlSegment().contains("FIND_IN_SET"));
}
@Test @Test
void concurrentDuplicateDuringImportIsSkipped() throws Exception { void concurrentDuplicateDuringImportIsSkipped() throws Exception {
when(adminUserMapper.selectById(23L)).thenReturn(user(23L, "normal", "member-a")); when(adminUserMapper.selectById(23L)).thenReturn(user(23L, "normal", "member-a"));
@@ -294,6 +347,7 @@ class DedupeTotalDataServiceTest {
when(shopManageGroupMapper.selectManagedMemberUserIds(10L)).thenReturn(List.of(23L)); when(shopManageGroupMapper.selectManagedMemberUserIds(10L)).thenReturn(List.of(23L));
DedupeTotalDataEntity entity = data(91L, 23L); DedupeTotalDataEntity entity = data(91L, 23L);
entity.setUploaderUsername("member-a"); entity.setUploaderUsername("member-a");
entity.setCountry("DE,UK");
entity.setCreatedAt(LocalDateTime.of(2026, 7, 20, 12, 30)); entity.setCreatedAt(LocalDateTime.of(2026, 7, 20, 12, 30));
when(dedupeTotalDataMapper.selectList(any())).thenReturn(List.of(entity)); when(dedupeTotalDataMapper.selectList(any())).thenReturn(List.of(entity));
@@ -305,11 +359,13 @@ class DedupeTotalDataServiceTest {
try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(bytes))) { try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(bytes))) {
var sheet = workbook.getSheetAt(0); var sheet = workbook.getSheetAt(0);
assertEquals("国家", sheet.getRow(0).getCell(2).getStringCellValue());
assertEquals("ASIN值", sheet.getRow(0).getCell(1).getStringCellValue()); assertEquals("ASIN值", sheet.getRow(0).getCell(1).getStringCellValue());
assertEquals("B012345678", sheet.getRow(1).getCell(1).getStringCellValue()); assertEquals("B012345678", sheet.getRow(1).getCell(1).getStringCellValue());
assertEquals("member-a", sheet.getRow(1).getCell(2).getStringCellValue()); assertEquals("DE,UK", sheet.getRow(1).getCell(2).getStringCellValue());
assertEquals("", sheet.getRow(1).getCell(3).getStringCellValue()); assertEquals("member-a", sheet.getRow(1).getCell(3).getStringCellValue());
assertEquals("2026-07-20 12:30:00", sheet.getRow(1).getCell(4).getStringCellValue()); assertEquals("", sheet.getRow(1).getCell(4).getStringCellValue());
assertEquals("2026-07-20 12:30:00", sheet.getRow(1).getCell(5).getStringCellValue());
} }
verify(shopManageGroupMapper).selectManagedMemberUserIds(10L); verify(shopManageGroupMapper).selectManagedMemberUserIds(10L);
@@ -548,4 +604,23 @@ class DedupeTotalDataServiceTest {
output.toByteArray()); output.toByteArray());
} }
} }
private MockMultipartFile asinCountryWorkbook(String asin, String country) throws Exception {
try (XSSFWorkbook workbook = new XSSFWorkbook();
ByteArrayOutputStream output = new ByteArrayOutputStream()) {
var sheet = workbook.createSheet("data");
Row header = sheet.createRow(0);
header.createCell(0).setCellValue("ASIN");
header.createCell(1).setCellValue("国家");
Row row = sheet.createRow(1);
row.createCell(0).setCellValue(asin);
row.createCell(1).setCellValue(country);
workbook.write(output);
return new MockMultipartFile(
"file",
"data.xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
output.toByteArray());
}
}
} }
+7
View File
@@ -2929,6 +2929,7 @@ def _format_dedupe_total_data_item(item):
return { return {
'id': item.get('id'), 'id': item.get('id'),
'data_value': item.get('dataValue') or '', 'data_value': item.get('dataValue') or '',
'country': item.get('country') or '',
'group_id': item.get('groupId'), 'group_id': item.get('groupId'),
'group_name': item.get('groupName') or '', 'group_name': item.get('groupName') or '',
'uploader_user_id': item.get('uploaderUserId'), 'uploader_user_id': item.get('uploaderUserId'),
@@ -2948,6 +2949,7 @@ def list_dedupe_total_data():
keyword = (request.args.get('keyword') or '').strip() keyword = (request.args.get('keyword') or '').strip()
username = (request.args.get('username') or '').strip() username = (request.args.get('username') or '').strip()
group_id = _parse_positive_group_id(request.args.get('group_id') or request.args.get('groupId')) group_id = _parse_positive_group_id(request.args.get('group_id') or request.args.get('groupId'))
country = (request.args.get('country') or '').strip()
start_date = (request.args.get('start_date') or request.args.get('startDate') 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() end_date = (request.args.get('end_date') or request.args.get('endDate') or '').strip()
params = { params = {
@@ -2957,6 +2959,8 @@ def list_dedupe_total_data():
'username': username, 'username': username,
'operatorId': current_row.get('id'), 'operatorId': current_row.get('id'),
} }
if country:
params['country'] = country
if start_date: if start_date:
params['startDate'] = start_date params['startDate'] = start_date
if end_date: if end_date:
@@ -2993,10 +2997,13 @@ def export_dedupe_total_data():
return denied return denied
params = {'operatorId': current_row.get('id')} params = {'operatorId': current_row.get('id')}
username = (request.args.get('username') or '').strip() username = (request.args.get('username') or '').strip()
country = (request.args.get('country') or '').strip()
start_date = (request.args.get('start_date') or request.args.get('startDate') 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() end_date = (request.args.get('end_date') or request.args.get('endDate') or '').strip()
if username: if username:
params['username'] = username params['username'] = username
if country:
params['country'] = country
if start_date: if start_date:
params['startDate'] = start_date params['startDate'] = start_date
if end_date: if end_date:
+20 -4
View File
@@ -2180,14 +2180,28 @@
var keyword = (document.getElementById('searchDedupeTotalData').value || '').trim(); var keyword = (document.getElementById('searchDedupeTotalData').value || '').trim();
var username = (document.getElementById('searchDedupeTotalDataUsername').value || '').trim(); var username = (document.getElementById('searchDedupeTotalDataUsername').value || '').trim();
var groupId = (document.getElementById('dedupeTotalDataGroupFilterId').value || '').trim(); var groupId = (document.getElementById('dedupeTotalDataGroupFilterId').value || '').trim();
var country = (document.getElementById('dedupeTotalDataCountryFilter').value || '').trim();
var dateRange = getDedupeTotalDataDateRange(); var dateRange = getDedupeTotalDataDateRange();
if (keyword) q += '&keyword=' + encodeURIComponent(keyword); if (keyword) q += '&keyword=' + encodeURIComponent(keyword);
if (username) q += '&username=' + encodeURIComponent(username); if (username) q += '&username=' + encodeURIComponent(username);
if (groupId) q += '&group_id=' + encodeURIComponent(groupId); if (groupId) q += '&group_id=' + encodeURIComponent(groupId);
if (country) q += '&country=' + encodeURIComponent(country);
if (dateRange.startDate) q += '&start_date=' + encodeURIComponent(dateRange.startDate); if (dateRange.startDate) q += '&start_date=' + encodeURIComponent(dateRange.startDate);
if (dateRange.endDate) q += '&end_date=' + encodeURIComponent(dateRange.endDate); if (dateRange.endDate) q += '&end_date=' + encodeURIComponent(dateRange.endDate);
return q; return q;
} }
function getDedupeTotalDataCountryLabel(countryCodes) {
var countryLabels = {
DE: '德国', UK: '英国', FR: '法国', IT: '意大利', ES: '西班牙', US: '美国'
};
if (Array.isArray(countryCodes)) {
return countryCodes.map(function (code) { return countryLabels[code] || code; }).join('、');
}
return String(countryCodes || '').split(/[,]/).map(function (code) {
code = code.trim();
return countryLabels[code] || code;
}).filter(Boolean).join('、');
}
function loadDedupeTotalData(page) { function loadDedupeTotalData(page) {
if (!validateDedupeTotalDataDateRange()) return; if (!validateDedupeTotalDataDateRange()) return;
dedupeTotalDataPage = page || 1; dedupeTotalDataPage = page || 1;
@@ -2196,15 +2210,15 @@
.then(function (res) { .then(function (res) {
var tbody = document.getElementById('dedupeTotalDataListBody'); var tbody = document.getElementById('dedupeTotalDataListBody');
if (!res.success) { if (!res.success) {
tbody.innerHTML = '<tr><td colspan="6" class="empty-tip">加载失败: ' + escapeHtml(res.error || '') + '</td></tr>'; tbody.innerHTML = '<tr><td colspan="7" class="empty-tip">加载失败: ' + escapeHtml(res.error || '') + '</td></tr>';
return; return;
} }
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) { tbody.innerHTML = items.map(function (item) {
return '<tr><td>' + escapeHtml(item.id) + '</td><td>' + escapeHtml(item.data_value || '') + '</td><td>' + escapeHtml(item.username || '') + '</td><td>' + escapeHtml(item.group_name || '未分组') + '</td><td>' + escapeHtml(item.created_at || '') + '</td><td>' + return '<tr><td>' + escapeHtml(item.id) + '</td><td>' + escapeHtml(item.data_value || '') + '</td><td>' + escapeHtml(getDedupeTotalDataCountryLabel(item.country)) + '</td><td>' + escapeHtml(item.username || '') + '</td><td>' + escapeHtml(item.group_name || '未分组') + '</td><td>' + escapeHtml(item.created_at || '') + '</td><td>' +
'<button class="btn btn-sm" data-dedupe-total-edit="' + escapeHtml(item.id) + '" data-value="' + escapeHtml(item.data_value || '') + '" data-group-id="' + escapeHtml(item.group_id || '') + '">编辑</button> ' + '<button class="btn btn-sm" data-dedupe-total-edit="' + escapeHtml(item.id) + '" data-value="' + escapeHtml(item.data_value || '') + '" data-group-id="' + escapeHtml(item.group_id || '') + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger" data-dedupe-total-delete="' + escapeHtml(item.id) + '" data-value="' + escapeHtml(item.data_value || '') + '">删除</button>' + '<button class="btn btn-sm btn-danger" data-dedupe-total-delete="' + escapeHtml(item.id) + '" data-value="' + escapeHtml(item.data_value || '') + '">删除</button>' +
'</td></tr>'; '</td></tr>';
@@ -2214,7 +2228,7 @@
bindDedupeTotalDataActions(); bindDedupeTotalDataActions();
}) })
.catch(function () { .catch(function () {
document.getElementById('dedupeTotalDataListBody').innerHTML = '<tr><td colspan="6" class="empty-tip">请求失败</td></tr>'; document.getElementById('dedupeTotalDataListBody').innerHTML = '<tr><td colspan="7" class="empty-tip">请求失败</td></tr>';
}); });
} }
function bindDedupeTotalDataActions() { function bindDedupeTotalDataActions() {
@@ -2268,11 +2282,13 @@
if (dedupeTotalDataExportButton.disabled) return; if (dedupeTotalDataExportButton.disabled) return;
var username = (document.getElementById('searchDedupeTotalDataUsername').value || '').trim(); var username = (document.getElementById('searchDedupeTotalDataUsername').value || '').trim();
var groupId = (document.getElementById('dedupeTotalDataGroupFilterId').value || '').trim(); var groupId = (document.getElementById('dedupeTotalDataGroupFilterId').value || '').trim();
var country = (document.getElementById('dedupeTotalDataCountryFilter').value || '').trim();
var dateRange = getDedupeTotalDataDateRange(); var dateRange = getDedupeTotalDataDateRange();
if (!validateDedupeTotalDataDateRange()) return; if (!validateDedupeTotalDataDateRange()) return;
var params = []; var params = [];
if (username) params.push('username=' + encodeURIComponent(username)); if (username) params.push('username=' + encodeURIComponent(username));
if (groupId) params.push('group_id=' + encodeURIComponent(groupId)); if (groupId) params.push('group_id=' + encodeURIComponent(groupId));
if (country) params.push('country=' + encodeURIComponent(country));
if (dateRange.startDate) params.push('start_date=' + encodeURIComponent(dateRange.startDate)); if (dateRange.startDate) params.push('start_date=' + encodeURIComponent(dateRange.startDate));
if (dateRange.endDate) params.push('end_date=' + encodeURIComponent(dateRange.endDate)); if (dateRange.endDate) params.push('end_date=' + encodeURIComponent(dateRange.endDate));
var originalButtonText = dedupeTotalDataExportButton.textContent; var originalButtonText = dedupeTotalDataExportButton.textContent;
+16 -3
View File
@@ -4,7 +4,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>管理后台 - 南日AI</title> <title>管理后台 - 数富AI</title>
<style> <style>
* { * {
margin: 0; margin: 0;
@@ -2030,7 +2030,7 @@
<h3 style="margin-bottom:16px;font-size:15px;">新增ASIN</h3> <h3 style="margin-bottom:16px;font-size:15px;">新增ASIN</h3>
<div class="form-row"> <div class="form-row">
<div class="form-group" style="min-width:320px;"> <div class="form-group" style="min-width:320px;">
<label>上传 Excel 文件(读取 ASIN 列)</label> <label>上传 Excel 文件(读取 ASIN 列,可选 国家 列</label>
<input type="file" id="dedupeTotalDataFile" accept=".xlsx,.xls"> <input type="file" id="dedupeTotalDataFile" accept=".xlsx,.xls">
</div> </div>
<div class="form-group" style="min-width:220px;"> <div class="form-group" style="min-width:220px;">
@@ -2091,6 +2091,18 @@
<option value="">全部分组</option> <option value="">全部分组</option>
</select> </select>
</div> </div>
<div class="form-group">
<label>国家</label>
<select id="dedupeTotalDataCountryFilter">
<option value="">全部国家</option>
<option value="DE">德国</option>
<option value="UK">英国</option>
<option value="FR">法国</option>
<option value="IT">意大利</option>
<option value="ES">西班牙</option>
<option value="US">美国</option>
</select>
</div>
<div class="form-group"> <div class="form-group">
<label>开始日期</label> <label>开始日期</label>
<input type="date" id="exportDedupeTotalDataStartDate"> <input type="date" id="exportDedupeTotalDataStartDate">
@@ -2109,6 +2121,7 @@
<tr> <tr>
<th>ID</th> <th>ID</th>
<th>ASIN值</th> <th>ASIN值</th>
<th>国家</th>
<th>用户名</th> <th>用户名</th>
<th>分组</th> <th>分组</th>
<th>创建时间</th> <th>创建时间</th>
@@ -3175,7 +3188,7 @@
</div> </div>
</div> </div>
</div> </div>
<script src="/static/admin.js?v=dedupe-export-wait-1"></script> <script src="/static/admin.js?v=dedupe-country-1"></script>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录 - 南日AI</title> <title>登录 - 数富AI</title>
<style> <style>
* { margin: 0; padding: 0; box-sizing: border-box; } * { margin: 0; padding: 0; box-sizing: border-box; }
body { body {
@@ -96,7 +96,7 @@
</head> </head>
<body> <body>
<header class="header"> <header class="header">
<span class="header-title">南日AI</span> <span class="header-title">数富AI</span>
</header> </header>
<div class="login-box"> <div class="login-box">