删除品牌
This commit is contained in:
1
app/assets/delete-brand-BgPrpPsj.css
Normal file
1
app/assets/delete-brand-BgPrpPsj.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -7,7 +7,7 @@
|
|||||||
<script type="module" crossorigin src="/assets/delete-brand.js"></script>
|
<script type="module" crossorigin src="/assets/delete-brand.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-Bl6M97uH.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-Bl6M97uH.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-8G_9KMXd.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-8G_9KMXd.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/delete-brand-D1WtGorm.css">
|
<link rel="stylesheet" crossorigin href="/assets/delete-brand-BgPrpPsj.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
702
app/user_data/2026_03_27.log
Normal file
702
app/user_data/2026_03_27.log
Normal file
File diff suppressed because one or more lines are too long
BIN
backend-java/aiimage-backend.log.2026-03-26.0.gz
Normal file
BIN
backend-java/aiimage-backend.log.2026-03-26.0.gz
Normal file
Binary file not shown.
@@ -16,6 +16,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
@@ -27,6 +28,11 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@RequestMapping("/api/brand")
|
@RequestMapping("/api/brand")
|
||||||
@@ -191,17 +197,34 @@ public class BrandTaskController {
|
|||||||
@GetMapping("/tasks/{taskId}/download")
|
@GetMapping("/tasks/{taskId}/download")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "下载品牌任务结果",
|
summary = "下载品牌任务结果",
|
||||||
description = "根据任务 ID 读取 brand_crawl_tasks.result_paths,优先跳转 zip_url;若没有 zip_url,则跳转第一个可用结果 URL。"
|
description = "根据任务 ID 读取 brand_crawl_tasks.result_paths,后端代理拉取 OSS 文件并直接返回给浏览器,避免跨域问题。"
|
||||||
)
|
)
|
||||||
@ApiResponses({
|
@ApiResponses({
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "302", description = "重定向到 OSS 结果文件地址"),
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "文件流(application/zip)"),
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "任务不存在或无结果可下载")
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "任务不存在或无结果可下载")
|
||||||
})
|
})
|
||||||
public ResponseEntity<Void> download(
|
public void download(
|
||||||
@Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId) {
|
@Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId,
|
||||||
String url = brandTaskService.resolveDownloadUrl(taskId);
|
HttpServletResponse response) throws Exception {
|
||||||
return ResponseEntity.status(302)
|
String ossUrl = brandTaskService.resolveDownloadUrl(taskId);
|
||||||
.header(HttpHeaders.LOCATION, url)
|
// 从 OSS URL 路径中取文件名
|
||||||
.build();
|
String rawPath = URI.create(ossUrl).getPath();
|
||||||
|
String filename = rawPath.substring(rawPath.lastIndexOf('/') + 1);
|
||||||
|
if (filename.isBlank()) {
|
||||||
|
filename = "brand_task_" + taskId + ".zip";
|
||||||
|
}
|
||||||
|
String encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8).replace("+", "%20");
|
||||||
|
response.setContentType("application/zip");
|
||||||
|
response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
|
||||||
|
"attachment; filename=\"" + filename + "\"; filename*=UTF-8''" + encodedFilename);
|
||||||
|
// 后端代理拉取 OSS 文件并流式写出
|
||||||
|
try (InputStream in = URI.create(ossUrl).toURL().openStream()) {
|
||||||
|
byte[] buffer = new byte[65536];
|
||||||
|
int read;
|
||||||
|
while ((read = in.read(buffer)) != -1) {
|
||||||
|
response.getOutputStream().write(buffer, 0, read);
|
||||||
|
}
|
||||||
|
response.getOutputStream().flush();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,7 +277,11 @@ public class BrandTaskService {
|
|||||||
String originalFilename = resolveOriginalFilename(sourceFile, sourceLocalFile);
|
String originalFilename = resolveOriginalFilename(sourceFile, sourceLocalFile);
|
||||||
File outputFile = buildNamedOutputFile(outputDir, buildResultFilename(originalFilename));
|
File outputFile = buildNamedOutputFile(outputDir, buildResultFilename(originalFilename));
|
||||||
writeBrandWorkbook(outputFile, request.getStrategy(), cachedFile, resultFile);
|
writeBrandWorkbook(outputFile, request.getStrategy(), cachedFile, resultFile);
|
||||||
outputEntries.add(new OutputEntry(sourceLocalFile, originalFilename, outputFile, outputFile.getName()));
|
outputEntries.add(new OutputEntry(
|
||||||
|
sourceLocalFile,
|
||||||
|
originalFilename,
|
||||||
|
outputFile,
|
||||||
|
originalFilename));
|
||||||
}
|
}
|
||||||
|
|
||||||
brandTaskProgressCacheService.updatePhase(taskId, BrandTaskProgressCacheService.PHASE_UPLOADING, finishedCount, totalCount);
|
brandTaskProgressCacheService.updatePhase(taskId, BrandTaskProgressCacheService.PHASE_UPLOADING, finishedCount, totalCount);
|
||||||
@@ -338,18 +342,24 @@ public class BrandTaskService {
|
|||||||
if (!(raw instanceof Map<?, ?> map)) {
|
if (!(raw instanceof Map<?, ?> map)) {
|
||||||
throw new BusinessException("无结果可下载");
|
throw new BusinessException("无结果可下载");
|
||||||
}
|
}
|
||||||
|
String stored = null;
|
||||||
Object zipUrl = map.get("zip_url");
|
Object zipUrl = map.get("zip_url");
|
||||||
if (zipUrl instanceof String zip && !zip.isBlank()) {
|
if (zipUrl instanceof String zip && !zip.isBlank()) {
|
||||||
return zip;
|
stored = zip;
|
||||||
}
|
} else {
|
||||||
Object urls = map.get("urls");
|
Object urls = map.get("urls");
|
||||||
if (urls instanceof List<?> list && !list.isEmpty()) {
|
if (urls instanceof List<?> list && !list.isEmpty()) {
|
||||||
Object first = list.get(0);
|
Object first = list.get(0);
|
||||||
if (first instanceof String url && !url.isBlank()) {
|
if (first instanceof String url && !url.isBlank()) {
|
||||||
return url;
|
stored = url;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new BusinessException("无结果可下载");
|
if (stored == null || stored.isBlank()) {
|
||||||
|
throw new BusinessException("无结果可下载");
|
||||||
|
}
|
||||||
|
// 统一通过 OssStorageService.generateFreshDownloadUrl 生成新鲜预签名 URL
|
||||||
|
return ossStorageService.generateFreshDownloadUrl(stored);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<BrandCrawlTaskEntity> listTaskEntities(Long userId) {
|
private List<BrandCrawlTaskEntity> listTaskEntities(Long userId) {
|
||||||
@@ -852,28 +862,29 @@ public class BrandTaskService {
|
|||||||
if (entries.isEmpty()) {
|
if (entries.isEmpty()) {
|
||||||
throw new BusinessException("没有可上传的结果文件");
|
throw new BusinessException("没有可上传的结果文件");
|
||||||
}
|
}
|
||||||
List<String> urls = new ArrayList<>();
|
List<String> fullUrls = new ArrayList<>();
|
||||||
for (OutputEntry entry : entries) {
|
for (OutputEntry entry : entries) {
|
||||||
|
// 存储完整公开地址,下载时会自动通过 resolveObjectKey 提取并重新签名(见 OssStorageService.resolveObjectKey)
|
||||||
String objectKey = ossStorageService.uploadResultFile(entry.resultFile(), "BRAND");
|
String objectKey = ossStorageService.uploadResultFile(entry.resultFile(), "BRAND");
|
||||||
urls.add(ossStorageService.generateDownloadUrl(objectKey));
|
fullUrls.add(ossStorageService.getPublicUrl(objectKey));
|
||||||
}
|
}
|
||||||
Map<String, Object> result = new LinkedHashMap<>();
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
result.put("urls", urls);
|
result.put("urls", fullUrls);
|
||||||
File zipFile = packageAsZip(taskId, entries);
|
File zipFile = packageAsZip(taskId, entries);
|
||||||
String zipObjectKey = ossStorageService.uploadResultFile(zipFile, "BRAND");
|
String zipObjectKey = ossStorageService.uploadResultFile(zipFile, "BRAND");
|
||||||
result.put("zip_url", ossStorageService.generateDownloadUrl(zipObjectKey));
|
result.put("zip_url", ossStorageService.getPublicUrl(zipObjectKey));
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private File packageAsZip(Long taskId, List<OutputEntry> entries) throws IOException {
|
private File packageAsZip(Long taskId, List<OutputEntry> entries) throws IOException {
|
||||||
File zipDir = FileUtil.mkdir(FileUtil.file(storageProperties.getLocalTempDir(), "brand-result", String.valueOf(taskId)));
|
File zipDir = FileUtil.mkdir(FileUtil.file(storageProperties.getLocalTempDir(), "brand-result", String.valueOf(taskId)));
|
||||||
File zipFile = buildNamedOutputFile(zipDir, "brand_task_" + taskId + ".zip");
|
File zipFile = buildNamedOutputFile(zipDir, buildArchiveFilename(entries));
|
||||||
Set<String> zipEntryNames = new LinkedHashSet<>();
|
Set<String> zipEntryNames = new LinkedHashSet<>();
|
||||||
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
|
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
|
||||||
byte[] buffer = new byte[8192];
|
byte[] buffer = new byte[8192];
|
||||||
for (OutputEntry entry : entries) {
|
for (OutputEntry entry : entries) {
|
||||||
writeZipEntry(zos, buffer, entry.sourceFile(), buildSourceZipEntry(entry.sourceFilename()), zipEntryNames);
|
writeZipEntry(zos, buffer, entry.sourceFile(), buildSourceZipEntry(entry.zipSourceFilename()), zipEntryNames);
|
||||||
writeZipEntry(zos, buffer, entry.resultFile(), buildResultZipEntry(entry.resultFilename()), zipEntryNames);
|
writeZipEntry(zos, buffer, entry.resultFile(), buildResultZipEntry(entry.zipResultFilename()), zipEntryNames);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return zipFile;
|
return zipFile;
|
||||||
@@ -933,8 +944,15 @@ public class BrandTaskService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String buildResultFilename(String originalFilename) {
|
private String buildResultFilename(String originalFilename) {
|
||||||
String sourceName = blankToDefault(originalFilename, "brand.xlsx");
|
return blankToDefault(originalFilename, "brand.xlsx");
|
||||||
return FileUtil.mainName(sourceName) + "_result.xlsx";
|
}
|
||||||
|
|
||||||
|
private String buildArchiveFilename(List<OutputEntry> entries) {
|
||||||
|
if (entries == null || entries.isEmpty()) {
|
||||||
|
return "brand.zip";
|
||||||
|
}
|
||||||
|
String sourceFilename = blankToDefault(entries.get(0).zipSourceFilename(), "brand.xlsx");
|
||||||
|
return FileUtil.mainName(sourceFilename) + ".zip";
|
||||||
}
|
}
|
||||||
|
|
||||||
private Integer defaultInteger(Integer value) {
|
private Integer defaultInteger(Integer value) {
|
||||||
@@ -1062,6 +1080,6 @@ public class BrandTaskService {
|
|||||||
List<String> uniqueBrands) {
|
List<String> uniqueBrands) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private record OutputEntry(File sourceFile, String sourceFilename, File resultFile, String resultFilename) {
|
private record OutputEntry(File sourceFile, String zipSourceFilename, File resultFile, String zipResultFilename) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,20 +114,19 @@ public class ConvertRunService {
|
|||||||
String contentType = useZipPackage ? "application/zip" : "text/plain";
|
String contentType = useZipPackage ? "application/zip" : "text/plain";
|
||||||
|
|
||||||
String ossObjectKey = ossStorageService.uploadResultFile(packagedResultFile, MODULE_TYPE);
|
String ossObjectKey = ossStorageService.uploadResultFile(packagedResultFile, MODULE_TYPE);
|
||||||
String downloadUrl = ossStorageService.generateDownloadUrl(ossObjectKey);
|
// 只存 objectKey
|
||||||
|
|
||||||
ConvertResultItemVo item = new ConvertResultItemVo();
|
ConvertResultItemVo item = new ConvertResultItemVo();
|
||||||
item.setSourceFilename(inputName);
|
item.setSourceFilename(inputName);
|
||||||
item.setSuccess(true);
|
item.setSuccess(true);
|
||||||
item.setOutputFilename(packagedFilename);
|
item.setOutputFilename(packagedFilename);
|
||||||
item.setDownloadUrl(downloadUrl);
|
item.setDownloadUrl(ossStorageService.generateFreshDownloadUrl(ossObjectKey));
|
||||||
|
|
||||||
FileResultEntity resultEntity = new FileResultEntity();
|
FileResultEntity resultEntity = new FileResultEntity();
|
||||||
resultEntity.setTaskId(task.getId());
|
resultEntity.setTaskId(task.getId());
|
||||||
resultEntity.setModuleType(MODULE_TYPE);
|
resultEntity.setModuleType(MODULE_TYPE);
|
||||||
resultEntity.setSourceFilename(inputName);
|
resultEntity.setSourceFilename(inputName);
|
||||||
resultEntity.setResultFilename(packagedFilename);
|
resultEntity.setResultFilename(packagedFilename);
|
||||||
resultEntity.setResultFileUrl(downloadUrl);
|
resultEntity.setResultFileUrl(ossObjectKey); // 存 objectKey
|
||||||
resultEntity.setResultFileSize(packagedResultFile.length());
|
resultEntity.setResultFileSize(packagedResultFile.length());
|
||||||
resultEntity.setResultContentType(contentType);
|
resultEntity.setResultContentType(contentType);
|
||||||
resultEntity.setSuccess(1);
|
resultEntity.setSuccess(1);
|
||||||
@@ -161,20 +160,19 @@ public class ConvertRunService {
|
|||||||
if (folderMode && !archiveEntries.isEmpty()) {
|
if (folderMode && !archiveEntries.isEmpty()) {
|
||||||
File zipFile = packageFolderConvertResultsAsZip(request.getArchiveName(), archiveEntries);
|
File zipFile = packageFolderConvertResultsAsZip(request.getArchiveName(), archiveEntries);
|
||||||
String ossObjectKey = ossStorageService.uploadResultFile(zipFile, MODULE_TYPE);
|
String ossObjectKey = ossStorageService.uploadResultFile(zipFile, MODULE_TYPE);
|
||||||
String downloadUrl = ossStorageService.generateDownloadUrl(ossObjectKey);
|
// 只存 objectKey
|
||||||
|
|
||||||
ConvertResultItemVo item = new ConvertResultItemVo();
|
ConvertResultItemVo item = new ConvertResultItemVo();
|
||||||
item.setSourceFilename(request.getArchiveName());
|
item.setSourceFilename(request.getArchiveName());
|
||||||
item.setOutputFilename(zipFile.getName());
|
item.setOutputFilename(zipFile.getName());
|
||||||
item.setSuccess(true);
|
item.setSuccess(true);
|
||||||
item.setDownloadUrl(downloadUrl);
|
item.setDownloadUrl(ossStorageService.generateFreshDownloadUrl(ossObjectKey));
|
||||||
|
|
||||||
FileResultEntity resultEntity = new FileResultEntity();
|
FileResultEntity resultEntity = new FileResultEntity();
|
||||||
resultEntity.setTaskId(task.getId());
|
resultEntity.setTaskId(task.getId());
|
||||||
resultEntity.setModuleType(MODULE_TYPE);
|
resultEntity.setModuleType(MODULE_TYPE);
|
||||||
resultEntity.setSourceFilename(request.getArchiveName());
|
resultEntity.setSourceFilename(request.getArchiveName());
|
||||||
resultEntity.setResultFilename(zipFile.getName());
|
resultEntity.setResultFilename(zipFile.getName());
|
||||||
resultEntity.setResultFileUrl(downloadUrl);
|
resultEntity.setResultFileUrl(ossObjectKey); // 存 objectKey
|
||||||
resultEntity.setResultFileSize(zipFile.length());
|
resultEntity.setResultFileSize(zipFile.length());
|
||||||
resultEntity.setResultContentType("application/zip");
|
resultEntity.setResultContentType("application/zip");
|
||||||
resultEntity.setSuccess(1);
|
resultEntity.setSuccess(1);
|
||||||
@@ -211,7 +209,9 @@ public class ConvertRunService {
|
|||||||
vo.setResultId(entity.getId());
|
vo.setResultId(entity.getId());
|
||||||
vo.setSourceFilename(entity.getSourceFilename());
|
vo.setSourceFilename(entity.getSourceFilename());
|
||||||
vo.setOutputFilename(entity.getResultFilename());
|
vo.setOutputFilename(entity.getResultFilename());
|
||||||
vo.setDownloadUrl(entity.getSuccess() != null && entity.getSuccess() == 1 ? entity.getResultFileUrl() : null);
|
vo.setDownloadUrl(entity.getSuccess() != null && entity.getSuccess() == 1
|
||||||
|
? ossStorageService.generateFreshDownloadUrl(entity.getResultFileUrl())
|
||||||
|
: null);
|
||||||
vo.setSuccess(entity.getSuccess() != null && entity.getSuccess() == 1);
|
vo.setSuccess(entity.getSuccess() != null && entity.getSuccess() == 1);
|
||||||
vo.setError(entity.getErrorMessage());
|
vo.setError(entity.getErrorMessage());
|
||||||
return vo;
|
return vo;
|
||||||
|
|||||||
@@ -106,13 +106,12 @@ public class DedupeRunService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String ossObjectKey = ossStorageService.uploadResultFile(outputFile, "DEDUPE");
|
String ossObjectKey = ossStorageService.uploadResultFile(outputFile, "DEDUPE");
|
||||||
String downloadUrl = ossStorageService.generateDownloadUrl(ossObjectKey);
|
// 只存 objectKey,不存预签名 URL
|
||||||
|
|
||||||
String downloadFilename = request.getFiles().size() == 1 ? inputName : outputFile.getName();
|
String downloadFilename = request.getFiles().size() == 1 ? inputName : outputFile.getName();
|
||||||
|
|
||||||
item.setSuccess(true);
|
item.setSuccess(true);
|
||||||
item.setOutputFilename(downloadFilename);
|
item.setOutputFilename(downloadFilename);
|
||||||
item.setDownloadUrl(downloadUrl);
|
item.setDownloadUrl(ossStorageService.generateFreshDownloadUrl(ossObjectKey));
|
||||||
successCount++;
|
successCount++;
|
||||||
|
|
||||||
FileResultEntity resultEntity = new FileResultEntity();
|
FileResultEntity resultEntity = new FileResultEntity();
|
||||||
@@ -120,7 +119,7 @@ public class DedupeRunService {
|
|||||||
resultEntity.setModuleType("DEDUPE");
|
resultEntity.setModuleType("DEDUPE");
|
||||||
resultEntity.setSourceFilename(inputName);
|
resultEntity.setSourceFilename(inputName);
|
||||||
resultEntity.setResultFilename(downloadFilename);
|
resultEntity.setResultFilename(downloadFilename);
|
||||||
resultEntity.setResultFileUrl(downloadUrl);
|
resultEntity.setResultFileUrl(ossObjectKey); // 存 objectKey
|
||||||
resultEntity.setResultFileSize(outputFile.length());
|
resultEntity.setResultFileSize(outputFile.length());
|
||||||
resultEntity.setResultContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
resultEntity.setResultContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||||
resultEntity.setSuccess(1);
|
resultEntity.setSuccess(1);
|
||||||
@@ -148,20 +147,19 @@ public class DedupeRunService {
|
|||||||
if (folderMode && !archiveEntries.isEmpty()) {
|
if (folderMode && !archiveEntries.isEmpty()) {
|
||||||
File zipFile = packageFolderDedupeResultsAsZip(request.getArchiveName(), archiveEntries);
|
File zipFile = packageFolderDedupeResultsAsZip(request.getArchiveName(), archiveEntries);
|
||||||
String ossObjectKey = ossStorageService.uploadResultFile(zipFile, "DEDUPE");
|
String ossObjectKey = ossStorageService.uploadResultFile(zipFile, "DEDUPE");
|
||||||
String downloadUrl = ossStorageService.generateDownloadUrl(ossObjectKey);
|
// 只存 objectKey
|
||||||
|
|
||||||
DedupeResultItemVo item = new DedupeResultItemVo();
|
DedupeResultItemVo item = new DedupeResultItemVo();
|
||||||
item.setSourceFilename(request.getArchiveName());
|
item.setSourceFilename(request.getArchiveName());
|
||||||
item.setOutputFilename(zipFile.getName());
|
item.setOutputFilename(zipFile.getName());
|
||||||
item.setSuccess(true);
|
item.setSuccess(true);
|
||||||
item.setDownloadUrl(downloadUrl);
|
item.setDownloadUrl(ossStorageService.generateFreshDownloadUrl(ossObjectKey));
|
||||||
|
|
||||||
FileResultEntity resultEntity = new FileResultEntity();
|
FileResultEntity resultEntity = new FileResultEntity();
|
||||||
resultEntity.setTaskId(task.getId());
|
resultEntity.setTaskId(task.getId());
|
||||||
resultEntity.setModuleType("DEDUPE");
|
resultEntity.setModuleType("DEDUPE");
|
||||||
resultEntity.setSourceFilename(request.getArchiveName());
|
resultEntity.setSourceFilename(request.getArchiveName());
|
||||||
resultEntity.setResultFilename(zipFile.getName());
|
resultEntity.setResultFilename(zipFile.getName());
|
||||||
resultEntity.setResultFileUrl(downloadUrl);
|
resultEntity.setResultFileUrl(ossObjectKey); // 存 objectKey
|
||||||
resultEntity.setResultFileSize(zipFile.length());
|
resultEntity.setResultFileSize(zipFile.length());
|
||||||
resultEntity.setResultContentType("application/zip");
|
resultEntity.setResultContentType("application/zip");
|
||||||
resultEntity.setSuccess(1);
|
resultEntity.setSuccess(1);
|
||||||
@@ -198,7 +196,9 @@ public class DedupeRunService {
|
|||||||
vo.setResultId(entity.getId());
|
vo.setResultId(entity.getId());
|
||||||
vo.setSourceFilename(entity.getSourceFilename());
|
vo.setSourceFilename(entity.getSourceFilename());
|
||||||
vo.setOutputFilename(entity.getResultFilename());
|
vo.setOutputFilename(entity.getResultFilename());
|
||||||
vo.setDownloadUrl(entity.getSuccess() != null && entity.getSuccess() == 1 ? entity.getResultFileUrl() : null);
|
vo.setDownloadUrl(entity.getSuccess() != null && entity.getSuccess() == 1
|
||||||
|
? ossStorageService.generateFreshDownloadUrl(entity.getResultFileUrl())
|
||||||
|
: null);
|
||||||
vo.setSuccess(entity.getSuccess() != null && entity.getSuccess() == 1);
|
vo.setSuccess(entity.getSuccess() != null && entity.getSuccess() == 1);
|
||||||
vo.setError(entity.getErrorMessage());
|
vo.setError(entity.getErrorMessage());
|
||||||
return vo;
|
return vo;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.nanri.aiimage.modules.deletebrand.controller;
|
|||||||
|
|
||||||
import com.nanri.aiimage.common.api.ApiResponse;
|
import com.nanri.aiimage.common.api.ApiResponse;
|
||||||
import com.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandRunRequest;
|
import com.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandRunRequest;
|
||||||
|
import com.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandSubmitResultRequest;
|
||||||
import com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandHistoryVo;
|
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.DeleteBrandRunVo;
|
||||||
import com.nanri.aiimage.modules.deletebrand.service.DeleteBrandRunService;
|
import com.nanri.aiimage.modules.deletebrand.service.DeleteBrandRunService;
|
||||||
@@ -26,13 +27,13 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
@RestController
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@RequestMapping("/api/delete-brand")
|
@RequestMapping("/api/delete-brand")
|
||||||
@Tag(name = "删除品牌", description = "按每个上传 Excel 的文件名匹配紫鸟店铺,并解析删除品牌表格预览。")
|
@Tag(name = "删除品牌", description = "解析固定格式 Excel 的删除ASIN数据(暂不联动紫鸟)。")
|
||||||
public class DeleteBrandRunController {
|
public class DeleteBrandRunController {
|
||||||
|
|
||||||
private final DeleteBrandRunService deleteBrandRunService;
|
private final DeleteBrandRunService deleteBrandRunService;
|
||||||
|
|
||||||
@PostMapping("/run")
|
@PostMapping("/run")
|
||||||
@Operation(summary = "执行删除品牌解析", description = "读取上传的删除品牌 Excel,按每个 Excel 文件名匹配紫鸟店铺,并返回受限预览数据和紫鸟店铺打开链接。")
|
@Operation(summary = "执行删除品牌解析", description = "读取上传的删除品牌 Excel,按国家分组解析并在每个国家内按 ASIN 去重,返回完整 payload。")
|
||||||
@ApiResponses({
|
@ApiResponses({
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "执行成功", content = @Content(schema = @Schema(implementation = DeleteBrandRunVo.class)))
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "执行成功", content = @Content(schema = @Schema(implementation = DeleteBrandRunVo.class)))
|
||||||
})
|
})
|
||||||
@@ -57,4 +58,13 @@ public class DeleteBrandRunController {
|
|||||||
deleteBrandRunService.deleteHistory(resultId, userId);
|
deleteBrandRunService.deleteHistory(resultId, userId);
|
||||||
return ApiResponse.success(null);
|
return ApiResponse.success(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/tasks/{taskId}/result")
|
||||||
|
@Operation(summary = "提交删除品牌处理结果", description = "前端插件处理完成后回传结果;后端将基于 run 时缓存的原始数据重组结果文件并上传(该逻辑后续实现)。")
|
||||||
|
public ApiResponse<Void> submitResult(
|
||||||
|
@PathVariable Long taskId,
|
||||||
|
@Valid @RequestBody DeleteBrandSubmitResultRequest request) {
|
||||||
|
deleteBrandRunService.submitResult(taskId, request);
|
||||||
|
return ApiResponse.success(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.nanri.aiimage.modules.deletebrand.model.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "删除品牌插件处理后的 ASIN 结果项")
|
||||||
|
public class DeleteBrandCountryResultItemDto {
|
||||||
|
@NotBlank(message = "asin 不能为空")
|
||||||
|
@Schema(description = "删除ASIN")
|
||||||
|
private String asin;
|
||||||
|
|
||||||
|
@Schema(description = "状态")
|
||||||
|
private String status;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.nanri.aiimage.modules.deletebrand.model.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "删除品牌插件处理后的国家结果")
|
||||||
|
public class DeleteBrandProcessedCountryDto {
|
||||||
|
@NotBlank(message = "country 不能为空")
|
||||||
|
@Schema(description = "国家")
|
||||||
|
private String country;
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@Schema(description = "处理后的 ASIN 列表")
|
||||||
|
private List<DeleteBrandCountryResultItemDto> items = new ArrayList<>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.nanri.aiimage.modules.deletebrand.model.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "删除品牌单文件结果回传")
|
||||||
|
public class DeleteBrandResultFileDto {
|
||||||
|
@NotBlank(message = "sourceFilename 不能为空")
|
||||||
|
@Schema(description = "源文件名")
|
||||||
|
private String sourceFilename;
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@Schema(description = "按国家分组的处理结果")
|
||||||
|
private List<DeleteBrandProcessedCountryDto> countries = new ArrayList<>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.nanri.aiimage.modules.deletebrand.model.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "删除品牌结果提交请求")
|
||||||
|
public class DeleteBrandSubmitResultRequest {
|
||||||
|
@Valid
|
||||||
|
@NotEmpty(message = "files 不能为空")
|
||||||
|
@Schema(description = "按源文件维度提交结果")
|
||||||
|
private List<DeleteBrandResultFileDto> files = new ArrayList<>();
|
||||||
|
}
|
||||||
@@ -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 = "删除品牌国家维度 ASIN 项")
|
||||||
|
public class DeleteBrandCountryAsinVo {
|
||||||
|
@Schema(description = "行号")
|
||||||
|
private Integer rowIndex;
|
||||||
|
|
||||||
|
@Schema(description = "删除ASIN")
|
||||||
|
private String asin;
|
||||||
|
|
||||||
|
@Schema(description = "状态")
|
||||||
|
private String status;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.nanri.aiimage.modules.deletebrand.model.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "删除品牌国家分组")
|
||||||
|
public class DeleteBrandCountryGroupVo {
|
||||||
|
@Schema(description = "国家")
|
||||||
|
private String country;
|
||||||
|
|
||||||
|
@Schema(description = "去重后 ASIN 数")
|
||||||
|
private Integer asinCount;
|
||||||
|
|
||||||
|
@Schema(description = "ASIN 列表")
|
||||||
|
private List<DeleteBrandCountryAsinVo> items = new ArrayList<>();
|
||||||
|
}
|
||||||
@@ -33,6 +33,15 @@ public class DeleteBrandResultItemVo {
|
|||||||
@Schema(description = "总行数")
|
@Schema(description = "总行数")
|
||||||
private Integer totalRows;
|
private Integer totalRows;
|
||||||
|
|
||||||
|
@Schema(description = "任务ID")
|
||||||
|
private Long taskId;
|
||||||
|
|
||||||
|
@Schema(description = "国家数量")
|
||||||
|
private Integer countryCount;
|
||||||
|
|
||||||
|
@Schema(description = "按国家分组的数据")
|
||||||
|
private List<DeleteBrandCountryGroupVo> countries = new ArrayList<>();
|
||||||
|
|
||||||
@Schema(description = "是否已截断")
|
@Schema(description = "是否已截断")
|
||||||
private boolean truncated;
|
private boolean truncated;
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,15 @@ import cn.hutool.core.util.IdUtil;
|
|||||||
import cn.hutool.json.JSONUtil;
|
import cn.hutool.json.JSONUtil;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.nanri.aiimage.common.exception.BusinessException;
|
import com.nanri.aiimage.common.exception.BusinessException;
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandCountryResultItemDto;
|
||||||
|
import com.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandProcessedCountryDto;
|
||||||
|
import com.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandResultFileDto;
|
||||||
import com.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandRunRequest;
|
import com.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandRunRequest;
|
||||||
import com.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandSourceFileDto;
|
import com.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandSourceFileDto;
|
||||||
|
import com.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandSubmitResultRequest;
|
||||||
|
import com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandCountryAsinVo;
|
||||||
|
import com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandCountryGroupVo;
|
||||||
import com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandHistoryVo;
|
import com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandHistoryVo;
|
||||||
import com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandPreviewRowVo;
|
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.DeleteBrandResultItemVo;
|
||||||
@@ -16,8 +23,6 @@ import com.nanri.aiimage.modules.task.mapper.FileResultMapper;
|
|||||||
import com.nanri.aiimage.modules.task.mapper.FileTaskMapper;
|
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.cache.ZiniaoShopCacheDto;
|
|
||||||
import com.nanri.aiimage.modules.ziniao.service.ZiniaoAuthService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.poi.ss.usermodel.DataFormatter;
|
import org.apache.poi.ss.usermodel.DataFormatter;
|
||||||
import org.apache.poi.ss.usermodel.Row;
|
import org.apache.poi.ss.usermodel.Row;
|
||||||
@@ -29,20 +34,21 @@ import java.io.File;
|
|||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class DeleteBrandRunService {
|
public class DeleteBrandRunService {
|
||||||
|
|
||||||
private static final String MODULE_TYPE = "DELETE_BRAND";
|
private static final String MODULE_TYPE = "DELETE_BRAND";
|
||||||
private static final int PREVIEW_LIMIT = 200;
|
|
||||||
|
|
||||||
private final FileTaskMapper fileTaskMapper;
|
private final FileTaskMapper fileTaskMapper;
|
||||||
private final FileResultMapper fileResultMapper;
|
private final FileResultMapper fileResultMapper;
|
||||||
private final LocalFileStorageService localFileStorageService;
|
private final LocalFileStorageService localFileStorageService;
|
||||||
private final ZiniaoAuthService ziniaoAuthService;
|
private final DeleteBrandTaskCacheService deleteBrandTaskCacheService;
|
||||||
|
private final com.nanri.aiimage.modules.ziniao.service.ZiniaoAuthService ziniaoAuthService;
|
||||||
|
|
||||||
public DeleteBrandRunVo run(DeleteBrandRunRequest request) {
|
public DeleteBrandRunVo run(DeleteBrandRunRequest request) {
|
||||||
if (request.getUserId() == null || request.getUserId() <= 0) {
|
if (request.getUserId() == null || request.getUserId() <= 0) {
|
||||||
@@ -53,21 +59,22 @@ public class DeleteBrandRunService {
|
|||||||
task.setTaskNo(MODULE_TYPE + "-" + IdUtil.getSnowflakeNextIdStr());
|
task.setTaskNo(MODULE_TYPE + "-" + IdUtil.getSnowflakeNextIdStr());
|
||||||
task.setModuleType(MODULE_TYPE);
|
task.setModuleType(MODULE_TYPE);
|
||||||
task.setTaskMode("IMMEDIATE");
|
task.setTaskMode("IMMEDIATE");
|
||||||
task.setStatus("SUCCESS");
|
task.setStatus("RUNNING");
|
||||||
task.setSourceFileCount(request.getFiles().size());
|
task.setSourceFileCount(request.getFiles().size());
|
||||||
task.setCreatedBy("user:" + request.getUserId());
|
task.setCreatedBy("user:" + request.getUserId());
|
||||||
task.setUserId(request.getUserId());
|
task.setUserId(request.getUserId());
|
||||||
task.setRequestJson(JSONUtil.toJsonStr(request));
|
task.setRequestJson(JSONUtil.toJsonStr(request));
|
||||||
task.setCreatedAt(LocalDateTime.now());
|
task.setCreatedAt(LocalDateTime.now());
|
||||||
task.setUpdatedAt(LocalDateTime.now());
|
task.setUpdatedAt(LocalDateTime.now());
|
||||||
task.setFinishedAt(LocalDateTime.now());
|
|
||||||
fileTaskMapper.insert(task);
|
fileTaskMapper.insert(task);
|
||||||
|
|
||||||
List<ZiniaoShopCacheDto> shops = ziniaoAuthService.listShopsForConfiguredUser();
|
// 先不与紫鸟联动:仅解析 Excel 并返回解析 payload(shop 匹配和 openStoreUrl 先不生成)
|
||||||
List<DeleteBrandResultItemVo> items = new ArrayList<>();
|
List<DeleteBrandResultItemVo> items = new ArrayList<>();
|
||||||
int successCount = 0;
|
int successCount = 0;
|
||||||
int failedCount = 0;
|
int failedCount = 0;
|
||||||
|
|
||||||
|
Map<String, ParsedDeleteBrandFile> parsedPayloadBySourceFilename = new LinkedHashMap<>();
|
||||||
|
|
||||||
for (DeleteBrandSourceFileDto sourceFile : request.getFiles()) {
|
for (DeleteBrandSourceFileDto sourceFile : request.getFiles()) {
|
||||||
DeleteBrandResultItemVo item = new DeleteBrandResultItemVo();
|
DeleteBrandResultItemVo item = new DeleteBrandResultItemVo();
|
||||||
item.setSourceFilename(sourceFile.getOriginalFilename());
|
item.setSourceFilename(sourceFile.getOriginalFilename());
|
||||||
@@ -78,20 +85,35 @@ public class DeleteBrandRunService {
|
|||||||
throw new BusinessException("上传文件不存在,请重新上传");
|
throw new BusinessException("上传文件不存在,请重新上传");
|
||||||
}
|
}
|
||||||
ParsedDeleteBrandFile parsed = parseDeleteBrandFile(inputFile);
|
ParsedDeleteBrandFile parsed = parseDeleteBrandFile(inputFile);
|
||||||
|
item.setTaskId(task.getId());
|
||||||
|
item.setCountryCount(parsed.countries().size());
|
||||||
|
item.setCountries(parsed.countries());
|
||||||
item.setTotalRows(parsed.totalRows());
|
item.setTotalRows(parsed.totalRows());
|
||||||
item.setTruncated(parsed.truncated());
|
item.setTruncated(false);
|
||||||
item.setPreviewRows(parsed.previewRows());
|
item.setPreviewRows(parsed.previewRows());
|
||||||
|
|
||||||
ZiniaoShopCacheDto matchedShop = matchShop(item.getShopName(), shops);
|
try {
|
||||||
if (matchedShop != null) {
|
com.nanri.aiimage.modules.ziniao.service.ZiniaoAuthService.StoreMatchResult matchResult = ziniaoAuthService.matchStoreByNameAcrossStaff(item.getShopName(), null);
|
||||||
item.setMatched(true);
|
item.setMatched(matchResult.matched());
|
||||||
item.setShopId(matchedShop.getShopId());
|
if (matchResult.matched()) {
|
||||||
item.setPlatform(matchedShop.getPlatform());
|
item.setShopId(matchResult.shopId());
|
||||||
item.setOpenStoreUrl(ziniaoAuthService.buildOpenStoreUrlForShop(matchedShop));
|
item.setPlatform(matchResult.platform());
|
||||||
} else {
|
item.setOpenStoreUrl(matchResult.openStoreUrl());
|
||||||
item.setMatched(false);
|
}
|
||||||
|
} catch (BusinessException ex) {
|
||||||
|
String message = ex.getMessage();
|
||||||
|
if (message != null && (message.contains("code=40004")
|
||||||
|
|| message.contains("userId存在无效的参数值")
|
||||||
|
|| message.contains("无权限")
|
||||||
|
|| message.contains("没有权限"))) {
|
||||||
|
item.setMatched(false);
|
||||||
|
} else {
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parsedPayloadBySourceFilename.put(sourceFile.getOriginalFilename(), parsed);
|
||||||
|
|
||||||
item.setSuccess(true);
|
item.setSuccess(true);
|
||||||
successCount++;
|
successCount++;
|
||||||
|
|
||||||
@@ -103,6 +125,7 @@ public class DeleteBrandRunService {
|
|||||||
resultEntity.setResultFileUrl(null);
|
resultEntity.setResultFileUrl(null);
|
||||||
resultEntity.setResultFileSize(0L);
|
resultEntity.setResultFileSize(0L);
|
||||||
resultEntity.setResultContentType("application/json");
|
resultEntity.setResultContentType("application/json");
|
||||||
|
resultEntity.setRowCount(parsed.totalRows());
|
||||||
resultEntity.setSuccess(1);
|
resultEntity.setSuccess(1);
|
||||||
resultEntity.setUserId(request.getUserId());
|
resultEntity.setUserId(request.getUserId());
|
||||||
resultEntity.setCreatedAt(LocalDateTime.now());
|
resultEntity.setCreatedAt(LocalDateTime.now());
|
||||||
@@ -131,8 +154,15 @@ public class DeleteBrandRunService {
|
|||||||
task.setFailedFileCount(failedCount);
|
task.setFailedFileCount(failedCount);
|
||||||
task.setResultJson(JSONUtil.toJsonStr(items));
|
task.setResultJson(JSONUtil.toJsonStr(items));
|
||||||
task.setUpdatedAt(LocalDateTime.now());
|
task.setUpdatedAt(LocalDateTime.now());
|
||||||
|
task.setFinishedAt(LocalDateTime.now());
|
||||||
|
task.setStatus(failedCount == 0 ? "SUCCESS" : "FAILED");
|
||||||
fileTaskMapper.updateById(task);
|
fileTaskMapper.updateById(task);
|
||||||
|
|
||||||
|
if (!parsedPayloadBySourceFilename.isEmpty()) {
|
||||||
|
deleteBrandTaskCacheService.saveParsedPayload(task.getId(), parsedPayloadBySourceFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
DeleteBrandRunVo vo = new DeleteBrandRunVo();
|
DeleteBrandRunVo vo = new DeleteBrandRunVo();
|
||||||
vo.setTotal(request.getFiles().size());
|
vo.setTotal(request.getFiles().size());
|
||||||
vo.setSuccessCount(successCount);
|
vo.setSuccessCount(successCount);
|
||||||
@@ -185,34 +215,68 @@ public class DeleteBrandRunService {
|
|||||||
throw new BusinessException("未识别到删除品牌表头");
|
throw new BusinessException("未识别到删除品牌表头");
|
||||||
}
|
}
|
||||||
|
|
||||||
List<DeleteBrandPreviewRowVo> previewRows = new ArrayList<>();
|
Map<String, Map<String, DeleteBrandCountryAsinVo>> grouped = new LinkedHashMap<>();
|
||||||
int totalRows = 0;
|
Map<String, String> displayCountryNames = new LinkedHashMap<>();
|
||||||
boolean truncated = false;
|
|
||||||
for (int rowNum = 2; rowNum <= sheet.getLastRowNum(); rowNum++) {
|
for (int rowNum = 2; rowNum <= sheet.getLastRowNum(); rowNum++) {
|
||||||
Row row = sheet.getRow(rowNum);
|
Row row = sheet.getRow(rowNum);
|
||||||
if (row == null) {
|
if (row == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (CountryColumnPair pair : pairs) {
|
for (CountryColumnPair pair : pairs) {
|
||||||
|
String country = pair.country();
|
||||||
String asin = normalizeCellText(formatter.formatCellValue(row.getCell(pair.asinColumnIndex())));
|
String asin = normalizeCellText(formatter.formatCellValue(row.getCell(pair.asinColumnIndex())));
|
||||||
String status = normalizeCellText(formatter.formatCellValue(row.getCell(pair.statusColumnIndex())));
|
String status = normalizeCellText(formatter.formatCellValue(row.getCell(pair.statusColumnIndex())));
|
||||||
if (asin.isBlank() && status.isBlank()) {
|
if (asin.isBlank() && status.isBlank()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
totalRows++;
|
|
||||||
if (previewRows.size() < PREVIEW_LIMIT) {
|
String asinKey = normalizeAsinKey(asin);
|
||||||
DeleteBrandPreviewRowVo item = new DeleteBrandPreviewRowVo();
|
if (asinKey.isBlank()) {
|
||||||
item.setRowIndex(rowNum + 1);
|
continue;
|
||||||
item.setCountry(pair.country());
|
|
||||||
item.setAsin(asin);
|
|
||||||
item.setStatus(status);
|
|
||||||
previewRows.add(item);
|
|
||||||
} else {
|
|
||||||
truncated = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
displayCountryNames.putIfAbsent(country, country);
|
||||||
|
Map<String, DeleteBrandCountryAsinVo> countryMap = grouped.computeIfAbsent(country, ignored -> new LinkedHashMap<>());
|
||||||
|
if (countryMap.containsKey(asinKey)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
DeleteBrandCountryAsinVo item = new DeleteBrandCountryAsinVo();
|
||||||
|
item.setRowIndex(rowNum + 1);
|
||||||
|
item.setAsin(asin);
|
||||||
|
item.setStatus(status);
|
||||||
|
countryMap.put(asinKey, item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new ParsedDeleteBrandFile(totalRows, truncated, previewRows);
|
|
||||||
|
List<DeleteBrandCountryGroupVo> countries = new ArrayList<>();
|
||||||
|
List<DeleteBrandPreviewRowVo> previewRows = new ArrayList<>();
|
||||||
|
int totalRows = 0;
|
||||||
|
for (CountryColumnPair pair : pairs) {
|
||||||
|
String country = pair.country();
|
||||||
|
Map<String, DeleteBrandCountryAsinVo> countryMap = grouped.get(country);
|
||||||
|
if (countryMap == null || countryMap.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
DeleteBrandCountryGroupVo groupVo = new DeleteBrandCountryGroupVo();
|
||||||
|
groupVo.setCountry(displayCountryNames.getOrDefault(country, country));
|
||||||
|
groupVo.getItems().addAll(countryMap.values());
|
||||||
|
groupVo.setAsinCount(groupVo.getItems().size());
|
||||||
|
countries.add(groupVo);
|
||||||
|
|
||||||
|
for (DeleteBrandCountryAsinVo asinVo : groupVo.getItems()) {
|
||||||
|
DeleteBrandPreviewRowVo preview = new DeleteBrandPreviewRowVo();
|
||||||
|
preview.setRowIndex(asinVo.getRowIndex());
|
||||||
|
preview.setCountry(groupVo.getCountry());
|
||||||
|
preview.setAsin(asinVo.getAsin());
|
||||||
|
preview.setStatus(asinVo.getStatus());
|
||||||
|
previewRows.add(preview);
|
||||||
|
totalRows++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ParsedDeleteBrandFile(totalRows, countries, previewRows);
|
||||||
} catch (BusinessException ex) {
|
} catch (BusinessException ex) {
|
||||||
throw ex;
|
throw ex;
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
@@ -220,6 +284,13 @@ public class DeleteBrandRunService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String normalizeAsinKey(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return value.replace(" ", "").trim().toUpperCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
private List<CountryColumnPair> resolveCountryPairs(Row titleRow, Row headerRow, DataFormatter formatter) {
|
private List<CountryColumnPair> resolveCountryPairs(Row titleRow, Row headerRow, DataFormatter formatter) {
|
||||||
List<CountryColumnPair> pairs = new ArrayList<>();
|
List<CountryColumnPair> pairs = new ArrayList<>();
|
||||||
int lastCellNum = Math.max(titleRow.getLastCellNum(), headerRow.getLastCellNum());
|
int lastCellNum = Math.max(titleRow.getLastCellNum(), headerRow.getLastCellNum());
|
||||||
@@ -238,19 +309,6 @@ public class DeleteBrandRunService {
|
|||||||
return pairs;
|
return pairs;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ZiniaoShopCacheDto matchShop(String shopName, List<ZiniaoShopCacheDto> shops) {
|
|
||||||
if (shopName == null || shopName.isBlank() || shops == null || shops.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String normalized = normalizeShopName(shopName);
|
|
||||||
for (ZiniaoShopCacheDto shop : shops) {
|
|
||||||
if (normalized.equals(normalizeShopName(shop.getShopName()))) {
|
|
||||||
return shop;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolveShopName(DeleteBrandSourceFileDto sourceFile) {
|
private String resolveShopName(DeleteBrandSourceFileDto sourceFile) {
|
||||||
String name = sourceFile.getOriginalFilename();
|
String name = sourceFile.getOriginalFilename();
|
||||||
if (name == null || name.isBlank()) {
|
if (name == null || name.isBlank()) {
|
||||||
@@ -259,16 +317,6 @@ public class DeleteBrandRunService {
|
|||||||
return FileUtil.mainName(name).trim();
|
return FileUtil.mainName(name).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String normalizeShopName(String value) {
|
|
||||||
if (value == null) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return value.replace("\u3000", " ")
|
|
||||||
.replace(" ", "")
|
|
||||||
.trim()
|
|
||||||
.toLowerCase(Locale.ROOT);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String normalizeCellText(String value) {
|
private String normalizeCellText(String value) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return "";
|
return "";
|
||||||
@@ -286,6 +334,65 @@ public class DeleteBrandRunService {
|
|||||||
private record CountryColumnPair(String country, int asinColumnIndex, int statusColumnIndex) {
|
private record CountryColumnPair(String country, int asinColumnIndex, int statusColumnIndex) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private record ParsedDeleteBrandFile(int totalRows, boolean truncated, List<DeleteBrandPreviewRowVo> previewRows) {
|
public void submitResult(Long taskId, DeleteBrandSubmitResultRequest request) {
|
||||||
|
if (taskId == null || taskId <= 0) {
|
||||||
|
throw new BusinessException("taskId 不合法");
|
||||||
|
}
|
||||||
|
if (request == null || request.getFiles() == null || request.getFiles().isEmpty()) {
|
||||||
|
throw new BusinessException("files 不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
||||||
|
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
||||||
|
throw new BusinessException("任务不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, ParsedDeleteBrandFile> parsedPayload = deleteBrandTaskCacheService.getParsedPayload(taskId,
|
||||||
|
new TypeReference<Map<String, ParsedDeleteBrandFile>>() {
|
||||||
|
});
|
||||||
|
if (parsedPayload == null || parsedPayload.isEmpty()) {
|
||||||
|
throw new BusinessException("任务原始数据已过期,请重新执行解析");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (DeleteBrandResultFileDto fileDto : request.getFiles()) {
|
||||||
|
if (fileDto.getSourceFilename() == null || fileDto.getSourceFilename().isBlank()) {
|
||||||
|
throw new BusinessException("sourceFilename 不能为空");
|
||||||
|
}
|
||||||
|
ParsedDeleteBrandFile parsedFile = parsedPayload.get(fileDto.getSourceFilename());
|
||||||
|
if (parsedFile == null) {
|
||||||
|
throw new BusinessException("sourceFilename 不匹配: " + fileDto.getSourceFilename());
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, DeleteBrandCountryGroupVo> byCountry = new LinkedHashMap<>();
|
||||||
|
for (DeleteBrandCountryGroupVo group : parsedFile.countries()) {
|
||||||
|
byCountry.put(group.getCountry(), group);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (DeleteBrandProcessedCountryDto countryDto : fileDto.getCountries()) {
|
||||||
|
DeleteBrandCountryGroupVo group = byCountry.get(countryDto.getCountry());
|
||||||
|
if (group == null) {
|
||||||
|
throw new BusinessException("country 不匹配: " + fileDto.getSourceFilename() + " / " + countryDto.getCountry());
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, DeleteBrandCountryAsinVo> allowed = new LinkedHashMap<>();
|
||||||
|
for (DeleteBrandCountryAsinVo asinVo : group.getItems()) {
|
||||||
|
allowed.put(normalizeAsinKey(asinVo.getAsin()), asinVo);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (DeleteBrandCountryResultItemDto itemDto : countryDto.getItems()) {
|
||||||
|
String key = normalizeAsinKey(itemDto.getAsin());
|
||||||
|
if (key.isBlank() || !allowed.containsKey(key)) {
|
||||||
|
throw new BusinessException("asin 不匹配: " + fileDto.getSourceFilename() + " / " + countryDto.getCountry() + " / " + itemDto.getAsin());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BusinessException("结果回传处理尚未实现:请在此处接入 xlsx 重组与 OSS 上传逻辑");
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ParsedDeleteBrandFile(int totalRows,
|
||||||
|
List<DeleteBrandCountryGroupVo> countries,
|
||||||
|
List<DeleteBrandPreviewRowVo> previewRows) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.nanri.aiimage.modules.deletebrand.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.nanri.aiimage.common.exception.BusinessException;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DeleteBrandTaskCacheService {
|
||||||
|
|
||||||
|
private static final long PAYLOAD_TTL_HOURS = 24;
|
||||||
|
|
||||||
|
private final StringRedisTemplate stringRedisTemplate;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
public void saveParsedPayload(Long taskId, Object payload) {
|
||||||
|
try {
|
||||||
|
stringRedisTemplate.opsForValue().set(buildPayloadKey(taskId), objectMapper.writeValueAsString(payload), Duration.ofHours(PAYLOAD_TTL_HOURS));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new BusinessException("暂存删除品牌原始数据失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> T getParsedPayload(Long taskId, TypeReference<T> typeReference) {
|
||||||
|
String raw = stringRedisTemplate.opsForValue().get(buildPayloadKey(taskId));
|
||||||
|
if (raw == null || raw.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return objectMapper.readValue(raw, typeReference);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new BusinessException("读取删除品牌原始数据失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(Long taskId) {
|
||||||
|
stringRedisTemplate.delete(buildPayloadKey(taskId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildPayloadKey(Long taskId) {
|
||||||
|
return "delete-brand:task:parsed-payload:" + taskId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.net.URI;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -17,13 +18,13 @@ public class OssStorageService {
|
|||||||
|
|
||||||
private final OssProperties ossProperties;
|
private final OssProperties ossProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传结果文件到 OSS,返回 objectKey(非预签名 URL)。
|
||||||
|
* 调用方应存储 objectKey,下载时通过 generateFreshDownloadUrl 按需生成链接。
|
||||||
|
*/
|
||||||
public String uploadResultFile(File file, String moduleType) {
|
public String uploadResultFile(File file, String moduleType) {
|
||||||
String objectKey = String.format("result/%s/%s/%s", moduleType.toLowerCase(), UUID.randomUUID(), file.getName());
|
String objectKey = String.format("result/%s/%s/%s", moduleType.toLowerCase(), UUID.randomUUID(), file.getName());
|
||||||
OSS ossClient = new OSSClientBuilder().build(
|
OSS ossClient = buildClient();
|
||||||
"https://" + ossProperties.getEndpoint(),
|
|
||||||
ossProperties.getAccessKeyId(),
|
|
||||||
ossProperties.getAccessKeySecret()
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
ossClient.putObject(ossProperties.getBucket(), objectKey, file);
|
ossClient.putObject(ossProperties.getBucket(), objectKey, file);
|
||||||
return objectKey;
|
return objectKey;
|
||||||
@@ -32,12 +33,11 @@ public class OssStorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 objectKey 生成预签名下载 URL(1小时有效)。
|
||||||
|
*/
|
||||||
public String generateDownloadUrl(String objectKey) {
|
public String generateDownloadUrl(String objectKey) {
|
||||||
OSS ossClient = new OSSClientBuilder().build(
|
OSS ossClient = buildClient();
|
||||||
"https://" + ossProperties.getEndpoint(),
|
|
||||||
ossProperties.getAccessKeyId(),
|
|
||||||
ossProperties.getAccessKeySecret()
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
Date expiration = new Date(System.currentTimeMillis() + 3600_000L);
|
Date expiration = new Date(System.currentTimeMillis() + 3600_000L);
|
||||||
URL url = ossClient.generatePresignedUrl(ossProperties.getBucket(), objectKey, expiration);
|
URL url = ossClient.generatePresignedUrl(ossProperties.getBucket(), objectKey, expiration);
|
||||||
@@ -46,4 +46,52 @@ public class OssStorageService {
|
|||||||
ossClient.shutdown();
|
ossClient.shutdown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取公开(无签名)URL,格式:https://{bucket}.{endpoint}/{objectKey}
|
||||||
|
*/
|
||||||
|
public String getPublicUrl(String objectKey) {
|
||||||
|
if (objectKey == null || objectKey.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return String.format("https://%s.%s/%s", ossProperties.getBucket(), ossProperties.getEndpoint(), objectKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从存储值中解析出 objectKey,兼容两种格式:
|
||||||
|
* - 旧格式:完整预签名 URL(https://bucket.endpoint/objectKey?Expires=...)
|
||||||
|
* - 新格式:直接是 objectKey(如 result/dedupe/uuid/file.xlsx)
|
||||||
|
*/
|
||||||
|
public String resolveObjectKey(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (value.startsWith("http://") || value.startsWith("https://")) {
|
||||||
|
String path = URI.create(value).getPath();
|
||||||
|
return path.startsWith("/") ? path.substring(1) : path;
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据存储值(objectKey 或旧格式预签名 URL)生成新鲜的预签名下载 URL。
|
||||||
|
* 供各模块 listHistory 使用,每次按需生成,避免旧 URL 1小时后过期。
|
||||||
|
*/
|
||||||
|
public String generateFreshDownloadUrl(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return generateDownloadUrl(resolveObjectKey(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private OSS buildClient() {
|
||||||
|
return new OSSClientBuilder().build(
|
||||||
|
"https://" + ossProperties.getEndpoint(),
|
||||||
|
ossProperties.getAccessKeyId(),
|
||||||
|
ossProperties.getAccessKeySecret()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,8 +123,7 @@ public class SplitRunService {
|
|||||||
if (!allChunkResults.isEmpty()) {
|
if (!allChunkResults.isEmpty()) {
|
||||||
File zipFile = packageTaskSplitResultsAsZip(request.getArchiveName(), successSourceNames, allChunkResults);
|
File zipFile = packageTaskSplitResultsAsZip(request.getArchiveName(), successSourceNames, allChunkResults);
|
||||||
String ossObjectKey = ossStorageService.uploadResultFile(zipFile, MODULE_TYPE);
|
String ossObjectKey = ossStorageService.uploadResultFile(zipFile, MODULE_TYPE);
|
||||||
String downloadUrl = ossStorageService.generateDownloadUrl(ossObjectKey);
|
// 只存 objectKey
|
||||||
|
|
||||||
SplitResultItemVo item = new SplitResultItemVo();
|
SplitResultItemVo item = new SplitResultItemVo();
|
||||||
item.setSourceFilename(folderMode && request.getArchiveName() != null && !request.getArchiveName().isBlank()
|
item.setSourceFilename(folderMode && request.getArchiveName() != null && !request.getArchiveName().isBlank()
|
||||||
? request.getArchiveName()
|
? request.getArchiveName()
|
||||||
@@ -132,7 +131,7 @@ public class SplitRunService {
|
|||||||
item.setOutputFilename(zipFile.getName());
|
item.setOutputFilename(zipFile.getName());
|
||||||
item.setSuccess(true);
|
item.setSuccess(true);
|
||||||
item.setRowCount(allChunkResults.stream().mapToInt(TaskSplitChunkResult::rowCount).sum());
|
item.setRowCount(allChunkResults.stream().mapToInt(TaskSplitChunkResult::rowCount).sum());
|
||||||
item.setDownloadUrl(downloadUrl);
|
item.setDownloadUrl(ossStorageService.generateFreshDownloadUrl(ossObjectKey));
|
||||||
item.setEntryCount(allChunkResults.size());
|
item.setEntryCount(allChunkResults.size());
|
||||||
item.setEntries(allChunkResults.stream().map(this::toArchiveEntry).toList());
|
item.setEntries(allChunkResults.stream().map(this::toArchiveEntry).toList());
|
||||||
|
|
||||||
@@ -141,7 +140,7 @@ public class SplitRunService {
|
|||||||
resultEntity.setModuleType(MODULE_TYPE);
|
resultEntity.setModuleType(MODULE_TYPE);
|
||||||
resultEntity.setSourceFilename(item.getSourceFilename());
|
resultEntity.setSourceFilename(item.getSourceFilename());
|
||||||
resultEntity.setResultFilename(zipFile.getName());
|
resultEntity.setResultFilename(zipFile.getName());
|
||||||
resultEntity.setResultFileUrl(downloadUrl);
|
resultEntity.setResultFileUrl(ossObjectKey); // 存 objectKey
|
||||||
resultEntity.setResultFileSize(zipFile.length());
|
resultEntity.setResultFileSize(zipFile.length());
|
||||||
resultEntity.setResultContentType(ZIP_CONTENT_TYPE);
|
resultEntity.setResultContentType(ZIP_CONTENT_TYPE);
|
||||||
resultEntity.setRowCount(item.getRowCount());
|
resultEntity.setRowCount(item.getRowCount());
|
||||||
@@ -179,7 +178,9 @@ public class SplitRunService {
|
|||||||
vo.setResultId(entity.getId());
|
vo.setResultId(entity.getId());
|
||||||
vo.setSourceFilename(entity.getSourceFilename());
|
vo.setSourceFilename(entity.getSourceFilename());
|
||||||
vo.setOutputFilename(entity.getResultFilename());
|
vo.setOutputFilename(entity.getResultFilename());
|
||||||
vo.setDownloadUrl(entity.getSuccess() != null && entity.getSuccess() == 1 ? entity.getResultFileUrl() : null);
|
vo.setDownloadUrl(entity.getSuccess() != null && entity.getSuccess() == 1
|
||||||
|
? ossStorageService.generateFreshDownloadUrl(entity.getResultFileUrl())
|
||||||
|
: null);
|
||||||
vo.setRowCount(entity.getRowCount());
|
vo.setRowCount(entity.getRowCount());
|
||||||
vo.setSuccess(entity.getSuccess() != null && entity.getSuccess() == 1);
|
vo.setSuccess(entity.getSuccess() != null && entity.getSuccess() == 1);
|
||||||
vo.setError(entity.getErrorMessage());
|
vo.setError(entity.getErrorMessage());
|
||||||
|
|||||||
@@ -6,13 +6,11 @@ import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoStaffItemVo;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public interface ZiniaoClient {
|
public interface ZiniaoClient {
|
||||||
String getAppToken();
|
|
||||||
|
|
||||||
Long getCompanyIdByApiKey();
|
Long getCompanyIdByApiKey();
|
||||||
|
|
||||||
List<ZiniaoStaffItemVo> listStaff(Long companyId);
|
List<ZiniaoStaffItemVo> listStaff(Long companyId);
|
||||||
|
|
||||||
List<ZiniaoShopCacheDto> listUserStores(Long companyId, Long userId, String userToken);
|
List<ZiniaoShopCacheDto> listUserStores(Long companyId, Long userId);
|
||||||
|
|
||||||
String getUserLoginToken(Long companyId, Long userId);
|
String getUserLoginToken(Long companyId, Long userId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import com.nanri.aiimage.common.exception.BusinessException;
|
|||||||
import com.nanri.aiimage.config.ZiniaoProperties;
|
import com.nanri.aiimage.config.ZiniaoProperties;
|
||||||
import com.nanri.aiimage.modules.ziniao.model.cache.ZiniaoShopCacheDto;
|
import com.nanri.aiimage.modules.ziniao.model.cache.ZiniaoShopCacheDto;
|
||||||
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoStaffItemVo;
|
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoStaffItemVo;
|
||||||
import com.nanri.aiimage.modules.ziniao.service.ZiniaoSessionCacheService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.HttpStatusCode;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
@@ -24,35 +24,6 @@ public class ZiniaoClientImpl implements ZiniaoClient {
|
|||||||
|
|
||||||
private final ZiniaoProperties ziniaoProperties;
|
private final ZiniaoProperties ziniaoProperties;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final ZiniaoSessionCacheService ziniaoSessionCacheService;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getAppToken() {
|
|
||||||
String cachedToken = ziniaoSessionCacheService.getAppToken();
|
|
||||||
if (cachedToken != null && !cachedToken.isBlank()) {
|
|
||||||
return cachedToken;
|
|
||||||
}
|
|
||||||
String raw = postWithApiKeyEmptyJsonBody(ziniaoProperties.getAppTokenPath(), "获取 appToken");
|
|
||||||
try {
|
|
||||||
JsonNode root = objectMapper.readTree(raw);
|
|
||||||
JsonNode data = firstNonNull(root.get("data"), root.get("result"), root);
|
|
||||||
String token = text(firstNonNull(
|
|
||||||
data == null ? null : data.get("appAuthToken"),
|
|
||||||
data == null ? null : data.get("appToken"),
|
|
||||||
root.get("appAuthToken"),
|
|
||||||
root.get("appToken")
|
|
||||||
));
|
|
||||||
if (token == null || token.isBlank()) {
|
|
||||||
throw new BusinessException("紫鸟 appToken 响应缺少 token");
|
|
||||||
}
|
|
||||||
ziniaoSessionCacheService.saveAppToken(token);
|
|
||||||
return token;
|
|
||||||
} catch (BusinessException ex) {
|
|
||||||
throw ex;
|
|
||||||
} catch (Exception ex) {
|
|
||||||
throw new BusinessException("解析紫鸟 appToken 响应失败");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Long getCompanyIdByApiKey() {
|
public Long getCompanyIdByApiKey() {
|
||||||
@@ -104,14 +75,14 @@ public class ZiniaoClientImpl implements ZiniaoClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ZiniaoShopCacheDto> listUserStores(Long companyId, Long userId, String userToken) {
|
public List<ZiniaoShopCacheDto> listUserStores(Long companyId, Long userId) {
|
||||||
String raw = postWithAuthorization(ziniaoProperties.getUserStoresPath(), userToken, Map.of(
|
String raw = postWithApiKey(ziniaoProperties.getUserStoresPath(), Map.of(
|
||||||
"companyId", companyId,
|
"companyId", String.valueOf(companyId),
|
||||||
"userId", userId,
|
"isAccurate", "",
|
||||||
|
"limit", "100",
|
||||||
"storeName", "",
|
"storeName", "",
|
||||||
"isAccurate", 1,
|
"page", "1",
|
||||||
"page", 1,
|
"userId", String.valueOf(userId)
|
||||||
"limit", 10
|
|
||||||
), "获取员工店铺列表");
|
), "获取员工店铺列表");
|
||||||
return parseUserStores(raw);
|
return parseUserStores(raw);
|
||||||
}
|
}
|
||||||
@@ -128,7 +99,15 @@ public class ZiniaoClientImpl implements ZiniaoClient {
|
|||||||
throw new BusinessException("获取紫鸟员工登录 token 失败: " + Objects.toString(text(root.get("msg")), "未知错误"));
|
throw new BusinessException("获取紫鸟员工登录 token 失败: " + Objects.toString(text(root.get("msg")), "未知错误"));
|
||||||
}
|
}
|
||||||
JsonNode data = firstNonNull(root.get("data"), root.get("result"));
|
JsonNode data = firstNonNull(root.get("data"), root.get("result"));
|
||||||
|
JsonNode payload = firstNonNull(
|
||||||
|
data == null ? null : data.get("data"),
|
||||||
|
data == null ? null : data.get("result"),
|
||||||
|
data,
|
||||||
|
root
|
||||||
|
);
|
||||||
String token = text(firstNonNull(
|
String token = text(firstNonNull(
|
||||||
|
payload == null ? null : payload.get("token"),
|
||||||
|
payload == null ? null : payload.get("loginToken"),
|
||||||
data == null ? null : data.get("token"),
|
data == null ? null : data.get("token"),
|
||||||
data == null ? null : data.get("loginToken"),
|
data == null ? null : data.get("loginToken"),
|
||||||
root.get("token"),
|
root.get("token"),
|
||||||
@@ -152,21 +131,9 @@ public class ZiniaoClientImpl implements ZiniaoClient {
|
|||||||
.uri(joinUrl(ziniaoProperties.getBaseUrl(), path))
|
.uri(joinUrl(ziniaoProperties.getBaseUrl(), path))
|
||||||
.headers(headers -> headers.setBearerAuth(apiKey))
|
.headers(headers -> headers.setBearerAuth(apiKey))
|
||||||
.retrieve()
|
.retrieve()
|
||||||
.body(String.class);
|
.onStatus(HttpStatusCode::isError, (req, res) -> {
|
||||||
validateSuccess(raw, action, path);
|
// 保留响应体,交给后续 validateSuccess 统一解析
|
||||||
return raw;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String postWithApiKeyEmptyJsonBody(String path, String action) {
|
|
||||||
String apiKey = requireText(ziniaoProperties.getApiKey(), "紫鸟 apiKey 未配置");
|
|
||||||
String raw = getRestClient().post()
|
|
||||||
.uri(joinUrl(ziniaoProperties.getBaseUrl(), path))
|
|
||||||
.headers(headers -> {
|
|
||||||
headers.setBearerAuth(apiKey);
|
|
||||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
|
||||||
})
|
})
|
||||||
.body("")
|
|
||||||
.retrieve()
|
|
||||||
.body(String.class);
|
.body(String.class);
|
||||||
validateSuccess(raw, action, path);
|
validateSuccess(raw, action, path);
|
||||||
return raw;
|
return raw;
|
||||||
@@ -185,6 +152,9 @@ public class ZiniaoClientImpl implements ZiniaoClient {
|
|||||||
}
|
}
|
||||||
String raw = request
|
String raw = request
|
||||||
.retrieve()
|
.retrieve()
|
||||||
|
.onStatus(HttpStatusCode::isError, (req, res) -> {
|
||||||
|
// 保留响应体,交给后续 validateSuccess 统一解析 code/sub_code/sub_msg
|
||||||
|
})
|
||||||
.body(String.class);
|
.body(String.class);
|
||||||
validateSuccess(raw, action, path);
|
validateSuccess(raw, action, path);
|
||||||
return raw;
|
return raw;
|
||||||
@@ -195,7 +165,11 @@ public class ZiniaoClientImpl implements ZiniaoClient {
|
|||||||
RestClient.RequestBodySpec request = getRestClient().post()
|
RestClient.RequestBodySpec request = getRestClient().post()
|
||||||
.uri(joinUrl(ziniaoProperties.getBaseUrl(), path))
|
.uri(joinUrl(ziniaoProperties.getBaseUrl(), path))
|
||||||
.headers(headers -> {
|
.headers(headers -> {
|
||||||
headers.set("Authorization", token);
|
if (token.regionMatches(true, 0, "Bearer ", 0, 7)) {
|
||||||
|
headers.set("Authorization", token);
|
||||||
|
} else {
|
||||||
|
headers.setBearerAuth(token);
|
||||||
|
}
|
||||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
});
|
});
|
||||||
if (body != null) {
|
if (body != null) {
|
||||||
@@ -212,7 +186,29 @@ public class ZiniaoClientImpl implements ZiniaoClient {
|
|||||||
try {
|
try {
|
||||||
JsonNode root = objectMapper.readTree(raw);
|
JsonNode root = objectMapper.readTree(raw);
|
||||||
if (!success(root)) {
|
if (!success(root)) {
|
||||||
throw new BusinessException("紫鸟接口返回失败(" + action + ", " + path + "): " + Objects.toString(text(root.get("msg")), "未知错误"));
|
String code = Objects.toString(text(root.get("code")), "");
|
||||||
|
String msg = Objects.toString(text(root.get("msg")), "未知错误");
|
||||||
|
String subCode = Objects.toString(text(root.get("sub_code")), "");
|
||||||
|
String subMsg = Objects.toString(text(root.get("sub_msg")), "");
|
||||||
|
|
||||||
|
StringBuilder detail = new StringBuilder();
|
||||||
|
if (!code.isBlank()) {
|
||||||
|
detail.append("code=").append(code);
|
||||||
|
}
|
||||||
|
if (!subCode.isBlank()) {
|
||||||
|
if (!detail.isEmpty()) detail.append(", ");
|
||||||
|
detail.append("sub_code=").append(subCode);
|
||||||
|
}
|
||||||
|
if (!subMsg.isBlank()) {
|
||||||
|
if (!detail.isEmpty()) detail.append(", ");
|
||||||
|
detail.append("sub_msg=").append(subMsg);
|
||||||
|
}
|
||||||
|
if (!detail.isEmpty()) {
|
||||||
|
detail.append(", ");
|
||||||
|
}
|
||||||
|
detail.append("msg=").append(msg);
|
||||||
|
|
||||||
|
throw new BusinessException("紫鸟接口返回失败(" + action + ", " + path + "): " + detail);
|
||||||
}
|
}
|
||||||
} catch (BusinessException ex) {
|
} catch (BusinessException ex) {
|
||||||
throw ex;
|
throw ex;
|
||||||
@@ -284,7 +280,13 @@ public class ZiniaoClientImpl implements ZiniaoClient {
|
|||||||
private List<ZiniaoShopCacheDto> parseUserStores(String raw) {
|
private List<ZiniaoShopCacheDto> parseUserStores(String raw) {
|
||||||
try {
|
try {
|
||||||
JsonNode root = objectMapper.readTree(raw);
|
JsonNode root = objectMapper.readTree(raw);
|
||||||
JsonNode itemsNode = firstNonNull(root.get("data"), root.get("result"));
|
JsonNode wrapper = firstNonNull(root.get("data"), root.get("result"), root);
|
||||||
|
JsonNode itemsNode = firstNonNull(
|
||||||
|
wrapper == null ? null : wrapper.get("data"),
|
||||||
|
wrapper == null ? null : wrapper.get("items"),
|
||||||
|
wrapper
|
||||||
|
);
|
||||||
|
|
||||||
List<ZiniaoShopCacheDto> items = new ArrayList<>();
|
List<ZiniaoShopCacheDto> items = new ArrayList<>();
|
||||||
if (itemsNode != null && itemsNode.isArray()) {
|
if (itemsNode != null && itemsNode.isArray()) {
|
||||||
for (JsonNode itemNode : itemsNode) {
|
for (JsonNode itemNode : itemsNode) {
|
||||||
|
|||||||
@@ -26,6 +26,75 @@ import java.util.UUID;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ZiniaoAuthService {
|
public class ZiniaoAuthService {
|
||||||
|
|
||||||
|
public StoreMatchResult matchStoreByNameAcrossStaff(String targetShopName, Long preferUserId) {
|
||||||
|
ensureEnabled();
|
||||||
|
String normalizedTarget = normalizeShopName(targetShopName);
|
||||||
|
if (normalizedTarget.isBlank()) {
|
||||||
|
return new StoreMatchResult(false, null, null, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Long companyId = resolveCompanyId();
|
||||||
|
List<ZiniaoStaffItemVo> staff = ziniaoClient.listStaff(companyId);
|
||||||
|
|
||||||
|
List<Long> userIds = new java.util.ArrayList<>();
|
||||||
|
if (preferUserId != null && preferUserId > 0) {
|
||||||
|
userIds.add(preferUserId);
|
||||||
|
}
|
||||||
|
for (ZiniaoStaffItemVo item : staff) {
|
||||||
|
if (item != null && item.getUserId() != null && item.getUserId() > 0 && (userIds.isEmpty() || !userIds.contains(item.getUserId()))) {
|
||||||
|
userIds.add(item.getUserId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Long staffUserId : userIds) {
|
||||||
|
if (staffUserId == null || staffUserId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<ZiniaoShopCacheDto> stores;
|
||||||
|
try {
|
||||||
|
stores = ziniaoClient.listUserStores(companyId, staffUserId);
|
||||||
|
} catch (BusinessException ex) {
|
||||||
|
if (isSkippableUserStoresError(ex)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ZiniaoShopCacheDto store : stores) {
|
||||||
|
String storeName = normalizeShopName(store == null ? null : store.getShopName());
|
||||||
|
if (!storeName.isBlank() && storeName.equals(normalizedTarget)) {
|
||||||
|
String openStoreUrl = buildOpenStoreUrl(store.getShopId(), staffUserId, ziniaoClient.getUserLoginToken(companyId, staffUserId));
|
||||||
|
return new StoreMatchResult(true, store.getShopId(), store.getShopName(), store.getPlatform(), staffUserId, openStoreUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StoreMatchResult(false, null, null, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isSkippableUserStoresError(BusinessException ex) {
|
||||||
|
String message = ex == null ? null : ex.getMessage();
|
||||||
|
if (message == null || message.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return message.contains("code=40004")
|
||||||
|
|| message.contains("userId存在无效的参数值")
|
||||||
|
|| message.contains("无权限")
|
||||||
|
|| message.contains("没有权限");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeShopName(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return value.replace("\u3000", " ")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record StoreMatchResult(boolean matched, String shopId, String shopName, String platform, Long matchedUserId,
|
||||||
|
String openStoreUrl) {
|
||||||
|
}
|
||||||
|
|
||||||
private final ZiniaoProperties ziniaoProperties;
|
private final ZiniaoProperties ziniaoProperties;
|
||||||
private final ZiniaoClient ziniaoClient;
|
private final ZiniaoClient ziniaoClient;
|
||||||
private final ZiniaoSessionCacheService ziniaoSessionCacheService;
|
private final ZiniaoSessionCacheService ziniaoSessionCacheService;
|
||||||
@@ -36,13 +105,13 @@ public class ZiniaoAuthService {
|
|||||||
ZiniaoSessionVo vo = new ZiniaoSessionVo();
|
ZiniaoSessionVo vo = new ZiniaoSessionVo();
|
||||||
vo.setSessionId(session.getSessionId());
|
vo.setSessionId(session.getSessionId());
|
||||||
vo.setEnabled(ziniaoProperties.isEnabled());
|
vo.setEnabled(ziniaoProperties.isEnabled());
|
||||||
vo.setAuthenticated(session.getAccessToken() != null && !session.getAccessToken().isBlank());
|
vo.setAuthenticated(Boolean.TRUE);
|
||||||
vo.setCompanyId(session.getCompanyId());
|
vo.setCompanyId(session.getCompanyId());
|
||||||
vo.setCurrentUserId(session.getCurrentUserId());
|
vo.setCurrentUserId(session.getCurrentUserId());
|
||||||
vo.setZiniaoUserId(session.getZiniaoUserId());
|
vo.setZiniaoUserId(session.getZiniaoUserId());
|
||||||
vo.setNickname(session.getNickname());
|
vo.setNickname(session.getNickname());
|
||||||
vo.setAppToken(session.getAccessToken());
|
vo.setAppToken(null);
|
||||||
vo.setTokenType(session.getTokenType());
|
vo.setTokenType(null);
|
||||||
vo.setExpireAt(session.getExpireAt());
|
vo.setExpireAt(session.getExpireAt());
|
||||||
vo.setShopCount(ziniaoSessionCacheService.getShops(session.getSessionId()).size());
|
vo.setShopCount(ziniaoSessionCacheService.getShops(session.getSessionId()).size());
|
||||||
vo.setCurrentShopId(session.getDefaultShopId());
|
vo.setCurrentShopId(session.getDefaultShopId());
|
||||||
@@ -61,8 +130,7 @@ public class ZiniaoAuthService {
|
|||||||
public ZiniaoShopListVo listShops(String sessionId, Long userId) {
|
public ZiniaoShopListVo listShops(String sessionId, Long userId) {
|
||||||
ZiniaoSessionCacheDto session = requireOrInitSession(sessionId, userId);
|
ZiniaoSessionCacheDto session = requireOrInitSession(sessionId, userId);
|
||||||
Long currentUserId = requireUserId(session.getCurrentUserId());
|
Long currentUserId = requireUserId(session.getCurrentUserId());
|
||||||
String userToken = ziniaoClient.getUserLoginToken(resolveCompanyId(), currentUserId);
|
List<ZiniaoShopCacheDto> shops = ziniaoClient.listUserStores(resolveCompanyId(), currentUserId);
|
||||||
List<ZiniaoShopCacheDto> shops = ziniaoClient.listUserStores(resolveCompanyId(), currentUserId, userToken);
|
|
||||||
ziniaoSessionCacheService.saveShops(session.getSessionId(), shops);
|
ziniaoSessionCacheService.saveShops(session.getSessionId(), shops);
|
||||||
if (!shops.isEmpty() && (session.getDefaultShopId() == null || session.getDefaultShopId().isBlank())) {
|
if (!shops.isEmpty() && (session.getDefaultShopId() == null || session.getDefaultShopId().isBlank())) {
|
||||||
session.setDefaultShopId(shops.get(0).getShopId());
|
session.setDefaultShopId(shops.get(0).getShopId());
|
||||||
@@ -86,7 +154,7 @@ public class ZiniaoAuthService {
|
|||||||
String userToken = ziniaoClient.getUserLoginToken(resolveCompanyId(), currentUserId);
|
String userToken = ziniaoClient.getUserLoginToken(resolveCompanyId(), currentUserId);
|
||||||
List<ZiniaoShopCacheDto> shops = ziniaoSessionCacheService.getShops(session.getSessionId());
|
List<ZiniaoShopCacheDto> shops = ziniaoSessionCacheService.getShops(session.getSessionId());
|
||||||
if (shops.isEmpty()) {
|
if (shops.isEmpty()) {
|
||||||
shops = ziniaoClient.listUserStores(resolveCompanyId(), currentUserId, userToken);
|
shops = ziniaoClient.listUserStores(resolveCompanyId(), currentUserId);
|
||||||
ziniaoSessionCacheService.saveShops(session.getSessionId(), shops);
|
ziniaoSessionCacheService.saveShops(session.getSessionId(), shops);
|
||||||
}
|
}
|
||||||
ZiniaoShopCacheDto targetShop = shops.stream()
|
ZiniaoShopCacheDto targetShop = shops.stream()
|
||||||
@@ -110,8 +178,7 @@ public class ZiniaoAuthService {
|
|||||||
public List<ZiniaoShopCacheDto> listShopsForConfiguredUser() {
|
public List<ZiniaoShopCacheDto> listShopsForConfiguredUser() {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
Long userId = parseConfiguredOpenStoreUserId();
|
Long userId = parseConfiguredOpenStoreUserId();
|
||||||
String userToken = ziniaoClient.getUserLoginToken(resolveCompanyId(), userId);
|
return ziniaoClient.listUserStores(resolveCompanyId(), userId);
|
||||||
return ziniaoClient.listUserStores(resolveCompanyId(), userId, userToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String buildOpenStoreUrlForShop(ZiniaoShopCacheDto shop) {
|
public String buildOpenStoreUrlForShop(ZiniaoShopCacheDto shop) {
|
||||||
@@ -128,9 +195,6 @@ public class ZiniaoAuthService {
|
|||||||
String sessionId = "ziniao_" + UUID.randomUUID().toString().replace("-", "");
|
String sessionId = "ziniao_" + UUID.randomUUID().toString().replace("-", "");
|
||||||
ZiniaoSessionCacheDto session = new ZiniaoSessionCacheDto();
|
ZiniaoSessionCacheDto session = new ZiniaoSessionCacheDto();
|
||||||
session.setSessionId(sessionId);
|
session.setSessionId(sessionId);
|
||||||
String appToken = ziniaoClient.getAppToken();
|
|
||||||
session.setAccessToken(maskToken(appToken));
|
|
||||||
session.setTokenType("Bearer");
|
|
||||||
session.setExpireAt(Instant.now().plusSeconds(ziniaoProperties.getSessionTtlHours() * 3600).toEpochMilli());
|
session.setExpireAt(Instant.now().plusSeconds(ziniaoProperties.getSessionTtlHours() * 3600).toEpochMilli());
|
||||||
session.setCompanyId(resolveCompanyId());
|
session.setCompanyId(resolveCompanyId());
|
||||||
session.setCurrentUserId(userId);
|
session.setCurrentUserId(userId);
|
||||||
@@ -186,7 +250,7 @@ public class ZiniaoAuthService {
|
|||||||
if (ziniaoProperties.getOpenStoreExtraArgs() != null && !ziniaoProperties.getOpenStoreExtraArgs().isBlank()) {
|
if (ziniaoProperties.getOpenStoreExtraArgs() != null && !ziniaoProperties.getOpenStoreExtraArgs().isBlank()) {
|
||||||
builder.queryParam("extraArgs", ziniaoProperties.getOpenStoreExtraArgs());
|
builder.queryParam("extraArgs", ziniaoProperties.getOpenStoreExtraArgs());
|
||||||
}
|
}
|
||||||
return builder.build(true).toUriString();
|
return builder.build().encode().toUriString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ensureEnabled() {
|
private void ensureEnabled() {
|
||||||
|
|||||||
@@ -17,27 +17,10 @@ import java.util.List;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ZiniaoSessionCacheService {
|
public class ZiniaoSessionCacheService {
|
||||||
|
|
||||||
private static final String APP_TOKEN_KEY = "ziniao:app-token";
|
|
||||||
|
|
||||||
private final StringRedisTemplate stringRedisTemplate;
|
private final StringRedisTemplate stringRedisTemplate;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final ZiniaoProperties ziniaoProperties;
|
private final ZiniaoProperties ziniaoProperties;
|
||||||
|
|
||||||
public void saveAppToken(String appToken) {
|
|
||||||
if (appToken == null || appToken.isBlank()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
stringRedisTemplate.opsForValue().set(APP_TOKEN_KEY, appToken.trim(), Duration.ofHours(ziniaoProperties.getSessionTtlHours()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAppToken() {
|
|
||||||
String value = stringRedisTemplate.opsForValue().get(APP_TOKEN_KEY);
|
|
||||||
if (value == null || value.isBlank()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return value.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void saveSession(ZiniaoSessionCacheDto session) {
|
public void saveSession(ZiniaoSessionCacheDto session) {
|
||||||
try {
|
try {
|
||||||
stringRedisTemplate.opsForValue().set(buildSessionKey(session.getSessionId()), objectMapper.writeValueAsString(session), Duration.ofHours(ziniaoProperties.getSessionTtlHours()));
|
stringRedisTemplate.opsForValue().set(buildSessionKey(session.getSessionId()), objectMapper.writeValueAsString(session), Duration.ofHours(ziniaoProperties.getSessionTtlHours()));
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<aside class="left-panel">
|
<aside class="left-panel">
|
||||||
<div class="section-title">选择文件</div>
|
<div class="section-title">选择文件</div>
|
||||||
<div class="upload-zone">
|
<div class="upload-zone">
|
||||||
<div class="hint">上传删除品牌 Excel;无论是直接上传文件还是上传文件夹批量导入,都会按每个 Excel 文件名匹配紫鸟店铺。</div>
|
<div class="hint">上传删除品牌 Excel;无论是直接上传文件还是上传文件夹批量导入,都会按每个 Excel 文件名作为店铺名。</div>
|
||||||
<div class="btns">
|
<div class="btns">
|
||||||
<button type="button" class="opt-btn" @click="selectFiles">选择 Excel 文件</button>
|
<button type="button" class="opt-btn" @click="selectFiles">选择 Excel 文件</button>
|
||||||
<button type="button" class="opt-btn" @click="selectFolder">选择文件夹</button>
|
<button type="button" class="opt-btn" @click="selectFolder">选择文件夹</button>
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<input type="checkbox" checked disabled />
|
<input type="checkbox" checked disabled />
|
||||||
<div>
|
<div>
|
||||||
<span class="label">文件名作为店铺名</span>
|
<span class="label">文件名作为店铺名</span>
|
||||||
<div class="desc">例如 鲍丽明.xlsx 会匹配紫鸟店铺 鲍丽明;选择文件夹时也仍按每个 Excel 文件名匹配</div>
|
<div class="desc">例如 鲍丽明.xlsx 会解析为店铺名 鲍丽明;当前先不联动紫鸟</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
@@ -44,8 +44,8 @@
|
|||||||
<label class="radio-item active">
|
<label class="radio-item active">
|
||||||
<input type="checkbox" checked disabled />
|
<input type="checkbox" checked disabled />
|
||||||
<div>
|
<div>
|
||||||
<span class="label">大表仅返回预览</span>
|
<span class="label">按国家分组去重</span>
|
||||||
<div class="desc">当前最多展示前 200 行,避免大文件直接拖慢页面</div>
|
<div class="desc">解析后按国家返回,并在每个国家内按 ASIN 去重(保留首次出现的状态)</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
{{ running ? '解析中...' : '开始解析' }}
|
{{ running ? '解析中...' : '开始解析' }}
|
||||||
</button>
|
</button>
|
||||||
<span class="loading-msg">
|
<span class="loading-msg">
|
||||||
{{ running ? '正在读取删除品牌数据并匹配紫鸟店铺,请稍候…' : '解析后会在右侧展示预览数据和紫鸟店铺打开链接' }}
|
{{ running ? '正在读取删除品牌数据,请稍候…' : '解析后会在右侧展示按国家分组的去重结果' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
@@ -84,13 +84,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="resultItems.length === 0" class="empty-tasks">
|
<div v-if="resultItems.length === 0" class="empty-tasks">
|
||||||
暂无删除品牌结果,完成解析后会在这里展示店铺匹配结果和预览数据
|
暂无删除品牌结果,完成解析后会在这里展示按国家分组的去重结果
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul v-else class="task-list clean-result-list">
|
<ul v-else class="task-list clean-result-list">
|
||||||
<li
|
<li
|
||||||
v-for="item in resultItems"
|
v-for="item in resultItems"
|
||||||
:key="`${item.resultId || item.sourceFilename}-${item.shopId || ''}`"
|
:key="`${item.resultId || item.taskId || item.sourceFilename}`"
|
||||||
class="task-item split-result-item"
|
class="task-item split-result-item"
|
||||||
>
|
>
|
||||||
<div class="left split-result-main">
|
<div class="left split-result-main">
|
||||||
@@ -98,8 +98,8 @@
|
|||||||
<div class="files">店铺名:{{ item.shopName || '-' }}</div>
|
<div class="files">店铺名:{{ item.shopName || '-' }}</div>
|
||||||
<div class="files">匹配结果:{{ item.matched ? `已匹配 ${item.shopId || ''}` : '未匹配到紫鸟店铺' }}</div>
|
<div class="files">匹配结果:{{ item.matched ? `已匹配 ${item.shopId || ''}` : '未匹配到紫鸟店铺' }}</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.totalRows !== undefined" class="time">预览 {{ item.previewRows?.length || 0 }} / {{ item.totalRows }} 行</div>
|
<div v-if="item.countryCount !== undefined" class="files">国家数:{{ item.countryCount }}</div>
|
||||||
<div v-if="item.truncated" class="files">数据较大,当前仅展示前 200 行</div>
|
<div v-if="item.totalRows !== undefined" class="time">去重后 {{ item.totalRows }} 条</div>
|
||||||
<div v-if="item.previewRows?.length" class="files split-entry-list delete-brand-preview">
|
<div v-if="item.previewRows?.length" class="files split-entry-list delete-brand-preview">
|
||||||
{{ formatPreview(item.previewRows) }}
|
{{ formatPreview(item.previewRows) }}
|
||||||
</div>
|
</div>
|
||||||
@@ -116,7 +116,7 @@
|
|||||||
class="download"
|
class="download"
|
||||||
@click="openShop(item.openStoreUrl)"
|
@click="openShop(item.openStoreUrl)"
|
||||||
>
|
>
|
||||||
打开店铺
|
打开紫鸟
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="item.resultId"
|
v-if="item.resultId"
|
||||||
@@ -241,7 +241,6 @@ async function submitRun() {
|
|||||||
})
|
})
|
||||||
summary.value = result
|
summary.value = result
|
||||||
resultItems.value = result.items || []
|
resultItems.value = result.items || []
|
||||||
await loadHistory()
|
|
||||||
ElMessage.success('删除品牌解析完成')
|
ElMessage.success('删除品牌解析完成')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(error instanceof Error ? error.message : '执行失败')
|
ElMessage.error(error instanceof Error ? error.message : '执行失败')
|
||||||
|
|||||||
@@ -127,6 +127,37 @@ export interface DeleteBrandPreviewRow {
|
|||||||
status: string
|
status: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DeleteBrandCountryAsinItem {
|
||||||
|
rowIndex: number
|
||||||
|
asin: string
|
||||||
|
status: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteBrandCountryGroup {
|
||||||
|
country: string
|
||||||
|
asinCount: number
|
||||||
|
items: DeleteBrandCountryAsinItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteBrandCountryResultItem {
|
||||||
|
asin: string
|
||||||
|
status?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteBrandProcessedCountry {
|
||||||
|
country: string
|
||||||
|
items: DeleteBrandCountryResultItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteBrandResultFile {
|
||||||
|
sourceFilename: string
|
||||||
|
countries: DeleteBrandProcessedCountry[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteBrandSubmitResultRequest {
|
||||||
|
files: DeleteBrandResultFile[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface DeleteBrandResultItem {
|
export interface DeleteBrandResultItem {
|
||||||
resultId?: number
|
resultId?: number
|
||||||
sourceFilename: string
|
sourceFilename: string
|
||||||
@@ -135,6 +166,9 @@ export interface DeleteBrandResultItem {
|
|||||||
shopId?: string
|
shopId?: string
|
||||||
platform?: string
|
platform?: string
|
||||||
openStoreUrl?: string
|
openStoreUrl?: string
|
||||||
|
taskId?: number
|
||||||
|
countryCount?: number
|
||||||
|
countries?: DeleteBrandCountryGroup[]
|
||||||
totalRows?: number
|
totalRows?: number
|
||||||
truncated?: boolean
|
truncated?: boolean
|
||||||
previewRows?: DeleteBrandPreviewRow[]
|
previewRows?: DeleteBrandPreviewRow[]
|
||||||
@@ -277,6 +311,10 @@ export function deleteDeleteBrandHistory(resultId: number) {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function submitDeleteBrandResult(taskId: number, request: DeleteBrandSubmitResultRequest) {
|
||||||
|
return unwrapJavaResponse(post<JavaApiResponse<null>, DeleteBrandSubmitResultRequest>(`${JAVA_API_PREFIX}/delete-brand/tasks/${taskId}/result`, request))
|
||||||
|
}
|
||||||
|
|
||||||
export function getJavaDownloadUrl(path: string) {
|
export function getJavaDownloadUrl(path: string) {
|
||||||
const raw = path.startsWith('http://') || path.startsWith('https://') ? path : `${JAVA_API_PREFIX}${path}`
|
const raw = path.startsWith('http://') || path.startsWith('https://') ? path : `${JAVA_API_PREFIX}${path}`
|
||||||
const separator = raw.includes('?') ? '&' : '?'
|
const separator = raw.includes('?') ? '&' : '?'
|
||||||
|
|||||||
Reference in New Issue
Block a user