refactor: support multi-task Flowable approval semantics

This commit is contained in:
selfrelease
2026-07-18 08:43:45 +08:00
parent c5d80aba43
commit e46aa00e80
10 changed files with 333 additions and 20 deletions
@@ -69,7 +69,6 @@ class ApprovalTaskService(
}
val operation = if (approved) "LEAVE_APPROVE" else "LEAVE_REJECT"
val action = if (approved) "LEAVE_REQUEST_APPROVED" else "LEAVE_REQUEST_REJECTED"
val targetStatus = if (approved) LeaveStatus.APPROVED else LeaveStatus.REJECTED
val fingerprint = fingerprint(operation, taskId, expectedVersion, comment)
val claim = leaveRequestRepository.claimOperation(
@@ -86,18 +85,37 @@ class ApprovalTaskService(
throw ApiException(HttpStatus.CONFLICT, "APPROVAL_TASK_COMPLETED", "审批任务已处理")
}
val transitioned = leaveRequestRepository.transitionForApprover(
tenantId = actor.tenantId,
actorId = actor.id,
id = request.id,
expectedVersion = expectedVersion,
toStatus = targetStatus,
eventId = UuidV7.generate(),
eventType = action,
traceId = MDC.get(TraceIdFilter.MDC_TRACE_ID) ?: "unknown",
) ?: transitionFailure(actor, request.id, expectedVersion)
val completion = workflowGateway.completeTask(taskId, approved, comment)
val traceId = MDC.get(TraceIdFilter.MDC_TRACE_ID) ?: "unknown"
val action = when {
completion.processEnded && approved -> "LEAVE_REQUEST_APPROVED"
completion.processEnded -> "LEAVE_REQUEST_REJECTED"
approved -> "LEAVE_APPROVAL_TASK_APPROVED"
else -> "LEAVE_APPROVAL_TASK_REJECTED"
}
val result = if (completion.processEnded) {
leaveRequestRepository.transitionForApprover(
tenantId = actor.tenantId,
actorId = actor.id,
id = request.id,
expectedVersion = expectedVersion,
toStatus = targetStatus,
eventId = UuidV7.generate(),
eventType = action,
traceId = traceId,
) ?: transitionFailure(actor, request.id, expectedVersion)
} else {
leaveRequestRepository.appendWorkflowEvent(
eventId = UuidV7.generate(),
tenantId = actor.tenantId,
leaveRequestId = request.id,
actorId = actor.id,
eventType = action,
traceId = traceId,
)
request
}
workflowGateway.completeTask(taskId, approved, comment)
auditService.recordSuccess(
actor = actor,
action = action,
@@ -106,12 +124,14 @@ class ApprovalTaskService(
idempotencyKey = idempotencyKey,
details = mapOf(
"taskId" to taskId,
"toStatus" to targetStatus.name,
"version" to transitioned.version,
"decision" to if (approved) "APPROVED" else "REJECTED",
"processEnded" to completion.processEnded,
"toStatus" to result.status.name,
"version" to result.version,
"comment" to comment?.trim(),
),
)
return transitioned
return result
}
private fun transitionFailure(actor: CurrentUser, id: java.util.UUID, expectedVersion: Long): Nothing {
@@ -105,6 +105,15 @@ interface LeaveRequestRepository {
eventType: String,
traceId: String,
): LeaveRequest?
fun appendWorkflowEvent(
eventId: UUID,
tenantId: UUID,
leaveRequestId: UUID,
actorId: UUID,
eventType: String,
traceId: String,
)
}
data class IdempotentDraft(
@@ -291,6 +291,30 @@ class JooqLeaveRequestRepository(
return transitioned
}
override fun appendWorkflowEvent(
eventId: UUID,
tenantId: UUID,
leaveRequestId: UUID,
actorId: UUID,
eventType: String,
traceId: String,
) {
dsl.execute(
"""
INSERT INTO business.leave_request_event (
id, tenant_id, leave_request_id, actor_id, event_type,
from_status, to_status, trace_id
) VALUES (?, ?, ?, ?, ?, 'PENDING', 'PENDING', ?)
""".trimIndent(),
eventId,
tenantId,
leaveRequestId,
actorId,
eventType,
traceId,
)
}
private fun map(record: Record): LeaveRequest = LeaveRequest(
id = record.get("id", UUID::class.java)!!,
tenantId = record.get("tenant_id", UUID::class.java)!!,
@@ -15,7 +15,7 @@ interface LeaveWorkflowGateway {
fun resolveTask(taskId: String): WorkflowTask?
fun completeTask(taskId: String, approved: Boolean, comment: String?)
fun completeTask(taskId: String, approved: Boolean, comment: String?): TaskCompletion
fun cancelProcess(processInstanceId: String, reason: String)
}
@@ -34,3 +34,8 @@ data class WorkflowTask(
val createdAt: Instant,
val completed: Boolean,
)
data class TaskCompletion(
val processInstanceId: String,
val processEnded: Boolean,
)
@@ -3,6 +3,7 @@ package com.all8ai.aioa.workflow.infrastructure
import com.all8ai.aioa.workflow.domain.LeaveWorkflowGateway
import com.all8ai.aioa.workflow.domain.StartedProcess
import com.all8ai.aioa.workflow.domain.WorkflowTask
import com.all8ai.aioa.workflow.domain.TaskCompletion
import org.flowable.engine.HistoryService
import org.flowable.engine.RuntimeService
import org.flowable.engine.TaskService
@@ -70,13 +71,17 @@ class FlowableLeaveWorkflowGateway(
)
}
override fun completeTask(taskId: String, approved: Boolean, comment: String?) {
override fun completeTask(taskId: String, approved: Boolean, comment: String?): TaskCompletion {
val task = taskService.createTaskQuery().taskId(taskId).singleResult()
?: error("Active workflow task not found: $taskId")
if (!comment.isNullOrBlank()) {
val task = taskService.createTaskQuery().taskId(taskId).singleResult()
?: error("Active workflow task not found: $taskId")
taskService.addComment(taskId, task.processInstanceId, comment.trim())
}
taskService.complete(taskId, mapOf("approved" to approved))
val processEnded = runtimeService.createProcessInstanceQuery()
.processInstanceId(task.processInstanceId)
.singleResult() == null
return TaskCompletion(task.processInstanceId, processEnded)
}
override fun cancelProcess(processInstanceId: String, reason: String) {