Переглянути джерело

feat(approvalFlow): 审批流综合优化 P0/P1/P2 + P1-5 并行网关; chore: bump version 2.4.80 / 1.0.47

核心引擎 / 后端:
- 新增 ApprovalFlowCompletedNode 子表(P1-5 并行网关的 Join 判定 / 未来节点级统计基座,方案B)
- FlowEngineService 重构: 引入 ProcessNextNode / MarkNodeCompleted / GetCompletedNodeIdSet / CancelAllPendingTasks
- 并行网关 Fork + Join 语义;Reject 时实例级取消所有 pending 任务;CompleteInstance 幂等
- P0-2 审批人类型新增 DepartmentLeader(部门主管)
- P1-6 流程代理 ApprovalFlowDelegate:CRUD + 并行任务 + 配对取消(FlowDelegateService)
- P1-7 流程版本快照 ApprovalFlowVersion:保存快照 / 列表 / 回退
- P1-8 批量同意 / 批量拒绝
- P2-10 实例详情返回 FlowJsonSnapshot + CompletedNodeIds + CurrentNodeIds
- P2-11 意见模板 ApprovalFlowCommentTemplate + FlowCommentTemplateService
- 异常升级机制: FlowTimeoutJob 后台作业
- FlowTaskOutput 增加 IsDelegate / DelegateForUser* 字段

前端 / Web:
- LogicFlow 设计器加入并行网关(PanelNode + PropertyCommon 提示面板)
- 新增 FlowGraphPreview 组件:支持多当前节点高亮 + 已完成节点着色
- TimelineDialog / center/index 消费 completedNodeIds + currentNodeIds(含旧实例回退推断)
- ApprovalPanel 自动取当前 userId;审批意见模板选择
- center/PendingList 批量操作 UI;顶栏 user.vue 待办徽标
- 新增流程代理管理页 delegate/index

文档:
- doc/审批流功能说明.md 更新
- doc/plan/审批流-综合优化方案.md(P0-P3 全量方案)
- doc/plan/审批流-流程升级机制实施方案.md

验证:
- 基础 / 部门主管 / 模板 / 版本 / 批量 / 代理 E2E 均通过
- P1-5 并行网关: _verify_parallel_e2e.py 16/16;_verify_real_parallel.py 14/14(订单评审真实并行流程,含顺序/反序/单支拒绝)
skygu 3 місяців тому
батько
коміт
67b2d696c2
40 змінених файлів з 3721 додано та 81 видалено
  1. 1 1
      Web/package.json
  2. 33 8
      Web/src/layout/navBars/topBar/user.vue
  3. 62 0
      Web/src/views/approvalFlow/api.ts
  4. 29 1
      Web/src/views/approvalFlow/center/components/ApprovalDialog.vue
  5. 49 27
      Web/src/views/approvalFlow/center/components/PendingList.vue
  6. 55 16
      Web/src/views/approvalFlow/center/components/TimelineDialog.vue
  7. 79 3
      Web/src/views/approvalFlow/center/index.vue
  8. 72 4
      Web/src/views/approvalFlow/component/ApprovalPanel.vue
  9. 160 0
      Web/src/views/approvalFlow/component/FlowGraphPreview.vue
  10. 35 0
      Web/src/views/approvalFlow/component/LogicFlow/Panel/PanelNode.vue
  11. 150 0
      Web/src/views/approvalFlow/component/LogicFlow/Property/PropertyCommon.vue
  12. 240 0
      Web/src/views/approvalFlow/delegate/index.vue
  13. 57 2
      Web/src/views/approvalFlow/index.vue
  14. 927 0
      doc/plan/审批流-流程升级机制实施方案.md
  15. 299 0
      doc/plan/审批流-综合优化方案.md
  16. 73 0
      doc/审批流功能说明.md
  17. 3 3
      server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj
  18. 39 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Entity/ApprovalFlowCommentTemplate.cs
  19. 43 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Entity/ApprovalFlowCompletedNode.cs
  20. 67 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Entity/ApprovalFlowDelegate.cs
  21. 19 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Entity/ApprovalFlowTask.cs
  22. 44 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Entity/ApprovalFlowVersion.cs
  23. 3 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Enum/ApproverTypeEnum.cs
  24. 3 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Enum/FlowLogActionEnum.cs
  25. 3 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Enum/FlowTaskStatusEnum.cs
  26. 86 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Job/FlowTimeoutJob.cs
  27. 1 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/SeedData/SysMenuSeedData.cs
  28. 72 1
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/ApprovalFlow/ApprovalFlowService.cs
  29. 32 2
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/ApprovalFlow/Dto/ApprovalFlowItem.cs
  30. 11 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/ApprovalFlow/Dto/ApprovalFlowOutput.cs
  31. 129 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/FlowCommentTemplate/FlowCommentTemplateService.cs
  32. 169 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/FlowDelegate/FlowDelegateService.cs
  33. 33 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/FlowEngine/Dto/FlowEngineDtos.cs
  34. 451 12
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/FlowEngine/FlowEngineService.cs
  35. 21 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/FlowInstance/Dto/FlowInstanceDtos.cs
  36. 21 1
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/FlowInstance/FlowInstanceService.cs
  37. 30 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/FlowNotify/FlowNotifyService.cs
  38. 16 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/FlowTask/Dto/FlowTaskDtos.cs
  39. 92 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/FlowTask/FlowTaskService.cs
  40. 12 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Startup.cs

+ 1 - 1
Web/package.json

@@ -1,7 +1,7 @@
 {
 	"name": "admin.net",
 	"type": "module",
-	"version": "2.4.77",
+	"version": "2.4.80",
 	"packageManager": "pnpm@10.32.1",
 	"lastBuildTime": "2026.03.15",
 	"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",

+ 33 - 8
Web/src/layout/navBars/topBar/user.vue

@@ -34,6 +34,13 @@
 		<div class="layout-navbars-breadcrumb-user-icon" @click="onLayoutSetingClick">
 			<i class="icon-skin iconfont" title="布局配置"></i>
 		</div>
+		<div class="layout-navbars-breadcrumb-user-icon" @click="goApprovalCenter" style="cursor: pointer">
+			<el-badge :value="approvalPendingCount" :hidden="approvalPendingCount <= 0" :max="99">
+				<el-icon title="审批待办">
+					<ele-Checked />
+				</el-icon>
+			</el-badge>
+		</div>
 		<div class="layout-navbars-breadcrumb-user-icon">
 			<el-popover placement="bottom" trigger="click" transition="el-zoom-in-top" :width="400" :persistent="false">
 				<template #reference>
@@ -92,7 +99,7 @@
 </template>
 
 <script setup lang="ts" name="layoutBreadcrumbUser">
-import { defineAsyncComponent, ref, computed, reactive, onMounted } from 'vue';
+import { defineAsyncComponent, ref, computed, reactive, onMounted, onUnmounted } from 'vue';
 import { useCssVar } from '@vueuse/core'
 import { useRouter } from 'vue-router';
 import { ElMessageBox, ElMessage, ElNotification } from 'element-plus';
@@ -110,8 +117,24 @@ import { accessTokenKey, clearAccessAfterReload, getAPI } from '/@/utils/axios-u
 import { SysAuthApi, SysNoticeApi, SysUserApi } from '/@/api-services/api';
 import { auth } from '/@/utils/authFunction';
 import { useLangStore } from '/@/stores/useLangStore';
+import { myPendingCount } from '/@/views/approvalFlow/api';
 
 const langStore = useLangStore();
+const approvalPendingCount = ref(0);
+let approvalPollTimer: ReturnType<typeof setInterval> | null = null;
+
+const fetchApprovalCount = async () => {
+	try {
+		const res = await myPendingCount();
+		approvalPendingCount.value = res.data?.result ?? 0;
+	} catch {
+		/* ignore */
+	}
+};
+
+const goApprovalCenter = () => {
+	router.push('/aidop/flowManage/approvalFlowCenter');
+};
 // 引入组件
 const UserNews = defineAsyncComponent(() => import('/@/layout/navBars/topBar/userNews.vue'));
 const Search = defineAsyncComponent(() => import('/@/layout/navBars/topBar/search.vue'));
@@ -289,14 +312,16 @@ onMounted(async () => {
 	// 接收站内信
 	signalR.on('PublicNotice', receiveNotice);
 
-	// // 处理消息已读
-	// mittBus.on('noticeRead', (id) => {
-	// 	const notice = state.noticeList.find((r: any) => r.id == id);
-	// 	if (notice == undefined) return;
+	// 审批待办数量
+	fetchApprovalCount();
+	approvalPollTimer = setInterval(fetchApprovalCount, 60000);
+});
 
-	// 	// 设置已读
-	// 	notice.readStatus = 1;
-	// });
+onUnmounted(() => {
+	if (approvalPollTimer) {
+		clearInterval(approvalPollTimer);
+		approvalPollTimer = null;
+	}
 });
 // // 页面卸载时
 // onUnmounted(() => {

+ 62 - 0
Web/src/views/approvalFlow/api.ts

@@ -97,3 +97,65 @@ export function addSign(data: { taskId: number; targetUserId: number; comment?:
 export function urgeFlow(data: { instanceId: number }) {
 	return axiosInstance.post(`${BASE}/flowTask/urge`, data);
 }
+
+export function escalateTask(data: { taskId: number; comment?: string }) {
+	return axiosInstance.post(`${BASE}/flowTask/escalate`, data);
+}
+
+export function getEscalationConfig(taskId: number) {
+	return axiosInstance.get<any>(`${BASE}/flowTask/getEscalationConfig`, { params: { taskId } });
+}
+
+export function batchApprove(data: { taskIds: number[]; comment?: string }) {
+	return axiosInstance.post<any>(`${BASE}/flowTask/batchApprove`, data);
+}
+
+export function batchReject(data: { taskIds: number[]; comment?: string }) {
+	return axiosInstance.post<any>(`${BASE}/flowTask/batchReject`, data);
+}
+
+// ── 版本管理 ──
+
+export function getVersionHistory(flowId: number) {
+	return axiosInstance.get<any>(`${BASE}/approvalFlow/versionHistory`, { params: { flowId } });
+}
+
+export function getVersionDetail(versionId: number) {
+	return axiosInstance.get<any>(`${BASE}/approvalFlow/versionDetail`, { params: { versionId } });
+}
+
+export function revertVersion(data: { id: number }) {
+	return axiosInstance.post(`${BASE}/approvalFlow/revertVersion`, data);
+}
+
+// ── 意见模板 ──
+
+export function getCommentTemplates() {
+	return axiosInstance.get<any>(`${BASE}/flowCommentTemplate/myList`);
+}
+
+export function addCommentTemplate(data: { content: string; orderNo?: number }) {
+	return axiosInstance.post<any>(`${BASE}/flowCommentTemplate/add`, data);
+}
+
+export function deleteCommentTemplate(data: { id: number }) {
+	return axiosInstance.post(`${BASE}/flowCommentTemplate/delete`, data);
+}
+
+// ── 审批代理 ──
+
+export function getMyDelegates() {
+	return axiosInstance.get<any>(`${BASE}/flowDelegate/myList`);
+}
+
+export function addDelegate(data: { delegateUserId: number; startTime: string; endTime: string; bizType?: string; remark?: string }) {
+	return axiosInstance.post<any>(`${BASE}/flowDelegate/add`, data);
+}
+
+export function updateDelegate(data: { id: number; delegateUserId: number; startTime: string; endTime: string; bizType?: string; remark?: string; isEnabled: boolean }) {
+	return axiosInstance.post(`${BASE}/flowDelegate/update`, data);
+}
+
+export function deleteDelegate(data: { id: number }) {
+	return axiosInstance.post(`${BASE}/flowDelegate/delete`, data);
+}

+ 29 - 1
Web/src/views/approvalFlow/center/components/ApprovalDialog.vue

@@ -1,6 +1,19 @@
 <template>
 	<el-dialog v-model="visible" :title="title" width="480px" draggable @close="onClose">
-		<el-input v-model="commentText" type="textarea" :rows="4" placeholder="请输入审批意见" />
+		<div style="display: flex; align-items: flex-start; gap: 8px">
+			<el-input v-model="commentText" type="textarea" :rows="4" placeholder="请输入审批意见" style="flex: 1" />
+			<el-dropdown trigger="click" @command="onSelectTemplate" @visible-change="(v: boolean) => v && loadCommentTemplates()">
+				<el-button size="small" type="info" plain>常用意见</el-button>
+				<template #dropdown>
+					<el-dropdown-menu>
+						<el-dropdown-item v-for="t in commentTemplates" :key="t.id" :command="t.content">
+							{{ t.content }}
+						</el-dropdown-item>
+						<el-dropdown-item v-if="commentTemplates.length === 0" disabled>暂无模板</el-dropdown-item>
+					</el-dropdown-menu>
+				</template>
+			</el-dropdown>
+		</div>
 		<template v-if="needTarget">
 			<el-form-item label="目标用户" style="margin-top: 12px">
 				<el-select
@@ -29,6 +42,7 @@
 import { ref, computed } from 'vue';
 import { getAPI } from '/@/utils/axios-utils';
 import { SysUserApi } from '/@/api-services/api';
+import { getCommentTemplates } from '../../api';
 
 export type ActionType = 'approve' | 'reject' | 'transfer' | 'addSign';
 
@@ -48,6 +62,20 @@ const submitting = ref(false);
 
 const userLoading = ref(false);
 const userOptions = ref<Array<{ id: number; account: string; realName: string }>>([]);
+const commentTemplates = ref<Array<{ id: number; content: string }>>([]);
+
+const loadCommentTemplates = async () => {
+	try {
+		const res = await getCommentTemplates();
+		commentTemplates.value = res.data?.result ?? [];
+	} catch {
+		/* ignore */
+	}
+};
+
+const onSelectTemplate = (content: string) => {
+	commentText.value = content;
+};
 
 const searchUsers = async (query: string) => {
 	if (!query) return;

+ 49 - 27
Web/src/views/approvalFlow/center/components/PendingList.vue

@@ -1,34 +1,49 @@
 <template>
-	<el-table :data="data" v-loading="loading" border style="width: 100%">
-		<el-table-column type="index" label="#" width="55" align="center" />
-		<el-table-column prop="title" label="流程标题" show-overflow-tooltip />
-		<el-table-column prop="bizType" label="业务类型" width="140" />
-		<el-table-column prop="bizNo" label="业务单号" width="160" show-overflow-tooltip />
-		<el-table-column prop="nodeName" label="当前节点" width="140" />
-		<el-table-column prop="initiatorName" label="发起人" width="100" />
-		<el-table-column prop="createTime" label="接收时间" width="180" />
-		<el-table-column label="操作" width="240" align="center" fixed="right">
-			<template #default="{ row }">
-				<el-button size="small" type="success" text @click="emit('approve', row)">同意</el-button>
-				<el-button size="small" type="danger" text @click="emit('reject', row)">拒绝</el-button>
-				<el-dropdown trigger="click" style="margin-left: 8px">
-					<el-button size="small" text>更多</el-button>
-					<template #dropdown>
-						<el-dropdown-menu>
-							<el-dropdown-item @click="emit('transfer', row)">转办</el-dropdown-item>
-							<el-dropdown-item @click="emit('return', row)">退回上一步</el-dropdown-item>
-							<el-dropdown-item @click="emit('addSign', row)">加签</el-dropdown-item>
-							<el-dropdown-item divided @click="emit('urge', row)">催办</el-dropdown-item>
-						</el-dropdown-menu>
-					</template>
-				</el-dropdown>
-				<el-button size="small" text @click="emit('timeline', row)">详情</el-button>
-			</template>
-		</el-table-column>
-	</el-table>
+	<div>
+		<div v-if="selectedRows.length > 0" style="margin-bottom: 8px; display: flex; align-items: center; gap: 8px">
+			<span style="color: #909399; font-size: 13px">已选 {{ selectedRows.length }} 项</span>
+			<el-button type="success" size="small" @click="emit('batchApprove', selectedRows)">批量同意</el-button>
+			<el-button type="danger" size="small" @click="emit('batchReject', selectedRows)">批量拒绝</el-button>
+		</div>
+		<el-table :data="data" v-loading="loading" border style="width: 100%" @selection-change="onSelectionChange">
+			<el-table-column type="selection" width="45" align="center" />
+			<el-table-column type="index" label="#" width="55" align="center" />
+			<el-table-column prop="title" label="流程标题" show-overflow-tooltip>
+				<template #default="{ row }">
+					<el-tag v-if="row.isDelegate" type="warning" size="small" style="margin-right: 6px" :title="`由 ${row.delegateForUserName} 委托`">代理</el-tag>
+					<span>{{ row.title }}</span>
+				</template>
+			</el-table-column>
+			<el-table-column prop="bizType" label="业务类型" width="140" />
+			<el-table-column prop="bizNo" label="业务单号" width="160" show-overflow-tooltip />
+			<el-table-column prop="nodeName" label="当前节点" width="140" />
+			<el-table-column prop="initiatorName" label="发起人" width="100" />
+			<el-table-column prop="createTime" label="接收时间" width="180" />
+			<el-table-column label="操作" width="240" align="center" fixed="right">
+				<template #default="{ row }">
+					<el-button size="small" type="success" text @click="emit('approve', row)">同意</el-button>
+					<el-button size="small" type="danger" text @click="emit('reject', row)">拒绝</el-button>
+					<el-dropdown trigger="click" style="margin-left: 8px">
+						<el-button size="small" text>更多</el-button>
+						<template #dropdown>
+							<el-dropdown-menu>
+								<el-dropdown-item @click="emit('transfer', row)">转办</el-dropdown-item>
+								<el-dropdown-item @click="emit('return', row)">退回上一步</el-dropdown-item>
+								<el-dropdown-item @click="emit('addSign', row)">加签</el-dropdown-item>
+								<el-dropdown-item divided @click="emit('urge', row)">催办</el-dropdown-item>
+							</el-dropdown-menu>
+						</template>
+					</el-dropdown>
+					<el-button size="small" text @click="emit('timeline', row)">详情</el-button>
+				</template>
+			</el-table-column>
+		</el-table>
+	</div>
 </template>
 
 <script setup lang="ts">
+import { ref } from 'vue';
+
 defineProps<{ data: any[]; loading: boolean }>();
 const emit = defineEmits<{
 	(e: 'approve', row: any): void;
@@ -38,5 +53,12 @@ const emit = defineEmits<{
 	(e: 'addSign', row: any): void;
 	(e: 'urge', row: any): void;
 	(e: 'timeline', row: any): void;
+	(e: 'batchApprove', rows: any[]): void;
+	(e: 'batchReject', rows: any[]): void;
 }>();
+
+const selectedRows = ref<any[]>([]);
+const onSelectionChange = (rows: any[]) => {
+	selectedRows.value = rows;
+};
 </script>

+ 55 - 16
Web/src/views/approvalFlow/center/components/TimelineDialog.vue

@@ -1,27 +1,47 @@
 <template>
-	<el-dialog v-model="visible" title="审批时间线" width="600px" draggable>
-		<el-timeline v-if="data.length > 0">
-			<el-timeline-item v-for="item in data" :key="item.id" :timestamp="item.createTime" placement="top">
-				<el-card shadow="never">
-					<div>
-						<el-tag v-if="item.nodeName" size="small" type="info" style="margin-right: 8px">{{ item.nodeName }}</el-tag>
-						<strong>{{ actionLabel(item.action) }}</strong>
-						<span style="margin-left: 12px; color: #909399">{{ item.operatorName }}</span>
-					</div>
-					<div v-if="item.comment" style="margin-top: 4px; color: #606266">{{ item.comment }}</div>
-				</el-card>
-			</el-timeline-item>
-		</el-timeline>
-		<el-empty v-else description="暂无审批记录" />
+	<el-dialog v-model="visible" title="流程详情" :width="hasFlowJson ? '900px' : '600px'" draggable destroy-on-close>
+		<el-tabs v-model="activeTab">
+			<el-tab-pane label="审批时间线" name="timeline">
+				<el-timeline v-if="data.length > 0">
+					<el-timeline-item v-for="item in data" :key="item.id" :timestamp="item.createTime" placement="top">
+						<el-card shadow="never">
+							<div>
+								<el-tag v-if="item.nodeName" size="small" type="info" style="margin-right: 8px">{{ item.nodeName }}</el-tag>
+								<strong>{{ actionLabel(item.action) }}</strong>
+								<span style="margin-left: 12px; color: #909399">{{ item.operatorName }}</span>
+							</div>
+							<div v-if="item.comment" style="margin-top: 4px; color: #606266">{{ item.comment }}</div>
+						</el-card>
+					</el-timeline-item>
+				</el-timeline>
+				<el-empty v-else description="暂无审批记录" />
+			</el-tab-pane>
+			<el-tab-pane v-if="hasFlowJson" label="流程图" name="graph">
+				<div style="height: 500px">
+					<FlowGraphPreview
+						v-if="activeTab === 'graph'"
+						:flow-json="flowJson!"
+						:current-node-id="currentNodeId"
+						:current-node-ids="currentNodeIds"
+						:completed-node-ids="completedNodeIds"
+					/>
+				</div>
+			</el-tab-pane>
+		</el-tabs>
 	</el-dialog>
 </template>
 
 <script setup lang="ts">
-import { computed } from 'vue';
+import { ref, computed, watch } from 'vue';
+import FlowGraphPreview from '../../component/FlowGraphPreview.vue';
 
 const props = defineProps<{
 	modelValue: boolean;
 	data: any[];
+	flowJson?: string;
+	currentNodeId?: string;
+	currentNodeIds?: string[];
+	completedNodeIds?: string[];
 }>();
 
 const emit = defineEmits<{
@@ -33,8 +53,27 @@ const visible = computed({
 	set: (v) => emit('update:modelValue', v),
 });
 
+const activeTab = ref('timeline');
+const hasFlowJson = computed(() => !!props.flowJson);
+
+watch(visible, (v) => {
+	if (v) activeTab.value = 'timeline';
+});
+
 const actionLabel = (a: number) => {
-	const map: Record<number, string> = { 1: '提交', 2: '同意', 3: '拒绝', 4: '转办', 5: '撤回', 6: '退回', 7: '加签', 8: '催办', 9: '取消', 10: '超时自动' };
+	const map: Record<number, string> = {
+		1: '提交',
+		2: '同意',
+		3: '拒绝',
+		4: '转办',
+		5: '撤回',
+		6: '退回',
+		7: '加签',
+		8: '催办',
+		9: '取消',
+		10: '超时自动',
+		11: '升级',
+	};
 	return map[a] ?? '操作';
 };
 </script>

+ 79 - 3
Web/src/views/approvalFlow/center/index.vue

@@ -38,6 +38,8 @@
 				@addSign="openDialog('addSign', $event)"
 				@urge="doUrge"
 				@timeline="openTimeline"
+				@batchApprove="doBatchApprove"
+				@batchReject="doBatchReject"
 			/>
 			<DoneList v-if="activeTab === 'done'" :data="tableData" :loading="loading" @timeline="openTimeline" />
 			<InitiatedList v-if="activeTab === 'initiated'" :data="tableData" :loading="loading" @timeline="openTimeline" @urge="doUrge" @withdraw="doWithdraw" />
@@ -57,7 +59,14 @@
 		</el-card>
 
 		<ApprovalDialog v-model="dialogVisible" :actionType="dialogAction" @confirm="submitAction" ref="dialogRef" />
-		<TimelineDialog v-model="timelineVisible" :data="timelineData" />
+		<TimelineDialog
+			v-model="timelineVisible"
+			:data="timelineData"
+			:flow-json="timelineFlowJson"
+			:current-node-id="timelineCurrentNodeId"
+			:current-node-ids="timelineCurrentNodeIds"
+			:completed-node-ids="timelineCompletedNodeIds"
+		/>
 	</div>
 </template>
 
@@ -77,7 +86,10 @@ import {
 	addSign,
 	urgeFlow,
 	getTimeline,
+	getInstanceDetail,
 	getBizTypeList,
+	batchApprove,
+	batchReject,
 } from '../api';
 import PendingList from './components/PendingList.vue';
 import DoneList from './components/DoneList.vue';
@@ -118,6 +130,10 @@ const currentRow = ref<any>(null);
 
 const timelineVisible = ref(false);
 const timelineData = ref<any[]>([]);
+const timelineFlowJson = ref<string | undefined>(undefined);
+const timelineCurrentNodeId = ref<string | undefined>(undefined);
+const timelineCurrentNodeIds = ref<string[]>([]);
+const timelineCompletedNodeIds = ref<string[]>([]);
 
 onMounted(() => {
 	loadBizTypes();
@@ -217,11 +233,71 @@ const doWithdraw = async (row: any) => {
 	} catch {}
 };
 
+const doBatchApprove = async (rows: any[]) => {
+	try {
+		await ElMessageBox.prompt(`确定批量同意 ${rows.length} 条待办吗?`, '批量同意', {
+			inputPlaceholder: '审批意见(可选)',
+			type: 'info',
+			confirmButtonText: '确定',
+			cancelButtonText: '取消',
+		}).then(async ({ value }) => {
+			const res = await batchApprove({ taskIds: rows.map((r) => r.id), comment: value || undefined });
+			const result = res.data?.result;
+			if (result?.failCount > 0) {
+				ElMessage.warning(`成功 ${result.successCount} 条,失败 ${result.failCount} 条`);
+			} else {
+				ElMessage.success(`批量同意成功(${result?.successCount ?? 0} 条)`);
+			}
+			loadData();
+			loadPendingCount();
+		});
+	} catch {}
+};
+
+const doBatchReject = async (rows: any[]) => {
+	try {
+		await ElMessageBox.prompt(`确定批量拒绝 ${rows.length} 条待办吗?`, '批量拒绝', {
+			inputPlaceholder: '拒绝理由(可选)',
+			type: 'warning',
+			confirmButtonText: '确定',
+			cancelButtonText: '取消',
+		}).then(async ({ value }) => {
+			const res = await batchReject({ taskIds: rows.map((r) => r.id), comment: value || undefined });
+			const result = res.data?.result;
+			if (result?.failCount > 0) {
+				ElMessage.warning(`成功 ${result.successCount} 条,失败 ${result.failCount} 条`);
+			} else {
+				ElMessage.success(`批量拒绝成功(${result?.successCount ?? 0} 条)`);
+			}
+			loadData();
+			loadPendingCount();
+		});
+	} catch {}
+};
+
 const openTimeline = async (row: any) => {
 	const instanceId = row.instanceId || row.id;
 	try {
-		const res = await getTimeline(instanceId);
-		timelineData.value = res.data?.result ?? [];
+		const [tlRes, detailRes] = await Promise.all([getTimeline(instanceId), getInstanceDetail(instanceId)]);
+		timelineData.value = tlRes.data?.result ?? [];
+		const detail = detailRes.data?.result;
+		timelineFlowJson.value = detail?.flowJsonSnapshot || undefined;
+		timelineCurrentNodeId.value = detail?.currentNodeId || undefined;
+		// 优先使用后端子表中的"已完成节点"列表(含网关)+ Pending 任务节点;
+		// 兼容旧实例(子表尚未建立时为空):回退到按任务状态推断
+		const currentIds: string[] = Array.isArray(detail?.currentNodeIds) ? detail.currentNodeIds : [];
+		let completedIds: string[] = Array.isArray(detail?.completedNodeIds) ? detail.completedNodeIds : [];
+		if (completedIds.length === 0) {
+			const fallback = new Set<string>();
+			(detail?.tasks ?? []).forEach((t: any) => {
+				if (t.status !== 0 && t.status !== 4) fallback.add(t.nodeId);
+			});
+			currentIds.forEach((id) => fallback.delete(id));
+			if (timelineCurrentNodeId.value) fallback.delete(timelineCurrentNodeId.value);
+			completedIds = Array.from(fallback);
+		}
+		timelineCurrentNodeIds.value = currentIds;
+		timelineCompletedNodeIds.value = completedIds;
 		timelineVisible.value = true;
 	} catch {
 		ElMessage.error('获取时间线失败');

+ 72 - 4
Web/src/views/approvalFlow/component/ApprovalPanel.vue

@@ -18,13 +18,29 @@
 
 			<!-- 当前用户待审批 -->
 			<div v-if="myTask" class="panel-section">
-				<el-input v-model="comment" type="textarea" :rows="2" placeholder="审批意见" style="margin-bottom: 8px" />
+				<div style="display: flex; align-items: flex-start; gap: 8px; margin-bottom: 8px">
+					<el-input v-model="comment" type="textarea" :rows="2" placeholder="审批意见" style="flex: 1" />
+					<el-dropdown trigger="click" @command="onSelectTemplate" @visible-change="(v: boolean) => v && loadCommentTemplates()">
+						<el-button size="small" type="info" plain>常用意见</el-button>
+						<template #dropdown>
+							<el-dropdown-menu>
+								<el-dropdown-item v-for="t in commentTemplates" :key="t.id" :command="t.content">
+									{{ t.content }}
+								</el-dropdown-item>
+								<el-dropdown-item v-if="commentTemplates.length === 0" disabled>暂无模板</el-dropdown-item>
+							</el-dropdown-menu>
+						</template>
+					</el-dropdown>
+				</div>
 				<div class="btn-row">
 					<el-button type="success" @click="doApprove" :loading="operating">同意</el-button>
 					<el-button type="danger" @click="doReject" :loading="operating">拒绝</el-button>
 					<el-button @click="openTransfer" :loading="operating">转办</el-button>
 					<el-button @click="doReturn" :loading="operating">退回上一步</el-button>
 					<el-button @click="openAddSign" :loading="operating">加签</el-button>
+					<el-button v-if="escalationConfig.enabled" type="warning" @click="doEscalate" :loading="operating">
+						升级
+					</el-button>
 				</div>
 			</div>
 
@@ -86,6 +102,7 @@ import { ref, watch, computed } from 'vue';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import { getAPI } from '/@/utils/axios-utils';
 import { SysUserApi } from '/@/api-services/api';
+import { useUserInfo } from '/@/stores/userInfo';
 import {
 	startFlow,
 	getInstanceByBiz,
@@ -97,8 +114,13 @@ import {
 	returnToPrev,
 	addSign,
 	urgeFlow,
+	escalateTask,
+	getEscalationConfig,
+	getCommentTemplates,
 } from '../api';
 
+const userStore = useUserInfo();
+
 const props = defineProps<{
 	bizType: string;
 	bizId: number;
@@ -107,6 +129,8 @@ const props = defineProps<{
 	currentUserId?: number;
 }>();
 
+const resolvedUserId = computed(() => props.currentUserId ?? userStore.userInfos?.id ?? 0);
+
 const emit = defineEmits<{
 	(e: 'flowStarted', instanceId: number): void;
 	(e: 'flowCompleted', status: number): void;
@@ -126,6 +150,22 @@ const userDialogMode = ref<'transfer' | 'addSign'>('transfer');
 const targetUserId = ref<number>(0);
 const dialogComment = ref('');
 
+const escalationConfig = ref<{ enabled: boolean; escalationApproverNames?: string }>({ enabled: false });
+const commentTemplates = ref<Array<{ id: number; content: string; isSystem: boolean; isMine: boolean }>>([]);
+
+const loadCommentTemplates = async () => {
+	try {
+		const res = await getCommentTemplates();
+		commentTemplates.value = res.data?.result ?? [];
+	} catch {
+		/* ignore */
+	}
+};
+
+const onSelectTemplate = (content: string) => {
+	comment.value = content;
+};
+
 const userLoading = ref(false);
 const userOptions = ref<Array<{ id: number; account: string; realName: string }>>([]);
 
@@ -146,8 +186,8 @@ const myTask = computed(() => {
 });
 
 const isInitiator = computed(() => {
-	if (!instance.value || !props.currentUserId) return false;
-	return instance.value.initiatorId === props.currentUserId;
+	if (!instance.value || !resolvedUserId.value) return false;
+	return instance.value.initiatorId === resolvedUserId.value;
 });
 
 /** 须在 watch(immediate) 之前定义,避免「Cannot access before initialization」 */
@@ -159,6 +199,18 @@ const loadInstance = async () => {
 		if (instance.value) {
 			const tlRes = await getTimeline(instance.value.id);
 			timeline.value = tlRes.data?.result ?? [];
+
+			const myT = instance.value.tasks?.find((t: any) => t.status === 0);
+			if (myT) {
+				try {
+					const escRes = await getEscalationConfig(myT.id);
+					escalationConfig.value = escRes.data?.result ?? { enabled: false };
+				} catch {
+					escalationConfig.value = { enabled: false };
+				}
+			} else {
+				escalationConfig.value = { enabled: false };
+			}
 		}
 	} catch {
 		/* 仍展示面板,便于发起人点击「提交审批」或重试 */
@@ -274,6 +326,22 @@ const doReturn = async () => {
 	}
 };
 
+const doEscalate = async () => {
+	if (!myTask.value) return;
+	const targetNames = escalationConfig.value.escalationApproverNames || '更高层级审批人';
+	await ElMessageBox.confirm(`确定将该任务升级给 ${targetNames} 吗?`, '升级确认', { type: 'warning' });
+	operating.value = true;
+	try {
+		await escalateTask({ taskId: myTask.value.id, comment: comment.value });
+		ElMessage.success('已升级');
+		comment.value = '';
+		await loadInstance();
+		emit('refresh');
+	} finally {
+		operating.value = false;
+	}
+};
+
 const doUrge = async () => {
 	if (!instance.value) return;
 	operating.value = true;
@@ -308,7 +376,7 @@ const instanceStatusTag = (s: number) => {
 	return map[s] ?? '';
 };
 const actionLabel = (a: number) => {
-	const map: Record<number, string> = { 1: '提交', 2: '同意', 3: '拒绝', 4: '转办', 5: '撤回', 6: '退回', 7: '加签', 8: '催办', 9: '取消', 10: '超时自动' };
+	const map: Record<number, string> = { 1: '提交', 2: '同意', 3: '拒绝', 4: '转办', 5: '撤回', 6: '退回', 7: '加签', 8: '催办', 9: '取消', 10: '超时自动', 11: '升级' };
 	return map[a] ?? '操作';
 };
 </script>

+ 160 - 0
Web/src/views/approvalFlow/component/FlowGraphPreview.vue

@@ -0,0 +1,160 @@
+<template>
+	<div class="flow-preview-wrapper">
+		<div class="flow-preview-legend">
+			<el-tag type="success" size="small">已完成</el-tag>
+			<el-tag type="warning" size="small" style="margin-left: 6px">进行中</el-tag>
+			<el-tag type="info" size="small" style="margin-left: 6px">未处理</el-tag>
+		</div>
+		<div ref="container" class="flow-preview-canvas"></div>
+	</div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue';
+
+import LogicFlow from '@logicflow/core';
+import { BpmnElement } from '@logicflow/extension';
+import '@logicflow/core/dist/index.css';
+import '@logicflow/extension/lib/style/index.css';
+
+import RegisterEdge from './LogicFlow/Register/RegisterEdge';
+import RegisterNode from './LogicFlow/Register/RegisterNode';
+
+const props = defineProps<{
+	/** 流程 JSON 字符串(通常来自 instance.flowJsonSnapshot) */
+	flowJson: string;
+	/** 当前正在审批的节点 Id(单值,兼容旧用法) */
+	currentNodeId?: string;
+	/** 当前正在审批的节点 Id 列表(并行场景可能多个,优先于 currentNodeId) */
+	currentNodeIds?: string[];
+	/** 已完成的节点 Id 列表 */
+	completedNodeIds?: string[];
+}>();
+
+const container = ref<HTMLDivElement>();
+const lf = ref<InstanceType<typeof LogicFlow>>();
+
+const COMPLETED_BG = '#e7f5ec';
+const COMPLETED_STROKE = '#67c23a';
+const CURRENT_BG = '#fdf6ec';
+const CURRENT_STROKE = '#e6a23c';
+
+const render = () => {
+	if (!container.value || !props.flowJson) return;
+
+	if (!lf.value) {
+		lf.value = new LogicFlow({
+			container: container.value,
+			width: container.value.clientWidth,
+			height: container.value.clientHeight || 400,
+			isSilentMode: true,
+			stopScrollGraph: false,
+			stopZoomGraph: false,
+			grid: { size: 10, type: 'dot' },
+			plugins: [BpmnElement],
+		});
+		RegisterNode.Register(lf.value);
+		RegisterEdge.Register(lf.value);
+	}
+
+	let data: any;
+	try {
+		data = JSON.parse(props.flowJson);
+	} catch {
+		return;
+	}
+
+	const completedSet = new Set(props.completedNodeIds ?? []);
+	const currentSet = new Set<string>();
+	if (props.currentNodeIds && props.currentNodeIds.length > 0) {
+		props.currentNodeIds.forEach((id) => id && currentSet.add(id));
+	} else if (props.currentNodeId) {
+		currentSet.add(props.currentNodeId);
+	}
+
+	const nodes = (data.nodes ?? []).map((n: any) => {
+		const node = { ...n };
+		const isCurrent = currentSet.has(n.id);
+		const isCompleted = !isCurrent && completedSet.has(n.id);
+		if (isCurrent || isCompleted) {
+			node.properties = { ...(node.properties ?? {}) };
+			node.properties.nodeStyle = {
+				fill: isCurrent ? CURRENT_BG : COMPLETED_BG,
+				stroke: isCurrent ? CURRENT_STROKE : COMPLETED_STROKE,
+				strokeWidth: 2,
+			};
+		}
+		return node;
+	});
+
+	lf.value.render({ nodes, edges: data.edges ?? [] });
+
+	setTimeout(() => {
+		if (!lf.value) return;
+		nodes.forEach((n: any) => {
+			const isCurrent = currentSet.has(n.id);
+			const isCompleted = !isCurrent && completedSet.has(n.id);
+			if (!isCurrent && !isCompleted) return;
+			try {
+				lf.value!.setProperties(n.id, {
+					...(n.properties ?? {}),
+					_highlightType: isCurrent ? 'current' : 'completed',
+				});
+			} catch {
+				/* ignore */
+			}
+		});
+	}, 50);
+
+	try {
+		const focusId = Array.from(currentSet)[0] || (nodes[0] && nodes[0].id);
+		lf.value.focusOn({ id: focusId });
+	} catch {
+		/* ignore */
+	}
+};
+
+watch(
+	() => [props.flowJson, props.currentNodeId, props.currentNodeIds, props.completedNodeIds],
+	() => nextTick(render),
+	{ deep: true }
+);
+
+onMounted(() => {
+	nextTick(render);
+});
+
+onBeforeUnmount(() => {
+	try {
+		(lf.value as any)?.destroy?.();
+	} catch {
+		/* ignore */
+	}
+});
+</script>
+
+<style scoped>
+.flow-preview-wrapper {
+	position: relative;
+	width: 100%;
+	height: 100%;
+	min-height: 420px;
+}
+.flow-preview-legend {
+	position: absolute;
+	top: 8px;
+	right: 12px;
+	z-index: 10;
+	background: rgba(255, 255, 255, 0.85);
+	padding: 4px 8px;
+	border-radius: 4px;
+}
+.flow-preview-canvas {
+	width: 100%;
+	height: 100%;
+	min-height: 420px;
+	background: #fafafa;
+	border: 1px solid #ebeef5;
+	border-radius: 4px;
+}
+</style>

+ 35 - 0
Web/src/views/approvalFlow/component/LogicFlow/Panel/PanelNode.vue

@@ -33,6 +33,11 @@ const state = reactive({
 			text: '网关',
 			class: 'bpmn-exclusiveGateway',
 		},
+		{
+			type: 'bpmn:parallelGateway',
+			text: '并行',
+			class: 'bpmn-parallelGateway',
+		},
 		{
 			type: 'task-node',
 			text: '任务',
@@ -110,5 +115,35 @@ const dragNode = (item: any) => {
 			center center no-repeat;
 		cursor: grab;
 	}
+	.bpmn-parallelGateway {
+		position: relative;
+		background: #fff;
+		border: 2px solid #409eff;
+		transform: rotate(45deg);
+		width: 24px;
+		height: 24px;
+		margin: 3px auto;
+		cursor: grab;
+	}
+	.bpmn-parallelGateway::before,
+	.bpmn-parallelGateway::after {
+		content: '';
+		position: absolute;
+		background: #409eff;
+	}
+	.bpmn-parallelGateway::before {
+		top: 50%;
+		left: 4px;
+		right: 4px;
+		height: 3px;
+		transform: translateY(-50%);
+	}
+	.bpmn-parallelGateway::after {
+		left: 50%;
+		top: 4px;
+		bottom: 4px;
+		width: 3px;
+		transform: translateX(-50%);
+	}
 }
 </style>

+ 150 - 0
Web/src/views/approvalFlow/component/LogicFlow/Property/PropertyCommon.vue

@@ -31,6 +31,7 @@
 									<el-option label="指定用户" value="SpecificUser" />
 									<el-option label="指定角色" value="Role" />
 									<el-option label="指定部门" value="Department" />
+									<el-option label="部门负责人" value="DepartmentLeader" />
 									<el-option label="发起人本人" value="Initiator" />
 								</el-select>
 							</el-form-item>
@@ -126,9 +127,67 @@
 									<el-option label="自动通知" value="Notify" />
 									<el-option label="自动通过" value="AutoApprove" />
 									<el-option label="自动拒绝" value="AutoReject" />
+									<el-option label="自动升级" value="AutoEscalate" />
 								</el-select>
 							</el-form-item>
 						</el-col>
+						<el-col :span="24" class="mb20">
+							<el-form-item label="允许手动升级">
+								<el-switch v-model="formData.enableManualEscalation" />
+							</el-form-item>
+						</el-col>
+
+						<!-- 升级目标配置 -->
+						<template v-if="needEscalationConfig">
+							<el-col :span="24"><el-divider content-position="left">升级目标配置</el-divider></el-col>
+							<el-col :span="24" class="mb20">
+								<el-form-item label="升级目标类型">
+									<el-select v-model="formData.escalationApproverType" placeholder="请选择" clearable @change="onEscalationTypeChange">
+										<el-option label="指定用户" value="SpecificUser" />
+										<el-option label="指定角色" value="Role" />
+										<el-option label="指定部门" value="Department" />
+										<el-option label="部门负责人" value="DepartmentLeader" />
+									</el-select>
+								</el-form-item>
+							</el-col>
+							<el-col :span="24" class="mb20" v-if="formData.escalationApproverType === 'SpecificUser'">
+								<el-form-item label="升级目标用户">
+									<el-select
+										v-model="escSelectedUserIds"
+										multiple filterable remote reserve-keyword
+										:remote-method="searchUsers"
+										:loading="userLoading"
+										placeholder="输入姓名或账号搜索"
+										style="width: 100%"
+										@change="onEscUserChange"
+									>
+										<el-option v-for="u in userOptions" :key="u.id" :label="`${u.realName}(${u.account})`" :value="u.id" />
+									</el-select>
+								</el-form-item>
+							</el-col>
+							<el-col :span="24" class="mb20" v-if="formData.escalationApproverType === 'Role'">
+								<el-form-item label="升级目标角色">
+									<el-select v-model="escSelectedRoleIds" multiple filterable placeholder="请选择角色" style="width: 100%" @change="onEscRoleChange">
+										<el-option v-for="r in roleOptions" :key="r.id" :label="r.name" :value="r.id" />
+									</el-select>
+								</el-form-item>
+							</el-col>
+							<el-col :span="24" class="mb20" v-if="formData.escalationApproverType === 'Department'">
+								<el-form-item label="升级目标部门">
+									<el-tree-select
+										v-model="escSelectedOrgIds"
+										:data="orgTreeData" multiple filterable check-strictly :render-after-expand="false"
+										:props="{ label: 'name', value: 'id', children: 'children' }"
+										placeholder="请选择部门" style="width: 100%" @change="onEscOrgChange"
+									/>
+								</el-form-item>
+							</el-col>
+							<el-col :span="24" class="mb20" v-if="formData.escalationApproverNames">
+								<el-form-item label="已选升级目标">
+									<el-tag v-for="name in formData.escalationApproverNames.split(',')" :key="name" size="small" type="warning" style="margin-right: 4px">{{ name }}</el-tag>
+								</el-form-item>
+							</el-col>
+						</template>
 					</el-row>
 					<el-row>
 						<el-col :span="24" style="text-align: right">
@@ -137,6 +196,19 @@
 					</el-row>
 				</el-tab-pane>
 
+				<!-- 并行网关说明(无可配置项) -->
+				<el-tab-pane label="并行网关" v-if="isParallelGateway">
+					<el-alert type="info" :closable="false" show-icon>
+						<template #title>并行网关(Parallel Gateway)</template>
+						<div style="margin-top: 6px; line-height: 1.6">
+							<p style="margin: 0">• <strong>分叉(Fork)</strong>:1 条入边 + ≥2 条出边,到达后同时开启所有后续分支。</p>
+							<p style="margin: 0">• <strong>汇合(Join)</strong>:≥2 条入边 + 1 条出边,等待所有前驱分支完成后再继续。</p>
+							<p style="margin: 0">• 任一分支被驳回将终止整个流程实例。</p>
+							<p style="margin: 0">• 本节点无可配置参数,由图的拓扑结构决定行为。</p>
+						</div>
+					</el-alert>
+				</el-tab-pane>
+
 				<!-- 网关节点属性 -->
 				<el-tab-pane label="条件分支" v-if="isGateway">
 					<div v-for="(cond, idx) in formData.conditions" :key="idx" style="margin-bottom: 16px; padding: 12px; border: 1px solid #eee; border-radius: 4px">
@@ -192,6 +264,11 @@ const isGateway = computed(() => {
 	return t === 'bpmn:exclusiveGateway';
 });
 
+const isParallelGateway = computed(() => {
+	const t = props.nodeData?.type;
+	return t === 'bpmn:parallelGateway';
+});
+
 // ── 用户搜索 ──
 const userLoading = ref(false);
 const userOptions = ref<Array<{ id: number; account: string; realName: string }>>([]);
@@ -268,6 +345,44 @@ const onOrgChange = () => {
 	formData.approverNames = selected.map((s) => s.name).join(',');
 };
 
+// ── 升级目标选择状态 ──
+const escSelectedUserIds = ref<number[]>([]);
+const escSelectedRoleIds = ref<number[]>([]);
+const escSelectedOrgIds = ref<number[]>([]);
+
+const needEscalationConfig = computed(() =>
+	formData.timeoutAction === 'AutoEscalate' || formData.enableManualEscalation,
+);
+
+const onEscalationTypeChange = () => {
+	formData.escalationApproverIds = '';
+	formData.escalationApproverNames = '';
+	escSelectedUserIds.value = [];
+	escSelectedRoleIds.value = [];
+	escSelectedOrgIds.value = [];
+	if (formData.escalationApproverType === 'Role') loadRoles();
+	if (formData.escalationApproverType === 'Department') loadOrgs();
+};
+
+const onEscUserChange = () => {
+	const selected = userOptions.value.filter((o) => escSelectedUserIds.value.includes(o.id));
+	formData.escalationApproverIds = selected.map((s) => s.id).join(',');
+	formData.escalationApproverNames = selected.map((s) => s.realName || s.account).join(',');
+};
+
+const onEscRoleChange = () => {
+	const selected = roleOptions.value.filter((o) => escSelectedRoleIds.value.includes(o.id));
+	formData.escalationApproverIds = selected.map((s) => s.id).join(',');
+	formData.escalationApproverNames = selected.map((s) => s.name).join(',');
+};
+
+const onEscOrgChange = () => {
+	const flat = flattenOrg(orgTreeData.value);
+	const selected = flat.filter((o) => escSelectedOrgIds.value.includes(o.id));
+	formData.escalationApproverIds = selected.map((s) => s.id).join(',');
+	formData.escalationApproverNames = selected.map((s) => s.name).join(',');
+};
+
 // ── 表单数据 ──
 const formData = reactive({
 	nodeName: '',
@@ -277,6 +392,10 @@ const formData = reactive({
 	multiApproveMode: 'Any',
 	timeoutHours: undefined as number | undefined,
 	timeoutAction: 'Notify',
+	enableManualEscalation: false,
+	escalationApproverType: '',
+	escalationApproverIds: '',
+	escalationApproverNames: '',
 	conditions: [] as Array<{ targetNodeId: string; expression: string; label: string; isDefault: boolean }>,
 });
 
@@ -313,6 +432,26 @@ const restoreSelection = async () => {
 	}
 };
 
+const restoreEscalationSelection = async () => {
+	const ids = formData.escalationApproverIds ? formData.escalationApproverIds.split(',').map(Number).filter(Boolean) : [];
+	const names = formData.escalationApproverNames ? formData.escalationApproverNames.split(',') : [];
+
+	if (formData.escalationApproverType === 'SpecificUser' && ids.length) {
+		for (const [i, id] of ids.entries()) {
+			if (!userOptions.value.some((o) => o.id === id)) {
+				userOptions.value.push({ id, account: '', realName: names[i] || String(id) });
+			}
+		}
+		escSelectedUserIds.value = ids;
+	} else if (formData.escalationApproverType === 'Role' && ids.length) {
+		await loadRoles();
+		escSelectedRoleIds.value = ids;
+	} else if (formData.escalationApproverType === 'Department' && ids.length) {
+		await loadOrgs();
+		escSelectedOrgIds.value = ids;
+	}
+};
+
 watch(
 	() => props.nodeData,
 	(val) => {
@@ -325,8 +464,13 @@ watch(
 		formData.multiApproveMode = p.multiApproveMode || 'Any';
 		formData.timeoutHours = p.timeoutHours;
 		formData.timeoutAction = p.timeoutAction || 'Notify';
+		formData.enableManualEscalation = !!p.enableManualEscalation;
+		formData.escalationApproverType = p.escalationApproverType || '';
+		formData.escalationApproverIds = p.escalationApproverIds || '';
+		formData.escalationApproverNames = p.escalationApproverNames || '';
 		formData.conditions = Array.isArray(p.conditions) ? [...p.conditions] : [];
 		restoreSelection();
+		restoreEscalationSelection();
 	},
 	{ immediate: true, deep: true }
 );
@@ -347,6 +491,12 @@ const saveProperties = () => {
 			data.timeoutHours = formData.timeoutHours;
 			data.timeoutAction = formData.timeoutAction;
 		}
+		data.enableManualEscalation = formData.enableManualEscalation;
+		if (formData.enableManualEscalation || formData.timeoutAction === 'AutoEscalate') {
+			data.escalationApproverType = formData.escalationApproverType;
+			data.escalationApproverIds = formData.escalationApproverIds;
+			data.escalationApproverNames = formData.escalationApproverNames;
+		}
 	}
 	if (isGateway.value) {
 		data.conditions = formData.conditions;

+ 240 - 0
Web/src/views/approvalFlow/delegate/index.vue

@@ -0,0 +1,240 @@
+<template>
+	<div class="flow-delegate-container">
+		<el-card shadow="hover">
+			<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px">
+				<div>
+					<el-text type="info" size="small">
+						设置代理后,在有效期内你的审批任务会同时分派给代理人(代理人或本人任一操作即可)。
+					</el-text>
+				</div>
+				<el-button type="primary" @click="openAdd">新增代理</el-button>
+			</div>
+
+			<el-table :data="tableData" v-loading="loading" border style="width: 100%">
+				<el-table-column prop="delegateUserName" label="代理人" width="160" />
+				<el-table-column label="有效期" width="320">
+					<template #default="{ row }">
+						{{ formatTime(row.startTime) }} ~ {{ formatTime(row.endTime) }}
+					</template>
+				</el-table-column>
+				<el-table-column label="业务范围" width="160">
+					<template #default="{ row }">
+						<el-tag v-if="!row.bizType" type="info" size="small">全部业务</el-tag>
+						<span v-else>{{ bizTypeNameMap[row.bizType] || row.bizType }}</span>
+					</template>
+				</el-table-column>
+				<el-table-column prop="remark" label="备注" show-overflow-tooltip />
+				<el-table-column label="状态" width="120" align="center">
+					<template #default="{ row }">
+						<el-tag v-if="!row.isEnabled" type="info" size="small">已禁用</el-tag>
+						<el-tag v-else-if="row.isEffective" type="success" size="small">生效中</el-tag>
+						<el-tag v-else type="warning" size="small">未生效</el-tag>
+					</template>
+				</el-table-column>
+				<el-table-column label="操作" width="180" align="center" fixed="right">
+					<template #default="{ row }">
+						<el-button size="small" text @click="openEdit(row)">编辑</el-button>
+						<el-button size="small" text type="danger" @click="handleDelete(row)">删除</el-button>
+					</template>
+				</el-table-column>
+			</el-table>
+		</el-card>
+
+		<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑代理' : '新增代理'" width="520px" draggable destroy-on-close>
+			<el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
+				<el-form-item label="代理人" prop="delegateUserId">
+					<el-select
+						v-model="form.delegateUserId"
+						filterable
+						remote
+						reserve-keyword
+						:remote-method="searchUsers"
+						:loading="userLoading"
+						placeholder="输入姓名或账号搜索"
+						style="width: 100%"
+					>
+						<el-option v-for="u in userOptions" :key="u.id" :label="`${u.realName}(${u.account})`" :value="u.id" />
+					</el-select>
+				</el-form-item>
+				<el-form-item label="有效期" prop="timeRange">
+					<el-date-picker
+						v-model="form.timeRange"
+						type="datetimerange"
+						range-separator="至"
+						start-placeholder="开始时间"
+						end-placeholder="结束时间"
+						value-format="YYYY-MM-DD HH:mm:ss"
+						style="width: 100%"
+					/>
+				</el-form-item>
+				<el-form-item label="业务范围">
+					<el-select v-model="form.bizType" placeholder="全部业务(默认)" clearable style="width: 100%">
+						<el-option v-for="b in bizTypeList" :key="b.code" :label="`${b.name} (${b.code})`" :value="b.code" />
+					</el-select>
+				</el-form-item>
+				<el-form-item label="备注">
+					<el-input v-model="form.remark" type="textarea" :rows="2" maxlength="256" show-word-limit />
+				</el-form-item>
+				<el-form-item v-if="isEdit" label="启用">
+					<el-switch v-model="form.isEnabled" />
+				</el-form-item>
+			</el-form>
+			<template #footer>
+				<el-button @click="dialogVisible = false">取消</el-button>
+				<el-button type="primary" @click="submitForm" :loading="submitting">确定</el-button>
+			</template>
+		</el-dialog>
+	</div>
+</template>
+
+<script lang="ts" setup name="flowDelegate">
+import { ref, reactive, onMounted, computed } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import type { FormInstance, FormRules } from 'element-plus';
+import { getAPI } from '/@/utils/axios-utils';
+import { SysUserApi } from '/@/api-services/api';
+import { getMyDelegates, addDelegate, updateDelegate, deleteDelegate, getBizTypeList } from '../api';
+
+const loading = ref(false);
+const tableData = ref<any[]>([]);
+const bizTypeList = ref<Array<{ code: string; name: string }>>([]);
+const bizTypeNameMap = computed(() => {
+	const m: Record<string, string> = {};
+	bizTypeList.value.forEach((b) => (m[b.code] = b.name));
+	return m;
+});
+
+const userLoading = ref(false);
+const userOptions = ref<Array<{ id: number; account: string; realName: string }>>([]);
+
+const dialogVisible = ref(false);
+const isEdit = ref(false);
+const submitting = ref(false);
+const formRef = ref<FormInstance>();
+
+const form = reactive<{
+	id: number;
+	delegateUserId: number | undefined;
+	timeRange: [string, string] | null;
+	bizType: string;
+	remark: string;
+	isEnabled: boolean;
+}>({
+	id: 0,
+	delegateUserId: undefined,
+	timeRange: null,
+	bizType: '',
+	remark: '',
+	isEnabled: true,
+});
+
+const formRules: FormRules = {
+	delegateUserId: [{ required: true, message: '请选择代理人', trigger: 'change' }],
+	timeRange: [{ required: true, message: '请选择有效期', trigger: 'change' }],
+};
+
+const formatTime = (t: string) => (t ? t.replace('T', ' ').substring(0, 19) : '');
+
+const loadData = async () => {
+	loading.value = true;
+	try {
+		const res = await getMyDelegates();
+		tableData.value = res.data?.result ?? [];
+	} finally {
+		loading.value = false;
+	}
+};
+
+const loadBizTypes = async () => {
+	try {
+		const res = await getBizTypeList();
+		bizTypeList.value = res.data?.result ?? [];
+	} catch {
+		/* ignore */
+	}
+};
+
+const searchUsers = async (query: string) => {
+	if (!query) return;
+	userLoading.value = true;
+	try {
+		const res = await getAPI(SysUserApi).apiSysUserPagePost({ page: 1, pageSize: 20, realName: query } as any);
+		userOptions.value = (res.data.result?.items ?? []).map((u: any) => ({ id: u.id, account: u.account ?? '', realName: u.realName ?? '' }));
+	} finally {
+		userLoading.value = false;
+	}
+};
+
+const openAdd = () => {
+	isEdit.value = false;
+	form.id = 0;
+	form.delegateUserId = undefined;
+	form.timeRange = null;
+	form.bizType = '';
+	form.remark = '';
+	form.isEnabled = true;
+	userOptions.value = [];
+	dialogVisible.value = true;
+};
+
+const openEdit = (row: any) => {
+	isEdit.value = true;
+	form.id = row.id;
+	form.delegateUserId = row.delegateUserId;
+	form.timeRange = [formatTime(row.startTime), formatTime(row.endTime)];
+	form.bizType = row.bizType || '';
+	form.remark = row.remark || '';
+	form.isEnabled = row.isEnabled;
+	userOptions.value = row.delegateUserId
+		? [{ id: row.delegateUserId, account: '', realName: row.delegateUserName || '' }]
+		: [];
+	dialogVisible.value = true;
+};
+
+const submitForm = async () => {
+	if (!formRef.value) return;
+	await formRef.value.validate(async (valid) => {
+		if (!valid) return;
+		submitting.value = true;
+		try {
+			const [startTime, endTime] = form.timeRange!;
+			const payload: any = {
+				delegateUserId: form.delegateUserId,
+				startTime,
+				endTime,
+				bizType: form.bizType || undefined,
+				remark: form.remark || undefined,
+			};
+			if (isEdit.value) {
+				await updateDelegate({ ...payload, id: form.id, isEnabled: form.isEnabled });
+				ElMessage.success('更新成功');
+			} else {
+				await addDelegate(payload);
+				ElMessage.success('新增成功');
+			}
+			dialogVisible.value = false;
+			loadData();
+		} finally {
+			submitting.value = false;
+		}
+	});
+};
+
+const handleDelete = async (row: any) => {
+	await ElMessageBox.confirm(`确认删除对 ${row.delegateUserName} 的代理吗?`, '提示', { type: 'warning' });
+	await deleteDelegate({ id: row.id });
+	ElMessage.success('删除成功');
+	loadData();
+};
+
+onMounted(() => {
+	loadData();
+	loadBizTypes();
+});
+</script>
+
+<style scoped>
+.flow-delegate-container {
+	padding: 10px;
+}
+</style>

+ 57 - 2
Web/src/views/approvalFlow/index.vue

@@ -64,11 +64,12 @@
 						<ModifyRecord :data="scope.row" />
 					</template>
 				</el-table-column>
-				<el-table-column label="操作" width="200" align="center" fixed="right" show-overflow-tooltip>
+				<el-table-column label="操作" width="260" align="center" fixed="right" show-overflow-tooltip>
 					<template #default="scope">
 						<el-button icon="ele-View" size="small" text type="primary" @click="openDetailDialog(scope.row)"> 查看 </el-button>
 						<el-button icon="ele-Edit" size="small" text type="primary" @click="openEditApprovalFlow(scope.row)"> 编辑 </el-button>
 						<el-button icon="ele-Upload" size="small" text type="success" @click="publishFlow(scope.row)"> 发布 </el-button>
+						<el-button icon="ele-Clock" size="small" text type="info" @click="openVersionHistory(scope.row)"> 版本 </el-button>
 						<el-button icon="ele-Delete" size="small" text type="primary" @click="delApprovalFlow(scope.row)"> 删除 </el-button>
 					</template>
 				</el-table-column>
@@ -91,6 +92,25 @@
 		<editDialog ref="editDialogRef" :title="state.dialogTitle" @reloadTable="handleQuery" />
 		<editFormDialog ref="editFormDialogRef" :title="state.dialogTitle" @reloadTable="handleQuery" />
 		<editFlowDialog ref="editFlowDialogRef" :title="state.dialogTitle" @updateFlow="handleFlow" @reloadTable="handleQuery" />
+
+		<!-- 版本历史对话框 -->
+		<el-dialog v-model="versionHistoryVisible" title="版本历史" width="600px" destroy-on-close>
+			<el-table :data="versionList" border v-loading="versionLoading" style="width: 100%">
+				<el-table-column prop="version" label="版本" width="80" align="center">
+					<template #default="{ row }">V{{ row.version }}</template>
+				</el-table-column>
+				<el-table-column prop="publisherName" label="发布人" width="120" />
+				<el-table-column prop="publishTime" label="发布时间" />
+				<el-table-column label="操作" width="100" align="center">
+					<template #default="{ row }">
+						<el-button size="small" text type="warning" @click="doRevertVersion(row)">回退</el-button>
+					</template>
+				</el-table-column>
+			</el-table>
+			<template v-if="versionList.length === 0 && !versionLoading">
+				<el-empty description="暂无版本历史" :image-size="80" />
+			</template>
+		</el-dialog>
 	</div>
 </template>
 
@@ -109,7 +129,7 @@ import ModifyRecord from '/@/components/table/modifyRecord.vue';
 import { getAPI } from '/@/utils/axios-utils';
 import { ApprovalFlowApi } from '/@/api-plugins/approvalFlow/api';
 import { ApprovalFlowInput, ApprovalFlowOutput } from '/@/api-plugins/approvalFlow/models';
-import { publishFlow as publishFlowApi } from './api';
+import { publishFlow as publishFlowApi, getVersionHistory, revertVersion } from './api';
 
 const showAdvanceQueryUI = ref(false);
 
@@ -218,6 +238,41 @@ const handleCurrentChange = (val: number) => {
 	handleQuery();
 };
 
+// 版本历史
+const versionHistoryVisible = ref(false);
+const versionList = ref<any[]>([]);
+const versionLoading = ref(false);
+const versionFlowId = ref(0);
+
+const openVersionHistory = async (row: ApprovalFlowOutput) => {
+	versionFlowId.value = row.id!;
+	versionHistoryVisible.value = true;
+	versionLoading.value = true;
+	try {
+		const res = await getVersionHistory(row.id!);
+		versionList.value = res.data?.result ?? [];
+	} catch {
+		versionList.value = [];
+	} finally {
+		versionLoading.value = false;
+	}
+};
+
+const doRevertVersion = (row: any) => {
+	ElMessageBox.confirm(`确定回退到 V${row.version} 版本吗?回退后流程将变为"未发布"状态`, '提示', {
+		confirmButtonText: '确定',
+		cancelButtonText: '取消',
+		type: 'warning',
+	})
+		.then(async () => {
+			await revertVersion({ id: row.id });
+			ElMessage.success('已回退');
+			versionHistoryVisible.value = false;
+			handleQuery();
+		})
+		.catch(() => {});
+};
+
 // 发布流程
 const publishFlow = (row: ApprovalFlowOutput) => {
 	ElMessageBox.confirm(`确定要发布审批流【${row.name}】吗?发布后版本号+1`, '提示', {

+ 927 - 0
doc/plan/审批流-流程升级机制实施方案.md

@@ -0,0 +1,927 @@
+# 审批流 — 流程升级机制实施方案
+
+> 文档定位:审批流引擎的通用能力增强——为审批节点增加超时自动处理和手动升级机制  
+> 需求来源:S8 异常协同模块的"异常升级"与"关闭确认"场景(见 `doc/S8异常协同-审批流集成功能说明.md`)  
+> 创建日期:2026-04-16
+
+---
+
+## 一、需求背景
+
+当前审批流引擎的流程设计器已支持在审批节点上配置 `timeoutHours`(超时时长)和 `timeoutAction`(超时动作:Notify / AutoApprove / AutoReject),但**后端没有任何定时扫描和执行逻辑**,这些配置项是空壳。
+
+S8 异常协同的升级场景进一步要求:当审批任务超时或当前审批人无力处理时,能自动/手动将任务升级到更高层级的审批人。这是一个通用能力,不仅 S8 需要,任何业务的审批流都可能用到。
+
+### 本方案目标
+
+在审批流引擎层面新增三项通用能力(不涉及 S8 业务代码改动):
+
+1. **超时自动处理**:让已有的 `timeoutHours` + `timeoutAction` 配置真正生效
+2. **超时自动升级**:新增 `AutoEscalate` 超时动作,超时后自动升级到配置的更高层级审批人
+3. **手动升级**:审批人可主动点击"升级",将任务转交给更高层级审批人
+
+---
+
+## 二、现状分析
+
+### 2.1 已有基础设施
+
+| 组件 | 位置 | 状态 |
+|------|------|------|
+| `FlowProperties.TimeoutHours` | `ApprovalFlowItem.cs` L113-115 | 已定义,`int?` |
+| `FlowProperties.TimeoutAction` | `ApprovalFlowItem.cs` L120-122 | 已定义,`string?`(`Notify` / `AutoApprove` / `AutoReject`) |
+| `FlowLogActionEnum.AutoTimeout = 10` | `FlowLogActionEnum.cs` L37 | 已定义 |
+| 设计器 UI:超时小时 + 超时动作 | `PropertyCommon.vue` L118-131 | 已实现(下拉选项仅 3 个) |
+| `FlowEngineService.Approve()` | `FlowEngineService.cs` L113-134 | 引擎核心,可复用其"推进到下一步"逻辑 |
+| 定时任务框架(Furion `IJob`) | `LogJob.cs` 等 | 项目已使用,可照搬模式 |
+
+### 2.2 完全缺失的部分
+
+| 缺失项 | 说明 |
+|--------|------|
+| 超时扫描定时任务 | 没有 `FlowTimeoutJob`,`timeoutHours` 配置无人消费 |
+| `AutoEscalate` 超时动作 | 枚举和 UI 均未包含 |
+| 升级目标配置 | `FlowProperties` 中无 `escalation*` 字段 |
+| 手动升级引擎方法 | `FlowEngineService` 无 `Escalate()` 方法 |
+| 手动升级 API 端点 | `FlowTaskService` 无升级接口 |
+| `Escalated` 任务状态 | `FlowTaskStatusEnum` 无此值 |
+| `Escalate` 日志动作 | `FlowLogActionEnum` 无此值 |
+| 升级通知模板 | `FlowNotifyService` 无升级相关通知 |
+| 前端升级按钮 | `ApprovalPanel.vue` 无"升级"操作 |
+
+---
+
+## 三、总体设计
+
+### 3.1 超时处理流程
+
+```
+┌─────────────────────────────────────────────────┐
+│  FlowTimeoutJob(每 5 分钟执行)                  │
+│                                                   │
+│  扫描条件:                                       │
+│    task.Status == Pending                          │
+│    AND 所属节点 timeoutHours > 0                   │
+│    AND task.CreateTime + timeoutHours < NOW        │
+│                                                   │
+│  按 timeoutAction 执行:                          │
+│    ├── Notify       → 发催办通知(仅首次)         │
+│    ├── AutoApprove  → 系统自动同意                 │
+│    ├── AutoReject   → 系统自动拒绝                 │
+│    └── AutoEscalate → 升级到配置的更高层级审批人    │
+└─────────────────────────────────────────────────┘
+```
+
+### 3.2 手动升级流程
+
+```
+审批人 → 点击"升级" → 引擎 Escalate()
+  ├── 当前任务标记为 Escalated
+  ├── 取消同节点其他 Pending 任务
+  ├── 读取节点升级目标配置
+  ├── 为升级目标创建新 Pending 任务(同节点)
+  ├── 写日志 FlowLogActionEnum.Escalate
+  └── 通知升级目标人
+```
+
+### 3.3 升级 vs 转办的区别
+
+| 维度 | 转办 (Transfer) | 升级 (Escalate) |
+|------|-----------------|-----------------|
+| 触发者 | 当前审批人 | 当前审批人 或 系统自动 |
+| 目标选择 | 审批人手动选一个用户 | 按节点预配置的升级目标自动路由 |
+| 语义 | 平级移交 | 向上请求更高权限介入 |
+| 任务状态 | `Transferred` | `Escalated`(新增) |
+| 日志动作 | `Transfer` | `Escalate`(新增)或 `AutoTimeout` |
+| 前提 | 始终可用 | 节点必须配置了升级目标 |
+
+---
+
+## 四、详细设计
+
+### 4.1 枚举扩展
+
+#### `FlowTaskStatusEnum.cs`
+
+```csharp
+[Description("已升级")]
+Escalated = 6,
+```
+
+#### `FlowLogActionEnum.cs`
+
+```csharp
+[Description("升级")]
+Escalate = 11,
+```
+
+### 4.2 节点属性扩展(`FlowProperties`)
+
+在 `ApprovalFlowItem.cs` 的 `FlowProperties` 类中新增升级目标配置字段:
+
+```csharp
+// ── 升级配置 ──
+
+/// <summary>
+/// 升级审批人类型:SpecificUser / Role / Department
+/// 仅当 timeoutAction == "AutoEscalate" 或启用手动升级时有效
+/// </summary>
+[JsonPropertyName("escalationApproverType")]
+[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+public string? EscalationApproverType { get; set; }
+
+/// <summary>
+/// 升级审批人/角色/部门 Id,逗号分隔
+/// </summary>
+[JsonPropertyName("escalationApproverIds")]
+[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+public string? EscalationApproverIds { get; set; }
+
+/// <summary>
+/// 升级审批人/角色/部门名称,逗号分隔(冗余,便于前端展示)
+/// </summary>
+[JsonPropertyName("escalationApproverNames")]
+[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+public string? EscalationApproverNames { get; set; }
+
+/// <summary>
+/// 是否允许手动升级(true 时审批面板显示"升级"按钮)
+/// </summary>
+[JsonPropertyName("enableManualEscalation")]
+[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+public bool? EnableManualEscalation { get; set; }
+```
+
+> 说明:升级目标的解析逻辑复用现有 `ResolveApprovers`,传入 `EscalationApproverType` + `EscalationApproverIds` 即可。
+
+### 4.3 `FlowEngineService` 新增方法
+
+#### 4.3.1 手动升级 `Escalate()`
+
+```csharp
+/// <summary>
+/// 手动升级 — 当前审批人主动将任务升级到更高层级
+/// </summary>
+public async Task Escalate(long taskId, string? comment)
+{
+    var task = await GetPendingTask(taskId);
+    var instance = await _instanceRep.GetByIdAsync(task.InstanceId)
+        ?? throw Oops.Oh("流程实例不存在");
+
+    var flowData = DeserializeFlowJson(instance.FlowJsonSnapshot);
+    var node = flowData.Nodes.FirstOrDefault(n => n.Id == task.NodeId)
+        ?? throw Oops.Oh("节点不存在");
+
+    var props = node.Properties;
+    if (string.IsNullOrWhiteSpace(props?.EscalationApproverType)
+        || string.IsNullOrWhiteSpace(props?.EscalationApproverIds))
+        throw Oops.Oh("该节点未配置升级目标,无法升级");
+
+    // 标记当前任务为已升级
+    task.Status = FlowTaskStatusEnum.Escalated;
+    task.Comment = comment;
+    task.ActionTime = DateTime.Now;
+    await _taskRep.AsUpdateable(task).ExecuteCommandAsync();
+
+    // 取消同节点其他 Pending 任务
+    await CancelPendingTasks(task.InstanceId, task.NodeId, task.Id);
+
+    // 解析升级目标并创建新任务
+    var escalationProps = new FlowProperties
+    {
+        ApproverType = props.EscalationApproverType,
+        ApproverIds = props.EscalationApproverIds,
+        ApproverNames = props.EscalationApproverNames,
+    };
+    var escalationApprovers = await ResolveApprovers(escalationProps, instance.InitiatorId);
+    if (escalationApprovers.Count == 0)
+        throw Oops.Oh("升级目标审批人列表为空");
+
+    var newTasks = escalationApprovers.Select(a => new ApprovalFlowTask
+    {
+        InstanceId = instance.Id,
+        NodeId = task.NodeId,
+        NodeName = task.NodeName,
+        AssigneeId = a.userId,
+        AssigneeName = a.userName,
+        Status = FlowTaskStatusEnum.Pending,
+        OrgId = instance.OrgId,
+    }).ToList();
+    await _taskRep.AsInsertable(newTasks).ExecuteCommandAsync();
+
+    // 日志
+    var targetNames = string.Join(", ", escalationApprovers.Select(a => a.userName));
+    await WriteLog(instance.Id, taskId, task.NodeId, FlowLogActionEnum.Escalate,
+        $"{comment} → 升级给 {targetNames}");
+
+    // 通知升级目标
+    var targetUserIds = escalationApprovers.Select(a => a.userId).Distinct().ToList();
+    await _notifyService.NotifyEscalated(targetUserIds, instance.Id, instance.Title,
+        _userManager.RealName, task.NodeName);
+}
+```
+
+#### 4.3.2 系统自动升级 `AutoEscalateTask()`(供定时任务调用)
+
+```csharp
+/// <summary>
+/// 系统自动升级(由定时任务调用,无 UserManager 上下文)
+/// </summary>
+public async Task AutoEscalateTask(ApprovalFlowTask task, FlowProperties nodeProps, ApprovalFlowInstance instance)
+{
+    // 标记当前任务
+    task.Status = FlowTaskStatusEnum.Escalated;
+    task.Comment = "系统自动升级(超时)";
+    task.ActionTime = DateTime.Now;
+    await _taskRep.AsUpdateable(task).ExecuteCommandAsync();
+
+    // 取消同节点其他 Pending 任务
+    await CancelPendingTasks(task.InstanceId, task.NodeId, task.Id);
+
+    // 解析升级目标
+    var escalationProps = new FlowProperties
+    {
+        ApproverType = nodeProps.EscalationApproverType,
+        ApproverIds = nodeProps.EscalationApproverIds,
+    };
+    var approvers = await ResolveApprovers(escalationProps, instance.InitiatorId);
+    if (approvers.Count == 0) return; // 无升级目标则静默跳过
+
+    var newTasks = approvers.Select(a => new ApprovalFlowTask
+    {
+        InstanceId = instance.Id,
+        NodeId = task.NodeId,
+        NodeName = task.NodeName,
+        AssigneeId = a.userId,
+        AssigneeName = a.userName,
+        Status = FlowTaskStatusEnum.Pending,
+        OrgId = instance.OrgId,
+    }).ToList();
+    await _taskRep.AsInsertable(newTasks).ExecuteCommandAsync();
+
+    // 日志(操作人记为"系统")
+    await _logRep.InsertAsync(new ApprovalFlowLog
+    {
+        InstanceId = instance.Id,
+        TaskId = task.Id,
+        NodeId = task.NodeId,
+        Action = FlowLogActionEnum.AutoTimeout,
+        OperatorId = 0,
+        OperatorName = "系统",
+        Comment = $"审批超时,自动升级给 {string.Join(", ", approvers.Select(a => a.userName))}",
+    });
+
+    // 通知
+    var targetUserIds = approvers.Select(a => a.userId).Distinct().ToList();
+    await _notifyService.NotifyEscalated(targetUserIds, instance.Id, instance.Title,
+        "系统", task.NodeName);
+}
+
+/// <summary>
+/// 系统自动同意(由定时任务调用)
+/// </summary>
+public async Task AutoApproveTask(ApprovalFlowTask task, ApprovalFlowInstance instance)
+{
+    task.Status = FlowTaskStatusEnum.Approved;
+    task.Comment = "系统自动通过(超时)";
+    task.ActionTime = DateTime.Now;
+    await _taskRep.AsUpdateable(task).ExecuteCommandAsync();
+
+    await _logRep.InsertAsync(new ApprovalFlowLog
+    {
+        InstanceId = instance.Id,
+        TaskId = task.Id,
+        NodeId = task.NodeId,
+        Action = FlowLogActionEnum.AutoTimeout,
+        OperatorId = 0,
+        OperatorName = "系统",
+        Comment = "审批超时,系统自动通过",
+    });
+
+    if (await IsNodeCompleted(instance, task.NodeId))
+    {
+        await InvokeHandler(instance.BizType,
+            h => h.OnNodeCompleted(instance.BizId, task.NodeId, task.NodeName ?? ""));
+        var flowData = DeserializeFlowJson(instance.FlowJsonSnapshot);
+        await AdvanceToNext(instance, flowData, task.NodeId);
+    }
+}
+
+/// <summary>
+/// 系统自动拒绝(由定时任务调用)
+/// </summary>
+public async Task AutoRejectTask(ApprovalFlowTask task, ApprovalFlowInstance instance)
+{
+    task.Status = FlowTaskStatusEnum.Rejected;
+    task.Comment = "系统自动拒绝(超时)";
+    task.ActionTime = DateTime.Now;
+    await _taskRep.AsUpdateable(task).ExecuteCommandAsync();
+
+    await CancelPendingTasks(task.InstanceId, task.NodeId, task.Id);
+
+    instance.Status = FlowInstanceStatusEnum.Rejected;
+    instance.EndTime = DateTime.Now;
+    await _instanceRep.AsUpdateable(instance).ExecuteCommandAsync();
+
+    await _logRep.InsertAsync(new ApprovalFlowLog
+    {
+        InstanceId = instance.Id,
+        TaskId = task.Id,
+        NodeId = task.NodeId,
+        Action = FlowLogActionEnum.AutoTimeout,
+        OperatorId = 0,
+        OperatorName = "系统",
+        Comment = "审批超时,系统自动拒绝",
+    });
+
+    await InvokeHandler(instance.BizType,
+        h => h.OnFlowCompleted(instance.BizId, FlowInstanceStatusEnum.Rejected));
+    await _notifyService.NotifyFlowCompleted(instance.InitiatorId, instance.Id,
+        instance.Title, FlowInstanceStatusEnum.Rejected);
+}
+```
+
+> **注意**:`AutoApproveTask`、`AutoRejectTask`、`AutoEscalateTask` 在定时任务中调用,此时 `_userManager` 无上下文,因此直接操作 DB 并手动写日志(操作人为"系统"),不走 `GetPendingTask`(其有 userId 校验)。同时需要将 `IsNodeCompleted`、`AdvanceToNext`、`CancelPendingTasks`、`ResolveApprovers` 等方法的访问修饰符从 `private` 改为 `internal`,以便定时任务类可以通过引擎服务调用它们(或者将自动处理逻辑全部封装在 `FlowEngineService` 内部,定时任务仅调用一个 public 方法)。
+
+**推荐封装方式**:在 `FlowEngineService` 中新增一个 public 方法供定时任务调用:
+
+```csharp
+/// <summary>
+/// 处理单个超时任务(由 FlowTimeoutJob 调用)
+/// </summary>
+public async Task HandleTimeoutTask(long taskId)
+{
+    var task = await _taskRep.GetByIdAsync(taskId);
+    if (task == null || task.Status != FlowTaskStatusEnum.Pending) return;
+
+    var instance = await _instanceRep.GetByIdAsync(task.InstanceId);
+    if (instance == null || instance.Status != FlowInstanceStatusEnum.Running) return;
+
+    var flowData = DeserializeFlowJson(instance.FlowJsonSnapshot);
+    var node = flowData.Nodes.FirstOrDefault(n => n.Id == task.NodeId);
+    var props = node?.Properties;
+    if (props == null) return;
+
+    var action = props.TimeoutAction;
+    switch (action)
+    {
+        case "Notify":
+            await _notifyService.NotifyUrge(new List<long> { task.AssigneeId },
+                instance.Id, instance.Title);
+            break;
+        case "AutoApprove":
+            await AutoApproveTask(task, instance);
+            break;
+        case "AutoReject":
+            await AutoRejectTask(task, instance);
+            break;
+        case "AutoEscalate":
+            await AutoEscalateTask(task, props, instance);
+            break;
+    }
+}
+```
+
+### 4.4 超时扫描定时任务 `FlowTimeoutJob`
+
+**新建文件**:`server/Plugins/Admin.NET.Plugin.ApprovalFlow/Job/FlowTimeoutJob.cs`
+
+```csharp
+using System.Text.Json;
+
+namespace Admin.NET.Plugin.ApprovalFlow;
+
+/// <summary>
+/// 审批超时自动处理作业 — 每 5 分钟扫描一次超时的待办任务
+/// </summary>
+[JobDetail("job_flow_timeout", Description = "审批超时自动处理",
+    GroupName = "default", Concurrent = false)]
+[Period(300000, TriggerId = "trigger_flow_timeout", Description = "每5分钟执行")]
+public class FlowTimeoutJob : IJob
+{
+    private readonly IServiceScopeFactory _scopeFactory;
+    private readonly ILogger _logger;
+
+    public FlowTimeoutJob(IServiceScopeFactory scopeFactory, ILoggerFactory loggerFactory)
+    {
+        _scopeFactory = scopeFactory;
+        _logger = loggerFactory.CreateLogger("FlowTimeoutJob");
+    }
+
+    public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
+    {
+        using var scope = _scopeFactory.CreateScope();
+        var db = scope.ServiceProvider.GetRequiredService<ISqlSugarClient>().CopyNew();
+        var engine = scope.ServiceProvider.GetRequiredService<FlowEngineService>();
+
+        // 1. 查询所有 Running 实例的 Pending 任务
+        var pendingTasks = await db.Queryable<ApprovalFlowTask>()
+            .InnerJoin<ApprovalFlowInstance>((t, i) => t.InstanceId == i.Id)
+            .Where((t, i) => t.Status == FlowTaskStatusEnum.Pending
+                          && i.Status == FlowInstanceStatusEnum.Running)
+            .Select((t, i) => new
+            {
+                Task = t,
+                FlowJsonSnapshot = i.FlowJsonSnapshot,
+            })
+            .ToListAsync();
+
+        var now = DateTime.Now;
+        var processed = 0;
+
+        foreach (var item in pendingTasks)
+        {
+            if (stoppingToken.IsCancellationRequested) break;
+
+            try
+            {
+                if (string.IsNullOrWhiteSpace(item.FlowJsonSnapshot)) continue;
+
+                var flowData = JsonSerializer.Deserialize<ApprovalFlowItem>(item.FlowJsonSnapshot);
+                var node = flowData?.Nodes?.FirstOrDefault(n => n.Id == item.Task.NodeId);
+                var props = node?.Properties;
+
+                if (props?.TimeoutHours == null || props.TimeoutHours <= 0) continue;
+                if (string.IsNullOrWhiteSpace(props.TimeoutAction)) continue;
+
+                // 计算超时:任务创建时间 + 超时小时数
+                var deadline = item.Task.CreateTime.AddHours(props.TimeoutHours.Value);
+                if (now < deadline) continue;
+
+                // 对 Notify 动作:避免重复通知(检查是否已有 AutoTimeout 日志)
+                if (props.TimeoutAction == "Notify")
+                {
+                    var alreadyNotified = await db.Queryable<ApprovalFlowLog>()
+                        .AnyAsync(log => log.TaskId == item.Task.Id
+                                      && log.Action == FlowLogActionEnum.AutoTimeout);
+                    if (alreadyNotified) continue;
+                }
+
+                await engine.HandleTimeoutTask(item.Task.Id);
+                processed++;
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "处理超时任务 {TaskId} 失败", item.Task.Id);
+            }
+        }
+
+        if (processed > 0)
+            _logger.LogInformation("FlowTimeoutJob 本轮处理了 {Count} 个超时任务", processed);
+    }
+}
+```
+
+### 4.5 API 端点扩展(`FlowTaskService`)
+
+新增升级接口:
+
+```csharp
+/// <summary>
+/// 升级
+/// </summary>
+[HttpPost]
+[ApiDescriptionSettings(Name = "Escalate")]
+[DisplayName("升级")]
+public async Task Escalate(TaskActionInput input)
+{
+    await _engine.Escalate(input.TaskId, input.Comment);
+}
+```
+
+新增查询接口(供前端判断节点是否配置了升级目标):
+
+```csharp
+/// <summary>
+/// 获取指定任务所属节点的升级配置
+/// </summary>
+[HttpGet]
+[ApiDescriptionSettings(Name = "GetEscalationConfig")]
+[DisplayName("获取升级配置")]
+public async Task<EscalationConfigOutput> GetEscalationConfig([FromQuery] long taskId)
+{
+    var task = await _taskRep.GetByIdAsync(taskId);
+    if (task == null) return new EscalationConfigOutput { Enabled = false };
+
+    var instance = await _instanceRep.GetByIdAsync(task.InstanceId);
+    if (instance == null) return new EscalationConfigOutput { Enabled = false };
+
+    var flowData = JsonSerializer.Deserialize<ApprovalFlowItem>(instance.FlowJsonSnapshot ?? "");
+    var node = flowData?.Nodes?.FirstOrDefault(n => n.Id == task.NodeId);
+    var props = node?.Properties;
+
+    return new EscalationConfigOutput
+    {
+        Enabled = props?.EnableManualEscalation == true
+                  && !string.IsNullOrWhiteSpace(props?.EscalationApproverType)
+                  && !string.IsNullOrWhiteSpace(props?.EscalationApproverIds),
+        EscalationApproverNames = props?.EscalationApproverNames,
+    };
+}
+```
+
+DTO 新增:
+
+```csharp
+public class EscalationConfigOutput
+{
+    public bool Enabled { get; set; }
+    public string? EscalationApproverNames { get; set; }
+}
+```
+
+### 4.6 通知扩展(`FlowNotifyService`)
+
+```csharp
+/// <summary>
+/// 升级通知 — 通知升级目标人
+/// </summary>
+public async Task NotifyEscalated(List<long> targetUserIds, long instanceId,
+    string title, string? fromName, string? nodeName)
+{
+    await NotifyUsers(targetUserIds, new FlowNotification
+    {
+        Type = FlowNotificationTypeEnum.Escalated,
+        InstanceId = instanceId,
+        Title = $"【升级】{title}",
+        Content = $"{fromName} 将审批任务({nodeName})升级给您,请及时处理。",
+    });
+}
+
+/// <summary>
+/// 超时提醒通知 — 通知当前审批人任务即将/已超时
+/// </summary>
+public async Task NotifyTimeout(List<long> userIds, long instanceId, string title)
+{
+    await NotifyUsers(userIds, new FlowNotification
+    {
+        Type = FlowNotificationTypeEnum.Timeout,
+        InstanceId = instanceId,
+        Title = $"【超时提醒】{title}",
+        Content = "您有一条审批任务已超时,请尽快处理。",
+    });
+}
+```
+
+`FlowNotificationTypeEnum` 新增:
+
+```csharp
+Escalated,
+Timeout,
+```
+
+### 4.7 前端设计器扩展(`PropertyCommon.vue`)
+
+在"审批配置" tab 中新增以下控件:
+
+**a. 超时动作增加"自动升级"选项**
+
+```vue
+<!-- 原有 3 个选项后增加 -->
+<el-option label="自动升级" value="AutoEscalate" />
+```
+
+**b. 升级目标配置区域**
+
+当 `timeoutAction == 'AutoEscalate'` 或 `enableManualEscalation == true` 时显示:
+
+```vue
+<el-col :span="24" class="mb20">
+    <el-form-item label="允许手动升级">
+        <el-switch v-model="formData.enableManualEscalation" />
+    </el-form-item>
+</el-col>
+
+<!-- 升级目标配置(当需要升级时显示) -->
+<template v-if="formData.timeoutAction === 'AutoEscalate' || formData.enableManualEscalation">
+    <el-divider content-position="left">升级目标配置</el-divider>
+    <el-col :span="24" class="mb20">
+        <el-form-item label="升级目标类型">
+            <el-select v-model="formData.escalationApproverType" placeholder="请选择">
+                <el-option label="指定用户" value="SpecificUser" />
+                <el-option label="指定角色" value="Role" />
+                <el-option label="指定部门" value="Department" />
+            </el-select>
+        </el-form-item>
+    </el-col>
+    <!-- 复用现有的用户搜索/角色下拉/部门树组件,绑定到 escalation* 字段 -->
+    <!-- ... 与上方审批人选择逻辑相同,字段改为 escalationApproverIds/Names -->
+</template>
+```
+
+**c. `formData` 和 `saveProperties` 扩展**
+
+```typescript
+// formData 新增
+escalationApproverType: '',
+escalationApproverIds: '',
+escalationApproverNames: '',
+enableManualEscalation: false,
+
+// saveProperties 中新增
+if (formData.timeoutAction === 'AutoEscalate' || formData.enableManualEscalation) {
+    data.escalationApproverType = formData.escalationApproverType;
+    data.escalationApproverIds = formData.escalationApproverIds;
+    data.escalationApproverNames = formData.escalationApproverNames;
+    data.enableManualEscalation = formData.enableManualEscalation;
+}
+```
+
+### 4.8 前端审批面板扩展(`ApprovalPanel.vue`)
+
+新增"升级"按钮,仅在节点启用了手动升级时显示:
+
+```vue
+<!-- 在现有操作按钮区域新增 -->
+<el-button
+    v-if="escalationConfig.enabled"
+    type="warning"
+    @click="showEscalateDialog = true"
+>
+    升级
+</el-button>
+```
+
+逻辑:
+1. 组件 mount 时,调用 `GET /api/flowTask/getEscalationConfig?taskId=xxx` 获取当前任务的升级配置
+2. 如果 `enabled == true`,显示"升级"按钮
+3. 点击后弹出确认弹窗(含升级原因 textarea)
+4. 确认后调用 `POST /api/flowTask/escalate`,传 `taskId` + `comment`
+5. 操作完成后触发 `refresh` 事件
+
+---
+
+## 五、改动文件清单
+
+| # | 文件 | 改动类型 | 说明 |
+|---|------|----------|------|
+| 1 | `Enum/FlowTaskStatusEnum.cs` | 修改 | 新增 `Escalated = 6` |
+| 2 | `Enum/FlowLogActionEnum.cs` | 修改 | 新增 `Escalate = 11` |
+| 3 | `Service/ApprovalFlow/Dto/ApprovalFlowItem.cs` | 修改 | `FlowProperties` 新增 4 个升级配置字段 |
+| 4 | `Service/FlowEngine/FlowEngineService.cs` | 修改 | 新增 `Escalate()`、`HandleTimeoutTask()`、`AutoEscalateTask()`、`AutoApproveTask()`、`AutoRejectTask()` |
+| 5 | `Service/FlowTask/FlowTaskService.cs` | 修改 | 新增 `Escalate`、`GetEscalationConfig` API |
+| 6 | `Service/FlowTask/Dto/FlowTaskDtos.cs` | 修改 | 新增 `EscalationConfigOutput` DTO |
+| 7 | `Service/FlowNotify/FlowNotifyService.cs` | 修改 | 新增 `NotifyEscalated`、`NotifyTimeout`;枚举新增 `Escalated`、`Timeout` |
+| 8 | **新建** `Job/FlowTimeoutJob.cs` | 新建 | 超时扫描定时任务 |
+| 9 | `Web/.../PropertyCommon.vue` | 修改 | 超时动作增加"自动升级";新增升级目标配置 UI |
+| 10 | `Web/.../ApprovalPanel.vue` | 修改 | 新增"升级"按钮和确认弹窗 |
+
+### 不改的部分
+
+- `IFlowBizHandler` 接口签名不变
+- 现有审批/拒绝/转办/退回/加签/催办/撤回逻辑不变
+- S8 业务代码不动
+- 数据库表结构不变(`FlowProperties` 存在 FlowJson 中,无需 ALTER TABLE)
+
+---
+
+## 六、验收标准
+
+### 6.1 超时自动处理(新增功能)
+
+- [ ] 配置超时 1 小时 + `Notify`:超时后审批人收到催办通知,且仅发一次
+- [ ] 配置超时 1 小时 + `AutoApprove`:超时后任务自动通过,流程正常推进到下一节点
+- [ ] 配置超时 1 小时 + `AutoReject`:超时后任务自动拒绝,流程结束,发起人收到拒绝通知
+- [ ] 配置超时 1 小时 + `AutoEscalate`:超时后任务自动升级到配置的更高层级审批人,升级目标收到新待办
+- [ ] 未配置超时的节点不受定时任务影响
+- [ ] 定时任务异常时不影响其他任务的扫描处理(单任务隔离)
+
+### 6.2 手动升级(新增功能)
+
+- [ ] 节点开启 `enableManualEscalation` 且配置了升级目标后,审批面板显示"升级"按钮
+- [ ] 节点未配置升级目标时,"升级"按钮不显示
+- [ ] 点击"升级"后,原任务标记 `Escalated`,同节点其他 Pending 任务被取消
+- [ ] 升级目标收到新的待办任务和通知
+- [ ] 升级后审批时间线正确记录升级事件(操作人、升级目标、升级原因)
+- [ ] 升级目标审批通过后,流程正常推进到下一节点
+- [ ] 升级目标审批拒绝后,流程正常终止
+
+### 6.3 设计器(新增功能)
+
+- [ ] 超时动作下拉新增"自动升级"选项
+- [ ] 新增"允许手动升级"开关
+- [ ] 选择"自动升级"或开启"允许手动升级"后,出现升级目标配置区域
+- [ ] 升级目标支持选择用户/角色/部门,与审批人配置体验一致
+- [ ] 配置保存后 FlowJson 中正确包含 `escalation*` 字段
+- [ ] 重新打开节点属性时,升级配置正确回显
+
+### 6.4 既有功能全量回归测试
+
+以下为审批流引擎现有功能的全量回归,确保本次改动不引入破坏。
+
+#### 6.4.1 流程发起
+
+- [ ] 正常发起审批流程,实例状态为 `Running`,第一个审批节点生成待办任务
+- [ ] 发起时业务回调 `OnFlowStarted` 正确触发
+- [ ] 未发布流程定义时,发起报错提示"未找到已发布流程定义"
+
+#### 6.4.2 同意(Approve)
+
+- [ ] 单人审批:同意后任务状态 `Approved`,流程推进到下一节点
+- [ ] 或签(Any):一人同意后其他 Pending 任务自动取消,流程推进
+- [ ] 会签(All):需所有人同意后流程才推进,部分同意时流程停留在当前节点
+- [ ] 最终节点同意后,实例状态 `Approved`,回调 `OnFlowCompleted(Approved)` 触发
+- [ ] 同意后 `OnNodeCompleted` 回调正确触发
+
+#### 6.4.3 拒绝(Reject)
+
+- [ ] 拒绝后任务状态 `Rejected`,同节点其他 Pending 任务取消
+- [ ] 实例状态 `Rejected`,回调 `OnFlowCompleted(Rejected)` 触发
+- [ ] 发起人收到拒绝通知
+
+#### 6.4.4 转办(Transfer)
+
+- [ ] 转办后原任务 `Transferred`,新任务 `Pending` 分配给目标用户
+- [ ] 目标用户收到转办通知
+- [ ] 目标用户可正常审批(同意/拒绝)
+
+#### 6.4.5 撤回(Withdraw)
+
+- [ ] 发起人可撤回 Running 状态的流程(仅首节点无人审批时)
+- [ ] 撤回后所有 Pending 任务取消,实例状态 `Cancelled`
+- [ ] 回调 `OnFlowCompleted(Cancelled)` 触发
+- [ ] 已有人审批过的流程不允许撤回
+
+#### 6.4.6 退回上一步(ReturnToPrev)
+
+- [ ] 退回后当前节点任务取消,上一个用户任务节点重新生成待办
+- [ ] 实例 `CurrentNodeId` 更新为上一节点
+- [ ] 被退回的审批人收到退回通知
+- [ ] 第一个审批节点无法退回(提示"已是第一个审批节点")
+
+#### 6.4.7 加签(AddSign)
+
+- [ ] 加签后新增一个 Pending 任务给目标用户,标记 `IsAddSign=true`
+- [ ] 原审批人的任务保持 Pending
+- [ ] 加签目标收到通知
+
+#### 6.4.8 催办(Urge)
+
+- [ ] 催办后所有当前节点 Pending 审批人收到催办通知
+- [ ] 时间线记录催办操作
+
+#### 6.4.9 条件网关(Exclusive Gateway)
+
+- [ ] 网关根据 `GetBizData` 返回的业务变量正确路由
+- [ ] 多条件匹配时走第一个命中的分支
+- [ ] 所有条件不匹配时走 `default` 分支
+- [ ] 无 `default` 分支且不匹配时走第一条出边
+
+#### 6.4.10 通知
+
+- [ ] 新任务通知、同意/拒绝通知、转办/退回/加签/催办/撤回通知均正常发送
+- [ ] SignalR 站内消息正常推送
+
+#### 6.4.11 审批中心页面
+
+- [ ] "我的待办"列表正确显示当前用户的 Pending 任务
+- [ ] "我的已办"列表正确显示已处理任务
+- [ ] "我发起的"列表正确显示发起人的流程实例及当前审批人
+- [ ] 待办数量接口返回正确
+
+#### 6.4.12 审批面板(ApprovalPanel)
+
+- [ ] 无实例时显示"提交审批"按钮,点击后发起流程
+- [ ] 有实例且当前用户有 Pending 任务时,显示同意/拒绝/转办/退回/加签按钮
+- [ ] 发起人在 Running 状态时可催办和撤回
+- [ ] 审批时间线正确展示所有操作记录
+
+---
+
+## 七、实施步骤
+
+### Step 1:后端枚举与 DTO 扩展
+
+**目标**:为升级机制准备数据模型基础
+
+| 序号 | 任务 | 文件 |
+|------|------|------|
+| 1.1 | `FlowTaskStatusEnum` 新增 `Escalated = 6` | `Enum/FlowTaskStatusEnum.cs` |
+| 1.2 | `FlowLogActionEnum` 新增 `Escalate = 11` | `Enum/FlowLogActionEnum.cs` |
+| 1.3 | `FlowProperties` 新增 `EscalationApproverType`、`EscalationApproverIds`、`EscalationApproverNames`、`EnableManualEscalation` 四个字段 | `Service/ApprovalFlow/Dto/ApprovalFlowItem.cs` |
+| 1.4 | 新增 `EscalationConfigOutput` DTO | `Service/FlowTask/Dto/FlowTaskDtos.cs` |
+| 1.5 | `FlowNotificationTypeEnum` 新增 `Escalated`、`Timeout` | `Service/FlowNotify/FlowNotifyService.cs` |
+
+**完成标志**:后端编译通过,无报错
+
+---
+
+### Step 2:引擎核心逻辑——手动升级
+
+**目标**:实现审批人主动升级操作
+
+| 序号 | 任务 | 文件 |
+|------|------|------|
+| 2.1 | 新增 `Escalate(long taskId, string? comment)` 方法 | `FlowEngineService.cs` |
+| 2.2 | 逻辑:校验节点升级配置 → 标记当前任务 `Escalated` → 取消同节点其他 Pending → 解析升级目标 → 创建新 Pending 任务 → 写日志 → 发通知 | `FlowEngineService.cs` |
+| 2.3 | `FlowNotifyService` 新增 `NotifyEscalated()` 方法 | `FlowNotifyService.cs` |
+| 2.4 | `FlowTaskService` 新增 `Escalate` API 端点 | `FlowTaskService.cs` |
+| 2.5 | `FlowTaskService` 新增 `GetEscalationConfig` 查询端点 | `FlowTaskService.cs` |
+
+**完成标志**:通过 API 调用手动升级,升级目标收到新待办任务,时间线记录正确
+
+---
+
+### Step 3:引擎核心逻辑——超时自动处理
+
+**目标**:实现超时自动通知/通过/拒绝/升级
+
+| 序号 | 任务 | 文件 |
+|------|------|------|
+| 3.1 | 新增 `HandleTimeoutTask(long taskId)` 统一入口方法 | `FlowEngineService.cs` |
+| 3.2 | 新增 `AutoApproveTask()`:系统自动同意,推进流程 | `FlowEngineService.cs` |
+| 3.3 | 新增 `AutoRejectTask()`:系统自动拒绝,终止流程 | `FlowEngineService.cs` |
+| 3.4 | 新增 `AutoEscalateTask()`:系统自动升级到配置目标 | `FlowEngineService.cs` |
+| 3.5 | `FlowNotifyService` 新增 `NotifyTimeout()` 方法 | `FlowNotifyService.cs` |
+
+**注意**:以上方法在定时任务中调用,无 `UserManager` 上下文,操作人记为"系统",不走 `GetPendingTask` 的用户校验
+
+**完成标志**:直接通过代码调用 `HandleTimeoutTask` 可执行 4 种超时动作,各自行为正确
+
+---
+
+### Step 4:超时扫描定时任务
+
+**目标**:让超时处理自动运行
+
+| 序号 | 任务 | 文件 |
+|------|------|------|
+| 4.1 | 新建 `FlowTimeoutJob`:每 5 分钟扫描超时的 Pending 任务 | **新建** `Job/FlowTimeoutJob.cs` |
+| 4.2 | 扫描逻辑:查所有 Running 实例的 Pending 任务 → 取节点 `timeoutHours` → 判断是否超时 → 调用 `HandleTimeoutTask` | `Job/FlowTimeoutJob.cs` |
+| 4.3 | `Notify` 动作防重复:检查是否已有 `AutoTimeout` 日志,有则跳过 | `Job/FlowTimeoutJob.cs` |
+| 4.4 | 单任务异常隔离:`try-catch` 包裹每个任务处理,不影响其他任务 | `Job/FlowTimeoutJob.cs` |
+
+**完成标志**:启动后端,在数据库中创建一个配置了超时的审批任务,等待 5 分钟后定时任务自动触发对应动作
+
+---
+
+### Step 5:前端设计器扩展
+
+**目标**:管理员可在流程设计器中配置升级目标
+
+| 序号 | 任务 | 文件 |
+|------|------|------|
+| 5.1 | 超时动作下拉增加"自动升级"(`AutoEscalate`)选项 | `PropertyCommon.vue` |
+| 5.2 | 新增"允许手动升级"开关(`enableManualEscalation`) | `PropertyCommon.vue` |
+| 5.3 | 当需要升级时(自动升级或手动升级启用),显示升级目标配置区域:类型选择 + 用户搜索/角色下拉/部门树 | `PropertyCommon.vue` |
+| 5.4 | `formData` 新增 `escalationApproverType`、`escalationApproverIds`、`escalationApproverNames`、`enableManualEscalation` | `PropertyCommon.vue` |
+| 5.5 | `saveProperties` 将升级配置写入节点 JSON | `PropertyCommon.vue` |
+| 5.6 | `watch(nodeData)` 中回显升级配置 | `PropertyCommon.vue` |
+
+**完成标志**:在设计器中配置升级目标,保存后重新打开节点属性,配置正确回显;发布流程后 FlowJson 包含 `escalation*` 字段
+
+---
+
+### Step 6:前端审批面板扩展
+
+**目标**:审批人可在审批面板中手动升级
+
+| 序号 | 任务 | 文件 |
+|------|------|------|
+| 6.1 | 组件加载时调用 `GetEscalationConfig` 获取当前任务的升级配置 | `ApprovalPanel.vue` |
+| 6.2 | 当 `enabled == true` 时显示"升级"按钮(`type="warning"`) | `ApprovalPanel.vue` |
+| 6.3 | 点击"升级"弹出确认弹窗:显示升级目标名称 + 升级原因 textarea | `ApprovalPanel.vue` |
+| 6.4 | 确认后调用 `POST /api/flowTask/escalate`,完成后触发 `refresh` | `ApprovalPanel.vue` |
+
+**完成标志**:在启用了手动升级的节点,审批面板显示"升级"按钮;点击后任务升级成功,面板刷新
+
+---
+
+### Step 7:编译与后端启动验证
+
+| 序号 | 任务 |
+|------|------|
+| 7.1 | 后端 `dotnet build` 编译通过,无错误无警告 |
+| 7.2 | 后端 `dotnet run` 启动正常,定时任务注册成功(日志中可见 `job_flow_timeout`) |
+| 7.3 | 前端 `pnpm dev` 启动正常,设计器和审批面板页面无报错 |
+
+---
+
+### Step 8:全量测试
+
+按照第六章"验收标准"逐项执行测试:
+
+| 序号 | 测试范围 | 对应验收章节 |
+|------|----------|------------|
+| 8.1 | 超时自动处理:4 种超时动作各一条 | 6.1 |
+| 8.2 | 手动升级:配置/未配置升级目标、升级后审批通过/拒绝 | 6.2 |
+| 8.3 | 设计器:配置保存、回显、FlowJson 内容 | 6.3 |
+| 8.4 | 全量回归:发起、同意、拒绝、转办、撤回、退回、加签、催办、网关、通知、审批中心、审批面板 | 6.4.1 ~ 6.4.12 |
+
+**完成标志**:验收标准全部 checkbox 打勾
+
+---
+
+## 八、工作量估算
+
+| 阶段 | 对应 Step | 内容 | 预计工时 |
+|------|-----------|------|----------|
+| 后端枚举 + DTO | Step 1 | 枚举新增、FlowProperties 扩展、DTO 新增 | 0.5h |
+| 手动升级 | Step 2 | Escalate 引擎方法 + API + 通知 | 2h |
+| 超时自动处理 | Step 3 | HandleTimeoutTask + Auto 系列方法 | 2h |
+| 定时任务 | Step 4 | FlowTimeoutJob | 1h |
+| 前端设计器 | Step 5 | PropertyCommon.vue 升级配置 UI | 2h |
+| 前端审批面板 | Step 6 | ApprovalPanel.vue 升级按钮 + 弹窗 | 1h |
+| 编译验证 | Step 7 | 前后端编译启动 | 0.5h |
+| 全量测试 | Step 8 | 新功能 + 全量回归 | 3h |
+| **合计** | | | **约 12h(1.5 天)** |

+ 299 - 0
doc/plan/审批流-综合优化方案.md

@@ -0,0 +1,299 @@
+# 审批流 — 综合优化方案
+
+> 本文档汇总 P0 / P1 / P2 / P3 剩余优化项的技术方案,确认后分步实施。
+
+---
+
+## 一、P0-2:新增"部门负责人"审批人类型
+
+### 需求
+
+当前审批人类型只有:指定用户、指定角色、指定部门(全员)、发起人本人。缺少"发起人所在部门的负责人"选项,导致多部门场景下无法自动定位审批人。
+
+### 方案
+
+**后端:**
+
+1. `FlowProperties` 的 `ApproverType` 新增枚举值 `DepartmentLeader`
+2. `ResolveApprovers()` 中新增分支:
+   - 根据 `initiatorId` 查 `SysUser.OrgId` 获取所属部门
+   - 根据 `SysOrg.ManagerUserId`(或该组织的 `ManagerUserId` 字段)获取部门负责人
+   - 如果 `SysOrg` 没有 `ManagerUserId`,回退查该部门下 `AccountType` 最高的用户
+3. 无需额外 `ApproverIds` — 发起时动态解析
+
+**前端:**
+
+4. `PropertyCommon.vue` 的"审批人类型"下拉增加"部门负责人"选项,选中后无需再选人/角色/部门
+5. 升级目标配置同样增加该选项
+
+**涉及文件:**
+- `FlowEngineService.cs`(`ResolveApprovers` 方法)
+- `PropertyCommon.vue`(审批人类型下拉)
+
+**数据库确认结果:**
+- `SysOrg.DirectorId`(bigint, nullable)— 部门负责人 ID,**字段已存在**但当前无数据
+- `SysUser.ManagerUserId`(bigint, nullable)— 用户的直属上级 ID
+
+**实现策略:** 优先用 `SysOrg.DirectorId`(组织负责人);如果未设置,回退用发起人的 `SysUser.ManagerUserId`(直属上级)。两者都未设则报错"该部门/用户未设置负责人"。
+
+---
+
+## 二、P0-3:ApprovalPanel 补传 currentUserId
+
+### 需求
+
+`ApprovalPanel` 组件的 `isInitiator` 计算属性依赖 `props.currentUserId`,但嵌入业务页面(如 `salesOrderForm.vue`)时未传入,导致发起人看不到"催办""撤回"按钮。
+
+### 方案
+
+**方案 A(推荐):组件内部自动获取**
+
+- `ApprovalPanel.vue` 内部从 Pinia store(`useUserInfo().userInfos.id`)获取当前用户 ID,不再依赖外部传入
+- `currentUserId` prop 变为可选,优先用 prop,没传则自动取
+- 好处:所有嵌入场景自动生效,无需逐个页面改
+
+**方案 B:修改每个嵌入页面**
+
+- 逐个在 `salesOrderForm.vue` 等页面传入 `currentUserId`
+- 缺点:每新增一个嵌入页都要记得传
+
+**涉及文件:**
+- `ApprovalPanel.vue`(推荐方案 A 只改这一个文件)
+
+---
+
+## 三、P1-5:并行网关
+
+### 需求
+
+当前仅有排他网关(二选一分支)。缺少并行网关,无法实现"财务和技术同时审批,都通过后才继续"的场景。
+
+### 方案
+
+1. LogicFlow 画布中注册新节点类型 `bpmn:parallelGateway`(分叉 + 汇合)
+2. 后端 `AdvanceToNext()` 遇到并行网关分叉时,沿所有出边同时创建任务
+3. 遇到并行网关汇合时,检查所有入边对应节点是否都已完成(`Join` 语义),全完成才继续推进
+4. `ApprovalFlowInstance` 增加 `CompletedNodeIds`(JSON 数组)字段跟踪已完成分支
+
+**涉及文件:**
+- `FlowEngineService.cs`(`AdvanceToNext` 中增加并行网关处理)
+- `ApprovalFlowItem.cs`(可能需要新增并行网关节点类型识别)
+- `ApprovalFlowInstance` 实体(新增 `CompletedNodeIds`)
+- 前端 LogicFlow 注册 `parallelGateway` 节点 + 属性面板
+
+**复杂度:** 高。需仔细处理并发、部分分支失败/拒绝时的汇合逻辑。
+
+---
+
+## 四、P1-6:审批代理/委托
+
+### 需求
+
+用户请假时无法设置"我的审批由 XX 代理",导致流程卡住。
+
+### 方案
+
+1. 新建实体 `ApprovalFlowDelegate`:`UserId`、`DelegateUserId`、`StartTime`、`EndTime`、`BizType`(可空=全局)、`IsEnabled`
+2. 用户在"个人中心"或"审批中心"设置代理
+3. `FlowEngineService.CreateTasksForNode()` 创建任务时检查 assignee 是否设了有效代理,是则**同时**给代理人创建一条并行任务(本人和代理人都能审批,任一人操作后取消另一人的待办)
+4. 审批中心"我的待办"同时展示代理人委托给我的待办(标记"代理"标签)
+
+**涉及文件:**
+- 新建 `Entity/ApprovalFlowDelegate.cs`
+- 新建 `Service/FlowDelegate/FlowDelegateService.cs`(CRUD + API)
+- `FlowEngineService.cs`(创建任务时检查代理)
+- `FlowTaskService.cs`(查询待办时合并代理人任务)
+- 前端新增"审批代理设置"页面
+
+**复杂度:** 中。
+
+---
+
+## 五、P1-7:流程版本管理
+
+### 需求
+
+修改已发布流程并重新发布后,在途实例仍引用旧版本的快照(`FlowJsonSnapshot`),但缺少明确的版本历史和回退机制。
+
+### 方案
+
+1. `ApprovalFlow` 实体已有 `Version` 字段。`Publish` 时将 `Version++` 并保存旧版本到 `ApprovalFlowVersion` 表(`FlowId`、`Version`、`FlowJson`、`PublishTime`)
+2. `FlowInstance.FlowJsonSnapshot` 已经在发起时快照了 — 在途实例不受影响(当前行为正确)
+3. 增加"版本历史"查看 API + 前端弹窗展示
+4. 可选:支持"回退到某版本"
+
+**涉及文件:**
+- 新建 `Entity/ApprovalFlowVersion.cs`
+- `ApprovalFlowService.cs`(Publish 时写版本历史)
+- 前端流程管理页增加"版本历史"按钮
+
+**复杂度:** 低。在途实例已自带快照,核心只是多存一份版本记录。
+
+---
+
+## 六、P1-8:批量审批
+
+### 需求
+
+待办列表不能勾选多条一键同意/拒绝,审批量大时效率低。
+
+### 方案
+
+1. 后端 `FlowTaskService` 新增 `BatchApprove(List<long> taskIds, string? comment)` 和 `BatchReject` API
+2. 内部循环调用 `FlowEngineService.Approve/Reject`,收集成功/失败结果
+3. 前端 `PendingList.vue` 增加 `el-table` 的 `@selection-change`,顶部增加"批量同意""批量拒绝"按钮
+
+**涉及文件:**
+- `FlowTaskService.cs`(新增 BatchApprove/BatchReject)
+- `FlowTaskDtos.cs`(新增 BatchActionInput)
+- `PendingList.vue`(勾选 + 按钮)
+- `center/index.vue`(事件处理)
+
+**复杂度:** 低。
+
+---
+
+## 七、P1-9:待办数量角标(导航栏)
+
+### 需求
+
+头部导航显示审批待办数量,用户无需进入审批中心即可感知。
+
+### 方案
+
+1. 后端 `myPendingCount` API 已存在
+2. 前端布局头部组件(如 `layout/navBars/breadcrumb/user.vue` 或类似)增加一个铃铛/消息图标 + `el-badge`
+3. 登录后和定期(60s)调用 `myPendingCount` 刷新
+
+**涉及文件:**
+- 前端布局头部组件(1 个文件)
+- 可能需要新增一个审批通知入口组件
+
+**复杂度:** 低。
+
+---
+
+## 八、P2-10:流程图预览
+
+### 需求
+
+审批中心查看某个流程实例时,能看到流程图并高亮当前所在节点。
+
+### 方案
+
+1. 前端新建 `FlowPreview.vue` 组件:使用 LogicFlow 只读模式加载 `flowJsonSnapshot`
+2. 根据 `instance.currentNodeId` 高亮对应节点(设置节点样式 `fill` / `stroke`)
+3. 审批中心时间线弹窗旁增加"流程图"Tab
+4. `ApprovalPanel` 里也可嵌入一个折叠区域展示
+
+**涉及文件:**
+- 新建 `component/FlowPreview.vue`
+- `TimelineDialog.vue` 或 `center/index.vue` 增加入口
+- `ApprovalPanel.vue` 可选增加折叠区
+
+**复杂度:** 中。LogicFlow 只读模式 + 节点高亮需要调 API。
+
+---
+
+## 九、P2-11:审批意见模板(方案 B — 后端表 + 预填常用语)
+
+### 需求
+
+常用审批意见快捷选择,用户可自定义管理。
+
+### 方案
+
+1. 新建实体 `ApprovalFlowCommentTemplate`:`Id`、`Content`(意见内容)、`OrderNo`(排序)、`IsSystem`(系统预置/用户自定义)、`UserId`(可空=公共模板)、`IsEnabled`
+2. 后端 `FlowCommentTemplateService`:CRUD API + 列表查询
+3. 系统预填以下常用意见(`IsSystem=true`):
+   - 同意
+   - 已审核,无问题
+   - 情况属实,同意处理
+   - 不同意,请补充材料后重新提交
+   - 退回修改
+   - 已阅,请按流程继续推进
+   - 金额/数量有误,请核实后重新提交
+   - 紧急事项,加急处理
+4. 前端审批意见输入框旁增加"常用意见"下拉按钮,点击展开列表,选中后填入输入框
+
+**涉及文件:**
+- 新建 `Entity/ApprovalFlowCommentTemplate.cs`
+- 新建 `Service/FlowCommentTemplate/FlowCommentTemplateService.cs`
+- `ApprovalPanel.vue`(意见输入框旁增加下拉)
+- `center/index.vue` 的 `ApprovalDialog`(同样增加)
+- `api.ts`(新增模板 API)
+
+**复杂度:** 低。
+
+---
+
+## 十、P2-12:移动端适配
+
+### 需求
+
+审批中心和审批面板在移动端(手机浏览器/企业微信内嵌)可用。
+
+### 方案
+
+1. 审批中心页面和 ApprovalPanel 增加响应式样式(媒体查询 `@media`)
+2. 按钮改为垂直排列、表格改为卡片列表
+3. 如有企业微信/钉钉集成,可通过 JS-SDK 推送消息跳转到审批页
+
+**复杂度:** 中。主要是 CSS 工作 + 测试。暂不涉及后端改动。
+
+---
+
+## 十一、P3-14:子流程 / 流程调用
+
+### 需求
+
+复杂业务中一个审批节点触发另一个审批流程(子流程),子流程完成后回到父流程继续。
+
+### 方案
+
+1. 新增节点类型 `bpmn:callActivity`(调用活动),配置目标子流程 `flowCode`
+2. 引擎推进到该节点时:启动子流程实例,父实例状态设为 `WaitingSubFlow`
+3. 子流程完成回调时:通知父流程继续推进
+
+**复杂度:** 高。涉及引擎状态机重大扩展。建议后期专项处理。
+
+---
+
+## 十二、P3-15:审批数据统计/报表
+
+### 需求
+
+管理层想看:各流程平均审批时长、各节点瓶颈、审批通过率/拒绝率等。
+
+### 方案
+
+1. 基于 `ApprovalFlowLog` + `ApprovalFlowTask` 表做聚合查询
+2. 后端新增 `FlowStatisticsService`,提供按流程/时间段/审批人维度的统计 API
+3. 前端新增"审批统计"页面(图表:ECharts)
+
+**复杂度:** 中。主要是 SQL 聚合 + 前端图表。
+
+---
+
+## 实施建议
+
+| 批次 | 项目 | 复杂度 | 预计工时 |
+|---|---|---|---|
+| **第一批** | P0-2 部门负责人、P0-3 currentUserId、P1-9 待办角标、P2-11 意见模板 | 低 | 4h |
+| **第二批** | P1-8 批量审批、P1-7 版本管理 | 低 | 4h |
+| **第三批** | P1-6 审批代理、P2-10 流程图预览 | 中 | 8h |
+| **第四批** | P1-5 并行网关 | 高 | 8h |
+| **第五批** | P2-12 移动端适配 | 中 | 6h |
+| *后期* | *P3-14 子流程、P3-15 统计报表* | *高* | *另行安排* |
+
+---
+
+## 已确认决策
+
+1. **P0-2**:`SysOrg.DirectorId` 存在(当前无数据),`SysUser.ManagerUserId` 存在。优先用 DirectorId,回退用 ManagerUserId。 ✓
+2. **P0-3**:采用方案 A — 组件内自动获取。 ✓
+3. **P1-5 并行网关**:本轮纳入。 ✓
+4. **P1-6 审批代理**:代理人和本人并行(两者都能审批)。 ✓
+5. **P2-11 意见模板**:采用方案 B — 后端表 + 预填常用审批意见。 ✓
+6. **P3 子流程和统计**:后期做,本轮不纳入。 ✓
+7. **实施批次顺序**:同意。 ✓

+ 73 - 0
doc/审批流功能说明.md

@@ -141,6 +141,79 @@
 | → 加急审批 | `urgent == 1` | 加急订单走快速审批 |
 | → 普通审批 | `default` | 其他订单走常规审批 |
 
+### 4.3 典型配置场景
+
+#### 场景一:按金额分级审批(不同金额不同审批人)
+
+**需求**:金额 3000 以下由副主任审批,3000 以上由主任审批。
+
+**流程结构:**
+
+```
+开始 → 排他网关 ─── [amount < 3000] ──→ 审批节点A(副主任)→ 结束
+                 └─ [amount >= 3000] ─→ 审批节点B(主任)  → 结束
+```
+
+**配置步骤:**
+
+1. 画一个**开始节点**,连到一个**排他网关**
+2. 从网关拉出**两条线**,分别连到两个**审批节点**,再各自连到**结束节点**
+3. 选中第一条出线,条件填 `amount < 3000`
+4. 选中第二条出线,条件填 `amount >= 3000`(或设为 `default`)
+5. 审批节点 A:审批人类型选"指定用户",选副主任
+6. 审批节点 B:审批人类型选"指定用户",选主任
+
+> **前提**:业务的 `IFlowBizHandler.GetBizData()` 需返回 `amount` 字段,网关条件中的变量名必须与返回的字典 key 一致。
+
+#### 场景二:金额越大审批层级越多
+
+**需求**:金额 3000 以下副主任即可,3000~10000 需副主任 + 主任两级审批,10000 以上还需加总经理。
+
+**流程结构:**
+
+```
+开始 → 排他网关 ─── [amount < 3000] ───→ 副主任审批 → 结束
+                 ├─ [amount < 10000] ──→ 副主任审批 → 主任审批 → 结束
+                 └─ [default] ─────────→ 副主任审批 → 主任审批 → 总经理审批 → 结束
+```
+
+**要点**:排他网关的条件按**从小到大**排列,引擎匹配到第一个满足的条件即走该分支。`default` 兜底最高金额档。
+
+#### 场景三:多条件组合(加急 + 金额)
+
+**需求**:加急且金额超过 5000 走总经理直批,其他走常规审批链。
+
+**配置**:
+
+| 出线 | 条件 | 目标 |
+|---|---|---|
+| → 总经理直批 | `urgent == 1 && amount > 5000` | 审批节点(总经理) |
+| → 常规审批 | `default` | 审批节点(部门经理)→ 审批节点(主任) |
+
+多条件用 `&&` 连接,所有条件同时满足时走该分支。
+
+#### 场景四:会签与或签
+
+**需求**:某个环节需要 3 位部门负责人共同审批(全票通过),或者只需任一人通过。
+
+**配置**:在审批节点的属性面板中:
+- 审批人类型选"指定用户",选中全部 3 位负责人
+- 多人审批方式选**会签**(全部通过才行)或**或签**(任一通过即可)
+
+#### 条件表达式中可用的变量
+
+条件表达式中的变量来源于业务模块的 `IFlowBizHandler.GetBizData()` 返回值。不同业务类型可提供不同的变量:
+
+**订单评审(ORDER_REVIEW)当前可用变量:**
+
+| 变量名 | 含义 | 示例值 | 表达式示例 |
+|---|---|---|---|
+| `urgent` | 是否加急(1=加急, 0=普通) | `1` | `urgent == 1` |
+| `customLevel` | 客户等级 | `3` | `customLevel >= 3` |
+| `orderType` | 订单类型(1=销售, 2=计划) | `1` | `orderType == 1` |
+
+如需增加新的条件变量(如金额 `amount`),由开发人员在对应业务的 `GetBizData()` 中添加返回字段即可,无需修改审批流引擎。
+
 #### 设计器工具栏
 
 | 按钮 | 功能 |

+ 3 - 3
server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 通用权限开发平台</Description>
-    <AssemblyVersion>1.0.44</AssemblyVersion>
-    <FileVersion>1.0.44</FileVersion>
-    <Version>1.0.44</Version>
+    <AssemblyVersion>1.0.47</AssemblyVersion>
+    <FileVersion>1.0.47</FileVersion>
+    <Version>1.0.47</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 39 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Entity/ApprovalFlowCommentTemplate.cs

@@ -0,0 +1,39 @@
+namespace Admin.NET.Plugin.ApprovalFlow;
+
+/// <summary>
+/// 审批意见模板表
+/// </summary>
+[SugarTable(null, "审批意见模板表")]
+public class ApprovalFlowCommentTemplate : EntityBase
+{
+    /// <summary>
+    /// 意见内容
+    /// </summary>
+    [SugarColumn(ColumnDescription = "意见内容", Length = 512)]
+    [Required, MaxLength(512)]
+    public string Content { get; set; } = "";
+
+    /// <summary>
+    /// 排序
+    /// </summary>
+    [SugarColumn(ColumnDescription = "排序", DefaultValue = "100")]
+    public int OrderNo { get; set; } = 100;
+
+    /// <summary>
+    /// 是否系统预置(系统预置不可删除)
+    /// </summary>
+    [SugarColumn(ColumnDescription = "是否系统预置", DefaultValue = "0")]
+    public bool IsSystem { get; set; }
+
+    /// <summary>
+    /// 所属用户Id(空 = 公共模板)
+    /// </summary>
+    [SugarColumn(ColumnDescription = "所属用户Id", IsNullable = true)]
+    public long? UserId { get; set; }
+
+    /// <summary>
+    /// 是否启用
+    /// </summary>
+    [SugarColumn(ColumnDescription = "是否启用", DefaultValue = "1")]
+    public bool IsEnabled { get; set; } = true;
+}

+ 43 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Entity/ApprovalFlowCompletedNode.cs

@@ -0,0 +1,43 @@
+namespace Admin.NET.Plugin.ApprovalFlow;
+
+/// <summary>
+/// 流程实例已完成节点表(P1-5 并行网关支持,同时也为 P3-15 节点级统计提供数据源)
+/// 记录某个流程实例中每个节点的完成状态,含 userTask / exclusiveGateway / parallelGateway 等所有节点类型。
+/// </summary>
+[SugarTable(null, "流程实例已完成节点表")]
+[SugarIndex("uq_flowcompleted_inst_node", nameof(InstanceId), OrderByType.Asc, nameof(NodeId), OrderByType.Asc, true)]
+public class ApprovalFlowCompletedNode : EntityBase
+{
+    /// <summary>
+    /// 流程实例 Id
+    /// </summary>
+    [SugarColumn(ColumnDescription = "流程实例Id")]
+    public long InstanceId { get; set; }
+
+    /// <summary>
+    /// 节点 Id(FlowJson 中的 node.id)
+    /// </summary>
+    [SugarColumn(ColumnDescription = "节点Id", Length = 64)]
+    [MaxLength(64)]
+    public string NodeId { get; set; } = "";
+
+    /// <summary>
+    /// 节点名称(冗余,便于展示与统计)
+    /// </summary>
+    [SugarColumn(ColumnDescription = "节点名称", Length = 128, IsNullable = true)]
+    [MaxLength(128)]
+    public string? NodeName { get; set; }
+
+    /// <summary>
+    /// 节点类型(如 bpmn:userTask / bpmn:exclusiveGateway / bpmn:parallelGateway)
+    /// </summary>
+    [SugarColumn(ColumnDescription = "节点类型", Length = 64, IsNullable = true)]
+    [MaxLength(64)]
+    public string? NodeType { get; set; }
+
+    /// <summary>
+    /// 完成时间
+    /// </summary>
+    [SugarColumn(ColumnDescription = "完成时间")]
+    public DateTime CompletedTime { get; set; } = DateTime.Now;
+}

+ 67 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Entity/ApprovalFlowDelegate.cs

@@ -0,0 +1,67 @@
+namespace Admin.NET.Plugin.ApprovalFlow;
+
+/// <summary>
+/// 审批代理/委托表
+/// </summary>
+[SugarTable(null, "审批代理/委托表")]
+[SugarIndex("idx_flowdelegate_user", nameof(UserId), OrderByType.Asc)]
+public class ApprovalFlowDelegate : EntityBase
+{
+    /// <summary>
+    /// 委托人(本人)Id
+    /// </summary>
+    [SugarColumn(ColumnDescription = "委托人Id")]
+    public long UserId { get; set; }
+
+    /// <summary>
+    /// 委托人姓名(冗余)
+    /// </summary>
+    [SugarColumn(ColumnDescription = "委托人姓名", Length = 64, IsNullable = true)]
+    [MaxLength(64)]
+    public string? UserName { get; set; }
+
+    /// <summary>
+    /// 代理人 Id
+    /// </summary>
+    [SugarColumn(ColumnDescription = "代理人Id")]
+    public long DelegateUserId { get; set; }
+
+    /// <summary>
+    /// 代理人姓名(冗余)
+    /// </summary>
+    [SugarColumn(ColumnDescription = "代理人姓名", Length = 64, IsNullable = true)]
+    [MaxLength(64)]
+    public string? DelegateUserName { get; set; }
+
+    /// <summary>
+    /// 生效开始时间
+    /// </summary>
+    [SugarColumn(ColumnDescription = "开始时间")]
+    public DateTime StartTime { get; set; }
+
+    /// <summary>
+    /// 生效结束时间
+    /// </summary>
+    [SugarColumn(ColumnDescription = "结束时间")]
+    public DateTime EndTime { get; set; }
+
+    /// <summary>
+    /// 仅适用于指定业务类型(空=全部业务)
+    /// </summary>
+    [SugarColumn(ColumnDescription = "业务类型编码", Length = 64, IsNullable = true)]
+    [MaxLength(64)]
+    public string? BizType { get; set; }
+
+    /// <summary>
+    /// 备注
+    /// </summary>
+    [SugarColumn(ColumnDescription = "备注", Length = 256, IsNullable = true)]
+    [MaxLength(256)]
+    public string? Remark { get; set; }
+
+    /// <summary>
+    /// 是否启用
+    /// </summary>
+    [SugarColumn(ColumnDescription = "是否启用", DefaultValue = "1")]
+    public bool IsEnabled { get; set; } = true;
+}

+ 19 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Entity/ApprovalFlowTask.cs

@@ -77,4 +77,23 @@ public class ApprovalFlowTask : EntityBaseOrg
     /// </summary>
     [SugarColumn(ColumnDescription = "加签发起人Id", IsNullable = true)]
     public long? AddSignById { get; set; }
+
+    /// <summary>
+    /// 是否为代理人任务(委托产生的并行任务)
+    /// </summary>
+    [SugarColumn(ColumnDescription = "是否代理任务", DefaultValue = "0")]
+    public bool IsDelegate { get; set; }
+
+    /// <summary>
+    /// 被代理人(原审批人)Id — IsDelegate=true 时有值
+    /// </summary>
+    [SugarColumn(ColumnDescription = "被代理人Id", IsNullable = true)]
+    public long? DelegateForUserId { get; set; }
+
+    /// <summary>
+    /// 被代理人姓名(冗余)
+    /// </summary>
+    [SugarColumn(ColumnDescription = "被代理人姓名", Length = 64, IsNullable = true)]
+    [MaxLength(64)]
+    public string? DelegateForUserName { get; set; }
 }

+ 44 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Entity/ApprovalFlowVersion.cs

@@ -0,0 +1,44 @@
+namespace Admin.NET.Plugin.ApprovalFlow;
+
+/// <summary>
+/// 审批流程版本历史表
+/// </summary>
+[SugarTable(null, "审批流程版本历史表")]
+public class ApprovalFlowVersion : EntityBase
+{
+    /// <summary>
+    /// 关联流程 Id
+    /// </summary>
+    [SugarColumn(ColumnDescription = "关联流程Id")]
+    public long FlowId { get; set; }
+
+    /// <summary>
+    /// 版本号
+    /// </summary>
+    [SugarColumn(ColumnDescription = "版本号")]
+    public int Version { get; set; }
+
+    /// <summary>
+    /// 流程 JSON 快照
+    /// </summary>
+    [SugarColumn(ColumnDescription = "流程JSON快照", ColumnDataType = StaticConfig.CodeFirst_BigString)]
+    public string? FlowJson { get; set; }
+
+    /// <summary>
+    /// 表单 JSON 快照
+    /// </summary>
+    [SugarColumn(ColumnDescription = "表单JSON快照", ColumnDataType = StaticConfig.CodeFirst_BigString)]
+    public string? FormJson { get; set; }
+
+    /// <summary>
+    /// 发布时间
+    /// </summary>
+    [SugarColumn(ColumnDescription = "发布时间")]
+    public DateTime PublishTime { get; set; }
+
+    /// <summary>
+    /// 发布人
+    /// </summary>
+    [SugarColumn(ColumnDescription = "发布人", Length = 64, IsNullable = true)]
+    public string? PublisherName { get; set; }
+}

+ 3 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Enum/ApproverTypeEnum.cs

@@ -17,4 +17,7 @@ public enum ApproverTypeEnum
 
     [Description("发起人")]
     Initiator = 4,
+
+    [Description("部门负责人")]
+    DepartmentLeader = 5,
 }

+ 3 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Enum/FlowLogActionEnum.cs

@@ -35,4 +35,7 @@ public enum FlowLogActionEnum
 
     [Description("超时自动")]
     AutoTimeout = 10,
+
+    [Description("升级")]
+    Escalate = 11,
 }

+ 3 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Enum/FlowTaskStatusEnum.cs

@@ -23,4 +23,7 @@ public enum FlowTaskStatusEnum
 
     [Description("已退回")]
     Returned = 5,
+
+    [Description("已升级")]
+    Escalated = 6,
 }

+ 86 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Job/FlowTimeoutJob.cs

@@ -0,0 +1,86 @@
+using System.Text.Json;
+using Admin.NET.Plugin.ApprovalFlow.Service;
+using Furion.Schedule;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Admin.NET.Plugin.ApprovalFlow;
+
+/// <summary>
+/// 审批超时自动处理作业 — 每 5 分钟扫描一次超时的待办任务
+/// </summary>
+[JobDetail("job_flow_timeout", Description = "审批超时自动处理",
+    GroupName = "default", Concurrent = false)]
+[Period(300000, TriggerId = "trigger_flow_timeout", Description = "每5分钟执行")]
+public class FlowTimeoutJob : IJob
+{
+    private readonly IServiceScopeFactory _scopeFactory;
+    private readonly ILogger _logger;
+
+    public FlowTimeoutJob(IServiceScopeFactory scopeFactory, ILoggerFactory loggerFactory)
+    {
+        _scopeFactory = scopeFactory;
+        _logger = loggerFactory.CreateLogger("FlowTimeoutJob");
+    }
+
+    public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
+    {
+        using var scope = _scopeFactory.CreateScope();
+        var db = scope.ServiceProvider.GetRequiredService<ISqlSugarClient>().CopyNew();
+        var engine = scope.ServiceProvider.GetRequiredService<FlowEngineService>();
+
+        var pendingTasks = await db.Queryable<ApprovalFlowTask>()
+            .InnerJoin<ApprovalFlowInstance>((t, i) => t.InstanceId == i.Id)
+            .Where((t, i) => t.Status == FlowTaskStatusEnum.Pending
+                          && i.Status == FlowInstanceStatusEnum.Running)
+            .Select((t, i) => new
+            {
+                TaskId = t.Id,
+                TaskCreatedAt = t.CreateTime,
+                NodeId = t.NodeId,
+                FlowJsonSnapshot = i.FlowJsonSnapshot,
+            })
+            .ToListAsync();
+
+        var now = DateTime.Now;
+        var processed = 0;
+
+        foreach (var item in pendingTasks)
+        {
+            if (stoppingToken.IsCancellationRequested) break;
+
+            try
+            {
+                if (string.IsNullOrWhiteSpace(item.FlowJsonSnapshot)) continue;
+
+                var flowData = JsonSerializer.Deserialize<ApprovalFlowItem>(item.FlowJsonSnapshot);
+                var node = flowData?.Nodes?.FirstOrDefault(n => n.Id == item.NodeId);
+                var props = node?.Properties;
+
+                if (props?.TimeoutHours == null || props.TimeoutHours <= 0) continue;
+                if (string.IsNullOrWhiteSpace(props.TimeoutAction)) continue;
+
+                var deadline = item.TaskCreatedAt.AddHours(props.TimeoutHours.Value);
+                if (now < deadline) continue;
+
+                if (props.TimeoutAction == "Notify")
+                {
+                    var alreadyNotified = await db.Queryable<ApprovalFlowLog>()
+                        .AnyAsync(log => log.TaskId == item.TaskId
+                                      && log.Action == FlowLogActionEnum.AutoTimeout);
+                    if (alreadyNotified) continue;
+                }
+
+                await engine.HandleTimeoutTask(item.TaskId);
+                processed++;
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "处理超时任务 {TaskId} 失败", item.TaskId);
+            }
+        }
+
+        if (processed > 0)
+            _logger.LogInformation("FlowTimeoutJob 本轮处理了 {Count} 个超时任务", processed);
+    }
+}

+ 1 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/SeedData/SysMenuSeedData.cs

@@ -23,6 +23,7 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
             new SysMenu{ Id=1310300010103, Pid=1310300010100, Title="业务类型", Path="/aidop/flowManage/approvalBizType", Name="approvalBizType", Component="/approvalFlow/bizType/index", Icon="ele-Collection", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2026-04-15 00:00:00"), OrderNo=100 },
             new SysMenu{ Id=1310300010101, Pid=1310300010100, Title="审批流程", Path="/aidop/flowManage/approvalFlow", Name="approvalFlow", Component="/approvalFlow/index", Icon="ele-Help", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=110 },
             new SysMenu{ Id=1310300010102, Pid=1310300010100, Title="审批中心", Path="/aidop/flowManage/approvalFlowCenter", Name="approvalFlowCenter", Component="/approvalFlow/center/index", Icon="ele-Checked", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2026-04-15 00:00:00"), OrderNo=120 },
+            new SysMenu{ Id=1310300010104, Pid=1310300010100, Title="审批代理", Path="/aidop/flowManage/flowDelegate", Name="flowDelegate", Component="/approvalFlow/delegate/index", Icon="ele-Switch", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2026-04-16 00:00:00"), OrderNo=130 },
         };
     }
 }

+ 72 - 1
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/ApprovalFlow/ApprovalFlowService.cs

@@ -15,10 +15,17 @@ namespace Admin.NET.Plugin.ApprovalFlow.Service;
 public class ApprovalFlowService : IDynamicApiController, ITransient
 {
     private readonly SqlSugarRepository<ApprovalFlow> _approvalFlowRep;
+    private readonly SqlSugarRepository<ApprovalFlowVersion> _versionRep;
+    private readonly UserManager _userManager;
 
-    public ApprovalFlowService(SqlSugarRepository<ApprovalFlow> approvalFlowRep)
+    public ApprovalFlowService(
+        SqlSugarRepository<ApprovalFlow> approvalFlowRep,
+        SqlSugarRepository<ApprovalFlowVersion> versionRep,
+        UserManager userManager)
     {
         _approvalFlowRep = approvalFlowRep;
+        _versionRep = versionRep;
+        _userManager = userManager;
     }
 
     /// <summary>
@@ -140,6 +147,70 @@ public class ApprovalFlowService : IDynamicApiController, ITransient
         await _approvalFlowRep.AsUpdateable(entity)
             .UpdateColumns(u => new { u.Version, u.IsPublished })
             .ExecuteCommandAsync();
+
+        await _versionRep.InsertAsync(new ApprovalFlowVersion
+        {
+            FlowId = entity.Id,
+            Version = entity.Version,
+            FlowJson = entity.FlowJson,
+            FormJson = entity.FormJson,
+            PublishTime = DateTime.Now,
+            PublisherName = _userManager.RealName,
+        });
+    }
+
+    /// <summary>
+    /// 获取流程版本历史
+    /// </summary>
+    [HttpGet]
+    [ApiDescriptionSettings(Name = "VersionHistory")]
+    [DisplayName("获取版本历史")]
+    public async Task<List<FlowVersionOutput>> VersionHistory([FromQuery] long flowId)
+    {
+        return await _versionRep.AsQueryable()
+            .Where(v => v.FlowId == flowId)
+            .OrderByDescending(v => v.Version)
+            .Select(v => new FlowVersionOutput
+            {
+                Id = v.Id,
+                Version = v.Version,
+                PublishTime = v.PublishTime,
+                PublisherName = v.PublisherName,
+            })
+            .ToListAsync();
+    }
+
+    /// <summary>
+    /// 获取指定版本详情(含 FlowJson)
+    /// </summary>
+    [HttpGet]
+    [ApiDescriptionSettings(Name = "VersionDetail")]
+    [DisplayName("获取版本详情")]
+    public async Task<ApprovalFlowVersion?> VersionDetail([FromQuery] long versionId)
+    {
+        return await _versionRep.GetByIdAsync(versionId);
+    }
+
+    /// <summary>
+    /// 回退到指定版本(将该版本的 FlowJson/FormJson 复制回主记录)
+    /// </summary>
+    [HttpPost]
+    [ApiDescriptionSettings(Name = "RevertVersion")]
+    [DisplayName("回退版本")]
+    public async Task RevertVersion([FromBody] BaseIdInput input)
+    {
+        var version = await _versionRep.GetByIdAsync(input.Id)
+            ?? throw Oops.Oh("版本记录不存在");
+
+        var flow = await _approvalFlowRep.GetByIdAsync(version.FlowId)
+            ?? throw Oops.Oh("流程不存在");
+
+        flow.FlowJson = version.FlowJson;
+        flow.FormJson = version.FormJson;
+        flow.IsPublished = false;
+        await _approvalFlowRep.AsUpdateable(flow)
+            .UpdateColumns(u => new { u.FlowJson, u.FormJson, u.IsPublished })
+            .ExecuteCommandAsync();
     }
 
     /// <summary>

+ 32 - 2
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/ApprovalFlow/Dto/ApprovalFlowItem.cs

@@ -112,15 +112,45 @@ public class FlowProperties
 
     [JsonPropertyName("timeoutHours")]
     [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
-    public int? TimeoutHours { get; set; }
+    public double? TimeoutHours { get; set; }
 
     /// <summary>
-    /// 超时动作:AutoApprove / AutoReject / Notify
+    /// 超时动作:AutoApprove / AutoReject / Notify / AutoEscalate
     /// </summary>
     [JsonPropertyName("timeoutAction")]
     [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
     public string? TimeoutAction { get; set; }
 
+    // ── 升级配置 ──
+
+    /// <summary>
+    /// 升级审批人类型:SpecificUser / Role / Department
+    /// </summary>
+    [JsonPropertyName("escalationApproverType")]
+    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+    public string? EscalationApproverType { get; set; }
+
+    /// <summary>
+    /// 升级审批人/角色/部门 Id,逗号分隔
+    /// </summary>
+    [JsonPropertyName("escalationApproverIds")]
+    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+    public string? EscalationApproverIds { get; set; }
+
+    /// <summary>
+    /// 升级审批人/角色/部门名称,逗号分隔(冗余,便于前端展示)
+    /// </summary>
+    [JsonPropertyName("escalationApproverNames")]
+    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+    public string? EscalationApproverNames { get; set; }
+
+    /// <summary>
+    /// 是否允许手动升级(true 时审批面板显示"升级"按钮)
+    /// </summary>
+    [JsonPropertyName("enableManualEscalation")]
+    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+    public bool? EnableManualEscalation { get; set; }
+
     // ── 网关节点属性 ──
 
     [JsonPropertyName("conditions")]

+ 11 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/ApprovalFlow/Dto/ApprovalFlowOutput.cs

@@ -100,4 +100,15 @@ public class ApprovalFlowOutput
     /// 软删除
     /// </summary>
     public bool IsDelete { get; set; }
+}
+
+/// <summary>
+/// 流程版本历史输出
+/// </summary>
+public class FlowVersionOutput
+{
+    public long Id { get; set; }
+    public int Version { get; set; }
+    public DateTime PublishTime { get; set; }
+    public string? PublisherName { get; set; }
 }

+ 129 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/FlowCommentTemplate/FlowCommentTemplateService.cs

@@ -0,0 +1,129 @@
+namespace Admin.NET.Plugin.ApprovalFlow.Service;
+
+/// <summary>
+/// 审批意见模板服务
+/// </summary>
+[ApiDescriptionSettings(ApprovalFlowConst.GroupName, Order = 60)]
+public class FlowCommentTemplateService : IDynamicApiController, ITransient
+{
+    private readonly SqlSugarRepository<ApprovalFlowCommentTemplate> _rep;
+    private readonly UserManager _userManager;
+
+    public FlowCommentTemplateService(
+        SqlSugarRepository<ApprovalFlowCommentTemplate> rep,
+        UserManager userManager)
+    {
+        _rep = rep;
+        _userManager = userManager;
+    }
+
+    /// <summary>
+    /// 获取当前用户可用的意见模板列表(公共 + 个人)
+    /// </summary>
+    [HttpGet]
+    [ApiDescriptionSettings(Name = "MyList")]
+    [DisplayName("获取意见模板列表")]
+    public async Task<List<CommentTemplateOutput>> MyList()
+    {
+        var userId = _userManager.UserId;
+        return await _rep.AsQueryable()
+            .Where(t => t.IsEnabled && (t.UserId == null || t.UserId == userId))
+            .OrderBy(t => t.OrderNo)
+            .Select(t => new CommentTemplateOutput
+            {
+                Id = t.Id,
+                Content = t.Content,
+                IsSystem = t.IsSystem,
+                IsMine = t.UserId != null && t.UserId == userId,
+            })
+            .ToListAsync();
+    }
+
+    /// <summary>
+    /// 新增个人意见模板
+    /// </summary>
+    [HttpPost]
+    [ApiDescriptionSettings(Name = "Add")]
+    [DisplayName("新增意见模板")]
+    public async Task<long> Add(CommentTemplateAddInput input)
+    {
+        var entity = new ApprovalFlowCommentTemplate
+        {
+            Content = input.Content,
+            OrderNo = input.OrderNo ?? 100,
+            IsSystem = false,
+            UserId = _userManager.UserId,
+            IsEnabled = true,
+        };
+        await _rep.InsertAsync(entity);
+        return entity.Id;
+    }
+
+    /// <summary>
+    /// 删除个人意见模板(系统预置不可删)
+    /// </summary>
+    [HttpPost]
+    [ApiDescriptionSettings(Name = "Delete")]
+    [DisplayName("删除意见模板")]
+    public async Task Delete(BaseIdInput input)
+    {
+        var entity = await _rep.GetByIdAsync(input.Id)
+            ?? throw Oops.Oh("模板不存在");
+        if (entity.IsSystem)
+            throw Oops.Oh("系统预置模板不可删除");
+        if (entity.UserId != _userManager.UserId)
+            throw Oops.Oh("只能删除自己的模板");
+        await _rep.DeleteByIdAsync(input.Id);
+    }
+
+    /// <summary>
+    /// 初始化系统预置意见模板(幂等)
+    /// </summary>
+    [NonAction]
+    public async Task EnsureSystemTemplates()
+    {
+        var exists = await _rep.AsQueryable().AnyAsync(t => t.IsSystem);
+        if (exists) return;
+
+        var templates = new[]
+        {
+            "同意",
+            "已审核,无问题",
+            "情况属实,同意处理",
+            "不同意,请补充材料后重新提交",
+            "退回修改",
+            "已阅,请按流程继续推进",
+            "金额/数量有误,请核实后重新提交",
+            "紧急事项,加急处理",
+        };
+
+        var order = 10;
+        foreach (var content in templates)
+        {
+            await _rep.InsertAsync(new ApprovalFlowCommentTemplate
+            {
+                Content = content,
+                OrderNo = order,
+                IsSystem = true,
+                UserId = null,
+                IsEnabled = true,
+            });
+            order += 10;
+        }
+    }
+}
+
+public class CommentTemplateOutput
+{
+    public long Id { get; set; }
+    public string Content { get; set; } = "";
+    public bool IsSystem { get; set; }
+    public bool IsMine { get; set; }
+}
+
+public class CommentTemplateAddInput
+{
+    [Required, MaxLength(512)]
+    public string Content { get; set; } = "";
+    public int? OrderNo { get; set; }
+}

+ 169 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/FlowDelegate/FlowDelegateService.cs

@@ -0,0 +1,169 @@
+namespace Admin.NET.Plugin.ApprovalFlow.Service;
+
+/// <summary>
+/// 审批代理/委托服务
+/// </summary>
+[ApiDescriptionSettings(ApprovalFlowConst.GroupName, Order = 55)]
+public class FlowDelegateService : IDynamicApiController, ITransient
+{
+    private readonly SqlSugarRepository<ApprovalFlowDelegate> _rep;
+    private readonly SqlSugarRepository<SysUser> _userRep;
+    private readonly UserManager _userManager;
+
+    public FlowDelegateService(
+        SqlSugarRepository<ApprovalFlowDelegate> rep,
+        SqlSugarRepository<SysUser> userRep,
+        UserManager userManager)
+    {
+        _rep = rep;
+        _userRep = userRep;
+        _userManager = userManager;
+    }
+
+    /// <summary>
+    /// 获取当前用户已设置的代理列表
+    /// </summary>
+    [HttpGet]
+    [ApiDescriptionSettings(Name = "MyList")]
+    [DisplayName("获取我的代理列表")]
+    public async Task<List<FlowDelegateOutput>> MyList()
+    {
+        var uid = _userManager.UserId;
+        var list = await _rep.AsQueryable()
+            .Where(d => d.UserId == uid)
+            .OrderByDescending(d => d.CreateTime)
+            .ToListAsync();
+
+        var now = DateTime.Now;
+        return list.Select(d => new FlowDelegateOutput
+        {
+            Id = d.Id,
+            DelegateUserId = d.DelegateUserId,
+            DelegateUserName = d.DelegateUserName,
+            StartTime = d.StartTime,
+            EndTime = d.EndTime,
+            BizType = d.BizType,
+            Remark = d.Remark,
+            IsEnabled = d.IsEnabled,
+            IsEffective = d.IsEnabled && now >= d.StartTime && now <= d.EndTime,
+        }).ToList();
+    }
+
+    /// <summary>
+    /// 新增代理
+    /// </summary>
+    [HttpPost]
+    [ApiDescriptionSettings(Name = "Add")]
+    [DisplayName("新增审批代理")]
+    public async Task<long> Add(FlowDelegateAddInput input)
+    {
+        if (input.DelegateUserId <= 0)
+            throw Oops.Oh("代理人不能为空");
+        if (input.DelegateUserId == _userManager.UserId)
+            throw Oops.Oh("代理人不能是自己");
+        if (input.EndTime <= input.StartTime)
+            throw Oops.Oh("结束时间必须晚于开始时间");
+
+        var delegateUser = await _userRep.GetByIdAsync(input.DelegateUserId)
+            ?? throw Oops.Oh("代理人不存在");
+
+        var entity = new ApprovalFlowDelegate
+        {
+            UserId = _userManager.UserId,
+            UserName = _userManager.RealName,
+            DelegateUserId = input.DelegateUserId,
+            DelegateUserName = delegateUser.RealName,
+            StartTime = input.StartTime,
+            EndTime = input.EndTime,
+            BizType = input.BizType,
+            Remark = input.Remark,
+            IsEnabled = true,
+        };
+        await _rep.InsertAsync(entity);
+        return entity.Id;
+    }
+
+    /// <summary>
+    /// 更新代理
+    /// </summary>
+    [HttpPost]
+    [ApiDescriptionSettings(Name = "Update")]
+    [DisplayName("更新审批代理")]
+    public async Task Update(FlowDelegateUpdateInput input)
+    {
+        var entity = await _rep.GetByIdAsync(input.Id)
+            ?? throw Oops.Oh("代理不存在");
+        if (entity.UserId != _userManager.UserId)
+            throw Oops.Oh("只能修改自己的代理");
+        if (input.EndTime <= input.StartTime)
+            throw Oops.Oh("结束时间必须晚于开始时间");
+
+        entity.StartTime = input.StartTime;
+        entity.EndTime = input.EndTime;
+        entity.BizType = input.BizType;
+        entity.Remark = input.Remark;
+        entity.IsEnabled = input.IsEnabled;
+
+        if (input.DelegateUserId > 0 && input.DelegateUserId != entity.DelegateUserId)
+        {
+            if (input.DelegateUserId == _userManager.UserId)
+                throw Oops.Oh("代理人不能是自己");
+            var delegateUser = await _userRep.GetByIdAsync(input.DelegateUserId)
+                ?? throw Oops.Oh("代理人不存在");
+            entity.DelegateUserId = input.DelegateUserId;
+            entity.DelegateUserName = delegateUser.RealName;
+        }
+
+        await _rep.AsUpdateable(entity).ExecuteCommandAsync();
+    }
+
+    /// <summary>
+    /// 删除代理
+    /// </summary>
+    [HttpPost]
+    [ApiDescriptionSettings(Name = "Delete")]
+    [DisplayName("删除审批代理")]
+    public async Task Delete(BaseIdInput input)
+    {
+        var entity = await _rep.GetByIdAsync(input.Id)
+            ?? throw Oops.Oh("代理不存在");
+        if (entity.UserId != _userManager.UserId)
+            throw Oops.Oh("只能删除自己的代理");
+        await _rep.DeleteByIdAsync(input.Id);
+    }
+}
+
+public class FlowDelegateOutput
+{
+    public long Id { get; set; }
+    public long DelegateUserId { get; set; }
+    public string? DelegateUserName { get; set; }
+    public DateTime StartTime { get; set; }
+    public DateTime EndTime { get; set; }
+    public string? BizType { get; set; }
+    public string? Remark { get; set; }
+    public bool IsEnabled { get; set; }
+    /// <summary>当前是否处于生效状态(启用 + 在时间窗口内)</summary>
+    public bool IsEffective { get; set; }
+}
+
+public class FlowDelegateAddInput
+{
+    [Required(ErrorMessage = "代理人不能为空")]
+    public long DelegateUserId { get; set; }
+    [Required(ErrorMessage = "开始时间不能为空")]
+    public DateTime StartTime { get; set; }
+    [Required(ErrorMessage = "结束时间不能为空")]
+    public DateTime EndTime { get; set; }
+    [MaxLength(64)]
+    public string? BizType { get; set; }
+    [MaxLength(256)]
+    public string? Remark { get; set; }
+}
+
+public class FlowDelegateUpdateInput : FlowDelegateAddInput
+{
+    [Required]
+    public long Id { get; set; }
+    public bool IsEnabled { get; set; } = true;
+}

+ 33 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/FlowEngine/Dto/FlowEngineDtos.cs

@@ -102,3 +102,36 @@ public class TaskUrgeInput
     [Required(ErrorMessage = "实例Id不能为空")]
     public long InstanceId { get; set; }
 }
+
+/// <summary>
+/// 批量审批输入
+/// </summary>
+public class BatchActionInput
+{
+    /// <summary>
+    /// 任务 Id 列表
+    /// </summary>
+    [Required(ErrorMessage = "任务Id列表不能为空")]
+    public List<long> TaskIds { get; set; } = new();
+
+    /// <summary>
+    /// 审批意见
+    /// </summary>
+    public string? Comment { get; set; }
+}
+
+/// <summary>
+/// 批量审批结果
+/// </summary>
+public class BatchActionResult
+{
+    public int SuccessCount { get; set; }
+    public int FailCount { get; set; }
+    public List<BatchActionFailItem> FailItems { get; set; } = new();
+}
+
+public class BatchActionFailItem
+{
+    public long TaskId { get; set; }
+    public string Error { get; set; } = "";
+}

+ 451 - 12
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/FlowEngine/FlowEngineService.cs

@@ -15,6 +15,9 @@ public class FlowEngineService : ITransient
     private readonly SqlSugarRepository<ApprovalFlowLog> _logRep;
     private readonly SqlSugarRepository<SysUserRole> _userRoleRep;
     private readonly SqlSugarRepository<SysUser> _userRep;
+    private readonly SqlSugarRepository<SysOrg> _orgRep;
+    private readonly SqlSugarRepository<ApprovalFlowDelegate> _delegateRep;
+    private readonly SqlSugarRepository<ApprovalFlowCompletedNode> _completedNodeRep;
     private readonly UserManager _userManager;
     private readonly SysOrgService _sysOrgService;
     private readonly FlowNotifyService _notifyService;
@@ -26,6 +29,9 @@ public class FlowEngineService : ITransient
         SqlSugarRepository<ApprovalFlowLog> logRep,
         SqlSugarRepository<SysUserRole> userRoleRep,
         SqlSugarRepository<SysUser> userRep,
+        SqlSugarRepository<SysOrg> orgRep,
+        SqlSugarRepository<ApprovalFlowDelegate> delegateRep,
+        SqlSugarRepository<ApprovalFlowCompletedNode> completedNodeRep,
         UserManager userManager,
         SysOrgService sysOrgService,
         FlowNotifyService notifyService)
@@ -36,6 +42,9 @@ public class FlowEngineService : ITransient
         _logRep = logRep;
         _userRoleRep = userRoleRep;
         _userRep = userRep;
+        _orgRep = orgRep;
+        _delegateRep = delegateRep;
+        _completedNodeRep = completedNodeRep;
         _userManager = userManager;
         _sysOrgService = sysOrgService;
         _notifyService = notifyService;
@@ -93,14 +102,19 @@ public class FlowEngineService : ITransient
             n.Type is "bpmn:startEvent" or "start-node")
             ?? throw Oops.Oh("流程图中未找到开始节点");
 
-        var firstTaskNodeId = FindNextNodeId(flowData, startNode.Id);
-        instance.CurrentNodeId = firstTaskNodeId;
-
         await _instanceRep.InsertAsync(instance);
 
         await WriteLog(instance.Id, null, startNode.Id, FlowLogActionEnum.Submit, input.Comment);
 
-        await CreateTasksForNode(instance, flowData, firstTaskNodeId);
+        // 开始节点登记为已完成,随后沿出边推进(兼容首节点即为并行网关 Fork 的拓扑)
+        await MarkNodeCompleted(instance.Id, startNode);
+        var firstOutgoing = flowData.Edges.Where(e => e.SourceNodeId == startNode.Id).Select(e => e.TargetNodeId).ToList();
+        if (firstOutgoing.Count == 0)
+            throw Oops.Oh("流程图开始节点未连接任何后继节点");
+        foreach (var target in firstOutgoing)
+        {
+            await ProcessNextNode(instance, flowData, target);
+        }
 
         await InvokeHandler(input.BizType, h => h.OnFlowStarted(input.BizId, instance.Id));
 
@@ -118,6 +132,9 @@ public class FlowEngineService : ITransient
         task.ActionTime = DateTime.Now;
         await _taskRep.AsUpdateable(task).ExecuteCommandAsync();
 
+        // P1-6 审批代理:同步取消配对的本人/代理任务
+        await CancelPairedDelegateTask(task);
+
         await WriteLog(task.InstanceId, taskId, task.NodeId, FlowLogActionEnum.Approve, comment);
 
         var instance = await _instanceRep.GetByIdAsync(task.InstanceId)
@@ -144,7 +161,8 @@ public class FlowEngineService : ITransient
         task.ActionTime = DateTime.Now;
         await _taskRep.AsUpdateable(task).ExecuteCommandAsync();
 
-        await CancelPendingTasks(task.InstanceId, task.NodeId, task.Id);
+        // Reject 导致流程终止:取消整个实例所有剩余 Pending 任务(含并行分支、配对代理任务等)
+        await CancelAllPendingTasks(task.InstanceId, task.Id);
 
         var instance = await _instanceRep.GetByIdAsync(task.InstanceId)
             ?? throw Oops.Oh("流程实例不存在");
@@ -302,6 +320,65 @@ public class FlowEngineService : ITransient
         await _notifyService.NotifyAddSign(targetUserId, task.InstanceId, instance?.Title ?? "", _userManager.RealName);
     }
 
+    /// <summary>
+    /// 手动升级 — 当前审批人主动将任务升级到更高层级
+    /// </summary>
+    public async Task Escalate(long taskId, string? comment)
+    {
+        var task = await GetPendingTask(taskId);
+        var instance = await _instanceRep.GetByIdAsync(task.InstanceId)
+            ?? throw Oops.Oh("流程实例不存在");
+
+        var flowData = DeserializeFlowJson(instance.FlowJsonSnapshot);
+        var node = flowData.Nodes.FirstOrDefault(n => n.Id == task.NodeId)
+            ?? throw Oops.Oh("节点不存在");
+
+        var props = node.Properties;
+        if (props?.EnableManualEscalation != true
+            || string.IsNullOrWhiteSpace(props.EscalationApproverType)
+            || string.IsNullOrWhiteSpace(props.EscalationApproverIds))
+            throw Oops.Oh("该节点未配置升级目标,无法升级");
+
+        task.Status = FlowTaskStatusEnum.Escalated;
+        task.Comment = comment;
+        task.ActionTime = DateTime.Now;
+        await _taskRep.AsUpdateable(task).ExecuteCommandAsync();
+
+        await CancelPendingTasks(task.InstanceId, task.NodeId, task.Id);
+
+        var escalationApprovers = await ResolveApprovers(
+            new FlowProperties
+            {
+                ApproverType = props.EscalationApproverType,
+                ApproverIds = props.EscalationApproverIds,
+                ApproverNames = props.EscalationApproverNames,
+            },
+            instance.InitiatorId);
+
+        if (escalationApprovers.Count == 0)
+            throw Oops.Oh("升级目标审批人列表为空");
+
+        var newTasks = escalationApprovers.Select(a => new ApprovalFlowTask
+        {
+            InstanceId = instance.Id,
+            NodeId = task.NodeId,
+            NodeName = task.NodeName,
+            AssigneeId = a.userId,
+            AssigneeName = a.userName,
+            Status = FlowTaskStatusEnum.Pending,
+            OrgId = instance.OrgId,
+        }).ToList();
+        await _taskRep.AsInsertable(newTasks).ExecuteCommandAsync();
+
+        var targetNames = string.Join(", ", escalationApprovers.Select(a => a.userName));
+        await WriteLog(instance.Id, taskId, task.NodeId, FlowLogActionEnum.Escalate,
+            $"{comment} → 升级给 {targetNames}");
+
+        var targetUserIds = escalationApprovers.Select(a => a.userId).Distinct().ToList();
+        await _notifyService.NotifyEscalated(targetUserIds, instance.Id, instance.Title,
+            _userManager.RealName, task.NodeName);
+    }
+
     /// <summary>
     /// 催办
     /// </summary>
@@ -321,19 +398,188 @@ public class FlowEngineService : ITransient
         await _notifyService.NotifyUrge(userIds, instanceId, instance.Title);
     }
 
+    // ═══════════════════════════════════════════
+    //  超时自动处理(由 FlowTimeoutJob 调用,无 UserManager 上下文)
+    // ═══════════════════════════════════════════
+
+    /// <summary>
+    /// 处理单个超时任务(由定时任务调用)
+    /// </summary>
+    public async Task HandleTimeoutTask(long taskId)
+    {
+        var task = await _taskRep.GetByIdAsync(taskId);
+        if (task == null || task.Status != FlowTaskStatusEnum.Pending) return;
+
+        var instance = await _instanceRep.GetByIdAsync(task.InstanceId);
+        if (instance == null || instance.Status != FlowInstanceStatusEnum.Running) return;
+
+        var flowData = DeserializeFlowJson(instance.FlowJsonSnapshot);
+        var node = flowData.Nodes.FirstOrDefault(n => n.Id == task.NodeId);
+        var props = node?.Properties;
+        if (props == null) return;
+
+        switch (props.TimeoutAction)
+        {
+            case "Notify":
+                await _notifyService.NotifyTimeout(
+                    new List<long> { task.AssigneeId }, instance.Id, instance.Title);
+                await WriteSystemLog(instance.Id, task.Id, task.NodeId,
+                    FlowLogActionEnum.AutoTimeout, "审批超时,已发送提醒通知");
+                break;
+            case "AutoApprove":
+                await AutoApproveTask(task, instance);
+                break;
+            case "AutoReject":
+                await AutoRejectTask(task, instance);
+                break;
+            case "AutoEscalate":
+                await AutoEscalateTask(task, props, instance);
+                break;
+        }
+    }
+
+    private async Task AutoApproveTask(ApprovalFlowTask task, ApprovalFlowInstance instance)
+    {
+        task.Status = FlowTaskStatusEnum.Approved;
+        task.Comment = "系统自动通过(超时)";
+        task.ActionTime = DateTime.Now;
+        await _taskRep.AsUpdateable(task).ExecuteCommandAsync();
+
+        await WriteSystemLog(instance.Id, task.Id, task.NodeId,
+            FlowLogActionEnum.AutoTimeout, "审批超时,系统自动通过");
+
+        if (await IsNodeCompleted(instance, task.NodeId))
+        {
+            await InvokeHandler(instance.BizType,
+                h => h.OnNodeCompleted(instance.BizId, task.NodeId, task.NodeName ?? ""));
+            var flowData = DeserializeFlowJson(instance.FlowJsonSnapshot);
+            await AdvanceToNext(instance, flowData, task.NodeId);
+        }
+    }
+
+    private async Task AutoRejectTask(ApprovalFlowTask task, ApprovalFlowInstance instance)
+    {
+        task.Status = FlowTaskStatusEnum.Rejected;
+        task.Comment = "系统自动拒绝(超时)";
+        task.ActionTime = DateTime.Now;
+        await _taskRep.AsUpdateable(task).ExecuteCommandAsync();
+
+        await CancelPendingTasks(task.InstanceId, task.NodeId, task.Id);
+
+        instance.Status = FlowInstanceStatusEnum.Rejected;
+        instance.EndTime = DateTime.Now;
+        await _instanceRep.AsUpdateable(instance).ExecuteCommandAsync();
+
+        await WriteSystemLog(instance.Id, task.Id, task.NodeId,
+            FlowLogActionEnum.AutoTimeout, "审批超时,系统自动拒绝");
+
+        await InvokeHandler(instance.BizType,
+            h => h.OnFlowCompleted(instance.BizId, FlowInstanceStatusEnum.Rejected));
+        await _notifyService.NotifyFlowCompleted(instance.InitiatorId, instance.Id,
+            instance.Title, FlowInstanceStatusEnum.Rejected);
+    }
+
+    private async Task AutoEscalateTask(ApprovalFlowTask task, FlowProperties nodeProps, ApprovalFlowInstance instance)
+    {
+        if (string.IsNullOrWhiteSpace(nodeProps.EscalationApproverType)
+            || string.IsNullOrWhiteSpace(nodeProps.EscalationApproverIds))
+            return;
+
+        task.Status = FlowTaskStatusEnum.Escalated;
+        task.Comment = "系统自动升级(超时)";
+        task.ActionTime = DateTime.Now;
+        await _taskRep.AsUpdateable(task).ExecuteCommandAsync();
+
+        await CancelPendingTasks(task.InstanceId, task.NodeId, task.Id);
+
+        var approvers = await ResolveApprovers(
+            new FlowProperties
+            {
+                ApproverType = nodeProps.EscalationApproverType,
+                ApproverIds = nodeProps.EscalationApproverIds,
+            },
+            instance.InitiatorId);
+
+        if (approvers.Count == 0) return;
+
+        var newTasks = approvers.Select(a => new ApprovalFlowTask
+        {
+            InstanceId = instance.Id,
+            NodeId = task.NodeId,
+            NodeName = task.NodeName,
+            AssigneeId = a.userId,
+            AssigneeName = a.userName,
+            Status = FlowTaskStatusEnum.Pending,
+            OrgId = instance.OrgId,
+        }).ToList();
+        await _taskRep.AsInsertable(newTasks).ExecuteCommandAsync();
+
+        var targetNames = string.Join(", ", approvers.Select(a => a.userName));
+        await WriteSystemLog(instance.Id, task.Id, task.NodeId,
+            FlowLogActionEnum.AutoTimeout, $"审批超时,自动升级给 {targetNames}");
+
+        var targetUserIds = approvers.Select(a => a.userId).Distinct().ToList();
+        await _notifyService.NotifyEscalated(targetUserIds, instance.Id, instance.Title,
+            "系统", task.NodeName);
+    }
+
+    private async Task WriteSystemLog(long instanceId, long? taskId, string? nodeId,
+        FlowLogActionEnum action, string? comment)
+    {
+        await _logRep.InsertAsync(new ApprovalFlowLog
+        {
+            InstanceId = instanceId,
+            TaskId = taskId,
+            NodeId = nodeId,
+            Action = action,
+            OperatorId = 0,
+            OperatorName = "系统",
+            Comment = comment,
+        });
+    }
+
     // ═══════════════════════════════════════════
     //  内部引擎方法
     // ═══════════════════════════════════════════
 
+    /// <summary>
+    /// 推进到下一节点。支持并行网关(Fork / Join)。
+    /// 行为约定:
+    /// - 进入本方法前,调用方(如 <see cref="Approve"/>)应已将 <paramref name="currentNodeId"/>(userTask)标记为完成节点;
+    ///   本方法会将途经的网关节点也写入 <see cref="ApprovalFlowCompletedNode"/>。
+    /// - 并行网关 Fork(出边&gt;=2):沿每条出边递归推进;
+    ///   并行网关 Join(入边&gt;=2):校验所有前驱节点是否都在"已完成"集合中,
+    ///   任一尚未完成则**静默等待**(不报错、不推进),由后续分支完成后再次触发 Join 校验。
+    /// </summary>
     private async Task AdvanceToNext(ApprovalFlowInstance instance, ApprovalFlowItem flowData, string currentNodeId)
     {
-        var nextNodeId = FindNextNodeId(flowData, currentNodeId);
-        if (nextNodeId == null)
+        // 当前节点可能是 userTask(完成记录由 Approve 写入)或网关(入口处已写入);此处统一确保幂等入库
+        var currentNode = flowData.Nodes.FirstOrDefault(n => n.Id == currentNodeId);
+        await MarkNodeCompleted(instance.Id, currentNode);
+
+        var outgoingEdges = flowData.Edges.Where(e => e.SourceNodeId == currentNodeId).ToList();
+        if (outgoingEdges.Count == 0)
         {
             await CompleteInstance(instance, FlowInstanceStatusEnum.Approved);
             return;
         }
 
+        // 非并行网关场景:当前节点一般只有 1 条出边
+        foreach (var edge in outgoingEdges)
+        {
+            await ProcessNextNode(instance, flowData, edge.TargetNodeId);
+        }
+    }
+
+    /// <summary>
+    /// 处理某个"下一节点"。根据节点类型分发:
+    /// - endEvent:所有分支任务都结束时触发实例完成
+    /// - exclusiveGateway:按条件选择分支
+    /// - parallelGateway:Fork 并行分发;Join 等待所有前驱完成
+    /// - userTask / 其他:创建任务
+    /// </summary>
+    private async Task ProcessNextNode(ApprovalFlowInstance instance, ApprovalFlowItem flowData, string nextNodeId)
+    {
         var nextNode = flowData.Nodes.FirstOrDefault(n => n.Id == nextNodeId);
         if (nextNode == null)
         {
@@ -343,27 +589,98 @@ public class FlowEngineService : ITransient
 
         if (nextNode.Type is "bpmn:endEvent" or "end-node")
         {
+            // 所有并行分支都已结束(无其它 Pending 任务)才真正完成实例
+            var hasOtherPending = await _taskRep.AsQueryable()
+                .AnyAsync(t => t.InstanceId == instance.Id && t.Status == FlowTaskStatusEnum.Pending);
+            if (hasOtherPending) return;
+
             await CompleteInstance(instance, FlowInstanceStatusEnum.Approved);
             return;
         }
 
         if (nextNode.Type is "bpmn:exclusiveGateway")
         {
+            await MarkNodeCompleted(instance.Id, nextNode);
             var bizData = await GetBizData(instance.BizType, instance.BizId);
             var targetNodeId = EvaluateGateway(nextNode.Properties?.Conditions, flowData, nextNode.Id, bizData);
             instance.CurrentNodeId = targetNodeId;
             await _instanceRep.AsUpdateable(instance).UpdateColumns(i => new { i.CurrentNodeId }).ExecuteCommandAsync();
-            await AdvanceToNext(instance, flowData, nextNode.Id);
+            await ProcessNextNode(instance, flowData, targetNodeId);
             return;
         }
 
+        if (nextNode.Type is "bpmn:parallelGateway")
+        {
+            var incoming = flowData.Edges.Where(e => e.TargetNodeId == nextNode.Id).Select(e => e.SourceNodeId).ToList();
+            var outgoing = flowData.Edges.Where(e => e.SourceNodeId == nextNode.Id).Select(e => e.TargetNodeId).ToList();
+
+            // Join 语义:入边 >= 2,需等所有前驱都已完成
+            if (incoming.Count >= 2)
+            {
+                var completedSet = await GetCompletedNodeIdSet(instance.Id);
+                if (!incoming.All(p => completedSet.Contains(p)))
+                {
+                    // 未汇合,静默等待后续分支抵达
+                    return;
+                }
+            }
+
+            // Fork 或 Join 通过:标记网关完成,沿所有出边推进
+            await MarkNodeCompleted(instance.Id, nextNode);
+            foreach (var target in outgoing)
+            {
+                await ProcessNextNode(instance, flowData, target);
+            }
+            return;
+        }
+
+        // userTask 或其他:创建任务
         instance.CurrentNodeId = nextNodeId;
         await _instanceRep.AsUpdateable(instance).UpdateColumns(i => new { i.CurrentNodeId }).ExecuteCommandAsync();
         await CreateTasksForNode(instance, flowData, nextNodeId);
     }
 
+    /// <summary>
+    /// 标记节点已完成(幂等:重复写入被唯一索引拦截后忽略)
+    /// </summary>
+    private async Task MarkNodeCompleted(long instanceId, ApprovalFlowNodeItem? node)
+    {
+        if (node == null) return;
+        try
+        {
+            await _completedNodeRep.InsertAsync(new ApprovalFlowCompletedNode
+            {
+                InstanceId = instanceId,
+                NodeId = node.Id,
+                NodeName = node.Properties?.NodeName ?? node.Text?.Value,
+                NodeType = node.Type,
+                CompletedTime = DateTime.Now,
+            });
+        }
+        catch
+        {
+            // 并发场景下可能触发唯一索引冲突,忽略(已存在即可)
+        }
+    }
+
+    /// <summary>
+    /// 查询实例已完成节点 Id 集合
+    /// </summary>
+    private async Task<HashSet<string>> GetCompletedNodeIdSet(long instanceId)
+    {
+        var ids = await _completedNodeRep.AsQueryable()
+            .Where(c => c.InstanceId == instanceId)
+            .Select(c => c.NodeId)
+            .ToListAsync();
+        return new HashSet<string>(ids);
+    }
+
     private async Task CompleteInstance(ApprovalFlowInstance instance, FlowInstanceStatusEnum status)
     {
+        // 幂等:并行分支同时到达 end 时避免重复完成
+        var latest = await _instanceRep.GetByIdAsync(instance.Id);
+        if (latest == null || latest.Status != FlowInstanceStatusEnum.Running) return;
+
         instance.Status = status;
         instance.EndTime = DateTime.Now;
         await _instanceRep.AsUpdateable(instance)
@@ -383,21 +700,101 @@ public class FlowEngineService : ITransient
         if (approvers.Count == 0)
             throw Oops.Oh($"节点 [{node.Properties?.NodeName ?? nodeId}] 未配置审批人或审批人列表为空");
 
+        var nodeName = node.Properties?.NodeName ?? node.Text?.Value;
         var tasks = approvers.Select(a => new ApprovalFlowTask
         {
             InstanceId = instance.Id,
             NodeId = nodeId,
-            NodeName = node.Properties?.NodeName ?? node.Text?.Value,
+            NodeName = nodeName,
             AssigneeId = a.userId,
             AssigneeName = a.userName,
             Status = FlowTaskStatusEnum.Pending,
             OrgId = instance.OrgId,
         }).ToList();
 
-        await _taskRep.AsInsertable(tasks).ExecuteCommandAsync();
+        // P1-6 审批代理:为每个原审批人检查是否存在有效代理,是则并行创建一条代理任务
+        var delegateTasks = new List<ApprovalFlowTask>();
+        foreach (var a in approvers)
+        {
+            var del = await FindEffectiveDelegate(a.userId, instance.BizType);
+            if (del == null) continue;
+            // 代理人和原审批人不能重复,代理人也不能是原审批人列表中其他人(避免同一人两条任务)
+            if (approvers.Any(x => x.userId == del.DelegateUserId)) continue;
+
+            delegateTasks.Add(new ApprovalFlowTask
+            {
+                InstanceId = instance.Id,
+                NodeId = nodeId,
+                NodeName = nodeName,
+                AssigneeId = del.DelegateUserId,
+                AssigneeName = del.DelegateUserName,
+                Status = FlowTaskStatusEnum.Pending,
+                OrgId = instance.OrgId,
+                IsDelegate = true,
+                DelegateForUserId = a.userId,
+                DelegateForUserName = a.userName,
+            });
+        }
+
+        var allTasks = tasks.Concat(delegateTasks).ToList();
+        await _taskRep.AsInsertable(allTasks).ExecuteCommandAsync();
+
+        var assigneeIds = allTasks.Select(t => t.AssigneeId).Distinct().ToList();
+        await _notifyService.NotifyNewTask(assigneeIds, instance.Id, instance.Title, nodeName);
+    }
+
+    /// <summary>
+    /// 查找指定用户当前生效的审批代理(时间窗口内 + 已启用 + BizType 匹配或全局)
+    /// </summary>
+    private async Task<ApprovalFlowDelegate?> FindEffectiveDelegate(long userId, string? bizType)
+    {
+        var now = DateTime.Now;
+        return await _delegateRep.AsQueryable()
+            .Where(d => d.UserId == userId
+                     && d.IsEnabled
+                     && d.StartTime <= now
+                     && d.EndTime >= now
+                     && (string.IsNullOrEmpty(d.BizType) || d.BizType == bizType))
+            .OrderBy(d => d.BizType == null ? 1 : 0) // 优先匹配指定 BizType 的代理
+            .OrderByDescending(d => d.CreateTime)
+            .FirstAsync();
+    }
+
+    /// <summary>
+    /// 取消与已完成任务配对的代理任务(P1-6)
+    /// - 本人任务完成:取消对应的代理任务
+    /// - 代理任务完成:取消对应的本人任务
+    /// </summary>
+    private async Task CancelPairedDelegateTask(ApprovalFlowTask completedTask)
+    {
+        ApprovalFlowTask? paired;
+        if (completedTask.IsDelegate)
+        {
+            var originalUserId = completedTask.DelegateForUserId ?? 0;
+            if (originalUserId == 0) return;
+            paired = await _taskRep.AsQueryable()
+                .Where(t => t.InstanceId == completedTask.InstanceId
+                         && t.NodeId == completedTask.NodeId
+                         && t.Status == FlowTaskStatusEnum.Pending
+                         && t.AssigneeId == originalUserId
+                         && !t.IsDelegate)
+                .FirstAsync();
+        }
+        else
+        {
+            paired = await _taskRep.AsQueryable()
+                .Where(t => t.InstanceId == completedTask.InstanceId
+                         && t.NodeId == completedTask.NodeId
+                         && t.Status == FlowTaskStatusEnum.Pending
+                         && t.IsDelegate
+                         && t.DelegateForUserId == completedTask.AssigneeId)
+                .FirstAsync();
+        }
 
-        var assigneeIds = tasks.Select(t => t.AssigneeId).Distinct().ToList();
-        await _notifyService.NotifyNewTask(assigneeIds, instance.Id, instance.Title, node.Properties?.NodeName ?? node.Text?.Value);
+        if (paired == null) return;
+        paired.Status = FlowTaskStatusEnum.Cancelled;
+        paired.ActionTime = DateTime.Now;
+        await _taskRep.AsUpdateable(paired).ExecuteCommandAsync();
     }
 
     private async Task<List<(long userId, string userName)>> ResolveApprovers(FlowProperties? props, long initiatorId)
@@ -446,6 +843,30 @@ public class FlowEngineService : ITransient
             return users.Select(u => (u.Id, u.RealName ?? "")).ToList();
         }
 
+        if (approverType == nameof(ApproverTypeEnum.DepartmentLeader))
+        {
+            var initiator = await _userRep.GetByIdAsync(initiatorId);
+            if (initiator == null || initiator.OrgId <= 0)
+                return new List<(long, string)>();
+
+            var org = await _orgRep.GetByIdAsync(initiator.OrgId);
+            if (org?.DirectorId != null && org.DirectorId > 0)
+            {
+                var director = await _userRep.GetByIdAsync(org.DirectorId.Value);
+                if (director != null)
+                    return new List<(long, string)> { (director.Id, director.RealName ?? "") };
+            }
+
+            if (initiator.ManagerUserId != null && initiator.ManagerUserId > 0)
+            {
+                var manager = await _userRep.GetByIdAsync(initiator.ManagerUserId.Value);
+                if (manager != null)
+                    return new List<(long, string)> { (manager.Id, manager.RealName ?? "") };
+            }
+
+            return new List<(long, string)>();
+        }
+
         return new List<(long, string)>();
     }
 
@@ -596,6 +1017,24 @@ public class FlowEngineService : ITransient
             await _taskRep.AsUpdateable(tasks).ExecuteCommandAsync();
     }
 
+    /// <summary>
+    /// 取消整个实例下所有剩余 Pending 任务(Reject 时跨并行分支使用)
+    /// </summary>
+    private async Task CancelAllPendingTasks(long instanceId, long? excludeTaskId = null)
+    {
+        var tasks = await _taskRep.AsQueryable()
+            .Where(t => t.InstanceId == instanceId && t.Status == FlowTaskStatusEnum.Pending)
+            .WhereIF(excludeTaskId.HasValue, t => t.Id != excludeTaskId!.Value)
+            .ToListAsync();
+        foreach (var t in tasks)
+        {
+            t.Status = FlowTaskStatusEnum.Cancelled;
+            t.ActionTime = DateTime.Now;
+        }
+        if (tasks.Count > 0)
+            await _taskRep.AsUpdateable(tasks).ExecuteCommandAsync();
+    }
+
     private async Task<ApprovalFlowTask> GetPendingTask(long taskId)
     {
         var task = await _taskRep.GetByIdAsync(taskId)

+ 21 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/FlowInstance/Dto/FlowInstanceDtos.cs

@@ -17,7 +17,19 @@ public class FlowInstanceDetailOutput
     public string? CurrentNodeId { get; set; }
     public DateTime StartTime { get; set; }
     public DateTime? EndTime { get; set; }
+    /// <summary>流程 JSON 快照(发起时冻结的版本),前端可用来渲染流程图</summary>
+    public string? FlowJsonSnapshot { get; set; }
     public List<FlowTaskOutput> Tasks { get; set; } = new();
+
+    /// <summary>
+    /// 已完成节点 Id 列表(来自 ApprovalFlowCompletedNode 子表,含 userTask 与网关节点)
+    /// </summary>
+    public List<string> CompletedNodeIds { get; set; } = new();
+
+    /// <summary>
+    /// 当前进行中的节点 Id 列表(Pending 任务 NodeId 去重;并行场景下会有多项)
+    /// </summary>
+    public List<string> CurrentNodeIds { get; set; } = new();
 }
 
 /// <summary>
@@ -33,6 +45,15 @@ public class FlowTaskOutput
     public FlowTaskStatusEnum Status { get; set; }
     public string? Comment { get; set; }
     public DateTime? ActionTime { get; set; }
+
+    /// <summary>是否为代理任务</summary>
+    public bool IsDelegate { get; set; }
+
+    /// <summary>被代理人(原审批人)Id</summary>
+    public long? DelegateForUserId { get; set; }
+
+    /// <summary>被代理人(原审批人)姓名</summary>
+    public string? DelegateForUserName { get; set; }
 }
 
 /// <summary>

+ 21 - 1
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/FlowInstance/FlowInstanceService.cs

@@ -10,17 +10,20 @@ public class FlowInstanceService : IDynamicApiController, ITransient
     private readonly SqlSugarRepository<ApprovalFlowInstance> _instanceRep;
     private readonly SqlSugarRepository<ApprovalFlowTask> _taskRep;
     private readonly SqlSugarRepository<ApprovalFlowLog> _logRep;
+    private readonly SqlSugarRepository<ApprovalFlowCompletedNode> _completedNodeRep;
 
     public FlowInstanceService(
         FlowEngineService engine,
         SqlSugarRepository<ApprovalFlowInstance> instanceRep,
         SqlSugarRepository<ApprovalFlowTask> taskRep,
-        SqlSugarRepository<ApprovalFlowLog> logRep)
+        SqlSugarRepository<ApprovalFlowLog> logRep,
+        SqlSugarRepository<ApprovalFlowCompletedNode> completedNodeRep)
     {
         _engine = engine;
         _instanceRep = instanceRep;
         _taskRep = taskRep;
         _logRep = logRep;
+        _completedNodeRep = completedNodeRep;
     }
 
     /// <summary>
@@ -50,6 +53,17 @@ public class FlowInstanceService : IDynamicApiController, ITransient
             .OrderBy(t => t.CreateTime)
             .ToListAsync();
 
+        var completedNodeIds = await _completedNodeRep.AsQueryable()
+            .Where(c => c.InstanceId == id)
+            .Select(c => c.NodeId)
+            .ToListAsync();
+
+        var currentNodeIds = tasks
+            .Where(t => t.Status == FlowTaskStatusEnum.Pending)
+            .Select(t => t.NodeId)
+            .Distinct()
+            .ToList();
+
         return new FlowInstanceDetailOutput
         {
             Id = instance.Id,
@@ -64,6 +78,7 @@ public class FlowInstanceService : IDynamicApiController, ITransient
             CurrentNodeId = instance.CurrentNodeId,
             StartTime = instance.StartTime,
             EndTime = instance.EndTime,
+            FlowJsonSnapshot = instance.FlowJsonSnapshot,
             Tasks = tasks.Select(t => new FlowTaskOutput
             {
                 Id = t.Id,
@@ -74,7 +89,12 @@ public class FlowInstanceService : IDynamicApiController, ITransient
                 Status = t.Status,
                 Comment = t.Comment,
                 ActionTime = t.ActionTime,
+                IsDelegate = t.IsDelegate,
+                DelegateForUserId = t.DelegateForUserId,
+                DelegateForUserName = t.DelegateForUserName,
             }).ToList(),
+            CompletedNodeIds = completedNodeIds,
+            CurrentNodeIds = currentNodeIds,
         };
     }
 

+ 30 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/FlowNotify/FlowNotifyService.cs

@@ -117,6 +117,34 @@ public class FlowNotifyService : ITransient
         });
     }
 
+    /// <summary>
+    /// 升级通知 — 通知升级目标人
+    /// </summary>
+    public async Task NotifyEscalated(List<long> targetUserIds, long instanceId, string title, string? fromName, string? nodeName)
+    {
+        await NotifyUsers(targetUserIds, new FlowNotification
+        {
+            Type = FlowNotificationTypeEnum.Escalated,
+            InstanceId = instanceId,
+            Title = $"【升级】{title}",
+            Content = $"{fromName} 将审批任务({nodeName})升级给您,请及时处理。",
+        });
+    }
+
+    /// <summary>
+    /// 超时提醒通知
+    /// </summary>
+    public async Task NotifyTimeout(List<long> userIds, long instanceId, string title)
+    {
+        await NotifyUsers(userIds, new FlowNotification
+        {
+            Type = FlowNotificationTypeEnum.Timeout,
+            InstanceId = instanceId,
+            Title = $"【超时提醒】{title}",
+            Content = "您有一条审批任务已超时,请尽快处理。",
+        });
+    }
+
     /// <summary>
     /// 撤回通知 — 通知被取消的审批人
     /// </summary>
@@ -199,6 +227,8 @@ public enum FlowNotificationTypeEnum
     Returned,
     AddSign,
     Withdrawn,
+    Escalated,
+    Timeout,
 }
 
 /// <summary>

+ 16 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/FlowTask/Dto/FlowTaskDtos.cs

@@ -29,4 +29,20 @@ public class TaskPageOutput
     public string? InitiatorName { get; set; }
     public DateTime StartTime { get; set; }
     public DateTime CreateTime { get; set; }
+
+    // P1-6 审批代理:是否为代理任务、被代理人
+    public bool IsDelegate { get; set; }
+    public string? DelegateForUserName { get; set; }
+}
+
+/// <summary>
+/// 升级配置查询输出
+/// </summary>
+public class EscalationConfigOutput
+{
+    /// <summary>是否允许升级(节点配置了升级目标且开启手动升级)</summary>
+    public bool Enabled { get; set; }
+
+    /// <summary>升级目标名称(便于前端展示)</summary>
+    public string? EscalationApproverNames { get; set; }
 }

+ 92 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/FlowTask/FlowTaskService.cs

@@ -53,6 +53,8 @@ public class FlowTaskService : IDynamicApiController, ITransient
                 InitiatorName = i.InitiatorName,
                 StartTime = i.StartTime,
                 CreateTime = t.CreateTime,
+                IsDelegate = t.IsDelegate,
+                DelegateForUserName = t.DelegateForUserName,
             })
             .ToPagedListAsync(input.Page, input.PageSize);
     }
@@ -87,6 +89,8 @@ public class FlowTaskService : IDynamicApiController, ITransient
                 InitiatorName = i.InitiatorName,
                 StartTime = i.StartTime,
                 CreateTime = t.CreateTime,
+                IsDelegate = t.IsDelegate,
+                DelegateForUserName = t.DelegateForUserName,
             })
             .ToPagedListAsync(input.Page, input.PageSize);
     }
@@ -221,6 +225,44 @@ public class FlowTaskService : IDynamicApiController, ITransient
         await _engine.AddSign(input.TaskId, input.TargetUserId, input.Comment);
     }
 
+    /// <summary>
+    /// 升级
+    /// </summary>
+    [HttpPost]
+    [ApiDescriptionSettings(Name = "Escalate")]
+    [DisplayName("升级")]
+    public async Task Escalate(TaskActionInput input)
+    {
+        await _engine.Escalate(input.TaskId, input.Comment);
+    }
+
+    /// <summary>
+    /// 获取指定任务所属节点的升级配置
+    /// </summary>
+    [HttpGet]
+    [ApiDescriptionSettings(Name = "GetEscalationConfig")]
+    [DisplayName("获取升级配置")]
+    public async Task<EscalationConfigOutput> GetEscalationConfig([FromQuery] long taskId)
+    {
+        var task = await _taskRep.GetByIdAsync(taskId);
+        if (task == null) return new EscalationConfigOutput { Enabled = false };
+
+        var instance = await _instanceRep.GetByIdAsync(task.InstanceId);
+        if (instance == null) return new EscalationConfigOutput { Enabled = false };
+
+        var flowData = System.Text.Json.JsonSerializer.Deserialize<ApprovalFlowItem>(instance.FlowJsonSnapshot ?? "{}");
+        var node = flowData?.Nodes?.FirstOrDefault(n => n.Id == task.NodeId);
+        var props = node?.Properties;
+
+        return new EscalationConfigOutput
+        {
+            Enabled = props?.EnableManualEscalation == true
+                      && !string.IsNullOrWhiteSpace(props?.EscalationApproverType)
+                      && !string.IsNullOrWhiteSpace(props?.EscalationApproverIds),
+            EscalationApproverNames = props?.EscalationApproverNames,
+        };
+    }
+
     /// <summary>
     /// 催办
     /// </summary>
@@ -231,4 +273,54 @@ public class FlowTaskService : IDynamicApiController, ITransient
     {
         await _engine.Urge(input.InstanceId);
     }
+
+    /// <summary>
+    /// 批量同意
+    /// </summary>
+    [HttpPost]
+    [ApiDescriptionSettings(Name = "BatchApprove")]
+    [DisplayName("批量同意")]
+    public async Task<BatchActionResult> BatchApprove(BatchActionInput input)
+    {
+        var result = new BatchActionResult();
+        foreach (var taskId in input.TaskIds)
+        {
+            try
+            {
+                await _engine.Approve(taskId, input.Comment);
+                result.SuccessCount++;
+            }
+            catch (Exception ex)
+            {
+                result.FailCount++;
+                result.FailItems.Add(new BatchActionFailItem { TaskId = taskId, Error = ex.Message });
+            }
+        }
+        return result;
+    }
+
+    /// <summary>
+    /// 批量拒绝
+    /// </summary>
+    [HttpPost]
+    [ApiDescriptionSettings(Name = "BatchReject")]
+    [DisplayName("批量拒绝")]
+    public async Task<BatchActionResult> BatchReject(BatchActionInput input)
+    {
+        var result = new BatchActionResult();
+        foreach (var taskId in input.TaskIds)
+        {
+            try
+            {
+                await _engine.Reject(taskId, input.Comment);
+                result.SuccessCount++;
+            }
+            catch (Exception ex)
+            {
+                result.FailCount++;
+                result.FailItems.Add(new BatchActionFailItem { TaskId = taskId, Error = ex.Message });
+            }
+        }
+        return result;
+    }
 }

+ 12 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Startup.cs

@@ -4,6 +4,7 @@
 //
 // 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
 
+using Admin.NET.Plugin.ApprovalFlow.Service;
 using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Hosting;
 
@@ -19,5 +20,16 @@ public class Startup : AppStartup
     public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
     {
         app.UseApprovalFlow();
+
+        using var scope = app.ApplicationServices.CreateScope();
+        var db = scope.ServiceProvider.GetRequiredService<ISqlSugarClient>();
+        db.CodeFirst.InitTables<ApprovalFlowCommentTemplate>();
+        db.CodeFirst.InitTables<ApprovalFlowVersion>();
+        db.CodeFirst.InitTables<ApprovalFlowDelegate>();
+        db.CodeFirst.InitTables<ApprovalFlowCompletedNode>();
+        db.CodeFirst.InitTables<ApprovalFlowTask>();
+
+        var templateService = scope.ServiceProvider.GetRequiredService<FlowCommentTemplateService>();
+        templateService.EnsureSystemTemplates().GetAwaiter().GetResult();
     }
 }