refactor(oa-erp): 优化临时任务模块代码与权限配置

- 重构所有实体类注释,补充业务定位、核心设计决策与规则依据
- 修复权限标识格式,将冒号改为斜杠分隔符合规范
- 新增syncStateToPendingFinal方法,用于兜底同步任务状态至待领导审核
- 优化批量查询明细逻辑,解决N+1查询问题
- 补充流程事件监听与状态流转的防御性逻辑
- 完善权限校验与幂等性处理逻辑
dev
zch 5 days ago
parent 9d529c00b1
commit 78e1d7c28f

@ -48,7 +48,7 @@ public class ErpTempTaskController extends BaseController {
/**
*
*/
@SaCheckPermission("oa:erp:tempTask:list")
@SaCheckPermission("oa/erp:tempTask:list")
@GetMapping("/list")
public TableDataInfo<ErpTempTaskVo> list(ErpTempTaskBo bo, PageQuery pageQuery) {
return erpTempTaskService.queryPageList(bo, pageQuery);
@ -57,7 +57,7 @@ public class ErpTempTaskController extends BaseController {
/**
*
*/
@SaCheckPermission("oa:erp:tempTask:export")
@SaCheckPermission("oa/erp:tempTask:export")
@Log(title = "软件部临时任务主", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(ErpTempTaskBo bo, HttpServletResponse response) {
@ -70,7 +70,7 @@ public class ErpTempTaskController extends BaseController {
*
* @param tempTaskId
*/
@SaCheckPermission("oa:erp:tempTask:query")
@SaCheckPermission("oa/erp:tempTask:query")
@GetMapping("/{tempTaskId}")
public R<ErpTempTaskVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable("tempTaskId") Long tempTaskId) {
@ -80,7 +80,7 @@ public class ErpTempTaskController extends BaseController {
/**
*
*/
@SaCheckPermission("oa:erp:tempTask:add")
@SaCheckPermission("oa/erp:tempTask:add")
@Log(title = "软件部临时任务主", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
@ -91,7 +91,7 @@ public class ErpTempTaskController extends BaseController {
/**
*
*/
@SaCheckPermission("oa:erp:tempTask:edit")
@SaCheckPermission("oa/erp:tempTask:edit")
@Log(title = "软件部临时任务主", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
@ -104,7 +104,7 @@ public class ErpTempTaskController extends BaseController {
*
* @param tempTaskIds
*/
@SaCheckPermission("oa:erp:tempTask:remove")
@SaCheckPermission("oa/erp:tempTask:remove")
@Log(title = "软件部临时任务主", businessType = BusinessType.DELETE)
@DeleteMapping("/{tempTaskIds}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@ -124,7 +124,7 @@ public class ErpTempTaskController extends BaseController {
/**
*
*/
@SaCheckPermission("oa:erp:tempTask:submit")
@SaCheckPermission("oa/erp:tempTask:edit")
@Log(title = "软件部临时任务提交审批", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PostMapping("/submit")
@ -135,7 +135,7 @@ public class ErpTempTaskController extends BaseController {
/**
* AD-12
*/
@SaCheckPermission("oa:erp:tempTask:edit")
@SaCheckPermission("oa/erp:tempTask:edit")
@Log(title = "临时任务主执行人审阅", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PostMapping("/assigneeReview")
@ -146,7 +146,7 @@ public class ErpTempTaskController extends BaseController {
/**
* AD-12
*/
@SaCheckPermission("oa:erp:tempTask:edit")
@SaCheckPermission("oa/erp:tempTask:edit")
@Log(title = "临时任务领导审批变量", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PostMapping("/leaderReviewVariables")
@ -157,7 +157,7 @@ public class ErpTempTaskController extends BaseController {
/**
*
*/
@SaCheckPermission("oa:erp:tempTask:edit")
@SaCheckPermission("oa/erp:tempTask:edit")
@Log(title = "临时任务领导审批", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PostMapping("/leaderReviewComplete")
@ -168,7 +168,7 @@ public class ErpTempTaskController extends BaseController {
/**
* +
*/
@SaCheckPermission("oa:erp:tempTask:query")
@SaCheckPermission("oa/erp:tempTask:query")
@GetMapping("/{tempTaskId}/members")
public R<List<ErpTempTaskMemberVo>> members(@NotNull(message = "主键不能为空")
@PathVariable("tempTaskId") Long tempTaskId) {
@ -178,7 +178,7 @@ public class ErpTempTaskController extends BaseController {
/**
* AD-13
*/
@SaCheckPermission("oa:erp:tempTask:worklog")
@SaCheckPermission("oa/erp:tempTask:edit")
@Log(title = "临时任务添加协作人", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping("/member")
@ -189,7 +189,7 @@ public class ErpTempTaskController extends BaseController {
/**
*
*/
@SaCheckPermission("oa:erp:tempTask:worklog")
@SaCheckPermission("oa/erp:tempTask:edit")
@Log(title = "临时任务移除协作人", businessType = BusinessType.DELETE)
@DeleteMapping("/member/{memberId}")
public R<Void> removeMember(@NotNull(message = "参与人ID不能为空")
@ -200,7 +200,7 @@ public class ErpTempTaskController extends BaseController {
/**
* AD-07~AD-09
*/
@SaCheckPermission("oa:erp:tempTask:query")
@SaCheckPermission("oa/erp:tempTask:query")
@GetMapping("/{tempTaskId}/worklogs")
public R<List<ErpTempTaskWorklogVo>> worklogs(@NotNull(message = "主键不能为空")
@PathVariable("tempTaskId") Long tempTaskId) {
@ -210,7 +210,7 @@ public class ErpTempTaskController extends BaseController {
/**
* /
*/
@SaCheckPermission("oa:erp:tempTask:worklog")
@SaCheckPermission("oa/erp:tempTask:edit")
@Log(title = "临时任务工时明细", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PostMapping("/worklog")
@ -221,7 +221,7 @@ public class ErpTempTaskController extends BaseController {
/**
*
*/
@SaCheckPermission("oa:erp:tempTask:worklog")
@SaCheckPermission("oa/erp:tempTask:edit")
@Log(title = "临时任务删除工时明细", businessType = BusinessType.DELETE)
@DeleteMapping("/worklog/{worklogId}")
public R<Void> delWorklog(@NotNull(message = "工时明细ID不能为空")
@ -232,7 +232,7 @@ public class ErpTempTaskController extends BaseController {
/**
* AD-14/AD-15
*/
@SaCheckPermission("oa:erp:tempTask:submitFinish")
@SaCheckPermission("oa/erp:tempTask:edit")
@Log(title = "临时任务提交完成", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PostMapping("/submitFinish")
@ -243,7 +243,7 @@ public class ErpTempTaskController extends BaseController {
/**
* AD-05/AD-16
*/
@SaCheckPermission("oa:erp:tempTask:score")
@SaCheckPermission("oa/erp:tempTask:edit")
@Log(title = "临时任务评分关闭", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PostMapping("/scoreAndClose")
@ -254,7 +254,19 @@ public class ErpTempTaskController extends BaseController {
/**
* AD-16
*/
@SaCheckPermission("oa:erp:tempTask:query")
/**
* "待领导审核"
* submitFinish leader_final taskStatus=34
*/
@SaCheckPermission("oa/erp:tempTask:list")
@PostMapping("/syncState/{tempTaskId}")
public R<Void> syncState(@NotNull(message = "主键不能为空")
@PathVariable("tempTaskId") Long tempTaskId) {
erpTempTaskService.syncStateToPendingFinal(tempTaskId);
return R.ok();
}
@SaCheckPermission("oa/erp:tempTask:query")
@GetMapping("/{tempTaskId}/scores")
public R<List<ErpTempTaskScoreVo>> scores(@NotNull(message = "主键不能为空")
@PathVariable("tempTaskId") Long tempTaskId) {

@ -15,7 +15,25 @@ import java.util.Date;
/**
* erp_temp_task
*
*
* <p>/
* +
* //</p>
*
* <h3> soft.md AD-01~AD-16 18</h3>
* <ul>
* <li>AD-01 / 1// OATT </li>
* <li>taskStatus+ flowStatus2-5 / 12</li>
* <li>AD-10 / 11</li>
* <li> + AD-13 / 8 / 15</li>
* <li>/AD-06 / 6</li>
* <li> totalHours AD-04 / AD-14 / 4 / 16</li>
* <li>AD-03 / AD-15 / 3 / 17</li>
* </ul>
*
* <p> ErpContractInfoServiceImpl / ErpProjectInfoServiceImpl
* flowCode='OATT' submitAndFlowStart + @EventListener processHandler
* 使 FlowConfigEnum / WorkflowStrategy / OaProcessEventHandler </p>
*
* @author zch
* @date 2026-06-17
@ -29,88 +47,117 @@ public class ErpTempTask extends TenantEntity {
private static final long serialVersionUID = 1L;
/**
* ID
* ID businessId
* Why Warm-Flow
*/
@TableId(value = "temp_task_id", type = IdType.ASSIGN_ID)
private Long tempTaskId;
/**
* LSRW+yyyyMMdd+4稿
* LSRW+yyyyMMdd+4稿
* Why便
* codeRuleService
*/
private String tempTaskCode;
/**
*
*
* Why
*/
private String taskTitle;
/**
*
*
* Why
*
*/
private String taskDesc;
/**
* 1 2 3AD-01
* 1= / 2= / 3=
* WhyAD-01 / 1 OATT
* projectId deptId
* //
*/
private String taskType;
/**
* /
*
* WhyAD-06 / 6
* actualStartTime/actualFinishTime vs //
*/
private Date planStartTime;
/**
* /
*
* WhyAD-06 / 6 planStartTime actualFinishTime
* "未到结束日不得提交完成"
*/
private Date planEndTime;
/**
*
*
* Why workDate actualStartTime
* workDate
*/
private Date actualStartTime;
/**
* /
*
* WhyAD-05 / 5 actualFinishTime
* planEndTime //AD-04 / 4
*/
private Date actualFinishTime;
/**
* ID
* ID=3 =1/2 null
* WhyAD-01=/
* "悬空"
*/
private Long projectId;
/**
*
*
* Why//
*
*/
private String projectCode;
/**
*
* projectCode
*/
private String projectName;
/**
* ID
* ID=1
* WhyAD-01realRequestDeptId
*
*/
private Long deptId;
/**
*
*
*/
private String deptName;
/**
* ID
* ID"谁创建并提交了流程"
* WhyAD-10 / 11 realRequesterId
* requesterId realRequesterId
*
*/
private Long requesterId;
/**
*
*
*/
private String requesterName;
/**
* ID
* ID
* WhyAD-10 / 11"谁操作发起""谁需要任务"//
* requesterId=()realRequesterId=() requesterId==realRequesterId
*
*/
private Long realRequesterId;
@ -120,7 +167,9 @@ public class ErpTempTask extends TenantEntity {
private String realRequesterName;
/**
* ID
* ID
* WhyAD-10 / 11
* realRequestDeptId=IDID
*/
private Long realRequestDeptId;
@ -130,75 +179,105 @@ public class ErpTempTask extends TenantEntity {
private String realRequestDeptName;
/**
* /ID
* /ID
* Whyleader_review+leader_final+
* ${softwareLeaderId}
*/
private Long softwareLeaderId;
/**
*
*
*/
private String softwareLeaderName;
/**
* ID
* ID
* WhyAD-13 / 8 assignee_review execute
* member_type=2
*
*/
private Long assigneeId;
/**
*
*
*/
private String assigneeName;
/**
*
* @TableField(exist=false)
* Whyassignee_review Warm-Flow
* /
*/
@TableField(exist = false)
private String assigneeOpinion;
/**
*
* @TableField(exist=false)
* Whyleader_review "通过/换人/驳回"
* 便
*/
@TableField(exist = false)
private String leaderOpinion;
/**
*
* @TableField(exist=false)
* Whyleader_final
*
*/
@TableField(exist = false)
private String leaderFinalOpinion;
/**
* //
* //
* Why vs worklog
*
*/
private BigDecimal estimateWorkload;
/**
* +AD-14
* + worklog
* WhyAD-14 / 16 erp_temp_task_worklog
* totalHours>0AD-14 SUM
* 使 worklog
*/
private BigDecimal totalHours;
/**
* 1 2 3 4 5 6
* 1= / 2= / 3= / 4= / 5= / 6=
* Why2-5 / 17 flowStatus
* (1)稿flowStatus=draft
* (2)flowStatus=waiting
* (3)flowStatus=waiting
* (4)/flowStatus=waitingAD-15 / 17
* (5)flowStatus=finishAD-04/AD-05 / 4/5
* (6)//flowStatus=invalid/termination/cancel
*
*/
private String taskStatus;
/**
* draft/waiting/finish/back/cancel/invalid/termination
* draft / waiting / finish / back / cancel / invalid / terminationWarm-Flow
* Why12 @EventListener processHandler Warm-Flow ProcessEvent
*
* taskStatus waiting taskStatus //
*
*/
private String flowStatus;
/**
* ID
* ID Warm-Flow
* WhyAD-11 / 12Warm-Flow CopySettingEnum
*
*/
private String ccUserIds;
/**
*
* 线
*/
private String remark;
/**
* 0 1
* 0= / 1=MyBatis-Plus @TableLogic
*/
@TableLogic
private String delFlag;

@ -12,7 +12,18 @@ import java.io.Serial;
/**
* erp_temp_task_member
* Why member_type /AD-13
*
* <p> member_type 12//
* AD-13 / 8 / 15</p>
*
* <h3></h3>
* <ul>
* <li>memberType=1 assignee_review execute </li>
* <li>memberType=2</li>
* <li>uk_ttm_task_user </li>
* <li>AD-15 / 17</li>
* <li>userName/memberDeptId /</li>
* </ul>
*
* @author zch
* @date 2026-06-22
@ -26,43 +37,53 @@ public class ErpTempTaskMember extends TenantEntity {
private static final long serialVersionUID = 1L;
/**
* IDID
* IDIDerp_temp_task_worklog.member_id erp_temp_task_score.member_id
*/
@TableId(value = "member_id", type = IdType.ASSIGN_ID)
private Long memberId;
/**
* ID
* ID erp_temp_task.temp_task_id
*/
private Long tempTaskId;
/**
* 1 2
* 1= / 2=
* WhyAD-13 / 8 / 15memberType=1 assignee_review
* execute
* memberType=2
* erp_temp_task.assigneeId 使 assigneeId member
*/
private String memberType;
/**
* ID
* ID sys_user
* Whyworklog score member
*/
private Long userId;
/**
*
*
* Why使
*
*/
private String userName;
/**
* ID
* ID
*/
private Long memberDeptId;
/**
*
*
* Why
*
*/
private String joinRemark;
/**
* 0 1
* 0= / 1=MyBatis-Plus @TableLogic
* Why
*/
@TableLogic
private String delFlag;

@ -13,8 +13,19 @@ import java.util.Date;
/**
* erp_temp_task_score
* Why
* AD-16
*
* <p> leader_final
* temp_task_scoreA++/A+/A/B/CAD-05 / 5
* AD-16 / 18</p>
*
* <h3></h3>
* <ul>
* <li>uk_tts_task_user </li>
* <li>A++ / A+ / A / B / C便</li>
* <li> scorerId== OR userId== </li>
* <li>scoreTime==actualFinishTime </li>
* <li>C 使 C</li>
* </ul>
*
* @author zch
* @date 2026-06-22
@ -28,48 +39,61 @@ public class ErpTempTaskScore extends TenantEntity {
private static final long serialVersionUID = 1L;
/**
* IDID
* IDID
*/
@TableId(value = "score_id", type = IdType.ASSIGN_ID)
private Long scoreId;
/**
* ID
* ID erp_temp_task.temp_task_id
*/
private Long tempTaskId;
/**
* ID
* ID erp_temp_task_member.member_id
* Why member memberType
*/
private Long memberId;
/**
* ID
* ID
* WhyAD-16 / 18
* scorerId==
* userId==
*/
private Long userId;
/**
* A++ A+ A B Ctemp_task_score
* A++ / A+ / A / B / C temp_task_score
* WhyAD-16 / 18
* A++=A+=A=
* B=C=
* 使 C
*/
private String scoreGrade;
/**
*
*
* Why便
*/
private String scoreRemark;
/**
* ID
* ID erp_temp_task.softwareLeaderId
* WhyAD-16
* userId scorerId userId
*/
private Long scorerId;
/**
*
*
* WhyAD-05 / 5scoreTime erp_temp_task.actualFinishTime
* //AD-04 / 4
*/
private Date scoreTime;
/**
* 0 1
* 0= / 1=MyBatis-Plus @TableLogic
*/
@TableLogic
private String delFlag;

@ -14,8 +14,20 @@ import java.util.Date;
/**
* erp_temp_task_worklog
* Why(week_start)+/
* 便AD-07~AD-09
*
* <p>//
* AD-07 / 7/AD-08 / 8/9
* AD-09 / 10AD-03 / 3
* AD-15/AD-04 / 17/4</p>
*
* <h3></h3>
* <ul>
* <li>weekStart/weekEnd workDate ~</li>
* <li>workDate+workContent </li>
* <li>ossId //</li>
* <li>lockFlag =0 =1 =1</li>
* <li>0.50</li>
* </ul>
*
* @author zch
* @date 2026-06-22
@ -29,63 +41,82 @@ public class ErpTempTaskWorklog extends TenantEntity {
private static final long serialVersionUID = 1L;
/**
* IDID
* IDID
*/
@TableId(value = "worklog_id", type = IdType.ASSIGN_ID)
private Long worklogId;
/**
* ID
* ID erp_temp_task.temp_task_id
*/
private Long tempTaskId;
/**
* IDerp_temp_task_member.member_id
* ID erp_temp_task_member.member_id
* Why member memberType /
* 16"任务总工时 + 按人员拆分"
*/
private Long memberId;
/**
* ID便
* ID member.userId
* Why member userId
*
*/
private Long userId;
/**
*
*
* WhyAD-07 / 7 workDate
*
*/
private Date weekStart;
/**
*
* weekStart 便
*/
private Date weekEnd;
/**
*
*
* WhyAD-08 / 9
* (task,user,work_date)
*/
private Date workDate;
/**
* /0.5
* /0.50.5
* Why
* //
*/
private BigDecimal hours;
/**
* /
* /
* Why"某天做了什么事,花了多少工时,有附件佐证"
*
*/
private String workContent;
/**
* ID
* ID MinIO/S3
* WhyAD-09 / 10
* 便
* AD-04 / 4
*/
private String ossId;
/**
* 0 1
* 0= / 1=
* WhyAD-03 / AD-15 / AD-04 / 3 / 17 / 4
* 0//11
* 1011
* saveWorklog/updateWorklog/delWorklog lockFlag=0 taskStatus=3
*/
private String lockFlag;
/**
* 0 1
* 0= / 1=MyBatis-Plus @TableLogic
*/
@TableLogic
private String delFlag;

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

@ -208,10 +208,17 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
return params != null && Convert.toBool(params.get("includeDetail"), false);
}
/**
* Why//
* queryN+1 taskIds IN(...) DB
* Map record DB
* listVisibleScore DB
*/
private void fillDetailRows(List<ErpTempTaskVo> records) {
if (records == null || records.isEmpty()) {
return;
}
// Why提取所有 taskId作为批量 IN 查询的 key 集合。
List<Long> taskIds = records.stream()
.map(ErpTempTaskVo::getTempTaskId)
.filter(Objects::nonNull)
@ -220,6 +227,7 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
return;
}
// Why一次 DB 查询拉取所有记录的参与人列表,构建 memberMap(按 taskId 分组)和 memberNameMap(按 memberId 映射姓名)。
List<ErpTempTaskMemberVo> members = memberMapper.selectVoList(Wrappers.<ErpTempTaskMember>lambdaQuery()
.in(ErpTempTaskMember::getTempTaskId, taskIds)
.orderByAsc(ErpTempTaskMember::getMemberType));
@ -230,6 +238,7 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
memberNameMap.computeIfAbsent(member.getTempTaskId(), key -> new HashMap<>()).put(member.getMemberId(), member.getUserName());
}
// Why一次 DB 查询拉取所有工时明细,按 taskId 分组并回填填报人姓名(从 memberNameMap 取)。
List<ErpTempTaskWorklogVo> worklogs = worklogMapper.selectVoList(Wrappers.<ErpTempTaskWorklog>lambdaQuery()
.in(ErpTempTaskWorklog::getTempTaskId, taskIds)
.orderByAsc(ErpTempTaskWorklog::getWeekStart)
@ -242,14 +251,19 @@ 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
// 此处 inline 过滤而非调用 listVisibleScore避免对每条记录再次查 DB。
List<ErpTempTaskScoreVo> scores = scoreMapper.selectVoList(Wrappers.<ErpTempTaskScore>lambdaQuery()
.in(ErpTempTaskScore::getTempTaskId, taskIds));
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());
if (!visible) {
continue;
@ -259,6 +273,7 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
scoreMap.computeIfAbsent(score.getTempTaskId(), key -> new ArrayList<>()).add(score);
}
// Why将已分组的数据 set 到每条 VO 上,供前端直接渲染。
for (ErpTempTaskVo record : records) {
Long taskId = record.getTempTaskId();
record.setMembers(memberMap.getOrDefault(taskId, Collections.emptyList()));
@ -325,22 +340,23 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
* @return
*/
@Override
// Why@GlobalTransactional 保证业务数据保存→流程发起→办结首任务节点在同一 Seata 分布式事务内原子提交或整体回滚,
// 避免出现”业务数据已入库但流程实例未创建”或”流程实例已创建但业务数据回滚”这类半一致状态。
@GlobalTransactional(rollbackFor = Exception.class)
public ErpTempTaskVo tempTaskSubmitAndFlowStart(ErpTempTaskBo bo) {
// 1. 发起提交前校验:任务描述与需求时间非空,且紧急优先级必须有紧急原因
validBeforeSubmit(bo);
// 2. 补齐草稿字段,如果是首次直接提交则在此节点生成 LSRW 唯一编号
prepareDraft(bo);
// 双状态机制初始化:任务提交后先进入审批中,后续执行/待审核/关闭由业务接口或流程节点显式推进。
// Why双状态机制初始化——taskStatus(2-审批中) 控制前端页面交互与业务语义flowStatus(waiting) 同步 Warm-Flow 引擎,
// 二者独立维护但联动,后续 execute/leader_final/关闭 由业务接口或流程事件精确推进,避免笼统写死。
bo.setTaskStatus(STATUS_APPROVING);
bo.setFlowStatus(BusinessStatusEnum.WAITING.getStatus());
boolean saved;
// 3. 根据 ID 判断是首次直接提交还是基于已有草稿提交,执行相应的落库:
// - 若 tempTaskId 为 null说明用户未存草稿直接在页面点击了"提交审批"并发起流程。
// 需将其转换为实体,进行入库前常规校验并插入数据库生成主键 ID再反写回 BO。
// - 若 tempTaskId 不为 null说明是由已有草稿发起提交或者是流程退回、撤销后重新提交。
// 需校验编辑权限 checkEditable 避免越权与非法篡改,再进行合法性校验并更新数据库记录。
// Why两分支覆盖”直接提交”与”草稿后提交”
// - tempTaskId==null用户未存草稿直接点”提交审批”需先 insert 获取主键 ID 作为流程 businessId
// - tempTaskId!=null基于已有草稿提交或退回/撤销后重新提交,走 update 保留原有 ID但必须先 checkEditable 防止越权篡改。
if (bo.getTempTaskId() == null) {
ErpTempTask add = MapstructUtils.convert(bo, ErpTempTask.class);
validEntityBeforeSave(add);
@ -352,31 +368,54 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
validEntityBeforeSave(update);
saved = baseMapper.updateById(update) > 0;
}
// 如果数据未成功保存或未反写 ID则直接抛出 ServiceException 异常回滚事务,防止没有绑定业务 ID 的“悬空流程实例”产生
// Why保存失败或无 businessId 时立即抛异常触发 Seata 回滚,防止”悬空流程实例”(没有绑定业务数据的流程记录,后续无法闭环)。
if (!saved || bo.getTempTaskId() == null) {
throw new ServiceException("临时任务保存失败,无法发起流程");
}
// V3发起时若已指定主执行人同步参与人表主执行人记录AD-13
// Why发起时若已指定主执行人同步写入参与人表AD-13。放在落库后而非落库前确保 taskId 已生成。
refreshMainAssigneeMember(bo.getTempTaskId(), bo.getAssigneeId(), bo.getAssigneeName());
// 4. 组装 Warm-Flow 远程流程启动对象
RemoteStartProcess startProcess = new RemoteStartProcess();
startProcess.setBusinessId(bo.getTempTaskId().toString());
startProcess.setFlowCode(FLOW_CODE); // 固定编码 OATT
// 构建用于流程走向/办理人解析的最小 variables Map
// Whyvariables 只携带流程路由has_assignee 网关)+ 办理人解析assigneeId/softwareLeaderId/requesterId所需最小字段
// 不将完整业务对象塞入避免流程变量表膨胀并降低序列化风险。ignore=true 仅在发起流程时使用,让引擎自动跳过已办完的申请人首节点。
startProcess.setVariables(buildWorkflowVariables(bo, true));
// 5. 补充待办中心/待办列表所需的业务展示扩展字段,包含编号和拼接后的标题
// WhybizExt 是审批中心/待办列表的展示用快照,存入流程实例表供前端列表渲染,不做流程路由决策。
bo.getBizExt().setBusinessId(startProcess.getBusinessId());
bo.getBizExt().setBusinessCode(bo.getTempTaskCode());
bo.getBizExt().setBusinessTitle(buildBusinessTitle(bo));
startProcess.setBizExt(bo.getBizExt());
// 6. 远程调用工作流服务发起并完成申请人节点
// WhystartCompleteTask 一步完成"创建流程实例 → 自动完成首节点(申请人)",避免申请人看到自己的待办。
// 失败直接抛业务异常触发全局回滚,确保不会产生半拉流程记录。
boolean started = remoteWorkflowService.startCompleteTask(startProcess);
if (!started) {
throw new ServiceException("流程发起异常");
}
// WhyWarm-Flow 的 anyNodeSkip:"submit" 机制——发起人同时是主执行人或软件部领导时,
// 工作流引擎会自动跳过 assignee_review 和/或 leader_review 节点直达 execute。
// 自动跳过完全在引擎内部完成,绕过了 buildLeaderReviewVariables。
// 因此必须在发起后补刀 taskStatus否则前端 canExecute 不会出现。
// 条件:①指定了主执行人 + 发起人==主执行人==领导 → 两节点全跳过 → 直达 execute
// ②未指定主执行人 + 发起人==领导 → 跳过 leader_review → 直达 execute
boolean initiatorIsAssignee = bo.getAssigneeId() != null
&& Objects.equals(bo.getRequesterId(), bo.getAssigneeId());
boolean initiatorIsLeader = Objects.equals(bo.getRequesterId(), bo.getSoftwareLeaderId());
boolean autoPassToExecute = (initiatorIsAssignee && initiatorIsLeader)
|| (bo.getAssigneeId() == null && initiatorIsLeader);
if (autoPassToExecute) {
ErpTempTask task = baseMapper.selectById(bo.getTempTaskId());
if (task != null && !STATUS_RUNNING.equals(task.getTaskStatus())) {
task.setTaskStatus(STATUS_RUNNING);
task.setActualStartTime(new Date());
baseMapper.updateById(task);
}
}
return baseMapper.selectCustomErpTempTaskVoById(bo.getTempTaskId());
}
@ -442,8 +481,9 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
/**
* ()
* 使 `@EventListener` `flow_status` `task_status`
*
* Why使 @EventListener flowCode
* flowStatus + taskStatus
*
*/
@EventListener(condition = "#processEvent.flowCode == 'OATT'")
public void processHandler(ProcessEvent processEvent) {
@ -458,11 +498,14 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
String status = processEvent.getStatus();
String nodeCode = processEvent.getNodeCode();
// 2) 业务状态按"流程状态 + 当前节点"精确映射AD-15避免执行/待审核态被笼统回写成审批中
// WhyWAITING 三路分叉——根据当前节点精确映射业务状态以避免"审批中"笼统覆盖:
// - execute 节点:流程流转到"执行"节点 → taskStatus=3(执行中) 让主执行人可填工时
// - leader_final 节点:流程流转到"领导最终审核" → taskStatus=4(待领导审核) 冻结工时只读
// - 其他审批节点(leader_review/cc):保持 taskStatus=2(审批中),前端只展示审批界面
if (Objects.equals(status, BusinessStatusEnum.WAITING.getStatus())) {
// Why执行节点=执行中(3),领导评分节点=待领导审核(4),其余审批节点=审批中(2)
if ("execute".equals(nodeCode)) {
task.setTaskStatus(STATUS_RUNNING);
// Why首次进入执行节点自动打上实际开始时间戳用于后续提前/按期/延期口径计算AD-06
if (task.getActualStartTime() == null) {
task.setActualStartTime(new Date());
}
@ -472,7 +515,8 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
task.setTaskStatus(STATUS_APPROVING);
}
} else if (Objects.equals(status, BusinessStatusEnum.FINISH.getStatus())) {
// Why流程结束=领导评分关闭scoreAndClose 已显式置5此处兜底防止漏写
// Why流程结束=领导评分关闭scoreAndClose 已显式置 taskStatus=5此处兜底防御
// 防止流程因异常路径如直接流程强制结束FINISH 但 taskStatus 未闭环的情况。
if (!STATUS_CLOSED.equals(task.getTaskStatus())) {
task.setTaskStatus(STATUS_CLOSED);
}
@ -480,9 +524,15 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
|| Objects.equals(status, BusinessStatusEnum.TERMINATION.getStatus())) {
task.setTaskStatus(STATUS_INVALID); // 6 作废
} else if (Objects.equals(status, BusinessStatusEnum.BACK.getStatus())) {
// Why领导最终审核驳回应回执行中并解锁工时不能依赖 Warm-Flow 版本对 nodeCode 源/目标节点的取值差异。
// Why退回分支——根据当前 taskStatus 和 nodeCode 决定回退到哪个状态:
// - STATUS_PENDING_FINAL(=4) 或 nodeCode=execute说明是从"领导最终审核"或"执行"节点退回,
// 应回到 taskStatus=3(执行中) 并 unlock 工时明细,让主执行人继续维护;
// - 其他情况(如从 leader_review 退回):回到 taskStatus=1(暂存) 让发起人重新编辑。
// 注意:不能依赖 Warm-Flow 版本对 nodeCode 源/目标节点的取值差异,
// 需同时以 taskStatus 做辅助判断,确保倒退后的业务状态一致且工时锁释放。
if (STATUS_PENDING_FINAL.equals(task.getTaskStatus()) || "execute".equals(nodeCode)) {
task.setTaskStatus(STATUS_RUNNING);
// Why退回时必须 unlock 工时明细否则提交完成时锁定的工时无法继续填写AD-15
worklogMapper.lockByTask(task.getTempTaskId(), "0");
} else {
task.setTaskStatus(STATUS_DRAFT);
@ -553,22 +603,31 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
/**
* variables Map
* variables OATT (has_assignee/reassigned)(softwareLeaderId/assigneeId)
* Whyvariables OATT (has_assignee/reassigned)(softwareLeaderId/assigneeId)
* BO > bo.sourceVariables()
*/
private Map<String, Object> buildWorkflowVariables(ErpTempTaskBo bo) {
return buildWorkflowVariables(bo, false);
}
/**
* WhyignorePermission (startCompleteTask) true
* "办理人必须是当前登录人"leader_review/execute/leader_final completeTask
* ignore=true
*/
private Map<String, Object> buildWorkflowVariables(ErpTempTaskBo bo, boolean ignorePermission) {
Map<String, Object> sourceVariables = bo.getVariables();
Map<String, Object> variables = new HashMap<>();
if (ignorePermission) {
variables.put("ignore", true); // 仅流程发起时自动完成申请人节点使用,真人办理节点不得绕过办理人校验
// Why仅流程发起时设为 true让引擎自动完成申请人节点时绕过办理人权限校验。
variables.put("ignore", true);
}
boolean hasAssignee = bo.getAssigneeId() != null || sourceVariables.get("assigneeId") != null;
variables.put("has_assignee", hasAssignee ? "1" : "0");
variables.put("reassigned", StringUtils.blankToDefault(Convert.toStr(sourceVariables.get("reassigned")), "0"));
// Why变量来源——先取 BO 直接字段(前端/业务接口最新值),为 null 时 fallback 到 sourceVariables 历史值,
// 保证变量不丢失且不覆盖为 null。
putIfPresent(variables, "taskTitle", firstNonNull(bo.getTaskTitle(), sourceVariables.get("taskTitle")));
putIfPresent(variables, "taskDesc", firstNonNull(bo.getTaskDesc(), sourceVariables.get("taskDesc")));
putIfPresent(variables, "taskType", firstNonNull(bo.getTaskType(), sourceVariables.get("taskType")));
@ -586,8 +645,8 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
putIfPresent(variables, "leaderOpinion", firstNonNull(bo.getLeaderOpinion(), sourceVariables.get("leaderOpinion")));
putIfPresent(variables, "actualStartTime", firstNonNull(bo.getActualStartTime(), sourceVariables.get("actualStartTime")));
putIfPresent(variables, "leaderFinalOpinion", firstNonNull(bo.getLeaderFinalOpinion(), sourceVariables.get("leaderFinalOpinion")));
putIfPresent(variables, "ccUserIds", firstNonNull(bo.getCcUserIds(), sourceVariables.get("ccUserIds")));
// 清理空值项,防止序列化问题
// putIfPresent(variables, "ccUserIds", firstNonNull(bo.getCcUserIds(), sourceVariables.get("ccUserIds")));
// Why清理 null 值条目,防止流程变量序列化/反序列化时 null 值导致类型推断错误或 JSON 清理逻辑不一致。
variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
return variables;
}
@ -650,26 +709,34 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
/**
* "主执行人"
* Why/
* (task,user) AD-12/AD-13
* Why/ memberType='1'
*
* 1. assigneeId /
* 2. delete
* 3. **** delete (memberType='2')
* (tempTaskId, userId)
* insert(memberType='1') UK = AD-12/AD-13
*/
private void refreshMainAssigneeMember(Long taskId, Long assigneeId, String assigneeName) {
if (taskId == null || assigneeId == null) {
return;
}
// 1. 查询当前主执行人记录
ErpTempTaskMember existMain = memberMapper.selectOne(Wrappers.<ErpTempTaskMember>lambdaQuery()
.eq(ErpTempTaskMember::getTempTaskId, taskId)
.eq(ErpTempTaskMember::getMemberType, MEMBER_TYPE_MAIN));
if (existMain != null) {
if (Objects.equals(existMain.getUserId(), assigneeId)) {
return; // 主执行人未变更,无需处理
return; // Why主执行人未变更幂等跳过避免不必要的 delete+insert。
}
// 2. 旧主执行人 ≠ 新主执行人,移除旧记录
memberMapper.deleteById(existMain.getMemberId());
}
// 若新主执行人此前以协作人身份参与,先移除其协作记录,规避唯一约束冲突
// 3. Why若新主执行人此前以协作人身份参与,先移除其协作记录,规避 (taskId,userId) 唯一约束冲突
memberMapper.delete(Wrappers.<ErpTempTaskMember>lambdaQuery()
.eq(ErpTempTaskMember::getTempTaskId, taskId)
.eq(ErpTempTaskMember::getUserId, assigneeId));
// 4. 插入新主执行人记录
ErpTempTaskMember main = new ErpTempTaskMember();
main.setTempTaskId(taskId);
main.setMemberType(MEMBER_TYPE_MAIN);
@ -680,7 +747,11 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
/**
* AD-12
* Why submitVerifycompleteTask
* Why
* Why**** Mapstruct //
* taskStatus/flowStatus/assigneeId/softwareLeaderId
* BO
* 退 submitVerify completeTask
*/
@Override
public Boolean assigneeReviewSubmit(ErpTempTaskBo bo) {
@ -691,6 +762,7 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
if (!Objects.equals(LoginHelper.getUserId(), task.getAssigneeId())) {
throw new ServiceException("只有主执行人可以审阅提交");
}
// Why逐一判断不为空再 set仅回写主执行人有权修改的字段其余字段taskStatus/flowStatus/assigneeId 等)保持不变。
if (StringUtils.isNotBlank(bo.getTaskTitle())) {
task.setTaskTitle(bo.getTaskTitle());
}
@ -712,7 +784,11 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
/**
*
* Whyreassigned
* Whyreassigned oldAssigneeId != newAssigneeId
* variables.reassigned
* Why @Transactional @GlobalTransactional leaderReviewAndComplete
* leaderReviewAndComplete @GlobalTransactional @Transactional Seata
* Spring Seata
*/
@Override
@Transactional(rollbackFor = Exception.class)
@ -731,21 +807,39 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
throw new ServiceException("进入执行前必须指定主执行人");
}
// Why持久化前先快照旧主执行人 ID用于后续对比判断 reassigned。
Long oldAssigneeId = task.getAssigneeId();
applyLeaderReviewFields(task, bo);
// Whyreassigned=0 意味着领导没有换主执行人,下一节点直接进入 execute执行
// 在此提前置 taskStatus=3(执行中),避免依赖跨 JVM 的 ProcessEvent 回写不及时。
// processHandler 仍作为兜底,但此处是业务主路径。
boolean willEnterExecute = Objects.equals(oldAssigneeId, task.getAssigneeId());
if (willEnterExecute) {
task.setTaskStatus(STATUS_RUNNING);
if (task.getActualStartTime() == null) {
task.setActualStartTime(new Date());
}
}
validEntityBeforeSave(task);
baseMapper.updateById(task);
// Why任务类型变更后清理不匹配的聚合字段如从"项目任务"改为"部门任务"时置空 projectId 等)
clearAggregationFields(task.getTempTaskId(), task.getTaskType());
// Why领导可能换主执行人同步更新参与人表保证一致性AD-13
refreshMainAssigneeMember(task.getTempTaskId(), task.getAssigneeId(), task.getAssigneeName());
Map<String, Object> variables = buildWorkflowVariables(toBo(task));
// Whyreassigned 必须基于持久化前后的 old vs new 主执行人 ID 对比计算,
// 确保流程网关has_assignee=1 且 reassigned=1 时走"主执行人审阅"节点,否则直入 execute准确无误。
variables.put("reassigned", Objects.equals(oldAssigneeId, task.getAssigneeId()) ? "0" : "1");
variables.put("has_assignee", "1");
return variables;
}
/**
* Warm-Flow
* + Warm-Flow Seata
* Why buildLeaderReviewVariables variables leaderOpinion variables
* completeTask @GlobalTransactional"业务数据变更""流程推进"
*
*/
@Override
@GlobalTransactional(rollbackFor = Exception.class)
@ -753,8 +847,10 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
if (bo == null) {
throw new ServiceException("临时任务审批数据不能为空");
}
// Why先校验 taskId 确实属于当前临时任务且 nodeCode=leader_review防止前端传入篡改的 taskId 越权办理。
validateWorkflowTaskBelongsToBusiness(bo.getTempTaskId(), bo.getTaskId(), "leader_review", LoginHelper.getUserId());
Map<String, Object> variables = buildLeaderReviewVariables(bo);
// WhyleaderOpinion 由 buildLeaderReviewVariables 构建后再追加,确保审批意见同时用于 variables 流转和审批记录 message 展示。
putIfPresent(variables, "leaderOpinion", bo.getLeaderOpinion());
RemoteCompleteTask completeTask = new RemoteCompleteTask();
@ -762,6 +858,7 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
completeTask.setMessage(StringUtils.blankToDefault(bo.getLeaderOpinion(), "领导审批"));
completeTask.setMessageType(Collections.singletonList("1"));
completeTask.setVariables(variables);
// WhycompleteTask 带 variables 推进当前任务variables 内的 has_assignee/reassigned 决定下一节点路由(主执行人审阅/直入执行)。
boolean completed = remoteWorkflowService.completeTask(completeTask);
if (!completed) {
throw new ServiceException("领导审批失败,流程未完成流转");
@ -769,6 +866,13 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
return Boolean.TRUE;
}
/**
* WhytaskType taskType
* - taskType='3'() projectId/projectCode/projectName deptId/deptName
* - taskType='1'() deptId/deptName projectId/projectCode/projectName
* - taskType='2'()
* "项目任务残留部门字段""部门任务残留项目字段"AD-01
*/
private void applyLeaderReviewFields(ErpTempTask task, ErpTempTaskBo bo) {
task.setTaskTitle(bo.getTaskTitle());
task.setTaskDesc(bo.getTaskDesc());
@ -784,14 +888,16 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
task.setSoftwareLeaderName(bo.getSoftwareLeaderName());
task.setAssigneeId(bo.getAssigneeId());
task.setAssigneeName(bo.getAssigneeName());
task.setCcUserIds(bo.getCcUserIds());
// task.setCcUserIds(bo.getCcUserIds());
task.setRemark(bo.getRemark());
// WhytaskType='3'(项目任务)时设置项目字段并清空部门字段,反之亦然,防止字段残留导致统计口径错误。
if ("3".equals(bo.getTaskType())) {
task.setProjectId(bo.getProjectId());
task.setProjectCode(bo.getProjectCode());
task.setProjectName(bo.getProjectName());
} else {
// Why非项目任务必须清空项目字段避免报表查询按项目筛选时误命中。
task.setProjectId(null);
task.setProjectCode(null);
task.setProjectName(null);
@ -800,6 +906,7 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
task.setDeptId(bo.getDeptId());
task.setDeptName(bo.getDeptName());
} else {
// Why非部门任务必须清空部门字段避免报表查询按部门筛选时误命中。
task.setDeptId(null);
task.setDeptName(null);
}
@ -858,7 +965,7 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
bo.setTotalHours(task.getTotalHours());
bo.setTaskStatus(task.getTaskStatus());
bo.setFlowStatus(task.getFlowStatus());
bo.setCcUserIds(task.getCcUserIds());
// bo.setCcUserIds(task.getCcUserIds());
bo.setRemark(task.getRemark());
bo.setFlowCode(FLOW_CODE);
bo.setVariables(new HashMap<>(16));
@ -941,10 +1048,12 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
if (task == null) {
throw new ServiceException("临时任务不存在");
}
// Why待领导审核(4)/已关闭(5)禁止改明细保证整单提交快照不被篡改AD-15
// Why限制仅 STATUS_RUNNING(3-执行中) 可维护工时。待领导审核(4)/已关闭(5)禁止改明细,
// 保证整单提交后快照(工时总数+明细不被后续篡改领导审核看到的与主执行人提交的完全一致AD-15
if (!STATUS_RUNNING.equals(task.getTaskStatus())) {
throw new ServiceException("当前任务非执行中状态,不允许维护工时明细");
}
// WhymemberId 从参与人表查询,不以前端传入为准,防止非参与人伪造 memberId 写入工时。
Long memberId = memberMapper.selectMemberId(bo.getTempTaskId(), LoginHelper.getUserId());
if (memberId == null) {
throw new ServiceException("您不是该任务参与人,无法填报工时");
@ -954,16 +1063,19 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
}
bo.setMemberId(memberId);
bo.setUserId(LoginHelper.getUserId());
// Why自然周周一~周日是统计归属维度由工作日期统一推导保证口径一致AD-07/AD-08
// Why自然周周一~周日)是统计归属维度,由工作日期的 DateUtil.beginOfWeek/endOfWeek 统一推导,
// 保证同一周内所有工时明细的 weekStart/weekEnd 完全一致避免跨口径统计偏差AD-07/AD-08
bo.setWeekStart(DateUtil.beginOfWeek(bo.getWorkDate()));
bo.setWeekEnd(DateUtil.endOfWeek(bo.getWorkDate()));
ErpTempTaskWorklog entity = MapstructUtils.convert(bo, ErpTempTaskWorklog.class);
entity.setUpdateBy(LoginHelper.getUserId());
if (entity.getWorklogId() == null) {
// Why新增工时默认 lockFlag='0'(未锁定),待提交完成后统一 lockByTask 设为 '1'。
entity.setCreateBy(LoginHelper.getUserId());
entity.setLockFlag("0");
return worklogMapper.insert(entity) > 0;
}
// Why修改前必须检查 lockFlag已锁定的工时明细拒绝修改保护提交完成后的数据完整性。
ErpTempTaskWorklog old = worklogMapper.selectById(entity.getWorklogId());
if (old != null && "1".equals(old.getLockFlag())) {
throw new ServiceException("该工时明细已锁定,不可修改");
@ -997,7 +1109,10 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
}
/**
* + AD-14/AD-15
* + + AD-14/AD-15
* Why"快照凝固"totalHours SUM
* lockFlag='1' //
* STATUS_RUNNING(3)STATUS_PENDING_FINAL(4)"暂时冻结待领导确认"
*/
@Override
@GlobalTransactional(rollbackFor = Exception.class)
@ -1013,15 +1128,20 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
if (!STATUS_RUNNING.equals(task.getTaskStatus())) {
throw new ServiceException("仅执行中任务可提交完成");
}
// Why提交完成前累计总工时必须>0否则报表统计失去基础AD-14
// Why提交前必须累计工时>0至少一条工时明细。空提交会导致报表统计失去数据基础AD-14
// sumHoursByTask 使用数据库 SUM 聚合,避免应用层循环累加的精度/并发问题。
BigDecimal total = worklogMapper.sumHoursByTask(tempTaskId);
if (total == null || total.compareTo(BigDecimal.ZERO) <= 0) {
throw new ServiceException("提交完成前任务累计总工时必须大于0且至少有一条工时明细");
}
// WhytotalHours 作为提交快照固化到主表,后续评分/报表直接读主表字段无需再聚合明细AD-14
task.setTotalHours(total);
task.setTaskStatus(STATUS_PENDING_FINAL); // 待领导审核(整单临时冻结)
worklogMapper.lockByTask(tempTaskId, "1"); // 锁定全部明细待审核期间不可改AD-15
// WhytaskStatus=4(待领导审核) 是提交后的"临时冻结态"整单工时只读不可改AD-15
task.setTaskStatus(STATUS_PENDING_FINAL);
// WhylockFlag='1' 一次性锁定全部工时明细领导审核期间禁止任何人对工时增删改AD-15
worklogMapper.lockByTask(tempTaskId, "1");
baseMapper.updateById(task);
// WhycompleteExecuteTask 在同一 Seata 事务内推进 execute 节点到 leader_final确保锁与流程推进原子。
completeExecuteTask(bo, task, total);
return Boolean.TRUE;
}
@ -1049,8 +1169,32 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
}
}
/**
* "待领导审核"(taskStatus=4)
*
* Why submitFinish leader_final
* taskStatus 3()
* submitFinish totalHours + + taskStatus4
*
* STATUS_RUNNING(3) STATUS_PENDING_FINAL(4) (5)
*/
@Override
public void syncStateToPendingFinal(Long tempTaskId) {
ErpTempTask task = baseMapper.selectById(tempTaskId);
if (task == null || !STATUS_RUNNING.equals(task.getTaskStatus())) {
return;
}
BigDecimal total = worklogMapper.sumHoursByTask(tempTaskId);
task.setTotalHours(total != null ? total : BigDecimal.ZERO);
task.setTaskStatus(STATUS_PENDING_FINAL);
worklogMapper.lockByTask(tempTaskId, "1");
baseMapper.updateById(task);
}
/**
* AD-05/AD-16
* Why + taskStatus5() + lockFlag='1'
* leader_final (softwareLeaderId)
*/
@Override
@GlobalTransactional(rollbackFor = Exception.class)
@ -1062,16 +1206,24 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
if (!Objects.equals(LoginHelper.getUserId(), task.getSoftwareLeaderId())) {
throw new ServiceException("只有软件部领导可以评分关闭");
}
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("仅待领导审核或执行中状态可评分关闭");
}
// Why必须对全部参与人(主+协作)都给出评分等级否则不允许关闭AD-16
// Why必须对全部参与人(主执行人+协作人)都给出评分等级不允许遗漏任何人。漏评会导致考核不公AD-16
List<ErpTempTaskMember> members = memberMapper.selectList(Wrappers.<ErpTempTaskMember>lambdaQuery()
.eq(ErpTempTaskMember::getTempTaskId, bo.getTempTaskId()));
validateScoreCoverage(bo, members);
Date now = new Date();
Long leaderId = LoginHelper.getUserId();
// 幂等:清理旧评分再批量写入,避免重复关闭产生多条
// Why先 delete 旧评分再批量 insert实现**幂等关闭**——若领导之前已提交但流程失败,本次重试不会产生重复评分记录。
// delete+insert 在 @GlobalTransactional 内原子执行,中间不会出现"删了但没插"的不可见窗口。
scoreMapper.delete(Wrappers.<ErpTempTaskScore>lambdaQuery()
.eq(ErpTempTaskScore::getTempTaskId, bo.getTempTaskId()));
for (ErpTempTaskScoreBo s : bo.getScoreList()) {
@ -1084,10 +1236,14 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
score.setScoreTime(now);
scoreMapper.insert(score);
}
task.setActualFinishTime(now); // 实际关闭时间=评分时间(提前/按期/延期口径AD-06
// WhyactualFinishTime = 评分时间,作为提前/按期/延期的口径基准AD-06
task.setActualFinishTime(now);
task.setTaskStatus(STATUS_CLOSED);
worklogMapper.lockByTask(bo.getTempTaskId(), "1"); // 永久锁定全部工时明细AD-04
// Why永久锁定全部工时明细 lockFlag='1'关闭后任何人均不可修改工时AD-04
worklogMapper.lockByTask(bo.getTempTaskId(), "1");
baseMapper.updateById(task);
// WhycompleteLeaderFinalTask 在同一事务内完成 leader_final 节点,流程随之结束(FINISH)
// processHandler 收到 FINISH 事件后会兜底确认 taskStatus 已置为 5。
completeLeaderFinalTask(bo);
return Boolean.TRUE;
}
@ -1143,11 +1299,23 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
}
}
/**
* Why taskId
* Warm-Flow flowHisTaskList businessId
* **线** taskId nodeCode approver
* 线 Map 10O(n)
*
* 1. taskId taskId
* 2. nodeCode leader_review execute
* 3. approver
* ServiceException @GlobalTransactional Seata
*/
@SuppressWarnings("unchecked")
private void validateWorkflowTaskBelongsToBusiness(Long tempTaskId, Long taskId, String expectedNodeCode, Long expectedUserId) {
if (tempTaskId == null || taskId == null) {
throw new ServiceException("临时任务ID和流程任务ID不能为空");
}
// WhyflowHisTaskList 返回该业务 ID 下的所有流程任务记录(含历史),从中定位当前 taskId 并校验。
Map<String, Object> flowInfo = remoteWorkflowService.flowHisTaskList(String.valueOf(tempTaskId));
Object listObj = flowInfo == null ? null : flowInfo.get("list");
if (!(listObj instanceof Collection<?> taskList)) {
@ -1155,6 +1323,7 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
}
String taskIdText = Convert.toStr(taskId);
String expectedUserIdText = Convert.toStr(expectedUserId);
// Why线性遍历查找匹配的 taskId找到后立即校验并 return未找到则抛异常。
for (Object item : taskList) {
if (!(item instanceof Map<?, ?> rawTask)) {
continue;
@ -1163,10 +1332,14 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
if (!Objects.equals(taskIdText, Convert.toStr(taskMap.get("id")))) {
continue;
}
// WhynodeCode 精确匹配——例如 scoreAndClose 要求 nodeCode="leader_final"
// submitFinish 要求 nodeCode="execute",防止在错误节点调用业务接口。
if (StringUtils.isNotBlank(expectedNodeCode)
&& !Objects.equals(expectedNodeCode, Convert.toStr(taskMap.get("nodeCode")))) {
throw new ServiceException("流程任务节点与当前业务操作不匹配");
}
// Whyapprover 是逗号分隔的办理人 ID 列表(如 "101,203"),需要解析后检查当前用户是否在其中。
// 支持会签/或签场景同时传入多个办理人的情况。
String approver = Convert.toStr(taskMap.get("approver"));
if (StringUtils.isNotBlank(approver) && StringUtils.isNotBlank(expectedUserIdText)) {
List<String> approvers = Arrays.stream(approver.split(","))
@ -1176,8 +1349,10 @@ public class ErpTempTaskServiceImpl implements IErpTempTaskService {
throw new ServiceException("当前用户不是该流程任务办理人");
}
}
return;
return; // 所有校验通过,找到匹配的流程任务
}
// Why遍历完未找到匹配 taskId说明前端传入的 taskId 不属于当前临时任务,
// 可能是跨业务窜用或者已过期的待办,直接拒绝。
throw new ServiceException("流程任务不属于当前临时任务");
}

Loading…
Cancel
Save