package app import ( "context" "encoding/json" "errors" "net" "net/http" "strconv" "strings" "time" "hyapp/pkg/appcode" mysqlstorage "hyapp/services/statistics-service/internal/storage/mysql" ) type queryHTTPServer struct { repo *mysqlstorage.Repository server *http.Server listener net.Listener } func newQueryHTTPServer(addr string, repo *mysqlstorage.Repository) (*queryHTTPServer, error) { addr = strings.TrimSpace(addr) if addr == "" { return nil, nil } listener, err := net.Listen("tcp", addr) if err != nil { return nil, err } s := &queryHTTPServer{repo: repo, listener: listener} mux := http.NewServeMux() mux.HandleFunc("/internal/v1/statistics/overview", s.overview) mux.HandleFunc("/internal/v1/statistics/self-game/events", s.selfGameEvents) mux.HandleFunc("/internal/v1/statistics/self-games/overview", s.selfGameOverview) s.server = &http.Server{Handler: mux, ReadHeaderTimeout: 2 * time.Second} return s, nil } func (s *queryHTTPServer) Run() error { if s == nil { return nil } err := s.server.Serve(s.listener) if errors.Is(err, http.ErrServerClosed) { return nil } return err } func (s *queryHTTPServer) Close(ctx context.Context) error { if s == nil { return nil } return s.server.Shutdown(ctx) } func (s *queryHTTPServer) overview(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() startMS := parseInt64(query.Get("start_ms")) endMS := parseInt64(query.Get("end_ms")) if startMS <= 0 { now := time.Now().UTC() startMS = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC).UnixMilli() } if endMS <= startMS { endMS = startMS + 24*time.Hour.Milliseconds() } overview, err := s.repo.QueryOverview(r.Context(), mysqlstorage.OverviewQuery{ AppCode: query.Get("app_code"), StatTZ: query.Get("stat_tz"), StartMS: startMS, EndMS: endMS, SeriesStartMS: parseInt64(query.Get("series_start_ms")), SeriesEndMS: parseInt64(query.Get("series_end_ms")), CountryID: parseInt64(query.Get("country_id")), RegionID: parseInt64(query.Get("region_id")), }) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]any{"error": err.Error()}) return } writeJSON(w, http.StatusOK, overview) } type selfGameH5EventRequest struct { AppCode string `json:"app_code"` EventID string `json:"event_id"` EventName string `json:"event_name"` GameID string `json:"game_id"` Page string `json:"page"` SessionID string `json:"session_id"` MatchID string `json:"match_id"` UserID int64 `json:"user_id"` CountryID int64 `json:"country_id"` RegionID int64 `json:"region_id"` Language string `json:"language"` ClientVersion string `json:"client_version"` H5Version string `json:"h5_version"` EntrySource string `json:"entry_source"` DurationMS int64 `json:"duration_ms"` Success bool `json:"success"` ErrorCode string `json:"error_code"` OccurredAtMS int64 `json:"occurred_at_ms"` } func (s *queryHTTPServer) selfGameEvents(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { writeJSON(w, http.StatusMethodNotAllowed, map[string]any{"error": "method not allowed"}) return } defer r.Body.Close() var body selfGameH5EventRequest if err := json.NewDecoder(r.Body).Decode(&body); err != nil { writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid json"}) return } body.EventID = strings.TrimSpace(body.EventID) body.EventName = strings.TrimSpace(body.EventName) if body.EventID == "" || body.EventName == "" { writeJSON(w, http.StatusBadRequest, map[string]any{"error": "event_id and event_name are required"}) return } if body.OccurredAtMS <= 0 { body.OccurredAtMS = time.Now().UTC().UnixMilli() } app := appcode.Normalize(body.AppCode) if app == "" { app = "lalu" } // 内部 ingest 只承接 gateway 已解析过的用户、App 和维度字段;statistics-service 只做幂等聚合,不反查业务库。 err := s.repo.ConsumeSelfGameH5Event(appcode.WithContext(r.Context(), app), mysqlstorage.SelfGameH5Event{ AppCode: app, EventID: body.EventID, EventName: body.EventName, GameID: body.GameID, Page: body.Page, SessionID: body.SessionID, MatchID: body.MatchID, UserID: body.UserID, CountryID: body.CountryID, RegionID: body.RegionID, Language: body.Language, ClientVersion: body.ClientVersion, H5Version: body.H5Version, EntrySource: body.EntrySource, DurationMS: body.DurationMS, Success: body.Success, ErrorCode: body.ErrorCode, OccurredAtMS: body.OccurredAtMS, }) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]any{"error": err.Error()}) return } writeJSON(w, http.StatusOK, map[string]any{"ok": true}) } func (s *queryHTTPServer) selfGameOverview(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() startMS := parseInt64(query.Get("start_ms")) endMS := parseInt64(query.Get("end_ms")) if startMS <= 0 { now := time.Now().UTC() startMS = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC).UnixMilli() } if endMS <= startMS { endMS = startMS + 24*time.Hour.Milliseconds() } overview, err := s.repo.QuerySelfGameOverview(r.Context(), mysqlstorage.SelfGameOverviewQuery{ AppCode: query.Get("app_code"), StatTZ: query.Get("stat_tz"), StartMS: startMS, EndMS: endMS, GameID: query.Get("game_id"), CountryID: parseInt64(query.Get("country_id")), RegionID: parseInt64(query.Get("region_id")), }) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]any{"error": err.Error()}) return } writeJSON(w, http.StatusOK, overview) } func parseInt64(value string) int64 { parsed, _ := strconv.ParseInt(strings.TrimSpace(value), 10, 64) return parsed } func writeJSON(w http.ResponseWriter, status int, payload any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(payload) }