Compare commits
3
Commits
a10f2e054c
...
11e077867b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11e077867b | ||
|
|
f7819b14e2 | ||
|
|
7eeaea55f8 |
@@ -9,13 +9,17 @@ name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [ main, codex/core-rc-20261023 ]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
cache_bust:
|
||||
description: '构建前清空 maven m2 缓存'
|
||||
required: false
|
||||
default: 'false'
|
||||
tests_only:
|
||||
description: '只验证测试和 JAR,不构建推送镜像或通知建档'
|
||||
required: false
|
||||
default: 'true'
|
||||
|
||||
jobs:
|
||||
build-push:
|
||||
@@ -36,6 +40,10 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
git clone --depth 1 --branch main "http://x-access-token:${GITHUB_TOKEN}@gitea.yunhaixingtong.com/${{ github.repository }}.git" /tmp/src
|
||||
git -C /tmp/src fetch --depth 1 origin "$GITHUB_SHA"
|
||||
git -C /tmp/src checkout --detach FETCH_HEAD
|
||||
test "$(git -C /tmp/src rev-parse HEAD)" = "$GITHUB_SHA"
|
||||
echo "CI_SOURCE_SHA=$GITHUB_SHA"
|
||||
cp -a /tmp/src/. .
|
||||
rm -rf /tmp/src
|
||||
# 取提交备注首行,脱去换行/双引号/反斜杠(避免破坏 webhook JSON),截断到 200 字符
|
||||
@@ -64,6 +72,7 @@ jobs:
|
||||
# GITHUB_RUN_ID 实例级唯一,再拼时间戳兜底。
|
||||
BUILD_CONTAINER="mavenbuild-${GITHUB_RUN_ID:-0}-$(date +%s)"
|
||||
echo "BUILD_CONTAINER=${BUILD_CONTAINER}"
|
||||
trap 'docker rm -f "$BUILD_CONTAINER" >/dev/null 2>&1 || true' EXIT
|
||||
docker rm -f "$BUILD_CONTAINER" 2>/dev/null || true
|
||||
docker run -d --name "$BUILD_CONTAINER" \
|
||||
--network infra-infra-net \
|
||||
@@ -71,13 +80,56 @@ jobs:
|
||||
-w /build maven:3.6.3-jdk-8 sleep 3600
|
||||
docker cp "${PWD}/." "$BUILD_CONTAINER":/build/
|
||||
docker exec "$BUILD_CONTAINER" sh -c "printf '%s' '<settings><profiles><profile><id>nexus-repo</id><repositories><repository><id>nexus-internal</id><url>http://nexus:8081/repository/maven-public/</url><releases><enabled>true</enabled></releases><snapshots><enabled>true</enabled><updatePolicy>always</updatePolicy></snapshots></repository></repositories></profile></profiles><activeProfiles><activeProfile>nexus-repo</activeProfile></activeProfiles></settings>' > /root/.m2/settings.xml"
|
||||
docker exec "$BUILD_CONTAINER" sh -c 'cd /build && mvn -B -DskipTests clean package'
|
||||
# 为真实 MySQL 测试创建本次 job 独占的临时数据库;不映射宿主端口、不连接业务库。
|
||||
DB_CONTAINER="${BUILD_CONTAINER}-db"
|
||||
cleanup_ci() {
|
||||
result=$?
|
||||
trap - EXIT
|
||||
docker rm -fv "$BUILD_CONTAINER" "$DB_CONTAINER" >/dev/null 2>&1 || true
|
||||
docker info >/dev/null 2>&1 || { echo 'CI_CONTAINER_CLEANUP=UNVERIFIED' >&2; exit 1; }
|
||||
if docker inspect "$BUILD_CONTAINER" >/dev/null 2>&1 || docker inspect "$DB_CONTAINER" >/dev/null 2>&1; then
|
||||
echo 'CI_CONTAINER_CLEANUP=FAILED' >&2
|
||||
exit 1
|
||||
fi
|
||||
echo 'CI_CONTAINER_CLEANUP=PASS'
|
||||
exit "$result"
|
||||
}
|
||||
trap cleanup_ci EXIT
|
||||
export MYSQL_ROOT_PASSWORD=$(od -An -N32 -tx1 /dev/urandom | tr -d ' \n')
|
||||
export MYSQL_PASSWORD=$(od -An -N32 -tx1 /dev/urandom | tr -d ' \n')
|
||||
docker create --name "$DB_CONTAINER" --network infra-infra-net \
|
||||
-e MYSQL_DATABASE=flowcenter_ci -e MYSQL_USER=flow_ci \
|
||||
-e MYSQL_PASSWORD -e MYSQL_ROOT_PASSWORD mysql:8.0 --skip-log-bin >/dev/null
|
||||
docker cp "${PWD}/ci/flow-test-schema.sql" "$DB_CONTAINER":/docker-entrypoint-initdb.d/001-schema.sql
|
||||
docker start "$DB_CONTAINER" >/dev/null
|
||||
ready=0
|
||||
for n in $(seq 1 60); do
|
||||
if docker exec "$DB_CONTAINER" sh -c 'MYSQL_PWD="$MYSQL_PASSWORD" mysql -h127.0.0.1 -u"$MYSQL_USER" "$MYSQL_DATABASE" -Nse "SELECT 1" >/dev/null 2>&1'; then ready=1; break; fi
|
||||
sleep 2
|
||||
done
|
||||
[ "$ready" -eq 1 ] || { echo 'Isolated CI MySQL did not become ready' >&2; exit 1; }
|
||||
export CI_TEST_DB_URL="jdbc:mysql://${DB_CONTAINER}:3306/flowcenter_ci?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"
|
||||
export CI_TEST_DB_USER=flow_ci CI_TEST_DB_PASSWORD="$MYSQL_PASSWORD"
|
||||
tables=$(docker exec "$DB_CONTAINER" sh -c 'MYSQL_PWD="$MYSQL_PASSWORD" mysql -h127.0.0.1 -u"$MYSQL_USER" "$MYSQL_DATABASE" -Nse "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema=DATABASE()"')
|
||||
[ "$tables" = 14 ] || { echo "Expected 14 CI schema tables, got $tables" >&2; exit 1; }
|
||||
echo "CI_SCHEMA_TABLES=$tables"
|
||||
# 先在独立空 schema 验证真实 validate 失败会阻断打包;随后再生成正向候选 JAR。
|
||||
printf '%s\n' 'CREATE DATABASE flowcenter_ci_invalid;' "GRANT ALL ON flowcenter_ci_invalid.* TO 'flow_ci'@'%';" | \
|
||||
docker exec -i "$DB_CONTAINER" sh -c 'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysql -uroot'
|
||||
positive_db_url="$CI_TEST_DB_URL"
|
||||
export CI_TEST_DB_URL="jdbc:mysql://${DB_CONTAINER}:3306/flowcenter_ci_invalid?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"
|
||||
docker exec -e CI_TEST_DB_URL -e CI_TEST_DB_USER -e CI_TEST_DB_PASSWORD \
|
||||
"$BUILD_CONTAINER" sh -c 'cd /build && sh ci/verify-failure-guard.sh'
|
||||
export CI_TEST_DB_URL="$positive_db_url"
|
||||
docker exec -e CI_TEST_DB_URL -e CI_TEST_DB_USER -e CI_TEST_DB_PASSWORD \
|
||||
"$BUILD_CONTAINER" sh -c 'cd /build && sh ci/test-gate.sh clean package'
|
||||
docker exec "$BUILD_CONTAINER" sh -c 'cd /build/target && cp *.jar app.jar'
|
||||
mkdir -p "${PWD}/target"
|
||||
docker cp "$BUILD_CONTAINER":/build/target/app.jar "${PWD}/target/app.jar"
|
||||
docker rm -f "$BUILD_CONTAINER"
|
||||
|
||||
- name: 构建并推送镜像
|
||||
if: ${{ success() && github.ref == 'refs/heads/main' && inputs.tests_only != 'true' }}
|
||||
run: |
|
||||
set -e
|
||||
TAG=$(echo "$GITHUB_SHA" | cut -c1-8)
|
||||
@@ -88,6 +140,7 @@ jobs:
|
||||
|
||||
# 通知 projman:webhook 收到后只建档 Release(status=pending),部署由运维在 DeployTab 手动触发(App.deployMode=manual)
|
||||
- name: 通知 projman 建档(部署由运维手动触发)
|
||||
if: ${{ success() && github.ref == 'refs/heads/main' && inputs.tests_only != 'true' }}
|
||||
run: |
|
||||
curl -sS -X POST "http://192.168.10.160:8090/projman/api/deploy/webhook" \
|
||||
-H "Content-Type: application/json" \
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# FL 指定门禁与独立 MySQL
|
||||
|
||||
`test-gate.sh` 是 Gitea 与本地等价 CI 共用的入口。无参运行 `test`;工作流使用
|
||||
`sh ci/test-gate.sh clean package`,真实测试未通过不会生成待发布 JAR,后续镜像步骤不会运行。
|
||||
所有指定类必须存在;Surefire 不允许零测试;Hibernate 固定 `ddl-auto=validate`。
|
||||
|
||||
工作流在 `infra-infra-net` 内新建 `${BUILD_CONTAINER}-db`,镜像 `mysql:8.0`,不映射宿主端口。
|
||||
在启动之前把 `flow-test-schema.sql` 复制进初始化目录;该文件只含 14 张表的结构,不含业务数据。
|
||||
镜像初始化阶段建立测试 schema,测试阶段只 validate。每个 job 的数据库容器名和随机凭据独立,
|
||||
EXIT trap 回收 Maven/MySQL 容器;不访问 160 业务库。
|
||||
|
||||
随机凭据仅经容器环境变量传递,不写仓库或日志。Maven 子容器必须获得:
|
||||
|
||||
- `CI_TEST_DB_URL`:MySQL JDBC URL;schema 必须为 `flowcenter_ci` 或 `flowcenter_ci_*`。
|
||||
- `CI_TEST_DB_USER`、`CI_TEST_DB_PASSWORD`:本次数据库的专用账号与密码。
|
||||
|
||||
脚本只允许 localhost、127.0.0.1 或 `mavenbuild-*-db` 主机,随后映射为 Spring datasource 环境变量。
|
||||
`OapiFlowTasksByBizTest` 在任何 cleanDb 前检查 JDBC 实际连接的 host/schema,防止 profile 覆盖错误。
|
||||
CI 场景必须实际连接 `flowcenter_ci*`,不能悄悄改用默认开发库;11 项继续为真 MySQL 测试。
|
||||
|
||||
本地等价运行:在独立测试库按此 schema 初始化后,通过进程环境设置上述三个变量,
|
||||
再执行 `sh ci/test-gate.sh clean package`。凭据不要写入命令历史或提交到仓库。
|
||||
|
||||
本次已在本机 MySQL 8.0.45 做正反等价验证:完整 schema 通过,空 schema 验证失败且不生成 JAR。
|
||||
三个 workflow 的手动入口新增 `tests_only=true`(默认),可以在 runner 上只验测试/JAR,不推镜像或通知建档。
|
||||
本机没有 Docker,Gitea runner 的容器启动、镜像拉取和网络连通未实跑;这些仍需在 runner 上验证,
|
||||
不能将等价 Maven 证据描述为实际 Gitea workflow 已通过。
|
||||
|
||||
实体结构变更时应显式刷新 SQL 快照并重新验证;不能把 validate 改为 update 来绕过缺表或缺列。
|
||||
|
||||
|
||||
## 核心 RC 功能分支实跑
|
||||
|
||||
`codex/core-rc-20261023` 的 push 只执行测试/JAR;镜像推送与通知同时受 main ref 与 tests_only 条件约束。
|
||||
检出后核验 `CI_SOURCE_SHA` 等于事件 SHA,不能使用随后更新的 main。
|
||||
FL 在同一独立 MySQL 容器额外创建空 `flowcenter_ci_invalid`,先运行 `verify-failure-guard.sh`:
|
||||
要求 11 项真实数据库测试因 validate 缺表报错、Maven 非零且无 JAR;随后对 14 表正常 schema 执行正向门禁。
|
||||
空库检查的预期失败不属于 380 项正向用例。回收检查打印 `CI_CONTAINER_CLEANUP=PASS`,同时删除本次容器的匿名卷。
|
||||
本文件是运行约定;只有取得对应远端准确 SHA 的实际日志,才能宣称 runner 已完成验证。
|
||||
@@ -0,0 +1,330 @@
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
/*!50503 SET NAMES utf8mb4 */;
|
||||
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
|
||||
/*!40103 SET TIME_ZONE='+00:00' */;
|
||||
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
|
||||
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `flw_app` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`create_by` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`create_time` datetime DEFAULT NULL,
|
||||
`update_by` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`update_time` datetime DEFAULT NULL,
|
||||
`app_key` varchar(64) COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`app_name` varchar(128) COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`enabled` int NOT NULL,
|
||||
`sch_id` bigint NOT NULL,
|
||||
`sort_no` int DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_flw_app_sch_key` (`sch_id`,`app_key`),
|
||||
KEY `idx_flw_app_sch_enabled` (`sch_id`,`enabled`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `flw_assignee_map` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`create_by` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`create_time` datetime DEFAULT NULL,
|
||||
`update_by` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`update_time` datetime DEFAULT NULL,
|
||||
`dept_key` varchar(64) COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`dept_name` varchar(128) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`sch_id` bigint NOT NULL,
|
||||
`status` int NOT NULL,
|
||||
`user_id` int NOT NULL,
|
||||
`user_name` varchar(128) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_flw_map_sch_dept_user` (`sch_id`,`dept_key`,`user_id`),
|
||||
KEY `idx_flw_map_sch_dept` (`sch_id`,`dept_key`,`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `flw_attachment` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`create_by` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`create_time` datetime DEFAULT NULL,
|
||||
`update_by` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`update_time` datetime DEFAULT NULL,
|
||||
`field_key` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`file_name` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`file_path` varchar(500) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`file_size` bigint DEFAULT NULL,
|
||||
`instance_id` bigint NOT NULL,
|
||||
`mime_type` varchar(128) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`sch_id` bigint NOT NULL,
|
||||
`web_url` varchar(500) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_flw_att_inst` (`instance_id`,`sch_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `flw_branch` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`create_by` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`create_time` datetime DEFAULT NULL,
|
||||
`update_by` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`update_time` datetime DEFAULT NULL,
|
||||
`branch_key` varchar(64) COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`instance_id` bigint NOT NULL,
|
||||
`status` int NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_flw_branch_inst_key` (`instance_id`,`branch_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `flw_callback_log` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`create_by` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`create_time` datetime DEFAULT NULL,
|
||||
`update_by` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`update_time` datetime DEFAULT NULL,
|
||||
`biz_id` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`biz_type` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`event` varchar(32) COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`event_id` varchar(32) COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`instance_id` bigint NOT NULL,
|
||||
`last_error` varchar(1000) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`last_http_status` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`next_retry_time` datetime DEFAULT NULL,
|
||||
`payload_json` longtext COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`push_time` datetime DEFAULT NULL,
|
||||
`reg_id` bigint NOT NULL,
|
||||
`retry_count` int NOT NULL,
|
||||
`sch_id` bigint NOT NULL,
|
||||
`status` int NOT NULL,
|
||||
`resp_body` text COLLATE utf8mb4_general_ci,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_flw_log_event` (`event_id`),
|
||||
KEY `idx_flw_log_scan` (`status`,`next_retry_time`),
|
||||
KEY `idx_flw_log_inst` (`instance_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `flw_callback_reg` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`create_by` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`create_time` datetime DEFAULT NULL,
|
||||
`update_by` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`update_time` datetime DEFAULT NULL,
|
||||
`app_key` varchar(64) COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`callback_url` varchar(500) COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`event_types` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`flow_guid` varchar(32) COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`sch_id` bigint NOT NULL,
|
||||
`secret` varchar(64) COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`status` int NOT NULL,
|
||||
`base_path` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_flw_reg_sch_app_flow` (`sch_id`,`app_key`,`flow_guid`),
|
||||
KEY `idx_flw_reg_flow` (`sch_id`,`flow_guid`,`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `flw_cc` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`instance_id` bigint NOT NULL COMMENT '归属办件',
|
||||
`sch_id` bigint NOT NULL COMMENT '租户键(冗余,列表免 join)',
|
||||
`node_id` varchar(64) NOT NULL COMMENT '定义节点 id',
|
||||
`node_name` varchar(128) DEFAULT NULL COMMENT '节点名快照',
|
||||
`user_id` int NOT NULL COMMENT '接收人 id',
|
||||
`user_name` varchar(128) DEFAULT NULL COMMENT '接收人姓名快照',
|
||||
`sent_time` datetime(3) NOT NULL COMMENT '抄送时间(节点激活时刻,毫秒)',
|
||||
`create_by` varchar(64) DEFAULT NULL,
|
||||
`create_time` datetime DEFAULT NULL,
|
||||
`update_by` varchar(64) DEFAULT NULL,
|
||||
`update_time` datetime DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_flw_cc_user` (`user_id`,`sent_time`),
|
||||
KEY `idx_flw_cc_inst` (`instance_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='抄送留痕(NOTIFY 节点逐接收人一行)';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `flw_def` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`create_by` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`create_time` datetime DEFAULT NULL,
|
||||
`update_by` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`update_time` datetime DEFAULT NULL,
|
||||
`code` varchar(64) COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`content_json` longtext COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`flow_guid` varchar(32) COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`form_id` bigint DEFAULT NULL,
|
||||
`item_code` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`name` varchar(128) COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`publish_time` datetime DEFAULT NULL,
|
||||
`sch_id` bigint NOT NULL,
|
||||
`status` int NOT NULL,
|
||||
`version` int NOT NULL,
|
||||
`app_key` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`change_log` varchar(500) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`form_uuid` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`form_version` int DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_flw_def_sch_code_ver` (`sch_id`,`code`,`version`),
|
||||
UNIQUE KEY `uk_flw_def_guid_ver` (`sch_id`,`flow_guid`,`version`),
|
||||
KEY `idx_flw_def_guid` (`sch_id`,`flow_guid`,`status`,`version`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `flw_dept` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`create_by` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`create_time` datetime DEFAULT NULL,
|
||||
`update_by` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`update_time` datetime DEFAULT NULL,
|
||||
`dept_name` varchar(128) COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`sch_id` bigint NOT NULL,
|
||||
`source_group_id` int DEFAULT NULL,
|
||||
`status` int NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_flw_dept_sch_srcgroup` (`sch_id`,`source_group_id`),
|
||||
KEY `idx_flw_dept_sch_status` (`sch_id`,`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `flw_dept_member` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`create_by` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`create_time` datetime DEFAULT NULL,
|
||||
`update_by` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`update_time` datetime DEFAULT NULL,
|
||||
`dept_id` bigint NOT NULL,
|
||||
`status` int NOT NULL,
|
||||
`user_id` int NOT NULL,
|
||||
`user_name` varchar(128) COLLATE utf8mb4_general_ci NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_flw_dept_member` (`dept_id`,`user_id`),
|
||||
KEY `idx_flw_dept_member_dept` (`dept_id`,`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `flw_instance` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`create_by` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`create_time` datetime DEFAULT NULL,
|
||||
`update_by` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`update_time` datetime DEFAULT NULL,
|
||||
`biz_id` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`biz_snapshot` longtext COLLATE utf8mb4_general_ci,
|
||||
`biz_title` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`biz_type` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`biz_url` varchar(500) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`current_nodes` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`def_code` varchar(64) COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`def_id` bigint NOT NULL,
|
||||
`def_version` int NOT NULL,
|
||||
`finish_time` datetime DEFAULT NULL,
|
||||
`form_data_json` longtext COLLATE utf8mb4_general_ci,
|
||||
`item_code` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`item_title` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`sch_id` bigint NOT NULL,
|
||||
`status` int NOT NULL,
|
||||
`submit_time` datetime DEFAULT NULL,
|
||||
`submitter_id` int NOT NULL,
|
||||
`submitter_name` varchar(128) COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`version` int NOT NULL,
|
||||
`apply_app_code` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`apply_app_key` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`assign_json` longtext COLLATE utf8mb4_general_ci,
|
||||
`apply_request_token` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`apply_round` int DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_flw_instance_biz` (`sch_id`,`biz_type`,`biz_id`),
|
||||
KEY `idx_flw_inst_submitter` (`sch_id`,`submitter_id`,`status`),
|
||||
KEY `idx_flw_inst_status_time` (`sch_id`,`status`,`submit_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `flw_route_log` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`create_by` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`create_time` datetime DEFAULT NULL,
|
||||
`update_by` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`update_time` datetime DEFAULT NULL,
|
||||
`attempt` int NOT NULL,
|
||||
`decide_time` datetime(3) NOT NULL,
|
||||
`instance_id` bigint NOT NULL,
|
||||
`node_id` varchar(64) COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`node_name` varchar(128) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`outlet` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`raw_response` text COLLATE utf8mb4_general_ci,
|
||||
`router_key` varchar(64) COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`sch_id` bigint NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_flw_route_log_inst` (`instance_id`,`node_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `flw_router` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`create_by` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`create_time` datetime DEFAULT NULL,
|
||||
`update_by` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`update_time` datetime DEFAULT NULL,
|
||||
`base_path` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`name` varchar(128) COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`path` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`router_key` varchar(64) COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`service_key` varchar(64) COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`status` int NOT NULL,
|
||||
`direct_host` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`reg_app_key` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_flw_router_key` (`router_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `flw_task` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`create_by` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`create_time` datetime DEFAULT NULL,
|
||||
`update_by` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`update_time` datetime DEFAULT NULL,
|
||||
`arrive_time` datetime(3) NOT NULL COMMENT '到达时间(毫秒)',
|
||||
`assignee_id` int NOT NULL,
|
||||
`assignee_name` varchar(128) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`deadline` datetime DEFAULT NULL,
|
||||
`handle_time` datetime(3) DEFAULT NULL COMMENT '办理时间(毫秒)',
|
||||
`instance_id` bigint NOT NULL,
|
||||
`node_id` varchar(64) COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`node_name` varchar(128) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`opinion` varchar(500) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`sch_id` bigint NOT NULL,
|
||||
`status` int NOT NULL,
|
||||
`urge_count` int NOT NULL,
|
||||
`edge_name` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_flw_task_inst_status` (`instance_id`,`status`),
|
||||
KEY `idx_flw_task_assignee` (`assignee_id`,`status`,`arrive_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
|
||||
|
||||
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
||||
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
cd "$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||
# 真实 MySQL 测试必须使用本次任务的独立 schema,禁止默认连开发/生产库。
|
||||
: "${CI_TEST_DB_URL:?CI_TEST_DB_URL is required}"
|
||||
: "${CI_TEST_DB_USER:?CI_TEST_DB_USER is required}"
|
||||
: "${CI_TEST_DB_PASSWORD:?CI_TEST_DB_PASSWORD is required}"
|
||||
case "$CI_TEST_DB_URL" in jdbc:mysql://*) ;; *) echo 'Expected a MySQL test URL' >&2; exit 2 ;; esac
|
||||
address=${CI_TEST_DB_URL#jdbc:mysql://}
|
||||
host=${address%%/*}; host=${host%%:*}
|
||||
case "$host" in localhost|127.0.0.1|mavenbuild-*-db) ;; *) echo 'Database host is outside isolated CI allowlist' >&2; exit 2 ;; esac
|
||||
schema=${address#*/}; schema=${schema%%\?*}
|
||||
case "$schema" in flowcenter_ci|flowcenter_ci_*) ;; *) echo 'Database schema is outside isolated CI prefix' >&2; exit 2 ;; esac
|
||||
export SPRING_DATASOURCE_URL="$CI_TEST_DB_URL"
|
||||
export SPRING_DATASOURCE_USERNAME="$CI_TEST_DB_USER"
|
||||
export SPRING_DATASOURCE_PASSWORD="$CI_TEST_DB_PASSWORD"
|
||||
TESTS=FlApplyRecoveryTest,FlFormBindingTest,OapiFlowApplyDefVersionTest,CallbackPropertiesTest,FlowDefAdminServicePublishContractTest,FormCenterSchemaClientTest,OapiFlowTasksByBizTest,OapiFlowP35DoneCcContractTest
|
||||
# Surefire 的选择器部分匹配也可能成功;每个指定类必须随源码存在。
|
||||
oldIFS=$IFS
|
||||
IFS=,
|
||||
for test in $TESTS; do
|
||||
[ -n "$(find src/test/java -type f -name "$test.java" -print -quit)" ] || { echo "Missing required test class: $test" >&2; exit 2; }
|
||||
done
|
||||
IFS=$oldIFS
|
||||
if [ "$#" -eq 0 ]; then set -- test; fi
|
||||
exec mvn -B "$@" -Dtest="$TESTS" -DfailIfNoTests=true -Dsurefire.failIfNoSpecifiedTests=true -DforkCount=0 -Dspring.jpa.hibernate.ddl-auto=validate < /dev/null
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
cd "$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||
: "${CI_TEST_DB_URL:?isolated empty MySQL schema URL is required}"
|
||||
case "$CI_TEST_DB_URL" in */flowcenter_ci_invalid\?*|*/flowcenter_ci_invalid_*\?*) ;; *) echo 'Negative guard requires its own invalid test schema' >&2; exit 2 ;; esac
|
||||
log=$(mktemp /tmp/flow-ci-negative.XXXXXX)
|
||||
trap 'rm -f "$log"' EXIT
|
||||
set +e
|
||||
sh ci/test-gate.sh clean package >"$log" 2>&1
|
||||
code=$?
|
||||
set -e
|
||||
cat "$log"
|
||||
echo "NEGATIVE_MVN_EXIT=$code"
|
||||
[ "$code" -ne 0 ] || { echo 'Empty schema unexpectedly passed' >&2; exit 1; }
|
||||
grep -q 'Schema-validation: missing table' "$log"
|
||||
grep -q 'Tests run: 92, Failures: 0, Errors: 11, Skipped: 0' "$log"
|
||||
[ -z "$(find target -maxdepth 1 -name '*.jar' -print -quit 2>/dev/null)" ] || { echo 'Failed gate produced a JAR' >&2; exit 1; }
|
||||
echo 'CI_EMPTY_SCHEMA_FAILURE_GUARD=PASS'
|
||||
@@ -0,0 +1,38 @@
|
||||
# 流程定义精确表单版本契约(2026-10-23,R2 / R1)
|
||||
|
||||
流程关联表单时必须同时绑定 `formUuid` 与正整数 `formVersion`。保存显式关联、发布、启用统一调用
|
||||
`GET /oapi/form/schema?schId=&uuid=&version=`,核对返回 UUID 和版本;不回落到当前版本。
|
||||
未关联表单的独立流程(Mode C)仍可使用;孤立版本号不是未关联流程,必须拒绝。
|
||||
|
||||
UUID-only 存量定义不能发布或启用,必须重新选择明确版本。当前开发阶段不做存量迁移。
|
||||
本轮不修改任何数据库;部署前核查 `def68` 的现有绑定即可。
|
||||
|
||||
FC 精确 schema 只返回 `publish_time != null` 的版本行。草稿不能作为发布契约。
|
||||
已发布历史版本仍可按坐标读取,不要求它是头表当前版本;停用/软删的历史读取与原授权边界保持。
|
||||
FC 将不存在、不可读、未发布版本统一为 `PARAM_ERROR(2)`,FL 不通过文案猜测资源存在性。
|
||||
|
||||
## 稳定错误码与恢复动作
|
||||
|
||||
API 信封 `code` 使用下列数值;`msg` 仅用于展示,消费方不得解析文案判断重试。
|
||||
|
||||
| code / 名称 | 含义 | 恢复动作 |
|
||||
|---|---|---|
|
||||
| 46101 `VERSION_REQUIRED` | 已指定 UUID,未指定版本 | 重新绑定;发布/启用均阻断,不调用 FC |
|
||||
| 46102 `INVALID_COORDINATE` | 半个坐标、版本非正整数/非整数/溢出 | 修正请求;不盲目重试 |
|
||||
| 46103 `SCHEMA_UNAVAILABLE` | 网络、空响应、服务端故障等拉取失败 | 可重试;客户端每次最多请求 2 次 |
|
||||
| 46104 `SCHEMA_CONTRACT_VIOLATION` | 返回 UUID/版本缺失、不合法或不匹配 | 阻断;检查契约或重新选择坐标,不盲目重试 |
|
||||
| 46105 `RESOURCE_UNAVAILABLE` | FC 业务码 2 或 HTTP 404,指定发布资源不存在或不可读 | 检查资源/发布版本/读授权,不盲目重试 |
|
||||
| 46106 `ACCESS_DENIED` | SDK 身份/授权业务码 3、4 | 修复身份/授权后重试,不自动重放 |
|
||||
|
||||
坐标核验失败、资源不可用与身份拒绝均不在客户端重试循环内重复请求。
|
||||
`SchemaUnavailableException` 与 `SchemaContractViolationException` 在公共边界分别翻译成错误码,
|
||||
保留 cause;不存在被公共包装吞掉后无法命中的分类 catch。
|
||||
启用只核验绑定,不覆盖此前发布的 `formContract` 快照;运行期继续使用流程版本里的原快照。
|
||||
|
||||
## 验证与边界
|
||||
|
||||
`FlFormBindingTest` 覆盖发布/启用缺版本、非法版本、孤立版本、四类失败码和不落库、启用保留快照。
|
||||
`FlowDefAdminServicePublishContractTest` 全部关联夹具带 v3,原表达式键查、类型桶查、域查红牌继续验证。
|
||||
`FormCenterSchemaClientTest` 覆盖不合法返回版本、UUID 不符、按业务码分类及重试次数。
|
||||
FC `OapiSchemaPublishedVersionTest` 验证真实服务层的草稿阻断和历史已发布版可读;其中仓储是桩,
|
||||
不是数据库或 160 验收。
|
||||
@@ -120,6 +120,10 @@ public class CallbackDispatcher {
|
||||
if (event.getData() != null) {
|
||||
data.putAll(event.getData());
|
||||
}
|
||||
// 身份在事件发布线程中冻结,不能用 AFTER_COMMIT 重读到的新轮给旧事件重新贴标。
|
||||
if (event.getRoundIdentity() != null) {
|
||||
data.putAll(event.getRoundIdentity());
|
||||
}
|
||||
payload.put("data", data);
|
||||
String payloadJson = objectMapper.writeValueAsString(payload);
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.yunhai.flowcenter.callback;
|
||||
|
||||
import com.yunhai.flowcenter.engine.FlowEvent;
|
||||
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** 同步发布阶段冻结轮次身份;不修改引擎或 apply/v2 七支分支。 */
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class FlowEventIdentityListener {
|
||||
private final FlwInstanceRepository instances;
|
||||
|
||||
@EventListener
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
public void capture(FlowEvent event) {
|
||||
instances.findById(event.getInstanceId()).ifPresent(instance ->
|
||||
event.captureRoundIdentity(instance.getApplyRound(), instance.getApplyRequestToken()));
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import com.yunhai.flowcenter.entity.FlwCallbackReg;
|
||||
import com.yunhai.flowcenter.entity.FlwDef;
|
||||
import com.yunhai.flowcenter.entity.FlwInstance;
|
||||
import com.yunhai.flowcenter.model.FlowModels;
|
||||
import com.yunhai.flowcenter.enums.FormBindingErrorCode;
|
||||
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
|
||||
import com.yunhai.flowcenter.service.FlowAdminGuard;
|
||||
import com.yunhai.flowcenter.service.FlowCallbackAdminService;
|
||||
@@ -145,7 +146,16 @@ public class FlowAdminController {
|
||||
(String) body.get("contentJson"), (String) body.get("baseGuid"),
|
||||
(String) body.get("appKey"), (String) body.get("changeLog"),
|
||||
(String) body.get("formUuid"),
|
||||
body.get("formVersion") == null ? null : Integer.valueOf(String.valueOf(body.get("formVersion"))));
|
||||
parseFormVersion(body.get("formVersion")));
|
||||
}
|
||||
|
||||
private static Integer parseFormVersion(Object value) {
|
||||
if (value == null) return null;
|
||||
try {
|
||||
return Integer.valueOf(String.valueOf(value));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new BizException(FormBindingErrorCode.INVALID_COORDINATE, "formVersion 必须是正整数", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -69,6 +69,15 @@ public class FlowEvent {
|
||||
private final String operatorName;
|
||||
/** 事件明细(result/opinion/rejectNode/task.created 的 n/m 等),可为空 */
|
||||
private final Map<String, Object> data;
|
||||
private Map<String, Object> roundIdentity;
|
||||
|
||||
public void captureRoundIdentity(Integer round, String requestToken) {
|
||||
if (roundIdentity != null) return;
|
||||
Map<String, Object> identity = new HashMap<>();
|
||||
identity.put("applyRound", round);
|
||||
identity.put("requestToken", requestToken);
|
||||
roundIdentity = java.util.Collections.unmodifiableMap(identity);
|
||||
}
|
||||
|
||||
public static FlowEvent of(Type type, Long instanceId) {
|
||||
return new FlowEvent(type, instanceId, null, null, null, new HashMap<>());
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.yunhai.flowcenter.enums;
|
||||
|
||||
import com.yunhai.platform.utils.exception.IErrorCode;
|
||||
|
||||
/** 管理面表单绑定契约:消费方按数值码判断恢复动作,不解析 msg。 */
|
||||
public enum FormBindingErrorCode implements IErrorCode {
|
||||
VERSION_REQUIRED(46101, "关联表单缺少明确版本"),
|
||||
INVALID_COORDINATE(46102, "关联表单坐标或版本不合法"),
|
||||
SCHEMA_UNAVAILABLE(46103, "关联表单 schema 拉取失败,可重试"),
|
||||
SCHEMA_CONTRACT_VIOLATION(46104, "关联表单 schema 坐标核验失败"),
|
||||
RESOURCE_UNAVAILABLE(46105, "关联表单或指定发布版本不存在或不可读"),
|
||||
ACCESS_DENIED(46106, "关联表单访问身份或授权被拒绝");
|
||||
|
||||
private final int code;
|
||||
private final String msg;
|
||||
|
||||
FormBindingErrorCode(int code, String msg) {
|
||||
this.code = code;
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCode() { return code; }
|
||||
|
||||
@Override
|
||||
public String getMsg() { return msg; }
|
||||
}
|
||||
@@ -60,6 +60,9 @@ public final class FlowApplyRetryModels {
|
||||
private String branch;
|
||||
/** 行上的当前轮次(消费方据此校正自己的命令表认知) */
|
||||
private Integer applyRound;
|
||||
/** 同一响应的真实状态/实例版本;分支判据保持不变。 */
|
||||
private Integer status;
|
||||
private Integer version;
|
||||
}
|
||||
|
||||
/** 分支码常量(与 {@link ApplyResultV2#branch} 同值域) */
|
||||
|
||||
@@ -107,6 +107,8 @@ public final class OapiModels {
|
||||
@Data
|
||||
public static class InstanceVo {
|
||||
private Long instanceId;
|
||||
private Integer applyRound;
|
||||
private String requestToken;
|
||||
private Integer status;
|
||||
private Integer version;
|
||||
private String nodeSummary;
|
||||
|
||||
@@ -7,6 +7,7 @@ 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;
|
||||
@@ -192,12 +193,13 @@ public class FlowDefAdminService {
|
||||
boolean pairing = formUuid != null && !clearing && formVersion == null;
|
||||
boolean versionOnly = formUuid == null && formVersion != null;
|
||||
if (pairing || versionOnly) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR,
|
||||
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.
|
||||
checkedSchema(schId, formUuid.trim(), formVersion, "保存"); // Validate before the first save.
|
||||
}
|
||||
FlwDef saved = save(schId, id, code, name, contentJson, baseGuid, appKey, changeLog);
|
||||
if (formUuid != null) {
|
||||
@@ -229,17 +231,8 @@ public class FlowDefAdminService {
|
||||
FlowDefModel model = FlowDefModel.parse(objectMapper, def.getContentJson());
|
||||
DefValidator.validate(model, enabledDeptKeys(schId), enabledRouterKeys());
|
||||
if (def.getFormUuid() != null && !def.getFormUuid().trim().isEmpty()) {
|
||||
Map<String, Object> snapshot;
|
||||
try {
|
||||
// 管理链始终使用确切版本;缺版本须重新选择,不回落当前发布版。
|
||||
snapshot = checkedSchema(schId, def.getFormUuid().trim(), def.getFormVersion());
|
||||
} catch (FormCenterSchemaClient.SchemaUnavailableException e) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR,
|
||||
"关联表单快照拉取失败,发布已阻断(可重试):" + e.getMessage());
|
||||
} catch (FormCenterSchemaClient.SchemaContractViolationException e) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR,
|
||||
"关联表单版本核验失败,发布已阻断(重试无意义,请重新选择表单与版本):" + e.getMessage());
|
||||
}
|
||||
Map<String, Object> snapshot = checkedSchema(schId, def.getFormUuid().trim(),
|
||||
def.getFormVersion(), "发布");
|
||||
List<String> problems = new ArrayList<>();
|
||||
validateExpressionKeysAgainstSnapshot(model, snapshot, problems);
|
||||
validateExpressionTypesAndDomains(model, snapshot, problems);
|
||||
@@ -248,6 +241,8 @@ public class FlowDefAdminService {
|
||||
"发布校验失败(表达式三查):\n" + String.join("\n", problems));
|
||||
}
|
||||
def.setContentJson(embedFormContract(def.getContentJson(), snapshot));
|
||||
} else if (def.getFormVersion() != null) {
|
||||
throw new BizException(FormBindingErrorCode.INVALID_COORDINATE, "关联表单版本必须同时绑定 formUuid");
|
||||
}
|
||||
def.setStatus(FlwEnums.DefStatus.PUBLISHED.getCode());
|
||||
def.setPublishTime(LocalDateTime.now());
|
||||
@@ -435,22 +430,38 @@ public class FlowDefAdminService {
|
||||
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());
|
||||
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) {
|
||||
if (version == null || version < 1) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "关联表单必须选择确切的正整数版本,请重新选择");
|
||||
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
|
||||
| FormCenterSchemaClient.SchemaContractViolationException e) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "关联表单核验失败,操作已阻断:" + e.getMessage());
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@ package com.yunhai.flowcenter.service;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.yunhai.platform.openapi.core.YschOpenCall;
|
||||
import com.yunhai.platform.openapi.core.OpenApiException;
|
||||
import com.yunhai.platform.utils.exception.DException;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -14,10 +17,10 @@ import java.util.Map;
|
||||
|
||||
/**
|
||||
* form-center schema 快照客户端(2026-10-20 C1 第一刀:发布期契约快照)。<br>
|
||||
* GET {目录解析 form-center}/oapi/form/schema?schId=&uuid= ——按 form_uuid 拉当前已发布版 schema
|
||||
* (T11 建立的 by-uuid 形态;version 缺省=当前发布版)。调用形态对齐 {@link RouteJudgeClient}:
|
||||
* GET {目录解析 form-center}/oapi/form/schema?schId=&uuid=&version= ——只取明确发布版本。
|
||||
* 保存、发布与启用共用精确坐标契约;无 UUID-only 回退。调用形态对齐 YschOpenCall:
|
||||
* 身份=本应用 appKey(签名拦截器由 {@link YschOpenCall} 装配链注入)。<br>
|
||||
* <b>只在发布期调用</b>(拉取失败=发布阻断,可重试);运行期对 form-center 零依赖(快照嵌 def 版本行)。<br>
|
||||
* <b>只在管理期调用</b>(拉取失败=阻断,可重试);运行期对 form-center 零依赖(快照嵌 def 版本行)。<br>
|
||||
* audience 解析:<b>契约 v2 C5/G3 起是字符串数组</b>(breaking,T13 切换)——本客户端按数组读取,
|
||||
* 快照不消费人群语义、只透传形态,兼容旧单值串(读到串自动包一层数组,迁移窗口容错)。
|
||||
*/
|
||||
@@ -60,48 +63,28 @@ public class FormCenterSchemaClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布期快照:{formUuid, formVersion, fields:[{key,type,required,enum?}], audience(数组)}。
|
||||
* fields 由 groups 扁平化(分组只是展示结构,表达式键域与分组无关);
|
||||
* select/radio 类字段的 options 值域收进 enum(供域查③,本轮仅透传)。
|
||||
*/
|
||||
public Map<String, Object> fetchSchema(Long schId, String formUuid) {
|
||||
RuntimeException last = null;
|
||||
for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
||||
try {
|
||||
Map<String, String> query = new HashMap<>();
|
||||
query.put("schId", String.valueOf(schId));
|
||||
query.put("uuid", formUuid);
|
||||
Map<String, Object> data = openCall.get(TARGET, SCHEMA_PATH, query, Map.class);
|
||||
if (data == null) {
|
||||
throw new IllegalStateException("schema 返回空 data");
|
||||
}
|
||||
return toSnapshot(data);
|
||||
} catch (RuntimeException e) {
|
||||
last = e;
|
||||
log.warn("[flow-center] form-center schema 快照拉取失败(第 {}/{} 次):uuid={} {}",
|
||||
attempt, MAX_ATTEMPTS, formUuid, e.toString());
|
||||
} catch (Exception e) {
|
||||
last = new IllegalStateException(e.toString(), e);
|
||||
log.warn("[flow-center] form-center schema 快照拉取异常(第 {}/{} 次):uuid={} {}",
|
||||
attempt, MAX_ATTEMPTS, formUuid, e.toString());
|
||||
}
|
||||
/** FC 将不存在、不可读、未发布的精确版本统一拒绝,不能靠文案区分资源存在性。 */
|
||||
public static class SchemaResourceUnavailableException extends RuntimeException {
|
||||
public SchemaResourceUnavailableException(Throwable cause) {
|
||||
super("表单或指定发布版本不存在或不可读", cause);
|
||||
}
|
||||
}
|
||||
|
||||
public static class SchemaAccessDeniedException extends RuntimeException {
|
||||
public SchemaAccessDeniedException(Throwable cause) {
|
||||
super("表单访问身份或授权被拒绝", cause);
|
||||
}
|
||||
throw new SchemaUnavailableException(
|
||||
"关联表单 schema 快照拉取失败(已重试 " + MAX_ATTEMPTS + " 次,发布阻断,可重试)", last);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布期快照的<b>确切版本</b>形态(v3/TASK_1):{@code GET /oapi/form/schema?schId=&uuid=&version=},
|
||||
* 并把<b>返回坐标与请求坐标逐件核对</b>——不符即 {@link SchemaContractViolationException}(不重试)。
|
||||
*
|
||||
* <p>刻意不与两参 {@link #fetchSchema(Long, String)} 共用循环体:那条是 C1 第一刀的既有行为
|
||||
* ({@code FormCenterSchemaClientTest} 逐条钉住它的重试与文案),改它等于同时动两条契约。
|
||||
* 新管理链(def 带 {@code formVersion})只走本方法。</p>
|
||||
* fields 按分组扁平化,options 值域收进 enum;没有缺省当前版本的重载。
|
||||
*/
|
||||
public Map<String, Object> fetchSchema(Long schId, String formUuid, Integer version) {
|
||||
if (version == null) {
|
||||
throw new IllegalArgumentException("按确切版本取快照必须给 version(缺版本=取当前发布版,那是旧口径)");
|
||||
if (version == null || version < 1 || formUuid == null || formUuid.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("按确切版本取快照必须给 uuid 与正整数 version");
|
||||
}
|
||||
RuntimeException last = null;
|
||||
for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
||||
@@ -119,6 +102,20 @@ public class FormCenterSchemaClient {
|
||||
return snapshot;
|
||||
} catch (SchemaContractViolationException e) {
|
||||
throw e;
|
||||
} catch (DException e) {
|
||||
int code = e.getCode() == null ? -1 : e.getCode().getCode();
|
||||
if (code == ErrorCode.PARAM_ERROR.getCode()) {
|
||||
throw new SchemaResourceUnavailableException(e);
|
||||
}
|
||||
if (code == ErrorCode.FORBIDDEN.getCode() || code == ErrorCode.UNAUTHORIZED.getCode()) {
|
||||
throw new SchemaAccessDeniedException(e);
|
||||
}
|
||||
last = e;
|
||||
} catch (OpenApiException e) {
|
||||
if (e.getHttpStatus() == 404) {
|
||||
throw new SchemaResourceUnavailableException(e);
|
||||
}
|
||||
last = e;
|
||||
} catch (RuntimeException e) {
|
||||
last = e;
|
||||
log.warn("[flow-center] form-center schema 精确版本拉取失败(第 {}/{} 次):uuid={} v={} {}",
|
||||
@@ -137,9 +134,15 @@ public class FormCenterSchemaClient {
|
||||
private static void requireCoordinate(Map<String, Object> snapshot, String formUuid, Integer version) {
|
||||
Object gotUuid = snapshot.get("formUuid");
|
||||
Object gotVersion = snapshot.get("formVersion");
|
||||
if (gotUuid == null || gotVersion == null
|
||||
Integer parsedVersion;
|
||||
try {
|
||||
parsedVersion = gotVersion == null ? null : Integer.valueOf(String.valueOf(gotVersion));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new SchemaContractViolationException("表单中心返回的版本不是合法整数");
|
||||
}
|
||||
if (gotUuid == null || parsedVersion == null || parsedVersion < 1
|
||||
|| !String.valueOf(gotUuid).equals(formUuid)
|
||||
|| Integer.parseInt(String.valueOf(gotVersion)) != version) {
|
||||
|| !parsedVersion.equals(version)) {
|
||||
throw new SchemaContractViolationException("表单中心返回的坐标与请求不符(请求 "
|
||||
+ formUuid + " v" + version + ",返回 " + gotUuid + " v" + gotVersion + ")");
|
||||
}
|
||||
|
||||
@@ -223,6 +223,15 @@ public class OapiFlowService {
|
||||
vo.setDuplicated(outcome.duplicated);
|
||||
vo.setBranch(outcome.branch);
|
||||
vo.setApplyRound(outcome.applyRound);
|
||||
instanceRepository.findById(outcome.instanceId).ifPresent(current -> {
|
||||
int currentRound = current.getApplyRound() == null ? 1 : current.getApplyRound();
|
||||
int outcomeRound = outcome.applyRound == null ? 1 : outcome.applyRound;
|
||||
if (currentRound == outcomeRound) {
|
||||
vo.setStatus(current.getStatus());
|
||||
vo.setVersion(current.getVersion());
|
||||
} // 已被另一请求推进到新轮时,让消费方按实例查询恢复,不混装两轮证据。
|
||||
|
||||
});
|
||||
return vo;
|
||||
}
|
||||
|
||||
@@ -252,6 +261,8 @@ public class OapiFlowService {
|
||||
vo.setInstanceId(existing.getId());
|
||||
vo.setDuplicated(true);
|
||||
vo.setApplyRound(existing.getApplyRound());
|
||||
vo.setStatus(existing.getStatus());
|
||||
vo.setVersion(existing.getVersion());
|
||||
vo.setBranch(token != null && token.equals(existing.getApplyRequestToken())
|
||||
? FlowApplyRetryModels.Branch.SAME_REQUEST : FlowApplyRetryModels.Branch.CONCURRENT_CREATED);
|
||||
log.warn("[flow-center] 并发建件命中既有实例:instance={} biz={}/{} branch={}",
|
||||
@@ -1112,6 +1123,8 @@ public class OapiFlowService {
|
||||
public OapiModels.InstanceVo toInstanceVo(FlwInstance instance) {
|
||||
OapiModels.InstanceVo vo = new OapiModels.InstanceVo();
|
||||
vo.setInstanceId(instance.getId());
|
||||
vo.setApplyRound(instance.getApplyRound());
|
||||
vo.setRequestToken(instance.getApplyRequestToken());
|
||||
vo.setStatus(instance.getStatus());
|
||||
vo.setVersion(instance.getVersion());
|
||||
vo.setBizTitle(instance.getBizTitle());
|
||||
|
||||
@@ -48,6 +48,9 @@ class OapiFlowTasksByBizTest extends FlowTestSupport {
|
||||
|
||||
private OapiSignedClient client;
|
||||
|
||||
@Autowired
|
||||
private javax.sql.DataSource dataSource;
|
||||
|
||||
@TestConfiguration
|
||||
static class StubConfig {
|
||||
@Bean
|
||||
@@ -58,7 +61,21 @@ class OapiFlowTasksByBizTest extends FlowTestSupport {
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
void setUp() throws Exception {
|
||||
// 在 cleanDb 之前验证真实连接,不能只相信环境变量已覆盖 profile。
|
||||
try (java.sql.Connection connection = dataSource.getConnection()) {
|
||||
String schema = connection.getCatalog();
|
||||
String host = java.net.URI.create(connection.getMetaData().getURL().substring(5)).getHost();
|
||||
System.out.println("CI_DB_TARGET=" + host + "/" + schema);
|
||||
assertTrue("127.0.0.1".equals(host) || "localhost".equals(host)
|
||||
|| (host != null && host.startsWith("mavenbuild-") && host.endsWith("-db")),
|
||||
"真库门禁只允许本机或本次 CI 容器");
|
||||
assertTrue("bsch_flowcenter_test".equals(schema) || "flowcenter_ci".equals(schema)
|
||||
|| (schema != null && schema.startsWith("flowcenter_ci_")), "拒绝清理非测试 schema");
|
||||
if (System.getenv("CI_TEST_DB_URL") != null) {
|
||||
assertTrue(schema.startsWith("flowcenter_ci"), "CI 必须实际连接独立 schema");
|
||||
}
|
||||
}
|
||||
cleanDb();
|
||||
client = new OapiSignedClient("http://localhost:" + port, APP_KEY, APP_SECRET);
|
||||
}
|
||||
|
||||
@@ -287,6 +287,35 @@ class FlApplyRecoveryTest {
|
||||
assertThat(existing.getApplyRound()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void callbackIdentityIsFrozenBeforeCommitAndNeverRelabelsOldEvent() throws Exception {
|
||||
FlwInstance row = instance(900L, 30, TOKEN_A, 1, "100018");
|
||||
row.setVersion(3);
|
||||
when(instanceRepository.findById(900L)).thenReturn(Optional.of(row));
|
||||
com.yunhai.flowcenter.engine.FlowEvent event = com.yunhai.flowcenter.engine.FlowEvent.of(
|
||||
com.yunhai.flowcenter.engine.FlowEvent.Type.REJECTED, 900L);
|
||||
new com.yunhai.flowcenter.callback.FlowEventIdentityListener(instanceRepository).capture(event);
|
||||
row.setApplyRound(2); row.setApplyRequestToken(TOKEN_B); row.setVersion(4);
|
||||
new com.yunhai.flowcenter.callback.FlowEventIdentityListener(instanceRepository).capture(event);
|
||||
assertThat(event.getRoundIdentity().get("applyRound")).isEqualTo(1);
|
||||
|
||||
com.yunhai.flowcenter.repository.FlwCallbackRegRepository regs = mock(com.yunhai.flowcenter.repository.FlwCallbackRegRepository.class);
|
||||
com.yunhai.flowcenter.callback.CallbackLogStore store = mock(com.yunhai.flowcenter.callback.CallbackLogStore.class);
|
||||
com.yunhai.flowcenter.entity.FlwCallbackReg reg = new com.yunhai.flowcenter.entity.FlwCallbackReg();
|
||||
reg.setId(7L);
|
||||
when(regs.findBySchIdAndFlowGuidAndStatus(SCH, FLOW_GUID, 0)).thenReturn(java.util.Collections.singletonList(reg));
|
||||
com.yunhai.flowcenter.callback.CallbackDispatcher dispatcher = new com.yunhai.flowcenter.callback.CallbackDispatcher(
|
||||
instanceRepository, defRepository, regs, mock(com.yunhai.flowcenter.repository.FlwCallbackLogRepository.class),
|
||||
store, mock(com.yunhai.flowcenter.callback.CallbackPushService.class), new com.yunhai.flowcenter.callback.CallbackProperties(), mapper);
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(dispatcher, "pushExecutor", mock(java.util.concurrent.ExecutorService.class));
|
||||
dispatcher.onFlowEvent(event);
|
||||
ArgumentCaptor<com.yunhai.flowcenter.entity.FlwCallbackLog> log = ArgumentCaptor.forClass(com.yunhai.flowcenter.entity.FlwCallbackLog.class);
|
||||
verify(store).persist(log.capture());
|
||||
com.fasterxml.jackson.databind.JsonNode data = mapper.readTree(log.getValue().getPayloadJson()).get("data");
|
||||
assertThat(data.get("applyRound").asInt()).isEqualTo(1);
|
||||
assertThat(data.get("requestToken").asText()).isEqualTo(TOKEN_A);
|
||||
}
|
||||
|
||||
// ==================== 夹具 ====================
|
||||
|
||||
/** 提交者恒 1001、调用应用恒 100018;不符的一侧都在被测行上构造 */
|
||||
|
||||
@@ -4,6 +4,9 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.yunhai.flowcenter.entity.FlwDef;
|
||||
import com.yunhai.flowcenter.engine.FlowEngine;
|
||||
import com.yunhai.flowcenter.enums.FlwEnums;
|
||||
import com.yunhai.flowcenter.enums.FormBindingErrorCode;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import com.yunhai.flowcenter.repository.FlwCallbackRegRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwDefRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
|
||||
@@ -45,13 +48,12 @@ import static org.mockito.Mockito.when;
|
||||
* 取消关联=两列同时清空(留一个没人认领的版本号是下一轮误用的起点);</li>
|
||||
* <li><b>发布取确切版本</b>:带版本的关联走 {@code schema?uuid=&version=};
|
||||
* <b>中心返回的坐标与请求不符即阻断发布</b>,而不是把错的快照嵌进 def 版本行;
|
||||
* 本列之前就已关联的存量行(formVersion=NULL)继续走"当前发布版"旧口径,行为逐字不变;</li>
|
||||
* 存量 UUID-only 行发布与启用同样阻断,必须重新绑定明确版本;</li>
|
||||
* <li><b>目录客户端的调用形态</b>:命名目标 {@code form-center} + 查询里带 schId/page/pageSize,
|
||||
* 空 data 按"目录不可用"拒(不给一个看起来正常的空列表掩盖授权没配)。</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>注:{@code FlowDefAdminServicePublishContractTest}/{@code FlowDefAdminServiceSaveTest} 钉的是
|
||||
* 改造前既有形态(两参 fetchSchema 与 8 参 save),本类只补 v3 新增的那几条,不改它们的断言。</p>
|
||||
* <p>发布与启用按同一错误码分类;表达式三查测试同时使用明确版本。</p>
|
||||
*/
|
||||
@DisplayName("流程定义绑定表单中心 - v3 单测(TASK_1)")
|
||||
class FlFormBindingTest {
|
||||
@@ -134,6 +136,7 @@ class FlFormBindingTest {
|
||||
@DisplayName("成对提交:uuid 与 version 一起落行")
|
||||
void pairIsSavedTogether() {
|
||||
when(defRepository.findById(9L)).thenReturn(Optional.of(draft(null, null)));
|
||||
when(schemaClient.fetchSchema(SCH, "FU1", 3)).thenReturn(snapshot("FU1", 3));
|
||||
|
||||
FlwDef saved = service.save(SCH, 9L, "work-leave", "请假", SIMPLE_JSON,
|
||||
null, null, null, "FU1", 3);
|
||||
@@ -198,7 +201,6 @@ class FlFormBindingTest {
|
||||
FlwDef published = service.publish(SCH, 9L);
|
||||
|
||||
verify(schemaClient).fetchSchema(SCH, "FU1", 3);
|
||||
verify(schemaClient, never()).fetchSchema(anyLong(), anyString());
|
||||
assertTrue(published.getContentJson().contains("\"formContract\""), published.getContentJson());
|
||||
assertTrue(published.getContentJson().contains("FU1"));
|
||||
}
|
||||
@@ -214,22 +216,112 @@ class FlFormBindingTest {
|
||||
|
||||
BizException e = assertThrows(BizException.class, () -> service.publish(SCH, 9L));
|
||||
|
||||
assertEquals(FormBindingErrorCode.SCHEMA_CONTRACT_VIOLATION.getCode(), e.getErrorCode().getCode());
|
||||
assertTrue(e.getMessage().contains("核验失败"), e.getMessage());
|
||||
assertTrue(e.getMessage().contains("发布已阻断"), e.getMessage());
|
||||
assertEquals(FlwEnums.DefStatus.DRAFT.getCode(), def.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("存量行(本列之前已关联、无版本):继续走当前发布版旧口径")
|
||||
void legacyRowWithoutVersionKeepsOldBehaviour() {
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
@DisplayName("缺版本:发布和启用均阻断,不回落当前版本")
|
||||
void missingVersionBlocksPublishAndEnable(boolean enable) {
|
||||
FlwDef def = draft("FU1", null);
|
||||
if (enable) def.setStatus(FlwEnums.DefStatus.DISABLED.getCode());
|
||||
when(defRepository.findById(9L)).thenReturn(Optional.of(def));
|
||||
when(schemaClient.fetchSchema(SCH, "FU1")).thenReturn(snapshot("FU1", 9));
|
||||
|
||||
service.publish(SCH, 9L);
|
||||
BizException ex = assertThrows(BizException.class, () -> changeStatus(enable));
|
||||
|
||||
verify(schemaClient).fetchSchema(SCH, "FU1");
|
||||
assertEquals(FormBindingErrorCode.VERSION_REQUIRED.getCode(), ex.getErrorCode().getCode());
|
||||
assertEquals(enable ? FlwEnums.DefStatus.DISABLED.getCode() : FlwEnums.DefStatus.DRAFT.getCode(), def.getStatus());
|
||||
verify(schemaClient, never()).fetchSchema(anyLong(), anyString(), any());
|
||||
verify(defRepository, never()).save(any());
|
||||
}
|
||||
|
||||
private FlwDef changeStatus(boolean enable) {
|
||||
return enable ? service.enable(SCH, 9L) : service.publish(SCH, 9L);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
void invalidVersionBlocksPublishAndEnable(boolean enable) {
|
||||
FlwDef def = draft("FU1", 0);
|
||||
if (enable) def.setStatus(FlwEnums.DefStatus.DISABLED.getCode());
|
||||
when(defRepository.findById(9L)).thenReturn(Optional.of(def));
|
||||
BizException ex = assertThrows(BizException.class, () -> changeStatus(enable));
|
||||
assertEquals(FormBindingErrorCode.INVALID_COORDINATE.getCode(), ex.getErrorCode().getCode());
|
||||
verify(schemaClient, never()).fetchSchema(anyLong(), anyString(), any());
|
||||
verify(defRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
void orphanVersionBlocksPublishAndEnable(boolean enable) {
|
||||
FlwDef def = draft(null, 3);
|
||||
if (enable) def.setStatus(FlwEnums.DefStatus.DISABLED.getCode());
|
||||
when(defRepository.findById(9L)).thenReturn(Optional.of(def));
|
||||
BizException ex = assertThrows(BizException.class, () -> changeStatus(enable));
|
||||
assertEquals(FormBindingErrorCode.INVALID_COORDINATE.getCode(), ex.getErrorCode().getCode());
|
||||
verify(defRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
void schemaFailuresHaveDistinctStableCodesAndDoNotWrite(boolean enable) {
|
||||
FlwDef def = draft("FU1", 3);
|
||||
if (enable) def.setStatus(FlwEnums.DefStatus.DISABLED.getCode());
|
||||
when(defRepository.findById(9L)).thenReturn(Optional.of(def));
|
||||
RuntimeException[] failures = {
|
||||
new FormCenterSchemaClient.SchemaUnavailableException("timeout", null),
|
||||
new FormCenterSchemaClient.SchemaContractViolationException("wrong version"),
|
||||
new FormCenterSchemaClient.SchemaResourceUnavailableException(null),
|
||||
new FormCenterSchemaClient.SchemaAccessDeniedException(null)
|
||||
};
|
||||
FormBindingErrorCode[] codes = {FormBindingErrorCode.SCHEMA_UNAVAILABLE,
|
||||
FormBindingErrorCode.SCHEMA_CONTRACT_VIOLATION, FormBindingErrorCode.RESOURCE_UNAVAILABLE,
|
||||
FormBindingErrorCode.ACCESS_DENIED};
|
||||
for (int i = 0; i < failures.length; i++) {
|
||||
when(schemaClient.fetchSchema(SCH, "FU1", 3)).thenThrow(failures[i]);
|
||||
BizException ex = assertThrows(BizException.class, () -> changeStatus(enable));
|
||||
assertEquals(codes[i].getCode(), ex.getErrorCode().getCode());
|
||||
assertSame(failures[i], ex.getCause());
|
||||
assertEquals(enable ? FlwEnums.DefStatus.DISABLED.getCode() : FlwEnums.DefStatus.DRAFT.getCode(), def.getStatus());
|
||||
assertEquals(FLOW_JSON, def.getContentJson());
|
||||
org.mockito.Mockito.reset(schemaClient);
|
||||
}
|
||||
verify(defRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void enableUsesExactVersionAndPreservesPublishedSnapshot() {
|
||||
FlwDef def = draft("FU1", 3);
|
||||
def.setStatus(FlwEnums.DefStatus.DISABLED.getCode());
|
||||
def.setContentJson(FLOW_JSON.substring(0, FLOW_JSON.length() - 1)
|
||||
+ ",\"formContract\":{\"formUuid\":\"FU1\",\"formVersion\":3,\"fields\":[]}}");
|
||||
String frozenJson = def.getContentJson();
|
||||
when(defRepository.findById(9L)).thenReturn(Optional.of(def));
|
||||
when(schemaClient.fetchSchema(SCH, "FU1", 3)).thenReturn(snapshot("FU1", 3));
|
||||
FlwDef enabled = service.enable(SCH, 9L);
|
||||
assertEquals(FlwEnums.DefStatus.PUBLISHED.getCode(), enabled.getStatus());
|
||||
assertEquals(frozenJson, enabled.getContentJson());
|
||||
verify(schemaClient).fetchSchema(SCH, "FU1", 3);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"oops", "3.5", "3.0", "2147483648", "", "true"})
|
||||
void malformedVersionAtHttpBoundaryHasStableCode(String value) {
|
||||
FlowAdminGuard guard = mock(FlowAdminGuard.class);
|
||||
when(guard.requireAdmin(any(), any())).thenReturn(SCH);
|
||||
com.yunhai.flowcenter.controller.api.FlowAdminController controller =
|
||||
new com.yunhai.flowcenter.controller.api.FlowAdminController(guard, service,
|
||||
null, null, null, null, null, null, new ObjectMapper(), null, null, null);
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("formUuid", "FU1");
|
||||
body.put("formVersion", value);
|
||||
BizException ex = assertThrows(BizException.class, () -> controller.defSave(null, body));
|
||||
assertEquals(FormBindingErrorCode.INVALID_COORDINATE.getCode(), ex.getErrorCode().getCode());
|
||||
verify(defRepository, never()).save(any());
|
||||
org.mockito.Mockito.verifyNoInteractions(schemaClient);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -240,7 +332,6 @@ class FlFormBindingTest {
|
||||
|
||||
service.publish(SCH, 9L);
|
||||
|
||||
verify(schemaClient, never()).fetchSchema(anyLong(), anyString());
|
||||
verify(schemaClient, never()).fetchSchema(anyLong(), anyString(), any());
|
||||
}
|
||||
|
||||
|
||||
+12
-9
@@ -85,6 +85,7 @@ class FlowDefAdminServicePublishContractTest {
|
||||
d.setAppKey("notice");
|
||||
d.setContentJson(JSON);
|
||||
d.setFormUuid(formUuid);
|
||||
d.setFormVersion(formUuid == null ? null : 3);
|
||||
return d;
|
||||
}
|
||||
|
||||
@@ -115,7 +116,7 @@ class FlowDefAdminServicePublishContractTest {
|
||||
@Test
|
||||
void publishEmbedsFormContractSnapshotIntoVersionRow() throws Exception {
|
||||
when(defRepository.findById(9L)).thenReturn(java.util.Optional.of(draft("FU1")));
|
||||
when(schemaClient.fetchSchema(SCH, "FU1")).thenReturn(snapshot());
|
||||
when(schemaClient.fetchSchema(SCH, "FU1", 3)).thenReturn(snapshot());
|
||||
|
||||
FlwDef published = service.publish(SCH, 9L);
|
||||
|
||||
@@ -134,7 +135,7 @@ class FlowDefAdminServicePublishContractTest {
|
||||
// 出口条件改成引用快照外变量
|
||||
d.setContentJson(JSON.replace("grade == 'g1'", "level == 'high'"));
|
||||
when(defRepository.findById(9L)).thenReturn(java.util.Optional.of(d));
|
||||
when(schemaClient.fetchSchema(SCH, "FU1")).thenReturn(snapshot());
|
||||
when(schemaClient.fetchSchema(SCH, "FU1", 3)).thenReturn(snapshot());
|
||||
|
||||
BizException ex = assertThrows(BizException.class, () -> service.publish(SCH, 9L));
|
||||
assertTrue(ex.getMessage().contains("level"), "红牌要点名未知变量:" + ex.getMessage());
|
||||
@@ -147,7 +148,7 @@ class FlowDefAdminServicePublishContractTest {
|
||||
FlwDef d = draft("FU1");
|
||||
d.setContentJson(JSON.replace("grade == 'g1'", "reason > 3"));
|
||||
when(defRepository.findById(9L)).thenReturn(java.util.Optional.of(d));
|
||||
when(schemaClient.fetchSchema(SCH, "FU1")).thenReturn(snapshot());
|
||||
when(schemaClient.fetchSchema(SCH, "FU1", 3)).thenReturn(snapshot());
|
||||
|
||||
BizException ex = assertThrows(BizException.class, () -> service.publish(SCH, 9L));
|
||||
assertTrue(ex.getMessage().contains("类型桶查②") && ex.getMessage().contains("reason"),
|
||||
@@ -159,7 +160,7 @@ class FlowDefAdminServicePublishContractTest {
|
||||
FlwDef d = draft("FU1");
|
||||
d.setContentJson(JSON.replace("grade == 'g1'", "days == 'three'"));
|
||||
when(defRepository.findById(9L)).thenReturn(java.util.Optional.of(d));
|
||||
when(schemaClient.fetchSchema(SCH, "FU1")).thenReturn(snapshot());
|
||||
when(schemaClient.fetchSchema(SCH, "FU1", 3)).thenReturn(snapshot());
|
||||
|
||||
BizException ex = assertThrows(BizException.class, () -> service.publish(SCH, 9L));
|
||||
assertTrue(ex.getMessage().contains("类型桶查②") && ex.getMessage().contains("days"),
|
||||
@@ -171,7 +172,7 @@ class FlowDefAdminServicePublishContractTest {
|
||||
FlwDef d = draft("FU1");
|
||||
d.setContentJson(JSON.replace("grade == 'g1'", "grade == 'g9'"));
|
||||
when(defRepository.findById(9L)).thenReturn(java.util.Optional.of(d));
|
||||
when(schemaClient.fetchSchema(SCH, "FU1")).thenReturn(snapshot());
|
||||
when(schemaClient.fetchSchema(SCH, "FU1", 3)).thenReturn(snapshot());
|
||||
|
||||
BizException ex = assertThrows(BizException.class, () -> service.publish(SCH, 9L));
|
||||
assertTrue(ex.getMessage().contains("域查③") && ex.getMessage().contains("g9"),
|
||||
@@ -184,7 +185,7 @@ class FlowDefAdminServicePublishContractTest {
|
||||
FlwDef d = draft("FU1");
|
||||
d.setContentJson(JSON.replace("grade == 'g1'", "days >= 2.5"));
|
||||
when(defRepository.findById(9L)).thenReturn(java.util.Optional.of(d));
|
||||
when(schemaClient.fetchSchema(SCH, "FU1")).thenReturn(snapshot());
|
||||
when(schemaClient.fetchSchema(SCH, "FU1", 3)).thenReturn(snapshot());
|
||||
|
||||
assertEquals(FlwEnums.DefStatus.PUBLISHED.getCode(), service.publish(SCH, 9L).getStatus());
|
||||
}
|
||||
@@ -195,7 +196,7 @@ class FlowDefAdminServicePublishContractTest {
|
||||
FlwDef d = draft("FU1");
|
||||
d.setContentJson(JSON.replace("grade == 'g1'", "bizType == 'x'"));
|
||||
when(defRepository.findById(9L)).thenReturn(java.util.Optional.of(d));
|
||||
when(schemaClient.fetchSchema(SCH, "FU1")).thenReturn(snapshot());
|
||||
when(schemaClient.fetchSchema(SCH, "FU1", 3)).thenReturn(snapshot());
|
||||
|
||||
assertEquals(FlwEnums.DefStatus.PUBLISHED.getCode(), service.publish(SCH, 9L).getStatus());
|
||||
}
|
||||
@@ -208,16 +209,18 @@ class FlowDefAdminServicePublishContractTest {
|
||||
|
||||
assertEquals(FlwEnums.DefStatus.PUBLISHED.getCode(), published.getStatus());
|
||||
assertEquals(JSON, published.getContentJson(), "存量线 content_json 一字不动");
|
||||
verify(schemaClient, never()).fetchSchema(anyLong(), anyString());
|
||||
verify(schemaClient, never()).fetchSchema(anyLong(), anyString(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void schemaUnavailableBlocksPublishAsRetryableBizError() {
|
||||
when(defRepository.findById(9L)).thenReturn(java.util.Optional.of(draft("FU1")));
|
||||
when(schemaClient.fetchSchema(SCH, "FU1"))
|
||||
when(schemaClient.fetchSchema(SCH, "FU1", 3))
|
||||
.thenThrow(new FormCenterSchemaClient.SchemaUnavailableException("timeout", null));
|
||||
|
||||
BizException ex = assertThrows(BizException.class, () -> service.publish(SCH, 9L));
|
||||
assertEquals(com.yunhai.flowcenter.enums.FormBindingErrorCode.SCHEMA_UNAVAILABLE.getCode(),
|
||||
ex.getErrorCode().getCode());
|
||||
assertTrue(ex.getMessage().contains("快照拉取失败"), "发布阻断文案可辨认:" + ex.getMessage());
|
||||
verify(defRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@@ -2,6 +2,12 @@ package com.yunhai.flowcenter.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.yunhai.platform.openapi.core.YschOpenCall;
|
||||
import com.yunhai.platform.openapi.core.OpenApiException;
|
||||
import com.yunhai.platform.openapi.core.OpenApiErrorCode;
|
||||
import com.yunhai.platform.utils.exception.DException;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.junit.jupiter.params.provider.NullSource;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -72,7 +78,7 @@ class FormCenterSchemaClientTest {
|
||||
when(openCall.get(anyString(), anyString(), anyMap(), eq(Map.class)))
|
||||
.thenReturn(schemaData("U1", 3, Arrays.asList("STUDENT", "TEACHER")));
|
||||
|
||||
Map<String, Object> snap = client.fetchSchema(27L, "U1");
|
||||
Map<String, Object> snap = client.fetchSchema(27L, "U1", 3);
|
||||
|
||||
assertEquals("U1", snap.get("formUuid"));
|
||||
assertEquals(3, snap.get("formVersion"));
|
||||
@@ -88,11 +94,11 @@ class FormCenterSchemaClientTest {
|
||||
|
||||
@Test
|
||||
void legacySingleStringAudienceIsWrappedAsArray() {
|
||||
Map<String, Object> data = schemaData("U1", 1, null);
|
||||
Map<String, Object> data = schemaData("U1", 3, null);
|
||||
data.put("audience", "STUDENT");
|
||||
when(openCall.get(anyString(), anyString(), anyMap(), eq(Map.class))).thenReturn(data);
|
||||
|
||||
Map<String, Object> snap = client.fetchSchema(27L, "U1");
|
||||
Map<String, Object> snap = client.fetchSchema(27L, "U1", 3);
|
||||
|
||||
assertEquals(Arrays.asList("STUDENT"), snap.get("audience"),
|
||||
"C5 切换窗口的旧单值串按一元素数组容错");
|
||||
@@ -104,7 +110,7 @@ class FormCenterSchemaClientTest {
|
||||
.thenThrow(new RuntimeException("connect refused"));
|
||||
|
||||
assertThrows(FormCenterSchemaClient.SchemaUnavailableException.class,
|
||||
() -> client.fetchSchema(27L, "U1"));
|
||||
() -> client.fetchSchema(27L, "U1", 3));
|
||||
verify(openCall, times(FormCenterSchemaClient.MAX_ATTEMPTS))
|
||||
.get(anyString(), anyString(), anyMap(), eq(Map.class));
|
||||
}
|
||||
@@ -115,7 +121,77 @@ class FormCenterSchemaClientTest {
|
||||
when(openCall.get(anyString(), anyString(), anyMap(), eq(Map.class))).thenReturn(null);
|
||||
|
||||
assertThrows(FormCenterSchemaClient.SchemaUnavailableException.class,
|
||||
() -> client.fetchSchema(27L, "U1"));
|
||||
assertTrue(true);
|
||||
() -> client.fetchSchema(27L, "U1", 3));
|
||||
verify(openCall, times(FormCenterSchemaClient.MAX_ATTEMPTS))
|
||||
.get(anyString(), anyString(), anyMap(), eq(Map.class));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@NullSource
|
||||
@ValueSource(strings = {"0", "-1", "4", "3.5", "wrong", "2147483648"})
|
||||
void malformedOrWrongReturnedVersionIsContractFailureWithoutRetry(String returnedVersion) {
|
||||
Map<String, Object> data = schemaData("U1", 3, Arrays.asList("STUDENT"));
|
||||
data.put("version", returnedVersion);
|
||||
when(openCall.get(anyString(), anyString(), anyMap(), eq(Map.class))).thenReturn(data);
|
||||
assertThrows(FormCenterSchemaClient.SchemaContractViolationException.class,
|
||||
() -> client.fetchSchema(27L, "U1", 3));
|
||||
verify(openCall, times(1)).get(anyString(), anyString(), anyMap(), eq(Map.class));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@NullSource
|
||||
@ValueSource(strings = {"", "U2"})
|
||||
void missingOrWrongReturnedUuidIsContractFailureWithoutRetry(String returnedUuid) {
|
||||
when(openCall.get(anyString(), anyString(), anyMap(), eq(Map.class)))
|
||||
.thenReturn(schemaData(returnedUuid, 3, Arrays.asList("STUDENT")));
|
||||
assertThrows(FormCenterSchemaClient.SchemaContractViolationException.class,
|
||||
() -> client.fetchSchema(27L, "U1", 3));
|
||||
verify(openCall, times(1)).get(anyString(), anyString(), anyMap(), eq(Map.class));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(ints = {0, -1})
|
||||
void illegalRequestedVersionMakesNoRemoteCall(int version) {
|
||||
assertThrows(IllegalArgumentException.class, () -> client.fetchSchema(27L, "U1", version));
|
||||
org.mockito.Mockito.verifyNoInteractions(openCall);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resourceRejectionUsesEnvelopeCodeAndDoesNotRetry() {
|
||||
// SDK 实际携带 OpenApiErrorCode,不会转换成本地 ErrorCode 枚举;文案刻意无关键词。
|
||||
when(openCall.get(anyString(), anyString(), anyMap(), eq(Map.class)))
|
||||
.thenThrow(new DException(new OpenApiErrorCode(2, "opaque")));
|
||||
assertThrows(FormCenterSchemaClient.SchemaResourceUnavailableException.class,
|
||||
() -> client.fetchSchema(27L, "U1", 3));
|
||||
verify(openCall, times(1)).get(anyString(), anyString(), anyMap(), eq(Map.class));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(ints = {3, 4})
|
||||
void authRejectionDoesNotBlindlyRetry(int code) {
|
||||
when(openCall.get(anyString(), anyString(), anyMap(), eq(Map.class)))
|
||||
.thenThrow(new DException(new OpenApiErrorCode(code, "opaque")));
|
||||
assertThrows(FormCenterSchemaClient.SchemaAccessDeniedException.class,
|
||||
() -> client.fetchSchema(27L, "U1", 3));
|
||||
verify(openCall, times(1)).get(anyString(), anyString(), anyMap(), eq(Map.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void http404DoesNotBlindlyRetry() {
|
||||
when(openCall.get(anyString(), anyString(), anyMap(), eq(Map.class)))
|
||||
.thenThrow(new OpenApiException("opaque", 404, ""));
|
||||
assertThrows(FormCenterSchemaClient.SchemaResourceUnavailableException.class,
|
||||
() -> client.fetchSchema(27L, "U1", 3));
|
||||
verify(openCall, times(1)).get(anyString(), anyString(), anyMap(), eq(Map.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void internalRemoteFailureRemainsRetryable() {
|
||||
when(openCall.get(anyString(), anyString(), anyMap(), eq(Map.class)))
|
||||
.thenThrow(new DException(new OpenApiErrorCode(6, "opaque")));
|
||||
assertThrows(FormCenterSchemaClient.SchemaUnavailableException.class,
|
||||
() -> client.fetchSchema(27L, "U1", 3));
|
||||
verify(openCall, times(FormCenterSchemaClient.MAX_ATTEMPTS))
|
||||
.get(anyString(), anyString(), anyMap(), eq(Map.class));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ class OapiFlowApplyDefVersionTest {
|
||||
private FlowInstanceService instanceService;
|
||||
private AssigneeResolver assigneeResolver;
|
||||
private OapiFlowService service;
|
||||
private FlwInstanceRepository instances;
|
||||
|
||||
@BeforeEach
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -54,7 +55,8 @@ class OapiFlowApplyDefVersionTest {
|
||||
defRepository = mock(FlwDefRepository.class);
|
||||
instanceService = mock(FlowInstanceService.class);
|
||||
assigneeResolver = mock(AssigneeResolver.class);
|
||||
service = new OapiFlowService(defRepository, mock(FlwInstanceRepository.class),
|
||||
instances = mock(FlwInstanceRepository.class);
|
||||
service = new OapiFlowService(defRepository, instances,
|
||||
mock(FlwTaskRepository.class), mock(FlwCcRepository.class),
|
||||
mock(FlwCallbackRegRepository.class), instanceService, mock(FlowTaskService.class),
|
||||
mock(FlowEngine.class), assigneeResolver, new ObjectMapper(),
|
||||
@@ -150,4 +152,20 @@ class OapiFlowApplyDefVersionTest {
|
||||
assertEquals(Integer.valueOf(1), vo.getDefVersion());
|
||||
assertTrue(vo.getNodes() == null || vo.getNodes().isEmpty());
|
||||
}
|
||||
@Test
|
||||
void v2ResponseCarriesRealRejectedStatusAndStableBranch() {
|
||||
stubDef(3);
|
||||
com.yunhai.flowcenter.model.FlowApplyRetryModels.ApplyRequestV2 body = new com.yunhai.flowcenter.model.FlowApplyRetryModels.ApplyRequestV2();
|
||||
body.setFlowGuid(FLOW_GUID); body.setBizType("svc-hall"); body.setBizId("42"); body.setSubmitterId(21);
|
||||
body.setRequestToken("token-1"); body.setAttempt(1);
|
||||
when(instanceService.applyGuarded(any(), any())).thenReturn(
|
||||
new FlowInstanceService.GuardedOutcome(500L, true, "SAME_REQUEST", 1));
|
||||
com.yunhai.flowcenter.entity.FlwInstance row = new com.yunhai.flowcenter.entity.FlwInstance();
|
||||
row.setId(500L); row.setStatus(30); row.setVersion(4); row.setApplyRound(1);
|
||||
when(instances.findById(500L)).thenReturn(Optional.of(row));
|
||||
com.yunhai.flowcenter.model.FlowApplyRetryModels.ApplyResultV2 result = service.applyV2(SCH_ID, "100009", body);
|
||||
assertEquals("SAME_REQUEST", result.getBranch()); assertEquals(Integer.valueOf(1), result.getApplyRound());
|
||||
assertEquals(Integer.valueOf(30), result.getStatus()); assertEquals(Integer.valueOf(4), result.getVersion());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user