删除品牌
This commit is contained in:
@@ -16,6 +16,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
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.RestController;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/api/brand")
|
||||
@@ -191,17 +197,34 @@ public class BrandTaskController {
|
||||
@GetMapping("/tasks/{taskId}/download")
|
||||
@Operation(
|
||||
summary = "下载品牌任务结果",
|
||||
description = "根据任务 ID 读取 brand_crawl_tasks.result_paths,优先跳转 zip_url;若没有 zip_url,则跳转第一个可用结果 URL。"
|
||||
description = "根据任务 ID 读取 brand_crawl_tasks.result_paths,后端代理拉取 OSS 文件并直接返回给浏览器,避免跨域问题。"
|
||||
)
|
||||
@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 = "任务不存在或无结果可下载")
|
||||
})
|
||||
public ResponseEntity<Void> download(
|
||||
@Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId) {
|
||||
String url = brandTaskService.resolveDownloadUrl(taskId);
|
||||
return ResponseEntity.status(302)
|
||||
.header(HttpHeaders.LOCATION, url)
|
||||
.build();
|
||||
public void download(
|
||||
@Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId,
|
||||
HttpServletResponse response) throws Exception {
|
||||
String ossUrl = brandTaskService.resolveDownloadUrl(taskId);
|
||||
// 从 OSS URL 路径中取文件名
|
||||
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);
|
||||
File outputFile = buildNamedOutputFile(outputDir, buildResultFilename(originalFilename));
|
||||
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);
|
||||
@@ -338,18 +342,24 @@ public class BrandTaskService {
|
||||
if (!(raw instanceof Map<?, ?> map)) {
|
||||
throw new BusinessException("无结果可下载");
|
||||
}
|
||||
String stored = null;
|
||||
Object zipUrl = map.get("zip_url");
|
||||
if (zipUrl instanceof String zip && !zip.isBlank()) {
|
||||
return zip;
|
||||
}
|
||||
Object urls = map.get("urls");
|
||||
if (urls instanceof List<?> list && !list.isEmpty()) {
|
||||
Object first = list.get(0);
|
||||
if (first instanceof String url && !url.isBlank()) {
|
||||
return url;
|
||||
stored = zip;
|
||||
} else {
|
||||
Object urls = map.get("urls");
|
||||
if (urls instanceof List<?> list && !list.isEmpty()) {
|
||||
Object first = list.get(0);
|
||||
if (first instanceof String url && !url.isBlank()) {
|
||||
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) {
|
||||
@@ -852,28 +862,29 @@ public class BrandTaskService {
|
||||
if (entries.isEmpty()) {
|
||||
throw new BusinessException("没有可上传的结果文件");
|
||||
}
|
||||
List<String> urls = new ArrayList<>();
|
||||
List<String> fullUrls = new ArrayList<>();
|
||||
for (OutputEntry entry : entries) {
|
||||
// 存储完整公开地址,下载时会自动通过 resolveObjectKey 提取并重新签名(见 OssStorageService.resolveObjectKey)
|
||||
String objectKey = ossStorageService.uploadResultFile(entry.resultFile(), "BRAND");
|
||||
urls.add(ossStorageService.generateDownloadUrl(objectKey));
|
||||
fullUrls.add(ossStorageService.getPublicUrl(objectKey));
|
||||
}
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("urls", urls);
|
||||
result.put("urls", fullUrls);
|
||||
File zipFile = packageAsZip(taskId, entries);
|
||||
String zipObjectKey = ossStorageService.uploadResultFile(zipFile, "BRAND");
|
||||
result.put("zip_url", ossStorageService.generateDownloadUrl(zipObjectKey));
|
||||
result.put("zip_url", ossStorageService.getPublicUrl(zipObjectKey));
|
||||
return result;
|
||||
}
|
||||
|
||||
private File packageAsZip(Long taskId, List<OutputEntry> entries) throws IOException {
|
||||
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<>();
|
||||
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
|
||||
byte[] buffer = new byte[8192];
|
||||
for (OutputEntry entry : entries) {
|
||||
writeZipEntry(zos, buffer, entry.sourceFile(), buildSourceZipEntry(entry.sourceFilename()), zipEntryNames);
|
||||
writeZipEntry(zos, buffer, entry.resultFile(), buildResultZipEntry(entry.resultFilename()), zipEntryNames);
|
||||
writeZipEntry(zos, buffer, entry.sourceFile(), buildSourceZipEntry(entry.zipSourceFilename()), zipEntryNames);
|
||||
writeZipEntry(zos, buffer, entry.resultFile(), buildResultZipEntry(entry.zipResultFilename()), zipEntryNames);
|
||||
}
|
||||
}
|
||||
return zipFile;
|
||||
@@ -933,8 +944,15 @@ public class BrandTaskService {
|
||||
}
|
||||
|
||||
private String buildResultFilename(String originalFilename) {
|
||||
String sourceName = blankToDefault(originalFilename, "brand.xlsx");
|
||||
return FileUtil.mainName(sourceName) + "_result.xlsx";
|
||||
return blankToDefault(originalFilename, "brand.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) {
|
||||
@@ -1062,6 +1080,6 @@ public class BrandTaskService {
|
||||
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 ossObjectKey = ossStorageService.uploadResultFile(packagedResultFile, MODULE_TYPE);
|
||||
String downloadUrl = ossStorageService.generateDownloadUrl(ossObjectKey);
|
||||
|
||||
// 只存 objectKey
|
||||
ConvertResultItemVo item = new ConvertResultItemVo();
|
||||
item.setSourceFilename(inputName);
|
||||
item.setSuccess(true);
|
||||
item.setOutputFilename(packagedFilename);
|
||||
item.setDownloadUrl(downloadUrl);
|
||||
item.setDownloadUrl(ossStorageService.generateFreshDownloadUrl(ossObjectKey));
|
||||
|
||||
FileResultEntity resultEntity = new FileResultEntity();
|
||||
resultEntity.setTaskId(task.getId());
|
||||
resultEntity.setModuleType(MODULE_TYPE);
|
||||
resultEntity.setSourceFilename(inputName);
|
||||
resultEntity.setResultFilename(packagedFilename);
|
||||
resultEntity.setResultFileUrl(downloadUrl);
|
||||
resultEntity.setResultFileUrl(ossObjectKey); // 存 objectKey
|
||||
resultEntity.setResultFileSize(packagedResultFile.length());
|
||||
resultEntity.setResultContentType(contentType);
|
||||
resultEntity.setSuccess(1);
|
||||
@@ -161,20 +160,19 @@ public class ConvertRunService {
|
||||
if (folderMode && !archiveEntries.isEmpty()) {
|
||||
File zipFile = packageFolderConvertResultsAsZip(request.getArchiveName(), archiveEntries);
|
||||
String ossObjectKey = ossStorageService.uploadResultFile(zipFile, MODULE_TYPE);
|
||||
String downloadUrl = ossStorageService.generateDownloadUrl(ossObjectKey);
|
||||
|
||||
// 只存 objectKey
|
||||
ConvertResultItemVo item = new ConvertResultItemVo();
|
||||
item.setSourceFilename(request.getArchiveName());
|
||||
item.setOutputFilename(zipFile.getName());
|
||||
item.setSuccess(true);
|
||||
item.setDownloadUrl(downloadUrl);
|
||||
item.setDownloadUrl(ossStorageService.generateFreshDownloadUrl(ossObjectKey));
|
||||
|
||||
FileResultEntity resultEntity = new FileResultEntity();
|
||||
resultEntity.setTaskId(task.getId());
|
||||
resultEntity.setModuleType(MODULE_TYPE);
|
||||
resultEntity.setSourceFilename(request.getArchiveName());
|
||||
resultEntity.setResultFilename(zipFile.getName());
|
||||
resultEntity.setResultFileUrl(downloadUrl);
|
||||
resultEntity.setResultFileUrl(ossObjectKey); // 存 objectKey
|
||||
resultEntity.setResultFileSize(zipFile.length());
|
||||
resultEntity.setResultContentType("application/zip");
|
||||
resultEntity.setSuccess(1);
|
||||
@@ -211,7 +209,9 @@ public class ConvertRunService {
|
||||
vo.setResultId(entity.getId());
|
||||
vo.setSourceFilename(entity.getSourceFilename());
|
||||
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.setError(entity.getErrorMessage());
|
||||
return vo;
|
||||
|
||||
@@ -106,13 +106,12 @@ public class DedupeRunService {
|
||||
}
|
||||
|
||||
String ossObjectKey = ossStorageService.uploadResultFile(outputFile, "DEDUPE");
|
||||
String downloadUrl = ossStorageService.generateDownloadUrl(ossObjectKey);
|
||||
|
||||
// 只存 objectKey,不存预签名 URL
|
||||
String downloadFilename = request.getFiles().size() == 1 ? inputName : outputFile.getName();
|
||||
|
||||
item.setSuccess(true);
|
||||
item.setOutputFilename(downloadFilename);
|
||||
item.setDownloadUrl(downloadUrl);
|
||||
item.setDownloadUrl(ossStorageService.generateFreshDownloadUrl(ossObjectKey));
|
||||
successCount++;
|
||||
|
||||
FileResultEntity resultEntity = new FileResultEntity();
|
||||
@@ -120,7 +119,7 @@ public class DedupeRunService {
|
||||
resultEntity.setModuleType("DEDUPE");
|
||||
resultEntity.setSourceFilename(inputName);
|
||||
resultEntity.setResultFilename(downloadFilename);
|
||||
resultEntity.setResultFileUrl(downloadUrl);
|
||||
resultEntity.setResultFileUrl(ossObjectKey); // 存 objectKey
|
||||
resultEntity.setResultFileSize(outputFile.length());
|
||||
resultEntity.setResultContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
resultEntity.setSuccess(1);
|
||||
@@ -148,20 +147,19 @@ public class DedupeRunService {
|
||||
if (folderMode && !archiveEntries.isEmpty()) {
|
||||
File zipFile = packageFolderDedupeResultsAsZip(request.getArchiveName(), archiveEntries);
|
||||
String ossObjectKey = ossStorageService.uploadResultFile(zipFile, "DEDUPE");
|
||||
String downloadUrl = ossStorageService.generateDownloadUrl(ossObjectKey);
|
||||
|
||||
// 只存 objectKey
|
||||
DedupeResultItemVo item = new DedupeResultItemVo();
|
||||
item.setSourceFilename(request.getArchiveName());
|
||||
item.setOutputFilename(zipFile.getName());
|
||||
item.setSuccess(true);
|
||||
item.setDownloadUrl(downloadUrl);
|
||||
item.setDownloadUrl(ossStorageService.generateFreshDownloadUrl(ossObjectKey));
|
||||
|
||||
FileResultEntity resultEntity = new FileResultEntity();
|
||||
resultEntity.setTaskId(task.getId());
|
||||
resultEntity.setModuleType("DEDUPE");
|
||||
resultEntity.setSourceFilename(request.getArchiveName());
|
||||
resultEntity.setResultFilename(zipFile.getName());
|
||||
resultEntity.setResultFileUrl(downloadUrl);
|
||||
resultEntity.setResultFileUrl(ossObjectKey); // 存 objectKey
|
||||
resultEntity.setResultFileSize(zipFile.length());
|
||||
resultEntity.setResultContentType("application/zip");
|
||||
resultEntity.setSuccess(1);
|
||||
@@ -198,7 +196,9 @@ public class DedupeRunService {
|
||||
vo.setResultId(entity.getId());
|
||||
vo.setSourceFilename(entity.getSourceFilename());
|
||||
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.setError(entity.getErrorMessage());
|
||||
return vo;
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.nanri.aiimage.modules.deletebrand.controller;
|
||||
|
||||
import com.nanri.aiimage.common.api.ApiResponse;
|
||||
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.DeleteBrandRunVo;
|
||||
import com.nanri.aiimage.modules.deletebrand.service.DeleteBrandRunService;
|
||||
@@ -26,13 +27,13 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/api/delete-brand")
|
||||
@Tag(name = "删除品牌", description = "按每个上传 Excel 的文件名匹配紫鸟店铺,并解析删除品牌表格预览。")
|
||||
@Tag(name = "删除品牌", description = "解析固定格式 Excel 的删除ASIN数据(暂不联动紫鸟)。")
|
||||
public class DeleteBrandRunController {
|
||||
|
||||
private final DeleteBrandRunService deleteBrandRunService;
|
||||
|
||||
@PostMapping("/run")
|
||||
@Operation(summary = "执行删除品牌解析", description = "读取上传的删除品牌 Excel,按每个 Excel 文件名匹配紫鸟店铺,并返回受限预览数据和紫鸟店铺打开链接。")
|
||||
@Operation(summary = "执行删除品牌解析", description = "读取上传的删除品牌 Excel,按国家分组解析并在每个国家内按 ASIN 去重,返回完整 payload。")
|
||||
@ApiResponses({
|
||||
@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);
|
||||
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 = "总行数")
|
||||
private Integer totalRows;
|
||||
|
||||
@Schema(description = "任务ID")
|
||||
private Long taskId;
|
||||
|
||||
@Schema(description = "国家数量")
|
||||
private Integer countryCount;
|
||||
|
||||
@Schema(description = "按国家分组的数据")
|
||||
private List<DeleteBrandCountryGroupVo> countries = new ArrayList<>();
|
||||
|
||||
@Schema(description = "是否已截断")
|
||||
private boolean truncated;
|
||||
|
||||
|
||||
@@ -5,8 +5,15 @@ import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
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.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.DeleteBrandPreviewRowVo;
|
||||
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.model.entity.FileResultEntity;
|
||||
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 org.apache.poi.ss.usermodel.DataFormatter;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
@@ -29,20 +34,21 @@ import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DeleteBrandRunService {
|
||||
|
||||
private static final String MODULE_TYPE = "DELETE_BRAND";
|
||||
private static final int PREVIEW_LIMIT = 200;
|
||||
|
||||
private final FileTaskMapper fileTaskMapper;
|
||||
private final FileResultMapper fileResultMapper;
|
||||
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) {
|
||||
if (request.getUserId() == null || request.getUserId() <= 0) {
|
||||
@@ -53,21 +59,22 @@ public class DeleteBrandRunService {
|
||||
task.setTaskNo(MODULE_TYPE + "-" + IdUtil.getSnowflakeNextIdStr());
|
||||
task.setModuleType(MODULE_TYPE);
|
||||
task.setTaskMode("IMMEDIATE");
|
||||
task.setStatus("SUCCESS");
|
||||
task.setStatus("RUNNING");
|
||||
task.setSourceFileCount(request.getFiles().size());
|
||||
task.setCreatedBy("user:" + request.getUserId());
|
||||
task.setUserId(request.getUserId());
|
||||
task.setRequestJson(JSONUtil.toJsonStr(request));
|
||||
task.setCreatedAt(LocalDateTime.now());
|
||||
task.setUpdatedAt(LocalDateTime.now());
|
||||
task.setFinishedAt(LocalDateTime.now());
|
||||
fileTaskMapper.insert(task);
|
||||
|
||||
List<ZiniaoShopCacheDto> shops = ziniaoAuthService.listShopsForConfiguredUser();
|
||||
// 先不与紫鸟联动:仅解析 Excel 并返回解析 payload(shop 匹配和 openStoreUrl 先不生成)
|
||||
List<DeleteBrandResultItemVo> items = new ArrayList<>();
|
||||
int successCount = 0;
|
||||
int failedCount = 0;
|
||||
|
||||
Map<String, ParsedDeleteBrandFile> parsedPayloadBySourceFilename = new LinkedHashMap<>();
|
||||
|
||||
for (DeleteBrandSourceFileDto sourceFile : request.getFiles()) {
|
||||
DeleteBrandResultItemVo item = new DeleteBrandResultItemVo();
|
||||
item.setSourceFilename(sourceFile.getOriginalFilename());
|
||||
@@ -78,20 +85,35 @@ public class DeleteBrandRunService {
|
||||
throw new BusinessException("上传文件不存在,请重新上传");
|
||||
}
|
||||
ParsedDeleteBrandFile parsed = parseDeleteBrandFile(inputFile);
|
||||
item.setTaskId(task.getId());
|
||||
item.setCountryCount(parsed.countries().size());
|
||||
item.setCountries(parsed.countries());
|
||||
item.setTotalRows(parsed.totalRows());
|
||||
item.setTruncated(parsed.truncated());
|
||||
item.setTruncated(false);
|
||||
item.setPreviewRows(parsed.previewRows());
|
||||
|
||||
ZiniaoShopCacheDto matchedShop = matchShop(item.getShopName(), shops);
|
||||
if (matchedShop != null) {
|
||||
item.setMatched(true);
|
||||
item.setShopId(matchedShop.getShopId());
|
||||
item.setPlatform(matchedShop.getPlatform());
|
||||
item.setOpenStoreUrl(ziniaoAuthService.buildOpenStoreUrlForShop(matchedShop));
|
||||
} else {
|
||||
item.setMatched(false);
|
||||
try {
|
||||
com.nanri.aiimage.modules.ziniao.service.ZiniaoAuthService.StoreMatchResult matchResult = ziniaoAuthService.matchStoreByNameAcrossStaff(item.getShopName(), null);
|
||||
item.setMatched(matchResult.matched());
|
||||
if (matchResult.matched()) {
|
||||
item.setShopId(matchResult.shopId());
|
||||
item.setPlatform(matchResult.platform());
|
||||
item.setOpenStoreUrl(matchResult.openStoreUrl());
|
||||
}
|
||||
} 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);
|
||||
successCount++;
|
||||
|
||||
@@ -103,6 +125,7 @@ public class DeleteBrandRunService {
|
||||
resultEntity.setResultFileUrl(null);
|
||||
resultEntity.setResultFileSize(0L);
|
||||
resultEntity.setResultContentType("application/json");
|
||||
resultEntity.setRowCount(parsed.totalRows());
|
||||
resultEntity.setSuccess(1);
|
||||
resultEntity.setUserId(request.getUserId());
|
||||
resultEntity.setCreatedAt(LocalDateTime.now());
|
||||
@@ -131,8 +154,15 @@ public class DeleteBrandRunService {
|
||||
task.setFailedFileCount(failedCount);
|
||||
task.setResultJson(JSONUtil.toJsonStr(items));
|
||||
task.setUpdatedAt(LocalDateTime.now());
|
||||
task.setFinishedAt(LocalDateTime.now());
|
||||
task.setStatus(failedCount == 0 ? "SUCCESS" : "FAILED");
|
||||
fileTaskMapper.updateById(task);
|
||||
|
||||
if (!parsedPayloadBySourceFilename.isEmpty()) {
|
||||
deleteBrandTaskCacheService.saveParsedPayload(task.getId(), parsedPayloadBySourceFilename);
|
||||
}
|
||||
|
||||
|
||||
DeleteBrandRunVo vo = new DeleteBrandRunVo();
|
||||
vo.setTotal(request.getFiles().size());
|
||||
vo.setSuccessCount(successCount);
|
||||
@@ -185,34 +215,68 @@ public class DeleteBrandRunService {
|
||||
throw new BusinessException("未识别到删除品牌表头");
|
||||
}
|
||||
|
||||
List<DeleteBrandPreviewRowVo> previewRows = new ArrayList<>();
|
||||
int totalRows = 0;
|
||||
boolean truncated = false;
|
||||
Map<String, Map<String, DeleteBrandCountryAsinVo>> grouped = new LinkedHashMap<>();
|
||||
Map<String, String> displayCountryNames = new LinkedHashMap<>();
|
||||
|
||||
for (int rowNum = 2; rowNum <= sheet.getLastRowNum(); rowNum++) {
|
||||
Row row = sheet.getRow(rowNum);
|
||||
if (row == null) {
|
||||
continue;
|
||||
}
|
||||
for (CountryColumnPair pair : pairs) {
|
||||
String country = pair.country();
|
||||
String asin = normalizeCellText(formatter.formatCellValue(row.getCell(pair.asinColumnIndex())));
|
||||
String status = normalizeCellText(formatter.formatCellValue(row.getCell(pair.statusColumnIndex())));
|
||||
if (asin.isBlank() && status.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
totalRows++;
|
||||
if (previewRows.size() < PREVIEW_LIMIT) {
|
||||
DeleteBrandPreviewRowVo item = new DeleteBrandPreviewRowVo();
|
||||
item.setRowIndex(rowNum + 1);
|
||||
item.setCountry(pair.country());
|
||||
item.setAsin(asin);
|
||||
item.setStatus(status);
|
||||
previewRows.add(item);
|
||||
} else {
|
||||
truncated = true;
|
||||
|
||||
String asinKey = normalizeAsinKey(asin);
|
||||
if (asinKey.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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) {
|
||||
throw 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) {
|
||||
List<CountryColumnPair> pairs = new ArrayList<>();
|
||||
int lastCellNum = Math.max(titleRow.getLastCellNum(), headerRow.getLastCellNum());
|
||||
@@ -238,19 +309,6 @@ public class DeleteBrandRunService {
|
||||
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) {
|
||||
String name = sourceFile.getOriginalFilename();
|
||||
if (name == null || name.isBlank()) {
|
||||
@@ -259,16 +317,6 @@ public class DeleteBrandRunService {
|
||||
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) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
@@ -286,6 +334,65 @@ public class DeleteBrandRunService {
|
||||
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 java.io.File;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
@@ -17,13 +18,13 @@ public class OssStorageService {
|
||||
|
||||
private final OssProperties ossProperties;
|
||||
|
||||
/**
|
||||
* 上传结果文件到 OSS,返回 objectKey(非预签名 URL)。
|
||||
* 调用方应存储 objectKey,下载时通过 generateFreshDownloadUrl 按需生成链接。
|
||||
*/
|
||||
public String uploadResultFile(File file, String moduleType) {
|
||||
String objectKey = String.format("result/%s/%s/%s", moduleType.toLowerCase(), UUID.randomUUID(), file.getName());
|
||||
OSS ossClient = new OSSClientBuilder().build(
|
||||
"https://" + ossProperties.getEndpoint(),
|
||||
ossProperties.getAccessKeyId(),
|
||||
ossProperties.getAccessKeySecret()
|
||||
);
|
||||
OSS ossClient = buildClient();
|
||||
try {
|
||||
ossClient.putObject(ossProperties.getBucket(), objectKey, file);
|
||||
return objectKey;
|
||||
@@ -32,12 +33,11 @@ public class OssStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 objectKey 生成预签名下载 URL(1小时有效)。
|
||||
*/
|
||||
public String generateDownloadUrl(String objectKey) {
|
||||
OSS ossClient = new OSSClientBuilder().build(
|
||||
"https://" + ossProperties.getEndpoint(),
|
||||
ossProperties.getAccessKeyId(),
|
||||
ossProperties.getAccessKeySecret()
|
||||
);
|
||||
OSS ossClient = buildClient();
|
||||
try {
|
||||
Date expiration = new Date(System.currentTimeMillis() + 3600_000L);
|
||||
URL url = ossClient.generatePresignedUrl(ossProperties.getBucket(), objectKey, expiration);
|
||||
@@ -46,4 +46,52 @@ public class OssStorageService {
|
||||
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()) {
|
||||
File zipFile = packageTaskSplitResultsAsZip(request.getArchiveName(), successSourceNames, allChunkResults);
|
||||
String ossObjectKey = ossStorageService.uploadResultFile(zipFile, MODULE_TYPE);
|
||||
String downloadUrl = ossStorageService.generateDownloadUrl(ossObjectKey);
|
||||
|
||||
// 只存 objectKey
|
||||
SplitResultItemVo item = new SplitResultItemVo();
|
||||
item.setSourceFilename(folderMode && request.getArchiveName() != null && !request.getArchiveName().isBlank()
|
||||
? request.getArchiveName()
|
||||
@@ -132,7 +131,7 @@ public class SplitRunService {
|
||||
item.setOutputFilename(zipFile.getName());
|
||||
item.setSuccess(true);
|
||||
item.setRowCount(allChunkResults.stream().mapToInt(TaskSplitChunkResult::rowCount).sum());
|
||||
item.setDownloadUrl(downloadUrl);
|
||||
item.setDownloadUrl(ossStorageService.generateFreshDownloadUrl(ossObjectKey));
|
||||
item.setEntryCount(allChunkResults.size());
|
||||
item.setEntries(allChunkResults.stream().map(this::toArchiveEntry).toList());
|
||||
|
||||
@@ -141,7 +140,7 @@ public class SplitRunService {
|
||||
resultEntity.setModuleType(MODULE_TYPE);
|
||||
resultEntity.setSourceFilename(item.getSourceFilename());
|
||||
resultEntity.setResultFilename(zipFile.getName());
|
||||
resultEntity.setResultFileUrl(downloadUrl);
|
||||
resultEntity.setResultFileUrl(ossObjectKey); // 存 objectKey
|
||||
resultEntity.setResultFileSize(zipFile.length());
|
||||
resultEntity.setResultContentType(ZIP_CONTENT_TYPE);
|
||||
resultEntity.setRowCount(item.getRowCount());
|
||||
@@ -179,7 +178,9 @@ public class SplitRunService {
|
||||
vo.setResultId(entity.getId());
|
||||
vo.setSourceFilename(entity.getSourceFilename());
|
||||
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.setSuccess(entity.getSuccess() != null && entity.getSuccess() == 1);
|
||||
vo.setError(entity.getErrorMessage());
|
||||
|
||||
@@ -6,13 +6,11 @@ import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoStaffItemVo;
|
||||
import java.util.List;
|
||||
|
||||
public interface ZiniaoClient {
|
||||
String getAppToken();
|
||||
|
||||
Long getCompanyIdByApiKey();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import com.nanri.aiimage.common.exception.BusinessException;
|
||||
import com.nanri.aiimage.config.ZiniaoProperties;
|
||||
import com.nanri.aiimage.modules.ziniao.model.cache.ZiniaoShopCacheDto;
|
||||
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoStaffItemVo;
|
||||
import com.nanri.aiimage.modules.ziniao.service.ZiniaoSessionCacheService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -24,35 +24,6 @@ public class ZiniaoClientImpl implements ZiniaoClient {
|
||||
|
||||
private final ZiniaoProperties ziniaoProperties;
|
||||
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
|
||||
public Long getCompanyIdByApiKey() {
|
||||
@@ -104,14 +75,14 @@ public class ZiniaoClientImpl implements ZiniaoClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ZiniaoShopCacheDto> listUserStores(Long companyId, Long userId, String userToken) {
|
||||
String raw = postWithAuthorization(ziniaoProperties.getUserStoresPath(), userToken, Map.of(
|
||||
"companyId", companyId,
|
||||
"userId", userId,
|
||||
public List<ZiniaoShopCacheDto> listUserStores(Long companyId, Long userId) {
|
||||
String raw = postWithApiKey(ziniaoProperties.getUserStoresPath(), Map.of(
|
||||
"companyId", String.valueOf(companyId),
|
||||
"isAccurate", "",
|
||||
"limit", "100",
|
||||
"storeName", "",
|
||||
"isAccurate", 1,
|
||||
"page", 1,
|
||||
"limit", 10
|
||||
"page", "1",
|
||||
"userId", String.valueOf(userId)
|
||||
), "获取员工店铺列表");
|
||||
return parseUserStores(raw);
|
||||
}
|
||||
@@ -128,7 +99,15 @@ public class ZiniaoClientImpl implements ZiniaoClient {
|
||||
throw new BusinessException("获取紫鸟员工登录 token 失败: " + Objects.toString(text(root.get("msg")), "未知错误"));
|
||||
}
|
||||
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(
|
||||
payload == null ? null : payload.get("token"),
|
||||
payload == null ? null : payload.get("loginToken"),
|
||||
data == null ? null : data.get("token"),
|
||||
data == null ? null : data.get("loginToken"),
|
||||
root.get("token"),
|
||||
@@ -152,21 +131,9 @@ public class ZiniaoClientImpl implements ZiniaoClient {
|
||||
.uri(joinUrl(ziniaoProperties.getBaseUrl(), path))
|
||||
.headers(headers -> headers.setBearerAuth(apiKey))
|
||||
.retrieve()
|
||||
.body(String.class);
|
||||
validateSuccess(raw, action, path);
|
||||
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);
|
||||
.onStatus(HttpStatusCode::isError, (req, res) -> {
|
||||
// 保留响应体,交给后续 validateSuccess 统一解析
|
||||
})
|
||||
.body("")
|
||||
.retrieve()
|
||||
.body(String.class);
|
||||
validateSuccess(raw, action, path);
|
||||
return raw;
|
||||
@@ -185,6 +152,9 @@ public class ZiniaoClientImpl implements ZiniaoClient {
|
||||
}
|
||||
String raw = request
|
||||
.retrieve()
|
||||
.onStatus(HttpStatusCode::isError, (req, res) -> {
|
||||
// 保留响应体,交给后续 validateSuccess 统一解析 code/sub_code/sub_msg
|
||||
})
|
||||
.body(String.class);
|
||||
validateSuccess(raw, action, path);
|
||||
return raw;
|
||||
@@ -195,7 +165,11 @@ public class ZiniaoClientImpl implements ZiniaoClient {
|
||||
RestClient.RequestBodySpec request = getRestClient().post()
|
||||
.uri(joinUrl(ziniaoProperties.getBaseUrl(), path))
|
||||
.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);
|
||||
});
|
||||
if (body != null) {
|
||||
@@ -212,7 +186,29 @@ public class ZiniaoClientImpl implements ZiniaoClient {
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(raw);
|
||||
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) {
|
||||
throw ex;
|
||||
@@ -284,7 +280,13 @@ public class ZiniaoClientImpl implements ZiniaoClient {
|
||||
private List<ZiniaoShopCacheDto> parseUserStores(String raw) {
|
||||
try {
|
||||
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<>();
|
||||
if (itemsNode != null && itemsNode.isArray()) {
|
||||
for (JsonNode itemNode : itemsNode) {
|
||||
|
||||
@@ -26,6 +26,75 @@ import java.util.UUID;
|
||||
@RequiredArgsConstructor
|
||||
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 ZiniaoClient ziniaoClient;
|
||||
private final ZiniaoSessionCacheService ziniaoSessionCacheService;
|
||||
@@ -36,13 +105,13 @@ public class ZiniaoAuthService {
|
||||
ZiniaoSessionVo vo = new ZiniaoSessionVo();
|
||||
vo.setSessionId(session.getSessionId());
|
||||
vo.setEnabled(ziniaoProperties.isEnabled());
|
||||
vo.setAuthenticated(session.getAccessToken() != null && !session.getAccessToken().isBlank());
|
||||
vo.setAuthenticated(Boolean.TRUE);
|
||||
vo.setCompanyId(session.getCompanyId());
|
||||
vo.setCurrentUserId(session.getCurrentUserId());
|
||||
vo.setZiniaoUserId(session.getZiniaoUserId());
|
||||
vo.setNickname(session.getNickname());
|
||||
vo.setAppToken(session.getAccessToken());
|
||||
vo.setTokenType(session.getTokenType());
|
||||
vo.setAppToken(null);
|
||||
vo.setTokenType(null);
|
||||
vo.setExpireAt(session.getExpireAt());
|
||||
vo.setShopCount(ziniaoSessionCacheService.getShops(session.getSessionId()).size());
|
||||
vo.setCurrentShopId(session.getDefaultShopId());
|
||||
@@ -61,8 +130,7 @@ public class ZiniaoAuthService {
|
||||
public ZiniaoShopListVo listShops(String sessionId, Long userId) {
|
||||
ZiniaoSessionCacheDto session = requireOrInitSession(sessionId, userId);
|
||||
Long currentUserId = requireUserId(session.getCurrentUserId());
|
||||
String userToken = ziniaoClient.getUserLoginToken(resolveCompanyId(), currentUserId);
|
||||
List<ZiniaoShopCacheDto> shops = ziniaoClient.listUserStores(resolveCompanyId(), currentUserId, userToken);
|
||||
List<ZiniaoShopCacheDto> shops = ziniaoClient.listUserStores(resolveCompanyId(), currentUserId);
|
||||
ziniaoSessionCacheService.saveShops(session.getSessionId(), shops);
|
||||
if (!shops.isEmpty() && (session.getDefaultShopId() == null || session.getDefaultShopId().isBlank())) {
|
||||
session.setDefaultShopId(shops.get(0).getShopId());
|
||||
@@ -86,7 +154,7 @@ public class ZiniaoAuthService {
|
||||
String userToken = ziniaoClient.getUserLoginToken(resolveCompanyId(), currentUserId);
|
||||
List<ZiniaoShopCacheDto> shops = ziniaoSessionCacheService.getShops(session.getSessionId());
|
||||
if (shops.isEmpty()) {
|
||||
shops = ziniaoClient.listUserStores(resolveCompanyId(), currentUserId, userToken);
|
||||
shops = ziniaoClient.listUserStores(resolveCompanyId(), currentUserId);
|
||||
ziniaoSessionCacheService.saveShops(session.getSessionId(), shops);
|
||||
}
|
||||
ZiniaoShopCacheDto targetShop = shops.stream()
|
||||
@@ -110,8 +178,7 @@ public class ZiniaoAuthService {
|
||||
public List<ZiniaoShopCacheDto> listShopsForConfiguredUser() {
|
||||
ensureEnabled();
|
||||
Long userId = parseConfiguredOpenStoreUserId();
|
||||
String userToken = ziniaoClient.getUserLoginToken(resolveCompanyId(), userId);
|
||||
return ziniaoClient.listUserStores(resolveCompanyId(), userId, userToken);
|
||||
return ziniaoClient.listUserStores(resolveCompanyId(), userId);
|
||||
}
|
||||
|
||||
public String buildOpenStoreUrlForShop(ZiniaoShopCacheDto shop) {
|
||||
@@ -128,9 +195,6 @@ public class ZiniaoAuthService {
|
||||
String sessionId = "ziniao_" + UUID.randomUUID().toString().replace("-", "");
|
||||
ZiniaoSessionCacheDto session = new ZiniaoSessionCacheDto();
|
||||
session.setSessionId(sessionId);
|
||||
String appToken = ziniaoClient.getAppToken();
|
||||
session.setAccessToken(maskToken(appToken));
|
||||
session.setTokenType("Bearer");
|
||||
session.setExpireAt(Instant.now().plusSeconds(ziniaoProperties.getSessionTtlHours() * 3600).toEpochMilli());
|
||||
session.setCompanyId(resolveCompanyId());
|
||||
session.setCurrentUserId(userId);
|
||||
@@ -186,7 +250,7 @@ public class ZiniaoAuthService {
|
||||
if (ziniaoProperties.getOpenStoreExtraArgs() != null && !ziniaoProperties.getOpenStoreExtraArgs().isBlank()) {
|
||||
builder.queryParam("extraArgs", ziniaoProperties.getOpenStoreExtraArgs());
|
||||
}
|
||||
return builder.build(true).toUriString();
|
||||
return builder.build().encode().toUriString();
|
||||
}
|
||||
|
||||
private void ensureEnabled() {
|
||||
|
||||
@@ -17,27 +17,10 @@ import java.util.List;
|
||||
@RequiredArgsConstructor
|
||||
public class ZiniaoSessionCacheService {
|
||||
|
||||
private static final String APP_TOKEN_KEY = "ziniao:app-token";
|
||||
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
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) {
|
||||
try {
|
||||
stringRedisTemplate.opsForValue().set(buildSessionKey(session.getSessionId()), objectMapper.writeValueAsString(session), Duration.ofHours(ziniaoProperties.getSessionTtlHours()));
|
||||
|
||||
Reference in New Issue
Block a user