hi-server/pkg/openinstall/openinstall.go
2026-02-08 18:49:14 -08:00

291 lines
8.3 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 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
}