实现项目归档功能
This commit is contained in:
@@ -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("项目归档成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除项目
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
* 校验项目是否允许写入
|
||||
*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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<com.ruoyi.common.utils.SecurityUtils> 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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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`
|
||||
- 保存单模型参数
|
||||
- 保存全部参数
|
||||
|
||||
拦截文案分别为:
|
||||
|
||||
- `已归档项目暂不允许上传或拉取数据`
|
||||
- `已归档项目暂不允许修改参数`
|
||||
|
||||
## 为什么这样实现
|
||||
|
||||
### 为什么新增专用归档接口
|
||||
|
||||
归档是独立业务动作,不适合混入“更新项目”接口,否则前端和后端都会失去明确语义,后续状态校验也会分散。
|
||||
|
||||
### 为什么把归档保护放到服务层
|
||||
|
||||
这次用户要求的是“归档后上传数据和参数配置不可操作”。如果只锁页面页签,依然可能通过接口直调写入数据,因此必须在服务层统一加一层归档态校验,前后端限制才真正一致。
|
||||
|
||||
## 结果
|
||||
|
||||
本次后端实施后,项目归档链路已经具备完整闭环:
|
||||
|
||||
- 列表页可真实归档
|
||||
- 非法状态无法归档
|
||||
- 已归档项目无法再上传或拉取数据
|
||||
- 已归档项目无法再修改参数
|
||||
@@ -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`。
|
||||
|
||||
### 为什么组件级保护仍要保留
|
||||
|
||||
页签锁定解决的是主入口问题,组件级保护解决的是复用和绕过问题。两层一起做,归档态只读限制才稳。
|
||||
|
||||
## 结果
|
||||
|
||||
本次前端实施后,用户在列表页归档项目后:
|
||||
|
||||
- 列表会显示真实归档结果
|
||||
- 已归档项目详情页中无法点击“上传数据”“参数配置”
|
||||
- 即使访问旧链接,也会自动切到“结果总览”
|
||||
@@ -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`
|
||||
- 参数保存链路会先检查归档态
|
||||
|
||||
## 结论
|
||||
|
||||
后端归档接口、状态流转和归档态写保护均已通过本地验证。
|
||||
@@ -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`
|
||||
- 新增归档逻辑未破坏既有打标轮询结构
|
||||
|
||||
## 结论
|
||||
|
||||
前端归档交互、页签禁用和组件级归档态保护均已通过本地验证。
|
||||
@@ -27,24 +27,15 @@
|
||||
</li>
|
||||
<li>
|
||||
<i class="el-icon-check"></i>
|
||||
<span>自动生成项目报告PDF</span>
|
||||
</li>
|
||||
<li>
|
||||
<i class="el-icon-check"></i>
|
||||
<span>移入归档库,可随时查看</span>
|
||||
<span>项目详情中的上传数据与参数配置页签将不可点击</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 删除选项 -->
|
||||
<div class="delete-option">
|
||||
<el-checkbox v-model="deleteData">同时删除项目相关数据(不可恢复)</el-checkbox>
|
||||
</div>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<div class="archive-hint">
|
||||
<i class="el-icon-info"></i>
|
||||
<span>归档后可从"归档库"中查看和恢复</span>
|
||||
<span>归档后项目进入只读查看状态,请确认后继续</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="param-config-container" v-loading="loading" element-loading-text="加载中...">
|
||||
<div v-if="isProjectTagging" class="readonly-tip">
|
||||
项目正在进行银行流水打标,参数暂不可修改。
|
||||
<div v-if="isProjectTagging || isProjectArchived" class="readonly-tip">
|
||||
{{ isProjectArchived ? "已归档项目暂不可修改参数。" : "项目正在进行银行流水打标,参数暂不可修改。" }}
|
||||
</div>
|
||||
|
||||
<!-- 模型参数卡片组(垂直堆叠) -->
|
||||
@@ -24,7 +24,7 @@
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
v-model="row.paramValue"
|
||||
:disabled="isProjectTagging"
|
||||
:disabled="isProjectTagging || isProjectArchived"
|
||||
placeholder="请输入阈值"
|
||||
@input="markAsModified(model.modelCode, row)"
|
||||
/>
|
||||
@@ -42,7 +42,7 @@
|
||||
|
||||
<!-- 统一保存按钮 -->
|
||||
<div class="button-section" v-if="!loading && modelGroups.length > 0">
|
||||
<el-button type="primary" :disabled="isProjectTagging || saving" @click="handleSaveAll" :loading="saving">
|
||||
<el-button type="primary" :disabled="isProjectTagging || isProjectArchived || saving" @click="handleSaveAll" :loading="saving">
|
||||
保存所有修改
|
||||
</el-button>
|
||||
<span v-if="modifiedCount > 0" class="modified-tip">
|
||||
@@ -82,6 +82,9 @@ export default {
|
||||
},
|
||||
isProjectTagging() {
|
||||
return String(this.projectInfo.projectStatus) === "3";
|
||||
},
|
||||
isProjectArchived() {
|
||||
return String(this.projectInfo.projectStatus) === "2";
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -159,6 +162,10 @@ export default {
|
||||
},
|
||||
|
||||
async handleSaveAll() {
|
||||
if (this.isProjectArchived) {
|
||||
this.$message.warning("已归档项目暂不可修改参数");
|
||||
return;
|
||||
}
|
||||
if (this.isProjectTagging) {
|
||||
this.$message.warning("项目正在进行银行流水打标,参数暂不可修改");
|
||||
return;
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<el-button
|
||||
size="small"
|
||||
icon="el-icon-download"
|
||||
:disabled="isProjectTagging"
|
||||
:disabled="isProjectTagging || isProjectArchived"
|
||||
@click="handleFetchBankInfo"
|
||||
>
|
||||
拉取本行信息
|
||||
@@ -26,7 +26,7 @@
|
||||
<el-button
|
||||
size="small"
|
||||
icon="el-icon-upload2"
|
||||
:disabled="isProjectTagging"
|
||||
:disabled="isProjectTagging || isProjectArchived"
|
||||
@click="handleGoCreditInfoPage"
|
||||
>
|
||||
征信导入
|
||||
@@ -34,8 +34,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isProjectTagging" class="tagging-lock-tip">
|
||||
项目正在进行银行流水打标,暂不可上传或拉取数据。
|
||||
<div v-if="isProjectTagging || isProjectArchived" class="tagging-lock-tip">
|
||||
{{ isProjectArchived ? "项目已归档,暂不可上传或拉取数据。" : "项目正在进行银行流水打标,暂不可上传或拉取数据。" }}
|
||||
</div>
|
||||
|
||||
<!-- 上传模块 -->
|
||||
@@ -183,7 +183,7 @@
|
||||
v-model="pullBankInfoForm.idCardText"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
:disabled="isProjectTagging"
|
||||
:disabled="isProjectTagging || isProjectArchived"
|
||||
placeholder="支持逗号、中文逗号、换行分隔"
|
||||
/>
|
||||
<div class="pull-bank-field-tip">
|
||||
@@ -201,7 +201,7 @@
|
||||
:show-file-list="false"
|
||||
:file-list="idCardFileList"
|
||||
accept=".xls,.xlsx"
|
||||
:disabled="isProjectTagging"
|
||||
:disabled="isProjectTagging || isProjectArchived"
|
||||
:on-change="handleIdCardFileChange"
|
||||
:on-remove="handleIdCardFileRemove"
|
||||
>
|
||||
@@ -233,7 +233,7 @@
|
||||
<el-date-picker
|
||||
class="pull-bank-range-picker"
|
||||
v-model="pullBankInfoForm.dateRange"
|
||||
:disabled="isProjectTagging"
|
||||
:disabled="isProjectTagging || isProjectArchived"
|
||||
type="daterange"
|
||||
value-format="yyyy-MM-dd"
|
||||
:picker-options="pullBankInfoDatePickerOptions"
|
||||
@@ -247,7 +247,7 @@
|
||||
<el-button @click="pullBankInfoDialogVisible = false">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="isProjectTagging"
|
||||
:disabled="isProjectTagging || isProjectArchived"
|
||||
:loading="pullBankInfoLoading"
|
||||
@click="handleConfirmPullBankInfo"
|
||||
>
|
||||
@@ -268,7 +268,7 @@
|
||||
drag
|
||||
action="#"
|
||||
multiple
|
||||
:disabled="isProjectTagging"
|
||||
:disabled="isProjectTagging || isProjectArchived"
|
||||
:auto-upload="false"
|
||||
:on-change="handleBatchFileChange"
|
||||
:show-file-list="false"
|
||||
@@ -307,7 +307,7 @@
|
||||
<el-button @click="batchUploadDialogVisible = false">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="isProjectTagging || selectedFiles.length === 0"
|
||||
:disabled="isProjectTagging || isProjectArchived || selectedFiles.length === 0"
|
||||
:loading="uploadLoading"
|
||||
@click="handleBatchUpload"
|
||||
>开始上传</el-button
|
||||
@@ -450,6 +450,9 @@ export default {
|
||||
isProjectTagging() {
|
||||
return String(this.projectInfo.projectStatus) === "3";
|
||||
},
|
||||
isProjectArchived() {
|
||||
return String(this.projectInfo.projectStatus) === "2";
|
||||
},
|
||||
isReportDisabled() {
|
||||
return ["0", "3"].includes(String(this.projectInfo.projectStatus));
|
||||
},
|
||||
@@ -531,7 +534,7 @@ export default {
|
||||
if (card.key === "transaction") {
|
||||
return {
|
||||
...card,
|
||||
disabled: this.isProjectTagging,
|
||||
disabled: this.isProjectTagging || this.isProjectArchived,
|
||||
};
|
||||
}
|
||||
return card;
|
||||
@@ -580,7 +583,7 @@ export default {
|
||||
this.$emit("menu-change", { key: "overview", route: "overview" });
|
||||
},
|
||||
handleGoCreditInfoPage() {
|
||||
if (this.isProjectTagging) {
|
||||
if (this.isProjectTagging || this.isProjectArchived) {
|
||||
return;
|
||||
}
|
||||
this.$router.push("/maintain/creditInfo");
|
||||
@@ -636,6 +639,9 @@ export default {
|
||||
return Array.from(new Set(merged)).join(", ");
|
||||
},
|
||||
async handleIdCardFileChange(file, fileList) {
|
||||
if (this.isProjectTagging || this.isProjectArchived) {
|
||||
return;
|
||||
}
|
||||
const latestFile = (fileList || []).slice(-1);
|
||||
const currentFile = latestFile[0] || file;
|
||||
const fileName = (currentFile && currentFile.name) || "";
|
||||
@@ -716,8 +722,12 @@ export default {
|
||||
return this.parseIdCardText(this.pullBankInfoForm.idCardText);
|
||||
},
|
||||
async handleConfirmPullBankInfo() {
|
||||
if (this.isProjectTagging) {
|
||||
this.$message.warning("项目正在进行银行流水打标,暂不可上传或拉取数据");
|
||||
if (this.isProjectTagging || this.isProjectArchived) {
|
||||
this.$message.warning(
|
||||
this.isProjectArchived
|
||||
? "项目已归档,暂不可上传或拉取数据"
|
||||
: "项目正在进行银行流水打标,暂不可上传或拉取数据"
|
||||
);
|
||||
return;
|
||||
}
|
||||
const idCards = this.buildFinalIdCardList();
|
||||
@@ -776,8 +786,12 @@ export default {
|
||||
},
|
||||
/** 拉取本行信息 */
|
||||
handleFetchBankInfo() {
|
||||
if (this.isProjectTagging) {
|
||||
this.$message.warning("项目正在进行银行流水打标,暂不可上传或拉取数据");
|
||||
if (this.isProjectTagging || this.isProjectArchived) {
|
||||
this.$message.warning(
|
||||
this.isProjectArchived
|
||||
? "项目已归档,暂不可上传或拉取数据"
|
||||
: "项目正在进行银行流水打标,暂不可上传或拉取数据"
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.resetPullBankInfoForm();
|
||||
@@ -868,8 +882,12 @@ export default {
|
||||
|
||||
/** 开始批量上传 */
|
||||
async handleBatchUpload() {
|
||||
if (this.isProjectTagging) {
|
||||
this.$message.warning("项目正在进行银行流水打标,暂不可上传或拉取数据");
|
||||
if (this.isProjectTagging || this.isProjectArchived) {
|
||||
this.$message.warning(
|
||||
this.isProjectArchived
|
||||
? "项目已归档,暂不可上传或拉取数据"
|
||||
: "项目正在进行银行流水打标,暂不可上传或拉取数据"
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (this.selectedFiles.length === 0) {
|
||||
|
||||
@@ -39,8 +39,8 @@
|
||||
@select="handleMenuSelect"
|
||||
class="nav-menu"
|
||||
>
|
||||
<el-menu-item index="upload">上传数据</el-menu-item>
|
||||
<el-menu-item index="config">参数配置</el-menu-item>
|
||||
<el-menu-item index="upload" :disabled="isArchiveLockedTab('upload')">上传数据</el-menu-item>
|
||||
<el-menu-item index="config" :disabled="isArchiveLockedTab('config')">参数配置</el-menu-item>
|
||||
<el-menu-item index="overview">结果总览</el-menu-item>
|
||||
<el-menu-item index="special">专项排查</el-menu-item>
|
||||
<el-menu-item index="detail">流水明细查询</el-menu-item>
|
||||
@@ -107,6 +107,11 @@ export default {
|
||||
projectStatusPollingLoading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isProjectArchived() {
|
||||
return String(this.projectInfo.projectStatus) === "2";
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
"$route.params.projectId"(newId) {
|
||||
this.stopProjectStatusPolling();
|
||||
@@ -123,6 +128,10 @@ export default {
|
||||
},
|
||||
"projectInfo.projectStatus"() {
|
||||
this.syncProjectStatusPolling();
|
||||
const accessibleTab = this.resolveAccessibleTab(this.activeTab);
|
||||
if (accessibleTab !== this.activeTab) {
|
||||
this.setActiveTab(accessibleTab);
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
@@ -138,7 +147,16 @@ export default {
|
||||
const tab = (this.$route.query && this.$route.query.tab) || "";
|
||||
const validTabs = ["upload", "config", "overview", "special", "detail"];
|
||||
const targetTab = validTabs.includes(tab) ? tab : "upload";
|
||||
this.setActiveTab(targetTab);
|
||||
this.setActiveTab(this.resolveAccessibleTab(targetTab));
|
||||
},
|
||||
isArchiveLockedTab(tab) {
|
||||
return this.isProjectArchived && ["upload", "config"].includes(tab);
|
||||
},
|
||||
resolveAccessibleTab(tab) {
|
||||
if (this.isArchiveLockedTab(tab)) {
|
||||
return "overview";
|
||||
}
|
||||
return tab;
|
||||
},
|
||||
setActiveTab(index) {
|
||||
this.activeTab = index;
|
||||
@@ -319,6 +337,9 @@ export default {
|
||||
},
|
||||
/** 菜单选择事件 */
|
||||
handleMenuSelect(index) {
|
||||
if (this.isArchiveLockedTab(index)) {
|
||||
return;
|
||||
}
|
||||
console.log("菜单选择:", index);
|
||||
this.setActiveTab(index);
|
||||
},
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {getStatusCounts, listProject, rebuildProjectTags} from '@/api/ccdiProject'
|
||||
import {archiveProject, getStatusCounts, listProject, rebuildProjectTags} from '@/api/ccdiProject'
|
||||
import SearchBar from './components/SearchBar'
|
||||
import ProjectTable from './components/ProjectTable'
|
||||
import QuickEntry from './components/QuickEntry'
|
||||
@@ -268,11 +268,17 @@ export default {
|
||||
this.archiveDialogVisible = true;
|
||||
},
|
||||
/** 确认归档 */
|
||||
handleConfirmArchive(data) {
|
||||
console.log("确认归档:", data);
|
||||
this.$modal.msgSuccess("项目已归档");
|
||||
this.archiveDialogVisible = false;
|
||||
this.getList();
|
||||
async handleConfirmArchive(data) {
|
||||
try {
|
||||
await archiveProject(data.projectId)
|
||||
this.$modal.msgSuccess("项目归档成功")
|
||||
this.archiveDialogVisible = false
|
||||
this.currentArchiveProject = null
|
||||
this.getList()
|
||||
} catch (error) {
|
||||
const message = error && error.message ? error.message : "项目归档失败,请稍后重试"
|
||||
this.$modal.msgError(message)
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
42
ruoyi-ui/tests/unit/project-archive-readonly-guard.test.js
Normal file
42
ruoyi-ui/tests/unit/project-archive-readonly-guard.test.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const uploadPath = path.resolve(
|
||||
__dirname,
|
||||
"../../src/views/ccdiProject/components/detail/UploadData.vue"
|
||||
);
|
||||
const paramPath = path.resolve(
|
||||
__dirname,
|
||||
"../../src/views/ccdiProject/components/detail/ParamConfig.vue"
|
||||
);
|
||||
const uploadSource = fs.readFileSync(uploadPath, "utf8");
|
||||
const paramSource = fs.readFileSync(paramPath, "utf8");
|
||||
|
||||
assert(
|
||||
uploadSource.includes('return String(this.projectInfo.projectStatus) === "2";'),
|
||||
"上传数据页应声明归档态判断"
|
||||
);
|
||||
|
||||
assert(
|
||||
/if\s*\(this\.isProjectTagging\s*\|\|\s*this\.isProjectArchived\)/.test(uploadSource),
|
||||
"上传数据页的操作入口应同时拦截打标中和已归档"
|
||||
);
|
||||
|
||||
assert(
|
||||
paramSource.includes('return String(this.projectInfo.projectStatus) === "2";'),
|
||||
"参数配置页应声明归档态判断"
|
||||
);
|
||||
|
||||
assert(
|
||||
paramSource.includes("已归档项目暂不可修改参数"),
|
||||
"参数配置页应展示归档态只读提示"
|
||||
);
|
||||
|
||||
assert(
|
||||
/:disabled="isProjectTagging \|\| isProjectArchived \|\| saving"/.test(paramSource) ||
|
||||
/:disabled="isProjectTagging \|\| isProjectArchived"/.test(paramSource),
|
||||
"参数保存入口应在归档态下禁用"
|
||||
);
|
||||
|
||||
console.log("project-archive-readonly-guard test passed");
|
||||
40
ruoyi-ui/tests/unit/project-detail-archive-tab-lock.test.js
Normal file
40
ruoyi-ui/tests/unit/project-detail-archive-tab-lock.test.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const componentPath = path.resolve(
|
||||
__dirname,
|
||||
"../../src/views/ccdiProject/detail.vue"
|
||||
);
|
||||
const source = fs.readFileSync(componentPath, "utf8");
|
||||
|
||||
assert(
|
||||
source.includes('index="upload"') &&
|
||||
source.includes(`:disabled="isArchiveLockedTab('upload')"`) &&
|
||||
source.includes('index="config"') &&
|
||||
source.includes(`:disabled="isArchiveLockedTab('config')"`),
|
||||
"上传数据和参数配置页签应支持归档态禁用"
|
||||
);
|
||||
|
||||
assert(
|
||||
/isProjectArchived\(\)\s*\{\s*return String\(this\.projectInfo\.projectStatus\) === "2";\s*\}/.test(
|
||||
source
|
||||
),
|
||||
"详情页应声明归档态判断"
|
||||
);
|
||||
|
||||
assert(
|
||||
/initActiveTabFromRoute\(\)\s*\{[\s\S]*?this\.resolveAccessibleTab\(targetTab\)/.test(
|
||||
source
|
||||
),
|
||||
"详情页应在路由初始化时校正归档态不可访问页签"
|
||||
);
|
||||
|
||||
assert(
|
||||
/handleMenuSelect\(index\)\s*\{[\s\S]*?if\s*\(this\.isArchiveLockedTab\(index\)\)\s*\{\s*return;\s*\}/.test(
|
||||
source
|
||||
),
|
||||
"点击禁用页签时不应切换"
|
||||
);
|
||||
|
||||
console.log("project-detail-archive-tab-lock test passed");
|
||||
43
ruoyi-ui/tests/unit/project-list-archive-flow.test.js
Normal file
43
ruoyi-ui/tests/unit/project-list-archive-flow.test.js
Normal file
@@ -0,0 +1,43 @@
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const pagePath = path.resolve(__dirname, "../../src/views/ccdiProject/index.vue");
|
||||
const dialogPath = path.resolve(
|
||||
__dirname,
|
||||
"../../src/views/ccdiProject/components/ArchiveConfirmDialog.vue"
|
||||
);
|
||||
const pageSource = fs.readFileSync(pagePath, "utf8");
|
||||
const dialogSource = fs.readFileSync(dialogPath, "utf8");
|
||||
|
||||
assert(
|
||||
pageSource.includes("await archiveProject(data.projectId)"),
|
||||
"确认归档后应调用真实归档接口"
|
||||
);
|
||||
|
||||
assert(
|
||||
pageSource.includes('this.$modal.msgSuccess("项目归档成功")') ||
|
||||
pageSource.includes("this.$modal.msgSuccess('项目归档成功')"),
|
||||
"归档成功后应提示项目归档成功"
|
||||
);
|
||||
|
||||
assert(
|
||||
pageSource.includes("项目归档失败,请稍后重试"),
|
||||
"归档失败时应有统一兜底提示"
|
||||
);
|
||||
|
||||
[
|
||||
"自动生成项目报告PDF",
|
||||
"移入归档库,可随时查看",
|
||||
"同时删除项目相关数据",
|
||||
"归档后可从\"归档库\"中查看和恢复",
|
||||
].forEach((token) => {
|
||||
assert(!dialogSource.includes(token), `归档弹窗不应保留超范围文案: ${token}`);
|
||||
});
|
||||
|
||||
assert(
|
||||
!dialogSource.includes("deleteData"),
|
||||
"归档弹窗不应再保留删除数据状态"
|
||||
);
|
||||
|
||||
console.log("project-list-archive-flow test passed");
|
||||
@@ -23,10 +23,10 @@ assert(
|
||||
);
|
||||
|
||||
assert(
|
||||
/syncUploadCardDisabledState\(\)\s*\{[\s\S]*?card\.key === "transaction"[\s\S]*?disabled:\s*this\.isProjectTagging/.test(
|
||||
/syncUploadCardDisabledState\(\)\s*\{[\s\S]*?card\.key === "transaction"[\s\S]*?disabled:\s*this\.isProjectTagging\s*\|\|\s*this\.isProjectArchived/.test(
|
||||
source
|
||||
),
|
||||
"流水导入卡片应在项目打标中时同步置灰"
|
||||
"流水导入卡片应在项目打标中或已归档时同步置灰"
|
||||
);
|
||||
|
||||
assert(
|
||||
|
||||
Reference in New Issue
Block a user