Files
email-serverr-cli/client.go
T
shiran fe43b9bdce docs(README): 更新文档为中文并完善API参考
- 将README从英文翻译为中文
- 添加详细的API参考文档,包括所有管理接口和枚举值说明
- 补充安装、快速开始、认证方式等使用指南

refactor(client): 优化客户端代码结构并添加详细注释

- 为所有API方法添加中文注释和使用说明
- 改进Client结构体和Option配置的设计
- 统一错误处理和响应结构的文档说明
2026-04-18 15:54:19 +08:00

208 lines
6.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}