docs(flow): add starter withdraw design

This commit is contained in:
PIGCLOUD committed 2026-03-18 23:57:10 +08:00
1 parent 9d6515d8fa
commit 2c70387ace
1 file changed
+431
@@ -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 `已撤回`