ToolJet + PostgreSQL:后台表格、查询和流程,一次搭成可远程访问的业务系统

每个团队手里都攒着一堆「就差一个表格页面」的需求:订单要能改状态、库存要能按条件查、请假单要能走一遍审批。后台表格、查询页、审批流程这三类东西,需求方觉得是小活,研发排期却一排就是一两周。

常规做法是 Spring Boot 加 Vue 自研一套 CRUD,可每来一个新表就重写一遍列表、筛选、分页和按钮逻辑,写完还得处理登录、权限、部署。代码量不大,但重复度极高,改一个字段要动三层。

本文分享一套把 PostgreSQL 直接变成业务系统的落地路径:ToolJet 做界面与流程编排 + PostgreSQL 做数据与约束 + Caddy 做远程访问收口,全程 Docker 起服务,可直接落地。

一、重复劳动的现场:三类需求吃掉八成排期

先把这些「小需求」拆开看,它们的共同点非常明确:

  • 表格页 :列表、筛选、分页、行内编辑,本质是 SELECT 加一个 UPDATE 按钮。
  • 查询页:几个输入框加一个条件组合,本质是一条参数化 SQL。
  • 流程页:提交、审核、驳回、通知,本质是状态机加一次消息推送。
  • 共同点:全部围绕已有的表结构,逻辑不超过一屏代码,却要套上完整的前后端工程。

查询 SQL → 状态流转 → 通知推送,这条链路在几乎每个内部系统里都会原样出现一遍。

**核心结论:**这些需求缺的不是代码能力,而是一个不用写代码的运行时。

二、选型依据:为什么落点是 ToolJet 而不是自研

把几个常见方案按「内部工具」这个场景比一遍:

维度 ToolJet Appsmith 自研 CRUD
开源与部署 CE 版可自托管 CE 版可自托管 完全自主
PostgreSQL 支持 一等公民,自带查询器 支持,需装插件 需自己写 DAO
内置工作流 Workflow 独立模块 较弱,靠 JS 编排 自己实现状态机
权限粒度 组 + 页面 + 查询级 组 + 页面级 自己设计
新增一张表的成本 十分钟级 半小时级 一到两天
  • 自托管优先:ToolJet CE 全部组件跑在自己机器上,业务表不出内网。
  • SQL 即逻辑:查询直接写原生 SQL,复杂 join 不用迁就 ORM。
  • Workflow 独立:审核流程不塞在页面 JS 里,单独可视化编排。

**核心结论:**内部工具的胜负手是「改一个字段的成本」,ToolJet 把这个成本压到了配置层。

三、起服务:Docker Compose 一次拉起 ToolJet 与元数据库

ToolJet 自身的用户、应用、查询定义也要存库,所以起两个 PostgreSQL:一个存元数据,一个放业务数据。

下面这段 compose 用 CE 版镜像起 ToolJet 与元数据库,业务库可以复用现有实例:

yaml 复制代码
services:
  tooljet:
    image: tooljet/tooljet-ce:latest
    depends_on: [postgres]
    environment:
      - NODE_ENV=production
      - TOOLJET_HOST=https://tooljet.internal
      - DB_HOST=postgres
      - DB_USER=tooljet
      - DB_PASS=change_me
      - LOCKDOWN_DEFAULT_WORKSPACE=true
    ports: ["8080:8080"]
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: tooljet
      POSTGRES_PASSWORD: change_me
    volumes: ["pgdata:/var/lib/postgresql/data"]
volumes:
  pgdata:

强约束:锁库账号 → 只读业务账号 → 页面级白名单,三个开关在起服务时就要定下来。

四、接数据源:把 PostgreSQL 挂进 ToolJet 并做好账号隔离

业务库绝不能用超级用户接进来,先在库里准备好专用账号:

sql 复制代码
-- 只给 ToolJet 查询用的只读账号
CREATE ROLE tj_reader LOGIN PASSWORD 'strong_pwd';
GRANT CONNECT ON DATABASE biz TO tj_reader;
GRANT USAGE ON SCHEMA public TO tj_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO tj_reader;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
  GRANT SELECT ON TABLES TO tj_reader;
  • 账号分级 :查询页绑只读账号,写操作单独绑一个只有 INSERT/UPDATE 权限的账号。
  • SSL 强制:数据源配置里勾选 Require SSL,跨机器访问不走明文。
  • Schema 白名单:只暴露业务 schema,元数据库不出现在数据源列表里。
  • 连接池留意 :ToolJet 默认每个查询新建连接,高并发页面要给 PostgreSQL 留足 max_connections。

**核心结论:**权限收在数据库层最稳,页面层再怎么配也拦不住一条手滑的 DELETE。

五、后台表格:从建表到 CRUD 页面的字段设计

建表时就把页面要用的东西设计进去,能省掉大量后续补丁:

字段 类型 设计意图
status varchar + CHECK 状态只允许白名单值,页面下拉框直接读约束
created_at timestamptz 默认 now() 列表默认按它倒序,分页稳定
updated_by varchar 行内编辑时由页面自动填入当前账号
version int 默认 0 乐观锁,防止两个人同时改一行

页面侧的操作建议:

  • 列表绑定查询 :拖一个 Table 组件,数据源指向上一步建好的 SELECT 查询。
  • 行内编辑走 UPDATE :编辑保存动作绑定 UPDATE ... WHERE id = {``{table.selectedRow.id}} AND version = {``{...}}。
  • 行数为 0 也要能看:加一个空状态提示,避免需求方以为系统挂了。

**核心结论:**把状态白名单、时间戳和乐观锁放进 DDL,页面就只剩拖拽配置。

六、查询页:参数化查询与前端联动

查询页的核心是一条绑定页面变量的参数化 SQL,ToolJet 用 :参数名 或 {``{组件值}} 占位:

sql 复制代码
-- 绑定三个页面控件:状态下拉、起止日期
SELECT o.id, o.amount, o.status, c.name AS customer,
       ROUND(EXTRACT(EPOCH FROM (o.paid_at - o.created_at))/3600, 1) AS hours_to_pay
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.status = :status
  AND o.created_at BETWEEN :from AND :to
ORDER BY o.created_at DESC
LIMIT 100;
  • 先跑再绑:查询编辑器里先填常量验证 SQL 正确,再把常量换成绑定表达式。
  • 分页放数据库 :别在前端切片,LIMIT/OFFSET 或游标交给 PostgreSQL。
  • 导出复用查询:导出按钮直接复用同一个查询,避免维护两份 SQL。

**核心结论:**一条参数化 SQL 加三个控件绑定,就等于一个查询页。

七、流程页:用 Workflow 把审核和通知串起来

ToolJet 的 Workflow 独立于页面存在,适合放状态机。链路长这样:

表单提交 → 创建待审记录 → 审核人收到通知 → 通过则改状态并写日志 / 驳回则回到申请人

Workflow 节点之间用 JavaScript 节点做格式转换,例如把金额从分转成元再推送给审核人:

js 复制代码
// Workflow 的 Code 节点:整理待推送的审批摘要
const rows = queries.fetchPending.list;
const items = rows.map(r => ({
  id: r.id,
  applicant: r.applicant,
  amount: (r.amount / 100).toFixed(2)
}));
const total = items.reduce((s, r) => s + Number(r.amount), 0);
return { items, summary: `共 ${items.length} 单,合计 ${total.toFixed(2)} 元` };
  • 审核动作落库:通过/驳回按钮只触发 Workflow,页面不直接写表,避免状态被绕过。
  • 日志单独一张表 :谁在什么时间改了什么状态,全部追加写入 audit_log。
  • 失败要重试:通知节点失败不应阻断状态变更,配置失败分支只记日志。

**核心结论:**状态变更全部走 Workflow,页面只负责收集参数,流程才不会被旁路。

八、权限体系:组、页面、查询三层管控

权限配错是内部工具最常见的事故源,按三层来收口:

  • 用户组:按角色建组(申请员、审核员、管理员),人员只进组不单独授权。
  • 页面可见性:审核页只对审核员组可见,别用「藏按钮」代替隐藏页面。
  • 查询可执行性:高危查询(如批量删除)单独建查询并绑定管理员组,页面拿不到就调不动。

检查顺序建议是组 → 页面 → 查询逐层往下验,反着查很难定位是哪一层漏了。

**核心结论:**页面隐藏只是体验,查询不可执行才是安全。

九、远程访问:反向代理与 HTTPS 收口

ToolJet 直接暴露公网等于把数据库查询能力公开,前面必须加一层代理:

bash 复制代码
# Caddy 配置:自动签证书并转发到本机 ToolJet
tooljet.example.com {
  reverse_proxy 127.0.0.1:8080
  header_up X-Forwarded-Proto {scheme}
  encode gzip
}
  • 只开 443 :8080 仅监听 127.0.0.1,防火墙不放行。
  • TOOLJET_HOST 必须改成公网域名:否则邮件里的链接、OAuth 回调都指错地址。
  • 加一层 IP 白名单:远程访问场景下,把办公网出口 IP 写进代理规则。
  • 先内网验证再上公网:证书、回调、WebSocket 三项都在内网走通一遍。

**核心结论:**远程访问的正确姿势是「代理收口 + 域名固定 + 白名单」,而不是把 8080 直接映射出去。

十、上线前检查清单:踩坑与排错对照表

把上线前最容易翻车的几项列成清单,逐条勾:

现象 常见原因 处理动作
页面打开但查询全部超时 业务库连接数被元数据库占满 分库并调大 max_connections
保存页面报权限错误 业务账号被误配成只读却执行了写查询 拆分读写两个数据源
远程访问白屏 TOOLJET_HOST 仍是 http://localhost 改为公网域名并重启容器
审核后状态没变 审核按钮直接写表,绕过了 Workflow 把写操作改为触发 Workflow
表格数据串行 两人同时编辑同一行 启用 version 乐观锁
  • 备份别只备份业务库:ToolJet 元数据库丢了,所有页面和流程定义一起丢。
  • 升级先在测试实例演练:CE 版跨大版本升级要跑迁移脚本,直接滚生产容易卡住。

**核心结论:**上线前把「连接、权限、域名、流程、并发」五项各验一次,能挡掉九成故障。

结语

n后台表格、查询和流程这三类需求,几乎每个团队都会反复遇到。把它们从自研 CRUD 里解放出来,用 ToolJet 承担界面与编排、PostgreSQL 承担数据与约束、Caddy 承担远程访问收口,新增一个业务模块就从「排期一周」变成「配置半小时」。

这套方案的价值不在省代码,而在于把权限、审计和流程闭环沉淀成平台能力,业务变化时只改配置不动工程。把重复的 CRUD 交给运行时,把精力留给真正有业务含义的规则。

相关推荐
先知后行。1 小时前
各种文件系统
linux·服务器·数据库
风若寒891 小时前
MySQL 主从同步,从库中断时间过长,需要主库全量进行覆盖一次
数据库·mysql·主从
l1t2 小时前
duckdb、clickhouse、datafusion、cedardb、tableauhyperapi 5种分析型数据库执行递归求数独SQL比较
数据库·sql·clickhouse
泡海椒2 小时前
JQuick-SQL Worker 生命周期:端口、启动与关闭规范
数据库·sql
麦壳饼2 小时前
SonnetDB:KNN 查询的距离语义与维度边界
数据库·sonnetdb
知行产研2 小时前
三年三起收购,拆解卡特彼勒AI并购版图
数据库·人工智能·mysql
flytalei2 小时前
从 Docker Compose 到 K3s:逐浪笔记首次完整部署与故障排查实战
笔记·docker·容器
2603_954708312 小时前
微能网协调控制箱突破传统微电网架构瓶颈
大数据·数据库·人工智能·架构·能源