package service import ( "bytes" "encoding/json" "fmt" "io/ioutil" "math" "net/http" "time" entities "dataengine/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 }