feat(flow): 支持节点 REST 表单权限解析

(cherry picked from commit 80459350acfcd41351497959884307dfa6bbf9c0)
This commit is contained in:
PIGCLOUD committed 2026-11-03 14:56:21 +08:00
1 parent 14cb5dfe0a
commit ddfc2b41d9
12 files changed
+464 -3

No files matched your search

@@ -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;
/**
* 无执行人处理配置 定义当节点找不到执行人时的处理策略
*/
@@ -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>
@@ -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>
@@ -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);
}
}
}
@@ -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获取流程定义实体
*
@@ -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);
}
@@ -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 分钟");
@@ -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);
}
@@ -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;
}
}
@@ -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;