feat: 外观专利改直连 LLM(DeepSeek/Gemini)、无效ASIN菜单更名 BRAND_DB、紫鸟店铺索引场景化
Build Backend JAR / build (push) Has been cancelled

- AppearancePatent:Coze 工作流切换为 llm-host 直连模型(title/appearance 双模型、行并发、重试),配置键迁移并保留旧环境变量兜底
- InvalidAsinDataMapper 补充按值+品牌唯一键查询
- ZiniaoShopIndexService 新增店铺分类/索引场景
- V98:无效ASIN菜单 rename 补充 BRAND_DB;docs 架构优化规划
- 配套单测更新
This commit is contained in:
2026-08-31 17:02:59 +08:00
parent 14a41e64f1
commit c8cd851b2f
20 changed files with 1291 additions and 2338 deletions
@@ -17,6 +17,8 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
@@ -82,6 +84,8 @@ class ZiniaoShopIndexServiceTest {
when(ziniaoAuthService.getOrLoadStaffForIndex("allowed-key", 2L)).thenReturn(List.of(staff(22L)));
when(ziniaoAuthService.getOrLoadUserStoresForIndex("allowed-key", 2L, 22L))
.thenReturn(List.of(shop("shop-2", "店铺B")));
when(ziniaoMemoryStoreService.listAliveEntitiesByType(
ZiniaoMemoryStoreService.CACHE_TYPE_SHOP_INDEX_ENTRY, 10000)).thenReturn(List.of());
service.refreshShopIndex();
@@ -98,7 +102,6 @@ class ZiniaoShopIndexServiceTest {
any(ZiniaoShopIndexEntryDto.class),
any(Duration.class)
);
verify(ziniaoMemoryStoreService, never()).listAliveEntitiesByType(any(), anyInt());
verify(ziniaoApiKeyProvider).markIpWhitelistBlocked(
eq(blocked),
eq("当前服务器 IP 未加入紫鸟白名单")
@@ -149,7 +152,6 @@ class ZiniaoShopIndexServiceTest {
any(ZiniaoShopIndexEntryDto.class),
any(Duration.class)
);
verify(ziniaoMemoryStoreService, never()).listAliveEntitiesByType(any(), anyInt());
verify(ziniaoApiKeyProvider).markIpWhitelistBlocked(
eq(partiallyBlocked),
eq("当前服务器 IP 未加入紫鸟白名单")
@@ -158,6 +160,76 @@ class ZiniaoShopIndexServiceTest {
verify(ziniaoApiKeyProvider, never()).markIpWhitelistAllowed(partiallyBlocked);
}
@Test
void whitelistBlockedApiKeyMarksItsActiveIndexRowsAsBypassEligible() throws Exception {
stubIpWhitelistDetection();
ZiniaoApiKeyProvider.ApiKeyAccount blocked = new ZiniaoApiKeyProvider.ApiKeyAccount("blocked-key", "blocked-account");
ZiniaoApiKeyProvider.ApiKeyAccount allowed = new ZiniaoApiKeyProvider.ApiKeyAccount("allowed-key", "allowed-account");
when(ziniaoApiKeyProvider.listApiKeyAccounts()).thenReturn(List.of(blocked, allowed));
when(ziniaoAuthService.resolveCompanyIdForIndex("blocked-key"))
.thenThrow(new BusinessException("当前服务器 IP 未加入紫鸟白名单"));
when(ziniaoAuthService.resolveCompanyIdForIndex("allowed-key")).thenReturn(2L);
when(ziniaoAuthService.getOrLoadStaffForIndex("allowed-key", 2L)).thenReturn(List.of(staff(22L)));
when(ziniaoAuthService.getOrLoadUserStoresForIndex("allowed-key", 2L, 22L))
.thenReturn(List.of(shop("shop-2", "店铺B")));
ZiniaoShopIndexEntryDto blockedDto = new ZiniaoShopIndexEntryDto();
blockedDto.setNormalizedShopName("blocked-shop");
blockedDto.setStatus("ACTIVE");
blockedDto.setApiKeyHash(sha256("blocked-key"));
blockedDto.setLastRefreshedAt(1000L);
ZiniaoMemoryStoreEntity blockedRow = new ZiniaoMemoryStoreEntity();
blockedRow.setId(1L);
blockedRow.setCacheType("SHOP_INDEX_ENTRY");
blockedRow.setCacheKey("s:blocked-shop");
blockedRow.setPayloadJson(new ObjectMapper().writeValueAsString(blockedDto));
blockedRow.setExpiresAt(LocalDateTime.now().plusHours(12));
ZiniaoShopIndexEntryDto allowedDto = new ZiniaoShopIndexEntryDto();
allowedDto.setNormalizedShopName("allowed-shop");
allowedDto.setStatus("ACTIVE");
allowedDto.setApiKeyHash(sha256("allowed-key"));
allowedDto.setLastRefreshedAt(1000L);
ZiniaoMemoryStoreEntity allowedRow = new ZiniaoMemoryStoreEntity();
allowedRow.setId(2L);
allowedRow.setCacheType("SHOP_INDEX_ENTRY");
allowedRow.setCacheKey("s:allowed-shop");
allowedRow.setPayloadJson(new ObjectMapper().writeValueAsString(allowedDto));
allowedRow.setExpiresAt(LocalDateTime.now().plusHours(12));
when(ziniaoMemoryStoreService.listAliveEntitiesByType("SHOP_INDEX_ENTRY", 10000))
.thenReturn(List.of(blockedRow, allowedRow));
service.refreshShopIndex();
ArgumentCaptor<List<ZiniaoMemoryStoreEntity>> captor = ArgumentCaptor.forClass(List.class);
verify(ziniaoMemoryStoreService).updateStaleMarks(captor.capture());
assertEquals(1, captor.getValue().size());
ZiniaoMemoryStoreEntity updated = captor.getValue().get(0);
assertEquals(1L, updated.getId());
var payload = new ObjectMapper().readTree(updated.getPayloadJson());
assertEquals("IP_WHITELIST", payload.get("refreshBlockedReason").asText());
assertEquals("ACTIVE", payload.get("status").asText());
assertTrue(payload.get("lastRefreshBlockedAt").asLong() >= 1000L,
"lastRefreshBlockedAt 应不小于行内 lastRefreshedAt,否则查询侧豁免条件不成立");
}
@Test
void laterWhitelistClearRemovesBlockedMarksFromIndexRows() throws Exception {
ZiniaoApiKeyProvider.ApiKeyAccount key = new ZiniaoApiKeyProvider.ApiKeyAccount("key", "acct");
when(ziniaoApiKeyProvider.listApiKeyAccounts()).thenReturn(List.of(key));
when(ziniaoAuthService.resolveCompanyIdForIndex("key")).thenReturn(1L);
when(ziniaoAuthService.getOrLoadStaffForIndex("key", 1L)).thenReturn(List.of(staff(11L)));
when(ziniaoAuthService.getOrLoadUserStoresForIndex("key", 1L, 11L))
.thenReturn(List.of(shop("s-1", "shop-1")));
when(ziniaoMemoryStoreService.listAliveEntitiesByType("SHOP_INDEX_ENTRY", 10000)).thenReturn(List.of());
service.refreshShopIndex();
verify(ziniaoMemoryStoreService, never()).updateStaleMarks(anyList());
}
@Test
void nonWhitelistCompanyFailureDoesNotOverwriteWhitelistStatus() {
ZiniaoApiKeyProvider.ApiKeyAccount failed = new ZiniaoApiKeyProvider.ApiKeyAccount("failed-key", "failed-account");
@@ -327,6 +399,16 @@ class ZiniaoShopIndexServiceTest {
return (ZiniaoShopIndexRefreshCursorDto) captor.getAllValues().getLast();
}
private String sha256(String value) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(value.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : hash) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
private void stubIpWhitelistDetection() {
when(ziniaoAuthService.isIpWhitelistError(any(BusinessException.class)))
.thenAnswer(invocation -> invocation.<BusinessException>getArgument(0).getMessage().contains("白名单"));