add:添加短链接服务
Build docker and publish / build (20.15.1) (push) Has been cancelled

This commit is contained in:
2026-01-24 00:32:08 -08:00
parent a98fcbfe73
commit 1d81df6664
51 changed files with 2957 additions and 621 deletions
+157
View File
@@ -0,0 +1,157 @@
package kutt
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
// Client 是 Kutt API 客户端
// 用于创建和管理短链接
type Client struct {
apiURL string // Kutt API 基础 URL
apiKey string // API 认证密钥
httpClient *http.Client // HTTP 客户端
}
// NewClient 创建一个新的 Kutt 客户端
//
// 参数:
// - apiURL: Kutt API 基础 URL (例如: https://kutt.it/api/v2)
// - apiKey: Kutt API 密钥
//
// 返回:
// - *Client: Kutt 客户端实例
func NewClient(apiURL, apiKey string) *Client {
return &Client{
apiURL: apiURL,
apiKey: apiKey,
httpClient: &http.Client{
Timeout: 10 * time.Second,
},
}
}
// CreateLinkRequest 创建短链接的请求参数
type CreateLinkRequest struct {
Target string `json:"target"` // 目标 URL (必填)
Description string `json:"description,omitempty"` // 链接描述 (可选)
ExpireIn string `json:"expire_in,omitempty"` // 过期时间,例如 "2 days" (可选)
Password string `json:"password,omitempty"` // 访问密码 (可选)
CustomURL string `json:"customurl,omitempty"` // 自定义短链后缀 (可选)
Reuse bool `json:"reuse,omitempty"` // 如果目标 URL 已存在则复用 (可选)
Domain string `json:"domain,omitempty"` // 自定义域名 (可选)
}
// Link 短链接响应结构
type Link struct {
ID string `json:"id"` // 链接 UUID
Address string `json:"address"` // 短链地址后缀
Banned bool `json:"banned"` // 是否被封禁
CreatedAt time.Time `json:"created_at"` // 创建时间
Link string `json:"link"` // 完整短链接 URL
Password bool `json:"password"` // 是否有密码保护
Target string `json:"target"` // 目标 URL
Description string `json:"description"` // 链接描述
UpdatedAt time.Time `json:"updated_at"` // 更新时间
VisitCount int `json:"visit_count"` // 访问次数
}
// ErrorResponse Kutt API 错误响应
type ErrorResponse struct {
Error string `json:"error"`
Message string `json:"message"`
}
// CreateShortLink 创建短链接
//
// 参数:
// - ctx: 上下文
// - req: 创建请求参数
//
// 返回:
// - *Link: 创建的短链接信息
// - error: 错误信息
func (c *Client) CreateShortLink(ctx context.Context, req *CreateLinkRequest) (*Link, error) {
// 序列化请求体
body, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("marshal request failed: %w", err)
}
// 创建 HTTP 请求
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.apiURL+"/links", bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request failed: %w", err)
}
// 设置请求头
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("X-API-KEY", c.apiKey)
// 发送请求
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("send request failed: %w", err)
}
defer resp.Body.Close()
// 读取响应体
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read response failed: %w", err)
}
// 检查响应状态
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
var errResp ErrorResponse
if err := json.Unmarshal(respBody, &errResp); err == nil && errResp.Error != "" {
return nil, fmt.Errorf("kutt api error: %s - %s", errResp.Error, errResp.Message)
}
return nil, fmt.Errorf("kutt api error: status %d, body: %s", resp.StatusCode, string(respBody))
}
// 解析响应
var link Link
if err := json.Unmarshal(respBody, &link); err != nil {
return nil, fmt.Errorf("unmarshal response failed: %w", err)
}
return &link, nil
}
// CreateInviteShortLink 为邀请码创建短链接
// 这是一个便捷方法,用于生成邀请链接
//
// 参数:
// - ctx: 上下文
// - baseURL: 注册页面基础 URL (例如: https://gethifast.net)
// - inviteCode: 邀请码
// - domain: 短链接域名 (例如: getsapp.net),可为空
//
// 返回:
// - string: 短链接 URL
// - error: 错误信息
func (c *Client) CreateInviteShortLink(ctx context.Context, baseURL, inviteCode, domain string) (string, error) {
// 构建目标 URL - 落地页
// 格式:https://gethifast.net/?ic=邀请码
targetURL := fmt.Sprintf("%s?ic=%s", baseURL, inviteCode)
req := &CreateLinkRequest{
Target: targetURL,
Description: fmt.Sprintf("Invite link for code: %s", inviteCode),
Reuse: true, // 如果已存在相同目标 URL 则复用
Domain: domain,
}
link, err := c.CreateShortLink(ctx, req)
if err != nil {
return "", err
}
return link.Link, nil
}
+14 -10
View File
@@ -24,22 +24,26 @@ func SystemConfigSliceReflectToStruct(slice []*system.System, structType any) {
}
if field.IsValid() && field.CanSet() {
switch config.Type {
case "string":
switch field.Kind() {
case reflect.String:
field.SetString(config.Value)
case "bool":
case reflect.Bool:
boolValue, _ := strconv.ParseBool(config.Value)
field.SetBool(boolValue)
case "int":
intValue, _ := strconv.Atoi(config.Value)
field.SetInt(int64(intValue))
case "int64":
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
intValue, _ := strconv.ParseInt(config.Value, 10, 64)
field.SetInt(intValue)
case "interface":
_ = json.Unmarshal([]byte(config.Value), field.Addr().Interface())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
uintValue, _ := strconv.ParseUint(config.Value, 10, 64)
field.SetUint(uintValue)
case reflect.Float32, reflect.Float64:
floatValue, _ := strconv.ParseFloat(config.Value, 64)
field.SetFloat(floatValue)
default:
break
// For interface, struct, map, slice, array - try JSON unmarshal
if config.Value != "" {
_ = json.Unmarshal([]byte(config.Value), field.Addr().Interface())
}
}
}
}