package douyin_openapi import ( "encoding/json" "errors" "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" "io" "net/http" "strconv" ) const ( code2Session = "/api/apps/v2/jscode2session" // 小程序登录地址 getEcpm = "https://minigame.zijieapi.com/mgplatform/api/apps/data/get_ecpm" // 获取ECPM ) // 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 } type Record struct { Aid string `json:"aid"` Cost int `json:"cost"` Did string `json:"did"` EventName string `json:"event_name"` EventTime string `json:"event_time"` OpenID string `json:"open_id"` ID int `json:"id"` } // GetEcpmParams 获取ECPM参数 type GetEcpmParams struct { AppId string `json:"app_id" form:"app_id"` OpenId string `json:"open_id" form:"open_id"` AccessToken string `json:"access_token" form:"access_token"` DateHour string `json:"date_hour" form:"date_hour"` PageSize int `json:"page_size" form:"page_size"` PageNo int `json:"page_no" form:"page_no"` } // GetEcpmResponseData 获取ECPM响应数据 type GetEcpmResponseData struct { BaseResp struct { StatusCode int `json:"StatusCode"` StatusMessage string `json:"StatusMessage"` } `json:"BaseResp"` Data struct { Records []Record `json:"records"` Total int `json:"total"` } `json:"data"` ErrMsg string `json:"err_msg"` ErrNo int `json:"err_no"` LogID string `json:"log_id"` } // GetEcpm 获取ECPM // https://bytedance.larkoffice.com/docx/Vg4yd0RDSovZINxJDyIc6THhnod // 根据分页大小循环获取,聚合总数返回 func (d *DouYinOpenApi) GetEcpm(params GetEcpmParams) (list []Record, err error) { fullURL := fmt.Sprintf("%s?open_id=%s&mp_id=%s&access_token=%s&date_hour=%s&page_size=500&page_no=", getEcpm, params.OpenId, params.AppId, params.AccessToken, params.DateHour) for { fullURL += strconv.Itoa(params.PageNo) var responseBody []byte func() { // 调用抖音 API var resp *http.Response resp, err = http.Get(fullURL) if err != nil { err = errors.New("调用抖音 API 失败") return } defer resp.Body.Close() // 读取抖音 API 响应数据 responseBody, err = io.ReadAll(resp.Body) if err != nil { err = errors.New("读取抖音 API 响应失败") return } }() // 解析抖音 API 响应数据 var apiResponse GetEcpmResponseData err = json.Unmarshal(responseBody, &apiResponse) if err != nil { err = errors.New("解析 API 响应数据失败") return } // 检查 API 是否返回错误 if apiResponse.ErrNo != 0 { err = fmt.Errorf("抖音 API 返回错误: %s (错误码: %d)", apiResponse.ErrMsg, apiResponse.ErrNo) return } list = append(list, apiResponse.Data.Records...) // 当页数据小于总页数时,无更多数据,返回 if len(apiResponse.Data.Records) <= params.PageSize { return } params.PageNo++ } }