后台去重数据新增国家
This commit is contained in:
+5
-3
@@ -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))
|
||||||
|
|||||||
+2
-2
@@ -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>
|
||||||
""")
|
""")
|
||||||
|
|||||||
+1
@@ -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;
|
||||||
|
|||||||
+3
@@ -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;
|
||||||
|
|
||||||
|
|||||||
+115
-17
@@ -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;
|
||||||
+12
-3
@@ -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());
|
||||||
|
|||||||
+78
-3
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user