一、前言
之前的文章已经把基础的静态界面已经搭建好了,那么现在需要绘制一个可以编辑流程定义的表单,我的脚手架工程使用了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