397 lines
10 KiB
Go
397 lines
10 KiB
Go
package service
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io/ioutil"
|
||
"math"
|
||
"net/http"
|
||
"time"
|
||
|
||
entities "cid/entities"
|
||
)
|
||
|
||
// APIService API服务接口
|
||
type APIService interface {
|
||
GetCampaignReport(params *entities.RequestParams) (*entities.APIResponse, error)
|
||
GetCampaignReportByTimeRange(advertiserID int64, startTime, endTime int64) (*entities.APIResponse, error)
|
||
GetAllCampaignReport(params *entities.RequestParams) (*entities.APIResponse, error)
|
||
GetAllCampaignReportByTimeRange(advertiserID int64, startTime, endTime int64) (*entities.APIResponse, error)
|
||
GetCampaignReportWithCallback(params *entities.RequestParams, callback func(*entities.APIResponse, error) bool) error
|
||
}
|
||
|
||
// KuaishouAPIService 快手API服务实现
|
||
type KuaishouAPIService struct {
|
||
BaseURL string
|
||
AccessToken string
|
||
HTTPClient *http.Client
|
||
MaxRetries int
|
||
RetryDelay time.Duration
|
||
RateLimit time.Duration // 请求间隔,避免触发限流
|
||
}
|
||
|
||
// NewKuaishouAPIService 创建API服务实例
|
||
func NewKuaishouAPIService(accessToken string) *KuaishouAPIService {
|
||
return &KuaishouAPIService{
|
||
BaseURL: "https://ad.e.kuaishou.com/rest/openapi/gw/esp/report",
|
||
AccessToken: accessToken,
|
||
HTTPClient: &http.Client{
|
||
Timeout: 30 * time.Second,
|
||
},
|
||
MaxRetries: 3,
|
||
RetryDelay: 2 * time.Second,
|
||
RateLimit: 200 * time.Millisecond, // 默认200ms间隔
|
||
}
|
||
}
|
||
|
||
// SetRateLimit 设置请求间隔
|
||
func (s *KuaishouAPIService) SetRateLimit(delay time.Duration) {
|
||
s.RateLimit = delay
|
||
}
|
||
|
||
// GetCampaignReport 获取广告计划报表(单页)
|
||
func (s *KuaishouAPIService) GetCampaignReport(params *entities.RequestParams) (*entities.APIResponse, error) {
|
||
for retry := 0; retry <= s.MaxRetries; retry++ {
|
||
response, err := s.doRequest(params)
|
||
if err == nil {
|
||
return response, nil
|
||
}
|
||
|
||
// 判断是否重试
|
||
if retry < s.MaxRetries {
|
||
time.Sleep(s.RetryDelay)
|
||
continue
|
||
}
|
||
|
||
return nil, err
|
||
}
|
||
|
||
return nil, fmt.Errorf("请求失败,已达到最大重试次数")
|
||
}
|
||
|
||
// doRequest 执行实际请求
|
||
func (s *KuaishouAPIService) doRequest(params *entities.RequestParams) (*entities.APIResponse, error) {
|
||
// 序列化请求参数
|
||
jsonData, err := json.Marshal(params)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("序列化失败: %v", err)
|
||
}
|
||
|
||
// 创建HTTP请求
|
||
url := fmt.Sprintf("%s/campaignReport", s.BaseURL)
|
||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
||
if err != nil {
|
||
return nil, fmt.Errorf("创建请求失败: %v", err)
|
||
}
|
||
|
||
// 设置请求头
|
||
s.setHeaders(req)
|
||
|
||
// 发送请求
|
||
resp, err := s.HTTPClient.Do(req)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("请求失败: %v", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
// 检查HTTP状态码
|
||
if resp.StatusCode != http.StatusOK {
|
||
body, _ := ioutil.ReadAll(resp.Body)
|
||
return nil, fmt.Errorf("HTTP状态码错误: %d, 响应: %s", resp.StatusCode, string(body))
|
||
}
|
||
|
||
// 读取响应
|
||
body, err := ioutil.ReadAll(resp.Body)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("读取响应失败: %v", err)
|
||
}
|
||
|
||
// 解析响应
|
||
var apiResponse entities.APIResponse
|
||
err = json.Unmarshal(body, &apiResponse)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("解析响应失败: %v", err)
|
||
}
|
||
|
||
// 检查API返回码
|
||
if apiResponse.Code != 0 {
|
||
return nil, fmt.Errorf("API返回错误: %s (code: %d)", apiResponse.Message, apiResponse.Code)
|
||
}
|
||
|
||
return &apiResponse, nil
|
||
}
|
||
|
||
// GetAllCampaignReport 获取所有分页数据
|
||
func (s *KuaishouAPIService) GetAllCampaignReport(params *entities.RequestParams) (*entities.APIResponse, error) {
|
||
// 1. 先获取第一页数据,得到总数
|
||
params.ResetPage()
|
||
firstPage, err := s.GetCampaignReport(params)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("获取第一页数据失败: %v", err)
|
||
}
|
||
|
||
if firstPage.Data == nil {
|
||
return firstPage, nil // 没有数据
|
||
}
|
||
|
||
totalCount := firstPage.Data.TotalCount
|
||
pageSize := params.PageInfo.PageSize
|
||
|
||
// 2. 如果只有一页,直接返回
|
||
if totalCount <= pageSize {
|
||
return firstPage, nil
|
||
}
|
||
|
||
// 3. 计算总页数
|
||
totalPages := int(math.Ceil(float64(totalCount) / float64(pageSize)))
|
||
|
||
// 4. 创建结果集
|
||
allResponses := make([]*entities.APIResponse, 0, totalPages)
|
||
allResponses = append(allResponses, firstPage)
|
||
|
||
// 5. 并发获取剩余页数据
|
||
pageChan := make(chan *pageResult, totalPages-1)
|
||
|
||
// 启动goroutine获取剩余页
|
||
for page := 2; page <= totalPages; page++ {
|
||
go func(currentPage int) {
|
||
// 复制参数并设置页码
|
||
pageParams := *params
|
||
pageParams.PageInfo = &entities.PageInfo{
|
||
CurrentPage: currentPage,
|
||
PageSize: pageSize,
|
||
}
|
||
|
||
response, err := s.GetCampaignReport(&pageParams)
|
||
pageChan <- &pageResult{
|
||
Page: currentPage,
|
||
Response: response,
|
||
Error: err,
|
||
}
|
||
|
||
// 控制请求频率
|
||
time.Sleep(s.RateLimit)
|
||
}(page)
|
||
}
|
||
|
||
// 6. 收集结果
|
||
errors := make([]error, 0)
|
||
for i := 0; i < totalPages-1; i++ {
|
||
result := <-pageChan
|
||
if result.Error != nil {
|
||
errors = append(errors, fmt.Errorf("第%d页获取失败: %v", result.Page, result.Error))
|
||
} else {
|
||
allResponses = append(allResponses, result.Response)
|
||
}
|
||
}
|
||
|
||
// 7. 合并数据
|
||
mergedResponse := entities.MergeData(allResponses)
|
||
|
||
// 8. 如果有错误,返回错误信息
|
||
if len(errors) > 0 {
|
||
// 可以记录错误日志,但继续返回已获取的数据
|
||
fmt.Printf("部分页面获取失败: %v\n", errors)
|
||
}
|
||
|
||
return mergedResponse, nil
|
||
}
|
||
|
||
// GetAllCampaignReportByTimeRange 根据时间范围获取所有数据
|
||
func (s *KuaishouAPIService) GetAllCampaignReportByTimeRange(advertiserID int64, startTime, endTime int64) (*entities.APIResponse, error) {
|
||
params := entities.NewRequestParams()
|
||
params.AdvertiserID = advertiserID
|
||
params.StartTime = startTime
|
||
params.EndTime = endTime
|
||
|
||
return s.GetAllCampaignReport(params)
|
||
}
|
||
|
||
// GetCampaignReportWithCallback 使用回调函数获取所有数据(流式处理,避免内存占用过大)
|
||
func (s *KuaishouAPIService) GetCampaignReportWithCallback(params *entities.RequestParams, callback func(*entities.APIResponse, error) bool) error {
|
||
// 1. 先获取第一页数据,得到总数
|
||
params.ResetPage()
|
||
firstPage, err := s.GetCampaignReport(params)
|
||
if err != nil {
|
||
return fmt.Errorf("获取第一页数据失败: %v", err)
|
||
}
|
||
|
||
// 调用回调处理第一页
|
||
if !callback(firstPage, nil) {
|
||
return nil // 回调要求停止
|
||
}
|
||
|
||
if firstPage.Data == nil || firstPage.Data.TotalCount <= params.PageInfo.PageSize {
|
||
return nil // 只有一页
|
||
}
|
||
|
||
totalCount := firstPage.Data.TotalCount
|
||
pageSize := params.PageInfo.PageSize
|
||
totalPages := int(math.Ceil(float64(totalCount) / float64(pageSize)))
|
||
|
||
// 2. 顺序获取剩余页数据(避免并发导致API限流)
|
||
for page := 2; page <= totalPages; page++ {
|
||
// 复制参数并设置页码
|
||
pageParams := *params
|
||
pageParams.PageInfo = &entities.PageInfo{
|
||
CurrentPage: page,
|
||
PageSize: pageSize,
|
||
}
|
||
|
||
response, err := s.GetCampaignReport(&pageParams)
|
||
if !callback(response, err) {
|
||
return nil // 回调要求停止
|
||
}
|
||
|
||
// 控制请求频率
|
||
time.Sleep(s.RateLimit)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// GetCampaignReportSequential 顺序获取所有数据(简单实现)
|
||
func (s *KuaishouAPIService) GetCampaignReportSequential(params *entities.RequestParams) (*entities.APIResponse, error) {
|
||
// 1. 先获取第一页数据,得到总数
|
||
params.ResetPage()
|
||
firstPage, err := s.GetCampaignReport(params)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("获取第一页数据失败: %v", err)
|
||
}
|
||
|
||
if firstPage.Data == nil {
|
||
return firstPage, nil
|
||
}
|
||
|
||
totalCount := firstPage.Data.TotalCount
|
||
pageSize := params.PageInfo.PageSize
|
||
|
||
// 2. 如果只有一页,直接返回
|
||
if totalCount <= pageSize {
|
||
return firstPage, nil
|
||
}
|
||
|
||
// 3. 顺序获取所有数据
|
||
allDetails := make([]*entities.ReportDetail, 0, totalCount)
|
||
if firstPage.Data.Detail != nil {
|
||
allDetails = append(allDetails, firstPage.Data.Detail...)
|
||
}
|
||
|
||
totalPages := int(math.Ceil(float64(totalCount) / float64(pageSize)))
|
||
|
||
for page := 2; page <= totalPages; page++ {
|
||
// 复制参数并设置页码
|
||
pageParams := *params
|
||
pageParams.PageInfo = &entities.PageInfo{
|
||
CurrentPage: page,
|
||
PageSize: pageSize,
|
||
}
|
||
|
||
response, err := s.GetCampaignReport(&pageParams)
|
||
if err != nil {
|
||
// 记录错误但继续获取
|
||
fmt.Printf("第%d页获取失败: %v\n", page, err)
|
||
continue
|
||
}
|
||
|
||
if response.Data != nil && response.Data.Detail != nil {
|
||
allDetails = append(allDetails, response.Data.Detail...)
|
||
}
|
||
|
||
// 控制请求频率
|
||
time.Sleep(s.RateLimit)
|
||
}
|
||
|
||
// 4. 构建完整响应
|
||
completeResponse := &entities.APIResponse{
|
||
Code: firstPage.Code,
|
||
Message: firstPage.Message,
|
||
Data: &entities.ReportData{
|
||
Sum: firstPage.Data.Sum,
|
||
Detail: allDetails,
|
||
TotalCount: totalCount,
|
||
},
|
||
}
|
||
|
||
return completeResponse, nil
|
||
}
|
||
|
||
// setHeaders 设置请求头
|
||
func (s *KuaishouAPIService) setHeaders(req *http.Request) {
|
||
req.Header.Set("Content-Type", "application/json")
|
||
req.Header.Set("Accept", "application/json")
|
||
if s.AccessToken != "" {
|
||
req.Header.Set("Access-Token", s.AccessToken)
|
||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.AccessToken))
|
||
}
|
||
}
|
||
|
||
// pageResult 分页结果
|
||
type pageResult struct {
|
||
Page int
|
||
Response *entities.APIResponse
|
||
Error error
|
||
}
|
||
|
||
// ReportStatistics 报表统计
|
||
type ReportStatistics struct {
|
||
TotalImpression int64
|
||
TotalClick int64
|
||
TotalCost float64
|
||
TotalGMV float64
|
||
TotalT0GMV float64
|
||
TotalT0OrderCnt int64
|
||
PageCount int
|
||
RecordCount int
|
||
}
|
||
|
||
// CalculateStatistics 计算统计信息
|
||
func CalculateStatistics(response *entities.APIResponse) *ReportStatistics {
|
||
stats := &ReportStatistics{}
|
||
|
||
if response.Data == nil {
|
||
return stats
|
||
}
|
||
|
||
stats.PageCount = 1 // 默认值
|
||
stats.RecordCount = response.Data.TotalCount
|
||
|
||
if response.Data.Detail != nil {
|
||
for _, detail := range response.Data.Detail {
|
||
stats.TotalImpression += detail.Impression
|
||
stats.TotalClick += detail.Click
|
||
stats.TotalCost += detail.CostTotal
|
||
stats.TotalGMV += detail.GMV
|
||
stats.TotalT0GMV += detail.T0GMV
|
||
stats.TotalT0OrderCnt += detail.T0OrderCnt
|
||
}
|
||
}
|
||
|
||
return stats
|
||
}
|
||
|
||
// GetAverageMetrics 获取平均指标
|
||
func (stats *ReportStatistics) GetAverageMetrics() map[string]float64 {
|
||
metrics := make(map[string]float64)
|
||
|
||
if stats.TotalImpression > 0 {
|
||
metrics["ctr"] = float64(stats.TotalClick) / float64(stats.TotalImpression) * 100
|
||
}
|
||
|
||
if stats.TotalClick > 0 {
|
||
metrics["cpc"] = stats.TotalCost / float64(stats.TotalClick)
|
||
}
|
||
|
||
if stats.TotalCost > 0 {
|
||
metrics["roi"] = stats.TotalGMV / stats.TotalCost
|
||
metrics["t0_roi"] = stats.TotalT0GMV / stats.TotalCost
|
||
}
|
||
|
||
if stats.TotalImpression > 0 {
|
||
metrics["cpm"] = stats.TotalCost / float64(stats.TotalImpression) * 1000
|
||
}
|
||
|
||
return metrics
|
||
}
|