代码初始化

This commit is contained in:
2026-04-02 11:51:44 +08:00
commit b87244638f
83 changed files with 13084 additions and 0 deletions

396
service/api_service.go Normal file
View File

@@ -0,0 +1,396 @@
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
}