Commit dddde8da0c8bb21e1d4efd4373924419f9ed2a38

Authored by zichun
0 parents

init

Showing 66 changed files with 12948 additions and 0 deletions
.gitignore 0 → 100644
  1 +++ a/.gitignore
  1 +# Java / Maven
  2 +engine/target/
  3 +*.class
  4 +
  5 +# Node / Vite
  6 +web/node_modules/
  7 +web/dist/
  8 +web/.vite/
  9 +
  10 +# 运行日志
  11 +.engine.log
  12 +*.log
  13 +
  14 +# IDE / OS
  15 +.idea/
  16 +.vscode/
  17 +*.iml
  18 +.DS_Store
... ...
CLAUDE.md 0 → 100644
  1 +++ a/CLAUDE.md
  1 +# CLAUDE.md — 本体驱动 DDD+EDA 元数据引擎
  2 +
  3 +给未来在本仓库工作的 Claude Code 的说明。
  4 +
  5 +## 这是什么
  6 +
  7 +一个**元数据驱动**系统:`models/` 下 11 份 YAML 本体(M0–M8、MetaRule)是**唯一事实源**。
  8 +- 后端 `engine/`(Java 21 / Spring Boot 3.5)是**通用解释引擎**——不含订单专用业务代码,加载 YAML 后通用执行命令。
  9 +- 前端 `web/`(React 18 / antd 5 / Vite)是**通用渲染引擎**——读 M8 模板动态生成页面。
  10 +
  11 +**铁律:改需求改 `models/*.yaml`,不要在引擎/前端里硬编码订单/客户/商品字段。** 若某能力必须加代码,应加"解释某类模型结构"的通用能力,而非某个具体聚合的分支。
  12 +
  13 +## 运行 / 验证
  14 +
  15 +```bash
  16 +./start.sh # 一键起后端(8080)+前端(5173/顺延)
  17 +cd engine && ./mvnw spring-boot:run # 仅后端(./mvnw 包装器,无需预装 Maven)
  18 +cd web && npm run dev # 仅前端
  19 +bash tests/verify-model-change.sh # 验证"改模型→改行为"(reload 需 X-Principal: backend_admin)
  20 +```
  21 +
  22 +改完模型:点页面右上「热重载模型」或 `curl -X POST localhost:8080/api/meta/reload`,无需重启。
  23 +
  24 +## 关键文件
  25 +
  26 +- `engine/.../model/ModelRepository.java` —— 元模型加载与按 M0-M8 语义的类型化查询(所有下游只读它)。
  27 +- `engine/.../command/CommandEngine.java` —— ★ 通用命令执行器(M4 flowSteps + M2 + MetaRule + M3 + ME)。
  28 +- `engine/.../meta/SceneSchemaService.java` —— 合成前端渲染 Schema(M8+M1+M2+M3+M5+MetaRule)。`select` 绑定到「聚合根自身标识」或「带 M1 `refAggregate` 的引用字段」时,通用地据被引用聚合给出下拉选项(如订单审核的审核人=员工下拉,值回填 `auditorId`)。
  29 +- `engine/.../persist/SchemaInitializer.java` —— 据 M3+M1 自动建表 + 写演示数据。
  30 +- `web/src/engine/SceneForm.tsx` / `FieldControl.tsx` —— 通用渲染引擎与组件注册表。
  31 +
  32 +## 结构化元数据约定(2026-07-20 审计整改后新增,引擎据此通用执行)
  33 +
  34 +- **M1 属性 `constraints`**:`{min, max, exclusiveMin, exclusiveMax, pattern, patternMessage, unique}` —— 通用校验,勿在引擎里写具体字段/中文子串。`unique:true` 会在 create/update 前按 M3 列查重(update 排除自身;加密字段用确定性密文比对)。
  35 +- **M2 命令 `commandType`**:`create|update|cancel` —— 决定合成默认程序 + create 超卖回滚策略;缺省 create。
  36 +- **M2 命令 `bizSteps`(命令级"闭指令集"程序,`CommandEngine.runProgram`/`runInstructions` 逐条通用解释,替代旧的硬编码 create/update/cancel)**:
  37 + 每步 `{stepNo, desc, instruction:{type, params}}`,`type` **只能取以下 10 个 opcode 之一**(闭集合,M0 `M2_BizStep.enum` 锁定,AI 转换产物可静态校验):
  38 + `LOAD_AGGREGATE`(按 `idSource` 加载既有根→update/cancel 语义) / `SET_FIELD`(`targetField` ← `source`(input.*/root.*/字面量) 或 `value`) /
  39 + `AUTO_FILL_FIELD`(`func`: `now_datetime|now_date|gen_id`) / `REMOTE_QUERY_SET_FIELD`(`remoteAggregate/queryKey/sourceField/targetField`) /
  40 + `CONDITION_SET_FIELD`(`conditions:[{when, value}]`,when 走 Aviator) / `CALL_OTHER_COMMAND`(`targetAggregate/targetCommand/paramMap/forEach/onFailure`,复用目标命令 `effect`) /
  41 + `COMMIT_TRANSACTION`(未 LOAD→insert 根+组合子实体;已 LOAD→update) /
  42 + `IF`(`{when, then:[子指令], else:[子指令]}`,when 走 Aviator,**命令级判断分支**,类比 M4 `condition`,`then/else` 内可嵌套任意 opcode) /
  43 + `REMOTE_SET_FIELD`(`remoteAggregate/keySource/targetField/source|value`,覆盖写回被引用聚合根字段) /
  44 + `REMOTE_INCREMENT_FIELD`(`remoteAggregate/keySource/targetField/amount`,原子 +N 被引用聚合根数值字段;被增字段须非空,见 seed 初始化)。
  45 + 缺省或仍是字符串描述的命令,按 `commandType` 合成规范程序,仍走同一台解释器。
  46 + 落库后统一:发 `emitEvents` → 跨聚合一致(ME 规则 + 指令内 CALL/REMOTE) → create 超卖回滚。表达式里 `input.`(命令入参)/`root.`(聚合现值) 命名空间由引擎改写后交 Aviator。
  47 + 演示命令 `AuditOrder`(`SCENE_AUDIT_ORDER`)即范例:按 `root.totalAmount>100` 用 `IF`+`REMOTE_SET_FIELD` 覆盖审核人备注,按 `input.auditResult` 用 `REMOTE_INCREMENT_FIELD` 累加审核人通过/驳回数;`REMOTE_QUERY` 查 `EmployeeAggregate`。
  48 +- **M2 命令 `effect`**:`{op: increment|decrement, targetField, keyParam, amountParam, guardField}` —— 跨聚合副作用由此通用执行。
  49 +- **M2 命令 `derivations`**:`[{targetField, sourceEntity, aggregate: sum, expression}]` —— 服务端重算字段(防篡改)。
  50 +- **ME `crossAggConsistencyRules`**:需带 `sourceItemEntity` + `paramMap{目标命令参数: 子实体字段}`。
  51 +- 后端 execute 返回稳定键 `rootId` / `rootIdField`;前端只读这两个,勿读 `orderId`。
  52 +- 校验/风控变量解析走 **M1 `refAggregate`**(`ModelRepository.refInfo`);不要在引擎写 CustomerAggregate/customerLevel/stockNum 之类字面量。
  53 +- **M4 场景 `stepType` 全集(`CommandEngine.runStep` 递归通用解释,枚举见 M0 `M4_SceneStep`)**:
  54 + 起止 `start|return|end|errorEnd`;数据 `readOnlyCheck|validate|derive|assign|transform|script`;
  55 + 命令/事件 `bindCommand|callScene|callService|emitEvent|waitEvent`;控制流 `condition|switch|parallel|while|loopItems`;
  56 + Saga/可靠性 `retry|compensate|timer|userTask`。子步骤放 `then/else/body/branches[].steps/cases[].steps/default`;
  57 + 表达式(`when`/`switchOn`/`assignments[].expression`/`script.expression`)走 Aviator(`RuleEngine.evalValue/evalBool`),变量取 master+M1 引用+循环行。
  58 + **switch 的分派键叫 `switchOn` 不叫 `on`**(YAML 1.1 把 `on/off/yes/no` 当布尔,`on:` 会变成 `true` 键)。
  59 + `bindCommand` 可带 `compensateAggregate/compensateCommand` 登记逆向补偿,失败时逆序触发(`SCENE_DEMO_STEPTYPES` 为可删演示场景)。
  60 + 演示简化:`parallel` 顺序执行后汇合、`timer/waitEvent/callService` 不真阻塞/外呼、`retry` 无瞬时故障注入、`userTask` 用 `approveParam` 入参模拟审批。
  61 +
  62 +## 约定
  63 +
  64 +- 存储默认 H2 内存库(MySQL 兼容模式);生产 profile 见 `deploy/`。
  65 +- Java 编译目标 21(`maven.compiler.release=21`),可在更高 JDK 上运行。
  66 +- 新增原子组件类型:在 `FieldControl.tsx` 注册表登记即可,业务页面零改动。
  67 +- 演示引导数据在 `engine/src/main/resources/seed-data.yaml`(非本体的一部分)。
  68 +- 订单审核场景 `SCENE_AUDIT_ORDER` 由 M8 `template_order_audit` 渲染:选订单 + 选审核人(员工引用下拉)+ 审核结论/备注,端到端演示 `AuditOrder` 的 7 opcode 程序。`auditResult` 为 M1 上的**瞬态字段**(无 M3 列,不落库,仅派生 `orderStatus`)。
  69 +- 未绑定 M8 模板的场景(如 `SCENE_DEMO_STEPTYPES`,字段为合成变量非 M1 字段):前端提供「无表单直接运行」入口,以空 payload 调 execute,引擎逐条通用解释 flowSteps 产出 trace。
  70 +- M5 `encryptRules.storageEncrypt` 字段由 `EncryptService` 落库加密/读时解密;热重载仅管理员(M8 dragRolePermission)。
  71 +- 审计整改逐条见 `docs/审计整改说明.md`。仍为演示简化:真实认证/幂等/M0 强校验。
... ...
README.md 0 → 100644
  1 +++ a/README.md
  1 +# 本体驱动 DDD+EDA · 元数据解释引擎(可运行 Demo)
  2 +
  3 +> 一份**完整的前后端**,一个**能运行的网页**:网页上可操作「新增订单」,且该操作**完全由 `models/` 下的 YAML 本体模型驱动**——**改了模型文件,前端表单与后端执行的操作定义同时改变,无需改一行代码**。
  4 +
  5 +本项目按 `request.txt` 的 11 层本体模型(M0–M8、MetaRule)落地为一个**元数据驱动**系统:
  6 +
  7 +- **后端 = 架构解释引擎**(Java 21 + Spring Boot 3.5):启动时加载 YAML,构建内存元模型,用**通用命令执行器**解释执行 `CreateOrder`(无任何订单专用业务代码)。
  8 +- **前端 = 通用渲染引擎**(React 18 + Ant Design 5 + Vite):读取 M8 模板 + 后端合成的元数据,**动态渲染**「新增订单」页并提交到通用命令 API。
  9 +
  10 +---
  11 +
  12 +## 一、快速运行
  13 +
  14 +前置:**Java 21+、Node 18+、npm**(Maven 无需预装——项目内含 `./mvnw` 包装器,首次运行自动下载 Maven 3.9.9 与全部依赖;本机实测 Java 25 + Node 26 亦可)。
  15 +
  16 +```bash
  17 +./start.sh
  18 +```
  19 +
  20 +脚本会:启动后端(8080) → 等待就绪 → 安装前端依赖 → 启动前端(Vite)。
  21 +浏览器打开 Vite 提示的 `Local` 地址(默认 `http://localhost:5173`,被占用则顺延如 5174),进入「用户创建订单完整流程」页即可操作**新增订单**。
  22 +
  23 +分别启动(调试用):
  24 +
  25 +```bash
  26 +# 终端 A —— 后端(用包装器,无需预装 Maven)
  27 +cd engine && ./mvnw spring-boot:run
  28 +# 终端 B —— 前端
  29 +cd web && npm install && npm run dev
  30 +```
  31 +
  32 +默认存储为 **H2 内存库(MySQL 兼容模式)**,零外部依赖开箱即跑;H2 控制台 `http://localhost:8080/h2-console`(JDBC URL: `jdbc:h2:mem:trade_db`,用户 `sa`,空密码)。
  33 +
  34 +---
  35 +
  36 +## 二、验证「改了模型文件 → 操作定义随之改变」(核心验收点)
  37 +
  38 +系统的关键在于**行为源于模型、而非代码**。三种典型验证(改完点页面右上「热重载模型」或
  39 +`curl -X POST localhost:8080/api/meta/reload -H 'X-Principal: backend_admin'`,无需重启;热重载仅管理员可触发):
  40 +
  41 +1. **改前端表单(M8)**:编辑 `models/M8-front-schema.yaml`,把某组件 `compType: input` 改成 `select`、改 `formLabel`、增删组件或改 `span` → 热重载 → 「新增订单」页对应字段的**控件类型/标签/布局立即改变**。
  42 +2. **改风控规则(MetaRule)**:编辑 `models/MetaRule-business.yaml`,把 `single_order_max_amount` 由 `50000` 改成 `100` → 热重载 → 普通用户下单金额超 100 即被 **REJECT**(同一笔请求从通过变为拦截)。
  43 +3. **改领域字段类型(M1)**:编辑 `models/M1-domain.yaml`,把某字段 `type: string` 改成 `int` → 热重载 → 该字段的输入控件类型随之改变(配合 M3 增列后即可落库)。
  44 +
  45 +> `curl` 复现规则 2:见 `tests/verify-model-change.sh`。
  46 +
  47 +---
  48 +
  49 +## 三、目录结构
  50 +
  51 +```
  52 +onto-implementation/
  53 +├── models/ # ★ 唯一事实源:11 份本体 YAML(M0–M8、MetaRule)
  54 +├── engine/ # 后端解释引擎(Spring Boot)
  55 +│ └── src/main/java/com/onto/engine/
  56 +│ ├── model/ # 元模型加载与类型化查询 (ModelRepository)
  57 +│ ├── meta/ # 场景渲染 Schema 合成 (SceneSchemaService)
  58 +│ ├── command/ # ★ 通用命令执行器 (CommandEngine)
  59 +│ ├── rule/ # Aviator MetaRule 引擎 (RuleEngine)
  60 +│ ├── persist/ # 据 M3 动态建表/读写 (SchemaInitializer/DynamicRepository)
  61 +│ ├── event/ # Outbox + 跨聚合最终一致 (EventEngine)
  62 +│ ├── security/ # M5 脱敏 (MaskService)
  63 +│ └── web/ # REST 控制器
  64 +├── web/ # 前端通用渲染引擎(React + Vite)
  65 +│ └── src/
  66 +│ ├── engine/ # ★ SchemaRenderer / FieldControl(组件注册表)
  67 +│ ├── panels.tsx # 执行追踪 / 溯源 / 模型查看器 / 订单事件
  68 +│ └── App.tsx
  69 +├── templates/ # 代码生成模板(M1→DDL、M2→命令、M8→React 组件)
  70 +├── deploy/ # 生产部署(docker-compose:MySQL8 + RocketMQ + Nginx)
  71 +├── docs/ # 场景推演文档(架构运行原理)
  72 +├── tests/ # 验证脚本
  73 +└── start.sh
  74 +```
  75 +
  76 +---
  77 +
  78 +## 四、架构分层与引擎如何"解释执行"
  79 +
  80 +| 层 | 职责 | 引擎如何使用 |
  81 +|---|---|---|
  82 +| **M0** meta-schema | 全域校验根规范 | 描述所有层的 JSON Schema,CI 可据此校验 YAML |
  83 +| **M1** domain | DDD 聚合唯一数据源 | 字段类型、组合子实体结构 → 前端控件类型 / DDL / 值转换 |
  84 +| **M2** command | 聚合命令入参/校验/事件 | 通用执行器据此校验入参、决定发何事件 |
  85 +| **ME** event | EDA 事件与跨聚合一致性 | Outbox 事件定义 + `OrderCreated→StockDeduct` |
  86 +| **M3** deploy | 实体↔表/字段映射、命令 API | 自动建表、动态 SQL、前端命令 URL |
  87 +| **M4** scene | Saga 流程编排 | 通用执行器逐 `flowSteps` 解释:readOnlyCheck→bindCommand→return |
  88 +| **M5** security | 权限/脱敏 | 权限校验、敏感字段掩码渲染 |
  89 +| **M6** monitor | 监控告警 | 指标/告警规范(文档态) |
  90 +| **M7** sla | 流量/熔断 | SLA 规范(文档态) |
  91 +| **MetaRule** | 前后端统一动态规则 | Aviator 实时执行:REJECT/ALERT,前后端同源 |
  92 +| **M8** front-schema | React 拖拽页面模板 | ★ 前端渲染引擎读取它动态生成整页 |
  93 +
  94 +**「新增订单」端到端(SCENE_CREATE_ORDER)**:
  95 +权限(M5) → readOnlyCheck 客户存在(M4/M1) → 入参校验(M2/M8) → 风控(MetaRule) → 落库(M3) → 发事件(ME/Outbox) → 跨聚合扣库存(ME 最终一致)。
  96 +页面右侧「执行追踪」把每一步及其来源层实时可视化。
  97 +
  98 +详见 [`docs/场景推演.md`](docs/场景推演.md)。
  99 +
  100 +---
  101 +
  102 +## 五、主要 API
  103 +
  104 +| 方法 | 路径 | 说明 |
  105 +|---|---|---|
  106 +| GET | `/api/meta/scenes` | 场景列表(M4) |
  107 +| GET | `/api/meta/scene/{sceneId}` | 场景渲染 Schema(M8+M1+M2+M3+M5+MetaRule 合成) |
  108 +| GET | `/api/meta/model/{code}` | 查看某层模型解析内容(M0..M8/MetaRule) |
  109 +| POST | `/api/meta/reload` | 热重载全部 YAML(改模型即时生效) |
  110 +| GET | `/api/data/{aggregateId}` | 引用下拉数据源(客户/商品,含脱敏) |
  111 +| POST | `/api/scene/{sceneId}/precheck` | 提交前预检(校验+MetaRule 风控,不落库) |
  112 +| POST | `/api/scene/{sceneId}/execute` | 通用执行场景命令(完整解释执行) |
  113 +
  114 +---
  115 +
  116 +## 六、通用性与安全边界(诚实说明)
  117 +
  118 +经 2026-07-20 审计后本轮"全面改造":把原先散落在 Java/TS 里的业务字面量**上移为可机器读的本体元数据**
  119 +(M1 `constraints`、M2 `commandType/effect/derivations`、ME `sourceItemEntity/paramMap`),
  120 +使校验、风控、跨聚合一致性、落库分派**全部模型驱动**。因此新增第二个聚合/场景(如已内置的"新增客户")
  121 +或重命名字段,通常**只改 YAML**。逐条整改见 [`docs/审计整改说明.md`](docs/审计整改说明.md)。
  122 +
  123 +**仍为演示简化(未做成生产级,明确标注)**:
  124 +- **认证**:未接入 OAuth2/JWT;主体来自 `X-Principal` 头(可伪造),仅演示 M5 权限"通过/拒绝"分支。热重载/新增客户已要求管理员主体。生产必须换真实认证。
  125 +- **幂等**:命令未加请求幂等键。
  126 +- **M0 校验**:M0 仍为规范文档,未在加载时对各层做 JSON-Schema 强校验。
  127 +
  128 +> M8 补全后四个场景(新增订单/新增客户/修改地址/取消订单)均可渲染并端到端可用:`update` 命令含子实体更新、`cancel` 走通用 `StockReturn` 回补库存(均有测试)。
  129 +
  130 +## 七、生产化部署(本机为轻量可跑版)
  131 +
  132 +为保证"本机开箱即跑",Demo 采用 H2 内存库 + 进程内 Outbox/事件总线 + **真实 Aviator 规则引擎 + 真实字段加密**。
  133 +生产映射(真 MySQL8 + RocketMQ + Nginx)见 [`deploy/`](deploy/):`docker-compose up` 拉起完整栈,后端切 `prod` profile
  134 +连真中间件、收紧 CORS、禁用 H2 控制台、要求经环境/密管注入 DB 口令与加密密钥;YAML 驱动逻辑与规则引擎**完全一致、无需改动**。
... ...
deploy/Dockerfile.engine 0 → 100644
  1 +++ a/deploy/Dockerfile.engine
  1 +# 多阶段构建:Maven 编译 -> 精简运行镜像
  2 +FROM maven:3.9-eclipse-temurin-21 AS build
  3 +WORKDIR /src
  4 +COPY engine/pom.xml .
  5 +RUN mvn -q -B dependency:go-offline
  6 +COPY engine/src ./src
  7 +RUN mvn -q -B -DskipTests package
  8 +
  9 +FROM eclipse-temurin:21-jre
  10 +WORKDIR /app
  11 +COPY --from=build /src/target/onto-engine.jar app.jar
  12 +# 模型目录通过挂载卷提供(改 YAML 即改行为)
  13 +ENV ONTO_MODELS_DIR=/app/models
  14 +EXPOSE 8080
  15 +ENTRYPOINT ["java","-jar","app.jar","--spring.profiles.active=prod"]
... ...
deploy/Dockerfile.web 0 → 100644
  1 +++ a/deploy/Dockerfile.web
  1 +# 构建前端静态资源 -> Nginx 托管
  2 +FROM node:20-alpine AS build
  3 +WORKDIR /src
  4 +COPY web/package.json web/package-lock.json* ./
  5 +RUN npm install
  6 +COPY web/ .
  7 +RUN npm run build
  8 +
  9 +FROM nginx:1.27-alpine
  10 +COPY --from=build /src/dist /usr/share/nginx/html
  11 +COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf
  12 +EXPOSE 80
... ...
deploy/README.md 0 → 100644
  1 +++ a/deploy/README.md
  1 +# 生产部署(docker-compose)
  2 +
  3 +将 Demo 的轻量替身映射到生产中间件:**Nginx + 引擎 + MySQL8 + RocketMQ**。
  4 +
  5 +```bash
  6 +# 仓库根目录执行
  7 +docker compose -f deploy/docker-compose.yml up --build
  8 +```
  9 +
  10 +- 前端: http://localhost:8088
  11 +- 引擎: http://localhost:8080
  12 +- MySQL: localhost:3306(trade/trade123)
  13 +- RocketMQ Namesrv: 9876
  14 +
  15 +关键点:
  16 +- `engine` 以 `prod` profile 启动,连真实 MySQL(`application-prod.yml`)。
  17 +- `models/` 以只读卷挂载进引擎;线上改 YAML 后 `curl -X POST http://localhost:8080/api/meta/reload` 即热更新,**无需重新构建镜像**。
  18 +- 业务逻辑与 MetaRule 规则引擎在 H2/MySQL 两种模式下**完全一致**,仅数据源不同。
  19 +
  20 +> 本机仅想快速看「新增订单」跑通,用仓库根目录 `./start.sh`(H2 内存库、零外部依赖)更快。
... ...
deploy/docker-compose.yml 0 → 100644
  1 +++ a/deploy/docker-compose.yml
  1 +# 生产拓扑映射:Nginx(前端) + 引擎 + MySQL8 + RocketMQ
  2 +# 从仓库根目录执行: docker compose -f deploy/docker-compose.yml up --build
  3 +# 说明:MySQL/RocketMQ 为生产映射;本机快速体验用根目录 ./start.sh(H2 内存库)即可。
  4 +services:
  5 + mysql:
  6 + image: mysql:8.0
  7 + environment:
  8 + MYSQL_ROOT_PASSWORD: root123
  9 + MYSQL_DATABASE: trade_db
  10 + MYSQL_USER: trade
  11 + MYSQL_PASSWORD: trade123
  12 + ports: ["3306:3306"]
  13 + healthcheck:
  14 + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-proot123"]
  15 + interval: 5s
  16 + timeout: 3s
  17 + retries: 20
  18 +
  19 + rocketmq-namesrv:
  20 + image: apache/rocketmq:5.3.0
  21 + command: sh mqnamesrv
  22 + ports: ["9876:9876"]
  23 +
  24 + rocketmq-broker:
  25 + image: apache/rocketmq:5.3.0
  26 + command: sh mqbroker -n rocketmq-namesrv:9876 --enable-proxy
  27 + environment:
  28 + NAMESRV_ADDR: rocketmq-namesrv:9876
  29 + depends_on: [rocketmq-namesrv]
  30 + ports: ["10911:10911", "8081:8080"]
  31 +
  32 + engine:
  33 + build:
  34 + context: ..
  35 + dockerfile: deploy/Dockerfile.engine
  36 + environment:
  37 + DB_HOST: mysql
  38 + DB_USER: trade
  39 + DB_PASS: trade123
  40 + ROCKETMQ_NAMESRV: rocketmq-namesrv:9876
  41 + volumes:
  42 + # 挂载模型目录:线上改 YAML + 调 /api/meta/reload 即热更新,无需重发版
  43 + - ../models:/app/models:ro
  44 + depends_on:
  45 + mysql:
  46 + condition: service_healthy
  47 + ports: ["8080:8080"]
  48 +
  49 + web:
  50 + build:
  51 + context: ..
  52 + dockerfile: deploy/Dockerfile.web
  53 + depends_on: [engine]
  54 + ports: ["8088:80"]
... ...
deploy/nginx.conf 0 → 100644
  1 +++ a/deploy/nginx.conf
  1 +server {
  2 + listen 80;
  3 + server_name _;
  4 +
  5 + root /usr/share/nginx/html;
  6 + index index.html;
  7 +
  8 + # SPA 路由回退
  9 + location / {
  10 + try_files $uri $uri/ /index.html;
  11 + }
  12 +
  13 + # 反代到后端引擎,前端无需硬编码后端地址
  14 + location /api/ {
  15 + proxy_pass http://engine:8080;
  16 + proxy_set_header Host $host;
  17 + proxy_set_header X-Real-IP $remote_addr;
  18 + }
  19 +}
... ...
docs/场景推演.md 0 → 100644
  1 +++ a/docs/场景推演.md
  1 +# 场景推演:一次「新增订单」如何被本体模型驱动执行
  2 +
  3 +本文用 `SCENE_CREATE_ORDER` 这一具体场景,逐步推演"架构引擎如何解释与执行模型",说明整体运行原理。
  4 +
  5 +---
  6 +
  7 +## 0. 一图看懂
  8 +
  9 +```
  10 + models/*.yaml (唯一事实源)
  11 + ┌───────────────────────────────────────────────────────────┐
  12 + │ M0 校验 M1 领域 M2 命令 ME 事件 M3 部署 M4 场景 │
  13 + │ M5 安全 M6 监控 M7 SLA MetaRule 规则 M8 前端模板 │
  14 + └───────────────────────────────────────────────────────────┘
  15 + │(启动/热重载加载) │(GET /api/meta/scene)
  16 + ▼ ▼
  17 + ┌───────────────────────┐ ┌──────────────────────────┐
  18 + │ 后端 解释引擎(Java) │◀──REST──│ 前端 渲染引擎(React) │
  19 + │ ModelRepository │ │ SceneForm/FieldControl │
  20 + │ CommandEngine (通用) │ │ 读 M8 动态生成「新增订单」│
  21 + └───────────────────────┘ └──────────────────────────┘
  22 +```
  23 +
  24 +**关键**:没有 `OrderController`、没有 `OrderService.createOrder()` 这类专用代码。`CommandEngine` 对任何场景都走同一套解释流程;`SceneForm` 对任何模板都走同一套渲染流程。
  25 +
  26 +---
  27 +
  28 +## 1. 页面从哪来:M8 → 合成 Schema → React 渲染
  29 +
  30 +前端请求 `GET /api/meta/scene/SCENE_CREATE_ORDER`。`SceneSchemaService` 做**跨层合成**:
  31 +
  32 +1. 取 M4 场景 `SCENE_CREATE_ORDER` → 得 `flowSteps`、`permissionBind:[order_create]`、绑定命令 `OrderAggregate.CreateOrder`。
  33 +2. 取 M8 中 `bindSceneId == SCENE_CREATE_ORDER` 的模板 `template_order_master_detail`,逐组件**富化**:
  34 + - 用 **M1** 补 `fieldType`(如 `totalAmount → decimal`、`quantity → int`)→ 前端据此选控件。
  35 + - 用 **M5** 补 `masked`(`receiverPhone/contactPhone` 打脱敏标记)。
  36 + - 对绑定到某聚合根标识的 `select`(`customerId→CustomerAggregate`、`productId→ProductAggregate`)补 `optionsSource`(下拉数据源端点)。
  37 +3. 附 **M2** 命令入参/校验、**M3** 命令 API 地址(`/api/v1/order/create`)、**MetaRule** 绑定本场景的规则、以及溯源信息。
  38 +
  39 +前端 `SceneForm` 拿到后按模板结构渲染:
  40 +- `card` + `aggregate_root` → **主信息卡片**(订单主字段)
  41 +- `table` + `aggregate_entity(OrderItem)` → **可增删行的明细表格**
  42 +- `card` + `aggregate_entity(PaymentTerm/DeliveryAddressEntity)` → **单行子实体卡片**
  43 +- 每个叶子组件按 `compType` 经 `FieldControl` 注册表映射到 antd 控件。
  44 +
  45 +> 因此:改 M8 的 `compType/formLabel/span/childComponents` → 页面即变;这就是"仅维护模板、不改前端源码"。
  46 +
  47 +---
  48 +
  49 +## 2. 提交做什么:payload → 通用命令执行
  50 +
  51 +用户填完点「提交」,前端按 M8 绑定把值组装为**领域结构**:
  52 +
  53 +```json
  54 +{
  55 + "master": { "customerId":"C002", "totalCurrency":"CNY", "totalAmount":7997, "createTime":"..." },
  56 + "details": {
  57 + "OrderItem": [ { "productId":"P001", "quantity":1, "itemPrice":6999, "itemCurrency":"CNY" } ],
  58 + "PaymentTerm": [ { "paymentType":"月结", "dueDays":30 } ],
  59 + "DeliveryAddressEntity": [ { "province":"广东", ... , "receiverPhone":"13800002222" } ]
  60 + }
  61 +}
  62 +```
  63 +
  64 +POST 到 `/api/scene/SCENE_CREATE_ORDER/execute`。`CommandEngine.execute` **逐 M4 flowSteps 解释**:
  65 +
  66 +| flowStep(M4) | 引擎动作 | 依据层 |
  67 +|---|---|---|
  68 +| `permission`(隐式) | `order_create` 的 `allowPrincipals` 是否含当前主体 | M5 |
  69 +| `readOnlyCheck: CustomerAggregate` | 校验 `master.customerId` 存在于客户表 | M4 + M1 + M3 |
  70 +| `bindCommand: CreateOrder` | 见下方"命令执行" | M2 |
  71 +| `return` | 汇总结果返回 | M4 |
  72 +
  73 +**命令执行(CreateOrder)内部**:
  74 +
  75 +1. **校验**:必填项来自 **M8 `required`** 标记(与前端同源);`收货手机号格式`、`dueDays>=0` 等来自 **M2 `validations`** 文本的解释。
  76 +2. **风控(MetaRule / Aviator)**:
  77 + - 订单级:以 `totalAmount` + 客户 `customerLevel`(查客户表得)评估 `R_ORDER_AMOUNT_LIMIT`:
  78 + `totalAmount > #{single_order_max_amount} && customerLevel != "VIP"` → 命中则 **REJECT**。
  79 + - 逐商品级:以每个商品 `stockNum` 评估 `R_STOCK_LOW_WARN`:`stockNum < #{stock_warn_min}` → **ALERT**(不阻断)。
  80 +3. **落库(M3)**:生成 `orderId`;据 `t_order_main` 的 `fieldMap` 写主单;据组合子实体(M1 relations)逐个写 `t_order_item / t_order_payment_term / t_order_address`,并补父外键 `order_id`。**列名、表名全部来自 M3**。
  81 +4. **发事件(ME/Outbox)**:据 M2 `emitEvents` 写 `OrderCreated / OrderItemAdded / ...` 到 `t_domain_outbox`;topic 取自 ME(无则按命名派生)。
  82 +5. **跨聚合最终一致(ME)**:匹配 `crossAggConsistencyRules` 中 `OrderCreated → ProductAggregate.StockDeduct`,逐商品扣减库存(库存列同样取自 M3)。
  83 +
  84 +页面右侧「执行追踪」会显示真实的逐步日志,例如:
  85 +
  86 +```
  87 +M5 permission — 主体 normal_user 通过权限校验 [order_create]
  88 +M4 readOnlyCheck — 校验 CustomerAggregate 引用存在:通过
  89 +M2/M8 validate — 命令 CreateOrder 入参校验通过
  90 +MetaRule metaRule — 风控规则评估完成,命中 0 条;REJECT=0,ALERT=0
  91 +M3 persist — 写入 Order(O3EQD8P2591) 及 3 条子实体,表结构源自 M3
  92 +ME emitEvents — 写入 Outbox 事件 4 条
  93 +ME crossConsistency — 触发 StockDeduct 扣减 1 个商品库存
  94 +M4 return — 场景执行完成,返回结果
  95 +```
  96 +
  97 +---
  98 +
  99 +## 3. 推演三条"改模型即改行为"
  100 +
  101 +### (a) 大额拦截:改 MetaRule
  102 +把 `single_order_max_amount: 50000 → 100`,热重载。**同一笔** 普通用户 7997 元订单:
  103 +- 改前:`7997 > 50000` 为假 → 通过;
  104 +- 改后:`7997 > 100 && level!="VIP"` 为真 → **REJECT**,返回 `普通用户单笔订单不可超过50000元...`。
  105 +> 规则只维护一份;前端「提交前预检」调用同一后端 MetaRule,实时给出同样结论。
  106 +
  107 +### (b) 表单改版:改 M8
  108 +把 `input_currency` 的 `compType: input → select`、改 `formLabel`,热重载。「新增订单」页的"结算币种"从文本框变为下拉、标签同步更新——**未动任何 React 代码**。
  109 +
  110 +### (c) 字段类型:改 M1
  111 +把某字段 `type: string → int`,热重载。合成 Schema 的 `fieldType` 变化 → 前端控件从 `Input` 变为 `InputNumber`;如需落库再在 M3 增列,`SchemaInitializer` 于重载时 `ALTER TABLE ADD COLUMN` 自动补列。
  112 +
  113 +---
  114 +
  115 +## 4. 为什么这套架构成立
  116 +
  117 +- **单一事实源**:字段口径(M1)被前端组件、后端校验、DDL、事件负载共同引用,天然对齐,杜绝前后端不一致。
  118 +- **解释而非生成**:引擎在运行时解释模型,故"热重载即变",无需重新编译/发版。
  119 +- **通用而非专用**:新增一个业务场景 = 新增一套 M1/M2/M3/M4/M8 描述 + 绑定 MetaRule,无需新增 Controller/Service/React 页面。
  120 +- **关注点分层**:业务专家维护 M8 模板与 MetaRule 规则;架构师维护 M0–M7 底座;二者解耦。
... ...
docs/审计报告-2026-07-20.md 0 → 100644
  1 +++ a/docs/审计报告-2026-07-20.md
  1 +# 审计报告 · 本体驱动 DDD+EDA 元数据引擎
  2 +
  3 +- **日期**:2026-07-20
  4 +- **范围**:`engine/`(17 个 Java 文件 ~1.9k 行)、`web/`(7 个 TS/TSX ~0.7k 行)、`models/`(11 份 YAML ~1.3k 行)、`deploy/`、配置
  5 +- **方法**:6 维度并行探查(铁律 / 正确性 / 安全 / EDA 一致性 / 前端 / 模型完整性)→ 每条发现由两名独立验证者对抗核验(一名"复现"、一名专职"反驳")。共 116 个 agent;**55 条候选 → 5 条被驳回、50 条通过复现验证、其中 32 条连"反驳者"也无法推翻**。全部源码另经人工逐条复核。
  6 +
  7 +> 置信度分层(本报告术语):
  8 +> - **A 级·双重确认**(32 条):复现者与反驳者**都**判定为真实缺陷 —— 最高置信。
  9 +> - **B 级·单边确认/严重度存疑**(18 条):复现者判定真实、反驳者提出异议(多为严重度或"是否属既定设计"之争)—— 需人工裁定。
  10 +> - **驳回**(5 条):复现者无法构造真实触发场景,或属"文档态/演示简化"的既定设计。
  11 +
  12 +---
  13 +
  14 +## 一句话结论
  15 +
  16 +**"铁律"被系统性违背:`engine/` 不是通用解释器,而是"带通用脚手架的订单专用解释器"。**
  17 +README 承诺的"改模型即改行为、不改一行代码"只在极窄切片内成立(M8 的 `compType`/`formLabel`/`span`、MetaRule 三个白名单变量的阈值、M1 类型 → DDL)。一旦**新增第二个聚合/场景、重命名字段、或修改跨聚合规则**,都必须改 Java/TS 代码。
  18 +
  19 +唯一的**数据损坏级**问题是**超卖链路**:库存不足/不存在时订单仍提交,且 demo 中无任何补偿接线。
  20 +
  21 +---
  22 +
  23 +## 二、按主题的执行摘要
  24 +
  25 +### A. 🔴 铁律违背(本项目最核心问题)
  26 +
  27 +两个"通用执行器"里塞满订单/客户/商品专用逻辑:
  28 +
  29 +| 位置 | 硬编码内容 |
  30 +|---|---|
  31 +| `CommandEngine.java:245-255` `runRules()` | 风控变量 `totalAmount`/`customerLevel`/`stockNum`,实体 `OrderItem`、字段 `productId` |
  32 +| `CommandEngine.java:225-234` `validate()` | **子串匹配中文** `"手机号"`/`"dueDays"` 触发校验,写死 `DeliveryAddressEntity.receiverPhone`、`PaymentTerm.dueDays` |
  33 +| `CommandEngine.java:276-294` | `customerLevel()` / `productStock()` 整段客户域/商品域方法(写死 `CustomerAggregate`/`ProductAggregate`/`cust_level`/`stock_num`) |
  34 +| `CommandEngine.java:185-186` | 用字面量 `"OrderCreated"` 触发跨聚合一致性,写死 `productId`/`quantity` |
  35 +| `CommandEngine.java:386` | `idPrefix()` = 聚合首字母(注释 "O for Order") |
  36 +| `EventEngine.java:73,89` | 跨聚合一致性只认 `"StockDeduct"`,写死 `Product`/`stockNum`/`ProductStockDeducted` |
  37 +| `EventEngine.java:25` | Outbox 表名 `t_domain_outbox` 写死,**无视 ME YAML 里可配的 `outbox.table`** |
  38 +| 前端 `SceneForm.tsx:96`、`panels.tsx:34/47/143`、`api.ts:27`、`App.tsx:15`、`DataController.java:31` | 写死 `orderId`、"订单创建成功"、`/data/orders/list`、`OrderAggregate`、订单/库存列名 |
  39 +
  40 +> **验证**:把 M1 里 `stockNum` 改名,`productStock()`/`deductStock()` 会**静默失效**(走 `getOrDefault` 兜底列名,取不到就当"无库存/无规则"),而非报错 —— 即"改模型不该改代码"承诺被打破的直接证据。
  41 +
  42 +### B. 🔴 正确性 / 数据一致性(可触发的真实 Bug)
  43 +
  44 +1. **超卖 / 库存不扣,订单照样提交**(`CommandEngine.java:185-197`)——`deductStock` 返回 `FAIL_INSUFFICIENT`/`SKIP_NOT_FOUND` 时**不抛异常**,`@Transactional execute()` 仍 `success=true` 提交。用 P002(库存 8) 下单 20 件即可复现。*(严重度存疑:若视作"事件最终一致 + 补偿"则可接受,但 demo 里没有任何补偿接线,`pollAndPublish` 只翻状态,故实为超卖漏洞。)*
  45 +2. **并发超卖 / 负库存**(`EventEngine.java:100-112`)——先 `SELECT` 读库存、Java 判 `current<qty`、再 `SET stock=stock-?` 无 `AND stock>=?`、无行锁。
  46 +3. **无幂等**(`CommandEngine.java:67`)——无请求键;双击/重试 = 重复订单 + 重复事件 + 重复扣减。
  47 +4. **`reload()` 与在途请求数据竞争**(`ModelRepository.java:51-64`)——`clear()` 后重填普通 `LinkedHashMap`,读取方无同步。
  48 +5. **非数字入 `int/decimal` 崩溃**(`DynamicRepository.java:104`)——预检通过、落库 `new BigDecimal("abc")` 抛未捕获异常 → HTTP 500。
  49 +6. `Map.of()` 遇 null `productId`/`quantity` NPE(`:183`);`deductStock` 对 NULL 库存列 `Long.parseLong("null")` 崩溃(`EventEngine.java:105`);`toBig()` 吞异常返回 0 → 非数字 `dueDays` 绕过 `>=0`(`:419`)。
  50 +7. **`executeCommand` 一律按"新建聚合根"落库**(`:148-168`)——`SCENE_MODIFY_ORDER_ADDR`/`SCENE_CANCEL_ORDER` 会**插入新订单**而非改/删(目前仅 Create 接线,属潜伏问题)。
  51 +
  52 +### C. 🟠 安全
  53 +
  54 +- **身份可伪造**(`CommandController.java:34`)——`X-Principal` 头直取、无认证,任何人可自称 `backend_admin`。
  55 +- **权限失败即放行**(`CommandEngine.java:359`)——场景无 `permissionBind` 时 `return true`。
  56 +- **越权读全表**——`/api/data/{aggregateId}`、`/orders/list` 无鉴权吐整表;`/api/meta/model/M5` 匿名暴露完整安全模型;`/api/meta/reload` 匿名改状态。
  57 +- **`storageEncrypt` 从未兑现**(`M5:30`)——声明手机号加密存储、前端也提示"存储加密",实际**明文入库**。
  58 +- **`totalAmount` 客户端可篡改**(`CommandEngine.java:246`)——从不按明细重算,且直接喂给风控 → 既能低报又能绕过限额。
  59 +- CORS `allowedOriginPatterns("*")` 未按 profile 隔离(含 prod);默认 profile 开无密码 H2 控制台;prod/compose 弱默认口令;负数 `quantity` 反向"增库存"。
  60 +- *非问题澄清:动态 SQL 表名/列名虽字符串拼接,但来源可信 YAML、值走 `?` 参数化,用户 payload 无法注入列名 → 仅纵深防御建议。*
  61 +
  62 +### D. 🟡 EDA / 事件架构(与宣称的落差)
  63 +
  64 +- 跨聚合一致性是**事务内同步内联调用**(`CommandEngine.java:185`),非 Outbox 驱动;`pollAndPublish` 只把 `NEW→PUBLISHED`,事件从不被消费 → "事件驱动最终一致"名不副实。
  65 +- `ME OrderCancelled→StockDeduct` 规则**永不触发**(引擎只用字面量 `"OrderCreated"` 调用),且语义**相反**(取消应回补库存)。
  66 +- `M2 CreateOrder.emitEvents` 引用 `OrderItemAdded`/`OrderPaymentTermSet`/`OrderDeliveryAddressSet`,`ME` **未定义**这三个事件。
  67 +- `eventPayload` 只从 `master` 取值(`:378-380`)→ payload 引用子实体/非领域字段的事件值为 null。
  68 +
  69 +### E. 🟡 模型完整性
  70 +
  71 +- `M8:20` 模板绑定 `SCENE_CREATE_CUSTOMER`,`M4` **从未定义** → 死模板。
  72 +- `M0` 自称"全域校验根规范"但**从未被引擎执行**,各层实际违反 M0 自身模式;`MetaRule.vip_discount_limit` 定义但无人引用。
  73 +- *合理设计:M6/M7 加载但不被读取 —— README 明确标注"文档态",不算缺陷。*
  74 +
  75 +---
  76 +
  77 +## 三、修复优先级
  78 +
  79 +1. **超卖 + 幂等**(唯一数据损坏级):库存不足在 persist 前预留并中止、或真正接补偿;`execute()` 加幂等键。
  80 +2. **收敛安全面**:真实认证替代 `X-Principal`、权限默认拒绝、`/api/meta/*` 与 `/api/data/*` 加鉴权、CORS 按 profile 收紧、兑现 `storageEncrypt`、服务端重算 `totalAmount`。
  81 +3. **兑现"通用"承诺**:把 `CommandEngine`/`EventEngine` 的订单/商品硬编码抽为"由 M2 结构化 `validations`、ME `crossAggConsistencyRules` 通用驱动";否则应在 README 诚实标注当前通用边界。
  82 +
  83 +---
  84 +
  85 +## 附录 A · A 级发现 —— 双重确认(复现者与反驳者一致判定为真实,共 32 条)
  86 +
  87 +> 本级发现两名对抗验证者独立都无法推翻,置信度最高。
  88 +
  89 +### A1. Cross-aggregate consistency is a hardcoded stock-deduction, not model-driven
  90 +
  91 +- **严重度**:BLOCKER **维度**:iron-rule **位置**:`engine/src/main/java/com/onto/engine/event/EventEngine.java:73`
  92 +- **缺陷**:The supposedly-generic EDA consistency engine only knows how to execute one specific business command (StockDeduct) and hardcodes the product aggregate root name, the stock field/column, item field names, and the emitted event.
  93 +- **证据**:
  94 +```
  95 +Line 73: `if ("StockDeduct".equals(targetCmd)) {` ; deductStock(): line 86 `repo.fieldMap(productAgg, "Product")`, line 89 `String stockCol = fmap.getOrDefault("stockNum", "stock_num");`, line 90 `Object productId = item.get("productId");`, line 91 `item.getOrDefault("quantity", item.get("deductQty"))`, line 96 `action.put("targetCommand", "StockDeduct");`, line 117 `append(productAgg, "ProductStockDeducted", Map.of("productId", productId, "stockNum", remaining));`
  96 +```
  97 +- **影响 / 触发场景**:ME-event.yaml declares `targetCommand: StockDeduct` generically, but the engine branches on the literal string "StockDeduct" and then applies a hand-coded decrement of the literal field `stockNum`/column `stock_num`, keyed by literal `productId`, emitting literal `ProductStockDeducted`. Renaming StockDeduct/stockNum/productId/ProductStockDeducted or the `Product` root in the YAML — or declaring any *other* cross-agg consistency command (e.g. a credit-limit deduction) — silently does nothing, because the only executable path is this order/product-specific if-branch. The decrement semantics should come from the M2 StockDeduct command definition + M1 field, executed generically (e.g. via CommandEngine), not baked in.
  98 +- **建议修复**:Drive the consistency action from the model: resolve the target command from M2, its target field/operation and emitted event from M2/ME, the root entity name via models.rootName(productAgg), and the mutated field via M1/M3 — no literal command/field/event names in Java.
  99 +- **复现核验**:All quoted evidence is verbatim-confirmed in engine/src/main/java/com/onto/engine/event/EventEngine.java. Line 73 `if ("StockDeduct".equals(targetCmd)) {` gates the entire cross-aggregate consistency execution on a literal business command name. deductStock() hardcodes the aggregate-root name (line 86 `repo.fieldMap(productAgg, "Product")`), the stock field/column (line 89 `fmap.getOrDefault("stockNum", "stock_num")`), the item keys (line 90 `item.get("productId")`, line 91 `item.getOrDefault("quantity", item.get("deductQty"))`), the command label (line 96), and the emitted event with its payload keys (line 117 `append(productAgg, "ProductStockDeducted", Map.of("productId", productId, "stock …
  100 +- **反驳核验**:I attempted to kill this finding on five fronts and all failed against the actual code.
  101 +
  102 +REACHABILITY: Not dead code. CommandEngine.java:186 calls eventEngine.applyCrossConsistency("OrderCreated", Map.of("items", items)) inside the live CreateOrder path, and deductStock() performs a real repo.execute("UPDATE " + table + " SET " + stockCol + " = " + stockCol + " - ? WHERE ...") at EventEngine.java:111. So it is live business logic.
  103 +
  104 +NOT A FALLBACK: EventEngine.java:73 `if ("StockDeduct".equals(targetCmd))` is a hard gate — cross-agg consistency runs ONLY when the model's targetCommand is the literal string "StockDeduct". deductStock() then hardcodes the aggregate root "Product" (lines 86-88 r …
  105 +
  106 +### A2. CommandEngine feeds the rule engine a hardcoded order/product-specific variable set
  107 +
  108 +- **严重度**:BLOCKER **维度**:iron-rule **位置**:`engine/src/main/java/com/onto/engine/command/CommandEngine.java:244`
  109 +- **缺陷**:runRules() — the heart of generic risk control — hardcodes the exact business variable names, entity, and field that MetaRule expressions reference, so the generic RuleEngine is fed a non-generic, order-aware payload.
  110 +- **证据**:
  111 +```
  112 +Lines 244-257: `Object level = customerLevel(ctx);` / `orderVars.put("totalAmount", ctx.master.get("totalAmount"));` / `orderVars.put("customerLevel", level);` ... `for (Map<String, Object> it : ctx.details.getOrDefault("OrderItem", List.of())) { Object pid = it.get("productId"); Long stock = productStock(pid); ... for (RuleHit h : ruleEngine.evaluate(ctx.sceneId, Map.of("stockNum", stock)))`
  113 +```
  114 +- **影响 / 触发场景**:RuleEngine.evaluate() is genuinely generic (it can enumerate a rule's referenced variables via exp.getVariableNames() and only fires when all are present), but CommandEngine hand-builds a map containing exactly `totalAmount`, `customerLevel`, `stockNum` and iterates the literal `OrderItem` entity reading literal `productId`. These are precisely the variables in MetaRule-business.yaml's matchConditions. Rename totalAmount/stockNum/customerLevel in M1, rename the OrderItem entity, or add a new rule referencing any other field, and the rule silently never fires — defeating the 'edit-YAML-only' promise for risk rules. The variable set should be derived from the rules' referenced variable names and resolved generically from payload/master/details/referenced aggregates.
  115 +- **建议修复**:Collect required variable names from the scene's MetaRule expressions, then resolve each generically (master field, detail field, or via M1 refAggregate lookup) instead of the hardcoded totalAmount/customerLevel/stockNum/OrderItem/productId literals.
  116 +- **复现核验**:Evidence verified verbatim in engine/src/main/java/com/onto/engine/command/CommandEngine.java, runRules() lines 241-260: line 246 `orderVars.put("totalAmount", ctx.master.get("totalAmount"))`, line 247 `orderVars.put("customerLevel", level)`, line 251 `ctx.details.getOrDefault("OrderItem", List.of())`, line 252 `it.get("productId")`, line 255 `Map.of("stockNum", stock)`. Helper methods extend the violation: customerLevel() (276-284) hardcodes customerId/CustomerAggregate/Customer/customerLevel; productStock() (286-294) hardcodes ProductAggregate/Product/stockNum. These identifiers are precisely those the iron rule enumerates as forbidden in engine/.\n\nThe finding's core mechanism is confirm …
  117 +- **反驳核验**:Confirmed by reading engine/.../command/CommandEngine.java:241-260 and rule/RuleEngine.java:34-67 and models/MetaRule-business.yaml. runRules() hand-builds the rule-engine payload with Java string literals: orderVars.put("totalAmount", ctx.master.get("totalAmount")) and orderVars.put("customerLevel", level) (lines 246-247), iterates ctx.details.getOrDefault("OrderItem", List.of()) reading it.get("productId") (lines 251-252), and calls ruleEngine.evaluate(ctx.sceneId, Map.of("stockNum", stock)) (line 255). These literals — totalAmount, customerLevel, stockNum, the OrderItem entity, the productId field — are exactly the business identifiers the iron rule forbids in engine/ (the task itself nam …
  118 +
  119 +### A3. No authentication; authorization trusts spoofable X-Principal header
  120 +
  121 +- **严重度**:BLOCKER **维度**:security **位置**:`engine/src/main/java/com/onto/engine/web/CommandController.java:34`
  122 +- **缺陷**:The caller's identity is taken verbatim from a client-supplied HTTP header with no authentication layer, so any client can act as any principal (including backend_admin).
  123 +- **证据**:
  124 +```
  125 +@RequestHeader(value = "X-Principal", required = false) String principal) {
  126 + String p = (principal == null || principal.isBlank()) ? defaultPrincipal : principal; // CommandController L34-35; permitted() only does allow.contains(principal) (CommandEngine L362)
  127 +```
  128 +- **影响 / 触发场景**:M5 declares authMode: oauth2_jwt, but pom.xml has NO spring-boot-starter-security and there is no filter/JWT validation anywhere. The principal is whatever the caller puts in X-Principal. Attack: curl -X POST http://host:8080/api/scene/SCENE_CANCEL_ORDER/execute -H 'X-Principal: backend_admin' -d '{...}' passes every permission check. Even with no header, the default 'normal_user' is authorized for order_create/modify/cancel, so all command scenes are effectively open to anonymous callers. The entire M5 permission model is decorative.
  129 +- **建议修复**:Introduce real authentication (Spring Security + JWT per the declared oauth2_jwt mode); derive the principal from a verified token/session, never from an arbitrary request header. Reject requests without a validated identity.
  130 +- **复现核验**:Every element of the finding is verified in the actual code.
  131 +
  132 +1. Spoofable identity (CommandController.java:34-36): `execute(...)` takes the principal verbatim from a client header — `@RequestHeader(value = "X-Principal", required = false) String principal`, then `String p = (principal == null || principal.isBlank()) ? defaultPrincipal : principal; return commandEngine.execute(sceneId, payload, p);`. The identity is 100% client-controlled with no validation.
  133 +
  134 +2. Authorization only does a membership test (CommandEngine.java:357-365): `permitted()` reads the scene's `permissionBind`, then for each perm checks `allow.contains(principal)` (L362) against M5's `allowPrincipals`. Nothing verifies t …
  135 +- **反驳核验**:The finding is factually confirmed by the actual files and survives refutation. CommandController.java:34-36 reads the caller identity verbatim from the client-controlled `X-Principal` header and, when absent/blank, falls back to `defaultPrincipal` = `normal_user` (L16-17, L35). CommandEngine.permitted() (L357-364) performs only authorization: `allow.contains(principal)` against M5 allowPrincipals — there is no authentication. I verified there is no auth layer: `grep spring-boot-starter-security` on engine/pom.xml returns 0, and there is no SecurityFilterChain / OncePerRequestFilter / HandlerInterceptor / @PreAuthorize anywhere in engine/src. WebConfig.java only sets permissive CORS. models/ …
  136 +
  137 +### A4. convert() throws NumberFormatException on non-numeric int/decimal field; precheck approves it
  138 +
  139 +- **严重度**:HIGH **维度**:correctness **位置**:`engine/src/main/java/com/onto/engine/persist/DynamicRepository.java:105`
  140 +- **缺陷**:A non-numeric (or comma/locale-formatted) value in any int/decimal domain field passes validation and precheck but crashes with an uncaught NumberFormatException at persist time, rolling back the transaction and returning HTTP 500.
  141 +- **证据**:
  142 +```
  143 +case "int" -> s.isEmpty() ? null : new BigDecimal(s).longValue();
  144 +case "decimal" -> s.isEmpty() ? null : new BigDecimal(s); // new BigDecimal("6,999.00") / new BigDecimal("abc") throws NumberFormatException, unguarded
  145 +```
  146 +- **影响 / 触发场景**:CommandEngine.validate() never checks that numeric domain fields are actually numeric (only required/phone/dueDays<0), and RuleEngine swallows the Aviator error, so precheck returns reject=false. Concrete: POST /api/scene/SCENE_CREATE_ORDER/execute with body {"master":{"customerId":"C001","totalCurrency":"CNY","totalAmount":"6,999.00"},"details":{"OrderItem":[{"productId":"P001","quantity":1}]}} (default principal normal_user is authorized). Precheck says OK; execute persists Order and hits new BigDecimal("6,999.00") -> NumberFormatException -> 500. Same crash for totalAmount="abc" or dueDays="abc". The 'real-time validation shares one source' guarantee is violated.
  147 +- **建议修复**:In convert(), catch NumberFormatException and either reject via a validation error or coerce safely; and/or add a generic numeric-format validation in CommandEngine.validate() (driven by M1 field type) so precheck and execute agree before reaching persist.
  148 +- **复现核验**:Verified the full chain against the actual source. DynamicRepository.java:104-105 does new BigDecimal(s).longValue() (int) and new BigDecimal(s) (decimal) with no guard; BigDecimal rejects grouping separators ("6,999.00") and non-numeric ("abc"), throwing NumberFormatException. totalAmount is type: decimal in M1 (M1-domain.yaml:57-58, an aggregate-root attribute) and is present in the M3 root fieldMap as total_amt (M3-deploy.yaml:60), so CommandEngine.executeCommand -> repo.insertDomain(rootName) -> convert("decimal", value) is on the persist path (CommandEngine.java:155).
  149 +
  150 +CommandEngine.validate() (lines 205-239) never validates numeric parseability: it checks only M8 required blank-ness, r …
  151 +- **反驳核验**:The finding survives refutation; every link in its chain is confirmed by the code. (1) DynamicRepository.java:104-105 does `new BigDecimal(s)` for int/decimal with no try/catch — this throws NumberFormatException on "6,999.00" (grouping separators are rejected by BigDecimal) and on "abc". Contrast the guarded parseDateTime (lines 118-126) and CommandEngine.toBig (lines 418-420), showing convert() is genuinely unprotected. (2) totalAmount is type `decimal` (M1-domain.yaml:58) and mapped totalAmount->total_amt (M3-deploy.yaml:60), so it is passed to convert("decimal", value) during persist (CommandEngine:155 insertDomain). (3) CommandEngine.validate() (lines 205-239) performs no numeric-parse …
  152 +
  153 +### A5. Non-atomic check-then-decrement on stock allows oversell / negative stock under concurrency
  154 +
  155 +- **严重度**:HIGH **维度**:eda **位置**:`engine/src/main/java/com/onto/engine/event/EventEngine.java:100`
  156 +- **缺陷**:deductStock reads current stock with a plain SELECT, checks current<qty in Java, then issues a blind 'SET stock = stock - qty' with no 'AND stock >= qty' guard and no row lock, so two concurrent orders can both pass the check and drive stock negative.
  157 +- **证据**:
  158 +```
  159 +L100: Map<String, Object> row = repo.findByPk(table, pkCol, productId);
  160 +L105: long current = Long.parseLong(String.valueOf(row.get(stockCol)));
  161 +L106: if (current < qty) { ...FAIL_INSUFFICIENT...; return action; }
  162 +L111: repo.execute("UPDATE " + table + " SET " + stockCol + " = " + stockCol + " - ? WHERE " + pkCol + " = ?", qty, productId);
  163 +The SELECT (findByPk) takes no lock; the UPDATE has no stock>=qty predicate.
  164 +```
  165 +- **影响 / 触发场景**:Product P001 stock=10. Two concurrent execute() calls each with quantity=8. Under READ_COMMITTED both findByPk read 10; both evaluate current<qty (10<8) false and pass. T1's UPDATE (stock=stock-8) row-locks and commits -> 2. T2's UPDATE then applies to the committed value: 2-8 = -6, commits. Final stock = -6: 16 units sold from 10 in stock. TOCTOU race with no compensating guard.
  166 +- **建议修复**:Make the decrement conditional and atomic: 'UPDATE t SET stock = stock - ? WHERE pk = ? AND stock >= ?' and treat rowsAffected==0 as FAIL_INSUFFICIENT, or SELECT ... FOR UPDATE before the check.
  167 +- **复现核验**:Verified all quoted evidence in engine/.../event/EventEngine.java: L100 findByPk (plain SELECT), L105-106 Java-side current<qty check, L111 blind UPDATE `SET stock_col = stock_col - ? WHERE pk = ?` with no `AND stock_col >= qty` guard. Cross-checked supporting facts: (1) DynamicRepository.findByPk (L80-84) uses `SELECT * ... WHERE pk = ?` with NO `FOR UPDATE`, so the read takes no lock. (2) SchemaInitializer.sqlType (L102) maps int->BIGINT with no CHECK/UNSIGNED, so the DB accepts negative stock. (3) The path is genuinely concurrent and reachable: CommandController.execute (L31-37) calls CommandEngine.execute with no synchronization; execute is @Transactional and reaches deductStock via appl …
  168 +- **反驳核验**:Refutation failed on every angle; the finding is a genuine, reachable TOCTOU that I reproduced against the exact stack.
  169 +
  170 +Reachability: deductStock (EventEngine.java:85-119) runs synchronously in the request thread: CommandEngine.execute (@Transactional, L67) -> executeCommand -> applyCrossConsistency("OrderCreated",...) (L186) -> deductStock (L77). Spring MVC/Tomcat is multi-threaded and HikariCP gives each concurrent request its own transaction, so two concurrent POST /command for one product race.
  171 +
  172 +No guard at any layer: findByPk issues a lock-free plain SELECT (DynamicRepository.java:80-84); the Java check `if (current < qty)` (EventEngine.java:106) is check-then-act; the UPDATE `SET stoc …
  173 +
  174 +### A6. reload() mutates a shared non-thread-safe map in place while request threads read it unsynchronized
  175 +
  176 +- **严重度**:HIGH **维度**:eda **位置**:`engine/src/main/java/com/onto/engine/model/ModelRepository.java:54`
  177 +- **缺陷**:load()/reload() do layers.clear() then re-populate a plain LinkedHashMap under 'synchronized', but every reader (scene(), command(), aggregates(), etc.) accesses that same map with no synchronization, so a reload racing an in-flight execute() can observe a half-empty/corrupt model.
  178 +- **证据**:
  179 +```
  180 +L48: private final Map<String, Object> layers = new LinkedHashMap<>();
  181 +L50: @PostConstruct public synchronized void load() {
  182 +L54: layers.clear();
  183 +L54-62: loop re-puts each layer.
  184 +L67: public synchronized void reload() { load(); }
  185 +Readers are NOT synchronized, e.g. L108-110 aggregates(): return asList(asMap(layers.get("M1")).get("aggregates")); and scene()/command()/templateByScene() all read layers directly. 'synchronized' guards writers against each other but not against concurrent readers.
  186 +```
  187 +- **影响 / 触发场景**:Thread A is inside execute() calling models.scene()/command()/templateByScene() while an admin POSTs /api/meta/reload. Thread B enters synchronized load() and runs layers.clear(). Thread A now sees layers.get("M4") == null -> scene() returns empty map -> permitted() (empty permissionBind) returns true, flowSteps is empty, and execute() returns success=true having created no order; or SceneSchemaService throws 'no scene'. Concurrent clear()+put()/get() on a HashMap can also lose entries or, during resize, spin. Model reads mid-flight are not a consistent snapshot.
  188 +- **建议修复**:Build a new immutable map locally and publish it atomically to a volatile reference (this.layers = Map.copyOf(fresh)) instead of clear()+put on a shared map; readers then always see a complete prior or new snapshot.
  189 +- **复现核验**:Evidence verified in ModelRepository.java. `layers` (L48) is a plain LinkedHashMap. load()/reload() are `synchronized` (L50-51, L67) and do `layers.clear()` (L53, finding said L54 — trivial off-by-one) then re-put 11 layers in a disk-I/O + YAML-parse loop (L54-62). Every reader is unsynchronized: scenes()/scene() L222-232, command() L184-195, templateByScene() L262-268, aggregates() L108-110 all do bare `layers.get(...)` with no lock held. `synchronized` locks `this` and only mutually excludes writers; readers never acquire that monitor, so reader-vs-writer is an unguarded data race.\n\nReachability is real: MetaController.reload() is POST /api/meta/reload (L76-85) on one Tomcat thread; Comm …
  190 +- **反驳核验**:The finding is a genuine, reachable data race that my best refutation could not kill. ModelRepository.java:48 declares `private final Map<String,Object> layers = new LinkedHashMap<>()` — final pins only the reference; contents are mutated. `synchronized load()` (L51) does `layers.clear()` (L53) then re-puts 11 entries (L54-62); `reload()` (L67) calls it. Every reader (scenes() L222-224, scene() L226, command() L184, aggregates() L108, templateByScene() L262) reads `layers.get(...)` with no synchronization, so there is no happens-before edge from writer to readers under the JMM. Both paths are reachable and concurrent: reload is exposed at MetaController.java:76 (POST /api/meta/reload) and is …
  191 +
  192 +### A7. OrdersEventsPanel hardcodes order/customer business fields
  193 +
  194 +- **严重度**:HIGH **维度**:frontend **位置**:`web/src/panels.tsx:146`
  195 +- **缺陷**:The 'orders' table columns are hardcoded to order/customer-specific field names instead of being derived from any model layer.
  196 +- **证据**:
  197 +```
  198 +columns={[
  199 + { title: '订单号', dataIndex: 'orderId' },
  200 + { title: '客户', dataIndex: 'customerId' },
  201 + { title: '金额', dataIndex: 'totalAmount' },
  202 + { title: '币种', dataIndex: 'totalCurrency' },
  203 +]}
  204 +```
  205 +- **影响 / 触发场景**:orderId/customerId/totalAmount/totalCurrency are business identifiers baked into the generic renderer. Renaming any of these in M1/M3 YAML (or switching the demo to a non-order aggregate) leaves this list showing empty columns — the panel is not schema-driven, directly violating the iron rule that changing requirements means editing YAML only.
  206 +- **建议修复**:Drive the list columns from a schema (e.g. OrderAggregate root attributes from /api/meta or a dedicated list-schema endpoint) so keys/labels come from M1/M8, not string literals.
  207 +- **复现核验**:Evidence verified exactly at web/src/panels.tsx:146-151: OrdersEventsPanel's 'orders' table columns are hardcoded to `dataIndex: 'orderId' | 'customerId' | 'totalAmount' | 'totalCurrency'` (plus rowKey="orderId" at line 144). These are order/customer business identifiers baked into web/, which the iron rule requires to be a generic renderer with no hardcoded business fields. The task definition literally lists `customerId` and `totalAmount` as example violations.
  208 +
  209 +Reproduction chain confirmed against the actual data path: api.orders() (api.ts:27) -> GET /data/orders/list -> DataController.orders() (DataController.java:32) -> DomainReadService.listRoot("OrderAggregate"), whose toDomain() (Dom …
  210 +- **反驳核验**:Code confirmed at web/src/panels.tsx:146-151: a static antd `columns` literal hardcodes business field names as dataIndex — `{title:'订单号', dataIndex:'orderId'}`, `{title:'客户', dataIndex:'customerId'}`, `{title:'金额', dataIndex:'totalAmount'}`, `{title:'币种', dataIndex:'totalCurrency'}`. `customerId` and `totalAmount` are listed verbatim in the iron rule as prohibited identifiers, and the rule covers all of web/. Every refutation angle fails: (1) Reachable — OrdersEventsPanel is rendered as the '订单/事件' tab in App.tsx:100. (2) Not a fallback — it is a fixed column list with no schema derivation; antd Table maps dataIndex straight onto row keys with no guarding default. (3) Triggerable impact — d …
  211 +
  212 +### A8. Success message hardcodes 'orderId' key and '订单' wording
  213 +
  214 +- **严重度**:HIGH **维度**:frontend **位置**:`web/src/engine/SceneForm.tsx:96`
  215 +- **缺陷**:On submit success the generic form reads r.orderId and says '订单创建成功', but the backend returns the root id under a dynamic field name.
  216 +- **证据**:
  217 +```
  218 +message.success(`订单创建成功:${r.orderId}`)
  219 +```
  220 +- **影响 / 触发场景**:CommandEngine.execute returns the root id via out.put(rootIdentifierField, rootId) where rootIdentifierField = models.rootIdentifier(aggregateId) — 'orderId' only by coincidence for OrderAggregate. For any other scene the toast shows '订单创建成功:undefined'. The order-specific field name and '订单' wording are hardcoded in the generic renderer, violating the iron rule.
  221 +- **建议修复**:Read the id generically (from the aggregate's root identifier field, or have the engine return a fixed key like result.rootId) and use schema.sceneName instead of the literal '订单创建成功'.
  222 +- **复现核验**:Evidence verified verbatim. web/src/engine/SceneForm.tsx:96 reads `message.success(`订单创建成功:${r.orderId}`)`, and this file's own header (lines 29-33) declares it the "场景通用渲染引擎" (generic scene render engine) — the web/src/engine/ layer the iron rule requires to be a generic renderer. The backend does NOT return the root id under a fixed `orderId` key: CommandEngine.java:149 computes `String rootIdentifierField = models.rootIdentifier(aggregateId)` and line 192 does `out.put(rootIdentifierField, rootId)`. ModelRepository.java:125-127 shows rootIdentifier reads `aggregateRoot(aggregateId).get("identifier")` from M1. M1-domain.yaml:51 has `identifier: orderId` for OrderAggregate only; other aggre …
  223 +- **反驳核验**:I could not kill this finding; it is a genuine, confirmed iron-rule violation. web/src/engine/SceneForm.tsx:96 reads `message.success(`订单创建成功:${r.orderId}`)`, hardcoding both the order-specific field name `orderId` and the "订单" wording inside a file whose own docstring (lines 29-33) declares it the generic scene render engine ("场景通用渲染引擎"). The backend contract confirms the coupling is wrong: CommandEngine.java:149,192 returns the root id under a DYNAMIC key — `String rootIdentifierField = models.rootIdentifier(aggregateId); ... out.put(rootIdentifierField, rootId);`. ModelRepository.java:125-127 shows `rootIdentifier` returns the aggregate's `identifier` from M1; it equals "orderId" only for …
  224 +
  225 +### A9. customerLevel() is a dedicated customer-domain method inside the generic engine
  226 +
  227 +- **严重度**:HIGH **维度**:iron-rule **位置**:`engine/src/main/java/com/onto/engine/command/CommandEngine.java:276`
  228 +- **缺陷**:A whole private method hardcodes the CustomerAggregate/Customer names, the customerId reference field, and the customerLevel field (with fallback column cust_level) — pure order-domain business logic in a file that must be a generic interpreter.
  229 +- **证据**:
  230 +```
  231 +Lines 276-284: `Object cid = ctx.master.get("customerId");` / `String table = repo.tableName("CustomerAggregate", "Customer");` / `String pk = repo.pkColumn("CustomerAggregate", "Customer");` / `String levelCol = repo.fieldMap("CustomerAggregate", "Customer").getOrDefault("customerLevel", "cust_level");`
  232 +```
  233 +- **影响 / 触发场景**:The knowledge that 'the order references a customer, which has a level attribute used by a rule' is encoded in Java, not YAML. M1 already models this relationship (Order.customerId has refAggregate: CustomerAggregate, and Customer has attribute customerLevel). Renaming customerId/customerLevel/CustomerAggregate/Customer in the YAML — or pointing the rule at a different referenced aggregate — silently breaks the large-order risk rule. Even the physical column fallback 'cust_level' is hardcoded.
  234 +- **建议修复**:Generically resolve referenced-aggregate attribute values using the M1 refAggregate/refIdentifier metadata of the field the rule needs, so no aggregate/entity/field/column literal appears in Java.
  235 +- **复现核验**:VERIFIED verbatim at CommandEngine.java:276-284. The private method customerLevel(Ctx) hardcodes five distinct business identifiers that the iron rule says must live only in YAML:
  236 +- Line 277: `Object cid = ctx.master.get("customerId");` — the order's reference-field name.
  237 +- Line 279: `repo.tableName("CustomerAggregate", "Customer")` — aggregate id + root name.
  238 +- Line 280: `repo.pkColumn("CustomerAggregate", "Customer")`.
  239 +- Line 281: `repo.fieldMap("CustomerAggregate", "Customer").getOrDefault("customerLevel", "cust_level")` — the domain attribute name AND a hardcoded physical column fallback `cust_level`.
  240 +
  241 +This is genuine order/customer-domain business knowledge ("an order references a custo …
  242 +- **反驳核验**:Confirmed genuine iron-rule violation at engine/.../command/CommandEngine.java:276-284. The method customerLevel() hardcodes business identifiers as Java string literals: line 277 `ctx.master.get("customerId")`, lines 279-280 `repo.tableName("CustomerAggregate","Customer")` / `repo.pkColumn("CustomerAggregate","Customer")`, and line 281 `repo.fieldMap("CustomerAggregate","Customer").getOrDefault("customerLevel","cust_level")`. The iron rule text explicitly lists `CustomerAggregate` and `customerId` as example violations, so this is a direct match — and CommandEngine's own header (lines 19-20) plus CLAUDE.md declare this file must contain NO customer-specific code.
  243 +
  244 +Every refutation angle fai …
  245 +
  246 +### A10. productStock() is a dedicated product-domain method inside the generic engine
  247 +
  248 +- **严重度**:HIGH **维度**:iron-rule **位置**:`engine/src/main/java/com/onto/engine/command/CommandEngine.java:286`
  249 +- **缺陷**:A whole private method hardcodes ProductAggregate/Product names and the stockNum field (fallback column stock_num) to look up inventory — product-specific business logic baked into the generic executor.
  250 +- **证据**:
  251 +```
  252 +Lines 286-294: `String table = repo.tableName("ProductAggregate", "Product");` / `String pk = repo.pkColumn("ProductAggregate", "Product");` / `String stockCol = repo.fieldMap("ProductAggregate", "Product").getOrDefault("stockNum", "stock_num");`
  253 +```
  254 +- **影响 / 触发场景**:The generic engine 'knows' there is a Product aggregate with a stockNum attribute and reads it directly to power the low-stock rule. Renaming ProductAggregate/Product/stockNum in the YAML silently breaks the stock-warning rule; adding a second inventory-bearing aggregate is impossible without code. Even the fallback column 'stock_num' is a hardcoded M3 detail.
  255 +- **建议修复**:Resolve the referenced aggregate + attribute needed by the rule from M1 refAggregate metadata generically; drop the ProductAggregate/Product/stockNum literals.
  256 +- **复现核验**:Evidence verified verbatim at CommandEngine.java:286-294. The private method `productStock(Object productId)` hardcodes the product-domain aggregate id "ProductAggregate", root name "Product", domain field "stockNum", and a magic fallback M3 column "stock_num":
  257 + L288 `String table = repo.tableName("ProductAggregate", "Product");`
  258 + L289 `String pk = repo.pkColumn("ProductAggregate", "Product");`
  259 + L290 `String stockCol = repo.fieldMap("ProductAggregate", "Product").getOrDefault("stockNum", "stock_num");`
  260 +It is reachable production logic: runRules() L251-257 iterates OrderItem details, calls productStock(pid) L253, and feeds the result as the rule variable "stockNum" (also hardcoded, L255 `M …
  261 +- **反驳核验**:Confirmed by reading engine/.../command/CommandEngine.java:286-294. The method productStock(Object productId) contains literal hardcoded business identifiers: line 288 `repo.tableName("ProductAggregate", "Product")`, line 289 `repo.pkColumn("ProductAggregate", "Product")`, line 290 `repo.fieldMap("ProductAggregate", "Product").getOrDefault("stockNum", "stock_num")`. These are exactly the identifiers the iron rule enumerates as violations (aggregate `ProductAggregate`, field `stockNum`), plus a concrete physical column name `stock_num`. Every refutation angle fails: (1) they are literal strings, not model-resolved values, so renaming ProductAggregate/Product/stockNum in YAML silently breaks t …
  262 +
  263 +### A11. validate() infers phone validation by string-matching Chinese free text and hardcodes the entity/field
  264 +
  265 +- **严重度**:HIGH **维度**:iron-rule **位置**:`engine/src/main/java/com/onto/engine/command/CommandEngine.java:224`
  266 +- **缺陷**:Phone-format validation is triggered by searching the M2 validations free-text for the word '手机号', then applied to the literal DeliveryAddressEntity entity and receiverPhone field with an 11-digit regex.
  267 +- **证据**:
  268 +```
  269 +Line 30: `private static final Pattern PHONE = Pattern.compile("^\\d{11}$");` ; lines 225-231: `if (cmdValidations.contains("手机号")) { for (Map<String, Object> row : ctx.details.getOrDefault("DeliveryAddressEntity", List.of())) { Object p = row.get("receiverPhone"); if (p != null && !PHONE.matcher(String.valueOf(p)).matches()) errors.add("收货手机号格式不合法:" + p); } }`
  270 +```
  271 +- **影响 / 触发场景**:Three separate business bindings baked into supposedly-generic validation: (1) the field name receiverPhone, (2) the entity name DeliveryAddressEntity, (3) the very existence of the check keyed on a fuzzy substring match of the Chinese phrase '手机号' in the M2 validations prose. Renaming receiverPhone or DeliveryAddressEntity in the YAML, or rewording the M2 validation text, silently disables/misfires the check. Format constraints should be structured model data (e.g. an M1 field format/regex), not scraped from prose.
  272 +- **建议修复**:Add a structured format/regex constraint to M1 fields (or M2 structured validations) and validate generically by iterating model-declared constraints; remove the receiverPhone/DeliveryAddressEntity/'手机号' literals and the hardcoded 11-digit pattern.
  273 +- **复现核验**:Evidence verified verbatim in engine/src/main/java/com/onto/engine/command/CommandEngine.java. Line 30: `private static final Pattern PHONE = Pattern.compile("^\\d{11}$");`. Lines 224-231: `String cmdValidations = String.join(" ", stringList(models.command(ctx.cmdAggregate, ctx.cmdId).get("validations")));` then `if (cmdValidations.contains("手机号")) { for (Map<String,Object> row : ctx.details.getOrDefault("DeliveryAddressEntity", List.of())) { Object p = row.get("receiverPhone"); if (p != null && !PHONE.matcher(String.valueOf(p)).matches()) errors.add("收货手机号格式不合法:" + p); } }`.
  274 +
  275 +All three bindings are genuine business identifiers pulled from the single-source-of-truth YAML, not generic tokens: …
  276 +- **反驳核验**:Refutation failed on every angle. The code at CommandEngine.java:30 and 224-231 hardcodes three business bindings into the generic engine: the entity literal "DeliveryAddressEntity" (line 226), the field literal "receiverPhone" (line 227), and an 11-digit PHONE regex ^\d{11}$ (line 30), triggered by a substring scrape of Chinese prose "手机号" (line 225). The task's iron rule names receiverPhone and DeliveryAddressEntity verbatim as violations, and CLAUDE.md forbids "某个具体聚合的分支".\n\nReachability is confirmed, not a dead path: validate(ctx) is called from precheck() (line 52) and executeCommand() (line 119) on every command. M2-command.yaml:124 lists "收货手机号格式合法" in the CreateOrder validations, so …
  277 +
  278 +### A12. validate() hardcodes the dueDays field and PaymentTerm entity for a non-negative check
  279 +
  280 +- **严重度**:HIGH **维度**:iron-rule **位置**:`engine/src/main/java/com/onto/engine/command/CommandEngine.java:232`
  281 +- **缺陷**:A 'dueDays >= 0' check is triggered by substring-matching 'dueDays' in the M2 validations text, then applied to the literal PaymentTerm entity and dueDays field.
  282 +- **证据**:
  283 +```
  284 +Lines 232-237: `if (cmdValidations.contains("dueDays")) { for (Map<String, Object> row : ctx.details.getOrDefault("PaymentTerm", List.of())) { Object d = row.get("dueDays"); if (d != null && toBig(d).signum() < 0) errors.add("账期天数 dueDays 不可为负"); } }`
  285 +```
  286 +- **影响 / 触发场景**:The engine hardcodes the PaymentTerm entity name and dueDays field name, and gates the rule on the presence of the literal token 'dueDays' inside M2 prose. Renaming dueDays or PaymentTerm in the YAML silently drops the validation. A numeric non-negativity constraint should be declared in the model, not inferred from free text plus hardcoded field names.
  287 +- **建议修复**:Express min/range constraints structurally in M1/M2 and enforce generically over model-declared numeric constraints; remove the PaymentTerm/dueDays literals.
  288 +- **复现核验**:Verified verbatim at engine/src/main/java/com/onto/engine/command/CommandEngine.java:232-237: `if (cmdValidations.contains("dueDays")) { for (Map<String,Object> row : ctx.details.getOrDefault("PaymentTerm", List.of())) { Object d = row.get("dueDays"); if (d != null && toBig(d).signum() < 0) errors.add("账期天数 dueDays 不可为负"); } }`. This hardcodes the order-domain business entity name "PaymentTerm" and business field name "dueDays" inside the generic engine, and gates the check on a substring match against M2 free-text prose (models/M2-command.yaml:123: "dueDays >= 0, depositRatio ∈ [0,1]"). Both identifiers are legitimate business model names (M1-domain.yaml:88,93; M3-deploy.yaml:71,77; M8-fron …
  289 +- **反驳核验**:The finding is confirmed; my refutation fails on every angle. CommandEngine.java:232-237 hardcodes three business identifiers inside the supposedly-generic engine: the entity name "PaymentTerm" (getOrDefault("PaymentTerm", List.of())), the field name "dueDays" (row.get("dueDays")), and gates the whole check on cmdValidations.contains("dueDays") — a substring match against M2 free-text prose. The iron rule in the task brief lists BOTH "dueDays" and "PaymentTerm" as canonical examples of violating identifiers, so these are not acceptable generic fallbacks.\n\nReachability is proven, not speculative: validate() is invoked from precheck() (line 52) and executeCommand() (line 119); the gate fires …
  290 +
  291 +### A13. Cross-consistency trigger event and item field names are hardcoded
  292 +
  293 +- **严重度**:HIGH **维度**:iron-rule **位置**:`engine/src/main/java/com/onto/engine/command/CommandEngine.java:181`
  294 +- **缺陷**:The engine triggers cross-aggregate consistency with the literal event name 'OrderCreated' and builds item maps with literal productId/quantity field names.
  295 +- **证据**:
  296 +```
  297 +Lines 182-186: `for (Map<String, Object> it : ctx.details.getOrDefault(firstItemEntity(aggregateId), List.of())) { items.add(Map.of("productId", it.get("productId"), "quantity", it.getOrDefault("quantity", 0))); } List<Map<String, Object>> stockActions = eventEngine.applyCrossConsistency("OrderCreated", Map.of("items", items));`
  298 +```
  299 +- **影响 / 触发场景**:Although events are emitted generically from cmd.emitEvents just above (lines 173-177), the cross-consistency call passes the hardcoded literal 'OrderCreated', plus hardcoded item field names productId/quantity. Rename OrderCreated in ME/M2 (or the OrderItem fields) and the down-stream stock deduction silently stops, even though ME.crossAggConsistencyRules still lists the rule. The trigger event(s) should be taken from the emitted events, and item field names from M1.
  300 +- **建议修复**:Iterate the events actually emitted (from M2 emitEvents/ME) and invoke consistency per matching ME rule; source line-item field names from M1 attributes rather than the productId/quantity/'OrderCreated' literals.
  301 +- **复现核验**:Evidence verified verbatim at CommandEngine.java:180-186. Line 186 passes the literal event name "OrderCreated" to eventEngine.applyCrossConsistency, and line 183 builds items using literal business field names "productId"/"quantity" read from the domain row (it.get("productId"), it.getOrDefault("quantity",0)). These are all business identifiers declared in the YAML models, not generic engine concepts: ME-event.yaml declares OrderAggregate.OrderCreated and crossAggConsistencyRules[triggerEvent: OrderCreated]; M1-domain.yaml:76,82 declares OrderItem fields productId and quantity; M2-command.yaml:127 declares CreateOrder emitEvents: OrderCreated. The iron rule EXPLICITLY names "OrderCreated" a …
  302 +- **反驳核验**:Confirmed verbatim at engine/.../command/CommandEngine.java:186 `eventEngine.applyCrossConsistency("OrderCreated", Map.of("items", items))` and :183 `items.add(Map.of("productId", it.get("productId"), "quantity", it.getOrDefault("quantity", 0)))`. This is a textbook iron-rule violation — the contract explicitly names "event name like 'OrderCreated'" and business field names as prohibited hardcodings inside engine/.
  303 +
  304 +Every refutation fails: (1) Not a fallback — "OrderCreated" is the only literal passed; in EventEngine.applyCrossConsistency (lines 66-70) it is matched exactly against rule.get("triggerEvent") from ME.crossAggConsistencyRules (ME-event.yaml:59 `triggerEvent: OrderCreated`), and …
  305 +
  306 +### A14. 跨聚合一致性触发事件名被硬编码为 OrderCreated,ME 的 OrderCancelled 规则永不触发
  307 +
  308 +- **严重度**:HIGH **维度**:model-integrity **位置**:`engine/src/main/java/com/onto/engine/command/CommandEngine.java:186`
  309 +- **缺陷**:CommandEngine 用字面量 "OrderCreated" 调用 applyCrossConsistency,而不是遍历本次实际发出的事件,导致 ME.crossAggConsistencyRules 中 triggerEvent: OrderCancelled → StockDeduct 这条规则被引擎完全忽略。
  310 +- **证据**:
  311 +```
  312 +CommandEngine.java:186 `eventEngine.applyCrossConsistency("OrderCreated", Map.of("items", items));`;ME-event.yaml:58-66 定义了两条规则,其中 63-66 `- triggerEvent: OrderCancelled / targetAggregate: ProductAggregate / targetCommand: StockDeduct`。
  313 +```
  314 +- **影响 / 触发场景**:SCENE_CANCEL_ORDER 执行 CancelOrder 时发出 OrderCancelled,但引擎只会用硬编码的 "OrderCreated" 去匹配跨聚合规则,OrderCancelled 那条一致性规则永远不会执行。违背『改 ME 增删跨聚合规则即改行为』的承诺;同时把业务事件名硬编码进通用引擎,违反铁律。
  315 +- **建议修复**:改为对本命令 emitEvents 中每个事件调用 applyCrossConsistency,由 ME.crossAggConsistencyRules 的 triggerEvent 自行匹配,而非硬编码 "OrderCreated"。
  316 +- **复现核验**:Evidence verified at CommandEngine.java:186 — the literal `eventEngine.applyCrossConsistency("OrderCreated", Map.of("items", items));` (and the matching hardcoded comment at line 180). executeCommand() is a single generic method invoked for every bindCommand step (lines 92-102), used identically for CreateOrder, ModifyOrderDeliveryAddress, and CancelOrder — the "OrderCreated" literal is applied unconditionally, never derived from the command being run.\n\nConcrete manifestation: executing SCENE_CANCEL_ORDER (M4-scene.yaml:32-40) runs executeCommand for CancelOrder (M2-command.yaml:160-169), which emits OrderCancelled. The engine even computes the real emitted events into `events` (lines 173- …
  317 +- **反驳核验**:CommandEngine.java:186 does literally hardcode the business event name: `eventEngine.applyCrossConsistency("OrderCreated", Map.of("items", items));` (comment line 180 confirms it is order-specific, not a generic fallback). By the project's iron-rule definition this is a genuine, reachable hardcoded business event name inside the generic engine, so I cannot honestly refute the finding to real=false. HOWEVER, the finding's claimed HIGH-severity functional impact — that the ME OrderCancelled→StockDeduct rule being skipped breaks cancel-order stock consistency — does not hold in any reachable scenario: (1) the `items` list (lines 181-184) is built from OrderItem details in the payload, but Cance …
  318 +
  319 +### A15. Client-controlled totalAmount is stored as-is and drives risk rules — total tampering + MetaRule bypass
  320 +
  321 +- **严重度**:HIGH **维度**:security **位置**:`engine/src/main/java/com/onto/engine/command/CommandEngine.java:246`
  322 +- **缺陷**:The order total is taken directly from the caller's master payload, never recomputed from line items, and that same untrusted value is fed to the REJECT/ALERT risk rules — so a caller can both understate the total and evade amount-based controls.
  323 +- **证据**:
  324 +```
  325 +orderVars.put("totalAmount", ctx.master.get("totalAmount")); // L246, untrusted payload value drives runRules; validate() (L205-239) never checks that item amounts sum to totalAmount
  326 +```
  327 +- **影响 / 触发场景**:insertDomain writes master.totalAmount straight into total_amt (it is a fieldMap column), and runRules evaluates MetaRule thresholds (e.g. high-amount REJECT) against the same client value. Attack: submit items worth 100000 but master.totalAmount=1 — the high-amount risk rule never fires and the order persists with total_amt=1. M2 lists validation '订单明细金额累加 ≈ 订单总金额' but it is not implemented, so the integrity/pricing control is absent.
  328 +- **建议修复**:Compute totalAmount server-side from persisted line items (price*qty) and use that computed value for both storage and rule evaluation; reject payloads whose declared total diverges from the computed sum.
  329 +- **复现核验**:Verified every link of the claim against the actual files. CommandEngine.java:246 `orderVars.put("totalAmount", ctx.master.get("totalAmount"))` takes totalAmount straight from the client payload (ctx.master is built from payload.get("master") at L322-323). That same map is fed to runRules (called on the real execute path at L130, and precheck L53). MetaRule-business.yaml:27-32 rule R_ORDER_AMOUNT_LIMIT gates a REJECT on `totalAmount > #{single_order_max_amount} && customerLevel != "VIP"` (threshold 50000) — so the untrusted value is the risk gate. The value is also persisted verbatim: M3-deploy.yaml:60 maps `totalAmount: total_amt`, and DynamicRepository.insertDomain (L46-69) writes any fiel …
  330 +- **反驳核验**:Every technical claim is verified against source, so the finding cannot be killed. CommandEngine.java:246 (orderVars.put("totalAmount", ctx.master.get("totalAmount"))) takes the total straight from the client payload.master (Ctx L322-323), which arrives unfiltered from POST /api/scene/{sceneId}/execute (CommandController.java:32-37, principal merely defaults to normal_user with no auth binding). That untrusted value both (a) drives the REJECT control R_ORDER_AMOUNT_LIMIT in MetaRule-business.yaml:30 ('totalAmount > #{single_order_max_amount} && customerLevel != "VIP"'), so understating it evades the 50000 cap, and (b) is persisted as-is: M3-deploy.yaml:60 maps totalAmount->total_amt and Dyna …
  331 +
  332 +### A16. TracePanel hardcodes StockDeduct / product-stock business fields
  333 +
  334 +- **严重度**:MEDIUM **维度**:frontend **位置**:`web/src/panels.tsx:47`
  335 +- **缺陷**:The stock-deduction visualization hardcodes the event name 'StockDeduct' and product/stock field names as table columns.
  336 +- **证据**:
  337 +```
  338 +<Text strong>跨聚合最终一致:StockDeduct 扣减库存</Text>
  339 +...
  340 +columns={[
  341 + { title: '商品', dataIndex: 'productId' },
  342 + { title: '扣减', dataIndex: 'deductQty' },
  343 + { title: '剩余', dataIndex: 'remainingStock' },
  344 +```
  345 +- **影响 / 触发场景**:'StockDeduct', 'productId', 'deductQty', 'remainingStock' are business/event identifiers embedded in the frontend. A YAML change to ME event names or product field names would not reflect here, and the panel is meaningless for any non-order domain — an iron-rule violation in the render engine.
  346 +- **建议修复**:Render cross-consistency actions generically from the engine's returned action shape (label the section from ME event metadata, render whatever keys the rows carry) rather than fixed column literals.
  347 +- **复现核验**:Evidence confirmed verbatim in web/src/panels.tsx (TracePanel). Line 47: `<Text strong>跨聚合最终一致:StockDeduct 扣减库存</Text>` hardcodes the ME event/command name "StockDeduct" plus Chinese product-stock business copy. Lines 51-53 hardcode three product/inventory field names as fixed table columns: `{ dataIndex: 'productId' }`, `{ dataIndex: 'deductQty' }`, `{ dataIndex: 'remainingStock' }`. These are business identifiers embedded in web/ — exactly the class the iron rule forbids (the contract explicitly lists "StockDeduct" and "stockNum"/product fields as example violations).
  348 +
  349 +Cross-checked against the backend: engine/src/main/java/com/onto/engine/event/EventEngine.java builds the stockActions map …
  350 +- **反驳核验**:Confirmed real by reading web/src/panels.tsx:45-56. The block literally hardcodes the business event name and product/inventory field names: line 47 `跨聚合最终一致:StockDeduct 扣减库存`, and columns dataIndex `productId` (51), `deductQty` (52), `remainingStock` (53). These are order/product-domain identifiers, verified as domain concepts in the YAML single source of truth: models/ME-event.yaml:38,61,65 (StockDeduct / ProductStockDeducted / productId,stockNum), models/M2-command.yaml:88-98 (cmdId StockDeduct, productId, deductQty), models/M1-domain.yaml:27,76 (identifier productId). Editing those YAML names would not reflect here, which is precisely the iron-rule failure mode the contract forbids for w …
  351 +
  352 +### A17. TracePanel result summary hardcodes orderId
  353 +
  354 +- **严重度**:MEDIUM **维度**:frontend **位置**:`web/src/panels.tsx:34`
  355 +- **缺陷**:The success Descriptions item is hardcoded to label '订单号 orderId' and read result.orderId.
  356 +- **证据**:
  357 +```
  358 +<Descriptions.Item label="订单号 orderId">{result.orderId}</Descriptions.Item>
  359 +```
  360 +- **影响 / 触发场景**:Same dynamic-root-id problem as the submit toast: result.orderId is only populated when the root identifier field happens to be 'orderId'. For any other aggregate this shows blank, and '订单号' is order-specific hardcoding in a supposedly generic trace panel.
  361 +- **建议修复**:Surface the root id under a stable generic key from the engine (e.g. result.rootId) and take the label from the aggregate's root-identifier metadata.
  362 +- **复现核验**:Evidence verified verbatim at web/src/panels.tsx:34: `<Descriptions.Item label="订单号 orderId">{result.orderId}</Descriptions.Item>`. The impact is real: the backend writes the root id under a DYNAMIC key, not a fixed "orderId". CommandEngine.java:148-149 computes `rootIdentifierField = models.rootIdentifier(aggregateId)` and line 192 does `out.put(rootIdentifierField, rootId)`. ModelRepository.rootIdentifier (lines 125-127) returns `aggregateRoot(...).get("identifier")` — read straight from the M1 YAML, so it is "orderId" only for the order aggregate. App.tsx passes the full execute result unchanged into TracePanel (line 37 setResult(r); line 98 <TracePanel result={result}/>). Concrete manife …
  363 +- **反驳核验**:I tried to kill this and the core survives. The mechanism is verified: CommandEngine.java:192 does `out.put(rootIdentifierField, rootId)` where `rootIdentifierField = models.rootIdentifier(aggregateId)` (ModelRepository.java:125-127 returns aggregateRoot.identifier). So the backend returns the root id under a DYNAMIC key — literally "orderId" for OrderAggregate, but a different key for any other aggregate. panels.tsx:34 hardcodes `result.orderId` and the label "订单号 orderId", so for a non-order aggregate this row would be blank and mislabeled. That is genuine order-specific hardcoding sitting in web/, exactly the pattern the iron rule names as a violation, and it is reachable via the project' …
  364 +
  365 +### A18. Default sceneId hardcoded to SCENE_CREATE_ORDER
  366 +
  367 +- **严重度**:MEDIUM **维度**:frontend **位置**:`web/src/App.tsx:15`
  368 +- **缺陷**:The initial selected scene is a hardcoded order-specific scene id rather than derived from the scenes list.
  369 +- **证据**:
  370 +```
  371 +const [sceneId, setSceneId] = useState<string>('SCENE_CREATE_ORDER')
  372 +```
  373 +- **影响 / 触发场景**:If the scene is renamed in M4 YAML, the first schema fetch 404s and the form fails to load until the user manually clicks another scene. The business scene id is hardcoded in the frontend, so a YAML rename is not transparently reflected — contrary to the iron rule.
  374 +- **建议修复**:Initialize sceneId to null and, after api.scenes() resolves, default to the first scene where hasTemplate is true.
  375 +- **复现核验**:Evidence verified exactly at web/src/App.tsx:14: `const [sceneId, setSceneId] = useState<string>('SCENE_CREATE_ORDER')`. Grep over web/src confirms this is the ONLY place in the frontend source where the business scene id appears; all other occurrences are in models/*.yaml (M4:10, M8:76, M5:16, M7:17, MetaRule:25) and backend tests. The default is never derived from the fetched scenes list — the useEffect on lines 23-27 fires on mount with the hardcoded sceneId and calls api.sceneSchema('SCENE_CREATE_ORDER') → GET /api/meta/scene/SCENE_CREATE_ORDER (api.ts:22, which throws on non-2xx at api.ts:6). Concrete manifestation: renaming SCENE_CREATE_ORDER in M4-scene.yaml plus its bindings (a legit …
  376 +- **反驳核验**:Confirmed against source. web/src/App.tsx:14 declares `const [sceneId, setSceneId] = useState<string>('SCENE_CREATE_ORDER')` — a literal, order-specific scene id. grep confirms SCENE_CREATE_ORDER is a real M4 business scene (models/M4-scene.yaml:10, also referenced in M5/M7/M8/MetaRule), so it is unambiguously a business identifier living inside web/, which the repo's iron rule explicitly names as a violation. Reachability: App.tsx:23-27 fires on first render because sceneId is truthy, calling api.sceneSchema('SCENE_CREATE_ORDER'); api.ts:5-7 throws on non-2xx, so a rename in M4 YAML makes the first schema fetch fail, App.tsx:26 shows an error toast, and the content card drops to the empty b …
  377 +
  378 +### A19. DataController hardcodes an order-specific list endpoint and aggregate name
  379 +
  380 +- **严重度**:MEDIUM **维度**:iron-rule **位置**:`engine/src/main/java/com/onto/engine/web/DataController.java:31`
  381 +- **缺陷**:A dedicated /orders/list endpoint hardcodes the aggregate id 'OrderAggregate', unlike the generic /data/{aggregateId} options endpoint next to it.
  382 +- **证据**:
  383 +```
  384 +Lines 30-33: `@GetMapping("/orders/list") public List<Map<String, Object>> orders() { return readService.listRoot("OrderAggregate"); }`
  385 +```
  386 +- **影响 / 触发场景**:A business-named route ('orders') and a hardcoded aggregate literal in a controller that is otherwise generic (the sibling method takes {aggregateId}). Renaming OrderAggregate in M1 breaks this endpoint; listing any other aggregate's roots requires new Java. Should be a generic parameterized route like /data/{aggregateId}/list.
  387 +- **建议修复**:Replace with a generic `@GetMapping("/{aggregateId}/list")` that forwards the path variable to listRoot, removing the OrderAggregate literal and the 'orders' path.
  388 +- **复现核验**:Verified evidence at engine/src/main/java/com/onto/engine/web/DataController.java:30-33 exactly matches the quote: `@GetMapping("/orders/list")` and `return readService.listRoot("OrderAggregate");`. This is an unambiguous iron-rule violation: "OrderAggregate" is a hardcoded business aggregate identifier (the exact forbidden category), and grep confirms it is a real aggregate declared across 7 YAML models (M1/M2/M3/M4/M7/M8/ME) while this is the sole .java occurrence. The violation is genuine and not merely stylistic because the downstream DomainReadService.listRoot(String aggregateId) (line 27) is fully parameterized/generic, and the sibling endpoint /{aggregateId} (line 24-27) is genuinely …
  389 +- **反驳核验**:Refutation attempt fails on every angle. DataController.java:30-33 literally contains `@GetMapping("/orders/list")` and `readService.listRoot("OrderAggregate")`. (1) "OrderAggregate" is a real business aggregate id, confirmed present in models/M1,M2,M3,M4,M7,M8 as one of three business aggregates (CustomerAggregate/ProductAggregate/OrderAggregate) — the iron rule explicitly names hardcoded aggregate names as a violation. (2) The path is live, not dead: web/src/api.ts:27 (`orders: () => get('/data/orders/list')`) is called from panels.tsx:138 and shown in the "订单/事件" tab (App.tsx:100). (3) No guarding genericity — the opposite: DomainReadService.listRoot(String aggregateId) is generic and the …
  390 +
  391 +### A20. api.ts hardcodes the order-specific data URL, contradicting its own contract
  392 +
  393 +- **严重度**:MEDIUM **维度**:iron-rule **位置**:`web/src/api.ts:27`
  394 +- **缺陷**:The API client hardcodes '/data/orders/list', despite the file's header comment asserting the frontend never hardcodes business addresses.
  395 +- **证据**:
  396 +```
  397 +Line 1: `// ...所有 URL 均由后端场景 Schema 给出(commandApi/optionsSource),前端不硬编码业务地址。` ; line 27: `orders: () => get('/data/orders/list'),`
  398 +```
  399 +- **影响 / 触发场景**:A business-specific URL baked into the generic renderer's API layer, directly violating the documented promise. Renaming the Order aggregate/endpoint requires editing this TS file. (Pairs with the DataController hardcode.)
  400 +- **建议修复**:Expose a generic list call `list: (aggregateId) => get(`/data/${aggregateId}/list`)` and have panels pass the aggregate id resolved from the scene schema.
  401 +- **复现核验**:Evidence verified verbatim. web/src/api.ts:1 asserts `所有 URL 均由后端场景 Schema 给出...前端不硬编码业务地址` (frontend does not hardcode business addresses), yet api.ts:27 `orders: () => get('/data/orders/list')` bakes the order-specific business word `orders` into a URL. The generic sibling on line 26 (`options: (aggregateId) => get('/data/'+aggregateId)`, parameterized by aggregate) proves a generic route pattern exists, so `orders/list` is a genuine special-case, not a necessary constant. It pairs with DataController.java:30-32, which hardcodes `@GetMapping("/orders/list")` → `readService.listRoot("OrderAggregate")` (business-literal in engine too). Concrete manifestation: per the iron rule, changing requ …
  402 +- **反驳核验**:The hardcode is verifiably present and reachable, so it cannot be killed as a false positive. web/src/api.ts:27 reads `orders: () => get('/data/orders/list')` — an order-specific path baked into the frontend. It is live, not dead code: panels.tsx:138 (`api.orders().then(setOrders)`) inside OrdersEventsPanel is mounted as a real tab at App.tsx:100 (key 'orders'). Renaming the order aggregate/endpoint would force editing this TS line plus DataController.java:30-33 (`/orders/list` -> listRoot("OrderAggregate")), so the frontend-to-order coupling the iron rule forbids genuinely exists. My best refutations only partially hold: (a) the line-1 comment's parenthetical does scope "所有 URL" to commandA …
  403 +
  404 +### A21. panels.tsx hardcodes Order/Product business field names as table columns
  405 +
  406 +- **严重度**:MEDIUM **维度**:iron-rule **位置**:`web/src/panels.tsx:143`
  407 +- **缺陷**:The results/inspection panels hardcode order and stock-action field names (orderId, customerId, totalAmount, totalCurrency, productId, deductQty, remainingStock) instead of deriving columns from the model.
  408 +- **证据**:
  409 +```
  410 +Lines 144-151: rowKey="orderId" with columns `{ title: '订单号', dataIndex: 'orderId' }, { title: '客户', dataIndex: 'customerId' }, { title: '金额', dataIndex: 'totalAmount' }, { title: '币种', dataIndex: 'totalCurrency' }`; also lines 34 `label="订单号 orderId">{result.orderId}` and 51-54 `dataIndex: 'productId' / 'deductQty' / 'remainingStock'`.
  411 +```
  412 +- **影响 / 触发场景**:The 'generic renderer' hardwires the Order aggregate root's field names and the stock-deduct action shape into React. Rename any of these in M1/M3, or drive the panel from a different aggregate, and the columns silently render empty. Column definitions for a root list should come from the model (M1 attributes / M8), like the main SceneForm does.
  413 +- **建议修复**:Build the orders/root table columns from the scene schema's aggregate attributes (M1) rather than literal dataIndex strings; render the created-id using the schema's rootIdentifier.
  414 +- **复现核验**:Evidence verified verbatim in web/src/panels.tsx. Line 34: `label="订单号 orderId">{result.orderId}`. Lines 144-150 (OrdersEventsPanel): rowKey="orderId" plus columns dataIndex 'orderId','customerId','totalAmount','totalCurrency'. Lines 51-53 (TracePanel stockActions table): dataIndex 'productId','deductQty','remainingStock'. These are Order-aggregate M1 field names and the StockDeduct action shape hardcoded into React.
  415 +
  416 +The data genuinely flows under these keys today, so the columns are live, not dead: DomainReadService.listRoot (lines 27-37) reverse-maps physical columns to M1 domain field names, so /data/orders/list yields rows keyed orderId/customerId/totalAmount/totalCurrency; EventEngine. …
  417 +- **反驳核验**:Verified in web/src/panels.tsx: line 34 renders `result.orderId` under label "订单号 orderId"; lines 51-53 hardcode stock-action columns `dataIndex: 'productId' / 'deductQty' / 'remainingStock'`; lines 147-150 hardcode order columns `dataIndex: 'orderId' / 'customerId' / 'totalAmount' / 'totalCurrency'` with rowKey="orderId" (line 144). These are exactly the business field names the iron rule names as violations ("customerId"/"totalAmount"), sitting inside web/. My best refutations all fail: (1) the "demo panel, not generic renderer" defense is void because the iron rule covers all of web/ with no carve-out, and the adjacent ProvenancePanel (line 83) even claims "本页面无任何业务硬编码" while OrdersEvents …
  418 +
  419 +### A22. SceneForm hardcodes the root-id field name and order-specific UI copy
  420 +
  421 +- **严重度**:MEDIUM **维度**:iron-rule **位置**:`web/src/engine/SceneForm.tsx:96`
  422 +- **缺陷**:The generic form reads the created id as r.orderId and labels its actions '新增订单', assuming the scene is always order creation.
  423 +- **证据**:
  424 +```
  425 +Line 96: `message.success(`订单创建成功:${r.orderId}`)` ; line 147: `<Button type="primary" loading={submitting} onClick={doSubmit}>提交「新增订单」</Button>`
  426 +```
  427 +- **影响 / 触发场景**:The backend returns the created id under a *dynamic* key (models.rootIdentifier(aggregateId), i.e. it would be customerId for a customer scene), so hardcoding `r.orderId` shows 'undefined' for any non-order scene. The button/message copy '新增订单' likewise ignores schema.sceneName. Renaming orderId in M1, or reusing the renderer for another aggregate/scene, breaks the success feedback despite the renderer being 'generic'.
  428 +- **建议修复**:Read the created id via the schema's rootIdentifier (or have the backend return it under a stable generic key like `rootId`), and derive button/message text from schema.sceneName/template.
  429 +- **复现核验**:Evidence verified verbatim. web/src/engine/SceneForm.tsx:96 reads the created id as `r.orderId` — `message.success(`订单创建成功:${r.orderId}`)` — and line 147 hardcodes order copy — `提交「新增订单」`. This component's own docstring (lines 29-33) declares it a "场景通用渲染引擎" ("完全由 M8 模板结构驱动…改模板即改页面"), i.e. it is supposed to be the generic renderer, so hardcoding the business field name `orderId` and the order-specific label 新增订单 is exactly the class of iron-rule violation the contract forbids in web/ (it enumerates field names like customerId as violations; orderId is the same category).
  430 +
  431 +The impact is confirmed against the backend: CommandEngine.executeCommand returns the created id under a DYNAMIC key, not …
  432 +- **反驳核验**:Finding survives refutation. Verified in web/src/engine/SceneForm.tsx:96 `message.success(`订单创建成功:${r.orderId}`)` and line 147 button copy `提交「新增订单」`, both inside the file CLAUDE.md designates as the ★ generic renderer. The backend confirms the coupling: CommandEngine.java:149 `String rootIdentifierField = models.rootIdentifier(aggregateId)` and line 192 `out.put(rootIdentifierField, rootId)` return the created id under a DYNAMIC key — `orderId` only for the order aggregate, `customerId` for a customer scene. So `r.orderId` is hard-bound to the order aggregate; any other aggregate yields `undefined`. types.ts:32 shows the schema already exposes a generic `sceneName` that the button/message i …
  433 +
  434 +### A23. MetaRule 可用变量被硬编码白名单限定,引用其他字段的规则被静默跳过
  435 +
  436 +- **严重度**:MEDIUM **维度**:model-integrity **位置**:`engine/src/main/java/com/onto/engine/command/CommandEngine.java:246`
  437 +- **缺陷**:runRules 只向规则引擎注入 totalAmount / customerLevel / stockNum 三个硬编码变量;RuleEngine 对『变量未在 env 中出现』的规则直接 continue,因此任何在 MetaRule 中新增、引用其他领域字段的规则都会被静默忽略。
  438 +- **证据**:
  439 +```
  440 +CommandEngine.java:246-247 `orderVars.put("totalAmount", ...)` `orderVars.put("customerLevel", level)`,第 255 行 `ruleEngine.evaluate(ctx.sceneId, Map.of("stockNum", stock))`;RuleEngine.java:50 `if (!env.keySet().containsAll(vars)) { continue; }`。MetaRule-business.yaml:17 定义的 vip_discount_limit 参数从未被任何 matchCondition 引用。
  441 +```
  442 +- **影响 / 触发场景**:README/HelpButton 宣称『改 MetaRule 表达式即改前后端行为』,但若在 MetaRule 增一条如 `dueDays > 60` 的规则,dueDays 从未注入 env,规则永远命中不了、也不报错——只有恰好用到这三个变量的规则才生效。这是『改 YAML→改行为』未被通用兑现,且 vip_discount_limit 为悬空死参数。
  443 +- **建议修复**:根据 payload(master+details) 通用地把所有领域字段注入规则 env(或至少注入规则 matchCondition 实际引用的变量),而非固定注入三个字段。
  444 +- **复现核验**:All quoted evidence verified in the actual files. CommandEngine.runRules (lines 241-260) injects rule variables at exactly two call sites: line 248 evaluates with orderVars={totalAmount, customerLevel} and line 255 evaluates with Map.of("stockNum", stock). These are the ONLY places variables reach RuleEngine.evaluate. RuleEngine.java:50 does `if (!env.keySet().containsAll(vars)) { continue; }` with no log at that continue, so any rule whose referenced variable is absent from env is silently skipped (env also holds the three global params single_order_max_amount/stock_warn_min/vip_discount_limit, but no business field beyond the hardcoded trio). Concrete manifestation: add a rule to ORDER_RIS …
  445 +- **反驳核验**:Core mechanism confirmed by reading the files. CommandEngine.runRules (lines 241-260) injects only hardcoded business variables into the rule env: totalAmount + customerLevel (lines 246-247, order-level) and stockNum per OrderItem (line 255); the extraction is itself order-specific (ctx.details.getOrDefault("OrderItem"...), it.get("productId"), ctx.master.get("totalAmount")). RuleEngine.evaluate line 50 `if (!env.keySet().containsAll(vars)) { continue; }` skips with NO log/error. So a MetaRule referencing any other modeled field is silently no-op'd. This is reachable and in-scope for THIS demo: dueDays is a real domain field (PaymentTerm, validated at CommandEngine:232-236) yet is never inje …
  446 +
  447 +### A24. 通用 executeCommand 一律按『新建聚合根』落库,修改/取消类命令被误执行为插入
  448 +
  449 +- **严重度**:MEDIUM **维度**:model-integrity **位置**:`engine/src/main/java/com/onto/engine/command/CommandEngine.java:155`
  450 +- **缺陷**:executeCommand 无视 M2 命令语义(bizSteps),对任何 bindCommand 步骤都生成新 rootId 并 insertDomain 主记录+子实体,因此 SCENE_MODIFY_ORDER_ADDR / SCENE_CANCEL_ORDER 会插入一条全新订单而非修改/取消既有订单。
  451 +- **证据**:
  452 +```
  453 +CommandEngine.java:148 `String rootId = IdGen.next(idPrefix(aggregateId));` 154 `master.putIfAbsent("createTime", ...)` 155 `repo.insertDomain(aggregateId, rootName, null, master, null);`(无 UPDATE 分支);M2-command.yaml ModifyOrderDeliveryAddress(132) 与 CancelOrder(160) 的 bizSteps 表达的是修改/作废语义;M4 场景 SCENE_MODIFY_ORDER_ADDR(22) / SCENE_CANCEL_ORDER(32) 都走 bindCommand。
  454 +```
  455 +- **影响 / 触发场景**:只要在 M4 定义一个非新增语义的场景并绑定命令,引擎就会错误地新建一条聚合根记录(ModifyOrderDeliveryAddress 会造出一条只有 orderId/createTime 的空订单,CancelOrder 亦然),并照常发事件、扣库存。引擎并未通用解释 M2 命令语义——与『通用执行任意场景命令』的承诺不符。
  456 +- **建议修复**:依据 M2 命令是否包含聚合根标识入参 / bizSteps 判定 create vs update/cancel,分别实现 insert 与 update,或在 M2/M0 引入命令类型字段供引擎分派。
  457 +- **复现核验**:Evidence verified against source. In CommandEngine.java, execute() (lines 82-106) dispatches every flowStep of stepType "bindCommand" to executeCommand(), which unconditionally does: line 148 `String rootId = IdGen.next(idPrefix(aggregateId));`, line 153 `master.put(rootIdentifierField, rootId)`, line 154 `master.putIfAbsent("createTime", ...)`, line 155 `repo.insertDomain(aggregateId, rootName, null, master, null)`. There is no UPDATE/cancel branch anywhere; the command's semantics (desc/bizSteps) are never consulted — only its `validations` text is scanned for the literals "手机号"/"dueDays" (lines 224-237). So the engine treats ALL commands as "create new aggregate root + insert".
  458 +
  459 +Concrete, …
  460 +- **反驳核验**:The finding survives every refutation attempt. CommandEngine.executeCommand (engine/.../command/CommandEngine.java:113-201) has a single, unconditional code path: line 148 `String rootId = IdGen.next(idPrefix(aggregateId))` generates a fresh root id, line 153 `master.put(rootIdentifierField, rootId)` overwrites any caller-supplied orderId, line 154 adds createTime, and line 155 `repo.insertDomain(aggregateId, rootName, null, master, null)` inserts a NEW aggregate-root row (plus children). There is no UPDATE/soft-delete branch and no reading of M2 command semantics (bizSteps/desc). So ModifyOrderDeliveryAddress and CancelOrder are executed as inserts.
  461 +
  462 +Reachability confirmed end-to-end: (1) B …
  463 +
  464 +### A25. 引擎在校验逻辑中硬编码 M1 实体名/字段名,重命名即静默失效
  465 +
  466 +- **严重度**:MEDIUM **维度**:model-integrity **位置**:`engine/src/main/java/com/onto/engine/command/CommandEngine.java:226`
  467 +- **缺陷**:validate/runRules 用字面量 "DeliveryAddressEntity"."receiverPhone"."PaymentTerm"."dueDays"."OrderItem"."productId" 定位数据,一旦 M1/M3/M8 中这些实体或字段被改名,对应的手机号/账期/库存校验会无声失效。
  468 +- **证据**:
  469 +```
  470 +CommandEngine.java:226 `ctx.details.getOrDefault("DeliveryAddressEntity", ...)` 227 `row.get("receiverPhone")` 233 `getOrDefault("PaymentTerm", ...)` / `row.get("dueDays")` 251 `getOrDefault("OrderItem", ...)` 183 `it.get("productId")`。这些名称均来自 M1-domain.yaml(如 receiverPhone:114, DeliveryAddressEntity:101)。
  471 +```
  472 +- **影响 / 触发场景**:作为『通用解释引擎』,这些应从 M2 validations/M1 结构动态推导。当前若把 M1 的 receiverPhone 或 DeliveryAddressEntity 改名(正是『改模型』场景),手机号格式校验直接找不到数据、静默跳过——latent break,且违反铁律。
  473 +- **建议修复**:校验目标应从 M1 attributes/M2 validations 结构化推导(例如为 M2 validations 引入结构化断言而非中文文本匹配),不在引擎内硬编码实体名与字段名。
  474 +- **复现核验**:Evidence verified exactly as quoted in CommandEngine.java:
  475 +
  476 +- Line 226: `for (Map<String, Object> row : ctx.details.getOrDefault("DeliveryAddressEntity", List.of()))` then line 227 `Object p = row.get("receiverPhone");`
  477 +- Line 232-234: `if (cmdValidations.contains("dueDays"))` → `ctx.details.getOrDefault("PaymentTerm", List.of())` → `row.get("dueDays")`
  478 +- Line 251-252: `for (Map<String, Object> it : ctx.details.getOrDefault("OrderItem", List.of()))` → `it.get("productId")`
  479 +- Line 183: `items.add(Map.of("productId", it.get("productId"), ...))`
  480 +
  481 +Every one of these literals is a business identifier that originates in models/M1-domain.yaml (confirmed: OrderItem@73, PaymentTerm@88, DeliveryAddres …
  482 +- **反驳核验**:I tried to kill this finding and could not. The cited literals are present verbatim in engine/.../command/CommandEngine.java: line 226 `ctx.details.getOrDefault("DeliveryAddressEntity", List.of())`, 227 `row.get("receiverPhone")`, 232-234 `getOrDefault("PaymentTerm", ...)` / `row.get("dueDays")`, 251 `getOrDefault("OrderItem", ...)`, 252/183 `it.get("productId")`. All are business-specific M1 identifiers (M1-domain.yaml:101 DeliveryAddressEntity, :114 receiverPhone, :88 PaymentTerm, :93 dueDays, :73 OrderItem, :27/:76 productId) — exactly the class of literal the project's iron rule names as forbidden inside engine/.\n\nRefutation attempts all fail: (1) The literal is the lookup KEY into ctx …
  483 +
  484 +### A26. M5 encryptRules.storageEncrypt 从未被引擎兑现,敏感字段明文入库
  485 +
  486 +- **严重度**:MEDIUM **维度**:model-integrity **位置**:`models/M5-security.yaml:30`
  487 +- **缺陷**:M5 声明 contactPhone/receiverPhone 需存储加密,前端控件也提示『存储加密』,但引擎落库时无任何加密逻辑,手机号以明文写入数据库。
  488 +- **证据**:
  489 +```
  490 +M5-security.yaml:29-30 `encryptRules:` `storageEncrypt: [contactPhone, receiverPhone]`;DynamicRepository.insertDomain(46-69) 仅按 M1 类型 convert 后原样 jdbc.update,无加密;全仓 grep `encrypt` 在 engine/web 源码零命中;FieldControl.tsx:80 Tooltip 文案 `M5 敏感字段:存储加密、展示脱敏` 属虚假宣称。
  491 +```
  492 +- **影响 / 触发场景**:M5 模型的 storageEncrypt 指令是未被引擎读取/执行的死配置,敏感 PII 明文落库,与模型声明和前端提示矛盾;『改 M5 即改安全行为』对加密维度不成立。
  493 +- **建议修复**:在 DynamicRepository 写入/DomainReadService 读取时按 M5.encryptRules.storageEncrypt 通用地加解密,或移除该未实现的模型声明与前端『存储加密』提示。
  494 +- **复现核验**:All quoted evidence verified directly in the source. models/M5-security.yaml:29-30 declares `encryptRules: storageEncrypt: [contactPhone, receiverPhone]`. DynamicRepository.insertDomain (lines 46-69) only type-converts values via `convert(type, ...)` (lines 54-56) and calls `jdbc.update(sql, args.toArray())` (line 68) with no encryption step. `grep -rniI "encrypt"` over engine/src and web/src yields zero hits (only match repo-wide is the React DOM event `onEncrypted` in the minified web/dist bundle). Crucially, ModelRepository (lines 234-250) exposes `securityModel()`, `maskRules()`, and `functionPermission()` but has NO accessor for `encryptRules`/`storageEncrypt` — so the directive is neve …
  495 +- **反驳核验**:The finding is factually airtight and survives refutation. Verified: (1) M5-security.yaml:29-30 declares encryptRules.storageEncrypt:[contactPhone,receiverPhone]; (2) M0-meta-schema.yaml:245 makes encryptRules a REQUIRED schema member, so it is a first-class declared contract; (3) ModelRepository.java:236-242 exposes securityModel()/maskRules() but has NO encryptRules/storageEncrypt accessor, and grep -rin "encrypt" over engine/src and web/src returns ZERO hits — the directive is never read by any engine or web code; (4) DynamicRepository.insertDomain (46-69) only convert()s by M1 type then plain jdbc.update — no encryption; (5) contactPhone/receiverPhone are real persisted fields (M1:13/114 …
  496 +
  497 +### A27. Negative/zero item quantity is unvalidated and inflates inventory via StockDeduct
  498 +
  499 +- **严重度**:MEDIUM **维度**:security **位置**:`engine/src/main/java/com/onto/engine/event/EventEngine.java:106`
  500 +- **缺陷**:Item quantity is never validated to be positive; a negative quantity passes the sufficiency check and the deduct SQL subtracts a negative number, increasing stock.
  501 +- **证据**:
  502 +```
  503 +long qty = ... Long.parseLong(...); // L92, may be negative
  504 + if (current < qty) { ... FAIL_INSUFFICIENT } // L106, negative qty passes
  505 + repo.execute("UPDATE " + table + " SET " + stockCol + " = " + stockCol + " - ? WHERE " + pkCol + " = ?", qty, productId); // L111: stock - (-N) = stock + N
  506 +```
  507 +- **影响 / 触发场景**:CommandEngine.validate() checks only phone format and dueDays; it never checks item quantity. Attack: submit an order item with quantity -1000; the sufficiency guard (current < qty) is false, and UPDATE stock_num = stock_num - (-1000) inflates product inventory by 1000. Zero/negative quantities also let an attacker create orders that deduct nothing while still passing.
  508 +- **建议修复**:Validate quantity > 0 (and enforce numeric bounds) in the command validation stage before persistence and before cross-aggregate stock actions.
  509 +- **复现核验**:Verified all quoted evidence exists as claimed. EventEngine.deductStock (engine/.../event/EventEngine.java): L92 parses qty via Long.parseLong preserving negative sign; L106 `if (current < qty)` returns FAIL_INSUFFICIENT only when stock is below qty — for a negative qty (e.g. -1000) with positive current stock, `current < qty` is false so it passes; L111 `UPDATE ... SET stock_num = stock_num - ?` with qty=-1000 computes stock_num-(-1000)=stock_num+1000, inflating inventory.
  510 +
  511 +Reachability confirmed end-to-end: CommandController.execute (POST /api/scene/{sceneId}/execute) forwards the raw request body to commandEngine.execute with principal defaulting to normal_user. CommandEngine.validate() ( …
  512 +- **反驳核验**:Refutation failed; the finding is a genuine, reachable defect. Mechanics: EventEngine.deductStock L92 parses qty via Long.parseLong without a sign check; a negative value (e.g. -1000) passes the L106 sufficiency guard `if (current < qty)` because a positive current is never < a negative qty, then L111 `UPDATE ... SET stock = stock - ?` computes stock - (-1000) = stock + 1000, inflating inventory. Reachability: CommandEngine.executeCommand step e (L181-186) builds items from raw payload detail rows via it.getOrDefault("quantity",0) and calls applyCrossConsistency("OrderCreated", items) -> deductStock; the Ctx constructor (L326-331) copies all payload keys verbatim, so a client-supplied quanti …
  513 +
  514 +### A28. Submit button label hardcodes '新增订单'
  515 +
  516 +- **严重度**:LOW **维度**:frontend **位置**:`web/src/engine/SceneForm.tsx:147`
  517 +- **缺陷**:The primary submit button text is the order-specific literal '提交「新增订单」' regardless of which scene is rendered.
  518 +- **证据**:
  519 +```
  520 +<Button type="primary" loading={submitting} onClick={doSubmit}>提交「新增订单」</Button>
  521 +```
  522 +- **影响 / 触发场景**:A generic renderer that loads any M4 scene still labels its submit action '新增订单'; changing the scene in YAML does not change the wording. Minor, but a business term hardcoded in the generic engine.
  523 +- **建议修复**:Use schema.sceneName or the command desc for the button label, e.g. `提交「${schema.sceneName}」`.
  524 +- **复现核验**:Verified: web/src/engine/SceneForm.tsx:147 contains exactly `<Button type="primary" loading={submitting} onClick={doSubmit}>提交「新增订单」</Button>`. This lives in SceneForm, self-documented (lines 29-33) as the "场景通用渲染引擎" (generic scene renderer), which the iron rule requires to be business-agnostic. "新增订单" (New Order) is order-specific terminology hardcoded in the generic engine.
  525 +
  526 +Concrete manifestation: SceneSchema (web/src/types.ts:33) already carries a scene-agnostic `sceneName`, and App.tsx:73/90 renders scenes generically from it. If an operator edits M4/M8 YAML to add or rename a non-order scene (e.g. a purchase-order or repair-ticket scene) and hot-reloads, the left nav and card title cor …
  527 +- **反驳核验**:Verified against web/src/engine/SceneForm.tsx:147: `<Button type="primary" loading={submitting} onClick={doSubmit}>提交「新增订单」</Button>`. The literal '新增订单' (new order) is unconditionally rendered as the primary submit button for EVERY scene. My best refutation attempts all fail: (1) not unreachable — it's the main submit button in the returned JSX (lines 144-148); (2) not an acceptable generic fallback — the component's own docstring (lines 29-33) calls it "场景通用渲染引擎" and CLAUDE.md lists it as "★ 通用渲染引擎"; the useEffect is keyed on schema.sceneId (line 65) and all real logic is schema-driven, so loading any non-order M4 scene still mislabels the button; a truly generic caption would be "提交"; (3) …
  528 +
  529 +### A29. firstItemEntity() falls back to the literal 'OrderItem'
  530 +
  531 +- **严重度**:LOW **维度**:iron-rule **位置**:`engine/src/main/java/com/onto/engine/command/CommandEngine.java:368`
  532 +- **缺陷**:When no composition child is found, the engine defaults the order-line entity to the hardcoded name 'OrderItem'.
  533 +- **证据**:
  534 +```
  535 +Lines 368-371: `private String firstItemEntity(String aggregateId) { List<String> ch = models.compositionChildren(aggregateId); return ch.isEmpty() ? "OrderItem" : ch.get(0); }`
  536 +```
  537 +- **影响 / 触发场景**:The primary path is model-driven (first composition child), but the fallback literal 'OrderItem' and the design assumption that 'the first composition child is the line-item collection carrying productId/quantity' embed order semantics. For an aggregate with a different or reordered composition layout, the wrong entity is chosen or a non-existent 'OrderItem' is queried.
  538 +- **建议修复**:Remove the literal fallback; if line items are meaningful they should be identified via an explicit model marker rather than positional 'first child' + hardcoded default.
  539 +- **复现核验**:Evidence verified verbatim at CommandEngine.java:367-371. The ternary `return ch.isEmpty() ? "OrderItem" : ch.get(0);` embeds the literal order-domain entity name "OrderItem" directly in engine code. The iron rule explicitly names "OrderItem" as a canonical example of a forbidden hardcoded entity identifier inside engine/, so this is an unambiguous textual iron-rule violation: the generic interpreter "knows" a domain entity name. Renaming/removing the order-line entity in the YAML would not update this fallback — i.e. a requirements change would require touching engine code, exactly what the rule forbids. Reachability: `firstItemEntity` is called unconditionally at line 182 for every command …
  540 +- **反驳核验**:Verified at CommandEngine.java:370 the literal `return ch.isEmpty() ? "OrderItem" : ch.get(0);`. The iron rule explicitly names "OrderItem" as a prohibited hardcoded entity name in engine/, so this is a genuine (if trivial) iron-rule violation — an order-specific entity name compiled into the generic interpreter. It is reinforced by line 251 (`ctx.details.getOrDefault("OrderItem", ...)`) and lines 183/186 (`it.get("productId")`, `applyCrossConsistency("OrderCreated", ...)`) which hardcode order semantics unconditionally in the same method. My best refutation only partially lands: (1) the fallback branch is effectively unreachable for the configured order aggregate because compositionChildren …
  541 +
  542 +### A30. App.tsx defaults the initial scene to the literal SCENE_CREATE_ORDER
  543 +
  544 +- **严重度**:LOW **维度**:iron-rule **位置**:`web/src/App.tsx:15`
  545 +- **缺陷**:The generic shell hardcodes which scene opens first instead of selecting from the fetched scene list.
  546 +- **证据**:
  547 +```
  548 +Line 15: `const [sceneId, setSceneId] = useState<string>('SCENE_CREATE_ORDER')`
  549 +```
  550 +- **影响 / 触发场景**:Scenes are otherwise fetched dynamically, but the initial selection is a business-specific scene id. If SCENE_CREATE_ORDER is renamed/removed in M4, the first schema load 404s until the user manually clicks another scene. Minor, but it means changing the demo's primary scene requires editing frontend code.
  551 +- **建议修复**:Initialize sceneId to the first scene with a bound template from the fetched /meta/scenes list, rather than a hardcoded id.
  552 +- **复现核验**:Evidence confirmed at web/src/App.tsx:14 (finding said 15, but the exact quoted code `const [sceneId, setSceneId] = useState<string>('SCENE_CREATE_ORDER')` is on line 14 — "at/near" the cited line). `SCENE_CREATE_ORDER` is a genuine business-specific scene identifier: it is defined in models/M4-scene.yaml:10 as the "用户创建订单完整流程" (create-order) scene and cross-referenced from M5/M7/M8/MetaRule. The iron rule explicitly forbids hardcoded business identifiers in web/; the generic shell should derive its initial scene from the dynamically fetched scene list, not pin a specific order scene.\n\nManifestation is reachable: the schema-loading useEffect (App.tsx:23-27) fires on mount with the hardcode …
  553 +- **反驳核验**:Confirmed at web/src/App.tsx line 14 (finding cites line 15 — off by one; line 15 is the `schema` state, but the quoted evidence matches line 14): `const [sceneId, setSceneId] = useState<string>('SCENE_CREATE_ORDER')`. This literal is a business-specific M4 scene id hardcoded in the generic frontend shell. My refutation attempts all fail: (1) The value is NOT overridden after fetch — the scenes effect at line 21 (`api.scenes().then(setScenes)`) never calls setSceneId, so the effect at lines 23-27 fires `api.sceneSchema('SCENE_CREATE_ORDER')` on mount and the literal persists until a manual click. (2) It is not an unavoidable seed — the existing `if (!sceneId) return` guard at line 24 already …
  554 +
  555 +### A31. SchemaInitializer.seed() hardcodes Customer/Product aggregate and entity names
  556 +
  557 +- **严重度**:LOW **维度**:iron-rule **位置**:`engine/src/main/java/com/onto/engine/persist/SchemaInitializer.java:121`
  558 +- **缺陷**:Demo seeding is written for exactly two named aggregates (CustomerAggregate/Customer, ProductAggregate/Product) instead of iterating the seed file generically.
  559 +- **证据**:
  560 +```
  561 +Lines 122-136: `repo.tableName("CustomerAggregate", "Customer")` ... `repo.insertDomain("CustomerAggregate", "Customer", null, row, null)` and `repo.tableName("ProductAggregate", "Product")` ... `repo.insertDomain("ProductAggregate", "Product", null, row, null)`
  562 +```
  563 +- **影响 / 触发场景**:seed-data.yaml is explicitly non-ontology demo scaffolding, so this is the mildest case, but the loader still hardcodes both aggregate ids and their root entity names ('Customer'/'Product'). Adding a seeded reference aggregate, or renaming these in M1, requires editing Java even though the seed file is keyed by aggregate id and could be driven generically (for each key → aggregateId, look up rootName, insert).
  564 +- **建议修复**:Iterate the seed map's keys as aggregate ids, resolve rootName via models.rootName(key), and insert generically — no CustomerAggregate/ProductAggregate/Customer/Product literals.
  565 +- **复现核验**:Evidence verified verbatim in engine/src/main/java/com/onto/engine/persist/SchemaInitializer.java. seed() (lines 113-140) hardcodes the aggregate ids and root entity names in four places: line 122 `repo.tableName("CustomerAggregate", "Customer")`, line 124 `seed.getOrDefault("CustomerAggregate", ...)`, line 127 `repo.insertDomain("CustomerAggregate", "Customer", null, row, null)`, and lines 132/134/136 the mirror image for `"ProductAggregate"`/`"Product"`. Line 126 additionally hardcodes a Customer-specific business field name: `row.putIfAbsent("registerTime", ...)`. The task's own iron-rule definition explicitly names "CustomerAggregate"/"ProductAggregate" as example aggregate identifiers w …
  566 +- **反驳核验**:Confirmed real iron-rule violation. In engine/src/main/java/com/onto/engine/persist/SchemaInitializer.java, seed() hardcodes business aggregate/entity identifiers as string literals: line 122 `repo.tableName("CustomerAggregate", "Customer")`, line 127 `repo.insertDomain("CustomerAggregate", "Customer", null, row, null)`, line 132 `repo.tableName("ProductAggregate", "Product")`, line 136 `repo.insertDomain("ProductAggregate", "Product", null, row, null)`, plus line 126 hardcodes the business field name `registerTime`. These are exactly the aggregate/entity/field identifiers the iron rule enumerates as forbidden inside engine/.
  567 +
  568 +Refutation attempts all fail: (1) The "non-ontology demo scaffold …
  569 +
  570 +### A32. ME OrderCancelled→StockDeduct 与 M2 CancelOrder『归还库存』语义相反
  571 +
  572 +- **严重度**:LOW **维度**:model-integrity **位置**:`models/ME-event.yaml:63`
  573 +- **缺陷**:取消订单本应回补库存,但 ME 把 OrderCancelled 的 targetCommand 设为 StockDeduct(扣减),而 ProductAggregate 根本没有『增加库存』命令;模型层面自相矛盾。
  574 +- **证据**:
  575 +```
  576 +ME-event.yaml:63-66 `- triggerEvent: OrderCancelled / targetAggregate: ProductAggregate / targetCommand: StockDeduct`;M2-command.yaml:167 CancelOrder.bizSteps `...3.远程调用Product聚合增加对应库存...`;M2 ProductAggregate 仅有 StockDeduct(88) 一个库存命令,无 StockReturn/StockAdd。EventEngine.deductStock(85-118) 也只做 `stock = stock - qty`。
  577 +```
  578 +- **影响 / 触发场景**:即便引擎将来正确按 OrderCancelled 触发跨聚合规则,也会对取消订单继续扣减库存,与业务意图完全相反;这是 ME↔M2 的内在建模不一致,且缺少回补库存的命令定义。
  579 +- **建议修复**:在 M2 ProductAggregate 增加 StockReturn/StockAdd 命令并在 ME 将 OrderCancelled 的 targetCommand 指向它;引擎 applyCrossConsistency 依 targetCommand 区分扣减/回补。
  580 +- **复现核验**:All quoted evidence verified. ME-event.yaml:63-66 maps `OrderCancelled → targetAggregate: ProductAggregate → targetCommand: StockDeduct` (deduct). M2-command.yaml:160-169 defines CancelOrder whose desc (161) is `取消订单,归还库存` and bizSteps step 3 (167) is `远程调用Product聚合增加对应库存` (INCREASE stock). M4-scene.yaml:32-40 SCENE_CANCEL_ORDER is likewise named `取消订单、归还库存`. So the cancel lifecycle intent is stated as "return/increase stock" in three model files, while ME routes it to StockDeduct — a direct semantic contradiction. Further, M2 ProductAggregate exposes only StockDeduct (line 88) with no StockReturn/StockAdd, and EventEngine.deductStock (85-118) only performs `stock = stock - qty` (line 111) w …
  581 +- **反驳核验**:The finding is a model-integrity claim, and the evidence is incontrovertibly verified. ME-event.yaml:63-66 routes OrderCancelled to targetCommand StockDeduct, which is a stock DECREMENT: M2-command.yaml:88 (only stock command in ProductAggregate) has validation "可用库存 >= deductQty" and emits ProductStockDeducted; EventEngine.java:111 executes `stock = stock - qty`. Yet three other model files unanimously say cancel must RETURN/increase stock: M4-scene.yaml:33 sceneName "取消订单、归还库存", M2-command.yaml:161 desc "取消订单,归还库存" and :167 bizSteps "...远程调用Product聚合增加对应库存...", and MetaRule-business.yaml:44-49 R_CANCEL_STOCK_RETURN effect COMPENSATE "订单取消自动返还锁定商品库存". No StockReturn/StockAdd command exists …
  582 +
  583 +
  584 +---
  585 +
  586 +## 附录 B · B 级发现 —— 单边确认 / 严重度存疑(复现者判真、反驳者提异议,共 18 条)
  587 +
  588 +> 复现者确认可触发,但反驳者对其严重度或"是否属既定设计"提出异议,需人工裁定。
  589 +
  590 +### B1. Insufficient/absent stock silently commits the order (cross-aggregate consistency computed then discarded)
  591 +
  592 +- **严重度**:BLOCKER **维度**:eda **位置**:`engine/src/main/java/com/onto/engine/command/CommandEngine.java:185`
  593 +- **缺陷**:When StockDeduct fails (insufficient stock or product not found), the failure is only recorded in a log map; the order insert + outbox event still commit, so orders exist against stock that was never deducted.
  594 +- **证据**:
  595 +```
  596 +CommandEngine.executeCommand():
  597 + L185: List<Map<String, Object>> stockActions = eventEngine.applyCrossConsistency("OrderCreated", Map.of("items", items));
  598 + L187: if (!stockActions.isEmpty()) { trace.add(...); }
  599 + L191: out.put("success", true);
  600 +EventEngine.deductStock():
  601 + L106: if (current < qty) {
  602 + L107: action.put("status", "FAIL_INSUFFICIENT");
  603 + L109: return action; // returns WITHOUT throwing
  604 + }
  605 + L101: if (row.isEmpty()) { action.put("status", "SKIP_NOT_FOUND"); return action; }
  606 +The returned status is never inspected; no exception is raised, so the @Transactional execute() commits.
  607 +```
  608 +- **影响 / 触发场景**:Order for product P001 (stock=1) with quantity=5: master + OrderItem rows are INSERTed, OrderCreated is written to the outbox, deductStock returns status=FAIL_INSUFFICIENT, executeCommand still sets success=true, and @Transactional on execute() commits everything. Result: an order committed for goods that were never deducted (oversell), with stock unchanged. Same for a bad/renamed productId -> SKIP_NOT_FOUND, order still commits. The cross-aggregate consistency the design promises is computed and thrown away.
  609 +- **建议修复**:Have deductStock throw a domain exception on FAIL_INSUFFICIENT/SKIP_NOT_FOUND (or return a failure that executeCommand checks) so the surrounding @Transactional rolls back the order, or move stock reservation before persist and abort the flow with stage=STOCK_INSUFFICIENT.
  610 +- **复现核验**:Evidence verified verbatim. CommandEngine.executeCommand L185-186 calls eventEngine.applyCrossConsistency("OrderCreated", ...); L187-189 only adds a trace when stockActions is non-empty and never inspects the per-item status; L191 unconditionally sets out.put("success", true). execute() is @Transactional (L67) and treats a non-FALSE success as normal flow, so absent any exception the transaction commits. EventEngine.deductStock returns action maps with status SKIP_NOT_FOUND (L101-103, product not found) or FAIL_INSUFFICIENT (L106-110, current<qty) WITHOUT throwing — the master insert (L155), child inserts (L165), and OrderCreated outbox append (L175) all precede this and thus commit.\n\nConc …
  611 +- **反驳核验**:The finding mischaracterizes an intentional eventual-consistency design as a blocker bug. (1) models/ME-event.yaml declares the OrderCreated -> ProductAggregate.StockDeduct rule with `consistencyType: eventual`. Under DDD/EDA, a transaction mutates one aggregate and cross-aggregate effects are decoupled; an order committing independently of stock deduction is the normal saga intermediate state, not corruption. Rolling the order transaction back on another aggregate's command failure (what the finding demands) would turn declared eventual consistency into synchronous strong consistency across aggregates, contradicting the model. (2) The finding's central factual claim — "the failure is only r …
  612 +
  613 +### B2. No idempotency / duplicate-submission protection on command execution
  614 +
  615 +- **严重度**:HIGH **维度**:eda **位置**:`engine/src/main/java/com/onto/engine/command/CommandEngine.java:67`
  616 +- **缺陷**:execute() accepts no client request/idempotency key and generates a fresh root id per call, so a retried or double-clicked POST creates duplicate orders, duplicate outbox events, and double stock deductions.
  617 +- **证据**:
  618 +```
  619 +L67: @Transactional public Map<String,Object> execute(String sceneId, Map<String,Object> payload, String principal) {
  620 +L148: String rootId = IdGen.next(idPrefix(aggregateId)); // new id every call
  621 +CommandController.execute() (L31-37) passes the raw payload through with no dedup token. Nothing in the flow checks for a prior identical request.
  622 +```
  623 +- **影响 / 触发场景**:Client network retry or a double-click on submit sends the same order payload twice. Each call gets a new orderId, inserts a full order, appends OrderCreated to the outbox, and deducts stock again. Two orders + double stock deduction for one business intent; no way to reconcile because there is no shared idempotency key.
  624 +- **建议修复**:Require a client-supplied idempotency key (or hash of payload) and persist it in a unique-constrained dedup table inside the same transaction; on conflict return the prior result instead of re-executing.
  625 +- **复现核验**:Evidence verified in-file. CommandEngine.execute() (L67-68) takes only (sceneId, payload, principal) with no idempotency/request key. L148 `String rootId = IdGen.next(idPrefix(aggregateId));` mints a fresh id per call. CommandController.execute() (L31-37) only reads the X-Principal header and forwards the raw payload; nothing accepts or checks a dedup token. The execute()->executeCommand() path has no prior-request lookup, no unique constraint on the payload, and no idempotency store. Downstream mutations are genuinely non-idempotent: executeCommand inserts the root+children via repo.insertDomain (L155/165), appends OrderCreated to the outbox (eventEngine.append, L175), and triggers cross-ag …
  626 +- **反驳核验**:I read CommandEngine.java, CommandController.java, EventEngine.java and SchemaInitializer.java. The finding's factual claims are all accurate: execute() (L67) takes no idempotency token, rootId=IdGen.next(...) (L148) mints a fresh id per call, the controller (L31-37) only reads X-Principal, EventEngine.append (L42) always generates a new eventId, deductStock (L111) always re-subtracts, and the only PRIMARY KEY on domain tables is the freshly-generated root id (SchemaInitializer L91) — no natural-key uniqueness. So there is genuinely no dedup at any layer.
  627 +
  628 +However, this is a MISSING HARDENING FEATURE, not a defect. Reasons the finding should not stand at the claimed 'high' severity:
  629 +
  630 +1) No c …
  631 +
  632 +### B3. M8 客户模板绑定的 SCENE_CREATE_CUSTOMER 在 M4 中不存在
  633 +
  634 +- **严重度**:HIGH **维度**:model-integrity **位置**:`models/M8-front-schema.yaml:20`
  635 +- **缺陷**:M8 template_customer_single 绑定 bindSceneId: SCENE_CREATE_CUSTOMER,但 M4 sceneModel 从未定义该场景,导致该模板永远无法被通用引擎渲染或执行。
  636 +- **证据**:
  637 +```
  638 +M8-front-schema.yaml:20 `bindSceneId: SCENE_CREATE_CUSTOMER`;M4-scene.yaml 仅定义 SCENE_CREATE_ORDER / SCENE_MODIFY_ORDER_ADDR / SCENE_CANCEL_ORDER(无 SCENE_CREATE_CUSTOMER)。grep 全仓 SCENE_CREATE_CUSTOMER 仅出现在 M8。
  639 +```
  640 +- **影响 / 触发场景**:MetaController.scenes() 只遍历 M4 scenes,SCENE_CREATE_CUSTOMER 不在其中,故前端侧边栏永远不列出客户模板;若前端直接请求 /api/meta/scene/SCENE_CREATE_CUSTOMER,SceneSchemaService.buildSceneSchema 第 26 行 `if (scene.isEmpty()) throw new NoSuchElementException` 直接抛异常。M2 的 CreateCustomer/CreateProduct 命令、M5 无对应权限,全部成为无法触达的死元数据——违背『M8 模板↔M4 场景』契约。
  641 +- **建议修复**:在 M4 增加 sceneId: SCENE_CREATE_CUSTOMER(含 permissionBind + flowSteps bindCommand: CreateCustomer),并在 M5 functionPermissions 增补对应 permId;或修正 M8 的 bindSceneId 指向已存在场景。
  642 +- **复现核验**:Every link in the claimed chain is verified against the actual files. (1) models/M8-front-schema.yaml:20 does contain `bindSceneId: SCENE_CREATE_CUSTOMER` on template `template_customer_single`. (2) models/M4-scene.yaml defines only SCENE_CREATE_ORDER (l.10), SCENE_MODIFY_ORDER_ADDR (l.22), SCENE_CANCEL_ORDER (l.32); repo-wide grep shows SCENE_CREATE_CUSTOMER exists nowhere except M8, so it is a dangling reference. (3) MetaController.scenes() (l.30) iterates models.scenes(), which reads strictly M4.sceneModel.sceneDefinitions (ModelRepository.java:222-224); the React sidebar is built entirely from that list (web/src/App.tsx:21,53), so the customer template can never be listed. There is no al …
  643 +- **反驳核验**:The finding's raw facts check out but its "high"/reachable-defect framing does not survive inspection.
  644 +
  645 +FACTS (confirmed): M4-scene.yaml only defines SCENE_CREATE_ORDER / SCENE_MODIFY_ORDER_ADDR / SCENE_CANCEL_ORDER (M4:10,22,32); M8-front-schema.yaml:20 `bindSceneId: SCENE_CREATE_CUSTOMER` has no matching M4 scene. True.
  646 +
  647 +WHY IT IS NOT A REACHABLE DEFECT:
  648 +1. No forward dereference exists. The ONLY consumer of M8 templates is a reverse lookup: ModelRepository.templateByScene (ModelRepository.java:262-267) iterates pageTemplates() and matches `sceneId.equals(tm.get("bindSceneId"))`, keyed on a REAL scene id passed in. pageTemplates()'s only caller is templateByScene itself (grep confirms line …
  649 +
  650 +### B4. M2 CreateOrder.emitEvents 引用了 ME 未定义的事件
  651 +
  652 +- **严重度**:HIGH **维度**:model-integrity **位置**:`models/M2-command.yaml:128`
  653 +- **缺陷**:CreateOrder 声明发出 OrderItemAdded / OrderPaymentTermSet / OrderDeliveryAddressSet,但 ME-event.yaml 的 OrderAggregate 只定义了 OrderCreated / OrderDeliveryAddressModified / OrderCancelled,三个事件在事件模型中不存在。
  654 +- **证据**:
  655 +```
  656 +M2-command.yaml:128-130 `- OrderItemAdded` `- OrderPaymentTermSet` `- OrderDeliveryAddressSet`;ME-event.yaml:45/49/53 OrderAggregate.events 仅有 `OrderCreated:` `OrderDeliveryAddressModified:` `OrderCancelled:`。
  657 +```
  658 +- **影响 / 触发场景**:CommandEngine 第 174-177 行遍历 cmd.emitEvents 调用 eventEngine.append;EventEngine.append 第 41 行对 ME 未声明的事件走 `def.isEmpty() ? deriveTopic(eventName)`,凭空派生出 topic_order_item_added 等 topic,且 eventPayload 因 eventDef 为空只塞 rootId。这些是 ME 从未声明的『幻影事件』,topic/payload/consumers 全部脱离事件本体——M2↔ME 不一致,消费者(product-service 等)也无从订阅。
  659 +- **建议修复**:要么在 ME-event.yaml 的 OrderAggregate 补齐 OrderItemAdded/OrderPaymentTermSet/OrderDeliveryAddressSet 的 topic+payload+consumers,要么从 M2 CreateOrder.emitEvents 删除这三个未建模事件。
  660 +- **复现核验**:Evidence fully verified. models/M2-command.yaml:126-130 declares CreateOrder.emitEvents = [OrderCreated, OrderItemAdded, OrderPaymentTermSet, OrderDeliveryAddressSet]. models/ME-event.yaml:43-56 defines OrderAggregate.events with only OrderCreated (45), OrderDeliveryAddressModified (49), OrderCancelled (53) — so OrderItemAdded/OrderPaymentTermSet/OrderDeliveryAddressSet are undefined. Grep over models/, engine/, web/ confirms those three names appear ONLY at M2:128-130. The runtime chain is real: CommandEngine.java:174-177 loops cmd.emitEvents calling eventEngine.append; eventPayload (373-384) resolves an empty eventDef (ModelRepository.eventDef:276-285 returns emptyMap for undefined events) …
  661 +- **反驳核验**:The literal observation is true: M2-command.yaml:126-130 declares CreateOrder.emitEvents = [OrderCreated, OrderItemAdded, OrderPaymentTermSet, OrderDeliveryAddressSet], while ME-event.yaml:44-56 defines only OrderCreated/OrderDeliveryAddressModified/OrderCancelled for OrderAggregate. But the finding's characterization as a high-severity defect does not survive. (1) The engine INTENTIONALLY supports events not declared in ME: EventEngine.append:40-41 comments "ME 已声明 topic 则用之;未声明的事件按命名约定派生 topic_snake_case" and branches def.isEmpty()?deriveTopic(...); ModelRepository.eventDef:284 returns emptyMap by design; CommandEngine.eventPayload:382 falls back to {rootId}. Nowhere is emitEvents⊆ME enfor …
  662 +
  663 +### B5. permitted() fails open when a scene has no permissionBind
  664 +
  665 +- **严重度**:HIGH **维度**:security **位置**:`engine/src/main/java/com/onto/engine/command/CommandEngine.java:359`
  666 +- **缺陷**:When a scene's permissionBind list is empty/absent the permission check returns true for everyone (default-allow / fail-open).
  667 +- **证据**:
  668 +```
  669 +List<Object> perms = asList(scene.get("permissionBind"));
  670 + if (perms.isEmpty()) return true; // any scene lacking permissionBind is executable by any principal
  671 +```
  672 +- **影响 / 触发场景**:Because the model is the source of truth and scenes are hot-reloadable (POST /api/meta/reload), any new/edited scene that omits permissionBind — or a typo that makes asList() return empty — becomes fully unauthenticated-executable. The safe default for an authorization gate must be deny, not allow. Attack: add a scene without permissionBind, reload, and anyone can run its bound command (persist rows, emit events, deduct stock).
  673 +- **建议修复**:Default to deny: return false (or throw) when permissionBind is empty/unresolved, and require every executable scene to bind at least one functionPermission.
  674 +- **复现核验**:Evidence confirmed at CommandEngine.java:357-364: permitted() does `if (perms.isEmpty()) return true;`, so a scene whose permissionBind is empty/absent authorizes every principal. It is reached from the execute() gate (line 76-77). There is genuinely no guardrail: ModelRepository.load()/reload() (lines 50-69) only calls `new Yaml().load(in)` with no schema validation, and the engine has no JSON-schema dependency. M0-meta-schema.yaml:226 lists permissionBind under `required`, but that is documentary and never enforced — a scene that omits the key or mistypes it (e.g. `permissonBind:`) loads fine, then scene.get("permissionBind") is null, asList()->empty, and permitted() returns true. Concrete …
  675 +- **反驳核验**:The literal code is as quoted: CommandEngine.java:359 `if (perms.isEmpty()) return true;` default-allows when permissionBind is empty. But the finding's framing as a high-severity, reachable authorization bypass does not hold up.
  676 +
  677 +(1) There is no authorization boundary to fail open against. `principal` is read verbatim from a client-controlled `X-Principal` header (CommandController.java:34-35); grep of engine/src and pom.xml shows NO Spring Security, no SecurityFilterChain/filter/@PreAuthorize, and CORS is wide open (WebConfig.java:12-15). Any anonymous caller can set X-Principal to any value, and allowPrincipals values are served publicly (MetaController GET /api/meta/model/{code} raw M5 a …
  678 +
  679 +### B6. Generic /api/data/{aggregateId} dumps entire root table of any aggregate with no authz
  680 +
  681 +- **严重度**:HIGH **维度**:security **位置**:`engine/src/main/java/com/onto/engine/web/DataController.java:24`
  682 +- **缺陷**:The options/list read endpoints perform no authentication or permission check and return every row of the requested aggregate's root table, exposing all customers/orders to anonymous callers.
  683 +- **证据**:
  684 +```
  685 +@GetMapping("/{aggregateId}")
  686 + public List<Map<String, Object>> options(@PathVariable String aggregateId) {
  687 + return readService.options(aggregateId); // DomainReadService.listRoot -> repo.listAll(table) = SELECT * FROM <table>; opt.put("data", d) returns ALL domain fields
  688 +```
  689 +- **影响 / 触发场景**:No principal is even passed to DataController; there is no ownership/row-level check. GET /api/data/CustomerAggregate returns every customer (name, level, masked phone); GET /api/data/OrderAggregate returns every order root row (customerId, totalAmount, etc.); GET /api/data/orders/list returns all orders. Any aggregateId is accepted, so the endpoint is a generic anonymous table-dump primitive. Only two fields (contactPhone/receiverPhone) are masked — all other columns are returned in the clear.
  690 +- **建议修复**:Require authentication and enforce a read permission (and per-record ownership for order data) before returning rows; restrict which aggregates are exposed as option sources rather than accepting an arbitrary aggregateId.
  691 +- **复现核验**:Evidence verified verbatim. DataController.java:24-27 exposes GET /api/data/{aggregateId} -> DomainReadService.options() -> listRoot() -> DynamicRepository.listAll() which runs `SELECT * FROM <table>` (DynamicRepository.java:76-78) and returns all rows. options() attaches every domain field via opt.put("data", d) (DomainReadService.java:48). M5-security-model.yaml maskRules contains only contactPhone and receiverPhone, so all other columns (customer name/level, order customerId/totalAmount) are returned in clear. There is NO authorization on this path: no @RequestHeader/principal param on DataController, no permission() call, and no Spring Security dependency in pom.xml (grep found none). Th …
  692 +- **反驳核验**:The finding is factually accurate (DataController.java:25-26 `options()` and :30-33 `orders()` return every root row with no principal), but it mischaracterizes an app-wide, intentional demo posture as a high-severity access-control regression in the reviewed engine code. Refutation on four grounds:
  693 +
  694 +1) There is NO authentication anywhere in the system to bypass. pom.xml has no spring-boot-starter-security (only web, jdbc, h2, mysql, snakeyaml, aviator). The only "identity" is the self-asserted, unverified `X-Principal` HTTP header on the command path (CommandController.java:34-35), defaulting to `normal_user` (application.yml:23). Any caller can send `X-Principal: admin`, so CommandEngine.p …
  695 +
  696 +### B7. Map.of() with user-derived nullable values throws NPE when building stock-deduct items
  697 +
  698 +- **严重度**:MEDIUM **维度**:correctness **位置**:`engine/src/main/java/com/onto/engine/command/CommandEngine.java:183`
  699 +- **缺陷**:Map.of() rejects null keys/values, so an OrderItem whose productId (or explicitly-null quantity) is null causes a NullPointerException while assembling cross-consistency items, aborting execute after the order was already persisted.
  700 +- **证据**:
  701 +```
  702 +for (Map<String, Object> it : ctx.details.getOrDefault(firstItemEntity(aggregateId), List.of())) {
  703 + items.add(Map.of("productId", it.get("productId"), "quantity", it.getOrDefault("quantity", 0)));
  704 +}
  705 +// Map.of throws NPE if it.get("productId") is null, or if key "quantity" is present with a null value (getOrDefault only substitutes for ABSENT keys)
  706 +```
  707 +- **影响 / 触发场景**:In the shipped M8, productId/quantity are required so validate() blocks the null case. But making a line-item field optional is exactly the supported 'edit YAML only' workflow: remove `required: true` from select_product in M8, then POST an OrderItem like {"quantity":1} (no productId). validate() passes, persist succeeds, then line 183 executes Map.of("productId", null, ...) -> NPE -> transaction rollback / HTTP 500. Also triggers if a client sends {"productId":"P001","quantity":null} once quantity is non-required. The generic engine should not assume these payload fields are non-null.
  708 +- **建议修复**:Use a null-tolerant map (e.g. new LinkedHashMap<>() with put/putIfAbsent, or HashMap) instead of Map.of, and skip/handle items with missing productId explicitly.
  709 +- **复现核验**:Evidence verified verbatim at CommandEngine.java:183: `items.add(Map.of("productId", it.get("productId"), "quantity", it.getOrDefault("quantity", 0)));`. Both NPE mechanisms are correct Java: `Map.of` (ImmutableCollections.MapN) calls Objects.requireNonNull on every value, and `getOrDefault` returns the map's null value when the key is PRESENT-but-null (it only substitutes the default for ABSENT keys). So a null/absent productId, or an explicit `"quantity":null`, both crash line 183.
  710 +
  711 +Reachability confirmed end-to-end: (1) M8 currently marks both fields required (M8-front-schema.yaml:144-146 productId required:true, 154-156 quantity required:true), and validate() (lines 208-222) rejects blan …
  712 +- **反驳核验**:The Map.of NPE pattern at CommandEngine.java:183 is real in the abstract (Map.of rejects null values, and getOrDefault only substitutes for ABSENT keys, not present-but-null), but it is NOT reachable in the delivered system, and the finding itself concedes this.
  713 +
  714 +Guard verified: validate() (lines 205-222) drives off ctx.requiredFields, populated by collectRequired() (lines 337-352) from every M8 childComponent carrying required:true, keyed by domainFieldName + the container's domainEntityName ("OrderItem"). In the shipped M8 (M8-front-schema.yaml:144-146 productId, 153-156 quantity) BOTH fields are required:true bound to OrderItem. So any OrderItem row with a null/blank productId or quantity …
  715 +
  716 +### B8. Cross-consistency is a hardcoded inline call decoupled from the outbox and from actually-emitted events
  717 +
  718 +- **严重度**:MEDIUM **维度**:eda **位置**:`engine/src/main/java/com/onto/engine/command/CommandEngine.java:186`
  719 +- **缺陷**:StockDeduct is triggered by a literal "OrderCreated" and item fields "productId"/"quantity" pulled straight from the payload in the same transaction, while the outbox poller only flips status NEW->PUBLISHED; the appended events are never consumed to drive side effects, so the 'event-driven eventual consistency' is neither event-driven nor eventual.
  720 +- **证据**:
  721 +```
  722 +CommandEngine L182-186:
  723 + for (Map<String, Object> it : ctx.details.getOrDefault(firstItemEntity(aggregateId), List.of())) {
  724 + items.add(Map.of("productId", it.get("productId"), "quantity", it.getOrDefault("quantity", 0)));
  725 + }
  726 + eventEngine.applyCrossConsistency("OrderCreated", Map.of("items", items));
  727 +EventEngine.pollAndPublish() L131-134 only logs and does UPDATE ... SET status='PUBLISHED'; no consumer reads payload_json to invoke StockDeduct. The emitted events (cmd.emitEvents, L174) and the cross-consistency trigger are wired independently.
  728 +```
  729 +- **影响 / 触发场景**:(a) If M2 emitEvents is renamed away from OrderCreated, the outbox no longer carries OrderCreated yet stock is still deducted (trigger ignores what was actually appended); conversely renaming keeps deduction firing off a stale literal. (b) If the item entity's field is renamed from productId/quantity in the model, items carry null productId -> deductStock returns SKIP_NOT_FOUND and stock is silently never deducted while the order commits. (c) Because deduction runs inline in execute()'s transaction, it is strong-consistent, not the 'eventual' path the outbox implies; the outbox is write-only decoration.
  730 +- **建议修复**:Drive cross-consistency from the outbox relay: have pollAndPublish (or a real consumer) read appended events and dispatch crossAggConsistencyRules by matching the persisted event_name/payload, instead of a literal "OrderCreated" and hardcoded item field names.
  731 +- **复现核验**:Evidence verified verbatim. CommandEngine.java L180-186 (inside the `@Transactional execute()` path, step "e"):
  732 +```
  733 +List<Map<String,Object>> items = new ArrayList<>();
  734 +for (Map<String,Object> it : ctx.details.getOrDefault(firstItemEntity(aggregateId), List.of())) {
  735 + items.add(Map.of("productId", it.get("productId"), "quantity", it.getOrDefault("quantity", 0)));
  736 +}
  737 +List<Map<String,Object>> stockActions = eventEngine.applyCrossConsistency("OrderCreated", Map.of("items", items));
  738 +```
  739 +The trigger event is the string literal "OrderCreated" and the item fields "productId"/"quantity" are pulled from the payload by hardcoded key. EventEngine.applyCrossConsistency (L66-82) matches on `rule.get("trig …
  740 +- **反驳核验**:The finding is an architecture/design-fidelity critique dressed as an EDA correctness defect; as an "eda" defect it produces no reachable wrong behavior in the system as designed and shipped.
  741 +
  742 +1) The mapping is in fact model-driven, contradicting "neither event-driven." At CommandEngine.java:186 the literal "OrderCreated" is only a lookup key; the actual decision OrderCreated→StockDeduct→targetAggregate is resolved from ME.yaml via EventEngine.applyCrossConsistency (L66-82: `for (Object r : models.crossConsistencyRules())` matching `rule.get("triggerEvent")`), and ModelRepository.crossConsistencyRules() (L287-289) reads ME-event.yaml's crossAggConsistencyRules (ME L58-62). So the consistency …
  743 +
  744 +### B9. Control type ignores M1 fieldType, contradicting advertised model-change behavior
  745 +
  746 +- **严重度**:MEDIUM **维度**:frontend **位置**:`web/src/engine/FieldControl.tsx:24`
  747 +- **缺陷**:FieldControl selects the widget solely from M8 compType and never consults the M1 fieldType, so changing a field's domain type does not change its control.
  748 +- **证据**:
  749 +```
  750 +switch (comp.compType) {
  751 + case 'number_input': ...
  752 + case 'input':
  753 + default: return <Input ... />
  754 +```
  755 +- **影响 / 触发场景**:App.tsx Help step 3 tells users '把某字段 type 由 string 改为 int → 热重载 → 该组件输入类型随之改变', but fieldType is only used for the small '[int]' tag in FieldLabel (SceneForm.tsx:161); the actual control stays whatever M8 compType says. This documented 'change model → change behavior' scenario silently does not work, undermining the core demo claim.
  756 +- **建议修复**:Either derive/override the control from fb.fieldType when compType is generic (int/decimal -> InputNumber), or correct the Help text to state the widget is driven by M8 compType, not M1 type.
  757 +- **复现核验**:Verified in the actual files. FieldControl.tsx:24 does `switch (comp.compType)` and never reads `fb.fieldType`; the only fieldBind props it uses are placeholder/formLabel/optionsSource/masked. Grep confirms `fieldType` is referenced in web/src only at types.ts:12 and SceneForm.tsx:161 (the small `[{fb.fieldType}]` secondary tag). SceneSchemaService.enrich() proves the two are decoupled: line 115 copies M8 compType verbatim while line 128 separately sets fbOut.fieldType from M1, so a change to M1 type cannot alter compType. App.tsx:122 (Help modal titled 验证「改模型→改行为」) explicitly promises step 3: change a field's M1 type string→int → hot reload → 该组件输入类型随之改变. Concrete repro: M8 line 101–106 bin …
  758 +- **反驳核验**:The finding files a code defect against FieldControl.tsx:24, but the code there is correct by design, not broken. FieldControl is deliberately a GENERIC widget registry keyed on M8 `compType`, and SceneSchemaService.enrich() (SceneSchemaService.java:116) copies `compType` verbatim from M8 — `out.put("compType", comp.get("compType"))`. M8-front-schema.yaml authors `compType` explicitly per component (input/select/number_input/date_picker). So widget selection is a presentation concern owned by M8, while domain type is owned by M1. That separation is defensible DDD layering, arguably MORE correct than auto-deriving the widget from the domain type (an `int` field might legitimately want a slide …
  759 +
  760 +### B10. M0 元校验根规范从未被引擎执行,且各层实际不符合 M0 的模式约束
  761 +
  762 +- **严重度**:MEDIUM **维度**:model-integrity **位置**:`models/M0-meta-schema.yaml:10`
  763 +- **缺陷**:M0 自称『全域校验根规范』,但 ModelRepository 只把 M0 当作原始层加载展示,从不对任何层做 JSON-Schema 校验;而其它层还大面积违反 M0 自身定义的模式。
  764 +- **证据**:
  765 +```
  766 +M0-meta-schema.yaml:10 Identifier `pattern: "^[a-z][a-zA-Z0-9]*$"`(禁止下划线),但 M8 的 compId(如 24:`card_customer_base`)、templateId(16:`template_customer_single`) 及 M4 permissionBind(order_create) 全是 snake_case;M0:21 SceneId `pattern: "^SCENE_[A-Z0-9_]+$"`,而 M8:278 template3 `bindSceneId: ""` 为空串不匹配。ModelRepository.java:50-64 load() 仅 `new Yaml().load(in)` 存入 layers,全仓无任何按 rootAllOntologySchema 的校验调用。
  767 +```
  768 +- **影响 / 触发场景**:M0 承诺的『统一前后端数据/组件/页面描述标准』只是装饰性文档:既不会在加载/热重载时拦截不合规模型,其它层与 M0 的 pattern 也系统性冲突。任何『先改 M0 收紧约束』的操作对运行时行为零影响,给出错误的合规安全感。
  769 +- **建议修复**:在 ModelRepository.load()/reload() 中用 M0 的 definitions 对各层做 JSON-Schema 校验并在不合规时报错;同时统一 Identifier/SceneId 模式与实际命名(snake_case 或改 M0 pattern)。
  770 +- **复现核验**:All quoted evidence verified against the actual files. M0-meta-schema.yaml:10 defines `Identifier: pattern: "^[a-z][a-zA-Z0-9]*$"` (no underscores) and M0:21 defines `SceneId: pattern: "^SCENE_[A-Z0-9_]+$"`. These patterns are bound via $ref to real fields: compId→Identifier (M0:298), templateId→Identifier (M0:310), permissionBind items→Identifier (M0:237), bindSceneId→SceneId (M0:314, required at M0:308). M0 also declares `rootAllOntologySchema` (M0:372-384) and its header calls itself the "全域校验根规范". Sibling models systematically violate these: M8:16 templateId `template_customer_single`, M8:24 compId `card_customer_base`, M4:13 permissionBind `order_create` all use snake_case (illegal unde …
  771 +- **反驳核验**:The finding's facts are accurate but it does not describe a reachable functional defect nor an iron-rule violation; it is an aspirational-documentation / cosmetic-consistency observation.
  772 +
  773 +Confirmed facts: ModelRepository.load() (lines 50-64) only calls `new Yaml(new LoaderOptions()).load(in)` and stores raw objects; grep shows no JSON-Schema library and no reference to `rootAllOntologySchema` anywhere in engine/ or web/. The only 3 code hits for "M0" are two doc comments and `LAYER_FILES.put("M0","M0-meta-schema.yaml")`, which loads it purely as a display layer (rawLayer). compIds/templateIds are snake_case (M8 lines 16,24,34...), `bindSceneId: ""` at M8:278, and `permissionBind:[order_crea …
  774 +
  775 +### B11. H2 web console enabled without authentication in the default profile
  776 +
  777 +- **严重度**:MEDIUM **维度**:security **位置**:`engine/src/main/resources/application.yml:13`
  778 +- **缺陷**:The default profile (used by start.sh and mvn spring-boot:run) exposes the H2 console at /h2-console with the sa account and an empty password.
  779 +- **证据**:
  780 +```
  781 +username: sa
  782 + password: ""
  783 + h2:
  784 + console:
  785 + enabled: true
  786 + path: /h2-console
  787 +```
  788 +- **影响 / 触发场景**:Anyone who can reach the engine port can browse to /h2-console and connect with jdbc:h2:mem:trade_db, user sa, empty password — full read/write/arbitrary-SQL on the running database (and H2 console SQL can reach the filesystem/RCE in some configs). There is no auth in front of it. This is the day-to-day run mode per CLAUDE.md.
  789 +- **建议修复**:Disable the H2 console by default (only enable behind auth on an explicitly local bind), and never ship an empty DB password on a network-reachable service.
  790 +- **复现核验**:Evidence verified at engine/src/main/resources/application.yml:11-16: `username: sa`, `password: ""`, and `h2.console.enabled: true` / `path: /h2-console`. No Spring Security exists (grep for SecurityFilterChain/spring-boot-starter-security/@EnableWebSecurity/httpBasic returns nothing; pom.xml has only starter-web/starter-jdbc/h2), and WebConfig.java only adds permissive CORS — so nothing guards /h2-console. Concrete reproduction: `./start.sh` or `mvn spring-boot:run` uses the default profile; browsing to http://localhost:8080/h2-console and connecting with jdbc:h2:mem:trade_db, user sa, empty password yields full SQL access to the live in-memory DB (same JVM, shared named mem DB), and H2 co …
  791 +- **反驳核验**:The config line is factually present (application.yml:11-16: username sa, password "", h2.console.enabled: true, path /h2-console), but it is not a reachable security defect. (1) Production disables it and the deployment forces the prod profile: deploy/Dockerfile.engine:15 hardcodes ENTRYPOINT java -jar app.jar --spring.profiles.active=prod, and application-prod.yml:9-11 sets h2.console.enabled: false (and switches to MySQL). deploy/README.md:16 confirms the engine runs with the prod profile. So no deployed topology exposes the console. (2) The enabled-console profile is only reachable via a local ./start.sh / mvn spring-boot:run run against jdbc:h2:mem:trade_db — an in-memory, ephemeral DB …
  792 +
  793 +### B12. CORS allows any origin on all /api/** endpoints in every profile
  794 +
  795 +- **严重度**:MEDIUM **维度**:security **位置**:`engine/src/main/java/com/onto/engine/config/WebConfig.java:12`
  796 +- **缺陷**:The CORS policy allows all origins, methods and headers for /api/**, and this config is not profile-scoped, so it applies to prod as well as dev.
  797 +- **证据**:
  798 +```
  799 +registry.addMapping("/api/**")
  800 + .allowedOriginPatterns("*")
  801 + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
  802 + .allowedHeaders("*");
  803 +```
  804 +- **影响 / 触发场景**:Combined with the lack of authentication and the header-based principal, any malicious website loaded in a victim's browser can issue cross-origin GET/POST to the engine (including /execute with X-Principal: backend_admin, since allowedHeaders('*') permits the custom header) and read all /api/data responses. There is no origin allow-list even for production.
  805 +- **建议修复**:Restrict allowedOrigins to the known front-end origin(s) per environment (profile-scoped config), and only allow the headers/methods actually required.
  806 +- **复现核验**:Evidence verified verbatim at engine/src/main/java/com/onto/engine/config/WebConfig.java:12-15: addMapping("/api/**").allowedOriginPatterns("*").allowedMethods("GET","POST","PUT","DELETE","OPTIONS").allowedHeaders("*"). WebConfig is an unconditional @Configuration (no @Profile), so the wide-open CORS applies to every profile including prod — application-prod.yml exists and points at a real MySQL, confirming a production deployment path.
  807 +
  808 +The impact is reachable because there is NO authentication in the app: grep shows no spring-security dependency in pom.xml, no SecurityFilterChain/@EnableWebSecurity anywhere. The only principal mechanism is the attacker-controllable X-Principal request head …
  809 +- **反驳核验**:Read WebConfig.java:12-15 and grepped all of engine/ for allowCredentials/SecurityFilterChain/cookies — none exist. The refutation stands on four points: (1) The CORS mapping uses allowedOriginPatterns("*") WITHOUT allowCredentials(true). Wildcard CORS is only a recognized vulnerability when paired with credentials; without it, the browser attaches no cookies/ambient auth and no credentialed data can be exfiltrated. (2) The engine has NO authentication at all (no Spring Security, no session, no cookies — confirmed by grep). The "principal" is a purely client-supplied X-Principal header (CommandController.java:34, default normal_user at application.yml:23). CORS exists to protect ambient auth …
  810 +
  811 +### B13. Model viewer /api/meta/model/{code} exposes the full security model unauthenticated
  812 +
  813 +- **严重度**:MEDIUM **维度**:security **位置**:`engine/src/main/java/com/onto/engine/web/MetaController.java:66`
  814 +- **缺陷**:Any caller can retrieve the raw parsed content of any model layer, including M5 with its principals, functionPermissions and mask/encrypt rules.
  815 +- **证据**:
  816 +```
  817 +@GetMapping("/model/{code}")
  818 + public Map<String, Object> model(@PathVariable String code) {
  819 + ... out.put("content", models.rawLayer(code)); // returns raw M5-security.yaml incl. allowPrincipals: [normal_user, backend_admin]
  820 +```
  821 +- **影响 / 触发场景**:GET /api/meta/model/M5 discloses the exact principal strings and which permissions map to which scenes. Because the principal is a spoofable header (see other finding), this hands an attacker the precise value (backend_admin) needed to escalate, plus the full mask/encrypt configuration. All of it is unauthenticated.
  822 +- **建议修复**:Gate the model-viewer and metadata endpoints behind authentication/authorization, and avoid returning the security layer (principals/permissions) to unprivileged callers.
  823 +- **复现核验**:Evidence verified. MetaController.java:66-72 returns models.rawLayer(code) with no auth; ModelRepository.java:94 confirms rawLayer just returns layers.get(code) (full parsed content, unfiltered). M5-security.yaml contains the exact sensitive data claimed: principals [normal_user, backend_admin], functionPermissions with allowPrincipals mapped to scenes, maskRules, and encryptRules. There is no authentication layer: engine/pom.xml has no spring-boot-starter-security, WebConfig only adds a wildcard CORS mapping, and no filter/interceptor exists. Reproducible manifestation: `curl http://localhost:8080/api/meta/model/M5` returns the full raw M5 security model to any anonymous caller. So the endp …
  824 +- **反驳核验**:The endpoint exists and does return raw M5 content unauthenticated, but the finding's substantive claims (medium-severity info disclosure that enables privilege escalation) are refuted by the actual code.
  825 +
  826 +1) It is an intentional core feature, not a leak. MetaController.java:66-73 `/api/meta/model/{code}` powers the frontend "模型查看器" (Model Viewer) tab — web/src/panels.tsx:110 comment: "直接查看任意本体层解析后的内容,证明「页面即模型」" and App.tsx:125 "「模型查看器」标签页可实时查看引擎当前加载的各层模型内容". The whole design contract (CLAUDE.md) is that the models are the openly-displayed SINGLE SOURCE OF TRUTH. Displaying every layer is the endpoint's designed purpose.
  827 +
  828 +2) No secrets are disclosed. M5-security.yaml (read in full) contains …
  829 +
  830 +### B14. Hardcoded default database credentials in prod profile and compose file
  831 +
  832 +- **严重度**:MEDIUM **维度**:security **位置**:`engine/src/main/resources/application-prod.yml:8`
  833 +- **缺陷**:The production profile falls back to a hardcoded DB password, and docker-compose commits weak MySQL root/user passwords.
  834 +- **证据**:
  835 +```
  836 +password: ${DB_PASS:trade123} // application-prod.yml L8; deploy/docker-compose.yml L8 MYSQL_ROOT_PASSWORD: root123, L11 MYSQL_PASSWORD: trade123
  837 +```
  838 +- **影响 / 触发场景**:If DB_PASS is not overridden, prod connects with the well-known committed password 'trade123'; the compose stack ships root123/trade123. Anyone with the repo (or scanning the deployment) knows the DB credentials, enabling direct database access on the exposed 3306 port.
  839 +- **建议修复**:Remove hardcoded fallbacks; require DB_PASS/root password to be injected via secrets with no default, and do not commit real credentials to the compose file.
  840 +- **复现核验**:Evidence verified exactly as quoted. engine/src/main/resources/application-prod.yml:8 reads `password: ${DB_PASS:trade123}`, so the production Spring profile silently falls back to the committed literal `trade123` when DB_PASS is unset (CWE-798/CWE-521 silent weak-credential fallback rather than a fail-fast). deploy/docker-compose.yml — whose own header comment calls it the production topology mapping ("生产拓扑映射...MySQL/RocketMQ 为生产映射") — commits `MYSQL_ROOT_PASSWORD: root123` (L8), `MYSQL_USER: trade`/`MYSQL_PASSWORD: trade123` (L10-11), passes `DB_PASS: trade123` to the engine (L39), and binds MySQL to the host via `ports: ["3306:3306"]` (L12). Concrete manifestation: deploying via the docum …
  841 +- **反驳核验**:Read application-prod.yml, docker-compose.yml, and application.yml. The finding does not survive refutation as a genuine reachable defect. (1) application-prod.yml:8 `password: ${DB_PASS:trade123}` is the canonical Spring env-var-with-fallback idiom; the file header (lines 1-2) explicitly documents `环境变量可覆盖...DB_PASS` and that the prod profile is opt-in via `--spring.profiles.active=prod`. `trade123` is a documented, overridable placeholder, not an accidentally leaked secret. (2) In docker-compose.yml the MySQL container is PROVISIONED with `trade123` (L11) and the engine connects with the same `trade123` (L39) — a self-contained stack must state its DB password somewhere; this is the standa …
  842 +
  843 +### B15. toBig() swallows parse errors and returns ZERO, letting non-numeric dueDays bypass the >=0 check
  844 +
  845 +- **严重度**:LOW **维度**:correctness **位置**:`engine/src/main/java/com/onto/engine/command/CommandEngine.java:419`
  846 +- **缺陷**:toBig() catches every exception and returns BigDecimal.ZERO, so a non-numeric dueDays like "abc" yields signum()==0 and silently passes the 'dueDays 不可为负' validation instead of being rejected.
  847 +- **证据**:
  848 +```
  849 +private static BigDecimal toBig(Object o) {
  850 + try { return new BigDecimal(String.valueOf(o)); } catch (Exception e) { return BigDecimal.ZERO; }
  851 +}
  852 +// used at line 235: if (d != null && toBig(d).signum() < 0) errors.add("账期天数 dueDays 不可为负");
  853 +```
  854 +- **影响 / 触发场景**:An invalid dueDays value (e.g. "abc", "3d", "1,0") is treated as 0 and accepted by the validator, then crashes downstream at DynamicRepository.convert("int", "abc"). The swallowed exception hides malformed input and produces a wrong validation verdict rather than a clear rejection.
  855 +- **建议修复**:Treat unparseable numeric input as a validation error (return an errors entry) instead of silently mapping it to ZERO.
  856 +- **复现核验**:Evidence verified. CommandEngine.java:418-420 `toBig()` catches every exception and returns BigDecimal.ZERO. It is used at line 235 (`if (d != null && toBig(d).signum() < 0) errors.add("账期天数 dueDays 不可为负")`), inside the branch guarded by line 232 `cmdValidations.contains("dueDays")`, which is satisfied because M2-command.yaml:123 contains `- dueDays >= 0, depositRatio ∈ [0,1]`. dueDays is declared int in M1-domain.yaml:93-94 and mapped to due_days in M3-deploy.yaml:77.
  857 +
  858 +Concrete manifestation: submit SCENE_CREATE_ORDER with details.PaymentTerm[0].dueDays = "abc". In validate(), toBig("abc") swallows the NumberFormatException → returns ZERO → signum()==0 is not < 0 → no error added → validati …
  859 +- **反驳核验**:The mechanism is literally true (toBig("abc") returns BigDecimal.ZERO so signum()==0 skips the "dueDays 不可为负" error at CommandEngine.java:235), but the claimed correctness impact is neutralized on every path.
  860 +
  861 +(1) The line-232 check is a narrow NEGATIVITY gate derived from M2 text "dueDays >= 0" (models/M2-command.yaml:123); its sole job is to reject negatives. "abc" is genuinely not a negative number, so not adding a negativity error is semantically consistent. This engine has no per-field numeric-type validator for any field — type enforcement is delegated to the persist layer by design.
  862 +
  863 +(2) No data ever gets accepted. dueDays is M1 type int (models/M1-domain.yaml:93-94) persisted to due_ …
  864 +
  865 +### B16. deductStock() unguarded Long.parseLong crashes on NULL stock column (asymmetric with productStock)
  866 +
  867 +- **严重度**:LOW **维度**:correctness **位置**:`engine/src/main/java/com/onto/engine/event/EventEngine.java:105`
  868 +- **缺陷**:deductStock only checks row.isEmpty() before Long.parseLong(String.valueOf(row.get(stockCol))); if the stock column is NULL the value becomes the literal "null" and parseLong throws NumberFormatException inside the order transaction.
  869 +- **证据**:
  870 +```
  871 +Map<String, Object> row = repo.findByPk(table, pkCol, productId);
  872 +if (row.isEmpty()) { action.put("status", "SKIP_NOT_FOUND"); return action; }
  873 +long current = Long.parseLong(String.valueOf(row.get(stockCol))); // row.get(stockCol)==null -> "null" -> NumberFormatException
  874 +```
  875 +- **影响 / 触发场景**:CommandEngine.productStock() guards this exact case (returns null when row.get(stockCol)==null, so precheck's runRules silently skips the low-stock ALERT), but deductStock() does not, so precheck and execute diverge: for a product row with a NULL stock value, precheck passes cleanly while OrderCreated -> StockDeduct crashes with NumberFormatException, rolling back a valid order. Same crash if stockNum is later modeled as decimal in M1 (H2 returns BigDecimal -> String.valueOf("100.0000") -> parseLong fails). Trigger: any Product whose stock_num is NULL (e.g. created without stockNum, or column added post-hoc) referenced by an OrderItem.
  876 +- **建议修复**:Null-check row.get(stockCol) (mirror productStock's guard) and parse via BigDecimal(...).longValue() to tolerate decimal storage; treat missing/NULL stock as 0 or SKIP rather than throwing.
  877 +- **复现核验**:Evidence confirmed verbatim. EventEngine.java:101 guards only `if (row.isEmpty())`, then line 105 does `long current = Long.parseLong(String.valueOf(row.get(stockCol)));`. When the stock column is NULL, `String.valueOf((Object)null)` yields the literal "null" and `Long.parseLong("null")` throws NumberFormatException. The asymmetry is real: CommandEngine.java:292 `productStock()` guards `if (row.isEmpty() || row.get(stockCol) == null) return null;` whereas deductStock does not. There is even an internal inconsistency inside deductStock: line 92 strips decimals for qty via `.replaceAll("\\..*$","")` but line 105 does not for stock.
  878 +
  879 +Concrete reproduction (NULL path): DynamicRepository.insertDo …
  880 +- **反驳核验**:The syntactic asymmetry is real: EventEngine.deductStock() (engine/.../event/EventEngine.java:105) does `long current = Long.parseLong(String.valueOf(row.get(stockCol)));` guarding only `row.isEmpty()` (line 101), whereas CommandEngine.productStock() (engine/.../command/CommandEngine.java:292-293) additionally guards `row.get(stockCol) == null`. But the claimed runtime crash requires a Product row whose stock_num is NULL, and that state is unreachable in the current models. (1) No product-creation path exists: M4-scene.yaml defines only SCENE_CREATE_ORDER/SCENE_MODIFY_ORDER_ADDR/SCENE_CANCEL_ORDER (none create a product), and M8 has only customer/order/blank templates — no product template. …
  881 +
  882 +### B17. Required fields are visual-only; no client-side validation
  883 +
  884 +- **严重度**:LOW **维度**:frontend **位置**:`web/src/engine/SceneForm.tsx:160`
  885 +- **缺陷**:M8 'required' renders a red asterisk but the Form.Items have no name/rules, so submit and precheck never enforce required fields on the client.
  886 +- **证据**:
  887 +```
  888 +{fb.required && <Text type="danger"> *</Text>}
  889 +```
  890 +- **影响 / 触发场景**:The <Form layout="vertical"> is decorative: fields are tracked in manual master/singles/tables state with no antd Form binding, so doSubmit/doPrecheck fire even with empty required fields and rely entirely on the backend to reject. The asterisk implies client validation that does not exist (poorer UX, extra round-trips).
  891 +- **建议修复**:Before doPrecheck/doSubmit, validate required fields from schema against collected state and surface inline errors, or wire fields through antd Form name/rules.
  892 +- **复现核验**:Verified in web/src/engine/SceneForm.tsx. Line 160 renders the required asterisk (`{fb.required && <Text type="danger"> *</Text>}`) inside FieldLabel. Every Form.Item (lines 176, 197, and table columns 224-231) is declared as `<Form.Item label={...}>` with no `name` and no `rules`, so antd's Form store never tracks these fields and can perform no validation. Line 135's `<Form layout="vertical">` has no `form` instance and no `onFinish`; submission is a plain Button onClick={doSubmit} (line 147). Field values are held in manual useState (master/singles/tables, lines 38-40) via FieldControl value/onChange, bypassing antd Form entirely. Neither doSubmit (90-107) nor doPrecheck (79-88) contains …
  893 +- **反驳核验**:The code claims are literally accurate: SceneForm.tsx:160 renders `{fb.required && <Text type="danger"> *</Text>}`, and the Form.Items at lines 176/197/232 have no name/rules, so antd validation is never engaged; doSubmit (90) and doPrecheck (79) build the payload from manual master/singles/tables state (38-40) and POST without a client-side required check. However this is not a defect. (1) The project's IRON RULE makes models/*.yaml the single source of truth and the backend (MetaRule + M2 + M3) the authoritative generic validator; duplicating required-field logic in React would create rule-drift, precisely what the architecture avoids — delegation is intentional design. (2) A pre-submit va …
  894 +
  895 +### B18. eventPayload 仅从 master 取值,ME 中落在子实体/非领域字段的 payload 解析为 null
  896 +
  897 +- **严重度**:LOW **维度**:model-integrity **位置**:`engine/src/main/java/com/onto/engine/command/CommandEngine.java:380`
  898 +- **缺陷**:eventPayload 对每个 ME payload 字段只从 master 读取,凡是 payload 引用子实体字段(receiverPhone)或根本不是领域字段(productIdList)的事件,其 payload 值都会是 null。
  899 +- **证据**:
  900 +```
  901 +CommandEngine.java:378-380 遍历 def.payload,`payload.put(field, field.equals(rootIdField) ? rootId : master.get(field))`;ME-event.yaml:51 OrderDeliveryAddressModified `payload: [orderId, receiverPhone]`(receiverPhone 属 DeliveryAddressEntity 子实体,不在 master);55 OrderCancelled `payload: [orderId, productIdList]`(productIdList 非任何 M1 字段)。
  902 +```
  903 +- **影响 / 触发场景**:当前 CreateOrder 的 OrderCreated payload[orderId,customerId,totalAmount] 恰好都在 master 才侥幸正确;一旦启用 ModifyOrderDeliveryAddress/CancelOrder 等命令,其事件 payload 的子实体/聚合字段会全部为 null,事件内容失真。属 latent break。
  904 +- **建议修复**:eventPayload 应能从 master 与 details 子实体、乃至聚合上下文中按字段名解析 payload 值,而非仅 master.get(field)。
  905 +- **复现核验**:Evidence verified at the cited lines. CommandEngine.java:378-380 builds event payloads with `payload.put(field, field.equals(rootIdField) ? rootId : master.get(field))`, so every payload field except the root id is resolved solely from `master`. ME-event.yaml:51 declares OrderDeliveryAddressModified payload [orderId, receiverPhone] and ME-event.yaml:55 declares OrderCancelled payload [orderId, productIdList].
  906 +
  907 +I constructed a concrete, reachable manifestation. SCENE_CANCEL_ORDER is a live scene (M4-scene.yaml:32-40) exposed via POST /api/scene/{sceneId}/execute (CommandController.java:31-37). Permission order_cancel allows normal_user/backend_admin (M5-security.yaml:21-23), and default princ …
  908 +- **反驳核验**:Mechanically the observation is accurate: CommandEngine.java:378-380 resolves each ME payload field via `field.equals(rootIdField) ? rootId : master.get(field)`, so a child-entity or non-domain field yields null. But this is not a reachable defect in the system.
  909 +
  910 +(1) Not wired: the events OrderDeliveryAddressModified / OrderCancelled are only emitted by commands ModifyOrderDeliveryAddress / CancelOrder, reachable only via scenes SCENE_MODIFY_ORDER_ADDR / SCENE_CANCEL_ORDER. M8-front-schema.yaml binds templates ONLY to SCENE_CREATE_CUSTOMER (line 20) and SCENE_CREATE_ORDER (line 76); templateByScene (ModelRepository.java:262-268) returns emptyMap otherwise. The generic renderer refuses these: …
  911 +
  912 +
  913 +---
  914 +
  915 +## 附录 C · 被驳回(5 条)
  916 +
  917 +本组候选未通过复现验证,主要为把"文档态/演示简化"的既定设计误报为缺陷(如 M6/M7 层未接线属设计意图、动态 SQL 标识符来自可信 YAML 不可被用户注入等),或严重度被下调至可忽略。详见运行日志 journal.jsonl。
... ...
docs/审计整改说明.md 0 → 100644
  1 +++ a/docs/审计整改说明.md
  1 +# 审计整改说明(对应 `审计报告-2026-07-20.md`)
  2 +
  3 +本轮按"全面改造"口径整改。核心思路:**把散落在 Java/TS 里的业务字面量上移为可机器读的本体元数据**,
  4 +让引擎真正通用地解释执行。所有改动均加法、向后兼容。
  5 +
  6 +## 一、模型层新增的结构化元数据(加法)
  7 +
  8 +| 模型 | 新增字段 | 用途 |
  9 +|---|---|---|
  10 +| M1 | 属性 `constraints`(min/max/pattern/patternMessage) | 通用校验(取代中文子串匹配 + 硬编码字段) |
  11 +| M2 | 命令 `commandType`(create/update/cancel) | 通用分派(取代"一律新建")|
  12 +| M2 | 命令 `effect`(op/targetField/keyParam/amountParam/guardField) | 跨聚合副作用通用执行(取代硬编码扣库存)|
  13 +| M2 | 命令 `derivations`(如 totalAmount=sum(quantity*itemPrice)) | 服务端重算(防 totalAmount 篡改)|
  14 +| M2 | 新增 `StockReturn` 命令 | 取消订单回补库存(修正 ME 与语义相反)|
  15 +| ME | 跨聚合规则 `sourceItemEntity` + `paramMap` | 通用组装目标命令入参(取代硬编码 productId/quantity)|
  16 +| ME | 补齐 `OrderItemAdded/OrderPaymentTermSet/OrderDeliveryAddressSet/ProductStockReturned` | 修复 M2 引用了 ME 未定义的事件 |
  17 +| M4 | 新增 `SCENE_CREATE_CUSTOMER` | 修复 M8 悬空绑定;顺带证明"第二个聚合/场景零代码可跑" |
  18 +| M5 | 新增 `customer_create` 权限 | 配套客户场景 |
  19 +
  20 +## 二、按发现的整改(A 级 32 条 / B 级 18 条)
  21 +
  22 +### 铁律违背 → 全部改为模型驱动
  23 +- **A1/A13/A16/B8 跨聚合硬编码 StockDeduct/Product/stockNum/productId/OrderCreated**:`EventEngine.applyCrossConsistency` 现遍历**实际发出的事件**,按 ME `crossAggConsistencyRules` 匹配,用 M2 `effect` + `paramMap` **通用执行**任意增/减副作用;`CommandEngine` 不再传字面量 `"OrderCreated"`。
  24 +- **A2/A9/A10/A23 风控变量白名单 + customerLevel()/productStock() 专用方法**:删除这两个方法;`runRules` 现把 master/detail 全字段注入规则 env,并按 **M1 `refAggregate`** 通用解析被引用聚合属性(customerLevel、stockNum 自动可用)。新增引用其他字段的规则即生效。
  25 +- **A11/A12/A25 中文子串匹配 + 硬编码实体/字段校验**:删除;改由 **M1 `constraints`**(pattern/min/max)通用校验,遍历 master 与各子实体行。
  26 +- **A29 firstItemEntity/idPrefix "O for Order"**:删除 `firstItemEntity` 字面量兜底(改由 ME `sourceItemEntity` 指定);`idPrefix` 取聚合根名首字母(通用)。
  27 +- **A31 seed 硬编码 Customer/Product**:改为遍历 `seed-data.yaml` 的聚合 key 通用落库。
  28 +- **A7/A8/A16/A17/A21/A22/A28 前端硬编码**:`SceneForm` 用 `schema.sceneName` + `result.rootId`(后端新增稳定键 `rootId/rootIdField`);`TracePanel`/`OrdersEventsPanel` 列改由 `/api/meta/aggregate/{id}` 的 M1 属性与引擎通用动作形状驱动。
  29 +- **A19/A20 /orders/list 硬编码 OrderAggregate**:改为通用 `/api/data/{aggregateId}/list`;前端 `api.list(agg)`。
  30 +- **A30 前端默认场景硬编码**:改为取 `/meta/scenes` 首个 `hasTemplate` 场景。
  31 +- **EventEngine Outbox 表名硬编码**:改读 ME `outbox.table`。
  32 +
  33 +### 正确性 / 数据一致性
  34 +- **B1 超卖**:create 语义下,跨聚合动作任一非 OK(库存不足/不存在)→ `setRollbackOnly` 回滚整单,返回 `CONSISTENCY_FAILED`(已加测试:P002 库存8 下单100 → 失败且库存仍为8)。
  35 +- **A5 并发扣减竞争**:改为原子条件更新 `... SET col=col+? WHERE pk=? AND col+?>=0`,受影响 0 行即判不足。
  36 +- **A27 负数数量膨胀库存**:`effect` 执行拒绝 amount≤0;且 M1 `constraints.min=1` 在校验阶段拦截。
  37 +- **A4/B15/B16 非数字入 int/decimal / dueDays 绕过 / NULL 库存崩溃**:校验阶段按 M1 类型做数值可解析性校验(precheck 与 execute 同源);`readLong` 容错 NULL。
  38 +- **A6 reload 数据竞争**:`ModelRepository` 改为**不可变快照 + volatile 原子发布**,读取方永远看到完整旧/新快照。
  39 +- **A24 修改/取消被误当新建插入**:按 `commandType` 分派 create/update/cancel,不再对非新建命令插入新根。
  40 +- **A14/A32 ME OrderCancelled→StockDeduct 语义相反且从不触发**:新增 `StockReturn`(increment),ME 指向它;触发改为遍历实际事件,cancel 场景从库加载明细回补库存。
  41 +- **B18 事件 payload 仅取 master**:`eventPayload` 现 master 优先、其次子实体行查找。
  42 +- **B7 Map.of NPE**:改用可空 Map 组装(现由 paramMap 通用组装,容忍空值并 SKIP)。
  43 +
  44 +### 安全
  45 +- **A26 storageEncrypt 从未兑现**:新增 `EncryptService`(AES-GCM),落库前加密、读出后解密(M5 声明的字段)。**已加测试断言物理列为 `enc:` 密文**。
  46 +- **A15 totalAmount 客户端篡改**:M2 `derivations` 服务端按明细重算,喂给风控与落库。
  47 +- **B5 permitted 失败即放行**:改为 fail-closed(无 permissionBind 的场景默认拒绝)。
  48 +- **A3 X-Principal 可伪造 / 无认证**:坦诚标注为**演示态 stub**(`CommandController` Javadoc + README);热重载改为**仅管理员**(M8 dragRolePermission)可触发。*真正的 OAuth2/JWT 属生产接入项,见"仍为演示简化"。*
  49 +- **B12 CORS 全放行**:改为按 profile 配置 `onto.cors.allowed-origins`,生产收紧为具体域名。
  50 +- **B11/B14 H2 控制台无鉴权 / 弱默认口令**:生产 profile 已禁用 H2 控制台;加密密钥与 CORS 域名在 prod profile 要求经环境/密管注入(文档强调)。
  51 +
  52 +### 模型完整性
  53 +- **B3 M8 绑定未定义的 SCENE_CREATE_CUSTOMER**:M4 补齐该场景 + M5 权限,客户模板变为可跑。
  54 +- **B4 M2 引用 ME 未定义事件**:ME 补齐三事件定义。
  55 +- **B9 控件不随 M1 type 变**:`FieldControl` 在 compType 为通用文本时按 M1 `fieldType`(int/decimal/boolean/datetime)升级控件,兑现"改 M1 type→控件变"。
  56 +
  57 +## 三、仍为演示简化(诚实标注,未在本轮做成生产级)
  58 +
  59 +- **真实认证(A3)**:未接入 Spring Security + OAuth2/JWT;`X-Principal` 仅演示 M5 权限分支。生产必须替换。
  60 +- **幂等(B2)**:未加请求幂等键;生产应加唯一约束去重表。
  61 +- **M0 JSON-Schema 强校验(B10)**:M0 仍为规范文档,未在加载时对各层做 Schema 校验(各层命名 snake_case 与 M0 Identifier 模式的历史不一致亦未强改)。
  62 +- 生产重组件(MySQL/RocketMQ/SkyWalking/Prometheus)仍以 `deploy/` docker-compose + 文档形式交付。
  63 +
  64 +> **更新(补全 M8 后)**:M8 已补齐 `SCENE_CANCEL_ORDER`(订单取消模板)与 `SCENE_MODIFY_ORDER_ADDR`(收货地址修改模板)两套模板,四个场景全部可渲染。引擎相应把 `update` 命令做成**含子实体更新**(`updateChildByParent`,据 M3 定位子实体表按父外键更新),`cancel` 走通用 `StockReturn` 回补库存。已加测试:改地址断言子实体行被更新、取消断言库存回补。故原"update/cancel 子实体级修改未接线"一项已闭合。
  65 +
  66 +## 四、验证
  67 +- 后端 9 个 JUnit 全绿:schema 合成、有效下单(通用一致性)、大额 REJECT、约束校验、**超卖回滚**、负数数量拒绝、预检、**客户权限**、**手机号密文落库**。
  68 +- `tests/verify-model-change.sh`:改 MetaRule 阈值 + 热重载 → 同单通过↔拦截。
  69 +- 浏览器端到端:订单与客户两个场景均由 M8 动态渲染;执行追踪显示 derive/通用跨聚合动作/正确 topic。
... ...
engine/.mvn/wrapper/maven-wrapper.properties 0 → 100644
  1 +++ a/engine/.mvn/wrapper/maven-wrapper.properties
  1 +wrapperVersion=3.3.4
  2 +distributionType=only-script
  3 +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
... ...
engine/mvnw 0 → 100755
  1 +++ a/engine/mvnw
  1 +#!/bin/sh
  2 +# ----------------------------------------------------------------------------
  3 +# Licensed to the Apache Software Foundation (ASF) under one
  4 +# or more contributor license agreements. See the NOTICE file
  5 +# distributed with this work for additional information
  6 +# regarding copyright ownership. The ASF licenses this file
  7 +# to you under the Apache License, Version 2.0 (the
  8 +# "License"); you may not use this file except in compliance
  9 +# with the License. You may obtain a copy of the License at
  10 +#
  11 +# http://www.apache.org/licenses/LICENSE-2.0
  12 +#
  13 +# Unless required by applicable law or agreed to in writing,
  14 +# software distributed under the License is distributed on an
  15 +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  16 +# KIND, either express or implied. See the License for the
  17 +# specific language governing permissions and limitations
  18 +# under the License.
  19 +# ----------------------------------------------------------------------------
  20 +
  21 +# ----------------------------------------------------------------------------
  22 +# Apache Maven Wrapper startup batch script, version 3.3.4
  23 +#
  24 +# Optional ENV vars
  25 +# -----------------
  26 +# JAVA_HOME - location of a JDK home dir, required when download maven via java source
  27 +# MVNW_REPOURL - repo url base for downloading maven distribution
  28 +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
  29 +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
  30 +# ----------------------------------------------------------------------------
  31 +
  32 +set -euf
  33 +[ "${MVNW_VERBOSE-}" != debug ] || set -x
  34 +
  35 +# OS specific support.
  36 +native_path() { printf %s\\n "$1"; }
  37 +case "$(uname)" in
  38 +CYGWIN* | MINGW*)
  39 + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
  40 + native_path() { cygpath --path --windows "$1"; }
  41 + ;;
  42 +esac
  43 +
  44 +# set JAVACMD and JAVACCMD
  45 +set_java_home() {
  46 + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
  47 + if [ -n "${JAVA_HOME-}" ]; then
  48 + if [ -x "$JAVA_HOME/jre/sh/java" ]; then
  49 + # IBM's JDK on AIX uses strange locations for the executables
  50 + JAVACMD="$JAVA_HOME/jre/sh/java"
  51 + JAVACCMD="$JAVA_HOME/jre/sh/javac"
  52 + else
  53 + JAVACMD="$JAVA_HOME/bin/java"
  54 + JAVACCMD="$JAVA_HOME/bin/javac"
  55 +
  56 + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
  57 + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
  58 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
  59 + return 1
  60 + fi
  61 + fi
  62 + else
  63 + JAVACMD="$(
  64 + 'set' +e
  65 + 'unset' -f command 2>/dev/null
  66 + 'command' -v java
  67 + )" || :
  68 + JAVACCMD="$(
  69 + 'set' +e
  70 + 'unset' -f command 2>/dev/null
  71 + 'command' -v javac
  72 + )" || :
  73 +
  74 + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
  75 + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
  76 + return 1
  77 + fi
  78 + fi
  79 +}
  80 +
  81 +# hash string like Java String::hashCode
  82 +hash_string() {
  83 + str="${1:-}" h=0
  84 + while [ -n "$str" ]; do
  85 + char="${str%"${str#?}"}"
  86 + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
  87 + str="${str#?}"
  88 + done
  89 + printf %x\\n $h
  90 +}
  91 +
  92 +verbose() { :; }
  93 +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
  94 +
  95 +die() {
  96 + printf %s\\n "$1" >&2
  97 + exit 1
  98 +}
  99 +
  100 +trim() {
  101 + # MWRAPPER-139:
  102 + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
  103 + # Needed for removing poorly interpreted newline sequences when running in more
  104 + # exotic environments such as mingw bash on Windows.
  105 + printf "%s" "${1}" | tr -d '[:space:]'
  106 +}
  107 +
  108 +scriptDir="$(dirname "$0")"
  109 +scriptName="$(basename "$0")"
  110 +
  111 +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
  112 +while IFS="=" read -r key value; do
  113 + case "${key-}" in
  114 + distributionUrl) distributionUrl=$(trim "${value-}") ;;
  115 + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
  116 + esac
  117 +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
  118 +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
  119 +
  120 +case "${distributionUrl##*/}" in
  121 +maven-mvnd-*bin.*)
  122 + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
  123 + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
  124 + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
  125 + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
  126 + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
  127 + :Linux*x86_64*) distributionPlatform=linux-amd64 ;;
  128 + *)
  129 + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
  130 + distributionPlatform=linux-amd64
  131 + ;;
  132 + esac
  133 + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
  134 + ;;
  135 +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
  136 +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
  137 +esac
  138 +
  139 +# apply MVNW_REPOURL and calculate MAVEN_HOME
  140 +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
  141 +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
  142 +distributionUrlName="${distributionUrl##*/}"
  143 +distributionUrlNameMain="${distributionUrlName%.*}"
  144 +distributionUrlNameMain="${distributionUrlNameMain%-bin}"
  145 +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
  146 +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
  147 +
  148 +exec_maven() {
  149 + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
  150 + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
  151 +}
  152 +
  153 +if [ -d "$MAVEN_HOME" ]; then
  154 + verbose "found existing MAVEN_HOME at $MAVEN_HOME"
  155 + exec_maven "$@"
  156 +fi
  157 +
  158 +case "${distributionUrl-}" in
  159 +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
  160 +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
  161 +esac
  162 +
  163 +# prepare tmp dir
  164 +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
  165 + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
  166 + trap clean HUP INT TERM EXIT
  167 +else
  168 + die "cannot create temp dir"
  169 +fi
  170 +
  171 +mkdir -p -- "${MAVEN_HOME%/*}"
  172 +
  173 +# Download and Install Apache Maven
  174 +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
  175 +verbose "Downloading from: $distributionUrl"
  176 +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
  177 +
  178 +# select .zip or .tar.gz
  179 +if ! command -v unzip >/dev/null; then
  180 + distributionUrl="${distributionUrl%.zip}.tar.gz"
  181 + distributionUrlName="${distributionUrl##*/}"
  182 +fi
  183 +
  184 +# verbose opt
  185 +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
  186 +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
  187 +
  188 +# normalize http auth
  189 +case "${MVNW_PASSWORD:+has-password}" in
  190 +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
  191 +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
  192 +esac
  193 +
  194 +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
  195 + verbose "Found wget ... using wget"
  196 + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
  197 +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
  198 + verbose "Found curl ... using curl"
  199 + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
  200 +elif set_java_home; then
  201 + verbose "Falling back to use Java to download"
  202 + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
  203 + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
  204 + cat >"$javaSource" <<-END
  205 + public class Downloader extends java.net.Authenticator
  206 + {
  207 + protected java.net.PasswordAuthentication getPasswordAuthentication()
  208 + {
  209 + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
  210 + }
  211 + public static void main( String[] args ) throws Exception
  212 + {
  213 + setDefault( new Downloader() );
  214 + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
  215 + }
  216 + }
  217 + END
  218 + # For Cygwin/MinGW, switch paths to Windows format before running javac and java
  219 + verbose " - Compiling Downloader.java ..."
  220 + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
  221 + verbose " - Running Downloader.java ..."
  222 + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
  223 +fi
  224 +
  225 +# If specified, validate the SHA-256 sum of the Maven distribution zip file
  226 +if [ -n "${distributionSha256Sum-}" ]; then
  227 + distributionSha256Result=false
  228 + if [ "$MVN_CMD" = mvnd.sh ]; then
  229 + echo "Checksum validation is not supported for maven-mvnd." >&2
  230 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
  231 + exit 1
  232 + elif command -v sha256sum >/dev/null; then
  233 + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
  234 + distributionSha256Result=true
  235 + fi
  236 + elif command -v shasum >/dev/null; then
  237 + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
  238 + distributionSha256Result=true
  239 + fi
  240 + else
  241 + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
  242 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
  243 + exit 1
  244 + fi
  245 + if [ $distributionSha256Result = false ]; then
  246 + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
  247 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
  248 + exit 1
  249 + fi
  250 +fi
  251 +
  252 +# unzip and move
  253 +if command -v unzip >/dev/null; then
  254 + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
  255 +else
  256 + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
  257 +fi
  258 +
  259 +# Find the actual extracted directory name (handles snapshots where filename != directory name)
  260 +actualDistributionDir=""
  261 +
  262 +# First try the expected directory name (for regular distributions)
  263 +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
  264 + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
  265 + actualDistributionDir="$distributionUrlNameMain"
  266 + fi
  267 +fi
  268 +
  269 +# If not found, search for any directory with the Maven executable (for snapshots)
  270 +if [ -z "$actualDistributionDir" ]; then
  271 + # enable globbing to iterate over items
  272 + set +f
  273 + for dir in "$TMP_DOWNLOAD_DIR"/*; do
  274 + if [ -d "$dir" ]; then
  275 + if [ -f "$dir/bin/$MVN_CMD" ]; then
  276 + actualDistributionDir="$(basename "$dir")"
  277 + break
  278 + fi
  279 + fi
  280 + done
  281 + set -f
  282 +fi
  283 +
  284 +if [ -z "$actualDistributionDir" ]; then
  285 + verbose "Contents of $TMP_DOWNLOAD_DIR:"
  286 + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
  287 + die "Could not find Maven distribution directory in extracted archive"
  288 +fi
  289 +
  290 +verbose "Found extracted Maven distribution directory: $actualDistributionDir"
  291 +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
  292 +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
  293 +
  294 +clean || :
  295 +exec_maven "$@"
... ...
engine/mvnw.cmd 0 → 100644
  1 +++ a/engine/mvnw.cmd
  1 +<# : batch portion
  2 +@REM ----------------------------------------------------------------------------
  3 +@REM Licensed to the Apache Software Foundation (ASF) under one
  4 +@REM or more contributor license agreements. See the NOTICE file
  5 +@REM distributed with this work for additional information
  6 +@REM regarding copyright ownership. The ASF licenses this file
  7 +@REM to you under the Apache License, Version 2.0 (the
  8 +@REM "License"); you may not use this file except in compliance
  9 +@REM with the License. You may obtain a copy of the License at
  10 +@REM
  11 +@REM http://www.apache.org/licenses/LICENSE-2.0
  12 +@REM
  13 +@REM Unless required by applicable law or agreed to in writing,
  14 +@REM software distributed under the License is distributed on an
  15 +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  16 +@REM KIND, either express or implied. See the License for the
  17 +@REM specific language governing permissions and limitations
  18 +@REM under the License.
  19 +@REM ----------------------------------------------------------------------------
  20 +
  21 +@REM ----------------------------------------------------------------------------
  22 +@REM Apache Maven Wrapper startup batch script, version 3.3.4
  23 +@REM
  24 +@REM Optional ENV vars
  25 +@REM MVNW_REPOURL - repo url base for downloading maven distribution
  26 +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
  27 +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
  28 +@REM ----------------------------------------------------------------------------
  29 +
  30 +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
  31 +@SET __MVNW_CMD__=
  32 +@SET __MVNW_ERROR__=
  33 +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
  34 +@SET PSModulePath=
  35 +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
  36 + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
  37 +)
  38 +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
  39 +@SET __MVNW_PSMODULEP_SAVE=
  40 +@SET __MVNW_ARG0_NAME__=
  41 +@SET MVNW_USERNAME=
  42 +@SET MVNW_PASSWORD=
  43 +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
  44 +@echo Cannot start maven from wrapper >&2 && exit /b 1
  45 +@GOTO :EOF
  46 +: end batch / begin powershell #>
  47 +
  48 +$ErrorActionPreference = "Stop"
  49 +if ($env:MVNW_VERBOSE -eq "true") {
  50 + $VerbosePreference = "Continue"
  51 +}
  52 +
  53 +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
  54 +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
  55 +if (!$distributionUrl) {
  56 + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
  57 +}
  58 +
  59 +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
  60 + "maven-mvnd-*" {
  61 + $USE_MVND = $true
  62 + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
  63 + $MVN_CMD = "mvnd.cmd"
  64 + break
  65 + }
  66 + default {
  67 + $USE_MVND = $false
  68 + $MVN_CMD = $script -replace '^mvnw','mvn'
  69 + break
  70 + }
  71 +}
  72 +
  73 +# apply MVNW_REPOURL and calculate MAVEN_HOME
  74 +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
  75 +if ($env:MVNW_REPOURL) {
  76 + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
  77 + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
  78 +}
  79 +$distributionUrlName = $distributionUrl -replace '^.*/',''
  80 +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
  81 +
  82 +$MAVEN_M2_PATH = "$HOME/.m2"
  83 +if ($env:MAVEN_USER_HOME) {
  84 + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
  85 +}
  86 +
  87 +if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
  88 + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
  89 +}
  90 +
  91 +$MAVEN_WRAPPER_DISTS = $null
  92 +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
  93 + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
  94 +} else {
  95 + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
  96 +}
  97 +
  98 +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
  99 +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
  100 +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
  101 +
  102 +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
  103 + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
  104 + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
  105 + exit $?
  106 +}
  107 +
  108 +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
  109 + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
  110 +}
  111 +
  112 +# prepare tmp dir
  113 +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
  114 +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
  115 +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
  116 +trap {
  117 + if ($TMP_DOWNLOAD_DIR.Exists) {
  118 + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
  119 + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
  120 + }
  121 +}
  122 +
  123 +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
  124 +
  125 +# Download and Install Apache Maven
  126 +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
  127 +Write-Verbose "Downloading from: $distributionUrl"
  128 +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
  129 +
  130 +$webclient = New-Object System.Net.WebClient
  131 +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
  132 + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
  133 +}
  134 +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
  135 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
  136 +
  137 +# If specified, validate the SHA-256 sum of the Maven distribution zip file
  138 +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
  139 +if ($distributionSha256Sum) {
  140 + if ($USE_MVND) {
  141 + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
  142 + }
  143 + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
  144 + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
  145 + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
  146 + }
  147 +}
  148 +
  149 +# unzip and move
  150 +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
  151 +
  152 +# Find the actual extracted directory name (handles snapshots where filename != directory name)
  153 +$actualDistributionDir = ""
  154 +
  155 +# First try the expected directory name (for regular distributions)
  156 +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
  157 +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
  158 +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
  159 + $actualDistributionDir = $distributionUrlNameMain
  160 +}
  161 +
  162 +# If not found, search for any directory with the Maven executable (for snapshots)
  163 +if (!$actualDistributionDir) {
  164 + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
  165 + $testPath = Join-Path $_.FullName "bin/$MVN_CMD"
  166 + if (Test-Path -Path $testPath -PathType Leaf) {
  167 + $actualDistributionDir = $_.Name
  168 + }
  169 + }
  170 +}
  171 +
  172 +if (!$actualDistributionDir) {
  173 + Write-Error "Could not find Maven distribution directory in extracted archive"
  174 +}
  175 +
  176 +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
  177 +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
  178 +try {
  179 + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
  180 +} catch {
  181 + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
  182 + Write-Error "fail to move MAVEN_HOME"
  183 + }
  184 +} finally {
  185 + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
  186 + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
  187 +}
  188 +
  189 +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
... ...
engine/pom.xml 0 → 100644
  1 +++ a/engine/pom.xml
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<project xmlns="http://maven.apache.org/POM/4.0.0"
  3 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  5 + <modelVersion>4.0.0</modelVersion>
  6 +
  7 + <parent>
  8 + <groupId>org.springframework.boot</groupId>
  9 + <artifactId>spring-boot-starter-parent</artifactId>
  10 + <version>3.5.6</version>
  11 + <relativePath/>
  12 + </parent>
  13 +
  14 + <groupId>com.onto</groupId>
  15 + <artifactId>onto-engine</artifactId>
  16 + <version>2.0.0</version>
  17 + <name>onto-engine</name>
  18 + <description>本体驱动 DDD+EDA 元数据解释引擎 —— 通用命令执行 + 动态渲染元数据服务</description>
  19 +
  20 + <properties>
  21 + <!-- 编译目标 Java 21 字节码,可在本机 JDK 25 上运行 -->
  22 + <java.version>21</java.version>
  23 + <maven.compiler.release>21</maven.compiler.release>
  24 + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  25 + </properties>
  26 +
  27 + <dependencies>
  28 + <dependency>
  29 + <groupId>org.springframework.boot</groupId>
  30 + <artifactId>spring-boot-starter-web</artifactId>
  31 + </dependency>
  32 + <dependency>
  33 + <groupId>org.springframework.boot</groupId>
  34 + <artifactId>spring-boot-starter-jdbc</artifactId>
  35 + </dependency>
  36 + <!-- H2 内存库(MySQL 兼容模式),零外部依赖开箱即跑 -->
  37 + <dependency>
  38 + <groupId>com.h2database</groupId>
  39 + <artifactId>h2</artifactId>
  40 + <scope>runtime</scope>
  41 + </dependency>
  42 + <!-- 生产切换 MySQL8 时启用(deploy/ 生产 profile) -->
  43 + <dependency>
  44 + <groupId>com.mysql</groupId>
  45 + <artifactId>mysql-connector-j</artifactId>
  46 + <scope>runtime</scope>
  47 + </dependency>
  48 + <!-- SnakeYAML:解析 M0-M8 / MetaRule 本体模型 -->
  49 + <dependency>
  50 + <groupId>org.yaml</groupId>
  51 + <artifactId>snakeyaml</artifactId>
  52 + </dependency>
  53 + <!-- Aviator:MetaRule 动态规则引擎(前后端同一份规则表达式) -->
  54 + <dependency>
  55 + <groupId>com.googlecode.aviator</groupId>
  56 + <artifactId>aviator</artifactId>
  57 + <version>5.4.3</version>
  58 + </dependency>
  59 + <dependency>
  60 + <groupId>org.springframework.boot</groupId>
  61 + <artifactId>spring-boot-starter-test</artifactId>
  62 + <scope>test</scope>
  63 + </dependency>
  64 + </dependencies>
  65 +
  66 + <build>
  67 + <finalName>onto-engine</finalName>
  68 + <plugins>
  69 + <plugin>
  70 + <groupId>org.springframework.boot</groupId>
  71 + <artifactId>spring-boot-maven-plugin</artifactId>
  72 + </plugin>
  73 + </plugins>
  74 + </build>
  75 +</project>
... ...
engine/src/main/java/com/onto/engine/EngineApplication.java 0 → 100644
  1 +++ a/engine/src/main/java/com/onto/engine/EngineApplication.java
  1 +package com.onto.engine;
  2 +
  3 +import org.springframework.boot.SpringApplication;
  4 +import org.springframework.boot.autoconfigure.SpringBootApplication;
  5 +import org.springframework.scheduling.annotation.EnableScheduling;
  6 +
  7 +/**
  8 + * 本体驱动 DDD+EDA 元数据解释引擎入口。
  9 + *
  10 + * <p>本引擎不包含任何"订单/客户/商品"的专用业务代码。它在启动时加载 models/ 下的 11 份
  11 + * 本体 YAML(M0-M8、MetaRule),构建内存元模型,并以通用方式:
  12 + * <ul>
  13 + * <li>据 M3 自动建表;</li>
  14 + * <li>据 M8+M1+M2+M5+MetaRule 合成前端渲染 Schema;</li>
  15 + * <li>据 M4 flowSteps + M2 命令定义 + MetaRule 通用解释执行命令;</li>
  16 + * <li>据 ME 事件定义 + 跨聚合一致性规则驱动 Outbox 事件与库存扣减。</li>
  17 + * </ul>
  18 + * 修改任意 YAML 后调用 POST /api/meta/reload(或重启)即可让行为随之改变,无需改一行 Java。
  19 + */
  20 +@SpringBootApplication
  21 +@EnableScheduling
  22 +public class EngineApplication {
  23 + public static void main(String[] args) {
  24 + SpringApplication.run(EngineApplication.class, args);
  25 + }
  26 +}
... ...
engine/src/main/java/com/onto/engine/command/CommandEngine.java 0 → 100644
  1 +++ a/engine/src/main/java/com/onto/engine/command/CommandEngine.java
  1 +package com.onto.engine.command;
  2 +
  3 +import com.onto.engine.event.EventEngine;
  4 +import com.onto.engine.model.ModelRepository;
  5 +import com.onto.engine.persist.DynamicRepository;
  6 +import com.onto.engine.rule.RuleEngine;
  7 +import com.onto.engine.rule.RuleHit;
  8 +import com.onto.engine.security.EncryptService;
  9 +import com.onto.engine.util.IdGen;
  10 +import org.slf4j.Logger;
  11 +import org.slf4j.LoggerFactory;
  12 +import org.springframework.stereotype.Service;
  13 +import org.springframework.transaction.annotation.Transactional;
  14 +import org.springframework.transaction.interceptor.TransactionAspectSupport;
  15 +
  16 +import java.math.BigDecimal;
  17 +import java.time.LocalDateTime;
  18 +import java.util.*;
  19 +import java.util.regex.Pattern;
  20 +
  21 +/**
  22 + * 通用命令执行器:不含任何具体聚合/字段的业务字面量。它读取 M4 场景 flowSteps、M2 命令定义
  23 + * (commandType / validations / derivations / effect)、M8 模板结构、M1 字段类型与约束/引用、
  24 + * M3 落库映射、MetaRule 规则、ME 事件与跨聚合规则,通用地"解释执行"任意场景命令。
  25 + *
  26 + * <p>解释流程:权限(M5) → readOnlyCheck(引用存在) → 派生(M2 derivations 服务端重算) →
  27 + * 校验(M8 required + M1 类型/约束) → 风控(MetaRule,变量按 M1 引用通用解析) →
  28 + * 按 commandType 落库/更新 → 发事件(ME) → 跨聚合最终一致(ME 规则 + M2 effect)。
  29 + */
  30 +@Service
  31 +public class CommandEngine {
  32 +
  33 + private static final Logger log = LoggerFactory.getLogger(CommandEngine.class);
  34 +
  35 + private final ModelRepository models;
  36 + private final DynamicRepository repo;
  37 + private final RuleEngine ruleEngine;
  38 + private final EventEngine eventEngine;
  39 + private final EncryptService encrypt;
  40 +
  41 + public CommandEngine(ModelRepository models, DynamicRepository repo,
  42 + RuleEngine ruleEngine, EventEngine eventEngine, EncryptService encrypt) {
  43 + this.models = models;
  44 + this.repo = repo;
  45 + this.ruleEngine = ruleEngine;
  46 + this.eventEngine = eventEngine;
  47 + this.encrypt = encrypt;
  48 + }
  49 +
  50 + /** 提交前预检:派生 + 校验 + 风控(不落库)。 */
  51 + public Map<String, Object> precheck(String sceneId, Map<String, Object> payload) {
  52 + Ctx ctx = new Ctx(sceneId, payload);
  53 + applyDerivations(ctx);
  54 + Map<String, Object> out = new LinkedHashMap<>();
  55 + List<String> errors = validate(ctx);
  56 + List<RuleHit> hits = runRules(ctx);
  57 + List<Map<String, Object>> rejects = new ArrayList<>();
  58 + List<Map<String, Object>> warns = new ArrayList<>();
  59 + for (RuleHit h : hits) (("REJECT".equals(h.effect())) ? rejects : warns).add(ruleToMap(h));
  60 + out.put("reject", !errors.isEmpty() || !rejects.isEmpty());
  61 + out.put("errors", errors);
  62 + out.put("rejectRules", rejects);
  63 + out.put("warnings", warns);
  64 + out.put("derivedMaster", ctx.master); // 让前端看到服务端重算后的值
  65 + return out;
  66 + }
  67 +
  68 + /** 完整执行场景命令。 */
  69 + @Transactional
  70 + public Map<String, Object> execute(String sceneId, Map<String, Object> payload, String principal) {
  71 + Ctx ctx = new Ctx(sceneId, payload);
  72 + List<Map<String, Object>> trace = new ArrayList<>();
  73 + Map<String, Object> result = new LinkedHashMap<>();
  74 + result.put("sceneId", sceneId);
  75 + result.put("trace", trace);
  76 +
  77 + if (!permitted(ctx.scene, principal)) {
  78 + return fail(result, "PERMISSION", "主体 " + principal + " 无权执行场景 " + sceneId);
  79 + }
  80 + trace.add(step("permission", "M5", "主体 " + principal + " 通过权限校验 " + ctx.scene.get("permissionBind")));
  81 +
  82 + Run run = new Run(result, trace, principal, ctx);
  83 + Flow flow = runSteps(asList(ctx.scene.get("flowSteps")), run);
  84 + if (flow == Flow.FAILED) return result; // result 已带 success=false
  85 + result.put("success", true);
  86 + return result;
  87 + }
  88 +
  89 + // ===================== 通用步骤解释器(M4 flowSteps;递归,支持控制流/Saga/EDA) =====================
  90 + //
  91 + // 所有 stepType 均按 M4 配置"通用解释",不含任何具体聚合/字段字面量。控制流步骤(condition/switch/
  92 + // parallel/while/loopItems/retry)携带嵌套子步骤,故用递归 runSteps 取代原先的扁平 switch。
  93 + // 演示简化:parallel 顺序执行后汇合;timer/waitEvent/callService 不做真实阻塞/外呼;retry 无瞬时故障注入。
  94 +
  95 + private enum Flow { CONTINUE, RETURNED, FAILED }
  96 +
  97 + /** 一次场景执行的运行态:结果、轨迹、Saga 补偿栈、主体、上下文、循环游标。 */
  98 + private final class Run {
  99 + final Map<String, Object> result;
  100 + final List<Map<String, Object>> trace;
  101 + final String principal;
  102 + final Ctx ctx;
  103 + final Deque<String[]> compensations = new ArrayDeque<>(); // 已提交步骤的 {aggregate, command},逆序补偿
  104 + Map<String, Object> loopItem; // loopItems 当前行,供表达式读取
  105 + Run(Map<String, Object> result, List<Map<String, Object>> trace, String principal, Ctx ctx) {
  106 + this.result = result; this.trace = trace; this.principal = principal; this.ctx = ctx;
  107 + }
  108 + }
  109 +
  110 + /** 顺序解释一串步骤;遇 return/end 停止,遇失败停止(失败前已触发补偿)。 */
  111 + private Flow runSteps(List<Object> steps, Run run) {
  112 + for (Object st : steps) {
  113 + Flow f = runStep(asMap(st), run);
  114 + if (f != Flow.CONTINUE) return f;
  115 + }
  116 + return Flow.CONTINUE;
  117 + }
  118 +
  119 + /** 单步分派:每种 stepType 的通用解释。 */
  120 + private Flow runStep(Map<String, Object> stp, Run run) {
  121 + String type = String.valueOf(stp.get("stepType"));
  122 + switch (type) {
  123 + // —— 起止 ——
  124 + case "start" -> run.trace.add(step("start", "M4",
  125 + "场景开始" + (stp.get("triggerType") == null ? "" : "(触发:" + stp.get("triggerType") + ")")));
  126 + case "return", "end" -> {
  127 + run.trace.add(step(type, "M4", "场景执行完成,返回结果"));
  128 + return Flow.RETURNED;
  129 + }
  130 + case "errorEnd" -> {
  131 + return failFlow(run, "ERROR_END", String.valueOf(stp.getOrDefault("message", "场景异常结束")));
  132 + }
  133 +
  134 + // —— 数据/校验/派生 ——
  135 + case "readOnlyCheck" -> {
  136 + String agg = String.valueOf(stp.get("bindAggregate"));
  137 + Map<String, Object> err = readOnlyCheck(agg, run.ctx);
  138 + if (err != null) return failFlow(run, "READ_ONLY_CHECK", String.valueOf(err.get("message")));
  139 + run.trace.add(step("readOnlyCheck", "M4", "校验 " + agg + " 引用存在:通过"));
  140 + }
  141 + case "validate" -> {
  142 + List<String> errors = validate(run.ctx);
  143 + if (!errors.isEmpty()) return failFlow(run, "VALIDATION", String.join(";", errors));
  144 + run.trace.add(step("validate", "M2/M8/M1", "独立校验步骤通过"));
  145 + }
  146 + case "derive" -> {
  147 + List<String> changed = applyDerivations(run.ctx);
  148 + run.trace.add(step("derive", "M2", "服务端重算字段 " + changed));
  149 + }
  150 + case "assign", "transform" -> doAssign(stp, run);
  151 + case "script" -> doScript(stp, run);
  152 +
  153 + // —— 命令/事件(EDA) ——
  154 + case "bindCommand" -> {
  155 + Map<String, Object> cmdResult = executeCommand(
  156 + String.valueOf(stp.get("bindAggregate")), String.valueOf(stp.get("bindCommand")), run.ctx, run.trace);
  157 + run.result.putAll(cmdResult);
  158 + if (Boolean.FALSE.equals(cmdResult.get("success"))) {
  159 + run.result.put("success", false);
  160 + return compensateAndFail(run); // 触发 Saga 补偿并失败
  161 + }
  162 + if (stp.get("compensateCommand") != null) { // 登记该步骤的逆向补偿命令
  163 + run.compensations.push(new String[]{
  164 + String.valueOf(stp.getOrDefault("compensateAggregate", stp.get("bindAggregate"))),
  165 + String.valueOf(stp.get("compensateCommand"))});
  166 + }
  167 + }
  168 + case "callScene" -> {
  169 + Flow f = callScene(String.valueOf(stp.get("callScene")), run);
  170 + if (f == Flow.FAILED) return Flow.FAILED;
  171 + }
  172 + case "callService" -> run.trace.add(step("callService", "M4",
  173 + "调用外部服务 " + stp.getOrDefault("method", "") + " "
  174 + + stp.getOrDefault("endpoint", stp.get("serviceRef")) + "(演示:记录意图,不实际外呼)"));
  175 + case "emitEvent" -> doEmitEvent(stp, run);
  176 + case "waitEvent" -> {
  177 + String ev = String.valueOf(stp.get("waitEvent"));
  178 + boolean arrived = eventEngine.recentOutbox(50).stream()
  179 + .anyMatch(o -> o.values().stream().anyMatch(v -> ev.equals(String.valueOf(v))));
  180 + run.trace.add(step("waitEvent", "ME",
  181 + "等待事件 " + ev + ":" + (arrived ? "已到达" : "未到达(演示:不阻塞,继续)")));
  182 + }
  183 +
  184 + // —— 控制流 ——
  185 + case "condition" -> {
  186 + boolean ok = ruleEngine.evalBool(String.valueOf(stp.get("when")), exprEnv(run));
  187 + run.trace.add(step("condition", "M4", "条件 [" + stp.get("when") + "] = " + ok));
  188 + Flow f = runSteps(asList(stp.get(ok ? "then" : "else")), run);
  189 + if (f != Flow.CONTINUE) return f;
  190 + }
  191 + case "switch" -> {
  192 + Object val = ruleEngine.evalValue(String.valueOf(stp.get("switchOn")), exprEnv(run));
  193 + List<Object> chosen = null;
  194 + for (Object c : asList(stp.get("cases"))) {
  195 + Map<String, Object> cm = asMap(c);
  196 + if (String.valueOf(cm.get("equals")).equals(String.valueOf(val))) { chosen = asList(cm.get("steps")); break; }
  197 + }
  198 + if (chosen == null) chosen = asList(stp.get("default"));
  199 + run.trace.add(step("switch", "M4", "分派 on=" + val));
  200 + Flow f = runSteps(chosen, run);
  201 + if (f != Flow.CONTINUE) return f;
  202 + }
  203 + case "parallel" -> {
  204 + List<Object> branches = asList(stp.get("branches"));
  205 + run.trace.add(step("parallel", "M4", "并行分支 " + branches.size() + " 条(演示:顺序执行后汇合)"));
  206 + for (Object b : branches) {
  207 + Flow f = runSteps(asList(asMap(b).get("steps")), run);
  208 + if (f == Flow.FAILED) return Flow.FAILED; // 任一分支失败即整体失败(补偿已在失败点触发)
  209 + }
  210 + }
  211 + case "while" -> {
  212 + int max = intOf(stp.get("maxIterations"), 1000), i = 0;
  213 + while (ruleEngine.evalBool(String.valueOf(stp.get("when")), exprEnv(run))) {
  214 + if (i++ >= max) { run.trace.add(step("while", "M4", "达到迭代上限 " + max + ",中止")); break; }
  215 + Flow f = runSteps(asList(stp.get("body")), run);
  216 + if (f != Flow.CONTINUE) return f;
  217 + }
  218 + run.trace.add(step("while", "M4", "循环结束,迭代 " + i + " 次"));
  219 + }
  220 + case "loopItems" -> {
  221 + String entity = String.valueOf(stp.get("itemsFrom"));
  222 + List<Map<String, Object>> rows = run.ctx.details.getOrDefault(entity, List.of());
  223 + run.trace.add(step("loopItems", "M4", "遍历子实体 " + entity + " 共 " + rows.size() + " 行"));
  224 + List<Object> body = asList(stp.get("body"));
  225 + for (Map<String, Object> row : rows) {
  226 + run.loopItem = row;
  227 + Flow f = runSteps(body, run);
  228 + run.loopItem = null;
  229 + if (f != Flow.CONTINUE) return f;
  230 + }
  231 + }
  232 +
  233 + // —— Saga / 可靠性 ——
  234 + case "retry" -> {
  235 + int max = intOf(stp.get("maxAttempts"), 2);
  236 + List<Object> body = asList(stp.get("body"));
  237 + Flow f = Flow.CONTINUE;
  238 + for (int a = 1; a <= max; a++) {
  239 + f = runSteps(body, run);
  240 + if (f != Flow.FAILED) break;
  241 + run.trace.add(step("retry", "M4", "第 " + a + " 次尝试失败" + (a < max ? ",重试" : ",已达上限")));
  242 + }
  243 + if (f != Flow.CONTINUE) return f;
  244 + }
  245 + case "compensate" -> {
  246 + run.trace.add(step("compensate", "M4/Saga", "显式补偿:逆序执行 " + run.compensations.size() + " 项"));
  247 + runCompensations(run);
  248 + }
  249 + case "timer" -> run.trace.add(step("timer", "M4",
  250 + "定时/延时 " + stp.getOrDefault("delayMs", stp.getOrDefault("timeoutMs", 0)) + "ms(演示:不实际阻塞)"));
  251 + case "userTask" -> {
  252 + Object who = stp.getOrDefault("assignee", stp.getOrDefault("role", "人工"));
  253 + String approveParam = stp.get("approveParam") == null ? null : String.valueOf(stp.get("approveParam"));
  254 + if (approveParam != null && !truthy(run.ctx.master.get(approveParam)))
  255 + return failFlow(run, "PENDING_APPROVAL", "待 " + who + " 审批(缺少 " + approveParam + "=true)");
  256 + run.trace.add(step("userTask", "M4", who + " 审批通过"));
  257 + }
  258 +
  259 + default -> run.trace.add(step(type, "M4", "跳过未识别步骤"));
  260 + }
  261 + return Flow.CONTINUE;
  262 + }
  263 +
  264 + /** 设置失败信息并触发补偿,返回 FAILED。 */
  265 + private Flow failFlow(Run run, String stage, String message) {
  266 + fail(run.result, stage, message);
  267 + return compensateAndFail(run);
  268 + }
  269 +
  270 + private Flow compensateAndFail(Run run) {
  271 + runCompensations(run);
  272 + return Flow.FAILED;
  273 + }
  274 +
  275 + /** 逆序执行已登记的补偿命令(Saga)。演示简化:与本地事务共存,回滚时补偿亦回滚。 */
  276 + private void runCompensations(Run run) {
  277 + while (!run.compensations.isEmpty()) {
  278 + String[] c = run.compensations.pop();
  279 + Map<String, Object> r = executeCommand(c[0], c[1], run.ctx, run.trace);
  280 + run.trace.add(step("compensate", "M4/Saga",
  281 + "补偿命令 " + c[1] + "@" + c[0] + " → " + (Boolean.FALSE.equals(r.get("success")) ? "失败" : "已执行")));
  282 + }
  283 + }
  284 +
  285 + /** 调用子场景:校验其权限,复用当前 master/details 作为入参,共享 trace/结果/补偿栈的独立上下文。 */
  286 + private Flow callScene(String subSceneId, Run run) {
  287 + Map<String, Object> subScene = models.scene(subSceneId);
  288 + if (subScene.isEmpty()) return failFlow(run, "SCENE_NOT_FOUND", "子场景不存在:" + subSceneId);
  289 + if (!permitted(subScene, run.principal))
  290 + return failFlow(run, "PERMISSION", "主体 " + run.principal + " 无权执行子场景 " + subSceneId);
  291 + run.trace.add(step("callScene", "M4", "进入子场景 " + subSceneId));
  292 + Map<String, Object> subPayload = new LinkedHashMap<>();
  293 + subPayload.put("master", run.ctx.master);
  294 + subPayload.put("details", run.ctx.details);
  295 + Run subRun = new Run(run.result, run.trace, run.principal, new Ctx(subSceneId, subPayload));
  296 + return runSteps(asList(subScene.get("flowSteps")), subRun);
  297 + }
  298 +
  299 + /** assign/transform:按表达式给 master 字段赋值(支持 assignments 列表或 targetField/expression 简写)。 */
  300 + private void doAssign(Map<String, Object> stp, Run run) {
  301 + List<Object> assigns = new ArrayList<>(asList(stp.get("assignments")));
  302 + if (stp.get("targetField") != null)
  303 + assigns.add(Map.of("targetField", stp.get("targetField"), "expression", String.valueOf(stp.get("expression"))));
  304 + List<String> changed = new ArrayList<>();
  305 + for (Object a : assigns) {
  306 + Map<String, Object> am = asMap(a);
  307 + String tf = String.valueOf(am.get("targetField"));
  308 + run.ctx.master.put(tf, ruleEngine.evalValue(String.valueOf(am.get("expression")), exprEnv(run)));
  309 + changed.add(tf);
  310 + }
  311 + run.trace.add(step(String.valueOf(stp.get("stepType")), "M4", "赋值/转换字段 " + changed));
  312 + }
  313 +
  314 + /** script:求值一段表达式,可选写回字段。 */
  315 + private void doScript(Map<String, Object> stp, Run run) {
  316 + Object v = ruleEngine.evalValue(String.valueOf(stp.get("expression")), exprEnv(run));
  317 + if (stp.get("assignTo") != null) run.ctx.master.put(String.valueOf(stp.get("assignTo")), v);
  318 + run.trace.add(step("script", "M4", "脚本求值 [" + stp.get("expression") + "] = " + v));
  319 + }
  320 +
  321 + /** emitEvent:显式发布一个领域事件(payload 通用地从 master/details 组装)。 */
  322 + private void doEmitEvent(Map<String, Object> stp, Run run) {
  323 + String agg = String.valueOf(stp.getOrDefault("bindAggregate", run.ctx.cmdAggregate));
  324 + String ev = String.valueOf(stp.get("emitEvent"));
  325 + String idField = models.rootIdentifier(agg);
  326 + Object rootId = run.ctx.master.getOrDefault(idField, run.result.get("rootId"));
  327 + eventEngine.append(agg, ev, eventPayload(agg, ev, String.valueOf(rootId), run.ctx.master, run.ctx.details));
  328 + run.trace.add(step("emitEvent", "ME", "发布事件 " + ev));
  329 + }
  330 +
  331 + /** 表达式上下文:master 扁平 + 按 M1 引用解析的被引用聚合属性 + 循环行 + 稳定键 rootId。 */
  332 + private Map<String, Object> exprEnv(Run run) {
  333 + Map<String, Object> env = new HashMap<>(run.ctx.master);
  334 + if (run.ctx.cmdAggregate != null) mergeReferences(env, run.ctx.cmdAggregate, null, run.ctx.master);
  335 + if (run.loopItem != null) env.putAll(run.loopItem);
  336 + if (run.result.get("rootId") != null) env.put("rootId", run.result.get("rootId"));
  337 + return env;
  338 + }
  339 +
  340 + private static boolean truthy(Object o) {
  341 + if (o == null) return false;
  342 + if (o instanceof Boolean b) return b;
  343 + String s = String.valueOf(o).trim();
  344 + return s.equalsIgnoreCase("true") || s.equals("1") || s.equalsIgnoreCase("yes");
  345 + }
  346 +
  347 + private static int intOf(Object o, int def) {
  348 + try { return Integer.parseInt(String.valueOf(o).trim()); } catch (Exception e) { return def; }
  349 + }
  350 +
  351 + // ===================== 命令执行(M2 bizSteps 指令程序通用解释) =====================
  352 +
  353 + private Map<String, Object> executeCommand(String aggregateId, String cmdId, Ctx ctx,
  354 + List<Map<String, Object>> trace) {
  355 + Map<String, Object> cmd = models.command(aggregateId, cmdId);
  356 + Map<String, Object> out = new LinkedHashMap<>();
  357 +
  358 + // 派生(服务端重算,如 totalAmount = sum(quantity*itemPrice))
  359 + List<String> derived = applyDerivations(ctx);
  360 + if (!derived.isEmpty()) trace.add(step("derive", "M2", "服务端重算字段 " + derived + "(防客户端篡改)"));
  361 +
  362 + // 校验(M8 required + M1 类型/约束)
  363 + List<String> errors = validate(ctx);
  364 + if (!errors.isEmpty()) {
  365 + out.put("success", false);
  366 + out.put("stage", "VALIDATION");
  367 + out.put("message", String.join(";", errors));
  368 + out.put("errors", errors);
  369 + return out;
  370 + }
  371 + trace.add(step("validate", "M2/M8/M1", "命令 " + cmdId + " 入参校验通过"));
  372 +
  373 + // 风控(MetaRule;变量按 M1 引用通用解析)
  374 + List<RuleHit> hits = runRules(ctx);
  375 + List<Map<String, Object>> appliedRules = new ArrayList<>();
  376 + List<Map<String, Object>> warnings = new ArrayList<>();
  377 + for (RuleHit h : hits) {
  378 + appliedRules.add(ruleToMap(h));
  379 + if ("REJECT".equals(h.effect())) {
  380 + out.put("success", false);
  381 + out.put("stage", "RULE_REJECT");
  382 + out.put("message", h.message());
  383 + out.put("appliedRules", appliedRules);
  384 + return out;
  385 + }
  386 + if ("ALERT".equals(h.effect())) warnings.add(ruleToMap(h));
  387 + }
  388 + trace.add(step("metaRule", "MetaRule", "风控评估完成,命中 " + hits.size()
  389 + + " 条;REJECT=0,ALERT=" + warnings.size()));
  390 +
  391 + // 命令落库:由 M2 bizSteps 指令程序通用解释执行(替代硬编码 create/update/cancel 分派)
  392 + Map<String, Object> body = runProgram(aggregateId, cmdId, ctx, trace);
  393 + if (Boolean.FALSE.equals(body.get("success"))) return body;
  394 +
  395 + body.put("command", cmdId);
  396 + body.put("commandType", models.commandType(aggregateId, cmdId));
  397 + body.put("appliedRules", appliedRules);
  398 + body.put("warnings", warnings);
  399 + return body;
  400 + }
  401 +
  402 + // ===================== 命令级 bizSteps 指令解释器(替代硬编码 create/update/cancel) =====================
  403 + //
  404 + // M2 命令的 bizSteps 是一段“闭指令集”程序(AI 转换产物,type 只能取下列枚举之一),引擎逐条通用解释,
  405 + // 不含任何具体聚合/字段字面量:
  406 + // LOAD_AGGREGATE / SET_FIELD / AUTO_FILL_FIELD / REMOTE_QUERY_SET_FIELD /
  407 + // CONDITION_SET_FIELD / CALL_OTHER_COMMAND / COMMIT_TRANSACTION
  408 + // 未声明 bizSteps(或仍是旧的字符串描述)的命令,按 commandType 合成等价规范程序,仍走同一台解释器。
  409 + // 落库后统一:发事件(M2 emitEvents) → 跨聚合最终一致(ME 规则 + 指令内 CALL 动作) → create 语义超卖回滚。
  410 +
  411 + /** 一次命令的解释运行态:待落库的聚合根、是否已加载(update/cancel)、跨聚合动作、失败即中止。 */
  412 + private final class Prog {
  413 + final String aggregateId, cmdId, rootName, rootIdField;
  414 + final Ctx ctx;
  415 + final List<Map<String, Object>> trace;
  416 + final Map<String, Object> work; // 待落库的聚合根字段(初始=入参 master)
  417 + Map<String, Object> loadedRow = Collections.emptyMap(); // LOAD 读到的库中现值(供 root.* 表达式读取)
  418 + boolean loaded = false;
  419 + Object rootId;
  420 + int childCount = 0;
  421 + final List<Map<String, Object>> crossActions = new ArrayList<>();
  422 + Map<String, Object> failure; // 非空表示中止并返回该结果
  423 +
  424 + Prog(String aggregateId, String cmdId, Ctx ctx, List<Map<String, Object>> trace) {
  425 + this.aggregateId = aggregateId; this.cmdId = cmdId; this.ctx = ctx; this.trace = trace;
  426 + this.rootName = models.rootName(aggregateId);
  427 + this.rootIdField = models.rootIdentifier(aggregateId);
  428 + this.work = new LinkedHashMap<>(ctx.master);
  429 + this.rootId = ctx.master.get(rootIdField);
  430 + }
  431 + }
  432 +
  433 + /** 解释执行命令的 bizSteps 指令程序,落库后完成发事件与跨聚合一致。 */
  434 + private Map<String, Object> runProgram(String aggregateId, String cmdId, Ctx ctx,
  435 + List<Map<String, Object>> trace) {
  436 + Prog p = new Prog(aggregateId, cmdId, ctx, trace);
  437 + runInstructions(commandProgram(aggregateId, cmdId), p);
  438 + if (p.failure != null) return p.failure;
  439 + return finalizeCommand(p);
  440 + }
  441 +
  442 + /** 顺序解释一串指令(IF 携带的 then/else 子指令经此递归执行);任一步置 failure 即中止。 */
  443 + private void runInstructions(List<Object> steps, Prog p) {
  444 + for (Object insObj : steps) {
  445 + if (p.failure != null) return;
  446 + Map<String, Object> ins = asMap(insObj);
  447 + // 每步可写作 {stepNo, desc, instruction:{type, params}}(AI 转换产物)或扁平 {type, params}(合成/嵌套)
  448 + Map<String, Object> instr = ins.containsKey("instruction") ? asMap(ins.get("instruction")) : ins;
  449 + runInstruction(String.valueOf(instr.get("type")), asMap(instr.get("params")), p);
  450 + }
  451 + }
  452 +
  453 + /** 取命令显式 bizSteps 指令程序;缺省或仍是字符串描述时,按 commandType 合成规范程序。 */
  454 + private List<Object> commandProgram(String aggregateId, String cmdId) {
  455 + Object steps = models.command(aggregateId, cmdId).get("bizSteps");
  456 + if (steps instanceof List<?> list && !list.isEmpty()) return new ArrayList<>(list);
  457 + // 合成默认程序:update/cancel 先 LOAD 校验存在,再 COMMIT;create 直接 COMMIT。
  458 + String type = models.commandType(aggregateId, cmdId);
  459 + List<Object> prog = new ArrayList<>();
  460 + if ("update".equals(type) || "cancel".equals(type)) {
  461 + prog.add(Map.of("type", "LOAD_AGGREGATE",
  462 + "params", Map.of("idSource", "input." + models.rootIdentifier(aggregateId))));
  463 + }
  464 + prog.add(Map.of("type", "COMMIT_TRANSACTION", "params", Map.of()));
  465 + return prog;
  466 + }
  467 +
  468 + /** 单条指令分派:每种 type 的通用解释(无任何聚合/字段字面量)。 */
  469 + private void runInstruction(String type, Map<String, Object> params, Prog p) {
  470 + switch (type) {
  471 + case "LOAD_AGGREGATE" -> opLoadAggregate(params, p);
  472 + case "SET_FIELD" -> opSetField(params, p);
  473 + case "AUTO_FILL_FIELD" -> opAutoFill(params, p);
  474 + case "REMOTE_QUERY_SET_FIELD" -> opRemoteQuery(params, p);
  475 + case "CONDITION_SET_FIELD" -> opConditionSet(params, p);
  476 + case "CALL_OTHER_COMMAND" -> opCallOther(params, p);
  477 + case "COMMIT_TRANSACTION" -> opCommit(params, p);
  478 + case "IF" -> opIf(params, p);
  479 + case "REMOTE_SET_FIELD" -> opRemoteSet(params, p);
  480 + case "REMOTE_INCREMENT_FIELD" -> opRemoteIncrement(params, p);
  481 + default -> p.trace.add(step(type, "M2", "跳过未识别指令"));
  482 + }
  483 + }
  484 +
  485 + /** LOAD_AGGREGATE:按 idSource 定位并加载既有聚合根(不存在即失败),标记为 update/cancel 语义。 */
  486 + private void opLoadAggregate(Map<String, Object> params, Prog p) {
  487 + Object id = resolveRef(String.valueOf(params.get("idSource")), p);
  488 + if (id == null || !repo.existsByPk(repo.tableName(p.aggregateId, p.rootName),
  489 + repo.pkColumn(p.aggregateId, p.rootName), id)) {
  490 + p.failure = fail(new LinkedHashMap<>(), "NOT_FOUND",
  491 + "待操作的 " + p.rootName + " 不存在:" + p.rootIdField + "=" + id);
  492 + return;
  493 + }
  494 + p.rootId = id;
  495 + p.loaded = true;
  496 + p.loadedRow = repo.loadDomainRow(p.aggregateId, p.rootName, id);
  497 + p.work.put(p.rootIdField, id);
  498 + p.trace.add(step("LOAD_AGGREGATE", "M2/M3", "加载 " + p.rootName + "(" + id + ")"));
  499 + }
  500 +
  501 + /** SET_FIELD:把 source(input.* / root.* / 字面量)或 value 赋给目标字段。 */
  502 + private void opSetField(Map<String, Object> params, Prog p) {
  503 + String tf = String.valueOf(params.get("targetField"));
  504 + Object v = params.containsKey("source")
  505 + ? resolveRef(String.valueOf(params.get("source")), p) : params.get("value");
  506 + p.work.put(tf, v);
  507 + p.trace.add(step("SET_FIELD", "M2", tf + " = " + v));
  508 + }
  509 +
  510 + /** AUTO_FILL_FIELD:按 func 自动生成值(now_datetime / now_date / gen_id)。 */
  511 + private void opAutoFill(Map<String, Object> params, Prog p) {
  512 + String tf = String.valueOf(params.get("targetField"));
  513 + String func = String.valueOf(params.get("func"));
  514 + Object v = switch (func) {
  515 + case "now_datetime" -> LocalDateTime.now().toString();
  516 + case "now_date" -> java.time.LocalDate.now().toString();
  517 + case "gen_id" -> IdGen.next(idPrefix(p.aggregateId));
  518 + default -> null;
  519 + };
  520 + p.work.put(tf, v);
  521 + p.trace.add(step("AUTO_FILL_FIELD", "M2", tf + " = " + v + "(" + func + ")"));
  522 + }
  523 +
  524 + /** REMOTE_QUERY_SET_FIELD:按 queryKey 远程查另一聚合根,取 sourceField 回填 targetField。 */
  525 + private void opRemoteQuery(Map<String, Object> params, Prog p) {
  526 + String remoteAgg = String.valueOf(params.get("remoteAggregate"));
  527 + Object key = resolveRef(String.valueOf(params.get("queryKey")), p);
  528 + String sourceField = String.valueOf(params.get("sourceField"));
  529 + String tf = String.valueOf(params.get("targetField"));
  530 + Map<String, Object> row = repo.loadDomainRow(remoteAgg, models.rootName(remoteAgg), key);
  531 + Object v = row.get(sourceField);
  532 + p.work.put(tf, v);
  533 + p.trace.add(step("REMOTE_QUERY_SET_FIELD", "M2/M3",
  534 + "查 " + remoteAgg + "(" + key + ")." + sourceField + " → " + tf + " = " + v));
  535 + }
  536 +
  537 + /** CONDITION_SET_FIELD:按 conditions 顺序取首个命中的 when(Aviator 布尔),赋对应 value。 */
  538 + private void opConditionSet(Map<String, Object> params, Prog p) {
  539 + String tf = String.valueOf(params.get("targetField"));
  540 + for (Object c : asList(params.get("conditions"))) {
  541 + Map<String, Object> cm = asMap(c);
  542 + String when = String.valueOf(cm.get("when"));
  543 + if (ruleEngine.evalBool(rewriteRefs(when), condEnv(p))) {
  544 + p.work.put(tf, cm.get("value"));
  545 + p.trace.add(step("CONDITION_SET_FIELD", "M2", tf + " = " + cm.get("value") + "(命中 " + when + ")"));
  546 + return;
  547 + }
  548 + }
  549 + p.trace.add(step("CONDITION_SET_FIELD", "M2", tf + " 无条件命中,保持不变"));
  550 + }
  551 +
  552 + /** IF:对 when(Aviator,input./root. 命名空间)求值,真跑 then 子指令、假跑 else 子指令(可嵌套,类比 M4 condition)。 */
  553 + private void opIf(Map<String, Object> params, Prog p) {
  554 + boolean ok = ruleEngine.evalBool(rewriteRefs(String.valueOf(params.get("when"))), condEnv(p));
  555 + p.trace.add(step("IF", "M2", "条件 [" + params.get("when") + "] = " + ok));
  556 + runInstructions(asList(params.get(ok ? "then" : "else")), p);
  557 + }
  558 +
  559 + /** REMOTE_SET_FIELD:按 keySource 定位被引用聚合根,把 source(input./root./字面量) 或 value 覆盖写入其 targetField。 */
  560 + private void opRemoteSet(Map<String, Object> params, Prog p) {
  561 + String remoteAgg = String.valueOf(params.get("remoteAggregate"));
  562 + Object key = resolveRef(String.valueOf(params.get("keySource")), p);
  563 + String tf = String.valueOf(params.get("targetField"));
  564 + Object v = params.containsKey("source") ? resolveRef(String.valueOf(params.get("source")), p) : params.get("value");
  565 + Map<String, Object> vals = new LinkedHashMap<>();
  566 + vals.put(tf, v);
  567 + int n = key == null ? 0 : repo.updateDomain(remoteAgg, models.rootName(remoteAgg), null, vals,
  568 + models.rootIdentifier(remoteAgg), key);
  569 + String status = n > 0 ? "OK" : "SKIP_NOT_FOUND";
  570 + p.crossActions.add(remoteAction(remoteAgg, "set", tf, key, v, status, null));
  571 + p.trace.add(step("REMOTE_SET_FIELD", "M2/M3",
  572 + "覆盖 " + remoteAgg + "(" + key + ")." + tf + " = " + v + "(" + status + ")"));
  573 + }
  574 +
  575 + /** REMOTE_INCREMENT_FIELD:按 keySource 定位被引用聚合根,对数值 targetField 原子 +amount(缺省 1)。 */
  576 + private void opRemoteIncrement(Map<String, Object> params, Prog p) {
  577 + String remoteAgg = String.valueOf(params.get("remoteAggregate"));
  578 + Object key = resolveRef(String.valueOf(params.get("keySource")), p);
  579 + String tf = String.valueOf(params.get("targetField"));
  580 + long amount = intOf(params.getOrDefault("amount", 1), 1);
  581 + String rootName = models.rootName(remoteAgg);
  582 + String table = repo.tableName(remoteAgg, rootName);
  583 + String col = repo.fieldMap(remoteAgg, rootName).getOrDefault(tf, tf);
  584 + String pkCol = repo.pkColumn(remoteAgg, rootName);
  585 + int n = key == null ? 0 : repo.adjustColumn(table, col, pkCol, key, amount, false);
  586 + Long remaining = key == null ? null : repo.readLong(table, col, pkCol, key);
  587 + String status = n > 0 ? "OK" : "SKIP_NOT_FOUND";
  588 + p.crossActions.add(remoteAction(remoteAgg, "increment", tf, key, amount, status, remaining));
  589 + p.trace.add(step("REMOTE_INCREMENT_FIELD", "M2/M3",
  590 + remoteAgg + "(" + key + ")." + tf + " += " + amount + " → " + remaining + "(" + status + ")"));
  591 + }
  592 +
  593 + /** 组装一条跨聚合写回动作(供前端「跨聚合一致」表格通用展示)。 */
  594 + private Map<String, Object> remoteAction(String agg, String op, String field, Object key,
  595 + Object amountOrValue, String status, Long remaining) {
  596 + Map<String, Object> a = new LinkedHashMap<>();
  597 + a.put("targetAggregate", agg);
  598 + a.put("targetCommand", "-");
  599 + a.put("op", op);
  600 + a.put("keyField", models.rootIdentifier(agg));
  601 + a.put("key", key);
  602 + a.put("field", field);
  603 + a.put("amount", amountOrValue);
  604 + a.put("status", status);
  605 + a.put("remaining", remaining);
  606 + return a;
  607 + }
  608 +
  609 + /** CALL_OTHER_COMMAND:调另一聚合根命令的结构化 effect(可 forEach 子实体批量);onFailure=rollback 时失败即回滚。 */
  610 + private void opCallOther(Map<String, Object> params, Prog p) {
  611 + String targetAgg = String.valueOf(params.getOrDefault("targetAggregate", params.get("aggregate")));
  612 + String targetCmd = String.valueOf(params.getOrDefault("targetCommand", params.get("command")));
  613 + Map<String, Object> paramMap = asMap(params.get("paramMap"));
  614 + boolean rollback = "rollback".equals(String.valueOf(params.get("onFailure")));
  615 + List<Map<String, Object>> rows;
  616 + if (params.get("forEach") != null) {
  617 + String entity = String.valueOf(params.get("forEach"));
  618 + rows = p.ctx.details.get(entity);
  619 + if ((rows == null || rows.isEmpty()) && p.rootId != null)
  620 + rows = repo.loadChildRows(p.aggregateId, entity, p.rootId);
  621 + if (rows == null) rows = List.of();
  622 + } else {
  623 + rows = List.of(p.work);
  624 + }
  625 + for (Map<String, Object> row : rows) {
  626 + Map<String, Object> callParams = new LinkedHashMap<>();
  627 + for (var pm : paramMap.entrySet()) callParams.put(pm.getKey(), row.get(String.valueOf(pm.getValue())));
  628 + Map<String, Object> action = eventEngine.applyCommandEffect(targetAgg, targetCmd, callParams);
  629 + p.crossActions.add(action);
  630 + if (rollback && !"OK".equals(action.get("status"))) {
  631 + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
  632 + p.failure = fail(new LinkedHashMap<>(), "CONSISTENCY_FAILED",
  633 + "跨聚合调用 " + targetCmd + "@" + targetAgg + " 失败,已回滚:" + action);
  634 + p.failure.put("stockActions", p.crossActions);
  635 + return;
  636 + }
  637 + }
  638 + p.trace.add(step("CALL_OTHER_COMMAND", "M2/ME", "调用 " + targetCmd + "@" + targetAgg + " ×" + rows.size()));
  639 + }
  640 +
  641 + /** COMMIT_TRANSACTION:把工作聚合根落库——已加载则 update(含子实体),否则 insert(生成主键 + 组合子实体)。 */
  642 + private void opCommit(Map<String, Object> params, Prog p) {
  643 + if (p.loaded) {
  644 + int n = repo.updateDomain(p.aggregateId, p.rootName, null, p.work, p.rootIdField, p.rootId);
  645 + int childUpdated = 0;
  646 + for (var e : p.ctx.details.entrySet())
  647 + for (Map<String, Object> row : e.getValue())
  648 + childUpdated += repo.updateChildByParent(p.aggregateId, e.getKey(), p.rootId, row);
  649 + p.trace.add(step("COMMIT_TRANSACTION", "M3",
  650 + "更新 " + p.rootName + "(" + p.rootId + ") 根字段 " + n + " 列,子实体 " + childUpdated + " 行"));
  651 + } else {
  652 + if (isBlank(p.rootId)) p.rootId = IdGen.next(idPrefix(p.aggregateId));
  653 + p.work.put(p.rootIdField, p.rootId);
  654 + p.work.putIfAbsent("createTime", LocalDateTime.now().toString());
  655 + repo.insertDomain(p.aggregateId, p.rootName, null, p.work, null);
  656 + String rootPkColumn = repo.pkColumn(p.aggregateId, p.rootName);
  657 + for (String entity : models.compositionChildren(p.aggregateId)) {
  658 + String localId = String.valueOf(models.entity(p.aggregateId, entity).get("localIdentifier"));
  659 + for (Map<String, Object> row : p.ctx.details.getOrDefault(entity, List.of())) {
  660 + Map<String, Object> r = new LinkedHashMap<>(row);
  661 + r.put(localId, IdGen.next("D"));
  662 + repo.insertDomain(p.aggregateId, entity, entity, r, Map.of(rootPkColumn, p.rootId));
  663 + p.childCount++;
  664 + }
  665 + }
  666 + p.trace.add(step("COMMIT_TRANSACTION", "M3",
  667 + "写入 " + p.rootName + "(" + p.rootId + ") 及 " + p.childCount + " 条子实体"));
  668 + }
  669 + }
  670 +
  671 + /** 落库后统一收尾:发事件 → 跨聚合一致(ME 规则 + 指令内 CALL)→ create 超卖回滚 → 组装稳定返回键。 */
  672 + private Map<String, Object> finalizeCommand(Prog p) {
  673 + List<Map<String, Object>> events =
  674 + emitEvents(p.aggregateId, p.cmdId, String.valueOf(p.rootId), p.work, p.ctx.details);
  675 + p.trace.add(step("emitEvents", "ME", "写入 Outbox 事件 " + events.size() + " 条"));
  676 +
  677 + List<Map<String, Object>> actions = new ArrayList<>(p.crossActions);
  678 + actions.addAll(triggerConsistency(events, p.aggregateId, p.rootId, p.ctx.details));
  679 +
  680 + // 超卖防护:create 语义下任一 ME 一致动作非 OK(库存不足/商品不存在)即回滚整个事务
  681 + if ("create".equals(models.commandType(p.aggregateId, p.cmdId))) {
  682 + for (Map<String, Object> a : actions) {
  683 + if (!"OK".equals(a.get("status"))) {
  684 + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
  685 + Map<String, Object> out = fail(new LinkedHashMap<>(), "CONSISTENCY_FAILED",
  686 + "跨聚合一致性失败(如库存不足/目标不存在),已回滚:" + a);
  687 + out.put("stockActions", actions);
  688 + p.trace.add(step("crossConsistency", "ME", "一致性失败 → 事务回滚"));
  689 + return out;
  690 + }
  691 + }
  692 + }
  693 + if (!actions.isEmpty())
  694 + p.trace.add(step("crossConsistency", "ME", "跨聚合一致动作 " + actions.size() + " 项"));
  695 +
  696 + Map<String, Object> out = new LinkedHashMap<>();
  697 + out.put("success", true);
  698 + out.put("rootId", p.rootId);
  699 + out.put("rootIdField", p.rootIdField);
  700 + out.put(p.rootIdField, p.rootId);
  701 + out.put("aggregateId", p.aggregateId);
  702 + out.put("childCount", p.childCount);
  703 + out.put("events", events);
  704 + out.put("stockActions", actions);
  705 + return out;
  706 + }
  707 +
  708 + /** 解析引用:input.<param> → 入参;root.<field> → 工作聚合(优先 work,其次库中现值);否则字面量。 */
  709 + private Object resolveRef(String ref, Prog p) {
  710 + if (ref == null) return null;
  711 + if (ref.startsWith("input.")) return p.ctx.master.get(ref.substring(6));
  712 + if (ref.startsWith("root.")) {
  713 + String f = ref.substring(5);
  714 + return p.work.containsKey(f) ? p.work.get(f) : p.loadedRow.get(f);
  715 + }
  716 + return ref;
  717 + }
  718 +
  719 + /** 条件表达式变量环境:入参(input__ 及扁平) + 工作聚合与库中现值(root__)。 */
  720 + private Map<String, Object> condEnv(Prog p) {
  721 + Map<String, Object> env = new HashMap<>();
  722 + p.loadedRow.forEach((k, v) -> env.put("root__" + k, v));
  723 + p.work.forEach((k, v) -> env.put("root__" + k, v));
  724 + p.ctx.master.forEach((k, v) -> { env.put("input__" + k, v); env.putIfAbsent(k, v); });
  725 + return env;
  726 + }
  727 +
  728 + /** 把表达式里的 input./root. 命名空间改写为 Aviator 合法变量名(与 condEnv 对应)。 */
  729 + private static String rewriteRefs(String expr) {
  730 + if (expr == null) return "";
  731 + return expr.replaceAll("\\binput\\.", "input__").replaceAll("\\broot\\.", "root__");
  732 + }
  733 +
  734 + private List<Map<String, Object>> emitEvents(String aggregateId, String cmdId, String rootId,
  735 + Map<String, Object> master,
  736 + Map<String, List<Map<String, Object>>> details) {
  737 + List<Map<String, Object>> events = new ArrayList<>();
  738 + for (Object evName : asList(models.command(aggregateId, cmdId).get("emitEvents"))) {
  739 + events.add(eventEngine.append(aggregateId, String.valueOf(evName),
  740 + eventPayload(aggregateId, String.valueOf(evName), rootId, master, details)));
  741 + }
  742 + return events;
  743 + }
  744 +
  745 + /** 遍历实际发出的事件,逐个触发匹配的 ME 跨聚合一致性规则。 */
  746 + private List<Map<String, Object>> triggerConsistency(List<Map<String, Object>> events, String aggregateId,
  747 + Object rootId,
  748 + Map<String, List<Map<String, Object>>> details) {
  749 + List<Map<String, Object>> actions = new ArrayList<>();
  750 + for (Map<String, Object> ev : events) {
  751 + actions.addAll(eventEngine.applyCrossConsistency(
  752 + String.valueOf(ev.get("eventName")), aggregateId, rootId, details));
  753 + }
  754 + return actions;
  755 + }
  756 +
  757 + // ===================== 派生 / 校验 / 风控(全部模型驱动) =====================
  758 +
  759 + /** 应用 M2 derivations:如 totalAmount = sum(quantity*itemPrice)。返回被重算的字段名。 */
  760 + private List<String> applyDerivations(Ctx ctx) {
  761 + List<String> changed = new ArrayList<>();
  762 + for (Object d : models.commandDerivations(ctx.cmdAggregate, ctx.cmdId)) {
  763 + Map<String, Object> dm = asMap(d);
  764 + String targetField = String.valueOf(dm.get("targetField"));
  765 + String sourceEntity = String.valueOf(dm.get("sourceEntity"));
  766 + String agg = String.valueOf(dm.get("aggregate"));
  767 + String expr = String.valueOf(dm.get("expression"));
  768 + if ("sum".equals(agg)) {
  769 + BigDecimal total = BigDecimal.ZERO;
  770 + for (Map<String, Object> row : ctx.details.getOrDefault(sourceEntity, List.of())) {
  771 + total = total.add(ruleEngine.evalNumber(expr, row));
  772 + }
  773 + ctx.master.put(targetField, total);
  774 + changed.add(targetField);
  775 + }
  776 + }
  777 + return changed;
  778 + }
  779 +
  780 + private List<String> validate(Ctx ctx) {
  781 + List<String> errors = new ArrayList<>();
  782 + // 必填(源自 M8 required)
  783 + for (RequiredField rf : ctx.requiredFields) {
  784 + if (rf.master) {
  785 + if (isBlank(ctx.master.get(rf.field))) errors.add("缺少必填项:" + rf.label);
  786 + } else {
  787 + List<Map<String, Object>> rows = ctx.details.getOrDefault(rf.entity, List.of());
  788 + if (rows.isEmpty()) errors.add("缺少必填子实体:" + rf.entity);
  789 + else for (int i = 0; i < rows.size(); i++)
  790 + if (isBlank(rows.get(i).get(rf.field)))
  791 + errors.add(rf.entity + " 第" + (i + 1) + "行缺少必填项:" + rf.label);
  792 + }
  793 + }
  794 + // 类型 + 约束(源自 M1,通用遍历 master 与各子实体行)
  795 + validateFields(errors, ctx.cmdAggregate, null, ctx.master, "");
  796 + for (var e : ctx.details.entrySet()) {
  797 + List<Map<String, Object>> rows = e.getValue();
  798 + for (int i = 0; i < rows.size(); i++)
  799 + validateFields(errors, ctx.cmdAggregate, e.getKey(), rows.get(i), e.getKey() + " 第" + (i + 1) + "行 ");
  800 + }
  801 + // 唯一性约束(跨行,需查库;update 时排除自身)
  802 + checkUnique(ctx, errors);
  803 + return errors;
  804 + }
  805 +
  806 + /** 校验 master 中带 constraints.unique 的字段在聚合根表内全局唯一(据 M3 定位列,加密字段用确定性密文比对)。 */
  807 + private void checkUnique(Ctx ctx, List<String> errors) {
  808 + String agg = ctx.cmdAggregate;
  809 + if (agg == null) return;
  810 + String rootName = models.rootName(agg);
  811 + boolean forUpdate = "update".equals(models.commandType(agg, ctx.cmdId));
  812 + Object pkValue = forUpdate ? ctx.master.get(models.rootIdentifier(agg)) : null;
  813 + for (var e : ctx.master.entrySet()) {
  814 + if (isBlank(e.getValue())) continue;
  815 + Map<String, Object> c = models.attributeConstraints(agg, null, e.getKey());
  816 + if (!Boolean.TRUE.equals(c.get("unique"))) continue;
  817 + String col = repo.fieldMap(agg, rootName).get(e.getKey());
  818 + if (col == null) continue;
  819 + Object qv = DynamicRepository.convert(models.fieldType(agg, null, e.getKey()), e.getValue());
  820 + if (models.encryptFields().contains(e.getKey())) qv = encrypt.encrypt(qv);
  821 + if (repo.existsOther(repo.tableName(agg, rootName), col, qv,
  822 + repo.pkColumn(agg, rootName), pkValue)) {
  823 + errors.add(e.getKey() + " 已存在,需全局唯一:" + e.getValue());
  824 + }
  825 + }
  826 + }
  827 +
  828 + private void validateFields(List<String> errors, String agg, String entity,
  829 + Map<String, Object> data, String prefix) {
  830 + for (var e : data.entrySet()) {
  831 + String field = e.getKey();
  832 + Object value = e.getValue();
  833 + if (isBlank(value)) continue;
  834 + String type = models.fieldType(agg, entity, field);
  835 + boolean numeric = "int".equals(type) || "decimal".equals(type);
  836 + BigDecimal num = null;
  837 + if (numeric) {
  838 + num = tryBig(value);
  839 + if (num == null) { errors.add(prefix + field + " 需为数字:" + value); continue; }
  840 + }
  841 + Map<String, Object> c = models.attributeConstraints(agg, entity, field);
  842 + if (c.isEmpty()) continue;
  843 + if (c.get("min") != null && num != null && num.compareTo(tryBig(c.get("min"))) < 0)
  844 + errors.add(prefix + field + " 不可小于 " + c.get("min"));
  845 + if (c.get("max") != null && num != null && num.compareTo(tryBig(c.get("max"))) > 0)
  846 + errors.add(prefix + field + " 不可大于 " + c.get("max"));
  847 + if (c.get("exclusiveMin") != null && num != null && num.compareTo(tryBig(c.get("exclusiveMin"))) <= 0)
  848 + errors.add(prefix + field + " 必须大于 " + c.get("exclusiveMin"));
  849 + if (c.get("exclusiveMax") != null && num != null && num.compareTo(tryBig(c.get("exclusiveMax"))) >= 0)
  850 + errors.add(prefix + field + " 必须小于 " + c.get("exclusiveMax"));
  851 + if (c.get("pattern") != null && !Pattern.matches(String.valueOf(c.get("pattern")), String.valueOf(value)))
  852 + errors.add(prefix + String.valueOf(c.getOrDefault("patternMessage", field + " 格式不合法")) + ":" + value);
  853 + }
  854 + }
  855 +
  856 + private List<RuleHit> runRules(Ctx ctx) {
  857 + List<RuleHit> hits = new ArrayList<>();
  858 + // 命令级:master 扁平 + 按 M1 引用解析出的被引用聚合属性(如客户等级)
  859 + Map<String, Object> env = new HashMap<>(ctx.master);
  860 + mergeReferences(env, ctx.cmdAggregate, null, ctx.master);
  861 + hits.addAll(ruleEngine.evaluate(ctx.sceneId, env));
  862 + // 逐子实体行:行字段 + 被引用聚合属性(如商品库存)
  863 + Set<String> seen = new HashSet<>();
  864 + for (var e : ctx.details.entrySet()) {
  865 + String entity = e.getKey();
  866 + List<Map<String, Object>> rows = e.getValue();
  867 + for (int i = 0; i < rows.size(); i++) {
  868 + Map<String, Object> rowEnv = new HashMap<>(rows.get(i));
  869 + mergeReferences(rowEnv, ctx.cmdAggregate, entity, rows.get(i));
  870 + for (RuleHit h : ruleEngine.evaluate(ctx.sceneId, rowEnv))
  871 + if (seen.add(h.ruleId() + "|" + entity + i)) hits.add(h);
  872 + }
  873 + }
  874 + return hits;
  875 + }
  876 +
  877 + /** 把 data 中"跨聚合引用字段"所指向聚合的属性并入 env(据 M1 refAggregate/refRoot,通用)。 */
  878 + private void mergeReferences(Map<String, Object> env, String agg, String entity, Map<String, Object> data) {
  879 + for (var e : data.entrySet()) {
  880 + Map<String, Object> ref = models.refInfo(agg, entity, e.getKey());
  881 + if (ref.isEmpty() || e.getValue() == null) continue;
  882 + Map<String, Object> refRow = repo.loadDomainRow(
  883 + String.valueOf(ref.get("refAggregate")), String.valueOf(ref.get("refRoot")), e.getValue());
  884 + refRow.forEach(env::putIfAbsent);
  885 + }
  886 + }
  887 +
  888 + // ===================== readOnlyCheck =====================
  889 +
  890 + private Map<String, Object> readOnlyCheck(String aggregateId, Ctx ctx) {
  891 + String idField = models.rootIdentifier(aggregateId);
  892 + Object idValue = ctx.master.get(idField);
  893 + if (idValue == null) return Map.of("message", "缺少引用键 " + idField);
  894 + String rootName = models.rootName(aggregateId);
  895 + if (!repo.existsByPk(repo.tableName(aggregateId, rootName), repo.pkColumn(aggregateId, rootName), idValue))
  896 + return Map.of("message", aggregateId + " 中不存在 " + idField + "=" + idValue);
  897 + return null;
  898 + }
  899 +
  900 + // ===================== 上下文 / 辅助 =====================
  901 +
  902 + private class Ctx {
  903 + final String sceneId;
  904 + final Map<String, Object> scene;
  905 + final Map<String, Object> template;
  906 + String cmdAggregate, cmdId;
  907 + final Map<String, Object> master;
  908 + final Map<String, List<Map<String, Object>>> details;
  909 + final List<RequiredField> requiredFields = new ArrayList<>();
  910 +
  911 + @SuppressWarnings("unchecked")
  912 + Ctx(String sceneId, Map<String, Object> payload) {
  913 + this.sceneId = sceneId;
  914 + this.scene = models.scene(sceneId);
  915 + this.template = models.templateByScene(sceneId);
  916 + findBoundCommand(asList(scene.get("flowSteps")));
  917 + this.master = payload.get("master") instanceof Map
  918 + ? new LinkedHashMap<>((Map<String, Object>) payload.get("master")) : new LinkedHashMap<>();
  919 + this.details = new LinkedHashMap<>();
  920 + if (payload.get("details") instanceof Map<?, ?> dm) {
  921 + for (var e : dm.entrySet()) {
  922 + List<Map<String, Object>> rows = new ArrayList<>();
  923 + for (Object row : asList(e.getValue())) rows.add(new LinkedHashMap<>(asMap(row)));
  924 + details.put(String.valueOf(e.getKey()), rows);
  925 + }
  926 + }
  927 + collectRequired();
  928 + }
  929 +
  930 + void collectRequired() {
  931 + for (Object c : asList(template.get("rootComponents"))) {
  932 + Map<String, Object> container = asMap(c);
  933 + Map<String, Object> fb = asMap(container.get("fieldBind"));
  934 + boolean isMaster = "aggregate_root".equals(fb.get("bindSourceType"));
  935 + String entity = fb.get("domainEntityName") == null ? null : String.valueOf(fb.get("domainEntityName"));
  936 + for (Object ch : asList(container.get("childComponents"))) {
  937 + Map<String, Object> lfb = asMap(asMap(ch).get("fieldBind"));
  938 + if (!Boolean.TRUE.equals(lfb.get("required"))) continue;
  939 + requiredFields.add(new RequiredField(isMaster, entity,
  940 + String.valueOf(lfb.get("domainFieldName")),
  941 + String.valueOf(lfb.getOrDefault("formLabel", lfb.get("domainFieldName")))));
  942 + }
  943 + }
  944 + }
  945 +
  946 + /** 递归定位场景绑定的主命令(含嵌套在 condition/switch/parallel/loop 等控制流内的 bindCommand)。 */
  947 + void findBoundCommand(List<Object> steps) {
  948 + for (Object st : steps) {
  949 + Map<String, Object> stp = asMap(st);
  950 + if ("bindCommand".equals(stp.get("stepType"))) {
  951 + cmdAggregate = String.valueOf(stp.get("bindAggregate"));
  952 + cmdId = String.valueOf(stp.get("bindCommand"));
  953 + }
  954 + for (String key : List.of("then", "else", "body", "default")) findBoundCommand(asList(stp.get(key)));
  955 + for (Object b : asList(stp.get("branches"))) findBoundCommand(asList(asMap(b).get("steps")));
  956 + for (Object c : asList(stp.get("cases"))) findBoundCommand(asList(asMap(c).get("steps")));
  957 + }
  958 + }
  959 + }
  960 +
  961 + private record RequiredField(boolean master, String entity, String field, String label) {}
  962 +
  963 + private boolean permitted(Map<String, Object> scene, String principal) {
  964 + List<Object> perms = asList(scene.get("permissionBind"));
  965 + if (perms.isEmpty()) return false; // 默认拒绝(fail-closed):未绑定权限的场景不可执行
  966 + for (Object permId : perms) {
  967 + if (asList(models.functionPermission(String.valueOf(permId)).get("allowPrincipals")).contains(principal))
  968 + return true;
  969 + }
  970 + return false;
  971 + }
  972 +
  973 + /** 事件 payload:字段值优先取 master,其次在子实体行中查找(修复子实体/非领域字段解析为 null)。 */
  974 + private Map<String, Object> eventPayload(String aggregateId, String eventName, String rootId,
  975 + Map<String, Object> master,
  976 + Map<String, List<Map<String, Object>>> details) {
  977 + Map<String, Object> def = models.eventDef(aggregateId, eventName);
  978 + String rootIdField = models.rootIdentifier(aggregateId);
  979 + Map<String, Object> payload = new LinkedHashMap<>();
  980 + for (Object f : asList(def.get("payload"))) {
  981 + String field = String.valueOf(f);
  982 + if (field.equals(rootIdField)) payload.put(field, rootId);
  983 + else if (master.containsKey(field)) payload.put(field, master.get(field));
  984 + else payload.put(field, findInDetails(details, field));
  985 + }
  986 + if (payload.isEmpty()) payload.put(rootIdField, rootId);
  987 + return payload;
  988 + }
  989 +
  990 + private Object findInDetails(Map<String, List<Map<String, Object>>> details, String field) {
  991 + for (List<Map<String, Object>> rows : details.values())
  992 + for (Map<String, Object> row : rows)
  993 + if (row.containsKey(field)) return row.get(field);
  994 + return null;
  995 + }
  996 +
  997 + /** 通用 ID 前缀:聚合根名首字母(Order→O, Customer→C)。 */
  998 + private String idPrefix(String aggregateId) {
  999 + String name = models.rootName(aggregateId);
  1000 + return name.isEmpty() ? "X" : name.substring(0, 1).toUpperCase();
  1001 + }
  1002 +
  1003 + private Map<String, Object> fail(Map<String, Object> result, String stage, String message) {
  1004 + result.put("success", false);
  1005 + result.put("stage", stage);
  1006 + result.put("message", message);
  1007 + return result;
  1008 + }
  1009 +
  1010 + private Map<String, Object> step(String name, String layer, String detail) {
  1011 + Map<String, Object> m = new LinkedHashMap<>();
  1012 + m.put("step", name);
  1013 + m.put("layer", layer);
  1014 + m.put("detail", detail);
  1015 + return m;
  1016 + }
  1017 +
  1018 + private Map<String, Object> ruleToMap(RuleHit h) {
  1019 + Map<String, Object> m = new LinkedHashMap<>();
  1020 + m.put("ruleId", h.ruleId());
  1021 + m.put("ruleName", h.ruleName());
  1022 + m.put("effect", h.effect());
  1023 + m.put("message", h.message());
  1024 + return m;
  1025 + }
  1026 +
  1027 + private static boolean isBlank(Object o) {
  1028 + return o == null || String.valueOf(o).trim().isEmpty();
  1029 + }
  1030 +
  1031 + private static BigDecimal tryBig(Object o) {
  1032 + try { return new BigDecimal(String.valueOf(o).trim()); } catch (Exception e) { return null; }
  1033 + }
  1034 +
  1035 + @SuppressWarnings("unchecked")
  1036 + private static Map<String, Object> asMap(Object o) {
  1037 + return o instanceof Map ? (Map<String, Object>) o : Collections.emptyMap();
  1038 + }
  1039 +
  1040 + @SuppressWarnings("unchecked")
  1041 + private static List<Object> asList(Object o) {
  1042 + return o instanceof List ? (List<Object>) o : Collections.emptyList();
  1043 + }
  1044 +}
... ...
engine/src/main/java/com/onto/engine/config/WebConfig.java 0 → 100644
  1 +++ a/engine/src/main/java/com/onto/engine/config/WebConfig.java
  1 +package com.onto.engine.config;
  2 +
  3 +import org.springframework.beans.factory.annotation.Value;
  4 +import org.springframework.context.annotation.Configuration;
  5 +import org.springframework.web.servlet.config.annotation.CorsRegistry;
  6 +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
  7 +
  8 +/**
  9 + * CORS 配置:允许来源按 profile 收紧(onto.cors.allowed-origins)。
  10 + * 开发默认 * 方便本机联调;生产 profile 应设为具体前端域名。
  11 + */
  12 +@Configuration
  13 +public class WebConfig implements WebMvcConfigurer {
  14 +
  15 + @Value("${onto.cors.allowed-origins:*}")
  16 + private String[] allowedOrigins;
  17 +
  18 + @Override
  19 + public void addCorsMappings(CorsRegistry registry) {
  20 + registry.addMapping("/api/**")
  21 + .allowedOriginPatterns(allowedOrigins)
  22 + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
  23 + .allowedHeaders("*");
  24 + }
  25 +}
... ...
engine/src/main/java/com/onto/engine/event/EventEngine.java 0 → 100644
  1 +++ a/engine/src/main/java/com/onto/engine/event/EventEngine.java
  1 +package com.onto.engine.event;
  2 +
  3 +import com.fasterxml.jackson.databind.ObjectMapper;
  4 +import com.onto.engine.model.ModelRepository;
  5 +import com.onto.engine.persist.DynamicRepository;
  6 +import com.onto.engine.util.IdGen;
  7 +import org.slf4j.Logger;
  8 +import org.slf4j.LoggerFactory;
  9 +import org.springframework.scheduling.annotation.Scheduled;
  10 +import org.springframework.stereotype.Component;
  11 +
  12 +import java.math.BigDecimal;
  13 +import java.time.LocalDateTime;
  14 +import java.util.*;
  15 +
  16 +/**
  17 + * EDA 事件引擎:Outbox 事件落库 + 跨聚合最终一致性——全部由模型驱动,无任何订单/商品字面量。
  18 + *
  19 + * <p>一致性执行流程(据 ME.crossAggConsistencyRules + M2 命令 effect):
  20 + * 匹配 triggerEvent → 取 sourceItemEntity 逐行 → 按 paramMap 组装目标命令入参 →
  21 + * 读目标命令 effect(op/targetField/keyParam/amountParam/guardField) → 原子条件更新目标聚合。
  22 + * 改 ME/M2 即改跨聚合行为,无需改代码。
  23 + */
  24 +@Component
  25 +public class EventEngine {
  26 +
  27 + private static final Logger log = LoggerFactory.getLogger(EventEngine.class);
  28 +
  29 + private final ModelRepository models;
  30 + private final DynamicRepository repo;
  31 + private final ObjectMapper json;
  32 +
  33 + public EventEngine(ModelRepository models, DynamicRepository repo, ObjectMapper json) {
  34 + this.models = models;
  35 + this.repo = repo;
  36 + this.json = json;
  37 + }
  38 +
  39 + private String outbox() { return models.outboxTable(); }
  40 +
  41 + /** 据 ME 事件定义把一条领域事件写入 Outbox(status=NEW)。 */
  42 + public Map<String, Object> append(String aggregateId, String eventName, Map<String, Object> payload) {
  43 + Map<String, Object> def = models.eventDef(aggregateId, eventName);
  44 + String topic = def.isEmpty() ? deriveTopic(eventName) : String.valueOf(def.get("topic"));
  45 + String eventId = IdGen.next("E");
  46 + String payloadJson;
  47 + try {
  48 + payloadJson = json.writeValueAsString(payload);
  49 + } catch (Exception e) {
  50 + payloadJson = "{}";
  51 + }
  52 + repo.execute("INSERT INTO " + outbox() +
  53 + " (event_id, aggregate_id, event_name, topic, payload_json, status, create_time)" +
  54 + " VALUES (?,?,?,?,?,?,?)",
  55 + eventId, aggregateId, eventName, topic, payloadJson, "NEW", LocalDateTime.now());
  56 + Map<String, Object> rec = new LinkedHashMap<>();
  57 + rec.put("eventId", eventId);
  58 + rec.put("eventName", eventName);
  59 + rec.put("topic", topic);
  60 + rec.put("payload", payload);
  61 + return rec;
  62 + }
  63 +
  64 + /**
  65 + * 应用跨聚合一致性规则(通用)。
  66 + *
  67 + * @param triggerEvent 触发事件名(由 CommandEngine 遍历实际发出的事件传入,非硬编码)
  68 + * @param sourceAggregate 触发聚合(用于在 payload 缺行时从库中按父外键加载子实体行)
  69 + * @param rootId 触发聚合根标识
  70 + * @param detailsByEntity 本次命令 payload 的子实体行(create 路径直接用;cancel 路径为空则查库)
  71 + */
  72 + @SuppressWarnings("unchecked")
  73 + public List<Map<String, Object>> applyCrossConsistency(String triggerEvent, String sourceAggregate,
  74 + Object rootId,
  75 + Map<String, List<Map<String, Object>>> detailsByEntity) {
  76 + List<Map<String, Object>> actions = new ArrayList<>();
  77 + for (Object r : models.crossConsistencyRules()) {
  78 + Map<String, Object> rule = (Map<String, Object>) r;
  79 + if (!triggerEvent.equals(String.valueOf(rule.get("triggerEvent")))) continue;
  80 +
  81 + String sourceEntity = String.valueOf(rule.get("sourceItemEntity"));
  82 + String targetAgg = String.valueOf(rule.get("targetAggregate"));
  83 + String targetCmd = String.valueOf(rule.get("targetCommand"));
  84 + Map<String, Object> paramMap = asMap(rule.get("paramMap"));
  85 + Map<String, Object> effect = models.commandEffect(targetAgg, targetCmd);
  86 + if (effect.isEmpty()) {
  87 + log.warn("跨聚合规则 {} 的目标命令 {} 未声明 effect,跳过", triggerEvent, targetCmd);
  88 + continue;
  89 + }
  90 +
  91 + List<Map<String, Object>> rows = detailsByEntity == null ? null : detailsByEntity.get(sourceEntity);
  92 + if ((rows == null || rows.isEmpty()) && rootId != null) {
  93 + rows = repo.loadChildRows(sourceAggregate, sourceEntity, rootId); // cancel 路径:从库加载
  94 + }
  95 + if (rows == null) rows = List.of();
  96 +
  97 + for (Map<String, Object> row : rows) {
  98 + // 按 paramMap 组装目标命令入参:{targetParam: row[sourceField]}
  99 + Map<String, Object> params = new LinkedHashMap<>();
  100 + for (var pm : paramMap.entrySet()) {
  101 + params.put(pm.getKey(), row.get(String.valueOf(pm.getValue())));
  102 + }
  103 + actions.add(executeEffect(targetAgg, targetCmd, effect, params));
  104 + }
  105 + }
  106 + return actions;
  107 + }
  108 +
  109 + /**
  110 + * 供命令解释器的 CALL_OTHER_COMMAND 指令调用:按目标命令声明的 M2 effect 通用执行其副作用
  111 + * (原子增/减 + 越界保护),返回动作结果(status 为 OK 或 FAIL/SKIP 系列)。目标命令未声明 effect 则跳过。
  112 + */
  113 + public Map<String, Object> applyCommandEffect(String targetAgg, String targetCmd, Map<String, Object> params) {
  114 + Map<String, Object> effect = models.commandEffect(targetAgg, targetCmd);
  115 + if (effect.isEmpty()) {
  116 + Map<String, Object> action = new LinkedHashMap<>();
  117 + action.put("targetAggregate", targetAgg);
  118 + action.put("targetCommand", targetCmd);
  119 + action.put("status", "SKIP_NO_EFFECT");
  120 + return action;
  121 + }
  122 + return executeEffect(targetAgg, targetCmd, effect, params);
  123 + }
  124 +
  125 + /** 通用执行一个命令的结构化 effect(增/减某数值列,原子且带越界保护)。 */
  126 + private Map<String, Object> executeEffect(String targetAgg, String targetCmd,
  127 + Map<String, Object> effect, Map<String, Object> params) {
  128 + String op = String.valueOf(effect.get("op"));
  129 + String targetField = String.valueOf(effect.get("targetField"));
  130 + String keyParam = String.valueOf(effect.get("keyParam"));
  131 + String amountParam = String.valueOf(effect.get("amountParam"));
  132 + boolean guard = effect.get("guardField") != null;
  133 +
  134 + String rootName = models.rootName(targetAgg);
  135 + String table = repo.tableName(targetAgg, rootName);
  136 + String pkCol = repo.pkColumn(targetAgg, rootName);
  137 + String col = repo.fieldMap(targetAgg, rootName).getOrDefault(targetField, targetField);
  138 +
  139 + Object keyValue = params.get(keyParam);
  140 + long amount = parseLong(params.get(amountParam));
  141 +
  142 + Map<String, Object> action = new LinkedHashMap<>();
  143 + action.put("targetAggregate", targetAgg);
  144 + action.put("targetCommand", targetCmd);
  145 + action.put("op", op);
  146 + action.put("keyField", keyParam);
  147 + action.put("key", keyValue);
  148 + action.put("field", targetField);
  149 + action.put("amount", amount);
  150 +
  151 + if (keyValue == null) { action.put("status", "SKIP_NO_KEY"); return action; }
  152 + if (amount <= 0) { action.put("status", "SKIP_NON_POSITIVE"); return action; } // 拒绝负数/零,防库存膨胀
  153 + if (repo.readLong(table, col, pkCol, keyValue) == null
  154 + && !repo.existsByPk(table, pkCol, keyValue)) {
  155 + action.put("status", "SKIP_NOT_FOUND");
  156 + return action;
  157 + }
  158 +
  159 + long delta = "decrement".equals(op) ? -amount : amount;
  160 + int affected = repo.adjustColumn(table, col, pkCol, keyValue, delta, guard);
  161 + if (affected == 0) {
  162 + action.put("status", "decrement".equals(op) ? "FAIL_INSUFFICIENT" : "FAIL_NOT_FOUND");
  163 + action.put("remaining", repo.readLong(table, col, pkCol, keyValue));
  164 + return action;
  165 + }
  166 + Long remaining = repo.readLong(table, col, pkCol, keyValue);
  167 + action.put("status", "OK");
  168 + action.put("remaining", remaining);
  169 +
  170 + // 通用发出目标命令声明的事件(如 ProductStockDeducted / ProductStockReturned)
  171 + for (Object ev : asList(models.command(targetAgg, targetCmd).get("emitEvents"))) {
  172 + Map<String, Object> payload = new LinkedHashMap<>();
  173 + payload.put(keyParam, keyValue);
  174 + payload.put(targetField, remaining);
  175 + append(targetAgg, String.valueOf(ev), payload);
  176 + }
  177 + return action;
  178 + }
  179 +
  180 + /** 轮询 Outbox,把 NEW 事件标记为已发布(模拟发布到消息总线 topic)。 */
  181 + @Scheduled(fixedDelay = 1000)
  182 + public void pollAndPublish() {
  183 + List<Map<String, Object>> pending;
  184 + try {
  185 + pending = repo.jdbc().queryForList(
  186 + "SELECT event_id, topic, event_name FROM " + outbox() + " WHERE status = 'NEW' LIMIT 50");
  187 + } catch (Exception e) {
  188 + return; // 表尚未就绪
  189 + }
  190 + for (Map<String, Object> ev : pending) {
  191 + log.info("[Outbox->Bus] 发布事件 {} 到 topic {}", ev.get("event_name"), ev.get("topic"));
  192 + repo.execute("UPDATE " + outbox() + " SET status='PUBLISHED' WHERE event_id=?", ev.get("event_id"));
  193 + }
  194 + }
  195 +
  196 + public List<Map<String, Object>> recentOutbox(int limit) {
  197 + return repo.jdbc().queryForList(
  198 + "SELECT event_id, aggregate_id, event_name, topic, status, create_time FROM " + outbox() +
  199 + " ORDER BY create_time DESC LIMIT " + limit);
  200 + }
  201 +
  202 + private static long parseLong(Object o) {
  203 + if (o == null) return 0;
  204 + try { return new BigDecimal(String.valueOf(o)).longValue(); } catch (Exception e) { return 0; }
  205 + }
  206 +
  207 + private static String deriveTopic(String eventName) {
  208 + return "topic_" + eventName.replaceAll("([a-z0-9])([A-Z])", "$1_$2").toLowerCase();
  209 + }
  210 +
  211 + @SuppressWarnings("unchecked")
  212 + private static Map<String, Object> asMap(Object o) {
  213 + return o instanceof Map ? (Map<String, Object>) o : Collections.emptyMap();
  214 + }
  215 +
  216 + @SuppressWarnings("unchecked")
  217 + private static List<Object> asList(Object o) {
  218 + return o instanceof List ? (List<Object>) o : Collections.emptyList();
  219 + }
  220 +}
... ...
engine/src/main/java/com/onto/engine/meta/SceneSchemaService.java 0 → 100644
  1 +++ a/engine/src/main/java/com/onto/engine/meta/SceneSchemaService.java
  1 +package com.onto.engine.meta;
  2 +
  3 +import com.onto.engine.model.ModelRepository;
  4 +import com.onto.engine.security.MaskService;
  5 +import org.springframework.stereotype.Service;
  6 +
  7 +import java.util.*;
  8 +
  9 +/**
  10 + * 场景渲染元数据合成服务:把 M8 页面模板与 M1 字段类型、M5 脱敏、M2 命令、M3 接口、MetaRule 规则
  11 + * 合并为一份"前端通用渲染引擎"可直接消费的 Schema。前端据此动态生成"新增订单"页,无任何业务硬编码。
  12 + */
  13 +@Service
  14 +public class SceneSchemaService {
  15 +
  16 + private final ModelRepository models;
  17 + private final MaskService mask;
  18 +
  19 + public SceneSchemaService(ModelRepository models, MaskService mask) {
  20 + this.models = models;
  21 + this.mask = mask;
  22 + }
  23 +
  24 + public Map<String, Object> buildSceneSchema(String sceneId) {
  25 + Map<String, Object> scene = models.scene(sceneId);
  26 + if (scene.isEmpty()) throw new NoSuchElementException("未找到场景: " + sceneId);
  27 + Map<String, Object> template = models.templateByScene(sceneId);
  28 +
  29 + // 从 M4 flowSteps(含嵌套控制流)递归找到本场景绑定的命令
  30 + String[] bound = findBoundCommand(asList(scene.get("flowSteps")), new String[]{null, null});
  31 + String cmdAggregate = bound[0], cmdId = bound[1];
  32 +
  33 + Map<String, Object> out = new LinkedHashMap<>();
  34 + out.put("sceneId", sceneId);
  35 + out.put("sceneName", scene.get("sceneName"));
  36 + out.put("aggregateScope", scene.get("aggregateScope"));
  37 + out.put("permissionBind", scene.get("permissionBind"));
  38 +
  39 + // M2 命令(前端提交时据此把表单值映射为命令入参)
  40 + if (cmdId != null) {
  41 + Map<String, Object> cmd = models.command(cmdAggregate, cmdId);
  42 + Map<String, Object> cmdOut = new LinkedHashMap<>();
  43 + cmdOut.put("aggregateId", cmdAggregate);
  44 + cmdOut.put("cmdId", cmdId);
  45 + cmdOut.put("desc", cmd.get("desc"));
  46 + cmdOut.put("inputParams", cmd.get("inputParams"));
  47 + cmdOut.put("validations", cmd.get("validations"));
  48 + cmdOut.put("bizSteps", cmd.get("bizSteps"));
  49 + cmdOut.put("emitEvents", cmd.get("emitEvents"));
  50 + out.put("command", cmdOut);
  51 + // M3 命令 API 地址(前端无硬编码 URL,从模型取)
  52 + Object api = asMap(models.deployOf(cmdAggregate).get("commandApi")).get(cmdId);
  53 + out.put("commandApi", api);
  54 + }
  55 +
  56 + // M8 模板(逐组件富化 M1 类型 / M5 脱敏 / 引用下拉数据源)
  57 + if (!template.isEmpty()) {
  58 + Map<String, Object> tOut = new LinkedHashMap<>();
  59 + tOut.put("templateId", template.get("templateId"));
  60 + tOut.put("templateName", template.get("templateName"));
  61 + tOut.put("templateType", template.get("templateType"));
  62 + tOut.put("renderMode", template.get("renderMode"));
  63 + tOut.put("masterAggregate", template.get("masterAggregate"));
  64 + tOut.put("detailAggregates", template.get("detailAggregates"));
  65 + List<Object> enriched = new ArrayList<>();
  66 + for (Object c : asList(template.get("rootComponents"))) {
  67 + enriched.add(enrich(asMap(c)));
  68 + }
  69 + tOut.put("rootComponents", enriched);
  70 + out.put("template", tOut);
  71 + }
  72 +
  73 + // MetaRule 规则(前端实时校验用,与后端同源)
  74 + List<Map<String, Object>> rules = new ArrayList<>();
  75 + for (Map<String, Object> group : models.ruleGroupsByScene(sceneId)) {
  76 + for (Object r : asList(group.get("rules"))) {
  77 + Map<String, Object> rule = asMap(r);
  78 + Map<String, Object> ro = new LinkedHashMap<>();
  79 + ro.put("groupId", group.get("groupId"));
  80 + ro.put("ruleId", rule.get("ruleId"));
  81 + ro.put("ruleName", rule.get("ruleName"));
  82 + ro.put("effect", rule.get("effect"));
  83 + ro.put("message", rule.get("message"));
  84 + ro.put("matchCondition", rule.get("matchCondition"));
  85 + rules.add(ro);
  86 + }
  87 + }
  88 + out.put("rules", rules);
  89 + out.put("ruleGlobalParams", models.ruleGlobalParams());
  90 +
  91 + // 溯源信息(供前端"运行原理"面板展示每部分来自哪一层)
  92 + Map<String, String> provenance = new LinkedHashMap<>();
  93 + provenance.put("template", "M8-front-schema.yaml");
  94 + provenance.put("fieldType", "M1-domain.yaml");
  95 + provenance.put("command", "M2-command.yaml");
  96 + provenance.put("commandApi", "M3-deploy.yaml");
  97 + provenance.put("mask", "M5-security.yaml");
  98 + provenance.put("rules", "MetaRule-business.yaml");
  99 + provenance.put("permission", "M5-security.yaml (functionPermissions)");
  100 + out.put("sourceLayers", provenance);
  101 +
  102 + return out;
  103 + }
  104 +
  105 + /** 递归富化一个 M8 组件。 */
  106 + private Map<String, Object> enrich(Map<String, Object> comp) {
  107 + Map<String, Object> out = new LinkedHashMap<>();
  108 + out.put("compId", comp.get("compId"));
  109 + out.put("compType", comp.get("compType"));
  110 + out.put("width", comp.get("width"));
  111 + out.put("span", comp.get("span"));
  112 +
  113 + Map<String, Object> fb = asMap(comp.get("fieldBind"));
  114 + if (!fb.isEmpty()) {
  115 + Map<String, Object> fbOut = new LinkedHashMap<>(fb);
  116 + String agg = String.valueOf(fb.get("bindAggregateId"));
  117 + String sourceType = String.valueOf(fb.get("bindSourceType"));
  118 + String entityName = fb.get("domainEntityName") == null ? null : String.valueOf(fb.get("domainEntityName"));
  119 + String field = String.valueOf(fb.get("domainFieldName"));
  120 + String entForType = "aggregate_root".equals(sourceType) ? null : entityName;
  121 +
  122 + fbOut.put("fieldType", models.fieldType(agg, entForType, field));
  123 + boolean masked = Boolean.TRUE.equals(fb.get("maskField")) || mask.isMasked(field);
  124 + fbOut.put("masked", masked);
  125 +
  126 + // 下拉引用数据源(通用,全部源自模型;无任何具体聚合/字段字面量):
  127 + // 1) select 绑定到某聚合根【自身标识】字段 => 选项来自该聚合(如客户/商品/订单下拉);
  128 + // 2) select 绑定到带 M1 refAggregate 的【引用】字段 => 选项来自被引用聚合,值=对方标识、回填本字段
  129 + // (如订单审核里 auditorId 引用 EmployeeAggregate.employeeId,渲染为员工下拉)。
  130 + if ("select".equals(comp.get("compType"))) {
  131 + if ("aggregate_root".equals(sourceType) && field.equals(models.rootIdentifier(agg))) {
  132 + fbOut.put("optionsSource", optionsSource(agg));
  133 + } else {
  134 + Map<String, Object> ref = models.refInfo(agg, entForType, field);
  135 + if (!ref.isEmpty()) fbOut.put("optionsSource", optionsSource(String.valueOf(ref.get("refAggregate"))));
  136 + }
  137 + }
  138 + out.put("fieldBind", fbOut);
  139 + }
  140 +
  141 + List<Object> children = asList(comp.get("childComponents"));
  142 + if (!children.isEmpty()) {
  143 + List<Object> co = new ArrayList<>();
  144 + for (Object ch : children) co.add(enrich(asMap(ch)));
  145 + out.put("childComponents", co);
  146 + }
  147 + return out;
  148 + }
  149 +
  150 + /** 某聚合根的下拉选项数据源描述(value=聚合根标识,label=展示字段,endpoint=/api/data/agg,通用)。 */
  151 + private Map<String, Object> optionsSource(String aggregateId) {
  152 + Map<String, Object> src = new LinkedHashMap<>();
  153 + src.put("aggregateId", aggregateId);
  154 + src.put("endpoint", "/api/data/" + aggregateId);
  155 + src.put("valueField", models.rootIdentifier(aggregateId));
  156 + src.put("labelField", displayField(aggregateId));
  157 + return src;
  158 + }
  159 +
  160 + /** 聚合根的展示字段:首个 string 类型属性(如 customerName/productName),否则用标识字段。 */
  161 + private String displayField(String aggregateId) {
  162 + for (Object at : models.rootAttributes(aggregateId)) {
  163 + Map<String, Object> am = asMap(at);
  164 + if ("string".equals(am.get("type"))) return String.valueOf(am.get("name"));
  165 + }
  166 + return models.rootIdentifier(aggregateId);
  167 + }
  168 +
  169 + /** 递归定位场景绑定的主命令(含嵌套控制流),返回 {aggregate, cmdId}(后者为最后命中)。 */
  170 + private static String[] findBoundCommand(List<Object> steps, String[] acc) {
  171 + for (Object st : steps) {
  172 + Map<String, Object> stp = asMap(st);
  173 + if ("bindCommand".equals(stp.get("stepType"))) {
  174 + acc[0] = String.valueOf(stp.get("bindAggregate"));
  175 + acc[1] = String.valueOf(stp.get("bindCommand"));
  176 + }
  177 + for (String key : List.of("then", "else", "body", "default")) findBoundCommand(asList(stp.get(key)), acc);
  178 + for (Object b : asList(stp.get("branches"))) findBoundCommand(asList(asMap(b).get("steps")), acc);
  179 + for (Object c : asList(stp.get("cases"))) findBoundCommand(asList(asMap(c).get("steps")), acc);
  180 + }
  181 + return acc;
  182 + }
  183 +
  184 + @SuppressWarnings("unchecked")
  185 + private static Map<String, Object> asMap(Object o) {
  186 + return o instanceof Map ? (Map<String, Object>) o : Collections.emptyMap();
  187 + }
  188 +
  189 + @SuppressWarnings("unchecked")
  190 + private static List<Object> asList(Object o) {
  191 + return o instanceof List ? (List<Object>) o : Collections.emptyList();
  192 + }
  193 +}
... ...
engine/src/main/java/com/onto/engine/model/ModelRepository.java 0 → 100644
  1 +++ a/engine/src/main/java/com/onto/engine/model/ModelRepository.java
  1 +package com.onto.engine.model;
  2 +
  3 +import jakarta.annotation.PostConstruct;
  4 +import org.slf4j.Logger;
  5 +import org.slf4j.LoggerFactory;
  6 +import org.springframework.beans.factory.annotation.Value;
  7 +import org.springframework.stereotype.Component;
  8 +import org.yaml.snakeyaml.LoaderOptions;
  9 +import org.yaml.snakeyaml.Yaml;
  10 +
  11 +import java.io.InputStream;
  12 +import java.nio.file.Files;
  13 +import java.nio.file.Path;
  14 +import java.nio.file.Paths;
  15 +import java.util.*;
  16 +
  17 +/**
  18 + * 本体元模型仓库:启动时(及热重载时)从 models 目录读取全部 YAML 本体文件,
  19 + * 构建内存元模型,并向引擎其余部分提供"按 M0-M8/MetaRule 语义"的类型化查询。
  20 + *
  21 + * <p>这是"架构引擎负责解释与执行"的解释根:所有下游逻辑都只读本仓库,不硬编码任何业务字段。
  22 + */
  23 +@Component
  24 +public class ModelRepository {
  25 +
  26 + private static final Logger log = LoggerFactory.getLogger(ModelRepository.class);
  27 +
  28 + /** 层短码 -> 文件名。顺序即模型查看器展示顺序。 */
  29 + private static final LinkedHashMap<String, String> LAYER_FILES = new LinkedHashMap<>();
  30 + static {
  31 + LAYER_FILES.put("M0", "M0-meta-schema.yaml");
  32 + LAYER_FILES.put("M1", "M1-domain.yaml");
  33 + LAYER_FILES.put("M2", "M2-command.yaml");
  34 + LAYER_FILES.put("ME", "ME-event.yaml");
  35 + LAYER_FILES.put("M3", "M3-deploy.yaml");
  36 + LAYER_FILES.put("M4", "M4-scene.yaml");
  37 + LAYER_FILES.put("M5", "M5-security.yaml");
  38 + LAYER_FILES.put("M6", "M6-monitor.yaml");
  39 + LAYER_FILES.put("M7", "M7-sla.yaml");
  40 + LAYER_FILES.put("MetaRule", "MetaRule-business.yaml");
  41 + LAYER_FILES.put("M8", "M8-front-schema.yaml");
  42 + }
  43 +
  44 + @Value("${onto.models-dir:../models}")
  45 + private String modelsDir;
  46 +
  47 + private volatile Path resolvedDir;
  48 + // 不可变快照 + volatile 原子发布:热重载与在途请求并发时,读取方要么看到旧快照、要么看到新快照,
  49 + // 绝不会看到"半空"中间态(修复 reload 与在途 execute 的数据竞争)。
  50 + private volatile Map<String, Object> layers = Map.of();
  51 +
  52 + @PostConstruct
  53 + public synchronized void load() {
  54 + Path dir = resolveModelsDir();
  55 + Map<String, Object> fresh = new LinkedHashMap<>();
  56 + for (var e : LAYER_FILES.entrySet()) {
  57 + Path f = dir.resolve(e.getValue());
  58 + try (InputStream in = Files.newInputStream(f)) {
  59 + fresh.put(e.getKey(), new Yaml(new LoaderOptions()).load(in));
  60 + } catch (Exception ex) {
  61 + throw new IllegalStateException("加载本体模型失败: " + f + " -> " + ex.getMessage(), ex);
  62 + }
  63 + }
  64 + this.resolvedDir = dir;
  65 + this.layers = Collections.unmodifiableMap(fresh); // 原子发布
  66 + log.info("本体模型已加载: dir={}, layers={}", dir.toAbsolutePath(), fresh.keySet());
  67 + }
  68 +
  69 + /** 热重载:重新读取磁盘上的 YAML,使模型改动即时生效(无需重启)。 */
  70 + public synchronized void reload() {
  71 + load();
  72 + }
  73 +
  74 + private Path resolveModelsDir() {
  75 + List<String> candidates = new ArrayList<>();
  76 + if (modelsDir != null && !modelsDir.isBlank()) candidates.add(modelsDir);
  77 + candidates.addAll(List.of("../models", "models", "../../models"));
  78 + for (String c : candidates) {
  79 + Path p = Paths.get(c).toAbsolutePath().normalize();
  80 + if (Files.isDirectory(p) && Files.exists(p.resolve("M1-domain.yaml"))) {
  81 + return p;
  82 + }
  83 + }
  84 + throw new IllegalStateException("找不到 models 目录,已尝试: " + candidates
  85 + + " (cwd=" + Paths.get("").toAbsolutePath() + ")");
  86 + }
  87 +
  88 + public Path modelsDir() { return resolvedDir; }
  89 +
  90 + // ===================== 通用访问器 =====================
  91 +
  92 + public Set<String> layerCodes() { return LAYER_FILES.keySet(); }
  93 +
  94 + public String fileNameOf(String code) { return LAYER_FILES.get(code); }
  95 +
  96 + /** 返回某层解析后的原始对象(供模型查看器/前端"运行原理"面板展示)。 */
  97 + public Object rawLayer(String code) { return layers.get(code); }
  98 +
  99 + @SuppressWarnings("unchecked")
  100 + static Map<String, Object> asMap(Object o) {
  101 + return o instanceof Map ? (Map<String, Object>) o : Collections.emptyMap();
  102 + }
  103 +
  104 + @SuppressWarnings("unchecked")
  105 + static List<Object> asList(Object o) {
  106 + return o instanceof List ? (List<Object>) o : Collections.emptyList();
  107 + }
  108 +
  109 + // ---------- M1 领域模型 ----------
  110 +
  111 + public List<Object> aggregates() {
  112 + return asList(asMap(layers.get("M1")).get("aggregates"));
  113 + }
  114 +
  115 + public Map<String, Object> aggregate(String aggregateId) {
  116 + for (Object a : aggregates()) {
  117 + Map<String, Object> m = asMap(a);
  118 + if (aggregateId.equals(m.get("aggregateId"))) return m;
  119 + }
  120 + return Collections.emptyMap();
  121 + }
  122 +
  123 + public Map<String, Object> aggregateRoot(String aggregateId) {
  124 + return asMap(aggregate(aggregateId).get("aggregateRoot"));
  125 + }
  126 +
  127 + /** 聚合根唯一标识字段名(如 orderId)。 */
  128 + public String rootIdentifier(String aggregateId) {
  129 + return String.valueOf(aggregateRoot(aggregateId).get("identifier"));
  130 + }
  131 +
  132 + public String rootName(String aggregateId) {
  133 + return String.valueOf(aggregateRoot(aggregateId).get("name"));
  134 + }
  135 +
  136 + public List<Object> rootAttributes(String aggregateId) {
  137 + return asList(aggregateRoot(aggregateId).get("attributes"));
  138 + }
  139 +
  140 + public List<Object> entities(String aggregateId) {
  141 + return asList(aggregate(aggregateId).get("entities"));
  142 + }
  143 +
  144 + public Map<String, Object> entity(String aggregateId, String entityName) {
  145 + for (Object e : entities(aggregateId)) {
  146 + Map<String, Object> m = asMap(e);
  147 + if (entityName.equals(m.get("name"))) return m;
  148 + }
  149 + return Collections.emptyMap();
  150 + }
  151 +
  152 + /**
  153 + * 查询某(聚合, [子实体])下某领域字段的 M1 类型。用于前端组件类型推导、DDL 生成、值转换。
  154 + * entityName 为 null 时查聚合根,否则查子实体;标识字段默认 string。
  155 + */
  156 + public String fieldType(String aggregateId, String entityName, String fieldName) {
  157 + List<Object> attrs;
  158 + String identifier;
  159 + if (entityName == null) {
  160 + attrs = rootAttributes(aggregateId);
  161 + identifier = rootIdentifier(aggregateId);
  162 + } else {
  163 + Map<String, Object> ent = entity(aggregateId, entityName);
  164 + attrs = asList(ent.get("attributes"));
  165 + identifier = String.valueOf(ent.get("localIdentifier"));
  166 + }
  167 + if (fieldName.equals(identifier)) return "string";
  168 + for (Object at : attrs) {
  169 + Map<String, Object> am = asMap(at);
  170 + if (fieldName.equals(am.get("name"))) return String.valueOf(am.get("type"));
  171 + }
  172 + return "string";
  173 + }
  174 +
  175 + /** 组合关系子实体名列表(如 OrderItem/PaymentTerm/DeliveryAddressEntity)。 */
  176 + public List<String> compositionChildren(String aggregateId) {
  177 + List<String> out = new ArrayList<>();
  178 + for (Object r : asList(aggregateRoot(aggregateId).get("relations"))) {
  179 + Map<String, Object> rm = asMap(r);
  180 + if ("composition".equals(rm.get("relationType"))) out.add(String.valueOf(rm.get("ref")));
  181 + }
  182 + return out;
  183 + }
  184 +
  185 + // ---------- M2 命令 ----------
  186 +
  187 + public Map<String, Object> command(String aggregateId, String cmdId) {
  188 + for (Object b : asList(asMap(layers.get("M2")).get("behaviors"))) {
  189 + Map<String, Object> bm = asMap(b);
  190 + if (aggregateId.equals(bm.get("aggregateId"))) {
  191 + for (Object c : asList(bm.get("commands"))) {
  192 + Map<String, Object> cm = asMap(c);
  193 + if (cmdId.equals(cm.get("cmdId"))) return cm;
  194 + }
  195 + }
  196 + }
  197 + return Collections.emptyMap();
  198 + }
  199 +
  200 + // ---------- M3 部署映射 ----------
  201 +
  202 + public Map<String, Object> deployOf(String aggregateId) {
  203 + Object dm = asMap(layers.get("M3")).get("deploymentMappings");
  204 + for (Object a : asList(asMap(dm).get("aggregateMappingList"))) {
  205 + Map<String, Object> am = asMap(a);
  206 + if (aggregateId.equals(am.get("aggregateId"))) return am;
  207 + }
  208 + return Collections.emptyMap();
  209 + }
  210 +
  211 + public List<Object> tableMappings(String aggregateId) {
  212 + return asList(deployOf(aggregateId).get("tableMappings"));
  213 + }
  214 +
  215 + public Map<String, Object> tableMapping(String aggregateId, String domainEntity) {
  216 + for (Object t : tableMappings(aggregateId)) {
  217 + Map<String, Object> tm = asMap(t);
  218 + if (domainEntity.equals(tm.get("domainEntity"))) return tm;
  219 + }
  220 + return Collections.emptyMap();
  221 + }
  222 +
  223 + // ---------- M4 场景 ----------
  224 +
  225 + public List<Object> scenes() {
  226 + return asList(asMap(asMap(layers.get("M4")).get("sceneModel")).get("sceneDefinitions"));
  227 + }
  228 +
  229 + public Map<String, Object> scene(String sceneId) {
  230 + for (Object s : scenes()) {
  231 + Map<String, Object> sm = asMap(s);
  232 + if (sceneId.equals(sm.get("sceneId"))) return sm;
  233 + }
  234 + return Collections.emptyMap();
  235 + }
  236 +
  237 + // ---------- M5 安全 ----------
  238 +
  239 + public Map<String, Object> securityModel() {
  240 + return asMap(asMap(layers.get("M5")).get("securityModel"));
  241 + }
  242 +
  243 + public Map<String, Object> maskRules() {
  244 + return asMap(securityModel().get("maskRules"));
  245 + }
  246 +
  247 + public Map<String, Object> functionPermission(String permId) {
  248 + for (Object p : asList(securityModel().get("functionPermissions"))) {
  249 + Map<String, Object> pm = asMap(p);
  250 + if (permId.equals(pm.get("permId"))) return pm;
  251 + }
  252 + return Collections.emptyMap();
  253 + }
  254 +
  255 + // ---------- M8 前端模板 ----------
  256 +
  257 + public List<Object> pageTemplates() {
  258 + return asList(asMap(asMap(layers.get("M8")).get("frontModel")).get("pageTemplates"));
  259 + }
  260 +
  261 + public Map<String, Object> frontGlobalConfig() {
  262 + return asMap(asMap(asMap(layers.get("M8")).get("frontModel")).get("reactGlobalConfig"));
  263 + }
  264 +
  265 + public Map<String, Object> templateByScene(String sceneId) {
  266 + for (Object t : pageTemplates()) {
  267 + Map<String, Object> tm = asMap(t);
  268 + if (sceneId.equals(tm.get("bindSceneId"))) return tm;
  269 + }
  270 + return Collections.emptyMap();
  271 + }
  272 +
  273 + // ---------- ME 事件 ----------
  274 +
  275 + public Map<String, Object> eventModel() {
  276 + return asMap(asMap(layers.get("ME")).get("eventModel"));
  277 + }
  278 +
  279 + public Map<String, Object> eventDef(String aggregateId, String eventName) {
  280 + for (Object a : asList(eventModel().get("aggregateEventDefinitions"))) {
  281 + Map<String, Object> am = asMap(a);
  282 + if (aggregateId.equals(am.get("aggregateId"))) {
  283 + Map<String, Object> events = asMap(am.get("events"));
  284 + return asMap(events.get(eventName));
  285 + }
  286 + }
  287 + return Collections.emptyMap();
  288 + }
  289 +
  290 + public List<Object> crossConsistencyRules() {
  291 + return asList(eventModel().get("crossAggConsistencyRules"));
  292 + }
  293 +
  294 + // ---------- MetaRule 规则 ----------
  295 +
  296 + public Map<String, Object> metaRuleModel() {
  297 + return asMap(asMap(layers.get("MetaRule")).get("metaRuleModel"));
  298 + }
  299 +
  300 + /** 全局规则参数 paramId -> defaultValue。 */
  301 + public Map<String, Object> ruleGlobalParams() {
  302 + Map<String, Object> out = new LinkedHashMap<>();
  303 + for (Object p : asList(metaRuleModel().get("ruleGlobalParams"))) {
  304 + Map<String, Object> pm = asMap(p);
  305 + out.put(String.valueOf(pm.get("paramId")), pm.get("defaultValue"));
  306 + }
  307 + return out;
  308 + }
  309 +
  310 + /** 绑定到指定场景的规则组列表。 */
  311 + public List<Map<String, Object>> ruleGroupsByScene(String sceneId) {
  312 + List<Map<String, Object>> out = new ArrayList<>();
  313 + for (Object g : asList(metaRuleModel().get("ruleGroups"))) {
  314 + Map<String, Object> gm = asMap(g);
  315 + if (sceneId.equals(gm.get("bindScene"))) out.add(gm);
  316 + }
  317 + return out;
  318 + }
  319 +
  320 + // ---------- M2 命令扩展元数据(commandType / effect / derivations) ----------
  321 +
  322 + /** 命令类型:create / update / cancel(缺省 create)。 */
  323 + public String commandType(String aggregateId, String cmdId) {
  324 + Object t = command(aggregateId, cmdId).get("commandType");
  325 + return t == null ? "create" : String.valueOf(t);
  326 + }
  327 +
  328 + /** 命令的结构化副作用(op/targetField/keyParam/amountParam/guardField)。 */
  329 + public Map<String, Object> commandEffect(String aggregateId, String cmdId) {
  330 + return asMap(command(aggregateId, cmdId).get("effect"));
  331 + }
  332 +
  333 + /** 命令的服务端派生规则(如 totalAmount = sum(quantity*itemPrice))。 */
  334 + public List<Object> commandDerivations(String aggregateId, String cmdId) {
  335 + return asList(command(aggregateId, cmdId).get("derivations"));
  336 + }
  337 +
  338 + // ---------- M1 字段约束 / 属性 / 引用 ----------
  339 +
  340 + public List<Object> attributesOf(String aggregateId, String entityName) {
  341 + return entityName == null ? rootAttributes(aggregateId)
  342 + : asList(entity(aggregateId, entityName).get("attributes"));
  343 + }
  344 +
  345 + /** 字段的结构化约束(min/max/pattern/...),无则空。 */
  346 + public Map<String, Object> attributeConstraints(String aggregateId, String entityName, String fieldName) {
  347 + for (Object at : attributesOf(aggregateId, entityName)) {
  348 + Map<String, Object> am = asMap(at);
  349 + if (fieldName.equals(am.get("name"))) return asMap(am.get("constraints"));
  350 + }
  351 + return Collections.emptyMap();
  352 + }
  353 +
  354 + /** 若字段是跨聚合引用,返回 {refAggregate, refRoot, refIdentifier};否则空。 */
  355 + public Map<String, Object> refInfo(String aggregateId, String entityName, String fieldName) {
  356 + for (Object at : attributesOf(aggregateId, entityName)) {
  357 + Map<String, Object> am = asMap(at);
  358 + if (fieldName.equals(am.get("name")) && am.get("refAggregate") != null) {
  359 + Map<String, Object> r = new LinkedHashMap<>();
  360 + r.put("refAggregate", am.get("refAggregate"));
  361 + r.put("refRoot", am.get("refRoot"));
  362 + r.put("refIdentifier", am.get("refIdentifier"));
  363 + return r;
  364 + }
  365 + }
  366 + return Collections.emptyMap();
  367 + }
  368 +
  369 + /** 聚合根展示字段:首个 string 属性,否则用标识字段。 */
  370 + public String displayField(String aggregateId) {
  371 + for (Object at : rootAttributes(aggregateId)) {
  372 + Map<String, Object> am = asMap(at);
  373 + if ("string".equals(am.get("type"))) return String.valueOf(am.get("name"));
  374 + }
  375 + return rootIdentifier(aggregateId);
  376 + }
  377 +
  378 + // ---------- ME / M5 扩展 ----------
  379 +
  380 + /** ME 声明的 Outbox 表名(缺省 t_domain_outbox)。 */
  381 + public String outboxTable() {
  382 + Object ob = asMap(eventModel().get("globalConfig")).get("outbox");
  383 + Object t = asMap(ob).get("table");
  384 + return t == null ? "t_domain_outbox" : String.valueOf(t);
  385 + }
  386 +
  387 + /** M5 声明需存储加密的领域字段集合。 */
  388 + public Set<String> encryptFields() {
  389 + Set<String> out = new LinkedHashSet<>();
  390 + for (Object f : asList(asMap(securityModel().get("encryptRules")).get("storageEncrypt"))) {
  391 + out.add(String.valueOf(f));
  392 + }
  393 + return out;
  394 + }
  395 +}
... ...
engine/src/main/java/com/onto/engine/persist/DomainReadService.java 0 → 100644
  1 +++ a/engine/src/main/java/com/onto/engine/persist/DomainReadService.java
  1 +package com.onto.engine.persist;
  2 +
  3 +import com.onto.engine.model.ModelRepository;
  4 +import com.onto.engine.security.EncryptService;
  5 +import com.onto.engine.security.MaskService;
  6 +import org.springframework.stereotype.Service;
  7 +
  8 +import java.util.*;
  9 +
  10 +/**
  11 + * 领域读服务:把物理表行按 M3 fieldMap 反向映射回领域字段,先解密 M5 加密字段、再脱敏。
  12 + * 用于下拉数据源与聚合根列表展示。
  13 + */
  14 +@Service
  15 +public class DomainReadService {
  16 +
  17 + private final ModelRepository models;
  18 + private final DynamicRepository repo;
  19 + private final MaskService mask;
  20 + private final EncryptService encrypt;
  21 +
  22 + public DomainReadService(ModelRepository models, DynamicRepository repo, MaskService mask, EncryptService encrypt) {
  23 + this.models = models;
  24 + this.repo = repo;
  25 + this.mask = mask;
  26 + this.encrypt = encrypt;
  27 + }
  28 +
  29 + /** 列出某聚合根的全部记录,转为领域字段并脱敏。 */
  30 + public List<Map<String, Object>> listRoot(String aggregateId) {
  31 + String rootName = models.rootName(aggregateId);
  32 + String table = repo.tableName(aggregateId, rootName);
  33 + Map<String, String> fmap = repo.fieldMap(aggregateId, rootName);
  34 + Map<String, String> inverse = invert(fmap);
  35 + List<Map<String, Object>> out = new ArrayList<>();
  36 + for (Map<String, Object> row : repo.listAll(table)) {
  37 + out.add(toDomain(row, inverse));
  38 + }
  39 + return out;
  40 + }
  41 +
  42 + /** 下拉选项:value=聚合根标识,label=展示字段,并携带全部领域字段(供前端联动,如带出售价)。 */
  43 + public List<Map<String, Object>> options(String aggregateId) {
  44 + String idField = models.rootIdentifier(aggregateId);
  45 + String labelField = displayField(aggregateId);
  46 + List<Map<String, Object>> out = new ArrayList<>();
  47 + for (Map<String, Object> d : listRoot(aggregateId)) {
  48 + Object id = d.get(idField);
  49 + Object display = d.get(labelField);
  50 + Map<String, Object> opt = new LinkedHashMap<>();
  51 + opt.put("value", id);
  52 + // 标签带上标识,避免展示字段不唯一时(如订单以币种为展示字段)选项难以区分
  53 + opt.put("label", (display != null && !display.equals(id)) ? display + " · " + id : id);
  54 + opt.put("data", d);
  55 + out.add(opt);
  56 + }
  57 + return out;
  58 + }
  59 +
  60 + private Map<String, Object> toDomain(Map<String, Object> row, Map<String, String> columnToField) {
  61 + Set<String> encFields = models.encryptFields();
  62 + Map<String, Object> d = new LinkedHashMap<>();
  63 + for (var e : row.entrySet()) {
  64 + String field = columnToField.getOrDefault(e.getKey().toLowerCase(), e.getKey());
  65 + Object value = e.getValue();
  66 + if (value != null && encFields.contains(field)) value = encrypt.decrypt(value); // 先解密
  67 + if (mask.isMasked(field)) value = mask.mask(field, value); // 再脱敏
  68 + d.put(field, value);
  69 + }
  70 + return d;
  71 + }
  72 +
  73 + private String displayField(String aggregateId) {
  74 + for (Object at : models.rootAttributes(aggregateId)) {
  75 + Map<?, ?> am = (Map<?, ?>) at;
  76 + if ("string".equals(am.get("type"))) return String.valueOf(am.get("name"));
  77 + }
  78 + return models.rootIdentifier(aggregateId);
  79 + }
  80 +
  81 + private Map<String, String> invert(Map<String, String> fieldToColumn) {
  82 + Map<String, String> inv = new LinkedHashMap<>();
  83 + for (var e : fieldToColumn.entrySet()) inv.put(e.getValue().toLowerCase(), e.getKey());
  84 + return inv;
  85 + }
  86 +}
... ...
engine/src/main/java/com/onto/engine/persist/DynamicRepository.java 0 → 100644
  1 +++ a/engine/src/main/java/com/onto/engine/persist/DynamicRepository.java
  1 +package com.onto.engine.persist;
  2 +
  3 +import com.onto.engine.model.ModelRepository;
  4 +import com.onto.engine.security.EncryptService;
  5 +import org.springframework.jdbc.core.JdbcTemplate;
  6 +import org.springframework.stereotype.Repository;
  7 +
  8 +import java.math.BigDecimal;
  9 +import java.time.LocalDateTime;
  10 +import java.time.format.DateTimeFormatter;
  11 +import java.util.*;
  12 +
  13 +/**
  14 + * 通用领域仓储:不依赖任何实体类,全部按 M3 tableMappings 的 fieldMap 把"领域字段"翻译为"物理列",
  15 + * 并按 M1 字段类型做值转换后落库。改 M3 映射或 M1 字段类型,SQL 随之改变。
  16 + * M5 声明 storageEncrypt 的字段在落库前加密、读出后解密。
  17 + */
  18 +@Repository
  19 +public class DynamicRepository {
  20 +
  21 + private final JdbcTemplate jdbc;
  22 + private final ModelRepository models;
  23 + private final EncryptService encrypt;
  24 +
  25 + public DynamicRepository(JdbcTemplate jdbc, ModelRepository models, EncryptService encrypt) {
  26 + this.jdbc = jdbc;
  27 + this.models = models;
  28 + this.encrypt = encrypt;
  29 + }
  30 +
  31 + /** 领域字段 -> 物理列(据 M3 fieldMap)。 */
  32 + @SuppressWarnings("unchecked")
  33 + public Map<String, String> fieldMap(String aggregateId, String domainEntity) {
  34 + Object fm = models.tableMapping(aggregateId, domainEntity).get("fieldMap");
  35 + return fm instanceof Map ? (Map<String, String>) fm : Collections.emptyMap();
  36 + }
  37 +
  38 + public String tableName(String aggregateId, String domainEntity) {
  39 + return String.valueOf(models.tableMapping(aggregateId, domainEntity).get("table"));
  40 + }
  41 +
  42 + public String pkColumn(String aggregateId, String domainEntity) {
  43 + return String.valueOf(models.tableMapping(aggregateId, domainEntity).get("pk"));
  44 + }
  45 +
  46 + /**
  47 + * 按领域字段写入一行。domainValues 以领域字段名为 key;extraColumns 以物理列名为 key(如父外键)。
  48 + * 仅写入既出现在 domainValues 又存在于 M3 fieldMap 中的字段。
  49 + */
  50 + public void insertDomain(String aggregateId, String domainEntity, String entityNameForType,
  51 + Map<String, Object> domainValues, Map<String, Object> extraColumns) {
  52 + Map<String, String> fmap = fieldMap(aggregateId, domainEntity);
  53 + Set<String> encFields = models.encryptFields();
  54 + List<String> cols = new ArrayList<>();
  55 + List<Object> args = new ArrayList<>();
  56 + for (var e : fmap.entrySet()) {
  57 + String domainField = e.getKey();
  58 + if (!domainValues.containsKey(domainField)) continue;
  59 + String type = models.fieldType(aggregateId, entityNameForType, domainField);
  60 + Object value = convert(type, domainValues.get(domainField));
  61 + if (value != null && encFields.contains(domainField)) value = encrypt.encrypt(value); // M5 存储加密
  62 + cols.add(e.getValue());
  63 + args.add(value);
  64 + }
  65 + if (extraColumns != null) {
  66 + for (var e : extraColumns.entrySet()) {
  67 + cols.add(e.getKey());
  68 + args.add(e.getValue());
  69 + }
  70 + }
  71 + if (cols.isEmpty()) return;
  72 + String table = tableName(aggregateId, domainEntity);
  73 + String placeholders = String.join(",", Collections.nCopies(cols.size(), "?"));
  74 + String sql = "INSERT INTO " + table + " (" + String.join(",", cols) + ") VALUES (" + placeholders + ")";
  75 + jdbc.update(sql, args.toArray());
  76 + }
  77 +
  78 + /** 按领域字段更新一行(据 M3 fieldMap;加密敏感字段)。返回受影响行数。 */
  79 + public int updateDomain(String aggregateId, String domainEntity, String entityNameForType,
  80 + Map<String, Object> domainValues, String pkDomainField, Object pkValue) {
  81 + Map<String, String> fmap = fieldMap(aggregateId, domainEntity);
  82 + Set<String> encFields = models.encryptFields();
  83 + List<String> sets = new ArrayList<>();
  84 + List<Object> args = new ArrayList<>();
  85 + for (var e : fmap.entrySet()) {
  86 + String df = e.getKey();
  87 + if (df.equals(pkDomainField) || !domainValues.containsKey(df)) continue;
  88 + Object v = convert(models.fieldType(aggregateId, entityNameForType, df), domainValues.get(df));
  89 + if (v != null && encFields.contains(df)) v = encrypt.encrypt(v);
  90 + sets.add(e.getValue() + " = ?");
  91 + args.add(v);
  92 + }
  93 + if (sets.isEmpty()) return 0;
  94 + String pkCol = fmap.getOrDefault(pkDomainField, pkColumn(aggregateId, domainEntity));
  95 + args.add(pkValue);
  96 + return jdbc.update("UPDATE " + tableName(aggregateId, domainEntity) + " SET "
  97 + + String.join(",", sets) + " WHERE " + pkCol + " = ?", args.toArray());
  98 + }
  99 +
  100 + /** 按父外键(=聚合根标识)更新某组合子实体的行(据 M3 fieldMap;加密敏感字段)。返回受影响行数。 */
  101 + public int updateChildByParent(String aggregateId, String childEntity, Object parentIdValue,
  102 + Map<String, Object> domainValues) {
  103 + String parentFk = pkColumn(aggregateId, models.rootName(aggregateId));
  104 + Set<String> encFields = models.encryptFields();
  105 + List<String> sets = new ArrayList<>();
  106 + List<Object> args = new ArrayList<>();
  107 + for (var e : fieldMap(aggregateId, childEntity).entrySet()) {
  108 + String df = e.getKey();
  109 + if (!domainValues.containsKey(df)) continue; // 未提供的字段(含子实体主键)不覆盖
  110 + Object v = convert(models.fieldType(aggregateId, childEntity, df), domainValues.get(df));
  111 + if (v != null && encFields.contains(df)) v = encrypt.encrypt(v);
  112 + sets.add(e.getValue() + " = ?");
  113 + args.add(v);
  114 + }
  115 + if (sets.isEmpty()) return 0;
  116 + args.add(parentIdValue);
  117 + return jdbc.update("UPDATE " + tableName(aggregateId, childEntity) + " SET "
  118 + + String.join(",", sets) + " WHERE " + parentFk + " = ?", args.toArray());
  119 + }
  120 +
  121 + public long count(String table) {
  122 + Long n = jdbc.queryForObject("SELECT COUNT(*) FROM " + table, Long.class);
  123 + return n == null ? 0 : n;
  124 + }
  125 +
  126 + public List<Map<String, Object>> listAll(String table) {
  127 + return jdbc.queryForList("SELECT * FROM " + table);
  128 + }
  129 +
  130 + public Map<String, Object> findByPk(String table, String pkColumn, Object pkValue) {
  131 + List<Map<String, Object>> rows =
  132 + jdbc.queryForList("SELECT * FROM " + table + " WHERE " + pkColumn + " = ?", pkValue);
  133 + return rows.isEmpty() ? Collections.emptyMap() : rows.get(0);
  134 + }
  135 +
  136 + public boolean existsByPk(String table, String pkColumn, Object pkValue) {
  137 + Long n = jdbc.queryForObject(
  138 + "SELECT COUNT(*) FROM " + table + " WHERE " + pkColumn + " = ?", Long.class, pkValue);
  139 + return n != null && n > 0;
  140 + }
  141 +
  142 + /** 是否存在 column=value 的其它行(excludePk 为空则不排除自身)——用于唯一性约束校验。 */
  143 + public boolean existsOther(String table, String column, Object value, String pkColumn, Object excludePk) {
  144 + Long n = (excludePk == null)
  145 + ? jdbc.queryForObject("SELECT COUNT(*) FROM " + table + " WHERE " + column + " = ?", Long.class, value)
  146 + : jdbc.queryForObject("SELECT COUNT(*) FROM " + table + " WHERE " + column + " = ? AND "
  147 + + pkColumn + " <> ?", Long.class, value, excludePk);
  148 + return n != null && n > 0;
  149 + }
  150 +
  151 + public int execute(String sql, Object... args) {
  152 + return jdbc.update(sql, args);
  153 + }
  154 +
  155 + public JdbcTemplate jdbc() { return jdbc; }
  156 +
  157 + /** 按聚合根标识加载一行,反向映射为领域字段并解密敏感字段(不脱敏)——供规则/引用解析。 */
  158 + public Map<String, Object> loadDomainRow(String aggregateId, String domainEntity, Object idValue) {
  159 + if (idValue == null) return Collections.emptyMap();
  160 + Map<String, Object> row = findByPk(tableName(aggregateId, domainEntity),
  161 + pkColumn(aggregateId, domainEntity), idValue);
  162 + if (row.isEmpty()) return Collections.emptyMap();
  163 + Set<String> encFields = models.encryptFields();
  164 + Map<String, Object> out = new LinkedHashMap<>();
  165 + for (var e : fieldMap(aggregateId, domainEntity).entrySet()) {
  166 + Object v = row.containsKey(e.getValue()) ? row.get(e.getValue()) : row.get(e.getValue().toLowerCase());
  167 + if (v != null && encFields.contains(e.getKey())) v = encrypt.decrypt(v);
  168 + out.put(e.getKey(), v);
  169 + }
  170 + return out;
  171 + }
  172 +
  173 + /**
  174 + * 原子条件调整数值列:delta 有符号(扣减为负,回补为正)。
  175 + * guardNonNegative=true 时要求结果不为负(如库存);返回受影响行数(0=不存在/越界,未变更)。
  176 + */
  177 + /** 加载某聚合根下某组合子实体的全部行(按父外键=根标识),反向映射为领域字段并解密。 */
  178 + public List<Map<String, Object>> loadChildRows(String aggregateId, String childEntity, Object parentIdValue) {
  179 + String parentFk = pkColumn(aggregateId, models.rootName(aggregateId));
  180 + String table = tableName(aggregateId, childEntity);
  181 + Set<String> encFields = models.encryptFields();
  182 + List<Map<String, Object>> out = new ArrayList<>();
  183 + for (Map<String, Object> row : jdbc.queryForList(
  184 + "SELECT * FROM " + table + " WHERE " + parentFk + " = ?", parentIdValue)) {
  185 + Map<String, Object> d = new LinkedHashMap<>();
  186 + for (var e : fieldMap(aggregateId, childEntity).entrySet()) {
  187 + Object v = row.containsKey(e.getValue()) ? row.get(e.getValue()) : row.get(e.getValue().toLowerCase());
  188 + if (v != null && encFields.contains(e.getKey())) v = encrypt.decrypt(v);
  189 + d.put(e.getKey(), v);
  190 + }
  191 + out.add(d);
  192 + }
  193 + return out;
  194 + }
  195 +
  196 + public int adjustColumn(String table, String column, String pkColumn, Object pkValue,
  197 + long delta, boolean guardNonNegative) {
  198 + if (delta < 0 && guardNonNegative) {
  199 + return jdbc.update("UPDATE " + table + " SET " + column + " = " + column + " + ? WHERE "
  200 + + pkColumn + " = ? AND " + column + " + ? >= 0", delta, pkValue, delta);
  201 + }
  202 + return jdbc.update("UPDATE " + table + " SET " + column + " = " + column + " + ? WHERE "
  203 + + pkColumn + " = ?", delta, pkValue);
  204 + }
  205 +
  206 + /** 读取某列当前数值(供展示扣减后余额)。null/缺失返回 null。 */
  207 + public Long readLong(String table, String column, String pkColumn, Object pkValue) {
  208 + Map<String, Object> row = findByPk(table, pkColumn, pkValue);
  209 + Object v = row.get(column);
  210 + if (v == null) v = row.get(column.toLowerCase());
  211 + if (v == null) return null;
  212 + try { return new BigDecimal(String.valueOf(v)).longValue(); } catch (Exception e) { return null; }
  213 + }
  214 +
  215 + // ===================== 值类型转换(据 M1 类型) =====================
  216 +
  217 + public static Object convert(String m1Type, Object raw) {
  218 + if (raw == null) return null;
  219 + String s = String.valueOf(raw).trim();
  220 + return switch (m1Type == null ? "string" : m1Type) {
  221 + case "int" -> s.isEmpty() ? null : new BigDecimal(s).longValue();
  222 + case "decimal" -> s.isEmpty() ? null : new BigDecimal(s);
  223 + case "boolean" -> (raw instanceof Boolean b) ? b : Boolean.parseBoolean(s);
  224 + case "datetime" -> parseDateTime(s);
  225 + default -> s;
  226 + };
  227 + }
  228 +
  229 + private static LocalDateTime parseDateTime(String s) {
  230 + if (s == null || s.isEmpty()) return LocalDateTime.now();
  231 + String v = s.replace('T', ' ');
  232 + // 去除毫秒/时区尾巴,尽量宽松
  233 + int dot = v.indexOf('.');
  234 + if (dot > 0) v = v.substring(0, dot);
  235 + try {
  236 + if (v.length() <= 10) { // 仅日期
  237 + return LocalDateTime.parse(v + " 00:00:00",
  238 + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
  239 + }
  240 + return LocalDateTime.parse(v, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
  241 + } catch (Exception ex) {
  242 + return LocalDateTime.now();
  243 + }
  244 + }
  245 +}
... ...
engine/src/main/java/com/onto/engine/persist/SchemaInitializer.java 0 → 100644
  1 +++ a/engine/src/main/java/com/onto/engine/persist/SchemaInitializer.java
  1 +package com.onto.engine.persist;
  2 +
  3 +import com.onto.engine.model.ModelRepository;
  4 +import jakarta.annotation.PostConstruct;
  5 +import org.slf4j.Logger;
  6 +import org.slf4j.LoggerFactory;
  7 +import org.springframework.core.io.ClassPathResource;
  8 +import org.springframework.jdbc.core.JdbcTemplate;
  9 +import org.springframework.stereotype.Component;
  10 +import org.yaml.snakeyaml.Yaml;
  11 +
  12 +import java.io.InputStream;
  13 +import java.time.LocalDateTime;
  14 +import java.util.*;
  15 +
  16 +/**
  17 + * 据 M3 tableMappings + M1 字段类型自动生成 DDL 建表,并写入演示引导数据。
  18 + * 这体现"表结构也由模型驱动":新增 M1 字段 + M3 列映射后,reload 会自动补列。
  19 + */
  20 +@Component
  21 +public class SchemaInitializer {
  22 +
  23 + private static final Logger log = LoggerFactory.getLogger(SchemaInitializer.class);
  24 +
  25 + private final ModelRepository models;
  26 + private final JdbcTemplate jdbc;
  27 + private final DynamicRepository repo;
  28 +
  29 + public SchemaInitializer(ModelRepository models, JdbcTemplate jdbc, DynamicRepository repo) {
  30 + this.models = models;
  31 + this.jdbc = jdbc;
  32 + this.repo = repo;
  33 + }
  34 +
  35 + @PostConstruct
  36 + public void init() {
  37 + ensureEventOutboxTable();
  38 + ensureDomainSchema();
  39 + seed();
  40 + }
  41 +
  42 + /** 供热重载后补齐新增表/列。 */
  43 + public void refresh() {
  44 + ensureDomainSchema();
  45 + }
  46 +
  47 + private void ensureEventOutboxTable() {
  48 + // ME.eventModel.globalConfig.outbox.table = t_domain_outbox
  49 + String table = "t_domain_outbox";
  50 + try {
  51 + Object outbox = ((Map<?, ?>) ((Map<?, ?>) models.eventModel().get("globalConfig")).get("outbox"));
  52 + if (outbox instanceof Map<?, ?> m && m.get("table") != null) table = String.valueOf(m.get("table"));
  53 + } catch (Exception ignore) { /* 用默认名 */ }
  54 + jdbc.execute("CREATE TABLE IF NOT EXISTS " + table + " (" +
  55 + "event_id VARCHAR(64) PRIMARY KEY, aggregate_id VARCHAR(64), event_name VARCHAR(64), " +
  56 + "topic VARCHAR(128), payload_json TEXT, status VARCHAR(16), create_time TIMESTAMP)");
  57 + }
  58 +
  59 + private void ensureDomainSchema() {
  60 + for (Object a : models.aggregates()) {
  61 + String aggId = String.valueOf(ModelRepositoryAccess.get(a, "aggregateId"));
  62 + String rootName = models.rootName(aggId);
  63 + String rootPkColumn = repo.pkColumn(aggId, rootName);
  64 + for (Object tmObj : models.tableMappings(aggId)) {
  65 + @SuppressWarnings("unchecked")
  66 + Map<String, Object> tm = (Map<String, Object>) tmObj;
  67 + String domainEntity = String.valueOf(tm.get("domainEntity"));
  68 + String table = String.valueOf(tm.get("table"));
  69 + String pk = String.valueOf(tm.get("pk"));
  70 + boolean isRoot = domainEntity.equals(rootName);
  71 + String entityNameForType = isRoot ? null : domainEntity;
  72 +
  73 + @SuppressWarnings("unchecked")
  74 + Map<String, String> fieldMap = (Map<String, String>) tm.get("fieldMap");
  75 +
  76 + createTableIfAbsent(table, pk);
  77 + for (var e : fieldMap.entrySet()) {
  78 + String type = models.fieldType(aggId, entityNameForType, e.getKey());
  79 + addColumnIfAbsent(table, e.getValue(), sqlType(type, e.getValue().equals(pk)));
  80 + }
  81 + // 组合子实体:补父外键列(据 M1 composition + M3 根主键列)
  82 + if (!isRoot && !rootPkColumn.equals("null")) {
  83 + addColumnIfAbsent(table, rootPkColumn, "VARCHAR(64)");
  84 + }
  85 + }
  86 + }
  87 + log.info("领域表结构已按 M3/M1 生成/校准完毕");
  88 + }
  89 +
  90 + private void createTableIfAbsent(String table, String pkColumn) {
  91 + jdbc.execute("CREATE TABLE IF NOT EXISTS " + table + " (" + pkColumn + " VARCHAR(64) PRIMARY KEY)");
  92 + }
  93 +
  94 + private void addColumnIfAbsent(String table, String column, String sqlType) {
  95 + // H2 支持 IF NOT EXISTS;已是主键列时 CREATE 已建,跳过
  96 + jdbc.execute("ALTER TABLE " + table + " ADD COLUMN IF NOT EXISTS " + column + " " + sqlType);
  97 + }
  98 +
  99 + private String sqlType(String m1Type, boolean isPk) {
  100 + if (isPk) return "VARCHAR(64)";
  101 + return switch (m1Type == null ? "string" : m1Type) {
  102 + case "int" -> "BIGINT";
  103 + case "decimal" -> "DECIMAL(18,4)";
  104 + case "datetime" -> "TIMESTAMP";
  105 + case "boolean" -> "BOOLEAN";
  106 + default -> "VARCHAR(255)";
  107 + };
  108 + }
  109 +
  110 + // ===================== 演示引导数据 =====================
  111 +
  112 + @SuppressWarnings("unchecked")
  113 + private void seed() {
  114 + Map<String, Object> seed;
  115 + try (InputStream in = new ClassPathResource("seed-data.yaml").getInputStream()) {
  116 + seed = new Yaml().load(in);
  117 + } catch (Exception ex) {
  118 + log.warn("未找到 seed-data.yaml,跳过引导数据: {}", ex.getMessage());
  119 + return;
  120 + }
  121 + if (seed == null) return;
  122 + // 通用:以聚合 id 为 key 遍历,解析根实体名后落库;缺失的 datetime 字段填当前时间。
  123 + for (var entry : seed.entrySet()) {
  124 + String aggregateId = entry.getKey();
  125 + if (models.aggregate(aggregateId).isEmpty()) continue;
  126 + String rootName = models.rootName(aggregateId);
  127 + if (repo.count(repo.tableName(aggregateId, rootName)) > 0) continue;
  128 +
  129 + List<String> datetimeFields = new ArrayList<>();
  130 + for (Object at : models.rootAttributes(aggregateId)) {
  131 + Map<String, Object> am = (Map<String, Object>) at;
  132 + if ("datetime".equals(am.get("type"))) datetimeFields.add(String.valueOf(am.get("name")));
  133 + }
  134 + int n = 0;
  135 + for (Object rowObj : (List<Object>) entry.getValue()) {
  136 + Map<String, Object> row = new LinkedHashMap<>((Map<String, Object>) rowObj);
  137 + for (String dt : datetimeFields) row.putIfAbsent(dt, LocalDateTime.now().toString());
  138 + repo.insertDomain(aggregateId, rootName, null, row, null);
  139 + n++;
  140 + }
  141 + log.info("已写入演示数据 {}:{} 条", aggregateId, n);
  142 + }
  143 + }
  144 +
  145 + /** 小工具:从任意 Map-like 对象取 key。 */
  146 + static final class ModelRepositoryAccess {
  147 + static Object get(Object mapLike, String key) {
  148 + return mapLike instanceof Map<?, ?> m ? m.get(key) : null;
  149 + }
  150 + }
  151 +}
... ...
engine/src/main/java/com/onto/engine/rule/RuleEngine.java 0 → 100644
  1 +++ a/engine/src/main/java/com/onto/engine/rule/RuleEngine.java
  1 +package com.onto.engine.rule;
  2 +
  3 +import com.googlecode.aviator.AviatorEvaluator;
  4 +import com.googlecode.aviator.Expression;
  5 +import com.onto.engine.model.ModelRepository;
  6 +import org.slf4j.Logger;
  7 +import org.slf4j.LoggerFactory;
  8 +import org.springframework.stereotype.Component;
  9 +
  10 +import java.math.BigDecimal;
  11 +import java.util.*;
  12 +import java.util.regex.Pattern;
  13 +
  14 +/**
  15 + * MetaRule 动态规则引擎(Aviator)。前端"实时校验"与后端"持久化校验"共用同一份 MetaRule 表达式,
  16 + * 规则仅维护一份。改 MetaRule-business.yaml 的阈值/表达式/message,reload 后前后端行为同时改变。
  17 + */
  18 +@Component
  19 +public class RuleEngine {
  20 +
  21 + private static final Logger log = LoggerFactory.getLogger(RuleEngine.class);
  22 + private static final Pattern PARAM_REF = Pattern.compile("#\\{([a-zA-Z0-9_]+)\\}");
  23 +
  24 + private final ModelRepository models;
  25 +
  26 + public RuleEngine(ModelRepository models) {
  27 + this.models = models;
  28 + }
  29 +
  30 + /**
  31 + * 针对某场景,按给定业务变量评估所有绑定规则组。
  32 + * 仅评估其"所引用变量"都在 env 中出现的规则(据此把订单级规则与逐商品级规则区分开)。
  33 + */
  34 + public List<RuleHit> evaluate(String sceneId, Map<String, Object> businessVars) {
  35 + Map<String, Object> env = new HashMap<>();
  36 + // 注入全局规则参数(#{paramId} 会被改写为变量 paramId)
  37 + models.ruleGlobalParams().forEach((k, v) -> env.put(k, toNumberIfPossible(v)));
  38 + businessVars.forEach((k, v) -> env.put(k, toNumberIfPossible(v)));
  39 +
  40 + List<RuleHit> hits = new ArrayList<>();
  41 + for (Map<String, Object> group : models.ruleGroupsByScene(sceneId)) {
  42 + for (Object r : asList(group.get("rules"))) {
  43 + Map<String, Object> rule = asMap(r);
  44 + String cond = String.valueOf(rule.get("matchCondition"));
  45 + // 把 #{paramId} 改写为变量名 paramId(全局参数已注入 env)
  46 + String processed = PARAM_REF.matcher(cond).replaceAll("$1");
  47 + try {
  48 + Expression exp = AviatorEvaluator.compile(processed, true);
  49 + List<String> vars = exp.getVariableNames();
  50 + if (!env.keySet().containsAll(vars)) {
  51 + continue; // 该规则所需变量在此上下文未提供,跳过
  52 + }
  53 + Object result = exp.execute(env);
  54 + if (Boolean.TRUE.equals(result)) {
  55 + hits.add(new RuleHit(
  56 + String.valueOf(rule.get("ruleId")),
  57 + String.valueOf(rule.get("ruleName")),
  58 + String.valueOf(rule.get("effect")),
  59 + String.valueOf(rule.get("message"))));
  60 + }
  61 + } catch (Exception ex) {
  62 + log.warn("规则求值失败 ruleId={} cond={} err={}", rule.get("ruleId"), processed, ex.getMessage());
  63 + }
  64 + }
  65 + }
  66 + return hits;
  67 + }
  68 +
  69 + /** 用 Aviator 求一个算术表达式(如 "quantity * itemPrice")为数值;出错或缺变量返回 0。 */
  70 + public BigDecimal evalNumber(String expression, Map<String, Object> row) {
  71 + Map<String, Object> env = new HashMap<>();
  72 + row.forEach((k, v) -> env.put(k, toNumberIfPossible(v)));
  73 + try {
  74 + Object r = AviatorEvaluator.compile(expression, true).execute(env);
  75 + return r == null ? BigDecimal.ZERO : new BigDecimal(String.valueOf(r));
  76 + } catch (Exception e) {
  77 + log.warn("派生表达式求值失败 expr={} err={}", expression, e.getMessage());
  78 + return BigDecimal.ZERO;
  79 + }
  80 + }
  81 +
  82 + /**
  83 + * 通用表达式求值(供 M4 场景步骤 condition/switch/while/assign/script 使用)。
  84 + * 与规则同源:支持 #{paramId} 引用全局参数,缺变量/出错返回 null。
  85 + */
  86 + public Object evalValue(String expression, Map<String, Object> vars) {
  87 + if (expression == null || expression.isBlank()) return null;
  88 + Map<String, Object> env = new HashMap<>();
  89 + models.ruleGlobalParams().forEach((k, v) -> env.put(k, toNumberIfPossible(v)));
  90 + vars.forEach((k, v) -> env.put(k, toNumberIfPossible(v)));
  91 + String processed = PARAM_REF.matcher(expression).replaceAll("$1");
  92 + try {
  93 + return AviatorEvaluator.compile(processed, true).execute(env);
  94 + } catch (Exception e) {
  95 + log.warn("表达式求值失败 expr={} err={}", processed, e.getMessage());
  96 + return null;
  97 + }
  98 + }
  99 +
  100 + /** 求布尔表达式(如 "totalAmount > single_order_max_amount");非真即 false。 */
  101 + public boolean evalBool(String expression, Map<String, Object> vars) {
  102 + Object v = evalValue(expression, vars);
  103 + return Boolean.TRUE.equals(v) || "true".equalsIgnoreCase(String.valueOf(v));
  104 + }
  105 +
  106 + private static Object toNumberIfPossible(Object v) {
  107 + if (v instanceof Number || v instanceof Boolean) return v;
  108 + if (v == null) return null;
  109 + String s = String.valueOf(v);
  110 + try {
  111 + if (s.matches("-?\\d+")) return Long.parseLong(s);
  112 + if (s.matches("-?\\d+\\.\\d+")) return new BigDecimal(s);
  113 + } catch (Exception ignore) { /* fall through */ }
  114 + return s;
  115 + }
  116 +
  117 + @SuppressWarnings("unchecked")
  118 + private static Map<String, Object> asMap(Object o) {
  119 + return o instanceof Map ? (Map<String, Object>) o : Collections.emptyMap();
  120 + }
  121 +
  122 + @SuppressWarnings("unchecked")
  123 + private static List<Object> asList(Object o) {
  124 + return o instanceof List ? (List<Object>) o : Collections.emptyList();
  125 + }
  126 +}
... ...
engine/src/main/java/com/onto/engine/rule/RuleHit.java 0 → 100644
  1 +++ a/engine/src/main/java/com/onto/engine/rule/RuleHit.java
  1 +package com.onto.engine.rule;
  2 +
  3 +/** 一条命中的 MetaRule 规则结果。 */
  4 +public record RuleHit(String ruleId, String ruleName, String effect, String message) {}
... ...
engine/src/main/java/com/onto/engine/security/EncryptService.java 0 → 100644
  1 +++ a/engine/src/main/java/com/onto/engine/security/EncryptService.java
  1 +package com.onto.engine.security;
  2 +
  3 +import org.springframework.beans.factory.annotation.Value;
  4 +import org.springframework.stereotype.Component;
  5 +
  6 +import javax.crypto.Cipher;
  7 +import javax.crypto.spec.GCMParameterSpec;
  8 +import javax.crypto.spec.SecretKeySpec;
  9 +import java.nio.charset.StandardCharsets;
  10 +import java.security.MessageDigest;
  11 +import java.util.Base64;
  12 +
  13 +/**
  14 + * 字段级存储加密(兑现 M5 encryptRules.storageEncrypt)。AES-256-GCM,落库前加密、读出后解密。
  15 + *
  16 + * <p>演示态使用固定派生密钥(可经 onto.encrypt-key 覆盖);生产应改为 KMS/密钥轮换。
  17 + * 密文以 "enc:" 前缀标记,便于幂等(已加密的值不会二次加密)。
  18 + */
  19 +@Component
  20 +public class EncryptService {
  21 +
  22 + private static final String PREFIX = "enc:";
  23 + private static final int IV_LEN = 12;
  24 + private static final int TAG_BITS = 128;
  25 +
  26 + private final SecretKeySpec key;
  27 + // 固定 IV 用于演示可复现(同明文→同密文,便于去重/等值查询);生产必须随机 IV。
  28 + private final byte[] iv;
  29 +
  30 + public EncryptService(@Value("${onto.encrypt-key:onto-demo-key}") String secret) {
  31 + try {
  32 + byte[] digest = MessageDigest.getInstance("SHA-256").digest(secret.getBytes(StandardCharsets.UTF_8));
  33 + this.key = new SecretKeySpec(digest, "AES");
  34 + byte[] ivBytes = new byte[IV_LEN];
  35 + System.arraycopy(digest, 0, ivBytes, 0, IV_LEN);
  36 + this.iv = ivBytes;
  37 + } catch (Exception e) {
  38 + throw new IllegalStateException("初始化加密服务失败", e);
  39 + }
  40 + }
  41 +
  42 + public String encrypt(Object plain) {
  43 + if (plain == null) return null;
  44 + String s = String.valueOf(plain);
  45 + if (s.startsWith(PREFIX)) return s; // 已加密,幂等
  46 + try {
  47 + Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
  48 + c.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(TAG_BITS, iv));
  49 + byte[] enc = c.doFinal(s.getBytes(StandardCharsets.UTF_8));
  50 + return PREFIX + Base64.getEncoder().encodeToString(enc);
  51 + } catch (Exception e) {
  52 + throw new IllegalStateException("加密失败", e);
  53 + }
  54 + }
  55 +
  56 + public String decrypt(Object stored) {
  57 + if (stored == null) return null;
  58 + String s = String.valueOf(stored);
  59 + if (!s.startsWith(PREFIX)) return s; // 非密文,原样返回
  60 + try {
  61 + Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
  62 + c.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(TAG_BITS, iv));
  63 + byte[] dec = c.doFinal(Base64.getDecoder().decode(s.substring(PREFIX.length())));
  64 + return new String(dec, StandardCharsets.UTF_8);
  65 + } catch (Exception e) {
  66 + return s; // 解密失败时不阻断读取
  67 + }
  68 + }
  69 +}
... ...
engine/src/main/java/com/onto/engine/security/MaskService.java 0 → 100644
  1 +++ a/engine/src/main/java/com/onto/engine/security/MaskService.java
  1 +package com.onto.engine.security;
  2 +
  3 +import com.onto.engine.model.ModelRepository;
  4 +import org.springframework.stereotype.Component;
  5 +
  6 +import java.util.regex.Matcher;
  7 +import java.util.regex.Pattern;
  8 +
  9 +/**
  10 + * M5 脱敏服务:按 maskRules(如 contactPhone/receiverPhone: "{first3}****{last4}")对敏感字段做掩码。
  11 + * 前端渲染引擎读取 maskField 标记,后端返回读数据时套用本服务,规则同源于 M5。
  12 + */
  13 +@Component
  14 +public class MaskService {
  15 +
  16 + private static final Pattern FIRST = Pattern.compile("\\{first(\\d+)\\}");
  17 + private static final Pattern LAST = Pattern.compile("\\{last(\\d+)\\}");
  18 +
  19 + private final ModelRepository models;
  20 +
  21 + public MaskService(ModelRepository models) {
  22 + this.models = models;
  23 + }
  24 +
  25 + /** 该领域字段是否配置了脱敏规则。 */
  26 + public boolean isMasked(String domainFieldName) {
  27 + return models.maskRules().containsKey(domainFieldName);
  28 + }
  29 +
  30 + /** 按 M5 规则对某字段值脱敏;无规则则原样返回。 */
  31 + public String mask(String domainFieldName, Object value) {
  32 + if (value == null) return null;
  33 + Object rule = models.maskRules().get(domainFieldName);
  34 + String s = String.valueOf(value);
  35 + if (rule == null) return s;
  36 + return applyPattern(String.valueOf(rule), s);
  37 + }
  38 +
  39 + private String applyPattern(String pattern, String value) {
  40 + String out = pattern;
  41 + out = replaceToken(FIRST, out, value, true);
  42 + out = replaceToken(LAST, out, value, false);
  43 + return out;
  44 + }
  45 +
  46 + private String replaceToken(Pattern p, String pattern, String value, boolean first) {
  47 + Matcher m = p.matcher(pattern);
  48 + StringBuilder sb = new StringBuilder();
  49 + while (m.find()) {
  50 + int n = Integer.parseInt(m.group(1));
  51 + String frag;
  52 + if (value.length() <= n) {
  53 + frag = value;
  54 + } else if (first) {
  55 + frag = value.substring(0, n);
  56 + } else {
  57 + frag = value.substring(value.length() - n);
  58 + }
  59 + m.appendReplacement(sb, Matcher.quoteReplacement(frag));
  60 + }
  61 + m.appendTail(sb);
  62 + return sb.toString();
  63 + }
  64 +}
... ...
engine/src/main/java/com/onto/engine/util/IdGen.java 0 → 100644
  1 +++ a/engine/src/main/java/com/onto/engine/util/IdGen.java
  1 +package com.onto.engine.util;
  2 +
  3 +import java.util.concurrent.atomic.AtomicLong;
  4 +
  5 +/** 轻量雪花风格 ID 生成器(ME.globalConfig.idGenerator = snowflake 的演示态实现)。 */
  6 +public final class IdGen {
  7 +
  8 + private static final AtomicLong SEQ = new AtomicLong(0);
  9 + private static final long EPOCH = 1_700_000_000_000L;
  10 +
  11 + private IdGen() {}
  12 +
  13 + public static String next(String prefix) {
  14 + long ms = System.currentTimeMillis() - EPOCH;
  15 + long seq = SEQ.incrementAndGet() & 0xFFF;
  16 + return prefix + Long.toString((ms << 12) | seq, 36).toUpperCase();
  17 + }
  18 +}
... ...
engine/src/main/java/com/onto/engine/web/CommandController.java 0 → 100644
  1 +++ a/engine/src/main/java/com/onto/engine/web/CommandController.java
  1 +package com.onto.engine.web;
  2 +
  3 +import com.onto.engine.command.CommandEngine;
  4 +import org.springframework.beans.factory.annotation.Value;
  5 +import org.springframework.web.bind.annotation.*;
  6 +
  7 +import java.util.Map;
  8 +
  9 +/**
  10 + * 命令 API:场景命令的预检与执行(通用,非订单专用)。
  11 + *
  12 + * <p><b>认证说明(演示态)</b>:M5 声明 authMode=oauth2_jwt,但本 Demo 未接入真实 OAuth2/JWT。
  13 + * 主体来自 {@code X-Principal} 头(可伪造),仅用于演示 M5 权限模型的"通过/拒绝"分支。
  14 + * 生产必须以经校验的 JWT/会话派生主体替换该头,切勿信任客户端自报身份。
  15 + */
  16 +@RestController
  17 +@RequestMapping("/api/scene")
  18 +public class CommandController {
  19 +
  20 + private final CommandEngine commandEngine;
  21 +
  22 + @Value("${onto.default-principal:normal_user}")
  23 + private String defaultPrincipal;
  24 +
  25 + public CommandController(CommandEngine commandEngine) {
  26 + this.commandEngine = commandEngine;
  27 + }
  28 +
  29 + /** 提交前实时预检:校验 + MetaRule 风控(不落库)。 */
  30 + @PostMapping("/{sceneId}/precheck")
  31 + public Map<String, Object> precheck(@PathVariable String sceneId,
  32 + @RequestBody Map<String, Object> payload) {
  33 + return commandEngine.precheck(sceneId, payload);
  34 + }
  35 +
  36 + /** 执行场景命令(完整解释执行:校验→风控→落库→发事件→跨聚合扣库存)。 */
  37 + @PostMapping("/{sceneId}/execute")
  38 + public Map<String, Object> execute(@PathVariable String sceneId,
  39 + @RequestBody Map<String, Object> payload,
  40 + @RequestHeader(value = "X-Principal", required = false) String principal) {
  41 + String p = (principal == null || principal.isBlank()) ? defaultPrincipal : principal;
  42 + return commandEngine.execute(sceneId, payload, p);
  43 + }
  44 +}
... ...
engine/src/main/java/com/onto/engine/web/DataController.java 0 → 100644
  1 +++ a/engine/src/main/java/com/onto/engine/web/DataController.java
  1 +package com.onto.engine.web;
  2 +
  3 +import com.onto.engine.event.EventEngine;
  4 +import com.onto.engine.persist.DomainReadService;
  5 +import org.springframework.web.bind.annotation.*;
  6 +
  7 +import java.util.List;
  8 +import java.util.Map;
  9 +
  10 +/** 业务数据 API:下拉引用数据源、已创建订单列表、事件 Outbox 查看。 */
  11 +@RestController
  12 +@RequestMapping("/api/data")
  13 +public class DataController {
  14 +
  15 + private final DomainReadService readService;
  16 + private final EventEngine eventEngine;
  17 +
  18 + public DataController(DomainReadService readService, EventEngine eventEngine) {
  19 + this.readService = readService;
  20 + this.eventEngine = eventEngine;
  21 + }
  22 +
  23 + /** 某聚合根的下拉选项(如客户、商品),value/label 及全部领域字段。 */
  24 + @GetMapping("/{aggregateId}")
  25 + public List<Map<String, Object>> options(@PathVariable String aggregateId) {
  26 + return readService.options(aggregateId);
  27 + }
  28 +
  29 + /** 某聚合根记录列表(通用,脱敏后)。取代旧的订单专用端点。 */
  30 + @GetMapping("/{aggregateId}/list")
  31 + public List<Map<String, Object>> list(@PathVariable String aggregateId) {
  32 + return readService.listRoot(aggregateId);
  33 + }
  34 +
  35 + /** 最近事件(Outbox)。 */
  36 + @GetMapping("/events/recent")
  37 + public List<Map<String, Object>> events() {
  38 + return eventEngine.recentOutbox(30);
  39 + }
  40 +}
... ...
engine/src/main/java/com/onto/engine/web/MetaController.java 0 → 100644
  1 +++ a/engine/src/main/java/com/onto/engine/web/MetaController.java
  1 +package com.onto.engine.web;
  2 +
  3 +import com.onto.engine.meta.SceneSchemaService;
  4 +import com.onto.engine.model.ModelRepository;
  5 +import com.onto.engine.persist.SchemaInitializer;
  6 +import org.springframework.http.HttpStatus;
  7 +import org.springframework.web.bind.annotation.*;
  8 +import org.springframework.web.server.ResponseStatusException;
  9 +
  10 +import java.util.*;
  11 +
  12 +/** 元数据 API:场景渲染 Schema、模型查看、热重载。 */
  13 +@RestController
  14 +@RequestMapping("/api/meta")
  15 +public class MetaController {
  16 +
  17 + private final ModelRepository models;
  18 + private final SceneSchemaService sceneSchema;
  19 + private final SchemaInitializer schemaInitializer;
  20 +
  21 + public MetaController(ModelRepository models, SceneSchemaService sceneSchema,
  22 + SchemaInitializer schemaInitializer) {
  23 + this.models = models;
  24 + this.sceneSchema = sceneSchema;
  25 + this.schemaInitializer = schemaInitializer;
  26 + }
  27 +
  28 + /** 场景列表(供前端切换/展示)。 */
  29 + @GetMapping("/scenes")
  30 + public List<Map<String, Object>> scenes() {
  31 + List<Map<String, Object>> out = new ArrayList<>();
  32 + for (Object s : models.scenes()) {
  33 + Map<?, ?> sm = (Map<?, ?>) s;
  34 + Map<String, Object> item = new LinkedHashMap<>();
  35 + item.put("sceneId", sm.get("sceneId"));
  36 + item.put("sceneName", sm.get("sceneName"));
  37 + Map<String, Object> tpl = models.templateByScene(String.valueOf(sm.get("sceneId")));
  38 + item.put("hasTemplate", !tpl.isEmpty());
  39 + item.put("templateName", tpl.get("templateName"));
  40 + out.add(item);
  41 + }
  42 + return out;
  43 + }
  44 +
  45 + /** 某场景的完整渲染元数据(M8+M1+M2+M3+M5+MetaRule 合成)。 */
  46 + @GetMapping("/scene/{sceneId}")
  47 + public Map<String, Object> scene(@PathVariable String sceneId) {
  48 + return sceneSchema.buildSceneSchema(sceneId);
  49 + }
  50 +
  51 + /** 某聚合根的标识与属性(供前端通用地构造列表列,无需硬编码字段名)。 */
  52 + @GetMapping("/aggregate/{aggregateId}")
  53 + public Map<String, Object> aggregate(@PathVariable String aggregateId) {
  54 + List<Map<String, Object>> attrs = new ArrayList<>();
  55 + for (Object at : models.rootAttributes(aggregateId)) {
  56 + Map<?, ?> am = (Map<?, ?>) at;
  57 + Map<String, Object> a = new LinkedHashMap<>();
  58 + a.put("name", am.get("name"));
  59 + a.put("type", am.get("type"));
  60 + attrs.add(a);
  61 + }
  62 + Map<String, Object> out = new LinkedHashMap<>();
  63 + out.put("aggregateId", aggregateId);
  64 + out.put("rootIdentifier", models.rootIdentifier(aggregateId));
  65 + out.put("displayField", models.displayField(aggregateId));
  66 + out.put("attributes", attrs);
  67 + return out;
  68 + }
  69 +
  70 + /** 已加载的本体模型层清单。 */
  71 + @GetMapping("/models")
  72 + public Map<String, Object> modelList() {
  73 + List<Map<String, Object>> layers = new ArrayList<>();
  74 + for (String code : models.layerCodes()) {
  75 + Map<String, Object> m = new LinkedHashMap<>();
  76 + m.put("code", code);
  77 + m.put("fileName", models.fileNameOf(code));
  78 + layers.add(m);
  79 + }
  80 + Map<String, Object> out = new LinkedHashMap<>();
  81 + out.put("modelsDir", models.modelsDir().toString());
  82 + out.put("layers", layers);
  83 + return out;
  84 + }
  85 +
  86 + /** 某一层解析后的模型内容(供"运行原理/模型查看器"展示)。 */
  87 + @GetMapping("/model/{code}")
  88 + public Map<String, Object> model(@PathVariable String code) {
  89 + Map<String, Object> out = new LinkedHashMap<>();
  90 + out.put("code", code);
  91 + out.put("fileName", models.fileNameOf(code));
  92 + out.put("content", models.rawLayer(code));
  93 + return out;
  94 + }
  95 +
  96 + /**
  97 + * 热重载:重新读取磁盘 YAML 并补齐表结构,使模型改动即时生效(无需重启)。
  98 + * 仅 M8 dragRolePermission 声明的管理员可触发(演示态用 X-Principal 头表示主体;生产应换真实认证)。
  99 + */
  100 + @PostMapping("/reload")
  101 + public Map<String, Object> reload(@RequestHeader(value = "X-Principal", required = false) String principal) {
  102 + List<Object> admins = asList(models.frontGlobalConfig().get("dragRolePermission"));
  103 + if (!admins.isEmpty() && !admins.contains(principal)) {
  104 + throw new ResponseStatusException(HttpStatus.FORBIDDEN,
  105 + "仅管理员 " + admins + " 可热重载模型(当前主体:" + principal + ")");
  106 + }
  107 + models.reload();
  108 + schemaInitializer.refresh();
  109 + Map<String, Object> out = new LinkedHashMap<>();
  110 + out.put("reloaded", true);
  111 + out.put("layers", models.layerCodes());
  112 + out.put("message", "本体模型已热重载,前端刷新即可看到新的表单与行为");
  113 + return out;
  114 + }
  115 +
  116 + @SuppressWarnings("unchecked")
  117 + private static List<Object> asList(Object o) {
  118 + return o instanceof List ? (List<Object>) o : java.util.Collections.emptyList();
  119 + }
  120 +}
... ...
engine/src/main/resources/application-prod.yml 0 → 100644
  1 +++ a/engine/src/main/resources/application-prod.yml
  1 +# 生产 profile:连真实 MySQL8。启用方式:--spring.profiles.active=prod
  2 +# 环境变量可覆盖:DB_HOST/DB_PORT/DB_NAME/DB_USER/DB_PASS
  3 +spring:
  4 + datasource:
  5 + url: jdbc:mysql://${DB_HOST:mysql}:${DB_PORT:3306}/${DB_NAME:trade_db}?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8&allowPublicKeyRetrieval=true
  6 + driver-class-name: com.mysql.cj.jdbc.Driver
  7 + username: ${DB_USER:trade}
  8 + password: ${DB_PASS:trade123}
  9 + h2:
  10 + console:
  11 + enabled: false
  12 +
  13 +onto:
  14 + models-dir: ${ONTO_MODELS_DIR:/app/models}
  15 + # 生产 CORS 收紧为具体前端域名(逗号分隔),勿用 *
  16 + cors:
  17 + allowed-origins: ${CORS_ORIGINS:http://localhost:8088}
  18 + # 生产字段加密密钥必须经密管注入,切勿用演示默认值
  19 + encrypt-key: ${ONTO_ENCRYPT_KEY:请在生产用密管注入}
... ...
engine/src/main/resources/application.yml 0 → 100644
  1 +++ a/engine/src/main/resources/application.yml
  1 +server:
  2 + port: 8080
  3 +
  4 +spring:
  5 + application:
  6 + name: onto-engine
  7 + # 默认 H2 内存库,MySQL8 兼容模式,零外部依赖开箱即跑
  8 + datasource:
  9 + url: jdbc:h2:mem:trade_db;MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE;DB_CLOSE_DELAY=-1
  10 + driver-class-name: org.h2.Driver
  11 + username: sa
  12 + password: ""
  13 + h2:
  14 + console:
  15 + enabled: true
  16 + path: /h2-console
  17 +
  18 +# 本体模型引擎配置
  19 +onto:
  20 + # 模型目录:引擎启动时从此目录加载全部 YAML 本体模型;支持热重载(POST /api/meta/reload)
  21 + models-dir: ../models
  22 + # 演示态默认主体(M5 权限主体)
  23 + default-principal: normal_user
  24 +
  25 +logging:
  26 + level:
  27 + com.onto.engine: INFO
... ...
engine/src/main/resources/seed-data.yaml 0 → 100644
  1 +++ a/engine/src/main/resources/seed-data.yaml
  1 +# 演示引导数据(非本体模型的一部分,仅用于让"新增订单"页有真实的客户/商品可选,并让扣库存有库存可扣)
  2 +# 字段名严格对齐 M1 领域模型;引擎按 M3 映射落库到 t_customer_main / t_product_main。
  3 +CustomerAggregate:
  4 + - customerId: C001
  5 + customerName: 张三(普通客户)
  6 + contactPhone: "13800000001"
  7 + customerLevel: 普通
  8 + - customerId: C002
  9 + customerName: 李四(VIP客户)
  10 + contactPhone: "13800000002"
  11 + customerLevel: VIP
  12 + - customerId: C003
  13 + customerName: 王五(高级VIP)
  14 + contactPhone: "13800000003"
  15 + customerLevel: 高级VIP
  16 +
  17 +EmployeeAggregate:
  18 + # 审核统计计数需初始化为 0,REMOTE_INCREMENT_FIELD 的 col = col + N 才能在非空值上累加
  19 + - employeeId: E001
  20 + employeeName: 审核员小王
  21 + department: 风控部
  22 + auditApproveCount: 0
  23 + auditRejectCount: 0
  24 + - employeeId: E002
  25 + employeeName: 审核主管李雷
  26 + department: 风控部
  27 + auditApproveCount: 0
  28 + auditRejectCount: 0
  29 +
  30 +ProductAggregate:
  31 + - productId: P001
  32 + productName: 笔记本电脑
  33 + skuCode: SKU-NB-001
  34 + category: 电脑办公
  35 + saleCurrency: CNY
  36 + salePrice: 6999
  37 + stockNum: 100
  38 + isOnSale: true
  39 + - productId: P002
  40 + productName: 无线鼠标(低库存)
  41 + skuCode: SKU-MS-002
  42 + category: 电脑配件
  43 + saleCurrency: CNY
  44 + salePrice: 199
  45 + stockNum: 8
  46 + isOnSale: true
  47 + - productId: P003
  48 + productName: 机械键盘
  49 + skuCode: SKU-KB-003
  50 + category: 电脑配件
  51 + saleCurrency: CNY
  52 + salePrice: 499
  53 + stockNum: 50
  54 + isOnSale: true
... ...
engine/src/test/java/com/onto/engine/OrderSceneTest.java 0 → 100644
  1 +++ a/engine/src/test/java/com/onto/engine/OrderSceneTest.java
  1 +package com.onto.engine;
  2 +
  3 +import com.fasterxml.jackson.databind.ObjectMapper;
  4 +import org.junit.jupiter.api.Test;
  5 +import org.springframework.beans.factory.annotation.Autowired;
  6 +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
  7 +import org.springframework.boot.test.context.SpringBootTest;
  8 +import org.springframework.http.MediaType;
  9 +import org.springframework.jdbc.core.JdbcTemplate;
  10 +import org.springframework.test.web.servlet.MockMvc;
  11 +
  12 +import static org.assertj.core.api.Assertions.assertThat;
  13 +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
  14 +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
  15 +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
  16 +
  17 +/**
  18 + * 「新增订单/新增客户」通用执行端到端测试:验证模型驱动的校验、风控、超卖防护、权限、跨聚合一致性。
  19 + */
  20 +@SpringBootTest
  21 +@AutoConfigureMockMvc
  22 +class OrderSceneTest {
  23 +
  24 + @Autowired
  25 + MockMvc mvc;
  26 +
  27 + @Autowired
  28 + JdbcTemplate jdbc;
  29 +
  30 + @Autowired
  31 + ObjectMapper json;
  32 +
  33 + /** 建一单并返回 orderId(用于 modify/cancel 场景测试)。 */
  34 + private String createOrder(String productId, int qty) throws Exception {
  35 + String body = ("""
  36 + {"master":{"customerId":"C002","totalCurrency":"CNY","createTime":"2026-07-20T10:00:00"},
  37 + "details":{"OrderItem":[{"productId":"%s","quantity":%d,"itemPrice":100,"itemCurrency":"CNY"}],
  38 + "PaymentTerm":[{"paymentType":"月结","dueDays":30}],
  39 + "DeliveryAddressEntity":[{"province":"广东","city":"深圳","district":"南山","streetDetail":"旧地址",
  40 + "receiverName":"李四","receiverPhone":"13800002222"}]}}
  41 + """).formatted(productId, qty);
  42 + String resp = mvc.perform(post("/api/scene/SCENE_CREATE_ORDER/execute")
  43 + .contentType(MediaType.APPLICATION_JSON).content(body))
  44 + .andExpect(jsonPath("$.success").value(true)).andReturn().getResponse().getContentAsString();
  45 + return json.readTree(resp).get("rootId").asText();
  46 + }
  47 +
  48 + @Test
  49 + void modifyOrderAddress_updatesChildEntityRow() throws Exception {
  50 + String oid = createOrder("P003", 1);
  51 + String mod = ("""
  52 + {"master":{"orderId":"%s"},
  53 + "details":{"DeliveryAddressEntity":[{"province":"北京","city":"北京","district":"海淀",
  54 + "streetDetail":"新地址999号","receiverName":"李四","receiverPhone":"13900009999"}]}}
  55 + """).formatted(oid);
  56 + mvc.perform(post("/api/scene/SCENE_MODIFY_ORDER_ADDR/execute").header("X-Principal", "backend_admin")
  57 + .contentType(MediaType.APPLICATION_JSON).content(mod))
  58 + .andExpect(jsonPath("$.success").value(true));
  59 + String street = jdbc.queryForObject(
  60 + "SELECT street FROM t_order_address WHERE order_id = ?", String.class, oid);
  61 + assertThat(street).isEqualTo("新地址999号");
  62 + }
  63 +
  64 + @Test
  65 + void cancelOrder_returnsStockViaGenericStockReturn() throws Exception {
  66 + String oid = createOrder("P001", 3);
  67 + Long afterCreate = jdbc.queryForObject(
  68 + "SELECT stock_num FROM t_product_main WHERE product_id = 'P001'", Long.class);
  69 + mvc.perform(post("/api/scene/SCENE_CANCEL_ORDER/execute").header("X-Principal", "backend_admin")
  70 + .contentType(MediaType.APPLICATION_JSON).content("{\"master\":{\"orderId\":\"" + oid + "\"}}"))
  71 + .andExpect(jsonPath("$.success").value(true))
  72 + .andExpect(jsonPath("$.stockActions[0].op").value("increment"));
  73 + Long afterCancel = jdbc.queryForObject(
  74 + "SELECT stock_num FROM t_product_main WHERE product_id = 'P001'", Long.class);
  75 + assertThat(afterCancel).isEqualTo(afterCreate + 3);
  76 + }
  77 +
  78 + @Test
  79 + void createProduct_asAdmin_persists_normalUserDenied() throws Exception {
  80 + String body = """
  81 + {"master":{"productName":"测试键帽","skuCode":"SKU-KC-999","category":"配件",
  82 + "saleCurrency":"CNY","salePrice":99,"stockNum":10,"isOnSale":true}}
  83 + """;
  84 + String resp = mvc.perform(post("/api/scene/SCENE_CREATE_PRODUCT/execute").header("X-Principal", "backend_admin")
  85 + .contentType(MediaType.APPLICATION_JSON).content(body))
  86 + .andExpect(jsonPath("$.success").value(true))
  87 + .andExpect(jsonPath("$.rootIdField").value("productId")).andReturn().getResponse().getContentAsString();
  88 + String pid = json.readTree(resp).get("rootId").asText();
  89 + assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM t_product_main WHERE product_id = ?", Long.class, pid))
  90 + .isEqualTo(1L);
  91 + // 普通用户无 product_create 权限 → 拒绝
  92 + mvc.perform(post("/api/scene/SCENE_CREATE_PRODUCT/execute").header("X-Principal", "normal_user")
  93 + .contentType(MediaType.APPLICATION_JSON).content(body))
  94 + .andExpect(jsonPath("$.success").value(false))
  95 + .andExpect(jsonPath("$.stage").value("PERMISSION"));
  96 + }
  97 +
  98 + @Test
  99 + void createProduct_negativePrice_isRejectedByConstraint() throws Exception {
  100 + String body = """
  101 + {"master":{"productName":"坏价商品","skuCode":"SKU-BAD-1","category":"x",
  102 + "saleCurrency":"CNY","salePrice":-5,"stockNum":10,"isOnSale":true}}
  103 + """;
  104 + mvc.perform(post("/api/scene/SCENE_CREATE_PRODUCT/execute").header("X-Principal", "backend_admin")
  105 + .contentType(MediaType.APPLICATION_JSON).content(body))
  106 + .andExpect(jsonPath("$.success").value(false))
  107 + .andExpect(jsonPath("$.stage").value("VALIDATION"));
  108 + }
  109 +
  110 + @Test
  111 + void createProduct_duplicateSku_isRejectedByUnique() throws Exception {
  112 + // 种子商品 P001 的 skuCode = SKU-NB-001,再建同 sku 应被唯一性约束拦截
  113 + String body = """
  114 + {"master":{"productName":"重复SKU","skuCode":"SKU-NB-001","category":"x",
  115 + "saleCurrency":"CNY","salePrice":10,"stockNum":1,"isOnSale":true}}
  116 + """;
  117 + mvc.perform(post("/api/scene/SCENE_CREATE_PRODUCT/execute").header("X-Principal", "backend_admin")
  118 + .contentType(MediaType.APPLICATION_JSON).content(body))
  119 + .andExpect(jsonPath("$.success").value(false))
  120 + .andExpect(jsonPath("$.stage").value("VALIDATION"));
  121 + }
  122 +
  123 + @Test
  124 + void modifyProductPrice_updatesRoot() throws Exception {
  125 + mvc.perform(post("/api/scene/SCENE_MODIFY_PRODUCT_PRICE/execute").header("X-Principal", "backend_admin")
  126 + .contentType(MediaType.APPLICATION_JSON)
  127 + .content("{\"master\":{\"productId\":\"P003\",\"salePrice\":333,\"saleCurrency\":\"CNY\"}}"))
  128 + .andExpect(jsonPath("$.success").value(true));
  129 + assertThat(jdbc.queryForObject("SELECT sale_price FROM t_product_main WHERE product_id='P003'", Double.class))
  130 + .isEqualTo(333.0);
  131 + }
  132 +
  133 + @Test
  134 + void modifyCustomer_updatesRoot() throws Exception {
  135 + mvc.perform(post("/api/scene/SCENE_MODIFY_CUSTOMER/execute").header("X-Principal", "backend_admin")
  136 + .contentType(MediaType.APPLICATION_JSON)
  137 + .content("{\"master\":{\"customerId\":\"C003\",\"customerLevel\":\"普通\"}}"))
  138 + .andExpect(jsonPath("$.success").value(true));
  139 + assertThat(jdbc.queryForObject("SELECT cust_level FROM t_customer_main WHERE customer_id='C003'", String.class))
  140 + .isEqualTo("普通");
  141 + }
  142 +
  143 + @Test
  144 + void auditOrder_highAmountPass_updatesOrderAndBumpsAuditorApproveCountAndRemark() throws Exception {
  145 + // 金额 2*100=200 (>100) + PASS:订单置 PASS、REMOTE_QUERY 回填审核人名;
  146 + // IF(root.totalAmount>100) → REMOTE_SET_FIELD 覆盖员工备注;IF(PASS) → REMOTE_INCREMENT_FIELD 通过数 +1。
  147 + String oid = createOrder("P003", 2);
  148 + String body = ("""
  149 + {"master":{"orderId":"%s","auditorId":"E001","auditResult":"PASS","auditRemark":"资料齐全,通过"}}
  150 + """).formatted(oid);
  151 + mvc.perform(post("/api/scene/SCENE_AUDIT_ORDER/execute").header("X-Principal", "backend_admin")
  152 + .contentType(MediaType.APPLICATION_JSON).content(body))
  153 + .andExpect(jsonPath("$.success").value(true))
  154 + .andExpect(jsonPath("$.command").value("AuditOrder"));
  155 + // 订单侧
  156 + assertThat(jdbc.queryForObject("SELECT order_status FROM t_order_main WHERE order_id=?", String.class, oid))
  157 + .isEqualTo("PASS");
  158 + assertThat(jdbc.queryForObject("SELECT auditor_name FROM t_order_main WHERE order_id=?", String.class, oid))
  159 + .isEqualTo("审核员小王"); // REMOTE_QUERY_SET_FIELD 回填
  160 + // 审核人(员工)侧:条件回写
  161 + assertThat(jdbc.queryForObject("SELECT approve_count FROM t_employee_main WHERE employee_id='E001'", Long.class))
  162 + .isEqualTo(1L);
  163 + assertThat(jdbc.queryForObject("SELECT remark FROM t_employee_main WHERE employee_id='E001'", String.class))
  164 + .isEqualTo("资料齐全,通过");
  165 + assertThat(jdbc.queryForObject("SELECT last_audit_time FROM t_employee_main WHERE employee_id='E001'", Object.class))
  166 + .isNotNull();
  167 + }
  168 +
  169 + @Test
  170 + void auditOrder_lowAmountReject_skipsRemarkAndBumpsRejectCount() throws Exception {
  171 + // 金额 1*100=100 (不 >100) + REJECT:IF(root.totalAmount>100) 为假 → 不写员工备注;IF(REJECT) → 驳回数 +1。
  172 + String oid = createOrder("P001", 1);
  173 + String body = ("""
  174 + {"master":{"orderId":"%s","auditorId":"E002","auditResult":"REJECT","auditRemark":"信息不全"}}
  175 + """).formatted(oid);
  176 + mvc.perform(post("/api/scene/SCENE_AUDIT_ORDER/execute").header("X-Principal", "backend_admin")
  177 + .contentType(MediaType.APPLICATION_JSON).content(body))
  178 + .andExpect(jsonPath("$.success").value(true));
  179 + assertThat(jdbc.queryForObject("SELECT reject_count FROM t_employee_main WHERE employee_id='E002'", Long.class))
  180 + .isEqualTo(1L);
  181 + // 金额不足,备注分支未执行,员工备注保持为空
  182 + assertThat(jdbc.queryForObject("SELECT remark FROM t_employee_main WHERE employee_id='E002'", String.class))
  183 + .isNull();
  184 + }
  185 +
  186 + @Test
  187 + void createEmployee_initsCountsToZero_thenModify_normalUserDenied() throws Exception {
  188 + // CreateEmployee 的 bizSteps 用 SET_FIELD 把两个审核计数初始化为 0
  189 + String create = """
  190 + {"master":{"employeeName":"新审核员小赵","department":"风控部"}}
  191 + """;
  192 + String resp = mvc.perform(post("/api/scene/SCENE_CREATE_EMPLOYEE/execute").header("X-Principal", "backend_admin")
  193 + .contentType(MediaType.APPLICATION_JSON).content(create))
  194 + .andExpect(jsonPath("$.success").value(true))
  195 + .andExpect(jsonPath("$.rootIdField").value("employeeId")).andReturn().getResponse().getContentAsString();
  196 + String eid = json.readTree(resp).get("rootId").asText();
  197 + assertThat(jdbc.queryForObject("SELECT approve_count FROM t_employee_main WHERE employee_id=?", Long.class, eid))
  198 + .isEqualTo(0L);
  199 + assertThat(jdbc.queryForObject("SELECT reject_count FROM t_employee_main WHERE employee_id=?", Long.class, eid))
  200 + .isEqualTo(0L);
  201 + // ModifyEmployee:改部门 + 备注
  202 + String modify = ("""
  203 + {"master":{"employeeId":"%s","department":"合规部","remark":"转岗"}}
  204 + """).formatted(eid);
  205 + mvc.perform(post("/api/scene/SCENE_MODIFY_EMPLOYEE/execute").header("X-Principal", "backend_admin")
  206 + .contentType(MediaType.APPLICATION_JSON).content(modify))
  207 + .andExpect(jsonPath("$.success").value(true));
  208 + assertThat(jdbc.queryForObject("SELECT dept FROM t_employee_main WHERE employee_id=?", String.class, eid))
  209 + .isEqualTo("合规部");
  210 + assertThat(jdbc.queryForObject("SELECT remark FROM t_employee_main WHERE employee_id=?", String.class, eid))
  211 + .isEqualTo("转岗");
  212 + // 普通用户无 employee_create 权限 → 拒绝
  213 + mvc.perform(post("/api/scene/SCENE_CREATE_EMPLOYEE/execute").header("X-Principal", "normal_user")
  214 + .contentType(MediaType.APPLICATION_JSON).content(create))
  215 + .andExpect(jsonPath("$.success").value(false))
  216 + .andExpect(jsonPath("$.stage").value("PERMISSION"));
  217 + }
  218 +
  219 + @Test
  220 + void demoStepTypes_noTemplate_interpretsAllStepTypes() throws Exception {
  221 + // 无表单场景:空 payload 直接解释 flowSteps,验证控制流/Saga/EDA/人工各类步骤通用解释成功(不落库)
  222 + mvc.perform(post("/api/scene/SCENE_DEMO_STEPTYPES/execute").header("X-Principal", "backend_admin")
  223 + .contentType(MediaType.APPLICATION_JSON).content("{}"))
  224 + .andExpect(status().isOk())
  225 + .andExpect(jsonPath("$.success").value(true));
  226 + }
  227 +
  228 + @Test
  229 + void auditScene_schema_rendersAuditorAsEmployeeReferenceDropdown() throws Exception {
  230 + // 通用引用下拉:auditorId 带 M1 refAggregate=EmployeeAggregate,合成 Schema 应给出员工选项数据源
  231 + mvc.perform(get("/api/meta/scene/SCENE_AUDIT_ORDER"))
  232 + .andExpect(status().isOk())
  233 + .andExpect(jsonPath("$.template.templateId").value("template_order_audit"))
  234 + .andExpect(jsonPath("$.template.rootComponents[0].childComponents[1].fieldBind.optionsSource.aggregateId")
  235 + .value("EmployeeAggregate"));
  236 + }
  237 +
  238 + @Test
  239 + void sensitivePhone_isEncryptedAtRest() {
  240 + // M5 storageEncrypt 生效:物理列存密文(enc: 前缀),而非明文手机号
  241 + String raw = jdbc.queryForObject("SELECT phone FROM t_customer_main WHERE customer_id = 'C001'", String.class);
  242 + assertThat(raw).startsWith("enc:");
  243 + assertThat(raw).doesNotContain("13800000001");
  244 + }
  245 +
  246 + private static final String VALID_ORDER = """
  247 + {"master":{"customerId":"C002","totalCurrency":"CNY","createTime":"2026-07-20T10:00:00"},
  248 + "details":{"OrderItem":[{"productId":"P003","quantity":1,"itemPrice":499,"itemCurrency":"CNY"},
  249 + {"productId":"P001","quantity":1,"itemPrice":6999,"itemCurrency":"CNY"}],
  250 + "PaymentTerm":[{"paymentType":"月结","dueDays":30}],
  251 + "DeliveryAddressEntity":[{"province":"广东","city":"深圳","district":"南山","streetDetail":"科技园1号",
  252 + "receiverName":"李四","receiverPhone":"13800002222"}]}}
  253 + """;
  254 +
  255 + // 普通用户 + 明细金额 8*6999=55992 > 50000(服务端重算),应被 MetaRule REJECT
  256 + private static final String OVER_LIMIT_ORDER = """
  257 + {"master":{"customerId":"C001","totalCurrency":"CNY","createTime":"2026-07-20T10:00:00"},
  258 + "details":{"OrderItem":[{"productId":"P001","quantity":8,"itemPrice":6999,"itemCurrency":"CNY"}],
  259 + "PaymentTerm":[{"paymentType":"现金","dueDays":0}],
  260 + "DeliveryAddressEntity":[{"province":"广东","city":"深圳","district":"南山","streetDetail":"x",
  261 + "receiverName":"张三","receiverPhone":"13800001111"}]}}
  262 + """;
  263 +
  264 + private static final String BAD_PHONE_ORDER = """
  265 + {"master":{"customerId":"C002","totalCurrency":"CNY","createTime":"2026-07-20T10:00:00"},
  266 + "details":{"OrderItem":[{"productId":"P003","quantity":1,"itemPrice":499,"itemCurrency":"CNY"}],
  267 + "PaymentTerm":[{"paymentType":"现金","dueDays":0}],
  268 + "DeliveryAddressEntity":[{"province":"x","city":"y","district":"z","streetDetail":"w",
  269 + "receiverName":"赵六","receiverPhone":"139"}]}}
  270 + """;
  271 +
  272 + // P002 库存 8,下单 100 件 → 跨聚合扣减失败 → 整单回滚(防超卖)
  273 + private static final String OVERSELL_ORDER = """
  274 + {"master":{"customerId":"C002","totalCurrency":"CNY","createTime":"2026-07-20T10:00:00"},
  275 + "details":{"OrderItem":[{"productId":"P002","quantity":100,"itemPrice":199,"itemCurrency":"CNY"}],
  276 + "PaymentTerm":[{"paymentType":"现金","dueDays":0}],
  277 + "DeliveryAddressEntity":[{"province":"x","city":"y","district":"z","streetDetail":"w",
  278 + "receiverName":"钱七","receiverPhone":"13800007777"}]}}
  279 + """;
  280 +
  281 + // 负数数量:M1 constraints.min=1 应拒绝
  282 + private static final String NEGATIVE_QTY_ORDER = """
  283 + {"master":{"customerId":"C002","totalCurrency":"CNY","createTime":"2026-07-20T10:00:00"},
  284 + "details":{"OrderItem":[{"productId":"P003","quantity":-5,"itemPrice":499,"itemCurrency":"CNY"}],
  285 + "PaymentTerm":[{"paymentType":"现金","dueDays":0}],
  286 + "DeliveryAddressEntity":[{"province":"x","city":"y","district":"z","streetDetail":"w",
  287 + "receiverName":"钱七","receiverPhone":"13800007777"}]}}
  288 + """;
  289 +
  290 + private static final String NEW_CUSTOMER = """
  291 + {"master":{"customerName":"测试客户","contactPhone":"13900000000","customerLevel":"普通"}}
  292 + """;
  293 +
  294 + @Test
  295 + void sceneSchema_isComposedFromModel() throws Exception {
  296 + mvc.perform(get("/api/meta/scene/SCENE_CREATE_ORDER"))
  297 + .andExpect(status().isOk())
  298 + .andExpect(jsonPath("$.template.templateType").value("master_detail_form"))
  299 + .andExpect(jsonPath("$.commandApi").value("/api/v1/order/create"))
  300 + .andExpect(jsonPath("$.template.rootComponents[0].compId").value("card_order_master"));
  301 + }
  302 +
  303 + @Test
  304 + void createOrder_valid_succeedsWithGenericConsistency() throws Exception {
  305 + mvc.perform(post("/api/scene/SCENE_CREATE_ORDER/execute")
  306 + .contentType(MediaType.APPLICATION_JSON).content(VALID_ORDER))
  307 + .andExpect(status().isOk())
  308 + .andExpect(jsonPath("$.success").value(true))
  309 + .andExpect(jsonPath("$.rootId").isNotEmpty())
  310 + .andExpect(jsonPath("$.rootIdField").value("orderId"))
  311 + .andExpect(jsonPath("$.childCount").value(4))
  312 + .andExpect(jsonPath("$.stockActions[0].status").value("OK"))
  313 + .andExpect(jsonPath("$.stockActions[0].targetCommand").value("StockDeduct"));
  314 + }
  315 +
  316 + @Test
  317 + void createOrder_overLimitNormalUser_isRejectedByMetaRule() throws Exception {
  318 + mvc.perform(post("/api/scene/SCENE_CREATE_ORDER/execute")
  319 + .contentType(MediaType.APPLICATION_JSON).content(OVER_LIMIT_ORDER))
  320 + .andExpect(status().isOk())
  321 + .andExpect(jsonPath("$.success").value(false))
  322 + .andExpect(jsonPath("$.stage").value("RULE_REJECT"));
  323 + }
  324 +
  325 + @Test
  326 + void createOrder_badPhone_failsConstraintValidation() throws Exception {
  327 + mvc.perform(post("/api/scene/SCENE_CREATE_ORDER/execute")
  328 + .contentType(MediaType.APPLICATION_JSON).content(BAD_PHONE_ORDER))
  329 + .andExpect(status().isOk())
  330 + .andExpect(jsonPath("$.success").value(false))
  331 + .andExpect(jsonPath("$.stage").value("VALIDATION"));
  332 + }
  333 +
  334 + @Test
  335 + void createOrder_insufficientStock_rollsBackNoOversell() throws Exception {
  336 + mvc.perform(post("/api/scene/SCENE_CREATE_ORDER/execute")
  337 + .contentType(MediaType.APPLICATION_JSON).content(OVERSELL_ORDER))
  338 + .andExpect(status().isOk())
  339 + .andExpect(jsonPath("$.success").value(false))
  340 + .andExpect(jsonPath("$.stage").value("CONSISTENCY_FAILED"));
  341 + }
  342 +
  343 + @Test
  344 + void createOrder_negativeQuantity_isRejected() throws Exception {
  345 + mvc.perform(post("/api/scene/SCENE_CREATE_ORDER/execute")
  346 + .contentType(MediaType.APPLICATION_JSON).content(NEGATIVE_QTY_ORDER))
  347 + .andExpect(status().isOk())
  348 + .andExpect(jsonPath("$.success").value(false))
  349 + .andExpect(jsonPath("$.stage").value("VALIDATION"));
  350 + }
  351 +
  352 + @Test
  353 + void precheck_overLimit_flagsReject() throws Exception {
  354 + mvc.perform(post("/api/scene/SCENE_CREATE_ORDER/precheck")
  355 + .contentType(MediaType.APPLICATION_JSON).content(OVER_LIMIT_ORDER))
  356 + .andExpect(status().isOk())
  357 + .andExpect(jsonPath("$.reject").value(true))
  358 + .andExpect(jsonPath("$.rejectRules[0].ruleId").value("R_ORDER_AMOUNT_LIMIT"));
  359 + }
  360 +
  361 + @Test
  362 + void createCustomer_requiresAdminPrincipal() throws Exception {
  363 + // 管理员:通过
  364 + mvc.perform(post("/api/scene/SCENE_CREATE_CUSTOMER/execute")
  365 + .header("X-Principal", "backend_admin")
  366 + .contentType(MediaType.APPLICATION_JSON).content(NEW_CUSTOMER))
  367 + .andExpect(status().isOk())
  368 + .andExpect(jsonPath("$.success").value(true))
  369 + .andExpect(jsonPath("$.rootIdField").value("customerId"));
  370 + // 普通用户:无权 → PERMISSION
  371 + mvc.perform(post("/api/scene/SCENE_CREATE_CUSTOMER/execute")
  372 + .header("X-Principal", "normal_user")
  373 + .contentType(MediaType.APPLICATION_JSON).content(NEW_CUSTOMER))
  374 + .andExpect(status().isOk())
  375 + .andExpect(jsonPath("$.success").value(false))
  376 + .andExpect(jsonPath("$.stage").value("PERMISSION"));
  377 + }
  378 +}
... ...
models/M0-meta-schema.yaml 0 → 100644
  1 +++ a/models/M0-meta-schema.yaml
  1 +# M0 全域元数据校验根规范 (v2.0)
  2 +# 全域本体校验根规范,新增React前端模板JSON Schema约束;统一前后端数据、组件、页面描述标准。
  3 +meta_model:
  4 + version: "2.0"
  5 + $schema: https://json-schema.org/draft/2020-12/schema
  6 + definitions:
  7 + # 基础通用标识类型
  8 + Identifier:
  9 + type: string
  10 + pattern: "^[a-z][a-zA-Z0-9]*$"
  11 + description: 小写驼峰字段/组件标识
  12 + UpperIdentifier:
  13 + type: string
  14 + pattern: "^[A-Z][a-zA-Z0-9]*$"
  15 + description: 大写驼峰聚合/命令/事件ID
  16 + NonEmptyString:
  17 + type: string
  18 + minLength: 1
  19 + SceneId:
  20 + type: string
  21 + pattern: "^SCENE_[A-Z0-9_]+$"
  22 + RuleId:
  23 + type: string
  24 + pattern: "^R_[A-Z0-9_]+$"
  25 + TopicName:
  26 + type: string
  27 + pattern: "^topic_[a-z0-9_]+$"
  28 + ConsistencyTypeEnum:
  29 + type: string
  30 + enum: [strong, eventual]
  31 + DbEngineEnum:
  32 + type: string
  33 + enum: [mysql8]
  34 + AuthModeEnum:
  35 + type: string
  36 + enum: [oauth2_jwt]
  37 +
  38 + # ========== M8前端专用枚举(新增) ==========
  39 + FrontTemplateTypeEnum:
  40 + type: string
  41 + enum: [single_table_form, master_detail_form, custom_drag_form]
  42 + description: single_table_form单实体表单;master_detail_form主从表单;custom_drag_form自定义空白拖拽模板
  43 + FrontComponentEnum:
  44 + type: string
  45 + enum: [input, number_input, date_picker, switch, table, card, text_area, select]
  46 + description: React底层原子拖拽组件
  47 + BindSourceTypeEnum:
  48 + type: string
  49 + enum: [aggregate_root, aggregate_entity]
  50 + description: 组件绑定数据源类型:聚合根/聚合内部子实体
  51 + ReactRenderModeEnum:
  52 + type: string
  53 + enum: [form_modal, page_container, drawer]
  54 + description: 页面渲染载体:弹窗/整页/侧边抽屉
  55 +
  56 + # M1 领域模型单元定义
  57 + M1_Attribute:
  58 + type: object
  59 + required: [name, type]
  60 + properties:
  61 + name: { $ref: "#/definitions/Identifier" }
  62 + type:
  63 + type: string
  64 + enum: [string, int, decimal, datetime, boolean]
  65 + refAggregate: { type: string }
  66 + refRoot: { type: string }
  67 + refIdentifier: { type: string }
  68 + description: { type: string }
  69 + M1_Relation:
  70 + type: object
  71 + required: [ref, relationType]
  72 + properties:
  73 + ref: { $ref: "#/definitions/NonEmptyString" }
  74 + relationType:
  75 + type: string
  76 + enum: [composition]
  77 + M1_Entity:
  78 + type: object
  79 + required: [name, localIdentifier, attributes]
  80 + properties:
  81 + name: { $ref: "#/definitions/NonEmptyString" }
  82 + localIdentifier: { $ref: "#/definitions/Identifier" }
  83 + attributes:
  84 + type: array
  85 + items: { $ref: "#/definitions/M1_Attribute" }
  86 + M1_AggregateRoot:
  87 + type: object
  88 + required: [name, identifier, attributes]
  89 + properties:
  90 + name: { $ref: "#/definitions/NonEmptyString" }
  91 + identifier: { $ref: "#/definitions/Identifier" }
  92 + attributes:
  93 + type: array
  94 + items: { $ref: "#/definitions/M1_Attribute" }
  95 + relations:
  96 + type: array
  97 + items: { $ref: "#/definitions/M1_Relation" }
  98 + M1_Aggregate:
  99 + type: object
  100 + required: [aggregateId, description, aggregateRoot]
  101 + properties:
  102 + aggregateId:
  103 + type: string
  104 + pattern: "^[A-Z][a-zA-Z]+Aggregate$"
  105 + description: { $ref: "#/definitions/NonEmptyString" }
  106 + aggregateRoot: { $ref: "#/definitions/M1_AggregateRoot" }
  107 + entities:
  108 + type: array
  109 + items: { $ref: "#/definitions/M1_Entity" }
  110 + valueObjects:
  111 + type: array
  112 + maxItems: 0
  113 +
  114 + # M2 命令行为单元定义
  115 + M2_InputParam:
  116 + type: object
  117 + required: [name, type]
  118 + properties:
  119 + name: { $ref: "#/definitions/Identifier" }
  120 + type: { type: string }
  121 + optional: { type: boolean }
  122 + # bizSteps 单步:一段“闭指令集”程序的一条。CommandEngine 逐条通用解释执行。
  123 + # ★ 闭集合的意义:AI 转换命令行为时 type 只能取下列枚举之一——越界值在模型校验层即被拒,
  124 + # 引擎面被封顶(仅需实现这些 opcode),AI 产物可静态校验。
  125 + M2_BizStep:
  126 + type: object
  127 + required: [instruction]
  128 + properties:
  129 + stepNo: { type: integer }
  130 + desc: { type: string }
  131 + instruction:
  132 + type: object
  133 + required: [type]
  134 + properties:
  135 + type:
  136 + type: string
  137 + # 数据/落库 opcode + 控制流/跨聚合写回 opcode(IF 携带 then/else 嵌套指令,类比 M4 condition,
  138 + # 使命令级 bizSteps 也具备判断分支能力;REMOTE_SET_FIELD/REMOTE_INCREMENT_FIELD 通用回写被引用聚合字段)
  139 + enum: [LOAD_AGGREGATE, SET_FIELD, AUTO_FILL_FIELD, REMOTE_QUERY_SET_FIELD,
  140 + CONDITION_SET_FIELD, CALL_OTHER_COMMAND, COMMIT_TRANSACTION,
  141 + IF, REMOTE_SET_FIELD, REMOTE_INCREMENT_FIELD]
  142 + params: { type: object }
  143 + M2_Command:
  144 + type: object
  145 + required: [cmdId, desc, inputParams, validations, emitEvents]
  146 + properties:
  147 + cmdId: { $ref: "#/definitions/UpperIdentifier" }
  148 + desc: { $ref: "#/definitions/NonEmptyString" }
  149 + inputParams:
  150 + type: array
  151 + items: { $ref: "#/definitions/M2_InputParam" }
  152 + validations:
  153 + type: array
  154 + items: { type: string }
  155 + # 命令行为:闭指令集程序(首选);或旧的自然语言描述字符串(向后兼容,引擎按 commandType 合成默认程序)
  156 + bizSteps:
  157 + oneOf:
  158 + - type: string
  159 + - type: array
  160 + items: { $ref: "#/definitions/M2_BizStep" }
  161 + emitEvents:
  162 + type: array
  163 + items: { $ref: "#/definitions/UpperIdentifier" }
  164 + sideEffect: { type: string }
  165 + M2_AggregateBehavior:
  166 + type: object
  167 + required: [aggregateId, aggregateRoot, commands]
  168 + properties:
  169 + aggregateId:
  170 + type: string
  171 + pattern: "^[A-Z][a-zA-Z]+Aggregate$"
  172 + aggregateRoot: { $ref: "#/definitions/NonEmptyString" }
  173 + commands:
  174 + type: array
  175 + items: { $ref: "#/definitions/M2_Command" }
  176 +
  177 + # ME 事件模型单元定义
  178 + ME_Event:
  179 + type: object
  180 + required: [topic, payload]
  181 + properties:
  182 + topic: { $ref: "#/definitions/TopicName" }
  183 + payload:
  184 + type: array
  185 + items: { $ref: "#/definitions/Identifier" }
  186 + consumers:
  187 + type: array
  188 + items: { type: string }
  189 + ME_OutboxConfig:
  190 + type: object
  191 + required: [enabled, table]
  192 + properties:
  193 + enabled: { type: boolean }
  194 + table: { $ref: "#/definitions/NonEmptyString" }
  195 + pollInterval: { type: string }
  196 + batchSize: { type: integer, minimum: 1 }
  197 + cleanupDays: { type: integer, minimum: 1 }
  198 + ME_CrossConsistencyRule:
  199 + type: object
  200 + required: [triggerEvent, targetAggregate, targetCommand, consistencyType]
  201 + properties:
  202 + triggerEvent: { $ref: "#/definitions/UpperIdentifier" }
  203 + targetAggregate:
  204 + type: string
  205 + pattern: "^[A-Z][a-zA-Z]+Aggregate$"
  206 + targetCommand: { $ref: "#/definitions/UpperIdentifier" }
  207 + consistencyType: { $ref: "#/definitions/ConsistencyTypeEnum" }
  208 +
  209 + # M3 存储部署映射单元定义
  210 + M3_TableMapping:
  211 + type: object
  212 + required: [domainEntity, table, pk, fieldMap]
  213 + properties:
  214 + domainEntity: { $ref: "#/definitions/NonEmptyString" }
  215 + table: { $ref: "#/definitions/NonEmptyString" }
  216 + pk: { $ref: "#/definitions/Identifier" }
  217 + shardRule: { type: string }
  218 + fieldMap:
  219 + type: object
  220 + additionalProperties: { type: string }
  221 + M3_AggregateDeploy:
  222 + type: object
  223 + required: [aggregateId, serviceName, commandApi, tableMappings]
  224 + properties:
  225 + aggregateId:
  226 + type: string
  227 + pattern: "^[A-Z][a-zA-Z]+Aggregate$"
  228 + serviceName:
  229 + type: string
  230 + pattern: "^[a-z-]+-service$"
  231 + commandApi:
  232 + type: object
  233 + additionalProperties:
  234 + type: string
  235 + pattern: "^/api/v1/.+"
  236 + tableMappings:
  237 + type: array
  238 + items: { $ref: "#/definitions/M3_TableMapping" }
  239 +
  240 + # M4 业务场景编排单元定义(Saga 编排步骤)
  241 + # stepType 全集按职责分组;引擎(CommandEngine)对每种类型做"模型驱动的通用解释",
  242 + # 不含任何具体聚合/字段字面量。所有非 stepType 的属性均为可选,按步骤类型取用。
  243 + M4_SceneStep:
  244 + type: object
  245 + required: [stepType]
  246 + properties:
  247 + stepType:
  248 + type: string
  249 + enum:
  250 + # —— 起止 ——
  251 + - start # 显式起点(可标注触发方式)
  252 + - return # 正常结束并返回结果
  253 + - end # 正常结束(return 的别名)
  254 + - errorEnd # 异常结束,抛出 message
  255 + # —— 数据/校验/派生 ——
  256 + - readOnlyCheck # 校验 bindAggregate 引用存在
  257 + - validate # 独立执行 M8 必填 + M1 类型/约束校验
  258 + - derive # 独立执行 M2 derivations 服务端重算
  259 + - assign # 按表达式给字段赋值
  260 + - transform # 数据转换(assign 的语义别名)
  261 + - script # 求值一段表达式,可写回字段
  262 + # —— 命令/事件(EDA) ——
  263 + - bindCommand # 绑定并执行某聚合的 M2 命令
  264 + - callScene # 调用子场景(嵌套编排/复用)
  265 + - callService # 调用外部系统/API
  266 + - emitEvent # 显式发布领域事件
  267 + - waitEvent # 等待某外部事件到达
  268 + # —— 控制流 ——
  269 + - condition # 排他分支(if/else)
  270 + - switch # 多路分支
  271 + - parallel # 并行网关(多分支汇合)
  272 + - while # 条件循环
  273 + - loopItems # 遍历某子实体集合(foreach)
  274 + # —— Saga/可靠性 ——
  275 + - retry # 带重试的子步骤
  276 + - compensate # 逆序执行已登记的补偿
  277 + - timer # 定时/延时/超时
  278 + - userTask # 人工任务/审批网关
  279 + # 命令绑定(bindCommand)
  280 + bindAggregate: { type: string }
  281 + bindCommand: { $ref: "#/definitions/UpperIdentifier" }
  282 + # Saga 补偿:bindCommand 成功后登记的逆向命令,失败时逆序触发
  283 + compensateAggregate: { type: string }
  284 + compensateCommand: { $ref: "#/definitions/UpperIdentifier" }
  285 + # 控制流:条件/循环谓词、switch 分派表达式与分支
  286 + when: { type: string } # condition/while 的布尔表达式
  287 + switchOn: { type: string } # switch 的分派表达式(避开 YAML 保留字 on/off)
  288 + then: # condition 为真的子步骤
  289 + type: array
  290 + items: { $ref: "#/definitions/M4_SceneStep" }
  291 + else: # condition 为假的子步骤
  292 + type: array
  293 + items: { $ref: "#/definitions/M4_SceneStep" }
  294 + body: # while/loopItems/retry 的子步骤
  295 + type: array
  296 + items: { $ref: "#/definitions/M4_SceneStep" }
  297 + cases: # switch 分支列表
  298 + type: array
  299 + items:
  300 + type: object
  301 + properties:
  302 + equals: { type: string }
  303 + steps:
  304 + type: array
  305 + items: { $ref: "#/definitions/M4_SceneStep" }
  306 + default: # switch 兜底子步骤
  307 + type: array
  308 + items: { $ref: "#/definitions/M4_SceneStep" }
  309 + branches: # parallel 并行分支
  310 + type: array
  311 + items:
  312 + type: object
  313 + properties:
  314 + steps:
  315 + type: array
  316 + items: { $ref: "#/definitions/M4_SceneStep" }
  317 + itemsFrom: { type: string } # loopItems 遍历的子实体名
  318 + maxIterations: { type: integer }# while/loopItems 迭代上限(防失控)
  319 + maxAttempts: { type: integer } # retry 最大尝试次数
  320 + # 数据步骤:assign/transform/script
  321 + assignments: # assign/transform 的赋值列表
  322 + type: array
  323 + items:
  324 + type: object
  325 + required: [targetField, expression]
  326 + properties:
  327 + targetField: { type: string }
  328 + expression: { type: string }
  329 + targetField: { type: string } # assign 单字段简写
  330 + expression: { type: string } # assign 简写/script 表达式
  331 + assignTo: { type: string } # script 结果写回字段
  332 + # 事件步骤:emitEvent/waitEvent/timer
  333 + emitEvent: { $ref: "#/definitions/UpperIdentifier" }
  334 + waitEvent: { $ref: "#/definitions/UpperIdentifier" }
  335 + timeoutMs: { type: integer }
  336 + delayMs: { type: integer }
  337 + # 人工任务:userTask
  338 + assignee: { type: string }
  339 + role: { type: string }
  340 + approveParam: { type: string } # 入参中表示"已审批"的字段名(true 放行)
  341 + # 外部调用:callService / 子场景:callScene
  342 + callScene: { $ref: "#/definitions/SceneId" }
  343 + serviceRef: { type: string }
  344 + endpoint: { type: string }
  345 + method: { type: string }
  346 + # 通用:起点触发方式 / 异常结束消息
  347 + triggerType: { type: string }
  348 + message: { type: string }
  349 + M4_Scene:
  350 + type: object
  351 + required: [sceneId, sceneName, aggregateScope, permissionBind, flowSteps]
  352 + properties:
  353 + sceneId: { $ref: "#/definitions/SceneId" }
  354 + sceneName: { $ref: "#/definitions/NonEmptyString" }
  355 + aggregateScope:
  356 + type: array
  357 + items:
  358 + type: string
  359 + pattern: "^[A-Z][a-zA-Z]+Aggregate$"
  360 + permissionBind:
  361 + type: array
  362 + items: { $ref: "#/definitions/Identifier" }
  363 + flowSteps:
  364 + type: array
  365 + items: { $ref: "#/definitions/M4_SceneStep" }
  366 +
  367 + # M5 安全权限单元定义
  368 + M5_SecurityRoot:
  369 + type: object
  370 + required: [globalConfig, principals, functionPermissions, maskRules, encryptRules]
  371 + # M6 监控告警单元定义
  372 + M6_Alert:
  373 + type: object
  374 + required: [desc, level, trigger]
  375 + properties:
  376 + desc: { type: string }
  377 + level:
  378 + type: string
  379 + enum: [严重告警, 警告告警]
  380 + trigger: { type: string }
  381 + M6_MonitorRoot:
  382 + type: object
  383 + required: [globalConfig, serviceMetricBind, alertRules]
  384 + # M7 流量SLA单元定义
  385 + M7_QualityRoot:
  386 + type: object
  387 + required: [aggregateSla, commandSla, sceneSla, flowControl]
  388 + # MetaRule 动态业务规则单元定义
  389 + MetaRule_Param:
  390 + type: object
  391 + required: [paramId, name, dataType, defaultValue]
  392 + MetaRule_Item:
  393 + type: object
  394 + required: [ruleId, ruleVersion, ruleName, matchCondition, effect, message]
  395 + properties:
  396 + ruleId: { $ref: "#/definitions/RuleId" }
  397 + effect:
  398 + type: string
  399 + enum: [REJECT, ALERT, SKIP_CHECK, CALC_FILL, COMPENSATE]
  400 + MetaRule_Group:
  401 + type: object
  402 + required: [groupId, groupType, bindScene, rules]
  403 +
  404 + # ===================== M8 前端拖拽模板完整定义(新增核心) =====================
  405 + M8_FrontFieldBind:
  406 + type: object
  407 + required: [bindAggregateId, bindSourceType, domainFieldName]
  408 + properties:
  409 + bindAggregateId:
  410 + type: string
  411 + pattern: "^[A-Z][a-zA-Z]+Aggregate$"
  412 + bindSourceType: { $ref: "#/definitions/BindSourceTypeEnum" }
  413 + domainEntityName: { type: string }
  414 + domainFieldName: { $ref: "#/definitions/Identifier" }
  415 + formLabel: { type: string }
  416 + placeholder: { type: string }
  417 + required: { type: boolean }
  418 + maskField: { type: boolean }
  419 + M8_FrontDragComponent:
  420 + type: object
  421 + required: [compId, compType, fieldBind]
  422 + properties:
  423 + compId: { $ref: "#/definitions/Identifier" }
  424 + compType: { $ref: "#/definitions/FrontComponentEnum" }
  425 + width: { type: string }
  426 + span: { type: integer }
  427 + fieldBind: { $ref: "#/definitions/M8_FrontFieldBind" }
  428 + childComponents:
  429 + type: array
  430 + items: { $ref: "#/definitions/M8_FrontDragComponent" }
  431 + M8_FrontPageTemplate:
  432 + type: object
  433 + required: [templateId, templateName, templateType, renderMode, bindSceneId, rootComponents]
  434 + properties:
  435 + templateId: { $ref: "#/definitions/Identifier" }
  436 + templateName: { $ref: "#/definitions/NonEmptyString" }
  437 + templateType: { $ref: "#/definitions/FrontTemplateTypeEnum" }
  438 + renderMode: { $ref: "#/definitions/ReactRenderModeEnum" }
  439 + bindSceneId: { $ref: "#/definitions/SceneId" }
  440 + masterAggregate: { type: string }
  441 + detailAggregates:
  442 + type: array
  443 + items: { type: string }
  444 + rootComponents:
  445 + type: array
  446 + items: { $ref: "#/definitions/M8_FrontDragComponent" }
  447 + M8_FrontSchema:
  448 + type: object
  449 + required: [reactGlobalConfig, pageTemplates]
  450 + properties:
  451 + reactGlobalConfig:
  452 + type: object
  453 + required: [uiLib, dragEngineStorage]
  454 + properties:
  455 + uiLib:
  456 + type: string
  457 + const: antd
  458 + dragEngineStorage:
  459 + type: string
  460 + const: mysql8
  461 + pageTemplates:
  462 + type: array
  463 + items: { $ref: "#/definitions/M8_FrontPageTemplate" }
  464 +
  465 + # 各分层根Schema定义(新增M8根校验)
  466 + M1_Schema:
  467 + type: object
  468 + required: [aggregates, globalConstraints]
  469 + M2_Schema:
  470 + type: object
  471 + required: [behaviors, globalBehaviorConstraints]
  472 + ME_Schema:
  473 + type: object
  474 + required: [eventModel]
  475 + M3_Schema:
  476 + type: object
  477 + required: [deploymentMappings]
  478 + M4_Schema:
  479 + type: object
  480 + required: [sceneModel]
  481 + M5_Schema:
  482 + type: object
  483 + required: [securityModel]
  484 + M6_Schema:
  485 + type: object
  486 + required: [monitorModel]
  487 + M7_Schema:
  488 + type: object
  489 + required: [qualityModel]
  490 + MetaRule_Schema:
  491 + type: object
  492 + required: [metaRuleModel]
  493 + M8_Schema:
  494 + type: object
  495 + required: [frontModel]
  496 +
  497 + rootAllOntologySchema:
  498 + type: object
  499 + oneOf:
  500 + - { $ref: "#/definitions/M1_Schema" }
  501 + - { $ref: "#/definitions/M2_Schema" }
  502 + - { $ref: "#/definitions/ME_Schema" }
  503 + - { $ref: "#/definitions/M3_Schema" }
  504 + - { $ref: "#/definitions/M4_Schema" }
  505 + - { $ref: "#/definitions/M5_Schema" }
  506 + - { $ref: "#/definitions/M6_Schema" }
  507 + - { $ref: "#/definitions/M7_Schema" }
  508 + - { $ref: "#/definitions/MetaRule_Schema" }
  509 + - { $ref: "#/definitions/M8_Schema" }
... ...
models/M1-domain.yaml 0 → 100644
  1 +++ a/models/M1-domain.yaml
  1 +# M1 DDD聚合领域静态模型 (v2.2:EmployeeAggregate 增审核统计字段 auditApproveCount/auditRejectCount/remark/lastAuditTime;订单审核字段见 Order)
  2 +# 领域聚合唯一数据源:前端所有拖拽组件强制绑定本层字段,禁止自定义字段。
  3 +aggregates:
  4 + # 聚合1:客户聚合 CustomerAggregate
  5 + - aggregateId: CustomerAggregate
  6 + description: 客户主数据聚合,独立事务边界
  7 + aggregateRoot:
  8 + name: Customer
  9 + identifier: customerId
  10 + attributes:
  11 + - name: customerName
  12 + type: string
  13 + - name: contactPhone
  14 + type: string
  15 + constraints:
  16 + pattern: "^\\d{11}$"
  17 + patternMessage: 手机号需为 11 位数字
  18 + - name: registerTime
  19 + type: datetime
  20 + - name: customerLevel
  21 + type: string
  22 + entities: []
  23 + valueObjects: []
  24 +
  25 + # 聚合2:产品聚合 ProductAggregate
  26 + - aggregateId: ProductAggregate
  27 + description: 商品主数据聚合,独立事务边界
  28 + aggregateRoot:
  29 + name: Product
  30 + identifier: productId
  31 + attributes:
  32 + - name: productName
  33 + type: string
  34 + - name: skuCode
  35 + type: string
  36 + constraints:
  37 + unique: true
  38 + - name: category
  39 + type: string
  40 + - name: saleCurrency
  41 + type: string
  42 + - name: salePrice
  43 + type: decimal
  44 + constraints:
  45 + exclusiveMin: 0
  46 + - name: stockNum
  47 + type: int
  48 + constraints:
  49 + min: 0
  50 + - name: isOnSale
  51 + type: boolean
  52 + entities: []
  53 + valueObjects: []
  54 +
  55 + # 聚合3:订单聚合 OrderAggregate
  56 + - aggregateId: OrderAggregate
  57 + description: 订单业务聚合,独立事务边界
  58 + aggregateRoot:
  59 + name: Order
  60 + identifier: orderId
  61 + attributes:
  62 + - name: createTime
  63 + type: datetime
  64 + - name: totalCurrency
  65 + type: string
  66 + - name: totalAmount
  67 + type: decimal
  68 + - name: customerId
  69 + type: string
  70 + refAggregate: CustomerAggregate
  71 + refRoot: Customer
  72 + refIdentifier: customerId
  73 + description: 客户ID,引用客户聚合根主键
  74 + # 审核相关字段:由 AuditOrder 命令的 bizSteps 指令程序通用填充(非订单专用引擎代码)
  75 + - name: orderStatus
  76 + type: string
  77 + description: 订单状态(如 CREATED/PASS/REJECT),AuditOrder 按审核结果置位
  78 + - name: auditorId
  79 + type: string
  80 + refAggregate: EmployeeAggregate
  81 + refRoot: Employee
  82 + refIdentifier: employeeId
  83 + description: 审核人工号,引用员工聚合根主键
  84 + - name: auditorName
  85 + type: string
  86 + - name: auditTime
  87 + type: datetime
  88 + - name: auditRemark
  89 + type: string
  90 + - name: auditResult
  91 + type: string
  92 + description: 审核结论(PASS/REJECT)——AuditOrder 命令入参,仅用于 CONDITION_SET_FIELD 派生 orderStatus;未在 M3 建列,故为不落库的瞬态字段
  93 + relations:
  94 + - ref: OrderItem
  95 + relationType: composition
  96 + - ref: PaymentTerm
  97 + relationType: composition
  98 + - ref: DeliveryAddressEntity
  99 + relationType: composition
  100 + entities:
  101 + - name: OrderItem
  102 + localIdentifier: itemId
  103 + attributes:
  104 + - name: productId
  105 + type: string
  106 + refAggregate: ProductAggregate
  107 + refRoot: Product
  108 + refIdentifier: productId
  109 + description: 商品ID,引用产品聚合根主键
  110 + - name: quantity
  111 + type: int
  112 + constraints:
  113 + min: 1
  114 + - name: itemCurrency
  115 + type: string
  116 + - name: itemPrice
  117 + type: decimal
  118 + - name: PaymentTerm
  119 + localIdentifier: termId
  120 + attributes:
  121 + - name: paymentType
  122 + type: string
  123 + - name: dueDays
  124 + type: int
  125 + constraints:
  126 + min: 0
  127 + - name: depositRatio
  128 + type: decimal
  129 + constraints:
  130 + min: 0
  131 + max: 1
  132 + - name: settleCurrency
  133 + type: string
  134 + - name: settleAmount
  135 + type: decimal
  136 + - name: DeliveryAddressEntity
  137 + localIdentifier: addrId
  138 + attributes:
  139 + - name: province
  140 + type: string
  141 + - name: city
  142 + type: string
  143 + - name: district
  144 + type: string
  145 + - name: streetDetail
  146 + type: string
  147 + - name: receiverName
  148 + type: string
  149 + - name: receiverPhone
  150 + type: string
  151 + constraints:
  152 + pattern: "^\\d{11}$"
  153 + patternMessage: 收货手机号需为 11 位数字
  154 + valueObjects: []
  155 +
  156 + # 聚合4:员工聚合 EmployeeAggregate(供 AuditOrder 的 REMOTE_QUERY_SET_FIELD 查审核人姓名;
  157 + # 并承载审核统计——AuditOrder 的 bizSteps 条件程序按审核结果通用地回写这些字段)
  158 + - aggregateId: EmployeeAggregate
  159 + description: 员工主数据聚合,独立事务边界
  160 + aggregateRoot:
  161 + name: Employee
  162 + identifier: employeeId
  163 + attributes:
  164 + - name: employeeName
  165 + type: string
  166 + - name: department
  167 + type: string
  168 + - name: auditApproveCount
  169 + type: int
  170 + constraints:
  171 + min: 0
  172 + description: 累计审核通过数——AuditOrder 命中 PASS 时 REMOTE_INCREMENT_FIELD +1
  173 + - name: auditRejectCount
  174 + type: int
  175 + constraints:
  176 + min: 0
  177 + description: 累计审核驳回数——AuditOrder 命中 REJECT 时 REMOTE_INCREMENT_FIELD +1
  178 + - name: remark
  179 + type: string
  180 + description: 备注——AuditOrder 在订单金额>100 时以 REMOTE_SET_FIELD 覆盖写入审核备注
  181 + - name: lastAuditTime
  182 + type: datetime
  183 + description: 上次审核时间——AuditOrder 每次审核以 REMOTE_SET_FIELD 覆盖为本次审核时间
  184 + entities: []
  185 + valueObjects: []
  186 +
  187 +globalConstraints:
  188 + - crossAggRefRule: 跨聚合仅允许引用对方【聚合根唯一标识】,禁止引用对方内部实体、值对象
  189 + - transactionBoundary: 一次事务只能修改同一个聚合内对象,跨聚合拆分为多个事务
  190 + - cascadeDelete: 聚合根删除时,级联删除自身内部所有组合子实体
  191 + - foreignKeyStrategy: "业务层做引用校验,默认不创建数据库物理外键;如需物理外键增加 dbForeignKey: true"
... ...
models/M2-command.yaml 0 → 100644
  1 +++ a/models/M2-command.yaml
  1 +# M2 聚合行为命令模型 (v2.2:新增 Employee 的 CreateEmployee/ModifyEmployee;AuditOrder 的 bizSteps 用 IF 判断分支 +
  2 +# REMOTE_SET_FIELD/REMOTE_INCREMENT_FIELD 条件回写审核人统计;修正 totalAmount 派生为 quantity*itemPrice)
  3 +# 聚合操作命令入参定义;React页面提交时自动映射表单组件值至命令参数,无硬编码接口参数。
  4 +behaviors:
  5 + # 1.客户聚合行为
  6 + - aggregateId: CustomerAggregate
  7 + aggregateRoot: Customer
  8 + commands:
  9 + - cmdId: CreateCustomer
  10 + desc: 新增客户主数据
  11 + commandType: create
  12 + inputParams:
  13 + - name: customerName
  14 + type: string
  15 + - name: contactPhone
  16 + type: string
  17 + - name: customerLevel
  18 + type: string
  19 + validations:
  20 + - contactPhone非空、手机号格式校验
  21 + - customerLevel取值范围[普通,VIP,高级VIP]
  22 + emitEvents:
  23 + - CustomerCreated
  24 + sideEffect: 仅写入本聚合,不可修改其他聚合
  25 +
  26 + - cmdId: ModifyCustomerInfo
  27 + desc: 修改客户名称、联系方式、等级
  28 + commandType: update
  29 + inputParams:
  30 + - name: customerId
  31 + type: string
  32 + - name: customerName
  33 + type: string
  34 + optional: true
  35 + - name: contactPhone
  36 + type: string
  37 + optional: true
  38 + - name: customerLevel
  39 + type: string
  40 + optional: true
  41 + validations:
  42 + - customerId必须存在
  43 + emitEvents:
  44 + - CustomerInfoModified
  45 +
  46 + # 2.产品聚合行为
  47 + - aggregateId: ProductAggregate
  48 + aggregateRoot: Product
  49 + commands:
  50 + - cmdId: CreateProduct
  51 + desc: 新增商品档案
  52 + commandType: create
  53 + inputParams:
  54 + - name: productName
  55 + type: string
  56 + - name: skuCode
  57 + type: string
  58 + - name: category
  59 + type: string
  60 + - name: saleCurrency
  61 + type: string
  62 + - name: salePrice
  63 + type: decimal
  64 + - name: stockNum
  65 + type: int
  66 + - name: isOnSale
  67 + type: boolean
  68 + validations:
  69 + - skuCode全局唯一
  70 + - salePrice > 0
  71 + - stockNum >= 0
  72 + emitEvents:
  73 + - ProductCreated
  74 +
  75 + - cmdId: ModifyProductPrice
  76 + desc: 修改售价与币种
  77 + commandType: update
  78 + inputParams:
  79 + - name: productId
  80 + type: string
  81 + - name: saleCurrency
  82 + type: string
  83 + optional: true
  84 + - name: salePrice
  85 + type: decimal
  86 + optional: true
  87 + validations:
  88 + - productId存在
  89 + - 修改后售价>0
  90 + emitEvents:
  91 + - ProductPriceChanged
  92 +
  93 + - cmdId: StockDeduct
  94 + desc: 下单扣减库存(订单聚合远程调用)
  95 + commandType: update
  96 + inputParams:
  97 + - name: productId
  98 + type: string
  99 + - name: deductQty
  100 + type: int
  101 + validations:
  102 + - 可用库存 >= deductQty
  103 + # 结构化副作用:引擎据此通用执行"按 keyParam 定位、对 targetField 做 op 运算、amountParam 为量、guardField 保证不越界"
  104 + effect:
  105 + op: decrement
  106 + targetField: stockNum
  107 + keyParam: productId
  108 + amountParam: deductQty
  109 + guardField: stockNum
  110 + emitEvents:
  111 + - ProductStockDeducted
  112 +
  113 + - cmdId: StockReturn
  114 + desc: 取消订单归还库存
  115 + commandType: update
  116 + inputParams:
  117 + - name: productId
  118 + type: string
  119 + - name: returnQty
  120 + type: int
  121 + validations: []
  122 + effect:
  123 + op: increment
  124 + targetField: stockNum
  125 + keyParam: productId
  126 + amountParam: returnQty
  127 + emitEvents:
  128 + - ProductStockReturned
  129 +
  130 + # 3.订单聚合行为
  131 + - aggregateId: OrderAggregate
  132 + aggregateRoot: Order
  133 + commands:
  134 + - cmdId: CreateOrder
  135 + desc: 创建订单主单+明细+付款条件+送货地址
  136 + commandType: create
  137 + # 结构化派生:落库/风控前,引擎按明细 sum(quantity*itemPrice) 服务端重算 totalAmount,杜绝客户端篡改
  138 + derivations:
  139 + - targetField: totalAmount
  140 + sourceEntity: OrderItem
  141 + aggregate: sum
  142 + expression: "quantity * itemPrice"
  143 + inputParams:
  144 + - name: customerId
  145 + type: string
  146 + - name: totalCurrency
  147 + type: string
  148 + - name: totalAmount
  149 + type: decimal
  150 + - name: items
  151 + type: array
  152 + - name: paymentTerm
  153 + type: object
  154 + - name: deliveryAddress
  155 + type: object
  156 + validations:
  157 + - customerId 存在于CustomerAggregate
  158 + - 所有productId存在于ProductAggregate
  159 + - 订单明细金额累加 ≈ 订单总金额
  160 + - dueDays >= 0, depositRatio ∈ [0,1]
  161 + - 收货手机号格式合法
  162 + # bizSteps 现为“闭指令集”程序(CommandEngine 逐条通用解释,替代硬编码 create 分派)。
  163 + # create 语义:COMMIT 落库聚合根 + 组合子实体(totalAmount 已由 derivations 服务端重算);
  164 + # 收尾发 emitEvents,ME 规则 OrderCreated→StockDeduct 逐明细扣库存,任一失败超卖回滚。
  165 + bizSteps:
  166 + - stepNo: 1
  167 + desc: 落库 Order 主记录及 OrderItem/PaymentTerm/DeliveryAddress 组合子实体
  168 + instruction:
  169 + type: COMMIT_TRANSACTION
  170 + params: {}
  171 + emitEvents:
  172 + - OrderCreated
  173 + - OrderItemAdded
  174 + - OrderPaymentTermSet
  175 + - OrderDeliveryAddressSet
  176 +
  177 + - cmdId: ModifyOrderDeliveryAddress
  178 + desc: 修改订单送货地址子实体
  179 + commandType: update
  180 + inputParams:
  181 + - name: orderId
  182 + type: string
  183 + - name: province
  184 + type: string
  185 + optional: true
  186 + - name: city
  187 + type: string
  188 + optional: true
  189 + - name: district
  190 + type: string
  191 + optional: true
  192 + - name: streetDetail
  193 + type: string
  194 + optional: true
  195 + - name: receiverName
  196 + type: string
  197 + optional: true
  198 + - name: receiverPhone
  199 + type: string
  200 + optional: true
  201 + validations:
  202 + - orderId存在
  203 + bizSteps:
  204 + - stepNo: 1
  205 + desc: 按 orderId 加载既有订单聚合根(不存在即失败)
  206 + instruction:
  207 + type: LOAD_AGGREGATE
  208 + params:
  209 + idSource: input.orderId
  210 + - stepNo: 2
  211 + desc: 更新命令携带的根字段与组合子实体(送货地址)后提交事务
  212 + instruction:
  213 + type: COMMIT_TRANSACTION
  214 + params: {}
  215 + emitEvents:
  216 + - OrderDeliveryAddressModified
  217 +
  218 + - cmdId: CancelOrder
  219 + desc: 取消订单,归还库存
  220 + commandType: cancel
  221 + inputParams:
  222 + - name: orderId
  223 + type: string
  224 + validations:
  225 + - 订单状态允许取消
  226 + # cancel 语义:LOAD 校验存在 + COMMIT(无字段变更);收尾发 OrderCancelled,
  227 + # ME 规则 OrderCancelled→StockReturn 从库加载明细逐项回补库存。
  228 + bizSteps:
  229 + - stepNo: 1
  230 + desc: 按 orderId 加载既有订单聚合根(不存在即失败)
  231 + instruction:
  232 + type: LOAD_AGGREGATE
  233 + params:
  234 + idSource: input.orderId
  235 + - stepNo: 2
  236 + desc: 提交事务(标记作废;库存回补由 ME 一致性规则完成)
  237 + instruction:
  238 + type: COMMIT_TRANSACTION
  239 + params: {}
  240 + emitEvents:
  241 + - OrderCancelled
  242 +
  243 + - cmdId: AuditOrder
  244 + desc: 审核订单:填审核人/时间/备注,按审核结果置订单状态(bizSteps 闭指令集程序)
  245 + commandType: update
  246 + inputParams:
  247 + - name: orderId
  248 + type: string
  249 + - name: auditorId
  250 + type: string
  251 + - name: auditRemark
  252 + type: string
  253 + optional: true
  254 + - name: auditResult
  255 + type: string
  256 + validations:
  257 + - orderId 存在
  258 + - auditResult ∈ [PASS, REJECT]
  259 + # ★ 展示“行为即数据”:这段程序由 CommandEngine 逐条通用解释,改步骤=改 YAML,无需改引擎。
  260 + # Step1:根据orderId加载完整订单聚合根
  261 + # Step2:系统当前时间自动赋值给auditTime
  262 + # Step3:前端传的auditorId赋值给订单的auditorId字段
  263 + # Step4:用auditorId去员工服务查审核人姓名,存到auditorName
  264 + # Step5:把审核备注赋值给auditRemark
  265 + # Step6:审核通过就把订单状态改成PASS,驳回就改成REJECT
  266 + # Step7:统一提交数据库事务
  267 + bizSteps:
  268 + - stepNo: 1
  269 + desc: 根据 orderId 加载完整订单聚合根
  270 + instruction:
  271 + type: LOAD_AGGREGATE
  272 + params:
  273 + idSource: input.orderId
  274 + - stepNo: 2
  275 + desc: 系统当前时间自动赋值给 auditTime
  276 + instruction:
  277 + type: AUTO_FILL_FIELD
  278 + params:
  279 + targetField: auditTime
  280 + func: now_datetime
  281 + - stepNo: 3
  282 + desc: 前端传的 auditorId 赋值给订单的 auditorId 字段
  283 + instruction:
  284 + type: SET_FIELD
  285 + params:
  286 + targetField: auditorId
  287 + source: input.auditorId
  288 + - stepNo: 4
  289 + desc: 用 auditorId 去员工聚合查审核人姓名,回填 auditorName
  290 + instruction:
  291 + type: REMOTE_QUERY_SET_FIELD
  292 + params:
  293 + remoteAggregate: EmployeeAggregate
  294 + queryKey: input.auditorId
  295 + sourceField: employeeName
  296 + targetField: auditorName
  297 + - stepNo: 5
  298 + desc: 把审核备注赋值给 auditRemark
  299 + instruction:
  300 + type: SET_FIELD
  301 + params:
  302 + targetField: auditRemark
  303 + source: input.auditRemark
  304 + - stepNo: 6
  305 + desc: 审核通过置 PASS,驳回置 REJECT
  306 + instruction:
  307 + type: CONDITION_SET_FIELD
  308 + params:
  309 + targetField: orderStatus
  310 + conditions:
  311 + - when: 'input.auditResult == "PASS"'
  312 + value: "PASS"
  313 + - when: 'input.auditResult == "REJECT"'
  314 + value: "REJECT"
  315 + # —— 命令级 bizSteps 现支持 IF 判断分支 + 跨聚合写回(类比 M4 flowSteps 的 condition)——
  316 + # 审核人(员工)统计随审核结果通用回写;when 走 Aviator,root.=订单现值 / input.=命令入参。
  317 + - stepNo: 7
  318 + desc: 订单金额 > 100 时,把本次审核备注覆盖写入审核人的备注字段(金额不足则不写)
  319 + instruction:
  320 + type: IF
  321 + params:
  322 + when: 'root.totalAmount > 100'
  323 + then:
  324 + - instruction:
  325 + type: REMOTE_SET_FIELD
  326 + params:
  327 + remoteAggregate: EmployeeAggregate
  328 + keySource: input.auditorId
  329 + targetField: remark
  330 + source: input.auditRemark
  331 + else: []
  332 + - stepNo: 8
  333 + desc: 审核通过(PASS)时,审核人累计通过数 +1
  334 + instruction:
  335 + type: IF
  336 + params:
  337 + when: 'input.auditResult == "PASS"'
  338 + then:
  339 + - instruction:
  340 + type: REMOTE_INCREMENT_FIELD
  341 + params:
  342 + remoteAggregate: EmployeeAggregate
  343 + keySource: input.auditorId
  344 + targetField: auditApproveCount
  345 + amount: 1
  346 + - stepNo: 9
  347 + desc: 审核驳回(REJECT)时,审核人累计驳回数 +1
  348 + instruction:
  349 + type: IF
  350 + params:
  351 + when: 'input.auditResult == "REJECT"'
  352 + then:
  353 + - instruction:
  354 + type: REMOTE_INCREMENT_FIELD
  355 + params:
  356 + remoteAggregate: EmployeeAggregate
  357 + keySource: input.auditorId
  358 + targetField: auditRejectCount
  359 + amount: 1
  360 + - stepNo: 10
  361 + desc: 记录审核人“上次审核时间”为本次审核时间
  362 + instruction:
  363 + type: REMOTE_SET_FIELD
  364 + params:
  365 + remoteAggregate: EmployeeAggregate
  366 + keySource: input.auditorId
  367 + targetField: lastAuditTime
  368 + source: root.auditTime
  369 + - stepNo: 11
  370 + desc: 统一提交数据库事务
  371 + instruction:
  372 + type: COMMIT_TRANSACTION
  373 + params: {}
  374 + emitEvents:
  375 + - OrderAudited
  376 +
  377 + # 4.员工聚合行为(“新增/修改员工”场景;亦为 AuditOrder 跨聚合回写的目标聚合根)
  378 + - aggregateId: EmployeeAggregate
  379 + aggregateRoot: Employee
  380 + commands:
  381 + - cmdId: CreateEmployee
  382 + desc: 新增员工主数据(审核统计计数初始化为 0)
  383 + commandType: create
  384 + inputParams:
  385 + - name: employeeName
  386 + type: string
  387 + - name: department
  388 + type: string
  389 + validations:
  390 + - employeeName 非空
  391 + # bizSteps:把两个审核计数初始化为 0(后续 REMOTE_INCREMENT_FIELD 才能在非空值上累加),再落库
  392 + bizSteps:
  393 + - stepNo: 1
  394 + desc: 审核通过数初始化 0
  395 + instruction:
  396 + type: SET_FIELD
  397 + params:
  398 + targetField: auditApproveCount
  399 + value: 0
  400 + - stepNo: 2
  401 + desc: 审核驳回数初始化 0
  402 + instruction:
  403 + type: SET_FIELD
  404 + params:
  405 + targetField: auditRejectCount
  406 + value: 0
  407 + - stepNo: 3
  408 + desc: 落库新员工
  409 + instruction:
  410 + type: COMMIT_TRANSACTION
  411 + params: {}
  412 + emitEvents:
  413 + - EmployeeCreated
  414 +
  415 + - cmdId: ModifyEmployee
  416 + desc: 修改员工姓名、部门、备注
  417 + commandType: update
  418 + inputParams:
  419 + - name: employeeId
  420 + type: string
  421 + - name: employeeName
  422 + type: string
  423 + optional: true
  424 + - name: department
  425 + type: string
  426 + optional: true
  427 + - name: remark
  428 + type: string
  429 + optional: true
  430 + validations:
  431 + - employeeId 必须存在
  432 + bizSteps:
  433 + - stepNo: 1
  434 + desc: 按 employeeId 加载既有员工聚合根(不存在即失败)
  435 + instruction:
  436 + type: LOAD_AGGREGATE
  437 + params:
  438 + idSource: input.employeeId
  439 + - stepNo: 2
  440 + desc: 更新命令携带的字段后提交事务
  441 + instruction:
  442 + type: COMMIT_TRANSACTION
  443 + params: {}
  444 + emitEvents:
  445 + - EmployeeModified
  446 +
  447 +globalBehaviorConstraints:
  448 + - 命令只能操作所属聚合内部对象,跨聚合仅允许调用对方聚合根暴露Command,禁止直接操作对方内部实体
  449 + - 单个命令事务边界仅限当前聚合;跨聚合操作拆为分布式事件最终一致性
  450 + - 子实体只能被所属聚合根命令修改,外部无法直接操作
  451 + - 所有跨聚合依赖仅基于对方聚合根ID做校验,不依赖内部字段
... ...
models/M3-deploy.yaml 0 → 100644
  1 +++ a/models/M3-deploy.yaml
  1 +# M3 服务与数据库部署映射 (v2.1:新增 EmployeeAggregate→t_employee_main、AuditOrder API 与订单审核列映射)
  2 +# 领域实体 -> 物理表/字段映射;引擎据此自动建表并生成动态SQL,前端无硬编码URL。
  3 +deploymentMappings:
  4 + globalConfig:
  5 + dbEngine: mysql8
  6 + defaultSchema: trade_db
  7 + idGen: snowflake
  8 +
  9 + aggregateMappingList:
  10 + - aggregateId: CustomerAggregate
  11 + serviceName: customer-service
  12 + commandApi:
  13 + CreateCustomer: /api/v1/customer/create
  14 + ModifyCustomerInfo: /api/v1/customer/modify
  15 + tableMappings:
  16 + - domainEntity: Customer
  17 + table: t_customer_main
  18 + pk: customer_id
  19 + fieldMap:
  20 + customerId: customer_id
  21 + customerName: cust_name
  22 + contactPhone: phone
  23 + registerTime: register_time
  24 + customerLevel: cust_level
  25 +
  26 + - aggregateId: ProductAggregate
  27 + serviceName: product-service
  28 + commandApi:
  29 + CreateProduct: /api/v1/product/create
  30 + ModifyProductPrice: /api/v1/product/price
  31 + StockDeduct: /api/v1/product/stock/deduct
  32 + tableMappings:
  33 + - domainEntity: Product
  34 + table: t_product_main
  35 + pk: product_id
  36 + fieldMap:
  37 + productId: product_id
  38 + productName: prod_name
  39 + skuCode: sku_code
  40 + category: category
  41 + saleCurrency: currency
  42 + salePrice: sale_price
  43 + stockNum: stock_num
  44 + isOnSale: is_on_sale
  45 +
  46 + - aggregateId: OrderAggregate
  47 + serviceName: order-service
  48 + commandApi:
  49 + CreateOrder: /api/v1/order/create
  50 + ModifyOrderDeliveryAddress: /api/v1/order/addr
  51 + CancelOrder: /api/v1/order/cancel
  52 + AuditOrder: /api/v1/order/audit
  53 + tableMappings:
  54 + - domainEntity: Order
  55 + table: t_order_main
  56 + pk: order_id
  57 + fieldMap:
  58 + orderId: order_id
  59 + createTime: create_time
  60 + totalCurrency: total_currency
  61 + totalAmount: total_amt
  62 + customerId: cust_id
  63 + orderStatus: order_status
  64 + auditorId: auditor_id
  65 + auditorName: auditor_name
  66 + auditTime: audit_time
  67 + auditRemark: audit_remark
  68 + - domainEntity: OrderItem
  69 + table: t_order_item
  70 + pk: item_id
  71 + fieldMap:
  72 + itemId: item_id
  73 + productId: product_id
  74 + quantity: buy_qty
  75 + itemCurrency: item_currency
  76 + itemPrice: item_price
  77 + - domainEntity: PaymentTerm
  78 + table: t_order_payment_term
  79 + pk: term_id
  80 + fieldMap:
  81 + termId: term_id
  82 + paymentType: pay_type
  83 + dueDays: due_days
  84 + depositRatio: deposit_ratio
  85 + settleCurrency: settle_currency
  86 + settleAmount: settle_amt
  87 + - domainEntity: DeliveryAddressEntity
  88 + table: t_order_address
  89 + pk: addr_id
  90 + fieldMap:
  91 + addrId: addr_id
  92 + province: province
  93 + city: city
  94 + district: district
  95 + streetDetail: street
  96 + receiverName: receiver_name
  97 + receiverPhone: receiver_phone
  98 +
  99 + - aggregateId: EmployeeAggregate
  100 + serviceName: employee-service
  101 + commandApi:
  102 + CreateEmployee: /api/v1/employee/create
  103 + ModifyEmployee: /api/v1/employee/modify
  104 + tableMappings:
  105 + - domainEntity: Employee
  106 + table: t_employee_main
  107 + pk: employee_id
  108 + fieldMap:
  109 + employeeId: employee_id
  110 + employeeName: emp_name
  111 + department: dept
  112 + auditApproveCount: approve_count
  113 + auditRejectCount: reject_count
  114 + remark: remark
  115 + lastAuditTime: last_audit_time
... ...
models/M4-scene.yaml 0 → 100644
  1 +++ a/models/M4-scene.yaml
  1 +# M4 业务Saga场景编排 (v2.2:新增 SCENE_CREATE_EMPLOYEE/SCENE_MODIFY_EMPLOYEE;含 SCENE_AUDIT_ORDER 审核 + SCENE_DEMO_STEPTYPES 全 stepType 演示)
  2 +# M8页面模板绑定SceneId,一套模板对应完整端到端业务流程。
  3 +sceneModel:
  4 + globalConfig:
  5 + engine: saga
  6 + consistency: eventual
  7 + maxRetry: 2
  8 +
  9 + sceneDefinitions:
  10 + - sceneId: SCENE_CREATE_ORDER
  11 + sceneName: 用户创建订单完整流程
  12 + aggregateScope: [CustomerAggregate, ProductAggregate, OrderAggregate]
  13 + permissionBind: [order_create]
  14 + flowSteps:
  15 + - stepType: readOnlyCheck
  16 + bindAggregate: CustomerAggregate
  17 + - stepType: bindCommand
  18 + bindAggregate: OrderAggregate
  19 + bindCommand: CreateOrder
  20 + - stepType: return
  21 +
  22 + - sceneId: SCENE_MODIFY_ORDER_ADDR
  23 + sceneName: 修改订单收货地址
  24 + aggregateScope: [OrderAggregate]
  25 + permissionBind: [order_modify_addr]
  26 + flowSteps:
  27 + - stepType: bindCommand
  28 + bindAggregate: OrderAggregate
  29 + bindCommand: ModifyOrderDeliveryAddress
  30 + - stepType: return
  31 +
  32 + - sceneId: SCENE_CANCEL_ORDER
  33 + sceneName: 取消订单、归还库存
  34 + aggregateScope: [OrderAggregate, ProductAggregate]
  35 + permissionBind: [order_cancel]
  36 + flowSteps:
  37 + - stepType: bindCommand
  38 + bindAggregate: OrderAggregate
  39 + bindCommand: CancelOrder
  40 + - stepType: return
  41 +
  42 + - sceneId: SCENE_AUDIT_ORDER
  43 + sceneName: 审核订单(bizSteps 闭指令集程序演示)
  44 + aggregateScope: [OrderAggregate, EmployeeAggregate]
  45 + permissionBind: [order_audit]
  46 + flowSteps:
  47 + - stepType: bindCommand
  48 + bindAggregate: OrderAggregate
  49 + bindCommand: AuditOrder
  50 + - stepType: return
  51 +
  52 + - sceneId: SCENE_CREATE_CUSTOMER
  53 + sceneName: 新增客户主数据
  54 + aggregateScope: [CustomerAggregate]
  55 + permissionBind: [customer_create]
  56 + flowSteps:
  57 + - stepType: bindCommand
  58 + bindAggregate: CustomerAggregate
  59 + bindCommand: CreateCustomer
  60 + - stepType: return
  61 +
  62 + - sceneId: SCENE_MODIFY_CUSTOMER
  63 + sceneName: 修改客户信息
  64 + aggregateScope: [CustomerAggregate]
  65 + permissionBind: [customer_modify]
  66 + flowSteps:
  67 + - stepType: bindCommand
  68 + bindAggregate: CustomerAggregate
  69 + bindCommand: ModifyCustomerInfo
  70 + - stepType: return
  71 +
  72 + - sceneId: SCENE_CREATE_PRODUCT
  73 + sceneName: 新增商品档案
  74 + aggregateScope: [ProductAggregate]
  75 + permissionBind: [product_create]
  76 + flowSteps:
  77 + - stepType: bindCommand
  78 + bindAggregate: ProductAggregate
  79 + bindCommand: CreateProduct
  80 + - stepType: return
  81 +
  82 + - sceneId: SCENE_MODIFY_PRODUCT_PRICE
  83 + sceneName: 修改商品售价
  84 + aggregateScope: [ProductAggregate]
  85 + permissionBind: [product_modify_price]
  86 + flowSteps:
  87 + - stepType: bindCommand
  88 + bindAggregate: ProductAggregate
  89 + bindCommand: ModifyProductPrice
  90 + - stepType: return
  91 +
  92 + - sceneId: SCENE_CREATE_EMPLOYEE
  93 + sceneName: 新增员工
  94 + aggregateScope: [EmployeeAggregate]
  95 + permissionBind: [employee_create]
  96 + flowSteps:
  97 + - stepType: bindCommand
  98 + bindAggregate: EmployeeAggregate
  99 + bindCommand: CreateEmployee
  100 + - stepType: return
  101 +
  102 + - sceneId: SCENE_MODIFY_EMPLOYEE
  103 + sceneName: 修改员工信息
  104 + aggregateScope: [EmployeeAggregate]
  105 + permissionBind: [employee_modify]
  106 + flowSteps:
  107 + - stepType: bindCommand
  108 + bindAggregate: EmployeeAggregate
  109 + bindCommand: ModifyEmployee
  110 + - stepType: return
  111 +
  112 + # 演示场景:集中展示新增的全部通用 stepType(控制流/数据/Saga/EDA/人工)。
  113 + # 不落库(无 bindCommand),字段为合成变量(非 M1 领域字段),故不绑定 M8 表单模板;
  114 + # 前端对无模板场景提供"无表单直接运行"入口(空 payload),引擎逐条通用解释 flowSteps 产出可读 trace。可安全删除。
  115 + - sceneId: SCENE_DEMO_STEPTYPES
  116 + sceneName: 演示:全部 stepType 通用解释
  117 + aggregateScope: [OrderAggregate]
  118 + permissionBind: [order_create]
  119 + flowSteps:
  120 + - stepType: start
  121 + triggerType: manual
  122 + - stepType: assign
  123 + assignments:
  124 + - { targetField: qty, expression: "3" }
  125 + - { targetField: unitPrice, expression: "100" }
  126 + - stepType: script
  127 + expression: "qty * unitPrice"
  128 + assignTo: amount
  129 + - stepType: condition
  130 + when: "amount > 200"
  131 + then:
  132 + - stepType: assign
  133 + targetField: tier
  134 + expression: "'BIG'"
  135 + else:
  136 + - stepType: assign
  137 + targetField: tier
  138 + expression: "'SMALL'"
  139 + - stepType: switch
  140 + switchOn: "tier"
  141 + cases:
  142 + - equals: "BIG"
  143 + steps:
  144 + - { stepType: assign, targetField: discount, expression: "0.9" }
  145 + - equals: "SMALL"
  146 + steps:
  147 + - { stepType: assign, targetField: discount, expression: "1" }
  148 + default:
  149 + - { stepType: assign, targetField: discount, expression: "1" }
  150 + - stepType: while
  151 + when: "qty > 0"
  152 + maxIterations: 10
  153 + body:
  154 + - { stepType: assign, targetField: qty, expression: "qty - 1" }
  155 + - stepType: parallel
  156 + branches:
  157 + - steps:
  158 + - { stepType: callService, method: GET, endpoint: /ext/credit-check }
  159 + - steps:
  160 + - { stepType: timer, delayMs: 500 }
  161 + - { stepType: waitEvent, waitEvent: OrderCreatedEvent }
  162 + - stepType: userTask
  163 + role: 主管
  164 + # 无表单演示场景不设 approveParam,审批网关自动放行;真实场景可设 approveParam 卡在待审批(缺 true 即 PENDING)
  165 + - stepType: retry
  166 + maxAttempts: 2
  167 + body:
  168 + - { stepType: script, expression: "amount * discount", assignTo: payable }
  169 + - stepType: return
... ...
models/M5-security.yaml 0 → 100644
  1 +++ a/models/M5-security.yaml
  1 +# M5 权限脱敏安全模型 (v2.1:新增 order_audit 权限,绑定 SCENE_AUDIT_ORDER)
  2 +# 区分模板编辑管理员权限、页面访问权限、敏感字段掩码渲染规则;前端渲染引擎自动读取执行。
  3 +securityModel:
  4 + globalConfig:
  5 + authMode: oauth2_jwt
  6 + auditEnable: true
  7 +
  8 + principals:
  9 + - principalId: normal_user
  10 + desc: C端普通消费者
  11 + - principalId: backend_admin
  12 + desc: 后台运营管理员
  13 +
  14 + functionPermissions:
  15 + - permId: customer_create
  16 + bindScene: SCENE_CREATE_CUSTOMER
  17 + allowPrincipals: [backend_admin]
  18 + - permId: order_create
  19 + bindScene: SCENE_CREATE_ORDER
  20 + allowPrincipals: [normal_user, backend_admin]
  21 + - permId: order_modify_addr
  22 + bindScene: SCENE_MODIFY_ORDER_ADDR
  23 + allowPrincipals: [normal_user, backend_admin]
  24 + - permId: order_cancel
  25 + bindScene: SCENE_CANCEL_ORDER
  26 + allowPrincipals: [normal_user, backend_admin]
  27 + - permId: order_audit
  28 + bindScene: SCENE_AUDIT_ORDER
  29 + allowPrincipals: [normal_user, backend_admin]
  30 + - permId: customer_modify
  31 + bindScene: SCENE_MODIFY_CUSTOMER
  32 + allowPrincipals: [backend_admin]
  33 + - permId: product_create
  34 + bindScene: SCENE_CREATE_PRODUCT
  35 + allowPrincipals: [backend_admin]
  36 + - permId: product_modify_price
  37 + bindScene: SCENE_MODIFY_PRODUCT_PRICE
  38 + allowPrincipals: [backend_admin]
  39 + - permId: employee_create
  40 + bindScene: SCENE_CREATE_EMPLOYEE
  41 + allowPrincipals: [backend_admin]
  42 + - permId: employee_modify
  43 + bindScene: SCENE_MODIFY_EMPLOYEE
  44 + allowPrincipals: [backend_admin]
  45 +
  46 + maskRules:
  47 + contactPhone: "{first3}****{last4}"
  48 + receiverPhone: "{first3}****{last4}"
  49 +
  50 + encryptRules:
  51 + storageEncrypt: [contactPhone, receiverPhone]
... ...
models/M6-monitor.yaml 0 → 100644
  1 +++ a/models/M6-monitor.yaml
  1 +# M6 全链路监控告警 (v2.0, 无修改)
  2 +# 前端页面加载、表单提交、接口异常自动埋点,匹配后端告警规则。
  3 +monitorModel:
  4 + globalConfig:
  5 + traceEngine: skywalking
  6 + metricExport: prometheus
  7 +
  8 + serviceMetricBind:
  9 + - serviceName: customer-service
  10 + metricItems: [qps, rt, error_count]
  11 + - serviceName: product-service
  12 + metricItems: [qps, rt, stock_deduct_fail]
  13 + - serviceName: order-service
  14 + metricItems: [qps, rt, create_fail, cancel_fail]
  15 +
  16 + alertRules:
  17 + - desc: 订单创建失败率大于5%
  18 + level: 严重告警
  19 + trigger: order-service.create_fail / order-service.qps > 0.05
  20 + - desc: 库存扣减失败突增
  21 + level: 警告告警
  22 + trigger: product-service.stock_deduct_fail > 10
... ...
models/M7-sla.yaml 0 → 100644
  1 +++ a/models/M7-sla.yaml
  1 +# M7 流量管控SLA规范 (v2.0, 无修改)
  2 +# 接口流量、熔断、超时规范,前端请求层统一复用SLA限流配置。
  3 +qualityModel:
  4 + aggregateSla:
  5 + CustomerAggregate:
  6 + availability: 99.95
  7 + ProductAggregate:
  8 + availability: 99.95
  9 + OrderAggregate:
  10 + availability: 99.99
  11 + commandSla:
  12 + CreateOrder:
  13 + RT: 300ms
  14 + StockDeduct:
  15 + RT: 100ms
  16 + sceneSla:
  17 + SCENE_CREATE_ORDER:
  18 + endToEndRT: 600ms
  19 + successRate: 99.9
  20 + flowControl:
  21 + globalLimitQps: 20000
  22 + circuitBreaker:
  23 + enable: true
  24 + failureThreshold: 20
  25 + waitDuration: 3000ms
... ...
models/M8-front-schema.yaml 0 → 100644
  1 +++ a/models/M8-front-schema.yaml
  1 +# M8 React前端可视化拖拽页面模板建模层 v2.2 (新增 template_order_audit 订单审核 + template_employee_create/modify 员工录入/修改)
  2 +# 约束:所有组件字段强制绑定M1领域模型,不允许自定义字段;自动关联M4场景、M2命令、M5脱敏、MetaRule规则。
  3 +# 业务迭代仅修改本模板,无需修改React底层业务源码 —— 通用渲染引擎读取本文件动态生成页面。
  4 +frontModel:
  5 + reactGlobalConfig:
  6 + uiLib: antd
  7 + dragEngineStorage: mysql8
  8 + renderFramework: react18
  9 + autoGenFormSubmit: true
  10 + autoBindApiFromScene: true
  11 + dragRolePermission: [backend_admin]
  12 + desc: 拖拽编辑器仅管理员可见;普通用户仅渲染页面,不可编辑模板
  13 +
  14 + pageTemplates:
  15 + # 模板1:单实体表单模板 - 客户录入
  16 + - templateId: template_customer_single
  17 + templateName: 客户单表单录入模板
  18 + templateType: single_table_form
  19 + renderMode: page_container
  20 + bindSceneId: SCENE_CREATE_CUSTOMER
  21 + masterAggregate: CustomerAggregate
  22 + detailAggregates: []
  23 + rootComponents:
  24 + - compId: card_customer_base
  25 + compType: card
  26 + width: 100%
  27 + span: 24
  28 + fieldBind:
  29 + bindAggregateId: CustomerAggregate
  30 + bindSourceType: aggregate_root
  31 + domainFieldName: customerId
  32 + formLabel: 客户基础信息卡片12
  33 + childComponents:
  34 + - compId: input_cust_name
  35 + compType: input
  36 + span: 12
  37 + fieldBind:
  38 + bindAggregateId: CustomerAggregate
  39 + bindSourceType: aggregate_root
  40 + domainFieldName: customerName
  41 + formLabel: 客户名称12123123
  42 + required: true
  43 + - compId: input_cust_phone
  44 + compType: input
  45 + span: 12
  46 + fieldBind:
  47 + bindAggregateId: CustomerAggregate
  48 + bindSourceType: aggregate_root
  49 + domainFieldName: contactPhone
  50 + formLabel: 联系手机号
  51 + required: true
  52 + maskField: true
  53 + - compId: select_cust_level
  54 + compType: select
  55 + span: 12
  56 + fieldBind:
  57 + bindAggregateId: CustomerAggregate
  58 + bindSourceType: aggregate_root
  59 + domainFieldName: customerLevel
  60 + formLabel: 客户等级
  61 + required: true
  62 + - compId: date_register
  63 + compType: date_picker
  64 + span: 12
  65 + fieldBind:
  66 + bindAggregateId: CustomerAggregate
  67 + bindSourceType: aggregate_root
  68 + domainFieldName: registerTime
  69 + formLabel: 注册时间
  70 +
  71 + # 模板2:主从表单模板 - 订单录入(聚合根+多子实体)
  72 + - templateId: template_order_master_detail
  73 + templateName: 订单主从录入模板
  74 + templateType: master_detail_form
  75 + renderMode: page_container
  76 + bindSceneId: SCENE_CREATE_ORDER
  77 + masterAggregate: OrderAggregate
  78 + detailAggregates: [OrderItem, PaymentTerm, DeliveryAddressEntity]
  79 + rootComponents:
  80 + # 订单主信息卡片
  81 + - compId: card_order_master
  82 + compType: card
  83 + width: 100%
  84 + span: 24
  85 + fieldBind:
  86 + bindAggregateId: OrderAggregate
  87 + bindSourceType: aggregate_root
  88 + domainFieldName: orderId
  89 + formLabel: 订单主信息
  90 + childComponents:
  91 + - compId: select_customer
  92 + compType: select
  93 + span: 12
  94 + fieldBind:
  95 + bindAggregateId: CustomerAggregate
  96 + bindSourceType: aggregate_root
  97 + domainFieldName: customerId
  98 + formLabel: 客户
  99 + required: true
  100 + - compId: input_currency
  101 + compType: input
  102 + span: 12
  103 + fieldBind:
  104 + bindAggregateId: OrderAggregate
  105 + bindSourceType: aggregate_root
  106 + domainFieldName: totalCurrency
  107 + formLabel: 结算币种
  108 + required: true
  109 + - compId: number_total_amt
  110 + compType: number_input
  111 + span: 12
  112 + fieldBind:
  113 + bindAggregateId: OrderAggregate
  114 + bindSourceType: aggregate_root
  115 + domainFieldName: totalAmount
  116 + formLabel: 订单总金额
  117 + required: true
  118 + - compId: date_create
  119 + compType: date_picker
  120 + span: 12
  121 + fieldBind:
  122 + bindAggregateId: OrderAggregate
  123 + bindSourceType: aggregate_root
  124 + domainFieldName: createTime
  125 + formLabel: 下单时间
  126 + # 订单明细表格子实体
  127 + - compId: table_order_item
  128 + compType: table
  129 + width: 100%
  130 + span: 24
  131 + fieldBind:
  132 + bindAggregateId: OrderAggregate
  133 + bindSourceType: aggregate_entity
  134 + domainEntityName: OrderItem
  135 + domainFieldName: itemId
  136 + formLabel: 订单商品明细
  137 + childComponents:
  138 + - compId: select_product
  139 + compType: select
  140 + span: 6
  141 + fieldBind:
  142 + bindAggregateId: ProductAggregate
  143 + bindSourceType: aggregate_root
  144 + domainFieldName: productId
  145 + formLabel: 商品
  146 + required: true
  147 + - compId: number_qty
  148 + compType: number_input
  149 + span: 6
  150 + fieldBind:
  151 + bindAggregateId: OrderAggregate
  152 + bindSourceType: aggregate_entity
  153 + domainEntityName: OrderItem
  154 + domainFieldName: quantity
  155 + formLabel: 购买数量
  156 + required: true
  157 + - compId: number_price
  158 + compType: number_input
  159 + span: 6
  160 + fieldBind:
  161 + bindAggregateId: OrderAggregate
  162 + bindSourceType: aggregate_entity
  163 + domainEntityName: OrderItem
  164 + domainFieldName: itemPrice
  165 + formLabel: 单品单价
  166 + - compId: input_item_currency
  167 + compType: input
  168 + span: 6
  169 + fieldBind:
  170 + bindAggregateId: OrderAggregate
  171 + bindSourceType: aggregate_entity
  172 + domainEntityName: OrderItem
  173 + domainFieldName: itemCurrency
  174 + formLabel: 单品币种
  175 + # 付款条件子实体卡片
  176 + - compId: card_payment_term
  177 + compType: card
  178 + width: 100%
  179 + span: 24
  180 + fieldBind:
  181 + bindAggregateId: OrderAggregate
  182 + bindSourceType: aggregate_entity
  183 + domainEntityName: PaymentTerm
  184 + domainFieldName: termId
  185 + formLabel: 付款条款
  186 + childComponents:
  187 + - compId: select_pay_type
  188 + compType: select
  189 + span: 12
  190 + fieldBind:
  191 + bindAggregateId: OrderAggregate
  192 + bindSourceType: aggregate_entity
  193 + domainEntityName: PaymentTerm
  194 + domainFieldName: paymentType
  195 + formLabel: 付款方式
  196 + - compId: number_duedays
  197 + compType: number_input
  198 + span: 12
  199 + fieldBind:
  200 + bindAggregateId: OrderAggregate
  201 + bindSourceType: aggregate_entity
  202 + domainEntityName: PaymentTerm
  203 + domainFieldName: dueDays
  204 + formLabel: 账期天数
  205 + # 收货地址子实体卡片
  206 + - compId: card_delivery_addr
  207 + compType: card
  208 + width: 100%
  209 + span: 24
  210 + fieldBind:
  211 + bindAggregateId: OrderAggregate
  212 + bindSourceType: aggregate_entity
  213 + domainEntityName: DeliveryAddressEntity
  214 + domainFieldName: addrId
  215 + formLabel: 收货地址
  216 + childComponents:
  217 + - compId: input_province
  218 + compType: input
  219 + span: 6
  220 + fieldBind:
  221 + bindAggregateId: OrderAggregate
  222 + bindSourceType: aggregate_entity
  223 + domainEntityName: DeliveryAddressEntity
  224 + domainFieldName: province
  225 + formLabel: 省份
  226 + - compId: input_city
  227 + compType: input
  228 + span: 6
  229 + fieldBind:
  230 + bindAggregateId: OrderAggregate
  231 + bindSourceType: aggregate_entity
  232 + domainEntityName: DeliveryAddressEntity
  233 + domainFieldName: city
  234 + formLabel: 城市
  235 + - compId: input_district
  236 + compType: input
  237 + span: 6
  238 + fieldBind:
  239 + bindAggregateId: OrderAggregate
  240 + bindSourceType: aggregate_entity
  241 + domainEntityName: DeliveryAddressEntity
  242 + domainFieldName: district
  243 + formLabel: 区县
  244 + - compId: input_street
  245 + compType: input
  246 + span: 6
  247 + fieldBind:
  248 + bindAggregateId: OrderAggregate
  249 + bindSourceType: aggregate_entity
  250 + domainEntityName: DeliveryAddressEntity
  251 + domainFieldName: streetDetail
  252 + formLabel: 详细街道
  253 + - compId: input_receiver
  254 + compType: input
  255 + span: 12
  256 + fieldBind:
  257 + bindAggregateId: OrderAggregate
  258 + bindSourceType: aggregate_entity
  259 + domainEntityName: DeliveryAddressEntity
  260 + domainFieldName: receiverName
  261 + formLabel: 收货人
  262 + - compId: input_receiver_phone
  263 + compType: input
  264 + span: 12
  265 + fieldBind:
  266 + bindAggregateId: OrderAggregate
  267 + bindSourceType: aggregate_entity
  268 + domainEntityName: DeliveryAddressEntity
  269 + domainFieldName: receiverPhone
  270 + formLabel: 收货电话
  271 + maskField: true
  272 +
  273 + # 模板3:单键操作模板 - 取消订单(cancel 命令,仅选订单即可,后端据 ME 补偿归还库存)
  274 + - templateId: template_order_cancel
  275 + templateName: 订单取消模板
  276 + templateType: single_table_form
  277 + renderMode: page_container
  278 + bindSceneId: SCENE_CANCEL_ORDER
  279 + masterAggregate: OrderAggregate
  280 + detailAggregates: []
  281 + rootComponents:
  282 + - compId: card_cancel_order
  283 + compType: card
  284 + width: 100%
  285 + span: 24
  286 + fieldBind:
  287 + bindAggregateId: OrderAggregate
  288 + bindSourceType: aggregate_root
  289 + domainFieldName: orderId
  290 + formLabel: 取消订单
  291 + childComponents:
  292 + - compId: select_cancel_order
  293 + compType: select
  294 + span: 12
  295 + fieldBind:
  296 + bindAggregateId: OrderAggregate
  297 + bindSourceType: aggregate_root
  298 + domainFieldName: orderId
  299 + formLabel: 待取消订单
  300 + required: true
  301 +
  302 + # 模板4:主从更新模板 - 修改订单收货地址(update 命令,主卡选订单+子实体卡录新地址)
  303 + - templateId: template_order_modify_addr
  304 + templateName: 订单收货地址修改模板
  305 + templateType: master_detail_form
  306 + renderMode: page_container
  307 + bindSceneId: SCENE_MODIFY_ORDER_ADDR
  308 + masterAggregate: OrderAggregate
  309 + detailAggregates: [DeliveryAddressEntity]
  310 + rootComponents:
  311 + # 目标订单(聚合根,提供 orderId 定位待更新订单)
  312 + - compId: card_modaddr_order
  313 + compType: card
  314 + width: 100%
  315 + span: 24
  316 + fieldBind:
  317 + bindAggregateId: OrderAggregate
  318 + bindSourceType: aggregate_root
  319 + domainFieldName: orderId
  320 + formLabel: 目标订单
  321 + childComponents:
  322 + - compId: select_modaddr_order
  323 + compType: select
  324 + span: 12
  325 + fieldBind:
  326 + bindAggregateId: OrderAggregate
  327 + bindSourceType: aggregate_root
  328 + domainFieldName: orderId
  329 + formLabel: 订单
  330 + required: true
  331 + # 新收货地址(DeliveryAddressEntity 子实体,字段/校验/脱敏与订单录入同源)
  332 + - compId: card_modaddr_addr
  333 + compType: card
  334 + width: 100%
  335 + span: 24
  336 + fieldBind:
  337 + bindAggregateId: OrderAggregate
  338 + bindSourceType: aggregate_entity
  339 + domainEntityName: DeliveryAddressEntity
  340 + domainFieldName: addrId
  341 + formLabel: 新收货地址
  342 + childComponents:
  343 + - compId: input_modaddr_province
  344 + compType: input
  345 + span: 6
  346 + fieldBind:
  347 + bindAggregateId: OrderAggregate
  348 + bindSourceType: aggregate_entity
  349 + domainEntityName: DeliveryAddressEntity
  350 + domainFieldName: province
  351 + formLabel: 省份
  352 + - compId: input_modaddr_city
  353 + compType: input
  354 + span: 6
  355 + fieldBind:
  356 + bindAggregateId: OrderAggregate
  357 + bindSourceType: aggregate_entity
  358 + domainEntityName: DeliveryAddressEntity
  359 + domainFieldName: city
  360 + formLabel: 城市
  361 + - compId: input_modaddr_district
  362 + compType: input
  363 + span: 6
  364 + fieldBind:
  365 + bindAggregateId: OrderAggregate
  366 + bindSourceType: aggregate_entity
  367 + domainEntityName: DeliveryAddressEntity
  368 + domainFieldName: district
  369 + formLabel: 区县
  370 + - compId: input_modaddr_street
  371 + compType: input
  372 + span: 6
  373 + fieldBind:
  374 + bindAggregateId: OrderAggregate
  375 + bindSourceType: aggregate_entity
  376 + domainEntityName: DeliveryAddressEntity
  377 + domainFieldName: streetDetail
  378 + formLabel: 详细街道
  379 + - compId: input_modaddr_receiver
  380 + compType: input
  381 + span: 12
  382 + fieldBind:
  383 + bindAggregateId: OrderAggregate
  384 + bindSourceType: aggregate_entity
  385 + domainEntityName: DeliveryAddressEntity
  386 + domainFieldName: receiverName
  387 + formLabel: 收货人
  388 + - compId: input_modaddr_receiver_phone
  389 + compType: input
  390 + span: 12
  391 + fieldBind:
  392 + bindAggregateId: OrderAggregate
  393 + bindSourceType: aggregate_entity
  394 + domainEntityName: DeliveryAddressEntity
  395 + domainFieldName: receiverPhone
  396 + formLabel: 收货电话
  397 + maskField: true
  398 +
  399 + # 模板5:客户信息修改模板(update;主卡选客户 + 可改字段,contactPhone 存储加密/展示脱敏)
  400 + - templateId: template_customer_modify
  401 + templateName: 客户信息修改模板
  402 + templateType: single_table_form
  403 + renderMode: page_container
  404 + bindSceneId: SCENE_MODIFY_CUSTOMER
  405 + masterAggregate: CustomerAggregate
  406 + detailAggregates: []
  407 + rootComponents:
  408 + - compId: card_modcust_base
  409 + compType: card
  410 + width: 100%
  411 + span: 24
  412 + fieldBind:
  413 + bindAggregateId: CustomerAggregate
  414 + bindSourceType: aggregate_root
  415 + domainFieldName: customerId
  416 + formLabel: 修改客户信息
  417 + childComponents:
  418 + - compId: select_modcust_id
  419 + compType: select
  420 + span: 12
  421 + fieldBind:
  422 + bindAggregateId: CustomerAggregate
  423 + bindSourceType: aggregate_root
  424 + domainFieldName: customerId
  425 + formLabel: 目标客户
  426 + required: true
  427 + - compId: input_modcust_name
  428 + compType: input
  429 + span: 12
  430 + fieldBind:
  431 + bindAggregateId: CustomerAggregate
  432 + bindSourceType: aggregate_root
  433 + domainFieldName: customerName
  434 + formLabel: 客户名称
  435 + - compId: input_modcust_phone
  436 + compType: input
  437 + span: 12
  438 + fieldBind:
  439 + bindAggregateId: CustomerAggregate
  440 + bindSourceType: aggregate_root
  441 + domainFieldName: contactPhone
  442 + formLabel: 联系手机号
  443 + maskField: true
  444 + - compId: select_modcust_level
  445 + compType: select
  446 + span: 12
  447 + fieldBind:
  448 + bindAggregateId: CustomerAggregate
  449 + bindSourceType: aggregate_root
  450 + domainFieldName: customerLevel
  451 + formLabel: 客户等级
  452 +
  453 + # 模板6:商品档案录入模板(create;productId 由引擎生成,故主卡不含 id 输入)
  454 + - templateId: template_product_single
  455 + templateName: 商品档案录入模板
  456 + templateType: single_table_form
  457 + renderMode: page_container
  458 + bindSceneId: SCENE_CREATE_PRODUCT
  459 + masterAggregate: ProductAggregate
  460 + detailAggregates: []
  461 + rootComponents:
  462 + - compId: card_product_base
  463 + compType: card
  464 + width: 100%
  465 + span: 24
  466 + fieldBind:
  467 + bindAggregateId: ProductAggregate
  468 + bindSourceType: aggregate_root
  469 + domainFieldName: productId
  470 + formLabel: 商品基础信息
  471 + childComponents:
  472 + - compId: input_prod_name
  473 + compType: input
  474 + span: 12
  475 + fieldBind:
  476 + bindAggregateId: ProductAggregate
  477 + bindSourceType: aggregate_root
  478 + domainFieldName: productName
  479 + formLabel: 商品名称
  480 + required: true
  481 + - compId: input_prod_sku
  482 + compType: input
  483 + span: 12
  484 + fieldBind:
  485 + bindAggregateId: ProductAggregate
  486 + bindSourceType: aggregate_root
  487 + domainFieldName: skuCode
  488 + formLabel: SKU编码
  489 + required: true
  490 + - compId: input_prod_category
  491 + compType: input
  492 + span: 12
  493 + fieldBind:
  494 + bindAggregateId: ProductAggregate
  495 + bindSourceType: aggregate_root
  496 + domainFieldName: category
  497 + formLabel: 商品类目
  498 + - compId: input_prod_currency
  499 + compType: input
  500 + span: 12
  501 + fieldBind:
  502 + bindAggregateId: ProductAggregate
  503 + bindSourceType: aggregate_root
  504 + domainFieldName: saleCurrency
  505 + formLabel: 销售币种
  506 + - compId: number_prod_price
  507 + compType: number_input
  508 + span: 12
  509 + fieldBind:
  510 + bindAggregateId: ProductAggregate
  511 + bindSourceType: aggregate_root
  512 + domainFieldName: salePrice
  513 + formLabel: 销售单价
  514 + required: true
  515 + - compId: number_prod_stock
  516 + compType: number_input
  517 + span: 12
  518 + fieldBind:
  519 + bindAggregateId: ProductAggregate
  520 + bindSourceType: aggregate_root
  521 + domainFieldName: stockNum
  522 + formLabel: 库存数量
  523 + required: true
  524 + - compId: switch_prod_onsale
  525 + compType: switch
  526 + span: 12
  527 + fieldBind:
  528 + bindAggregateId: ProductAggregate
  529 + bindSourceType: aggregate_root
  530 + domainFieldName: isOnSale
  531 + formLabel: 是否上架
  532 +
  533 + # 模板7:商品售价修改模板(update;主卡选商品 + 改币种/售价,售价字段在聚合根表可真正落库)
  534 + - templateId: template_product_modify_price
  535 + templateName: 商品售价修改模板
  536 + templateType: single_table_form
  537 + renderMode: page_container
  538 + bindSceneId: SCENE_MODIFY_PRODUCT_PRICE
  539 + masterAggregate: ProductAggregate
  540 + detailAggregates: []
  541 + rootComponents:
  542 + - compId: card_modprice_base
  543 + compType: card
  544 + width: 100%
  545 + span: 24
  546 + fieldBind:
  547 + bindAggregateId: ProductAggregate
  548 + bindSourceType: aggregate_root
  549 + domainFieldName: productId
  550 + formLabel: 修改商品售价
  551 + childComponents:
  552 + - compId: select_modprice_id
  553 + compType: select
  554 + span: 12
  555 + fieldBind:
  556 + bindAggregateId: ProductAggregate
  557 + bindSourceType: aggregate_root
  558 + domainFieldName: productId
  559 + formLabel: 目标商品
  560 + required: true
  561 + - compId: input_modprice_currency
  562 + compType: input
  563 + span: 12
  564 + fieldBind:
  565 + bindAggregateId: ProductAggregate
  566 + bindSourceType: aggregate_root
  567 + domainFieldName: saleCurrency
  568 + formLabel: 销售币种
  569 + - compId: number_modprice_price
  570 + compType: number_input
  571 + span: 12
  572 + fieldBind:
  573 + bindAggregateId: ProductAggregate
  574 + bindSourceType: aggregate_root
  575 + domainFieldName: salePrice
  576 + formLabel: 新售价
  577 +
  578 + # 模板9:订单审核模板(update;主卡选订单 + 选审核人 + 审核结论/备注)
  579 + # 演示 AuditOrder 的 bizSteps 闭指令集程序:LOAD_AGGREGATE → AUTO_FILL(auditTime) → SET_FIELD(auditorId)
  580 + # → REMOTE_QUERY_SET_FIELD(按 auditorId 查 EmployeeAggregate.employeeName 回填 auditorName)
  581 + # → SET_FIELD(auditRemark) → CONDITION_SET_FIELD(auditResult → orderStatus) → COMMIT_TRANSACTION。
  582 + # 审核人下拉:auditorId 带 M1 refAggregate=EmployeeAggregate,渲染引擎通用地据引用取员工选项(非硬编码)。
  583 + - templateId: template_order_audit
  584 + templateName: 订单审核模板
  585 + templateType: single_table_form
  586 + renderMode: page_container
  587 + bindSceneId: SCENE_AUDIT_ORDER
  588 + masterAggregate: OrderAggregate
  589 + detailAggregates: []
  590 + rootComponents:
  591 + - compId: card_audit_order
  592 + compType: card
  593 + width: 100%
  594 + span: 24
  595 + fieldBind:
  596 + bindAggregateId: OrderAggregate
  597 + bindSourceType: aggregate_root
  598 + domainFieldName: orderId
  599 + formLabel: 订单审核
  600 + childComponents:
  601 + - compId: select_audit_order
  602 + compType: select
  603 + span: 12
  604 + fieldBind:
  605 + bindAggregateId: OrderAggregate
  606 + bindSourceType: aggregate_root
  607 + domainFieldName: orderId
  608 + formLabel: 待审核订单
  609 + required: true
  610 + - compId: select_audit_auditor
  611 + compType: select
  612 + span: 12
  613 + fieldBind:
  614 + bindAggregateId: OrderAggregate
  615 + bindSourceType: aggregate_root
  616 + domainFieldName: auditorId
  617 + formLabel: 审核人(引用员工聚合)
  618 + required: true
  619 + - compId: select_audit_result
  620 + compType: select
  621 + span: 12
  622 + fieldBind:
  623 + bindAggregateId: OrderAggregate
  624 + bindSourceType: aggregate_root
  625 + domainFieldName: auditResult
  626 + formLabel: 审核结论(PASS/REJECT)
  627 + required: true
  628 + - compId: input_audit_remark
  629 + compType: text_area
  630 + span: 12
  631 + fieldBind:
  632 + bindAggregateId: OrderAggregate
  633 + bindSourceType: aggregate_root
  634 + domainFieldName: auditRemark
  635 + formLabel: 审核备注
  636 +
  637 + # 模板10:员工档案录入模板(create;employeeId 由引擎生成,审核计数由 CreateEmployee 的 bizSteps 置 0)
  638 + - templateId: template_employee_create
  639 + templateName: 员工档案录入模板
  640 + templateType: single_table_form
  641 + renderMode: page_container
  642 + bindSceneId: SCENE_CREATE_EMPLOYEE
  643 + masterAggregate: EmployeeAggregate
  644 + detailAggregates: []
  645 + rootComponents:
  646 + - compId: card_employee_base
  647 + compType: card
  648 + width: 100%
  649 + span: 24
  650 + fieldBind:
  651 + bindAggregateId: EmployeeAggregate
  652 + bindSourceType: aggregate_root
  653 + domainFieldName: employeeId
  654 + formLabel: 员工基础信息
  655 + childComponents:
  656 + - compId: input_emp_name
  657 + compType: input
  658 + span: 12
  659 + fieldBind:
  660 + bindAggregateId: EmployeeAggregate
  661 + bindSourceType: aggregate_root
  662 + domainFieldName: employeeName
  663 + formLabel: 员工姓名
  664 + required: true
  665 + - compId: input_emp_dept
  666 + compType: input
  667 + span: 12
  668 + fieldBind:
  669 + bindAggregateId: EmployeeAggregate
  670 + bindSourceType: aggregate_root
  671 + domainFieldName: department
  672 + formLabel: 部门
  673 +
  674 + # 模板11:员工信息修改模板(update;主卡选员工 + 改姓名/部门/备注)
  675 + - templateId: template_employee_modify
  676 + templateName: 员工信息修改模板
  677 + templateType: single_table_form
  678 + renderMode: page_container
  679 + bindSceneId: SCENE_MODIFY_EMPLOYEE
  680 + masterAggregate: EmployeeAggregate
  681 + detailAggregates: []
  682 + rootComponents:
  683 + - compId: card_modemp_base
  684 + compType: card
  685 + width: 100%
  686 + span: 24
  687 + fieldBind:
  688 + bindAggregateId: EmployeeAggregate
  689 + bindSourceType: aggregate_root
  690 + domainFieldName: employeeId
  691 + formLabel: 修改员工信息
  692 + childComponents:
  693 + - compId: select_modemp_id
  694 + compType: select
  695 + span: 12
  696 + fieldBind:
  697 + bindAggregateId: EmployeeAggregate
  698 + bindSourceType: aggregate_root
  699 + domainFieldName: employeeId
  700 + formLabel: 目标员工
  701 + required: true
  702 + - compId: input_modemp_name
  703 + compType: input
  704 + span: 12
  705 + fieldBind:
  706 + bindAggregateId: EmployeeAggregate
  707 + bindSourceType: aggregate_root
  708 + domainFieldName: employeeName
  709 + formLabel: 员工姓名
  710 + - compId: input_modemp_dept
  711 + compType: input
  712 + span: 12
  713 + fieldBind:
  714 + bindAggregateId: EmployeeAggregate
  715 + bindSourceType: aggregate_root
  716 + domainFieldName: department
  717 + formLabel: 部门
  718 + - compId: input_modemp_remark
  719 + compType: text_area
  720 + span: 12
  721 + fieldBind:
  722 + bindAggregateId: EmployeeAggregate
  723 + bindSourceType: aggregate_root
  724 + domainFieldName: remark
  725 + formLabel: 备注
  726 +
  727 + # 模板8:空白自定义拖拽模板(全新业务场景使用)
  728 + - templateId: template_custom_blank
  729 + templateName: 空白自定义拖拽模板
  730 + templateType: custom_drag_form
  731 + renderMode: drawer
  732 + bindSceneId: ""
  733 + masterAggregate: null
  734 + detailAggregates: []
  735 + rootComponents: []
... ...
models/ME-event.yaml 0 → 100644
  1 +++ a/models/ME-event.yaml
  1 +# ME EDA事件驱动模型 (v2.1:新增 OrderAudited 事件)
  2 +# 页面提交完成后监听领域事件,自动刷新页面、弹窗反馈、触发异步补偿逻辑。
  3 +eventModel:
  4 + globalConfig:
  5 + idGenerator: snowflake
  6 + bus: rocketmq
  7 + retry: 3
  8 + dlq: topic_msg_dlq
  9 + outbox:
  10 + enabled: true
  11 + table: t_domain_outbox
  12 + pollInterval: 1000ms
  13 + batchSize: 50
  14 + cleanupDays: 7
  15 +
  16 + aggregateEventDefinitions:
  17 + - aggregateId: CustomerAggregate
  18 + events:
  19 + CustomerCreated:
  20 + topic: topic_customer_create
  21 + payload: [customerId, customerName, customerLevel]
  22 + consumers: [customer-service]
  23 + CustomerInfoModified:
  24 + topic: topic_customer_modify
  25 + payload: [customerId, contactPhone, customerLevel]
  26 + consumers: [customer-service]
  27 +
  28 + - aggregateId: ProductAggregate
  29 + events:
  30 + ProductCreated:
  31 + topic: topic_product_create
  32 + payload: [productId, skuCode, salePrice, stockNum]
  33 + consumers: [product-service]
  34 + ProductPriceChanged:
  35 + topic: topic_product_price
  36 + payload: [productId, salePrice, saleCurrency]
  37 + consumers: [order-service]
  38 + ProductStockDeducted:
  39 + topic: topic_stock_deduct
  40 + payload: [productId, stockNum]
  41 + consumers: [order-service]
  42 + ProductStockReturned:
  43 + topic: topic_stock_return
  44 + payload: [productId, stockNum]
  45 + consumers: [order-service]
  46 +
  47 + - aggregateId: OrderAggregate
  48 + events:
  49 + OrderCreated:
  50 + topic: topic_order_create
  51 + payload: [orderId, customerId, totalAmount]
  52 + consumers: [product-service, customer-service]
  53 + OrderItemAdded:
  54 + topic: topic_order_item_added
  55 + payload: [orderId]
  56 + consumers: [order-service]
  57 + OrderPaymentTermSet:
  58 + topic: topic_order_payment_term_set
  59 + payload: [orderId]
  60 + consumers: [order-service]
  61 + OrderDeliveryAddressSet:
  62 + topic: topic_order_delivery_address_set
  63 + payload: [orderId]
  64 + consumers: [order-service]
  65 + OrderDeliveryAddressModified:
  66 + topic: topic_order_addr_modify
  67 + payload: [orderId, receiverPhone]
  68 + consumers: [order-service]
  69 + OrderCancelled:
  70 + topic: topic_order_cancel
  71 + payload: [orderId, productIdList]
  72 + consumers: [product-service]
  73 + OrderAudited:
  74 + topic: topic_order_audited
  75 + payload: [orderId, orderStatus, auditorId, auditorName]
  76 + consumers: [order-service]
  77 +
  78 + - aggregateId: EmployeeAggregate
  79 + events:
  80 + EmployeeCreated:
  81 + topic: topic_employee_create
  82 + payload: [employeeId, employeeName, department]
  83 + consumers: [employee-service]
  84 + EmployeeModified:
  85 + topic: topic_employee_modify
  86 + payload: [employeeId, employeeName, department, remark]
  87 + consumers: [employee-service]
  88 +
  89 + # 跨聚合最终一致规则(结构化):
  90 + # sourceItemEntity 指明从触发聚合的哪个子实体取"逐项上下文";
  91 + # paramMap 把子实体字段映射为目标命令入参;引擎据此 + M2 命令的 effect 通用执行,无硬编码。
  92 + crossAggConsistencyRules:
  93 + - triggerEvent: OrderCreated
  94 + sourceItemEntity: OrderItem
  95 + targetAggregate: ProductAggregate
  96 + targetCommand: StockDeduct
  97 + paramMap:
  98 + productId: productId
  99 + deductQty: quantity
  100 + consistencyType: eventual
  101 + - triggerEvent: OrderCancelled
  102 + sourceItemEntity: OrderItem
  103 + targetAggregate: ProductAggregate
  104 + targetCommand: StockReturn
  105 + paramMap:
  106 + productId: productId
  107 + returnQty: quantity
  108 + consistencyType: eventual
... ...
models/MetaRule-business.yaml 0 → 100644
  1 +++ a/models/MetaRule-business.yaml
  1 +# MetaRule 前端后端统一动态规则引擎 (v2.0, 无修改)
  2 +# Aviator统一动态规则:前端渲染引擎实时执行(显隐/禁用/输入拦截/预警),后端执行持久化校验,规则仅维护一份。
  3 +metaRuleModel:
  4 + globalConfig:
  5 + ruleEngine: aviator
  6 + hotUpdate: true
  7 +
  8 + ruleGlobalParams:
  9 + - paramId: single_order_max_amount
  10 + name: 单笔订单最大限额
  11 + dataType: decimal
  12 + defaultValue: 50000
  13 + - paramId: stock_warn_min
  14 + name: 库存预警最低值
  15 + dataType: int
  16 + defaultValue: 20
  17 + - paramId: vip_discount_limit
  18 + name: VIP免风控金额阈值
  19 + dataType: decimal
  20 + defaultValue: 10000
  21 +
  22 + ruleGroups:
  23 + - groupId: ORDER_RISK_RULE
  24 + groupType: 下单风控校验
  25 + bindScene: SCENE_CREATE_ORDER
  26 + rules:
  27 + - ruleId: R_ORDER_AMOUNT_LIMIT
  28 + ruleVersion: "1.0"
  29 + ruleName: 大额订单拦截
  30 + matchCondition: 'totalAmount > #{single_order_max_amount} && customerLevel != "VIP"'
  31 + effect: REJECT
  32 + message: 普通用户单笔订单不可超过50000元,请拆分下单
  33 + - ruleId: R_STOCK_LOW_WARN
  34 + ruleVersion: "1.0"
  35 + ruleName: 低库存预警
  36 + matchCondition: 'stockNum < #{stock_warn_min}'
  37 + effect: ALERT
  38 + message: 商品库存低于预警值,请及时补货
  39 +
  40 + - groupId: ORDER_COMPENSATE_RULE
  41 + groupType: 订单取消补偿逻辑
  42 + bindScene: SCENE_CANCEL_ORDER
  43 + rules:
  44 + - ruleId: R_CANCEL_STOCK_RETURN
  45 + ruleVersion: "1.0"
  46 + ruleName: 取消订单归还库存
  47 + matchCondition: "true"
  48 + effect: COMPENSATE
  49 + message: 订单取消自动返还锁定商品库存
... ...
request.txt 0 → 100644
  1 +++ a/request.txt
  1 +
  2 +1、根据下面模型文件,完整代码实现案例(含前端React端到端测试页Demo),特别是M4的flowSteps,M2的bizSteps,MetaRule,架构引擎负责解释与执行,各层釆用什么具体技术,M 8前端模板定义及如何与具体业务绑定,并用案例方式推演解释整体架构的运行原理
  3 +
  4 +2、以上方案实现使用 Claude Code 全流程开发,包含工程目录结构、Claude专用配置、全套建模YAML、后端Java引擎源码、React前端Demo、场景推演文档、代码生成模板、部署脚本、测试用例等等。
  5 +
  6 +模型文件如下:
  7 +本体驱动 DDD+EDA 全量11份YAML架构文档 v2.0
  8 +架构总述
  9 +版本:v2.0 | 状态:生产级完备,支持代码生成
  10 +架构分层全集:M0-meta-schema、M1-domain、M2-command、ME-event、M3-deploy、M4-scene、M5-security、M6-monitor、M7-sla、MetaRule-business、M8-front-schema
  11 +核心扩展:新增M8-front-schema React可视化拖拽建模层,M0补充前端模板完整JSON Schema校验;全量兼容原有DDD+EDA本体模型,无破坏性变更、仅增量扩展;实现业务专家无代码拖拽生成React页面,页面数据源强制绑定M1领域模型,页面自动关联M4场景、M2命令API、M5脱敏权限、MetaRule动态规则,实现后期仅维护模板、不修改前端业务源码。
  12 +
  13 +一、M0-meta-schema.yaml(全域元数据校验根规范,新增M8前端Schema定义)
  14 +meta_model:
  15 + version: "2.0"
  16 + $schema: https://json-schema.org/draft/2020-12/schema
  17 + definitions:
  18 + # 基础通用标识类型
  19 + Identifier:
  20 + type: string
  21 + pattern: "^[a-z][a-zA-Z0-9]*$"
  22 + description: 小写驼峰字段/组件标识
  23 + UpperIdentifier:
  24 + type: string
  25 + pattern: "^[A-Z][a-zA-Z0-9]*$"
  26 + description: 大写驼峰聚合/命令/事件ID
  27 + NonEmptyString:
  28 + type: string
  29 + minLength: 1
  30 + SceneId:
  31 + type: string
  32 + pattern: "^SCENE_[A-Z0-9_]+$"
  33 + RuleId:
  34 + type: string
  35 + pattern: "^R_[A-Z0-9_]+$"
  36 + TopicName:
  37 + type: string
  38 + pattern: "^topic_[a-z0-9_]+$"
  39 + ConsistencyTypeEnum:
  40 + type: string
  41 + enum: [strong, eventual]
  42 + DbEngineEnum:
  43 + type: string
  44 + enum: [mysql8]
  45 + AuthModeEnum:
  46 + type: string
  47 + enum: [oauth2_jwt]
  48 +
  49 + # ========== M8前端专用枚举(新增) ==========
  50 + FrontTemplateTypeEnum:
  51 + type: string
  52 + enum: [single_table_form, master_detail_form, custom_drag_form]
  53 + description: single_table_form单实体表单;master_detail_form主从表单;custom_drag_form自定义空白拖拽模板
  54 + FrontComponentEnum:
  55 + type: string
  56 + enum: [input, number_input, date_picker, switch, table, card, text_area, select]
  57 + description: React底层原子拖拽组件
  58 + BindSourceTypeEnum:
  59 + type: string
  60 + enum: [aggregate_root, aggregate_entity]
  61 + description: 组件绑定数据源类型:聚合根/聚合内部子实体
  62 + ReactRenderModeEnum:
  63 + type: string
  64 + enum: [form_modal, page_container, drawer]
  65 + description: 页面渲染载体:弹窗/整页/侧边抽屉
  66 +
  67 + # M1 领域模型单元定义
  68 + M1_Attribute:
  69 + type: object
  70 + required: [name, type]
  71 + properties:
  72 + name: { $ref: "#/definitions/Identifier" }
  73 + type:
  74 + type: string
  75 + enum: [string, int, decimal, datetime, boolean]
  76 + refAggregate: { type: string }
  77 + refRoot: { type: string }
  78 + refIdentifier: { type: string }
  79 + description: { type: string }
  80 + M1_Relation:
  81 + type: object
  82 + required: [ref, relationType]
  83 + properties:
  84 + ref: { $ref: "#/definitions/NonEmptyString" }
  85 + relationType:
  86 + type: string
  87 + enum: [composition]
  88 + M1_Entity:
  89 + type: object
  90 + required: [name, localIdentifier, attributes]
  91 + properties:
  92 + name: { $ref: "#/definitions/NonEmptyString" }
  93 + localIdentifier: { $ref: "#/definitions/Identifier" }
  94 + attributes:
  95 + type: array
  96 + items: { $ref: "#/definitions/M1_Attribute" }
  97 + M1_AggregateRoot:
  98 + type: object
  99 + required: [name, identifier, attributes]
  100 + properties:
  101 + name: { $ref: "#/definitions/NonEmptyString" }
  102 + identifier: { $ref: "#/definitions/Identifier" }
  103 + attributes:
  104 + type: array
  105 + items: { $ref: "#/definitions/M1_Attribute" }
  106 + relations:
  107 + type: array
  108 + items: { $ref: "#/definitions/M1_Relation" }
  109 + M1_Aggregate:
  110 + type: object
  111 + required: [aggregateId, description, aggregateRoot]
  112 + properties:
  113 + aggregateId:
  114 + type: string
  115 + pattern: "^[A-Z][a-zA-Z]+Aggregate$"
  116 + description: { $ref: "#/definitions/NonEmptyString" }
  117 + aggregateRoot: { $ref: "#/definitions/M1_AggregateRoot" }
  118 + entities:
  119 + type: array
  120 + items: { $ref: "#/definitions/M1_Entity" }
  121 + valueObjects:
  122 + type: array
  123 + maxItems: 0
  124 +
  125 + # M2 命令行为单元定义
  126 + M2_InputParam:
  127 + type: object
  128 + required: [name, type]
  129 + properties:
  130 + name: { $ref: "#/definitions/Identifier" }
  131 + type: { type: string }
  132 + optional: { type: boolean }
  133 + M2_Command:
  134 + type: object
  135 + required: [cmdId, desc, inputParams, validations, emitEvents]
  136 + properties:
  137 + cmdId: { $ref: "#/definitions/UpperIdentifier" }
  138 + desc: { $ref: "#/definitions/NonEmptyString" }
  139 + inputParams:
  140 + type: array
  141 + items: { $ref: "#/definitions/M2_InputParam" }
  142 + validations:
  143 + type: array
  144 + items: { type: string }
  145 + bizSteps: { type: string }
  146 + emitEvents:
  147 + type: array
  148 + items: { $ref: "#/definitions/UpperIdentifier" }
  149 + sideEffect: { type: string }
  150 + M2_AggregateBehavior:
  151 + type: object
  152 + required: [aggregateId, aggregateRoot, commands]
  153 + properties:
  154 + aggregateId:
  155 + type: string
  156 + pattern: "^[A-Z][a-zA-Z]+Aggregate$"
  157 + aggregateRoot: { $ref: "#/definitions/NonEmptyString" }
  158 + commands:
  159 + type: array
  160 + items: { $ref: "#/definitions/M2_Command" }
  161 +
  162 + # ME 事件模型单元定义
  163 + ME_Event:
  164 + type: object
  165 + required: [topic, payload]
  166 + properties:
  167 + topic: { $ref: "#/definitions/TopicName" }
  168 + payload:
  169 + type: array
  170 + items: { $ref: "#/definitions/Identifier" }
  171 + consumers:
  172 + type: array
  173 + items: { type: string }
  174 + ME_OutboxConfig:
  175 + type: object
  176 + required: [enabled, table]
  177 + properties:
  178 + enabled: { type: boolean }
  179 + table: { $ref: "#/definitions/NonEmptyString" }
  180 + pollInterval: { type: string }
  181 + batchSize: { type: integer, minimum:1 }
  182 + cleanupDays: { type: integer, minimum:1 }
  183 + ME_CrossConsistencyRule:
  184 + type: object
  185 + required: [triggerEvent, targetAggregate, targetCommand, consistencyType]
  186 + properties:
  187 + triggerEvent: { $ref: "#/definitions/UpperIdentifier" }
  188 + targetAggregate:
  189 + type: string
  190 + pattern: "^[A-Z][a-zA-Z]+Aggregate$"
  191 + targetCommand: { $ref: "#/definitions/UpperIdentifier" }
  192 + consistencyType: { $ref: "#/definitions/ConsistencyTypeEnum" }
  193 +
  194 + # M3 存储部署映射单元定义
  195 + M3_TableMapping:
  196 + type: object
  197 + required: [domainEntity, table, pk, fieldMap]
  198 + properties:
  199 + domainEntity: { $ref: "#/definitions/NonEmptyString" }
  200 + table: { $ref: "#/definitions/NonEmptyString" }
  201 + pk: { $ref: "#/definitions/Identifier" }
  202 + shardRule: { type: string }
  203 + fieldMap:
  204 + type: object
  205 + additionalProperties: { type: string }
  206 + M3_AggregateDeploy:
  207 + type: object
  208 + required: [aggregateId, serviceName, commandApi, tableMappings]
  209 + properties:
  210 + aggregateId:
  211 + type: string
  212 + pattern: "^[A-Z][a-zA-Z]+Aggregate$"
  213 + serviceName:
  214 + type: string
  215 + pattern: "^[a-z-]+-service$"
  216 + commandApi:
  217 + type: object
  218 + additionalProperties:
  219 + type: string
  220 + pattern: "^/api/v1/.+"
  221 + tableMappings:
  222 + type: array
  223 + items: { $ref: "#/definitions/M3_TableMapping" }
  224 +
  225 + # M4 业务场景编排单元定义
  226 + M4_SceneStep:
  227 + type: object
  228 + required: [stepType]
  229 + properties:
  230 + stepType:
  231 + type: string
  232 + enum: [bindCommand, readOnlyCheck, loopItems, return]
  233 + bindAggregate: { type: string }
  234 + bindCommand: { $ref: "#/definitions/UpperIdentifier" }
  235 + M4_Scene:
  236 + type: object
  237 + required: [sceneId, sceneName, aggregateScope, permissionBind, flowSteps]
  238 + properties:
  239 + sceneId: { $ref: "#/definitions/SceneId" }
  240 + sceneName: { $ref: "#/definitions/NonEmptyString" }
  241 + aggregateScope:
  242 + type: array
  243 + items:
  244 + type: string
  245 + pattern: "^[A-Z][a-zA-Z]+Aggregate$"
  246 + permissionBind:
  247 + type: array
  248 + items: { $ref: "#/definitions/Identifier" }
  249 + flowSteps:
  250 + type: array
  251 + items: { $ref: "#/definitions/M4_SceneStep" }
  252 +
  253 + # M5 安全权限单元定义
  254 + M5_SecurityRoot:
  255 + type: object
  256 + required: [globalConfig, principals, functionPermissions, maskRules, encryptRules]
  257 + # M6 监控告警单元定义
  258 + M6_Alert:
  259 + type: object
  260 + required: [desc, level, trigger]
  261 + properties:
  262 + desc: { type: string }
  263 + level:
  264 + type: string
  265 + enum: [严重告警, 警告告警]
  266 + trigger: { type: string }
  267 + M6_MonitorRoot:
  268 + type: object
  269 + required: [globalConfig, serviceMetricBind, alertRules]
  270 + # M7 流量SLA单元定义
  271 + M7_QualityRoot:
  272 + type: object
  273 + required: [aggregateSla, commandSla, sceneSla, flowControl]
  274 + # MetaRule 动态业务规则单元定义
  275 + MetaRule_Param:
  276 + type: object
  277 + required: [paramId, name, dataType, defaultValue]
  278 + MetaRule_Item:
  279 + type: object
  280 + required: [ruleId, ruleVersion, ruleName, matchCondition, effect, message]
  281 + properties:
  282 + ruleId: { $ref: "#/definitions/RuleId" }
  283 + effect:
  284 + type: string
  285 + enum: [REJECT, ALERT, SKIP_CHECK, CALC_FILL, COMPENSATE]
  286 + MetaRule_Group:
  287 + type: object
  288 + required: [groupId, groupType, bindScene, rules]
  289 +
  290 + # ===================== M8 前端拖拽模板完整定义(新增核心) =====================
  291 + M8_FrontFieldBind:
  292 + type: object
  293 + required: [bindAggregateId, bindSourceType, domainFieldName]
  294 + properties:
  295 + bindAggregateId:
  296 + type: string
  297 + pattern: "^[A-Z][a-zA-Z]+Aggregate$"
  298 + bindSourceType: { $ref: "#/definitions/BindSourceTypeEnum" }
  299 + domainEntityName: { type: string }
  300 + domainFieldName: { $ref: "#/definitions/Identifier" }
  301 + formLabel: { type: string }
  302 + placeholder: { type: string }
  303 + required: { type: boolean }
  304 + maskField: { type: boolean }
  305 + M8_FrontDragComponent:
  306 + type: object
  307 + required: [compId, compType, fieldBind]
  308 + properties:
  309 + compId: { $ref: "#/definitions/Identifier" }
  310 + compType: { $ref: "#/definitions/FrontComponentEnum" }
  311 + width: { type: string }
  312 + span: { type: integer }
  313 + fieldBind: { $ref: "#/definitions/M8_FrontFieldBind" }
  314 + childComponents:
  315 + type: array
  316 + items: { $ref: "#/definitions/M8_FrontDragComponent" }
  317 + M8_FrontPageTemplate:
  318 + type: object
  319 + required: [templateId, templateName, templateType, renderMode, bindSceneId, rootComponents]
  320 + properties:
  321 + templateId: { $ref: "#/definitions/Identifier" }
  322 + templateName: { $ref: "#/definitions/NonEmptyString" }
  323 + templateType: { $ref: "#/definitions/FrontTemplateTypeEnum" }
  324 + renderMode: { $ref: "#/definitions/ReactRenderModeEnum" }
  325 + bindSceneId: { $ref: "#/definitions/SceneId" }
  326 + masterAggregate: { type: string }
  327 + detailAggregates:
  328 + type: array
  329 + items: { type: string }
  330 + rootComponents:
  331 + type: array
  332 + items: { $ref: "#/definitions/M8_FrontDragComponent" }
  333 + M8_FrontSchema:
  334 + type: object
  335 + required: [reactGlobalConfig, pageTemplates]
  336 + properties:
  337 + reactGlobalConfig:
  338 + type: object
  339 + required: [uiLib, dragEngineStorage]
  340 + properties:
  341 + uiLib:
  342 + type: string
  343 + const: antd
  344 + dragEngineStorage:
  345 + type: string
  346 + const: mysql8
  347 + pageTemplates:
  348 + type: array
  349 + items: { $ref: "#/definitions/M8_FrontPageTemplate" }
  350 +
  351 + # 各分层根Schema定义(新增M8根校验)
  352 + M1_Schema:
  353 + type: object
  354 + required: [aggregates, globalConstraints]
  355 + M2_Schema:
  356 + type: object
  357 + required: [behaviors, globalBehaviorConstraints]
  358 + ME_Schema:
  359 + type: object
  360 + required: [eventModel]
  361 + M3_Schema:
  362 + type: object
  363 + required: [deploymentMappings]
  364 + M4_Schema:
  365 + type: object
  366 + required: [sceneModel]
  367 + M5_Schema:
  368 + type: object
  369 + required: [securityModel]
  370 + M6_Schema:
  371 + type: object
  372 + required: [monitorModel]
  373 + M7_Schema:
  374 + type: object
  375 + required: [qualityModel]
  376 + MetaRule_Schema:
  377 + type: object
  378 + required: [metaRuleModel]
  379 + M8_Schema:
  380 + type: object
  381 + required: [frontModel]
  382 +
  383 + rootAllOntologySchema:
  384 + type: object
  385 + oneOf:
  386 + - {$ref: "#/definitions/M1_Schema"}
  387 + - {$ref: "#/definitions/M2_Schema"}
  388 + - {$ref: "#/definitions/ME_Schema"}
  389 + - {$ref: "#/definitions/M3_Schema"}
  390 + - {$ref: "#/definitions/M4_Schema"}
  391 + - {$ref: "#/definitions/M5_Schema"}
  392 + - {$ref: "#/definitions/M6_Schema"}
  393 + - {$ref: "#/definitions/M7_Schema"}
  394 + - {$ref: "#/definitions/MetaRule_Schema"}
  395 + - {$ref: "#/definitions/M8_Schema"}
  396 +二、M1-domain.yaml(DDD领域聚合静态模型,无修改)
  397 +# M1 DDD聚合领域静态模型
  398 +aggregates:
  399 + # 聚合1:客户聚合 CustomerAggregate
  400 + - aggregateId: CustomerAggregate
  401 + description: 客户主数据聚合,独立事务边界
  402 + aggregateRoot:
  403 + name: Customer
  404 + identifier: customerId
  405 + attributes:
  406 + - name: customerName
  407 + type: string
  408 + - name: contactPhone
  409 + type: string
  410 + - name: registerTime
  411 + type: datetime
  412 + - name: customerLevel
  413 + type: string
  414 + entities: []
  415 + valueObjects: []
  416 +
  417 + # 聚合2:产品聚合 ProductAggregate
  418 + - aggregateId: ProductAggregate
  419 + description: 商品主数据聚合,独立事务边界
  420 + aggregateRoot:
  421 + name: Product
  422 + identifier: productId
  423 + attributes:
  424 + - name: productName
  425 + type: string
  426 + - name: skuCode
  427 + type: string
  428 + - name: category
  429 + type: string
  430 + - name: saleCurrency
  431 + type: string
  432 + - name: salePrice
  433 + type: decimal
  434 + - name: stockNum
  435 + type: int
  436 + - name: isOnSale
  437 + type: boolean
  438 + entities: []
  439 + valueObjects: []
  440 +
  441 + # 聚合3:订单聚合 OrderAggregate
  442 + - aggregateId: OrderAggregate
  443 + description: 订单业务聚合,独立事务边界
  444 + aggregateRoot:
  445 + name: Order
  446 + identifier: orderId
  447 + attributes:
  448 + - name: createTime
  449 + type: datetime
  450 + - name: totalCurrency
  451 + type: string
  452 + - name: totalAmount
  453 + type: decimal
  454 + - name: customerId
  455 + type: string
  456 + refAggregate: CustomerAggregate
  457 + refRoot: Customer
  458 + refIdentifier: customerId
  459 + description: 客户ID,引用客户聚合根主键
  460 + relations:
  461 + - ref: OrderItem
  462 + relationType: composition
  463 + - ref: PaymentTerm
  464 + relationType: composition
  465 + - ref: DeliveryAddressEntity
  466 + relationType: composition
  467 + entities:
  468 + - name: OrderItem
  469 + localIdentifier: itemId
  470 + attributes:
  471 + - name: productId
  472 + type: string
  473 + refAggregate: ProductAggregate
  474 + refRoot: Product
  475 + refIdentifier: productId
  476 + description: 商品ID,引用产品聚合根主键
  477 + - name: quantity
  478 + type: int
  479 + - name: itemCurrency
  480 + type: string
  481 + - name: itemPrice
  482 + type: decimal
  483 + - name: PaymentTerm
  484 + localIdentifier: termId
  485 + attributes:
  486 + - name: paymentType
  487 + type: string
  488 + - name: dueDays
  489 + type: int
  490 + - name: depositRatio
  491 + type: decimal
  492 + - name: settleCurrency
  493 + type: string
  494 + - name: settleAmount
  495 + type: decimal
  496 + - name: DeliveryAddressEntity
  497 + localIdentifier: addrId
  498 + attributes:
  499 + - name: province
  500 + type: string
  501 + - name: city
  502 + type: string
  503 + - name: district
  504 + type: string
  505 + - name: streetDetail
  506 + type: string
  507 + - name: receiverName
  508 + type: string
  509 + - name: receiverPhone
  510 + type: string
  511 + valueObjects: []
  512 +
  513 +globalConstraints:
  514 + - crossAggRefRule: 跨聚合仅允许引用对方【聚合根唯一标识】,禁止引用对方内部实体、值对象
  515 + - transactionBoundary: 一次事务只能修改同一个聚合内对象,跨聚合拆分为多个事务
  516 + - cascadeDelete: 聚合根删除时,级联删除自身内部所有组合子实体
  517 + - foreignKeyStrategy: 业务层做引用校验,默认不创建数据库物理外键;如需物理外键增加 dbForeignKey: true
  518 +三、M2-command.yaml(聚合命令行为模型,无修改)
  519 +# M2 聚合行为命令模型
  520 +behaviors:
  521 + # 1.客户聚合行为
  522 + - aggregateId: CustomerAggregate
  523 + aggregateRoot: Customer
  524 + commands:
  525 + - cmdId: CreateCustomer
  526 + desc: 新增客户主数据
  527 + inputParams:
  528 + - name: customerName
  529 + type: string
  530 + - name: contactPhone
  531 + type: string
  532 + - name: customerLevel
  533 + type: string
  534 + validations:
  535 + - contactPhone非空、手机号格式校验
  536 + - customerLevel取值范围[普通,VIP,高级VIP]
  537 + emitEvents:
  538 + - CustomerCreated
  539 + sideEffect: 仅写入本聚合,不可修改其他聚合
  540 +
  541 + - cmdId: ModifyCustomerInfo
  542 + desc: 修改客户名称、联系方式、等级
  543 + inputParams:
  544 + - name: customerId
  545 + type: string
  546 + - name: customerName
  547 + type: string
  548 + optional: true
  549 + - name: contactPhone
  550 + type: string
  551 + optional: true
  552 + - name: customerLevel
  553 + type: string
  554 + optional: true
  555 + validations:
  556 + - customerId必须存在
  557 + emitEvents:
  558 + - CustomerInfoModified
  559 +
  560 + # 2.产品聚合行为
  561 + - aggregateId: ProductAggregate
  562 + aggregateRoot: Product
  563 + commands:
  564 + - cmdId: CreateProduct
  565 + desc: 新增商品档案
  566 + inputParams:
  567 + - name: productName
  568 + type: string
  569 + - name: skuCode
  570 + type: string
  571 + - name: category
  572 + type: string
  573 + - name: saleCurrency
  574 + type: string
  575 + - name: salePrice
  576 + type: decimal
  577 + - name: stockNum
  578 + type: int
  579 + - name: isOnSale
  580 + type: boolean
  581 + validations:
  582 + - skuCode全局唯一
  583 + - salePrice > 0
  584 + - stockNum >= 0
  585 + emitEvents:
  586 + - ProductCreated
  587 +
  588 + - cmdId: ModifyProductPrice
  589 + desc: 修改售价与币种
  590 + inputParams:
  591 + - name: productId
  592 + type: string
  593 + - name: saleCurrency
  594 + type: string
  595 + optional: true
  596 + - name: salePrice
  597 + type: decimal
  598 + optional: true
  599 + validations:
  600 + - productId存在
  601 + - 修改后售价>0
  602 + emitEvents:
  603 + - ProductPriceChanged
  604 +
  605 + - cmdId: StockDeduct
  606 + desc: 下单扣减库存(订单聚合远程调用)
  607 + inputParams:
  608 + - name: productId
  609 + type: string
  610 + - name: deductQty
  611 + type: int
  612 + validations:
  613 + - 可用库存 >= deductQty
  614 + emitEvents:
  615 + - ProductStockDeducted
  616 +
  617 + # 3.订单聚合行为
  618 + - aggregateId: OrderAggregate
  619 + aggregateRoot: Order
  620 + commands:
  621 + - cmdId: CreateOrder
  622 + desc: 创建订单主单+明细+付款条件+送货地址
  623 + inputParams:
  624 + - name: customerId
  625 + type: string
  626 + - name: totalCurrency
  627 + type: string
  628 + - name: totalAmount
  629 + type: decimal
  630 + - name: items
  631 + type: array
  632 + - name: paymentTerm
  633 + type: object
  634 + - name: deliveryAddress
  635 + type: object
  636 + validations:
  637 + - customerId 存在于CustomerAggregate
  638 + - 所有productId存在于ProductAggregate
  639 + - 订单明细金额累加 ≈ 订单总金额
  640 + - dueDays >= 0, depositRatio ∈ [0,1]
  641 + - 收货手机号格式合法
  642 + bizSteps: "1.构建Order聚合根主记录 2.批量构建OrderItem子实体 3.构建PaymentTerm、DeliveryAddressEntity子实体 4.远程调用Product聚合StockDeduct扣减各商品库存"
  643 + emitEvents:
  644 + - OrderCreated
  645 + - OrderItemAdded
  646 + - OrderPaymentTermSet
  647 + - OrderDeliveryAddressSet
  648 +
  649 + - cmdId: ModifyOrderDeliveryAddress
  650 + desc: 修改订单送货地址子实体
  651 + inputParams:
  652 + - name: orderId
  653 + type: string
  654 + - name: province
  655 + type: string
  656 + optional: true
  657 + - name: city
  658 + type: string
  659 + optional: true
  660 + - name: district
  661 + type: string
  662 + optional: true
  663 + - name: streetDetail
  664 + type: string
  665 + optional: true
  666 + - name: receiverName
  667 + type: string
  668 + optional: true
  669 + - name: receiverPhone
  670 + type: string
  671 + optional: true
  672 + validations:
  673 + - orderId存在
  674 + emitEvents:
  675 + - OrderDeliveryAddressModified
  676 +
  677 + - cmdId: CancelOrder
  678 + desc: 取消订单,归还库存
  679 + inputParams:
  680 + - name: orderId
  681 + type: string
  682 + validations:
  683 + - 订单状态允许取消
  684 + bizSteps: "1.校验订单可取消状态 2.查询订单全部明细商品与数量 3.远程调用Product聚合增加对应库存 4.标记订单作废"
  685 + emitEvents:
  686 + - OrderCancelled
  687 +
  688 +globalBehaviorConstraints:
  689 + - 命令只能操作所属聚合内部对象,跨聚合仅允许调用对方聚合根暴露Command,禁止直接操作对方内部实体
  690 + - 单个命令事务边界仅限当前聚合;跨聚合操作拆为分布式事件最终一致性
  691 + - 子实体只能被所属聚合根命令修改,外部无法直接操作
  692 + - 所有跨聚合依赖仅基于对方聚合根ID做校验,不依赖内部字段
  693 +四、ME-event.yaml(EDA事件驱动模型,无修改)
  694 +eventModel:
  695 + globalConfig:
  696 + idGenerator: snowflake
  697 + bus: rocketmq
  698 + retry: 3
  699 + dlq: topic_msg_dlq
  700 + outbox:
  701 + enabled: true
  702 + table: t_domain_outbox
  703 + pollInterval: 1000ms
  704 + batchSize: 50
  705 + cleanupDays: 7
  706 +
  707 + aggregateEventDefinitions:
  708 + - aggregateId: CustomerAggregate
  709 + events:
  710 + CustomerCreated:
  711 + topic: topic_customer_create
  712 + payload: [customerId,customerName,customerLevel]
  713 + consumers: [customer-service]
  714 + CustomerInfoModified:
  715 + topic: topic_customer_modify
  716 + payload: [customerId,contactPhone,customerLevel]
  717 + consumers: [customer-service]
  718 +
  719 + - aggregateId: ProductAggregate
  720 + events:
  721 + ProductCreated:
  722 + topic: topic_product_create
  723 + payload: [productId,skuCode,salePrice,stockNum]
  724 + consumers: [product-service]
  725 + ProductPriceChanged:
  726 + topic: topic_product_price
  727 + payload: [productId,salePrice,saleCurrency]
  728 + consumers: [order-service]
  729 + ProductStockDeducted:
  730 + topic: topic_stock_deduct
  731 + payload: [productId,stockNum]
  732 + consumers: [order-service]
  733 +
  734 + - aggregateId: OrderAggregate
  735 + events:
  736 + OrderCreated:
  737 + topic: topic_order_create
  738 + payload: [orderId,customerId,totalAmount]
  739 + consumers: [product-service,customer-service]
  740 + OrderDeliveryAddressModified:
  741 + topic: topic_order_addr_modify
  742 + payload: [orderId,receiverPhone]
  743 + consumers: [order-service]
  744 + OrderCancelled:
  745 + topic: topic_order_cancel
  746 + payload: [orderId,productIdList]
  747 + consumers: [product-service]
  748 +
  749 + crossAggConsistencyRules:
  750 + - triggerEvent: OrderCreated
  751 + targetAggregate: ProductAggregate
  752 + targetCommand: StockDeduct
  753 + consistencyType: eventual
  754 + - triggerEvent: OrderCancelled
  755 + targetAggregate: ProductAggregate
  756 + targetCommand: StockDeduct
  757 + consistencyType: eventual
  758 +五、M3-deploy.yaml(服务存储部署映射,无修改)
  759 +deploymentMappings:
  760 + globalConfig:
  761 + dbEngine: mysql8
  762 + defaultSchema: trade_db
  763 + idGen: snowflake
  764 +
  765 + aggregateMappingList:
  766 + - aggregateId: CustomerAggregate
  767 + serviceName: customer-service
  768 + commandApi:
  769 + CreateCustomer: /api/v1/customer/create
  770 + ModifyCustomerInfo: /api/v1/customer/modify
  771 + tableMappings:
  772 + - domainEntity: Customer
  773 + table: t_customer_main
  774 + pk: customer_id
  775 + fieldMap:
  776 + customerId: customer_id
  777 + customerName: cust_name
  778 + contactPhone: phone
  779 + registerTime: register_time
  780 + customerLevel: cust_level
  781 +
  782 + - aggregateId: ProductAggregate
  783 + serviceName: product-service
  784 + commandApi:
  785 + CreateProduct: /api/v1/product/create
  786 + ModifyProductPrice: /api/v1/product/price
  787 + StockDeduct: /api/v1/product/stock/deduct
  788 + tableMappings:
  789 + - domainEntity: Product
  790 + table: t_product_main
  791 + pk: product_id
  792 + fieldMap:
  793 + productId: product_id
  794 + productName: prod_name
  795 + skuCode: sku_code
  796 + category: category
  797 + saleCurrency: currency
  798 + salePrice: sale_price
  799 + stockNum: stock_num
  800 + isOnSale: is_on_sale
  801 +
  802 + - aggregateId: OrderAggregate
  803 + serviceName: order-service
  804 + commandApi:
  805 + CreateOrder: /api/v1/order/create
  806 + ModifyOrderDeliveryAddress: /api/v1/order/addr
  807 + CancelOrder: /api/v1/order/cancel
  808 + tableMappings:
  809 + - domainEntity: Order
  810 + table: t_order_main
  811 + pk: order_id
  812 + fieldMap:
  813 + orderId: order_id
  814 + createTime: create_time
  815 + totalCurrency: total_currency
  816 + totalAmount: total_amt
  817 + customerId: cust_id
  818 + - domainEntity: OrderItem
  819 + table: t_order_item
  820 + pk: item_id
  821 + fieldMap:
  822 + itemId: item_id
  823 + productId: product_id
  824 + quantity: buy_qty
  825 + itemCurrency: item_currency
  826 + itemPrice: item_price
  827 + - domainEntity: PaymentTerm
  828 + table: t_order_payment_term
  829 + pk: term_id
  830 + fieldMap:
  831 + termId: term_id
  832 + paymentType: pay_type
  833 + dueDays: due_days
  834 + depositRatio: deposit_ratio
  835 + settleCurrency: settle_currency
  836 + settleAmount: settle_amt
  837 + - domainEntity: DeliveryAddressEntity
  838 + table: t_order_address
  839 + pk: addr_id
  840 + fieldMap:
  841 + addrId: addr_id
  842 + province: province
  843 + city: city
  844 + district: district
  845 + streetDetail: street
  846 + receiverName: receiver_name
  847 + receiverPhone: receiver_phone
  848 +六、M4-scene.yaml(业务Saga场景编排,无修改)
  849 +sceneModel:
  850 + globalConfig:
  851 + engine: saga
  852 + consistency: eventual
  853 + maxRetry: 2
  854 +
  855 + sceneDefinitions:
  856 + - sceneId: SCENE_CREATE_ORDER
  857 + sceneName: 用户创建订单完整流程
  858 + aggregateScope: [CustomerAggregate,ProductAggregate,OrderAggregate]
  859 + permissionBind: [order_create]
  860 + flowSteps:
  861 + - stepType: readOnlyCheck
  862 + bindAggregate: CustomerAggregate
  863 + - stepType: bindCommand
  864 + bindAggregate: OrderAggregate
  865 + bindCommand: CreateOrder
  866 + - stepType: return
  867 +
  868 + - sceneId: SCENE_MODIFY_ORDER_ADDR
  869 + sceneName: 修改订单收货地址
  870 + aggregateScope: [OrderAggregate]
  871 + permissionBind: [order_modify_addr]
  872 + flowSteps:
  873 + - stepType: bindCommand
  874 + bindAggregate: OrderAggregate
  875 + bindCommand: ModifyOrderDeliveryAddress
  876 + - stepType: return
  877 +
  878 + - sceneId: SCENE_CANCEL_ORDER
  879 + sceneName: 取消订单、归还库存
  880 + aggregateScope: [OrderAggregate,ProductAggregate]
  881 + permissionBind: [order_cancel]
  882 + flowSteps:
  883 + - stepType: bindCommand
  884 + bindAggregate: OrderAggregate
  885 + bindCommand: CancelOrder
  886 + - stepType: return
  887 +七、M5-security.yaml(权限脱敏安全模型,无修改)
  888 +securityModel:
  889 + globalConfig:
  890 + authMode: oauth2_jwt
  891 + auditEnable: true
  892 +
  893 + principals:
  894 + - principalId: normal_user
  895 + desc: C端普通消费者
  896 + - principalId: backend_admin
  897 + desc: 后台运营管理员
  898 +
  899 + functionPermissions:
  900 + - permId: order_create
  901 + bindScene: SCENE_CREATE_ORDER
  902 + allowPrincipals: [normal_user,backend_admin]
  903 + - permId: order_modify_addr
  904 + bindScene: SCENE_MODIFY_ORDER_ADDR
  905 + allowPrincipals: [normal_user,backend_admin]
  906 + - permId: order_cancel
  907 + bindScene: SCENE_CANCEL_ORDER
  908 + allowPrincipals: [normal_user,backend_admin]
  909 +
  910 + maskRules:
  911 + contactPhone: "{first3}****{last4}"
  912 + receiverPhone: "{first3}****{last4}"
  913 +
  914 + encryptRules:
  915 + storageEncrypt: [contactPhone,receiverPhone]
  916 +八、M6-monitor.yaml(全链路监控告警,无修改)
  917 +monitorModel:
  918 + globalConfig:
  919 + traceEngine: skywalking
  920 + metricExport: prometheus
  921 +
  922 + serviceMetricBind:
  923 + - serviceName: customer-service
  924 + metricItems: [qps,rt,error_count]
  925 + - serviceName: product-service
  926 + metricItems: [qps,rt,stock_deduct_fail]
  927 + - serviceName: order-service
  928 + metricItems: [qps,rt,create_fail,cancel_fail]
  929 +
  930 + alertRules:
  931 + - desc: 订单创建失败率大于5%
  932 + level: 严重告警
  933 + trigger: order-service.create_fail / order-service.qps > 0.05
  934 + - desc: 库存扣减失败突增
  935 + level: 警告告警
  936 + trigger: product-service.stock_deduct_fail > 10
  937 +九、M7-sla.yaml(流量管控SLA规范,无修改)
  938 +qualityModel:
  939 + aggregateSla:
  940 + CustomerAggregate:
  941 + availability: 99.95
  942 + ProductAggregate:
  943 + availability: 99.95
  944 + OrderAggregate:
  945 + availability: 99.99
  946 + commandSla:
  947 + CreateOrder:
  948 + RT: 300ms
  949 + StockDeduct:
  950 + RT: 100ms
  951 + sceneSla:
  952 + SCENE_CREATE_ORDER:
  953 + endToEndRT: 600ms
  954 + successRate: 99.9
  955 + flowControl:
  956 + globalLimitQps: 20000
  957 + circuitBreaker:
  958 + enable: true
  959 + failureThreshold: 20
  960 + waitDuration: 3000ms
  961 +十、MetaRule-business.yaml(前端后端统一动态规则引擎,无修改)
  962 +metaRuleModel:
  963 + globalConfig:
  964 + ruleEngine: aviator
  965 + hotUpdate: true
  966 +
  967 + ruleGlobalParams:
  968 + - paramId: single_order_max_amount
  969 + name: 单笔订单最大限额
  970 + dataType: decimal
  971 + defaultValue: 50000
  972 + - paramId: stock_warn_min
  973 + name: 库存预警最低值
  974 + dataType: int
  975 + defaultValue: 20
  976 + - paramId: vip_discount_limit
  977 + name: VIP免风控金额阈值
  978 + dataType: decimal
  979 + defaultValue: 10000
  980 +
  981 + ruleGroups:
  982 + - groupId: ORDER_RISK_RULE
  983 + groupType: 下单风控校验
  984 + bindScene: SCENE_CREATE_ORDER
  985 + rules:
  986 + - ruleId: R_ORDER_AMOUNT_LIMIT
  987 + ruleVersion: 1.0
  988 + ruleName: 大额订单拦截
  989 + matchCondition: totalAmount > #{single_order_max_amount} && customerLevel != "VIP"
  990 + effect: REJECT
  991 + message: 普通用户单笔订单不可超过50000元,请拆分下单
  992 + - ruleId: R_STOCK_LOW_WARN
  993 + ruleVersion: 1.0
  994 + ruleName: 低库存预警
  995 + matchCondition: stockNum < #{stock_warn_min}
  996 + effect: ALERT
  997 + message: 商品库存低于预警值,请及时补货
  998 +
  999 + - groupId: ORDER_COMPENSATE_RULE
  1000 + groupType: 订单取消补偿逻辑
  1001 + bindScene: SCENE_CANCEL_ORDER
  1002 + rules:
  1003 + - ruleId: R_CANCEL_STOCK_RETURN
  1004 + ruleVersion: 1.0
  1005 + ruleName: 取消订单归还库存
  1006 + matchCondition: true
  1007 + effect: COMPENSATE
  1008 + message: 订单取消自动返还锁定商品库存
  1009 +十一、M8-front-schema.yaml(新增核心:React可视化拖拽前端模板建模层)
  1010 +# M8 React前端可视化拖拽页面模板建模层 v2.0
  1011 +# 约束:所有组件字段强制绑定M1领域模型,不允许自定义字段;自动关联M4场景、M2命令、M5脱敏、MetaRule规则
  1012 +frontModel:
  1013 + reactGlobalConfig:
  1014 + uiLib: antd
  1015 + dragEngineStorage: mysql8
  1016 + renderFramework: react18
  1017 + autoGenFormSubmit: true
  1018 + autoBindApiFromScene: true
  1019 + dragRolePermission: [backend_admin]
  1020 + desc: 拖拽编辑器仅管理员可见;普通用户仅渲染页面,不可编辑模板
  1021 +
  1022 + pageTemplates:
  1023 + # 模板1:单实体表单模板 - 客户录入
  1024 + - templateId: template_customer_single
  1025 + templateName: 客户单表单录入模板
  1026 + templateType: single_table_form
  1027 + renderMode: page_container
  1028 + bindSceneId: SCENE_CREATE_CUSTOMER
  1029 + masterAggregate: CustomerAggregate
  1030 + detailAggregates: []
  1031 + rootComponents:
  1032 + - compId: card_customer_base
  1033 + compType: card
  1034 + width: 100%
  1035 + span: 24
  1036 + fieldBind:
  1037 + bindAggregateId: CustomerAggregate
  1038 + bindSourceType: aggregate_root
  1039 + domainFieldName: customerId
  1040 + formLabel: 客户基础信息卡片
  1041 + childComponents:
  1042 + - compId: input_cust_name
  1043 + compType: input
  1044 + span: 12
  1045 + fieldBind:
  1046 + bindAggregateId: CustomerAggregate
  1047 + bindSourceType: aggregate_root
  1048 + domainFieldName: customerName
  1049 + formLabel: 客户名称
  1050 + required: true
  1051 + - compId: input_cust_phone
  1052 + compType: input
  1053 + span: 12
  1054 + fieldBind:
  1055 + bindAggregateId: CustomerAggregate
  1056 + bindSourceType: aggregate_root
  1057 + domainFieldName: contactPhone
  1058 + formLabel: 联系手机号
  1059 + required: true
  1060 + maskField: true
  1061 + - compId: select_cust_level
  1062 + compType: select
  1063 + span: 12
  1064 + fieldBind:
  1065 + bindAggregateId: CustomerAggregate
  1066 + bindSourceType: aggregate_root
  1067 + domainFieldName: customerLevel
  1068 + formLabel: 客户等级
  1069 + required: true
  1070 + - compId: date_register
  1071 + compType: date_picker
  1072 + span: 12
  1073 + fieldBind:
  1074 + bindAggregateId: CustomerAggregate
  1075 + bindSourceType: aggregate_root
  1076 + domainFieldName: registerTime
  1077 + formLabel: 注册时间
  1078 +
  1079 + # 模板2:主从表单模板 - 订单录入(聚合根+多子实体)
  1080 + - templateId: template_order_master_detail
  1081 + templateName: 订单主从录入模板
  1082 + templateType: master_detail_form
  1083 + renderMode: page_container
  1084 + bindSceneId: SCENE_CREATE_ORDER
  1085 + masterAggregate: OrderAggregate
  1086 + detailAggregates: [OrderItem,PaymentTerm,DeliveryAddressEntity]
  1087 + rootComponents:
  1088 + # 订单主信息卡片
  1089 + - compId: card_order_master
  1090 + compType: card
  1091 + width: 100%
  1092 + span: 24
  1093 + fieldBind:
  1094 + bindAggregateId: OrderAggregate
  1095 + bindSourceType: aggregate_root
  1096 + domainFieldName: orderId
  1097 + formLabel: 订单主信息
  1098 + childComponents:
  1099 + - compId: select_customer
  1100 + compType: select
  1101 + span: 12
  1102 + fieldBind:
  1103 + bindAggregateId: CustomerAggregate
  1104 + bindSourceType: aggregate_root
  1105 + domainFieldName: customerId
  1106 + formLabel: 客户
  1107 + required: true
  1108 + - compId: input_currency
  1109 + compType: input
  1110 + span: 12
  1111 + fieldBind:
  1112 + bindAggregateId: OrderAggregate
  1113 + bindSourceType: aggregate_root
  1114 + domainFieldName: totalCurrency
  1115 + formLabel: 结算币种
  1116 + required: true
  1117 + - compId: number_total_amt
  1118 + compType: number_input
  1119 + span: 12
  1120 + fieldBind:
  1121 + bindAggregateId: OrderAggregate
  1122 + bindSourceType: aggregate_root
  1123 + domainFieldName: totalAmount
  1124 + formLabel: 订单总金额
  1125 + required: true
  1126 + - compId: date_create
  1127 + compType: date_picker
  1128 + span: 12
  1129 + fieldBind:
  1130 + bindAggregateId: OrderAggregate
  1131 + bindSourceType: aggregate_root
  1132 + domainFieldName: createTime
  1133 + formLabel: 下单时间
  1134 + # 订单明细表格子实体
  1135 + - compId: table_order_item
  1136 + compType: table
  1137 + width: 100%
  1138 + span: 24
  1139 + fieldBind:
  1140 + bindAggregateId: OrderAggregate
  1141 + bindSourceType: aggregate_entity
  1142 + domainEntityName: OrderItem
  1143 + domainFieldName: itemId
  1144 + formLabel: 订单商品明细
  1145 + childComponents:
  1146 + - compId: select_product
  1147 + compType: select
  1148 + span: 6
  1149 + fieldBind:
  1150 + bindAggregateId: ProductAggregate
  1151 + bindSourceType: aggregate_root
  1152 + domainFieldName: productId
  1153 + formLabel: 商品
  1154 + required: true
  1155 + - compId: number_qty
  1156 + compType: number_input
  1157 + span: 6
  1158 + fieldBind:
  1159 + bindAggregateId: OrderAggregate
  1160 + bindSourceType: aggregate_entity
  1161 + domainEntityName: OrderItem
  1162 + domainFieldName: quantity
  1163 + formLabel: 购买数量
  1164 + required: true
  1165 + - compId: number_price
  1166 + compType: number_input
  1167 + span: 6
  1168 + fieldBind:
  1169 + bindAggregateId: OrderAggregate
  1170 + bindSourceType: aggregate_entity
  1171 + domainEntityName: OrderItem
  1172 + domainFieldName: itemPrice
  1173 + formLabel: 单品单价
  1174 + - compId: input_item_currency
  1175 + compType: input
  1176 + span: 6
  1177 + fieldBind:
  1178 + bindAggregateId: OrderAggregate
  1179 + bindSourceType: aggregate_entity
  1180 + domainEntityName: OrderItem
  1181 + domainFieldName: itemCurrency
  1182 + formLabel: 单品币种
  1183 + # 付款条件子实体卡片
  1184 + - compId: card_payment_term
  1185 + compType: card
  1186 + width: 100%
  1187 + span: 24
  1188 + fieldBind:
  1189 + bindAggregateId: OrderAggregate
  1190 + bindSourceType: aggregate_entity
  1191 + domainEntityName: PaymentTerm
  1192 + domainFieldName: termId
  1193 + formLabel: 付款条款
  1194 + childComponents:
  1195 + - compId: select_pay_type
  1196 + compType: select
  1197 + span: 12
  1198 + fieldBind:
  1199 + bindAggregateId: OrderAggregate
  1200 + bindSourceType: aggregate_entity
  1201 + domainEntityName: PaymentTerm
  1202 + domainFieldName: paymentType
  1203 + formLabel: 付款方式
  1204 + - compId: number_duedays
  1205 + compType: number_input
  1206 + span: 12
  1207 + fieldBind:
  1208 + bindAggregateId: OrderAggregate
  1209 + bindSourceType: aggregate_entity
  1210 + domainEntityName: PaymentTerm
  1211 + domainFieldName: dueDays
  1212 + formLabel: 账期天数
  1213 + # 收货地址子实体卡片
  1214 + - compId: card_delivery_addr
  1215 + compType: card
  1216 + width: 100%
  1217 + span: 24
  1218 + fieldBind:
  1219 + bindAggregateId: OrderAggregate
  1220 + bindSourceType: aggregate_entity
  1221 + domainEntityName: DeliveryAddressEntity
  1222 + domainFieldName: addrId
  1223 + formLabel: 收货地址
  1224 + childComponents:
  1225 + - compId: input_province
  1226 + compType: input
  1227 + span: 6
  1228 + fieldBind:
  1229 + bindAggregateId: OrderAggregate
  1230 + bindSourceType: aggregate_entity
  1231 + domainEntityName: DeliveryAddressEntity
  1232 + domainFieldName: province
  1233 + formLabel: 省份
  1234 + - compId: input_city
  1235 + compType: input
  1236 + span: 6
  1237 + fieldBind:
  1238 + bindAggregateId: OrderAggregate
  1239 + bindSourceType: aggregate_entity
  1240 + domainEntityName: DeliveryAddressEntity
  1241 + domainFieldName: city
  1242 + formLabel: 城市
  1243 + - compId: input_district
  1244 + compType: input
  1245 + span: 6
  1246 + fieldBind:
  1247 + bindAggregateId: OrderAggregate
  1248 + bindSourceType: aggregate_entity
  1249 + domainEntityName: DeliveryAddressEntity
  1250 + domainFieldName: district
  1251 + formLabel: 区县
  1252 + - compId: input_street
  1253 + compType: input
  1254 + span: 6
  1255 + fieldBind:
  1256 + bindAggregateId: OrderAggregate
  1257 + bindSourceType: aggregate_entity
  1258 + domainEntityName: DeliveryAddressEntity
  1259 + domainFieldName: streetDetail
  1260 + formLabel: 详细街道
  1261 + - compId: input_receiver
  1262 + compType: input
  1263 + span: 12
  1264 + fieldBind:
  1265 + bindAggregateId: OrderAggregate
  1266 + bindSourceType: aggregate_entity
  1267 + domainEntityName: DeliveryAddressEntity
  1268 + domainFieldName: receiverName
  1269 + formLabel: 收货人
  1270 + - compId: input_receiver_phone
  1271 + compType: input
  1272 + span: 12
  1273 + fieldBind:
  1274 + bindAggregateId: OrderAggregate
  1275 + bindSourceType: aggregate_entity
  1276 + domainEntityName: DeliveryAddressEntity
  1277 + domainFieldName: receiverPhone
  1278 + formLabel: 收货电话
  1279 + maskField: true
  1280 +
  1281 + # 模板3:空白自定义拖拽模板(全新业务场景使用)
  1282 + - templateId: template_custom_blank
  1283 + templateName: 空白自定义拖拽模板
  1284 + templateType: custom_drag_form
  1285 + renderMode: drawer
  1286 + bindSceneId: ""
  1287 + masterAggregate: null
  1288 + detailAggregates: []
  1289 + rootComponents: []
  1290 +完整架构分层定位总总结
  1291 +
  1292 +全套11层分层职责
  1293 +
  1294 +1. M0-meta-schema:全域本体校验根规范,新增React前端模板JSON Schema约束,CI流水线校验所有YAML语法合法;统一前后端数据、组件、页面描述标准。
  1295 +
  1296 +2. M1-domain:DDD领域聚合唯一数据源,前端所有拖拽组件强制绑定本层字段,禁止自定义字段,保证前后端数据口径完全统一。
  1297 +
  1298 +3. M2-command:聚合操作命令入参定义,React页面提交时自动映射表单组件值至命令参数,无硬编码接口参数。
  1299 +
  1300 +4. ME-event:EDA事件总线定义,页面提交完成后监听领域事件,自动刷新页面、弹窗反馈、触发异步补偿逻辑。
  1301 +
  1302 +5. M3-deploy:服务与数据库映射,自动关联页面场景对应的后端HTTP接口地址,前端无硬编码URL。
  1303 +
  1304 +6. M4-scene:Saga业务流程编排,M8页面模板绑定SceneId,一套模板对应完整端到端业务流程。
  1305 +
  1306 +7. M5-security:权限、脱敏、数据加密规范;区分模板编辑管理员权限、页面访问权限、敏感字段掩码渲染规则,前端渲染引擎自动读取执行。
  1307 +
  1308 +8. M6-monitor:全链路监控指标,前端页面加载、表单提交、接口异常自动埋点,匹配后端告警规则。
  1309 +
  1310 +9. M7-sla:接口流量、熔断、超时规范,前端请求层统一复用SLA限流配置。
  1311 +
  1312 +10. MetaRule-business:Aviator统一动态规则,前端渲染引擎实时执行,控制组件显隐、禁用、输入拦截、预警弹窗;后端执行持久化校验,规则仅维护一份。
  1313 +
  1314 +11. M8-front-schema(新增):React18+Ant Design可视化拖拽建模层,业务专家无代码操作;内置单表单/主从表单/空白模板三类模板;模板持久化MySQL;自动联动上游全部M层元数据;业务迭代仅修改模板yaml,无需修改React底层业务代码,满足后期专业人员独立维护模板的核心需求。
  1315 +
  1316 +业务落地闭环流程
  1317 +
  1318 +1. 架构工程师维护M0-M7、MetaRule底层元模型(一次性开发,极少变更);
  1319 +
  1320 +2. 业务建模/专业维护人员仅操作M8-front-schema模板:可视化拖拽绑定M1字段生成页面;
  1321 +
  1322 +3. 模板绑定M4场景后,系统自动关联M2命令API、M5脱敏权限、MetaRule动态规则;
  1323 +
  1324 +4. 前端React通用渲染引擎读取M8模板动态生成页面,无业务硬编码;
  1325 +
  1326 +5. 业务需求变更:仅调整M8模板组件配置,无需发布前端JS/TS源码,支持配置中心热更新。
... ...
start.sh 0 → 100755
  1 +++ a/start.sh
  1 +#!/usr/bin/env bash
  2 +# 一键启动:后端元数据引擎(8080) + 前端渲染引擎(5173/自动顺延)
  3 +# 用法: ./start.sh
  4 +set -euo pipefail
  5 +ROOT="$(cd "$(dirname "$0")" && pwd)"
  6 +
  7 +echo "==> 环境检查(Maven 由 ./mvnw 包装器自动下载,无需预装)"
  8 +command -v java >/dev/null || { echo "缺少 Java (建议 21+)"; exit 1; }
  9 +command -v node >/dev/null || { echo "缺少 Node.js (建议 18+)"; exit 1; }
  10 +command -v npm >/dev/null || { echo "缺少 npm"; exit 1; }
  11 +
  12 +ENGINE_LOG="$ROOT/.engine.log"
  13 +echo "==> 启动后端引擎 (日志: $ENGINE_LOG;首次会下载 Maven 与依赖)"
  14 +( cd "$ROOT/engine" && ./mvnw -q -DskipTests spring-boot:run > "$ENGINE_LOG" 2>&1 ) &
  15 +ENGINE_PID=$!
  16 +
  17 +cleanup() {
  18 + echo; echo "==> 停止服务"
  19 + kill "$ENGINE_PID" 2>/dev/null || true
  20 + # 兜底:杀掉占用 8080 的子进程
  21 + if command -v lsof >/dev/null; then
  22 + PIDS=$(lsof -ti tcp:8080 2>/dev/null || true); [ -n "$PIDS" ] && kill $PIDS 2>/dev/null || true
  23 + fi
  24 +}
  25 +trap cleanup EXIT INT TERM
  26 +
  27 +echo "==> 等待引擎就绪 (http://localhost:8080)"
  28 +for i in $(seq 1 60); do
  29 + if curl -s -o /dev/null http://localhost:8080/api/meta/models 2>/dev/null; then
  30 + echo " 引擎已就绪 ✔"; break
  31 + fi
  32 + sleep 2
  33 + [ "$i" = "60" ] && { echo "引擎启动超时,请查看 $ENGINE_LOG"; exit 1; }
  34 +done
  35 +
  36 +echo "==> 安装前端依赖 (首次)"
  37 +( cd "$ROOT/web" && [ -d node_modules ] || npm install )
  38 +
  39 +echo "==> 启动前端 (Vite)。浏览器打开提示的 Local 地址,进入「新增订单」页。"
  40 +echo " 后端: http://localhost:8080 H2 控制台: http://localhost:8080/h2-console"
  41 +cd "$ROOT/web" && npm run dev
... ...
templates/README.md 0 → 100644
  1 +++ a/templates/README.md
  1 +# 代码生成模板
  2 +
  3 +本引擎默认采用**运行时解释**模型(改 YAML 即改行为,无需生成代码)。当团队更偏好**静态生成**时,可用下列模板由模型一次性生成源码(Mustache 风格占位符,配合任意模板引擎/脚本渲染)。
  4 +
  5 +| 模板 | 输入模型 | 产物 |
  6 +|---|---|---|
  7 +| `ddl.sql.mustache` | M3 tableMappings + M1 类型 | 建表 DDL |
  8 +| `react-page.tsx.mustache` | M8 pageTemplate + M1 | 静态 React 录入页 |
  9 +| `command-handler.java.mustache` | M2 command + M4 scene | 命令处理器骨架 |
  10 +
  11 +渲染入参对象可由 `GET /api/meta/model/{code}` 拿到的解析结果拼装。两条路线(解释/生成)共享同一套 `models/`,可按团队工程习惯二选一或混用。
... ...
templates/command-handler.java.mustache 0 → 100644
  1 +++ a/templates/command-handler.java.mustache
  1 +// 由 M2-command.yaml (command) + M4-scene.yaml + ME-event.yaml 生成的命令处理器骨架
  2 +// 渲染上下文:{ aggregateId, cmdId, inputParams:[{name,type,optional}], validations:[...], emitEvents:[...] }
  3 +package com.onto.generated.{{aggregateLower}};
  4 +
  5 +import org.springframework.stereotype.Service;
  6 +
  7 +@Service
  8 +public class {{cmdId}}Handler {
  9 +
  10 + /** {{desc}} */
  11 + public {{cmdId}}Result handle({{cmdId}}Command cmd) {
  12 + // 1. 校验(源自 M2 validations)
  13 + {{#validations}}
  14 + // - {{.}}
  15 + {{/validations}}
  16 +
  17 + // 2. 业务步骤(源自 M2 bizSteps):{{bizSteps}}
  18 +
  19 + // 3. 发出领域事件(源自 M2 emitEvents / ME 定义)
  20 + {{#emitEvents}}
  21 + // emit {{.}}
  22 + {{/emitEvents}}
  23 + return new {{cmdId}}Result();
  24 + }
  25 +
  26 + public record {{cmdId}}Command({{#inputParams}}{{javaType}} {{name}}{{^last}}, {{/last}}{{/inputParams}}) {}
  27 + public record {{cmdId}}Result() {}
  28 +}
... ...
templates/ddl.sql.mustache 0 → 100644
  1 +++ a/templates/ddl.sql.mustache
  1 +-- 由 M3-deploy.yaml (tableMappings) + M1-domain.yaml (字段类型) 生成
  2 +-- 渲染上下文:{ aggregates: [ { tables: [ { table, pk, columns:[{col,type,isPk}], parentFk } ] } ] }
  3 +{{#aggregates}}
  4 +{{#tables}}
  5 +CREATE TABLE IF NOT EXISTS {{table}} (
  6 +{{#columns}}
  7 + {{col}} {{type}}{{#isPk}} PRIMARY KEY{{/isPk}},
  8 +{{/columns}}
  9 +{{#parentFk}}
  10 + {{parentFk}} VARCHAR(64), -- 组合子实体父外键(据 M1 composition)
  11 +{{/parentFk}}
  12 + _row_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  13 +);
  14 +{{/tables}}
  15 +{{/aggregates}}
... ...
templates/react-page.tsx.mustache 0 → 100644
  1 +++ a/templates/react-page.tsx.mustache
  1 +// 由 M8-front-schema.yaml (pageTemplate) + M1 字段类型生成的静态录入页骨架
  2 +// 渲染上下文:{ templateName, sceneId, commandApi, masterFields:[...], detailEntities:[{name, fields:[...]}] }
  3 +import { Form, Input, InputNumber, DatePicker, Select, Card, Button } from 'antd'
  4 +
  5 +export default function {{pascalTemplateId}}Page() {
  6 + const [form] = Form.useForm()
  7 + const submit = async () => {
  8 + const values = await form.validateFields()
  9 + await fetch('{{commandApi}}', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(values) })
  10 + }
  11 + return (
  12 + <Form form={form} layout="vertical">
  13 + <Card title="主信息">
  14 + {{#masterFields}}
  15 + <Form.Item name="{{field}}" label="{{label}}"{{#required}} rules={[{ required: true }]}{{/required}}>
  16 + {{control}}
  17 + </Form.Item>
  18 + {{/masterFields}}
  19 + </Card>
  20 + {{#detailEntities}}
  21 + <Card title="{{name}}(子实体)">
  22 + {{#fields}}
  23 + <Form.Item name={['{{../name}}', '{{field}}']} label="{{label}}">{{control}}</Form.Item>
  24 + {{/fields}}
  25 + </Card>
  26 + {{/detailEntities}}
  27 + <Button type="primary" onClick={submit}>提交</Button>
  28 + </Form>
  29 + )
  30 +}
... ...
tests/verify-model-change.sh 0 → 100755
  1 +++ a/tests/verify-model-change.sh
  1 +#!/usr/bin/env bash
  2 +# 验证"改了模型文件 → 执行的操作定义随之改变"(针对 MetaRule 阈值)。
  3 +# 前置:后端已在 8080 运行(./start.sh 或 cd engine && mvn spring-boot:run)。
  4 +set -euo pipefail
  5 +ROOT="$(cd "$(dirname "$0")/.." && pwd)"
  6 +BASE="http://localhost:8080"
  7 +MODEL="$ROOT/models/MetaRule-business.yaml"
  8 +
  9 +# 明细 1 件 × 7997 → 引擎服务端重算 totalAmount=7997(无视 master 传入值,防篡改)
  10 +ORDER='{"master":{"customerId":"C001","totalCurrency":"CNY","createTime":"2026-07-20T10:00:00"},
  11 + "details":{"OrderItem":[{"productId":"P003","quantity":1,"itemPrice":7997,"itemCurrency":"CNY"}],
  12 + "PaymentTerm":[{"paymentType":"现金","dueDays":0}],
  13 + "DeliveryAddressEntity":[{"province":"广东","city":"深圳","district":"南山","streetDetail":"x","receiverName":"张三","receiverPhone":"13800001111"}]}}'
  14 +
  15 +say() { printf "\n\033[1m%s\033[0m\n" "$1"; }
  16 +exec_order() { curl -s -X POST "$BASE/api/scene/SCENE_CREATE_ORDER/execute" -H 'Content-Type: application/json' -d "$ORDER"; }
  17 +field() { python3 -c "import sys,json;d=json.load(sys.stdin);print(d.get('$1'))"; }
  18 +
  19 +curl -s -o /dev/null "$BASE/api/meta/models" || { echo "后端未就绪,请先启动 8080"; exit 1; }
  20 +
  21 +say "A) 改动前:普通用户 C001,金额 7997 (< 50000)"
  22 +echo "$ORDER" | exec_order </dev/null >/dev/null 2>&1 || true
  23 +R=$(exec_order); echo " success=$(echo "$R" | field success)"
  24 +
  25 +say "B) 仅改模型:single_order_max_amount 50000 -> 100(无代码改动)"
  26 +perl -0pi -e 's/defaultValue: 50000/defaultValue: 100/' "$MODEL"
  27 +curl -s -X POST "$BASE/api/meta/reload" -H 'X-Principal: backend_admin' >/dev/null; echo " 已热重载"
  28 +
  29 +say "C) 改动后:同一笔订单 (7997 > 100) 应被 REJECT"
  30 +R=$(exec_order)
  31 +echo " success=$(echo "$R" | field success) stage=$(echo "$R" | field stage) message=$(echo "$R" | field message)"
  32 +
  33 +say "D) 恢复模型 100 -> 50000 并重载"
  34 +perl -0pi -e 's/defaultValue: 100\b/defaultValue: 50000/' "$MODEL"
  35 +curl -s -X POST "$BASE/api/meta/reload" -H 'X-Principal: backend_admin' >/dev/null; echo " 已恢复"
  36 +
  37 +say "结论:同一请求、仅改 YAML,结果从『通过』变为『拦截』——行为源于模型而非代码。"
... ...
web/index.html 0 → 100644
  1 +++ a/web/index.html
  1 +<!doctype html>
  2 +<html lang="zh-CN">
  3 + <head>
  4 + <meta charset="UTF-8" />
  5 + <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  6 + <title>本体驱动 · 元数据引擎 Demo</title>
  7 + </head>
  8 + <body>
  9 + <div id="root"></div>
  10 + <script type="module" src="/src/main.tsx"></script>
  11 + </body>
  12 +</html>
... ...
web/package-lock.json 0 → 100644
  1 +++ a/web/package-lock.json
  1 +{
  2 + "name": "onto-web",
  3 + "version": "2.0.0",
  4 + "lockfileVersion": 3,
  5 + "requires": true,
  6 + "packages": {
  7 + "": {
  8 + "name": "onto-web",
  9 + "version": "2.0.0",
  10 + "dependencies": {
  11 + "@ant-design/icons": "^5.5.1",
  12 + "antd": "^5.21.6",
  13 + "dayjs": "^1.11.13",
  14 + "react": "^18.3.1",
  15 + "react-dom": "^18.3.1"
  16 + },
  17 + "devDependencies": {
  18 + "@types/react": "^18.3.12",
  19 + "@types/react-dom": "^18.3.1",
  20 + "@vitejs/plugin-react": "^4.3.3",
  21 + "typescript": "^5.6.3",
  22 + "vite": "^5.4.10"
  23 + }
  24 + },
  25 + "node_modules/@ant-design/colors": {
  26 + "version": "7.2.1",
  27 + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.1.tgz",
  28 + "integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==",
  29 + "license": "MIT",
  30 + "dependencies": {
  31 + "@ant-design/fast-color": "^2.0.6"
  32 + }
  33 + },
  34 + "node_modules/@ant-design/cssinjs": {
  35 + "version": "1.24.0",
  36 + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.24.0.tgz",
  37 + "integrity": "sha512-K4cYrJBsgvL+IoozUXYjbT6LHHNt+19a9zkvpBPxLjFHas1UpPM2A5MlhROb0BT8N8WoavM5VsP9MeSeNK/3mg==",
  38 + "license": "MIT",
  39 + "dependencies": {
  40 + "@babel/runtime": "^7.11.1",
  41 + "@emotion/hash": "^0.8.0",
  42 + "@emotion/unitless": "^0.7.5",
  43 + "classnames": "^2.3.1",
  44 + "csstype": "^3.1.3",
  45 + "rc-util": "^5.35.0",
  46 + "stylis": "^4.3.4"
  47 + },
  48 + "peerDependencies": {
  49 + "react": ">=16.0.0",
  50 + "react-dom": ">=16.0.0"
  51 + }
  52 + },
  53 + "node_modules/@ant-design/cssinjs-utils": {
  54 + "version": "1.1.3",
  55 + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.3.tgz",
  56 + "integrity": "sha512-nOoQMLW1l+xR1Co8NFVYiP8pZp3VjIIzqV6D6ShYF2ljtdwWJn5WSsH+7kvCktXL/yhEtWURKOfH5Xz/gzlwsg==",
  57 + "license": "MIT",
  58 + "dependencies": {
  59 + "@ant-design/cssinjs": "^1.21.0",
  60 + "@babel/runtime": "^7.23.2",
  61 + "rc-util": "^5.38.0"
  62 + },
  63 + "peerDependencies": {
  64 + "react": ">=16.9.0",
  65 + "react-dom": ">=16.9.0"
  66 + }
  67 + },
  68 + "node_modules/@ant-design/fast-color": {
  69 + "version": "2.0.6",
  70 + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz",
  71 + "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==",
  72 + "license": "MIT",
  73 + "dependencies": {
  74 + "@babel/runtime": "^7.24.7"
  75 + },
  76 + "engines": {
  77 + "node": ">=8.x"
  78 + }
  79 + },
  80 + "node_modules/@ant-design/icons": {
  81 + "version": "5.6.1",
  82 + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz",
  83 + "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==",
  84 + "license": "MIT",
  85 + "dependencies": {
  86 + "@ant-design/colors": "^7.0.0",
  87 + "@ant-design/icons-svg": "^4.4.0",
  88 + "@babel/runtime": "^7.24.8",
  89 + "classnames": "^2.2.6",
  90 + "rc-util": "^5.31.1"
  91 + },
  92 + "engines": {
  93 + "node": ">=8"
  94 + },
  95 + "peerDependencies": {
  96 + "react": ">=16.0.0",
  97 + "react-dom": ">=16.0.0"
  98 + }
  99 + },
  100 + "node_modules/@ant-design/icons-svg": {
  101 + "version": "4.5.0",
  102 + "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.5.0.tgz",
  103 + "integrity": "sha512-1BTUFyKPTBZ53MuTP8s0k5SFEXL7o3VHEOwLgzaoWKwnBeqIcqUtVshc4SKzhI6uACfqhJqBwBUE9FsWR3uULA==",
  104 + "license": "MIT"
  105 + },
  106 + "node_modules/@ant-design/react-slick": {
  107 + "version": "1.1.2",
  108 + "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.1.2.tgz",
  109 + "integrity": "sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA==",
  110 + "license": "MIT",
  111 + "dependencies": {
  112 + "@babel/runtime": "^7.10.4",
  113 + "classnames": "^2.2.5",
  114 + "json2mq": "^0.2.0",
  115 + "resize-observer-polyfill": "^1.5.1",
  116 + "throttle-debounce": "^5.0.0"
  117 + },
  118 + "peerDependencies": {
  119 + "react": ">=16.9.0"
  120 + }
  121 + },
  122 + "node_modules/@babel/code-frame": {
  123 + "version": "7.29.7",
  124 + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
  125 + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
  126 + "dev": true,
  127 + "license": "MIT",
  128 + "dependencies": {
  129 + "@babel/helper-validator-identifier": "^7.29.7",
  130 + "js-tokens": "^4.0.0",
  131 + "picocolors": "^1.1.1"
  132 + },
  133 + "engines": {
  134 + "node": ">=6.9.0"
  135 + }
  136 + },
  137 + "node_modules/@babel/compat-data": {
  138 + "version": "7.29.7",
  139 + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
  140 + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
  141 + "dev": true,
  142 + "license": "MIT",
  143 + "engines": {
  144 + "node": ">=6.9.0"
  145 + }
  146 + },
  147 + "node_modules/@babel/core": {
  148 + "version": "7.29.7",
  149 + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
  150 + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
  151 + "dev": true,
  152 + "license": "MIT",
  153 + "dependencies": {
  154 + "@babel/code-frame": "^7.29.7",
  155 + "@babel/generator": "^7.29.7",
  156 + "@babel/helper-compilation-targets": "^7.29.7",
  157 + "@babel/helper-module-transforms": "^7.29.7",
  158 + "@babel/helpers": "^7.29.7",
  159 + "@babel/parser": "^7.29.7",
  160 + "@babel/template": "^7.29.7",
  161 + "@babel/traverse": "^7.29.7",
  162 + "@babel/types": "^7.29.7",
  163 + "@jridgewell/remapping": "^2.3.5",
  164 + "convert-source-map": "^2.0.0",
  165 + "debug": "^4.1.0",
  166 + "gensync": "^1.0.0-beta.2",
  167 + "json5": "^2.2.3",
  168 + "semver": "^6.3.1"
  169 + },
  170 + "engines": {
  171 + "node": ">=6.9.0"
  172 + },
  173 + "funding": {
  174 + "type": "opencollective",
  175 + "url": "https://opencollective.com/babel"
  176 + }
  177 + },
  178 + "node_modules/@babel/generator": {
  179 + "version": "7.29.7",
  180 + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
  181 + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
  182 + "dev": true,
  183 + "license": "MIT",
  184 + "dependencies": {
  185 + "@babel/parser": "^7.29.7",
  186 + "@babel/types": "^7.29.7",
  187 + "@jridgewell/gen-mapping": "^0.3.12",
  188 + "@jridgewell/trace-mapping": "^0.3.28",
  189 + "jsesc": "^3.0.2"
  190 + },
  191 + "engines": {
  192 + "node": ">=6.9.0"
  193 + }
  194 + },
  195 + "node_modules/@babel/helper-compilation-targets": {
  196 + "version": "7.29.7",
  197 + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
  198 + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
  199 + "dev": true,
  200 + "license": "MIT",
  201 + "dependencies": {
  202 + "@babel/compat-data": "^7.29.7",
  203 + "@babel/helper-validator-option": "^7.29.7",
  204 + "browserslist": "^4.24.0",
  205 + "lru-cache": "^5.1.1",
  206 + "semver": "^6.3.1"
  207 + },
  208 + "engines": {
  209 + "node": ">=6.9.0"
  210 + }
  211 + },
  212 + "node_modules/@babel/helper-globals": {
  213 + "version": "7.29.7",
  214 + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
  215 + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
  216 + "dev": true,
  217 + "license": "MIT",
  218 + "engines": {
  219 + "node": ">=6.9.0"
  220 + }
  221 + },
  222 + "node_modules/@babel/helper-module-imports": {
  223 + "version": "7.29.7",
  224 + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
  225 + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
  226 + "dev": true,
  227 + "license": "MIT",
  228 + "dependencies": {
  229 + "@babel/traverse": "^7.29.7",
  230 + "@babel/types": "^7.29.7"
  231 + },
  232 + "engines": {
  233 + "node": ">=6.9.0"
  234 + }
  235 + },
  236 + "node_modules/@babel/helper-module-transforms": {
  237 + "version": "7.29.7",
  238 + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
  239 + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
  240 + "dev": true,
  241 + "license": "MIT",
  242 + "dependencies": {
  243 + "@babel/helper-module-imports": "^7.29.7",
  244 + "@babel/helper-validator-identifier": "^7.29.7",
  245 + "@babel/traverse": "^7.29.7"
  246 + },
  247 + "engines": {
  248 + "node": ">=6.9.0"
  249 + },
  250 + "peerDependencies": {
  251 + "@babel/core": "^7.0.0"
  252 + }
  253 + },
  254 + "node_modules/@babel/helper-plugin-utils": {
  255 + "version": "7.29.7",
  256 + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
  257 + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
  258 + "dev": true,
  259 + "license": "MIT",
  260 + "engines": {
  261 + "node": ">=6.9.0"
  262 + }
  263 + },
  264 + "node_modules/@babel/helper-string-parser": {
  265 + "version": "7.29.7",
  266 + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
  267 + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
  268 + "dev": true,
  269 + "license": "MIT",
  270 + "engines": {
  271 + "node": ">=6.9.0"
  272 + }
  273 + },
  274 + "node_modules/@babel/helper-validator-identifier": {
  275 + "version": "7.29.7",
  276 + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
  277 + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
  278 + "dev": true,
  279 + "license": "MIT",
  280 + "engines": {
  281 + "node": ">=6.9.0"
  282 + }
  283 + },
  284 + "node_modules/@babel/helper-validator-option": {
  285 + "version": "7.29.7",
  286 + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
  287 + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
  288 + "dev": true,
  289 + "license": "MIT",
  290 + "engines": {
  291 + "node": ">=6.9.0"
  292 + }
  293 + },
  294 + "node_modules/@babel/helpers": {
  295 + "version": "7.29.7",
  296 + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
  297 + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
  298 + "dev": true,
  299 + "license": "MIT",
  300 + "dependencies": {
  301 + "@babel/template": "^7.29.7",
  302 + "@babel/types": "^7.29.7"
  303 + },
  304 + "engines": {
  305 + "node": ">=6.9.0"
  306 + }
  307 + },
  308 + "node_modules/@babel/parser": {
  309 + "version": "7.29.7",
  310 + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
  311 + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
  312 + "dev": true,
  313 + "license": "MIT",
  314 + "dependencies": {
  315 + "@babel/types": "^7.29.7"
  316 + },
  317 + "bin": {
  318 + "parser": "bin/babel-parser.js"
  319 + },
  320 + "engines": {
  321 + "node": ">=6.0.0"
  322 + }
  323 + },
  324 + "node_modules/@babel/plugin-transform-react-jsx-self": {
  325 + "version": "7.29.7",
  326 + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
  327 + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
  328 + "dev": true,
  329 + "license": "MIT",
  330 + "dependencies": {
  331 + "@babel/helper-plugin-utils": "^7.29.7"
  332 + },
  333 + "engines": {
  334 + "node": ">=6.9.0"
  335 + },
  336 + "peerDependencies": {
  337 + "@babel/core": "^7.0.0-0"
  338 + }
  339 + },
  340 + "node_modules/@babel/plugin-transform-react-jsx-source": {
  341 + "version": "7.29.7",
  342 + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
  343 + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
  344 + "dev": true,
  345 + "license": "MIT",
  346 + "dependencies": {
  347 + "@babel/helper-plugin-utils": "^7.29.7"
  348 + },
  349 + "engines": {
  350 + "node": ">=6.9.0"
  351 + },
  352 + "peerDependencies": {
  353 + "@babel/core": "^7.0.0-0"
  354 + }
  355 + },
  356 + "node_modules/@babel/runtime": {
  357 + "version": "7.29.7",
  358 + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
  359 + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
  360 + "license": "MIT",
  361 + "engines": {
  362 + "node": ">=6.9.0"
  363 + }
  364 + },
  365 + "node_modules/@babel/template": {
  366 + "version": "7.29.7",
  367 + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
  368 + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
  369 + "dev": true,
  370 + "license": "MIT",
  371 + "dependencies": {
  372 + "@babel/code-frame": "^7.29.7",
  373 + "@babel/parser": "^7.29.7",
  374 + "@babel/types": "^7.29.7"
  375 + },
  376 + "engines": {
  377 + "node": ">=6.9.0"
  378 + }
  379 + },
  380 + "node_modules/@babel/traverse": {
  381 + "version": "7.29.7",
  382 + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
  383 + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
  384 + "dev": true,
  385 + "license": "MIT",
  386 + "dependencies": {
  387 + "@babel/code-frame": "^7.29.7",
  388 + "@babel/generator": "^7.29.7",
  389 + "@babel/helper-globals": "^7.29.7",
  390 + "@babel/parser": "^7.29.7",
  391 + "@babel/template": "^7.29.7",
  392 + "@babel/types": "^7.29.7",
  393 + "debug": "^4.3.1"
  394 + },
  395 + "engines": {
  396 + "node": ">=6.9.0"
  397 + }
  398 + },
  399 + "node_modules/@babel/types": {
  400 + "version": "7.29.7",
  401 + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
  402 + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
  403 + "dev": true,
  404 + "license": "MIT",
  405 + "dependencies": {
  406 + "@babel/helper-string-parser": "^7.29.7",
  407 + "@babel/helper-validator-identifier": "^7.29.7"
  408 + },
  409 + "engines": {
  410 + "node": ">=6.9.0"
  411 + }
  412 + },
  413 + "node_modules/@emotion/hash": {
  414 + "version": "0.8.0",
  415 + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
  416 + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==",
  417 + "license": "MIT"
  418 + },
  419 + "node_modules/@emotion/unitless": {
  420 + "version": "0.7.5",
  421 + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
  422 + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==",
  423 + "license": "MIT"
  424 + },
  425 + "node_modules/@esbuild/aix-ppc64": {
  426 + "version": "0.21.5",
  427 + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
  428 + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
  429 + "cpu": [
  430 + "ppc64"
  431 + ],
  432 + "dev": true,
  433 + "license": "MIT",
  434 + "optional": true,
  435 + "os": [
  436 + "aix"
  437 + ],
  438 + "engines": {
  439 + "node": ">=12"
  440 + }
  441 + },
  442 + "node_modules/@esbuild/android-arm": {
  443 + "version": "0.21.5",
  444 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
  445 + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
  446 + "cpu": [
  447 + "arm"
  448 + ],
  449 + "dev": true,
  450 + "license": "MIT",
  451 + "optional": true,
  452 + "os": [
  453 + "android"
  454 + ],
  455 + "engines": {
  456 + "node": ">=12"
  457 + }
  458 + },
  459 + "node_modules/@esbuild/android-arm64": {
  460 + "version": "0.21.5",
  461 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
  462 + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
  463 + "cpu": [
  464 + "arm64"
  465 + ],
  466 + "dev": true,
  467 + "license": "MIT",
  468 + "optional": true,
  469 + "os": [
  470 + "android"
  471 + ],
  472 + "engines": {
  473 + "node": ">=12"
  474 + }
  475 + },
  476 + "node_modules/@esbuild/android-x64": {
  477 + "version": "0.21.5",
  478 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
  479 + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
  480 + "cpu": [
  481 + "x64"
  482 + ],
  483 + "dev": true,
  484 + "license": "MIT",
  485 + "optional": true,
  486 + "os": [
  487 + "android"
  488 + ],
  489 + "engines": {
  490 + "node": ">=12"
  491 + }
  492 + },
  493 + "node_modules/@esbuild/darwin-arm64": {
  494 + "version": "0.21.5",
  495 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
  496 + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
  497 + "cpu": [
  498 + "arm64"
  499 + ],
  500 + "dev": true,
  501 + "license": "MIT",
  502 + "optional": true,
  503 + "os": [
  504 + "darwin"
  505 + ],
  506 + "engines": {
  507 + "node": ">=12"
  508 + }
  509 + },
  510 + "node_modules/@esbuild/darwin-x64": {
  511 + "version": "0.21.5",
  512 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
  513 + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
  514 + "cpu": [
  515 + "x64"
  516 + ],
  517 + "dev": true,
  518 + "license": "MIT",
  519 + "optional": true,
  520 + "os": [
  521 + "darwin"
  522 + ],
  523 + "engines": {
  524 + "node": ">=12"
  525 + }
  526 + },
  527 + "node_modules/@esbuild/freebsd-arm64": {
  528 + "version": "0.21.5",
  529 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
  530 + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
  531 + "cpu": [
  532 + "arm64"
  533 + ],
  534 + "dev": true,
  535 + "license": "MIT",
  536 + "optional": true,
  537 + "os": [
  538 + "freebsd"
  539 + ],
  540 + "engines": {
  541 + "node": ">=12"
  542 + }
  543 + },
  544 + "node_modules/@esbuild/freebsd-x64": {
  545 + "version": "0.21.5",
  546 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
  547 + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
  548 + "cpu": [
  549 + "x64"
  550 + ],
  551 + "dev": true,
  552 + "license": "MIT",
  553 + "optional": true,
  554 + "os": [
  555 + "freebsd"
  556 + ],
  557 + "engines": {
  558 + "node": ">=12"
  559 + }
  560 + },
  561 + "node_modules/@esbuild/linux-arm": {
  562 + "version": "0.21.5",
  563 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
  564 + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
  565 + "cpu": [
  566 + "arm"
  567 + ],
  568 + "dev": true,
  569 + "license": "MIT",
  570 + "optional": true,
  571 + "os": [
  572 + "linux"
  573 + ],
  574 + "engines": {
  575 + "node": ">=12"
  576 + }
  577 + },
  578 + "node_modules/@esbuild/linux-arm64": {
  579 + "version": "0.21.5",
  580 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
  581 + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
  582 + "cpu": [
  583 + "arm64"
  584 + ],
  585 + "dev": true,
  586 + "license": "MIT",
  587 + "optional": true,
  588 + "os": [
  589 + "linux"
  590 + ],
  591 + "engines": {
  592 + "node": ">=12"
  593 + }
  594 + },
  595 + "node_modules/@esbuild/linux-ia32": {
  596 + "version": "0.21.5",
  597 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
  598 + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
  599 + "cpu": [
  600 + "ia32"
  601 + ],
  602 + "dev": true,
  603 + "license": "MIT",
  604 + "optional": true,
  605 + "os": [
  606 + "linux"
  607 + ],
  608 + "engines": {
  609 + "node": ">=12"
  610 + }
  611 + },
  612 + "node_modules/@esbuild/linux-loong64": {
  613 + "version": "0.21.5",
  614 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
  615 + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
  616 + "cpu": [
  617 + "loong64"
  618 + ],
  619 + "dev": true,
  620 + "license": "MIT",
  621 + "optional": true,
  622 + "os": [
  623 + "linux"
  624 + ],
  625 + "engines": {
  626 + "node": ">=12"
  627 + }
  628 + },
  629 + "node_modules/@esbuild/linux-mips64el": {
  630 + "version": "0.21.5",
  631 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
  632 + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
  633 + "cpu": [
  634 + "mips64el"
  635 + ],
  636 + "dev": true,
  637 + "license": "MIT",
  638 + "optional": true,
  639 + "os": [
  640 + "linux"
  641 + ],
  642 + "engines": {
  643 + "node": ">=12"
  644 + }
  645 + },
  646 + "node_modules/@esbuild/linux-ppc64": {
  647 + "version": "0.21.5",
  648 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
  649 + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
  650 + "cpu": [
  651 + "ppc64"
  652 + ],
  653 + "dev": true,
  654 + "license": "MIT",
  655 + "optional": true,
  656 + "os": [
  657 + "linux"
  658 + ],
  659 + "engines": {
  660 + "node": ">=12"
  661 + }
  662 + },
  663 + "node_modules/@esbuild/linux-riscv64": {
  664 + "version": "0.21.5",
  665 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
  666 + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
  667 + "cpu": [
  668 + "riscv64"
  669 + ],
  670 + "dev": true,
  671 + "license": "MIT",
  672 + "optional": true,
  673 + "os": [
  674 + "linux"
  675 + ],
  676 + "engines": {
  677 + "node": ">=12"
  678 + }
  679 + },
  680 + "node_modules/@esbuild/linux-s390x": {
  681 + "version": "0.21.5",
  682 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
  683 + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
  684 + "cpu": [
  685 + "s390x"
  686 + ],
  687 + "dev": true,
  688 + "license": "MIT",
  689 + "optional": true,
  690 + "os": [
  691 + "linux"
  692 + ],
  693 + "engines": {
  694 + "node": ">=12"
  695 + }
  696 + },
  697 + "node_modules/@esbuild/linux-x64": {
  698 + "version": "0.21.5",
  699 + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
  700 + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
  701 + "cpu": [
  702 + "x64"
  703 + ],
  704 + "dev": true,
  705 + "license": "MIT",
  706 + "optional": true,
  707 + "os": [
  708 + "linux"
  709 + ],
  710 + "engines": {
  711 + "node": ">=12"
  712 + }
  713 + },
  714 + "node_modules/@esbuild/netbsd-x64": {
  715 + "version": "0.21.5",
  716 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
  717 + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
  718 + "cpu": [
  719 + "x64"
  720 + ],
  721 + "dev": true,
  722 + "license": "MIT",
  723 + "optional": true,
  724 + "os": [
  725 + "netbsd"
  726 + ],
  727 + "engines": {
  728 + "node": ">=12"
  729 + }
  730 + },
  731 + "node_modules/@esbuild/openbsd-x64": {
  732 + "version": "0.21.5",
  733 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
  734 + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
  735 + "cpu": [
  736 + "x64"
  737 + ],
  738 + "dev": true,
  739 + "license": "MIT",
  740 + "optional": true,
  741 + "os": [
  742 + "openbsd"
  743 + ],
  744 + "engines": {
  745 + "node": ">=12"
  746 + }
  747 + },
  748 + "node_modules/@esbuild/sunos-x64": {
  749 + "version": "0.21.5",
  750 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
  751 + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
  752 + "cpu": [
  753 + "x64"
  754 + ],
  755 + "dev": true,
  756 + "license": "MIT",
  757 + "optional": true,
  758 + "os": [
  759 + "sunos"
  760 + ],
  761 + "engines": {
  762 + "node": ">=12"
  763 + }
  764 + },
  765 + "node_modules/@esbuild/win32-arm64": {
  766 + "version": "0.21.5",
  767 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
  768 + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
  769 + "cpu": [
  770 + "arm64"
  771 + ],
  772 + "dev": true,
  773 + "license": "MIT",
  774 + "optional": true,
  775 + "os": [
  776 + "win32"
  777 + ],
  778 + "engines": {
  779 + "node": ">=12"
  780 + }
  781 + },
  782 + "node_modules/@esbuild/win32-ia32": {
  783 + "version": "0.21.5",
  784 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
  785 + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
  786 + "cpu": [
  787 + "ia32"
  788 + ],
  789 + "dev": true,
  790 + "license": "MIT",
  791 + "optional": true,
  792 + "os": [
  793 + "win32"
  794 + ],
  795 + "engines": {
  796 + "node": ">=12"
  797 + }
  798 + },
  799 + "node_modules/@esbuild/win32-x64": {
  800 + "version": "0.21.5",
  801 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
  802 + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
  803 + "cpu": [
  804 + "x64"
  805 + ],
  806 + "dev": true,
  807 + "license": "MIT",
  808 + "optional": true,
  809 + "os": [
  810 + "win32"
  811 + ],
  812 + "engines": {
  813 + "node": ">=12"
  814 + }
  815 + },
  816 + "node_modules/@jridgewell/gen-mapping": {
  817 + "version": "0.3.13",
  818 + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
  819 + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
  820 + "dev": true,
  821 + "license": "MIT",
  822 + "dependencies": {
  823 + "@jridgewell/sourcemap-codec": "^1.5.0",
  824 + "@jridgewell/trace-mapping": "^0.3.24"
  825 + }
  826 + },
  827 + "node_modules/@jridgewell/remapping": {
  828 + "version": "2.3.5",
  829 + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
  830 + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
  831 + "dev": true,
  832 + "license": "MIT",
  833 + "dependencies": {
  834 + "@jridgewell/gen-mapping": "^0.3.5",
  835 + "@jridgewell/trace-mapping": "^0.3.24"
  836 + }
  837 + },
  838 + "node_modules/@jridgewell/resolve-uri": {
  839 + "version": "3.1.2",
  840 + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
  841 + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
  842 + "dev": true,
  843 + "license": "MIT",
  844 + "engines": {
  845 + "node": ">=6.0.0"
  846 + }
  847 + },
  848 + "node_modules/@jridgewell/sourcemap-codec": {
  849 + "version": "1.5.5",
  850 + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
  851 + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
  852 + "dev": true,
  853 + "license": "MIT"
  854 + },
  855 + "node_modules/@jridgewell/trace-mapping": {
  856 + "version": "0.3.31",
  857 + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
  858 + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
  859 + "dev": true,
  860 + "license": "MIT",
  861 + "dependencies": {
  862 + "@jridgewell/resolve-uri": "^3.1.0",
  863 + "@jridgewell/sourcemap-codec": "^1.4.14"
  864 + }
  865 + },
  866 + "node_modules/@rc-component/async-validator": {
  867 + "version": "5.1.2",
  868 + "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.1.2.tgz",
  869 + "integrity": "sha512-WYbrZSjzznU1ekD0qFq2qRxt309VoS61MTG5npnFQlKYcoy9IzU8T+ZCIhq5bGAXRbXysABFWTspicMfmWFwow==",
  870 + "license": "MIT",
  871 + "dependencies": {
  872 + "@babel/runtime": "^7.24.4"
  873 + },
  874 + "engines": {
  875 + "node": ">=14.x"
  876 + }
  877 + },
  878 + "node_modules/@rc-component/color-picker": {
  879 + "version": "2.0.1",
  880 + "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-2.0.1.tgz",
  881 + "integrity": "sha512-WcZYwAThV/b2GISQ8F+7650r5ZZJ043E57aVBFkQ+kSY4C6wdofXgB0hBx+GPGpIU0Z81eETNoDUJMr7oy/P8Q==",
  882 + "license": "MIT",
  883 + "dependencies": {
  884 + "@ant-design/fast-color": "^2.0.6",
  885 + "@babel/runtime": "^7.23.6",
  886 + "classnames": "^2.2.6",
  887 + "rc-util": "^5.38.1"
  888 + },
  889 + "peerDependencies": {
  890 + "react": ">=16.9.0",
  891 + "react-dom": ">=16.9.0"
  892 + }
  893 + },
  894 + "node_modules/@rc-component/context": {
  895 + "version": "1.4.0",
  896 + "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-1.4.0.tgz",
  897 + "integrity": "sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w==",
  898 + "license": "MIT",
  899 + "dependencies": {
  900 + "@babel/runtime": "^7.10.1",
  901 + "rc-util": "^5.27.0"
  902 + },
  903 + "peerDependencies": {
  904 + "react": ">=16.9.0",
  905 + "react-dom": ">=16.9.0"
  906 + }
  907 + },
  908 + "node_modules/@rc-component/mini-decimal": {
  909 + "version": "1.1.4",
  910 + "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.4.tgz",
  911 + "integrity": "sha512-xiuXcaCwyOWpD8a8scdExFl+bntNphAW8XeenL1ig2en0AAZY0Pcp4pC0dI22qJ+NvxKn9RoNIoRdqYU3BLH4w==",
  912 + "license": "MIT",
  913 + "dependencies": {
  914 + "@babel/runtime": "^7.18.0"
  915 + },
  916 + "engines": {
  917 + "node": ">=8.x"
  918 + }
  919 + },
  920 + "node_modules/@rc-component/mutate-observer": {
  921 + "version": "1.1.0",
  922 + "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-1.1.0.tgz",
  923 + "integrity": "sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==",
  924 + "license": "MIT",
  925 + "dependencies": {
  926 + "@babel/runtime": "^7.18.0",
  927 + "classnames": "^2.3.2",
  928 + "rc-util": "^5.24.4"
  929 + },
  930 + "engines": {
  931 + "node": ">=8.x"
  932 + },
  933 + "peerDependencies": {
  934 + "react": ">=16.9.0",
  935 + "react-dom": ">=16.9.0"
  936 + }
  937 + },
  938 + "node_modules/@rc-component/portal": {
  939 + "version": "1.1.2",
  940 + "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.2.tgz",
  941 + "integrity": "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==",
  942 + "license": "MIT",
  943 + "dependencies": {
  944 + "@babel/runtime": "^7.18.0",
  945 + "classnames": "^2.3.2",
  946 + "rc-util": "^5.24.4"
  947 + },
  948 + "engines": {
  949 + "node": ">=8.x"
  950 + },
  951 + "peerDependencies": {
  952 + "react": ">=16.9.0",
  953 + "react-dom": ">=16.9.0"
  954 + }
  955 + },
  956 + "node_modules/@rc-component/qrcode": {
  957 + "version": "1.1.3",
  958 + "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.1.3.tgz",
  959 + "integrity": "sha512-aGv6alnn4HbDEsURzKP+jv13rbi1VxmAYfBNZr5GKF1iohMNWy5tAVoJ1E3cOvzMB1kbUPvCXchM6zSFlRGPhA==",
  960 + "license": "MIT",
  961 + "dependencies": {
  962 + "@babel/runtime": "^7.24.7"
  963 + },
  964 + "engines": {
  965 + "node": ">=8.x"
  966 + },
  967 + "peerDependencies": {
  968 + "react": ">=16.9.0",
  969 + "react-dom": ">=16.9.0"
  970 + }
  971 + },
  972 + "node_modules/@rc-component/tour": {
  973 + "version": "1.15.1",
  974 + "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-1.15.1.tgz",
  975 + "integrity": "sha512-Tr2t7J1DKZUpfJuDZWHxyxWpfmj8EZrqSgyMZ+BCdvKZ6r1UDsfU46M/iWAAFBy961Ssfom2kv5f3UcjIL2CmQ==",
  976 + "license": "MIT",
  977 + "dependencies": {
  978 + "@babel/runtime": "^7.18.0",
  979 + "@rc-component/portal": "^1.0.0-9",
  980 + "@rc-component/trigger": "^2.0.0",
  981 + "classnames": "^2.3.2",
  982 + "rc-util": "^5.24.4"
  983 + },
  984 + "engines": {
  985 + "node": ">=8.x"
  986 + },
  987 + "peerDependencies": {
  988 + "react": ">=16.9.0",
  989 + "react-dom": ">=16.9.0"
  990 + }
  991 + },
  992 + "node_modules/@rc-component/trigger": {
  993 + "version": "2.3.1",
  994 + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.3.1.tgz",
  995 + "integrity": "sha512-ORENF39PeXTzM+gQEshuk460Z8N4+6DkjpxlpE7Q3gYy1iBpLrx0FOJz3h62ryrJZ/3zCAUIkT1Pb/8hHWpb3A==",
  996 + "license": "MIT",
  997 + "dependencies": {
  998 + "@babel/runtime": "^7.23.2",
  999 + "@rc-component/portal": "^1.1.0",
  1000 + "classnames": "^2.3.2",
  1001 + "rc-motion": "^2.0.0",
  1002 + "rc-resize-observer": "^1.3.1",
  1003 + "rc-util": "^5.44.0"
  1004 + },
  1005 + "engines": {
  1006 + "node": ">=8.x"
  1007 + },
  1008 + "peerDependencies": {
  1009 + "react": ">=16.9.0",
  1010 + "react-dom": ">=16.9.0"
  1011 + }
  1012 + },
  1013 + "node_modules/@rolldown/pluginutils": {
  1014 + "version": "1.0.0-beta.27",
  1015 + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
  1016 + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
  1017 + "dev": true,
  1018 + "license": "MIT"
  1019 + },
  1020 + "node_modules/@rollup/rollup-android-arm-eabi": {
  1021 + "version": "4.62.2",
  1022 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
  1023 + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==",
  1024 + "cpu": [
  1025 + "arm"
  1026 + ],
  1027 + "dev": true,
  1028 + "license": "MIT",
  1029 + "optional": true,
  1030 + "os": [
  1031 + "android"
  1032 + ]
  1033 + },
  1034 + "node_modules/@rollup/rollup-android-arm64": {
  1035 + "version": "4.62.2",
  1036 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz",
  1037 + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==",
  1038 + "cpu": [
  1039 + "arm64"
  1040 + ],
  1041 + "dev": true,
  1042 + "license": "MIT",
  1043 + "optional": true,
  1044 + "os": [
  1045 + "android"
  1046 + ]
  1047 + },
  1048 + "node_modules/@rollup/rollup-darwin-arm64": {
  1049 + "version": "4.62.2",
  1050 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz",
  1051 + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==",
  1052 + "cpu": [
  1053 + "arm64"
  1054 + ],
  1055 + "dev": true,
  1056 + "license": "MIT",
  1057 + "optional": true,
  1058 + "os": [
  1059 + "darwin"
  1060 + ]
  1061 + },
  1062 + "node_modules/@rollup/rollup-darwin-x64": {
  1063 + "version": "4.62.2",
  1064 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz",
  1065 + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==",
  1066 + "cpu": [
  1067 + "x64"
  1068 + ],
  1069 + "dev": true,
  1070 + "license": "MIT",
  1071 + "optional": true,
  1072 + "os": [
  1073 + "darwin"
  1074 + ]
  1075 + },
  1076 + "node_modules/@rollup/rollup-freebsd-arm64": {
  1077 + "version": "4.62.2",
  1078 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz",
  1079 + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==",
  1080 + "cpu": [
  1081 + "arm64"
  1082 + ],
  1083 + "dev": true,
  1084 + "license": "MIT",
  1085 + "optional": true,
  1086 + "os": [
  1087 + "freebsd"
  1088 + ]
  1089 + },
  1090 + "node_modules/@rollup/rollup-freebsd-x64": {
  1091 + "version": "4.62.2",
  1092 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz",
  1093 + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==",
  1094 + "cpu": [
  1095 + "x64"
  1096 + ],
  1097 + "dev": true,
  1098 + "license": "MIT",
  1099 + "optional": true,
  1100 + "os": [
  1101 + "freebsd"
  1102 + ]
  1103 + },
  1104 + "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
  1105 + "version": "4.62.2",
  1106 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz",
  1107 + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==",
  1108 + "cpu": [
  1109 + "arm"
  1110 + ],
  1111 + "dev": true,
  1112 + "libc": [
  1113 + "glibc"
  1114 + ],
  1115 + "license": "MIT",
  1116 + "optional": true,
  1117 + "os": [
  1118 + "linux"
  1119 + ]
  1120 + },
  1121 + "node_modules/@rollup/rollup-linux-arm-musleabihf": {
  1122 + "version": "4.62.2",
  1123 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz",
  1124 + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==",
  1125 + "cpu": [
  1126 + "arm"
  1127 + ],
  1128 + "dev": true,
  1129 + "libc": [
  1130 + "musl"
  1131 + ],
  1132 + "license": "MIT",
  1133 + "optional": true,
  1134 + "os": [
  1135 + "linux"
  1136 + ]
  1137 + },
  1138 + "node_modules/@rollup/rollup-linux-arm64-gnu": {
  1139 + "version": "4.62.2",
  1140 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz",
  1141 + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==",
  1142 + "cpu": [
  1143 + "arm64"
  1144 + ],
  1145 + "dev": true,
  1146 + "libc": [
  1147 + "glibc"
  1148 + ],
  1149 + "license": "MIT",
  1150 + "optional": true,
  1151 + "os": [
  1152 + "linux"
  1153 + ]
  1154 + },
  1155 + "node_modules/@rollup/rollup-linux-arm64-musl": {
  1156 + "version": "4.62.2",
  1157 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz",
  1158 + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==",
  1159 + "cpu": [
  1160 + "arm64"
  1161 + ],
  1162 + "dev": true,
  1163 + "libc": [
  1164 + "musl"
  1165 + ],
  1166 + "license": "MIT",
  1167 + "optional": true,
  1168 + "os": [
  1169 + "linux"
  1170 + ]
  1171 + },
  1172 + "node_modules/@rollup/rollup-linux-loong64-gnu": {
  1173 + "version": "4.62.2",
  1174 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz",
  1175 + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==",
  1176 + "cpu": [
  1177 + "loong64"
  1178 + ],
  1179 + "dev": true,
  1180 + "libc": [
  1181 + "glibc"
  1182 + ],
  1183 + "license": "MIT",
  1184 + "optional": true,
  1185 + "os": [
  1186 + "linux"
  1187 + ]
  1188 + },
  1189 + "node_modules/@rollup/rollup-linux-loong64-musl": {
  1190 + "version": "4.62.2",
  1191 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz",
  1192 + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==",
  1193 + "cpu": [
  1194 + "loong64"
  1195 + ],
  1196 + "dev": true,
  1197 + "libc": [
  1198 + "musl"
  1199 + ],
  1200 + "license": "MIT",
  1201 + "optional": true,
  1202 + "os": [
  1203 + "linux"
  1204 + ]
  1205 + },
  1206 + "node_modules/@rollup/rollup-linux-ppc64-gnu": {
  1207 + "version": "4.62.2",
  1208 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz",
  1209 + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==",
  1210 + "cpu": [
  1211 + "ppc64"
  1212 + ],
  1213 + "dev": true,
  1214 + "libc": [
  1215 + "glibc"
  1216 + ],
  1217 + "license": "MIT",
  1218 + "optional": true,
  1219 + "os": [
  1220 + "linux"
  1221 + ]
  1222 + },
  1223 + "node_modules/@rollup/rollup-linux-ppc64-musl": {
  1224 + "version": "4.62.2",
  1225 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz",
  1226 + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==",
  1227 + "cpu": [
  1228 + "ppc64"
  1229 + ],
  1230 + "dev": true,
  1231 + "libc": [
  1232 + "musl"
  1233 + ],
  1234 + "license": "MIT",
  1235 + "optional": true,
  1236 + "os": [
  1237 + "linux"
  1238 + ]
  1239 + },
  1240 + "node_modules/@rollup/rollup-linux-riscv64-gnu": {
  1241 + "version": "4.62.2",
  1242 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz",
  1243 + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==",
  1244 + "cpu": [
  1245 + "riscv64"
  1246 + ],
  1247 + "dev": true,
  1248 + "libc": [
  1249 + "glibc"
  1250 + ],
  1251 + "license": "MIT",
  1252 + "optional": true,
  1253 + "os": [
  1254 + "linux"
  1255 + ]
  1256 + },
  1257 + "node_modules/@rollup/rollup-linux-riscv64-musl": {
  1258 + "version": "4.62.2",
  1259 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz",
  1260 + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==",
  1261 + "cpu": [
  1262 + "riscv64"
  1263 + ],
  1264 + "dev": true,
  1265 + "libc": [
  1266 + "musl"
  1267 + ],
  1268 + "license": "MIT",
  1269 + "optional": true,
  1270 + "os": [
  1271 + "linux"
  1272 + ]
  1273 + },
  1274 + "node_modules/@rollup/rollup-linux-s390x-gnu": {
  1275 + "version": "4.62.2",
  1276 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz",
  1277 + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==",
  1278 + "cpu": [
  1279 + "s390x"
  1280 + ],
  1281 + "dev": true,
  1282 + "libc": [
  1283 + "glibc"
  1284 + ],
  1285 + "license": "MIT",
  1286 + "optional": true,
  1287 + "os": [
  1288 + "linux"
  1289 + ]
  1290 + },
  1291 + "node_modules/@rollup/rollup-linux-x64-gnu": {
  1292 + "version": "4.62.2",
  1293 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz",
  1294 + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==",
  1295 + "cpu": [
  1296 + "x64"
  1297 + ],
  1298 + "dev": true,
  1299 + "libc": [
  1300 + "glibc"
  1301 + ],
  1302 + "license": "MIT",
  1303 + "optional": true,
  1304 + "os": [
  1305 + "linux"
  1306 + ]
  1307 + },
  1308 + "node_modules/@rollup/rollup-linux-x64-musl": {
  1309 + "version": "4.62.2",
  1310 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz",
  1311 + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==",
  1312 + "cpu": [
  1313 + "x64"
  1314 + ],
  1315 + "dev": true,
  1316 + "libc": [
  1317 + "musl"
  1318 + ],
  1319 + "license": "MIT",
  1320 + "optional": true,
  1321 + "os": [
  1322 + "linux"
  1323 + ]
  1324 + },
  1325 + "node_modules/@rollup/rollup-openbsd-x64": {
  1326 + "version": "4.62.2",
  1327 + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz",
  1328 + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==",
  1329 + "cpu": [
  1330 + "x64"
  1331 + ],
  1332 + "dev": true,
  1333 + "license": "MIT",
  1334 + "optional": true,
  1335 + "os": [
  1336 + "openbsd"
  1337 + ]
  1338 + },
  1339 + "node_modules/@rollup/rollup-openharmony-arm64": {
  1340 + "version": "4.62.2",
  1341 + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz",
  1342 + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==",
  1343 + "cpu": [
  1344 + "arm64"
  1345 + ],
  1346 + "dev": true,
  1347 + "license": "MIT",
  1348 + "optional": true,
  1349 + "os": [
  1350 + "openharmony"
  1351 + ]
  1352 + },
  1353 + "node_modules/@rollup/rollup-win32-arm64-msvc": {
  1354 + "version": "4.62.2",
  1355 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz",
  1356 + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==",
  1357 + "cpu": [
  1358 + "arm64"
  1359 + ],
  1360 + "dev": true,
  1361 + "license": "MIT",
  1362 + "optional": true,
  1363 + "os": [
  1364 + "win32"
  1365 + ]
  1366 + },
  1367 + "node_modules/@rollup/rollup-win32-ia32-msvc": {
  1368 + "version": "4.62.2",
  1369 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz",
  1370 + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==",
  1371 + "cpu": [
  1372 + "ia32"
  1373 + ],
  1374 + "dev": true,
  1375 + "license": "MIT",
  1376 + "optional": true,
  1377 + "os": [
  1378 + "win32"
  1379 + ]
  1380 + },
  1381 + "node_modules/@rollup/rollup-win32-x64-gnu": {
  1382 + "version": "4.62.2",
  1383 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz",
  1384 + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==",
  1385 + "cpu": [
  1386 + "x64"
  1387 + ],
  1388 + "dev": true,
  1389 + "license": "MIT",
  1390 + "optional": true,
  1391 + "os": [
  1392 + "win32"
  1393 + ]
  1394 + },
  1395 + "node_modules/@rollup/rollup-win32-x64-msvc": {
  1396 + "version": "4.62.2",
  1397 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz",
  1398 + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==",
  1399 + "cpu": [
  1400 + "x64"
  1401 + ],
  1402 + "dev": true,
  1403 + "license": "MIT",
  1404 + "optional": true,
  1405 + "os": [
  1406 + "win32"
  1407 + ]
  1408 + },
  1409 + "node_modules/@types/babel__core": {
  1410 + "version": "7.20.5",
  1411 + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
  1412 + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
  1413 + "dev": true,
  1414 + "license": "MIT",
  1415 + "dependencies": {
  1416 + "@babel/parser": "^7.20.7",
  1417 + "@babel/types": "^7.20.7",
  1418 + "@types/babel__generator": "*",
  1419 + "@types/babel__template": "*",
  1420 + "@types/babel__traverse": "*"
  1421 + }
  1422 + },
  1423 + "node_modules/@types/babel__generator": {
  1424 + "version": "7.27.0",
  1425 + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
  1426 + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
  1427 + "dev": true,
  1428 + "license": "MIT",
  1429 + "dependencies": {
  1430 + "@babel/types": "^7.0.0"
  1431 + }
  1432 + },
  1433 + "node_modules/@types/babel__template": {
  1434 + "version": "7.4.4",
  1435 + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
  1436 + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
  1437 + "dev": true,
  1438 + "license": "MIT",
  1439 + "dependencies": {
  1440 + "@babel/parser": "^7.1.0",
  1441 + "@babel/types": "^7.0.0"
  1442 + }
  1443 + },
  1444 + "node_modules/@types/babel__traverse": {
  1445 + "version": "7.28.0",
  1446 + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
  1447 + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
  1448 + "dev": true,
  1449 + "license": "MIT",
  1450 + "dependencies": {
  1451 + "@babel/types": "^7.28.2"
  1452 + }
  1453 + },
  1454 + "node_modules/@types/estree": {
  1455 + "version": "1.0.9",
  1456 + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
  1457 + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
  1458 + "dev": true,
  1459 + "license": "MIT"
  1460 + },
  1461 + "node_modules/@types/prop-types": {
  1462 + "version": "15.7.15",
  1463 + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
  1464 + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
  1465 + "dev": true,
  1466 + "license": "MIT"
  1467 + },
  1468 + "node_modules/@types/react": {
  1469 + "version": "18.3.31",
  1470 + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz",
  1471 + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
  1472 + "dev": true,
  1473 + "license": "MIT",
  1474 + "dependencies": {
  1475 + "@types/prop-types": "*",
  1476 + "csstype": "^3.2.2"
  1477 + }
  1478 + },
  1479 + "node_modules/@types/react-dom": {
  1480 + "version": "18.3.7",
  1481 + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
  1482 + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
  1483 + "dev": true,
  1484 + "license": "MIT",
  1485 + "peerDependencies": {
  1486 + "@types/react": "^18.0.0"
  1487 + }
  1488 + },
  1489 + "node_modules/@vitejs/plugin-react": {
  1490 + "version": "4.7.0",
  1491 + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
  1492 + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
  1493 + "dev": true,
  1494 + "license": "MIT",
  1495 + "dependencies": {
  1496 + "@babel/core": "^7.28.0",
  1497 + "@babel/plugin-transform-react-jsx-self": "^7.27.1",
  1498 + "@babel/plugin-transform-react-jsx-source": "^7.27.1",
  1499 + "@rolldown/pluginutils": "1.0.0-beta.27",
  1500 + "@types/babel__core": "^7.20.5",
  1501 + "react-refresh": "^0.17.0"
  1502 + },
  1503 + "engines": {
  1504 + "node": "^14.18.0 || >=16.0.0"
  1505 + },
  1506 + "peerDependencies": {
  1507 + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
  1508 + }
  1509 + },
  1510 + "node_modules/antd": {
  1511 + "version": "5.29.3",
  1512 + "resolved": "https://registry.npmjs.org/antd/-/antd-5.29.3.tgz",
  1513 + "integrity": "sha512-3DdbGCa9tWAJGcCJ6rzR8EJFsv2CtyEbkVabZE14pfgUHfCicWCj0/QzQVLDYg8CPfQk9BH7fHCoTXHTy7MP/A==",
  1514 + "license": "MIT",
  1515 + "dependencies": {
  1516 + "@ant-design/colors": "^7.2.1",
  1517 + "@ant-design/cssinjs": "^1.23.0",
  1518 + "@ant-design/cssinjs-utils": "^1.1.3",
  1519 + "@ant-design/fast-color": "^2.0.6",
  1520 + "@ant-design/icons": "^5.6.1",
  1521 + "@ant-design/react-slick": "~1.1.2",
  1522 + "@babel/runtime": "^7.26.0",
  1523 + "@rc-component/color-picker": "~2.0.1",
  1524 + "@rc-component/mutate-observer": "^1.1.0",
  1525 + "@rc-component/qrcode": "~1.1.0",
  1526 + "@rc-component/tour": "~1.15.1",
  1527 + "@rc-component/trigger": "^2.3.0",
  1528 + "classnames": "^2.5.1",
  1529 + "copy-to-clipboard": "^3.3.3",
  1530 + "dayjs": "^1.11.11",
  1531 + "rc-cascader": "~3.34.0",
  1532 + "rc-checkbox": "~3.5.0",
  1533 + "rc-collapse": "~3.9.0",
  1534 + "rc-dialog": "~9.6.0",
  1535 + "rc-drawer": "~7.3.0",
  1536 + "rc-dropdown": "~4.2.1",
  1537 + "rc-field-form": "~2.7.1",
  1538 + "rc-image": "~7.12.0",
  1539 + "rc-input": "~1.8.0",
  1540 + "rc-input-number": "~9.5.0",
  1541 + "rc-mentions": "~2.20.0",
  1542 + "rc-menu": "~9.16.1",
  1543 + "rc-motion": "^2.9.5",
  1544 + "rc-notification": "~5.6.4",
  1545 + "rc-pagination": "~5.1.0",
  1546 + "rc-picker": "~4.11.3",
  1547 + "rc-progress": "~4.0.0",
  1548 + "rc-rate": "~2.13.1",
  1549 + "rc-resize-observer": "^1.4.3",
  1550 + "rc-segmented": "~2.7.0",
  1551 + "rc-select": "~14.16.8",
  1552 + "rc-slider": "~11.1.9",
  1553 + "rc-steps": "~6.0.1",
  1554 + "rc-switch": "~4.1.0",
  1555 + "rc-table": "~7.54.0",
  1556 + "rc-tabs": "~15.7.0",
  1557 + "rc-textarea": "~1.10.2",
  1558 + "rc-tooltip": "~6.4.0",
  1559 + "rc-tree": "~5.13.1",
  1560 + "rc-tree-select": "~5.27.0",
  1561 + "rc-upload": "~4.11.0",
  1562 + "rc-util": "^5.44.4",
  1563 + "scroll-into-view-if-needed": "^3.1.0",
  1564 + "throttle-debounce": "^5.0.2"
  1565 + },
  1566 + "funding": {
  1567 + "type": "opencollective",
  1568 + "url": "https://opencollective.com/ant-design"
  1569 + },
  1570 + "peerDependencies": {
  1571 + "react": ">=16.9.0",
  1572 + "react-dom": ">=16.9.0"
  1573 + }
  1574 + },
  1575 + "node_modules/baseline-browser-mapping": {
  1576 + "version": "2.10.43",
  1577 + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz",
  1578 + "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==",
  1579 + "dev": true,
  1580 + "license": "Apache-2.0",
  1581 + "bin": {
  1582 + "baseline-browser-mapping": "dist/cli.cjs"
  1583 + },
  1584 + "engines": {
  1585 + "node": ">=6.0.0"
  1586 + }
  1587 + },
  1588 + "node_modules/browserslist": {
  1589 + "version": "4.28.6",
  1590 + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz",
  1591 + "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==",
  1592 + "dev": true,
  1593 + "funding": [
  1594 + {
  1595 + "type": "opencollective",
  1596 + "url": "https://opencollective.com/browserslist"
  1597 + },
  1598 + {
  1599 + "type": "tidelift",
  1600 + "url": "https://tidelift.com/funding/github/npm/browserslist"
  1601 + },
  1602 + {
  1603 + "type": "github",
  1604 + "url": "https://github.com/sponsors/ai"
  1605 + }
  1606 + ],
  1607 + "license": "MIT",
  1608 + "dependencies": {
  1609 + "baseline-browser-mapping": "^2.10.42",
  1610 + "caniuse-lite": "^1.0.30001803",
  1611 + "electron-to-chromium": "^1.5.389",
  1612 + "node-releases": "^2.0.51",
  1613 + "update-browserslist-db": "^1.2.3"
  1614 + },
  1615 + "bin": {
  1616 + "browserslist": "cli.js"
  1617 + },
  1618 + "engines": {
  1619 + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
  1620 + }
  1621 + },
  1622 + "node_modules/caniuse-lite": {
  1623 + "version": "1.0.30001806",
  1624 + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz",
  1625 + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==",
  1626 + "dev": true,
  1627 + "funding": [
  1628 + {
  1629 + "type": "opencollective",
  1630 + "url": "https://opencollective.com/browserslist"
  1631 + },
  1632 + {
  1633 + "type": "tidelift",
  1634 + "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
  1635 + },
  1636 + {
  1637 + "type": "github",
  1638 + "url": "https://github.com/sponsors/ai"
  1639 + }
  1640 + ],
  1641 + "license": "CC-BY-4.0"
  1642 + },
  1643 + "node_modules/classnames": {
  1644 + "version": "2.5.1",
  1645 + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
  1646 + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
  1647 + "license": "MIT"
  1648 + },
  1649 + "node_modules/compute-scroll-into-view": {
  1650 + "version": "3.1.1",
  1651 + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz",
  1652 + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==",
  1653 + "license": "MIT"
  1654 + },
  1655 + "node_modules/convert-source-map": {
  1656 + "version": "2.0.0",
  1657 + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
  1658 + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
  1659 + "dev": true,
  1660 + "license": "MIT"
  1661 + },
  1662 + "node_modules/copy-to-clipboard": {
  1663 + "version": "3.3.3",
  1664 + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
  1665 + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==",
  1666 + "license": "MIT",
  1667 + "dependencies": {
  1668 + "toggle-selection": "^1.0.6"
  1669 + }
  1670 + },
  1671 + "node_modules/csstype": {
  1672 + "version": "3.2.3",
  1673 + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
  1674 + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
  1675 + "license": "MIT"
  1676 + },
  1677 + "node_modules/dayjs": {
  1678 + "version": "1.11.21",
  1679 + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz",
  1680 + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==",
  1681 + "license": "MIT"
  1682 + },
  1683 + "node_modules/debug": {
  1684 + "version": "4.4.3",
  1685 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
  1686 + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
  1687 + "dev": true,
  1688 + "license": "MIT",
  1689 + "dependencies": {
  1690 + "ms": "^2.1.3"
  1691 + },
  1692 + "engines": {
  1693 + "node": ">=6.0"
  1694 + },
  1695 + "peerDependenciesMeta": {
  1696 + "supports-color": {
  1697 + "optional": true
  1698 + }
  1699 + }
  1700 + },
  1701 + "node_modules/electron-to-chromium": {
  1702 + "version": "1.5.393",
  1703 + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.393.tgz",
  1704 + "integrity": "sha512-kiDJdIUawuEIcp9XoICKp1iTYDEbgguIPq526N1Q7jIQDeQ3CqoMx71025PI/7E48Ddtw2HuWsVjY7afEgNxmg==",
  1705 + "dev": true,
  1706 + "license": "ISC"
  1707 + },
  1708 + "node_modules/esbuild": {
  1709 + "version": "0.21.5",
  1710 + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
  1711 + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
  1712 + "dev": true,
  1713 + "hasInstallScript": true,
  1714 + "license": "MIT",
  1715 + "bin": {
  1716 + "esbuild": "bin/esbuild"
  1717 + },
  1718 + "engines": {
  1719 + "node": ">=12"
  1720 + },
  1721 + "optionalDependencies": {
  1722 + "@esbuild/aix-ppc64": "0.21.5",
  1723 + "@esbuild/android-arm": "0.21.5",
  1724 + "@esbuild/android-arm64": "0.21.5",
  1725 + "@esbuild/android-x64": "0.21.5",
  1726 + "@esbuild/darwin-arm64": "0.21.5",
  1727 + "@esbuild/darwin-x64": "0.21.5",
  1728 + "@esbuild/freebsd-arm64": "0.21.5",
  1729 + "@esbuild/freebsd-x64": "0.21.5",
  1730 + "@esbuild/linux-arm": "0.21.5",
  1731 + "@esbuild/linux-arm64": "0.21.5",
  1732 + "@esbuild/linux-ia32": "0.21.5",
  1733 + "@esbuild/linux-loong64": "0.21.5",
  1734 + "@esbuild/linux-mips64el": "0.21.5",
  1735 + "@esbuild/linux-ppc64": "0.21.5",
  1736 + "@esbuild/linux-riscv64": "0.21.5",
  1737 + "@esbuild/linux-s390x": "0.21.5",
  1738 + "@esbuild/linux-x64": "0.21.5",
  1739 + "@esbuild/netbsd-x64": "0.21.5",
  1740 + "@esbuild/openbsd-x64": "0.21.5",
  1741 + "@esbuild/sunos-x64": "0.21.5",
  1742 + "@esbuild/win32-arm64": "0.21.5",
  1743 + "@esbuild/win32-ia32": "0.21.5",
  1744 + "@esbuild/win32-x64": "0.21.5"
  1745 + }
  1746 + },
  1747 + "node_modules/escalade": {
  1748 + "version": "3.2.0",
  1749 + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
  1750 + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
  1751 + "dev": true,
  1752 + "license": "MIT",
  1753 + "engines": {
  1754 + "node": ">=6"
  1755 + }
  1756 + },
  1757 + "node_modules/fsevents": {
  1758 + "version": "2.3.3",
  1759 + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
  1760 + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
  1761 + "dev": true,
  1762 + "hasInstallScript": true,
  1763 + "license": "MIT",
  1764 + "optional": true,
  1765 + "os": [
  1766 + "darwin"
  1767 + ],
  1768 + "engines": {
  1769 + "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
  1770 + }
  1771 + },
  1772 + "node_modules/gensync": {
  1773 + "version": "1.0.0-beta.2",
  1774 + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
  1775 + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
  1776 + "dev": true,
  1777 + "license": "MIT",
  1778 + "engines": {
  1779 + "node": ">=6.9.0"
  1780 + }
  1781 + },
  1782 + "node_modules/js-tokens": {
  1783 + "version": "4.0.0",
  1784 + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
  1785 + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
  1786 + "license": "MIT"
  1787 + },
  1788 + "node_modules/jsesc": {
  1789 + "version": "3.1.0",
  1790 + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
  1791 + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
  1792 + "dev": true,
  1793 + "license": "MIT",
  1794 + "bin": {
  1795 + "jsesc": "bin/jsesc"
  1796 + },
  1797 + "engines": {
  1798 + "node": ">=6"
  1799 + }
  1800 + },
  1801 + "node_modules/json2mq": {
  1802 + "version": "0.2.0",
  1803 + "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz",
  1804 + "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==",
  1805 + "license": "MIT",
  1806 + "dependencies": {
  1807 + "string-convert": "^0.2.0"
  1808 + }
  1809 + },
  1810 + "node_modules/json5": {
  1811 + "version": "2.2.3",
  1812 + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
  1813 + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
  1814 + "dev": true,
  1815 + "license": "MIT",
  1816 + "bin": {
  1817 + "json5": "lib/cli.js"
  1818 + },
  1819 + "engines": {
  1820 + "node": ">=6"
  1821 + }
  1822 + },
  1823 + "node_modules/loose-envify": {
  1824 + "version": "1.4.0",
  1825 + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
  1826 + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
  1827 + "license": "MIT",
  1828 + "dependencies": {
  1829 + "js-tokens": "^3.0.0 || ^4.0.0"
  1830 + },
  1831 + "bin": {
  1832 + "loose-envify": "cli.js"
  1833 + }
  1834 + },
  1835 + "node_modules/lru-cache": {
  1836 + "version": "5.1.1",
  1837 + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
  1838 + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
  1839 + "dev": true,
  1840 + "license": "ISC",
  1841 + "dependencies": {
  1842 + "yallist": "^3.0.2"
  1843 + }
  1844 + },
  1845 + "node_modules/ms": {
  1846 + "version": "2.1.3",
  1847 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
  1848 + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
  1849 + "dev": true,
  1850 + "license": "MIT"
  1851 + },
  1852 + "node_modules/nanoid": {
  1853 + "version": "3.3.16",
  1854 + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
  1855 + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
  1856 + "dev": true,
  1857 + "funding": [
  1858 + {
  1859 + "type": "github",
  1860 + "url": "https://github.com/sponsors/ai"
  1861 + }
  1862 + ],
  1863 + "license": "MIT",
  1864 + "bin": {
  1865 + "nanoid": "bin/nanoid.cjs"
  1866 + },
  1867 + "engines": {
  1868 + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
  1869 + }
  1870 + },
  1871 + "node_modules/node-releases": {
  1872 + "version": "2.0.51",
  1873 + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz",
  1874 + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==",
  1875 + "dev": true,
  1876 + "license": "MIT",
  1877 + "engines": {
  1878 + "node": ">=18"
  1879 + }
  1880 + },
  1881 + "node_modules/picocolors": {
  1882 + "version": "1.1.1",
  1883 + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
  1884 + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
  1885 + "dev": true,
  1886 + "license": "ISC"
  1887 + },
  1888 + "node_modules/postcss": {
  1889 + "version": "8.5.20",
  1890 + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz",
  1891 + "integrity": "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==",
  1892 + "dev": true,
  1893 + "funding": [
  1894 + {
  1895 + "type": "opencollective",
  1896 + "url": "https://opencollective.com/postcss/"
  1897 + },
  1898 + {
  1899 + "type": "tidelift",
  1900 + "url": "https://tidelift.com/funding/github/npm/postcss"
  1901 + },
  1902 + {
  1903 + "type": "github",
  1904 + "url": "https://github.com/sponsors/ai"
  1905 + }
  1906 + ],
  1907 + "license": "MIT",
  1908 + "dependencies": {
  1909 + "nanoid": "^3.3.16",
  1910 + "picocolors": "^1.1.1",
  1911 + "source-map-js": "^1.2.1"
  1912 + },
  1913 + "engines": {
  1914 + "node": "^10 || ^12 || >=14"
  1915 + }
  1916 + },
  1917 + "node_modules/rc-cascader": {
  1918 + "version": "3.34.0",
  1919 + "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.34.0.tgz",
  1920 + "integrity": "sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag==",
  1921 + "license": "MIT",
  1922 + "dependencies": {
  1923 + "@babel/runtime": "^7.25.7",
  1924 + "classnames": "^2.3.1",
  1925 + "rc-select": "~14.16.2",
  1926 + "rc-tree": "~5.13.0",
  1927 + "rc-util": "^5.43.0"
  1928 + },
  1929 + "peerDependencies": {
  1930 + "react": ">=16.9.0",
  1931 + "react-dom": ">=16.9.0"
  1932 + }
  1933 + },
  1934 + "node_modules/rc-checkbox": {
  1935 + "version": "3.5.0",
  1936 + "resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.5.0.tgz",
  1937 + "integrity": "sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg==",
  1938 + "license": "MIT",
  1939 + "dependencies": {
  1940 + "@babel/runtime": "^7.10.1",
  1941 + "classnames": "^2.3.2",
  1942 + "rc-util": "^5.25.2"
  1943 + },
  1944 + "peerDependencies": {
  1945 + "react": ">=16.9.0",
  1946 + "react-dom": ">=16.9.0"
  1947 + }
  1948 + },
  1949 + "node_modules/rc-collapse": {
  1950 + "version": "3.9.0",
  1951 + "resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.9.0.tgz",
  1952 + "integrity": "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA==",
  1953 + "license": "MIT",
  1954 + "dependencies": {
  1955 + "@babel/runtime": "^7.10.1",
  1956 + "classnames": "2.x",
  1957 + "rc-motion": "^2.3.4",
  1958 + "rc-util": "^5.27.0"
  1959 + },
  1960 + "peerDependencies": {
  1961 + "react": ">=16.9.0",
  1962 + "react-dom": ">=16.9.0"
  1963 + }
  1964 + },
  1965 + "node_modules/rc-dialog": {
  1966 + "version": "9.6.0",
  1967 + "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.6.0.tgz",
  1968 + "integrity": "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg==",
  1969 + "license": "MIT",
  1970 + "dependencies": {
  1971 + "@babel/runtime": "^7.10.1",
  1972 + "@rc-component/portal": "^1.0.0-8",
  1973 + "classnames": "^2.2.6",
  1974 + "rc-motion": "^2.3.0",
  1975 + "rc-util": "^5.21.0"
  1976 + },
  1977 + "peerDependencies": {
  1978 + "react": ">=16.9.0",
  1979 + "react-dom": ">=16.9.0"
  1980 + }
  1981 + },
  1982 + "node_modules/rc-drawer": {
  1983 + "version": "7.3.0",
  1984 + "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.3.0.tgz",
  1985 + "integrity": "sha512-DX6CIgiBWNpJIMGFO8BAISFkxiuKitoizooj4BDyee8/SnBn0zwO2FHrNDpqqepj0E/TFTDpmEBCyFuTgC7MOg==",
  1986 + "license": "MIT",
  1987 + "dependencies": {
  1988 + "@babel/runtime": "^7.23.9",
  1989 + "@rc-component/portal": "^1.1.1",
  1990 + "classnames": "^2.2.6",
  1991 + "rc-motion": "^2.6.1",
  1992 + "rc-util": "^5.38.1"
  1993 + },
  1994 + "peerDependencies": {
  1995 + "react": ">=16.9.0",
  1996 + "react-dom": ">=16.9.0"
  1997 + }
  1998 + },
  1999 + "node_modules/rc-dropdown": {
  2000 + "version": "4.2.1",
  2001 + "resolved": "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-4.2.1.tgz",
  2002 + "integrity": "sha512-YDAlXsPv3I1n42dv1JpdM7wJ+gSUBfeyPK59ZpBD9jQhK9jVuxpjj3NmWQHOBceA1zEPVX84T2wbdb2SD0UjmA==",
  2003 + "license": "MIT",
  2004 + "dependencies": {
  2005 + "@babel/runtime": "^7.18.3",
  2006 + "@rc-component/trigger": "^2.0.0",
  2007 + "classnames": "^2.2.6",
  2008 + "rc-util": "^5.44.1"
  2009 + },
  2010 + "peerDependencies": {
  2011 + "react": ">=16.11.0",
  2012 + "react-dom": ">=16.11.0"
  2013 + }
  2014 + },
  2015 + "node_modules/rc-field-form": {
  2016 + "version": "2.7.1",
  2017 + "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-2.7.1.tgz",
  2018 + "integrity": "sha512-vKeSifSJ6HoLaAB+B8aq/Qgm8a3dyxROzCtKNCsBQgiverpc4kWDQihoUwzUj+zNWJOykwSY4dNX3QrGwtVb9A==",
  2019 + "license": "MIT",
  2020 + "dependencies": {
  2021 + "@babel/runtime": "^7.18.0",
  2022 + "@rc-component/async-validator": "^5.0.3",
  2023 + "rc-util": "^5.32.2"
  2024 + },
  2025 + "engines": {
  2026 + "node": ">=8.x"
  2027 + },
  2028 + "peerDependencies": {
  2029 + "react": ">=16.9.0",
  2030 + "react-dom": ">=16.9.0"
  2031 + }
  2032 + },
  2033 + "node_modules/rc-image": {
  2034 + "version": "7.12.0",
  2035 + "resolved": "https://registry.npmjs.org/rc-image/-/rc-image-7.12.0.tgz",
  2036 + "integrity": "sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q==",
  2037 + "license": "MIT",
  2038 + "dependencies": {
  2039 + "@babel/runtime": "^7.11.2",
  2040 + "@rc-component/portal": "^1.0.2",
  2041 + "classnames": "^2.2.6",
  2042 + "rc-dialog": "~9.6.0",
  2043 + "rc-motion": "^2.6.2",
  2044 + "rc-util": "^5.34.1"
  2045 + },
  2046 + "peerDependencies": {
  2047 + "react": ">=16.9.0",
  2048 + "react-dom": ">=16.9.0"
  2049 + }
  2050 + },
  2051 + "node_modules/rc-input": {
  2052 + "version": "1.8.0",
  2053 + "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.8.0.tgz",
  2054 + "integrity": "sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA==",
  2055 + "license": "MIT",
  2056 + "dependencies": {
  2057 + "@babel/runtime": "^7.11.1",
  2058 + "classnames": "^2.2.1",
  2059 + "rc-util": "^5.18.1"
  2060 + },
  2061 + "peerDependencies": {
  2062 + "react": ">=16.0.0",
  2063 + "react-dom": ">=16.0.0"
  2064 + }
  2065 + },
  2066 + "node_modules/rc-input-number": {
  2067 + "version": "9.5.0",
  2068 + "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.5.0.tgz",
  2069 + "integrity": "sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag==",
  2070 + "license": "MIT",
  2071 + "dependencies": {
  2072 + "@babel/runtime": "^7.10.1",
  2073 + "@rc-component/mini-decimal": "^1.0.1",
  2074 + "classnames": "^2.2.5",
  2075 + "rc-input": "~1.8.0",
  2076 + "rc-util": "^5.40.1"
  2077 + },
  2078 + "peerDependencies": {
  2079 + "react": ">=16.9.0",
  2080 + "react-dom": ">=16.9.0"
  2081 + }
  2082 + },
  2083 + "node_modules/rc-mentions": {
  2084 + "version": "2.20.0",
  2085 + "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.20.0.tgz",
  2086 + "integrity": "sha512-w8HCMZEh3f0nR8ZEd466ATqmXFCMGMN5UFCzEUL0bM/nGw/wOS2GgRzKBcm19K++jDyuWCOJOdgcKGXU3fXfbQ==",
  2087 + "license": "MIT",
  2088 + "dependencies": {
  2089 + "@babel/runtime": "^7.22.5",
  2090 + "@rc-component/trigger": "^2.0.0",
  2091 + "classnames": "^2.2.6",
  2092 + "rc-input": "~1.8.0",
  2093 + "rc-menu": "~9.16.0",
  2094 + "rc-textarea": "~1.10.0",
  2095 + "rc-util": "^5.34.1"
  2096 + },
  2097 + "peerDependencies": {
  2098 + "react": ">=16.9.0",
  2099 + "react-dom": ">=16.9.0"
  2100 + }
  2101 + },
  2102 + "node_modules/rc-menu": {
  2103 + "version": "9.16.1",
  2104 + "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.16.1.tgz",
  2105 + "integrity": "sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg==",
  2106 + "license": "MIT",
  2107 + "dependencies": {
  2108 + "@babel/runtime": "^7.10.1",
  2109 + "@rc-component/trigger": "^2.0.0",
  2110 + "classnames": "2.x",
  2111 + "rc-motion": "^2.4.3",
  2112 + "rc-overflow": "^1.3.1",
  2113 + "rc-util": "^5.27.0"
  2114 + },
  2115 + "peerDependencies": {
  2116 + "react": ">=16.9.0",
  2117 + "react-dom": ">=16.9.0"
  2118 + }
  2119 + },
  2120 + "node_modules/rc-motion": {
  2121 + "version": "2.9.5",
  2122 + "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.5.tgz",
  2123 + "integrity": "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==",
  2124 + "license": "MIT",
  2125 + "dependencies": {
  2126 + "@babel/runtime": "^7.11.1",
  2127 + "classnames": "^2.2.1",
  2128 + "rc-util": "^5.44.0"
  2129 + },
  2130 + "peerDependencies": {
  2131 + "react": ">=16.9.0",
  2132 + "react-dom": ">=16.9.0"
  2133 + }
  2134 + },
  2135 + "node_modules/rc-notification": {
  2136 + "version": "5.6.4",
  2137 + "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-5.6.4.tgz",
  2138 + "integrity": "sha512-KcS4O6B4qzM3KH7lkwOB7ooLPZ4b6J+VMmQgT51VZCeEcmghdeR4IrMcFq0LG+RPdnbe/ArT086tGM8Snimgiw==",
  2139 + "license": "MIT",
  2140 + "dependencies": {
  2141 + "@babel/runtime": "^7.10.1",
  2142 + "classnames": "2.x",
  2143 + "rc-motion": "^2.9.0",
  2144 + "rc-util": "^5.20.1"
  2145 + },
  2146 + "engines": {
  2147 + "node": ">=8.x"
  2148 + },
  2149 + "peerDependencies": {
  2150 + "react": ">=16.9.0",
  2151 + "react-dom": ">=16.9.0"
  2152 + }
  2153 + },
  2154 + "node_modules/rc-overflow": {
  2155 + "version": "1.5.0",
  2156 + "resolved": "https://registry.npmjs.org/rc-overflow/-/rc-overflow-1.5.0.tgz",
  2157 + "integrity": "sha512-Lm/v9h0LymeUYJf0x39OveU52InkdRXqnn2aYXfWmo8WdOonIKB2kfau+GF0fWq6jPgtdO9yMqveGcK6aIhJmg==",
  2158 + "license": "MIT",
  2159 + "dependencies": {
  2160 + "@babel/runtime": "^7.11.1",
  2161 + "classnames": "^2.2.1",
  2162 + "rc-resize-observer": "^1.0.0",
  2163 + "rc-util": "^5.37.0"
  2164 + },
  2165 + "peerDependencies": {
  2166 + "react": ">=16.9.0",
  2167 + "react-dom": ">=16.9.0"
  2168 + }
  2169 + },
  2170 + "node_modules/rc-pagination": {
  2171 + "version": "5.1.0",
  2172 + "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-5.1.0.tgz",
  2173 + "integrity": "sha512-8416Yip/+eclTFdHXLKTxZvn70duYVGTvUUWbckCCZoIl3jagqke3GLsFrMs0bsQBikiYpZLD9206Ej4SOdOXQ==",
  2174 + "license": "MIT",
  2175 + "dependencies": {
  2176 + "@babel/runtime": "^7.10.1",
  2177 + "classnames": "^2.3.2",
  2178 + "rc-util": "^5.38.0"
  2179 + },
  2180 + "peerDependencies": {
  2181 + "react": ">=16.9.0",
  2182 + "react-dom": ">=16.9.0"
  2183 + }
  2184 + },
  2185 + "node_modules/rc-picker": {
  2186 + "version": "4.11.3",
  2187 + "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.11.3.tgz",
  2188 + "integrity": "sha512-MJ5teb7FlNE0NFHTncxXQ62Y5lytq6sh5nUw0iH8OkHL/TjARSEvSHpr940pWgjGANpjCwyMdvsEV55l5tYNSg==",
  2189 + "license": "MIT",
  2190 + "dependencies": {
  2191 + "@babel/runtime": "^7.24.7",
  2192 + "@rc-component/trigger": "^2.0.0",
  2193 + "classnames": "^2.2.1",
  2194 + "rc-overflow": "^1.3.2",
  2195 + "rc-resize-observer": "^1.4.0",
  2196 + "rc-util": "^5.43.0"
  2197 + },
  2198 + "engines": {
  2199 + "node": ">=8.x"
  2200 + },
  2201 + "peerDependencies": {
  2202 + "date-fns": ">= 2.x",
  2203 + "dayjs": ">= 1.x",
  2204 + "luxon": ">= 3.x",
  2205 + "moment": ">= 2.x",
  2206 + "react": ">=16.9.0",
  2207 + "react-dom": ">=16.9.0"
  2208 + },
  2209 + "peerDependenciesMeta": {
  2210 + "date-fns": {
  2211 + "optional": true
  2212 + },
  2213 + "dayjs": {
  2214 + "optional": true
  2215 + },
  2216 + "luxon": {
  2217 + "optional": true
  2218 + },
  2219 + "moment": {
  2220 + "optional": true
  2221 + }
  2222 + }
  2223 + },
  2224 + "node_modules/rc-progress": {
  2225 + "version": "4.0.0",
  2226 + "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-4.0.0.tgz",
  2227 + "integrity": "sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw==",
  2228 + "license": "MIT",
  2229 + "dependencies": {
  2230 + "@babel/runtime": "^7.10.1",
  2231 + "classnames": "^2.2.6",
  2232 + "rc-util": "^5.16.1"
  2233 + },
  2234 + "peerDependencies": {
  2235 + "react": ">=16.9.0",
  2236 + "react-dom": ">=16.9.0"
  2237 + }
  2238 + },
  2239 + "node_modules/rc-rate": {
  2240 + "version": "2.13.1",
  2241 + "resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.13.1.tgz",
  2242 + "integrity": "sha512-QUhQ9ivQ8Gy7mtMZPAjLbxBt5y9GRp65VcUyGUMF3N3fhiftivPHdpuDIaWIMOTEprAjZPC08bls1dQB+I1F2Q==",
  2243 + "license": "MIT",
  2244 + "dependencies": {
  2245 + "@babel/runtime": "^7.10.1",
  2246 + "classnames": "^2.2.5",
  2247 + "rc-util": "^5.0.1"
  2248 + },
  2249 + "engines": {
  2250 + "node": ">=8.x"
  2251 + },
  2252 + "peerDependencies": {
  2253 + "react": ">=16.9.0",
  2254 + "react-dom": ">=16.9.0"
  2255 + }
  2256 + },
  2257 + "node_modules/rc-resize-observer": {
  2258 + "version": "1.4.3",
  2259 + "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz",
  2260 + "integrity": "sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==",
  2261 + "license": "MIT",
  2262 + "dependencies": {
  2263 + "@babel/runtime": "^7.20.7",
  2264 + "classnames": "^2.2.1",
  2265 + "rc-util": "^5.44.1",
  2266 + "resize-observer-polyfill": "^1.5.1"
  2267 + },
  2268 + "peerDependencies": {
  2269 + "react": ">=16.9.0",
  2270 + "react-dom": ">=16.9.0"
  2271 + }
  2272 + },
  2273 + "node_modules/rc-segmented": {
  2274 + "version": "2.7.1",
  2275 + "resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.7.1.tgz",
  2276 + "integrity": "sha512-izj1Nw/Dw2Vb7EVr+D/E9lUTkBe+kKC+SAFSU9zqr7WV2W5Ktaa9Gc7cB2jTqgk8GROJayltaec+DBlYKc6d+g==",
  2277 + "license": "MIT",
  2278 + "dependencies": {
  2279 + "@babel/runtime": "^7.11.1",
  2280 + "classnames": "^2.2.1",
  2281 + "rc-motion": "^2.4.4",
  2282 + "rc-util": "^5.17.0"
  2283 + },
  2284 + "peerDependencies": {
  2285 + "react": ">=16.0.0",
  2286 + "react-dom": ">=16.0.0"
  2287 + }
  2288 + },
  2289 + "node_modules/rc-select": {
  2290 + "version": "14.16.8",
  2291 + "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.8.tgz",
  2292 + "integrity": "sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg==",
  2293 + "license": "MIT",
  2294 + "dependencies": {
  2295 + "@babel/runtime": "^7.10.1",
  2296 + "@rc-component/trigger": "^2.1.1",
  2297 + "classnames": "2.x",
  2298 + "rc-motion": "^2.0.1",
  2299 + "rc-overflow": "^1.3.1",
  2300 + "rc-util": "^5.16.1",
  2301 + "rc-virtual-list": "^3.5.2"
  2302 + },
  2303 + "engines": {
  2304 + "node": ">=8.x"
  2305 + },
  2306 + "peerDependencies": {
  2307 + "react": "*",
  2308 + "react-dom": "*"
  2309 + }
  2310 + },
  2311 + "node_modules/rc-slider": {
  2312 + "version": "11.1.9",
  2313 + "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.9.tgz",
  2314 + "integrity": "sha512-h8IknhzSh3FEM9u8ivkskh+Ef4Yo4JRIY2nj7MrH6GQmrwV6mcpJf5/4KgH5JaVI1H3E52yCdpOlVyGZIeph5A==",
  2315 + "license": "MIT",
  2316 + "dependencies": {
  2317 + "@babel/runtime": "^7.10.1",
  2318 + "classnames": "^2.2.5",
  2319 + "rc-util": "^5.36.0"
  2320 + },
  2321 + "engines": {
  2322 + "node": ">=8.x"
  2323 + },
  2324 + "peerDependencies": {
  2325 + "react": ">=16.9.0",
  2326 + "react-dom": ">=16.9.0"
  2327 + }
  2328 + },
  2329 + "node_modules/rc-steps": {
  2330 + "version": "6.0.1",
  2331 + "resolved": "https://registry.npmjs.org/rc-steps/-/rc-steps-6.0.1.tgz",
  2332 + "integrity": "sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g==",
  2333 + "license": "MIT",
  2334 + "dependencies": {
  2335 + "@babel/runtime": "^7.16.7",
  2336 + "classnames": "^2.2.3",
  2337 + "rc-util": "^5.16.1"
  2338 + },
  2339 + "engines": {
  2340 + "node": ">=8.x"
  2341 + },
  2342 + "peerDependencies": {
  2343 + "react": ">=16.9.0",
  2344 + "react-dom": ">=16.9.0"
  2345 + }
  2346 + },
  2347 + "node_modules/rc-switch": {
  2348 + "version": "4.1.0",
  2349 + "resolved": "https://registry.npmjs.org/rc-switch/-/rc-switch-4.1.0.tgz",
  2350 + "integrity": "sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg==",
  2351 + "license": "MIT",
  2352 + "dependencies": {
  2353 + "@babel/runtime": "^7.21.0",
  2354 + "classnames": "^2.2.1",
  2355 + "rc-util": "^5.30.0"
  2356 + },
  2357 + "peerDependencies": {
  2358 + "react": ">=16.9.0",
  2359 + "react-dom": ">=16.9.0"
  2360 + }
  2361 + },
  2362 + "node_modules/rc-table": {
  2363 + "version": "7.54.0",
  2364 + "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.54.0.tgz",
  2365 + "integrity": "sha512-/wDTkki6wBTjwylwAGjpLKYklKo9YgjZwAU77+7ME5mBoS32Q4nAwoqhA2lSge6fobLW3Tap6uc5xfwaL2p0Sw==",
  2366 + "license": "MIT",
  2367 + "dependencies": {
  2368 + "@babel/runtime": "^7.10.1",
  2369 + "@rc-component/context": "^1.4.0",
  2370 + "classnames": "^2.2.5",
  2371 + "rc-resize-observer": "^1.1.0",
  2372 + "rc-util": "^5.44.3",
  2373 + "rc-virtual-list": "^3.14.2"
  2374 + },
  2375 + "engines": {
  2376 + "node": ">=8.x"
  2377 + },
  2378 + "peerDependencies": {
  2379 + "react": ">=16.9.0",
  2380 + "react-dom": ">=16.9.0"
  2381 + }
  2382 + },
  2383 + "node_modules/rc-tabs": {
  2384 + "version": "15.7.0",
  2385 + "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.7.0.tgz",
  2386 + "integrity": "sha512-ZepiE+6fmozYdWf/9gVp7k56PKHB1YYoDsKeQA1CBlJ/POIhjkcYiv0AGP0w2Jhzftd3AVvZP/K+V+Lpi2ankA==",
  2387 + "license": "MIT",
  2388 + "dependencies": {
  2389 + "@babel/runtime": "^7.11.2",
  2390 + "classnames": "2.x",
  2391 + "rc-dropdown": "~4.2.0",
  2392 + "rc-menu": "~9.16.0",
  2393 + "rc-motion": "^2.6.2",
  2394 + "rc-resize-observer": "^1.0.0",
  2395 + "rc-util": "^5.34.1"
  2396 + },
  2397 + "engines": {
  2398 + "node": ">=8.x"
  2399 + },
  2400 + "peerDependencies": {
  2401 + "react": ">=16.9.0",
  2402 + "react-dom": ">=16.9.0"
  2403 + }
  2404 + },
  2405 + "node_modules/rc-textarea": {
  2406 + "version": "1.10.2",
  2407 + "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.10.2.tgz",
  2408 + "integrity": "sha512-HfaeXiaSlpiSp0I/pvWpecFEHpVysZ9tpDLNkxQbMvMz6gsr7aVZ7FpWP9kt4t7DB+jJXesYS0us1uPZnlRnwQ==",
  2409 + "license": "MIT",
  2410 + "dependencies": {
  2411 + "@babel/runtime": "^7.10.1",
  2412 + "classnames": "^2.2.1",
  2413 + "rc-input": "~1.8.0",
  2414 + "rc-resize-observer": "^1.0.0",
  2415 + "rc-util": "^5.27.0"
  2416 + },
  2417 + "peerDependencies": {
  2418 + "react": ">=16.9.0",
  2419 + "react-dom": ">=16.9.0"
  2420 + }
  2421 + },
  2422 + "node_modules/rc-tooltip": {
  2423 + "version": "6.4.0",
  2424 + "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.4.0.tgz",
  2425 + "integrity": "sha512-kqyivim5cp8I5RkHmpsp1Nn/Wk+1oeloMv9c7LXNgDxUpGm+RbXJGL+OPvDlcRnx9DBeOe4wyOIl4OKUERyH1g==",
  2426 + "license": "MIT",
  2427 + "dependencies": {
  2428 + "@babel/runtime": "^7.11.2",
  2429 + "@rc-component/trigger": "^2.0.0",
  2430 + "classnames": "^2.3.1",
  2431 + "rc-util": "^5.44.3"
  2432 + },
  2433 + "peerDependencies": {
  2434 + "react": ">=16.9.0",
  2435 + "react-dom": ">=16.9.0"
  2436 + }
  2437 + },
  2438 + "node_modules/rc-tree": {
  2439 + "version": "5.13.1",
  2440 + "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.13.1.tgz",
  2441 + "integrity": "sha512-FNhIefhftobCdUJshO7M8uZTA9F4OPGVXqGfZkkD/5soDeOhwO06T/aKTrg0WD8gRg/pyfq+ql3aMymLHCTC4A==",
  2442 + "license": "MIT",
  2443 + "dependencies": {
  2444 + "@babel/runtime": "^7.10.1",
  2445 + "classnames": "2.x",
  2446 + "rc-motion": "^2.0.1",
  2447 + "rc-util": "^5.16.1",
  2448 + "rc-virtual-list": "^3.5.1"
  2449 + },
  2450 + "engines": {
  2451 + "node": ">=10.x"
  2452 + },
  2453 + "peerDependencies": {
  2454 + "react": "*",
  2455 + "react-dom": "*"
  2456 + }
  2457 + },
  2458 + "node_modules/rc-tree-select": {
  2459 + "version": "5.27.0",
  2460 + "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.27.0.tgz",
  2461 + "integrity": "sha512-2qTBTzwIT7LRI1o7zLyrCzmo5tQanmyGbSaGTIf7sYimCklAToVVfpMC6OAldSKolcnjorBYPNSKQqJmN3TCww==",
  2462 + "license": "MIT",
  2463 + "dependencies": {
  2464 + "@babel/runtime": "^7.25.7",
  2465 + "classnames": "2.x",
  2466 + "rc-select": "~14.16.2",
  2467 + "rc-tree": "~5.13.0",
  2468 + "rc-util": "^5.43.0"
  2469 + },
  2470 + "peerDependencies": {
  2471 + "react": "*",
  2472 + "react-dom": "*"
  2473 + }
  2474 + },
  2475 + "node_modules/rc-upload": {
  2476 + "version": "4.11.0",
  2477 + "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.11.0.tgz",
  2478 + "integrity": "sha512-ZUyT//2JAehfHzjWowqROcwYJKnZkIUGWaTE/VogVrepSl7AFNbQf4+zGfX4zl9Vrj/Jm8scLO0R6UlPDKK4wA==",
  2479 + "license": "MIT",
  2480 + "dependencies": {
  2481 + "@babel/runtime": "^7.18.3",
  2482 + "classnames": "^2.2.5",
  2483 + "rc-util": "^5.2.0"
  2484 + },
  2485 + "peerDependencies": {
  2486 + "react": ">=16.9.0",
  2487 + "react-dom": ">=16.9.0"
  2488 + }
  2489 + },
  2490 + "node_modules/rc-util": {
  2491 + "version": "5.44.4",
  2492 + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.4.tgz",
  2493 + "integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==",
  2494 + "license": "MIT",
  2495 + "dependencies": {
  2496 + "@babel/runtime": "^7.18.3",
  2497 + "react-is": "^18.2.0"
  2498 + },
  2499 + "peerDependencies": {
  2500 + "react": ">=16.9.0",
  2501 + "react-dom": ">=16.9.0"
  2502 + }
  2503 + },
  2504 + "node_modules/rc-virtual-list": {
  2505 + "version": "3.19.2",
  2506 + "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.19.2.tgz",
  2507 + "integrity": "sha512-Ys6NcjwGkuwkeaWBDqfI3xWuZ7rDiQXlH1o2zLfFzATfEgXcqpk8CkgMfbJD81McqjcJVez25a3kPxCR807evA==",
  2508 + "license": "MIT",
  2509 + "dependencies": {
  2510 + "@babel/runtime": "^7.20.0",
  2511 + "classnames": "^2.2.6",
  2512 + "rc-resize-observer": "^1.0.0",
  2513 + "rc-util": "^5.36.0"
  2514 + },
  2515 + "engines": {
  2516 + "node": ">=8.x"
  2517 + },
  2518 + "peerDependencies": {
  2519 + "react": ">=16.9.0",
  2520 + "react-dom": ">=16.9.0"
  2521 + }
  2522 + },
  2523 + "node_modules/react": {
  2524 + "version": "18.3.1",
  2525 + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
  2526 + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
  2527 + "license": "MIT",
  2528 + "dependencies": {
  2529 + "loose-envify": "^1.1.0"
  2530 + },
  2531 + "engines": {
  2532 + "node": ">=0.10.0"
  2533 + }
  2534 + },
  2535 + "node_modules/react-dom": {
  2536 + "version": "18.3.1",
  2537 + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
  2538 + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
  2539 + "license": "MIT",
  2540 + "dependencies": {
  2541 + "loose-envify": "^1.1.0",
  2542 + "scheduler": "^0.23.2"
  2543 + },
  2544 + "peerDependencies": {
  2545 + "react": "^18.3.1"
  2546 + }
  2547 + },
  2548 + "node_modules/react-is": {
  2549 + "version": "18.3.1",
  2550 + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
  2551 + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
  2552 + "license": "MIT"
  2553 + },
  2554 + "node_modules/react-refresh": {
  2555 + "version": "0.17.0",
  2556 + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
  2557 + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
  2558 + "dev": true,
  2559 + "license": "MIT",
  2560 + "engines": {
  2561 + "node": ">=0.10.0"
  2562 + }
  2563 + },
  2564 + "node_modules/resize-observer-polyfill": {
  2565 + "version": "1.5.1",
  2566 + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
  2567 + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
  2568 + "license": "MIT"
  2569 + },
  2570 + "node_modules/rollup": {
  2571 + "version": "4.62.2",
  2572 + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz",
  2573 + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==",
  2574 + "dev": true,
  2575 + "license": "MIT",
  2576 + "dependencies": {
  2577 + "@types/estree": "1.0.9"
  2578 + },
  2579 + "bin": {
  2580 + "rollup": "dist/bin/rollup"
  2581 + },
  2582 + "engines": {
  2583 + "node": ">=18.0.0",
  2584 + "npm": ">=8.0.0"
  2585 + },
  2586 + "optionalDependencies": {
  2587 + "@rollup/rollup-android-arm-eabi": "4.62.2",
  2588 + "@rollup/rollup-android-arm64": "4.62.2",
  2589 + "@rollup/rollup-darwin-arm64": "4.62.2",
  2590 + "@rollup/rollup-darwin-x64": "4.62.2",
  2591 + "@rollup/rollup-freebsd-arm64": "4.62.2",
  2592 + "@rollup/rollup-freebsd-x64": "4.62.2",
  2593 + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2",
  2594 + "@rollup/rollup-linux-arm-musleabihf": "4.62.2",
  2595 + "@rollup/rollup-linux-arm64-gnu": "4.62.2",
  2596 + "@rollup/rollup-linux-arm64-musl": "4.62.2",
  2597 + "@rollup/rollup-linux-loong64-gnu": "4.62.2",
  2598 + "@rollup/rollup-linux-loong64-musl": "4.62.2",
  2599 + "@rollup/rollup-linux-ppc64-gnu": "4.62.2",
  2600 + "@rollup/rollup-linux-ppc64-musl": "4.62.2",
  2601 + "@rollup/rollup-linux-riscv64-gnu": "4.62.2",
  2602 + "@rollup/rollup-linux-riscv64-musl": "4.62.2",
  2603 + "@rollup/rollup-linux-s390x-gnu": "4.62.2",
  2604 + "@rollup/rollup-linux-x64-gnu": "4.62.2",
  2605 + "@rollup/rollup-linux-x64-musl": "4.62.2",
  2606 + "@rollup/rollup-openbsd-x64": "4.62.2",
  2607 + "@rollup/rollup-openharmony-arm64": "4.62.2",
  2608 + "@rollup/rollup-win32-arm64-msvc": "4.62.2",
  2609 + "@rollup/rollup-win32-ia32-msvc": "4.62.2",
  2610 + "@rollup/rollup-win32-x64-gnu": "4.62.2",
  2611 + "@rollup/rollup-win32-x64-msvc": "4.62.2",
  2612 + "fsevents": "~2.3.2"
  2613 + }
  2614 + },
  2615 + "node_modules/scheduler": {
  2616 + "version": "0.23.2",
  2617 + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
  2618 + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
  2619 + "license": "MIT",
  2620 + "dependencies": {
  2621 + "loose-envify": "^1.1.0"
  2622 + }
  2623 + },
  2624 + "node_modules/scroll-into-view-if-needed": {
  2625 + "version": "3.1.0",
  2626 + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz",
  2627 + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==",
  2628 + "license": "MIT",
  2629 + "dependencies": {
  2630 + "compute-scroll-into-view": "^3.0.2"
  2631 + }
  2632 + },
  2633 + "node_modules/semver": {
  2634 + "version": "6.3.1",
  2635 + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
  2636 + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
  2637 + "dev": true,
  2638 + "license": "ISC",
  2639 + "bin": {
  2640 + "semver": "bin/semver.js"
  2641 + }
  2642 + },
  2643 + "node_modules/source-map-js": {
  2644 + "version": "1.2.1",
  2645 + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
  2646 + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
  2647 + "dev": true,
  2648 + "license": "BSD-3-Clause",
  2649 + "engines": {
  2650 + "node": ">=0.10.0"
  2651 + }
  2652 + },
  2653 + "node_modules/string-convert": {
  2654 + "version": "0.2.1",
  2655 + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz",
  2656 + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==",
  2657 + "license": "MIT"
  2658 + },
  2659 + "node_modules/stylis": {
  2660 + "version": "4.4.0",
  2661 + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz",
  2662 + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==",
  2663 + "license": "MIT"
  2664 + },
  2665 + "node_modules/throttle-debounce": {
  2666 + "version": "5.0.2",
  2667 + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz",
  2668 + "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==",
  2669 + "license": "MIT",
  2670 + "engines": {
  2671 + "node": ">=12.22"
  2672 + }
  2673 + },
  2674 + "node_modules/toggle-selection": {
  2675 + "version": "1.0.6",
  2676 + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
  2677 + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==",
  2678 + "license": "MIT"
  2679 + },
  2680 + "node_modules/typescript": {
  2681 + "version": "5.9.3",
  2682 + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
  2683 + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
  2684 + "dev": true,
  2685 + "license": "Apache-2.0",
  2686 + "bin": {
  2687 + "tsc": "bin/tsc",
  2688 + "tsserver": "bin/tsserver"
  2689 + },
  2690 + "engines": {
  2691 + "node": ">=14.17"
  2692 + }
  2693 + },
  2694 + "node_modules/update-browserslist-db": {
  2695 + "version": "1.2.3",
  2696 + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
  2697 + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
  2698 + "dev": true,
  2699 + "funding": [
  2700 + {
  2701 + "type": "opencollective",
  2702 + "url": "https://opencollective.com/browserslist"
  2703 + },
  2704 + {
  2705 + "type": "tidelift",
  2706 + "url": "https://tidelift.com/funding/github/npm/browserslist"
  2707 + },
  2708 + {
  2709 + "type": "github",
  2710 + "url": "https://github.com/sponsors/ai"
  2711 + }
  2712 + ],
  2713 + "license": "MIT",
  2714 + "dependencies": {
  2715 + "escalade": "^3.2.0",
  2716 + "picocolors": "^1.1.1"
  2717 + },
  2718 + "bin": {
  2719 + "update-browserslist-db": "cli.js"
  2720 + },
  2721 + "peerDependencies": {
  2722 + "browserslist": ">= 4.21.0"
  2723 + }
  2724 + },
  2725 + "node_modules/vite": {
  2726 + "version": "5.4.21",
  2727 + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
  2728 + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
  2729 + "dev": true,
  2730 + "license": "MIT",
  2731 + "dependencies": {
  2732 + "esbuild": "^0.21.3",
  2733 + "postcss": "^8.4.43",
  2734 + "rollup": "^4.20.0"
  2735 + },
  2736 + "bin": {
  2737 + "vite": "bin/vite.js"
  2738 + },
  2739 + "engines": {
  2740 + "node": "^18.0.0 || >=20.0.0"
  2741 + },
  2742 + "funding": {
  2743 + "url": "https://github.com/vitejs/vite?sponsor=1"
  2744 + },
  2745 + "optionalDependencies": {
  2746 + "fsevents": "~2.3.3"
  2747 + },
  2748 + "peerDependencies": {
  2749 + "@types/node": "^18.0.0 || >=20.0.0",
  2750 + "less": "*",
  2751 + "lightningcss": "^1.21.0",
  2752 + "sass": "*",
  2753 + "sass-embedded": "*",
  2754 + "stylus": "*",
  2755 + "sugarss": "*",
  2756 + "terser": "^5.4.0"
  2757 + },
  2758 + "peerDependenciesMeta": {
  2759 + "@types/node": {
  2760 + "optional": true
  2761 + },
  2762 + "less": {
  2763 + "optional": true
  2764 + },
  2765 + "lightningcss": {
  2766 + "optional": true
  2767 + },
  2768 + "sass": {
  2769 + "optional": true
  2770 + },
  2771 + "sass-embedded": {
  2772 + "optional": true
  2773 + },
  2774 + "stylus": {
  2775 + "optional": true
  2776 + },
  2777 + "sugarss": {
  2778 + "optional": true
  2779 + },
  2780 + "terser": {
  2781 + "optional": true
  2782 + }
  2783 + }
  2784 + },
  2785 + "node_modules/yallist": {
  2786 + "version": "3.1.1",
  2787 + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
  2788 + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
  2789 + "dev": true,
  2790 + "license": "ISC"
  2791 + }
  2792 + }
  2793 +}
... ...
web/package.json 0 → 100644
  1 +++ a/web/package.json
  1 +{
  2 + "name": "onto-web",
  3 + "private": true,
  4 + "version": "2.0.0",
  5 + "type": "module",
  6 + "description": "本体驱动前端通用渲染引擎:读取 M8 模板动态生成页面,无业务硬编码",
  7 + "scripts": {
  8 + "dev": "vite",
  9 + "build": "vite build",
  10 + "preview": "vite preview --port 5173"
  11 + },
  12 + "dependencies": {
  13 + "@ant-design/icons": "^5.5.1",
  14 + "antd": "^5.21.6",
  15 + "dayjs": "^1.11.13",
  16 + "react": "^18.3.1",
  17 + "react-dom": "^18.3.1"
  18 + },
  19 + "devDependencies": {
  20 + "@types/react": "^18.3.12",
  21 + "@types/react-dom": "^18.3.1",
  22 + "@vitejs/plugin-react": "^4.3.3",
  23 + "typescript": "^5.6.3",
  24 + "vite": "^5.4.10"
  25 + }
  26 +}
... ...
web/src/App.tsx 0 → 100644
  1 +++ a/web/src/App.tsx
  1 +import { useEffect, useState } from 'react'
  2 +import { Layout, Card, Tabs, Button, Space, Typography, Tag, message, Spin, Modal, Select } from 'antd'
  3 +import { ReloadOutlined, ThunderboltOutlined, BranchesOutlined, UserOutlined } from '@ant-design/icons'
  4 +import { api } from './api'
  5 +import type { SceneSchema } from './types'
  6 +import { SceneForm } from './engine/SceneForm'
  7 +import { TracePanel, ProvenancePanel, ModelViewer, OrdersEventsPanel } from './panels'
  8 +
  9 +const { Header, Sider, Content } = Layout
  10 +const { Title, Text, Paragraph } = Typography
  11 +
  12 +export default function App() {
  13 + const [scenes, setScenes] = useState<any[]>([])
  14 + const [sceneId, setSceneId] = useState<string>('') // 初始不硬编码,待场景列表返回后取首个可渲染场景
  15 + const [schema, setSchema] = useState<SceneSchema | null>(null)
  16 + const [loading, setLoading] = useState(false)
  17 + const [result, setResult] = useState<any>(null)
  18 + const [refreshKey, setRefreshKey] = useState(0)
  19 + const [formKey, setFormKey] = useState(0)
  20 + // 演示态主体(M5 principal):随手切换以观察权限"通过/拒绝"分支;管理员方可热重载/新增客户
  21 + const [principal, setPrincipal] = useState<string>('backend_admin')
  22 +
  23 + // 拉取场景列表(首个可渲染场景兜底选中;已有选择则保留,以便热重载后停留在当前场景)
  24 + const loadScenes = () =>
  25 + api.scenes().then((list) => {
  26 + setScenes(list)
  27 + setSceneId((cur) => cur || list.find((s: any) => s.hasTemplate)?.sceneId || '')
  28 + }).catch(() => message.error('无法连接引擎(8080),请先启动后端'))
  29 +
  30 + useEffect(() => { loadScenes() }, [])
  31 +
  32 + useEffect(() => {
  33 + if (!sceneId) return
  34 + setLoading(true); setResult(null)
  35 + api.sceneSchema(sceneId).then(setSchema).catch((e) => message.error('加载场景失败:' + e.message)).finally(() => setLoading(false))
  36 + }, [sceneId, formKey])
  37 +
  38 + async function reloadModels() {
  39 + try {
  40 + const r = await api.reload(principal)
  41 + message.success(r.message)
  42 + await loadScenes() // 重新拉取场景列表:模板增删 / hasTemplate 变化即时反映到侧栏
  43 + setFormKey((k) => k + 1) // 强制重新拉取当前场景 schema + 重挂载表单
  44 + } catch (e: any) { message.error('热重载失败:' + e.message) }
  45 + }
  46 +
  47 + const onExecuted = (r: any) => { setResult(r); if (r.success) setRefreshKey((k) => k + 1) }
  48 +
  49 + return (
  50 + <Layout style={{ minHeight: '100vh' }}>
  51 + <Header style={{ background: '#001529', display: 'flex', alignItems: 'center', gap: 16 }}>
  52 + <BranchesOutlined style={{ color: '#69b1ff', fontSize: 22 }} />
  53 + <Title level={4} style={{ color: '#fff', margin: 0 }}>本体驱动 DDD+EDA · 元数据引擎 Demo</Title>
  54 + <Tag color="blue">改 YAML → 页面与行为随之变</Tag>
  55 + <div style={{ flex: 1 }} />
  56 + <Space>
  57 + <UserOutlined style={{ color: '#69b1ff' }} />
  58 + <Select size="small" value={principal} onChange={setPrincipal} style={{ width: 140 }}
  59 + options={[{ value: 'backend_admin', label: '后台管理员' }, { value: 'normal_user', label: '普通用户' }]} />
  60 + </Space>
  61 + <Button icon={<ReloadOutlined />} onClick={reloadModels} ghost>热重载模型</Button>
  62 + <HelpButton />
  63 + </Header>
  64 + <Layout>
  65 + <Sider width={260} theme="light" style={{ padding: 12 }}>
  66 + <Text type="secondary">业务场景(M4)</Text>
  67 + <div style={{ marginTop: 8 }}>
  68 + {scenes.map((s) => {
  69 + const active = sceneId === s.sceneId
  70 + return (
  71 + <div
  72 + key={s.sceneId}
  73 + onClick={() => setSceneId(s.sceneId)}
  74 + style={{
  75 + padding: '10px 12px',
  76 + marginBottom: 8,
  77 + borderRadius: 8,
  78 + cursor: 'pointer',
  79 + background: active ? '#e6f4ff' : '#fff',
  80 + border: `1px solid ${active ? '#91caff' : '#f0f0f0'}`,
  81 + transition: 'all .2s',
  82 + }}
  83 + >
  84 + <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
  85 + <ThunderboltOutlined style={{ color: active ? '#1677ff' : '#999' }} />
  86 + <span style={{ fontWeight: 500, color: active ? '#1677ff' : 'inherit' }}>{s.sceneName}</span>
  87 + </div>
  88 + <div style={{ fontSize: 11, color: '#999', marginTop: 4, marginLeft: 22 }}>
  89 + {s.hasTemplate ? s.templateName : '无表单 · 直接运行 flowSteps'}
  90 + </div>
  91 + </div>
  92 + )
  93 + })}
  94 + </div>
  95 + <Paragraph type="secondary" style={{ fontSize: 12, marginTop: 16 }}>
  96 + 业务场景均由 M8 模板动态渲染并端到端执行(订单/客户/商品/审核…)。
  97 + 末位「演示:全部 stepType」无表单,点开可直接运行、观察引擎逐类步骤的通用解释。
  98 + 部分场景需管理员主体(M5 权限,可在右上切换)。
  99 + </Paragraph>
  100 + </Sider>
  101 +
  102 + <Content style={{ padding: 16 }}>
  103 + <Spin spinning={loading}>
  104 + <div style={{ display: 'flex', gap: 16, alignItems: 'flex-start' }}>
  105 + <Card style={{ flex: '1 1 62%' }} title={<Space><b>{schema?.sceneName || '加载中'}</b><Tag color="green">{sceneId}</Tag></Space>}>
  106 + {schema?.template ? <SceneForm key={formKey + sceneId} schema={schema} onExecuted={onExecuted} principal={principal} />
  107 + : schema ? <RunOnlyScene key={sceneId} schema={schema} onExecuted={onExecuted} principal={principal} />
  108 + : null}
  109 + </Card>
  110 +
  111 + <Card style={{ flex: '1 1 38%', position: 'sticky', top: 16 }} styles={{ body: { paddingTop: 8 } }}>
  112 + <Tabs
  113 + items={[
  114 + { key: 'trace', label: '执行追踪', children: <TracePanel result={result} /> },
  115 + { key: 'prov', label: '溯源/规则', children: schema ? <ProvenancePanel schema={schema} /> : null },
  116 + { key: 'orders', label: '数据/事件', children: <OrdersEventsPanel refreshKey={refreshKey} aggregateId={schema?.template?.masterAggregate || schema?.aggregateScope?.[0]} /> },
  117 + { key: 'model', label: '模型查看器', children: <ModelViewer /> },
  118 + ]} />
  119 + </Card>
  120 + </div>
  121 + </Spin>
  122 + </Content>
  123 + </Layout>
  124 + </Layout>
  125 + )
  126 +}
  127 +
  128 +/**
  129 + * 无表单场景运行器:用于未绑定 M8 模板的场景(如"演示:全部 stepType",其字段为合成变量、非 M1 领域字段)。
  130 + * 直接以空 payload 调用同一执行接口,引擎会逐条"通用解释"M4 flowSteps,产出可读执行轨迹(见右侧「执行追踪」)。
  131 + */
  132 +function RunOnlyScene({ schema, onExecuted, principal }:
  133 + { schema: SceneSchema; onExecuted: (r: any) => void; principal: string }) {
  134 + const [running, setRunning] = useState(false)
  135 + async function run() {
  136 + setRunning(true)
  137 + try {
  138 + const r = await api.execute(schema.sceneId, { master: {}, details: {} }, principal)
  139 + onExecuted(r)
  140 + if (r.success) message.success('场景执行完成,见右侧「执行追踪」')
  141 + else message.error(`${r.stage} · ${r.message}`)
  142 + } catch (e: any) {
  143 + message.error('执行失败:' + e.message)
  144 + } finally {
  145 + setRunning(false)
  146 + }
  147 + }
  148 + return (
  149 + <div>
  150 + <Space wrap style={{ marginBottom: 12 }}>
  151 + <Tag color="purple">场景 {schema.sceneId}</Tag>
  152 + <Tag color="geekblue">无 M8 表单模板</Tag>
  153 + <Tag color="gold">权限 {schema.permissionBind?.join(',')}</Tag>
  154 + </Space>
  155 + <Paragraph type="secondary">
  156 + 该场景的字段为演示用合成变量(非 M1 领域字段),故不绑定 M8 表单模板。点下方按钮,引擎会以空 payload
  157 + 逐条<b>通用解释</b> M4 <Text code>flowSteps</Text> —— 起止 / 数据与派生 / 控制流(condition·switch·while·parallel)
  158 + / Saga 可靠性(retry·compensate·timer)/ EDA(emitEvent·waitEvent)/ 人工任务,产出可读执行轨迹。
  159 + </Paragraph>
  160 + <Button type="primary" loading={running} onClick={run}>运行(无表单,直接解释 flowSteps)</Button>
  161 + </div>
  162 + )
  163 +}
  164 +
  165 +function HelpButton() {
  166 + const [open, setOpen] = useState(false)
  167 + return (
  168 + <>
  169 + <Button ghost onClick={() => setOpen(true)}>验证「改模型→改行为」</Button>
  170 + <Modal open={open} onCancel={() => setOpen(false)} footer={null} width={720} title="如何验证:改了模型文件,操作定义同时改变">
  171 + <Paragraph>本 Demo 的核心是<b>元数据驱动</b>:前端表单与后端行为都由 <Text code>models/*.yaml</Text> 实时解释,改模型即改行为,无需改任何代码。试试:</Paragraph>
  172 + <ol style={{ lineHeight: 2 }}>
  173 + <li>编辑 <Text code>models/M8-front-schema.yaml</Text>:给订单主信息卡片再加一个绑定 M1 字段的组件(如 totalAmount 旁加一个 input 绑 totalCurrency 的副本),或删掉某字段 → 点顶部<b>「热重载模型」</b> → 表单立即多/少一个字段。</li>
  174 + <li>编辑 <Text code>models/MetaRule-business.yaml</Text>:把 <Text code>single_order_max_amount</Text> 从 50000 改为 100 → 热重载 → 普通用户下单超 100 即被 <Tag color="red">REJECT</Tag>。</li>
  175 + <li>编辑 <Text code>models/M1-domain.yaml</Text>:把某字段 type 由 string 改为 int → 热重载 → 该组件输入类型随之改变;配合 M3 增列后可落库。</li>
  176 + <li>编辑 <Text code>models/M5-security.yaml</Text> 的 maskRules → 热重载 → 脱敏展示规则随之变化。</li>
  177 + </ol>
  178 + <Paragraph type="secondary">「模型查看器」标签页可实时查看引擎当前加载的各层模型内容。</Paragraph>
  179 + </Modal>
  180 + </>
  181 + )
  182 +}
... ...
web/src/api.ts 0 → 100644
  1 +++ a/web/src/api.ts
  1 +// 引擎 API 客户端。所有业务 URL 均由后端场景 Schema 给出(commandApi/optionsSource),前端不硬编码业务地址。
  2 +// 主体身份经 X-Principal 头传递(演示态;生产应换真实认证)。
  3 +const BASE = '/api'
  4 +
  5 +async function get<T = any>(path: string): Promise<T> {
  6 + const r = await fetch(BASE + path)
  7 + if (!r.ok) throw new Error(`GET ${path} -> ${r.status}`)
  8 + return r.json()
  9 +}
  10 +
  11 +async function post<T = any>(path: string, body: any, principal?: string): Promise<T> {
  12 + const headers: Record<string, string> = { 'Content-Type': 'application/json' }
  13 + if (principal) headers['X-Principal'] = principal
  14 + const r = await fetch(BASE + path, { method: 'POST', headers, body: JSON.stringify(body) })
  15 + if (!r.ok) {
  16 + let msg = `POST ${path} -> ${r.status}`
  17 + try { const j = await r.json(); if (j?.message) msg = j.message } catch { /* ignore */ }
  18 + throw new Error(msg)
  19 + }
  20 + return r.json()
  21 +}
  22 +
  23 +export const api = {
  24 + scenes: () => get('/meta/scenes'),
  25 + sceneSchema: (sceneId: string) => get(`/meta/scene/${sceneId}`),
  26 + aggregate: (aggregateId: string) => get(`/meta/aggregate/${aggregateId}`),
  27 + models: () => get('/meta/models'),
  28 + model: (code: string) => get(`/meta/model/${code}`),
  29 + reload: (principal?: string) => post('/meta/reload', {}, principal),
  30 + options: (aggregateId: string) => get(`/data/${aggregateId}`),
  31 + list: (aggregateId: string) => get(`/data/${aggregateId}/list`),
  32 + events: () => get('/data/events/recent'),
  33 + precheck: (sceneId: string, payload: any, principal?: string) => post(`/scene/${sceneId}/precheck`, payload, principal),
  34 + execute: (sceneId: string, payload: any, principal?: string) => post(`/scene/${sceneId}/execute`, payload, principal),
  35 +}
... ...
web/src/engine/FieldControl.tsx 0 → 100644
  1 +++ a/web/src/engine/FieldControl.tsx
  1 +import { Input, InputNumber, DatePicker, Switch, Select, Tooltip } from 'antd'
  2 +import { LockOutlined } from '@ant-design/icons'
  3 +import dayjs from 'dayjs'
  4 +import type { DragComponent, OptionItem } from '../types'
  5 +
  6 +/**
  7 + * 组件注册表:把 M8 的 compType 原子组件映射到 Ant Design 控件。
  8 + * 这是"前端通用渲染引擎"的最底层——新增/替换控件只需在此登记,业务页面零改动。
  9 + */
  10 +export function FieldControl({
  11 + comp,
  12 + value,
  13 + onChange,
  14 + optionsMap,
  15 +}: {
  16 + comp: DragComponent
  17 + value: any
  18 + onChange: (v: any) => void
  19 + optionsMap: Record<string, OptionItem[]>
  20 +}) {
  21 + const fb = comp.fieldBind || ({} as any)
  22 + const placeholder = fb.placeholder || `请输入${fb.formLabel || ''}`
  23 +
  24 + // M1 类型兜底:当 M8 用通用文本控件、但领域类型是数值/布尔/时间时,据 M1 fieldType 升级控件。
  25 + // 于是"改 M1 的 type" 也会改变渲染出的控件(兑现 Help 步骤 3)。
  26 + const ft = fb.fieldType
  27 + const generic = comp.compType === 'input' || comp.compType === 'text_area'
  28 + if (generic && (ft === 'int' || ft === 'decimal')) {
  29 + return <InputNumber style={{ width: '100%' }} value={value} onChange={onChange} placeholder={placeholder} />
  30 + }
  31 + if (generic && ft === 'boolean') return <Switch checked={!!value} onChange={onChange} />
  32 + if (generic && ft === 'datetime') {
  33 + return (
  34 + <DatePicker style={{ width: '100%' }} showTime value={value ? dayjs(value) : null}
  35 + onChange={(d) => onChange(d ? d.format('YYYY-MM-DDTHH:mm:ss') : null)} />
  36 + )
  37 + }
  38 +
  39 + switch (comp.compType) {
  40 + case 'number_input':
  41 + return <InputNumber style={{ width: '100%' }} value={value} onChange={onChange} placeholder={placeholder} />
  42 +
  43 + case 'date_picker':
  44 + return (
  45 + <DatePicker
  46 + style={{ width: '100%' }}
  47 + showTime
  48 + value={value ? dayjs(value) : null}
  49 + onChange={(d) => onChange(d ? d.format('YYYY-MM-DDTHH:mm:ss') : null)}
  50 + />
  51 + )
  52 +
  53 + case 'switch':
  54 + return <Switch checked={!!value} onChange={onChange} />
  55 +
  56 + case 'select': {
  57 + if (fb.optionsSource) {
  58 + const opts = optionsMap[fb.optionsSource.aggregateId] || []
  59 + return (
  60 + <Select
  61 + style={{ width: '100%' }}
  62 + showSearch
  63 + optionFilterProp="label"
  64 + placeholder={`请选择${fb.formLabel || ''}`}
  65 + value={value ?? undefined}
  66 + onChange={onChange}
  67 + options={opts.map((o) => ({ value: o.value, label: o.label }))}
  68 + />
  69 + )
  70 + }
  71 + // 绑定到普通字符串字段的 select 在 M1 中无枚举来源,通用引擎降级为可输入选择
  72 + return (
  73 + <Select
  74 + style={{ width: '100%' }}
  75 + mode="tags"
  76 + maxCount={1}
  77 + placeholder={`请输入${fb.formLabel || ''}`}
  78 + value={value ? [value] : []}
  79 + onChange={(arr: string[]) => onChange(arr[arr.length - 1] ?? null)}
  80 + />
  81 + )
  82 + }
  83 +
  84 + case 'text_area':
  85 + return <Input.TextArea value={value} onChange={(e) => onChange(e.target.value)} placeholder={placeholder} />
  86 +
  87 + case 'input':
  88 + default:
  89 + return (
  90 + <Input
  91 + value={value}
  92 + onChange={(e) => onChange(e.target.value)}
  93 + placeholder={placeholder}
  94 + suffix={fb.masked ? (
  95 + <Tooltip title="M5 敏感字段:存储加密、展示脱敏">
  96 + <LockOutlined style={{ color: '#faad14' }} />
  97 + </Tooltip>
  98 + ) : <span />}
  99 + />
  100 + )
  101 + }
  102 +}
... ...
web/src/engine/SceneForm.tsx 0 → 100644
  1 +++ a/web/src/engine/SceneForm.tsx
  1 +import { useEffect, useMemo, useState } from 'react'
  2 +import {
  3 + Card, Row, Col, Form, Button, Table, Space, Tag, Typography, Alert, message, Divider, Empty,
  4 +} from 'antd'
  5 +import { PlusOutlined, DeleteOutlined } from '@ant-design/icons'
  6 +import { api } from '../api'
  7 +import type { DragComponent, OptionItem, SceneSchema } from '../types'
  8 +import { FieldControl } from './FieldControl'
  9 +
  10 +const { Text } = Typography
  11 +
  12 +type Role = 'master' | 'single' | 'table'
  13 +
  14 +function roleOf(container: DragComponent): Role {
  15 + const fb = container.fieldBind || ({} as any)
  16 + if (fb.bindSourceType === 'aggregate_root') return 'master'
  17 + if (container.compType === 'table') return 'table'
  18 + return 'single'
  19 +}
  20 +
  21 +function collectOptionSources(comps: DragComponent[], acc: Set<string>) {
  22 + for (const c of comps) {
  23 + const src = c.fieldBind?.optionsSource
  24 + if (src) acc.add(src.aggregateId)
  25 + if (c.childComponents) collectOptionSources(c.childComponents, acc)
  26 + }
  27 +}
  28 +
  29 +/**
  30 + * 场景通用渲染引擎:读取合成后的场景 Schema,动态生成表单。
  31 + * 完全由 M8 模板结构驱动——主表(aggregate_root)渲染为卡片字段,
  32 + * 子实体(aggregate_entity)按 table/card 渲染为多行表格或单行卡片。改模板即改页面。
  33 + */
  34 +export function SceneForm({ schema, onExecuted, principal }:
  35 + { schema: SceneSchema; onExecuted: (r: any) => void; principal: string }) {
  36 + const template = schema.template!
  37 + const containers = template.rootComponents || []
  38 +
  39 + const [master, setMaster] = useState<Record<string, any>>({})
  40 + const [singles, setSingles] = useState<Record<string, Record<string, any>>>({})
  41 + const [tables, setTables] = useState<Record<string, Array<Record<string, any>>>>({})
  42 + const [optionsMap, setOptionsMap] = useState<Record<string, OptionItem[]>>({})
  43 + const [precheck, setPrecheck] = useState<any>(null)
  44 + const [submitting, setSubmitting] = useState(false)
  45 +
  46 + // 初始化表单状态(据模板结构)
  47 + useEffect(() => {
  48 + const s: Record<string, Record<string, any>> = {}
  49 + const t: Record<string, Array<Record<string, any>>> = {}
  50 + for (const c of containers) {
  51 + const role = roleOf(c)
  52 + const entity = c.fieldBind?.domainEntityName
  53 + if (role === 'single' && entity) s[entity] = {}
  54 + if (role === 'table' && entity) t[entity] = [{}]
  55 + }
  56 + setMaster({})
  57 + setSingles(s)
  58 + setTables(t)
  59 + setPrecheck(null)
  60 + // 拉取所有引用下拉数据源
  61 + const aggs = new Set<string>()
  62 + collectOptionSources(containers, aggs)
  63 + Promise.all([...aggs].map((a) => api.options(a).then((opts) => [a, opts] as const)))
  64 + .then((pairs) => setOptionsMap(Object.fromEntries(pairs)))
  65 + .catch(() => {})
  66 + }, [schema.sceneId])
  67 +
  68 + function buildPayload() {
  69 + const details: Record<string, any[]> = {}
  70 + for (const c of containers) {
  71 + const role = roleOf(c)
  72 + const entity = c.fieldBind?.domainEntityName
  73 + if (!entity) continue
  74 + if (role === 'table') details[entity] = tables[entity] || []
  75 + if (role === 'single') details[entity] = [singles[entity] || {}]
  76 + }
  77 + return { master, details }
  78 + }
  79 +
  80 + async function doPrecheck() {
  81 + try {
  82 + const r = await api.precheck(schema.sceneId, buildPayload(), principal)
  83 + setPrecheck(r)
  84 + if (r.reject) message.warning('预检未通过,请查看提示')
  85 + else message.success('预检通过,可提交')
  86 + } catch (e: any) {
  87 + message.error('预检失败:' + e.message)
  88 + }
  89 + }
  90 +
  91 + async function doSubmit() {
  92 + setSubmitting(true)
  93 + try {
  94 + const r = await api.execute(schema.sceneId, buildPayload(), principal)
  95 + onExecuted(r)
  96 + if (r.success) {
  97 + message.success(`${schema.sceneName}成功:${r.rootId ?? ''}`)
  98 + setPrecheck(null)
  99 + } else {
  100 + message.error(`${r.stage} · ${r.message}`)
  101 + setPrecheck({ reject: true, errors: r.errors || [], rejectRules: r.appliedRules?.filter((x: any) => x.effect === 'REJECT') || [], warnings: r.warnings || [] })
  102 + }
  103 + } catch (e: any) {
  104 + message.error('提交失败:' + e.message)
  105 + } finally {
  106 + setSubmitting(false)
  107 + }
  108 + }
  109 +
  110 + return (
  111 + <div>
  112 + <Space wrap style={{ marginBottom: 12 }}>
  113 + <Tag color="blue">模板 {template.templateId}</Tag>
  114 + <Tag color="geekblue">{template.templateType}</Tag>
  115 + <Tag color="purple">主聚合 {template.masterAggregate}</Tag>
  116 + <Tag color="cyan">命令 API {schema.commandApi}</Tag>
  117 + <Tag color="gold">权限 {schema.permissionBind?.join(',')}</Tag>
  118 + </Space>
  119 +
  120 + {precheck && (precheck.reject || precheck.warnings?.length > 0) && (
  121 + <Alert
  122 + style={{ marginBottom: 12 }}
  123 + type={precheck.reject ? 'error' : 'warning'}
  124 + showIcon
  125 + message={precheck.reject ? '预检拦截(MetaRule / 校验)' : '预检提示'}
  126 + description={
  127 + <div>
  128 + {(precheck.errors || []).map((e: string, i: number) => <div key={'e' + i}>❌ {e}</div>)}
  129 + {(precheck.rejectRules || []).map((r: any, i: number) => <div key={'r' + i}>⛔ [{r.ruleId}] {r.message}</div>)}
  130 + {(precheck.warnings || []).map((w: any, i: number) => <div key={'w' + i}>⚠️ [{w.ruleId}] {w.message}</div>)}
  131 + </div>
  132 + }
  133 + />
  134 + )}
  135 +
  136 + <Form layout="vertical">
  137 + {containers.map((c) => {
  138 + const role = roleOf(c)
  139 + if (role === 'master') return <MasterCard key={c.compId} container={c} values={master} setValues={setMaster} optionsMap={optionsMap} />
  140 + if (role === 'single') return <SingleCard key={c.compId} container={c} state={singles} setState={setSingles} optionsMap={optionsMap} />
  141 + return <TableCard key={c.compId} container={c} state={tables} setState={setTables} optionsMap={optionsMap} />
  142 + })}
  143 + </Form>
  144 +
  145 + <Divider />
  146 + <Space>
  147 + <Button onClick={doPrecheck}>提交前预检(MetaRule 实时风控)</Button>
  148 + <Button type="primary" loading={submitting} onClick={doSubmit}>提交「{schema.sceneName}」</Button>
  149 + </Space>
  150 + </div>
  151 + )
  152 +}
  153 +
  154 +// ---------- 字段标签(展示 M1 绑定溯源,强化"字段强绑定领域模型") ----------
  155 +function FieldLabel({ comp }: { comp: DragComponent }) {
  156 + const fb = comp.fieldBind!
  157 + const path = `${fb.bindAggregateId}${fb.domainEntityName ? '.' + fb.domainEntityName : ''}.${fb.domainFieldName}`
  158 + return (
  159 + <span>
  160 + {fb.formLabel}
  161 + {fb.required && <Text type="danger"> *</Text>}
  162 + <Text type="secondary" style={{ fontSize: 11, marginLeft: 6 }}>[{fb.fieldType}]</Text>
  163 + {fb.masked && <Tag color="orange" style={{ marginLeft: 6, fontSize: 10 }}>脱敏</Tag>}
  164 + <div title={path} style={{ fontSize: 10, color: '#999', fontWeight: 400,
  165 + whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: '100%' }}>⇠ {path}</div>
  166 + </span>
  167 + )
  168 +}
  169 +
  170 +function MasterCard({ container, values, setValues, optionsMap }: any) {
  171 + const children: DragComponent[] = container.childComponents || []
  172 + return (
  173 + <Card size="small" title={<b>{container.fieldBind?.formLabel}</b>} style={{ marginBottom: 12 }}>
  174 + <Row gutter={16}>
  175 + {children.map((ch) => (
  176 + <Col span={ch.span || 12} key={ch.compId}>
  177 + <Form.Item label={<FieldLabel comp={ch} />}>
  178 + <FieldControl comp={ch} value={values[ch.fieldBind!.domainFieldName]}
  179 + onChange={(v) => setValues((prev: any) => ({ ...prev, [ch.fieldBind!.domainFieldName]: v }))}
  180 + optionsMap={optionsMap} />
  181 + </Form.Item>
  182 + </Col>
  183 + ))}
  184 + </Row>
  185 + </Card>
  186 + )
  187 +}
  188 +
  189 +function SingleCard({ container, state, setState, optionsMap }: any) {
  190 + const entity = container.fieldBind?.domainEntityName
  191 + const children: DragComponent[] = container.childComponents || []
  192 + const values = state[entity] || {}
  193 + return (
  194 + <Card size="small" title={<b>{container.fieldBind?.formLabel}</b>} extra={<Tag>子实体 {entity}</Tag>} style={{ marginBottom: 12 }}>
  195 + <Row gutter={16}>
  196 + {children.map((ch) => (
  197 + <Col span={ch.span || 12} key={ch.compId}>
  198 + <Form.Item label={<FieldLabel comp={ch} />}>
  199 + <FieldControl comp={ch} value={values[ch.fieldBind!.domainFieldName]}
  200 + onChange={(v) => setState((prev: any) => ({ ...prev, [entity]: { ...prev[entity], [ch.fieldBind!.domainFieldName]: v } }))}
  201 + optionsMap={optionsMap} />
  202 + </Form.Item>
  203 + </Col>
  204 + ))}
  205 + </Row>
  206 + </Card>
  207 + )
  208 +}
  209 +
  210 +function TableCard({ container, state, setState, optionsMap }: any) {
  211 + const entity = container.fieldBind?.domainEntityName
  212 + const children: DragComponent[] = container.childComponents || []
  213 + const rows: Array<Record<string, any>> = state[entity] || []
  214 +
  215 + const setCell = (idx: number, field: string, v: any) =>
  216 + setState((prev: any) => {
  217 + const next = [...(prev[entity] || [])]
  218 + next[idx] = { ...next[idx], [field]: v }
  219 + return { ...prev, [entity]: next }
  220 + })
  221 + const addRow = () => setState((prev: any) => ({ ...prev, [entity]: [...(prev[entity] || []), {}] }))
  222 + const delRow = (idx: number) => setState((prev: any) => ({ ...prev, [entity]: (prev[entity] || []).filter((_: any, i: number) => i !== idx) }))
  223 +
  224 + const columns = [
  225 + ...children.map((ch) => ({
  226 + title: <FieldLabel comp={ch} />,
  227 + key: ch.compId,
  228 + render: (_: any, __: any, idx: number) => (
  229 + <FieldControl comp={ch} value={rows[idx]?.[ch.fieldBind!.domainFieldName]}
  230 + onChange={(v) => setCell(idx, ch.fieldBind!.domainFieldName, v)} optionsMap={optionsMap} />
  231 + ),
  232 + })),
  233 + {
  234 + title: '操作', key: '_op', width: 70,
  235 + render: (_: any, __: any, idx: number) => (
  236 + <Button danger size="small" icon={<DeleteOutlined />} onClick={() => delRow(idx)} />
  237 + ),
  238 + },
  239 + ]
  240 +
  241 + return (
  242 + <Card size="small" title={<b>{container.fieldBind?.formLabel}</b>} extra={<Tag>子实体 {entity}(多行)</Tag>}
  243 + style={{ marginBottom: 12 }}>
  244 + <Table size="small" pagination={false} rowKey={(_, i) => String(i)}
  245 + dataSource={rows.map((r, i) => ({ ...r, _k: i }))} columns={columns as any}
  246 + locale={{ emptyText: <Empty description="暂无明细行" /> }} />
  247 + <Button type="dashed" block icon={<PlusOutlined />} style={{ marginTop: 8 }} onClick={addRow}>新增一行明细</Button>
  248 + </Card>
  249 + )
  250 +}
... ...
web/src/main.tsx 0 → 100644
  1 +++ a/web/src/main.tsx
  1 +import React from 'react'
  2 +import ReactDOM from 'react-dom/client'
  3 +import { ConfigProvider } from 'antd'
  4 +import zhCN from 'antd/locale/zh_CN'
  5 +import 'antd/dist/reset.css'
  6 +import App from './App'
  7 +
  8 +ReactDOM.createRoot(document.getElementById('root')!).render(
  9 + <React.StrictMode>
  10 + <ConfigProvider locale={zhCN}>
  11 + <App />
  12 + </ConfigProvider>
  13 + </React.StrictMode>,
  14 +)
... ...
web/src/panels.tsx 0 → 100644
  1 +++ a/web/src/panels.tsx
  1 +import { useEffect, useState } from 'react'
  2 +import { Timeline, Tag, Table, Descriptions, Alert, Empty, Select, Button, Space, Typography, List } from 'antd'
  3 +import { api } from './api'
  4 +import type { SceneSchema } from './types'
  5 +
  6 +const { Text, Paragraph } = Typography
  7 +
  8 +const LAYER_COLOR: Record<string, string> = {
  9 + M8: 'blue', M1: 'green', M2: 'volcano', 'M2/M8': 'volcano', M3: 'geekblue',
  10 + M4: 'purple', M5: 'orange', ME: 'magenta', MetaRule: 'red',
  11 +}
  12 +
  13 +/** 执行追踪:把引擎返回的 trace/事件/扣库存/风控可视化,直观展示"元数据解释执行"全过程。 */
  14 +export function TracePanel({ result }: { result: any }) {
  15 + if (!result) return <Empty description="尚未执行。请在左侧填写并提交一次「新增订单」,这里会显示引擎逐层解释执行的过程。" />
  16 +
  17 + if (result.success === false) {
  18 + return (
  19 + <div>
  20 + <Alert type="error" showIcon message={`执行被拦截:${result.stage}`} description={result.message} />
  21 + {result.appliedRules?.length > 0 && (
  22 + <List size="small" style={{ marginTop: 12 }} header={<b>命中的 MetaRule 规则</b>}
  23 + dataSource={result.appliedRules}
  24 + renderItem={(r: any) => <List.Item><Tag color={r.effect === 'REJECT' ? 'red' : 'orange'}>{r.effect}</Tag>[{r.ruleId}] {r.message}</List.Item>} />
  25 + )}
  26 + <TraceTimeline trace={result.trace} />
  27 + </div>
  28 + )
  29 + }
  30 +
  31 + return (
  32 + <div>
  33 + <Descriptions size="small" column={1} bordered>
  34 + <Descriptions.Item label={`聚合根标识 ${result.rootIdField || ''}`}>{result.rootId}</Descriptions.Item>
  35 + {result.childCount != null && <Descriptions.Item label="写入子实体数">{result.childCount}</Descriptions.Item>}
  36 + </Descriptions>
  37 +
  38 + <TraceTimeline trace={result.trace} />
  39 +
  40 + <Text strong>领域事件(ME → Outbox)</Text>
  41 + <div style={{ margin: '6px 0' }}>
  42 + {(result.events || []).map((e: any) => <Tag key={e.eventId} color="magenta" style={{ marginBottom: 4 }}>{e.eventName} → {e.topic}</Tag>)}
  43 + </div>
  44 +
  45 + {result.stockActions?.length > 0 && (
  46 + <>
  47 + <Text strong>跨聚合最终一致(ME 规则 + M2 effect 通用执行)</Text>
  48 + <Table size="small" pagination={false} style={{ marginTop: 6 }} rowKey={(_, i) => String(i)}
  49 + dataSource={result.stockActions}
  50 + columns={[
  51 + { title: '目标聚合', dataIndex: 'targetAggregate' },
  52 + { title: '命令', dataIndex: 'targetCommand' },
  53 + { title: '键', render: (_: any, a: any) => `${a.keyField}=${a.key}` },
  54 + { title: '变更', render: (_: any, a: any) => `${a.op} ${a.amount}` },
  55 + { title: '剩余', dataIndex: 'remaining' },
  56 + { title: '状态', dataIndex: 'status', render: (s: string) => <Tag color={s === 'OK' ? 'green' : 'red'}>{s}</Tag> },
  57 + ]} />
  58 + </>
  59 + )}
  60 +
  61 + {result.warnings?.length > 0 && (
  62 + <Alert style={{ marginTop: 12 }} type="warning" showIcon message="MetaRule 预警(ALERT,不阻断)"
  63 + description={result.warnings.map((w: any, i: number) => <div key={i}>⚠️ [{w.ruleId}] {w.message}</div>)} />
  64 + )}
  65 + </div>
  66 + )
  67 +}
  68 +
  69 +function TraceTimeline({ trace }: { trace: any[] }) {
  70 + if (!trace?.length) return null
  71 + return (
  72 + <Timeline style={{ marginTop: 16 }}
  73 + items={trace.map((s) => ({
  74 + color: LAYER_COLOR[s.layer] || 'gray',
  75 + children: <span><Tag color={LAYER_COLOR[s.layer] || 'default'}>{s.layer}</Tag><b>{s.step}</b> — {s.detail}</span>,
  76 + }))} />
  77 + )
  78 +}
  79 +
  80 +/** 溯源面板:展示页面各部分分别来自哪一层模型,以及自动关联的 MetaRule 规则。 */
  81 +export function ProvenancePanel({ schema }: { schema: SceneSchema }) {
  82 + const sl = schema.sourceLayers || {}
  83 + return (
  84 + <div>
  85 + <Paragraph type="secondary">本页面无任何业务硬编码,各部分均由下列本体模型层实时合成:</Paragraph>
  86 + <Table size="small" pagination={false} rowKey="k"
  87 + dataSource={Object.entries(sl).map(([k, v]) => ({ k, v }))}
  88 + columns={[
  89 + { title: '页面要素', dataIndex: 'k' },
  90 + { title: '来源模型层', dataIndex: 'v', render: (v: string) => <Tag color="geekblue">{v}</Tag> },
  91 + ]} />
  92 + <Text strong>自动关联的 MetaRule 动态规则</Text>
  93 + <List size="small" style={{ marginTop: 6 }} dataSource={schema.rules || []}
  94 + renderItem={(r: any) => (
  95 + <List.Item>
  96 + <div>
  97 + <Tag color={r.effect === 'REJECT' ? 'red' : 'orange'}>{r.effect}</Tag>
  98 + <b>{r.ruleName}</b> <Text code>{r.ruleId}</Text>
  99 + <div><Text type="secondary" style={{ fontSize: 12 }}>{r.matchCondition}</Text></div>
  100 + <div><Text type="secondary" style={{ fontSize: 12 }}>→ {r.message}</Text></div>
  101 + </div>
  102 + </List.Item>
  103 + )} />
  104 + <Text strong>规则全局参数</Text>
  105 + <div style={{ marginTop: 6 }}>
  106 + {Object.entries(schema.ruleGlobalParams || {}).map(([k, v]) => <Tag key={k}>{k} = {String(v)}</Tag>)}
  107 + </div>
  108 + </div>
  109 + )
  110 +}
  111 +
  112 +/** 模型查看器:直接查看任意本体层解析后的内容,证明"页面即模型"。 */
  113 +export function ModelViewer() {
  114 + const [layers, setLayers] = useState<any[]>([])
  115 + const [code, setCode] = useState<string>('M8')
  116 + const [content, setContent] = useState<any>(null)
  117 + const [dir, setDir] = useState('')
  118 +
  119 + useEffect(() => { api.models().then((d) => { setLayers(d.layers); setDir(d.modelsDir) }) }, [])
  120 + useEffect(() => { if (code) api.model(code).then((d) => setContent(d.content)) }, [code])
  121 +
  122 + return (
  123 + <div>
  124 + <Space style={{ marginBottom: 8 }}>
  125 + <Select value={code} style={{ width: 320 }} onChange={setCode}
  126 + options={layers.map((l) => ({ value: l.code, label: `${l.code} · ${l.fileName}` }))} />
  127 + </Space>
  128 + <Paragraph type="secondary" style={{ fontSize: 12 }}>模型目录:{dir}</Paragraph>
  129 + <pre style={{ background: '#0f172a', color: '#e2e8f0', padding: 12, borderRadius: 6, maxHeight: 460, overflow: 'auto', fontSize: 12 }}>
  130 + {content ? JSON.stringify(content, null, 2) : '加载中…'}
  131 + </pre>
  132 + </div>
  133 + )
  134 +}
  135 +
  136 +/** 聚合根记录与事件列表(列由 M1 属性驱动,通用)。 */
  137 +export function OrdersEventsPanel({ refreshKey, aggregateId }: { refreshKey: number; aggregateId?: string }) {
  138 + const [cols, setCols] = useState<any[]>([])
  139 + const [rowKeyField, setRowKeyField] = useState<string>('')
  140 + const [rows, setRows] = useState<any[]>([])
  141 + const [events, setEvents] = useState<any[]>([])
  142 +
  143 + const load = () => {
  144 + if (aggregateId) {
  145 + api.aggregate(aggregateId).then((meta: any) => {
  146 + setRowKeyField(meta.rootIdentifier)
  147 + setCols([
  148 + { title: meta.rootIdentifier, dataIndex: meta.rootIdentifier },
  149 + ...(meta.attributes || []).map((a: any) => ({ title: a.name, dataIndex: a.name })),
  150 + ])
  151 + })
  152 + api.list(aggregateId).then(setRows)
  153 + }
  154 + api.events().then(setEvents)
  155 + }
  156 + useEffect(load, [refreshKey, aggregateId])
  157 +
  158 + return (
  159 + <div>
  160 + <Space style={{ marginBottom: 8 }}><Button size="small" onClick={load}>刷新</Button></Space>
  161 + <Text strong>{aggregateId} 记录(列源自 M1,敏感字段脱敏)</Text>
  162 + <Table size="small" pagination={false} scroll={{ x: true }} style={{ marginTop: 6, marginBottom: 16 }}
  163 + rowKey={(r: any, i) => String(r[rowKeyField] ?? i)} dataSource={rows} columns={cols} />
  164 + <Text strong>事件 Outbox(最近)</Text>
  165 + <Table size="small" pagination={false} style={{ marginTop: 6 }} rowKey="event_id"
  166 + dataSource={events}
  167 + columns={[
  168 + { title: '事件', dataIndex: 'event_name' },
  169 + { title: 'topic', dataIndex: 'topic' },
  170 + { title: '状态', dataIndex: 'status', render: (s: string) => <Tag color={s === 'PUBLISHED' ? 'green' : 'blue'}>{s}</Tag> },
  171 + ]} />
  172 + </div>
  173 + )
  174 +}
... ...
web/src/types.ts 0 → 100644
  1 +++ a/web/src/types.ts
  1 +// 与后端 SceneSchemaService 输出对应的类型(宽松定义,随模型演进自动兼容)
  2 +
  3 +export interface FieldBind {
  4 + bindAggregateId: string
  5 + bindSourceType: 'aggregate_root' | 'aggregate_entity'
  6 + domainEntityName?: string
  7 + domainFieldName: string
  8 + formLabel?: string
  9 + placeholder?: string
  10 + required?: boolean
  11 + maskField?: boolean
  12 + fieldType?: string
  13 + masked?: boolean
  14 + optionsSource?: {
  15 + aggregateId: string
  16 + endpoint: string
  17 + valueField: string
  18 + labelField: string
  19 + }
  20 +}
  21 +
  22 +export interface DragComponent {
  23 + compId: string
  24 + compType: 'input' | 'number_input' | 'date_picker' | 'switch' | 'table' | 'card' | 'text_area' | 'select'
  25 + width?: string
  26 + span?: number
  27 + fieldBind?: FieldBind
  28 + childComponents?: DragComponent[]
  29 +}
  30 +
  31 +export interface SceneSchema {
  32 + sceneId: string
  33 + sceneName: string
  34 + aggregateScope: string[]
  35 + permissionBind: string[]
  36 + command?: any
  37 + commandApi?: string
  38 + template?: {
  39 + templateId: string
  40 + templateName: string
  41 + templateType: string
  42 + renderMode: string
  43 + masterAggregate: string
  44 + detailAggregates: string[]
  45 + rootComponents: DragComponent[]
  46 + }
  47 + rules?: Array<{ ruleId: string; ruleName: string; effect: string; message: string; matchCondition: string }>
  48 + ruleGlobalParams?: Record<string, any>
  49 + sourceLayers?: Record<string, string>
  50 +}
  51 +
  52 +export type OptionItem = { value: any; label: any; data: Record<string, any> }
... ...
web/tsconfig.json 0 → 100644
  1 +++ a/web/tsconfig.json
  1 +{
  2 + "compilerOptions": {
  3 + "target": "ES2020",
  4 + "useDefineForClassFields": true,
  5 + "lib": ["ES2020", "DOM", "DOM.Iterable"],
  6 + "module": "ESNext",
  7 + "skipLibCheck": true,
  8 + "moduleResolution": "bundler",
  9 + "allowImportingTsExtensions": true,
  10 + "resolveJsonModule": true,
  11 + "isolatedModules": true,
  12 + "noEmit": true,
  13 + "jsx": "react-jsx",
  14 + "strict": false,
  15 + "noUnusedLocals": false,
  16 + "noUnusedParameters": false,
  17 + "allowJs": true
  18 + },
  19 + "include": ["src"]
  20 +}
... ...
web/vite.config.ts 0 → 100644
  1 +++ a/web/vite.config.ts
  1 +import { defineConfig } from 'vite'
  2 +import react from '@vitejs/plugin-react'
  3 +
  4 +// 开发服务器把 /api 代理到引擎(8080),避免跨域并让前端无需硬编码后端地址
  5 +export default defineConfig({
  6 + plugins: [react()],
  7 + server: {
  8 + port: 5174,
  9 + proxy: {
  10 + '/api': {
  11 + target: 'http://localhost:8091',
  12 + changeOrigin: true,
  13 + },
  14 + },
  15 + },
  16 +})
... ...