diff --git a/.gitignore b/.gitignore index d865f33..0c8527a 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ backend-java/*.iml # desktop app desktop/ +ERP-Demo/ diff --git a/backend-java/sdk-java-5.0.6.jar b/backend-java/sdk-java-5.0.6.jar new file mode 100644 index 0000000..6af6a20 Binary files /dev/null and b/backend-java/sdk-java-5.0.6.jar differ diff --git a/backend-java/src/main/java/com/nanri/aiimage/config/ModuleCleanupProperties.java b/backend-java/src/main/java/com/nanri/aiimage/config/ModuleCleanupProperties.java new file mode 100644 index 0000000..ae09b7c --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/config/ModuleCleanupProperties.java @@ -0,0 +1,15 @@ +package com.nanri.aiimage.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.ArrayList; +import java.util.List; + +@Data +@ConfigurationProperties(prefix = "aiimage.module-cleanup") +public class ModuleCleanupProperties { + private boolean enabled = true; + private String cron = "0 0 0 * * *"; + private List moduleTypes = new ArrayList<>(List.of("DEDUPE", "SPLIT", "CONVERT", "DELETE_BRAND")); +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/config/PropertiesConfig.java b/backend-java/src/main/java/com/nanri/aiimage/config/PropertiesConfig.java index f6c596d..f2de092 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/config/PropertiesConfig.java +++ b/backend-java/src/main/java/com/nanri/aiimage/config/PropertiesConfig.java @@ -4,6 +4,6 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Configuration; @Configuration -@EnableConfigurationProperties({OssProperties.class, StorageProperties.class, BrandProgressProperties.class, ZiniaoProperties.class}) +@EnableConfigurationProperties({OssProperties.class, StorageProperties.class, BrandProgressProperties.class, ZiniaoProperties.class, ModuleCleanupProperties.class}) public class PropertiesConfig { } diff --git a/backend-java/src/main/java/com/nanri/aiimage/config/ZiniaoProperties.java b/backend-java/src/main/java/com/nanri/aiimage/config/ZiniaoProperties.java index 4f54a38..9243bf3 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/config/ZiniaoProperties.java +++ b/backend-java/src/main/java/com/nanri/aiimage/config/ZiniaoProperties.java @@ -9,9 +9,13 @@ public class ZiniaoProperties { private boolean enabled; private String baseUrl; private String apiKey; + private String appId; + private String appSecret; private Long companyId; - private String shopsPath; - private String switchShopPath; + private String appTokenPath; + private String staffListPath; + private String userStoresPath; + private String userLoginTokenPath; private String openStoreScheme; private String openStoreUserId; private String openStoreLaunchUrl; diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/brand/controller/BrandTaskController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/brand/controller/BrandTaskController.java index b4ad774..6639ea9 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/brand/controller/BrandTaskController.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/brand/controller/BrandTaskController.java @@ -124,8 +124,9 @@ public class BrandTaskController { @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "任务不存在") }) public ApiResponse getTask( - @Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId) { - return ApiResponse.success(brandTaskService.getTaskDetailLegacy(taskId)); + @Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId, + @Parameter(description = "用户 ID", required = true) @RequestParam Long userId) { + return ApiResponse.success(brandTaskService.getTaskDetailLegacy(taskId, userId)); } @PostMapping("/tasks/{taskId}/result") @@ -153,8 +154,9 @@ public class BrandTaskController { }) public ApiResponse submitResult( @Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId, + @Parameter(description = "用户 ID", required = true) @RequestParam Long userId, @Valid @RequestBody BrandCrawlResultRequest request) { - brandTaskService.submitCrawlResult(taskId, request); + brandTaskService.submitCrawlResult(taskId, userId, request); return ApiResponse.success(new BrandSimpleVo(true)); } @@ -168,8 +170,9 @@ public class BrandTaskController { @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "任务不存在或当前状态不可取消") }) public ApiResponse cancelTask( - @Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId) { - brandTaskService.cancelTask(taskId); + @Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId, + @Parameter(description = "用户 ID", required = true) @RequestParam Long userId) { + brandTaskService.cancelTask(taskId, userId); return ApiResponse.success(new BrandSimpleVo(true)); } @@ -183,8 +186,9 @@ public class BrandTaskController { @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "任务不存在或正在执行中不可删除") }) public ApiResponse deleteTask( - @Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId) { - brandTaskService.deleteTask(taskId); + @Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId, + @Parameter(description = "用户 ID", required = true) @RequestParam Long userId) { + brandTaskService.deleteTask(taskId, userId); return ApiResponse.success(new BrandSimpleVo(true)); } @@ -198,8 +202,9 @@ public class BrandTaskController { @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "任务不存在或无结果可下载") }) public ResponseEntity download( - @Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId) { - String url = brandTaskService.resolveDownloadUrl(taskId); + @Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId, + @Parameter(description = "用户 ID", required = true) @RequestParam Long userId) { + String url = brandTaskService.resolveDownloadUrl(taskId, userId); return ResponseEntity.status(302) .header(HttpHeaders.LOCATION, url) .build(); diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/brand/service/BrandTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/brand/service/BrandTaskService.java index db1f4ff..1ab7e99 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/brand/service/BrandTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/brand/service/BrandTaskService.java @@ -133,8 +133,14 @@ public class BrandTaskService { return vo; } - public LegacyBrandTaskDetailVo getTaskDetailLegacy(Long taskId) { - BrandCrawlTaskEntity task = requireTask(taskId); + public BrandTaskDetailVo getTaskDetail(Long taskId, Long userId) { + BrandTaskDetailVo vo = new BrandTaskDetailVo(); + vo.setTask(toTaskItem(requireTask(taskId, userId))); + return vo; + } + + public LegacyBrandTaskDetailVo getTaskDetailLegacy(Long taskId, Long userId) { + BrandCrawlTaskEntity task = requireTask(taskId, userId); LegacyBrandTaskDetailVo vo = new LegacyBrandTaskDetailVo(); vo.setTask(toLegacyTaskItem(task)); vo.setLine_progress(buildLineProgress(taskId)); @@ -198,8 +204,8 @@ public class BrandTaskService { return vo; } - public void submitCrawlResult(Long taskId, BrandCrawlResultRequest request) { - BrandCrawlTaskEntity task = requireTask(taskId); + public void submitCrawlResult(Long taskId, Long userId, BrandCrawlResultRequest request) { + BrandCrawlTaskEntity task = requireTask(taskId, userId); if (STATUS_CANCELLED.equalsIgnoreCase(blankToDefault(task.getStatus(), STATUS_PENDING))) { throw new BusinessException("任务已取消"); } @@ -304,9 +310,11 @@ public class BrandTaskService { } } - public void cancelTask(Long taskId) { + public void cancelTask(Long taskId, Long userId) { + requireTask(taskId, userId); int updated = brandCrawlTaskMapper.update(null, new LambdaUpdateWrapper() .eq(BrandCrawlTaskEntity::getId, taskId) + .eq(BrandCrawlTaskEntity::getUserId, userId) .in(BrandCrawlTaskEntity::getStatus, STATUS_PENDING, STATUS_RUNNING) .set(BrandCrawlTaskEntity::getStatus, STATUS_CANCELLED)); if (updated == 0) { @@ -315,17 +323,19 @@ public class BrandTaskService { brandTaskProgressCacheService.delete(taskId); } - public void deleteTask(Long taskId) { + public void deleteTask(Long taskId, Long userId) { + requireTask(taskId, userId); int deleted = brandCrawlTaskMapper.delete(new LambdaQueryWrapper() .eq(BrandCrawlTaskEntity::getId, taskId) + .eq(BrandCrawlTaskEntity::getUserId, userId) .ne(BrandCrawlTaskEntity::getStatus, STATUS_RUNNING)); if (deleted == 0) { throw new BusinessException("任务不存在或正在执行中无法删除"); } } - public String resolveDownloadUrl(Long taskId) { - BrandCrawlTaskEntity task = requireTask(taskId); + public String resolveDownloadUrl(Long taskId, Long userId) { + BrandCrawlTaskEntity task = requireTask(taskId, userId); Object raw = parseJsonValue(task.getResultPaths()); if (!(raw instanceof Map map)) { throw new BusinessException("无结果可下载"); @@ -600,6 +610,17 @@ public class BrandTaskService { return entity; } + private BrandCrawlTaskEntity requireTask(Long taskId, Long userId) { + if (userId == null || userId <= 0) { + throw new BusinessException("userId 不合法"); + } + BrandCrawlTaskEntity entity = brandCrawlTaskMapper.selectById(taskId); + if (entity == null || !userId.equals(entity.getUserId())) { + throw new BusinessException("任务不存在"); + } + return entity; + } + private File resolveSourceFile(BrandSourceFileDto sourceFile) { String fileUrl = sourceFile.getFileUrl(); if (fileUrl == null || fileUrl.isBlank()) { @@ -661,11 +682,15 @@ public class BrandTaskService { String value = index == null ? "" : normalizeCellText(formatter.formatCellValue(row.getCell(index))); rowData.put(column, value); } - rowData.put("__rowIndex", rowNum + 1); - rows.add(rowData); - String brand = normalizeCellText(Objects.toString(rowData.getOrDefault("品牌", ""), "")); - if (!brand.isBlank()) { - uniqueBrands.add(brand); + String brand = normalizeCellText(Objects.toString(rowData.getOrDefault("\u54c1\u724c", ""), "")); + if (brand.isBlank()) { + rowData.put("__rowIndex", rowNum + 1); + rows.add(rowData); + continue; + } + if (uniqueBrands.add(brand)) { + rowData.put("__rowIndex", rowNum + 1); + rows.add(rowData); } } return new ParsedBrandFile(sheet.getSheetName(), columns, rows, new ArrayList<>(uniqueBrands)); diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/convert/controller/ConvertRunController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/convert/controller/ConvertRunController.java index babd8f6..2ffc659 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/convert/controller/ConvertRunController.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/convert/controller/ConvertRunController.java @@ -7,6 +7,7 @@ import com.nanri.aiimage.modules.convert.model.vo.ConvertRunVo; import com.nanri.aiimage.modules.convert.service.ConvertRunService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -24,6 +25,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.io.File; @@ -66,35 +68,43 @@ public class ConvertRunController { } @GetMapping("/history") - @Operation(summary = "查询格式转换历史", description = "查询最近的格式转换成功记录,用于页面右侧历史下载列表展示。") + @Operation(summary = "查询格式转换历史", description = "查询当前用户最近的格式转换成功记录,用于页面右侧历史下载列表展示。") @ApiResponses({ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "查询成功", content = @Content(schema = @Schema(implementation = ConvertHistoryVo.class))) }) - public ApiResponse history() { + public ApiResponse history( + @Parameter(name = "user_id", description = "当前登录用户 ID", required = true, in = ParameterIn.QUERY) + @RequestParam("user_id") Long userId) { ConvertHistoryVo vo = new ConvertHistoryVo(); - vo.setItems(convertRunService.listHistory()); + vo.setItems(convertRunService.listHistory(userId)); return ApiResponse.success(vo); } @DeleteMapping("/history/{resultId}") - @Operation(summary = "删除格式转换历史", description = "删除一条格式转换历史记录。") + @Operation(summary = "删除格式转换历史", description = "删除当前用户的一条格式转换历史记录。") @ApiResponses({ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "删除成功"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "结果记录不存在") }) - public ApiResponse deleteHistory(@Parameter(description = "格式转换结果记录 ID", required = true) @PathVariable Long resultId) { - convertRunService.deleteHistory(resultId); + public ApiResponse deleteHistory( + @Parameter(description = "格式转换结果记录 ID", required = true) @PathVariable Long resultId, + @Parameter(name = "user_id", description = "当前登录用户 ID", required = true, in = ParameterIn.QUERY) + @RequestParam("user_id") Long userId) { + convertRunService.deleteHistory(resultId, userId); return ApiResponse.success(null); } @GetMapping("/results/{resultId}/download") - @Operation(summary = "下载格式转换结果", description = "根据结果记录 ID 下载已生成的转换结果文件。桌面端前端可继续通过 save_file_from_url 选择保存位置。") + @Operation(summary = "下载格式转换结果", description = "根据结果记录 ID 下载当前用户的转换结果文件。桌面端前端可继续通过 save_file_from_url 选择保存位置。") @ApiResponses({ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "返回结果文件流"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "结果文件不存在") }) - public ResponseEntity download(@Parameter(description = "格式转换结果记录 ID", required = true) @PathVariable Long resultId) { - File resultFile = convertRunService.getResultFile(resultId); + public ResponseEntity download( + @Parameter(description = "格式转换结果记录 ID", required = true) @PathVariable Long resultId, + @Parameter(name = "user_id", description = "当前登录用户 ID", required = true, in = ParameterIn.QUERY) + @RequestParam("user_id") Long userId) { + File resultFile = convertRunService.getResultFile(resultId, userId); String encodedFilename = URLEncoder.encode(resultFile.getName(), StandardCharsets.UTF_8).replaceAll("\\+", "%20"); return ResponseEntity.ok() .contentType(MediaType.APPLICATION_OCTET_STREAM) diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/convert/model/dto/ConvertRunRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/convert/model/dto/ConvertRunRequest.java index bad0cbc..858b53c 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/convert/model/dto/ConvertRunRequest.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/convert/model/dto/ConvertRunRequest.java @@ -1,9 +1,11 @@ package com.nanri.aiimage.modules.convert.model.dto; +import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; import lombok.Data; import java.util.List; @@ -23,4 +25,9 @@ public class ConvertRunRequest { @Schema(description = "文件夹上传场景下的总压缩包名称(不含扩展名)") private String archiveName; + + @NotNull(message = "user_id 不能为空") + @JsonProperty("user_id") + @Schema(description = "当前登录用户 ID") + private Long userId; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/convert/service/ConvertRunService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/convert/service/ConvertRunService.java index 50445f8..97962c2 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/convert/service/ConvertRunService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/convert/service/ConvertRunService.java @@ -71,7 +71,8 @@ public class ConvertRunService { task.setTaskMode("IMMEDIATE"); task.setStatus("SUCCESS"); task.setSourceFileCount(request.getFiles().size()); - task.setCreatedBy("system"); + task.setCreatedBy("user:" + request.getUserId()); + task.setUserId(request.getUserId()); task.setRequestJson(JSONUtil.toJsonStr(request)); task.setCreatedAt(LocalDateTime.now()); task.setUpdatedAt(LocalDateTime.now()); @@ -130,6 +131,7 @@ public class ConvertRunService { resultEntity.setResultFileSize(packagedResultFile.length()); resultEntity.setResultContentType(contentType); resultEntity.setSuccess(1); + resultEntity.setUserId(request.getUserId()); resultEntity.setCreatedAt(LocalDateTime.now()); fileResultMapper.insert(resultEntity); item.setResultId(resultEntity.getId()); @@ -150,6 +152,7 @@ public class ConvertRunService { resultEntity.setSourceFilename(sourceFile.getOriginalFilename()); resultEntity.setSuccess(0); resultEntity.setErrorMessage(ex.getMessage()); + resultEntity.setUserId(request.getUserId()); resultEntity.setCreatedAt(LocalDateTime.now()); fileResultMapper.insert(resultEntity); } @@ -175,6 +178,7 @@ public class ConvertRunService { resultEntity.setResultFileSize(zipFile.length()); resultEntity.setResultContentType("application/zip"); resultEntity.setSuccess(1); + resultEntity.setUserId(request.getUserId()); resultEntity.setCreatedAt(LocalDateTime.now()); fileResultMapper.insert(resultEntity); item.setResultId(resultEntity.getId()); @@ -195,9 +199,10 @@ public class ConvertRunService { return vo; } - public List listHistory() { + public List listHistory(Long userId) { return fileResultMapper.selectList(new LambdaQueryWrapper() .eq(FileResultEntity::getModuleType, MODULE_TYPE) + .eq(FileResultEntity::getUserId, userId) .orderByDesc(FileResultEntity::getCreatedAt) .last("limit 200")) .stream() @@ -214,17 +219,17 @@ public class ConvertRunService { .toList(); } - public void deleteHistory(Long resultId) { + public void deleteHistory(Long resultId, Long userId) { FileResultEntity entity = fileResultMapper.selectById(resultId); - if (entity == null || !MODULE_TYPE.equals(entity.getModuleType())) { + if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) { throw new BusinessException("Convert result record does not exist."); } fileResultMapper.deleteById(resultId); } - public File getResultFile(Long resultId) { + public File getResultFile(Long resultId, Long userId) { FileResultEntity entity = fileResultMapper.selectById(resultId); - if (entity == null || entity.getResultFileUrl() == null || !MODULE_TYPE.equals(entity.getModuleType())) { + if (entity == null || entity.getResultFileUrl() == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) { throw new BusinessException("Convert result file does not exist."); } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/controller/DedupeRunController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/controller/DedupeRunController.java index 26f2571..d953d7e 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/controller/DedupeRunController.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/controller/DedupeRunController.java @@ -7,6 +7,7 @@ import com.nanri.aiimage.modules.dedupe.model.vo.DedupeRunVo; import com.nanri.aiimage.modules.dedupe.service.DedupeRunService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -23,6 +24,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -60,35 +62,43 @@ public class DedupeRunController { } @GetMapping("/history") - @Operation(summary = "查询去重历史", description = "查询最近的去重成功记录,用于页面右侧历史下载列表展示。") + @Operation(summary = "查询去重历史", description = "查询当前用户最近的去重成功记录,用于页面右侧历史下载列表展示。") @ApiResponses({ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "查询成功", content = @Content(schema = @Schema(implementation = DedupeHistoryVo.class))) }) - public ApiResponse history() { + public ApiResponse history( + @Parameter(name = "user_id", description = "当前登录用户 ID", required = true, in = ParameterIn.QUERY) + @RequestParam("user_id") Long userId) { DedupeHistoryVo vo = new DedupeHistoryVo(); - vo.setItems(dedupeRunService.listHistory()); + vo.setItems(dedupeRunService.listHistory(userId)); return ApiResponse.success(vo); } @DeleteMapping("/history/{resultId}") - @Operation(summary = "删除去重历史", description = "删除一条去重历史记录。") + @Operation(summary = "删除去重历史", description = "删除当前用户的一条去重历史记录。") @ApiResponses({ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "删除成功"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "结果记录不存在") }) - public ApiResponse deleteHistory(@Parameter(description = "去重结果记录 ID", required = true) @PathVariable Long resultId) { - dedupeRunService.deleteHistory(resultId); + public ApiResponse deleteHistory( + @Parameter(description = "去重结果记录 ID", required = true) @PathVariable Long resultId, + @Parameter(name = "user_id", description = "当前登录用户 ID", required = true, in = ParameterIn.QUERY) + @RequestParam("user_id") Long userId) { + dedupeRunService.deleteHistory(resultId, userId); return ApiResponse.success(null); } @GetMapping("/results/{resultId}/download") - @Operation(summary = "下载数据去重结果", description = "根据结果记录 ID 下载清洗后的 Excel 文件。") + @Operation(summary = "下载数据去重结果", description = "根据结果记录 ID 下载当前用户的清洗后 Excel 文件。") @ApiResponses({ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "返回结果文件流"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "结果文件不存在") }) - public ResponseEntity download(@Parameter(description = "去重结果记录 ID", required = true) @PathVariable Long resultId) { - Resource resource = dedupeRunService.getResultFile(resultId); + public ResponseEntity download( + @Parameter(description = "去重结果记录 ID", required = true) @PathVariable Long resultId, + @Parameter(name = "user_id", description = "当前登录用户 ID", required = true, in = ParameterIn.QUERY) + @RequestParam("user_id") Long userId) { + Resource resource = dedupeRunService.getResultFile(resultId, userId); return ResponseEntity.ok() .contentType(MediaType.APPLICATION_OCTET_STREAM) .header(HttpHeaders.CONTENT_DISPOSITION, "attachment") diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/model/dto/DedupeRunRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/model/dto/DedupeRunRequest.java index c141275..41386c5 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/model/dto/DedupeRunRequest.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/model/dto/DedupeRunRequest.java @@ -1,8 +1,10 @@ package com.nanri.aiimage.modules.dedupe.model.dto; +import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; import lombok.Data; import java.util.List; @@ -31,4 +33,9 @@ public class DedupeRunRequest { @Schema(description = "文件夹上传场景下的总压缩包名称(不含扩展名)") private String archiveName; + + @NotNull(message = "user_id 不能为空") + @JsonProperty("user_id") + @Schema(description = "当前登录用户 ID") + private Long userId; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/service/DedupeRunService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/service/DedupeRunService.java index 2c44bca..8953a1f 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/service/DedupeRunService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/service/DedupeRunService.java @@ -62,7 +62,8 @@ public class DedupeRunService { task.setTaskMode("IMMEDIATE"); task.setStatus("SUCCESS"); task.setSourceFileCount(request.getFiles().size()); - task.setCreatedBy("system"); + task.setCreatedBy("user:" + request.getUserId()); + task.setUserId(request.getUserId()); task.setRequestJson(JSONUtil.toJsonStr(request)); task.setCreatedAt(LocalDateTime.now()); task.setUpdatedAt(LocalDateTime.now()); @@ -123,6 +124,7 @@ public class DedupeRunService { resultEntity.setResultFileSize(outputFile.length()); resultEntity.setResultContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); resultEntity.setSuccess(1); + resultEntity.setUserId(request.getUserId()); resultEntity.setCreatedAt(LocalDateTime.now()); fileResultMapper.insert(resultEntity); } catch (Exception ex) { @@ -136,6 +138,7 @@ public class DedupeRunService { resultEntity.setSourceFilename(sourceFile.getOriginalFilename()); resultEntity.setSuccess(0); resultEntity.setErrorMessage(ex.getMessage()); + resultEntity.setUserId(request.getUserId()); resultEntity.setCreatedAt(LocalDateTime.now()); fileResultMapper.insert(resultEntity); } @@ -162,6 +165,7 @@ public class DedupeRunService { resultEntity.setResultFileSize(zipFile.length()); resultEntity.setResultContentType("application/zip"); resultEntity.setSuccess(1); + resultEntity.setUserId(request.getUserId()); resultEntity.setCreatedAt(LocalDateTime.now()); fileResultMapper.insert(resultEntity); item.setResultId(resultEntity.getId()); @@ -182,9 +186,10 @@ public class DedupeRunService { return vo; } - public List listHistory() { + public List listHistory(Long userId) { return fileResultMapper.selectList(new LambdaQueryWrapper() .eq(FileResultEntity::getModuleType, "DEDUPE") + .eq(FileResultEntity::getUserId, userId) .orderByDesc(FileResultEntity::getCreatedAt) .last("limit 50")) .stream() @@ -201,17 +206,17 @@ public class DedupeRunService { .toList(); } - public void deleteHistory(Long resultId) { + public void deleteHistory(Long resultId, Long userId) { FileResultEntity entity = fileResultMapper.selectById(resultId); - if (entity == null || !"DEDUPE".equals(entity.getModuleType())) { + if (entity == null || !"DEDUPE".equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) { throw new BusinessException("记录不存在"); } fileResultMapper.deleteById(resultId); } - public Resource getResultFile(Long resultId) { + public Resource getResultFile(Long resultId, Long userId) { FileResultEntity entity = fileResultMapper.selectById(resultId); - if (entity == null || entity.getResultFileUrl() == null || !"DEDUPE".equals(entity.getModuleType())) { + if (entity == null || entity.getResultFileUrl() == null || !"DEDUPE".equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) { throw new BusinessException("结果文件不存在"); } File file = new File(entity.getResultFileUrl()); diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/controller/DeleteBrandRunController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/controller/DeleteBrandRunController.java new file mode 100644 index 0000000..a6e98a3 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/controller/DeleteBrandRunController.java @@ -0,0 +1,60 @@ +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.vo.DeleteBrandHistoryVo; +import com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandRunVo; +import com.nanri.aiimage.modules.deletebrand.service.DeleteBrandRunService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/delete-brand") +@Tag(name = "删除品牌", description = "按每个上传 Excel 的文件名匹配紫鸟店铺,并解析删除品牌表格预览。") +public class DeleteBrandRunController { + + private final DeleteBrandRunService deleteBrandRunService; + + @PostMapping("/run") + @Operation(summary = "执行删除品牌解析", description = "读取上传的删除品牌 Excel,按每个 Excel 文件名匹配紫鸟店铺,并返回受限预览数据和紫鸟店铺打开链接。") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "执行成功", content = @Content(schema = @Schema(implementation = DeleteBrandRunVo.class))) + }) + public ApiResponse run(@Valid @RequestBody DeleteBrandRunRequest request) { + return ApiResponse.success(deleteBrandRunService.run(request)); + } + + @GetMapping("/history") + @Operation(summary = "查询删除品牌历史", description = "按 user_id 查询删除品牌历史。") + public ApiResponse history( + @Parameter(name = "user_id", description = "当前登录用户 ID", required = true, in = ParameterIn.QUERY) + @RequestParam("user_id") Long userId) { + return ApiResponse.success(deleteBrandRunService.listHistory(userId)); + } + + @DeleteMapping("/history/{resultId}") + @Operation(summary = "删除删除品牌历史", description = "按 user_id 删除一条删除品牌历史。") + public ApiResponse deleteHistory( + @PathVariable Long resultId, + @Parameter(name = "user_id", description = "当前登录用户 ID", required = true, in = ParameterIn.QUERY) + @RequestParam("user_id") Long userId) { + deleteBrandRunService.deleteHistory(resultId, userId); + return ApiResponse.success(null); + } +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/model/dto/DeleteBrandRunRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/model/dto/DeleteBrandRunRequest.java new file mode 100644 index 0000000..9ce0482 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/model/dto/DeleteBrandRunRequest.java @@ -0,0 +1,24 @@ +package com.nanri.aiimage.modules.deletebrand.model.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +@Data +@Schema(description = "删除品牌执行请求") +public class DeleteBrandRunRequest { + @Valid + @NotEmpty(message = "请先上传待处理文件") + @Schema(description = "已上传源文件列表") + private List files; + + @NotNull(message = "user_id 不能为空") + @JsonProperty("user_id") + @Schema(description = "当前登录用户 ID") + private Long userId; +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/model/dto/DeleteBrandSourceFileDto.java b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/model/dto/DeleteBrandSourceFileDto.java new file mode 100644 index 0000000..58ef71f --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/model/dto/DeleteBrandSourceFileDto.java @@ -0,0 +1,17 @@ +package com.nanri.aiimage.modules.deletebrand.model.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "删除品牌源文件") +public class DeleteBrandSourceFileDto { + @Schema(description = "上传文件 key") + private String fileKey; + + @Schema(description = "原始文件名") + private String originalFilename; + + @Schema(description = "相对路径,仅用于批量上传展示和定位,不参与店铺匹配") + private String relativePath; +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/model/vo/DeleteBrandHistoryVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/model/vo/DeleteBrandHistoryVo.java new file mode 100644 index 0000000..9e2c054 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/model/vo/DeleteBrandHistoryVo.java @@ -0,0 +1,14 @@ +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 DeleteBrandHistoryVo { + @Schema(description = "历史列表") + private List items = new ArrayList<>(); +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/model/vo/DeleteBrandPreviewRowVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/model/vo/DeleteBrandPreviewRowVo.java new file mode 100644 index 0000000..25b1058 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/model/vo/DeleteBrandPreviewRowVo.java @@ -0,0 +1,20 @@ +package com.nanri.aiimage.modules.deletebrand.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "删除品牌预览行") +public class DeleteBrandPreviewRowVo { + @Schema(description = "行号") + private Integer rowIndex; + + @Schema(description = "国家") + private String country; + + @Schema(description = "删除ASIN") + private String asin; + + @Schema(description = "状态") + private String status; +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/model/vo/DeleteBrandResultItemVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/model/vo/DeleteBrandResultItemVo.java new file mode 100644 index 0000000..7c162cd --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/model/vo/DeleteBrandResultItemVo.java @@ -0,0 +1,47 @@ +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 DeleteBrandResultItemVo { + @Schema(description = "结果记录ID") + private Long resultId; + + @Schema(description = "源文件名") + private String sourceFilename; + + @Schema(description = "按 Excel 文件名解析出的店铺名") + private String shopName; + + @Schema(description = "是否匹配到紫鸟店铺") + private boolean matched; + + @Schema(description = "紫鸟店铺ID") + private String shopId; + + @Schema(description = "紫鸟店铺平台") + private String platform; + + @Schema(description = "紫鸟打开链接") + private String openStoreUrl; + + @Schema(description = "总行数") + private Integer totalRows; + + @Schema(description = "是否已截断") + private boolean truncated; + + @Schema(description = "预览行") + private List previewRows = new ArrayList<>(); + + @Schema(description = "是否成功") + private boolean success; + + @Schema(description = "错误信息") + private String error; +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/model/vo/DeleteBrandRunVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/model/vo/DeleteBrandRunVo.java new file mode 100644 index 0000000..dc22b20 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/model/vo/DeleteBrandRunVo.java @@ -0,0 +1,23 @@ +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 DeleteBrandRunVo { + @Schema(description = "处理文件总数") + private Integer total; + + @Schema(description = "成功文件数") + private Integer successCount; + + @Schema(description = "失败文件数") + private Integer failedCount; + + @Schema(description = "结果列表") + private List items = new ArrayList<>(); +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/service/DeleteBrandRunService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/service/DeleteBrandRunService.java new file mode 100644 index 0000000..f32f2f7 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/service/DeleteBrandRunService.java @@ -0,0 +1,291 @@ +package com.nanri.aiimage.modules.deletebrand.service; + +import cn.hutool.core.io.FileUtil; +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.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandRunRequest; +import com.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandSourceFileDto; +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; +import com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandRunVo; +import com.nanri.aiimage.modules.file.service.LocalFileStorageService; +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; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.io.FileInputStream; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +@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; + + public DeleteBrandRunVo run(DeleteBrandRunRequest request) { + if (request.getUserId() == null || request.getUserId() <= 0) { + throw new BusinessException("user_id 不合法"); + } + + FileTaskEntity task = new FileTaskEntity(); + task.setTaskNo(MODULE_TYPE + "-" + IdUtil.getSnowflakeNextIdStr()); + task.setModuleType(MODULE_TYPE); + task.setTaskMode("IMMEDIATE"); + task.setStatus("SUCCESS"); + 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 shops = ziniaoAuthService.listShopsForConfiguredUser(); + List items = new ArrayList<>(); + int successCount = 0; + int failedCount = 0; + + for (DeleteBrandSourceFileDto sourceFile : request.getFiles()) { + DeleteBrandResultItemVo item = new DeleteBrandResultItemVo(); + item.setSourceFilename(sourceFile.getOriginalFilename()); + item.setShopName(resolveShopName(sourceFile)); + try { + File inputFile = localFileStorageService.findLocalSourceFile(sourceFile.getFileKey()); + if (inputFile == null || !inputFile.exists()) { + throw new BusinessException("上传文件不存在,请重新上传"); + } + ParsedDeleteBrandFile parsed = parseDeleteBrandFile(inputFile); + item.setTotalRows(parsed.totalRows()); + item.setTruncated(parsed.truncated()); + 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); + } + + item.setSuccess(true); + successCount++; + + FileResultEntity resultEntity = new FileResultEntity(); + resultEntity.setTaskId(task.getId()); + resultEntity.setModuleType(MODULE_TYPE); + resultEntity.setSourceFilename(item.getSourceFilename()); + resultEntity.setResultFilename(item.getShopName()); + resultEntity.setResultFileUrl(null); + resultEntity.setResultFileSize(0L); + resultEntity.setResultContentType("application/json"); + resultEntity.setSuccess(1); + resultEntity.setUserId(request.getUserId()); + resultEntity.setCreatedAt(LocalDateTime.now()); + fileResultMapper.insert(resultEntity); + item.setResultId(resultEntity.getId()); + } catch (Exception ex) { + item.setSuccess(false); + item.setError(ex.getMessage()); + failedCount++; + + FileResultEntity resultEntity = new FileResultEntity(); + resultEntity.setTaskId(task.getId()); + resultEntity.setModuleType(MODULE_TYPE); + resultEntity.setSourceFilename(sourceFile.getOriginalFilename()); + resultEntity.setSuccess(0); + resultEntity.setErrorMessage(ex.getMessage()); + resultEntity.setUserId(request.getUserId()); + resultEntity.setCreatedAt(LocalDateTime.now()); + fileResultMapper.insert(resultEntity); + item.setResultId(resultEntity.getId()); + } + items.add(item); + } + + task.setSuccessFileCount(successCount); + task.setFailedFileCount(failedCount); + task.setResultJson(JSONUtil.toJsonStr(items)); + task.setUpdatedAt(LocalDateTime.now()); + fileTaskMapper.updateById(task); + + DeleteBrandRunVo vo = new DeleteBrandRunVo(); + vo.setTotal(request.getFiles().size()); + vo.setSuccessCount(successCount); + vo.setFailedCount(failedCount); + vo.setItems(items); + return vo; + } + + public DeleteBrandHistoryVo listHistory(Long userId) { + DeleteBrandHistoryVo vo = new DeleteBrandHistoryVo(); + vo.setItems(fileResultMapper.selectList(new LambdaQueryWrapper() + .eq(FileResultEntity::getModuleType, MODULE_TYPE) + .eq(FileResultEntity::getUserId, userId) + .orderByDesc(FileResultEntity::getCreatedAt) + .last("limit 100")) + .stream() + .map(entity -> { + DeleteBrandResultItemVo item = new DeleteBrandResultItemVo(); + item.setResultId(entity.getId()); + item.setSourceFilename(entity.getSourceFilename()); + item.setShopName(entity.getResultFilename()); + item.setSuccess(entity.getSuccess() != null && entity.getSuccess() == 1); + item.setError(entity.getErrorMessage()); + return item; + }) + .toList()); + return vo; + } + + public void deleteHistory(Long resultId, Long userId) { + FileResultEntity entity = fileResultMapper.selectById(resultId); + if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) { + throw new BusinessException("记录不存在"); + } + fileResultMapper.deleteById(resultId); + } + + private ParsedDeleteBrandFile parseDeleteBrandFile(File inputFile) { + DataFormatter formatter = new DataFormatter(); + try (FileInputStream fis = new FileInputStream(inputFile); Workbook workbook = org.apache.poi.ss.usermodel.WorkbookFactory.create(fis)) { + Sheet sheet = workbook.getSheetAt(0); + Row titleRow = sheet.getRow(0); + Row headerRow = sheet.getRow(1); + if (titleRow == null || headerRow == null) { + throw new BusinessException("Excel 表头不完整"); + } + + List pairs = resolveCountryPairs(titleRow, headerRow, formatter); + if (pairs.isEmpty()) { + throw new BusinessException("未识别到删除品牌表头"); + } + + List previewRows = new ArrayList<>(); + int totalRows = 0; + boolean truncated = false; + for (int rowNum = 2; rowNum <= sheet.getLastRowNum(); rowNum++) { + Row row = sheet.getRow(rowNum); + if (row == null) { + continue; + } + for (CountryColumnPair pair : pairs) { + 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; + } + } + } + return new ParsedDeleteBrandFile(totalRows, truncated, previewRows); + } catch (BusinessException ex) { + throw ex; + } catch (Exception ex) { + throw new BusinessException("读取删除品牌 Excel 失败"); + } + } + + private List resolveCountryPairs(Row titleRow, Row headerRow, DataFormatter formatter) { + List pairs = new ArrayList<>(); + int lastCellNum = Math.max(titleRow.getLastCellNum(), headerRow.getLastCellNum()); + for (int i = 0; i < lastCellNum; i++) { + String title = normalizeCellText(formatter.formatCellValue(titleRow.getCell(i))); + String firstHeader = normalizeCellText(formatter.formatCellValue(headerRow.getCell(i))); + String secondHeader = normalizeCellText(formatter.formatCellValue(headerRow.getCell(i + 1))); + if (title.isBlank()) { + continue; + } + if ("删除ASIN".equals(firstHeader) && "状态".equals(secondHeader)) { + pairs.add(new CountryColumnPair(title, i, i + 1)); + i++; + } + } + return pairs; + } + + private ZiniaoShopCacheDto matchShop(String shopName, List 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()) { + return ""; + } + 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 ""; + } + return value.replace("\ufeff", "") + .replace("\u3000", " ") + .replace("\r\n", " ") + .replace("\r", " ") + .replace("\n", " ") + .replace("\t", " ") + .trim() + .replaceAll("\\s+", " "); + } + + private record CountryColumnPair(String country, int asinColumnIndex, int statusColumnIndex) { + } + + private record ParsedDeleteBrandFile(int totalRows, boolean truncated, List previewRows) { + } +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/split/controller/SplitRunController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/split/controller/SplitRunController.java index 879dd69..bb0fc3d 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/split/controller/SplitRunController.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/split/controller/SplitRunController.java @@ -7,6 +7,7 @@ import com.nanri.aiimage.modules.split.model.vo.SplitRunVo; import com.nanri.aiimage.modules.split.service.SplitRunService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -23,6 +24,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -59,35 +61,43 @@ public class SplitRunController { } @GetMapping("/history") - @Operation(summary = "查询数据拆分历史", description = "查询最近的数据拆分成功记录,用于页面右侧历史下载列表展示。") + @Operation(summary = "查询数据拆分历史", description = "查询当前用户最近的数据拆分成功记录,用于页面右侧历史下载列表展示。") @ApiResponses({ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "查询成功", content = @Content(schema = @Schema(implementation = SplitHistoryVo.class))) }) - public ApiResponse history() { + public ApiResponse history( + @Parameter(name = "user_id", description = "当前登录用户 ID", required = true, in = ParameterIn.QUERY) + @RequestParam("user_id") Long userId) { SplitHistoryVo vo = new SplitHistoryVo(); - vo.setItems(splitRunService.listHistory()); + vo.setItems(splitRunService.listHistory(userId)); return ApiResponse.success(vo); } @DeleteMapping("/history/{resultId}") - @Operation(summary = "删除数据拆分历史", description = "删除一条数据拆分历史记录。") + @Operation(summary = "删除数据拆分历史", description = "删除当前用户的一条数据拆分历史记录。") @ApiResponses({ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "删除成功"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "结果记录不存在") }) - public ApiResponse deleteHistory(@Parameter(description = "数据拆分结果记录 ID", required = true) @PathVariable Long resultId) { - splitRunService.deleteHistory(resultId); + public ApiResponse deleteHistory( + @Parameter(description = "数据拆分结果记录 ID", required = true) @PathVariable Long resultId, + @Parameter(name = "user_id", description = "当前登录用户 ID", required = true, in = ParameterIn.QUERY) + @RequestParam("user_id") Long userId) { + splitRunService.deleteHistory(resultId, userId); return ApiResponse.success(null); } @GetMapping("/results/{resultId}/download") - @Operation(summary = "下载数据拆分结果", description = "根据结果记录 ID 下载拆分后的 Excel 文件。") + @Operation(summary = "下载数据拆分结果", description = "根据结果记录 ID 下载当前用户的拆分结果文件。") @ApiResponses({ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "返回结果文件流"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "结果文件不存在") }) - public ResponseEntity download(@Parameter(description = "数据拆分结果记录 ID", required = true) @PathVariable Long resultId) { - Resource resource = splitRunService.getResultFile(resultId); + public ResponseEntity download( + @Parameter(description = "数据拆分结果记录 ID", required = true) @PathVariable Long resultId, + @Parameter(name = "user_id", description = "当前登录用户 ID", required = true, in = ParameterIn.QUERY) + @RequestParam("user_id") Long userId) { + Resource resource = splitRunService.getResultFile(resultId, userId); return ResponseEntity.ok() .contentType(MediaType.APPLICATION_OCTET_STREAM) .header(HttpHeaders.CONTENT_DISPOSITION, "attachment") diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/split/model/dto/SplitRunRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/split/model/dto/SplitRunRequest.java index 97dbcb9..6726991 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/split/model/dto/SplitRunRequest.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/split/model/dto/SplitRunRequest.java @@ -1,9 +1,11 @@ package com.nanri.aiimage.modules.split.model.dto; +import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; import lombok.Data; import java.util.List; @@ -33,4 +35,9 @@ public class SplitRunRequest { @Schema(description = "文件夹上传场景下的总压缩包名称(不含扩展名)") private String archiveName; + + @NotNull(message = "user_id 不能为空") + @JsonProperty("user_id") + @Schema(description = "当前登录用户 ID") + private Long userId; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/split/service/SplitRunService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/split/service/SplitRunService.java index 53015a0..a05f420 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/split/service/SplitRunService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/split/service/SplitRunService.java @@ -67,7 +67,8 @@ public class SplitRunService { task.setTaskMode("IMMEDIATE"); task.setStatus("SUCCESS"); task.setSourceFileCount(request.getFiles().size()); - task.setCreatedBy("system"); + task.setCreatedBy("user:" + request.getUserId()); + task.setUserId(request.getUserId()); task.setRequestJson(JSONUtil.toJsonStr(request)); task.setCreatedAt(LocalDateTime.now()); task.setUpdatedAt(LocalDateTime.now()); @@ -113,6 +114,7 @@ public class SplitRunService { resultEntity.setSourceFilename(sourceFile.getOriginalFilename()); resultEntity.setSuccess(0); resultEntity.setErrorMessage(ex.getMessage()); + resultEntity.setUserId(request.getUserId()); resultEntity.setCreatedAt(LocalDateTime.now()); fileResultMapper.insert(resultEntity); } @@ -144,6 +146,7 @@ public class SplitRunService { resultEntity.setResultContentType(ZIP_CONTENT_TYPE); resultEntity.setRowCount(item.getRowCount()); resultEntity.setSuccess(1); + resultEntity.setUserId(request.getUserId()); resultEntity.setCreatedAt(LocalDateTime.now()); fileResultMapper.insert(resultEntity); item.setResultId(resultEntity.getId()); @@ -164,9 +167,10 @@ public class SplitRunService { return vo; } - public List listHistory() { + public List listHistory(Long userId) { return fileResultMapper.selectList(new LambdaQueryWrapper() .eq(FileResultEntity::getModuleType, MODULE_TYPE) + .eq(FileResultEntity::getUserId, userId) .orderByDesc(FileResultEntity::getCreatedAt) .last("limit 100")) .stream() @@ -186,17 +190,17 @@ public class SplitRunService { .toList(); } - public void deleteHistory(Long resultId) { + public void deleteHistory(Long resultId, Long userId) { FileResultEntity entity = fileResultMapper.selectById(resultId); - if (entity == null || !MODULE_TYPE.equals(entity.getModuleType())) { + if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) { throw new BusinessException("Split result record does not exist."); } fileResultMapper.deleteById(resultId); } - public Resource getResultFile(Long resultId) { + public Resource getResultFile(Long resultId, Long userId) { FileResultEntity entity = fileResultMapper.selectById(resultId); - if (entity == null || entity.getResultFileUrl() == null || !MODULE_TYPE.equals(entity.getModuleType())) { + if (entity == null || entity.getResultFileUrl() == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) { throw new BusinessException("Split result file does not exist."); } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/task/model/entity/FileResultEntity.java b/backend-java/src/main/java/com/nanri/aiimage/modules/task/model/entity/FileResultEntity.java index e6c8359..aed5a5c 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/task/model/entity/FileResultEntity.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/task/model/entity/FileResultEntity.java @@ -24,5 +24,6 @@ public class FileResultEntity { private Integer rowCount; private Integer success; private String errorMessage; + private Long userId; private LocalDateTime createdAt; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/task/model/entity/FileTaskEntity.java b/backend-java/src/main/java/com/nanri/aiimage/modules/task/model/entity/FileTaskEntity.java index ae3fdc1..15f574d 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/task/model/entity/FileTaskEntity.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/task/model/entity/FileTaskEntity.java @@ -24,6 +24,7 @@ public class FileTaskEntity { private String resultJson; private String errorMessage; private String createdBy; + private Long userId; private LocalDateTime createdAt; private LocalDateTime updatedAt; private LocalDateTime finishedAt; diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/ModuleHistoryCleanupService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/ModuleHistoryCleanupService.java new file mode 100644 index 0000000..ee9471f --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/ModuleHistoryCleanupService.java @@ -0,0 +1,52 @@ +package com.nanri.aiimage.modules.task.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.nanri.aiimage.config.ModuleCleanupProperties; +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 lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ModuleHistoryCleanupService { + + private final ModuleCleanupProperties moduleCleanupProperties; + private final FileTaskMapper fileTaskMapper; + private final FileResultMapper fileResultMapper; + + @Transactional + @Scheduled(cron = "${aiimage.module-cleanup.cron:0 0 0 * * *}") + public void cleanupConfiguredModules() { + if (!moduleCleanupProperties.isEnabled()) { + return; + } + List moduleTypes = moduleCleanupProperties.getModuleTypes(); + if (moduleTypes == null || moduleTypes.isEmpty()) { + return; + } + + int deletedResults = fileResultMapper.delete(new LambdaQueryWrapper() + .in(FileResultEntity::getModuleType, moduleTypes)); + + int resetTasks = fileTaskMapper.update(null, new LambdaUpdateWrapper() + .in(FileTaskEntity::getModuleType, moduleTypes) + .set(FileTaskEntity::getResultJson, null) + .set(FileTaskEntity::getRequestJson, null) + .set(FileTaskEntity::getErrorMessage, null)); + + int deletedTasks = fileTaskMapper.delete(new LambdaQueryWrapper() + .in(FileTaskEntity::getModuleType, moduleTypes)); + + log.info("模块历史清理完成: moduleTypes={}, deletedResults={}, resetTasks={}, deletedTasks={}", moduleTypes, deletedResults, resetTasks, deletedTasks); + } +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/client/ZiniaoClient.java b/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/client/ZiniaoClient.java index 2c2a1c2..6aedaff 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/client/ZiniaoClient.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/client/ZiniaoClient.java @@ -1,9 +1,18 @@ package com.nanri.aiimage.modules.ziniao.client; import com.nanri.aiimage.modules.ziniao.model.cache.ZiniaoShopCacheDto; +import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoStaffItemVo; import java.util.List; public interface ZiniaoClient { - List listShops(); + String getAppToken(); + + Long getCompanyIdByApiKey(); + + List listStaff(Long companyId); + + List listUserStores(Long companyId, Long userId, String userToken); + + String getUserLoginToken(Long companyId, Long userId); } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/client/ZiniaoClientImpl.java b/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/client/ZiniaoClientImpl.java index 3ff2bc1..beb321b 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/client/ZiniaoClientImpl.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/client/ZiniaoClientImpl.java @@ -5,6 +5,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; 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.MediaType; import org.springframework.http.client.SimpleClientHttpRequestFactory; @@ -13,6 +15,7 @@ import org.springframework.web.client.RestClient; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Objects; @Component @@ -21,22 +24,222 @@ public class ZiniaoClientImpl implements ZiniaoClient { private final ZiniaoProperties ziniaoProperties; private final ObjectMapper objectMapper; + private final ZiniaoSessionCacheService ziniaoSessionCacheService; @Override - public List listShops() { - if (ziniaoProperties.getCompanyId() == null || ziniaoProperties.getCompanyId() <= 0) { - throw new BusinessException("紫鸟 companyId 未配置"); + public String getAppToken() { + String cachedToken = ziniaoSessionCacheService.getAppToken(); + if (cachedToken != null && !cachedToken.isBlank()) { + return cachedToken; } - String raw = getRestClient().post() - .uri(joinUrl(ziniaoProperties.getBaseUrl(), ziniaoProperties.getShopsPath())) - .headers(headers -> { - headers.setBearerAuth(ziniaoProperties.getApiKey()); - headers.setContentType(MediaType.APPLICATION_JSON); - }) - .body(java.util.Map.of("companyId", ziniaoProperties.getCompanyId())) + 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() { + String raw = getWithApiKey("/app/builtin/company", "获取 companyId"); + try { + JsonNode root = objectMapper.readTree(raw); + JsonNode data = firstNonNull(root.get("data"), root.get("result"), root); + Long companyId = longValue(firstNonNull(data == null ? null : data.get("companyId"), root.get("companyId"))); + if (companyId == null || companyId <= 0) { + throw new BusinessException("紫鸟 companyId 响应缺少 companyId"); + } + return companyId; + } catch (BusinessException ex) { + throw ex; + } catch (Exception ex) { + throw new BusinessException("解析紫鸟 companyId 响应失败"); + } + } + + @Override + public List listStaff(Long companyId) { + String raw = postWithApiKey(ziniaoProperties.getStaffListPath(), Map.of( + "companyId", String.valueOf(companyId), + "level", "", + "isAccurate", "", + "limit", "100", + "page", "1", + "departmentIds", List.of(), + "delflag", "", + "name", "", + "username", "" + ), "获取员工列表"); + try { + JsonNode root = objectMapper.readTree(raw); + JsonNode dataNode = firstNonNull(root.get("data"), root.get("result")); + JsonNode itemsNode = dataNode == null ? null : firstNonNull(dataNode.get("data"), dataNode.get("items"), dataNode); + List items = new ArrayList<>(); + if (itemsNode != null && itemsNode.isArray()) { + for (JsonNode itemNode : itemsNode) { + addStaffItem(items, itemNode); + } + } else if (itemsNode != null && itemsNode.isObject()) { + addStaffItem(items, itemsNode); + } + return items; + } catch (Exception ex) { + throw new BusinessException("解析紫鸟员工列表失败"); + } + } + + @Override + public List listUserStores(Long companyId, Long userId, String userToken) { + String raw = postWithAuthorization(ziniaoProperties.getUserStoresPath(), userToken, Map.of( + "companyId", companyId, + "userId", userId, + "storeName", "", + "isAccurate", 1, + "page", 1, + "limit", 10 + ), "获取员工店铺列表"); + return parseUserStores(raw); + } + + @Override + public String getUserLoginToken(Long companyId, Long userId) { + String raw = postWithApiKey(ziniaoProperties.getUserLoginTokenPath(), Map.of( + "companyId", String.valueOf(companyId), + "userId", String.valueOf(userId) + ), "获取员工登录 token"); + try { + JsonNode root = objectMapper.readTree(raw); + if (!success(root)) { + throw new BusinessException("获取紫鸟员工登录 token 失败: " + Objects.toString(text(root.get("msg")), "未知错误")); + } + JsonNode data = firstNonNull(root.get("data"), root.get("result")); + String token = text(firstNonNull( + data == null ? null : data.get("token"), + data == null ? null : data.get("loginToken"), + root.get("token"), + root.get("loginToken") + )); + if (token == null || token.isBlank()) { + throw new BusinessException("紫鸟员工登录 token 响应缺少 token"); + } + return token; + } catch (BusinessException ex) { + throw ex; + } catch (Exception ex) { + throw new BusinessException("解析紫鸟员工登录 token 响应失败"); + } + } + + + private String getWithApiKey(String path, String action) { + String apiKey = requireText(ziniaoProperties.getApiKey(), "紫鸟 apiKey 未配置"); + String raw = getRestClient().get() + .uri(joinUrl(ziniaoProperties.getBaseUrl(), path)) + .headers(headers -> headers.setBearerAuth(apiKey)) .retrieve() .body(String.class); - return parseShops(raw); + 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); + }) + .body("") + .retrieve() + .body(String.class); + validateSuccess(raw, action, path); + return raw; + } + + private String postWithApiKey(String path, Map body, String action) { + String apiKey = requireText(ziniaoProperties.getApiKey(), "紫鸟 apiKey 未配置"); + RestClient.RequestBodySpec request = getRestClient().post() + .uri(joinUrl(ziniaoProperties.getBaseUrl(), path)) + .headers(headers -> { + headers.setBearerAuth(apiKey); + headers.setContentType(MediaType.APPLICATION_JSON); + }); + if (body != null) { + request.body(body); + } + String raw = request + .retrieve() + .body(String.class); + validateSuccess(raw, action, path); + return raw; + } + + private String postWithAuthorization(String path, String authorization, Map body, String action) { + String token = requireText(authorization, "紫鸟授权 token 未配置"); + RestClient.RequestBodySpec request = getRestClient().post() + .uri(joinUrl(ziniaoProperties.getBaseUrl(), path)) + .headers(headers -> { + headers.set("Authorization", token); + headers.setContentType(MediaType.APPLICATION_JSON); + }); + if (body != null) { + request.body(body); + } + String raw = request + .retrieve() + .body(String.class); + validateSuccess(raw, action, path); + return raw; + } + + private void validateSuccess(String raw, String action, String path) { + try { + JsonNode root = objectMapper.readTree(raw); + if (!success(root)) { + throw new BusinessException("紫鸟接口返回失败(" + action + ", " + path + "): " + Objects.toString(text(root.get("msg")), "未知错误")); + } + } catch (BusinessException ex) { + throw ex; + } catch (Exception ex) { + throw new BusinessException("解析紫鸟接口响应失败(" + action + ", " + path + ")"); + } + } + + private boolean success(JsonNode root) { + String code = text(root.get("code")); + if (code != null && !"0".equals(code)) { + return false; + } + String status = text(root.get("status")); + Long ret = longValue(root.get("ret")); + if ((ret != null && ret != 0L) || (status != null && !"success".equalsIgnoreCase(status))) { + return false; + } + JsonNode wrapper = root.get("data"); + if (wrapper != null && wrapper.isObject()) { + String wrapperStatus = text(wrapper.get("status")); + Long wrapperRet = longValue(wrapper.get("ret")); + if ((wrapperRet != null && wrapperRet != 0L) || (wrapperStatus != null && !"success".equalsIgnoreCase(wrapperStatus))) { + return false; + } + } + return true; } private RestClient getRestClient() { @@ -46,21 +249,20 @@ public class ZiniaoClientImpl implements ZiniaoClient { return RestClient.builder().requestFactory(requestFactory).build(); } + private void addStaffItem(List items, JsonNode itemNode) { + ZiniaoStaffItemVo item = new ZiniaoStaffItemVo(); + item.setUserId(longValue(firstNonNull(itemNode.get("userId"), itemNode.get("user_id")))); + item.setUsername(text(firstNonNull(itemNode.get("username"), itemNode.get("userName")))); + item.setName(text(itemNode.get("name"))); + if (item.getUserId() != null) { + items.add(item); + } + } + private List parseShops(String raw) { try { JsonNode root = objectMapper.readTree(raw); - String code = text(root.get("code")); - if (code != null && !"0".equals(code)) { - throw new BusinessException("紫鸟店铺接口返回失败: " + Objects.toString(text(root.get("msg")), "未知错误")); - } JsonNode wrapper = root.get("data"); - if (wrapper != null && wrapper.isObject()) { - String status = text(wrapper.get("status")); - Long ret = longValue(wrapper.get("ret")); - if ((ret != null && ret != 0L) || (status != null && !"success".equalsIgnoreCase(status))) { - throw new BusinessException("紫鸟店铺接口业务失败: " + Objects.toString(status, "unknown")); - } - } JsonNode itemsNode = wrapper == null ? null : wrapper.get("data"); List items = new ArrayList<>(); if (itemsNode != null && itemsNode.isArray()) { @@ -75,12 +277,33 @@ public class ZiniaoClientImpl implements ZiniaoClient { } } return items; - } catch (BusinessException ex) { - throw ex; } catch (Exception ex) { throw new BusinessException("解析紫鸟店铺列表失败"); } } + private List parseUserStores(String raw) { + try { + JsonNode root = objectMapper.readTree(raw); + JsonNode itemsNode = firstNonNull(root.get("data"), root.get("result")); + List items = new ArrayList<>(); + if (itemsNode != null && itemsNode.isArray()) { + for (JsonNode itemNode : itemsNode) { + ZiniaoShopCacheDto item = new ZiniaoShopCacheDto(); + item.setShopId(text(firstNonNull(itemNode.get("id"), itemNode.get("shopId"), itemNode.get("accountId"), itemNode.get("account_id")))); + item.setShopName(text(firstNonNull(itemNode.get("name"), itemNode.get("shopName"), itemNode.get("accountName"), itemNode.get("account_name")))); + item.setPlatform(text(firstNonNull(itemNode.get("platform"), itemNode.get("platformName"), itemNode.get("site"), itemNode.get("siteName")))); + if (item.getShopId() != null && !item.getShopId().isBlank()) { + items.add(item); + } + } + } + return items; + } catch (Exception ex) { + throw new BusinessException("解析紫鸟员工店铺列表失败"); + } + } + + private String joinUrl(String baseUrl, String path) { String base = Objects.toString(baseUrl, "").trim(); @@ -121,4 +344,11 @@ public class ZiniaoClientImpl implements ZiniaoClient { return null; } } + + private String requireText(String value, String message) { + if (value == null || value.isBlank() || "change-me".equalsIgnoreCase(value.trim())) { + throw new BusinessException(message); + } + return value.trim(); + } } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/controller/ZiniaoAuthController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/controller/ZiniaoAuthController.java index 9164249..5e0bd17 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/controller/ZiniaoAuthController.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/controller/ZiniaoAuthController.java @@ -1,12 +1,11 @@ package com.nanri.aiimage.modules.ziniao.controller; import com.nanri.aiimage.common.api.ApiResponse; -import com.nanri.aiimage.modules.ziniao.model.dto.ZiniaoSwitchShopRequest; -import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoCurrentShopVo; -import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoLoginUrlVo; +import com.nanri.aiimage.modules.ziniao.model.dto.ZiniaoOpenShopRequest; +import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoOpenShopVo; import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoSessionVo; import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoShopListVo; -import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoSwitchShopVo; +import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoStaffListVo; import com.nanri.aiimage.modules.ziniao.service.ZiniaoAuthService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -15,8 +14,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpHeaders; -import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -27,71 +24,44 @@ import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor @RequestMapping("/api/ziniao") -@Tag(name = "紫鸟接入", description = "紫鸟 ApiKey 模式下的配置状态、店铺列表、当前店铺与切换店铺接口") +@Tag(name = "紫鸟接入", description = "基于 API Key 直连紫鸟接口,获取 appToken、员工、店铺并生成打开店铺链接") public class ZiniaoAuthController { private final ZiniaoAuthService ziniaoAuthService; - @GetMapping("/login-url") - @Operation(summary = "获取紫鸟接入信息", description = "兼容保留接口,返回当前 ApiKey 模式的接入信息和 sessionId。") - @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "获取成功", content = @Content(schema = @Schema(implementation = ZiniaoLoginUrlVo.class))) - }) - public ApiResponse getLoginUrl() { - return ApiResponse.success(ziniaoAuthService.getLoginUrl()); - } - - @GetMapping("/callback") - @Operation(summary = "兼容保留回调接口", description = "ApiKey 模式下无真实登录回调,该接口仅兼容旧调用并返回 302。") - @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "302", description = "重定向到兼容地址") - }) - public ResponseEntity callback(@RequestParam(required = false) String code, @RequestParam(required = false) String state) { - try { - String redirectUrl = ziniaoAuthService.handleCallback(code, state); - return ResponseEntity.status(302) - .header(HttpHeaders.LOCATION, redirectUrl) - .build(); - } catch (Exception ex) { - return ResponseEntity.status(302) - .header(HttpHeaders.LOCATION, ziniaoAuthService.buildFailureRedirect(ex.getMessage())) - .build(); - } - } - @GetMapping("/session") - @Operation(summary = "获取紫鸟接入状态", description = "根据 sessionId 获取当前 ApiKey 模式会话与当前店铺信息。") + @Operation(summary = "获取紫鸟会话状态", description = "自动获取或复用 appToken,并返回 companyId、当前员工 userId、脱敏 token 和当前店铺信息。") @ApiResponses({ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "获取成功", content = @Content(schema = @Schema(implementation = ZiniaoSessionVo.class))) }) - public ApiResponse getSession(@RequestParam String sessionId) { - return ApiResponse.success(ziniaoAuthService.getSession(sessionId)); + public ApiResponse getSession(@RequestParam(required = false) String sessionId, @RequestParam(required = false) Long userId) { + return ApiResponse.success(ziniaoAuthService.getSession(sessionId, userId)); + } + + @GetMapping("/staff") + @Operation(summary = "获取紫鸟员工列表", description = "使用 API Key 直连紫鸟接口,查询当前 companyId 下的员工列表,供前端绑定 employee userId。") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "获取成功", content = @Content(schema = @Schema(implementation = ZiniaoStaffListVo.class))) + }) + public ApiResponse listStaff() { + return ApiResponse.success(ziniaoAuthService.listStaff()); } @GetMapping("/shops") - @Operation(summary = "获取紫鸟店铺列表", description = "根据 sessionId 使用配置的 ApiKey 与 companyId 拉取最新店铺列表。") + @Operation(summary = "获取员工可见店铺列表", description = "先通过 API Key 获取 companyId,再按员工 userId 查询该员工有权限的店铺列表,并缓存到当前 session。") @ApiResponses({ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "获取成功", content = @Content(schema = @Schema(implementation = ZiniaoShopListVo.class))) }) - public ApiResponse listShops(@RequestParam String sessionId) { - return ApiResponse.success(ziniaoAuthService.listShops(sessionId)); + public ApiResponse listShops(@RequestParam String sessionId, @RequestParam Long userId) { + return ApiResponse.success(ziniaoAuthService.listShops(sessionId, userId)); } - @GetMapping("/shops/current") - @Operation(summary = "获取当前紫鸟店铺", description = "根据 sessionId 获取当前已选中的店铺。") + @PostMapping("/shops/open") + @Operation(summary = "生成店铺打开链接", description = "按指定员工 userId 和 shopId 获取员工登录 token,并返回可直接拉起紫鸟客户端的 openStoreUrl。") @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "获取成功", content = @Content(schema = @Schema(implementation = ZiniaoCurrentShopVo.class))) + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "打开链接生成成功", content = @Content(schema = @Schema(implementation = ZiniaoOpenShopVo.class))) }) - public ApiResponse getCurrentShop(@RequestParam String sessionId) { - return ApiResponse.success(ziniaoAuthService.getCurrentShop(sessionId)); - } - - @PostMapping("/shops/switch") - @Operation(summary = "切换当前紫鸟店铺", description = "根据 sessionId 和 shopId 切换当前选中的店铺。") - @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "切换成功", content = @Content(schema = @Schema(implementation = ZiniaoSwitchShopVo.class))) - }) - public ApiResponse switchShop(@Valid @RequestBody ZiniaoSwitchShopRequest request) { - return ApiResponse.success(ziniaoAuthService.switchShop(request)); + public ApiResponse openShop(@Valid @RequestBody ZiniaoOpenShopRequest request) { + return ApiResponse.success(ziniaoAuthService.openShop(request)); } } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/model/cache/ZiniaoSessionCacheDto.java b/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/model/cache/ZiniaoSessionCacheDto.java index cfc72e7..df01a7e 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/model/cache/ZiniaoSessionCacheDto.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/model/cache/ZiniaoSessionCacheDto.java @@ -11,5 +11,7 @@ public class ZiniaoSessionCacheDto { private Long expireAt; private String ziniaoUserId; private String nickname; + private Long companyId; + private Long currentUserId; private String defaultShopId; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/model/dto/ZiniaoOpenShopRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/model/dto/ZiniaoOpenShopRequest.java new file mode 100644 index 0000000..225a998 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/model/dto/ZiniaoOpenShopRequest.java @@ -0,0 +1,17 @@ +package com.nanri.aiimage.modules.ziniao.model.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +public class ZiniaoOpenShopRequest { + @NotBlank(message = "sessionId 不能为空") + private String sessionId; + + @NotNull(message = "userId 不能为空") + private Long userId; + + @NotBlank(message = "shopId 不能为空") + private String shopId; +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/model/dto/ZiniaoSwitchShopRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/model/dto/ZiniaoSwitchShopRequest.java index eddeffd..0d2cd0c 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/model/dto/ZiniaoSwitchShopRequest.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/model/dto/ZiniaoSwitchShopRequest.java @@ -1,6 +1,7 @@ package com.nanri.aiimage.modules.ziniao.model.dto; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import lombok.Data; @Data @@ -8,6 +9,9 @@ public class ZiniaoSwitchShopRequest { @NotBlank(message = "sessionId 不能为空") private String sessionId; + @NotNull(message = "userId 不能为空") + private Long userId; + @NotBlank(message = "shopId 不能为空") private String shopId; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/model/vo/ZiniaoOpenShopVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/model/vo/ZiniaoOpenShopVo.java new file mode 100644 index 0000000..81d14b3 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/model/vo/ZiniaoOpenShopVo.java @@ -0,0 +1,23 @@ +package com.nanri.aiimage.modules.ziniao.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "紫鸟打开店铺结果") +public class ZiniaoOpenShopVo { + @Schema(description = "店铺ID") + private String shopId; + + @Schema(description = "店铺名称") + private String shopName; + + @Schema(description = "员工用户ID") + private Long userId; + + @Schema(description = "通过 API Key 调用接口获取的员工登录 token") + private String loginToken; + + @Schema(description = "打开店铺URL") + private String openStoreUrl; +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/model/vo/ZiniaoSessionVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/model/vo/ZiniaoSessionVo.java index 05ed620..7e30639 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/model/vo/ZiniaoSessionVo.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/model/vo/ZiniaoSessionVo.java @@ -9,15 +9,30 @@ public class ZiniaoSessionVo { @Schema(description = "会话 ID") private String sessionId; - @Schema(description = "是否已认证") + @Schema(description = "是否已启用") + private Boolean enabled; + + @Schema(description = "是否已获取应用 token") private Boolean authenticated; + @Schema(description = "公司 ID") + private Long companyId; + + @Schema(description = "当前员工 userId") + private Long currentUserId; + @Schema(description = "紫鸟用户 ID") private String ziniaoUserId; @Schema(description = "紫鸟昵称") private String nickname; + @Schema(description = "脱敏后的 appToken") + private String appToken; + + @Schema(description = "token 类型") + private String tokenType; + @Schema(description = "会话过期时间戳(毫秒)") private Long expireAt; diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/model/vo/ZiniaoStaffItemVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/model/vo/ZiniaoStaffItemVo.java new file mode 100644 index 0000000..f047713 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/model/vo/ZiniaoStaffItemVo.java @@ -0,0 +1,17 @@ +package com.nanri.aiimage.modules.ziniao.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "紫鸟员工") +public class ZiniaoStaffItemVo { + @Schema(description = "员工用户ID") + private Long userId; + + @Schema(description = "员工账号") + private String username; + + @Schema(description = "员工姓名") + private String name; +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/model/vo/ZiniaoStaffListVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/model/vo/ZiniaoStaffListVo.java new file mode 100644 index 0000000..a0568a1 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/model/vo/ZiniaoStaffListVo.java @@ -0,0 +1,14 @@ +package com.nanri.aiimage.modules.ziniao.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 ZiniaoStaffListVo { + @Schema(description = "员工列表") + private List items = new ArrayList<>(); +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/service/ZiniaoAuthService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/service/ZiniaoAuthService.java index 9535f15..e3e28a0 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/service/ZiniaoAuthService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/service/ZiniaoAuthService.java @@ -5,22 +5,22 @@ import com.nanri.aiimage.config.ZiniaoProperties; import com.nanri.aiimage.modules.ziniao.client.ZiniaoClient; import com.nanri.aiimage.modules.ziniao.model.cache.ZiniaoSessionCacheDto; import com.nanri.aiimage.modules.ziniao.model.cache.ZiniaoShopCacheDto; -import com.nanri.aiimage.modules.ziniao.model.dto.ZiniaoSwitchShopRequest; -import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoCurrentShopVo; -import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoLoginUrlVo; +import com.nanri.aiimage.modules.ziniao.model.dto.ZiniaoOpenShopRequest; +import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoOpenShopVo; import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoSessionVo; import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoShopItemVo; import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoShopListVo; -import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoSwitchShopVo; +import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoStaffItemVo; +import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoStaffListVo; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.web.util.UriComponentsBuilder; import java.nio.charset.StandardCharsets; import java.time.Instant; +import java.util.Base64; import java.util.List; import java.util.UUID; -import java.util.Base64; @Service @RequiredArgsConstructor @@ -30,163 +30,150 @@ public class ZiniaoAuthService { private final ZiniaoClient ziniaoClient; private final ZiniaoSessionCacheService ziniaoSessionCacheService; - public ZiniaoLoginUrlVo getLoginUrl() { + public ZiniaoSessionVo getSession(String sessionId, Long userId) { ensureEnabled(); - String sessionId = ensureSession(); - ZiniaoLoginUrlVo vo = new ZiniaoLoginUrlVo(); - vo.setState(sessionId); - vo.setExpireAt(Instant.now().plusSeconds(ziniaoProperties.getSessionTtlHours() * 3600).toEpochMilli()); - vo.setLoginUrl(ziniaoProperties.getBaseUrl()); - return vo; - } - - public String handleCallback(String code, String state) { - ensureEnabled(); - String sessionId = ensureSession(); - return ziniaoProperties.getBaseUrl() + "?sessionId=" + sessionId; - } - - public ZiniaoSessionVo getSession(String sessionId) { - ZiniaoSessionCacheDto session = requireSession(sessionId); - List shops = ziniaoSessionCacheService.getShops(sessionId); - ZiniaoShopCacheDto currentShop = getOrInitCurrentShop(sessionId, shops); + ZiniaoSessionCacheDto session = sessionId == null || sessionId.isBlank() ? initSession(userId) : requireOrInitSession(sessionId, userId); ZiniaoSessionVo vo = new ZiniaoSessionVo(); vo.setSessionId(session.getSessionId()); - vo.setAuthenticated(true); + vo.setEnabled(ziniaoProperties.isEnabled()); + vo.setAuthenticated(session.getAccessToken() != null && !session.getAccessToken().isBlank()); + 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.setExpireAt(session.getExpireAt()); - vo.setShopCount(shops.size()); - if (currentShop != null) { - vo.setCurrentShopId(currentShop.getShopId()); - vo.setCurrentShopName(currentShop.getShopName()); - } + vo.setShopCount(ziniaoSessionCacheService.getShops(session.getSessionId()).size()); + vo.setCurrentShopId(session.getDefaultShopId()); + vo.setCurrentShopName(resolveCurrentShopName(session.getSessionId(), session.getDefaultShopId())); return vo; } - public ZiniaoShopListVo listShops(String sessionId) { - requireSession(sessionId); - List shops = ziniaoClient.listShops(); - ziniaoSessionCacheService.saveShops(sessionId, shops); - ZiniaoShopCacheDto currentShop = getOrInitCurrentShop(sessionId, shops); + public ZiniaoStaffListVo listStaff() { + ensureEnabled(); + ZiniaoStaffListVo vo = new ZiniaoStaffListVo(); + Long companyId = resolveCompanyIdForStaff(); + vo.getItems().addAll(ziniaoClient.listStaff(companyId)); + return vo; + } + + public ZiniaoShopListVo listShops(String sessionId, Long userId) { + ZiniaoSessionCacheDto session = requireOrInitSession(sessionId, userId); + Long currentUserId = requireUserId(session.getCurrentUserId()); + String userToken = ziniaoClient.getUserLoginToken(resolveCompanyId(), currentUserId); + List shops = ziniaoClient.listUserStores(resolveCompanyId(), currentUserId, userToken); + ziniaoSessionCacheService.saveShops(session.getSessionId(), shops); + if (!shops.isEmpty() && (session.getDefaultShopId() == null || session.getDefaultShopId().isBlank())) { + session.setDefaultShopId(shops.get(0).getShopId()); + ziniaoSessionCacheService.saveSession(session); + } ZiniaoShopListVo vo = new ZiniaoShopListVo(); for (ZiniaoShopCacheDto shop : shops) { ZiniaoShopItemVo item = new ZiniaoShopItemVo(); item.setShopId(shop.getShopId()); item.setShopName(shop.getShopName()); item.setPlatform(shop.getPlatform()); - item.setSelected(currentShop != null && shop.getShopId().equals(currentShop.getShopId())); + item.setSelected(shop.getShopId() != null && shop.getShopId().equals(session.getDefaultShopId())); vo.getItems().add(item); } return vo; } - public ZiniaoCurrentShopVo getCurrentShop(String sessionId) { - requireSession(sessionId); - ZiniaoShopCacheDto currentShop = getOrInitCurrentShop(sessionId, ziniaoSessionCacheService.getShops(sessionId)); - if (currentShop == null) { - throw new BusinessException("当前会话没有可用店铺"); + public ZiniaoOpenShopVo openShop(ZiniaoOpenShopRequest request) { + ZiniaoSessionCacheDto session = requireOrInitSession(request.getSessionId(), request.getUserId()); + Long currentUserId = requireUserId(request.getUserId() != null ? request.getUserId() : session.getCurrentUserId()); + String userToken = ziniaoClient.getUserLoginToken(resolveCompanyId(), currentUserId); + List shops = ziniaoSessionCacheService.getShops(session.getSessionId()); + if (shops.isEmpty()) { + shops = ziniaoClient.listUserStores(resolveCompanyId(), currentUserId, userToken); + ziniaoSessionCacheService.saveShops(session.getSessionId(), shops); } - ZiniaoCurrentShopVo vo = new ZiniaoCurrentShopVo(); - vo.setShopId(currentShop.getShopId()); - vo.setShopName(currentShop.getShopName()); - vo.setPlatform(currentShop.getPlatform()); - vo.setSelectedAt(currentShop.getSelectedAt()); - return vo; - } - - public ZiniaoSwitchShopVo switchShop(ZiniaoSwitchShopRequest request) { - requireSession(request.getSessionId()); - List shops = ziniaoSessionCacheService.getShops(request.getSessionId()); ZiniaoShopCacheDto targetShop = shops.stream() .filter(item -> item.getShopId() != null && item.getShopId().equals(request.getShopId())) .findFirst() - .orElseThrow(() -> new BusinessException("店铺不存在或不属于当前会话")); - targetShop.setSelectedAt(Instant.now().toEpochMilli()); - ziniaoSessionCacheService.saveCurrentShop(request.getSessionId(), targetShop); - ZiniaoSwitchShopVo vo = new ZiniaoSwitchShopVo(); - vo.setSuccess(true); - vo.setCurrentShopId(targetShop.getShopId()); - vo.setCurrentShopName(targetShop.getShopName()); - vo.setOpenStoreUrl(buildOpenStoreUrl(targetShop)); + .orElseThrow(() -> new BusinessException("店铺不存在或不属于当前员工")); + session.setCurrentUserId(currentUserId); + session.setDefaultShopId(targetShop.getShopId()); + ziniaoSessionCacheService.saveSession(session); + ziniaoSessionCacheService.saveCurrentShop(session.getSessionId(), targetShop); + + ZiniaoOpenShopVo vo = new ZiniaoOpenShopVo(); + vo.setUserId(currentUserId); + vo.setShopId(targetShop.getShopId()); + vo.setShopName(targetShop.getShopName()); + vo.setLoginToken(userToken); + vo.setOpenStoreUrl(buildOpenStoreUrl(targetShop.getShopId(), currentUserId, userToken)); return vo; } - public String buildFailureRedirect(String message) { - return ziniaoProperties.getBaseUrl() + "?message=" + (message == null || message.isBlank() ? "紫鸟授权失败" : message); + public List listShopsForConfiguredUser() { + ensureEnabled(); + Long userId = parseConfiguredOpenStoreUserId(); + String userToken = ziniaoClient.getUserLoginToken(resolveCompanyId(), userId); + return ziniaoClient.listUserStores(resolveCompanyId(), userId, userToken); } - private String ensureSession() { + public String buildOpenStoreUrlForShop(ZiniaoShopCacheDto shop) { + ensureEnabled(); + if (shop == null || shop.getShopId() == null || shop.getShopId().isBlank()) { + throw new BusinessException("店铺不存在"); + } + Long userId = parseConfiguredOpenStoreUserId(); + String loginToken = ziniaoClient.getUserLoginToken(resolveCompanyId(), userId); + return buildOpenStoreUrl(shop.getShopId(), userId, loginToken); + } + + private ZiniaoSessionCacheDto initSession(Long userId) { String sessionId = "ziniao_" + UUID.randomUUID().toString().replace("-", ""); ZiniaoSessionCacheDto session = new ZiniaoSessionCacheDto(); session.setSessionId(sessionId); - session.setAccessToken(maskApiKey(ziniaoProperties.getApiKey())); + String appToken = ziniaoClient.getAppToken(); + session.setAccessToken(maskToken(appToken)); session.setTokenType("Bearer"); session.setExpireAt(Instant.now().plusSeconds(ziniaoProperties.getSessionTtlHours() * 3600).toEpochMilli()); - session.setZiniaoUserId(String.valueOf(defaultCompanyId())); - session.setNickname("ApiKey模式"); + session.setCompanyId(resolveCompanyId()); + session.setCurrentUserId(userId); + session.setZiniaoUserId(String.valueOf(userId == null ? 0L : userId)); + session.setNickname("API Key模式"); ziniaoSessionCacheService.saveSession(session); - return sessionId; + return session; } - private ZiniaoSessionCacheDto requireSession(String sessionId) { - ensureEnabled(); + private ZiniaoSessionCacheDto requireOrInitSession(String sessionId, Long userId) { if (sessionId == null || sessionId.isBlank()) { - throw new BusinessException("sessionId 不能为空"); + return initSession(userId); } ZiniaoSessionCacheDto session = ziniaoSessionCacheService.getSession(sessionId); if (session == null) { - throw new BusinessException("当前会话不存在或已过期"); + return initSession(userId); + } + if (userId != null && userId > 0) { + session.setCurrentUserId(userId); + session.setZiniaoUserId(String.valueOf(userId)); + ziniaoSessionCacheService.saveSession(session); } return session; } - private ZiniaoShopCacheDto getOrInitCurrentShop(String sessionId, List shops) { - ZiniaoShopCacheDto current = ziniaoSessionCacheService.getCurrentShop(sessionId); - if (current != null) { - return current; - } - if (shops == null || shops.isEmpty()) { + private String resolveCurrentShopName(String sessionId, String currentShopId) { + if (currentShopId == null || currentShopId.isBlank()) { return null; } - ZiniaoShopCacheDto first = shops.get(0); - first.setSelectedAt(Instant.now().toEpochMilli()); - ziniaoSessionCacheService.saveCurrentShop(sessionId, first); - return first; + return ziniaoSessionCacheService.getShops(sessionId).stream() + .filter(item -> currentShopId.equals(item.getShopId())) + .map(ZiniaoShopCacheDto::getShopName) + .findFirst() + .orElse(null); } - private void ensureEnabled() { - if (!ziniaoProperties.isEnabled()) { - throw new BusinessException("紫鸟集成未启用,请先配置环境变量"); - } - if (ziniaoProperties.getApiKey() == null || ziniaoProperties.getApiKey().isBlank() || "change-me".equalsIgnoreCase(ziniaoProperties.getApiKey())) { - throw new BusinessException("紫鸟 ApiKey 未配置"); - } - } - - private long defaultCompanyId() { - return ziniaoProperties.getCompanyId() == null ? 0L : ziniaoProperties.getCompanyId(); - } - - private String maskApiKey(String apiKey) { - if (apiKey == null || apiKey.isBlank()) { - return ""; - } - if (apiKey.length() <= 8) { - return apiKey; - } - return apiKey.substring(0, 4) + "****" + apiKey.substring(apiKey.length() - 4); - } - - private String buildOpenStoreUrl(ZiniaoShopCacheDto shop) { + private String buildOpenStoreUrl(String storeId, Long userId, String loginToken) { String scheme = requireText(ziniaoProperties.getOpenStoreScheme(), "紫鸟 open-store-scheme 未配置"); - String userId = requireText(ziniaoProperties.getOpenStoreUserId(), "紫鸟 open-store-user-id 未配置"); - String apiKey = requireText(ziniaoProperties.getApiKey(), "紫鸟 ApiKey 未配置"); String launchUrl = requireText(ziniaoProperties.getOpenStoreLaunchUrl(), "紫鸟 open-store-launch-url 未配置"); String encodedLaunchUrl = Base64.getEncoder().encodeToString(launchUrl.getBytes(StandardCharsets.UTF_8)); UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(scheme) - .queryParam("storeId", shop.getShopId()) - .queryParam("openapiToken", apiKey) + .queryParam("storeId", storeId) + .queryParam("openapiToken", loginToken) .queryParam("userId", userId) .queryParam("lanuchUrl", encodedLaunchUrl) .queryParam("autoopen", Boolean.TRUE.equals(ziniaoProperties.getOpenStoreAutoOpen())) @@ -202,8 +189,56 @@ public class ZiniaoAuthService { return builder.build(true).toUriString(); } + private void ensureEnabled() { + if (!ziniaoProperties.isEnabled()) { + throw new BusinessException("紫鸟集成未启用,请先配置环境变量"); + } + requireText(ziniaoProperties.getApiKey(), "紫鸟 apiKey 未配置"); + } + + private Long resolveCompanyIdForStaff() { + return resolveCompanyId(); + } + + private Long requireUserId(Long userId) { + if (userId == null || userId <= 0) { + throw new BusinessException("请先选择紫鸟员工"); + } + return userId; + } + + private Long resolveCompanyId() { + if (ziniaoProperties.getCompanyId() != null && ziniaoProperties.getCompanyId() > 0) { + return ziniaoProperties.getCompanyId(); + } + return ziniaoClient.getCompanyIdByApiKey(); + } + + private Long requireCompanyId() { + return resolveCompanyId(); + } + + private Long parseConfiguredOpenStoreUserId() { + String configured = requireText(ziniaoProperties.getOpenStoreUserId(), "紫鸟 open-store-user-id 未配置"); + try { + return Long.parseLong(configured); + } catch (Exception ex) { + throw new BusinessException("紫鸟 open-store-user-id 配置不合法"); + } + } + + private String maskToken(String token) { + if (token == null || token.isBlank()) { + return ""; + } + if (token.length() <= 8) { + return token; + } + return token.substring(0, 4) + "****" + token.substring(token.length() - 4); + } + private String requireText(String value, String message) { - if (value == null || value.isBlank()) { + if (value == null || value.isBlank() || "change-me".equalsIgnoreCase(value.trim())) { throw new BusinessException(message); } return value.trim(); diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/service/ZiniaoSessionCacheService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/service/ZiniaoSessionCacheService.java index 61b4a51..d99a37d 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/service/ZiniaoSessionCacheService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/service/ZiniaoSessionCacheService.java @@ -17,10 +17,27 @@ 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())); diff --git a/backend-java/src/main/resources/application-local.example.yml b/backend-java/src/main/resources/application-local.example.yml index 7642af0..8b41c59 100644 --- a/backend-java/src/main/resources/application-local.example.yml +++ b/backend-java/src/main/resources/application-local.example.yml @@ -15,12 +15,20 @@ AIIMAGE_OSS_ACCESS_KEY_SECRET=bQSZnFH455i8tzyOgeahJmUzwmhynz AIIMAGE_STORAGE_LOCAL_TEMP_DIR=F:/project/crawler-plugin/backend-java/data/tmp +AIIMAGE_MODULE_CLEANUP_ENABLED=true +AIIMAGE_MODULE_CLEANUP_CRON=0 0 0 * * * +AIIMAGE_MODULE_CLEANUP_MODULE_TYPES=DEDUPE,SPLIT,CONVERT,DELETE_BRAND + AIIMAGE_ZINIAO_ENABLED=false AIIMAGE_ZINIAO_BASE_URL=https://sbappstoreapi.ziniao.com/openapi-router AIIMAGE_ZINIAO_API_KEY=change-me +AIIMAGE_ZINIAO_APP_ID=change-me +AIIMAGE_ZINIAO_APP_SECRET=change-me AIIMAGE_ZINIAO_COMPANY_ID=0 -AIIMAGE_ZINIAO_SHOPS_PATH=/superbrowser/rest/v1/erp/shop/list -AIIMAGE_ZINIAO_SWITCH_SHOP_PATH= +AIIMAGE_ZINIAO_APP_TOKEN_PATH=/auth/get_app_token +AIIMAGE_ZINIAO_STAFF_LIST_PATH=/superbrowser/rest/v1/erp/staff/list +AIIMAGE_ZINIAO_USER_STORES_PATH=/superbrowser/rest/v1/erp/user/stores +AIIMAGE_ZINIAO_USER_LOGIN_TOKEN_PATH=/superbrowser/rest/v1/token/user-login AIIMAGE_ZINIAO_OPEN_STORE_SCHEME=superbrowser://OpenStrore AIIMAGE_ZINIAO_OPEN_STORE_USER_ID= AIIMAGE_ZINIAO_OPEN_STORE_LAUNCH_URL=https://www.baidu.com diff --git a/backend-java/src/main/resources/application.yml b/backend-java/src/main/resources/application.yml index dbee479..250819e 100644 --- a/backend-java/src/main/resources/application.yml +++ b/backend-java/src/main/resources/application.yml @@ -72,13 +72,21 @@ aiimage: failed-ttl-hours: ${AIIMAGE_BRAND_PROGRESS_FAILED_TTL_HOURS:2} heartbeat-timeout-minutes: ${AIIMAGE_BRAND_PROGRESS_HEARTBEAT_TIMEOUT_MINUTES:15} stale-check-cron: ${AIIMAGE_BRAND_PROGRESS_STALE_CHECK_CRON:0 */2 * * * *} + module-cleanup: + enabled: ${AIIMAGE_MODULE_CLEANUP_ENABLED:true} + cron: ${AIIMAGE_MODULE_CLEANUP_CRON:0 0 0 * * *} + module-types: ${AIIMAGE_MODULE_CLEANUP_MODULE_TYPES:DEDUPE,SPLIT,CONVERT,DELETE_BRAND} ziniao: enabled: ${AIIMAGE_ZINIAO_ENABLED:false} base-url: ${AIIMAGE_ZINIAO_BASE_URL:https://sbappstoreapi.ziniao.com/openapi-router} - api-key: ${AIIMAGE_ZINIAO_API_KEY:change-me} + api-key: ${AIIMAGE_ZINIAO_API_KEY:} + app-id: ${AIIMAGE_ZINIAO_APP_ID:change-me} + app-secret: ${AIIMAGE_ZINIAO_APP_SECRET:change-me} company-id: ${AIIMAGE_ZINIAO_COMPANY_ID:0} - shops-path: ${AIIMAGE_ZINIAO_SHOPS_PATH:/superbrowser/rest/v1/erp/shop/list} - switch-shop-path: ${AIIMAGE_ZINIAO_SWITCH_SHOP_PATH:} + app-token-path: ${AIIMAGE_ZINIAO_APP_TOKEN_PATH:/auth/get_app_token} + staff-list-path: ${AIIMAGE_ZINIAO_STAFF_LIST_PATH:/superbrowser/rest/v1/erp/staff/list} + user-stores-path: ${AIIMAGE_ZINIAO_USER_STORES_PATH:/superbrowser/rest/v1/erp/user/stores} + user-login-token-path: ${AIIMAGE_ZINIAO_USER_LOGIN_TOKEN_PATH:/superbrowser/rest/v1/token/user-login} open-store-scheme: ${AIIMAGE_ZINIAO_OPEN_STORE_SCHEME:superbrowser://OpenStrore} open-store-user-id: ${AIIMAGE_ZINIAO_OPEN_STORE_USER_ID:} open-store-launch-url: ${AIIMAGE_ZINIAO_OPEN_STORE_LAUNCH_URL:https://www.baidu.com} diff --git a/frontend-vue/delete-brand.html b/frontend-vue/delete-brand.html new file mode 100644 index 0000000..985a883 --- /dev/null +++ b/frontend-vue/delete-brand.html @@ -0,0 +1,12 @@ + + + + + + 删除品牌 - 数富AI + + +
+ + + diff --git a/frontend-vue/src/delete-brand-main.ts b/frontend-vue/src/delete-brand-main.ts new file mode 100644 index 0000000..7c3a7f4 --- /dev/null +++ b/frontend-vue/src/delete-brand-main.ts @@ -0,0 +1,7 @@ +import { createApp } from 'vue' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import '@/styles/main.css' +import BrandDeleteBrandTab from '@/pages/brand/components/BrandDeleteBrandTab.vue' + +createApp(BrandDeleteBrandTab).use(ElementPlus).mount('#app') diff --git a/frontend-vue/src/pages/brand/components/BrandDeleteBrandTab.vue b/frontend-vue/src/pages/brand/components/BrandDeleteBrandTab.vue new file mode 100644 index 0000000..caea652 --- /dev/null +++ b/frontend-vue/src/pages/brand/components/BrandDeleteBrandTab.vue @@ -0,0 +1,346 @@ + + + + + diff --git a/frontend-vue/src/pages/brand/components/BrandTopBar.vue b/frontend-vue/src/pages/brand/components/BrandTopBar.vue index fdc74fe..ccfda6b 100644 --- a/frontend-vue/src/pages/brand/components/BrandTopBar.vue +++ b/frontend-vue/src/pages/brand/components/BrandTopBar.vue @@ -20,7 +20,7 @@ diff --git a/frontend-vue/src/shared/api/brand.ts b/frontend-vue/src/shared/api/brand.ts index 12a0288..073a977 100644 --- a/frontend-vue/src/shared/api/brand.ts +++ b/frontend-vue/src/shared/api/brand.ts @@ -1,5 +1,14 @@ import { requestDeleteJson, requestGetJson, requestPostJson } from '@/shared/api/http' +function getCurrentUserId() { + const raw = typeof window === 'undefined' ? '' : window.localStorage.getItem('uid') || '' + const value = Number(raw) + if (!Number.isFinite(value) || value <= 0) { + throw new Error('未获取到用户ID') + } + return value +} + export interface BrandExpandFolderItem { absolutePath: string relativePath: string @@ -47,11 +56,11 @@ export interface BrandTaskMutationResponse { const API_PREFIX = '' export function getBrandTaskEventsUrl(taskId: string | number) { - return `${API_PREFIX}/api/brand/tasks/${taskId}/events` + return `${API_PREFIX}/api/brand/tasks/${taskId}/events?user_id=${encodeURIComponent(String(getCurrentUserId()))}` } export function getBrandTaskDownloadUrl(taskId: string | number) { - return `${API_PREFIX}/api/brand/download/${taskId}` + return `${API_PREFIX}/api/brand/tasks/${taskId}/download?user_id=${encodeURIComponent(String(getCurrentUserId()))}` } export function getBrandTemplateXlsxUrl() { @@ -75,23 +84,23 @@ export function runBrandNow(paths: string[], strategy: string) { } export function createBrandTask(paths: string[], strategy: string) { - return requestPostJson(`${API_PREFIX}/api/brand/tasks`, { paths, strategy }) + return requestPostJson(`${API_PREFIX}/api/brand/tasks?userId=${encodeURIComponent(String(getCurrentUserId()))}`, { paths, strategy }) } export function getBrandTasks() { - return requestGetJson(`${API_PREFIX}/api/brand/tasks`) + return requestGetJson(`${API_PREFIX}/api/brand/tasks?userId=${encodeURIComponent(String(getCurrentUserId()))}`) } export function getBrandTask(taskId: string | number) { - return requestGetJson(`${API_PREFIX}/api/brand/tasks/${taskId}`) + return requestGetJson(`${API_PREFIX}/api/brand/tasks/${taskId}?userId=${encodeURIComponent(String(getCurrentUserId()))}`) } export function cancelBrandTask(taskId: string | number) { - return requestPostJson(`${API_PREFIX}/api/brand/tasks/${taskId}/cancel`) + return requestPostJson(`${API_PREFIX}/api/brand/tasks/${taskId}/cancel?userId=${encodeURIComponent(String(getCurrentUserId()))}`) } export function deleteBrandTask(taskId: string | number) { - return requestDeleteJson(`${API_PREFIX}/api/brand/tasks/${taskId}`) + return requestDeleteJson(`${API_PREFIX}/api/brand/tasks/${taskId}?userId=${encodeURIComponent(String(getCurrentUserId()))}`) } export function createBrandTaskEvents(taskId: string | number) { diff --git a/frontend-vue/src/shared/api/java-modules.ts b/frontend-vue/src/shared/api/java-modules.ts index a8f85e5..d7f34fd 100644 --- a/frontend-vue/src/shared/api/java-modules.ts +++ b/frontend-vue/src/shared/api/java-modules.ts @@ -2,6 +2,15 @@ import { del, get, http, post, type JavaApiResponse, unwrapJavaResponse } from ' const JAVA_API_PREFIX = '/newApi/api' +function getCurrentUserId() { + const raw = typeof window === 'undefined' ? '' : window.localStorage.getItem('uid') || '' + const value = Number(raw) + if (!Number.isFinite(value) || value <= 0) { + throw new Error('未获取到用户ID') + } + return value +} + export interface UploadedFileRef { fileKey: string originalFilename?: string @@ -43,6 +52,7 @@ export interface DedupeRunRequest { keepUnderscoreIds: boolean keepIntegerMainIdsWhenNoSubIds: boolean archiveName?: string + user_id: number } export interface SplitArchiveEntry { @@ -80,6 +90,7 @@ export interface SplitRunRequest { rowsPerFile?: number parts?: number archiveName?: string + user_id: number } export interface ConvertResultItem { @@ -106,6 +117,45 @@ export interface ConvertRunRequest { files: UploadedFileRef[] templateId: string archiveName?: string + user_id: number +} + +export interface DeleteBrandPreviewRow { + rowIndex: number + country: string + asin: string + status: string +} + +export interface DeleteBrandResultItem { + resultId?: number + sourceFilename: string + shopName?: string + matched: boolean + shopId?: string + platform?: string + openStoreUrl?: string + totalRows?: number + truncated?: boolean + previewRows?: DeleteBrandPreviewRow[] + success: boolean + error?: string +} + +export interface DeleteBrandRunVo { + total: number + successCount: number + failedCount: number + items: DeleteBrandResultItem[] +} + +export interface DeleteBrandHistoryVo { + items: DeleteBrandResultItem[] +} + +export interface DeleteBrandRunRequest { + files: UploadedFileRef[] + user_id: number } export interface ConvertTemplateVo { @@ -133,28 +183,42 @@ export function getExcelInfo(fileKey: string) { })) } -export function runDedupe(request: DedupeRunRequest) { - return unwrapJavaResponse(post, DedupeRunRequest>(`${JAVA_API_PREFIX}/dedupe/run`, request)) +export function runDedupe(request: Omit | DedupeRunRequest) { + return unwrapJavaResponse(post, DedupeRunRequest>(`${JAVA_API_PREFIX}/dedupe/run`, { + ...request, + user_id: getCurrentUserId(), + })) } export function getDedupeHistory() { - return unwrapJavaResponse(get>(`${JAVA_API_PREFIX}/dedupe/history`)) + return unwrapJavaResponse(get>(`${JAVA_API_PREFIX}/dedupe/history`, { + params: { user_id: getCurrentUserId() }, + })) } export function deleteDedupeHistory(resultId: number) { - return unwrapJavaResponse(del>(`${JAVA_API_PREFIX}/dedupe/history/${resultId}`)) + return unwrapJavaResponse(del>(`${JAVA_API_PREFIX}/dedupe/history/${resultId}`, { + params: { user_id: getCurrentUserId() }, + })) } -export function runSplit(request: SplitRunRequest) { - return unwrapJavaResponse(post, SplitRunRequest>(`${JAVA_API_PREFIX}/split/run`, request)) +export function runSplit(request: Omit | SplitRunRequest) { + return unwrapJavaResponse(post, SplitRunRequest>(`${JAVA_API_PREFIX}/split/run`, { + ...request, + user_id: getCurrentUserId(), + })) } export function getSplitHistory() { - return unwrapJavaResponse(get>(`${JAVA_API_PREFIX}/split/history`)) + return unwrapJavaResponse(get>(`${JAVA_API_PREFIX}/split/history`, { + params: { user_id: getCurrentUserId() }, + })) } export function deleteSplitHistory(resultId: number) { - return unwrapJavaResponse(del>(`${JAVA_API_PREFIX}/split/history/${resultId}`)) + return unwrapJavaResponse(del>(`${JAVA_API_PREFIX}/split/history/${resultId}`, { + params: { user_id: getCurrentUserId() }, + })) } export function getConvertTemplates() { @@ -175,20 +239,48 @@ export function deleteConvertTemplate(templateCode: string) { return unwrapJavaResponse(del>(`${JAVA_API_PREFIX}/convert/templates/${templateCode}`)) } -export function runConvert(request: ConvertRunRequest) { - return unwrapJavaResponse(post, ConvertRunRequest>(`${JAVA_API_PREFIX}/convert/run`, request)) +export function runConvert(request: Omit | ConvertRunRequest) { + return unwrapJavaResponse(post, ConvertRunRequest>(`${JAVA_API_PREFIX}/convert/run`, { + ...request, + user_id: getCurrentUserId(), + })) } export function getConvertHistory() { - return unwrapJavaResponse(get>(`${JAVA_API_PREFIX}/convert/history`)) + return unwrapJavaResponse(get>(`${JAVA_API_PREFIX}/convert/history`, { + params: { user_id: getCurrentUserId() }, + })) } export function deleteConvertHistory(resultId: number) { - return unwrapJavaResponse(del>(`${JAVA_API_PREFIX}/convert/history/${resultId}`)) + return unwrapJavaResponse(del>(`${JAVA_API_PREFIX}/convert/history/${resultId}`, { + params: { user_id: getCurrentUserId() }, + })) +} + +export function runDeleteBrand(request: Omit | DeleteBrandRunRequest) { + return unwrapJavaResponse(post, DeleteBrandRunRequest>(`${JAVA_API_PREFIX}/delete-brand/run`, { + ...request, + user_id: getCurrentUserId(), + })) +} + +export function getDeleteBrandHistory() { + return unwrapJavaResponse(get>(`${JAVA_API_PREFIX}/delete-brand/history`, { + params: { user_id: getCurrentUserId() }, + })) +} + +export function deleteDeleteBrandHistory(resultId: number) { + return unwrapJavaResponse(del>(`${JAVA_API_PREFIX}/delete-brand/history/${resultId}`, { + params: { user_id: getCurrentUserId() }, + })) } export function getJavaDownloadUrl(path: string) { - return 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('?') ? '&' : '?' + return `${raw}${separator}user_id=${encodeURIComponent(String(getCurrentUserId()))}` } export { JAVA_API_PREFIX, http } diff --git a/frontend-vue/vite.config.ts b/frontend-vue/vite.config.ts index 06f4957..92fc6cc 100644 --- a/frontend-vue/vite.config.ts +++ b/frontend-vue/vite.config.ts @@ -45,6 +45,7 @@ export default defineConfig({ dedupe: resolve(__dirname, 'dedupe.html'), convert: resolve(__dirname, 'convert.html'), split: resolve(__dirname, 'split.html'), + 'delete-brand': resolve(__dirname, 'delete-brand.html'), }, output: { entryFileNames: 'assets/[name].js',