更新后端新增内容

This commit is contained in:
super
2026-03-26 16:42:25 +08:00
parent 6434ec3e00
commit 8106a8ab7e
48 changed files with 1788 additions and 286 deletions

Binary file not shown.

View File

@@ -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"));
}

View File

@@ -4,6 +4,6 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties({OssProperties.class, StorageProperties.class, BrandProgressProperties.class, ZiniaoProperties.class})
@EnableConfigurationProperties({OssProperties.class, StorageProperties.class, BrandProgressProperties.class, ZiniaoProperties.class, ModuleCleanupProperties.class})
public class PropertiesConfig {
}

View File

@@ -9,9 +9,13 @@ public class ZiniaoProperties {
private boolean enabled;
private String baseUrl;
private String apiKey;
private String appId;
private String appSecret;
private Long companyId;
private String shopsPath;
private String switchShopPath;
private String appTokenPath;
private String staffListPath;
private String userStoresPath;
private String userLoginTokenPath;
private String openStoreScheme;
private String openStoreUserId;
private String openStoreLaunchUrl;

View File

@@ -124,8 +124,9 @@ public class BrandTaskController {
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "任务不存在")
})
public ApiResponse<LegacyBrandTaskDetailVo> getTask(
@Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId) {
return ApiResponse.success(brandTaskService.getTaskDetailLegacy(taskId));
@Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId,
@Parameter(description = "用户 ID", required = true) @RequestParam Long userId) {
return ApiResponse.success(brandTaskService.getTaskDetailLegacy(taskId, userId));
}
@PostMapping("/tasks/{taskId}/result")
@@ -153,8 +154,9 @@ public class BrandTaskController {
})
public ApiResponse<BrandSimpleVo> submitResult(
@Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId,
@Parameter(description = "用户 ID", required = true) @RequestParam Long userId,
@Valid @RequestBody BrandCrawlResultRequest request) {
brandTaskService.submitCrawlResult(taskId, request);
brandTaskService.submitCrawlResult(taskId, userId, request);
return ApiResponse.success(new BrandSimpleVo(true));
}
@@ -168,8 +170,9 @@ public class BrandTaskController {
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "任务不存在或当前状态不可取消")
})
public ApiResponse<BrandSimpleVo> cancelTask(
@Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId) {
brandTaskService.cancelTask(taskId);
@Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId,
@Parameter(description = "用户 ID", required = true) @RequestParam Long userId) {
brandTaskService.cancelTask(taskId, userId);
return ApiResponse.success(new BrandSimpleVo(true));
}
@@ -183,8 +186,9 @@ public class BrandTaskController {
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "任务不存在或正在执行中不可删除")
})
public ApiResponse<BrandSimpleVo> deleteTask(
@Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId) {
brandTaskService.deleteTask(taskId);
@Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId,
@Parameter(description = "用户 ID", required = true) @RequestParam Long userId) {
brandTaskService.deleteTask(taskId, userId);
return ApiResponse.success(new BrandSimpleVo(true));
}
@@ -198,8 +202,9 @@ public class BrandTaskController {
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "任务不存在或无结果可下载")
})
public ResponseEntity<Void> download(
@Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId) {
String url = brandTaskService.resolveDownloadUrl(taskId);
@Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId,
@Parameter(description = "用户 ID", required = true) @RequestParam Long userId) {
String url = brandTaskService.resolveDownloadUrl(taskId, userId);
return ResponseEntity.status(302)
.header(HttpHeaders.LOCATION, url)
.build();

View File

@@ -133,8 +133,14 @@ public class BrandTaskService {
return vo;
}
public LegacyBrandTaskDetailVo getTaskDetailLegacy(Long taskId) {
BrandCrawlTaskEntity task = requireTask(taskId);
public BrandTaskDetailVo getTaskDetail(Long taskId, Long userId) {
BrandTaskDetailVo vo = new BrandTaskDetailVo();
vo.setTask(toTaskItem(requireTask(taskId, userId)));
return vo;
}
public LegacyBrandTaskDetailVo getTaskDetailLegacy(Long taskId, Long userId) {
BrandCrawlTaskEntity task = requireTask(taskId, userId);
LegacyBrandTaskDetailVo vo = new LegacyBrandTaskDetailVo();
vo.setTask(toLegacyTaskItem(task));
vo.setLine_progress(buildLineProgress(taskId));
@@ -198,8 +204,8 @@ public class BrandTaskService {
return vo;
}
public void submitCrawlResult(Long taskId, BrandCrawlResultRequest request) {
BrandCrawlTaskEntity task = requireTask(taskId);
public void submitCrawlResult(Long taskId, Long userId, BrandCrawlResultRequest request) {
BrandCrawlTaskEntity task = requireTask(taskId, userId);
if (STATUS_CANCELLED.equalsIgnoreCase(blankToDefault(task.getStatus(), STATUS_PENDING))) {
throw new BusinessException("任务已取消");
}
@@ -304,9 +310,11 @@ public class BrandTaskService {
}
}
public void cancelTask(Long taskId) {
public void cancelTask(Long taskId, Long userId) {
requireTask(taskId, userId);
int updated = brandCrawlTaskMapper.update(null, new LambdaUpdateWrapper<BrandCrawlTaskEntity>()
.eq(BrandCrawlTaskEntity::getId, taskId)
.eq(BrandCrawlTaskEntity::getUserId, userId)
.in(BrandCrawlTaskEntity::getStatus, STATUS_PENDING, STATUS_RUNNING)
.set(BrandCrawlTaskEntity::getStatus, STATUS_CANCELLED));
if (updated == 0) {
@@ -315,17 +323,19 @@ public class BrandTaskService {
brandTaskProgressCacheService.delete(taskId);
}
public void deleteTask(Long taskId) {
public void deleteTask(Long taskId, Long userId) {
requireTask(taskId, userId);
int deleted = brandCrawlTaskMapper.delete(new LambdaQueryWrapper<BrandCrawlTaskEntity>()
.eq(BrandCrawlTaskEntity::getId, taskId)
.eq(BrandCrawlTaskEntity::getUserId, userId)
.ne(BrandCrawlTaskEntity::getStatus, STATUS_RUNNING));
if (deleted == 0) {
throw new BusinessException("任务不存在或正在执行中无法删除");
}
}
public String resolveDownloadUrl(Long taskId) {
BrandCrawlTaskEntity task = requireTask(taskId);
public String resolveDownloadUrl(Long taskId, Long userId) {
BrandCrawlTaskEntity task = requireTask(taskId, userId);
Object raw = parseJsonValue(task.getResultPaths());
if (!(raw instanceof Map<?, ?> map)) {
throw new BusinessException("无结果可下载");
@@ -600,6 +610,17 @@ public class BrandTaskService {
return entity;
}
private BrandCrawlTaskEntity requireTask(Long taskId, Long userId) {
if (userId == null || userId <= 0) {
throw new BusinessException("userId 不合法");
}
BrandCrawlTaskEntity entity = brandCrawlTaskMapper.selectById(taskId);
if (entity == null || !userId.equals(entity.getUserId())) {
throw new BusinessException("任务不存在");
}
return entity;
}
private File resolveSourceFile(BrandSourceFileDto sourceFile) {
String fileUrl = sourceFile.getFileUrl();
if (fileUrl == null || fileUrl.isBlank()) {
@@ -661,11 +682,15 @@ public class BrandTaskService {
String value = index == null ? "" : normalizeCellText(formatter.formatCellValue(row.getCell(index)));
rowData.put(column, value);
}
rowData.put("__rowIndex", rowNum + 1);
rows.add(rowData);
String brand = normalizeCellText(Objects.toString(rowData.getOrDefault("品牌", ""), ""));
if (!brand.isBlank()) {
uniqueBrands.add(brand);
String brand = normalizeCellText(Objects.toString(rowData.getOrDefault("\u54c1\u724c", ""), ""));
if (brand.isBlank()) {
rowData.put("__rowIndex", rowNum + 1);
rows.add(rowData);
continue;
}
if (uniqueBrands.add(brand)) {
rowData.put("__rowIndex", rowNum + 1);
rows.add(rowData);
}
}
return new ParsedBrandFile(sheet.getSheetName(), columns, rows, new ArrayList<>(uniqueBrands));

View File

@@ -7,6 +7,7 @@ import com.nanri.aiimage.modules.convert.model.vo.ConvertRunVo;
import com.nanri.aiimage.modules.convert.service.ConvertRunService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
@@ -24,6 +25,7 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.File;
@@ -66,35 +68,43 @@ public class ConvertRunController {
}
@GetMapping("/history")
@Operation(summary = "查询格式转换历史", description = "查询最近的格式转换成功记录,用于页面右侧历史下载列表展示。")
@Operation(summary = "查询格式转换历史", description = "查询当前用户最近的格式转换成功记录,用于页面右侧历史下载列表展示。")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "查询成功", content = @Content(schema = @Schema(implementation = ConvertHistoryVo.class)))
})
public ApiResponse<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();
vo.setItems(convertRunService.listHistory());
vo.setItems(convertRunService.listHistory(userId));
return ApiResponse.success(vo);
}
@DeleteMapping("/history/{resultId}")
@Operation(summary = "删除格式转换历史", description = "删除一条格式转换历史记录。")
@Operation(summary = "删除格式转换历史", description = "删除当前用户的一条格式转换历史记录。")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "删除成功"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "结果记录不存在")
})
public ApiResponse<Void> deleteHistory(@Parameter(description = "格式转换结果记录 ID", required = true) @PathVariable Long resultId) {
convertRunService.deleteHistory(resultId);
public ApiResponse<Void> deleteHistory(
@Parameter(description = "格式转换结果记录 ID", required = true) @PathVariable Long resultId,
@Parameter(name = "user_id", description = "当前登录用户 ID", required = true, in = ParameterIn.QUERY)
@RequestParam("user_id") Long userId) {
convertRunService.deleteHistory(resultId, userId);
return ApiResponse.success(null);
}
@GetMapping("/results/{resultId}/download")
@Operation(summary = "下载格式转换结果", description = "根据结果记录 ID 下载已生成的转换结果文件。桌面端前端可继续通过 save_file_from_url 选择保存位置。")
@Operation(summary = "下载格式转换结果", description = "根据结果记录 ID 下载当前用户的转换结果文件。桌面端前端可继续通过 save_file_from_url 选择保存位置。")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "返回结果文件流"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "结果文件不存在")
})
public ResponseEntity<Resource> download(@Parameter(description = "格式转换结果记录 ID", required = true) @PathVariable Long resultId) {
File resultFile = convertRunService.getResultFile(resultId);
public ResponseEntity<Resource> download(
@Parameter(description = "格式转换结果记录 ID", required = true) @PathVariable Long resultId,
@Parameter(name = "user_id", description = "当前登录用户 ID", required = true, in = ParameterIn.QUERY)
@RequestParam("user_id") Long userId) {
File resultFile = convertRunService.getResultFile(resultId, userId);
String encodedFilename = URLEncoder.encode(resultFile.getName(), StandardCharsets.UTF_8).replaceAll("\\+", "%20");
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)

View File

@@ -1,9 +1,11 @@
package com.nanri.aiimage.modules.convert.model.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
@@ -23,4 +25,9 @@ public class ConvertRunRequest {
@Schema(description = "文件夹上传场景下的总压缩包名称(不含扩展名)")
private String archiveName;
@NotNull(message = "user_id 不能为空")
@JsonProperty("user_id")
@Schema(description = "当前登录用户 ID")
private Long userId;
}

View File

@@ -71,7 +71,8 @@ public class ConvertRunService {
task.setTaskMode("IMMEDIATE");
task.setStatus("SUCCESS");
task.setSourceFileCount(request.getFiles().size());
task.setCreatedBy("system");
task.setCreatedBy("user:" + request.getUserId());
task.setUserId(request.getUserId());
task.setRequestJson(JSONUtil.toJsonStr(request));
task.setCreatedAt(LocalDateTime.now());
task.setUpdatedAt(LocalDateTime.now());
@@ -130,6 +131,7 @@ public class ConvertRunService {
resultEntity.setResultFileSize(packagedResultFile.length());
resultEntity.setResultContentType(contentType);
resultEntity.setSuccess(1);
resultEntity.setUserId(request.getUserId());
resultEntity.setCreatedAt(LocalDateTime.now());
fileResultMapper.insert(resultEntity);
item.setResultId(resultEntity.getId());
@@ -150,6 +152,7 @@ public class ConvertRunService {
resultEntity.setSourceFilename(sourceFile.getOriginalFilename());
resultEntity.setSuccess(0);
resultEntity.setErrorMessage(ex.getMessage());
resultEntity.setUserId(request.getUserId());
resultEntity.setCreatedAt(LocalDateTime.now());
fileResultMapper.insert(resultEntity);
}
@@ -175,6 +178,7 @@ public class ConvertRunService {
resultEntity.setResultFileSize(zipFile.length());
resultEntity.setResultContentType("application/zip");
resultEntity.setSuccess(1);
resultEntity.setUserId(request.getUserId());
resultEntity.setCreatedAt(LocalDateTime.now());
fileResultMapper.insert(resultEntity);
item.setResultId(resultEntity.getId());
@@ -195,9 +199,10 @@ public class ConvertRunService {
return vo;
}
public List<ConvertResultItemVo> listHistory() {
public List<ConvertResultItemVo> listHistory(Long userId) {
return fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
.eq(FileResultEntity::getUserId, userId)
.orderByDesc(FileResultEntity::getCreatedAt)
.last("limit 200"))
.stream()
@@ -214,17 +219,17 @@ public class ConvertRunService {
.toList();
}
public void deleteHistory(Long resultId) {
public void deleteHistory(Long resultId, Long userId) {
FileResultEntity entity = fileResultMapper.selectById(resultId);
if (entity == null || !MODULE_TYPE.equals(entity.getModuleType())) {
if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {
throw new BusinessException("Convert result record does not exist.");
}
fileResultMapper.deleteById(resultId);
}
public File getResultFile(Long resultId) {
public File getResultFile(Long resultId, Long userId) {
FileResultEntity entity = fileResultMapper.selectById(resultId);
if (entity == null || entity.getResultFileUrl() == null || !MODULE_TYPE.equals(entity.getModuleType())) {
if (entity == null || entity.getResultFileUrl() == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {
throw new BusinessException("Convert result file does not exist.");
}

View File

@@ -7,6 +7,7 @@ import com.nanri.aiimage.modules.dedupe.model.vo.DedupeRunVo;
import com.nanri.aiimage.modules.dedupe.service.DedupeRunService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
@@ -23,6 +24,7 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@@ -60,35 +62,43 @@ public class DedupeRunController {
}
@GetMapping("/history")
@Operation(summary = "查询去重历史", description = "查询最近的去重成功记录,用于页面右侧历史下载列表展示。")
@Operation(summary = "查询去重历史", description = "查询当前用户最近的去重成功记录,用于页面右侧历史下载列表展示。")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "查询成功", content = @Content(schema = @Schema(implementation = DedupeHistoryVo.class)))
})
public ApiResponse<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();
vo.setItems(dedupeRunService.listHistory());
vo.setItems(dedupeRunService.listHistory(userId));
return ApiResponse.success(vo);
}
@DeleteMapping("/history/{resultId}")
@Operation(summary = "删除去重历史", description = "删除一条去重历史记录。")
@Operation(summary = "删除去重历史", description = "删除当前用户的一条去重历史记录。")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "删除成功"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "结果记录不存在")
})
public ApiResponse<Void> deleteHistory(@Parameter(description = "去重结果记录 ID", required = true) @PathVariable Long resultId) {
dedupeRunService.deleteHistory(resultId);
public ApiResponse<Void> deleteHistory(
@Parameter(description = "去重结果记录 ID", required = true) @PathVariable Long resultId,
@Parameter(name = "user_id", description = "当前登录用户 ID", required = true, in = ParameterIn.QUERY)
@RequestParam("user_id") Long userId) {
dedupeRunService.deleteHistory(resultId, userId);
return ApiResponse.success(null);
}
@GetMapping("/results/{resultId}/download")
@Operation(summary = "下载数据去重结果", description = "根据结果记录 ID 下载清洗后 Excel 文件。")
@Operation(summary = "下载数据去重结果", description = "根据结果记录 ID 下载当前用户的清洗后 Excel 文件。")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "返回结果文件流"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "结果文件不存在")
})
public ResponseEntity<Resource> download(@Parameter(description = "去重结果记录 ID", required = true) @PathVariable Long resultId) {
Resource resource = dedupeRunService.getResultFile(resultId);
public ResponseEntity<Resource> download(
@Parameter(description = "去重结果记录 ID", required = true) @PathVariable Long resultId,
@Parameter(name = "user_id", description = "当前登录用户 ID", required = true, in = ParameterIn.QUERY)
@RequestParam("user_id") Long userId) {
Resource resource = dedupeRunService.getResultFile(resultId, userId);
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment")

View File

@@ -1,8 +1,10 @@
package com.nanri.aiimage.modules.dedupe.model.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
@@ -31,4 +33,9 @@ public class DedupeRunRequest {
@Schema(description = "文件夹上传场景下的总压缩包名称(不含扩展名)")
private String archiveName;
@NotNull(message = "user_id 不能为空")
@JsonProperty("user_id")
@Schema(description = "当前登录用户 ID")
private Long userId;
}

View File

@@ -62,7 +62,8 @@ public class DedupeRunService {
task.setTaskMode("IMMEDIATE");
task.setStatus("SUCCESS");
task.setSourceFileCount(request.getFiles().size());
task.setCreatedBy("system");
task.setCreatedBy("user:" + request.getUserId());
task.setUserId(request.getUserId());
task.setRequestJson(JSONUtil.toJsonStr(request));
task.setCreatedAt(LocalDateTime.now());
task.setUpdatedAt(LocalDateTime.now());
@@ -123,6 +124,7 @@ public class DedupeRunService {
resultEntity.setResultFileSize(outputFile.length());
resultEntity.setResultContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
resultEntity.setSuccess(1);
resultEntity.setUserId(request.getUserId());
resultEntity.setCreatedAt(LocalDateTime.now());
fileResultMapper.insert(resultEntity);
} catch (Exception ex) {
@@ -136,6 +138,7 @@ public class DedupeRunService {
resultEntity.setSourceFilename(sourceFile.getOriginalFilename());
resultEntity.setSuccess(0);
resultEntity.setErrorMessage(ex.getMessage());
resultEntity.setUserId(request.getUserId());
resultEntity.setCreatedAt(LocalDateTime.now());
fileResultMapper.insert(resultEntity);
}
@@ -162,6 +165,7 @@ public class DedupeRunService {
resultEntity.setResultFileSize(zipFile.length());
resultEntity.setResultContentType("application/zip");
resultEntity.setSuccess(1);
resultEntity.setUserId(request.getUserId());
resultEntity.setCreatedAt(LocalDateTime.now());
fileResultMapper.insert(resultEntity);
item.setResultId(resultEntity.getId());
@@ -182,9 +186,10 @@ public class DedupeRunService {
return vo;
}
public List<DedupeResultItemVo> listHistory() {
public List<DedupeResultItemVo> listHistory(Long userId) {
return fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
.eq(FileResultEntity::getModuleType, "DEDUPE")
.eq(FileResultEntity::getUserId, userId)
.orderByDesc(FileResultEntity::getCreatedAt)
.last("limit 50"))
.stream()
@@ -201,17 +206,17 @@ public class DedupeRunService {
.toList();
}
public void deleteHistory(Long resultId) {
public void deleteHistory(Long resultId, Long userId) {
FileResultEntity entity = fileResultMapper.selectById(resultId);
if (entity == null || !"DEDUPE".equals(entity.getModuleType())) {
if (entity == null || !"DEDUPE".equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {
throw new BusinessException("记录不存在");
}
fileResultMapper.deleteById(resultId);
}
public Resource getResultFile(Long resultId) {
public Resource getResultFile(Long resultId, Long userId) {
FileResultEntity entity = fileResultMapper.selectById(resultId);
if (entity == null || entity.getResultFileUrl() == null || !"DEDUPE".equals(entity.getModuleType())) {
if (entity == null || entity.getResultFileUrl() == null || !"DEDUPE".equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {
throw new BusinessException("结果文件不存在");
}
File file = new File(entity.getResultFileUrl());

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<>();
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<>();
}

View File

@@ -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) {
}
}

View File

@@ -7,6 +7,7 @@ import com.nanri.aiimage.modules.split.model.vo.SplitRunVo;
import com.nanri.aiimage.modules.split.service.SplitRunService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
@@ -23,6 +24,7 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@@ -59,35 +61,43 @@ public class SplitRunController {
}
@GetMapping("/history")
@Operation(summary = "查询数据拆分历史", description = "查询最近的数据拆分成功记录,用于页面右侧历史下载列表展示。")
@Operation(summary = "查询数据拆分历史", description = "查询当前用户最近的数据拆分成功记录,用于页面右侧历史下载列表展示。")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "查询成功", content = @Content(schema = @Schema(implementation = SplitHistoryVo.class)))
})
public ApiResponse<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();
vo.setItems(splitRunService.listHistory());
vo.setItems(splitRunService.listHistory(userId));
return ApiResponse.success(vo);
}
@DeleteMapping("/history/{resultId}")
@Operation(summary = "删除数据拆分历史", description = "删除一条数据拆分历史记录。")
@Operation(summary = "删除数据拆分历史", description = "删除当前用户的一条数据拆分历史记录。")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "删除成功"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "结果记录不存在")
})
public ApiResponse<Void> deleteHistory(@Parameter(description = "数据拆分结果记录 ID", required = true) @PathVariable Long resultId) {
splitRunService.deleteHistory(resultId);
public ApiResponse<Void> deleteHistory(
@Parameter(description = "数据拆分结果记录 ID", required = true) @PathVariable Long resultId,
@Parameter(name = "user_id", description = "当前登录用户 ID", required = true, in = ParameterIn.QUERY)
@RequestParam("user_id") Long userId) {
splitRunService.deleteHistory(resultId, userId);
return ApiResponse.success(null);
}
@GetMapping("/results/{resultId}/download")
@Operation(summary = "下载数据拆分结果", description = "根据结果记录 ID 下载拆分后的 Excel 文件。")
@Operation(summary = "下载数据拆分结果", description = "根据结果记录 ID 下载当前用户的拆分结果文件。")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "返回结果文件流"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "结果文件不存在")
})
public ResponseEntity<Resource> download(@Parameter(description = "数据拆分结果记录 ID", required = true) @PathVariable Long resultId) {
Resource resource = splitRunService.getResultFile(resultId);
public ResponseEntity<Resource> download(
@Parameter(description = "数据拆分结果记录 ID", required = true) @PathVariable Long resultId,
@Parameter(name = "user_id", description = "当前登录用户 ID", required = true, in = ParameterIn.QUERY)
@RequestParam("user_id") Long userId) {
Resource resource = splitRunService.getResultFile(resultId, userId);
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment")

View File

@@ -1,9 +1,11 @@
package com.nanri.aiimage.modules.split.model.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
@@ -33,4 +35,9 @@ public class SplitRunRequest {
@Schema(description = "文件夹上传场景下的总压缩包名称(不含扩展名)")
private String archiveName;
@NotNull(message = "user_id 不能为空")
@JsonProperty("user_id")
@Schema(description = "当前登录用户 ID")
private Long userId;
}

View File

@@ -67,7 +67,8 @@ public class SplitRunService {
task.setTaskMode("IMMEDIATE");
task.setStatus("SUCCESS");
task.setSourceFileCount(request.getFiles().size());
task.setCreatedBy("system");
task.setCreatedBy("user:" + request.getUserId());
task.setUserId(request.getUserId());
task.setRequestJson(JSONUtil.toJsonStr(request));
task.setCreatedAt(LocalDateTime.now());
task.setUpdatedAt(LocalDateTime.now());
@@ -113,6 +114,7 @@ public class SplitRunService {
resultEntity.setSourceFilename(sourceFile.getOriginalFilename());
resultEntity.setSuccess(0);
resultEntity.setErrorMessage(ex.getMessage());
resultEntity.setUserId(request.getUserId());
resultEntity.setCreatedAt(LocalDateTime.now());
fileResultMapper.insert(resultEntity);
}
@@ -144,6 +146,7 @@ public class SplitRunService {
resultEntity.setResultContentType(ZIP_CONTENT_TYPE);
resultEntity.setRowCount(item.getRowCount());
resultEntity.setSuccess(1);
resultEntity.setUserId(request.getUserId());
resultEntity.setCreatedAt(LocalDateTime.now());
fileResultMapper.insert(resultEntity);
item.setResultId(resultEntity.getId());
@@ -164,9 +167,10 @@ public class SplitRunService {
return vo;
}
public List<SplitResultItemVo> listHistory() {
public List<SplitResultItemVo> listHistory(Long userId) {
return fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
.eq(FileResultEntity::getUserId, userId)
.orderByDesc(FileResultEntity::getCreatedAt)
.last("limit 100"))
.stream()
@@ -186,17 +190,17 @@ public class SplitRunService {
.toList();
}
public void deleteHistory(Long resultId) {
public void deleteHistory(Long resultId, Long userId) {
FileResultEntity entity = fileResultMapper.selectById(resultId);
if (entity == null || !MODULE_TYPE.equals(entity.getModuleType())) {
if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {
throw new BusinessException("Split result record does not exist.");
}
fileResultMapper.deleteById(resultId);
}
public Resource getResultFile(Long resultId) {
public Resource getResultFile(Long resultId, Long userId) {
FileResultEntity entity = fileResultMapper.selectById(resultId);
if (entity == null || entity.getResultFileUrl() == null || !MODULE_TYPE.equals(entity.getModuleType())) {
if (entity == null || entity.getResultFileUrl() == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {
throw new BusinessException("Split result file does not exist.");
}

View File

@@ -24,5 +24,6 @@ public class FileResultEntity {
private Integer rowCount;
private Integer success;
private String errorMessage;
private Long userId;
private LocalDateTime createdAt;
}

View File

@@ -24,6 +24,7 @@ public class FileTaskEntity {
private String resultJson;
private String errorMessage;
private String createdBy;
private Long userId;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private LocalDateTime finishedAt;

View File

@@ -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);
}
}

View File

@@ -1,9 +1,18 @@
package com.nanri.aiimage.modules.ziniao.client;
import com.nanri.aiimage.modules.ziniao.model.cache.ZiniaoShopCacheDto;
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoStaffItemVo;
import java.util.List;
public interface ZiniaoClient {
List<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);
}

View File

@@ -5,6 +5,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.config.ZiniaoProperties;
import com.nanri.aiimage.modules.ziniao.model.cache.ZiniaoShopCacheDto;
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoStaffItemVo;
import com.nanri.aiimage.modules.ziniao.service.ZiniaoSessionCacheService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
@@ -13,6 +15,7 @@ import org.springframework.web.client.RestClient;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@Component
@@ -21,22 +24,222 @@ public class ZiniaoClientImpl implements ZiniaoClient {
private final ZiniaoProperties ziniaoProperties;
private final ObjectMapper objectMapper;
private final ZiniaoSessionCacheService ziniaoSessionCacheService;
@Override
public List<ZiniaoShopCacheDto> listShops() {
if (ziniaoProperties.getCompanyId() == null || ziniaoProperties.getCompanyId() <= 0) {
throw new BusinessException("紫鸟 companyId 未配置");
public String getAppToken() {
String cachedToken = ziniaoSessionCacheService.getAppToken();
if (cachedToken != null && !cachedToken.isBlank()) {
return cachedToken;
}
String raw = getRestClient().post()
.uri(joinUrl(ziniaoProperties.getBaseUrl(), ziniaoProperties.getShopsPath()))
.headers(headers -> {
headers.setBearerAuth(ziniaoProperties.getApiKey());
headers.setContentType(MediaType.APPLICATION_JSON);
})
.body(java.util.Map.of("companyId", ziniaoProperties.getCompanyId()))
String raw = postWithApiKeyEmptyJsonBody(ziniaoProperties.getAppTokenPath(), "获取 appToken");
try {
JsonNode root = objectMapper.readTree(raw);
JsonNode data = firstNonNull(root.get("data"), root.get("result"), root);
String token = text(firstNonNull(
data == null ? null : data.get("appAuthToken"),
data == null ? null : data.get("appToken"),
root.get("appAuthToken"),
root.get("appToken")
));
if (token == null || token.isBlank()) {
throw new BusinessException("紫鸟 appToken 响应缺少 token");
}
ziniaoSessionCacheService.saveAppToken(token);
return token;
} catch (BusinessException ex) {
throw ex;
} catch (Exception ex) {
throw new BusinessException("解析紫鸟 appToken 响应失败");
}
}
@Override
public Long getCompanyIdByApiKey() {
String raw = getWithApiKey("/app/builtin/company", "获取 companyId");
try {
JsonNode root = objectMapper.readTree(raw);
JsonNode data = firstNonNull(root.get("data"), root.get("result"), root);
Long companyId = longValue(firstNonNull(data == null ? null : data.get("companyId"), root.get("companyId")));
if (companyId == null || companyId <= 0) {
throw new BusinessException("紫鸟 companyId 响应缺少 companyId");
}
return companyId;
} catch (BusinessException ex) {
throw ex;
} catch (Exception ex) {
throw new BusinessException("解析紫鸟 companyId 响应失败");
}
}
@Override
public List<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()
.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() {
@@ -46,21 +249,20 @@ public class ZiniaoClientImpl implements ZiniaoClient {
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) {
try {
JsonNode root = objectMapper.readTree(raw);
String code = text(root.get("code"));
if (code != null && !"0".equals(code)) {
throw new BusinessException("紫鸟店铺接口返回失败: " + Objects.toString(text(root.get("msg")), "未知错误"));
}
JsonNode wrapper = root.get("data");
if (wrapper != null && wrapper.isObject()) {
String status = text(wrapper.get("status"));
Long ret = longValue(wrapper.get("ret"));
if ((ret != null && ret != 0L) || (status != null && !"success".equalsIgnoreCase(status))) {
throw new BusinessException("紫鸟店铺接口业务失败: " + Objects.toString(status, "unknown"));
}
}
JsonNode itemsNode = wrapper == null ? null : wrapper.get("data");
List<ZiniaoShopCacheDto> items = new ArrayList<>();
if (itemsNode != null && itemsNode.isArray()) {
@@ -75,12 +277,33 @@ public class ZiniaoClientImpl implements ZiniaoClient {
}
}
return items;
} catch (BusinessException ex) {
throw ex;
} catch (Exception ex) {
throw new BusinessException("解析紫鸟店铺列表失败");
}
}
private List<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) {
String base = Objects.toString(baseUrl, "").trim();
@@ -121,4 +344,11 @@ public class ZiniaoClientImpl implements ZiniaoClient {
return null;
}
}
private String requireText(String value, String message) {
if (value == null || value.isBlank() || "change-me".equalsIgnoreCase(value.trim())) {
throw new BusinessException(message);
}
return value.trim();
}
}

View File

@@ -1,12 +1,11 @@
package com.nanri.aiimage.modules.ziniao.controller;
import com.nanri.aiimage.common.api.ApiResponse;
import com.nanri.aiimage.modules.ziniao.model.dto.ZiniaoSwitchShopRequest;
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoCurrentShopVo;
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoLoginUrlVo;
import com.nanri.aiimage.modules.ziniao.model.dto.ZiniaoOpenShopRequest;
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoOpenShopVo;
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoSessionVo;
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoShopListVo;
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoSwitchShopVo;
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoStaffListVo;
import com.nanri.aiimage.modules.ziniao.service.ZiniaoAuthService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
@@ -15,8 +14,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -27,71 +24,44 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/ziniao")
@Tag(name = "紫鸟接入", description = "紫鸟 ApiKey 模式下的配置状态、店铺列表、当前店铺与切换店铺接口")
@Tag(name = "紫鸟接入", description = "基于 API Key 直连紫鸟接口,获取 appToken、员工、店铺并生成打开店铺链接")
public class ZiniaoAuthController {
private final ZiniaoAuthService ziniaoAuthService;
@GetMapping("/login-url")
@Operation(summary = "获取紫鸟接入信息", description = "兼容保留接口,返回当前 ApiKey 模式的接入信息和 sessionId。")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "获取成功", content = @Content(schema = @Schema(implementation = ZiniaoLoginUrlVo.class)))
})
public ApiResponse<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")
@Operation(summary = "获取紫鸟接入状态", description = "根据 sessionId 获取当前 ApiKey 模式会话与当前店铺信息。")
@Operation(summary = "获取紫鸟会话状态", description = "自动获取或复用 appToken并返回 companyId、当前员工 userId、脱敏 token 和当前店铺信息。")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "获取成功", content = @Content(schema = @Schema(implementation = ZiniaoSessionVo.class)))
})
public ApiResponse<ZiniaoSessionVo> getSession(@RequestParam String sessionId) {
return ApiResponse.success(ziniaoAuthService.getSession(sessionId));
public ApiResponse<ZiniaoSessionVo> getSession(@RequestParam(required = false) String sessionId, @RequestParam(required = false) Long userId) {
return ApiResponse.success(ziniaoAuthService.getSession(sessionId, userId));
}
@GetMapping("/staff")
@Operation(summary = "获取紫鸟员工列表", description = "使用 API Key 直连紫鸟接口,查询当前 companyId 下的员工列表,供前端绑定 employee userId。")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "获取成功", content = @Content(schema = @Schema(implementation = ZiniaoStaffListVo.class)))
})
public ApiResponse<ZiniaoStaffListVo> listStaff() {
return ApiResponse.success(ziniaoAuthService.listStaff());
}
@GetMapping("/shops")
@Operation(summary = "获取紫鸟店铺列表", description = "根据 sessionId 使用配置的 ApiKey companyId 拉取最新店铺列表")
@Operation(summary = "获取员工可见店铺列表", description = "先通过 API Key 获取 companyId,再按员工 userId 查询该员工有权限的店铺列表,并缓存到当前 session")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "获取成功", content = @Content(schema = @Schema(implementation = ZiniaoShopListVo.class)))
})
public ApiResponse<ZiniaoShopListVo> listShops(@RequestParam String sessionId) {
return ApiResponse.success(ziniaoAuthService.listShops(sessionId));
public ApiResponse<ZiniaoShopListVo> listShops(@RequestParam String sessionId, @RequestParam Long userId) {
return ApiResponse.success(ziniaoAuthService.listShops(sessionId, userId));
}
@GetMapping("/shops/current")
@Operation(summary = "获取当前紫鸟店铺", description = "根据 sessionId 获取当前已选中的店铺")
@PostMapping("/shops/open")
@Operation(summary = "生成店铺打开链接", description = "按指定员工 userId 和 shopId 获取员工登录 token并返回可直接拉起紫鸟客户端的 openStoreUrl")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "获取成功", content = @Content(schema = @Schema(implementation = ZiniaoCurrentShopVo.class)))
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "打开链接生成成功", content = @Content(schema = @Schema(implementation = ZiniaoOpenShopVo.class)))
})
public ApiResponse<ZiniaoCurrentShopVo> getCurrentShop(@RequestParam String sessionId) {
return ApiResponse.success(ziniaoAuthService.getCurrentShop(sessionId));
}
@PostMapping("/shops/switch")
@Operation(summary = "切换当前紫鸟店铺", description = "根据 sessionId 和 shopId 切换当前选中的店铺。")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "切换成功", content = @Content(schema = @Schema(implementation = ZiniaoSwitchShopVo.class)))
})
public ApiResponse<ZiniaoSwitchShopVo> switchShop(@Valid @RequestBody ZiniaoSwitchShopRequest request) {
return ApiResponse.success(ziniaoAuthService.switchShop(request));
public ApiResponse<ZiniaoOpenShopVo> openShop(@Valid @RequestBody ZiniaoOpenShopRequest request) {
return ApiResponse.success(ziniaoAuthService.openShop(request));
}
}

View File

@@ -11,5 +11,7 @@ public class ZiniaoSessionCacheDto {
private Long expireAt;
private String ziniaoUserId;
private String nickname;
private Long companyId;
private Long currentUserId;
private String defaultShopId;
}

View File

@@ -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;
}

View File

@@ -1,6 +1,7 @@
package com.nanri.aiimage.modules.ziniao.model.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
@@ -8,6 +9,9 @@ public class ZiniaoSwitchShopRequest {
@NotBlank(message = "sessionId 不能为空")
private String sessionId;
@NotNull(message = "userId 不能为空")
private Long userId;
@NotBlank(message = "shopId 不能为空")
private String shopId;
}

View File

@@ -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;
}

View File

@@ -9,15 +9,30 @@ public class ZiniaoSessionVo {
@Schema(description = "会话 ID")
private String sessionId;
@Schema(description = "是否已认证")
@Schema(description = "是否已启用")
private Boolean enabled;
@Schema(description = "是否已获取应用 token")
private Boolean authenticated;
@Schema(description = "公司 ID")
private Long companyId;
@Schema(description = "当前员工 userId")
private Long currentUserId;
@Schema(description = "紫鸟用户 ID")
private String ziniaoUserId;
@Schema(description = "紫鸟昵称")
private String nickname;
@Schema(description = "脱敏后的 appToken")
private String appToken;
@Schema(description = "token 类型")
private String tokenType;
@Schema(description = "会话过期时间戳(毫秒)")
private Long expireAt;

View File

@@ -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;
}

View File

@@ -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<>();
}

View File

@@ -5,22 +5,22 @@ import com.nanri.aiimage.config.ZiniaoProperties;
import com.nanri.aiimage.modules.ziniao.client.ZiniaoClient;
import com.nanri.aiimage.modules.ziniao.model.cache.ZiniaoSessionCacheDto;
import com.nanri.aiimage.modules.ziniao.model.cache.ZiniaoShopCacheDto;
import com.nanri.aiimage.modules.ziniao.model.dto.ZiniaoSwitchShopRequest;
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoCurrentShopVo;
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoLoginUrlVo;
import com.nanri.aiimage.modules.ziniao.model.dto.ZiniaoOpenShopRequest;
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoOpenShopVo;
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoSessionVo;
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoShopItemVo;
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoShopListVo;
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoSwitchShopVo;
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoStaffItemVo;
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoStaffListVo;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.web.util.UriComponentsBuilder;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Base64;
import java.util.List;
import java.util.UUID;
import java.util.Base64;
@Service
@RequiredArgsConstructor
@@ -30,163 +30,150 @@ public class ZiniaoAuthService {
private final ZiniaoClient ziniaoClient;
private final ZiniaoSessionCacheService ziniaoSessionCacheService;
public ZiniaoLoginUrlVo getLoginUrl() {
public ZiniaoSessionVo getSession(String sessionId, Long userId) {
ensureEnabled();
String sessionId = ensureSession();
ZiniaoLoginUrlVo vo = new ZiniaoLoginUrlVo();
vo.setState(sessionId);
vo.setExpireAt(Instant.now().plusSeconds(ziniaoProperties.getSessionTtlHours() * 3600).toEpochMilli());
vo.setLoginUrl(ziniaoProperties.getBaseUrl());
return vo;
}
public String handleCallback(String code, String state) {
ensureEnabled();
String sessionId = ensureSession();
return ziniaoProperties.getBaseUrl() + "?sessionId=" + sessionId;
}
public ZiniaoSessionVo getSession(String sessionId) {
ZiniaoSessionCacheDto session = requireSession(sessionId);
List<ZiniaoShopCacheDto> shops = ziniaoSessionCacheService.getShops(sessionId);
ZiniaoShopCacheDto currentShop = getOrInitCurrentShop(sessionId, shops);
ZiniaoSessionCacheDto session = sessionId == null || sessionId.isBlank() ? initSession(userId) : requireOrInitSession(sessionId, userId);
ZiniaoSessionVo vo = new ZiniaoSessionVo();
vo.setSessionId(session.getSessionId());
vo.setAuthenticated(true);
vo.setEnabled(ziniaoProperties.isEnabled());
vo.setAuthenticated(session.getAccessToken() != null && !session.getAccessToken().isBlank());
vo.setCompanyId(session.getCompanyId());
vo.setCurrentUserId(session.getCurrentUserId());
vo.setZiniaoUserId(session.getZiniaoUserId());
vo.setNickname(session.getNickname());
vo.setAppToken(session.getAccessToken());
vo.setTokenType(session.getTokenType());
vo.setExpireAt(session.getExpireAt());
vo.setShopCount(shops.size());
if (currentShop != null) {
vo.setCurrentShopId(currentShop.getShopId());
vo.setCurrentShopName(currentShop.getShopName());
}
vo.setShopCount(ziniaoSessionCacheService.getShops(session.getSessionId()).size());
vo.setCurrentShopId(session.getDefaultShopId());
vo.setCurrentShopName(resolveCurrentShopName(session.getSessionId(), session.getDefaultShopId()));
return vo;
}
public ZiniaoShopListVo listShops(String sessionId) {
requireSession(sessionId);
List<ZiniaoShopCacheDto> shops = ziniaoClient.listShops();
ziniaoSessionCacheService.saveShops(sessionId, shops);
ZiniaoShopCacheDto currentShop = getOrInitCurrentShop(sessionId, shops);
public ZiniaoStaffListVo listStaff() {
ensureEnabled();
ZiniaoStaffListVo vo = new ZiniaoStaffListVo();
Long companyId = resolveCompanyIdForStaff();
vo.getItems().addAll(ziniaoClient.listStaff(companyId));
return vo;
}
public ZiniaoShopListVo listShops(String sessionId, Long userId) {
ZiniaoSessionCacheDto session = requireOrInitSession(sessionId, userId);
Long currentUserId = requireUserId(session.getCurrentUserId());
String userToken = ziniaoClient.getUserLoginToken(resolveCompanyId(), currentUserId);
List<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();
for (ZiniaoShopCacheDto shop : shops) {
ZiniaoShopItemVo item = new ZiniaoShopItemVo();
item.setShopId(shop.getShopId());
item.setShopName(shop.getShopName());
item.setPlatform(shop.getPlatform());
item.setSelected(currentShop != null && shop.getShopId().equals(currentShop.getShopId()));
item.setSelected(shop.getShopId() != null && shop.getShopId().equals(session.getDefaultShopId()));
vo.getItems().add(item);
}
return vo;
}
public ZiniaoCurrentShopVo getCurrentShop(String sessionId) {
requireSession(sessionId);
ZiniaoShopCacheDto currentShop = getOrInitCurrentShop(sessionId, ziniaoSessionCacheService.getShops(sessionId));
if (currentShop == null) {
throw new BusinessException("当前会话没有可用店铺");
public ZiniaoOpenShopVo openShop(ZiniaoOpenShopRequest request) {
ZiniaoSessionCacheDto session = requireOrInitSession(request.getSessionId(), request.getUserId());
Long currentUserId = requireUserId(request.getUserId() != null ? request.getUserId() : session.getCurrentUserId());
String userToken = ziniaoClient.getUserLoginToken(resolveCompanyId(), currentUserId);
List<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()
.filter(item -> item.getShopId() != null && item.getShopId().equals(request.getShopId()))
.findFirst()
.orElseThrow(() -> new BusinessException("店铺不存在或不属于当前会话"));
targetShop.setSelectedAt(Instant.now().toEpochMilli());
ziniaoSessionCacheService.saveCurrentShop(request.getSessionId(), targetShop);
ZiniaoSwitchShopVo vo = new ZiniaoSwitchShopVo();
vo.setSuccess(true);
vo.setCurrentShopId(targetShop.getShopId());
vo.setCurrentShopName(targetShop.getShopName());
vo.setOpenStoreUrl(buildOpenStoreUrl(targetShop));
.orElseThrow(() -> new BusinessException("店铺不存在或不属于当前员工"));
session.setCurrentUserId(currentUserId);
session.setDefaultShopId(targetShop.getShopId());
ziniaoSessionCacheService.saveSession(session);
ziniaoSessionCacheService.saveCurrentShop(session.getSessionId(), targetShop);
ZiniaoOpenShopVo vo = new ZiniaoOpenShopVo();
vo.setUserId(currentUserId);
vo.setShopId(targetShop.getShopId());
vo.setShopName(targetShop.getShopName());
vo.setLoginToken(userToken);
vo.setOpenStoreUrl(buildOpenStoreUrl(targetShop.getShopId(), currentUserId, userToken));
return vo;
}
public String buildFailureRedirect(String message) {
return ziniaoProperties.getBaseUrl() + "?message=" + (message == null || message.isBlank() ? "紫鸟授权失败" : message);
public List<ZiniaoShopCacheDto> listShopsForConfiguredUser() {
ensureEnabled();
Long userId = parseConfiguredOpenStoreUserId();
String userToken = ziniaoClient.getUserLoginToken(resolveCompanyId(), userId);
return ziniaoClient.listUserStores(resolveCompanyId(), userId, userToken);
}
private String ensureSession() {
public String buildOpenStoreUrlForShop(ZiniaoShopCacheDto shop) {
ensureEnabled();
if (shop == null || shop.getShopId() == null || shop.getShopId().isBlank()) {
throw new BusinessException("店铺不存在");
}
Long userId = parseConfiguredOpenStoreUserId();
String loginToken = ziniaoClient.getUserLoginToken(resolveCompanyId(), userId);
return buildOpenStoreUrl(shop.getShopId(), userId, loginToken);
}
private ZiniaoSessionCacheDto initSession(Long userId) {
String sessionId = "ziniao_" + UUID.randomUUID().toString().replace("-", "");
ZiniaoSessionCacheDto session = new ZiniaoSessionCacheDto();
session.setSessionId(sessionId);
session.setAccessToken(maskApiKey(ziniaoProperties.getApiKey()));
String appToken = ziniaoClient.getAppToken();
session.setAccessToken(maskToken(appToken));
session.setTokenType("Bearer");
session.setExpireAt(Instant.now().plusSeconds(ziniaoProperties.getSessionTtlHours() * 3600).toEpochMilli());
session.setZiniaoUserId(String.valueOf(defaultCompanyId()));
session.setNickname("ApiKey模式");
session.setCompanyId(resolveCompanyId());
session.setCurrentUserId(userId);
session.setZiniaoUserId(String.valueOf(userId == null ? 0L : userId));
session.setNickname("API Key模式");
ziniaoSessionCacheService.saveSession(session);
return sessionId;
return session;
}
private ZiniaoSessionCacheDto requireSession(String sessionId) {
ensureEnabled();
private ZiniaoSessionCacheDto requireOrInitSession(String sessionId, Long userId) {
if (sessionId == null || sessionId.isBlank()) {
throw new BusinessException("sessionId 不能为空");
return initSession(userId);
}
ZiniaoSessionCacheDto session = ziniaoSessionCacheService.getSession(sessionId);
if (session == null) {
throw new BusinessException("当前会话不存在或已过期");
return initSession(userId);
}
if (userId != null && userId > 0) {
session.setCurrentUserId(userId);
session.setZiniaoUserId(String.valueOf(userId));
ziniaoSessionCacheService.saveSession(session);
}
return session;
}
private ZiniaoShopCacheDto getOrInitCurrentShop(String sessionId, List<ZiniaoShopCacheDto> shops) {
ZiniaoShopCacheDto current = ziniaoSessionCacheService.getCurrentShop(sessionId);
if (current != null) {
return current;
}
if (shops == null || shops.isEmpty()) {
private String resolveCurrentShopName(String sessionId, String currentShopId) {
if (currentShopId == null || currentShopId.isBlank()) {
return null;
}
ZiniaoShopCacheDto first = shops.get(0);
first.setSelectedAt(Instant.now().toEpochMilli());
ziniaoSessionCacheService.saveCurrentShop(sessionId, first);
return first;
return ziniaoSessionCacheService.getShops(sessionId).stream()
.filter(item -> currentShopId.equals(item.getShopId()))
.map(ZiniaoShopCacheDto::getShopName)
.findFirst()
.orElse(null);
}
private void ensureEnabled() {
if (!ziniaoProperties.isEnabled()) {
throw new BusinessException("紫鸟集成未启用,请先配置环境变量");
}
if (ziniaoProperties.getApiKey() == null || ziniaoProperties.getApiKey().isBlank() || "change-me".equalsIgnoreCase(ziniaoProperties.getApiKey())) {
throw new BusinessException("紫鸟 ApiKey 未配置");
}
}
private long defaultCompanyId() {
return ziniaoProperties.getCompanyId() == null ? 0L : ziniaoProperties.getCompanyId();
}
private String maskApiKey(String apiKey) {
if (apiKey == null || apiKey.isBlank()) {
return "";
}
if (apiKey.length() <= 8) {
return apiKey;
}
return apiKey.substring(0, 4) + "****" + apiKey.substring(apiKey.length() - 4);
}
private String buildOpenStoreUrl(ZiniaoShopCacheDto shop) {
private String buildOpenStoreUrl(String storeId, Long userId, String loginToken) {
String scheme = requireText(ziniaoProperties.getOpenStoreScheme(), "紫鸟 open-store-scheme 未配置");
String userId = requireText(ziniaoProperties.getOpenStoreUserId(), "紫鸟 open-store-user-id 未配置");
String apiKey = requireText(ziniaoProperties.getApiKey(), "紫鸟 ApiKey 未配置");
String launchUrl = requireText(ziniaoProperties.getOpenStoreLaunchUrl(), "紫鸟 open-store-launch-url 未配置");
String encodedLaunchUrl = Base64.getEncoder().encodeToString(launchUrl.getBytes(StandardCharsets.UTF_8));
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(scheme)
.queryParam("storeId", shop.getShopId())
.queryParam("openapiToken", apiKey)
.queryParam("storeId", storeId)
.queryParam("openapiToken", loginToken)
.queryParam("userId", userId)
.queryParam("lanuchUrl", encodedLaunchUrl)
.queryParam("autoopen", Boolean.TRUE.equals(ziniaoProperties.getOpenStoreAutoOpen()))
@@ -202,8 +189,56 @@ public class ZiniaoAuthService {
return builder.build(true).toUriString();
}
private void ensureEnabled() {
if (!ziniaoProperties.isEnabled()) {
throw new BusinessException("紫鸟集成未启用,请先配置环境变量");
}
requireText(ziniaoProperties.getApiKey(), "紫鸟 apiKey 未配置");
}
private Long resolveCompanyIdForStaff() {
return resolveCompanyId();
}
private Long requireUserId(Long userId) {
if (userId == null || userId <= 0) {
throw new BusinessException("请先选择紫鸟员工");
}
return userId;
}
private Long resolveCompanyId() {
if (ziniaoProperties.getCompanyId() != null && ziniaoProperties.getCompanyId() > 0) {
return ziniaoProperties.getCompanyId();
}
return ziniaoClient.getCompanyIdByApiKey();
}
private Long requireCompanyId() {
return resolveCompanyId();
}
private Long parseConfiguredOpenStoreUserId() {
String configured = requireText(ziniaoProperties.getOpenStoreUserId(), "紫鸟 open-store-user-id 未配置");
try {
return Long.parseLong(configured);
} catch (Exception ex) {
throw new BusinessException("紫鸟 open-store-user-id 配置不合法");
}
}
private String maskToken(String token) {
if (token == null || token.isBlank()) {
return "";
}
if (token.length() <= 8) {
return token;
}
return token.substring(0, 4) + "****" + token.substring(token.length() - 4);
}
private String requireText(String value, String message) {
if (value == null || value.isBlank()) {
if (value == null || value.isBlank() || "change-me".equalsIgnoreCase(value.trim())) {
throw new BusinessException(message);
}
return value.trim();

View File

@@ -17,10 +17,27 @@ import java.util.List;
@RequiredArgsConstructor
public class ZiniaoSessionCacheService {
private static final String APP_TOKEN_KEY = "ziniao:app-token";
private final StringRedisTemplate stringRedisTemplate;
private final ObjectMapper objectMapper;
private final ZiniaoProperties ziniaoProperties;
public void saveAppToken(String appToken) {
if (appToken == null || appToken.isBlank()) {
return;
}
stringRedisTemplate.opsForValue().set(APP_TOKEN_KEY, appToken.trim(), Duration.ofHours(ziniaoProperties.getSessionTtlHours()));
}
public String getAppToken() {
String value = stringRedisTemplate.opsForValue().get(APP_TOKEN_KEY);
if (value == null || value.isBlank()) {
return null;
}
return value.trim();
}
public void saveSession(ZiniaoSessionCacheDto session) {
try {
stringRedisTemplate.opsForValue().set(buildSessionKey(session.getSessionId()), objectMapper.writeValueAsString(session), Duration.ofHours(ziniaoProperties.getSessionTtlHours()));

View File

@@ -15,12 +15,20 @@ AIIMAGE_OSS_ACCESS_KEY_SECRET=bQSZnFH455i8tzyOgeahJmUzwmhynz
AIIMAGE_STORAGE_LOCAL_TEMP_DIR=F:/project/crawler-plugin/backend-java/data/tmp
AIIMAGE_MODULE_CLEANUP_ENABLED=true
AIIMAGE_MODULE_CLEANUP_CRON=0 0 0 * * *
AIIMAGE_MODULE_CLEANUP_MODULE_TYPES=DEDUPE,SPLIT,CONVERT,DELETE_BRAND
AIIMAGE_ZINIAO_ENABLED=false
AIIMAGE_ZINIAO_BASE_URL=https://sbappstoreapi.ziniao.com/openapi-router
AIIMAGE_ZINIAO_API_KEY=change-me
AIIMAGE_ZINIAO_APP_ID=change-me
AIIMAGE_ZINIAO_APP_SECRET=change-me
AIIMAGE_ZINIAO_COMPANY_ID=0
AIIMAGE_ZINIAO_SHOPS_PATH=/superbrowser/rest/v1/erp/shop/list
AIIMAGE_ZINIAO_SWITCH_SHOP_PATH=
AIIMAGE_ZINIAO_APP_TOKEN_PATH=/auth/get_app_token
AIIMAGE_ZINIAO_STAFF_LIST_PATH=/superbrowser/rest/v1/erp/staff/list
AIIMAGE_ZINIAO_USER_STORES_PATH=/superbrowser/rest/v1/erp/user/stores
AIIMAGE_ZINIAO_USER_LOGIN_TOKEN_PATH=/superbrowser/rest/v1/token/user-login
AIIMAGE_ZINIAO_OPEN_STORE_SCHEME=superbrowser://OpenStrore
AIIMAGE_ZINIAO_OPEN_STORE_USER_ID=
AIIMAGE_ZINIAO_OPEN_STORE_LAUNCH_URL=https://www.baidu.com

View File

@@ -72,13 +72,21 @@ aiimage:
failed-ttl-hours: ${AIIMAGE_BRAND_PROGRESS_FAILED_TTL_HOURS:2}
heartbeat-timeout-minutes: ${AIIMAGE_BRAND_PROGRESS_HEARTBEAT_TIMEOUT_MINUTES:15}
stale-check-cron: ${AIIMAGE_BRAND_PROGRESS_STALE_CHECK_CRON:0 */2 * * * *}
module-cleanup:
enabled: ${AIIMAGE_MODULE_CLEANUP_ENABLED:true}
cron: ${AIIMAGE_MODULE_CLEANUP_CRON:0 0 0 * * *}
module-types: ${AIIMAGE_MODULE_CLEANUP_MODULE_TYPES:DEDUPE,SPLIT,CONVERT,DELETE_BRAND}
ziniao:
enabled: ${AIIMAGE_ZINIAO_ENABLED:false}
base-url: ${AIIMAGE_ZINIAO_BASE_URL:https://sbappstoreapi.ziniao.com/openapi-router}
api-key: ${AIIMAGE_ZINIAO_API_KEY:change-me}
api-key: ${AIIMAGE_ZINIAO_API_KEY:}
app-id: ${AIIMAGE_ZINIAO_APP_ID:change-me}
app-secret: ${AIIMAGE_ZINIAO_APP_SECRET:change-me}
company-id: ${AIIMAGE_ZINIAO_COMPANY_ID:0}
shops-path: ${AIIMAGE_ZINIAO_SHOPS_PATH:/superbrowser/rest/v1/erp/shop/list}
switch-shop-path: ${AIIMAGE_ZINIAO_SWITCH_SHOP_PATH:}
app-token-path: ${AIIMAGE_ZINIAO_APP_TOKEN_PATH:/auth/get_app_token}
staff-list-path: ${AIIMAGE_ZINIAO_STAFF_LIST_PATH:/superbrowser/rest/v1/erp/staff/list}
user-stores-path: ${AIIMAGE_ZINIAO_USER_STORES_PATH:/superbrowser/rest/v1/erp/user/stores}
user-login-token-path: ${AIIMAGE_ZINIAO_USER_LOGIN_TOKEN_PATH:/superbrowser/rest/v1/token/user-login}
open-store-scheme: ${AIIMAGE_ZINIAO_OPEN_STORE_SCHEME:superbrowser://OpenStrore}
open-store-user-id: ${AIIMAGE_ZINIAO_OPEN_STORE_USER_ID:}
open-store-launch-url: ${AIIMAGE_ZINIAO_OPEN_STORE_LAUNCH_URL:https://www.baidu.com}