C1余量:类型桶查②+域查③红牌(启发式字面量形态匹配,保守边界不执行表达式)
build / build-push (push) Failing after 17m24s

This commit is contained in:
chenxing
2026-10-20 22:52:43 +08:00
parent 97dec39d45
commit 2f54f37ab3
2 changed files with 145 additions and 2 deletions
@@ -214,9 +214,10 @@ public class FlowDefAdminService {
}
List<String> problems = new ArrayList<>();
validateExpressionKeysAgainstSnapshot(model, snapshot, problems);
validateExpressionTypesAndDomains(model, snapshot, problems);
if (!problems.isEmpty()) {
throw new BizException(ErrorCode.PARAM_ERROR,
"发布校验失败(表达式键查①):\n" + String.join("\n", problems));
"发布校验失败(表达式三查):\n" + String.join("\n", problems));
}
def.setContentJson(embedFormContract(def.getContentJson(), snapshot));
}
@@ -286,6 +287,101 @@ public class FlowDefAdminService {
}
}
// ===================== 三查②③(2026-10-20 C1 第一刀余量项) =====================
/** 数值桶:可与数值字面量比大小的快照字段类型 */
private static final java.util.Set<String> NUMERIC_TYPES =
new java.util.HashSet<>(java.util.Arrays.asList("number"));
/** 值域字面量型(enum 域查③适用):options 收进快照 enum 的类型 */
private static final java.util.Set<String> ENUM_TYPES =
new java.util.HashSet<>(java.util.Arrays.asList("select", "radio"));
/** `字段 op '字面量'` / `字段 op 数值`(op 含两侧空格容忍) */
private static final java.util.regex.Pattern FIELD_STR_EQ = java.util.regex.Pattern.compile(
"([A-Za-z_][A-Za-z0-9_]*)\\s*(==|!=)\\s*'([^']*)'");
private static final java.util.regex.Pattern FIELD_NUM_CMP = java.util.regex.Pattern.compile(
"([A-Za-z_][A-Za-z0-9_]*)\\s*(>=|<=|>|<)\\s*(-?\\d+(\\.\\d+)?)");
private static final java.util.regex.Pattern NUM_FIELD_CMP = java.util.regex.Pattern.compile(
"(-?\\d+(\\.\\d+)?)\\s*(>=|<=|>|<)\\s*([A-Za-z_][A-Za-z0-9_]*)");
/**
* 类型桶查②红牌 + 域查③红牌(<b>启发式实现</b>:正则模式匹配字面量比较形态,非 AST——
* v2 §4.7 保守边界:不执行表达式、不做隐式转换,只拦"数值比较 × 非数值字段"与
* "枚举等值 × 字面量不在值域"两类高发错配;不命中的形态留给运行期既有挂起纪律)。
*/
@SuppressWarnings("unchecked")
private void validateExpressionTypesAndDomains(FlowDefModel model, Map<String, Object> snapshot,
List<String> problems) {
List<Map<String, Object>> fields = (List<Map<String, Object>>) snapshot.get("fields");
if (fields == null || fields.isEmpty()) {
return;
}
Map<String, Map<String, Object>> fieldByKey = new HashMap<>();
for (Map<String, Object> f : fields) {
Object k = f.get("key");
if (k != null) {
fieldByKey.put(String.valueOf(k), f);
}
}
for (FlowDefModel.Node n : model.nodes) {
if (n == null || !n.expressionJudge()) {
continue;
}
for (FlowDefModel.Edge e : model.outEdges(n.id)) {
if (e.condition == null || e.condition.trim().isEmpty()) {
continue;
}
String where = "节点「" + n.name + "」出口「" + e.name + "」";
String expr = e.condition.trim();
// ② 类型桶查:数值比较算子作用于非数值字段 → 红牌
java.util.regex.Matcher m = FIELD_NUM_CMP.matcher(expr);
while (m.find()) {
checkNumericBucket(where, m.group(1), m.group(2), fieldByKey, problems);
}
m = NUM_FIELD_CMP.matcher(expr);
while (m.find()) {
checkNumericBucket(where, m.group(4), m.group(3), fieldByKey, problems);
}
// ② 类型桶查(字符串侧):文本字面量等值比较数值字段 → 红牌
// ③ 域查:枚举字段等值比较的字面量 ∉ 快照 enum 值域 → 红牌
m = FIELD_STR_EQ.matcher(expr);
while (m.find()) {
String key = m.group(1);
String op = m.group(2);
String literal = m.group(3);
Map<String, Object> field = fieldByKey.get(key);
if (field == null) {
continue; // 内置变量或未知键:①已拦/不属本查
}
String type = field.get("type") == null ? null : String.valueOf(field.get("type"));
if (NUMERIC_TYPES.contains(type)) {
problems.add(where + ":字段 " + key + " 是 number 类型,不能与字符串字面量 '"
+ literal + "' 做 " + op + " 比较(类型桶查②)");
} else if (ENUM_TYPES.contains(type) && "==".equals(op)) {
List<String> enums = (List<String>) field.get("enum");
if (enums != null && !enums.isEmpty() && !enums.contains(literal)) {
problems.add(where + ":字段 " + key + " 的值域是 " + enums
+ ",字面量 '" + literal + "' 不在其中(域查③)");
}
}
}
}
}
}
private void checkNumericBucket(String where, String key, String op,
Map<String, Map<String, Object>> fieldByKey, List<String> problems) {
Map<String, Object> field = fieldByKey.get(key);
if (field == null) {
return;
}
String type = field.get("type") == null ? null : String.valueOf(field.get("type"));
if (type != null && !NUMERIC_TYPES.contains(type)) {
problems.add(where + ":字段 " + key + " 是 " + type + " 类型,不能做数值比较 "
+ key + " " + op + " 数字(类型桶查②)");
}
}
/** 停用:不可新发起,存量跑完 */
@Transactional
public void offline(Long schId, Long id) {
@@ -138,10 +138,57 @@ class FlowDefAdminServicePublishContractTest {
BizException ex = assertThrows(BizException.class, () -> service.publish(SCH, 9L));
assertTrue(ex.getMessage().contains("level"), "红牌要点名未知变量:" + ex.getMessage());
assertTrue(ex.getMessage().contains("键查"), "错误文案给可定位的键查提示");
assertTrue(ex.getMessage().contains("不在关联表单字段集内"), "键查①给可定位的问题行");
verify(defRepository, never()).save(any());
}
@Test
void numericCompareOnTextFieldIsRedCardTypeBucket() {
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());
BizException ex = assertThrows(BizException.class, () -> service.publish(SCH, 9L));
assertTrue(ex.getMessage().contains("类型桶查②") && ex.getMessage().contains("reason"),
"text 字段做数值比较要点名(类型桶查②):" + ex.getMessage());
}
@Test
void stringLiteralEqualsOnNumberFieldIsRedCardTypeBucket() {
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());
BizException ex = assertThrows(BizException.class, () -> service.publish(SCH, 9L));
assertTrue(ex.getMessage().contains("类型桶查②") && ex.getMessage().contains("days"),
"number 字段与字符串字面量等值要拦:" + ex.getMessage());
}
@Test
void enumLiteralOutOfDomainIsRedCard() {
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());
BizException ex = assertThrows(BizException.class, () -> service.publish(SCH, 9L));
assertTrue(ex.getMessage().contains("域查③") && ex.getMessage().contains("g9"),
"枚举字面量不在值域要点名(域查③):" + ex.getMessage());
}
@Test
void numericFieldWithNumericLiteralPassesAllChecks() {
// days(number) > 3 与内置 schId > 0:happy path(publishEmbeds…已覆盖主链),此处锁数值正向桶
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());
assertEquals(FlwEnums.DefStatus.PUBLISHED.getCode(), service.publish(SCH, 9L).getStatus());
}
@Test
void builtinVariablesPassKeyCheck() {
// days 与 schId(内置)都合法 → 上面的 happy path 已覆盖;此处单独断言纯内置表达式