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 }