feat(admin): A1 管理后台收敛 Java 单后台完整实现并修复 guard 误拦内部令牌
Build Backend JAR / build (push) Has been cancelled
Build Backend JAR / build (push) Has been cancelled
- Java 补齐后台全部迁移差集:shopduplicatecheck 店铺数据重复检查模块(V108 扫描表+查询/扫描服务)、 PinyinAbbrUtil 拼音缩写、ImageHistory 接口调整为内部可用、AdminUser 支持内部令牌操作并放宽列表上限 - Flask 后台 admin_api.py 路由收敛转发 Java、admin.html/admin.js 适配新后台形态 - AdminApiGuardFilter 对可信 X-Internal-Token 放行(controller 自校验兜底),修复客户端仅凭 内部令牌调用 /api/admin/shop-manages/credential 被误拦 401 - 测试:AdminApiGuardFilterTest 补可信/假令牌用例;AdminUserServiceTest 补菜单权限 mock; shopduplicatecheck 新增查询/聚合/CSV 单测
This commit is contained in:
@@ -62,6 +62,44 @@ class AdminApiGuardFilterTest {
|
||||
assertThat(response.getContentAsString()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void trustedInternalTokenBypassesGuardButReachesController() throws Exception {
|
||||
// 仅凭 X-Internal-Token 的合法内部调用(如 shop-manages/credential 由 controller 自校验)
|
||||
// 应被 guard 放行,而不是拦成 401 —— 修复客户端带令牌却被 guard 误拦的故障。
|
||||
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
|
||||
when(authSupport.isTrustedInternalToken(any())).thenReturn(true);
|
||||
AdminApiGuardFilter filter = newFilter(authSupport, true, "");
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/admin/shop-manages/credential");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
MockFilterChain chain = new MockFilterChain();
|
||||
|
||||
filter.doFilter(request, response, chain);
|
||||
|
||||
verify(authSupport, never()).requireUserOrInternal(any());
|
||||
assertThat(chain.getRequest()).isNotNull();
|
||||
assertThat(response.getContentAsString()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void untrustedTokenStillRejected() throws Exception {
|
||||
// 令牌不匹配(X-Internal-Token 错值/泄露)时 guard 仍然拦截
|
||||
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
|
||||
when(authSupport.isTrustedInternalToken(any())).thenReturn(false);
|
||||
when(authSupport.requireUserOrInternal(any()))
|
||||
.thenThrow(new BusinessException(401, "未登录"));
|
||||
AdminApiGuardFilter filter = newFilter(authSupport, true, "");
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/admin/shop-keys");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
MockFilterChain chain = new MockFilterChain();
|
||||
|
||||
filter.doFilter(request, response, chain);
|
||||
|
||||
JsonNode body = objectMapper.readTree(response.getContentAsString());
|
||||
assertThat(body.path("success").asBoolean()).isFalse();
|
||||
assertThat(body.path("code").asInt()).isEqualTo(401);
|
||||
assertThat(chain.getRequest()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void anonymousAdminApiRequestRejectedWith401Body() throws Exception {
|
||||
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
|
||||
|
||||
+2
@@ -40,6 +40,7 @@ class AdminUserServiceTest {
|
||||
request.setColumnIds(List.of(11L, 12L));
|
||||
|
||||
when(authSupport.currentRole(operator)).thenReturn("admin");
|
||||
when(permissionService.hasAnyAdminMenu(eq(operator), any())).thenReturn(true);
|
||||
when(passwordEncoder.hash("secret1")).thenReturn("hashed");
|
||||
when(userMapper.insert(any(AdminUserEntity.class))).thenAnswer(invocation -> {
|
||||
invocation.<AdminUserEntity>getArgument(0).setId(20L);
|
||||
@@ -73,6 +74,7 @@ class AdminUserServiceTest {
|
||||
request.setColumnIds(List.of(21L));
|
||||
|
||||
when(authSupport.currentRole(operator)).thenReturn("admin");
|
||||
when(permissionService.hasAnyAdminMenu(eq(operator), any())).thenReturn(true);
|
||||
when(userMapper.selectById(20L)).thenReturn(target);
|
||||
|
||||
service.updateUser(operator, 20L, request);
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package com.nanri.aiimage.modules.shopduplicatecheck;
|
||||
|
||||
import com.nanri.aiimage.modules.shopduplicatecheck.model.payload.DuplicateScanPayload;
|
||||
import com.nanri.aiimage.modules.shopduplicatecheck.service.ShopDataDuplicateCheckScanService.DuplicateScanView;
|
||||
import com.nanri.aiimage.modules.shopduplicatecheck.service.support.DuplicateCheckAggregator;
|
||||
import com.nanri.aiimage.modules.shopduplicatecheck.service.support.RawRow;
|
||||
import com.nanri.aiimage.modules.shopduplicatecheck.service.support.ShopParsed;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 撞款测试共享夹具:4 家店 / 3 组 / 6 个 ASIN,与 Python test_admin_shop_data_duplicate_check.py 的
|
||||
* 分布一致。经 DuplicateCheckAggregator 聚合出真实 payload/summary(shops/items 按扫描排序规则落序)。
|
||||
*/
|
||||
public final class TestFixtures {
|
||||
|
||||
private TestFixtures() {
|
||||
}
|
||||
|
||||
public static List<ShopParsed> shopParsed() {
|
||||
ShopParsed shopA = new ShopParsed("ShopA", "GroupA", List.of("UK", "DE"), List.of(
|
||||
new RawRow("A0000001", "2026-08-30", "GBP 9.99", "BrandA", "UK"),
|
||||
new RawRow("B0000001", "2026-08-30", "EUR 10.99", "BrandA", "DE"),
|
||||
new RawRow("E0000001", "2026年8月29日 上午4:34", "GBP 12.00", "BrandE", "UK")));
|
||||
ShopParsed shopB = new ShopParsed("ShopB", "GroupA", List.of("UK"), List.of(
|
||||
new RawRow("A0000001", "2026-08-30 08:30:00", "GBP 8.50", "BrandA", "UK"),
|
||||
new RawRow("E0000001", "2026-08-30", "GBP 12.00", "BrandE", "UK"),
|
||||
new RawRow("F0000001", "2026-08-28", "GBP 6.00", "BrandF", "UK")));
|
||||
ShopParsed shopC = new ShopParsed("ShopC", "GroupC", List.of("FR"), List.of(
|
||||
new RawRow("C0000001", "2026-08-29", "EUR 7.50", "BrandC", "FR"),
|
||||
new RawRow("E0000001", "2026-08-31", "EUR 12.00", "BrandE", "FR")));
|
||||
ShopParsed shopD = new ShopParsed("ShopD", "GroupD", List.of("UK"), List.of(
|
||||
new RawRow("D0000001", "2026-08-28", "GBP 5.00", "BrandD", "UK"),
|
||||
new RawRow("F0000001", "2026-08-30", "GBP 6.00", "BrandF", "UK")));
|
||||
return List.of(shopA, shopB, shopC, shopD);
|
||||
}
|
||||
|
||||
public static DuplicateScanPayload payload() {
|
||||
return DuplicateCheckAggregator.aggregate(shopParsed(), "job").payload();
|
||||
}
|
||||
|
||||
public static DuplicateScanView view() {
|
||||
DuplicateCheckAggregator.Aggregate aggregate = DuplicateCheckAggregator.aggregate(shopParsed(), "job");
|
||||
return new DuplicateScanView("2026-09-04 03:10:00", aggregate.summary(), aggregate.payload());
|
||||
}
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
package com.nanri.aiimage.modules.shopduplicatecheck.service;
|
||||
|
||||
import com.nanri.aiimage.modules.shopduplicatecheck.TestFixtures;
|
||||
import com.nanri.aiimage.modules.shopduplicatecheck.mapper.ShopDuplicateCheckSourceMapper;
|
||||
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.DuplicateScanSummary;
|
||||
import com.nanri.aiimage.modules.shopduplicatecheck.model.payload.DuplicateShop;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
class ShopDataDuplicateCheckQueryServiceTest {
|
||||
|
||||
private ShopDataDuplicateCheckQueryService service;
|
||||
private ShopDataDuplicateCheckScanService.DuplicateScanView view;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
ShopDuplicateCheckSourceMapper mapper = mock(ShopDuplicateCheckSourceMapper.class);
|
||||
service = new ShopDataDuplicateCheckQueryService(mapper);
|
||||
view = TestFixtures.view();
|
||||
}
|
||||
|
||||
private ShopDataDuplicateCheckQueryService.Filters noFilter() {
|
||||
return ShopDataDuplicateCheckQueryService.Filters.clean("", "", "", "", "", "");
|
||||
}
|
||||
|
||||
@Test
|
||||
void overviewSuperAdminSeesAll() {
|
||||
Map<String, Object> data = service.overviewData(view, null);
|
||||
assertThat(data.get("pending")).isEqualTo(false);
|
||||
assertThat(data.get("scanned_at")).isEqualTo("2026-09-04 03:10:00");
|
||||
DuplicateScanSummary summary = (DuplicateScanSummary) data.get("summary");
|
||||
assertThat(summary.asinTotal()).isEqualTo(6);
|
||||
assertThat(summary.recordTotal()).isEqualTo(10);
|
||||
assertThat(summary.duplicateAsinTotal()).isEqualTo(3);
|
||||
assertThat(summary.shopCount()).isEqualTo(4);
|
||||
assertThat(((List<?>) data.get("shops"))).hasSize(4);
|
||||
}
|
||||
|
||||
@Test
|
||||
void leaderOverviewStatsRecomputedAfterCrop() {
|
||||
Set<String> visible = Set.of("shopa", "shopb");
|
||||
Map<String, Object> data = service.overviewData(view, visible);
|
||||
DuplicateScanSummary summary = (DuplicateScanSummary) data.get("summary");
|
||||
assertThat(summary.shopCount()).isEqualTo(2);
|
||||
assertThat(summary.asinTotal()).isEqualTo(4);
|
||||
assertThat(summary.recordTotal()).isEqualTo(6);
|
||||
assertThat(summary.duplicateAsinTotal()).isEqualTo(2);
|
||||
assertThat(summary.duplicateShopCount()).isEqualTo(2);
|
||||
assertThat(summary.siteCount()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void leaderSeesOnlyOwnGroupShops() {
|
||||
Set<String> visible = Set.of("shopa", "shopb");
|
||||
Map<String, Object> data = service.itemsData(view, visible, 1, 20, "all", noFilter());
|
||||
@SuppressWarnings("unchecked")
|
||||
List<DuplicateShop> shops = (List<DuplicateShop>) data.get("shops");
|
||||
assertThat(shops.stream().map(DuplicateShop::shopName).toList())
|
||||
.containsExactly("ShopA", "ShopB");
|
||||
assertThat(data.get("total")).isEqualTo(4);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<DuplicateItem> items = (List<DuplicateItem>) data.get("items");
|
||||
DuplicateItem a = items.stream().filter(item -> "A0000001".equals(item.asin())).findFirst().orElseThrow();
|
||||
assertThat(a.shopCount()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void leaderSeesNothingWithoutAnyGroup() {
|
||||
Map<String, Object> data = service.itemsData(view, Set.of(), 1, 20, "all", noFilter());
|
||||
assertThat(data.get("shops")).isEqualTo(List.of());
|
||||
assertThat(data.get("total")).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void monitorOnlyKeepsRepeatedAsins() {
|
||||
Map<String, Object> data = service.itemsData(view, null, 1, 20, "monitor", noFilter());
|
||||
assertThat(data.get("total")).isEqualTo(3);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<DuplicateShop> shops = (List<DuplicateShop>) data.get("shops");
|
||||
assertThat(shops.stream().map(DuplicateShop::shopName).toList())
|
||||
.containsExactly("ShopA", "ShopB", "ShopC", "ShopD");
|
||||
}
|
||||
|
||||
@Test
|
||||
void itemFilterByAsinAndSite() {
|
||||
ShopDataDuplicateCheckQueryService.Filters filters =
|
||||
ShopDataDuplicateCheckQueryService.Filters.clean("A0000001", "", "", "UK", "", "");
|
||||
Map<String, Object> data = service.itemsData(view, null, 1, 20, "all", filters);
|
||||
assertThat(data.get("total")).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailSortedByShopCountThenEarliestShelfDesc() {
|
||||
Map<String, Object> data = service.detailData(view, null, 1, 24, noFilter());
|
||||
assertThat(data.get("total")).isEqualTo(3);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> items = (List<Map<String, Object>>) data.get("items");
|
||||
assertThat(items).extracting(map -> map.get("asin")).containsExactly("E0000001", "A0000001", "F0000001");
|
||||
Map<String, Object> e = items.get(0);
|
||||
assertThat(e.get("first_date")).isEqualTo("2026-08-29 04:34:00");
|
||||
assertThat(e.get("brand")).isEqualTo("BrandE");
|
||||
assertThat(e.get("shop_names")).isEqualTo(List.of("ShopA", "ShopB", "ShopC"));
|
||||
@SuppressWarnings("unchecked")
|
||||
List<DuplicateOccurrence> occurrences = (List<DuplicateOccurrence>) e.get("occurrences");
|
||||
assertThat(occurrences).extracting(DuplicateOccurrence::date)
|
||||
.containsExactly("2026年8月29日 上午4:34", "2026-08-30", "2026-08-31");
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailLeaderCropRemovesSingleShopAsin() {
|
||||
Set<String> visible = Set.of("shopa", "shopb");
|
||||
Map<String, Object> data = service.detailData(view, visible, 1, 24, noFilter());
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> items = (List<Map<String, Object>>) data.get("items");
|
||||
// E 的 ShopC 记录被裁掉后剩 2 店;A/E 同为 2 店,按"最早上架晚者在前" A 早于 E
|
||||
assertThat(items).extracting(map -> map.get("asin")).containsExactly("A0000001", "E0000001");
|
||||
}
|
||||
|
||||
@Test
|
||||
void pendingShapes() {
|
||||
Map<String, Object> overview = service.pendingOverview();
|
||||
assertThat(overview.get("pending")).isEqualTo(true);
|
||||
assertThat(overview.get("summary")).isEqualTo(Map.of());
|
||||
Map<String, Object> list = service.pendingList(1, 20);
|
||||
assertThat(list.get("total")).isEqualTo(0);
|
||||
assertThat(list.get("scanned_at")).isEqualTo("");
|
||||
}
|
||||
|
||||
@Test
|
||||
void exportCsvBomHeaderAndMonitorRows() throws Exception {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
service.writeExport(view, null, "monitor", noFilter(), out);
|
||||
byte[] bytes = out.toByteArray();
|
||||
assertThat(bytes[0]).isEqualTo((byte) 0xEF);
|
||||
assertThat(bytes[1]).isEqualTo((byte) 0xBB);
|
||||
assertThat(bytes[2]).isEqualTo((byte) 0xBF);
|
||||
String text = new String(bytes, StandardCharsets.UTF_8);
|
||||
String[] lines = text.split("\r\n");
|
||||
assertThat(lines[0]).endsWith("ASIN,店铺数,店铺,分组,站点,上架时间,价格,品牌");
|
||||
// 数据行 = E(3 记录) + A(2) + F(2) = 7,非空行总数为 8(尾随 CRLF 会产生一个空元素)
|
||||
long nonEmpty = java.util.Arrays.stream(lines).filter(line -> !line.isEmpty()).count();
|
||||
assertThat(nonEmpty).isEqualTo(8);
|
||||
assertThat(lines[1]).startsWith("E0000001,3,");
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package com.nanri.aiimage.modules.shopduplicatecheck.service.support;
|
||||
|
||||
import com.nanri.aiimage.modules.shopduplicatecheck.TestFixtures;
|
||||
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.DuplicateScanSummary;
|
||||
import com.nanri.aiimage.modules.shopduplicatecheck.model.payload.DuplicateShop;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class DuplicateCheckAggregatorTest {
|
||||
|
||||
@Test
|
||||
void aggregatesFixtureExactlyLikePython() {
|
||||
DuplicateCheckAggregator.Aggregate aggregate =
|
||||
DuplicateCheckAggregator.aggregate(TestFixtures.shopParsed(), "job");
|
||||
DuplicateScanSummary summary = aggregate.summary();
|
||||
assertThat(summary.shopCount()).isEqualTo(4);
|
||||
assertThat(summary.asinTotal()).isEqualTo(6);
|
||||
assertThat(summary.recordTotal()).isEqualTo(10);
|
||||
assertThat(summary.duplicateAsinTotal()).isEqualTo(3);
|
||||
assertThat(summary.duplicateShopCount()).isEqualTo(4);
|
||||
assertThat(summary.siteCount()).isEqualTo(3);
|
||||
assertThat(summary.asinPerShop()).isEqualTo(2.5);
|
||||
assertThat(summary.source()).isEqualTo("job");
|
||||
|
||||
List<DuplicateShop> shops = aggregate.payload().shops();
|
||||
assertThat(shops.stream().map(DuplicateShop::shopName).toList())
|
||||
.containsExactly("ShopA", "ShopB", "ShopC", "ShopD");
|
||||
assertThat(shops.stream().map(DuplicateShop::asinCount).toList())
|
||||
.containsExactly(3, 3, 2, 2);
|
||||
|
||||
List<DuplicateItem> items = aggregate.payload().items();
|
||||
// 扫描排序:店铺数倒序 → ASIN 升序
|
||||
assertThat(items.stream().map(DuplicateItem::asin).toList())
|
||||
.containsExactly("E0000001", "A0000001", "F0000001", "B0000001", "C0000001", "D0000001");
|
||||
}
|
||||
|
||||
@Test
|
||||
void occurrenceCarriesSheetCountry() {
|
||||
DuplicateCheckAggregator.Aggregate aggregate =
|
||||
DuplicateCheckAggregator.aggregate(TestFixtures.shopParsed(), "job");
|
||||
DuplicateItem e = aggregate.payload().items().get(0);
|
||||
assertThat(e.shopCount()).isEqualTo(3);
|
||||
assertThat(e.recordCount()).isEqualTo(3);
|
||||
assertThat(e.occurrences().stream()
|
||||
.map(DuplicateOccurrence::country).toList())
|
||||
.containsExactlyInAnyOrder("UK", "UK", "FR");
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.nanri.aiimage.modules.shopduplicatecheck.service.support;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class DuplicateCheckCsvWriterTest {
|
||||
|
||||
@Test
|
||||
void writesBomCrlfAndMinimalQuoting() throws Exception {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
DuplicateCheckCsvWriter.write(out, List.of(
|
||||
List.of("A", "B"),
|
||||
List.of("with,comma", "say \"hi\"", "two\nlines")));
|
||||
byte[] bytes = out.toByteArray();
|
||||
assertThat(bytes[0]).isEqualTo((byte) 0xEF);
|
||||
assertThat(bytes[1]).isEqualTo((byte) 0xBB);
|
||||
assertThat(bytes[2]).isEqualTo((byte) 0xBF);
|
||||
String text = new String(bytes, StandardCharsets.UTF_8);
|
||||
// BOM 为首个字符,其后紧跟表头
|
||||
assertThat((int) text.charAt(0)).isEqualTo(0xFEFF);
|
||||
assertThat(text.charAt(1)).isEqualTo('A');
|
||||
assertThat(text).contains("A,B\r\n");
|
||||
assertThat(text).contains("\"with,comma\"");
|
||||
assertThat(text).contains("\"say \"\"hi\"\"\"");
|
||||
assertThat(text).contains("\"two\nlines\"");
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package com.nanri.aiimage.modules.shopduplicatecheck.service.support;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class DuplicateCheckTimeNormalizerTest {
|
||||
|
||||
@Test
|
||||
void normalizesPythonSortTimeCases() {
|
||||
assertThat(DuplicateCheckTimeNormalizer.normalizeSortTime("2026-08-30"))
|
||||
.isEqualTo("2026-08-30 00:00:00");
|
||||
assertThat(DuplicateCheckTimeNormalizer.normalizeSortTime("2026-08-30 08:30:00"))
|
||||
.isEqualTo("2026-08-30 08:30:00");
|
||||
assertThat(DuplicateCheckTimeNormalizer.normalizeSortTime("2026年8月29日 上午4:34"))
|
||||
.isEqualTo("2026-08-29 04:34:00");
|
||||
assertThat(DuplicateCheckTimeNormalizer.normalizeSortTime("2026年8月29日 下午2:15"))
|
||||
.isEqualTo("2026-08-29 14:15:00");
|
||||
assertThat(DuplicateCheckTimeNormalizer.normalizeSortTime("2026年8月29日 晚上8:00"))
|
||||
.isEqualTo("2026-08-29 20:00:00");
|
||||
assertThat(DuplicateCheckTimeNormalizer.normalizeSortTime("2026年8月29日 凌晨12:05"))
|
||||
.isEqualTo("2026-08-29 00:05:00");
|
||||
assertThat(DuplicateCheckTimeNormalizer.normalizeSortTime("2026/8/18 4:34"))
|
||||
.isEqualTo("2026-08-18 04:34:00");
|
||||
assertThat(DuplicateCheckTimeNormalizer.normalizeSortTime("未知日期"))
|
||||
.isEqualTo("未知日期");
|
||||
assertThat(DuplicateCheckTimeNormalizer.normalizeSortTime(""))
|
||||
.isEqualTo("");
|
||||
assertThat(DuplicateCheckTimeNormalizer.normalizeSortTime(null))
|
||||
.isEqualTo("");
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidCalendarDateFallsBackToOriginal() {
|
||||
assertThat(DuplicateCheckTimeNormalizer.normalizeSortTime("2026-13-01 04:34"))
|
||||
.isEqualTo("2026-13-01 04:34");
|
||||
}
|
||||
|
||||
@Test
|
||||
void dateKeyNormalizesCommonFormats() {
|
||||
assertThat(DuplicateCheckTimeNormalizer.dateKey("2026-08-30T08:30")).isEqualTo("2026-08-30");
|
||||
assertThat(DuplicateCheckTimeNormalizer.dateKey("2026-08-30 08:30:00")).isEqualTo("2026-08-30");
|
||||
assertThat(DuplicateCheckTimeNormalizer.dateKey("2026年8月19日 上午4:34")).isEqualTo("2026-08-19");
|
||||
assertThat(DuplicateCheckTimeNormalizer.dateKey("2026.8.19")).isEqualTo("2026-08-19");
|
||||
assertThat(DuplicateCheckTimeNormalizer.dateKey("2026/8/19")).isEqualTo("2026-08-19");
|
||||
assertThat(DuplicateCheckTimeNormalizer.dateKey("abc")).isEqualTo("abc");
|
||||
assertThat(DuplicateCheckTimeNormalizer.dateKey("")).isEqualTo("");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user