feat(workflow): 添加作业撤回功能支持

- 新增 EightworkInfoWithdrawCmd 命令类用于撤回请求参数
- 实现 EightworkInfoWithdrawExe 执行器处理撤回业务逻辑
- 在 EightworkInfoDO 和相关模型中添加 lockFlag 字段控制撤回权限
- 添加 withdraw 接口到 EightworkInfoController 和服务层
- 实现物理删除 task_log 记录的批量删除功能
- 添加盲板工作类型筛选查询条件
- 修改菜单枚举配置支持新的作业类型路径
- 实现 CAS 乐观锁机制防止并发冲突
master
fangjiakai 2026-04-09 09:47:13 +08:00
parent 9fe44ac750
commit f6e39da335
20 changed files with 348 additions and 12 deletions

View File

@ -5,6 +5,7 @@ import com.zcloud.eightwork.api.EightworkInfoServiceI;
import com.zcloud.eightwork.dto.EightworkInfoAddCmd;
import com.zcloud.eightwork.dto.EightworkInfoPageQry;
import com.zcloud.eightwork.dto.EightworkInfoUpdateCmd;
import com.zcloud.eightwork.dto.EightworkInfoWithdrawCmd;
import com.zcloud.eightwork.dto.ForceTerminateCmd;
import com.zcloud.eightwork.dto.clientobject.StatisticsByWorkTypeCO;
import com.zcloud.eightwork.dto.clientobject.EightworkInfoCO;
@ -45,8 +46,8 @@ public class EightworkInfoController {
@ApiOperation("分页")
@PostMapping("/list")
public PageResponse<EightworkInfoCO> page(@RequestBody EightworkInfoPageQry qry) {
qry.setEqDepartmentId(AuthContext.getOrgId());
qry.setEqCreateId(AuthContext.getUserId());
// qry.setEqDepartmentId(AuthContext.getOrgId());
// qry.setEqCreateId(AuthContext.getUserId());
return eightworkInfoService.listPage(qry);
}
@ -83,6 +84,13 @@ public class EightworkInfoController {
return SingleResponse.buildSuccess();
}
@ApiOperation("撤回作业到暂存")
@PostMapping("/withdraw")
public Response withdraw(@Validated @RequestBody EightworkInfoWithdrawCmd cmd) {
eightworkInfoService.withdraw(cmd.getId(), cmd.getReason());
return SingleResponse.buildSuccess();
}
@ApiOperation("强制终止工作流")
@PostMapping("/forceTerminate")
public Response forceTerminate(@RequestBody ForceTerminateCmd cmd) {

View File

@ -152,6 +152,7 @@ public class EightworkInfoSaveDraftExe {
eightworkInfo.setStatus(DRAFT_STATUS);
eightworkInfo.setInfo(cmd.getInfo().toJSONString());
eightworkInfo.setDepartmentId(cmd.getDepartmentId());
eightworkInfo.setLockFlag(2); // 设置为未锁定(可撤回)
eightworkInfoRepository.save(eightworkInfo);
log.info("主表创建成功: workId={}", eightworkInfo.getWorkId());
@ -222,6 +223,7 @@ public class EightworkInfoSaveDraftExe {
existingInfo.setGasFlag(cmd.getGasFlag());
existingInfo.setInfo(cmd.getInfo().toJSONString());
existingInfo.setDepartmentId(cmd.getDepartmentId());
existingInfo.setLockFlag(2); // 设置为未锁定(可撤回)
}
/**

View File

@ -0,0 +1,139 @@
package com.zcloud.eightwork.command;
import com.alibaba.cola.exception.BizException;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.zcloud.eightwork.domain.gateway.TaskLogGateway;
import com.zcloud.eightwork.domain.model.TaskLogE;
import com.zcloud.eightwork.domain.model.enums.TaskLogStatus;
import com.zcloud.eightwork.dto.EightworkInfoWithdrawCmd;
import com.zcloud.eightwork.persistence.dataobject.EightworkInfoDO;
import com.zcloud.eightwork.persistence.dataobject.TaskLogDO;
import com.zcloud.eightwork.persistence.repository.EightworkInfoRepository;
import com.zcloud.eightwork.persistence.repository.MeasuresLogsRepository;
import com.zcloud.eightwork.persistence.repository.TaskLogRepository;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
/**
*
*
*
* @Author fangjiakai
* @Date 2026-04-07
*/
@Slf4j
@Component
@AllArgsConstructor
public class EightworkInfoWithdrawExe {
private static final Integer LOCK_FLAG_LOCKED = 1; // 锁定不可撤回
private static final Integer LOCK_FLAG_UNLOCKED = 2; // 未锁定可撤回
private static final Integer DRAFT_STATUS = 0; // 暂存状态
private static final Long APPLY_STEP_ID = 1L; // 申请步骤ID
private final EightworkInfoRepository eightworkInfoRepository;
private final TaskLogRepository taskLogRepository;
private final TaskLogGateway taskLogGateway;
private final MeasuresLogsRepository measuresLogsRepository;
@Transactional(rollbackFor = Exception.class)
public void execute(EightworkInfoWithdrawCmd cmd) {
log.info("开始撤回作业: id={}, reason={}", cmd.getId(), cmd.getReason());
// 1. 查询作业记录(加锁,确保并发安全)
EightworkInfoDO infoDO = eightworkInfoRepository.getById(cmd.getId());
if (infoDO == null) {
throw new BizException("作业记录不存在");
}
// 记录当前状态,用于并发检测
Integer currentStatus = infoDO.getStatus();
Integer currentLockFlag = infoDO.getLockFlag();
// 2. 检查是否可以撤回(未锁定)
if (LOCK_FLAG_LOCKED.equals(currentLockFlag)) {
throw new BizException("作业已进行审批流程,无法撤回");
}
// 3. 检查当前状态是否允许撤回
if (!TaskLogStatus.APPROVED.equalsCode(currentStatus)) {
throw new BizException("只能撤回进行中的作业");
}
String workId = infoDO.getWorkId();
// 4. 检查是否满足撤回条件:除申请外无完成流程
List<TaskLogE> logs = taskLogGateway.listAllByWorkId(workId);
boolean canWithdraw = checkCanWithdraw(logs);
if (!canWithdraw) {
throw new BizException("作业已有审批流程进行,无法撤回");
}
// 5. 再次检查状态(防止在检查期间状态被修改)
EightworkInfoDO latestInfo = eightworkInfoRepository.getById(cmd.getId());
if (!latestInfo.getStatus().equals(currentStatus) ||
!latestInfo.getLockFlag().equals(currentLockFlag)) {
log.warn("撤回失败:作业状态已被修改 originalStatus={}, originalLockFlag={}, currentStatus={}, currentLockFlag={}",
currentStatus, currentLockFlag, latestInfo.getStatus(), latestInfo.getLockFlag());
throw new BizException("操作失败:作业状态已被修改,请刷新后重试");
}
// 6. 撤回操作
// 6.1 更新主表状态为暂存
latestInfo.setStatus(DRAFT_STATUS);
latestInfo.setLockFlag(LOCK_FLAG_UNLOCKED);
eightworkInfoRepository.updateById(latestInfo);
// 6.2 删除除申请外的其他 task_log 记录(物理删除)
List<Long> toDeleteIds = logs.stream()
.filter(taskLog -> !taskLog.getStepId().equals(APPLY_STEP_ID))
.map(TaskLogE::getId)
.collect(Collectors.toList());
if (!toDeleteIds.isEmpty()) {
taskLogRepository.physicalDeleteByIds(toDeleteIds);
log.info("已物理删除 {} 条非申请步骤记录", toDeleteIds.size());
}
// 6.3 将申请步骤状态改为未开始
TaskLogE applyLog = logs.stream()
.filter(taskLog -> taskLog.getStepId().equals(APPLY_STEP_ID))
.findFirst()
.orElse(null);
if (applyLog != null) {
// 无论申请步骤当前是什么状态(进行中或通过),都改为未开始
TaskLogDO updateLog = new TaskLogDO();
updateLog.setId(applyLog.getId());
updateLog.setStatus(TaskLogStatus.IN_PROGRESS.getCode());
taskLogRepository.updateById(updateLog);
log.info("申请步骤状态已改为未开始");
}
log.info("作业撤回成功: workId={}, id={}", workId, cmd.getId());
}
/**
*
*
*/
private boolean checkCanWithdraw(List<TaskLogE> logs) {
for (TaskLogE taskLog : logs) {
// 跳过申请步骤
if (APPLY_STEP_ID.equals(taskLog.getStepId())) {
continue;
}
// 如果其他步骤是完成状态,说明已有审批流程进行
if (TaskLogStatus.APPROVED.equalsCode(taskLog.getStatus())) {
log.info("不可撤回: stepId={}, stepName={}, status={}",
taskLog.getStepId(), taskLog.getStepName(), taskLog.getStatus());
return false;
}
}
return true;
}
}

View File

@ -363,6 +363,9 @@ public class TaskLogAddExe {
);
handleInfoStep(eightworkInfo,cmd.getSignLogs());
// 设置初始锁定标识为未锁定(可撤回)
eightworkInfo.setLockFlag(2);
eightworkInfoRepository.save(eightworkInfo);
return eightworkInfo.getWorkId();
}

View File

@ -138,6 +138,18 @@ public class TaskLogUpdateExe {
*
*/
private static final Integer FORCE_TERMINATE_STATUS = 998;
/**
*
*/
private static final Integer LOCK_FLAG_LOCKED = 1;
/**
*
*/
private static final Integer LOCK_FLAG_UNLOCKED = 2;
/**
* ID
*/
private static final Long APPLY_STEP_ID = 1L;
@Transactional(rollbackFor = Exception.class)
public void nextStep(TaskLogNextCmd cmd) {
@ -251,6 +263,12 @@ public class TaskLogUpdateExe {
log.info("附件步骤已保存附件: filePath={}", cmd.getFilePath());
}
// 非申请步骤完成时,更新主表 lockFlag 为锁定状态(不可撤回)
if (!APPLY_STEP_ID.equals(currentLog.getStepId()) &&
TaskLogStatus.APPROVED.equalsCode(cmd.getStatus())) {
updateLockFlagToLocked(cmd.getWorkId());
}
// 发送待办完成事件
sendTodoCompleteEvent(currentLog.getId());
@ -760,6 +778,52 @@ public class TaskLogUpdateExe {
log.info("已保存关闭原因: workId={}, closeReason={}", workId, closeReason);
}
/**
* lockFlag
*
* 使
*/
private void updateLockFlagToLocked(String workId) {
// 使用查询构建器,确保获取最新状态
EightworkInfoDO infoDO = eightworkInfoRepository.getOne(
new LambdaQueryWrapper<EightworkInfoDO>()
.eq(EightworkInfoDO::getWorkId, workId)
.select(EightworkInfoDO::getId, EightworkInfoDO::getLockFlag,
EightworkInfoDO::getStatus, EightworkInfoDO::getWorkId));
if (infoDO == null) {
log.warn("未找到作业信息,无法更新锁定标识: workId={}", workId);
return;
}
// 检查当前状态,确保作业未被撤回
if (!TaskLogStatus.APPROVED.equalsCode(infoDO.getStatus())) {
log.info("作业状态已变更(可能已被撤回),跳过锁定: workId={}, status={}",
workId, infoDO.getStatus());
return;
}
// 如果已经是锁定状态,跳过
if (LOCK_FLAG_LOCKED.equals(infoDO.getLockFlag())) {
return;
}
infoDO.setLockFlag(LOCK_FLAG_LOCKED);
// 使用 CAS 方式更新(防止并发)
boolean updated = eightworkInfoRepository.updateLockFlagToLocked(
infoDO.getId(),
LOCK_FLAG_UNLOCKED, // 期望的当前值(未锁定)
LOCK_FLAG_LOCKED, // 新值(锁定)
TaskLogStatus.APPROVED.getCode()); // 期望的当前状态
if (updated) {
log.info("作业已锁定,不可撤回: workId={}", workId);
} else {
log.info("锁定失败:作业状态可能已被修改(可能已被撤回): workId={}", workId);
}
}
/**
*
* status2

View File

@ -8,6 +8,7 @@ import com.zcloud.eightwork.api.TaskLogServiceI;
import com.zcloud.eightwork.command.EightworkInfoAddExe;
import com.zcloud.eightwork.command.EightworkInfoRemoveExe;
import com.zcloud.eightwork.command.EightworkInfoUpdateExe;
import com.zcloud.eightwork.command.EightworkInfoWithdrawExe;
import com.zcloud.eightwork.command.query.EightworkInfoQueryExe;
import com.zcloud.eightwork.domain.gateway.TaskLogGateway;
import com.zcloud.eightwork.domain.model.TaskLogE;
@ -44,6 +45,7 @@ public class EightworkInfoServiceImpl implements EightworkInfoServiceI {
private final TaskLogServiceI taskLogService;
private final TaskLogGateway taskLogGateway;
private final EightworkInfoRepository eightworkInfoRepository;
private final EightworkInfoWithdrawExe eightworkInfoWithdrawExe;
@Override
public EightworkInfoCO queryById(Long id) {
@ -135,6 +137,26 @@ public class EightworkInfoServiceImpl implements EightworkInfoServiceI {
log.info("工作流已强制终止: workId={}, stepId={}, closeReason={}", workId, currentLog.getStepId(), closeReason);
}
/**
*
*
* @param id ID
* @param reason
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void withdraw(Long id, String reason) {
log.info("撤回作业: id={}, reason={}", id, reason);
com.zcloud.eightwork.dto.EightworkInfoWithdrawCmd cmd = new com.zcloud.eightwork.dto.EightworkInfoWithdrawCmd();
cmd.setId(id);
cmd.setReason(reason);
eightworkInfoWithdrawExe.execute(cmd);
log.info("作业已撤回: id={}", id);
}
@Override
public List<StatisticsByWorkTypeCO> statisticsByWorkType(EightworkInfoPageQry qry){
return eightworkInfoQueryExe.statisticsByWorkType(qry);

View File

@ -38,5 +38,13 @@ public interface EightworkInfoServiceI {
void forceTerminate(Long id, String closeReason);
List<StatisticsByWorkTypeCO> statisticsByWorkType(EightworkInfoPageQry qry);
/**
*
*
* @param id ID
* @param reason
*/
void withdraw(Long id, String reason);
}

View File

@ -62,6 +62,9 @@ public class EightworkInfoPageQry extends PageQuery {
private Integer eqIsInnerWork;
private String likeLimitedSpaceNameAndCode;
/**
*
*/
private String eqBlindboardWorkType;
}

View File

@ -0,0 +1,23 @@
package com.zcloud.eightwork.dto;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
*
*
* @Author fangjiakai
* @Date 2026-04-07
*/
@Data
public class EightworkInfoWithdrawCmd {
@ApiModelProperty(value = "主表ID", required = true)
@NotNull(message = "主表ID不能为空")
private Long id;
@ApiModelProperty(value = "撤回原因")
private String reason;
}

View File

@ -53,6 +53,9 @@ public class EightworkInfoCO extends ClientObject {
//状态
@ApiModelProperty(value = "状态")
private Integer status;
//锁定标识 1锁定不可撤回 2未锁定可撤回
@ApiModelProperty(value = "锁定标识 1锁定不可撤回 2未锁定可撤回")
private Integer lockFlag;
//详细信息
@ApiModelProperty(value = "详细信息")
private JSONObject info;

View File

@ -37,6 +37,8 @@ public class EightworkInfoE extends BaseE {
private String currentStep;
//状态
private Integer status;
//锁定标识 1锁定不可撤回 2未锁定可撤回
private Integer lockFlag;
//详细信息
private String info;
}

View File

@ -14,15 +14,16 @@ import java.util.stream.Collectors;
@Getter
public enum MenuEnum {
FIREWORK_LIST("/hidden/container/branchCompany/average/ledger/list", "hidden-tz-fgs"),
SPACEWORK_LIST("/hidden/container/branchCompany/average/ignore/list", "hidden-hl-fgs"),
HIDDEN_QR_FGS("/hidden/container/branchCompany/average/confirm/list", "hidden-qr-fgs"),
HIDDEN_YS_FGS("/hidden/container/branchCompany/average/acceptance/list", "hidden-ys-fgs"),
HIDDEN_YQ_FGS("/hidden/container/branchCompany/average/postponement/list", "hidden-yq-fgs"),
HIDDEN_ZG_FGS("/hidden/container/branchCompany/average/rectification/list", "hidden-zg-fgs"),
HIDDEN_TSCZ_FGS("/hidden/container/branchCompany/average/specialDisposal/list", "hidden-tscz-fgs"),
HIDDEN_YHQRR_FGS("/hidden/container/branchCompany/average/confirmUser", "hidden-yhqrr-fgs")
;
HOT_WORK("/eightwork/container/enterprise/hotWork/homework/list", "hotwork-list"),
BLINDBOARD_WORK("/eightwork/container/enterprise/blindBoardWork/homework/list", "blindboardwork-list"),
BREAKGROUND_WORK("/eightwork/container/enterprise/digWork/homework/list", "digwork-list"),
CONFINEDSPACE("/eightwork/container/enterprise/confinedSpaceWork/ledger/list", "confinedspacework-ledger-list"),
CONFINEDSPACE_WORK("/eightwork/container/enterprise/confinedSpaceWork/homework/list", "confinedspacework-list"),
CUTROAD_WORK("/eightwork/container/enterprise/cutWork/homework/list", "cutwork-list"),
ELECTRICITY_WORK("/eightwork/container/enterprise/electricWork/homework/list", "electricwork-list"),
HIGH_WORK("/eightwork/container/enterprise/highPlaceWork/homework/list", "highplacework-list"),
HOISTING("/eightwork/container/enterprise/liftingWork/homework/list", "liftingwork-list");
private final String path;
private final String menuKey;

View File

@ -58,6 +58,9 @@ public class EightworkInfoDO extends BaseDO {
//状态
@ApiModelProperty(value = "状态")
private Integer status;
//锁定标识 1锁定不可撤回 2未锁定可撤回
@ApiModelProperty(value = "锁定标识 1锁定不可撤回 2未锁定可撤回")
private Integer lockFlag;
//详细信息
@ApiModelProperty(value = "详细信息")
private String info;

View File

@ -35,5 +35,13 @@ public interface TaskLogMapper extends BaseMapper<TaskLogDO> {
* @return
*/
int physicalDeleteByWorkId(@Param("workId") String workId);
/**
* ID
*
* @param ids ID
* @return
*/
int physicalDeleteByIds(@Param("ids") List<Long> ids);
}

View File

@ -24,5 +24,17 @@ public interface EightworkInfoRepository extends BaseRepository<EightworkInfoDO>
Long countByWorkType(String workType);
List<StatisticsByWorkTypeDTO> statisticsByWorkType(Map<String, Object> params);
/**
* 使 CAS lockFlag
* lockFlag status
*
* @param id ID
* @param expectedLockFlag lockFlag
* @param newLockFlag lockFlag
* @param expectedStatus status
* @return true=false=
*/
boolean updateLockFlagToLocked(Long id, Integer expectedLockFlag, Integer newLockFlag, Integer expectedStatus);
}

View File

@ -34,5 +34,13 @@ public interface TaskLogRepository extends BaseRepository<TaskLogDO> {
* @return
*/
int physicalDeleteByWorkId(String workId);
/**
* ID
*
* @param ids ID
* @return
*/
int physicalDeleteByIds(List<Long> ids);
}

View File

@ -72,4 +72,15 @@ public class EightworkInfoRepositoryImpl extends BaseRepositoryImpl<EightworkInf
public List<StatisticsByWorkTypeDTO> statisticsByWorkType(Map<String, Object> params){
return eightworkInfoMapper.statisticsByWorkType(params);
}
@Override
public boolean updateLockFlagToLocked(Long id, Integer expectedLockFlag, Integer newLockFlag, Integer expectedStatus) {
LambdaUpdateWrapper<EightworkInfoDO> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(EightworkInfoDO::getId, id)
.eq(EightworkInfoDO::getLockFlag, expectedLockFlag) // CAS期望的当前值
.eq(EightworkInfoDO::getStatus, expectedStatus) // CAS期望的状态
.set(EightworkInfoDO::getLockFlag, newLockFlag); // 新值
return update(updateWrapper);
}
}

View File

@ -68,5 +68,11 @@ public class TaskLogRepositoryImpl extends BaseRepositoryImpl<TaskLogMapper, Tas
// 使用 Mapper 中定义的物理删除 SQL
return taskLogMapper.physicalDeleteByWorkId(workId);
}
@Override
public int physicalDeleteByIds(List<Long> ids) {
// 使用 Mapper 中定义的物理删除 SQL
return taskLogMapper.physicalDeleteByIds(ids);
}
}

View File

@ -72,6 +72,9 @@
<if test="params.likeWorkContent != null and params.likeWorkContent != ''">
and t.info->>'$.workContent' like concat('%', #{params.likeCreateName}, '%')
</if>
<if test="params.eqBlindboardWorkType != null and params.eqBlindboardWorkType != ''">
and t.info->>'$.blindboardWorkType' = #{params.eqBlindboardWorkType}
</if>
<if test="params.geWorkStartTime != null and params.geWorkStartTime != ''">
and t.info->>'$.workStartTime' &gt;= #{params.geWorkStartTime}
</if>

View File

@ -64,5 +64,12 @@
<delete id="physicalDeleteByWorkId">
DELETE FROM task_log WHERE work_id = #{workId}
</delete>
<delete id="physicalDeleteByIds">
DELETE FROM task_log WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
</mapper>