From 2c70387acee97804a104347ea04d9b83dcfb00d7 Mon Sep 17 00:00:00 2001 From: PIGCLOUD Date: Wed, 18 Mar 2026 23:57:10 +0800 Subject: [PATCH] docs(flow): add starter withdraw design --- docs/plans/2026-03-18-flow-withdraw-design.md | 431 ++++++++++++++++++ 1 file changed, 431 insertions(+) create mode 100644 docs/plans/2026-03-18-flow-withdraw-design.md diff --git a/docs/plans/2026-03-18-flow-withdraw-design.md b/docs/plans/2026-03-18-flow-withdraw-design.md new file mode 100644 index 000000000..9f2c57e16 --- /dev/null +++ b/docs/plans/2026-03-18-flow-withdraw-design.md @@ -0,0 +1,431 @@ +# Flow Starter Withdraw Design + +## Scope + +This design covers the starter-side "withdraw process" capability across: + +- `pigx/pigx-flow` +- `pigx-ui-pro/src/views/flow` + +The goal is to add a clear "withdraw" action for starters without conflating it with the existing "terminate process" capability. + +## Problem Statement + +The current starter-facing page [started.vue](/Users/lengleng/work/dev1227/pigx-ui-pro/src/views/flow/task/started.vue) exposes `终止流程`, but the backend implementation is a force-stop semantic: + +- API: `POST /task/task/stopProcessInstance` +- Service: `TaskServiceImpl.stopProcessInstance` +- End reason: `finishReason=9` + +This is not the same as business-level "withdraw": + +- `withdraw` means the starter takes back a just-submitted process before the first approval has been consumed +- `terminate` means a stronger administrative stop on an in-flight process + +If the project keeps reusing terminate for starter withdraw, UI labels, audit semantics, business status sync, and future permission rules will remain mixed. + +## Confirmed Product Decisions + +### Withdraw Semantics + +Use product option `B`: + +- the starter can withdraw only when the first approval stage has not been processed yet + +### Starter-Side Operation Set + +On the starter side: + +- remove the generic `终止流程` button +- keep `撤回` as the only starter-side stop action + +### Business Visibility + +Business pages such as leave requests must explicitly show `已撤回`, not collapse it into `已结束`. + +## Goals + +1. Add a clear starter-only `撤回` action +2. Keep `撤回` and `终止` as separate backend semantics +3. Compute withdraw availability server-side and expose it to the UI +4. Sync withdraw result to business tables through the existing flow status event chain +5. Avoid duplicating withdraw logic in each business form page + +## Non-Goals + +1. Do not redesign admin-side force-stop permissions in this iteration +2. Do not add withdraw-reason input in V1 +3. Do not add business-form-specific withdraw buttons in every detail page in V1 + +## Recommended Placement + +### Primary Entry + +Use the starter list page: + +- [started.vue](/Users/lengleng/work/dev1227/pigx-ui-pro/src/views/flow/task/started.vue) + +Reason: + +- it already represents the "我发起的" mental model +- it already groups starter-only actions such as `重新提交` +- it keeps withdraw as a platform capability instead of pushing it into each business page + +### Secondary Entry + +Add the same withdraw action in the detail drawer header opened from `started.vue`. + +Reason: + +- users often inspect details before acting +- it avoids forcing the user back to the list page + +### Not Recommended for V1 + +Do not make business detail pages such as [detail.vue](/Users/lengleng/work/dev1227/pigx-ui-pro/src/views/flow/oaLeave/detail.vue) the primary entry. + +Reason: + +- this would duplicate logic across each business form +- withdraw is a workflow-center capability, not a business-form-specific capability + +## User Interaction Design + +### Starter List Display Rules + +On [started.vue](/Users/lengleng/work/dev1227/pigx-ui-pro/src/views/flow/task/started.vue): + +- show `撤回` only when `canWithdraw === true` +- show `重新提交` when `rejectToStarter === true` +- do not show the old `终止流程` button for normal starters +- show `已撤回` when `finishReason === '3'` + +### Detail Drawer Display Rules + +In the detail drawer header: + +- show `撤回` when `canWithdraw === true` +- optionally show disabled state plus `withdrawDisabledReason` when not allowed + +### Confirmation Copy + +Use a confirmation dialog before submit: + +- title: `确认撤回流程?` +- content: `仅首个审批节点尚未处理时可撤回。撤回后流程将结束,且不可恢复。` + +## Approach Comparison + +### Option 1: Reuse Existing Stop API + +Description: + +- keep calling `stopProcessInstance` +- only change button label from `终止流程` to `撤回` + +Pros: + +- lowest implementation cost + +Cons: + +- backend semantic remains wrong +- audit and business sync still treat starter withdraw as terminate +- impossible to distinguish `已撤回` from `已终止` + +Conclusion: + +- reject + +### Option 2: New Withdraw API with Shared Internal Termination Path + +Description: + +- add a new starter-facing withdraw API +- keep terminate as a separate API +- internally share low-level engine shutdown logic + +Pros: + +- clear product semantics +- supports distinct UI labels and business status mapping +- keeps future admin permission expansion clean + +Cons: + +- needs small backend refactor + +Conclusion: + +- recommend + +### Option 3: Business-Form-Specific Withdraw Entry First + +Description: + +- add withdraw directly into each business detail page + +Pros: + +- strong local context + +Cons: + +- duplicated logic across forms +- weak platform consistency + +Conclusion: + +- reject for V1 + +## Backend Design + +### New API + +Add a new endpoint in [ProcessTaskController.java](/Users/lengleng/work/dev1227/pigx/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/controller/ProcessTaskController.java): + +```http +POST /task/task/withdrawProcessInstance +``` + +Request body: + +```json +{ + "processInstanceId": "xxx" +} +``` + +Use existing DTO [TaskParamDto.java](/Users/lengleng/work/dev1227/pigx/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/TaskParamDto.java) in V1. + +### Service Contract + +Add method to [ITaskService.java](/Users/lengleng/work/dev1227/pigx/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/ITaskService.java): + +```java +R withdrawProcessInstance(TaskParamDto taskParamDto); +``` + +Implement in [TaskServiceImpl.java](/Users/lengleng/work/dev1227/pigx/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/TaskServiceImpl.java). + +### Withdraw Eligibility Rule + +Do not try to hard-parse "first approval node" from the process graph for V1. + +Instead, use the current persistence model: + +1. query `process_instance_record` +2. require: + - current user is starter + - `status == 1` + - no final `finishReason` +3. reject when runtime variable `rejectToStarter == true` +4. inspect `process_node_record_assign_user` +5. if any record exists for the same `processInstanceId` where: + - `nodeId != 'root'` + - and the task has already been processed + then withdraw is not allowed + +Suggested definition of "already processed": + +- `end_time is not null`, or +- `status != JXZ` + +This rule aligns with the current audit model because completed-task reads already rely on [ProcessNodeRecordAssignUser.java](/Users/lengleng/work/dev1227/pigx/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/entity/ProcessNodeRecordAssignUser.java). + +### Shared Internal Termination Logic + +Refactor the current terminate implementation into a shared internal method, for example: + +```java +private R terminateProcessInstance(String processInstanceId, String finishReason) +``` + +Use it from: + +- `stopProcessInstance(..., finishReason='9')` +- `withdrawProcessInstance(..., finishReason='3')` + +This keeps engine-level stop logic shared while preserving product-level semantics. + +### Persistence Update Rules + +On successful withdraw: + +1. end the Flowable runtime instance +2. update `process_instance_record` + - `status = 2` + - `finish_reason = '3'` + - `end_time = now` +3. close in-progress `process_node_record` +4. close in-progress `process_node_record_assign_user` + +The main requirement is that no runtime or local audit record remains in a "进行中" state after withdraw. + +## Frontend Design + +### API Layer + +Add a new API in [src/api/flow/task/index.ts](/Users/lengleng/work/dev1227/pigx-ui-pro/src/api/flow/task/index.ts): + +```ts +export function withdrawProcessInstance(param: { processInstanceId: string }) { ... } +``` + +### Starter List Data + +Extend [ProcessInstanceRecordVo.java](/Users/lengleng/work/dev1227/pigx/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/vo/ProcessInstanceRecordVo.java) with: + +- `Boolean canWithdraw` +- `String withdrawDisabledReason` + +Populate both in: + +- `queryMineStarted` +- `detail(processInstanceId)` + +This keeps all action availability server-driven. + +### Starter Page Behavior + +Update [started.vue](/Users/lengleng/work/dev1227/pigx-ui-pro/src/views/flow/task/started.vue): + +1. remove starter-facing `终止流程` +2. add `撤回` +3. keep `重新提交` unchanged for `rejectToStarter` +4. add `已撤回` tag for `finishReason === '3'` +5. add withdraw action in drawer header as secondary entry + +## Flow Status and Business Status Design + +### Flow-Level Final States + +Keep `process_instance_record.status` as coarse state: + +- `1` 进行中 +- `2` 已结束 + +Use `finish_reason` for final meaning: + +- `1` 通过 +- `0` 拒绝 +- `3` 撤回 +- `9` 终止 + +### Business Table Mapping + +For leave requests in [BpmOaLeaveEntity.java](/Users/lengleng/work/dev1227/pigx/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/entity/BpmOaLeaveEntity.java), the current `leaveStatus` values are too coarse. + +Recommended mapping: + +- `0` 待审批 +- `1` 进行中 +- `2` 已通过 +- `3` 已撤回 +- `4` 已拒绝 +- `5` 已终止 + +If schema change must stay minimal, a fallback is adding a separate finish-reason field. That is not recommended for V1 because UI filtering and display would become more fragmented. + +### Event-Driven Sync + +Continue using the existing status event chain: + +- [FlowProcessEventListener.java](/Users/lengleng/work/dev1227/pigx/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/listeners/FlowProcessEventListener.java) +- [NodeTaskCompleteNotify.java](/Users/lengleng/work/dev1227/pigx/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/listeners/NodeTaskCompleteNotify.java) +- [IProcessInstanceStatusEventService.java](/Users/lengleng/work/dev1227/pigx/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/event/IProcessInstanceStatusEventService.java) + +Update [BpmOaLeaveServiceImpl.java](/Users/lengleng/work/dev1227/pigx/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/BpmOaLeaveServiceImpl.java) so `handleStatusEvent` maps based on: + +- `status` +- `finishReason` + +not just `status`. + +This design should become the standard pattern for other business tables too. + +## Detail Response Contract + +The current [ProcessInstanceServiceImpl.java](/Users/lengleng/work/dev1227/pigx/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/ProcessInstanceServiceImpl.java) `detail(processInstanceId)` response should be extended with: + +- `canWithdraw` +- `withdrawDisabledReason` +- current `finishReason` + +This lets the drawer header and later business detail integrations share one source of truth. + +## Validation and Error Handling + +### Backend Errors + +Suggested failure messages: + +- `流程不存在或已结束` +- `仅发起人可撤回流程` +- `驳回待提交状态不可撤回` +- `首个审批节点已处理,无法撤回` + +### Frontend Behavior + +On withdraw failure: + +- show backend error message directly when available +- refresh list after success +- close detail drawer after success if triggered there + +## Acceptance Criteria + +### UI Acceptance + +1. `我发起的` page shows `撤回` only for eligible in-progress instances +2. `重新提交` remains available only for reject-to-starter instances +3. starter-facing `终止流程` no longer appears +4. withdrawn instances show `已撤回` +5. detail drawer header stays consistent with list action availability + +### Backend Acceptance + +1. withdraw is rejected once any non-root approval record has been processed +2. withdraw ends the runtime instance cleanly +3. `process_instance_record.finish_reason='3'` after withdraw +4. no in-progress node or assign-user record remains after withdraw +5. existing terminate path still yields `finishReason='9'` + +### Business Acceptance + +1. leave request business status explicitly displays `已撤回` +2. leave request no longer collapses withdraw into generic `已结束` + +## Risks + +1. `ProcessNodeRecordAssignUser.status` comments and actual usage are not fully consistent, so withdraw implementation should rely on current persisted behavior, not comments alone +2. some business modules may currently map only `status` and ignore `finishReason`, causing partial UI mismatch until each listener implementation is updated +3. parallel first-approval scenarios must use "any processed non-root approval record" as the blocking condition + +## Rollout Order + +1. add backend withdraw API and shared termination helper +2. add `finishReason='3'` support in flow list/detail responses +3. update starter page actions and status tags +4. update leave business status mapping and display +5. verify happy path and blocked path with real process instances + +## Implementation Outline + +The `writing-plans` skill referenced by the brainstorming workflow is not available in this workspace. As a fallback, use this execution order: + +1. Backend + - add `withdrawProcessInstance` controller/service method + - extract shared terminate helper + - return `canWithdraw` and `withdrawDisabledReason` from starter list and detail +2. Frontend + - add withdraw API client + - replace starter-side terminate button with withdraw + - add withdrawn status tag and detail-header action +3. Business sync + - extend leave status mapping for withdrawn + - update leave list/detail labels and filters if needed +4. Verification + - test starter can withdraw before first approval action + - test withdraw blocked after any first-stage approval processing + - test business page shows `已撤回`