docs(README): 更新文档为中文并完善API参考
- 将README从英文翻译为中文 - 添加详细的API参考文档,包括所有管理接口和枚举值说明 - 补充安装、快速开始、认证方式等使用指南 refactor(client): 优化客户端代码结构并添加详细注释 - 为所有API方法添加中文注释和使用说明 - 改进Client结构体和Option配置的设计 - 统一错误处理和响应结构的文档说明
This commit is contained in:
@@ -1,46 +1,38 @@
|
|||||||
# email-serverr-cli
|
# 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
|
```bash
|
||||||
go get gitea.s1f.ren/shiran/email-serverr-cli
|
go get gitea.s1f.ren/shiran/email-serverr-cli
|
||||||
```
|
```
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
### Management Client (ServiceAuth)
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
import emailcli "gitea.s1f.ren/shiran/email-serverr-cli"
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 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
|
```go
|
||||||
client := emailcli.NewAppClient(
|
client := emailcli.NewAppClient(
|
||||||
@@ -53,130 +45,393 @@ resp, err := client.SendMail(context.Background(), emailcli.SendMailReq{
|
|||||||
To: []string{"recipient@example.com"},
|
To: []string{"recipient@example.com"},
|
||||||
Subject: "Hello",
|
Subject: "Hello",
|
||||||
Body: "<h1>Hello World</h1>",
|
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 |
|
```go
|
||||||
|------|-------------|--------|----------|
|
client := emailcli.NewServiceClient(baseURL, token,
|
||||||
| ServiceAuth | `NewServiceClient` | `Authorization: Bearer <token>` | Management APIs |
|
emailcli.WithTimeout(60*time.Second),
|
||||||
| AppAuth | `NewAppClient` | `X-App-Key` + `X-App-Secret` | Mail sending |
|
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 |
|
### Account.Status(账号状态)
|
||||||
|--------|-------------|
|
| 值 | 含义 |
|
||||||
| `CreateAccount` | Create mail account |
|
|----|------|
|
||||||
| `ListAccounts` | List accounts with filters |
|
| 0 | 禁用 |
|
||||||
| `GetAccount` | Get account details |
|
| 1 | 启用 |
|
||||||
| `UpdateAccount` | Update account |
|
|
||||||
| `DeleteAccount` | Delete account |
|
|
||||||
| `ResetAccountSecret` | Reset app secret |
|
|
||||||
|
|
||||||
### Signatures (ServiceAuth)
|
### Account.AuditMode(审核模式)
|
||||||
|
| 值 | 含义 |
|
||||||
|
|----|------|
|
||||||
|
| 0 | 免审核(直接入队) |
|
||||||
|
| 1 | 自动(按规则判定) |
|
||||||
|
| 2 | 人工(待审核) |
|
||||||
|
|
||||||
| Method | Description |
|
### Signature.Status(签名状态)
|
||||||
|--------|-------------|
|
| 值 | 含义 |
|
||||||
| `CreateSignature` | Create signature |
|
|----|------|
|
||||||
| `ListSignatures` | List signatures with filters |
|
| 0 | 待审核 |
|
||||||
| `GetSignature` | Get signature details |
|
| 1 | 已通过 |
|
||||||
| `UpdateSignature` | Update signature |
|
| 2 | 已驳回 |
|
||||||
| `DeleteSignature` | Delete signature |
|
|
||||||
| `AuditSignature` | Approve or reject signature |
|
|
||||||
|
|
||||||
### Mail Logs (ServiceAuth)
|
### MailLog.Status(邮件状态)
|
||||||
|
| 值 | 含义 |
|
||||||
|
|----|------|
|
||||||
|
| 0 | 待审核 |
|
||||||
|
| 1 | 排队中 |
|
||||||
|
| 2 | 发送中 |
|
||||||
|
| 3 | 成功 |
|
||||||
|
| 4 | 失败 |
|
||||||
|
| 5 | 放弃 |
|
||||||
|
| 6 | 驳回 |
|
||||||
|
|
||||||
| Method | Description |
|
### MailQuota.QuotaType(配额类型)
|
||||||
|--------|-------------|
|
| 值 | 含义 |
|
||||||
| `ListMailLogs` | List mail logs with filters |
|
|----|------|
|
||||||
| `GetMailLog` | Get mail log detail with body |
|
| 1 | 总量配额(`total` 为总发送上限) |
|
||||||
| `GetMailStats` | Get status statistics |
|
| 2 | 周期配额(到期自动重置) |
|
||||||
|
|
||||||
### Quotas (ServiceAuth)
|
### MailQuota.CycleUnit(配额周期单位)
|
||||||
|
当 `quota_type=2` 时生效,取值:`day`、`week`、`month`、`year`
|
||||||
|
|
||||||
| Method | Description |
|
### MailQuota.Status
|
||||||
|--------|-------------|
|
| 值 | 含义 |
|
||||||
| `CreateQuota` | Create quota |
|
|----|------|
|
||||||
| `ListQuotas` | List quotas with filters |
|
| 0 | 禁用 |
|
||||||
| `GetQuotaSummary` | Get quota summary for account |
|
| 1 | 启用 |
|
||||||
| `UpdateQuota` | Update quota |
|
|
||||||
| `DeleteQuota` | Delete quota |
|
|
||||||
|
|
||||||
### Channels (ServiceAuth)
|
### Channel.Status / SenderAccount.Status
|
||||||
|
| 值 | 含义 |
|
||||||
|
|----|------|
|
||||||
|
| 0 | 禁用 |
|
||||||
|
| 1 | 启用 |
|
||||||
|
|
||||||
| Method | Description |
|
### Channel.Strategy(发信挑选策略)
|
||||||
|--------|-------------|
|
| 值 | 含义 |
|
||||||
| `CreateChannel` | Create channel |
|
|------|------|
|
||||||
| `ListChannels` | List channels with filters |
|
| `round_robin` | 轮询(默认) |
|
||||||
| `UpdateChannel` | Update channel |
|
| `weight` | 按 `weight` 加权随机 |
|
||||||
| `DeleteChannel` | Delete channel |
|
| `least_used` | 今日发送数最少优先 |
|
||||||
|
|
||||||
### Sender Accounts (ServiceAuth)
|
### AuditRule.Action(规则动作)
|
||||||
|
| 值 | 含义 |
|
||||||
|
|----|------|
|
||||||
|
| 1 | 自动通过 |
|
||||||
|
| 2 | 自动驳回 |
|
||||||
|
| 3 | 转人工 |
|
||||||
|
|
||||||
| Method | Description |
|
### AuditRule.RuleType / Target(规则类型与目标)
|
||||||
|--------|-------------|
|
- `RuleType` 取值: `keyword`、`regex`、`domain`
|
||||||
| `CreateSender` | Create sender under channel |
|
- `Target` 取值: `subject`、`body`、`to`、`from`
|
||||||
| `ListSendersByChannel` | List senders for channel |
|
|
||||||
| `UpdateSender` | Update sender |
|
|
||||||
| `DeleteSender` | Delete sender |
|
|
||||||
|
|
||||||
### Audits (ServiceAuth)
|
### MailAudit.AuditType(审核来源)
|
||||||
|
| 值 | 含义 |
|
||||||
|
|----|------|
|
||||||
|
| 1 | 自动 |
|
||||||
|
| 2 | 人工 |
|
||||||
|
|
||||||
| Method | Description |
|
### MailAudit.Action(审核动作)
|
||||||
|--------|-------------|
|
| 值 | 含义 |
|
||||||
| `ListAuditPending` | List pending audit items |
|
|----|------|
|
||||||
| `GetAuditPendingDetail` | Get pending item detail |
|
| 1 | 通过 |
|
||||||
| `ApproveAudit` | Approve single item |
|
| 2 | 驳回 |
|
||||||
| `RejectAudit` | Reject single item |
|
|
||||||
| `BatchApproveAudit` | Batch approve |
|
|
||||||
| `BatchRejectAudit` | Batch reject |
|
|
||||||
| `ListAuditLogs` | List audit history |
|
|
||||||
| `GetAuditStats` | Get audit statistics |
|
|
||||||
|
|
||||||
### 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 |
|
所有管理接口挂载在 `/api/v1` 下。以下按功能模块分组,并给出方法签名、HTTP 路径与关键参数说明。
|
||||||
|--------|-------------|
|
|
||||||
| `GetQueueStatus` | Get queue lengths |
|
|
||||||
| `ListQueuePending` | List pending queue items |
|
|
||||||
| `CancelQueueItem` | Cancel queued mail |
|
|
||||||
| `RetryQueueItem` | Retry failed mail |
|
|
||||||
|
|
||||||
### Health Checks (ServiceAuth)
|
### 一、发送邮件(AppAuth)
|
||||||
|
|
||||||
| Method | Description |
|
#### `SendMail(ctx, req SendMailReq) -> *SendMailResp`
|
||||||
|--------|-------------|
|
|
||||||
| `ListCheckLogs` | List check logs |
|
|
||||||
| `GetCheckSummary` | Get sender health summary |
|
|
||||||
| `TriggerCheck` | Trigger health check |
|
|
||||||
|
|
||||||
## 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
|
```go
|
||||||
resp, err := client.SendMail(ctx, req)
|
resp, err := client.SendMail(ctx, req)
|
||||||
@@ -188,13 +443,17 @@ if err != nil {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Options
|
分页接口统一包装:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
client := emailcli.NewServiceClient(
|
type PaginationResult[T any] struct {
|
||||||
"https://your-server.com",
|
List []T `json:"list"`
|
||||||
"token",
|
Total int64 `json:"total"`
|
||||||
emailcli.WithTimeout(60 * time.Second),
|
Page int `json:"page"`
|
||||||
emailcli.WithHTTPClient(customClient),
|
PageSize int `json:"page_size"`
|
||||||
)
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|||||||
+19
@@ -5,10 +5,16 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CreateAccount 创建邮件账号。返回的 AppSecret 是明文,仅此一次,需妥善保存。
|
||||||
|
//
|
||||||
|
// POST /api/v1/accounts ServiceAuth
|
||||||
func (c *Client) CreateAccount(ctx context.Context, req CreateAccountReq) (*CreateAccountResp, error) {
|
func (c *Client) CreateAccount(ctx context.Context, req CreateAccountReq) (*CreateAccountResp, error) {
|
||||||
return post[*CreateAccountResp](c, ctx, "/api/v1/accounts", req)
|
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) {
|
func (c *Client) ListAccounts(ctx context.Context, q AccountListQuery) (*PaginationResult[Account], error) {
|
||||||
params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{
|
params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{
|
||||||
"user_id": q.UserID,
|
"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))
|
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) {
|
func (c *Client) GetAccount(ctx context.Context, id uint) (*Account, error) {
|
||||||
return get[*Account](c, ctx, fmt.Sprintf("/api/v1/accounts/%d", id), nil)
|
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) {
|
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)
|
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 {
|
func (c *Client) DeleteAccount(ctx context.Context, id uint) error {
|
||||||
_, err := del[any](c, ctx, fmt.Sprintf("/api/v1/accounts/%d", id))
|
_, err := del[any](c, ctx, fmt.Sprintf("/api/v1/accounts/%d", id))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResetAccountSecret 重置账号 AppSecret,旧密钥立即失效。
|
||||||
|
// 返回的新 AppSecret 是明文,仅此一次。
|
||||||
|
//
|
||||||
|
// POST /api/v1/accounts/{id}/reset-secret ServiceAuth
|
||||||
func (c *Client) ResetAccountSecret(ctx context.Context, id uint) (*ResetSecretResp, error) {
|
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)
|
return post[*ResetSecretResp](c, ctx, fmt.Sprintf("/api/v1/accounts/%d/reset-secret", id), nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import (
|
|||||||
"fmt"
|
"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) {
|
func (c *Client) ListAuditPending(ctx context.Context, q AuditPendingQuery) (*PaginationResult[MailLog], error) {
|
||||||
params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{
|
params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{
|
||||||
"user_id": q.UserID,
|
"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))
|
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) {
|
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)
|
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 {
|
func (c *Client) ApproveAudit(ctx context.Context, id uint) error {
|
||||||
_, err := post[any](c, ctx, fmt.Sprintf("/api/v1/audits/%d/approve", id), nil)
|
_, err := post[any](c, ctx, fmt.Sprintf("/api/v1/audits/%d/approve", id), nil)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RejectAudit 审核驳回单封邮件,被驳回的邮件会退还配额。
|
||||||
|
//
|
||||||
|
// POST /api/v1/audits/{id}/reject ServiceAuth
|
||||||
func (c *Client) RejectAudit(ctx context.Context, id uint, req AuditRejectReq) error {
|
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)
|
_, err := post[any](c, ctx, fmt.Sprintf("/api/v1/audits/%d/reject", id), req)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BatchApproveAudit 批量通过。MailLogIDs 至少 1 个。
|
||||||
|
//
|
||||||
|
// POST /api/v1/audits/batch/approve ServiceAuth
|
||||||
func (c *Client) BatchApproveAudit(ctx context.Context, req BatchAuditApproveReq) error {
|
func (c *Client) BatchApproveAudit(ctx context.Context, req BatchAuditApproveReq) error {
|
||||||
_, err := post[any](c, ctx, "/api/v1/audits/batch/approve", req)
|
_, err := post[any](c, ctx, "/api/v1/audits/batch/approve", req)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BatchRejectAudit 批量驳回。建议填写 RejectReason 方便溯源。
|
||||||
|
//
|
||||||
|
// POST /api/v1/audits/batch/reject ServiceAuth
|
||||||
func (c *Client) BatchRejectAudit(ctx context.Context, req BatchAuditRejectReq) error {
|
func (c *Client) BatchRejectAudit(ctx context.Context, req BatchAuditRejectReq) error {
|
||||||
_, err := post[any](c, ctx, "/api/v1/audits/batch/reject", req)
|
_, err := post[any](c, ctx, "/api/v1/audits/batch/reject", req)
|
||||||
return err
|
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) {
|
func (c *Client) ListAuditLogs(ctx context.Context, q AuditLogQuery) (*PaginationResult[MailAudit], error) {
|
||||||
params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{
|
params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{
|
||||||
"audit_type": q.AuditType,
|
"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))
|
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) {
|
func (c *Client) GetAuditStats(ctx context.Context) (*AuditStats, error) {
|
||||||
return get[*AuditStats](c, ctx, "/api/v1/audits/stats", nil)
|
return get[*AuditStats](c, ctx, "/api/v1/audits/stats", nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,31 +5,57 @@ import (
|
|||||||
"fmt"
|
"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) {
|
func (c *Client) CreateAuditRule(ctx context.Context, req CreateAuditRuleReq) (*AuditRule, error) {
|
||||||
return post[*AuditRule](c, ctx, "/api/v1/audit-rules", req)
|
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) {
|
func (c *Client) ListAuditRules(ctx context.Context) ([]AuditRule, error) {
|
||||||
return get[[]AuditRule](c, ctx, "/api/v1/audit-rules", nil)
|
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) {
|
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)
|
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) {
|
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)
|
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 {
|
func (c *Client) DeleteAuditRule(ctx context.Context, id uint) error {
|
||||||
_, err := del[any](c, ctx, fmt.Sprintf("/api/v1/audit-rules/%d", id))
|
_, err := del[any](c, ctx, fmt.Sprintf("/api/v1/audit-rules/%d", id))
|
||||||
return err
|
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) {
|
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})
|
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) {
|
func (c *Client) TestAuditRule(ctx context.Context, req TestAuditRuleReq) (*TestAuditRuleResp, error) {
|
||||||
return post[*TestAuditRuleResp](c, ctx, "/api/v1/audit-rules/test", req)
|
return post[*TestAuditRuleResp](c, ctx, "/api/v1/audit-rules/test", req)
|
||||||
}
|
}
|
||||||
|
|||||||
+13
@@ -5,10 +5,17 @@ import (
|
|||||||
"fmt"
|
"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) {
|
func (c *Client) CreateChannel(ctx context.Context, req CreateChannelReq) (*Channel, error) {
|
||||||
return post[*Channel](c, ctx, "/api/v1/channels", req)
|
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) {
|
func (c *Client) ListChannels(ctx context.Context, q ChannelListQuery) (*PaginationResult[Channel], error) {
|
||||||
params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{
|
params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{
|
||||||
"status": q.Status,
|
"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))
|
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) {
|
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)
|
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 {
|
func (c *Client) DeleteChannel(ctx context.Context, id uint) error {
|
||||||
_, err := del[any](c, ctx, fmt.Sprintf("/api/v1/channels/%d", id))
|
_, err := del[any](c, ctx, fmt.Sprintf("/api/v1/channels/%d", id))
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import (
|
|||||||
"fmt"
|
"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) {
|
func (c *Client) ListCheckLogs(ctx context.Context, q CheckLogQuery) (*PaginationResult[CheckLog], error) {
|
||||||
params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{
|
params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{
|
||||||
"sender_account_id": q.SenderAccountID,
|
"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))
|
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) {
|
func (c *Client) GetCheckSummary(ctx context.Context) ([]SenderHealth, error) {
|
||||||
return get[[]SenderHealth](c, ctx, "/api/v1/check-logs/summary", nil)
|
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) {
|
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)
|
return post[*TriggerCheckResp](c, ctx, fmt.Sprintf("/api/v1/check-logs/trigger/%d", senderAccountID), nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,25 +12,45 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Client 是访问 Email Server 后端的 HTTP 客户端。
|
||||||
|
// 同一个 Client 可以同时持有 ServiceToken 与 AppKey/Secret,
|
||||||
|
// 两种头会同时带上;一般建议按用途分别创建实例。
|
||||||
|
//
|
||||||
|
// 构造方式:
|
||||||
|
// - NewServiceClient: 管理端(Authorization: Bearer <token>)
|
||||||
|
// - NewAppClient: 发件端 (X-App-Key / X-App-Secret)
|
||||||
type Client struct {
|
type Client struct {
|
||||||
baseURL string
|
baseURL string // 基础地址,不带尾斜杠,例如 https://api.example.com
|
||||||
serviceToken string
|
serviceToken string // ServiceAuth 令牌(管理接口)
|
||||||
appKey string
|
appKey string // AppAuth AppKey(发件接口)
|
||||||
appSecret string
|
appSecret string // AppAuth AppSecret
|
||||||
httpClient *http.Client
|
httpClient *http.Client // 底层 HTTP 客户端,默认超时 30s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Option 用于在构造 Client 时传入可选配置。
|
||||||
type Option func(*Client)
|
type Option func(*Client)
|
||||||
|
|
||||||
|
// WithHTTPClient 使用自定义 *http.Client,例如配置代理、传输层、证书等。
|
||||||
func WithHTTPClient(hc *http.Client) Option {
|
func WithHTTPClient(hc *http.Client) Option {
|
||||||
return func(c *Client) { c.httpClient = hc }
|
return func(c *Client) { c.httpClient = hc }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithTimeout 修改底层 HTTP 客户端的超时(默认 30 秒)。
|
||||||
|
//
|
||||||
|
// 注意:如果同时使用 WithHTTPClient,请确保先设置自定义客户端再应用超时。
|
||||||
func WithTimeout(d time.Duration) Option {
|
func WithTimeout(d time.Duration) Option {
|
||||||
return func(c *Client) { c.httpClient.Timeout = d }
|
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 {
|
func NewServiceClient(baseURL, serviceToken string, opts ...Option) *Client {
|
||||||
c := &Client{
|
c := &Client{
|
||||||
baseURL: strings.TrimRight(baseURL, "/"),
|
baseURL: strings.TrimRight(baseURL, "/"),
|
||||||
@@ -43,7 +63,14 @@ func NewServiceClient(baseURL, serviceToken string, opts ...Option) *Client {
|
|||||||
return c
|
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 {
|
func NewAppClient(baseURL, appKey, appSecret string, opts ...Option) *Client {
|
||||||
c := &Client{
|
c := &Client{
|
||||||
baseURL: strings.TrimRight(baseURL, "/"),
|
baseURL: strings.TrimRight(baseURL, "/"),
|
||||||
@@ -57,25 +84,39 @@ func NewAppClient(baseURL, appKey, appSecret string, opts ...Option) *Client {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// APIResponse 是后端统一响应体。
|
||||||
|
//
|
||||||
|
// { "code": 200, "message": "ok", "data": ... }
|
||||||
|
//
|
||||||
|
// SDK 内部会解包 Data,正常情况下调用方无需直接使用该类型。
|
||||||
type APIResponse[T any] struct {
|
type APIResponse[T any] struct {
|
||||||
Code int `json:"code"`
|
Code int `json:"code"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Data T `json:"data,omitempty"`
|
Data T `json:"data,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// APIError 表示后端业务错误(HTTP 状态码可能仍是 200,但 code != 200)。
|
||||||
|
//
|
||||||
|
// 使用 errors.As 判断:
|
||||||
|
//
|
||||||
|
// var apiErr *emailcli.APIError
|
||||||
|
// if errors.As(err, &apiErr) { ... }
|
||||||
type APIError struct {
|
type APIError struct {
|
||||||
Code int
|
Code int // 业务错误码(非 HTTP 状态码)
|
||||||
Message string
|
Message string // 错误描述
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Error 实现 error 接口。
|
||||||
func (e *APIError) Error() string {
|
func (e *APIError) Error() string {
|
||||||
return fmt.Sprintf("api error %d: %s", e.Code, e.Message)
|
return fmt.Sprintf("api error %d: %s", e.Code, e.Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// url 拼接出完整请求 URL。
|
||||||
func (c *Client) url(path string) string {
|
func (c *Client) url(path string) string {
|
||||||
return c.baseURL + path
|
return c.baseURL + path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setAuth 根据客户端类型自动写入认证头。
|
||||||
func (c *Client) setAuth(req *http.Request) {
|
func (c *Client) setAuth(req *http.Request) {
|
||||||
if c.serviceToken != "" {
|
if c.serviceToken != "" {
|
||||||
req.Header.Set("Authorization", "Bearer "+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) {
|
func doRequest[T any](c *Client, ctx context.Context, method, path string, body interface{}, query url.Values) (T, error) {
|
||||||
var zero T
|
var zero T
|
||||||
|
|
||||||
@@ -136,18 +186,22 @@ func doRequest[T any](c *Client, ctx context.Context, method, path string, body
|
|||||||
return apiResp.Data, nil
|
return apiResp.Data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get 是 doRequest 的 GET 便捷函数。
|
||||||
func get[T any](c *Client, ctx context.Context, path string, query url.Values) (T, error) {
|
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)
|
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) {
|
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)
|
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) {
|
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)
|
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) {
|
func del[T any](c *Client, ctx context.Context, path string) (T, error) {
|
||||||
return doRequest[T](c, ctx, http.MethodDelete, path, nil, nil)
|
return doRequest[T](c, ctx, http.MethodDelete, path, nil, nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" // 规则驳回
|
||||||
|
)
|
||||||
@@ -5,12 +5,22 @@ import (
|
|||||||
"fmt"
|
"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) {
|
func (c *Client) SendMail(ctx context.Context, req SendMailReq) (*SendMailResp, error) {
|
||||||
return post[*SendMailResp](c, ctx, "/api/v1/mail/send", req)
|
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) {
|
func (c *Client) ListMailLogs(ctx context.Context, q MailLogListQuery) (*PaginationResult[MailLog], error) {
|
||||||
params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{
|
params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{
|
||||||
"user_id": q.UserID,
|
"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))
|
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) {
|
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)
|
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) {
|
func (c *Client) GetMailStats(ctx context.Context) ([]MailStatItem, error) {
|
||||||
return get[[]MailStatItem](c, ctx, "/api/v1/mail-logs/stats", nil)
|
return get[[]MailStatItem](c, ctx, "/api/v1/mail-logs/stats", nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,15 @@ import (
|
|||||||
"net/url"
|
"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 {
|
func buildQuery(params map[string]interface{}) url.Values {
|
||||||
q := url.Values{}
|
q := url.Values{}
|
||||||
for k, v := range params {
|
for k, v := range params {
|
||||||
@@ -45,6 +54,8 @@ func buildQuery(params map[string]interface{}) url.Values {
|
|||||||
return q
|
return q
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// paginationParams 将分页结构体转换为通用 map 形式的查询参数,
|
||||||
|
// 供 mergeParams 与 buildQuery 继续拼装。Page / PageSize <=0 时会被忽略。
|
||||||
func paginationParams(p PaginationQuery) map[string]interface{} {
|
func paginationParams(p PaginationQuery) map[string]interface{} {
|
||||||
m := map[string]interface{}{}
|
m := map[string]interface{}{}
|
||||||
if p.Page > 0 {
|
if p.Page > 0 {
|
||||||
@@ -56,6 +67,8 @@ func paginationParams(p PaginationQuery) map[string]interface{} {
|
|||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mergeParams 合并多个查询参数 map,后者覆盖前者。
|
||||||
|
// 用于把分页参数与业务过滤参数拼在一起传给 buildQuery。
|
||||||
func mergeParams(maps ...map[string]interface{}) map[string]interface{} {
|
func mergeParams(maps ...map[string]interface{}) map[string]interface{} {
|
||||||
result := map[string]interface{}{}
|
result := map[string]interface{}{}
|
||||||
for _, m := range maps {
|
for _, m := range maps {
|
||||||
|
|||||||
@@ -5,10 +5,16 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// GetQueueStatus 查询各通道当前排队长度与延迟重试队列长度。
|
||||||
|
//
|
||||||
|
// GET /api/v1/queue/status ServiceAuth
|
||||||
func (c *Client) GetQueueStatus(ctx context.Context) (*QueueStatusData, error) {
|
func (c *Client) GetQueueStatus(ctx context.Context) (*QueueStatusData, error) {
|
||||||
return get[*QueueStatusData](c, ctx, "/api/v1/queue/status", nil)
|
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) {
|
func (c *Client) ListQueuePending(ctx context.Context, q QueuePendingQuery) (*PaginationResult[MailLog], error) {
|
||||||
params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{
|
params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{
|
||||||
"channel_id": q.ChannelID,
|
"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))
|
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 {
|
func (c *Client) CancelQueueItem(ctx context.Context, mailLogID uint) error {
|
||||||
_, err := post[any](c, ctx, fmt.Sprintf("/api/v1/queue/%d/cancel", mailLogID), nil)
|
_, err := post[any](c, ctx, fmt.Sprintf("/api/v1/queue/%d/cancel", mailLogID), nil)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RetryQueueItem 重新入队一封失败的邮件。
|
||||||
|
//
|
||||||
|
// POST /api/v1/queue/{mailLogID}/retry ServiceAuth
|
||||||
func (c *Client) RetryQueueItem(ctx context.Context, mailLogID uint) error {
|
func (c *Client) RetryQueueItem(ctx context.Context, mailLogID uint) error {
|
||||||
_, err := post[any](c, ctx, fmt.Sprintf("/api/v1/queue/%d/retry", mailLogID), nil)
|
_, err := post[any](c, ctx, fmt.Sprintf("/api/v1/queue/%d/retry", mailLogID), nil)
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -5,10 +5,17 @@ import (
|
|||||||
"fmt"
|
"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) {
|
func (c *Client) CreateQuota(ctx context.Context, req CreateQuotaReq) (*MailQuota, error) {
|
||||||
return post[*MailQuota](c, ctx, "/api/v1/quotas", req)
|
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) {
|
func (c *Client) ListQuotas(ctx context.Context, q QuotaListQuery) (*PaginationResult[MailQuota], error) {
|
||||||
params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{
|
params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{
|
||||||
"account_id": q.AccountID,
|
"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))
|
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) {
|
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)
|
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) {
|
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)
|
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 {
|
func (c *Client) DeleteQuota(ctx context.Context, id uint) error {
|
||||||
_, err := del[any](c, ctx, fmt.Sprintf("/api/v1/quotas/%d", id))
|
_, err := del[any](c, ctx, fmt.Sprintf("/api/v1/quotas/%d", id))
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -5,10 +5,17 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CreateSender 在指定通道下创建 SMTP 发信账号。
|
||||||
|
// SmtpPassword 会加密存储,不会在后续接口中返回。
|
||||||
|
//
|
||||||
|
// POST /api/v1/channels/{channelID}/senders ServiceAuth
|
||||||
func (c *Client) CreateSender(ctx context.Context, channelID uint, req CreateSenderReq) (*SenderAccount, error) {
|
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)
|
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) {
|
func (c *Client) ListSendersByChannel(ctx context.Context, channelID uint, q SenderListQuery) (*PaginationResult[SenderAccount], error) {
|
||||||
params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{
|
params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{
|
||||||
"status": q.Status,
|
"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))
|
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) {
|
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)
|
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 {
|
func (c *Client) DeleteSender(ctx context.Context, id uint) error {
|
||||||
_, err := del[any](c, ctx, fmt.Sprintf("/api/v1/senders/%d", id))
|
_, err := del[any](c, ctx, fmt.Sprintf("/api/v1/senders/%d", id))
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -5,10 +5,16 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CreateSignature 创建签名,新建签名默认 Status=0(待审核),需要调用 AuditSignature 审核后方可使用。
|
||||||
|
//
|
||||||
|
// POST /api/v1/signatures ServiceAuth
|
||||||
func (c *Client) CreateSignature(ctx context.Context, req CreateSignatureReq) (*Signature, error) {
|
func (c *Client) CreateSignature(ctx context.Context, req CreateSignatureReq) (*Signature, error) {
|
||||||
return post[*Signature](c, ctx, "/api/v1/signatures", req)
|
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) {
|
func (c *Client) ListSignatures(ctx context.Context, q SignatureListQuery) (*PaginationResult[Signature], error) {
|
||||||
params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{
|
params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{
|
||||||
"account_id": q.AccountID,
|
"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))
|
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) {
|
func (c *Client) GetSignature(ctx context.Context, id uint) (*Signature, error) {
|
||||||
return get[*Signature](c, ctx, fmt.Sprintf("/api/v1/signatures/%d", id), nil)
|
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) {
|
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)
|
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 {
|
func (c *Client) DeleteSignature(ctx context.Context, id uint) error {
|
||||||
_, err := del[any](c, ctx, fmt.Sprintf("/api/v1/signatures/%d", id))
|
_, err := del[any](c, ctx, fmt.Sprintf("/api/v1/signatures/%d", id))
|
||||||
return err
|
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 {
|
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)
|
_, err := post[any](c, ctx, fmt.Sprintf("/api/v1/signatures/%d/audit", id), req)
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -2,7 +2,14 @@ package emailcli
|
|||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
// PaginationResult is the generic paginated response wrapper.
|
// PaginationResult 是分页接口的统一返回结构。
|
||||||
|
//
|
||||||
|
// {
|
||||||
|
// "list": [...], // 当前页数据
|
||||||
|
// "total": 123, // 总记录数
|
||||||
|
// "page": 1,
|
||||||
|
// "page_size": 20
|
||||||
|
// }
|
||||||
type PaginationResult[T any] struct {
|
type PaginationResult[T any] struct {
|
||||||
List []T `json:"list"`
|
List []T `json:"list"`
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
@@ -10,13 +17,17 @@ type PaginationResult[T any] struct {
|
|||||||
PageSize int `json:"page_size"`
|
PageSize int `json:"page_size"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PaginationQuery 是所有列表接口的分页参数基类,
|
||||||
|
// 业务 Query 结构体可通过嵌入来复用。
|
||||||
|
// - Page: 页码,从 1 开始;省略时后端默认 1
|
||||||
|
// - PageSize: 每页大小,省略时后端默认 20,最大 100
|
||||||
type PaginationQuery struct {
|
type PaginationQuery struct {
|
||||||
Page int `json:"page,omitempty"`
|
Page int `json:"page,omitempty"`
|
||||||
PageSize int `json:"page_size,omitempty"`
|
PageSize int `json:"page_size,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- GormModel fields ---
|
// GormModel 对应后端 gorm.Model。注意后端该部分没有自定义 json tag,
|
||||||
|
// 因此字段使用 PascalCase 序列化(与数据库模型保持一致)。
|
||||||
type GormModel struct {
|
type GormModel struct {
|
||||||
ID uint `json:"ID"`
|
ID uint `json:"ID"`
|
||||||
CreatedAt time.Time `json:"CreatedAt"`
|
CreatedAt time.Time `json:"CreatedAt"`
|
||||||
@@ -24,84 +35,104 @@ type GormModel struct {
|
|||||||
DeletedAt *time.Time `json:"DeletedAt"`
|
DeletedAt *time.Time `json:"DeletedAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Account ---
|
// ============================================================
|
||||||
|
// Account 邮件账号
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Account 表示一个邮件发送账号。
|
||||||
|
// AppKey 用于发件认证;AppSecret 只在创建或重置时返回一次,
|
||||||
|
// 入库时以 bcrypt 保存,SDK 不会回传。
|
||||||
type Account struct {
|
type Account struct {
|
||||||
GormModel
|
GormModel
|
||||||
UserID int `json:"user_id"`
|
UserID int `json:"user_id"` // 关联主平台用户 ID
|
||||||
Name string `json:"name"`
|
Name string `json:"name"` // 账号名称
|
||||||
AppKey string `json:"app_key"`
|
AppKey string `json:"app_key"` // 发件公钥
|
||||||
Status int8 `json:"status"`
|
Status int8 `json:"status"` // 账号状态: 0=禁用 1=启用
|
||||||
AuditMode int8 `json:"audit_mode"`
|
AuditMode int8 `json:"audit_mode"` // 审核模式: 0=免审核 1=自动 2=人工
|
||||||
RateLimit int `json:"rate_limit"`
|
RateLimit int `json:"rate_limit"` // 频率限制(封/分钟),0 表示不限
|
||||||
AllowedChannels string `json:"allowed_channels"`
|
DefaultChannelID *uint `json:"default_channel_id"` // 默认发件通道 ID
|
||||||
DefaultSignatureID *uint `json:"default_signature_id"`
|
AllowedChannels string `json:"allowed_channels"` // 允许的通道 ID 列表 JSON 字符串,如 "[1,2]";空串=不限
|
||||||
Remark string `json:"remark"`
|
DefaultSignatureID *uint `json:"default_signature_id"` // 默认签名 ID
|
||||||
|
Remark string `json:"remark"` // 备注
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateAccountReq 是创建账号的请求体。
|
||||||
type CreateAccountReq struct {
|
type CreateAccountReq struct {
|
||||||
UserID int `json:"user_id"`
|
UserID int `json:"user_id"` // 必填,关联用户 ID
|
||||||
Name string `json:"name"`
|
Name string `json:"name"` // 必填,账号名称,≤100
|
||||||
AuditMode *int8 `json:"audit_mode,omitempty"`
|
AuditMode *int8 `json:"audit_mode,omitempty"` // 可选,默认 0 免审核
|
||||||
RateLimit *int `json:"rate_limit,omitempty"`
|
RateLimit *int `json:"rate_limit,omitempty"` // 可选,0=不限
|
||||||
Remark string `json:"remark,omitempty"`
|
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 {
|
type CreateAccountResp struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
AppKey string `json:"app_key"`
|
AppKey string `json:"app_key"`
|
||||||
AppSecret string `json:"app_secret"`
|
AppSecret string `json:"app_secret"` // 明文密钥,务必妥善保存
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateAccountReq 所有字段均为可选;只对传入字段做局部更新。
|
||||||
type UpdateAccountReq struct {
|
type UpdateAccountReq struct {
|
||||||
Name *string `json:"name,omitempty"`
|
Name *string `json:"name,omitempty"`
|
||||||
Status *int8 `json:"status,omitempty"`
|
Status *int8 `json:"status,omitempty"` // 0=禁用 1=启用
|
||||||
AuditMode *int8 `json:"audit_mode,omitempty"`
|
AuditMode *int8 `json:"audit_mode,omitempty"` // 0=免审核 1=自动 2=人工
|
||||||
RateLimit *int `json:"rate_limit,omitempty"`
|
RateLimit *int `json:"rate_limit,omitempty"` // 封/分钟,0=不限
|
||||||
AllowedChannels *string `json:"allowed_channels,omitempty"`
|
DefaultChannelID *uint `json:"default_channel_id,omitempty"` // 默认发件通道 ID
|
||||||
DefaultSignatureID *uint `json:"default_signature_id,omitempty"`
|
AllowedChannels *string `json:"allowed_channels,omitempty"` // 允许通道 ID 列表 JSON
|
||||||
|
DefaultSignatureID *uint `json:"default_signature_id,omitempty"` // 默认签名 ID
|
||||||
Remark *string `json:"remark,omitempty"`
|
Remark *string `json:"remark,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AccountListQuery 是账号列表过滤参数。
|
||||||
type AccountListQuery struct {
|
type AccountListQuery struct {
|
||||||
PaginationQuery
|
PaginationQuery
|
||||||
UserID *int `json:"user_id,omitempty"`
|
UserID *int `json:"user_id,omitempty"` // 按用户过滤
|
||||||
Status *int8 `json:"status,omitempty"`
|
Status *int8 `json:"status,omitempty"` // 0=禁用 1=启用
|
||||||
Keyword string `json:"keyword,omitempty"`
|
Keyword string `json:"keyword,omitempty"` // 模糊匹配 name / remark
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResetSecretResp 是重置密钥后的返回。
|
||||||
type ResetSecretResp struct {
|
type ResetSecretResp struct {
|
||||||
AppSecret string `json:"app_secret"`
|
AppSecret string `json:"app_secret"` // 新生成的明文密钥
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Signature ---
|
// ============================================================
|
||||||
|
// Signature 签名
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Signature 表示一个邮件签名。
|
||||||
|
// EnglishName 会作为 From 地址的 local 部分组装发件人(签名@域名)。
|
||||||
type Signature struct {
|
type Signature struct {
|
||||||
GormModel
|
GormModel
|
||||||
UserID int `json:"user_id"`
|
UserID int `json:"user_id"`
|
||||||
AccountID *uint `json:"account_id"`
|
AccountID *uint `json:"account_id"` // 为空表示用户全局签名
|
||||||
Title string `json:"title"`
|
Title string `json:"title"` // 中文抬头
|
||||||
EnglishName string `json:"english_name"`
|
EnglishName string `json:"english_name"` // 英文标识(用于拼 From 地址)
|
||||||
Content string `json:"content"`
|
Content string `json:"content"` // HTML 签名正文
|
||||||
Applicant string `json:"applicant"`
|
Applicant string `json:"applicant"` // 申请人
|
||||||
ApplicantInfo string `json:"applicant_info"`
|
ApplicantInfo string `json:"applicant_info"` // 申请说明
|
||||||
Status int8 `json:"status"`
|
Status int8 `json:"status"` // 0=待审核 1=已通过 2=已驳回
|
||||||
RejectReason string `json:"reject_reason"`
|
RejectReason string `json:"reject_reason"`
|
||||||
Auditor string `json:"auditor"`
|
Auditor string `json:"auditor"`
|
||||||
AuditedAt *time.Time `json:"audited_at"`
|
AuditedAt *time.Time `json:"audited_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateSignatureReq 是创建签名的请求体。新建签名默认 Status=0(待审核)。
|
||||||
type CreateSignatureReq struct {
|
type CreateSignatureReq struct {
|
||||||
UserID int `json:"user_id"`
|
UserID int `json:"user_id"` // 必填
|
||||||
AccountID *uint `json:"account_id,omitempty"`
|
AccountID *uint `json:"account_id,omitempty"` // 可选,绑定账号
|
||||||
Title string `json:"title"`
|
Title string `json:"title"` // 必填,中文抬头
|
||||||
EnglishName string `json:"english_name"`
|
EnglishName string `json:"english_name"` // 必填,英文标识
|
||||||
Content string `json:"content,omitempty"`
|
Content string `json:"content,omitempty"` // HTML 签名内容
|
||||||
Applicant string `json:"applicant,omitempty"`
|
Applicant string `json:"applicant,omitempty"` // 申请人
|
||||||
ApplicantInfo string `json:"applicant_info,omitempty"`
|
ApplicantInfo string `json:"applicant_info,omitempty"` // 申请说明
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateSignatureReq 局部更新签名字段。
|
||||||
type UpdateSignatureReq struct {
|
type UpdateSignatureReq struct {
|
||||||
Title *string `json:"title,omitempty"`
|
Title *string `json:"title,omitempty"`
|
||||||
EnglishName *string `json:"english_name,omitempty"`
|
EnglishName *string `json:"english_name,omitempty"`
|
||||||
@@ -110,64 +141,79 @@ type UpdateSignatureReq struct {
|
|||||||
ApplicantInfo *string `json:"applicant_info,omitempty"`
|
ApplicantInfo *string `json:"applicant_info,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AuditSignatureReq 是审核签名的请求体。
|
||||||
type AuditSignatureReq struct {
|
type AuditSignatureReq struct {
|
||||||
Status int8 `json:"status"`
|
Status int8 `json:"status"` // 1=通过 2=驳回
|
||||||
RejectReason string `json:"reject_reason,omitempty"`
|
RejectReason string `json:"reject_reason,omitempty"` // 驳回时填写
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SignatureListQuery 是签名列表过滤参数。
|
||||||
type SignatureListQuery struct {
|
type SignatureListQuery struct {
|
||||||
PaginationQuery
|
PaginationQuery
|
||||||
AccountID *uint `json:"account_id,omitempty"`
|
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"`
|
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 {
|
type SendMailReq struct {
|
||||||
To []string `json:"to"`
|
To []string `json:"to"` // 必填,收件人列表(至少 1 个)
|
||||||
Cc []string `json:"cc,omitempty"`
|
Cc []string `json:"cc,omitempty"` // 抄送
|
||||||
Bcc []string `json:"bcc,omitempty"`
|
Bcc []string `json:"bcc,omitempty"` // 密送
|
||||||
Subject string `json:"subject"`
|
Subject string `json:"subject"` // 必填,主题
|
||||||
Body string `json:"body"`
|
Body string `json:"body"` // 必填,正文
|
||||||
ContentType string `json:"content_type,omitempty"`
|
ContentType string `json:"content_type,omitempty"` // text/plain 或 text/html,默认 text/html
|
||||||
Channel string `json:"channel"`
|
Channel string `json:"channel,omitempty"` // 通道 code;留空走默认/允许通道
|
||||||
SignatureID *uint `json:"signature_id,omitempty"`
|
SignatureID *uint `json:"signature_id,omitempty"` // 指定签名 ID(优先级低于 title)
|
||||||
SignatureTitle string `json:"signature_title,omitempty"`
|
SignatureTitle string `json:"signature_title,omitempty"` // 按 title + user_id 查找已审核签名
|
||||||
Attachments []AttachmentItem `json:"attachments,omitempty"`
|
Attachments []AttachmentItem `json:"attachments,omitempty"` // 附件
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AttachmentItem 附件项,Content 为 base64 编码字符串。
|
||||||
type AttachmentItem struct {
|
type AttachmentItem struct {
|
||||||
Filename string `json:"filename"`
|
Filename string `json:"filename"` // 文件名(含扩展名)
|
||||||
Content string `json:"content"`
|
Content string `json:"content"` // base64 编码的文件内容
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendMailResp 是发送邮件的返回结果。
|
||||||
type SendMailResp struct {
|
type SendMailResp struct {
|
||||||
MailLogID uint `json:"mail_log_id"`
|
MailLogID uint `json:"mail_log_id"` // 创建的邮件日志 ID
|
||||||
Status string `json:"status"`
|
Status string `json:"status"` // queued / pending_audit / rejected
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Mail Log ---
|
// ============================================================
|
||||||
|
// MailLog 邮件日志
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// MailLog 邮件日志记录。
|
||||||
|
// Status 枚举:0=待审核 1=排队中 2=发送中 3=成功 4=失败 5=放弃 6=驳回
|
||||||
type MailLog struct {
|
type MailLog struct {
|
||||||
GormModel
|
GormModel
|
||||||
UserID int `json:"user_id"`
|
UserID int `json:"user_id"`
|
||||||
AccountID uint `json:"account_id"`
|
AccountID uint `json:"account_id"`
|
||||||
QuotaID *uint `json:"quota_id"`
|
QuotaID *uint `json:"quota_id"` // 扣减的配额记录 ID
|
||||||
ChannelID *uint `json:"channel_id"`
|
ChannelID *uint `json:"channel_id"` // 使用的发件通道 ID
|
||||||
SenderAccountID *uint `json:"sender_account_id"`
|
SenderAccountID *uint `json:"sender_account_id"` // 实际使用的发信账号 ID
|
||||||
SignatureID *uint `json:"signature_id"`
|
SignatureID *uint `json:"signature_id"`
|
||||||
MessageID string `json:"message_id"`
|
MessageID string `json:"message_id"` // SMTP 返回的 Message-ID
|
||||||
FromAddress string `json:"from_address"`
|
FromAddress string `json:"from_address"` // 实际 From 地址
|
||||||
ToAddresses string `json:"to_addresses"`
|
ToAddresses string `json:"to_addresses"` // JSON 字符串数组
|
||||||
CcAddresses string `json:"cc_addresses"`
|
CcAddresses string `json:"cc_addresses"` // JSON 字符串数组
|
||||||
BccAddresses string `json:"bcc_addresses"`
|
BccAddresses string `json:"bcc_addresses"` // JSON 字符串数组
|
||||||
Subject string `json:"subject"`
|
Subject string `json:"subject"`
|
||||||
ContentType string `json:"content_type"`
|
ContentType string `json:"content_type"`
|
||||||
HasAttachment bool `json:"has_attachment"`
|
HasAttachment bool `json:"has_attachment"`
|
||||||
SourceIP string `json:"source_ip"`
|
SourceIP string `json:"source_ip"` // 请求来源 IP
|
||||||
SourceType string `json:"source_type"`
|
SourceType string `json:"source_type"` // http / smtp
|
||||||
Status int8 `json:"status"`
|
Status int8 `json:"status"`
|
||||||
RetryCount int `json:"retry_count"`
|
RetryCount int `json:"retry_count"`
|
||||||
MaxRetry int `json:"max_retry"`
|
MaxRetry int `json:"max_retry"`
|
||||||
@@ -175,51 +221,61 @@ type MailLog struct {
|
|||||||
SentAt *time.Time `json:"sent_at"`
|
SentAt *time.Time `json:"sent_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MailLogListQuery 是邮件日志列表过滤参数。
|
||||||
type MailLogListQuery struct {
|
type MailLogListQuery struct {
|
||||||
PaginationQuery
|
PaginationQuery
|
||||||
UserID *int `json:"user_id,omitempty"`
|
UserID *int `json:"user_id,omitempty"`
|
||||||
AccountID *uint `json:"account_id,omitempty"`
|
AccountID *uint `json:"account_id,omitempty"`
|
||||||
Status *int8 `json:"status,omitempty"`
|
Status *int8 `json:"status,omitempty"` // 见 MailLog.Status 枚举
|
||||||
StartDate string `json:"start_date,omitempty"`
|
StartDate string `json:"start_date,omitempty"` // 格式 YYYY-MM-DD
|
||||||
EndDate string `json:"end_date,omitempty"`
|
EndDate string `json:"end_date,omitempty"` // 格式 YYYY-MM-DD
|
||||||
To string `json:"to,omitempty"`
|
To string `json:"to,omitempty"` // 精确匹配收件人
|
||||||
Keyword string `json:"keyword,omitempty"`
|
Keyword string `json:"keyword,omitempty"` // 模糊匹配主题/收件人
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MailLogDetail 邮件日志详情,包含完整正文。
|
||||||
type MailLogDetail struct {
|
type MailLogDetail struct {
|
||||||
Log MailLog `json:"log"`
|
Log MailLog `json:"log"`
|
||||||
Body string `json:"body"`
|
Body string `json:"body"` // 邮件正文
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MailStatItem 按状态分组的邮件计数,用于概览统计。
|
||||||
type MailStatItem struct {
|
type MailStatItem struct {
|
||||||
Status int8 `json:"status"`
|
Status int8 `json:"status"`
|
||||||
Count int64 `json:"count"`
|
Count int64 `json:"count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Quota ---
|
// ============================================================
|
||||||
|
// Quota 配额
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// MailQuota 配额记录。
|
||||||
|
// - QuotaType=1 总量:Total 为生命周期内的总配额
|
||||||
|
// - QuotaType=2 周期:每个 CycleUnit 周期重置 Used
|
||||||
type MailQuota struct {
|
type MailQuota struct {
|
||||||
GormModel
|
GormModel
|
||||||
UserID int `json:"user_id"`
|
UserID int `json:"user_id"`
|
||||||
AccountID uint `json:"account_id"`
|
AccountID uint `json:"account_id"`
|
||||||
QuotaType int8 `json:"quota_type"`
|
QuotaType int8 `json:"quota_type"` // 1=总量 2=周期
|
||||||
Total int `json:"total"`
|
Total int `json:"total"`
|
||||||
Used int `json:"used"`
|
Used int `json:"used"`
|
||||||
ExpireAt *time.Time `json:"expire_at"`
|
ExpireAt *time.Time `json:"expire_at"` // 到期时间,仅总量配额有意义
|
||||||
CycleUnit string `json:"cycle_unit"`
|
CycleUnit string `json:"cycle_unit"` // day/week/month/year
|
||||||
CycleResetAt *time.Time `json:"cycle_reset_at"`
|
CycleResetAt *time.Time `json:"cycle_reset_at"` // 周期起始时间
|
||||||
Status int8 `json:"status"`
|
Status int8 `json:"status"` // 0=禁用 1=启用
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateQuotaReq 创建配额。
|
||||||
type CreateQuotaReq struct {
|
type CreateQuotaReq struct {
|
||||||
AccountID uint `json:"account_id"`
|
AccountID uint `json:"account_id"` // 必填
|
||||||
QuotaType int8 `json:"quota_type"`
|
QuotaType int8 `json:"quota_type"` // 必填,1=总量 2=周期
|
||||||
Total int `json:"total"`
|
Total int `json:"total"` // 必填,配额上限
|
||||||
ExpireAt string `json:"expire_at,omitempty"`
|
ExpireAt string `json:"expire_at,omitempty"` // YYYY-MM-DD HH:mm:ss
|
||||||
CycleUnit string `json:"cycle_unit,omitempty"`
|
CycleUnit string `json:"cycle_unit,omitempty"` // day/week/month/year
|
||||||
CycleResetAt string `json:"cycle_reset_at,omitempty"`
|
CycleResetAt string `json:"cycle_reset_at,omitempty"` // 周期起点
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateQuotaReq 局部更新配额。
|
||||||
type UpdateQuotaReq struct {
|
type UpdateQuotaReq struct {
|
||||||
Total *int `json:"total,omitempty"`
|
Total *int `json:"total,omitempty"`
|
||||||
Status *int8 `json:"status,omitempty"`
|
Status *int8 `json:"status,omitempty"`
|
||||||
@@ -228,6 +284,7 @@ type UpdateQuotaReq struct {
|
|||||||
CycleResetAt *string `json:"cycle_reset_at,omitempty"`
|
CycleResetAt *string `json:"cycle_reset_at,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QuotaListQuery 配额列表过滤参数。
|
||||||
type QuotaListQuery struct {
|
type QuotaListQuery struct {
|
||||||
PaginationQuery
|
PaginationQuery
|
||||||
AccountID *uint `json:"account_id,omitempty"`
|
AccountID *uint `json:"account_id,omitempty"`
|
||||||
@@ -235,32 +292,38 @@ type QuotaListQuery struct {
|
|||||||
Status *int8 `json:"status,omitempty"`
|
Status *int8 `json:"status,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QuotaSummary 指定账号的配额汇总信息。
|
||||||
type QuotaSummary struct {
|
type QuotaSummary struct {
|
||||||
AccountID uint `json:"account_id"`
|
AccountID uint `json:"account_id"`
|
||||||
TotalQuota int `json:"total_quota"`
|
TotalQuota int `json:"total_quota"` // 所有配额累加总量
|
||||||
TotalUsed int `json:"total_used"`
|
TotalUsed int `json:"total_used"` // 已用
|
||||||
TotalRemaining int `json:"total_remaining"`
|
TotalRemaining int `json:"total_remaining"` // 剩余
|
||||||
Details []MailQuota `json:"details"`
|
Details []MailQuota `json:"details"` // 每条配额明细
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Channel ---
|
// ============================================================
|
||||||
|
// Channel 通道
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Channel 发件通道,一个通道下可挂多个 SenderAccount,由 Strategy 决定分发逻辑。
|
||||||
type Channel struct {
|
type Channel struct {
|
||||||
GormModel
|
GormModel
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Code string `json:"code"`
|
Code string `json:"code"` // 唯一标识,发件请求中 channel 字段填这个
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Strategy string `json:"strategy"`
|
Strategy string `json:"strategy"` // round_robin / weight / least_used
|
||||||
Status int8 `json:"status"`
|
Status int8 `json:"status"` // 0=禁用 1=启用
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateChannelReq 创建通道。
|
||||||
type CreateChannelReq struct {
|
type CreateChannelReq struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"` // 必填
|
||||||
Code string `json:"code"`
|
Code string `json:"code"` // 必填,唯一
|
||||||
Description string `json:"description,omitempty"`
|
Description string `json:"description,omitempty"`
|
||||||
Strategy string `json:"strategy,omitempty"`
|
Strategy string `json:"strategy,omitempty"` // 默认 round_robin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateChannelReq 局部更新通道。
|
||||||
type UpdateChannelReq struct {
|
type UpdateChannelReq struct {
|
||||||
Name *string `json:"name,omitempty"`
|
Name *string `json:"name,omitempty"`
|
||||||
Description *string `json:"description,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
@@ -268,14 +331,18 @@ type UpdateChannelReq struct {
|
|||||||
Status *int8 `json:"status,omitempty"`
|
Status *int8 `json:"status,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ChannelListQuery 通道列表过滤参数。
|
||||||
type ChannelListQuery struct {
|
type ChannelListQuery struct {
|
||||||
PaginationQuery
|
PaginationQuery
|
||||||
Status *int8 `json:"status,omitempty"`
|
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 {
|
type SenderAccount struct {
|
||||||
GormModel
|
GormModel
|
||||||
ChannelID uint `json:"channel_id"`
|
ChannelID uint `json:"channel_id"`
|
||||||
@@ -286,27 +353,29 @@ type SenderAccount struct {
|
|||||||
SmtpSSL bool `json:"smtp_ssl"`
|
SmtpSSL bool `json:"smtp_ssl"`
|
||||||
FromName string `json:"from_name"`
|
FromName string `json:"from_name"`
|
||||||
FromAddress string `json:"from_address"`
|
FromAddress string `json:"from_address"`
|
||||||
DailyLimit int `json:"daily_limit"`
|
DailyLimit int `json:"daily_limit"` // 每日发送上限,0=不限
|
||||||
DailySent int `json:"daily_sent"`
|
DailySent int `json:"daily_sent"` // 当日已发送数
|
||||||
Weight int `json:"weight"`
|
Weight int `json:"weight"` // Strategy=weight 时使用
|
||||||
Status int8 `json:"status"`
|
Status int8 `json:"status"` // 0=禁用 1=启用
|
||||||
LastCheckAt *time.Time `json:"last_check_at"`
|
LastCheckAt *time.Time `json:"last_check_at"`
|
||||||
LastCheckResult string `json:"last_check_result"`
|
LastCheckResult string `json:"last_check_result"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateSenderReq 创建发信账号。Password 只存密文,不会回显。
|
||||||
type CreateSenderReq struct {
|
type CreateSenderReq struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"` // 必填
|
||||||
SmtpHost string `json:"smtp_host"`
|
SmtpHost string `json:"smtp_host"` // 必填
|
||||||
SmtpPort int `json:"smtp_port"`
|
SmtpPort int `json:"smtp_port"` // 必填
|
||||||
SmtpUser string `json:"smtp_user"`
|
SmtpUser string `json:"smtp_user"` // 必填
|
||||||
SmtpPassword string `json:"smtp_password"`
|
SmtpPassword string `json:"smtp_password"` // 必填
|
||||||
SmtpSSL *bool `json:"smtp_ssl,omitempty"`
|
SmtpSSL *bool `json:"smtp_ssl,omitempty"`
|
||||||
FromName string `json:"from_name,omitempty"`
|
FromName string `json:"from_name,omitempty"`
|
||||||
FromAddress string `json:"from_address"`
|
FromAddress string `json:"from_address"` // 必填
|
||||||
DailyLimit *int `json:"daily_limit,omitempty"`
|
DailyLimit *int `json:"daily_limit,omitempty"`
|
||||||
Weight *int `json:"weight,omitempty"`
|
Weight *int `json:"weight,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateSenderReq 局部更新发信账号。
|
||||||
type UpdateSenderReq struct {
|
type UpdateSenderReq struct {
|
||||||
Name *string `json:"name,omitempty"`
|
Name *string `json:"name,omitempty"`
|
||||||
SmtpHost *string `json:"smtp_host,omitempty"`
|
SmtpHost *string `json:"smtp_host,omitempty"`
|
||||||
@@ -321,91 +390,108 @@ type UpdateSenderReq struct {
|
|||||||
Status *int8 `json:"status,omitempty"`
|
Status *int8 `json:"status,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SenderListQuery 发信账号列表过滤参数。
|
||||||
type SenderListQuery struct {
|
type SenderListQuery struct {
|
||||||
PaginationQuery
|
PaginationQuery
|
||||||
Status *int8 `json:"status,omitempty"`
|
Status *int8 `json:"status,omitempty"`
|
||||||
Keyword string `json:"keyword,omitempty"`
|
Keyword string `json:"keyword,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Audit ---
|
// ============================================================
|
||||||
|
// Audit 审核
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// MailAudit 邮件审核记录。
|
||||||
type MailAudit struct {
|
type MailAudit struct {
|
||||||
GormModel
|
GormModel
|
||||||
MailLogID uint `json:"mail_log_id"`
|
MailLogID uint `json:"mail_log_id"`
|
||||||
UserID int `json:"user_id"`
|
UserID int `json:"user_id"`
|
||||||
AccountID uint `json:"account_id"`
|
AccountID uint `json:"account_id"`
|
||||||
AuditType int8 `json:"audit_type"`
|
AuditType int8 `json:"audit_type"` // 1=自动 2=人工
|
||||||
Action int8 `json:"action"`
|
Action int8 `json:"action"` // 1=通过 2=驳回
|
||||||
RejectReason string `json:"reject_reason"`
|
RejectReason string `json:"reject_reason"`
|
||||||
HitRules string `json:"hit_rules"`
|
HitRules string `json:"hit_rules"` // 命中规则列表 JSON
|
||||||
Auditor string `json:"auditor"`
|
Auditor string `json:"auditor"`
|
||||||
AuditedAt time.Time `json:"audited_at"`
|
AuditedAt time.Time `json:"audited_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AuditPendingQuery 待审核列表过滤参数。
|
||||||
type AuditPendingQuery struct {
|
type AuditPendingQuery struct {
|
||||||
PaginationQuery
|
PaginationQuery
|
||||||
UserID *int `json:"user_id,omitempty"`
|
UserID *int `json:"user_id,omitempty"`
|
||||||
AccountID *uint `json:"account_id,omitempty"`
|
AccountID *uint `json:"account_id,omitempty"`
|
||||||
Keyword string `json:"keyword,omitempty"`
|
Keyword string `json:"keyword,omitempty"` // 模糊匹配主题/收件人
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AuditLogQuery 审核记录列表过滤参数。
|
||||||
type AuditLogQuery struct {
|
type AuditLogQuery struct {
|
||||||
PaginationQuery
|
PaginationQuery
|
||||||
AuditType *int8 `json:"audit_type,omitempty"`
|
AuditType *int8 `json:"audit_type,omitempty"` // 1=自动 2=人工
|
||||||
Action *int8 `json:"action,omitempty"`
|
Action *int8 `json:"action,omitempty"` // 1=通过 2=驳回
|
||||||
UserID *int `json:"user_id,omitempty"`
|
UserID *int `json:"user_id,omitempty"`
|
||||||
StartDate string `json:"start_date,omitempty"`
|
StartDate string `json:"start_date,omitempty"` // YYYY-MM-DD
|
||||||
EndDate string `json:"end_date,omitempty"`
|
EndDate string `json:"end_date,omitempty"` // YYYY-MM-DD
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AuditRejectReq 驳回审核请求体。
|
||||||
type AuditRejectReq struct {
|
type AuditRejectReq struct {
|
||||||
RejectReason string `json:"reject_reason"`
|
RejectReason string `json:"reject_reason"` // 建议填写驳回原因
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BatchAuditApproveReq 批量通过请求体。
|
||||||
type BatchAuditApproveReq struct {
|
type BatchAuditApproveReq struct {
|
||||||
MailLogIDs []uint `json:"mail_log_ids"`
|
MailLogIDs []uint `json:"mail_log_ids"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BatchAuditRejectReq 批量驳回请求体。
|
||||||
type BatchAuditRejectReq struct {
|
type BatchAuditRejectReq struct {
|
||||||
MailLogIDs []uint `json:"mail_log_ids"`
|
MailLogIDs []uint `json:"mail_log_ids"`
|
||||||
RejectReason string `json:"reject_reason"`
|
RejectReason string `json:"reject_reason"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AuditStats 审核概览统计(用于仪表盘)。
|
||||||
type AuditStats struct {
|
type AuditStats struct {
|
||||||
PendingCount int64 `json:"pending_count"`
|
PendingCount int64 `json:"pending_count"` // 待审核数量
|
||||||
TodayDetails []AuditDetailCount `json:"today_details"`
|
TodayDetails []AuditDetailCount `json:"today_details"` // 今日按 type/action 分组
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AuditDetailCount 按 AuditType+Action 分组的计数。
|
||||||
type AuditDetailCount struct {
|
type AuditDetailCount struct {
|
||||||
AuditType int8 `json:"audit_type"`
|
AuditType int8 `json:"audit_type"`
|
||||||
Action int8 `json:"action"`
|
Action int8 `json:"action"`
|
||||||
Count int64 `json:"count"`
|
Count int64 `json:"count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Audit Rule ---
|
// ============================================================
|
||||||
|
// AuditRule 审核规则
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// AuditRule 审核规则。自动审核会按 Priority 从高到低依次评估,
|
||||||
|
// 命中即返回 Action。
|
||||||
type AuditRule struct {
|
type AuditRule struct {
|
||||||
GormModel
|
GormModel
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
RuleType string `json:"rule_type"`
|
RuleType string `json:"rule_type"` // keyword/regex/domain
|
||||||
Target string `json:"target"`
|
Target string `json:"target"` // subject/body/to/from
|
||||||
Condition string `json:"condition"`
|
Condition string `json:"condition"` // 关键词或正则
|
||||||
Action int8 `json:"action"`
|
Action int8 `json:"action"` // 1=自动通过 2=自动驳回 3=转人工
|
||||||
Priority int `json:"priority"`
|
Priority int `json:"priority"` // 数字越大优先级越高
|
||||||
Status int8 `json:"status"`
|
Status int8 `json:"status"` // 0=禁用 1=启用
|
||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateAuditRuleReq 创建规则。
|
||||||
type CreateAuditRuleReq struct {
|
type CreateAuditRuleReq struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"` // 必填
|
||||||
RuleType string `json:"rule_type"`
|
RuleType string `json:"rule_type"` // 必填,keyword/regex/domain
|
||||||
Target string `json:"target"`
|
Target string `json:"target"` // 必填,subject/body/to/from
|
||||||
Condition string `json:"condition"`
|
Condition string `json:"condition"` // 必填
|
||||||
Action int8 `json:"action"`
|
Action int8 `json:"action"` // 必填,1/2/3
|
||||||
Priority *int `json:"priority,omitempty"`
|
Priority *int `json:"priority,omitempty"`
|
||||||
Remark string `json:"remark,omitempty"`
|
Remark string `json:"remark,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateAuditRuleReq 局部更新规则。
|
||||||
type UpdateAuditRuleReq struct {
|
type UpdateAuditRuleReq struct {
|
||||||
Name *string `json:"name,omitempty"`
|
Name *string `json:"name,omitempty"`
|
||||||
RuleType *string `json:"rule_type,omitempty"`
|
RuleType *string `json:"rule_type,omitempty"`
|
||||||
@@ -417,6 +503,7 @@ type UpdateAuditRuleReq struct {
|
|||||||
Remark *string `json:"remark,omitempty"`
|
Remark *string `json:"remark,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestAuditRuleReq 在不发送邮件的前提下测试规则命中情况。
|
||||||
type TestAuditRuleReq struct {
|
type TestAuditRuleReq struct {
|
||||||
Subject string `json:"subject,omitempty"`
|
Subject string `json:"subject,omitempty"`
|
||||||
Body string `json:"body,omitempty"`
|
Body string `json:"body,omitempty"`
|
||||||
@@ -425,24 +512,32 @@ type TestAuditRuleReq struct {
|
|||||||
AccountID uint `json:"account_id,omitempty"`
|
AccountID uint `json:"account_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestAuditRuleResp 规则测试结果。
|
||||||
type TestAuditRuleResp struct {
|
type TestAuditRuleResp struct {
|
||||||
Action string `json:"action"`
|
Action string `json:"action"` // approve/reject/to_manual/none
|
||||||
HitRules []HitRuleEntry `json:"hit_rules"`
|
HitRules []HitRuleEntry `json:"hit_rules"` // 命中的规则列表
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HitRuleEntry 命中规则信息。
|
||||||
type HitRuleEntry struct {
|
type HitRuleEntry struct {
|
||||||
RuleID uint `json:"rule_id"`
|
RuleID uint `json:"rule_id"`
|
||||||
RuleName string `json:"rule_name"`
|
RuleName string `json:"rule_name"`
|
||||||
RuleType string `json:"rule_type"`
|
RuleType string `json:"rule_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Queue ---
|
// ============================================================
|
||||||
|
// Queue 发送队列
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// QueueStatusData 队列状态快照。
|
||||||
|
// - Queues: key = 通道 code, value = 该通道排队长度
|
||||||
|
// - DelayQueue: 延迟重试队列长度
|
||||||
type QueueStatusData struct {
|
type QueueStatusData struct {
|
||||||
Queues map[string]int `json:"queues"`
|
Queues map[string]int `json:"queues"`
|
||||||
DelayQueue int `json:"delay_queue"`
|
DelayQueue int `json:"delay_queue"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QueuePendingQuery 队列待发送邮件过滤参数。
|
||||||
type QueuePendingQuery struct {
|
type QueuePendingQuery struct {
|
||||||
PaginationQuery
|
PaginationQuery
|
||||||
ChannelID *uint `json:"channel_id,omitempty"`
|
ChannelID *uint `json:"channel_id,omitempty"`
|
||||||
@@ -450,20 +545,25 @@ type QueuePendingQuery struct {
|
|||||||
AccountID *uint `json:"account_id,omitempty"`
|
AccountID *uint `json:"account_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Check ---
|
// ============================================================
|
||||||
|
// Check 健康检查
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// CheckLog 发信账号健康检查记录。
|
||||||
|
// 通过向 verification 地址发测试邮件并等待 webhook 回调验证收信可用性。
|
||||||
type CheckLog struct {
|
type CheckLog struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
SenderAccountID uint `json:"sender_account_id"`
|
SenderAccountID uint `json:"sender_account_id"`
|
||||||
VerificationCode string `json:"verification_code"`
|
VerificationCode string `json:"verification_code"`
|
||||||
SentAt time.Time `json:"sent_at"`
|
SentAt time.Time `json:"sent_at"`
|
||||||
Received bool `json:"received"`
|
Received bool `json:"received"` // 是否收到回执
|
||||||
ReceivedAt *time.Time `json:"received_at"`
|
ReceivedAt *time.Time `json:"received_at"`
|
||||||
LatencyMs int `json:"latency_ms"`
|
LatencyMs int `json:"latency_ms"` // 端到端延迟,毫秒
|
||||||
ErrorMessage string `json:"error_message"`
|
ErrorMessage string `json:"error_message"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckLogQuery 健康检查日志过滤参数。
|
||||||
type CheckLogQuery struct {
|
type CheckLogQuery struct {
|
||||||
PaginationQuery
|
PaginationQuery
|
||||||
SenderAccountID *uint `json:"sender_account_id,omitempty"`
|
SenderAccountID *uint `json:"sender_account_id,omitempty"`
|
||||||
@@ -471,6 +571,7 @@ type CheckLogQuery struct {
|
|||||||
EndDate string `json:"end_date,omitempty"`
|
EndDate string `json:"end_date,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SenderHealth 发信账号健康汇总。
|
||||||
type SenderHealth struct {
|
type SenderHealth struct {
|
||||||
SenderAccountID uint `json:"sender_account_id"`
|
SenderAccountID uint `json:"sender_account_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -481,8 +582,9 @@ type SenderHealth struct {
|
|||||||
SuccessChecks int64 `json:"success_checks"`
|
SuccessChecks int64 `json:"success_checks"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TriggerCheckResp 手动触发健康检查的返回。
|
||||||
type TriggerCheckResp struct {
|
type TriggerCheckResp struct {
|
||||||
Result string `json:"result"`
|
Result string `json:"result"` // ok / failed
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
CheckLog *CheckLog `json:"check_log,omitempty"`
|
CheckLog *CheckLog `json:"check_log,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user