From 485b4dd4851b1283b2d6d4743d088dc6c7d0f58f Mon Sep 17 00:00:00 2001
From: super <2903208875@qq.com>
Date: Tue, 24 Mar 2026 22:22:37 +0800
Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E5=93=81=E7=89=8C=E9=83=A8?=
=?UTF-8?q?=E5=88=86=E6=94=B9=E9=80=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
backend-java/pom.xml | 4 +
.../com/nanri/aiimage/AiImageApplication.java | 2 +
.../config/BrandProgressProperties.java | 13 +
.../aiimage/config/PropertiesConfig.java | 2 +-
.../aiimage/config/StorageProperties.java | 4 +
.../brand/controller/BrandTaskController.java | 207 ++++
.../brand/mapper/BrandCrawlTaskMapper.java | 9 +
.../model/dto/BrandCrawlResultFileDto.java | 48 +
.../model/dto/BrandCrawlResultRequest.java | 21 +
.../brand/model/dto/BrandInvalidBrandDto.java | 18 +
.../model/dto/BrandParsedFileCacheDto.java | 16 +
.../brand/model/dto/BrandQueryFailedDto.java | 15 +
.../brand/model/dto/BrandSourceFileDto.java | 20 +
.../model/dto/BrandTaskCreateRequest.java | 27 +
.../brand/model/dto/BrandTaskRunRequest.java | 20 +
.../model/entity/BrandCrawlTaskEntity.java | 33 +
.../brand/model/vo/BrandCrawlPayloadVo.java | 28 +
.../modules/brand/model/vo/BrandSimpleVo.java | 13 +
.../brand/model/vo/BrandTaskCreateVo.java | 11 +
.../brand/model/vo/BrandTaskDetailVo.java | 11 +
.../brand/model/vo/BrandTaskItemVo.java | 23 +
.../brand/model/vo/BrandTaskListVo.java | 13 +
.../vo/LegacyBrandLineProgressInfoVo.java | 27 +
.../model/vo/LegacyBrandLineProgressVo.java | 15 +
.../model/vo/LegacyBrandTaskCreateVo.java | 9 +
.../model/vo/LegacyBrandTaskDetailVo.java | 15 +
.../brand/model/vo/LegacyBrandTaskItemVo.java | 44 +
.../brand/model/vo/LegacyBrandTaskListVo.java | 11 +
.../BrandTaskProgressCacheService.java | 182 +++
.../brand/service/BrandTaskService.java | 1025 +++++++++++++++++
.../controller/DedupeRunController.java | 2 +
.../dedupe/mapper/DedupeTotalDataMapper.java | 16 +
.../dedupe/service/DedupeRunService.java | 36 +-
.../service/DedupeTotalDataService.java | 23 +
.../file/service/LocalFileStorageService.java | 2 +-
.../file/service/LocalTempCleanupService.java | 116 ++
.../src/main/resources/application.yml | 16 +
37 files changed, 2086 insertions(+), 11 deletions(-)
create mode 100644 backend-java/src/main/java/com/nanri/aiimage/config/BrandProgressProperties.java
create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/brand/controller/BrandTaskController.java
create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/brand/mapper/BrandCrawlTaskMapper.java
create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/dto/BrandCrawlResultFileDto.java
create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/dto/BrandCrawlResultRequest.java
create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/dto/BrandInvalidBrandDto.java
create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/dto/BrandParsedFileCacheDto.java
create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/dto/BrandQueryFailedDto.java
create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/dto/BrandSourceFileDto.java
create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/dto/BrandTaskCreateRequest.java
create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/dto/BrandTaskRunRequest.java
create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/entity/BrandCrawlTaskEntity.java
create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/vo/BrandCrawlPayloadVo.java
create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/vo/BrandSimpleVo.java
create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/vo/BrandTaskCreateVo.java
create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/vo/BrandTaskDetailVo.java
create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/vo/BrandTaskItemVo.java
create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/vo/BrandTaskListVo.java
create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/vo/LegacyBrandLineProgressInfoVo.java
create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/vo/LegacyBrandLineProgressVo.java
create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/vo/LegacyBrandTaskCreateVo.java
create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/vo/LegacyBrandTaskDetailVo.java
create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/vo/LegacyBrandTaskItemVo.java
create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/vo/LegacyBrandTaskListVo.java
create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/brand/service/BrandTaskProgressCacheService.java
create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/brand/service/BrandTaskService.java
create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/file/service/LocalTempCleanupService.java
diff --git a/backend-java/pom.xml b/backend-java/pom.xml
index bf45bcb..187f51e 100644
--- a/backend-java/pom.xml
+++ b/backend-java/pom.xml
@@ -42,6 +42,10 @@
org.springframework.boot
spring-boot-starter-security
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
com.baomidou
mybatis-plus-spring-boot3-starter
diff --git a/backend-java/src/main/java/com/nanri/aiimage/AiImageApplication.java b/backend-java/src/main/java/com/nanri/aiimage/AiImageApplication.java
index a85d792..f86b27e 100644
--- a/backend-java/src/main/java/com/nanri/aiimage/AiImageApplication.java
+++ b/backend-java/src/main/java/com/nanri/aiimage/AiImageApplication.java
@@ -2,8 +2,10 @@ package com.nanri.aiimage;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
+@EnableScheduling
public class AiImageApplication {
public static void main(String[] args) {
diff --git a/backend-java/src/main/java/com/nanri/aiimage/config/BrandProgressProperties.java b/backend-java/src/main/java/com/nanri/aiimage/config/BrandProgressProperties.java
new file mode 100644
index 0000000..c270006
--- /dev/null
+++ b/backend-java/src/main/java/com/nanri/aiimage/config/BrandProgressProperties.java
@@ -0,0 +1,13 @@
+package com.nanri.aiimage.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@Data
+@ConfigurationProperties(prefix = "aiimage.brand-progress")
+public class BrandProgressProperties {
+ private long ttlHours = 24;
+ private long failedTtlHours = 2;
+ private long heartbeatTimeoutMinutes = 15;
+ private String staleCheckCron = "0 */2 * * * *";
+}
diff --git a/backend-java/src/main/java/com/nanri/aiimage/config/PropertiesConfig.java b/backend-java/src/main/java/com/nanri/aiimage/config/PropertiesConfig.java
index 2b77762..5a099af 100644
--- a/backend-java/src/main/java/com/nanri/aiimage/config/PropertiesConfig.java
+++ b/backend-java/src/main/java/com/nanri/aiimage/config/PropertiesConfig.java
@@ -4,6 +4,6 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Configuration;
@Configuration
-@EnableConfigurationProperties({OssProperties.class, StorageProperties.class})
+@EnableConfigurationProperties({OssProperties.class, StorageProperties.class, BrandProgressProperties.class})
public class PropertiesConfig {
}
diff --git a/backend-java/src/main/java/com/nanri/aiimage/config/StorageProperties.java b/backend-java/src/main/java/com/nanri/aiimage/config/StorageProperties.java
index da584bf..7eb4830 100644
--- a/backend-java/src/main/java/com/nanri/aiimage/config/StorageProperties.java
+++ b/backend-java/src/main/java/com/nanri/aiimage/config/StorageProperties.java
@@ -7,4 +7,8 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "aiimage.storage")
public class StorageProperties {
private String localTempDir;
+ private boolean cleanupEnabled = true;
+ private String cleanupCron = "0 0 */6 * * *";
+ private long sourceRetentionHours = 48;
+ private long resultRetentionHours = 24;
}
diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/brand/controller/BrandTaskController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/brand/controller/BrandTaskController.java
new file mode 100644
index 0000000..b4ad774
--- /dev/null
+++ b/backend-java/src/main/java/com/nanri/aiimage/modules/brand/controller/BrandTaskController.java
@@ -0,0 +1,207 @@
+package com.nanri.aiimage.modules.brand.controller;
+
+import com.nanri.aiimage.common.api.ApiResponse;
+import com.nanri.aiimage.modules.brand.model.dto.BrandCrawlResultRequest;
+import com.nanri.aiimage.modules.brand.model.dto.BrandTaskCreateRequest;
+import com.nanri.aiimage.modules.brand.model.vo.BrandCrawlPayloadVo;
+import com.nanri.aiimage.modules.brand.model.vo.BrandSimpleVo;
+import com.nanri.aiimage.modules.brand.model.vo.LegacyBrandTaskDetailVo;
+import com.nanri.aiimage.modules.brand.model.vo.LegacyBrandTaskListVo;
+import com.nanri.aiimage.modules.brand.service.BrandTaskService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+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.http.HttpHeaders;
+import org.springframework.http.ResponseEntity;
+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/brand")
+@Tag(name = "品牌任务", description = "品牌任务接口。Java 端只负责下载并解析 OSS 文件、返回待爬取数据、接收爬取结果并生成 xlsx/上传 OSS/落旧表,不负责实际爬虫执行。")
+public class BrandTaskController {
+
+ private final BrandTaskService brandTaskService;
+
+ @PostMapping("/tasks")
+ @Operation(
+ summary = "创建品牌任务并返回待爬取数据",
+ description = """
+ 创建一条品牌任务记录到旧表 brand_crawl_tasks,随后立即下载并读取 Excel,
+ 将原始解析结果临时缓存到后端,并把 sheetName、columns、rows、uniqueBrands 等待爬取数据返回给前端。
+
+ 适用流程:
+ 1. 前端先拿到 OSS 文件链接;
+ 2. 再调用本接口创建任务;
+ 3. 前端拿到返回数据后自行执行品牌爬取;
+ 4. 爬取结束后调用结果提交接口回传结果。
+ """)
+ @ApiResponses({
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "创建成功,返回任务 ID 和待爬取数据", content = @Content(schema = @Schema(implementation = BrandCrawlPayloadVo.class))),
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "请求参数不合法或文件列表为空"),
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "500", description = "下载或解析 Excel 或创建任务失败")
+ })
+ public ApiResponse createTask(
+ @Parameter(description = "用户 ID", required = true) @RequestParam Long userId,
+ @Valid @RequestBody BrandTaskCreateRequest request) {
+ return ApiResponse.success(brandTaskService.createTaskAndBuildPayload(userId, request));
+ }
+
+ @GetMapping("/tasks")
+ @Operation(
+ summary = "获取品牌任务列表",
+ description = """
+ 查询旧表 brand_crawl_tasks 最近的任务列表,返回前端任务面板需要的核心信息。
+
+ 返回内容包括:
+ - id:任务 ID;
+ - file_paths:任务创建时记录的源文件信息;
+ - desc:任务描述,通常由 strategy + 文件名摘要组成;
+ - strategy:品牌匹配方式;
+ - status:任务状态(pending/running/success/failed/cancelled);
+ - result_paths:结果文件地址信息,通常包含 urls 和 zip_url;
+ - error_message:失败原因;
+ - progress_current / progress_total:当前进度;
+ - created_at / updated_at:创建和更新时间。
+
+ 典型用途:
+ - 页面任务列表展示;
+ - 判断任务是否已完成;
+ - 决定是否显示下载按钮;
+ - 展示失败原因和进度。
+ """
+ )
+ @ApiResponses({
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "查询成功,返回任务列表", content = @Content(schema = @Schema(implementation = LegacyBrandTaskListVo.class)))
+ })
+ public ApiResponse listTasks(
+ @Parameter(description = "用户 ID", required = true) @RequestParam Long userId) {
+ return ApiResponse.success(brandTaskService.listTasksLegacy(userId));
+ }
+
+ @GetMapping("/tasks/{taskId}")
+ @Operation(
+ summary = "获取品牌任务详情",
+ description = """
+ 根据任务 ID 查询单条品牌任务详情,返回任务基础信息 + 文件级摘要进度 + Redis 中基于结果分片累计出的实时行级进度。
+
+ 返回内容包括:
+ - task.id:任务 ID;
+ - task.file_paths:任务绑定的源文件信息;
+ - task.desc / task.strategy / task.status:任务描述、策略、状态;
+ - task.result_paths:结果文件地址。单文件场景通常只有 urls,多文件场景通常同时包含 urls 和 zip_url;
+ - task.error_message:失败原因;
+ - task.progress_current / task.progress_total:文件级摘要进度,表示已完成文件数 / 总文件数;
+ - task.created_at / task.updated_at:创建和更新时间;
+ - line_progress.has_progress:当前是否存在 Redis 实时进度;
+ - line_progress.info.file_index / file_total:当前处理到第几个文件 / 总文件数;
+ - line_progress.info.file_name:当前处理文件名;
+ - line_progress.info.current_line / total_lines:当前文件已处理行数 / 总行数;
+ - line_progress.info.phase:当前阶段,可能为 crawling / assembling / uploading / failed。
+
+ 典型用途:
+ - 前端轮询任务状态;
+ - 展示当前文件与当前行进度;
+ - 提交结果后确认任务是否已 success;
+ - 下载前校验任务结果是否已生成;
+ - 任务失败时获取具体错误信息。
+ """
+ )
+ @ApiResponses({
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "查询成功,返回单条任务详情", content = @Content(schema = @Schema(implementation = LegacyBrandTaskDetailVo.class))),
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "任务不存在")
+ })
+ public ApiResponse getTask(
+ @Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId) {
+ return ApiResponse.success(brandTaskService.getTaskDetailLegacy(taskId));
+ }
+
+ @PostMapping("/tasks/{taskId}/result")
+ @Operation(
+ summary = "提交品牌任务爬取结果",
+ description = """
+ 前端完成品牌爬取后,将按品牌汇总后的结果分片提交给本接口。
+ Java 端会累计接收前端每次提交的少量结果(例如 5 个品牌),
+ 并直接根据 keptRows / invalidBrands / queryFailedBrands 的数量推进 Redis 中的实时进度。
+ 在所有文件都提交完成后再基于创建任务时缓存的原始数据统一:
+ - 按“品牌”列删除 invalidBrands 对应的原始行,生成主 sheet;
+ - keptRows 用于统计已处理且保留的品牌进度;
+ - 生成“不符合品牌”和“查询失败品牌” sheet;
+ - 输出 xlsx;
+ - 上传 OSS;
+ - 回写 brand_crawl_tasks.result_paths/status/error_message;
+
+ 处理期间前端可轮询任务详情接口 `GET /api/brand/tasks/{taskId}`,
+ 获取 status、progress_current、progress_total、error_message、line_progress 等实时进度信息。
+ """)
+ @ApiResponses({
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "提交成功,结果文件已生成并落库", content = @Content(schema = @Schema(implementation = BrandSimpleVo.class))),
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "请求参数不合法、fileUrl 不匹配或结果数据为空"),
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "500", description = "生成结果文件或上传 OSS 失败")
+ })
+ public ApiResponse submitResult(
+ @Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId,
+ @Valid @RequestBody BrandCrawlResultRequest request) {
+ brandTaskService.submitCrawlResult(taskId, request);
+ return ApiResponse.success(new BrandSimpleVo(true));
+ }
+
+ @PostMapping("/tasks/{taskId}/cancel")
+ @Operation(
+ summary = "取消品牌任务",
+ description = "将任务状态从 pending/running 更新为 cancelled。适用于前端停止本地爬取时同步取消任务记录。"
+ )
+ @ApiResponses({
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "取消成功", content = @Content(schema = @Schema(implementation = BrandSimpleVo.class))),
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "任务不存在或当前状态不可取消")
+ })
+ public ApiResponse cancelTask(
+ @Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId) {
+ brandTaskService.cancelTask(taskId);
+ return ApiResponse.success(new BrandSimpleVo(true));
+ }
+
+ @DeleteMapping("/tasks/{taskId}")
+ @Operation(
+ summary = "删除品牌任务",
+ description = "删除旧表 brand_crawl_tasks 中的任务记录。仅允许删除非 running 状态任务。"
+ )
+ @ApiResponses({
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "删除成功", content = @Content(schema = @Schema(implementation = BrandSimpleVo.class))),
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "任务不存在或正在执行中不可删除")
+ })
+ public ApiResponse deleteTask(
+ @Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId) {
+ brandTaskService.deleteTask(taskId);
+ return ApiResponse.success(new BrandSimpleVo(true));
+ }
+
+ @GetMapping("/tasks/{taskId}/download")
+ @Operation(
+ summary = "下载品牌任务结果",
+ description = "根据任务 ID 读取 brand_crawl_tasks.result_paths,优先跳转 zip_url;若没有 zip_url,则跳转第一个可用结果 URL。"
+ )
+ @ApiResponses({
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "302", description = "重定向到 OSS 结果文件地址"),
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "任务不存在或无结果可下载")
+ })
+ public ResponseEntity download(
+ @Parameter(description = "品牌任务 ID", required = true) @PathVariable Long taskId) {
+ String url = brandTaskService.resolveDownloadUrl(taskId);
+ return ResponseEntity.status(302)
+ .header(HttpHeaders.LOCATION, url)
+ .build();
+ }
+}
diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/brand/mapper/BrandCrawlTaskMapper.java b/backend-java/src/main/java/com/nanri/aiimage/modules/brand/mapper/BrandCrawlTaskMapper.java
new file mode 100644
index 0000000..50afef9
--- /dev/null
+++ b/backend-java/src/main/java/com/nanri/aiimage/modules/brand/mapper/BrandCrawlTaskMapper.java
@@ -0,0 +1,9 @@
+package com.nanri.aiimage.modules.brand.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.nanri.aiimage.modules.brand.model.entity.BrandCrawlTaskEntity;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface BrandCrawlTaskMapper extends BaseMapper {
+}
diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/dto/BrandCrawlResultFileDto.java b/backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/dto/BrandCrawlResultFileDto.java
new file mode 100644
index 0000000..79804db
--- /dev/null
+++ b/backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/dto/BrandCrawlResultFileDto.java
@@ -0,0 +1,48 @@
+package com.nanri.aiimage.modules.brand.model.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+@Schema(description = "单个品牌文件结果分片。前端只需按品牌提交保留品牌、不符合品牌、查询失败品牌;后端会基于创建任务时缓存的原始行数据生成最终结果文件。")
+public class BrandCrawlResultFileDto {
+
+ @NotBlank(message = "fileUrl 不能为空")
+ @Schema(description = "源文件 OSS 链接,必须与创建任务时的文件项一一对应。")
+ private String fileUrl;
+
+ @Schema(description = "原始文件名。为空时会回退使用任务创建时记录的 originalFilename。")
+ private String originalFilename;
+
+ @Schema(description = "相对路径。文件夹上传场景下用于生成 zip 时恢复目录层级。")
+ private String relativePath;
+
+ @Schema(description = "主 sheet 名称。为空时默认输出为 Sheet1。")
+ private String mainSheetName;
+
+ @NotNull(message = "chunkIndex 不能为空")
+ @Schema(description = "当前分片序号,从 1 开始。", example = "1")
+ private Integer chunkIndex;
+
+ @NotNull(message = "chunkTotal 不能为空")
+ @Schema(description = "当前文件总分片数。", example = "10")
+ private Integer chunkTotal;
+
+ @Schema(description = "当前文件总行数。后端据此展示精确行级进度。", example = "1000")
+ private Integer totalLines;
+
+ @Schema(description = "本批处理后应保留的品牌列表。仅用于后端累计进度,不需要回传整行数据。")
+ private List keptRows;
+
+ @Valid
+ @Schema(description = "本批处理后判定为不符合品牌的记录列表。会写入“不符合品牌” sheet;删除主 sheet 数据时仅按 brand 字段匹配删除。")
+ private List invalidBrands;
+
+ @Schema(description = "本批查询失败的品牌列表。后端会写入“查询失败品牌” sheet。")
+ private List queryFailedBrands;
+}
diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/dto/BrandCrawlResultRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/dto/BrandCrawlResultRequest.java
new file mode 100644
index 0000000..0e2ee37
--- /dev/null
+++ b/backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/dto/BrandCrawlResultRequest.java
@@ -0,0 +1,21 @@
+package com.nanri.aiimage.modules.brand.model.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotEmpty;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+@Schema(description = "品牌爬取结果提交请求。前端按文件分片提交不符合品牌数据和查询失败品牌数据;后端会基于创建任务时缓存的原始行数据生成最终结果文件。")
+public class BrandCrawlResultRequest {
+
+ @Schema(description = "品牌匹配方式。应与创建任务时使用的 strategy 保持一致。", example = "Terms")
+ private String strategy = "Terms";
+
+ @Valid
+ @NotEmpty(message = "files 不能为空")
+ @Schema(description = "本次提交的文件结果分片列表。每个分片只需要提交 fileUrl、chunkIndex、chunkTotal、totalLines、keptRows、invalidBrands、queryFailedBrands。invalidBrands 需包含品牌、国家、状态;主 sheet 删除时仅按品牌字段匹配。")
+ private List files;
+}
diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/dto/BrandInvalidBrandDto.java b/backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/dto/BrandInvalidBrandDto.java
new file mode 100644
index 0000000..7ba9a75
--- /dev/null
+++ b/backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/dto/BrandInvalidBrandDto.java
@@ -0,0 +1,18 @@
+package com.nanri.aiimage.modules.brand.model.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Data
+@Schema(description = "不符合品牌记录。Java 会将该列表输出到结果 xlsx 的“不符合品牌” sheet。")
+public class BrandInvalidBrandDto {
+
+ @Schema(description = "命中的品牌名称。")
+ private String brand;
+
+ @Schema(description = "命中的国家/地区。")
+ private String country;
+
+ @Schema(description = "命中的品牌状态,例如 已注册、待决。")
+ private String status;
+}
diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/dto/BrandParsedFileCacheDto.java b/backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/dto/BrandParsedFileCacheDto.java
new file mode 100644
index 0000000..424d81c
--- /dev/null
+++ b/backend-java/src/main/java/com/nanri/aiimage/modules/brand/model/dto/BrandParsedFileCacheDto.java
@@ -0,0 +1,16 @@
+package com.nanri.aiimage.modules.brand.model.dto;
+
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+@Data
+public class BrandParsedFileCacheDto {
+ private String fileUrl;
+ private String originalFilename;
+ private String relativePath;
+ private String sheetName;
+ private List columns;
+ private List