task-48: invalid ASIN 记录改为批量 INSERT IGNORE/upsert,唯一键扩为 (data_value, brand)
This commit is contained in:
+205
@@ -0,0 +1,205 @@
|
||||
package com.nanri.aiimage.modules.collectdata.util;
|
||||
|
||||
import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataResultRowVo;
|
||||
import com.nanri.aiimage.modules.invalidasin.mapper.InvalidAsinDataMapper;
|
||||
import com.nanri.aiimage.modules.invalidasin.model.entity.InvalidAsinDataEntity;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Task 48:将 invalid ASIN 记录改为批量 INSERT IGNORE/upsert。
|
||||
* CollectDataInvalidAsinBatchWriter 按批次把不合规 ASIN 行批量写入
|
||||
* biz_invalid_asin_data(INSERT IGNORE,按唯一键幂等),空批次/无合法
|
||||
* data_value 的批次不发起任何 insertBatchIgnore 调用;远程失败降级记录
|
||||
* 计数(跳过写入),可恢复后继续,不抛出中断整个提交流程的异常。
|
||||
*/
|
||||
class CollectDataInvalidAsinBatchWriterTest {
|
||||
|
||||
private InvalidAsinDataMapper invalidAsinDataMapper;
|
||||
private CollectDataInvalidAsinBatchWriter writer;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
invalidAsinDataMapper = mock(InvalidAsinDataMapper.class);
|
||||
writer = new CollectDataInvalidAsinBatchWriter(invalidAsinDataMapper, 10);
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_048_asin_normal_default_path() {
|
||||
// 正常输入:批量 INSERT IGNORE,实体字段完整,返回写入行数。
|
||||
when(invalidAsinDataMapper.insertBatchIgnore(anyList())).thenReturn(2);
|
||||
|
||||
int written = writer.writeBatch(List.of(
|
||||
row("B000000001", "Zara"),
|
||||
row("B000000002", "nike")
|
||||
));
|
||||
|
||||
assertEquals(2, written, "两行都写入");
|
||||
ArgumentCaptor<List<InvalidAsinDataEntity>> captor = ArgumentCaptor.forClass(List.class);
|
||||
verify(invalidAsinDataMapper).insertBatchIgnore(captor.capture());
|
||||
List<InvalidAsinDataEntity> entities = captor.getValue();
|
||||
assertEquals(2, entities.size(), "按行批量写入");
|
||||
assertEquals("B000000001", entities.get(0).getDataValue(), "data_value 为 ASIN");
|
||||
assertEquals("zara", entities.get(0).getBrand(), "brand 小写标准化");
|
||||
assertEquals("AUTO", entities.get(0).getRecordSource(), "record_source 保持 AUTO");
|
||||
assertEquals("nike", entities.get(1).getBrand(), "第二行 brand 小写标准化");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_048_asin_normal_multiple_items() {
|
||||
// 批量场景:100 行 = 10 批次,顺序稳定不丢失,每批实体数量正确。
|
||||
when(invalidAsinDataMapper.insertBatchIgnore(anyList())).thenReturn(10);
|
||||
List<CollectDataResultRowVo> rows = new ArrayList<>();
|
||||
for (int i = 0; i < 100; i++) {
|
||||
rows.add(row("B" + String.format("%09d", i + 1), "brand-" + (i % 7)));
|
||||
}
|
||||
|
||||
int written = writer.writeBatch(rows);
|
||||
|
||||
assertEquals(100, written, "全部写入");
|
||||
ArgumentCaptor<List<InvalidAsinDataEntity>> captor = ArgumentCaptor.forClass(List.class);
|
||||
verify(invalidAsinDataMapper, times(10)).insertBatchIgnore(captor.capture());
|
||||
for (int i = 0; i < 10; i++) {
|
||||
List<InvalidAsinDataEntity> batch = captor.getAllValues().get(i);
|
||||
assertEquals(10, batch.size(), "批次 " + i + " 满 10 行");
|
||||
}
|
||||
assertEquals("B000000001", captor.getAllValues().get(0).get(0).getDataValue(),
|
||||
"顺序稳定");
|
||||
assertEquals("B000000100", captor.getAllValues().get(9).get(9).getDataValue(),
|
||||
"末批末行不丢失");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_048_asin_normal_repeated_operation_is_idempotent() {
|
||||
// 幂等:重复执行同一输入不产生重复记录(唯一键 + INSERT IGNORE 语义),
|
||||
// 每行最多被批量写入一次,空品牌行跳过不写。
|
||||
when(invalidAsinDataMapper.insertBatchIgnore(anyList())).thenAnswer(
|
||||
invocation -> ((List<?>) invocation.getArgument(0)).size());
|
||||
List<CollectDataResultRowVo> rows = List.of(
|
||||
row("B000000001", "Nike"),
|
||||
row("B000000002", "Zara"),
|
||||
row("B000000003", "")
|
||||
);
|
||||
|
||||
int first = writer.writeBatch(rows);
|
||||
int second = writer.writeBatch(rows);
|
||||
|
||||
assertEquals(2, first, "首轮 2 个合法行写入");
|
||||
assertEquals(2, second, "重复执行同样只写合法行(幂等键由 SQL 保证)");
|
||||
ArgumentCaptor<List<InvalidAsinDataEntity>> captor = ArgumentCaptor.forClass(List.class);
|
||||
verify(invalidAsinDataMapper, times(2)).insertBatchIgnore(captor.capture());
|
||||
assertEquals(2, captor.getAllValues().get(1).size(), "重复批次不含空品牌行");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_048_asin_boundary_empty_input() {
|
||||
// 空输入:空列表与全空行均不发起任何批量写入调用,返回 0。
|
||||
int written = writer.writeBatch(null);
|
||||
assertEquals(0, written, "null 输入返回 0");
|
||||
verify(invalidAsinDataMapper, never()).insertBatchIgnore(anyList());
|
||||
|
||||
int blankWritten = writer.writeBatch(List.of(row("", "nike"), row("B000000001", "")));
|
||||
assertEquals(0, blankWritten, "无合法行返回 0");
|
||||
verify(invalidAsinDataMapper, never()).insertBatchIgnore(anyList());
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_048_asin_boundary_single_item() {
|
||||
// 单元素:单行单品牌正常批量写入;批次大小 1 的边界也正常。
|
||||
when(invalidAsinDataMapper.insertBatchIgnore(anyList())).thenReturn(1);
|
||||
|
||||
int written = writer.writeBatch(List.of(row("B000000001", "solo")));
|
||||
|
||||
assertEquals(1, written, "单行写入成功");
|
||||
ArgumentCaptor<List<InvalidAsinDataEntity>> captor = ArgumentCaptor.forClass(List.class);
|
||||
verify(invalidAsinDataMapper).insertBatchIgnore(captor.capture());
|
||||
assertEquals(1, captor.getValue().size(), "单行批次");
|
||||
assertEquals("solo", captor.getValue().get(0).getBrand(), "brand 小写");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_048_asin_boundary_limit_and_overflow() {
|
||||
// 上限/超限:批次大小 3,10 行 = 4 批(3+3+3+1),无无界积累。
|
||||
when(invalidAsinDataMapper.insertBatchIgnore(anyList())).thenAnswer(
|
||||
invocation -> ((List<?>) invocation.getArgument(0)).size());
|
||||
CollectDataInvalidAsinBatchWriter smallWriter =
|
||||
new CollectDataInvalidAsinBatchWriter(invalidAsinDataMapper, 3);
|
||||
List<CollectDataResultRowVo> rows = new ArrayList<>();
|
||||
for (int i = 0; i < 10; i++) {
|
||||
rows.add(row("B" + String.format("%09d", i + 1), "brand"));
|
||||
}
|
||||
|
||||
int written = smallWriter.writeBatch(rows);
|
||||
|
||||
assertEquals(10, written, "全部写入");
|
||||
ArgumentCaptor<List<InvalidAsinDataEntity>> captor = ArgumentCaptor.forClass(List.class);
|
||||
verify(invalidAsinDataMapper, times(4)).insertBatchIgnore(captor.capture());
|
||||
assertEquals(3, captor.getAllValues().get(0).size(), "首批 3 行");
|
||||
assertEquals(3, captor.getAllValues().get(2).size(), "第三批 3 行");
|
||||
assertEquals(1, captor.getAllValues().get(3).size(), "末批 1 行");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_048_asin_invalid_input_rejected() {
|
||||
// 非法参数:缺失 ASIN 或缺失品牌的行跳过,不写入、不抛异常;
|
||||
// null 行安全跳过不计数。
|
||||
when(invalidAsinDataMapper.insertBatchIgnore(anyList())).thenReturn(1);
|
||||
List<CollectDataResultRowVo> rows = new ArrayList<>();
|
||||
rows.add(null);
|
||||
rows.add(row(null, "nike"));
|
||||
rows.add(row("B000000001", null));
|
||||
rows.add(row(" B000000002 ", " nike "));
|
||||
|
||||
int written = writer.writeBatch(rows);
|
||||
|
||||
assertEquals(1, written, "仅完全合法的 1 行写入");
|
||||
ArgumentCaptor<List<InvalidAsinDataEntity>> captor = ArgumentCaptor.forClass(List.class);
|
||||
verify(invalidAsinDataMapper).insertBatchIgnore(captor.capture());
|
||||
assertEquals(1, captor.getValue().size(), "跳过非法行");
|
||||
assertEquals("B000000002", captor.getValue().get(0).getDataValue(),
|
||||
"ASIN trim 后写入");
|
||||
assertEquals("nike", captor.getValue().get(0).getBrand(), "brand trim 后小写");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_048_asin_dependency_failure_releases_resources() {
|
||||
// 依赖失败:批量写入抛错时跳过该批(不中断、不泄漏),恢复后继续写入,
|
||||
// 不抛出中断整个提交流程的异常。
|
||||
doThrow(new RuntimeException("db down"))
|
||||
.doReturn(2)
|
||||
.when(invalidAsinDataMapper).insertBatchIgnore(anyList());
|
||||
List<CollectDataResultRowVo> rows = new ArrayList<>();
|
||||
for (int i = 0; i < 20; i++) {
|
||||
rows.add(row("B" + String.format("%09d", i + 1), "brand"));
|
||||
}
|
||||
|
||||
int written = writer.writeBatch(rows);
|
||||
|
||||
assertEquals(2, written, "首批失败跳过,第二批 2 行写入");
|
||||
verify(invalidAsinDataMapper, times(2)).insertBatchIgnore(anyList());
|
||||
ArgumentCaptor<List<InvalidAsinDataEntity>> captor = ArgumentCaptor.forClass(List.class);
|
||||
verify(invalidAsinDataMapper, times(2)).insertBatchIgnore(captor.capture());
|
||||
assertTrue(captor.getAllValues().get(1).size() > 0, "恢复后批次非空");
|
||||
}
|
||||
|
||||
private static CollectDataResultRowVo row(String asin, String brand) {
|
||||
CollectDataResultRowVo row = new CollectDataResultRowVo();
|
||||
row.setAsin(asin);
|
||||
row.setBrand(brand);
|
||||
return row;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user