From 2039acdfe667b355270097b1873a7073e12a44fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Sat, 29 Aug 2026 14:22:07 +0800 Subject: [PATCH] =?UTF-8?q?task-1:=20=E5=BB=BA=E7=AB=8B=20Similar=20ASIN?= =?UTF-8?q?=20=E6=80=A7=E8=83=BD=E5=9F=BA=E7=BA=BF=E5=A4=B9=E5=85=B7=20(10?= =?UTF-8?q?00/5000=E8=A1=8C=E3=80=81=E5=9B=BE=E7=89=87=E5=BC=80=E5=85=B3?= =?UTF-8?q?=E3=80=81chunk=E4=B8=8Epayload=E9=87=87=E6=A0=B7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 SimilarAsinPerfFixture 确定性夹具:按 sourceFileKey+rowIndex 派生字段, 支持 0/1/1000/5000 行、图片开关两种模式、chunk 划分与 payload 字节采样; 空输入返回空、超限(>5000行/非法chunk/空key)抛 IllegalArgumentException。 含 10 个测试覆盖默认路径/批量/幂等/边界/非法输入/依赖失败恢复。 --- .../util/SimilarAsinPerfFixture.java | 139 ++++ .../util/SimilarAsinPerfFixtureTest.java | 198 +++++ check_progress.py | 50 ++ progress.json | 710 ++++++++++++++++++ 4 files changed, 1097 insertions(+) create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/util/SimilarAsinPerfFixture.java create mode 100644 backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/util/SimilarAsinPerfFixtureTest.java create mode 100644 check_progress.py create mode 100644 progress.json diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/util/SimilarAsinPerfFixture.java b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/util/SimilarAsinPerfFixture.java new file mode 100644 index 00000000..af5be0c1 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/util/SimilarAsinPerfFixture.java @@ -0,0 +1,139 @@ +package com.nanri.aiimage.modules.similarasin.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinParsedRowVo; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Similar ASIN 性能基线夹具:生成 1000/5000 行解析数据、图片开关两种模式、 + * chunk 划分与 payload 大小采样,供性能基线测试与压测复用。 + * 上限约束:单次最多 MAX_ROWS 行,防止基线夹具本身造成无界内存增长。 + */ +@Slf4j +public class SimilarAsinPerfFixture { + + public static final int MAX_ROWS = 5000; + public static final int DEFAULT_CHUNK_SIZE = 200; + + private static final String[] COUNTRIES = {"英国", "德国", "法国", "意大利", "西班牙"}; + private static final String[] TITLES = { + "Women Floral Dress Summer Casual", + "Men Cotton T-Shirt Crew Neck", + "Kids Waterproof Rain Jacket", + "Fitness Yoga Pants High Waist", + "Home Office Desk Lamp LED", + "Stainless Steel Water Bottle 750ml", + "Wireless Bluetooth Earbuds Pro", + "Pet Grooming Brush Cat Dog" + }; + private static final String[] SKU_PREFIX = {"SKU", "MSKU", "ASIN-ITEM", "PROD"}; + + private final ObjectMapper objectMapper; + + public SimilarAsinPerfFixture(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + /** 生成 rowCount 行解析行数据;withImages=true 时为每行生成 http 图片 URL。rowCount 超出 [0, MAX_ROWS] 时拒绝。 + * 所有字段由 sourceFileKey + rowIndex 确定性派生,同一输入必然产生相同输出(幂等)。 */ + public List generateRows(String sourceFileKey, int rowCount, boolean withImages) { + if (sourceFileKey == null || sourceFileKey.isBlank()) { + throw new IllegalArgumentException("sourceFileKey 不能为空"); + } + if (rowCount < 0 || rowCount > MAX_ROWS) { + throw new IllegalArgumentException("rowCount 必须在 [0, " + MAX_ROWS + "] 范围内,实际 " + rowCount); + } + List rows = new ArrayList<>(rowCount); + for (int i = 0; i < rowCount; i++) { + SimilarAsinParsedRowVo row = new SimilarAsinParsedRowVo(); + int rowIndex = i + 1; + long seed = (sourceFileKey.hashCode() * 31L + rowIndex) & 0x7fffffffL; + String sourceId = String.valueOf(rowIndex); + row.setSourceFileKey(sourceFileKey); + row.setSourceFilename(sourceFileKey.substring(sourceFileKey.lastIndexOf('/') + 1)); + row.setRowIndex(rowIndex); + row.setSourceId(sourceId); + row.setDisplayId(sourceId); + row.setRowToken(rowTokenFor(sourceFileKey, rowIndex)); + row.setAsin(deterministicAsin(seed)); + row.setCountry(COUNTRIES[(int) (seed >> 5) % COUNTRIES.length]); + row.setSku(SKU_PREFIX[(int) (seed >> 9) % SKU_PREFIX.length] + "-" + (1000 + rowIndex)); + row.setTitle(TITLES[(int) (seed >> 13) % TITLES.length]); + if (withImages) { + row.setUrl("https://m.media-amazon.com/images/I/" + deterministicAsin(seed ^ 0x5DEDE5B5L) + ".jpg"); + } else { + row.setUrl(""); + } + Map values = new LinkedHashMap<>(); + values.put("id", sourceId); + values.put("asin", row.getAsin()); + values.put("国家", row.getCountry()); + values.put("价格", String.format("%.2f", 1 + (seed % 9900) / 100.0)); + values.put("货号", row.getSku()); + values.put("标题", row.getTitle()); + if (withImages) { + values.put("主图URL", row.getUrl()); + } + row.setValues(values); + rows.add(row); + } + return rows; + } + + public String rowTokenFor(String sourceFileKey, Integer rowIndex) { + return sourceFileKey + "::row::" + rowIndex; + } + + /** 按 chunkSize 顺序划分;chunkSize 必须为正数,行集合不能为 null。 */ + public List> splitChunks(List rows, int chunkSize) { + if (rows == null) { + throw new IllegalArgumentException("rows 不能为 null"); + } + if (chunkSize <= 0) { + throw new IllegalArgumentException("chunkSize 必须为正数,实际 " + chunkSize); + } + List> chunks = new ArrayList<>(); + if (rows.isEmpty()) { + return chunks; + } + for (int from = 0; from < rows.size(); from += chunkSize) { + int to = Math.min(from + chunkSize, rows.size()); + chunks.add(new ArrayList<>(rows.subList(from, to))); + } + return chunks; + } + + /** 采样全量行 payload 大小与 chunk 划分数。序列化失败时向上抛出不产生部分结果。 */ + public Metrics samplePayload(List rows, boolean withImages, int chunkSize) { + List> chunks = splitChunks(rows, chunkSize); + if (rows.isEmpty()) { + return new Metrics(0, 0, 0); + } + try { + byte[] bytes = objectMapper.writeValueAsString(rows).getBytes(StandardCharsets.UTF_8); + return new Metrics(rows.size(), chunks.size(), bytes.length); + } catch (Exception ex) { + throw new IllegalStateException("payload 采样序列化失败", ex); + } + } + + public record Metrics(int rowCount, int chunkCount, long payloadBytes) { + } + + private static String deterministicAsin(long seed) { + StringBuilder sb = new StringBuilder("B0"); + long state = seed; + for (int i = 0; i < 8; i++) { + state = state * 6364136223846793005L + 1442695040888963407L; + int pick = (int) ((state >>> 33) % 36); + sb.append(pick < 10 ? (char) ('0' + pick) : (char) ('A' + pick - 10)); + } + return sb.toString(); + } +} diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/util/SimilarAsinPerfFixtureTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/util/SimilarAsinPerfFixtureTest.java new file mode 100644 index 00000000..429b671f --- /dev/null +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/util/SimilarAsinPerfFixtureTest.java @@ -0,0 +1,198 @@ +package com.nanri.aiimage.modules.similarasin.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinParsedRowVo; +import org.junit.jupiter.api.Test; + +import java.util.LinkedHashMap; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Task 1:Similar ASIN 性能基线夹具(1000/5000 行、图片开关、chunk 数与 payload 大小采样)。 + * 先写测试确认 RED,再实现 SimilarAsinPerfFixture。 + */ +class SimilarAsinPerfFixtureTest { + + private final SimilarAsinPerfFixture fixture = new SimilarAsinPerfFixture(new ObjectMapper()); + + @Test + void test_task_001_payload_chunk_image_normal_default_path() { + // 1000 行、带图片开关,默认 chunk 大小 + List rows = fixture.generateRows("uploads/20260829/base.xlsx", 1000, true); + assertEquals(1000, rows.size()); + // 行必须包含 url 图片地址 + assertFalse(rows.get(0).getUrl().isBlank()); + assertTrue(rows.get(0).getUrl().startsWith("http")); + // rowToken 稳定且唯一 + assertEquals(rows.get(0).getRowToken(), fixture.rowTokenFor(rows.get(0).getSourceFileKey(), rows.get(0).getRowIndex())); + assertEquals(1000, rows.stream().map(SimilarAsinParsedRowVo::getRowToken).distinct().count()); + + List> chunks = fixture.splitChunks(rows, 200); + assertEquals(5, chunks.size()); + assertEquals(200, chunks.get(0).size()); + + SimilarAsinPerfFixture.Metrics metrics = fixture.samplePayload(rows, true, 200); + assertEquals(1000, metrics.rowCount()); + assertEquals(5, metrics.chunkCount()); + assertTrue(metrics.payloadBytes() > 0, "payload 采样字节数必须大于 0"); + } + + @Test + void test_task_001_payload_chunk_image_normal_multiple_items() { + // 5000 行批量场景:顺序稳定、chunk 数正确、结果不丢失 + List rows = fixture.generateRows("uploads/20260829/multi.xlsx", 5000, false); + assertEquals(5000, rows.size()); + assertTrue(rows.get(0).getUrl().isBlank(), "图片开关关闭时 url 必须为空"); + + List> chunks = fixture.splitChunks(rows, 200); + assertEquals(25, chunks.size()); + // 顺序稳定:拼接后与原始一致 + List restored = chunks.stream().flatMap(List::stream).toList(); + assertEquals(rows.size(), restored.size()); + for (int i = 0; i < rows.size(); i++) { + assertEquals(rows.get(i).getRowToken(), restored.get(i).getRowToken()); + } + // 所有行 rowIndex 递增 + for (int i = 0; i < rows.size(); i++) { + assertEquals(i + 1, rows.get(i).getRowIndex()); + } + } + + @Test + void test_task_001_payload_chunk_image_normal_repeated_operation_is_idempotent() { + List first = fixture.generateRows("uploads/20260829/idem.xlsx", 1000, true); + List second = fixture.generateRows("uploads/20260829/idem.xlsx", 1000, true); + // 同一输入重复生成:token 完全一致,不产生重复差异 + assertEquals(first.size(), second.size()); + for (int i = 0; i < first.size(); i++) { + assertEquals(first.get(i).getRowToken(), second.get(i).getRowToken()); + assertEquals(first.get(i).getAsin(), second.get(i).getAsin()); + } + // splitChunks 幂等:两次划分 chunk 数一致 + assertEquals(fixture.splitChunks(first, 200).size(), fixture.splitChunks(second, 200).size()); + // 采样指标幂等 + SimilarAsinPerfFixture.Metrics m1 = fixture.samplePayload(first, true, 200); + SimilarAsinPerfFixture.Metrics m2 = fixture.samplePayload(second, true, 200); + assertEquals(m1.payloadBytes(), m2.payloadBytes()); + } + + @Test + void test_task_001_payload_chunk_image_boundary_empty_input() { + List rows = fixture.generateRows("uploads/20260829/empty.xlsx", 0, true); + assertNotNull(rows); + assertEquals(0, rows.size()); + // 空行 splitChunks 返回空,不产生无效 chunk + assertEquals(0, fixture.splitChunks(rows, 200).size()); + // 空行采样:行数 0、chunk 0、payload 字节数为 0(不创建任何载荷) + SimilarAsinPerfFixture.Metrics metrics = fixture.samplePayload(rows, true, 200); + assertEquals(0, metrics.rowCount()); + assertEquals(0, metrics.chunkCount()); + assertEquals(0, metrics.payloadBytes()); + } + + @Test + void test_task_001_payload_chunk_image_boundary_single_item() { + // 单行:不依赖批量路径且结果正确 + List rows = fixture.generateRows("uploads/20260829/single.xlsx", 1, true); + assertEquals(1, rows.size()); + assertEquals(1, rows.get(0).getRowIndex()); + assertEquals(1, fixture.splitChunks(rows, 200).size()); + // 单行 chunk 划分后仍只含 1 行 + List> chunks = fixture.splitChunks(rows, 200); + assertEquals(1, chunks.get(0).size()); + SimilarAsinPerfFixture.Metrics metrics = fixture.samplePayload(rows, true, 200); + assertEquals(1, metrics.rowCount()); + assertEquals(1, metrics.chunkCount()); + assertTrue(metrics.payloadBytes() > 0); + } + + @Test + void test_task_001_payload_chunk_image_boundary_limit_and_overflow() { + // 超过最大行数(5000)时拒绝,不发生无界内存增长 + assertThrows(IllegalArgumentException.class, () -> fixture.generateRows("uploads/20260829/overflow.xlsx", 5001, true)); + // 达到最大允许值 5000 时允许 + assertEquals(5000, fixture.generateRows("uploads/20260829/max.xlsx", 5000, true).size()); + // chunkSize 非法值拒绝 + assertThrows(IllegalArgumentException.class, () -> fixture.splitChunks(fixture.generateRows("uploads/20260829/a.xlsx", 100, true), 0)); + assertThrows(IllegalArgumentException.class, () -> fixture.splitChunks(fixture.generateRows("uploads/20260829/b.xlsx", 100, true), -1)); + } + + @Test + void test_task_001_payload_chunk_image_invalid_input_rejected() { + // null 文件 key 拒绝 + assertThrows(IllegalArgumentException.class, () -> fixture.generateRows(null, 100, true)); + // 空白文件 key 拒绝 + assertThrows(IllegalArgumentException.class, () -> fixture.generateRows(" ", 100, true)); + // 负行数拒绝 + assertThrows(IllegalArgumentException.class, () -> fixture.generateRows("uploads/20260829/neg.xlsx", -1, true)); + // null 行集合分块拒绝 + assertThrows(IllegalArgumentException.class, () -> fixture.splitChunks(null, 200)); + } + + @Test + void test_task_001_payload_chunk_image_dependency_failure_releases_resources() { + // 采样时序列化器失败(mock 抛异常):错误可恢复,不产生部分结果 + ObjectMapper broken = new ObjectMapper() { + @Override + public String writeValueAsString(Object value) { + throw new IllegalStateException("serializer down"); + } + }; + SimilarAsinPerfFixture failingFixture = new SimilarAsinPerfFixture(broken); + List rows = failingFixture.generateRows("uploads/20260829/fail.xlsx", 1000, true); + assertThrows(IllegalStateException.class, () -> failingFixture.samplePayload(rows, true, 200)); + // 恢复后(换回正常 mapper)仍能正常工作 + SimilarAsinPerfFixture.Metrics recovered = fixture.samplePayload(fixture.generateRows("uploads/20260829/recover.xlsx", 1000, true), true, 200); + assertTrue(recovered.payloadBytes() > 0); + assertNotNull(recovered); + // 行对象在失败后仍可复用(不持有任何锁或已关闭资源) + assertTrue(rows.get(0).getAsin().startsWith("B0")); + // 验证图片开关两种模式下行字段差异明确 + List noImg = fixture.generateRows("uploads/20260829/nimg.xlsx", 10, false); + assertTrue(noImg.get(0).getUrl().isBlank()); + } + + @Test + void test_task_001_payload_chunk_image_normal_fields_populated() { + // 行字段完整性:asin/country/sku/title/values 均填充且稳定 + List rows = fixture.generateRows("uploads/20260829/fields.xlsx", 10, false); + SimilarAsinParsedRowVo row = rows.get(3); + assertTrue(row.getAsin().matches("B0[A-Z0-9]{8}")); + assertFalse(row.getCountry().isBlank()); + assertFalse(row.getSku().isBlank()); + assertFalse(row.getTitle().isBlank()); + assertNotNull(row.getValues()); + assertFalse(row.getValues().isEmpty()); + assertTrue(row.getValues().containsKey("asin")); + assertTrue(row.getValues().containsKey("国家")); + // 行号与 sourceId 关联正确 + assertEquals("4", row.getSourceId()); + assertEquals(4, row.getRowIndex()); + // values 是独立副本,修改不影响后续生成 + row.getValues().put("价格", "999"); + List again = fixture.generateRows("uploads/20260829/fields.xlsx", 10, false); + assertTrue(!again.get(3).getValues().getOrDefault("价格", "").equals("999")); + } + + @Test + void test_task_001_payload_chunk_image_boundary_chunk_size_edge() { + // chunk 边界:行数恰好整除 / 有余数 / 单 chunk 放不下 + List rows = fixture.generateRows("uploads/20260829/edge.xlsx", 100, false); + // 100 行 / 40 → 3 chunks(40+40+20) + List> chunks = fixture.splitChunks(rows, 40); + assertEquals(3, chunks.size()); + assertEquals(40, chunks.get(0).size()); + assertEquals(20, chunks.get(2).size()); + // chunkSize 大于总行数 → 单 chunk + assertEquals(1, fixture.splitChunks(rows, 500).size()); + // chunkSize 恰好等于行数 → 单 chunk 全量 + assertEquals(1, fixture.splitChunks(rows, 100).size()); + assertEquals(100, fixture.splitChunks(rows, 100).get(0).size()); + } +} diff --git a/check_progress.py b/check_progress.py new file mode 100644 index 00000000..058b47dd --- /dev/null +++ b/check_progress.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +"""检查 app_client 的 Step 2/后续开发任务是否全部完成。""" +from __future__ import annotations + +import json +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parent +PROGRESS_PATH = ROOT / "progress.json" + + +def main() -> int: + if not PROGRESS_PATH.is_file(): + print("progress.json not found", file=sys.stderr) + return 2 + + try: + data = json.loads(PROGRESS_PATH.read_text(encoding="utf-8")) + except Exception as exc: + print(f"invalid progress.json: {exc}", file=sys.stderr) + return 2 + + total = data.get("total_tasks") + tasks = data.get("tasks") + if not isinstance(total, int) or total <= 0 or not isinstance(tasks, list): + print("invalid progress shape", file=sys.stderr) + return 2 + if len(tasks) != total: + print(f"task count mismatch: total_tasks={total}, tasks={len(tasks)}", file=sys.stderr) + return 2 + + ids = [task.get("id") for task in tasks if isinstance(task, dict)] + if ids != list(range(1, total + 1)): + print("task ids are not contiguous from 1", file=sys.stderr) + return 2 + + pending = [task for task in tasks if task.get("status") != "done"] + if pending: + print(f"pending tasks: {len(pending)}/{total}") + print("all tasks done: false") + return 1 + + print(f"all tasks done: true ({total})") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/progress.json b/progress.json new file mode 100644 index 00000000..a13dde2e --- /dev/null +++ b/progress.json @@ -0,0 +1,710 @@ +{ + "version": 1, + "total_tasks": 100, + "completed": 1, + "rounds": 1, + "started_at": "2026-08-29T14:10:48+08:00", + "updated_at": "2026-08-29T14:20:00+08:00", + "tasks": [ + { + "id": 1, + "title": "建立 Similar ASIN 性能基线夹具:1000/5000 行、图片开关、chunk 数与 payload 大小采样", + "module": "similarasin", + "dependency": "无", + "status": "done" + }, + { + "id": 2, + "title": "将解析载荷改为单一规范行集合,消除 items/groups/allItems 重复数据结构", + "module": "similarasin", + "dependency": "1", + "status": "pending" + }, + { + "id": 3, + "title": "保留旧 payload 读取兼容逻辑,并验证新旧结构均可恢复全量行", + "module": "similarasin", + "dependency": "2", + "status": "pending" + }, + { + "id": 4, + "title": "解析接口改为只返回固定数量预览行,完整行仅保存在后端任务载荷", + "module": "similarasin", + "dependency": "3", + "status": "pending" + }, + { + "id": 5, + "title": "为预览行数量增加配置边界、空文件和超限输入校验", + "module": "similarasin", + "dependency": "4", + "status": "pending" + }, + { + "id": 6, + "title": "将分组数据改为索引/范围引用,避免 groups 嵌套复制完整行对象", + "module": "similarasin", + "dependency": "5", + "status": "pending" + }, + { + "id": 7, + "title": "限制单文件大小、最大行数和最大字段长度,防止解析任务无界增长", + "module": "similarasin", + "dependency": "6", + "status": "pending" + }, + { + "id": 8, + "title": "将 WorkbookFactory 输入解析改为受控读取,并验证超大 Excel 的失败提示", + "module": "similarasin", + "dependency": "7", + "status": "pending" + }, + { + "id": 9, + "title": "将 chunk 查询从单行分页改为批量 keyset 分页,保持低内存读取", + "module": "similarasin", + "dependency": "8", + "status": "pending" + }, + { + "id": 10, + "title": "为 chunk 结果建立按 row key 的批量索引,消除跨 chunk 线性扫描", + "module": "similarasin", + "dependency": "9", + "status": "pending" + }, + { + "id": 11, + "title": "将 Coze 结果合并的重复检测从 O(n²) 改为 HashSet/稳定 row key", + "module": "similarasin", + "dependency": "10", + "status": "pending" + }, + { + "id": 12, + "title": "扩展 Coze 结果缓冲覆盖范围,减少频繁读写完整 chunk payload", + "module": "similarasin", + "dependency": "11", + "status": "pending" + }, + { + "id": 13, + "title": "为 chunk 合并增加单次最大行数与 payload 字节上限", + "module": "similarasin", + "dependency": "12", + "status": "pending" + }, + { + "id": 14, + "title": "图片 DB cache 改为批量读取缩略图,并只更新实际命中的 last_used_at", + "module": "similarasin", + "dependency": "13", + "status": "pending" + }, + { + "id": 15, + "title": "将图片缓存访问时间更新改为异步批量刷新,减少逐图 UPDATE", + "module": "similarasin", + "dependency": "14", + "status": "pending" + }, + { + "id": 16, + "title": "图片预取改为短预算 best-effort,超时后直接回退 URL", + "module": "similarasin", + "dependency": "15", + "status": "pending" + }, + { + "id": 17, + "title": "优化图片解码采样、像素上限和 JPEG 质量搜索,降低 CPU 与堆峰值", + "module": "similarasin", + "dependency": "16", + "status": "pending" + }, + { + "id": 18, + "title": "统一图片 spool 生命周期,确保超时、取消和异常路径删除临时文件", + "module": "similarasin", + "dependency": "17", + "status": "pending" + }, + { + "id": 19, + "title": "将 Coze 请求/响应及 Python 回传日志改为采样、截断和 DEBUG 级别", + "module": "similarasin", + "dependency": "18", + "status": "pending" + }, + { + "id": 20, + "title": "完成 Similar ASIN 端到端压测、JFR/GC 分析与结果文件兼容回归", + "module": "similarasin", + "dependency": "19", + "status": "pending" + }, + { + "id": 21, + "title": "建立店铺抓取性能基线:单店铺 1k/5k 行、多国家、图片成功/失败场景", + "module": "shopdatacrawl", + "dependency": "无", + "status": "pending" + }, + { + "id": 22, + "title": "将店铺 Excel 图片缓存替换为有界字节缓存", + "module": "shopdatacrawl", + "dependency": "21", + "status": "pending" + }, + { + "id": 23, + "title": "图片嵌入成功后立即释放外部缩略图字节副本", + "module": "shopdatacrawl", + "dependency": "22", + "status": "pending" + }, + { + "id": 24, + "title": "为店铺图片预取增加任务级数量、字节和超时上限", + "module": "shopdatacrawl", + "dependency": "23", + "status": "pending" + }, + { + "id": 25, + "title": "评估并实现店铺结果 workbook 的 SXSSF 或 spool 化写入路径", + "module": "shopdatacrawl", + "dependency": "24", + "status": "pending" + }, + { + "id": 26, + "title": "为模板 workbook 增加大行数下的样式、图片和工作表兼容测试", + "module": "shopdatacrawl", + "dependency": "25", + "status": "pending" + }, + { + "id": 27, + "title": "将 chunk 接收改为原子插入/幂等 upsert,减少先查后插", + "module": "shopdatacrawl", + "dependency": "26", + "status": "pending" + }, + { + "id": 28, + "title": "以 scope 计数器替代每个 chunk 的 COUNT(*) 完整统计", + "module": "shopdatacrawl", + "dependency": "27", + "status": "pending" + }, + { + "id": 29, + "title": "合并 scope 状态查询与更新,减少单 chunk 数据库往返", + "module": "shopdatacrawl", + "dependency": "28", + "status": "pending" + }, + { + "id": 30, + "title": "为国家结果行建立稳定去重键,替换线性重复扫描", + "module": "shopdatacrawl", + "dependency": "29", + "status": "pending" + }, + { + "id": 31, + "title": "将任务快照改为轻量进度字段,避免每次写入完整结果 JSON", + "module": "shopdatacrawl", + "dependency": "30", + "status": "pending" + }, + { + "id": 32, + "title": "将 task entity 本地缓存替换为有容量和过期回收的实现", + "module": "shopdatacrawl", + "dependency": "31", + "status": "pending" + }, + { + "id": 33, + "title": "将店铺源文件 key 映射改为确定路径,取消临时目录递归扫描", + "module": "shopdatacrawl", + "dependency": "32", + "status": "pending" + }, + { + "id": 34, + "title": "将 ownerInstanceId 从 JSON 查询迁移到显式列并补充索引", + "module": "shopdatacrawl", + "dependency": "33", + "status": "pending" + }, + { + "id": 35, + "title": "将每日累计文件改为数据层增量模型,避免每次下载并重写完整 XLSX", + "module": "shopdatacrawl", + "dependency": "34", + "status": "pending" + }, + { + "id": 36, + "title": "为每日累计文件引入版本号/CAS,缩短店铺级锁的持有时间", + "module": "shopdatacrawl", + "dependency": "35", + "status": "pending" + }, + { + "id": 37, + "title": "拆分每日累计文件组装与任务结果接收,增加异步文件作业状态", + "module": "shopdatacrawl", + "dependency": "36", + "status": "pending" + }, + { + "id": 38, + "title": "历史列表与进度查询增加分页、字段裁剪和批量任务加载", + "module": "shopdatacrawl", + "dependency": "37", + "status": "pending" + }, + { + "id": 39, + "title": "补充删除、超时、重复回传和累计文件失败的资源清理测试", + "module": "shopdatacrawl", + "dependency": "38", + "status": "pending" + }, + { + "id": 40, + "title": "完成店铺抓取压测并比较内存、CPU、DB QPS、对象存储流量和锁等待", + "module": "shopdatacrawl", + "dependency": "39", + "status": "pending" + }, + { + "id": 41, + "title": "建立 Collect Data 1k/10k 行、多个 chunk 和品牌检测场景基线", + "module": "collectdata", + "dependency": "无", + "status": "pending" + }, + { + "id": 42, + "title": "限制采集解析的文件大小、最大行数和单 chunk 行数", + "module": "collectdata", + "dependency": "41", + "status": "pending" + }, + { + "id": 43, + "title": "将采集源文件查找改为确定路径/索引查询", + "module": "collectdata", + "dependency": "42", + "status": "pending" + }, + { + "id": 44, + "title": "保留原始 chunk payload 的同时,减少逐行 extra JSON 的重复序列化", + "module": "collectdata", + "dependency": "43", + "status": "pending" + }, + { + "id": 45, + "title": "将 ASIN 去重查询与无效品牌查询统一为批量集合查询", + "module": "collectdata", + "dependency": "44", + "status": "pending" + }, + { + "id": 46, + "title": "跳过空品牌批次的无效远程品牌检查请求", + "module": "collectdata", + "dependency": "45", + "status": "pending" + }, + { + "id": 47, + "title": "为品牌检查结果增加任务内短期缓存,避免同品牌重复远程调用", + "module": "collectdata", + "dependency": "46", + "status": "pending" + }, + { + "id": 48, + "title": "将 invalid ASIN 记录改为批量 INSERT IGNORE/upsert", + "module": "collectdata", + "dependency": "47", + "status": "pending" + }, + { + "id": 49, + "title": "将结果明细从逐行 RustFS 对象改为 chunk 级 payload 存储", + "module": "collectdata", + "dependency": "48", + "status": "pending" + }, + { + "id": 50, + "title": "为结果明细设计批量 upsert mapper 与幂等唯一键", + "module": "collectdata", + "dependency": "49", + "status": "pending" + }, + { + "id": 51, + "title": "将 accepted 行的序列化和 hash 计算改为批量处理", + "module": "collectdata", + "dependency": "50", + "status": "pending" + }, + { + "id": 52, + "title": "生成结果文件时按 chunk 一次读取,取消逐行对象读取", + "module": "collectdata", + "dependency": "51", + "status": "pending" + }, + { + "id": 53, + "title": "将 rawRows 与 finalRows 的内存生命周期分段,避免同时长期驻留", + "module": "collectdata", + "dependency": "52", + "status": "pending" + }, + { + "id": 54, + "title": "将 finalRowCount 从每个 chunk COUNT(*) 改为任务内增量计数", + "module": "collectdata", + "dependency": "53", + "status": "pending" + }, + { + "id": 55, + "title": "将进度统计更新改为节流/合并写,减少高频 task UPDATE", + "module": "collectdata", + "dependency": "54", + "status": "pending" + }, + { + "id": 56, + "title": "为采集结果文件增加流式写入失败后的临时文件清理", + "module": "collectdata", + "dependency": "55", + "status": "pending" + }, + { + "id": 57, + "title": "为采集结果对象增加数据库删除与物理对象删除的一致性处理", + "module": "collectdata", + "dependency": "56", + "status": "pending" + }, + { + "id": 58, + "title": "补充外部品牌服务不可用、RustFS 超时和重复 chunk 的降级测试", + "module": "collectdata", + "dependency": "57", + "status": "pending" + }, + { + "id": 59, + "title": "完成采集模块数据库索引、批量 SQL 和对象存储调用次数验证", + "module": "collectdata", + "dependency": "58", + "status": "pending" + }, + { + "id": 60, + "title": "完成采集模块 10k 行压测并验收结果完整性、内存和吞吐", + "module": "collectdata", + "dependency": "59", + "status": "pending" + }, + { + "id": 61, + "title": "建立共享任务链路资源指标基线:线程、连接、队列、GC、Redis、RustFS 和 DB", + "module": "shared", + "dependency": "无", + "status": "pending" + }, + { + "id": 62, + "title": "为本地任务实体缓存增加最大条目数、TTL 和定时清理", + "module": "shared", + "dependency": "61", + "status": "pending" + }, + { + "id": 63, + "title": "为前端/后端进度快照增加写入去重和最小更新间隔", + "module": "shared", + "dependency": "62", + "status": "pending" + }, + { + "id": 64, + "title": "将 transient payload 压缩改为直接 gzip 二进制流上传", + "module": "shared", + "dependency": "63", + "status": "pending" + }, + { + "id": 65, + "title": "为 transient payload 读取增加流式解压和解压后字节上限", + "module": "shared", + "dependency": "64", + "status": "pending" + }, + { + "id": 66, + "title": "限制 RustFS 并发读写与重试的总资源预算,防止多任务叠加爆发", + "module": "shared", + "dependency": "65", + "status": "pending" + }, + { + "id": 67, + "title": "复用 RustFS/MinIO 客户端与 HTTP 连接池,减少每次操作创建客户端", + "module": "shared", + "dependency": "66", + "status": "pending" + }, + { + "id": 68, + "title": "将 payload 引用删除改为批量引用检查与异步物理删除", + "module": "shared", + "dependency": "67", + "status": "pending" + }, + { + "id": 69, + "title": "为数据库删除任务补充 transient payload 指针收集和清理队列", + "module": "shared", + "dependency": "68", + "status": "pending" + }, + { + "id": 70, + "title": "将历史清理改为 keyset 分页、小批量和短事务", + "module": "shared", + "dependency": "69", + "status": "pending" + }, + { + "id": 71, + "title": "清理日志改为数量与 sample ID,禁止输出超长任务 ID 列表", + "module": "shared", + "dependency": "70", + "status": "pending" + }, + { + "id": 72, + "title": "为文件作业实现数据库原子 claim,避免重复派发同一 job", + "module": "shared", + "dependency": "71", + "status": "pending" + }, + { + "id": 73, + "title": "为本地文件作业队列增加 in-flight 去重和队列背压", + "module": "shared", + "dependency": "72", + "status": "pending" + }, + { + "id": 74, + "title": "隔离调度线程池、文件作业线程池和外部 Coze/图片执行池", + "module": "shared", + "dependency": "73", + "status": "pending" + }, + { + "id": 75, + "title": "为虚拟线程任务增加等待队列上限与拒绝/延迟指标", + "module": "shared", + "dependency": "74", + "status": "pending" + }, + { + "id": 76, + "title": "将 JSON owner 查询迁移到显式列并补充任务/状态复合索引", + "module": "shared", + "dependency": "75", + "status": "pending" + }, + { + "id": 77, + "title": "统一 Coze、品牌检查和紫鸟 HTTP 客户端的连接复用策略", + "module": "shared", + "dependency": "76", + "status": "pending" + }, + { + "id": 78, + "title": "为所有外部调用增加耗时、重试、失败率和 payload 字节指标", + "module": "shared", + "dependency": "77", + "status": "pending" + }, + { + "id": 79, + "title": "为对象存储、数据库和队列增加故障注入测试", + "module": "shared", + "dependency": "78", + "status": "pending" + }, + { + "id": 80, + "title": "补充 JVM 堆、直接内存、临时磁盘和连接池容量配置说明", + "module": "shared", + "dependency": "79", + "status": "pending" + }, + { + "id": 81, + "title": "建立前端任务轮询请求量、响应体大小和页面内存基线", + "module": "frontend", + "dependency": "无", + "status": "pending" + }, + { + "id": 82, + "title": "为进度响应 Map 增加 TTL 清理与最大条目数", + "module": "frontend", + "dependency": "81", + "status": "pending" + }, + { + "id": 83, + "title": "统一不同页面的轮询去重、in-flight 合并和终态清理", + "module": "frontend", + "dependency": "82", + "status": "pending" + }, + { + "id": 84, + "title": "优化店铺抓取队列状态合并,消除 historyItems 的线性重复查找", + "module": "frontend", + "dependency": "83", + "status": "pending" + }, + { + "id": 85, + "title": "优化 Similar ASIN 轮询与文件生成等待,避免重复 force 请求", + "module": "frontend", + "dependency": "84", + "status": "pending" + }, + { + "id": 86, + "title": "将隐藏页面轮询间隔、前台恢复和退避策略统一配置化", + "module": "frontend", + "dependency": "85", + "status": "pending" + }, + { + "id": 87, + "title": "限制 localStorage 中任务、快照和队列数据的最大数量/字节数", + "module": "frontend", + "dependency": "86", + "status": "pending" + }, + { + "id": 88, + "title": "解析结果前端只接收预览数据,避免大 payload 进入响应式对象", + "module": "frontend", + "dependency": "87", + "status": "pending" + }, + { + "id": 89, + "title": "清理页面卸载时的所有 timer、请求和临时 URL", + "module": "frontend", + "dependency": "88", + "status": "pending" + }, + { + "id": 90, + "title": "为进度接口增加断网、超时、服务恢复和重复响应测试", + "module": "frontend", + "dependency": "89", + "status": "pending" + }, + { + "id": 91, + "title": "按页面拆分 Element Plus 与公共业务 chunk", + "module": "frontend", + "dependency": "90", + "status": "pending" + }, + { + "id": 92, + "title": "配置 Vite manualChunks 并比较各页面首屏传输大小", + "module": "frontend", + "dependency": "91", + "status": "pending" + }, + { + "id": 93, + "title": "补充 Similar ASIN、店铺抓取和采集数据页面的 E2E 核心路径", + "module": "frontend", + "dependency": "92", + "status": "pending" + }, + { + "id": 94, + "title": "补充移动端与桌面端响应式页面验收截图", + "module": "frontend", + "dependency": "93", + "status": "pending" + }, + { + "id": 95, + "title": "补充深色主题、错误提示、重试和终态刷新验收", + "module": "frontend", + "dependency": "94", + "status": "pending" + }, + { + "id": 96, + "title": "建立 Java/Python/Vue 三端统一的 API 字段兼容检查", + "module": "frontend", + "dependency": "95", + "status": "pending" + }, + { + "id": 97, + "title": "执行 Java 全量测试、Python unittest、Vue 类型检查与构建", + "module": "frontend", + "dependency": "96", + "status": "pending" + }, + { + "id": 98, + "title": "执行真实启动、健康检查、核心请求和外部依赖调用验证", + "module": "frontend", + "dependency": "97", + "status": "pending" + }, + { + "id": 99, + "title": "执行全链路压测并记录 CPU、内存、GC、DB、Redis、RustFS、网络结果", + "module": "frontend", + "dependency": "98", + "status": "pending" + }, + { + "id": 100, + "title": "完成发布前回滚演练、git commit 对应关系检查和交付清单", + "module": "frontend", + "dependency": "99", + "status": "pending" + } + ] +} \ No newline at end of file