实现流程项目逻辑删除与恢复
This commit is contained in:
@@ -10,6 +10,7 @@ public final class CcdiProjectStatusConstants {
|
||||
public static final String ARCHIVED = "2";
|
||||
public static final String TAGGING = "3";
|
||||
public static final String TAG_FAILED = "4";
|
||||
public static final String DELETED = "5";
|
||||
|
||||
private CcdiProjectStatusConstants() {
|
||||
}
|
||||
|
||||
@@ -74,12 +74,23 @@ public class CcdiProjectController extends BaseController {
|
||||
*/
|
||||
@DeleteMapping("/{projectId}")
|
||||
@Operation(summary = "删除项目")
|
||||
@PreAuthorize("@ss.hasPermi('ccdi:project:remove')")
|
||||
@PreAuthorize("@ss.hasPermi('ccdi:project:list')")
|
||||
public AjaxResult deleteProject(@PathVariable Long projectId) {
|
||||
boolean success = projectService.deleteProject(projectId);
|
||||
boolean success = projectService.deleteProject(projectId, SecurityUtils.getUsername());
|
||||
return success ? AjaxResult.success("项目删除成功") : AjaxResult.error("项目删除失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复项目
|
||||
*/
|
||||
@PostMapping("/{projectId}/restore")
|
||||
@Operation(summary = "恢复项目")
|
||||
@PreAuthorize("@ss.hasPermi('ccdi:project:edit')")
|
||||
public AjaxResult restoreProject(@PathVariable Long projectId) {
|
||||
projectService.restoreProject(projectId, SecurityUtils.getUsername());
|
||||
return AjaxResult.success("项目恢复成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询项目详情
|
||||
*/
|
||||
|
||||
@@ -32,7 +32,7 @@ public class CcdiProject implements Serializable {
|
||||
/** 配置方式:default-全局默认,custom-自定义 */
|
||||
private String configType;
|
||||
|
||||
/** 项目状态:0-进行中,1-已完成,2-已归档,3-打标中,4-打标失败 */
|
||||
/** 项目状态:0-进行中,1-已完成,2-已归档,3-打标中,4-打标失败,5-已删除 */
|
||||
private String status;
|
||||
|
||||
/** 是否归档:0-未归档,1-已归档 */
|
||||
@@ -54,7 +54,7 @@ public class CcdiProject implements Serializable {
|
||||
private Integer lsfxProjectId;
|
||||
|
||||
/** 删除标志:0-存在,2-删除 */
|
||||
@TableLogic
|
||||
@TableLogic(value = "0", delval = "2")
|
||||
private String delFlag;
|
||||
|
||||
/** 创建者 */
|
||||
|
||||
@@ -14,4 +14,7 @@ public class CcdiProjectQueryDTO {
|
||||
|
||||
/** 项目状态 */
|
||||
private String status;
|
||||
|
||||
/** 是否查询已删除项目列表 */
|
||||
private Boolean includeDeleted;
|
||||
}
|
||||
|
||||
@@ -26,4 +26,7 @@ public class CcdiProjectStatusCountsVO {
|
||||
|
||||
/** 打标失败项目数(状态4) */
|
||||
private Long status4;
|
||||
|
||||
/** 已删除项目数(状态5) */
|
||||
private Long status5;
|
||||
}
|
||||
|
||||
@@ -67,4 +67,7 @@ public class CcdiProjectVO {
|
||||
|
||||
/** 当前用户是否可操作 */
|
||||
private Boolean canOperate;
|
||||
|
||||
/** 当前用户是否可删除 */
|
||||
private Boolean canDelete;
|
||||
}
|
||||
|
||||
@@ -39,6 +39,32 @@ public interface CcdiProjectMapper extends BaseMapper<CcdiProject> {
|
||||
List<CcdiProjectHistoryListItemVO> selectHistoryProjects(@Param("queryDTO") CcdiProjectQueryDTO queryDTO,
|
||||
@Param("scope") ProjectAccessScope scope);
|
||||
|
||||
/**
|
||||
* 业务逻辑删除项目,仅更新项目主表状态和删除标记。
|
||||
*
|
||||
* @param projectId 项目ID
|
||||
* @param operator 操作人
|
||||
* @return 更新行数
|
||||
*/
|
||||
int markProjectDeleted(@Param("projectId") Long projectId, @Param("operator") String operator);
|
||||
|
||||
/**
|
||||
* 恢复已删除项目为已完成。
|
||||
*
|
||||
* @param projectId 项目ID
|
||||
* @param operator 操作人
|
||||
* @return 更新行数
|
||||
*/
|
||||
int restoreDeletedProject(@Param("projectId") Long projectId, @Param("operator") String operator);
|
||||
|
||||
/**
|
||||
* 统计已删除项目数量。
|
||||
*
|
||||
* @param scope 项目访问范围
|
||||
* @return 已删除项目数量
|
||||
*/
|
||||
Long selectDeletedProjectCount(@Param("scope") ProjectAccessScope scope);
|
||||
|
||||
/**
|
||||
* 更新项目总人数与风险人数
|
||||
*
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.ruoyi.ccdi.project.service;
|
||||
|
||||
import com.ruoyi.ccdi.project.domain.CcdiProject;
|
||||
import com.ruoyi.ccdi.project.domain.ProjectAccessScope;
|
||||
import com.ruoyi.ccdi.project.constants.CcdiProjectStatusConstants;
|
||||
import com.ruoyi.ccdi.project.domain.entity.CcdiBankStatement;
|
||||
import com.ruoyi.ccdi.project.domain.entity.CcdiEvidence;
|
||||
import com.ruoyi.ccdi.project.domain.entity.CcdiFileUploadRecord;
|
||||
@@ -58,8 +59,17 @@ public class CcdiProjectAccessService {
|
||||
return scope.isSuperAdmin() || Objects.equals(scope.getUsername(), project.getCreateBy());
|
||||
}
|
||||
|
||||
public boolean canDelete(CcdiProject project) {
|
||||
if (project == null) {
|
||||
return false;
|
||||
}
|
||||
ProjectAccessScope scope = buildCurrentScope();
|
||||
return isProjectAdmin(scope) || Objects.equals(scope.getUsername(), project.getCreateBy());
|
||||
}
|
||||
|
||||
public void assertCanRead(Long projectId) {
|
||||
CcdiProject project = getRequiredProject(projectId);
|
||||
assertProjectNotDeleted(project);
|
||||
ProjectAccessScope scope = buildCurrentScope();
|
||||
if (scope.isViewAllProjects() || Objects.equals(scope.getUsername(), project.getCreateBy())) {
|
||||
return;
|
||||
@@ -69,12 +79,29 @@ public class CcdiProjectAccessService {
|
||||
|
||||
public void assertCanOperate(Long projectId) {
|
||||
CcdiProject project = getRequiredProject(projectId);
|
||||
assertProjectNotDeleted(project);
|
||||
if (canOperate(project)) {
|
||||
return;
|
||||
}
|
||||
throw new ServiceException("无权操作该项目");
|
||||
}
|
||||
|
||||
public void assertCanDelete(Long projectId) {
|
||||
CcdiProject project = getRequiredProject(projectId);
|
||||
assertProjectNotDeleted(project);
|
||||
if (canDelete(project)) {
|
||||
return;
|
||||
}
|
||||
throw new ServiceException("无权删除该项目");
|
||||
}
|
||||
|
||||
public void assertCanManageDeletedProjects() {
|
||||
if (isProjectAdmin(buildCurrentScope())) {
|
||||
return;
|
||||
}
|
||||
throw new ServiceException("无权管理已删除项目");
|
||||
}
|
||||
|
||||
public void assertCanReadByBankStatementId(Long bankStatementId) {
|
||||
CcdiBankStatement statement = getRequiredBankStatement(bankStatementId);
|
||||
assertCanRead(statement.getProjectId());
|
||||
@@ -115,6 +142,19 @@ public class CcdiProjectAccessService {
|
||||
return project;
|
||||
}
|
||||
|
||||
private void assertProjectNotDeleted(CcdiProject project) {
|
||||
if (project == null) {
|
||||
return;
|
||||
}
|
||||
if (CcdiProjectStatusConstants.DELETED.equals(project.getStatus()) || "2".equals(project.getDelFlag())) {
|
||||
throw new ServiceException("项目已删除");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isProjectAdmin(ProjectAccessScope scope) {
|
||||
return scope != null && (scope.isSuperAdmin() || scope.isProjectManager());
|
||||
}
|
||||
|
||||
private CcdiBankStatement getRequiredBankStatement(Long bankStatementId) {
|
||||
if (bankStatementId == null) {
|
||||
throw new ServiceException("流水ID不能为空");
|
||||
|
||||
@@ -36,9 +36,18 @@ public interface ICcdiProjectService {
|
||||
* 删除项目
|
||||
*
|
||||
* @param projectId 项目ID
|
||||
* @param operator 操作人
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean deleteProject(Long projectId);
|
||||
boolean deleteProject(Long projectId, String operator);
|
||||
|
||||
/**
|
||||
* 恢复已删除项目
|
||||
*
|
||||
* @param projectId 项目ID
|
||||
* @param operator 操作人
|
||||
*/
|
||||
void restoreProject(Long projectId, String operator);
|
||||
|
||||
/**
|
||||
* 查询项目详情
|
||||
|
||||
@@ -117,9 +117,22 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteProject(Long projectId) {
|
||||
projectAccessService.assertCanOperate(projectId);
|
||||
return projectMapper.deleteById(projectId) > 0;
|
||||
public boolean deleteProject(Long projectId, String operator) {
|
||||
projectAccessService.assertCanDelete(projectId);
|
||||
return projectMapper.markProjectDeleted(projectId, resolveOperator(operator)) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restoreProject(Long projectId, String operator) {
|
||||
projectAccessService.assertCanManageDeletedProjects();
|
||||
int rows = projectMapper.restoreDeletedProject(projectId, resolveOperator(operator));
|
||||
if (rows <= 0) {
|
||||
throw new ServiceException("仅已删除项目允许恢复");
|
||||
}
|
||||
log.info("【项目】项目状态变更: projectId={}, oldStatus={}, oldStatusLabel={}, newStatus={}, newStatusLabel={}, operator={}",
|
||||
projectId, CcdiProjectStatusConstants.DELETED, resolveStatusLabel(CcdiProjectStatusConstants.DELETED),
|
||||
CcdiProjectStatusConstants.COMPLETED, resolveStatusLabel(CcdiProjectStatusConstants.COMPLETED),
|
||||
resolveOperator(operator));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -201,6 +214,12 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
|
||||
.eq(CcdiProject::getStatus, CcdiProjectStatusConstants.TAG_FAILED));
|
||||
vo.setStatus4(status4Count);
|
||||
|
||||
Long status5Count = 0L;
|
||||
if (isProjectAdmin(scope)) {
|
||||
status5Count = projectMapper.selectDeletedProjectCount(scope);
|
||||
}
|
||||
vo.setStatus5(status5Count);
|
||||
|
||||
return vo;
|
||||
}
|
||||
|
||||
@@ -229,6 +248,9 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
|
||||
public void updateProjectStatus(Long projectId, String status, String operator) {
|
||||
CcdiProject project = getRequiredProject(projectId);
|
||||
String oldStatus = project.getStatus();
|
||||
if (CcdiProjectStatusConstants.DELETED.equals(project.getStatus())) {
|
||||
throw new ServiceException("已删除项目不允许更新状态");
|
||||
}
|
||||
if (CcdiProjectStatusConstants.ARCHIVED.equals(project.getStatus())
|
||||
&& !CcdiProjectStatusConstants.ARCHIVED.equals(status)) {
|
||||
throw new ServiceException("已归档项目不允许重新进入打标流程");
|
||||
@@ -247,6 +269,9 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
|
||||
@Override
|
||||
public void ensureProjectCanStartTagging(Long projectId) {
|
||||
CcdiProject project = getRequiredProject(projectId);
|
||||
if (CcdiProjectStatusConstants.DELETED.equals(project.getStatus())) {
|
||||
throw new ServiceException("已删除项目不允许重新进入打标流程");
|
||||
}
|
||||
if (CcdiProjectStatusConstants.ARCHIVED.equals(project.getStatus())) {
|
||||
throw new ServiceException("已归档项目不允许重新进入打标流程");
|
||||
}
|
||||
@@ -255,6 +280,9 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
|
||||
@Override
|
||||
public void ensureProjectNotArchived(Long projectId, String message) {
|
||||
CcdiProject project = getRequiredProject(projectId);
|
||||
if (CcdiProjectStatusConstants.DELETED.equals(project.getStatus())) {
|
||||
throw new ServiceException("已删除项目暂不允许操作");
|
||||
}
|
||||
if (CcdiProjectStatusConstants.ARCHIVED.equals(project.getStatus())) {
|
||||
throw new ServiceException(message);
|
||||
}
|
||||
@@ -263,6 +291,9 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
|
||||
@Override
|
||||
public void ensureProjectWritable(Long projectId, String message) {
|
||||
CcdiProject project = getRequiredProject(projectId);
|
||||
if (CcdiProjectStatusConstants.DELETED.equals(project.getStatus())) {
|
||||
throw new ServiceException("已删除项目暂不允许操作");
|
||||
}
|
||||
if (CcdiProjectStatusConstants.TAGGING.equals(project.getStatus())) {
|
||||
throw new ServiceException(message);
|
||||
}
|
||||
@@ -283,6 +314,7 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
|
||||
case CcdiProjectStatusConstants.ARCHIVED -> "已归档";
|
||||
case CcdiProjectStatusConstants.TAGGING -> "打标中";
|
||||
case CcdiProjectStatusConstants.TAG_FAILED -> "打标失败";
|
||||
case CcdiProjectStatusConstants.DELETED -> "已删除";
|
||||
default -> "未知";
|
||||
};
|
||||
}
|
||||
@@ -301,12 +333,17 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
|
||||
|
||||
private LambdaQueryWrapper<CcdiProject> buildScopeWrapper(ProjectAccessScope scope) {
|
||||
LambdaQueryWrapper<CcdiProject> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.ne(CcdiProject::getStatus, CcdiProjectStatusConstants.DELETED);
|
||||
if (scope != null && !scope.isViewAllProjects()) {
|
||||
wrapper.eq(CcdiProject::getCreateBy, scope.getUsername());
|
||||
}
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private boolean isProjectAdmin(ProjectAccessScope scope) {
|
||||
return scope != null && (scope.isSuperAdmin() || scope.isProjectManager());
|
||||
}
|
||||
|
||||
private void fillProjectExtraFields(List<CcdiProjectVO> records) {
|
||||
if (records == null || records.isEmpty()) {
|
||||
return;
|
||||
@@ -329,6 +366,7 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
|
||||
boolean ownedByCurrentUser = Objects.equals(scope.getUsername(), vo.getCreateBy());
|
||||
vo.setOwnedByCurrentUser(ownedByCurrentUser);
|
||||
vo.setCanOperate(scope.isSuperAdmin() || ownedByCurrentUser);
|
||||
vo.setCanDelete(isProjectAdmin(scope) || ownedByCurrentUser);
|
||||
}
|
||||
|
||||
private String resolveOperator(String operator) {
|
||||
|
||||
@@ -40,13 +40,23 @@
|
||||
FROM ccdi_project p
|
||||
LEFT JOIN sys_user u ON p.create_by = u.user_name AND u.del_flag = '0'
|
||||
<where>
|
||||
<choose>
|
||||
<when test="queryDTO != null and queryDTO.includeDeleted == true and scope != null and scope.viewAllProjects">
|
||||
AND p.del_flag = '2'
|
||||
AND p.status = '5'
|
||||
</when>
|
||||
<otherwise>
|
||||
AND p.del_flag = '0'
|
||||
AND p.status != '5'
|
||||
</otherwise>
|
||||
</choose>
|
||||
<if test="scope != null and !scope.viewAllProjects">
|
||||
AND p.create_by = #{scope.username}
|
||||
</if>
|
||||
<if test="queryDTO.projectName != null and queryDTO.projectName != ''">
|
||||
AND p.project_name LIKE CONCAT('%', #{queryDTO.projectName}, '%')
|
||||
</if>
|
||||
<if test="queryDTO.status != null and queryDTO.status != ''">
|
||||
<if test="!(queryDTO != null and queryDTO.includeDeleted == true) and queryDTO.status != null and queryDTO.status != ''">
|
||||
AND p.status = #{queryDTO.status}
|
||||
</if>
|
||||
</where>
|
||||
@@ -64,6 +74,7 @@
|
||||
FROM ccdi_project p
|
||||
<where>
|
||||
p.status in ('1', '2')
|
||||
AND p.del_flag = '0'
|
||||
<if test="scope != null and !scope.viewAllProjects">
|
||||
AND p.create_by = #{scope.username}
|
||||
</if>
|
||||
@@ -74,6 +85,41 @@
|
||||
ORDER BY p.update_time DESC
|
||||
</select>
|
||||
|
||||
<update id="markProjectDeleted">
|
||||
update ccdi_project
|
||||
set status = '5',
|
||||
del_flag = '2',
|
||||
update_by = #{operator},
|
||||
update_time = now()
|
||||
where project_id = #{projectId}
|
||||
and del_flag = '0'
|
||||
and status != '5'
|
||||
</update>
|
||||
|
||||
<update id="restoreDeletedProject">
|
||||
update ccdi_project
|
||||
set status = '1',
|
||||
del_flag = '0',
|
||||
is_archived = 0,
|
||||
update_by = #{operator},
|
||||
update_time = now()
|
||||
where project_id = #{projectId}
|
||||
and del_flag = '2'
|
||||
and status = '5'
|
||||
</update>
|
||||
|
||||
<select id="selectDeletedProjectCount" resultType="java.lang.Long">
|
||||
select count(1)
|
||||
from ccdi_project p
|
||||
<where>
|
||||
p.del_flag = '2'
|
||||
and p.status = '5'
|
||||
<if test="scope != null and !scope.viewAllProjects">
|
||||
AND p.create_by = #{scope.username}
|
||||
</if>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
<update id="updateRiskCountsByProjectId">
|
||||
update ccdi_project
|
||||
set target_count = #{targetCount},
|
||||
|
||||
@@ -1,28 +1,20 @@
|
||||
package com.ruoyi.ccdi.project.controller;
|
||||
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectImportHistoryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectVO;
|
||||
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.DeleteMapping;
|
||||
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.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CcdiProjectControllerTest {
|
||||
@@ -35,16 +27,6 @@ class CcdiProjectControllerTest {
|
||||
|
||||
@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);
|
||||
@@ -59,24 +41,32 @@ class CcdiProjectControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldImportFromHistoryAndReturnCreatedProject() {
|
||||
CcdiProjectImportHistoryDTO dto = new CcdiProjectImportHistoryDTO();
|
||||
dto.setProjectName("新建项目");
|
||||
void shouldRestoreProject() throws Exception {
|
||||
Method method = CcdiProjectController.class.getMethod("restoreProject", Long.class);
|
||||
PostMapping postMapping = method.getAnnotation(PostMapping.class);
|
||||
PreAuthorize preAuthorize = method.getAnnotation(PreAuthorize.class);
|
||||
Operation operation = method.getAnnotation(Operation.class);
|
||||
|
||||
CcdiProjectVO project = new CcdiProjectVO();
|
||||
project.setProjectId(88L);
|
||||
project.setProjectName("新建项目");
|
||||
when(projectService.importFromHistory(eq(dto), eq("tester"))).thenReturn(project);
|
||||
assertNotNull(postMapping);
|
||||
assertEquals("/{projectId}/restore", postMapping.value()[0]);
|
||||
assertNotNull(preAuthorize);
|
||||
assertEquals("@ss.hasPermi('ccdi:project:edit')", preAuthorize.value());
|
||||
assertNotNull(operation);
|
||||
assertEquals("恢复项目", operation.summary());
|
||||
}
|
||||
|
||||
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");
|
||||
@Test
|
||||
void shouldAllowProjectListUsersToReachBusinessDeleteCheck() throws Exception {
|
||||
Method method = CcdiProjectController.class.getMethod("deleteProject", Long.class);
|
||||
DeleteMapping deleteMapping = method.getAnnotation(DeleteMapping.class);
|
||||
PreAuthorize preAuthorize = method.getAnnotation(PreAuthorize.class);
|
||||
Operation operation = method.getAnnotation(Operation.class);
|
||||
|
||||
AjaxResult result = controller.importFromHistory(dto);
|
||||
|
||||
assertEquals(200, result.get("code"));
|
||||
assertEquals("项目创建成功", result.get("msg"));
|
||||
assertSame(project, result.get("data"));
|
||||
verify(projectService).importFromHistory(dto, "tester");
|
||||
}
|
||||
assertNotNull(deleteMapping);
|
||||
assertEquals("/{projectId}", deleteMapping.value()[0]);
|
||||
assertNotNull(preAuthorize);
|
||||
assertEquals("@ss.hasPermi('ccdi:project:list')", preAuthorize.value());
|
||||
assertNotNull(operation);
|
||||
assertEquals("删除项目", operation.summary());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import ch.qos.logback.classic.Logger;
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||
import ch.qos.logback.core.read.ListAppender;
|
||||
import com.ruoyi.ccdi.project.domain.CcdiProject;
|
||||
import com.ruoyi.ccdi.project.domain.ProjectAccessScope;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectImportHistoryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSaveDTO;
|
||||
@@ -83,6 +84,48 @@ class CcdiProjectServiceImplTest {
|
||||
assertEquals(5L, counts.getStatus4());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCountDeletedProjectsSeparatelyForManagerScope() {
|
||||
when(projectAccessService.buildCurrentScope()).thenReturn(
|
||||
new ProjectAccessScope("manager", true, false, true)
|
||||
);
|
||||
when(projectMapper.selectCount(any())).thenReturn(10L, 3L, 4L, 2L, 1L, 5L);
|
||||
when(projectMapper.selectDeletedProjectCount(any())).thenReturn(6L);
|
||||
|
||||
CcdiProjectStatusCountsVO counts = service.getStatusCounts();
|
||||
|
||||
assertEquals(6L, counts.getStatus5());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMarkProjectDeletedWithoutCallingBaseDelete() {
|
||||
when(projectMapper.markProjectDeleted(40L, "tester")).thenReturn(1);
|
||||
|
||||
boolean result = service.deleteProject(40L, "tester");
|
||||
|
||||
assertTrue(result);
|
||||
verify(projectAccessService).assertCanDelete(40L);
|
||||
verify(projectMapper).markProjectDeleted(40L, "tester");
|
||||
verify(projectMapper, never()).deleteById(40L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRestoreDeletedProjectToCompleted() {
|
||||
when(projectMapper.restoreDeletedProject(40L, "tester")).thenReturn(1);
|
||||
|
||||
service.restoreProject(40L, "tester");
|
||||
|
||||
verify(projectAccessService).assertCanManageDeletedProjects();
|
||||
verify(projectMapper).restoreDeletedProject(40L, "tester");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectRestoreWhenProjectIsNotDeleted() {
|
||||
when(projectMapper.restoreDeletedProject(41L, "tester")).thenReturn(0);
|
||||
|
||||
assertThrows(ServiceException.class, () -> service.restoreProject(41L, "tester"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnLatestFailedTagTaskOnFailedProjectDetail() {
|
||||
Date endTime = new Date();
|
||||
@@ -127,6 +170,19 @@ class CcdiProjectServiceImplTest {
|
||||
() -> service.updateProjectStatus(99L, "3", "system"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectUpdatingDeletedProjectToTaggingOrCompleted() {
|
||||
CcdiProject deleted = new CcdiProject();
|
||||
deleted.setProjectId(99L);
|
||||
deleted.setStatus("5");
|
||||
when(projectMapper.selectById(99L)).thenReturn(deleted);
|
||||
|
||||
assertThrows(ServiceException.class,
|
||||
() -> service.updateProjectStatus(99L, "3", "system"));
|
||||
assertThrows(ServiceException.class,
|
||||
() -> service.updateProjectStatus(99L, "1", "system"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectWritingWhenProjectIsTagging() {
|
||||
CcdiProject tagging = new CcdiProject();
|
||||
|
||||
@@ -18,6 +18,8 @@ class CcdiProjectStatusSqlTest {
|
||||
String migrationSql = Files.readString(repoRoot.resolve("sql/migration/2026-03-18-add-project-tagging-status.sql"));
|
||||
String tagFailedMigrationSql =
|
||||
Files.readString(repoRoot.resolve("sql/migration/2026-05-27-add-project-tag-failed-status.sql"));
|
||||
String deletedMigrationSql =
|
||||
Files.readString(repoRoot.resolve("sql/migration/2026-07-02-add-project-deleted-status.sql"));
|
||||
|
||||
assertTrue(initSql.contains("打标中"));
|
||||
assertTrue(initSql.contains("'3'"));
|
||||
@@ -34,5 +36,14 @@ class CcdiProjectStatusSqlTest {
|
||||
assertTrue(tagFailedMigrationSql.contains("'4'"));
|
||||
assertTrue(tagFailedMigrationSql.contains("latest_task.status = 'FAILED'"));
|
||||
assertTrue(tagFailedMigrationSql.contains("project.status IN ('0', '3')"));
|
||||
|
||||
assertTrue(initSql.contains("已删除"));
|
||||
assertTrue(initSql.contains("'5'"));
|
||||
assertTrue(prodInitSql.contains("已删除"));
|
||||
assertTrue(prodInitSql.contains("'5','ccdi_project_status'"));
|
||||
assertTrue(deletedMigrationSql.contains("ccdi_project_status"));
|
||||
assertTrue(deletedMigrationSql.contains("已删除"));
|
||||
assertTrue(deletedMigrationSql.contains("'5'"));
|
||||
assertTrue(deletedMigrationSql.contains("utf8mb4_general_ci"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# 流程列表项目删除与恢复后端实施计划
|
||||
|
||||
## 目标
|
||||
|
||||
在项目管理后端将删除改为业务逻辑删除,只更新 `ccdi_project` 主表状态与删除标记,不删除项目内上传记录、流水、标签结果、证据等关联数据。管理员范围按 `admin` 与 `manager` 执行,普通用户仅允许删除本人创建项目。
|
||||
|
||||
## 实施内容
|
||||
|
||||
1. 项目状态新增 `5-已删除`,同步常量、实体注释、状态统计 VO、状态文案、初始化 SQL 与迁移 SQL。
|
||||
2. 项目删除接口保留原路径,入口权限允许拥有项目列表访问的用户进入,业务权限由 `CcdiProjectAccessService` 判断:`admin/manager` 可删除全部,普通用户仅可删除本人创建项目。
|
||||
3. 删除实现改为 Mapper 专用更新语句,仅设置 `status='5'`、`del_flag='2'`、`update_by`、`update_time`,不调用 MyBatis Plus `deleteById`,不操作关联表。
|
||||
4. 新增恢复接口 `POST /ccdi/project/{projectId}/restore`,仅 `admin/manager` 可恢复 `status='5' AND del_flag='2'` 的项目,恢复为 `status='1'`、`del_flag='0'`、`is_archived=0`。
|
||||
5. 列表查询增加 `includeDeleted`:默认只查 `del_flag='0'` 且排除 `status='5'`;仅管理员查询已删除列表时查 `del_flag='2' AND status='5'`。
|
||||
6. 状态统计增加 `status5`,仅管理员返回已删除数量,普通用户返回 0。
|
||||
7. 详情、操作、归档、打标状态流转等链路对已删除项目按不可读或不可操作处理,异步状态更新不得覆盖已删除状态。
|
||||
|
||||
## 验证范围
|
||||
|
||||
后端定向测试覆盖删除只更新项目主表、恢复到已完成、普通入口可进入业务删除校验、默认列表与已删除列表条件、状态字典与迁移 SQL、已删除状态不可被异步打标状态覆盖。
|
||||
@@ -0,0 +1,18 @@
|
||||
# 流程列表项目删除与恢复前端实施计划
|
||||
|
||||
## 目标
|
||||
|
||||
在流程列表页增加项目删除与恢复操作。默认列表不展示已删除项目;`admin` 和 `manager` 可切换到已删除列表并恢复项目;普通用户只能在非删除列表中删除自己有权限删除的项目。
|
||||
|
||||
## 实施内容
|
||||
|
||||
1. `ruoyi-ui/src/api/ccdiProject.js` 增加 `restoreProject(projectId)`,删除继续复用现有 `delProject(projectId)`。
|
||||
2. 列表页查询参数增加 `includeDeleted`,普通状态 tab 固定传 `includeDeleted=false`;管理员点击“已删除”入口时传 `includeDeleted=true` 且不传普通 `status`。
|
||||
3. 搜索条增加“已删除”tab,仅 `admin/manager` 可见,数量使用后端 `status5`。
|
||||
4. 项目表格增加删除和恢复按钮:普通列表中按 `canDelete` 且状态不是 `5` 展示删除;删除列表仅展示恢复按钮,不展示进入项目、查看结果、重新分析、归档。
|
||||
5. 删除与恢复均使用确认弹窗,删除文案明确项目内数据不会删除,恢复文案明确恢复为已完成状态;成功后刷新列表与统计。
|
||||
6. 状态颜色补充 `5-已删除` 的危险色展示,字典值由后端 SQL 提供。
|
||||
|
||||
## 验证范围
|
||||
|
||||
前端源码断言覆盖已删除入口管理员可见、`includeDeleted` 参数、删除/恢复 API 调用、普通列表删除按钮、删除列表仅恢复按钮;构建验证 Vue 模板与打包链路。
|
||||
@@ -0,0 +1,37 @@
|
||||
# 流程列表项目删除与恢复实施记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
后端新增 `5-已删除` 项目状态,删除项目时仅更新 `ccdi_project.status='5'` 与 `del_flag='2'`,不删除任何项目关联数据;新增恢复接口,将已删除项目恢复为 `已完成(status=1)` 并清除删除标记。列表默认排除删除态,管理员通过 `includeDeleted=true` 查询删除列表并获取 `status5` 统计。
|
||||
|
||||
前端在流程列表中新增删除按钮和管理员“已删除”列表入口,删除列表只展示恢复按钮。删除、恢复都增加确认弹窗,并在成功后刷新列表和状态统计。
|
||||
|
||||
删除按钮已调整为红色文本按钮,悬停时使用浅红背景,便于和普通操作按钮区分。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- 后端:项目 Controller、Service、权限服务、Mapper XML、项目状态常量、DTO/VO、项目初始化 SQL 与迁移 SQL。
|
||||
- 前端:项目列表页、搜索条、项目表格、项目 API。
|
||||
- 数据:新增项目状态字典 `5-已删除`,更新项目主表 `status` 字段注释,新增迁移脚本。
|
||||
|
||||
## 验证记录
|
||||
|
||||
1. `mvn -pl ccdi-project -am -Dtest=CcdiProjectServiceImplTest,CcdiProjectControllerTest,CcdiProjectMapperXmlTest,CcdiProjectStatusSqlTest -DfailIfNoTests=false -Dsurefire.failIfNoSpecifiedTests=false test`
|
||||
- 结果:通过,26 个测试全部成功。
|
||||
2. `source ~/.nvm/nvm.sh && cd ruoyi-ui && nvm use && node tests/unit/project-list-archive-flow.test.js && node tests/unit/project-list-reanalyze-flow.test.js`
|
||||
- 结果:通过。
|
||||
3. `source ~/.nvm/nvm.sh && cd ruoyi-ui && nvm use && node tests/unit/project-table-style.test.js && node tests/unit/project-list-archive-flow.test.js && node tests/unit/project-list-reanalyze-flow.test.js`
|
||||
- 结果:通过,删除按钮红色样式断言通过。
|
||||
4. `source ~/.nvm/nvm.sh && cd ruoyi-ui && nvm use && npm run build:prod`
|
||||
- 结果:通过,仅存在既有资源体积警告。
|
||||
5. 使用应用内浏览器打开真实页面 `http://localhost:1024/ccdiProject`,以 `admin` 登录验证项目 `删除恢复验证项目-20260702093910`:
|
||||
- 默认列表可搜索到项目并显示“删除”按钮。
|
||||
- 删除确认弹窗提示“项目内数据不会删除”,确认后默认列表不再展示该项目。
|
||||
- “已删除”列表展示该项目且仅有“恢复”按钮,不展示进入项目、查看结果、重新分析、归档。
|
||||
- 恢复确认弹窗提示“项目将恢复为已完成状态”,确认后已删除列表不再展示该项目,接口复核默认列表中该项目状态为 `1`。
|
||||
- 验收结束后已删除本轮验证项目数据。
|
||||
- 验收期间启动的后端进程已通过 `bin/restart_java_backend.sh stop` 关闭;前端 1024 服务为验收前已存在进程,未额外关闭。
|
||||
|
||||
## 后续验收
|
||||
|
||||
已完成本地真实页面验收。后续上线前需在目标环境执行迁移脚本并使用管理员账号复核删除、已删除列表与恢复链路。
|
||||
@@ -52,6 +52,14 @@ export function delProject(projectIds) {
|
||||
})
|
||||
}
|
||||
|
||||
// 恢复已删除初核项目
|
||||
export function restoreProject(projectId) {
|
||||
return request({
|
||||
url: '/ccdi/project/' + projectId + '/restore',
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
// 导出初核项目
|
||||
export function exportProject(query) {
|
||||
return request({
|
||||
|
||||
@@ -92,23 +92,66 @@
|
||||
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="350"
|
||||
width="390"
|
||||
align="left"
|
||||
fixed="right"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
v-if="['0', '3', '4'].includes(scope.row.status)"
|
||||
size="mini"
|
||||
type="text"
|
||||
:icon="canOperate(scope.row) ? 'el-icon-right' : 'el-icon-view'"
|
||||
@click="handleEnter(scope.row)"
|
||||
>
|
||||
{{ canOperate(scope.row) ? "进入项目" : "查看项目" }}
|
||||
</el-button>
|
||||
|
||||
<template v-if="scope.row.status === '1'">
|
||||
<template v-if="deletedList">
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-refresh-left"
|
||||
@click="handleRestore(scope.row)"
|
||||
>
|
||||
恢复
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<el-button
|
||||
v-if="['0', '3', '4'].includes(scope.row.status)"
|
||||
size="mini"
|
||||
type="text"
|
||||
:icon="canOperate(scope.row) ? 'el-icon-right' : 'el-icon-view'"
|
||||
@click="handleEnter(scope.row)"
|
||||
>
|
||||
{{ canOperate(scope.row) ? "进入项目" : "查看项目" }}
|
||||
</el-button>
|
||||
|
||||
<template v-if="scope.row.status === '1'">
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-view"
|
||||
@click="handleViewResult(scope.row)"
|
||||
>
|
||||
查看结果
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canOperate(scope.row)"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-refresh"
|
||||
:loading="reAnalyzeLoadingMap[String(scope.row.projectId)]"
|
||||
:disabled="reAnalyzeLoadingMap[String(scope.row.projectId)]"
|
||||
@click="handleReAnalyze(scope.row)"
|
||||
>
|
||||
重新分析
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canOperate(scope.row)"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-folder"
|
||||
@click="handleArchive(scope.row)"
|
||||
>
|
||||
归档
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<el-button
|
||||
v-if="scope.row.status === '2'"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-view"
|
||||
@@ -116,37 +159,18 @@
|
||||
>
|
||||
查看结果
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
v-if="canOperate(scope.row)"
|
||||
v-if="canDelete(scope.row) && scope.row.status !== '5'"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-refresh"
|
||||
:loading="reAnalyzeLoadingMap[String(scope.row.projectId)]"
|
||||
:disabled="reAnalyzeLoadingMap[String(scope.row.projectId)]"
|
||||
@click="handleReAnalyze(scope.row)"
|
||||
class="delete-button"
|
||||
icon="el-icon-delete"
|
||||
@click="handleDelete(scope.row)"
|
||||
>
|
||||
重新分析
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canOperate(scope.row)"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-folder"
|
||||
@click="handleArchive(scope.row)"
|
||||
>
|
||||
归档
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<el-button
|
||||
v-if="scope.row.status === '2'"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-view"
|
||||
@click="handleViewResult(scope.row)"
|
||||
>
|
||||
查看结果
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -193,6 +217,10 @@ export default {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
deletedList: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getStatusColor(status) {
|
||||
@@ -202,6 +230,7 @@ export default {
|
||||
2: "#8c8c8c",
|
||||
3: "#fa8c16",
|
||||
4: "#f56c6c",
|
||||
5: "#f56c6c",
|
||||
};
|
||||
return colorMap[status] || "#8c8c8c";
|
||||
},
|
||||
@@ -230,9 +259,18 @@ export default {
|
||||
handleArchive(row) {
|
||||
this.$emit("archive", row);
|
||||
},
|
||||
handleDelete(row) {
|
||||
this.$emit("delete", row);
|
||||
},
|
||||
handleRestore(row) {
|
||||
this.$emit("restore", row);
|
||||
},
|
||||
canOperate(row) {
|
||||
return !row || row.canOperate !== false;
|
||||
},
|
||||
canDelete(row) {
|
||||
return !row || row.canDelete !== false;
|
||||
},
|
||||
handleSizeChange(val) {
|
||||
this.$emit("pagination", {
|
||||
pageNum: this.pageParams.pageNum,
|
||||
@@ -367,6 +405,15 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .el-button--text.delete-button {
|
||||
color: #f56c6c;
|
||||
|
||||
&:hover {
|
||||
color: #dd6161;
|
||||
background-color: rgba(245, 108, 108, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .el-pagination {
|
||||
margin-top: 24px;
|
||||
text-align: right;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</div>
|
||||
<div class="tab-filters">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
v-for="tab in visibleTabs"
|
||||
:key="tab.value"
|
||||
:class="['tab-item', { active: activeTab === tab.value }]"
|
||||
@click="handleTabChange(tab.value)"
|
||||
@@ -41,8 +41,13 @@ export default {
|
||||
'1': 0,
|
||||
'2': 0,
|
||||
'3': 0,
|
||||
'4': 0
|
||||
'4': 0,
|
||||
'5': 0
|
||||
})
|
||||
},
|
||||
showDeletedTab: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -55,20 +60,32 @@ export default {
|
||||
{ label: '已完成', value: '1', count: 0 },
|
||||
{ label: '已归档', value: '2', count: 0 },
|
||||
{ label: '打标中', value: '3', count: 0 },
|
||||
{ label: '打标失败', value: '4', count: 0 }
|
||||
{ label: '打标失败', value: '4', count: 0 },
|
||||
{ label: '已删除', value: 'deleted', count: 0 }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
visibleTabs() {
|
||||
return this.tabs.filter(tab => tab.value !== 'deleted' || this.showDeletedTab)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
tabCounts: {
|
||||
handler(newVal) {
|
||||
this.tabs = this.tabs.map(tab => ({
|
||||
...tab,
|
||||
count: newVal[tab.value] || 0
|
||||
count: newVal[tab.value === 'deleted' ? '5' : tab.value] || 0
|
||||
}))
|
||||
},
|
||||
immediate: true,
|
||||
deep: true
|
||||
},
|
||||
showDeletedTab(newVal) {
|
||||
if (!newVal && this.activeTab === 'deleted') {
|
||||
this.activeTab = 'all'
|
||||
this.emitQuery()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -83,9 +100,11 @@ export default {
|
||||
},
|
||||
/** 发送查询 */
|
||||
emitQuery() {
|
||||
const includeDeleted = this.activeTab === 'deleted'
|
||||
this.$emit('query', {
|
||||
projectName: this.searchKeyword || null,
|
||||
status: this.activeTab === 'all' ? null : this.activeTab
|
||||
status: includeDeleted || this.activeTab === 'all' ? null : this.activeTab,
|
||||
includeDeleted
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<search-bar
|
||||
:show-search="showSearch"
|
||||
:tab-counts="tabCounts"
|
||||
:show-deleted-tab="isProjectAdmin"
|
||||
@query="handleQuery"
|
||||
/>
|
||||
|
||||
@@ -20,11 +21,14 @@
|
||||
:total="total"
|
||||
:page-params="queryParams"
|
||||
:re-analyze-loading-map="reAnalyzeLoadingMap"
|
||||
:deleted-list="queryParams.includeDeleted === true"
|
||||
@pagination="handlePagination"
|
||||
@enter="handleEnter"
|
||||
@view-result="handleViewResult"
|
||||
@re-analyze="handleReAnalyze"
|
||||
@archive="handleArchive"
|
||||
@delete="handleDelete"
|
||||
@restore="handleRestore"
|
||||
/>
|
||||
|
||||
<!-- 快捷入口区 -->
|
||||
@@ -63,7 +67,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {archiveProject, getStatusCounts, listProject, rebuildProjectTags} from '@/api/ccdiProject'
|
||||
import {archiveProject, delProject, getStatusCounts, listProject, rebuildProjectTags, restoreProject} from '@/api/ccdiProject'
|
||||
import SearchBar from './components/SearchBar'
|
||||
import ProjectTable from './components/ProjectTable'
|
||||
import QuickEntry from './components/QuickEntry'
|
||||
@@ -96,7 +100,8 @@ export default {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
projectName: null,
|
||||
status: null
|
||||
status: null,
|
||||
includeDeleted: false
|
||||
},
|
||||
// 标签页数量统计
|
||||
tabCounts: {
|
||||
@@ -105,7 +110,8 @@ export default {
|
||||
'1': 0,
|
||||
'2': 0,
|
||||
'3': 0,
|
||||
'4': 0
|
||||
'4': 0,
|
||||
'5': 0
|
||||
},
|
||||
// 新增/编辑弹窗
|
||||
addDialogVisible: false,
|
||||
@@ -123,6 +129,12 @@ export default {
|
||||
created() {
|
||||
this.getList();
|
||||
},
|
||||
computed: {
|
||||
isProjectAdmin() {
|
||||
const roles = this.$store && this.$store.getters ? this.$store.getters.roles || [] : []
|
||||
return roles.includes("admin") || roles.includes("manager")
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** 查询项目列表 */
|
||||
getList() {
|
||||
@@ -145,7 +157,8 @@ export default {
|
||||
'1': counts.status1 || 0,
|
||||
'2': counts.status2 || 0,
|
||||
'3': counts.status3 || 0,
|
||||
'4': counts.status4 || 0
|
||||
'4': counts.status4 || 0,
|
||||
'5': counts.status5 || 0
|
||||
}
|
||||
|
||||
this.loading = false
|
||||
@@ -157,10 +170,21 @@ export default {
|
||||
},
|
||||
/** 搜索按钮操作 */
|
||||
handleQuery(queryParams) {
|
||||
const nextQueryParams = { ...this.queryParams }
|
||||
if (queryParams) {
|
||||
this.queryParams = { ...this.queryParams, ...queryParams };
|
||||
Object.assign(nextQueryParams, queryParams)
|
||||
}
|
||||
this.queryParams.pageNum = 1;
|
||||
if (nextQueryParams.includeDeleted && !this.isProjectAdmin) {
|
||||
nextQueryParams.includeDeleted = false
|
||||
nextQueryParams.status = null
|
||||
}
|
||||
if (nextQueryParams.includeDeleted) {
|
||||
nextQueryParams.status = null
|
||||
} else {
|
||||
nextQueryParams.includeDeleted = false
|
||||
}
|
||||
nextQueryParams.pageNum = 1
|
||||
this.queryParams = nextQueryParams
|
||||
this.getList();
|
||||
},
|
||||
/** 分页事件处理 */
|
||||
@@ -312,9 +336,60 @@ export default {
|
||||
this.$modal.msgError(message)
|
||||
}
|
||||
},
|
||||
/** 删除项目 */
|
||||
async handleDelete(row) {
|
||||
if (!this.canDelete(row)) {
|
||||
this.$modal.msgWarning("当前项目不能删除")
|
||||
return
|
||||
}
|
||||
try {
|
||||
await this.$modal.confirm(
|
||||
`确认删除项目“${row.projectName}”吗?删除后项目进入已删除列表,项目内数据不会删除。`
|
||||
)
|
||||
} catch (confirmError) {
|
||||
if (confirmError === "cancel" || confirmError === "close") {
|
||||
return
|
||||
}
|
||||
throw confirmError
|
||||
}
|
||||
try {
|
||||
await delProject(row.projectId)
|
||||
this.$modal.msgSuccess("项目删除成功")
|
||||
this.getList()
|
||||
} catch (error) {
|
||||
const message = error && error.message ? error.message : "项目删除失败,请稍后重试"
|
||||
this.$modal.msgError(message)
|
||||
}
|
||||
},
|
||||
/** 恢复已删除项目 */
|
||||
async handleRestore(row) {
|
||||
if (!this.isProjectAdmin) {
|
||||
this.$modal.msgWarning("当前用户不能恢复项目")
|
||||
return
|
||||
}
|
||||
try {
|
||||
await this.$modal.confirm(`确认恢复项目“${row.projectName}”吗?项目将恢复为已完成状态。`)
|
||||
} catch (confirmError) {
|
||||
if (confirmError === "cancel" || confirmError === "close") {
|
||||
return
|
||||
}
|
||||
throw confirmError
|
||||
}
|
||||
try {
|
||||
await restoreProject(row.projectId)
|
||||
this.$modal.msgSuccess("项目恢复成功")
|
||||
this.getList()
|
||||
} catch (error) {
|
||||
const message = error && error.message ? error.message : "项目恢复失败,请稍后重试"
|
||||
this.$modal.msgError(message)
|
||||
}
|
||||
},
|
||||
canOperate(row) {
|
||||
return !row || row.canOperate !== false
|
||||
},
|
||||
canDelete(row) {
|
||||
return !row || row.canDelete !== false
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -10,6 +10,46 @@ const dialogPath = path.resolve(
|
||||
const pageSource = fs.readFileSync(pagePath, "utf8");
|
||||
const dialogSource = fs.readFileSync(dialogPath, "utf8");
|
||||
|
||||
assert(
|
||||
pageSource.includes("delProject") && pageSource.includes("restoreProject"),
|
||||
"项目列表页应引入删除和恢复接口"
|
||||
);
|
||||
|
||||
assert(
|
||||
pageSource.includes("includeDeleted: false"),
|
||||
"默认查询参数应不包含已删除项目"
|
||||
);
|
||||
|
||||
assert(
|
||||
pageSource.includes(':show-deleted-tab="isProjectAdmin"'),
|
||||
"已删除入口应仅对项目管理员角色展示"
|
||||
);
|
||||
|
||||
assert(
|
||||
pageSource.includes("'5': counts.status5 || 0"),
|
||||
"状态统计应接入已删除数量"
|
||||
);
|
||||
|
||||
assert(
|
||||
pageSource.includes('await delProject(row.projectId)'),
|
||||
"删除确认后应调用项目删除接口"
|
||||
);
|
||||
|
||||
assert(
|
||||
pageSource.includes('await restoreProject(row.projectId)'),
|
||||
"恢复确认后应调用项目恢复接口"
|
||||
);
|
||||
|
||||
assert(
|
||||
pageSource.includes("项目内数据不会删除"),
|
||||
"删除确认文案应明确项目内数据不会删除"
|
||||
);
|
||||
|
||||
assert(
|
||||
pageSource.includes("项目将恢复为已完成状态"),
|
||||
"恢复确认文案应明确恢复到已完成状态"
|
||||
);
|
||||
|
||||
assert(
|
||||
pageSource.includes("await archiveProject(data.projectId)"),
|
||||
"确认归档后应调用真实归档接口"
|
||||
|
||||
@@ -6,6 +6,48 @@ const pagePath = path.resolve(__dirname, "../../src/views/ccdiProject/index.vue"
|
||||
const tablePath = path.resolve(__dirname, "../../src/views/ccdiProject/components/ProjectTable.vue");
|
||||
const pageSource = fs.readFileSync(pagePath, "utf8");
|
||||
const tableSource = fs.readFileSync(tablePath, "utf8");
|
||||
const searchBarPath = path.resolve(__dirname, "../../src/views/ccdiProject/components/SearchBar.vue");
|
||||
const searchBarSource = fs.readFileSync(searchBarPath, "utf8");
|
||||
|
||||
assert(
|
||||
searchBarSource.includes("{ label: '已删除', value: 'deleted'"),
|
||||
"搜索条应提供独立的已删除列表入口"
|
||||
);
|
||||
|
||||
assert(
|
||||
searchBarSource.includes("tab.value !== 'deleted' || this.showDeletedTab"),
|
||||
"已删除入口应由 showDeletedTab 控制可见性"
|
||||
);
|
||||
|
||||
assert(
|
||||
searchBarSource.includes("const includeDeleted = this.activeTab === 'deleted'"),
|
||||
"切换已删除入口时应生成 includeDeleted 查询态"
|
||||
);
|
||||
|
||||
assert(
|
||||
searchBarSource.includes("status: includeDeleted || this.activeTab === 'all' ? null : this.activeTab"),
|
||||
"已删除列表不应和普通状态 tab 混用"
|
||||
);
|
||||
|
||||
assert(
|
||||
tableSource.includes('v-if="deletedList"'),
|
||||
"删除列表应使用独立操作区"
|
||||
);
|
||||
|
||||
assert(
|
||||
tableSource.includes('@click="handleRestore(scope.row)"'),
|
||||
"删除列表应提供恢复按钮"
|
||||
);
|
||||
|
||||
assert(
|
||||
tableSource.includes('v-if="canDelete(scope.row) && scope.row.status !== \'5\'"'),
|
||||
"普通列表应按 canDelete 展示删除按钮且排除已删除状态"
|
||||
);
|
||||
|
||||
assert(
|
||||
/<template v-if="deletedList">[\s\S]*?handleRestore\(scope\.row\)[\s\S]*?<\/template>\s*<template v-else>[\s\S]*?handleViewResult/.test(tableSource),
|
||||
"删除列表只展示恢复动作,进入项目和查看结果应留在普通列表分支"
|
||||
);
|
||||
|
||||
assert(
|
||||
pageSource.includes("rebuildProjectTags({ projectId: row.projectId })"),
|
||||
|
||||
@@ -23,5 +23,14 @@ assert(
|
||||
!source.includes("::v-deep .el-table"),
|
||||
"项目列表不应继续保留自定义深度表格皮肤"
|
||||
);
|
||||
assert(
|
||||
source.includes('class="delete-button"'),
|
||||
"项目列表删除按钮应使用独立红色样式类"
|
||||
);
|
||||
assert(
|
||||
source.includes("::v-deep .el-button--text.delete-button") &&
|
||||
source.includes("color: #f56c6c"),
|
||||
"项目列表删除按钮应渲染为红色"
|
||||
);
|
||||
|
||||
console.log("project-table-style test passed");
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -11,7 +11,7 @@ CREATE TABLE `ccdi_project` (
|
||||
`project_name` VARCHAR(200) NOT NULL COMMENT '项目名称',
|
||||
`description` VARCHAR(500) DEFAULT NULL COMMENT '项目描述',
|
||||
`config_type` VARCHAR(20) NOT NULL DEFAULT 'default' COMMENT '配置方式:default-全局默认,custom-自定义',
|
||||
`status` CHAR(1) NOT NULL DEFAULT '0' COMMENT '项目状态:0-进行中,1-已完成,2-已归档,3-打标中,4-打标失败',
|
||||
`status` CHAR(1) NOT NULL DEFAULT '0' COMMENT '项目状态:0-进行中,1-已完成,2-已归档,3-打标中,4-打标失败,5-已删除',
|
||||
`is_archived` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否归档:0-未归档,1-已归档',
|
||||
`target_count` INT NOT NULL DEFAULT 0 COMMENT '目标人数',
|
||||
`high_risk_count` INT NOT NULL DEFAULT 0 COMMENT '高风险人数',
|
||||
@@ -29,7 +29,7 @@ CREATE TABLE `ccdi_project` (
|
||||
INDEX `idx_is_archived` (`is_archived`),
|
||||
INDEX `idx_del_flag` (`del_flag`),
|
||||
INDEX `idx_create_time` (`create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='纪检初核项目表';
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='纪检初核项目表';
|
||||
|
||||
-- ----------------------------
|
||||
-- 3. 插入项目状态字典
|
||||
@@ -43,7 +43,8 @@ VALUES
|
||||
(2, '已完成', '1', 'ccdi_project_status', '', 'success', 'N', '0', 'admin', NOW()),
|
||||
(3, '已归档', '2', 'ccdi_project_status', '', 'info', 'N', '0', 'admin', NOW()),
|
||||
(4, '打标中', '3', 'ccdi_project_status', '', 'warning', 'N', '0', 'admin', NOW()),
|
||||
(5, '打标失败', '4', 'ccdi_project_status', '', 'danger', 'N', '0', 'admin', NOW());
|
||||
(5, '打标失败', '4', 'ccdi_project_status', '', 'danger', 'N', '0', 'admin', NOW()),
|
||||
(6, '已删除', '5', 'ccdi_project_status', '', 'danger', 'N', '0', 'admin', NOW());
|
||||
|
||||
-- ----------------------------
|
||||
-- 4. 插入配置方式字典
|
||||
|
||||
41
sql/migration/2026-07-02-add-project-deleted-status.sql
Normal file
41
sql/migration/2026-07-02-add-project-deleted-status.sql
Normal file
@@ -0,0 +1,41 @@
|
||||
ALTER TABLE ccdi_project
|
||||
MODIFY COLUMN status CHAR(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0'
|
||||
COMMENT '项目状态:0-进行中,1-已完成,2-已归档,3-打标中,4-打标失败,5-已删除';
|
||||
|
||||
INSERT INTO sys_dict_data (
|
||||
dict_sort,
|
||||
dict_label,
|
||||
dict_value,
|
||||
dict_type,
|
||||
css_class,
|
||||
list_class,
|
||||
is_default,
|
||||
status,
|
||||
create_by,
|
||||
create_time,
|
||||
remark
|
||||
)
|
||||
SELECT
|
||||
6,
|
||||
'已删除',
|
||||
'5',
|
||||
'ccdi_project_status',
|
||||
'',
|
||||
'danger',
|
||||
'N',
|
||||
'0',
|
||||
'admin',
|
||||
NOW(),
|
||||
'项目逻辑删除状态'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM sys_dict_data
|
||||
WHERE dict_type = 'ccdi_project_status'
|
||||
AND dict_value = '5'
|
||||
);
|
||||
|
||||
UPDATE sys_role
|
||||
SET remark = '可查看全部项目,可删除和恢复全部项目,其他操作由后端项目归属校验控制',
|
||||
update_by = 'admin',
|
||||
update_time = NOW()
|
||||
WHERE role_key = 'manager';
|
||||
Reference in New Issue
Block a user