更新地址 更新后端
This commit is contained in:
@@ -24,6 +24,11 @@ public class SimilarAsinProperties {
|
||||
* 不影响 AppearancePatentProperties 的同名值。
|
||||
*/
|
||||
private int cozeBatchSize = 3;
|
||||
/**
|
||||
* img_switch=false 时单次提交 Coze 的 row 数。
|
||||
* 不走图片检测时工作流压力小,恢复到 10 行一批以提高吞吐;开启图片检测时仍使用 cozeBatchSize。
|
||||
*/
|
||||
private int cozeTextOnlyBatchSize = 10;
|
||||
private int cozeConnectTimeoutMillis = 10000;
|
||||
private int cozeReadTimeoutMillis = 60000;
|
||||
private int cozePollIntervalMillis = 30000;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.nanri.aiimage.modules.productcategory.controller;
|
||||
|
||||
import com.nanri.aiimage.common.api.ApiResponse;
|
||||
import com.nanri.aiimage.common.util.DownloadHeaderUtil;
|
||||
import com.nanri.aiimage.modules.productcategory.model.dto.ProductCategorySaveRequest;
|
||||
import com.nanri.aiimage.modules.productcategory.model.vo.ProductCategoryItemVo;
|
||||
import com.nanri.aiimage.modules.productcategory.model.vo.ProductCategoryListVo;
|
||||
@@ -9,6 +10,9 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
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;
|
||||
@@ -19,12 +23,17 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/api/admin")
|
||||
@Tag(name = "商品类目管理", description = "维护商品类目树")
|
||||
public class ProductCategoryController {
|
||||
|
||||
private static final DateTimeFormatter EXPORT_FILENAME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");
|
||||
|
||||
private final ProductCategoryService productCategoryService;
|
||||
|
||||
@GetMapping("/product-categories")
|
||||
@@ -51,6 +60,18 @@ public class ProductCategoryController {
|
||||
return ApiResponse.success(productCategoryService.search(keyword, page, pageSize));
|
||||
}
|
||||
|
||||
@GetMapping("/product-categories/export")
|
||||
@Operation(summary = "导出商品类目")
|
||||
public ResponseEntity<byte[]> export(@RequestParam(value = "keyword", required = false) String keyword) {
|
||||
byte[] bytes = productCategoryService.export(keyword);
|
||||
String filename = "product-categories-" + LocalDateTime.now().format(EXPORT_FILENAME_FORMATTER) + ".xlsx";
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, DownloadHeaderUtil.contentDisposition(filename))
|
||||
.contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))
|
||||
.contentLength(bytes.length)
|
||||
.body(bytes);
|
||||
}
|
||||
|
||||
@PostMapping("/product-category")
|
||||
@Operation(summary = "新增商品类目")
|
||||
public ApiResponse<ProductCategoryItemVo> create(@Valid @RequestBody ProductCategorySaveRequest request) {
|
||||
|
||||
@@ -9,9 +9,15 @@ import com.nanri.aiimage.modules.productcategory.model.entity.ProductCategoryEnt
|
||||
import com.nanri.aiimage.modules.productcategory.model.vo.ProductCategoryItemVo;
|
||||
import com.nanri.aiimage.modules.productcategory.model.vo.ProductCategoryListVo;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
@@ -25,6 +31,8 @@ import java.util.Set;
|
||||
@RequiredArgsConstructor
|
||||
public class ProductCategoryService {
|
||||
|
||||
private static final DateTimeFormatter EXPORT_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
private final ProductCategoryMapper productCategoryMapper;
|
||||
|
||||
public ProductCategoryListVo list() {
|
||||
@@ -169,6 +177,26 @@ public class ProductCategoryService {
|
||||
return vo;
|
||||
}
|
||||
|
||||
public byte[] export(String keyword) {
|
||||
ProductCategoryListVo list = list(keyword);
|
||||
List<ProductCategoryItemVo> rows = list.getItems() == null ? List.of() : list.getItems();
|
||||
Map<Long, String> nameById = buildExportNameById(rows);
|
||||
try (SXSSFWorkbook workbook = new SXSSFWorkbook(100);
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||
Sheet sheet = workbook.createSheet("商品类目");
|
||||
writeExportHeader(sheet);
|
||||
for (int index = 0; index < rows.size(); index++) {
|
||||
writeExportRow(sheet, index + 1, rows.get(index), nameById);
|
||||
}
|
||||
applyExportColumnWidths(sheet);
|
||||
workbook.write(outputStream);
|
||||
workbook.dispose();
|
||||
return outputStream.toByteArray();
|
||||
} catch (Exception ex) {
|
||||
throw new BusinessException("导出商品类目失败");
|
||||
}
|
||||
}
|
||||
|
||||
private ProductCategoryListVo buildSearchPageVo(List<ProductCategoryEntity> rows, Long total, long page, long pageSize) {
|
||||
Map<Long, Integer> childCountById = buildChildCountById(rows);
|
||||
List<ProductCategoryItemVo> items = rows.stream()
|
||||
@@ -200,6 +228,82 @@ public class ProductCategoryService {
|
||||
return vo;
|
||||
}
|
||||
|
||||
private void writeExportHeader(Sheet sheet) {
|
||||
Row header = sheet.createRow(0);
|
||||
String[] headers = {
|
||||
"序号", "关系类型", "上级类目", "类目名称", "层级路径", "类目编码",
|
||||
"层级", "排序", "来源", "备注", "子类目数", "类目ID", "父级ID", "创建时间", "更新时间"
|
||||
};
|
||||
for (int i = 0; i < headers.length; i++) {
|
||||
header.createCell(i).setCellValue(headers[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeExportRow(Sheet sheet, int rowNo, ProductCategoryItemVo item, Map<Long, String> nameById) {
|
||||
Row row = sheet.createRow(rowNo);
|
||||
int col = 0;
|
||||
row.createCell(col++).setCellValue(rowNo);
|
||||
row.createCell(col++).setCellValue(resolveRelationType(item));
|
||||
row.createCell(col++).setCellValue(resolveParentName(item, nameById));
|
||||
row.createCell(col++).setCellValue(indentName(item));
|
||||
row.createCell(col++).setCellValue(blankToEmpty(item.getPath()));
|
||||
row.createCell(col++).setCellValue(blankToEmpty(item.getCategoryKey()));
|
||||
row.createCell(col++).setCellValue(item.getLevel() == null ? 0 : item.getLevel());
|
||||
row.createCell(col++).setCellValue(item.getSortOrder() == null ? 0 : item.getSortOrder());
|
||||
row.createCell(col++).setCellValue(Boolean.TRUE.equals(item.getIsBuiltin()) ? "内置" : "自定义");
|
||||
row.createCell(col++).setCellValue(blankToEmpty(item.getDescription()));
|
||||
row.createCell(col++).setCellValue(item.getChildCount() == null ? 0 : item.getChildCount());
|
||||
row.createCell(col++).setCellValue(item.getId() == null ? "" : String.valueOf(item.getId()));
|
||||
row.createCell(col++).setCellValue(item.getParentId() == null ? "" : String.valueOf(item.getParentId()));
|
||||
row.createCell(col++).setCellValue(formatExportTime(item.getCreatedAt()));
|
||||
row.createCell(col).setCellValue(formatExportTime(item.getUpdatedAt()));
|
||||
}
|
||||
|
||||
private void applyExportColumnWidths(Sheet sheet) {
|
||||
int[] widths = {2400, 4200, 7200, 8600, 12000, 7200, 2400, 2400, 2800, 10000, 3200, 3600, 3600, 5200, 5200};
|
||||
for (int i = 0; i < widths.length; i++) {
|
||||
sheet.setColumnWidth(i, widths[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<Long, String> buildExportNameById(List<ProductCategoryItemVo> rows) {
|
||||
Map<Long, String> nameById = new HashMap<>();
|
||||
for (ProductCategoryItemVo row : rows) {
|
||||
if (row.getId() != null) {
|
||||
nameById.put(row.getId(), row.getName());
|
||||
}
|
||||
}
|
||||
return nameById;
|
||||
}
|
||||
|
||||
private String resolveRelationType(ProductCategoryItemVo item) {
|
||||
int level = item.getLevel() == null ? 0 : item.getLevel();
|
||||
if (level <= 0) {
|
||||
return "顶级类目";
|
||||
}
|
||||
return level + "级子类目";
|
||||
}
|
||||
|
||||
private String resolveParentName(ProductCategoryItemVo item, Map<Long, String> nameById) {
|
||||
if (item.getParentId() == null) {
|
||||
return "无";
|
||||
}
|
||||
String parentName = nameById.get(item.getParentId());
|
||||
if (parentName == null || parentName.isBlank()) {
|
||||
return "上级ID: " + item.getParentId();
|
||||
}
|
||||
return parentName + "(ID: " + item.getParentId() + ")";
|
||||
}
|
||||
|
||||
private String indentName(ProductCategoryItemVo item) {
|
||||
int level = Math.max(0, item.getLevel() == null ? 0 : item.getLevel());
|
||||
return " ".repeat(level) + blankToEmpty(item.getName());
|
||||
}
|
||||
|
||||
private String formatExportTime(LocalDateTime time) {
|
||||
return time == null ? "" : time.format(EXPORT_TIME_FORMATTER);
|
||||
}
|
||||
|
||||
private Map<Long, Integer> buildChildCountById(List<ProductCategoryEntity> rows) {
|
||||
Map<Long, Integer> childCountById = new HashMap<>();
|
||||
if (rows.isEmpty()) {
|
||||
@@ -450,6 +554,10 @@ public class ProductCategoryService {
|
||||
return normalized.length() > 512 ? normalized.substring(0, 512) : normalized;
|
||||
}
|
||||
|
||||
private String blankToEmpty(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
private String generateCategoryKey(Long parentId, String name) {
|
||||
String source = (parentId == null ? "root" : String.valueOf(parentId)) + ":" + name;
|
||||
return "custom_" + DigestUtil.sha1Hex(source).substring(0, 20);
|
||||
|
||||
@@ -1231,7 +1231,7 @@ public class SimilarAsinTaskService {
|
||||
String prompt = readAiPrompt(task);
|
||||
String apiKey = readApiKey(task);
|
||||
boolean imgSwitch = readImgSwitch(task);
|
||||
int batchSize = Math.max(1, properties.getCozeBatchSize());
|
||||
int batchSize = resolveCozeBatchSize(imgSwitch);
|
||||
List<SimilarAsinResultRowDto> result = new ArrayList<>();
|
||||
for (int i = 0; i < items.size(); i += batchSize) {
|
||||
result.addAll(cozeClient.inspect(items.subList(i, Math.min(i + batchSize, items.size())), prompt, apiKey, imgSwitch));
|
||||
@@ -1553,6 +1553,11 @@ public class SimilarAsinTaskService {
|
||||
}
|
||||
}
|
||||
|
||||
private int resolveCozeBatchSize(boolean imgSwitch) {
|
||||
int configured = imgSwitch ? properties.getCozeBatchSize() : properties.getCozeTextOnlyBatchSize();
|
||||
return Math.max(1, configured);
|
||||
}
|
||||
|
||||
private void finalizeTask(FileTaskEntity task, String error, int rowCount, boolean assembleWorkbook) {
|
||||
String finalError = error;
|
||||
FileResultEntity result = null;
|
||||
@@ -1687,7 +1692,7 @@ public class SimilarAsinTaskService {
|
||||
.eq(TaskChunkEntity::getModuleType, MODULE_TYPE)
|
||||
.orderByAsc(TaskChunkEntity::getChunkIndex));
|
||||
Map<String, List<SimilarAsinParsedRowVo>> allRowsByBaseId = loadAllRowsByBaseId(task);
|
||||
int batchSize = Math.max(1, properties.getCozeBatchSize());
|
||||
int batchSize = resolveCozeBatchSize(readImgSwitch(task));
|
||||
int cozeWorkUnits = countCozeWorkUnits(chunks, batchSize);
|
||||
int totalProgressUnits = Math.max(3, cozeWorkUnits + 3);
|
||||
if (countPendingCozeStates(task.getId()) > 0) {
|
||||
@@ -1921,7 +1926,7 @@ public class SimilarAsinTaskService {
|
||||
String prompt = readAiPrompt(task);
|
||||
String apiKey = readApiKey(task);
|
||||
boolean imgSwitch = readImgSwitch(task);
|
||||
int batchSize = Math.max(1, properties.getCozeBatchSize());
|
||||
int batchSize = resolveCozeBatchSize(imgSwitch);
|
||||
// P1-1:检测到 720712008 风暴时强制把 batch 降到 1,隔离毒行;
|
||||
// 持续 5+ 次提交命中率 ≥ 40% 才会触发,正常波动不影响吞吐。
|
||||
if (isPoisonStormActive(task.getId())) {
|
||||
|
||||
@@ -167,6 +167,7 @@ aiimage:
|
||||
coze-workflow-id: ${AIIMAGE_SIMILAR_ASIN_COZE_WORKFLOW_ID:7639708860686024756}
|
||||
coze-token: ${AIIMAGE_SIMILAR_ASIN_COZE_TOKEN:}
|
||||
coze-batch-size: ${AIIMAGE_SIMILAR_ASIN_COZE_BATCH_SIZE:3}
|
||||
coze-text-only-batch-size: ${AIIMAGE_SIMILAR_ASIN_COZE_TEXT_ONLY_BATCH_SIZE:10}
|
||||
coze-credential-stripe-size: ${AIIMAGE_SIMILAR_ASIN_COZE_CREDENTIAL_STRIPE_SIZE:0}
|
||||
coze-connect-timeout-millis: ${AIIMAGE_SIMILAR_ASIN_COZE_CONNECT_TIMEOUT_MILLIS:10000}
|
||||
coze-read-timeout-millis: ${AIIMAGE_SIMILAR_ASIN_COZE_READ_TIMEOUT_MILLIS:60000}
|
||||
|
||||
Reference in New Issue
Block a user