requireLine() 存在性判断取反,导致已存在的流程线被拒绝、不存在的线反被放行, 线级停发/恢复线上 100% 失败。改为查询为空时拒绝。 - FlowDefAdminService.java:302 补 ! 号(唯一生产代码改动) - FlDefLineOfflineTest 改用真实 FlowDefAdminService(删除业务副本 spy),补非法 GUID/空值/空白/错误学校/trim/在途隔离断言 - FlNewInstancePathTest 新增新实例路径定向测试
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
# FL 后端核心修复执行回执 — Codex
|
||||
|
||||
执行日期:2026-10-27(Asia/Shanghai)。范围依据:《FL后端核心修复任务书_Codex_无人值守版.md》。
|
||||
|
||||
结论:**已核实**线级守卫的取反缺陷已作最小修复,测试改为真实调用生产 Service;定向测试 43/43 通过。**待核实(环境阻塞)**真实数据库提交后刷新回读、HTTP 联调及真实 AFTER_COMMIT 行为。本回执不代表整个 FL 终审通过。
|
||||
|
||||
## 1. 基线与范围
|
||||
|
||||
- 仓库:`D:/SoftProject/yunhai/flow-center/service`。
|
||||
- **已核实**分支 `main`,HEAD `a10f2e054c2422ae2dfde82926ae567ad9096093`。这是本地仓库基线,不是本次核实过的线上运行版本。
|
||||
- **已核实**开工时 `git status --short` 共 104 项;`FlowDefAdminService.java` 已有修改,`FlDefLineOfflineTest.java` 已存在但未跟踪。均按已有 WIP 保护。
|
||||
- 开工状态和已跟踪差异见 [baseline-status.txt](evidence/core-fix-20261027/baseline-status.txt)、[baseline-tracked.patch](evidence/core-fix-20261027/baseline-tracked.patch);两个目标文件的原始快照也保存在同目录。
|
||||
- 已读上级 `D:/SoftProject/yunhai/AGENTS.md`、`docs/云海AI代理探索与性能纪律.md`、`docs/云海项目导航索引.md`。入口所指 `docs/云海平台架构画像_资产与文档基线盘点.md` 在指定路径不存在,未据此推断环境事实。
|
||||
- 任务书所列线上验收结论按**历史材料记载**引用:新实例 152–157 的回调 36/36 对账成功;本轮未重新访问生产或重跑矩阵,也未取得那些报告的独立正文。
|
||||
|
||||
## 2. 本轮代码差异
|
||||
|
||||
仅包含三份 Java 文件;以开工 WIP 为基线的独立差异见 [task-only.patch](evidence/core-fix-20261027/task-only.patch)。该补丁不是相对 HEAD 的完整发布补丁;应用前应保留原 WIP。
|
||||
|
||||
| 文件 | 本轮修改与原因 |
|
||||
|---|---|
|
||||
| `src/main/java/com/yunhai/flowcenter/service/FlowDefAdminService.java:302` | 在存在性查询前补 `!`,改为缺失时拒绝。保留 null/空白拒绝、trim 和 `(schId, flowGuid)` 查询范围。生产代码仅改这一处判断。 |
|
||||
| `src/test/java/com/yunhai/flowcenter/service/FlDefLineOfflineTest.java` | 删除复制业务逻辑的 `FlwDefAdminServiceSpy`,用已有构造辅助方法创建真实 `FlowDefAdminService`;补齐停发/恢复非法 GUID、空值、空白、错误学校、trim、发布内容及在途协作隔离断言。 |
|
||||
| `src/test/java/com/yunhai/flowcenter/service/FlNewInstancePathTest.java` | 增加新实例路径定向测试:真实创建 Service、引擎、任务 Service、激活意图 Service 和监听器串接;repository、人员解析和发布器为 mock。 |
|
||||
|
||||
未改 pom、生产配置、前端、FC、Notice、UserBase、网关或 SQL;未新增回填机制,未关闭 D09,未改状态机或事件契约。已有这些路径的差异属于开工 WIP。
|
||||
|
||||
## 3. 真实调用与缺陷捕获证据
|
||||
|
||||
先仅替换测试替身,保留错误生产判断,执行原 7 项停发测试:**已核实**失败 1、错误 1,跳过 0。
|
||||
|
||||
- `lineOfflineDelegatesBulkUpdate`:真实存在的 `g-line` 被生产 `requireLine` 抛错拒绝。
|
||||
- `unknownLineRejected`:不存在的 `g-nope` 未抛异常,断言失败。
|
||||
|
||||
原始输出见 [red-test.log](evidence/core-fix-20261027/red-test.log)。随后才修复生产判断并补测试。当前测试字段类型是 `FlowDefAdminService`,由 `new FlowDefAdminService(...)` 实例化,直接调用其公开 `offlineLine` / `enableLine`;被测类和被测方法均未 mock/spy,也没有测试替身里的守卫副本。这一真实失败足以证明重新写反判断会被捕获。
|
||||
|
||||
## 4. 测试结果及实际验证层级
|
||||
|
||||
| 范围 | 结果 | 状态与限制 |
|
||||
|---|---:|---|
|
||||
| `FlDefLineOfflineTest` | 14/14 | **已核实**真实管理 Service:已有线双路径通过,分别委托批量更新 1/0;未知 GUID、null、空串、两种空白和错误学校双路径拒绝且不更新;trim;发布版本、内容及在途协作隔离;新发起/预览停发守卫。repository 为 mock。 |
|
||||
| `FlNewInstancePathTest` | 1/1 | **已核实**新实例创建预期办理人的 pending 任务;任务创建事件通过生产监听器/Service 登记本次真实任务成员、定义 ID 和轮次;合法审批记录意图并推进到完成。把定义线标志置为停发后,在途任务仍使用原 defId/version 完成;未查询最新已发布版本替换原版本。 |
|
||||
| `FlApplyRecoveryTest#createsWhenAbsentAndStampsIdentity` | 1/1 | **已核实**新建实例分支、token/轮次、审批中状态与 START 委托。仅执行这个方法。 |
|
||||
| `FlAdvanceIntentRecoveryTest` | 11/11 | **已核实**生产激活结算 Service 的推进/重试/代次保护定向回归。 |
|
||||
| `FlStateEventRecoveryTest` | 7/7 | **已核实**生产事实记录器的事件身份及冻结载荷等断言。 |
|
||||
| `CallbackRecoveryDeliveryTest` | 9/9 | **已核实**生产分发/恢复逻辑和接收契约的内存协作测试;不是线上网络送达验证。 |
|
||||
| 合计 | **43/43** | Failures 0 / Errors 0 / Skipped 0;Maven BUILD SUCCESS,43.529 秒。 |
|
||||
|
||||
完整输出见 [targeted-test.log](evidence/core-fix-20261027/targeted-test.log),同目录保存了这六个类的 Surefire XML 和文本报告,避免后续运行覆盖证据。Windows Maven 输出部分中文因原工具链编码显示乱码,测试名、数量及 XML 可复核。
|
||||
|
||||
新路径测试直接调用真实业务代码;测试显式驱动同步监听器和后提交入口,并仅初始化 Spring synchronization 以校验激活批次完整性。**没有真实事务、Spring AOP、JPA 锁或数据库提交;不能把这个测试描述为真实 AFTER_COMMIT/数据库集成通过。**
|
||||
|
||||
## 5. 实际命令
|
||||
|
||||
在 service 仓库执行:
|
||||
|
||||
```powershell
|
||||
git status --short
|
||||
git branch --show-current
|
||||
git rev-parse HEAD
|
||||
git diff --binary
|
||||
mvn -version
|
||||
|
||||
# 生产缺陷仍存在,仅将测试改为调用真实 Service;预期失败,exit=1
|
||||
mvn -o '-Dmaven.repo.local=D:\SoftProject\yunhai\.m2\repository' '-Dtest=FlDefLineOfflineTest' test
|
||||
|
||||
# 修复后定向验证;exit=0
|
||||
mvn -o '-Dmaven.repo.local=D:\SoftProject\yunhai\.m2\repository' '-Dtest=FlDefLineOfflineTest,FlNewInstancePathTest,FlApplyRecoveryTest#createsWhenAbsentAndStampsIdentity,FlAdvanceIntentRecoveryTest,FlStateEventRecoveryTest,CallbackRecoveryDeliveryTest' test
|
||||
|
||||
git diff --check -- src/main/java/com/yunhai/flowcenter/service/FlowDefAdminService.java
|
||||
git diff --no-index --check docs/evidence/core-fix-20261027/FlDefLineOfflineTest.before.java.txt src/test/java/com/yunhai/flowcenter/service/FlDefLineOfflineTest.java
|
||||
git apply --reverse --check docs/evidence/core-fix-20261027/task-only.patch
|
||||
```
|
||||
|
||||
**已核实**JDK `1.8.0_121`、Maven `3.9.16`;复用了既有私有依赖本地仓库,离线构建测试可用,没有变更依赖或全局配置。末项只检查补丁,不应用补丁;最终 exit=0,见 [patch-check.txt](evidence/core-fix-20261027/patch-check.txt)。补丁初次导出为 CRLF 导致检查失败,改为 UTF-8/LF 后通过,Java 源码不受此导出修正影响。
|
||||
|
||||
## 6. 环境阻塞与未处理事项
|
||||
|
||||
**已核实**使用 `Get-Command docker`、环境变量存在性检查、`System.Net.Sockets.TcpClient.ConnectAsync('127.0.0.1', port)` 检查本地已知测试入口;证据见 [environment.txt](evidence/core-fix-20261027/environment.txt):
|
||||
|
||||
- Docker 命令不可用,`CI_TEST_DB_URL` / `SPRING_DATASOURCE_URL` 未配置。
|
||||
- `127.0.0.1:3306` 与 `127.0.0.1:13429` 未连接成功。这仅说明所查入口不可用,不声称全机不存在其他数据库。
|
||||
- 当前 `application-test.yml` 指向本机测试库,`FlowEngineIT` 又要求端口 13429 / `fl_d09_20261026`;共享 `FlowTestSupport.cleanDb()` 含全表删除。未启动此类 IT,未尝试使用开发或生产库替代。
|
||||
|
||||
因此以下为**待核实(环境阻塞)**:停发提交后 `line_offline=1`、恢复提交后 `line_offline=0` 的数据库刷新/回读;真实仓储批量更新的全版本/学校隔离;真实事务下新实例、任务推进与回调提交链。所需条件是明确隔离、可写且专用的测试数据库及连接参数,后续测试应只创建新测试记录并限定清理范围,禁止直接运行共享清库 helper。
|
||||
|
||||
**已核实(静态检查)**仓储 `updateLineOffline` JPQL 仅更新匹配 `schId + flowGuid` 的 `lineOffline`,不更新发布状态、版本、内容或实例。数据库行为仍需上项补证。
|
||||
|
||||
未运行 C01 八项、`CallbackDispatchIT` 七项或完整 CI;本次只有一个管理守卫字符变更,定向回归已覆盖直接影响。历史任务 339、notice.id=78、旧激活意图回填、历史事件补发、重置数据、D14 均不在范围。未发现需要扩展异常语义的已证实新路径缺陷。
|
||||
|
||||
## 7. WorkBuddy 联调说明(准备完成,未执行外部写入)
|
||||
|
||||
在获准的隔离测试环境、使用新数据,按以下顺序复验:
|
||||
|
||||
1. 用本校管理员创建/发布一条测试流程线,先发起新实例并记录 defId/version、任务 ID;确认该任务在 `flw_advance_intent.activation_task_ids` 中有本次激活证明。
|
||||
2. `POST /api/flow/admin/def/offlineLine`,JSON `{"flowGuid":"实际测试GUID"}`。学校来自登录管理员上下文,不从请求体指定。期望返回受影响行数;读本校该 GUID 全版本 `flw_def.line_offline` 应全为 1。
|
||||
3. 刷新页面并用 `GET /api/flow/admin/def/versions?flowGuid=...` 回读,仍为停发;新发起和选人预览应提示停发。继续合法办理第 1 步的新实例,在途仍应按原版本推进完成。
|
||||
4. `POST /api/flow/admin/def/enableLine`,同一 JSON。期望受影响行数,刷新/数据库回读全版本 `line_offline=0`;之后新发起使用最高已发布版。
|
||||
5. 对照测试前后各版本 `status/version/content_json` 及原实例 `def_id/def_version` 应保持不变;空白、不存在 GUID 和另一学校管理员访问应被拒绝且不更新。
|
||||
|
||||
路由如有服务 context-path,再加环境配置中的前缀。观察 `flw_instance`、`flw_task`、`flw_advance_intent` 的新记录身份/状态,以及对应新 `flw_state_event`、`flw_callback_log` 的稳定 event_id 和投递状态;本次用少量定向新事件,无需重跑六类 36 项矩阵。若出现 D09 无激活证明,先核对是否确为新建实例及其 TASK_CREATED 注册,禁止对旧任务绕过 fail-closed。
|
||||
|
||||
## 8. 交付边界
|
||||
|
||||
已交付:最小生产修复、真实调用回归测试、新实例路径测试、实际红/绿日志、可复核代码差异与本回执。未执行历史回填、生产数据库操作、部署或 Git commit/push。数据库与环境联调阻塞不影响已完成代码工作;最终 FL 代码审计仍待前端交付和 WorkBuddy 集成复验后另行安排。
|
||||
@@ -0,0 +1,259 @@
|
||||
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.FlowApplyRetryModels;
|
||||
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.mockito.ArgumentCaptor;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 线级停发与发布封存的单测(2026-10-25 T03):
|
||||
*
|
||||
* <ul>
|
||||
* <li><b>线级停发</b>:整线停止新发起/预览;不存在"停发后回退旧版"(停发不是降级);
|
||||
* 在途实例不经新发起解析路径,天然不受影响;恢复只归零标志,不改发布内容。</li>
|
||||
* <li><b>发布封存</b>:updateMeta 对非草稿版本只改行内目录元数据,content_json 保持发布时字节
|
||||
* (审计 E02 关闭:已发布文档不再被原地改写)。</li>
|
||||
* </ul>
|
||||
*/
|
||||
@DisplayName("线级停发/恢复 + 发布封存(T03)")
|
||||
class FlDefLineOfflineTest {
|
||||
|
||||
private static final Long SCH = 1L;
|
||||
private static final String GUID = "g-line";
|
||||
|
||||
private FlwDefRepository defRepository;
|
||||
private FlwDefAdminServiceSpy adminService;
|
||||
private OapiFlowService oapiService;
|
||||
private FlwInstanceRepository instances;
|
||||
private com.yunhai.flowcenter.service.FlowInstanceService instanceService;
|
||||
|
||||
/** 仅暴露 protected/私有依赖注入的薄壳:真实 FlowDefAdminService 构造依赖过多,用 spy 隔离外部协作 */
|
||||
static class FlwDefAdminServiceSpy {
|
||||
final FlwDefRepository defRepository;
|
||||
FlwDefAdminServiceSpy(FlwDefRepository defRepository) {
|
||||
this.defRepository = defRepository;
|
||||
}
|
||||
int offlineLine(Long schId, String flowGuid) {
|
||||
if (flowGuid == null || flowGuid.trim().isEmpty()
|
||||
|| !defRepository.findFirstBySchIdAndFlowGuidOrderByVersionDesc(schId, flowGuid.trim())
|
||||
.isPresent()) {
|
||||
throw new BizException(com.yunhai.platform.utils.exception.ErrorCode.PARAM_ERROR,
|
||||
"流程线不存在:" + flowGuid);
|
||||
}
|
||||
return defRepository.updateLineOffline(schId, flowGuid.trim(), 1);
|
||||
}
|
||||
int enableLine(Long schId, String flowGuid) {
|
||||
if (flowGuid == null || flowGuid.trim().isEmpty()
|
||||
|| !defRepository.findFirstBySchIdAndFlowGuidOrderByVersionDesc(schId, flowGuid.trim())
|
||||
.isPresent()) {
|
||||
throw new BizException(com.yunhai.platform.utils.exception.ErrorCode.PARAM_ERROR,
|
||||
"流程线不存在:" + flowGuid);
|
||||
}
|
||||
return defRepository.updateLineOffline(schId, flowGuid.trim(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
@SuppressWarnings("unchecked")
|
||||
void setUp() {
|
||||
defRepository = mock(FlwDefRepository.class);
|
||||
adminService = new FlwDefAdminServiceSpy(defRepository);
|
||||
instances = mock(FlwInstanceRepository.class);
|
||||
instanceService = mock(FlowInstanceService.class);
|
||||
oapiService = new OapiFlowService(defRepository, instances,
|
||||
mock(FlwTaskRepository.class), mock(FlwCcRepository.class),
|
||||
mock(FlwCallbackRegRepository.class), instanceService, mock(FlowTaskService.class),
|
||||
mock(FlowEngine.class), mock(AssigneeResolver.class), new ObjectMapper(),
|
||||
mock(OpenApiUserClient.class), mock(OpenApiAppClient.class),
|
||||
mock(DiagramTokenService.class), mock(ObjectProvider.class));
|
||||
when(defRepository.findFirstBySchIdAndFlowGuidOrderByVersionDesc(eq(SCH), eq(GUID)))
|
||||
.thenReturn(Optional.of(def(9, 3, 0)));
|
||||
}
|
||||
|
||||
// ==================== 线级停发/恢复(管理面委托) ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("offlineLine:批量置 1;enableLine:批量归零")
|
||||
void lineOfflineDelegatesBulkUpdate() {
|
||||
when(defRepository.updateLineOffline(SCH, GUID, 1)).thenReturn(3);
|
||||
assertThat(adminService.offlineLine(SCH, GUID)).isEqualTo(3);
|
||||
verify(defRepository).updateLineOffline(SCH, GUID, 1);
|
||||
|
||||
when(defRepository.updateLineOffline(SCH, GUID, 0)).thenReturn(3);
|
||||
assertThat(adminService.enableLine(SCH, GUID)).isEqualTo(3);
|
||||
verify(defRepository).updateLineOffline(SCH, GUID, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("不存在的流程线:拒绝且不出更新")
|
||||
void unknownLineRejected() {
|
||||
assertThatThrownBy(() -> adminService.offlineLine(SCH, "g-nope"))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("流程线不存在");
|
||||
verify(defRepository, never()).updateLineOffline(anyLong(), anyString(), anyInt());
|
||||
}
|
||||
|
||||
// ==================== 新发起/预览的线级守卫 ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("线停发:applyV2 新发起被拒且不建实例(不回退旧版)")
|
||||
void applyRejectedWhenLineOffline() {
|
||||
FlwDef offline = def(9, 3, 1);
|
||||
when(defRepository.findFirstBySchIdAndFlowGuidAndStatusOrderByVersionDesc(
|
||||
eq(SCH), eq(GUID), eq(FlwEnums.DefStatus.PUBLISHED.getCode())))
|
||||
.thenReturn(Optional.of(offline));
|
||||
|
||||
FlowApplyRetryModels.ApplyRequestV2 body = new FlowApplyRetryModels.ApplyRequestV2();
|
||||
body.setFlowGuid(GUID);
|
||||
body.setBizType("svc-hall");
|
||||
body.setBizId("42");
|
||||
body.setSubmitterId(7);
|
||||
|
||||
assertThatThrownBy(() -> oapiService.applyV2(SCH, "100018", body))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("停发");
|
||||
verify(instanceService, never()).applyGuarded(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("线在线:新发起照常解析最高已发布版")
|
||||
void applyPassesWhenLineOnline() {
|
||||
FlwDef online = def(9, 3, 0);
|
||||
when(defRepository.findFirstBySchIdAndFlowGuidAndStatusOrderByVersionDesc(
|
||||
eq(SCH), eq(GUID), eq(FlwEnums.DefStatus.PUBLISHED.getCode())))
|
||||
.thenReturn(Optional.of(online));
|
||||
when(instanceService.applyGuarded(any(), any()))
|
||||
.thenReturn(new FlowInstanceService.GuardedOutcome(
|
||||
500L, false, FlowApplyRetryModels.Branch.CREATED, 1));
|
||||
|
||||
FlowApplyRetryModels.ApplyRequestV2 body = new FlowApplyRetryModels.ApplyRequestV2();
|
||||
body.setFlowGuid(GUID);
|
||||
body.setBizType("svc-hall");
|
||||
body.setBizId("42");
|
||||
body.setSubmitterId(7);
|
||||
|
||||
FlowApplyRetryModels.ApplyResultV2 vo = oapiService.applyV2(SCH, "100018", body);
|
||||
assertThat(vo.getInstanceId()).isEqualTo(500L);
|
||||
ArgumentCaptor<FlowInstanceService.ApplyCommand> cmd =
|
||||
ArgumentCaptor.forClass(FlowInstanceService.ApplyCommand.class);
|
||||
verify(instanceService).applyGuarded(cmd.capture(), any());
|
||||
assertThat(cmd.getValue().def.getId()).isEqualTo(9L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("预览同口径:线停发拒绝(选人预览不是新发起旁路)")
|
||||
void previewRejectedWhenLineOffline() {
|
||||
FlwDef offline = def(9, 3, 1);
|
||||
when(defRepository.findFirstBySchIdAndFlowGuidAndStatusOrderByVersionDesc(
|
||||
eq(SCH), eq(GUID), eq(FlwEnums.DefStatus.PUBLISHED.getCode())))
|
||||
.thenReturn(Optional.of(offline));
|
||||
|
||||
assertThatThrownBy(() -> oapiService.applyPreview(SCH, GUID, 7))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("停发");
|
||||
}
|
||||
|
||||
// ==================== 发布封存(updateMeta) ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("updateMeta 对已发布版本:行 name/appKey 可改,content_json 保持发布字节")
|
||||
void updateMetaFreezesPublishedDocument() throws Exception {
|
||||
FlowDefAdminService service = newFlowDefAdminService();
|
||||
FlwDef published = def(9, 3, FlwEnums.DefStatus.PUBLISHED.getCode());
|
||||
published.setName("旧名");
|
||||
String originalJson = published.getContentJson();
|
||||
when(defRepository.findById(9L)).thenReturn(Optional.of(published));
|
||||
when(defRepository.save(any())).thenAnswer(c -> c.getArgument(0));
|
||||
ReflectionTestUtils.setField(service, "appRepository", appRepo(true));
|
||||
|
||||
FlwDef saved = service.updateMeta(SCH, 9L, "新名", null);
|
||||
|
||||
assertThat(saved.getName()).isEqualTo("新名");
|
||||
assertThat(saved.getContentJson()).isEqualTo(originalJson);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("updateMeta 对草稿:content_json 顶层 name 照旧同步(草稿文档可变)")
|
||||
void updateMetaStillSyncsDraftDocument() throws Exception {
|
||||
FlowDefAdminService service = newFlowDefAdminService();
|
||||
FlwDef draft = def(9, 3, 0);
|
||||
draft.setStatus(FlwEnums.DefStatus.DRAFT.getCode());
|
||||
draft.setName("旧名");
|
||||
draft.setContentJson("{\"name\":\"旧名\",\"nodes\":[],\"edges\":[]}");
|
||||
when(defRepository.findById(9L)).thenReturn(Optional.of(draft));
|
||||
when(defRepository.save(any())).thenAnswer(c -> c.getArgument(0));
|
||||
ReflectionTestUtils.setField(service, "appRepository", appRepo(true));
|
||||
|
||||
FlwDef saved = service.updateMeta(SCH, 9L, "新名", null);
|
||||
|
||||
assertThat(saved.getContentJson()).contains("\"新名\"");
|
||||
}
|
||||
|
||||
// ==================== fixtures ====================
|
||||
|
||||
private FlowDefAdminService newFlowDefAdminService() {
|
||||
// 依赖协作全部 mock(构造器形参多,反射装配与生产 Spring 无关,只测本类逻辑)
|
||||
return new FlowDefAdminService(
|
||||
defRepository,
|
||||
mock(com.yunhai.flowcenter.repository.FlwRouterRepository.class),
|
||||
mock(com.yunhai.flowcenter.repository.FlwCallbackRegRepository.class),
|
||||
appRepo(true),
|
||||
mock(com.yunhai.flowcenter.repository.FlwInstanceRepository.class),
|
||||
mock(FlowDeptAdminService.class),
|
||||
mock(FlowEngine.class),
|
||||
new ObjectMapper(),
|
||||
mock(FormCenterSchemaClient.class));
|
||||
}
|
||||
|
||||
private com.yunhai.flowcenter.repository.FlwAppRepository appRepo(boolean enabled) {
|
||||
com.yunhai.flowcenter.repository.FlwAppRepository repo =
|
||||
mock(com.yunhai.flowcenter.repository.FlwAppRepository.class);
|
||||
when(repo.isEnabledApp(anyLong(), anyString())).thenReturn(enabled);
|
||||
return repo;
|
||||
}
|
||||
|
||||
private FlwDef def(int id, int version, int lineOffline) {
|
||||
FlwDef def = new FlwDef();
|
||||
def.setId((long) id);
|
||||
def.setSchId(SCH);
|
||||
def.setFlowGuid(GUID);
|
||||
def.setCode("leave");
|
||||
def.setName("请假");
|
||||
def.setVersion(version);
|
||||
def.setStatus(FlwEnums.DefStatus.PUBLISHED.getCode());
|
||||
def.setLineOffline(lineOffline);
|
||||
def.setContentJson("{\"nodes\":[], \"edges\":[], \"start\":\"n1\"}");
|
||||
return def;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,462 @@
|
||||
package com.yunhai.flowcenter.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.yunhai.flowcenter.engine.DefValidator;
|
||||
import com.yunhai.flowcenter.engine.FlowDefModel;
|
||||
import com.yunhai.flowcenter.engine.VariableContract;
|
||||
import com.yunhai.flowcenter.engine.FlowEngine;
|
||||
import com.yunhai.flowcenter.entity.FlwAssigneeMap;
|
||||
import com.yunhai.flowcenter.entity.FlwDef;
|
||||
import com.yunhai.flowcenter.enums.FlwEnums;
|
||||
import com.yunhai.flowcenter.enums.FormBindingErrorCode;
|
||||
import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwDefRepository;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 流程定义管理(M1:JSON 文本编辑模式,概览 §9.2):
|
||||
* 保存=草稿直改/已发布升版本(flowGuid 跨版本不变);发布跑静态校验(把"任务无主"拦在发布期)。
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FlowDefAdminService {
|
||||
|
||||
private final FlwDefRepository defRepository;
|
||||
private final com.yunhai.flowcenter.repository.FlwRouterRepository routerRepository;
|
||||
private final com.yunhai.flowcenter.repository.FlwCallbackRegRepository regRepository;
|
||||
private final com.yunhai.flowcenter.repository.FlwAppRepository appRepository;
|
||||
private final com.yunhai.flowcenter.repository.FlwInstanceRepository instanceRepository;
|
||||
private final FlowDeptAdminService deptAdminService;
|
||||
private final FlowEngine flowEngine;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final FormCenterSchemaClient formCenterSchemaClient;
|
||||
|
||||
/** 定义列表:status/appKey 可选过滤(各流程线各版本都展示;appKey 精确匹配,2026-10-10 按应用筛选) */
|
||||
public List<FlwDef> list(Long schId, Integer status, String appKey) {
|
||||
List<FlwDef> rows = status != null
|
||||
? defRepository.findBySchIdAndStatusOrderByUpdateTimeDesc(schId, status)
|
||||
: defRepository.findBySchIdOrderByUpdateTimeDesc(schId);
|
||||
String key = appKey == null ? null : appKey.trim();
|
||||
if (key != null && !key.isEmpty()) {
|
||||
rows = rows.stream().filter(d -> key.equals(d.getAppKey())).collect(Collectors.toList());
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** 应用清单("按应用筛选"下拉数据源):本校 flw_def 已标注 ∪ 已注册回调的应用,去重排序 */
|
||||
public List<String> distinctAppKeys(Long schId) {
|
||||
TreeSet<String> keys = new TreeSet<>();
|
||||
keys.addAll(defRepository.findDistinctAppKeys(schId));
|
||||
keys.addAll(regRepository.findDistinctAppKeysBySchId(schId));
|
||||
return new ArrayList<>(keys);
|
||||
}
|
||||
|
||||
/** 全量列表(管理端首页:各流程线最新版本+状态聚合展示简化为全量返回) */
|
||||
public List<FlwDef> listAll(Long schId) {
|
||||
return defRepository.findPublishedLatestPerGuid(schId);
|
||||
}
|
||||
|
||||
public FlwDef get(Long schId, Long id) {
|
||||
FlwDef def = requireDef(schId, id);
|
||||
return def;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存(2026-10-10 起唯一键为 uk(sch,flow_guid,version),编码降级为可重名的分类标签):
|
||||
* - id 非空:草稿直改;已发布/停用=复制升版本(同 flowGuid,version=max+1)
|
||||
* - id 空 + baseGuid 非空:在该流程线上开新版本草稿(version=max+1,flowGuid 不变)——前端"升版本"
|
||||
* - id 空:永远新开一条流程线(flowGuid 生成,version=1);同编码多条线合法,前端保存前有软提示
|
||||
* appKey=归属应用(筛选维度,2026-10-12 拍板必填):新建必须显式传;升版本/直改可继承源行,
|
||||
* 但源行为空时必须显式补填——存量 NULL 线随编辑自然收敛(白名单只校验显式传入值,继承值不重校)
|
||||
* changeLog=升版本改动说明(议题二③):只随新建行落库(三个建新行分支),草稿直改行不写也不清空
|
||||
*/
|
||||
@Transactional
|
||||
public FlwDef save(Long schId, Long id, String code, String name, String contentJson,
|
||||
String baseGuid, String appKey, String changeLog) {
|
||||
String cleanLog = (changeLog == null || changeLog.trim().isEmpty()) ? null
|
||||
: changeLog.trim().substring(0, Math.min(500, changeLog.trim().length()));
|
||||
String cleanAppKey = (appKey == null || appKey.trim().isEmpty()) ? null : appKey.trim();
|
||||
// 归属应用白名单(2026-10-10 应用设置):显式传入非空时必须 ∈ flw_app(sch, enabled=1);
|
||||
// 继承值不重校(既成事实,应用停用不应卡死升版本)。不在校池/未开启的应用由此拦下(拍板 D2:不做存量兜底)
|
||||
if (cleanAppKey != null && !appRepository.isEnabledApp(schId, cleanAppKey)) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR,
|
||||
"归属应用未开启或不存在(" + cleanAppKey + "):请在 流程管理 > 应用设置 中开启后重试");
|
||||
}
|
||||
// JSON 先解析再落库(语法错误不进库)
|
||||
FlowDefModel parsed = FlowDefModel.parse(objectMapper, contentJson);
|
||||
if (code == null || code.trim().isEmpty() || name == null || name.trim().isEmpty()) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "code/name 必填");
|
||||
}
|
||||
FlwDef def;
|
||||
if (id == null && baseGuid != null && !baseGuid.trim().isEmpty()) {
|
||||
// 在既有流程线上开新版本草稿(appKey 缺省继承源线)
|
||||
FlwDef base = defRepository.findFirstBySchIdAndFlowGuidOrderByVersionDesc(schId, baseGuid.trim())
|
||||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "baseGuid 流程线不存在"));
|
||||
def = new FlwDef();
|
||||
def.setSchId(schId);
|
||||
def.setFlowGuid(baseGuid.trim());
|
||||
def.setCode(code.trim());
|
||||
def.setVersion(defRepository.maxVersionByGuid(schId, baseGuid.trim()) + 1);
|
||||
def.setStatus(FlwEnums.DefStatus.DRAFT.getCode());
|
||||
def.setAppKey(cleanAppKey != null ? cleanAppKey : base.getAppKey());
|
||||
def.setFormUuid(base.getFormUuid());
|
||||
def.setFormVersion(base.getFormVersion());
|
||||
} else if (id == null) {
|
||||
// 新建:永远新开一条流程线,版本从 1 起(不再做同编码草稿复用)
|
||||
def = new FlwDef();
|
||||
def.setSchId(schId);
|
||||
def.setFlowGuid(UUID.randomUUID().toString().replace("-", "").toLowerCase());
|
||||
def.setCode(code.trim());
|
||||
def.setVersion(1);
|
||||
def.setStatus(FlwEnums.DefStatus.DRAFT.getCode());
|
||||
def.setAppKey(cleanAppKey);
|
||||
} else {
|
||||
def = requireDef(schId, id);
|
||||
if (!def.getCode().equals(code.trim())) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "流程编码不可修改");
|
||||
}
|
||||
if (cleanAppKey != null) {
|
||||
def.setAppKey(cleanAppKey);
|
||||
}
|
||||
if (def.getStatus() != FlwEnums.DefStatus.DRAFT.getCode()) {
|
||||
// 升版本:复制行,保持 flowGuid
|
||||
FlwDef next = new FlwDef();
|
||||
next.setSchId(schId);
|
||||
next.setFlowGuid(def.getFlowGuid());
|
||||
next.setCode(def.getCode());
|
||||
next.setVersion(defRepository.maxVersionByGuid(schId, def.getFlowGuid()) + 1);
|
||||
next.setStatus(FlwEnums.DefStatus.DRAFT.getCode());
|
||||
next.setAppKey(cleanAppKey != null ? cleanAppKey : def.getAppKey());
|
||||
next.setFormUuid(def.getFormUuid());
|
||||
next.setFormVersion(def.getFormVersion());
|
||||
def = next;
|
||||
}
|
||||
}
|
||||
def.setCode(code.trim());
|
||||
def.setName(name.trim());
|
||||
// 归属应用必填(2026-10-12 拍板):各分支归一后落库值不得为空——新建必须显式传;
|
||||
// 升版本/直改继承源行,源行为空的存量线编辑时强制补填(19 条存量随编辑自然收敛)
|
||||
if (def.getAppKey() == null || def.getAppKey().trim().isEmpty()) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR,
|
||||
"归属应用必填:请选择归属应用后再保存(应用需先在 流程管理 > 应用设置 中开启)");
|
||||
}
|
||||
// 改动说明只写新建行(新建/baseGuid 开版/已发布复制升版三分支 id 必为 null);草稿直改不动
|
||||
if (def.getId() == null) {
|
||||
def.setChangeLog(cleanLog);
|
||||
}
|
||||
def.setContentJson(withoutFormContract(contentJson)); // Only publish may author a contract snapshot.
|
||||
// 定义 JSON 里的 code/name 与行字段一致性校验(给绑定回显用行字段,JSON 内为主)
|
||||
if (parsed.code != null && !parsed.code.trim().isEmpty() && !parsed.code.trim().equals(def.getCode())) {
|
||||
def.setCode(parsed.code.trim());
|
||||
}
|
||||
if (parsed.name != null && !parsed.name.trim().isEmpty()) {
|
||||
def.setName(parsed.name.trim());
|
||||
}
|
||||
try {
|
||||
return defRepository.save(def);
|
||||
} catch (DataIntegrityViolationException e) {
|
||||
// 撞唯一键 (sch,flow_guid,version):并发升版本同一线时可能撞号,转可理解提示(原样抛出=笼统"内部服务错误")
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "保存冲突:该流程可能正被他人同时升版本,请返回列表刷新后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存重载(2026-10-20 C1 第一刀):formUuid=关联表单定义 uuid(form-center 对外身份,32 位)。<br>
|
||||
* <b>发布后不可改,改=升版本</b>由既有升版分派天然保证(非 DRAFT 直改必走升版新行);
|
||||
* 参数 null=保持不动(缺省语义,兼容旧调用面);空串=显式取消关联(归一 null 落库);
|
||||
* 非空=设置,保存前按确切版本远端核验,失败不写入。
|
||||
* <br>v3/TASK_1 追加 {@code formVersion}:与 formUuid <b>成对</b>,规则是——
|
||||
* ① 两个都给了值=设关联(版本按服务端收到的整数存,之后发布按这组确切坐标核验);
|
||||
* ② 只给一个=拒(半个坐标既存不下也没法核验,比"存了再说"便宜);
|
||||
* ③ formUuid 给空串=取消关联,<b>同时清空版本</b>(留着一个没人认领的版本号是下一轮误用的起点);
|
||||
* ④ formUuid 给 null=这条指令不存在(两个坐标都不动,兼容旧调用面)。
|
||||
*/
|
||||
@Transactional
|
||||
public FlwDef save(Long schId, Long id, String code, String name, String contentJson,
|
||||
String baseGuid, String appKey, String changeLog, String formUuid, Integer formVersion) {
|
||||
boolean clearing = formUuid != null && formUuid.trim().isEmpty();
|
||||
boolean pairing = formUuid != null && !clearing && formVersion == null;
|
||||
boolean versionOnly = formUuid == null && formVersion != null;
|
||||
if (pairing || versionOnly) {
|
||||
throw new BizException(formVersion == null ? FormBindingErrorCode.VERSION_REQUIRED
|
||||
: FormBindingErrorCode.INVALID_COORDINATE,
|
||||
"关联表单必须成对提交 formUuid 与 formVersion(只传一个拒):当前 formUuid="
|
||||
+ (formUuid == null ? "未传" : formUuid.trim()) + ",formVersion=" + formVersion);
|
||||
}
|
||||
if (formUuid != null && !clearing) {
|
||||
checkedSchema(schId, formUuid.trim(), formVersion, "保存"); // Validate before the first save.
|
||||
}
|
||||
FlwDef saved = save(schId, id, code, name, contentJson, baseGuid, appKey, changeLog);
|
||||
if (formUuid != null) {
|
||||
String clean = formUuid.trim();
|
||||
saved.setFormUuid(clean.isEmpty() ? null : clean);
|
||||
saved.setFormVersion(clean.isEmpty() ? null : formVersion);
|
||||
}
|
||||
try {
|
||||
return defRepository.save(saved);
|
||||
} catch (DataIntegrityViolationException e) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "保存冲突:该流程可能正被他人同时升版本,请返回列表刷新后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/** 发布:静态校验(START/END 唯一、无孤儿无环、DEPT_KEY 有启用映射)后置为已发布。<br>
|
||||
* 2026-10-20 C1 第一刀:def 关联了表单(form_uuid 非空)时,发布期拉 form-center schema 快照
|
||||
* 嵌入版本行 content_json.formContract(与 defId 快照纪律同构:在途办件按发起时快照校验),
|
||||
* 并做表达式<b>键查①</b>红牌(变量 ⊆ 快照字段 ∪ 内置变量)。拉取失败=发布阻断(可重试);
|
||||
* 运行期对 form-center 零依赖;新发布的无绑定版本必须显式声明 variables。 */
|
||||
@Transactional
|
||||
public FlwDef publish(Long schId, Long id) {
|
||||
FlwDef def = requireDef(schId, id);
|
||||
if (def.getStatus() == FlwEnums.DefStatus.PUBLISHED.getCode()) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "已是发布状态");
|
||||
}
|
||||
if (def.getStatus() != FlwEnums.DefStatus.DRAFT.getCode()) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "停用版本请走启用;修改表单关联须保存为新版本草稿");
|
||||
}
|
||||
FlowDefModel model = FlowDefModel.parse(objectMapper, def.getContentJson());
|
||||
if (def.getFormUuid() != null && !def.getFormUuid().trim().isEmpty()) {
|
||||
Map<String, Object> snapshot = checkedSchema(schId, def.getFormUuid().trim(),
|
||||
def.getFormVersion(), "发布");
|
||||
model.variables = VariableContract.fromSnapshot(snapshot);
|
||||
VariableContract.validateDefinition(model, true);
|
||||
DefValidator.validate(model, enabledDeptKeys(schId), enabledRouterKeys());
|
||||
def.setContentJson(embedFormContract(def.getContentJson(), snapshot, model.variables));
|
||||
} else if (def.getFormVersion() != null) {
|
||||
throw new BizException(FormBindingErrorCode.INVALID_COORDINATE, "关联表单版本必须同时绑定 formUuid");
|
||||
}
|
||||
if (def.getFormUuid() == null || def.getFormUuid().trim().isEmpty()) {
|
||||
VariableContract.validateDefinition(model, true);
|
||||
DefValidator.validate(model, enabledDeptKeys(schId), enabledRouterKeys());
|
||||
}
|
||||
def.setStatus(FlwEnums.DefStatus.PUBLISHED.getCode());
|
||||
def.setPublishTime(LocalDateTime.now());
|
||||
return defRepository.save(def);
|
||||
}
|
||||
|
||||
/** content_json 顶层嵌入 formContract 节(ObjectNode 保字段顺序;原值存在则整体替换=本版快照) */
|
||||
private String embedFormContract(String contentJson, Map<String, Object> snapshot,
|
||||
List<FlowDefModel.Variable> variables) {
|
||||
try {
|
||||
com.fasterxml.jackson.databind.node.ObjectNode root = (com.fasterxml.jackson.databind.node.ObjectNode)
|
||||
objectMapper.readTree(contentJson);
|
||||
root.set("formContract", objectMapper.valueToTree(snapshot));
|
||||
root.set("variables", objectMapper.valueToTree(variables));
|
||||
return objectMapper.writeValueAsString(root);
|
||||
} catch (BizException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "快照嵌入失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 停用:不可新发起,存量跑完 */
|
||||
@Transactional
|
||||
public void offline(Long schId, Long id) {
|
||||
FlwDef def = requireDef(schId, id);
|
||||
if (def.getStatus() == FlwEnums.DefStatus.DRAFT.getCode()) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "草稿不能停用后启用,请先通过发布校验");
|
||||
}
|
||||
def.setStatus(FlwEnums.DefStatus.DISABLED.getCode());
|
||||
defRepository.save(def);
|
||||
}
|
||||
|
||||
/**
|
||||
* 线级停发(2026-10-25 T03):整条 flowGuid 停止新发起/预览,在途实例按各自固定 defId 原版跑完。
|
||||
* 与版本级 {@link #offline} 正交(可叠加);批量 UPDATE 全版本行,读任一行即读线状态。
|
||||
* 不改写任何发布内容字节,恢复走 {@link #enableLine}。
|
||||
*/
|
||||
@Transactional
|
||||
public int offlineLine(Long schId, String flowGuid) {
|
||||
requireLine(schId, flowGuid);
|
||||
return defRepository.updateLineOffline(schId, flowGuid.trim(), 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 线级恢复(T03):只恢复线的可用性——line_offline 归零;各版本 status/发布内容/版本指针
|
||||
* 一律不动(停发期间发布新版不受影响,恢复后新发起即用当时最高已发布版)。
|
||||
*/
|
||||
@Transactional
|
||||
public int enableLine(Long schId, String flowGuid) {
|
||||
requireLine(schId, flowGuid);
|
||||
return defRepository.updateLineOffline(schId, flowGuid.trim(), 0);
|
||||
}
|
||||
|
||||
private void requireLine(Long schId, String flowGuid) {
|
||||
if (flowGuid == null || flowGuid.trim().isEmpty()
|
||||
|| defRepository.findFirstBySchIdAndFlowGuidOrderByVersionDesc(schId, flowGuid.trim())
|
||||
.isPresent()) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "流程线不存在:" + flowGuid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用(2026-09-18):只对"停用"版恢复为已发布,与 offline 对称。
|
||||
* 复用发布期的静态校验(把"任务无主"重新拦一次)——若建的时候某科室还有启用办理人、之后
|
||||
* 被停用,这里会点名报错(如"科室 X 无启用办理人"),管理员在办理人映射页点"启用"即可解。
|
||||
*/
|
||||
@Transactional
|
||||
public FlwDef enable(Long schId, Long id) {
|
||||
FlwDef def = requireDef(schId, id);
|
||||
if (def.getStatus() == FlwEnums.DefStatus.PUBLISHED.getCode()) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "该流程已是发布状态,无需启用");
|
||||
}
|
||||
if (def.getStatus() != FlwEnums.DefStatus.DISABLED.getCode()) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "仅停用的流程可启用;草稿请走发布");
|
||||
}
|
||||
FlowDefModel model = FlowDefModel.parse(objectMapper, def.getContentJson());
|
||||
DefValidator.validate(model, enabledDeptKeys(schId), enabledRouterKeys());
|
||||
if (def.getFormUuid() != null && !def.getFormUuid().trim().isEmpty()) {
|
||||
checkedSchema(schId, def.getFormUuid().trim(), def.getFormVersion(), "启用");
|
||||
} else if (def.getFormVersion() != null) {
|
||||
throw new BizException(FormBindingErrorCode.INVALID_COORDINATE, "关联表单版本必须同时绑定 formUuid");
|
||||
}
|
||||
def.setStatus(FlwEnums.DefStatus.PUBLISHED.getCode());
|
||||
def.setPublishTime(LocalDateTime.now());
|
||||
return defRepository.save(def);
|
||||
}
|
||||
|
||||
private Map<String, Object> checkedSchema(Long schId, String uuid, Integer version, String operation) {
|
||||
if (version == null) {
|
||||
throw new BizException(FormBindingErrorCode.VERSION_REQUIRED, "关联表单缺少明确版本,"
|
||||
+ operation + "已阻断,请重新选择表单与版本");
|
||||
}
|
||||
if (version < 1 || uuid == null || uuid.trim().isEmpty()) {
|
||||
throw new BizException(FormBindingErrorCode.INVALID_COORDINATE,
|
||||
"关联表单必须选择确切的正整数版本," + operation + "已阻断,请重新选择");
|
||||
}
|
||||
try {
|
||||
return formCenterSchemaClient.fetchSchema(schId, uuid, version);
|
||||
} catch (FormCenterSchemaClient.SchemaUnavailableException e) {
|
||||
throw new BizException(FormBindingErrorCode.SCHEMA_UNAVAILABLE,
|
||||
"关联表单快照拉取失败," + operation + "已阻断(可重试)", e);
|
||||
} catch (FormCenterSchemaClient.SchemaContractViolationException e) {
|
||||
throw new BizException(FormBindingErrorCode.SCHEMA_CONTRACT_VIOLATION,
|
||||
"关联表单版本核验失败," + operation + "已阻断(请重新选择表单与版本)", e);
|
||||
} catch (FormCenterSchemaClient.SchemaResourceUnavailableException e) {
|
||||
throw new BizException(FormBindingErrorCode.RESOURCE_UNAVAILABLE,
|
||||
"关联表单或指定发布版本不存在或不可读," + operation + "已阻断", e);
|
||||
} catch (FormCenterSchemaClient.SchemaAccessDeniedException e) {
|
||||
throw new BizException(FormBindingErrorCode.ACCESS_DENIED,
|
||||
"关联表单访问身份或授权被拒绝," + operation + "已阻断,请检查授权", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String withoutFormContract(String contentJson) {
|
||||
try {
|
||||
com.fasterxml.jackson.databind.node.ObjectNode root = (com.fasterxml.jackson.databind.node.ObjectNode)
|
||||
objectMapper.readTree(contentJson);
|
||||
root.remove("formContract");
|
||||
return objectMapper.writeValueAsString(root);
|
||||
} catch (Exception e) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "流程定义必须是有效的JSON对象");
|
||||
}
|
||||
}
|
||||
|
||||
/** 同一流程线全部版本(降序) */
|
||||
public List<FlwDef> versions(Long schId, String flowGuid) {
|
||||
return defRepository.findBySchIdAndFlowGuidOrderByVersionDesc(schId, flowGuid);
|
||||
}
|
||||
|
||||
/** 在途办件数(议题二④):flowGuid → 审批中(10)办件数;口径=def_id 精确匹配该线各版本;请求的 guid 全部有键(无在途=0) */
|
||||
public Map<String, Long> inflightCounts(Long schId, Collection<String> flowGuids) {
|
||||
Map<String, Long> result = new HashMap<>();
|
||||
if (flowGuids == null || flowGuids.isEmpty()) return result;
|
||||
flowGuids.forEach(g -> result.put(g, 0L));
|
||||
List<FlwDef> defs = defRepository.findBySchIdAndFlowGuidIn(schId, flowGuids);
|
||||
if (defs.isEmpty()) return result;
|
||||
Map<Long, String> idToGuid = new HashMap<>();
|
||||
defs.forEach(d -> idToGuid.put(d.getId(), d.getFlowGuid()));
|
||||
instanceRepository.countApprovingByDefIds(schId, idToGuid.keySet()).forEach(r -> {
|
||||
Long defId = ((Number) r[0]).longValue();
|
||||
Long cnt = ((Number) r[1]).longValue();
|
||||
result.merge(idToGuid.get(defId), cnt, Long::sum);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 元数据原地编辑(2026-10-10 四议题·议题2,拍板 D4):已发布/停用/草稿均可只改 name/appKey
|
||||
* 而<b>不升版本</b>——结构改动仍走 save 的升版本路径,两者完全隔离(本方法不碰
|
||||
* code/flow_guid/version/status,不进升版本分支)。appKey 校验同 save 口径(显式传入须在白名单)。
|
||||
* content_json 顶层 name 同步更新(只动顶层字段不动结构,消除"行 name 与 JSON name 的脏窗口";
|
||||
* 亦修复 API 直调方经 parsed.name 覆盖行字段时读到旧名的口径)。
|
||||
* 并发说明:与升版本路径无共享可变行(升版本建新行),不加锁。
|
||||
*/
|
||||
@Transactional
|
||||
public FlwDef updateMeta(Long schId, Long id, String name, String appKey) {
|
||||
FlwDef def = requireDef(schId, id);
|
||||
if (name == null || name.trim().isEmpty()) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "流程名称不能为空");
|
||||
}
|
||||
String cleanName = name.trim();
|
||||
String cleanAppKey = (appKey == null || appKey.trim().isEmpty()) ? null : appKey.trim();
|
||||
if (cleanAppKey != null && !appRepository.isEnabledApp(schId, cleanAppKey)) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR,
|
||||
"归属应用未开启或不存在(" + cleanAppKey + "):请在 流程管理 > 应用设置 中开启后重试");
|
||||
}
|
||||
def.setName(cleanName);
|
||||
def.setAppKey(cleanAppKey);
|
||||
// 发布封存(2026-10-25 T03):非草稿版本的执行文档不可变——行 name 照改(目录元数据),
|
||||
// content_json 顶层 name 保持发布时字节,消除"已发布文档被原地改写"的旁路
|
||||
//(审计 E02:updateMeta 曾同步改已发布 contentJson 顶层 name)。
|
||||
if (def.getStatus() != null && def.getStatus() == FlwEnums.DefStatus.DRAFT.getCode()) {
|
||||
def.setContentJson(syncTopLevelName(def.getContentJson(), cleanName));
|
||||
}
|
||||
try {
|
||||
return defRepository.save(def);
|
||||
} catch (DataIntegrityViolationException e) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "保存冲突:该流程可能正被他人同时编辑,请刷新后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/** contentJson 顶层 name 同步(ObjectNode 保字段顺序;结构零改动),返回新 JSON 串 */
|
||||
private String syncTopLevelName(String contentJson, String name) {
|
||||
try {
|
||||
com.fasterxml.jackson.databind.node.ObjectNode root = (com.fasterxml.jackson.databind.node.ObjectNode)
|
||||
objectMapper.readTree(contentJson);
|
||||
root.put("name", name);
|
||||
return objectMapper.writeValueAsString(root);
|
||||
} catch (Exception e) {
|
||||
// contentJson 在 save 期已保证可解析;防御口径:解析失败不同步(行 name 已更新,JSON 维持原值)
|
||||
return contentJson;
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> enabledDeptKeys(Long schId) {
|
||||
// 2026-10-10 科室管理切源:发布校验白名单改查 flw_dept/flw_dept_member(拍板 D1)
|
||||
return deptAdminService.enabledDeptKeys(schId);
|
||||
}
|
||||
|
||||
/** CONDITION routerKey 发布校验白名单(flw_router 启用键全集,全局不分校) */
|
||||
private Set<String> enabledRouterKeys() {
|
||||
return routerRepository.findByStatusOrderByIdAsc(1).stream()
|
||||
.map(com.yunhai.flowcenter.entity.FlwRouter::getRouterKey)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private FlwDef requireDef(Long schId, Long id) {
|
||||
FlwDef def = defRepository.findById(id)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "流程定义不存在:" + id));
|
||||
if (!def.getSchId().equals(schId)) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "流程定义不属于该学校");
|
||||
}
|
||||
return def;
|
||||
}
|
||||
|
||||
}
|
||||
+74
File diff suppressed because one or more lines are too long
+72
File diff suppressed because one or more lines are too long
+76
File diff suppressed because one or more lines are too long
+66
File diff suppressed because one or more lines are too long
+79
File diff suppressed because one or more lines are too long
+66
File diff suppressed because one or more lines are too long
@@ -0,0 +1,104 @@
|
||||
M .gitea/workflows/build.yml
|
||||
M pom.xml
|
||||
M src/main/java/com/yunhai/flowcenter/callback/CallbackDispatcher.java
|
||||
M src/main/java/com/yunhai/flowcenter/callback/CallbackLogStore.java
|
||||
M src/main/java/com/yunhai/flowcenter/callback/CallbackPushService.java
|
||||
M src/main/java/com/yunhai/flowcenter/controller/api/FlowAdminController.java
|
||||
M src/main/java/com/yunhai/flowcenter/controller/api/FlowUserController.java
|
||||
M src/main/java/com/yunhai/flowcenter/controller/oapi/OapiFlowController.java
|
||||
M src/main/java/com/yunhai/flowcenter/engine/AssigneeResolver.java
|
||||
M src/main/java/com/yunhai/flowcenter/engine/DefValidator.java
|
||||
M src/main/java/com/yunhai/flowcenter/engine/FlowAdvanceService.java
|
||||
M src/main/java/com/yunhai/flowcenter/engine/FlowDefModel.java
|
||||
M src/main/java/com/yunhai/flowcenter/engine/FlowEngine.java
|
||||
M src/main/java/com/yunhai/flowcenter/engine/FlowEvent.java
|
||||
M src/main/java/com/yunhai/flowcenter/engine/FlowNodeAdvanceListener.java
|
||||
M src/main/java/com/yunhai/flowcenter/entity/FlwCallbackLog.java
|
||||
M src/main/java/com/yunhai/flowcenter/entity/FlwCallbackReg.java
|
||||
M src/main/java/com/yunhai/flowcenter/entity/FlwDef.java
|
||||
M src/main/java/com/yunhai/flowcenter/entity/FlwInstance.java
|
||||
M src/main/java/com/yunhai/flowcenter/model/FlowApplyRetryModels.java
|
||||
M src/main/java/com/yunhai/flowcenter/model/OapiModels.java
|
||||
M src/main/java/com/yunhai/flowcenter/repository/FlwCallbackLogRepository.java
|
||||
M src/main/java/com/yunhai/flowcenter/repository/FlwCcRepository.java
|
||||
M src/main/java/com/yunhai/flowcenter/repository/FlwDefRepository.java
|
||||
M src/main/java/com/yunhai/flowcenter/repository/FlwInstanceRepository.java
|
||||
M src/main/java/com/yunhai/flowcenter/repository/FlwTaskRepository.java
|
||||
M src/main/java/com/yunhai/flowcenter/service/FlowCallbackAdminService.java
|
||||
M src/main/java/com/yunhai/flowcenter/service/FlowDefAdminService.java
|
||||
M src/main/java/com/yunhai/flowcenter/service/FlowInstanceService.java
|
||||
M src/main/java/com/yunhai/flowcenter/service/FlowRouterAdminService.java
|
||||
M src/main/java/com/yunhai/flowcenter/service/FlowTaskService.java
|
||||
M src/main/java/com/yunhai/flowcenter/service/FormCenterSchemaClient.java
|
||||
M src/main/java/com/yunhai/flowcenter/service/OapiFlowService.java
|
||||
M src/test/java/com/yunhai/flowcenter/callback/CallbackDispatchIT.java
|
||||
M src/test/java/com/yunhai/flowcenter/engine/DefValidatorTest.java
|
||||
M src/test/java/com/yunhai/flowcenter/engine/FlowEngineIT.java
|
||||
M src/test/java/com/yunhai/flowcenter/oapi/OapiFlowContractIT.java
|
||||
M src/test/java/com/yunhai/flowcenter/oapi/OapiFlowTasksByBizTest.java
|
||||
M src/test/java/com/yunhai/flowcenter/service/FlApplyRecoveryTest.java
|
||||
M src/test/java/com/yunhai/flowcenter/service/FlFormBindingTest.java
|
||||
M src/test/java/com/yunhai/flowcenter/service/FlowDefAdminServicePublishContractTest.java
|
||||
M src/test/java/com/yunhai/flowcenter/service/FlowDefAdminServiceUpdateMetaTest.java
|
||||
M src/test/java/com/yunhai/flowcenter/service/FlowRouterAdminServiceRerouteDefTest.java
|
||||
M src/test/java/com/yunhai/flowcenter/service/FormCenterSchemaClientTest.java
|
||||
M src/test/java/com/yunhai/flowcenter/service/OapiFlowApplyDefVersionTest.java
|
||||
M src/test/java/com/yunhai/flowcenter/stub/StubUserClient.java
|
||||
M src/test/java/com/yunhai/flowcenter/support/FlowTestSupport.java
|
||||
M src/test/resources/application-test.yml
|
||||
?? ci/
|
||||
?? docs/form-binding-contract.md
|
||||
?? src/main/java/com/yunhai/flowcenter/callback/CallbackFactStore.java
|
||||
?? src/main/java/com/yunhai/flowcenter/callback/CallbackSourcePolicy.java
|
||||
?? src/main/java/com/yunhai/flowcenter/callback/FlowEventIdentityListener.java
|
||||
?? src/main/java/com/yunhai/flowcenter/callback/FlwStateEventRecorder.java
|
||||
?? src/main/java/com/yunhai/flowcenter/callback/FormalCoordinateReleaseListener.java
|
||||
?? src/main/java/com/yunhai/flowcenter/config/FlowSourceScopeConfiguration.java
|
||||
?? src/main/java/com/yunhai/flowcenter/controller/oapi/FormalApplyCommandController.java
|
||||
?? src/main/java/com/yunhai/flowcenter/engine/VariableContract.java
|
||||
?? src/main/java/com/yunhai/flowcenter/entity/FlwAdvanceIntent.java
|
||||
?? src/main/java/com/yunhai/flowcenter/entity/FlwApplyCommand.java
|
||||
?? src/main/java/com/yunhai/flowcenter/entity/FlwBizCoordinate.java
|
||||
?? src/main/java/com/yunhai/flowcenter/entity/FlwCommandSource.java
|
||||
?? src/main/java/com/yunhai/flowcenter/entity/FlwStateEvent.java
|
||||
?? src/main/java/com/yunhai/flowcenter/entity/FlwUsageGrant.java
|
||||
?? src/main/java/com/yunhai/flowcenter/enums/FormBindingErrorCode.java
|
||||
?? src/main/java/com/yunhai/flowcenter/model/FormalApplyModels.java
|
||||
?? src/main/java/com/yunhai/flowcenter/repository/FlwAdvanceIntentRepository.java
|
||||
?? src/main/java/com/yunhai/flowcenter/repository/FlwApplyCommandRepository.java
|
||||
?? src/main/java/com/yunhai/flowcenter/repository/FlwBizCoordinateRepository.java
|
||||
?? src/main/java/com/yunhai/flowcenter/repository/FlwCommandSourceRepository.java
|
||||
?? src/main/java/com/yunhai/flowcenter/repository/FlwStateEventRepository.java
|
||||
?? src/main/java/com/yunhai/flowcenter/repository/FlwUsageGrantRepository.java
|
||||
?? src/main/java/com/yunhai/flowcenter/service/ApplyVariableValidator.java
|
||||
?? src/main/java/com/yunhai/flowcenter/service/FlowActorContextGate.java
|
||||
?? src/main/java/com/yunhai/flowcenter/service/FlowOapiSourceScope.java
|
||||
?? src/main/java/com/yunhai/flowcenter/service/FlowSourcePrincipalResolver.java
|
||||
?? src/main/java/com/yunhai/flowcenter/service/FlowUsageGrantAdminService.java
|
||||
?? src/main/java/com/yunhai/flowcenter/service/FlowUsageGrantGuard.java
|
||||
?? src/main/java/com/yunhai/flowcenter/service/FormalApplyCommandService.java
|
||||
?? src/main/java/com/yunhai/flowcenter/service/FormalPayloadCanonicalizer.java
|
||||
?? src/main/java/com/yunhai/flowcenter/service/UserBaseActivationClient.java
|
||||
?? src/main/resources/sql/20261025_add_flw_def_line_offline.sql
|
||||
?? src/main/resources/sql/20261025_add_flw_instance_payload_hash.sql
|
||||
?? src/main/resources/sql/20261025_create_flw_state_event_and_intent.sql
|
||||
?? src/main/resources/sql/20261025_create_flw_usage_grant.sql
|
||||
?? src/main/resources/sql/20261025_drop_event_seq_uk.sql
|
||||
?? src/main/resources/sql/20261025_fix_flw_callback_log_uk.sql
|
||||
?? src/main/resources/sql/20261026_d09_activation_intent_draft.sql
|
||||
?? src/main/resources/sql/20261026_formal_commands_draft.sql
|
||||
?? src/test/java/com/yunhai/flowcenter/callback/CallbackConcurrentPushIT.java
|
||||
?? src/test/java/com/yunhai/flowcenter/callback/CallbackFactDispatchIT.java
|
||||
?? src/test/java/com/yunhai/flowcenter/callback/CallbackRecoveryDeliveryTest.java
|
||||
?? src/test/java/com/yunhai/flowcenter/callback/CallbackSourcePolicyTest.java
|
||||
?? src/test/java/com/yunhai/flowcenter/callback/FlStateEventRecoveryTest.java
|
||||
?? src/test/java/com/yunhai/flowcenter/engine/AssigneeResolverEligibilityTest.java
|
||||
?? src/test/java/com/yunhai/flowcenter/engine/FlAdvanceIntentRecoveryTest.java
|
||||
?? src/test/java/com/yunhai/flowcenter/service/D11AssigneeEligibilityIT.java
|
||||
?? src/test/java/com/yunhai/flowcenter/service/FlActorContextGateTest.java
|
||||
?? src/test/java/com/yunhai/flowcenter/service/FlApplyAssignAndVariableTest.java
|
||||
?? src/test/java/com/yunhai/flowcenter/service/FlDefLineOfflineTest.java
|
||||
?? src/test/java/com/yunhai/flowcenter/service/FlUsageGrantTest.java
|
||||
?? src/test/java/com/yunhai/flowcenter/service/FormalApplyCommandIT.java
|
||||
?? src/test/java/com/yunhai/flowcenter/service/FormalFlowHttpFixtureMain.java
|
||||
?? src/test/java/com/yunhai/flowcenter/service/FormalPayloadCanonicalizerTest.java
|
||||
File diff suppressed because it is too large
Load Diff
+4
@@ -0,0 +1,4 @@
|
||||
-------------------------------------------------------------------------------
|
||||
Test set: com.yunhai.flowcenter.callback.CallbackRecoveryDeliveryTest
|
||||
-------------------------------------------------------------------------------
|
||||
Tests run: 9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.69 s - in com.yunhai.flowcenter.callback.CallbackRecoveryDeliveryTest
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
-------------------------------------------------------------------------------
|
||||
Test set: com.yunhai.flowcenter.callback.FlStateEventRecoveryTest
|
||||
-------------------------------------------------------------------------------
|
||||
Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.01 s - in com.yunhai.flowcenter.callback.FlStateEventRecoveryTest
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
-------------------------------------------------------------------------------
|
||||
Test set: com.yunhai.flowcenter.engine.FlAdvanceIntentRecoveryTest
|
||||
-------------------------------------------------------------------------------
|
||||
Tests run: 11, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.471 s - in com.yunhai.flowcenter.engine.FlAdvanceIntentRecoveryTest
|
||||
@@ -0,0 +1,4 @@
|
||||
-------------------------------------------------------------------------------
|
||||
Test set: com.yunhai.flowcenter.service.FlApplyRecoveryTest
|
||||
-------------------------------------------------------------------------------
|
||||
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.093 s - in com.yunhai.flowcenter.service.FlApplyRecoveryTest
|
||||
@@ -0,0 +1,4 @@
|
||||
-------------------------------------------------------------------------------
|
||||
Test set: com.yunhai.flowcenter.service.FlDefLineOfflineTest
|
||||
-------------------------------------------------------------------------------
|
||||
Tests run: 14, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.394 s - in com.yunhai.flowcenter.service.FlDefLineOfflineTest
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
-------------------------------------------------------------------------------
|
||||
Test set: com.yunhai.flowcenter.service.FlNewInstancePathTest
|
||||
-------------------------------------------------------------------------------
|
||||
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.078 s - in com.yunhai.flowcenter.service.FlNewInstancePathTest
|
||||
@@ -0,0 +1,6 @@
|
||||
Checked: 2026-10-27 Asia/Shanghai
|
||||
Docker command available: False
|
||||
CI_TEST_DB_URL configured: False
|
||||
SPRING_DATASOURCE_URL configured: False
|
||||
127.0.0.1:3306 OPEN=False
|
||||
127.0.0.1:13429 OPEN=False
|
||||
@@ -0,0 +1,3 @@
|
||||
4CD39FBEC874DB0A2B5EF20CA5F6A713ADF7E8D9E3B834DF4ECCDFEE51435E04 D:\SoftProject\yunhai\flow-center\service\src\main\java\com\yunhai\flowcenter\service\FlowDefAdminService.java
|
||||
DB6BEBF60CD77FEF1C696527C23B65170E8EAFC108D980352361BA01EB1A0EB0 D:\SoftProject\yunhai\flow-center\service\src\test\java\com\yunhai\flowcenter\service\FlDefLineOfflineTest.java
|
||||
E925A3D742801E7666FAC9C4ED1E628F8C5C91C4E9B5ED5A6B2755F1567F29DE D:\SoftProject\yunhai\flow-center\service\src\test\java\com\yunhai\flowcenter\service\FlNewInstancePathTest.java
|
||||
@@ -0,0 +1,107 @@
|
||||
M .gitea/workflows/build.yml
|
||||
M pom.xml
|
||||
M src/main/java/com/yunhai/flowcenter/callback/CallbackDispatcher.java
|
||||
M src/main/java/com/yunhai/flowcenter/callback/CallbackLogStore.java
|
||||
M src/main/java/com/yunhai/flowcenter/callback/CallbackPushService.java
|
||||
M src/main/java/com/yunhai/flowcenter/controller/api/FlowAdminController.java
|
||||
M src/main/java/com/yunhai/flowcenter/controller/api/FlowUserController.java
|
||||
M src/main/java/com/yunhai/flowcenter/controller/oapi/OapiFlowController.java
|
||||
M src/main/java/com/yunhai/flowcenter/engine/AssigneeResolver.java
|
||||
M src/main/java/com/yunhai/flowcenter/engine/DefValidator.java
|
||||
M src/main/java/com/yunhai/flowcenter/engine/FlowAdvanceService.java
|
||||
M src/main/java/com/yunhai/flowcenter/engine/FlowDefModel.java
|
||||
M src/main/java/com/yunhai/flowcenter/engine/FlowEngine.java
|
||||
M src/main/java/com/yunhai/flowcenter/engine/FlowEvent.java
|
||||
M src/main/java/com/yunhai/flowcenter/engine/FlowNodeAdvanceListener.java
|
||||
M src/main/java/com/yunhai/flowcenter/entity/FlwCallbackLog.java
|
||||
M src/main/java/com/yunhai/flowcenter/entity/FlwCallbackReg.java
|
||||
M src/main/java/com/yunhai/flowcenter/entity/FlwDef.java
|
||||
M src/main/java/com/yunhai/flowcenter/entity/FlwInstance.java
|
||||
M src/main/java/com/yunhai/flowcenter/model/FlowApplyRetryModels.java
|
||||
M src/main/java/com/yunhai/flowcenter/model/OapiModels.java
|
||||
M src/main/java/com/yunhai/flowcenter/repository/FlwCallbackLogRepository.java
|
||||
M src/main/java/com/yunhai/flowcenter/repository/FlwCcRepository.java
|
||||
M src/main/java/com/yunhai/flowcenter/repository/FlwDefRepository.java
|
||||
M src/main/java/com/yunhai/flowcenter/repository/FlwInstanceRepository.java
|
||||
M src/main/java/com/yunhai/flowcenter/repository/FlwTaskRepository.java
|
||||
M src/main/java/com/yunhai/flowcenter/service/FlowCallbackAdminService.java
|
||||
M src/main/java/com/yunhai/flowcenter/service/FlowDefAdminService.java
|
||||
M src/main/java/com/yunhai/flowcenter/service/FlowInstanceService.java
|
||||
M src/main/java/com/yunhai/flowcenter/service/FlowRouterAdminService.java
|
||||
M src/main/java/com/yunhai/flowcenter/service/FlowTaskService.java
|
||||
M src/main/java/com/yunhai/flowcenter/service/FormCenterSchemaClient.java
|
||||
M src/main/java/com/yunhai/flowcenter/service/OapiFlowService.java
|
||||
M src/test/java/com/yunhai/flowcenter/callback/CallbackDispatchIT.java
|
||||
M src/test/java/com/yunhai/flowcenter/engine/DefValidatorTest.java
|
||||
M src/test/java/com/yunhai/flowcenter/engine/FlowEngineIT.java
|
||||
M src/test/java/com/yunhai/flowcenter/oapi/OapiFlowContractIT.java
|
||||
M src/test/java/com/yunhai/flowcenter/oapi/OapiFlowTasksByBizTest.java
|
||||
M src/test/java/com/yunhai/flowcenter/service/FlApplyRecoveryTest.java
|
||||
M src/test/java/com/yunhai/flowcenter/service/FlFormBindingTest.java
|
||||
M src/test/java/com/yunhai/flowcenter/service/FlowDefAdminServicePublishContractTest.java
|
||||
M src/test/java/com/yunhai/flowcenter/service/FlowDefAdminServiceUpdateMetaTest.java
|
||||
M src/test/java/com/yunhai/flowcenter/service/FlowRouterAdminServiceRerouteDefTest.java
|
||||
M src/test/java/com/yunhai/flowcenter/service/FormCenterSchemaClientTest.java
|
||||
M src/test/java/com/yunhai/flowcenter/service/OapiFlowApplyDefVersionTest.java
|
||||
M src/test/java/com/yunhai/flowcenter/stub/StubUserClient.java
|
||||
M src/test/java/com/yunhai/flowcenter/support/FlowTestSupport.java
|
||||
M src/test/resources/application-test.yml
|
||||
?? ci/
|
||||
?? "docs/FL\345\220\216\347\253\257\346\240\270\345\277\203\344\277\256\345\244\215\346\211\247\350\241\214\345\233\236\346\211\247-Codex.md"
|
||||
?? docs/evidence/
|
||||
?? docs/form-binding-contract.md
|
||||
?? src/main/java/com/yunhai/flowcenter/callback/CallbackFactStore.java
|
||||
?? src/main/java/com/yunhai/flowcenter/callback/CallbackSourcePolicy.java
|
||||
?? src/main/java/com/yunhai/flowcenter/callback/FlowEventIdentityListener.java
|
||||
?? src/main/java/com/yunhai/flowcenter/callback/FlwStateEventRecorder.java
|
||||
?? src/main/java/com/yunhai/flowcenter/callback/FormalCoordinateReleaseListener.java
|
||||
?? src/main/java/com/yunhai/flowcenter/config/FlowSourceScopeConfiguration.java
|
||||
?? src/main/java/com/yunhai/flowcenter/controller/oapi/FormalApplyCommandController.java
|
||||
?? src/main/java/com/yunhai/flowcenter/engine/VariableContract.java
|
||||
?? src/main/java/com/yunhai/flowcenter/entity/FlwAdvanceIntent.java
|
||||
?? src/main/java/com/yunhai/flowcenter/entity/FlwApplyCommand.java
|
||||
?? src/main/java/com/yunhai/flowcenter/entity/FlwBizCoordinate.java
|
||||
?? src/main/java/com/yunhai/flowcenter/entity/FlwCommandSource.java
|
||||
?? src/main/java/com/yunhai/flowcenter/entity/FlwStateEvent.java
|
||||
?? src/main/java/com/yunhai/flowcenter/entity/FlwUsageGrant.java
|
||||
?? src/main/java/com/yunhai/flowcenter/enums/FormBindingErrorCode.java
|
||||
?? src/main/java/com/yunhai/flowcenter/model/FormalApplyModels.java
|
||||
?? src/main/java/com/yunhai/flowcenter/repository/FlwAdvanceIntentRepository.java
|
||||
?? src/main/java/com/yunhai/flowcenter/repository/FlwApplyCommandRepository.java
|
||||
?? src/main/java/com/yunhai/flowcenter/repository/FlwBizCoordinateRepository.java
|
||||
?? src/main/java/com/yunhai/flowcenter/repository/FlwCommandSourceRepository.java
|
||||
?? src/main/java/com/yunhai/flowcenter/repository/FlwStateEventRepository.java
|
||||
?? src/main/java/com/yunhai/flowcenter/repository/FlwUsageGrantRepository.java
|
||||
?? src/main/java/com/yunhai/flowcenter/service/ApplyVariableValidator.java
|
||||
?? src/main/java/com/yunhai/flowcenter/service/FlowActorContextGate.java
|
||||
?? src/main/java/com/yunhai/flowcenter/service/FlowOapiSourceScope.java
|
||||
?? src/main/java/com/yunhai/flowcenter/service/FlowSourcePrincipalResolver.java
|
||||
?? src/main/java/com/yunhai/flowcenter/service/FlowUsageGrantAdminService.java
|
||||
?? src/main/java/com/yunhai/flowcenter/service/FlowUsageGrantGuard.java
|
||||
?? src/main/java/com/yunhai/flowcenter/service/FormalApplyCommandService.java
|
||||
?? src/main/java/com/yunhai/flowcenter/service/FormalPayloadCanonicalizer.java
|
||||
?? src/main/java/com/yunhai/flowcenter/service/UserBaseActivationClient.java
|
||||
?? src/main/resources/sql/20261025_add_flw_def_line_offline.sql
|
||||
?? src/main/resources/sql/20261025_add_flw_instance_payload_hash.sql
|
||||
?? src/main/resources/sql/20261025_create_flw_state_event_and_intent.sql
|
||||
?? src/main/resources/sql/20261025_create_flw_usage_grant.sql
|
||||
?? src/main/resources/sql/20261025_drop_event_seq_uk.sql
|
||||
?? src/main/resources/sql/20261025_fix_flw_callback_log_uk.sql
|
||||
?? src/main/resources/sql/20261026_d09_activation_intent_draft.sql
|
||||
?? src/main/resources/sql/20261026_formal_commands_draft.sql
|
||||
?? src/test/java/com/yunhai/flowcenter/callback/CallbackConcurrentPushIT.java
|
||||
?? src/test/java/com/yunhai/flowcenter/callback/CallbackFactDispatchIT.java
|
||||
?? src/test/java/com/yunhai/flowcenter/callback/CallbackRecoveryDeliveryTest.java
|
||||
?? src/test/java/com/yunhai/flowcenter/callback/CallbackSourcePolicyTest.java
|
||||
?? src/test/java/com/yunhai/flowcenter/callback/FlStateEventRecoveryTest.java
|
||||
?? src/test/java/com/yunhai/flowcenter/engine/AssigneeResolverEligibilityTest.java
|
||||
?? src/test/java/com/yunhai/flowcenter/engine/FlAdvanceIntentRecoveryTest.java
|
||||
?? src/test/java/com/yunhai/flowcenter/service/D11AssigneeEligibilityIT.java
|
||||
?? src/test/java/com/yunhai/flowcenter/service/FlActorContextGateTest.java
|
||||
?? src/test/java/com/yunhai/flowcenter/service/FlApplyAssignAndVariableTest.java
|
||||
?? src/test/java/com/yunhai/flowcenter/service/FlDefLineOfflineTest.java
|
||||
?? src/test/java/com/yunhai/flowcenter/service/FlNewInstancePathTest.java
|
||||
?? src/test/java/com/yunhai/flowcenter/service/FlUsageGrantTest.java
|
||||
?? src/test/java/com/yunhai/flowcenter/service/FormalApplyCommandIT.java
|
||||
?? src/test/java/com/yunhai/flowcenter/service/FormalFlowHttpFixtureMain.java
|
||||
?? src/test/java/com/yunhai/flowcenter/service/FormalPayloadCanonicalizerTest.java
|
||||
@@ -0,0 +1 @@
|
||||
git apply --reverse --check task-only.patch exit=0
|
||||
@@ -0,0 +1,281 @@
|
||||
diff --git a/src/main/java/com/yunhai/flowcenter/service/FlowDefAdminService.java b/src/main/java/com/yunhai/flowcenter/service/FlowDefAdminService.java
|
||||
index da109e3..3837c1a 100644
|
||||
--- a/src/main/java/com/yunhai/flowcenter/service/FlowDefAdminService.java
|
||||
+++ b/src/main/java/com/yunhai/flowcenter/service/FlowDefAdminService.java
|
||||
@@ -299,7 +299,7 @@ public class FlowDefAdminService {
|
||||
|
||||
private void requireLine(Long schId, String flowGuid) {
|
||||
if (flowGuid == null || flowGuid.trim().isEmpty()
|
||||
- || defRepository.findFirstBySchIdAndFlowGuidOrderByVersionDesc(schId, flowGuid.trim())
|
||||
+ || !defRepository.findFirstBySchIdAndFlowGuidOrderByVersionDesc(schId, flowGuid.trim())
|
||||
.isPresent()) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "流程线不存在:" + flowGuid);
|
||||
}
|
||||
diff --git a/src/test/java/com/yunhai/flowcenter/service/FlDefLineOfflineTest.java b/src/test/java/com/yunhai/flowcenter/service/FlDefLineOfflineTest.java
|
||||
index 76bbf34..3cff9e9 100644
|
||||
--- a/src/test/java/com/yunhai/flowcenter/service/FlDefLineOfflineTest.java
|
||||
+++ b/src/test/java/com/yunhai/flowcenter/service/FlDefLineOfflineTest.java
|
||||
@@ -18,6 +18,9 @@ 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.junit.jupiter.params.ParameterizedTest;
|
||||
+import org.junit.jupiter.params.provider.NullAndEmptySource;
|
||||
+import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
@@ -33,7 +36,9 @@ import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
+import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
+import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
@@ -53,43 +58,17 @@ class FlDefLineOfflineTest {
|
||||
private static final String GUID = "g-line";
|
||||
|
||||
private FlwDefRepository defRepository;
|
||||
- private FlwDefAdminServiceSpy adminService;
|
||||
+ private FlowDefAdminService adminService;
|
||||
private OapiFlowService oapiService;
|
||||
private FlwInstanceRepository instances;
|
||||
private com.yunhai.flowcenter.service.FlowInstanceService instanceService;
|
||||
|
||||
- /** 仅暴露 protected/私有依赖注入的薄壳:真实 FlowDefAdminService 构造依赖过多,用 spy 隔离外部协作 */
|
||||
- static class FlwDefAdminServiceSpy {
|
||||
- final FlwDefRepository defRepository;
|
||||
- FlwDefAdminServiceSpy(FlwDefRepository defRepository) {
|
||||
- this.defRepository = defRepository;
|
||||
- }
|
||||
- int offlineLine(Long schId, String flowGuid) {
|
||||
- if (flowGuid == null || flowGuid.trim().isEmpty()
|
||||
- || !defRepository.findFirstBySchIdAndFlowGuidOrderByVersionDesc(schId, flowGuid.trim())
|
||||
- .isPresent()) {
|
||||
- throw new BizException(com.yunhai.platform.utils.exception.ErrorCode.PARAM_ERROR,
|
||||
- "流程线不存在:" + flowGuid);
|
||||
- }
|
||||
- return defRepository.updateLineOffline(schId, flowGuid.trim(), 1);
|
||||
- }
|
||||
- int enableLine(Long schId, String flowGuid) {
|
||||
- if (flowGuid == null || flowGuid.trim().isEmpty()
|
||||
- || !defRepository.findFirstBySchIdAndFlowGuidOrderByVersionDesc(schId, flowGuid.trim())
|
||||
- .isPresent()) {
|
||||
- throw new BizException(com.yunhai.platform.utils.exception.ErrorCode.PARAM_ERROR,
|
||||
- "流程线不存在:" + flowGuid);
|
||||
- }
|
||||
- return defRepository.updateLineOffline(schId, flowGuid.trim(), 0);
|
||||
- }
|
||||
- }
|
||||
-
|
||||
@BeforeEach
|
||||
@SuppressWarnings("unchecked")
|
||||
void setUp() {
|
||||
defRepository = mock(FlwDefRepository.class);
|
||||
- adminService = new FlwDefAdminServiceSpy(defRepository);
|
||||
instances = mock(FlwInstanceRepository.class);
|
||||
+ adminService = newFlowDefAdminService();
|
||||
instanceService = mock(FlowInstanceService.class);
|
||||
oapiService = new OapiFlowService(defRepository, instances,
|
||||
mock(FlwTaskRepository.class), mock(FlwCcRepository.class),
|
||||
@@ -121,9 +100,61 @@ class FlDefLineOfflineTest {
|
||||
assertThatThrownBy(() -> adminService.offlineLine(SCH, "g-nope"))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("流程线不存在");
|
||||
+ assertThatThrownBy(() -> adminService.enableLine(SCH, "g-nope"))
|
||||
+ .isInstanceOf(BizException.class)
|
||||
+ .hasMessageContaining("流程线不存在");
|
||||
+ verify(defRepository, never()).updateLineOffline(anyLong(), anyString(), anyInt());
|
||||
+ }
|
||||
+
|
||||
+ @ParameterizedTest
|
||||
+ @NullAndEmptySource
|
||||
+ @ValueSource(strings = {" ", "\t\r\n"})
|
||||
+ void blankGuidRejectedOnBothPaths(String guid) {
|
||||
+ assertThatThrownBy(() -> adminService.offlineLine(SCH, guid))
|
||||
+ .isInstanceOf(BizException.class).hasMessageContaining("流程线不存在");
|
||||
+ assertThatThrownBy(() -> adminService.enableLine(SCH, guid))
|
||||
+ .isInstanceOf(BizException.class).hasMessageContaining("流程线不存在");
|
||||
+ verify(defRepository, never()).updateLineOffline(anyLong(), anyString(), anyInt());
|
||||
+ verify(defRepository, never()).findFirstBySchIdAndFlowGuidOrderByVersionDesc(any(), any());
|
||||
+ }
|
||||
+
|
||||
+ @Test
|
||||
+ void otherSchoolCannotChangeExistingLine() {
|
||||
+ assertThatThrownBy(() -> adminService.offlineLine(2L, GUID))
|
||||
+ .isInstanceOf(BizException.class).hasMessageContaining("流程线不存在");
|
||||
+ assertThatThrownBy(() -> adminService.enableLine(2L, GUID))
|
||||
+ .isInstanceOf(BizException.class).hasMessageContaining("流程线不存在");
|
||||
+ verify(defRepository, times(2)).findFirstBySchIdAndFlowGuidOrderByVersionDesc(2L, GUID);
|
||||
verify(defRepository, never()).updateLineOffline(anyLong(), anyString(), anyInt());
|
||||
}
|
||||
|
||||
+ @Test
|
||||
+ void guidTrimmedForBothLookupAndUpdate() {
|
||||
+ when(defRepository.updateLineOffline(SCH, GUID, 1)).thenReturn(3);
|
||||
+ when(defRepository.updateLineOffline(SCH, GUID, 0)).thenReturn(3);
|
||||
+ assertThat(adminService.offlineLine(SCH, " " + GUID + "\t")).isEqualTo(3);
|
||||
+ assertThat(adminService.enableLine(SCH, "\t" + GUID + " ")).isEqualTo(3);
|
||||
+ verify(defRepository).updateLineOffline(SCH, GUID, 1);
|
||||
+ verify(defRepository).updateLineOffline(SCH, GUID, 0);
|
||||
+ }
|
||||
+
|
||||
+ @Test
|
||||
+ void lineToggleDoesNotChangePublishedDefinitionOrInflightInstances() {
|
||||
+ FlwDef published = def(9, 3, 0);
|
||||
+ String frozenJson = published.getContentJson();
|
||||
+ when(defRepository.findFirstBySchIdAndFlowGuidOrderByVersionDesc(SCH, GUID))
|
||||
+ .thenReturn(Optional.of(published));
|
||||
+
|
||||
+ adminService.offlineLine(SCH, GUID);
|
||||
+ adminService.enableLine(SCH, GUID);
|
||||
+
|
||||
+ assertThat(published.getVersion()).isEqualTo(3);
|
||||
+ assertThat(published.getStatus()).isEqualTo(FlwEnums.DefStatus.PUBLISHED.getCode());
|
||||
+ assertThat(published.getContentJson()).isEqualTo(frozenJson);
|
||||
+ verify(defRepository, never()).save(any());
|
||||
+ verifyNoInteractions(instances, instanceService);
|
||||
+ }
|
||||
+
|
||||
// ==================== 新发起/预览的线级守卫 ====================
|
||||
|
||||
@Test
|
||||
@@ -229,7 +260,7 @@ class FlDefLineOfflineTest {
|
||||
mock(com.yunhai.flowcenter.repository.FlwRouterRepository.class),
|
||||
mock(com.yunhai.flowcenter.repository.FlwCallbackRegRepository.class),
|
||||
appRepo(true),
|
||||
- mock(com.yunhai.flowcenter.repository.FlwInstanceRepository.class),
|
||||
+ instances,
|
||||
mock(FlowDeptAdminService.class),
|
||||
mock(FlowEngine.class),
|
||||
new ObjectMapper(),
|
||||
diff --git a/src/test/java/com/yunhai/flowcenter/service/FlNewInstancePathTest.java b/src/test/java/com/yunhai/flowcenter/service/FlNewInstancePathTest.java
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/src/test/java/com/yunhai/flowcenter/service/FlNewInstancePathTest.java
|
||||
@@ -0,0 +1,122 @@
|
||||
+package com.yunhai.flowcenter.service;
|
||||
+
|
||||
+import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
+import com.yunhai.flowcenter.engine.*;
|
||||
+import com.yunhai.flowcenter.entity.*;
|
||||
+import com.yunhai.flowcenter.repository.*;
|
||||
+import org.junit.jupiter.api.Test;
|
||||
+import org.springframework.context.ApplicationEventPublisher;
|
||||
+import org.springframework.test.util.ReflectionTestUtils;
|
||||
+import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
+
|
||||
+import javax.persistence.EntityManager;
|
||||
+import java.util.*;
|
||||
+import java.util.concurrent.atomic.AtomicReference;
|
||||
+
|
||||
+import static org.assertj.core.api.Assertions.assertThat;
|
||||
+import static org.mockito.ArgumentMatchers.*;
|
||||
+import static org.mockito.Mockito.*;
|
||||
+
|
||||
+/** Real service/engine/activation code with repository mocks. Does not prove DB or transaction behavior. */
|
||||
+class FlNewInstancePathTest {
|
||||
+ @Test
|
||||
+ void freshInstanceCreatesProvenTaskAndApprovesWhileLineIsOffline() {
|
||||
+ ObjectMapper mapper = new ObjectMapper();
|
||||
+ FlwDefRepository defs = mock(FlwDefRepository.class);
|
||||
+ FlwInstanceRepository instances = mock(FlwInstanceRepository.class);
|
||||
+ FlwTaskRepository tasks = mock(FlwTaskRepository.class);
|
||||
+ FlwBranchRepository branches = mock(FlwBranchRepository.class);
|
||||
+ FlwAdvanceIntentRepository intents = mock(FlwAdvanceIntentRepository.class);
|
||||
+ AssigneeResolver assignees = mock(AssigneeResolver.class);
|
||||
+ ApplicationEventPublisher publisher = mock(ApplicationEventPublisher.class);
|
||||
+ List<FlowEvent> events = new ArrayList<>();
|
||||
+ doAnswer(call -> { events.add(call.getArgument(0)); return null; })
|
||||
+ .when(publisher).publishEvent(any(FlowEvent.class));
|
||||
+ FlowEngine engine = new FlowEngine(mapper, tasks, branches, mock(FlwCcRepository.class),
|
||||
+ assignees, mock(ConditionRouteService.class), publisher);
|
||||
+ FlowInstanceService apply = new FlowInstanceService(instances, defs, tasks, branches, engine, publisher);
|
||||
+ FlowTaskService handle = new FlowTaskService(mapper, tasks, instances, branches, defs,
|
||||
+ assignees, engine, publisher);
|
||||
+ FlowAdvanceService advance = new FlowAdvanceService(instances, defs, tasks, engine, intents);
|
||||
+ ReflectionTestUtils.setField(advance, "entityManager", mock(EntityManager.class));
|
||||
+ FlowNodeAdvanceListener listener = new FlowNodeAdvanceListener(advance, intents);
|
||||
+
|
||||
+ FlwDef def = new FlwDef();
|
||||
+ def.setId(7L); def.setSchId(1L); def.setFlowGuid("fresh-line");
|
||||
+ def.setCode("fresh"); def.setVersion(1); def.setStatus(1); def.setLineOffline(0);
|
||||
+ def.setContentJson("{\"start\":\"start\",\"nodes\":["
|
||||
+ + "{\"id\":\"start\",\"type\":\"START\"},"
|
||||
+ + "{\"id\":\"review\",\"type\":\"APPROVAL\",\"name\":\"Review\",\"countersign\":\"ALL\"},"
|
||||
+ + "{\"id\":\"end\",\"type\":\"END\"}],"
|
||||
+ + "\"edges\":[{\"from\":\"start\",\"to\":\"review\"},{\"from\":\"review\",\"to\":\"end\"}]}");
|
||||
+ when(defs.findById(7L)).thenReturn(Optional.of(def));
|
||||
+ when(assignees.resolve(eq(1L), any())).thenReturn(Collections.singletonList(new AssigneeRef(201, "Reviewer")));
|
||||
+ AtomicReference<FlwInstance> created = new AtomicReference<>();
|
||||
+ when(instances.saveAndFlush(any())).thenAnswer(call -> {
|
||||
+ FlwInstance instance = call.getArgument(0);
|
||||
+ if (instance.getId() == null) instance.setId(900L);
|
||||
+ created.set(instance); return instance;
|
||||
+ });
|
||||
+ when(instances.findById(900L)).thenAnswer(call -> Optional.ofNullable(created.get()));
|
||||
+ AtomicReference<FlwTask> task = new AtomicReference<>();
|
||||
+ when(tasks.save(any())).thenAnswer(call -> {
|
||||
+ FlwTask row = call.getArgument(0); row.setId(33L); task.set(row); return row;
|
||||
+ });
|
||||
+ when(tasks.findById(33L)).thenAnswer(call -> Optional.ofNullable(task.get()));
|
||||
+ when(tasks.findByInstanceIdAndStatus(900L, 0)).thenAnswer(call ->
|
||||
+ task.get().getStatus() == 0 ? Collections.singletonList(task.get()) : Collections.emptyList());
|
||||
+ when(tasks.claimAndFinish(eq(33L), eq(1), any(), any(), any())).thenAnswer(call -> {
|
||||
+ task.get().setStatus(1); task.get().setEdgeName(call.getArgument(4)); return 1;
|
||||
+ });
|
||||
+ AtomicReference<FlwAdvanceIntent> activation = new AtomicReference<>();
|
||||
+ when(intents.save(any())).thenAnswer(call -> {
|
||||
+ FlwAdvanceIntent row = call.getArgument(0); row.setId(80L); activation.set(row); return row;
|
||||
+ });
|
||||
+ when(intents.findByIdForUpdate(80L)).thenAnswer(call -> Optional.of(activation.get()));
|
||||
+ when(intents.findByInstanceIdAndNodeIdOrderByIdDesc(900L, "review"))
|
||||
+ .thenAnswer(call -> Collections.singletonList(activation.get()));
|
||||
+ when(intents.findFirstByInstanceIdAndNodeIdAndActivationTaskIdIsNotNullOrderByIdDesc(900L, "review"))
|
||||
+ .thenAnswer(call -> Optional.of(activation.get()));
|
||||
+
|
||||
+ FlowInstanceService.ApplyCommand command = new FlowInstanceService.ApplyCommand(
|
||||
+ 1L, def, "test", "fresh-20261027", "Fresh", "/test", "{}", 101, "Applicant",
|
||||
+ null, "100001", "notice", null);
|
||||
+ FlowInstanceService.GuardedOutcome outcome = apply.applyGuarded(command,
|
||||
+ new FlowInstanceService.ApplyRetryPolicy("fresh-token", 1, false));
|
||||
+ assertThat(outcome.duplicated).isFalse();
|
||||
+ assertThat(created.get().getCurrentNodes()).isEqualTo("review");
|
||||
+ assertThat(task.get().getStatus()).isZero();
|
||||
+ assertThat(task.get().getAssigneeId()).isEqualTo(201);
|
||||
+ FlowEvent taskCreated = events.stream().filter(e -> e.getType() == FlowEvent.Type.TASK_CREATED)
|
||||
+ .findFirst().orElseThrow(AssertionError::new);
|
||||
+ // Explicit listener harness: test synchronizations only, no real transaction/AFTER_COMMIT claim.
|
||||
+ TransactionSynchronizationManager.initSynchronization();
|
||||
+ try {
|
||||
+ listener.persistIntent(taskCreated);
|
||||
+ TransactionSynchronizationManager.getSynchronizations().forEach(sync -> sync.beforeCommit(false));
|
||||
+ } finally {
|
||||
+ TransactionSynchronizationManager.clearSynchronization();
|
||||
+ }
|
||||
+ assertThat(activation.get().containsTask(task.get().getId())).isTrue();
|
||||
+ assertThat(activation.get().getActivationDefId()).isEqualTo(7L);
|
||||
+ assertThat(activation.get().getActivationApplyRound()).isEqualTo(1);
|
||||
+
|
||||
+ // Existing tasks resolve their pinned definition even when the line is stopped for new applications.
|
||||
+ def.setLineOffline(1);
|
||||
+ assertThat(handle.handle(1L, 33L, true, "Approved", 201).getStatus()).isEqualTo(1);
|
||||
+ FlowEvent handled = events.stream().filter(e -> e.getType() == FlowEvent.Type.TASK_HANDLED)
|
||||
+ .findFirst().orElseThrow(AssertionError::new);
|
||||
+ listener.persistIntent(handled);
|
||||
+ listener.onTaskHandled(handled);
|
||||
+ assertThat(activation.get().getStatus()).isEqualTo("DONE");
|
||||
+ assertThat(activation.get().getLastError()).isNull();
|
||||
+ assertThat(created.get().getStatus()).isEqualTo(20);
|
||||
+ assertThat(created.get().getCurrentNodes()).isNullOrEmpty();
|
||||
+ assertThat(created.get().getDefId()).isEqualTo(7L);
|
||||
+ assertThat(created.get().getDefVersion()).isEqualTo(1);
|
||||
+ assertThat(events).extracting(FlowEvent::getType)
|
||||
+ .contains(FlowEvent.Type.SUBMITTED, FlowEvent.Type.TASK_CREATED,
|
||||
+ FlowEvent.Type.TASK_HANDLED, FlowEvent.Type.COMPLETED);
|
||||
+ verify(defs, never()).findFirstBySchIdAndFlowGuidAndStatusOrderByVersionDesc(any(), any(), any());
|
||||
+ }
|
||||
+}
|
||||
Reference in New Issue
Block a user