feat: add hotgame callback compatibility proxy
This commit is contained in:
parent
27ab1317d2
commit
919364d4d0
@ -3,9 +3,13 @@ package http
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
gamev1 "hyapp.local/api/proto/game/v1"
|
||||
@ -173,6 +177,86 @@ func TestGameCallbackIsRawPublicResponse(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHotgameCompatFallbacksToHyappReyouCallback(t *testing.T) {
|
||||
body := `{"gameId":"101","uid":"420001","token":"hyapp-token","sign":"s"}`
|
||||
gameClient := &fakeGatewayGameClient{callbackResp: &gamev1.CallbackResponse{RawBody: []byte(`{"errorCode":0}`), ContentType: "application/json"}}
|
||||
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
|
||||
handler.SetGameClient(gameClient)
|
||||
router := handler.Routes(auth.NewVerifier("secret"))
|
||||
request := httptest.NewRequest(http.MethodPost, "/getUserInfo?nonce=1", bytes.NewReader([]byte(body)))
|
||||
request.Header.Set("X-App-Code", "lalu")
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusOK || recorder.Body.String() != `{"errorCode":0}` {
|
||||
t.Fatalf("hotgame hyapp callback response mismatch: status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
if gameClient.lastCallback == nil || gameClient.lastCallback.GetPlatformCode() != "reyou" || gameClient.lastCallback.GetOperation() != "getUserInfo" || string(gameClient.lastCallback.GetRawBody()) != body || gameClient.lastCallback.GetQuery()["nonce"] != "1" {
|
||||
t.Fatalf("hotgame hyapp callback request mismatch: %+v", gameClient.lastCallback)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHotgameCompatProxiesChatapp3TokenWithoutGameCallback(t *testing.T) {
|
||||
token := chatapp3HotgameToken("v1", 1001, "LIKEI", 4102444800000, 1700000000000)
|
||||
body := `{"orderId":"o1","gameId":"13","roundId":"r1","uid":"1001","coin":12,"type":1,"token":"` + token + `","sign":"s"}`
|
||||
var gotHost string
|
||||
var gotPath string
|
||||
var gotQuery string
|
||||
var gotBody string
|
||||
var gotContentType string
|
||||
oldTransport := http.DefaultTransport
|
||||
http.DefaultTransport = roundTripFunc(func(request *http.Request) (*http.Response, error) {
|
||||
gotHost = request.URL.Host
|
||||
gotPath = request.URL.Path
|
||||
gotQuery = request.URL.RawQuery
|
||||
gotContentType = request.Header.Get("Content-Type")
|
||||
raw, _ := io.ReadAll(request.Body)
|
||||
gotBody = string(raw)
|
||||
header := http.Header{}
|
||||
header.Set("Content-Type", "application/json")
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: header,
|
||||
Body: io.NopCloser(bytes.NewReader([]byte(`{"errorCode":0,"data":{"source":"chatapp3"}}`))),
|
||||
Request: request,
|
||||
}, nil
|
||||
})
|
||||
defer func() { http.DefaultTransport = oldTransport }()
|
||||
gameClient := &fakeGatewayGameClient{callbackResp: &gamev1.CallbackResponse{RawBody: []byte(`{"errorCode":0}`), ContentType: "application/json"}}
|
||||
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
|
||||
handler.SetGameClient(gameClient)
|
||||
router := handler.Routes(auth.NewVerifier("secret"))
|
||||
request := httptest.NewRequest(http.MethodPost, "/updateBalance?nonce=1", bytes.NewReader([]byte(body)))
|
||||
request.Header.Set("X-App-Code", "lalu")
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusOK || recorder.Body.String() != `{"errorCode":0,"data":{"source":"chatapp3"}}` {
|
||||
t.Fatalf("hotgame chatapp3 proxy response mismatch: status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
if gotHost != "jvapi.haiyihy.com" || gotPath != "/go/updateBalance" || gotQuery != "nonce=1" || gotBody != body || gotContentType != "application/json" {
|
||||
t.Fatalf("hotgame chatapp3 proxy request mismatch: host=%q path=%q query=%q content_type=%q body=%s", gotHost, gotPath, gotQuery, gotContentType, gotBody)
|
||||
}
|
||||
if gameClient.lastCallback != nil {
|
||||
t.Fatalf("chatapp3 token must not enter hyapp game callback: %+v", gameClient.lastCallback)
|
||||
}
|
||||
}
|
||||
|
||||
func chatapp3HotgameToken(version string, userID int64, sysOrigin string, expireMs int64, releaseMs int64) string {
|
||||
payload := url.QueryEscape(version + ":" + strconv.FormatInt(userID, 10) + ":" + sysOrigin + ":" + strconv.FormatInt(expireMs, 10) + ":" + strconv.FormatInt(releaseMs, 10))
|
||||
return "sign." + base64.StdEncoding.EncodeToString([]byte(payload))
|
||||
}
|
||||
|
||||
type roundTripFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (fn roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
return fn(request)
|
||||
}
|
||||
|
||||
type fakeGatewayGameClient struct {
|
||||
listResp *gamev1.ListGamesResponse
|
||||
recentResp *gamev1.ListGamesResponse
|
||||
|
||||
@ -125,19 +125,23 @@ func (h *Handler) launchGame(writer http.ResponseWriter, request *http.Request)
|
||||
}
|
||||
|
||||
func (h *Handler) handleGameCallback(writer http.ResponseWriter, request *http.Request) {
|
||||
if h.gameClient == nil {
|
||||
http.Error(writer, "upstream service error", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
raw, err := io.ReadAll(request.Body)
|
||||
if err != nil {
|
||||
http.Error(writer, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
h.handleGameCallbackRaw(writer, request, raw, strings.TrimSpace(request.PathValue("platform_code")), strings.TrimSpace(request.PathValue("operation")))
|
||||
}
|
||||
|
||||
func (h *Handler) handleGameCallbackRaw(writer http.ResponseWriter, request *http.Request, raw []byte, platformCode string, operation string) {
|
||||
if h.gameClient == nil {
|
||||
http.Error(writer, "upstream service error", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
resp, err := h.gameClient.HandleCallback(request.Context(), &gamev1.CallbackRequest{
|
||||
Meta: gameMeta(request),
|
||||
PlatformCode: strings.TrimSpace(request.PathValue("platform_code")),
|
||||
Operation: strings.TrimSpace(request.PathValue("operation")),
|
||||
PlatformCode: strings.TrimSpace(platformCode),
|
||||
Operation: strings.TrimSpace(operation),
|
||||
RawBody: raw,
|
||||
Headers: flattenedHeaders(request),
|
||||
Query: flattenedQuery(request),
|
||||
|
||||
@ -12,6 +12,8 @@ import (
|
||||
type Handler struct {
|
||||
gameClient client.GameClient
|
||||
userProfileClient client.UserProfileClient
|
||||
// 热游固定回调地址先进入 hyapp gateway;chatapp3 老 token 需要原样转给 chatapp3,hyapp token 继续落到 game-service。
|
||||
hotgameCompatHTTPClient *http.Client
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
@ -21,18 +23,21 @@ type Config struct {
|
||||
|
||||
func New(config Config) *Handler {
|
||||
return &Handler{
|
||||
gameClient: config.GameClient,
|
||||
userProfileClient: config.UserProfileClient,
|
||||
gameClient: config.GameClient,
|
||||
userProfileClient: config.UserProfileClient,
|
||||
hotgameCompatHTTPClient: hotgameCompatHTTPClient(),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) Handlers() httproutes.GameHandlers {
|
||||
return httproutes.GameHandlers{
|
||||
ListGames: h.listGames,
|
||||
ListRecentGames: h.listRecentGames,
|
||||
GetBridgeScript: h.getBridgeScript,
|
||||
LaunchGame: h.launchGame,
|
||||
HandleCallback: h.handleGameCallback,
|
||||
ListGames: h.listGames,
|
||||
ListRecentGames: h.listRecentGames,
|
||||
GetBridgeScript: h.getBridgeScript,
|
||||
LaunchGame: h.launchGame,
|
||||
HandleCallback: h.handleGameCallback,
|
||||
HandleHotgameGetUserInfo: h.handleHotgameGetUserInfo,
|
||||
HandleHotgameUpdateBalance: h.handleHotgameUpdateBalance,
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,3 +60,11 @@ func (h *Handler) LaunchGame(writer http.ResponseWriter, request *http.Request)
|
||||
func (h *Handler) HandleCallback(writer http.ResponseWriter, request *http.Request) {
|
||||
h.handleGameCallback(writer, request)
|
||||
}
|
||||
|
||||
func (h *Handler) HandleHotgameGetUserInfo(writer http.ResponseWriter, request *http.Request) {
|
||||
h.handleHotgameGetUserInfo(writer, request)
|
||||
}
|
||||
|
||||
func (h *Handler) HandleHotgameUpdateBalance(writer http.ResponseWriter, request *http.Request) {
|
||||
h.handleHotgameUpdateBalance(writer, request)
|
||||
}
|
||||
|
||||
@ -0,0 +1,243 @@
|
||||
package gameapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
hotgameCompatPlatformCode = "reyou"
|
||||
hotgameCompatProxyDefaultBaseURL = "https://jvapi.haiyihy.com/go"
|
||||
hotgameCompatProxyDefaultTimeout = 10 * time.Second
|
||||
hotgameCompatGetUserInfoOperation = "getUserInfo"
|
||||
hotgameCompatUpdateBalanceOperation = "updateBalance"
|
||||
)
|
||||
|
||||
func (h *Handler) handleHotgameGetUserInfo(writer http.ResponseWriter, request *http.Request) {
|
||||
h.handleHotgameCompatCallback(writer, request, hotgameCompatGetUserInfoOperation)
|
||||
}
|
||||
|
||||
func (h *Handler) handleHotgameUpdateBalance(writer http.ResponseWriter, request *http.Request) {
|
||||
h.handleHotgameCompatCallback(writer, request, hotgameCompatUpdateBalanceOperation)
|
||||
}
|
||||
|
||||
func (h *Handler) handleHotgameCompatCallback(writer http.ResponseWriter, request *http.Request, operation string) {
|
||||
raw, err := io.ReadAll(request.Body)
|
||||
if err != nil {
|
||||
http.Error(writer, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// 热游固定回调地址无法按域名区分 hyapp 和 chatapp3;这里用 token 结构做最小路由键。
|
||||
// chatapp3 的用户 token 是 sign.base64(version:userId:sysOrigin:expire:release),hyapp 当前热游 token 是 JWT 或本服务 session。
|
||||
if isChatapp3HotgameToken(hotgameCompatCallbackToken(raw)) {
|
||||
h.proxyChatapp3HotgameCallback(writer, request, raw, operation)
|
||||
return
|
||||
}
|
||||
// 非 chatapp3 token 全部继续走 hyapp 自己的 reyou 平台配置,避免 hyapp 自有热游游戏误转到 chatapp3。
|
||||
h.handleGameCallbackRaw(writer, request, raw, hotgameCompatPlatformCode, operation)
|
||||
}
|
||||
|
||||
func (h *Handler) proxyChatapp3HotgameCallback(writer http.ResponseWriter, request *http.Request, raw []byte, operation string) {
|
||||
target, err := hotgameCompatProxyURL(operation, request.URL.RawQuery)
|
||||
if err != nil {
|
||||
http.Error(writer, "proxy config error", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
proxyReq, err := http.NewRequestWithContext(request.Context(), http.MethodPost, target, bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
http.Error(writer, "proxy request error", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
copyHotgameProxyRequestHeaders(proxyReq, request)
|
||||
appendHotgameForwardedHeaders(proxyReq, request)
|
||||
|
||||
resp, err := h.hotgameCompatHTTPClient.Do(proxyReq)
|
||||
if err != nil {
|
||||
http.Error(writer, "proxy upstream error", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
copyHotgameProxyResponseHeaders(writer.Header(), resp.Header)
|
||||
writer.WriteHeader(resp.StatusCode)
|
||||
_, _ = io.Copy(writer, resp.Body)
|
||||
}
|
||||
|
||||
func hotgameCompatHTTPClient() *http.Client {
|
||||
return &http.Client{Timeout: hotgameCompatProxyDefaultTimeout}
|
||||
}
|
||||
|
||||
func hotgameCompatCallbackToken(raw []byte) string {
|
||||
var payload map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw, &payload); err != nil {
|
||||
return ""
|
||||
}
|
||||
return hotgameCompatJSONText(payload["token"])
|
||||
}
|
||||
|
||||
func hotgameCompatJSONText(raw json.RawMessage) string {
|
||||
text := strings.TrimSpace(string(raw))
|
||||
if text == "" || text == "null" {
|
||||
return ""
|
||||
}
|
||||
var decoded string
|
||||
if err := json.Unmarshal(raw, &decoded); err == nil {
|
||||
return strings.TrimSpace(decoded)
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func isChatapp3HotgameToken(token string) bool {
|
||||
token = strings.TrimSpace(token)
|
||||
if chatapp3HotgameTokenCandidateMatches(token) {
|
||||
return true
|
||||
}
|
||||
if decoded, err := url.QueryUnescape(token); err == nil && strings.TrimSpace(decoded) != token {
|
||||
return chatapp3HotgameTokenCandidateMatches(decoded)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func chatapp3HotgameTokenCandidateMatches(token string) bool {
|
||||
token = normalizeChatapp3HotgameTokenCandidate(token)
|
||||
parts := strings.SplitN(token, ".", 2)
|
||||
if len(parts) != 2 || strings.TrimSpace(parts[0]) == "" || strings.TrimSpace(parts[1]) == "" {
|
||||
return false
|
||||
}
|
||||
decoded, err := base64.StdEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
payload := strings.TrimSpace(string(decoded))
|
||||
if unescaped, err := url.QueryUnescape(payload); err == nil {
|
||||
payload = strings.TrimSpace(unescaped)
|
||||
}
|
||||
fields := strings.Split(payload, ":")
|
||||
if len(fields) != 5 || strings.TrimSpace(fields[0]) == "" || strings.TrimSpace(fields[2]) == "" {
|
||||
return false
|
||||
}
|
||||
userID, userErr := strconv.ParseInt(strings.TrimSpace(fields[1]), 10, 64)
|
||||
_, expireErr := strconv.ParseInt(strings.TrimSpace(fields[3]), 10, 64)
|
||||
_, releaseErr := strconv.ParseInt(strings.TrimSpace(fields[4]), 10, 64)
|
||||
// 这里只判定归属,不判定过期;过期 token 仍必须转给 chatapp3,让原项目按自己的错误码回复热游。
|
||||
return userID > 0 && userErr == nil && expireErr == nil && releaseErr == nil
|
||||
}
|
||||
|
||||
func normalizeChatapp3HotgameTokenCandidate(token string) string {
|
||||
token = strings.TrimSpace(token)
|
||||
if strings.HasPrefix(strings.ToLower(token), "bearer ") {
|
||||
token = strings.TrimSpace(token[len("bearer "):])
|
||||
}
|
||||
parts := strings.SplitN(token, ".", 2)
|
||||
if len(parts) != 2 || parts[1] == "" {
|
||||
return token
|
||||
}
|
||||
switch len(parts[1]) % 4 {
|
||||
case 2:
|
||||
parts[1] += "=="
|
||||
case 3:
|
||||
parts[1] += "="
|
||||
}
|
||||
return strings.TrimSpace(parts[0]) + "." + strings.TrimSpace(parts[1])
|
||||
}
|
||||
|
||||
func hotgameCompatProxyURL(operation string, rawQuery string) (string, error) {
|
||||
parsed, err := url.Parse(hotgameCompatProxyDefaultBaseURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
parsed.Path = singleJoiningSlash(parsed.Path, operation)
|
||||
if strings.TrimSpace(rawQuery) != "" {
|
||||
if parsed.RawQuery == "" {
|
||||
parsed.RawQuery = rawQuery
|
||||
} else {
|
||||
parsed.RawQuery += "&" + rawQuery
|
||||
}
|
||||
}
|
||||
return parsed.String(), nil
|
||||
}
|
||||
|
||||
func singleJoiningSlash(left string, right string) string {
|
||||
leftSlash := strings.HasSuffix(left, "/")
|
||||
rightSlash := strings.HasPrefix(right, "/")
|
||||
switch {
|
||||
case leftSlash && rightSlash:
|
||||
return left + strings.TrimLeft(right, "/")
|
||||
case !leftSlash && !rightSlash:
|
||||
return left + "/" + right
|
||||
default:
|
||||
return left + right
|
||||
}
|
||||
}
|
||||
|
||||
func copyHotgameProxyRequestHeaders(dst *http.Request, src *http.Request) {
|
||||
for key, values := range src.Header {
|
||||
if isHopByHopHeader(key) {
|
||||
continue
|
||||
}
|
||||
for _, value := range values {
|
||||
dst.Header.Add(key, value)
|
||||
}
|
||||
}
|
||||
if dst.Header.Get("Content-Type") == "" {
|
||||
dst.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
}
|
||||
|
||||
func appendHotgameForwardedHeaders(dst *http.Request, src *http.Request) {
|
||||
if host := strings.TrimSpace(src.Host); host != "" {
|
||||
dst.Header.Set("X-Forwarded-Host", host)
|
||||
}
|
||||
if proto := strings.TrimSpace(src.Header.Get("X-Forwarded-Proto")); proto != "" {
|
||||
dst.Header.Set("X-Forwarded-Proto", proto)
|
||||
} else if src.TLS != nil {
|
||||
dst.Header.Set("X-Forwarded-Proto", "https")
|
||||
} else {
|
||||
dst.Header.Set("X-Forwarded-Proto", "http")
|
||||
}
|
||||
if ip := requestClientIP(src); ip != "" {
|
||||
if prior := strings.TrimSpace(src.Header.Get("X-Forwarded-For")); prior != "" {
|
||||
dst.Header.Set("X-Forwarded-For", prior+", "+ip)
|
||||
} else {
|
||||
dst.Header.Set("X-Forwarded-For", ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func requestClientIP(request *http.Request) string {
|
||||
host, _, err := net.SplitHostPort(strings.TrimSpace(request.RemoteAddr))
|
||||
if err == nil {
|
||||
return host
|
||||
}
|
||||
return strings.TrimSpace(request.RemoteAddr)
|
||||
}
|
||||
|
||||
func copyHotgameProxyResponseHeaders(dst http.Header, src http.Header) {
|
||||
for key, values := range src {
|
||||
if isHopByHopHeader(key) {
|
||||
continue
|
||||
}
|
||||
for _, value := range values {
|
||||
dst.Add(key, value)
|
||||
}
|
||||
}
|
||||
if dst.Get("Content-Type") == "" {
|
||||
dst.Set("Content-Type", "application/json")
|
||||
}
|
||||
}
|
||||
|
||||
func isHopByHopHeader(key string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(key)) {
|
||||
case "connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailer", "transfer-encoding", "upgrade", "host", "content-length":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -236,11 +236,13 @@ type VIPHandlers struct {
|
||||
}
|
||||
|
||||
type GameHandlers struct {
|
||||
ListGames http.HandlerFunc
|
||||
ListRecentGames http.HandlerFunc
|
||||
GetBridgeScript http.HandlerFunc
|
||||
LaunchGame http.HandlerFunc
|
||||
HandleCallback http.HandlerFunc
|
||||
ListGames http.HandlerFunc
|
||||
ListRecentGames http.HandlerFunc
|
||||
GetBridgeScript http.HandlerFunc
|
||||
LaunchGame http.HandlerFunc
|
||||
HandleCallback http.HandlerFunc
|
||||
HandleHotgameGetUserInfo http.HandlerFunc
|
||||
HandleHotgameUpdateBalance http.HandlerFunc
|
||||
}
|
||||
|
||||
type routes struct {
|
||||
@ -274,6 +276,10 @@ func (r routes) public(path string, method string, next http.HandlerFunc) {
|
||||
r.register(path, method, r.config.PublicWrap, next)
|
||||
}
|
||||
|
||||
func (r routes) rootPublic(path string, method string, next http.HandlerFunc) {
|
||||
r.registerRaw(path, method, r.config.PublicWrap, next)
|
||||
}
|
||||
|
||||
func (r routes) auth(path string, method string, next http.HandlerFunc) {
|
||||
r.register(path, method, r.config.AuthWrap, next)
|
||||
}
|
||||
@ -283,11 +289,15 @@ func (r routes) profile(path string, method string, next http.HandlerFunc) {
|
||||
}
|
||||
|
||||
func (r routes) register(path string, method string, wrap Wrapper, next http.HandlerFunc) {
|
||||
r.registerRaw(APIV1Prefix+path, method, wrap, next)
|
||||
}
|
||||
|
||||
func (r routes) registerRaw(path string, method string, wrap Wrapper, next http.HandlerFunc) {
|
||||
if method != "" {
|
||||
// method 继续走 gateway envelope,而不是使用 ServeMux 自动 405。
|
||||
next = httpkit.RequireMethod(method, next)
|
||||
}
|
||||
r.mux.Handle(APIV1Prefix+path, wrap(next))
|
||||
r.mux.Handle(path, wrap(next))
|
||||
}
|
||||
|
||||
func (r routes) registerAuthRoutes() {
|
||||
@ -514,5 +524,8 @@ func (r routes) registerGameRoutes() {
|
||||
r.profile("/games/recent", http.MethodGet, h.ListRecentGames)
|
||||
r.profile("/games/bridge-script", http.MethodGet, h.GetBridgeScript)
|
||||
r.profile("/games/{game_id}/launch", http.MethodPost, h.LaunchGame)
|
||||
// 热游后台只能配置根路径回调;这里仍复用 public wrapper,让请求日志、request_id 和 app_code 解析保持一致。
|
||||
r.rootPublic("/getUserInfo", http.MethodPost, h.HandleHotgameGetUserInfo)
|
||||
r.rootPublic("/updateBalance", http.MethodPost, h.HandleHotgameUpdateBalance)
|
||||
r.public("/game-callbacks/{platform_code}/{operation}", http.MethodPost, h.HandleCallback)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user