紫鸟查询店铺更新
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -40,4 +40,25 @@ public class ZiniaoProperties {
|
||||
* null/空/<=0 表示不限制(不推荐)。
|
||||
*/
|
||||
private Integer keyScanMaxSeconds;
|
||||
|
||||
/**
|
||||
* 店铺索引后台刷新 cron。
|
||||
*/
|
||||
private String shopIndexRefreshCron;
|
||||
|
||||
/**
|
||||
* 店铺索引条目保鲜分钟数。
|
||||
*/
|
||||
private Integer shopIndexFreshMinutes;
|
||||
|
||||
/**
|
||||
* 店铺索引条目 TTL 小时数。
|
||||
*/
|
||||
private Integer shopIndexEntryTtlHours;
|
||||
|
||||
/**
|
||||
* 每次索引刷新最多扫描多少个 apiKey;
|
||||
* null/空/<=0 表示不限制(不推荐)。
|
||||
*/
|
||||
private Integer shopIndexRefreshBatchSize;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,12 @@ public class DeleteBrandResultItemVo {
|
||||
@Schema(description = "紫鸟打开链接")
|
||||
private String openStoreUrl;
|
||||
|
||||
@Schema(description = "匹配状态: MATCHED / PENDING / CONFLICT")
|
||||
private String matchStatus;
|
||||
|
||||
@Schema(description = "匹配说明")
|
||||
private String matchMessage;
|
||||
|
||||
@Schema(description = "总行数")
|
||||
private Integer totalRows;
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@ package com.nanri.aiimage.modules.deletebrand.model.vo;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Schema(description = "删除品牌任务详情响应")
|
||||
public class DeleteBrandTaskDetailVo {
|
||||
@@ -11,4 +14,7 @@ public class DeleteBrandTaskDetailVo {
|
||||
|
||||
@Schema(description = "实时行级进度信息,来自 Redis")
|
||||
private DeleteBrandLineProgressVo line_progress;
|
||||
|
||||
@Schema(description = "任务结果项快照")
|
||||
private List<DeleteBrandResultItemVo> items = new ArrayList<>();
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.nanri.aiimage.modules.task.mapper.FileTaskMapper;
|
||||
import com.nanri.aiimage.modules.task.model.entity.FileResultEntity;
|
||||
import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity;
|
||||
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoShopMatchResultVo;
|
||||
import com.nanri.aiimage.modules.ziniao.service.ZiniaoShopIndexService;
|
||||
import com.nanri.aiimage.modules.ziniao.service.ZiniaoShopSwitchService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -125,14 +126,14 @@ public class DeleteBrandRunService {
|
||||
ZiniaoShopMatchResultVo matchResult = normalizedShopName.isBlank()
|
||||
? ziniaoShopSwitchService.emptyMatchResult()
|
||||
: storeMatchByShopName.computeIfAbsent(normalizedShopName,
|
||||
ignored -> ziniaoShopSwitchService.matchStoreByNameAcrossStaff(item.getShopName(), null));
|
||||
ignored -> ziniaoShopSwitchService.findIndexedStoreByName(item.getShopName()));
|
||||
item.setMatchStatus(matchResult.getMatchStatus());
|
||||
item.setMatchMessage(matchResult.getMatchMessage());
|
||||
item.setMatched(matchResult.isMatched());
|
||||
if (matchResult.isMatched()) {
|
||||
item.setShopId(matchResult.getShopId());
|
||||
item.setPlatform(matchResult.getPlatform());
|
||||
item.setOpenStoreUrl(matchResult.getOpenStoreUrl());
|
||||
} else {
|
||||
throw new BusinessException("未匹配到紫鸟店铺,跳过处理");
|
||||
}
|
||||
} catch (BusinessException ex) {
|
||||
String message = ex.getMessage();
|
||||
@@ -155,10 +156,12 @@ public class DeleteBrandRunService {
|
||||
cacheDto.setPreviewRows(parsed.previewRows());
|
||||
parsedPayloadByFileIdentity.put(fileIdentity, cacheDto);
|
||||
processableFileCount++;
|
||||
parsedSuccessCount++;
|
||||
} else {
|
||||
parsedFailedCount++;
|
||||
}
|
||||
|
||||
item.setSuccess(true);
|
||||
parsedSuccessCount++;
|
||||
|
||||
FileResultEntity resultEntity = new FileResultEntity();
|
||||
resultEntity.setTaskId(task.getId());
|
||||
@@ -195,7 +198,7 @@ public class DeleteBrandRunService {
|
||||
items.add(item);
|
||||
}
|
||||
|
||||
task.setSuccessFileCount(0);
|
||||
task.setSuccessFileCount(parsedSuccessCount);
|
||||
task.setFailedFileCount(parsedFailedCount);
|
||||
task.setResultJson(JSONUtil.toJsonStr(items));
|
||||
task.setUpdatedAt(LocalDateTime.now());
|
||||
@@ -278,9 +281,13 @@ public class DeleteBrandRunService {
|
||||
}
|
||||
if (entity.getSourceFilename().equals(candidate.getSourceFilename())) {
|
||||
item.setMatched(candidate.isMatched());
|
||||
item.setMatchStatus(candidate.getMatchStatus());
|
||||
item.setMatchMessage(candidate.getMatchMessage());
|
||||
// 如果物理结果已经是成功了,说明已经处理过了,强制置为匹配成功
|
||||
if (item.isSuccess()) {
|
||||
item.setMatched(true);
|
||||
item.setMatchStatus("MATCHED");
|
||||
item.setMatchMessage(null);
|
||||
}
|
||||
|
||||
item.setShopId(candidate.getShopId());
|
||||
@@ -535,6 +542,7 @@ public class DeleteBrandRunService {
|
||||
}
|
||||
|
||||
private DeleteBrandTaskDetailVo buildTaskDetail(FileTaskEntity task, java.util.Map<Object, Object> progress) {
|
||||
task = refreshIndexedMatchesIfNeeded(task);
|
||||
DeleteBrandTaskItemVo taskVo = new DeleteBrandTaskItemVo();
|
||||
taskVo.setId(task.getId());
|
||||
taskVo.setTaskNo(task.getTaskNo());
|
||||
@@ -560,9 +568,129 @@ public class DeleteBrandRunService {
|
||||
DeleteBrandTaskDetailVo detail = new DeleteBrandTaskDetailVo();
|
||||
detail.setTask(taskVo);
|
||||
detail.setLine_progress(progressVo);
|
||||
detail.setItems(resolveTaskResultItems(task));
|
||||
return detail;
|
||||
}
|
||||
|
||||
private List<DeleteBrandResultItemVo> resolveTaskResultItems(FileTaskEntity task) {
|
||||
if (task == null || task.getResultJson() == null || task.getResultJson().isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
try {
|
||||
List<DeleteBrandResultItemVo> items = objectMapper.readValue(task.getResultJson(), new TypeReference<List<DeleteBrandResultItemVo>>() {
|
||||
});
|
||||
if (items == null || items.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return items.stream().peek(item -> {
|
||||
if (item == null) {
|
||||
return;
|
||||
}
|
||||
item.setTaskId(task.getId());
|
||||
item.setTaskStatus(task.getStatus());
|
||||
if ("SUCCESS".equals(task.getStatus()) && item.isSuccess()) {
|
||||
item.setMatched(true);
|
||||
item.setMatchStatus(ZiniaoShopIndexService.MATCH_STATUS_MATCHED);
|
||||
item.setMatchMessage(null);
|
||||
}
|
||||
}).toList();
|
||||
} catch (Exception ignored) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
private FileTaskEntity refreshIndexedMatchesIfNeeded(FileTaskEntity task) {
|
||||
if (task == null || task.getResultJson() == null || task.getResultJson().isBlank()) {
|
||||
return task;
|
||||
}
|
||||
if (!"RUNNING".equals(task.getStatus()) && !"FAILED".equals(task.getStatus())) {
|
||||
return task;
|
||||
}
|
||||
try {
|
||||
List<DeleteBrandResultItemVo> items = objectMapper.readValue(task.getResultJson(), new TypeReference<List<DeleteBrandResultItemVo>>() {
|
||||
});
|
||||
if (items == null || items.isEmpty()) {
|
||||
return task;
|
||||
}
|
||||
boolean changed = false;
|
||||
int matchedCount = 0;
|
||||
int unmatchedCount = 0;
|
||||
for (DeleteBrandResultItemVo item : items) {
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
String currentMatchStatus = item.getMatchStatus();
|
||||
if (currentMatchStatus == null || ZiniaoShopIndexService.MATCH_STATUS_MATCHED.equals(currentMatchStatus)) {
|
||||
if (item.isMatched()) {
|
||||
matchedCount++;
|
||||
} else {
|
||||
unmatchedCount++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!ZiniaoShopIndexService.MATCH_STATUS_PENDING.equals(currentMatchStatus)
|
||||
&& !ZiniaoShopIndexService.MATCH_STATUS_STALE.equals(currentMatchStatus)) {
|
||||
unmatchedCount++;
|
||||
continue;
|
||||
}
|
||||
String normalizedShopName = ziniaoShopSwitchService.normalizeShopName(item.getShopName());
|
||||
if (normalizedShopName.isBlank()) {
|
||||
unmatchedCount++;
|
||||
continue;
|
||||
}
|
||||
ZiniaoShopMatchResultVo refreshed = ziniaoShopSwitchService.findIndexedStoreByName(item.getShopName());
|
||||
if (refreshed != null) {
|
||||
String oldStatus = item.getMatchStatus();
|
||||
boolean oldMatched = item.isMatched();
|
||||
String oldShopId = item.getShopId();
|
||||
String oldPlatform = item.getPlatform();
|
||||
String oldOpenStoreUrl = item.getOpenStoreUrl();
|
||||
String oldMessage = item.getMatchMessage();
|
||||
|
||||
item.setMatched(refreshed.isMatched());
|
||||
item.setMatchStatus(refreshed.getMatchStatus());
|
||||
item.setMatchMessage(refreshed.getMatchMessage());
|
||||
item.setShopId(refreshed.getShopId());
|
||||
item.setPlatform(refreshed.getPlatform());
|
||||
item.setOpenStoreUrl(refreshed.getOpenStoreUrl());
|
||||
|
||||
if (!java.util.Objects.equals(oldStatus, item.getMatchStatus())
|
||||
|| oldMatched != item.isMatched()
|
||||
|| !java.util.Objects.equals(oldShopId, item.getShopId())
|
||||
|| !java.util.Objects.equals(oldPlatform, item.getPlatform())
|
||||
|| !java.util.Objects.equals(oldOpenStoreUrl, item.getOpenStoreUrl())
|
||||
|| !java.util.Objects.equals(oldMessage, item.getMatchMessage())) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (item.isMatched()) {
|
||||
matchedCount++;
|
||||
} else {
|
||||
unmatchedCount++;
|
||||
}
|
||||
}
|
||||
if (!changed) {
|
||||
return task;
|
||||
}
|
||||
task.setResultJson(JSONUtil.toJsonStr(items));
|
||||
task.setSourceFileCount(matchedCount);
|
||||
task.setFailedFileCount(unmatchedCount);
|
||||
task.setUpdatedAt(LocalDateTime.now());
|
||||
if (matchedCount > 0 && "FAILED".equals(task.getStatus())) {
|
||||
task.setStatus("RUNNING");
|
||||
task.setErrorMessage(null);
|
||||
task.setFinishedAt(null);
|
||||
}
|
||||
fileTaskMapper.updateById(task);
|
||||
if ("RUNNING".equals(task.getStatus())) {
|
||||
deleteBrandTaskCacheService.saveTaskCache(task);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.debug("refreshIndexedMatchesIfNeeded skipped for taskId={}, msg={}", task.getId(), ex.getMessage());
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
private DeleteBrandLineProgressVo buildLineProgress(Long taskId) {
|
||||
return buildLineProgress(taskId, null);
|
||||
}
|
||||
@@ -926,7 +1054,9 @@ public class DeleteBrandRunService {
|
||||
item.setCountries(parsedFile.getCountries());
|
||||
item.setPreviewRows(parsedFile.getPreviewRows());
|
||||
item.setTruncated(false);
|
||||
item.setSuccess(true);
|
||||
item.setMatched(true);
|
||||
item.setMatchStatus("MATCHED");
|
||||
item.setMatchMessage(null);
|
||||
finalItems.add(item);
|
||||
successCount++;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ import com.nanri.aiimage.modules.shopkey.model.dto.ShopKeyUpdateRequest;
|
||||
import com.nanri.aiimage.modules.shopkey.model.entity.ShopKeyEntity;
|
||||
import com.nanri.aiimage.modules.shopkey.model.vo.ShopKeyItemVo;
|
||||
import com.nanri.aiimage.modules.shopkey.model.vo.ShopKeyPageVo;
|
||||
import com.nanri.aiimage.modules.ziniao.service.ZiniaoShopIndexService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -16,9 +18,11 @@ import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class ShopKeyService {
|
||||
|
||||
private final ShopKeyMapper shopKeyMapper;
|
||||
private final ZiniaoShopIndexService ziniaoShopIndexService;
|
||||
|
||||
public ShopKeyPageVo page(long page, long pageSize) {
|
||||
long safePage = Math.max(page, 1);
|
||||
@@ -46,6 +50,7 @@ public class ShopKeyService {
|
||||
entity.setZiniaoAccountName(ziniaoAccountName);
|
||||
entity.setZiniaoToken(ziniaoToken);
|
||||
shopKeyMapper.insert(entity);
|
||||
triggerShopIndexRefresh();
|
||||
return toItemVo(getById(entity.getId()));
|
||||
}
|
||||
|
||||
@@ -57,6 +62,7 @@ public class ShopKeyService {
|
||||
entity.setZiniaoAccountName(ziniaoAccountName);
|
||||
entity.setZiniaoToken(ziniaoToken);
|
||||
shopKeyMapper.updateById(entity);
|
||||
triggerShopIndexRefresh();
|
||||
return toItemVo(getById(id));
|
||||
}
|
||||
|
||||
@@ -64,6 +70,7 @@ public class ShopKeyService {
|
||||
public void delete(Long id) {
|
||||
ShopKeyEntity entity = getById(id);
|
||||
shopKeyMapper.deleteById(entity.getId());
|
||||
triggerShopIndexRefresh();
|
||||
}
|
||||
|
||||
private ShopKeyEntity getById(Long id) {
|
||||
@@ -91,4 +98,12 @@ public class ShopKeyService {
|
||||
vo.setUpdatedAt(entity.getUpdatedAt());
|
||||
return vo;
|
||||
}
|
||||
|
||||
private void triggerShopIndexRefresh() {
|
||||
try {
|
||||
ziniaoShopIndexService.invalidateIndex();
|
||||
} catch (Exception ex) {
|
||||
log.warn("[ziniao-index] refresh trigger failed after shop key change: {}", ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -108,5 +108,9 @@ aiimage:
|
||||
shops-cache-minutes: ${AIIMAGE_ZINIAO_SHOPS_CACHE_MINUTES:30}
|
||||
connect-timeout-seconds: ${AIIMAGE_ZINIAO_CONNECT_TIMEOUT_SECONDS:5}
|
||||
read-timeout-seconds: ${AIIMAGE_ZINIAO_READ_TIMEOUT_SECONDS:15}
|
||||
key-scan-max-keys: ${AIIMAGE_ZINIAO_KEY_SCAN_MAX_KEYS:50}
|
||||
key-scan-max-seconds: ${AIIMAGE_ZINIAO_KEY_SCAN_MAX_SECONDS:15}
|
||||
key-scan-max-keys: ${AIIMAGE_ZINIAO_KEY_SCAN_MAX_KEYS:100}
|
||||
key-scan-max-seconds: ${AIIMAGE_ZINIAO_KEY_SCAN_MAX_SECONDS:30}
|
||||
shop-index-refresh-cron: ${AIIMAGE_ZINIAO_SHOP_INDEX_REFRESH_CRON:0 */10 * * * *}
|
||||
shop-index-fresh-minutes: ${AIIMAGE_ZINIAO_SHOP_INDEX_FRESH_MINUTES:20}
|
||||
shop-index-entry-ttl-hours: ${AIIMAGE_ZINIAO_SHOP_INDEX_ENTRY_TTL_HOURS:12}
|
||||
shop-index-refresh-batch-size: ${AIIMAGE_ZINIAO_SHOP_INDEX_REFRESH_BATCH_SIZE:100}
|
||||
|
||||
Reference in New Issue
Block a user