Skip to content

低代码平台开发总览 ​

适用端:后端 + Web | 类型:机制

定制功能

本页覆盖的四个域(数据建模、表单设计、数据接口、门户)与取号管理,上游 yudao 一个都没有。上游只有代码生成器(生成 Java/Vue 源码,产物脱离平台运行)。本项目的低代码是「运行时解释 JSON 产物」,两者不是同一类东西。

这一页是 01 域的地图:先讲清「一张表怎么变成一个能用的业务功能」这条主链路,再讲清四个域各自的边界与它们之间唯一的耦合点。要改具体某一域,读完这页再进对应子页。

前置阅读:后端模块架构、数据库约定与表总览。

它解决什么问题 ​

一条业务功能从零到能用,在本平台要走完四段。每一段落在一个不同的模块里,段与段之间只通过数据库表和 Spring 事件耦合,没有跨模块的方法调用:

全链路最贵的一类缺陷

这条链上每一段的失败模式都是「不报错」:DDL 执行了一半没法撤、菜单建出来了但路径形态错一格就白屏、权限资源没登记则页面一切正常只是行级过滤静默失效、表名集合是内存态一重启就空。所以四个域的代码里塞满了「宁可失败得明明白白」的取舍——读到那些看似过度的校验和刻意同步的事件时,先假设它有理由,再去注释里找。

四个域的边界 ​

域管什么不管什么子页
数据模型物理表的登记与结构变更(DDL)不生成代码、不管业务数据数据模型
表单设计JSON 产物 → 可访问可授权可回滚的表单不建表、不改表表单设计后端、表单运行时
数据接口把一段 SQL/JSON/HTTP 变成一个可调用的取数端点不管表结构、完全没有行级权限数据接口
门户首页看板的布局协议与发布不产数据,只摆放取数卡片门户
取号管理业务单据编号的生成与并发安全不关心编号存在哪张表取号管理
自研控件设计器里能拖出什么、前端怎么渲染—自研控件开发

数据接口与表单是两条独立的路

两者都能「查一张表的数据」,但不共享任何权限机制。表单运行时的行级过滤在 SQL 构建层介入(FormDesignSqlBuilder 6 个注入点),字段裁剪在 Service 层;数据接口走的是 SqlExecutor + SqlPolicyValidator,没有行级权限这一层。给数据接口配的行级规则不会被调用。这是已知边界,不是待修的缺陷。

核心机制 ​

唯一的跨模块耦合点:一个事件 + 一份契约 DTO ​

enlev-module-system 依赖 enlev-module-infra,反向不行。所以 infra 侧的表单发布要驱动 system 侧的权限登记,只有一条路:广播事件,让 system 来监听。

件位置作用
FormDesignPublishedEventinfra api/formdesign/event/携带一份 FormResourceDescriptorDTO
FormDesignDeletedEvent同上携带改写前的原编码(不是墓碑编码)
FormResourceDescriptorDTOinfra api/formdesign/dto/跨模块契约;Item.isGrantworthy:142 决定哪些列值得落资源行
FormResourceDescriptorBuilderinfra api/formdesign/组装契约,build:61;降级约定见子页
FormDesignPublishedListenersystem framework/permission/core/dataset/@EventListener(不是 AFTER_COMMIT),onPublished:40
DatasetResourceServicesystem service/permission/register:88 五步 / unregister:119

这个事件必须同步、必须在事务里

FormDesignPublishedListener 的类注释把这个取舍写得很直白:换成 @TransactionalEventListener(AFTER_COMMIT) 的话,失败时表单已经发布出去了,而它的数据集资源、敏感字段资源、语义槽位一个都没有——页面一切正常,只有行级/字段级权限悄悄地全都不生效。这正是本项目最贵的缺陷形态:配了、成功了、没生效、没提示。代价是「权限模块出问题会挡住表单发布」,这个代价是明说的、看得见的。

反向的耦合也有,但走的是 FormDesignApi(infra 提供给 agent 模块),不涉及 system。previewPublishMenu 就是只在这里暴露、没有 admin 端点的一个例子。

原生模块怎么进这套体系:@PermissionDataset ​

在线表单靠发布事件登记数据集。原生模块(system_users、system_dept 这些)没有「发布」这个动作,走的是另一条路:在 DO 上标注解,启动期扫描登记。

java
@PermissionDataset(value = "system:user", name = "用户")
public class AdminUserDO extends TenantBaseDO { ... }

全仓真正标注了的只有 2 个:DeptDO.java:19(system:dept / 组织)与 AdminUserDO.java:23(system:user / 用户)。

件位置
注解定义enlev-framework/enlev-common/.../biz/system/permission/annotation/PermissionDataset.java
扫描器system framework/permission/core/dataset/NativeDatasetScanner.java
登记 Runnersystem framework/permission/runner/NativeDatasetRegisterRunner.java(@Order(6))

NativeDatasetRegisterRunner 补的是行级权限的另一半:在此之前,进得了 RuleDataPermissionRule 表名集合的只有在线表单,原生模块的表一张都没有——在授权页上给它们配的行级规则配了不会被调用,且不报错。

六个启动 Runner:顺序是契约 ​

system framework/permission/runner/ 下六个 ApplicationRunner,@Order 1→6,新增 Runner 的 @Order 必须排在它依赖的那个之后:

@OrderRunner做什么不跑会怎样
1ResourceMigrateRunnersystem_menu 按钮层 → sys_resource后面全没数据。🔴 幂等判据在塌缩开启后换了:原本「按 menu_id 判断已迁移就跳过」不再成立,实测非目录菜单里 341 / 666 个已没有承载自己 menu_id 的存活资源行(塌缩前是 0)
2GrantMigrateRunnersystem_role_menu → sys_grant🔴 migrate() 是 insert-only(只补不删)。旧表还有写入方时每次启动都跑,会构成一条单向的半通路
3PermissionScanRunner扫描代码里所有 @PreAuthorize("@ss.hasPermission('...')"),按命名约定推导资源树、补缺失的中间目录节点「代码里有但库里没有」的叶子权限点不会被发现——意味着某个功能永远鉴权失败
4BuiltInRuleSeedRunner把 8 个内置规则模板的描述行写进 sys_rule(按 template 判重,幂等)授权页的规则下拉框是空的
5DatasetTableRegisterRunner按存活的 DATASET 行重建 RuleDataPermissionRule 的表名集合🔴 所有已配好的行级权限规则静默失效。查询照常返回结果,只是不再有过滤,越权数据就这么出去了,没有报错、没有日志、界面完全正常
6NativeDatasetRegisterRunner扫描 @PermissionDataset,登记原生表原生模块的行级规则配了不会被调用

表名集合是内存态的

RuleDataPermissionRule.getTableNames() 是框架决定「要不要改写这条 SQL」的依据,它只在表单发布时被填进去,应用一重启就空了。@Order(5) 那个 Runner 存在的全部意义就是把它重建回来。这就是为什么它排在权限扫描之后、原生登记之前:原生登记也要往这个集合里加表名,得先有一个重建好的集合。

两套 SQL 安全机制,判据完全不同 ​

这是最容易搞混的一处。两个域都要执行用户写出来的 SQL,但走的是两条独立的实现,安全模型相反:

数据模型(DDL)数据接口(DML)
类SqlSafeGuardSqlPolicyValidator
模型标识符白名单JSqlParser 解析 + 策略黑名单
为什么DDL 里全是标识符,没有值可绑定;白名单比转义可靠DML 里有真正的参数,用 PreparedStatement 绑定,只需管住「语句形态」
兜底受管表前缀拒绝框架表写前缀拒绝(act_/ai_/bpm_/flw_/infra_/qrtz_/sys_/system_)

两者不共用代码,改一边不会影响另一边。详见各子页。

数据模型 ​

低代码域共 14 张定制表,全部 infra_ 前缀,完整列清单由脚本生成于 docs/.generated/tables-lowcode.md(不是站点页面):

组表
数据模型infra_data_model、infra_data_model_log、infra_data_source_config
表单设计infra_form_design、infra_form_design_version、infra_form_design_menu、infra_form_design_data_log
数据接口infra_data_interface、infra_data_interface_version、infra_data_interface_log
门户infra_portal、infra_portal_version
取号infra_bill_template、infra_bill_template_log

租户维度不统一

infra_form_design 是 @TenantIgnore 全局表,没有 tenant_id 列(一张表单登记一次、所有租户共用);而 infra_form_design_data_log 有 tenant_id。同组两张表不同口径,别按「同一模块的表都一样」推断。逐表核对见各子页。

三张表的唯一索引不含 deleted

infra_form_design.uk_code、infra_data_interface.idx_code(非唯一,更危险)、以及 system 侧的 sys_slot_mapping.uk_resource_slot。软删的行会继续占住编码,所以:

  • 表单软删必须把 code 改写成墓碑 <code>_del_<id>(tombstoneCode:366);
  • 数据接口的 code 撞了没有任何数据库层的保护;
  • 槽位映射必须物理删除,软删会被墓碑永久占住。

怎么扩展 ​

我要加一个新的低代码域(比如「报表」) ​

按现有四域的形状,最小可行清单:

  1. 表:主表 + _version 表(如果要发布/回滚)+ _log 表(如果要审计)。命名沿用 infra_<域名> / infra_<域名>_version。DDL 记得跑 node tools/gen-tables.mjs 更新语料。
  2. DO:继承 BaseDO(全局表)或 TenantBaseDO(租户表),先决定租户维度——这个决定一旦上线就很难改。
  3. Service:唯一入口。发布类动作要 @Transactional,且乐观锁每条写路径自己查行数(拦截器冲突时不抛异常)。
  4. Controller:权限串沿用 infra:<域名>:{query|create|update|delete|publish}。启动期 PermissionScanRunner(@Order 3) 会自动把缺失的中间目录节点补进 sys_resource,但叶子权限点漂移只会告警不会自建。
  5. 如果要参与行级权限:得走 @PermissionDataset 或发布事件登记成 DATASET,并让表名进 RuleDataPermissionRule 的集合。漏这一步的表现是「规则配了不生效且不报错」。
  6. 前端:api/infra/<域名>/index.ts ↔ views/infra/<域名>/index.vue + 同级 XxxForm.vue(这是本仓的 CRUD 配对约定)。

我要给表单加一个动作权限 ​

见表单设计后端 · 怎么扩展。要点:改 buildMenuConfig 的 actions 数组、改运行时的端点动作映射、改前端 GRANT_ACTION_OPTIONS——三处没有任何自动对齐,且已发布的存量表单不会自动补这一行。

我要加一个自研控件 ​

见自研控件开发 · 八步。要点:两张注册表(form-create 的与 form-designer 的)不是同一张,且 components[tag] 的查找发生在 isNativeTag 分支之前。

坑与注意 ​

跨域的、不属于任何单一子页的坑:

  1. 🔴 业务模块只有 5 个:enlev-module-{system,infra,bpm,ai,agent}。上游的 enlev-module-im、支付、商城、CRM 等已连目录一起删除(12347efcd1),mvn -pl enlev-module-im 必然失败——不是因为被注释,是因为不存在。别按上游 yudao 的模块清单猜这里有什么,详见后端架构与模块依赖。
  2. 🔴 模块是扁平 jar,没有 -api / -biz 拆分。api 只是同一个构件里的一个 package。这是与上游 yudao 最大的结构差异,代码生成器模板已按扁平模块写好,调整模块结构要同步改模板。
  3. src/lib/form-create 与 src/lib/form-designer 是上游源码副本,要求零 diff。不要修改、重排格式或「顺手优化」。vite.config.ts 里对应的 alias 顺序敏感。详见自研控件开发。
  4. schema_fingerprint 恒为 NULL 是刻意的(FormDesignDO:299-315),而 DDL 的列注释描述了一个不存在的复核行为。别照着列注释实现,也别「修好」它。
  5. 「版本」这个词在四个域里指四件不同的事:infra_form_design.version(乐观锁)/ published_version(线上第几版)/ schema_version(JSON 产物结构版本)/ infra_form_design_version(快照表);数据模型没有版本表;门户的版本机制与表单的不是同一套代码;数据接口有 infra_data_interface_version 但语义又是另一套。
  6. DDL 不进事务、不可回滚(MySQL 的限制,不是设计选择)。数据模型那套五道闸存在的全部意义就是把「改坏了没法撤」的概率压到最低。
  7. pnpm test:e2e 是坏的——playwright.config.ts 和 tests/e2e/ 都不存在。不要运行也不要尝试补全。
  8. pnpm dev-server 指向上游芋道演示后端,不是本地服务。别用它验证本地后端改动。
  9. enlev-module-agent 没有 src/test/resources/application-unit-test.yaml,其 156 个测试绝大多数是纯 Mockito。要在那里加 DB 测试得先补这份配置。

→ 本节是贴着上下文的提醒;按症状查的全域清单见坑位索引。

测试 ​

域位置规模
表单设计 / 运行时infra src/test/java/.../formdesign/60 个测试文件
数据模型infra src/test/java/.../datamodel/见子页
数据接口infra src/test/java/.../datainterface/见子页
取号infra src/test/java/.../bill/84 个 @Test,跨 7 个类(含 32 线程 / 200 任务的并发证明)
门户infra src/test/java/.../portal/26 个
权限派生system DatasetResourceServiceImplTest22 个 @Test,纯 Mockito

基类在 enlev-spring-boot-starter-test:BaseDbUnitTest(H2,profile unit-test,需 /sql/clean.sql)、BaseMockitoUnitTest、BaseRedisUnitTest。

未证实 ​

  • 本页的链路图与各域边界来自逐域读码,但没有端到端跑一遍完整的「建表 → 配表单 → 发布 → 授权 → 运行时」链路去验证每一段的衔接。各子页标注的实测项是局部的。
  • 六个 Runner 的 @Order 取值与类注释逐个核对过,但启动期实际执行顺序没有用日志验证(Spring 对同 @Order 值的排序未定义,此处六个值互不相同所以不受影响)。
  • 「341 / 666」「12 行悬挂」这类数字全部来自源码注释,是历史快照。GrantDecisionService:482-484 专门警告「别把任何一个数字当常量用」。

相关页面 ​

本域子页

跨域