> **用于发送 Kakao 通知消息、品牌消息与短信的官方 PHP SDK**

`sendgo/php` 是不依赖任何框架的**纯 PHP 核心包**。Laravel、Symfony、WordPress 扩展包都构建在它之上。

要求 PHP 8.2 及以上。

---

## 安装

```bash
composer require sendgo/php
```

---

## 快速上手

```php
<?php

use Sendgo\Php\Sendgo;

$sendgo = new Sendgo([
    'access_key'       => $_ENV['SENDGO_ACCESS_KEY'],
    'secret_key'       => $_ENV['SENDGO_SECRET_KEY'],
    'kakao_sender_key' => $_ENV['SENDGO_KAKAO_SENDER_KEY'],
    'sms_sender_key'   => $_ENV['SENDGO_SMS_SENDER_KEY'],
    'api_version'      => 'v2',
]);

$sendgo->alimtalk->send([
    'templateCode' => 'ORDER_CONFIRM_001',
    'contacts'     => [
        ['contact' => '01012345678', 'name' => '洪吉童', 'var1' => 'ORD-001'],
    ],
]);
```

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

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

同名的方法调用（`$sendgo->alimtalk()`）也可用，这是为了让 Laravel 门面（facade）的转发能够正常工作。

配置数组的键使用 **snake_case**（`access_key`），而请求体的键使用 **camelCase**（`templateCode`）—— 前者是本 SDK 的约定，后者是 REST API 的约定。

---

## 通知消息

```php
<?php

// 批量发送
$sendgo->alimtalk->send([
    'templateCode' => 'ORDER_CONFIRM_001',
    'contacts'     => [
        ['contact' => '01011111111', 'var1' => 'ORD-001', 'var2' => '29,000元'],
        ['contact' => '01022222222', 'var1' => 'ORD-002', 'var2' => '15,000元'],
    ],
]);

// 预约发送
$sendgo->alimtalk->send([
    'templateCode' => 'PROMO_SUMMER_2026',
    'scheduleType' => 'SCHEDULED',
    'at'           => '2026-07-28 09:00:00',
    'contacts'     => [['contact' => '01012345678', 'var1' => '夏季限定 5 折']],
]);

// 失败时自动回退为短信
$sendgo->alimtalk->send([
    'templateCode' => 'DELIVERY_START_001',
    'replaceSms'   => 'Y',
    'smsSubject'   => '[发货通知]',
    'smsContent'   => "您购买的商品已出库。\n运单号：1234567890",
    'contacts'     => [['contact' => '01012345678', 'var1' => 'ORD-001']],
]);
```

`replaceSms` 的回退仅在 **Kakao 侧投递失败**时触发。若模板代码本身不存在，请求会直接失败，不会回退，因此模板代码应在部署前核对。

---

## 好友消息

> ⚠️ **已停用 —— 按 Kakao 政策，好友消息已于 2025-12-31 停止服务。**
> 自 2026-01-01 起，好友消息的发送请求由 Kakao 侧自动改以**品牌消息（自由形式）**发出。
> 调用仍会成功；而且自由文本类型（`FT`/`FI`/`FW`）发往单个接收者的路径目前仍只有这一条，
> 因此现有代码无需立即改动。
>
> 以下情形请改用**品牌消息**：
> - 基于模板的富文本类型（`FL`/`FC`/`FM`/`FP`/`FA`）
> - **非**频道好友的接收者（`targeting` = `N` / `I`）
> - 向已同意接收的全部频道好友群发（`targeting` = `F`）
>
> 消息类型一一对应，转换由服务端处理 —— `FT`→`BT`、`FI`→`BI`、`FW`→`BW`、`FL`→`BL`、
> `FC`→`BC`、`FM`→`BM`、`FP`→`BP`、`FA`→`BA`。
```php
<?php

// 文本型
$sendgo->friendtalk->send([
    'content'  => '7 月限时特惠开始了，欢迎查看。',
    'contacts' => [['contact' => '01012345678']],
]);

// 图片型
$sendgo->friendtalk->send([
    'messageType' => 'FI',
    'content'     => '本周特价商品',
    'imageUrl'    => 'https://cdn.example.com/banner.jpg',
    'imageLink'   => 'https://example.com/event',
    'contacts'    => [['contact' => '01012345678']],
]);

// 带按钮
$sendgo->friendtalk->send([
    'content'  => '优惠券已到账，立即使用。',
    'buttons'  => [
        ['name' => '领取优惠券', 'type' => 'WL', 'linkMo' => 'https://example.com/coupon'],
    ],
    'contacts' => [['contact' => '01012345678']],
]);
```

---

## 品牌消息

品牌消息是好友消息的后继渠道：可触达**非频道好友**的接收者（`'targeting' => 'N'`），也可向**已同意接收的全部频道好友群发**（`'targeting' => 'F'`）。消息类型与好友消息一一对应（`FT`→`BT`、`FI`→`BI`、`FW`→`BW`、`FL`→`BL`、`FC`→`BC`、`FM`→`BM`、`FP`→`BP`、`FA`→`BA`）—— 传入好友消息的代码，服务端会自动转换。

> 仅 v2 支持。请设置 `'api_version' => 'v2'`。

```php
<?php

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

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

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

`broadcast()` 等同于把 `targeting` 固定为 `F` 的 `send()`，两者接受相同的参数。

`targeting` 的取值含义：

| 值 | 含义 |
|----|------|
| `M` | 频道好友中的指定接收者 |
| `N` | 非频道好友的接收者 |
| `I` | 按标识符指定的接收者 |
| `F` | 已同意接收的全部频道好友（群发） |

---

## 短信 / 长短信 / 多媒体短信

```php
<?php

// SMS（90 字节以内）
$sendgo->sms->sendSms([
    'content'  => '[Sendgo] 验证码：123456（请在 5 分钟内输入）',
    'contacts' => [['contact' => '01012345678']],
]);

// LMS（长文，2,000 字节以内）
$sendgo->sms->sendLms([
    'subject'  => '[重要] 服务维护通知',
    'content'  => "服务将于以下时间进行维护。\n\n■ 时间：2026-07-25 02:00 ~ 06:00",
    'contacts' => [['contact' => '01012345678']],
]);

// MMS（含图片）
$sendgo->sms->sendMms([
    'subject'  => '[活动] 7 月特惠',
    'content'  => '欢迎查看本月特价商品。',
    'contacts' => [['contact' => '01012345678']],
]);
```

超出 90 字节的内容会被拒绝而**不会**自动升级为 LMS，因此请按内容长度选择正确的方法。

---

## 错误处理

```php
<?php

use Sendgo\Php\Exception\SendgoException;

try {
    $sendgo->alimtalk->send([...]);
} catch (SendgoException $e) {
    error_log(sprintf(
        'Sendgo %d [%s] %s: %s',
        $e->getStatusCode(),
        $e->getErrorCode(),
        $e->getEndpoint(),
        $e->getMessage()
    ));

    match ($e->getErrorCode()) {
        'INVALID_ACCESS_KEY',
        'INVALID_SECRET_KEY'    => alertOps('请检查 Sendgo 密钥'),
        'IP_NOT_ALLOWED'        => alertOps('该 IP 未在白名单中'),
        'PAYMENT_REQUIRED'      => alertOps('Sendgo 余额不足'),
        'INVALID_TEMPLATE_CODE' => logWarning('模板不存在'),
        default                 => null,
    };
}
```

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

---

## 配置项

| 键 | 是否必填 | 默认值 | 说明 |
|----|----------|--------|------|
| `access_key` | **必填** | — | Sendgo 访问密钥 |
| `secret_key` | **必填** | — | Sendgo 私密密钥 |
| `kakao_sender_key` | 选填 | `null` | Kakao 发送者资料密钥 |
| `sms_sender_key` | 选填 | `null` | 短信主叫号码密钥 |
| `api_version` | 选填 | `v1` | API 版本（`v1` \| `v2`） |
| `url` | 选填 | `https://sendgo.io` | API 基础地址 |

---

## 短链接

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

> 仅 v2 支持。

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

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

```php
// 创建短链接
$short = $sendgo->shortUrl->create([
    'targetUrl' => 'https://example.com/promotions/summer-sale',
    'title'     => '夏季促销落地页',
]);

// 放入消息正文的短链接
$link = $short['data']['shortUrl'];
$code = $short['data']['code'];

// 反应统计 —— 按日趋势 + 设备/来源/国家分解
$stats = $sendgo->shortUrl->stats($code, ['from' => '2026-08-01']);

$sendgo->shortUrl->list(['count' => 10]);
$sendgo->shortUrl->show($code);
$sendgo->shortUrl->deactivate($code);   // 仅停止重定向，统计保留
```

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

---

## 包信息

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

### 如何获取 API 密钥

登录 Sendgo 后，在 **API/SDK → API 对接** 菜单中签发访问密钥与私密密钥。
使用通知消息／好友消息需在 **Kakao 渠道** 登记发送者资料以获取 `kakao_sender_key`；使用短信需在 **发送号码** 菜单登记主叫号码。