261 lines
8.2 KiB
Go
261 lines
8.2 KiB
Go
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)
|
||
}
|