紫鸟查询店铺更新

This commit is contained in:
super
2026-04-02 01:15:50 +08:00
parent 63043c56d4
commit 1fdef3c67f
17 changed files with 891 additions and 51 deletions
@@ -1,12 +1,14 @@
package com.nanri.aiimage.modules.ziniao.controller;
import com.nanri.aiimage.common.api.ApiResponse;
import com.nanri.aiimage.modules.ziniao.model.cache.ZiniaoShopIndexRefreshCursorDto;
import com.nanri.aiimage.modules.ziniao.model.dto.ZiniaoOpenShopRequest;
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoOpenShopVo;
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoSessionVo;
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoShopListVo;
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoStaffListVo;
import com.nanri.aiimage.modules.ziniao.service.ZiniaoAuthService;
import com.nanri.aiimage.modules.ziniao.service.ZiniaoShopIndexService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -28,6 +30,7 @@ import org.springframework.web.bind.annotation.RestController;
public class ZiniaoAuthController {
private final ZiniaoAuthService ziniaoAuthService;
private final ZiniaoShopIndexService ziniaoShopIndexService;
@GetMapping("/session")
@Operation(summary = "获取紫鸟会话状态", description = "自动获取或复用 appToken,并返回 companyId、当前员工 userId、脱敏 token 和当前店铺信息。")
@@ -47,6 +50,12 @@ public class ZiniaoAuthController {
return ApiResponse.success(ziniaoAuthService.listStaff());
}
@GetMapping("/index-refresh")
@Operation(summary = "获取紫鸟店铺索引刷新状态", description = "返回后台定时刷新紫鸟店铺索引的最近一次执行状态、时间和无效 userId 样本。")
public ApiResponse<ZiniaoShopIndexRefreshCursorDto> getIndexRefreshStatus() {
return ApiResponse.success(ziniaoShopIndexService.getRefreshCursor());
}
@GetMapping("/shops")
@Operation(summary = "获取员工可见店铺列表", description = "先通过 API Key 获取 companyId,再按员工 userId 查询该员工有权限的店铺列表,并缓存到当前 session。")
@ApiResponses({
@@ -62,6 +62,24 @@ public class ZiniaoMemoryStoreService {
return get(cacheType, cacheKey, type);
}
public <T> List<T> listByType(String cacheType, Class<T> valueType, int limit) {
String normalizedType = normalizeRequired(cacheType, "cacheType 不能为空");
int safeLimit = Math.max(limit, 1);
List<ZiniaoMemoryStoreEntity> entities = ziniaoMemoryStoreMapper.selectList(new LambdaQueryWrapper<ZiniaoMemoryStoreEntity>()
.eq(ZiniaoMemoryStoreEntity::getCacheType, normalizedType)
.orderByAsc(ZiniaoMemoryStoreEntity::getCacheKey)
.last("LIMIT " + safeLimit));
if (entities.isEmpty()) {
return List.of();
}
LocalDateTime now = LocalDateTime.now();
return entities.stream()
.filter(entity -> entity.getExpiresAt() != null && entity.getExpiresAt().isAfter(now))
.map(entity -> deserialize(entity.getPayloadJson(), valueType))
.filter(Objects::nonNull)
.toList();
}
@Transactional
public void put(String cacheType, String cacheKey, Object payload, Duration ttl) {
String normalizedType = normalizeRequired(cacheType, "cacheType 不能为空");
@@ -103,6 +121,24 @@ public class ZiniaoMemoryStoreService {
}
}
@Transactional
public int deleteByType(String cacheType, int limit) {
String normalizedType = normalizeRequired(cacheType, "cacheType 不能为空");
int safeLimit = Math.max(limit, 1);
List<ZiniaoMemoryStoreEntity> entities = ziniaoMemoryStoreMapper.selectList(new LambdaQueryWrapper<ZiniaoMemoryStoreEntity>()
.eq(ZiniaoMemoryStoreEntity::getCacheType, normalizedType)
.orderByAsc(ZiniaoMemoryStoreEntity::getCacheKey)
.last("LIMIT " + safeLimit));
if (entities.isEmpty()) {
return 0;
}
int deleted = 0;
for (ZiniaoMemoryStoreEntity entity : entities) {
deleted += ziniaoMemoryStoreMapper.deleteById(entity.getId());
}
return deleted;
}
@Transactional
public int deleteExpired(int limit) {
int safeLimit = Math.max(limit, 1);
@@ -129,6 +165,14 @@ public class ZiniaoMemoryStoreService {
.last("LIMIT 1"));
}
private <T> T deserialize(String payloadJson, Class<T> valueType) {
try {
return objectMapper.readValue(payloadJson, valueType);
} catch (Exception ex) {
throw new BusinessException("读取紫鸟记忆存储失败");
}
}
private boolean isExpired(ZiniaoMemoryStoreEntity entity) {
return entity.getExpiresAt() == null || !entity.getExpiresAt().isAfter(LocalDateTime.now());
}
@@ -0,0 +1,24 @@
package com.nanri.aiimage.modules.ziniao.model.cache;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class ZiniaoShopIndexEntryDto {
private String normalizedShopName;
private String shopId;
private String shopName;
private String platform;
private Long matchedUserId;
private Long companyId;
private String apiKeyHash;
private String status;
private String message;
private Integer candidateCount;
private List<String> sampleShopIds = new ArrayList<>();
private List<Long> sampleUserIds = new ArrayList<>();
private Long lastSeenAt;
private Long lastRefreshedAt;
}
@@ -0,0 +1,18 @@
package com.nanri.aiimage.modules.ziniao.model.cache;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class ZiniaoShopIndexRefreshCursorDto {
private String scopeKey;
private String status;
private String message;
private Long lastStartedAt;
private Long lastFinishedAt;
private Long lastSuccessAt;
private Integer invalidUserCount;
private List<Long> sampleInvalidUserIds = new ArrayList<>();
}
@@ -27,4 +27,10 @@ public class ZiniaoShopMatchResultVo {
@Schema(description = "打开店铺链接")
private String openStoreUrl;
@Schema(description = "匹配状态: MATCHED / PENDING / CONFLICT")
private String matchStatus;
@Schema(description = "匹配说明")
private String matchMessage;
}
@@ -112,6 +112,64 @@ public class ZiniaoAuthService {
return new StoreMatchResult(false, null, null, null, null, null);
}
public String buildOpenStoreUrlByScope(String apiKey, Long companyId, Long userId, String shopId) {
ensureEnabled();
if (apiKey == null || apiKey.isBlank()) {
throw new BusinessException("紫鸟 apiKey 未配置");
}
if (companyId == null || companyId <= 0) {
throw new BusinessException("紫鸟 companyId 不合法");
}
if (userId == null || userId <= 0) {
throw new BusinessException("紫鸟 userId 不合法");
}
if (shopId == null || shopId.isBlank()) {
throw new BusinessException("店铺不存在");
}
String loginToken = ziniaoClient.getUserLoginToken(apiKey, companyId, userId);
return buildOpenStoreUrl(shopId, userId, loginToken);
}
public Long resolveCompanyIdForIndex(String apiKey) {
return resolveCompanyId(apiKey);
}
public List<ZiniaoStaffItemVo> getOrLoadStaffForIndex(String apiKey, Long companyId) {
return getOrLoadStaff(apiKey, companyId);
}
public List<ZiniaoShopCacheDto> getOrLoadUserStoresForIndex(String apiKey, Long companyId, Long userId) {
return getOrLoadUserStores(apiKey, companyId, userId);
}
public boolean isInvalidUserStoresError(BusinessException ex) {
String message = ex == null ? null : ex.getMessage();
if (message == null || message.isBlank()) {
return false;
}
return message.contains("code=40004")
|| message.contains("userId存在无效的参数值");
}
public void evictInvalidUserForIndex(String apiKey, Long companyId, Long userId) {
if (apiKey == null || apiKey.isBlank() || companyId == null || companyId <= 0 || userId == null || userId <= 0) {
return;
}
String staffCacheKey = buildApiKeyHash(apiKey) + ":" + companyId;
ziniaoMemoryStoreService.getList(CACHE_TYPE_STAFF_LIST, staffCacheKey, ZiniaoStaffItemVo.class)
.ifPresent(staff -> {
List<ZiniaoStaffItemVo> filtered = staff.stream()
.filter(item -> item != null && !userId.equals(item.getUserId()))
.toList();
if (filtered.isEmpty()) {
ziniaoMemoryStoreService.delete(CACHE_TYPE_STAFF_LIST, staffCacheKey);
} else {
ziniaoMemoryStoreService.put(CACHE_TYPE_STAFF_LIST, staffCacheKey, filtered, STAFF_LIST_CACHE_TTL);
}
});
ziniaoMemoryStoreService.delete(CACHE_TYPE_USER_STORES, buildApiKeyHash(apiKey) + ":" + companyId + ":" + userId);
}
private boolean isSkippableUserStoresError(BusinessException ex) {
String message = ex == null ? null : ex.getMessage();
if (message == null || message.isBlank()) {
@@ -0,0 +1,23 @@
package com.nanri.aiimage.modules.ziniao.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@Slf4j
public class ZiniaoShopIndexRefreshService {
private final ZiniaoShopIndexService ziniaoShopIndexService;
@Scheduled(cron = "${aiimage.ziniao.shop-index-refresh-cron:0 */10 * * * *}")
public void refreshShopIndex() {
try {
ziniaoShopIndexService.refreshShopIndex();
} catch (Exception ex) {
log.warn("[ziniao-index] refresh failed: {}", ex.getMessage());
}
}
}
@@ -0,0 +1,414 @@
package com.nanri.aiimage.modules.ziniao.service;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.config.ZiniaoProperties;
import com.nanri.aiimage.modules.ziniao.memory.service.ZiniaoMemoryStoreService;
import com.nanri.aiimage.modules.ziniao.model.cache.ZiniaoShopIndexEntryDto;
import com.nanri.aiimage.modules.ziniao.model.cache.ZiniaoShopIndexRefreshCursorDto;
import com.nanri.aiimage.modules.ziniao.model.cache.ZiniaoShopCacheDto;
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoShopMatchResultVo;
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoStaffItemVo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
@Service
@RequiredArgsConstructor
@Slf4j
public class ZiniaoShopIndexService {
public static final String STATUS_ACTIVE = "ACTIVE";
public static final String STATUS_STALE = "STALE";
public static final String STATUS_CONFLICT = "CONFLICT";
public static final String STATUS_PENDING = "PENDING";
public static final String MATCH_STATUS_MATCHED = "MATCHED";
public static final String MATCH_STATUS_PENDING = "PENDING";
public static final String MATCH_STATUS_CONFLICT = "CONFLICT";
public static final String MATCH_STATUS_STALE = "INDEX_STALE";
private static final String CACHE_TYPE_SHOP_INDEX_ENTRY = "SHOP_INDEX_ENTRY";
private static final String CACHE_TYPE_SHOP_INDEX_SCOPE_SNAPSHOT = "SHOP_INDEX_SCOPE_SNAPSHOT";
private static final String CACHE_TYPE_SHOP_INDEX_REFRESH_CURSOR = "SHOP_INDEX_REFRESH_CURSOR";
private static final Duration DEFAULT_ENTRY_TTL = Duration.ofHours(12);
private static final Duration DEFAULT_CURSOR_TTL = Duration.ofHours(24);
private static final int DEFAULT_REFRESH_BATCH_SIZE = 100;
private static final int DEFAULT_LIST_BY_TYPE_LIMIT = 5000;
private final ZiniaoMemoryStoreService ziniaoMemoryStoreService;
private final ZiniaoApiKeyProvider ziniaoApiKeyProvider;
private final ZiniaoAuthService ziniaoAuthService;
private final ZiniaoProperties ziniaoProperties;
public ZiniaoShopIndexRefreshCursorDto getRefreshCursor() {
ZiniaoShopIndexRefreshCursorDto cursor = ziniaoMemoryStoreService
.get(CACHE_TYPE_SHOP_INDEX_REFRESH_CURSOR, "global", ZiniaoShopIndexRefreshCursorDto.class)
.orElse(null);
if (cursor == null) {
cursor = new ZiniaoShopIndexRefreshCursorDto();
cursor.setScopeKey("global");
cursor.setStatus("IDLE");
cursor.setMessage("索引刷新任务尚未执行");
}
return cursor;
}
public ZiniaoShopMatchResultVo findIndexedStoreByName(String targetShopName) {
String normalizedShopName = normalizeShopName(targetShopName);
if (normalizedShopName.isBlank()) {
return pendingResult("店铺名为空,无法匹配索引");
}
ZiniaoShopIndexEntryDto entry = ziniaoMemoryStoreService
.get(CACHE_TYPE_SHOP_INDEX_ENTRY, normalizedShopName, ZiniaoShopIndexEntryDto.class)
.orElse(null);
if (entry == null) {
return pendingResult("店铺索引未命中,请等待后台刷新");
}
if (STATUS_CONFLICT.equals(entry.getStatus())) {
return conflictResult(entry.getMessage() == null || entry.getMessage().isBlank()
? "存在多个同名店铺,请人工确认"
: entry.getMessage());
}
if (!STATUS_ACTIVE.equals(entry.getStatus())) {
return staleOrPendingResult(entry.getMessage() == null || entry.getMessage().isBlank()
? "店铺索引暂不可用,请等待后台刷新"
: entry.getMessage(), entry);
}
if (entry.getShopId() == null || entry.getShopId().isBlank() || entry.getMatchedUserId() == null || entry.getMatchedUserId() <= 0) {
return pendingResult("店铺索引信息不完整,请等待后台刷新");
}
String apiKey = findApiKeyByHash(entry.getApiKeyHash());
if (apiKey == null) {
return pendingResult("店铺索引所依赖的紫鸟 key 已失效,请等待后台刷新");
}
Long companyId = entry.getCompanyId();
if (companyId == null || companyId <= 0) {
return pendingResult("店铺索引缺少 companyId,请等待后台刷新");
}
String openStoreUrl = ziniaoAuthService.buildOpenStoreUrlByScope(apiKey, companyId, entry.getMatchedUserId(), entry.getShopId());
ZiniaoShopMatchResultVo vo = new ZiniaoShopMatchResultVo();
vo.setMatched(true);
vo.setShopId(entry.getShopId());
vo.setShopName(entry.getShopName());
vo.setPlatform(entry.getPlatform());
vo.setMatchedUserId(entry.getMatchedUserId());
vo.setOpenStoreUrl(openStoreUrl);
vo.setMatchStatus(isFresh(entry) ? MATCH_STATUS_MATCHED : MATCH_STATUS_STALE);
vo.setMatchMessage(isFresh(entry) ? null : "店铺索引已过保鲜期,请等待后台刷新");
return vo;
}
public void refreshShopIndex() {
long now = Instant.now().toEpochMilli();
ZiniaoShopIndexRefreshCursorDto cursor = new ZiniaoShopIndexRefreshCursorDto();
cursor.setScopeKey("global");
cursor.setStatus("RUNNING");
cursor.setLastStartedAt(now);
ziniaoMemoryStoreService.put(CACHE_TYPE_SHOP_INDEX_REFRESH_CURSOR, "global", cursor, DEFAULT_CURSOR_TTL);
Map<String, List<ZiniaoShopIndexEntryDto>> grouped = new LinkedHashMap<>();
Set<Long> allInvalidUserIds = new LinkedHashSet<>();
int refreshBatchSize = resolveRefreshBatchSize();
try {
List<String> apiKeys = ziniaoApiKeyProvider.listApiKeys();
if (refreshBatchSize > 0 && apiKeys.size() > refreshBatchSize) {
apiKeys = apiKeys.subList(0, refreshBatchSize);
}
for (String apiKey : apiKeys) {
Long companyId;
try {
companyId = ziniaoAuthService.resolveCompanyIdForIndex(apiKey);
} catch (BusinessException ex) {
log.warn("[ziniao-index] skip apiKey while resolving companyId, msg={}", ex.getMessage());
continue;
}
Set<Long> invalidUserIds = new LinkedHashSet<>();
List<ZiniaoStaffItemVo> staff = ziniaoAuthService.getOrLoadStaffForIndex(apiKey, companyId);
for (Long userId : buildUserIds(staff)) {
if (invalidUserIds.contains(userId)) {
continue;
}
List<ZiniaoShopCacheDto> stores;
try {
stores = ziniaoAuthService.getOrLoadUserStoresForIndex(apiKey, companyId, userId);
} catch (BusinessException ex) {
if (ziniaoAuthService.isInvalidUserStoresError(ex)) {
invalidUserIds.add(userId);
allInvalidUserIds.add(userId);
ziniaoAuthService.evictInvalidUserForIndex(apiKey, companyId, userId);
}
log.warn("[ziniao-index] skip user stores, companyId={}, userId={}, msg={}", companyId, userId, ex.getMessage());
continue;
}
ziniaoMemoryStoreService.put(
CACHE_TYPE_SHOP_INDEX_SCOPE_SNAPSHOT,
buildScopeSnapshotKey(apiKey, companyId, userId),
stores,
resolveEntryTtl()
);
for (ZiniaoShopCacheDto store : stores) {
String normalizedShopName = normalizeShopName(store == null ? null : store.getShopName());
if (normalizedShopName.isBlank()) {
continue;
}
ZiniaoShopIndexEntryDto entry = new ZiniaoShopIndexEntryDto();
entry.setNormalizedShopName(normalizedShopName);
entry.setShopId(store.getShopId());
entry.setShopName(store.getShopName());
entry.setPlatform(store.getPlatform());
entry.setMatchedUserId(userId);
entry.setCompanyId(companyId);
entry.setApiKeyHash(buildApiKeyHash(apiKey));
entry.setStatus(STATUS_ACTIVE);
entry.setLastSeenAt(now);
entry.setLastRefreshedAt(now);
List<ZiniaoShopIndexEntryDto> candidates = grouped.computeIfAbsent(normalizedShopName, ignored -> new ArrayList<>());
if (candidates.stream().noneMatch(existing -> sameCandidate(existing, entry))) {
candidates.add(entry);
}
}
}
if (!invalidUserIds.isEmpty()) {
for (Long invalidUserId : invalidUserIds) {
ziniaoMemoryStoreService.delete(
CACHE_TYPE_SHOP_INDEX_SCOPE_SNAPSHOT,
buildScopeSnapshotKey(apiKey, companyId, invalidUserId)
);
}
}
}
for (Map.Entry<String, List<ZiniaoShopIndexEntryDto>> groupedEntry : grouped.entrySet()) {
String normalizedShopName = groupedEntry.getKey();
List<ZiniaoShopIndexEntryDto> candidates = groupedEntry.getValue();
ZiniaoShopIndexEntryDto entryToStore = candidates.get(0);
if (candidates.size() > 1) {
entryToStore = buildConflictEntry(normalizedShopName, candidates, now);
}
ziniaoMemoryStoreService.put(CACHE_TYPE_SHOP_INDEX_ENTRY, normalizedShopName, entryToStore, resolveEntryTtl());
}
markMissingEntriesAsStale(grouped.keySet(), now);
cursor.setStatus("SUCCESS");
cursor.setMessage(allInvalidUserIds.isEmpty()
? null
: "本轮刷新已跳过无效 userId 数: " + allInvalidUserIds.size());
cursor.setInvalidUserCount(allInvalidUserIds.size());
cursor.setSampleInvalidUserIds(allInvalidUserIds.stream().limit(20).toList());
cursor.setLastFinishedAt(now);
cursor.setLastSuccessAt(now);
ziniaoMemoryStoreService.put(CACHE_TYPE_SHOP_INDEX_REFRESH_CURSOR, "global", cursor, DEFAULT_CURSOR_TTL);
} catch (Exception ex) {
cursor.setStatus("FAILED");
cursor.setMessage(ex.getMessage());
cursor.setLastFinishedAt(now);
ziniaoMemoryStoreService.put(CACHE_TYPE_SHOP_INDEX_REFRESH_CURSOR, "global", cursor, DEFAULT_CURSOR_TTL);
if (ex instanceof BusinessException businessException) {
throw businessException;
}
throw new BusinessException("刷新紫鸟店铺索引失败");
}
}
public void invalidateIndex() {
ziniaoMemoryStoreService.delete(CACHE_TYPE_SHOP_INDEX_REFRESH_CURSOR, "global");
}
public String normalizeShopName(String value) {
if (value == null) {
return "";
}
return value.replace("\u3000", " ").trim();
}
private ZiniaoShopMatchResultVo pendingResult(String message) {
ZiniaoShopMatchResultVo vo = new ZiniaoShopMatchResultVo();
vo.setMatched(false);
vo.setMatchStatus(MATCH_STATUS_PENDING);
vo.setMatchMessage(message);
return vo;
}
private ZiniaoShopMatchResultVo conflictResult(String message) {
ZiniaoShopMatchResultVo vo = new ZiniaoShopMatchResultVo();
vo.setMatched(false);
vo.setMatchStatus(MATCH_STATUS_CONFLICT);
vo.setMatchMessage(message);
return vo;
}
private ZiniaoShopIndexEntryDto buildConflictEntry(String normalizedShopName, List<ZiniaoShopIndexEntryDto> candidates, long now) {
ZiniaoShopIndexEntryDto representative = candidates == null || candidates.isEmpty() ? null : candidates.get(0);
ZiniaoShopIndexEntryDto entry = new ZiniaoShopIndexEntryDto();
entry.setNormalizedShopName(normalizedShopName);
entry.setStatus(STATUS_CONFLICT);
entry.setShopName(representative == null ? normalizedShopName : representative.getShopName());
entry.setCompanyId(representative == null ? null : representative.getCompanyId());
entry.setApiKeyHash(representative == null ? null : representative.getApiKeyHash());
entry.setCandidateCount(candidates == null ? 0 : candidates.size());
entry.setMessage("存在多个同名店铺,请人工确认(候选数: " + entry.getCandidateCount() + "");
entry.setLastSeenAt(now);
entry.setLastRefreshedAt(now);
if (candidates != null) {
for (ZiniaoShopIndexEntryDto candidate : candidates) {
if (candidate == null) {
continue;
}
if (candidate.getShopId() != null && !candidate.getShopId().isBlank() && !entry.getSampleShopIds().contains(candidate.getShopId())) {
entry.getSampleShopIds().add(candidate.getShopId());
}
if (candidate.getMatchedUserId() != null && candidate.getMatchedUserId() > 0 && !entry.getSampleUserIds().contains(candidate.getMatchedUserId())) {
entry.getSampleUserIds().add(candidate.getMatchedUserId());
}
}
}
return entry;
}
private ZiniaoShopMatchResultVo staleOrPendingResult(String defaultMessage, ZiniaoShopIndexEntryDto entry) {
if (entry != null && STATUS_STALE.equals(entry.getStatus())) {
ZiniaoShopMatchResultVo vo = new ZiniaoShopMatchResultVo();
vo.setMatched(false);
vo.setMatchStatus(MATCH_STATUS_STALE);
vo.setMatchMessage(defaultMessage);
return vo;
}
return pendingResult(defaultMessage);
}
private void markMissingEntriesAsStale(Set<String> activeNames, long now) {
List<ZiniaoShopIndexEntryDto> existingEntries = ziniaoMemoryStoreService.listByType(
CACHE_TYPE_SHOP_INDEX_ENTRY,
ZiniaoShopIndexEntryDto.class,
DEFAULT_LIST_BY_TYPE_LIMIT
);
if (existingEntries.isEmpty()) {
return;
}
for (ZiniaoShopIndexEntryDto existingEntry : existingEntries) {
if (existingEntry == null || existingEntry.getNormalizedShopName() == null || existingEntry.getNormalizedShopName().isBlank()) {
continue;
}
if (activeNames.contains(existingEntry.getNormalizedShopName())) {
continue;
}
if (STATUS_CONFLICT.equals(existingEntry.getStatus())
&& (existingEntry.getCandidateCount() == null || existingEntry.getCandidateCount() <= 0)
&& (existingEntry.getShopName() == null || existingEntry.getShopName().isBlank())
&& (existingEntry.getApiKeyHash() == null || existingEntry.getApiKeyHash().isBlank())
&& (existingEntry.getCompanyId() == null || existingEntry.getCompanyId() <= 0)) {
ziniaoMemoryStoreService.delete(CACHE_TYPE_SHOP_INDEX_ENTRY, existingEntry.getNormalizedShopName());
continue;
}
existingEntry.setStatus(STATUS_STALE);
existingEntry.setMessage("店铺索引已过期,请等待后台刷新");
existingEntry.setLastRefreshedAt(now);
ziniaoMemoryStoreService.put(
CACHE_TYPE_SHOP_INDEX_ENTRY,
existingEntry.getNormalizedShopName(),
existingEntry,
resolveEntryTtl()
);
}
}
private boolean isFresh(ZiniaoShopIndexEntryDto entry) {
if (entry == null || entry.getLastRefreshedAt() == null || entry.getLastRefreshedAt() <= 0) {
return false;
}
Integer freshMinutes = ziniaoProperties.getShopIndexFreshMinutes();
if (freshMinutes == null || freshMinutes <= 0) {
return true;
}
long freshWindowMillis = Duration.ofMinutes(freshMinutes).toMillis();
return Instant.now().toEpochMilli() - entry.getLastRefreshedAt() <= freshWindowMillis;
}
private Duration resolveEntryTtl() {
Integer ttlHours = ziniaoProperties.getShopIndexEntryTtlHours();
if (ttlHours == null || ttlHours <= 0) {
return DEFAULT_ENTRY_TTL;
}
return Duration.ofHours(ttlHours);
}
private int resolveRefreshBatchSize() {
Integer batchSize = ziniaoProperties.getShopIndexRefreshBatchSize();
if (batchSize == null || batchSize <= 0) {
return DEFAULT_REFRESH_BATCH_SIZE;
}
return batchSize;
}
private List<Long> buildUserIds(List<ZiniaoStaffItemVo> staff) {
List<Long> userIds = new ArrayList<>();
for (ZiniaoStaffItemVo item : staff) {
if (item != null && item.getUserId() != null && item.getUserId() > 0 && !userIds.contains(item.getUserId())) {
userIds.add(item.getUserId());
}
}
return userIds;
}
private boolean sameCandidate(ZiniaoShopIndexEntryDto left, ZiniaoShopIndexEntryDto right) {
if (left == null || right == null) {
return false;
}
if (left.getShopId() != null && right.getShopId() != null) {
return Objects.equals(left.getShopId(), right.getShopId())
&& Objects.equals(left.getMatchedUserId(), right.getMatchedUserId())
&& Objects.equals(left.getCompanyId(), right.getCompanyId())
&& Objects.equals(left.getApiKeyHash(), right.getApiKeyHash());
}
return Objects.equals(left.getShopName(), right.getShopName())
&& Objects.equals(left.getMatchedUserId(), right.getMatchedUserId())
&& Objects.equals(left.getCompanyId(), right.getCompanyId())
&& Objects.equals(left.getApiKeyHash(), right.getApiKeyHash());
}
private String buildScopeSnapshotKey(String apiKey, Long companyId, Long userId) {
return buildApiKeyHash(apiKey) + ":" + companyId + ":" + userId;
}
private String findApiKeyByHash(String apiKeyHash) {
if (apiKeyHash == null || apiKeyHash.isBlank()) {
return null;
}
for (String apiKey : ziniaoApiKeyProvider.listApiKeys()) {
if (apiKeyHash.equals(buildApiKeyHash(apiKey))) {
return apiKey;
}
}
return null;
}
private String buildApiKeyHash(String apiKey) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(apiKey.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : hash) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (Exception ex) {
throw new BusinessException("生成紫鸟 apiKey 缓存键失败");
}
}
}
@@ -10,6 +10,7 @@ import org.springframework.stereotype.Service;
public class ZiniaoShopSwitchService {
private final ZiniaoAuthService ziniaoAuthService;
private final ZiniaoShopIndexService ziniaoShopIndexService;
public ZiniaoShopMatchResultVo matchStoreByNameAcrossStaff(String targetShopName, Long preferUserId) {
String normalizedShopName = normalizeShopName(targetShopName);
@@ -18,18 +19,27 @@ public class ZiniaoShopSwitchService {
}
ZiniaoAuthService.StoreMatchResult result = ziniaoAuthService.matchStoreByNameAcrossStaff(normalizedShopName, preferUserId);
return new ZiniaoShopMatchResultVo(
result.matched(),
result.shopId(),
result.shopName(),
result.platform(),
result.matchedUserId(),
result.openStoreUrl()
);
ZiniaoShopMatchResultVo vo = new ZiniaoShopMatchResultVo();
vo.setMatched(result.matched());
vo.setShopId(result.shopId());
vo.setShopName(result.shopName());
vo.setPlatform(result.platform());
vo.setMatchedUserId(result.matchedUserId());
vo.setOpenStoreUrl(result.openStoreUrl());
vo.setMatchStatus(result.matched() ? ZiniaoShopIndexService.MATCH_STATUS_MATCHED : ZiniaoShopIndexService.MATCH_STATUS_PENDING);
return vo;
}
public ZiniaoShopMatchResultVo findIndexedStoreByName(String targetShopName) {
return ziniaoShopIndexService.findIndexedStoreByName(targetShopName);
}
public ZiniaoShopMatchResultVo emptyMatchResult() {
return new ZiniaoShopMatchResultVo(false, null, null, null, null, null);
ZiniaoShopMatchResultVo vo = new ZiniaoShopMatchResultVo();
vo.setMatched(false);
vo.setMatchStatus(ZiniaoShopIndexService.MATCH_STATUS_PENDING);
vo.setMatchMessage("店铺名为空,无法匹配索引");
return vo;
}
public String normalizeShopName(String value) {