package providers import ( "context" "encoding/json" "fmt" "net/http" "net/url" "strings" "time" ) // DouyinProvider 抖音 OAuth提供者 // 抖音 OAuth 文档:https://developer.open-douyin.com/docs/resource/zh-CN/dop/develop/openapi/account-permission/get-access-token type DouyinProvider struct { ClientKey string // 抖音开放平台 client_key ClientSecret string // 抖音开放平台 client_secret RedirectURI string } // DouyinTokenResponse 抖音 Token响应 type DouyinTokenResponse struct { Data struct { AccessToken string `json:"access_token"` ExpiresIn int `json:"expires_in"` RefreshToken string `json:"refresh_token"` RefreshExpiresIn int `json:"refresh_expires_in"` OpenID string `json:"open_id"` Scope string `json:"scope"` } `json:"data"` Message string `json:"message"` } // DouyinUserInfo 抖音用户信息 type DouyinUserInfo struct { Data struct { OpenID string `json:"open_id"` UnionID string `json:"union_id"` Nickname string `json:"nickname"` Avatar string `json:"avatar"` Gender int `json:"gender"` // 0:未知 1:男 2:女 Country string `json:"country"` Province string `json:"province"` City string `json:"city"` } `json:"data"` Message string `json:"message"` } // NewDouyinProvider 创建抖音 OAuth提供者 func NewDouyinProvider(clientKey, clientSecret, redirectURI string) *DouyinProvider { return &DouyinProvider{ ClientKey: clientKey, ClientSecret: clientSecret, RedirectURI: redirectURI, } } // GetAuthURL 获取抖音授权URL func (d *DouyinProvider) GetAuthURL(state string) (string, error) { authURL := fmt.Sprintf( "https://open.douyin.com/platform/oauth/connect?client_key=%s&redirect_uri=%s&response_type=code&scope=user_info&state=%s", d.ClientKey, url.QueryEscape(d.RedirectURI), url.QueryEscape(state), ) return authURL, nil } // ExchangeCode 用授权码换取 access_token func (d *DouyinProvider) ExchangeCode(ctx context.Context, code string) (*DouyinTokenResponse, error) { tokenURL := "https://open.douyin.com/oauth/access_token/" data := url.Values{} data.Set("client_key", d.ClientKey) data.Set("client_secret", d.ClientSecret) data.Set("code", code) data.Set("grant_type", "authorization_code") req, err := http.NewRequestWithContext(ctx, "POST", tokenURL, strings.NewReader(data.Encode())) if err != nil { return nil, fmt.Errorf("create request failed: %w", err) } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() body, err := readOAuthResponseBody(resp) if err != nil { return nil, fmt.Errorf("read response failed: %w", err) } var tokenResp DouyinTokenResponse if err := json.Unmarshal(body, &tokenResp); err != nil { return nil, fmt.Errorf("parse token response failed: %w", err) } if tokenResp.Data.AccessToken == "" { return nil, fmt.Errorf("抖音 OAuth: %s", tokenResp.Message) } return &tokenResp, nil } // GetUserInfo 获取抖音用户信息 func (d *DouyinProvider) GetUserInfo(ctx context.Context, accessToken, openID string) (*DouyinUserInfo, error) { userInfoURL := fmt.Sprintf("https://open.douyin.com/oauth/userinfo/?open_id=%s&access_token=%s", url.QueryEscape(openID), url.QueryEscape(accessToken)) req, err := http.NewRequestWithContext(ctx, "GET", userInfoURL, nil) if err != nil { return nil, fmt.Errorf("create request failed: %w", err) } client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() body, err := readOAuthResponseBody(resp) if err != nil { return nil, fmt.Errorf("read response failed: %w", err) } var userInfo DouyinUserInfo if err := json.Unmarshal(body, &userInfo); err != nil { return nil, fmt.Errorf("parse user info failed: %w", err) } return &userInfo, nil }