fe43b9bdce
- 将README从英文翻译为中文 - 添加详细的API参考文档,包括所有管理接口和枚举值说明 - 补充安装、快速开始、认证方式等使用指南 refactor(client): 优化客户端代码结构并添加详细注释 - 为所有API方法添加中文注释和使用说明 - 改进Client结构体和Option配置的设计 - 统一错误处理和响应结构的文档说明
208 lines
6.2 KiB
Go
208 lines
6.2 KiB
Go
package emailcli
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"net/url"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// Client 是访问 Email Server 后端的 HTTP 客户端。
|
||
// 同一个 Client 可以同时持有 ServiceToken 与 AppKey/Secret,
|
||
// 两种头会同时带上;一般建议按用途分别创建实例。
|
||
//
|
||
// 构造方式:
|
||
// - NewServiceClient: 管理端(Authorization: Bearer <token>)
|
||
// - NewAppClient: 发件端 (X-App-Key / X-App-Secret)
|
||
type Client struct {
|
||
baseURL string // 基础地址,不带尾斜杠,例如 https://api.example.com
|
||
serviceToken string // ServiceAuth 令牌(管理接口)
|
||
appKey string // AppAuth AppKey(发件接口)
|
||
appSecret string // AppAuth AppSecret
|
||
httpClient *http.Client // 底层 HTTP 客户端,默认超时 30s
|
||
}
|
||
|
||
// Option 用于在构造 Client 时传入可选配置。
|
||
type Option func(*Client)
|
||
|
||
// WithHTTPClient 使用自定义 *http.Client,例如配置代理、传输层、证书等。
|
||
func WithHTTPClient(hc *http.Client) Option {
|
||
return func(c *Client) { c.httpClient = hc }
|
||
}
|
||
|
||
// WithTimeout 修改底层 HTTP 客户端的超时(默认 30 秒)。
|
||
//
|
||
// 注意:如果同时使用 WithHTTPClient,请确保先设置自定义客户端再应用超时。
|
||
func WithTimeout(d time.Duration) Option {
|
||
return func(c *Client) { c.httpClient.Timeout = d }
|
||
}
|
||
|
||
// NewServiceClient 创建一个管理端客户端。
|
||
//
|
||
// 参数:
|
||
// - baseURL: 后端根地址,例如 "https://mail.example.com"
|
||
// - serviceToken: 后端环境变量 SERVICE_TOKEN 的值
|
||
// - opts: 可选项(WithHTTPClient / WithTimeout)
|
||
//
|
||
// 所有请求会自动带上 Authorization: Bearer <serviceToken> 头,
|
||
// 覆盖账号、签名、配额、通道、发信、审核、队列、健康检查等全部管理接口。
|
||
func NewServiceClient(baseURL, serviceToken string, opts ...Option) *Client {
|
||
c := &Client{
|
||
baseURL: strings.TrimRight(baseURL, "/"),
|
||
serviceToken: serviceToken,
|
||
httpClient: &http.Client{Timeout: 30 * time.Second},
|
||
}
|
||
for _, o := range opts {
|
||
o(c)
|
||
}
|
||
return c
|
||
}
|
||
|
||
// NewAppClient 创建一个发件客户端。
|
||
//
|
||
// 参数:
|
||
// - baseURL: 后端根地址
|
||
// - appKey / appSecret: 管理端创建账号时返回的凭据
|
||
// - opts: 可选项(WithHTTPClient / WithTimeout)
|
||
//
|
||
// 只能用于调用 SendMail 接口。若要调用管理接口,请改用 NewServiceClient。
|
||
func NewAppClient(baseURL, appKey, appSecret string, opts ...Option) *Client {
|
||
c := &Client{
|
||
baseURL: strings.TrimRight(baseURL, "/"),
|
||
appKey: appKey,
|
||
appSecret: appSecret,
|
||
httpClient: &http.Client{Timeout: 30 * time.Second},
|
||
}
|
||
for _, o := range opts {
|
||
o(c)
|
||
}
|
||
return c
|
||
}
|
||
|
||
// APIResponse 是后端统一响应体。
|
||
//
|
||
// { "code": 200, "message": "ok", "data": ... }
|
||
//
|
||
// SDK 内部会解包 Data,正常情况下调用方无需直接使用该类型。
|
||
type APIResponse[T any] struct {
|
||
Code int `json:"code"`
|
||
Message string `json:"message"`
|
||
Data T `json:"data,omitempty"`
|
||
}
|
||
|
||
// APIError 表示后端业务错误(HTTP 状态码可能仍是 200,但 code != 200)。
|
||
//
|
||
// 使用 errors.As 判断:
|
||
//
|
||
// var apiErr *emailcli.APIError
|
||
// if errors.As(err, &apiErr) { ... }
|
||
type APIError struct {
|
||
Code int // 业务错误码(非 HTTP 状态码)
|
||
Message string // 错误描述
|
||
}
|
||
|
||
// Error 实现 error 接口。
|
||
func (e *APIError) Error() string {
|
||
return fmt.Sprintf("api error %d: %s", e.Code, e.Message)
|
||
}
|
||
|
||
// url 拼接出完整请求 URL。
|
||
func (c *Client) url(path string) string {
|
||
return c.baseURL + path
|
||
}
|
||
|
||
// setAuth 根据客户端类型自动写入认证头。
|
||
func (c *Client) setAuth(req *http.Request) {
|
||
if c.serviceToken != "" {
|
||
req.Header.Set("Authorization", "Bearer "+c.serviceToken)
|
||
}
|
||
if c.appKey != "" {
|
||
req.Header.Set("X-App-Key", c.appKey)
|
||
req.Header.Set("X-App-Secret", c.appSecret)
|
||
}
|
||
}
|
||
|
||
// doRequest 是所有 API 的底层实现:组装请求 → 发送 → 解包 APIResponse[T]。
|
||
//
|
||
// 参数说明:
|
||
// - method: HTTP 方法(GET/POST/PUT/DELETE)
|
||
// - path: API 路径(以 / 开头),例如 /api/v1/accounts
|
||
// - body: 请求体(会被 json.Marshal),无则传 nil
|
||
// - query: URL 查询参数,通常由 buildQuery 生成
|
||
//
|
||
// 返回:解包后的 data 字段。code != 200 时返回 *APIError。
|
||
func doRequest[T any](c *Client, ctx context.Context, method, path string, body interface{}, query url.Values) (T, error) {
|
||
var zero T
|
||
|
||
var bodyReader io.Reader
|
||
if body != nil {
|
||
b, err := json.Marshal(body)
|
||
if err != nil {
|
||
return zero, fmt.Errorf("marshal body: %w", err)
|
||
}
|
||
bodyReader = bytes.NewReader(b)
|
||
}
|
||
|
||
reqURL := c.url(path)
|
||
if len(query) > 0 {
|
||
reqURL += "?" + query.Encode()
|
||
}
|
||
|
||
req, err := http.NewRequestWithContext(ctx, method, reqURL, bodyReader)
|
||
if err != nil {
|
||
return zero, fmt.Errorf("new request: %w", err)
|
||
}
|
||
|
||
if body != nil {
|
||
req.Header.Set("Content-Type", "application/json")
|
||
}
|
||
c.setAuth(req)
|
||
|
||
resp, err := c.httpClient.Do(req)
|
||
if err != nil {
|
||
return zero, fmt.Errorf("do request: %w", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
respBody, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
return zero, fmt.Errorf("read response: %w", err)
|
||
}
|
||
|
||
var apiResp APIResponse[T]
|
||
if err := json.Unmarshal(respBody, &apiResp); err != nil {
|
||
return zero, fmt.Errorf("unmarshal response (status %d): %w\nbody: %s", resp.StatusCode, err, string(respBody))
|
||
}
|
||
|
||
if apiResp.Code != 200 {
|
||
return zero, &APIError{Code: apiResp.Code, Message: apiResp.Message}
|
||
}
|
||
|
||
return apiResp.Data, nil
|
||
}
|
||
|
||
// get 是 doRequest 的 GET 便捷函数。
|
||
func get[T any](c *Client, ctx context.Context, path string, query url.Values) (T, error) {
|
||
return doRequest[T](c, ctx, http.MethodGet, path, nil, query)
|
||
}
|
||
|
||
// post 是 doRequest 的 POST 便捷函数。
|
||
func post[T any](c *Client, ctx context.Context, path string, body interface{}) (T, error) {
|
||
return doRequest[T](c, ctx, http.MethodPost, path, body, nil)
|
||
}
|
||
|
||
// put 是 doRequest 的 PUT 便捷函数。
|
||
func put[T any](c *Client, ctx context.Context, path string, body interface{}) (T, error) {
|
||
return doRequest[T](c, ctx, http.MethodPut, path, body, nil)
|
||
}
|
||
|
||
// del 是 doRequest 的 DELETE 便捷函数。
|
||
func del[T any](c *Client, ctx context.Context, path string) (T, error) {
|
||
return doRequest[T](c, ctx, http.MethodDelete, path, nil, nil)
|
||
}
|