- 撞款详情抽屉:上架时间默认降序(晚到早)、去掉列头(按时间升序)小括号、加宽至 760px、 去掉全屏遮罩(底层仍可点击)、抽屉贴到「撞款详情」区块位置而非浏览器顶部 - 修复抽屉 fixed 定位失效:.tab-panel.active 入场动画带 transform 会成为包含块, 打开时把 drawer mask 挂到 body 下恢复视口定位 - 新增导入(新增-测试)/ 导入(删除-测试)按钮与上传弹窗,对应后端端点 /api/admin/shop-data-crawl/duplicate-check-import(ADD=按店名替换基线记录、DELETE=移除该店记录) - 新增 ShopDataDuplicateCheckImportService 导入服务(复用 WorkbookParser/Aggregator,与扫描共用 Redis 锁) - 新增 3 个单测:导入替换重聚合、删除移除、无基线报错 - _preview_mock.py 加入 .gitignore(本地预览工具不入库)
This commit is contained in:
+126
@@ -0,0 +1,126 @@
|
||||
package com.nanri.aiimage.modules.shopduplicatecheck.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.nanri.aiimage.common.exception.BusinessException;
|
||||
import com.nanri.aiimage.common.service.DistributedJobLockService;
|
||||
import com.nanri.aiimage.modules.shopduplicatecheck.mapper.ShopDataDuplicateScanMapper;
|
||||
import com.nanri.aiimage.modules.shopduplicatecheck.model.entity.ShopDataDuplicateScanEntity;
|
||||
import com.nanri.aiimage.modules.shopduplicatecheck.model.payload.DuplicateItem;
|
||||
import com.nanri.aiimage.modules.shopduplicatecheck.model.payload.DuplicateOccurrence;
|
||||
import com.nanri.aiimage.modules.shopduplicatecheck.model.payload.DuplicateScanPayload;
|
||||
import com.nanri.aiimage.modules.shopduplicatecheck.model.payload.DuplicateShop;
|
||||
import com.nanri.aiimage.modules.shopduplicatecheck.service.ShopDataDuplicateCheckScanService.DuplicateScanView;
|
||||
import com.nanri.aiimage.modules.shopduplicatecheck.service.support.DuplicateCheckWorkbookParser;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 撞款导入(新增-测试/删除-测试)单测:Excel 解析、ADDR 替换、DELETE 移除、基线缺失报错。
|
||||
*/
|
||||
class ShopDataDuplicateCheckImportServiceTest {
|
||||
|
||||
private ShopDataDuplicateCheckScanService scanService;
|
||||
private ShopDataDuplicateScanMapper scanMapper;
|
||||
private ShopDataDuplicateCheckImportService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
scanService = mock(ShopDataDuplicateCheckScanService.class);
|
||||
scanMapper = mock(ShopDataDuplicateScanMapper.class);
|
||||
DistributedJobLockService lockService = mock(DistributedJobLockService.class);
|
||||
DistributedJobLockService.LockHandle lock = mock(DistributedJobLockService.LockHandle.class);
|
||||
when(lockService.tryLock(anyString(), any())).thenReturn(lock);
|
||||
service = new ShopDataDuplicateCheckImportService(
|
||||
scanService, scanMapper, lockService, new ObjectMapper());
|
||||
}
|
||||
|
||||
private DuplicateScanView baseView() {
|
||||
DuplicateOccurrence occA = new DuplicateOccurrence("B09PNTZM4L", "2026-08-18 04:34", "12.3", "HANYTON", "小曾", "G1", List.of("UK"), "UK");
|
||||
DuplicateOccurrence occB = new DuplicateOccurrence("B09PNTZM4L", "2026-08-19 05:48", "10.1", "HANYTON", "郭悦晴", "G2", List.of("DE"), "DE");
|
||||
DuplicateShop shopA = new DuplicateShop("小曾", "G1", List.of("UK"), 1, 1);
|
||||
DuplicateShop shopB = new DuplicateShop("郭悦晴", "G2", List.of("DE"), 1, 1);
|
||||
DuplicateItem item = new DuplicateItem("B09PNTZM4L", 2, 2, List.of(occA, occB));
|
||||
DuplicateScanPayload payload = new DuplicateScanPayload(List.of(shopA, shopB), List.of(item));
|
||||
return new DuplicateScanView("2026-09-04 00:00:00", null, payload);
|
||||
}
|
||||
|
||||
@Test
|
||||
void addImportReplacesShopRowsAndReaggregates() throws Exception {
|
||||
when(scanService.loadLatest()).thenReturn(baseView());
|
||||
when(scanMapper.insert(any(ShopDataDuplicateScanEntity.class))).thenAnswer(invocation -> {
|
||||
invocation.<ShopDataDuplicateScanEntity>getArgument(0).setId(99L);
|
||||
return 1;
|
||||
});
|
||||
|
||||
ShopDataDuplicateCheckImportService.ImportSummary summary =
|
||||
service.importExcel("小曾", "UK,FR", workbook(), false);
|
||||
|
||||
assertThat(summary.totalRows()).isEqualTo(2);
|
||||
assertThat(summary.shopCount()).isEqualTo(2);
|
||||
assertThat(summary.recordCount()).isEqualTo(3);
|
||||
verify(scanMapper).insert(any(ShopDataDuplicateScanEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteImportRemovesShopRows() throws Exception {
|
||||
when(scanService.loadLatest()).thenReturn(baseView());
|
||||
when(scanMapper.insert(any(ShopDataDuplicateScanEntity.class))).thenAnswer(invocation -> {
|
||||
invocation.<ShopDataDuplicateScanEntity>getArgument(0).setId(99L);
|
||||
return 1;
|
||||
});
|
||||
|
||||
ShopDataDuplicateCheckImportService.ImportSummary summary =
|
||||
service.importExcel("小曾", "UK", workbook(), true);
|
||||
|
||||
assertThat(summary.shopCount()).isEqualTo(1);
|
||||
assertThat(summary.recordCount()).isEqualTo(1);
|
||||
verify(scanMapper).insert(any(ShopDataDuplicateScanEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void importWithoutBaselineFails() {
|
||||
when(scanService.loadLatest()).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> service.importExcel("小曾", "UK", workbook(), false))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.hasMessageContaining("暂无扫描结果");
|
||||
verify(scanMapper, never()).insert(any(ShopDataDuplicateScanEntity.class));
|
||||
}
|
||||
|
||||
/** 构造含 ASIN/日期两行的最小 xlsx。 */
|
||||
private byte[] workbook() throws Exception {
|
||||
var wb = new org.apache.poi.xssf.usermodel.XSSFWorkbook();
|
||||
var sheet = wb.createSheet("英国");
|
||||
var header = sheet.createRow(0);
|
||||
header.createCell(0).setCellValue("ASIN");
|
||||
header.createCell(1).setCellValue("日期");
|
||||
header.createCell(2).setCellValue("价格");
|
||||
header.createCell(3).setCellValue("品牌");
|
||||
var r1 = sheet.createRow(1);
|
||||
r1.createCell(0).setCellValue("B09PNTZM4L");
|
||||
r1.createCell(1).setCellValue("2026-09-02 10:00");
|
||||
r1.createCell(2).setCellValue("15.5");
|
||||
r1.createCell(3).setCellValue("HANYTON");
|
||||
var r2 = sheet.createRow(2);
|
||||
r2.createCell(0).setCellValue("B0TEST1234");
|
||||
r2.createCell(1).setCellValue("2026-09-03 11:30");
|
||||
r2.createCell(2).setCellValue("9.9");
|
||||
r2.createCell(3).setCellValue("OTHER");
|
||||
try (var out = new java.io.ByteArrayOutputStream()) {
|
||||
wb.write(out);
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user