commit 69be4bcb827150e3b229c51562cb24746e031bbe Author: shiran Date: Fri Jun 5 15:03:55 2026 +0800 feat: init sms-server-cli SDK Co-authored-by: Cursor 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"` +}