From 5e5056b99d56b9ab449e5e5bffe66cf96ea7c569 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:46:42 +0800 Subject: [PATCH] =?UTF-8?q?task-143:=20publish=20=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E7=BA=A7=20file=20=E7=8A=B6=E6=80=81=E8=AF=AD=E4=B9=89?= =?UTF-8?q?=E5=A5=91=E7=BA=A6=EF=BC=88ready=20=E7=94=B1=20result.success+u?= =?UTF-8?q?rl=20=E6=8E=A8=E5=AF=BC=E3=80=81job=20=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E7=8B=AC=E7=AB=8B=E3=80=81=E5=A4=9A=E6=96=87=E4=BB=B6=E6=B7=B7?= =?UTF-8?q?=E5=90=88=E3=80=81=E6=97=A0=E7=BB=93=E6=9E=9C=20null=EF=BC=89+?= =?UTF-8?q?=208=20=E6=9D=A1=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../contract/PublishFileLevelStatusTest.java | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 backend-java/src/test/java/com/nanri/aiimage/modules/task/contract/PublishFileLevelStatusTest.java diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/task/contract/PublishFileLevelStatusTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/task/contract/PublishFileLevelStatusTest.java new file mode 100644 index 00000000..784214a9 --- /dev/null +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/task/contract/PublishFileLevelStatusTest.java @@ -0,0 +1,170 @@ +package com.nanri.aiimage.modules.task.contract; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nanri.aiimage.config.InstanceMetadata; +import com.nanri.aiimage.modules.file.service.LocalFileStorageService; +import com.nanri.aiimage.modules.file.service.oss.OssStorageService; +import com.nanri.aiimage.modules.publish.mapper.PublishFileMapper; +import com.nanri.aiimage.modules.publish.mapper.PublishItemMapper; +import com.nanri.aiimage.modules.publish.model.vo.PublishResultVo; +import com.nanri.aiimage.modules.publish.service.PublishTaskService; +import com.nanri.aiimage.modules.publish.service.PublishWorkbookService; +import com.nanri.aiimage.modules.task.mapper.FileResultMapper; +import com.nanri.aiimage.modules.task.mapper.FileTaskMapper; +import com.nanri.aiimage.modules.task.mapper.TaskChunkMapper; +import com.nanri.aiimage.modules.task.mapper.TaskScopeStateMapper; +import com.nanri.aiimage.modules.task.model.entity.FileResultEntity; +import com.nanri.aiimage.modules.task.model.entity.TaskFileJobEntity; +import com.nanri.aiimage.modules.task.service.TaskDistributedLockService; +import com.nanri.aiimage.modules.task.service.TaskFileJobService; +import com.nanri.aiimage.modules.task.service.TaskProgressLightAssembler; +import com.nanri.aiimage.modules.task.service.TransientPayloadStorageService; +import com.nanri.aiimage.modules.ziniao.service.ZiniaoShopSwitchService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.transaction.support.TransactionTemplate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; + +/** + * task-143:publish 文件级 file 状态语义契约(plan 08)。 + * publish 的 file 状态为文件级(PublishResultVo.fileReady 由 result.success+url + * 推导,PublishItemsPageVo.fileStatus = file.status),与其余模块 job 级不同; + * 语义固化。 + */ +@ExtendWith(MockitoExtension.class) +class PublishFileLevelStatusTest { + + private static final Long RESULT_ID = 9201L; + + @Mock private LocalFileStorageService localFileStorageService; + @Mock private ZiniaoShopSwitchService ziniaoShopSwitchService; + @Mock private PublishWorkbookService workbookService; + @Mock private PublishFileMapper publishFileMapper; + @Mock private PublishItemMapper publishItemMapper; + @Mock private FileTaskMapper fileTaskMapper; + @Mock private FileResultMapper fileResultMapper; + @Mock private TaskChunkMapper taskChunkMapper; + @Mock private TaskScopeStateMapper taskScopeStateMapper; + @Mock private TaskFileJobService taskFileJobService; + @Mock private TaskDistributedLockService taskDistributedLockService; + @Mock private TransientPayloadStorageService transientPayloadStorageService; + @Mock private OssStorageService ossStorageService; + @Mock private ObjectMapper objectMapper; + @Mock private TransactionTemplate transactionTemplate; + @Mock private InstanceMetadata instanceMetadata; + @Mock private TaskProgressLightAssembler taskProgressLightAssembler; + + private PublishTaskService service; + + @BeforeEach + void setUp() { + service = new PublishTaskService( + localFileStorageService, ziniaoShopSwitchService, workbookService, publishFileMapper, + publishItemMapper, fileTaskMapper, fileResultMapper, taskChunkMapper, taskScopeStateMapper, + taskFileJobService, taskDistributedLockService, transientPayloadStorageService, ossStorageService, + objectMapper, transactionTemplate, instanceMetadata, taskProgressLightAssembler); + lenient().when(ossStorageService.generateFreshDownloadUrl(any())) + .thenReturn("https://dl.example/publish-result.zip"); + } + + private PublishResultVo toVo(FileResultEntity result, TaskFileJobEntity job) { + return (PublishResultVo) ReflectionTestUtils.invokeMethod(service, "toResultVo", result, job); + } + + private FileResultEntity result(int success, String url) { + FileResultEntity result = new FileResultEntity(); + result.setId(RESULT_ID); + result.setSuccess(success); + result.setResultFileUrl(url); + result.setResultFilename("PUBLISH-1_上架结果.zip"); + return result; + } + + private TaskFileJobEntity job() { + TaskFileJobEntity job = new TaskFileJobEntity(); + job.setId(9301L); + job.setStatus("RUNNING"); + job.setRetryCount(1); + job.setErrorMessage("写入慢"); + return job; + } + + @Test + void readyTrueWhenSuccessAndUrlPresent() { + PublishResultVo vo = toVo(result(1, "oss://result/p.zip"), null); + + assertTrue(vo.getFileReady()); + assertEquals("https://dl.example/publish-result.zip", vo.getDownloadUrl()); + } + + @Test + void readyFalseWhenUrlMissing() { + PublishResultVo vo = toVo(result(1, null), null); + + assertFalse(vo.getFileReady()); + assertNull(vo.getDownloadUrl(), "未 ready 不生成下载直链"); + } + + @Test + void failedFileNeverReady() { + PublishResultVo vo = toVo(result(0, "oss://result/p.zip"), null); + + assertFalse(vo.getFileReady(), "success=0 的文件不算 ready"); + assertNull(vo.getDownloadUrl()); + } + + @Test + void perFileReadyIsIndependent() { + PublishResultVo ready = toVo(result(1, "oss://a.zip"), null); + PublishResultVo pending = toVo(result(1, null), null); + + assertTrue(ready.getFileReady()); + assertFalse(pending.getFileReady()); + } + + @Test + void mixedFilesShowMixedReadyState() { + PublishResultVo failed = toVo(result(0, "oss://f.zip"), null); + PublishResultVo ready = toVo(result(1, "oss://r.zip"), null); + + assertFalse(failed.getFileReady()); + assertTrue(ready.getFileReady()); + } + + @Test + void noResultReturnsNull() { + assertNull(toVo(null, null), "无结果记录不返回 VO"); + } + + @Test + void jobFieldsMappedWhenJobPresent() { + PublishResultVo vo = toVo(result(1, "oss://p.zip"), job()); + + assertEquals(9301L, vo.getFileJobId()); + assertEquals("RUNNING", vo.getFileJobStatus()); + assertEquals(1, vo.getFileJobRetryCount()); + assertEquals("写入慢", vo.getFileJobError()); + } + + @Test + void semanticsFrozen() { + PublishResultVo vo = toVo(result(1, "oss://p.zip"), job()); + + // 快照:ready 由 result 行推导(非 job),job 字段独立携带(文件级语义) + assertTrue(vo.getFileReady()); + assertEquals("PUBLISH-1_上架结果.zip", vo.getResultFilename()); + assertEquals(RESULT_ID, vo.getResultId()); + assertEquals("RUNNING", vo.getFileJobStatus()); + } +}