package openinstall import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "time" ) var ( // OpenInstall 数据接口基础 URL apiBaseURL = "https://data.openinstall.com" ) // Client for OpenInstall API type Client struct { apiKey string httpClient *http.Client } // NewClient creates a new OpenInstall client // apiKey: OpenInstall 数据接口 ApiKey func NewClient(apiKey string) *Client { return &Client{ apiKey: apiKey, httpClient: &http.Client{ Timeout: 10 * time.Second, }, } } // PlatformStats represents statistics for a specific platform type PlatformStats struct { Platform string `json:"platform"` Count int64 `json:"count"` // 下载/安装量 } // PlatformDownloads 各端下载量统计 type PlatformDownloads struct { Total int64 `json:"total"` // 总量 IOS int64 `json:"ios"` // iOS Android int64 `json:"android"` // Android Windows int64 `json:"windows"` // Windows Mac int64 `json:"mac"` // Mac Comparison *MonthComparison `json:"comparison"` // 环比数据 } // MonthComparison 月度对比数据 type MonthComparison struct { LastMonthTotal int64 `json:"lastMonthTotal"` // 上月总量 Change int64 `json:"change"` // 变化量 (正数=增长, 负数=下降) ChangePercent float64 `json:"changePercent"` // 变化百分比 } // APIResponse 通用响应结构 type APIResponse struct { Code int `json:"code"` Error *string `json:"error"` Body json.RawMessage `json:"body"` } // GrowthData 新增安装数据 type GrowthData struct { Date string `json:"date"` Visit int64 `json:"visit"` // 访问量 Click int64 `json:"click"` // 点击量 Install int64 `json:"install"` // 安装量 Register int64 `json:"register"` // 注册量 SurviveD1 int64 `json:"survive_d1"` // 1日留存 SurviveD7 int64 `json:"survive_d7"` // 7日留存 SurviveD30 int64 `json:"survive_d30"` // 30日留存 } // DistributionData 设备分布数据 type DistributionData struct { Key string `json:"key"` Value int64 `json:"value"` } // GetPlatformDownloads 获取各端下载量统计(当月数据 + 环比) func (c *Client) GetPlatformDownloads(ctx context.Context, channel string) (*PlatformDownloads, error) { now := time.Now() // 当月数据:本月1号到今天 startOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) endOfMonth := now // 上月数据:上月1号到上月最后一天 startOfLastMonth := startOfMonth.AddDate(0, -1, 0) endOfLastMonth := startOfMonth.AddDate(0, 0, -1) // 获取当月各平台数据 currentMonthData, err := c.getPlatformData(ctx, startOfMonth, endOfMonth, channel) if err != nil { return nil, fmt.Errorf("failed to get current month data: %w", err) } // 获取上月各平台数据 lastMonthData, err := c.getPlatformData(ctx, startOfLastMonth, endOfLastMonth, channel) if err != nil { return nil, fmt.Errorf("failed to get last month data: %w", err) } // 计算总量 currentTotal := currentMonthData.IOS + currentMonthData.Android + currentMonthData.Windows + currentMonthData.Mac lastTotal := lastMonthData.IOS + lastMonthData.Android + lastMonthData.Windows + lastMonthData.Mac // 计算环比 change := currentTotal - lastTotal changePercent := float64(0) if lastTotal > 0 { changePercent = (float64(change) / float64(lastTotal)) * 100 } return &PlatformDownloads{ Total: currentTotal, IOS: currentMonthData.IOS, Android: currentMonthData.Android, Windows: currentMonthData.Windows, Mac: currentMonthData.Mac, Comparison: &MonthComparison{ LastMonthTotal: lastTotal, Change: change, ChangePercent: changePercent, }, }, nil } // getPlatformData 获取指定时间范围内各平台的数据 func (c *Client) getPlatformData(ctx context.Context, startDate, endDate time.Time, channel string) (*PlatformDownloads, error) { result := &PlatformDownloads{} // 获取 iOS 数据 iosData, err := c.getDeviceDistribution(ctx, startDate, endDate, "ios", "total", channel) if err != nil { return nil, fmt.Errorf("failed to get iOS data: %w", err) } for _, item := range iosData { result.IOS += item.Value } // 获取 Android 数据 androidData, err := c.getDeviceDistribution(ctx, startDate, endDate, "android", "total", channel) if err != nil { return nil, fmt.Errorf("failed to get Android data: %w", err) } for _, item := range androidData { result.Android += item.Value } // Windows 和 Mac 暂时设为 0 (需要从其他数据源获取) result.Windows = 0 result.Mac = 0 return result, nil } // getDeviceDistribution 获取设备分布数据 func (c *Client) getDeviceDistribution(ctx context.Context, startDate, endDate time.Time, platform, sumBy, channel string) ([]DistributionData, error) { apiURL := fmt.Sprintf("%s/data/sum/growth", apiBaseURL) params := url.Values{} params.Add("apiKey", c.apiKey) params.Add("beginDate", startDate.Format("2006-01-02")) params.Add("endDate", endDate.Format("2006-01-02")) params.Add("platform", platform) params.Add("sumBy", sumBy) if channel != "" { params.Add("channelCode", channel) } params.Add("excludeDuplication", "0") fullURL := fmt.Sprintf("%s?%s", apiURL, params.Encode()) req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to send request: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response: %w", err) } var apiResp APIResponse if err := json.Unmarshal(body, &apiResp); err != nil { return nil, fmt.Errorf("failed to parse response: %w", err) } if apiResp.Code != 0 { errMsg := "unknown error" if apiResp.Error != nil { errMsg = *apiResp.Error } return nil, fmt.Errorf("API error (code=%d): %s", apiResp.Code, errMsg) } var distData []DistributionData if err := json.Unmarshal(apiResp.Body, &distData); err != nil { return nil, fmt.Errorf("failed to parse distribution data: %w", err) } return distData, nil } // GetPlatformStats 获取平台统计数据(兼容旧接口) func (c *Client) GetPlatformStats(ctx context.Context, startDate, endDate time.Time) ([]PlatformStats, error) { // 调用新增安装数据接口 growthData, err := c.GetGrowthData(ctx, startDate, endDate, "total") if err != nil { return nil, fmt.Errorf("failed to get growth data: %w", err) } // 如果没有数据,返回空列表 if len(growthData) == 0 { return []PlatformStats{}, nil } // 合并所有数据 var totalVisits, totalClicks int64 for _, data := range growthData { totalVisits += data.Visit totalClicks += data.Click } return []PlatformStats{ { Platform: "All", Count: totalVisits + totalClicks, }, }, nil } // GetGrowthData 获取新增安装数据 func (c *Client) GetGrowthData(ctx context.Context, startDate, endDate time.Time, statType string) ([]GrowthData, error) { apiURL := fmt.Sprintf("%s/data/event/growth", apiBaseURL) params := url.Values{} params.Add("apiKey", c.apiKey) params.Add("startDate", startDate.Format("2006-01-02")) params.Add("endDate", endDate.Format("2006-01-02")) params.Add("statType", statType) fullURL := fmt.Sprintf("%s?%s", apiURL, params.Encode()) req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to send request: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response: %w", err) } var apiResp APIResponse if err := json.Unmarshal(body, &apiResp); err != nil { return nil, fmt.Errorf("failed to parse response: %w", err) } if apiResp.Code != 0 { errMsg := "unknown error" if apiResp.Error != nil { errMsg = *apiResp.Error } return nil, fmt.Errorf("API error (code=%d): %s", apiResp.Code, errMsg) } var growthData []GrowthData if err := json.Unmarshal(apiResp.Body, &growthData); err != nil { return nil, fmt.Errorf("failed to parse growth data: %w", err) } return growthData, nil }