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

`github.com/send-go/go` 仅使用标准库实现，**没有任何第三方依赖**。

---

## 安装

```bash
go get github.com/send-go/go
```

---

## 快速上手

```go
package main

import (
	"log"
	"os"

	"github.com/send-go/go/sendgo"
)

func main() {
	client, err := sendgo.New(sendgo.Config{
		AccessKey:      os.Getenv("SENDGO_ACCESS_KEY"),
		SecretKey:      os.Getenv("SENDGO_SECRET_KEY"),
		KakaoSenderKey: os.Getenv("SENDGO_KAKAO_SENDER_KEY"),
		SmsSenderKey:   os.Getenv("SENDGO_SMS_SENDER_KEY"),
		APIVersion:     "v2",
	})
	if err != nil {
		log.Fatal(err)
	}

	err = client.Alimtalk.Send(sendgo.AlimtalkRequest{
		TemplateCode: "ORDER_CONFIRM_001",
		Contacts: []sendgo.Contact{
			{Contact: "01012345678", Name: "洪吉童", Var1: "ORD-001"},
		},
	})
	if err != nil {
		log.Printf("发送失败: %v", err)
	}
}
```

`sendgo.New` 在 `AccessKey` 或 `SecretKey` 为空时返回 error，因此配置缺失会在启动时暴露，而不是等到第一次发送。

各发送渠道都是客户端上的字段：

| 字段 | 渠道 |
|------|------|
| `client.Alimtalk` | Kakao 通知消息 |
| `client.Friendtalk` | Kakao 好友消息 |
| `client.BrandMessage` | Kakao 品牌消息（仅 v2） |
| `client.SMS` | SMS / LMS / MMS |

请创建一次客户端并复用，令牌缓存才能生效。客户端可安全地并发使用。

---

## 通知消息

```go
// 批量发送
err := client.Alimtalk.Send(sendgo.AlimtalkRequest{
	TemplateCode: "ORDER_CONFIRM_001",
	Contacts: []sendgo.Contact{
		{Contact: "01011111111", Var1: "ORD-001", Var2: "29,000元"},
		{Contact: "01022222222", Var1: "ORD-002", Var2: "15,000元"},
	},
})

// 预约发送
err = client.Alimtalk.Send(sendgo.AlimtalkRequest{
	TemplateCode: "PROMO_SUMMER_2026",
	ScheduleType: "SCHEDULED",
	At:           sendgo.String("2026-07-28 09:00:00"),
	Contacts:     []sendgo.Contact{{Contact: "01012345678", Var1: "夏季限定 5 折"}},
})

// 失败时自动回退为短信
err = client.Alimtalk.Send(sendgo.AlimtalkRequest{
	TemplateCode: "DELIVERY_START_001",
	ReplaceSms:   "Y",
	SmsSubject:   sendgo.String("[发货通知]"),
	SmsContent:   sendgo.String("您购买的商品已出库。"),
	Contacts:     []sendgo.Contact{{Contact: "01012345678", Var1: "ORD-001"}},
})
```

---

## 好友消息

> ⚠️ **已停用 —— 按 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`。
```go
// 文本型
err := client.Friendtalk.Send(sendgo.FriendtalkRequest{
	Content:  "7 月限时特惠开始了，欢迎查看。",
	Contacts: []sendgo.Contact{{Contact: "01012345678"}},
})

// 图片型
err = client.Friendtalk.Send(sendgo.FriendtalkRequest{
	MessageType: "FI",
	Content:     "本周特价商品",
	ImageURL:    sendgo.String("https://cdn.example.com/banner.jpg"),
	ImageLink:   sendgo.String("https://example.com/event"),
	Contacts:    []sendgo.Contact{{Contact: "01012345678"}},
})
```

---

## 品牌消息

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

> 仅 v2 支持。请设置 `APIVersion: "v2"`。

```go
// 单条发送 —— Targeting 为 M/N/I 时必须提供 Contacts
result, err := client.BrandMessage.Send(sendgo.BrandMessageRequest{
	Targeting:          "M",
	MessageType:        "FL",
	FriendTemplateUUID: "9cd5460b-6458-4edc-9b11-c26d3013c340",
	Contacts:           []sendgo.Contact{{Contact: "01012345678", Var1: "29,000元"}},
})

// 群发 —— 已同意接收的全部频道好友（不传 Contacts）
result, err = client.BrandMessage.Broadcast(sendgo.BrandMessageRequest{
	MessageType:        "FW",
	FriendTemplateUUID: "9cd5460b-6458-4edc-9b11-c26d3013c340",
})

// 群发在上游为异步处理，需轮询查看进度
campaignID := result["data"].(map[string]any)["campaignId"].(string)
detail, err := client.BrandMessage.Campaign(campaignID)

list, err := client.BrandMessage.Campaigns(sendgo.BrandMessageListQuery{
	From:  "2026-08-01",
	Count: 10,
})
```

`Send` 与 `Broadcast` 返回 `map[string]any`（其余渠道只返回 error），因为响应中包含发送件数与 `campaignId` 等需要读取的数据。取值时请使用带 ok 判断的类型断言，避免响应结构变化导致 panic。

`Targeting` 为空时默认为 `"M"`；`Broadcast` 会强制设为 `"F"`。

---

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

```go
// SMS（90 字节以内）
err := client.SMS.SendSMS(sendgo.SmsRequest{
	Content:  "[Sendgo] 验证码：123456（请在 5 分钟内输入）",
	Contacts: []sendgo.Contact{{Contact: "01012345678"}},
})

// LMS（长文，2,000 字节以内）
err = client.SMS.SendLMS(sendgo.SmsRequest{
	Subject:  sendgo.String("[重要] 服务维护通知"),
	Content:  "服务将于 2026-07-25 02:00 ~ 06:00 进行维护。",
	Contacts: []sendgo.Contact{{Contact: "01012345678"}},
})

// MMS（含图片）
err = client.SMS.SendMMS(sendgo.SmsRequest{
	Subject:  sendgo.String("[活动] 7 月特惠"),
	Content:  "欢迎查看本月特价商品。",
	Contacts: []sendgo.Contact{{Contact: "01012345678"}},
})
```

方法名使用 Go 的缩写词大写惯例：`SendSMS`、`SendLMS`、`SendMMS`（而非 `SendSms`）。

---

## 错误处理

```go
import (
	"errors"
	"log"

	"github.com/send-go/go/sendgo"
)

err := client.Alimtalk.Send(req)

var sgErr *sendgo.SendgoError
if errors.As(err, &sgErr) {
	log.Printf("Sendgo %d [%s]: %s", sgErr.StatusCode, sgErr.ErrorCode, sgErr.Message)

	switch sgErr.ErrorCode {
	case "INVALID_ACCESS_KEY", "INVALID_SECRET_KEY":
		// 是我们的配置有误，而非调用方的请求有误。
		alertOps("请检查 Sendgo 密钥")
	case "IP_NOT_ALLOWED":
		alertOps("该 IP 未在白名单中")
	case "PAYMENT_REQUIRED":
		alertOps("Sendgo 余额不足")
	default:
		if sgErr.StatusCode >= 500 {
			// 临时故障，可重试。
			retryLater(req)
		}
	}
} else if err != nil {
	log.Printf("非 Sendgo 错误: %v", err)
}
```

错误类型为 `*sendgo.SendgoError`（指针），因此 `errors.As` 的目标变量也必须声明为指针类型。
请根据 `ErrorCode` 分支，而不要匹配错误消息文本 —— 消息文案可能变更，错误代码才是契约。
`TOKEN_EXPIRED` 与 `TOKEN_MISMATCH` 已在 SDK 内部处理：会重新签发令牌并自动重试该请求一次。

---

## 配置项

| 字段 | 是否必填 | 默认值 | 说明 |
|------|----------|--------|------|
| `AccessKey` | **必填** | — | Sendgo 访问密钥 |
| `SecretKey` | **必填** | — | Sendgo 私密密钥 |
| `KakaoSenderKey` | 选填 | `""` | Kakao 发送者资料密钥 |
| `SmsSenderKey` | 选填 | `""` | 短信主叫号码密钥 |
| `APIVersion` | 选填 | `"v1"` | API 版本（`v1` \| `v2`） |
| `BaseURL` | 选填 | `"https://sendgo.io"` | API 基础地址 |

---

## 短链接

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

> 仅 v2 支持。

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

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

```go
created, err := client.ShortURL.Create(sendgo.ShortURLRequest{
	TargetURL: "https://example.com/promotions/summer-sale",
	Title:     "夏季促销落地页",
})
if err != nil {
	log.Fatal(err)
}

data := created["data"].(map[string]any)
code := data["code"].(string)

// 反应统计 —— 按日趋势 + 设备/来源/国家分解
stats, err := client.ShortURL.Stats(code, sendgo.ShortURLStatsQuery{From: "2026-08-01"})

client.ShortURL.List(sendgo.ShortURLListQuery{Count: 10})
client.ShortURL.Show(code)
client.ShortURL.Deactivate(code) // 仅停止重定向，统计保留
```

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

---

## 包信息

- **包名**：`github.com/send-go/go`（Go Modules）
- **导入路径**：`github.com/send-go/go/sendgo`
- **仓库**：[send-go/go](https://github.com/send-go/go)
- **文档**：https://pkg.go.dev/github.com/send-go/go
- **许可**：MIT

### 如何获取 API 密钥

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