214 lines
5.7 KiB
Go
214 lines
5.7 KiB
Go
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 位以上业务 ID,admin 层统一改成字符串透给 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
|
||
}
|
||
}
|