后台查询Asin文档更新

This commit is contained in:
super
2026-04-24 15:14:14 +08:00
parent 326f78705d
commit 0caf62c3d2
34 changed files with 798 additions and 540 deletions
@@ -1,6 +1,11 @@
package com.nanri.aiimage.modules.queryasin.controller;
import com.nanri.aiimage.common.api.ApiResponse;
import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskCandidateAddRequest;
import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskMatchShopsRequest;
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskCandidateVo;
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskDashboardVo;
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskMatchShopsVo;
import com.nanri.aiimage.modules.queryasin.model.dto.QueryAsinCreateTaskRequest;
import com.nanri.aiimage.modules.queryasin.model.dto.QueryAsinSubmitResultRequest;
import com.nanri.aiimage.modules.queryasin.model.dto.QueryAsinTaskBatchRequest;
@@ -9,11 +14,6 @@ import com.nanri.aiimage.modules.queryasin.model.vo.QueryAsinHistoryVo;
import com.nanri.aiimage.modules.queryasin.model.vo.QueryAsinTaskBatchVo;
import com.nanri.aiimage.modules.queryasin.service.QueryAsinResolveService;
import com.nanri.aiimage.modules.queryasin.service.QueryAsinTaskService;
import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskCandidateAddRequest;
import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskMatchShopsRequest;
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskCandidateVo;
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskDashboardVo;
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskMatchShopsVo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
@@ -46,14 +46,14 @@ import java.util.List;
@RequestMapping("/api/query-asin")
@Tag(
name = "查询ASIN",
description = "查询ASIN模块:管理备选店铺、批量匹配紫鸟店铺、创建任务、接收 Python 分片结果、自动收尾生成 Excel 并提供下载。涉及查询和删除接口需要携带 user_id。")
description = "查询 ASIN 模块:管理备选店铺、批量匹配紫鸟店铺、创建任务、接收 Python 分片结果、自动收尾生成 Excel 并提供下载。查询和删除接口需要携带 user_id。")
public class QueryAsinTaskController {
private final QueryAsinResolveService queryAsinResolveService;
private final QueryAsinTaskService queryAsinTaskService;
@GetMapping("/candidates")
@Operation(summary = "查询备选店铺列表", description = "返回当前用户在查询ASIN模块中已保存的备选店铺。")
@Operation(summary = "查询备选店铺列表", description = "返回当前用户在查询 ASIN 模块中已保存的备选店铺。")
public ApiResponse<List<ProductRiskCandidateVo>> listCandidates(
@Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1")
@RequestParam("user_id") Long userId) {
@@ -61,10 +61,10 @@ public class QueryAsinTaskController {
}
@PostMapping("/candidates")
@Operation(summary = "新增备选店铺", description = "向查询ASIN备选区新增一条店铺记录,供后续匹配和创建任务使用。")
@Operation(summary = "新增备选店铺", description = "向查询 ASIN 备选区新增一条店铺记录,供后续匹配和创建任务使用。")
public ApiResponse<ProductRiskCandidateVo> addCandidate(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "新增备选店铺请求,需传入用户 ID 和店铺名。",
description = "新增备选店铺请求,需传入用户 ID 和店铺名。",
required = true,
content = @Content(
mediaType = "application/json",
@@ -93,7 +93,7 @@ public class QueryAsinTaskController {
}
@PostMapping("/match-shops")
@Operation(summary = "批量匹配店铺", description = "根据店铺名批量匹配紫鸟店铺索引,返回是否命中、店铺 ID、平台、公司和匹配状态。")
@Operation(summary = "批量匹配店铺", description = "根据店铺名批量匹配紫鸟店铺索引,返回是否命中、店铺 ID、平台、公司和匹配状态。")
public ApiResponse<ProductRiskMatchShopsVo> matchShops(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "批量匹配请求,传入 user_id 和待匹配的店铺名称列表。",
@@ -125,7 +125,7 @@ public class QueryAsinTaskController {
}
@GetMapping("/history")
@Operation(summary = "查询任务记录", description = "返回当前用户在查询ASIN模块中的当前任务和历史任务记录。")
@Operation(summary = "查询任务记录", description = "返回当前用户在查询 ASIN 模块中的当前任务和历史任务记录。")
public ApiResponse<QueryAsinHistoryVo> history(
@Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1")
@RequestParam("user_id") Long userId) {
@@ -133,16 +133,16 @@ public class QueryAsinTaskController {
}
@PostMapping("/tasks/progress/batch")
@Operation(summary = "批量查询查询ASIN任务进度", description = "仅返回任务状态和店铺结果摘要,用于前端轮询降载。")
@Operation(summary = "批量查询查询 ASIN 任务进度", description = "仅返回任务状态和店铺结果摘要,用于前端轮询降载。")
public ApiResponse<QueryAsinTaskBatchVo> taskProgressBatch(@Valid @RequestBody QueryAsinTaskBatchRequest request) {
return ApiResponse.success(queryAsinTaskService.getTaskProgressBatch(request.getTaskIds()));
}
@PostMapping("/tasks")
@Operation(summary = "创建查询ASIN任务", description = "根据已匹配的店铺创建任务和占位结果记录,后续由 Python 端处理并回传结果")
@Operation(summary = "创建查询 ASIN 任务", description = "根据已匹配的店铺创建任务和占位结果记录,并把后台维护的整张 ASIN 表数据随店铺项返回给 Python 端。")
public ApiResponse<QueryAsinCreateTaskVo> createTask(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "创建任务请求。items 中每一项代表一个待处理店铺,需携带默认模板结构",
description = "创建任务请求。items 中每一项代表一个待处理店铺,queryAsins 是后台维护的整张 ASIN 表数据,不按店铺过滤",
required = true,
content = @Content(
mediaType = "application/json",
@@ -161,23 +161,14 @@ public class QueryAsinTaskController {
"companyName": "示例公司",
"matchStatus": "MATCHED",
"matchMessage": "索引已命中",
"countrySections": [
"queryAsins": [
{
"country": "德国",
"rows": [
{
"status": "全部",
"quantity": "",
"deleteQuantity": "",
"processStatus": ""
}
]
}
],
"cartRatios": [
"country": "DE",
"asins": ["B0BRZZR3N2", "B0BRZZR3N3"]
},
{
"country": "德国",
"ratio": ""
"country": "FR",
"asins": ["B0BRZZR3N4"]
}
]
}
@@ -190,124 +181,72 @@ public class QueryAsinTaskController {
@PostMapping("/tasks/{taskId}/result")
@Operation(
summary = "提交查询ASIN结果",
description = "供 Python 端回传处理结果。支持按店铺分片多次提交,服务端会合并分片、按店铺完成状态自动收尾,并在任务结束后生成 Excel")
summary = "提交查询 ASIN 结果",
description = "供 Python 端回传查询 ASIN 结果。业务数据包含店铺名,以及德国、英国、法国、意大利、西班牙 5 个国家下的 ASIN 和处理状态;submissionId 用于日志追踪,shopDone 可不传,不传时后端默认该店铺已提交完成")
public ApiResponse<Void> submitResult(
@Parameter(description = "任务主键,必须是运行中的任务", example = "3089")
@Parameter(description = "任务主键,必须是运行中的查询 ASIN 任务", example = "3089")
@PathVariable Long taskId,
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "查询ASIN结果回传请求。shops 表示本次提交的店铺结果分片列表。shopDone=true 表示该店铺已全部提交完成",
description = "查询 ASIN 结果回传请求。shops 表示店铺结果列表;countryResults 对应导出表中的国家 ASIN 列和处理状态列;error 用于回传单店铺失败原因",
required = true,
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = QueryAsinSubmitResultRequest.class),
examples = {
@ExampleObject(
name = "成功分片示例",
summary = "一个店铺分片回传,包含状态数据和购物车比例数据",
value = """
examples = @ExampleObject(
name = "回传示例",
summary = "按店铺和国家返回 ASIN 与处理状态",
value = """
{
"shops": [
{
"shopName": "郭亚芳",
"submissionId": "query-asin:3089:郭亚芳:1711111111111",
"shopDone": true,
"countryResults": [
{
"shops": [
"country": "DE",
"items": [
{
"shopName": "郭亚芳",
"submissionId": "query-asin:3089:郭亚芳:1711111111111",
"chunkIndex": 1,
"chunkTotal": 2,
"shopDone": false,
"countrySections": [
{
"country": "德国",
"rows": [
{
"status": "正常",
"quantity": "12",
"deleteQuantity": "3",
"processStatus": "处理中"
},
{
"status": "下架",
"quantity": "2",
"deleteQuantity": "2",
"processStatus": "已完成"
}
]
},
{
"country": "英国",
"rows": [
{
"status": "正常",
"quantity": "5",
"deleteQuantity": "1",
"processStatus": "处理中"
}
]
}
],
"cartRatios": [
{
"country": "德国",
"ratio": "25%"
},
{
"country": "英国",
"ratio": "18%"
}
]
"asin": "B0BRZZR3N2",
"status": "正常"
},
{
"asin": "B0BRZZR3N3",
"status": "未找到"
}
]
}
"""),
@ExampleObject(
name = "店铺完成示例",
summary = "最后一片提交完成后,将 shopDone 置为 true",
value = """
},
{
"shops": [
"country": "UK",
"items": [
{
"shopName": "郭亚芳",
"submissionId": "query-asin:3089:郭亚芳:1711111111111",
"chunkIndex": 2,
"chunkTotal": 2,
"shopDone": true,
"countrySections": [
{
"country": "法国",
"rows": [
{
"status": "正常",
"quantity": "4",
"deleteQuantity": "0",
"processStatus": "已完成"
}
]
}
],
"cartRatios": [
{
"country": "法国",
"ratio": "12%"
}
]
"asin": "B0BRZZR3N6",
"status": "正常"
}
]
}
"""),
@ExampleObject(
name = "失败示例",
summary = "店铺处理失败时直接回传错误信息",
value = """
},
{
"shops": [
"country": "FR",
"items": [
{
"shopName": "郭亚芳",
"shopDone": true,
"error": "紫鸟页面加载超时,未能完成删除数据采集"
"asin": "B0BRZZR3N4",
"status": "正常"
}
]
},
{
"country": "IT",
"items": []
},
{
"country": "ES",
"items": []
}
""")
}))
]
}
]
}
""")))
@Valid @RequestBody QueryAsinSubmitResultRequest request,
jakarta.servlet.http.HttpServletResponse response) {
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
@@ -317,7 +256,7 @@ public class QueryAsinTaskController {
}
@GetMapping("/results/{resultId}/download")
@Operation(summary = "下载结果文件", description = "按结果记录下载服务端已生成并上传到 OSS 的查询ASIN Excel 文件。")
@Operation(summary = "下载结果文件", description = "按结果记录下载服务端已生成并上传到 OSS 的查询 ASIN Excel 文件。")
public void downloadResult(
@Parameter(description = "结果记录主键", example = "4599")
@PathVariable Long resultId,
@@ -348,7 +287,7 @@ public class QueryAsinTaskController {
}
@DeleteMapping("/tasks/{taskId}")
@Operation(summary = "删除任务", description = "删除整条查询ASIN任务以及该任务下关联的所有结果记录。")
@Operation(summary = "删除任务", description = "删除整条查询 ASIN 任务以及该任务下关联的所有结果记录。")
public ApiResponse<Void> deleteTask(
@Parameter(description = "任务主键", example = "3089")
@PathVariable Long taskId,
@@ -359,7 +298,7 @@ public class QueryAsinTaskController {
}
@DeleteMapping("/history/{resultId}")
@Operation(summary = "删除单条历史记录", description = "删除一条查询ASIN结果记录,并同步重算其所属任务状态。")
@Operation(summary = "删除单条历史记录", description = "删除一条查询 ASIN 结果记录,并同步重算其所属任务状态。")
public ApiResponse<Void> deleteHistory(
@Parameter(description = "结果记录主键", example = "4599")
@PathVariable Long resultId,
@@ -369,6 +308,3 @@ public class QueryAsinTaskController {
return ApiResponse.success(null);
}
}
@@ -0,0 +1,15 @@
package com.nanri.aiimage.modules.queryasin.model.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "查询 ASIN 单条结果")
public class QueryAsinAsinStatusDto {
@Schema(description = "ASIN", example = "B0BRZZR3N2")
private String asin;
@Schema(description = "Python 查询后返回的状态", example = "正常")
private String status;
}
@@ -1,16 +0,0 @@
package com.nanri.aiimage.modules.queryasin.model.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "购物车比例数据")
public class QueryAsinCartRatioDto {
@Schema(description = "国家名称", example = "德国")
private String country;
@Schema(description = "购物车比例", example = "25%")
private String ratio;
}
@@ -7,12 +7,12 @@ import java.util.ArrayList;
import java.util.List;
@Data
@Schema(description = "查询 ASIN 国家维度 ASIN 列表")
@Schema(description = "查询 ASIN 国家维度 ASIN 清单")
public class QueryAsinCountryAsinsDto {
@Schema(description = "国家编码,支持 DE、UK、FR、IT、ES", example = "DE")
private String country;
@Schema(description = "该国家需要查询的 ASIN 列表")
@Schema(description = "后台维护的该国家 ASIN 清单,创建任务时会整体推给 Python")
private List<String> asins = new ArrayList<>();
}
@@ -1,25 +0,0 @@
package com.nanri.aiimage.modules.queryasin.model.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "国家状态数据中的单行记录")
public class QueryAsinCountryMetricRowDto {
@Schema(description = "商品状态", example = "正常")
private String status;
@Schema(description = "该状态下的商品数量", example = "12")
private String quantity;
@JsonProperty("deleteQuantity")
@Schema(description = "已删除数量", example = "3")
private String deleteQuantity;
@JsonProperty("processStatus")
@Schema(description = "处理结果或处理状态", example = "处理中")
private String processStatus;
}
@@ -0,0 +1,18 @@
package com.nanri.aiimage.modules.queryasin.model.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
@Schema(description = "查询 ASIN 国家维度结果")
public class QueryAsinCountryResultDto {
@Schema(description = "国家编码,支持 DE、UK、FR、IT、ES", example = "DE")
private String country;
@Schema(description = "该国家下的 ASIN 查询结果列表")
private List<QueryAsinAsinStatusDto> items = new ArrayList<>();
}
@@ -1,19 +0,0 @@
package com.nanri.aiimage.modules.queryasin.model.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
@Schema(description = "单个国家的状态数据分组")
public class QueryAsinCountrySectionDto {
@Schema(description = "国家名称", example = "德国")
private String country;
@Schema(description = "该国家下的多行状态数据")
private List<QueryAsinCountryMetricRowDto> rows = new ArrayList<>();
}
@@ -8,42 +8,26 @@ import java.util.ArrayList;
import java.util.List;
@Data
@Schema(description = "单个店铺的查询ASIN结果分片")
@Schema(description = "单个店铺的查询 ASIN 结果")
public class QueryAsinShopPayloadDto {
@JsonProperty("shopName")
@Schema(description = "店铺名称,服务端按店铺名称聚合分片", example = "郭亚芳")
@Schema(description = "店铺名称", example = "郭亚芳")
private String shopName;
@Schema(description = "店铺处理失败时的错误信息;有值时该店铺会直接标记失败", example = "紫鸟页面加载超时,未能完成删除数据采集")
@Schema(description = "店铺处理失败时的错误信息;有值时该店铺会直接标记失败", example = "紫鸟页面加载超时,未能完成 ASIN 查询")
private String error;
@JsonProperty("countrySections")
@Schema(description = "各国家的状态数据列表。同一国家可分多片提交,服务端会按顺序合并")
private List<QueryAsinCountrySectionDto> countrySections = new ArrayList<>();
@JsonProperty("cartRatios")
@Schema(description = "购物车比例数据列表,通常只在店铺首行展示")
private List<QueryAsinCartRatioDto> cartRatios = new ArrayList<>();
@JsonProperty("queryAsins")
@Schema(description = "Python 回传的查询 ASIN 结果分片;如果 Python 仅透传后台 ASIN,也按该字段提交")
private List<QueryAsinCountryAsinsDto> queryAsins = new ArrayList<>();
@JsonProperty("countryResults")
@Schema(description = "Python 回传的查询 ASIN 结果。每个国家下按 ASIN + 状态返回,对应导出表中的国家列和状态列")
private List<QueryAsinCountryResultDto> countryResults = new ArrayList<>();
@JsonProperty("shopDone")
@Schema(description = "该店铺是否已全部提交完成。最后一片传 true", example = "true")
@Schema(description = "该店铺是否已全部提交完成。不传时按已完成处理;分片提交时,中间分片传 false,最后一片传 true", example = "true")
private Boolean shopDone;
@JsonProperty("submissionId")
@Schema(description = "本次店铺提交批次标识,便于问题排查与日志", example = "query-asin:3089:郭亚芳:1711111111111")
@Schema(description = "本次店铺提交批次标识,便于问题排查与日志", example = "query-asin:3089:郭亚芳:1711111111111")
private String submissionId;
@JsonProperty("chunkIndex")
@Schema(description = "当前分片序号,建议从 1 开始", example = "1")
private Integer chunkIndex;
@JsonProperty("chunkTotal")
@Schema(description = "当前店铺总分片数", example = "2")
private Integer chunkTotal;
}
@@ -9,7 +9,7 @@ import java.util.ArrayList;
import java.util.List;
@Data
@Schema(description = "查询ASIN结果回传请求")
@Schema(description = "查询 ASIN 结果回传请求")
public class QueryAsinSubmitResultRequest {
@Valid
@@ -17,4 +17,3 @@ public class QueryAsinSubmitResultRequest {
@Schema(description = "本次提交的店铺结果分片列表,支持一次提交多个店铺", requiredMode = Schema.RequiredMode.REQUIRED)
private List<QueryAsinShopPayloadDto> shops = new ArrayList<>();
}
@@ -9,7 +9,7 @@ import java.util.ArrayList;
import java.util.List;
@Data
@Schema(description = "查询ASIN任务中的单个店铺项")
@Schema(description = "查询 ASIN 任务中的单个店铺项")
public class QueryAsinTaskItemDto {
@JsonProperty("shopName")
@@ -40,6 +40,6 @@ public class QueryAsinTaskItemDto {
@Valid
@JsonProperty("queryAsins")
@Schema(description = "后台查询 ASIN 维护表中的国家 ASIN 列表;查询ASIN任务按全表返回,不按店铺过滤")
@Schema(description = "后台查询 ASIN 维护表中的国家 ASIN 清单;查询 ASIN 任务按全表返回,不按店铺过滤")
private List<QueryAsinCountryAsinsDto> queryAsins = new ArrayList<>();
}
@@ -1,9 +1,8 @@
package com.nanri.aiimage.modules.queryasin.model.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.nanri.aiimage.modules.queryasin.model.dto.QueryAsinCartRatioDto;
import com.nanri.aiimage.modules.queryasin.model.dto.QueryAsinCountryAsinsDto;
import com.nanri.aiimage.modules.queryasin.model.dto.QueryAsinCountrySectionDto;
import com.nanri.aiimage.modules.queryasin.model.dto.QueryAsinCountryResultDto;
import lombok.Data;
import java.time.LocalDateTime;
@@ -45,13 +44,9 @@ public class QueryAsinResultItemVo {
private String outputFilename;
private String downloadUrl;
@JsonProperty("countrySections")
private List<QueryAsinCountrySectionDto> countrySections = new ArrayList<>();
@JsonProperty("cartRatios")
private List<QueryAsinCartRatioDto> cartRatios = new ArrayList<>();
@JsonProperty("queryAsins")
private List<QueryAsinCountryAsinsDto> queryAsins = new ArrayList<>();
}
@JsonProperty("countryResults")
private List<QueryAsinCountryResultDto> countryResults = new ArrayList<>();
}
@@ -1,7 +1,9 @@
package com.nanri.aiimage.modules.queryasin.service;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.modules.queryasin.model.dto.QueryAsinAsinStatusDto;
import com.nanri.aiimage.modules.queryasin.model.dto.QueryAsinCountryAsinsDto;
import com.nanri.aiimage.modules.queryasin.model.dto.QueryAsinCountryResultDto;
import com.nanri.aiimage.modules.queryasin.model.vo.QueryAsinResultItemVo;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Row;
@@ -11,6 +13,7 @@ import org.springframework.stereotype.Service;
import java.io.File;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -20,7 +23,21 @@ import java.util.Map;
public class QueryAsinExcelAssemblyService {
private static final List<String> COUNTRIES = List.of("DE", "UK", "FR", "IT", "ES");
private static final String[] HEADER = {"店铺名", "德国", "英国", "法国", "意大利", "西班牙"};
private static final Map<String, String> COUNTRY_NAMES = Map.of(
"DE", "德国",
"UK", "英国",
"FR", "法国",
"IT", "意大利",
"ES", "西班牙"
);
private static final String[] HEADER = {
"店铺名",
"德国", "状态",
"英国", "状态",
"法国", "状态",
"意大利", "状态",
"西班牙", "状态"
};
public void writeWorkbook(File outputXlsx, List<QueryAsinResultItemVo> items) {
SXSSFWorkbook workbook = new SXSSFWorkbook(200);
@@ -38,14 +55,17 @@ public class QueryAsinExcelAssemblyService {
if (item == null || Boolean.FALSE.equals(item.getSuccess())) {
continue;
}
Map<String, List<String>> asinsByCountry = asinsByCountry(item.getQueryAsins());
int maxRows = maxRows(asinsByCountry);
Map<String, List<QueryAsinAsinStatusDto>> resultsByCountry = resultsByCountry(item);
int maxRows = maxRows(resultsByCountry);
for (int i = 0; i < maxRows; i++) {
Row row = sheet.createRow(rowIndex++);
row.createCell(0).setCellValue(i == 0 ? safe(item.getShopName()) : "");
for (int c = 0; c < COUNTRIES.size(); c++) {
List<String> asins = asinsByCountry.get(COUNTRIES.get(c));
row.createCell(c + 1).setCellValue(asins != null && i < asins.size() ? safe(asins.get(i)) : "");
List<QueryAsinAsinStatusDto> rows = resultsByCountry.get(COUNTRIES.get(c));
QueryAsinAsinStatusDto result = rows != null && i < rows.size() ? rows.get(i) : null;
int asinColumn = 1 + c * 2;
row.createCell(asinColumn).setCellValue(result == null ? "" : safe(result.getAsin()));
row.createCell(asinColumn + 1).setCellValue(result == null ? "" : safe(result.getStatus()));
}
}
rowIndex++;
@@ -53,7 +73,7 @@ public class QueryAsinExcelAssemblyService {
workbook.write(outputStream);
} catch (Exception ex) {
log.warn("[query-asin] write workbook failed: {}", ex.getMessage());
throw new BusinessException("generate query-asin excel failed: " + ex.getMessage());
throw new BusinessException("生成查询 ASIN Excel 失败: " + ex.getMessage());
} finally {
try {
workbook.close();
@@ -70,42 +90,106 @@ public class QueryAsinExcelAssemblyService {
}
for (QueryAsinResultItemVo item : items) {
if (item != null && !Boolean.FALSE.equals(item.getSuccess())) {
count += maxRows(asinsByCountry(item.getQueryAsins()));
count += maxRows(resultsByCountry(item));
}
}
return count;
}
private Map<String, List<String>> asinsByCountry(List<QueryAsinCountryAsinsDto> queryAsins) {
Map<String, List<String>> map = new LinkedHashMap<>();
for (String country : COUNTRIES) {
map.put(country, List.of());
}
if (queryAsins == null) {
private Map<String, List<QueryAsinAsinStatusDto>> resultsByCountry(QueryAsinResultItemVo item) {
Map<String, List<QueryAsinAsinStatusDto>> map = emptyCountryMap();
if (item == null) {
return map;
}
for (QueryAsinCountryAsinsDto item : queryAsins) {
if (item == null || item.getCountry() == null) {
continue;
if (item.getCountryResults() != null && !item.getCountryResults().isEmpty()) {
for (QueryAsinCountryResultDto countryResult : item.getCountryResults()) {
String country = normalizeCountry(countryResult == null ? null : countryResult.getCountry());
if (!map.containsKey(country)) {
continue;
}
map.put(country, normalizeResultItems(countryResult.getItems()));
}
String country = item.getCountry().trim().toUpperCase();
if (map.containsKey(country)) {
map.put(country, item.getAsins() == null ? List.of() : item.getAsins());
return map;
}
// 兼容旧链路:如果 Python 仍然只透传 queryAsins,则状态列留空。
if (item.getQueryAsins() != null) {
for (QueryAsinCountryAsinsDto countryAsins : item.getQueryAsins()) {
String country = normalizeCountry(countryAsins == null ? null : countryAsins.getCountry());
if (!map.containsKey(country)) {
continue;
}
List<QueryAsinAsinStatusDto> rows = new ArrayList<>();
for (String asin : countryAsins.getAsins() == null ? List.<String>of() : countryAsins.getAsins()) {
String normalizedAsin = normalizeAsin(asin);
if (normalizedAsin.isEmpty()) {
continue;
}
QueryAsinAsinStatusDto row = new QueryAsinAsinStatusDto();
row.setAsin(normalizedAsin);
row.setStatus("");
rows.add(row);
}
map.put(country, rows);
}
}
return map;
}
private int maxRows(Map<String, List<String>> asinsByCountry) {
private Map<String, List<QueryAsinAsinStatusDto>> emptyCountryMap() {
Map<String, List<QueryAsinAsinStatusDto>> map = new LinkedHashMap<>();
for (String country : COUNTRIES) {
map.put(country, List.of());
}
return map;
}
private List<QueryAsinAsinStatusDto> normalizeResultItems(List<QueryAsinAsinStatusDto> items) {
List<QueryAsinAsinStatusDto> out = new ArrayList<>();
if (items == null) {
return out;
}
for (QueryAsinAsinStatusDto source : items) {
String asin = normalizeAsin(source == null ? null : source.getAsin());
String status = source == null ? "" : safe(source.getStatus()).trim();
if (asin.isEmpty() && status.isEmpty()) {
continue;
}
QueryAsinAsinStatusDto item = new QueryAsinAsinStatusDto();
item.setAsin(asin);
item.setStatus(status);
out.add(item);
}
return out;
}
private int maxRows(Map<String, List<QueryAsinAsinStatusDto>> resultsByCountry) {
int max = 1;
for (List<String> asins : asinsByCountry.values()) {
if (asins != null && asins.size() > max) {
max = asins.size();
for (List<QueryAsinAsinStatusDto> rows : resultsByCountry.values()) {
if (rows != null && rows.size() > max) {
max = rows.size();
}
}
return max;
}
private String normalizeCountry(String country) {
if (country == null) {
return "";
}
String value = country.trim().toUpperCase();
for (Map.Entry<String, String> entry : COUNTRY_NAMES.entrySet()) {
if (entry.getValue().equals(country.trim())) {
return entry.getKey();
}
}
return value;
}
private String normalizeAsin(String value) {
return value == null ? "" : value.trim().toUpperCase();
}
private String safe(String value) {
return value == null ? "" : value;
}
@@ -8,10 +8,9 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.config.TaskPressureProperties;
import com.nanri.aiimage.modules.file.service.oss.OssStorageService;
import com.nanri.aiimage.modules.queryasin.model.dto.QueryAsinCartRatioDto;
import com.nanri.aiimage.modules.queryasin.model.dto.QueryAsinAsinStatusDto;
import com.nanri.aiimage.modules.queryasin.model.dto.QueryAsinCountryAsinsDto;
import com.nanri.aiimage.modules.queryasin.model.dto.QueryAsinCountryMetricRowDto;
import com.nanri.aiimage.modules.queryasin.model.dto.QueryAsinCountrySectionDto;
import com.nanri.aiimage.modules.queryasin.model.dto.QueryAsinCountryResultDto;
import com.nanri.aiimage.modules.queryasin.model.dto.QueryAsinCreateTaskRequest;
import com.nanri.aiimage.modules.queryasin.model.dto.QueryAsinShopPayloadDto;
import com.nanri.aiimage.modules.queryasin.model.dto.QueryAsinSubmitResultRequest;
@@ -523,11 +522,8 @@ public class QueryAsinTaskService {
item.setDownloadUrl(blank(entity.getResultFileUrl())
? item.getDownloadUrl()
: ossStorageService.generateFreshDownloadUrl(entity.getResultFileUrl()));
if (item.getCountrySections() == null) {
item.setCountrySections(new ArrayList<>());
}
if (item.getCartRatios() == null) {
item.setCartRatios(new ArrayList<>());
if (item.getCountryResults() == null) {
item.setCountryResults(new ArrayList<>());
}
if (item.getQueryAsins() == null) {
item.setQueryAsins(new ArrayList<>());
@@ -568,8 +564,7 @@ public class QueryAsinTaskService {
vo.setError(result.getErrorMessage());
vo.setCreatedAt(result.getCreatedAt());
vo.setFinishedAt(finishedAt);
vo.setCountrySections(new ArrayList<>());
vo.setCartRatios(new ArrayList<>());
vo.setCountryResults(new ArrayList<>());
vo.setQueryAsins(copyCountryAsins(item.getQueryAsins()));
vo.setOutputFilename(result.getResultFilename());
vo.setDownloadUrl(null);
@@ -642,15 +637,11 @@ public class QueryAsinTaskService {
if (merged == null) {
merged = new QueryAsinShopPayloadDto();
merged.setShopName(shopKey);
merged.setCountrySections(new ArrayList<>());
merged.setCartRatios(new ArrayList<>());
merged.setQueryAsins(new ArrayList<>());
merged.setCountryResults(new ArrayList<>());
}
merged.setShopName(firstNonBlank(incoming.getShopName(), merged.getShopName()));
merged.setSubmissionId(firstNonBlank(incoming.getSubmissionId(), merged.getSubmissionId()));
merged.setChunkIndex(incoming.getChunkIndex());
merged.setChunkTotal(incoming.getChunkTotal());
if (!blank(incoming.getError())) {
merged.setError(incoming.getError().trim());
@@ -659,10 +650,8 @@ public class QueryAsinTaskService {
return merged;
}
merged.setCountrySections(mergeCountrySections(merged.getCountrySections(), incoming.getCountrySections()));
merged.setCartRatios(mergeCartRatios(merged.getCartRatios(), incoming.getCartRatios()));
merged.setQueryAsins(mergeCountryAsins(merged.getQueryAsins(), incoming.getQueryAsins()));
if (Boolean.TRUE.equals(incoming.getShopDone())) {
merged.setCountryResults(mergeCountryResults(merged.getCountryResults(), incoming.getCountryResults()));
if (incoming.getShopDone() == null || Boolean.TRUE.equals(incoming.getShopDone())) {
merged.setShopDone(Boolean.TRUE);
}
taskCacheService.saveShopMergedPayload(taskId, shopKey, merged);
@@ -674,11 +663,7 @@ public class QueryAsinTaskService {
return;
}
snapshot.setShopName(firstNonBlank(payload.getShopName(), snapshot.getShopName()));
snapshot.setCountrySections(copyCountrySections(payload.getCountrySections()));
snapshot.setCartRatios(copyCartRatios(payload.getCartRatios()));
if (payload.getQueryAsins() != null && !payload.getQueryAsins().isEmpty()) {
snapshot.setQueryAsins(copyCountryAsins(payload.getQueryAsins()));
}
snapshot.setCountryResults(copyCountryResults(payload.getCountryResults()));
if (!blank(payload.getError())) {
snapshot.setError(payload.getError().trim());
}
@@ -700,37 +685,38 @@ public class QueryAsinTaskService {
snapshot.setError(blankToNull(error));
}
private List<QueryAsinCountrySectionDto> mergeCountrySections(List<QueryAsinCountrySectionDto> base, List<QueryAsinCountrySectionDto> incoming) {
Map<String, QueryAsinCountrySectionDto> map = new LinkedHashMap<>();
for (QueryAsinCountrySectionDto section : copyCountrySections(base)) {
map.put(section.getCountry(), section);
private List<QueryAsinCountryResultDto> mergeCountryResults(List<QueryAsinCountryResultDto> base, List<QueryAsinCountryResultDto> incoming) {
Map<String, QueryAsinCountryResultDto> map = new LinkedHashMap<>();
for (QueryAsinCountryResultDto item : copyCountryResults(base)) {
map.put(item.getCountry(), item);
}
for (QueryAsinCountrySectionDto section : copyCountrySections(incoming)) {
QueryAsinCountrySectionDto existing = map.get(section.getCountry());
if (existing == null) {
map.put(section.getCountry(), section);
for (QueryAsinCountryResultDto item : copyCountryResults(incoming)) {
if (blank(item.getCountry())) {
continue;
}
List<QueryAsinCountryMetricRowDto> mergedRows = existing.getRows() == null
? new ArrayList<>()
: new ArrayList<>(existing.getRows());
if (section.getRows() != null) {
mergedRows.addAll(section.getRows());
QueryAsinCountryResultDto existing = map.get(item.getCountry());
if (existing == null) {
map.put(item.getCountry(), item);
continue;
}
existing.setRows(mergedRows);
}
return new ArrayList<>(map.values());
}
private List<QueryAsinCartRatioDto> mergeCartRatios(List<QueryAsinCartRatioDto> base, List<QueryAsinCartRatioDto> incoming) {
Map<String, QueryAsinCartRatioDto> map = new LinkedHashMap<>();
for (QueryAsinCartRatioDto ratio : copyCartRatios(base)) {
map.put(ratio.getCountry(), ratio);
}
for (QueryAsinCartRatioDto ratio : copyCartRatios(incoming)) {
if (!blank(ratio.getCountry())) {
map.put(ratio.getCountry(), ratio);
List<QueryAsinAsinStatusDto> merged = new ArrayList<>(existing.getItems() == null ? List.of() : existing.getItems());
for (QueryAsinAsinStatusDto row : item.getItems() == null ? List.<QueryAsinAsinStatusDto>of() : item.getItems()) {
String asin = normalizeAsin(row == null ? null : row.getAsin());
String status = row == null ? "" : firstNonBlank(row.getStatus(), "").trim();
if (asin.isEmpty() && status.isEmpty()) {
continue;
}
boolean exists = merged.stream().anyMatch(old ->
Objects.equals(normalizeAsin(old.getAsin()), asin)
&& Objects.equals(firstNonBlank(old.getStatus(), "").trim(), status));
if (!exists) {
QueryAsinAsinStatusDto normalized = new QueryAsinAsinStatusDto();
normalized.setAsin(asin);
normalized.setStatus(status);
merged.add(normalized);
}
}
existing.setItems(merged);
}
return new ArrayList<>(map.values());
}
@@ -741,17 +727,19 @@ public class QueryAsinTaskService {
map.put(item.getCountry(), item);
}
for (QueryAsinCountryAsinsDto item : copyCountryAsins(incoming)) {
if (blank(item.getCountry())) {
String country = normalizeCountry(item.getCountry());
if (blank(country)) {
continue;
}
QueryAsinCountryAsinsDto existing = map.get(item.getCountry());
item.setCountry(country);
QueryAsinCountryAsinsDto existing = map.get(country);
if (existing == null) {
map.put(item.getCountry(), item);
map.put(country, item);
continue;
}
List<String> merged = new ArrayList<>(existing.getAsins() == null ? List.of() : existing.getAsins());
for (String asin : item.getAsins() == null ? List.<String>of() : item.getAsins()) {
String normalized = asin == null ? "" : asin.trim().toUpperCase();
String normalized = normalizeAsin(asin);
if (!normalized.isEmpty() && !merged.contains(normalized)) {
merged.add(normalized);
}
@@ -765,35 +753,18 @@ public class QueryAsinTaskService {
if (payload == null) {
return false;
}
if (payload.getQueryAsins() != null) {
for (QueryAsinCountryAsinsDto item : payload.getQueryAsins()) {
if (item != null && item.getAsins() != null && !item.getAsins().isEmpty()) {
return true;
}
}
}
if (payload.getCountrySections() != null) {
for (QueryAsinCountrySectionDto section : payload.getCountrySections()) {
if (section == null || section.getRows() == null) {
if (payload.getCountryResults() != null) {
for (QueryAsinCountryResultDto countryResult : payload.getCountryResults()) {
if (countryResult == null || countryResult.getItems() == null) {
continue;
}
for (QueryAsinCountryMetricRowDto row : section.getRows()) {
if (row != null && (!blank(row.getStatus())
|| !blank(row.getQuantity())
|| !blank(row.getDeleteQuantity())
|| !blank(row.getProcessStatus()))) {
for (QueryAsinAsinStatusDto item : countryResult.getItems()) {
if (item != null && (!blank(item.getAsin()) || !blank(item.getStatus()))) {
return true;
}
}
}
}
if (payload.getCartRatios() != null) {
for (QueryAsinCartRatioDto ratio : payload.getCartRatios()) {
if (ratio != null && (!blank(ratio.getCountry()) || !blank(ratio.getRatio()))) {
return true;
}
}
}
return false;
}
@@ -872,35 +843,32 @@ public class QueryAsinTaskService {
}
}
private List<QueryAsinCountrySectionDto> copyCountrySections(List<QueryAsinCountrySectionDto> sections) {
List<QueryAsinCountrySectionDto> copy = new ArrayList<>();
if (sections == null) {
private List<QueryAsinCountryResultDto> copyCountryResults(List<QueryAsinCountryResultDto> results) {
List<QueryAsinCountryResultDto> copy = new ArrayList<>();
if (results == null) {
return copy;
}
for (QueryAsinCountrySectionDto section : sections) {
if (section == null) {
for (QueryAsinCountryResultDto source : results) {
if (source == null || blank(source.getCountry())) {
continue;
}
QueryAsinCountrySectionDto item = new QueryAsinCountrySectionDto();
item.setCountry(section.getCountry());
item.setRows(section.getRows() == null ? new ArrayList<>() : new ArrayList<>(section.getRows()));
copy.add(item);
}
return copy;
}
private List<QueryAsinCartRatioDto> copyCartRatios(List<QueryAsinCartRatioDto> ratios) {
List<QueryAsinCartRatioDto> copy = new ArrayList<>();
if (ratios == null) {
return copy;
}
for (QueryAsinCartRatioDto ratio : ratios) {
if (ratio == null) {
continue;
QueryAsinCountryResultDto item = new QueryAsinCountryResultDto();
item.setCountry(normalizeCountry(source.getCountry()));
List<QueryAsinAsinStatusDto> rows = new ArrayList<>();
if (source.getItems() != null) {
for (QueryAsinAsinStatusDto sourceRow : source.getItems()) {
String asin = normalizeAsin(sourceRow == null ? null : sourceRow.getAsin());
String status = sourceRow == null ? "" : firstNonBlank(sourceRow.getStatus(), "").trim();
if (asin.isEmpty() && status.isEmpty()) {
continue;
}
QueryAsinAsinStatusDto row = new QueryAsinAsinStatusDto();
row.setAsin(asin);
row.setStatus(status);
rows.add(row);
}
}
QueryAsinCartRatioDto item = new QueryAsinCartRatioDto();
item.setCountry(ratio.getCountry());
item.setRatio(ratio.getRatio());
item.setItems(rows);
copy.add(item);
}
return copy;
@@ -916,11 +884,11 @@ public class QueryAsinTaskService {
continue;
}
QueryAsinCountryAsinsDto item = new QueryAsinCountryAsinsDto();
item.setCountry(source.getCountry().trim().toUpperCase());
item.setCountry(normalizeCountry(source.getCountry()));
List<String> asins = new ArrayList<>();
if (source.getAsins() != null) {
for (String asin : source.getAsins()) {
String normalized = asin == null ? "" : asin.trim().toUpperCase();
String normalized = normalizeAsin(asin);
if (!normalized.isEmpty() && !asins.contains(normalized)) {
asins.add(normalized);
}
@@ -936,6 +904,25 @@ public class QueryAsinTaskService {
return !blank(first) ? first : second;
}
private String normalizeCountry(String country) {
if (country == null) {
return "";
}
String value = country.trim().toUpperCase();
return switch (value) {
case "德国" -> "DE";
case "英国" -> "UK";
case "法国" -> "FR";
case "意大利" -> "IT";
case "西班牙" -> "ES";
default -> value;
};
}
private String normalizeAsin(String asin) {
return asin == null ? "" : asin.trim().toUpperCase();
}
private String safeFileStem(String value) {
String raw = value == null ? "result" : value.trim();
String safe = raw.replaceAll("[\\\\/:*?\"<>|]+", "_");