自建表與資料模型擴充
預建的 ERP 表覆蓋了通用的企業流程,但每個產業都有自己的東西——健身房要記量測數據、旅行社要記團體行程、補習班要記課堂出席。這一章說明怎麼擴充資料模型。
三條擴充路徑
| 路徑 | 適合 | 可查詢/過濾 | 有型別約束 | 可建關聯 |
|---|---|---|---|---|
custom_data JSONB | 少量、非結構化的附加欄位 | 有限 | 否 | 否 |
| 延伸欄位 | 在既有 ERP 表上加欄位 | 是 | 是 | 是 |
| 自建表 | 全新的資料實體 | 是 | 是 | 是 |
判斷方式很簡單:這筆資料是既有實體的附加屬性,還是一個新實體? 客戶多一個「會員卡號」是前者(延伸欄位);「課堂出席紀錄」是後者(自建表)。
自建表:租戶層級的資源
這是自建表最重要、也最容易被誤解的特性:
自建表綁的是組織,不是應用程式。
同一個組織下的所有 Custom App、以及組織自己的資料中心介面,看到的是同一批表、同一份資料。
這帶來一條必要紀律:建表前先確認有沒有可重用的既有表。同一張「客戶」表不該因為兩個應用各建一次,就分裂成兩份互不相通的資料。
雙軌命名:顯示名與實體名
每張表、每個欄位都有兩個名字:
| 顯示名 | 實體名 | |
|---|---|---|
| 誰決定 | 您提供 | 系統從顯示名自動生成,或由您顯式指定 |
| 可否修改 | 隨時可改 | 建立後永不可變 |
| 字元集 | 任意(中文常見) | 純 ASCII |
| 用途 | 介面呈現 | API 識別、關聯目標指定 |
所有 API 與 SDK 在指涉既有表/欄位時一律使用實體名。 顯示名只是標籤——改顯示名不會影響任何既有的程式碼或引用。
實體名的生成會避開三個母體:同組織既有的自建表名、SQL 保留字、以及平台內建的 ERP 表名。例如顯示名「Customers」在空白環境下會得到 customers_2 而不是 customers,因為後者已被 ERP 佔用。
若您顯式指定的實體名撞到上述任一項,系統會回報衝突並明確告訴您撞到哪一類,而不是靜默改名。
欄位型別
| 型別 | 說明 | 額外契約 |
|---|---|---|
text | 文字 | |
number | 數值 | |
boolean | 布林 | |
date | 日期 | |
datetime | 日期時間 | |
select | 單選 | 必須提供選項集;值受資料庫層級約束 |
relation | 關聯 | 見下節 |
json | 結構化資料 | |
image | 圖片 | 見下節 |
每張表都會自動帶系統欄位(識別碼、建立時間、更新時間)。系統欄位不可刪、不可改型別,也不計入欄位配額。
relation 欄位
relation 可以指向兩種目標:另一張自建表,或平台內建的 ERP 表。
這代表您可以建立一張「課堂出席紀錄」自建表,其中的「學員」欄位直接關聯到 ERP 的 customers 表——不需要複製一份客戶資料,也不需要自己維護對應關係。
image 欄位
image 欄位提供完整的圖片處理:上傳走平台代理(而非前端直接上傳到儲存空間),讀取時取得帶簽章的臨時網址,並受跨組織存取控制保護。圖片會計入組織的儲存配額。
誰能改結構
結構操作僅限管理員(持有 system.admin 的使用者):建表、改表、刪表、建欄位、改欄位、刪欄位、以及 ERP 延伸欄位的定義。
資料操作(記錄的增刪改查)與結構讀取維持一般開發權限(builder.access)。
管的是 schema 的形狀,不是它的使用。這道授權在入口層執行,且三個入口(REST API、SDK、AI 工具)語意一致——AI 通道不能繞過 REST 的權限閘。
刪除是兩段式的
刪表與刪欄位都需要兩步,且由伺服器端強制,不是前端的確認彈窗:
- 影響預覽:系統先告訴您這個刪除會影響什麼——有多少筆資料、有哪些關聯指向它
- 實體名強確認:您必須輸入該表/欄位的實體名才能執行
刪除不可復原。這道設計刻意讓誤刪變得困難。
三個入口,一層服務
自建表的所有操作——結構與資料——都收斂到單一服務層。三個入口共用它:
| 入口 | 對面是誰 | 身分來源 |
|---|---|---|
| REST API | 組織使用者(資料中心介面) | 登入使用者的 session |
SDK(ctx.db) | Custom App 的執行期程式碼 | 應用程式的執行身分 + Scope |
| Builder 工具 | AI 開發助手 | Builder session 的組織身分 |
這層服務是唯一的護欄所在地。 入口不重新實作驗證,也不繞過它:
- 租戶隔離:所有操作以組織為錨;跨組織存取一律回報「不存在」,不洩漏他人資源是否存在
- 結構操作序列化:結構變更走組織層級的鎖,併發建表不會互相撕裂
- 配額:每組織的表數上限、每表的欄位數上限,在鎖內檢查以防併發繞過
- 型別與值驗證:欄位型別、選項集、預設值、關聯目標的合法性
- 錯誤反譯:資料庫層級的錯誤(唯一鍵衝突、必填為空、外鍵違反、依賴物件存在)會翻成結構化錯誤碼與可行動的訊息,不會裸拋技術訊息給使用者
任何新入口都應該打這層,而不是自己拼 SQL。
從 SDK 存取自建表
def execute(ctx):
# 列出組織的所有自建表
tables = ctx.db.list_tables()
# 查詢(用實體名)
rows = ctx.db.query_table('attendance', filters=[
{'column': 'class_date', 'op': 'gte', 'value': '2026-08-01'}
])
# 新增
ctx.db.insert_row('attendance', {
'student_id': ctx.params['customer_id'], # relation 指向 ERP customers
'class_date': '2026-08-10',
'status': 'present',
})
# 更新 / 刪除
ctx.db.update_row('attendance', row_id, {'status': 'late'})
ctx.db.delete_row('attendance', row_id)
前端則透過內建的 src/db.ts SDK 呼叫,語意相同。詳見第 8 章。
ERP 延伸欄位
延伸欄位讓您在既有 ERP 表上新增真正的欄位——與 custom_data 不同,它可查詢、可過濾、有型別約束,也能被引用系統授權。
適用時機:這筆資料是既有實體的固有屬性,且您需要用它來篩選或排序。例如在 customers 上加「會員到期日」,之後就能查詢「本月到期的會員」。
刪除延伸欄位同樣走兩段式流程,並提供影響預覽。