214 lines
5.7 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 dashboard
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"hyapp-admin-server/internal/config"
"hyapp-admin-server/internal/integration/userclient"
"hyapp-admin-server/internal/repository"
)
type DashboardService struct {
store *repository.Store
cfg config.Config
httpClient *http.Client
user userclient.Client
}
type StatisticsQuery struct {
AppCode string
StatTZ string
StartMS int64
EndMS int64
SeriesStartMS int64
SeriesEndMS int64
RegionID int64
CountryID int64
}
type SelfGameStatisticsQuery struct {
AppCode string
StatTZ string
RequestID string
StartMS int64
EndMS int64
RegionID int64
CountryID int64
GameID string
}
func NewService(store *repository.Store, cfg config.Config, user userclient.Client) *DashboardService {
return &DashboardService{
store: store,
cfg: cfg,
httpClient: &http.Client{Timeout: cfg.StatisticsService.RequestTimeout},
user: user,
}
}
func (s *DashboardService) Overview() (*repository.DashboardOverview, error) {
return s.store.DashboardOverview()
}
func (s *DashboardService) StatisticsOverview(ctx context.Context, query StatisticsQuery) (map[string]any, error) {
values := url.Values{}
values.Set("app_code", query.AppCode)
if statTZ := strings.TrimSpace(query.StatTZ); statTZ != "" {
values.Set("stat_tz", statTZ)
}
if query.StartMS > 0 {
values.Set("start_ms", strconv.FormatInt(query.StartMS, 10))
}
if query.EndMS > 0 {
values.Set("end_ms", strconv.FormatInt(query.EndMS, 10))
}
if query.SeriesStartMS > 0 {
values.Set("series_start_ms", strconv.FormatInt(query.SeriesStartMS, 10))
}
if query.SeriesEndMS > 0 {
values.Set("series_end_ms", strconv.FormatInt(query.SeriesEndMS, 10))
}
if query.RegionID > 0 {
values.Set("region_id", strconv.FormatInt(query.RegionID, 10))
}
if query.CountryID > 0 {
values.Set("country_id", strconv.FormatInt(query.CountryID, 10))
}
return s.statisticsGET(ctx, "/internal/v1/statistics/overview", values)
}
func (s *DashboardService) SelfGameStatisticsOverview(ctx context.Context, query SelfGameStatisticsQuery) (map[string]any, error) {
values := url.Values{}
values.Set("app_code", query.AppCode)
if statTZ := strings.TrimSpace(query.StatTZ); statTZ != "" {
values.Set("stat_tz", statTZ)
}
if query.StartMS > 0 {
values.Set("start_ms", strconv.FormatInt(query.StartMS, 10))
}
if query.EndMS > 0 {
values.Set("end_ms", strconv.FormatInt(query.EndMS, 10))
}
if query.RegionID > 0 {
values.Set("region_id", strconv.FormatInt(query.RegionID, 10))
}
if query.CountryID > 0 {
values.Set("country_id", strconv.FormatInt(query.CountryID, 10))
}
if gameID := strings.TrimSpace(query.GameID); gameID != "" && gameID != "all" {
values.Set("game_id", gameID)
}
// 数据大屏只通过 statistics-service 读取自研游戏聚合指标,避免 admin 服务绕过聚合层去扫业务明细表。
overview, err := s.statisticsGET(ctx, "/internal/v1/statistics/self-games/overview", values)
if err != nil {
return nil, err
}
// 高风险用户只对 statistics-service 已返回的 topN user_id 做批量资料补全;头像、昵称、短 ID 不进入统计库,避免聚合链路反查用户表。
_ = s.enrichSelfGameRiskUsers(ctx, query, overview)
return overview, nil
}
func (s *DashboardService) statisticsGET(ctx context.Context, path string, values url.Values) (map[string]any, error) {
baseURL := s.cfg.StatisticsService.BaseURL + path
req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"?"+values.Encode(), nil)
if err != nil {
return nil, err
}
resp, err := s.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("statistics service returned status %d", resp.StatusCode)
}
var out map[string]any
decoder := json.NewDecoder(resp.Body)
decoder.UseNumber()
if err := decoder.Decode(&out); err != nil {
return nil, err
}
return out, nil
}
func (s *DashboardService) enrichSelfGameRiskUsers(ctx context.Context, query SelfGameStatisticsQuery, overview map[string]any) error {
if s.user == nil || overview == nil {
return nil
}
rows, ok := overview["user_risk"].([]any)
if !ok || len(rows) == 0 {
return nil
}
userIDs := make([]int64, 0, len(rows))
seen := map[int64]struct{}{}
for _, raw := range rows {
row, ok := raw.(map[string]any)
if !ok {
continue
}
userID := anyInt64(row["user_id"])
if userID <= 0 {
continue
}
// user_id 是 16 位以上业务 IDadmin 层统一改成字符串透给 H5避免浏览器 JSON number 精度丢失。
row["user_id"] = strconv.FormatInt(userID, 10)
if _, exists := seen[userID]; exists {
continue
}
seen[userID] = struct{}{}
userIDs = append(userIDs, userID)
}
if len(userIDs) == 0 {
return nil
}
users, err := s.user.BatchGetUsers(ctx, userclient.BatchGetUsersRequest{
RequestID: query.RequestID,
Caller: "admin-server",
UserIDs: userIDs,
})
if err != nil {
return err
}
for _, raw := range rows {
row, ok := raw.(map[string]any)
if !ok {
continue
}
user := users[anyInt64(row["user_id"])]
if user == nil {
continue
}
row["avatar"] = user.Avatar
row["nickname"] = user.Username
row["username"] = user.Username
row["display_user_id"] = user.DisplayUserID
row["short_id"] = user.DisplayUserID
}
return nil
}
func anyInt64(value any) int64 {
switch typed := value.(type) {
case int64:
return typed
case int:
return int64(typed)
case float64:
return int64(typed)
case json.Number:
parsed, _ := typed.Int64()
return parsed
case string:
parsed, _ := strconv.ParseInt(strings.TrimSpace(typed), 10, 64)
return parsed
default:
return 0
}
}