|
|
|
|
@ -251,20 +251,23 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Long uid = LoginHelper.getUserId();
|
|
|
|
|
// Why:构建 leaderMap(taskId→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);
|
|
|
|
|
// Why:totalHours 作为提交快照固化到主表,后续评分/报表直接读主表字段,无需再聚合明细(AD-14)。
|
|
|
|
|
task.setTotalHours(total);
|
|
|
|
|
// Why:taskStatus=4(待领导审核) 是提交后的"临时冻结态",整单工时只读不可改(AD-15)。
|
|
|
|
|
@ -1173,19 +1168,27 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
|
|
|
|
|
* 兜底同步任务状态至"待领导审核"(taskStatus=4)。
|
|
|
|
|
*
|
|
|
|
|
* Why:执行人可能绕过 submitFinish 直接通过工作流通用审批按钮将流程推到 leader_final,
|
|
|
|
|
* 此时 taskStatus 仍为 3(执行中)。本方法在编辑页加载时被前端主动调起,
|
|
|
|
|
* 补齐 submitFinish 缺失的聚合 totalHours + 锁定工时明细 + taskStatus→4。
|
|
|
|
|
* 或流程事件先把 taskStatus 置为 4 但未形成 totalHours 快照。本方法在编辑页加载时被前端主动调起,
|
|
|
|
|
* 补齐聚合 totalHours + 锁定工时明细 + taskStatus→4。
|
|
|
|
|
*
|
|
|
|
|
* 幂等保证:仅 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,找到后校验 flowStatus、nodeCode 和 approver 是否与预期一致。
|
|
|
|
|
* 线性搜索而非 Map 的原因是:流程任务列表通常较短(10条以内),O(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("流程任务不是当前待办,不能执行该业务操作");
|
|
|
|
|
}
|
|
|
|
|
// Why:nodeCode 精确匹配——例如 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 {
|
|
|
|
|
|