2026-06-30 14:11:50 +08:00

261 lines
8.2 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 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/platform-grants/users", s.platformGrantUsers)
mux.HandleFunc("/internal/v1/statistics/platform-grants/records", s.platformGrantRecords)
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)
}
func (s *queryHTTPServer) platformGrantUsers(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
pageSize := parsePositiveInt(query.Get("page_size"))
if pageSize == 0 {
pageSize = parsePositiveInt(query.Get("pageSize"))
}
page, err := s.repo.QueryPlatformGrantCoinUsers(r.Context(), mysqlstorage.PlatformGrantCoinQuery{
AppCode: query.Get("app_code"),
StatTZ: query.Get("stat_tz"),
StartMS: parseInt64(query.Get("start_ms")),
EndMS: parseInt64(query.Get("end_ms")),
StatDay: query.Get("stat_day"),
CountryID: parseInt64(query.Get("country_id")),
RegionID: parseInt64(query.Get("region_id")),
Page: parsePositiveInt(query.Get("page")),
PageSize: pageSize,
})
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]any{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, page)
}
func (s *queryHTTPServer) platformGrantRecords(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
userID := parseInt64(query.Get("user_id"))
if userID <= 0 {
writeJSON(w, http.StatusBadRequest, map[string]any{"error": "user_id is required"})
return
}
pageSize := parsePositiveInt(query.Get("page_size"))
if pageSize == 0 {
pageSize = parsePositiveInt(query.Get("pageSize"))
}
page, err := s.repo.QueryPlatformGrantCoinRecords(r.Context(), mysqlstorage.PlatformGrantCoinQuery{
AppCode: query.Get("app_code"),
StatTZ: query.Get("stat_tz"),
StartMS: parseInt64(query.Get("start_ms")),
EndMS: parseInt64(query.Get("end_ms")),
StatDay: query.Get("stat_day"),
CountryID: parseInt64(query.Get("country_id")),
RegionID: parseInt64(query.Get("region_id")),
UserID: userID,
Page: parsePositiveInt(query.Get("page")),
PageSize: pageSize,
})
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]any{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, page)
}
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 parsePositiveInt(value string) int {
parsed, _ := strconv.Atoi(strings.TrimSpace(value))
if parsed < 0 {
return 0
}
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)
}