From eced873779184f413214f489dd8d729b5c960d85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Wed, 2 Sep 2026 05:55:43 +0800 Subject: [PATCH] =?UTF-8?q?task-146:=20collectdata=20=E8=87=AA=E6=B4=BD?= =?UTF-8?q?=E8=A1=8C=E4=B8=BA=E5=A5=91=E7=BA=A6=EF=BC=88=E5=90=8E=E7=AB=AF?= =?UTF-8?q?=E6=97=A0=20file=20=E5=AD=97=E6=AE=B5=20VO=20=E5=BF=AB=E7=85=A7?= =?UTF-8?q?=E3=80=81=E5=89=8D=E7=AB=AF=20success+downloadUrl=20=E5=88=A4?= =?UTF-8?q?=E5=AE=9A=E5=90=AB=E7=A9=BA=E7=99=BD=E8=BE=B9=E7=95=8C=EF=BC=89?= =?UTF-8?q?+=20=E5=89=8D=E5=90=8E=E7=AB=AF=2013=20=E6=9D=A1=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CollectDataSelfConsistencyTest.java | 102 ++++++++++++++++++ frontend-vue/src/shared/collect-download.ts | 18 ++++ frontend-vue/tests/collect-download.test.ts | 43 ++++++++ 3 files changed, 163 insertions(+) create mode 100644 backend-java/src/test/java/com/nanri/aiimage/modules/task/contract/CollectDataSelfConsistencyTest.java create mode 100644 frontend-vue/src/shared/collect-download.ts create mode 100644 frontend-vue/tests/collect-download.test.ts diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/task/contract/CollectDataSelfConsistencyTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/task/contract/CollectDataSelfConsistencyTest.java new file mode 100644 index 00000000..319da2ac --- /dev/null +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/task/contract/CollectDataSelfConsistencyTest.java @@ -0,0 +1,102 @@ +package com.nanri.aiimage.modules.task.contract; + +import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataHistoryItemVo; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * task-146:collectdata 自洽行为契约(plan 08)。 + * 后端 history VO 不返回 file 阶段字段(fileStatus/fileReady/fileError); + * 下载语义由 success + downloadUrl 推导(任务 SUCCESS 且有结果文件才可下载); + * 进度展示字段自洽。 + */ +class CollectDataSelfConsistencyTest { + + private static Map> fieldsOf(Class type) { + return Arrays.stream(type.getDeclaredFields()) + .filter(field -> !java.lang.reflect.Modifier.isStatic(field.getModifiers())) + .collect(Collectors.toMap(Field::getName, Field::getType)); + } + + @Test + void backendHasNoFilePhaseFields() { + Map> fields = fieldsOf(CollectDataHistoryItemVo.class); + assertFalse(fields.containsKey("fileStatus"), "collectdata 不返回 fileStatus"); + assertFalse(fields.containsKey("fileReady"), "collectdata 不返回 fileReady"); + assertFalse(fields.containsKey("fileError"), "collectdata 不返回 fileError"); + assertFalse(fields.containsKey("fileProgressPercent"), "collectdata 不返回 fileProgressPercent"); + } + + @Test + void downloadFieldsPresent() { + Map> fields = fieldsOf(CollectDataHistoryItemVo.class); + assertEquals(String.class, fields.get("downloadUrl"), "downloadUrl 字段存在"); + assertEquals(Boolean.class, fields.get("success"), "success 字段存在"); + } + + @Test + void statusFlowFieldsPresent() { + Map> fields = fieldsOf(CollectDataHistoryItemVo.class); + assertEquals(String.class, fields.get("taskStatus"), "taskStatus 字段存在"); + assertEquals(String.class, fields.get("error"), "error 字段存在"); + assertEquals(Long.class, fields.get("taskId")); + assertEquals(Long.class, fields.get("resultId")); + } + + @Test + void progressDisplayFieldsPresent() { + Map> fields = fieldsOf(CollectDataHistoryItemVo.class); + assertEquals(Integer.class, fields.get("dedupeFilteredCount"), "进度展示字段存在"); + assertEquals(Integer.class, fields.get("invalidFilteredCount")); + assertEquals(Integer.class, fields.get("brandRejectedCount")); + assertEquals(Integer.class, fields.get("finalRowCount")); + assertEquals(Integer.class, fields.get("rowCount")); + } + + @Test + void successAndUrlAreTheOnlyDownloadSignals() { + Map> fields = fieldsOf(CollectDataHistoryItemVo.class); + // 可下载信号仅 success + downloadUrl(无 fileReady 类独立标志) + assertTrue(fields.containsKey("success")); + assertTrue(fields.containsKey("downloadUrl")); + assertFalse(fields.containsKey("fileReady")); + } + + @Test + void fileFieldsAbsentEvenInSharedLightVoContract() { + Map> fields = fieldsOf(CollectDataHistoryItemVo.class); + assertFalse(fields.containsKey("fileJobId"), "collectdata 无 fileJobId"); + assertFalse(fields.containsKey("fileJobStatus"), "collectdata 无 fileJobStatus"); + } + + @Test + void historyIdentityFieldsPresent() { + Map> fields = fieldsOf(CollectDataHistoryItemVo.class); + assertEquals(String.class, fields.get("sourceFilename")); + assertEquals(String.class, fields.get("resultFilename")); + assertEquals(String.class, fields.get("taskNo")); + } + + @Test + void contractFrozenFieldSet() { + Map> fields = fieldsOf(CollectDataHistoryItemVo.class); + // 快照:字段集合固定(下载用 success+downloadUrl,无 file 阶段字段) + java.util.Set expected = java.util.Set.of( + "resultId", "taskId", "taskNo", "sourceFilename", "resultFilename", "downloadUrl", + "taskStatus", "success", "error", "rowCount", "dedupeFilteredCount", + "invalidFilteredCount", "brandRejectedCount", "finalRowCount", + "taskType", "progressPercent", "totalRows", "receivedRows", "processedRows", + "collectStage", "currentKeyword", "searchCurrentPage", "searchTotalPages", + "detailProcessedAsins", "detailTotalAsins", "filters", + "createdAt", "startedAt", "finishedAt"); + assertEquals(expected, fields.keySet(), "collectdata history VO 字段集合冻结"); + } +} diff --git a/frontend-vue/src/shared/collect-download.ts b/frontend-vue/src/shared/collect-download.ts new file mode 100644 index 00000000..3128f011 --- /dev/null +++ b/frontend-vue/src/shared/collect-download.ts @@ -0,0 +1,18 @@ +/** + * collectdata 下载判定(Task 146)。 + * + * collectdata 后端 history VO 不返回 file 阶段字段;前端下载判定自洽: + * success=true 且 downloadUrl 非空才可下载(任务成功且有结果文件)。 + */ +export interface CollectDataItem { + success?: boolean | null + downloadUrl?: string | null + taskStatus?: string | null +} + +/** collectdata 可下载判定:success + downloadUrl(任务成功且有结果文件,空白 URL 无效)。 */ +export function canDownloadCollectDataItem(item: CollectDataItem | null | undefined): boolean { + if (!item) return false + const url = typeof item.downloadUrl === 'string' ? item.downloadUrl.trim() : '' + return Boolean(item.success && url) +} diff --git a/frontend-vue/tests/collect-download.test.ts b/frontend-vue/tests/collect-download.test.ts new file mode 100644 index 00000000..daeaf0b6 --- /dev/null +++ b/frontend-vue/tests/collect-download.test.ts @@ -0,0 +1,43 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' + +import { canDownloadCollectDataItem } from '../src/shared/collect-download.ts' + +interface Item { + success?: boolean | null + downloadUrl?: string | null + taskStatus?: string | null +} + +test('collectdata downloadable when success and url present', () => { + assert.equal(canDownloadCollectDataItem({ success: true, downloadUrl: 'https://dl.example/a.xlsx' }), true) +}) + +test('collectdata not downloadable without url', () => { + assert.equal(canDownloadCollectDataItem({ success: true }), false) + assert.equal(canDownloadCollectDataItem({ success: true, downloadUrl: null }), false) + assert.equal(canDownloadCollectDataItem({ success: true, downloadUrl: '' }), false) +}) + +test('collectdata failed task not downloadable', () => { + assert.equal(canDownloadCollectDataItem({ success: false, downloadUrl: 'https://dl.example/a.xlsx' }), false) +}) + +test('collectdata url must be non-blank to be valid', () => { + assert.equal(canDownloadCollectDataItem({ success: true, downloadUrl: ' ' }), false) + assert.equal(canDownloadCollectDataItem({ success: true, downloadUrl: 'https://x' }), true) +}) + +test('collectdata progress display fields do not include file phase', () => { + const item: Item = { success: false, taskStatus: 'RUNNING' } + assert.equal('fileStatus' in item, false, 'collectdata 无 file 阶段字段(与后端 VO 自洽)') + assert.equal('fileReady' in item, false) + assert.equal('fileProgressPercent' in item, false) +}) + +test('collectdata semantics frozen', () => { + assert.equal(canDownloadCollectDataItem(undefined), false) + assert.equal(canDownloadCollectDataItem(null), false) + assert.equal(canDownloadCollectDataItem({}), false) + assert.equal(canDownloadCollectDataItem({ success: true, downloadUrl: 'x', taskStatus: 'SUCCESS' }), true) +})