refactor(oa/erp): 优化临时任务状态同步与权限校验逻辑

1. 重构syncStateToPendingFinal方法,新增taskId参数并增加流程待办校验
2. 拆分工时校验逻辑为公共方法requirePositiveTotalHours
3. 优化评分可见性判断逻辑,移除softwareLeaderId依赖
4. 清理冗余的softwareLeaderId相关字段与校验逻辑
5. 调整接口权限校验与异常提示信息
dev
zch 5 days ago
parent 78e1d7c28f
commit 39761d3fad

@ -256,13 +256,15 @@ public class ErpTempTaskController extends BaseController {
*/
/**
* "待领导审核"
* submitFinish leader_final taskStatus=34
* submitFinish leader_final
*/
@SaCheckPermission("oa/erp:tempTask:list")
@SaCheckPermission("oa/erp:tempTask:edit")
@PostMapping("/syncState/{tempTaskId}")
public R<Void> syncState(@NotNull(message = "主键不能为空")
@PathVariable("tempTaskId") Long tempTaskId) {
erpTempTaskService.syncStateToPendingFinal(tempTaskId);
@PathVariable("tempTaskId") Long tempTaskId,
@NotNull(message = "流程任务ID不能为空")
@RequestParam("taskId") Long taskId) {
erpTempTaskService.syncStateToPendingFinal(tempTaskId, taskId);
return R.ok();
}

@ -107,12 +107,13 @@ public interface IErpTempTaskService {
Boolean leaderReviewAndComplete(ErpTempTaskBo bo);
/**
* leader_final taskStatus 3
* totalHours + + taskStatus4()
* leader_final
* totalHours + + taskStatus4()
*
* @param tempTaskId ID
* @param taskId ID
*/
void syncStateToPendingFinal(Long tempTaskId);
void syncStateToPendingFinal(Long tempTaskId, Long taskId);
/**
* +

@ -251,20 +251,23 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
}
Long uid = LoginHelper.getUserId();
// Why构建 leaderMaptaskId→softwareLeaderId用于评分可见性判断。
Map<Long, Long> leaderMap = new HashMap<>(taskIds.size());
for (ErpTempTaskVo record : records) {
leaderMap.put(record.getTempTaskId(), record.getSoftwareLeaderId());
}
// Why一次 DB 查询拉取所有评分明细,在应用层循环中进行**评分可见性过滤**
// 领导(softwareLeaderId)看全部评分被评分人自己只看到自己被评的分数其他人看不到任何评分记录AD-16
// 评分人(scorerId)看全部评分被评分人自己只看到自己被评的分数其他人看不到任何评分记录AD-16
// 评分人由 Warm-Flow leader_final 节点的 permissionFlag 硬编码决定,业务代码不存 softwareLeaderId。
// 此处 inline 过滤而非调用 listVisibleScore避免对每条记录再次查 DB。
List<ErpTempTaskScoreVo> scores = scoreMapper.selectVoList(Wrappers.<ErpTempTaskScore>lambdaQuery()
.in(ErpTempTaskScore::getTempTaskId, taskIds));
// Why从评分记录的 scorerId 推导"谁是评分人"——评分人能看到该任务下全部评分。
Set<Long> scorerTaskIds = new HashSet<>();
for (ErpTempTaskScoreVo score : scores) {
if (Objects.equals(uid, score.getScorerId())) {
scorerTaskIds.add(score.getTempTaskId());
}
}
Map<Long, List<ErpTempTaskScoreVo>> scoreMap = new HashMap<>(taskIds.size());
for (ErpTempTaskScoreVo score : scores) {
// Why可见性隔离——只有领导(softwareLeaderId)或被评分人本人才能看到评分。
boolean visible = Objects.equals(uid, leaderMap.get(score.getTempTaskId())) || Objects.equals(uid, score.getUserId());
// Why可见性隔离——评分人看全部评分,被评分人本人只看自己的,其他人看不到任何评分。
boolean visible = scorerTaskIds.contains(score.getTempTaskId()) || Objects.equals(uid, score.getUserId());
if (!visible) {
continue;
}
@ -596,9 +599,6 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
&& bo.getPlanEndTime().before(bo.getPlanStartTime())) {
throw new ServiceException("计划完成时间不能早于计划开始时间");
}
if (bo.getSoftwareLeaderId() == null) {
throw new ServiceException("软件部领导不能为空");
}
}
/**
@ -639,7 +639,6 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
putIfPresent(variables, "planStartTime", firstNonNull(bo.getPlanStartTime(), sourceVariables.get("planStartTime")));
putIfPresent(variables, "planEndTime", firstNonNull(bo.getPlanEndTime(), sourceVariables.get("planEndTime")));
putIfPresent(variables, "assigneeId", firstNonNull(bo.getAssigneeId(), sourceVariables.get("assigneeId")));
putIfPresent(variables, "softwareLeaderId", firstNonNull(bo.getSoftwareLeaderId(), sourceVariables.get("softwareLeaderId")));
putIfPresent(variables, "estimateWorkload", firstNonNull(bo.getEstimateWorkload(), sourceVariables.get("estimateWorkload")));
putIfPresent(variables, "assigneeOpinion", firstNonNull(bo.getAssigneeOpinion(), sourceVariables.get("assigneeOpinion")));
putIfPresent(variables, "leaderOpinion", firstNonNull(bo.getLeaderOpinion(), sourceVariables.get("leaderOpinion")));
@ -800,9 +799,6 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
if (task == null) {
throw new ServiceException("临时任务不存在");
}
if (!Objects.equals(LoginHelper.getUserId(), task.getSoftwareLeaderId())) {
throw new ServiceException("只有软件部领导可以审批调整临时任务");
}
if (bo.getAssigneeId() == null) {
throw new ServiceException("进入执行前必须指定主执行人");
}
@ -884,8 +880,6 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
task.setRealRequesterName(bo.getRealRequesterName());
task.setRealRequestDeptId(bo.getRealRequestDeptId());
task.setRealRequestDeptName(bo.getRealRequestDeptName());
task.setSoftwareLeaderId(bo.getSoftwareLeaderId());
task.setSoftwareLeaderName(bo.getSoftwareLeaderName());
task.setAssigneeId(bo.getAssigneeId());
task.setAssigneeName(bo.getAssigneeName());
// task.setCcUserIds(bo.getCcUserIds());
@ -957,8 +951,6 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
bo.setRealRequesterName(task.getRealRequesterName());
bo.setRealRequestDeptId(task.getRealRequestDeptId());
bo.setRealRequestDeptName(task.getRealRequestDeptName());
bo.setSoftwareLeaderId(task.getSoftwareLeaderId());
bo.setSoftwareLeaderName(task.getSoftwareLeaderName());
bo.setAssigneeId(task.getAssigneeId());
bo.setAssigneeName(task.getAssigneeName());
bo.setEstimateWorkload(task.getEstimateWorkload());
@ -1019,7 +1011,13 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
throw new ServiceException("主执行人不可移除,请通过流程换人");
}
ErpTempTask task = baseMapper.selectById(member.getTempTaskId());
if (task != null && !Objects.equals(LoginHelper.getUserId(), task.getAssigneeId())) {
if (task == null) {
throw new ServiceException("临时任务不存在");
}
if (!STATUS_RUNNING.equals(task.getTaskStatus())) {
throw new ServiceException("仅执行中任务可移除协作人");
}
if (!Objects.equals(LoginHelper.getUserId(), task.getAssigneeId())) {
throw new ServiceException("只有主执行人可以移除协作人");
}
return memberMapper.deleteById(memberId) > 0;
@ -1130,10 +1128,7 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
}
// Why提交前必须累计工时>0至少一条工时明细。空提交会导致报表统计失去数据基础AD-14
// sumHoursByTask 使用数据库 SUM 聚合,避免应用层循环累加的精度/并发问题。
BigDecimal total = worklogMapper.sumHoursByTask(tempTaskId);
if (total == null || total.compareTo(BigDecimal.ZERO) <= 0) {
throw new ServiceException("提交完成前任务累计总工时必须大于0且至少有一条工时明细");
}
BigDecimal total = requirePositiveTotalHours(tempTaskId);
// WhytotalHours 作为提交快照固化到主表,后续评分/报表直接读主表字段无需再聚合明细AD-14
task.setTotalHours(total);
// WhytaskStatus=4(待领导审核) 是提交后的"临时冻结态"整单工时只读不可改AD-15
@ -1173,19 +1168,27 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
* "待领导审核"(taskStatus=4)
*
* Why submitFinish leader_final
* taskStatus 3()
* submitFinish totalHours + + taskStatus4
* taskStatus 4 totalHours
* totalHours + + taskStatus4
*
* STATUS_RUNNING(3) STATUS_PENDING_FINAL(4) (5)
*/
* STATUS_RUNNING(3)/STATUS_PENDING_FINAL(4) leader_final
*/
@Override
public void syncStateToPendingFinal(Long tempTaskId) {
@Transactional(rollbackFor = Exception.class)
public void syncStateToPendingFinal(Long tempTaskId, Long taskId) {
validateWorkflowTaskBelongsToBusiness(tempTaskId, taskId, "leader_final", LoginHelper.getUserId());
ErpTempTask task = baseMapper.selectById(tempTaskId);
if (task == null || !STATUS_RUNNING.equals(task.getTaskStatus())) {
return;
if (task == null) {
throw new ServiceException("临时任务不存在");
}
BigDecimal total = worklogMapper.sumHoursByTask(tempTaskId);
task.setTotalHours(total != null ? total : BigDecimal.ZERO);
if (!STATUS_RUNNING.equals(task.getTaskStatus()) && !STATUS_PENDING_FINAL.equals(task.getTaskStatus())) {
throw new ServiceException("当前任务状态不允许同步至待领导审核");
}
BigDecimal total = task.getTotalHours();
if (total == null || total.compareTo(BigDecimal.ZERO) <= 0) {
total = requirePositiveTotalHours(tempTaskId);
}
task.setTotalHours(total);
task.setTaskStatus(STATUS_PENDING_FINAL);
worklogMapper.lockByTask(tempTaskId, "1");
baseMapper.updateById(task);
@ -1203,18 +1206,12 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
if (task == null) {
throw new ServiceException("临时任务不存在");
}
if (!Objects.equals(LoginHelper.getUserId(), task.getSoftwareLeaderId())) {
throw new ServiceException("只有软件部领导可以评分关闭");
// Why评分关闭只能消费已提交完成形成的冻结快照不能在执行中直接评分关闭。
if (!STATUS_PENDING_FINAL.equals(task.getTaskStatus())) {
throw new ServiceException("仅待领导审核状态可评分关闭,请先提交完成");
}
// Why正常情况下 taskStatus 应由 submitFinish 置为 4(待领导审核),但执行人可能绕过 submitFinish
// 直接通过工作流通用审批按钮将流程推到 leader_final此时 taskStatus 仍为 3(执行中)。
// 对于 taskStatus=3 但流程已在 leader_final 的情况,此处兜底补齐 submitFinish 的落库动作。
if (STATUS_RUNNING.equals(task.getTaskStatus())) {
BigDecimal total = worklogMapper.sumHoursByTask(bo.getTempTaskId());
task.setTotalHours(total != null ? total : BigDecimal.ZERO);
worklogMapper.lockByTask(bo.getTempTaskId(), "1");
} else if (!STATUS_PENDING_FINAL.equals(task.getTaskStatus())) {
throw new ServiceException("仅待领导审核或执行中状态可评分关闭");
if (task.getTotalHours() == null || task.getTotalHours().compareTo(BigDecimal.ZERO) <= 0) {
throw new ServiceException("任务未形成有效工时提交快照,不能评分关闭");
}
// Why必须对全部参与人(主执行人+协作人)都给出评分等级不允许遗漏任何人。漏评会导致考核不公AD-16
List<ErpTempTaskMember> members = memberMapper.selectList(Wrappers.<ErpTempTaskMember>lambdaQuery()
@ -1248,6 +1245,14 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
return Boolean.TRUE;
}
private BigDecimal requirePositiveTotalHours(Long tempTaskId) {
BigDecimal total = worklogMapper.sumHoursByTask(tempTaskId);
if (total == null || total.compareTo(BigDecimal.ZERO) <= 0) {
throw new ServiceException("提交完成前任务累计总工时必须大于0且至少有一条工时明细");
}
return total;
}
private void validateScoreCoverage(ErpTempTaskScoreSubmitBo bo, List<ErpTempTaskMember> members) {
if (members == null || members.isEmpty()) {
throw new ServiceException("任务参与人不存在,无法评分关闭");
@ -1302,12 +1307,13 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
/**
* Why taskId
* Warm-Flow flowHisTaskList businessId
* **线** taskId nodeCode approver
* **线** taskId flowStatusnodeCode approver
* 线 Map 10O(n)
*
* 1. taskId taskId
* 2. nodeCode leader_review execute
* 3. approver
* 1. taskId taskId
* 2. flowStatus waiting taskId
* 3. nodeCode leader_review execute
* 4. approver
* ServiceException @GlobalTransactional Seata
*/
@SuppressWarnings("unchecked")
@ -1332,6 +1338,10 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
if (!Objects.equals(taskIdText, Convert.toStr(taskMap.get("id")))) {
continue;
}
// Why只允许当前待办任务驱动业务动作。历史已办 taskId 即便 businessId/nodeCode/approver 匹配,也不能重放同步或审批。
if (!Objects.equals(BusinessStatusEnum.WAITING.getStatus(), Convert.toStr(taskMap.get("flowStatus")))) {
throw new ServiceException("流程任务不是当前待办,不能执行该业务操作");
}
// WhynodeCode 精确匹配——例如 scoreAndClose 要求 nodeCode="leader_final"
// submitFinish 要求 nodeCode="execute",防止在错误节点调用业务接口。
if (StringUtils.isNotBlank(expectedNodeCode)
@ -1361,13 +1371,18 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
*/
@Override
public List<ErpTempTaskScoreVo> listVisibleScore(Long tempTaskId) {
ErpTempTask task = baseMapper.selectById(tempTaskId);
if (task == null) {
if (tempTaskId == null) {
return Collections.emptyList();
}
Long uid = LoginHelper.getUserId();
// Why评分人由 Warm-Flow leader_final 节点 permissionFlag 硬编码决定,业务代码不存 softwareLeaderId。
// 判断当前用户是否为评分人:查该任务下是否存在 scorerId == uid 的评分记录。
// 评分人看全部评分被评分人只能看自己被评的分数AD-16
boolean isScorer = scoreMapper.selectCount(Wrappers.<ErpTempTaskScore>lambdaQuery()
.eq(ErpTempTaskScore::getTempTaskId, tempTaskId)
.eq(ErpTempTaskScore::getScorerId, uid)) > 0;
List<ErpTempTaskScoreVo> list;
if (Objects.equals(uid, task.getSoftwareLeaderId())) {
if (isScorer) {
list = scoreMapper.selectVoList(Wrappers.<ErpTempTaskScore>lambdaQuery()
.eq(ErpTempTaskScore::getTempTaskId, tempTaskId));
} else {

Loading…
Cancel
Save