实现项目归档功能
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.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
import com.ruoyi.common.utils.SecurityUtils;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
@@ -53,6 +54,17 @@ public class CcdiProjectController extends BaseController {
|
|||||||
return AjaxResult.success("项目更新成功", vo);
|
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();
|
CcdiProjectStatusCountsVO getStatusCounts();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 归档项目
|
||||||
|
*
|
||||||
|
* @param projectId 项目ID
|
||||||
|
* @param operator 操作人
|
||||||
|
*/
|
||||||
|
void archiveProject(Long projectId, String operator);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新项目状态
|
* 更新项目状态
|
||||||
*
|
*
|
||||||
@@ -76,6 +84,14 @@ public interface ICcdiProjectService {
|
|||||||
*/
|
*/
|
||||||
void ensureProjectCanStartTagging(Long projectId);
|
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("开始日期不能晚于结束日期");
|
throw new IllegalArgumentException("开始日期不能晚于结束日期");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
projectService.ensureProjectNotArchived(projectId, "已归档项目暂不允许上传或拉取数据");
|
||||||
projectService.ensureProjectWritable(projectId, "当前项目正在进行银行流水打标,暂不允许上传或拉取数据");
|
projectService.ensureProjectWritable(projectId, "当前项目正在进行银行流水打标,暂不允许上传或拉取数据");
|
||||||
|
|
||||||
CcdiProject project = projectMapper.selectById(projectId);
|
CcdiProject project = projectMapper.selectById(projectId);
|
||||||
@@ -323,6 +324,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
|||||||
log.info("【文件上传】开始批量上传: projectId={}, 文件数量={}, username={}",
|
log.info("【文件上传】开始批量上传: projectId={}, 文件数量={}, username={}",
|
||||||
projectId, files.length, username);
|
projectId, files.length, username);
|
||||||
|
|
||||||
|
projectService.ensureProjectNotArchived(projectId, "已归档项目暂不允许上传或拉取数据");
|
||||||
projectService.ensureProjectWritable(projectId, "当前项目正在进行银行流水打标,暂不允许上传或拉取数据");
|
projectService.ensureProjectWritable(projectId, "当前项目正在进行银行流水打标,暂不允许上传或拉取数据");
|
||||||
|
|
||||||
// 1. 生成批次ID
|
// 1. 生成批次ID
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ public class CcdiModelParamServiceImpl implements ICcdiModelParamService {
|
|||||||
Long projectId = saveDTO.getProjectId();
|
Long projectId = saveDTO.getProjectId();
|
||||||
|
|
||||||
if (projectId > 0) {
|
if (projectId > 0) {
|
||||||
|
projectService.ensureProjectNotArchived(projectId, "已归档项目暂不允许修改参数");
|
||||||
projectService.ensureProjectWritable(projectId, "当前项目正在进行银行流水打标,暂不允许修改参数");
|
projectService.ensureProjectWritable(projectId, "当前项目正在进行银行流水打标,暂不允许修改参数");
|
||||||
switchToCustomConfigIfNeeded(getRequiredProject(projectId));
|
switchToCustomConfigIfNeeded(getRequiredProject(projectId));
|
||||||
}
|
}
|
||||||
@@ -192,6 +193,7 @@ public class CcdiModelParamServiceImpl implements ICcdiModelParamService {
|
|||||||
Long projectId = saveAllDTO.getProjectId();
|
Long projectId = saveAllDTO.getProjectId();
|
||||||
|
|
||||||
if (projectId > 0) {
|
if (projectId > 0) {
|
||||||
|
projectService.ensureProjectNotArchived(projectId, "已归档项目暂不允许修改参数");
|
||||||
projectService.ensureProjectWritable(projectId, "当前项目正在进行银行流水打标,暂不允许修改参数");
|
projectService.ensureProjectWritable(projectId, "当前项目正在进行银行流水打标,暂不允许修改参数");
|
||||||
switchToCustomConfigIfNeeded(getRequiredProject(projectId));
|
switchToCustomConfigIfNeeded(getRequiredProject(projectId));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,6 +152,26 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
|
|||||||
return vo;
|
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
|
@Override
|
||||||
public void updateProjectStatus(Long projectId, String status, String operator) {
|
public void updateProjectStatus(Long projectId, String status, String operator) {
|
||||||
CcdiProject project = getRequiredProject(projectId);
|
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
|
@Override
|
||||||
public void ensureProjectWritable(Long projectId, String message) {
|
public void ensureProjectWritable(Long projectId, String message) {
|
||||||
CcdiProject project = getRequiredProject(projectId);
|
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("admin", inserted.get().get(0).getUploadUser());
|
||||||
assertEquals("uploading", inserted.get().get(0).getFileStatus());
|
assertEquals("uploading", inserted.get().get(0).getFileStatus());
|
||||||
assertEquals(1, TransactionSynchronizationManager.getSynchronizations().size());
|
assertEquals(1, TransactionSynchronizationManager.getSynchronizations().size());
|
||||||
|
verify(projectService).ensureProjectNotArchived(PROJECT_ID, "已归档项目暂不允许上传或拉取数据");
|
||||||
|
verify(projectService).ensureProjectWritable(PROJECT_ID, "当前项目正在进行银行流水打标,暂不允许上传或拉取数据");
|
||||||
} finally {
|
} finally {
|
||||||
TransactionSynchronizationManager.clearSynchronization();
|
TransactionSynchronizationManager.clearSynchronization();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ class CcdiModelParamServiceImplTest {
|
|||||||
service.saveAllParams(buildSaveAllDto());
|
service.saveAllParams(buildSaveAllDto());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
verify(projectService).ensureProjectNotArchived(40L, "已归档项目暂不允许修改参数");
|
||||||
verify(bankTagService).submitAutoRebuild(40L, TriggerType.AUTO_PARAM_CHANGE);
|
verify(bankTagService).submitAutoRebuild(40L, TriggerType.AUTO_PARAM_CHANGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,6 +179,7 @@ class CcdiModelParamServiceImplTest {
|
|||||||
service.saveParams(saveDTO);
|
service.saveParams(saveDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
verify(projectService).ensureProjectNotArchived(40L, "已归档项目暂不允许修改参数");
|
||||||
verify(bankTagService).submitAutoRebuild(40L, TriggerType.AUTO_PARAM_CHANGE);
|
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.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.doAnswer;
|
import static org.mockito.Mockito.doAnswer;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
@@ -68,6 +69,43 @@ class CcdiProjectServiceImplTest {
|
|||||||
() -> service.ensureProjectWritable(40L, "当前项目正在进行银行流水打标,暂不允许修改参数"));
|
() -> 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
|
@Test
|
||||||
void shouldLogProjectInitialStatusWhenProjectIsCreated() {
|
void shouldLogProjectInitialStatusWhenProjectIsCreated() {
|
||||||
CcdiProjectSaveDTO dto = new CcdiProjectSaveDTO();
|
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>
|
||||||
<li>
|
<li>
|
||||||
<i class="el-icon-check"></i>
|
<i class="el-icon-check"></i>
|
||||||
<span>自动生成项目报告PDF</span>
|
<span>项目详情中的上传数据与参数配置页签将不可点击</span>
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<i class="el-icon-check"></i>
|
|
||||||
<span>移入归档库,可随时查看</span>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 删除选项 -->
|
|
||||||
<div class="delete-option">
|
|
||||||
<el-checkbox v-model="deleteData">同时删除项目相关数据(不可恢复)</el-checkbox>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 提示信息 -->
|
<!-- 提示信息 -->
|
||||||
<div class="archive-hint">
|
<div class="archive-hint">
|
||||||
<i class="el-icon-info"></i>
|
<i class="el-icon-info"></i>
|
||||||
<span>归档后可从"归档库"中查看和恢复</span>
|
<span>归档后项目进入只读查看状态,请确认后继续</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -77,8 +68,7 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
submitting: false,
|
submitting: false
|
||||||
deleteData: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -93,44 +83,21 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
|
||||||
visible(val) {
|
|
||||||
if (!val) {
|
|
||||||
this.deleteData = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
handleConfirm() {
|
handleConfirm() {
|
||||||
if (this.deleteData) {
|
this.doArchive()
|
||||||
this.$confirm('删除后数据将无法恢复,是否确认删除?', '警告', {
|
|
||||||
confirmButtonText: '确认删除',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning'
|
|
||||||
}).then(() => {
|
|
||||||
this.doArchive()
|
|
||||||
}).catch(() => {
|
|
||||||
// 取消删除,但仍归档
|
|
||||||
this.deleteData = false
|
|
||||||
this.doArchive()
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.doArchive()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
doArchive() {
|
doArchive() {
|
||||||
this.submitting = true
|
this.submitting = true
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.submitting = false
|
this.submitting = false
|
||||||
this.$emit('confirm', {
|
this.$emit('confirm', {
|
||||||
projectId: this.projectData.projectId,
|
projectId: this.projectData.projectId
|
||||||
deleteData: this.deleteData
|
|
||||||
})
|
})
|
||||||
}, 500)
|
}, 500)
|
||||||
},
|
},
|
||||||
handleClose() {
|
handleClose() {
|
||||||
this.$emit('close')
|
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 {
|
.archive-hint {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="param-config-container" v-loading="loading" element-loading-text="加载中...">
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- 模型参数卡片组(垂直堆叠) -->
|
<!-- 模型参数卡片组(垂直堆叠) -->
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="row.paramValue"
|
v-model="row.paramValue"
|
||||||
:disabled="isProjectTagging"
|
:disabled="isProjectTagging || isProjectArchived"
|
||||||
placeholder="请输入阈值"
|
placeholder="请输入阈值"
|
||||||
@input="markAsModified(model.modelCode, row)"
|
@input="markAsModified(model.modelCode, row)"
|
||||||
/>
|
/>
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
|
|
||||||
<!-- 统一保存按钮 -->
|
<!-- 统一保存按钮 -->
|
||||||
<div class="button-section" v-if="!loading && modelGroups.length > 0">
|
<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>
|
</el-button>
|
||||||
<span v-if="modifiedCount > 0" class="modified-tip">
|
<span v-if="modifiedCount > 0" class="modified-tip">
|
||||||
@@ -82,6 +82,9 @@ export default {
|
|||||||
},
|
},
|
||||||
isProjectTagging() {
|
isProjectTagging() {
|
||||||
return String(this.projectInfo.projectStatus) === "3";
|
return String(this.projectInfo.projectStatus) === "3";
|
||||||
|
},
|
||||||
|
isProjectArchived() {
|
||||||
|
return String(this.projectInfo.projectStatus) === "2";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -159,6 +162,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async handleSaveAll() {
|
async handleSaveAll() {
|
||||||
|
if (this.isProjectArchived) {
|
||||||
|
this.$message.warning("已归档项目暂不可修改参数");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.isProjectTagging) {
|
if (this.isProjectTagging) {
|
||||||
this.$message.warning("项目正在进行银行流水打标,参数暂不可修改");
|
this.$message.warning("项目正在进行银行流水打标,参数暂不可修改");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<el-button
|
<el-button
|
||||||
size="small"
|
size="small"
|
||||||
icon="el-icon-download"
|
icon="el-icon-download"
|
||||||
:disabled="isProjectTagging"
|
:disabled="isProjectTagging || isProjectArchived"
|
||||||
@click="handleFetchBankInfo"
|
@click="handleFetchBankInfo"
|
||||||
>
|
>
|
||||||
拉取本行信息
|
拉取本行信息
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
<el-button
|
<el-button
|
||||||
size="small"
|
size="small"
|
||||||
icon="el-icon-upload2"
|
icon="el-icon-upload2"
|
||||||
:disabled="isProjectTagging"
|
:disabled="isProjectTagging || isProjectArchived"
|
||||||
@click="handleGoCreditInfoPage"
|
@click="handleGoCreditInfoPage"
|
||||||
>
|
>
|
||||||
征信导入
|
征信导入
|
||||||
@@ -34,8 +34,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="isProjectTagging" class="tagging-lock-tip">
|
<div v-if="isProjectTagging || isProjectArchived" class="tagging-lock-tip">
|
||||||
项目正在进行银行流水打标,暂不可上传或拉取数据。
|
{{ isProjectArchived ? "项目已归档,暂不可上传或拉取数据。" : "项目正在进行银行流水打标,暂不可上传或拉取数据。" }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 上传模块 -->
|
<!-- 上传模块 -->
|
||||||
@@ -183,7 +183,7 @@
|
|||||||
v-model="pullBankInfoForm.idCardText"
|
v-model="pullBankInfoForm.idCardText"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:rows="5"
|
:rows="5"
|
||||||
:disabled="isProjectTagging"
|
:disabled="isProjectTagging || isProjectArchived"
|
||||||
placeholder="支持逗号、中文逗号、换行分隔"
|
placeholder="支持逗号、中文逗号、换行分隔"
|
||||||
/>
|
/>
|
||||||
<div class="pull-bank-field-tip">
|
<div class="pull-bank-field-tip">
|
||||||
@@ -201,7 +201,7 @@
|
|||||||
:show-file-list="false"
|
:show-file-list="false"
|
||||||
:file-list="idCardFileList"
|
:file-list="idCardFileList"
|
||||||
accept=".xls,.xlsx"
|
accept=".xls,.xlsx"
|
||||||
:disabled="isProjectTagging"
|
:disabled="isProjectTagging || isProjectArchived"
|
||||||
:on-change="handleIdCardFileChange"
|
:on-change="handleIdCardFileChange"
|
||||||
:on-remove="handleIdCardFileRemove"
|
:on-remove="handleIdCardFileRemove"
|
||||||
>
|
>
|
||||||
@@ -233,7 +233,7 @@
|
|||||||
<el-date-picker
|
<el-date-picker
|
||||||
class="pull-bank-range-picker"
|
class="pull-bank-range-picker"
|
||||||
v-model="pullBankInfoForm.dateRange"
|
v-model="pullBankInfoForm.dateRange"
|
||||||
:disabled="isProjectTagging"
|
:disabled="isProjectTagging || isProjectArchived"
|
||||||
type="daterange"
|
type="daterange"
|
||||||
value-format="yyyy-MM-dd"
|
value-format="yyyy-MM-dd"
|
||||||
:picker-options="pullBankInfoDatePickerOptions"
|
:picker-options="pullBankInfoDatePickerOptions"
|
||||||
@@ -247,7 +247,7 @@
|
|||||||
<el-button @click="pullBankInfoDialogVisible = false">取消</el-button>
|
<el-button @click="pullBankInfoDialogVisible = false">取消</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
:disabled="isProjectTagging"
|
:disabled="isProjectTagging || isProjectArchived"
|
||||||
:loading="pullBankInfoLoading"
|
:loading="pullBankInfoLoading"
|
||||||
@click="handleConfirmPullBankInfo"
|
@click="handleConfirmPullBankInfo"
|
||||||
>
|
>
|
||||||
@@ -268,7 +268,7 @@
|
|||||||
drag
|
drag
|
||||||
action="#"
|
action="#"
|
||||||
multiple
|
multiple
|
||||||
:disabled="isProjectTagging"
|
:disabled="isProjectTagging || isProjectArchived"
|
||||||
:auto-upload="false"
|
:auto-upload="false"
|
||||||
:on-change="handleBatchFileChange"
|
:on-change="handleBatchFileChange"
|
||||||
:show-file-list="false"
|
:show-file-list="false"
|
||||||
@@ -307,7 +307,7 @@
|
|||||||
<el-button @click="batchUploadDialogVisible = false">取消</el-button>
|
<el-button @click="batchUploadDialogVisible = false">取消</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
:disabled="isProjectTagging || selectedFiles.length === 0"
|
:disabled="isProjectTagging || isProjectArchived || selectedFiles.length === 0"
|
||||||
:loading="uploadLoading"
|
:loading="uploadLoading"
|
||||||
@click="handleBatchUpload"
|
@click="handleBatchUpload"
|
||||||
>开始上传</el-button
|
>开始上传</el-button
|
||||||
@@ -450,6 +450,9 @@ export default {
|
|||||||
isProjectTagging() {
|
isProjectTagging() {
|
||||||
return String(this.projectInfo.projectStatus) === "3";
|
return String(this.projectInfo.projectStatus) === "3";
|
||||||
},
|
},
|
||||||
|
isProjectArchived() {
|
||||||
|
return String(this.projectInfo.projectStatus) === "2";
|
||||||
|
},
|
||||||
isReportDisabled() {
|
isReportDisabled() {
|
||||||
return ["0", "3"].includes(String(this.projectInfo.projectStatus));
|
return ["0", "3"].includes(String(this.projectInfo.projectStatus));
|
||||||
},
|
},
|
||||||
@@ -531,7 +534,7 @@ export default {
|
|||||||
if (card.key === "transaction") {
|
if (card.key === "transaction") {
|
||||||
return {
|
return {
|
||||||
...card,
|
...card,
|
||||||
disabled: this.isProjectTagging,
|
disabled: this.isProjectTagging || this.isProjectArchived,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return card;
|
return card;
|
||||||
@@ -580,7 +583,7 @@ export default {
|
|||||||
this.$emit("menu-change", { key: "overview", route: "overview" });
|
this.$emit("menu-change", { key: "overview", route: "overview" });
|
||||||
},
|
},
|
||||||
handleGoCreditInfoPage() {
|
handleGoCreditInfoPage() {
|
||||||
if (this.isProjectTagging) {
|
if (this.isProjectTagging || this.isProjectArchived) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.$router.push("/maintain/creditInfo");
|
this.$router.push("/maintain/creditInfo");
|
||||||
@@ -636,6 +639,9 @@ export default {
|
|||||||
return Array.from(new Set(merged)).join(", ");
|
return Array.from(new Set(merged)).join(", ");
|
||||||
},
|
},
|
||||||
async handleIdCardFileChange(file, fileList) {
|
async handleIdCardFileChange(file, fileList) {
|
||||||
|
if (this.isProjectTagging || this.isProjectArchived) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const latestFile = (fileList || []).slice(-1);
|
const latestFile = (fileList || []).slice(-1);
|
||||||
const currentFile = latestFile[0] || file;
|
const currentFile = latestFile[0] || file;
|
||||||
const fileName = (currentFile && currentFile.name) || "";
|
const fileName = (currentFile && currentFile.name) || "";
|
||||||
@@ -716,8 +722,12 @@ export default {
|
|||||||
return this.parseIdCardText(this.pullBankInfoForm.idCardText);
|
return this.parseIdCardText(this.pullBankInfoForm.idCardText);
|
||||||
},
|
},
|
||||||
async handleConfirmPullBankInfo() {
|
async handleConfirmPullBankInfo() {
|
||||||
if (this.isProjectTagging) {
|
if (this.isProjectTagging || this.isProjectArchived) {
|
||||||
this.$message.warning("项目正在进行银行流水打标,暂不可上传或拉取数据");
|
this.$message.warning(
|
||||||
|
this.isProjectArchived
|
||||||
|
? "项目已归档,暂不可上传或拉取数据"
|
||||||
|
: "项目正在进行银行流水打标,暂不可上传或拉取数据"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const idCards = this.buildFinalIdCardList();
|
const idCards = this.buildFinalIdCardList();
|
||||||
@@ -776,8 +786,12 @@ export default {
|
|||||||
},
|
},
|
||||||
/** 拉取本行信息 */
|
/** 拉取本行信息 */
|
||||||
handleFetchBankInfo() {
|
handleFetchBankInfo() {
|
||||||
if (this.isProjectTagging) {
|
if (this.isProjectTagging || this.isProjectArchived) {
|
||||||
this.$message.warning("项目正在进行银行流水打标,暂不可上传或拉取数据");
|
this.$message.warning(
|
||||||
|
this.isProjectArchived
|
||||||
|
? "项目已归档,暂不可上传或拉取数据"
|
||||||
|
: "项目正在进行银行流水打标,暂不可上传或拉取数据"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.resetPullBankInfoForm();
|
this.resetPullBankInfoForm();
|
||||||
@@ -868,8 +882,12 @@ export default {
|
|||||||
|
|
||||||
/** 开始批量上传 */
|
/** 开始批量上传 */
|
||||||
async handleBatchUpload() {
|
async handleBatchUpload() {
|
||||||
if (this.isProjectTagging) {
|
if (this.isProjectTagging || this.isProjectArchived) {
|
||||||
this.$message.warning("项目正在进行银行流水打标,暂不可上传或拉取数据");
|
this.$message.warning(
|
||||||
|
this.isProjectArchived
|
||||||
|
? "项目已归档,暂不可上传或拉取数据"
|
||||||
|
: "项目正在进行银行流水打标,暂不可上传或拉取数据"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.selectedFiles.length === 0) {
|
if (this.selectedFiles.length === 0) {
|
||||||
|
|||||||
@@ -39,8 +39,8 @@
|
|||||||
@select="handleMenuSelect"
|
@select="handleMenuSelect"
|
||||||
class="nav-menu"
|
class="nav-menu"
|
||||||
>
|
>
|
||||||
<el-menu-item index="upload">上传数据</el-menu-item>
|
<el-menu-item index="upload" :disabled="isArchiveLockedTab('upload')">上传数据</el-menu-item>
|
||||||
<el-menu-item index="config">参数配置</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="overview">结果总览</el-menu-item>
|
||||||
<el-menu-item index="special">专项排查</el-menu-item>
|
<el-menu-item index="special">专项排查</el-menu-item>
|
||||||
<el-menu-item index="detail">流水明细查询</el-menu-item>
|
<el-menu-item index="detail">流水明细查询</el-menu-item>
|
||||||
@@ -107,6 +107,11 @@ export default {
|
|||||||
projectStatusPollingLoading: false,
|
projectStatusPollingLoading: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
isProjectArchived() {
|
||||||
|
return String(this.projectInfo.projectStatus) === "2";
|
||||||
|
},
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
"$route.params.projectId"(newId) {
|
"$route.params.projectId"(newId) {
|
||||||
this.stopProjectStatusPolling();
|
this.stopProjectStatusPolling();
|
||||||
@@ -123,6 +128,10 @@ export default {
|
|||||||
},
|
},
|
||||||
"projectInfo.projectStatus"() {
|
"projectInfo.projectStatus"() {
|
||||||
this.syncProjectStatusPolling();
|
this.syncProjectStatusPolling();
|
||||||
|
const accessibleTab = this.resolveAccessibleTab(this.activeTab);
|
||||||
|
if (accessibleTab !== this.activeTab) {
|
||||||
|
this.setActiveTab(accessibleTab);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
@@ -138,7 +147,16 @@ export default {
|
|||||||
const tab = (this.$route.query && this.$route.query.tab) || "";
|
const tab = (this.$route.query && this.$route.query.tab) || "";
|
||||||
const validTabs = ["upload", "config", "overview", "special", "detail"];
|
const validTabs = ["upload", "config", "overview", "special", "detail"];
|
||||||
const targetTab = validTabs.includes(tab) ? tab : "upload";
|
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) {
|
setActiveTab(index) {
|
||||||
this.activeTab = index;
|
this.activeTab = index;
|
||||||
@@ -319,6 +337,9 @@ export default {
|
|||||||
},
|
},
|
||||||
/** 菜单选择事件 */
|
/** 菜单选择事件 */
|
||||||
handleMenuSelect(index) {
|
handleMenuSelect(index) {
|
||||||
|
if (this.isArchiveLockedTab(index)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.log("菜单选择:", index);
|
console.log("菜单选择:", index);
|
||||||
this.setActiveTab(index);
|
this.setActiveTab(index);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {getStatusCounts, listProject, rebuildProjectTags} from '@/api/ccdiProject'
|
import {archiveProject, getStatusCounts, listProject, rebuildProjectTags} from '@/api/ccdiProject'
|
||||||
import SearchBar from './components/SearchBar'
|
import SearchBar from './components/SearchBar'
|
||||||
import ProjectTable from './components/ProjectTable'
|
import ProjectTable from './components/ProjectTable'
|
||||||
import QuickEntry from './components/QuickEntry'
|
import QuickEntry from './components/QuickEntry'
|
||||||
@@ -268,11 +268,17 @@ export default {
|
|||||||
this.archiveDialogVisible = true;
|
this.archiveDialogVisible = true;
|
||||||
},
|
},
|
||||||
/** 确认归档 */
|
/** 确认归档 */
|
||||||
handleConfirmArchive(data) {
|
async handleConfirmArchive(data) {
|
||||||
console.log("确认归档:", data);
|
try {
|
||||||
this.$modal.msgSuccess("项目已归档");
|
await archiveProject(data.projectId)
|
||||||
this.archiveDialogVisible = false;
|
this.$modal.msgSuccess("项目归档成功")
|
||||||
this.getList();
|
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(
|
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
|
source
|
||||||
),
|
),
|
||||||
"流水导入卡片应在项目打标中时同步置灰"
|
"流水导入卡片应在项目打标中或已归档时同步置灰"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert(
|
assert(
|
||||||
|
|||||||
Reference in New Issue
Block a user