feat: init sms-server-cli SDK
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
# sms-server-cli
|
||||
|
||||
短信服务 Go SDK,用于对接 [sms-server](https://gitea.s1f.ren/shiran/sms-server) 的 HTTP API。
|
||||
|
||||
```
|
||||
模块路径: gitea.s1f.ren/shiran/sms-server-cli
|
||||
Go 版本: 1.21+
|
||||
```
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
go get gitea.s1f.ren/shiran/sms-server-cli
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 使用 ServiceToken(服务端对服务端)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
smscli "gitea.s1f.ren/shiran/sms-server-cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client := smscli.NewServiceClient("https://sms.example.com", "your-service-token")
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建签名
|
||||
sig, err := client.CreateSignature(ctx, smscli.CreateSignatureReq{
|
||||
Title: "我的应用",
|
||||
ApplicantName: "张三",
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("签名 ID: %d\n", sig.ID)
|
||||
|
||||
// 批量发送短信
|
||||
resp, err := client.SendBatch(ctx, smscli.SendBatchReq{
|
||||
SignatureID: sig.ID,
|
||||
TemplateID: 1,
|
||||
Params: map[string]string{"code": "123456"},
|
||||
Phones: []string{"13800138000"},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("消息 ID: %s, 计费条数: %d\n", resp.MsgID, resp.FeeCount)
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 UserToken(用户令牌)
|
||||
|
||||
```go
|
||||
client := smscli.NewUserTokenClient("https://sms.example.com", "your-user-token")
|
||||
|
||||
resp, err := client.SendBatch(ctx, smscli.SendBatchReq{
|
||||
SignatureID: 1,
|
||||
TemplateID: 2,
|
||||
Params: map[string]string{"code": "654321"},
|
||||
Phones: []string{"13900139000"},
|
||||
})
|
||||
```
|
||||
|
||||
## 认证方式
|
||||
|
||||
| 方式 | 构造函数 | Header | 适用场景 |
|
||||
|------|---------|--------|---------|
|
||||
| ServiceAuth | `NewServiceClient(baseURL, token)` | `Authorization: ServiceToken <token>` | 服务端对服务端调用 |
|
||||
| BearerAuth | `NewBearerClient(baseURL, token)` | `Authorization: Bearer <token>` | 已登录用户调用 |
|
||||
| UserTokenAuth | `NewUserTokenClient(baseURL, token)` | `Authorization: UserToken <token>` | 用户令牌调用 |
|
||||
|
||||
## 枚举值
|
||||
|
||||
### ReviewStatus(审核状态)
|
||||
|
||||
| 常量 | 值 | 说明 |
|
||||
|------|---|------|
|
||||
| `ReviewStatusDraft` | 0 | 草稿 |
|
||||
| `ReviewStatusReviewing` | 1 | 审核中 |
|
||||
| `ReviewStatusApproved` | 2 | 已通过 |
|
||||
| `ReviewStatusRejected` | 3 | 已驳回 |
|
||||
|
||||
### QuotaType(额度类型)
|
||||
|
||||
| 常量 | 值 | 说明 |
|
||||
|------|---|------|
|
||||
| `QuotaTypeLongTerm` | 1 | 长期额度 |
|
||||
| `QuotaTypeShortTerm` | 2 | 短期额度 |
|
||||
| `QuotaTypeCycle` | 3 | 周期额度 |
|
||||
|
||||
### SendStatus(发送状态)
|
||||
|
||||
| 常量 | 值 | 说明 |
|
||||
|------|---|------|
|
||||
| `SendStatusPending` | 0 | 待发送 |
|
||||
| `SendStatusSubmitted` | 1 | 已提交 |
|
||||
| `SendStatusSuccess` | 2 | 发送成功 |
|
||||
| `SendStatusFailed` | 3 | 发送失败 |
|
||||
| `SendStatusRejected` | 4 | 已拒绝 |
|
||||
|
||||
### AuthType(认证类型)
|
||||
|
||||
| 常量 | 值 | 说明 |
|
||||
|------|---|------|
|
||||
| `AuthTypeAuthToken` | 1 | 登录令牌 |
|
||||
| `AuthTypeServiceToken` | 2 | 服务令牌 |
|
||||
| `AuthTypeUserToken` | 3 | 用户令牌 |
|
||||
|
||||
## API 参考
|
||||
|
||||
### 签名管理(Signature)
|
||||
|
||||
#### 用户接口
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `CreateSignature(ctx, req)` | 创建签名 |
|
||||
| `ListSignatures(ctx, query)` | 获取签名列表 |
|
||||
| `GetSignature(ctx, id)` | 获取签名详情 |
|
||||
| `UpdateSignature(ctx, id, req)` | 更新签名 |
|
||||
| `DeleteSignature(ctx, id)` | 删除签名 |
|
||||
| `SubmitSignature(ctx, id)` | 提交签名审核 |
|
||||
|
||||
#### 管理员接口
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `AdminListSignatures(ctx, query)` | 获取签名列表(支持 UserID/Status 筛选) |
|
||||
| `AdminGetSignature(ctx, id)` | 获取签名详情 |
|
||||
| `AdminCreateSignature(ctx, req)` | 创建签名(可指定用户) |
|
||||
| `AdminUpdateSignature(ctx, id, req)` | 更新签名 |
|
||||
| `AdminDeleteSignature(ctx, id)` | 删除签名 |
|
||||
| `AdminSubmitSignature(ctx, id)` | 提交签名审核 |
|
||||
| `ApproveSignature(ctx, id)` | 审核通过签名 |
|
||||
| `RejectSignature(ctx, id, req)` | 驳回签名 |
|
||||
|
||||
### 模板管理(Template)
|
||||
|
||||
#### 用户接口
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `CreateTemplate(ctx, req)` | 创建模板 |
|
||||
| `ListTemplates(ctx, query)` | 获取模板列表 |
|
||||
| `GetTemplate(ctx, id)` | 获取模板详情 |
|
||||
| `UpdateTemplate(ctx, id, req)` | 更新模板 |
|
||||
| `DeleteTemplate(ctx, id)` | 删除模板 |
|
||||
| `SubmitTemplate(ctx, id)` | 提交模板审核 |
|
||||
| `ListRecommendedTemplates(ctx, query)` | 获取推荐模板列表 |
|
||||
|
||||
#### 管理员接口
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `AdminListTemplates(ctx, query)` | 获取模板列表(支持 UserID/Status 筛选) |
|
||||
| `AdminListRecommendedTemplates(ctx, query)` | 获取推荐模板列表 |
|
||||
| `AdminGetTemplate(ctx, id)` | 获取模板详情 |
|
||||
| `AdminCreateTemplate(ctx, req)` | 创建模板(可指定用户) |
|
||||
| `AdminUpdateTemplate(ctx, id, req)` | 更新模板 |
|
||||
| `AdminDeleteTemplate(ctx, id)` | 删除模板 |
|
||||
| `AdminSubmitTemplate(ctx, id)` | 提交模板审核 |
|
||||
| `ApproveTemplate(ctx, id)` | 审核通过模板 |
|
||||
| `RejectTemplate(ctx, id, req)` | 驳回模板 |
|
||||
| `CreateRecommendedTemplate(ctx, req)` | 创建推荐模板 |
|
||||
| `UpdateRecommendedTemplate(ctx, id, req)` | 更新推荐模板 |
|
||||
| `DeleteRecommendedTemplate(ctx, id)` | 删除推荐模板 |
|
||||
|
||||
### 用户令牌管理(Token)
|
||||
|
||||
#### 用户接口
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `CreateUserToken(ctx, req)` | 创建令牌 |
|
||||
| `ListUserTokens(ctx, query)` | 获取令牌列表 |
|
||||
| `GetUserToken(ctx, id)` | 获取令牌详情 |
|
||||
| `UpdateUserToken(ctx, id, req)` | 更新令牌 |
|
||||
| `DeleteUserToken(ctx, id)` | 删除令牌 |
|
||||
| `ToggleUserToken(ctx, id)` | 切换令牌启用/禁用 |
|
||||
|
||||
#### 管理员接口
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `AdminListUserTokens(ctx, query)` | 获取令牌列表(支持 UserID/Status 筛选) |
|
||||
| `AdminGetUserToken(ctx, id)` | 获取令牌详情 |
|
||||
| `AdminCreateUserToken(ctx, req)` | 创建令牌(可指定用户) |
|
||||
| `AdminUpdateUserToken(ctx, id, req)` | 更新令牌 |
|
||||
| `AdminDeleteUserToken(ctx, id)` | 删除令牌 |
|
||||
| `AdminToggleUserToken(ctx, id)` | 切换令牌启用/禁用 |
|
||||
|
||||
### 额度管理(Quota)
|
||||
|
||||
#### 用户接口
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `ListQuotas(ctx, query)` | 获取额度列表 |
|
||||
| `GetQuotaSummary(ctx)` | 获取额度汇总 |
|
||||
|
||||
#### 管理员接口
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `AdminListQuotas(ctx, query)` | 获取额度列表(支持 UserID 筛选) |
|
||||
| `AdminGetQuotaSummary(ctx, userID)` | 获取指定用户的额度汇总 |
|
||||
| `CreateQuota(ctx, req)` | 创建额度 |
|
||||
| `UpdateQuota(ctx, id, req)` | 更新额度 |
|
||||
| `DeleteQuota(ctx, id)` | 删除额度 |
|
||||
|
||||
### 发送管理(Send)
|
||||
|
||||
#### 用户接口
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `SendBatch(ctx, req)` | 批量发送(相同内容,多号码) |
|
||||
| `SendMulti(ctx, req)` | 个性化群发(不同参数,多号码) |
|
||||
| `ListSendRecords(ctx, query)` | 获取发送记录列表 |
|
||||
| `GetSendStatus(ctx, msgID)` | 查询发送状态 |
|
||||
|
||||
#### 管理员接口
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `AdminListSendRecords(ctx, query)` | 获取发送记录列表(支持 UserID 筛选) |
|
||||
| `AdminGetSendStatus(ctx, msgID)` | 查询发送状态 |
|
||||
|
||||
### 适配器管理(Adapter)
|
||||
|
||||
#### 管理员接口
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `GetAdapterBalance(ctx)` | 查询适配器余额 |
|
||||
|
||||
## 响应结构
|
||||
|
||||
所有 API 的 HTTP 响应统一为以下 JSON 结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": { }
|
||||
}
|
||||
```
|
||||
|
||||
SDK 会自动处理响应解析,`code != 0` 时返回包含 `msg` 的 error。
|
||||
|
||||
分页列表接口返回 `PaginationResult[T]`:
|
||||
|
||||
```json
|
||||
{
|
||||
"list": [],
|
||||
"total": 100,
|
||||
"page": 1
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package smscli
|
||||
|
||||
import "context"
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 管理员接口
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
// GetAdapterBalance 管理员查询短信适配器余额。
|
||||
// GET /api/sms/admin/adapter/balance
|
||||
func (c *Client) GetAdapterBalance(ctx context.Context) ([]AdapterBalance, error) {
|
||||
return get[[]AdapterBalance](c, ctx, "/api/sms/admin/adapter/balance", nil)
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
package smscli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type authMode int
|
||||
|
||||
const (
|
||||
authModeBearer authMode = 1
|
||||
authModeUserToken authMode = 2
|
||||
)
|
||||
|
||||
// Client 是访问 SMS Server 后端的 HTTP 客户端。
|
||||
//
|
||||
// 构造方式:
|
||||
// - NewServiceClient: 管理端(自动调用 service-token-login 获取 Bearer Token)
|
||||
// - NewBearerClient: 管理端(使用已有的 Bearer Token)
|
||||
// - NewUserTokenClient: 发送端(X-SMS-Token + 请求签名)
|
||||
type Client struct {
|
||||
baseURL string
|
||||
mode authMode
|
||||
bearer string
|
||||
userToken string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// Option 用于在构造 Client 时传入可选配置。
|
||||
type Option func(*Client)
|
||||
|
||||
// WithHTTPClient 使用自定义 *http.Client。
|
||||
func WithHTTPClient(hc *http.Client) Option {
|
||||
return func(c *Client) { c.httpClient = hc }
|
||||
}
|
||||
|
||||
// WithTimeout 修改底层 HTTP 客户端超时(默认 30 秒)。
|
||||
func WithTimeout(d time.Duration) Option {
|
||||
return func(c *Client) { c.httpClient.Timeout = d }
|
||||
}
|
||||
|
||||
// NewServiceClient 创建管理端客户端。
|
||||
//
|
||||
// 自动调用 POST /api/auth/service-token-login 将 serviceToken
|
||||
// 兑换为 Bearer Token,后续请求通过 Authorization: Bearer 认证。
|
||||
// 可访问全部管理接口(签名/模板/Token/额度/发送记录/适配器等)。
|
||||
func NewServiceClient(baseURL, serviceToken string, opts ...Option) (*Client, error) {
|
||||
c := &Client{
|
||||
baseURL: strings.TrimRight(baseURL, "/"),
|
||||
mode: authModeBearer,
|
||||
httpClient: &http.Client{Timeout: 30 * time.Second},
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(c)
|
||||
}
|
||||
|
||||
token, err := c.serviceTokenLogin(context.Background(), serviceToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("service token login: %w", err)
|
||||
}
|
||||
c.bearer = token
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// NewBearerClient 创建使用已有 Bearer Token 的客户端。
|
||||
//
|
||||
// 适用于已通过其他方式获取 Bearer Token 的场景(如用户中心 Token)。
|
||||
func NewBearerClient(baseURL, bearerToken string, opts ...Option) *Client {
|
||||
c := &Client{
|
||||
baseURL: strings.TrimRight(baseURL, "/"),
|
||||
mode: authModeBearer,
|
||||
bearer: bearerToken,
|
||||
httpClient: &http.Client{Timeout: 30 * time.Second},
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// NewUserTokenClient 创建发送端客户端。
|
||||
//
|
||||
// 使用 X-SMS-Token 头认证,POST/PUT 请求体会自动计算 sign 签名。
|
||||
// 主要用于调用短信发送接口,也可管理当前用户的签名/模板/Token 等资源。
|
||||
func NewUserTokenClient(baseURL, userToken string, opts ...Option) *Client {
|
||||
c := &Client{
|
||||
baseURL: strings.TrimRight(baseURL, "/"),
|
||||
mode: authModeUserToken,
|
||||
userToken: userToken,
|
||||
httpClient: &http.Client{Timeout: 30 * time.Second},
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// APIResponse 是后端统一响应体。
|
||||
type APIResponse[T any] struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data T `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// APIError 表示后端业务错误(code != 200)。
|
||||
//
|
||||
// 使用 errors.As 判断:
|
||||
//
|
||||
// var apiErr *smscli.APIError
|
||||
// if errors.As(err, &apiErr) { ... }
|
||||
type APIError struct {
|
||||
Code int
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *APIError) Error() string {
|
||||
return fmt.Sprintf("api error %d: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
func (c *Client) fullURL(path string) string {
|
||||
return c.baseURL + path
|
||||
}
|
||||
|
||||
func (c *Client) setAuth(req *http.Request) {
|
||||
switch c.mode {
|
||||
case authModeBearer:
|
||||
req.Header.Set("Authorization", "Bearer "+c.bearer)
|
||||
case authModeUserToken:
|
||||
req.Header.Set("X-SMS-Token", c.userToken)
|
||||
}
|
||||
}
|
||||
|
||||
// CalcSign 按 sms-server 签名算法计算 sign 值。
|
||||
//
|
||||
// 算法:排序 key → "key:value" 拼接 ";" → 去空白 → base64 → MD5(base64+token)。
|
||||
func CalcSign(body map[string]interface{}, token string) string {
|
||||
keys := make([]string, 0, len(body))
|
||||
for k := range body {
|
||||
if k == "sign" {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
pairs := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
v := body[k]
|
||||
var valStr string
|
||||
switch tv := v.(type) {
|
||||
case string:
|
||||
valStr = tv
|
||||
case json.Number:
|
||||
valStr = tv.String()
|
||||
default:
|
||||
b, _ := json.Marshal(v)
|
||||
valStr = string(b)
|
||||
}
|
||||
pairs = append(pairs, k+":"+valStr)
|
||||
}
|
||||
|
||||
joined := strings.Join(pairs, ";")
|
||||
var sb strings.Builder
|
||||
for _, r := range joined {
|
||||
if !unicode.IsSpace(r) {
|
||||
sb.WriteRune(r)
|
||||
}
|
||||
}
|
||||
b64 := base64.StdEncoding.EncodeToString([]byte(sb.String()))
|
||||
hash := md5.Sum([]byte(b64 + token))
|
||||
return fmt.Sprintf("%x", hash)
|
||||
}
|
||||
|
||||
func structToMap(v interface{}) (map[string]interface{}, error) {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decoder := json.NewDecoder(bytes.NewReader(b))
|
||||
decoder.UseNumber()
|
||||
var m map[string]interface{}
|
||||
if err := decoder.Decode(&m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func doRequest[T any](c *Client, ctx context.Context, method, path string, body interface{}, query url.Values) (T, error) {
|
||||
var zero T
|
||||
|
||||
var bodyReader io.Reader
|
||||
if body != nil {
|
||||
if c.mode == authModeUserToken && (method == http.MethodPost || method == http.MethodPut) {
|
||||
bodyMap, err := structToMap(body)
|
||||
if err != nil {
|
||||
return zero, fmt.Errorf("marshal body to map: %w", err)
|
||||
}
|
||||
bodyMap["sign"] = CalcSign(bodyMap, c.userToken)
|
||||
b, err := json.Marshal(bodyMap)
|
||||
if err != nil {
|
||||
return zero, fmt.Errorf("marshal signed body: %w", err)
|
||||
}
|
||||
bodyReader = bytes.NewReader(b)
|
||||
} else {
|
||||
b, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return zero, fmt.Errorf("marshal body: %w", err)
|
||||
}
|
||||
bodyReader = bytes.NewReader(b)
|
||||
}
|
||||
}
|
||||
|
||||
reqURL := c.fullURL(path)
|
||||
if len(query) > 0 {
|
||||
reqURL += "?" + query.Encode()
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, reqURL, bodyReader)
|
||||
if err != nil {
|
||||
return zero, fmt.Errorf("new request: %w", err)
|
||||
}
|
||||
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
c.setAuth(req)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return zero, fmt.Errorf("do request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return zero, fmt.Errorf("read response: %w", err)
|
||||
}
|
||||
|
||||
var apiResp APIResponse[T]
|
||||
if err := json.Unmarshal(respBody, &apiResp); err != nil {
|
||||
return zero, fmt.Errorf("unmarshal response (status %d): %w\nbody: %s", resp.StatusCode, err, string(respBody))
|
||||
}
|
||||
|
||||
if apiResp.Code != 200 {
|
||||
return zero, &APIError{Code: apiResp.Code, Message: apiResp.Message}
|
||||
}
|
||||
|
||||
return apiResp.Data, nil
|
||||
}
|
||||
|
||||
func (c *Client) serviceTokenLogin(ctx context.Context, serviceToken string) (string, error) {
|
||||
body := map[string]string{"service_token": serviceToken}
|
||||
b, _ := json.Marshal(body)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.fullURL("/api/auth/service-token-login"), bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var apiResp APIResponse[map[string]interface{}]
|
||||
if err := json.Unmarshal(respBody, &apiResp); err != nil {
|
||||
return "", fmt.Errorf("unmarshal login response: %w\nbody: %s", err, string(respBody))
|
||||
}
|
||||
|
||||
if apiResp.Code != 200 {
|
||||
return "", &APIError{Code: apiResp.Code, Message: apiResp.Message}
|
||||
}
|
||||
|
||||
token, ok := apiResp.Data["token"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("login response missing token")
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// Ping 健康检查。
|
||||
//
|
||||
// GET /api/index/ping
|
||||
func (c *Client) Ping(ctx context.Context) error {
|
||||
_, err := get[any](c, ctx, "/api/index/ping", nil)
|
||||
return err
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func del[T any](c *Client, ctx context.Context, path string) (T, error) {
|
||||
return doRequest[T](c, ctx, http.MethodDelete, path, nil, nil)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package smscli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// buildQuery 将 map[string]interface{} 转成 url.Values。
|
||||
//
|
||||
// nil / 空字符串 / 值为 0 的 int/uint 会被忽略;
|
||||
// int8 不做零值过滤(0 可能是合法状态值);
|
||||
// 指针类型为 nil 时忽略,否则取值写入。
|
||||
func buildQuery(params map[string]interface{}) url.Values {
|
||||
q := url.Values{}
|
||||
for k, v := range params {
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
if val != "" {
|
||||
q.Set(k, val)
|
||||
}
|
||||
case int:
|
||||
if val != 0 {
|
||||
q.Set(k, fmt.Sprintf("%d", val))
|
||||
}
|
||||
case int8:
|
||||
q.Set(k, fmt.Sprintf("%d", val))
|
||||
case uint:
|
||||
if val != 0 {
|
||||
q.Set(k, fmt.Sprintf("%d", val))
|
||||
}
|
||||
case *int:
|
||||
if val != nil {
|
||||
q.Set(k, fmt.Sprintf("%d", *val))
|
||||
}
|
||||
case *int8:
|
||||
if val != nil {
|
||||
q.Set(k, fmt.Sprintf("%d", *val))
|
||||
}
|
||||
case *uint:
|
||||
if val != nil {
|
||||
q.Set(k, fmt.Sprintf("%d", *val))
|
||||
}
|
||||
default:
|
||||
q.Set(k, fmt.Sprintf("%v", v))
|
||||
}
|
||||
}
|
||||
return q
|
||||
}
|
||||
|
||||
func paginationParams(p PaginationQuery) map[string]interface{} {
|
||||
m := map[string]interface{}{}
|
||||
if p.Page > 0 {
|
||||
m["page"] = p.Page
|
||||
}
|
||||
if p.PageSize > 0 {
|
||||
m["page_size"] = p.PageSize
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func mergeParams(maps ...map[string]interface{}) map[string]interface{} {
|
||||
result := map[string]interface{}{}
|
||||
for _, m := range maps {
|
||||
for k, v := range m {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package smscli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 用户接口
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
// ListQuotas 获取当前用户的额度列表(分页)。
|
||||
// GET /api/sms/quota/list
|
||||
func (c *Client) ListQuotas(ctx context.Context, q PaginationQuery) (PaginationResult[SmsQuota], error) {
|
||||
params := paginationParams(q)
|
||||
return get[PaginationResult[SmsQuota]](c, ctx, "/api/sms/quota/list", buildQuery(params))
|
||||
}
|
||||
|
||||
// GetQuotaSummary 获取当前用户的额度汇总。
|
||||
// GET /api/sms/quota/summary
|
||||
func (c *Client) GetQuotaSummary(ctx context.Context) (QuotaSummary, error) {
|
||||
return get[QuotaSummary](c, ctx, "/api/sms/quota/summary", nil)
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 管理员接口
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
// AdminListQuotas 管理员获取额度列表,可按 UserID 筛选。
|
||||
// GET /api/sms/admin/quota/list
|
||||
func (c *Client) AdminListQuotas(ctx context.Context, q QuotaListQuery) (PaginationResult[SmsQuota], error) {
|
||||
params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{
|
||||
"user_id": q.UserID,
|
||||
})
|
||||
return get[PaginationResult[SmsQuota]](c, ctx, "/api/sms/admin/quota/list", buildQuery(params))
|
||||
}
|
||||
|
||||
// AdminGetQuotaSummary 管理员获取指定用户的额度汇总。
|
||||
// GET /api/sms/admin/quota/summary
|
||||
func (c *Client) AdminGetQuotaSummary(ctx context.Context, userID uint) (QuotaSummary, error) {
|
||||
q := buildQuery(map[string]interface{}{"user_id": userID})
|
||||
return get[QuotaSummary](c, ctx, "/api/sms/admin/quota/summary", q)
|
||||
}
|
||||
|
||||
// CreateQuota 管理员创建额度。
|
||||
// POST /api/sms/admin/quota
|
||||
func (c *Client) CreateQuota(ctx context.Context, req CreateQuotaReq) (SmsQuota, error) {
|
||||
return post[SmsQuota](c, ctx, "/api/sms/admin/quota", req)
|
||||
}
|
||||
|
||||
// UpdateQuota 管理员更新指定额度。
|
||||
// PUT /api/sms/admin/quota/:id
|
||||
func (c *Client) UpdateQuota(ctx context.Context, id uint, req UpdateQuotaReq) (SmsQuota, error) {
|
||||
return put[SmsQuota](c, ctx, fmt.Sprintf("/api/sms/admin/quota/%d", id), req)
|
||||
}
|
||||
|
||||
// DeleteQuota 管理员删除指定额度。
|
||||
// DELETE /api/sms/admin/quota/:id
|
||||
func (c *Client) DeleteQuota(ctx context.Context, id uint) error {
|
||||
_, err := del[any](c, ctx, fmt.Sprintf("/api/sms/admin/quota/%d", id))
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package smscli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 用户接口
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
// SendBatch 批量发送短信(相同内容发送到多个号码)。
|
||||
// POST /api/sms/send/batch
|
||||
func (c *Client) SendBatch(ctx context.Context, req SendBatchReq) (SendResp, error) {
|
||||
return post[SendResp](c, ctx, "/api/sms/send/batch", req)
|
||||
}
|
||||
|
||||
// SendMulti 个性化群发短信(每个号码使用不同参数)。
|
||||
// POST /api/sms/send/multi
|
||||
func (c *Client) SendMulti(ctx context.Context, req SendMultiReq) (SendResp, error) {
|
||||
return post[SendResp](c, ctx, "/api/sms/send/multi", req)
|
||||
}
|
||||
|
||||
// ListSendRecords 获取当前用户的发送记录列表(分页,支持多条件筛选)。
|
||||
// GET /api/sms/send/record
|
||||
func (c *Client) ListSendRecords(ctx context.Context, q SendRecordQuery) (PaginationResult[SmsSendRecord], error) {
|
||||
params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{
|
||||
"signature_id": q.SignatureID,
|
||||
"template_id": q.TemplateID,
|
||||
"phone": q.Phone,
|
||||
"status": q.Status,
|
||||
"start_time": q.StartTime,
|
||||
"end_time": q.EndTime,
|
||||
})
|
||||
return get[PaginationResult[SmsSendRecord]](c, ctx, "/api/sms/send/record", buildQuery(params))
|
||||
}
|
||||
|
||||
// GetSendStatus 根据消息 ID 查询发送状态。
|
||||
// GET /api/sms/send/status/:msgId
|
||||
func (c *Client) GetSendStatus(ctx context.Context, msgID string) (SmsSendRecord, error) {
|
||||
return get[SmsSendRecord](c, ctx, fmt.Sprintf("/api/sms/send/status/%s", msgID), nil)
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 管理员接口
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
// AdminListSendRecords 管理员获取发送记录列表,可按 UserID 及其他条件筛选。
|
||||
// GET /api/sms/admin/send/record
|
||||
func (c *Client) AdminListSendRecords(ctx context.Context, q AdminSendRecordQuery) (PaginationResult[SmsSendRecord], error) {
|
||||
params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{
|
||||
"user_id": q.UserID,
|
||||
"signature_id": q.SignatureID,
|
||||
"template_id": q.TemplateID,
|
||||
"phone": q.Phone,
|
||||
"status": q.Status,
|
||||
"start_time": q.StartTime,
|
||||
"end_time": q.EndTime,
|
||||
})
|
||||
return get[PaginationResult[SmsSendRecord]](c, ctx, "/api/sms/admin/send/record", buildQuery(params))
|
||||
}
|
||||
|
||||
// AdminGetSendStatus 管理员根据消息 ID 查询发送状态。
|
||||
// GET /api/sms/admin/send/status/:msgId
|
||||
func (c *Client) AdminGetSendStatus(ctx context.Context, msgID string) (SmsSendRecord, error) {
|
||||
return get[SmsSendRecord](c, ctx, fmt.Sprintf("/api/sms/admin/send/status/%s", msgID), nil)
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package smscli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 用户接口
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
// CreateSignature 创建短信签名。
|
||||
// POST /api/sms/signature
|
||||
func (c *Client) CreateSignature(ctx context.Context, req CreateSignatureReq) (SmsSignature, error) {
|
||||
return post[SmsSignature](c, ctx, "/api/sms/signature", req)
|
||||
}
|
||||
|
||||
// ListSignatures 获取当前用户的签名列表(分页)。
|
||||
// GET /api/sms/signature/list
|
||||
func (c *Client) ListSignatures(ctx context.Context, q PaginationQuery) (PaginationResult[SmsSignature], error) {
|
||||
params := paginationParams(q)
|
||||
return get[PaginationResult[SmsSignature]](c, ctx, "/api/sms/signature/list", buildQuery(params))
|
||||
}
|
||||
|
||||
// GetSignature 获取指定签名详情。
|
||||
// GET /api/sms/signature/:id
|
||||
func (c *Client) GetSignature(ctx context.Context, id uint) (SmsSignature, error) {
|
||||
return get[SmsSignature](c, ctx, fmt.Sprintf("/api/sms/signature/%d", id), nil)
|
||||
}
|
||||
|
||||
// UpdateSignature 更新指定签名。
|
||||
// PUT /api/sms/signature/:id
|
||||
func (c *Client) UpdateSignature(ctx context.Context, id uint, req UpdateSignatureReq) (SmsSignature, error) {
|
||||
return put[SmsSignature](c, ctx, fmt.Sprintf("/api/sms/signature/%d", id), req)
|
||||
}
|
||||
|
||||
// DeleteSignature 删除指定签名。
|
||||
// DELETE /api/sms/signature/:id
|
||||
func (c *Client) DeleteSignature(ctx context.Context, id uint) error {
|
||||
_, err := del[any](c, ctx, fmt.Sprintf("/api/sms/signature/%d", id))
|
||||
return err
|
||||
}
|
||||
|
||||
// SubmitSignature 提交签名进入审核。
|
||||
// POST /api/sms/signature/:id/submit
|
||||
func (c *Client) SubmitSignature(ctx context.Context, id uint) error {
|
||||
_, err := post[any](c, ctx, fmt.Sprintf("/api/sms/signature/%d/submit", id), nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 管理员接口
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
// AdminListSignatures 管理员获取签名列表,可按 UserID、Status 筛选。
|
||||
// GET /api/sms/admin/signature/list
|
||||
func (c *Client) AdminListSignatures(ctx context.Context, q SignatureListQuery) (PaginationResult[SmsSignature], error) {
|
||||
params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{
|
||||
"user_id": q.UserID,
|
||||
"status": q.Status,
|
||||
})
|
||||
return get[PaginationResult[SmsSignature]](c, ctx, "/api/sms/admin/signature/list", buildQuery(params))
|
||||
}
|
||||
|
||||
// AdminGetSignature 管理员获取指定签名详情。
|
||||
// GET /api/sms/admin/signature/:id
|
||||
func (c *Client) AdminGetSignature(ctx context.Context, id uint) (SmsSignature, error) {
|
||||
return get[SmsSignature](c, ctx, fmt.Sprintf("/api/sms/admin/signature/%d", id), nil)
|
||||
}
|
||||
|
||||
// AdminCreateSignature 管理员创建签名(可指定 UserID)。
|
||||
// POST /api/sms/admin/signature
|
||||
func (c *Client) AdminCreateSignature(ctx context.Context, req AdminCreateSignatureReq) (SmsSignature, error) {
|
||||
return post[SmsSignature](c, ctx, "/api/sms/admin/signature", req)
|
||||
}
|
||||
|
||||
// AdminUpdateSignature 管理员更新指定签名。
|
||||
// PUT /api/sms/admin/signature/:id
|
||||
func (c *Client) AdminUpdateSignature(ctx context.Context, id uint, req AdminUpdateSignatureReq) (SmsSignature, error) {
|
||||
return put[SmsSignature](c, ctx, fmt.Sprintf("/api/sms/admin/signature/%d", id), req)
|
||||
}
|
||||
|
||||
// AdminDeleteSignature 管理员删除指定签名。
|
||||
// DELETE /api/sms/admin/signature/:id
|
||||
func (c *Client) AdminDeleteSignature(ctx context.Context, id uint) error {
|
||||
_, err := del[any](c, ctx, fmt.Sprintf("/api/sms/admin/signature/%d", id))
|
||||
return err
|
||||
}
|
||||
|
||||
// AdminSubmitSignature 管理员提交签名进入审核。
|
||||
// POST /api/sms/admin/signature/:id/submit
|
||||
func (c *Client) AdminSubmitSignature(ctx context.Context, id uint) error {
|
||||
_, err := post[any](c, ctx, fmt.Sprintf("/api/sms/admin/signature/%d/submit", id), nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// ApproveSignature 管理员审核通过签名。
|
||||
// POST /api/sms/admin/signature/:id/approve
|
||||
func (c *Client) ApproveSignature(ctx context.Context, id uint) error {
|
||||
_, err := post[any](c, ctx, fmt.Sprintf("/api/sms/admin/signature/%d/approve", id), nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// RejectSignature 管理员驳回签名,需提供驳回原因。
|
||||
// POST /api/sms/admin/signature/:id/reject
|
||||
func (c *Client) RejectSignature(ctx context.Context, id uint, req RejectReq) error {
|
||||
_, err := post[any](c, ctx, fmt.Sprintf("/api/sms/admin/signature/%d/reject", id), req)
|
||||
return err
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
package smscli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 用户接口
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
// CreateTemplate 创建短信模板。
|
||||
// POST /api/sms/template
|
||||
func (c *Client) CreateTemplate(ctx context.Context, req CreateTemplateReq) (SmsTemplate, error) {
|
||||
return post[SmsTemplate](c, ctx, "/api/sms/template", req)
|
||||
}
|
||||
|
||||
// ListTemplates 获取当前用户的模板列表(分页)。
|
||||
// GET /api/sms/template/list
|
||||
func (c *Client) ListTemplates(ctx context.Context, q PaginationQuery) (PaginationResult[SmsTemplate], error) {
|
||||
params := paginationParams(q)
|
||||
return get[PaginationResult[SmsTemplate]](c, ctx, "/api/sms/template/list", buildQuery(params))
|
||||
}
|
||||
|
||||
// GetTemplate 获取指定模板详情。
|
||||
// GET /api/sms/template/:id
|
||||
func (c *Client) GetTemplate(ctx context.Context, id uint) (SmsTemplate, error) {
|
||||
return get[SmsTemplate](c, ctx, fmt.Sprintf("/api/sms/template/%d", id), nil)
|
||||
}
|
||||
|
||||
// UpdateTemplate 更新指定模板。
|
||||
// PUT /api/sms/template/:id
|
||||
func (c *Client) UpdateTemplate(ctx context.Context, id uint, req UpdateTemplateReq) (SmsTemplate, error) {
|
||||
return put[SmsTemplate](c, ctx, fmt.Sprintf("/api/sms/template/%d", id), req)
|
||||
}
|
||||
|
||||
// DeleteTemplate 删除指定模板。
|
||||
// DELETE /api/sms/template/:id
|
||||
func (c *Client) DeleteTemplate(ctx context.Context, id uint) error {
|
||||
_, err := del[any](c, ctx, fmt.Sprintf("/api/sms/template/%d", id))
|
||||
return err
|
||||
}
|
||||
|
||||
// SubmitTemplate 提交模板进入审核。
|
||||
// POST /api/sms/template/:id/submit
|
||||
func (c *Client) SubmitTemplate(ctx context.Context, id uint) error {
|
||||
_, err := post[any](c, ctx, fmt.Sprintf("/api/sms/template/%d/submit", id), nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// ListRecommendedTemplates 获取推荐模板列表(分页)。
|
||||
// GET /api/sms/template/recommended
|
||||
func (c *Client) ListRecommendedTemplates(ctx context.Context, q PaginationQuery) (PaginationResult[SmsRecommendedTemplate], error) {
|
||||
params := paginationParams(q)
|
||||
return get[PaginationResult[SmsRecommendedTemplate]](c, ctx, "/api/sms/template/recommended", buildQuery(params))
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 管理员接口
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
// AdminListTemplates 管理员获取模板列表,可按 UserID、Status 筛选。
|
||||
// GET /api/sms/admin/template/list
|
||||
func (c *Client) AdminListTemplates(ctx context.Context, q TemplateListQuery) (PaginationResult[SmsTemplate], error) {
|
||||
params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{
|
||||
"user_id": q.UserID,
|
||||
"status": q.Status,
|
||||
})
|
||||
return get[PaginationResult[SmsTemplate]](c, ctx, "/api/sms/admin/template/list", buildQuery(params))
|
||||
}
|
||||
|
||||
// AdminListRecommendedTemplates 管理员获取推荐模板列表(分页)。
|
||||
// GET /api/sms/admin/template/recommended
|
||||
func (c *Client) AdminListRecommendedTemplates(ctx context.Context, q PaginationQuery) (PaginationResult[SmsRecommendedTemplate], error) {
|
||||
params := paginationParams(q)
|
||||
return get[PaginationResult[SmsRecommendedTemplate]](c, ctx, "/api/sms/admin/template/recommended", buildQuery(params))
|
||||
}
|
||||
|
||||
// AdminGetTemplate 管理员获取指定模板详情。
|
||||
// GET /api/sms/admin/template/:id
|
||||
func (c *Client) AdminGetTemplate(ctx context.Context, id uint) (SmsTemplate, error) {
|
||||
return get[SmsTemplate](c, ctx, fmt.Sprintf("/api/sms/admin/template/%d", id), nil)
|
||||
}
|
||||
|
||||
// AdminCreateTemplate 管理员创建模板(可指定 UserID)。
|
||||
// POST /api/sms/admin/template
|
||||
func (c *Client) AdminCreateTemplate(ctx context.Context, req AdminCreateTemplateReq) (SmsTemplate, error) {
|
||||
return post[SmsTemplate](c, ctx, "/api/sms/admin/template", req)
|
||||
}
|
||||
|
||||
// AdminUpdateTemplate 管理员更新指定模板。
|
||||
// PUT /api/sms/admin/template/:id
|
||||
func (c *Client) AdminUpdateTemplate(ctx context.Context, id uint, req AdminUpdateTemplateReq) (SmsTemplate, error) {
|
||||
return put[SmsTemplate](c, ctx, fmt.Sprintf("/api/sms/admin/template/%d", id), req)
|
||||
}
|
||||
|
||||
// AdminDeleteTemplate 管理员删除指定模板。
|
||||
// DELETE /api/sms/admin/template/:id
|
||||
func (c *Client) AdminDeleteTemplate(ctx context.Context, id uint) error {
|
||||
_, err := del[any](c, ctx, fmt.Sprintf("/api/sms/admin/template/%d", id))
|
||||
return err
|
||||
}
|
||||
|
||||
// AdminSubmitTemplate 管理员提交模板进入审核。
|
||||
// POST /api/sms/admin/template/:id/submit
|
||||
func (c *Client) AdminSubmitTemplate(ctx context.Context, id uint) error {
|
||||
_, err := post[any](c, ctx, fmt.Sprintf("/api/sms/admin/template/%d/submit", id), nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// ApproveTemplate 管理员审核通过模板。
|
||||
// POST /api/sms/admin/template/:id/approve
|
||||
func (c *Client) ApproveTemplate(ctx context.Context, id uint) error {
|
||||
_, err := post[any](c, ctx, fmt.Sprintf("/api/sms/admin/template/%d/approve", id), nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// RejectTemplate 管理员驳回模板,需提供驳回原因。
|
||||
// POST /api/sms/admin/template/:id/reject
|
||||
func (c *Client) RejectTemplate(ctx context.Context, id uint, req RejectReq) error {
|
||||
_, err := post[any](c, ctx, fmt.Sprintf("/api/sms/admin/template/%d/reject", id), req)
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateRecommendedTemplate 管理员创建推荐模板。
|
||||
// POST /api/sms/admin/template/recommended
|
||||
func (c *Client) CreateRecommendedTemplate(ctx context.Context, req CreateRecommendedTemplateReq) (SmsRecommendedTemplate, error) {
|
||||
return post[SmsRecommendedTemplate](c, ctx, "/api/sms/admin/template/recommended", req)
|
||||
}
|
||||
|
||||
// UpdateRecommendedTemplate 管理员更新推荐模板。
|
||||
// PUT /api/sms/admin/template/recommended/:id
|
||||
func (c *Client) UpdateRecommendedTemplate(ctx context.Context, id uint, req UpdateRecommendedTemplateReq) (SmsRecommendedTemplate, error) {
|
||||
return put[SmsRecommendedTemplate](c, ctx, fmt.Sprintf("/api/sms/admin/template/recommended/%d", id), req)
|
||||
}
|
||||
|
||||
// DeleteRecommendedTemplate 管理员删除推荐模板。
|
||||
// DELETE /api/sms/admin/template/recommended/:id
|
||||
func (c *Client) DeleteRecommendedTemplate(ctx context.Context, id uint) error {
|
||||
_, err := del[any](c, ctx, fmt.Sprintf("/api/sms/admin/template/recommended/%d", id))
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package smscli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 用户接口
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
// CreateUserToken 创建用户令牌。
|
||||
// POST /api/sms/token
|
||||
func (c *Client) CreateUserToken(ctx context.Context, req CreateTokenReq) (SmsUserToken, error) {
|
||||
return post[SmsUserToken](c, ctx, "/api/sms/token", req)
|
||||
}
|
||||
|
||||
// ListUserTokens 获取当前用户的令牌列表(分页)。
|
||||
// GET /api/sms/token/list
|
||||
func (c *Client) ListUserTokens(ctx context.Context, q PaginationQuery) (PaginationResult[SmsUserToken], error) {
|
||||
params := paginationParams(q)
|
||||
return get[PaginationResult[SmsUserToken]](c, ctx, "/api/sms/token/list", buildQuery(params))
|
||||
}
|
||||
|
||||
// GetUserToken 获取指定令牌详情。
|
||||
// GET /api/sms/token/:id
|
||||
func (c *Client) GetUserToken(ctx context.Context, id uint) (SmsUserToken, error) {
|
||||
return get[SmsUserToken](c, ctx, fmt.Sprintf("/api/sms/token/%d", id), nil)
|
||||
}
|
||||
|
||||
// UpdateUserToken 更新指定令牌。
|
||||
// PUT /api/sms/token/:id
|
||||
func (c *Client) UpdateUserToken(ctx context.Context, id uint, req UpdateTokenReq) (SmsUserToken, error) {
|
||||
return put[SmsUserToken](c, ctx, fmt.Sprintf("/api/sms/token/%d", id), req)
|
||||
}
|
||||
|
||||
// DeleteUserToken 删除指定令牌。
|
||||
// DELETE /api/sms/token/:id
|
||||
func (c *Client) DeleteUserToken(ctx context.Context, id uint) error {
|
||||
_, err := del[any](c, ctx, fmt.Sprintf("/api/sms/token/%d", id))
|
||||
return err
|
||||
}
|
||||
|
||||
// ToggleUserToken 切换令牌的启用/禁用状态。
|
||||
// POST /api/sms/token/:id/toggle
|
||||
func (c *Client) ToggleUserToken(ctx context.Context, id uint) error {
|
||||
_, err := post[any](c, ctx, fmt.Sprintf("/api/sms/token/%d/toggle", id), nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 管理员接口
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
// AdminListUserTokens 管理员获取令牌列表,可按 UserID、Status 筛选。
|
||||
// GET /api/sms/admin/token/list
|
||||
func (c *Client) AdminListUserTokens(ctx context.Context, q TokenListQuery) (PaginationResult[SmsUserToken], error) {
|
||||
params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{
|
||||
"user_id": q.UserID,
|
||||
"status": q.Status,
|
||||
})
|
||||
return get[PaginationResult[SmsUserToken]](c, ctx, "/api/sms/admin/token/list", buildQuery(params))
|
||||
}
|
||||
|
||||
// AdminGetUserToken 管理员获取指定令牌详情。
|
||||
// GET /api/sms/admin/token/:id
|
||||
func (c *Client) AdminGetUserToken(ctx context.Context, id uint) (SmsUserToken, error) {
|
||||
return get[SmsUserToken](c, ctx, fmt.Sprintf("/api/sms/admin/token/%d", id), nil)
|
||||
}
|
||||
|
||||
// AdminCreateUserToken 管理员创建令牌(可指定 UserID)。
|
||||
// POST /api/sms/admin/token
|
||||
func (c *Client) AdminCreateUserToken(ctx context.Context, req AdminCreateTokenReq) (SmsUserToken, error) {
|
||||
return post[SmsUserToken](c, ctx, "/api/sms/admin/token", req)
|
||||
}
|
||||
|
||||
// AdminUpdateUserToken 管理员更新指定令牌。
|
||||
// PUT /api/sms/admin/token/:id
|
||||
func (c *Client) AdminUpdateUserToken(ctx context.Context, id uint, req UpdateTokenReq) (SmsUserToken, error) {
|
||||
return put[SmsUserToken](c, ctx, fmt.Sprintf("/api/sms/admin/token/%d", id), req)
|
||||
}
|
||||
|
||||
// AdminDeleteUserToken 管理员删除指定令牌。
|
||||
// DELETE /api/sms/admin/token/:id
|
||||
func (c *Client) AdminDeleteUserToken(ctx context.Context, id uint) error {
|
||||
_, err := del[any](c, ctx, fmt.Sprintf("/api/sms/admin/token/%d", id))
|
||||
return err
|
||||
}
|
||||
|
||||
// AdminToggleUserToken 管理员切换令牌的启用/禁用状态。
|
||||
// POST /api/sms/admin/token/:id/toggle
|
||||
func (c *Client) AdminToggleUserToken(ctx context.Context, id uint) error {
|
||||
_, err := post[any](c, ctx, fmt.Sprintf("/api/sms/admin/token/%d/toggle", id), nil)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
package smscli
|
||||
|
||||
import "time"
|
||||
|
||||
// PaginationResult 分页返回。
|
||||
type PaginationResult[T any] struct {
|
||||
List []T `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
}
|
||||
|
||||
// PaginationQuery 分页参数。
|
||||
type PaginationQuery struct {
|
||||
Page int `json:"page,omitempty"`
|
||||
PageSize int `json:"page_size,omitempty"`
|
||||
}
|
||||
|
||||
// GormModel 对应后端 gorm.Model。
|
||||
type GormModel struct {
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
DeletedAt *time.Time `json:"DeletedAt"`
|
||||
}
|
||||
|
||||
const (
|
||||
ReviewStatusDraft = 0
|
||||
ReviewStatusReviewing = 1
|
||||
ReviewStatusApproved = 2
|
||||
ReviewStatusRejected = 3
|
||||
)
|
||||
|
||||
const (
|
||||
QuotaTypeLongTerm = 1
|
||||
QuotaTypeShortTerm = 2
|
||||
QuotaTypeCycle = 3
|
||||
)
|
||||
|
||||
const (
|
||||
SendStatusPending = 0
|
||||
SendStatusSubmitted = 1
|
||||
SendStatusSuccess = 2
|
||||
SendStatusFailed = 3
|
||||
SendStatusRejected = 4
|
||||
)
|
||||
|
||||
const (
|
||||
AuthTypeAuthToken = 1
|
||||
AuthTypeServiceToken = 2
|
||||
AuthTypeUserToken = 3
|
||||
)
|
||||
|
||||
// SmsSignature 短信签名。Status: 0=草稿 1=审核中 2=已通过 3=已驳回
|
||||
type SmsSignature struct {
|
||||
GormModel
|
||||
UserID uint `json:"user_id"`
|
||||
Title string `json:"title"`
|
||||
ApplicantName string `json:"applicant_name"`
|
||||
ApplicantIDCard string `json:"applicant_id_card"`
|
||||
ApplicantCompany string `json:"applicant_company"`
|
||||
LicenseURL string `json:"license_url"`
|
||||
Status int8 `json:"status"`
|
||||
RejectReason string `json:"reject_reason"`
|
||||
ReviewedBy *uint `json:"reviewed_by"`
|
||||
ReviewedAt *time.Time `json:"reviewed_at"`
|
||||
}
|
||||
|
||||
type CreateSignatureReq struct {
|
||||
Title string `json:"title"`
|
||||
ApplicantName string `json:"applicant_name"`
|
||||
ApplicantIDCard string `json:"applicant_id_card,omitempty"`
|
||||
ApplicantCompany string `json:"applicant_company,omitempty"`
|
||||
LicenseURL string `json:"license_url,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateSignatureReq struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
ApplicantName string `json:"applicant_name,omitempty"`
|
||||
ApplicantIDCard string `json:"applicant_id_card,omitempty"`
|
||||
ApplicantCompany string `json:"applicant_company,omitempty"`
|
||||
LicenseURL string `json:"license_url,omitempty"`
|
||||
}
|
||||
|
||||
type AdminCreateSignatureReq struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Title string `json:"title"`
|
||||
ApplicantName string `json:"applicant_name"`
|
||||
ApplicantIDCard string `json:"applicant_id_card,omitempty"`
|
||||
ApplicantCompany string `json:"applicant_company,omitempty"`
|
||||
LicenseURL string `json:"license_url,omitempty"`
|
||||
}
|
||||
|
||||
type AdminUpdateSignatureReq struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
ApplicantName string `json:"applicant_name,omitempty"`
|
||||
ApplicantIDCard string `json:"applicant_id_card,omitempty"`
|
||||
ApplicantCompany string `json:"applicant_company,omitempty"`
|
||||
LicenseURL string `json:"license_url,omitempty"`
|
||||
Status *int8 `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
type RejectReq struct {
|
||||
RejectReason string `json:"reject_reason"`
|
||||
}
|
||||
|
||||
type SignatureListQuery struct {
|
||||
PaginationQuery
|
||||
UserID *uint `json:"user_id,omitempty"`
|
||||
Status *int8 `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
type TemplateParam struct {
|
||||
Key string `json:"key"`
|
||||
Type string `json:"type"`
|
||||
MaxLen int `json:"max_len"`
|
||||
}
|
||||
|
||||
// SmsTemplate 短信模板。Status: 0=草稿 1=审核中 2=已通过 3=已驳回
|
||||
type SmsTemplate struct {
|
||||
GormModel
|
||||
UserID uint `json:"user_id"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
Params []TemplateParam `json:"params"`
|
||||
RecommendedID *uint `json:"recommended_id"`
|
||||
IsModified int8 `json:"is_modified"`
|
||||
Status int8 `json:"status"`
|
||||
RejectReason string `json:"reject_reason"`
|
||||
ReviewedBy *uint `json:"reviewed_by"`
|
||||
ReviewedAt *time.Time `json:"reviewed_at"`
|
||||
}
|
||||
|
||||
type CreateTemplateReq struct {
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
Params []TemplateParam `json:"params,omitempty"`
|
||||
RecommendedID *uint `json:"recommended_id,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateTemplateReq struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Params []TemplateParam `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
type AdminCreateTemplateReq struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
Params []TemplateParam `json:"params,omitempty"`
|
||||
RecommendedID *uint `json:"recommended_id,omitempty"`
|
||||
}
|
||||
|
||||
type AdminUpdateTemplateReq struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Params []TemplateParam `json:"params,omitempty"`
|
||||
Status *int8 `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
type TemplateListQuery struct {
|
||||
PaginationQuery
|
||||
UserID *uint `json:"user_id,omitempty"`
|
||||
Status *int8 `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
type SmsRecommendedTemplate struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
Params []TemplateParam `json:"params"`
|
||||
Category string `json:"category"`
|
||||
CreatedBy uint `json:"created_by"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type CreateRecommendedTemplateReq struct {
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
Params []TemplateParam `json:"params,omitempty"`
|
||||
Category string `json:"category,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateRecommendedTemplateReq struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Params []TemplateParam `json:"params,omitempty"`
|
||||
Category string `json:"category,omitempty"`
|
||||
}
|
||||
|
||||
type SmsUserToken struct {
|
||||
GormModel
|
||||
UserID uint `json:"user_id"`
|
||||
Token string `json:"token"`
|
||||
Name string `json:"name"`
|
||||
QuotaLimit *int `json:"quota_limit"`
|
||||
QuotaUsed int `json:"quota_used"`
|
||||
TemplateIDs []uint `json:"template_ids"`
|
||||
ExpireAt *time.Time `json:"expire_at"`
|
||||
IsActive int8 `json:"is_active"`
|
||||
LastUsedAt *time.Time `json:"last_used_at"`
|
||||
LastUsedIP string `json:"last_used_ip"`
|
||||
}
|
||||
|
||||
type CreateTokenReq struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
QuotaLimit *int `json:"quota_limit,omitempty"`
|
||||
TemplateIDs []uint `json:"template_ids,omitempty"`
|
||||
ExpireAt string `json:"expire_at,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateTokenReq struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
QuotaLimit *int `json:"quota_limit,omitempty"`
|
||||
TemplateIDs []uint `json:"template_ids,omitempty"`
|
||||
ExpireAt string `json:"expire_at,omitempty"`
|
||||
}
|
||||
|
||||
type AdminCreateTokenReq struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
QuotaLimit *int `json:"quota_limit,omitempty"`
|
||||
TemplateIDs []uint `json:"template_ids,omitempty"`
|
||||
ExpireAt string `json:"expire_at,omitempty"`
|
||||
}
|
||||
|
||||
type TokenListQuery struct {
|
||||
PaginationQuery
|
||||
UserID *uint `json:"user_id,omitempty"`
|
||||
Status *int8 `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// SmsQuota 短信额度。QuotaType: 1=长期 2=短期 3=周期
|
||||
type SmsQuota struct {
|
||||
GormModel
|
||||
UserID uint `json:"user_id"`
|
||||
QuotaType int8 `json:"quota_type"`
|
||||
Total int `json:"total"`
|
||||
Used int `json:"used"`
|
||||
ExpireAt *time.Time `json:"expire_at"`
|
||||
CycleUnit string `json:"cycle_unit"`
|
||||
CycleValue int `json:"cycle_value"`
|
||||
NextRefreshAt *time.Time `json:"next_refresh_at"`
|
||||
IsActive int8 `json:"is_active"`
|
||||
}
|
||||
|
||||
type CreateQuotaReq struct {
|
||||
UserID uint `json:"user_id"`
|
||||
QuotaType int8 `json:"quota_type"`
|
||||
Total int `json:"total"`
|
||||
ExpireAt string `json:"expire_at,omitempty"`
|
||||
CycleUnit string `json:"cycle_unit,omitempty"`
|
||||
CycleValue int `json:"cycle_value,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateQuotaReq struct {
|
||||
Total *int `json:"total,omitempty"`
|
||||
ExpireAt string `json:"expire_at,omitempty"`
|
||||
CycleUnit string `json:"cycle_unit,omitempty"`
|
||||
CycleValue *int `json:"cycle_value,omitempty"`
|
||||
IsActive *int8 `json:"is_active,omitempty"`
|
||||
}
|
||||
|
||||
type QuotaListQuery struct {
|
||||
PaginationQuery
|
||||
UserID *uint `json:"user_id,omitempty"`
|
||||
}
|
||||
|
||||
type QuotaSummary struct {
|
||||
TotalRemaining int `json:"total_remaining"`
|
||||
LongTerm int `json:"long_term"`
|
||||
ShortTerm int `json:"short_term"`
|
||||
Cycle int `json:"cycle"`
|
||||
}
|
||||
|
||||
type SendBatchReq struct {
|
||||
UserID uint `json:"user_id,omitempty"`
|
||||
SignatureID uint `json:"signature_id"`
|
||||
TemplateID uint `json:"template_id"`
|
||||
Params map[string]string `json:"params,omitempty"`
|
||||
Phones []string `json:"phones"`
|
||||
Adapter string `json:"adapter,omitempty"`
|
||||
}
|
||||
|
||||
type SendMultiReq struct {
|
||||
UserID uint `json:"user_id,omitempty"`
|
||||
SignatureID uint `json:"signature_id"`
|
||||
TemplateID uint `json:"template_id"`
|
||||
Items []SendMultiItem `json:"items"`
|
||||
Adapter string `json:"adapter,omitempty"`
|
||||
}
|
||||
|
||||
type SendMultiItem struct {
|
||||
Phone string `json:"phone"`
|
||||
Params map[string]string `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
type SendResp struct {
|
||||
RecordID uint `json:"record_id"`
|
||||
MsgID string `json:"msg_id"`
|
||||
FeeCount int `json:"fee_count"`
|
||||
PhoneCount int `json:"phone_count"`
|
||||
}
|
||||
|
||||
type SmsSendRecord struct {
|
||||
ID uint `json:"id"`
|
||||
UserID uint `json:"user_id"`
|
||||
AuthType int8 `json:"auth_type"`
|
||||
TokenID *uint `json:"token_id"`
|
||||
SignatureID uint `json:"signature_id"`
|
||||
TemplateID uint `json:"template_id"`
|
||||
SendType int8 `json:"send_type"`
|
||||
Params map[string]interface{} `json:"params"`
|
||||
FinalContent string `json:"final_content"`
|
||||
PhoneNumbers []string `json:"phone_numbers"`
|
||||
SourceIP string `json:"source_ip"`
|
||||
AdapterName string `json:"adapter_name"`
|
||||
AdapterMsgID string `json:"adapter_msg_id"`
|
||||
Status int8 `json:"status"`
|
||||
CallbackRaw map[string]interface{} `json:"callback_raw"`
|
||||
FeeCount int `json:"fee_count"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type SendRecordQuery struct {
|
||||
PaginationQuery
|
||||
SignatureID uint `json:"signature_id,omitempty"`
|
||||
TemplateID uint `json:"template_id,omitempty"`
|
||||
Phone string `json:"phone,omitempty"`
|
||||
Status *int `json:"status,omitempty"`
|
||||
StartTime string `json:"start_time,omitempty"`
|
||||
EndTime string `json:"end_time,omitempty"`
|
||||
}
|
||||
|
||||
type AdminSendRecordQuery struct {
|
||||
PaginationQuery
|
||||
UserID *uint `json:"user_id,omitempty"`
|
||||
SignatureID uint `json:"signature_id,omitempty"`
|
||||
TemplateID uint `json:"template_id,omitempty"`
|
||||
Phone string `json:"phone,omitempty"`
|
||||
Status *int `json:"status,omitempty"`
|
||||
StartTime string `json:"start_time,omitempty"`
|
||||
EndTime string `json:"end_time,omitempty"`
|
||||
}
|
||||
|
||||
type AdapterBalance struct {
|
||||
Adapter string `json:"adapter"`
|
||||
Balance interface{} `json:"balance"`
|
||||
}
|
||||
Reference in New Issue
Block a user