youtu_ecpm/router/router.go

256 lines
6.6 KiB
Go
Raw Normal View History

2025-01-13 18:08:17 +08:00
package router
import (
"encoding/json"
"errors"
"fmt"
credential "github.com/bytedance/douyin-openapi-credential-go/client"
openApiSdkClient "github.com/bytedance/douyin-openapi-sdk-go/client"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"io"
"net/http"
"net/url"
"strconv"
"time"
viperConfig "youtu_ecpm/config"
"youtu_ecpm/log"
)
func InitGin(r *gin.Engine) *gin.Engine {
r.GET("/get-ecpm", getEcpm)
r.GET("/code2openId", code2openId)
return r
}
// 定义抖音 API 响应的结构体
type APIResponse 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"`
}
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"`
}
type EcpmReq struct {
OpenId string `json:"open_id" form:"open_id"`
MpId string `json:"mp_id" form:"mp_id" binding:"required"`
}
func code2openId(c *gin.Context) {
code := c.Query("code")
anonymousOpenid := c.Query("anonymous_openid")
openId, err := AppsJsCode2session(code, anonymousOpenid)
if err != nil {
c.JSON(200, gin.H{
"code": 500,
"msg": http.StatusOK,
})
return
}
c.String(http.StatusOK, openId)
}
func getEcpm(c *gin.Context) {
var req EcpmReq
if err := c.ShouldBind(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.OpenId == "" {
c.JSON(http.StatusBadRequest, false)
return
}
// 获取动态的 access_token
accessToken, err := GetAccessToken()
if err != nil {
errorMessage := fmt.Sprintf("获取 access_token 失败: %v", err)
fmt.Println(errorMessage)
c.String(http.StatusInternalServerError, errorMessage)
return
}
res, err := getEcpmFromApi(req, accessToken)
// 计算 ECPM
totalCost := 0
totalRecords := len(res)
for _, record := range res {
totalCost += record.Cost
}
// 如果没有记录,则返回错误
if totalRecords == 0 {
errorMessage := "未找到记录,无法计算 ECPM"
fmt.Println(errorMessage)
c.JSON(http.StatusOK, false)
return
}
// 总 cost / 100000 * 1000 / 总记录数
ecpm := float64(totalCost) / 100000 * 1000 / float64(totalRecords)
// 打印日志
log.ZapLog.Info("ECPM:", zap.Float64("ecpm", ecpm))
// 根据 ECPM 值判断并返回 "true" 或 "false"
var result bool
if ecpm > float64(viperConfig.GetEcpmValue()) && viperConfig.GetEcpmView() > 2 {
result = true
} else {
result = false
}
// 返回结果
c.JSON(http.StatusOK, result)
}
func getEcpmFromApi(req EcpmReq, accessToken string) (list []Record, err error) {
// 构造抖音 API 请求的 URL
apiURL := "https://minigame.zijieapi.com/mgplatform/api/apps/data/get_ecpm"
params := url.Values{}
params.Add("open_id", req.OpenId)
params.Add("mp_id", req.MpId)
fullURL := fmt.Sprintf("%s?open_id=%s&mp_id=%s&access_token=%s&date_hour=%s&page_size=500&page_no=", apiURL, req.OpenId, req.MpId, accessToken, time.Now().Format(time.DateOnly))
pageNo := 1
for {
fullURL += strconv.Itoa(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 APIResponse
err = json.Unmarshal(responseBody, &apiResponse)
if err != nil {
err = errors.New("解析 API 响应数据失败")
return
}
// 检查 API 是否返回错误
if apiResponse.ErrNo != 0 {
errorMessage := fmt.Sprintf("抖音 API 返回错误: %s (错误码: %d)", apiResponse.ErrMsg, apiResponse.ErrNo)
err = errors.New(errorMessage)
return
}
list = append(list, apiResponse.Data.Records...)
if len(apiResponse.Data.Records) <= 500 {
return
}
pageNo++
}
}
func AppsJsCode2session(code, anonymousOpenid string) (openId string, err error) {
/* 构建请求参数该代码示例中只给出部分参数请用户根据需要自行构建参数值
token:
1.若用户自行维护token,将用户维护的token赋值给该参数即可
2.SDK包中有获取token的函数请根据接口path在OpenAPI SDK 总览文档中查找获取token函数的名字
在使用过程中请注意token互刷问题
header:
sdk中默认填充content-type请求头若不需要填充除content-type之外的请求头删除该参数即可
*/
sdkRequest := &openApiSdkClient.AppsJscode2sessionRequest{}
sdkRequest.SetAppid(viperConfig.GetAppId())
if code == "" && anonymousOpenid == "" {
err = errors.New("code can not be empty")
return
}
if code == "" {
sdkRequest.SetAnonymousCode(anonymousOpenid)
} else {
sdkRequest.SetCode(code)
}
sdkRequest.SetSecret(viperConfig.GetSecret())
// sdk调用
sdkResponse, err := sdkClient.AppsJscode2session(sdkRequest)
if err != nil {
return
}
if sdkResponse.Error != nil && *sdkResponse.Error != 0 {
if sdkResponse.Errmsg != nil {
err = errors.New(*sdkResponse.Errmsg)
} else {
err = errors.New("api 未知错误")
}
return
}
if sdkResponse.Openid == nil {
err = errors.New("open id is nil")
return
}
return *sdkResponse.Openid, nil
}
var sdkClient *openApiSdkClient.Client
func init() {
// 初始化 SDK 客户端,设置 app_id 和 secret
opt := new(credential.Config).
SetClientKey(viperConfig.GetAppId()). // 替换为你的 app_id
SetClientSecret(viperConfig.GetSecret()) // 替换为你的 secret
var err error
// 创建 SDK 客户端
sdkClient, err = openApiSdkClient.NewClient(opt)
if err != nil {
panic(err)
}
}
// 获取 access_token 的函数
func GetAccessToken() (accecksToken string, err error) {
// 构造获取 token 的请求参数
sdkRequest := &openApiSdkClient.AppsV2TokenRequest{}
sdkRequest.SetAppid(viperConfig.GetAppId()) // 设置应用 ID
sdkRequest.SetGrantType("client_credential") // 设置授权类型client_credentials 模式)
sdkRequest.SetSecret(viperConfig.GetSecret()) // 设置密钥
// 调用 SDK 获取 access_token
sdkResponse, err := sdkClient.AppsV2Token(sdkRequest)
if err != nil {
return "", fmt.Errorf("SDK 调用失败: %v", err)
}
// 返回 access_token
return *sdkResponse.Data.AccessToken, nil
}