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 `已撤回` diff --git a/docs/plans/2026-03-19-flow-withdraw-resubmit-design.md b/docs/plans/2026-03-19-flow-withdraw-resubmit-design.md new file mode 100644 index 000000000..014067af5 --- /dev/null +++ b/docs/plans/2026-03-19-flow-withdraw-resubmit-design.md @@ -0,0 +1,377 @@ +# Flow Withdraw Resubmit Design + +## Scope + +This design covers starter-side "resubmit after withdraw" behavior for: + +- `pigx-ui-pro/src/views/flow/task/started.vue` +- dynamic flow forms +- custom business forms + +The capability is limited to processes already withdrawn by the starter. It does not reopen the old process instance. + +## Problem Statement + +The current flow center supports: + +- withdraw for starter-side in-progress instances +- resubmit for `rejectToStarter` + +But it does not support the common case where the starter withdraws because they found mistakes in the form and wants to: + +1. edit the old submitted data +2. keep the withdrawn record as history +3. start a brand new process instance from the corrected data + +This must work for both: + +- dynamic flow forms +- custom business forms + +## Confirmed Product Decisions + +1. After withdraw, resubmission must create a new process instance +2. The old withdrawn record must remain as a separate historical record +3. The entry point should be a single user-facing capability: `重新编辑提交` +4. The implementation path should split by form type: + - dynamic flow form: edit in flow center drawer + - business form: edit in a page/container path +5. Previous manually selected approvers / copy recipients should be carried as defaults and remain editable + +## Goals + +1. Add `重新编辑提交` for withdrawn starter records +2. Let users correct form mistakes before submitting again +3. Preserve withdrawn process history +4. Reuse existing form loading and start-process capabilities where possible +5. Support both dynamic and business forms in the same product capability + +## Non-Goals + +1. Do not reopen the old withdrawn Flowable instance +2. Do not merge new submission into the old withdrawn record +3. Do not copy approval history from the old instance to the new one +4. Do not introduce a dedicated backend "withdraw-resubmit" API in V1 + +## Recommended Approach + +### Option 1: Unified Entry, Split by Form Type + +Description: + +- show one action named `重新编辑提交` +- branch internally by form type +- dynamic flow forms open an editable drawer in flow center +- business forms go through a common restart container page + +Pros: + +- unified user mental model +- minimal duplication +- works for both form types +- avoids forcing every business form page to implement workflow-side participant editing on its own + +Cons: + +- needs a common restart context loader +- needs one new common business-form restart container page + +Conclusion: + +- recommend + +### Option 2: Always Jump to a Full Page + +Description: + +- both dynamic forms and business forms always jump to a dedicated page + +Pros: + +- interaction form is consistent + +Cons: + +- duplicates current dynamic-form drawer flow +- larger change surface + +Conclusion: + +- not recommended for V1 + +### Option 3: Always Stay in started.vue Drawer + +Description: + +- both dynamic forms and business forms are handled inside `started.vue` + +Pros: + +- entry is centralized + +Cons: + +- business forms often depend on route/page context +- harder to keep complex custom forms maintainable + +Conclusion: + +- reject + +## Product Behavior + +### Starter List + +On [started.vue](/Users/lengleng/work/dev1227/pigx-ui-pro/src/views/flow/task/started.vue): + +- records with process status `已撤回` should display `重新编辑提交` +- records with `rejectToStarter` should keep the current `重新提交` +- these are different capabilities and must remain separate + +### Old Record Handling + +After successful resubmission: + +- the old record remains `已撤回` +- a new record appears as `进行中` + +### Button Naming + +Use a single starter-facing action name: + +- `重新编辑提交` + +Reason: + +- it makes the user intent explicit +- it distinguishes this flow from `驳回待提交 -> 重新提交` + +## Data Flow Design + +### Shared Source Data + +When the user clicks `重新编辑提交`, frontend should load: + +1. old instance snapshot via `detail(processInstanceId)` +2. current flow definition via `getFlowDetail(flowId)` + +The old instance snapshot provides: + +- previous form data +- previous manually selected participant values stored in submitted process params + +The current flow definition provides: + +- current form type +- current form component path / dynamic form schema +- current selectable participant nodes + +### Participant Default Value Strategy + +Only carry forward values the user explicitly selected before. + +Use old submitted process params such as: + +- `nodeId_assignee_select` + +Do not carry forward: + +- historical approval results +- historical comments +- runtime-derived approval outcomes + +If a previously selected node no longer exists in the current flow definition: + +- ignore the old value + +If a new required selectable node is present in the current definition: + +- require the user to select it again before submit + +## Form-Type Split + +### Dynamic Flow Form + +When `formType=10`: + +- stay in [started.vue](/Users/lengleng/work/dev1227/pigx-ui-pro/src/views/flow/task/started.vue) +- open an editable restart drawer +- left side reuses dynamic form rendering +- right side reuses [FlowNodeFormatData.vue](/Users/lengleng/work/dev1227/pigx-ui-pro/src/views/flow/form/tools/FlowNodeFormatData.vue) +- old participant selections are injected into `nodeUser` +- submit starts a new process instance + +### Business Form + +When `formType=20`: + +- do not jump directly to each existing business create page as the primary architecture +- add a common restart container page under flow task module +- the container page is responsible for: + - loading the business form create component + - loading the current flow definition + - rendering the right-side participant panel + - orchestrating final submit as a new process start + +Reason: + +- existing business pages usually do not own a common workflow participant panel +- a common restart container keeps workflow behavior centralized and reusable + +## Frontend Component Boundary + +### started.vue + +Responsibilities: + +- expose `重新编辑提交` entry for withdrawn records +- decide route vs drawer by form type +- continue handling current starter list concerns + +### New Restart Context Loader + +Add a shared loader/composable responsible for: + +- fetching old instance detail +- fetching current flow detail +- building initial form values +- building initial participant defaults +- exposing the resolved restart mode + +### FlowNodeFormatData.vue + +Needs a small enhancement: + +- allow external default `nodeUser` injection for restart scenarios + +### New Business Restart Container Page + +Responsibilities: + +- render the business create component dynamically +- render `FlowNodeFormatData` +- merge edited business form data and participant selections +- start a new process instance + +## Existing Capabilities Reused + +### Frontend + +- [dynamicComponent.ts](/Users/lengleng/work/dev1227/pigx-ui-pro/src/views/flow/workflow/utils/dynamicComponent.ts) +- [useTaskForm.ts](/Users/lengleng/work/dev1227/pigx-ui-pro/src/views/flow/task/composables/useTaskForm.ts) +- [FlowNodeFormatData.vue](/Users/lengleng/work/dev1227/pigx-ui-pro/src/views/flow/form/tools/FlowNodeFormatData.vue) +- [startFlow](/Users/lengleng/work/dev1227/pigx-ui-pro/src/api/flow/flow/index.ts) + +### Backend + +V1 should reuse existing APIs: + +- `detail(processInstanceId)` +- `getFlowDetail(flowId)` +- `startProcessInstance / startFlow` + +No dedicated backend restart API is required in V1. + +## Business Form Compatibility + +For business forms to participate in V1: + +- their create component must support loading old data by `processInstanceId` + +Example: + +- [create.vue](/Users/lengleng/work/dev1227/pigx-ui-pro/src/views/flow/oaLeave/create.vue) already supports querying old data with `processInstanceId` + +If some business form cannot load old data this way: + +- it is out of V1 support unless adapted + +## Submit Semantics + +### Dynamic Form Submit + +Payload: + +- edited dynamic form data +- participant selections from `FlowNodeFormatData` + +Behavior: + +- call process start API +- create a new process instance + +### Business Form Submit + +Payload: + +- edited business form data from the business create component +- participant selections from `FlowNodeFormatData` + +Behavior: + +- call business-form submit path that starts a new process instance +- create a new process instance + +## Success and Failure UX + +### Success + +Message: + +- `已重新发起流程` + +Behavior: + +- close restart drawer or leave restart page +- return to starter list +- refresh list + +### Failure + +If current flow definition is unavailable: + +- `当前流程已不可发起` + +If business form component path is invalid: + +- `业务表单不可用,无法重新发起` + +If old participant values are no longer valid: + +- do not block opening +- require reselection at validation time + +## Acceptance Criteria + +1. Withdrawn starter records show `重新编辑提交` +2. Dynamic flow forms open editable restart drawer with old form values +3. Business forms open common restart container with old form values +4. Previous manually selected approvers/copy users are prefilled when possible +5. User can change both form data and participant selections before submit +6. Submit creates a new process instance +7. Old withdrawn record remains unchanged +8. New record appears independently in starter list + +## Risks + +1. Business forms vary in how much they depend on route context +2. Some business create components may not yet support restart-container integration cleanly +3. Participant default values rely on old submitted explicit selections, not on all runtime assignment types + +## Recommended V1 Rollout + +1. Dynamic flow forms first +2. Common restart container for business forms +3. Verify `oaLeave` as the first business-form example +4. Expand to additional business forms that already support `processInstanceId`-based reload + +## Implementation Outline + +The `writing-plans` skill referenced by brainstorming is unavailable in this workspace. As a fallback, implement in this order: + +1. Add withdrawn-record `重新编辑提交` action to starter list +2. Add restart context loader and split logic by form type +3. Add dynamic-form restart drawer path +4. Add business-form restart container page +5. Enhance participant panel to accept default values +6. Verify `oaLeave` and one dynamic form end to end