first commit
This commit is contained in:
commit
9ab243733d
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
.idea
|
132
access-token/access_token.go
Normal file
132
access-token/access_token.go
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
package access_token
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"gitea.youtukeji.com.cn/xiabin/douyin-openapi/cache"
|
||||||
|
"gitea.youtukeji.com.cn/xiabin/douyin-openapi/util"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 正式地址
|
||||||
|
const accessTokenURL = "https://developer.toutiao.com/api/apps/v2/token"
|
||||||
|
|
||||||
|
// 沙盒地址
|
||||||
|
const sandBoxTokenURL = "https://open-sandbox.douyin.com/api/apps/v2/token"
|
||||||
|
|
||||||
|
// AccessToken 管理AccessToken 的基础接口
|
||||||
|
type AccessToken interface {
|
||||||
|
GetCacheKey() string // 获取缓存的key
|
||||||
|
SetCacheKey(key string) // 设置缓存key
|
||||||
|
GetAccessToken() (string, error) // 获取token
|
||||||
|
}
|
||||||
|
|
||||||
|
// accessTokenLock 初始化全局锁防止并发获取token
|
||||||
|
var accessTokenLock = new(sync.Mutex)
|
||||||
|
|
||||||
|
// DefaultAccessToken 默认的token管理类
|
||||||
|
type DefaultAccessToken struct {
|
||||||
|
AppId string // app_id string 是 小程序的 app_id
|
||||||
|
AppSecret string // app_secret string 是 小程序的密钥
|
||||||
|
GrantType string // grant_type string 是 固定值“client_credentials”
|
||||||
|
Cache cache.Cache // 缓存组件
|
||||||
|
accessTokenLock *sync.Mutex // 读写锁
|
||||||
|
accessTokenCacheKey string // 缓存的key
|
||||||
|
SandBox bool // 是否沙盒地址 默认 false 线上地址
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDefaultAccessToken 实例化默认的token管理类
|
||||||
|
func NewDefaultAccessToken(appId, appSecret string, cache cache.Cache, IsSandbox bool) AccessToken {
|
||||||
|
if cache == nil {
|
||||||
|
panic(any("cache is need"))
|
||||||
|
}
|
||||||
|
token := &DefaultAccessToken{
|
||||||
|
AppId: appId,
|
||||||
|
AppSecret: appSecret,
|
||||||
|
GrantType: "client_credential",
|
||||||
|
Cache: cache,
|
||||||
|
accessTokenCacheKey: fmt.Sprintf("douyin_openapi_access_token_%s", appId),
|
||||||
|
accessTokenLock: accessTokenLock,
|
||||||
|
SandBox: IsSandbox,
|
||||||
|
}
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCacheKey 获取缓存key
|
||||||
|
func (dd *DefaultAccessToken) GetCacheKey() string {
|
||||||
|
return dd.accessTokenCacheKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCacheKey 设置缓存key
|
||||||
|
func (dd *DefaultAccessToken) SetCacheKey(key string) {
|
||||||
|
dd.accessTokenCacheKey = key
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAccessToken 获取token
|
||||||
|
func (dd *DefaultAccessToken) GetAccessToken() (string, error) {
|
||||||
|
// 先尝试从缓存中获取如果不存在就调用接口获取
|
||||||
|
if val := dd.Cache.Get(dd.GetCacheKey()); val != nil {
|
||||||
|
return val.(string), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加锁防止并发获取接口
|
||||||
|
dd.accessTokenLock.Lock()
|
||||||
|
defer dd.accessTokenLock.Unlock()
|
||||||
|
|
||||||
|
// 双捡防止重复获取
|
||||||
|
if val := dd.Cache.Get(dd.GetCacheKey()); val != nil {
|
||||||
|
return val.(string), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始调用接口获取token
|
||||||
|
api := accessTokenURL
|
||||||
|
if dd.SandBox {
|
||||||
|
api = sandBoxTokenURL
|
||||||
|
}
|
||||||
|
reqAccessToken, err := GetTokenFromServer(api, dd.AppId, dd.AppSecret)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// 设置缓存
|
||||||
|
expires := reqAccessToken.Data.ExpiresIn - 1500
|
||||||
|
err = dd.Cache.Set(dd.GetCacheKey(), reqAccessToken.Data.AccessToken, time.Duration(expires)*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return reqAccessToken.Data.AccessToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResAccessToken 获取token的返回结构体
|
||||||
|
type ResAccessToken struct {
|
||||||
|
ErrNo int `json:"err_no,omitempty"`
|
||||||
|
ErrTips string `json:"err_tips,omitempty"`
|
||||||
|
Data ResAccessTokenData `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResAccessTokenData struct {
|
||||||
|
AccessToken string `json:"access_token,omitempty"`
|
||||||
|
ExpiresIn int `json:"expires_in,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTokenFromServer 从抖音服务器获取token
|
||||||
|
func GetTokenFromServer(apiUrl string, appId, appSecret string) (resAccessToken ResAccessToken, err error) {
|
||||||
|
params := map[string]interface{}{
|
||||||
|
"appid": appId,
|
||||||
|
"secret": appSecret,
|
||||||
|
"grant_type": "client_credential",
|
||||||
|
}
|
||||||
|
body, err := util.PostJSON(apiUrl, params)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(body, &resAccessToken)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if resAccessToken.ErrNo != 0 {
|
||||||
|
err = fmt.Errorf("get access_token error : errcode=%v , errormsg=%v", resAccessToken.ErrTips, resAccessToken.ErrNo)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
76
cache/cache.go
vendored
Normal file
76
cache/cache.go
vendored
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cache any
|
||||||
|
type Cache interface {
|
||||||
|
Get(key string) any
|
||||||
|
Set(key string, val any, timeout time.Duration) error
|
||||||
|
IsExist(key string) bool
|
||||||
|
Delete(key string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// data 存储数据用的
|
||||||
|
type data struct {
|
||||||
|
Data any
|
||||||
|
Expired time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory 实现一个内存缓存
|
||||||
|
type Memory struct {
|
||||||
|
sync.Map
|
||||||
|
}
|
||||||
|
|
||||||
|
var m = sync.Map{}
|
||||||
|
|
||||||
|
// NewMemory 实例化一个内存缓存器
|
||||||
|
func NewMemory() Cache {
|
||||||
|
return &Memory{
|
||||||
|
sync.Map{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 获取缓存的值
|
||||||
|
func (mem *Memory) Get(key string) any {
|
||||||
|
if val, ok := mem.Load(key); ok {
|
||||||
|
val := val.(*data)
|
||||||
|
// 判断缓存是否过期
|
||||||
|
if val.Expired.Before(time.Now()) {
|
||||||
|
// 删除这个key
|
||||||
|
_ = mem.Delete(key)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return val.Data
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set 设置一个值
|
||||||
|
func (mem *Memory) Set(key string, val any, timeout time.Duration) error {
|
||||||
|
mem.Store(key, &data{
|
||||||
|
Data: val,
|
||||||
|
Expired: time.Now().Add(timeout),
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsExist 判断值是否存在
|
||||||
|
func (mem *Memory) IsExist(key string) bool {
|
||||||
|
if val, ok := mem.Load(key); ok {
|
||||||
|
val := val.(*data)
|
||||||
|
if val.Expired.Before(time.Now()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除一个值
|
||||||
|
func (mem *Memory) Delete(key string) error {
|
||||||
|
return mem.Delete(key)
|
||||||
|
|
||||||
|
}
|
106
douyin_openapi.go
Normal file
106
douyin_openapi.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
package douyin_openapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
accessToken "gitea.youtukeji.com.cn/xiabin/douyin-openapi/access-token"
|
||||||
|
"gitea.youtukeji.com.cn/xiabin/douyin-openapi/cache"
|
||||||
|
"gitea.youtukeji.com.cn/xiabin/douyin-openapi/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
code2Session = "/api/apps/v2/jscode2session" // 小程序登录地址
|
||||||
|
)
|
||||||
|
|
||||||
|
// DouYinOpenApiConfig 实例化配置
|
||||||
|
type DouYinOpenApiConfig struct {
|
||||||
|
AppId string
|
||||||
|
AppSecret string
|
||||||
|
AccessToken accessToken.AccessToken
|
||||||
|
Cache cache.Cache
|
||||||
|
IsSandbox bool
|
||||||
|
Token string
|
||||||
|
Salt string
|
||||||
|
}
|
||||||
|
|
||||||
|
// DouYinOpenApi 基类
|
||||||
|
type DouYinOpenApi struct {
|
||||||
|
Config DouYinOpenApiConfig
|
||||||
|
BaseApi string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDouYinOpenApi 实例化一个抖音openapi实例
|
||||||
|
func NewDouYinOpenApi(config DouYinOpenApiConfig) *DouYinOpenApi {
|
||||||
|
if config.Cache == nil {
|
||||||
|
config.Cache = cache.NewMemory()
|
||||||
|
}
|
||||||
|
if config.AccessToken == nil {
|
||||||
|
config.AccessToken = accessToken.NewDefaultAccessToken(config.AppId, config.AppSecret, config.Cache, config.IsSandbox)
|
||||||
|
}
|
||||||
|
BaseApi := "https://developer.toutiao.com"
|
||||||
|
if config.IsSandbox {
|
||||||
|
BaseApi = "https://open-sandbox.douyin.com"
|
||||||
|
}
|
||||||
|
return &DouYinOpenApi{
|
||||||
|
Config: config,
|
||||||
|
BaseApi: BaseApi,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetApiUrl 获取api地址
|
||||||
|
func (d *DouYinOpenApi) GetApiUrl(url string) string {
|
||||||
|
return fmt.Sprintf("%s%s", d.BaseApi, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostJson 封装公共的请求方法
|
||||||
|
func (d *DouYinOpenApi) PostJson(api string, params any, response any) (err error) {
|
||||||
|
body, err := util.PostJSON(api, params)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(body, &response)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Code2SessionParams 小程序登录 所需参数
|
||||||
|
type Code2SessionParams struct {
|
||||||
|
Appid string `json:"appid,omitempty"`
|
||||||
|
Secret string `json:"secret,omitempty"`
|
||||||
|
AnonymousCode string `json:"anonymous_code,omitempty"`
|
||||||
|
Code string `json:"code,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Code2SessionResponse 小程序登录返回值
|
||||||
|
type Code2SessionResponse struct {
|
||||||
|
ErrNo int `json:"err_no,omitempty"`
|
||||||
|
ErrTips string `json:"err_tips,omitempty"`
|
||||||
|
Data Code2SessionResponseData `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Code2SessionResponseData struct {
|
||||||
|
SessionKey string `json:"session_key,omitempty"`
|
||||||
|
Openid string `json:"openid,omitempty"`
|
||||||
|
AnonymousOpenid string `json:"anonymous_openid,omitempty"`
|
||||||
|
UnionId string `json:"unionid,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Code2Session 小程序登录
|
||||||
|
func (d *DouYinOpenApi) Code2Session(code, anonymousCode string) (code2SessionResponse Code2SessionResponse, err error) {
|
||||||
|
params := Code2SessionParams{
|
||||||
|
Appid: d.Config.AppId,
|
||||||
|
Secret: d.Config.AppSecret,
|
||||||
|
AnonymousCode: anonymousCode,
|
||||||
|
Code: code,
|
||||||
|
}
|
||||||
|
err = d.PostJson(d.GetApiUrl(code2Session), params, &code2SessionResponse)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if code2SessionResponse.ErrNo != 0 {
|
||||||
|
return code2SessionResponse, fmt.Errorf("小程序登录错误: %s %d", code2SessionResponse.ErrTips, code2SessionResponse.ErrNo)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
62
douyin_openapi_test.go
Normal file
62
douyin_openapi_test.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package douyin_openapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
accessToken "gitea.youtukeji.com.cn/xiabin/douyin-openapi/access-token"
|
||||||
|
"gitea.youtukeji.com.cn/xiabin/douyin-openapi/cache"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 声明测试常量
|
||||||
|
const (
|
||||||
|
AppId = ""
|
||||||
|
AppSecret = ""
|
||||||
|
Token = ""
|
||||||
|
Salt = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
// 声明一个缓存实例
|
||||||
|
var Cache cache.Cache
|
||||||
|
|
||||||
|
// 声明全局openApi实例
|
||||||
|
var OpenApi *DouYinOpenApi
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Cache = cache.NewMemory()
|
||||||
|
OpenApi = NewDouYinOpenApi(DouYinOpenApiConfig{
|
||||||
|
AppId: AppId,
|
||||||
|
AppSecret: AppSecret,
|
||||||
|
IsSandbox: false,
|
||||||
|
Token: Token,
|
||||||
|
Salt: Salt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试获取新的token
|
||||||
|
func TestDouyinOpenapi_NewDefaultAccessToken(t *testing.T) {
|
||||||
|
token := accessToken.NewDefaultAccessToken(AppId, AppSecret, Cache, true)
|
||||||
|
getAccessToken, err := token.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("got a error: %s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Logf("got a value: %s", getAccessToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基准测试看获取token的次数?
|
||||||
|
func BenchmarkDouyinOpenapi_NewDefaultAccessToken(b *testing.B) {
|
||||||
|
token := accessToken.NewDefaultAccessToken(AppId, AppSecret, Cache, true)
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
getAccessToken, err := token.GetAccessToken()
|
||||||
|
b.Logf("get token: %s %+v", getAccessToken, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试小程序登录
|
||||||
|
func TestDouYinOpenApi_Code2Session(t *testing.T) {
|
||||||
|
session, err := OpenApi.Code2Session("1111", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("got a error %s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Logf("got a value %+v", session)
|
||||||
|
}
|
3
go.mod
Normal file
3
go.mod
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module gitea.youtukeji.com.cn/xiabin/douyin-openapi
|
||||||
|
|
||||||
|
go 1.23.4
|
27
util/http.go
Normal file
27
util/http.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PostJSON post json 数据请求
|
||||||
|
func PostJSON(uri string, obj any) ([]byte, error) {
|
||||||
|
marshal, err := json.Marshal(obj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
response, err := http.Post(uri, "application/json;charset=utf-8", bytes.NewBuffer(marshal))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
if response.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("http get error : uri=%v , statusCode=%v", uri, response.StatusCode)
|
||||||
|
}
|
||||||
|
return io.ReadAll(response.Body)
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user