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

`sendgo` 是不依赖任何框架的**核心 gem**，仅使用标准库（`net/http`）。Rails 集成包构建在它之上。

要求 Ruby 3.1 及以上。

---

## 安装

```ruby
# Gemfile
gem "sendgo"
```

```bash
bundle install
```

或直接安装：

```bash
gem install sendgo
```

---

## 快速上手

```ruby
require "sendgo"

sendgo = Sendgo::Client.new(
  access_key:       ENV.fetch("SENDGO_ACCESS_KEY"),
  secret_key:       ENV.fetch("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(
  template_code: "ORDER_CONFIRM_001",
  contacts: [{ contact: "01012345678", name: "洪吉童", var1: "ORD-001" }]
)
```

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

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

所有方法都使用**关键字参数**（`template_code:`），而 `contacts` 内部散列的键使用 **REST API 的原样键名**（`contact:`、`var1:`）。

请创建一次客户端并复用，令牌缓存才能生效。

---

## 通知消息

```ruby
# 批量发送
sendgo.alimtalk.send(
  template_code: "ORDER_CONFIRM_001",
  contacts: [
    { contact: "01011111111", var1: "ORD-001", var2: "29,000元" },
    { contact: "01022222222", var1: "ORD-002", var2: "15,000元" }
  ]
)

# 预约发送
sendgo.alimtalk.send(
  template_code: "PROMO_SUMMER_2026",
  schedule_type: "SCHEDULED",
  at: "2026-07-28 09:00:00",
  contacts: [{ contact: "01012345678", var1: "夏季限定 5 折" }]
)

# 失败时自动回退为短信
sendgo.alimtalk.send(
  template_code: "DELIVERY_START_001",
  replace_sms: "Y",
  sms_subject: "[发货通知]",
  sms_content: "您购买的商品已出库。",
  contacts: [{ contact: "01012345678", var1: "ORD-001" }]
)
```

`template_code:` 与 `contacts:` 是必填的关键字参数，遗漏时会立即抛出 `ArgumentError`，而不是在服务端才失败。

---

## 好友消息

> ⚠️ **已停用 —— 按 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`。
```ruby
# 文本型
sendgo.friendtalk.send(
  content: "7 月限时特惠开始了，欢迎查看。",
  contacts: [{ contact: "01012345678" }]
)

# 图片型
sendgo.friendtalk.send(
  message_type: "FI",
  content: "本周特价商品",
  image_url: "https://cdn.example.com/banner.jpg",
  image_link: "https://example.com/event",
  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"`。

```ruby
# 单条发送 —— targeting 为 M/N/I 时必须提供 contacts
sendgo.brand_message.send(
  targeting: "M",
  message_type: "FL",
  friend_template_uuid: "9cd5460b-6458-4edc-9b11-c26d3013c340",
  contacts: [{ contact: "01012345678", var1: "29,000元" }]
)

# 群发 —— 已同意接收的全部频道好友（不传 contacts）
result = sendgo.brand_message.broadcast(
  message_type: "FW",
  friend_template_uuid: "9cd5460b-6458-4edc-9b11-c26d3013c340"
)

# 群发在上游为异步处理，需轮询查看进度
sendgo.brand_message.campaign(result.dig("data", "campaignId"))
sendgo.brand_message.campaigns(from: "2026-08-01", count: 10)
```

`friend_template_uuid:` 是必填的关键字参数；`message_type:` 默认为 `"FT"`，`targeting:` 默认为 `"M"`。
`broadcast` 等同于把 `targeting` 固定为 `"F"` 的 `send`，两者接受相同的参数。

`targeting` 的取值含义：

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

---

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

```ruby
# SMS（90 字节以内）
sendgo.sms.send_sms(
  content: "[Sendgo] 验证码：123456（请在 5 分钟内输入）",
  contacts: [{ contact: "01012345678" }]
)

# LMS（长文，2,000 字节以内）
sendgo.sms.send_lms(
  content: "服务将于 2026-07-25 02:00 ~ 06:00 进行维护。",
  contacts: [{ contact: "01012345678" }],
  subject: "[重要] 服务维护通知"
)

# MMS（含图片）
sendgo.sms.send_mms(
  content: "欢迎查看本月特价商品。",
  contacts: [{ contact: "01012345678" }],
  subject: "[活动] 7 月特惠"
)
```

`send_sms` / `send_lms` / `send_mms` 会分别把 `message_type` 固定为 `SMS` / `LMS` / `MMS`，因此不会发错类型。

---

## 错误处理

```ruby
begin
  sendgo.alimtalk.send(
    template_code: "ORDER_CONFIRM_001",
    contacts: [{ contact: "01012345678" }]
  )
rescue Sendgo::SendgoError => e
  logger.error("Sendgo #{e.status_code} [#{e.error_code}] #{e.endpoint}: #{e.message}")

  case e.error_code
  when "INVALID_ACCESS_KEY", "INVALID_SECRET_KEY"
    # 是我们的配置有误，而非调用方的请求有误。
    alert_ops("请检查 Sendgo 密钥")
  when "IP_NOT_ALLOWED"
    alert_ops("该 IP 未在白名单中")
  when "PAYMENT_REQUIRED"
    alert_ops("Sendgo 余额不足")
  else
    raise if e.status_code >= 500   # 临时故障，交由重试机制处理
  end
end
```

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

---

## 配置项

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

---

## 短链接

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

> 仅 v2 支持。

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

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

```ruby
created = sendgo.short_url.create(
  target_url: "https://example.com/promotions/summer-sale",
  title: "夏季促销落地页"
)

code = created.dig("data", "code")
link = created.dig("data", "shortUrl")

# 反应统计 —— 按日趋势 + 设备/来源/国家分解
stats = sendgo.short_url.stats(code, from: "2026-08-01")

sendgo.short_url.list(count: 10)
sendgo.short_url.show(code)
sendgo.short_url.deactivate(code)   # 仅停止重定向，统计保留
```

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

---

## 包信息

- **包名**：`sendgo`（RubyGems）
- **仓库**：[send-go/ruby](https://github.com/send-go/ruby)
- **注册表**：https://rubygems.org/gems/sendgo
- **许可**：MIT

### 如何获取 API 密钥

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