Files
data-engine/service/api_service.go
2026-04-30 13:45:41 +08:00

397 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}