> **官方 WordPress 插件：发送 Kakao 通知消息、品牌消息与短信，并支持 WooCommerce 订单自动通知**

`sendgo/wordpress` 通过 Composer 打包 [`sendgo/php`](https://github.com/send-go/php) 核心，并接入 WordPress：提供密钥设置页，以及订单状态变化时通知买家的 WooCommerce 挂钩。

要求 PHP 8.2 及以上。

---

## 安装

### 从 wordpress.org 或 zip 安装（推荐）

发布用的 zip 已把核心 SDK 打包在 `vendor/` 中，无需 Composer 即可运行。
在 **插件 → 安装插件** 上传该 zip，或解压到 `wp-content/plugins/sendgo`。

### 使用 Composer

```bash
composer require sendgo/wordpress
```

或在插件目录内执行：

```bash
cd wp-content/plugins/sendgo
composer install
```

`composer install` 会生成包含核心 SDK 的 `vendor/autoload.php`。直接从源码检出的副本没有 `vendor/`
目录，**缺少它插件就无法发送** —— `client()` 返回 `null`，后台会出现提示说明原因。

### 启用插件

在 WordPress 后台 **插件** 页面启用 **Sendgo**。

---

## 配置

打开 **设置 → Sendgo**：

| 字段 | 说明 |
|------|------|
| Access Key | Sendgo 访问密钥 |
| Secret Key | Sendgo 私密密钥 |
| Kakao Sender Key | Kakao 发送者资料密钥（通知消息／品牌消息） |
| SMS Sender Key | 短信主叫号码密钥（SMS / LMS / MMS） |
| API Version | `v1` 或 `v2` |

访问密钥与私密密钥**都**填写后发送功能才会启用。密钥只保存在服务端的 `sendgo_options` 选项中，不会暴露给前端。

API 版本默认为 **`v1`**。品牌消息与短链接需要 `v2`，若要使用这些渠道请先切换。

> API 基础地址读取自 `url` 选项，但设置页没有对应字段。若需指向其他主机，请以代码方式设置：
> ```php
> $options = get_option('sendgo_options', []);
> $options['url'] = 'https://staging.sendgo.io';
> update_option('sendgo_options', $options);
> ```
> 在 1.2.4 之前，只要保存一次设置页，该值就会丢失。现在保存后会保留。

---

## WooCommerce 订单通知

启用 WooCommerce 后，插件会在以下状态变化时通知账单电话：

- **已完成**（`woocommerce_order_status_completed`）
- **处理中**（`woocommerce_order_status_processing`）

在 **设置 → Sendgo → WooCommerce Order Notifications** 中**按状态分别**配置：

| 字段 | 说明 |
|------|------|
| `[Order completed] Alimtalk template code` | 完成状态发送的模板代码。订单号作为第一个变量（`#{var1}`）传入。 |
| `[Order completed] SMS text used if Alimtalk fails` | 通知消息失败或未填模板代码时发送的短信正文。支持 `{order_number}` 占位符。 |
| `[Processing] Alimtalk template code` | 进入处理中状态时发送的模板代码。 |
| `[Processing] SMS text used if Alimtalk fails` | 处理中状态的短信回退正文。 |

**留空的状态不会发送任何消息。** 两个状态都填写意味着每个订单会收到两条消息（处理中 + 已完成），因此请只配置真正需要通知的状态。

每个订单**每个状态只发送一次**。发送成功后插件会写入订单元数据（`_sendgo_notified_completed` / `_sendgo_notified_processing`），因此在后台重新保存订单、或其他插件再次设置该状态都不会重复发送。发送失败时不写入，下一次状态变化会重试。

发送失败绝不会中断结算或订单流程 —— 异常会被捕获并写入 WooCommerce 日志（source：`sendgo`）。

> **从 1.0.x 升级** —— 旧版本两个状态共用同一个 `order_template_code`，因此订单从处理中变为已完成时会**发送两次相同的消息**，导致客户被重复计费。现在已完成状态沿用原来的选项键，处理中状态需要填写新字段才会发送。原有配置照常工作，只是重复发送消失了。

### 电话号码

发送前会把账单电话中的非数字字符全部去掉（`preg_replace('/[^0-9]/', ...)`），因此 `010-1234-5678` 与 `+82 10 1234 5678` 都需要本身就是 API 能接受的韩国号码 —— 插件只去标点，不做国家码转换。账单电话为空的订单会被静默跳过。

---

## 在自己的代码中发送

插件加载后可直接使用核心客户端：

```php
$client = Sendgo_Plugin::instance()->client();

if ($client) {
    // 通知消息
    $client->alimtalk->send([
        'templateCode' => 'ORDER_CONFIRM_001',
        'contacts'     => [['contact' => '01012345678', 'var1' => 'ORD-001']],
    ]);

    // 短信
    $client->sms->sendSms([
        'content'  => '验证码：123456',
        'contacts' => [['contact' => '01012345678']],
    ]);
}
```

**务必判断 `$client` 是否为空** —— 密钥未设置或缺少 `vendor/autoload.php` 时它返回 `null`，对 `null` 调用方法会让所在页面白屏。

各发送渠道都是客户端上的属性：

| 属性 | 渠道 |
|------|------|
| `$client->alimtalk` | Kakao 通知消息 |
| `$client->friendtalk` | Kakao 好友消息 |
| `$client->brandMessage` | Kakao 品牌消息（仅 v2） |
| `$client->sms` | SMS / LMS / MMS |

客户端在单次请求内被记忆化，因此反复调用 `Sendgo_Plugin::instance()->client()` 开销很小。

### 挂到自己的事件上

```php
add_action('user_register', function (int $user_id): void {
    $client = Sendgo_Plugin::instance()->client();
    if (!$client) {
        return;
    }

    $user  = get_userdata($user_id);
    $phone = preg_replace('/[^0-9]/', '', (string) get_user_meta($user_id, 'billing_phone', true));

    if ('' === $phone) {
        return;
    }

    try {
        $client->alimtalk->send([
            'templateCode' => 'WELCOME_001',
            'contacts'     => [['contact' => $phone, 'var1' => $user->display_name]],
        ]);
    } catch (\Throwable $e) {
        // 绝不能让消息发送失败影响注册流程。
        error_log('Sendgo welcome message failed: ' . $e->getMessage());
    }
}, 10, 1);
```

用 `try`/`catch` 包起来是关键：WordPress 挂钩内未处理的 `SendgoException` 会在触发它的页面上变成致命错误。

---

## 品牌消息

> **好友消息已于 2025-12-31 停止服务。** 自 2026-01-01 起，Kakao 会将好友消息请求
> 自动改以品牌消息（自由形式）发出。`friendtalk` 仍可调用，且自由文本类型
> `FT`/`FI`/`FW` 发往单个接收者的路径目前仍只有这一条 —— 基于模板的富文本类型
> （`FL`/`FC`/`FM`/`FP`/`FA`）、非好友定向（`N`/`I`）与群发（`F`）请使用品牌消息。
品牌消息是好友消息的后继渠道：可触达**非频道好友**的接收者（`'targeting' => 'N'`），也可向**已同意接收的全部频道好友群发**（`'targeting' => 'F'`）。消息类型与好友消息一一对应（`FT`→`BT`、`FI`→`BI`、`FW`→`BW`、`FL`→`BL`、`FC`→`BC`、`FM`→`BM`、`FP`→`BP`、`FA`→`BA`）。

> **仅 v2 支持。** 请把 **设置 → Sendgo → API Version** 改为 `v2`；插件默认为 `v1`。

```php
$client = Sendgo_Plugin::instance()->client();

// 单条发送 —— targeting 为 M/N/I 时必须提供 contacts
$client->brandMessage->send([
    'targeting'          => 'M',
    'messageType'        => 'FL',
    'friendTemplateUuid' => '9cd5460b-6458-4edc-9b11-c26d3013c340',
    'contacts'           => [['contact' => '01012345678', 'var1' => '29,000元']],
]);

// 群发 —— 已同意接收的全部频道好友（不传接收者列表）
$result = $client->brandMessage->broadcast([
    'messageType'        => 'FW',
    'friendTemplateUuid' => '9cd5460b-6458-4edc-9b11-c26d3013c340',
]);

// 群发在上游为异步处理，需轮询查看进度
$client->brandMessage->campaign($result['data']['campaignId']);
$client->brandMessage->campaigns(['count' => 10]);
```

群发可能触达你的整个好友列表，请务必从明确的管理操作触发 —— WP-CLI 命令，或带权限校验的 `admin-post` 处理器 —— 绝不要放在公开挂钩上。

---

## 异常处理

```php
use Sendgo\Php\Exception\SendgoException;

try {
    $client->alimtalk->send([...]);
} catch (SendgoException $e) {
    // 发送失败应记录日志，不要暴露给购物者。
    if (function_exists('wc_get_logger')) {
        wc_get_logger()->error(
            sprintf('Sendgo %d [%s]: %s', $e->getStatusCode(), $e->getErrorCode(), $e->getMessage()),
            ['source' => 'sendgo']
        );
    }
}
```

请根据 `getErrorCode()` 分支，而不要匹配错误消息文本 —— 消息文案可能变更，错误代码才是契约。
`TOKEN_EXPIRED` 与 `TOKEN_MISMATCH` 已在核心 SDK 内部处理：会重新签发令牌并自动重试该请求一次。

---

## 常见问题

**没有 WooCommerce 也能用吗？**
可以。`Sendgo_Plugin::instance()->client()` 在任何 WordPress 站点都能工作；只有订单自动通知依赖 WooCommerce。

**`client()` 返回 `null`。**
要么访问密钥／私密密钥未设置，要么插件来自源码且没有执行 `composer install`，`vendor/autoload.php`
不存在（后一种情况后台会有提示）。发布用的 zip 已含 `vendor/`，不会出现这种情况。

**什么都没发出，也没有报错。**
请确认已在控制台 **集成管理 → 集成信息** 中登记 WordPress 站点的出口 IP —— 未登记地址发来的请求会被拒绝。
在共享主机或 CDN 之后，该地址与浏览器显示的并不相同。

**凭据保存在哪里？**
服务端的 `sendgo_options` 选项中。它们从不输出到前端，私密密钥在后台以密码框形式渲染。

**卸载会清理干净吗？**
`uninstall.php` 会删除 `sendgo_options` 选项。`_sendgo_notified_*` 订单元数据会保留 —— 它无害，而删除它意味着对每一个订单做一次批量写入。

---

## 短链接

短链接会缩短消息正文中的链接，并统计该链接是否被真正点击。
短信按字节计费，因此链接更短就意味着正文可以写更多内容。

> 仅 v2 支持。

再次缩短同一个原始 URL 会**直接返回已有的链接**。若希望按活动分别统计反应，
请使用 `forceNew` 生成新的短码。

`deactivate` 不会删除链接，只停止重定向。当已发送消息中的链接必须失效时使用它；
累计统计会保留，访问已停止的链接会返回 `410 Gone`。

```php
$client = Sendgo_Plugin::instance()->client();

if ($client) {
    $short = $client->shortUrl->create([
        'targetUrl' => get_permalink($post_id),
        'title'     => get_the_title($post_id),
    ]);

    $link = $short['data']['shortUrl'];
}
```

`stats` 返回按日趋势（`daily`）以及按设备（`byDevice`）、来源（`byReferer`）、国家（`byCountry`）的分解。按日趋势读取的是预聚合表，因此点击量再大响应时间也保持稳定。

---

## 连接的外部服务

本插件通过 Sendgo API（https://sendgo.io）发送 Kakao 通知消息、品牌消息与 SMS/LMS/MMS。
发送需要 Sendgo 账号，且为付费服务。

- 每次发送前，会用访问密钥与私密密钥请求一个短期令牌。
- WooCommerce 订单变为 `处理中`／`已完成`，且该状态已配置模板代码或短信回退正文时，
  会连同发送者密钥与模板代码一起发送买家的账单电话与订单号。
- 在自己的代码中直接调用客户端时，传入的收件号码与正文会原样发送。

仅安装或启用插件、或模板代码与短信回退正文都为空时，不会发送任何数据。

- 服务条款：https://sendgo.io/terms-of-service
- 隐私政策：https://sendgo.io/privacy-policy

---

## 包信息

- **包名**：`sendgo/wordpress`（Packagist）
- **仓库**：[send-go/wordpress](https://github.com/send-go/wordpress)
- **注册表**：https://packagist.org/packages/sendgo/wordpress
- **许可**：MIT

### 如何获取 API 密钥

登录 Sendgo 后，在 **集成管理 → 集成信息** 菜单中签发访问密钥与私密密钥，并在同一页面登记允许调用的
IP 地址 —— 未登记地址发来的请求会被拒绝。
使用通知消息／品牌消息需在 **发信配置文件管理** 登记发送者资料以获取 Kakao 发送者密钥；使用短信需在
**发信号码管理** 菜单登记主叫号码。