feat: add parallel OA and HR leave review
This commit is contained in:
+12
@@ -87,6 +87,16 @@ class LeaveRequestService(
|
||||
if (durationMinutes > 1440 && oaAdministratorId == approverId) {
|
||||
throw ApiException(HttpStatus.CONFLICT, "APPROVER_SEPARATION_REQUIRED", "长期请假的主管和 OA 复核人必须为不同人员")
|
||||
}
|
||||
val requiresParallelAnnualReview = durationMinutes > 1440 && existing.type == LeaveType.ANNUAL
|
||||
val hrReviewerId = if (requiresParallelAnnualReview) {
|
||||
routingRepository.findHrReviewer(actor.tenantId)
|
||||
?: throw ApiException(HttpStatus.CONFLICT, "HR_REVIEWER_NOT_FOUND", "未找到有效的人力资源复核人")
|
||||
} else {
|
||||
oaAdministratorId
|
||||
}
|
||||
if (requiresParallelAnnualReview && hrReviewerId in setOf(actor.id, approverId, oaAdministratorId)) {
|
||||
throw ApiException(HttpStatus.CONFLICT, "APPROVER_SEPARATION_REQUIRED", "长期年假的主管、OA 和 HR 复核人必须为不同人员")
|
||||
}
|
||||
val outcome = transition(
|
||||
actor = actor,
|
||||
id = id,
|
||||
@@ -105,7 +115,9 @@ class LeaveRequestService(
|
||||
actor.id,
|
||||
approverId,
|
||||
oaAdministratorId,
|
||||
hrReviewerId,
|
||||
durationMinutes,
|
||||
existing.type.name,
|
||||
)
|
||||
repository.attachWorkflow(
|
||||
actor.tenantId,
|
||||
|
||||
+2
@@ -6,4 +6,6 @@ interface ApprovalRoutingRepository {
|
||||
fun findDepartmentManager(tenantId: UUID, applicantId: UUID): UUID?
|
||||
|
||||
fun findOaAdministrator(tenantId: UUID): UUID?
|
||||
|
||||
fun findHrReviewer(tenantId: UUID): UUID?
|
||||
}
|
||||
|
||||
+8
-1
@@ -41,6 +41,12 @@ class JooqApprovalRoutingRepository(
|
||||
)?.get("user_id", UUID::class.java)
|
||||
|
||||
override fun findOaAdministrator(tenantId: UUID): UUID? =
|
||||
findUserByRole(tenantId, "oa_admin")
|
||||
|
||||
override fun findHrReviewer(tenantId: UUID): UUID? =
|
||||
findUserByRole(tenantId, "hr_reviewer")
|
||||
|
||||
private fun findUserByRole(tenantId: UUID, roleCode: String): UUID? =
|
||||
dsl.fetchOne(
|
||||
"""
|
||||
SELECT ur.user_id
|
||||
@@ -48,7 +54,7 @@ class JooqApprovalRoutingRepository(
|
||||
JOIN authz.role role
|
||||
ON role.tenant_id = ur.tenant_id
|
||||
AND role.id = ur.role_id
|
||||
AND role.code = 'oa_admin'
|
||||
AND role.code = ?
|
||||
AND role.status = 'ACTIVE'
|
||||
JOIN identity.user_account account
|
||||
ON account.tenant_id = ur.tenant_id
|
||||
@@ -60,6 +66,7 @@ class JooqApprovalRoutingRepository(
|
||||
ORDER BY ur.effective_from, ur.user_id
|
||||
LIMIT 1
|
||||
""".trimIndent(),
|
||||
roleCode,
|
||||
tenantId,
|
||||
)?.get("user_id", UUID::class.java)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@ interface LeaveWorkflowGateway {
|
||||
applicantId: UUID,
|
||||
approverId: UUID,
|
||||
oaAdministratorId: UUID,
|
||||
hrReviewerId: UUID,
|
||||
durationMinutes: Long,
|
||||
leaveType: String,
|
||||
): StartedProcess
|
||||
|
||||
fun listAssignedTasks(assigneeId: UUID): List<WorkflowTask>
|
||||
|
||||
+4
@@ -23,7 +23,9 @@ class FlowableLeaveWorkflowGateway(
|
||||
applicantId: UUID,
|
||||
approverId: UUID,
|
||||
oaAdministratorId: UUID,
|
||||
hrReviewerId: UUID,
|
||||
durationMinutes: Long,
|
||||
leaveType: String,
|
||||
): StartedProcess {
|
||||
val process = runtimeService.createProcessInstanceBuilder()
|
||||
.processDefinitionKey(PROCESS_DEFINITION_KEY)
|
||||
@@ -35,7 +37,9 @@ class FlowableLeaveWorkflowGateway(
|
||||
"applicantId" to applicantId.toString(),
|
||||
"approverId" to approverId.toString(),
|
||||
"oaAdministratorId" to oaAdministratorId.toString(),
|
||||
"hrReviewerId" to hrReviewerId.toString(),
|
||||
"durationMinutes" to durationMinutes,
|
||||
"leaveType" to leaveType,
|
||||
),
|
||||
)
|
||||
.start()
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
INSERT INTO organization.position (id, tenant_id, code, name, status)
|
||||
VALUES (
|
||||
'30000000-0000-7000-8000-000000000004',
|
||||
'00000000-0000-7000-8000-000000000001',
|
||||
'hr-reviewer',
|
||||
'人力资源复核人',
|
||||
'ACTIVE'
|
||||
);
|
||||
|
||||
INSERT INTO identity.user_account (
|
||||
id, tenant_id, keycloak_subject, username, display_name, email, status
|
||||
) VALUES (
|
||||
'40000000-0000-7000-8000-000000000004',
|
||||
'00000000-0000-7000-8000-000000000001',
|
||||
'10000000-0000-7000-8000-000000000004',
|
||||
'hr',
|
||||
'HR 复核人',
|
||||
'hr@example.local',
|
||||
'ACTIVE'
|
||||
);
|
||||
|
||||
INSERT INTO organization.user_assignment (
|
||||
id, tenant_id, user_id, department_id, position_id, is_primary
|
||||
) VALUES (
|
||||
'50000000-0000-7000-8000-000000000004',
|
||||
'00000000-0000-7000-8000-000000000001',
|
||||
'40000000-0000-7000-8000-000000000004',
|
||||
'20000000-0000-7000-8000-000000000001',
|
||||
'30000000-0000-7000-8000-000000000004',
|
||||
TRUE
|
||||
);
|
||||
|
||||
INSERT INTO authz.role (id, tenant_id, code, name, status)
|
||||
VALUES (
|
||||
'60000000-0000-7000-8000-000000000004',
|
||||
'00000000-0000-7000-8000-000000000001',
|
||||
'hr_reviewer',
|
||||
'人力资源复核人',
|
||||
'ACTIVE'
|
||||
);
|
||||
|
||||
INSERT INTO authz.user_role (tenant_id, user_id, role_id)
|
||||
VALUES (
|
||||
'00000000-0000-7000-8000-000000000001',
|
||||
'40000000-0000-7000-8000-000000000004',
|
||||
'60000000-0000-7000-8000-000000000004'
|
||||
);
|
||||
@@ -0,0 +1,7 @@
|
||||
INSERT INTO authz.user_role (tenant_id, user_id, role_id)
|
||||
VALUES (
|
||||
'00000000-0000-7000-8000-000000000001',
|
||||
'40000000-0000-7000-8000-000000000004',
|
||||
'60000000-0000-7000-8000-000000000001'
|
||||
)
|
||||
ON CONFLICT DO NOTHING;
|
||||
@@ -3,45 +3,71 @@
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:flowable="http://flowable.org/bpmn"
|
||||
targetNamespace="https://aioa.all8ai.com/processes">
|
||||
<process id="leaveApproval" name="请假分级审批" isExecutable="true">
|
||||
<process id="leaveApproval" name="请假分级与并行会签" isExecutable="true">
|
||||
<startEvent id="start" name="申请已提交"/>
|
||||
<sequenceFlow id="flow-start-manager" sourceRef="start" targetRef="managerReview"/>
|
||||
|
||||
<userTask id="managerReview"
|
||||
name="部门主管审批"
|
||||
flowable:assignee="${approverId}"/>
|
||||
<userTask id="managerReview" name="部门主管审批" flowable:assignee="${approverId}"/>
|
||||
<sequenceFlow id="flow-manager-decision" sourceRef="managerReview" targetRef="managerDecision"/>
|
||||
|
||||
<exclusiveGateway id="managerDecision" name="主管审批结果"/>
|
||||
<sequenceFlow id="flow-manager-rejected" sourceRef="managerDecision" targetRef="rejectedEnd">
|
||||
<sequenceFlow id="flow-manager-rejected" sourceRef="managerDecision" targetRef="rejectedTerminate">
|
||||
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${approved == false}]]></conditionExpression>
|
||||
</sequenceFlow>
|
||||
<sequenceFlow id="flow-manager-approved" sourceRef="managerDecision" targetRef="durationDecision">
|
||||
<sequenceFlow id="flow-manager-approved" sourceRef="managerDecision" targetRef="routeDecision">
|
||||
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${approved == true}]]></conditionExpression>
|
||||
</sequenceFlow>
|
||||
|
||||
<exclusiveGateway id="durationDecision" name="是否超过 24 小时"/>
|
||||
<sequenceFlow id="flow-short-approved" sourceRef="durationDecision" targetRef="approvedEnd">
|
||||
<exclusiveGateway id="routeDecision" name="按时长和类型路由"/>
|
||||
<sequenceFlow id="flow-short-approved" sourceRef="routeDecision" targetRef="approvedEnd">
|
||||
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${durationMinutes <= 1440}]]></conditionExpression>
|
||||
</sequenceFlow>
|
||||
<sequenceFlow id="flow-long-oa-review" sourceRef="durationDecision" targetRef="oaReview">
|
||||
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${durationMinutes > 1440}]]></conditionExpression>
|
||||
<sequenceFlow id="flow-long-nonannual-oa" sourceRef="routeDecision" targetRef="oaSerialReview">
|
||||
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${durationMinutes > 1440 && leaveType != 'ANNUAL'}]]></conditionExpression>
|
||||
</sequenceFlow>
|
||||
<sequenceFlow id="flow-long-annual-parallel" sourceRef="routeDecision" targetRef="parallelSplit">
|
||||
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${durationMinutes > 1440 && leaveType == 'ANNUAL'}]]></conditionExpression>
|
||||
</sequenceFlow>
|
||||
|
||||
<userTask id="oaReview"
|
||||
name="OA 管理员复核"
|
||||
flowable:assignee="${oaAdministratorId}"/>
|
||||
<sequenceFlow id="flow-oa-decision" sourceRef="oaReview" targetRef="oaDecision"/>
|
||||
|
||||
<exclusiveGateway id="oaDecision" name="OA 复核结果"/>
|
||||
<sequenceFlow id="flow-oa-approved" sourceRef="oaDecision" targetRef="approvedEnd">
|
||||
<userTask id="oaSerialReview" name="OA 管理员复核" flowable:assignee="${oaAdministratorId}"/>
|
||||
<sequenceFlow id="flow-oa-serial-decision" sourceRef="oaSerialReview" targetRef="oaSerialDecision"/>
|
||||
<exclusiveGateway id="oaSerialDecision" name="OA 复核结果"/>
|
||||
<sequenceFlow id="flow-oa-serial-approved" sourceRef="oaSerialDecision" targetRef="approvedEnd">
|
||||
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${approved == true}]]></conditionExpression>
|
||||
</sequenceFlow>
|
||||
<sequenceFlow id="flow-oa-rejected" sourceRef="oaDecision" targetRef="rejectedEnd">
|
||||
<sequenceFlow id="flow-oa-serial-rejected" sourceRef="oaSerialDecision" targetRef="rejectedTerminate">
|
||||
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${approved == false}]]></conditionExpression>
|
||||
</sequenceFlow>
|
||||
|
||||
<parallelGateway id="parallelSplit" name="OA 与 HR 并行复核"/>
|
||||
<sequenceFlow id="flow-parallel-oa" sourceRef="parallelSplit" targetRef="oaParallelReview"/>
|
||||
<sequenceFlow id="flow-parallel-hr" sourceRef="parallelSplit" targetRef="hrReview"/>
|
||||
|
||||
<userTask id="oaParallelReview" name="OA 并行复核" flowable:assignee="${oaAdministratorId}"/>
|
||||
<sequenceFlow id="flow-oa-parallel-decision" sourceRef="oaParallelReview" targetRef="oaParallelDecision"/>
|
||||
<exclusiveGateway id="oaParallelDecision" name="OA 并行复核结果"/>
|
||||
<sequenceFlow id="flow-oa-parallel-approved" sourceRef="oaParallelDecision" targetRef="parallelJoin">
|
||||
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${approved == true}]]></conditionExpression>
|
||||
</sequenceFlow>
|
||||
<sequenceFlow id="flow-oa-parallel-rejected" sourceRef="oaParallelDecision" targetRef="rejectedTerminate">
|
||||
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${approved == false}]]></conditionExpression>
|
||||
</sequenceFlow>
|
||||
|
||||
<userTask id="hrReview" name="HR 并行复核" flowable:assignee="${hrReviewerId}"/>
|
||||
<sequenceFlow id="flow-hr-decision" sourceRef="hrReview" targetRef="hrDecision"/>
|
||||
<exclusiveGateway id="hrDecision" name="HR 复核结果"/>
|
||||
<sequenceFlow id="flow-hr-approved" sourceRef="hrDecision" targetRef="parallelJoin">
|
||||
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${approved == true}]]></conditionExpression>
|
||||
</sequenceFlow>
|
||||
<sequenceFlow id="flow-hr-rejected" sourceRef="hrDecision" targetRef="rejectedTerminate">
|
||||
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${approved == false}]]></conditionExpression>
|
||||
</sequenceFlow>
|
||||
|
||||
<parallelGateway id="parallelJoin" name="全部复核通过"/>
|
||||
<sequenceFlow id="flow-parallel-approved-end" sourceRef="parallelJoin" targetRef="approvedEnd"/>
|
||||
|
||||
<endEvent id="approvedEnd" name="已批准"/>
|
||||
<endEvent id="rejectedEnd" name="已驳回"/>
|
||||
<endEvent id="rejectedTerminate" name="已驳回并终止其他分支">
|
||||
<terminateEventDefinition/>
|
||||
</endEvent>
|
||||
</process>
|
||||
</definitions>
|
||||
|
||||
+2
@@ -108,7 +108,9 @@ class ApprovalTaskServiceTest {
|
||||
applicantId: UUID,
|
||||
approverId: UUID,
|
||||
oaAdministratorId: UUID,
|
||||
hrReviewerId: UUID,
|
||||
durationMinutes: Long,
|
||||
leaveType: String,
|
||||
): StartedProcess = error("Not used")
|
||||
|
||||
override fun listAssignedTasks(assigneeId: UUID): List<WorkflowTask> = listOf(task)
|
||||
|
||||
+5
@@ -137,6 +137,9 @@ class LeaveRequestServiceTest {
|
||||
|
||||
override fun findOaAdministrator(tenantId: UUID) =
|
||||
UUID.fromString("40000000-0000-7000-8000-000000000003")
|
||||
|
||||
override fun findHrReviewer(tenantId: UUID) =
|
||||
UUID.fromString("40000000-0000-7000-8000-000000000004")
|
||||
},
|
||||
FakeWorkflowGateway(),
|
||||
)
|
||||
@@ -148,7 +151,9 @@ class LeaveRequestServiceTest {
|
||||
applicantId: UUID,
|
||||
approverId: UUID,
|
||||
oaAdministratorId: UUID,
|
||||
hrReviewerId: UUID,
|
||||
durationMinutes: Long,
|
||||
leaveType: String,
|
||||
) = StartedProcess("process-$leaveRequestId", "leaveApproval:1:test")
|
||||
|
||||
override fun listAssignedTasks(assigneeId: UUID): List<WorkflowTask> = emptyList()
|
||||
|
||||
Reference in New Issue
Block a user