技术栈:

前端

vue3+TypeScript+Element-Plus

- 插槽 slot

- 本地缓存

后端

java+Spring Boot+Mybatis-Plus

- DTO、BO、VO的转换

- 基于 Mybatis 执行数据库存储过程(有返回数据集、无返回数据集)

- 日期范围查询,传递字符串参数,由数据库(SQL Server)处理:

开始日期条件:将字符串转为日期,并取当天开始时间

and cam_CreateDate >= CONVERT(datetime, #{queryRequest.createDateRange.beginDate}, 120)

结束日期条件:取当天结束时间(即第二天开始时间的前一毫秒)

and cam_CreateDate < DATEADD(day, 1, CONVERT(datetime, #{queryRequest.createDateRange.endDate}, 120))

- BaseMapper 批量插入、批量更新、批量删除,基于 MyBatis-Plus 3.5.0+

- 本地缓存

开发规范

- 请求对象:前后端命名一致,大驼峰,后缀 Request,如:CapitalAllocateQueryRequest

- 响应对象:前后端命名一致,大驼峰,后缀 Response,如:CapitalAllocateResponse

- 变量命名:前后端涉及 List、Array、[] 的统一使用复数,如:capitalAllocates

- 方法命名:前后端涉及 List、Array、[] 的统一使用复数,如:saveCapitalAllocateDetails

- 前端API文件命名:小驼峰,如:fileUpload.ts

- 前端API获取类的方法命名:前缀 fetch,如:fetchCapitalAllocateDetailsByCapitalId

- 前端Store文件命名:小驼峰,如:capitalAllocateStore.ts

- 前端Store命名:小驼峰,前缀 use,后缀 Store,如:useCapitalAllocateStore

- 前端Hook文件命名:小驼峰,前缀 use,如:useActivityInstanceSubmitOption.ts

- 前端Hook命名:小驼峰,前缀 use,如:useActivityInstanceSubmitOption

- 前端Store/Hook获取类的方法命名:前缀 get,如:getCapitalAllocatePage

- 前端事件命名:前缀 handle,如:handleDelete

- 前端type文件命名:大驼峰,如:Capital.ts

- 前端枚举文件命名:大驼峰,后缀.enum,如:Capital.enum.ts

- 前端接口/枚举命名:大驼峰,如:WorkflowInstanceNode

- 后端控制层:活动对象有 DTO、VO

- 后端服务层:活动对象有 Entity、DTO、BO

- 后端Mapper:活动对象有 Entity、DTO

- 后端DTO、VO复数:小写复数,如:capitalAllocateDTOs、capitalAllocateVOs

应用效果:

主界面

若尚未获取数据,则自动刷新加载数据。数据加载后,打开其他页面,再回来时,保留数据状态,可以手动刷新数据。

查看

办理

退回

前端核心代码:

主界面组件:资金分配

src\views\capital\CapitalAllocate.vue

<script setup lang="ts">
/**
 * 资金分配
 */
defineOptions({
  name: "CapitalAllocate"
});
import { capitalAllocateApi, fetchActivityEditableFields } from "@/api";
import {
  CapitalAllocateDetailForm,
  CapitalAllocateMainForm,
  CapitalAllocateOperation,
  CapitalAllocateQueryDialog,
  CapitalAllocateTable,
  CommonPageTitle,
  WorkflowMainDialog
} from "@/components";
import { useCapitalSourceOptions } from "@/hooks";
import { useCapitalAllocateStore } from "@/stores";
import type {
  ActivityEditableFieldResponse,
  CapitalAllocateDetailResponse,
  CapitalAllocateQueryRequest,
  CapitalAllocateResponse
} from "@/types";
import { WorkflowInstanceState } from "@/types";
import { ElMessage } from "element-plus";
import { computed, nextTick, onMounted, ref, watch } from "vue";

// Store
const store = useCapitalAllocateStore();

// 查询对话框显示标识
const queryDialogVisible = ref(false);

// 资金分配表格实例 ref
const capitalAllocateTableRef = ref<InstanceType<typeof CapitalAllocateTable> | null>(null);
// 边框标识
const isBorder = ref(false);

// 资金分配明细列表
const allocateDetails = ref<CapitalAllocateDetailResponse[]>([]);
// 标记:资金分配明细列表数据是否加载中
const detailLoading = ref(false);
// 标记:资金分配明细列表数据是否被修改
const detailModified = ref(false);
// 标记:资金分配明细列表数据是否保存中
const detailSaving = ref(false);
// 标记:资金分配明细列表数据是否已加载完成,用于处理allocateDetails监听,数据刚加载完成,视为未被修改
const detailLoaded = ref(false);
// 资金分配明细使用部门,使用部门id生成办理选项缓存key
const detailDeptId = computed(() =>
  allocateDetails.value && allocateDetails.value.length > 0 ? allocateDetails.value[0].deptId : ""
);

// 流程对话框实例 ref
const workflowMainDialogRef = ref<InstanceType<typeof WorkflowMainDialog> | null>(null);

// 基础选项:资金来源选项
const { capitalSourceOptions } = useCapitalSourceOptions();

// 获取可编辑字段列表
const editableFields = ref<ActivityEditableFieldResponse[]>([]);
// 计算属性:可编辑字段列表(已去重)
const editableFieldNames = computed(() => {
  const names = editableFields.value.map((item) => item.fieldName);
  return [...new Set(names)];
});

// 处理分页改变(改变当前页码或每页大小)
const handlePageChange = async (currentPage: number, pageSize: number) => {
  store.queryRequest.current = currentPage;
  store.queryRequest.size = pageSize;
  await getPageData();
};

// 获取分页数据
const getPageData = async () => {
  await store.getCapitalAllocatePage();
};

// 刷新数据
const handleRefresh = async (event?: MouseEvent, done?: () => void) => {
  try {
    // store 初始化,重置数据状态
    store.$reset();

    // 设置刷新专属的查询参数
    store.clearQueryRequest();
    store.queryRequest.allocateStates = [WorkflowInstanceState.AUDITING]; // 只能查询办理中的数据
    store.setCurrentPage(1);
    await getPageData();
  } finally {
    // 操作完成后调用 done,通知子组件(CapitalAllocateOperation),再由子组件(CapitalAllocateOperation)通知孙组件(BasePreventReClickButtonEmit)结束加载状态(确保 done 存在才调用)
    done?.();
  }
};

// 搜索
const handleSearch = async () => {
  if (!store.keyword) {
    ElMessage.warning("请输入关键字进行搜索");
    return;
  }

  // 设置搜索专属的查询参数
  store.clearQueryRequest();
  store.queryRequest.allocateStates = [WorkflowInstanceState.AUDITING]; // 只能查询办理中的数据
  store.queryRequest.capitalNo = store.keyword;
  store.queryRequest.capitalName = store.keyword;
  store.setCurrentPage(1);
  await getPageData();
};

// 查询
const handleQuery = async () => {
  handleQueryReset();

  queryDialogVisible.value = true;
};

// 查询对话框 - 查询
const handleQueryConfirm = async (event?: MouseEvent, done?: () => void) => {
  try {
    // 检查
    if (
      !store.queryRequest.capitalNo &&
      !store.queryRequest.capitalName &&
      !store.queryRequest.capitalType &&
      !store.queryRequest.capitalSource &&
      store.queryRequest.allocateStates?.length === 0 &&
      (!store.queryRequest.createDateRange?.beginDate || !store.queryRequest.createDateRange?.endDate)
    ) {
      ElMessage.warning("请输入查询条件");
      return;
    }

    store.setCurrentPage(1);
    await getPageData();

    // 关闭对话框、清空搜索关键字
    handleQueryCancel();
    store.clearKeyword();
  } finally {
    // 操作完成后调用 done,通知子组件(CapitalAllocateQueryDialog),再由子组件(CapitalAllocateQueryDialog)通知孙组件(BasePreventReClickButtonEmit)结束加载状态(确保 done 存在才调用)
    done?.();
  }
};

// 查询对话框 - 重置
const handleQueryReset = () => {
  store.clearQueryRequest();
};

// 查询对话框 - 取消
const handleQueryCancel = () => {
  queryDialogVisible.value = false;
};

// 查询对话框 - 处理更改查询请求对象
const handleUpdateQueryRequest = (newQueryRequest: CapitalAllocateQueryRequest) => {
  store.updateQueryRequest(newQueryRequest);
};

// 表格行点击
const handleRowClick = (row: CapitalAllocateResponse) => {
  // 防止重复点击
  if (row.id === store.currentCapitalAllocate?.id) {
    return;
  }

  // 设置当前选择行
  store.setCurrentCapitalAllocate(row);
};

// 查看
const handleView = async (row: CapitalAllocateResponse) => {
  // 设置当前选择行
  store.setCurrentCapitalAllocate(row);
  // 设置当前选择行高亮显示
  capitalAllocateTableRef.value?.setCurrentRow(row);

  // 等待 DOM 更新,确保 WorkflowMainDialog 渲染完成
  await nextTick();

  // 打开对话框
  workflowMainDialogRef.value?.openDialog();

  // 获取数据
  if (store.currentCapitalAllocate) {
    getData(store.currentCapitalAllocate.id, store.currentCapitalAllocate.activityInstance.activityId);
  }
};

// 获取数据
const getData = async (id: number, activityId: number) => {
  detailLoading.value = true;

  try {
    // 并发请求
    const [detailResult, fieldResult] = await Promise.allSettled([
      // 获取资金分配明细列表
      capitalAllocateApi.fetchDetailsByAllocateId(id),
      // 获取活动可编辑字段
      fetchActivityEditableFields(activityId)
    ]);

    // 辅助函数:处理单个结果,返回是否成功(fulfilled且有数据)
    const handleResult = (result: PromiseSettledResult<any>, ref: any, errorMsg: string): boolean => {
      if (result.status === "fulfilled" && result.value?.data) {
        ref.value = result.value.data;
        return true;
      } else {
        return false;
      }
    };

    // 处理数据
    handleResult(detailResult, allocateDetails, "资金分配明细加载失败");
    handleResult(fieldResult, editableFields, "可编辑字段加载失败");

    // 等待 watch 回调执行(此时 detailLoaded 仍为 false,不会错误修改 detailModified,解决数据刚加载完时不会是被修改状态)
    await nextTick();

    detailLoaded.value = true;
    detailModified.value = false;
  } catch (error) {
    console.error("请求过程发生意外错误", error);
    ElMessage.error("系统错误,请稍后重试");
  } finally {
    detailLoading.value = false;
  }
};

// 删除
const handleDelete = () => {};

// 设置列宽
const handleSetColumnWidth = () => {
  isBorder.value = !isBorder.value;
};

// 重置列宽
const handleResetColumnWidth = () => {
  capitalAllocateTableRef.value?.resetColumnWidth();
};

// 处理已办理(提交或退回后),本地删除数据(表格当前选择行)
const handleAudited = () => {
  store.deleteCurrentCapitalAllocate();
};

// 校验分配明细数据
const validateDetail = () => {
  if (editableFieldNames.value.includes("userMoniker")) {
    const emptyUserPersons = allocateDetails.value.filter((item) => !item.userPerson);
    if (emptyUserPersons.length > 0) {
      ElMessage.warning("使用人不能为空");
      return false;
    }
  }

  return true;
};

// 保存分配明细数据
const handleSaveDetail = async (done: () => void) => {
  detailSaving.value = true;
  try {
    await capitalAllocateApi.saveDetails(allocateDetails.value);
    detailModified.value = false;
    done();
  } finally {
    detailSaving.value = false;
  }
};

// 深度监听 allocateDetails 内部变化,仅在数据加载完成后才响应
watch(
  () => allocateDetails.value,
  () => {
    // 只有数据已加载完成,且不是由程序重置引起的修改,才视为用户修改
    if (detailLoaded.value) {
      detailModified.value = true;
    }
  },
  { deep: true }
);

// 组件挂载后,若 store 中尚无数据,就自动刷新数据(这时刷新数据按钮不会显示加载状态)
onMounted(() => {
  if (store.capitalAllocates.length === 0) {
    handleRefresh();
  }

  // 从其他页面跳转回来时,重新设置表格当前行高亮显示
  if (store.currentCapitalAllocate) {
    capitalAllocateTableRef.value?.setCurrentRow(store.currentCapitalAllocate);
  }
});
</script>

<template>
  <el-container class="container">
    <el-header class="header">
      <!-- 页面标题 -->
      <CommonPageTitle
        :title="store.moduleName ?? `资金分配`"
        @set-column-width="handleSetColumnWidth"
        @reset-column-width="handleResetColumnWidth" />

      <!-- 操作栏 -->
      <CapitalAllocateOperation
        v-model:keyword="store.keyword"
        :current-capital-allocate="store.currentCapitalAllocate"
        @refresh="handleRefresh"
        @search="handleSearch"
        @query="handleQuery" />
    </el-header>

    <el-main class="main">
      <!-- 表格 -->
      <CapitalAllocateTable
        ref="capitalAllocateTableRef"
        :data="store.capitalAllocates"
        :loading="store.loading"
        :border="isBorder"
        @row-click="handleRowClick"
        @view="handleView"
        @delete="handleDelete" />
    </el-main>

    <el-footer class="footer">
      <!-- 分页 -->
      <el-pagination
        :total="store.total"
        :page-sizes="[20, 50, 100, 200, 1000]"
        v-model:page-size="store.queryRequest.size"
        v-model:current-page="store.queryRequest.current"
        background
        layout="total, sizes, prev, pager, next, jumper"
        @change="handlePageChange" />
    </el-footer>
  </el-container>

  <div>
    <!-- 查询对话框 -->
    <CapitalAllocateQueryDialog
      v-model:visible="queryDialogVisible"
      :queryRequest="store.queryRequest"
      @update:queryRequest="handleUpdateQueryRequest"
      :capital-source-options="capitalSourceOptions"
      :state-options="store.stateOptions"
      @confirm="handleQueryConfirm"
      @reset="handleQueryReset"
      @cancel="handleQueryCancel" />

    <!-- 资金分配流程对话框 -->
    <!-- <CapitalAllocateWorkflowInstanceDialog
      ref="capitalAllocateWorkflowInstanceDialogRef"
      :capital-allocate="store.currentCapitalAllocate"
      @audited="handleAudited" /> -->

    <WorkflowMainDialog
      v-if="store.currentCapitalAllocate?.activityInstance"
      ref="workflowMainDialogRef"
      title="资金分配"
      :activity-instance="store.currentCapitalAllocate?.activityInstance"
      :loading="detailLoading"
      :is-modified="detailModified"
      :dept-id="detailDeptId"
      :saving="detailSaving"
      :validate="validateDetail"
      :save-handler="handleSaveDetail"
      @audited="handleAudited">
      <!-- 默认插槽 + 作用域插槽:业务内容 -->
      <template #default>
        <!-- 基本信息展示表单 -->
        <CapitalAllocateMainForm :capital-allocate="store.currentCapitalAllocate" />

        <!-- 分配明细表格表单 -->
        <CapitalAllocateDetailForm
          :allocate-details="allocateDetails"
          :loading="detailLoading"
          :editable-field-names="editableFieldNames" />
      </template>
    </WorkflowMainDialog>
  </div>
</template>

<style scoped lang="scss">
// 选择 container 所有直接子元素(不包括孙级)
.container > * {
  margin: 0;
  padding: 0;
}

.container {
  height: 100%;
  border: 1px solid #ebeef5;

  .header {
    height: auto;
  }

  .main {
    border: 1px solid #ebeef5;

    .content-container {
      height: 100%;

      .content-container-aside {
        border-right: 1px solid #ebeef5;
        padding-right: 2px;
        width: 200px;
      }

      .content-container-main {
        margin: 0;
        padding: 0;
      }
    }
  }

  .footer {
    height: auto;
    padding: 0 10px;
  }
}
</style>

子组件

页面标题组件

src\components\common\CommonPageTitle.vue

<script setup lang="ts">
/**
 * 页面标题组件
 */
defineOptions({
  name: "CommonPageTitle"
});

interface Props {
  /** 标题文本 */
  title?: string;
}
const props = defineProps<Props>();

const emit = defineEmits<{
  (e: "set-column-width"): void;
  (e: "reset-column-width"): void;
}>();

// 设置列宽
const onSetColumnWidthClick = () => {
  emit("set-column-width");
};

// 重置列宽
const onResetColumnWidthClick = () => {
  emit("reset-column-width");
};
</script>

<template>
  <div class="header-title">
    <span>{{ title }}</span>
    <el-dropdown split-button trigger="hover" @click="onSetColumnWidthClick">
      设置列宽
      <template #dropdown>
        <el-dropdown-menu>
          <el-dropdown-item @click="onResetColumnWidthClick">重置列宽</el-dropdown-item>
        </el-dropdown-menu>
      </template>
    </el-dropdown>
  </div>
</template>

<style scoped lang="scss">
.header-title {
  margin: 10px;
  font-size: 18px;
  display: flex;
  justify-content: space-between;
}
</style>

资金分配操作栏组件

src\views\capital\CapitalAllocate\comps\CapitalAllocateOperation.vue

<script setup lang="ts">
/**
 * 资金分配操作栏组件
 */
defineOptions({
  name: "CapitalAllocateOperation"
});
import { BasePreventReClickButton, BasePreventReClickButtonEmit } from "@/components";
import type { CapitalAllocateResponse } from "@/types";
import { Search } from "@element-plus/icons-vue";

interface Props {
  /** 搜索关键字 */
  keyword: string;
  /** 当前选中的资金分配 */
  currentCapitalAllocate: CapitalAllocateResponse | null;
}
const props = defineProps<Props>();

// 调用签名语法(Call Signature)
// 类型定义与 emit 的实际调用方式高度一致,例如 emit("update:visible", true)。
// 可以为每个事件添加 JSDoc 注释,便于文档生成。
// const emit = defineEmits<{
//   /**
//    * 更新搜索关键字
//    * @param value 搜索关键字
//    */
//   (e: "update:keyword", value: string): void;
//   /**
//    * 刷新数据
//    */
//   (e: "refresh"): void;
//   /**
//    * 搜索
//    */
//   (e: "search"): void;
//   /**
//    * 查询
//    */
//   (e: "query"): void;
// }>();

// 对象字面量语法(Vue 3.3+ 推荐)
// 更加简洁,尤其适合事件参数简单的情况。
// 对事件参数的校验更严格(元组中的顺序和类型必须匹配)。
const emit = defineEmits<{
  /**
   * 更新搜索关键字
   * @param value 搜索关键字
   */
  "update:keyword": [value: string];
  /**
   * 刷新数据
   */
  refresh: [event: MouseEvent, done: () => void];
  /**
   * 搜索
   */
  search: [];
  /**
   * 查询
   */
  query: [];
}>();
</script>

<template>
  <div class="header-operation">
    <div class="button-group">
      <BasePreventReClickButton class="short-btn" type="primary" plain @click="emit(`query`)">
        查询
      </BasePreventReClickButton>

      <!-- 确保 $emit 在当前作用域可用(<script setup> 中 $emit 会自动暴露给模板) -->
      <!-- 使用 @click="(...args) => $emit(`refresh`, ...args)" 简化替代 @click="(event, done) => emit(`refresh`, event, done)" -->
      <BasePreventReClickButtonEmit
        class="long-btn"
        type="primary"
        plain
        @click="(...args) => $emit(`refresh`, ...args)"
        :timeout="0">
        刷新数据
      </BasePreventReClickButtonEmit>
    </div>

    <div class="query-div">
      <el-input
        class="query-input"
        :model-value="keyword"
        @update:model-value="emit(`update:keyword`, $event)"
        placeholder="输入资金序号或名称搜索"
        clearable>
        <template #append>
          <BasePreventReClickButton type="primary" :icon="Search" @click="emit(`search`)" :delay="100" />
        </template>
      </el-input>
    </div>
  </div>
</template>

<style scoped lang="scss">
.header-operation {
  margin: 10px;
  display: flex;
  justify-content: space-between;

  .query-div {
    display: flex;
    justify-content: flex-end;

    .query-input {
      width: 230px;
    }
  }
}
</style>

资金分配表格组件

src\views\capital\CapitalAllocate\comps\CapitalAllocateTable.vue

<script setup lang="ts">
/**
 * 资金分配表格组件
 */
defineOptions({
  name: "CapitalAllocateTable"
});
import { BasePreventReClickButton, BasePreventReClickButtonEmit } from "@/components";
import { useSetTableColumnWidth, useWorkflowInstanceStateFormatter } from "@/hooks";
import type { CapitalAllocateResponse, ElTableExtendedInstance, IColumnConfig } from "@/types";
import { WorkflowInstanceState } from "@/types";
import type { TableColumnCtx } from "element-plus";
import { ElMessage } from "element-plus";
import { computed, nextTick, onMounted, ref } from "vue";

interface Props {
  /** 表格数据 */
  data: CapitalAllocateResponse[];
  /** 加载状态 */
  loading: boolean;
  /** 是否显示边框 */
  border?: boolean;
}
const props = defineProps<Props>();

// 调用签名语法(Call Signature)
// 类型定义与 emit 的实际调用方式高度一致,例如 emit("update:visible", true)。
// 可以为每个事件添加 JSDoc 注释,便于文档生成。
// const emit = defineEmits<{
//   /**
//    * 行点击事件
//    * @param row 行数据
//    */
//   (e: "row-click", row: CapitalAllocate): void;
//   /**
//    * 查看
//    * @param row 行数据
//    */
//   (e: "view", row: CapitalAllocate): void;
//   /**
//    * 删除
//    * @param row 行数据
//    */
//   (e: "delete", row: CapitalAllocate): void;
// }>();

// 对象字面量语法(Vue 3.3+ 推荐)
// 更加简洁,尤其适合事件参数简单的情况。
// 对事件参数的校验更严格(元组中的顺序和类型必须匹配)。
const emit = defineEmits<{
  /**
   * 行点击事件
   * @param row 行数据
   */
  "row-click": [row: CapitalAllocateResponse];
  /**
   * 查看
   * @param row 行数据
   */
  view: [row: CapitalAllocateResponse];
  /**
   * 删除
   * @param row 行数据
   */
  delete: [row: CapitalAllocateResponse];
}>();

// 表格实例 ref
const tableRef = ref<ElTableExtendedInstance | null>(null);

// 表格列配置,【后续优化:从数据库中获取,或者传递参数,由 useSetTableColumnWidth 从数据库中获取】
const columnConfigs = ref<IColumnConfig[]>([
  { prop: "createTime", label: "创建时间", width: 165 },
  { prop: "capitalNo", label: "资金序号", width: 150 },
  { prop: "capitalName", label: "资金名称", width: 250 },
  { prop: "capitalType", label: "资金类别", width: 250 },
  { prop: "capitalSource", label: "资金来源", width: 120 },
  { prop: "capitalTotal", label: "指标预算总额", width: 120 },
  { prop: "capitalValidTotal", label: "指标可用总额", width: 120 },
  { prop: "allocateState", label: "状态", width: 100 }
]);

// 设置表格列宽
const setTableColumnWidth = computed(() => {
  return tableRef.value ? useSetTableColumnWidth(tableRef, columnConfigs, ref("CapitalAllocate_main")) : null;
});

// 状态格式化
const { formatState } = useWorkflowInstanceStateFormatter();

// 行点击
const handleRowClick = (row: CapitalAllocateResponse) => {
  emit("row-click", row);
};

// 查看
const handleView = (row: CapitalAllocateResponse) => {
  emit("view", row);
};

// 删除
const handleDelete = (row: CapitalAllocateResponse) => {
  emit("delete", row);
};

// 处理表头拖拽结束
const handleHeaderDragend = (
  newWidth: number,
  oldWidth: number,
  column: TableColumnCtx<CapitalAllocateResponse>,
  event: MouseEvent
) => {
  // 保存表格列宽
  setTableColumnWidth.value?.saveColumnWidth(newWidth, oldWidth, column, event);
};

onMounted(async () => {
  await nextTick();

  // 设置表格列宽
  setTableColumnWidth.value?.setColumnWidth();
});

// 暴露方法供父组件调用
defineExpose({
  /** 设置当前高亮行 */
  setCurrentRow: (row: CapitalAllocateResponse) => {
    tableRef.value?.setCurrentRow(row);
  },
  /** 重置列宽 */
  resetColumnWidth: () => {
    setTableColumnWidth.value?.resetColumnWidth();
    ElMessage.success("重置列宽成功,重新加载页面后生效");
  }
});
</script>

<template>
  <el-table
    ref="tableRef"
    :data="props.data"
    v-loading="props.loading"
    highlight-current-row
    stripe
    :border="props.border"
    style="height: 100%"
    @header-dragend="handleHeaderDragend"
    @row-click="handleRowClick">
    <!-- 数据列 -->
    <el-table-column prop="createTime" label="创建时间" width="165" fixed="left" />
    <el-table-column prop="capitalNo" label="资金序号" width="150" fixed="left" />
    <el-table-column prop="capitalName" label="资金名称" min-width="250" fixed="left" />
    <el-table-column prop="capitalType" label="资金类别" min-width="250" />
    <el-table-column prop="capitalSource" label="资金来源" width="120" />
    <el-table-column prop="capitalTotal" label="指标预算总额" width="120" />
    <el-table-column prop="capitalValidTotal" label="指标可用总额" width="120" />
    <el-table-column prop="allocateState" label="状态" width="100">
      <template #default="scoped">
        {{ formatState(scoped.row.allocateState) }}
      </template>
    </el-table-column>

    <!-- 操作列 -->
    <el-table-column label="操作" min-width="150" header-align="center" align="center" fixed="right">
      <template #default="{ row }">
        <!-- 防抖按钮,使用 @click.stop 阻止冒泡,这样就只会触发表格行内的按钮点击事件 @click,不会触发表格行点击事件 @row-click -->
        <BasePreventReClickButtonEmit
          class="table-btn"
          type="primary"
          size="default"
          link
          :show-loading="false"
          :timeout="1"
          @click.stop="handleView(row)">
          查看
        </BasePreventReClickButtonEmit>

        <!-- 防抖按钮,不阻止冒泡,这样触发表格行内的按钮点击事件 @click,也会触发表格行点击事件 @row-click -->
        <BasePreventReClickButton
          class="table-btn"
          type="danger"
          size="default"
          link
          :loading="false"
          :disabled="
            (row as CapitalAllocateResponse).allocateState === WorkflowInstanceState.CLOSED ||
            (row as CapitalAllocateResponse).allocateState === WorkflowInstanceState.AUDITING
          "
          @click="handleDelete(row)">
          删除
        </BasePreventReClickButton>
      </template>
    </el-table-column>
  </el-table>
</template>

<style scoped lang="scss"></style>

资金分配查询对话框组件

src\views\capital\CapitalAllocate\comps\CapitalAllocateQueryDialog.vue

<script setup lang="ts">
/**
 * 资金分配查询对话框组件
 */
defineOptions({
  name: "CapitalAllocateQueryDialog"
});
import { BasePreventReClickButtonEmit } from "@/components";
import type { CapitalAllocateQueryRequest } from "@/types";
import { computed } from "vue";

interface Props {
  /** 对话框显示标识 */
  visible: boolean;
  /** 查询请求对象(因为是对象类型,不使用v-model,而是使用:model-value + @update:model-value) */
  queryRequest: CapitalAllocateQueryRequest;
  /** 资金来源选项列表 */
  capitalSourceOptions: string[];
  /** 资金状态选项列表 */
  stateOptions: readonly { label: string; value: number }[];
}
const props = defineProps<Props>();

// 调用签名语法(Call Signature)
// 类型定义与 emit 的实际调用方式高度一致,例如 emit("update:visible", true)。
// 可以为每个事件添加 JSDoc 注释,便于文档生成。
// const emit = defineEmits<{
//   /**
//    * 更新对话框可见性
//    * @param visible 新的可见状态
//    */
//   (e: "update:visible", visible: boolean): void;
//   /**
//    * 更新查询请求对象
//    * @param queryRequest 新的查询请求对象
//    */
//   (e: "update:queryRequest", queryRequest: CapitalAllocateQueryRequest): void;
//   /**
//    * 确定查询
//    */
//   (e: "confirm"): void;
//   /**
//    * 重置
//    */
//   (e: "reset"): void;
//   /**
//    * 取消
//    */
//   (e: "cancel"): void;
// }>();

// 对象字面量语法(Vue 3.3+ 推荐)
// 更加简洁,尤其适合事件参数简单的情况。
// 对事件参数的校验更严格(元组中的顺序和类型必须匹配)。
const emit = defineEmits<{
  /**
   * 更新对话框可见性
   * @param visible 新的可见状态
   */
  "update:visible": [visible: boolean];
  /**
   * 更新查询对象
   * @param queryRequest 新的查询对象
   */
  "update:queryRequest": [queryRequest: CapitalAllocateQueryRequest];
  /**
   * 确定查询
   */
  confirm: [event: MouseEvent, done: () => void];
  /**
   * 重置
   */
  reset: [];
  /**
   * 取消
   */
  cancel: [];
}>();

// 计算属性:日期范围(双向绑定)
const dateRangeValue = computed({
  get: () => {
    const { createDateRange } = props.queryRequest;
    // 如果两个日期都存在,返回数组
    if (createDateRange?.beginDate && createDateRange?.endDate) {
      return [createDateRange.beginDate, createDateRange.endDate];
    }

    // 返回空数组
    return [];
  },
  set: (value: [string, string] | null) => {
    const newQueryObj = { ...props.queryRequest };
    if (value && value.length === 2) {
      // 确保createDateRange对象存在
      if (!newQueryObj.createDateRange) {
        newQueryObj.createDateRange = {
          beginDate: "",
          endDate: ""
        };
      }
      newQueryObj.createDateRange.beginDate = value[0];
      newQueryObj.createDateRange.endDate = value[1];
    } else {
      // 清空日期范围
      if (newQueryObj.createDateRange) {
        newQueryObj.createDateRange.beginDate = "";
        newQueryObj.createDateRange.endDate = "";
      }
    }
    emit("update:queryRequest", newQueryObj);
  }
});

// 确定
const handleConfirm = (event: MouseEvent, done: () => void) => {
  emit("confirm", event, done);
};

// 重置
const handleReset = () => {
  emit("reset");
};

// 关闭对话框(对话框右上角关闭按钮):只供【对话框右上角关闭事件 @close】调用的情况
const handleClose = () => {
  emit("cancel");
  emit("update:visible", false);
};

// 取消:只供【对话框底部取消按钮点击事件 @click】调用的情况
const handleCancel = () => {
  emit("cancel");
  emit("update:visible", false);
};

// // 取消:【对话框底部取消按钮点击事件 @click】和【对话框右上角关闭事件 @close】都是调用 handleCancel 的情况
// const handleCancel = () => {
//   // 如果对话框已经关闭,忽略本次调用(通常由对话框右上角关闭事件 @close 触发)
//   if (!props.visible) return;

//   emit("update:visible", false);
// };
</script>

<template>
  <div>
    <el-dialog
      class="query-dialog"
      title="查询信息"
      width="400px"
      top="0vh"
      center
      style="border-radius: 10px"
      :model-value="visible"
      :close-on-press-escape="true"
      :close-on-click-modal="false"
      :show-close="true"
      @close="handleClose">
      <template #default>
        <el-form :model="queryRequest" label-width="auto" style="margin: 8px 16px">
          <el-row :gutter="10">
            <el-col :span="24">
              <el-form-item label="资金序号" label-position="right">
                <el-input
                  :model-value="queryRequest.capitalNo"
                  @update:model-value="$emit(`update:queryRequest`, { ...queryRequest, capitalNo: $event })"
                  clearable />
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="10">
            <el-col :span="24">
              <el-form-item label="资金名称" label-position="right">
                <el-input
                  :model-value="queryRequest.capitalName"
                  @update:model-value="$emit(`update:queryRequest`, { ...queryRequest, capitalName: $event })"
                  clearable />
              </el-form-item>
            </el-col>
          </el-row>

          <el-row :gutter="10">
            <el-col :span="24">
              <el-form-item label="资金类别" label-position="right">
                <el-input
                  :model-value="queryRequest.capitalType"
                  @update:model-value="$emit(`update:queryRequest`, { ...queryRequest, capitalType: $event })"
                  clearable />
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="10">
            <el-col :span="24">
              <el-form-item label="资金来源" label-position="right">
                <el-select
                  :model-value="queryRequest.capitalSource"
                  @update:model-value="$emit(`update:queryRequest`, { ...queryRequest, capitalSource: $event })"
                  placeholder="请选择"
                  clearable
                  filterable
                  allow-create
                  default-first-option>
                  <el-option v-for="item in capitalSourceOptions" :label="item" :value="item" />
                </el-select>
              </el-form-item>
            </el-col>
          </el-row>

          <el-row :gutter="10">
            <el-col :span="24">
              <el-form-item label="分配状态" label-position="right">
                <el-select
                  :model-value="queryRequest.allocateStates"
                  @update:model-value="$emit(`update:queryRequest`, { ...queryRequest, allocateStates: $event })"
                  placeholder="请选择"
                  clearable
                  filterable
                  default-first-option
                  multiple>
                  <el-option v-for="item in stateOptions" :label="item.label" :value="item.value" />
                </el-select>
              </el-form-item>
            </el-col>
          </el-row>

          <el-row :gutter="10">
            <el-col :span="24">
              <el-form-item label="创建日期" label-position="right">
                <el-date-picker
                  v-model="dateRangeValue"
                  style="width: 240px"
                  type="daterange"
                  unlink-panels
                  start-placeholder="开始日期"
                  range-separator="至"
                  end-placeholder="结束日期"
                  format="YYYY-MM-DD"
                  value-format="YYYY-MM-DD" />
              </el-form-item>
            </el-col>
          </el-row>
        </el-form>
      </template>

      <!-- 底部按钮,模态框底部插槽,就算没有内容,也要写一个空的插槽,否则会影响布局 -->
      <template #footer>
        <div class="footer-div">
          <BasePreventReClickButtonEmit class="btn" type="primary" :timeout="0" @click="handleConfirm">
            查询
          </BasePreventReClickButtonEmit>
          <el-button class="btn" @click="handleReset">重置</el-button>
          <el-button class="btn" @click="handleCancel">取消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>

<style scoped lang="scss"></style>

流程对话框组件

src\components\common\WorkflowMainDialog.vue

<script setup lang="ts">
/**
 * 流程对话框组件
 * 提供流程操作栏(办理、退回、撤回、保存、关闭)和审批信息表格,业务内容通过插槽自定义。
 */
defineOptions({
  name: "WorkflowMainDialog"
});
import { fetchAuditInfos } from "@/api";
import {
  BasePreventReClickButton,
  BasePreventReClickButtonEmit,
  WorkflowRejectDialog,
  WorkflowSubmitDialog
} from "@/components";
import { useActivityInstanceRejectOption, useActivityInstanceSubmitOption } from "@/hooks";
import type { ActivityInstance, AuditInfoResponse } from "@/types";
import { WorkflowInstanceNode, WorkflowInstanceState } from "@/types";
import { ElMessage } from "element-plus";
import { computed, nextTick, onUnmounted, ref, watch } from "vue";

interface Props {
  /** 对话框标题 */
  title?: string;
  /** 活动实例(包含必要信息) */
  activityInstance: ActivityInstance;
  /** 业务数据加载状态 */
  loading?: boolean;
  /** 业务数据是否被修改 */
  isModified?: boolean;
  /** 业务数据是否保存中 */
  saving?: boolean;
  /** 部门id(用于生成缓存键) */
  deptId?: string;
  /** 账号/用户名(用于生成缓存键) */
  userName?: string;
  /** 校验业务数据(返回 false 或 reject 则return) */
  validate?: () => boolean | Promise<boolean>;
  /** 保存函数(接收一个 done 回调,保存完成后调用,如果 emit 这里命名了 save,则 props 那里就不能命名 onSave,不然就有命名冲突) */
  saveHandler?: (done: () => void) => void | Promise<void>;
}
const props = withDefaults(defineProps<Props>(), {
  title: "流程办理",
  loading: false,
  isModified: false,
  saving: false,
  validate: undefined,
  saveHandler: undefined
});

const emit = defineEmits<{
  /**
   * 已办理(办理/退回后)
   */
  audited: [];
  /**
   * 保存(已通过 saveHandler 处理,此事件可作为额外通知,如果 emit 这里命名了 save,则 props 那里就不能命名 onSave,不然就有命名冲突)
   */
  save: [];
}>();

// 对话框显示标识
const visible = ref(false);

// 办理信息列表
const auditInfos = ref<AuditInfoResponse[]>([]);
// 办理信息加载状态
const auditInfoLoading = ref(false);

// 标记:组件是否已卸载
let isMounted = true;
// 用于增强竞态防护
let requestId = 0;

// 流程提交对话框实例 ref
const workflowSubmitDialogRef = ref<InstanceType<typeof WorkflowSubmitDialog> | null>(null);
// 流程退回对话框实例 ref
const workflowRejectDialogRef = ref<InstanceType<typeof WorkflowRejectDialog> | null>(null);

// 按钮禁用状态
// 办理的禁用逻辑:数据加载中、数据被修改、数据保存中、流程实例已结案、流程实例节点为异常情况
const buttonAuditDisabled = computed(
  () =>
    props.loading ||
    props.isModified ||
    props.saving ||
    props.activityInstance.workflowInstanceState === WorkflowInstanceState.CLOSED ||
    props.activityInstance.workflowInstanceNode === WorkflowInstanceNode.UNKNOWN
);
// 退回的禁用逻辑:数据加载中、数据被修改、数据保存中、流程实例已结案、流程实例节点为首活动/启动活动/异常情况
const buttonBackDisabled = computed(
  () =>
    props.loading ||
    props.isModified ||
    props.saving ||
    props.activityInstance.workflowInstanceState === WorkflowInstanceState.CLOSED ||
    props.activityInstance.workflowInstanceNode === WorkflowInstanceNode.FIRST ||
    props.activityInstance.workflowInstanceNode === WorkflowInstanceNode.UNKNOWN
);
// 撤回的禁用逻辑:数据加载中、数据被修改、数据保存中、流程实例已结案、流程实例节点为异常情况、目前暂时禁用
const buttonWithdrawDisabled = computed(
  () =>
    props.loading ||
    props.isModified ||
    props.saving ||
    props.activityInstance.workflowInstanceState === WorkflowInstanceState.CLOSED ||
    props.activityInstance.workflowInstanceNode === WorkflowInstanceNode.UNKNOWN ||
    true
);
// 保存的禁用逻辑:数据加载中、数据保存中、数据未修改、数据为空、保存后
const buttonSaveDisabled = computed(() => props.loading || props.saving || !props.isModified || !props.saveHandler);

// 活动实例退回选项 hook:退回选项、加载状态、获取退回选项
const { rejectOption, rejectLoading, getRejectOption } = useActivityInstanceRejectOption();
// 活动实例提交选项 hook:提交选项、加载状态、获取提交选项
const { submitOption, submitLoading, getSubmitOption } = useActivityInstanceSubmitOption();

// 办理
const handleAudit = async () => {
  // 校验数据
  if (props.validate && !(await props.validate())) return;

  // 打开流程提交对话框
  workflowSubmitDialogRef.value?.openDialog();

  await getSubmitOption(props.activityInstance, props.deptId, props.userName);
};

// 退回
const handleBack = async () => {
  if (!props.activityInstance) return;

  // 打开流程退回对话框
  workflowRejectDialogRef.value?.openDialog();

  await getRejectOption(props.activityInstance);
};

// 撤回
const handleWithdraw = async () => {};

// 保存(带 done 回调)
const handleSaveWithDone = async (event?: MouseEvent, done?: () => void) => {
  if (props.saveHandler) {
    props.saveHandler(() => {
      // 操作完成后调用 done,通知子组件结束加载状态(确保 done 存在才调用)
      done?.();
    });
  } else {
    // 操作完成后调用 done,通知子组件结束加载状态(确保 done 存在才调用)
    done?.();
  }
};

// 关闭
const handleClose = async () => {
  visible.value = false;
};

// 处理办理(提交或退回)时的关闭
const handleCloseOnAudit = async () => {
  emit("audited");

  await nextTick();

  // 关闭对话框
  handleClose();
};

// 获取审批信息
const getAuditInfoList = async (workflowInstanceId: number) => {
  const currentId = ++requestId;
  auditInfoLoading.value = true;

  try {
    const result = await fetchAuditInfos(workflowInstanceId);
    if (!isMounted || currentId !== requestId) return;

    if (result?.data) {
      auditInfos.value = result.data;
    } else {
      ElMessage.error("审批信息加载失败");
    }
  } catch (error) {
    console.error("获取审批信息失败:", error);
    ElMessage.error("审批信息加载异常");
  } finally {
    if (isMounted && currentId === requestId) {
      auditInfoLoading.value = false;
    }
  }
};

// 停止 watch 的函数:监听对话框打开和 activityInstance 变化,自动加载审批信息
const stopWatch = watch(
  // 监听一个包含两个依赖项的数组:visible 和 capitalAllocate 的 id
  () => [visible.value, props.activityInstance?.workflowInstanceId],
  async ([newVisible, newWorkflowInstanceId], [oldVisible, oldWorkflowInstanceId]) => {
    if (newVisible && newWorkflowInstanceId) {
      await getAuditInfoList(newWorkflowInstanceId as number);
    }
  }
);

// 监听部门id、账号/用户名(用于生成缓存键),自动加载办理选项
watch(
  () => [props.deptId, props.userName],
  ([newDeptId, newUserName]) => {
    if (newDeptId || newUserName) {
      getSubmitOption(props.activityInstance, props.deptId, props.userName);
    }
  }
);

onUnmounted(() => {
  isMounted = false;

  // 停止监听,避免内存泄漏
  stopWatch();
});

defineExpose({
  /** 打开资金分配对话框 */
  openDialog: async () => {
    visible.value = true;
  }
});
</script>

<template>
  <div>
    <el-dialog
      class="workflow-main-dialog"
      :title="title"
      width="1130px"
      top="0vh"
      center
      style="border-radius: 10px"
      :model-value="visible"
      :close-on-press-escape="true"
      :close-on-click-modal="false"
      :show-close="true"
      @close="visible = false">
      <template #default>
        <!-- 具名插槽 + 作用域插槽:操作栏插槽(可自定义,默认提供标准按钮) -->
        <slot
          name="actions"
          :auditDisabled="buttonAuditDisabled"
          :backDisabled="buttonBackDisabled"
          :withdrawDisabled="buttonWithdrawDisabled"
          :saveDisabled="buttonSaveDisabled">
          <div class="button-group">
            <BasePreventReClickButtonEmit
              class="short-btn"
              type="primary"
              :disabled="buttonAuditDisabled"
              :show-loading="false"
              :timeout="500"
              @click="handleAudit">
              办理
            </BasePreventReClickButtonEmit>

            <BasePreventReClickButtonEmit
              class="short-btn"
              type="primary"
              plain
              :disabled="buttonBackDisabled"
              :show-loading="false"
              :timeout="500"
              @click="handleBack">
              退回
            </BasePreventReClickButtonEmit>

            <BasePreventReClickButton
              v-if="props.saveHandler"
              class="short-btn"
              type="primary"
              plain
              :disabled="buttonWithdrawDisabled"
              @click="handleWithdraw">
              撤回
            </BasePreventReClickButton>

            <BasePreventReClickButtonEmit
              class="short-btn"
              type="primary"
              plain
              :disabled="buttonSaveDisabled"
              :timeout="0"
              @click="handleSaveWithDone">
              保存
            </BasePreventReClickButtonEmit>

            <!-- 具名插槽:操作栏扩展按钮 -->
            <slot name="actions-expand" />

            <BasePreventReClickButtonEmit
              class="short-btn"
              type="primary"
              plain
              :show-loading="false"
              :timeout="500"
              @click="handleClose">
              关闭
            </BasePreventReClickButtonEmit>
          </div>
        </slot>

        <!-- 默认插槽 + 作用域插槽:业务内容 -->
        <slot :activity-instance="activityInstance" :loading="loading" :saving="saving" :modified="isModified" />

        <!-- 审批信息表格表单 -->
        <el-form class="card card-audit" :model="auditInfos" label-width="auto">
          <el-table :data="auditInfos" v-loading="auditInfoLoading">
            <el-table-column prop="activityName" label="流程节点" width="230" show-overflow-tooltip />
            <el-table-column prop="auditorName" label="办理人" width="125" show-overflow-tooltip />
            <el-table-column prop="auditTime" label="办理时间" width="280" show-overflow-tooltip />
            <el-table-column prop="auditIdea" label="意见" min-width="100" show-overflow-tooltip />
            <el-table-column prop="auditStateDesc" label="状态" width="120" show-overflow-tooltip />
          </el-table>
        </el-form>
      </template>

      <!-- 底部按钮,模态框底部插槽,就算没有内容,也要写一个空的插槽,否则会影响布局 -->
      <template #footer> </template>
    </el-dialog>
  </div>

  <!-- 流程提交对话框 -->
  <WorkflowSubmitDialog
    v-if="activityInstance"
    ref="workflowSubmitDialogRef"
    :activity-instance="activityInstance"
    :audit-option="submitOption"
    :audit-option-loading="submitLoading"
    @submit="handleCloseOnAudit" />

  <!-- 流程退回对话框 -->
  <WorkflowRejectDialog
    v-if="activityInstance"
    ref="workflowRejectDialogRef"
    :activity-instance="activityInstance"
    :audit-option="rejectOption"
    :audit-option-loading="rejectLoading"
    @reject="handleCloseOnAudit" />
</template>

<style scoped lang="scss">
// 现代主流风格:干净、卡片化、柔和阴影、适度圆角
.workflow-main-dialog {
  // 覆盖 Element Plus 默认内边距,让内容直接贴边
  :deep(.el-dialog__header) {
    padding: 16px 20px;
    margin-right: 0;
    border-bottom: 1px solid var(--el-border-color-light);
  }

  :deep(.el-dialog__body) {
    padding: 20px;
  }

  :deep(.el-dialog__footer) {
    padding: 0;
  }

  // 按钮组区域
  .button-group {
    display: flex;
    margin-bottom: 20px;
    margin-left: 16px;

    // 自定义按钮样式
    :deep(.el-button) {
      border-radius: 30px;
      padding: 10px 24px;
      font-weight: 500;
      transition: all 0.2s;

      &.el-button--primary {
        background: linear-gradient(145deg, #2d6ff7, #1e4fd9);
        border: none;
        box-shadow: 0 4px 10px rgba(45, 111, 247, 0.3);
        &:hover {
          background: linear-gradient(145deg, #1e4fd9, #0f3bb3);
          box-shadow: 0 6px 14px rgba(45, 111, 247, 0.4);
          transform: translateY(-1px);
        }
      }

      &.el-button--primary.is-plain {
        background: #ffffff;
        border: 1px solid #d0d9e8;
        color: #1e293b;
        box-shadow: none;
        &:hover {
          border-color: #2d6ff7;
          color: #2d6ff7;
          background-color: #f0f6ff;
        }
      }
    }

    // 按钮禁用状态样式
    :deep(.el-button.is-disabled),
    :deep(.el-button:disabled) {
      opacity: 0.6; // 降低透明度
      cursor: not-allowed; // 显示不可点击光标
      background-color: #f5f7fa !important; // 灰色背景(可根据设计调整)
      border-color: #e4e7ed !important; // 灰色边框
      color: #c0c4cc !important; // 灰色文字
      box-shadow: none !important; // 移除阴影
      pointer-events: auto; // 保持光标样式生效(无需阻止点击,disabled 已阻止)

      // 如果是纯色主按钮,可单独调整
      &.el-button--primary {
        background-color: #a0a0a0 !important;
        border-color: #909090 !important;
      }

      // 如果是朴素按钮,可调整背景为白色,文字灰色等
      &.el-button--primary.is-plain {
        background-color: #fafafa !important;
        color: #b0b0b0 !important;
      }
    }
  }
}

// 卡片通用样式
.card {
  background-color: #ffffff;
  border-radius: 10px;
  margin-bottom: 30px;

  &:last-child {
    margin-bottom: 0;
  }
}

.card-audit {
  padding: 0; // 表格本身自带内边距,卡片内边距交给表格
  overflow: hidden; // 让表格圆角生效

  .el-table {
    --el-table-border-color: transparent; // 隐藏外边框,用卡片阴影代替
    --el-table-header-bg-color: #f9fafb;
    --el-table-tr-bg-color: transparent;
  }

  :deep(.el-table__header-wrapper) {
    th {
      padding: 12px 0;
      border-bottom: 1px solid var(--el-border-color-lighter);
    }
  }

  :deep(.el-table__body-wrapper) {
    td {
      padding: 12px 0;
      border-bottom: 1px solid var(--el-border-color-lighter);
    }
  }

  // 表格加载状态样式优化
  :deep(.el-loading-mask) {
    background-color: rgba(255, 255, 255, 0.7);
    border-radius: 12px;
  }
}
</style>

资金信息基础表单(主项表单)

src\views\capital\CapitalAllocate\comps\CapitalAllocateMainForm.vue

<script setup lang="ts">
/**
 * 资金信息基础表单(主项表单)
 */
defineOptions({
  name: "CapitalAllocateMainForm"
});
import type { CapitalAllocateResponse } from "@/types";

interface Props {
  capitalAllocate: CapitalAllocateResponse;
}
const props = withDefaults(defineProps<Props>(), {
  capitalAllocate: () => ({}) as CapitalAllocateResponse
});
</script>

<template>
  <el-form class="card card-main" :model="capitalAllocate" label-width="auto">
    <el-row :gutter="10">
      <el-col :span="12">
        <el-form-item label="资金序号" label-position="right">
          <el-input :value="capitalAllocate.capitalNo" />
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="资金名称" label-position="right">
          <el-input :value="capitalAllocate.capitalName" />
        </el-form-item>
      </el-col>
    </el-row>

    <el-row :gutter="10">
      <el-col :span="12">
        <el-form-item label="资金来源" label-position="right">
          <el-input :value="capitalAllocate.capitalSource" />
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="资金类别" label-position="right">
          <el-input :value="capitalAllocate.capitalType" />
        </el-form-item>
      </el-col>
    </el-row>

    <el-row :gutter="10">
      <el-col :span="12">
        <el-form-item label="指标预算总额" label-position="right">
          <el-input :value="capitalAllocate.capitalTotal" />
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="指标可用总额" label-position="right">
          <el-input :value="capitalAllocate.capitalValidTotal" />
        </el-form-item>
      </el-col>
    </el-row>
  </el-form>
</template>

<style scoped lang="scss">
// 卡片通用样式
.card {
  background-color: #ffffff;
  border-radius: 10px;
  margin-bottom: 30px;

  &:last-child {
    margin-bottom: 0;
  }
}

// 主要卡片
.card-main {
  :deep(.el-form-item) {
    margin-bottom: 8px; // 紧凑但留白
  }

  :deep(.el-form-item__label) {
    height: 38px;
    line-height: 38px;
  }

  // 只读输入框样式:无边框,浅灰背景,文字加粗
  :deep(.el-input__wrapper) {
    background-color: #f8fafc;
    box-shadow: none;
    border-radius: 10px;
    padding: 4px 12px;
  }
}
</style>

资金分配明细表格表单

src\views\capital\CapitalAllocate\comps\CapitalAllocateDetailForm.vue

<script setup lang="ts">
/**
 * 资金分配明细表格表单
 */
defineOptions({
  name: "CapitalAllocateDetailForm"
});
import { useDepartmentUserTree } from "@/hooks";
import type { CapitalAllocateDetailResponse } from "@/types";

interface Props {
  allocateDetails: CapitalAllocateDetailResponse[];
  loading?: boolean;
  editableFieldNames: string[];
}
const props = withDefaults(defineProps<Props>(), {
  allocateDetails: () => [] as CapitalAllocateDetailResponse[],
  loading: false,
  editableFieldNames: () => [] as string[]
});

// 部门用户树 hook
const { departmentUserTreeData } = useDepartmentUserTree();
</script>

<template>
  <el-form class="card card-detail" :model="allocateDetails" label-width="auto">
    <el-table :data="allocateDetails" v-loading="loading" highlight-current-row>
      <el-table-column prop="deptName" label="指标使用部门" width="230" show-overflow-tooltip />
      <el-table-column prop="total" label="指标分配金额" width="125" show-overflow-tooltip />
      <el-table-column prop="budget" label="预算情况" width="140" show-overflow-tooltip />
      <el-table-column prop="payType" label="支出分类" width="140" show-overflow-tooltip />
      <el-table-column prop="payMode" label="支出方式" width="130" show-overflow-tooltip />
      <el-table-column prop="assistDeptName" label="协助部门" min-width="100" show-overflow-tooltip />
      <el-table-column prop="userMoniker" label="使用人" width="120" show-overflow-tooltip>
        <template #default="{ row }">
          <el-cascader
            v-if="editableFieldNames.includes(`userMoniker`)"
            ref="cascaderRef"
            v-model="row.userPerson"
            :options="departmentUserTreeData"
            :show-all-levels="false"
            clearable
            filterable
            :props="{ emitPath: false }" />
        </template>
      </el-table-column>
    </el-table>
  </el-form>
</template>

<style scoped lang="scss">
// 卡片通用样式
.card {
  background-color: #ffffff;
  border-radius: 10px;
  margin-bottom: 30px;

  &:last-child {
    margin-bottom: 0;
  }
}

// 表格卡片
.card-detail {
  padding: 0; // 表格本身自带内边距,卡片内边距交给表格
  overflow: hidden; // 让表格圆角生效

  .el-table {
    --el-table-border-color: transparent; // 隐藏外边框,用卡片阴影代替
    --el-table-header-bg-color: #f9fafb;
    --el-table-tr-bg-color: transparent;
  }

  :deep(.el-table__header-wrapper) {
    th {
      padding: 12px 0;
      border-bottom: 1px solid var(--el-border-color-lighter);
    }
  }

  :deep(.el-table__body-wrapper) {
    td {
      padding: 12px 0;
      border-bottom: 1px solid var(--el-border-color-lighter);
    }
  }

  // 表格加载状态样式优化
  :deep(.el-loading-mask) {
    background-color: rgba(255, 255, 255, 0.7);
    border-radius: 12px;
  }
}
</style>

Store

src\stores\capitalAllocateStore.ts

import { capitalAllocateApi } from "@/api";
import { useModuleName } from "@/hooks";
import type { CapitalAllocateQueryRequest, CapitalAllocateResponse } from "@/types";
import { ModuleAuthority, WorkflowInstanceState } from "@/types";
import { defineStore } from "pinia";
import { reactive, ref, watch } from "vue";

/**
 * 资金分配 Store
 */
export const useCapitalAllocateStore = defineStore("capitalAllocate", () => {
  /** 模块名称 */
  const { moduleName } = useModuleName(ModuleAuthority.CapitalAllocate);
  /** 资金分配总数 */
  const total = ref(0);
  /** 资金分配列表 */
  const capitalAllocates = ref<CapitalAllocateResponse[]>([]);
  /** 当前的资金分配 */
  const currentCapitalAllocate = ref<CapitalAllocateResponse | null>(null);
  /** 获取默认查询字段(使用函数返回默认值,每次返回全新对象,既避免了引用共享,又比深拷贝更直观,不处理分页参数) */
  const getDefaultQueryFields = () => ({
    capitalNo: "",
    capitalName: "",
    capitalType: "",
    capitalSource: "",
    allocateStates: [],
    createDateRange: {
      beginDate: "",
      endDate: ""
    }
  });
  /** 资金分配查询请求对象 */
  const queryRequest = reactive<CapitalAllocateQueryRequest>({
    ...getDefaultQueryFields(), // 展开默认查询字段
    current: 1,
    size: 20
  });
  /** 搜索关键字 */
  const keyword = ref("");
  /** 加载状态 */
  const loading = ref(false);
  /** 资金分配状态选项 */
  const stateOptions = [
    { label: "办理中", value: WorkflowInstanceState.AUDITING },
    { label: "审批完成", value: WorkflowInstanceState.AUDITED },
    { label: "已结案", value: WorkflowInstanceState.CLOSED }
  ] as const;

  /**
   * 获取资金分配列表或分页数据的通用方法,封装了数据获取、加载状态管理和错误处理逻辑,提升代码复用性和可维护性。
   * @param apiCall API 调用函数,返回包含data属性的Promise对象
   * @param onSuccess 成功回调函数,处理返回的数据
   * @returns Promise<void> 无返回值的异步函数
   */
  const getData = async <T>(apiCall: () => Promise<{ data: T }>, onSuccess: (data: T) => void) => {
    // 防止重复请求
    if (loading.value) return;

    loading.value = true;
    try {
      const result = await apiCall();
      onSuccess(result.data);
    } catch (error) {
      // 错误处理逻辑,开发环境打印错误日志
      if (import.meta.env.DEV) {
        console.error("数据获取失败:", error);
      }
    } finally {
      loading.value = false;
    }
  };

  /**
   * 获取资金分配分页
   */
  const getCapitalAllocatePage = async () => {
    await getData(
      () => capitalAllocateApi.fetchPage(queryRequest),
      (data) => {
        capitalAllocates.value = data.rows;
        total.value = data.total;
      }
    );
  };

  /**
   * 设置当前选择的资金分配
   * @param capitalAllocate 资金分配
   */
  const setCurrentCapitalAllocate = (capitalAllocate?: CapitalAllocateResponse) => {
    if (capitalAllocate) {
      currentCapitalAllocate.value = capitalAllocate;
    } else {
      currentCapitalAllocate.value = null;
    }
  };

  /**
   * 删除当前选择的资金分配
   * @param capitalAllocate 资金分配
   */
  const deleteCurrentCapitalAllocate = () => {
    if (!currentCapitalAllocate.value) return;

    capitalAllocates.value = capitalAllocates.value.filter((item) => item.id !== currentCapitalAllocate.value?.id);

    total.value = total.value - 1;
  };

  /**
   * 设置分页当前页码
   * @param current 当前页码
   */
  const setCurrentPage = (current: number) => {
    queryRequest.current = current;
  };

  /**
   * 清空查询请求对象(保留分页参数)
   */
  const clearQueryRequest = () => {
    // 仅将默认查询字段浅拷贝到 queryRequest,current 和 size 保持不变
    Object.assign(queryRequest, getDefaultQueryFields()); // 每次重置都取全新默认字段,使用Object.assign浅拷贝+函数返回全新默认对象的方式,这样既简单又可靠,无需引入深拷贝的复杂性。
  };

  /**
   * 清空搜索关键字
   */
  const clearKeyword = () => {
    keyword.value = "";
  };

  /**
   * 更新查询请求对象
   * @param newQueryRequest 新的查询请求对象
   */
  const updateQueryRequest = (newQueryRequest: CapitalAllocateQueryRequest) => {
    Object.assign(queryRequest, newQueryRequest);
  };

  /**
   * 重置数据状态
   */
  const $reset = () => {
    capitalAllocates.value = [];
    total.value = 0;
    currentCapitalAllocate.value = null;
    keyword.value = "";
    loading.value = false;
    clearQueryRequest();
  };

  // 监听响应式 ref capitalAllocateList 的变化
  watch(capitalAllocates, () => {
    // 重新赋值时,清除 currentCapitalAllocate
    currentCapitalAllocate.value = null;
  });

  return {
    /** 模块名称 */
    moduleName,
    /** 资金分配总数 */
    total,
    /** 资金分配列表 */
    capitalAllocates,
    /** 当前的资金分配 */
    currentCapitalAllocate,
    /** 资金分配查询请求对象 */
    queryRequest,
    /** 搜索关键字 */
    keyword,
    /** 加载状态 */
    loading,
    /** 资金分配状态选项 */
    stateOptions,
    /** 获取资分配分页 */
    getCapitalAllocatePage,
    /** 设置当前的资金分配 */
    setCurrentCapitalAllocate,
    /** 删除当前的资金分配 */
    deleteCurrentCapitalAllocate,
    /** 设置分页当前页码  */
    setCurrentPage,
    /** 清空资金信息查询请求对象(保留分页参数) */
    clearQueryRequest,
    /** 清空搜索关键字 */
    clearKeyword,
    /** 更新查询请求对象 */
    updateQueryRequest,
    /** 重置数据状态 */
    $reset
  };
});

export default useCapitalAllocateStore;

API

src\api\capital.ts

import type {
  CapitalAllocateCreateRequest,
  CapitalAllocateDetailResponse,
  CapitalAllocateDetailUpdateRequest,
  CapitalAllocateQueryRequest,
  CapitalAllocateResponse,
  CapitalInfoQueryRequest,
  CapitalInfoRequest,
  CapitalInfoResponse,
  PageResult
} from "@/types";
import { request } from "@/utils";

// 基础选项,如:资金账户选项、资金来源选项、指标来源选项、指标类别选项、预算情况选项、支出分类选项、支出方式选项

/**
 * 获取资金账户选项
 * @returns 字符串列表
 */
export const fetchCapitalAccountOptions = () => {
  return request.get<string[]>("/capital/options/account");
};

/**
 * 获取资金来源选项
 * @returns 字符串列表
 */
export const fetchCapitalSourceOptions = () => {
  return request.get<string[]>("/capital/options/source");
};

/**
 * 获取指标来源选项
 * @returns 字符串列表
 */
export const fetchCapitalIndexSourceOptions = () => {
  return request.get<string[]>("/capital/options/index-source");
};

/**
 * 获取指标类别选项
 * @returns 字符串列表
 */
export const fetchCapitalIndexTypeOptions = () => {
  return request.get<string[]>("/capital/options/index-type");
};

/**
 * 获取预算情况选项
 * @returns 字符串列表
 */
export const fetchCapitalBudgetOptions = () => {
  return request.get<string[]>("/capital/options/budget");
};

/**
 * 获取支出分类选项
 * @returns 字符串列表
 */
export const fetchCapitalPayTypeOptions = () => {
  return request.get<string[]>("/capital/options/pay-type");
};

/**
 * 获取支出方式选项
 * @returns 字符串列表
 */
export const fetchCapitalPayModeOptions = () => {
  return request.get<string[]>("/capital/options/pay-mode");
};

// 资金信息

/**
 * 增加资金信息
 * @param capitalInfo 资金信息
 * @returns 返回新增记录的ID
 */
export const addCapitalInfo = (capitalInfo: CapitalInfoRequest) => {
  return request.post<number>("/capital/info/add", capitalInfo);
};

/**
 * 增加资金信息(可能附带上传附件和上传文件)
 * @param formData 表单数据,包含的数据有:文件数据(uploadFile和uploadFiles)和资金信息(capitalInfo)
 * @returns
 */
export const addCapitalInfoByFormData = (formData: FormData) => {
  return request.post<number>("/capital/info/add-by-formdata", formData);
};

/**
 * 获取资金信息列表
 * @param queryObj 查询对象,包含查询条件
 * @returns 返回资金信息数组
 */
export const fetchCapitalInfos = (queryObj: CapitalInfoQueryRequest) => {
  return request.post<CapitalInfoResponse[]>("/capital/info/list", queryObj);
};

/**
 * 获取资金信息分页
 * @param queryObj 查询对象,包含查询条件
 * @returns 返回资金信息分页结果
 */
export const fetchCapitalInfoPage = (queryObj: CapitalInfoQueryRequest) => {
  return request.post<PageResult<CapitalInfoResponse>>("/capital/info/page", queryObj);
};

/**
 * 刷新资金信息列表
 * @returns 返回所有资金信息的数组
 */
export const refreshCapitalInfos = () => {
  return request.get<CapitalInfoResponse[]>("/capital/info/list");
};

/**
 * 刷新资金信息分页
 * @param current 当前页码
 * @param size 每页大小
 * @returns 返回资金信息分页结果
 */
export const refreshCapitalInfoPage = (current: number, size: number) => {
  return request.get<PageResult<CapitalInfoResponse>>("/capital/info/page", {
    params: {
      current: current,
      size: size
    }
  });
};

/**
 * 通过关键字获取资金信息列表
 * @param keyword 搜索关键字
 * @returns 返回匹配的资金信息数组
 */
export const fetchCapitalInfosByKeyword = (keyword: string) => {
  return request.get<CapitalInfoResponse[]>("/capital/info/list-keyword", {
    params: {
      keyword: keyword
    }
  });
};

/**
 * 通过关键字获取资金信息分页
 * @param keyword 搜索关键字
 * @returns 返回匹配的资金信息分页结果
 */
export const fetchCapitalInfoPageByKeyword = (current: number, size: number, keyword: string) => {
  return request.get<PageResult<CapitalInfoResponse>>("/capital/info/page-keyword", {
    params: {
      current: current,
      size: size,
      keyword: keyword
    }
  });
};

/**
 * 修改资金信息
 * @param capitalInfo 包含更新后资金信息的对象
 * @returns 返回受影响的记录数
 */
export const updateCapitalInfo = (capitalInfo: CapitalInfoResponse) => {
  return request.post<number>("/capital/info/update", capitalInfo);
};

/**
 * 删除资金信息
 * @param id 主键id
 * @returns 返回删除操作的结果
 */
export const deleteCapitalInfo = (id: number) => {
  return request.delete<void>(`/capital/info/${id}`);
};

/**
 * 结案资金信息
 * @param id 主键id
 * @returns 返回操作结果
 */
export const completeCapitalInfo = (id: number) => {
  return request.post<number>(`/capital/info/update-complete/${id}`);
};

/**
 * 上传附件
 * @param formData 表单数据,包含的数据有:文件数据(uploadFile)和主键(key)
 * @returns 返回上传操作的结果
 */
export const uploadAttachment = (formData: FormData) => {
  return request.post<void>("/capital/info/upload-attachment", formData);
};

/**
 * 下载附件
 * @param id 主键id
 * @returns 返回Blob类型的附件数据
 */
export const downloadAttachment = (id: number) => {
  return request.get<Blob>(`/capital/info/download-attachment/${id}`, {
    // 响应类型为 blob,用于接收二进制数据流
    responseType: "blob"
  });
};

/**
 * 提取附件文本内容
 * @param id 主键id
 * @returns 返回文本内容
 */
export const extractAttachment = (id: number) => {
  return request.get<string>(`/capital/info/extract-attachment/${id}`);
};

/**
 * 清除附件
 * @param id 主键id
 * @returns 返回清除操作的结果
 */
export const clearAttachment = (id: number) => {
  return request.post<void>(`/capital/info/clear-attachment/${id}`);
};

/**
 * 导入数据,上传excel文件 + 后端解析excel文件 + 后端保存数据到数据库
 * @param formData 表单数据,包含的数据有:文件数据(uploadFile)
 * @returns 返回操作的结果
 */
export const importData = (formData: FormData) => {
  return request.post<void>("/capital/info/import-data", formData);
};

/**
 * 导出数据,后端解析数据,生成 Excel 文件流数据
 * @param queryObj 查询对象,包含查询条件
 * @returns 返回Blob类型的excel文件数据
 */
export const exportData = (queryObj: CapitalInfoQueryRequest) => {
  return request.post<Blob>("/capital/info/export-data", queryObj, {
    // 响应类型为 blob,用于接收二进制数据流
    responseType: "blob"
  });
};

/**
 * 使用模板导出数据,后端解析数据,打开 Excel 模板,修改 Excel 文件流数据
 * @param queryObj 查询对象,包含查询条件
 * @returns 返回Blob类型的excel文件数据
 */
export const exportDataByTemplate = (queryObj: CapitalInfoQueryRequest) => {
  return request.post<Blob>("/capital/info/export-data-by-template", queryObj, {
    // 响应类型为 blob,用于接收二进制数据流
    responseType: "blob"
  });
};

// 资金分配

/**
 * 获取资金分配分页
 * @param queryObj 查询对象,包含查询条件
 * @returns 返回资金信息分页结果
 */
export const fetchCapitalAllocatePage = (queryObj: CapitalAllocateQueryRequest) => {
  return request.post<PageResult<CapitalAllocateResponse>>("/capital/allocate/page", queryObj);
};

/**
 * 获取资金分配明细列表 - 通过资金信息id
 * @param capitalId 资金信息id
 * @returns 返回资金分配明细列表
 */
export const fetchCapitalAllocateDetailsByCapitalId = (capitalId: number) => {
  return request.get<CapitalAllocateDetailResponse[]>(`/capital/allocate/detail-list/${capitalId}`);
};

/**
 * 获取资金分配明细列表 - 通过资金分配id
 * @param allocateId 资金分配id
 * @returns 返回资金分配明细列表
 */
export const fetchCapitalAllocateDetailsByAllocateId = (allocateId: number) => {
  return request.get<CapitalAllocateDetailResponse[]>(`/capital/allocate/detail-list-by-allocate/${allocateId}`);
};

/**
 * 保存资金分配明细列表
 * @param allocateId 资金分配id
 * @returns 返回资金分配明细列表
 */
export const saveCapitalAllocateDetails = (capitalAllocateDetails: CapitalAllocateDetailUpdateRequest[]) => {
  return request.post<void>("/capital/allocate/save-detail", capitalAllocateDetails);
};

/**
 * 生成资金分配工作流程
 * @param id 资金信息id
 * @param capitalAllocateDatas 资金分配数据
 * @returns 返回操作的结果
 */
export const generateCapitalAllocateWorkflow = (id: number, capitalAllocateDatas: CapitalAllocateCreateRequest[]) => {
  return request.post<void>(`/capital/allocate/generate-workflow/${id}`, capitalAllocateDatas);
};

// 可选:基础选项API集合,便于统一管理相关API方法,提供聚合对象,便于某些场景使用
export const capitalOptionsApi = {
  /** 获取资金账户选项 */
  fetchAccountOptions: fetchCapitalAccountOptions,
  /** 获取资金来源选项 */
  fetchSourceOptions: fetchCapitalSourceOptions,
  /** 获取指标来源选项 */
  fetchIndexSourceOptions: fetchCapitalIndexSourceOptions,
  /** 获取指标类别选项 */
  fetchIndexTypeOptions: fetchCapitalIndexTypeOptions,
  /** 获取预算情况选项 */
  fetchBudgetOptions: fetchCapitalBudgetOptions,
  /** 获取支出分类选项 */
  fetchPayTypeOptions: fetchCapitalPayTypeOptions,
  /** 获取支出方式选项 */
  fetchPayModeOptions: fetchCapitalPayModeOptions
};

// 可选:资金信息API集合,便于统一管理相关API方法,提供聚合对象,便于某些场景使用
export const capitalInfoApi = {
  /** 增加资金信息 */
  addCapitalInfo,
  /** 增加资金信息(可能附带上传附件和上传文件) */
  addCapitalInfoByFormData,
  /** 获取资金信息列表 */
  fetchCapitalInfos,
  /** 获取资金信息分页 */
  fetchCapitalInfoPage,
  /** 通过关键字获取资金信息列表 */
  fetchCapitalInfosByKeyword,
  /** 通过关键字获取资金信息分页 */
  fetchCapitalInfoPageByKeyword,
  /** 刷新资金信息列表 */
  refreshCapitalInfos,
  /** 刷新资金信息分页 */
  refreshCapitalInfoPage,
  /** 修改资金信息 */
  updateCapitalInfo,
  /** 删除资金信息 */
  deleteCapitalInfo,
  /** 结案资金信息 */
  completeCapitalInfo,
  /** 上传附件 */
  uploadAttachment,
  /** 下载附件 */
  downloadAttachment,
  /** 提取附件文本内容 */
  extractAttachment,
  /** 清除附件 */
  clearAttachment,
  /** 导入数据,上传excel文件 + 后端解析excel文件 + 后端保存数据到数据库 */
  importData,
  /** 导出数据,后端解析数据,生成 Excel 文件流数据 */
  exportData,
  /** 使用模板导出数据,后端解析数据,打开 Excel 模板,修改 Excel 文件流数据 */
  exportDataByTemplate,
  /** 获取资金分配明细列表 - 通过资金信息id */
  fetchCapitalAllocateDetails: fetchCapitalAllocateDetailsByCapitalId,
  /** 生成资金分配工作流程 */
  generateCapitalAllocateWorkflow
};

// 可选:资金分配API集合,便于统一管理相关API方法,提供聚合对象,便于某些场景使用
export const capitalAllocateApi = {
  /** 获取资金分配分页 */
  fetchPage: fetchCapitalAllocatePage,
  /** 获取资金分配明细列表 - 通过资金信息id */
  fetchDetailsByCapitalId: fetchCapitalAllocateDetailsByCapitalId,
  /** 获取资金分配明细列表 - 通过资金分配id */
  fetchDetailsByAllocateId: fetchCapitalAllocateDetailsByAllocateId,
  /** 生成资金分配工作流程 */
  generateWorkflow: generateCapitalAllocateWorkflow,
  /** 保存资金分配明细列表 */
  saveDetails: saveCapitalAllocateDetails
};

src\api\workflow.ts

import type {
  ActivityEditableFieldResponse,
  ActivityInstanceRejectOptionResponse,
  ActivityInstanceRejectRequest,
  ActivityInstanceRequest,
  ActivityInstanceSubmitOptionResponse,
  ActivityInstanceSubmitRequest,
  AuditInfoResponse
} from "@/types";
import { request } from "@/utils";

/**
 * 获取审批信息列表
 * @param workflowInstanceId 流程实例id
 * @returns 返回审批信息列表
 */
export const fetchAuditInfos = (workflowInstanceId: number) => {
  return request.get<AuditInfoResponse[]>(`/workflow/audit-info-list/${workflowInstanceId}`);
};

/**
 * 获取活动实例提交选项,包含办理意见选项和办理人选项
 * @param activityInstance 活动实例
 * @returns 返回活动实例提交选项
 */
export const fetchSubmitOption = (activityInstance: ActivityInstanceRequest) => {
  return request.post<ActivityInstanceSubmitOptionResponse>("/workflow/submit-option", activityInstance);
};

/**
 * 获取活动实例退回选项,包含办理意见选项和办理人选项
 * @param activityInstance 活动实例
 * @returns 返回活动实例退回选项
 */
export const fetchRejectOption = (activityInstance: ActivityInstanceRequest) => {
  return request.post<ActivityInstanceRejectOptionResponse>("/workflow/reject-option", activityInstance);
};

/**
 * 提交活动实例
 * @param submitData 提交的数据
 */
export const submitActivityInstance = (submitData: ActivityInstanceSubmitRequest) => {
  return request.post<void>("/workflow/submit", submitData);
};

/**
 * 退回活动实例
 * @param rejectData 退回的数据
 */
export const rejectActivityInstance = (rejectData: ActivityInstanceRejectRequest) => {
  return request.post<void>("/workflow/reject", rejectData);
};

/**
 * 获取活动可编辑字段列表
 * @param activityId 活动id
 * @returns 返回可编辑字段列表
 */
export const fetchActivityEditableFields = (activityId: number) => {
  return request.get<ActivityEditableFieldResponse[]>(`/workflow/activity-editable-field-list/${activityId}`);
};

接口定义

src\types\Capital.ts

import type { ActivityInstance, BasePageQuery } from "@/types";

/** 资金信息 */
export interface CapitalInfo {
  /** 唯一id */
  id: number;
  /** 资金序号 */
  capitalNo: string;
  /** 资金名称 */
  capitalName: string;
  /** 资金类别 */
  capitalType: string;
  /** 指标预算总额 */
  capitalTotal: number;
  /** 指标剩余额 */
  capitalLeaveTotal: number;
  /** 指标可用总额 */
  capitalValidTotal: number;
  /** 指标类别 */
  capitalIndexType: string;
  /** 资金账户 */
  capitalAccount: string;
  /** 资金来源 */
  capitalSource: string;
  /** 指标来源 */
  capitalIndexSource: string;
  /** 资金年份 */
  capitalYear: number;
  /** 资金状态 */
  capitalState: number;
  /** 备注信息 */
  remark: string;
  /** 创建时间 */
  createTime: string;
  /** 附件路径名称 */
  filePathname: string;
  /** 附件是否空内容 */
  isNullContent: boolean;
}

/** 资金信息请求对象 */
export interface CapitalInfoRequest extends CapitalInfo {
}

/** 资金信息响应对象 */
export interface CapitalInfoResponse extends CapitalInfo {
  /** 资金分配明细列表 */
  capitalAllocateDetails?: CapitalAllocateDetailResponse[];
}

/** 资金信息查询请求对象 */
export interface CapitalInfoQueryRequest extends BasePageQuery  {
  /** 资金序号 */
  capitalNo?: string;
  /** 资金名称 */
  capitalName?: string;
  /** 资金类别 */
  capitalType?: string;
  /** 指标类别 */
  capitalIndexType?: string;
  /** 资金账户 */
  capitalAccount?: string;
  /** 资金来源 */
  capitalSource?: string;
  /** 指标来源 */
  capitalIndexSource?: string;
  /** 资金年份 */
  capitalYear?: number;
  /** 资金状态 */
  capitalStates?: number[];
  /** 备注信息 */
  remark?: string;
  /** 创建日期范围 */
  // createDateRange?: string[];
  // createDateRange?: [string, string] | []; // 元组
  // createBeginDate?: string;
  // createEndDate?: string;
  createDateRange?: DateRange;
}

/** 日期范围 */
export interface DateRange {
  beginDate?: string;
  endDate?: string;
}

/** 资金分配响应对象 */
export interface CapitalAllocateResponse {
  /** 唯一id */
  id: number;
  /** 创建日期 */
  createTime: string;
  /** 资金序号 */
  capitalNo: string;
  /** 资金名称 */
  capitalName: string;
  /** 资金类别 */
  capitalType: string;
  /** 资金来源 */
  capitalSource: string;
  /** 指标预算总额 */
  capitalTotal: number;
  /** 指标可用总额 */
  capitalValidTotal: number;
  /** 状态 */
  allocateState: number;
  /** 活动实例 */
  activityInstance: ActivityInstance;
}

/** 资金分配明细更新请求对象 */
export interface CapitalAllocateDetailUpdateRequest {
  /** id */
  id: number;
  /** 使用人 */
  userPerson: string;
  /** 使用人姓名 */
  userMoniker: string;
}

/** 资金分配明细响应对象 */
export interface CapitalAllocateDetailResponse {
  /** id */
  id: number;
  /** 指标使用部门 */
  deptId: string;
  /** 指标使用部门名称 */
  deptName: string;
  /** 指标分配金额 */
  total: number;
  /** 预算情况 */
  budget: string;
  /** 支出分类 */
  payType: string;
  /** 支出方式 */
  payMode: string;
  /** 当前可使用额 */
  validTotal: number;
  /** 协助部门名称 */
  assistDeptName: string;
  /** 回笼金额 */
  backTotal: number;
  /** 回笼日期 */
  backTime: string;
  /** 使用人 */
  userPerson: string;
  /** 使用人姓名 */
  userMoniker: string;
}

/** 资金分配创建请求对象 */
export interface CapitalAllocateCreateRequest {
  /** 指标使用部门 */
  deptId: string;
  /** 指标分配金额 */
  total: number;
  /** 预算情况 */
  budget: string;
  /** 支出分类 */
  payType: string;
  /** 支出方式 */
  payMode: string;
  /** 协助部门名称 */
  assistDeptName: string;
}

/** 资金分配查询请求对象 */
export interface CapitalAllocateQueryRequest extends BasePageQuery  {
  /** 资金序号 */
  capitalNo?: string;
  /** 资金名称 */
  capitalName?: string;
  /** 资金类别 */
  capitalType?: string;
  /** 资金来源 */
  capitalSource?: string;
  /** 状态 */
  allocateStates?: number[];
  /** 创建日期范围 */
  createDateRange?: DateRange;
}

src\types\Workflow.ts

import { ActivityRejectMode, WorkflowInstanceNode } from "@/types";

/**
 * 活动实例
 */
export interface ActivityInstance {
  /** 工作流程id */
  workflowId: number;
  /** 流程版本id */
  concretWorkflowId: number;
  /** 流程实例id */  
  workflowInstanceId: number;
  /** 活动实例id */ 
  activityInstanceId: number;
  /** 当前活动id */
  activityId: number;
  /** 当前活动允许办理人 */
  allowAuditor: string;
  /** 业务主键 */
  businessKey: string;
  /** 流程实例状态 */
  workflowInstanceState: number;
  /** 流程实例节点 */
  workflowInstanceNode: WorkflowInstanceNode;
}

/**
 * 活动实例请求参数
 */
export type ActivityInstanceRequest = ActivityInstance;

/**
 * 办理信息
 */
export interface AuditInfoResponse {
  /** 活动名称/流程环节 */
  activityName: string;
  /** 办理人姓名 */
  auditorName: string;
  /** 办理意见 */
  auditIdea: string;
  /** 办理时间 */
  auditTime: string;
  /** 办理状态 */
  auditStateDesc: string;
}

/**
 * 活动实例办理
 */
export interface ActivityInstanceAudit {
  /** 办理意见 */
  auditIdea: string | null;
  /** 目标办理人(办理给谁) */
  targetAuditor: string | null;
  /** 活动实例 */
  activityInstance: ActivityInstance | null;
}

/**
 * 活动实例提交参数
 */
export interface ActivityInstanceSubmitRequest extends ActivityInstanceAudit {
  
}

/**
 * 活动实例退回参数
 */
export interface ActivityInstanceRejectRequest extends ActivityInstanceAudit {
  
}

/**
 * 人员部门信息
 */
export interface UserDept {
  /** 账号/用户名 */
  userName: string;
  /** 姓名 */
  moniker: string;
  /** 部门id */
  deptId: string;
  /** 部门名称 */
  deptName: string;
  /** 排序号 */
  sortNo: number;
}

/**
 * 提交的角色成员
 */
export interface SubmitRoleMember extends UserDept {
  /** 角色id */
  roleId: number;
  /** 角色名称 */
  roleName: string;
}

/**
 * 退回的办理成员
 */
export interface RejectAuditMember extends UserDept {
  /** 活动id */
  activityId: number;
  /** 活动名称 */
  activityName: string;
}

/**
 * 活动提交选项
 */
export interface ActivityInstanceSubmitOptionResponse {
  /** 办理意见选项 */
  auditIdeaOptions: string[];
  /** 目标办理人(角色成员)选项 */
  auditMemberOptions: SubmitRoleMember[];
  /** 提交方式 */
  submitMode: ActivityRejectMode;
  /** 目标活动名称 */
  targetActivityName: string;
}

/**
 * 活动退回选项
 */
export interface ActivityInstanceRejectOptionResponse {
  /** 办理意见选项 */
  auditIdeaOptions: string[];
  /** 目标办理人(角色成员)选项 */
  auditMemberOptions: RejectAuditMember[];
  /** 退回方式 */
  rejectMode: ActivityRejectMode;
  /** 目标活动名称 */
  targetActivityName: string;
}

/**
 * 活动可编辑字段
 */
export interface ActivityEditableFieldResponse {
  /** 字段名称 */
  fieldName: string;
  /** 不能为空 */
  notNull: boolean;
}

src\types\Workflow.enum.ts

/**
 * 流程实例状态枚举
 */
export enum WorkflowInstanceState {
  /**
   * 办理中
   */
  AUDITING = 0,
  /**
   * 审批完成
   */
  AUDITED = 2,
  /**
    * 已结案
    */
  CLOSED = 1
}

/**
 * 流程实例节点枚举
 */
export enum WorkflowInstanceNode {
  /**
   * 首活动/启动活动
   */
  FIRST = "FIRST",
  /**
   * 中间活动
   */
  MIDDLE = "MIDDLE",
  /**
    * 末活动/最后活动
    */
  LAST = "LAST",
  /**
    * 异常情况
    */
  UNKNOWN = "UNKNOWN"
}

/**
 * 活动提交方式枚举
 */
export enum ActivitySubmitType {
  /**
   * 提交给指定人员
   */
  PERSONNEL = 0,
  /**
   * 提交给角色或人
   */
  ROLE = 1
}

/**
 * 活动退回方式枚举
 */
export enum ActivityRejectMode {
  /**
   * 退回到前一步
   */
  PREVIOUS = 0,
  /**
   * 退回到第一步
   */
  FIRST = 1
}

Hook

src\hooks\useActivityInstanceRejectOption.ts

import { fetchRejectOption } from "@/api";
import type { ActivityInstance, ActivityInstanceRejectOptionResponse } from "@/types";
import { ref } from "vue";

/**
 * 活动实例退回选项 hook
 * @returns
 */
export function useActivityInstanceRejectOption() {
  /** 退回选项 */
  const rejectOption = ref<ActivityInstanceRejectOptionResponse | null>(null);
  /** 退回选项加载状态 */
  const rejectLoading = ref(false);

  /**
   * 获取退回选项
   * @param activityInstance 活动实例
   */
  const getRejectOption = async (activityInstance: ActivityInstance) => {
    try {
      rejectLoading.value = true;
      const result = await fetchRejectOption(activityInstance);
      rejectOption.value = result.data;
    } catch (error) {
    } finally {
      rejectLoading.value = false;
    }
  };

  return {
    /** 退回选项 */
    rejectOption,
    /** 退回选项加载状态 */
    rejectLoading,
    /** 获取退回选项 */
    getRejectOption
  };
}

export default useActivityInstanceRejectOption;

src\hooks\useActivityInstanceSubmitOption.ts

import { fetchSubmitOption } from "@/api";
import type { ActivityInstance, ActivityInstanceSubmitOptionResponse } from "@/types";
import { onUnmounted, ref } from "vue";

/**
 * 活动实例提交选项 hook
 * 缓存机制:通过 Map 缓存请求结果,并使用 promise 缓存避免并发重复请求,这是典型的防抖/缓存优化,能有效减少网络开销。
 * 组件卸载防护:利用 isUnmounted 标记和 onUnmounted 清理,防止异步回调更新已卸载组件的状态,避免内存泄漏。
 * @returns
 */
export function useActivityInstanceSubmitOption() {
  /** 提交选项 */
  const submitOption = ref<ActivityInstanceSubmitOptionResponse | null>(null);
  /** 提交选项缓存:Map<concretWorkflowId-activityId-deptId-userName, { data?: T; promise?: Promise<T> }> */
  const submitOptionCache = new Map<
    string,
    { data?: ActivityInstanceSubmitOptionResponse; promise?: Promise<ActivityInstanceSubmitOptionResponse> }
  >();
  /** 提交选项加载状态 */
  const submitLoading = ref(false);
  /** 提交选项错误信息 */
  const submitError = ref<Error | null>(null);
  /** 标记组件是否已卸载,用于避免异步回调更新状态 */
  const isUnmounted = ref(false);
  /** 当前正在进行的请求计数(用于精确控制 loading) */
  let pendingCount = 0;

  /**
   * 生成缓存键
   */
  const getCacheKey = (
    concretWorkflowId: string | number,
    activityId: string | number,
    deptId?: string,
    userName?: string
  ): string => {
    return `${concretWorkflowId}-${activityId}-${deptId ?? ""}-${userName ?? ""}`;
  };

  /**
   * 获取提交选项(缓存机制)
   * @param activityInstance 活动实例
   * @param deptId 部门 ID(可选,用于缓存键)
   * @param userName 用户名(可选,用于缓存键)
   * @returns Promise<ActivityInstanceSubmitOptionResponse | null>
   */
  const getSubmitOption = async (
    activityInstance: ActivityInstance,
    deptId?: string,
    userName?: string
  ): Promise<ActivityInstanceSubmitOptionResponse | null> => {
    const concretWorkflowId = activityInstance.concretWorkflowId;
    const activityId = activityInstance.activityId;

    // 参数校验:缺少必要字段则返回 null(避免生成无效缓存键)
    if (!concretWorkflowId || !activityId) {
      console.warn("[useActivityInstanceSubmitOption] 缺少 concretWorkflowId 或 activityId");
      return null;
    }

    const key = getCacheKey(concretWorkflowId, activityId, deptId, userName);
    const cached = submitOptionCache.get(key);

    // 有缓存数据,直接返回并更新响应式状态
    if (cached?.data) {
      if (!isUnmounted.value) {
        submitOption.value = cached.data;
      }
      return cached.data;
    }

    // 有正在进行的请求,复用 Promise
    if (cached?.promise) {
      return cached.promise;
    }

    // 发起新请求
    pendingCount++;
    submitLoading.value = true;
    submitError.value = null;

    const promise = fetchSubmitOption(activityInstance)
      .then((result) => {
        const data = result.data;
        if (!isUnmounted.value) {
          submitOption.value = data;
          // 存入缓存(移除 promise,仅保留数据)
          submitOptionCache.set(key, { data });
        }
        return data;
      })
      .catch((err) => {
        if (!isUnmounted.value) {
          submitError.value = err;
          // 请求失败,从缓存中移除该条目(避免永久占位)
          submitOptionCache.delete(key);
        }
        throw err; // 将错误抛给调用方处理
      })
      .finally(() => {
        pendingCount--;
        if (!isUnmounted.value) {
          // 只有当所有请求都完成时才关闭 loading
          submitLoading.value = pendingCount > 0;
        }
      });

    // 先将 promise 存入缓存,以便后续并发请求复用
    submitOptionCache.set(key, { promise });

    return promise;
  };

  // 组件卸载时清理
  onUnmounted(() => {
    isUnmounted.value = true; // 标记已卸载
    submitOptionCache.clear(); // 清理缓存,避免内存泄漏
  });

  return {
    /** 提交选项 */
    submitOption,
    /** 提交选项缓存 */
    submitOptionCache,
    /** 提交选项加载状态 */
    submitLoading,
    /** 提交选项错误信息 */
    submitError,
    /** 获取提交选项(缓存机制) */
    getSubmitOption
  };
}

export default useActivityInstanceSubmitOption;

src\hooks\useAuditActivityInstance.ts

import { rejectActivityInstance, submitActivityInstance } from "@/api";
import type { ActivityInstanceRejectRequest, ActivityInstanceSubmitRequest } from "@/types";

/**
 * 办理活动实例 hook
 * @returns
 */
export function useAuditActivityInstance() {
  /**
   * 提交
   * @param submitData 提交数据
   */
  const submit = async (submitData: ActivityInstanceSubmitRequest) => {
    await submitActivityInstance(submitData);
  };

  /**
   * 退回
   * @param rejectData 退回数据
   */
  const reject = async (rejectData: ActivityInstanceRejectRequest) => {
    await rejectActivityInstance(rejectData);
  };

  return {
    /** 提交 */
    submit,
    /** 退回 */
    reject
  };
}

export default useAuditActivityInstance;

src\hooks\useWorkflowInstanceStateFormatter.ts

/**
 * 资金信息状态格式化 hook
 * @returns
 */
export const useCapitalInfoStateFormatter = () => {
  const CAPITAL_STATE_MAP = {
    0: "使用中",
    1: "已回笼",
    2: "已结案"
  } as const;

  const formatState = (state: number): string => {
    return CAPITAL_STATE_MAP[state as keyof typeof CAPITAL_STATE_MAP] ?? "未知";
  };

  const getStateOptions = () => {
    return Object.entries(CAPITAL_STATE_MAP).map(([value, label]) => ({
      value: Number(value),
      label
    }));
  };

  return {
    formatState,
    getStateOptions
  };
};

export default useCapitalInfoStateFormatter;

后端核心代码:

控制层

资金分配控制器

src/main/java/com/weiyu/controller/CapitalAllocateController.java

package com.weiyu.controller;

import com.weiyu.model.*;
import com.weiyu.service.CapitalAllocateService;
import com.weiyu.util.DateUtils;
import com.weiyu.util.SecurityUtils;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * 资金分配控制器
 */
@RestController
@RequestMapping("/capital/allocate")
@RequiredArgsConstructor
@Slf4j
@Validated
public class CapitalAllocateController {

    private final CapitalAllocateService allocateService;

    /**
     * 查询资金分配分页
     *
     * @param queryRequest 查询请求对象
     * @return {@link Result}&lt;{@link PageResult}&lt;{@link CapitalAllocateResponse}&gt;&gt;
     */
    @PostMapping("/page")
    public Result<PageResult<CapitalAllocateResponse>> queryPage(
            @RequestBody @Valid CapitalAllocateQueryRequest queryRequest
    ) {

        String endpoint = "/capital/allocate/list";
        String method = "queryPage";

        log.info("【资金分配】查询资金分配分页,{},{},queryRequest = {}",
                endpoint, method, SecurityUtils.safeForLog(queryRequest));

        // 1、基本校验已由 @Valid 完成,Spring会自动执行以下验证:
        //    a. 检查@Pattern注解(格式验证)
        //    b. 调用@AssertTrue注解的方法(业务逻辑验证)

        // 处理校验结果,由全局异常 【处理方法参数验证异常(MethodArgumentNotValidException)】 处理校验结果

        // 2、处理业务逻辑校验,检查空条件
        if (isAllQueryParamsEmpty(queryRequest)) {
            log.warn("【资金信息】 至少需要一个查询条件,{},{}", endpoint, method);
            return Result.error("请至少输入一个查询条件");
        }

        // 3、处理业务逻辑校验 - 日期范围
        String dateRangeError = DateUtils.validateDateRange(queryRequest.createDateRange());
        if (dateRangeError != null) {
            return Result.error(dateRangeError);
        }

        PageResult<CapitalAllocateDTO> dtoPage = allocateService.queryPage(queryRequest);

        // DTO -> VO
        PageResult<CapitalAllocateResponse> voPage = new PageResult<>();
        List<CapitalAllocateResponse> voList = dtoPage.getRows().stream()
                .map(dto -> {
                    CapitalAllocateResponse vo = new CapitalAllocateResponse();
                    BeanUtils.copyProperties(dto, vo);

                    // 手动处理:VO与DTO不同属性名的内容
                    vo.setActivityInstance(dto.getActivityInstanceRequest());

                    return vo;
                })
                .toList();

        voPage.setRows(voList);
        voPage.setTotal(dtoPage.getTotal());

        return Result.success(voPage);
    }

    /**
     * 查询资金分配明细列表 - 通过资金信息id
     *
     * @param capitalId 资金信息id
     * @return {@link Result}&lt;{@link List}&lt;{@link CapitalAllocateDetailResponse}&gt;&gt;
     */
    @GetMapping("/detail-list/{capitalId}")
    public Result<List<CapitalAllocateDetailResponse>> queryDetailList(@PathVariable @Min(1) Integer capitalId) {

        String endpoint = "/capital/allocate/detail-list/";
        String method = "queryDetailList";

        log.info("【资金分配】通过资金信息id,查询资金分配明细列表,{}{},{}", endpoint, capitalId, method);

        List<CapitalAllocateDetailDTO> dtoList = allocateService.queryDetais(capitalId);

        // DTO -> VO
        List<CapitalAllocateDetailResponse> voList = dtoList.stream()
                .map(dto -> {
                    CapitalAllocateDetailResponse vo = new CapitalAllocateDetailResponse();
                    BeanUtils.copyProperties(dto, vo);
                    return vo;
                })
                .toList();

        return Result.success(voList);
    }

    /**
     * 查询资金分配明细列表 - 通过资金分配id
     *
     * @param allocateId 资金分配id
     * @return {@link Result}&lt;{@link List}&lt;{@link CapitalAllocateDetailResponse}&gt;&gt;
     */
    @GetMapping("/detail-list-by-allocate/{allocateId}")
    public Result<List<CapitalAllocateDetailResponse>> queryDetailListByAllocate(
            @PathVariable @Min(1) Integer allocateId
    ) {

        String endpoint = "/capital/allocate/detail-list-by-allocate/";
        String method = "queryDetailListByAllocate";

        log.info("【资金分配】通过资金分配id,查询资金分配明细列表,{}{},{}", endpoint, allocateId, method);

        List<CapitalAllocateDetailDTO> dtoList = allocateService.queryDetailsByAllocateId(allocateId);

        // DTO -> VO
        List<CapitalAllocateDetailResponse> voList = dtoList.stream()
                .map(dto -> {
                    CapitalAllocateDetailResponse vo = new CapitalAllocateDetailResponse();
                    BeanUtils.copyProperties(dto, vo);
                    return vo;
                })
                .toList();

        return Result.success(voList);
    }

    /**
     * 保存资金分配明细列表
     *
     * @param updateRequests 资金分配明细更新请求对象列表
     * @return {@link Result}&lt;{@link Void}&gt;
     */
    @PostMapping("/save-detail")
    public Result<Void> saveDetailList(@RequestBody List<CapitalAllocateDetailUpdateRequest> updateRequests) {

        String endpoint = "/capital/allocate/save-detail";
        String method = "saveDetailList";

        log.info("【资金分配】保存资金分配明细列表,{},{},updateRequests = {}",
                endpoint, method, SecurityUtils.safeForLog(updateRequests));

        allocateService.saveDetails(updateRequests);

        return Result.success();
    }

    /**
     * 生成资金分派工作流程
     *
     * @param capitalId      资金信息id
     * @param createRequests 资金分配明细创建请求对象列表
     * @return {@link Result}&lt;{@link Void}&gt;
     */
    @PostMapping("/generate-workflow/{capitalId}")
    public Result<Void> generateWorkflow(
            @PathVariable @Min(value = 1, message = "资金信息id不能小于1") Integer capitalId,
            @RequestBody @Valid List<CapitalAllocateCreateRequest> createRequests
    ) {

        String endpoint = "/capital/allocate/generate-workflow/";
        String method = "generateWorkflow";

        log.info("【资金分配】生成资金分配工作流程,{}{},{},createRequests = {}",
                endpoint, capitalId, method, SecurityUtils.safeForLog(createRequests));

        allocateService.generateWorkflow(capitalId, createRequests);

        return Result.success();
    }

    /**
     * 检查所有查询条件是否都为空
     */
    private boolean isAllQueryParamsEmpty(CapitalAllocateQueryRequest queryRequest) {
        if (queryRequest == null) {
            return true;
        }

        return StringUtils.isBlank(queryRequest.capitalNo())
               && StringUtils.isBlank(queryRequest.capitalName())
               && StringUtils.isBlank(queryRequest.capitalType())
               && StringUtils.isBlank(queryRequest.capitalSource())
               && (queryRequest.allocateStates() == null || queryRequest.allocateStates().isEmpty())
               && (queryRequest.createDateRange() == null
                   || (StringUtils.isBlank(queryRequest.createDateRange().getBeginDate())
                       && StringUtils.isBlank(queryRequest.createDateRange().getEndDate())));
    }
}

工作流程控制器

src/main/java/com/weiyu/controller/WorkflowController.java

package com.weiyu.controller;

import com.weiyu.model.*;
import com.weiyu.service.WorkflowService;
import com.weiyu.util.SecurityUtils;
import jakarta.validation.constraints.Min;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * 工作流程控制器
 */
@RestController
@RequestMapping("/workflow")
@RequiredArgsConstructor
@Slf4j
@Validated
public class WorkflowController {

    private final WorkflowService workflowService;

    /**
     * 查询办理信息列表
     *
     * @param workflowInstanceId 流程实例id
     * @return {@link Result}&lt;{@link List}&lt;{@link AuditInfoResponse}&gt;&gt;
     */
    @GetMapping("/audit-info-list/{workflowInstanceId}")
    public Result<List<AuditInfoResponse>> queryAuditInfoList(@PathVariable @Min(1) Integer workflowInstanceId) {

        String endpoint = "/workflow/audit-info-list/";
        String method = "queryAuditInfoList";

        log.info("【工作流程】查询办理信息列表,{}{},{}", endpoint, workflowInstanceId, method);

        List<AuditInfoDTO> auditInfoDTOs = workflowService.queryAuditInfoList(workflowInstanceId);

        // DTO -> VO
        List<AuditInfoResponse> auditInfoResponses = auditInfoDTOs.stream()
                .map(dto -> {
                    AuditInfoResponse vo = new AuditInfoResponse();
                    BeanUtils.copyProperties(dto, vo);
                    return vo;
                })
                .toList();

        return Result.success(auditInfoResponses);
    }

    /**
     * 查询活动可编辑字段列表
     *
     * @param activityId 活动id
     * @return {@link Result}&lt;{@link List}&lt;{@link ActivityEditableFieldResponse}&gt;&gt;
     */
    @GetMapping("activity-editable-field-list/{activityId}")
    public Result<List<ActivityEditableFieldResponse>> queryActivityEditableField(
            @PathVariable @Min(1) Integer activityId
    ) {

        String endpoint = "/workflow/activity-editable-field-list/";
        String method = "queryActivityEditableField";

        log.info("【工作流程】查询活动可编辑字段列表,{}{},{}", endpoint, activityId, method);

        List<ActivityEditableFieldDTO> editableFieldDTOs = workflowService.queryActivityEditableField(activityId);

        // DTO -> VO
        List<ActivityEditableFieldResponse> editableFieldResponses = editableFieldDTOs.stream()
                .map(dto -> {
                    ActivityEditableFieldResponse vo = new ActivityEditableFieldResponse();
                    BeanUtils.copyProperties(dto, vo);
                    if (!StringUtils.isBlank(dto.getFieldAlias())) {
                        vo.setFieldName(dto.getFieldAlias());
                    }
                    return vo;
                })
                .toList();

        return Result.success(editableFieldResponses);
    }

    /**
     * 查询活动实例提交选项
     *
     * @param activityInstanceRequest 活动实例请求对象
     * @return {@link Result}&lt;{@link ActivityInstanceSubmitOptionResponse}&gt;
     */
    @PostMapping("/submit-option")
    public Result<ActivityInstanceSubmitOptionResponse> querySubmitOption(
            @RequestBody ActivityInstanceRequest activityInstanceRequest
    ) {

        String endpoint = "/workflow/submit-option";
        String method = "querySubmitOption";

        log.info("【工作流程】查询活动实例提交选项,{},{},activityInstanceRequest = {}",
                endpoint, method, SecurityUtils.safeForLog(activityInstanceRequest));

        ActivityInstanceSubmitOptionDTO submitOptionDTO = workflowService.querySubmitOption(activityInstanceRequest);

        // DTO -> VO
        ActivityInstanceSubmitOptionResponse submitOptionResponse = new ActivityInstanceSubmitOptionResponse();
        BeanUtils.copyProperties(submitOptionDTO, submitOptionResponse);

        return Result.success(submitOptionResponse);
    }

    /**
     * 提交
     *
     * @param submitRequest 提交的请求对象
     */
    @PostMapping("/submit")
    public Result<Void> submit(@RequestBody ActivityInstanceSubmitRequest submitRequest) {

        String endpoint = "/workflow/submit";
        String method = "submit";

        log.info("【工作流程】提交,{},{},submitRequest = {}",
                endpoint, method, SecurityUtils.safeForLog(submitRequest));

        workflowService.submit(submitRequest);

        return Result.success();
    }

    /**
     * 查询活动实例退回选项
     *
     * @param activityInstanceRequest 活动实例请求对象
     * @return {@link Result}&lt;{@link ActivityInstanceRejectOptionResponse}&gt;
     */
    @PostMapping("/reject-option")
    public Result<ActivityInstanceRejectOptionResponse> queryRejectOption(
            @RequestBody ActivityInstanceRequest activityInstanceRequest
    ) {

        String endpoint = "/workflow/reject-option";
        String method = "queryRejectOption";

        log.info("【工作流程】查询活动实例退回选项,{},{},activityInstanceRequest = {}",
                endpoint, method, SecurityUtils.safeForLog(activityInstanceRequest));

        ActivityInstanceRejectOptionDTO rejectOptionDTO = workflowService.queryRejectOption(activityInstanceRequest);

        // DTO -> VO
        ActivityInstanceRejectOptionResponse rejectOptionResponse = new ActivityInstanceRejectOptionResponse();
        BeanUtils.copyProperties(rejectOptionDTO, rejectOptionResponse);

        return Result.success(rejectOptionResponse);
    }

    /**
     * 退回
     *
     * @param rejectRequest 退回的请求对象
     */
    @PostMapping("/reject")
    public Result<Void> reject(@RequestBody ActivityInstanceRejectRequest rejectRequest) {

        String endpoint = "/workflow/reject";
        String method = "reject";

        log.info("【工作流程】退回,{},{},submitRequest = {}",
                endpoint, method, SecurityUtils.safeForLog(rejectRequest));

        workflowService.reject(rejectRequest);

        return Result.success();
    }
}

服务层

资金分配服务

src/main/java/com/weiyu/service/CapitalAllocateService.java

package com.weiyu.service;

import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.weiyu.constant.WorkflowInstanceStateConstants;
import com.weiyu.enumeration.WorkflowInstanceNode;
import com.weiyu.exception.BusinessException;
import com.weiyu.mapper.CapitalAllocateDetailMapper;
import com.weiyu.mapper.CapitalAllocateMapper;
import com.weiyu.model.*;
import com.weiyu.util.PublicUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * 资金分配服务
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class CapitalAllocateService {

    private final CapitalAllocateDetailMapper allocateDetailMapper;
    private final CapitalAllocateMapper allocateMapper;

    private final CapitalInfoService capitalInfoService;
    private final UserService userService;
    private final WorkflowService workflowService;

    /**
     * 查询资金分配分页
     */
    public PageResult<CapitalAllocateDTO> queryPage(CapitalAllocateQueryRequest queryRequest) {
        // 1、分页查询
        // 1.1、启用分页
        PageHelper.startPage(queryRequest.current(), queryRequest.size());
        // 1.2、查询
        List<CapitalAllocateQueryResultDTO> queryResuls;
        if (StringUtils.isBlank(queryRequest.capitalType()) &&
            StringUtils.isBlank(queryRequest.capitalSource()) &&
            queryRequest.createDateRange() != null &&
            StringUtils.isBlank(queryRequest.createDateRange().getBeginDate()) &&
            StringUtils.isBlank(queryRequest.createDateRange().getEndDate()) &&
            queryRequest.allocateStates() != null && !queryRequest.allocateStates().isEmpty() &&
            queryRequest.allocateStates().equals(
                    List.of(WorkflowInstanceStateConstants.AUDITING)) && // 使用 List.of 创建常量列表比较
            !StringUtils.isBlank(queryRequest.capitalNo()) && !StringUtils.isBlank(queryRequest.capitalName()) &&
            queryRequest.capitalNo().equals(queryRequest.capitalName())
        ) {
            String keyword = queryRequest.capitalName();
            queryResuls = allocateMapper.selectListByKeyword(keyword, PublicUtils.getUserName());
        } else {
            queryResuls = allocateMapper.selectListByCustom(queryRequest, PublicUtils.getUserName());
        }

        // 1.3、转换成分页,使用Page包装查询结果
        // Page<CapitalAllocateDTO> page = (Page<CapitalAllocateDTO>) capitalAllocateDTOs;
        // 1.3、转换成分页,使用PageInfo包装查询结果(推荐,更安全)
        PageInfo<CapitalAllocateQueryResultDTO> page = new PageInfo<>(queryResuls);

        // 2、数据转换,DTO -> DTO
        List<CapitalAllocateDTO> capitalAllocateDTOs = toCapitalAllocateDTOs(queryResuls);

        // 3、分页结果
        PageResult<CapitalAllocateDTO> pageResult = new PageResult<>();
        pageResult.setRows(capitalAllocateDTOs);
        pageResult.setTotal(page.getTotal());

        return pageResult;
    }

    /**
     * DTO 转换为 DTO
     */
    private List<CapitalAllocateDTO> toCapitalAllocateDTOs(
            List<CapitalAllocateQueryResultDTO> capitalAllocateQueryResultDTOs
    ) {
        return capitalAllocateQueryResultDTOs.stream()
                .map(bo -> {
                    CapitalAllocateDTO capitalAllocateDTO = new CapitalAllocateDTO();
                    ActivityInstanceRequest activityInstanceRequest = new ActivityInstanceRequest();

                    // 复制属性
                    BeanUtils.copyProperties(bo, capitalAllocateDTO);
                    BeanUtils.copyProperties(bo, activityInstanceRequest);

                    // 处理数据,获取流程实例节点(首活动/启动活动、中间活动、末活动/最后活动)
                    WorkflowInstanceNode workflowInstanceNode = workflowService.getWorkflowInstanceNode(activityInstanceRequest);
                    activityInstanceRequest.setWorkflowInstanceNode(workflowInstanceNode);

                    // DTO -> DTO
                    capitalAllocateDTO.setActivityInstanceRequest(activityInstanceRequest);

                    return capitalAllocateDTO;
                })
                .collect(Collectors.toList());
    }

    /**
     * 查询资金分配明细列表 - 通过资金信息id
     */
    public List<CapitalAllocateDetailDTO> queryDetais(Integer capitalId) {
        return allocateDetailMapper.selectListByCapitalInfoId(capitalId);
    }

    /**
     * 查询资金分配明细列表 - 通过资金分配id
     */
    public List<CapitalAllocateDetailDTO> queryDetailsByAllocateId(Integer allocateId) {
        return allocateDetailMapper.selectListByAllocateId(allocateId);
    }

    /**
     * 保存资金分配明细列表
     */
    @Transactional
    public void saveDetails(List<CapitalAllocateDetailUpdateRequest> updateRequests) {
        // 预加载全部用户
        List<User> users = userService.queryUserListByCommand("all", 0);

        // List -> Map
        Map<String, String> userMap = new HashMap<>();
        users.forEach(user -> userMap.put(user.getUserName(), user.getMoniker()));

        // DTO -> Entity
        List<CapitalAllocateDetail> details = updateRequests.stream()
                .map(dto -> {
                    CapitalAllocateDetail entity = new CapitalAllocateDetail();
                    entity.setId(dto.getId());
                    // 允许更新为 null,实体类中已经设置更新策略,字段更新为 ALWAYS,updateStrategy = FieldStrategy.ALWAYS
                    entity.setUserPerson(dto.getUserPerson());
                    // 允许更新为 null,实体类中已经设置更新策略,字段更新为 ALWAYS,updateStrategy = FieldStrategy.ALWAYS
                    entity.setUserMoniker(userMap.get(dto.getUserPerson()));

                    return entity;
                })
                .toList();

        // 批量更新(使用 MyBatis-Plus 3.5.0+ 的默认方法)
        allocateDetailMapper.updateById(details, 100);
    }

    /**
     * 生成资金分派工作流程
     */
    @Transactional
    public void generateWorkflow(Integer capitalId, List<CapitalAllocateCreateRequest> createRequests) {
        // 获取当前用户
        String userName = PublicUtils.getUserName();
        User user = userService.queryByUserName(userName);
        if (user == null) {
            throw new BusinessException("用户不存在");
        }

        // 构建明细实体列表并累计指标分配金额
        List<CapitalAllocateDetail> allocateDetails = new ArrayList<>();
        // 累计指标分配金额,赋初值
        BigDecimal sumtotal = BigDecimal.ZERO;
        for (CapitalAllocateCreateRequest createDTO : createRequests) {
            CapitalAllocateDetail allocateDetail = new CapitalAllocateDetail();
            // DTO -> Entity
            BeanUtils.copyProperties(createDTO, allocateDetail);
            allocateDetail.setMasterId(0); // 主表ID暂为0,后续可能需关联
            allocateDetail.setAllocater(user.getMoniker()); // 分配人

            // 增加资金分配明细
            allocateDetails.add(allocateDetail);

            // 累计指标分配金额
            sumtotal = sumtotal.add(allocateDetail.getTotal());
        }

        // 批量插入资金分配明细(使用 MyBatis-Plus 3.5.0+ 的默认方法)
        if (!allocateDetails.isEmpty()) {
            allocateDetailMapper.insert(allocateDetails, 100);
        }

        // 生成资金分配工作流程,执行存储过程(有输入参数,无输出参数,无返回数据集)
        allocateMapper.generateWorkflow(userName, capitalId);

        // 更新资金信息指标可用总额
        CapitalInfo capitalInfo = capitalInfoService.getById(capitalId);
        if (capitalInfo == null || capitalInfo.getCapitalTotal() == null) {
            return;
        }
        BigDecimal validTotal = capitalInfo.getCapitalValidTotal().subtract(sumtotal);
        capitalInfoService.updateCapitalValidTotal(capitalId, validTotal);
    }

    /**
     * 转换为资金分配明细列表 DTO
     */
    @SuppressWarnings("unused")
    private List<CapitalAllocateDetailDTO> toCapitalAllocateDetailDTOs(
            List<CapitalAllocateDetail> capitalAllocateDetails
    ) {
        // Entity -> DTO
        return capitalAllocateDetails.stream()
                .map(allocateDetail -> {
                    CapitalAllocateDetailDTO allocateDetailDTO = new CapitalAllocateDetailDTO();

                    // 复制属性
                    BeanUtils.copyProperties(allocateDetail, allocateDetailDTO);

                    // 手动处理其他属性(属性名称不一致的)

                    return allocateDetailDTO;
                })
                .toList();
    }
}

工作流程服务

src/main/java/com/weiyu/service/WorkflowService.java

package com.weiyu.service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.weiyu.constant.ActivityFilterTypeConstants;
import com.weiyu.constant.ActivityInstanceStateConstants;
import com.weiyu.constant.WorkflowInstanceStateConstants;
import com.weiyu.enumeration.ActivityRejectMode;
import com.weiyu.enumeration.WorkflowInstanceNode;
import com.weiyu.exception.BusinessException;
import com.weiyu.mapper.*;
import com.weiyu.model.*;
import com.weiyu.util.PublicUtils;
import com.weiyu.util.SecurityUtils;
import com.weiyu.util.SqlScriptUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.*;

/**
 * 工作流程服务
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class WorkflowService {

    private final WorkflowMapper workflowMapper;
    private final ActivityMapper activityMapper;
    private final TransitionMapper transitionMapper;
    private final AuditInfoMapper auditInfoMapper;
    private final UserMapper userMapper;
    private final ActivityInstanceMapper activityInstanceMapper;
    private final AuditLogMapper auditLogMapper;
    private final WorkflowInstanceMapper workflowInstanceMapper;
    private final ActivityEditableFieldMapper activityEditableFieldMapper;

    /**
     * 查询办理信息列表
     */
    public List<AuditInfoDTO> queryAuditInfoList(Integer workflowInstanceId) {
        List<AuditInfoJoinQueryResultDTO> queryDTOs = workflowMapper.selectAuditInfoListByJoinQuery(workflowInstanceId);

        // Entity -> BO,预加载所有相关数据,避免每次遍历都查询数据库
        ActivityInstanceAuditContextBO auditStateDescBO = new ActivityInstanceAuditContextBO();
        List<ActivityInstance> activityInstances = getActivityInstancesByWorkflowInstanceId(workflowInstanceId);
        List<AuditInfo> auditInfos = getAuditInfosByWorkflowInstanceId(workflowInstanceId);
        List<AuditLog> auditLogs = getAuditLogsByWorkflowInstanceId(workflowInstanceId);
        auditStateDescBO.setActivityInstances(activityInstances);
        auditStateDescBO.setAuditInfos(auditInfos);
        auditStateDescBO.setAuditLogs(auditLogs);

        List<AuditInfoDTO> auditInfoDTOs = new ArrayList<>();
        for (AuditInfoJoinQueryResultDTO queryDTO : queryDTOs) {
            AuditInfoDTO auditInfoDTO = new AuditInfoDTO();
            BeanUtils.copyProperties(queryDTO, auditInfoDTO);

            // 计算办理状态
            String auditStateDesc = getActivityInstanceAuditStateDesc(
                    auditStateDescBO,
                    queryDTO.activityId(),
                    queryDTO.auditor()
            );
            auditInfoDTO.setAuditStateDesc(auditStateDesc);

            auditInfoDTOs.add(auditInfoDTO);
        }

        return auditInfoDTOs;
    }

    /**
     * 通过流程实例id,获取活动实例列表
     */
    private List<ActivityInstance> getActivityInstancesByWorkflowInstanceId(Integer workflowInstanceId) {
        LambdaQueryWrapper<ActivityInstance> wrapper = new LambdaQueryWrapper<>();

        wrapper.eq(ActivityInstance::getWorkflowInstanceId, workflowInstanceId);

        return activityInstanceMapper.selectList(wrapper);
    }

    /**
     * 通过流程实例id,获取办理信息列表
     */
    private List<AuditInfo> getAuditInfosByWorkflowInstanceId(Integer workflowInstanceId) {
        LambdaQueryWrapper<AuditInfo> wrapper = new LambdaQueryWrapper<>();

        wrapper.eq(AuditInfo::getWorkflowInstanceId, workflowInstanceId);

        return auditInfoMapper.selectList(wrapper);
    }

    /**
     * 通过流程实例id,获取办理日志列表
     */
    private List<AuditLog> getAuditLogsByWorkflowInstanceId(Integer workflowInstanceId) {
        LambdaQueryWrapper<AuditLog> wrapper = new LambdaQueryWrapper<>();

        wrapper.eq(AuditLog::getWorkflowInstanceId, workflowInstanceId);

        return auditLogMapper.selectList(wrapper);
    }

    /**
     * 获取活动实例办理状态
     */
    private String getActivityInstanceAuditStateDesc(
            ActivityInstanceAuditContextBO auditStateDescBO,
            Integer activityId,
            String auditor
    ) {
        List<ActivityInstance> activityInstances = auditStateDescBO.getActivityInstances().stream()
                .filter(activityInstance -> Objects.equals(activityInstance.getActivityId(), activityId))
                .toList();

        if (activityInstances.size() == 1) {
            if (activityInstances.get(0).getState() == ActivityInstanceStateConstants.AUDITED) {
                List<AuditLog> auditLogs = auditStateDescBO.getAuditLogs();

                if (auditLogs == null || auditLogs.isEmpty()) {
                    return "已提交";
                } else {
                    // 获取最后元素
                    AuditLog auditLog = auditLogs.get(auditLogs.size() - 1);
                    if (auditLog.getIsSubmit()) {
                        return "已提交";
                    } else {
                        return "已退回";
                    }
                }
            } else {
                return "待办理";
            }
        } else {
            List<AuditInfo> auditInfos = auditStateDescBO.getAuditInfos().stream()
                    .filter(auditInfo ->
                            Objects.equals(auditInfo.getActivityId(), activityId) &&
                            Objects.equals(auditInfo.getAuditor(), auditor)
                    )
                    .toList();

            AuditInfo auditInfo = auditInfos.isEmpty() ? null : auditInfos.get(0);
            if (auditInfo != null && auditInfo.getAuditTime() == null) {
                return "待办理";
            } else {
                return "已提交";
            }
        }
    }

    /**
     * 查询活动可编辑字段列表
     */
    @Cacheable(value = "activityEditableFields", key = "#activityId")
    public List<ActivityEditableFieldDTO> queryActivityEditableField(Integer activityId) {
        LambdaQueryWrapper<ActivityEditableField> wrapper = new LambdaQueryWrapper<>();

        wrapper.eq(ActivityEditableField::getActivityId, activityId);

        List<ActivityEditableField> activityEditableFields = activityEditableFieldMapper.selectList(wrapper);

        // Entity -> DTO
        return activityEditableFields.stream()
                .map(entity -> {
                    ActivityEditableFieldDTO dto = new ActivityEditableFieldDTO();
                    BeanUtils.copyProperties(entity, dto);
                    return dto;
                })
                .toList();
    }

    /**
     * 查询活动实例提交选项
     */
    @Cacheable(value = "submitOptions",
            key = "#p0.activityInstanceId + '-' + #p0.activityId + '-' + #p0.workflowInstanceId",
            unless = "#p0.activityInstanceId == null || #p0.activityId == null || #p0.workflowInstanceId == null")
    public ActivityInstanceSubmitOptionDTO querySubmitOption(ActivityInstanceRequest activityInstanceRequest) {
        ActivityInstanceSubmitOptionDTO submitOptionDTO = new ActivityInstanceSubmitOptionDTO();

        // 获取当前活动
        Activity currentActivity = activityMapper.selectById(activityInstanceRequest.getActivityId());

        submitOptionDTO.setSubmitMode(currentActivity.getSubmitMode());

        // 办理意见选项
        List<String> auditIdeaOptions = getAuditIdeaOptions(currentActivity);
        submitOptionDTO.setAuditIdeaOptions(auditIdeaOptions);

        // 目标活动id
        Integer targetActivityId = getTargetActivityId(activityInstanceRequest.getActivityInstanceId());
        // 获取目标活动
        Activity targetActivity = activityMapper.selectById(targetActivityId);

        // 有目标活动,当前不是最后活动
        if (targetActivity != null) {
            // 办理人选项
            // DTO -> Entity
            ActivityInstance activityInstance = new ActivityInstance();
            BeanUtils.copyProperties(activityInstanceRequest, activityInstance);
            List<RoleMemberDTO> auditMemberOptions = getSubmitMemberOptions(activityInstance, targetActivity);
            submitOptionDTO.setAuditMemberOptions(auditMemberOptions);

            submitOptionDTO.setTargetActivityName(targetActivity.getName());
        }

        return submitOptionDTO;
    }

    /**
     * 获取办理意见选项
     */
    private List<String> getAuditIdeaOptions(Activity activity) {
        // 获取意见选项字符串
        String idea = activity == null ? null : activity.getIdea();

        if (StringUtils.isBlank(idea)) {
            return new ArrayList<>();
        } else {
            // 这样既能处理 \n(Linux/Unix),也能正确处理 \r\n(Windows),去除多余的 \r 字符。
            return List.of(idea.split("\\r?\\n"));
        }
    }

    /**
     * 获取当前活动实例的目标活动id(下一个活动的id)
     */
    private Integer getTargetActivityId(Integer activityInstanceId) {
        // 获取变迁
        Transition transition = getTransition(activityInstanceId);

        return transition == null ? 0 : transition.getTargetActivityId();
    }

    /**
     * 获取变迁
     */
    private Transition getTransition(Integer activityInstanceId) {
        // 获取变迁id
        Integer transitionId = getTransitionId(activityInstanceId);

        return transitionMapper.selectById(transitionId);
    }

    /**
     * 获取变迁id
     */
    private Integer getTransitionId(Integer activityInstanceId) {
        // 获取变迁数据,通过执行存储过程,从返回数据集中获取变迁数据
        // 因为通过执行存储过程返回数据集(最多只有一行记录),数据集的字段是动态的,所以这里使用 LinkedHashMap<String, Object> 来接收
        LinkedHashMap<String, Object> transitData = workflowMapper.getTransitionMap(activityInstanceId);

        // 变迁id
        return (Integer) transitData.get("TransitID");
    }

    /**
     * 获取提交人选项
     */
    private List<RoleMemberDTO> getSubmitMemberOptions(ActivityInstance activityInstance, Activity activity) {
        if (activity == null) {
            activity = getActivity(activityInstance);
        }

        if (activity == null) return new ArrayList<>();

        // 构造角色成员查询 DTO
        RoleMemberQueryDTO queryDTO = new RoleMemberQueryDTO();
        queryDTO.setActivityId(activity.getId());
        queryDTO.setWorkflowInstanceId(activityInstance.getWorkflowInstanceId());

        // 过虑脚本
        String filterScript = switch (activity.getFilterType()) {
            case ActivityFilterTypeConstants.DEPT -> " ";
            case ActivityFilterTypeConstants.ACTIVITY -> "  ";
            case ActivityFilterTypeConstants.SCRIPT -> activity.getFilterValue();
            default -> "";
        };

        if (!StringUtils.isBlank(filterScript)) {
            // 处理过虑脚本中的参数
            filterScript = filterScript.replace(":WorkflowInstanceID", "#{workflowInstanceId}");

            // 确保SQL安全
            if (SqlScriptUtils.containsUnsafeKeywords(filterScript)) {
                log.warn("活动 = {},其过虑内容 = {},包含不安全的SQL关键字,已省略",
                        SecurityUtils.safeForLog(activity), filterScript);
                filterScript = "";
            }

            queryDTO.setFilterScript(filterScript);
        }

        return workflowMapper.selectRoleMemberList(queryDTO);
    }

    /**
     * 查询活动实例退回选项
     */
    @Cacheable(value = "rejectOptions",
            key = "#p0.activityInstanceId + '-' + #p0.activityId + '-' + #p0.workflowInstanceId",
            unless = "#p0.activityInstanceId == null || #p0.activityId == null || #p0.workflowInstanceId == null")
    public ActivityInstanceRejectOptionDTO queryRejectOption(ActivityInstanceRequest activityInstanceRequest) {
        ActivityInstanceRejectOptionDTO rejectOptionDTO = new ActivityInstanceRejectOptionDTO();

        // 获取当前活动
        Activity currentActivity = activityMapper.selectById(activityInstanceRequest.getActivityId());

        // 获取当前活动的办理意见选项
        List<String> auditIdeaOptions = getAuditIdeaOptions(currentActivity);
        rejectOptionDTO.setAuditIdeaOptions(auditIdeaOptions);

        // 获取当前活动的退回方式
        ActivityRejectMode rejectMode = currentActivity.getRejectMode();
        rejectOptionDTO.setRejectMode(rejectMode);

        // 退回给的目标活动实例
        ActivityInstance rejectToTargetActivityInstance;
        switch (rejectMode) {
            case FIRST -> // 获取首活动/启动活动
                    rejectToTargetActivityInstance = getStartActivityInstance(activityInstanceRequest);
            case PREVIOUS -> // 获取源头活动(上一个提交过来的活动)
                    rejectToTargetActivityInstance = getSourceActivityInstance(activityInstanceRequest);
            default -> rejectToTargetActivityInstance = new ActivityInstance();
        }

        // 通过退回给的目标活动实例,获取退回给的目标活动
        Activity rejectToTargetActivity = null;
        if (rejectToTargetActivityInstance != null) {
            rejectToTargetActivity = activityMapper.selectById(rejectToTargetActivityInstance.getActivityId());
        }

        // 获取退回给的目标活动的退回人选项
        List<ActivityInstanceAuditorDTO> rejectMemberOptions = getRejectMemberOptions(
                rejectToTargetActivityInstance,
                rejectToTargetActivity
        );
        rejectOptionDTO.setAuditMemberOptions(rejectMemberOptions);


        if (rejectToTargetActivity != null) {
            rejectOptionDTO.setTargetActivityId(rejectToTargetActivity.getId());
        }
        if (rejectToTargetActivity != null) {
            rejectOptionDTO.setTargetActivityName(rejectToTargetActivity.getName());
        }

        return rejectOptionDTO;
    }

    /**
     * 获取退回人选项
     */
    private List<ActivityInstanceAuditorDTO> getRejectMemberOptions(ActivityInstance activityInstance, Activity activity) {
        if (activity == null) {
            activity = getActivity(activityInstance);
        }

        List<ActivityInstanceAuditorDTO> rejectMemberOptions = new ArrayList<>();
        ActivityInstanceAuditorDTO auditorDTO = new ActivityInstanceAuditorDTO();
        // Entity -> DTO
        auditorDTO.setActivityId(activity.getId());
        auditorDTO.setActivityName(activity.getName());
        auditorDTO.setUserName(activityInstance.getAuditor());
        auditorDTO.setMoniker(activityInstance.getAuditorName());
        auditorDTO.setDeptId(activityInstance.getAuditorDeptId());
        auditorDTO.setDeptName(activityInstance.getAuditorDeptName());

        rejectMemberOptions.add(auditorDTO);

        return rejectMemberOptions;
    }

    /**
     * 通过活动实例,获取活动
     */
    private Activity getActivity(ActivityInstance activityInstance) {
        return activityMapper.selectById(activityInstance.getActivityId());
    }

    /**
     * 获取首活动实例/启动活动实例
     */
    private ActivityInstance getStartActivityInstance(ActivityInstanceRequest activityInstanceRequest) {
        // 通过流程实例id,获取活动实例列表
        LambdaQueryWrapper<ActivityInstance> activityInstanceWrapper = new LambdaQueryWrapper<>();
        activityInstanceWrapper.eq(ActivityInstance::getWorkflowInstanceId, activityInstanceRequest.getWorkflowInstanceId());
        List<ActivityInstance> activityInstances = activityInstanceMapper.selectList(activityInstanceWrapper);

        // 返回首活动实例/启动活动实例
        return activityInstances.get(0);
    }

    /**
     * 获取源头活动实例(上一个提交过来的活动实例)
     */
    private ActivityInstance getSourceActivityInstance(ActivityInstanceRequest activityInstanceRequest) {
        // 1、获取审批日志
        LambdaQueryWrapper<AuditLog> auditLogWrapper = new LambdaQueryWrapper<>();
        auditLogWrapper
                .eq(AuditLog::getIsSubmit, true)
                .eq(AuditLog::getTargetActivityInstanceId, activityInstanceRequest.getActivityInstanceId())
                .eq(AuditLog::getWorkflowInstanceId, activityInstanceRequest.getWorkflowInstanceId());
        // 因为有退回,造成有重复记录
        List<AuditLog> auditLogs = auditLogMapper.selectList(auditLogWrapper);

        if (auditLogs.isEmpty()) return null;

        // 2、获取源头活动实例id(上一个提交过来的活动实例id)
        Integer sourceActivityInstanceID = auditLogs.get(0).getActivityInstanceId();

        // 3、返回源头活动实例(上一个提交过来的活动实例)
        return activityInstanceMapper.selectById(sourceActivityInstanceID);
    }

    /**
     * 获取流程实例节点
     */
    @Cacheable(value = "workflowInstanceNode",
            key = "#p0.activityInstanceId + '-' + #p0.activityId",
            unless = "#p0.activityInstanceId == null || #p0.activityId == null")
    public WorkflowInstanceNode getWorkflowInstanceNode(ActivityInstanceRequest activityInstanceRequest) {
        // 目标活动id
        Integer targetActivityId = getTargetActivityId(activityInstanceRequest.getActivityInstanceId());
        // 获取目标活动
        Activity targetActivity = activityMapper.selectById(targetActivityId);

        // 有目标活动,当前不是最后活动
        if (targetActivity != null) {
            // 获取当前活动
            Activity currentCtivity = activityMapper.selectById(activityInstanceRequest.getActivityId());

            if (currentCtivity == null) {
                // 提高容错性,这里不抛出异常,而是返回 WorkflowInstanceNode.UNKNOWN
                // throw new BusinessException("无效的参数:活动id = " + activityInstanceDTO.getActivityId());
                return WorkflowInstanceNode.UNKNOWN;
            }

            if (currentCtivity.getIsStartNode()) {
                return WorkflowInstanceNode.FIRST;
            } else {
                return WorkflowInstanceNode.MIDDLE;
            }
        }
        // 无目标活动,当前就是最后活动/末活动
        else {
            return WorkflowInstanceNode.LAST;
        }
    }

    /**
     * 提交
     */
    @Transactional
    public void submit(ActivityInstanceSubmitRequest submitRequest) {
        // 当前用户
        String userName = PublicUtils.getUserName();
        // 办理时间
        LocalDateTime auditTime = LocalDateTime.now();

        // 获取当前活动实例
        ActivityInstanceRequest currentActivityInstance = submitRequest.getActivityInstance();

        // 更新办理信息(当前活动办理),更新办理人、办理意见、办理时间
        LambdaUpdateWrapper<AuditInfo> auditInfoWrapper = new LambdaUpdateWrapper<>();
        auditInfoWrapper
                .set(AuditInfo::getAuditor, userName)
                .set(AuditInfo::getAuditIdea, submitRequest.getAuditIdea())
                .set(AuditInfo::getAuditTime, auditTime)
                .eq(AuditInfo::getWorkflowInstanceId, currentActivityInstance.getWorkflowInstanceId())
                .eq(AuditInfo::getActivityId, currentActivityInstance.getActivityId());
        auditInfoMapper.update(auditInfoWrapper);

        // 更新活动实例(当前活动实例),更新状态为已办理
        LambdaUpdateWrapper<ActivityInstance> activityInstanceWrapper = new LambdaUpdateWrapper<>();
        activityInstanceWrapper
                .set(ActivityInstance::getState, ActivityInstanceStateConstants.AUDITED)
                .eq(ActivityInstance::getId, currentActivityInstance.getActivityInstanceId());
        activityInstanceMapper.update(activityInstanceWrapper);

        // 获取变迁(活动/当前活动/源头活动 -> 目标活动/下一活动)
        Transition transition = getTransition(currentActivityInstance.getActivityInstanceId());
        // 有变迁,即有目标活动/下一活动
        if (transition != null) {
            // 查找办理信息,查找条件:流程实例id+目标活动id
            LambdaQueryWrapper<AuditInfo> auditInfoQueryWrapper = new LambdaQueryWrapper<>();
            auditInfoQueryWrapper
                    .eq(AuditInfo::getWorkflowInstanceId, currentActivityInstance.getWorkflowInstanceId())
                    .eq(AuditInfo::getActivityId, transition.getTargetActivityId());
            List<AuditInfo> auditInfos = auditInfoMapper.selectList(auditInfoQueryWrapper);

            if (!auditInfos.isEmpty()) {
                // 更新办理信息(目标活动办理/下一活动办理),更新办理人,清空办理意见、办理时间
                auditInfoWrapper.clear();
                auditInfoWrapper
                        .set(AuditInfo::getAuditor, submitRequest.getTargetAuditor())
                        .set(AuditInfo::getAuditIdea, null)
                        .set(AuditInfo::getAuditTime, null)
                        .eq(AuditInfo::getWorkflowInstanceId, currentActivityInstance.getWorkflowInstanceId())
                        .eq(AuditInfo::getActivityId, transition.getTargetActivityId());
                auditInfoMapper.update(auditInfoWrapper);
            } else {
                // 生成办理信息(目标活动办理/下一活动办理)
                AuditInfo auditInfo = getAuditInfo(submitRequest, transition);
                auditInfoMapper.insert(auditInfo);
            }

            // 查找活动实例,查找条件:流程实例id+目标活动id
            LambdaQueryWrapper<ActivityInstance> activityInstanceQueryWrapper = new LambdaQueryWrapper<>();
            activityInstanceQueryWrapper
                    .eq(ActivityInstance::getWorkflowInstanceId, currentActivityInstance.getWorkflowInstanceId())
                    .eq(ActivityInstance::getActivityId, transition.getTargetActivityId());
            List<ActivityInstance> activityInstances = activityInstanceMapper.selectList(activityInstanceQueryWrapper);

            // 目标活动实例/下一活动实例
            ActivityInstance targetActivityInstance;
            if (!activityInstances.isEmpty()) {
                targetActivityInstance = activityInstances.get(0);
                // 更新活动实例(目标活动实例/下一活动实例),更新状态为办理中/待办理
                activityInstanceWrapper.clear();
                activityInstanceWrapper
                        .set(ActivityInstance::getState, ActivityInstanceStateConstants.PENDING)
                        .eq(ActivityInstance::getWorkflowInstanceId, targetActivityInstance.getWorkflowInstanceId())
                        .eq(ActivityInstance::getActivityId, targetActivityInstance.getActivityId());
                activityInstanceMapper.update(activityInstanceWrapper);
            } else {
                // 生成活动实例(目标活动实例/下一活动实例)
                targetActivityInstance = getActivityInstance(submitRequest, transition, auditTime);
                activityInstanceMapper.insert(targetActivityInstance);
            }

            // 生成办理日志(活动/当前活动/源头活动 -[变迁]> 目标活动)
            AuditLog auditLog = getAuditLog(submitRequest, transition, auditTime, targetActivityInstance);
            auditLogMapper.insert(auditLog);

            // 设置流程实例的审批完成标识
            LambdaUpdateWrapper<WorkflowInstance> workflowInstanceWrapper = new LambdaUpdateWrapper<>();
            workflowInstanceWrapper
                    .set(WorkflowInstance::getAudited, transition.getAudited())
                    .eq(WorkflowInstance::getId, currentActivityInstance.getWorkflowInstanceId());
            workflowInstanceMapper.update(workflowInstanceWrapper);
        }
        // 无变迁,即当前是最后活动
        else {
            // 更新流程实例,更新状态为已结案
            LambdaUpdateWrapper<WorkflowInstance> workflowInstanceWrapper = new LambdaUpdateWrapper<>();
            workflowInstanceWrapper
                    .set(WorkflowInstance::getState, WorkflowInstanceStateConstants.CLOSED)
                    .eq(WorkflowInstance::getId, currentActivityInstance.getWorkflowInstanceId());
            workflowInstanceMapper.update(workflowInstanceWrapper);
        }

        // 调用提交事件
        workflowMapper.callSubmitEvent(
                currentActivityInstance.getWorkflowInstanceId(),
                currentActivityInstance.getActivityId(),
                userName
        );
    }

    /**
     * 获取办理信息
     */
    private AuditInfo getAuditInfo(ActivityInstanceAudit auditDTO, Transition transition) {
        AuditInfo auditInfo = new AuditInfo();
        auditInfo.setWorkflowInstanceId(auditDTO.getActivityInstance().getWorkflowInstanceId());
        auditInfo.setActivityId(transition.getTargetActivityId());
        auditInfo.setSourceActivityId(auditDTO.getActivityInstance().getActivityId());
        auditInfo.setAuditor(auditDTO.getTargetAuditor());

        return auditInfo;
    }

    /**
     * 获取活动实例
     */
    private ActivityInstance getActivityInstance(
            ActivityInstanceAudit auditDTO,
            Transition transition,
            LocalDateTime auditTime
    ) {
        ActivityInstance activityInstance = new ActivityInstance();

        ActivityInstanceRequest instanceDTO = auditDTO.getActivityInstance();
        if (instanceDTO != null) {
            activityInstance.setWorkflowInstanceId(instanceDTO.getWorkflowInstanceId());
            activityInstance.setBusinessKey(auditDTO.getActivityInstance().getBusinessKey());
            String taskSummary = String.format("%s:%s-web", "资金分配", instanceDTO.getBusinessKey());
            activityInstance.setTaskSummary(taskSummary);
        } else {
            activityInstance.setTaskSummary("未知:未知-web");
        }

        User targetUser = userMapper.selectByUserName(auditDTO.getTargetAuditor());
        if (targetUser != null) {
            activityInstance.setAuditor(targetUser.getUserName());
            activityInstance.setAuditorName(targetUser.getMoniker());
            activityInstance.setAuditorDeptId(targetUser.getDeptId());
            activityInstance.setAuditorDeptName(targetUser.getDeptName());
        }

        activityInstance.setActivityId(transition.getTargetActivityId());
        activityInstance.setAuditor(auditDTO.getTargetAuditor());

        activityInstance.setTaskId(20024);
        activityInstance.setCreateTime(auditTime);
        activityInstance.setState(ActivityInstanceStateConstants.PENDING);

        return activityInstance;
    }

    /**
     * 获取办理日志
     */
    private AuditLog getAuditLog(
            ActivityInstanceAudit auditDTO,
            Transition transition,
            LocalDateTime auditTime,
            ActivityInstance targetActivityInstance
    ) {
        AuditLog auditLog = new AuditLog();
        auditLog.setWorkflowId(auditDTO.getActivityInstance().getWorkflowId());
        auditLog.setConcretWorkflowId(auditDTO.getActivityInstance().getConcretWorkflowId());
        auditLog.setConcretWorkflowVersion("");
        auditLog.setWorkflowInstanceId(auditDTO.getActivityInstance().getWorkflowInstanceId());
        auditLog.setActivityId(auditDTO.getActivityInstance().getActivityId());
        auditLog.setActivityInstanceId(auditDTO.getActivityInstance().getActivityInstanceId());

        if (transition == null) {
            // 退回
            auditLog.setTransitionId(-1);
            auditLog.setTargetActivityId(targetActivityInstance.getActivityId());
            auditLog.setIsSubmit(false);
        } else {
            // 提交
            auditLog.setTransitionId(transition.getId());
            auditLog.setTargetActivityId(transition.getTargetActivityId());
            auditLog.setIsSubmit(true);
        }

        auditLog.setTargetActivityInstanceId(targetActivityInstance.getId());
        auditLog.setAuditor(PublicUtils.getUserName());
        auditLog.setAuditIdea(auditDTO.getAuditIdea());
        auditLog.setAuditTime(auditTime);

        return auditLog;
    }

    /**
     * 退回
     */
    public void reject(ActivityInstanceRejectRequest rejectRequest) {
        // 当前用户
        String userName = PublicUtils.getUserName();
        // 办理时间
        LocalDateTime auditTime = LocalDateTime.now();

        // 获取当前活动实例
        ActivityInstanceRequest currentActivityInstance = rejectRequest.getActivityInstance();

        // 获取当前活动
        Activity currentActivity = activityMapper.selectById(rejectRequest.getActivityInstance().getActivityId());

        // 获取当前活动的退回方式
        ActivityRejectMode rejectMode = currentActivity.getRejectMode();

        // 退回给的目标活动实例
        ActivityInstance rejectToTargetActivityInstance;
        switch (rejectMode) {
            case FIRST -> // 获取首活动/启动活动
                    rejectToTargetActivityInstance = getStartActivityInstance(currentActivityInstance);
            case PREVIOUS -> // 获取源头活动(上一个提交过来的活动)
                    rejectToTargetActivityInstance = getSourceActivityInstance(currentActivityInstance);
            default -> {
                log.error("退回失败:参数无效,rejectMode = {}", rejectMode);
                throw new BusinessException("退回失败");
            }
        }

        // 更新办理信息(当前活动办理),更新办理人、办理意见、办理时间
        LambdaUpdateWrapper<AuditInfo> auditInfoWrapper = new LambdaUpdateWrapper<>();
        auditInfoWrapper
                .set(AuditInfo::getAuditor, userName)
                .set(AuditInfo::getAuditIdea, rejectRequest.getAuditIdea())
                .set(AuditInfo::getAuditTime, auditTime)
                .eq(AuditInfo::getWorkflowInstanceId, currentActivityInstance.getWorkflowInstanceId())
                .eq(AuditInfo::getActivityId, currentActivityInstance.getActivityId());
        auditInfoMapper.update(auditInfoWrapper);

        // 更新活动实例(当前活动实例),更新状态为已办理
        LambdaUpdateWrapper<ActivityInstance> activityInstanceWrapper = new LambdaUpdateWrapper<>();
        activityInstanceWrapper
                .set(ActivityInstance::getState, ActivityInstanceStateConstants.AUDITED)
                .eq(ActivityInstance::getId, currentActivityInstance.getActivityInstanceId());
        activityInstanceMapper.update(activityInstanceWrapper);

        // 更新活动实例(更新退回给的目标活动实例),更新状态为办理中/待办理
        activityInstanceWrapper.clear();
        if (rejectToTargetActivityInstance != null) {
            activityInstanceWrapper
                    .set(ActivityInstance::getState, ActivityInstanceStateConstants.PENDING)
                    .eq(ActivityInstance::getId, rejectToTargetActivityInstance.getId());
        }
        activityInstanceMapper.update(activityInstanceWrapper);

        // 生成办理日志(活动/当前活动/源头活动 -[退回无变迁]> 目标活动)
        AuditLog auditLog = getAuditLog(rejectRequest, null, auditTime, rejectToTargetActivityInstance);
        auditLogMapper.insert(auditLog);

        // 清除流程实例的审批完成标识
        LambdaUpdateWrapper<WorkflowInstance> workflowInstanceWrapper = new LambdaUpdateWrapper<>();
        workflowInstanceWrapper
                .set(WorkflowInstance::getAudited, false)
                .eq(WorkflowInstance::getId, currentActivityInstance.getWorkflowInstanceId())
                .eq(WorkflowInstance::getAudited, true);
        workflowInstanceMapper.update(workflowInstanceWrapper);

        // 调用退回事件
        workflowMapper.callRejectEvent(
                currentActivityInstance.getWorkflowInstanceId(),
                currentActivityInstance.getActivityId(),
                userName
        );
    }
}

Mapper

资金分配 Mapper

src/main/java/com/weiyu/mapper/CapitalAllocateMapper.java

package com.weiyu.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.weiyu.model.CapitalAllocateQueryResultDTO;
import com.weiyu.model.CapitalAllocateMaster;
import com.weiyu.model.CapitalAllocateQueryRequest;

import java.util.List;

/**
 * 资金分配 Mapper
 */
public interface CapitalAllocateMapper extends BaseMapper<CapitalAllocateMaster> {
    /**
     * 生成资金分配工作流程
     */
    void generateWorkflow(String userName, Integer capitalId);

    /**
     * 查询资金分配列表,通过关键字搜索
     */
    List<CapitalAllocateQueryResultDTO> selectListByKeyword(String keyword, String userName);

    /**
     * 查询资金分配列表,通过自定义查询
     */
    List<CapitalAllocateQueryResultDTO> selectListByCustom(CapitalAllocateQueryRequest queryRequest, String userName);
}

资金分配明细 Mapper

src/main/java/com/weiyu/mapper/CapitalAllocateDetailMapper.java

package com.weiyu.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.weiyu.model.CapitalAllocateDetail;
import com.weiyu.model.CapitalAllocateDetailDTO;

import java.util.List;

/**
 * 资金分配明细 Mapper
 */
public interface CapitalAllocateDetailMapper extends BaseMapper<CapitalAllocateDetail> {
    /**
     * 查询资金分配明细列表 - 通过资金信息id
     */
    List<CapitalAllocateDetailDTO> selectListByCapitalInfoId(Integer capitalId);

    /**
     * 查询资金分配明细列表 - 通过资金分配id
     */
    List<CapitalAllocateDetailDTO> selectListByAllocateId(Integer allocateId);
}

流程实例 Mapper

src/main/java/com/weiyu/mapper/WorkflowInstanceMapper.java

package com.weiyu.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.weiyu.model.WorkflowInstance;

/**
 * 流程实例 Mapper
 */
public interface WorkflowInstanceMapper extends BaseMapper<WorkflowInstance> {
}

活动实例 Mapper

src/main/java/com/weiyu/mapper/ActivityInstanceMapper.java

package com.weiyu.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.weiyu.model.ActivityInstance;

/**
 * 活动实例 Mapper
 */
public interface ActivityInstanceMapper extends BaseMapper<ActivityInstance> {
}

办理信息 Mapper

src/main/java/com/weiyu/mapper/AuditInfoMapper.java

package com.weiyu.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.weiyu.model.AuditInfo;

/**
 * 办理信息 Mapper
 */
public interface AuditInfoMapper extends BaseMapper<AuditInfo> {
}

办理日志 Mapper

src/main/java/com/weiyu/mapper/AuditLogMapper.java

package com.weiyu.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.weiyu.model.AuditLog;

/**
 * 办理日志 Mapper
 */
public interface AuditLogMapper extends BaseMapper<AuditLog> {
}

 工作流程 Mapper

src/main/java/com/weiyu/mapper/WorkflowMapper.java

package com.weiyu.mapper;

import com.weiyu.model.AuditInfoJoinQueryDTO;
import com.weiyu.model.RoleMemberQueryDTO;
import com.weiyu.model.RoleMemberDTO;

import java.util.LinkedHashMap;
import java.util.List;

/**
 * 工作流程 Mapper
 */
public interface WorkflowMapper {

    /**
     * 联合查询办理信息列表
     */
    List<AuditInfoJoinQueryDTO> selectAuditInfoListByJoinQuery(Integer workflowInstanceId);

    /**
     * 查询角色成员列表
     */
    List<RoleMemberDTO> selectRoleMemberList(RoleMemberQueryDTO queryDTO);

    /**
     * 获取变迁id,通过执行存储过程,从返回数据集中获取变迁id
     * 因为通过执行存储过程返回数据集(最多只有一行记录),数据集的字段是动态的,所以这里使用 LinkedHashMap<String, Object> 来接收
     */
    LinkedHashMap<String, Object> getTransitionMap(Integer activityInstanceId);

    /**
     * 调用提交事件
     */
    void callSubmitEvent(Integer workflowInstanceId, Integer activityId, String userName);

    /**
     * 调用退回事件
     */
    void callRejectEvent(Integer workflowInstanceId, Integer activityId, String userName);
}

活动 Mapper

src/main/java/com/weiyu/mapper/ActivityMapper.java

package com.weiyu.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.weiyu.model.Activity;

/**
 * 活动 Mapper
 */
public interface ActivityMapper extends BaseMapper<Activity> {
}

活动可编辑字段 Mapper

src/main/java/com/weiyu/mapper/ActivityEditableFieldMapper.java

package com.weiyu.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.weiyu.model.ActivityEditableField;

/**
 * 活动可编辑字段 Mapper
 */
public interface ActivityEditableFieldMapper extends BaseMapper<ActivityEditableField> {
}

变迁 Mapper

src/main/java/com/weiyu/mapper/TransitionMapper.java

package com.weiyu.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.weiyu.model.Transition;

/**
 * 变迁 Mapper
 */
public interface TransitionMapper extends BaseMapper<Transition> {
}

XML

src/main/resources/com/weiyu/mapper/CapitalAllocateMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.weiyu.mapper.CapitalAllocateMapper">
    <!-- mssql -->
    <!-- 生成资金分配工作流程,使用标准 JDBC 调用语法,jdbcType 保留与否均可,建议保留以提高数据库兼容性 -->
    <update id="generateWorkflow" statementType="CALLABLE">
        {
        call proc_CapitalAllocate(
        #{userName, jdbcType=VARCHAR},
        #{capitalId, jdbcType=INTEGER}
        )
        }
    </update>

    <!-- 资金分配列表查询字段 -->
    <sql id="capitalAllocateSelectFields">
        cam_ID as id,
        cam_CreateDate as createTime,
        cim_CapitalNo as capitalNo,
        cim_CapitalName as capitalName,
        cim_CapitalType as capitalType,
        cim_CapitalSource as capitalSource,
        cim_CapitalTotal as capitalTotal,
        cim_CapitalValidTotal as capitalValidTotal,
        StateFlag as allocateState,
        WorkflowID as workflowId,
        ConcretWorkflowID as concretWorkflowId,
        WorkflowInstanceID as workflowInstanceId,
        ActivityInstanceID as activityInstanceId,
        ActivityID as activityId,
        Actor as allowAuditor,
        BizPKValue as businessKey,
        StateFlag as workflowInstanceState
    </sql>

    <!-- 查询资金分配列表,通过关键字搜索 -->
    <select id="selectListByKeyword" resultType="com.weiyu.model.CapitalAllocateQueryResultDTO">
        select
        <include refid="capitalAllocateSelectFields" />
        from CapitalAllocateMaster
        inner join CapitalInfoManage on cim_ID = cam_MasterID
        inner join vw_BizPKValueList on cam_ID = cast(BizPKValue as int) and Actor = #{userName}
        where WorkflowID = 24
        and StateFlag = 0
        and (cim_CapitalNo like '%' + #{keyword} + '%' or cim_CapitalName like '%' + #{keyword} + '%')
    </select>

    <!-- 查询资金分配列表,通过自定义查询 -->
    <select id="selectListByCustom" resultType="com.weiyu.model.CapitalAllocateQueryResultDTO">
        select
        <include refid="capitalAllocateSelectFields" />
        from CapitalAllocateMaster
        inner join CapitalInfoManage on cim_ID = cam_MasterID
        inner join vw_BizPKValueList on cam_ID = cast(BizPKValue as int) and Actor = #{userName}
        <where>
            WorkflowID = 24
            <if test="queryRequest.allocateStates != null and queryRequest.allocateStates.size() > 0">
                and StateFlag in
                <foreach collection="queryRequest.allocateStates" item="item" separator="," open="(" close=")">
                    #{item}
                </foreach>
            </if>

            <if test="queryRequest.capitalNo != null and queryRequest.capitalNo != ''">
                and cim_CapitalNo like '%' + #{queryRequest.capitalNo} + '%'
            </if>

            <if test="queryRequest.capitalName != null and queryRequest.capitalName != ''">
                and cim_CapitalName like '%' + #{queryRequest.capitalName} + '%'
            </if>

            <if test="queryRequest.capitalType != null and queryRequest.capitalType != ''">
                and cim_CapitalType like '%' + #{queryRequest.capitalType} + '%'
            </if>

            <if test="queryRequest.capitalSource != null and queryRequest.capitalSource != ''">
                and cim_CapitalSource like '%' + #{queryRequest.capitalSource} + '%'
            </if>

            <!-- 开始日期条件:将字符串转为日期,并取当天开始时间 -->
            <if test="queryRequest.createDateRange != null and queryRequest.createDateRange.beginDate != null and queryRequest.createDateRange.beginDate != ''">
                and cam_CreateDate >= CONVERT(datetime, #{queryRequest.createDateRange.beginDate}, 120)
            </if>

            <!-- 结束日期条件:取当天结束时间(即第二天开始时间的前一毫秒) -->
            <if test="queryRequest.createDateRange != null and queryRequest.createDateRange.endDate != null and queryRequest.createDateRange.endDate != ''">
                and cam_CreateDate &lt; DATEADD(day, 1, CONVERT(datetime, #{queryRequest.createDateRange.endDate}, 120))
            </if>
        </where>
    </select>
</mapper>

src/main/resources/com/weiyu/mapper/CapitalAllocateDetailMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.weiyu.mapper.CapitalAllocateDetailMapper">
    <!-- mssql -->
    <!-- 资金分配明细查询字段 -->
    <sql id="capitalAllocateDetailSelectFields">
        cad.cad_ID as id,
        cad.cad_CapitalIndexDept as deptId,
        dept1.dep_Name as deptName,
        cad.cad_CapitalIndexTotal as total,
        cad.cad_CapitalBudgetPlan as budget,
        cad.cad_CapitalPayType as payType,
        cad.cad_CapitalPayMode as payMode,
        cad.cad_CapitalIndexValidTotal as validTotal,
        cad.cad_CapitalIndexAssistDept as assistDeptName,
        cad.cad_CapitalIndexBackTotal as backTotal,
        cad.cad_CapitalIndexBackTime as backTime,
        cad.cad_Person as userPerson,
        cad.cad_PersonName as userMoniker
    </sql>

    <!-- 查询资金分配明细列表 - 通过资金信息id -->
    <select id="selectListByCapitalInfoId" resultType="com.weiyu.model.CapitalAllocateDetailDTO">
        select
        <include refid="capitalAllocateDetailSelectFields" />
        from CapitalAllocateDetail cad
        inner join CapitalAllocateMaster cam on cam.cam_ID = cad.cad_MasterID
        inner join CapitalInfoManage cim on cim.cim_ID = cam.cam_MasterID
        left join Department dept1 on dept1.dep_ID = cad.cad_CapitalIndexDept
        where cim_ID = #{capitalId}
    </select>

    <!-- 查询资金分配明细列表 - 通过资金分配id -->
    <select id="selectListByAllocateId" resultType="com.weiyu.model.CapitalAllocateDetailDTO">
        select
        <include refid="capitalAllocateDetailSelectFields" />
        from CapitalAllocateDetail cad
        inner join CapitalAllocateMaster cam on cam.cam_ID = cad.cad_MasterID
        inner join CapitalInfoManage cim on cim.cim_ID = cam.cam_MasterID
        left join Department dept1 on dept1.dep_ID = cad.cad_CapitalIndexDept
        where cam_ID = #{allocateId}
    </select>
</mapper>

src/main/resources/com/weiyu/mapper/WorkflowMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.weiyu.mapper.WorkflowMapper">
    <!-- mssql -->
    <!-- 查询办理信息列表 -->
    <select id="selectAuditInfoListByJoinQuery" resultType="com.weiyu.model.AuditInfoJoinQueryResultDTO">
        select
        ai_ID as id,
        ai_ActivityID as activityId,
        act_Name as activityName,
        ai_Auditor as auditor,
        emp_Name as auditorName,
        ai_AuditTime as auditTime,
        ai_AuditIdea as auditIdea
        from AuditInfo
        inner join Activity on act_ID = ai_ActivityID
        left join Employee on emp_ID = ai_Auditor
        where ai_WorkflowInstanceID = #{workflowInstanceId}
    </select>

    <!-- 查询角色成员列表 -->
    <select id="selectRoleMemberList" resultType="com.weiyu.model.RoleMemberDTO">
        select distinct
        qmp_RoleID as roleId,
        qmr_RoleName as roleName,
        qmr_EmpID as userName,
        emp_Name as moniker,
        emp_DeptID as deptId,
        dep_Name as deptName,
        qmr_NumberOrder as sortNo
        from VW_RolePerson
        where exists(
        select 1
        from Activity_BizRole
        where ABR_ActivityID = #{activityId} and ABR_BizRoleID = qmp_RoleID
        )
        <!-- 动态拼接 sql 脚本(过虑脚本),简单的黑名单校验(使用拼接SQL的模式,不能绝对防止SQL注入,更安全的实践:避免使用 ${}) -->
        <if test="filterScript != null
        and !filterScript.contains(' delete ') and !filterScript.contains(' update ') and !filterScript.contains(' insert ')
        and !filterScript.contains(' execute ') and !filterScript.contains(' exec ') and !filterScript.contains(' call ')
        and !filterScript.contains(' drop ') and !filterScript.contains(' alter ') and !filterScript.contains(' create ')">
            ${filterScript}
        </if>
        order by qmr_NumberOrder
    </select>

    <!-- 获取变迁id,查询结果集返回两列:[TransitID],[(无列名)] -->
    <!-- 使用标准 JDBC 调用语法,jdbcType 保留与否均可,建议保留以提高数据库兼容性 -->
    <!-- 因为通过执行存储过程返回数据集(最多只有一行记录),数据集的字段是动态的,所以这里 resultType="java.util.LinkedHashMap" -->
    <select id="getTransitionMap" resultType="java.util.LinkedHashMap">
        {
        call proc_GetExecutableTransit(
        #{activityInstanceId, jdbcType=INTEGER}
        )
        }
    </select>

    <!-- 调用提交事件 -->
    <!-- 使用标准 JDBC 调用语法,jdbcType 保留与否均可,建议保留以提高数据库兼容性 -->
    <update id="callSubmitEvent">
        {
        call proc_WorkflowInstanceActivitySubmitAction(
        #{workflowInstanceId, jdbcType=INTEGER},
        #{activityId, jdbcType=INTEGER},
        #{userName, jdbcType=VARCHAR}
        )
        }
    </update>

    <!-- 调用退回事件 -->
    <!-- 使用标准 JDBC 调用语法,jdbcType 保留与否均可,建议保留以提高数据库兼容性 -->
    <update id="callRejectEvent">
        {
        call proc_WorkflowInstanceActivityWithdrawAction(
        #{workflowInstanceId, jdbcType=INTEGER},
        #{activityId, jdbcType=INTEGER},
        #{userName, jdbcType=VARCHAR}
        )
        }
    </update>
</mapper>

实体类

资金分配主表实体类

src/main/java/com/weiyu/model/CapitalAllocateMaster.java

package com.weiyu.model;

import com.baomidou.mybatisplus.annotation.*;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

/**
 * 资金分配主表实体类
 */
@Data
@NoArgsConstructor
@TableName("CapitalAllocateMaster")
public class CapitalAllocateMaster {
    /**
     * 主键id
     */
    @TableId(value = "cam_ID", type = IdType.AUTO)
    private Integer id;

    /**
     * 关联主表id(CapitalInfo.id)
     */
    @TableField("cam_MasterID")
    @NotNull(message = "关联主表id不能为空")
    @Min(value = 1, message = "关联主表id不能小于1")
    private Integer masterId;

    /**
     * 创建时间
     */
    @TableField(value = "cam_CreateDate", fill = FieldFill.INSERT)
    private LocalDateTime createTime;
}

资金分配明细实体类

src/main/java/com/weiyu/model/CapitalAllocateDetail.java

package com.weiyu.model;

import com.baomidou.mybatisplus.annotation.*;
import jakarta.validation.constraints.*;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * 资金分配明细实体类
 */
@Data
@NoArgsConstructor
@TableName("CapitalAllocateDetail")
public class CapitalAllocateDetail {
    /**
     * 主键id
     */
    @TableId(value = "cad_ID", type = IdType.AUTO)
    private Integer id;

    /**
     * 关联主表id(CapitalAllocateMaster.id)
     */
    @TableField("cad_MasterID")
    @NotNull(message = "关联主表id不能为空")
    @Min(value = 0, message = "关联主表id不能为负数")
    private Integer masterId;

    /**
     * 指标使用部门
     */
    @TableField("cad_CapitalIndexDept")
    @NotBlank(message = "指标使用部门不能为空")
    private String deptId;

    /**
     * 指标分配金额
     */
    @TableField("cad_CapitalIndexTotal")
    @NotNull(message = "指标分配金额不能为空")
    @DecimalMin(value = "0.0", message = "指标分配金额不能为负数")
    private BigDecimal total;

    /**
     * 预算情况
     */
    @TableField("cad_CapitalBudgetPlan")
    @NotBlank(message = "预算情况不能为空")
    private String budget;

    /**
     * 支出分类
     */
    @TableField("cad_CapitalPayType")
    @NotBlank(message = "支出分类不能为空")
    private String payType;

    /**
     * 支出方式
     */
    @TableField("cad_CapitalPayMode")
    @NotBlank(message = "支出方式不能为空")
    private String payMode;

    /**
     * 指标可用金额
     */
    @TableField("cad_CapitalIndexValidTotal")
    private BigDecimal validTotal;

    /**
     * 协助部门名称
     */
    @TableField("cad_CapitalIndexAssistDept")
    @Size(max = 50)
    private String assistDeptName;

    /**
     * 使用人
     */
    @TableField(value = "cad_Person", updateStrategy = FieldStrategy.ALWAYS) // 允许更新为 null,需设置更新策略,字段更新为 ALWAYS
    @Size(max = 20)
    private String userPerson;

    /**
     * 使用人姓名
     */
    @TableField(value = "cad_PersonName", updateStrategy = FieldStrategy.ALWAYS) // 允许更新为 null,需设置更新策略,字段更新为 ALWAYS
    @Size(max = 50)
    private String userMoniker;

    /**
     * 回笼金额
     */
    @TableField("cad_CapitalIndexBackTotal")
    @DecimalMin(value = "0.0", message = "回笼金额不能为负数")
    private BigDecimal backTotal;

    /**
     * 回笼日期
     */
    @TableField("cad_CapitalIndexBackTime")
    private LocalDateTime backTime;

    /**
     * 分配人姓名
     */
    @TableField("cad_Remark")
    @Size(max = 50)
    private String allocater;

    /**
     * 创建时间
     */
    @TableField(value = "cad_CreateDate", fill = FieldFill.INSERT)
    private LocalDateTime createTime;
}

流程实例实体类

src/main/java/com/weiyu/model/WorkflowInstance.java

package com.weiyu.model;

import com.baomidou.mybatisplus.annotation.*;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

/**
 * 流程实例实体类
 */
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
@TableName("WorkflowInstance")
public class WorkflowInstance {
    /**
     * 主键id
     */
    @TableId(value = "wi_ID", type = IdType.AUTO)
    private Integer id;

    /**
     * 流程实例id
     */
    @TableField("wi_ConcretWorkflowID")
    @NotNull
    @Min(1)
    private Integer concretWorkflowId;

    /**
     * 创建人
     */
    @TableField("wi_Creator")
    @NotNull
    @Size(max = 20)
    private String creator;

    /**
     * 创建时间
     */
    @TableField(value = "wi_CreateDate", fill = FieldFill.INSERT)
    @NotNull
    private LocalDateTime createTime;

    /**
     * 业务主键
     */
    @TableField("wi_BizPKValue")
    @NotBlank
    @Size(max = 100)
    private String businessKey;

    /**
     * 流程实例状态
     */
    @TableField("wi_State")
    @NotNull
    private Integer state;

    /**
     * 允许终止/允许放弃
     */
    @TableField("wi_CanHangup")
    private Boolean canHangup;

    /**
     * 是否审批完成
     */
    @TableField("wi_Audited")
    private Boolean audited;
}

活动实例实体类

src/main/java/com/weiyu/model/ActivityInstance.java

package com.weiyu.model;

import com.baomidou.mybatisplus.annotation.*;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

/**
 * 活动实例实体类
 */
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
@TableName("ActivityInstance")
public class ActivityInstance {
    /**
     * 主键id
     */
    @TableId(value = "ai_ID", type = IdType.AUTO)
    private Integer id;

    /**
     * 流程实例id
     */
    @TableField("ai_WorkflowInstanceID")
    @NotNull
    @Min(1)
    private Integer workflowInstanceId;

    /**
     * 活动id
     */
    @TableField("ai_ActivityID")
    @NotNull
    @Min(1)
    private Integer activityId;

    /**
     * 执行人
     */
    @TableField("ai_Actor")
    @Size(max = 20)
    private String auditor;

    /**
     * 执行人姓名
     */
    @TableField("ai_ActorName")
    @Size(max = 50)
    private String auditorName;

    /**
     * 执行人部门id
     */
    @TableField("ai_ExecuteDeptID")
    @Size(max = 10)
    private String auditorDeptId;

    /**
     * 执行人部门名称
     */
    @TableField("ai_ExecuteDeptName")
    @Size(max = 50)
    private String auditorDeptName;

    /**
     * 任务描述
     */
    @TableField("ai_TaskSummary")
    @Size(max = 100)
    private String taskSummary;

    /**
     * 任务id
     */
    @TableField("ai_TaskID")
    @NotNull
    private Integer taskId;

    /**
     * 业务主键
     */
    @TableField("ai_BizPKValue")
    @NotBlank
    @Size(max = 100)
    private String businessKey;

    /**
     * 创建时间
     */
    @TableField(value = "ai_CreateDate", fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    /**
     * 状态
     */
    @TableField("ai_State")
    @NotNull
    private Integer state;
}

办理信息实体类

src/main/java/com/weiyu/model/AuditInfo.java

package com.weiyu.model;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

/**
 * 办理信息实体类
 */
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
@TableName("AuditInfo")
public class AuditInfo {
    /**
     * 主键id
     */
    @TableId(value = "ai_ID", type = IdType.AUTO)
    private Integer id;

    /**
     * 流程实例id
     */
    @TableField("ai_WorkflowInstanceID")
    @NotNull
    @Min(1)
    private Integer workflowInstanceId;

    /**
     * 活动id
     */
    @TableField("ai_ActivityID")
    @NotNull
    @Min(1)
    private Integer activityId;

    /**
     * 来源活动id
     */
    @TableField("ai_SourceActivityID")
    @NotNull
    @Min(1)
    private Integer sourceActivityId;

    /**
     * 办理人
     */
    @TableField("ai_Auditor")
    @Size(max = 20)
    private String auditor;

    /**
     * 办理意见
     */
    @TableField("ai_AuditIdea")
    @Size(max = 500)
    private String auditIdea;

    /**
     * 办理时间
     */
    @TableField("ai_AuditTime")
    private LocalDateTime auditTime;
}

办理日志实体类

src/main/java/com/weiyu/model/AuditLog.java

package com.weiyu.model;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

/**
 * 办理日志实体类
 * 用于描述:(活动/当前活动/源头活动 -[变迁]> 目标活动)
 */
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
@TableName("AuditLog")
public class AuditLog {
    /**
     * 主键id
     */
    @TableId(value = "al_ID", type = IdType.AUTO)
    private Integer id;

    /**
     * 流程id
     */
    @TableField("al_WorkflowID")
    @NotNull
    @Min(1)
    private Integer workflowId;

    /**
     * 流程版本id
     */
    @TableField("al_ConcretWorkflowID")
    @NotNull
    @Min(1)
    private Integer concretWorkflowId;

    /**
     * 流程版本号
     */
    @TableField("al_ConcretWorkflowVersion")
    @NotBlank
    @Size(max = 100)
    private String concretWorkflowVersion;

    /**
     * 流程实例id
     */
    @TableField("al_WorkflowInstanceID")
    @NotNull
    @Min(1)
    private Integer workflowInstanceId;

    /**
     * 活动id
     */
    @TableField("al_ActivityID")
    @NotNull
    @Min(1)
    private Integer activityId;

    /**
     * 活动实例id
     */
    @TableField("al_ActivityInstanceID")
    @NotNull
    @Min(1)
    private Integer activityInstanceId;

    /**
     * 变迁id
     */
    @TableField("al_TransitionID")
    private Integer transitionId;

    /**
     * 目标活动id
     */
    @TableField("al_TargetActivityID")
    private Integer targetActivityId;

    /**
     * 目标活动实例id
     */
    @TableField("al_TargetActivityInstanceID")
    private Integer targetActivityInstanceId;

    /**
     * 是否提交
     */
    @TableField("al_Direction")
    @NotNull
    private Boolean isSubmit;

    /**
     * 办理人
     */
    @TableField("al_Handler")
    @NotNull
    @Size(max = 20)
    private String auditor;

    /**
     * 办理意见
     */
    @TableField("al_HandlerIdea")
    @Size(max = 500)
    private String auditIdea;

    /**
     * 办理时间
     */
    @TableField("al_HandleTime")
    @NotNull
    private LocalDateTime auditTime;
}

活动实体类

src/main/java/com/weiyu/model/Activity.java

package com.weiyu.model;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.weiyu.enumeration.ActivityRejectMode;
import com.weiyu.enumeration.ActivitySubmitMode;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

/**
 * 活动实体类
 */
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
@TableName("Activity")
public class Activity {
    /**
     * 主键id
     */
    @TableId(value = "act_ID", type = IdType.AUTO)
    private Integer id;

    /**
     * 流程版本id
     */
    @TableField("act_ConcretWorkflowID")
    @NotNull
    @Min(1)
    private Integer concretWorkflowId;

    /**
     * 活动名称
     */
    @TableField("act_Name")
    @NotBlank
    @Size(max = 100)
    private String name;

    /**
     * 是否首活动(启动节点)
     */
    @TableField("act_IsStartNode")
    @NotNull
    private Boolean isStartNode;

    /**
     * 办理意见
     */
    @TableField("act_Idea")
    @Size(max = 500)
    private String idea;

    /**
     * 过滤模式
     */
    @TableField("act_FilterType")
    @NotNull
    @Min(0)
    private Integer filterType;

    /**
     * 过滤内容
     */
    @TableField("act_FilterValue")
    @Size(max = 500)
    private String filterValue;

    /**
     * 提交方式
     */
    @TableField("act_ExecuteType")
    private ActivitySubmitMode submitMode;

    /**
     * 退回方式
     */
    @TableField("act_WithdrawType")
    private ActivityRejectMode rejectMode;
}

活动可编辑字段实体类

src/main/java/com/weiyu/model/ActivityEditableField.java

package com.weiyu.model;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

/**
 * 活动可编辑字段实体类
 */
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
@TableName("EditableFieldsOfActivity")
public class ActivityEditableField {
    /**
     * 主键id
     */
    @TableId(value = "efo_ID", type = IdType.AUTO)
    private Integer id;

    /**
     * 活动id
     */
    @TableField("efo_ActivityID")
    @NotNull
    @Min(1)
    private Integer activityId;

    /**
     * 表名
     */
    @TableField("efo_TableName")
    @NotNull
    @Size(max = 100)
    private String tableName;

    /**
     * 字段名
     */
    @TableField("efo_FieldName")
    @NotNull
    @Size(max = 100)
    private String fieldName;

    /**
     * 字段别名
     */
    @TableField("efo_FieldAlias")
    @Size(max = 100)
    private String fieldAlias;

    /**
     * 不能为空
     */
    @TableField("efo_NotNull")
    private Boolean notNull;
}

DTO

资金分配创建请求对象

src/main/java/com/weiyu/model/CapitalAllocateCreateRequest.java

package com.weiyu.model;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;

/**
 * 资金分配创建请求对象
 */
@Data
@NoArgsConstructor
public class CapitalAllocateCreateRequest {
    /**
     * 指标使用部门
     */
    @NotBlank(message = "指标使用部门不能为空")
    private String deptId;

    /**
     * 指标分配金额
     */
    @NotNull(message = "指标分配金额不能为空")
    private BigDecimal total;

    /**
     * 预算情况
     */
    @NotBlank(message = "预算情况不能为空")
    private String budget;

    /**
     * 支出分类
     */
    @NotBlank(message = "支出分类不能为空")
    private String payType;

    /**
     * 支出方式
     */
    @NotBlank(message = "支出方式不能为空")
    private String payMode;

    /**
     * 协助部门名称
     */
    private String assistDeptName;
}

资金分配明细 DTO

src/main/java/com/weiyu/model/CapitalAllocateDetailDTO.java

package com.weiyu.model;

import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * 资金分配明细 DTO
 */
@Data
@NoArgsConstructor
public class CapitalAllocateDetailDTO {
    /**
     * 主键id
     */
    private Integer id;

    /**
     * 指标使用部门
     */
    private String deptId;

    /**
     * 指标使用部门名称
     */
    private String deptName;

    /**
     * 指标分配金额
     */
    private BigDecimal total;

    /**
     * 预算情况
     */
    private String budget;

    /**
     * 支出分类
     */
    private String payType;

    /**
     * 支出分类
     */
    private String payMode;

    /**
     * 指标分配金额
     */
    private BigDecimal validTotal;

    /**
     * 协助部门名称
     */
    private String assistDeptName;

    /**
     * 回笼金额
     */
    private BigDecimal backTotal;

    /**
     * 回笼日期
     */
    private LocalDateTime backTime;

    /**
     * 使用人
     */
    private String userPerson;

    /**
     * 使用人姓名
     */
    private String userMoniker;
}

资金分配明细更新请求对象

src/main/java/com/weiyu/model/CapitalAllocateDetailUpdateRequest.java

package com.weiyu.model;

import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 资金分配明细更新请求对象
 */
@Data
@NoArgsConstructor
public class CapitalAllocateDetailUpdateRequest {
    /**
     * 主键id
     */
    private Integer id;

    /**
     * 使用人
     */
    private String userPerson;

    /**
     * 使用人姓名
     */
    private String userMoniker;
}

资金分配 DTO

src/main/java/com/weiyu/model/CapitalAllocateDTO.java

package com.weiyu.model;

import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * 资金分配 DTO
 */
@Data
@NoArgsConstructor
public class CapitalAllocateDTO {
    /**
     * 唯一id
     */
    private Integer id;
    /**
     * 创建日期
     */
    private LocalDateTime createTime;
    /**
     * 资金序号
     */
    private String capitalNo;
    /**
     * 资金名称
     */
    private String capitalName;
    /**
     * 资金类别
     */
    private String capitalType;
    /**
     * 资金来源
     */
    private String capitalSource;
    /**
     * 指标预算总额
     */
    private BigDecimal capitalTotal;
    /**
     * 指标可用总额
     */
    private BigDecimal capitalValidTotal;
    /**
     * 状态
     */
    private Integer allocateState;
    /**
     * 活动实例
     */
    private ActivityInstanceRequest activityInstanceRequest;
}

资金分配查询请求对象

src/main/java/com/weiyu/model/CapitalAllocateQueryRequest.java

package com.weiyu.model;

import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

import java.util.List;

/**
 * 资金分配查询请求对象
 *
 * @param capitalNo       资金序号
 * @param capitalName     资金名称
 * @param capitalType     资金类别
 * @param capitalSource   资金来源
 * @param allocateStates  状态
 * @param createDateRange 创建日期范围
 * @param current         当前页码
 * @param size            每页大小
 */
public record CapitalAllocateQueryRequest(
        @Size(max = 20) String capitalNo,
        @Size(max = 100) String capitalName,
        @Size(max = 100) String capitalType,
        @Size(max = 100) String capitalSource,
        List<Integer> allocateStates,
        @Valid DateRange createDateRange, // 启用嵌套验证
        @NotNull
        @Min(value = 1, message = "当前页码不能小于1")
        Integer current, // 当前页码
        @NotNull
        @Min(value = 1, message = "每页大小不能小于1")
        @Max(value = 1000, message = "每页大小不能大于1000")
        Integer size // 每页大小
) {
}

资金分配查询结果 DTO

src/main/java/com/weiyu/model/CapitalAllocateQueryResultDTO.java

package com.weiyu.model;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * 资金分配查询结果 DTO
 */
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class CapitalAllocateQueryResultDTO extends ActivityInstanceRequest {
    /**
     * 唯一id
     */
    private Integer id;
    /**
     * 创建日期
     */
    private LocalDateTime createTime;
    /**
     * 资金序号
     */
    private String capitalNo;
    /**
     * 资金名称
     */
    private String capitalName;
    /**
     * 资金类别
     */
    private String capitalType;
    /**
     * 资金来源
     */
    private String capitalSource;
    /**
     * 指标预算总额
     */
    private BigDecimal capitalTotal;
    /**
     * 指标可用总额
     */
    private BigDecimal capitalValidTotal;
    /**
     * 状态
     */
    private Integer allocateState;
}

活动实例请求对象

src/main/java/com/weiyu/model/ActivityInstanceRequest.java

package com.weiyu.model;

import com.weiyu.enumeration.WorkflowInstanceNode;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 活动实例请求对象
 */
@Data
@NoArgsConstructor
public class ActivityInstanceRequest {
    /**
     * 工作流程id
     */
    Integer workflowId;
    /**
     * 流程版本id
     */
    Integer concretWorkflowId;
    /**
     * 流程实例id
     */
    Integer workflowInstanceId;
    /**
     * 活动实例id
     */
    Integer activityInstanceId;
    /**
     * 当前活动id
     */
    Integer activityId;
    /**
     * 当前活动允许执行人
     */
    String allowAuditor;
    /**
     * 业务主键
     */
    String businessKey;
    /**
     * 流程实例状态
     */
    Integer workflowInstanceState;
    /**
     * 流程实例节点(首活动/启动活动、中间活动、末活动/最后活动)
     */
    WorkflowInstanceNode workflowInstanceNode;
}

活动实例办理人 DTO

src/main/java/com/weiyu/model/ActivityInstanceAuditorDTO.java

package com.weiyu.model;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

/**
 * 活动实例办理人 DTO
 */
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class ActivityInstanceAuditorDTO extends UserDeptDTO {
    /**
     * 活动id
     */
    private Integer activityId;
    /**
     * 活动名称
     */
    private String activityName;
}

活动实例提交选项 DTO

src/main/java/com/weiyu/model/ActivityInstanceSubmitOptionDTO.java

package com.weiyu.model;

import com.weiyu.enumeration.ActivitySubmitMode;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

/**
 * 活动实例提交选项 DTO
 */
@Data
@NoArgsConstructor
public class ActivityInstanceSubmitOptionDTO {
    /**
     * 办理意见选项
     */
    private List<String> auditIdeaOptions;
    /**
     * 目标办理人(角色成员)选项
     */
    private List<RoleMemberDTO> auditMemberOptions;
    /**
     * 提交方式
     */
    private ActivitySubmitMode submitMode;
    /**
     * 目标活动名称 - 提交给的活动名称
     */
    private String targetActivityName;
}

活动实例提交请求对象

src/main/java/com/weiyu/model/ActivityInstanceSubmitRequest.java

package com.weiyu.model;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

/**
 * 活动实例提交请求对象
 */
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class ActivityInstanceSubmitRequest extends ActivityInstanceAudit {
}

活动实例退回选项 DTO

src/main/java/com/weiyu/model/ActivityInstanceRejectOptionDTO.java

package com.weiyu.model;

import com.weiyu.enumeration.ActivityRejectMode;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

/**
 * 活动实例退回选项 DTO
 */
@Data
@NoArgsConstructor
public class ActivityInstanceRejectOptionDTO {
    /**
     * 办理意见选项
     */
    private List<String> auditIdeaOptions;
    /**
     * 目标办理人(角色成员)选项
     */
    private List<ActivityInstanceAuditorDTO> auditMemberOptions;
    /**
     * 目标活动id - 退回到的活动id
     */
    private Integer targetActivityId;
    /**
     * 目标活动名称 - 退回到的活动名称
     */
    private String targetActivityName;
    /**
     * 退回方式
     */
    private ActivityRejectMode rejectMode;
}

活动实例退回请求对象

 src/main/java/com/weiyu/model/ActivityInstanceRejectRequest.java

package com.weiyu.model;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

/**
 * 活动实例退回请求对象
 */
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class ActivityInstanceRejectRequest extends ActivityInstanceAudit {
}

办理信息 DTO

src/main/java/com/weiyu/model/AuditInfoDTO.java

package com.weiyu.model;

import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

/**
 * 办理信息 DTO
 */
@Data
@NoArgsConstructor
public class AuditInfoDTO {
    /**
     * 活动名称/任务名称
     */
    private String activityName;
    /**
     * 办理人姓名
     */
    private String auditorName;
    /**
     * 办理时间
     */
    private LocalDateTime auditTime;
    /**
     * 办理意见
     */
    private String auditIdea;
    /**
     * 办理状态
     */
    private String auditStateDesc;
}

办理信息联合查询结果 DTO

src/main/java/com/weiyu/model/AuditInfoJoinQueryResultDTO.java

package com.weiyu.model;

import java.time.LocalDateTime;

/**
 * 办理信息联合查询结果 DTO
 *
 * @param id           主键id
 * @param activityId   活动id
 * @param activityName 活动名称
 * @param auditor      办理人
 * @param auditorName  办理人姓名
 * @param auditTime    办理时间
 * @param auditIdea    办理意见
 */
public record AuditInfoJoinQueryResultDTO(
        Integer id,
        Integer activityId,
        String activityName,
        String auditor,
        String auditorName,
        LocalDateTime auditTime,
        String auditIdea
) {
}

基础 DTO

活动实例办理

src/main/java/com/weiyu/model/ActivityInstanceAudit.java

package com.weiyu.model;

import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 活动实例办理
 */
@Data
@NoArgsConstructor
public class ActivityInstanceAudit {
    /**
     * 办理意见
     */
    private String auditIdea;
    /**
     * 目标办理人(办理给谁)
     */
    private String targetAuditor;
    /**
     * 活动实例
     */
    @NotNull
    private ActivityInstanceRequest activityInstance;
}

BO

活动实例审批上下文 BO

src/main/java/com/weiyu/model/ActivityInstanceAuditContextBO.java

package com.weiyu.model;

import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

/**
 * 活动实例审批上下文 BO
 */
@Data
@NoArgsConstructor
public class ActivityInstanceAuditContextBO {
    /**
     * 活动实例列表
     */
    private List<ActivityInstance> activityInstances;
    /**
     * 审批信息列表
     */
    private List<AuditInfo> auditInfos;
    /**
     * 审批日志列表
     */
    private List<AuditLog> auditLogs;
}

VO

资金分配响应对象

src/main/java/com/weiyu/model/CapitalAllocateResponse .java

package com.weiyu.model;

import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * 资金分配响应对象
 */
@Data
@NoArgsConstructor
public class CapitalAllocateResponse {
    /**
     * 唯一id
     */
    private Integer id;
    /**
     * 创建日期
     */
    private LocalDateTime createTime;
    /**
     * 资金序号
     */
    private String capitalNo;
    /**
     * 资金名称
     */
    private String capitalName;
    /**
     * 资金类别
     */
    private String capitalType;
    /**
     * 资金来源
     */
    private String capitalSource;
    /**
     * 指标预算总额
     */
    private BigDecimal capitalTotal;
    /**
     * 指标可用总额
     */
    private BigDecimal capitalValidTotal;
    /**
     * 状态
     */
    private Integer allocateState;
    /**
     * 活动实例
     */
    private ActivityInstanceRequest activityInstance;
}

资金分配明细响应对象

src/main/java/com/weiyu/model/CapitalAllocateDetailResponse.java

package com.weiyu.model;

import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * 资金分配明细响应对象
 */
@Data
@NoArgsConstructor
public class CapitalAllocateDetailResponse {
    /**
     * 主键id
     */
    private Integer id;

    /**
     * 指标使用部门
     */
    private String deptId;

    /**
     * 指标使用部门名称
     */
    private String deptName;

    /**
     * 指标分配金额
     */
    private BigDecimal total;

    /**
     * 预算情况
     */
    private String budget;

    /**
     * 支出分类
     */
    private String payType;

    /**
     * 支出分类
     */
    private String payMode;

    /**
     * 指标分配金额
     */
    private BigDecimal validTotal;

    /**
     * 协助部门名称
     */
    private String assistDeptName;

    /**
     * 回笼金额
     */
    private BigDecimal backTotal;

    /**
     * 回笼日期
     */
    private LocalDateTime backTime;

    /**
     * 使用人
     */
    private String userPerson;

    /**
     * 使用人姓名
     */
    private String userMoniker;
}

活动可编辑字段响应对象

src/main/java/com/weiyu/model/ActivityEditableFieldResponse.java

package com.weiyu.model;

import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 活动可编辑字段响应对象
 */
@Data
@NoArgsConstructor
public class ActivityEditableFieldResponse {
    /**
     * 字段名
     */
    private String fieldName;

    /**
     * 不能为空
     */
    private Boolean notNull;
}

活动实例提交选项响应对象

src/main/java/com/weiyu/model/ActivityInstanceSubmitOptionResponse.java

package com.weiyu.model;

import com.weiyu.enumeration.ActivityRejectMode;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

/**
 * 活动实例提交选项响应对象
 */
@Data
@NoArgsConstructor
public class ActivityInstanceSubmitOptionResponse {
    /**
     * 办理意见选项
     */
    private List<String> auditIdeaOptions;
    /**
     * 目标办理人(角色成员)选项
     */
    private List<RoleMemberDTO> auditMemberOptions;
    /**
     * 目标活动id - 退回到的活动id
     */
    private Integer targetActivityId;
    /**
     * 目标活动名称 - 退回到的活动名称
     */
    private String targetActivityName;
    /**
     * 退回方式
     */
    private ActivityRejectMode rejectMode;
}

活动实例退回选项响应对象

src/main/java/com/weiyu/model/ActivityInstanceRejectOptionResponse .java

package com.weiyu.model;

import com.weiyu.enumeration.ActivityRejectMode;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

/**
 * 活动实例退回选项响应对象
 */
@Data
@NoArgsConstructor
public class ActivityInstanceRejectOptionResponse {
    /**
     * 办理意见选项
     */
    private List<String> auditIdeaOptions;
    /**
     * 目标办理人(角色成员)选项
     */
    private List<ActivityInstanceAuditorDTO> auditMemberOptions;
    /**
     * 退回方式
     */
    private ActivityRejectMode rejectMode;
    /**
     * 目标活动名称 - 退回给的活动名称
     */
    private String targetActivityName;
}

办理信息响应对象

src/main/java/com/weiyu/model/AuditInfoResponse.java

package com.weiyu.model;

import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

/**
 * 办理信息响应对象
 */
@Data
@NoArgsConstructor
public class AuditInfoResponse {
    /**
     * 活动名称/流程环节
     */
    private String activityName;
    /**
     * 办理人姓名
     */
    private String auditorName;
    /**
     * 办理时间
     */
    private LocalDateTime auditTime;
    /**
     * 办理意见
     */
    private String auditIdea;
    /**
     * 办理状态
     */
    private String auditStateDesc;
}

枚举

流程实例节点枚举

src/main/java/com/weiyu/enumeration/WorkflowInstanceNode.java

package com.weiyu.enumeration;

import lombok.Getter;

/**
 * 流程实例节点枚举,用于描述当前办理的活动实例处于流程实例中的哪个环节
 * <p>
 * FIRST:首活动/启动活动、MIDDLE:中间活动、LAST:末活动/最后活动
 */
@Getter
public enum WorkflowInstanceNode {
    /**
     * 首活动/启动活动
     */
    FIRST("FIRST"),

    /**
     * 中间活动
     */
    MIDDLE("MIDDLE"),

    /**
     * 末活动/最后活动
     */
    LAST("LAST"),

    /**
     * 异常情况
     */
    UNKNOWN("UNKNOWN");

    private final String value;

    WorkflowInstanceNode(String value) {
        this.value = value;
    }
}

活动提交方式枚举

src/main/java/com/weiyu/enumeration/ActivitySubmitMode.java

package com.weiyu.enumeration;

import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.Getter;

/**
 * 活动提交方式枚举
 */
@Getter
public enum ActivitySubmitMode {
    /**
     * 提交给指定人员
     */
    PERSONNEL(0, "提交给指定人员"),

    /**
     * 提交给角色或人
     */
    ROLE(1, "提交给角色或人");

    @EnumValue // MyBatis-Plus 数据库映射,标记使用该字段的值存入数据库,保存到数据库就是0、1、2等整数
    @JsonValue // Jackson JSON 序列化,标记使用该字段的值进行序列化,前端接收到的就是0、1、2等数字
    private final Integer value;

    //@JsonValue // Jackson JSON 序列化,标记使用该字段的值进行序列化,前端接收到的就是提交给指定人员、提交给角色或人等字符串
    private final String displayName;

    ActivitySubmitMode(Integer value, String displayName) {
        this.value = value;
        this.displayName = displayName;
    }
}

活动退回方式枚举

src/main/java/com/weiyu/enumeration/ActivityRejectMode.java

package com.weiyu.enumeration;

import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.Getter;

/**
 * 活动退回方式枚举
 */
@Getter
public enum ActivityRejectMode {
    /**
     * 退回到前一步
     */
    PREVIOUS(0, "退回到前一步"),

    /**
     * 退回到第一步
     */
    FIRST(1, "退回到第一步");

    @EnumValue // MyBatis-Plus 数据库映射,标记使用该字段的值存入数据库,保存到数据库就是0、1、2等整数
    @JsonValue // Jackson JSON 序列化,标记使用该字段的值进行序列化,前端接收到的就是0、1、2等数字
    private final Integer value;

    //@JsonValue // Jackson JSON 序列化,标记使用该字段的值进行序列化,前端接收到的就是退回到前一步、退回到第一步等字符串
    private final String displayName;

    ActivityRejectMode(Integer value, String displayName) {
        this.value = value;
        this.displayName = displayName;
    }
}

常量

流程实例状态常量类

src/main/java/com/weiyu/constant/WorkflowInstanceStateConstants.java

package com.weiyu.constant;

/**
 * 流程实例状态常量类
 */
@SuppressWarnings("all")
public class WorkflowInstanceStateConstants {

    private WorkflowInstanceStateConstants() {
        // 工具类,防止实例化
    }

    /**
     * 办理中
     */
    public static  final int AUDITING = 0;

    /**
     * 已结案
     */
    public static final int CLOSED = 1;
}

活动实例状态常量类

src/main/java/com/weiyu/constant/ActivityInstanceStateConstants.java

package com.weiyu.constant;

/**
 * 活动实例状态常量类
 */
@SuppressWarnings("all")
public class ActivityInstanceStateConstants {

    private ActivityInstanceStateConstants() {
        // 工具类,防止实例化
    }

    /**
     * 未提交(活动实例未开始)
     */
    public static final int UNSUBMITTED = 0;

    /**
     * 办理中/待办理(活动实例正在处理中)
     */
    public static  final int PENDING = 1;

    /**
     * 已办理(活动实例已办结)
     */
    public static final int AUDITED = 2;
}

活动过虑模式常量类

src/main/java/com/weiyu/constant/ActivityFilterTypeConstants.java

package com.weiyu.constant;

/**
 * 活动过虑模式常量类
 */
public class ActivityFilterTypeConstants {

    private ActivityFilterTypeConstants() {
        // 工具类,防止实例化
    }

    /**
     * 无过虑
     */
    public static  final int NO_FILTER = 0;

    /**
     * 部门
     */
    public static final int DEPT = 1;

    /**
     * 活动
     */
    public static final int ACTIVITY = 2;

    /**
     * 脚本
     */
    public static final int SCRIPT = 3;
}

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐