256 lines
6.6 KiB
Go
256 lines
6.6 KiB
Go
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
|
||
}
|