紫鸟查询店铺更新
This commit is contained in:
@@ -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