+202
@@ -0,0 +1,202 @@
|
||||
package com.nanri.aiimage.modules.dedupe.controller;
|
||||
|
||||
import com.nanri.aiimage.common.exception.BusinessException;
|
||||
import com.nanri.aiimage.modules.admin.support.AdminAuthSupport;
|
||||
import com.nanri.aiimage.modules.dedupe.service.DedupeTotalDataService;
|
||||
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
|
||||
import com.nanri.aiimage.modules.permission.service.PermissionMenuService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.time.LocalDate;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class DedupeTotalDataControllerTest {
|
||||
|
||||
@Test
|
||||
void exportReturnsStreamingBodyAndDelegatesToService() throws Exception {
|
||||
DedupeTotalDataService service = mock(DedupeTotalDataService.class);
|
||||
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
|
||||
PermissionMenuService permissionMenuService = mock(PermissionMenuService.class);
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
DedupeTotalDataController controller = new DedupeTotalDataController(
|
||||
service, authSupport, permissionMenuService);
|
||||
AdminUserEntity operator = new AdminUserEntity();
|
||||
operator.setId(8L);
|
||||
operator.setRole("super_admin");
|
||||
when(authSupport.requireUser(request)).thenReturn(operator);
|
||||
when(authSupport.currentRole(operator)).thenReturn("super_admin");
|
||||
doAnswer(invocation -> {
|
||||
OutputStream outputStream = invocation.getArgument(0);
|
||||
outputStream.write(new byte[]{1, 2, 3});
|
||||
return null;
|
||||
}).when(service).writeExport(
|
||||
any(OutputStream.class),
|
||||
eq("member"),
|
||||
eq(LocalDate.of(2026, 7, 1)),
|
||||
eq(LocalDate.of(2026, 7, 31)),
|
||||
eq(3L),
|
||||
eq(8L));
|
||||
|
||||
ResponseEntity<StreamingResponseBody> response = controller.export(
|
||||
"member",
|
||||
LocalDate.of(2026, 7, 1),
|
||||
LocalDate.of(2026, 7, 31),
|
||||
3L,
|
||||
request);
|
||||
|
||||
assertNotNull(response.getBody());
|
||||
assertTrue(response.getBody() instanceof StreamingResponseBody);
|
||||
assertEquals(MediaType.parseMediaType(
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
|
||||
response.getHeaders().getContentType());
|
||||
assertNotNull(response.getHeaders().getFirst(HttpHeaders.CONTENT_DISPOSITION));
|
||||
assertTrue(response.getHeaders().getFirst(HttpHeaders.CONTENT_DISPOSITION)
|
||||
.contains("dedupe-total-data-"));
|
||||
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
response.getBody().writeTo(outputStream);
|
||||
|
||||
assertArrayEquals(new byte[]{1, 2, 3}, outputStream.toByteArray());
|
||||
verify(service).writeExport(
|
||||
any(OutputStream.class),
|
||||
eq("member"),
|
||||
eq(LocalDate.of(2026, 7, 1)),
|
||||
eq(LocalDate.of(2026, 7, 31)),
|
||||
eq(3L),
|
||||
eq(8L));
|
||||
verify(service, never()).writeMonthlyZipExport(
|
||||
any(OutputStream.class), any(), any(), any(), any(), any());
|
||||
verifyNoInteractions(permissionMenuService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void exportWithIncompleteDatesStaysSingleXlsx() throws Exception {
|
||||
DedupeTotalDataService service = mock(DedupeTotalDataService.class);
|
||||
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
|
||||
PermissionMenuService permissionMenuService = mock(PermissionMenuService.class);
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
DedupeTotalDataController controller = new DedupeTotalDataController(
|
||||
service, authSupport, permissionMenuService);
|
||||
AdminUserEntity operator = new AdminUserEntity();
|
||||
operator.setId(8L);
|
||||
operator.setRole("super_admin");
|
||||
when(authSupport.requireUser(request)).thenReturn(operator);
|
||||
when(authSupport.currentRole(operator)).thenReturn("super_admin");
|
||||
|
||||
ResponseEntity<StreamingResponseBody> response = controller.export(
|
||||
"member",
|
||||
null,
|
||||
LocalDate.of(2026, 8, 2),
|
||||
3L,
|
||||
request);
|
||||
|
||||
assertEquals(MediaType.parseMediaType(
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
|
||||
response.getHeaders().getContentType());
|
||||
assertTrue(response.getHeaders().getFirst(HttpHeaders.CONTENT_DISPOSITION)
|
||||
.contains(".xlsx"));
|
||||
assertNotNull(response.getBody());
|
||||
response.getBody().writeTo(new ByteArrayOutputStream());
|
||||
|
||||
verify(service).writeExport(
|
||||
any(OutputStream.class),
|
||||
eq("member"),
|
||||
isNull(),
|
||||
eq(LocalDate.of(2026, 8, 2)),
|
||||
eq(3L),
|
||||
eq(8L));
|
||||
verify(service, never()).writeMonthlyZipExport(
|
||||
any(OutputStream.class), any(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void crossMonthExportReturnsZipAndDelegatesMonthlyExport() throws Exception {
|
||||
DedupeTotalDataService service = mock(DedupeTotalDataService.class);
|
||||
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
|
||||
PermissionMenuService permissionMenuService = mock(PermissionMenuService.class);
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
DedupeTotalDataController controller = new DedupeTotalDataController(
|
||||
service, authSupport, permissionMenuService);
|
||||
AdminUserEntity operator = new AdminUserEntity();
|
||||
operator.setId(8L);
|
||||
operator.setRole("super_admin");
|
||||
when(authSupport.requireUser(request)).thenReturn(operator);
|
||||
when(authSupport.currentRole(operator)).thenReturn("super_admin");
|
||||
doAnswer(invocation -> {
|
||||
OutputStream outputStream = invocation.getArgument(0);
|
||||
outputStream.write(new byte[]{4, 5});
|
||||
return null;
|
||||
}).when(service).writeMonthlyZipExport(
|
||||
any(OutputStream.class),
|
||||
eq("member"),
|
||||
eq(LocalDate.of(2026, 7, 15)),
|
||||
eq(LocalDate.of(2026, 8, 2)),
|
||||
eq(3L),
|
||||
eq(8L));
|
||||
|
||||
ResponseEntity<StreamingResponseBody> response = controller.export(
|
||||
"member",
|
||||
LocalDate.of(2026, 7, 15),
|
||||
LocalDate.of(2026, 8, 2),
|
||||
3L,
|
||||
request);
|
||||
|
||||
assertEquals(MediaType.parseMediaType("application/zip"), response.getHeaders().getContentType());
|
||||
assertTrue(response.getHeaders().getFirst(HttpHeaders.CONTENT_DISPOSITION)
|
||||
.contains(".zip"));
|
||||
assertNotNull(response.getBody());
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
response.getBody().writeTo(outputStream);
|
||||
|
||||
assertArrayEquals(new byte[]{4, 5}, outputStream.toByteArray());
|
||||
verify(service).writeMonthlyZipExport(
|
||||
any(OutputStream.class),
|
||||
eq("member"),
|
||||
eq(LocalDate.of(2026, 7, 15)),
|
||||
eq(LocalDate.of(2026, 8, 2)),
|
||||
eq(3L),
|
||||
eq(8L));
|
||||
verify(service, never()).writeExport(any(OutputStream.class), any(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void reversedDateRangeIsRejectedBeforeStreamingBodyIsReturned() {
|
||||
DedupeTotalDataService service = mock(DedupeTotalDataService.class);
|
||||
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
|
||||
PermissionMenuService permissionMenuService = mock(PermissionMenuService.class);
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
DedupeTotalDataController controller = new DedupeTotalDataController(
|
||||
service, authSupport, permissionMenuService);
|
||||
|
||||
BusinessException exception = assertThrows(BusinessException.class, () -> controller.export(
|
||||
"member",
|
||||
LocalDate.of(2026, 8, 2),
|
||||
LocalDate.of(2026, 7, 15),
|
||||
3L,
|
||||
request));
|
||||
|
||||
assertEquals(400, exception.getCode());
|
||||
verifyNoInteractions(service, authSupport, permissionMenuService);
|
||||
}
|
||||
}
|
||||
+115
@@ -33,9 +33,13 @@ import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
@@ -312,6 +316,75 @@ class DedupeTotalDataServiceTest {
|
||||
verify(dedupeTotalDataMapper).selectList(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeExportPagesThroughLargeResultSet() throws Exception {
|
||||
when(adminUserMapper.selectById(1L)).thenReturn(user(1L, "super_admin", "root"));
|
||||
when(dedupeTotalDataMapper.selectList(any()))
|
||||
.thenReturn(exportBatch(5000L, 2000), List.of(data(3000L, 23L)));
|
||||
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
service.writeExport(output, "", null, null, null, 1L);
|
||||
|
||||
verify(dedupeTotalDataMapper, times(2)).selectList(any());
|
||||
try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(output.toByteArray()))) {
|
||||
var sheet = workbook.getSheetAt(0);
|
||||
assertEquals(2001, sheet.getLastRowNum());
|
||||
assertEquals("5000", sheet.getRow(1).getCell(0).getStringCellValue());
|
||||
assertEquals("3000", sheet.getRow(2001).getCell(0).getStringCellValue());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
void writeMonthlyZipExportSplitsByMonthAndClipsDateRanges() throws Exception {
|
||||
TableInfoHelper.initTableInfo(
|
||||
new MapperBuilderAssistant(new MybatisConfiguration(), ""),
|
||||
DedupeTotalDataEntity.class);
|
||||
when(adminUserMapper.selectById(1L)).thenReturn(user(1L, "super_admin", "root"));
|
||||
when(shopManageGroupMapper.selectById(3L)).thenReturn(group(3L));
|
||||
DedupeTotalDataEntity july = data(500L, 23L);
|
||||
july.setDataValue("JULY");
|
||||
july.setCreatedAt(LocalDateTime.of(2026, 7, 31, 10, 0));
|
||||
DedupeTotalDataEntity august = data(400L, 23L);
|
||||
august.setDataValue("AUGUST");
|
||||
august.setCreatedAt(LocalDateTime.of(2026, 8, 1, 10, 0));
|
||||
when(dedupeTotalDataMapper.selectList(any()))
|
||||
.thenReturn(List.of(july), List.of(august));
|
||||
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
service.writeMonthlyZipExport(
|
||||
output,
|
||||
" member ",
|
||||
LocalDate.of(2026, 7, 15),
|
||||
LocalDate.of(2026, 8, 2),
|
||||
3L,
|
||||
1L);
|
||||
|
||||
Map<String, byte[]> entries = unzip(output.toByteArray());
|
||||
assertEquals(List.of(
|
||||
"dedupe-total-data-2026-07.xlsx",
|
||||
"dedupe-total-data-2026-08.xlsx"), new ArrayList<>(entries.keySet()));
|
||||
assertWorkbookDataValue(entries.get("dedupe-total-data-2026-07.xlsx"), "JULY");
|
||||
assertWorkbookDataValue(entries.get("dedupe-total-data-2026-08.xlsx"), "AUGUST");
|
||||
|
||||
ArgumentCaptor<LambdaQueryWrapper<DedupeTotalDataEntity>> queryCaptor =
|
||||
ArgumentCaptor.forClass((Class) LambdaQueryWrapper.class);
|
||||
verify(dedupeTotalDataMapper, times(2)).selectList(queryCaptor.capture());
|
||||
List<LambdaQueryWrapper<DedupeTotalDataEntity>> queries = queryCaptor.getAllValues();
|
||||
assertQueryContainsText(queries.get(0), "member");
|
||||
assertQueryContains(queries.get(0),
|
||||
LocalDate.of(2026, 7, 15).atStartOfDay(),
|
||||
LocalDate.of(2026, 8, 1).atStartOfDay(),
|
||||
3L);
|
||||
assertQueryContainsText(queries.get(1), "member");
|
||||
assertQueryContains(queries.get(1),
|
||||
LocalDate.of(2026, 8, 1).atStartOfDay(),
|
||||
LocalDate.of(2026, 8, 3).atStartOfDay(),
|
||||
3L);
|
||||
verify(adminUserMapper).selectById(1L);
|
||||
verify(shopManageGroupMapper).selectById(3L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void exportRejectsReversedDateRange() {
|
||||
assertThrows(BusinessException.class, () -> service.export(
|
||||
@@ -404,6 +477,48 @@ class DedupeTotalDataServiceTest {
|
||||
return entity;
|
||||
}
|
||||
|
||||
private List<DedupeTotalDataEntity> exportBatch(long startId, int count) {
|
||||
List<DedupeTotalDataEntity> rows = new ArrayList<>(count);
|
||||
for (long id = startId; id > startId - count; id--) {
|
||||
rows.add(data(id, 23L));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private Map<String, byte[]> unzip(byte[] bytes) throws Exception {
|
||||
Map<String, byte[]> entries = new LinkedHashMap<>();
|
||||
try (ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(bytes))) {
|
||||
ZipEntry entry;
|
||||
while ((entry = zipInputStream.getNextEntry()) != null) {
|
||||
ByteArrayOutputStream entryOutput = new ByteArrayOutputStream();
|
||||
zipInputStream.transferTo(entryOutput);
|
||||
entries.put(entry.getName(), entryOutput.toByteArray());
|
||||
zipInputStream.closeEntry();
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
private void assertWorkbookDataValue(byte[] bytes, String expectedValue) throws Exception {
|
||||
try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(bytes))) {
|
||||
assertEquals(expectedValue, workbook.getSheetAt(0).getRow(1).getCell(1).getStringCellValue());
|
||||
}
|
||||
}
|
||||
|
||||
private void assertQueryContains(LambdaQueryWrapper<DedupeTotalDataEntity> query, Object... values) {
|
||||
query.getSqlSegment();
|
||||
for (Object value : values) {
|
||||
assertTrue(query.getParamNameValuePairs().containsValue(value));
|
||||
}
|
||||
}
|
||||
|
||||
private void assertQueryContainsText(LambdaQueryWrapper<DedupeTotalDataEntity> query, String expectedText) {
|
||||
query.getSqlSegment();
|
||||
assertTrue(query.getParamNameValuePairs().values().stream()
|
||||
.map(String::valueOf)
|
||||
.anyMatch(value -> value.contains(expectedText)));
|
||||
}
|
||||
|
||||
private void stubWritableGroup(Long operatorId, Long groupId) {
|
||||
when(shopManageGroupMapper.selectAccessibleGroupIds(operatorId)).thenReturn(List.of(groupId));
|
||||
when(shopManageGroupMapper.selectUserIdsByGroupIds(List.of(groupId))).thenReturn(List.of(operatorId));
|
||||
|
||||
+155
@@ -16,10 +16,12 @@ import com.nanri.aiimage.modules.task.service.TaskResultPayloadService;
|
||||
import com.nanri.aiimage.modules.ziniao.service.ZiniaoShopSwitchService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -29,8 +31,10 @@ import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@@ -146,4 +150,155 @@ class PriceTrackTaskServiceTest {
|
||||
verify(priceTrackLoopRunService).syncLoopRunAfterChildTerminal(taskId);
|
||||
verify(lock).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void submitResultComputesModifyCountFromOriginalRowsBeforeAssembly() {
|
||||
long taskId = 22165L;
|
||||
String shopName = "shop-a";
|
||||
|
||||
FileTaskEntity task = runningTask(taskId);
|
||||
FileResultEntity result = pendingResult(taskId, shopName);
|
||||
|
||||
PriceTrackSubmitResultRequest.AsinResult changed = asinRow("B001", "\u6539\u4ef7\u6210\u529f", null);
|
||||
PriceTrackSubmitResultRequest.AsinResult skipped = asinRow("B002", "\u8df3\u8fc7\uff0c\u65e0\u9700\u6539\u4ef7", null);
|
||||
PriceTrackSubmitResultRequest.AsinResult invalidBaseline = asinRow("B003", "UPDATED", null);
|
||||
PriceTrackSubmitResultRequest.AsinResult missingBaseline = asinRow("B004", "UPDATED", null);
|
||||
PriceTrackSubmitResultRequest.AsinResult statusOnly = asinRow("B005", null, "UPDATED");
|
||||
PriceTrackSubmitResultRequest.AsinResult blankBaseline = asinRow("B006", "UPDATED", null);
|
||||
changed.setModifyCount("1");
|
||||
skipped.setModifyCount("1");
|
||||
invalidBaseline.setModifyCount("1");
|
||||
missingBaseline.setModifyCount("1");
|
||||
statusOnly.setModifyCount("1");
|
||||
blankBaseline.setModifyCount("1");
|
||||
|
||||
PriceTrackSubmitResultRequest.ShopResult shopResult = shopResult(
|
||||
shopName,
|
||||
Map.of("DE", List.of(changed, skipped, invalidBaseline, missingBaseline, statusOnly, blankBaseline)));
|
||||
PriceTrackSubmitResultRequest request = new PriceTrackSubmitResultRequest();
|
||||
request.setShops(List.of(shopResult));
|
||||
|
||||
Map<String, List<Map<String, String>>> originalRows = Map.of("de", List.of(
|
||||
originalRow("b001", "3.0"),
|
||||
originalRow("b002", "5"),
|
||||
originalRow("b003", "abc"),
|
||||
originalRow("b005", ""),
|
||||
originalRow("b006", "")
|
||||
));
|
||||
|
||||
TaskDistributedLockService.LockHandle lock = mock(TaskDistributedLockService.LockHandle.class);
|
||||
when(taskDistributedLockService.acquire("PRICE_TRACK", taskId)).thenReturn(lock);
|
||||
when(priceTrackTaskCacheService.getTaskCacheBatch(List.of(taskId))).thenReturn(Map.of(taskId, task));
|
||||
when(fileResultMapper.selectList(any())).thenReturn(List.of(result));
|
||||
when(ziniaoShopSwitchService.normalizeShopName(shopName)).thenReturn(shopName);
|
||||
when(excelAssemblyService.normalizeCountriesMap(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
when(taskResultPayloadService.getLatest(eq(taskId), eq("PRICE_TRACK"), eq("price-track-asin-rows"), eq(Map.class)))
|
||||
.thenReturn(originalRows);
|
||||
when(taskFileJobService.findAssembleJobsByResultIds(anyString(), any())).thenReturn(Map.of());
|
||||
|
||||
service.submitResult(taskId, request);
|
||||
|
||||
ArgumentCaptor<PriceTrackSubmitResultRequest.ShopResult> payloadCaptor =
|
||||
ArgumentCaptor.forClass(PriceTrackSubmitResultRequest.ShopResult.class);
|
||||
verify(taskResultPayloadService).saveLatest(eq(taskId), eq("PRICE_TRACK"), eq(shopName), payloadCaptor.capture());
|
||||
List<PriceTrackSubmitResultRequest.AsinResult> savedRows = payloadCaptor.getValue().getCountries().get("DE");
|
||||
assertEquals("4", savedRows.get(0).getModifyCount());
|
||||
assertEquals("5", savedRows.get(1).getModifyCount());
|
||||
assertEquals("1", savedRows.get(2).getModifyCount());
|
||||
assertEquals("1", savedRows.get(3).getModifyCount());
|
||||
assertEquals("0", savedRows.get(4).getModifyCount());
|
||||
assertEquals("1", savedRows.get(5).getModifyCount());
|
||||
verify(taskFileJobService).enqueueAssembleResult(taskId, "PRICE_TRACK", result.getId(), shopName);
|
||||
verify(lock).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void submitResultUsesZeroBaselineWhenOriginalRowsPayloadIsMissing() {
|
||||
long taskId = 22166L;
|
||||
String shopName = "shop-b";
|
||||
|
||||
FileTaskEntity task = runningTask(taskId);
|
||||
FileResultEntity result = pendingResult(taskId, shopName);
|
||||
|
||||
PriceTrackSubmitResultRequest.AsinResult changed = asinRow("B010", "UPDATED", null);
|
||||
PriceTrackSubmitResultRequest.AsinResult skipped = asinRow("B011", "\u8df3\u8fc7\uff0c\u65e0\u9700\u6539\u4ef7", null);
|
||||
changed.setModifyCount("99");
|
||||
skipped.setModifyCount("99");
|
||||
|
||||
PriceTrackSubmitResultRequest.ShopResult shopResult = shopResult(
|
||||
shopName,
|
||||
Map.of("UK", List.of(changed, skipped)));
|
||||
PriceTrackSubmitResultRequest request = new PriceTrackSubmitResultRequest();
|
||||
request.setShops(List.of(shopResult));
|
||||
|
||||
TaskDistributedLockService.LockHandle lock = mock(TaskDistributedLockService.LockHandle.class);
|
||||
when(taskDistributedLockService.acquire("PRICE_TRACK", taskId)).thenReturn(lock);
|
||||
when(priceTrackTaskCacheService.getTaskCacheBatch(List.of(taskId))).thenReturn(Map.of(taskId, task));
|
||||
when(fileResultMapper.selectList(any())).thenReturn(List.of(result));
|
||||
when(ziniaoShopSwitchService.normalizeShopName(shopName)).thenReturn(shopName);
|
||||
when(excelAssemblyService.normalizeCountriesMap(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
when(taskResultPayloadService.getLatest(eq(taskId), eq("PRICE_TRACK"), eq("price-track-asin-rows"), eq(Map.class)))
|
||||
.thenReturn(null);
|
||||
when(taskFileJobService.findAssembleJobsByResultIds(anyString(), any())).thenReturn(Map.of());
|
||||
|
||||
service.submitResult(taskId, request);
|
||||
service.submitResult(taskId, request);
|
||||
|
||||
ArgumentCaptor<PriceTrackSubmitResultRequest.ShopResult> payloadCaptor =
|
||||
ArgumentCaptor.forClass(PriceTrackSubmitResultRequest.ShopResult.class);
|
||||
verify(taskResultPayloadService, times(2))
|
||||
.saveLatest(eq(taskId), eq("PRICE_TRACK"), eq(shopName), payloadCaptor.capture());
|
||||
List<PriceTrackSubmitResultRequest.AsinResult> savedRows = payloadCaptor.getAllValues().get(1).getCountries().get("UK");
|
||||
assertEquals("1", savedRows.get(0).getModifyCount());
|
||||
assertEquals("0", savedRows.get(1).getModifyCount());
|
||||
verify(taskFileJobService, times(2)).enqueueAssembleResult(taskId, "PRICE_TRACK", result.getId(), shopName);
|
||||
verify(lock, times(2)).close();
|
||||
}
|
||||
|
||||
private FileTaskEntity runningTask(long taskId) {
|
||||
FileTaskEntity task = new FileTaskEntity();
|
||||
task.setId(taskId);
|
||||
task.setUserId(672L);
|
||||
task.setModuleType("PRICE_TRACK");
|
||||
task.setStatus("RUNNING");
|
||||
task.setRequestJson("{}");
|
||||
return task;
|
||||
}
|
||||
|
||||
private FileResultEntity pendingResult(long taskId, String shopName) {
|
||||
FileResultEntity result = new FileResultEntity();
|
||||
result.setId(taskId + 1000L);
|
||||
result.setTaskId(taskId);
|
||||
result.setModuleType("PRICE_TRACK");
|
||||
result.setSourceFilename(shopName);
|
||||
result.setSuccess(0);
|
||||
return result;
|
||||
}
|
||||
|
||||
private PriceTrackSubmitResultRequest.ShopResult shopResult(
|
||||
String shopName,
|
||||
Map<String, List<PriceTrackSubmitResultRequest.AsinResult>> countries) {
|
||||
PriceTrackSubmitResultRequest.ShopResult shopResult = new PriceTrackSubmitResultRequest.ShopResult();
|
||||
shopResult.setShopName(shopName);
|
||||
shopResult.setCountries(countries);
|
||||
shopResult.setSuccess(true);
|
||||
shopResult.setError("");
|
||||
return shopResult;
|
||||
}
|
||||
|
||||
private PriceTrackSubmitResultRequest.AsinResult asinRow(String asin, String priceChangeStatus, String status) {
|
||||
PriceTrackSubmitResultRequest.AsinResult row = new PriceTrackSubmitResultRequest.AsinResult();
|
||||
row.setShopMallName("mall");
|
||||
row.setAsin(asin);
|
||||
row.setPriceChangeStatus(priceChangeStatus);
|
||||
row.setStatus(status);
|
||||
return row;
|
||||
}
|
||||
|
||||
private Map<String, String> originalRow(String asin, String modifyCount) {
|
||||
Map<String, String> row = new LinkedHashMap<>();
|
||||
row.put("asin", asin);
|
||||
row.put("modifyCount", modifyCount);
|
||||
return row;
|
||||
}
|
||||
}
|
||||
|
||||
+138
-20
@@ -5,23 +5,30 @@ import com.nanri.aiimage.modules.shopkey.mapper.SkipPriceAsinMapper;
|
||||
import com.nanri.aiimage.modules.shopkey.model.dto.SkipPriceAsinCreateRequest;
|
||||
import com.nanri.aiimage.modules.shopkey.model.entity.ShopManageGroupEntity;
|
||||
import com.nanri.aiimage.modules.shopkey.model.entity.SkipPriceAsinEntity;
|
||||
import com.nanri.aiimage.modules.shopkey.model.vo.QueryAsinImportProgressVo;
|
||||
import com.nanri.aiimage.modules.shopkey.model.vo.SkipPriceAsinItemVo;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.file.Files;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -40,18 +47,40 @@ class SkipPriceAsinServiceTest {
|
||||
private SkipPriceAsinService service;
|
||||
|
||||
@Test
|
||||
void createInsertsNewRowWhenGroupAndShopAlreadyExist() {
|
||||
ShopManageGroupEntity group = new ShopManageGroupEntity();
|
||||
group.setId(10L);
|
||||
group.setGroupName("group-a");
|
||||
when(shopManageGroupService.getAccessibleById(10L, 7L, true)).thenReturn(group);
|
||||
void createSkipsDuplicateCountryAsinWithoutWriting() {
|
||||
when(shopManageGroupService.getAccessibleById(10L, 7L, true)).thenReturn(group());
|
||||
|
||||
SkipPriceAsinEntity existing = new SkipPriceAsinEntity();
|
||||
existing.setId(100L);
|
||||
existing.setGroupId(10L);
|
||||
existing.setShopName("shop-a");
|
||||
existing.setAsinDe("OLD-ASIN");
|
||||
lenient().when(skipPriceAsinMapper.selectOne(any())).thenReturn(existing);
|
||||
existing.setAsinDe("DUP-ASIN");
|
||||
existing.setMinimumPriceDe(new BigDecimal("14.00"));
|
||||
when(skipPriceAsinMapper.selectOne(any())).thenReturn(existing);
|
||||
|
||||
SkipPriceAsinItemVo result = service.create(request(List.of("DE"),
|
||||
Map.of("DE", "dup-asin"),
|
||||
Map.of("DE", new BigDecimal("99.99"))), 7L, true);
|
||||
|
||||
verify(skipPriceAsinMapper, never()).insert(any(SkipPriceAsinEntity.class));
|
||||
verify(skipPriceAsinMapper, never()).updateById(any(SkipPriceAsinEntity.class));
|
||||
assertEquals(100L, result.getId());
|
||||
assertEquals("DUP-ASIN", result.getAsinDe());
|
||||
assertEquals(new BigDecimal("14.00"), result.getMinimumPriceDe());
|
||||
assertEquals(new BigDecimal("14.00"), existing.getMinimumPriceDe());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createInsertsOnlyNonDuplicateCountriesWithoutUpdatingExisting() {
|
||||
when(shopManageGroupService.getAccessibleById(10L, 7L, true)).thenReturn(group());
|
||||
|
||||
SkipPriceAsinEntity existing = new SkipPriceAsinEntity();
|
||||
existing.setId(100L);
|
||||
existing.setGroupId(10L);
|
||||
existing.setShopName("shop-a");
|
||||
existing.setAsinDe("DUP-ASIN");
|
||||
existing.setMinimumPriceDe(new BigDecimal("14.00"));
|
||||
when(skipPriceAsinMapper.selectOne(any())).thenReturn(existing).thenReturn(null);
|
||||
|
||||
AtomicReference<SkipPriceAsinEntity> inserted = new AtomicReference<>();
|
||||
when(skipPriceAsinMapper.insert(any(SkipPriceAsinEntity.class))).thenAnswer(invocation -> {
|
||||
@@ -62,23 +91,112 @@ class SkipPriceAsinServiceTest {
|
||||
});
|
||||
when(skipPriceAsinMapper.selectById(101L)).thenAnswer(invocation -> inserted.get());
|
||||
|
||||
SkipPriceAsinCreateRequest request = new SkipPriceAsinCreateRequest();
|
||||
request.setGroupId(10L);
|
||||
request.setShopName("shop-a");
|
||||
request.setCountries(List.of("DE"));
|
||||
request.setAsinMappings(Map.of("DE", "NEW-ASIN"));
|
||||
request.setMinimumPriceMappings(Map.of("DE", new BigDecimal("19.99")));
|
||||
|
||||
SkipPriceAsinItemVo result = service.create(request, 7L, true);
|
||||
SkipPriceAsinItemVo result = service.create(request(List.of("DE", "UK"),
|
||||
Map.of("DE", "dup-asin", "UK", "new-asin"),
|
||||
Map.of("DE", new BigDecimal("99.99"), "UK", new BigDecimal("19.99"))), 7L, true);
|
||||
|
||||
ArgumentCaptor<SkipPriceAsinEntity> captor = ArgumentCaptor.forClass(SkipPriceAsinEntity.class);
|
||||
verify(skipPriceAsinMapper).insert(captor.capture());
|
||||
verify(skipPriceAsinMapper, never()).selectOne(any());
|
||||
verify(skipPriceAsinMapper, never()).updateById(any(SkipPriceAsinEntity.class));
|
||||
assertNotSame(existing, captor.getValue());
|
||||
assertEquals("OLD-ASIN", existing.getAsinDe());
|
||||
assertNull(captor.getValue().getAsinDe());
|
||||
assertNull(captor.getValue().getMinimumPriceDe());
|
||||
assertEquals("NEW-ASIN", captor.getValue().getAsinUk());
|
||||
assertEquals(new BigDecimal("19.99"), captor.getValue().getMinimumPriceUk());
|
||||
assertEquals("DUP-ASIN", existing.getAsinDe());
|
||||
assertEquals(new BigDecimal("14.00"), existing.getMinimumPriceDe());
|
||||
assertEquals(101L, result.getId());
|
||||
assertEquals("NEW-ASIN", result.getAsinUk());
|
||||
assertEquals(new BigDecimal("19.99"), result.getMinimumPriceUk());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createInsertsDifferentAsinForSameShopAndCountry() {
|
||||
when(shopManageGroupService.getAccessibleById(10L, 7L, true)).thenReturn(group());
|
||||
when(skipPriceAsinMapper.selectOne(any())).thenReturn(null);
|
||||
|
||||
AtomicReference<SkipPriceAsinEntity> inserted = new AtomicReference<>();
|
||||
when(skipPriceAsinMapper.insert(any(SkipPriceAsinEntity.class))).thenAnswer(invocation -> {
|
||||
SkipPriceAsinEntity entity = invocation.getArgument(0);
|
||||
entity.setId(101L);
|
||||
inserted.set(entity);
|
||||
return 1;
|
||||
});
|
||||
when(skipPriceAsinMapper.selectById(101L)).thenAnswer(invocation -> inserted.get());
|
||||
|
||||
SkipPriceAsinItemVo result = service.create(request(List.of("DE"),
|
||||
Map.of("DE", "new-asin"),
|
||||
Map.of("DE", new BigDecimal("19.99"))), 7L, true);
|
||||
|
||||
ArgumentCaptor<SkipPriceAsinEntity> captor = ArgumentCaptor.forClass(SkipPriceAsinEntity.class);
|
||||
verify(skipPriceAsinMapper).insert(captor.capture());
|
||||
verify(skipPriceAsinMapper, never()).updateById(any(SkipPriceAsinEntity.class));
|
||||
assertEquals("NEW-ASIN", captor.getValue().getAsinDe());
|
||||
assertEquals(new BigDecimal("19.99"), captor.getValue().getMinimumPriceDe());
|
||||
assertEquals(101L, result.getId());
|
||||
assertEquals("NEW-ASIN", result.getAsinDe());
|
||||
assertEquals(new BigDecimal("19.99"), result.getMinimumPriceDe());
|
||||
}
|
||||
|
||||
@Test
|
||||
void importSkipsDuplicateCountryAsinWithoutWriting() throws Exception {
|
||||
SkipPriceAsinEntity existing = new SkipPriceAsinEntity();
|
||||
existing.setId(100L);
|
||||
existing.setGroupId(10L);
|
||||
existing.setShopName("shop-a");
|
||||
existing.setAsinUk("DUP-ASIN");
|
||||
existing.setMinimumPriceUk(new BigDecimal("14.00"));
|
||||
when(skipPriceAsinMapper.selectOne(any())).thenReturn(existing);
|
||||
|
||||
File workbookFile = importWorkbook("DUP-ASIN", "99.99");
|
||||
QueryAsinImportProgressVo progress = new QueryAsinImportProgressVo();
|
||||
try {
|
||||
ReflectionTestUtils.invokeMethod(service, "processImportFile",
|
||||
workbookFile, "shop-a.xlsx", 10L, "shop-a", false, progress);
|
||||
} finally {
|
||||
Files.deleteIfExists(workbookFile.toPath());
|
||||
}
|
||||
|
||||
verify(skipPriceAsinMapper, never()).insert(any(SkipPriceAsinEntity.class));
|
||||
verify(skipPriceAsinMapper, never()).updateById(any(SkipPriceAsinEntity.class));
|
||||
assertEquals(1, progress.getTotalRows());
|
||||
assertEquals(1, progress.getProcessedRows());
|
||||
assertEquals(1, progress.getAsinCount());
|
||||
assertEquals(0, progress.getInsertedCount());
|
||||
assertEquals(1, progress.getSkippedCount());
|
||||
assertEquals(new BigDecimal("14.00"), existing.getMinimumPriceUk());
|
||||
}
|
||||
|
||||
private ShopManageGroupEntity group() {
|
||||
ShopManageGroupEntity group = new ShopManageGroupEntity();
|
||||
group.setId(10L);
|
||||
group.setGroupName("group-a");
|
||||
return group;
|
||||
}
|
||||
|
||||
private SkipPriceAsinCreateRequest request(List<String> countries, Map<String, String> asinMappings,
|
||||
Map<String, BigDecimal> minimumPriceMappings) {
|
||||
SkipPriceAsinCreateRequest request = new SkipPriceAsinCreateRequest();
|
||||
request.setGroupId(10L);
|
||||
request.setShopName("shop-a");
|
||||
request.setCountries(countries);
|
||||
request.setAsinMappings(asinMappings);
|
||||
request.setMinimumPriceMappings(minimumPriceMappings);
|
||||
return request;
|
||||
}
|
||||
|
||||
private File importWorkbook(String asin, String minimumPrice) throws Exception {
|
||||
File file = File.createTempFile("skip-price-asin-test-", ".xlsx");
|
||||
try (Workbook workbook = new XSSFWorkbook();
|
||||
FileOutputStream outputStream = new FileOutputStream(file)) {
|
||||
Sheet sheet = workbook.createSheet("import");
|
||||
sheet.createRow(0).createCell(0).setCellValue("英国");
|
||||
sheet.getRow(0).createCell(1).setCellValue("英国");
|
||||
sheet.createRow(1).createCell(0).setCellValue("ASIN");
|
||||
sheet.getRow(1).createCell(1).setCellValue("最低价");
|
||||
sheet.createRow(2).createCell(0).setCellValue(asin);
|
||||
sheet.getRow(2).createCell(1).setCellValue(minimumPrice);
|
||||
workbook.write(outputStream);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user