docs(README): 更新文档为中文并完善API参考

- 将README从英文翻译为中文
- 添加详细的API参考文档,包括所有管理接口和枚举值说明
- 补充安装、快速开始、认证方式等使用指南

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

- 为所有API方法添加中文注释和使用说明
- 改进Client结构体和Option配置的设计
- 统一错误处理和响应结构的文档说明
This commit is contained in:
shiran
2026-04-18 15:54:19 +08:00
parent fe19922eff
commit fe43b9bdce
15 changed files with 1013 additions and 291 deletions
+394 -135
View File
@@ -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: "<h1>Hello World</h1>",
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 <token>` | 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 <token>` | 所有 `/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
+19
View File
@@ -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)
}
+24
View File
@@ -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)
}
+26
View File
@@ -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)
}
+13
View File
@@ -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
+9
View File
@@ -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)
}
+63 -9
View File
@@ -12,25 +12,45 @@ import (
"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
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 <serviceToken> 头,
// 覆盖账号、签名、配额、通道、发信、审核、队列、健康检查等全部管理接口。
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)
}
+127
View File
@@ -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: "<h1>Hi</h1>",
// })
//
// 所有后端接口统一返回:
//
// { "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" // 规则驳回
)
+18 -2
View File
@@ -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)
}
+13
View File
@@ -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 {
+12
View File
@@ -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
+16
View File
@@ -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
+13
View File
@@ -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
+19
View File
@@ -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
+247 -145
View File
@@ -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"`
}