diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectController.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectController.java index 1f66f7bb..b9e61bf7 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectController.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectController.java @@ -14,6 +14,7 @@ import com.ruoyi.ccdi.project.service.ICcdiProjectService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; +import com.ruoyi.common.utils.SecurityUtils; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -53,6 +54,17 @@ public class CcdiProjectController extends BaseController { return AjaxResult.success("项目更新成功", vo); } + /** + * 归档项目 + */ + @PostMapping("/{projectId}/archive") + @Operation(summary = "归档项目") + @PreAuthorize("@ss.hasPermi('ccdi:project:edit')") + public AjaxResult archiveProject(@PathVariable Long projectId) { + projectService.archiveProject(projectId, SecurityUtils.getUsername()); + return AjaxResult.success("项目归档成功"); + } + /** * 删除项目 */ diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectService.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectService.java index fe6813ae..31bfd841 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectService.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectService.java @@ -60,6 +60,14 @@ public interface ICcdiProjectService { */ CcdiProjectStatusCountsVO getStatusCounts(); + /** + * 归档项目 + * + * @param projectId 项目ID + * @param operator 操作人 + */ + void archiveProject(Long projectId, String operator); + /** * 更新项目状态 * @@ -76,6 +84,14 @@ public interface ICcdiProjectService { */ void ensureProjectCanStartTagging(Long projectId); + /** + * 校验项目是否未归档 + * + * @param projectId 项目ID + * @param message 拒绝文案 + */ + void ensureProjectNotArchived(Long projectId, String message); + /** * 校验项目是否允许写入 * diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java index 02f18376..9c670e00 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java @@ -169,6 +169,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService { throw new IllegalArgumentException("开始日期不能晚于结束日期"); } + projectService.ensureProjectNotArchived(projectId, "已归档项目暂不允许上传或拉取数据"); projectService.ensureProjectWritable(projectId, "当前项目正在进行银行流水打标,暂不允许上传或拉取数据"); CcdiProject project = projectMapper.selectById(projectId); @@ -323,6 +324,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService { log.info("【文件上传】开始批量上传: projectId={}, 文件数量={}, username={}", projectId, files.length, username); + projectService.ensureProjectNotArchived(projectId, "已归档项目暂不允许上传或拉取数据"); projectService.ensureProjectWritable(projectId, "当前项目正在进行银行流水打标,暂不允许上传或拉取数据"); // 1. 生成批次ID diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImpl.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImpl.java index c8fcca29..6c46e22a 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImpl.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImpl.java @@ -111,6 +111,7 @@ public class CcdiModelParamServiceImpl implements ICcdiModelParamService { Long projectId = saveDTO.getProjectId(); if (projectId > 0) { + projectService.ensureProjectNotArchived(projectId, "已归档项目暂不允许修改参数"); projectService.ensureProjectWritable(projectId, "当前项目正在进行银行流水打标,暂不允许修改参数"); switchToCustomConfigIfNeeded(getRequiredProject(projectId)); } @@ -192,6 +193,7 @@ public class CcdiModelParamServiceImpl implements ICcdiModelParamService { Long projectId = saveAllDTO.getProjectId(); if (projectId > 0) { + projectService.ensureProjectNotArchived(projectId, "已归档项目暂不允许修改参数"); projectService.ensureProjectWritable(projectId, "当前项目正在进行银行流水打标,暂不允许修改参数"); switchToCustomConfigIfNeeded(getRequiredProject(projectId)); } diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java index 25308559..cbe83348 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java @@ -152,6 +152,26 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService { return vo; } + @Override + public void archiveProject(Long projectId, String operator) { + CcdiProject project = getRequiredProject(projectId); + if (CcdiProjectStatusConstants.ARCHIVED.equals(project.getStatus())) { + throw new ServiceException("项目已归档,无需重复操作"); + } + if (!CcdiProjectStatusConstants.COMPLETED.equals(project.getStatus())) { + throw new ServiceException("仅已完成项目允许归档"); + } + project.setStatus(CcdiProjectStatusConstants.ARCHIVED); + project.setIsArchived(1); + project.setUpdateBy(operator); + project.setUpdateTime(new Date()); + projectMapper.updateById(project); + log.info("【项目】项目状态变更: projectId={}, projectName={}, oldStatus={}, oldStatusLabel={}, newStatus={}, newStatusLabel={}, operator={}", + project.getProjectId(), project.getProjectName(), CcdiProjectStatusConstants.COMPLETED, + resolveStatusLabel(CcdiProjectStatusConstants.COMPLETED), CcdiProjectStatusConstants.ARCHIVED, + resolveStatusLabel(CcdiProjectStatusConstants.ARCHIVED), resolveOperator(operator)); + } + @Override public void updateProjectStatus(Long projectId, String status, String operator) { CcdiProject project = getRequiredProject(projectId); @@ -179,6 +199,14 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService { } } + @Override + public void ensureProjectNotArchived(Long projectId, String message) { + CcdiProject project = getRequiredProject(projectId); + if (CcdiProjectStatusConstants.ARCHIVED.equals(project.getStatus())) { + throw new ServiceException(message); + } + } + @Override public void ensureProjectWritable(Long projectId, String message) { CcdiProject project = getRequiredProject(projectId); diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectControllerContractTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectControllerContractTest.java new file mode 100644 index 00000000..10d51227 --- /dev/null +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectControllerContractTest.java @@ -0,0 +1,33 @@ +package com.ruoyi.ccdi.project.controller; + +import io.swagger.v3.oas.annotations.Operation; +import org.junit.jupiter.api.Test; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +import java.lang.reflect.Method; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class CcdiProjectControllerContractTest { + + @Test + void shouldExposeArchiveProjectEndpointContract() throws Exception { + RequestMapping requestMapping = CcdiProjectController.class.getAnnotation(RequestMapping.class); + Method method = CcdiProjectController.class.getMethod("archiveProject", Long.class); + PostMapping postMapping = method.getAnnotation(PostMapping.class); + PreAuthorize preAuthorize = method.getAnnotation(PreAuthorize.class); + Operation operation = method.getAnnotation(Operation.class); + + assertNotNull(requestMapping); + assertEquals("/ccdi/project", requestMapping.value()[0]); + assertNotNull(postMapping); + assertEquals("/{projectId}/archive", postMapping.value()[0]); + assertNotNull(preAuthorize); + assertEquals("@ss.hasPermi('ccdi:project:edit')", preAuthorize.value()); + assertNotNull(operation); + assertEquals("归档项目", operation.summary()); + } +} diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectControllerTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectControllerTest.java new file mode 100644 index 00000000..2baa70f2 --- /dev/null +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectControllerTest.java @@ -0,0 +1,55 @@ +package com.ruoyi.ccdi.project.controller; + +import com.ruoyi.ccdi.project.service.ICcdiProjectService; +import com.ruoyi.common.core.domain.AjaxResult; +import io.swagger.v3.oas.annotations.Operation; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PostMapping; + +import java.lang.reflect.Method; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class CcdiProjectControllerTest { + + @InjectMocks + private CcdiProjectController controller; + + @Mock + private ICcdiProjectService projectService; + + @Test + void shouldArchiveProject() throws Exception { + try (MockedStatic mocked = mockStatic(com.ruoyi.common.utils.SecurityUtils.class)) { + mocked.when(com.ruoyi.common.utils.SecurityUtils::getUsername).thenReturn("tester"); + + AjaxResult result = controller.archiveProject(40L); + + assertEquals(200, result.get("code")); + assertEquals("项目归档成功", result.get("msg")); + verify(projectService).archiveProject(40L, "tester"); + } + + Method method = CcdiProjectController.class.getMethod("archiveProject", Long.class); + PostMapping postMapping = method.getAnnotation(PostMapping.class); + PreAuthorize preAuthorize = method.getAnnotation(PreAuthorize.class); + Operation operation = method.getAnnotation(Operation.class); + + assertNotNull(postMapping); + assertEquals("/{projectId}/archive", postMapping.value()[0]); + assertNotNull(preAuthorize); + assertEquals("@ss.hasPermi('ccdi:project:edit')", preAuthorize.value()); + assertNotNull(operation); + assertEquals("归档项目", operation.summary()); + } +} diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java index cebe32f7..b979a552 100644 --- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java @@ -154,6 +154,8 @@ class CcdiFileUploadServiceImplTest { assertEquals("admin", inserted.get().get(0).getUploadUser()); assertEquals("uploading", inserted.get().get(0).getFileStatus()); assertEquals(1, TransactionSynchronizationManager.getSynchronizations().size()); + verify(projectService).ensureProjectNotArchived(PROJECT_ID, "已归档项目暂不允许上传或拉取数据"); + verify(projectService).ensureProjectWritable(PROJECT_ID, "当前项目正在进行银行流水打标,暂不允许上传或拉取数据"); } finally { TransactionSynchronizationManager.clearSynchronization(); } diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImplTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImplTest.java index 6dc2c8dc..afea5fef 100644 --- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImplTest.java +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImplTest.java @@ -152,6 +152,7 @@ class CcdiModelParamServiceImplTest { service.saveAllParams(buildSaveAllDto()); } + verify(projectService).ensureProjectNotArchived(40L, "已归档项目暂不允许修改参数"); verify(bankTagService).submitAutoRebuild(40L, TriggerType.AUTO_PARAM_CHANGE); } @@ -178,6 +179,7 @@ class CcdiModelParamServiceImplTest { service.saveParams(saveDTO); } + verify(projectService).ensureProjectNotArchived(40L, "已归档项目暂不允许修改参数"); verify(bankTagService).submitAutoRebuild(40L, TriggerType.AUTO_PARAM_CHANGE); } diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImplTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImplTest.java index 52b5e739..498ace8c 100644 --- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImplTest.java +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImplTest.java @@ -23,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @@ -68,6 +69,43 @@ class CcdiProjectServiceImplTest { () -> service.ensureProjectWritable(40L, "当前项目正在进行银行流水打标,暂不允许修改参数")); } + @Test + void shouldArchiveCompletedProject() { + CcdiProject project = new CcdiProject(); + project.setProjectId(40L); + project.setProjectName("专案A"); + project.setStatus("1"); + project.setIsArchived(0); + when(projectMapper.selectById(40L)).thenReturn(project); + + service.archiveProject(40L, "tester"); + + assertEquals("2", project.getStatus()); + assertEquals(1, project.getIsArchived()); + verify(projectMapper).updateById(project); + } + + @Test + void shouldRejectArchivingProjectWhenStatusIsNotCompleted() { + CcdiProject project = new CcdiProject(); + project.setProjectId(41L); + project.setStatus("0"); + when(projectMapper.selectById(41L)).thenReturn(project); + + assertThrows(ServiceException.class, () -> service.archiveProject(41L, "tester")); + } + + @Test + void shouldRejectWritingWhenProjectIsArchived() { + CcdiProject archived = new CcdiProject(); + archived.setProjectId(42L); + archived.setStatus("2"); + when(projectMapper.selectById(42L)).thenReturn(archived); + + assertThrows(ServiceException.class, + () -> service.ensureProjectNotArchived(42L, "已归档项目暂不允许修改参数")); + } + @Test void shouldLogProjectInitialStatusWhenProjectIsCreated() { CcdiProjectSaveDTO dto = new CcdiProjectSaveDTO(); diff --git a/docs/reports/implementation/2026-03-24-project-archive-backend-record.md b/docs/reports/implementation/2026-03-24-project-archive-backend-record.md new file mode 100644 index 00000000..48691777 --- /dev/null +++ b/docs/reports/implementation/2026-03-24-project-archive-backend-record.md @@ -0,0 +1,91 @@ +# 项目归档后端实施记录 + +## 本次改动概述 + +本次后端实施围绕“项目归档”和“归档后禁止继续写入”两条主线展开: + +1. 在项目管理控制器中新增归档接口 +2. 在项目服务中新增归档动作与归档态校验 +3. 将“已归档不可写”保护下沉到上传数据与参数保存服务入口 + +## 改动文件 + +- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectController.java` +- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectService.java` +- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java` +- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java` +- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImpl.java` +- `ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectControllerTest.java` +- `ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectControllerContractTest.java` +- `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImplTest.java` +- `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java` +- `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImplTest.java` + +## 实现说明 + +### 1. 新增项目归档接口 + +新增接口: + +- `POST /ccdi/project/{projectId}/archive` + +控制器仅负责: + +- 读取 `projectId` +- 获取当前登录用户名 +- 调用项目服务归档动作 +- 返回“项目归档成功” + +### 2. 项目服务新增归档动作 + +在 `CcdiProjectServiceImpl` 中新增: + +- `archiveProject(Long projectId, String operator)` +- `ensureProjectNotArchived(Long projectId, String message)` + +归档动作规则: + +- 仅允许 `status = 1` 的已完成项目归档 +- 若项目已归档,直接拒绝重复归档 +- 归档成功后写入: + - `status = 2` + - `isArchived = 1` + - `updateBy` + - `updateTime` + +### 3. 补齐归档态后端写保护 + +本次不仅做了前端入口限制,还把后端入口一起封住,避免通过绕过页面直接调用接口继续写入。 + +新增归档校验已接入: + +- `CcdiFileUploadServiceImpl` + - 拉取本行信息 + - 批量上传文件 +- `CcdiModelParamServiceImpl` + - 保存单模型参数 + - 保存全部参数 + +拦截文案分别为: + +- `已归档项目暂不允许上传或拉取数据` +- `已归档项目暂不允许修改参数` + +## 为什么这样实现 + +### 为什么新增专用归档接口 + +归档是独立业务动作,不适合混入“更新项目”接口,否则前端和后端都会失去明确语义,后续状态校验也会分散。 + +### 为什么把归档保护放到服务层 + +这次用户要求的是“归档后上传数据和参数配置不可操作”。如果只锁页面页签,依然可能通过接口直调写入数据,因此必须在服务层统一加一层归档态校验,前后端限制才真正一致。 + +## 结果 + +本次后端实施后,项目归档链路已经具备完整闭环: + +- 列表页可真实归档 +- 非法状态无法归档 +- 已归档项目无法再上传或拉取数据 +- 已归档项目无法再修改参数 diff --git a/docs/reports/implementation/2026-03-24-project-archive-frontend-record.md b/docs/reports/implementation/2026-03-24-project-archive-frontend-record.md new file mode 100644 index 00000000..d7fbc1c2 --- /dev/null +++ b/docs/reports/implementation/2026-03-24-project-archive-frontend-record.md @@ -0,0 +1,100 @@ +# 项目归档前端实施记录 + +## 本次改动概述 + +本次前端实施围绕三个目标完成: + +1. 项目列表“归档”按钮接入真实接口 +2. 归档确认弹窗收敛到本次需求边界 +3. 已归档项目在详情页中锁定“上传数据”“参数配置”,并在子组件中补只读保护 + +## 改动文件 + +- `ruoyi-ui/src/views/ccdiProject/index.vue` +- `ruoyi-ui/src/views/ccdiProject/components/ArchiveConfirmDialog.vue` +- `ruoyi-ui/src/views/ccdiProject/detail.vue` +- `ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue` +- `ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue` +- `ruoyi-ui/tests/unit/project-list-archive-flow.test.js` +- `ruoyi-ui/tests/unit/project-detail-archive-tab-lock.test.js` +- `ruoyi-ui/tests/unit/project-archive-readonly-guard.test.js` +- `ruoyi-ui/tests/unit/upload-data-disabled-cards.test.js` + +## 实现说明 + +### 1. 列表页接入真实归档动作 + +在 `index.vue` 中: + +- 引入 `archiveProject` +- 将 `handleConfirmArchive` 改为真实异步调用 +- 成功后提示“项目归档成功” +- 关闭弹窗并刷新列表 +- 失败时展示统一失败提示或透传错误消息 + +### 2. 收敛归档确认弹窗 + +在 `ArchiveConfirmDialog.vue` 中移除了与本次需求无关的旧内容: + +- 自动生成 PDF +- 归档库查看 / 恢复 +- 同时删除项目相关数据 + +保留为纯确认弹窗,只表达: + +- 确认归档项目 +- 归档后上传数据和参数配置页签不可点击 +- 项目进入只读查看状态 + +### 3. 详情页禁用归档态页签 + +在 `detail.vue` 中新增: + +- `isProjectArchived` +- `isArchiveLockedTab(tab)` +- `resolveAccessibleTab(tab)` + +并将其应用到: + +- 顶部“上传数据”“参数配置”页签的 `disabled` +- `initActiveTabFromRoute()` 的路由初始化校正 +- `handleMenuSelect()` 的点击拦截 +- 项目状态变化后的当前页签校正 + +### 4. 子组件增加归档态只读保护 + +虽然页签已经不可点,但仍在子组件内补了一层保护,避免未来从其他入口复用组件时绕过限制。 + +`UploadData.vue`: + +- 顶部按钮在归档态下禁用 +- 拉取本行信息、身份证文件解析、批量上传等操作方法在归档态前置返回 +- 流水上传卡片在归档态下置灰 + +`ParamConfig.vue`: + +- 展示“已归档项目暂不可修改参数”提示 +- 输入框和保存按钮在归档态下禁用 +- `handleSaveAll()` 在归档态直接返回 + +## 为什么这样实现 + +### 为什么弹窗文案必须收敛 + +弹窗文案必须和真实业务动作一致,否则会出现“页面承诺了删数据 / 生成 PDF / 归档库,但实际没做”的偏差,容易造成误解。 + +### 为什么页签禁用和 URL 拦截要同时做 + +只做禁用态还不够,用户仍可通过地址栏直接访问 `?tab=upload` 或 `?tab=config`。因此必须在路由初始化时同步改写到 `overview`。 + +### 为什么组件级保护仍要保留 + +页签锁定解决的是主入口问题,组件级保护解决的是复用和绕过问题。两层一起做,归档态只读限制才稳。 + +## 结果 + +本次前端实施后,用户在列表页归档项目后: + +- 列表会显示真实归档结果 +- 已归档项目详情页中无法点击“上传数据”“参数配置” +- 即使访问旧链接,也会自动切到“结果总览” diff --git a/docs/tests/records/2026-03-24-project-archive-backend-verification.md b/docs/tests/records/2026-03-24-project-archive-backend-verification.md new file mode 100644 index 00000000..ef89640f --- /dev/null +++ b/docs/tests/records/2026-03-24-project-archive-backend-verification.md @@ -0,0 +1,40 @@ +# 项目归档后端验证记录 + +## 验证范围 + +- 归档接口仅允许已完成项目调用 +- 归档成功后状态和 `isArchived` 正确写入 +- 已归档项目后端禁止上传或拉取数据 +- 已归档项目后端禁止修改参数 + +## 执行命令 + +```bash +mvn -pl ccdi-project -Dtest=CcdiProjectServiceImplTest,CcdiFileUploadServiceImplTest,CcdiModelParamServiceImplTest,CcdiProjectControllerTest,CcdiProjectControllerContractTest test +``` + +## 验证结果 + +- 命令执行成功,退出码为 `0` +- 本次共执行 `44` 条测试 +- 结果为: + - `Failures: 0` + - `Errors: 0` + - `Skipped: 0` + +## 覆盖结论 + +- `CcdiProjectControllerTest` / `CcdiProjectControllerContractTest` + - 归档接口路径、权限注解、返回结果已生效 +- `CcdiProjectServiceImplTest` + - 已完成项目可归档 + - 非已完成项目不可归档 + - 已归档项目会被写保护拒绝 +- `CcdiFileUploadServiceImplTest` + - 上传/拉取链路会先检查归档态 +- `CcdiModelParamServiceImplTest` + - 参数保存链路会先检查归档态 + +## 结论 + +后端归档接口、状态流转和归档态写保护均已通过本地验证。 diff --git a/docs/tests/records/2026-03-24-project-archive-frontend-verification.md b/docs/tests/records/2026-03-24-project-archive-frontend-verification.md new file mode 100644 index 00000000..188e40fd --- /dev/null +++ b/docs/tests/records/2026-03-24-project-archive-frontend-verification.md @@ -0,0 +1,44 @@ +# 项目归档前端验证记录 + +## 验证范围 + +- 列表归档按钮接入真实接口 +- 归档确认弹窗文案已收敛 +- 详情页上传数据与参数配置页签不可点击 +- 归档态下上传页与参数页存在组件级只读保护 + +## 执行命令 + +```bash +cd ruoyi-ui +node tests/unit/project-list-archive-flow.test.js +node tests/unit/project-detail-archive-tab-lock.test.js +node tests/unit/upload-data-disabled-cards.test.js +node tests/unit/project-archive-readonly-guard.test.js +node tests/unit/project-detail-tagging-polling.test.js +``` + +## 验证结果 + +- 五条前端契约测试均执行成功 +- 所有命令退出码为 `0` + +## 覆盖结论 + +- `project-list-archive-flow.test.js` + - 列表归档已接真实接口 + - 归档成功提示正确 + - 弹窗已清理超需求文案 +- `project-detail-archive-tab-lock.test.js` + - 已归档项目的上传数据与参数配置页签被锁定 + - 路由初始化会校正受限页签 +- `upload-data-disabled-cards.test.js` + - 流水上传卡片在归档态和打标态都会置灰 +- `project-archive-readonly-guard.test.js` + - 上传页和参数页都已声明归档态保护 +- `project-detail-tagging-polling.test.js` + - 新增归档逻辑未破坏既有打标轮询结构 + +## 结论 + +前端归档交互、页签禁用和组件级归档态保护均已通过本地验证。 diff --git a/ruoyi-ui/src/views/ccdiProject/components/ArchiveConfirmDialog.vue b/ruoyi-ui/src/views/ccdiProject/components/ArchiveConfirmDialog.vue index d009e478..da43bb70 100644 --- a/ruoyi-ui/src/views/ccdiProject/components/ArchiveConfirmDialog.vue +++ b/ruoyi-ui/src/views/ccdiProject/components/ArchiveConfirmDialog.vue @@ -27,24 +27,15 @@
  • - 自动生成项目报告PDF -
  • -
  • - - 移入归档库,可随时查看 + 项目详情中的上传数据与参数配置页签将不可点击
  • - -
    - 同时删除项目相关数据(不可恢复) -
    -
    - 归档后可从"归档库"中查看和恢复 + 归档后项目进入只读查看状态,请确认后继续
    @@ -77,8 +68,7 @@ export default { }, data() { return { - submitting: false, - deleteData: false + submitting: false } }, computed: { @@ -93,44 +83,21 @@ export default { } } }, - watch: { - visible(val) { - if (!val) { - this.deleteData = false - } - } - }, methods: { handleConfirm() { - if (this.deleteData) { - this.$confirm('删除后数据将无法恢复,是否确认删除?', '警告', { - confirmButtonText: '确认删除', - cancelButtonText: '取消', - type: 'warning' - }).then(() => { - this.doArchive() - }).catch(() => { - // 取消删除,但仍归档 - this.deleteData = false - this.doArchive() - }) - } else { - this.doArchive() - } + this.doArchive() }, doArchive() { this.submitting = true setTimeout(() => { this.submitting = false this.$emit('confirm', { - projectId: this.projectData.projectId, - deleteData: this.deleteData + projectId: this.projectData.projectId }) }, 500) }, handleClose() { this.$emit('close') - this.deleteData = false } } } @@ -203,22 +170,6 @@ export default { } } -.delete-option { - margin-bottom: 16px; - padding: 12px; - background-color: #fef0f0; - border-radius: 6px; - border: 1px solid #fde2e2; - - :deep(.el-checkbox__label) { - color: #F56C6C; - } - - :deep(.el-checkbox__input.is-checked + .el-checkbox__label) { - color: #F56C6C; - } -} - .archive-hint { display: flex; align-items: center; diff --git a/ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue b/ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue index 47324a15..618835f6 100644 --- a/ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue +++ b/ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue @@ -1,7 +1,7 @@