Sfoglia il codice sorgente

feat: approval flow engine + center + BizType + integration doc (Web 2.4.49 / API 1.0.13)

Made-with: Cursor
skygu 6 mesi fa
parent
commit
fef67b9a12
52 ha cambiato i file con 4315 aggiunte e 109 eliminazioni
  1. 21 1
      .gitattributes
  2. 1 1
      Web/package.json
  3. BIN
      Web/public/app-icons/aidop-logo.png
  4. 11 0
      Web/src/views/aidop/business/salesOrderForm.vue
  5. 99 0
      Web/src/views/approvalFlow/api.ts
  6. 189 0
      Web/src/views/approvalFlow/bizType/index.vue
  7. 86 0
      Web/src/views/approvalFlow/center/components/ApprovalDialog.vue
  8. 34 0
      Web/src/views/approvalFlow/center/components/DoneList.vue
  9. 45 0
      Web/src/views/approvalFlow/center/components/InitiatedList.vue
  10. 42 0
      Web/src/views/approvalFlow/center/components/PendingList.vue
  11. 40 0
      Web/src/views/approvalFlow/center/components/TimelineDialog.vue
  12. 236 0
      Web/src/views/approvalFlow/center/index.vue
  13. 327 0
      Web/src/views/approvalFlow/component/ApprovalPanel.vue
  14. 31 1
      Web/src/views/approvalFlow/component/LogicFlow/Panel/PanelControl.vue
  15. 329 19
      Web/src/views/approvalFlow/component/LogicFlow/Property/PropertyCommon.vue
  16. 11 9
      Web/src/views/approvalFlow/component/LogicFlow/Property/PropertyDialog.vue
  17. 21 2
      Web/src/views/approvalFlow/component/editDialog.vue
  18. 26 0
      Web/src/views/approvalFlow/index.vue
  19. 309 0
      doc/审批流集成开发指南.md
  20. 3 3
      server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj
  21. 1 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Admin.NET.Plugin.AiDOP.csproj
  22. 31 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Order/ContractReviewBizHandler.cs
  23. 72 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Order/OrderReviewBizHandler.cs
  24. 48 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Order/SeOrderService.cs
  25. 9 1
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Configuration/ApprovalFlow.json
  26. 36 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Entity/ApprovalBizType.cs
  27. 19 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Entity/ApprovalFlow.cs
  28. 93 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Entity/ApprovalFlowInstance.cs
  29. 54 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Entity/ApprovalFlowLog.cs
  30. 80 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Entity/ApprovalFlowTask.cs
  31. 20 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Enum/ApproverTypeEnum.cs
  32. 26 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Enum/FlowInstanceStatusEnum.cs
  33. 38 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Enum/FlowLogActionEnum.cs
  34. 26 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Enum/FlowTaskStatusEnum.cs
  35. 14 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Enum/MultiApproveModeEnum.cs
  36. 8 21
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Middleware/ApprovalFlowMiddleware.cs
  37. 4 1
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/SeedData/SysMenuSeedData.cs
  38. 50 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/ApprovalFlow/ApprovalFlowService.cs
  39. 5 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/ApprovalFlow/Dto/ApprovalFlowInput.cs
  40. 73 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/ApprovalFlow/Dto/ApprovalFlowItem.cs
  41. 15 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/ApprovalFlow/Dto/ApprovalFlowOutput.cs
  42. 145 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/BizType/BizTypeService.cs
  43. 86 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/BizType/Dto/BizTypeDtos.cs
  44. 104 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/FlowEngine/Dto/FlowEngineDtos.cs
  45. 617 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/FlowEngine/FlowEngineService.cs
  46. 35 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/FlowEngine/IFlowBizHandler.cs
  47. 82 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/FlowInstance/Dto/FlowInstanceDtos.cs
  48. 122 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/FlowInstance/FlowInstanceService.cs
  49. 214 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/FlowNotify/FlowNotifyService.cs
  50. 32 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/FlowTask/Dto/FlowTaskDtos.cs
  51. 234 0
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/FlowTask/FlowTaskService.cs
  52. 61 50
      server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/SysApproval/SysApprovalService.cs

+ 21 - 1
.gitattributes

@@ -1 +1,21 @@
-* text=auto eol=lf
+* text=auto eol=lf
+
+# Binary files — do not apply line-ending conversion
+*.png binary
+*.jpg binary
+*.jpeg binary
+*.gif binary
+*.ico binary
+*.bmp binary
+*.webp binary
+*.svg binary
+*.woff binary
+*.woff2 binary
+*.ttf binary
+*.eot binary
+*.zip binary
+*.gz binary
+*.tar binary
+*.dll binary
+*.exe binary
+*.pdf binary

+ 1 - 1
Web/package.json

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

BIN
Web/public/app-icons/aidop-logo.png


+ 11 - 0
Web/src/views/aidop/business/salesOrderForm.vue

@@ -118,6 +118,16 @@
 			</el-table>
 		</el-form>
 
+		<!-- 审批流程面板 -->
+		<ApprovalPanel
+			v-if="form.id"
+			bizType="ORDER_REVIEW"
+			:bizId="form.id"
+			:bizNo="form.billNo"
+			:title="`订单评审-${form.billNo}`"
+			@refresh="loadDetail"
+		/>
+
 		<div class="footer">
 			<el-button @click="$emit('cancel')">取消</el-button>
 			<el-button type="primary" :disabled="isView" :loading="saving" @click="onSave">保存</el-button>
@@ -138,6 +148,7 @@ import { computed, onMounted, reactive, ref } from 'vue';
 import { ElMessage, FormInstance, FormRules } from 'element-plus';
 import SelectCustomer from './selectCustomer.vue';
 import SelectItem from './selectItem.vue';
+import ApprovalPanel from '/@/views/approvalFlow/component/ApprovalPanel.vue';
 import { fetchSeOrderDetail, saveSeOrder, type SeOrderEntry, type SeOrderUpsert } from '../api/seOrderReview';
 import { type CustomerRow, type ItemRow } from '../api/universalSelector';
 

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

@@ -0,0 +1,99 @@
+import { axiosInstance } from '/@/utils/axios-utils';
+
+const BASE = '/api';
+
+// ── 流程定义扩展 ──
+
+export function publishFlow(id: number) {
+	return axiosInstance.post(`${BASE}/approvalFlow/publish`, { id });
+}
+
+export function getBizTypes() {
+	return axiosInstance.get<any>(`${BASE}/approvalFlow/bizTypes`);
+}
+
+// ── 业务类型管理 ──
+
+export function getBizTypeList() {
+	return axiosInstance.get<any>(`${BASE}/bizType/list`);
+}
+
+export function getBizTypePage(data: { page: number; pageSize: number; code?: string; name?: string }) {
+	return axiosInstance.post<any>(`${BASE}/bizType/page`, data);
+}
+
+export function addBizType(data: { code: string; name: string; remark?: string; isEnabled?: boolean }) {
+	return axiosInstance.post<any>(`${BASE}/bizType/add`, data);
+}
+
+export function updateBizType(data: { id: number; code: string; name: string; remark?: string; isEnabled?: boolean }) {
+	return axiosInstance.post(`${BASE}/bizType/update`, data);
+}
+
+export function deleteBizType(data: { id: number }) {
+	return axiosInstance.post(`${BASE}/bizType/delete`, data);
+}
+
+// ── 流程实例 ──
+
+export function startFlow(data: { bizType: string; bizId: number; bizNo?: string; title?: string; comment?: string }) {
+	return axiosInstance.post(`${BASE}/flowInstance/start`, data);
+}
+
+export function getInstanceDetail(id: number) {
+	return axiosInstance.get<any>(`${BASE}/flowInstance/detail`, { params: { id } });
+}
+
+export function getInstanceByBiz(bizType: string, bizId: number) {
+	return axiosInstance.get<any>(`${BASE}/flowInstance/getByBiz`, { params: { bizType, bizId } });
+}
+
+export function getTimeline(instanceId: number) {
+	return axiosInstance.get<any>(`${BASE}/flowInstance/timeline`, { params: { instanceId } });
+}
+
+// ── 审批任务 ──
+
+export function myPendingPage(data: { page: number; pageSize: number; bizType?: string }) {
+	return axiosInstance.post<any>(`${BASE}/flowTask/myPendingPage`, data);
+}
+
+export function myDonePage(data: { page: number; pageSize: number; bizType?: string }) {
+	return axiosInstance.post<any>(`${BASE}/flowTask/myDonePage`, data);
+}
+
+export function myInitiatedPage(data: { page: number; pageSize: number; bizType?: string; status?: number }) {
+	return axiosInstance.post<any>(`${BASE}/flowTask/myInitiatedPage`, data);
+}
+
+export function myPendingCount() {
+	return axiosInstance.get<any>(`${BASE}/flowTask/myPendingCount`);
+}
+
+export function approveTask(data: { taskId: number; comment?: string }) {
+	return axiosInstance.post(`${BASE}/flowTask/approve`, data);
+}
+
+export function rejectTask(data: { taskId: number; comment?: string }) {
+	return axiosInstance.post(`${BASE}/flowTask/reject`, data);
+}
+
+export function transferTask(data: { taskId: number; targetUserId: number; comment?: string }) {
+	return axiosInstance.post(`${BASE}/flowTask/transfer`, data);
+}
+
+export function withdrawFlow(data: { instanceId: number }) {
+	return axiosInstance.post(`${BASE}/flowTask/withdraw`, data);
+}
+
+export function returnToPrev(data: { taskId: number; comment?: string }) {
+	return axiosInstance.post(`${BASE}/flowTask/returnToPrev`, data);
+}
+
+export function addSign(data: { taskId: number; targetUserId: number; comment?: string }) {
+	return axiosInstance.post(`${BASE}/flowTask/addSign`, data);
+}
+
+export function urgeFlow(data: { instanceId: number }) {
+	return axiosInstance.post(`${BASE}/flowTask/urge`, data);
+}

+ 189 - 0
Web/src/views/approvalFlow/bizType/index.vue

@@ -0,0 +1,189 @@
+<template>
+	<div class="biz-type-container">
+		<el-card shadow="hover" :body-style="{ paddingBottom: '0' }">
+			<el-form :inline="true" :model="queryParams">
+				<el-form-item label="编码">
+					<el-input v-model="queryParams.code" placeholder="编码" clearable @keyup.enter="loadData" />
+				</el-form-item>
+				<el-form-item label="名称">
+					<el-input v-model="queryParams.name" placeholder="名称" clearable @keyup.enter="loadData" />
+				</el-form-item>
+				<el-form-item>
+					<el-button type="primary" @click="loadData">查询</el-button>
+					<el-button type="success" @click="openAdd">新增</el-button>
+				</el-form-item>
+			</el-form>
+		</el-card>
+
+		<el-card shadow="hover" style="margin-top: 5px">
+			<el-table :data="tableData" v-loading="loading" border style="width: 100%">
+				<el-table-column type="index" label="#" width="55" align="center" />
+				<el-table-column prop="code" label="业务类型编码" width="200" show-overflow-tooltip />
+				<el-table-column prop="name" label="名称" width="200" show-overflow-tooltip />
+				<el-table-column prop="remark" label="备注" show-overflow-tooltip />
+				<el-table-column prop="source" label="来源" width="120" align="center">
+					<template #default="{ row }">
+						<el-tag :type="row.source === 'code' ? 'warning' : ''" size="small">
+							{{ row.source === 'code' ? '代码注册' : '手动添加' }}
+						</el-tag>
+					</template>
+				</el-table-column>
+				<el-table-column prop="isEnabled" label="状态" width="80" align="center">
+					<template #default="{ row }">
+						<el-tag v-if="row.id" :type="row.isEnabled ? 'success' : 'info'" size="small">
+							{{ row.isEnabled ? '启用' : '禁用' }}
+						</el-tag>
+						<span v-else>-</span>
+					</template>
+				</el-table-column>
+				<el-table-column label="操作" width="140" align="center" fixed="right">
+					<template #default="{ row }">
+						<template v-if="row.id">
+							<el-button size="small" text @click="openEdit(row)">编辑</el-button>
+							<el-button size="small" text type="danger" @click="handleDelete(row)" :disabled="row.source === 'code'">删除</el-button>
+						</template>
+						<span v-else style="color: #909399; font-size: 12px">代码管理</span>
+					</template>
+				</el-table-column>
+			</el-table>
+
+			<el-pagination
+				v-model:page-size="queryParams.pageSize"
+				v-model:current-page="queryParams.page"
+				:page-sizes="[10, 20, 50]"
+				:total="total"
+				@current-change="loadData"
+				@size-change="loadData"
+				layout="total, sizes, prev, pager, next, jumper"
+				background
+				small
+				style="margin-top: 12px"
+			/>
+		</el-card>
+
+		<!-- 新增 / 编辑弹窗 -->
+		<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px" draggable destroy-on-close>
+			<el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
+				<el-form-item label="业务类型编码" prop="code">
+					<el-input v-model="form.code" placeholder="如 ORDER_REVIEW" maxlength="64" show-word-limit :disabled="isEdit" />
+				</el-form-item>
+				<el-form-item label="名称" prop="name">
+					<el-input v-model="form.name" placeholder="如 订单评审" maxlength="128" show-word-limit />
+				</el-form-item>
+				<el-form-item label="备注" prop="remark">
+					<el-input v-model="form.remark" type="textarea" :rows="2" maxlength="256" show-word-limit />
+				</el-form-item>
+				<el-form-item label="启用" prop="isEnabled">
+					<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="approvalBizType">
+import { ref, reactive, onMounted } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import type { FormInstance, FormRules } from 'element-plus';
+import { getBizTypePage, addBizType, updateBizType, deleteBizType } from '../api';
+
+const loading = ref(false);
+const tableData = ref<any[]>([]);
+const total = ref(0);
+const queryParams = reactive({ page: 1, pageSize: 20, code: '', name: '' });
+
+const dialogVisible = ref(false);
+const dialogTitle = ref('');
+const isEdit = ref(false);
+const submitting = ref(false);
+const formRef = ref<FormInstance>();
+const form = reactive({
+	id: 0,
+	code: '',
+	name: '',
+	remark: '',
+	isEnabled: true,
+});
+
+const formRules: FormRules = {
+	code: [
+		{ required: true, message: '编码不能为空', trigger: 'blur' },
+		{ pattern: /^[A-Z][A-Z0-9_]*$/, message: '编码格式:大写字母开头,仅含大写字母/数字/下划线', trigger: 'blur' },
+	],
+	name: [{ required: true, message: '名称不能为空', trigger: 'blur' }],
+};
+
+onMounted(() => loadData());
+
+const loadData = async () => {
+	loading.value = true;
+	try {
+		const res = await getBizTypePage(queryParams);
+		tableData.value = res.data?.result?.items ?? [];
+		total.value = res.data?.result?.total ?? 0;
+	} finally {
+		loading.value = false;
+	}
+};
+
+const resetForm = () => {
+	form.id = 0;
+	form.code = '';
+	form.name = '';
+	form.remark = '';
+	form.isEnabled = true;
+};
+
+const openAdd = () => {
+	resetForm();
+	isEdit.value = false;
+	dialogTitle.value = '新增业务类型';
+	dialogVisible.value = true;
+};
+
+const openEdit = (row: any) => {
+	form.id = row.id;
+	form.code = row.code;
+	form.name = row.name;
+	form.remark = row.remark ?? '';
+	form.isEnabled = row.isEnabled ?? true;
+	isEdit.value = true;
+	dialogTitle.value = '编辑业务类型';
+	dialogVisible.value = true;
+};
+
+const submitForm = async () => {
+	await formRef.value?.validate();
+	submitting.value = true;
+	try {
+		if (isEdit.value) {
+			await updateBizType(form);
+			ElMessage.success('更新成功');
+		} else {
+			await addBizType(form);
+			ElMessage.success('新增成功');
+		}
+		dialogVisible.value = false;
+		loadData();
+	} finally {
+		submitting.value = false;
+	}
+};
+
+const handleDelete = async (row: any) => {
+	await ElMessageBox.confirm(`确定删除业务类型【${row.code}】吗?`, '提示', { type: 'warning' });
+	await deleteBizType({ id: row.id });
+	ElMessage.success('删除成功');
+	loadData();
+};
+</script>
+
+<style scoped>
+.biz-type-container {
+	padding: 0;
+}
+</style>

+ 86 - 0
Web/src/views/approvalFlow/center/components/ApprovalDialog.vue

@@ -0,0 +1,86 @@
+<template>
+	<el-dialog v-model="visible" :title="title" width="480px" draggable @close="onClose">
+		<el-input v-model="commentText" type="textarea" :rows="4" placeholder="请输入审批意见" />
+		<template v-if="needTarget">
+			<el-form-item label="目标用户" style="margin-top: 12px">
+				<el-select
+					v-model="targetUserId"
+					filterable
+					remote
+					reserve-keyword
+					:remote-method="searchUsers"
+					:loading="userLoading"
+					placeholder="输入姓名或账号搜索"
+					style="width: 100%"
+					clearable
+				>
+					<el-option v-for="u in userOptions" :key="u.id" :label="`${u.realName}(${u.account})`" :value="u.id" />
+				</el-select>
+			</el-form-item>
+		</template>
+		<template #footer>
+			<el-button @click="visible = false">取消</el-button>
+			<el-button type="primary" @click="onConfirm" :loading="submitting">确定</el-button>
+		</template>
+	</el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, computed } from 'vue';
+import { getAPI } from '/@/utils/axios-utils';
+import { SysUserApi } from '/@/api-services/api';
+
+export type ActionType = 'approve' | 'reject' | 'transfer' | 'addSign';
+
+const props = defineProps<{
+	modelValue: boolean;
+	actionType: ActionType;
+}>();
+
+const emit = defineEmits<{
+	(e: 'update:modelValue', val: boolean): void;
+	(e: 'confirm', payload: { comment: string; targetUserId?: number }): void;
+}>();
+
+const commentText = ref('');
+const targetUserId = ref<number>();
+const submitting = ref(false);
+
+const userLoading = ref(false);
+const userOptions = ref<Array<{ id: number; account: string; realName: string }>>([]);
+
+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 visible = computed({
+	get: () => props.modelValue,
+	set: (v) => emit('update:modelValue', v),
+});
+
+const title = computed(() => {
+	const map: Record<ActionType, string> = { approve: '同意审批', reject: '拒绝审批', transfer: '转办', addSign: '加签' };
+	return map[props.actionType];
+});
+
+const needTarget = computed(() => props.actionType === 'transfer' || props.actionType === 'addSign');
+
+const onClose = () => {
+	commentText.value = '';
+	targetUserId.value = undefined;
+	userOptions.value = [];
+};
+
+const onConfirm = () => {
+	emit('confirm', { comment: commentText.value, targetUserId: targetUserId.value });
+};
+
+defineExpose({ setSubmitting: (v: boolean) => (submitting.value = v) });
+</script>

+ 34 - 0
Web/src/views/approvalFlow/center/components/DoneList.vue

@@ -0,0 +1,34 @@
+<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="nodeName" label="审批节点" width="140" />
+		<el-table-column prop="status" label="审批结果" width="100" align="center">
+			<template #default="{ row }">
+				<el-tag :type="taskStatusTag(row.status)" size="small">{{ taskStatusLabel(row.status) }}</el-tag>
+			</template>
+		</el-table-column>
+		<el-table-column prop="comment" label="意见" show-overflow-tooltip />
+		<el-table-column prop="actionTime" label="操作时间" width="180" />
+		<el-table-column label="操作" width="80" align="center" fixed="right">
+			<template #default="{ row }">
+				<el-button size="small" text @click="emit('timeline', row)">详情</el-button>
+			</template>
+		</el-table-column>
+	</el-table>
+</template>
+
+<script setup lang="ts">
+defineProps<{ data: any[]; loading: boolean }>();
+const emit = defineEmits<{ (e: 'timeline', row: any): void }>();
+
+const taskStatusLabel = (s: number) => {
+	const map: Record<number, string> = { 0: '待审批', 1: '已同意', 2: '已拒绝', 3: '已转办', 4: '已取消', 5: '已退回' };
+	return map[s] ?? '未知';
+};
+const taskStatusTag = (s: number) => {
+	const map: Record<number, string> = { 0: '', 1: 'success', 2: 'danger', 3: 'warning', 4: 'info', 5: 'warning' };
+	return map[s] ?? '';
+};
+</script>

+ 45 - 0
Web/src/views/approvalFlow/center/components/InitiatedList.vue

@@ -0,0 +1,45 @@
+<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="currentAssigneeName" label="当前审批人" width="140" show-overflow-tooltip>
+			<template #default="{ row }">
+				<span v-if="row.currentAssigneeName">{{ row.currentAssigneeName }}</span>
+				<span v-else style="color: #c0c4cc">-</span>
+			</template>
+		</el-table-column>
+		<el-table-column prop="status" label="状态" width="100" align="center">
+			<template #default="{ row }">
+				<el-tag :type="instanceStatusTag(row.status)" size="small">{{ instanceStatusLabel(row.status) }}</el-tag>
+			</template>
+		</el-table-column>
+		<el-table-column prop="startTime" label="发起时间" width="180" />
+		<el-table-column label="操作" width="180" align="center" fixed="right">
+			<template #default="{ row }">
+				<el-button size="small" text @click="emit('timeline', row)">详情</el-button>
+				<el-button v-if="row.status === 1" size="small" text type="primary" @click="emit('urge', row)">催办</el-button>
+				<el-button v-if="row.status === 1" size="small" text type="warning" @click="emit('withdraw', row)">撤回</el-button>
+			</template>
+		</el-table-column>
+	</el-table>
+</template>
+
+<script setup lang="ts">
+defineProps<{ data: any[]; loading: boolean }>();
+const emit = defineEmits<{
+	(e: 'timeline', row: any): void;
+	(e: 'urge', row: any): void;
+	(e: 'withdraw', row: any): void;
+}>();
+
+const instanceStatusLabel = (s: number) => {
+	const map: Record<number, string> = { 0: '草稿', 1: '审批中', 2: '已通过', 3: '已拒绝', 4: '已撤销', 5: '已终止' };
+	return map[s] ?? '未知';
+};
+const instanceStatusTag = (s: number) => {
+	const map: Record<number, string> = { 0: 'info', 1: '', 2: 'success', 3: 'danger', 4: 'warning', 5: 'danger' };
+	return map[s] ?? '';
+};
+</script>

+ 42 - 0
Web/src/views/approvalFlow/center/components/PendingList.vue

@@ -0,0 +1,42 @@
+<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>
+</template>
+
+<script setup lang="ts">
+defineProps<{ data: any[]; loading: boolean }>();
+const emit = defineEmits<{
+	(e: 'approve', row: any): void;
+	(e: 'reject', row: any): void;
+	(e: 'transfer', row: any): void;
+	(e: 'return', row: any): void;
+	(e: 'addSign', row: any): void;
+	(e: 'urge', row: any): void;
+	(e: 'timeline', row: any): void;
+}>();
+</script>

+ 40 - 0
Web/src/views/approvalFlow/center/components/TimelineDialog.vue

@@ -0,0 +1,40 @@
+<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>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue';
+
+const props = defineProps<{
+	modelValue: boolean;
+	data: any[];
+}>();
+
+const emit = defineEmits<{
+	(e: 'update:modelValue', val: boolean): void;
+}>();
+
+const visible = computed({
+	get: () => props.modelValue,
+	set: (v) => emit('update:modelValue', v),
+});
+
+const actionLabel = (a: number) => {
+	const map: Record<number, string> = { 1: '提交', 2: '同意', 3: '拒绝', 4: '转办', 5: '撤回', 6: '退回', 7: '加签', 8: '催办', 9: '取消', 10: '超时自动' };
+	return map[a] ?? '操作';
+};
+</script>

+ 236 - 0
Web/src/views/approvalFlow/center/index.vue

@@ -0,0 +1,236 @@
+<template>
+	<div class="approval-center-container">
+		<el-card shadow="hover" :body-style="{ paddingBottom: '0' }">
+			<el-tabs v-model="activeTab" @tab-change="onTabChange">
+				<el-tab-pane label="我的待办" name="pending">
+					<template #label>
+						<span>
+							我的待办
+							<el-badge v-if="pendingCount > 0" :value="pendingCount" :max="99" class="tab-badge" />
+						</span>
+					</template>
+				</el-tab-pane>
+				<el-tab-pane label="我的已办" name="done" />
+				<el-tab-pane label="我发起的" name="initiated" />
+			</el-tabs>
+			<el-form :inline="true" style="margin-top: 4px">
+				<el-form-item label="业务类型">
+					<el-select v-model="filterBizType" placeholder="全部" clearable filterable style="width: 180px" @change="loadData">
+						<el-option v-for="bt in bizTypeOptions" :key="bt.code" :label="`${bt.name}(${bt.code})`" :value="bt.code" />
+					</el-select>
+				</el-form-item>
+				<el-form-item>
+					<el-button type="primary" icon="ele-Search" @click="loadData">查询</el-button>
+					<el-button icon="ele-Refresh" @click="resetFilter">重置</el-button>
+				</el-form-item>
+			</el-form>
+		</el-card>
+
+		<el-card class="full-table" shadow="hover" style="margin-top: 5px">
+			<PendingList
+				v-if="activeTab === 'pending'"
+				:data="tableData"
+				:loading="loading"
+				@approve="openDialog('approve', $event)"
+				@reject="openDialog('reject', $event)"
+				@transfer="openDialog('transfer', $event)"
+				@return="doReturnToPrev"
+				@addSign="openDialog('addSign', $event)"
+				@urge="doUrge"
+				@timeline="openTimeline"
+			/>
+			<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" />
+
+			<el-pagination
+				v-model:page-size="pageSize"
+				v-model:current-page="currentPage"
+				:page-sizes="[10, 20, 50]"
+				:total="total"
+				@current-change="loadData"
+				@size-change="loadData"
+				layout="total, sizes, prev, pager, next, jumper"
+				background
+				small
+				style="margin-top: 12px"
+			/>
+		</el-card>
+
+		<ApprovalDialog v-model="dialogVisible" :actionType="dialogAction" @confirm="submitAction" ref="dialogRef" />
+		<TimelineDialog v-model="timelineVisible" :data="timelineData" />
+	</div>
+</template>
+
+<script lang="ts" setup name="approvalFlowCenter">
+import { ref, onMounted } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import {
+	myPendingPage,
+	myDonePage,
+	myInitiatedPage,
+	myPendingCount,
+	approveTask,
+	rejectTask,
+	transferTask,
+	withdrawFlow,
+	returnToPrev,
+	addSign,
+	urgeFlow,
+	getTimeline,
+	getBizTypeList,
+} from '../api';
+import PendingList from './components/PendingList.vue';
+import DoneList from './components/DoneList.vue';
+import InitiatedList from './components/InitiatedList.vue';
+import ApprovalDialog from './components/ApprovalDialog.vue';
+import TimelineDialog from './components/TimelineDialog.vue';
+
+import type { ActionType } from './components/ApprovalDialog.vue';
+
+const activeTab = ref('pending');
+const loading = ref(false);
+const tableData = ref<any[]>([]);
+const currentPage = ref(1);
+const pageSize = ref(20);
+const total = ref(0);
+const pendingCount = ref(0);
+
+const filterBizType = ref('');
+const bizTypeOptions = ref<Array<{ code: string; name: string }>>([]);
+
+const loadBizTypes = async () => {
+	try {
+		const res = await getBizTypeList();
+		bizTypeOptions.value = res.data?.result ?? [];
+	} catch {}
+};
+
+const resetFilter = () => {
+	filterBizType.value = '';
+	currentPage.value = 1;
+	loadData();
+};
+
+const dialogVisible = ref(false);
+const dialogAction = ref<ActionType>('approve');
+const dialogRef = ref<InstanceType<typeof ApprovalDialog>>();
+const currentRow = ref<any>(null);
+
+const timelineVisible = ref(false);
+const timelineData = ref<any[]>([]);
+
+onMounted(() => {
+	loadBizTypes();
+	loadData();
+	loadPendingCount();
+});
+
+const onTabChange = () => {
+	currentPage.value = 1;
+	loadData();
+};
+
+const loadPendingCount = async () => {
+	try {
+		const res = await myPendingCount();
+		pendingCount.value = res.data?.result ?? 0;
+	} catch {}
+};
+
+const loadData = async () => {
+	loading.value = true;
+	try {
+		const params: any = { page: currentPage.value, pageSize: pageSize.value };
+		if (filterBizType.value) params.bizType = filterBizType.value;
+		let res: any;
+		if (activeTab.value === 'pending') {
+			res = await myPendingPage(params);
+		} else if (activeTab.value === 'done') {
+			res = await myDonePage(params);
+		} else {
+			res = await myInitiatedPage(params);
+		}
+		tableData.value = res.data?.result?.items ?? [];
+		total.value = res.data?.result?.total ?? 0;
+	} finally {
+		loading.value = false;
+	}
+};
+
+const openDialog = (type: ActionType, row: any) => {
+	currentRow.value = row;
+	dialogAction.value = type;
+	dialogVisible.value = true;
+};
+
+const submitAction = async (payload: { comment: string; targetUserId?: number }) => {
+	if (!currentRow.value) return;
+	dialogRef.value?.setSubmitting(true);
+	try {
+		const taskId = currentRow.value.id;
+		if (dialogAction.value === 'approve') {
+			await approveTask({ taskId, comment: payload.comment });
+			ElMessage.success('已同意');
+		} else if (dialogAction.value === 'reject') {
+			await rejectTask({ taskId, comment: payload.comment });
+			ElMessage.success('已拒绝');
+		} else if (dialogAction.value === 'transfer') {
+			if (!payload.targetUserId) return ElMessage.warning('请输入目标用户Id');
+			await transferTask({ taskId, targetUserId: payload.targetUserId, comment: payload.comment });
+			ElMessage.success('已转办');
+		} else if (dialogAction.value === 'addSign') {
+			if (!payload.targetUserId) return ElMessage.warning('请输入目标用户Id');
+			await addSign({ taskId, targetUserId: payload.targetUserId, comment: payload.comment });
+			ElMessage.success('已加签');
+		}
+		dialogVisible.value = false;
+		loadData();
+		loadPendingCount();
+	} finally {
+		dialogRef.value?.setSubmitting(false);
+	}
+};
+
+const doReturnToPrev = async (row: any) => {
+	try {
+		await ElMessageBox.confirm('确定退回上一步吗?', '提示', { type: 'warning' });
+		await returnToPrev({ taskId: row.id });
+		ElMessage.success('已退回');
+		loadData();
+	} catch {}
+};
+
+const doUrge = async (row: any) => {
+	try {
+		const instanceId = row.instanceId || row.id;
+		await urgeFlow({ instanceId });
+		ElMessage.success('催办成功');
+	} catch {}
+};
+
+const doWithdraw = async (row: any) => {
+	try {
+		await ElMessageBox.confirm('确定撤回该流程吗?(仅在无人审批时可撤回)', '提示', { type: 'warning' });
+		await withdrawFlow({ instanceId: row.id });
+		ElMessage.success('已撤回');
+		loadData();
+	} catch {}
+};
+
+const openTimeline = async (row: any) => {
+	const instanceId = row.instanceId || row.id;
+	try {
+		const res = await getTimeline(instanceId);
+		timelineData.value = res.data?.result ?? [];
+		timelineVisible.value = true;
+	} catch {
+		ElMessage.error('获取时间线失败');
+	}
+};
+</script>
+
+<style scoped>
+.tab-badge {
+	margin-left: 4px;
+}
+</style>

+ 327 - 0
Web/src/views/approvalFlow/component/ApprovalPanel.vue

@@ -0,0 +1,327 @@
+<template>
+	<div class="approval-panel" v-if="visible">
+		<el-divider content-position="left">审批流程</el-divider>
+
+		<div v-if="!instance" class="panel-section">
+			<el-button type="primary" @click="doStartFlow" :loading="starting">提交审批</el-button>
+		</div>
+
+		<div v-else>
+			<el-descriptions :column="2" border size="small" style="margin-bottom: 12px">
+				<el-descriptions-item label="流程标题">{{ instance.title }}</el-descriptions-item>
+				<el-descriptions-item label="状态">
+					<el-tag :type="instanceStatusTag(instance.status)" size="small">{{ instanceStatusLabel(instance.status) }}</el-tag>
+				</el-descriptions-item>
+				<el-descriptions-item label="发起人">{{ instance.initiatorName }}</el-descriptions-item>
+				<el-descriptions-item label="发起时间">{{ instance.startTime }}</el-descriptions-item>
+			</el-descriptions>
+
+			<!-- 当前用户待审批 -->
+			<div v-if="myTask" class="panel-section">
+				<el-input v-model="comment" type="textarea" :rows="2" placeholder="审批意见" style="margin-bottom: 8px" />
+				<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>
+				</div>
+			</div>
+
+			<!-- 发起人操作 -->
+			<div v-if="isInitiator && instance.status === 1" class="panel-section">
+				<el-button-group>
+					<el-button type="warning" @click="doUrge" :loading="operating">催办</el-button>
+					<el-button @click="doWithdraw" :loading="operating">撤回</el-button>
+				</el-button-group>
+			</div>
+
+			<!-- 审批时间线 -->
+			<el-collapse style="margin-top: 12px">
+				<el-collapse-item title="审批时间线">
+					<el-timeline>
+						<el-timeline-item v-for="item in timeline" :key="item.id" :timestamp="item.createTime" placement="top">
+							<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: 8px; color: #909399">{{ item.operatorName }}</span>
+							<div v-if="item.comment" style="color: #606266">{{ item.comment }}</div>
+						</el-timeline-item>
+					</el-timeline>
+				</el-collapse-item>
+			</el-collapse>
+		</div>
+
+		<!-- 转办 / 加签 选人对话框 -->
+		<el-dialog v-model="userDialogVisible" :title="userDialogTitle" width="400px" destroy-on-close append-to-body>
+			<el-form label-width="80px">
+				<el-form-item label="目标用户">
+					<el-select
+						v-model="targetUserId"
+						filterable
+						remote
+						reserve-keyword
+						:remote-method="searchUsers"
+						:loading="userLoading"
+						placeholder="输入姓名或账号搜索"
+						style="width: 100%"
+						clearable
+					>
+						<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="备注">
+					<el-input v-model="dialogComment" type="textarea" :rows="2" />
+				</el-form-item>
+			</el-form>
+			<template #footer>
+				<el-button @click="userDialogVisible = false">取消</el-button>
+				<el-button type="primary" @click="submitUserDialog" :loading="operating">确定</el-button>
+			</template>
+		</el-dialog>
+	</div>
+</template>
+
+<script setup lang="ts">
+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 {
+	startFlow,
+	getInstanceByBiz,
+	getTimeline,
+	approveTask,
+	rejectTask,
+	transferTask,
+	withdrawFlow,
+	returnToPrev,
+	addSign,
+	urgeFlow,
+} from '../api';
+
+const props = defineProps<{
+	bizType: string;
+	bizId: number;
+	bizNo?: string;
+	title?: string;
+	currentUserId?: number;
+}>();
+
+const emit = defineEmits<{
+	(e: 'flowStarted', instanceId: number): void;
+	(e: 'flowCompleted', status: number): void;
+	(e: 'refresh'): void;
+}>();
+
+const visible = ref(false);
+const instance = ref<any>(null);
+const timeline = ref<any[]>([]);
+const comment = ref('');
+const starting = ref(false);
+const operating = ref(false);
+
+const userDialogVisible = ref(false);
+const userDialogTitle = ref('');
+const userDialogMode = ref<'transfer' | 'addSign'>('transfer');
+const targetUserId = ref<number>(0);
+const dialogComment = ref('');
+
+const userLoading = ref(false);
+const userOptions = ref<Array<{ id: number; account: string; realName: string }>>([]);
+
+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 myTask = computed(() => {
+	if (!instance.value?.tasks) return null;
+	return instance.value.tasks.find((t: any) => t.status === 0);
+});
+
+const isInitiator = computed(() => {
+	if (!instance.value || !props.currentUserId) return false;
+	return instance.value.initiatorId === props.currentUserId;
+});
+
+watch(
+	() => [props.bizType, props.bizId],
+	() => {
+		if (props.bizType && props.bizId) {
+			loadInstance();
+		}
+	},
+	{ immediate: true }
+);
+
+const loadInstance = async () => {
+	try {
+		const res = await getInstanceByBiz(props.bizType, props.bizId);
+		instance.value = res.data?.result ?? null;
+		visible.value = true;
+		if (instance.value) {
+			const tlRes = await getTimeline(instance.value.id);
+			timeline.value = tlRes.data?.result ?? [];
+		}
+	} catch {
+		visible.value = true;
+	}
+};
+
+const doStartFlow = async () => {
+	starting.value = true;
+	try {
+		const res = await startFlow({
+			bizType: props.bizType,
+			bizId: props.bizId,
+			bizNo: props.bizNo,
+			title: props.title,
+		});
+		ElMessage.success('审批已提交');
+		emit('flowStarted', res.data?.result);
+		await loadInstance();
+	} finally {
+		starting.value = false;
+	}
+};
+
+const doApprove = async () => {
+	if (!myTask.value) return;
+	operating.value = true;
+	try {
+		await approveTask({ taskId: myTask.value.id, comment: comment.value });
+		ElMessage.success('已同意');
+		comment.value = '';
+		await loadInstance();
+		emit('refresh');
+	} finally {
+		operating.value = false;
+	}
+};
+
+const doReject = async () => {
+	if (!myTask.value) return;
+	operating.value = true;
+	try {
+		await rejectTask({ taskId: myTask.value.id, comment: comment.value });
+		ElMessage.success('已拒绝');
+		comment.value = '';
+		await loadInstance();
+		emit('refresh');
+	} finally {
+		operating.value = false;
+	}
+};
+
+const openTransfer = () => {
+	userDialogMode.value = 'transfer';
+	userDialogTitle.value = '转办';
+	targetUserId.value = 0;
+	dialogComment.value = comment.value;
+	userDialogVisible.value = true;
+};
+
+const openAddSign = () => {
+	userDialogMode.value = 'addSign';
+	userDialogTitle.value = '加签';
+	targetUserId.value = 0;
+	dialogComment.value = comment.value;
+	userDialogVisible.value = true;
+};
+
+const submitUserDialog = async () => {
+	if (!myTask.value || !targetUserId.value) {
+		ElMessage.warning('请输入目标用户ID');
+		return;
+	}
+	operating.value = true;
+	try {
+		if (userDialogMode.value === 'transfer') {
+			await transferTask({ taskId: myTask.value.id, targetUserId: targetUserId.value, comment: dialogComment.value });
+			ElMessage.success('转办成功');
+		} else {
+			await addSign({ taskId: myTask.value.id, targetUserId: targetUserId.value, comment: dialogComment.value });
+			ElMessage.success('加签成功');
+		}
+		userDialogVisible.value = false;
+		comment.value = '';
+		await loadInstance();
+		emit('refresh');
+	} finally {
+		operating.value = false;
+	}
+};
+
+const doReturn = async () => {
+	if (!myTask.value) return;
+	await ElMessageBox.confirm('确定退回上一步吗?', '提示', { type: 'warning' });
+	operating.value = true;
+	try {
+		await returnToPrev({ 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;
+	try {
+		await urgeFlow({ instanceId: instance.value.id });
+		ElMessage.success('催办成功');
+	} finally {
+		operating.value = false;
+	}
+};
+
+const doWithdraw = async () => {
+	if (!instance.value) return;
+	await ElMessageBox.confirm('确定撤回该审批流程吗?撤回后将取消所有待审批任务。', '提示', { type: 'warning' });
+	operating.value = true;
+	try {
+		await withdrawFlow({ instanceId: instance.value.id });
+		ElMessage.success('已撤回');
+		await loadInstance();
+		emit('refresh');
+	} finally {
+		operating.value = false;
+	}
+};
+
+const instanceStatusLabel = (s: number) => {
+	const map: Record<number, string> = { 0: '草稿', 1: '审批中', 2: '已通过', 3: '已拒绝', 4: '已撤销', 5: '已终止' };
+	return map[s] ?? '未知';
+};
+const instanceStatusTag = (s: number) => {
+	const map: Record<number, string> = { 0: 'info', 1: '', 2: 'success', 3: 'danger', 4: 'warning', 5: 'danger' };
+	return map[s] ?? '';
+};
+const actionLabel = (a: number) => {
+	const map: Record<number, string> = { 1: '提交', 2: '同意', 3: '拒绝', 4: '转办', 5: '撤回', 6: '退回', 7: '加签', 8: '催办', 9: '取消', 10: '超时自动' };
+	return map[a] ?? '操作';
+};
+</script>
+
+<style scoped>
+.approval-panel {
+	margin-top: 12px;
+}
+.panel-section {
+	margin-bottom: 12px;
+}
+.btn-row {
+	display: flex;
+	gap: 8px;
+	flex-wrap: wrap;
+}
+</style>

+ 31 - 1
Web/src/views/approvalFlow/component/LogicFlow/Panel/PanelControl.vue

@@ -16,7 +16,7 @@
 </template>
 
 <script setup lang="ts">
-import { reactive } from 'vue';
+import { reactive, watch, onMounted, onBeforeUnmount } from 'vue';
 
 var props = defineProps({
 	lf: Object,
@@ -28,6 +28,36 @@ const state = reactive({
 	redoDisable: true,
 });
 
+function updateHistoryState() {
+	if (!props.lf) return;
+	const { undoAble, redoAble } = props.lf.history.getUndoRedo?.() ?? {};
+	if (undoAble !== undefined) {
+		state.undoDisable = !undoAble;
+		state.redoDisable = !redoAble;
+	}
+}
+
+function onHistoryChange({ data }: any) {
+	if (data) {
+		state.undoDisable = !data.undoAble;
+		state.redoDisable = !data.redoAble;
+	}
+}
+
+watch(
+	() => props.lf,
+	(lf) => {
+		if (!lf) return;
+		lf.on('history:change', onHistoryChange);
+		updateHistoryState();
+	},
+	{ immediate: true }
+);
+
+onBeforeUnmount(() => {
+	props.lf?.off('history:change', onHistoryChange);
+});
+
 const $_zoomIn = () => {
 	props.lf?.zoom(true);
 };

+ 329 - 19
Web/src/views/approvalFlow/component/LogicFlow/Property/PropertyCommon.vue

@@ -1,26 +1,169 @@
 <template>
 	<div class="property-common">
-		<el-form :model="state.ruleForm" ref="ruleFormRef" label-width="auto" :rules="rules">
-			<el-form-item v-show="false">
-				<el-input v-model="nodeData.id" />
-			</el-form-item>
+		<el-form :model="formData" ref="ruleFormRef" label-width="auto">
 			<el-tabs>
 				<el-tab-pane label="基本信息">
 					<el-row :gutter="35">
-						<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
-							<el-form-item label="编号" prop="id">
-								<el-input v-model="nodeData.id" placeholder="请输入编号" maxlength="32" readonly show-word-limit clearable />
+						<el-col :span="24" class="mb20">
+							<el-form-item label="编号">
+								<el-input v-model="nodeData.id" readonly />
 							</el-form-item>
 						</el-col>
-						<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
-							<el-form-item label="属性" prop="type">
-								<el-input v-model="nodeData.type" placeholder="请输入编号" maxlength="32" readonly show-word-limit clearable />
+						<el-col :span="24" class="mb20">
+							<el-form-item label="类型">
+								<el-input v-model="nodeData.type" readonly />
 							</el-form-item>
 						</el-col>
 					</el-row>
 				</el-tab-pane>
-				<el-tab-pane label="扩展信息">
-					<el-row :gutter="35"></el-row>
+
+				<!-- 用户任务节点属性 -->
+				<el-tab-pane label="审批配置" v-if="isUserTask">
+					<el-row :gutter="35">
+						<el-col :span="24" class="mb20">
+							<el-form-item label="节点名称">
+								<el-input v-model="formData.nodeName" placeholder="请输入节点名称" clearable />
+							</el-form-item>
+						</el-col>
+						<el-col :span="24" class="mb20">
+							<el-form-item label="审批人类型">
+								<el-select v-model="formData.approverType" placeholder="请选择" clearable>
+									<el-option label="指定用户" value="SpecificUser" />
+									<el-option label="指定角色" value="Role" />
+									<el-option label="指定部门" value="Department" />
+									<el-option label="发起人本人" value="Initiator" />
+								</el-select>
+							</el-form-item>
+						</el-col>
+
+						<!-- 指定用户:远程搜索多选 -->
+						<el-col :span="24" class="mb20" v-if="formData.approverType === 'SpecificUser'">
+							<el-form-item label="选择用户">
+								<el-select
+									v-model="selectedUserIds"
+									multiple
+									filterable
+									remote
+									reserve-keyword
+									:remote-method="searchUsers"
+									:loading="userLoading"
+									placeholder="输入姓名或账号搜索"
+									style="width: 100%"
+									@change="onUserChange"
+								>
+									<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.approverType === 'Role'">
+							<el-form-item label="选择角色">
+								<el-select
+									v-model="selectedRoleIds"
+									multiple
+									filterable
+									placeholder="请选择角色"
+									style="width: 100%"
+									@change="onRoleChange"
+								>
+									<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.approverType === 'Department'">
+							<el-form-item label="选择部门">
+								<el-tree-select
+									v-model="selectedOrgIds"
+									:data="orgTreeData"
+									multiple
+									filterable
+									check-strictly
+									:render-after-expand="false"
+									:props="{ label: 'name', value: 'id', children: 'children' }"
+									placeholder="请选择部门"
+									style="width: 100%"
+									@change="onOrgChange"
+								/>
+							</el-form-item>
+						</el-col>
+
+						<!-- 只读回显已选 -->
+						<el-col :span="24" class="mb20" v-if="formData.approverType && formData.approverType !== 'Initiator' && formData.approverNames">
+							<el-form-item label="已选">
+								<el-tag v-for="name in formData.approverNames.split(',')" :key="name" size="small" style="margin-right: 4px">{{ name }}</el-tag>
+							</el-form-item>
+						</el-col>
+
+						<el-col :span="24" class="mb20">
+							<el-form-item label="审批方式">
+								<el-select v-model="formData.multiApproveMode" placeholder="请选择" clearable>
+									<el-option label="或签(一人通过即可)" value="Any" />
+									<el-option label="会签(所有人通过)" value="All" />
+								</el-select>
+							</el-form-item>
+						</el-col>
+						<el-col :span="24" class="mb20">
+							<el-form-item label="超时(小时)">
+								<el-input-number v-model="formData.timeoutHours" :min="0" :max="720" controls-position="right" />
+							</el-form-item>
+						</el-col>
+						<el-col :span="24" class="mb20" v-if="formData.timeoutHours">
+							<el-form-item label="超时动作">
+								<el-select v-model="formData.timeoutAction" placeholder="请选择">
+									<el-option label="自动通知" value="Notify" />
+									<el-option label="自动通过" value="AutoApprove" />
+									<el-option label="自动拒绝" value="AutoReject" />
+								</el-select>
+							</el-form-item>
+						</el-col>
+					</el-row>
+					<el-row>
+						<el-col :span="24" style="text-align: right">
+							<el-button type="primary" @click="saveProperties">保存到节点</el-button>
+						</el-col>
+					</el-row>
+				</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">
+						<el-row :gutter="12">
+							<el-col :span="22">
+								<el-form-item :label="'分支 ' + (idx + 1) + ' 目标节点'">
+									<el-input v-model="cond.targetNodeId" placeholder="目标节点 Id" clearable />
+								</el-form-item>
+								<el-form-item label="条件表达式">
+									<el-input v-model="cond.expression" placeholder="如 bizData.amount > 100000" clearable />
+								</el-form-item>
+								<el-form-item label="描述">
+									<el-input v-model="cond.label" placeholder="条件描述" clearable />
+								</el-form-item>
+								<el-checkbox v-model="cond.isDefault">默认分支</el-checkbox>
+							</el-col>
+							<el-col :span="2" style="display: flex; align-items: center">
+								<el-button type="danger" icon="ele-Delete" circle size="small" @click="formData.conditions.splice(idx, 1)" />
+							</el-col>
+						</el-row>
+					</div>
+					<el-button type="primary" plain @click="addCondition" style="margin-bottom: 12px">+ 添加分支</el-button>
+					<el-row>
+						<el-col :span="24" style="text-align: right">
+							<el-button type="primary" @click="saveProperties">保存到节点</el-button>
+						</el-col>
+					</el-row>
 				</el-tab-pane>
 			</el-tabs>
 		</el-form>
@@ -28,21 +171,188 @@
 </template>
 
 <script setup lang="ts">
-import { reactive, ref } from 'vue';
-import type { FormRules } from 'element-plus';
+import { reactive, ref, computed, watch } from 'vue';
+import { getAPI } from '/@/utils/axios-utils';
+import { SysUserApi, SysRoleApi, SysOrgApi } from '/@/api-services/api';
 
-var props = defineProps({
-	nodeData: Object,
+const props = defineProps({
+	nodeData: { type: Object, default: () => ({}) },
 });
 const emit = defineEmits(['setProperties']);
 
 const ruleFormRef = ref();
 
-const state = reactive({
-	ruleForm: {} as any,
+const isUserTask = computed(() => {
+	const t = props.nodeData?.type;
+	return t === 'bpmn:userTask' || t === 'user-node' || t === 'task-node';
 });
 
-const rules = ref<FormRules>({});
+const isGateway = computed(() => {
+	const t = props.nodeData?.type;
+	return t === 'bpmn:exclusiveGateway';
+});
+
+// ── 用户搜索 ──
+const userLoading = ref(false);
+const userOptions = ref<Array<{ id: number; account: string; realName: string }>>([]);
+const selectedUserIds = ref<number[]>([]);
+
+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);
+		const items = res.data.result?.items ?? [];
+		const existing = userOptions.value.filter((o) => selectedUserIds.value.includes(o.id));
+		const merged = [...existing];
+		for (const u of items) {
+			if (u.id && !merged.some((m) => m.id === u.id)) {
+				merged.push({ id: u.id, account: u.account ?? '', realName: u.realName ?? '' });
+			}
+		}
+		userOptions.value = merged;
+	} finally {
+		userLoading.value = false;
+	}
+};
+
+const onUserChange = () => {
+	const selected = userOptions.value.filter((o) => selectedUserIds.value.includes(o.id));
+	formData.approverIds = selected.map((s) => s.id).join(',');
+	formData.approverNames = selected.map((s) => s.realName || s.account).join(',');
+};
+
+// ── 角色列表 ──
+const roleOptions = ref<Array<{ id: number; name: string }>>([]);
+const selectedRoleIds = ref<number[]>([]);
+let roleLoaded = false;
+
+const loadRoles = async () => {
+	if (roleLoaded) return;
+	const res = await getAPI(SysRoleApi).apiSysRoleListGet();
+	roleOptions.value = (res.data.result ?? []).map((r: any) => ({ id: r.id, name: r.name ?? '' }));
+	roleLoaded = true;
+};
+
+const onRoleChange = () => {
+	const selected = roleOptions.value.filter((o) => selectedRoleIds.value.includes(o.id));
+	formData.approverIds = selected.map((s) => s.id).join(',');
+	formData.approverNames = selected.map((s) => s.name).join(',');
+};
+
+// ── 部门树 ──
+const orgTreeData = ref<any[]>([]);
+const selectedOrgIds = ref<number[]>([]);
+let orgLoaded = false;
+
+const loadOrgs = async () => {
+	if (orgLoaded) return;
+	const res = await getAPI(SysOrgApi).apiSysOrgTreeGet(0);
+	orgTreeData.value = res.data.result ?? [];
+	orgLoaded = true;
+};
+
+const flattenOrg = (nodes: any[]): Array<{ id: number; name: string }> => {
+	const result: Array<{ id: number; name: string }> = [];
+	for (const n of nodes) {
+		result.push({ id: n.id, name: n.name ?? '' });
+		if (n.children?.length) result.push(...flattenOrg(n.children));
+	}
+	return result;
+};
+
+const onOrgChange = () => {
+	const flat = flattenOrg(orgTreeData.value);
+	const selected = flat.filter((o) => selectedOrgIds.value.includes(o.id));
+	formData.approverIds = selected.map((s) => s.id).join(',');
+	formData.approverNames = selected.map((s) => s.name).join(',');
+};
+
+// ── 表单数据 ──
+const formData = reactive({
+	nodeName: '',
+	approverType: '',
+	approverIds: '',
+	approverNames: '',
+	multiApproveMode: 'Any',
+	timeoutHours: undefined as number | undefined,
+	timeoutAction: 'Notify',
+	conditions: [] as Array<{ targetNodeId: string; expression: string; label: string; isDefault: boolean }>,
+});
+
+// 审批人类型切换时清空已选
+watch(
+	() => formData.approverType,
+	(newType, oldType) => {
+		if (oldType && newType !== oldType) {
+			formData.approverIds = '';
+			formData.approverNames = '';
+			selectedUserIds.value = [];
+			selectedRoleIds.value = [];
+			selectedOrgIds.value = [];
+		}
+		if (newType === 'Role') loadRoles();
+		if (newType === 'Department') loadOrgs();
+	}
+);
+
+// 节点数据变化时回显
+const restoreSelection = async () => {
+	const ids = formData.approverIds ? formData.approverIds.split(',').map(Number).filter(Boolean) : [];
+	const names = formData.approverNames ? formData.approverNames.split(',') : [];
+
+	if (formData.approverType === 'SpecificUser' && ids.length) {
+		userOptions.value = ids.map((id, i) => ({ id, account: '', realName: names[i] || String(id) }));
+		selectedUserIds.value = ids;
+	} else if (formData.approverType === 'Role' && ids.length) {
+		await loadRoles();
+		selectedRoleIds.value = ids;
+	} else if (formData.approverType === 'Department' && ids.length) {
+		await loadOrgs();
+		selectedOrgIds.value = ids;
+	}
+};
+
+watch(
+	() => props.nodeData,
+	(val) => {
+		if (!val?.properties) return;
+		const p = val.properties;
+		formData.nodeName = p.nodeName || '';
+		formData.approverType = p.approverType || '';
+		formData.approverIds = p.approverIds || '';
+		formData.approverNames = p.approverNames || '';
+		formData.multiApproveMode = p.multiApproveMode || 'Any';
+		formData.timeoutHours = p.timeoutHours;
+		formData.timeoutAction = p.timeoutAction || 'Notify';
+		formData.conditions = Array.isArray(p.conditions) ? [...p.conditions] : [];
+		restoreSelection();
+	},
+	{ immediate: true, deep: true }
+);
+
+const addCondition = () => {
+	formData.conditions.push({ targetNodeId: '', expression: '', label: '', isDefault: false });
+};
+
+const saveProperties = () => {
+	const data: Record<string, any> = {};
+	if (isUserTask.value) {
+		data.nodeName = formData.nodeName;
+		data.approverType = formData.approverType;
+		data.approverIds = formData.approverIds;
+		data.approverNames = formData.approverNames;
+		data.multiApproveMode = formData.multiApproveMode;
+		if (formData.timeoutHours) {
+			data.timeoutHours = formData.timeoutHours;
+			data.timeoutAction = formData.timeoutAction;
+		}
+	}
+	if (isGateway.value) {
+		data.conditions = formData.conditions;
+	}
+	emit('setProperties', data);
+};
 </script>
 
 <style lang="scss" scoped></style>

+ 11 - 9
Web/src/views/approvalFlow/component/LogicFlow/Property/PropertyDialog.vue

@@ -4,22 +4,24 @@
 	</div>
 </template>
 
-<script setup>
+<script setup lang="ts">
 import { reactive, ref } from 'vue';
+import { ElMessage } from 'element-plus';
 
 import PropertyCommon from '/@/views/approvalFlow/component/LogicFlow/Property/PropertyCommon.vue';
 
-var props = defineProps({
-	lf: Object,
-	nodeData: Object,
+const props = defineProps({
+	lf: Object as any,
+	nodeData: Object as any,
 });
 const emit = defineEmits(['setPropertiesFinish']);
 
-const ruleFormRef = ref();
-
-const state = reactive({});
-
-const setProperties = () => {};
+const setProperties = (data: Record<string, any>) => {
+	if (!props.lf || !props.nodeData?.id) return;
+	props.lf.setProperties(props.nodeData.id, data);
+	ElMessage.success('节点属性已保存');
+	emit('setPropertiesFinish');
+};
 </script>
 
 <style lang="scss" scoped>

+ 21 - 2
Web/src/views/approvalFlow/component/editDialog.vue

@@ -24,6 +24,13 @@
 									<el-input v-model="state.ruleForm.name" placeholder="请输入名称" maxlength="32" show-word-limit clearable />
 								</el-form-item>
 							</el-col>
+						<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
+							<el-form-item label="业务类型" prop="bizType">
+								<el-select v-model="state.ruleForm.bizType" placeholder="请选择业务类型" clearable filterable style="width: 100%">
+									<el-option v-for="bt in bizTypeOptions" :key="bt.code" :label="`${bt.name}(${bt.code})`" :value="bt.code" />
+								</el-select>
+							</el-form-item>
+						</el-col>
 							<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
 								<el-form-item label="状态" prop="status" :rules="[{ required: true, message: '状态不能为空', trigger: 'blur' }]">
 									<g-sys-dict code="LabStatusEnum" v-model="state.ruleForm.status" render-as="select" />
@@ -63,11 +70,12 @@
 </template>
 
 <script setup lang="ts">
-import { reactive, ref } from 'vue';
+import { reactive, ref, onMounted } from 'vue';
 import { ElMessage } from 'element-plus';
 import type { FormRules } from 'element-plus';
 import { getAPI } from '/@/utils/axios-utils';
 import { ApprovalFlowApi } from '/@/api-plugins/approvalFlow/api';
+import { getBizTypeList } from '../api';
 
 // 父级传递来的参数
 var props = defineProps({
@@ -91,7 +99,17 @@ const state = reactive({
 	ruleForm: {} as any,
 });
 
-// 自行添加其他规则
+const bizTypeOptions = ref<{ code: string; name: string; source: string }[]>([]);
+
+const loadBizTypes = async () => {
+	try {
+		const res = await getBizTypeList();
+		bizTypeOptions.value = res.data?.result ?? [];
+	} catch {}
+};
+
+onMounted(() => loadBizTypes());
+
 const rules = ref<FormRules>({
 	name: [
 		{
@@ -107,6 +125,7 @@ const openDialog = async (row: any) => {
 	let rowData = JSON.parse(JSON.stringify(row));
 	state.ruleForm = rowData.id ? (await getAPI(ApprovalFlowApi).apiApprovalFlowDetailGet(rowData.id)).data.result : rowData;
 	state.isShowDialog = true;
+	loadBizTypes();
 };
 
 // 关闭弹窗

+ 26 - 0
Web/src/views/approvalFlow/index.vue

@@ -42,6 +42,13 @@
 				<el-table-column type="index" label="序号" width="55" align="center" />
 				<el-table-column prop="code" label="编号" width="140" show-overflow-tooltip />
 				<el-table-column prop="name" label="名称" show-overflow-tooltip />
+				<el-table-column prop="bizType" label="业务类型" width="140" show-overflow-tooltip />
+				<el-table-column prop="version" label="版本" width="70" align="center" />
+				<el-table-column prop="isPublished" label="发布" width="80" align="center">
+					<template #default="scope">
+						<el-tag :type="scope.row.isPublished ? 'success' : 'info'" size="small">{{ scope.row.isPublished ? '已发布' : '未发布' }}</el-tag>
+					</template>
+				</el-table-column>
 				<el-table-column prop="formJson" label="表单" align="center" show-overflow-tooltip>
 					<template #default="scope">
 						<el-button icon="ele-Edit" size="small" text type="primary" @click="openEditFormDialog(scope.row)"> 表单 </el-button>
@@ -61,6 +68,7 @@
 					<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-Delete" size="small" text type="primary" @click="delApprovalFlow(scope.row)"> 删除 </el-button>
 					</template>
 				</el-table-column>
@@ -101,6 +109,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';
 
 const showAdvanceQueryUI = ref(false);
 
@@ -208,6 +217,23 @@ const handleCurrentChange = (val: number) => {
 	state.tableParams.page = val;
 	handleQuery();
 };
+
+// 发布流程
+const publishFlow = (row: ApprovalFlowOutput) => {
+	ElMessageBox.confirm(`确定要发布审批流【${row.name}】吗?发布后版本号+1`, '提示', {
+		confirmButtonText: '确定',
+		cancelButtonText: '取消',
+		type: 'warning',
+	})
+		.then(async () => {
+			if (row.id) {
+				await publishFlowApi(row.id);
+				handleQuery();
+				ElMessage.success('发布成功');
+			}
+		})
+		.catch(() => {});
+};
 </script>
 
 <style scoped>

+ 309 - 0
doc/审批流集成开发指南.md

@@ -0,0 +1,309 @@
+# 审批流集成开发指南
+
+本文档说明如何将已有的审批流插件(`Admin.NET.Plugin.ApprovalFlow`)集成到业务模块中。以**订单评审(ORDER_REVIEW)**为实例,适用于所有需要接入审批流的业务场景。
+
+---
+
+## 一、整体架构
+
+审批流是一个独立插件,业务系统通过 **一个后端接口 + 一个前端组件** 与它对接,不需要编写任何审批相关的 API 或页面。
+
+```
+┌──────────────────────────────────┐
+│         业务模块(如订单管理)       │
+│                                  │
+│  ┌────────────────────────┐      │
+│  │  IFlowBizHandler 实现类  │◄─────── 引擎自动回调(审批发起/结束/节点完成)
+│  └────────────────────────┘      │
+│                                  │
+│  ┌────────────────────────┐      │         ┌──────────────────────────┐
+│  │   业务表单页面(Vue)     │      │         │    审批流插件(通用)       │
+│  │                        │      │         │                          │
+│  │  ┌──────────────────┐  │      │         │  FlowEngineService       │
+│  │  │  ApprovalPanel   │──┼──────┼────────►│  FlowTaskService API     │
+│  │  │  组件             │  │      │         │  FlowInstanceService API │
+│  │  └──────────────────┘  │      │         │  FlowNotifyService       │
+│  └────────────────────────┘      │         └──────────────────────────┘
+└──────────────────────────────────┘
+```
+
+**数据流:**
+
+1. 用户在业务表单点击"提交审批" → `ApprovalPanel` 调用通用 API `/api/flowInstance/start`
+2. 引擎根据 `BizType` 查找已发布的流程定义,创建流程实例和任务
+3. 引擎自动回调业务模块的 `IFlowBizHandler.OnFlowStarted()`,更新业务状态
+4. 审批人在"审批中心"或业务表单中操作(同意/拒绝/转办等)
+5. 流程结束时引擎自动回调 `IFlowBizHandler.OnFlowCompleted()`,更新业务最终状态
+
+---
+
+## 二、后端集成(共 2 步)
+
+### 第 1 步:添加项目引用
+
+在业务插件的 `.csproj` 文件中添加对审批流插件的引用:
+
+```xml
+<ProjectReference Include="..\Admin.NET.Plugin.ApprovalFlow\Admin.NET.Plugin.ApprovalFlow.csproj" />
+```
+
+### 第 2 步:实现 `IFlowBizHandler` 接口
+
+新建一个类,实现 `IFlowBizHandler` 接口并注册为 `ITransient`。
+
+#### 接口定义
+
+```csharp
+// 文件位置:server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/FlowEngine/IFlowBizHandler.cs
+
+public interface IFlowBizHandler
+{
+    /// 业务类型编码(与流程定义的 BizType 匹配)
+    string BizType { get; }
+
+    /// 流程发起后回调(建议:更新业务表状态为"审批中")
+    Task OnFlowStarted(long bizId, long instanceId) => Task.CompletedTask;
+
+    /// 流程结束后回调(必须:更新业务表最终状态)
+    Task OnFlowCompleted(long bizId, FlowInstanceStatusEnum finalStatus);
+
+    /// 单个节点审批完成回调(可选:按节点推进业务进度)
+    Task OnNodeCompleted(long bizId, string nodeId, string nodeName) => Task.CompletedTask;
+
+    /// 获取业务数据用于网关条件表达式求值(可选,仅条件分支时需要)
+    /// 返回 key-value 字典,key 对应条件表达式中的变量名
+    Task<Dictionary<string, object>> GetBizData(long bizId)
+        => Task.FromResult(new Dictionary<string, object>());
+}
+```
+
+#### 各方法说明
+
+| 方法 | 何时被引擎自动调用 | 你需要做什么 | 是否必须 |
+|---|---|---|---|
+| `BizType` | 引擎匹配 Handler 时 | 返回业务类型编码,如 `"ORDER_REVIEW"` | **必须** |
+| `OnFlowStarted` | 用户提交审批成功后 | 更新业务记录状态为"审批中" | 建议实现 |
+| `OnFlowCompleted` | 流程走完(通过/拒绝/撤销) | 根据结果更新业务记录最终状态 | **必须** |
+| `OnNodeCompleted` | 每个审批节点完成时 | 按节点推进业务进度(多数场景留空) | 可选 |
+| `GetBizData` | 流程经过条件网关时 | 返回业务字段供网关表达式判断 | 仅条件分支时需要 |
+
+#### 完整示例:订单评审
+
+```csharp
+// 文件位置:server/Plugins/Admin.NET.Plugin.AiDOP/Order/OrderReviewBizHandler.cs
+
+using Admin.NET.Plugin.ApprovalFlow;
+using Admin.NET.Plugin.ApprovalFlow.Service;
+
+namespace Admin.NET.Plugin.AiDOP.Order;
+
+public class OrderReviewBizHandler : IFlowBizHandler, ITransient
+{
+    public string BizType => "ORDER_REVIEW";
+
+    private readonly SqlSugarRepository<SeOrder> _seOrderRep;
+
+    public OrderReviewBizHandler(SqlSugarRepository<SeOrder> seOrderRep)
+    {
+        _seOrderRep = seOrderRep;
+    }
+
+    public async Task OnFlowStarted(long bizId, long instanceId)
+    {
+        // 审批发起 → 更新订单状态
+        await _seOrderRep.AsUpdateable()
+            .SetColumns(o => o.FlowState == "审批中")
+            .Where(o => o.Id == bizId)
+            .ExecuteCommandAsync();
+    }
+
+    public async Task OnFlowCompleted(long bizId, FlowInstanceStatusEnum finalStatus)
+    {
+        // 审批结束 → 根据结果更新订单最终状态
+        var state = finalStatus switch
+        {
+            FlowInstanceStatusEnum.Approved  => "已通过",
+            FlowInstanceStatusEnum.Rejected  => "已拒绝",
+            FlowInstanceStatusEnum.Cancelled => "已撤销",
+            _ => "已终止",
+        };
+        await _seOrderRep.AsUpdateable()
+            .SetColumns(o => o.FlowState == state)
+            .Where(o => o.Id == bizId)
+            .ExecuteCommandAsync();
+    }
+
+    public Task OnNodeCompleted(long bizId, string nodeId, string nodeName)
+    {
+        // 本场景不需要按节点推进,直接返回
+        return Task.CompletedTask;
+    }
+
+    public async Task<Dictionary<string, object>> GetBizData(long bizId)
+    {
+        // 为条件网关提供业务数据
+        // 网关表达式示例:urgent == 1 → 走加急审批线
+        var order = await _seOrderRep.GetByIdAsync(bizId);
+        if (order == null) return new Dictionary<string, object>();
+        return new Dictionary<string, object>
+        {
+            ["urgent"]      = order.Urgent ?? 0,
+            ["customLevel"] = order.CustomLevel ?? 0,
+            ["orderType"]   = order.OrderType ?? 0,
+        };
+    }
+}
+```
+
+#### 注意事项
+
+- 类必须同时实现 `IFlowBizHandler` 和 `ITransient`(Furion DI 自动注册)
+- `BizType` 字符串必须与"业务类型管理"中注册的编码**完全一致**
+- 引擎通过 DI 容器自动发现所有 `IFlowBizHandler` 实现,无需手动注册
+- 你**不需要编写任何审批 API**,同意/拒绝/转办/加签/退回/催办/撤回等操作全部由审批流插件的通用 API 提供
+
+---
+
+## 三、前端集成(共 1 步)
+
+在业务表单页面嵌入 `ApprovalPanel` 组件即可。
+
+### 使用方式
+
+```vue
+<script setup>
+import ApprovalPanel from '/@/views/approvalFlow/component/ApprovalPanel.vue';
+</script>
+
+<template>
+  <!-- 你的业务表单内容 -->
+  <div>...</div>
+
+  <!-- 审批面板 -->
+  <ApprovalPanel
+    v-if="form.id"
+    bizType="ORDER_REVIEW"
+    :bizId="form.id"
+    :bizNo="form.billNo"
+    :title="`订单评审-${form.billNo}`"
+    @refresh="loadDetail"
+  />
+</template>
+```
+
+### Props
+
+| 参数 | 类型 | 必填 | 说明 |
+|---|---|---|---|
+| `bizType` | string | 是 | 业务类型编码,与后端 Handler 的 `BizType` 一致 |
+| `bizId` | number | 是 | 业务记录主键(如订单 ID) |
+| `bizNo` | string | 否 | 业务单号,用于显示 |
+| `title` | string | 否 | 审批标题,如 `"订单评审-SO20260415001"` |
+| `currentUserId` | number | 否 | 当前登录用户 ID,用于控制"撤回""催办"按钮可见性 |
+
+### Events
+
+| 事件 | 触发时机 | 用途 |
+|---|---|---|
+| `@refresh` | 任何审批操作完成后 | 重新加载业务表单数据 |
+| `@flowStarted` | 审批提交成功后 | 可选,用于额外 UI 反馈 |
+| `@flowCompleted` | 流程走完后 | 可选,用于额外 UI 反馈 |
+
+### 组件自动处理的逻辑
+
+| 业务状态 | 组件自动行为 |
+|---|---|
+| 未发起审批 | 显示"提交审批"按钮 |
+| 已发起、当前用户是审批人 | 显示:同意、拒绝、转办、退回上一步、加签 |
+| 已发起、当前用户是发起人 | 显示:催办、撤回 |
+| 审批已结束 | 显示最终状态(已通过/已拒绝/已撤销)+ 完整审批时间线 |
+
+---
+
+## 四、配置步骤(管理员操作)
+
+代码集成完成后,管理员在前端页面进行以下配置:
+
+1. **注册业务类型**:进入 **Ai-DOP → 流程管理 → 业务类型**,添加编码(如 `ORDER_REVIEW`)和名称(如 `订单评审`)
+2. **新建审批流程**:进入 **Ai-DOP → 流程管理 → 审批流程**,点击"新增",选择业务类型为 `ORDER_REVIEW`
+3. **设计流程**:在流程列表点击"流程"进入设计器,拖拽节点配置审批人、条件分支等
+4. **发布流程**:设计完成后点击"发布"按钮
+
+发布后,用户在对应业务表单中即可看到"提交审批"按钮。
+
+---
+
+## 五、条件网关配置说明
+
+如果流程中包含"排他网关"(条件分支),需要在设计器中为网关的出线配置条件表达式。
+
+### 表达式语法
+
+```
+字段名 运算符 值
+```
+
+支持的运算符:`==`、`!=`、`>`、`<`、`>=`、`<=`
+
+多条件用 `&&` 连接:
+
+```
+urgent == 1 && customLevel >= 3
+```
+
+### 表达式中的变量
+
+变量来源于 `IFlowBizHandler.GetBizData()` 返回的字典 key。以订单评审为例:
+
+| 变量名 | 含义 | 示例表达式 |
+|---|---|---|
+| `urgent` | 是否加急(1=加急, 0=普通) | `urgent == 1` |
+| `customLevel` | 客户等级 | `customLevel >= 3` |
+| `orderType` | 订单类型(1=销售, 2=计划) | `orderType == 1` |
+
+### 默认路径
+
+将某条出线的条件设为 `default`(或留空),当其他条件均不满足时走该路径。
+
+---
+
+## 六、审批流通用 API 一览
+
+以下 API 由审批流插件统一提供,业务模块无需重复开发:
+
+### 流程实例
+
+| 方法 | 路径 | 说明 |
+|---|---|---|
+| POST | `/api/flowInstance/start` | 发起流程 |
+| GET | `/api/flowInstance/detail?id=` | 获取流程实例详情 |
+| GET | `/api/flowInstance/getByBiz?bizType=&bizId=` | 按业务查询流程实例 |
+| GET | `/api/flowInstance/timeline?instanceId=` | 获取审批时间线 |
+
+### 审批任务
+
+| 方法 | 路径 | 说明 |
+|---|---|---|
+| POST | `/api/flowTask/myPendingPage` | 我的待办(分页) |
+| POST | `/api/flowTask/myDonePage` | 我的已办(分页) |
+| POST | `/api/flowTask/myInitiatedPage` | 我发起的(分页) |
+| GET | `/api/flowTask/myPendingCount` | 待办数量 |
+| POST | `/api/flowTask/approve` | 同意 |
+| POST | `/api/flowTask/reject` | 拒绝 |
+| POST | `/api/flowTask/transfer` | 转办 |
+| POST | `/api/flowTask/returnToPrev` | 退回上一步 |
+| POST | `/api/flowTask/addSign` | 加签 |
+| POST | `/api/flowTask/withdraw` | 撤回 |
+| POST | `/api/flowTask/urge` | 催办 |
+
+---
+
+## 七、接入新业务的快速清单
+
+当你需要给另一个业务(如合同评审、采购审批、费用报销)接入审批流时,按以下清单操作:
+
+- [ ] **后端**:新建 `XxxBizHandler : IFlowBizHandler, ITransient`,实现 `BizType` + 回调方法
+- [ ] **前端**:在业务表单中嵌入 `<ApprovalPanel bizType="XXX" :bizId="record.id" />`
+- [ ] **配置**:在"业务类型管理"注册编码 → 在"审批流程"设计器画流程并发布
+
+无需新写 API、新建页面、新加路由。全部复用审批流插件的通用能力。

+ 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.11</AssemblyVersion>
-    <FileVersion>1.0.11</FileVersion>
-    <Version>1.0.11</Version>
+    <AssemblyVersion>1.0.13</AssemblyVersion>
+    <FileVersion>1.0.13</FileVersion>
+    <Version>1.0.13</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 1 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Admin.NET.Plugin.AiDOP.csproj

@@ -12,6 +12,7 @@
 
   <ItemGroup>
     <ProjectReference Include="..\..\Admin.NET.Core\Admin.NET.Core.csproj" />
+    <ProjectReference Include="..\Admin.NET.Plugin.ApprovalFlow\Admin.NET.Plugin.ApprovalFlow.csproj" />
   </ItemGroup>
 
   <ItemGroup>

+ 31 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Order/ContractReviewBizHandler.cs

@@ -0,0 +1,31 @@
+using Admin.NET.Plugin.ApprovalFlow;
+using Admin.NET.Plugin.ApprovalFlow.Service;
+
+namespace Admin.NET.Plugin.AiDOP.Order;
+
+/// <summary>
+/// 合同评审审批业务回调
+/// 注册 BizType = "CONTRACT_REVIEW"
+/// 待合同实体创建后补齐具体逻辑
+/// </summary>
+public class ContractReviewBizHandler : IFlowBizHandler, ITransient
+{
+    public string BizType => "CONTRACT_REVIEW";
+
+    public Task OnFlowStarted(long bizId, long instanceId)
+    {
+        // TODO: 更新合同状态为"审批中"
+        return Task.CompletedTask;
+    }
+
+    public Task OnFlowCompleted(long bizId, FlowInstanceStatusEnum finalStatus)
+    {
+        // TODO: 更新合同最终状态
+        return Task.CompletedTask;
+    }
+
+    public Task OnNodeCompleted(long bizId, string nodeId, string nodeName)
+    {
+        return Task.CompletedTask;
+    }
+}

+ 72 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Order/OrderReviewBizHandler.cs

@@ -0,0 +1,72 @@
+using Admin.NET.Plugin.ApprovalFlow;
+using Admin.NET.Plugin.ApprovalFlow.Service;
+
+namespace Admin.NET.Plugin.AiDOP.Order;
+
+/// <summary>
+/// 订单评审审批业务回调
+/// 注册 BizType = "ORDER_REVIEW"
+/// </summary>
+public class OrderReviewBizHandler : IFlowBizHandler, ITransient
+{
+    public string BizType => "ORDER_REVIEW";
+
+    private readonly SqlSugarRepository<SeOrder> _seOrderRep;
+
+    public OrderReviewBizHandler(SqlSugarRepository<SeOrder> seOrderRep)
+    {
+        _seOrderRep = seOrderRep;
+    }
+
+    public async Task OnFlowStarted(long bizId, long instanceId)
+    {
+        try
+        {
+            await _seOrderRep.AsUpdateable()
+                .SetColumns(o => o.FlowState == "审批中")
+                .Where(o => o.Id == bizId)
+                .ExecuteCommandAsync();
+        }
+        catch (Exception) { /* 表不存在时静默跳过 */ }
+    }
+
+    public async Task OnFlowCompleted(long bizId, FlowInstanceStatusEnum finalStatus)
+    {
+        var state = finalStatus switch
+        {
+            FlowInstanceStatusEnum.Approved => "已通过",
+            FlowInstanceStatusEnum.Rejected => "已拒绝",
+            FlowInstanceStatusEnum.Cancelled => "已撤销",
+            _ => "已终止",
+        };
+        try
+        {
+            await _seOrderRep.AsUpdateable()
+                .SetColumns(o => o.FlowState == state)
+                .Where(o => o.Id == bizId)
+                .ExecuteCommandAsync();
+        }
+        catch (Exception) { /* 表不存在时静默跳过 */ }
+    }
+
+    public Task OnNodeCompleted(long bizId, string nodeId, string nodeName)
+    {
+        return Task.CompletedTask;
+    }
+
+    public async Task<Dictionary<string, object>> GetBizData(long bizId)
+    {
+        try
+        {
+            var order = await _seOrderRep.GetByIdAsync(bizId);
+            if (order == null) return new Dictionary<string, object>();
+            return new Dictionary<string, object>
+            {
+                ["urgent"] = order.Urgent ?? 0,
+                ["customLevel"] = order.CustomLevel ?? 0,
+                ["orderType"] = order.OrderType ?? 0,
+            };
+        }
+        catch (Exception) { return new Dictionary<string, object>(); }
+    }
+}

+ 48 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Order/SeOrderService.cs

@@ -433,4 +433,52 @@ public class SeOrderService : IDynamicApiController, ITransient
         public string? Extra { get; set; }
         public long? Id { get; set; }
     }
+
+    // ══════════════════════════════════════════════════════════════
+    // 提交订单评审审批
+    // ══════════════════════════════════════════════════════════════
+
+    /// <summary>提交订单评审审批流程</summary>
+    [DisplayName("提交订单评审审批")]
+    [HttpPost("seorder/submitReview")]
+    public async Task<object> SubmitOrderReview([FromBody] SubmitReviewInput input)
+    {
+        var order = await _seOrderRep.GetByIdAsync(input.OrderId)
+            ?? throw Oops.Oh("订单不存在");
+        var engine = App.GetRequiredService<Admin.NET.Plugin.ApprovalFlow.Service.FlowEngineService>();
+        var instanceId = await engine.StartFlow(new Admin.NET.Plugin.ApprovalFlow.Service.StartFlowInput
+        {
+            BizType = "ORDER_REVIEW",
+            BizId = order.Id,
+            BizNo = order.BillNo,
+            Title = $"订单评审-{order.BillNo}",
+            Comment = input.Comment,
+        });
+        return new { instanceId };
+    }
+
+    /// <summary>提交合同评审审批流程</summary>
+    [DisplayName("提交合同评审审批")]
+    [HttpPost("seorder/submitContractReview")]
+    public async Task<object> SubmitContractReview([FromBody] SubmitReviewInput input)
+    {
+        var order = await _seOrderRep.GetByIdAsync(input.OrderId)
+            ?? throw Oops.Oh("订单不存在");
+        var engine = App.GetRequiredService<Admin.NET.Plugin.ApprovalFlow.Service.FlowEngineService>();
+        var instanceId = await engine.StartFlow(new Admin.NET.Plugin.ApprovalFlow.Service.StartFlowInput
+        {
+            BizType = "CONTRACT_REVIEW",
+            BizId = order.Id,
+            BizNo = order.BillNo,
+            Title = $"合同评审-{order.BillNo}",
+            Comment = input.Comment,
+        });
+        return new { instanceId };
+    }
+
+    public class SubmitReviewInput
+    {
+        public long OrderId { get; set; }
+        public string? Comment { get; set; }
+    }
 }

+ 9 - 1
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Configuration/ApprovalFlow.json

@@ -9,5 +9,13 @@
     "Order": 100
   },
   "ApprovalFlow": {
+    "AuditEnabled": false,
+    "Notify": {
+      "SignalR": true,
+      "DingTalk": false,
+      "WorkWeixin": false,
+      "Email": false,
+      "Sms": false
+    }
   }
-}
+}

+ 36 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Entity/ApprovalBizType.cs

@@ -0,0 +1,36 @@
+namespace Admin.NET.Plugin.ApprovalFlow;
+
+/// <summary>
+/// 审批业务类型表
+/// </summary>
+[SugarTable(null, "审批业务类型表")]
+[SugarIndex("uk_biztypecode", nameof(Code), OrderByType.Asc, true)]
+public class ApprovalBizType : EntityBase
+{
+    /// <summary>
+    /// 业务类型编码(唯一)
+    /// </summary>
+    [SugarColumn(ColumnDescription = "业务类型编码", Length = 64)]
+    [Required, MaxLength(64)]
+    public string Code { get; set; } = "";
+
+    /// <summary>
+    /// 业务类型名称
+    /// </summary>
+    [SugarColumn(ColumnDescription = "业务类型名称", Length = 128)]
+    [Required, MaxLength(128)]
+    public string Name { 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/ApprovalFlow.cs

@@ -50,4 +50,23 @@ public class ApprovalFlow : EntityBaseOrgDel
     [SugarColumn(ColumnDescription = "备注", Length = 256)]
     [MaxLength(256)]
     public string? Remark { get; set; }
+
+    /// <summary>
+    /// 业务类型编码(如 CONTRACT_REVIEW、ORDER_REVIEW)
+    /// </summary>
+    [SugarColumn(ColumnDescription = "业务类型编码", Length = 64, IsNullable = true)]
+    [MaxLength(64)]
+    public string? BizType { get; set; }
+
+    /// <summary>
+    /// 版本号,每次发布 +1
+    /// </summary>
+    [SugarColumn(ColumnDescription = "版本号", DefaultValue = "0")]
+    public int Version { get; set; }
+
+    /// <summary>
+    /// 是否已发布
+    /// </summary>
+    [SugarColumn(ColumnDescription = "是否已发布", DefaultValue = "0")]
+    public bool IsPublished { get; set; }
 }

+ 93 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Entity/ApprovalFlowInstance.cs

@@ -0,0 +1,93 @@
+namespace Admin.NET.Plugin.ApprovalFlow;
+
+/// <summary>
+/// 审批流程实例表
+/// </summary>
+[SugarTable(null, "审批流程实例表")]
+[SugarIndex("idx_flowinstance_biztype_bizid", nameof(BizType), OrderByType.Asc, nameof(BizId), OrderByType.Asc)]
+[SugarIndex("idx_flowinstance_initiator", nameof(InitiatorId), OrderByType.Asc)]
+public class ApprovalFlowInstance : EntityBaseOrg
+{
+    /// <summary>
+    /// 关联流程定义 Id
+    /// </summary>
+    [SugarColumn(ColumnDescription = "流程定义Id")]
+    public long FlowId { get; set; }
+
+    /// <summary>
+    /// 发起时的流程版本
+    /// </summary>
+    [SugarColumn(ColumnDescription = "流程版本")]
+    public int FlowVersion { get; set; }
+
+    /// <summary>
+    /// 业务类型编码
+    /// </summary>
+    [SugarColumn(ColumnDescription = "业务类型编码", Length = 64)]
+    [MaxLength(64)]
+    public string BizType { get; set; } = "";
+
+    /// <summary>
+    /// 关联业务单据 Id
+    /// </summary>
+    [SugarColumn(ColumnDescription = "业务单据Id")]
+    public long BizId { get; set; }
+
+    /// <summary>
+    /// 业务单号(冗余)
+    /// </summary>
+    [SugarColumn(ColumnDescription = "业务单号", Length = 128, IsNullable = true)]
+    [MaxLength(128)]
+    public string? BizNo { get; set; }
+
+    /// <summary>
+    /// 流程标题
+    /// </summary>
+    [SugarColumn(ColumnDescription = "流程标题", Length = 256)]
+    [MaxLength(256)]
+    public string Title { get; set; } = "";
+
+    /// <summary>
+    /// 发起人 Id
+    /// </summary>
+    [SugarColumn(ColumnDescription = "发起人Id")]
+    public long InitiatorId { get; set; }
+
+    /// <summary>
+    /// 发起人姓名(冗余)
+    /// </summary>
+    [SugarColumn(ColumnDescription = "发起人姓名", Length = 64, IsNullable = true)]
+    [MaxLength(64)]
+    public string? InitiatorName { get; set; }
+
+    /// <summary>
+    /// 流程状态
+    /// </summary>
+    [SugarColumn(ColumnDescription = "流程状态")]
+    public FlowInstanceStatusEnum Status { get; set; }
+
+    /// <summary>
+    /// 当前所在节点 Id
+    /// </summary>
+    [SugarColumn(ColumnDescription = "当前节点Id", Length = 64, IsNullable = true)]
+    [MaxLength(64)]
+    public string? CurrentNodeId { get; set; }
+
+    /// <summary>
+    /// 发起时的完整 FlowJson 快照(含节点属性)
+    /// </summary>
+    [SugarColumn(ColumnDescription = "FlowJson快照", ColumnDataType = StaticConfig.CodeFirst_BigString, IsNullable = true)]
+    public string? FlowJsonSnapshot { get; set; }
+
+    /// <summary>
+    /// 发起时间
+    /// </summary>
+    [SugarColumn(ColumnDescription = "发起时间")]
+    public DateTime StartTime { get; set; }
+
+    /// <summary>
+    /// 结束时间
+    /// </summary>
+    [SugarColumn(ColumnDescription = "结束时间", IsNullable = true)]
+    public DateTime? EndTime { get; set; }
+}

+ 54 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Entity/ApprovalFlowLog.cs

@@ -0,0 +1,54 @@
+namespace Admin.NET.Plugin.ApprovalFlow;
+
+/// <summary>
+/// 审批流程操作日志表
+/// </summary>
+[SugarTable(null, "审批流程操作日志表")]
+[SugarIndex("idx_flowlog_instance", nameof(InstanceId), OrderByType.Asc)]
+public class ApprovalFlowLog : EntityBase
+{
+    /// <summary>
+    /// 关联流程实例 Id
+    /// </summary>
+    [SugarColumn(ColumnDescription = "流程实例Id")]
+    public long InstanceId { get; set; }
+
+    /// <summary>
+    /// 关联任务 Id(可空,Submit 无 Task)
+    /// </summary>
+    [SugarColumn(ColumnDescription = "任务Id", IsNullable = true)]
+    public long? TaskId { get; set; }
+
+    /// <summary>
+    /// 节点 Id
+    /// </summary>
+    [SugarColumn(ColumnDescription = "节点Id", Length = 64, IsNullable = true)]
+    [MaxLength(64)]
+    public string? NodeId { get; set; }
+
+    /// <summary>
+    /// 操作动作
+    /// </summary>
+    [SugarColumn(ColumnDescription = "操作动作")]
+    public FlowLogActionEnum Action { get; set; }
+
+    /// <summary>
+    /// 操作人 Id
+    /// </summary>
+    [SugarColumn(ColumnDescription = "操作人Id")]
+    public long OperatorId { get; set; }
+
+    /// <summary>
+    /// 操作人姓名
+    /// </summary>
+    [SugarColumn(ColumnDescription = "操作人姓名", Length = 64, IsNullable = true)]
+    [MaxLength(64)]
+    public string? OperatorName { get; set; }
+
+    /// <summary>
+    /// 操作说明/审批意见
+    /// </summary>
+    [SugarColumn(ColumnDescription = "审批意见", Length = 1024, IsNullable = true)]
+    [MaxLength(1024)]
+    public string? Comment { get; set; }
+}

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

@@ -0,0 +1,80 @@
+namespace Admin.NET.Plugin.ApprovalFlow;
+
+/// <summary>
+/// 审批任务表
+/// </summary>
+[SugarTable(null, "审批任务表")]
+[SugarIndex("idx_flowtask_instance", nameof(InstanceId), OrderByType.Asc)]
+[SugarIndex("idx_flowtask_assignee_status", nameof(AssigneeId), OrderByType.Asc, nameof(Status), OrderByType.Asc)]
+public class ApprovalFlowTask : EntityBaseOrg
+{
+    /// <summary>
+    /// 关联流程实例 Id
+    /// </summary>
+    [SugarColumn(ColumnDescription = "流程实例Id")]
+    public long InstanceId { get; set; }
+
+    /// <summary>
+    /// 对应 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>
+    /// 指派审批人 Id
+    /// </summary>
+    [SugarColumn(ColumnDescription = "审批人Id")]
+    public long AssigneeId { get; set; }
+
+    /// <summary>
+    /// 审批人姓名(冗余)
+    /// </summary>
+    [SugarColumn(ColumnDescription = "审批人姓名", Length = 64, IsNullable = true)]
+    [MaxLength(64)]
+    public string? AssigneeName { get; set; }
+
+    /// <summary>
+    /// 任务状态
+    /// </summary>
+    [SugarColumn(ColumnDescription = "任务状态")]
+    public FlowTaskStatusEnum Status { get; set; }
+
+    /// <summary>
+    /// 审批意见
+    /// </summary>
+    [SugarColumn(ColumnDescription = "审批意见", Length = 1024, IsNullable = true)]
+    [MaxLength(1024)]
+    public string? Comment { get; set; }
+
+    /// <summary>
+    /// 操作时间
+    /// </summary>
+    [SugarColumn(ColumnDescription = "操作时间", IsNullable = true)]
+    public DateTime? ActionTime { get; set; }
+
+    /// <summary>
+    /// 转办目标人 Id
+    /// </summary>
+    [SugarColumn(ColumnDescription = "转办目标人Id", IsNullable = true)]
+    public long? TransferToId { get; set; }
+
+    /// <summary>
+    /// 是否为加签产生的任务
+    /// </summary>
+    [SugarColumn(ColumnDescription = "是否加签任务", DefaultValue = "0")]
+    public bool IsAddSign { get; set; }
+
+    /// <summary>
+    /// 加签发起人 Id
+    /// </summary>
+    [SugarColumn(ColumnDescription = "加签发起人Id", IsNullable = true)]
+    public long? AddSignById { get; set; }
+}

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

@@ -0,0 +1,20 @@
+namespace Admin.NET.Plugin.ApprovalFlow;
+
+/// <summary>
+/// 审批人类型枚举
+/// </summary>
+[Description("审批人类型枚举")]
+public enum ApproverTypeEnum
+{
+    [Description("指定用户")]
+    SpecificUser = 1,
+
+    [Description("指定角色")]
+    Role = 2,
+
+    [Description("指定部门")]
+    Department = 3,
+
+    [Description("发起人")]
+    Initiator = 4,
+}

+ 26 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Enum/FlowInstanceStatusEnum.cs

@@ -0,0 +1,26 @@
+namespace Admin.NET.Plugin.ApprovalFlow;
+
+/// <summary>
+/// 流程实例状态枚举
+/// </summary>
+[Description("流程实例状态枚举")]
+public enum FlowInstanceStatusEnum
+{
+    [Description("草稿")]
+    Draft = 0,
+
+    [Description("审批中")]
+    Running = 1,
+
+    [Description("已通过")]
+    Approved = 2,
+
+    [Description("已拒绝")]
+    Rejected = 3,
+
+    [Description("已撤销")]
+    Cancelled = 4,
+
+    [Description("已终止")]
+    Terminated = 5,
+}

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

@@ -0,0 +1,38 @@
+namespace Admin.NET.Plugin.ApprovalFlow;
+
+/// <summary>
+/// 流程操作日志动作枚举
+/// </summary>
+[Description("流程操作日志动作枚举")]
+public enum FlowLogActionEnum
+{
+    [Description("提交")]
+    Submit = 1,
+
+    [Description("同意")]
+    Approve = 2,
+
+    [Description("拒绝")]
+    Reject = 3,
+
+    [Description("转办")]
+    Transfer = 4,
+
+    [Description("撤回")]
+    Withdraw = 5,
+
+    [Description("退回")]
+    Return = 6,
+
+    [Description("加签")]
+    AddSign = 7,
+
+    [Description("催办")]
+    Urge = 8,
+
+    [Description("取消")]
+    Cancel = 9,
+
+    [Description("超时自动")]
+    AutoTimeout = 10,
+}

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

@@ -0,0 +1,26 @@
+namespace Admin.NET.Plugin.ApprovalFlow;
+
+/// <summary>
+/// 审批任务状态枚举
+/// </summary>
+[Description("审批任务状态枚举")]
+public enum FlowTaskStatusEnum
+{
+    [Description("待审批")]
+    Pending = 0,
+
+    [Description("已同意")]
+    Approved = 1,
+
+    [Description("已拒绝")]
+    Rejected = 2,
+
+    [Description("已转办")]
+    Transferred = 3,
+
+    [Description("已取消")]
+    Cancelled = 4,
+
+    [Description("已退回")]
+    Returned = 5,
+}

+ 14 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Enum/MultiApproveModeEnum.cs

@@ -0,0 +1,14 @@
+namespace Admin.NET.Plugin.ApprovalFlow;
+
+/// <summary>
+/// 多人审批模式枚举
+/// </summary>
+[Description("多人审批模式枚举")]
+public enum MultiApproveModeEnum
+{
+    [Description("或签(一人通过即可)")]
+    Any = 1,
+
+    [Description("会签(所有人通过)")]
+    All = 2,
+}

+ 8 - 21
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Middleware/ApprovalFlowMiddleware.cs

@@ -1,25 +1,11 @@
-// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
-//
-// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
-//
-// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
-
 using Admin.NET.Plugin.ApprovalFlow.Service;
 using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Http;
 
 namespace Admin.NET.Plugin.ApprovalFlow;
 
-/// <summary>
-/// 扩展审批流中间件
-/// </summary>
 public static class ApprovalFlowMiddlewareExtensions
 {
-    /// <summary>
-    /// 使用审批流
-    /// </summary>
-    /// <param name="builder"></param>
-    /// <returns></returns>
     public static IApplicationBuilder UseApprovalFlow(this IApplicationBuilder builder)
     {
         return builder.UseMiddleware<ApprovalFlowMiddleware>();
@@ -27,24 +13,25 @@ public static class ApprovalFlowMiddlewareExtensions
 }
 
 /// <summary>
-/// 审批流中间件
+/// 审批流中间件 — 可选审计模式(B8 fix: 改用 InvokeAsync 参数注入)
 /// </summary>
 public class ApprovalFlowMiddleware
 {
     private readonly RequestDelegate _next;
-    private readonly SysApprovalService _sysApprovalService;
 
     public ApprovalFlowMiddleware(RequestDelegate next)
     {
         _next = next;
-        _sysApprovalService = App.GetRequiredService<SysApprovalService>();
     }
 
-    public async Task InvokeAsync(HttpContext context)
+    public async Task InvokeAsync(HttpContext context, SysApprovalService sysApprovalService)
     {
-        await _sysApprovalService.MatchApproval(context);
+        var enabled = App.GetConfig<bool>("ApprovalFlow:AuditEnabled", false);
+        if (enabled)
+        {
+            await sysApprovalService.MatchApproval(context);
+        }
 
-        // 调用下一个中间件
         await _next(context);
     }
-}
+}

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

@@ -19,7 +19,10 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
     {
         return new[]
         {
-            new SysMenu{ Id=1310300010101, Pid=1300300000101, Title="审批流程", Path="/platform/approvalFlow", Name="approvalFlow", Component="/approvalFlow/index", Icon="ele-Help", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=2000 },
+            new SysMenu{ Id=1310300010100, Pid=1320990000101, Title="流程管理", Path="/aidop/flowManage", Name="aidopFlowManage", Component="Layout", Icon="ele-SetUp", Type=MenuTypeEnum.Dir, CreateTime=DateTime.Parse("2026-04-15 00:00:00"), OrderNo=275 },
+            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 },
         };
     }
 }

+ 50 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/ApprovalFlow/ApprovalFlowService.cs

@@ -122,6 +122,56 @@ public class ApprovalFlowService : IDynamicApiController, ITransient
         return prefix + DateTime.Now.ToString("yyMMdd") + string.Format("{0:d2}", count + 1);
     }
 
+    /// <summary>
+    /// 发布流程(Version+1, IsPublished=true)
+    /// </summary>
+    [ApiDescriptionSettings(Name = "Publish"), HttpPost]
+    [DisplayName("发布审批流")]
+    public async Task Publish([FromBody] BaseIdInput input)
+    {
+        var entity = await _approvalFlowRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
+        if (string.IsNullOrWhiteSpace(entity.FlowJson))
+            throw Oops.Oh("请先设计流程图再发布");
+        if (string.IsNullOrWhiteSpace(entity.BizType))
+            throw Oops.Oh("请先设置业务类型再发布");
+
+        entity.Version += 1;
+        entity.IsPublished = true;
+        await _approvalFlowRep.AsUpdateable(entity)
+            .UpdateColumns(u => new { u.Version, u.IsPublished })
+            .ExecuteCommandAsync();
+    }
+
+    /// <summary>
+    /// 获取业务类型列表
+    /// </summary>
+    [HttpGet]
+    [ApiDescriptionSettings(Name = "BizTypes")]
+    [DisplayName("获取业务类型列表")]
+    public async Task<List<string>> GetBizTypes()
+    {
+        return await _approvalFlowRep.AsQueryable()
+            .Where(u => !string.IsNullOrWhiteSpace(u.BizType) && !u.IsDelete)
+            .Select(u => u.BizType!)
+            .Distinct()
+            .ToListAsync();
+    }
+
+    /// <summary>
+    /// 获取指定业务类型的最新已发布流程定义
+    /// </summary>
+    [HttpGet]
+    [ApiDescriptionSettings(Name = "PublishedByBizType")]
+    [DisplayName("获取已发布流程")]
+    public async Task<ApprovalFlowOutput?> GetPublishedByBizType([FromQuery] string bizType)
+    {
+        return await _approvalFlowRep.AsQueryable()
+            .Where(u => u.BizType == bizType && u.IsPublished && !u.IsDelete)
+            .OrderByDescending(u => u.Version)
+            .Select<ApprovalFlowOutput>()
+            .FirstAsync();
+    }
+
     [HttpGet]
     [ApiDescriptionSettings(Name = "FlowList")]
     [DisplayName("获取审批流结构")]

+ 5 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/ApprovalFlow/Dto/ApprovalFlowInput.cs

@@ -36,6 +36,11 @@ public class ApprovalFlowBaseInput
     /// </summary>
     public virtual string? Remark { get; set; }
 
+    /// <summary>
+    /// 业务类型编码
+    /// </summary>
+    public virtual string? BizType { get; set; }
+
     /// <summary>
     /// 创建时间
     /// </summary>

+ 73 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/ApprovalFlow/Dto/ApprovalFlowItem.cs

@@ -70,8 +70,81 @@ public class ApprovalFlowEdgeItem
     public List<FlowEdgePointItem> PointsList { get; set; }
 }
 
+/// <summary>
+/// 节点属性(存储在 FlowJson 中每个 node/edge 的 properties)
+/// 向后兼容:所有字段可空,旧数据反序列化不会报错
+/// </summary>
 public class FlowProperties
 {
+    // ── 用户任务节点属性 ──
+
+    [JsonPropertyName("nodeName")]
+    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+    public string? NodeName { get; set; }
+
+    /// <summary>
+    /// 审批人类型:SpecificUser / Role / Department / Initiator
+    /// </summary>
+    [JsonPropertyName("approverType")]
+    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+    public string? ApproverType { get; set; }
+
+    /// <summary>
+    /// 审批人/角色/部门 Id,逗号分隔
+    /// </summary>
+    [JsonPropertyName("approverIds")]
+    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+    public string? ApproverIds { get; set; }
+
+    /// <summary>
+    /// 审批人/角色/部门名称,逗号分隔(冗余,便于前端展示)
+    /// </summary>
+    [JsonPropertyName("approverNames")]
+    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+    public string? ApproverNames { get; set; }
+
+    /// <summary>
+    /// 多人审批模式:Any(或签) / All(会签)
+    /// </summary>
+    [JsonPropertyName("multiApproveMode")]
+    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+    public string? MultiApproveMode { get; set; }
+
+    [JsonPropertyName("timeoutHours")]
+    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+    public int? TimeoutHours { get; set; }
+
+    /// <summary>
+    /// 超时动作:AutoApprove / AutoReject / Notify
+    /// </summary>
+    [JsonPropertyName("timeoutAction")]
+    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+    public string? TimeoutAction { get; set; }
+
+    // ── 网关节点属性 ──
+
+    [JsonPropertyName("conditions")]
+    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+    public List<GatewayCondition>? Conditions { get; set; }
+}
+
+/// <summary>
+/// 排他网关条件分支
+/// </summary>
+public class GatewayCondition
+{
+    [JsonPropertyName("targetNodeId")]
+    public string TargetNodeId { get; set; } = "";
+
+    [JsonPropertyName("expression")]
+    public string Expression { get; set; } = "";
+
+    [JsonPropertyName("label")]
+    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+    public string? Label { get; set; }
+
+    [JsonPropertyName("isDefault")]
+    public bool IsDefault { get; set; }
 }
 
 public class FlowTextItem

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

@@ -41,6 +41,21 @@ public class ApprovalFlowOutput
     /// </summary>
     public string? Remark { get; set; }
 
+    /// <summary>
+    /// 业务类型编码
+    /// </summary>
+    public string? BizType { get; set; }
+
+    /// <summary>
+    /// 版本号
+    /// </summary>
+    public int Version { get; set; }
+
+    /// <summary>
+    /// 是否已发布
+    /// </summary>
+    public bool IsPublished { get; set; }
+
     /// <summary>
     /// 创建时间
     /// </summary>

+ 145 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/BizType/BizTypeService.cs

@@ -0,0 +1,145 @@
+namespace Admin.NET.Plugin.ApprovalFlow.Service;
+
+/// <summary>
+/// 审批业务类型管理服务
+/// </summary>
+[ApiDescriptionSettings(ApprovalFlowConst.GroupName, Order = 80)]
+public class BizTypeService : IDynamicApiController, ITransient
+{
+    private readonly SqlSugarRepository<ApprovalBizType> _rep;
+    private readonly SqlSugarRepository<ApprovalFlow> _flowRep;
+
+    public BizTypeService(
+        SqlSugarRepository<ApprovalBizType> rep,
+        SqlSugarRepository<ApprovalFlow> flowRep)
+    {
+        _rep = rep;
+        _flowRep = flowRep;
+    }
+
+    /// <summary>
+    /// 获取启用的业务类型列表(合并 DB + 代码注册)
+    /// </summary>
+    [HttpGet]
+    [ApiDescriptionSettings(Name = "List")]
+    [DisplayName("获取业务类型列表")]
+    public async Task<List<BizTypeItemOutput>> GetList()
+    {
+        var dbItems = await _rep.AsQueryable()
+            .Where(u => u.IsEnabled)
+            .OrderBy(u => u.Code)
+            .ToListAsync();
+
+        var result = dbItems.Select(u => new BizTypeItemOutput
+        {
+            Code = u.Code,
+            Name = u.Name,
+            Remark = u.Remark,
+            Source = "manual",
+        }).ToList();
+
+        var handlers = App.GetServices<IFlowBizHandler>();
+        if (handlers != null)
+        {
+            foreach (var h in handlers)
+            {
+                if (result.All(r => r.Code != h.BizType))
+                {
+                    result.Add(new BizTypeItemOutput
+                    {
+                        Code = h.BizType,
+                        Name = h.BizType,
+                        Source = "code",
+                    });
+                }
+                else
+                {
+                    var existing = result.First(r => r.Code == h.BizType);
+                    existing.Source = "code";
+                }
+            }
+        }
+
+        return result.OrderBy(r => r.Code).ToList();
+    }
+
+    /// <summary>
+    /// 分页查询业务类型
+    /// </summary>
+    [HttpPost]
+    [ApiDescriptionSettings(Name = "Page")]
+    [DisplayName("分页查询业务类型")]
+    public async Task<SqlSugarPagedList<BizTypePageOutput>> Page(BizTypePageInput input)
+    {
+        var dbPage = await _rep.AsQueryable()
+            .WhereIF(!string.IsNullOrWhiteSpace(input.Code), u => u.Code.Contains(input.Code!))
+            .WhereIF(!string.IsNullOrWhiteSpace(input.Name), u => u.Name.Contains(input.Name!))
+            .OrderBy(u => u.Id)
+            .ToPagedListAsync(input.Page, input.PageSize);
+
+        var handlers = App.GetServices<IFlowBizHandler>();
+        var handlerCodes = handlers?.Select(h => h.BizType).ToHashSet() ?? new HashSet<string>();
+
+        var result = dbPage.Adapt<SqlSugarPagedList<BizTypePageOutput>>();
+        foreach (var item in result.Items)
+        {
+            item.Source = handlerCodes.Contains(item.Code) ? "code" : "manual";
+        }
+        return result;
+    }
+
+    /// <summary>
+    /// 新增业务类型
+    /// </summary>
+    [HttpPost]
+    [ApiDescriptionSettings(Name = "Add")]
+    [DisplayName("新增业务类型")]
+    public async Task<long> Add(BizTypeAddInput input)
+    {
+        var exists = await _rep.AsQueryable().AnyAsync(u => u.Code == input.Code);
+        if (exists) throw Oops.Oh($"业务类型编码 [{input.Code}] 已存在");
+
+        var entity = input.Adapt<ApprovalBizType>();
+        await _rep.InsertAsync(entity);
+        return entity.Id;
+    }
+
+    /// <summary>
+    /// 更新业务类型
+    /// </summary>
+    [HttpPost]
+    [ApiDescriptionSettings(Name = "Update")]
+    [DisplayName("更新业务类型")]
+    public async Task Update(BizTypeUpdateInput input)
+    {
+        var entity = await _rep.GetByIdAsync(input.Id)
+            ?? throw Oops.Oh("业务类型不存在");
+
+        var codeConflict = await _rep.AsQueryable()
+            .AnyAsync(u => u.Code == input.Code && u.Id != input.Id);
+        if (codeConflict) throw Oops.Oh($"业务类型编码 [{input.Code}] 已被占用");
+
+        input.Adapt(entity);
+        await _rep.AsUpdateable(entity)
+            .IgnoreColumns(ignoreAllNullColumns: true)
+            .ExecuteCommandAsync();
+    }
+
+    /// <summary>
+    /// 删除业务类型
+    /// </summary>
+    [HttpPost]
+    [ApiDescriptionSettings(Name = "Delete")]
+    [DisplayName("删除业务类型")]
+    public async Task Delete(BaseIdInput input)
+    {
+        var entity = await _rep.GetByIdAsync(input.Id)
+            ?? throw Oops.Oh("业务类型不存在");
+
+        var used = await _flowRep.AsQueryable()
+            .AnyAsync(u => u.BizType == entity.Code && !u.IsDelete);
+        if (used) throw Oops.Oh($"业务类型 [{entity.Code}] 已被审批流程引用,不可删除");
+
+        await _rep.DeleteByIdAsync(input.Id);
+    }
+}

+ 86 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/BizType/Dto/BizTypeDtos.cs

@@ -0,0 +1,86 @@
+namespace Admin.NET.Plugin.ApprovalFlow.Service;
+
+/// <summary>
+/// 业务类型列表项输出
+/// </summary>
+public class BizTypeItemOutput
+{
+    /// <summary>
+    /// 业务类型编码
+    /// </summary>
+    public string Code { get; set; } = "";
+
+    /// <summary>
+    /// 业务类型名称
+    /// </summary>
+    public string Name { get; set; } = "";
+
+    /// <summary>
+    /// 备注
+    /// </summary>
+    public string? Remark { get; set; }
+
+    /// <summary>
+    /// 来源:code=代码注册, manual=手动添加
+    /// </summary>
+    public string Source { get; set; } = "manual";
+}
+
+/// <summary>
+/// 分页查询输入
+/// </summary>
+public class BizTypePageInput : BasePageInput
+{
+    public string? Code { get; set; }
+    public string? Name { get; set; }
+}
+
+/// <summary>
+/// 分页输出
+/// </summary>
+public class BizTypePageOutput
+{
+    public long Id { get; set; }
+    public string Code { get; set; } = "";
+    public string Name { get; set; } = "";
+    public string? Remark { get; set; }
+    public bool IsEnabled { get; set; }
+    public DateTime? CreateTime { get; set; }
+
+    /// <summary>
+    /// 来源:code / manual
+    /// </summary>
+    public string Source { get; set; } = "manual";
+}
+
+/// <summary>
+/// 新增输入
+/// </summary>
+public class BizTypeAddInput
+{
+    /// <summary>
+    /// 业务类型编码
+    /// </summary>
+    [Required(ErrorMessage = "编码不能为空")]
+    [MaxLength(64)]
+    public string Code { get; set; } = "";
+
+    /// <summary>
+    /// 业务类型名称
+    /// </summary>
+    [Required(ErrorMessage = "名称不能为空")]
+    [MaxLength(128)]
+    public string Name { get; set; } = "";
+
+    public string? Remark { get; set; }
+    public bool IsEnabled { get; set; } = true;
+}
+
+/// <summary>
+/// 更新输入
+/// </summary>
+public class BizTypeUpdateInput : BizTypeAddInput
+{
+    [Required(ErrorMessage = "Id不能为空")]
+    public long Id { get; set; }
+}

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

@@ -0,0 +1,104 @@
+namespace Admin.NET.Plugin.ApprovalFlow.Service;
+
+/// <summary>
+/// 发起流程输入
+/// </summary>
+public class StartFlowInput
+{
+    /// <summary>
+    /// 业务类型编码(与流程定义的 BizType 匹配)
+    /// </summary>
+    [Required(ErrorMessage = "业务类型不能为空")]
+    public string BizType { get; set; } = "";
+
+    /// <summary>
+    /// 关联业务单据 Id
+    /// </summary>
+    [Required(ErrorMessage = "业务Id不能为空")]
+    public long BizId { get; set; }
+
+    /// <summary>
+    /// 业务单号
+    /// </summary>
+    public string? BizNo { get; set; }
+
+    /// <summary>
+    /// 流程标题
+    /// </summary>
+    public string? Title { get; set; }
+
+    /// <summary>
+    /// 提交说明
+    /// </summary>
+    public string? Comment { get; set; }
+
+    /// <summary>
+    /// 业务数据(用于条件分支评估)
+    /// </summary>
+    public Dictionary<string, object>? BizData { get; set; }
+}
+
+/// <summary>
+/// 审批操作输入
+/// </summary>
+public class TaskActionInput
+{
+    /// <summary>
+    /// 任务 Id
+    /// </summary>
+    [Required(ErrorMessage = "任务Id不能为空")]
+    public long TaskId { get; set; }
+
+    /// <summary>
+    /// 审批意见
+    /// </summary>
+    public string? Comment { get; set; }
+}
+
+/// <summary>
+/// 转办输入
+/// </summary>
+public class TaskTransferInput : TaskActionInput
+{
+    /// <summary>
+    /// 转办目标人 Id
+    /// </summary>
+    [Required(ErrorMessage = "目标人不能为空")]
+    public long TargetUserId { get; set; }
+}
+
+/// <summary>
+/// 撤回输入
+/// </summary>
+public class TaskWithdrawInput
+{
+    /// <summary>
+    /// 流程实例 Id
+    /// </summary>
+    [Required(ErrorMessage = "实例Id不能为空")]
+    public long InstanceId { get; set; }
+}
+
+/// <summary>
+/// 加签输入
+/// </summary>
+public class TaskAddSignInput : TaskActionInput
+{
+    /// <summary>
+    /// 加签目标人 Id
+    /// </summary>
+    [Required(ErrorMessage = "目标人不能为空")]
+    public long TargetUserId { get; set; }
+}
+
+/// <summary>
+/// 催办输入
+/// </summary>
+public class TaskUrgeInput
+{
+    /// <summary>
+    /// 流程实例 Id
+    /// </summary>
+    [Required(ErrorMessage = "实例Id不能为空")]
+    public long InstanceId { get; set; }
+}

+ 617 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/FlowEngine/FlowEngineService.cs

@@ -0,0 +1,617 @@
+using System.Text.Json;
+
+namespace Admin.NET.Plugin.ApprovalFlow.Service;
+
+/// <summary>
+/// 流程推进引擎 — 核心状态机
+/// 不暴露为 API,由其他 Service 内部调用
+/// </summary>
+public class FlowEngineService : ITransient
+{
+    private readonly SqlSugarRepository<ApprovalFlow> _flowRep;
+    private readonly SqlSugarRepository<ApprovalFlowInstance> _instanceRep;
+    private readonly SqlSugarRepository<ApprovalFlowTask> _taskRep;
+    private readonly SqlSugarRepository<ApprovalFlowLog> _logRep;
+    private readonly SqlSugarRepository<SysUserRole> _userRoleRep;
+    private readonly SqlSugarRepository<SysUser> _userRep;
+    private readonly UserManager _userManager;
+    private readonly FlowNotifyService _notifyService;
+
+    public FlowEngineService(
+        SqlSugarRepository<ApprovalFlow> flowRep,
+        SqlSugarRepository<ApprovalFlowInstance> instanceRep,
+        SqlSugarRepository<ApprovalFlowTask> taskRep,
+        SqlSugarRepository<ApprovalFlowLog> logRep,
+        SqlSugarRepository<SysUserRole> userRoleRep,
+        SqlSugarRepository<SysUser> userRep,
+        UserManager userManager,
+        FlowNotifyService notifyService)
+    {
+        _flowRep = flowRep;
+        _instanceRep = instanceRep;
+        _taskRep = taskRep;
+        _logRep = logRep;
+        _userRoleRep = userRoleRep;
+        _userRep = userRep;
+        _userManager = userManager;
+        _notifyService = notifyService;
+    }
+
+    // ═══════════════════════════════════════════
+    //  核心生命周期
+    // ═══════════════════════════════════════════
+
+    /// <summary>
+    /// 发起流程
+    /// </summary>
+    public async Task<long> StartFlow(StartFlowInput input)
+    {
+        var flow = await _flowRep.AsQueryable()
+            .Where(u => u.BizType == input.BizType && u.IsPublished && !u.IsDelete)
+            .OrderByDescending(u => u.Version)
+            .FirstAsync() ?? throw Oops.Oh($"未找到业务类型 [{input.BizType}] 的已发布流程定义");
+
+        if (string.IsNullOrWhiteSpace(flow.FlowJson))
+            throw Oops.Oh("流程定义的 FlowJson 为空,请先设计流程图");
+
+        var flowData = JsonSerializer.Deserialize<ApprovalFlowItem>(flow.FlowJson)
+            ?? throw Oops.Oh("FlowJson 反序列化失败");
+
+        var instance = new ApprovalFlowInstance
+        {
+            FlowId = flow.Id,
+            FlowVersion = flow.Version,
+            BizType = input.BizType,
+            BizId = input.BizId,
+            BizNo = input.BizNo,
+            Title = input.Title ?? $"{flow.Name}-{input.BizNo}",
+            InitiatorId = _userManager.UserId,
+            InitiatorName = _userManager.RealName,
+            Status = FlowInstanceStatusEnum.Running,
+            FlowJsonSnapshot = flow.FlowJson,
+            StartTime = DateTime.Now,
+        };
+
+        var startNode = flowData.Nodes.FirstOrDefault(n =>
+            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);
+
+        await InvokeHandler(input.BizType, h => h.OnFlowStarted(input.BizId, instance.Id));
+
+        return instance.Id;
+    }
+
+    /// <summary>
+    /// 同意
+    /// </summary>
+    public async Task Approve(long taskId, string? comment)
+    {
+        var task = await GetPendingTask(taskId);
+        task.Status = FlowTaskStatusEnum.Approved;
+        task.Comment = comment;
+        task.ActionTime = DateTime.Now;
+        await _taskRep.AsUpdateable(task).ExecuteCommandAsync();
+
+        await WriteLog(task.InstanceId, taskId, task.NodeId, FlowLogActionEnum.Approve, comment);
+
+        var instance = await _instanceRep.GetByIdAsync(task.InstanceId)
+            ?? throw Oops.Oh("流程实例不存在");
+
+        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 Reject(long taskId, string? comment)
+    {
+        var task = await GetPendingTask(taskId);
+        task.Status = FlowTaskStatusEnum.Rejected;
+        task.Comment = comment;
+        task.ActionTime = DateTime.Now;
+        await _taskRep.AsUpdateable(task).ExecuteCommandAsync();
+
+        await CancelPendingTasks(task.InstanceId, task.NodeId, task.Id);
+
+        var instance = await _instanceRep.GetByIdAsync(task.InstanceId)
+            ?? throw Oops.Oh("流程实例不存在");
+        instance.Status = FlowInstanceStatusEnum.Rejected;
+        instance.EndTime = DateTime.Now;
+        await _instanceRep.AsUpdateable(instance).ExecuteCommandAsync();
+
+        await WriteLog(instance.Id, taskId, task.NodeId, FlowLogActionEnum.Reject, comment);
+        await InvokeHandler(instance.BizType,
+            h => h.OnFlowCompleted(instance.BizId, FlowInstanceStatusEnum.Rejected));
+        await _notifyService.NotifyFlowCompleted(instance.InitiatorId, instance.Id, instance.Title, FlowInstanceStatusEnum.Rejected);
+    }
+
+    // ═══════════════════════════════════════════
+    //  扩展操作
+    // ═══════════════════════════════════════════
+
+    /// <summary>
+    /// 转办
+    /// </summary>
+    public async Task Transfer(long taskId, long targetUserId, string? comment)
+    {
+        var task = await GetPendingTask(taskId);
+        task.Status = FlowTaskStatusEnum.Transferred;
+        task.Comment = comment;
+        task.ActionTime = DateTime.Now;
+        task.TransferToId = targetUserId;
+        await _taskRep.AsUpdateable(task).ExecuteCommandAsync();
+
+        var targetUser = await _userRep.GetByIdAsync(targetUserId);
+        var newTask = new ApprovalFlowTask
+        {
+            InstanceId = task.InstanceId,
+            NodeId = task.NodeId,
+            NodeName = task.NodeName,
+            AssigneeId = targetUserId,
+            AssigneeName = targetUser?.RealName,
+            Status = FlowTaskStatusEnum.Pending,
+        };
+        await _taskRep.InsertAsync(newTask);
+
+        await WriteLog(task.InstanceId, taskId, task.NodeId, FlowLogActionEnum.Transfer,
+            $"{comment} → 转办给 {targetUser?.RealName}");
+
+        var instance = await _instanceRep.GetByIdAsync(task.InstanceId);
+        await _notifyService.NotifyTransferred(targetUserId, task.InstanceId, instance?.Title ?? "", _userManager.RealName);
+    }
+
+    /// <summary>
+    /// 撤回(发起人撤回)
+    /// </summary>
+    public async Task Withdraw(long instanceId)
+    {
+        var instance = await _instanceRep.GetByIdAsync(instanceId)
+            ?? throw Oops.Oh("流程实例不存在");
+        if (instance.InitiatorId != _userManager.UserId)
+            throw Oops.Oh("只有发起人可以撤回");
+        if (instance.Status != FlowInstanceStatusEnum.Running)
+            throw Oops.Oh("当前流程状态不允许撤回");
+
+        var pendingTasks = await _taskRep.AsQueryable()
+            .Where(t => t.InstanceId == instanceId && t.Status == FlowTaskStatusEnum.Pending)
+            .ToListAsync();
+        var doneTasks = await _taskRep.AsQueryable()
+            .Where(t => t.InstanceId == instanceId &&
+                        t.Status != FlowTaskStatusEnum.Pending &&
+                        t.Status != FlowTaskStatusEnum.Cancelled)
+            .CountAsync();
+        if (doneTasks > 0)
+            throw Oops.Oh("已有人审批过,不可撤回");
+
+        var cancelledUserIds = pendingTasks.Select(t => t.AssigneeId).Distinct().ToList();
+
+        foreach (var t in pendingTasks)
+        {
+            t.Status = FlowTaskStatusEnum.Cancelled;
+            t.ActionTime = DateTime.Now;
+        }
+        await _taskRep.AsUpdateable(pendingTasks).ExecuteCommandAsync();
+
+        instance.Status = FlowInstanceStatusEnum.Cancelled;
+        instance.EndTime = DateTime.Now;
+        await _instanceRep.AsUpdateable(instance).ExecuteCommandAsync();
+
+        await WriteLog(instanceId, null, instance.CurrentNodeId, FlowLogActionEnum.Withdraw, null);
+        await InvokeHandler(instance.BizType,
+            h => h.OnFlowCompleted(instance.BizId, FlowInstanceStatusEnum.Cancelled));
+
+        await _notifyService.NotifyWithdrawn(cancelledUserIds, instanceId, instance.Title, instance.InitiatorName);
+    }
+
+    /// <summary>
+    /// 退回上一步
+    /// </summary>
+    public async Task ReturnToPrev(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 prevNodeId = FindPrevUserTaskNodeId(flowData, task.NodeId);
+        if (prevNodeId == null)
+            throw Oops.Oh("已是第一个审批节点,无法退回");
+
+        await CancelPendingTasks(task.InstanceId, task.NodeId);
+        task.Status = FlowTaskStatusEnum.Returned;
+        task.Comment = comment;
+        task.ActionTime = DateTime.Now;
+        await _taskRep.AsUpdateable(task).ExecuteCommandAsync();
+
+        instance.CurrentNodeId = prevNodeId;
+        await _instanceRep.AsUpdateable(instance).ExecuteCommandAsync();
+
+        await CreateTasksForNode(instance, flowData, prevNodeId);
+        await WriteLog(instance.Id, taskId, task.NodeId, FlowLogActionEnum.Return, comment);
+
+        var returnedTasks = await _taskRep.AsQueryable()
+            .Where(t => t.InstanceId == instance.Id && t.NodeId == prevNodeId && t.Status == FlowTaskStatusEnum.Pending)
+            .ToListAsync();
+        var returnedUserIds = returnedTasks.Select(t => t.AssigneeId).Distinct().ToList();
+        await _notifyService.NotifyReturned(returnedUserIds, instance.Id, instance.Title, _userManager.RealName);
+    }
+
+    /// <summary>
+    /// 加签
+    /// </summary>
+    public async Task AddSign(long taskId, long targetUserId, string? comment)
+    {
+        var task = await GetPendingTask(taskId);
+        var targetUser = await _userRep.GetByIdAsync(targetUserId);
+
+        var newTask = new ApprovalFlowTask
+        {
+            InstanceId = task.InstanceId,
+            NodeId = task.NodeId,
+            NodeName = task.NodeName,
+            AssigneeId = targetUserId,
+            AssigneeName = targetUser?.RealName,
+            Status = FlowTaskStatusEnum.Pending,
+            IsAddSign = true,
+            AddSignById = _userManager.UserId,
+        };
+        await _taskRep.InsertAsync(newTask);
+
+        await WriteLog(task.InstanceId, taskId, task.NodeId, FlowLogActionEnum.AddSign,
+            $"{comment} → 加签给 {targetUser?.RealName}");
+
+        var instance = await _instanceRep.GetByIdAsync(task.InstanceId);
+        await _notifyService.NotifyAddSign(targetUserId, task.InstanceId, instance?.Title ?? "", _userManager.RealName);
+    }
+
+    /// <summary>
+    /// 催办
+    /// </summary>
+    public async Task Urge(long instanceId)
+    {
+        var instance = await _instanceRep.GetByIdAsync(instanceId)
+            ?? throw Oops.Oh("流程实例不存在");
+        if (instance.Status != FlowInstanceStatusEnum.Running)
+            throw Oops.Oh("当前流程不在审批中");
+
+        await WriteLog(instanceId, null, instance.CurrentNodeId, FlowLogActionEnum.Urge, "催办");
+
+        var pendingTasks = await _taskRep.AsQueryable()
+            .Where(t => t.InstanceId == instanceId && t.Status == FlowTaskStatusEnum.Pending)
+            .ToListAsync();
+        var userIds = pendingTasks.Select(t => t.AssigneeId).Distinct().ToList();
+        await _notifyService.NotifyUrge(userIds, instanceId, instance.Title);
+    }
+
+    // ═══════════════════════════════════════════
+    //  内部引擎方法
+    // ═══════════════════════════════════════════
+
+    private async Task AdvanceToNext(ApprovalFlowInstance instance, ApprovalFlowItem flowData, string currentNodeId)
+    {
+        var nextNodeId = FindNextNodeId(flowData, currentNodeId);
+        if (nextNodeId == null)
+        {
+            await CompleteInstance(instance, FlowInstanceStatusEnum.Approved);
+            return;
+        }
+
+        var nextNode = flowData.Nodes.FirstOrDefault(n => n.Id == nextNodeId);
+        if (nextNode == null)
+        {
+            await CompleteInstance(instance, FlowInstanceStatusEnum.Approved);
+            return;
+        }
+
+        if (nextNode.Type is "bpmn:endEvent" or "end-node")
+        {
+            await CompleteInstance(instance, FlowInstanceStatusEnum.Approved);
+            return;
+        }
+
+        if (nextNode.Type is "bpmn:exclusiveGateway")
+        {
+            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);
+            return;
+        }
+
+        instance.CurrentNodeId = nextNodeId;
+        await _instanceRep.AsUpdateable(instance).UpdateColumns(i => new { i.CurrentNodeId }).ExecuteCommandAsync();
+        await CreateTasksForNode(instance, flowData, nextNodeId);
+    }
+
+    private async Task CompleteInstance(ApprovalFlowInstance instance, FlowInstanceStatusEnum status)
+    {
+        instance.Status = status;
+        instance.EndTime = DateTime.Now;
+        await _instanceRep.AsUpdateable(instance)
+            .UpdateColumns(i => new { i.Status, i.EndTime })
+            .ExecuteCommandAsync();
+        await InvokeHandler(instance.BizType,
+            h => h.OnFlowCompleted(instance.BizId, status));
+        await _notifyService.NotifyFlowCompleted(instance.InitiatorId, instance.Id, instance.Title, status);
+    }
+
+    private async Task CreateTasksForNode(ApprovalFlowInstance instance, ApprovalFlowItem flowData, string nodeId)
+    {
+        var node = flowData.Nodes.FirstOrDefault(n => n.Id == nodeId)
+            ?? throw Oops.Oh($"FlowJson 中未找到节点 [{nodeId}]");
+
+        var approvers = await ResolveApprovers(node.Properties, instance.InitiatorId);
+        if (approvers.Count == 0)
+            throw Oops.Oh($"节点 [{node.Properties?.NodeName ?? nodeId}] 未配置审批人或审批人列表为空");
+
+        var tasks = approvers.Select(a => new ApprovalFlowTask
+        {
+            InstanceId = instance.Id,
+            NodeId = nodeId,
+            NodeName = node.Properties?.NodeName ?? node.Text?.Value,
+            AssigneeId = a.userId,
+            AssigneeName = a.userName,
+            Status = FlowTaskStatusEnum.Pending,
+        }).ToList();
+
+        await _taskRep.AsInsertable(tasks).ExecuteCommandAsync();
+
+        var assigneeIds = tasks.Select(t => t.AssigneeId).Distinct().ToList();
+        await _notifyService.NotifyNewTask(assigneeIds, instance.Id, instance.Title, node.Properties?.NodeName ?? node.Text?.Value);
+    }
+
+    private async Task<List<(long userId, string userName)>> ResolveApprovers(FlowProperties? props, long initiatorId)
+    {
+        if (props == null || string.IsNullOrWhiteSpace(props.ApproverType))
+            return new List<(long, string)>();
+
+        var approverType = props.ApproverType;
+
+        if (approverType == nameof(ApproverTypeEnum.Initiator))
+        {
+            var initiator = await _userRep.GetByIdAsync(initiatorId);
+            return initiator != null
+                ? new List<(long, string)> { (initiator.Id, initiator.RealName ?? "") }
+                : new List<(long, string)>();
+        }
+
+        if (string.IsNullOrWhiteSpace(props.ApproverIds))
+            return new List<(long, string)>();
+
+        var ids = props.ApproverIds.Split(',', StringSplitOptions.RemoveEmptyEntries)
+            .Select(s => long.TryParse(s.Trim(), out var v) ? v : 0).Where(id => id > 0).ToList();
+
+        if (approverType == nameof(ApproverTypeEnum.SpecificUser))
+        {
+            var users = await _userRep.AsQueryable()
+                .Where(u => ids.Contains(u.Id)).ToListAsync();
+            return users.Select(u => (u.Id, u.RealName ?? "")).ToList();
+        }
+
+        if (approverType == nameof(ApproverTypeEnum.Role))
+        {
+            var userIds = await _userRoleRep.AsQueryable()
+                .Where(ur => ids.Contains(ur.RoleId))
+                .Select(ur => ur.UserId)
+                .ToListAsync();
+            var users = await _userRep.AsQueryable()
+                .Where(u => userIds.Contains(u.Id)).ToListAsync();
+            return users.Select(u => (u.Id, u.RealName ?? "")).ToList();
+        }
+
+        if (approverType == nameof(ApproverTypeEnum.Department))
+        {
+            var users = await _userRep.AsQueryable()
+                .Where(u => ids.Contains(u.OrgId)).ToListAsync();
+            return users.Select(u => (u.Id, u.RealName ?? "")).ToList();
+        }
+
+        return new List<(long, string)>();
+    }
+
+    /// <summary>
+    /// 评估排他网关 — 依次尝试各非默认分支的条件表达式,首个匹配的获胜;
+    /// 全不匹配则走默认分支;无默认则走第一条出边
+    /// 支持简单比较表达式:variable op value(op: ==,!=,>,>=,&lt;,&lt;=)
+    /// </summary>
+    private string EvaluateGateway(List<GatewayCondition>? conditions, ApprovalFlowItem flowData, string gatewayNodeId, Dictionary<string, object>? bizData)
+    {
+        if (conditions != null && conditions.Count > 0 && bizData != null && bizData.Count > 0)
+        {
+            foreach (var cond in conditions.Where(c => !c.IsDefault))
+            {
+                if (!string.IsNullOrWhiteSpace(cond.Expression) && EvalSimpleExpression(cond.Expression, bizData))
+                    return cond.TargetNodeId;
+            }
+
+            var defaultBranch = conditions.FirstOrDefault(c => c.IsDefault);
+            if (defaultBranch != null)
+                return defaultBranch.TargetNodeId;
+        }
+        else if (conditions != null && conditions.Count > 0)
+        {
+            var defaultBranch = conditions.FirstOrDefault(c => c.IsDefault);
+            if (defaultBranch != null) return defaultBranch.TargetNodeId;
+            return conditions.First().TargetNodeId;
+        }
+
+        var edge = flowData.Edges.FirstOrDefault(e => e.SourceNodeId == gatewayNodeId);
+        return edge?.TargetNodeId ?? throw Oops.Oh("排他网关没有出边");
+    }
+
+    /// <summary>
+    /// 简单表达式求值:支持 "field op value" 格式(如 "urgent == 1", "customLevel >= 3", "amount > 10000")
+    /// 多条件用 &amp;&amp; 连接
+    /// </summary>
+    private static bool EvalSimpleExpression(string expression, Dictionary<string, object> bizData)
+    {
+        var parts = expression.Split("&&", StringSplitOptions.TrimEntries);
+        foreach (var part in parts)
+        {
+            if (!EvalSingleComparison(part.Trim(), bizData))
+                return false;
+        }
+        return true;
+    }
+
+    private static bool EvalSingleComparison(string expr, Dictionary<string, object> bizData)
+    {
+        string[] ops = { ">=", "<=", "!=", "==", ">", "<" };
+        foreach (var op in ops)
+        {
+            var idx = expr.IndexOf(op, StringComparison.Ordinal);
+            if (idx < 0) continue;
+
+            var fieldName = expr[..idx].Trim();
+            var valueStr = expr[(idx + op.Length)..].Trim().Trim('"', '\'');
+
+            if (!bizData.TryGetValue(fieldName, out var fieldValue))
+                return false;
+
+            if (decimal.TryParse(fieldValue?.ToString(), out var numLeft) && decimal.TryParse(valueStr, out var numRight))
+            {
+                return op switch
+                {
+                    "==" => numLeft == numRight,
+                    "!=" => numLeft != numRight,
+                    ">" => numLeft > numRight,
+                    ">=" => numLeft >= numRight,
+                    "<" => numLeft < numRight,
+                    "<=" => numLeft <= numRight,
+                    _ => false,
+                };
+            }
+
+            var strLeft = fieldValue?.ToString() ?? "";
+            return op switch
+            {
+                "==" => strLeft.Equals(valueStr, StringComparison.OrdinalIgnoreCase),
+                "!=" => !strLeft.Equals(valueStr, StringComparison.OrdinalIgnoreCase),
+                _ => false,
+            };
+        }
+        return false;
+    }
+
+    private async Task<Dictionary<string, object>?> GetBizData(string bizType, long bizId)
+    {
+        var handlers = App.GetServices<IFlowBizHandler>();
+        var handler = handlers?.FirstOrDefault(h => h.BizType == bizType);
+        if (handler == null) return null;
+        return await handler.GetBizData(bizId);
+    }
+
+    private string? FindNextNodeId(ApprovalFlowItem flowData, string currentNodeId)
+    {
+        var edge = flowData.Edges.FirstOrDefault(e => e.SourceNodeId == currentNodeId);
+        return edge?.TargetNodeId;
+    }
+
+    private string? FindPrevUserTaskNodeId(ApprovalFlowItem flowData, string currentNodeId)
+    {
+        var inEdge = flowData.Edges.FirstOrDefault(e => e.TargetNodeId == currentNodeId);
+        if (inEdge == null) return null;
+
+        var prevNode = flowData.Nodes.FirstOrDefault(n => n.Id == inEdge.SourceNodeId);
+        if (prevNode == null) return null;
+
+        if (prevNode.Type is "bpmn:userTask" or "user-node" or "task-node")
+            return prevNode.Id;
+
+        // 递归跳过网关等非用户任务节点
+        return FindPrevUserTaskNodeId(flowData, prevNode.Id);
+    }
+
+    private async Task<bool> IsNodeCompleted(ApprovalFlowInstance instance, string nodeId)
+    {
+        var flowData = DeserializeFlowJson(instance.FlowJsonSnapshot);
+        var node = flowData.Nodes.FirstOrDefault(n => n.Id == nodeId);
+        var mode = node?.Properties?.MultiApproveMode;
+
+        if (mode == nameof(MultiApproveModeEnum.All))
+        {
+            var pendingCount = await _taskRep.AsQueryable()
+                .Where(t => t.InstanceId == instance.Id && t.NodeId == nodeId && t.Status == FlowTaskStatusEnum.Pending)
+                .CountAsync();
+            return pendingCount == 0;
+        }
+
+        // 默认或签(Any):一人通过即完成,取消其他 Pending
+        await CancelPendingTasks(instance.Id, nodeId);
+        return true;
+    }
+
+    private async Task CancelPendingTasks(long instanceId, string nodeId, long? excludeTaskId = null)
+    {
+        var tasks = await _taskRep.AsQueryable()
+            .Where(t => t.InstanceId == instanceId && t.NodeId == nodeId && 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)
+            ?? throw Oops.Oh("审批任务不存在");
+        if (task.Status != FlowTaskStatusEnum.Pending)
+            throw Oops.Oh("该任务已处理");
+        if (task.AssigneeId != _userManager.UserId)
+            throw Oops.Oh("当前用户不是该任务的审批人");
+        return task;
+    }
+
+    private async Task WriteLog(long instanceId, long? taskId, string? nodeId, FlowLogActionEnum action, string? comment)
+    {
+        await _logRep.InsertAsync(new ApprovalFlowLog
+        {
+            InstanceId = instanceId,
+            TaskId = taskId,
+            NodeId = nodeId,
+            Action = action,
+            OperatorId = _userManager.UserId,
+            OperatorName = _userManager.RealName,
+            Comment = comment,
+        });
+    }
+
+    private static ApprovalFlowItem DeserializeFlowJson(string? json)
+    {
+        if (string.IsNullOrWhiteSpace(json))
+            throw Oops.Oh("FlowJson 快照为空");
+        return JsonSerializer.Deserialize<ApprovalFlowItem>(json)
+            ?? throw Oops.Oh("FlowJson 反序列化失败");
+    }
+
+    private async Task InvokeHandler(string bizType, Func<IFlowBizHandler, Task> action)
+    {
+        var handlers = App.GetServices<IFlowBizHandler>();
+        var handler = handlers?.FirstOrDefault(h => h.BizType == bizType);
+        if (handler != null)
+            await action(handler);
+    }
+}

+ 35 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/FlowEngine/IFlowBizHandler.cs

@@ -0,0 +1,35 @@
+namespace Admin.NET.Plugin.ApprovalFlow.Service;
+
+/// <summary>
+/// 业务流程回调处理器接口
+/// 每种 BizType 实现一个 Handler,注册到 DI 容器
+/// </summary>
+public interface IFlowBizHandler
+{
+    /// <summary>
+    /// 业务类型编码(与流程定义的 BizType 匹配)
+    /// </summary>
+    string BizType { get; }
+
+    /// <summary>
+    /// 流程发起后回调(可选:更新业务表状态为"审批中")
+    /// </summary>
+    Task OnFlowStarted(long bizId, long instanceId) => Task.CompletedTask;
+
+    /// <summary>
+    /// 流程结束后回调(必须:更新业务表最终状态)
+    /// </summary>
+    Task OnFlowCompleted(long bizId, FlowInstanceStatusEnum finalStatus);
+
+    /// <summary>
+    /// 单个节点审批完成回调(可选:按节点推进业务进度)
+    /// </summary>
+    Task OnNodeCompleted(long bizId, string nodeId, string nodeName) => Task.CompletedTask;
+
+    /// <summary>
+    /// 获取业务数据用于网关条件表达式求值(可选)
+    /// 返回 key-value 字典,key 对应条件表达式中的变量名
+    /// 例如: { "amount": 50000, "urgent": 1, "customLevel": 3 }
+    /// </summary>
+    Task<Dictionary<string, object>> GetBizData(long bizId) => Task.FromResult(new Dictionary<string, object>());
+}

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

@@ -0,0 +1,82 @@
+namespace Admin.NET.Plugin.ApprovalFlow.Service;
+
+/// <summary>
+/// 流程实例详情输出
+/// </summary>
+public class FlowInstanceDetailOutput
+{
+    public long Id { get; set; }
+    public long FlowId { get; set; }
+    public string BizType { get; set; } = "";
+    public long BizId { get; set; }
+    public string? BizNo { get; set; }
+    public string Title { get; set; } = "";
+    public long InitiatorId { get; set; }
+    public string? InitiatorName { get; set; }
+    public FlowInstanceStatusEnum Status { get; set; }
+    public string? CurrentNodeId { get; set; }
+    public DateTime StartTime { get; set; }
+    public DateTime? EndTime { get; set; }
+    public List<FlowTaskOutput> Tasks { get; set; } = new();
+}
+
+/// <summary>
+/// 审批任务输出
+/// </summary>
+public class FlowTaskOutput
+{
+    public long Id { get; set; }
+    public string NodeId { get; set; } = "";
+    public string? NodeName { get; set; }
+    public long AssigneeId { get; set; }
+    public string? AssigneeName { get; set; }
+    public FlowTaskStatusEnum Status { get; set; }
+    public string? Comment { get; set; }
+    public DateTime? ActionTime { get; set; }
+}
+
+/// <summary>
+/// 审批时间线条目
+/// </summary>
+public class FlowTimelineItem
+{
+    public long Id { get; set; }
+    public string? NodeId { get; set; }
+    public string? NodeName { get; set; }
+    public FlowLogActionEnum Action { get; set; }
+    public long OperatorId { get; set; }
+    public string? OperatorName { get; set; }
+    public string? Comment { get; set; }
+    public DateTime CreateTime { get; set; }
+}
+
+/// <summary>
+/// 流程实例分页查询输入
+/// </summary>
+public class InstancePageInput : BasePageInput
+{
+    public string? BizType { get; set; }
+    public FlowInstanceStatusEnum? Status { get; set; }
+}
+
+/// <summary>
+/// 流程实例分页输出
+/// </summary>
+public class InstancePageOutput
+{
+    public long Id { get; set; }
+    public string BizType { get; set; } = "";
+    public long BizId { get; set; }
+    public string? BizNo { get; set; }
+    public string Title { get; set; } = "";
+    public FlowInstanceStatusEnum Status { get; set; }
+    public string? CurrentNodeId { get; set; }
+    public DateTime StartTime { get; set; }
+    public DateTime? EndTime { get; set; }
+    public string? InitiatorName { get; set; }
+
+    /// <summary>
+    /// 当前待审批人姓名(多人逗号分隔)
+    /// </summary>
+    public string? CurrentAssigneeName { get; set; }
+}

+ 122 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/FlowInstance/FlowInstanceService.cs

@@ -0,0 +1,122 @@
+namespace Admin.NET.Plugin.ApprovalFlow.Service;
+
+/// <summary>
+/// 流程实例服务
+/// </summary>
+[ApiDescriptionSettings(ApprovalFlowConst.GroupName, Order = 95)]
+public class FlowInstanceService : IDynamicApiController, ITransient
+{
+    private readonly FlowEngineService _engine;
+    private readonly SqlSugarRepository<ApprovalFlowInstance> _instanceRep;
+    private readonly SqlSugarRepository<ApprovalFlowTask> _taskRep;
+    private readonly SqlSugarRepository<ApprovalFlowLog> _logRep;
+
+    public FlowInstanceService(
+        FlowEngineService engine,
+        SqlSugarRepository<ApprovalFlowInstance> instanceRep,
+        SqlSugarRepository<ApprovalFlowTask> taskRep,
+        SqlSugarRepository<ApprovalFlowLog> logRep)
+    {
+        _engine = engine;
+        _instanceRep = instanceRep;
+        _taskRep = taskRep;
+        _logRep = logRep;
+    }
+
+    /// <summary>
+    /// 发起流程
+    /// </summary>
+    [HttpPost]
+    [ApiDescriptionSettings(Name = "Start")]
+    [DisplayName("发起流程")]
+    public async Task<long> Start(StartFlowInput input)
+    {
+        return await _engine.StartFlow(input);
+    }
+
+    /// <summary>
+    /// 查询实例详情
+    /// </summary>
+    [HttpGet]
+    [ApiDescriptionSettings(Name = "Detail")]
+    [DisplayName("流程实例详情")]
+    public async Task<FlowInstanceDetailOutput?> GetDetail([FromQuery] long id)
+    {
+        var instance = await _instanceRep.GetByIdAsync(id);
+        if (instance == null) return null;
+
+        var tasks = await _taskRep.AsQueryable()
+            .Where(t => t.InstanceId == id)
+            .OrderBy(t => t.CreateTime)
+            .ToListAsync();
+
+        return new FlowInstanceDetailOutput
+        {
+            Id = instance.Id,
+            FlowId = instance.FlowId,
+            BizType = instance.BizType,
+            BizId = instance.BizId,
+            BizNo = instance.BizNo,
+            Title = instance.Title,
+            InitiatorId = instance.InitiatorId,
+            InitiatorName = instance.InitiatorName,
+            Status = instance.Status,
+            CurrentNodeId = instance.CurrentNodeId,
+            StartTime = instance.StartTime,
+            EndTime = instance.EndTime,
+            Tasks = tasks.Select(t => new FlowTaskOutput
+            {
+                Id = t.Id,
+                NodeId = t.NodeId,
+                NodeName = t.NodeName,
+                AssigneeId = t.AssigneeId,
+                AssigneeName = t.AssigneeName,
+                Status = t.Status,
+                Comment = t.Comment,
+                ActionTime = t.ActionTime,
+            }).ToList(),
+        };
+    }
+
+    /// <summary>
+    /// 根据业务单据查询流程实例
+    /// </summary>
+    [HttpGet]
+    [ApiDescriptionSettings(Name = "GetByBiz")]
+    [DisplayName("按业务查流程")]
+    public async Task<FlowInstanceDetailOutput?> GetByBiz([FromQuery] string bizType, [FromQuery] long bizId)
+    {
+        var instance = await _instanceRep.AsQueryable()
+            .Where(i => i.BizType == bizType && i.BizId == bizId)
+            .OrderByDescending(i => i.CreateTime)
+            .FirstAsync();
+        if (instance == null) return null;
+        return await GetDetail(instance.Id);
+    }
+
+    /// <summary>
+    /// 审批时间线
+    /// </summary>
+    [HttpGet]
+    [ApiDescriptionSettings(Name = "Timeline")]
+    [DisplayName("审批时间线")]
+    public async Task<List<FlowTimelineItem>> GetTimeline([FromQuery] long instanceId)
+    {
+        return await _logRep.AsQueryable()
+            .LeftJoin<ApprovalFlowTask>((l, t) => l.TaskId == t.Id)
+            .Where((l, t) => l.InstanceId == instanceId)
+            .OrderBy((l, t) => l.CreateTime)
+            .Select((l, t) => new FlowTimelineItem
+            {
+                Id = l.Id,
+                NodeId = l.NodeId,
+                NodeName = t.NodeName,
+                Action = l.Action,
+                OperatorId = l.OperatorId,
+                OperatorName = l.OperatorName,
+                Comment = l.Comment,
+                CreateTime = l.CreateTime,
+            })
+            .ToListAsync();
+    }
+}

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

@@ -0,0 +1,214 @@
+using Admin.NET.Core.Service;
+using Microsoft.AspNetCore.SignalR;
+
+namespace Admin.NET.Plugin.ApprovalFlow.Service;
+
+/// <summary>
+/// 流程通知服务 — 按 JSON 配置分渠道调度
+/// </summary>
+public class FlowNotifyService : ITransient
+{
+    private readonly IHubContext<OnlineUserHub, IOnlineUserHub> _hubContext;
+    private readonly SysCacheService _cacheService;
+
+    public FlowNotifyService(
+        IHubContext<OnlineUserHub, IOnlineUserHub> hubContext,
+        SysCacheService cacheService)
+    {
+        _hubContext = hubContext;
+        _cacheService = cacheService;
+    }
+
+    public async Task NotifyUsers(List<long> userIds, FlowNotification notification)
+    {
+        if (userIds.Count == 0) return;
+
+        var cfg = App.GetConfig<NotifyChannelConfig>("ApprovalFlow:Notify");
+
+        if (cfg?.SignalR != false)
+            await SendSignalR(userIds, notification);
+
+        if (cfg?.DingTalk == true)
+            await SendDingTalk(userIds, notification);
+
+        if (cfg?.WorkWeixin == true)
+            await SendWorkWeixin(userIds, notification);
+
+        if (cfg?.Email == true)
+            await SendEmail(userIds, notification);
+
+        if (cfg?.Sms == true)
+            await SendSms(userIds, notification);
+    }
+
+    public async Task NotifyUrge(List<long> userIds, long instanceId, string title)
+    {
+        await NotifyUsers(userIds, new FlowNotification
+        {
+            Type = FlowNotificationTypeEnum.Urge,
+            InstanceId = instanceId,
+            Title = $"【催办】{title}",
+            Content = "流程发起人催促您尽快审批,请及时处理。",
+        });
+    }
+
+    public async Task NotifyNewTask(List<long> userIds, long instanceId, string title, string? nodeName)
+    {
+        await NotifyUsers(userIds, new FlowNotification
+        {
+            Type = FlowNotificationTypeEnum.NewTask,
+            InstanceId = instanceId,
+            Title = $"【待审批】{title}",
+            Content = $"您有一条新的审批任务({nodeName}),请及时处理。",
+        });
+    }
+
+    public async Task NotifyFlowCompleted(long initiatorId, long instanceId, string title, FlowInstanceStatusEnum finalStatus)
+    {
+        var statusText = finalStatus == FlowInstanceStatusEnum.Approved ? "已通过" : "已拒绝";
+        await NotifyUsers(new List<long> { initiatorId }, new FlowNotification
+        {
+            Type = FlowNotificationTypeEnum.FlowCompleted,
+            InstanceId = instanceId,
+            Title = $"【审批{statusText}】{title}",
+            Content = $"您发起的审批流程已{statusText}。",
+        });
+    }
+
+    /// <summary>
+    /// 转办通知 — 通知被转办人
+    /// </summary>
+    public async Task NotifyTransferred(long targetUserId, long instanceId, string title, string? fromName)
+    {
+        await NotifyUsers(new List<long> { targetUserId }, new FlowNotification
+        {
+            Type = FlowNotificationTypeEnum.Transferred,
+            InstanceId = instanceId,
+            Title = $"【转办】{title}",
+            Content = $"{fromName} 将一条审批任务转交给您,请及时处理。",
+        });
+    }
+
+    /// <summary>
+    /// 退回通知 — 通知被退回节点的审批人
+    /// </summary>
+    public async Task NotifyReturned(List<long> userIds, long instanceId, string title, string? returnedBy)
+    {
+        await NotifyUsers(userIds, new FlowNotification
+        {
+            Type = FlowNotificationTypeEnum.Returned,
+            InstanceId = instanceId,
+            Title = $"【退回】{title}",
+            Content = $"{returnedBy} 已退回该审批,请重新审核。",
+        });
+    }
+
+    /// <summary>
+    /// 加签通知 — 通知被加签人
+    /// </summary>
+    public async Task NotifyAddSign(long targetUserId, long instanceId, string title, string? fromName)
+    {
+        await NotifyUsers(new List<long> { targetUserId }, new FlowNotification
+        {
+            Type = FlowNotificationTypeEnum.AddSign,
+            InstanceId = instanceId,
+            Title = $"【加签】{title}",
+            Content = $"{fromName} 邀请您参与审批,请及时处理。",
+        });
+    }
+
+    /// <summary>
+    /// 撤回通知 — 通知被取消的审批人
+    /// </summary>
+    public async Task NotifyWithdrawn(List<long> userIds, long instanceId, string title, string? initiatorName)
+    {
+        await NotifyUsers(userIds, new FlowNotification
+        {
+            Type = FlowNotificationTypeEnum.Withdrawn,
+            InstanceId = instanceId,
+            Title = $"【已撤回】{title}",
+            Content = $"{initiatorName} 已撤回该审批流程。",
+        });
+    }
+
+    // ═══════════════════════════════════════════
+    //  各渠道发送实现
+    // ═══════════════════════════════════════════
+
+    private async Task SendSignalR(List<long> userIds, FlowNotification notification)
+    {
+        var onlineUsers = _cacheService.HashGetAll<SysOnlineUser>(CacheConst.KeyUserOnline);
+        var connectionIds = onlineUsers
+            .Where(u => userIds.Contains(u.Value.UserId))
+            .Select(u => u.Value.ConnectionId)
+            .ToList();
+
+        if (connectionIds.Count == 0) return;
+
+        await _hubContext.Clients.Clients(connectionIds).ReceiveMessage(new
+        {
+            title = notification.Title,
+            message = notification.Content,
+            type = notification.Type.ToString(),
+            instanceId = notification.InstanceId,
+        });
+    }
+
+    private Task SendDingTalk(List<long> userIds, FlowNotification notification)
+    {
+        // TODO: 接入 DingTalk 开放平台 API
+        return Task.CompletedTask;
+    }
+
+    private Task SendWorkWeixin(List<long> userIds, FlowNotification notification)
+    {
+        // TODO: 接入企业微信消息推送 API
+        return Task.CompletedTask;
+    }
+
+    private Task SendEmail(List<long> userIds, FlowNotification notification)
+    {
+        // TODO: 接入 SMTP / 邮件服务
+        return Task.CompletedTask;
+    }
+
+    private Task SendSms(List<long> userIds, FlowNotification notification)
+    {
+        // TODO: 接入短信服务
+        return Task.CompletedTask;
+    }
+}
+
+public class FlowNotification
+{
+    public FlowNotificationTypeEnum Type { get; set; }
+    public long InstanceId { get; set; }
+    public string Title { get; set; } = "";
+    public string Content { get; set; } = "";
+}
+
+/// <summary>
+/// 通知类型
+/// </summary>
+public enum FlowNotificationTypeEnum
+{
+    NewTask,
+    Urge,
+    FlowCompleted,
+    Transferred,
+    Returned,
+    AddSign,
+    Withdrawn,
+}
+
+/// <summary>
+/// 通知渠道配置(来自 ApprovalFlow:Notify JSON)
+/// </summary>
+public class NotifyChannelConfig
+{
+    public bool SignalR { get; set; } = true;
+    public bool DingTalk { get; set; }
+    public bool WorkWeixin { get; set; }
+    public bool Email { get; set; }
+    public bool Sms { get; set; }
+}

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

@@ -0,0 +1,32 @@
+namespace Admin.NET.Plugin.ApprovalFlow.Service;
+
+/// <summary>
+/// 审批任务分页查询输入
+/// </summary>
+public class TaskPageInput : BasePageInput
+{
+    public string? BizType { get; set; }
+}
+
+/// <summary>
+/// 审批任务分页输出
+/// </summary>
+public class TaskPageOutput
+{
+    public long Id { get; set; }
+    public long InstanceId { get; set; }
+    public string NodeId { get; set; } = "";
+    public string? NodeName { get; set; }
+    public FlowTaskStatusEnum Status { get; set; }
+    public string? Comment { get; set; }
+    public DateTime? ActionTime { get; set; }
+
+    // 关联实例信息
+    public string Title { get; set; } = "";
+    public string BizType { get; set; } = "";
+    public long BizId { get; set; }
+    public string? BizNo { get; set; }
+    public string? InitiatorName { get; set; }
+    public DateTime StartTime { get; set; }
+    public DateTime CreateTime { get; set; }
+}

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

@@ -0,0 +1,234 @@
+namespace Admin.NET.Plugin.ApprovalFlow.Service;
+
+/// <summary>
+/// 审批任务服务
+/// </summary>
+[ApiDescriptionSettings(ApprovalFlowConst.GroupName, Order = 90)]
+public class FlowTaskService : IDynamicApiController, ITransient
+{
+    private readonly FlowEngineService _engine;
+    private readonly SqlSugarRepository<ApprovalFlowTask> _taskRep;
+    private readonly SqlSugarRepository<ApprovalFlowInstance> _instanceRep;
+    private readonly UserManager _userManager;
+
+    public FlowTaskService(
+        FlowEngineService engine,
+        SqlSugarRepository<ApprovalFlowTask> taskRep,
+        SqlSugarRepository<ApprovalFlowInstance> instanceRep,
+        UserManager userManager)
+    {
+        _engine = engine;
+        _taskRep = taskRep;
+        _instanceRep = instanceRep;
+        _userManager = userManager;
+    }
+
+    // ── 查询 ──
+
+    /// <summary>
+    /// 我的待办(分页)
+    /// </summary>
+    [HttpPost]
+    [ApiDescriptionSettings(Name = "MyPendingPage")]
+    [DisplayName("我的待办")]
+    public async Task<SqlSugarPagedList<TaskPageOutput>> MyPendingPage(TaskPageInput input)
+    {
+        var userId = _userManager.UserId;
+        return await _taskRep.AsQueryable()
+            .InnerJoin<ApprovalFlowInstance>((t, i) => t.InstanceId == i.Id)
+            .Where((t, i) => t.AssigneeId == userId && t.Status == FlowTaskStatusEnum.Pending)
+            .WhereIF(!string.IsNullOrWhiteSpace(input.BizType), (t, i) => i.BizType == input.BizType)
+            .OrderByDescending((t, i) => t.CreateTime)
+            .Select((t, i) => new TaskPageOutput
+            {
+                Id = t.Id,
+                InstanceId = t.InstanceId,
+                NodeId = t.NodeId,
+                NodeName = t.NodeName,
+                Status = t.Status,
+                Title = i.Title,
+                BizType = i.BizType,
+                BizId = i.BizId,
+                BizNo = i.BizNo,
+                InitiatorName = i.InitiatorName,
+                StartTime = i.StartTime,
+                CreateTime = t.CreateTime,
+            })
+            .ToPagedListAsync(input.Page, input.PageSize);
+    }
+
+    /// <summary>
+    /// 我的已办(分页)
+    /// </summary>
+    [HttpPost]
+    [ApiDescriptionSettings(Name = "MyDonePage")]
+    [DisplayName("我的已办")]
+    public async Task<SqlSugarPagedList<TaskPageOutput>> MyDonePage(TaskPageInput input)
+    {
+        var userId = _userManager.UserId;
+        return await _taskRep.AsQueryable()
+            .InnerJoin<ApprovalFlowInstance>((t, i) => t.InstanceId == i.Id)
+            .Where((t, i) => t.AssigneeId == userId && t.Status != FlowTaskStatusEnum.Pending)
+            .WhereIF(!string.IsNullOrWhiteSpace(input.BizType), (t, i) => i.BizType == input.BizType)
+            .OrderByDescending((t, i) => t.ActionTime)
+            .Select((t, i) => new TaskPageOutput
+            {
+                Id = t.Id,
+                InstanceId = t.InstanceId,
+                NodeId = t.NodeId,
+                NodeName = t.NodeName,
+                Status = t.Status,
+                Comment = t.Comment,
+                ActionTime = t.ActionTime,
+                Title = i.Title,
+                BizType = i.BizType,
+                BizId = i.BizId,
+                BizNo = i.BizNo,
+                InitiatorName = i.InitiatorName,
+                StartTime = i.StartTime,
+                CreateTime = t.CreateTime,
+            })
+            .ToPagedListAsync(input.Page, input.PageSize);
+    }
+
+    /// <summary>
+    /// 我发起的(分页)
+    /// </summary>
+    [HttpPost]
+    [ApiDescriptionSettings(Name = "MyInitiatedPage")]
+    [DisplayName("我发起的")]
+    public async Task<SqlSugarPagedList<InstancePageOutput>> MyInitiatedPage(InstancePageInput input)
+    {
+        var userId = _userManager.UserId;
+        var page = await _instanceRep.AsQueryable()
+            .Where(i => i.InitiatorId == userId)
+            .WhereIF(!string.IsNullOrWhiteSpace(input.BizType), i => i.BizType == input.BizType)
+            .WhereIF(input.Status.HasValue, i => i.Status == input.Status!.Value)
+            .OrderByDescending(i => i.CreateTime)
+            .Select(i => new InstancePageOutput
+            {
+                Id = i.Id,
+                BizType = i.BizType,
+                BizId = i.BizId,
+                BizNo = i.BizNo,
+                Title = i.Title,
+                Status = i.Status,
+                CurrentNodeId = i.CurrentNodeId,
+                StartTime = i.StartTime,
+                EndTime = i.EndTime,
+                InitiatorName = i.InitiatorName,
+            })
+            .ToPagedListAsync(input.Page, input.PageSize);
+
+        var runningIds = page.Items.Where(x => x.Status == FlowInstanceStatusEnum.Running).Select(x => x.Id).ToList();
+        if (runningIds.Count > 0)
+        {
+            var pendingAssignees = await _taskRep.AsQueryable()
+                .Where(t => runningIds.Contains(t.InstanceId) && t.Status == FlowTaskStatusEnum.Pending)
+                .Select(t => new { t.InstanceId, t.AssigneeName })
+                .ToListAsync();
+            var grouped = pendingAssignees.GroupBy(x => x.InstanceId)
+                .ToDictionary(g => g.Key, g => string.Join(", ", g.Select(x => x.AssigneeName).Distinct()));
+            foreach (var item in page.Items)
+            {
+                if (grouped.TryGetValue(item.Id, out var names))
+                    item.CurrentAssigneeName = names;
+            }
+        }
+
+        return page;
+    }
+
+    /// <summary>
+    /// 待办数量
+    /// </summary>
+    [HttpGet]
+    [ApiDescriptionSettings(Name = "MyPendingCount")]
+    [DisplayName("待办数量")]
+    public async Task<int> MyPendingCount()
+    {
+        var userId = _userManager.UserId;
+        return await _taskRep.AsQueryable()
+            .Where(t => t.AssigneeId == userId && t.Status == FlowTaskStatusEnum.Pending)
+            .CountAsync();
+    }
+
+    // ── 操作 ──
+
+    /// <summary>
+    /// 同意
+    /// </summary>
+    [HttpPost]
+    [ApiDescriptionSettings(Name = "Approve")]
+    [DisplayName("同意")]
+    public async Task Approve(TaskActionInput input)
+    {
+        await _engine.Approve(input.TaskId, input.Comment);
+    }
+
+    /// <summary>
+    /// 拒绝
+    /// </summary>
+    [HttpPost]
+    [ApiDescriptionSettings(Name = "Reject")]
+    [DisplayName("拒绝")]
+    public async Task Reject(TaskActionInput input)
+    {
+        await _engine.Reject(input.TaskId, input.Comment);
+    }
+
+    /// <summary>
+    /// 转办
+    /// </summary>
+    [HttpPost]
+    [ApiDescriptionSettings(Name = "Transfer")]
+    [DisplayName("转办")]
+    public async Task Transfer(TaskTransferInput input)
+    {
+        await _engine.Transfer(input.TaskId, input.TargetUserId, input.Comment);
+    }
+
+    /// <summary>
+    /// 撤回
+    /// </summary>
+    [HttpPost]
+    [ApiDescriptionSettings(Name = "Withdraw")]
+    [DisplayName("撤回")]
+    public async Task Withdraw(TaskWithdrawInput input)
+    {
+        await _engine.Withdraw(input.InstanceId);
+    }
+
+    /// <summary>
+    /// 退回上一步
+    /// </summary>
+    [HttpPost]
+    [ApiDescriptionSettings(Name = "ReturnToPrev")]
+    [DisplayName("退回上一步")]
+    public async Task ReturnToPrev(TaskActionInput input)
+    {
+        await _engine.ReturnToPrev(input.TaskId, input.Comment);
+    }
+
+    /// <summary>
+    /// 加签
+    /// </summary>
+    [HttpPost]
+    [ApiDescriptionSettings(Name = "AddSign")]
+    [DisplayName("加签")]
+    public async Task AddSign(TaskAddSignInput input)
+    {
+        await _engine.AddSign(input.TaskId, input.TargetUserId, input.Comment);
+    }
+
+    /// <summary>
+    /// 催办
+    /// </summary>
+    [HttpPost]
+    [ApiDescriptionSettings(Name = "Urge")]
+    [DisplayName("催办")]
+    public async Task Urge(TaskUrgeInput input)
+    {
+        await _engine.Urge(input.InstanceId);
+    }
+}

+ 61 - 50
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/SysApproval/SysApprovalService.cs

@@ -1,20 +1,25 @@
-// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
-//
-// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
-//
-// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
-
 using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Options;
 
 namespace Admin.NET.Plugin.ApprovalFlow.Service;
 
+/// <summary>
+/// 审批流审计服务 — 仅做请求审计日志,不用于核心审批推进
+/// </summary>
 public class SysApprovalService : ITransient
 {
     private readonly SqlSugarRepository<ApprovalFlowRecord> _approvalFlowRep;
     private readonly SqlSugarRepository<ApprovalFormRecord> _approvalFormRep;
     private readonly ApprovalFlowService _approvalFlowService;
 
-    public SysApprovalService(SqlSugarRepository<ApprovalFlowRecord> approvalFlowRep, SqlSugarRepository<ApprovalFormRecord> approvalFormRep, ApprovalFlowService approvalFlowService)
+    private static List<string>? _cachedRoutes;
+    private static DateTime _cacheTime = DateTime.MinValue;
+    private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5);
+
+    public SysApprovalService(
+        SqlSugarRepository<ApprovalFlowRecord> approvalFlowRep,
+        SqlSugarRepository<ApprovalFormRecord> approvalFormRep,
+        ApprovalFlowService approvalFlowService)
     {
         _approvalFlowRep = approvalFlowRep;
         _approvalFormRep = approvalFormRep;
@@ -22,60 +27,66 @@ public class SysApprovalService : ITransient
     }
 
     /// <summary>
-    /// 匹配审批流程
+    /// 匹配审批流程(审计模式 — 仅记录,不阻断)
     /// </summary>
-    /// <param name="context"></param>
-    /// <returns></returns>
     [NonAction]
     public async Task MatchApproval(HttpContext context)
     {
-        var request = context.Request;
-        var response = context.Response;
+        var path = context.Request.Path.ToString();
+        var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
 
-        var path = request.Path.ToString().Split("/");
+        if (segments.Length < 3 || !segments[0].Equals("api", StringComparison.OrdinalIgnoreCase))
+            return;
 
-        var method = request.Method;
-        var qs = request.QueryString;
-        var h = request.Headers;
-        var b = request.Body;
+        var funcName = segments[1];
+        var typeName = segments[2];
 
-        var requestHeaders = request.Headers;
-        var responseHeaders = response.Headers;
-
-        var serviceName = path[1];
-        if (serviceName.StartsWith("api"))
+        var routes = await GetCachedRoutes();
+        if (!routes.Any(r =>
         {
-            if (path.Length > 3)
-            {
-                var funcName = path[2];
-                var typeName = path[3];
+            var parts = r.Split('/');
+            return parts.Length >= 2
+                && parts[0].Equals(funcName, StringComparison.OrdinalIgnoreCase)
+                && parts[1].Equals(typeName, StringComparison.OrdinalIgnoreCase);
+        }))
+            return;
 
-                var list = await _approvalFlowService.FormRoutes();
-                if (list.Any(u => u.Contains(funcName) && u.Contains(typeName)))
-                {
-                    var approvalFlow = new ApprovalFlowRecord
-                    {
-                        FormName = funcName,
-                        CreateTime = DateTime.Now,
-                    };
+        var approvalFlow = new ApprovalFlowRecord
+        {
+            FormName = funcName,
+            CreateTime = DateTime.Now,
+        };
+        await _approvalFlowRep.InsertAsync(approvalFlow);
 
-                    // 判断是否需要审批
-                    await _approvalFlowRep.InsertAsync(approvalFlow);
+        var approvalForm = new ApprovalFormRecord
+        {
+            FlowId = approvalFlow.Id,
+            FormName = funcName,
+            FormType = typeName,
+            CreateTime = DateTime.Now,
+        };
+        await _approvalFormRep.InsertAsync(approvalForm);
+    }
 
-                    var approvalForm = new ApprovalFormRecord
-                    {
-                        FlowId = approvalFlow.Id,
-                        FormName = funcName,
-                        FormType = typeName,
-                        CreateTime = DateTime.Now,
-                    };
+    /// <summary>
+    /// 带缓存的路由列表,避免每个请求都查数据库 (B1 fix)
+    /// </summary>
+    private async Task<List<string>> GetCachedRoutes()
+    {
+        if (_cachedRoutes != null && DateTime.Now - _cacheTime < CacheDuration)
+            return _cachedRoutes;
 
-                    // 判断是否需要审批
-                    await _approvalFormRep.InsertAsync(approvalForm);
-                }
-            }
-        }
+        _cachedRoutes = await _approvalFlowService.FormRoutes();
+        _cacheTime = DateTime.Now;
+        return _cachedRoutes;
+    }
 
-        await Task.CompletedTask;
+    /// <summary>
+    /// 清除路由缓存(发布/更新流程时调用)
+    /// </summary>
+    [NonAction]
+    public static void InvalidateRouteCache()
+    {
+        _cachedRoutes = null;
     }
-}
+}