From 69be4bcb827150e3b229c51562cb24746e031bbe Mon Sep 17 00:00:00 2001 From: shiran Date: Fri, 5 Jun 2026 15:03:55 +0800 Subject: [PATCH] feat: init sms-server-cli SDK Co-authored-by: Cursor --- README.md | 272 +++++++++++++++++++++++++++++++++++++++ adapter.go | 13 ++ client.go | 322 ++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 3 + query.go | 72 +++++++++++ quota.go | 62 +++++++++ send.go | 67 ++++++++++ signature.go | 109 ++++++++++++++++ template.go | 142 +++++++++++++++++++++ token.go | 95 ++++++++++++++ types.go | 351 +++++++++++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 1508 insertions(+) create mode 100644 README.md create mode 100644 adapter.go create mode 100644 client.go create mode 100644 go.mod create mode 100644 query.go create mode 100644 quota.go create mode 100644 send.go create mode 100644 signature.go create mode 100644 template.go create mode 100644 token.go create mode 100644 types.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..8c03f20 --- /dev/null +++ b/README.md @@ -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 ` | 服务端对服务端调用 | +| BearerAuth | `NewBearerClient(baseURL, token)` | `Authorization: Bearer ` | 已登录用户调用 | +| UserTokenAuth | `NewUserTokenClient(baseURL, token)` | `Authorization: UserToken ` | 用户令牌调用 | + +## 枚举值 + +### 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 diff --git a/adapter.go b/adapter.go new file mode 100644 index 0000000..d6711af --- /dev/null +++ b/adapter.go @@ -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) +} diff --git a/client.go b/client.go new file mode 100644 index 0000000..e3424f8 --- /dev/null +++ b/client.go @@ -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) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5711f10 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module gitea.s1f.ren/shiran/sms-server-cli + +go 1.22.0 diff --git a/query.go b/query.go new file mode 100644 index 0000000..6f72f61 --- /dev/null +++ b/query.go @@ -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 +} diff --git a/quota.go b/quota.go new file mode 100644 index 0000000..5becde5 --- /dev/null +++ b/quota.go @@ -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 +} diff --git a/send.go b/send.go new file mode 100644 index 0000000..cabc08f --- /dev/null +++ b/send.go @@ -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) +} diff --git a/signature.go b/signature.go new file mode 100644 index 0000000..1619aef --- /dev/null +++ b/signature.go @@ -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 +} diff --git a/template.go b/template.go new file mode 100644 index 0000000..f90cb81 --- /dev/null +++ b/template.go @@ -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 +} diff --git a/token.go b/token.go new file mode 100644 index 0000000..a9af3dd --- /dev/null +++ b/token.go @@ -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 +} diff --git a/types.go b/types.go new file mode 100644 index 0000000..28e5be1 --- /dev/null +++ b/types.go @@ -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"` +}