> ## Documentation Index
> Fetch the complete documentation index at: https://docs.alforse.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 架構

> 給開發人員的 Alforse 技術堆疊導覽：兩個應用程式、後端閘道、權限與列級安全性、資料庫、發件匣與執行器，以及工作流程與 CAD 的引擎。

本頁寫給要與 Alforse 整合，或想了解畫面背後運作方式的開發人員。內容先追蹤一次從瀏覽器寫入資料庫、再傳回的完整過程，然後逐層說明。

## 概觀

Alforse 由兩個共用同一套製造資料模型的應用程式組成：

| 應用程式 | 主機 | 用途 |
| - | - | - |
| **ERP** | `app.alforse.com` | 負責業務面：報價、採購、規劃、庫存、會計、出貨。 |
| **MES** | `mes.app.alforse.com` | 擷取並指揮生產現場的作業，通常在每個工作中心使用平板電腦。 |

兩者背後是同一個後端。

### 如何閱讀本頁

先看[系統概覽](#the-system-at-a-glance)，再看[完整追蹤一次寫入](#tracing-one-write-end-to-end)；後者以前者為前提。之後各節自成一體，可以任意順序閱讀。

技術術語會連結到頁尾[詞彙表](#glossary)中的簡短定義，例如 [Postgres](#postgres)。

NOTE

**路徑與數量可能會變動**

以下每個檔案路徑、行號與數量，都反映撰寫當時的儲存庫狀態。請將它們視為指引，而非保證。

***

## 系統概覽

所有內容都放在單一 [monorepo](#monorepo) 中，以 pnpm workspaces 與 [Turborepo](#turborepo) 運作，分為兩層。

### 畫面

兩個 React Router 應用程式 `apps/erp` 與 `apps/mes` 在伺服器端渲染其 React。它們共用的程式碼打包在 `packages/web/*` 之下；其中包括 `auth`、`database`、`api`、`jobs`、`planning`、`react`、`utils` 與 `workflows`。

### 後端

應用程式主機只有一個，即 `apps/backend`。除了提供 API 之外，它還組合了以下原生層：

| 資料夾 | 內容 |
| - | - |
| `kernel/` | 各領域共用的契約，例如授權、身分、核准、事件、記錄與工作流程，以及交易埠。 |
| `modules/` | 跨產品使用的主資料。例如：單位、產品、含地址的往來對象、文件編號與人員。 |
| `products/` | 十七個產品領域；`inventory-and-logistics`、`quality-management` 與 `source-to-pay` 是其中三個。 |
| `packages/database/` | SQL 遷移、套用遷移的執行器，以及 Postgres 儲存層。 |

所有資料都存放在同一個 [Postgres](#postgres) 資料庫中，它是記錄系統。它依公司套用列級權限，也包含供給背景作業的交易式發件匣。每個部分都在[逐層說明](#layer-by-layer)中解釋。

### 塑造一切的五項特性

1. **畫面從不直接存取資料庫。** ERP 與 MES 的伺服器端程式碼與掛載於 `/api/web` 的後端閘道溝通。閘道理解畫面最初撰寫時所用的同一套 REST、RPC、驗證與儲存協定，並把每個呼叫轉交給位於 `kernel/`、`modules/`、`products/` 或 `packages/database/` 之一的原生擁有者。
2. **資料庫讓租戶彼此隔離。** 列級安全性意味著一家公司的資料列無法被另一家公司存取；[驗證與授權](#authentication-and-authorization)說明其做法。
3. **相關的寫入一起提交。** 原生擁有者參與呼叫端的工作單元，其引發的任何事件都在同一筆交易中寫入發件匣。
4. **沒有獨立的工作系統。** 後端程序內的執行器處理排程與佇列中的任務，另有一個投遞工作者負責送出通知與 webhook。詳情見[寫入之後](#after-the-write-events-and-background-work)。
5. **CAD 處理同樣在後端內。** 幾何轉換與組裝規劃都在 `/api/web/assembler` 之後進行；不需要另外部署其他東西。

***

## 完整追蹤一次寫入

範例：採購人員儲存一張新的採購單。請跟著這個請求走過表單、驗證、權限檢查、寫入資料庫、文件編號配置，最後到重新整理的畫面。

### 以一張圖呈現整趟旅程

```mermaid theme={null}
sequenceDiagram
  participant B as Browser
  participant E as ERP server (React Router)
  participant G as Backend gateway
  participant P as Postgres
  B->>E: POST form
  E->>E: validate with zod schema
  E->>G: requirePermissions(create: purchasing)
  G-->>E: permission decision
  E->>G: rpc get_next_sequence
  G-->>E: allocate PO000123
  E->>G: insert purchaseOrder + children
  G->>P: write rows and outbox row
  E-->>B: 302 redirect
  B->>E: GET the new order page
  E->>G: loader reads through the gateway
```

資料列一儲存完成，請求就結束了。通知與 webhook 等副作用稍後才發生，由發件匣取出處理。

### 程式碼中的逐步過程

#### 1. 按鈕

`apps/erp/app/modules/purchasing/ui/PurchaseOrder/PurchaseOrdersTable.tsx:535`

```tsx theme={null}
<New label={t`Purchase Order`} to={path.to.newPurchaseOrder} />
```

請注意，這是通往表單的連結，而不是提交按鈕。在 UI 中，`permissions.can(...)` 會隱藏使用者不能做的事，這純粹是一種體貼；真正有效的判斷是在伺服器上做出的。連結一律透過 `path.to.*`，也就是 `apps/erp/app/utils/path.ts` 中的路由登錄表，因此應用程式的 URL 絕不會用手打。

#### 2. 表單

`apps/erp/app/modules/purchasing/ui/PurchaseOrder/PurchaseOrderForm.tsx:116`

表單元件是 `ValidatedForm`（套件 `@alforse/web-form`）。底層它就是一般的 HTML 表單；在其上，它會顯示 schema 的錯誤，無論錯誤來自瀏覽器或伺服器。驗證器與預設值透過 props 傳入。

#### 3. Schema

`apps/erp/app/modules/purchasing/purchasing.models.ts:146`

規則寫在 `purchaseOrderValidator` 中，它是一個 [zod](#zod) 物件，同時產生執行期檢查與對應的 TypeScript 型別。HTML 表單值由 `zod-form-data`（`zfd`）整理：例如 `zfd.text()` 會把空欄位的 `""` 對應為 `undefined`。瀏覽器與伺服器共用這同一個 schema，因此套用相同的規則。

#### 4. 路由

`apps/erp/app/routes/x+/purchase-order+/new.tsx:30`

```ts theme={null}
export async function action({ request }: ActionFunctionArgs) {
  assertIsPost(request);
  const { client, companyId, companyGroupId, userId } =
    await requirePermissions(request, {
      create: "purchasing",
      bypassRls: true
    });

  const formData = await request.formData();
  const validation = await validator(newPurchaseOrderValidator).validate(
    formData
  );
  if (validation.error) return validationError(validation.error);
  /* ... check that the supplier and location belong to the company ... */
  const result = await insertPurchaseOrder(client, {
    ...validation.data,
    companyId,
    companyGroupId,
    createdBy: userId,
    customFields: setCustomFields(formData)
  });
  /* ... flash an error and redirect, or redirect to the new order ... */
}
```

Alforse 中的寫入都遵循同一個順序：

> **1. 確認請求方法，2. 檢查權限，3. 驗證，4. 呼叫服務，5. 重新導向。**

`apps/erp/app/routes/` 底下的路由檔案可以匯出三樣與此流程相關的東西：

| 匯出 | 職責 | 執行於 |
| - | - | - |
| [`loader`](#loader) | 取得頁面所需的資料 | 僅伺服器 |
| [`action`](#action) | 處理送出的表單 | 僅伺服器 |
| `export default` | 頁面的 React 元件 | 先在伺服器，再在瀏覽器 |

`loader` 與 `action` 都不會進入瀏覽器的 bundle，這就是兩者可以使用僅限伺服器的程式碼的原因。

#### 5. 服務函式

`apps/erp/app/modules/purchasing/purchasing.service.ts:1663`（`insertPurchaseOrder`）

資料存取委派給服務函式，絕不在路由中進行。每個服務函式的第一個參數是 client，並以 `supabase-js` 熟悉的 `{ data, error }` 形式回應。該 client 由 `getAlforseServiceRole()`（`packages/web/auth/src/lib/supabase/client.server.ts:13`）建立；儘管名稱是歷史遺留，它是以本次請求登入的使用者身分，與位於 `/api/web` 的原生閘道溝通。一個 client 只屬於一次請求，無法在其他請求中使用。

```ts theme={null}
const seq = await client.rpc("get_next_sequence", {
  sequence_name: "purchaseOrder",
  company_id: input.companyId
});

const order = await client
  .from("purchaseOrder")
  .insert({
    purchaseOrderId,
    supplierId: input.supplierId,
    /* ... */
  });
```

服務接著會儲存該訂單的交貨與付款資料列。其中每個 client 呼叫都以獨立請求送到閘道。若一組寫入必須同時成功或同時失敗，閘道會把它轉給一個原生擁有者，由後者在單一資料庫交易中執行整個操作（見[寫入資料](#writing-data)）。

#### 6. 資料庫中的編號

`client.rpc("get_next_sequence", ...)` 這個呼叫是一個 [RPC](#rpc)，由編號模組 `modules/master-document-numbering/` 回應。該模組在與送出動作相同的交易內，領取下一個計數值，並對它套用公司的前綴、補零與日期代碼。[編號序列](https://docs.alforse.com/zh-Hant/docs/reference/sequences.md)說明其格式。

#### 7. 回到畫面

最後，action 執行 `throw redirect(...)`。React Router 接著載入目的地：其 `loader` 透過閘道取得最新狀態，頁面隨之渲染。另外，每次非 GET 的送出之後，React Router 會自行重新執行目前頁面的 loader。

***

## 逐層說明

### 瀏覽器與路由檔案

兩個畫面應用程式都建立在框架模式的 **React Router v7** 上，並在伺服器端渲染 React，因此單一路由檔案可同時包含資料載入與 UI。ERP 或 MES 畫面不使用 Remix 與 Next.js；建置由 [Vite](#vite) 負責，樣式使用 Tailwind v4。只有 `apps/backend` 不同，它是一個 Next.js 應用程式主機。

#### 路由即檔案

`apps/erp/app/routes/` 有超過 1,600 個檔案，直接對應到 URL 空間：多虧 `remix-flat-routes`（設定於 `apps/erp/app/routes.ts`），資料夾結構\_就是\_ URL 結構。

| 模式 | 範例 | 對 URL 的影響 |
| - | - | - |
| 以 `+/` 結尾的資料夾，如 `x+/` | `x+/purchase-order+/new.tsx` | 同時是資料夾與 URL 區段：`/x/purchase-order/new` |
| `_public+/` | `_public+/` | 無。資料夾僅用於歸類檔案；**不會**新增區段 |
| `_layout.tsx` | `_layout.tsx` | 無。它包住其資料夾內的每個路由 |
| `$` 前綴 | `$orderId.tsx` | 參數：`/:orderId` |
| 名稱內的 `.` | `dimensions.new.tsx` | 變成 `/`：`dimensions/new` |
| `_index.tsx` | `_index.tsx` | 資料夾本身 URL 的頁面 |
| `.server.ts` 後綴 | `*.server.ts` | 無。路由器會忽略它，且該輔助程式永遠不會送到瀏覽器 |

最上層資料夾依對象區分應用程式：

| 資料夾 | 內容 |
| - | - |
| `x+` | 需驗證的應用程式路由 |
| `api+` | JSON 端點 |
| `_public+` | 驗證路由 |
| `share+` | 以權杖驗證的公開連結 |
| `select-company+`、`onboarding+` | 公司選擇與首次設定 |

#### 讀取資料

頁面的資料來自伺服器上的 `loader`，它呼叫服務函式並回傳 JSON；元件以 `useLoaderData<typeof loader>()` 取得。每個頁面都需要的資料（使用者、公司、權限與功能旗標）由 `apps/erp/app/routes/x+/_layout.tsx` 載入一次。元件透過 `apps/erp/app/hooks/` 中的 hooks 取得，例如 `useUser()`、`usePermissions()` 與 `useCompanySettings()`。

#### 從 UI 寫入資料

表單 post 到其路由的 `action`，與[追蹤的那次寫入](#tracing-one-write-end-to-end)完全相同。當使用者應該留在頁面上時，例如重新排序資料列、行內編輯或刪除，請改用 `useFetcher()` 送出；頁面事後仍會重新驗證。

#### 共用 UI

撰寫新元件之前，請先看這些地方：

| 位置 | 內容 |
| - | - |
| `packages/web/react/` | `@alforse/web-react`，即設計系統：按鈕、對話框、組合方塊、選單、版面基本元件。 |
| `apps/erp/app/components/` | 應用程式專屬的元件，其中最主要的是 `components/Table/Table.tsx`，每個清單畫面都使用它。 |

#### 瀏覽器中的狀態

瀏覽器保存的狀態很少。各處通用的參照清單（品項、客戶、供應商、人員）存放在 `apps/erp/app/stores/` 下的 store 中，並由 `RealtimeDataProvider` 重新整理。一次性的 UI 狀態放在 [zustand](#zustand)。使用者及其權限的來源一律是路由 loader。

#### 翻譯

文字以 [Lingui](#lingui) 翻譯：將字串包在 `` t`...` `` 或 `<Trans>` 中，再執行 `pnpm lingui:compile` 建置語系目錄。

#### MES

MES 共用同一套技術堆疊，但結構較扁平。它沒有 `modules/` 資料夾；其服務位於 `apps/mes/app/services/`，約 100 個路由位於同一層，UI 的尺寸則是為戴手套的手在平板上操作而設計。

### 驗證與授權

沒有任何伺服器端入口會略過 `requirePermissions`，它同時處理驗證與授權：

```ts theme={null}
const { client, companyId, userId } = await requirePermissions(request, {
  create: "purchasing",
});
```

其定義位於 `packages/web/auth/src/services/auth.server.ts:152`。一次呼叫會經過四個階段：

1. **辨識呼叫者。** 瀏覽器請求帶有原生 session cookie；公開 API 請求帶有 `alforse-key` 標頭。對於金鑰，原生金鑰擁有者會依路由所需的權限驗證金鑰，並套用速率限制。詳情見 [API 金鑰](https://docs.alforse.com/zh-Hant/docs/building/api-keys.md)。
2. **請求決策。** 路由所需的權限，取自 `view`、`create`、`update`、`delete`，以及選用的 `role`，會被送到後端的原生授權擁有者。它依使用者在所選公司中的授權做出決定。授權由三個來源累加：員工類型、使用者所屬群組，以及直接指派。
3. **拒絕不被允許的請求。** 沒有有效的 session（沒有或已過期）時，session 會被銷毀，使用者回到登入頁。缺少權限則會產生帶有 **Access Denied** 訊息的重新導向。如果決策結果所屬的使用者或公司與 session 所持有的不同，請求會因身分不一致而被拒絕。
4. **交還上下文。** 成功時，路由會取得一個限定於本次請求的 client，以及 `companyId`、`companyGroupId` 與 `userId`。

#### 多租戶與列級安全性

許多公司共用同一個資料庫，這使 Alforse 成為**[多租戶](#multi-tenancy)**。目前啟用哪家公司由 session 決定；URL 不扮演任何角色。

租戶資料表啟用並強制套用**[列級安全性（RLS）](#rls)**；基準遷移 `packages/database/migrations/0001_baseline.sql` 以 `ALTER TABLE ... FORCE ROW LEVEL SECURITY` 完成此事。在每筆交易開始時，目前的公司與操作者會被存為交易區域設定，每個政策都以這些值檢查資料列。要求另一家公司的記錄，結果為空。

#### 平台與工作區是分開的

工作區生命週期、方案與權益由平台控制平面管理，這是與各客戶工作區分離的安全網域。平台端的權限不會隱含讀取客戶業務記錄的權利。

#### 其他類型的使用者

使用入口網站的客戶與供應商只會獲得較窄的權限，任何路由都可以要求 `{ role: "employee" }`。在 `share+/` 之下，路由完全不需要登入：存取取決於 URL 中攜帶的難以猜測的權杖。

### 寫入資料

ERP 與 MES 中的畫面保留了它們最初撰寫時所用的資料存取風格。後端不發放資料庫連線，而是把每種風格轉換成原生擁有者。共有四條路徑。

#### 1. 閘道 client

| 呼叫 | 端點 | 解析為 |
| - | - | - |
| `from(table).select` | `/api/web/rest/v1/[relation]` | 原生讀取關聯 |
| `from(table).insert/update/delete` | `/api/web/rest/v1/[relation]` | 擁有該資料的產品或模組。它驗證命令，然後透過 `packages/database/src/` 中的 store 寫入。 |
| `rpc(name, args)` | `/api/web/rest/v1/rpc/[name]` | 對應到該 RPC 的原生擁有者 |

#### 2. 具名原生操作

許多畫面透過 `packages/web/database/src/client.ts` 中的 `getPostgresClient`，以 [Kysely](#kysely) 描述查詢。這個建構器從不產生 SQL。它記錄查詢的形狀，並把它對應到 `packages/web/database/native-operations/` 登錄表中恰好一個具名操作，該處有 `purchasing.ts`、`inventory.ts`、`production-*` 與 `quality.ts` 等檔案。對應不到任何操作的查詢會被拒絕，且拒絕訊息會指出該查詢。像拖曳重新排序（`apps/erp/app/modules/shared/sort-order.ts`）這類多列寫入，就靠這點達成要嘛全成、要嘛全敗。

#### 3. 原生擁有者與工作單元

重要的業務寫入實作為 `products/*` 與 `packages/database/src/` 中的類別；例如出貨或發票過帳、發料，以及完成生產數量。每一個都在一個**工作單元**內執行，也就是整個業務交易共用一條 Postgres 連線，並在其上設定好操作者與公司。本體中的任何失敗都會讓整體回滾，而預計在寫入成功後觸發的 hook，只有在提交完成之後才會觸發。

#### 4. RPC

最窄的路徑是以 `client.rpc("name", args)` 叫用的具名函式；[追蹤的那次寫入](#tracing-one-write-end-to-end)中的 `get_next_sequence` 就是一例。每個 RPC 都有一個對應的原生擁有者，或出現在一份明確的未對應 RPC 清單上。

規劃是獨立的領域：[MRP](#mrp) 與生產排程實作在 `packages/web/planning` 中，並搭配規劃擁有者。

### 資料庫

資料庫是 PostgreSQL，前面沒有任何東西：沒有 PostgREST、沒有 Supabase 專案、沒有 edge functions。後端是資料庫唯一的 client，並以專用角色連線，即一個受 RLS 約束的執行期角色，以及排程器與投遞工作者各自獨立的角色。

#### 遷移

每個**[遷移](#migration)**是 `packages/database/migrations/` 中一個編號的 SQL 檔案。

| 規則 | 說明 |
| - | - |
| 基準 | `0001_baseline.sql` 永不變更。它包含新資料庫的完整 schema 與參照資料列。 |
| 後續變更 | 每項變更一個新檔案，從 `0002` 起編號，命名為 `<ordinal>_<description>.sql`。 |
| 順序 | 依數字。每個檔案都在由執行器擁有的交易內執行。 |
| 不可變性 | 一旦套用，檔案就不會被編輯、重新命名、重新排序或刪除；執行器會儲存每個檔案的校驗碼來強制執行此規則。 |

規則記載於 `packages/database/migrations/README.md`；執行器是 `packages/database/src/migrations.ts`。

#### Schema 與資料表

基準帶來大約 740 個資料表，分布於兩個 schema。`platform` 服務控制平面與原生擁有者；`web` 是畫面讀取所用的契約 schema。[檢視表](#view)提供讀取面。

#### 識別碼

ID 由應用程式產生，而非由 Postgres 產生，格式為 `{prefix}_{base58(uuidv7)}`，並存放在 `text` 主鍵中。每種記錄類型擁有其他類型不會使用的前綴。結果可依建立時間排序，同時在日誌行或 URL 中仍然易讀。

#### 讀取與寫入

規則是**透過擁有者寫入，從關聯讀取**。寫入只儲存來源資料。畫面在其之上顯示的一切，例如合計、名稱與縮圖，都由讀取關聯推導而來。

#### 檔案

上傳的檔案寫入物件儲存，畫面透過 `/api/web/storage` 路由存取。支援三種提供者類型：Cloudflare R2、任何與 S3 相容的儲存，以及客戶儲存閘道（見 `apps/backend/src/production/blob-storage-provider.ts` 及其鄰近檔案）。每家公司都有自己的金鑰命名空間與配額，並可加上備份複製設定檔。

***

## 寫入之後：事件與背景作業

負責寫入的程式碼不會同時執行後續工作。相反地，擁有者會在進行業務變更的那筆交易內，把網域事件加入**交易式發件匣**（`platform."outbox"`）。如果該交易回滾，事件也隨之消失；如果提交，事件就保證存在。

```mermaid theme={null}
flowchart TD
  W[Business write] -->|same transaction| O[(Outbox row)]
  O -->|after commit| D[Delivery worker]
  O -->|after commit| R[Native runner]
  D --> N[Notification inbox, email, webhooks]
  R --> T[Queued tasks and scheduled tasks]
```

### 投遞保證

投遞為至少一次（at-least-once），這就是處理常式被寫成冪等的原因。暫時性失敗會依帶有嘗試次數上限與有上限的指數退避的政策重試；永久性失敗則停止。`kernel/events/` 包含事件匯流排、該重試政策與 webhook 投遞。

### 原生執行器

背景任務由執行器執行，它是後端程序本身的一部分（`apps/backend/src/runner/`）。每個任務都經過四個階段：

1. **接收。** 在 `platform."backgroundTaskRun"` 新增一列，指出公司、任務與冪等鍵。
2. **領取。** 執行器取得該列的租約。
3. **執行。** 為該任務註冊的處理常式，在綁定該公司的交易中執行。
4. **記錄。** 該次執行最終狀態為 `pending`、`running`、`retrying`、`succeeded` 或 `deadLettered`。

失敗的嘗試會重試，直到上限。由於冪等鍵在重試與租約接手過程中保持不變，處理常式可以判斷自己是否已完成工作。畫面仍以熟悉的名稱稱呼這些工作（job），`alforse/company-export` 這類任務名稱與 `packages/web/jobs` 套件保留了這些名稱，但其背後沒有外部的工作服務。

### 排程工作

`apps/backend/src/runner/native-runner-schedules.ts` 宣告排程。其 cron 表達式以 UTC 解讀，每次觸發都會分散到所有啟用中的公司。

| 頻率 | 工作 |
| - | - |
| 每 15 分鐘 | MRP；通知摘要 |
| 每小時 | 租賃計費；合約工時計費 |
| 每日 00:00 | 匯率更新 |
| 每日 01:00 | 夜間重新規劃 |
| 每日 02:00 | 稽核日誌封存 |
| 每日 04:00 | 工作流程執行紀錄保留 |

投遞有其自己的排程：通知中繼每分鐘執行，維護到期派工每兩分鐘執行，另有清理 blob 與備份新增檔案的其他工作。

### 通知

通知會進入每位使用者的收件匣。後端發布變更推送，例如私有主題 `user:<userId>:notification`；推送會告訴 ERP 哪些資料列變更了，但不包含其內容，ERP 再自行取得所需資料。電子郵件則透過所設定的任一交易型郵件傳輸送出。

***

## 其他引擎

### 工作流程

工作流程是客戶在拖放畫布上自行設計的自動化。不應與執行器內建的任務混淆：

| 面向 | 工作流程 | 內建任務 |
| - | - | - |
| 作者 | 客戶，使用畫布 | Alforse 工程師，以 TypeScript 撰寫 |
| 保存為 | 含版本的工作流程定義 | 原始碼 |
| 變更方式 | 客戶每次編輯時 | 僅隨部署 |

定義由 `packages/web/workflows` 與 `kernel/workflows` 中的程式碼描述並驗證。每次執行及其內的每個步驟，都由引擎記錄以供日後檢視。有兩個執行器排程支援它：每十分鐘一次的工作流程後備機制，以及執行歷史的保留。功能本身在[工作流程](https://docs.alforse.com/zh-Hant/docs/reference/workflows.md)中說明。

### CAD 與組裝規劃

這裡有兩項工作。STEP 格式的 CAD 檔案會轉成瀏覽器可顯示的 3D 模型，並計算出避免碰撞的拆解順序，用來驅動組裝說明。程式碼位於後端的 `apps/backend/src/cad/`：`occt-import-js`（編譯為 WebAssembly 的 OpenCASCADE）負責解析幾何，運動規劃器則在旁邊。

兩個應用程式都在 `/api/web/assembler` 呼叫它，並以 bearer 金鑰驗證，再以 `packages/web/viewer` 顯示輸出。耗時較長的轉換會作為執行器任務執行；呼叫的頁面可以等待完成或輪詢。

***

## 詞彙表

本頁所連結術語的簡短定義，依 A 到 Z 排列。

### Action

路由檔案中用來處理送出表單的匯出。它在伺服器上執行，絕不會送到瀏覽器。

### Kysely

TypeScript 的 SQL 查詢建構器。在此處它只描述一個操作；後端把該描述轉換成具名原生操作。

### Lingui

用於翻譯的函式庫。程式碼中標記的字串會被抽取成每種語言一個檔案，供譯者填寫。

### Loader

路由檔案中用來收集其頁面所顯示資料的匯出。與 action 一樣僅在伺服器端執行。

### Migration（遷移）

一個編號的 SQL 檔案，內含一項 schema 變更。基準加上依序的後續檔案合在一起，*就是* schema。

### Monorepo

單一 Git 儲存庫，容納多個應用程式與共用函式庫，並一起版本控管與發行。

### MRP

物料需求規劃（Material Requirements Planning）的縮寫：根據需求與現有庫存，決定要採購或生產什麼，以及何時完成。

### Multi-tenancy（多租戶）

以一個資料庫與一個執行中的應用程式服務許多獨立的客戶公司，同時每家公司只看得到自己的資料。

### Postgres

PostgreSQL，Alforse 所建構於其上的關聯式資料庫。

### RLS

列級安全性（Row Level Security）。Postgres 會自動把每個資料表的規則附加到每個查詢上，正是這項規則讓各公司看不到彼此的資料。

### RPC

在本頁中：從應用程式碼透過 `client.rpc("name", args)` 叫用具名函式，由後端轉給原生擁有者。

### Turborepo

透過追蹤各套件之間的相依關係來協調建置，使 `pnpm build` 只重做有變動的部分。

### View（檢視表）

一個儲存的查詢，可以像資料表一樣讀取，但不能寫入。

### Vite

建置 ERP 與 MES，並在開發期間提供服務。

### zod

讓你只需定義一次資料的形狀，就能同時推導出驗證與 TypeScript 型別。

### zustand

極簡的 React 狀態函式庫，在此僅用於可拋棄的 UI 狀態，例如對話框是否顯示。

***

## 相關內容

**Webhook**

讓事件在發生時送到你自己的端點。

[Open](https://docs.alforse.com/zh-Hant/docs/building/webhooks.md)

**API 金鑰**

為程式提供其專屬的公開 API 憑證。

[Open](https://docs.alforse.com/zh-Hant/docs/building/api-keys.md)

**編號序列**

`get_next_sequence` 如何格式化文件編號。

[Open](https://docs.alforse.com/zh-Hant/docs/reference/sequences.md)

**備份**

建立並還原公司資料與檔案的快照。

[Open](https://docs.alforse.com/zh-Hant/docs/platform/backups.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.