删除模块 取消打开紫鸟

This commit is contained in:
super
2026-03-31 23:12:46 +08:00
parent 25ce9f74b8
commit 6eb0f33421
9 changed files with 260 additions and 60 deletions

Binary file not shown.

View File

@@ -7,6 +7,7 @@ import com.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandTaskBatchReque
import com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandHistoryVo;
import com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandRunVo;
import com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandTaskBatchVo;
import com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandTaskDeletionStatusVo;
import com.nanri.aiimage.modules.deletebrand.service.DeleteBrandRunService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -83,6 +84,15 @@ public class DeleteBrandRunController {
return ApiResponse.success(deleteBrandRunService.getTaskDetails(request.getTaskIds()));
}
@GetMapping("/tasks/{taskId}/deletion-status")
@Operation(summary = "获取删除品牌任务删除状态", description = "供 Python/插件按 taskId 查询该删除品牌任务是否已被前端删空。")
public ApiResponse<DeleteBrandTaskDeletionStatusVo> getTaskDeletionStatus(
@PathVariable Long taskId,
@Parameter(name = "user_id", description = "当前登录用户 ID", required = true, in = ParameterIn.QUERY)
@RequestParam("user_id") Long userId) {
return ApiResponse.success(deleteBrandRunService.getTaskDeletionStatus(taskId, userId));
}
@PostMapping("/tasks/{taskId}/result")
@Operation(summary = "提交删除品牌处理结果", description = "插件/前端分片回传处理结果;后端累计分片并推进进度,全部完成后再执行最终组装与落库。")
public ApiResponse<Void> submitResult(

View File

@@ -0,0 +1,17 @@
package com.nanri.aiimage.modules.deletebrand.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "删除品牌任务删除状态响应")
public class DeleteBrandTaskDeletionStatusVo {
@Schema(description = "任务ID")
private Long taskId;
@Schema(description = "该任务的结果记录是否已被全部删除")
private boolean deleted;
@Schema(description = "该任务当前剩余的结果记录数量")
private long remainingResultCount;
}

View File

@@ -23,6 +23,7 @@ import com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandLineProgressVo;
import com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandPreviewRowVo;
import com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandResultItemVo;
import com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandRunVo;
import com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandTaskDeletionStatusVo;
import com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandTaskDetailVo;
import com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandTaskItemVo;
import com.nanri.aiimage.modules.file.service.LocalFileStorageService;
@@ -31,6 +32,8 @@ import com.nanri.aiimage.modules.task.mapper.FileResultMapper;
import com.nanri.aiimage.modules.task.mapper.FileTaskMapper;
import com.nanri.aiimage.modules.task.model.entity.FileResultEntity;
import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity;
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoShopMatchResultVo;
import com.nanri.aiimage.modules.ziniao.service.ZiniaoShopSwitchService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Cell;
@@ -68,7 +71,7 @@ public class DeleteBrandRunService {
private final FileResultMapper fileResultMapper;
private final LocalFileStorageService localFileStorageService;
private final DeleteBrandTaskCacheService deleteBrandTaskCacheService;
private final com.nanri.aiimage.modules.ziniao.service.ZiniaoAuthService ziniaoAuthService;
private final ZiniaoShopSwitchService ziniaoShopSwitchService;
private final OssStorageService ossStorageService;
private final ObjectMapper objectMapper;
private final DeleteBrandProgressProperties deleteBrandProgressProperties;
@@ -96,7 +99,7 @@ public class DeleteBrandRunService {
int parsedSuccessCount = 0;
int parsedFailedCount = 0;
Map<String, DeleteBrandParsedFileCacheDto> parsedPayloadByFileIdentity = new LinkedHashMap<>();
Map<String, com.nanri.aiimage.modules.ziniao.service.ZiniaoAuthService.StoreMatchResult> storeMatchByShopName = new LinkedHashMap<>();
Map<String, ZiniaoShopMatchResultVo> storeMatchByShopName = new LinkedHashMap<>();
for (DeleteBrandSourceFileDto sourceFile : request.getFiles()) {
DeleteBrandResultItemVo item = new DeleteBrandResultItemVo();
@@ -117,25 +120,22 @@ public class DeleteBrandRunService {
item.setPreviewRows(parsed.previewRows());
try {
String normalizedShopName = normalizeShopName(item.getShopName());
com.nanri.aiimage.modules.ziniao.service.ZiniaoAuthService.StoreMatchResult matchResult = normalizedShopName.isBlank()
? new com.nanri.aiimage.modules.ziniao.service.ZiniaoAuthService.StoreMatchResult(false, null, null, null, null, null)
String normalizedShopName = ziniaoShopSwitchService.normalizeShopName(item.getShopName());
ZiniaoShopMatchResultVo matchResult = normalizedShopName.isBlank()
? ziniaoShopSwitchService.emptyMatchResult()
: storeMatchByShopName.computeIfAbsent(normalizedShopName,
ignored -> ziniaoAuthService.matchStoreByNameAcrossStaff(item.getShopName(), null));
item.setMatched(matchResult.matched());
if (matchResult.matched()) {
item.setShopId(matchResult.shopId());
item.setPlatform(matchResult.platform());
item.setOpenStoreUrl(matchResult.openStoreUrl());
ignored -> ziniaoShopSwitchService.matchStoreByNameAcrossStaff(item.getShopName(), null));
item.setMatched(matchResult.isMatched());
if (matchResult.isMatched()) {
item.setShopId(matchResult.getShopId());
item.setPlatform(matchResult.getPlatform());
item.setOpenStoreUrl(matchResult.getOpenStoreUrl());
} else {
throw new BusinessException("未匹配到紫鸟店铺,跳过处理");
}
} catch (BusinessException ex) {
String message = ex.getMessage();
if (message != null && (message.contains("code=40004")
|| message.contains("userId存在无效的参数值")
|| message.contains("无权限")
|| message.contains("没有权限")) && !message.contains("白名单")) {
if (ziniaoShopSwitchService.isSkippableMatchError(ex)) {
log.warn("[ziniao-match] suppressed upstream permission error for {}, msg: {}", item.getShopName(), message);
item.setMatched(false);
} else {
@@ -301,6 +301,24 @@ public class DeleteBrandRunService {
return vo;
}
public DeleteBrandTaskDeletionStatusVo getTaskDeletionStatus(Long taskId, Long userId) {
FileTaskEntity task = fileTaskMapper.selectById(taskId);
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) {
throw new BusinessException("任务不存在");
}
Long remainingResultCount = fileResultMapper.selectCount(new LambdaQueryWrapper<FileResultEntity>()
.eq(FileResultEntity::getTaskId, taskId)
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
.eq(FileResultEntity::getUserId, userId));
DeleteBrandTaskDeletionStatusVo vo = new DeleteBrandTaskDeletionStatusVo();
vo.setTaskId(taskId);
vo.setRemainingResultCount(remainingResultCount == null ? 0L : remainingResultCount);
vo.setDeleted(vo.getRemainingResultCount() == 0);
return vo;
}
public void deleteHistory(Long resultId, Long userId) {
FileResultEntity entity = fileResultMapper.selectById(resultId);
if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {
@@ -400,13 +418,6 @@ public class DeleteBrandRunService {
return value.replace(" ", "").trim().toUpperCase(Locale.ROOT);
}
private String normalizeShopName(String value) {
if (value == null) {
return "";
}
return value.replace("\u3000", " ").trim();
}
private List<CountryColumnPair> resolveCountryPairs(Row titleRow, Row headerRow, DataFormatter formatter) {
List<CountryColumnPair> pairs = new ArrayList<>();
int lastCellNum = Math.max(titleRow.getLastCellNum(), headerRow.getLastCellNum());
@@ -876,7 +887,7 @@ public class DeleteBrandRunService {
throw new BusinessException("存在未完成的结果分片: " + parsedFile.getSourceFilename());
}
MergedDeleteBrandFile mergedFile = mergeChunks(parsedFile, chunks);
File outputFile = buildResultWorkbook(task.getId(), parsedFile, mergedFile);
File outputFile = buildResultWorkbookPreserveLayout(task.getId(), parsedFile, mergedFile);
deleteBrandTaskCacheService.saveProgress(task.getId(), Map.of(
"phase", DeleteBrandTaskCacheService.PHASE_UPLOADING,
@@ -1032,6 +1043,52 @@ public class DeleteBrandRunService {
}
}
private File buildResultWorkbookPreserveLayout(Long taskId,
DeleteBrandParsedFileCacheDto parsedFile,
MergedDeleteBrandFile mergedFile) {
File outputDir = FileUtil.mkdir(FileUtil.file(System.getProperty("java.io.tmpdir"), "delete-brand-result", String.valueOf(taskId)));
File outputFile = buildNamedOutputFile(outputDir, blankToDefault(parsedFile.getSourceFilename(), "delete-brand-result.xlsx"));
try (Workbook workbook = new XSSFWorkbook(); FileOutputStream outputStream = new FileOutputStream(outputFile)) {
Sheet sheet = workbook.createSheet("删除品牌结果");
Row countryRow = sheet.createRow(0);
Row headerRow = sheet.createRow(1);
for (int countryIndex = 0; countryIndex < mergedFile.countries().size(); countryIndex++) {
DeleteBrandProcessedCountryDto country = mergedFile.countries().get(countryIndex);
int asinColumnIndex = countryIndex * 2;
int statusColumnIndex = asinColumnIndex + 1;
createTextCell(countryRow, asinColumnIndex, country.getCountry());
createTextCell(headerRow, asinColumnIndex, "删除ASIN");
createTextCell(headerRow, statusColumnIndex, "状态");
List<DeleteBrandCountryResultItemDto> items = country.getItems();
if (items == null) {
continue;
}
for (int itemIndex = 0; itemIndex < items.size(); itemIndex++) {
DeleteBrandCountryResultItemDto item = items.get(itemIndex);
Row row = sheet.getRow(itemIndex + 2);
if (row == null) {
row = sheet.createRow(itemIndex + 2);
}
createTextCell(row, asinColumnIndex, item.getAsin());
createTextCell(row, statusColumnIndex, item.getStatus());
}
}
for (int i = 0; i < mergedFile.countries().size() * 2; i++) {
sheet.autoSizeColumn(i);
}
workbook.write(outputStream);
return outputFile;
} catch (Exception ex) {
throw new BusinessException("生成删除品牌结果文件失败: " + parsedFile.getSourceFilename());
}
}
private void createTextCell(Row row, int index, String value) {
Cell cell = row.createCell(index);
cell.setCellValue(value == null ? "" : value);

View File

@@ -0,0 +1,30 @@
package com.nanri.aiimage.modules.ziniao.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "紫鸟店铺匹配结果")
public class ZiniaoShopMatchResultVo {
@Schema(description = "是否匹配成功")
private boolean matched;
@Schema(description = "店铺ID")
private String shopId;
@Schema(description = "店铺名称")
private String shopName;
@Schema(description = "平台")
private String platform;
@Schema(description = "匹配到的员工 userId")
private Long matchedUserId;
@Schema(description = "打开店铺链接")
private String openStoreUrl;
}

View File

@@ -0,0 +1,53 @@
package com.nanri.aiimage.modules.ziniao.service;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoShopMatchResultVo;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ZiniaoShopSwitchService {
private final ZiniaoAuthService ziniaoAuthService;
public ZiniaoShopMatchResultVo matchStoreByNameAcrossStaff(String targetShopName, Long preferUserId) {
String normalizedShopName = normalizeShopName(targetShopName);
if (normalizedShopName.isBlank()) {
return emptyMatchResult();
}
ZiniaoAuthService.StoreMatchResult result = ziniaoAuthService.matchStoreByNameAcrossStaff(normalizedShopName, preferUserId);
return new ZiniaoShopMatchResultVo(
result.matched(),
result.shopId(),
result.shopName(),
result.platform(),
result.matchedUserId(),
result.openStoreUrl()
);
}
public ZiniaoShopMatchResultVo emptyMatchResult() {
return new ZiniaoShopMatchResultVo(false, null, null, null, null, null);
}
public String normalizeShopName(String value) {
if (value == null) {
return "";
}
return value.replace("\u3000", " ").trim();
}
public boolean isSkippableMatchError(BusinessException ex) {
String message = ex == null ? null : ex.getMessage();
if (message == null || message.isBlank()) {
return false;
}
return (message.contains("code=40004")
|| message.contains("userId\u5b58\u5728\u65e0\u6548\u7684\u53c2\u6570\u503c")
|| message.contains("\u65e0\u6743\u9650")
|| message.contains("\u6ca1\u6709\u6743\u9650"))
&& !message.contains("\u767d\u540d\u5355");
}
}