mirror of
http://git.pig4cloud.com/pig/pigx.git
synced 2026-11-08 04:39:00 +08:00
feat(flow): 支持节点 REST 表单权限解析
(cherry picked from commit 80459350acfcd41351497959884307dfa6bbf9c0)
This commit is contained in:
1 parent
14cb5dfe0a
commit
ddfc2b41d9
12 files changed
+464
-3
No files matched your search
+15
@@ -0,0 +1,15 @@
|
||||
package com.pig4cloud.pigx.flow.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/** 打开动态表单时查询字段权限的 REST 配置。 */
|
||||
@Data
|
||||
public class FormPermissionConfig {
|
||||
|
||||
/** 是否启用 REST 权限查询;null 或 false 表示使用静态权限。 */
|
||||
private Boolean enabled;
|
||||
|
||||
/** 已启用时必填的完整 HTTP(S) 地址,不能包含账号或片段。 */
|
||||
private String url;
|
||||
|
||||
}
|
||||
@@ -129,6 +129,9 @@ public class Node {
|
||||
*/
|
||||
private Map<String, String> formPerms;
|
||||
|
||||
/** REST 返回值按字段覆盖静态权限;未启用时沿用 formPerms。 */
|
||||
private FormPermissionConfig formPermissionConfig;
|
||||
|
||||
/**
|
||||
* 无执行人处理配置 定义当节点找不到执行人时的处理策略
|
||||
*/
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.pig4cloud.pigx.flow.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/** 打开发起表单;initialData 为草稿与业务入口合并后的初始数据。 */
|
||||
@Data
|
||||
public class OpenProcessFormDto {
|
||||
|
||||
/** 待发起流程的业务标识,必填。 */
|
||||
@NotBlank
|
||||
private String flowId;
|
||||
|
||||
/** 可选初始数据,由调用方按草稿低于入口参数的优先级合并,覆盖表单默认值。 */
|
||||
private Map<String, Object> initialData;
|
||||
|
||||
/** 恢复草稿时校验发布版本,防止将旧稿数据送给新版本的权限接口。 */
|
||||
private String draftVersion;
|
||||
|
||||
}
|
||||
@@ -34,6 +34,9 @@ public class ProcessVO extends Process {
|
||||
*/
|
||||
private Map<String, Object> variableMap;
|
||||
|
||||
/** 打开发起表单时合并默认值、草稿和入口数据后的初始值。 */
|
||||
private Map<String, Object> formData;
|
||||
|
||||
/**
|
||||
* 发起人节点的表单权限配置
|
||||
* <p>
|
||||
|
||||
+15
@@ -1,10 +1,13 @@
|
||||
package com.pig4cloud.pigx.flow.controller;
|
||||
|
||||
import com.pig4cloud.pigx.common.core.util.R;
|
||||
import com.pig4cloud.pigx.flow.dto.OpenProcessFormDto;
|
||||
import com.pig4cloud.pigx.flow.entity.Process;
|
||||
import com.pig4cloud.pigx.flow.service.IProcessService;
|
||||
import com.pig4cloud.pigx.flow.vo.ProcessVO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
@@ -41,6 +44,18 @@ public class ProcessController {
|
||||
return processService.getDetail(flowId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并表单默认值与调用方初始数据,并查询发起节点的字段权限。
|
||||
* @param request 发起请求,不可为 null;初始数据和草稿版本可省略
|
||||
* @return 包含初始数据和权限的流程详情;草稿版本不匹配时返回失败结果
|
||||
* @throws IllegalStateException REST 权限加载失败时抛出,阻止继续填写
|
||||
*/
|
||||
@Operation(summary = "打开发起表单", description = "合并初始数据并同步查询节点字段权限")
|
||||
@PostMapping("openForm")
|
||||
public R<ProcessVO> openForm(@Valid @RequestBody OpenProcessFormDto request) {
|
||||
return processService.openForm(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新流程定义
|
||||
* <p>
|
||||
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
package com.pig4cloud.pigx.flow.service;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.pig4cloud.pigx.flow.constant.NodeTypeEnum;
|
||||
import com.pig4cloud.pigx.flow.constant.ProcessInstanceConstant.FormPermClass;
|
||||
import com.pig4cloud.pigx.flow.dto.FormPermissionConfig;
|
||||
import com.pig4cloud.pigx.flow.dto.Node;
|
||||
import com.pig4cloud.pigx.flow.dto.ProcessNodeRecordAssignUserParamDto;
|
||||
import com.pig4cloud.pigx.flow.entity.Process;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/** 同步查询动态表单权限;调用失败时不回退、不重试,也不修改静态配置。 */
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FormPermissionService {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* 校验已启用的 REST 权限地址;未配置或未启用时跳过校验。
|
||||
* @param config 节点 REST 权限配置,可为 null
|
||||
* @throws IllegalArgumentException 地址不是完整 HTTP(S) URL,或含账号、片段时抛出
|
||||
*/
|
||||
public static void validateConfig(FormPermissionConfig config) {
|
||||
if (Objects.isNull(config) || !Boolean.TRUE.equals(config.getEnabled())) return;
|
||||
try {
|
||||
URI uri = URI.create(StrUtil.trim(config.getUrl()));
|
||||
if ((!"http".equalsIgnoreCase(uri.getScheme()) && !"https".equalsIgnoreCase(uri.getScheme()))
|
||||
|| StrUtil.isBlank(uri.getHost()) || Objects.nonNull(uri.getUserInfo())
|
||||
|| Objects.nonNull(uri.getFragment())) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
catch (Exception exception) {
|
||||
throw new IllegalArgumentException("REST 表单权限地址必须是完整的 HTTP(S) URL,且不能包含账号或片段");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步查询字段权限并覆盖静态权限副本,未返回的字段保持静态值,抄送编辑权限转为只读。
|
||||
* 未启用 REST 或使用自定义业务表单时返回静态权限副本;失败不重试、不回退。
|
||||
* @param node 当前节点及其 REST 配置,不可为 null
|
||||
* @param process 提供流程标识、表单配置和合法字段的流程定义,不可为 null
|
||||
* @param staticPermissions 静态权限,可为 null;原映射不会被修改
|
||||
* @param context 当前用户和表单数据上下文,不可为 null;尚未生成的实例、任务标识可为空
|
||||
* @return 合并后的独立权限映射,不为 null
|
||||
* @throws IllegalStateException 地址、外部请求、响应或表单元数据无效时阻止表单加载
|
||||
*/
|
||||
public Map<String, String> resolve(Node node, Process process, Map<String, String> staticPermissions,
|
||||
ProcessNodeRecordAssignUserParamDto context) {
|
||||
Map<String, String> permissions = new HashMap<>();
|
||||
if (Objects.nonNull(staticPermissions)) permissions.putAll(staticPermissions);
|
||||
FormPermissionConfig config = node.getFormPermissionConfig();
|
||||
if (Objects.isNull(config) || !Boolean.TRUE.equals(config.getEnabled())) return permissions;
|
||||
String requestId = UUID.randomUUID().toString();
|
||||
try {
|
||||
if (StrUtil.isNotBlank(process.getFormConfig())
|
||||
&& objectMapper.readTree(process.getFormConfig()).path("formType").asInt() == 20) {
|
||||
return permissions;
|
||||
}
|
||||
validateConfig(config);
|
||||
// 沿用任务完成 WebHook 的字段与类型,不伪造任务完成类型或审批意见。
|
||||
ObjectNode payload = objectMapper.createObjectNode();
|
||||
payload.put("flowId", process.getFlowId());
|
||||
payload.put("processDefinitionId", context.getProcessDefinitionId());
|
||||
payload.put("processInstanceId", context.getProcessInstanceId());
|
||||
payload.put("nodeId", node.getId());
|
||||
payload.put("nodeName", node.getName());
|
||||
payload.put("taskId", context.getTaskId());
|
||||
payload.put("executionId", context.getExecutionId());
|
||||
payload.put("userId", context.getUserId());
|
||||
payload.put("data", context.getData());
|
||||
payload.put("localData", context.getLocalData());
|
||||
try (HttpResponse response = HttpRequest.post(config.getUrl().trim())
|
||||
.header("X-Flow-Event-Id", requestId)
|
||||
.header("Idempotency-Key", requestId)
|
||||
.body(objectMapper.writeValueAsString(payload))
|
||||
.timeout(10000)
|
||||
.setFollowRedirects(false)
|
||||
.execute()) {
|
||||
if (response.getStatus() < 200 || response.getStatus() >= 300) {
|
||||
throw new IllegalStateException("HTTP " + response.getStatus());
|
||||
}
|
||||
JsonNode result = objectMapper.readTree(response.body());
|
||||
if (Objects.isNull(result) || !result.path("code").isIntegralNumber() || result.path("code").asLong() != 0
|
||||
|| !result.path("data").isObject()) {
|
||||
throw new IllegalStateException("响应必须为成功的 R<Map<String, String>>");
|
||||
}
|
||||
Set<String> fields = new HashSet<>();
|
||||
collectFields(objectMapper.readTree(process.getFormItems()), fields);
|
||||
var entries = result.get("data").fields();
|
||||
while (entries.hasNext()) {
|
||||
var entry = entries.next();
|
||||
JsonNode value = entry.getValue();
|
||||
if (!fields.contains(entry.getKey()) || !value.isTextual()
|
||||
|| !Set.of(FormPermClass.EDIT, FormPermClass.READ, FormPermClass.HIDE).contains(value.textValue())) {
|
||||
throw new IllegalStateException("响应包含未知字段或非法权限值");
|
||||
}
|
||||
permissions.put(entry.getKey(), value.textValue());
|
||||
}
|
||||
}
|
||||
if (NodeTypeEnum.CC.getValue().equals(node.getType())) {
|
||||
permissions.replaceAll((field, permission) -> FormPermClass.EDIT.equals(permission) ? FormPermClass.READ
|
||||
: permission);
|
||||
}
|
||||
return permissions;
|
||||
}
|
||||
catch (Exception exception) {
|
||||
// 不记录业务数据、接口响应或 URL 中可能携带的凭据。
|
||||
log.warn("REST 表单权限加载失败,请求 {},流程 {},节点 {},原因 {}", requestId, process.getFlowId(), node.getId(),
|
||||
exception.getClass().getSimpleName());
|
||||
throw new IllegalStateException("表单权限加载失败,无法填写或办理");
|
||||
}
|
||||
}
|
||||
|
||||
private void collectFields(JsonNode items, Set<String> fields) {
|
||||
if (Objects.isNull(items) || !items.isArray()) return;
|
||||
for (JsonNode item : items) {
|
||||
String field = item.path("field").asText();
|
||||
if (StrUtil.isNotBlank(field)) fields.add(field);
|
||||
collectFields(item.get("children"), fields);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+9
@@ -3,6 +3,7 @@ package com.pig4cloud.pigx.flow.service;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.pig4cloud.pigx.common.core.util.R;
|
||||
import com.pig4cloud.pigx.flow.entity.Process;
|
||||
import com.pig4cloud.pigx.flow.dto.OpenProcessFormDto;
|
||||
import com.pig4cloud.pigx.flow.vo.ProcessVO;
|
||||
|
||||
/**
|
||||
@@ -26,6 +27,14 @@ public interface IProcessService extends IService<Process> {
|
||||
*/
|
||||
R<ProcessVO> getDetail(String flowId);
|
||||
|
||||
/**
|
||||
* 合并表单默认值与调用方初始数据,并查询发起节点的字段权限。
|
||||
* @param request 发起请求,不可为 null;初始数据和草稿版本可省略
|
||||
* @return 包含初始数据和权限的流程详情;草稿版本不匹配时返回失败结果
|
||||
* @throws IllegalStateException REST 权限加载失败时抛出,阻止继续填写
|
||||
*/
|
||||
R<ProcessVO> openForm(OpenProcessFormDto request);
|
||||
|
||||
/**
|
||||
* 根据流程ID获取流程定义实体
|
||||
*
|
||||
|
||||
+15
-1
@@ -4,13 +4,16 @@ import cn.hutool.core.lang.Dict;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.pig4cloud.pigx.common.core.util.R;
|
||||
import com.pig4cloud.pigx.common.security.util.SecurityUtils;
|
||||
import com.pig4cloud.pigx.flow.dto.Node;
|
||||
import com.pig4cloud.pigx.flow.dto.ProcessNodeRecordAssignUserParamDto;
|
||||
import com.pig4cloud.pigx.flow.entity.Process;
|
||||
import com.pig4cloud.pigx.flow.entity.ProcessCopy;
|
||||
import com.pig4cloud.pigx.flow.mapper.ProcessCopyMapper;
|
||||
import com.pig4cloud.pigx.flow.service.IProcessCopyService;
|
||||
import com.pig4cloud.pigx.flow.service.IProcessNodeDataService;
|
||||
import com.pig4cloud.pigx.flow.service.IProcessService;
|
||||
import com.pig4cloud.pigx.flow.service.FormPermissionService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -46,6 +49,8 @@ public class ProcessCopyServiceImpl extends ServiceImpl<ProcessCopyMapper, Proce
|
||||
|
||||
private final RuntimeService runtimeService;
|
||||
|
||||
private final FormPermissionService formPermissionService;
|
||||
|
||||
/**
|
||||
* 查询单个抄送详细信息
|
||||
* <p>
|
||||
@@ -93,13 +98,22 @@ public class ProcessCopyServiceImpl extends ServiceImpl<ProcessCopyMapper, Proce
|
||||
definitionId = historic.getProcessDefinitionId();
|
||||
}
|
||||
Node node = nodeDataService.getPublishedNode(definitionId, nodeId).getData();
|
||||
ProcessNodeRecordAssignUserParamDto context = new ProcessNodeRecordAssignUserParamDto();
|
||||
context.setProcessDefinitionId(definitionId);
|
||||
context.setProcessInstanceId(processCopy.getProcessInstanceId());
|
||||
context.setUserId(SecurityUtils.getUser().getId());
|
||||
context.setData(formData);
|
||||
var formPerms = NodeUtil.publishedFormPermissions(node, oaForms.getFormItems());
|
||||
if (Objects.nonNull(node.getFormPermissionConfig()) && Boolean.TRUE.equals(node.getFormPermissionConfig().getEnabled())) {
|
||||
formPerms = formPermissionService.resolve(node, oaForms, formPerms, context);
|
||||
}
|
||||
|
||||
// 构建返回数据
|
||||
Dict set = Dict.create()
|
||||
.set("process", oaForms.getProcess()) // 流程定义
|
||||
.set("formItems", oaForms.getFormItems()) // 表单项定义
|
||||
.set("formData", formData) // 表单数据
|
||||
.set("formPerms", NodeUtil.publishedFormPermissions(node, oaForms.getFormItems())); // 表单权限
|
||||
.set("formPerms", formPerms); // 表单权限
|
||||
return R.ok(set);
|
||||
}
|
||||
|
||||
|
||||
+69
@@ -14,13 +14,17 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.pig4cloud.pigx.common.core.constant.enums.YesNoEnum;
|
||||
import com.pig4cloud.pigx.common.core.util.R;
|
||||
import com.pig4cloud.pigx.common.data.tenant.TenantContextHolder;
|
||||
import com.pig4cloud.pigx.common.sequence.sequence.Sequence;
|
||||
import com.pig4cloud.pigx.common.security.util.SecurityUtils;
|
||||
import com.pig4cloud.pigx.flow.constant.FormTypeEnum;
|
||||
import com.pig4cloud.pigx.flow.dto.Node;
|
||||
import com.pig4cloud.pigx.flow.dto.NodeUser;
|
||||
import com.pig4cloud.pigx.flow.dto.OpenProcessFormDto;
|
||||
import com.pig4cloud.pigx.flow.dto.ProcessNodeRecordAssignUserParamDto;
|
||||
import com.pig4cloud.pigx.flow.entity.Process;
|
||||
import com.pig4cloud.pigx.flow.entity.ProcessStarter;
|
||||
import com.pig4cloud.pigx.flow.event.FlowStatusEventPublisher;
|
||||
@@ -28,6 +32,7 @@ import com.pig4cloud.pigx.flow.mapper.ProcessMapper;
|
||||
import com.pig4cloud.pigx.flow.service.IProcessService;
|
||||
import com.pig4cloud.pigx.flow.service.IProcessNodeDataService;
|
||||
import com.pig4cloud.pigx.flow.service.IProcessStarterService;
|
||||
import com.pig4cloud.pigx.flow.service.FormPermissionService;
|
||||
import com.pig4cloud.pigx.flow.support.utils.ModelUtil;
|
||||
import com.pig4cloud.pigx.flow.support.utils.NodeUtil;
|
||||
import com.pig4cloud.pigx.flow.vo.FormItemVO;
|
||||
@@ -45,6 +50,8 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Map;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
/**
|
||||
* 流程定义服务实现类
|
||||
@@ -74,6 +81,8 @@ public class ProcessServiceImpl extends ServiceImpl<ProcessMapper, Process> impl
|
||||
|
||||
private final FlowStatusEventPublisher flowStatusEventPublisher;
|
||||
|
||||
private final FormPermissionService formPermissionService;
|
||||
|
||||
/**
|
||||
* 获取流程详细信息
|
||||
* <p>
|
||||
@@ -89,6 +98,65 @@ public class ProcessServiceImpl extends ServiceImpl<ProcessMapper, Process> impl
|
||||
return R.ok(processVO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并表单默认值和调用方初始数据,并同步加载发起节点的字段权限。
|
||||
* @param request 发起请求,不可为 null;initialData、draftVersion 可省略
|
||||
* @return 带初始数据及权限的流程;草稿版本不匹配时返回失败结果且不查询 REST
|
||||
* @throws IllegalStateException REST 权限加载失败时抛出,阻止继续填写
|
||||
* @throws com.fasterxml.jackson.core.JsonProcessingException 表单元数据无法解析时抛出
|
||||
*/
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public R<ProcessVO> openForm(OpenProcessFormDto request) {
|
||||
ProcessVO process = getProcessVO(request.getFlowId());
|
||||
if (Objects.nonNull(request.getDraftVersion())
|
||||
&& !request.getDraftVersion().equals(Objects.toString(process.getUniqueId(), StrUtil.EMPTY))) {
|
||||
return R.failed("流程已发布新版本,该草稿暂不能恢复。请保留草稿并重新填写新申请。");
|
||||
}
|
||||
Node node = objectMapper.readValue(process.getProcess(), Node.class);
|
||||
Map<String, Object> formData = new LinkedHashMap<>();
|
||||
boolean nestedPaths = StrUtil.isNotBlank(process.getFormConfig())
|
||||
&& "buzzform".equals(objectMapper.readTree(process.getFormConfig()).path("engine").asText());
|
||||
collectInitialValues(objectMapper.readTree(process.getFormItems()), formData, nestedPaths);
|
||||
if (Objects.nonNull(request.getInitialData())) formData.putAll(request.getInitialData());
|
||||
process.setFormData(formData);
|
||||
ProcessNodeRecordAssignUserParamDto context = new ProcessNodeRecordAssignUserParamDto();
|
||||
var definition = repositoryService.createProcessDefinitionQuery()
|
||||
.deploymentId(process.getUniqueId())
|
||||
.singleResult();
|
||||
if (Objects.nonNull(definition)) context.setProcessDefinitionId(definition.getId());
|
||||
context.setUserId(SecurityUtils.getUser().getId());
|
||||
context.setData(objectMapper.writeValueAsString(formData));
|
||||
process.setFormPerms(formPermissionService.resolve(node, process, process.getFormPerms(), context));
|
||||
return R.ok(process);
|
||||
}
|
||||
|
||||
/** 两种表单引擎共用 formItems 元数据,React 的嵌套字段路径需还原成对象。 */
|
||||
@SuppressWarnings("unchecked")
|
||||
private void collectInitialValues(JsonNode items, Map<String, Object> values, boolean nestedPaths) {
|
||||
if (Objects.isNull(items) || !items.isArray()) return;
|
||||
for (JsonNode item : items) {
|
||||
String field = item.path("field").asText();
|
||||
JsonNode value = item.get("value");
|
||||
if (Objects.isNull(value)) value = item.path("props").get("value");
|
||||
if (StrUtil.isNotBlank(field) && Objects.nonNull(value)) {
|
||||
String[] path = nestedPaths ? StrUtil.splitToArray(field, StrUtil.C_DOT) : new String[] { field };
|
||||
Map<String, Object> target = values;
|
||||
for (int i = 0; i < path.length - 1; i++) {
|
||||
Object child = target.computeIfAbsent(path[i], key -> new LinkedHashMap<String, Object>());
|
||||
// 数组项的默认值由表单引擎逐项处理,不能写到根级字段。
|
||||
if (!(child instanceof Map)) {
|
||||
target = null;
|
||||
break;
|
||||
}
|
||||
target = (Map<String, Object>) child;
|
||||
}
|
||||
if (Objects.nonNull(target)) target.put(path[path.length - 1], objectMapper.convertValue(value, Object.class));
|
||||
}
|
||||
collectInitialValues(item.get("children"), values, nestedPaths);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取流程视图对象
|
||||
* <p>
|
||||
@@ -334,6 +402,7 @@ public class ProcessServiceImpl extends ServiceImpl<ProcessMapper, Process> impl
|
||||
private void validateDeliveryConfig(Node node) {
|
||||
if (java.util.Objects.isNull(node))
|
||||
return;
|
||||
FormPermissionService.validateConfig(node.getFormPermissionConfig());
|
||||
if (java.util.Objects.nonNull(node.getReminderMinutes())
|
||||
&& (node.getReminderMinutes() < 0 || node.getReminderMinutes() > 525600))
|
||||
throw new IllegalArgumentException("超时提醒必须为 0 到 525600 分钟");
|
||||
|
||||
+17
-1
@@ -87,6 +87,8 @@ public class TaskServiceImpl implements ITaskService {
|
||||
|
||||
private final ProcessTaskAddSignRecordMapper processTaskAddSignRecordMapper;
|
||||
|
||||
private final FormPermissionService formPermissionService;
|
||||
|
||||
/**
|
||||
* 查询首页数据看板
|
||||
* <p>
|
||||
@@ -307,6 +309,20 @@ public class TaskServiceImpl implements ITaskService {
|
||||
return R.failed("流程不存在");
|
||||
}
|
||||
|
||||
Map<String, String> formPerms = NodeUtil.publishedFormPermissions(node, oaForms.getFormItems());
|
||||
if (!view && task.isPresent() && Objects.nonNull(node.getFormPermissionConfig())
|
||||
&& Boolean.TRUE.equals(node.getFormPermissionConfig().getEnabled())) {
|
||||
ProcessNodeRecordAssignUserParamDto context = new ProcessNodeRecordAssignUserParamDto();
|
||||
context.setProcessDefinitionId(taskResultDto.getProcessDefinitionId());
|
||||
context.setProcessInstanceId(taskResultDto.getProcessInstanceId());
|
||||
context.setTaskId(taskId);
|
||||
context.setExecutionId(task.get().getExecutionId());
|
||||
context.setUserId(userId);
|
||||
context.setData(objectMapper.writeValueAsString(paramMap));
|
||||
context.setLocalData(objectMapper.writeValueAsString(taskService.getVariablesLocal(taskId)));
|
||||
formPerms = formPermissionService.resolve(node, oaForms, formPerms, context);
|
||||
}
|
||||
|
||||
Dict set = Dict.create()
|
||||
.set("taskId", taskId)
|
||||
.set("nodeId", taskResultDto.getNodeId())
|
||||
@@ -320,7 +336,7 @@ public class TaskServiceImpl implements ITaskService {
|
||||
.set("formItems", oaForms.getFormItems())
|
||||
.set("formData", paramMap)
|
||||
.set("formConfig", oaForms.getFormConfig())
|
||||
.set("formPerms", NodeUtil.publishedFormPermissions(node, oaForms.getFormItems()));
|
||||
.set("formPerms", formPerms);
|
||||
|
||||
return R.ok(set);
|
||||
}
|
||||
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
package com.pig4cloud.pigx.flow.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.pig4cloud.pigx.common.security.service.PigxUser;
|
||||
import com.pig4cloud.pigx.flow.dto.FormPermissionConfig;
|
||||
import com.pig4cloud.pigx.flow.dto.Node;
|
||||
import com.pig4cloud.pigx.flow.dto.OpenProcessFormDto;
|
||||
import com.pig4cloud.pigx.flow.dto.ProcessNodeRecordAssignUserParamDto;
|
||||
import com.pig4cloud.pigx.flow.entity.Process;
|
||||
import com.pig4cloud.pigx.flow.service.impl.ProcessServiceImpl;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.flowable.engine.RepositoryService;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
|
||||
|
||||
class FormPermissionServiceTests {
|
||||
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
void queriesWebhookAndOverlaysPermissionsWithoutMutatingStaticConfiguration() throws Exception {
|
||||
var requests = new LinkedBlockingQueue<JsonNode>();
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||
server.createContext("/permissions", exchange -> {
|
||||
try (exchange) {
|
||||
requests.add(mapper.readTree(exchange.getRequestBody()));
|
||||
byte[] body = "{\"code\":0,\"data\":{\"a\":\"H\",\"b\":\"E\"}}".getBytes(StandardCharsets.UTF_8);
|
||||
exchange.sendResponseHeaders(200, body.length);
|
||||
exchange.getResponseBody().write(body);
|
||||
}
|
||||
});
|
||||
server.start();
|
||||
try {
|
||||
Node node = node(server);
|
||||
Map<String, String> original = Map.of("a", "E", "b", "R", "c", "R");
|
||||
node.setFormPerms(original);
|
||||
ProcessNodeRecordAssignUserParamDto context = new ProcessNodeRecordAssignUserParamDto();
|
||||
context.setProcessDefinitionId("leave:1:definition");
|
||||
context.setUserId(1001L);
|
||||
context.setData("{\"businessType\":2}");
|
||||
FormPermissionService service = new FormPermissionService(mapper);
|
||||
Process process = process();
|
||||
process.setProcess(mapper.writeValueAsString(node));
|
||||
process.setUniqueId("deployment");
|
||||
process.setFormItems("[{\"field\":\"a\",\"value\":1},{\"field\":\"b\",\"value\":\"default\"},{\"field\":\"c\"}]");
|
||||
RepositoryService repository = mock(RepositoryService.class, RETURNS_DEEP_STUBS);
|
||||
when(repository.createProcessDefinitionQuery().deploymentId("deployment").singleResult().getId())
|
||||
.thenReturn("leave:1:definition");
|
||||
ProcessServiceImpl opening = new ProcessServiceImpl(null, repository, null, mapper, null, null, service) {
|
||||
@Override
|
||||
public Process getByFlowId(String flowId) {
|
||||
return process;
|
||||
}
|
||||
};
|
||||
try {
|
||||
PigxUser user = mock(PigxUser.class);
|
||||
when(user.getId()).thenReturn(1001L);
|
||||
SecurityContextHolder.getContext().setAuthentication(
|
||||
new UsernamePasswordAuthenticationToken(user, "unused", List.of()));
|
||||
OpenProcessFormDto input = new OpenProcessFormDto();
|
||||
input.setFlowId("leave");
|
||||
input.setInitialData(Map.of("businessType", 2, "b", "draft"));
|
||||
var opened = opening.openForm(input).getData();
|
||||
assertThat(opened.getFormPerms()).isEqualTo(Map.of("a", "H", "b", "E", "c", "R"));
|
||||
assertThat(opened.getFormData()).containsEntry("a", 1).containsEntry("b", "draft")
|
||||
.containsEntry("businessType", 2);
|
||||
}
|
||||
finally {
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
JsonNode request = requests.poll(1, TimeUnit.SECONDS);
|
||||
assertThat(request.get("data").isTextual()).isTrue();
|
||||
assertThat(mapper.readTree(request.get("data").asText()).get("businessType").asInt()).isEqualTo(2);
|
||||
assertThat(request.get("userId").asLong()).isEqualTo(1001L);
|
||||
assertThat(request.get("taskId").isNull()).isTrue();
|
||||
assertThat(request.has("taskType")).isFalse();
|
||||
assertThat(node.getFormPerms()).isEqualTo(original);
|
||||
// 抄送仍然只读;关闭 REST 后不再外调。
|
||||
node.setType(2);
|
||||
assertThat(service.resolve(node, process(), original, context)).containsEntry("b", "R");
|
||||
requests.clear();
|
||||
node.getFormPermissionConfig().setEnabled(false);
|
||||
assertThat(service.resolve(node, process(), original, context)).isEqualTo(original);
|
||||
assertThat(requests.poll(100, TimeUnit.MILLISECONDS)).isNull();
|
||||
}
|
||||
finally {
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidResponseBlocksTheWholeFormAndIsNotRetried() throws Exception {
|
||||
var requests = new LinkedBlockingQueue<Boolean>();
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||
server.createContext("/permissions", exchange -> {
|
||||
try (exchange) {
|
||||
requests.add(true);
|
||||
byte[] body = "{\"code\":0,\"data\":{\"a\":\"H\",\"unknown\":\"E\"}}".getBytes(StandardCharsets.UTF_8);
|
||||
exchange.sendResponseHeaders(200, body.length);
|
||||
exchange.getResponseBody().write(body);
|
||||
}
|
||||
});
|
||||
server.start();
|
||||
try {
|
||||
Map<String, String> original = Map.of("a", "E");
|
||||
assertThatThrownBy(() -> new FormPermissionService(mapper).resolve(node(server), process(), original,
|
||||
new ProcessNodeRecordAssignUserParamDto()))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("表单权限加载失败,无法填写或办理");
|
||||
assertThat(requests.poll(1, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(requests.poll(100, TimeUnit.MILLISECONDS)).isNull();
|
||||
assertThat(original).containsEntry("a", "E");
|
||||
}
|
||||
finally {
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
|
||||
private Node node(HttpServer server) {
|
||||
FormPermissionConfig config = new FormPermissionConfig();
|
||||
config.setEnabled(true);
|
||||
config.setUrl("http://127.0.0.1:" + server.getAddress().getPort() + "/permissions");
|
||||
Node node = new Node();
|
||||
node.setId("root");
|
||||
node.setType(0);
|
||||
node.setName("发起人");
|
||||
node.setFormPermissionConfig(config);
|
||||
return node;
|
||||
}
|
||||
|
||||
private Process process() {
|
||||
Process process = new Process();
|
||||
process.setFlowId("leave");
|
||||
process.setFormConfig("{\"formType\":10}");
|
||||
process.setFormItems("[{\"field\":\"a\"},{\"field\":\"b\"},{\"field\":\"c\"}]");
|
||||
return process;
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -175,7 +175,7 @@ class ProcessNodeVersionTests {
|
||||
ProcessDefinition a = publish("leave", 1001L, "R");
|
||||
long before = jdbc.queryForObject("select count(*) from process_node_data", Long.class);
|
||||
ProcessServiceImpl target = new ProcessServiceImpl(mock(IProcessStarterService.class),
|
||||
engine.getRepositoryService(), nodes, new ObjectMapper(), null, null) {
|
||||
engine.getRepositoryService(), nodes, new ObjectMapper(), null, null, null) {
|
||||
@Override
|
||||
public Process getByFlowId(String flowId) {
|
||||
return null;
|
||||
|
||||
Reference in new issue
Block a user