package statisticsclient import ( "context" "encoding/json" "fmt" "net/http" "net/url" "strconv" "strings" "time" ) type ActivityTemplateDataQuery struct { AppCode string TemplateID string PublishedVersion int64 RegionID int64 StartMS int64 EndMS int64 } type ActivityTemplateOverview struct { TemplateID string `json:"template_id"` TemplateCode string `json:"template_code"` PublishedVersion int64 `json:"published_version"` Participants int64 `json:"participants"` VisitCount int64 `json:"visit_count"` GiftUsers int64 `json:"gift_users"` TaskUsers int64 `json:"task_users"` GiftEventCount int64 `json:"gift_event_count"` GiftCount int64 `json:"gift_count"` CoinAmount int64 `json:"coin_amount"` TaskProgress int64 `json:"task_progress"` TaskCompleted int64 `json:"task_completed"` TaskClaimed int64 `json:"task_claimed"` } type ActivityTemplateTrendPoint struct { StatDay string `json:"stat_day"` Participants int64 `json:"participants"` VisitCount int64 `json:"visit_count"` GiftUsers int64 `json:"gift_users"` TaskUsers int64 `json:"task_users"` GiftEventCount int64 `json:"gift_event_count"` GiftCount int64 `json:"gift_count"` CoinAmount int64 `json:"coin_amount"` TaskProgress int64 `json:"task_progress"` TaskCompleted int64 `json:"task_completed"` TaskClaimed int64 `json:"task_claimed"` } type ActivityTemplateGiftStat struct { GiftID string `json:"gift_id"` GiftUsers int64 `json:"gift_users"` GiftEventCount int64 `json:"gift_event_count"` GiftCount int64 `json:"gift_count"` CoinAmount int64 `json:"coin_amount"` } type ActivityTemplateTaskStat struct { TaskKey string `json:"task_key"` TaskType string `json:"task_type"` TaskUsers int64 `json:"task_users"` TaskProgress int64 `json:"task_progress"` TaskCompleted int64 `json:"task_completed"` TaskClaimed int64 `json:"task_claimed"` } // ActivityTemplateData mirrors statistics-service's aggregate-only read model. // Admin intentionally does not reconstruct any of these counters from activity owner tables. type ActivityTemplateData struct { Overview ActivityTemplateOverview `json:"overview"` Trend []ActivityTemplateTrendPoint `json:"trend"` Gifts []ActivityTemplateGiftStat `json:"gifts"` Tasks []ActivityTemplateTaskStat `json:"tasks"` Granularity string `json:"granularity"` Timezone string `json:"timezone"` StartMS int64 `json:"start_ms"` EndMS int64 `json:"end_ms"` RegionID int64 `json:"region_id"` DimensionLimit int `json:"dimension_limit"` GiftSort string `json:"gift_sort"` TaskSort string `json:"task_sort"` ServerTimeMS int64 `json:"server_time_ms"` } type ActivityTemplateDataClient interface { ActivityTemplateData(context.Context, ActivityTemplateDataQuery) (ActivityTemplateData, error) } type HTTPClient struct { baseURL string client *http.Client } func NewHTTP(baseURL string, timeout time.Duration) *HTTPClient { if timeout <= 0 { timeout = 3 * time.Second } return &HTTPClient{ baseURL: strings.TrimRight(strings.TrimSpace(baseURL), "/"), client: &http.Client{Timeout: timeout}, } } func (c *HTTPClient) ActivityTemplateData(ctx context.Context, query ActivityTemplateDataQuery) (ActivityTemplateData, error) { values := url.Values{} values.Set("app_code", strings.TrimSpace(query.AppCode)) values.Set("template_id", strings.TrimSpace(query.TemplateID)) values.Set("published_version", strconv.FormatInt(query.PublishedVersion, 10)) values.Set("start_ms", strconv.FormatInt(query.StartMS, 10)) values.Set("end_ms", strconv.FormatInt(query.EndMS, 10)) if query.RegionID > 0 { values.Set("region_id", strconv.FormatInt(query.RegionID, 10)) } req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/internal/v1/statistics/activity-templates/data?"+values.Encode(), nil) if err != nil { return ActivityTemplateData{}, err } resp, err := c.client.Do(req) if err != nil { return ActivityTemplateData{}, err } defer resp.Body.Close() if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { var body struct { Error string `json:"error"` } _ = json.NewDecoder(resp.Body).Decode(&body) return ActivityTemplateData{}, fmt.Errorf("statistics service returned status %d: %s", resp.StatusCode, strings.TrimSpace(body.Error)) } var out ActivityTemplateData if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { return ActivityTemplateData{}, err } // Arrays must remain arrays for dense table/chart clients; nil would serialize as null and force needless branches. if out.Trend == nil { out.Trend = []ActivityTemplateTrendPoint{} } if out.Gifts == nil { out.Gifts = []ActivityTemplateGiftStat{} } if out.Tasks == nil { out.Tasks = []ActivityTemplateTaskStat{} } return out, nil }