feat: add parallel OA and HR leave review
This commit is contained in:
@@ -30,6 +30,7 @@ export GRADLE_USER_HOME=/tmp/aioa-gradle-home
|
|||||||
- Flowable 7.2 部门主管审批 BPMN、待办、批准和驳回
|
- Flowable 7.2 部门主管审批 BPMN、待办、批准和驳回
|
||||||
- 复杂流程安全边界:中间串行/并行任务不提前结束业务申请
|
- 复杂流程安全边界:中间串行/并行任务不提前结束业务申请
|
||||||
- 条件串行流程:不超过 24 小时由主管审批,超过 24 小时增加 OA 管理员复核
|
- 条件串行流程:不超过 24 小时由主管审批,超过 24 小时增加 OA 管理员复核
|
||||||
|
- 并行会签:超过 24 小时的年假由 OA 与 HR 同时复核,全部同意才通过
|
||||||
|
|
||||||
Flowable 开发环境自动维护 `flowable` Schema。生产环境必须设置 `FLOWABLE_SCHEMA_UPDATE=false`,并通过受控数据库变更流程管理 Flowable 表结构。
|
Flowable 开发环境自动维护 `flowable` Schema。生产环境必须设置 `FLOWABLE_SCHEMA_UPDATE=false`,并通过受控数据库变更流程管理 Flowable 表结构。
|
||||||
|
|
||||||
@@ -43,6 +44,7 @@ Flowable 开发环境自动维护 `flowable` Schema。生产环境必须设置 `
|
|||||||
| `employee` | `Employee123!` | 普通员工 |
|
| `employee` | `Employee123!` | 普通员工 |
|
||||||
| `manager` | `Manager123!` | 员工、部门主管 |
|
| `manager` | `Manager123!` | 员工、部门主管 |
|
||||||
| `admin` | `Admin123!` | 员工、OA 管理员 |
|
| `admin` | `Admin123!` | 员工、OA 管理员 |
|
||||||
|
| `hr` | `HrReviewer123!` | 员工、人力资源复核人 |
|
||||||
|
|
||||||
这些凭据只允许用于本地开发,生产 Realm 不得导入测试用户,也不得启用密码模式。
|
这些凭据只允许用于本地开发,生产 Realm 不得导入测试用户,也不得启用密码模式。
|
||||||
|
|
||||||
|
|||||||
+12
@@ -87,6 +87,16 @@ class LeaveRequestService(
|
|||||||
if (durationMinutes > 1440 && oaAdministratorId == approverId) {
|
if (durationMinutes > 1440 && oaAdministratorId == approverId) {
|
||||||
throw ApiException(HttpStatus.CONFLICT, "APPROVER_SEPARATION_REQUIRED", "长期请假的主管和 OA 复核人必须为不同人员")
|
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(
|
val outcome = transition(
|
||||||
actor = actor,
|
actor = actor,
|
||||||
id = id,
|
id = id,
|
||||||
@@ -105,7 +115,9 @@ class LeaveRequestService(
|
|||||||
actor.id,
|
actor.id,
|
||||||
approverId,
|
approverId,
|
||||||
oaAdministratorId,
|
oaAdministratorId,
|
||||||
|
hrReviewerId,
|
||||||
durationMinutes,
|
durationMinutes,
|
||||||
|
existing.type.name,
|
||||||
)
|
)
|
||||||
repository.attachWorkflow(
|
repository.attachWorkflow(
|
||||||
actor.tenantId,
|
actor.tenantId,
|
||||||
|
|||||||
+2
@@ -6,4 +6,6 @@ interface ApprovalRoutingRepository {
|
|||||||
fun findDepartmentManager(tenantId: UUID, applicantId: UUID): UUID?
|
fun findDepartmentManager(tenantId: UUID, applicantId: UUID): UUID?
|
||||||
|
|
||||||
fun findOaAdministrator(tenantId: 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)
|
)?.get("user_id", UUID::class.java)
|
||||||
|
|
||||||
override fun findOaAdministrator(tenantId: UUID): UUID? =
|
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(
|
dsl.fetchOne(
|
||||||
"""
|
"""
|
||||||
SELECT ur.user_id
|
SELECT ur.user_id
|
||||||
@@ -48,7 +54,7 @@ class JooqApprovalRoutingRepository(
|
|||||||
JOIN authz.role role
|
JOIN authz.role role
|
||||||
ON role.tenant_id = ur.tenant_id
|
ON role.tenant_id = ur.tenant_id
|
||||||
AND role.id = ur.role_id
|
AND role.id = ur.role_id
|
||||||
AND role.code = 'oa_admin'
|
AND role.code = ?
|
||||||
AND role.status = 'ACTIVE'
|
AND role.status = 'ACTIVE'
|
||||||
JOIN identity.user_account account
|
JOIN identity.user_account account
|
||||||
ON account.tenant_id = ur.tenant_id
|
ON account.tenant_id = ur.tenant_id
|
||||||
@@ -60,6 +66,7 @@ class JooqApprovalRoutingRepository(
|
|||||||
ORDER BY ur.effective_from, ur.user_id
|
ORDER BY ur.effective_from, ur.user_id
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
|
roleCode,
|
||||||
tenantId,
|
tenantId,
|
||||||
)?.get("user_id", UUID::class.java)
|
)?.get("user_id", UUID::class.java)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ interface LeaveWorkflowGateway {
|
|||||||
applicantId: UUID,
|
applicantId: UUID,
|
||||||
approverId: UUID,
|
approverId: UUID,
|
||||||
oaAdministratorId: UUID,
|
oaAdministratorId: UUID,
|
||||||
|
hrReviewerId: UUID,
|
||||||
durationMinutes: Long,
|
durationMinutes: Long,
|
||||||
|
leaveType: String,
|
||||||
): StartedProcess
|
): StartedProcess
|
||||||
|
|
||||||
fun listAssignedTasks(assigneeId: UUID): List<WorkflowTask>
|
fun listAssignedTasks(assigneeId: UUID): List<WorkflowTask>
|
||||||
|
|||||||
+4
@@ -23,7 +23,9 @@ class FlowableLeaveWorkflowGateway(
|
|||||||
applicantId: UUID,
|
applicantId: UUID,
|
||||||
approverId: UUID,
|
approverId: UUID,
|
||||||
oaAdministratorId: UUID,
|
oaAdministratorId: UUID,
|
||||||
|
hrReviewerId: UUID,
|
||||||
durationMinutes: Long,
|
durationMinutes: Long,
|
||||||
|
leaveType: String,
|
||||||
): StartedProcess {
|
): StartedProcess {
|
||||||
val process = runtimeService.createProcessInstanceBuilder()
|
val process = runtimeService.createProcessInstanceBuilder()
|
||||||
.processDefinitionKey(PROCESS_DEFINITION_KEY)
|
.processDefinitionKey(PROCESS_DEFINITION_KEY)
|
||||||
@@ -35,7 +37,9 @@ class FlowableLeaveWorkflowGateway(
|
|||||||
"applicantId" to applicantId.toString(),
|
"applicantId" to applicantId.toString(),
|
||||||
"approverId" to approverId.toString(),
|
"approverId" to approverId.toString(),
|
||||||
"oaAdministratorId" to oaAdministratorId.toString(),
|
"oaAdministratorId" to oaAdministratorId.toString(),
|
||||||
|
"hrReviewerId" to hrReviewerId.toString(),
|
||||||
"durationMinutes" to durationMinutes,
|
"durationMinutes" to durationMinutes,
|
||||||
|
"leaveType" to leaveType,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.start()
|
.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:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xmlns:flowable="http://flowable.org/bpmn"
|
xmlns:flowable="http://flowable.org/bpmn"
|
||||||
targetNamespace="https://aioa.all8ai.com/processes">
|
targetNamespace="https://aioa.all8ai.com/processes">
|
||||||
<process id="leaveApproval" name="请假分级审批" isExecutable="true">
|
<process id="leaveApproval" name="请假分级与并行会签" isExecutable="true">
|
||||||
<startEvent id="start" name="申请已提交"/>
|
<startEvent id="start" name="申请已提交"/>
|
||||||
<sequenceFlow id="flow-start-manager" sourceRef="start" targetRef="managerReview"/>
|
<sequenceFlow id="flow-start-manager" sourceRef="start" targetRef="managerReview"/>
|
||||||
|
|
||||||
<userTask id="managerReview"
|
<userTask id="managerReview" name="部门主管审批" flowable:assignee="${approverId}"/>
|
||||||
name="部门主管审批"
|
|
||||||
flowable:assignee="${approverId}"/>
|
|
||||||
<sequenceFlow id="flow-manager-decision" sourceRef="managerReview" targetRef="managerDecision"/>
|
<sequenceFlow id="flow-manager-decision" sourceRef="managerReview" targetRef="managerDecision"/>
|
||||||
|
|
||||||
<exclusiveGateway id="managerDecision" name="主管审批结果"/>
|
<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>
|
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${approved == false}]]></conditionExpression>
|
||||||
</sequenceFlow>
|
</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>
|
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${approved == true}]]></conditionExpression>
|
||||||
</sequenceFlow>
|
</sequenceFlow>
|
||||||
|
|
||||||
<exclusiveGateway id="durationDecision" name="是否超过 24 小时"/>
|
<exclusiveGateway id="routeDecision" name="按时长和类型路由"/>
|
||||||
<sequenceFlow id="flow-short-approved" sourceRef="durationDecision" targetRef="approvedEnd">
|
<sequenceFlow id="flow-short-approved" sourceRef="routeDecision" targetRef="approvedEnd">
|
||||||
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${durationMinutes <= 1440}]]></conditionExpression>
|
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${durationMinutes <= 1440}]]></conditionExpression>
|
||||||
</sequenceFlow>
|
</sequenceFlow>
|
||||||
<sequenceFlow id="flow-long-oa-review" sourceRef="durationDecision" targetRef="oaReview">
|
<sequenceFlow id="flow-long-nonannual-oa" sourceRef="routeDecision" targetRef="oaSerialReview">
|
||||||
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${durationMinutes > 1440}]]></conditionExpression>
|
<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>
|
</sequenceFlow>
|
||||||
|
|
||||||
<userTask id="oaReview"
|
<userTask id="oaSerialReview" name="OA 管理员复核" flowable:assignee="${oaAdministratorId}"/>
|
||||||
name="OA 管理员复核"
|
<sequenceFlow id="flow-oa-serial-decision" sourceRef="oaSerialReview" targetRef="oaSerialDecision"/>
|
||||||
flowable:assignee="${oaAdministratorId}"/>
|
<exclusiveGateway id="oaSerialDecision" name="OA 复核结果"/>
|
||||||
<sequenceFlow id="flow-oa-decision" sourceRef="oaReview" targetRef="oaDecision"/>
|
<sequenceFlow id="flow-oa-serial-approved" sourceRef="oaSerialDecision" targetRef="approvedEnd">
|
||||||
|
|
||||||
<exclusiveGateway id="oaDecision" name="OA 复核结果"/>
|
|
||||||
<sequenceFlow id="flow-oa-approved" sourceRef="oaDecision" targetRef="approvedEnd">
|
|
||||||
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${approved == true}]]></conditionExpression>
|
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${approved == true}]]></conditionExpression>
|
||||||
</sequenceFlow>
|
</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>
|
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${approved == false}]]></conditionExpression>
|
||||||
</sequenceFlow>
|
</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="approvedEnd" name="已批准"/>
|
||||||
<endEvent id="rejectedEnd" name="已驳回"/>
|
<endEvent id="rejectedTerminate" name="已驳回并终止其他分支">
|
||||||
|
<terminateEventDefinition/>
|
||||||
|
</endEvent>
|
||||||
</process>
|
</process>
|
||||||
</definitions>
|
</definitions>
|
||||||
|
|||||||
+2
@@ -108,7 +108,9 @@ class ApprovalTaskServiceTest {
|
|||||||
applicantId: UUID,
|
applicantId: UUID,
|
||||||
approverId: UUID,
|
approverId: UUID,
|
||||||
oaAdministratorId: UUID,
|
oaAdministratorId: UUID,
|
||||||
|
hrReviewerId: UUID,
|
||||||
durationMinutes: Long,
|
durationMinutes: Long,
|
||||||
|
leaveType: String,
|
||||||
): StartedProcess = error("Not used")
|
): StartedProcess = error("Not used")
|
||||||
|
|
||||||
override fun listAssignedTasks(assigneeId: UUID): List<WorkflowTask> = listOf(task)
|
override fun listAssignedTasks(assigneeId: UUID): List<WorkflowTask> = listOf(task)
|
||||||
|
|||||||
+5
@@ -137,6 +137,9 @@ class LeaveRequestServiceTest {
|
|||||||
|
|
||||||
override fun findOaAdministrator(tenantId: UUID) =
|
override fun findOaAdministrator(tenantId: UUID) =
|
||||||
UUID.fromString("40000000-0000-7000-8000-000000000003")
|
UUID.fromString("40000000-0000-7000-8000-000000000003")
|
||||||
|
|
||||||
|
override fun findHrReviewer(tenantId: UUID) =
|
||||||
|
UUID.fromString("40000000-0000-7000-8000-000000000004")
|
||||||
},
|
},
|
||||||
FakeWorkflowGateway(),
|
FakeWorkflowGateway(),
|
||||||
)
|
)
|
||||||
@@ -148,7 +151,9 @@ class LeaveRequestServiceTest {
|
|||||||
applicantId: UUID,
|
applicantId: UUID,
|
||||||
approverId: UUID,
|
approverId: UUID,
|
||||||
oaAdministratorId: UUID,
|
oaAdministratorId: UUID,
|
||||||
|
hrReviewerId: UUID,
|
||||||
durationMinutes: Long,
|
durationMinutes: Long,
|
||||||
|
leaveType: String,
|
||||||
) = StartedProcess("process-$leaveRequestId", "leaveApproval:1:test")
|
) = StartedProcess("process-$leaveRequestId", "leaveApproval:1:test")
|
||||||
|
|
||||||
override fun listAssignedTasks(assigneeId: UUID): List<WorkflowTask> = emptyList()
|
override fun listAssignedTasks(assigneeId: UUID): List<WorkflowTask> = emptyList()
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
"realm": [
|
"realm": [
|
||||||
{ "name": "employee", "description": "普通员工" },
|
{ "name": "employee", "description": "普通员工" },
|
||||||
{ "name": "department_manager", "description": "部门主管" },
|
{ "name": "department_manager", "description": "部门主管" },
|
||||||
{ "name": "oa_admin", "description": "OA 管理员" }
|
{ "name": "oa_admin", "description": "OA 管理员" },
|
||||||
|
{ "name": "hr_reviewer", "description": "人力资源复核人" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"clients": [
|
"clients": [
|
||||||
@@ -79,6 +80,18 @@
|
|||||||
"attributes": { "tenant_id": ["00000000-0000-7000-8000-000000000001"] },
|
"attributes": { "tenant_id": ["00000000-0000-7000-8000-000000000001"] },
|
||||||
"credentials": [{ "type": "password", "value": "Admin123!", "temporary": false }],
|
"credentials": [{ "type": "password", "value": "Admin123!", "temporary": false }],
|
||||||
"realmRoles": ["employee", "oa_admin"]
|
"realmRoles": ["employee", "oa_admin"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "10000000-0000-7000-8000-000000000004",
|
||||||
|
"username": "hr",
|
||||||
|
"enabled": true,
|
||||||
|
"emailVerified": true,
|
||||||
|
"firstName": "人力",
|
||||||
|
"lastName": "HR",
|
||||||
|
"email": "hr@example.local",
|
||||||
|
"attributes": { "tenant_id": ["00000000-0000-7000-8000-000000000001"] },
|
||||||
|
"credentials": [{ "type": "password", "value": "HrReviewer123!", "temporary": false }],
|
||||||
|
"realmRoles": ["employee", "hr_reviewer"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
- [x] 发起、主管待办、批准、驳回、撤回和时间线
|
- [x] 发起、主管待办、批准、驳回、撤回和时间线
|
||||||
- [x] 多任务流程的中间审批事件与最终流程结束判定
|
- [x] 多任务流程的中间审批事件与最终流程结束判定
|
||||||
- [x] 按请假时长路由的主管与 OA 条件串行审批
|
- [x] 按请假时长路由的主管与 OA 条件串行审批
|
||||||
|
- [x] 长期年假的 OA 与 HR 并行会签及任一驳回终止
|
||||||
- 附件、通知、弱网恢复和幂等处理
|
- 附件、通知、弱网恢复和幂等处理
|
||||||
|
|
||||||
## M3:AI 最小闭环
|
## M3:AI 最小闭环
|
||||||
|
|||||||
@@ -44,9 +44,10 @@ Flowable 负责任务和流程路径,`business.leave_request` 仍是请假业
|
|||||||
└─ 同意
|
└─ 同意
|
||||||
├─ 时长 ≤ 24 小时 → APPROVED
|
├─ 时长 ≤ 24 小时 → APPROVED
|
||||||
└─ 时长 > 24 小时
|
└─ 时长 > 24 小时
|
||||||
→ OA 管理员复核
|
├─ 非年假 → OA 管理员串行复核
|
||||||
├─ 同意 → APPROVED
|
└─ 年假 → OA 与 HR 并行复核
|
||||||
└─ 驳回 → REJECTED
|
├─ 全部同意 → APPROVED
|
||||||
|
└─ 任一驳回 → 终止其他分支并 REJECTED
|
||||||
```
|
```
|
||||||
|
|
||||||
该流程已经验证条件网关和多级串行任务。后端状态同步按通用多任务语义设计,不会在第一个串行或并行任务完成时提前结束申请。
|
该流程已经验证条件网关、多级串行、并行拆分、并行汇聚和终止事件。后端状态同步按通用多任务语义设计,不会在第一个串行或并行任务完成时提前结束申请。
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
- 已结束流程不能撤回;审批前允许申请人撤回。
|
- 已结束流程不能撤回;审批前允许申请人撤回。
|
||||||
- 请假时长不超过 24 小时,由部门主管审批后结束。
|
- 请假时长不超过 24 小时,由部门主管审批后结束。
|
||||||
- 请假时长超过 24 小时,部门主管同意后必须由 OA 管理员串行复核。
|
- 请假时长超过 24 小时,部门主管同意后必须由 OA 管理员串行复核。
|
||||||
|
- 超过 24 小时的年假由 OA 管理员与 HR 并行复核,全部同意才批准,任一驳回即终止流程。
|
||||||
- 长期请假的申请人、部门主管和 OA 复核人必须满足职责分离。
|
- 长期请假的申请人、部门主管和 OA 复核人必须满足职责分离。
|
||||||
- 所有写请求必须携带 `Idempotency-Key`。
|
- 所有写请求必须携带 `Idempotency-Key`。
|
||||||
- 状态更新必须使用版本号防止并发覆盖。
|
- 状态更新必须使用版本号防止并发覆盖。
|
||||||
|
|||||||
@@ -15,3 +15,5 @@
|
|||||||
| AI 发起申请 | 用户确认后 | 用户确认后 | 用户确认后 |
|
| AI 发起申请 | 用户确认后 | 用户确认后 | 用户确认后 |
|
||||||
|
|
||||||
后端授权模型为 RBAC + 数据范围 + ABAC。表格是产品规则,不替代服务端逐资源鉴权。
|
后端授权模型为 RBAC + 数据范围 + ABAC。表格是产品规则,不替代服务端逐资源鉴权。
|
||||||
|
|
||||||
|
长期年假额外包含 `hr_reviewer` 业务角色。该角色只能处理明确分配给自己的 HR 复核任务,不能因此查看全组织申请。
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ backend_url="${BACKEND_URL:-http://localhost:8080}"
|
|||||||
|
|
||||||
curl --fail --silent --show-error "${backend_url}/actuator/health" | jq .
|
curl --fail --silent --show-error "${backend_url}/actuator/health" | jq .
|
||||||
|
|
||||||
for username in employee manager admin; do
|
for username in employee manager admin hr; do
|
||||||
case "${username}" in
|
case "${username}" in
|
||||||
employee) password='Employee123!' ;;
|
employee) password='Employee123!' ;;
|
||||||
manager) password='Manager123!' ;;
|
manager) password='Manager123!' ;;
|
||||||
admin) password='Admin123!' ;;
|
admin) password='Admin123!' ;;
|
||||||
|
hr) password='HrReviewer123!' ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
token="$({
|
token="$({
|
||||||
|
|||||||
Reference in New Issue
Block a user