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

`io.sendgo:sendgo-java` 是不依赖任何框架的**核心客户端**。Spring Boot Starter 构建在它之上。

要求 Java 17 及以上。

---

## 安装

### Gradle

```groovy
implementation "io.sendgo:sendgo-java:1.1.0"
```

### Maven

```xml
<dependency>
    <groupId>io.sendgo</groupId>
    <artifactId>sendgo-java</artifactId>
    <version>1.1.0</version>
</dependency>
```

---

## 快速上手

```java
import io.sendgo.SendgoClient;
import io.sendgo.SendgoConfig;
import io.sendgo.model.AlimtalkRequest;
import io.sendgo.model.Contact;

SendgoClient sendgo = new SendgoClient(SendgoConfig.builder()
        .accessKey(System.getenv("SENDGO_ACCESS_KEY"))
        .secretKey(System.getenv("SENDGO_SECRET_KEY"))
        .kakaoSenderKey(System.getenv("SENDGO_KAKAO_SENDER_KEY"))
        .smsSenderKey(System.getenv("SENDGO_SMS_SENDER_KEY"))
        .build());

sendgo.alimtalk().send(AlimtalkRequest.builder()
        .templateCode("ORDER_CONFIRM_001")
        .contact(Contact.builder().contact("01012345678").var1("ORD-001").build())
        .build());
```

各发送渠道都是客户端上的**方法**：

| 方法 | 渠道 |
|------|------|
| `sendgo.alimtalk()` | Kakao 通知消息 |
| `sendgo.friendtalk()` | Kakao 好友消息 |
| `sendgo.brandMessage()` | Kakao 品牌消息（仅 v2） |
| `sendgo.sms()` | SMS / LMS / MMS |

`apiVersion` 的默认值为 **`v2`**，因此无需显式设置；只有需要固定使用 v1 的旧对接才要指定。

请创建一次客户端并复用（作为单例或 DI Bean），令牌缓存才能生效。

---

## 通知消息

```java
// 批量发送 —— `contact(...)` 每次调用都会追加一位接收者；
// 也可用 `contacts(List.of(...))` 一次性指定全部
sendgo.alimtalk().send(AlimtalkRequest.builder()
        .templateCode("ORDER_CONFIRM_001")
        .contact(Contact.builder().contact("01011111111").var1("ORD-001").var2("29,000元").build())
        .contact(Contact.builder().contact("01022222222").var1("ORD-002").var2("15,000元").build())
        .build());

// 预约发送
sendgo.alimtalk().send(AlimtalkRequest.builder()
        .templateCode("PROMO_SUMMER_2026")
        .scheduleType("SCHEDULED")
        .at("2026-07-28 09:00:00")
        .contact(Contact.builder().contact("01012345678").var1("夏季限定 5 折").build())
        .build());

// 失败时自动回退为短信
sendgo.alimtalk().send(AlimtalkRequest.builder()
        .templateCode("DELIVERY_START_001")
        .replaceSms("Y")
        .smsSubject("[发货通知]")
        .smsContent("您购买的商品已出库。")
        .contact(Contact.builder().contact("01012345678").var1("ORD-001").build())
        .build());
```

### 任意命名的模板变量

除 `var1` ~ `var8` 之外，也可使用任意名称的变量（对应模板中的 `#{title}`）：

```java
Contact.builder()
        .contact("01012345678")
        .variable("title", "订单确认")
        .variable("amount", "29,000元")
        .build();
```

---

## 好友消息

> ⚠️ **已停用 —— 按 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`。
```java
import io.sendgo.model.FriendtalkRequest;

// 文本型
sendgo.friendtalk().send(FriendtalkRequest.builder()
        .content("7 月限时特惠开始了，欢迎查看。")
        .contact(Contact.builder().contact("01012345678").build())
        .build());

// 图片型
sendgo.friendtalk().send(FriendtalkRequest.builder()
        .messageType("FI")
        .content("本周特价商品")
        .imageUrl("https://cdn.example.com/banner.jpg")
        .imageLink("https://example.com/event")
        .contact(Contact.builder().contact("01012345678").build())
        .build());
```

---

## 品牌消息

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

> 仅 v2 支持，也是本 SDK 的默认版本。

```java
import io.sendgo.model.BrandMessageRequest;
import java.util.Map;

// 单条发送 —— targeting 为 M/N/I 时必须提供接收者
Map<String, Object> result = sendgo.brandMessage().send(BrandMessageRequest.builder()
        .targeting("M")
        .messageType("FL")
        .friendTemplateUuid("9cd5460b-6458-4edc-9b11-c26d3013c340")
        .contact(Contact.builder().contact("01012345678").var1("29,000元").build())
        .build());

// 群发 —— 已同意接收的全部频道好友（不传接收者）
Map<String, Object> accepted = sendgo.brandMessage().broadcast(BrandMessageRequest.builder()
        .messageType("FW")
        .friendTemplateUuid("9cd5460b-6458-4edc-9b11-c26d3013c340")
        .build());

// 群发在上游为异步处理，需轮询查看进度
Map<String, Object> detail = sendgo.brandMessage().campaign(campaignId);
```

`targeting` 的取值含义：

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

品牌消息的方法返回 `Map<String, Object>`（其余渠道返回 void），因为响应中包含发送件数与 `campaignId` 等需要读取的数据。

---

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

```java
import io.sendgo.model.SmsRequest;

// SMS（90 字节以内）
sendgo.sms().sendSms(SmsRequest.sms()
        .content("[Sendgo] 验证码：123456（请在 5 分钟内输入）")
        .contact(Contact.builder().contact("01012345678").build()));

// LMS（长文，2,000 字节以内）
sendgo.sms().sendLms(SmsRequest.lms()
        .subject("[重要] 服务维护通知")
        .content("服务将于 2026-07-25 02:00 ~ 06:00 进行维护。")
        .contact(Contact.builder().contact("01012345678").build()));

// MMS（含图片）
sendgo.sms().sendMms(SmsRequest.mms()
        .subject("[活动] 7 月特惠")
        .content("欢迎查看本月特价商品。")
        .contact(Contact.builder().contact("01012345678").build()));
```

`SmsRequest` **不使用** `builder()`/`build()`：它通过 `sms()` / `lms()` / `mms()` 静态工厂创建，
再用返回自身的链式 setter 配置。

`sendSms` / `sendLms` / `sendMms` 会强制设置消息类型，因此工厂方法与发送方法不会互相矛盾；
`send(...)` 则按请求自身携带的类型发送。

---

## 错误处理

```java
import io.sendgo.exception.SendgoException;

try {
    sendgo.alimtalk().send(request);
} catch (SendgoException e) {
    log.error("Sendgo {} [{}] {}: {}",
            e.getStatusCode(), e.getErrorCode(), e.getEndpoint(), e.getMessage());

    switch (e.getErrorCode()) {
        case "INVALID_ACCESS_KEY", "INVALID_SECRET_KEY" ->
                // 是我们的配置有误，而非调用方的请求有误。
                alertOps("请检查 Sendgo 密钥");
        case "IP_NOT_ALLOWED" ->
                alertOps("该 IP 未在白名单中");
        case "PAYMENT_REQUIRED" ->
                alertOps("Sendgo 余额不足");
        default -> {
            if (e.getStatusCode() >= 500) {
                throw e;   // 临时故障，交由重试机制处理
            }
        }
    }
}
```

`SendgoException` 是运行时异常，因此不会强制 `catch`。这也意味着**忘记捕获**时会向上冒泡 —— 在不希望发送失败影响主流程的位置务必显式处理。
请根据 `getErrorCode()` 分支，而不要匹配错误消息文本 —— 消息文案可能变更，错误代码才是契约。
`TOKEN_EXPIRED` 与 `TOKEN_MISMATCH` 已在 SDK 内部处理：会重新签发令牌并自动重试该请求一次。

---

## 配置项

| 构建器方法 | 是否必填 | 默认值 | 说明 |
|------------|----------|--------|------|
| `accessKey` | **必填** | — | Sendgo 访问密钥 |
| `secretKey` | **必填** | — | Sendgo 私密密钥 |
| `kakaoSenderKey` | 选填 | `null` | Kakao 发送者资料密钥 |
| `smsSenderKey` | 选填 | `null` | 短信主叫号码密钥 |
| `apiVersion` | 选填 | `"v2"` | API 版本（`v1` \| `v2`） |
| `baseUrl` | 选填 | `"https://sendgo.io"` | API 基础地址 |

---

## 短链接

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

> 仅 v2 支持。

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

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

```java
import io.sendgo.model.ShortUrlRequest;

Map<String, Object> created = sendgo.shortUrl().create(ShortUrlRequest.builder()
        .targetUrl("https://example.com/promotions/summer-sale")
        .title("夏季促销落地页")
        .build());

@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) created.get("data");
String code = (String) data.get("code");

// 反应统计 —— 按日趋势 + 设备/来源/国家分解
Map<String, Object> stats = sendgo.shortUrl().stats(code, "2026-08-01", null);

sendgo.shortUrl().list(null, null, 10);
sendgo.shortUrl().show(code);
sendgo.shortUrl().deactivate(code);   // 仅停止重定向，统计保留
```

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

---

## 包信息

- **包名**：`io.sendgo:sendgo-java`（Maven Central）
- **仓库**：[send-go/java](https://github.com/send-go/java)
- **注册表**：https://central.sonatype.com/artifact/io.sendgo/sendgo-java
- **许可**：MIT

### 如何获取 API 密钥

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