技术思考问题19:低代码省代码,能省架构吗?

芒格模型:能力圈与机会成本下的低代码架构边界

低代码省的是表单实现,省不掉的是数据主权与权限边界。

text 复制代码
[读者] 正在用低代码承接内部系统与对外工单的架构师
[痛点] 页面搭得快,但租户隔离、接口限流、迁移路径没人负责
[现在读] 因为下一版要接入跨境交易与物流计费
[读完] 能用能力圈与机会成本划出低代码的承载边界
text 复制代码
[旧方案] 表单/审批/报表全部可视化配置 + 直连 ERP 连接器
    |
    v
[新需求] 多租户、个性化交易规则、跨系统物流计费
    |
    v
[冲突] 配置层被迫承担领域事务与权限隔离
    |
    v
[后果] 平台改造成本上升,数据主权与迁移自由度流失

我是老李,带一支做跨境商户服务的团队。这个行业的节奏很直白:业务这周提需求,下周就想上线,架构师的排期表在业务眼里只是一张纸。

我们手里的系统是一个售后工单工作台:商户提单,客服审批,退款与补发,时效报表,后面还挂着 ERP、CRM 和物流三个外部系统。表单、审批、报表都是典型到不能再典型的 CRUD 加流程,天生属于低代码的目标场景。最初一个租户、一套规则时,我们把页面和流程配出来,两周交付,业务很满意。

真正的变化不是来自技术栈,而是来自规模:租户从 1 个变成 40 个,规则从一套变成每户一套,物流计费从「按重量」变成「分区分段 + 时段系数」。同一套可视化配置,开始在同一个平台上承载三种完全不同的东西------界面、领域事务和权限边界。

低代码到底省掉了什么?是重复的表单与流程实现,还是我们本该自己承担的架构责任?

当个性化交易规则和复杂领域事务被塞进可视化配置,为什么平台的改造成本反而上升了?

能力圈的边界应该画在哪里------哪些业务可以放在平台模型里,哪些必须留给独立业务服务?

01、故事

场景是跨境商户的售后工单工作台。任务是:把商户提单、客服审批、退款补发、时效报表和 ERP/CRM/物流同步,做成一套能让业务自助调整的内部系统。

当前方案是这样组织的:页面用低代码平台的画布式编辑器拖出来,审批流用平台自带的流程模型配出来,报表用平台的可视化查询配出来,外部系统的数据通过平台连接器拉取与回写。整个过程没有一行手写后端代码,交付周期以周为单位。

这套方案在第一个阶段完全正确。问题出现在第二阶段:商户开始要求「我们家的退款规则是 T+3,他们家的必须是 T+7」「我们的物流计费要按分区分段加时段系数」。业务的第一反应是把这个需求交给配置人员,配置人员的第一反应是去看平台还有没有更灵活的表达方式。

于是发生了微妙的变化:原本用来描述界面的模型,开始被用来描述业务规则;原本用来描述流程的模型,开始被用来描述事务边界。冲突点很清晰------可视化表达的灵活度增长是线性的,而领域复杂度的增长不是。

当配置层开始承担它本来不该承担的语义时,快就不再是优势,只是把成本推到了排障和迁移那一天。

页面搭得快

责任落得慢

一处配置错

三更排障忙

02、问题

旧方案失效的信号,不是页面坏了,而是三类问题同时冒出来。

第一类是隔离失效。报表页出现了不属于当前租户的工单号,因为分租户的过滤被实现成了页面上的筛选控件,而不是数据层的约束。第二类是同步失效。审批通过后 ERP 长时间收不到状态,因为出站调用撞上限流就被直接丢弃,没有重试也没有幂等。第三类是演化失效。每加一条个性化规则,配置的嵌套层级就深一层,改一次要回归一遍,没人敢动。

业务影响是可量化的:客服在多个页面之间人工比对,运营日报的数对不上,财务对退款时效有异议时拿不到可信的链路证据。技术表现同样明确:越权查询、丢单、规则回归,三者共享同一个根因------责任被放在了错误的层次。

要证明这件事已经被解决,完成标准必须是可验证的,而不是「感觉好多了」:

  • 跨租户隔离测试通过:以任意租户身份查询,均无法返回其他租户的任何行。
  • 外部同步在限流下不丢单:被拒绝的请求必须落库,可重放且重放不产生重复写入。
  • 迁移演练可执行:把业务数据与权限模型导出重建一次,业务系统仍能正常工作。
  • 指标可度量:平均交付周期、维护人时、接口错误率三项可统计、可对比。

快省的是形

慢省的是界

标准不落地

交付即返工

03、原理

这一篇只需要两个模型,但它们必须一起用。

能力圈回答的是「我承诺什么」:只在可理解、可治理的范围内承诺交付。注意限定词是两个------可理解,意味着团队能说清它的行为;可治理,意味着出问题时你能从数据层到界面层完整定位并修复。低代码平台在 CRUD、OA、内部运营台这类场景里同时满足两条;在高并发核心交易、跨系统分布式事务里通常不满足。

机会成本回答的是「我放弃了什么」:选择快速搭建,就放弃了自由定制与迁移灵活性的一部分。这不是缺陷,是定价。问题是很多团队只记账了收益,没有记账成本。

第一个反直觉判断:低代码最贵的不是授权费用,而是数据模型与权限模型被平台结构绑定后失去的迁移选择权。当业务数据、元数据和身份权限混在同一个平台的存储结构里,你迁移的就不再是页面,而是资产。

第二个反直觉判断:把逻辑搬进可视化配置,往往不是「少写代码」,而是把代码变成了没有类型检查、没有代码评审、没有单元测试的形态。它的错误更晚暴露,定位成本更高,回归范围更难界定。

理解运行时,才能理解边界。按官方文档,Microsoft 的 Power Platform 参考架构把 Canvas 应用、模型驱动应用、Dataverse 与连接器分层列举,元数据、业务数据和集成通道是三件不同的事;Mendix 官方运行时架构说明其 Runtime 直接解释模型,因此不能把全部低代码产品一概描述成「把元数据编译成源码」;Appsmith 的构建模式则是 Widget、Datasource、Query 与 JavaScript 的组合,页面逻辑与数据访问是分离的。这三类事实指向同一个工程结论:真正决定边界的不是「解不解释模型」,而是你把哪一类责任放进了平台、哪一类留在了自己手里。

模型有人解

边界无人管

省的是实现

留的是主权

04、架构

text 复制代码
[输入] 工单事件 / 审批动作 / 报表查询
    |
    v
[模块] 低代码页面层(Widget/Canvas) + 平台模型(元数据与流程)
    |
    v
[数据/状态] 元数据(平台) + 业务数据(自有库) + 身份权限(IdP/行级策略)
    |
    v
[处理] 平台Query/连接器 + 独立业务服务API(计费/订单事务)
    |
    v
[输出] 工单状态 / ERP-CRM-物流同步 / 报表指标

这个架构的关键不是组件清单,而是边界线画在哪。

边界一:页面层与模型层属于能力圈之内。表单、列表、审批流转、基础报表,配置即可,改造成本低,业务可自助。

边界二:业务数据与身份权限属于责任区,不进平台的私有结构。业务数据落在自有数据库,租户标识是数据的一部分而不是页面的一个筛选条件;身份与权限由外部身份源和数据库行级策略共同保证。

边界三:领域事务走独立服务 API。复杂物流计费、订单状态机这类高频变更且需要测试覆盖的逻辑,保留独立服务,低代码层只负责调用与展示。

收益是双层的:页面交付保持高速,同时数据主权、集成语义和权限约束留在可迁移的位置。代价是引入了第二套发布单元和一套接口契约,团队要维护 API 网关、补偿队列和对接文档,复杂度确实上升了。

适用条件也很清楚:当业务以 CRUD 和内部流程为主,且领域规则相对稳定,这层边界可以很薄;当领域规则开始高频变化,就必须把边界加厚,否则成本会以回归风险和平台升级风险的形式返还。

页面在外层

数据在内核

服务守边界

权限不下沉

05、实战一次

先做最小的 V1:一个能录入工单、按租户查询、写库留痕的工作台。不追求完整,只要求能跑通并暴露后续问题。

环境与依赖(镜像与版本请按当前官方文档核验):

  • Docker 与 Docker Compose
  • Appsmith 社区版容器
  • PostgreSQL 16 容器

docker-compose.yml:

yaml 复制代码
services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: app_pwd
      POSTGRES_DB: ticketdb
    ports:
      - "5432:5432"
    volumes:
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro

  appsmith:
    image: appsmith/appsmith-ce:latest
    ports:
      - "8080:80"
    depends_on:
      - postgres
    volumes:
      - appsmith_data:/appsmith-stacks

volumes:
  appsmith_data:

init.sql(业务数据与出站留痕表):

sql 复制代码
CREATE TABLE ticket (
  id         bigserial PRIMARY KEY,
  tenant_id  text        NOT NULL,
  order_no   text        NOT NULL,
  channel    text        NOT NULL,
  content    text        NOT NULL,
  status     text        NOT NULL DEFAULT 'NEW',
  created_at timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX idx_ticket_tenant_created ON ticket (tenant_id, created_at DESC);

CREATE TABLE outbox_sync (
  id         bigserial PRIMARY KEY,
  ticket_id  bigint      NOT NULL REFERENCES ticket(id),
  target     text        NOT NULL,
  payload    jsonb       NOT NULL,
  state      text        NOT NULL DEFAULT 'PENDING',
  retry      int         NOT NULL DEFAULT 0,
  idem_key   text        NOT NULL UNIQUE,
  created_at timestamptz NOT NULL DEFAULT now()
);

启动:

bash 复制代码
docker compose up -d
docker compose ps

未在当前环境实测,以下为预期结果:两个容器状态为 running / healthy;浏览器打开 http://localhost:8080 可进入平台初始化流程。

在平台中新建 PostgreSQL 数据源,Host 填 postgres,端口 5432,库 ticketdb。然后建两个 Query。

写入 Query(insert_ticket):

sql 复制代码
INSERT INTO ticket (tenant_id, order_no, channel, content)
VALUES (
  {{ appsmith.store.tenantId }},
  {{ Input_order.text }},
  {{ Select_channel.selectedOptionValue }},
  {{ Input_content.text }}
)
RETURNING id, status, created_at;

查询 Query(list_ticket):

sql 复制代码
SELECT id, order_no, channel, status, created_at
FROM ticket
WHERE tenant_id = {{ appsmith.store.tenantId }}
ORDER BY created_at DESC
LIMIT 50;

页面用 Widget 摆出输入框、下拉框、表格和提交按钮,提交按钮的 onClick 走一段 JavaScript:

javascript 复制代码
{{ (async () => {
  if (!appsmith.store.tenantId) throw new Error("tenantId missing");
  if (!Input_order.text || !Input_content.text) throw new Error("order_no/content required");
  const row = await insert_ticket.run();
  await list_ticket.run();
  showAlert("工单已创建: " + row[0].id, "success");
})() }}

验证:先以 tenantId = A 提交一条工单,表格立刻出现新行;再把 store 里的 tenantId 改成 B,刷新后表格应为空。

示例输出:

json 复制代码
[{"id": 1024, "order_no": "SO-20240517-001", "channel": "LOGISTICS", "status": "NEW", "created_at": "2024-05-17T09:12:31Z"}]

V1 跑通了,但它留下了三个已经很明显的缺口:outbox_sync 没有任何消费者;连接器调用没有任何限流与退避;租户隔离完全依赖每个 Query 的作者记得写 tenant_id 条件。这三条缺口,正是下一章要排查的对象。

一库起两表

一页跑通链

先证能交付

再谈能走远

06、排查

排查一:跨租户越权。

现象:租户 B 的报表页出现了租户 A 的工单号。

怀疑:第一反应是前端表格控件的分组或缓存出了问题。

检查:不要看页面,看 SQL。把报表页绑定的那个 Query(report_ticket)取出来,直接在 psql 里执行。

证据:report_ticket 的语句是 SELECT ... FROM ticket WHERE status <> 'CLOSED',条件里根本没有 tenant_id;页面上的租户过滤来自一个「数据集筛选」控件。

根因:权限隔离被实现成了 UI 规则,而不是数据层约束。UI 规则对开发者是可选动作,对攻击者是绕过题。

修复:所有涉及业务数据的 Query 强制带 tenant_id 条件,同时在数据库层为 ticket 启用行级安全策略,形成双重约束。

错误尝试: 我先怀疑是平台缓存,重启容器并清了浏览器缓存,问题依旧。这个尝试是错的,因为缓存的错误形态是「旧数据延迟出现」,而不是「出现了本不该属于这个租户的数据集合」------集合包含关系指向查询条件缺失,与数据新鲜度无关。用重启代替观察,只会浪费一次时间窗口。

排查二:限流下状态不同步。

现象:审批通过三分钟后,ERP 仍未收到工单关闭状态;运维手动点两次重试,ERP 里出现了两条重复记录。

怀疑:一度认为是连接器丢包或网络抖动。

检查:拉取平台连接器的调用日志,再对 ERP 网关的 access log,按同一时间窗对齐。

证据:网关返回 429 并带 Retry-After;平台侧没有任何重试记录;手动重试走的是同一条无幂等键的写入路径,因此产生了重复。

根因:出站同步缺少幂等键与重试队列,被限流直接失败即丢单,重试即重复。

修复:所有出站写入先落 outbox_sync,用 idem_key 唯一约束保证幂等,由独立 worker 做指数退避重放,并统一经网关限流。

集合出越权

重启误根因

限流丢一单

幂等补一回

07、优化

V2 的每一项修改都必须来自第 06 章的证据,不能凭感觉加东西。

根因一:权限约束不在数据层。修改:为 ticket 启用行级安全策略,会话级设置租户上下文;所有低代码 Query 显式带租户条件。原因:UI 层的过滤是可选动作,数据层约束是强制动作。新行为:即使有人漏写条件,跨租户查询也只能返回空集。验证:用租户 B 的身份执行报表 Query,结果为空,且日志中能看到策略生效。

sql 复制代码
ALTER TABLE ticket ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON ticket
  USING (tenant_id = current_setting('app.tenant_id', true));

根因二:出站同步无幂等、无重试。修改:写库与出站解耦,使用 outbox_sync 表承载待发送事件,idem_key 唯一约束兜底。原因:限流是外部系统的正常行为,必须按正常路径处理,而不是按异常路径丢弃。新行为:被拒绝的请求留在库中并可重放,重放不会产生重复记录。验证:构造一次 429 响应,确认事件状态从 PENDING 流转到 SENT,且 ERP 侧只出现一条记录。

根因三:领域逻辑侵入配置层。修改:把物流计费与订单状态机移出可视化配置,保留独立业务服务 API,低代码层只做调用与展示。原因:这两块属于高频变更、必须可测试的领域事务,留在配置层等于把它们放进一个没有单元测试的环境。新行为:规则变更走服务发布与回归测试,页面不变。验证:改一次计费系数,页面无需重新配置即可生效。

验证方案(不预设数字,只给方法):隔离测试用自动化用例枚举租户组合;补偿测试用故障注入模拟 429 与超时;迁移演练把业务数据导出到独立库后重连平台;指标层面统计平均交付周期、维护人时、接口错误率三个维度的前后对比。

病根在数据

药落补偿层

计费出平台

边界收回来

08、演进

text 复制代码
[同一输入]
    |
    +--[V1] 页面直连库与连接器 / 快,但越权与丢单靠人盯
    |
    +--[V2] 页面 + 数据层隔离 + outbox 补偿 + 独立业务服务
    |
[Trade-off]
{得到:可迁移的边界与可验证的隔离;失去:一次性配置的速度优势与部分交付自由度;适用边界:CRUD/OA/内部运营台,不承载高并发核心交易}

正确性上,V1 的正确性依赖每个配置作者的自觉,V2 把关键约束下沉到数据库与队列,正确性由结构保证。稳定性上,V1 遇到限流即失败,V2 把外部波动吸收在 outbox 层。复杂度上,V1 只有一个发布单元,V2 多了 worker 与业务服务两个单元,运维面扩大。成本上,V1 前期投入最低,V2 前期投入更高但把成本从「每次事故排障」前移成了「一次性的架构建设」。适用范围上,V2 的做法在 CRUD 场景里是过剩的,在跨系统事务场景里是必需的。

遗留问题也要写清楚:worker 的告警还没做,outbox 表的清理策略还没定,多租户的行级策略在高并发下的连接级设置开销还没压测。这些不是设计缺陷,是下一个迭代的输入。

旧路快而窄

新路慢而宽

取舍非优劣

只在边界间

09、洞见

9.1 能力圈不是能力上限,而是承诺边界

讨论低代码时,团队最常争论的是「平台能不能做到」。这个问题问错了方向。平台通常能做到,代价是你要承诺一堆你并不打算长期治理的东西。反直觉判断:低代码平台的能力上限很少成为真正的障碍,真正的障碍是团队承诺的范围超过了可治理的范围。所以画能力圈时,判断标准不是「能不能配出来」,而是「出事时我能不能从数据层到界面层完整定位并修复」。圈内承诺,圈外留接口。

9.2 机会成本:选「快」就放弃了迁移自由度

反直觉判断:低代码最贵的一笔账,不在授权费,而在数据与权限模型被平台结构绑定后失去的迁移选择权。选择快速搭建的那一刻,你就已经把「随时换掉这个平台」这个选项折价卖掉了。这不是说不能选,而是说必须把这笔机会成本写进决策记录,而不是等到迁移评估时才第一次看见它。

9.3 配置承担领域逻辑,就变成没有编译器的代码

把逻辑搬进可视化配置,账面代码量下降,但复杂度并没有消失,只是从「有类型检查、有代码评审、有单元测试」的形态,转移到了「靠人眼与回归」的形态。这个转移在规则稳定时看不出代价,在规则高频变化时会以排障时间和回归风险的形式集中返还。判断标准很简单:这块逻辑如果三个月改一次并需要测试覆盖,它就不该待在配置层。

9.4 三类责任不能外包:元数据、业务数据、身份权限

官方架构文档把应用模型、数据平台与连接器分层列举,这不是产品介绍,而是责任提示。元数据可以托管在平台,因为它描述的是界面与流程;业务数据必须有明确的归属与备份方式,因为它属于资产;身份权限必须有独立于页面的约束点,因为它属于安全边界。把这三件事混在一起交给配置层,得到的不是集成,而是耦合。

圈内才承诺

圈外留接口

快是机会价

慢是迁移金

10、系统落地

原来有什么:一个用低代码平台搭起来的工单工作台,表单、审批、报表全部配置化,页面直连数据库和外部连接器,交付快但隔离、补偿和迁移都靠人盯。

本篇新增什么:三类归属的划分(平台元数据、自有业务数据、独立身份权限),数据库行级租户隔离,outbox_sync 出站补偿队列与幂等键,以及物流计费与订单事务的独立业务服务 API 边界。

现在能做什么:在能力圈内继续以周为单位交付标准化页面;同时保证任何一条查询都无法跨租户返回数据;外部系统被限流时不会丢单,重放不会产生重复;规则变更走服务发布,不需要重新配置页面。

还缺什么:worker 的失败告警与 outbox 清理策略;多租户行级策略在连接级设置下的性能压测;自动化隔离测试集尚未进流水线;平均交付周期、维护人时、接口错误率三个指标还没有落地看板。

下一步如何演进:先把隔离测试与补偿重放测试做成流水线的一部分,让架构约束变成提交门禁;再评估还有哪些模块属于「高频变化 + 需要测试覆盖」,继续从配置层向业务服务迁移;最后用迁移演练反向验证边界是否真的可迁移。

先定三类权

再划两层界

缺的记清楚

下一步再补

11、小结

text 复制代码
Q1 → 低代码省掉的是重复的表单与流程实现,省不掉数据主权、系统集成与权限隔离
Q2 → 配置层承担领域事务时缺少编译期与评审,成本转移到平台升级与排障
Q3 → 边界是:CRUD/OA/内部运营台可承载,高并发核心交易与跨系统分布式事务走独立服务
状态 → 一套带租户隔离、出站补偿与迁移演练路径的工单工作台 V1 + V2

问的是省什么

答的是不能省

页面可低代码

责任须高架构

12、作业

12.1 理解题:能力圈与机会成本在低代码选型中分别回答什么问题?

参考答案:能力圈回答「我承诺交付什么」,标准是可理解、可治理;机会成本回答「我为这次选择放弃了什么」,低代码选择快速搭建时放弃的是自由定制与迁移灵活性的一部分。两者必须同时评估:只看能力圈会高估平台,只看机会成本会否定一切工具。

12.2 实战题:如何让工单表在数据层具备租户隔离能力?

参考答案:启用行级安全并建立策略,会话开始时设置租户上下文,同时所有查询显式带租户条件,形成双重约束。

sql 复制代码
ALTER TABLE ticket ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON ticket
  USING (tenant_id = current_setting('app.tenant_id', true));
-- 会话侧
SET app.tenant_id = 'tenant_A';

验证方式是用租户 B 的身份执行同一查询,结果应为空集。

12.3 排障题:审批通过后 ERP 长时间未收到状态,手动重试又出现重复记录,如何定位与修复?

参考答案:先对齐平台连接器日志与 ERP 网关 access log,确认返回码是否为 429 或超时,以及平台侧是否存在重试记录;再确认写入路径是否有幂等键。重复记录说明写入不幂等,丢单说明失败被直接放弃。修复方向是出站事件先落库、用幂等键唯一约束兜底、由独立 worker 指数退避重放,并统一经网关限流。

12.4 架构判断题:团队要把「分区分段 + 时段系数」的物流计费塞进可视化配置,是否同意?

参考答案:不同意。计费属于高频变更且需要测试覆盖的领域事务,放进配置层会让每次系数调整都变成平台内变更与全量回归,且缺少单元测试手段。正确做法是保留独立计费服务 API,低代码层只负责页面与调用,规则变更走服务发布流程。

做题对边界

动手验隔离

排障找证据

判断看取舍

13、思考

回到最初那个冲突:低代码到底省掉了什么。答案不是「省掉了代码」,而是「省掉了重复的界面与流程实现,同时把一部分自由定制与迁移能力作为机会成本付了出去」。这笔成本本身没有对错,只有是否被记账。

能力圈的价值也在这里。它不是给平台划上限,而是给团队的承诺划边界:圈内的事,用最省的方式做;圈外的事,用最可控的方式留接口。数据主权、系统集成、权限隔离这三类责任,无论页面用不用低代码,都必须由你自己承担,区别只在于你是主动承担,还是等到出事故那天被动承担。

页面可以低代码,责任不能低架构。

一念辨边界

一证定根因

一改知取舍

一役见真章

相关推荐
智慧光迅AINOPOL1 小时前
10~50 人规模办公网络组网技术分析:架构对比与选型参考
网络·架构·智能路由器
涵美女程序猿1 小时前
ChatCut online 剪微服务熔断测试录屏:仪表盘告警、降级日志与恢复时刻怎样对应
程序人生·微服务·云原生·架构·电脑
@PHARAOH2 小时前
WHAT - Vite 8(Rolldown 底层构建引擎升级)
前端·架构
hhb_6183 小时前
智联AI办公微服务技术全解析
人工智能·微服务·架构
智恒百亿3 小时前
5090 八卡服务器踩坑实录:架构、多卡通信、PyTorch Distributed 一次说清楚
服务器·pytorch·架构
阿文和她的Key4 小时前
国产AI芯片涨价20%-50%:企业算力采购的四层成本架构重估
人工智能·架构
安易算力4 小时前
智算中心网络架构选型:RDMA、InfiniBand与以太网技术深度对比及部署实践
网络·架构
用户777397909934 小时前
REA 逆向工程炸出 15W+ 热度,我却在想另一件事:当 AI 写的代码比你能读的还多,怎么办?
架构
Thneonl5 小时前
Spark 比 MR 快在哪?管住最贵的 shuffle
后端·架构