紫鸟查询店铺更新
This commit is contained in:
34492
aiimage-backend.log
34492
aiimage-backend.log
File diff suppressed because it is too large
Load Diff
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 表示不限制(不推荐)。
|
* null/空/<=0 表示不限制(不推荐)。
|
||||||
*/
|
*/
|
||||||
private Integer keyScanMaxSeconds;
|
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 = "紫鸟打开链接")
|
@Schema(description = "紫鸟打开链接")
|
||||||
private String openStoreUrl;
|
private String openStoreUrl;
|
||||||
|
|
||||||
|
@Schema(description = "匹配状态: MATCHED / PENDING / CONFLICT")
|
||||||
|
private String matchStatus;
|
||||||
|
|
||||||
|
@Schema(description = "匹配说明")
|
||||||
|
private String matchMessage;
|
||||||
|
|
||||||
@Schema(description = "总行数")
|
@Schema(description = "总行数")
|
||||||
private Integer totalRows;
|
private Integer totalRows;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ package com.nanri.aiimage.modules.deletebrand.model.vo;
|
|||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Schema(description = "删除品牌任务详情响应")
|
@Schema(description = "删除品牌任务详情响应")
|
||||||
public class DeleteBrandTaskDetailVo {
|
public class DeleteBrandTaskDetailVo {
|
||||||
@@ -11,4 +14,7 @@ public class DeleteBrandTaskDetailVo {
|
|||||||
|
|
||||||
@Schema(description = "实时行级进度信息,来自 Redis")
|
@Schema(description = "实时行级进度信息,来自 Redis")
|
||||||
private DeleteBrandLineProgressVo line_progress;
|
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.FileResultEntity;
|
||||||
import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity;
|
import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity;
|
||||||
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoShopMatchResultVo;
|
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 com.nanri.aiimage.modules.ziniao.service.ZiniaoShopSwitchService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -125,14 +126,14 @@ public class DeleteBrandRunService {
|
|||||||
ZiniaoShopMatchResultVo matchResult = normalizedShopName.isBlank()
|
ZiniaoShopMatchResultVo matchResult = normalizedShopName.isBlank()
|
||||||
? ziniaoShopSwitchService.emptyMatchResult()
|
? ziniaoShopSwitchService.emptyMatchResult()
|
||||||
: storeMatchByShopName.computeIfAbsent(normalizedShopName,
|
: 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());
|
item.setMatched(matchResult.isMatched());
|
||||||
if (matchResult.isMatched()) {
|
if (matchResult.isMatched()) {
|
||||||
item.setShopId(matchResult.getShopId());
|
item.setShopId(matchResult.getShopId());
|
||||||
item.setPlatform(matchResult.getPlatform());
|
item.setPlatform(matchResult.getPlatform());
|
||||||
item.setOpenStoreUrl(matchResult.getOpenStoreUrl());
|
item.setOpenStoreUrl(matchResult.getOpenStoreUrl());
|
||||||
} else {
|
|
||||||
throw new BusinessException("未匹配到紫鸟店铺,跳过处理");
|
|
||||||
}
|
}
|
||||||
} catch (BusinessException ex) {
|
} catch (BusinessException ex) {
|
||||||
String message = ex.getMessage();
|
String message = ex.getMessage();
|
||||||
@@ -155,10 +156,12 @@ public class DeleteBrandRunService {
|
|||||||
cacheDto.setPreviewRows(parsed.previewRows());
|
cacheDto.setPreviewRows(parsed.previewRows());
|
||||||
parsedPayloadByFileIdentity.put(fileIdentity, cacheDto);
|
parsedPayloadByFileIdentity.put(fileIdentity, cacheDto);
|
||||||
processableFileCount++;
|
processableFileCount++;
|
||||||
|
parsedSuccessCount++;
|
||||||
|
} else {
|
||||||
|
parsedFailedCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
item.setSuccess(true);
|
item.setSuccess(true);
|
||||||
parsedSuccessCount++;
|
|
||||||
|
|
||||||
FileResultEntity resultEntity = new FileResultEntity();
|
FileResultEntity resultEntity = new FileResultEntity();
|
||||||
resultEntity.setTaskId(task.getId());
|
resultEntity.setTaskId(task.getId());
|
||||||
@@ -195,7 +198,7 @@ public class DeleteBrandRunService {
|
|||||||
items.add(item);
|
items.add(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
task.setSuccessFileCount(0);
|
task.setSuccessFileCount(parsedSuccessCount);
|
||||||
task.setFailedFileCount(parsedFailedCount);
|
task.setFailedFileCount(parsedFailedCount);
|
||||||
task.setResultJson(JSONUtil.toJsonStr(items));
|
task.setResultJson(JSONUtil.toJsonStr(items));
|
||||||
task.setUpdatedAt(LocalDateTime.now());
|
task.setUpdatedAt(LocalDateTime.now());
|
||||||
@@ -278,9 +281,13 @@ public class DeleteBrandRunService {
|
|||||||
}
|
}
|
||||||
if (entity.getSourceFilename().equals(candidate.getSourceFilename())) {
|
if (entity.getSourceFilename().equals(candidate.getSourceFilename())) {
|
||||||
item.setMatched(candidate.isMatched());
|
item.setMatched(candidate.isMatched());
|
||||||
|
item.setMatchStatus(candidate.getMatchStatus());
|
||||||
|
item.setMatchMessage(candidate.getMatchMessage());
|
||||||
// 如果物理结果已经是成功了,说明已经处理过了,强制置为匹配成功
|
// 如果物理结果已经是成功了,说明已经处理过了,强制置为匹配成功
|
||||||
if (item.isSuccess()) {
|
if (item.isSuccess()) {
|
||||||
item.setMatched(true);
|
item.setMatched(true);
|
||||||
|
item.setMatchStatus("MATCHED");
|
||||||
|
item.setMatchMessage(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
item.setShopId(candidate.getShopId());
|
item.setShopId(candidate.getShopId());
|
||||||
@@ -535,6 +542,7 @@ public class DeleteBrandRunService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private DeleteBrandTaskDetailVo buildTaskDetail(FileTaskEntity task, java.util.Map<Object, Object> progress) {
|
private DeleteBrandTaskDetailVo buildTaskDetail(FileTaskEntity task, java.util.Map<Object, Object> progress) {
|
||||||
|
task = refreshIndexedMatchesIfNeeded(task);
|
||||||
DeleteBrandTaskItemVo taskVo = new DeleteBrandTaskItemVo();
|
DeleteBrandTaskItemVo taskVo = new DeleteBrandTaskItemVo();
|
||||||
taskVo.setId(task.getId());
|
taskVo.setId(task.getId());
|
||||||
taskVo.setTaskNo(task.getTaskNo());
|
taskVo.setTaskNo(task.getTaskNo());
|
||||||
@@ -560,9 +568,129 @@ public class DeleteBrandRunService {
|
|||||||
DeleteBrandTaskDetailVo detail = new DeleteBrandTaskDetailVo();
|
DeleteBrandTaskDetailVo detail = new DeleteBrandTaskDetailVo();
|
||||||
detail.setTask(taskVo);
|
detail.setTask(taskVo);
|
||||||
detail.setLine_progress(progressVo);
|
detail.setLine_progress(progressVo);
|
||||||
|
detail.setItems(resolveTaskResultItems(task));
|
||||||
return detail;
|
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) {
|
private DeleteBrandLineProgressVo buildLineProgress(Long taskId) {
|
||||||
return buildLineProgress(taskId, null);
|
return buildLineProgress(taskId, null);
|
||||||
}
|
}
|
||||||
@@ -926,7 +1054,9 @@ public class DeleteBrandRunService {
|
|||||||
item.setCountries(parsedFile.getCountries());
|
item.setCountries(parsedFile.getCountries());
|
||||||
item.setPreviewRows(parsedFile.getPreviewRows());
|
item.setPreviewRows(parsedFile.getPreviewRows());
|
||||||
item.setTruncated(false);
|
item.setTruncated(false);
|
||||||
item.setSuccess(true);
|
item.setMatched(true);
|
||||||
|
item.setMatchStatus("MATCHED");
|
||||||
|
item.setMatchMessage(null);
|
||||||
finalItems.add(item);
|
finalItems.add(item);
|
||||||
successCount++;
|
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.entity.ShopKeyEntity;
|
||||||
import com.nanri.aiimage.modules.shopkey.model.vo.ShopKeyItemVo;
|
import com.nanri.aiimage.modules.shopkey.model.vo.ShopKeyItemVo;
|
||||||
import com.nanri.aiimage.modules.shopkey.model.vo.ShopKeyPageVo;
|
import com.nanri.aiimage.modules.shopkey.model.vo.ShopKeyPageVo;
|
||||||
|
import com.nanri.aiimage.modules.ziniao.service.ZiniaoShopIndexService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -16,9 +18,11 @@ import java.util.List;
|
|||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
public class ShopKeyService {
|
public class ShopKeyService {
|
||||||
|
|
||||||
private final ShopKeyMapper shopKeyMapper;
|
private final ShopKeyMapper shopKeyMapper;
|
||||||
|
private final ZiniaoShopIndexService ziniaoShopIndexService;
|
||||||
|
|
||||||
public ShopKeyPageVo page(long page, long pageSize) {
|
public ShopKeyPageVo page(long page, long pageSize) {
|
||||||
long safePage = Math.max(page, 1);
|
long safePage = Math.max(page, 1);
|
||||||
@@ -46,6 +50,7 @@ public class ShopKeyService {
|
|||||||
entity.setZiniaoAccountName(ziniaoAccountName);
|
entity.setZiniaoAccountName(ziniaoAccountName);
|
||||||
entity.setZiniaoToken(ziniaoToken);
|
entity.setZiniaoToken(ziniaoToken);
|
||||||
shopKeyMapper.insert(entity);
|
shopKeyMapper.insert(entity);
|
||||||
|
triggerShopIndexRefresh();
|
||||||
return toItemVo(getById(entity.getId()));
|
return toItemVo(getById(entity.getId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +62,7 @@ public class ShopKeyService {
|
|||||||
entity.setZiniaoAccountName(ziniaoAccountName);
|
entity.setZiniaoAccountName(ziniaoAccountName);
|
||||||
entity.setZiniaoToken(ziniaoToken);
|
entity.setZiniaoToken(ziniaoToken);
|
||||||
shopKeyMapper.updateById(entity);
|
shopKeyMapper.updateById(entity);
|
||||||
|
triggerShopIndexRefresh();
|
||||||
return toItemVo(getById(id));
|
return toItemVo(getById(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +70,7 @@ public class ShopKeyService {
|
|||||||
public void delete(Long id) {
|
public void delete(Long id) {
|
||||||
ShopKeyEntity entity = getById(id);
|
ShopKeyEntity entity = getById(id);
|
||||||
shopKeyMapper.deleteById(entity.getId());
|
shopKeyMapper.deleteById(entity.getId());
|
||||||
|
triggerShopIndexRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
private ShopKeyEntity getById(Long id) {
|
private ShopKeyEntity getById(Long id) {
|
||||||
@@ -91,4 +98,12 @@ public class ShopKeyService {
|
|||||||
vo.setUpdatedAt(entity.getUpdatedAt());
|
vo.setUpdatedAt(entity.getUpdatedAt());
|
||||||
return vo;
|
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;
|
package com.nanri.aiimage.modules.ziniao.controller;
|
||||||
|
|
||||||
import com.nanri.aiimage.common.api.ApiResponse;
|
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.dto.ZiniaoOpenShopRequest;
|
||||||
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoOpenShopVo;
|
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.ZiniaoSessionVo;
|
||||||
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoShopListVo;
|
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoShopListVo;
|
||||||
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoStaffListVo;
|
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoStaffListVo;
|
||||||
import com.nanri.aiimage.modules.ziniao.service.ZiniaoAuthService;
|
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.Operation;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
@@ -28,6 +30,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
public class ZiniaoAuthController {
|
public class ZiniaoAuthController {
|
||||||
|
|
||||||
private final ZiniaoAuthService ziniaoAuthService;
|
private final ZiniaoAuthService ziniaoAuthService;
|
||||||
|
private final ZiniaoShopIndexService ziniaoShopIndexService;
|
||||||
|
|
||||||
@GetMapping("/session")
|
@GetMapping("/session")
|
||||||
@Operation(summary = "获取紫鸟会话状态", description = "自动获取或复用 appToken,并返回 companyId、当前员工 userId、脱敏 token 和当前店铺信息。")
|
@Operation(summary = "获取紫鸟会话状态", description = "自动获取或复用 appToken,并返回 companyId、当前员工 userId、脱敏 token 和当前店铺信息。")
|
||||||
@@ -47,6 +50,12 @@ public class ZiniaoAuthController {
|
|||||||
return ApiResponse.success(ziniaoAuthService.listStaff());
|
return ApiResponse.success(ziniaoAuthService.listStaff());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/index-refresh")
|
||||||
|
@Operation(summary = "获取紫鸟店铺索引刷新状态", description = "返回后台定时刷新紫鸟店铺索引的最近一次执行状态、时间和无效 userId 样本。")
|
||||||
|
public ApiResponse<ZiniaoShopIndexRefreshCursorDto> getIndexRefreshStatus() {
|
||||||
|
return ApiResponse.success(ziniaoShopIndexService.getRefreshCursor());
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/shops")
|
@GetMapping("/shops")
|
||||||
@Operation(summary = "获取员工可见店铺列表", description = "先通过 API Key 获取 companyId,再按员工 userId 查询该员工有权限的店铺列表,并缓存到当前 session。")
|
@Operation(summary = "获取员工可见店铺列表", description = "先通过 API Key 获取 companyId,再按员工 userId 查询该员工有权限的店铺列表,并缓存到当前 session。")
|
||||||
@ApiResponses({
|
@ApiResponses({
|
||||||
|
|||||||
@@ -62,6 +62,24 @@ public class ZiniaoMemoryStoreService {
|
|||||||
return get(cacheType, cacheKey, type);
|
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
|
@Transactional
|
||||||
public void put(String cacheType, String cacheKey, Object payload, Duration ttl) {
|
public void put(String cacheType, String cacheKey, Object payload, Duration ttl) {
|
||||||
String normalizedType = normalizeRequired(cacheType, "cacheType 不能为空");
|
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
|
@Transactional
|
||||||
public int deleteExpired(int limit) {
|
public int deleteExpired(int limit) {
|
||||||
int safeLimit = Math.max(limit, 1);
|
int safeLimit = Math.max(limit, 1);
|
||||||
@@ -129,6 +165,14 @@ public class ZiniaoMemoryStoreService {
|
|||||||
.last("LIMIT 1"));
|
.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) {
|
private boolean isExpired(ZiniaoMemoryStoreEntity entity) {
|
||||||
return entity.getExpiresAt() == null || !entity.getExpiresAt().isAfter(LocalDateTime.now());
|
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 = "打开店铺链接")
|
@Schema(description = "打开店铺链接")
|
||||||
private String openStoreUrl;
|
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);
|
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) {
|
private boolean isSkippableUserStoresError(BusinessException ex) {
|
||||||
String message = ex == null ? null : ex.getMessage();
|
String message = ex == null ? null : ex.getMessage();
|
||||||
if (message == null || message.isBlank()) {
|
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 {
|
public class ZiniaoShopSwitchService {
|
||||||
|
|
||||||
private final ZiniaoAuthService ziniaoAuthService;
|
private final ZiniaoAuthService ziniaoAuthService;
|
||||||
|
private final ZiniaoShopIndexService ziniaoShopIndexService;
|
||||||
|
|
||||||
public ZiniaoShopMatchResultVo matchStoreByNameAcrossStaff(String targetShopName, Long preferUserId) {
|
public ZiniaoShopMatchResultVo matchStoreByNameAcrossStaff(String targetShopName, Long preferUserId) {
|
||||||
String normalizedShopName = normalizeShopName(targetShopName);
|
String normalizedShopName = normalizeShopName(targetShopName);
|
||||||
@@ -18,18 +19,27 @@ public class ZiniaoShopSwitchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ZiniaoAuthService.StoreMatchResult result = ziniaoAuthService.matchStoreByNameAcrossStaff(normalizedShopName, preferUserId);
|
ZiniaoAuthService.StoreMatchResult result = ziniaoAuthService.matchStoreByNameAcrossStaff(normalizedShopName, preferUserId);
|
||||||
return new ZiniaoShopMatchResultVo(
|
ZiniaoShopMatchResultVo vo = new ZiniaoShopMatchResultVo();
|
||||||
result.matched(),
|
vo.setMatched(result.matched());
|
||||||
result.shopId(),
|
vo.setShopId(result.shopId());
|
||||||
result.shopName(),
|
vo.setShopName(result.shopName());
|
||||||
result.platform(),
|
vo.setPlatform(result.platform());
|
||||||
result.matchedUserId(),
|
vo.setMatchedUserId(result.matchedUserId());
|
||||||
result.openStoreUrl()
|
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() {
|
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) {
|
public String normalizeShopName(String value) {
|
||||||
|
|||||||
@@ -108,5 +108,9 @@ aiimage:
|
|||||||
shops-cache-minutes: ${AIIMAGE_ZINIAO_SHOPS_CACHE_MINUTES:30}
|
shops-cache-minutes: ${AIIMAGE_ZINIAO_SHOPS_CACHE_MINUTES:30}
|
||||||
connect-timeout-seconds: ${AIIMAGE_ZINIAO_CONNECT_TIMEOUT_SECONDS:5}
|
connect-timeout-seconds: ${AIIMAGE_ZINIAO_CONNECT_TIMEOUT_SECONDS:5}
|
||||||
read-timeout-seconds: ${AIIMAGE_ZINIAO_READ_TIMEOUT_SECONDS:15}
|
read-timeout-seconds: ${AIIMAGE_ZINIAO_READ_TIMEOUT_SECONDS:15}
|
||||||
key-scan-max-keys: ${AIIMAGE_ZINIAO_KEY_SCAN_MAX_KEYS:50}
|
key-scan-max-keys: ${AIIMAGE_ZINIAO_KEY_SCAN_MAX_KEYS:100}
|
||||||
key-scan-max-seconds: ${AIIMAGE_ZINIAO_KEY_SCAN_MAX_SECONDS:15}
|
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}
|
||||||
|
|||||||
@@ -105,7 +105,7 @@
|
|||||||
<div class="left split-result-main">
|
<div class="left split-result-main">
|
||||||
<span class="id" :title="item.sourceFilename">{{ item.sourceFilename || '-' }}</span>
|
<span class="id" :title="item.sourceFilename">{{ item.sourceFilename || '-' }}</span>
|
||||||
<div class="files">店铺名:{{ item.shopName || '-' }}</div>
|
<div class="files">店铺名:{{ item.shopName || '-' }}</div>
|
||||||
<div class="files">匹配结果:{{ item.matched ? `已匹配 ${item.shopId || ''}` : '未匹配到紫鸟店铺' }}</div>
|
<div class="files">匹配结果:{{ formatMatchResult(item) }}</div>
|
||||||
<div v-if="item.platform" class="files">平台:{{ item.platform }}</div>
|
<div v-if="item.platform" class="files">平台:{{ item.platform }}</div>
|
||||||
<div v-if="getQueueStatus(item)" class="files">队列状态:{{ getQueueStatus(item) }}</div>
|
<div v-if="getQueueStatus(item)" class="files">队列状态:{{ getQueueStatus(item) }}</div>
|
||||||
<div v-if="item.countryCount !== undefined && item.matched" class="files">国家数:{{ item.countryCount
|
<div v-if="item.countryCount !== undefined && item.matched" class="files">国家数:{{ item.countryCount
|
||||||
@@ -158,7 +158,7 @@
|
|||||||
<div class="left split-result-main">
|
<div class="left split-result-main">
|
||||||
<span class="id" :title="item.sourceFilename">{{ item.sourceFilename || '-' }}</span>
|
<span class="id" :title="item.sourceFilename">{{ item.sourceFilename || '-' }}</span>
|
||||||
<div class="files">店铺名:{{ item.shopName || '-' }}</div>
|
<div class="files">店铺名:{{ item.shopName || '-' }}</div>
|
||||||
<div class="files">匹配结果:{{ item.matched ? `已匹配 ${item.shopId || ''}` : '未匹配到紫鸟店铺' }}</div>
|
<div class="files">匹配结果:{{ formatMatchResult(item) }}</div>
|
||||||
<div v-if="item.platform" class="files">平台:{{ item.platform }}</div>
|
<div v-if="item.platform" class="files">平台:{{ item.platform }}</div>
|
||||||
<div v-if="item.countryCount !== undefined && item.matched" class="files">国家数:{{ item.countryCount
|
<div v-if="item.countryCount !== undefined && item.matched" class="files">国家数:{{ item.countryCount
|
||||||
}}</div>
|
}}</div>
|
||||||
@@ -281,8 +281,7 @@ function loadSessionTasksFromStorage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function saveSessionTask(taskId: number, items: DeleteBrandResultItem[], pushResult?: string, payloadText?: string) {
|
function saveSessionTask(taskId: number, items: DeleteBrandResultItem[], pushResult?: string, payloadText?: string) {
|
||||||
// 只将解析成功且匹配成功的任务放入当前队列中,失败的将留在历史记录里
|
const validItems = items.filter(i => i.matchStatus === 'MATCHED' && i.success !== false)
|
||||||
const validItems = items.filter(i => i.matched !== false && i.success !== false)
|
|
||||||
if (validItems.length === 0) return
|
if (validItems.length === 0) return
|
||||||
|
|
||||||
const newTask: StoredCurrentTask = {
|
const newTask: StoredCurrentTask = {
|
||||||
@@ -326,6 +325,9 @@ function getTaskStatus(taskId?: number) {
|
|||||||
|
|
||||||
function getDisplayError(item: DeleteBrandResultItem) {
|
function getDisplayError(item: DeleteBrandResultItem) {
|
||||||
if (item.error) return item.error
|
if (item.error) return item.error
|
||||||
|
if (item.matchStatus && item.matchStatus !== 'MATCHED') {
|
||||||
|
return item.matchMessage || ''
|
||||||
|
}
|
||||||
const taskId = item.taskId
|
const taskId = item.taskId
|
||||||
if (!taskId) return ''
|
if (!taskId) return ''
|
||||||
return taskDetails.value[taskId]?.task?.errorMessage || ''
|
return taskDetails.value[taskId]?.task?.errorMessage || ''
|
||||||
@@ -339,18 +341,37 @@ function shouldShowProgress(item: DeleteBrandResultItem) {
|
|||||||
|
|
||||||
function normalizeDeleteBrandItems(items: DeleteBrandResultItem[]) {
|
function normalizeDeleteBrandItems(items: DeleteBrandResultItem[]) {
|
||||||
return items.map((item) => {
|
return items.map((item) => {
|
||||||
// 只有在既没成功,又显式标记为未匹配时,才做错误兜底
|
if (item.matchStatus === 'MATCHED') {
|
||||||
if (!item.success && item.matched === false) {
|
return item
|
||||||
return {
|
}
|
||||||
...item,
|
return {
|
||||||
error: item.error || '未匹配到紫鸟店铺',
|
...item,
|
||||||
openStoreUrl: undefined,
|
matched: false,
|
||||||
}
|
openStoreUrl: undefined,
|
||||||
|
error: item.error || undefined,
|
||||||
}
|
}
|
||||||
return item
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatMatchResult(item: DeleteBrandResultItem) {
|
||||||
|
if (item.matchStatus === 'MATCHED') {
|
||||||
|
return `已匹配 ${item.shopId || ''}`.trim()
|
||||||
|
}
|
||||||
|
if (item.matchMessage) {
|
||||||
|
return item.matchMessage
|
||||||
|
}
|
||||||
|
if (item.matchStatus === 'CONFLICT') {
|
||||||
|
return '存在多个同名店铺,请人工确认'
|
||||||
|
}
|
||||||
|
if (item.matchStatus === 'PENDING' || item.matchStatus === 'INDEX_STALE') {
|
||||||
|
return '店铺索引暂未就绪'
|
||||||
|
}
|
||||||
|
if (item.matchStatus) {
|
||||||
|
return item.matchStatus
|
||||||
|
}
|
||||||
|
return item.matched ? `已匹配 ${item.shopId || ''}`.trim() : '待匹配'
|
||||||
|
}
|
||||||
|
|
||||||
function mergeVisibleItems() {
|
function mergeVisibleItems() {
|
||||||
resultItems.value = [...currentSectionItems.value, ...historySectionItems.value]
|
resultItems.value = [...currentSectionItems.value, ...historySectionItems.value]
|
||||||
}
|
}
|
||||||
@@ -369,10 +390,6 @@ function updateSummary(items: DeleteBrandResultItem[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getItemTaskId(item: DeleteBrandResultItem) {
|
|
||||||
return item.taskId || 0
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatProgress(taskId: number) {
|
function formatProgress(taskId: number) {
|
||||||
if (!taskId) return ''
|
if (!taskId) return ''
|
||||||
const detail = taskDetails.value[taskId]
|
const detail = taskDetails.value[taskId]
|
||||||
@@ -410,11 +427,6 @@ function formatProgressPercent(taskId: number) {
|
|||||||
return status === 'SUCCESS' ? 100 : 0
|
return status === 'SUCCESS' ? 100 : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTaskRunning(taskId: number) {
|
|
||||||
const status = taskDetails.value[taskId]?.task?.status
|
|
||||||
return status === 'RUNNING'
|
|
||||||
}
|
|
||||||
|
|
||||||
function isTaskTerminal(taskId: number) {
|
function isTaskTerminal(taskId: number) {
|
||||||
const status = taskDetails.value[taskId]?.task?.status
|
const status = taskDetails.value[taskId]?.task?.status
|
||||||
return status === 'SUCCESS' || status === 'FAILED'
|
return status === 'SUCCESS' || status === 'FAILED'
|
||||||
@@ -477,18 +489,58 @@ function canDownloadTaskResult(item: DeleteBrandResultItem) {
|
|||||||
return status === 'SUCCESS'
|
return status === 'SUCCESS'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mergeTaskDetailItemsIntoSession(detail: DeleteBrandTaskDetailVo) {
|
||||||
|
const taskId = detail?.task?.id
|
||||||
|
if (!taskId || !detail?.items?.length) return false
|
||||||
|
const sessionTask = sessionTasks.value.find(t => t.taskId === taskId)
|
||||||
|
if (!sessionTask) return false
|
||||||
|
|
||||||
|
let changed = false
|
||||||
|
const mergedItems = sessionTask.items.map((item) => {
|
||||||
|
const latest = detail.items?.find((candidate) => candidate.resultId === item.resultId || candidate.sourceFilename === item.sourceFilename)
|
||||||
|
if (!latest) return item
|
||||||
|
const merged = {
|
||||||
|
...item,
|
||||||
|
...latest,
|
||||||
|
success: item.success ?? latest.success,
|
||||||
|
}
|
||||||
|
if (JSON.stringify(merged) !== JSON.stringify(item)) {
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
return merged
|
||||||
|
})
|
||||||
|
|
||||||
|
const validItems = mergedItems.filter(i => i.matchStatus === 'MATCHED' && i.success !== false)
|
||||||
|
if (JSON.stringify(validItems) !== JSON.stringify(sessionTask.items)) {
|
||||||
|
sessionTask.items = validItems
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed && typeof window !== 'undefined') {
|
||||||
|
window.localStorage.setItem(getStorageKey(), JSON.stringify(sessionTasks.value))
|
||||||
|
}
|
||||||
|
return changed
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshTaskDetails(taskIds?: number[]) {
|
async function refreshTaskDetails(taskIds?: number[]) {
|
||||||
const ids = taskIds || getPollingTaskIds()
|
const ids = taskIds || getPollingTaskIds()
|
||||||
if (!ids.length) return
|
if (!ids.length) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const batch = await getDeleteBrandTaskDetails(ids)
|
const batch = await getDeleteBrandTaskDetails(ids)
|
||||||
|
let changed = false
|
||||||
for (const detail of batch.items || []) {
|
for (const detail of batch.items || []) {
|
||||||
const id = detail?.task?.id
|
const id = detail?.task?.id
|
||||||
if (typeof id === 'number' && id > 0) {
|
if (typeof id === 'number' && id > 0) {
|
||||||
taskDetails.value[id] = detail
|
taskDetails.value[id] = detail
|
||||||
|
if (mergeTaskDetailItemsIntoSession(detail)) {
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (changed) {
|
||||||
|
syncResultState()
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// ignore polling failures
|
// ignore polling failures
|
||||||
}
|
}
|
||||||
@@ -649,7 +701,8 @@ async function submitRun() {
|
|||||||
})),
|
})),
|
||||||
})
|
})
|
||||||
const normalizedItems = normalizeDeleteBrandItems(result.items || [])
|
const normalizedItems = normalizeDeleteBrandItems(result.items || [])
|
||||||
const hasUnmatched = normalizedItems.some((item) => !item.matched)
|
const hasMatchedItems = normalizedItems.some((item) => item.matchStatus === 'MATCHED')
|
||||||
|
const hasPendingItems = normalizedItems.some((item) => item.matchStatus && item.matchStatus !== 'MATCHED')
|
||||||
|
|
||||||
queuePushResult.value = '解析完成,等待手动点击打开紫鸟进行单任务处理'
|
queuePushResult.value = '解析完成,等待手动点击打开紫鸟进行单任务处理'
|
||||||
queuePayloadText.value = ''
|
queuePayloadText.value = ''
|
||||||
@@ -659,10 +712,12 @@ async function submitRun() {
|
|||||||
}
|
}
|
||||||
syncResultState()
|
syncResultState()
|
||||||
|
|
||||||
if (hasUnmatched) {
|
if (hasPendingItems) {
|
||||||
ElMessage.error('存在未匹配到紫鸟店铺的文件,请检查报错内容。这些文件已自动归档至历史记录。')
|
ElMessage.warning('部分文件尚未命中可用店铺索引,已保留状态信息,请等待后台刷新后重试。')
|
||||||
} else {
|
} else if (hasMatchedItems) {
|
||||||
ElMessage.success('删除品牌解析完成,请手动推送')
|
ElMessage.success('删除品牌解析完成,请手动推送')
|
||||||
|
} else {
|
||||||
|
ElMessage.warning('当前没有可推送的已匹配文件,请先等待店铺索引刷新。')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新历史记录,以展示那些未进入队列的失败项
|
// 更新历史记录,以展示那些未进入队列的失败项
|
||||||
@@ -783,10 +838,7 @@ function findNextAutoRunnableItem() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function runItem(item: DeleteBrandResultItem, options?: { auto?: boolean }) {
|
async function runItem(item: DeleteBrandResultItem, options?: { auto?: boolean }) {
|
||||||
if (isPythonQueueBusy()) {
|
if (options?.auto && isPythonQueueBusy()) {
|
||||||
if (!options?.auto) {
|
|
||||||
ElMessage.warning('当前存在正在处理的任务,请等待其完成后再推送下一条!')
|
|
||||||
}
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -816,7 +868,7 @@ async function runItem(item: DeleteBrandResultItem, options?: { auto?: boolean }
|
|||||||
const pushResult = await api.enqueue_json(payload)
|
const pushResult = await api.enqueue_json(payload)
|
||||||
let qpr = ''
|
let qpr = ''
|
||||||
if (pushResult?.success) {
|
if (pushResult?.success) {
|
||||||
qpr = `已单独推送文件 ${item.sourceFilename},当前队列长度:${pushResult.queue_size ?? '-'}`
|
qpr = `已推送文件 ${item.sourceFilename},当前队列长度:${pushResult.queue_size ?? '-'}`
|
||||||
|
|
||||||
const sessionItem = sessionTask.items.find(i => i.resultId === item.resultId || i.sourceFilename === item.sourceFilename)
|
const sessionItem = sessionTask.items.find(i => i.resultId === item.resultId || i.sourceFilename === item.sourceFilename)
|
||||||
if (sessionItem) {
|
if (sessionItem) {
|
||||||
@@ -874,10 +926,6 @@ async function triggerItemRun(item: DeleteBrandResultItem) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openShop(url: string) {
|
|
||||||
window.open(url, '_blank')
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadSessionTasksFromStorage()
|
loadSessionTasksFromStorage()
|
||||||
const taskIds = getPollingTaskIds()
|
const taskIds = getPollingTaskIds()
|
||||||
|
|||||||
@@ -211,12 +211,16 @@ export interface DeleteBrandTaskBatchVo {
|
|||||||
missingTaskIds: number[]
|
missingTaskIds: number[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DeleteBrandMatchStatus = 'MATCHED' | 'PENDING' | 'CONFLICT' | 'INDEX_STALE'
|
||||||
|
|
||||||
export interface DeleteBrandResultItem {
|
export interface DeleteBrandResultItem {
|
||||||
resultId?: number
|
resultId?: number
|
||||||
fileKey?: string
|
fileKey?: string
|
||||||
sourceFilename: string
|
sourceFilename: string
|
||||||
shopName?: string
|
shopName?: string
|
||||||
matched?: boolean
|
matched?: boolean
|
||||||
|
matchStatus?: DeleteBrandMatchStatus
|
||||||
|
matchMessage?: string
|
||||||
shopId?: string
|
shopId?: string
|
||||||
platform?: string
|
platform?: string
|
||||||
openStoreUrl?: string
|
openStoreUrl?: string
|
||||||
|
|||||||
Reference in New Issue
Block a user