> ## 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

**路径和数量可能会变化**

下文中的每个文件路径、行号和数量都反映撰写时的代码仓库。请将它们视为指引，而非保证。

***

## 系统一览

所有内容都位于由 pnpm 工作区和 [Turborepo](#turborepo) 运行的单个 [monorepo](#monorepo) 中，分为两层。

### 界面层

两个 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} />
```

注意，这是指向表单的链接，而不是提交按钮。在界面中，`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）的错误，无论这些错误来自浏览器还是服务器。它通过属性接收校验器和默认值。

#### 3. 模式

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

规则位于 `purchaseOrderValidator` 中，这是一个 [zod](#zod) 对象，既产生运行时检查，也产生匹配的 TypeScript 类型。HTML 表单值由 `zod-form-data`（`zfd`）清理：例如，`zfd.text()` 会把空字段的 `""` 映射为 `undefined`。浏览器和服务器共用这一个模式，因此应用相同的规则。

#### 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` 都不会进入浏览器包，这就是二者可以使用仅限服务器的代码的原因。

#### 5. 服务函数

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

数据访问委托给服务函数，绝不在路由中进行。每个服务函数接收一个客户端作为第一个参数，并以 `supabase-js` 中熟悉的 `{ data, error }` 形式作答。该客户端由 `getAlforseServiceRole()`（`packages/web/auth/src/lib/supabase/client.server.ts:13`）构建；尽管名称沿用旧称，它是以本次请求中登录的用户身份与 `/api/web` 上的原生网关通信。客户端属于某一个请求，在任何其他请求中都无法使用。

```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,
    /* ... */
  });
```

服务接着存储订单的交货和付款行。这些客户端调用中的每一个都作为单独的请求到达网关。当一组写入必须作为一个整体成功或失败时，网关会将其路由到一个原生所有者，由它在单个数据库事务内执行整个操作（见[写入数据](#writing-data)）。

#### 6. 数据库中的编号

调用 `client.rpc("get_next_sequence", ...)` 是一个 [RPC](#rpc)，由编号模块 `modules/master-document-numbering/` 应答。在与提交相同的事务内，该模块领取下一个计数值，并为其应用公司的前缀、填充位数和日期标记。格式见[编号序列](https://docs.alforse.com/zh/docs/reference/sequences.md)。

#### 7. 回到界面

最后，action 执行 `throw redirect(...)`。随后 React Router 加载目标页面：其 `loader` 通过网关获取最新状态，页面渲染。另外，在每次非 GET 提交之后，React Router 会自行重新运行当前页面的 loader。

***

## 逐层剖析

### 浏览器与路由文件

两个界面应用都构建在框架模式下的 **React Router v7** 上，并在服务器上渲染 React，这让单个路由文件可以同时承载其数据加载和界面。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/` 中的钩子访问，例如 `useUser()`、`usePermissions()` 和 `useCompanySettings()`。

#### 从界面写入数据

表单提交到其路由的 `action`，与[被跟踪的写入](#tracing-one-write-end-to-end)中完全一致。当用户应停留在页面上时，例如重新排序行、行内编辑或删除，请改用 `useFetcher()` 提交；页面之后仍会重新验证。

#### 共享界面

编写新组件之前，先看看这些地方：

| 位置 | 你会找到什么 |
| - | - |
| `packages/web/react/` | `@alforse/web-react`，设计系统：按钮、模态框、组合框、菜单、布局基础组件。 |
| `apps/erp/app/components/` | 应用专用的组件，其中最主要的是 `components/Table/Table.tsx`，每个列表界面都使用它。 |

#### 浏览器中的状态

浏览器保存的状态非常少。到处使用的参考列表（物料、客户、供应商、人员）保存在 `apps/erp/app/stores/` 下的存储中，并由 `RealtimeDataProvider` 刷新。一次性的界面状态放在 [zustand](#zustand) 中。用户及其权限的来源始终是路由 loader。

#### 翻译

文本使用 [Lingui](#lingui) 翻译：把字符串包在 `` t`...` `` 或 `<Trans>` 中，然后运行 `pnpm lingui:compile` 构建目录。

#### MES

MES 共用同一技术栈，但结构更扁平。它没有 `modules/` 文件夹；其服务位于 `apps/mes/app/services/`，大约 100 个路由处于同一层级，界面按戴手套的手在平板电脑上操作的尺寸设计。

### 认证与授权

没有任何服务端入口可以绕过 `requirePermissions`，它同时处理认证和授权：

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

它的定义位于 `packages/web/auth/src/services/auth.server.ts:152`。一次调用要经过四个阶段：

1. **识别调用方。** 浏览器请求携带原生会话 cookie；公共 API 请求携带 `alforse-key` 标头。对于密钥，原生密钥所有者会根据路由所需的权限验证密钥并应用速率限制。详情见 [API 密钥](https://docs.alforse.com/zh/docs/building/api-keys.md)。
2. **请求决定。** 路由所需的权限取自 `view`、`create`、`update`、`delete`，以及可选的 `role`，会发送给后端的原生授权所有者。它依据用户在所选公司中的授权作出决定。授权来自三个来源的叠加：员工类型、用户所属的组，以及直接分配。
3. **拒绝不被允许的请求。** 没有有效会话（无会话或已过期）时，会话会被销毁，用户被带到登录页。缺少权限则产生携带 **Access Denied** 消息的重定向。如果返回的决定对应的用户或公司与会话中的不一致，请求会作为身份不一致被拒绝。
4. **交回上下文。** 成功时，路由会得到一个限定于本次请求的客户端，以及 `companyId`、`companyGroupId` 和 `userId`。

#### 多租户与行级安全

许多公司共用一个数据库，这使 Alforse 成为**[多租户](#multi-tenancy)**。当前活动的公司由会话决定；URL 不起作用。

租户表启用并强制了**[行级安全（RLS）](#rls)**；基线迁移 `packages/database/migrations/0001_baseline.sql` 通过 `ALTER TABLE ... FORCE ROW LEVEL SECURITY` 实现。在每个事务开始时，当前公司和操作者会被保存为事务局部设置，每条策略都据此检查行。请求另一个公司的记录，结果为空。

#### 平台与工作区相互分离

工作区生命周期、套餐和权益由平台控制平面管理，这是一个与各客户工作区分离的安全域。平台一侧的权限不会隐含读取客户业务记录的权利。

#### 其他类型的用户

使用门户的客户和供应商获得更窄的权限，任何路由都可以要求 `{ role: "employee" }`。在 `share+/` 下，路由完全不需要登录：访问取决于 URL 中携带的不可猜测的令牌。

### 写入数据

ERP 和 MES 中的界面保留了它们最初编写时的数据访问风格。后端并不是交出数据库连接，而是把每种风格转换为一个原生所有者。共有四条路径。

#### 1. 网关客户端

| 调用 | 端点 | 解析为 |
| - | - | - |
| `from(table).select` | `/api/web/rest/v1/[relation]` | 原生读取关系 |
| `from(table).insert/update/delete` | `/api/web/rest/v1/[relation]` | 拥有该数据的产品或模块。它校验命令，然后通过 `packages/database/src/` 中的存储写入。 |
| `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 连接，并在其上设置操作者和公司。正文中的任何失败都会让全部回滚，而旨在成功写入之后执行的钩子只会在提交发生后触发。

#### 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 项目，也没有边缘函数。后端是数据库唯一的客户端，并以专用角色连接，即受 RLS 约束的运行时角色，以及调度器和投递工作进程各自独立的角色。

#### 迁移

每个**[迁移](#migration)**是 `packages/database/migrations/` 中一个带编号的 SQL 文件。

| 规则 | 详情 |
| - | - |
| 基线 | `0001_baseline.sql` 永不改变。它包含新数据库的完整模式及其参考行。 |
| 后续更改 | 每次更改一个新文件，从 `0002` 起编号，命名为 `<ordinal>_<description>.sql`。 |
| 顺序 | 按数字。每个文件在运行器拥有的事务内运行。 |
| 不可变性 | 文件一旦应用，就绝不编辑、重命名、重新排序或删除；运行器会存储每个文件的校验和来强制执行这一点。 |

这些规则记录在 `packages/database/migrations/README.md` 中；运行器是 `packages/database/src/migrations.ts`。

#### 模式与表

基线带来大约 740 张表，分布在两个模式中。`platform` 服务于控制平面和原生所有者；`web` 是界面读取的契约模式。[视图](#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]
```

### 投递保证

投递为至少一次，这就是处理程序被编写为幂等的原因。临时故障会按带尝试次数上限和有上限的指数退避的策略重试；永久故障则停止。`kernel/events/` 包含事件总线、该重试策略和 Webhook 投递。

### 原生运行器

后台任务由属于后端进程本身的运行器执行（`apps/backend/src/runner/`）。每个任务都经过四个阶段：

1. **接收。** 向 `platform."backgroundTaskRun"` 添加一行，写明公司、任务和幂等键。
2. **领取。** 运行器获取该行的租约。
3. **运行。** 为该任务注册的处理程序在绑定到公司的事务中执行。
4. **记录。** 运行最终的状态为 `pending`、`running`、`retrying`、`succeeded` 或 `deadLettered` 之一。

失败的尝试会被重试，直至达到上限。由于幂等键在重试和租约接管期间保持不变，处理程序可以判断自己是否已完成工作。界面仍用熟悉的名称称呼作业，`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/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 文件，包含一次模式更改。基线加上后续文件按顺序合在一起，*就是* 模式。

### 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 状态库，这里仅限用于可随时丢弃的界面状态，例如模态框是否显示。

***

## 相关内容

**Webhooks**

让事件在发生时发送到你自己的端点。

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

**API 密钥**

为程序提供其访问公共 API 的专属凭证。

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

**编号序列**

`get_next_sequence` 如何格式化单据编号。

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

**备份**

创建并恢复公司数据和文件的快照。

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


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