Flowable7.x学习笔记(六)Vue3+SpringBoot3实现流程定义保存功能

一、前言

        之前的文章已经把基础的静态界面已经搭建好了,那么现在需要绘制一个可以编辑流程定义的表单,我的脚手架工程使用了element-plus组件库,按需导入使用【el-drawer】抽屉这个组件放表单,然后传递到后端保存。

二、后端:自动生成基础代码

        这一步不需要手动创建基础代码,直接使用mybatis-plus插件即可,这类插件有很多,免费的收费都有,这里因为是写公开文章的原因,选择使用【mybatisplus】这个免费插件,接下来我演示一下使用方法,大家有别的用的顺手的插件可以跳过这个步骤。

2.1 工具在线安装方式

        在 IDEA 的 【文件】-【设置】-【插件】中直接搜索【mybatisplus】安装,安装之后可能需要重启IDEA。

2.2 配置数据源

        在 IDEA 的 【工具】-【Config DataBase】中输入mysql链接信息,工具提供了测试链接功能,测试完整后确认即可。

2.3 使用工具

        在 IDEA 的 【工具】-【Config Generator】的工具界面选中需要生成基础代码的表格,输入模块路径和包路径,指定文件夹名称【实体】/【Mapper】/【Service】/【ServiceImpl】等信息,具体可参照我的设置以及实际生成的代码,设置完成之后需要点击【check field】按钮确认目标表格中需要生成的具体字段,确认无误后点击生成按钮即可。

        额外解释一下,选择分配ID的作用是指定ID生成规则为雪花ID,微服务系统不建议使用自增ID,雪花ID更具有可靠性。

2.4 修改部分生成的代码

        这个工具生成的代码实际还需要做一些优化,接下来我依次操作

① 补充实体类中依赖

比如:TableField依赖

② Serial注解修饰序列化

serialVersionUID 属性

③ 补充填充字段

        我在mybatis-plus配置中自定义了一些自动填充字段,所以在这些字段中需要手动补充一下TableField注解

        我的配置文件如下:

package com.ceair.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.ceair.entity.model.UserInfo;
import com.ceair.util.UserInfoUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import java.time.LocalDateTime;

/**
 * @author wangbaohai
 * @ClassName MybatisPlusConfig
 * @description: MybatisPlus分页插件
 * @date 2024年11月19日
 * @version: 1.0.0
 */
@Configuration
@EnableTransactionManagement //开启事务
@RequiredArgsConstructor
@Slf4j
public class MybatisPlusConfig {

    private final UserInfoUtils userInfoUtils;

    /**
     * 新的分页插件,一缓和二缓遵循mybatis的规则,
     * 需要设置 MybatisConfiguration#useDeprecatedExecutor = false
     * 避免缓存出现问题(该属性会在旧插件移除后一同移除)
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }

    /**
     * 创建一个MetaObjectHandler的Bean,用于处理实体对象的自动填充逻辑。
     * 该处理器在插入和更新操作时,会自动填充一些公共字段,如创建者、修改者、创建时间、修改时间等。
     *
     * @return MetaObjectHandler 返回一个实现了MetaObjectHandler接口的匿名类实例,用于处理自动填充逻辑。
     */
    @Bean
    public MetaObjectHandler metaObjectHandler() {
        return new MetaObjectHandler() {
            /**
             * 在插入操作时自动填充字段。
             * 该方法会从认证信息中获取当前用户信息,并填充以下字段:
             * - deleted: 设置为0,表示未删除
             * - creatorId: 设置为当前用户的ID
             * - creatorName: 设置为当前用户的账号
             * - createTime: 设置为当前时间
             * - modifierId: 设置为当前用户的ID
             * - modifierName: 设置为当前用户的账号
             * - modifyTime: 设置为当前时间
             *
             * @param metaObject 元对象,表示待填充的实体对象
             */
            @Override
            public void insertFill(MetaObject metaObject) {
                UserInfo userInfo = getUserInfoSafely();

                // 填充创建者和修改者字段
                fillCreatorFields(metaObject, userInfo);
                fillModifierFields(metaObject, userInfo);
            }

            /**
             * 在更新操作时自动填充字段。
             * 该方法会从认证信息中获取当前用户信息,并填充以下字段:
             * - modifierId: 设置为当前用户的ID
             * - modifierName: 设置为当前用户的账号
             * - modifyTime: 设置为当前时间
             *
             * @param metaObject 元对象,表示待填充的实体对象
             */
            @Override
            public void updateFill(MetaObject metaObject) {
                UserInfo userInfo = getUserInfoSafely();

                // 填充修改者字段
                fillModifierFields(metaObject, userInfo);
            }

            /**
             * 安全地获取用户信息,避免空指针异常。
             *
             * @return UserInfo 当前用户信息,如果为空则构建一个系统用户。
             */
            private UserInfo getUserInfoSafely() {
                UserInfo userInfo = userInfoUtils.getUserInfoFromAuthentication();
                if (userInfo == null) {
                    log.warn("无法获取用户信息,使用系统用户进行自动填充操作!");
                    userInfo = new UserInfo();
                    userInfo.setId(0L);
                    userInfo.setAccount("系统自动操作");
                }
                return userInfo;
            }

            /**
             * 填充创建者相关字段。
             *
             * @param metaObject 元对象
             * @param userInfo   用户信息
             */
            private void fillCreatorFields(MetaObject metaObject, UserInfo userInfo) {
                this.strictInsertFill(metaObject, "recordVersion", () -> 0, Integer.class);
                this.strictInsertFill(metaObject, "deleted", () -> false, Boolean.class);
                this.strictInsertFill(metaObject, "sort", () -> 0, Integer.class);
                this.strictInsertFill(metaObject, "creatorId", userInfo::getId, Long.class);
                this.strictInsertFill(metaObject, "creatorName", userInfo::getAccount, String.class);
                this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class);
            }

            /**
             * 填充修改者相关字段。
             *
             * @param metaObject 元对象
             * @param userInfo   用户信息
             */
            private void fillModifierFields(MetaObject metaObject, UserInfo userInfo) {
                this.strictUpdateFill(metaObject, "modifierId", userInfo::getId, Long.class);
                this.strictUpdateFill(metaObject, "modifierName", userInfo::getAccount, String.class);
                this.strictUpdateFill(metaObject, "modifyTime", LocalDateTime::now, LocalDateTime.class);
            }
        };
    }
}

2.5 生成代码展示

package com.ceair.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * <p>
 * 流程定义表
 * </p>
 *
 * @author wangbaohai
 * @since 2025-04-12
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("process_definition")
public class ProcessDefinition implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;

    /**
     * 流程定义表主键ID
     */
    @TableId(value = "id", type = IdType.ASSIGN_ID)
    private Long id;

    /**
     * 流程唯一标识(业务中使用的 key)
     */
    @TableField("process_key")
    private String processKey;

    /**
     * 流程名称
     */
    @TableField("process_name")
    private String processName;

    /**
     * 流程版本号
     */
    @TableField("process_version")
    private Integer processVersion;

    /**
     * BPMN XML存放在MongoDB中的ID
     */
    @TableField("xml_mongo_id")
    private String xmlMongoId;

    /**
     * 状态:0-草稿, 1-发布, 2-停用
     */
    @TableField("process_status")
    private Integer processStatus;

    /**
     * 流程描述/说明
     */
    @TableField("description")
    private String description;

    /**
     * 0:启用,1:删除
     */
    @TableField(value = "deleted", fill = FieldFill.INSERT)
    private Boolean deleted;

    /**
     * 创建人id
     */
    @TableField(value = "creator_id", fill = FieldFill.INSERT)
    private Long creatorId;

    /**
     * 创建人名称
     */
    @TableField(value = "creator_name", fill = FieldFill.INSERT)
    private String creatorName;

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

    /**
     * 修改人id
     */
    @TableField(value = "modifier_id", fill = FieldFill.INSERT_UPDATE)
    private Long modifierId;

    /**
     * 修改人名称
     */
    @TableField(value = "modifier_name", fill = FieldFill.INSERT_UPDATE)
    private String modifierName;

    /**
     * 修改时间
     */
    @TableField(value = "modify_time", fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime modifyTime;

    /**
     * 版本号
     */
    @TableField(value = "record_version", fill = FieldFill.INSERT)
    private Integer recordVersion;

    /**
     * 扩展字段1
     */
    @TableField("attribute1")
    private String attribute1;

    /**
     * 扩展字段2
     */
    @TableField("attribute2")
    private String attribute2;

    /**
     * 扩展字段3
     */
    @TableField("attribute3")
    private String attribute3;

    /**
     * 扩展字段4
     */
    @TableField("attribute4")
    private String attribute4;

    /**
     * 扩展字段5
     */
    @TableField("attribute5")
    private String attribute5;


}
package com.ceair.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ceair.entity.ProcessDefinition;
import org.apache.ibatis.annotations.Mapper;

/**
 * <p>
 * 流程定义表 Mapper 接口
 * </p>
 *
 * @author wangbaohai
 * @since 2025-04-12
 */
@Mapper
public interface ProcessDefinitionMapper extends BaseMapper<ProcessDefinition> {

}
package com.ceair.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.ceair.entity.ProcessDefinition;

/**
 * <p>
 * 流程定义表 服务类
 * </p>
 *
 * @author wangbaohai
 * @since 2025-04-12
 */
public interface IProcessDefinitionService extends IService<ProcessDefinition> {

}
package com.ceair.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ceair.entity.ProcessDefinition;
import com.ceair.mapper.ProcessDefinitionMapper;
import com.ceair.service.IProcessDefinitionService;
import org.springframework.stereotype.Service;

/**
 * <p>
 * 流程定义表 服务实现类
 * </p>
 *
 * @author wangbaohai
 * @since 2025-04-12
 */
@Service
public class ProcessDefinitionServiceImpl extends ServiceImpl<ProcessDefinitionMapper, ProcessDefinition> implements IProcessDefinitionService {

}
<?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.ceair.mapper.ProcessDefinitionMapper">

    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="com.ceair.entity.ProcessDefinition">
        <id column="id" property="id"/>
        <result column="process_key" property="processKey"/>
        <result column="process_name" property="processName"/>
        <result column="process_version" property="processVersion"/>
        <result column="xml_mongo_id" property="xmlMongoId"/>
        <result column="process_status" property="processStatus"/>
        <result column="description" property="description"/>
        <result column="deleted" property="deleted"/>
        <result column="creator_id" property="creatorId"/>
        <result column="creator_name" property="creatorName"/>
        <result column="create_time" property="createTime"/>
        <result column="modifier_id" property="modifierId"/>
        <result column="modifier_name" property="modifierName"/>
        <result column="modify_time" property="modifyTime"/>
        <result column="record_version" property="recordVersion"/>
        <result column="attribute1" property="attribute1"/>
        <result column="attribute2" property="attribute2"/>
        <result column="attribute3" property="attribute3"/>
        <result column="attribute4" property="attribute4"/>
        <result column="attribute5" property="attribute5"/>
    </resultMap>

</mapper>

三、后端:创建实体对应的VO

        需要注意的是,ID属性因为是雪花ID,长度会超过VUE前端能接收的最大范围而失去精度,所以需要将字段转成字符串给前端,方案也很简单就是用 @JsonSerialize(using = ToStringSerializer.class) 注解修饰ID字段,另外所有日期字段需要使用 @DateTimeFormat(pattern = "yyyy-MM-dd") 和 @JsonFormat(pattern = "yyyy-MM-dd",timezone = "GMT+8") 注解修改,解决前后端的日期格式统一问题

具体代码如下:

package com.ceair.entity.vo;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;

import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * @author wangbaohai
 * @ClassName ProcessDefinitionVO
 * @description: 流程定义表VO
 * @date 2025年04月12日
 * @version: 1.0.0
 */
@Data
public class ProcessDefinitionVO implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;

    /**
     * 流程定义表主键ID
     */
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;

    /**
     * 流程唯一标识(业务中使用的 key)
     */
    private String processKey;

    /**
     * 流程名称
     */
    private String processName;

    /**
     * 流程版本号
     */
    private Integer processVersion;

    /**
     * BPMN XML存放在MongoDB中的ID
     */
    private String xmlMongoId;

    /**
     * 状态:0-草稿, 1-发布, 2-停用
     */
    private Integer processStatus;

    /**
     * 流程描述/说明
     */
    private String description;

    /**
     * 0:启用,1:删除
     */
    private Boolean deleted;

    /**
     * 创建人id
     */
    private Long creatorId;

    /**
     * 创建人名称
     */
    private String creatorName;

    /**
     * 创建时间
     */
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @JsonFormat(pattern = "yyyy-MM-dd",timezone = "GMT+8")
    private LocalDateTime createTime;

    /**
     * 修改人id
     */
    private Long modifierId;

    /**
     * 修改人名称
     */
    private String modifierName;

    /**
     * 修改时间
     */
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @JsonFormat(pattern = "yyyy-MM-dd",timezone = "GMT+8")
    private LocalDateTime modifyTime;

    /**
     * 版本号
     */
    private Integer recordVersion;

    /**
     * 扩展字段1
     */
    private String attribute1;

    /**
     * 扩展字段2
     */
    private String attribute2;

    /**
     * 扩展字段3
     */
    private String attribute3;

    /**
     * 扩展字段4
     */
    private String attribute4;

    /**
     * 扩展字段5
     */
    private String attribute5;

}

四、后端:创建保存接口参数

package com.ceair.entity.request;

import lombok.Data;

import java.io.Serial;
import java.io.Serializable;

/**
 * @author wangbaohai
 * @ClassName ProcessDefinitionInOrUpReq
 * @description: 新增或更新流程定义请求对象
 * @date 2025年04月12日
 * @version: 1.0.0
 */
@Data
public class ProcessDefinitionInOrUpReq implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;

    /**
     * 流程定义表主键ID
     */
    private Long id;

    /**
     * 流程唯一标识(业务中使用的 key)
     */
    private String processKey;

    /**
     * 流程名称
     */
    private String processName;

    /**
     * 流程版本号
     */
    private Integer processVersion;

    /**
     * 状态:0-草稿, 1-发布, 2-停用
     */
    private Integer processStatus;

    /**
     * 流程描述/说明
     */
    private String description;

    /**
     * 0:启用,1:删除
     */
    private Boolean deleted;

}

五、后端:创建实体转换工具

        通常我们需要将实体类转换成VO传递给前端,这有数据安全的考量也有编码规范的要求,但是我们一般又懒得手动映射,所以就用【mapstruct】工具转换,接下来我直接演示使用方法。

① 引入依赖,注意这个依赖一定要放在 lombok的依赖后面,不然会转换失败,这是工具的一个BUG,目前只有这种解决方案。

<!--mapstruct-->
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
</dependency>

② 创建转换工具 ProcessDefinitionStructMapper

package com.ceair.utils;

import com.ceair.entity.ProcessDefinition;
import com.ceair.entity.vo.ProcessDefinitionVO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

/**
 * @author wangbaohai
 * @ClassName ProcessDefinitionStructMapper
 * @description: 流程定义结构映射器
 * 提供 ProcessDefinition 和 ProcessDefinitionVO 之间的双向映射功能。
 * 注意事项:
 * 1. 输入参数不允许为 null,否则会抛出 IllegalArgumentException。
 * 2. 如果字段包含复杂对象(如集合或嵌套对象),请确保映射规则已正确定义。
 * @date 2025年04月12日
 * @version: 1.0.0
 */
@Mapper
public interface ProcessDefinitionStructMapper {

    ProcessDefinitionStructMapper INSTANCE = Mappers.getMapper(ProcessDefinitionStructMapper.class);

    /**
     * 将 ProcessDefinition 转换为 ProcessDefinitionVO。
     *
     * @param processDefinition 输入的 ProcessDefinition 对象,不能为空。
     * @return 转换后的 ProcessDefinitionVO 对象。
     * @throws IllegalArgumentException 如果输入参数为 null。
     */
    default ProcessDefinitionVO toVO(ProcessDefinition processDefinition) {
        if (processDefinition == null) {
            throw new IllegalArgumentException("Input parameter 'processDefinition' cannot be null.");
        }
        return mapToVO(processDefinition);
    }

    /**
     * 映射逻辑的具体实现。
     *
     * @param processDefinition 输入的 ProcessDefinition 对象。
     * @return 转换后的 ProcessDefinitionVO 对象。
     */
//    @Mapping(target = "complexField", source = "complexField") // 示例:显式定义复杂字段映射规则
    ProcessDefinitionVO mapToVO(ProcessDefinition processDefinition);

    /**
     * 将 ProcessDefinitionVO 转换为 ProcessDefinition。
     *
     * @param processDefinitionVO 输入的 ProcessDefinitionVO 对象,不能为空。
     * @return 转换后的 ProcessDefinition 对象。
     * @throws IllegalArgumentException 如果输入参数为 null。
     */
    default ProcessDefinition toEntity(ProcessDefinitionVO processDefinitionVO) {
        if (processDefinitionVO == null) {
            throw new IllegalArgumentException("Input parameter 'processDefinitionVO' cannot be null.");
        }
        return mapToEntity(processDefinitionVO);
    }

    /**
     * 映射逻辑的具体实现。
     *
     * @param processDefinitionVO 输入的 ProcessDefinitionVO 对象。
     * @return 转换后的 ProcessDefinition 对象。
     */
//    @Mapping(target = "complexField", source = "complexField") // 示例:显式定义复杂字段映射规则
    ProcessDefinition mapToEntity(ProcessDefinitionVO processDefinitionVO);
}

六、后端:创建 save 接口

IProcessDefinitionService中创建 save 方法

/**
 * 保存或更新流程定义信息。
 *
 * @param processDefinitionInOrUpReq 包含流程定义的输入或更新请求对象。
 *                                   该对象应包含必要的流程定义信息,例如名称、描述、版本等。
 *                                   如果为 null,则可能会导致保存操作失败。
 * @return 返回保存后的流程定义视图对象 (ProcessDefinitionVO)。
 *         该对象包含了保存后的流程定义详细信息,例如唯一标识符、状态等。
 *         如果保存失败,则可能返回 null 或抛出异常,具体行为取决于实现逻辑。
 */
ProcessDefinitionVO save(ProcessDefinitionInOrUpReq processDefinitionInOrUpReq);

七、后端:实现 save 方法

ProcessDefinitionServiceImpl中现实 save 方法

/**
 * 保存或更新流程定义信息。
 *
 * @param processDefinitionInOrUpReq 流程定义的输入参数对象,包含流程键(processKey)、流程版本(processVersion)等信息。
 *                                   该参数不能为空,且关键字段(如 processKey 和 processVersion)也不能为空。
 * @return 返回保存或更新后的流程定义视图对象(ProcessDefinitionVO)。
 * @throws IllegalArgumentException 如果输入参数为空或关键字段缺失,则抛出此异常。
 * @throws BusinessException 如果存在重复的流程定义(唯一键冲突)或其他业务逻辑错误,则抛出此异常。
 * @throws Exception 如果在保存或更新过程中发生系统级错误,则抛出此异常。
 */
@Override
public ProcessDefinitionVO save(ProcessDefinitionInOrUpReq processDefinitionInOrUpReq) {
    try {
        // 参数校验:确保输入参数不为空且关键字段完整
        if (processDefinitionInOrUpReq == null || StringUtils.isEmpty(processDefinitionInOrUpReq.getProcessKey()) || Objects.isNull(processDefinitionInOrUpReq.getProcessVersion())) {
            log.error("ProcessDefinitionInOrUpReq 参数为空或关键字段缺失, 输入参数: {}", processDefinitionInOrUpReq);
            throw new IllegalArgumentException("ProcessDefinitionInOrUpReq 参数或者关键字段不能为空");
        }

        // 初始化 ProcessDefinition 对象,用于存储流程定义信息
        ProcessDefinition processDefinition = new ProcessDefinition();

        // 将输入参数中的非空数据赋值给 ProcessDefinition 对象,避免空值覆盖已有数据
        BeanUtil.copyProperties(processDefinitionInOrUpReq, processDefinition,
                CopyOptions.create().ignoreNullValue());

        // 如果 id 为空,说明是新增流程操作,需要进行唯一索引校验(processKey + processVersion)
        if (processDefinition.getId() == null) {
            // 根据 processKey 和 processVersion 查询是否存在相同记录
            List<ProcessDefinition> existProcessDefinitions = lambdaQuery()
                    .eq(ProcessDefinition::getProcessKey, processDefinition.getProcessKey())
                    .eq(ProcessDefinition::getProcessVersion, processDefinition.getProcessVersion()).list();

            // 如果存在相同记录,抛出业务异常
            if (!existProcessDefinitions.isEmpty()) {
                log.error("存在相同流程定义,processKey:{}, processVersion:{}", processDefinition.getProcessKey(),
                        processDefinition.getProcessVersion());
                throw new BusinessException(ResultCode.FAILED.getCode(),
                        "存在相同流程定义,重复唯一键是:" + processDefinition.getProcessKey() + "-" + processDefinition.getProcessVersion());
            }
        }

        // 保存或更新流程定义信息
        saveOrUpdate(processDefinition);

        // 将保存后的 ProcessDefinition 对象转换为视图对象并返回
        return ProcessDefinitionStructMapper.INSTANCE.toVO(processDefinition);
    } catch (IllegalArgumentException e) {
        // 参数校验异常处理
        log.error("参数校验失败, 输入参数: {}, 错误信息: {}", processDefinitionInOrUpReq, e.getMessage(), e);
        throw e;
    } catch (BusinessException e) {
        // 业务异常处理
        log.error("业务异常, 输入参数: {}, 错误信息: {}", processDefinitionInOrUpReq, e.getMessage(), e);
        throw e;
    } catch (Exception e) {
        // 系统异常处理
        log.error("保存或更新流程定义时发生系统错误, 输入参数: {}", processDefinitionInOrUpReq, e);
        throw new BusinessException("保存或更新流程定义失败", e);
    }
    }

八、后端:创建接口

package com.ceair.controller;

import com.ceair.entity.request.ProcessDefinitionInOrUpReq;
import com.ceair.entity.result.Result;
import com.ceair.entity.vo.ProcessDefinitionVO;
import com.ceair.service.IProcessDefinitionService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author wangbaohai
 * @ClassName ProcessDefinitionController
 * @description: 流程定义管理相关接口
 * @date 2025年04月12日
 * @version: 1.0.0
 */
@RestController
@RequestMapping("/api/v1/definition")
@RequiredArgsConstructor
@Slf4j
@Tag(name = "流程定义管理", description = "流程定义管理相关接口")
public class ProcessDefinitionController {

    private final IProcessDefinitionService processDefinitionService;

    /**
     * 新增或更新流程定义。
     * <p>
     * 该方法通过调用服务层的保存逻辑,处理流程定义的新增或更新操作。
     * 如果操作成功,则返回封装的成功响应;如果发生异常,则记录错误日志并返回错误响应。
     *
     * @param req 流程定义新增或更新请求对象,包含必要的流程定义信息。
     *            该参数为必需项,不能为空。
     * @return 返回一个Result对象,包含操作结果:
     * - 如果操作成功,返回封装的ProcessDefinitionVO对象;
     * - 如果操作失败,返回错误信息及原因。
     */
    @PreAuthorize("hasAnyAuthority('/api/v1/definition/save')") // 限制访问权限,仅允许拥有'/api/v1/definition/save'权限的用户调用该方法
    @Parameter(name = "req", description = "流程定义新增或更新请求对象", required = true) // 定义接口参数的描述信息,用于生成API文档
    @Operation(summary = "新增或更新流程定义") // 定义接口的摘要信息,用于生成API文档
    @PostMapping("/save") // 映射HTTP POST请求到该方法,URL路径为"/api/v1/definition/save"
    public Result<ProcessDefinitionVO> save(@RequestBody ProcessDefinitionInOrUpReq req) {
        // 初始化流程定义视图对象
        ProcessDefinitionVO processDefinitionVO;

        // 调用服务层方法保存流程定义,并将结果封装为成功响应返回
        try {
            processDefinitionVO = processDefinitionService.save(req);
        } catch (IllegalArgumentException e) {
            // 捕获参数校验或业务逻辑异常,记录错误日志并返回错误响应
            log.error("新增或更新流程定义失败,参数异常: {}", e.getMessage(), e);
            return Result.error("新增或更新流程定义失败,原因:" + e.getMessage());
        } catch (Exception e) {
            // 捕获其他异常,记录错误日志并返回错误响应
            log.error("新增或更新流程定义失败 具体原因为 : {}", e.getMessage());
            return Result.error("新增或更新流程定义失败,失败原因:" + e.getMessage());
        }

        // 返回操作成功的响应结果
        return Result.success(processDefinitionVO);
    }


}

九、测试新接口

使用工程中已集成的SpringDoc

跳出授权码登录界面,使用admin账号登录,账号的角色是超级管理员

登录之后成功获取认证权限

模拟请求

发现没有权限,是因为没有配置权限,@PreAuthorize("hasAnyAuthority('/api/v1/definition/save')")生效拦截了,那么现在创建按钮权限,并且赋予给当前admin用户的超级管理员

分配好权限之后,再次认证调用接口发现ok了

最后我们再到数据库中确认一下数据是否正确入库,发现也是ok的,后端接口部分就完成了。

十、前端:包装axios

主要是创建请求拦截器和响应拦截器,https.ts 请求拦截器主要是在请求头增加认证信息,用于鉴权的;响应拦截器是收集非业务异常的。

代码如下:

import type {
  AxiosInstance,
  AxiosRequestConfig,
  AxiosResponse,
  InternalAxiosRequestConfig,
} from 'axios'
// 引入路由模块,用于页面跳转
import router from '@/router/index'
// 引入用户状态管理模块,用于获取和管理用户信息
import useUserStore from '@/store/modules/user'
// 引入axios库,用于发起HTTP请求
import axios from 'axios'
// 引入Element Plus的ElMessage组件,用于显示全局提示信息
import { ElMessage } from 'element-plus'

/**
 * 定义Token接口,用于描述OAuth2认证中返回的Token结构。
 */
interface Token {
  token_type: string // 认证类型,例如Bearer
  access_token: string // 访问令牌
}

/**
 * HTTP状态码与错误信息的映射表,用于根据HTTP状态码快速获取对应的错误提示信息。
 */
const HTTP_ERROR_MESSAGES: Record<number, string> = {
  400: '请求参数错误(400)', // 客户端请求参数有误
  401: '未授权,请重新登录(401)', // 用户未授权或Token失效
  403: '拒绝访问(403)', // 服务器拒绝访问
  404: '请求路径出错(404)', // 请求资源不存在
  408: '请求超时(408)', // 请求超时
  500: '服务器错误(500)', // 服务器内部错误
  501: '服务未实现(501)', // 服务器不支持该请求方法
  502: '网络错误(502)', // 网关错误
  503: '服务不可用(503)', // 服务暂时不可用
  504: '网络超时(504)', // 网关超时
  505: 'HTTP版本不受支持(505)', // 服务器不支持请求的HTTP版本
}

/**
 * 错误提示函数,用于统一显示错误信息。
 * @param message - 错误信息内容,通常为HTTP状态码对应的错误描述。
 */
function showError(message: string) {
  ElMessage({
    showClose: true, // 显示关闭按钮
    message: `${message},请检查网络或联系管理员!`, // 提示信息内容
    type: 'error', // 提示类型为错误
  })
}

/**
 * Request类,封装了基于axios的HTTP请求功能,支持自定义配置、拦截器以及错误处理。
 */
export class Request {
  // axios 实例,用于发起HTTP请求
  instance: AxiosInstance

  /**
   * 构造函数,用于初始化axios实例并设置请求和响应拦截器。
   * @param config - Axios请求配置对象,包含基础URL、超时时间等配置项。
   */
  constructor(config: AxiosRequestConfig) {
    // 使用axios.create创建一个带有默认配置的axios实例
    this.instance = axios.create(config)
    // 设置请求和响应拦截器
    this.setupInterceptors()
  }

  /**
   * 统一设置请求和响应拦截器。
   * 请求拦截器用于在请求发送前添加认证信息;
   * 响应拦截器用于处理响应数据和错误。
   */
  private setupInterceptors() {
    // 添加请求拦截器
    this.instance.interceptors.request.use(this.handleRequest, this.handleError)
    // 添加响应拦截器
    this.instance.interceptors.response.use(this.handleResponse, this.handleError)
  }

  /**
   * 请求拦截器逻辑,用于在请求发送前添加认证信息。
   * 如果存在用户Token且请求路径不是/oauth2/token,则将Token添加到请求头中。
   * @param config - 内部Axios请求配置对象,包含请求的URL、方法、头信息等。
   * @returns 修改后的Axios请求配置对象。
   */
  private handleRequest = (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
    const userStore = useUserStore() // 获取用户状态管理实例
    const token = userStore.token // 获取用户的认证Token

    // 校验Token和请求路径,如果满足条件,则将Token添加到请求头中
    if (token && config.url !== '/oauth2/token') {
      const { token_type, access_token } = JSON.parse(token) as Token // 解析Token
      config.headers!.Authorization = `${token_type} ${access_token}` // 将Token添加到请求头
    }

    return config
  }

  /**
   * 响应拦截器逻辑,用于提取响应数据。
   * 默认情况下,返回响应中的data字段。
   * @param res - Axios响应对象,包含服务器返回的完整响应信息。
   * @returns 响应数据,即res.data。
   */
  private handleResponse = (res: AxiosResponse): any => res.data

  /**
   * 错误处理逻辑,用于统一处理请求和响应中的错误。
   * 根据错误类型和HTTP状态码,显示对应的错误提示信息,并执行特定操作。
   * @param err - 错误对象,包含错误信息和响应数据。
   * @returns 返回被拒绝的Promise,包含错误响应。
   */
  private handleError = (err: any): Promise<any> => {
    // 如果是网络错误,直接返回被拒绝的Promise
    if (err.code === 'ERR_NETWORK') {
      return Promise.reject(err)
    }

    const status = err.response?.status // 获取HTTP状态码
    const messageText = HTTP_ERROR_MESSAGES[status] || `连接出错(${status})!` // 获取对应的错误提示信息

    // 如果状态码为401(未授权),重置用户状态并跳转到OAuth2重定向页面
    if (status === 401) {
      useUserStore().$reset() // 重置用户状态
      router.push({ path: '/OAuth2Redirect' }) // 跳转到OAuth2重定向页面
    }

    // 设置错误信息并显示提示
    err.response.statusText = messageText
    showError(messageText)

    return Promise.reject(err.response) // 返回被拒绝的Promise
  }

  /**
   * 发起通用HTTP请求。
   * @param config - Axios请求配置对象,包含请求的URL、方法、参数等。
   * @returns 包含响应数据的Promise。
   */
  request<T = any>(config: AxiosRequestConfig): Promise<AxiosResponse<T>> {
    return this.instance.request<T>(config)
  }

  /**
   * 发起GET请求。
   * @param config - Axios请求配置对象,包含请求的URL、参数等。
   * @returns 包含响应数据的Promise。
   */
  get<T = any>(config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
    return this.request<T>({ ...config, method: 'GET' })
  }

  /**
   * 发起POST请求。
   * @param config - Axios请求配置对象,包含请求的URL、参数等。
   * @returns 包含响应数据的Promise。
   */
  post<T = any>(config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
    return this.request<T>({ ...config, method: 'POST' })
  }

  /**
   * 发起DELETE请求。
   * @param config - Axios请求配置对象,包含请求的URL、参数等。
   * @returns 包含响应数据的Promise。
   */
  delete<T = any>(config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
    return this.request<T>({ ...config, method: 'DELETE' })
  }

  /**
   * 发起PUT请求。
   * @param config - Axios请求配置对象,包含请求的URL、参数等。
   * @returns 包含响应数据的Promise。
   */
  put<T = any>(config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
    return this.request<T>({ ...config, method: 'PUT' })
  }

  /**
   * 发起PATCH请求。
   * @param config - Axios请求配置对象,包含请求的URL、参数等。
   * @returns 包含响应数据的Promise。
   */
  patch<T = any>(config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
    return this.request<T>({ ...config, method: 'PATCH' })
  }
}

// 默认导出Request类的实例
export default Request

十一、自定义请求头

        request.ts 主要定义请求地址,超时时间等需要自定义的请求头

代码如下:

import Request from '@/utils/http/https'

const request = new Request({
  // 统一网关接口地址,由网关管理路由(实际生产环境是需要配置Nginx或者其他负载均衡器,不可以直接访问网关服务)
  baseURL: import.meta.env.VITE_GATEWAY_API,
  timeout: 60 * 1000,
  withCredentials: true,
})

export default request

十二、定义请求数据类型

(TS语法规范)

代码如下:

export interface ProcessDefinitionInOrUpReq {
  id: number // 流程定义表主键ID
  processKey: string // 流程唯一标识(业务中使用的 key)
  processName: string // 流程名称
  processVersion: number // 流程版本号
  processStatus: number // 状态:0-草稿, 1-发布, 2-停用
  description: string // 流程描述/说明
  deleted: boolean // 0:启用, 1:删除
}

十三、定义请求接口方法

代码如下:

import type { ProcessDefinitionInOrUpReq } from './processDefinitionType'
import request from '@/utils/http/request'

// 保存流程定义
export function saveProcessDefinition(data: ProcessDefinitionInOrUpReq) {
  return request.post<any>({
    url: '/pm-process/api/v1/definition/save',
    data,
  })
}

十四、添加抽屉

提供新增流程定义表单

① 控制参数

首先在原有的流程定义的vue基础上新增响应式数据控制【是否展示/抽屉标题/实例/参数/校验规则】等

具体代码如下:

// 定义响应式数据 showProcessDefinitionDrawer 控制添加流程抽屉的显示与隐藏
const showProcessDefinitionDrawer = ref<boolean>(false)
// 定义响应式数据 processDefinitionDrawerTitle 控制添加流程抽屉的标题
const processDefinitionDrawerTitle = ref<string>('')
// 定义响应式数据 operatePDFormRef 收集表单实例
const operatePDFormRef = ref()
// 定义数据 processDefinitionParams 表单参数
const processDefinitionParams = reactive<ProcessDefinitionInOrUpReq>({
  id: 0,
  processKey: '',
  processName: '',
  processVersion: 1,
  processStatus: 0,
  description: '',
  deleted: false,
})
// 定义数据 operatePDRules 表单校验规则
const operatePDRules = reactive({
  processKey: [
    // 必填校验
    { required: true, message: '请输入流程唯一标识', trigger: 'blur' },
    // 长度校验,不能超过50个字符
    { max: 50, message: '长度不能超过50个字符', trigger: 'blur' },
  ],
  processName: [
    // 必填校验
    { required: true, message: '请输入流程名称', trigger: 'blur' },
    // 长度校验,不能超过50个字符
    { max: 50, message: '长度不能超过50个字符', trigger: 'blur' },
  ],
  processVersion: [
    // 必填校验
    { required: true, message: '请输入流程版本号', trigger: 'blur' },
    // 必须是 0到9 组成的数字
    { pattern: /^\d+$/, message: '必须是0到9组成的数字', trigger: 'blur' },
  ],
})

② 原先按钮改造

然后再原先的【添加流程】按钮增加 @click 事件绑定打开抽屉方法,其中的  【v-hasButton="`btn.definition.save`"】自定义指令是我用来控制按钮权限的,如不需要可以去除

<el-button v-hasButton="`btn.definition.save`" type="primary" icon="Plus" @click="addProcessDefinition">
  添加流程
</el-button>

③ 打开抽屉方法

新增打开抽屉方法,需要通过设置响应式参数完成,并且清除数据以及校验结果,保证新的抽屉是纯净的

代码如下:

/**
 * 打开添加流程的抽屉并初始化相关状态。
 *
 * @function addProcessDefinition
 * @description 该函数用于打开添加流程的抽屉,设置抽屉标题,清空流程参数对象,并清除表单校验信息。
 *              主要用于在用户点击“添加流程”时初始化界面和数据状态。
 *
 * @param {undefined} 无参数
 *
 * @returns {undefined} 无返回值
 */
function addProcessDefinition() {
  // 打开添加流程抽屉,设置其可见性为 true
  showProcessDefinitionDrawer.value = true

  // 设置抽屉标题为“添加流程”,明确当前操作的目的
  processDefinitionDrawerTitle.value = '添加流程'

  // 清空流程参数对象,确保所有字段恢复到初始状态
  Object.assign(processDefinitionParams, {
    id: null,
    processKey: '',
    processName: '',
    processVersion: 1,
    processStatus: 0,
    description: '',
    deleted: false,
  })

  // 使用 nextTick 确保 DOM 更新完成后清除表单校验信息
  nextTick(() => {
    operatePDFormRef.value.clearValidate()
  })
}

⑤ 实际保存接口

代码如下:

/**
 * 保存流程定义的异步函数。
 *
 * @function onSaveProcessDefinition
 * @description 该函数用于校验表单数据、保存流程定义,并根据保存结果执行相应的操作(如关闭抽屉或显示提示信息)。
 *              如果保存失败或发生异常,会向用户显示错误信息。
 *
 * @returns {Promise<void>} 无返回值,但可能抛出异常。
 */
async function onSaveProcessDefinition() {
  try {
    // 校验表单数据,确保用户输入符合要求
    await operatePDFormRef.value.validate()

    // 调用接口保存流程定义数据
    const result: any = await saveProcessDefinition(processDefinitionParams)

    // 判断保存结果是否成功
    if (result.success && result.code === 200) {
      // 如果保存成功,关闭抽屉并提示用户操作成功
      showProcessDefinitionDrawer.value = false
      ElMessage({
        message: '操作成功',
        type: 'success',
      })
    }
    else {
      // 如果保存失败,提示用户具体的错误信息
      ElMessage({
        message: result.message,
        type: 'error',
      })
    }
  }
  catch (error) {
    // 捕获异常并处理错误信息
    let errorMessage = '未知错误'
    if (error instanceof Error) {
      errorMessage = error.message
    }
    else {
      errorMessage = '表单校验失败'
    }
    ElMessage({
      message: `保存失败: ${errorMessage}`,
      type: 'error',
    })
  }
}

⑥ 表单抽屉

代码如下:

<!-- 添加流程抽屉 -->
  <el-drawer v-model="showProcessDefinitionDrawer">
    <!-- 抽屉标题区域 -->
    <template #header>
      <h4>{{ processDefinitionDrawerTitle }}</h4>
    </template>
    <!-- 抽屉表单区域 -->
    <template #default>
      <el-form ref="operatePDFormRef" :model="processDefinitionParams" :rules="operatePDRules">
        <el-form-item label="流程唯一标识" prop="processKey">
          <el-input v-model="processDefinitionParams.processKey" placeholder="请输入流程唯一标识" />
        </el-form-item>
        <el-form-item label="流程名称" prop="processName">
          <el-input v-model="processDefinitionParams.processName" placeholder="请输入流程名称" />
        </el-form-item>
        <el-form-item label="流程版本号" prop="processVersion">
          <el-input v-model="processDefinitionParams.processVersion" placeholder="请输入流程版本号" />
        </el-form-item>
        <el-form-item label="流程状态" prop="processStatus">
          <el-select v-model="processDefinitionParams.processStatus" placeholder="请选择流程状态">
            <el-option label="草稿" :value="0" />
            <el-option label="发布" :value="1" />
            <el-option label="停用" :value="2" />
          </el-select>
        </el-form-item>
        <el-form-item label="流程描述/说明" prop="description">
          <el-input v-model="processDefinitionParams.description" placeholder="请输入流程描述/说明" />
        </el-form-item>
        <el-form-item label="是否删除" prop="deleted">
          <el-switch v-model="processDefinitionParams.deleted" active-text="是" inactive-text="否" />
        </el-form-item>
      </el-form>
    </template>
    <!-- 抽屉底层 按钮 区域 -->
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="showProcessDefinitionDrawer = false">
          取消
        </el-button>
        <el-button v-hasButton="`btn.definition.save`" type="primary" @click="onSaveProcessDefinition">
          确定
        </el-button>
      </div>
    </template>
  </el-drawer>

十五、测试界面功能

最后验证数据库

至此 保存功能就完成了,下一篇文章我们来做分页查询功能

后记

本篇文章的前后端仓库地址请查询专栏第一篇文章

本文的后端分支是 process-1

本文的前端分支是 process-3

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值