主从表、库存扣减、审批通过自动入库------低代码的三个传统翻车点,这次一个都没翻。

写在前面
低代码平台有个经典的"三连翻车"剧本,做过业务系统的都熟:
- 第一翻:主从表。单表 CRUD 演示得很溜,一遇到"采购单 + 采购明细"这种主子结构,就开始让你"自定义开发"。
- 第二翻:业务逻辑。字段增删改查是零代码,但"审批通过后扣库存"这种事,平台两手一摊:写代码吧。
- 第三翻:审批联动。流程引擎是流程引擎,业务数据是业务数据,中间的缝靠人肉填。
所以低代码平台演示的时候都很好看,真到进销存这种场景就露馅------因为进销存恰好把三个翻车点占全了:单据全是主子结构,库存变更全是业务逻辑,每张单据都要走审批。
Forge Admin 最近干了一件事:用纯低代码方式,把一个完整的采购仓储模块搭了出来------物料、供应商、仓库、采购、出库、调拨,10 张业务表、10 个业务对象、6 个应用入口,没有写任何 Vue 页面,也没有写任何 Java Controller。
这篇文章就拆它是怎么扛住这三个翻车点的。不讲概念,直接看实现。
一、先立规矩:什么叫"零代码"
先把话说清楚,免得评论区吵架。
"零代码"不是说这个系统里没有代码------动作引擎、台账服务、运行时装配,这些平台能力当然是代码写的,而且写得很硬核。
"零代码"指的是:交付这个进销存应用本身,没有产生一行业务代码 。没有 PurchaseOrderController.java,没有 purchase-order.vue。交付物是一组配置数据:
bash
ai_lowcode_domain 业务领域(采购仓储)
└── ai_business_suite 业务套件
└── ai_business_object 业务对象 ×10(物料/供应商/仓库/采购单/采购明细/...)
└── ai_crud_config 运行配置(页面协议 → AiCrudPage 五件配置)
└── designer_options.actions 业务动作(审批回调、入库、锁定...)
└── ai_business_app 应用入口 ×6(RUNTIME 模式,指向 configKey)
└── ai_business_binding 能力挂接(流程回调绑定)
这些配置全部通过初始化 SQL 交付, tenant_id = 1。换句话说:应用即数据。这是判断一个低代码平台成色的分水岭------应用到底是"画出来的页面",还是一套可以版本化、可以迁移、可以审查的结构化资产。

二、翻车点一:主从表
采购单长这样:主表是单据头(单号、供应商、仓库、备注),子表是采购明细行(物料、数量、单价)。这是业务系统里最普通的结构,也是低代码平台最常见的翻车现场。
2.1 设计态:一份协议描述主子结构
Forge 的主从页面不是"画"出来的,是协议驱动的。ai_crud_config.page_schema 里的核心结构:
json
{
"layoutType": "master-detail-crud",
"primaryModelCode": "PW_PURCHASE_ORDER",
"modelRefs": [
{ "modelCode": "PW_PURCHASE_ORDER", "primary": true,
"relations": [{ "relationType": "ONE_TO_MANY",
"sourceField": "id", "targetField": "purchaseId" }] },
{ "modelCode": "PW_PURCHASE_ORDER_ITEM", "primary": false,
"props": { "saveMode": "merge", "recordSelector": { "...": "..." } } }
]
}
发布时,LowcodeRuntimeConfigBuilder.buildRuntimeConfig() 把这份协议编译成 AiCrudPage 的运行配置(searchSchema / columnsSchema / editSchema / apiConfig / options),主从结构编进 options.masterDetailConfig。运行页按 configKey 请求 GET /ai/crud-config/render/{configKey},拿到 layoutType = master-detail-crud 后加载主从模板组件。
链路里没有一行是采购专用的------换成"订单 + 订单明细"、"报销单 + 费用明细",协议结构一模一样。
2.2 运行态:两个硬细节
协议谁都会写,运行态的两个细节才见功力。
细节一:记录选择器。 明细行不是只能手敲。子表头部有"选择物料"按钮,弹出记录选择器------支持多字段关键词搜索、支持 ${formData.xxx} 动态过滤(比如只显示当前供应商有报价的物料),选中的记录按 fieldMappings 映射后批量追加到子表。这个交互在手写页面里都经常被偷懒省掉。
细节二:merge 保存。 主子表保存最烦的问题:用户在编辑页删了一行明细,提交时怎么办?
简单粗暴的做法是"全删重插"(Forge 里叫 replace 模式,也是默认模式),但这对有审计要求的单据不可接受。采购明细用的是 merge 模式:前端删除一行时不物理移除,而是打上 _deleted 标记,提交时组织成这样的 payload:
yaml
{
main: { /* 主表字段 */ },
children: {
pw_purchase_order_item: [
{ id: 101, _deleted: true }, // 删除(走逻辑删)
{ id: 102, quantity: 50 }, // 更新(先校验行归属)
{ materialId: 'MAT-003', quantity: 200 } // 无 id,新增
]
}
}
后端 DynamicCrudService.mergeMasterDetailChildRows() 按标记分流:_deleted + 有 id → 逻辑删除;无 id → 插入;有 id → 先校验这一行确实属于当前主记录再更新。最后这条校验很关键------没有它,改个 id 就能改别人单据的明细行,这是主从表接口的经典越权漏洞。

三、翻车点二:库存怎么算
这是全文的核心,也是低代码平台最心虚的地方。
先说 Forge 的立场:业务逻辑不靠生成代码,也不靠脚本引擎,而是靠一个白名单制的"动作引擎" 。
3.1 动作 = 一组有事务、有幂等的白名单步骤
一个业务动作(action)挂在业务对象上,类型为 COMMAND,由若干步骤(step)组成。步骤类型是白名单,目前六种:
| 步骤类型 | 语义 |
|---|---|
UPDATE_FIELD |
更新当前/目标记录字段 |
CREATE_RECORD |
创建记录 |
START_FLOW |
发起审批流程 |
SEND_MESSAGE |
发送消息 |
FOREACH |
循环集合(如明细行),嵌套执行子步骤 |
DOMAIN_ACTION |
调用领域动作 SPI,首个实现是 QUANTITY 数量台账 |
为什么不做成"写一段 JS/Groovy 随便跑"?因为脚本引擎是个无底洞:没有权限边界、没有审计、没有事务保证、出了问题没法排查。白名单步骤每一项都有明确的语义、参数协议和权限校验,BusinessActionExecutionService 统一入口执行:幂等检查(RUNNING 预占日志 + 请求摘要防并发)→ TransactionTemplate 单事务顺序执行 → 写执行日志。宁可步骤类型少一点,也不把动作引擎做成任意脚本执行器。
3.2 数量台账:库存不是字段,是账
很多系统里"库存"就是商品表上的一个 stock 字段,扣库存就是 update stock = stock - ?。这在演示里没问题,在生产里是灾难:并发超卖、重复提交重复扣、出了问题查无对证。
Forge 把数量做成了台账,三张表:
ai_business_quantity_balance:余额表(账户 + 项目 + 维度,当前数量、锁定数量)ai_business_quantity_ledger:流水表(每笔变动,idempotency_key唯一键)ai_business_quantity_lock:锁定表(预占、释放、核销)
操作类型五种:INBOUND(入库)、LOCK(锁定)、RELEASE(释放)、COMMIT(核销)、TRANSFER(转移)。所有数量必须是整数最小单位,传小数直接抛错------和"金额用分"是同一个工程哲学。
3.3 采购入库动作的完整配置
看真实配置。采购单审批通过后的入库动作 inbound_purchase_stock:
bash
{ "actionConfig": { "successBehavior": "refreshList", "steps": [
{ "stepCode": "foreach_purchase_items", "stepType": "FOREACH",
"stepConfig": {
"collectionPath": "record.children.pw_purchase_order_item",
"itemAlias": "item",
"steps": [
{ "stepCode": "purchase_item_inbound", "stepType": "DOMAIN_ACTION",
"stepConfig": {
"actionType": "QUANTITY", "operationType": "INBOUND",
"params": {
"accountCode": "${record.main.warehouseId}",
"itemCode": "${item.materialId}",
"quantity": "${item.quantity}",
"sourceDetailId": "${item.id}",
"remark": "采购审批通过入库"
} } } ] } } ] } }
读法很直白:遍历采购单的每一行明细,以仓库为账户、物料为项目,逐行入库。${...} 表达式取上下文里的值,sourceDetailId 把台账流水和明细行关联起来------以后任何一笔库存都能反查出是哪张单据的哪一行造成的。
幂等是强制要求:动作没带幂等键时,引擎用 来源对象|来源记录|明细行|动作|步骤|操作类型 拼一个稳定基再哈希,保证同一个审批回调重试多少次都只记一次账。
出库单比入库更进一步,用的是三件套:提交时 LOCK(预占,不影响可用判断)、审批通过 COMMIT(按锁定核销)、审批驳回 RELEASE(释放锁定) 。调拨则是 TRANSFER,从源仓库扣、向目标仓库加,拆成源端/目标端两条流水。
这就是"库存怎么算"的答案:不是一个字段,是账户模型 + 流水 + 幂等 + 锁定语义,而且全部是配置,不是代码。

四、翻车点三:审批联动
最后一个翻车点:流程和业务怎么接上。
Forge 的原则是不新建审批引擎------流程继续走 Flowable,低代码只负责"审批结果出来之后干什么"。接法分三步:
第一步,行按钮发起审批。 采购单列表上有"提交审批"按钮,对应一个 COMMAND 动作,里面是一个 START_FLOW 步骤:配置流程模型 key,用 fieldMapping 把 record.main.purchaseNo 等字段映射成流程变量。注意这里用的是平台通用审批流 (leave_multi),不是为采购定制的流程------零定制的又一个佐证。
第二步,绑定回调动作。 在 ai_business_binding 里配一行 JSON,把"审批通过"接到入库动作:
sql
-- 采购单:审批通过 → 入库
JSON_OBJECT('APPROVED', 'inbound_purchase_stock')
-- 出库单:通过 → 核销锁定,驳回 → 释放锁定
JSON_OBJECT('APPROVED', 'commit_outbound_stock',
'REJECTED', 'release_outbound_stock')
第三步,引擎回调执行。 流程引擎事件通过 @FlowCallback 注解进入 BusinessFlowService,按审批结果解析出 actionCode,构造执行请求调同一个动作引擎。回调的幂等键格式是 flowCallback:{processInstanceId}:{result}:{actionCode}------同一个流程实例的同一个结果,副作用只执行一次。
而且 spec 里有一条硬性要求:流程回调失败必须可见,不允许"审批成功但库存没动"这种静默失败。回调执行同样写执行日志,失败了能查、能追溯、能重放。
到这里,三张单据的完整闭环就出来了:
scss
采购单:提交审批(START_FLOW) → 通过(APPROVED) → FOREACH 明细逐行 INBOUND
出库单:提交时 LOCK → 通过 COMMIT / 驳回 RELEASE
调拨单:审批通过 → TRANSFER(源仓库减、目标仓库加,双流水)
五、交付清单与诚实的边界
最后交付了什么:
- 10 个业务对象:物料、供应商、供应商报价、仓库(主数据);采购单/出库单/调拨单(单据)+ 各自明细
- 6 个应用入口:物料、供应商、仓储、采购、出库、调拨管理,从应用中心进入
- 完整的库存语义:入库、预占、核销、释放、转移,流水可追溯到明细行
- 演示数据:P.O 42.5 水泥、HRB400 螺纹钢、YJV 电力电缆......拿来就能点(金额字段单位是分,符合工程约定)
- 仓库详情:余额、流水、锁定、关联单据四个数据面板;物料详情含供应商报价和最近流水
也得说清楚没做到什么:像素级的 UI 还原还没做到------统计卡片、底部固定操作栏这类视觉细节,是平台 UI 模板层的能力缺口,不是业务逻辑缺口。业务逻辑层面,验收文档的结论是 11 个原型页面全部"低代码等价还原"。
这个边界很重要。低代码平台吹牛的常见方式是把"UI 自由度"和"业务能力"混为一谈。Forge 这轮恰好反过来:业务能力(主从、台账、审批联动)做到了零代码,UI 模板的丰富度承认还有差距。前者是地基,后者是装修------地基没打好的平台,装修再漂亮也没用。
六、三个值得抄的取舍
复盘一下这轮设计里最有价值的三个判断:
- 动作引擎做白名单,不做脚本引擎。 牺牲了"什么都能写"的幻觉,换来权限、事务、幂等、审计全套工程保障。业务逻辑从"代码"变成了"可审查的配置资产"。
- 数量走台账,不走字段。 账户 + 流水 + 锁定 + 幂等键,库存从"一个会被改坏的数字"变成"一本可追溯的账"。这个模型不只适用于库存------积分、余额、额度,所有"会变动的数量"都能套。
- 流程回调复用同一个动作底座。 手动按钮、定时触发器、审批回调,三个触发点走同一个执行引擎、同一套幂等和日志。没有为审批单独发明一套机制。
体验 Forge Admin
- 在线演示:www.dlforgelab.com:8084/forge/login
- 默认账号:admin / 123456
- Gitee:gitee.com/ForgeLab/fo...
下一篇预告:动作引擎的执行底座再往下拆一层------幂等键怎么设计、并发怎么防、执行日志怎么查,把"业务动作"这件事的工程细节讲透。
你用过哪些低代码平台?主从表和业务逻辑这两关,它们过去了吗?评论区聊聊。