From fe43b9bdce32cc6386b98a18e342a163f7ea0a69 Mon Sep 17 00:00:00 2001 From: shiran Date: Sat, 18 Apr 2026 15:54:19 +0800 Subject: [PATCH] =?UTF-8?q?docs(README):=20=E6=9B=B4=E6=96=B0=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E4=B8=BA=E4=B8=AD=E6=96=87=E5=B9=B6=E5=AE=8C=E5=96=84?= =?UTF-8?q?API=E5=8F=82=E8=80=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将README从英文翻译为中文 - 添加详细的API参考文档,包括所有管理接口和枚举值说明 - 补充安装、快速开始、认证方式等使用指南 refactor(client): 优化客户端代码结构并添加详细注释 - 为所有API方法添加中文注释和使用说明 - 改进Client结构体和Option配置的设计 - 统一错误处理和响应结构的文档说明 --- README.md | 529 +++++++++++++++++++++++++++++++++++++------------- account.go | 19 ++ audit.go | 24 +++ audit_rule.go | 26 +++ channel.go | 13 ++ check.go | 9 + client.go | 72 ++++++- doc.go | 127 ++++++++++++ mail.go | 20 +- query.go | 13 ++ queue.go | 12 ++ quota.go | 16 ++ sender.go | 13 ++ signature.go | 19 ++ types.go | 392 +++++++++++++++++++++++-------------- 15 files changed, 1013 insertions(+), 291 deletions(-) create mode 100644 doc.go diff --git a/README.md b/README.md index e3fe4fb..561d539 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,38 @@ # email-serverr-cli -Go client library for the Email Server API. +Email Server API 的 Go 客户端库。提供管理端(ServiceAuth)与发件端(AppAuth)两种客户端, +覆盖邮件发送、账号、签名、配额、通道/发信、审核、队列、健康检查等全部后端能力。 -## Install +- 模块路径: `gitea.s1f.ren/shiran/email-serverr-cli` +- 依赖: 仅使用标准库 `net/http`、`encoding/json`,无第三方依赖 +- 风格: 对每一类资源一个文件,请求/响应类型在 `types.go`,底层请求在 `client.go` + +## 安装 ```bash go get gitea.s1f.ren/shiran/email-serverr-cli ``` -## Quick Start - -### Management Client (ServiceAuth) - ```go -package main - -import ( - "context" - "fmt" - emailcli "gitea.s1f.ren/shiran/email-serverr-cli" -) - -func main() { - client := emailcli.NewServiceClient( - "https://your-server.com", - "your-service-token", - ) - - // List accounts - accounts, err := client.ListAccounts(context.Background(), emailcli.AccountListQuery{ - PaginationQuery: emailcli.PaginationQuery{Page: 1, PageSize: 20}, - }) - if err != nil { - panic(err) - } - for _, a := range accounts.List { - fmt.Printf("Account: %s (ID: %d)\n", a.Name, a.ID) - } -} +import emailcli "gitea.s1f.ren/shiran/email-serverr-cli" ``` -### Mail Sending Client (AppAuth) +## 快速开始 + +### 管理客户端(ServiceAuth) + +```go +client := emailcli.NewServiceClient( + "https://your-server.com", + "your-service-token", +) + +accounts, err := client.ListAccounts(context.Background(), emailcli.AccountListQuery{ + PaginationQuery: emailcli.PaginationQuery{Page: 1, PageSize: 20}, +}) +``` + +### 发件客户端(AppAuth) ```go client := emailcli.NewAppClient( @@ -53,130 +45,393 @@ resp, err := client.SendMail(context.Background(), emailcli.SendMailReq{ To: []string{"recipient@example.com"}, Subject: "Hello", Body: "

Hello World

", - Channel: "default", + // Channel 可选:不传时优先使用账号默认通道,其次使用允许通道列表首个可用项 }) -if err != nil { - panic(err) -} -fmt.Printf("Mail sent: log_id=%d status=%s\n", resp.MailLogID, resp.Status) ``` -## Authentication +### 客户端选项 -| Mode | Constructor | Header | Use Case | -|------|-------------|--------|----------| -| ServiceAuth | `NewServiceClient` | `Authorization: Bearer ` | Management APIs | -| AppAuth | `NewAppClient` | `X-App-Key` + `X-App-Secret` | Mail sending | +```go +client := emailcli.NewServiceClient(baseURL, token, + emailcli.WithTimeout(60*time.Second), + emailcli.WithHTTPClient(customClient), +) +``` -## API Reference +## 认证方式 -### Mail (AppAuth) +| 模式 | 构造函数 | 请求头 | 适用范围 | +|------|----------|--------|----------| +| ServiceAuth | `NewServiceClient` | `Authorization: Bearer ` | 所有 `/api/v1` 管理接口 | +| AppAuth | `NewAppClient` | `X-App-Key` + `X-App-Secret` | 仅 `POST /api/v1/mail/send` | -| Method | Description | -|--------|-------------| -| `SendMail` | Send an email | +--- -### Accounts (ServiceAuth) +## 枚举值说明 -| Method | Description | -|--------|-------------| -| `CreateAccount` | Create mail account | -| `ListAccounts` | List accounts with filters | -| `GetAccount` | Get account details | -| `UpdateAccount` | Update account | -| `DeleteAccount` | Delete account | -| `ResetAccountSecret` | Reset app secret | +### Account.Status(账号状态) +| 值 | 含义 | +|----|------| +| 0 | 禁用 | +| 1 | 启用 | -### Signatures (ServiceAuth) +### Account.AuditMode(审核模式) +| 值 | 含义 | +|----|------| +| 0 | 免审核(直接入队) | +| 1 | 自动(按规则判定) | +| 2 | 人工(待审核) | -| Method | Description | -|--------|-------------| -| `CreateSignature` | Create signature | -| `ListSignatures` | List signatures with filters | -| `GetSignature` | Get signature details | -| `UpdateSignature` | Update signature | -| `DeleteSignature` | Delete signature | -| `AuditSignature` | Approve or reject signature | +### Signature.Status(签名状态) +| 值 | 含义 | +|----|------| +| 0 | 待审核 | +| 1 | 已通过 | +| 2 | 已驳回 | -### Mail Logs (ServiceAuth) +### MailLog.Status(邮件状态) +| 值 | 含义 | +|----|------| +| 0 | 待审核 | +| 1 | 排队中 | +| 2 | 发送中 | +| 3 | 成功 | +| 4 | 失败 | +| 5 | 放弃 | +| 6 | 驳回 | -| Method | Description | -|--------|-------------| -| `ListMailLogs` | List mail logs with filters | -| `GetMailLog` | Get mail log detail with body | -| `GetMailStats` | Get status statistics | +### MailQuota.QuotaType(配额类型) +| 值 | 含义 | +|----|------| +| 1 | 总量配额(`total` 为总发送上限) | +| 2 | 周期配额(到期自动重置) | -### Quotas (ServiceAuth) +### MailQuota.CycleUnit(配额周期单位) +当 `quota_type=2` 时生效,取值:`day`、`week`、`month`、`year` -| Method | Description | -|--------|-------------| -| `CreateQuota` | Create quota | -| `ListQuotas` | List quotas with filters | -| `GetQuotaSummary` | Get quota summary for account | -| `UpdateQuota` | Update quota | -| `DeleteQuota` | Delete quota | +### MailQuota.Status +| 值 | 含义 | +|----|------| +| 0 | 禁用 | +| 1 | 启用 | -### Channels (ServiceAuth) +### Channel.Status / SenderAccount.Status +| 值 | 含义 | +|----|------| +| 0 | 禁用 | +| 1 | 启用 | -| Method | Description | -|--------|-------------| -| `CreateChannel` | Create channel | -| `ListChannels` | List channels with filters | -| `UpdateChannel` | Update channel | -| `DeleteChannel` | Delete channel | +### Channel.Strategy(发信挑选策略) +| 值 | 含义 | +|------|------| +| `round_robin` | 轮询(默认) | +| `weight` | 按 `weight` 加权随机 | +| `least_used` | 今日发送数最少优先 | -### Sender Accounts (ServiceAuth) +### AuditRule.Action(规则动作) +| 值 | 含义 | +|----|------| +| 1 | 自动通过 | +| 2 | 自动驳回 | +| 3 | 转人工 | -| Method | Description | -|--------|-------------| -| `CreateSender` | Create sender under channel | -| `ListSendersByChannel` | List senders for channel | -| `UpdateSender` | Update sender | -| `DeleteSender` | Delete sender | +### AuditRule.RuleType / Target(规则类型与目标) +- `RuleType` 取值: `keyword`、`regex`、`domain` +- `Target` 取值: `subject`、`body`、`to`、`from` -### Audits (ServiceAuth) +### MailAudit.AuditType(审核来源) +| 值 | 含义 | +|----|------| +| 1 | 自动 | +| 2 | 人工 | -| Method | Description | -|--------|-------------| -| `ListAuditPending` | List pending audit items | -| `GetAuditPendingDetail` | Get pending item detail | -| `ApproveAudit` | Approve single item | -| `RejectAudit` | Reject single item | -| `BatchApproveAudit` | Batch approve | -| `BatchRejectAudit` | Batch reject | -| `ListAuditLogs` | List audit history | -| `GetAuditStats` | Get audit statistics | +### MailAudit.Action(审核动作) +| 值 | 含义 | +|----|------| +| 1 | 通过 | +| 2 | 驳回 | -### Audit Rules (ServiceAuth) +### SendMailReq.ContentType +取值:`text/plain`(默认)、`text/html` -| Method | Description | -|--------|-------------| -| `CreateAuditRule` | Create rule | -| `ListAuditRules` | List all rules | -| `GetAuditRule` | Get rule details | -| `UpdateAuditRule` | Update rule | -| `DeleteAuditRule` | Delete rule | -| `UpdateAuditRuleStatus` | Toggle rule status | -| `TestAuditRule` | Test rules against sample | +--- -### Queue (ServiceAuth) +## API 参考 -| Method | Description | -|--------|-------------| -| `GetQueueStatus` | Get queue lengths | -| `ListQueuePending` | List pending queue items | -| `CancelQueueItem` | Cancel queued mail | -| `RetryQueueItem` | Retry failed mail | +所有管理接口挂载在 `/api/v1` 下。以下按功能模块分组,并给出方法签名、HTTP 路径与关键参数说明。 -### Health Checks (ServiceAuth) +### 一、发送邮件(AppAuth) -| Method | Description | -|--------|-------------| -| `ListCheckLogs` | List check logs | -| `GetCheckSummary` | Get sender health summary | -| `TriggerCheck` | Trigger health check | +#### `SendMail(ctx, req SendMailReq) -> *SendMailResp` -## Error Handling +`POST /api/v1/mail/send` + +`SendMailReq` 字段: + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `To` | `[]string` | 是 | 收件人列表,至少 1 个 | +| `Cc` | `[]string` | 否 | 抄送 | +| `Bcc` | `[]string` | 否 | 密送 | +| `Subject` | `string` | 是 | 主题 | +| `Body` | `string` | 是 | 正文 | +| `ContentType` | `string` | 否 | 默认 `text/html` | +| `Channel` | `string` | 否 | 通道 `code`。为空时按 账号默认通道 → 账号允许通道首个可用 顺序自动解析 | +| `SignatureID` | `*uint` | 否 | 指定签名 ID(用户必须拥有且已审核) | +| `SignatureTitle` | `string` | 否 | 按 title + user_id 选择签名;未传则使用默认签名 | +| `Attachments` | `[]AttachmentItem` | 否 | 附件(`filename` + base64 `content`) | + +`SendMailResp` 字段: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `MailLogID` | `uint` | 创建的邮件日志 ID | +| `Status` | `string` | `queued` / `pending_audit` / `rejected` | + +--- + +### 二、账号管理(ServiceAuth) + +#### `CreateAccount(ctx, req CreateAccountReq) -> *CreateAccountResp` +`POST /api/v1/accounts` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `UserID` | `int` | 是 | 关联用户 ID | +| `Name` | `string` | 是 | 账号名称(≤100) | +| `AuditMode` | `*int8` | 否 | 审核模式枚举,默认 `0` | +| `RateLimit` | `*int` | 否 | 频率限制(封/分钟),`0` 表示不限 | +| `DefaultChannelID` | `*uint` | 否 | 默认发件通道 ID | +| `AllowedChannels` | `string` | 否 | 允许发件通道 ID 列表的 JSON 字符串,例如 `"[1,2]"`。为空时视作不限制 | +| `Remark` | `string` | 否 | 备注 | + +返回 `CreateAccountResp`(首次创建返回明文 `AppSecret`): +```json +{ "id": 1, "app_key": "...", "app_secret": "只在此次展示", "name": "..." } +``` + +#### `ListAccounts(ctx, q AccountListQuery) -> *PaginationResult[Account]` +`GET /api/v1/accounts?page=&page_size=&user_id=&status=&keyword=` + +| 参数 | 类型 | 说明 | +|------|------|------| +| `Page`/`PageSize` | `int` | 分页,`PageSize` 默认 20 | +| `UserID` | `*int` | 按用户筛选 | +| `Status` | `*int8` | 账号状态(0/1) | +| `Keyword` | `string` | 模糊匹配 `name` 或 `remark` | + +#### `GetAccount(ctx, id uint) -> *Account` +`GET /api/v1/accounts/{id}` + +#### `UpdateAccount(ctx, id uint, req UpdateAccountReq) -> *Account` +`PUT /api/v1/accounts/{id}` +所有字段均为可选指针,只更新已传字段。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `Name` | `*string` | | +| `Status` | `*int8` | 启用/禁用 | +| `AuditMode` | `*int8` | | +| `RateLimit` | `*int` | | +| `DefaultChannelID` | `*uint` | 默认发件通道 | +| `AllowedChannels` | `*string` | 允许发件通道 ID 列表 JSON | +| `DefaultSignatureID` | `*uint` | 默认签名 ID | +| `Remark` | `*string` | | + +#### `DeleteAccount(ctx, id uint) error` +`DELETE /api/v1/accounts/{id}` + +#### `ResetAccountSecret(ctx, id uint) -> *ResetSecretResp` +`POST /api/v1/accounts/{id}/reset-secret` +返回新生成的明文 `AppSecret`。 + +--- + +### 三、签名管理(ServiceAuth) + +#### `CreateSignature(ctx, req CreateSignatureReq) -> *Signature` +`POST /api/v1/signatures` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `UserID` | `int` | 是 | | +| `AccountID` | `*uint` | 否 | 绑定账号,为空表示用户全局签名 | +| `Title` | `string` | 是 | 中文抬头 | +| `EnglishName` | `string` | 是 | 英文标识(用于组装 From 地址) | +| `Content` | `string` | 否 | HTML 签名内容 | +| `Applicant` | `string` | 否 | 申请人 | +| `ApplicantInfo` | `string` | 否 | 申请说明 | + +新建默认为 `Status=0`(待审核)。 + +#### `ListSignatures(ctx, q SignatureListQuery) -> *PaginationResult[Signature]` +`GET /api/v1/signatures?page=&page_size=&user_id=&account_id=&status=&keyword=` + +#### `GetSignature(ctx, id uint)` / `UpdateSignature` / `DeleteSignature` +`GET|PUT|DELETE /api/v1/signatures/{id}` + +#### `AuditSignature(ctx, id uint, req AuditSignatureReq)` +`POST /api/v1/signatures/{id}/audit` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `Action` | `int8` | `1`=通过,`2`=驳回 | +| `RejectReason` | `string` | 驳回时建议填写 | +| `Auditor` | `string` | 审核人标识 | + +--- + +### 四、邮件日志(ServiceAuth) + +#### `ListMailLogs(ctx, q MailLogListQuery) -> *PaginationResult[MailLog]` +`GET /api/v1/mail-logs` + +| 参数 | 类型 | 说明 | +|------|------|------| +| `UserID` | `*int` | | +| `AccountID` | `*uint` | | +| `Status` | `*int8` | 见 `MailLog.Status` 枚举 | +| `StartDate`/`EndDate` | `string` | `YYYY-MM-DD` | +| `To` | `string` | 收件人精确匹配 | +| `Keyword` | `string` | 模糊匹配主题或收件人 | + +#### `GetMailLog(ctx, id uint) -> *MailLogDetail` +`GET /api/v1/mail-logs/{id}` 返回 `MailLog` + 正文 `Body`。 + +#### `GetMailStats(ctx) -> []MailStatItem` +`GET /api/v1/mail-logs/stats` 按 `Status` 分组的邮件数统计。 + +--- + +### 五、配额管理(ServiceAuth) + +#### `CreateQuota(ctx, req CreateQuotaReq) -> *MailQuota` +`POST /api/v1/quotas` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `UserID` | `int` | 是 | | +| `AccountID` | `uint` | 是 | | +| `QuotaType` | `int8` | 是 | `1`=总量,`2`=周期 | +| `Total` | `int` | 是 | 额度上限 | +| `ExpireAt` | `string` | 否 | `YYYY-MM-DD HH:mm:ss` | +| `CycleUnit` | `string` | 否 | `day` / `week` / `month` / `year` | +| `CycleResetAt` | `string` | 否 | 周期起始时间 | + +#### `ListQuotas(ctx, q QuotaListQuery)` · `GetQuotaSummary(ctx, accountID)` · `UpdateQuota(ctx, id, req)` · `DeleteQuota(ctx, id)` + +`UpdateQuotaReq` 支持局部更新 `Total`、`Status`、`ExpireAt`、`CycleUnit`、`CycleResetAt`。 + +`QuotaListQuery` 过滤参数:`UserID`、`AccountID`、`QuotaType`、`Status`。 + +--- + +### 六、通道与发信账号(ServiceAuth) + +#### 通道 `Channel` + +- `CreateChannel(ctx, req CreateChannelReq) -> *Channel` — `POST /api/v1/channels` +- `ListChannels(ctx, q ChannelListQuery) -> *PaginationResult[Channel]` — `GET /api/v1/channels` +- `UpdateChannel(ctx, id, req UpdateChannelReq) -> *Channel` — `PUT /api/v1/channels/{id}` +- `DeleteChannel(ctx, id uint) error` — `DELETE /api/v1/channels/{id}` + +`CreateChannelReq` 字段: + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `Code` | `string` | 是 | 唯一标识(发送邮件用) | +| `Name` | `string` | 是 | | +| `Description` | `string` | 否 | | +| `Strategy` | `string` | 否 | 见 `Channel.Strategy` 枚举 | + +#### 发信账号 `SenderAccount` + +- `CreateSender(ctx, channelID uint, req CreateSenderReq) -> *SenderAccount` — `POST /api/v1/channels/{channelID}/senders` +- `ListSendersByChannel(ctx, channelID, q SenderListQuery) -> *PaginationResult[SenderAccount]` — `GET /api/v1/channels/{channelID}/senders` +- `UpdateSender(ctx, id uint, req UpdateSenderReq) -> *SenderAccount` — `PUT /api/v1/senders/{id}` +- `DeleteSender(ctx, id uint) error` — `DELETE /api/v1/senders/{id}` + +`CreateSenderReq` 关键字段:`Name`、`SmtpHost`、`SmtpPort`、`SmtpUser`、`SmtpPassword`、`SmtpSSL`、`FromName`、`FromAddress`、`DailyLimit`、`Weight`。 + +--- + +### 七、审核(ServiceAuth) + +- `ListAuditPending(ctx, q AuditPendingQuery) -> *PaginationResult[MailLog]` — `GET /api/v1/audits/pending` +- `GetAuditPendingDetail(ctx, id uint) -> *MailLogDetail` — `GET /api/v1/audits/pending/{id}` +- `ApproveAudit(ctx, id uint) error` — `POST /api/v1/audits/{id}/approve` +- `RejectAudit(ctx, id uint, req AuditRejectReq) error` — `POST /api/v1/audits/{id}/reject` +- `BatchApproveAudit(ctx, req BatchAuditApproveReq) error` — `POST /api/v1/audits/batch/approve` +- `BatchRejectAudit(ctx, req BatchAuditRejectReq) error` — `POST /api/v1/audits/batch/reject` +- `ListAuditLogs(ctx, q AuditLogQuery) -> *PaginationResult[MailAudit]` — `GET /api/v1/audits/logs` +- `GetAuditStats(ctx) -> *AuditStats` — `GET /api/v1/audits/stats` + +`AuditPendingQuery` 过滤:`AccountID`、`UserID`、`Keyword`。 +`AuditLogQuery` 过滤:`AccountID`、`UserID`、`Action`、`AuditType`、`StartDate`、`EndDate`。 + +--- + +### 八、审核规则(ServiceAuth) + +- `CreateAuditRule(ctx, req CreateAuditRuleReq) -> *AuditRule` — `POST /api/v1/audit-rules` +- `ListAuditRules(ctx) -> []AuditRule` — `GET /api/v1/audit-rules` +- `GetAuditRule(ctx, id uint) -> *AuditRule` — `GET /api/v1/audit-rules/{id}` +- `UpdateAuditRule(ctx, id uint, req UpdateAuditRuleReq) -> *AuditRule` — `PUT /api/v1/audit-rules/{id}` +- `DeleteAuditRule(ctx, id uint) error` — `DELETE /api/v1/audit-rules/{id}` +- `UpdateAuditRuleStatus(ctx, id uint, status int8) -> *AuditRule` — `PUT /api/v1/audit-rules/{id}/status` +- `TestAuditRule(ctx, req TestAuditRuleReq) -> *TestAuditRuleResp` — `POST /api/v1/audit-rules/test` + +`CreateAuditRuleReq` 字段: + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `Name` | `string` | 是 | | +| `RuleType` | `string` | 是 | 见 `AuditRule.RuleType` 枚举 | +| `Target` | `string` | 是 | 见 `AuditRule.Target` 枚举 | +| `Condition` | `string` | 是 | 关键词或正则表达式 | +| `Action` | `int8` | 是 | 见 `AuditRule.Action` 枚举 | +| `Priority` | `int` | 否 | 数字越大优先级越高 | +| `Remark` | `string` | 否 | | + +--- + +### 九、队列(ServiceAuth) + +- `GetQueueStatus(ctx) -> *QueueStatusData` — `GET /api/v1/queue/status` +- `ListQueuePending(ctx, q QueuePendingQuery) -> *PaginationResult[MailLog]` — `GET /api/v1/queue/pending` +- `CancelQueueItem(ctx, mailLogID uint) error` — `POST /api/v1/queue/{mailLogID}/cancel` +- `RetryQueueItem(ctx, mailLogID uint) error` — `POST /api/v1/queue/{mailLogID}/retry` + +`QueueStatusData` 返回各通道队列长度与延迟队列长度: + +```go +type QueueStatusData struct { + Queues map[string]int64 `json:"queues"` // key = channel code + DelayQueue int64 `json:"delay_queue"` +} +``` + +--- + +### 十、健康检查(ServiceAuth) + +- `ListCheckLogs(ctx, q CheckLogQuery) -> *PaginationResult[CheckLog]` — `GET /api/v1/check-logs` +- `GetCheckSummary(ctx) -> []SenderHealth` — `GET /api/v1/check-logs/summary` +- `TriggerCheck(ctx, senderAccountID uint) -> *TriggerCheckResp` — `POST /api/v1/check-logs/trigger/{senderAccountID}` + +`CheckLogQuery` 过滤:`SenderAccountID`、`Received`、`StartDate`、`EndDate`。 + +--- + +## 统一返回结构 + +后端所有接口统一使用: + +```json +{ "code": 200, "message": "ok", "data": { /* 业务数据 */ } } +``` + +SDK 内部会解包 `data` 并在非 200 时返回 `*APIError`: ```go resp, err := client.SendMail(ctx, req) @@ -188,13 +443,17 @@ if err != nil { } ``` -## Options +分页接口统一包装: ```go -client := emailcli.NewServiceClient( - "https://your-server.com", - "token", - emailcli.WithTimeout(60 * time.Second), - emailcli.WithHTTPClient(customClient), -) +type PaginationResult[T any] struct { + List []T `json:"list"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` +} ``` + +## License + +MIT diff --git a/account.go b/account.go index 30ccaa5..4e414f6 100644 --- a/account.go +++ b/account.go @@ -5,10 +5,16 @@ import ( "fmt" ) +// CreateAccount 创建邮件账号。返回的 AppSecret 是明文,仅此一次,需妥善保存。 +// +// POST /api/v1/accounts ServiceAuth func (c *Client) CreateAccount(ctx context.Context, req CreateAccountReq) (*CreateAccountResp, error) { return post[*CreateAccountResp](c, ctx, "/api/v1/accounts", req) } +// ListAccounts 分页查询账号。支持按用户、状态、关键字过滤。 +// +// GET /api/v1/accounts?page=&page_size=&user_id=&status=&keyword= ServiceAuth func (c *Client) ListAccounts(ctx context.Context, q AccountListQuery) (*PaginationResult[Account], error) { params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{ "user_id": q.UserID, @@ -18,19 +24,32 @@ func (c *Client) ListAccounts(ctx context.Context, q AccountListQuery) (*Paginat return get[*PaginationResult[Account]](c, ctx, "/api/v1/accounts", buildQuery(params)) } +// GetAccount 获取单个账号详情。 +// +// GET /api/v1/accounts/{id} ServiceAuth func (c *Client) GetAccount(ctx context.Context, id uint) (*Account, error) { return get[*Account](c, ctx, fmt.Sprintf("/api/v1/accounts/%d", id), nil) } +// UpdateAccount 局部更新账号字段(只更新 req 中非 nil 的字段)。 +// +// PUT /api/v1/accounts/{id} ServiceAuth func (c *Client) UpdateAccount(ctx context.Context, id uint, req UpdateAccountReq) (*Account, error) { return put[*Account](c, ctx, fmt.Sprintf("/api/v1/accounts/%d", id), req) } +// DeleteAccount 删除账号(软删除)。 +// +// DELETE /api/v1/accounts/{id} ServiceAuth func (c *Client) DeleteAccount(ctx context.Context, id uint) error { _, err := del[any](c, ctx, fmt.Sprintf("/api/v1/accounts/%d", id)) return err } +// ResetAccountSecret 重置账号 AppSecret,旧密钥立即失效。 +// 返回的新 AppSecret 是明文,仅此一次。 +// +// POST /api/v1/accounts/{id}/reset-secret ServiceAuth func (c *Client) ResetAccountSecret(ctx context.Context, id uint) (*ResetSecretResp, error) { return post[*ResetSecretResp](c, ctx, fmt.Sprintf("/api/v1/accounts/%d/reset-secret", id), nil) } diff --git a/audit.go b/audit.go index 932d57d..525e9a7 100644 --- a/audit.go +++ b/audit.go @@ -5,6 +5,9 @@ import ( "fmt" ) +// ListAuditPending 分页查询待人工审核的邮件。 +// +// GET /api/v1/audits/pending?page=&page_size=&user_id=&account_id=&keyword= ServiceAuth func (c *Client) ListAuditPending(ctx context.Context, q AuditPendingQuery) (*PaginationResult[MailLog], error) { params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{ "user_id": q.UserID, @@ -14,30 +17,48 @@ func (c *Client) ListAuditPending(ctx context.Context, q AuditPendingQuery) (*Pa return get[*PaginationResult[MailLog]](c, ctx, "/api/v1/audits/pending", buildQuery(params)) } +// GetAuditPendingDetail 获取待审核邮件的完整详情(含正文)。 +// +// GET /api/v1/audits/pending/{id} ServiceAuth func (c *Client) GetAuditPendingDetail(ctx context.Context, id uint) (*MailLogDetail, error) { return get[*MailLogDetail](c, ctx, fmt.Sprintf("/api/v1/audits/pending/%d", id), nil) } +// ApproveAudit 审核通过单封邮件,通过后邮件立即入队发送。 +// +// POST /api/v1/audits/{id}/approve ServiceAuth func (c *Client) ApproveAudit(ctx context.Context, id uint) error { _, err := post[any](c, ctx, fmt.Sprintf("/api/v1/audits/%d/approve", id), nil) return err } +// RejectAudit 审核驳回单封邮件,被驳回的邮件会退还配额。 +// +// POST /api/v1/audits/{id}/reject ServiceAuth func (c *Client) RejectAudit(ctx context.Context, id uint, req AuditRejectReq) error { _, err := post[any](c, ctx, fmt.Sprintf("/api/v1/audits/%d/reject", id), req) return err } +// BatchApproveAudit 批量通过。MailLogIDs 至少 1 个。 +// +// POST /api/v1/audits/batch/approve ServiceAuth func (c *Client) BatchApproveAudit(ctx context.Context, req BatchAuditApproveReq) error { _, err := post[any](c, ctx, "/api/v1/audits/batch/approve", req) return err } +// BatchRejectAudit 批量驳回。建议填写 RejectReason 方便溯源。 +// +// POST /api/v1/audits/batch/reject ServiceAuth func (c *Client) BatchRejectAudit(ctx context.Context, req BatchAuditRejectReq) error { _, err := post[any](c, ctx, "/api/v1/audits/batch/reject", req) return err } +// ListAuditLogs 分页查询审核历史记录。 +// +// GET /api/v1/audits/logs?page=&page_size=&audit_type=&action=&user_id=&start_date=&end_date= ServiceAuth func (c *Client) ListAuditLogs(ctx context.Context, q AuditLogQuery) (*PaginationResult[MailAudit], error) { params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{ "audit_type": q.AuditType, @@ -49,6 +70,9 @@ func (c *Client) ListAuditLogs(ctx context.Context, q AuditLogQuery) (*Paginatio return get[*PaginationResult[MailAudit]](c, ctx, "/api/v1/audits/logs", buildQuery(params)) } +// GetAuditStats 审核概览统计:待审核数量、今日自动/人工通过/驳回分布。 +// +// GET /api/v1/audits/stats ServiceAuth func (c *Client) GetAuditStats(ctx context.Context) (*AuditStats, error) { return get[*AuditStats](c, ctx, "/api/v1/audits/stats", nil) } diff --git a/audit_rule.go b/audit_rule.go index 2a24369..742274d 100644 --- a/audit_rule.go +++ b/audit_rule.go @@ -5,31 +5,57 @@ import ( "fmt" ) +// CreateAuditRule 创建审核规则。 +// - RuleType: keyword / regex / domain +// - Target: subject / body / to / from +// - Action: RuleActionApprove / RuleActionReject / RuleActionToManual +// - Priority: 数字越大优先级越高,未传时默认 0 +// +// POST /api/v1/audit-rules ServiceAuth func (c *Client) CreateAuditRule(ctx context.Context, req CreateAuditRuleReq) (*AuditRule, error) { return post[*AuditRule](c, ctx, "/api/v1/audit-rules", req) } +// ListAuditRules 列出全部规则(不分页,按 Priority 降序)。 +// +// GET /api/v1/audit-rules ServiceAuth func (c *Client) ListAuditRules(ctx context.Context) ([]AuditRule, error) { return get[[]AuditRule](c, ctx, "/api/v1/audit-rules", nil) } +// GetAuditRule 获取规则详情。 +// +// GET /api/v1/audit-rules/{id} ServiceAuth func (c *Client) GetAuditRule(ctx context.Context, id uint) (*AuditRule, error) { return get[*AuditRule](c, ctx, fmt.Sprintf("/api/v1/audit-rules/%d", id), nil) } +// UpdateAuditRule 局部更新规则。 +// +// PUT /api/v1/audit-rules/{id} ServiceAuth func (c *Client) UpdateAuditRule(ctx context.Context, id uint, req UpdateAuditRuleReq) (*AuditRule, error) { return put[*AuditRule](c, ctx, fmt.Sprintf("/api/v1/audit-rules/%d", id), req) } +// DeleteAuditRule 删除规则(软删除)。 +// +// DELETE /api/v1/audit-rules/{id} ServiceAuth func (c *Client) DeleteAuditRule(ctx context.Context, id uint) error { _, err := del[any](c, ctx, fmt.Sprintf("/api/v1/audit-rules/%d", id)) return err } +// UpdateAuditRuleStatus 仅更新规则启停状态。status 使用 StatusEnabled/StatusDisabled 常量。 +// +// PUT /api/v1/audit-rules/{id}/status ServiceAuth func (c *Client) UpdateAuditRuleStatus(ctx context.Context, id uint, status int8) (*AuditRule, error) { return put[*AuditRule](c, ctx, fmt.Sprintf("/api/v1/audit-rules/%d/status", id), map[string]int8{"status": status}) } +// TestAuditRule 用样本内容试跑规则命中,不真正发件。 +// 返回的 Action 为 approve/reject/to_manual/none 之一。 +// +// POST /api/v1/audit-rules/test ServiceAuth func (c *Client) TestAuditRule(ctx context.Context, req TestAuditRuleReq) (*TestAuditRuleResp, error) { return post[*TestAuditRuleResp](c, ctx, "/api/v1/audit-rules/test", req) } diff --git a/channel.go b/channel.go index 0b88bef..acdddfe 100644 --- a/channel.go +++ b/channel.go @@ -5,10 +5,17 @@ import ( "fmt" ) +// CreateChannel 创建发件通道。Code 为唯一标识,发件请求的 channel 字段填写此 Code。 +// Strategy 可取 StrategyRoundRobin / StrategyWeight / StrategyLeastUsed。 +// +// POST /api/v1/channels ServiceAuth func (c *Client) CreateChannel(ctx context.Context, req CreateChannelReq) (*Channel, error) { return post[*Channel](c, ctx, "/api/v1/channels", req) } +// ListChannels 分页查询通道,支持按状态与关键字过滤。 +// +// GET /api/v1/channels?page=&page_size=&status=&keyword= ServiceAuth func (c *Client) ListChannels(ctx context.Context, q ChannelListQuery) (*PaginationResult[Channel], error) { params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{ "status": q.Status, @@ -17,10 +24,16 @@ func (c *Client) ListChannels(ctx context.Context, q ChannelListQuery) (*Paginat return get[*PaginationResult[Channel]](c, ctx, "/api/v1/channels", buildQuery(params)) } +// UpdateChannel 局部更新通道。Code 不可修改;修改 Status 可启用/禁用通道。 +// +// PUT /api/v1/channels/{id} ServiceAuth func (c *Client) UpdateChannel(ctx context.Context, id uint, req UpdateChannelReq) (*Channel, error) { return put[*Channel](c, ctx, fmt.Sprintf("/api/v1/channels/%d", id), req) } +// DeleteChannel 删除通道(软删除)。通道下如有启用中的发信账号,建议先清空。 +// +// DELETE /api/v1/channels/{id} ServiceAuth func (c *Client) DeleteChannel(ctx context.Context, id uint) error { _, err := del[any](c, ctx, fmt.Sprintf("/api/v1/channels/%d", id)) return err diff --git a/check.go b/check.go index 715d7ed..727d03a 100644 --- a/check.go +++ b/check.go @@ -5,6 +5,9 @@ import ( "fmt" ) +// ListCheckLogs 分页查询发信账号的健康检查记录。 +// +// GET /api/v1/check-logs?page=&page_size=&sender_account_id=&start_date=&end_date= ServiceAuth func (c *Client) ListCheckLogs(ctx context.Context, q CheckLogQuery) (*PaginationResult[CheckLog], error) { params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{ "sender_account_id": q.SenderAccountID, @@ -14,10 +17,16 @@ func (c *Client) ListCheckLogs(ctx context.Context, q CheckLogQuery) (*Paginatio return get[*PaginationResult[CheckLog]](c, ctx, "/api/v1/check-logs", buildQuery(params)) } +// GetCheckSummary 获取全部发信账号的健康汇总(近期成功率、状态等)。 +// +// GET /api/v1/check-logs/summary ServiceAuth func (c *Client) GetCheckSummary(ctx context.Context) ([]SenderHealth, error) { return get[[]SenderHealth](c, ctx, "/api/v1/check-logs/summary", nil) } +// TriggerCheck 手动触发一次指定发信账号的健康检查(同步返回结果)。 +// +// POST /api/v1/check-logs/trigger/{senderAccountID} ServiceAuth func (c *Client) TriggerCheck(ctx context.Context, senderAccountID uint) (*TriggerCheckResp, error) { return post[*TriggerCheckResp](c, ctx, fmt.Sprintf("/api/v1/check-logs/trigger/%d", senderAccountID), nil) } diff --git a/client.go b/client.go index 8a5c18d..c5c8002 100644 --- a/client.go +++ b/client.go @@ -12,25 +12,45 @@ import ( "time" ) +// Client 是访问 Email Server 后端的 HTTP 客户端。 +// 同一个 Client 可以同时持有 ServiceToken 与 AppKey/Secret, +// 两种头会同时带上;一般建议按用途分别创建实例。 +// +// 构造方式: +// - NewServiceClient: 管理端(Authorization: Bearer ) +// - NewAppClient: 发件端 (X-App-Key / X-App-Secret) type Client struct { - baseURL string - serviceToken string - appKey string - appSecret string - httpClient *http.Client + 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 creates a client authenticated with a service token (management APIs). +// NewServiceClient 创建一个管理端客户端。 +// +// 参数: +// - baseURL: 后端根地址,例如 "https://mail.example.com" +// - serviceToken: 后端环境变量 SERVICE_TOKEN 的值 +// - opts: 可选项(WithHTTPClient / WithTimeout) +// +// 所有请求会自动带上 Authorization: Bearer 头, +// 覆盖账号、签名、配额、通道、发信、审核、队列、健康检查等全部管理接口。 func NewServiceClient(baseURL, serviceToken string, opts ...Option) *Client { c := &Client{ baseURL: strings.TrimRight(baseURL, "/"), @@ -43,7 +63,14 @@ func NewServiceClient(baseURL, serviceToken string, opts ...Option) *Client { return c } -// NewAppClient creates a client authenticated with AppKey/AppSecret (mail sending API). +// NewAppClient 创建一个发件客户端。 +// +// 参数: +// - baseURL: 后端根地址 +// - appKey / appSecret: 管理端创建账号时返回的凭据 +// - opts: 可选项(WithHTTPClient / WithTimeout) +// +// 只能用于调用 SendMail 接口。若要调用管理接口,请改用 NewServiceClient。 func NewAppClient(baseURL, appKey, appSecret string, opts ...Option) *Client { c := &Client{ baseURL: strings.TrimRight(baseURL, "/"), @@ -57,25 +84,39 @@ func NewAppClient(baseURL, appKey, appSecret string, opts ...Option) *Client { 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 - Message string + 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) @@ -86,6 +127,15 @@ func (c *Client) setAuth(req *http.Request) { } } +// 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 @@ -136,18 +186,22 @@ func doRequest[T any](c *Client, ctx context.Context, method, path string, body 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) } diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..7109e07 --- /dev/null +++ b/doc.go @@ -0,0 +1,127 @@ +// Package emailcli 提供 Email Server 后端 API 的 Go 客户端。 +// +// 两种客户端: +// - NewServiceClient: 使用 SERVICE_TOKEN,访问所有 /api/v1 管理接口 +// (账号/签名/配额/通道/审核/队列/健康检查等)。 +// - NewAppClient: 使用 App Key + App Secret,仅用于 POST /api/v1/mail/send 发送邮件。 +// +// 快速上手: +// +// client := emailcli.NewServiceClient("https://server.com", "service-token") +// accounts, err := client.ListAccounts(ctx, emailcli.AccountListQuery{ +// PaginationQuery: emailcli.PaginationQuery{Page: 1, PageSize: 20}, +// }) +// +// mailer := emailcli.NewAppClient("https://server.com", "app-key", "app-secret") +// resp, err := mailer.SendMail(ctx, emailcli.SendMailReq{ +// To: []string{"recipient@example.com"}, +// Subject: "Hello", Body: "

Hi

", +// }) +// +// 所有后端接口统一返回: +// +// { "code": 200, "message": "ok", "data": <业务数据> } +// +// SDK 会自动解包 data;当 code != 200 时返回 *APIError。 +// +// 枚举值集中在此常量表,调用方可直接使用,避免手写 magic number。 +package emailcli + +// --- Account.Status / Channel.Status / SenderAccount.Status / MailQuota.Status 通用启禁用 --- +const ( + StatusDisabled int8 = 0 // 禁用 + StatusEnabled int8 = 1 // 启用 +) + +// --- Account.AuditMode 账号审核模式 --- +const ( + AuditModeNone int8 = 0 // 免审核,直接入队 + AuditModeAuto int8 = 1 // 自动审核,由规则判定 + AuditModeManual int8 = 2 // 人工审核 +) + +// --- Signature.Status 签名状态 --- +const ( + SignatureStatusPending int8 = 0 // 待审核 + SignatureStatusApproved int8 = 1 // 已通过 + SignatureStatusRejected int8 = 2 // 已驳回 +) + +// --- MailLog.Status 邮件状态 --- +const ( + MailStatusPendingAudit int8 = 0 // 待审核 + MailStatusQueued int8 = 1 // 排队中 + MailStatusSending int8 = 2 // 发送中 + MailStatusSuccess int8 = 3 // 成功 + MailStatusFailed int8 = 4 // 失败 + MailStatusAbandoned int8 = 5 // 放弃 + MailStatusRejected int8 = 6 // 驳回 +) + +// --- MailQuota.QuotaType 配额类型 --- +const ( + QuotaTypeTotal int8 = 1 // 总量配额 + QuotaTypeCycle int8 = 2 // 周期配额,需要配合 CycleUnit +) + +// --- MailQuota.CycleUnit 周期单位,仅在 QuotaType=2 时生效 --- +const ( + CycleUnitDay = "day" + CycleUnitWeek = "week" + CycleUnitMonth = "month" + CycleUnitYear = "year" +) + +// --- Channel.Strategy 通道下多发信账号的挑选策略 --- +const ( + StrategyRoundRobin = "round_robin" // 轮询(默认) + StrategyWeight = "weight" // 按 Weight 加权随机 + StrategyLeastUsed = "least_used" // 今日发送数最少优先 +) + +// --- AuditRule.Action 规则命中后的动作 --- +const ( + RuleActionApprove int8 = 1 // 自动通过 + RuleActionReject int8 = 2 // 自动驳回 + RuleActionToManual int8 = 3 // 转人工审核 +) + +// --- AuditRule.RuleType 规则匹配方式 --- +const ( + RuleTypeKeyword = "keyword" // 关键词包含 + RuleTypeRegex = "regex" // 正则匹配 + RuleTypeDomain = "domain" // 域名匹配 +) + +// --- AuditRule.Target 规则匹配目标 --- +const ( + RuleTargetSubject = "subject" + RuleTargetBody = "body" + RuleTargetTo = "to" + RuleTargetFrom = "from" +) + +// --- MailAudit.AuditType 审核来源 --- +const ( + AuditTypeAuto int8 = 1 // 规则自动 + AuditTypeManual int8 = 2 // 人工 +) + +// --- MailAudit.Action 审核动作 --- +const ( + AuditActionApprove int8 = 1 // 通过 + AuditActionReject int8 = 2 // 驳回 +) + +// --- SendMailReq.ContentType 邮件正文类型 --- +const ( + ContentTypeText = "text/plain" + ContentTypeHTML = "text/html" +) + +// --- SendMailResp.Status 发送结果状态 --- +const ( + SendStatusQueued = "queued" // 已入队,待发送 + SendStatusPendingAudit = "pending_audit" // 待人工审核 + SendStatusRejected = "rejected" // 规则驳回 +) diff --git a/mail.go b/mail.go index e05cfdb..44f8131 100644 --- a/mail.go +++ b/mail.go @@ -5,12 +5,22 @@ import ( "fmt" ) -// SendMail sends an email. Requires an AppClient (X-App-Key/X-App-Secret auth). +// SendMail 发送邮件。必须使用 NewAppClient 构造的客户端(X-App-Key/X-App-Secret 认证)。 +// +// req.Channel 可为空: +// 1. 若指定且存在已启用通道,则校验该通道是否在账号 AllowedChannels 内; +// 2. 若未指定,按 Account.DefaultChannelID → Account.AllowedChannels 顺序自动挑选。 +// +// 返回的 Status 见 SendStatusQueued / SendStatusPendingAudit / SendStatusRejected。 +// +// POST /api/v1/mail/send AppAuth func (c *Client) SendMail(ctx context.Context, req SendMailReq) (*SendMailResp, error) { return post[*SendMailResp](c, ctx, "/api/v1/mail/send", req) } -// ListMailLogs lists mail log records with filters (ServiceAuth). +// ListMailLogs 分页查询邮件日志,支持按用户、账号、状态、时间范围、收件人、关键字过滤。 +// +// GET /api/v1/mail-logs?page=&page_size=&user_id=&account_id=&status=&start_date=&end_date=&to=&keyword= ServiceAuth func (c *Client) ListMailLogs(ctx context.Context, q MailLogListQuery) (*PaginationResult[MailLog], error) { params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{ "user_id": q.UserID, @@ -24,10 +34,16 @@ func (c *Client) ListMailLogs(ctx context.Context, q MailLogListQuery) (*Paginat return get[*PaginationResult[MailLog]](c, ctx, "/api/v1/mail-logs", buildQuery(params)) } +// GetMailLog 获取邮件日志详情(包含完整正文 Body)。 +// +// GET /api/v1/mail-logs/{id} ServiceAuth func (c *Client) GetMailLog(ctx context.Context, id uint) (*MailLogDetail, error) { return get[*MailLogDetail](c, ctx, fmt.Sprintf("/api/v1/mail-logs/%d", id), nil) } +// GetMailStats 按状态聚合的邮件计数,用于概览面板。 +// +// GET /api/v1/mail-logs/stats ServiceAuth func (c *Client) GetMailStats(ctx context.Context) ([]MailStatItem, error) { return get[[]MailStatItem](c, ctx, "/api/v1/mail-logs/stats", nil) } diff --git a/query.go b/query.go index 1bd9cee..82f199d 100644 --- a/query.go +++ b/query.go @@ -5,6 +5,15 @@ import ( "net/url" ) +// buildQuery 将 map[string]interface{} 转成 url.Values。 +// +// 规则: +// - nil / 空字符串 / 值为 0 的 int/uint 会被忽略(视作未传) +// - int8 不做零值过滤(因为 0 可能是合法的状态值,如 Status=0 禁用) +// - 指针类型 (*int / *int8 / *uint) 为 nil 时忽略,否则取值写入 +// - 其它类型走 fmt.Sprintf("%v") 兜底 +// +// 这种设计允许调用方使用零值(空字符串 / 0)来表达"该过滤项不设置"。 func buildQuery(params map[string]interface{}) url.Values { q := url.Values{} for k, v := range params { @@ -45,6 +54,8 @@ func buildQuery(params map[string]interface{}) url.Values { return q } +// paginationParams 将分页结构体转换为通用 map 形式的查询参数, +// 供 mergeParams 与 buildQuery 继续拼装。Page / PageSize <=0 时会被忽略。 func paginationParams(p PaginationQuery) map[string]interface{} { m := map[string]interface{}{} if p.Page > 0 { @@ -56,6 +67,8 @@ func paginationParams(p PaginationQuery) map[string]interface{} { return m } +// mergeParams 合并多个查询参数 map,后者覆盖前者。 +// 用于把分页参数与业务过滤参数拼在一起传给 buildQuery。 func mergeParams(maps ...map[string]interface{}) map[string]interface{} { result := map[string]interface{}{} for _, m := range maps { diff --git a/queue.go b/queue.go index 638e2d1..359c3f8 100644 --- a/queue.go +++ b/queue.go @@ -5,10 +5,16 @@ import ( "fmt" ) +// GetQueueStatus 查询各通道当前排队长度与延迟重试队列长度。 +// +// GET /api/v1/queue/status ServiceAuth func (c *Client) GetQueueStatus(ctx context.Context) (*QueueStatusData, error) { return get[*QueueStatusData](c, ctx, "/api/v1/queue/status", nil) } +// ListQueuePending 分页查询排队中/发送中的邮件。 +// +// GET /api/v1/queue/pending?page=&page_size=&channel_id=&user_id=&account_id= ServiceAuth func (c *Client) ListQueuePending(ctx context.Context, q QueuePendingQuery) (*PaginationResult[MailLog], error) { params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{ "channel_id": q.ChannelID, @@ -18,11 +24,17 @@ func (c *Client) ListQueuePending(ctx context.Context, q QueuePendingQuery) (*Pa return get[*PaginationResult[MailLog]](c, ctx, "/api/v1/queue/pending", buildQuery(params)) } +// CancelQueueItem 取消一封队列中的邮件,邮件状态会被标记为放弃并退还配额。 +// +// POST /api/v1/queue/{mailLogID}/cancel ServiceAuth func (c *Client) CancelQueueItem(ctx context.Context, mailLogID uint) error { _, err := post[any](c, ctx, fmt.Sprintf("/api/v1/queue/%d/cancel", mailLogID), nil) return err } +// RetryQueueItem 重新入队一封失败的邮件。 +// +// POST /api/v1/queue/{mailLogID}/retry ServiceAuth func (c *Client) RetryQueueItem(ctx context.Context, mailLogID uint) error { _, err := post[any](c, ctx, fmt.Sprintf("/api/v1/queue/%d/retry", mailLogID), nil) return err diff --git a/quota.go b/quota.go index 4767a73..f3f79fe 100644 --- a/quota.go +++ b/quota.go @@ -5,10 +5,17 @@ import ( "fmt" ) +// CreateQuota 创建配额。QuotaType=1 为总量配额(需填 ExpireAt), +// QuotaType=2 为周期配额(需填 CycleUnit、CycleResetAt)。 +// +// POST /api/v1/quotas ServiceAuth func (c *Client) CreateQuota(ctx context.Context, req CreateQuotaReq) (*MailQuota, error) { return post[*MailQuota](c, ctx, "/api/v1/quotas", req) } +// ListQuotas 分页查询配额,支持按账号、用户、状态过滤。 +// +// GET /api/v1/quotas?page=&page_size=&account_id=&user_id=&status= ServiceAuth func (c *Client) ListQuotas(ctx context.Context, q QuotaListQuery) (*PaginationResult[MailQuota], error) { params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{ "account_id": q.AccountID, @@ -18,14 +25,23 @@ func (c *Client) ListQuotas(ctx context.Context, q QuotaListQuery) (*PaginationR return get[*PaginationResult[MailQuota]](c, ctx, "/api/v1/quotas", buildQuery(params)) } +// GetQuotaSummary 获取指定账号的配额汇总(总量/已用/剩余)。 +// +// GET /api/v1/quotas/summary/{accountID} ServiceAuth func (c *Client) GetQuotaSummary(ctx context.Context, accountID uint) (*QuotaSummary, error) { return get[*QuotaSummary](c, ctx, fmt.Sprintf("/api/v1/quotas/summary/%d", accountID), nil) } +// UpdateQuota 局部更新配额。可更新 Total、Status、ExpireAt、CycleUnit、CycleResetAt。 +// +// PUT /api/v1/quotas/{id} ServiceAuth func (c *Client) UpdateQuota(ctx context.Context, id uint, req UpdateQuotaReq) (*MailQuota, error) { return put[*MailQuota](c, ctx, fmt.Sprintf("/api/v1/quotas/%d", id), req) } +// DeleteQuota 删除配额(软删除)。 +// +// DELETE /api/v1/quotas/{id} ServiceAuth func (c *Client) DeleteQuota(ctx context.Context, id uint) error { _, err := del[any](c, ctx, fmt.Sprintf("/api/v1/quotas/%d", id)) return err diff --git a/sender.go b/sender.go index d0df56b..7754aa3 100644 --- a/sender.go +++ b/sender.go @@ -5,10 +5,17 @@ import ( "fmt" ) +// CreateSender 在指定通道下创建 SMTP 发信账号。 +// SmtpPassword 会加密存储,不会在后续接口中返回。 +// +// POST /api/v1/channels/{channelID}/senders ServiceAuth func (c *Client) CreateSender(ctx context.Context, channelID uint, req CreateSenderReq) (*SenderAccount, error) { return post[*SenderAccount](c, ctx, fmt.Sprintf("/api/v1/channels/%d/senders", channelID), req) } +// ListSendersByChannel 分页查询指定通道下的发信账号。 +// +// GET /api/v1/channels/{channelID}/senders?page=&page_size=&status=&keyword= ServiceAuth func (c *Client) ListSendersByChannel(ctx context.Context, channelID uint, q SenderListQuery) (*PaginationResult[SenderAccount], error) { params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{ "status": q.Status, @@ -17,10 +24,16 @@ func (c *Client) ListSendersByChannel(ctx context.Context, channelID uint, q Sen return get[*PaginationResult[SenderAccount]](c, ctx, fmt.Sprintf("/api/v1/channels/%d/senders", channelID), buildQuery(params)) } +// UpdateSender 局部更新发信账号。 +// +// PUT /api/v1/senders/{id} ServiceAuth func (c *Client) UpdateSender(ctx context.Context, id uint, req UpdateSenderReq) (*SenderAccount, error) { return put[*SenderAccount](c, ctx, fmt.Sprintf("/api/v1/senders/%d", id), req) } +// DeleteSender 删除发信账号(软删除)。 +// +// DELETE /api/v1/senders/{id} ServiceAuth func (c *Client) DeleteSender(ctx context.Context, id uint) error { _, err := del[any](c, ctx, fmt.Sprintf("/api/v1/senders/%d", id)) return err diff --git a/signature.go b/signature.go index aeeea8e..04d1c11 100644 --- a/signature.go +++ b/signature.go @@ -5,10 +5,16 @@ import ( "fmt" ) +// CreateSignature 创建签名,新建签名默认 Status=0(待审核),需要调用 AuditSignature 审核后方可使用。 +// +// POST /api/v1/signatures ServiceAuth func (c *Client) CreateSignature(ctx context.Context, req CreateSignatureReq) (*Signature, error) { return post[*Signature](c, ctx, "/api/v1/signatures", req) } +// ListSignatures 分页查询签名。 +// +// GET /api/v1/signatures?page=&page_size=&user_id=&account_id=&status=&keyword= ServiceAuth func (c *Client) ListSignatures(ctx context.Context, q SignatureListQuery) (*PaginationResult[Signature], error) { params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{ "account_id": q.AccountID, @@ -19,19 +25,32 @@ func (c *Client) ListSignatures(ctx context.Context, q SignatureListQuery) (*Pag return get[*PaginationResult[Signature]](c, ctx, "/api/v1/signatures", buildQuery(params)) } +// GetSignature 获取签名详情。 +// +// GET /api/v1/signatures/{id} ServiceAuth func (c *Client) GetSignature(ctx context.Context, id uint) (*Signature, error) { return get[*Signature](c, ctx, fmt.Sprintf("/api/v1/signatures/%d", id), nil) } +// UpdateSignature 局部更新签名。更新后若已通过,建议重新提交审核。 +// +// PUT /api/v1/signatures/{id} ServiceAuth func (c *Client) UpdateSignature(ctx context.Context, id uint, req UpdateSignatureReq) (*Signature, error) { return put[*Signature](c, ctx, fmt.Sprintf("/api/v1/signatures/%d", id), req) } +// DeleteSignature 删除签名(软删除)。 +// +// DELETE /api/v1/signatures/{id} ServiceAuth func (c *Client) DeleteSignature(ctx context.Context, id uint) error { _, err := del[any](c, ctx, fmt.Sprintf("/api/v1/signatures/%d", id)) return err } +// AuditSignature 审核签名。Status 使用 SignatureStatusApproved/SignatureStatusRejected 常量。 +// 驳回时建议在 req.RejectReason 中填写原因。 +// +// POST /api/v1/signatures/{id}/audit ServiceAuth func (c *Client) AuditSignature(ctx context.Context, id uint, req AuditSignatureReq) error { _, err := post[any](c, ctx, fmt.Sprintf("/api/v1/signatures/%d/audit", id), req) return err diff --git a/types.go b/types.go index 9ef7059..08ec7e6 100644 --- a/types.go +++ b/types.go @@ -2,7 +2,14 @@ package emailcli import "time" -// PaginationResult is the generic paginated response wrapper. +// PaginationResult 是分页接口的统一返回结构。 +// +// { +// "list": [...], // 当前页数据 +// "total": 123, // 总记录数 +// "page": 1, +// "page_size": 20 +// } type PaginationResult[T any] struct { List []T `json:"list"` Total int64 `json:"total"` @@ -10,13 +17,17 @@ type PaginationResult[T any] struct { PageSize int `json:"page_size"` } +// PaginationQuery 是所有列表接口的分页参数基类, +// 业务 Query 结构体可通过嵌入来复用。 +// - Page: 页码,从 1 开始;省略时后端默认 1 +// - PageSize: 每页大小,省略时后端默认 20,最大 100 type PaginationQuery struct { Page int `json:"page,omitempty"` PageSize int `json:"page_size,omitempty"` } -// --- GormModel fields --- - +// GormModel 对应后端 gorm.Model。注意后端该部分没有自定义 json tag, +// 因此字段使用 PascalCase 序列化(与数据库模型保持一致)。 type GormModel struct { ID uint `json:"ID"` CreatedAt time.Time `json:"CreatedAt"` @@ -24,84 +35,104 @@ type GormModel struct { DeletedAt *time.Time `json:"DeletedAt"` } -// --- Account --- +// ============================================================ +// Account 邮件账号 +// ============================================================ +// Account 表示一个邮件发送账号。 +// AppKey 用于发件认证;AppSecret 只在创建或重置时返回一次, +// 入库时以 bcrypt 保存,SDK 不会回传。 type Account struct { GormModel - UserID int `json:"user_id"` - Name string `json:"name"` - AppKey string `json:"app_key"` - Status int8 `json:"status"` - AuditMode int8 `json:"audit_mode"` - RateLimit int `json:"rate_limit"` - AllowedChannels string `json:"allowed_channels"` - DefaultSignatureID *uint `json:"default_signature_id"` - Remark string `json:"remark"` + UserID int `json:"user_id"` // 关联主平台用户 ID + Name string `json:"name"` // 账号名称 + AppKey string `json:"app_key"` // 发件公钥 + Status int8 `json:"status"` // 账号状态: 0=禁用 1=启用 + AuditMode int8 `json:"audit_mode"` // 审核模式: 0=免审核 1=自动 2=人工 + RateLimit int `json:"rate_limit"` // 频率限制(封/分钟),0 表示不限 + DefaultChannelID *uint `json:"default_channel_id"` // 默认发件通道 ID + AllowedChannels string `json:"allowed_channels"` // 允许的通道 ID 列表 JSON 字符串,如 "[1,2]";空串=不限 + DefaultSignatureID *uint `json:"default_signature_id"` // 默认签名 ID + Remark string `json:"remark"` // 备注 } +// CreateAccountReq 是创建账号的请求体。 type CreateAccountReq struct { - UserID int `json:"user_id"` - Name string `json:"name"` - AuditMode *int8 `json:"audit_mode,omitempty"` - RateLimit *int `json:"rate_limit,omitempty"` - Remark string `json:"remark,omitempty"` + UserID int `json:"user_id"` // 必填,关联用户 ID + Name string `json:"name"` // 必填,账号名称,≤100 + AuditMode *int8 `json:"audit_mode,omitempty"` // 可选,默认 0 免审核 + RateLimit *int `json:"rate_limit,omitempty"` // 可选,0=不限 + DefaultChannelID *uint `json:"default_channel_id,omitempty"` // 可选,默认发件通道 ID + AllowedChannels string `json:"allowed_channels,omitempty"` // 可选,允许通道 ID 列表的 JSON 字符串 + Remark string `json:"remark,omitempty"` // 可选,备注 } +// CreateAccountResp 是创建账号后的返回,AppSecret 仅此一次明文展示。 type CreateAccountResp struct { ID uint `json:"id"` AppKey string `json:"app_key"` - AppSecret string `json:"app_secret"` + AppSecret string `json:"app_secret"` // 明文密钥,务必妥善保存 Name string `json:"name"` } +// UpdateAccountReq 所有字段均为可选;只对传入字段做局部更新。 type UpdateAccountReq struct { Name *string `json:"name,omitempty"` - Status *int8 `json:"status,omitempty"` - AuditMode *int8 `json:"audit_mode,omitempty"` - RateLimit *int `json:"rate_limit,omitempty"` - AllowedChannels *string `json:"allowed_channels,omitempty"` - DefaultSignatureID *uint `json:"default_signature_id,omitempty"` + Status *int8 `json:"status,omitempty"` // 0=禁用 1=启用 + AuditMode *int8 `json:"audit_mode,omitempty"` // 0=免审核 1=自动 2=人工 + RateLimit *int `json:"rate_limit,omitempty"` // 封/分钟,0=不限 + DefaultChannelID *uint `json:"default_channel_id,omitempty"` // 默认发件通道 ID + AllowedChannels *string `json:"allowed_channels,omitempty"` // 允许通道 ID 列表 JSON + DefaultSignatureID *uint `json:"default_signature_id,omitempty"` // 默认签名 ID Remark *string `json:"remark,omitempty"` } +// AccountListQuery 是账号列表过滤参数。 type AccountListQuery struct { PaginationQuery - UserID *int `json:"user_id,omitempty"` - Status *int8 `json:"status,omitempty"` - Keyword string `json:"keyword,omitempty"` + UserID *int `json:"user_id,omitempty"` // 按用户过滤 + Status *int8 `json:"status,omitempty"` // 0=禁用 1=启用 + Keyword string `json:"keyword,omitempty"` // 模糊匹配 name / remark } +// ResetSecretResp 是重置密钥后的返回。 type ResetSecretResp struct { - AppSecret string `json:"app_secret"` + AppSecret string `json:"app_secret"` // 新生成的明文密钥 } -// --- Signature --- +// ============================================================ +// Signature 签名 +// ============================================================ +// Signature 表示一个邮件签名。 +// EnglishName 会作为 From 地址的 local 部分组装发件人(签名@域名)。 type Signature struct { GormModel UserID int `json:"user_id"` - AccountID *uint `json:"account_id"` - Title string `json:"title"` - EnglishName string `json:"english_name"` - Content string `json:"content"` - Applicant string `json:"applicant"` - ApplicantInfo string `json:"applicant_info"` - Status int8 `json:"status"` + AccountID *uint `json:"account_id"` // 为空表示用户全局签名 + Title string `json:"title"` // 中文抬头 + EnglishName string `json:"english_name"` // 英文标识(用于拼 From 地址) + Content string `json:"content"` // HTML 签名正文 + Applicant string `json:"applicant"` // 申请人 + ApplicantInfo string `json:"applicant_info"` // 申请说明 + Status int8 `json:"status"` // 0=待审核 1=已通过 2=已驳回 RejectReason string `json:"reject_reason"` Auditor string `json:"auditor"` AuditedAt *time.Time `json:"audited_at"` } +// CreateSignatureReq 是创建签名的请求体。新建签名默认 Status=0(待审核)。 type CreateSignatureReq struct { - UserID int `json:"user_id"` - AccountID *uint `json:"account_id,omitempty"` - Title string `json:"title"` - EnglishName string `json:"english_name"` - Content string `json:"content,omitempty"` - Applicant string `json:"applicant,omitempty"` - ApplicantInfo string `json:"applicant_info,omitempty"` + UserID int `json:"user_id"` // 必填 + AccountID *uint `json:"account_id,omitempty"` // 可选,绑定账号 + Title string `json:"title"` // 必填,中文抬头 + EnglishName string `json:"english_name"` // 必填,英文标识 + Content string `json:"content,omitempty"` // HTML 签名内容 + Applicant string `json:"applicant,omitempty"` // 申请人 + ApplicantInfo string `json:"applicant_info,omitempty"` // 申请说明 } +// UpdateSignatureReq 局部更新签名字段。 type UpdateSignatureReq struct { Title *string `json:"title,omitempty"` EnglishName *string `json:"english_name,omitempty"` @@ -110,64 +141,79 @@ type UpdateSignatureReq struct { ApplicantInfo *string `json:"applicant_info,omitempty"` } +// AuditSignatureReq 是审核签名的请求体。 type AuditSignatureReq struct { - Status int8 `json:"status"` - RejectReason string `json:"reject_reason,omitempty"` + Status int8 `json:"status"` // 1=通过 2=驳回 + RejectReason string `json:"reject_reason,omitempty"` // 驳回时填写 } +// SignatureListQuery 是签名列表过滤参数。 type SignatureListQuery struct { PaginationQuery AccountID *uint `json:"account_id,omitempty"` - Status *int8 `json:"status,omitempty"` + Status *int8 `json:"status,omitempty"` // 0=待审核 1=已通过 2=已驳回 UserID *int `json:"user_id,omitempty"` - Keyword string `json:"keyword,omitempty"` + Keyword string `json:"keyword,omitempty"` // 模糊匹配 title / english_name } -// --- Mail --- +// ============================================================ +// Mail 发送邮件 +// ============================================================ +// SendMailReq 是 POST /api/v1/mail/send 的请求体。 +// +// Channel 为空时,后端按以下顺序自动选择: +// 1. Account.DefaultChannelID 对应的已启用通道; +// 2. Account.AllowedChannels 列表中的首个已启用通道。 type SendMailReq struct { - To []string `json:"to"` - Cc []string `json:"cc,omitempty"` - Bcc []string `json:"bcc,omitempty"` - Subject string `json:"subject"` - Body string `json:"body"` - ContentType string `json:"content_type,omitempty"` - Channel string `json:"channel"` - SignatureID *uint `json:"signature_id,omitempty"` - SignatureTitle string `json:"signature_title,omitempty"` - Attachments []AttachmentItem `json:"attachments,omitempty"` + To []string `json:"to"` // 必填,收件人列表(至少 1 个) + Cc []string `json:"cc,omitempty"` // 抄送 + Bcc []string `json:"bcc,omitempty"` // 密送 + Subject string `json:"subject"` // 必填,主题 + Body string `json:"body"` // 必填,正文 + ContentType string `json:"content_type,omitempty"` // text/plain 或 text/html,默认 text/html + Channel string `json:"channel,omitempty"` // 通道 code;留空走默认/允许通道 + SignatureID *uint `json:"signature_id,omitempty"` // 指定签名 ID(优先级低于 title) + SignatureTitle string `json:"signature_title,omitempty"` // 按 title + user_id 查找已审核签名 + Attachments []AttachmentItem `json:"attachments,omitempty"` // 附件 } +// AttachmentItem 附件项,Content 为 base64 编码字符串。 type AttachmentItem struct { - Filename string `json:"filename"` - Content string `json:"content"` + Filename string `json:"filename"` // 文件名(含扩展名) + Content string `json:"content"` // base64 编码的文件内容 } +// SendMailResp 是发送邮件的返回结果。 type SendMailResp struct { - MailLogID uint `json:"mail_log_id"` - Status string `json:"status"` + MailLogID uint `json:"mail_log_id"` // 创建的邮件日志 ID + Status string `json:"status"` // queued / pending_audit / rejected } -// --- Mail Log --- +// ============================================================ +// MailLog 邮件日志 +// ============================================================ +// MailLog 邮件日志记录。 +// Status 枚举:0=待审核 1=排队中 2=发送中 3=成功 4=失败 5=放弃 6=驳回 type MailLog struct { GormModel UserID int `json:"user_id"` AccountID uint `json:"account_id"` - QuotaID *uint `json:"quota_id"` - ChannelID *uint `json:"channel_id"` - SenderAccountID *uint `json:"sender_account_id"` + QuotaID *uint `json:"quota_id"` // 扣减的配额记录 ID + ChannelID *uint `json:"channel_id"` // 使用的发件通道 ID + SenderAccountID *uint `json:"sender_account_id"` // 实际使用的发信账号 ID SignatureID *uint `json:"signature_id"` - MessageID string `json:"message_id"` - FromAddress string `json:"from_address"` - ToAddresses string `json:"to_addresses"` - CcAddresses string `json:"cc_addresses"` - BccAddresses string `json:"bcc_addresses"` + MessageID string `json:"message_id"` // SMTP 返回的 Message-ID + FromAddress string `json:"from_address"` // 实际 From 地址 + ToAddresses string `json:"to_addresses"` // JSON 字符串数组 + CcAddresses string `json:"cc_addresses"` // JSON 字符串数组 + BccAddresses string `json:"bcc_addresses"` // JSON 字符串数组 Subject string `json:"subject"` ContentType string `json:"content_type"` HasAttachment bool `json:"has_attachment"` - SourceIP string `json:"source_ip"` - SourceType string `json:"source_type"` + SourceIP string `json:"source_ip"` // 请求来源 IP + SourceType string `json:"source_type"` // http / smtp Status int8 `json:"status"` RetryCount int `json:"retry_count"` MaxRetry int `json:"max_retry"` @@ -175,51 +221,61 @@ type MailLog struct { SentAt *time.Time `json:"sent_at"` } +// MailLogListQuery 是邮件日志列表过滤参数。 type MailLogListQuery struct { PaginationQuery UserID *int `json:"user_id,omitempty"` AccountID *uint `json:"account_id,omitempty"` - Status *int8 `json:"status,omitempty"` - StartDate string `json:"start_date,omitempty"` - EndDate string `json:"end_date,omitempty"` - To string `json:"to,omitempty"` - Keyword string `json:"keyword,omitempty"` + Status *int8 `json:"status,omitempty"` // 见 MailLog.Status 枚举 + StartDate string `json:"start_date,omitempty"` // 格式 YYYY-MM-DD + EndDate string `json:"end_date,omitempty"` // 格式 YYYY-MM-DD + To string `json:"to,omitempty"` // 精确匹配收件人 + Keyword string `json:"keyword,omitempty"` // 模糊匹配主题/收件人 } +// MailLogDetail 邮件日志详情,包含完整正文。 type MailLogDetail struct { Log MailLog `json:"log"` - Body string `json:"body"` + Body string `json:"body"` // 邮件正文 } +// MailStatItem 按状态分组的邮件计数,用于概览统计。 type MailStatItem struct { Status int8 `json:"status"` Count int64 `json:"count"` } -// --- Quota --- +// ============================================================ +// Quota 配额 +// ============================================================ +// MailQuota 配额记录。 +// - QuotaType=1 总量:Total 为生命周期内的总配额 +// - QuotaType=2 周期:每个 CycleUnit 周期重置 Used type MailQuota struct { GormModel UserID int `json:"user_id"` AccountID uint `json:"account_id"` - QuotaType int8 `json:"quota_type"` + QuotaType int8 `json:"quota_type"` // 1=总量 2=周期 Total int `json:"total"` Used int `json:"used"` - ExpireAt *time.Time `json:"expire_at"` - CycleUnit string `json:"cycle_unit"` - CycleResetAt *time.Time `json:"cycle_reset_at"` - Status int8 `json:"status"` + ExpireAt *time.Time `json:"expire_at"` // 到期时间,仅总量配额有意义 + CycleUnit string `json:"cycle_unit"` // day/week/month/year + CycleResetAt *time.Time `json:"cycle_reset_at"` // 周期起始时间 + Status int8 `json:"status"` // 0=禁用 1=启用 } +// CreateQuotaReq 创建配额。 type CreateQuotaReq struct { - AccountID uint `json:"account_id"` - QuotaType int8 `json:"quota_type"` - Total int `json:"total"` - ExpireAt string `json:"expire_at,omitempty"` - CycleUnit string `json:"cycle_unit,omitempty"` - CycleResetAt string `json:"cycle_reset_at,omitempty"` + AccountID uint `json:"account_id"` // 必填 + QuotaType int8 `json:"quota_type"` // 必填,1=总量 2=周期 + Total int `json:"total"` // 必填,配额上限 + ExpireAt string `json:"expire_at,omitempty"` // YYYY-MM-DD HH:mm:ss + CycleUnit string `json:"cycle_unit,omitempty"` // day/week/month/year + CycleResetAt string `json:"cycle_reset_at,omitempty"` // 周期起点 } +// UpdateQuotaReq 局部更新配额。 type UpdateQuotaReq struct { Total *int `json:"total,omitempty"` Status *int8 `json:"status,omitempty"` @@ -228,6 +284,7 @@ type UpdateQuotaReq struct { CycleResetAt *string `json:"cycle_reset_at,omitempty"` } +// QuotaListQuery 配额列表过滤参数。 type QuotaListQuery struct { PaginationQuery AccountID *uint `json:"account_id,omitempty"` @@ -235,32 +292,38 @@ type QuotaListQuery struct { Status *int8 `json:"status,omitempty"` } +// QuotaSummary 指定账号的配额汇总信息。 type QuotaSummary struct { AccountID uint `json:"account_id"` - TotalQuota int `json:"total_quota"` - TotalUsed int `json:"total_used"` - TotalRemaining int `json:"total_remaining"` - Details []MailQuota `json:"details"` + TotalQuota int `json:"total_quota"` // 所有配额累加总量 + TotalUsed int `json:"total_used"` // 已用 + TotalRemaining int `json:"total_remaining"` // 剩余 + Details []MailQuota `json:"details"` // 每条配额明细 } -// --- Channel --- +// ============================================================ +// Channel 通道 +// ============================================================ +// Channel 发件通道,一个通道下可挂多个 SenderAccount,由 Strategy 决定分发逻辑。 type Channel struct { GormModel Name string `json:"name"` - Code string `json:"code"` + Code string `json:"code"` // 唯一标识,发件请求中 channel 字段填这个 Description string `json:"description"` - Strategy string `json:"strategy"` - Status int8 `json:"status"` + Strategy string `json:"strategy"` // round_robin / weight / least_used + Status int8 `json:"status"` // 0=禁用 1=启用 } +// CreateChannelReq 创建通道。 type CreateChannelReq struct { - Name string `json:"name"` - Code string `json:"code"` + Name string `json:"name"` // 必填 + Code string `json:"code"` // 必填,唯一 Description string `json:"description,omitempty"` - Strategy string `json:"strategy,omitempty"` + Strategy string `json:"strategy,omitempty"` // 默认 round_robin } +// UpdateChannelReq 局部更新通道。 type UpdateChannelReq struct { Name *string `json:"name,omitempty"` Description *string `json:"description,omitempty"` @@ -268,14 +331,18 @@ type UpdateChannelReq struct { Status *int8 `json:"status,omitempty"` } +// ChannelListQuery 通道列表过滤参数。 type ChannelListQuery struct { PaginationQuery Status *int8 `json:"status,omitempty"` - Keyword string `json:"keyword,omitempty"` + Keyword string `json:"keyword,omitempty"` // 模糊匹配 name / code } -// --- Sender Account --- +// ============================================================ +// SenderAccount 发信账号(SMTP) +// ============================================================ +// SenderAccount 挂在某个 Channel 下的 SMTP 发信账号。 type SenderAccount struct { GormModel ChannelID uint `json:"channel_id"` @@ -286,27 +353,29 @@ type SenderAccount struct { SmtpSSL bool `json:"smtp_ssl"` FromName string `json:"from_name"` FromAddress string `json:"from_address"` - DailyLimit int `json:"daily_limit"` - DailySent int `json:"daily_sent"` - Weight int `json:"weight"` - Status int8 `json:"status"` + DailyLimit int `json:"daily_limit"` // 每日发送上限,0=不限 + DailySent int `json:"daily_sent"` // 当日已发送数 + Weight int `json:"weight"` // Strategy=weight 时使用 + Status int8 `json:"status"` // 0=禁用 1=启用 LastCheckAt *time.Time `json:"last_check_at"` LastCheckResult string `json:"last_check_result"` } +// CreateSenderReq 创建发信账号。Password 只存密文,不会回显。 type CreateSenderReq struct { - Name string `json:"name"` - SmtpHost string `json:"smtp_host"` - SmtpPort int `json:"smtp_port"` - SmtpUser string `json:"smtp_user"` - SmtpPassword string `json:"smtp_password"` + Name string `json:"name"` // 必填 + SmtpHost string `json:"smtp_host"` // 必填 + SmtpPort int `json:"smtp_port"` // 必填 + SmtpUser string `json:"smtp_user"` // 必填 + SmtpPassword string `json:"smtp_password"` // 必填 SmtpSSL *bool `json:"smtp_ssl,omitempty"` FromName string `json:"from_name,omitempty"` - FromAddress string `json:"from_address"` + FromAddress string `json:"from_address"` // 必填 DailyLimit *int `json:"daily_limit,omitempty"` Weight *int `json:"weight,omitempty"` } +// UpdateSenderReq 局部更新发信账号。 type UpdateSenderReq struct { Name *string `json:"name,omitempty"` SmtpHost *string `json:"smtp_host,omitempty"` @@ -321,91 +390,108 @@ type UpdateSenderReq struct { Status *int8 `json:"status,omitempty"` } +// SenderListQuery 发信账号列表过滤参数。 type SenderListQuery struct { PaginationQuery Status *int8 `json:"status,omitempty"` Keyword string `json:"keyword,omitempty"` } -// --- Audit --- +// ============================================================ +// Audit 审核 +// ============================================================ +// MailAudit 邮件审核记录。 type MailAudit struct { GormModel MailLogID uint `json:"mail_log_id"` UserID int `json:"user_id"` AccountID uint `json:"account_id"` - AuditType int8 `json:"audit_type"` - Action int8 `json:"action"` + AuditType int8 `json:"audit_type"` // 1=自动 2=人工 + Action int8 `json:"action"` // 1=通过 2=驳回 RejectReason string `json:"reject_reason"` - HitRules string `json:"hit_rules"` + HitRules string `json:"hit_rules"` // 命中规则列表 JSON Auditor string `json:"auditor"` AuditedAt time.Time `json:"audited_at"` } +// AuditPendingQuery 待审核列表过滤参数。 type AuditPendingQuery struct { PaginationQuery UserID *int `json:"user_id,omitempty"` AccountID *uint `json:"account_id,omitempty"` - Keyword string `json:"keyword,omitempty"` + Keyword string `json:"keyword,omitempty"` // 模糊匹配主题/收件人 } +// AuditLogQuery 审核记录列表过滤参数。 type AuditLogQuery struct { PaginationQuery - AuditType *int8 `json:"audit_type,omitempty"` - Action *int8 `json:"action,omitempty"` + AuditType *int8 `json:"audit_type,omitempty"` // 1=自动 2=人工 + Action *int8 `json:"action,omitempty"` // 1=通过 2=驳回 UserID *int `json:"user_id,omitempty"` - StartDate string `json:"start_date,omitempty"` - EndDate string `json:"end_date,omitempty"` + StartDate string `json:"start_date,omitempty"` // YYYY-MM-DD + EndDate string `json:"end_date,omitempty"` // YYYY-MM-DD } +// AuditRejectReq 驳回审核请求体。 type AuditRejectReq struct { - RejectReason string `json:"reject_reason"` + RejectReason string `json:"reject_reason"` // 建议填写驳回原因 } +// BatchAuditApproveReq 批量通过请求体。 type BatchAuditApproveReq struct { MailLogIDs []uint `json:"mail_log_ids"` } +// BatchAuditRejectReq 批量驳回请求体。 type BatchAuditRejectReq struct { MailLogIDs []uint `json:"mail_log_ids"` RejectReason string `json:"reject_reason"` } +// AuditStats 审核概览统计(用于仪表盘)。 type AuditStats struct { - PendingCount int64 `json:"pending_count"` - TodayDetails []AuditDetailCount `json:"today_details"` + PendingCount int64 `json:"pending_count"` // 待审核数量 + TodayDetails []AuditDetailCount `json:"today_details"` // 今日按 type/action 分组 } +// AuditDetailCount 按 AuditType+Action 分组的计数。 type AuditDetailCount struct { AuditType int8 `json:"audit_type"` Action int8 `json:"action"` Count int64 `json:"count"` } -// --- Audit Rule --- +// ============================================================ +// AuditRule 审核规则 +// ============================================================ +// AuditRule 审核规则。自动审核会按 Priority 从高到低依次评估, +// 命中即返回 Action。 type AuditRule struct { GormModel Name string `json:"name"` - RuleType string `json:"rule_type"` - Target string `json:"target"` - Condition string `json:"condition"` - Action int8 `json:"action"` - Priority int `json:"priority"` - Status int8 `json:"status"` + RuleType string `json:"rule_type"` // keyword/regex/domain + Target string `json:"target"` // subject/body/to/from + Condition string `json:"condition"` // 关键词或正则 + Action int8 `json:"action"` // 1=自动通过 2=自动驳回 3=转人工 + Priority int `json:"priority"` // 数字越大优先级越高 + Status int8 `json:"status"` // 0=禁用 1=启用 Remark string `json:"remark"` } +// CreateAuditRuleReq 创建规则。 type CreateAuditRuleReq struct { - Name string `json:"name"` - RuleType string `json:"rule_type"` - Target string `json:"target"` - Condition string `json:"condition"` - Action int8 `json:"action"` + Name string `json:"name"` // 必填 + RuleType string `json:"rule_type"` // 必填,keyword/regex/domain + Target string `json:"target"` // 必填,subject/body/to/from + Condition string `json:"condition"` // 必填 + Action int8 `json:"action"` // 必填,1/2/3 Priority *int `json:"priority,omitempty"` Remark string `json:"remark,omitempty"` } +// UpdateAuditRuleReq 局部更新规则。 type UpdateAuditRuleReq struct { Name *string `json:"name,omitempty"` RuleType *string `json:"rule_type,omitempty"` @@ -417,6 +503,7 @@ type UpdateAuditRuleReq struct { Remark *string `json:"remark,omitempty"` } +// TestAuditRuleReq 在不发送邮件的前提下测试规则命中情况。 type TestAuditRuleReq struct { Subject string `json:"subject,omitempty"` Body string `json:"body,omitempty"` @@ -425,24 +512,32 @@ type TestAuditRuleReq struct { AccountID uint `json:"account_id,omitempty"` } +// TestAuditRuleResp 规则测试结果。 type TestAuditRuleResp struct { - Action string `json:"action"` - HitRules []HitRuleEntry `json:"hit_rules"` + Action string `json:"action"` // approve/reject/to_manual/none + HitRules []HitRuleEntry `json:"hit_rules"` // 命中的规则列表 } +// HitRuleEntry 命中规则信息。 type HitRuleEntry struct { RuleID uint `json:"rule_id"` RuleName string `json:"rule_name"` RuleType string `json:"rule_type"` } -// --- Queue --- +// ============================================================ +// Queue 发送队列 +// ============================================================ +// QueueStatusData 队列状态快照。 +// - Queues: key = 通道 code, value = 该通道排队长度 +// - DelayQueue: 延迟重试队列长度 type QueueStatusData struct { Queues map[string]int `json:"queues"` DelayQueue int `json:"delay_queue"` } +// QueuePendingQuery 队列待发送邮件过滤参数。 type QueuePendingQuery struct { PaginationQuery ChannelID *uint `json:"channel_id,omitempty"` @@ -450,20 +545,25 @@ type QueuePendingQuery struct { AccountID *uint `json:"account_id,omitempty"` } -// --- Check --- +// ============================================================ +// Check 健康检查 +// ============================================================ +// CheckLog 发信账号健康检查记录。 +// 通过向 verification 地址发测试邮件并等待 webhook 回调验证收信可用性。 type CheckLog struct { ID uint `json:"id"` SenderAccountID uint `json:"sender_account_id"` VerificationCode string `json:"verification_code"` SentAt time.Time `json:"sent_at"` - Received bool `json:"received"` + Received bool `json:"received"` // 是否收到回执 ReceivedAt *time.Time `json:"received_at"` - LatencyMs int `json:"latency_ms"` + LatencyMs int `json:"latency_ms"` // 端到端延迟,毫秒 ErrorMessage string `json:"error_message"` CreatedAt time.Time `json:"created_at"` } +// CheckLogQuery 健康检查日志过滤参数。 type CheckLogQuery struct { PaginationQuery SenderAccountID *uint `json:"sender_account_id,omitempty"` @@ -471,6 +571,7 @@ type CheckLogQuery struct { EndDate string `json:"end_date,omitempty"` } +// SenderHealth 发信账号健康汇总。 type SenderHealth struct { SenderAccountID uint `json:"sender_account_id"` Name string `json:"name"` @@ -481,8 +582,9 @@ type SenderHealth struct { SuccessChecks int64 `json:"success_checks"` } +// TriggerCheckResp 手动触发健康检查的返回。 type TriggerCheckResp struct { - Result string `json:"result"` + Result string `json:"result"` // ok / failed Error string `json:"error,omitempty"` CheckLog *CheckLog `json:"check_log,omitempty"` }