更新后端新增内容
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -32,3 +32,4 @@ backend-java/*.iml
|
|||||||
|
|
||||||
# desktop app
|
# desktop app
|
||||||
desktop/
|
desktop/
|
||||||
|
ERP-Demo/
|
||||||
|
|||||||
BIN
backend-java/sdk-java-5.0.6.jar
Normal file
BIN
backend-java/sdk-java-5.0.6.jar
Normal file
Binary file not shown.
@@ -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<String> moduleTypes = new ArrayList<>(List.of("DEDUPE", "SPLIT", "CONVERT", "DELETE_BRAND"));
|
||||||
|
}
|
||||||
@@ -4,6 +4,6 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
|
|||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
@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 {
|
public class PropertiesConfig {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,13 @@ public class ZiniaoProperties {
|
|||||||
private boolean enabled;
|
private boolean enabled;
|
||||||
private String baseUrl;
|
private String baseUrl;
|
||||||
private String apiKey;
|
private String apiKey;
|
||||||
|
private String appId;
|
||||||
|
private String appSecret;
|
||||||
private Long companyId;
|
private Long companyId;
|
||||||
private String shopsPath;
|
private String appTokenPath;
|
||||||
private String switchShopPath;
|
private String staffListPath;
|
||||||
|
private String userStoresPath;
|
||||||
|
private String userLoginTokenPath;
|
||||||
private String openStoreScheme;
|
private String openStoreScheme;
|
||||||
private String openStoreUserId;
|
private String openStoreUserId;
|
||||||
private String openStoreLaunchUrl;
|
private String openStoreLaunchUrl;
|
||||||
|
|||||||
@@ -124,8 +124,9 @@ public class BrandTaskController {
|
|||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "任务不存在")
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "任务不存在")
|
||||||
})
|
})
|
||||||
public ApiResponse<LegacyBrandTaskDetailVo> getTask(
|
public ApiResponse<LegacyBrandTaskDetailVo> getTask(
|
||||||
@Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId) {
|
@Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId,
|
||||||
return ApiResponse.success(brandTaskService.getTaskDetailLegacy(taskId));
|
@Parameter(description = "用户 ID", required = true) @RequestParam Long userId) {
|
||||||
|
return ApiResponse.success(brandTaskService.getTaskDetailLegacy(taskId, userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/tasks/{taskId}/result")
|
@PostMapping("/tasks/{taskId}/result")
|
||||||
@@ -153,8 +154,9 @@ public class BrandTaskController {
|
|||||||
})
|
})
|
||||||
public ApiResponse<BrandSimpleVo> submitResult(
|
public ApiResponse<BrandSimpleVo> submitResult(
|
||||||
@Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId,
|
@Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId,
|
||||||
|
@Parameter(description = "用户 ID", required = true) @RequestParam Long userId,
|
||||||
@Valid @RequestBody BrandCrawlResultRequest request) {
|
@Valid @RequestBody BrandCrawlResultRequest request) {
|
||||||
brandTaskService.submitCrawlResult(taskId, request);
|
brandTaskService.submitCrawlResult(taskId, userId, request);
|
||||||
return ApiResponse.success(new BrandSimpleVo(true));
|
return ApiResponse.success(new BrandSimpleVo(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,8 +170,9 @@ public class BrandTaskController {
|
|||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "任务不存在或当前状态不可取消")
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "任务不存在或当前状态不可取消")
|
||||||
})
|
})
|
||||||
public ApiResponse<BrandSimpleVo> cancelTask(
|
public ApiResponse<BrandSimpleVo> cancelTask(
|
||||||
@Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId) {
|
@Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId,
|
||||||
brandTaskService.cancelTask(taskId);
|
@Parameter(description = "用户 ID", required = true) @RequestParam Long userId) {
|
||||||
|
brandTaskService.cancelTask(taskId, userId);
|
||||||
return ApiResponse.success(new BrandSimpleVo(true));
|
return ApiResponse.success(new BrandSimpleVo(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,8 +186,9 @@ public class BrandTaskController {
|
|||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "任务不存在或正在执行中不可删除")
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "任务不存在或正在执行中不可删除")
|
||||||
})
|
})
|
||||||
public ApiResponse<BrandSimpleVo> deleteTask(
|
public ApiResponse<BrandSimpleVo> deleteTask(
|
||||||
@Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId) {
|
@Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId,
|
||||||
brandTaskService.deleteTask(taskId);
|
@Parameter(description = "用户 ID", required = true) @RequestParam Long userId) {
|
||||||
|
brandTaskService.deleteTask(taskId, userId);
|
||||||
return ApiResponse.success(new BrandSimpleVo(true));
|
return ApiResponse.success(new BrandSimpleVo(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,8 +202,9 @@ public class BrandTaskController {
|
|||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "任务不存在或无结果可下载")
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "任务不存在或无结果可下载")
|
||||||
})
|
})
|
||||||
public ResponseEntity<Void> download(
|
public ResponseEntity<Void> download(
|
||||||
@Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId) {
|
@Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId,
|
||||||
String url = brandTaskService.resolveDownloadUrl(taskId);
|
@Parameter(description = "用户 ID", required = true) @RequestParam Long userId) {
|
||||||
|
String url = brandTaskService.resolveDownloadUrl(taskId, userId);
|
||||||
return ResponseEntity.status(302)
|
return ResponseEntity.status(302)
|
||||||
.header(HttpHeaders.LOCATION, url)
|
.header(HttpHeaders.LOCATION, url)
|
||||||
.build();
|
.build();
|
||||||
|
|||||||
@@ -133,8 +133,14 @@ public class BrandTaskService {
|
|||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public LegacyBrandTaskDetailVo getTaskDetailLegacy(Long taskId) {
|
public BrandTaskDetailVo getTaskDetail(Long taskId, Long userId) {
|
||||||
BrandCrawlTaskEntity task = requireTask(taskId);
|
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();
|
LegacyBrandTaskDetailVo vo = new LegacyBrandTaskDetailVo();
|
||||||
vo.setTask(toLegacyTaskItem(task));
|
vo.setTask(toLegacyTaskItem(task));
|
||||||
vo.setLine_progress(buildLineProgress(taskId));
|
vo.setLine_progress(buildLineProgress(taskId));
|
||||||
@@ -198,8 +204,8 @@ public class BrandTaskService {
|
|||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void submitCrawlResult(Long taskId, BrandCrawlResultRequest request) {
|
public void submitCrawlResult(Long taskId, Long userId, BrandCrawlResultRequest request) {
|
||||||
BrandCrawlTaskEntity task = requireTask(taskId);
|
BrandCrawlTaskEntity task = requireTask(taskId, userId);
|
||||||
if (STATUS_CANCELLED.equalsIgnoreCase(blankToDefault(task.getStatus(), STATUS_PENDING))) {
|
if (STATUS_CANCELLED.equalsIgnoreCase(blankToDefault(task.getStatus(), STATUS_PENDING))) {
|
||||||
throw new BusinessException("任务已取消");
|
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<BrandCrawlTaskEntity>()
|
int updated = brandCrawlTaskMapper.update(null, new LambdaUpdateWrapper<BrandCrawlTaskEntity>()
|
||||||
.eq(BrandCrawlTaskEntity::getId, taskId)
|
.eq(BrandCrawlTaskEntity::getId, taskId)
|
||||||
|
.eq(BrandCrawlTaskEntity::getUserId, userId)
|
||||||
.in(BrandCrawlTaskEntity::getStatus, STATUS_PENDING, STATUS_RUNNING)
|
.in(BrandCrawlTaskEntity::getStatus, STATUS_PENDING, STATUS_RUNNING)
|
||||||
.set(BrandCrawlTaskEntity::getStatus, STATUS_CANCELLED));
|
.set(BrandCrawlTaskEntity::getStatus, STATUS_CANCELLED));
|
||||||
if (updated == 0) {
|
if (updated == 0) {
|
||||||
@@ -315,17 +323,19 @@ public class BrandTaskService {
|
|||||||
brandTaskProgressCacheService.delete(taskId);
|
brandTaskProgressCacheService.delete(taskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteTask(Long taskId) {
|
public void deleteTask(Long taskId, Long userId) {
|
||||||
|
requireTask(taskId, userId);
|
||||||
int deleted = brandCrawlTaskMapper.delete(new LambdaQueryWrapper<BrandCrawlTaskEntity>()
|
int deleted = brandCrawlTaskMapper.delete(new LambdaQueryWrapper<BrandCrawlTaskEntity>()
|
||||||
.eq(BrandCrawlTaskEntity::getId, taskId)
|
.eq(BrandCrawlTaskEntity::getId, taskId)
|
||||||
|
.eq(BrandCrawlTaskEntity::getUserId, userId)
|
||||||
.ne(BrandCrawlTaskEntity::getStatus, STATUS_RUNNING));
|
.ne(BrandCrawlTaskEntity::getStatus, STATUS_RUNNING));
|
||||||
if (deleted == 0) {
|
if (deleted == 0) {
|
||||||
throw new BusinessException("任务不存在或正在执行中无法删除");
|
throw new BusinessException("任务不存在或正在执行中无法删除");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public String resolveDownloadUrl(Long taskId) {
|
public String resolveDownloadUrl(Long taskId, Long userId) {
|
||||||
BrandCrawlTaskEntity task = requireTask(taskId);
|
BrandCrawlTaskEntity task = requireTask(taskId, userId);
|
||||||
Object raw = parseJsonValue(task.getResultPaths());
|
Object raw = parseJsonValue(task.getResultPaths());
|
||||||
if (!(raw instanceof Map<?, ?> map)) {
|
if (!(raw instanceof Map<?, ?> map)) {
|
||||||
throw new BusinessException("无结果可下载");
|
throw new BusinessException("无结果可下载");
|
||||||
@@ -600,6 +610,17 @@ public class BrandTaskService {
|
|||||||
return entity;
|
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) {
|
private File resolveSourceFile(BrandSourceFileDto sourceFile) {
|
||||||
String fileUrl = sourceFile.getFileUrl();
|
String fileUrl = sourceFile.getFileUrl();
|
||||||
if (fileUrl == null || fileUrl.isBlank()) {
|
if (fileUrl == null || fileUrl.isBlank()) {
|
||||||
@@ -661,11 +682,15 @@ public class BrandTaskService {
|
|||||||
String value = index == null ? "" : normalizeCellText(formatter.formatCellValue(row.getCell(index)));
|
String value = index == null ? "" : normalizeCellText(formatter.formatCellValue(row.getCell(index)));
|
||||||
rowData.put(column, value);
|
rowData.put(column, value);
|
||||||
}
|
}
|
||||||
rowData.put("__rowIndex", rowNum + 1);
|
String brand = normalizeCellText(Objects.toString(rowData.getOrDefault("\u54c1\u724c", ""), ""));
|
||||||
rows.add(rowData);
|
if (brand.isBlank()) {
|
||||||
String brand = normalizeCellText(Objects.toString(rowData.getOrDefault("品牌", ""), ""));
|
rowData.put("__rowIndex", rowNum + 1);
|
||||||
if (!brand.isBlank()) {
|
rows.add(rowData);
|
||||||
uniqueBrands.add(brand);
|
continue;
|
||||||
|
}
|
||||||
|
if (uniqueBrands.add(brand)) {
|
||||||
|
rowData.put("__rowIndex", rowNum + 1);
|
||||||
|
rows.add(rowData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new ParsedBrandFile(sheet.getSheetName(), columns, rows, new ArrayList<>(uniqueBrands));
|
return new ParsedBrandFile(sheet.getSheetName(), columns, rows, new ArrayList<>(uniqueBrands));
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import com.nanri.aiimage.modules.convert.model.vo.ConvertRunVo;
|
|||||||
import com.nanri.aiimage.modules.convert.service.ConvertRunService;
|
import com.nanri.aiimage.modules.convert.service.ConvertRunService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
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.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
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.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -66,35 +68,43 @@ public class ConvertRunController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/history")
|
@GetMapping("/history")
|
||||||
@Operation(summary = "查询格式转换历史", description = "查询最近的格式转换成功记录,用于页面右侧历史下载列表展示。")
|
@Operation(summary = "查询格式转换历史", description = "查询当前用户最近的格式转换成功记录,用于页面右侧历史下载列表展示。")
|
||||||
@ApiResponses({
|
@ApiResponses({
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "查询成功", content = @Content(schema = @Schema(implementation = ConvertHistoryVo.class)))
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "查询成功", content = @Content(schema = @Schema(implementation = ConvertHistoryVo.class)))
|
||||||
})
|
})
|
||||||
public ApiResponse<ConvertHistoryVo> history() {
|
public ApiResponse<ConvertHistoryVo> history(
|
||||||
|
@Parameter(name = "user_id", description = "当前登录用户 ID", required = true, in = ParameterIn.QUERY)
|
||||||
|
@RequestParam("user_id") Long userId) {
|
||||||
ConvertHistoryVo vo = new ConvertHistoryVo();
|
ConvertHistoryVo vo = new ConvertHistoryVo();
|
||||||
vo.setItems(convertRunService.listHistory());
|
vo.setItems(convertRunService.listHistory(userId));
|
||||||
return ApiResponse.success(vo);
|
return ApiResponse.success(vo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/history/{resultId}")
|
@DeleteMapping("/history/{resultId}")
|
||||||
@Operation(summary = "删除格式转换历史", description = "删除一条格式转换历史记录。")
|
@Operation(summary = "删除格式转换历史", description = "删除当前用户的一条格式转换历史记录。")
|
||||||
@ApiResponses({
|
@ApiResponses({
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "删除成功"),
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "删除成功"),
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "结果记录不存在")
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "结果记录不存在")
|
||||||
})
|
})
|
||||||
public ApiResponse<Void> deleteHistory(@Parameter(description = "格式转换结果记录 ID", required = true) @PathVariable Long resultId) {
|
public ApiResponse<Void> deleteHistory(
|
||||||
convertRunService.deleteHistory(resultId);
|
@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);
|
return ApiResponse.success(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/results/{resultId}/download")
|
@GetMapping("/results/{resultId}/download")
|
||||||
@Operation(summary = "下载格式转换结果", description = "根据结果记录 ID 下载已生成的转换结果文件。桌面端前端可继续通过 save_file_from_url 选择保存位置。")
|
@Operation(summary = "下载格式转换结果", description = "根据结果记录 ID 下载当前用户的转换结果文件。桌面端前端可继续通过 save_file_from_url 选择保存位置。")
|
||||||
@ApiResponses({
|
@ApiResponses({
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "返回结果文件流"),
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "返回结果文件流"),
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "结果文件不存在")
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "结果文件不存在")
|
||||||
})
|
})
|
||||||
public ResponseEntity<Resource> download(@Parameter(description = "格式转换结果记录 ID", required = true) @PathVariable Long resultId) {
|
public ResponseEntity<Resource> download(
|
||||||
File resultFile = convertRunService.getResultFile(resultId);
|
@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");
|
String encodedFilename = URLEncoder.encode(resultFile.getName(), StandardCharsets.UTF_8).replaceAll("\\+", "%20");
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package com.nanri.aiimage.modules.convert.model.dto;
|
package com.nanri.aiimage.modules.convert.model.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.NotEmpty;
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -23,4 +25,9 @@ public class ConvertRunRequest {
|
|||||||
|
|
||||||
@Schema(description = "文件夹上传场景下的总压缩包名称(不含扩展名)")
|
@Schema(description = "文件夹上传场景下的总压缩包名称(不含扩展名)")
|
||||||
private String archiveName;
|
private String archiveName;
|
||||||
|
|
||||||
|
@NotNull(message = "user_id 不能为空")
|
||||||
|
@JsonProperty("user_id")
|
||||||
|
@Schema(description = "当前登录用户 ID")
|
||||||
|
private Long userId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,8 @@ public class ConvertRunService {
|
|||||||
task.setTaskMode("IMMEDIATE");
|
task.setTaskMode("IMMEDIATE");
|
||||||
task.setStatus("SUCCESS");
|
task.setStatus("SUCCESS");
|
||||||
task.setSourceFileCount(request.getFiles().size());
|
task.setSourceFileCount(request.getFiles().size());
|
||||||
task.setCreatedBy("system");
|
task.setCreatedBy("user:" + request.getUserId());
|
||||||
|
task.setUserId(request.getUserId());
|
||||||
task.setRequestJson(JSONUtil.toJsonStr(request));
|
task.setRequestJson(JSONUtil.toJsonStr(request));
|
||||||
task.setCreatedAt(LocalDateTime.now());
|
task.setCreatedAt(LocalDateTime.now());
|
||||||
task.setUpdatedAt(LocalDateTime.now());
|
task.setUpdatedAt(LocalDateTime.now());
|
||||||
@@ -130,6 +131,7 @@ public class ConvertRunService {
|
|||||||
resultEntity.setResultFileSize(packagedResultFile.length());
|
resultEntity.setResultFileSize(packagedResultFile.length());
|
||||||
resultEntity.setResultContentType(contentType);
|
resultEntity.setResultContentType(contentType);
|
||||||
resultEntity.setSuccess(1);
|
resultEntity.setSuccess(1);
|
||||||
|
resultEntity.setUserId(request.getUserId());
|
||||||
resultEntity.setCreatedAt(LocalDateTime.now());
|
resultEntity.setCreatedAt(LocalDateTime.now());
|
||||||
fileResultMapper.insert(resultEntity);
|
fileResultMapper.insert(resultEntity);
|
||||||
item.setResultId(resultEntity.getId());
|
item.setResultId(resultEntity.getId());
|
||||||
@@ -150,6 +152,7 @@ public class ConvertRunService {
|
|||||||
resultEntity.setSourceFilename(sourceFile.getOriginalFilename());
|
resultEntity.setSourceFilename(sourceFile.getOriginalFilename());
|
||||||
resultEntity.setSuccess(0);
|
resultEntity.setSuccess(0);
|
||||||
resultEntity.setErrorMessage(ex.getMessage());
|
resultEntity.setErrorMessage(ex.getMessage());
|
||||||
|
resultEntity.setUserId(request.getUserId());
|
||||||
resultEntity.setCreatedAt(LocalDateTime.now());
|
resultEntity.setCreatedAt(LocalDateTime.now());
|
||||||
fileResultMapper.insert(resultEntity);
|
fileResultMapper.insert(resultEntity);
|
||||||
}
|
}
|
||||||
@@ -175,6 +178,7 @@ public class ConvertRunService {
|
|||||||
resultEntity.setResultFileSize(zipFile.length());
|
resultEntity.setResultFileSize(zipFile.length());
|
||||||
resultEntity.setResultContentType("application/zip");
|
resultEntity.setResultContentType("application/zip");
|
||||||
resultEntity.setSuccess(1);
|
resultEntity.setSuccess(1);
|
||||||
|
resultEntity.setUserId(request.getUserId());
|
||||||
resultEntity.setCreatedAt(LocalDateTime.now());
|
resultEntity.setCreatedAt(LocalDateTime.now());
|
||||||
fileResultMapper.insert(resultEntity);
|
fileResultMapper.insert(resultEntity);
|
||||||
item.setResultId(resultEntity.getId());
|
item.setResultId(resultEntity.getId());
|
||||||
@@ -195,9 +199,10 @@ public class ConvertRunService {
|
|||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ConvertResultItemVo> listHistory() {
|
public List<ConvertResultItemVo> listHistory(Long userId) {
|
||||||
return fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
return fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
||||||
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
||||||
|
.eq(FileResultEntity::getUserId, userId)
|
||||||
.orderByDesc(FileResultEntity::getCreatedAt)
|
.orderByDesc(FileResultEntity::getCreatedAt)
|
||||||
.last("limit 200"))
|
.last("limit 200"))
|
||||||
.stream()
|
.stream()
|
||||||
@@ -214,17 +219,17 @@ public class ConvertRunService {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteHistory(Long resultId) {
|
public void deleteHistory(Long resultId, Long userId) {
|
||||||
FileResultEntity entity = fileResultMapper.selectById(resultId);
|
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.");
|
throw new BusinessException("Convert result record does not exist.");
|
||||||
}
|
}
|
||||||
fileResultMapper.deleteById(resultId);
|
fileResultMapper.deleteById(resultId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public File getResultFile(Long resultId) {
|
public File getResultFile(Long resultId, Long userId) {
|
||||||
FileResultEntity entity = fileResultMapper.selectById(resultId);
|
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.");
|
throw new BusinessException("Convert result file does not exist.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import com.nanri.aiimage.modules.dedupe.model.vo.DedupeRunVo;
|
|||||||
import com.nanri.aiimage.modules.dedupe.service.DedupeRunService;
|
import com.nanri.aiimage.modules.dedupe.service.DedupeRunService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
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.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
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.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -60,35 +62,43 @@ public class DedupeRunController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/history")
|
@GetMapping("/history")
|
||||||
@Operation(summary = "查询去重历史", description = "查询最近的去重成功记录,用于页面右侧历史下载列表展示。")
|
@Operation(summary = "查询去重历史", description = "查询当前用户最近的去重成功记录,用于页面右侧历史下载列表展示。")
|
||||||
@ApiResponses({
|
@ApiResponses({
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "查询成功", content = @Content(schema = @Schema(implementation = DedupeHistoryVo.class)))
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "查询成功", content = @Content(schema = @Schema(implementation = DedupeHistoryVo.class)))
|
||||||
})
|
})
|
||||||
public ApiResponse<DedupeHistoryVo> history() {
|
public ApiResponse<DedupeHistoryVo> history(
|
||||||
|
@Parameter(name = "user_id", description = "当前登录用户 ID", required = true, in = ParameterIn.QUERY)
|
||||||
|
@RequestParam("user_id") Long userId) {
|
||||||
DedupeHistoryVo vo = new DedupeHistoryVo();
|
DedupeHistoryVo vo = new DedupeHistoryVo();
|
||||||
vo.setItems(dedupeRunService.listHistory());
|
vo.setItems(dedupeRunService.listHistory(userId));
|
||||||
return ApiResponse.success(vo);
|
return ApiResponse.success(vo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/history/{resultId}")
|
@DeleteMapping("/history/{resultId}")
|
||||||
@Operation(summary = "删除去重历史", description = "删除一条去重历史记录。")
|
@Operation(summary = "删除去重历史", description = "删除当前用户的一条去重历史记录。")
|
||||||
@ApiResponses({
|
@ApiResponses({
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "删除成功"),
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "删除成功"),
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "结果记录不存在")
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "结果记录不存在")
|
||||||
})
|
})
|
||||||
public ApiResponse<Void> deleteHistory(@Parameter(description = "去重结果记录 ID", required = true) @PathVariable Long resultId) {
|
public ApiResponse<Void> deleteHistory(
|
||||||
dedupeRunService.deleteHistory(resultId);
|
@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);
|
return ApiResponse.success(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/results/{resultId}/download")
|
@GetMapping("/results/{resultId}/download")
|
||||||
@Operation(summary = "下载数据去重结果", description = "根据结果记录 ID 下载清洗后的 Excel 文件。")
|
@Operation(summary = "下载数据去重结果", description = "根据结果记录 ID 下载当前用户的清洗后 Excel 文件。")
|
||||||
@ApiResponses({
|
@ApiResponses({
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "返回结果文件流"),
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "返回结果文件流"),
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "结果文件不存在")
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "结果文件不存在")
|
||||||
})
|
})
|
||||||
public ResponseEntity<Resource> download(@Parameter(description = "去重结果记录 ID", required = true) @PathVariable Long resultId) {
|
public ResponseEntity<Resource> download(
|
||||||
Resource resource = dedupeRunService.getResultFile(resultId);
|
@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()
|
return ResponseEntity.ok()
|
||||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment")
|
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment")
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package com.nanri.aiimage.modules.dedupe.model.dto;
|
package com.nanri.aiimage.modules.dedupe.model.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import jakarta.validation.constraints.NotEmpty;
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -31,4 +33,9 @@ public class DedupeRunRequest {
|
|||||||
|
|
||||||
@Schema(description = "文件夹上传场景下的总压缩包名称(不含扩展名)")
|
@Schema(description = "文件夹上传场景下的总压缩包名称(不含扩展名)")
|
||||||
private String archiveName;
|
private String archiveName;
|
||||||
|
|
||||||
|
@NotNull(message = "user_id 不能为空")
|
||||||
|
@JsonProperty("user_id")
|
||||||
|
@Schema(description = "当前登录用户 ID")
|
||||||
|
private Long userId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,8 @@ public class DedupeRunService {
|
|||||||
task.setTaskMode("IMMEDIATE");
|
task.setTaskMode("IMMEDIATE");
|
||||||
task.setStatus("SUCCESS");
|
task.setStatus("SUCCESS");
|
||||||
task.setSourceFileCount(request.getFiles().size());
|
task.setSourceFileCount(request.getFiles().size());
|
||||||
task.setCreatedBy("system");
|
task.setCreatedBy("user:" + request.getUserId());
|
||||||
|
task.setUserId(request.getUserId());
|
||||||
task.setRequestJson(JSONUtil.toJsonStr(request));
|
task.setRequestJson(JSONUtil.toJsonStr(request));
|
||||||
task.setCreatedAt(LocalDateTime.now());
|
task.setCreatedAt(LocalDateTime.now());
|
||||||
task.setUpdatedAt(LocalDateTime.now());
|
task.setUpdatedAt(LocalDateTime.now());
|
||||||
@@ -123,6 +124,7 @@ public class DedupeRunService {
|
|||||||
resultEntity.setResultFileSize(outputFile.length());
|
resultEntity.setResultFileSize(outputFile.length());
|
||||||
resultEntity.setResultContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
resultEntity.setResultContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||||
resultEntity.setSuccess(1);
|
resultEntity.setSuccess(1);
|
||||||
|
resultEntity.setUserId(request.getUserId());
|
||||||
resultEntity.setCreatedAt(LocalDateTime.now());
|
resultEntity.setCreatedAt(LocalDateTime.now());
|
||||||
fileResultMapper.insert(resultEntity);
|
fileResultMapper.insert(resultEntity);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
@@ -136,6 +138,7 @@ public class DedupeRunService {
|
|||||||
resultEntity.setSourceFilename(sourceFile.getOriginalFilename());
|
resultEntity.setSourceFilename(sourceFile.getOriginalFilename());
|
||||||
resultEntity.setSuccess(0);
|
resultEntity.setSuccess(0);
|
||||||
resultEntity.setErrorMessage(ex.getMessage());
|
resultEntity.setErrorMessage(ex.getMessage());
|
||||||
|
resultEntity.setUserId(request.getUserId());
|
||||||
resultEntity.setCreatedAt(LocalDateTime.now());
|
resultEntity.setCreatedAt(LocalDateTime.now());
|
||||||
fileResultMapper.insert(resultEntity);
|
fileResultMapper.insert(resultEntity);
|
||||||
}
|
}
|
||||||
@@ -162,6 +165,7 @@ public class DedupeRunService {
|
|||||||
resultEntity.setResultFileSize(zipFile.length());
|
resultEntity.setResultFileSize(zipFile.length());
|
||||||
resultEntity.setResultContentType("application/zip");
|
resultEntity.setResultContentType("application/zip");
|
||||||
resultEntity.setSuccess(1);
|
resultEntity.setSuccess(1);
|
||||||
|
resultEntity.setUserId(request.getUserId());
|
||||||
resultEntity.setCreatedAt(LocalDateTime.now());
|
resultEntity.setCreatedAt(LocalDateTime.now());
|
||||||
fileResultMapper.insert(resultEntity);
|
fileResultMapper.insert(resultEntity);
|
||||||
item.setResultId(resultEntity.getId());
|
item.setResultId(resultEntity.getId());
|
||||||
@@ -182,9 +186,10 @@ public class DedupeRunService {
|
|||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<DedupeResultItemVo> listHistory() {
|
public List<DedupeResultItemVo> listHistory(Long userId) {
|
||||||
return fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
return fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
||||||
.eq(FileResultEntity::getModuleType, "DEDUPE")
|
.eq(FileResultEntity::getModuleType, "DEDUPE")
|
||||||
|
.eq(FileResultEntity::getUserId, userId)
|
||||||
.orderByDesc(FileResultEntity::getCreatedAt)
|
.orderByDesc(FileResultEntity::getCreatedAt)
|
||||||
.last("limit 50"))
|
.last("limit 50"))
|
||||||
.stream()
|
.stream()
|
||||||
@@ -201,17 +206,17 @@ public class DedupeRunService {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteHistory(Long resultId) {
|
public void deleteHistory(Long resultId, Long userId) {
|
||||||
FileResultEntity entity = fileResultMapper.selectById(resultId);
|
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("记录不存在");
|
throw new BusinessException("记录不存在");
|
||||||
}
|
}
|
||||||
fileResultMapper.deleteById(resultId);
|
fileResultMapper.deleteById(resultId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Resource getResultFile(Long resultId) {
|
public Resource getResultFile(Long resultId, Long userId) {
|
||||||
FileResultEntity entity = fileResultMapper.selectById(resultId);
|
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("结果文件不存在");
|
throw new BusinessException("结果文件不存在");
|
||||||
}
|
}
|
||||||
File file = new File(entity.getResultFileUrl());
|
File file = new File(entity.getResultFileUrl());
|
||||||
|
|||||||
@@ -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<DeleteBrandRunVo> run(@Valid @RequestBody DeleteBrandRunRequest request) {
|
||||||
|
return ApiResponse.success(deleteBrandRunService.run(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/history")
|
||||||
|
@Operation(summary = "查询删除品牌历史", description = "按 user_id 查询删除品牌历史。")
|
||||||
|
public ApiResponse<DeleteBrandHistoryVo> 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<Void> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<DeleteBrandSourceFileDto> files;
|
||||||
|
|
||||||
|
@NotNull(message = "user_id 不能为空")
|
||||||
|
@JsonProperty("user_id")
|
||||||
|
@Schema(description = "当前登录用户 ID")
|
||||||
|
private Long userId;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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<DeleteBrandResultItemVo> items = new ArrayList<>();
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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<DeleteBrandPreviewRowVo> previewRows = new ArrayList<>();
|
||||||
|
|
||||||
|
@Schema(description = "是否成功")
|
||||||
|
private boolean success;
|
||||||
|
|
||||||
|
@Schema(description = "错误信息")
|
||||||
|
private String error;
|
||||||
|
}
|
||||||
@@ -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<DeleteBrandResultItemVo> items = new ArrayList<>();
|
||||||
|
}
|
||||||
@@ -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<ZiniaoShopCacheDto> shops = ziniaoAuthService.listShopsForConfiguredUser();
|
||||||
|
List<DeleteBrandResultItemVo> 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<FileResultEntity>()
|
||||||
|
.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<CountryColumnPair> pairs = resolveCountryPairs(titleRow, headerRow, formatter);
|
||||||
|
if (pairs.isEmpty()) {
|
||||||
|
throw new BusinessException("未识别到删除品牌表头");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DeleteBrandPreviewRowVo> 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<CountryColumnPair> resolveCountryPairs(Row titleRow, Row headerRow, DataFormatter formatter) {
|
||||||
|
List<CountryColumnPair> 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<ZiniaoShopCacheDto> shops) {
|
||||||
|
if (shopName == null || shopName.isBlank() || shops == null || shops.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String normalized = normalizeShopName(shopName);
|
||||||
|
for (ZiniaoShopCacheDto shop : shops) {
|
||||||
|
if (normalized.equals(normalizeShopName(shop.getShopName()))) {
|
||||||
|
return shop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveShopName(DeleteBrandSourceFileDto sourceFile) {
|
||||||
|
String name = sourceFile.getOriginalFilename();
|
||||||
|
if (name == null || name.isBlank()) {
|
||||||
|
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<DeleteBrandPreviewRowVo> previewRows) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import com.nanri.aiimage.modules.split.model.vo.SplitRunVo;
|
|||||||
import com.nanri.aiimage.modules.split.service.SplitRunService;
|
import com.nanri.aiimage.modules.split.service.SplitRunService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
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.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
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.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -59,35 +61,43 @@ public class SplitRunController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/history")
|
@GetMapping("/history")
|
||||||
@Operation(summary = "查询数据拆分历史", description = "查询最近的数据拆分成功记录,用于页面右侧历史下载列表展示。")
|
@Operation(summary = "查询数据拆分历史", description = "查询当前用户最近的数据拆分成功记录,用于页面右侧历史下载列表展示。")
|
||||||
@ApiResponses({
|
@ApiResponses({
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "查询成功", content = @Content(schema = @Schema(implementation = SplitHistoryVo.class)))
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "查询成功", content = @Content(schema = @Schema(implementation = SplitHistoryVo.class)))
|
||||||
})
|
})
|
||||||
public ApiResponse<SplitHistoryVo> history() {
|
public ApiResponse<SplitHistoryVo> history(
|
||||||
|
@Parameter(name = "user_id", description = "当前登录用户 ID", required = true, in = ParameterIn.QUERY)
|
||||||
|
@RequestParam("user_id") Long userId) {
|
||||||
SplitHistoryVo vo = new SplitHistoryVo();
|
SplitHistoryVo vo = new SplitHistoryVo();
|
||||||
vo.setItems(splitRunService.listHistory());
|
vo.setItems(splitRunService.listHistory(userId));
|
||||||
return ApiResponse.success(vo);
|
return ApiResponse.success(vo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/history/{resultId}")
|
@DeleteMapping("/history/{resultId}")
|
||||||
@Operation(summary = "删除数据拆分历史", description = "删除一条数据拆分历史记录。")
|
@Operation(summary = "删除数据拆分历史", description = "删除当前用户的一条数据拆分历史记录。")
|
||||||
@ApiResponses({
|
@ApiResponses({
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "删除成功"),
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "删除成功"),
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "结果记录不存在")
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "结果记录不存在")
|
||||||
})
|
})
|
||||||
public ApiResponse<Void> deleteHistory(@Parameter(description = "数据拆分结果记录 ID", required = true) @PathVariable Long resultId) {
|
public ApiResponse<Void> deleteHistory(
|
||||||
splitRunService.deleteHistory(resultId);
|
@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);
|
return ApiResponse.success(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/results/{resultId}/download")
|
@GetMapping("/results/{resultId}/download")
|
||||||
@Operation(summary = "下载数据拆分结果", description = "根据结果记录 ID 下载拆分后的 Excel 文件。")
|
@Operation(summary = "下载数据拆分结果", description = "根据结果记录 ID 下载当前用户的拆分结果文件。")
|
||||||
@ApiResponses({
|
@ApiResponses({
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "返回结果文件流"),
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "返回结果文件流"),
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "结果文件不存在")
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "结果文件不存在")
|
||||||
})
|
})
|
||||||
public ResponseEntity<Resource> download(@Parameter(description = "数据拆分结果记录 ID", required = true) @PathVariable Long resultId) {
|
public ResponseEntity<Resource> download(
|
||||||
Resource resource = splitRunService.getResultFile(resultId);
|
@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()
|
return ResponseEntity.ok()
|
||||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment")
|
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment")
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package com.nanri.aiimage.modules.split.model.dto;
|
package com.nanri.aiimage.modules.split.model.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.NotEmpty;
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -33,4 +35,9 @@ public class SplitRunRequest {
|
|||||||
|
|
||||||
@Schema(description = "文件夹上传场景下的总压缩包名称(不含扩展名)")
|
@Schema(description = "文件夹上传场景下的总压缩包名称(不含扩展名)")
|
||||||
private String archiveName;
|
private String archiveName;
|
||||||
|
|
||||||
|
@NotNull(message = "user_id 不能为空")
|
||||||
|
@JsonProperty("user_id")
|
||||||
|
@Schema(description = "当前登录用户 ID")
|
||||||
|
private Long userId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ public class SplitRunService {
|
|||||||
task.setTaskMode("IMMEDIATE");
|
task.setTaskMode("IMMEDIATE");
|
||||||
task.setStatus("SUCCESS");
|
task.setStatus("SUCCESS");
|
||||||
task.setSourceFileCount(request.getFiles().size());
|
task.setSourceFileCount(request.getFiles().size());
|
||||||
task.setCreatedBy("system");
|
task.setCreatedBy("user:" + request.getUserId());
|
||||||
|
task.setUserId(request.getUserId());
|
||||||
task.setRequestJson(JSONUtil.toJsonStr(request));
|
task.setRequestJson(JSONUtil.toJsonStr(request));
|
||||||
task.setCreatedAt(LocalDateTime.now());
|
task.setCreatedAt(LocalDateTime.now());
|
||||||
task.setUpdatedAt(LocalDateTime.now());
|
task.setUpdatedAt(LocalDateTime.now());
|
||||||
@@ -113,6 +114,7 @@ public class SplitRunService {
|
|||||||
resultEntity.setSourceFilename(sourceFile.getOriginalFilename());
|
resultEntity.setSourceFilename(sourceFile.getOriginalFilename());
|
||||||
resultEntity.setSuccess(0);
|
resultEntity.setSuccess(0);
|
||||||
resultEntity.setErrorMessage(ex.getMessage());
|
resultEntity.setErrorMessage(ex.getMessage());
|
||||||
|
resultEntity.setUserId(request.getUserId());
|
||||||
resultEntity.setCreatedAt(LocalDateTime.now());
|
resultEntity.setCreatedAt(LocalDateTime.now());
|
||||||
fileResultMapper.insert(resultEntity);
|
fileResultMapper.insert(resultEntity);
|
||||||
}
|
}
|
||||||
@@ -144,6 +146,7 @@ public class SplitRunService {
|
|||||||
resultEntity.setResultContentType(ZIP_CONTENT_TYPE);
|
resultEntity.setResultContentType(ZIP_CONTENT_TYPE);
|
||||||
resultEntity.setRowCount(item.getRowCount());
|
resultEntity.setRowCount(item.getRowCount());
|
||||||
resultEntity.setSuccess(1);
|
resultEntity.setSuccess(1);
|
||||||
|
resultEntity.setUserId(request.getUserId());
|
||||||
resultEntity.setCreatedAt(LocalDateTime.now());
|
resultEntity.setCreatedAt(LocalDateTime.now());
|
||||||
fileResultMapper.insert(resultEntity);
|
fileResultMapper.insert(resultEntity);
|
||||||
item.setResultId(resultEntity.getId());
|
item.setResultId(resultEntity.getId());
|
||||||
@@ -164,9 +167,10 @@ public class SplitRunService {
|
|||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<SplitResultItemVo> listHistory() {
|
public List<SplitResultItemVo> listHistory(Long userId) {
|
||||||
return fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
return fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
||||||
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
||||||
|
.eq(FileResultEntity::getUserId, userId)
|
||||||
.orderByDesc(FileResultEntity::getCreatedAt)
|
.orderByDesc(FileResultEntity::getCreatedAt)
|
||||||
.last("limit 100"))
|
.last("limit 100"))
|
||||||
.stream()
|
.stream()
|
||||||
@@ -186,17 +190,17 @@ public class SplitRunService {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteHistory(Long resultId) {
|
public void deleteHistory(Long resultId, Long userId) {
|
||||||
FileResultEntity entity = fileResultMapper.selectById(resultId);
|
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.");
|
throw new BusinessException("Split result record does not exist.");
|
||||||
}
|
}
|
||||||
fileResultMapper.deleteById(resultId);
|
fileResultMapper.deleteById(resultId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Resource getResultFile(Long resultId) {
|
public Resource getResultFile(Long resultId, Long userId) {
|
||||||
FileResultEntity entity = fileResultMapper.selectById(resultId);
|
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.");
|
throw new BusinessException("Split result file does not exist.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,5 +24,6 @@ public class FileResultEntity {
|
|||||||
private Integer rowCount;
|
private Integer rowCount;
|
||||||
private Integer success;
|
private Integer success;
|
||||||
private String errorMessage;
|
private String errorMessage;
|
||||||
|
private Long userId;
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public class FileTaskEntity {
|
|||||||
private String resultJson;
|
private String resultJson;
|
||||||
private String errorMessage;
|
private String errorMessage;
|
||||||
private String createdBy;
|
private String createdBy;
|
||||||
|
private Long userId;
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
private LocalDateTime finishedAt;
|
private LocalDateTime finishedAt;
|
||||||
|
|||||||
@@ -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<String> moduleTypes = moduleCleanupProperties.getModuleTypes();
|
||||||
|
if (moduleTypes == null || moduleTypes.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int deletedResults = fileResultMapper.delete(new LambdaQueryWrapper<FileResultEntity>()
|
||||||
|
.in(FileResultEntity::getModuleType, moduleTypes));
|
||||||
|
|
||||||
|
int resetTasks = fileTaskMapper.update(null, new LambdaUpdateWrapper<FileTaskEntity>()
|
||||||
|
.in(FileTaskEntity::getModuleType, moduleTypes)
|
||||||
|
.set(FileTaskEntity::getResultJson, null)
|
||||||
|
.set(FileTaskEntity::getRequestJson, null)
|
||||||
|
.set(FileTaskEntity::getErrorMessage, null));
|
||||||
|
|
||||||
|
int deletedTasks = fileTaskMapper.delete(new LambdaQueryWrapper<FileTaskEntity>()
|
||||||
|
.in(FileTaskEntity::getModuleType, moduleTypes));
|
||||||
|
|
||||||
|
log.info("模块历史清理完成: moduleTypes={}, deletedResults={}, resetTasks={}, deletedTasks={}", moduleTypes, deletedResults, resetTasks, deletedTasks);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,18 @@
|
|||||||
package com.nanri.aiimage.modules.ziniao.client;
|
package com.nanri.aiimage.modules.ziniao.client;
|
||||||
|
|
||||||
import com.nanri.aiimage.modules.ziniao.model.cache.ZiniaoShopCacheDto;
|
import com.nanri.aiimage.modules.ziniao.model.cache.ZiniaoShopCacheDto;
|
||||||
|
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoStaffItemVo;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public interface ZiniaoClient {
|
public interface ZiniaoClient {
|
||||||
List<ZiniaoShopCacheDto> listShops();
|
String getAppToken();
|
||||||
|
|
||||||
|
Long getCompanyIdByApiKey();
|
||||||
|
|
||||||
|
List<ZiniaoStaffItemVo> listStaff(Long companyId);
|
||||||
|
|
||||||
|
List<ZiniaoShopCacheDto> listUserStores(Long companyId, Long userId, String userToken);
|
||||||
|
|
||||||
|
String getUserLoginToken(Long companyId, Long userId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import com.nanri.aiimage.common.exception.BusinessException;
|
import com.nanri.aiimage.common.exception.BusinessException;
|
||||||
import com.nanri.aiimage.config.ZiniaoProperties;
|
import com.nanri.aiimage.config.ZiniaoProperties;
|
||||||
import com.nanri.aiimage.modules.ziniao.model.cache.ZiniaoShopCacheDto;
|
import com.nanri.aiimage.modules.ziniao.model.cache.ZiniaoShopCacheDto;
|
||||||
|
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoStaffItemVo;
|
||||||
|
import com.nanri.aiimage.modules.ziniao.service.ZiniaoSessionCacheService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||||
@@ -13,6 +15,7 @@ import org.springframework.web.client.RestClient;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@@ -21,22 +24,222 @@ public class ZiniaoClientImpl implements ZiniaoClient {
|
|||||||
|
|
||||||
private final ZiniaoProperties ziniaoProperties;
|
private final ZiniaoProperties ziniaoProperties;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
private final ZiniaoSessionCacheService ziniaoSessionCacheService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ZiniaoShopCacheDto> listShops() {
|
public String getAppToken() {
|
||||||
if (ziniaoProperties.getCompanyId() == null || ziniaoProperties.getCompanyId() <= 0) {
|
String cachedToken = ziniaoSessionCacheService.getAppToken();
|
||||||
throw new BusinessException("紫鸟 companyId 未配置");
|
if (cachedToken != null && !cachedToken.isBlank()) {
|
||||||
|
return cachedToken;
|
||||||
}
|
}
|
||||||
String raw = getRestClient().post()
|
String raw = postWithApiKeyEmptyJsonBody(ziniaoProperties.getAppTokenPath(), "获取 appToken");
|
||||||
.uri(joinUrl(ziniaoProperties.getBaseUrl(), ziniaoProperties.getShopsPath()))
|
try {
|
||||||
.headers(headers -> {
|
JsonNode root = objectMapper.readTree(raw);
|
||||||
headers.setBearerAuth(ziniaoProperties.getApiKey());
|
JsonNode data = firstNonNull(root.get("data"), root.get("result"), root);
|
||||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
String token = text(firstNonNull(
|
||||||
})
|
data == null ? null : data.get("appAuthToken"),
|
||||||
.body(java.util.Map.of("companyId", ziniaoProperties.getCompanyId()))
|
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<ZiniaoStaffItemVo> 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<ZiniaoStaffItemVo> 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<ZiniaoShopCacheDto> 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()
|
.retrieve()
|
||||||
.body(String.class);
|
.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<String, Object> 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<String, Object> 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() {
|
private RestClient getRestClient() {
|
||||||
@@ -46,21 +249,20 @@ public class ZiniaoClientImpl implements ZiniaoClient {
|
|||||||
return RestClient.builder().requestFactory(requestFactory).build();
|
return RestClient.builder().requestFactory(requestFactory).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void addStaffItem(List<ZiniaoStaffItemVo> 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<ZiniaoShopCacheDto> parseShops(String raw) {
|
private List<ZiniaoShopCacheDto> parseShops(String raw) {
|
||||||
try {
|
try {
|
||||||
JsonNode root = objectMapper.readTree(raw);
|
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");
|
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");
|
JsonNode itemsNode = wrapper == null ? null : wrapper.get("data");
|
||||||
List<ZiniaoShopCacheDto> items = new ArrayList<>();
|
List<ZiniaoShopCacheDto> items = new ArrayList<>();
|
||||||
if (itemsNode != null && itemsNode.isArray()) {
|
if (itemsNode != null && itemsNode.isArray()) {
|
||||||
@@ -75,12 +277,33 @@ public class ZiniaoClientImpl implements ZiniaoClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
} catch (BusinessException ex) {
|
|
||||||
throw ex;
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
throw new BusinessException("解析紫鸟店铺列表失败");
|
throw new BusinessException("解析紫鸟店铺列表失败");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private List<ZiniaoShopCacheDto> parseUserStores(String raw) {
|
||||||
|
try {
|
||||||
|
JsonNode root = objectMapper.readTree(raw);
|
||||||
|
JsonNode itemsNode = firstNonNull(root.get("data"), root.get("result"));
|
||||||
|
List<ZiniaoShopCacheDto> 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) {
|
private String joinUrl(String baseUrl, String path) {
|
||||||
String base = Objects.toString(baseUrl, "").trim();
|
String base = Objects.toString(baseUrl, "").trim();
|
||||||
@@ -121,4 +344,11 @@ public class ZiniaoClientImpl implements ZiniaoClient {
|
|||||||
return null;
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
package com.nanri.aiimage.modules.ziniao.controller;
|
package com.nanri.aiimage.modules.ziniao.controller;
|
||||||
|
|
||||||
import com.nanri.aiimage.common.api.ApiResponse;
|
import com.nanri.aiimage.common.api.ApiResponse;
|
||||||
import com.nanri.aiimage.modules.ziniao.model.dto.ZiniaoSwitchShopRequest;
|
import com.nanri.aiimage.modules.ziniao.model.dto.ZiniaoOpenShopRequest;
|
||||||
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoCurrentShopVo;
|
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoOpenShopVo;
|
||||||
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoLoginUrlVo;
|
|
||||||
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoSessionVo;
|
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoSessionVo;
|
||||||
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoShopListVo;
|
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoShopListVo;
|
||||||
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoSwitchShopVo;
|
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoStaffListVo;
|
||||||
import com.nanri.aiimage.modules.ziniao.service.ZiniaoAuthService;
|
import com.nanri.aiimage.modules.ziniao.service.ZiniaoAuthService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
@@ -15,8 +14,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
@@ -27,71 +24,44 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
@RestController
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@RequestMapping("/api/ziniao")
|
@RequestMapping("/api/ziniao")
|
||||||
@Tag(name = "紫鸟接入", description = "紫鸟 ApiKey 模式下的配置状态、店铺列表、当前店铺与切换店铺接口")
|
@Tag(name = "紫鸟接入", description = "基于 API Key 直连紫鸟接口,获取 appToken、员工、店铺并生成打开店铺链接")
|
||||||
public class ZiniaoAuthController {
|
public class ZiniaoAuthController {
|
||||||
|
|
||||||
private final ZiniaoAuthService ziniaoAuthService;
|
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<ZiniaoLoginUrlVo> 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<Void> 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")
|
@GetMapping("/session")
|
||||||
@Operation(summary = "获取紫鸟接入状态", description = "根据 sessionId 获取当前 ApiKey 模式会话与当前店铺信息。")
|
@Operation(summary = "获取紫鸟会话状态", description = "自动获取或复用 appToken,并返回 companyId、当前员工 userId、脱敏 token 和当前店铺信息。")
|
||||||
@ApiResponses({
|
@ApiResponses({
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "获取成功", content = @Content(schema = @Schema(implementation = ZiniaoSessionVo.class)))
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "获取成功", content = @Content(schema = @Schema(implementation = ZiniaoSessionVo.class)))
|
||||||
})
|
})
|
||||||
public ApiResponse<ZiniaoSessionVo> getSession(@RequestParam String sessionId) {
|
public ApiResponse<ZiniaoSessionVo> getSession(@RequestParam(required = false) String sessionId, @RequestParam(required = false) Long userId) {
|
||||||
return ApiResponse.success(ziniaoAuthService.getSession(sessionId));
|
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<ZiniaoStaffListVo> listStaff() {
|
||||||
|
return ApiResponse.success(ziniaoAuthService.listStaff());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/shops")
|
@GetMapping("/shops")
|
||||||
@Operation(summary = "获取紫鸟店铺列表", description = "根据 sessionId 使用配置的 ApiKey 与 companyId 拉取最新店铺列表。")
|
@Operation(summary = "获取员工可见店铺列表", description = "先通过 API Key 获取 companyId,再按员工 userId 查询该员工有权限的店铺列表,并缓存到当前 session。")
|
||||||
@ApiResponses({
|
@ApiResponses({
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "获取成功", content = @Content(schema = @Schema(implementation = ZiniaoShopListVo.class)))
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "获取成功", content = @Content(schema = @Schema(implementation = ZiniaoShopListVo.class)))
|
||||||
})
|
})
|
||||||
public ApiResponse<ZiniaoShopListVo> listShops(@RequestParam String sessionId) {
|
public ApiResponse<ZiniaoShopListVo> listShops(@RequestParam String sessionId, @RequestParam Long userId) {
|
||||||
return ApiResponse.success(ziniaoAuthService.listShops(sessionId));
|
return ApiResponse.success(ziniaoAuthService.listShops(sessionId, userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/shops/current")
|
@PostMapping("/shops/open")
|
||||||
@Operation(summary = "获取当前紫鸟店铺", description = "根据 sessionId 获取当前已选中的店铺。")
|
@Operation(summary = "生成店铺打开链接", description = "按指定员工 userId 和 shopId 获取员工登录 token,并返回可直接拉起紫鸟客户端的 openStoreUrl。")
|
||||||
@ApiResponses({
|
@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<ZiniaoCurrentShopVo> getCurrentShop(@RequestParam String sessionId) {
|
public ApiResponse<ZiniaoOpenShopVo> openShop(@Valid @RequestBody ZiniaoOpenShopRequest request) {
|
||||||
return ApiResponse.success(ziniaoAuthService.getCurrentShop(sessionId));
|
return ApiResponse.success(ziniaoAuthService.openShop(request));
|
||||||
}
|
|
||||||
|
|
||||||
@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<ZiniaoSwitchShopVo> switchShop(@Valid @RequestBody ZiniaoSwitchShopRequest request) {
|
|
||||||
return ApiResponse.success(ziniaoAuthService.switchShop(request));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,5 +11,7 @@ public class ZiniaoSessionCacheDto {
|
|||||||
private Long expireAt;
|
private Long expireAt;
|
||||||
private String ziniaoUserId;
|
private String ziniaoUserId;
|
||||||
private String nickname;
|
private String nickname;
|
||||||
|
private Long companyId;
|
||||||
|
private Long currentUserId;
|
||||||
private String defaultShopId;
|
private String defaultShopId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.nanri.aiimage.modules.ziniao.model.dto;
|
package com.nanri.aiimage.modules.ziniao.model.dto;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@@ -8,6 +9,9 @@ public class ZiniaoSwitchShopRequest {
|
|||||||
@NotBlank(message = "sessionId 不能为空")
|
@NotBlank(message = "sessionId 不能为空")
|
||||||
private String sessionId;
|
private String sessionId;
|
||||||
|
|
||||||
|
@NotNull(message = "userId 不能为空")
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
@NotBlank(message = "shopId 不能为空")
|
@NotBlank(message = "shopId 不能为空")
|
||||||
private String shopId;
|
private String shopId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -9,15 +9,30 @@ public class ZiniaoSessionVo {
|
|||||||
@Schema(description = "会话 ID")
|
@Schema(description = "会话 ID")
|
||||||
private String sessionId;
|
private String sessionId;
|
||||||
|
|
||||||
@Schema(description = "是否已认证")
|
@Schema(description = "是否已启用")
|
||||||
|
private Boolean enabled;
|
||||||
|
|
||||||
|
@Schema(description = "是否已获取应用 token")
|
||||||
private Boolean authenticated;
|
private Boolean authenticated;
|
||||||
|
|
||||||
|
@Schema(description = "公司 ID")
|
||||||
|
private Long companyId;
|
||||||
|
|
||||||
|
@Schema(description = "当前员工 userId")
|
||||||
|
private Long currentUserId;
|
||||||
|
|
||||||
@Schema(description = "紫鸟用户 ID")
|
@Schema(description = "紫鸟用户 ID")
|
||||||
private String ziniaoUserId;
|
private String ziniaoUserId;
|
||||||
|
|
||||||
@Schema(description = "紫鸟昵称")
|
@Schema(description = "紫鸟昵称")
|
||||||
private String nickname;
|
private String nickname;
|
||||||
|
|
||||||
|
@Schema(description = "脱敏后的 appToken")
|
||||||
|
private String appToken;
|
||||||
|
|
||||||
|
@Schema(description = "token 类型")
|
||||||
|
private String tokenType;
|
||||||
|
|
||||||
@Schema(description = "会话过期时间戳(毫秒)")
|
@Schema(description = "会话过期时间戳(毫秒)")
|
||||||
private Long expireAt;
|
private Long expireAt;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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<ZiniaoStaffItemVo> items = new ArrayList<>();
|
||||||
|
}
|
||||||
@@ -5,22 +5,22 @@ import com.nanri.aiimage.config.ZiniaoProperties;
|
|||||||
import com.nanri.aiimage.modules.ziniao.client.ZiniaoClient;
|
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.ZiniaoSessionCacheDto;
|
||||||
import com.nanri.aiimage.modules.ziniao.model.cache.ZiniaoShopCacheDto;
|
import com.nanri.aiimage.modules.ziniao.model.cache.ZiniaoShopCacheDto;
|
||||||
import com.nanri.aiimage.modules.ziniao.model.dto.ZiniaoSwitchShopRequest;
|
import com.nanri.aiimage.modules.ziniao.model.dto.ZiniaoOpenShopRequest;
|
||||||
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoCurrentShopVo;
|
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoOpenShopVo;
|
||||||
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoLoginUrlVo;
|
|
||||||
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoSessionVo;
|
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoSessionVo;
|
||||||
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoShopItemVo;
|
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.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 lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.util.UriComponentsBuilder;
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.Base64;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -30,163 +30,150 @@ public class ZiniaoAuthService {
|
|||||||
private final ZiniaoClient ziniaoClient;
|
private final ZiniaoClient ziniaoClient;
|
||||||
private final ZiniaoSessionCacheService ziniaoSessionCacheService;
|
private final ZiniaoSessionCacheService ziniaoSessionCacheService;
|
||||||
|
|
||||||
public ZiniaoLoginUrlVo getLoginUrl() {
|
public ZiniaoSessionVo getSession(String sessionId, Long userId) {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
String sessionId = ensureSession();
|
ZiniaoSessionCacheDto session = sessionId == null || sessionId.isBlank() ? initSession(userId) : requireOrInitSession(sessionId, userId);
|
||||||
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<ZiniaoShopCacheDto> shops = ziniaoSessionCacheService.getShops(sessionId);
|
|
||||||
ZiniaoShopCacheDto currentShop = getOrInitCurrentShop(sessionId, shops);
|
|
||||||
ZiniaoSessionVo vo = new ZiniaoSessionVo();
|
ZiniaoSessionVo vo = new ZiniaoSessionVo();
|
||||||
vo.setSessionId(session.getSessionId());
|
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.setZiniaoUserId(session.getZiniaoUserId());
|
||||||
vo.setNickname(session.getNickname());
|
vo.setNickname(session.getNickname());
|
||||||
|
vo.setAppToken(session.getAccessToken());
|
||||||
|
vo.setTokenType(session.getTokenType());
|
||||||
vo.setExpireAt(session.getExpireAt());
|
vo.setExpireAt(session.getExpireAt());
|
||||||
vo.setShopCount(shops.size());
|
vo.setShopCount(ziniaoSessionCacheService.getShops(session.getSessionId()).size());
|
||||||
if (currentShop != null) {
|
vo.setCurrentShopId(session.getDefaultShopId());
|
||||||
vo.setCurrentShopId(currentShop.getShopId());
|
vo.setCurrentShopName(resolveCurrentShopName(session.getSessionId(), session.getDefaultShopId()));
|
||||||
vo.setCurrentShopName(currentShop.getShopName());
|
|
||||||
}
|
|
||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ZiniaoShopListVo listShops(String sessionId) {
|
public ZiniaoStaffListVo listStaff() {
|
||||||
requireSession(sessionId);
|
ensureEnabled();
|
||||||
List<ZiniaoShopCacheDto> shops = ziniaoClient.listShops();
|
ZiniaoStaffListVo vo = new ZiniaoStaffListVo();
|
||||||
ziniaoSessionCacheService.saveShops(sessionId, shops);
|
Long companyId = resolveCompanyIdForStaff();
|
||||||
ZiniaoShopCacheDto currentShop = getOrInitCurrentShop(sessionId, shops);
|
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<ZiniaoShopCacheDto> 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();
|
ZiniaoShopListVo vo = new ZiniaoShopListVo();
|
||||||
for (ZiniaoShopCacheDto shop : shops) {
|
for (ZiniaoShopCacheDto shop : shops) {
|
||||||
ZiniaoShopItemVo item = new ZiniaoShopItemVo();
|
ZiniaoShopItemVo item = new ZiniaoShopItemVo();
|
||||||
item.setShopId(shop.getShopId());
|
item.setShopId(shop.getShopId());
|
||||||
item.setShopName(shop.getShopName());
|
item.setShopName(shop.getShopName());
|
||||||
item.setPlatform(shop.getPlatform());
|
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);
|
vo.getItems().add(item);
|
||||||
}
|
}
|
||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ZiniaoCurrentShopVo getCurrentShop(String sessionId) {
|
public ZiniaoOpenShopVo openShop(ZiniaoOpenShopRequest request) {
|
||||||
requireSession(sessionId);
|
ZiniaoSessionCacheDto session = requireOrInitSession(request.getSessionId(), request.getUserId());
|
||||||
ZiniaoShopCacheDto currentShop = getOrInitCurrentShop(sessionId, ziniaoSessionCacheService.getShops(sessionId));
|
Long currentUserId = requireUserId(request.getUserId() != null ? request.getUserId() : session.getCurrentUserId());
|
||||||
if (currentShop == null) {
|
String userToken = ziniaoClient.getUserLoginToken(resolveCompanyId(), currentUserId);
|
||||||
throw new BusinessException("当前会话没有可用店铺");
|
List<ZiniaoShopCacheDto> 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<ZiniaoShopCacheDto> shops = ziniaoSessionCacheService.getShops(request.getSessionId());
|
|
||||||
ZiniaoShopCacheDto targetShop = shops.stream()
|
ZiniaoShopCacheDto targetShop = shops.stream()
|
||||||
.filter(item -> item.getShopId() != null && item.getShopId().equals(request.getShopId()))
|
.filter(item -> item.getShopId() != null && item.getShopId().equals(request.getShopId()))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElseThrow(() -> new BusinessException("店铺不存在或不属于当前会话"));
|
.orElseThrow(() -> new BusinessException("店铺不存在或不属于当前员工"));
|
||||||
targetShop.setSelectedAt(Instant.now().toEpochMilli());
|
session.setCurrentUserId(currentUserId);
|
||||||
ziniaoSessionCacheService.saveCurrentShop(request.getSessionId(), targetShop);
|
session.setDefaultShopId(targetShop.getShopId());
|
||||||
ZiniaoSwitchShopVo vo = new ZiniaoSwitchShopVo();
|
ziniaoSessionCacheService.saveSession(session);
|
||||||
vo.setSuccess(true);
|
ziniaoSessionCacheService.saveCurrentShop(session.getSessionId(), targetShop);
|
||||||
vo.setCurrentShopId(targetShop.getShopId());
|
|
||||||
vo.setCurrentShopName(targetShop.getShopName());
|
ZiniaoOpenShopVo vo = new ZiniaoOpenShopVo();
|
||||||
vo.setOpenStoreUrl(buildOpenStoreUrl(targetShop));
|
vo.setUserId(currentUserId);
|
||||||
|
vo.setShopId(targetShop.getShopId());
|
||||||
|
vo.setShopName(targetShop.getShopName());
|
||||||
|
vo.setLoginToken(userToken);
|
||||||
|
vo.setOpenStoreUrl(buildOpenStoreUrl(targetShop.getShopId(), currentUserId, userToken));
|
||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String buildFailureRedirect(String message) {
|
public List<ZiniaoShopCacheDto> listShopsForConfiguredUser() {
|
||||||
return ziniaoProperties.getBaseUrl() + "?message=" + (message == null || message.isBlank() ? "紫鸟授权失败" : message);
|
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("-", "");
|
String sessionId = "ziniao_" + UUID.randomUUID().toString().replace("-", "");
|
||||||
ZiniaoSessionCacheDto session = new ZiniaoSessionCacheDto();
|
ZiniaoSessionCacheDto session = new ZiniaoSessionCacheDto();
|
||||||
session.setSessionId(sessionId);
|
session.setSessionId(sessionId);
|
||||||
session.setAccessToken(maskApiKey(ziniaoProperties.getApiKey()));
|
String appToken = ziniaoClient.getAppToken();
|
||||||
|
session.setAccessToken(maskToken(appToken));
|
||||||
session.setTokenType("Bearer");
|
session.setTokenType("Bearer");
|
||||||
session.setExpireAt(Instant.now().plusSeconds(ziniaoProperties.getSessionTtlHours() * 3600).toEpochMilli());
|
session.setExpireAt(Instant.now().plusSeconds(ziniaoProperties.getSessionTtlHours() * 3600).toEpochMilli());
|
||||||
session.setZiniaoUserId(String.valueOf(defaultCompanyId()));
|
session.setCompanyId(resolveCompanyId());
|
||||||
session.setNickname("ApiKey模式");
|
session.setCurrentUserId(userId);
|
||||||
|
session.setZiniaoUserId(String.valueOf(userId == null ? 0L : userId));
|
||||||
|
session.setNickname("API Key模式");
|
||||||
ziniaoSessionCacheService.saveSession(session);
|
ziniaoSessionCacheService.saveSession(session);
|
||||||
return sessionId;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ZiniaoSessionCacheDto requireSession(String sessionId) {
|
private ZiniaoSessionCacheDto requireOrInitSession(String sessionId, Long userId) {
|
||||||
ensureEnabled();
|
|
||||||
if (sessionId == null || sessionId.isBlank()) {
|
if (sessionId == null || sessionId.isBlank()) {
|
||||||
throw new BusinessException("sessionId 不能为空");
|
return initSession(userId);
|
||||||
}
|
}
|
||||||
ZiniaoSessionCacheDto session = ziniaoSessionCacheService.getSession(sessionId);
|
ZiniaoSessionCacheDto session = ziniaoSessionCacheService.getSession(sessionId);
|
||||||
if (session == null) {
|
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;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ZiniaoShopCacheDto getOrInitCurrentShop(String sessionId, List<ZiniaoShopCacheDto> shops) {
|
private String resolveCurrentShopName(String sessionId, String currentShopId) {
|
||||||
ZiniaoShopCacheDto current = ziniaoSessionCacheService.getCurrentShop(sessionId);
|
if (currentShopId == null || currentShopId.isBlank()) {
|
||||||
if (current != null) {
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
if (shops == null || shops.isEmpty()) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
ZiniaoShopCacheDto first = shops.get(0);
|
return ziniaoSessionCacheService.getShops(sessionId).stream()
|
||||||
first.setSelectedAt(Instant.now().toEpochMilli());
|
.filter(item -> currentShopId.equals(item.getShopId()))
|
||||||
ziniaoSessionCacheService.saveCurrentShop(sessionId, first);
|
.map(ZiniaoShopCacheDto::getShopName)
|
||||||
return first;
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ensureEnabled() {
|
private String buildOpenStoreUrl(String storeId, Long userId, String loginToken) {
|
||||||
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) {
|
|
||||||
String scheme = requireText(ziniaoProperties.getOpenStoreScheme(), "紫鸟 open-store-scheme 未配置");
|
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 launchUrl = requireText(ziniaoProperties.getOpenStoreLaunchUrl(), "紫鸟 open-store-launch-url 未配置");
|
||||||
String encodedLaunchUrl = Base64.getEncoder().encodeToString(launchUrl.getBytes(StandardCharsets.UTF_8));
|
String encodedLaunchUrl = Base64.getEncoder().encodeToString(launchUrl.getBytes(StandardCharsets.UTF_8));
|
||||||
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(scheme)
|
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(scheme)
|
||||||
.queryParam("storeId", shop.getShopId())
|
.queryParam("storeId", storeId)
|
||||||
.queryParam("openapiToken", apiKey)
|
.queryParam("openapiToken", loginToken)
|
||||||
.queryParam("userId", userId)
|
.queryParam("userId", userId)
|
||||||
.queryParam("lanuchUrl", encodedLaunchUrl)
|
.queryParam("lanuchUrl", encodedLaunchUrl)
|
||||||
.queryParam("autoopen", Boolean.TRUE.equals(ziniaoProperties.getOpenStoreAutoOpen()))
|
.queryParam("autoopen", Boolean.TRUE.equals(ziniaoProperties.getOpenStoreAutoOpen()))
|
||||||
@@ -202,8 +189,56 @@ public class ZiniaoAuthService {
|
|||||||
return builder.build(true).toUriString();
|
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) {
|
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);
|
throw new BusinessException(message);
|
||||||
}
|
}
|
||||||
return value.trim();
|
return value.trim();
|
||||||
|
|||||||
@@ -17,10 +17,27 @@ import java.util.List;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ZiniaoSessionCacheService {
|
public class ZiniaoSessionCacheService {
|
||||||
|
|
||||||
|
private static final String APP_TOKEN_KEY = "ziniao:app-token";
|
||||||
|
|
||||||
private final StringRedisTemplate stringRedisTemplate;
|
private final StringRedisTemplate stringRedisTemplate;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final ZiniaoProperties ziniaoProperties;
|
private final ZiniaoProperties ziniaoProperties;
|
||||||
|
|
||||||
|
public void saveAppToken(String appToken) {
|
||||||
|
if (appToken == null || appToken.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stringRedisTemplate.opsForValue().set(APP_TOKEN_KEY, appToken.trim(), Duration.ofHours(ziniaoProperties.getSessionTtlHours()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAppToken() {
|
||||||
|
String value = stringRedisTemplate.opsForValue().get(APP_TOKEN_KEY);
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
public void saveSession(ZiniaoSessionCacheDto session) {
|
public void saveSession(ZiniaoSessionCacheDto session) {
|
||||||
try {
|
try {
|
||||||
stringRedisTemplate.opsForValue().set(buildSessionKey(session.getSessionId()), objectMapper.writeValueAsString(session), Duration.ofHours(ziniaoProperties.getSessionTtlHours()));
|
stringRedisTemplate.opsForValue().set(buildSessionKey(session.getSessionId()), objectMapper.writeValueAsString(session), Duration.ofHours(ziniaoProperties.getSessionTtlHours()));
|
||||||
|
|||||||
@@ -15,12 +15,20 @@ AIIMAGE_OSS_ACCESS_KEY_SECRET=bQSZnFH455i8tzyOgeahJmUzwmhynz
|
|||||||
|
|
||||||
AIIMAGE_STORAGE_LOCAL_TEMP_DIR=F:/project/crawler-plugin/backend-java/data/tmp
|
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_ENABLED=false
|
||||||
AIIMAGE_ZINIAO_BASE_URL=https://sbappstoreapi.ziniao.com/openapi-router
|
AIIMAGE_ZINIAO_BASE_URL=https://sbappstoreapi.ziniao.com/openapi-router
|
||||||
AIIMAGE_ZINIAO_API_KEY=change-me
|
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_COMPANY_ID=0
|
||||||
AIIMAGE_ZINIAO_SHOPS_PATH=/superbrowser/rest/v1/erp/shop/list
|
AIIMAGE_ZINIAO_APP_TOKEN_PATH=/auth/get_app_token
|
||||||
AIIMAGE_ZINIAO_SWITCH_SHOP_PATH=
|
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_SCHEME=superbrowser://OpenStrore
|
||||||
AIIMAGE_ZINIAO_OPEN_STORE_USER_ID=
|
AIIMAGE_ZINIAO_OPEN_STORE_USER_ID=
|
||||||
AIIMAGE_ZINIAO_OPEN_STORE_LAUNCH_URL=https://www.baidu.com
|
AIIMAGE_ZINIAO_OPEN_STORE_LAUNCH_URL=https://www.baidu.com
|
||||||
|
|||||||
@@ -72,13 +72,21 @@ aiimage:
|
|||||||
failed-ttl-hours: ${AIIMAGE_BRAND_PROGRESS_FAILED_TTL_HOURS:2}
|
failed-ttl-hours: ${AIIMAGE_BRAND_PROGRESS_FAILED_TTL_HOURS:2}
|
||||||
heartbeat-timeout-minutes: ${AIIMAGE_BRAND_PROGRESS_HEARTBEAT_TIMEOUT_MINUTES:15}
|
heartbeat-timeout-minutes: ${AIIMAGE_BRAND_PROGRESS_HEARTBEAT_TIMEOUT_MINUTES:15}
|
||||||
stale-check-cron: ${AIIMAGE_BRAND_PROGRESS_STALE_CHECK_CRON:0 */2 * * * *}
|
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:
|
ziniao:
|
||||||
enabled: ${AIIMAGE_ZINIAO_ENABLED:false}
|
enabled: ${AIIMAGE_ZINIAO_ENABLED:false}
|
||||||
base-url: ${AIIMAGE_ZINIAO_BASE_URL:https://sbappstoreapi.ziniao.com/openapi-router}
|
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}
|
company-id: ${AIIMAGE_ZINIAO_COMPANY_ID:0}
|
||||||
shops-path: ${AIIMAGE_ZINIAO_SHOPS_PATH:/superbrowser/rest/v1/erp/shop/list}
|
app-token-path: ${AIIMAGE_ZINIAO_APP_TOKEN_PATH:/auth/get_app_token}
|
||||||
switch-shop-path: ${AIIMAGE_ZINIAO_SWITCH_SHOP_PATH:}
|
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-scheme: ${AIIMAGE_ZINIAO_OPEN_STORE_SCHEME:superbrowser://OpenStrore}
|
||||||
open-store-user-id: ${AIIMAGE_ZINIAO_OPEN_STORE_USER_ID:}
|
open-store-user-id: ${AIIMAGE_ZINIAO_OPEN_STORE_USER_ID:}
|
||||||
open-store-launch-url: ${AIIMAGE_ZINIAO_OPEN_STORE_LAUNCH_URL:https://www.baidu.com}
|
open-store-launch-url: ${AIIMAGE_ZINIAO_OPEN_STORE_LAUNCH_URL:https://www.baidu.com}
|
||||||
|
|||||||
12
frontend-vue/delete-brand.html
Normal file
12
frontend-vue/delete-brand.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>删除品牌 - 数富AI</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/delete-brand-main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
7
frontend-vue/src/delete-brand-main.ts
Normal file
7
frontend-vue/src/delete-brand-main.ts
Normal file
@@ -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')
|
||||||
346
frontend-vue/src/pages/brand/components/BrandDeleteBrandTab.vue
Normal file
346
frontend-vue/src/pages/brand/components/BrandDeleteBrandTab.vue
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-shell module-page">
|
||||||
|
<BrandTopBar active="delete-brand" />
|
||||||
|
|
||||||
|
<div class="main-content">
|
||||||
|
<aside class="left-panel">
|
||||||
|
<div class="section-title">选择文件</div>
|
||||||
|
<div class="upload-zone">
|
||||||
|
<div class="hint">上传删除品牌 Excel;无论是直接上传文件还是上传文件夹批量导入,都会按每个 Excel 文件名匹配紫鸟店铺。</div>
|
||||||
|
<div class="btns">
|
||||||
|
<button type="button" class="opt-btn" @click="selectFiles">选择 Excel 文件</button>
|
||||||
|
<button type="button" class="opt-btn" @click="selectFolder">选择文件夹</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="selected-files clean-placeholder split-selected-files">
|
||||||
|
<template v-if="selectedPaths.length">
|
||||||
|
<span v-for="path in displayPaths" :key="path">{{ path }}</span>
|
||||||
|
<span v-if="selectedPaths.length > displayPaths.length" class="more-line">
|
||||||
|
还有 {{ selectedPaths.length - displayPaths.length }} 个文件未展开显示
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<span v-else>暂未选择删除品牌文件</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-title">执行说明</div>
|
||||||
|
<div class="option-group">
|
||||||
|
<label class="radio-item active">
|
||||||
|
<input type="checkbox" checked disabled />
|
||||||
|
<div>
|
||||||
|
<span class="label">文件名作为店铺名</span>
|
||||||
|
<div class="desc">例如 鲍丽明.xlsx 会匹配紫鸟店铺 鲍丽明;选择文件夹时也仍按每个 Excel 文件名匹配</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="radio-item active">
|
||||||
|
<input type="checkbox" checked disabled />
|
||||||
|
<div>
|
||||||
|
<span class="label">文件夹仅用于批量导入</span>
|
||||||
|
<div class="desc">文件夹名和子目录名不会参与店铺匹配,只是方便一次上传多个 Excel</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="radio-item active">
|
||||||
|
<input type="checkbox" checked disabled />
|
||||||
|
<div>
|
||||||
|
<span class="label">大表仅返回预览</span>
|
||||||
|
<div class="desc">当前最多展示前 200 行,避免大文件直接拖慢页面</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="run-row">
|
||||||
|
<button type="button" class="btn-run" :disabled="running" @click="submitRun">
|
||||||
|
{{ running ? '解析中...' : '开始解析' }}
|
||||||
|
</button>
|
||||||
|
<span class="loading-msg">
|
||||||
|
{{ running ? '正在读取删除品牌数据并匹配紫鸟店铺,请稍候…' : '解析后会在右侧展示预览数据和紫鸟店铺打开链接' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<section class="right-panel">
|
||||||
|
<div class="panel-header">删除品牌结果</div>
|
||||||
|
<div class="task-list-wrap">
|
||||||
|
<div class="clean-result-summary">
|
||||||
|
<div class="summary-card">
|
||||||
|
<span class="summary-label">已处理文件</span>
|
||||||
|
<strong>{{ summary.total }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="summary-card">
|
||||||
|
<span class="summary-label">成功结果</span>
|
||||||
|
<strong>{{ summary.successCount }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="summary-card">
|
||||||
|
<span class="summary-label">失败文件</span>
|
||||||
|
<strong>{{ summary.failedCount }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result-list-wrap">
|
||||||
|
<div class="result-list-header">
|
||||||
|
<span>删除品牌结果列表</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="resultItems.length === 0" class="empty-tasks">
|
||||||
|
暂无删除品牌结果,完成解析后会在这里展示店铺匹配结果和预览数据
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul v-else class="task-list clean-result-list">
|
||||||
|
<li
|
||||||
|
v-for="item in resultItems"
|
||||||
|
:key="`${item.resultId || item.sourceFilename}-${item.shopId || ''}`"
|
||||||
|
class="task-item split-result-item"
|
||||||
|
>
|
||||||
|
<div class="left split-result-main">
|
||||||
|
<span class="id" :title="item.sourceFilename">{{ item.sourceFilename || '-' }}</span>
|
||||||
|
<div class="files">店铺名:{{ item.shopName || '-' }}</div>
|
||||||
|
<div class="files">匹配结果:{{ item.matched ? `已匹配 ${item.shopId || ''}` : '未匹配到紫鸟店铺' }}</div>
|
||||||
|
<div v-if="item.platform" class="files">平台:{{ item.platform }}</div>
|
||||||
|
<div v-if="item.totalRows !== undefined" class="time">预览 {{ item.previewRows?.length || 0 }} / {{ item.totalRows }} 行</div>
|
||||||
|
<div v-if="item.truncated" class="files">数据较大,当前仅展示前 200 行</div>
|
||||||
|
<div v-if="item.previewRows?.length" class="files split-entry-list delete-brand-preview">
|
||||||
|
{{ formatPreview(item.previewRows) }}
|
||||||
|
</div>
|
||||||
|
<div v-if="item.error" class="files">错误信息:{{ item.error }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="task-right split-result-actions">
|
||||||
|
<span class="status" :class="item.success ? 'success' : 'failed'">
|
||||||
|
{{ item.success ? '已完成' : '失败' }}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
v-if="item.openStoreUrl"
|
||||||
|
type="button"
|
||||||
|
class="download"
|
||||||
|
@click="openShop(item.openStoreUrl)"
|
||||||
|
>
|
||||||
|
打开店铺
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="item.resultId"
|
||||||
|
type="button"
|
||||||
|
class="btn-delete"
|
||||||
|
@click="deleteHistoryRecord(item.resultId)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import BrandTopBar from './BrandTopBar.vue'
|
||||||
|
import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared/api/brand'
|
||||||
|
import {
|
||||||
|
deleteDeleteBrandHistory,
|
||||||
|
getDeleteBrandHistory,
|
||||||
|
runDeleteBrand,
|
||||||
|
type DeleteBrandPreviewRow,
|
||||||
|
type DeleteBrandResultItem,
|
||||||
|
type DeleteBrandRunVo,
|
||||||
|
} from '@/shared/api/java-modules'
|
||||||
|
import { getPywebviewApi, type UploadedJavaFile } from '@/shared/bridges/pywebview'
|
||||||
|
|
||||||
|
const selectedPaths = ref<string[]>([])
|
||||||
|
const uploadedFiles = ref<UploadedJavaFile[]>([])
|
||||||
|
const running = ref(false)
|
||||||
|
const resultItems = ref<DeleteBrandResultItem[]>([])
|
||||||
|
const summary = ref<DeleteBrandRunVo>({ total: 0, successCount: 0, failedCount: 0, items: [] })
|
||||||
|
const displayPaths = computed(() => selectedPaths.value.slice(0, 8))
|
||||||
|
|
||||||
|
function updateSummary(items: DeleteBrandResultItem[]) {
|
||||||
|
summary.value = {
|
||||||
|
total: items.length,
|
||||||
|
successCount: items.filter((item) => item.success).length,
|
||||||
|
failedCount: items.filter((item) => !item.success).length,
|
||||||
|
items,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadPathsToJava(paths: Array<string | BrandExpandFolderItem>) {
|
||||||
|
const api = getPywebviewApi()
|
||||||
|
if (!api?.upload_file_to_java) {
|
||||||
|
throw new Error('当前桌面端未提供文件上传桥接能力')
|
||||||
|
}
|
||||||
|
const uploaded: UploadedJavaFile[] = []
|
||||||
|
for (const item of paths) {
|
||||||
|
const filePath = typeof item === 'string' ? item : item.absolutePath
|
||||||
|
const relativePath = typeof item === 'string' ? undefined : item.relativePath
|
||||||
|
const result = await api.upload_file_to_java(filePath, relativePath)
|
||||||
|
if (!result?.success || !result.data) {
|
||||||
|
throw new Error(result?.error || result?.message || `上传失败:${filePath}`)
|
||||||
|
}
|
||||||
|
uploaded.push(result.data)
|
||||||
|
}
|
||||||
|
return uploaded
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectFiles() {
|
||||||
|
const api = getPywebviewApi()
|
||||||
|
if (!api?.select_brand_xlsx_files) {
|
||||||
|
ElMessage.warning('当前环境不支持文件选择,请在本机客户端中打开')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const paths = await api.select_brand_xlsx_files()
|
||||||
|
if (!paths?.length) return
|
||||||
|
selectedPaths.value = paths
|
||||||
|
uploadedFiles.value = await uploadPathsToJava(paths)
|
||||||
|
ElMessage.success(`已选择 ${paths.length} 个删除品牌文件`)
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error instanceof Error ? error.message : '选择失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectFolder() {
|
||||||
|
const api = getPywebviewApi()
|
||||||
|
if (!api?.select_brand_folder) {
|
||||||
|
ElMessage.warning('当前环境不支持文件夹选择,请在本机客户端中打开')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const folder = await api.select_brand_folder()
|
||||||
|
if (!folder) return
|
||||||
|
const result = await expandBrandFolderRecursive(folder)
|
||||||
|
if (!result.success || !result.items?.length) {
|
||||||
|
ElMessage.warning(result.error || '该文件夹下没有 xlsx 文件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
selectedPaths.value = result.items.map((item) => item.relativePath || item.absolutePath)
|
||||||
|
uploadedFiles.value = await uploadPathsToJava(result.items)
|
||||||
|
ElMessage.success(`已选择文件夹内 ${result.items.length} 个 xlsx 文件`)
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error instanceof Error ? error.message : '选择失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitRun() {
|
||||||
|
if (!uploadedFiles.value.length) {
|
||||||
|
ElMessage.warning('请先选择待处理文件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
running.value = true
|
||||||
|
const result = await runDeleteBrand({
|
||||||
|
files: uploadedFiles.value.map((item) => ({
|
||||||
|
fileKey: item.fileKey,
|
||||||
|
originalFilename: item.originalFilename,
|
||||||
|
relativePath: item.relativePath,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
summary.value = result
|
||||||
|
resultItems.value = result.items || []
|
||||||
|
await loadHistory()
|
||||||
|
ElMessage.success('删除品牌解析完成')
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error instanceof Error ? error.message : '执行失败')
|
||||||
|
} finally {
|
||||||
|
running.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadHistory() {
|
||||||
|
try {
|
||||||
|
const response = await getDeleteBrandHistory()
|
||||||
|
resultItems.value = response.items || []
|
||||||
|
updateSummary(resultItems.value)
|
||||||
|
} catch {
|
||||||
|
// ignore history load errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteHistoryRecord(resultId: number) {
|
||||||
|
try {
|
||||||
|
await deleteDeleteBrandHistory(resultId)
|
||||||
|
await loadHistory()
|
||||||
|
ElMessage.success('已删除')
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error instanceof Error ? error.message : '删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPreview(rows: DeleteBrandPreviewRow[]) {
|
||||||
|
const preview = rows.slice(0, 5).map((item) => `${item.country}:${item.asin}${item.status ? `(${item.status})` : ''}`)
|
||||||
|
const hiddenCount = rows.length - preview.length
|
||||||
|
return hiddenCount > 0 ? `${preview.join('、')} 等 ${rows.length} 条` : preview.join('、')
|
||||||
|
}
|
||||||
|
|
||||||
|
function openShop(url: string) {
|
||||||
|
window.open(url, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadHistory().catch(() => undefined)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.module-page { min-height: 100vh; background: #1a1a1a; }
|
||||||
|
.main-content { display: flex; min-height: calc(100vh - 56px); height: calc(100vh - 56px); }
|
||||||
|
.left-panel { width: 380px; background: #1e1e1e; padding: 20px; overflow-y: auto; border-right: 1px solid #2a2a2a; }
|
||||||
|
.right-panel { flex: 1; display: flex; flex-direction: column; min-width: 0; background: #1a1a1a; }
|
||||||
|
.section-title { font-size: 13px; color: #bbb; margin-bottom: 10px; }
|
||||||
|
.upload-zone { border: 1px dashed #3a3a3a; border-radius: 10px; padding: 24px; text-align: center; background: #252525; margin-bottom: 20px; transition: all 0.2s ease; }
|
||||||
|
.upload-zone:hover { border-color: #3498db; background: #2a2a2a; }
|
||||||
|
.hint { color: #888; font-size: 13px; margin-bottom: 12px; line-height: 1.5; }
|
||||||
|
.btns { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
|
||||||
|
.opt-btn, .btn-run, .download, .btn-delete { border: none; cursor: pointer; transition: all 0.2s ease; }
|
||||||
|
.opt-btn { padding: 8px 16px; font-size: 13px; color: #ccc; background: #2a2a2a; border: 1px solid #3a3a3a; border-radius: 6px; }
|
||||||
|
.opt-btn:hover { color: #3498db; background: #333; border-color: #3498db; }
|
||||||
|
.selected-files { margin-top: 14px; font-size: 12px; color: #888; max-height: 112px; overflow-y: auto; text-align: left; }
|
||||||
|
.selected-files span { display: block; margin: 4px 0; word-break: break-all; }
|
||||||
|
.more-line { color: #b8c1cc; }
|
||||||
|
.split-selected-files { max-height: 120px; min-height: 72px; padding-right: 4px; }
|
||||||
|
.option-group { margin-bottom: 20px; }
|
||||||
|
.radio-item { display: flex; align-items: flex-start; gap: 10px; cursor: pointer; padding: 10px 12px; border-radius: 8px; background: #252525; border: 1px solid #2a2a2a; margin-bottom: 8px; transition: all 0.2s ease; }
|
||||||
|
.radio-item:hover, .radio-item.active { background: #2a2a2a; border-color: #3a3a3a; }
|
||||||
|
.radio-item input { margin-top: 3px; }
|
||||||
|
.label { font-weight: 500; color: #e0e0e0; font-size: 13px; }
|
||||||
|
.desc { font-size: 12px; color: #888; margin-top: 2px; line-height: 1.5; font-weight: 400; }
|
||||||
|
.run-row { display: flex; align-items: center; flex-wrap: wrap; gap: 10px 12px; margin-top: 16px; }
|
||||||
|
.btn-run { padding: 10px 22px; background: #3498db; color: #fff; border-radius: 8px; font-size: 14px; }
|
||||||
|
.btn-run:hover { background: #2980b9; }
|
||||||
|
.btn-run:disabled { background: #555; color: #999; cursor: not-allowed; }
|
||||||
|
.loading-msg { color: #3498db; font-size: 13px; }
|
||||||
|
.panel-header { padding: 16px 20px; border-bottom: 1px solid #2a2a2a; font-size: 15px; font-weight: 600; color: #ddd; }
|
||||||
|
.task-list-wrap { flex: 1; overflow-y: auto; padding: 16px; }
|
||||||
|
.task-list { list-style: none; margin: 0; padding: 0; }
|
||||||
|
.task-item { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 14px 16px; border: 1px solid #2a2a2a; border-radius: 8px; margin-bottom: 10px; background: #1e1e1e; }
|
||||||
|
.split-result-item { align-items: flex-start; }
|
||||||
|
.left { flex: 1; min-width: 0; }
|
||||||
|
.split-result-main { display: flex; flex-direction: column; gap: 4px; }
|
||||||
|
.id { display: inline-block; font-weight: 600; color: #e0e0e0; font-size: 13px; }
|
||||||
|
.files { font-size: 12px; color: #888; margin-top: 4px; }
|
||||||
|
.time { font-size: 12px; color: #666; margin-top: 2px; }
|
||||||
|
.split-entry-list { line-height: 1.6; word-break: break-all; max-width: 100%; }
|
||||||
|
.delete-brand-preview { white-space: normal; }
|
||||||
|
.task-right { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; justify-content: flex-end; }
|
||||||
|
.split-result-actions { flex-shrink: 0; min-width: 180px; justify-content: flex-end; align-self: center; }
|
||||||
|
.status { padding: 4px 10px; border-radius: 6px; font-size: 12px; white-space: nowrap; }
|
||||||
|
.status.success { background: rgba(46, 204, 113, 0.18); color: #2ecc71; }
|
||||||
|
.status.failed { background: rgba(231, 76, 60, 0.18); color: #ff6b6b; }
|
||||||
|
.download { padding: 6px 10px; border-radius: 6px; font-size: 12px; background: rgba(52, 152, 219, 0.18); color: #69b6ff; }
|
||||||
|
.download:hover { background: rgba(52, 152, 219, 0.28); }
|
||||||
|
.btn-delete { padding: 6px 10px; border-radius: 6px; font-size: 12px; background: rgba(231, 76, 60, 0.12); color: #ff8f8f; }
|
||||||
|
.btn-delete:hover { background: rgba(231, 76, 60, 0.22); }
|
||||||
|
.empty-tasks { color: #666; font-size: 13px; padding: 24px 8px; }
|
||||||
|
.clean-placeholder { max-height: 112px; }
|
||||||
|
.clean-result-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 220px)); gap: 16px; margin-bottom: 18px; }
|
||||||
|
.summary-card { padding: 16px 18px; border: 1px solid #2a2a2a; border-radius: 10px; background: #1e1e1e; }
|
||||||
|
.summary-card strong { display: block; margin-top: 8px; font-size: 24px; color: #eaf4ff; }
|
||||||
|
.summary-label { font-size: 12px; color: #8d8d8d; }
|
||||||
|
.result-list-wrap { border: 1px solid #2a2a2a; border-radius: 10px; background: #1e1e1e; min-height: 260px; }
|
||||||
|
.result-list-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 16px; border-bottom: 1px solid #2a2a2a; font-size: 14px; color: #ddd; }
|
||||||
|
@media (max-width: 1100px) { .main-content { flex-direction: column; height: auto; } .left-panel { width: 100%; border-right: none; border-bottom: 1px solid #2a2a2a; } .right-panel { min-height: 420px; } .task-item { flex-direction: column; align-items: flex-start; } .task-right { width: 100%; justify-content: flex-start; } .split-result-actions { min-width: 0; align-self: flex-start; } .clean-result-summary { grid-template-columns: 1fr; } }
|
||||||
|
</style>
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
active: 'brand' | 'dedupe' | 'convert' | 'split'
|
active: 'brand' | 'dedupe' | 'convert' | 'split' | 'delete-brand'
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const active = props.active
|
const active = props.active
|
||||||
@@ -30,6 +30,7 @@ const navItems = [
|
|||||||
{ key: 'dedupe', label: '数据去重', href: '/new_web_source/dedupe.html' },
|
{ key: 'dedupe', label: '数据去重', href: '/new_web_source/dedupe.html' },
|
||||||
{ key: 'split', label: '数据拆分', href: '/new_web_source/split.html' },
|
{ key: 'split', label: '数据拆分', href: '/new_web_source/split.html' },
|
||||||
{ key: 'convert', label: '格式转换', href: '/new_web_source/convert.html' },
|
{ key: 'convert', label: '格式转换', href: '/new_web_source/convert.html' },
|
||||||
|
{ key: 'delete-brand', label: '删除品牌', href: '/new_web_source/delete-brand.html' },
|
||||||
] as const
|
] as const
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
import { requestDeleteJson, requestGetJson, requestPostJson } from '@/shared/api/http'
|
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 {
|
export interface BrandExpandFolderItem {
|
||||||
absolutePath: string
|
absolutePath: string
|
||||||
relativePath: string
|
relativePath: string
|
||||||
@@ -47,11 +56,11 @@ export interface BrandTaskMutationResponse {
|
|||||||
const API_PREFIX = ''
|
const API_PREFIX = ''
|
||||||
|
|
||||||
export function getBrandTaskEventsUrl(taskId: string | number) {
|
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) {
|
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() {
|
export function getBrandTemplateXlsxUrl() {
|
||||||
@@ -75,23 +84,23 @@ export function runBrandNow(paths: string[], strategy: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createBrandTask(paths: string[], strategy: string) {
|
export function createBrandTask(paths: string[], strategy: string) {
|
||||||
return requestPostJson<BrandTaskMutationResponse>(`${API_PREFIX}/api/brand/tasks`, { paths, strategy })
|
return requestPostJson<BrandTaskMutationResponse>(`${API_PREFIX}/api/brand/tasks?userId=${encodeURIComponent(String(getCurrentUserId()))}`, { paths, strategy })
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBrandTasks() {
|
export function getBrandTasks() {
|
||||||
return requestGetJson<BrandTaskListResponse>(`${API_PREFIX}/api/brand/tasks`)
|
return requestGetJson<BrandTaskListResponse>(`${API_PREFIX}/api/brand/tasks?userId=${encodeURIComponent(String(getCurrentUserId()))}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBrandTask(taskId: string | number) {
|
export function getBrandTask(taskId: string | number) {
|
||||||
return requestGetJson<BrandTaskDetailResponse>(`${API_PREFIX}/api/brand/tasks/${taskId}`)
|
return requestGetJson<BrandTaskDetailResponse>(`${API_PREFIX}/api/brand/tasks/${taskId}?userId=${encodeURIComponent(String(getCurrentUserId()))}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cancelBrandTask(taskId: string | number) {
|
export function cancelBrandTask(taskId: string | number) {
|
||||||
return requestPostJson<BrandTaskMutationResponse>(`${API_PREFIX}/api/brand/tasks/${taskId}/cancel`)
|
return requestPostJson<BrandTaskMutationResponse>(`${API_PREFIX}/api/brand/tasks/${taskId}/cancel?userId=${encodeURIComponent(String(getCurrentUserId()))}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteBrandTask(taskId: string | number) {
|
export function deleteBrandTask(taskId: string | number) {
|
||||||
return requestDeleteJson<BrandTaskMutationResponse>(`${API_PREFIX}/api/brand/tasks/${taskId}`)
|
return requestDeleteJson<BrandTaskMutationResponse>(`${API_PREFIX}/api/brand/tasks/${taskId}?userId=${encodeURIComponent(String(getCurrentUserId()))}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createBrandTaskEvents(taskId: string | number) {
|
export function createBrandTaskEvents(taskId: string | number) {
|
||||||
|
|||||||
@@ -2,6 +2,15 @@ import { del, get, http, post, type JavaApiResponse, unwrapJavaResponse } from '
|
|||||||
|
|
||||||
const JAVA_API_PREFIX = '/newApi/api'
|
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 {
|
export interface UploadedFileRef {
|
||||||
fileKey: string
|
fileKey: string
|
||||||
originalFilename?: string
|
originalFilename?: string
|
||||||
@@ -43,6 +52,7 @@ export interface DedupeRunRequest {
|
|||||||
keepUnderscoreIds: boolean
|
keepUnderscoreIds: boolean
|
||||||
keepIntegerMainIdsWhenNoSubIds: boolean
|
keepIntegerMainIdsWhenNoSubIds: boolean
|
||||||
archiveName?: string
|
archiveName?: string
|
||||||
|
user_id: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SplitArchiveEntry {
|
export interface SplitArchiveEntry {
|
||||||
@@ -80,6 +90,7 @@ export interface SplitRunRequest {
|
|||||||
rowsPerFile?: number
|
rowsPerFile?: number
|
||||||
parts?: number
|
parts?: number
|
||||||
archiveName?: string
|
archiveName?: string
|
||||||
|
user_id: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConvertResultItem {
|
export interface ConvertResultItem {
|
||||||
@@ -106,6 +117,45 @@ export interface ConvertRunRequest {
|
|||||||
files: UploadedFileRef[]
|
files: UploadedFileRef[]
|
||||||
templateId: string
|
templateId: string
|
||||||
archiveName?: 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 {
|
export interface ConvertTemplateVo {
|
||||||
@@ -133,28 +183,42 @@ export function getExcelInfo(fileKey: string) {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function runDedupe(request: DedupeRunRequest) {
|
export function runDedupe(request: Omit<DedupeRunRequest, 'user_id'> | DedupeRunRequest) {
|
||||||
return unwrapJavaResponse(post<JavaApiResponse<DedupeRunVo>, DedupeRunRequest>(`${JAVA_API_PREFIX}/dedupe/run`, request))
|
return unwrapJavaResponse(post<JavaApiResponse<DedupeRunVo>, DedupeRunRequest>(`${JAVA_API_PREFIX}/dedupe/run`, {
|
||||||
|
...request,
|
||||||
|
user_id: getCurrentUserId(),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDedupeHistory() {
|
export function getDedupeHistory() {
|
||||||
return unwrapJavaResponse(get<JavaApiResponse<DedupeHistoryVo>>(`${JAVA_API_PREFIX}/dedupe/history`))
|
return unwrapJavaResponse(get<JavaApiResponse<DedupeHistoryVo>>(`${JAVA_API_PREFIX}/dedupe/history`, {
|
||||||
|
params: { user_id: getCurrentUserId() },
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteDedupeHistory(resultId: number) {
|
export function deleteDedupeHistory(resultId: number) {
|
||||||
return unwrapJavaResponse(del<JavaApiResponse<null>>(`${JAVA_API_PREFIX}/dedupe/history/${resultId}`))
|
return unwrapJavaResponse(del<JavaApiResponse<null>>(`${JAVA_API_PREFIX}/dedupe/history/${resultId}`, {
|
||||||
|
params: { user_id: getCurrentUserId() },
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function runSplit(request: SplitRunRequest) {
|
export function runSplit(request: Omit<SplitRunRequest, 'user_id'> | SplitRunRequest) {
|
||||||
return unwrapJavaResponse(post<JavaApiResponse<SplitRunVo>, SplitRunRequest>(`${JAVA_API_PREFIX}/split/run`, request))
|
return unwrapJavaResponse(post<JavaApiResponse<SplitRunVo>, SplitRunRequest>(`${JAVA_API_PREFIX}/split/run`, {
|
||||||
|
...request,
|
||||||
|
user_id: getCurrentUserId(),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSplitHistory() {
|
export function getSplitHistory() {
|
||||||
return unwrapJavaResponse(get<JavaApiResponse<SplitHistoryVo>>(`${JAVA_API_PREFIX}/split/history`))
|
return unwrapJavaResponse(get<JavaApiResponse<SplitHistoryVo>>(`${JAVA_API_PREFIX}/split/history`, {
|
||||||
|
params: { user_id: getCurrentUserId() },
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteSplitHistory(resultId: number) {
|
export function deleteSplitHistory(resultId: number) {
|
||||||
return unwrapJavaResponse(del<JavaApiResponse<null>>(`${JAVA_API_PREFIX}/split/history/${resultId}`))
|
return unwrapJavaResponse(del<JavaApiResponse<null>>(`${JAVA_API_PREFIX}/split/history/${resultId}`, {
|
||||||
|
params: { user_id: getCurrentUserId() },
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getConvertTemplates() {
|
export function getConvertTemplates() {
|
||||||
@@ -175,20 +239,48 @@ export function deleteConvertTemplate(templateCode: string) {
|
|||||||
return unwrapJavaResponse(del<JavaApiResponse<null>>(`${JAVA_API_PREFIX}/convert/templates/${templateCode}`))
|
return unwrapJavaResponse(del<JavaApiResponse<null>>(`${JAVA_API_PREFIX}/convert/templates/${templateCode}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function runConvert(request: ConvertRunRequest) {
|
export function runConvert(request: Omit<ConvertRunRequest, 'user_id'> | ConvertRunRequest) {
|
||||||
return unwrapJavaResponse(post<JavaApiResponse<ConvertRunVo>, ConvertRunRequest>(`${JAVA_API_PREFIX}/convert/run`, request))
|
return unwrapJavaResponse(post<JavaApiResponse<ConvertRunVo>, ConvertRunRequest>(`${JAVA_API_PREFIX}/convert/run`, {
|
||||||
|
...request,
|
||||||
|
user_id: getCurrentUserId(),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getConvertHistory() {
|
export function getConvertHistory() {
|
||||||
return unwrapJavaResponse(get<JavaApiResponse<ConvertHistoryVo>>(`${JAVA_API_PREFIX}/convert/history`))
|
return unwrapJavaResponse(get<JavaApiResponse<ConvertHistoryVo>>(`${JAVA_API_PREFIX}/convert/history`, {
|
||||||
|
params: { user_id: getCurrentUserId() },
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteConvertHistory(resultId: number) {
|
export function deleteConvertHistory(resultId: number) {
|
||||||
return unwrapJavaResponse(del<JavaApiResponse<null>>(`${JAVA_API_PREFIX}/convert/history/${resultId}`))
|
return unwrapJavaResponse(del<JavaApiResponse<null>>(`${JAVA_API_PREFIX}/convert/history/${resultId}`, {
|
||||||
|
params: { user_id: getCurrentUserId() },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runDeleteBrand(request: Omit<DeleteBrandRunRequest, 'user_id'> | DeleteBrandRunRequest) {
|
||||||
|
return unwrapJavaResponse(post<JavaApiResponse<DeleteBrandRunVo>, DeleteBrandRunRequest>(`${JAVA_API_PREFIX}/delete-brand/run`, {
|
||||||
|
...request,
|
||||||
|
user_id: getCurrentUserId(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDeleteBrandHistory() {
|
||||||
|
return unwrapJavaResponse(get<JavaApiResponse<DeleteBrandHistoryVo>>(`${JAVA_API_PREFIX}/delete-brand/history`, {
|
||||||
|
params: { user_id: getCurrentUserId() },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteDeleteBrandHistory(resultId: number) {
|
||||||
|
return unwrapJavaResponse(del<JavaApiResponse<null>>(`${JAVA_API_PREFIX}/delete-brand/history/${resultId}`, {
|
||||||
|
params: { user_id: getCurrentUserId() },
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getJavaDownloadUrl(path: string) {
|
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 }
|
export { JAVA_API_PREFIX, http }
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export default defineConfig({
|
|||||||
dedupe: resolve(__dirname, 'dedupe.html'),
|
dedupe: resolve(__dirname, 'dedupe.html'),
|
||||||
convert: resolve(__dirname, 'convert.html'),
|
convert: resolve(__dirname, 'convert.html'),
|
||||||
split: resolve(__dirname, 'split.html'),
|
split: resolve(__dirname, 'split.html'),
|
||||||
|
'delete-brand': resolve(__dirname, 'delete-brand.html'),
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
entryFileNames: 'assets/[name].js',
|
entryFileNames: 'assets/[name].js',
|
||||||
|
|||||||
Reference in New Issue
Block a user