fix(bpm):动态表单必填字段后端补校验,缺必填不再建成脏实例

接口发起动态表单(formType=10 流程表单)流程时,必填字段未填也能
code=0 发起成功(前后端均漏校验,A51),建成变量缺失的脏实例。

在管理端 API 发起入口(VO 路径)补动态表单必填校验:按表单字段
定义(兼容 field/key 两种字段标识、数组/对象两种 required 标记)
逐一检查变量,缺失即抛新增业务错误 1_009_004_009。
仅校验 VO 路径:内部业务链路(DTO 路径,如 OA 请假只传部分变量)
不经过本校验,避免误伤。
This commit is contained in:
dadiyang
2026-09-24 21:47:40 +08:00
parent 8e43004cf6
commit 7c29bf220b
2 changed files with 45 additions and 0 deletions
@@ -44,6 +44,7 @@ public interface ErrorCodeConstants {
ErrorCode PROCESS_INSTANCE_HTTP_CALL_ERROR = new ErrorCode(1_009_004_006, "流程 Http 请求调用失败");
ErrorCode PROCESS_INSTANCE_APPROVE_USER_SELECT_ASSIGNEES_NOT_CONFIG = new ErrorCode(1_009_004_007, "下一个任务({})的审批人未配置");
ErrorCode PROCESS_INSTANCE_CANCEL_CHILD_FAIL_NOT_ALLOW = new ErrorCode(1_009_004_008, "子流程取消失败,子流程不允许取消");
ErrorCode PROCESS_INSTANCE_START_FORM_FIELD_REQUIRED = new ErrorCode(1_009_004_009, "发起流程失败,表单字段({})为必填项,不能为空");
// ========== 流程任务 1-009-005-000 ==========
ErrorCode TASK_OPERATE_FAIL_ASSIGN_NOT_SELF = new ErrorCode(1_009_005_001, "操作失败,原因:该任务的审批人不是你");
@@ -22,6 +22,7 @@ import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
import cn.iocoder.yudao.module.bpm.dal.redis.BpmProcessIdRedisDAO;
import cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeTypeEnum;
import cn.iocoder.yudao.module.bpm.enums.task.BpmAttachmentTypeEnum;
@@ -763,11 +764,54 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
// 获得流程定义
ProcessDefinition definition = processDefinitionService
.getProcessDefinition(createReqVO.getProcessDefinitionId());
// 校验动态表单必填字段(前端校验可被接口直调绕过,后端必须兜底,否则必填缺失也会建成脏实例)
validateDynamicFormRequiredFields(definition, createReqVO.getVariables());
// 发起流程
return createProcessInstance0(userId, definition, createReqVO.getVariables(), null,
createReqVO.getStartUserSelectAssignees());
}
/**
* 校验动态表单(formType=10 流程表单)的必填字段
*
* 注意:仅校验接口发起(VO 路径)入口。内部业务链路(DTO 路径,例如 OA 请假)
* 由业务代码自行组装变量,不经过本校验,避免误伤。
*/
private void validateDynamicFormRequiredFields(ProcessDefinition definition, Map<String, Object> variables) {
BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService
.getProcessDefinitionInfo(definition.getId());
if (processDefinitionInfo == null
|| !BpmModelFormTypeEnum.NORMAL.getType().equals(processDefinitionInfo.getFormType())
|| CollUtil.isEmpty(processDefinitionInfo.getFormFields())) {
return;
}
List<String> fieldStrs = processDefinitionInfo.getFormFields();
// formFields 是 JSON 数组,每个元素本身又是 JSON 字符串;两种表单设计器并存:
// 字段标识可能是 field 或 key,必填标记可能是数组 ["required"] 或对象 {"required": true}
for (String fieldStr : fieldStrs) {
Map<String, Object> field = JsonUtils.parseMap(fieldStr);
boolean required = false;
Object validate = field.get("validate");
if (validate instanceof List) {
required = ((List<?>) validate).contains("required");
} else if (validate instanceof Map) {
required = Boolean.TRUE.equals(((Map<?, ?>) validate).get("required"));
}
if (!required) {
continue;
}
String fieldId = Convert.toStr(field.get("field"), Convert.toStr(field.get("key")));
if (StrUtil.isEmpty(fieldId)) {
continue;
}
Object value = variables == null ? null : variables.get(fieldId);
if (value == null || (value instanceof String && StrUtil.isBlank((String) value))) {
throw exception(PROCESS_INSTANCE_START_FORM_FIELD_REQUIRED,
Convert.toStr(field.get("title"), fieldId));
}
}
}
@Override
@DataPermission(enable = false) // 关闭数据权限,避免查询不到用户数据。相关案例:https://gitee.com/zhijiantianya/yudao-cloud/issues/ID1UYA
public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO createReqDTO) {