Compare commits
7
Commits
775b2392fa
...
8ceb2a4247
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ceb2a4247 | ||
|
|
ad87005169 | ||
|
|
448c8d36f4 | ||
|
|
f4121b2c1e | ||
|
|
82ffe2f030 | ||
|
|
008aee3c88 | ||
|
|
81d9a075e8 |
@@ -56,6 +56,10 @@
|
||||
<dependency>
|
||||
<groupId>com.yunhai.platform</groupId>
|
||||
<artifactId>yh-appearance-spring-boot-starter</artifactId>
|
||||
<!-- 显式版本(2026-10-17 过夜任务环境修复,与 NoticeService 同因):该件只由 BasePlatform
|
||||
的 feat/appearance-sdk 分支及其 BOM 托管,本次接入件工作基于 feat/flow-embed(BOM 无此条目),
|
||||
不写版本则依赖解析直接失败、本仓无法打包。appearance 分支合流后应删本行回落 BOM。 -->
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<!-- 平台 auth starter(0928 后快照)的 WeChatAuthAutoConfiguration 签名引用 StringRedisTemplate,
|
||||
data-redis 为 optional,接入前提=应用自带(见该 starter pom 注释);缺失时 bean 签名解析直接
|
||||
|
||||
@@ -54,6 +54,7 @@ public class FlowAdminController {
|
||||
private final com.fasterxml.jackson.databind.ObjectMapper objectMapper;
|
||||
private final com.yunhai.flowcenter.service.FlowAppAdminService appAdminService;
|
||||
private final com.yunhai.flowcenter.service.FlowDeptAdminService deptAdminService;
|
||||
private final com.yunhai.flowcenter.repository.FlwRouteLogRepository routeLogRepository;
|
||||
|
||||
// ==================== 流程定义 ====================
|
||||
|
||||
@@ -413,10 +414,13 @@ public class FlowAdminController {
|
||||
/**
|
||||
* 重跑路由(判断失败挂起的恢复入口,2026-09-24 §4.4):重新调判断服务,成功即按出口续走;
|
||||
* 仍失败回业务错(办件继续挂起,route_log 已落新行)。
|
||||
* useLatestDef=true(F5-4 僵尸逃生门):按该 flowGuid 当前已发布最高版本重跑并刷新办件定义快照。
|
||||
*/
|
||||
@PostMapping("/instance/{id}/reroute")
|
||||
public void instanceReroute(BoaUser user, @PathVariable("id") Long id) {
|
||||
routerAdminService.reroute(adminGuard.requireAdmin(user, null), id);
|
||||
public void instanceReroute(BoaUser user, @PathVariable("id") Long id,
|
||||
@RequestParam(value = "useLatestDef", required = false, defaultValue = "false")
|
||||
boolean useLatestDef) {
|
||||
routerAdminService.reroute(adminGuard.requireAdmin(user, null), id, useLatestDef);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -462,18 +466,34 @@ public class FlowAdminController {
|
||||
|
||||
// ==================== 办件查询 ====================
|
||||
|
||||
/**
|
||||
* 管理端办件分页(F11-4 增 hung 筛选与行级红标):hung=true 只看路由挂起办件
|
||||
* (口径见 {@link com.yunhai.flowcenter.repository.FlwInstanceRepository#pageAdmin});
|
||||
* 每行回填 {@code routeHung}(按页一次 {@code findHungInstanceIds} 查询,非逐行 N+1)。
|
||||
*/
|
||||
@GetMapping("/instance/page")
|
||||
public BasePageResult<FlwInstance> instancePage(
|
||||
BoaUser user,
|
||||
@RequestParam(value = "status", required = false) Integer status,
|
||||
@RequestParam(value = "keyword", required = false) String keyword,
|
||||
@RequestParam(value = "hung", required = false) Boolean hung,
|
||||
@RequestParam(value = "page", required = false) Integer page,
|
||||
@RequestParam(value = "size", required = false) Integer size) {
|
||||
Long schId = adminGuard.requireAdmin(user, null);
|
||||
Page<FlwInstance> p = instanceRepository.pageAdmin(schId, status, keyword,
|
||||
Page<FlwInstance> p = instanceRepository.pageAdmin(schId, status, keyword, hung,
|
||||
PageRequest.of(page == null || page < 1 ? 0 : page - 1,
|
||||
size == null || size < 1 || size > 100 ? 10 : size,
|
||||
Sort.by(Sort.Order.desc("id"))));
|
||||
List<Long> pageIds = new java.util.ArrayList<>();
|
||||
for (FlwInstance i : p.getContent()) {
|
||||
pageIds.add(i.getId());
|
||||
}
|
||||
// 行级红标:不在挂起集合内的行显式置 false(前端只判 truthy,null 也行,但显式更可读)
|
||||
java.util.Set<Long> hungIds = pageIds.isEmpty() ? java.util.Collections.emptySet()
|
||||
: new java.util.HashSet<>(routeLogRepository.findHungInstanceIds(pageIds));
|
||||
for (FlwInstance i : p.getContent()) {
|
||||
i.setRouteHung(hungIds.contains(i.getId()));
|
||||
}
|
||||
int pi = page == null || page < 1 ? 1 : page;
|
||||
int ps = size == null || size < 1 || size > 100 ? 10 : size;
|
||||
return new BasePageResult<>(p.getContent(), (int) p.getTotalElements(), pi, ps,
|
||||
|
||||
@@ -111,8 +111,9 @@ public class OapiFlowController {
|
||||
@RequestParam(value = "bizType", required = false) String bizType,
|
||||
@RequestParam(value = "bizId", required = false) String bizId,
|
||||
@RequestParam(value = "mode", required = false) String mode,
|
||||
@RequestParam(value = "taskId", required = false) Long taskId,
|
||||
@RequestParam("schId") Long schId) {
|
||||
return flowService.embedUrl(schId, instanceId, bizType, bizId, mode);
|
||||
return flowService.embedUrl(schId, instanceId, bizType, bizId, mode, taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -128,4 +128,12 @@ public class FlwInstance extends BaseEntity {
|
||||
@Version
|
||||
@Column(name = "version", nullable = false)
|
||||
private Integer version;
|
||||
|
||||
/**
|
||||
* 台账展示位(F11-4):该办件是否「路由挂起」(审批中且最新 attempt 的路由判断行无出口)。
|
||||
* 非持久列——由管理端办件分页(FlowAdminController.instancePage)按页查询后填充,
|
||||
* 随实体序列化给前端画红标;业务代码不得读写该字段。
|
||||
*/
|
||||
@javax.persistence.Transient
|
||||
private Boolean routeHung;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,8 @@ public final class FlwEnums {
|
||||
public static final String NODE_RESUBMIT = "resubmit";
|
||||
/** 留痕事件行保留节点 id:撤回 */
|
||||
public static final String NODE_WITHDRAW = "withdraw";
|
||||
/** 留痕事件行保留节点 id:管理端按新版定义重跑路由(F5-4,nodeName 载明 v旧→v新) */
|
||||
public static final String NODE_REROUTE_DEF = "reroute-def";
|
||||
|
||||
private final int code;
|
||||
|
||||
|
||||
@@ -42,6 +42,12 @@ public final class OapiModels {
|
||||
private Map<String, Object> formData;
|
||||
/** 发起时指派(可选,2026-10-13):[{nodeId, userIds}],校验失败业务码拒绝发起 */
|
||||
private List<AssignItem> assigns;
|
||||
/**
|
||||
* 期望的定义版本(可选,2026-10-17 S2.7 版本竞态):调用方拿 apply/preview 时回带的
|
||||
* {@code defVersion} 原样传回。不匹配即业务码拒绝,报「流程已更新,请刷新后重新选择审批人」,
|
||||
* 不再让选人结果落到改版后的线上而报「指派节点不存在」。不传=现状行为逐字不变(存量零影响)。
|
||||
*/
|
||||
private Integer expectedDefVersion;
|
||||
}
|
||||
|
||||
/** 发起时指派项(apply 可选字段) */
|
||||
@@ -55,6 +61,11 @@ public final class OapiModels {
|
||||
@Data
|
||||
public static class ApplyPreviewVo {
|
||||
private String flowGuid;
|
||||
/**
|
||||
* 本次预览依据的已发布定义版本(2026-10-17 S2.7):调用方在 apply 时以
|
||||
* {@code expectedDefVersion} 原样回带,改版即人类可读拒绝而非「指派节点不存在」。
|
||||
*/
|
||||
private Integer defVersion;
|
||||
private List<PreviewNodeVo> nodes;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,9 @@ import java.util.Collections;
|
||||
* - 任务被办理:todo -1(办理人)
|
||||
* - 完成/拒绝/撤回:msgType=1、bizType=flow.result、content 必填(结果+意见)、msgNo 带 version
|
||||
* (重提重启多轮终态互不幂等吞掉)
|
||||
* - 退回:bizType=flow.returned、msgNo 带 version,通知被退回节点的新待办人
|
||||
* 结果/退回与待办/抄送同跳转策略(2026-10-17 F8 缺口①):bizUrl 优先 + jumpApp=applyAppCode,
|
||||
* 空回落中心页且归属留空——四类消息的页面归属口径从此一致。
|
||||
* - 催办:bizType=flow.urge、msgNo=urge:{taskId}:{第N次}
|
||||
* 全部在 AFTER_COMMIT 监听器里发送,失败不阻断(UserMsgClient 内部容错)。
|
||||
*/
|
||||
@@ -77,9 +80,8 @@ public class UserMsgNotifier {
|
||||
// 按_by-key 换得的 app_code,接收方按它查基址拼绝对地址);applyAppCode 空(老数据/
|
||||
// UserBase 不可达)或未传 bizUrl 回落流程中心页(/task/{taskId}、/instance/{id}),
|
||||
// 页面属自己(jumpApp 留空按发送方回落)。bizType 是消息分类,不是路由键,不再冒充。
|
||||
boolean useBizUrl = instance.getBizUrl() != null && !instance.getBizUrl().trim().isEmpty();
|
||||
String jumpPath = useBizUrl ? instance.getBizUrl() : "/task/" + task.getId();
|
||||
String jumpApp = useBizUrl ? instance.getApplyAppCode() : null;
|
||||
String jumpPath = jumpPath(instance, "/task/" + task.getId());
|
||||
String jumpApp = jumpApp(instance);
|
||||
userMsgClient.sendUserMsg(instance.getSchId().intValue(),
|
||||
Collections.singletonList(task.getAssigneeId()), 2,
|
||||
"flow.task", String.valueOf(instance.getId()), title, content,
|
||||
@@ -124,7 +126,8 @@ public class UserMsgNotifier {
|
||||
userMsgClient.sendUserMsg(instance.getSchId().intValue(), targets, 1,
|
||||
"flow.returned", String.valueOf(instance.getId()),
|
||||
"办件被退回:" + safe(instance.getBizTitle()), content.toString(),
|
||||
"/instance/" + instance.getId(), null,
|
||||
// F8 缺口①:退回消息与待办/抄送同策略——业务方详情页优先、归属申报给 apply 调用方
|
||||
jumpPath(instance, "/instance/" + instance.getId()), jumpApp(instance),
|
||||
"returned:" + instance.getId() + ":" + instance.getVersion(), 30);
|
||||
}
|
||||
|
||||
@@ -147,14 +150,11 @@ public class UserMsgNotifier {
|
||||
? "" : "(" + nodeName + ")");
|
||||
// 跳转与任务到达同策略:bizUrl(业务方详情页,页面归属=apply 调用方 applyAppCode)优先,
|
||||
// 空回落办件详情(属自己)
|
||||
boolean useBizUrl = instance.getBizUrl() != null && !instance.getBizUrl().trim().isEmpty();
|
||||
String jumpPath = useBizUrl ? instance.getBizUrl() : "/instance/" + instance.getId();
|
||||
String jumpApp = useBizUrl ? instance.getApplyAppCode() : null;
|
||||
userMsgClient.sendUserMsg(instance.getSchId().intValue(),
|
||||
Collections.singletonList((Integer) ccUserId), 1,
|
||||
"flow.cc", String.valueOf(instance.getId()),
|
||||
"您有新的抄送:" + bizTitle, content,
|
||||
jumpPath, jumpApp, "cc:" + ccId, 30);
|
||||
jumpPath(instance, "/instance/" + instance.getId()), jumpApp(instance), "cc:" + ccId, 30);
|
||||
}
|
||||
|
||||
private void notifyResult(FlowEvent event) {
|
||||
@@ -188,7 +188,9 @@ public class UserMsgNotifier {
|
||||
Collections.singletonList(instance.getSubmitterId()), 1,
|
||||
"flow.result", String.valueOf(instance.getId()),
|
||||
"办件状态更新:" + bizTitle, content.toString(),
|
||||
"/instance/" + instance.getId(), null,
|
||||
// F8 缺口①:结果消息与待办/抄送同策略——业务方详情页优先(子应用详情页 iframe
|
||||
// 显终态进度),jumpApp 申报给 apply 调用方;bizUrl 空仍回落中心办件详情
|
||||
jumpPath(instance, "/instance/" + instance.getId()), jumpApp(instance),
|
||||
"result:" + instance.getId() + ":" + instance.getVersion(), 30);
|
||||
}
|
||||
|
||||
@@ -209,6 +211,23 @@ public class UserMsgNotifier {
|
||||
"/task/" + task.getId(), null, "urge:" + task.getId() + ":" + count, 7);
|
||||
}
|
||||
|
||||
/**
|
||||
* 四类消息(待办/抄送/结果/退回)共用的跳转路径策略:办件 bizUrl 优先,空/空白回落中心页。
|
||||
* bizUrl 自 2026-09-26 起是裸相对路径(不含域名/ctx/#),接收方按 jumpApp 查基址拼绝对地址。
|
||||
*/
|
||||
private static String jumpPath(FlwInstance instance, String centerFallback) {
|
||||
return hasBizUrl(instance) ? instance.getBizUrl() : centerFallback;
|
||||
}
|
||||
|
||||
/** 页面归属:走 bizUrl 时申报给 apply 调用方(applyAppCode),回落中心页时留空按发送方归属 */
|
||||
private static String jumpApp(FlwInstance instance) {
|
||||
return hasBizUrl(instance) ? instance.getApplyAppCode() : null;
|
||||
}
|
||||
|
||||
private static boolean hasBizUrl(FlwInstance instance) {
|
||||
return instance.getBizUrl() != null && !instance.getBizUrl().trim().isEmpty();
|
||||
}
|
||||
|
||||
private FlwInstance instanceOf(Long instanceId) {
|
||||
return instanceId == null ? null : instanceRepository.findById(instanceId).orElse(null);
|
||||
}
|
||||
|
||||
@@ -20,4 +20,7 @@ public interface FlwCcRepository extends JpaRepository<FlwCc, Long> {
|
||||
|
||||
/** 办件轨迹/进度留痕 */
|
||||
List<FlwCc> findByInstanceIdOrderByIdAsc(Long instanceId);
|
||||
|
||||
/** 可见性守卫用(F7 前置补丁):本办件是否抄送过该人——只读可见,不授予办理权 */
|
||||
boolean existsByInstanceIdAndUserId(Long instanceId, Integer userId);
|
||||
}
|
||||
|
||||
@@ -27,12 +27,23 @@ public interface FlwInstanceRepository extends JpaRepository<FlwInstance, Long>
|
||||
@Param("status") Integer status, @Param("appKey") String appKey,
|
||||
Pageable pageable);
|
||||
|
||||
/** 管理端办件分页(状态/关键字可空) */
|
||||
/**
|
||||
* 管理端办件分页(状态/关键字可空)。
|
||||
* hung=true 只看「路由挂起」:审批中(status=10)且存在某 (instance,node) 的<b>最大 attempt</b>
|
||||
* flw_route_log 行 outlet 仍为 NULL(与 {@link com.yunhai.flowcenter.repository.FlwRouteLogRepository#findHungInstanceIds}
|
||||
* 同口径——历史低 attempt 失败行不参与判定,防「重跑已成功仍报挂起」误报);hung=null/false 不过滤。
|
||||
*/
|
||||
@Query("select i from FlwInstance i where i.schId = :schId "
|
||||
+ "and (:status is null or i.status = :status) "
|
||||
+ "and (:keyword is null or i.bizTitle like concat('%', :keyword, '%')) order by i.id desc")
|
||||
+ "and (:keyword is null or i.bizTitle like concat('%', :keyword, '%')) "
|
||||
+ "and (:hung is null or :hung = false or (i.status = 10 and exists ("
|
||||
+ "select r.id from FlwRouteLog r where r.instanceId = i.id and r.outlet is null "
|
||||
+ "and r.attempt = (select max(r2.attempt) from FlwRouteLog r2 "
|
||||
+ "where r2.instanceId = r.instanceId and r2.nodeId = r.nodeId)))) "
|
||||
+ "order by i.id desc")
|
||||
Page<FlwInstance> pageAdmin(@Param("schId") Long schId, @Param("status") Integer status,
|
||||
@Param("keyword") String keyword, Pageable pageable);
|
||||
@Param("keyword") String keyword, @Param("hung") Boolean hung,
|
||||
Pageable pageable);
|
||||
|
||||
/** 按业务类型/状态分页(oapi instances 查询;bizType/status 均可空=不过滤) */
|
||||
@Query("select i from FlwInstance i where i.schId = :schId "
|
||||
|
||||
@@ -5,6 +5,7 @@ import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public interface FlwRouteLogRepository extends JpaRepository<FlwRouteLog, Long> {
|
||||
@@ -16,4 +17,16 @@ public interface FlwRouteLogRepository extends JpaRepository<FlwRouteLog, Long>
|
||||
@Query("select coalesce(max(l.attempt), 0) from FlwRouteLog l "
|
||||
+ "where l.instanceId = :instanceId and l.nodeId = :nodeId")
|
||||
int maxAttempt(@Param("instanceId") Long instanceId, @Param("nodeId") String nodeId);
|
||||
|
||||
/**
|
||||
* 台账「路由挂起」红标数据源(F11-4):页内办件中仍挂起的 instanceId 集合。
|
||||
* 判定口径:某 (instance, node) 的<b>最大 attempt</b> 行 outlet 仍为 NULL 才算挂着——
|
||||
* 退回重走/管理端重跑会写更高 attempt 的新行,历史低 attempt 的失败行(outlet NULL)不参与判定,
|
||||
* 防「已重跑成功仍报挂起」的误报。
|
||||
*/
|
||||
@Query("select distinct r.instanceId from FlwRouteLog r where r.instanceId in :instanceIds "
|
||||
+ "and r.outlet is null "
|
||||
+ "and r.attempt = (select max(r2.attempt) from FlwRouteLog r2 "
|
||||
+ "where r2.instanceId = r.instanceId and r2.nodeId = r.nodeId)")
|
||||
List<Long> findHungInstanceIds(@Param("instanceIds") Collection<Long> instanceIds);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.yunhai.flowcenter.service;
|
||||
|
||||
import com.yunhai.flowcenter.entity.FlwInstance;
|
||||
import com.yunhai.flowcenter.repository.FlwCcRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwTaskRepository;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
@@ -14,6 +15,8 @@ import java.util.List;
|
||||
/**
|
||||
* 用户面访问守卫(概览 §十):提交人只读写自己办件;办理人只读写分派给自己的任务;
|
||||
* 管理员本校可查、不可代批。
|
||||
* 2026-10-17(F7 前置补丁):放行口径增「该办件 flw_cc 行接收人」——抄送接收人点开工作台
|
||||
* "抄送我的"行或抄送消息回落链接此前一律 403,抄送自留因此不闭环。只读可见,办理权仍按 assigneeId。
|
||||
* 管理员口径与 FlowAdminGuard / svc-hall ApplyViewGuard / Notice NoticeFlowGuard 对齐:
|
||||
* type==1(超管)或 roleId∈{2,5,7}(校级管理员)——三者必须同口径,否则校级管理员
|
||||
* 能列办件却在详情页 403(本类原先只认 type==1,2026-09-14 校正)。
|
||||
@@ -26,8 +29,9 @@ public class FlowAccessGuard {
|
||||
private static final List<Integer> ADMIN_ROLE_IDS = Arrays.asList(2, 5, 7);
|
||||
|
||||
private final FlwTaskRepository taskRepository;
|
||||
private final FlwCcRepository ccRepository;
|
||||
|
||||
/** 办件详情可见:提交人本人 / 该办件任务办理人 / 本校管理员(超管或校级管理员) */
|
||||
/** 办件详情可见:提交人本人 / 该办件任务办理人 / 本校管理员(超管或校级管理员)/ 该办件抄送接收人 */
|
||||
public void requireInstanceVisible(FlwInstance instance, BoaUser user) {
|
||||
if (user == null || user.getId() == null) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "未登录");
|
||||
@@ -37,12 +41,19 @@ public class FlowAccessGuard {
|
||||
}
|
||||
if (!taskRepository.findByInstanceIdOrderByIdAsc(instance.getId()).stream()
|
||||
.anyMatch(t -> user.getId().equals(t.getAssigneeId()))) {
|
||||
if (!isAdminOf(instance, user)) {
|
||||
// F7 前置补丁:抄送接收人可读(工作台"抄送我的"行与抄送消息回落链接不再 403)。
|
||||
// 只授予可见,办理权仍在 FlowTaskService 按 assigneeId 复核。
|
||||
if (!isCcRecipient(instance, user) && !isAdminOf(instance, user)) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "仅提交人/办理人/本校管理员可查看该办件");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 该用户是否本办件的抄送接收人(flw_cc 行,逐人一行) */
|
||||
private boolean isCcRecipient(FlwInstance instance, BoaUser user) {
|
||||
return ccRepository.existsByInstanceIdAndUserId(instance.getId(), user.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 本校管理员(可查不可代批;办理权在 FlowTaskService 里再按 assigneeId 校验)。
|
||||
* 超管与校级管理员均限本校——跨校仍 403,不放大可见范围。
|
||||
|
||||
@@ -33,6 +33,7 @@ public class FlowRouterAdminService {
|
||||
private final FlwRouterRepository routerRepository;
|
||||
private final FlwInstanceRepository instanceRepository;
|
||||
private final FlwDefRepository defRepository;
|
||||
private final com.yunhai.flowcenter.repository.FlwTaskRepository taskRepository;
|
||||
private final FlowEngine flowEngine;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@@ -153,9 +154,23 @@ public class FlowRouterAdminService {
|
||||
/**
|
||||
* 重跑路由(挂起恢复唯一入口,§4.4):办件须在途且挂在 CONDITION 节点(currentNodes 含之、
|
||||
* 该节点无待办任务)。重新调判断,成功即续走推进;仍失败抛业务错(挂起不变,留痕已落新行)。
|
||||
* 默认按办件落库的 def 快照重跑(现行为逐字不变)。
|
||||
*/
|
||||
@Transactional
|
||||
public void reroute(Long schId, Long instanceId) {
|
||||
reroute(schId, instanceId, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重跑路由(F5-4 僵尸逃生门):useLatestDef=true 时 def 改取该 flowGuid 当前已发布最高版本,
|
||||
* 办件 defId/defCode/defVersion 快照同步刷新(与 R4 重启同语义,只刷定义三列,不动业务快照/
|
||||
* 状态/时间),trace 落一行留痕(保留节点 id=reroute-def,nodeName 载明 v旧→v新)。
|
||||
* 锚定 128/129 僵尸案例:办件挂在 v1 的 CONDITION 节点、修好的表达式已发布为 v2,按 def 快照
|
||||
* 重跑永远打转——此开关让管理员一键切到 v2 续走。在途 CONDITION 节点在新定义不存在时
|
||||
* 按人类可读业务错拒绝(不猜测节点映射)。useLatestDef=false 现行为逐字不变。
|
||||
*/
|
||||
@Transactional
|
||||
public void reroute(Long schId, Long instanceId, boolean useLatestDef) {
|
||||
FlwInstance instance = instanceRepository.findById(instanceId)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "办件不存在:" + instanceId));
|
||||
if (!instance.getSchId().equals(schId)) {
|
||||
@@ -168,15 +183,58 @@ public class FlowRouterAdminService {
|
||||
if (def == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "办件定义缺失");
|
||||
}
|
||||
if (useLatestDef) {
|
||||
def = refreshToLatestPublishedDef(instance, def);
|
||||
}
|
||||
String conditionNode = findParkedCondition(instance, def);
|
||||
if (conditionNode == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR,
|
||||
"办件未挂在条件分支节点(无挂起的路由判断,无需重跑)");
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, useLatestDef
|
||||
? "在途条件节点在最新已发布定义中不存在(节点已被改版移除/改型),无法按新定义重跑"
|
||||
: "办件未挂在条件分支节点(无挂起的路由判断,无需重跑)");
|
||||
}
|
||||
flowEngine.rerouteCondition(instance, def, conditionNode);
|
||||
instanceRepository.save(instance);
|
||||
}
|
||||
|
||||
/** 取 flowGuid 当前已发布最高版本并刷新办件定义快照(仅三列;已是最新则原样返回不落留痕) */
|
||||
private FlwDef refreshToLatestPublishedDef(FlwInstance instance, FlwDef current) {
|
||||
FlwDef latest = defRepository.findFirstBySchIdAndFlowGuidAndStatusOrderByVersionDesc(
|
||||
instance.getSchId(), current.getFlowGuid(), FlwEnums.DefStatus.PUBLISHED.getCode())
|
||||
.orElse(null);
|
||||
if (latest == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "该流程当前无已发布版本,无法按最新定义重跑");
|
||||
}
|
||||
if (latest.getId().equals(instance.getDefId())) {
|
||||
return latest;
|
||||
}
|
||||
instance.setDefId(latest.getId());
|
||||
instance.setDefCode(latest.getCode());
|
||||
instance.setDefVersion(latest.getVersion());
|
||||
appendRerouteDefTrace(instance, current.getVersion(), latest.getVersion());
|
||||
return latest;
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义切换留痕行(同 FlowInstanceService.appendTraceEvent 形态;彼方法私有且该类冻结,不共用):
|
||||
* status=6 + 保留节点 id,nodeName 载明版本迁移;不参与引擎任何判断(同 submit/withdraw 行)。
|
||||
*/
|
||||
private void appendRerouteDefTrace(FlwInstance instance, Integer fromVersion, Integer toVersion) {
|
||||
java.time.LocalDateTime now = java.time.LocalDateTime.now();
|
||||
com.yunhai.flowcenter.entity.FlwTask row = new com.yunhai.flowcenter.entity.FlwTask();
|
||||
row.setInstanceId(instance.getId());
|
||||
row.setSchId(instance.getSchId());
|
||||
row.setNodeId(FlwEnums.TaskStatus.NODE_REROUTE_DEF);
|
||||
row.setNodeName("按新版定义重跑(v" + fromVersion + "→v" + toVersion + ")");
|
||||
// assignee=0:管理端动作无自然办理人,落 0 不产生待办(同发起行脏数据兜底口径)
|
||||
row.setAssigneeId(0);
|
||||
row.setAssigneeName("管理端");
|
||||
row.setStatus(FlwEnums.TaskStatus.SUBMITTED.getCode());
|
||||
row.setArriveTime(now);
|
||||
row.setHandleTime(now);
|
||||
row.setUrgeCount(0);
|
||||
taskRepository.save(row);
|
||||
}
|
||||
|
||||
/** 在途节点里找 CONDITION(多个时取第一个;正常挂起只会有一个) */
|
||||
private String findParkedCondition(FlwInstance instance, FlwDef def) {
|
||||
String cur = instance.getCurrentNodes();
|
||||
|
||||
@@ -120,6 +120,14 @@ public class OapiFlowService {
|
||||
schId, body.getFlowGuid(), FlwEnums.DefStatus.PUBLISHED.getCode())
|
||||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR,
|
||||
"流程不存在或未发布:" + body.getFlowGuid()));
|
||||
// S2.7 版本竞态(可选、opt-in):调用方回带预览时的定义版本,改版即人类可读拒绝,
|
||||
// 免得选好的 nodeId/候选人在新版本里对不上而报「指派节点不存在」。不传=现状逐字不变。
|
||||
if (body.getExpectedDefVersion() != null
|
||||
&& !body.getExpectedDefVersion().equals(def.getVersion())) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR,
|
||||
"流程已更新,请刷新后重新选择审批人(期望定义版本 " + body.getExpectedDefVersion()
|
||||
+ ",当前已发布版本 " + def.getVersion() + ")");
|
||||
}
|
||||
String assignJson = renderAssignJson(schId, def, body.getAssigns(), body.getSubmitterId());
|
||||
String cleanAppKey = appKey == null || appKey.trim().isEmpty() ? null : appKey.trim();
|
||||
FlowInstanceService.ApplyCommand cmd = new FlowInstanceService.ApplyCommand(
|
||||
@@ -253,6 +261,7 @@ public class OapiFlowService {
|
||||
FlowDefModel model = FlowDefModel.parse(objectMapper, def.getContentJson());
|
||||
OapiModels.ApplyPreviewVo vo = new OapiModels.ApplyPreviewVo();
|
||||
vo.setFlowGuid(def.getFlowGuid());
|
||||
vo.setDefVersion(def.getVersion());
|
||||
List<OapiModels.PreviewNodeVo> nodes = new ArrayList<>();
|
||||
for (FlowDefModel.Node node : model.nodes) {
|
||||
if (!FlowDefModel.TYPE_APPROVAL.equals(node.type) || node.assignPolicy == null) {
|
||||
@@ -504,13 +513,39 @@ public class OapiFlowService {
|
||||
* 办件进度嵌入页换址(token 模式,与换图同构):同一 token 两处消费——嵌入页数据
|
||||
* /open/flow/embed/data 与流程图 /open/flow/diagram/img 均免签认领该 token。定位二选一:
|
||||
* instanceId(与办件直接复核)/ bizType+bizId(对账口径,办件不存在与 progress 同文案)。
|
||||
* 用户级可见性(本人/办理人/管理员)由子应用后端在调本端点前完成——oapi 层只认 schId。
|
||||
* 用户级可见性(本人/办理人/管理员/该办件抄送接收人)由子应用后端在调本端点前完成——oapi 层只认 schId。
|
||||
* 2026-10-17 F7 前置补丁后,中心用户面口径({@link FlowAccessGuard#requireInstanceVisible})已含抄送
|
||||
* 接收人;签发侧必须与之一致,否则"抄送我的"点开得、进度嵌入页换址却拒发(2026-10-17 记)。
|
||||
*/
|
||||
public OapiModels.EmbedUrlVo embedUrl(Long schId, Long instanceId, String bizType, String bizId, String mode) {
|
||||
return embedUrl(schId, instanceId, bizType, bizId, mode, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* mode=task(F9)直跳中心任务办理页:pageUrl = front-url + {@code #/task/{taskId}},
|
||||
* 无 token(CAS 会话鉴权页,办理权由该页加载时的用户面接口按 assigneeId 复核)——
|
||||
* 子应用"在自家页放办理入口"场景用;taskId 必填,缺则 PARAM_ERROR。
|
||||
*/
|
||||
public OapiModels.EmbedUrlVo embedUrl(Long schId, Long instanceId, String bizType, String bizId,
|
||||
String mode, Long taskId) {
|
||||
// 配置缺失优先于办件定位:fail-fast 纪律要的是"无论如何都不发打不开的地址"
|
||||
if (embedFrontUrl == null || embedFrontUrl.trim().isEmpty()) {
|
||||
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "嵌入页地址未配置:flow-center.embed.front-url");
|
||||
}
|
||||
String base = embedFrontUrl.trim();
|
||||
if (!base.endsWith("/")) {
|
||||
base = base + "/";
|
||||
}
|
||||
String normMode = mode == null || mode.trim().isEmpty() ? "auto" : mode.trim();
|
||||
if ("task".equals(normMode)) {
|
||||
if (taskId == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "mode=task 需要 taskId(任务办理页定位)");
|
||||
}
|
||||
OapiModels.EmbedUrlVo out = new OapiModels.EmbedUrlVo();
|
||||
out.setPageUrl(base + "#/task/" + taskId);
|
||||
out.setExpiresIn(0);
|
||||
return out;
|
||||
}
|
||||
Long resolvedInstanceId;
|
||||
if (instanceId != null) {
|
||||
resolvedInstanceId = requireInstance(schId, instanceId).getId();
|
||||
@@ -525,12 +560,7 @@ public class OapiFlowService {
|
||||
resolvedInstanceId = vo.getInstanceId();
|
||||
}
|
||||
// mode 与 DiagramTokenService 内部缺省同值("auto"),URL 里显式带上仅为前端展示冗余
|
||||
String normMode = mode == null || mode.trim().isEmpty() ? "auto" : mode.trim();
|
||||
String token = tokenService.issue(schId, resolvedInstanceId, null, normMode);
|
||||
String base = embedFrontUrl.trim();
|
||||
if (!base.endsWith("/")) {
|
||||
base = base + "/";
|
||||
}
|
||||
OapiModels.EmbedUrlVo out = new OapiModels.EmbedUrlVo();
|
||||
out.setPageUrl(base + "#/embed/instance?token=" + token + "&mode=" + normMode);
|
||||
out.setExpiresIn(DiagramTokenService.TTL_MS / 1000);
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package com.yunhai.flowcenter.controller.api;
|
||||
|
||||
import com.yunhai.flowcenter.entity.FlwInstance;
|
||||
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwRouteLogRepository;
|
||||
import com.yunhai.flowcenter.service.FlowAdminGuard;
|
||||
import com.yunhai.platform.utils.model.BasePageResult;
|
||||
import com.yunhai.platform.utils.model.BoaUser;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* F11-4 台账「路由挂起」可观测单测:hung 筛选透传 + 行级 routeHung 红标三态
|
||||
* (在途+挂起 / 在途未挂 / 已终态)。挂起判定 JPQL 口径(最大 attempt 行 outlet 仍 NULL)
|
||||
* 在 {@link FlwRouteLogRepository#findHungInstanceIds} 与 pageAdmin 的 exists 子查询里,
|
||||
* 语义断言由 160 人工验收(V 过滤)与真库回归兜住;本测锁定控制器编排与红标填充。
|
||||
*/
|
||||
class FlowAdminInstanceHungTest {
|
||||
|
||||
private final FlowAdminGuard adminGuard = mock(FlowAdminGuard.class);
|
||||
private final FlwInstanceRepository instanceRepository = mock(FlwInstanceRepository.class);
|
||||
private final FlwRouteLogRepository routeLogRepository = mock(FlwRouteLogRepository.class);
|
||||
|
||||
private final FlowAdminController controller = new FlowAdminController(
|
||||
adminGuard,
|
||||
mock(com.yunhai.flowcenter.service.FlowDefAdminService.class),
|
||||
mock(com.yunhai.flowcenter.service.FlowMapAdminService.class),
|
||||
mock(com.yunhai.flowcenter.service.FlowCallbackAdminService.class),
|
||||
mock(com.yunhai.flowcenter.service.FlowRouterAdminService.class),
|
||||
instanceRepository,
|
||||
mock(com.yunhai.flowcenter.service.FlowDiagramService.class),
|
||||
mock(com.yunhai.flowcenter.service.DiagramImageService.class),
|
||||
mock(com.fasterxml.jackson.databind.ObjectMapper.class),
|
||||
mock(com.yunhai.flowcenter.service.FlowAppAdminService.class),
|
||||
mock(com.yunhai.flowcenter.service.FlowDeptAdminService.class),
|
||||
routeLogRepository);
|
||||
|
||||
private static FlwInstance instance(long id, int status) {
|
||||
FlwInstance i = new FlwInstance();
|
||||
i.setId(id);
|
||||
i.setStatus(status);
|
||||
return i;
|
||||
}
|
||||
|
||||
@Test
|
||||
void hungFilterPassedThroughToRepository() {
|
||||
when(adminGuard.requireAdmin(any(), any())).thenReturn(990001L);
|
||||
when(instanceRepository.pageAdmin(eq(990001L), any(), any(), eq(Boolean.TRUE), any()))
|
||||
.thenReturn(new PageImpl<>(Arrays.asList(instance(1, 10))));
|
||||
when(routeLogRepository.findHungInstanceIds(any())).thenReturn(Arrays.asList());
|
||||
|
||||
controller.instancePage(new BoaUser(), null, null, Boolean.TRUE, 1, 10);
|
||||
|
||||
ArgumentCaptor<Boolean> hung = ArgumentCaptor.forClass(Boolean.class);
|
||||
verify(instanceRepository).pageAdmin(eq(990001L), isNull(), isNull(), hung.capture(), any());
|
||||
assertEquals(Boolean.TRUE, hung.getValue(), "hung=true 必须原样透传到分页查询");
|
||||
}
|
||||
|
||||
@Test
|
||||
void routeHungBadgeThreeStates() {
|
||||
when(adminGuard.requireAdmin(any(), any())).thenReturn(990001L);
|
||||
// 三行:1=在途且在挂起集合;2=在途但不在集合;3=已终态(集合必然不含)
|
||||
when(instanceRepository.pageAdmin(eq(990001L), any(), any(), any(), any()))
|
||||
.thenReturn(new PageImpl<>(Arrays.asList(instance(1, 10), instance(2, 10), instance(3, 20))));
|
||||
when(routeLogRepository.findHungInstanceIds(any()))
|
||||
.thenReturn(Arrays.asList(1L));
|
||||
|
||||
BasePageResult<FlwInstance> result = controller.instancePage(new BoaUser(), null, null, null, 1, 10);
|
||||
|
||||
List<FlwInstance> rows = result.getContent();
|
||||
assertEquals(3, rows.size());
|
||||
assertTrue(rows.get(0).getRouteHung(), "在途且最新 attempt 路由行无出口 → 红标 true");
|
||||
assertFalse(rows.get(1).getRouteHung(), "在途但无挂起路由行 → false");
|
||||
assertFalse(rows.get(2).getRouteHung(), "已终态办件不参与挂起判定 → false");
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyPageSkipsHungLookup() {
|
||||
when(adminGuard.requireAdmin(any(), any())).thenReturn(990001L);
|
||||
when(instanceRepository.pageAdmin(eq(990001L), any(), any(), any(), any()))
|
||||
.thenReturn(new PageImpl<>(Arrays.asList()));
|
||||
|
||||
controller.instancePage(new BoaUser(), 10, null, null, 1, 10);
|
||||
|
||||
verify(routeLogRepository, never()).findHungInstanceIds(any());
|
||||
}
|
||||
}
|
||||
@@ -151,4 +151,75 @@ class UserMsgNotifierBizUrlTest {
|
||||
assertEquals("/task/78", userMsg.lastJumpPath);
|
||||
assertNull(userMsg.lastJumpApp);
|
||||
}
|
||||
|
||||
/**
|
||||
* F8 缺口①:结果消息(completed/rejected/withdrawn)不再硬编码归流程中心,
|
||||
* 与待办/抄送同策略——bizUrl 优先 + jumpApp=applyAppCode。
|
||||
*/
|
||||
@Test
|
||||
void resultMessage_followsBizUrlStrategy() {
|
||||
FlwInstance instance = instance(11L, "/leave/detail?id=88", "notice");
|
||||
when(instanceRepository.findById(11L)).thenReturn(Optional.of(instance));
|
||||
|
||||
notifier.onFlowEvent(FlowEvent.of(FlowEvent.Type.COMPLETED, 11L, null,
|
||||
java.util.Collections.singletonMap("result", "通过")));
|
||||
|
||||
assertEquals("/leave/detail?id=88", userMsg.lastJumpPath);
|
||||
assertEquals("notice", userMsg.lastJumpApp);
|
||||
}
|
||||
|
||||
/** 存量兼容:老办件无 bizUrl 时结果消息仍回落中心办件详情、归属留空 */
|
||||
@Test
|
||||
void resultMessageWithoutBizUrl_fallsBackToCenterInstancePage() {
|
||||
FlwInstance instance = instance(12L, " ", "notice");
|
||||
when(instanceRepository.findById(12L)).thenReturn(Optional.of(instance));
|
||||
|
||||
notifier.onFlowEvent(FlowEvent.of(FlowEvent.Type.REJECTED, 12L));
|
||||
|
||||
assertEquals("/instance/12", userMsg.lastJumpPath);
|
||||
assertNull(userMsg.lastJumpApp);
|
||||
}
|
||||
|
||||
/** F8 缺口①:退回消息同策略(四类消息归属口径从此一致) */
|
||||
@Test
|
||||
void returnedMessage_followsBizUrlStrategy() {
|
||||
FlwInstance instance = instance(13L, "/hall/detail?id=9", "svc-hall");
|
||||
when(instanceRepository.findById(13L)).thenReturn(Optional.of(instance));
|
||||
java.util.Map<String, Object> data = new java.util.HashMap<>();
|
||||
data.put("targetUserIds", java.util.Arrays.asList(201, 202));
|
||||
data.put("fromNodeName", "科室初审");
|
||||
data.put("opinion", "请补材料");
|
||||
|
||||
notifier.onFlowEvent(FlowEvent.of(FlowEvent.Type.RETURNED, 13L, null, data));
|
||||
|
||||
assertEquals("/hall/detail?id=9", userMsg.lastJumpPath);
|
||||
assertEquals("svc-hall", userMsg.lastJumpApp);
|
||||
}
|
||||
|
||||
/** 抄送消息(原已同策略)纳入同一断言口径,防后续回归 */
|
||||
@Test
|
||||
void ccMessage_followsBizUrlStrategy() {
|
||||
FlwInstance instance = instance(14L, "/notice/detail?serid=3", "notice");
|
||||
when(instanceRepository.findById(14L)).thenReturn(Optional.of(instance));
|
||||
java.util.Map<String, Object> data = new java.util.HashMap<>();
|
||||
data.put("ccUserId", 301);
|
||||
data.put("ccId", 55);
|
||||
data.put("nodeName", "抄送分管领导");
|
||||
|
||||
notifier.onFlowEvent(FlowEvent.of(FlowEvent.Type.CC_SENT, 14L, null, data));
|
||||
|
||||
assertEquals("/notice/detail?serid=3", userMsg.lastJumpPath);
|
||||
assertEquals("notice", userMsg.lastJumpApp);
|
||||
}
|
||||
|
||||
private FlwInstance instance(long id, String bizUrl, String applyAppCode) {
|
||||
FlwInstance instance = new FlwInstance();
|
||||
instance.setId(id);
|
||||
instance.setSchId(1L);
|
||||
instance.setBizTitle("测试办件");
|
||||
instance.setSubmitterId(101);
|
||||
instance.setBizUrl(bizUrl);
|
||||
instance.setApplyAppCode(applyAppCode);
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.yunhai.flowcenter.service;
|
||||
|
||||
import com.yunhai.flowcenter.entity.FlwInstance;
|
||||
import com.yunhai.flowcenter.entity.FlwTask;
|
||||
import com.yunhai.flowcenter.repository.FlwCcRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwTaskRepository;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.utils.model.BoaUser;
|
||||
@@ -15,6 +16,7 @@ import java.util.Collections;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -26,15 +28,18 @@ import static org.mockito.Mockito.when;
|
||||
class FlowAccessGuardTest {
|
||||
|
||||
private FlwTaskRepository taskRepository;
|
||||
private FlwCcRepository ccRepository;
|
||||
private FlowAccessGuard guard;
|
||||
private FlwInstance instance;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
taskRepository = mock(FlwTaskRepository.class);
|
||||
guard = new FlowAccessGuard(taskRepository);
|
||||
ccRepository = mock(FlwCcRepository.class);
|
||||
guard = new FlowAccessGuard(taskRepository, ccRepository);
|
||||
when(taskRepository.findByInstanceIdOrderByIdAsc(anyLong()))
|
||||
.thenReturn(Collections.emptyList());
|
||||
when(ccRepository.existsByInstanceIdAndUserId(anyLong(), anyInt())).thenReturn(false);
|
||||
|
||||
instance = new FlwInstance();
|
||||
ReflectionTestUtils.setField(instance, "id", 100L);
|
||||
@@ -94,4 +99,29 @@ class FlowAccessGuardTest {
|
||||
assertThrows(BizException.class,
|
||||
() -> guard.requireInstanceVisible(instance, user(37, 1, null)));
|
||||
}
|
||||
|
||||
/** F7 前置补丁:抄送接收人只读可见(工作台"抄送我的"行、抄送消息回落链接不再 403) */
|
||||
@Test
|
||||
void ccRecipientCanView() {
|
||||
when(ccRepository.existsByInstanceIdAndUserId(100L, 44)).thenReturn(true);
|
||||
|
||||
assertDoesNotThrow(() -> guard.requireInstanceVisible(instance, user(44, 2, 990001, 9)));
|
||||
}
|
||||
|
||||
/** 陌生人(非提交人/办理人/管理员/抄送人)仍 403——补丁不放大可见范围 */
|
||||
@Test
|
||||
void strangerSameSchoolIsStillDenied() {
|
||||
BizException e = assertThrows(BizException.class,
|
||||
() -> guard.requireInstanceVisible(instance, user(45, 2, 990001, 9)));
|
||||
assertEquals("仅提交人/办理人/本校管理员可查看该办件", e.getMessage());
|
||||
}
|
||||
|
||||
/** 跨校抄送接收人不存在即拒;抄送行必须命中本办件(instanceId 维度,不按 schId 泛化) */
|
||||
@Test
|
||||
void ccLookupIsScopedToInstance() {
|
||||
when(ccRepository.existsByInstanceIdAndUserId(100L, 46)).thenReturn(false);
|
||||
|
||||
assertThrows(BizException.class,
|
||||
() -> guard.requireInstanceVisible(instance, user(46, 2, 990001, 9)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,8 +389,10 @@ class FlowDiagramServiceTest {
|
||||
taskService = mock(FlowTaskService.class);
|
||||
instanceService = mock(FlowInstanceService.class);
|
||||
diagramService = mock(FlowDiagramService.class);
|
||||
// 真守卫(同 FlowAccessGuardTest 口径):提交人/办理人/本校管理员
|
||||
FlowAccessGuard accessGuard = new FlowAccessGuard(taskRepository);
|
||||
// 真守卫(同 FlowAccessGuardTest 口径):提交人/办理人/本校管理员/抄送接收人
|
||||
// 抄送分支本组用例不覆盖,空 mock 即 false(陌生人仍 403 的断言不受影响)
|
||||
FlowAccessGuard accessGuard = new FlowAccessGuard(taskRepository,
|
||||
mock(com.yunhai.flowcenter.repository.FlwCcRepository.class));
|
||||
controller = new FlowUserController(queryService, taskService, instanceService,
|
||||
accessGuard, diagramService, mock(com.yunhai.flowcenter.service.DiagramImageService.class));
|
||||
}
|
||||
@@ -491,7 +493,8 @@ class FlowDiagramServiceTest {
|
||||
mock(com.yunhai.flowcenter.service.DiagramImageService.class),
|
||||
new ObjectMapper(),
|
||||
mock(com.yunhai.flowcenter.service.FlowAppAdminService.class),
|
||||
mock(com.yunhai.flowcenter.service.FlowDeptAdminService.class));
|
||||
mock(com.yunhai.flowcenter.service.FlowDeptAdminService.class),
|
||||
mock(com.yunhai.flowcenter.repository.FlwRouteLogRepository.class));
|
||||
}
|
||||
|
||||
private BoaUser user(int id, Integer type, Integer schId, Integer... roleIds) {
|
||||
|
||||
@@ -40,6 +40,7 @@ class FlowRouterAdminServiceRegisterTest {
|
||||
service = new FlowRouterAdminService(routerRepository,
|
||||
mock(com.yunhai.flowcenter.repository.FlwInstanceRepository.class),
|
||||
mock(com.yunhai.flowcenter.repository.FlwDefRepository.class),
|
||||
mock(com.yunhai.flowcenter.repository.FlwTaskRepository.class),
|
||||
mock(com.yunhai.flowcenter.engine.FlowEngine.class),
|
||||
new com.fasterxml.jackson.databind.ObjectMapper());
|
||||
when(routerRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
package com.yunhai.flowcenter.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.yunhai.flowcenter.entity.FlwDef;
|
||||
import com.yunhai.flowcenter.entity.FlwInstance;
|
||||
import com.yunhai.flowcenter.entity.FlwTask;
|
||||
import com.yunhai.flowcenter.enums.FlwEnums;
|
||||
import com.yunhai.flowcenter.repository.FlwDefRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwRouterRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwTaskRepository;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* F5-4 僵尸逃生门(useLatestDef):v1 表达式必失败挂起 → 默认重跑仍按 v1 快照(行为逐字不变);
|
||||
* useLatestDef=true 改取当前已发布最高版本 v2 → 快照刷新 + trace 留痕 + 引擎按 v2 续走;
|
||||
* 在途 CONDITION 节点在新定义不存在时报人类可读业务错。锚定 128/129 僵尸案例。
|
||||
*/
|
||||
@DisplayName("reroute useLatestDef(F5-4 僵尸逃生门)")
|
||||
class FlowRouterAdminServiceRerouteDefTest {
|
||||
|
||||
private static final Long SCH = 990001L;
|
||||
|
||||
private FlwInstanceRepository instanceRepository;
|
||||
private FlwDefRepository defRepository;
|
||||
private FlwTaskRepository taskRepository;
|
||||
private com.yunhai.flowcenter.engine.FlowEngine flowEngine;
|
||||
private FlowRouterAdminService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
instanceRepository = mock(FlwInstanceRepository.class);
|
||||
defRepository = mock(FlwDefRepository.class);
|
||||
taskRepository = mock(FlwTaskRepository.class);
|
||||
flowEngine = mock(com.yunhai.flowcenter.engine.FlowEngine.class);
|
||||
service = new FlowRouterAdminService(mock(FlwRouterRepository.class), instanceRepository,
|
||||
defRepository, taskRepository, flowEngine, new ObjectMapper());
|
||||
}
|
||||
|
||||
/** v1:CONDITION 节点 n1(办件挂在此);v2:n1 已被移除的改版 */
|
||||
private static FlwDef def(long id, int version, String contentJson) {
|
||||
FlwDef def = new FlwDef();
|
||||
def.setId(id);
|
||||
def.setSchId(SCH);
|
||||
def.setFlowGuid("g-zombie");
|
||||
def.setCode("leave");
|
||||
def.setVersion(version);
|
||||
def.setStatus(FlwEnums.DefStatus.PUBLISHED.getCode());
|
||||
def.setContentJson(contentJson);
|
||||
return def;
|
||||
}
|
||||
|
||||
private static final String V1 = "{\"nodes\":[{\"id\":\"n1\",\"type\":\"CONDITION\"}],"
|
||||
+ "\"edges\":[],\"start\":\"n1\"}";
|
||||
private static final String V2 = "{\"nodes\":[{\"id\":\"n1\",\"type\":\"APPROVAL\"}],"
|
||||
+ "\"edges\":[],\"start\":\"n1\"}";
|
||||
private static final String V2_WITH_COND = "{\"nodes\":[{\"id\":\"n1\",\"type\":\"CONDITION\"}],"
|
||||
+ "\"edges\":[{\"from\":\"n1\",\"to\":\"n2\",\"name\":\"go\"}],\"start\":\"n1\"}";
|
||||
|
||||
private FlwInstance parkedInstance(FlwDef def) {
|
||||
FlwInstance instance = new FlwInstance();
|
||||
instance.setId(500L);
|
||||
instance.setSchId(SCH);
|
||||
instance.setStatus(FlwEnums.InstanceStatus.APPROVING.getCode());
|
||||
instance.setDefId(def.getId());
|
||||
instance.setDefCode(def.getCode());
|
||||
instance.setDefVersion(def.getVersion());
|
||||
instance.setCurrentNodes("n1");
|
||||
when(instanceRepository.findById(500L)).thenReturn(Optional.of(instance));
|
||||
when(defRepository.findById(def.getId())).thenReturn(Optional.of(def));
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultRerouteUsesDefSnapshotVerbatim() {
|
||||
FlwDef v1 = def(11L, 1, V1);
|
||||
FlwInstance instance = parkedInstance(v1);
|
||||
|
||||
service.reroute(SCH, 500L);
|
||||
|
||||
ArgumentCaptor<FlwDef> defCap = ArgumentCaptor.forClass(FlwDef.class);
|
||||
verify(flowEngine).rerouteCondition(any(), defCap.capture(), eq("n1"));
|
||||
assertEquals(11L, defCap.getValue().getId(), "默认重跑按落库 def 快照(v1)");
|
||||
assertEquals(11L, instance.getDefId(), "默认不刷新定义快照");
|
||||
verify(taskRepository, never()).save(any());
|
||||
verify(defRepository, never())
|
||||
.findFirstBySchIdAndFlowGuidAndStatusOrderByVersionDesc(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void v1FailureKeepsInstanceHungUnderDefault() {
|
||||
FlwDef v1 = def(11L, 1, V1);
|
||||
parkedInstance(v1);
|
||||
// 引擎按 v1 重跑仍失败(判断服务还是挂)——业务错上抛,办件保持挂起
|
||||
org.mockito.Mockito.doThrow(new BizException(ErrorCode.PARAM_ERROR, "判断仍失败,办件继续挂起"))
|
||||
.when(flowEngine).rerouteCondition(any(), eq(v1), eq("n1"));
|
||||
|
||||
BizException e = assertThrows(BizException.class, () -> service.reroute(SCH, 500L));
|
||||
assertTrue(e.getMessage().contains("判断仍失败"), "v1 必失败场景默认重跑仍失败挂起(逃生门入口即为此)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void useLatestDefSwitchesToV2AndRefreshesSnapshotWithTrace() {
|
||||
FlwDef v1 = def(11L, 1, V1);
|
||||
FlwDef v2 = def(12L, 2, V2_WITH_COND);
|
||||
FlwInstance instance = parkedInstance(v1);
|
||||
when(defRepository.findFirstBySchIdAndFlowGuidAndStatusOrderByVersionDesc(
|
||||
eq(SCH), eq("g-zombie"), eq(FlwEnums.DefStatus.PUBLISHED.getCode())))
|
||||
.thenReturn(Optional.of(v2));
|
||||
|
||||
service.reroute(SCH, 500L, true);
|
||||
|
||||
ArgumentCaptor<FlwDef> defCap = ArgumentCaptor.forClass(FlwDef.class);
|
||||
verify(flowEngine).rerouteCondition(any(), defCap.capture(), eq("n1"));
|
||||
assertEquals(12L, defCap.getValue().getId(), "useLatestDef=true 引擎按 v2 续走");
|
||||
assertEquals(12L, instance.getDefId(), "定义快照三列同步刷新(R4 同语义)");
|
||||
assertEquals(Integer.valueOf(2), instance.getDefVersion());
|
||||
ArgumentCaptor<FlwTask> trace = ArgumentCaptor.forClass(FlwTask.class);
|
||||
verify(taskRepository).save(trace.capture());
|
||||
assertEquals(FlwEnums.TaskStatus.NODE_REROUTE_DEF, trace.getValue().getNodeId(),
|
||||
"trace 落保留节点 id 的留痕行");
|
||||
assertTrue(trace.getValue().getNodeName().contains("v1→v2"), "留痕行载明版本迁移");
|
||||
verify(instanceRepository).save(instance);
|
||||
}
|
||||
|
||||
@Test
|
||||
void useLatestDefWithSameVersionIsNoopRefresh() {
|
||||
FlwDef v1 = def(11L, 1, V1);
|
||||
FlwInstance instance = parkedInstance(v1);
|
||||
when(defRepository.findFirstBySchIdAndFlowGuidAndStatusOrderByVersionDesc(
|
||||
eq(SCH), eq("g-zombie"), eq(FlwEnums.DefStatus.PUBLISHED.getCode())))
|
||||
.thenReturn(Optional.of(v1));
|
||||
|
||||
service.reroute(SCH, 500L, true);
|
||||
|
||||
verify(flowEngine).rerouteCondition(any(), eq(v1), eq("n1"));
|
||||
verify(taskRepository, never()).save(any());
|
||||
assertEquals(11L, instance.getDefId(), "已是最新版不落留痕行、不刷新");
|
||||
}
|
||||
|
||||
@Test
|
||||
void useLatestDefRejectsWhenNodeMissingInNewDef() {
|
||||
FlwDef v1 = def(11L, 1, V1);
|
||||
// v2 把 n1 改型为 APPROVAL:在途 CONDITION 节点在新定义不存在
|
||||
FlwDef v2 = def(12L, 2, V2);
|
||||
parkedInstance(v1);
|
||||
when(defRepository.findFirstBySchIdAndFlowGuidAndStatusOrderByVersionDesc(
|
||||
eq(SCH), eq("g-zombie"), eq(FlwEnums.DefStatus.PUBLISHED.getCode())))
|
||||
.thenReturn(Optional.of(v2));
|
||||
|
||||
BizException e = assertThrows(BizException.class, () -> service.reroute(SCH, 500L, true));
|
||||
assertTrue(e.getMessage().contains("最新已发布定义中不存在"), "节点在新定义缺失要报人类可读错,不是笼统的无需重跑");
|
||||
verify(flowEngine, never()).rerouteCondition(any(), any(), any());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package com.yunhai.flowcenter.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.yunhai.flowcenter.engine.AssigneeResolver;
|
||||
import com.yunhai.flowcenter.engine.FlowEngine;
|
||||
import com.yunhai.flowcenter.entity.FlwDef;
|
||||
import com.yunhai.flowcenter.enums.FlwEnums;
|
||||
import com.yunhai.flowcenter.model.OapiModels;
|
||||
import com.yunhai.flowcenter.repository.FlwCallbackRegRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwCcRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwDefRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwTaskRepository;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.openapi.base.OpenApiAppClient;
|
||||
import com.yunhai.platform.openapi.base.OpenApiUserClient;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* S2.7 版本竞态(⭐):apply 可选回带预览时的定义版本,改版即人类可读拒绝。
|
||||
* 三态必测——不传(存量调用方,行为逐字不变)、传对(放行)、传旧(拒绝且不落任何引擎动作)。
|
||||
* 另钉住 preview 回带 defVersion(调用方拿得到的前提)。
|
||||
*/
|
||||
@DisplayName("apply 期望定义版本(版本竞态人类可读报错)")
|
||||
class OapiFlowApplyDefVersionTest {
|
||||
|
||||
private static final Long SCH_ID = 1L;
|
||||
private static final String FLOW_GUID = "g-assign";
|
||||
|
||||
private FlwDefRepository defRepository;
|
||||
private FlowInstanceService instanceService;
|
||||
private AssigneeResolver assigneeResolver;
|
||||
private OapiFlowService service;
|
||||
|
||||
@BeforeEach
|
||||
@SuppressWarnings("unchecked")
|
||||
void setUp() {
|
||||
defRepository = mock(FlwDefRepository.class);
|
||||
instanceService = mock(FlowInstanceService.class);
|
||||
assigneeResolver = mock(AssigneeResolver.class);
|
||||
service = new OapiFlowService(defRepository, mock(FlwInstanceRepository.class),
|
||||
mock(FlwTaskRepository.class), mock(FlwCcRepository.class),
|
||||
mock(FlwCallbackRegRepository.class), instanceService, mock(FlowTaskService.class),
|
||||
mock(FlowEngine.class), assigneeResolver, new ObjectMapper(),
|
||||
mock(OpenApiUserClient.class), mock(OpenApiAppClient.class),
|
||||
mock(DiagramTokenService.class), mock(ObjectProvider.class));
|
||||
// 本校校验在单测里关闭(@Value 字段未注入=默认 false),引擎出参给一个既有办件
|
||||
when(instanceService.apply(any(FlowInstanceService.ApplyCommand.class)))
|
||||
.thenReturn(new FlowInstanceService.ApplyOutcome(500L, false));
|
||||
}
|
||||
|
||||
private static FlwDef publishedDef(int version) {
|
||||
FlwDef def = new FlwDef();
|
||||
def.setId(9L);
|
||||
def.setSchId(SCH_ID);
|
||||
def.setFlowGuid(FLOW_GUID);
|
||||
def.setVersion(version);
|
||||
def.setStatus(FlwEnums.DefStatus.PUBLISHED.getCode());
|
||||
def.setContentJson("{\"nodes\":[], \"edges\":[], \"start\":\"n1\"}");
|
||||
return def;
|
||||
}
|
||||
|
||||
private static OapiModels.ApplyBody applyBody(Integer expectedDefVersion) {
|
||||
OapiModels.ApplyBody body = new OapiModels.ApplyBody();
|
||||
body.setFlowGuid(FLOW_GUID);
|
||||
body.setBizType("notice");
|
||||
body.setBizId("77");
|
||||
body.setBizTitle("标题");
|
||||
body.setSubmitterId(21);
|
||||
body.setSubmitterName("甲");
|
||||
body.setExpectedDefVersion(expectedDefVersion);
|
||||
return body;
|
||||
}
|
||||
|
||||
private void stubDef(int version) {
|
||||
when(defRepository.findFirstBySchIdAndFlowGuidAndStatusOrderByVersionDesc(
|
||||
eq(SCH_ID), eq(FLOW_GUID), eq(FlwEnums.DefStatus.PUBLISHED.getCode())))
|
||||
.thenReturn(Optional.of(publishedDef(version)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void withoutExpectedVersion_behavesExactlyAsBefore() {
|
||||
stubDef(3);
|
||||
|
||||
OapiModels.ApplyResultVo vo = service.apply(SCH_ID, "100009", applyBody(null));
|
||||
|
||||
assertEquals(Long.valueOf(500L), vo.getInstanceId());
|
||||
verify(instanceService).apply(any(FlowInstanceService.ApplyCommand.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchingVersion_passesThrough() {
|
||||
stubDef(3);
|
||||
|
||||
OapiModels.ApplyResultVo vo = service.apply(SCH_ID, "100009", applyBody(3));
|
||||
|
||||
assertEquals(Long.valueOf(500L), vo.getInstanceId());
|
||||
verify(instanceService).apply(any(FlowInstanceService.ApplyCommand.class));
|
||||
}
|
||||
|
||||
/** 改版后按旧预览结果提交:人类可读文案,且引擎与指派校验都不被触发(不污染幂等键) */
|
||||
@Test
|
||||
void staleExpectedVersion_rejectedWithReadableMessage_beforeAnyEngineAction() {
|
||||
stubDef(4);
|
||||
|
||||
BizException e = assertThrows(BizException.class,
|
||||
() -> service.apply(SCH_ID, "100009", applyBody(3)));
|
||||
|
||||
assertTrue(e.getMessage().contains("流程已更新,请刷新后重新选择审批人"), e.getMessage());
|
||||
assertTrue(e.getMessage().contains("期望定义版本 3") && e.getMessage().contains("当前已发布版本 4"),
|
||||
e.getMessage());
|
||||
verify(instanceService, never()).apply(any(FlowInstanceService.ApplyCommand.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void previewCarriesDefVersionSoCallerCanEchoIt() {
|
||||
stubDef(7);
|
||||
when(assigneeResolver.resolve(anyLong(), any())).thenReturn(java.util.Collections.emptyList());
|
||||
|
||||
OapiModels.ApplyPreviewVo vo = service.applyPreview(SCH_ID, FLOW_GUID, 21);
|
||||
|
||||
assertEquals(Integer.valueOf(7), vo.getDefVersion());
|
||||
assertEquals(FLOW_GUID, vo.getFlowGuid());
|
||||
assertTrue(vo.getNodes() == null || vo.getNodes().isEmpty());
|
||||
}
|
||||
|
||||
/** 存量兼容:无指派节点的流程 preview 仍返回空节点集(defVersion 不影响既有消费方) */
|
||||
@Test
|
||||
void previewKeepsEmptyNodesForFlowWithoutAssignPolicy() {
|
||||
stubDef(1);
|
||||
|
||||
OapiModels.ApplyPreviewVo vo = service.applyPreview(SCH_ID, FLOW_GUID, 21);
|
||||
|
||||
assertEquals(Integer.valueOf(1), vo.getDefVersion());
|
||||
assertTrue(vo.getNodes() == null || vo.getNodes().isEmpty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.yunhai.flowcenter.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.yunhai.flowcenter.engine.AssigneeResolver;
|
||||
import com.yunhai.flowcenter.engine.FlowEngine;
|
||||
import com.yunhai.flowcenter.model.OapiModels;
|
||||
import com.yunhai.flowcenter.repository.FlwCallbackRegRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwCcRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwDefRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwTaskRepository;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.openapi.base.OpenApiAppClient;
|
||||
import com.yunhai.platform.openapi.base.OpenApiUserClient;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* F9 mode=task 直跳任务办理页(P6.2):pageUrl = front-url + #/task/{taskId}(无 token);
|
||||
* taskId 缺失 PARAM_ERROR;front-url 未配置沿用 fail-fast 纪律(先于一切定位)。
|
||||
*/
|
||||
@DisplayName("embed/url mode=task(F9 任务页直跳)")
|
||||
class OapiFlowEmbedTaskUrlTest {
|
||||
|
||||
private OapiFlowService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new OapiFlowService(Mockito.mock(FlwDefRepository.class),
|
||||
Mockito.mock(FlwInstanceRepository.class), Mockito.mock(FlwTaskRepository.class),
|
||||
Mockito.mock(FlwCcRepository.class), Mockito.mock(FlwCallbackRegRepository.class),
|
||||
Mockito.mock(FlowInstanceService.class), Mockito.mock(FlowTaskService.class),
|
||||
Mockito.mock(FlowEngine.class), Mockito.mock(AssigneeResolver.class),
|
||||
new ObjectMapper(), Mockito.mock(OpenApiUserClient.class),
|
||||
Mockito.mock(OpenApiAppClient.class), Mockito.mock(DiagramTokenService.class),
|
||||
Mockito.mock(ObjectProvider.class));
|
||||
}
|
||||
|
||||
private void frontUrl(String value) {
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(service, "embedFrontUrl", value);
|
||||
}
|
||||
|
||||
@Test
|
||||
void taskModeIssuesCenterTaskPageUrlWithoutToken() {
|
||||
frontUrl("http://fc.local/flow-center");
|
||||
OapiModels.EmbedUrlVo vo = service.embedUrl(1L, null, null, null, "task", 88L);
|
||||
assertEquals("http://fc.local/flow-center/#/task/88", vo.getPageUrl(),
|
||||
"pageUrl = front-url(补尾斜杠)+ #/task/{taskId}");
|
||||
assertEquals(0L, vo.getExpiresIn(), "无 token 模式无有效期,expiresIn=0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void taskModeWithoutTaskIdRejected() {
|
||||
frontUrl("http://fc.local/flow-center");
|
||||
BizException e = assertThrows(BizException.class,
|
||||
() -> service.embedUrl(1L, null, null, null, "task", null));
|
||||
assertEquals(ErrorCode.PARAM_ERROR, e.getErrorCode(), "缺 taskId 是参数错误");
|
||||
assertTrue(e.getMessage().contains("taskId"), "报错须点名缺失参数");
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingFrontUrlStillFailsFastEvenForTaskMode() {
|
||||
frontUrl(" ");
|
||||
BizException e = assertThrows(BizException.class,
|
||||
() -> service.embedUrl(1L, null, null, null, "task", 88L));
|
||||
assertEquals(ErrorCode.INTERNAL_SERVER_ERROR, e.getErrorCode(),
|
||||
"配置缺失优先于 mode 分支:宁可拒绝也不发打不开的地址");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user