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

# 预订确认和放弃预订恢复

> 使用自定义事件、Journeys 和数据源根据实时预订状态发送预订确认或恢复邮件。

## 概述

在本教程中，您将设置一个常见的预订工作流程：

* 在用户完成预订后发送预订确认邮件。
* 如果用户开始预订但未及时完成，则发送恢复邮件。

完成后，您将拥有：

* 两个自定义事件（`booking_started`、`booking_complete`）
* 一个根据完成与放弃情况分支的 Journey
* 用于确认详情的预订数据源
* 用于恢复激励的可选优惠券数据源

本指南重点介绍 OneSignal 的配置。您的预订系统和后端可以用任何语言或框架实现。

### 设置流程

1. 您的应用跟踪 `booking_started` [自定义事件](./custom-events)。
2. 这会将用户加入 Journey。
3. Journey 等待 `booking_complete` 事件，如果未及时收到，则发送后续提醒。
4. 如果预订完成，OneSignal 在发送时调用预订数据源，并发送包含最新预订详情的确认邮件。
5. 如果预订未在等待窗口内完成，Journey 会走过期路径并发送恢复邮件。

***

## 设置

### 先决条件

开始之前，请确保您具备：

* 启用了 [**邮件**](./email-setup) 渠道的 OneSignal 应用
* 可以以 JSON 格式返回预订和/或优惠券数据的后端端点
* 在您的应用、后端和 OneSignal [外部 ID](./users#external-id) 之间共享的稳定用户标识符
* 访问 [自定义事件](./custom-events)

### 1. 跟踪预订事件

跟踪以下自定义事件。这些可以来自您的应用（使用我们的 SDK）或后端（使用我们的 REST API）。

**事件名称：**

* `booking_started` — 当用户开始预订流程时
* `booking_complete` — 当预订成功完成时

<Tabs>
  <Tab title="应用 - 移动端和网页端">
    在我们的 [移动 SDK](./mobile-sdk-reference) 和/或 [网页 SDK](./web-sdk-reference) 上使用 `trackEvent()` 方法直接从您的应用/网站发送自定义事件。

    ```js 示例 theme={null}
    OneSignal.User.trackEvent("booking_started");
    OneSignal.User.trackEvent("booking_complete");
    ```
  </Tab>

  <Tab title="服务器端">
    如果您从后端跟踪事件，请使用 [创建自定义事件 API](/reference/create-custom-events) 将事件发送到 OneSignal。

    ```bash theme={null}
    curl --request POST \
      --url https://api.onesignal.com/apps/{app_id}/custom_events \
      --header 'Authorization: Key YOUR_APP_API_KEY' \
      --header 'Content-Type: application/json' \
      --data '{
        "events": [
          {
            "name": "booking_started",
            "external_id": "user123",
            "properties": {}
          }
        ]
      }'
    ```
  </Tab>
</Tabs>

<Note>
  跟踪事件和从后端返回数据时使用相同的用户身份。不匹配的 ID 是缺少个性化的最常见原因。
</Note>

### 2. 创建数据源别名

在 OneSignal 中，转到 **设置 > 数据源** 并创建以下别名。

**预订数据源：**
使用此源在发送时提取最新预订详情。

* **别名：** `booking_data`
* **方法：** GET
* **URL：**

```liquid 示例端点 theme={null}
https://your-domain.com/datafeed/booking?user_id={{subscription.external_id}}
```

示例响应：

```json JSON theme={null}
{
  "first_name": "Sam",
  "last_booking": {
    "service_type": "Consultation",
    "booking_date": "January 22, 2026",
    "booking_time": "2:00 PM",
    "price": 45
  }
}
```

**优惠券数据源（可选）：**

如果您想在恢复邮件中包含优惠券代码，请使用此可选源。

* **别名：** `coupon`
* **方法：** GET
* **URL：**

```liquid 示例端点 theme={null}
https://your-domain.com/datafeed/coupon?user_id={{subscription.external_id}}
```

示例响应：

```json JSON theme={null}
{
  "first_name": "Sam",
  "code": "PROMO8F3K2",
  "discount_text": "10%",
  "expires_in_hours": 2,
  "deep_link": "https://your-domain.com/checkout?coupon=PROMO8F3K2"
}
```

<Warning>
  保护您的数据源端点。在生产环境中，在请求标头中发送 API 密钥（例如 `x-api-key`），并在 **设置 > 数据源** 中配置该标头，而不是在 URL 中嵌入密钥。
</Warning>

### 3. 创建邮件模板

#### 预订确认邮件：

**主题：**

```text theme={null}
您的预订详情
```

**正文：**

```liquid theme={null}
您好 {{ data_feed.booking_data.first_name | default: "客户" }}，

感谢您的预订！以下是您的预约详情：

服务：{{ data_feed.booking_data.last_booking.service_type }}
日期：{{ data_feed.booking_data.last_booking.booking_date }}
时间：{{ data_feed.booking_data.last_booking.booking_time }}
价格：${{ data_feed.booking_data.last_booking.price }}

我们期待与您见面！
```

#### 预订恢复邮件

**主题：**

```text theme={null}
完成您的预订并享受优惠
```

**正文：**

<Tabs>
  <Tab title="使用优惠券数据源">
    ```liquid theme={null}
    您好 {{ data_feed.coupon.first_name | default: "客户" }}，

    在接下来的 {{ data_feed.coupon.expires_in_hours }} 小时内完成您的预订，
    使用此代码即可享受 {{ data_feed.coupon.discount_text }} 优惠：

    {{ data_feed.coupon.code }}

    请在这里使用：
    {{ data_feed.coupon.deep_link }}
    ```
  </Tab>

  <Tab title="不使用优惠券数据源">
    ```text theme={null}
    您好， 

    您尚未完成预订！ 

    立即完成预订，为下次预约节省费用。

    使用此链接完成您的预订：
    [在此插入深度链接]
    ```
  </Tab>
</Tabs>

<Note>
  在 Liquid 中始终包含 `default` 过滤器，以防止数据源字段缺失时出现空白内容。
</Note>

### 4. 构建 Journey

1. 在 OneSignal 中，转到 **消息 > Journeys > 创建 Journey**

2. 将 **入口触发器** 设置为：
   * **自定义事件：** `booking_started`

3. 添加 **等待直到** 步骤：
   * **条件：** 发生自定义事件
   * **事件名称：** `booking_complete`
   * **最大等待时间：** 10 分钟
   * 启用过期路径

4. 配置分支：
   * **已完成：** 发送预订确认邮件
     * **数据源：** `booking_data`
   * **已过期：** 发送恢复邮件
     * **数据源：** `coupon`

<Info>
  过期分支允许您处理放弃情况，无需在应用中添加额外逻辑。请参见：

  * [Journey 设置](./journeys-settings) - 了解自定义事件入口和出口规则的详情
  * [Journeys 操作](./journeys-actions) - 了解等待直到步骤和过期分支的详情
</Info>

### 5. 测试和验证

#### 验证事件

从您的应用或后端触发自定义事件并确认。

在 OneSignal 中，转到 **分析 > 自定义事件** 并确认您看到：

* 为您的外部 ID 显示 `booking_started` 事件
* 为您的外部 ID 显示 `booking_complete` 事件

#### 验证数据源

使用已知用户 ID 手动调用您的数据源端点并确认：

* 返回 200 响应
* 存在所有预期字段

#### 验证邮件

从 Journey 编辑器发送测试消息并确认：

* 预订邮件包含真实预订详情
* 恢复邮件包含有效优惠券
* 没有 Liquid 变量显示为空

<Check>
  如果个性化缺失，请确认数据源请求中的用户 ID 与触发 Journey 的用户匹配。
</Check>

## 示例：数据源实现

<Accordion title="示例 Node.js 数据源端点">
  此示例显示了预订确认和恢复数据源的最小 Express 实现。您的后端语言、框架和数据源可以不同，只要 JSON 响应结构与您的邮件模板匹配即可。

  ### 预订数据源示例

  ```js theme={null}
  import express from "express";

  const app = express();

  function dataFeedAuth(req, res, next) {
    if (req.headers["x-api-key"] !== process.env.DATAFEED_API_KEY) {
      return res.status(401).json({ error: "Unauthorized" });
    }
    next();
  }

  app.get("/datafeed/booking", dataFeedAuth, async (req, res) => {
    const { user_id } = req.query;

    if (!user_id) {
      return res.status(400).json({ error: "Missing user_id" });
    }

    const booking = await getLatestBookingForUser(user_id);

    if (!booking) {
      return res.status(404).json({ error: "No booking found" });
    }

    res.json({
      first_name: booking.first_name,
      last_booking: {
        service_type: booking.service_type,
        booking_date: booking.booking_date,
        booking_time: booking.booking_time,
        price: booking.price
      }
    });
  });
  ```

  ### 优惠券数据源示例

  ```js theme={null}
  app.get("/datafeed/coupon", dataFeedAuth, async (req, res) => {
    const { user_id } = req.query;

    if (!user_id) {
      return res.status(400).json({ error: "Missing user_id" });
    }

    const coupon = await generateCouponForUser(user_id);

    res.json({
      first_name: coupon.first_name,
      code: coupon.code,
      discount_text: coupon.discount_text,
      expires_in_hours: coupon.expires_in_hours,
      deep_link: coupon.deep_link
    });
  });
  ```

  ### 实现指南

  * 保持响应快速（数据源在发送时被调用）
  * 始终返回可预测的 JSON 结构
  * 当没有数据存在时使用 404
  * 通过请求标头发送的 API 密钥保护端点
</Accordion>

## 常见问题

### 邮件显示空值

* 数据源返回 404
* JSON 响应中的字段名称已更改
* 用户身份不匹配

### Journey 不分支

* 未跟踪 `booking_complete` 事件
* 事件名称不匹配（区分大小写）
* 事件发生在等待窗口之外

### 数据源返回 401 或 403

* API 密钥缺失或无效
* 标头未在数据源设置中配置

## 下一步

* 添加事件属性（服务类型、价格）以实现更高级的 Journey 条件
* 添加额外的恢复步骤，如推送或短信提醒
* 使用 Journey 出口规则防止重复恢复消息

***
