灵仙游戏
This commit is contained in:
parent
9d4dd79a86
commit
5e0e02cd2a
@ -117,7 +117,7 @@ type GamePlatform struct {
|
||||
CreatedAtMs int64 `protobuf:"varint,7,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"`
|
||||
UpdatedAtMs int64 `protobuf:"varint,8,opt,name=updated_at_ms,json=updatedAtMs,proto3" json:"updated_at_ms,omitempty"`
|
||||
AdapterType string `protobuf:"bytes,9,opt,name=adapter_type,json=adapterType,proto3" json:"adapter_type,omitempty"`
|
||||
// callback_secret is write-only for admin updates; list responses keep it empty.
|
||||
// callback_secret is visible to admin platform configuration pages and writable for key rotation.
|
||||
CallbackSecret string `protobuf:"bytes,10,opt,name=callback_secret,json=callbackSecret,proto3" json:"callback_secret,omitempty"`
|
||||
CallbackSecretSet bool `protobuf:"varint,11,opt,name=callback_secret_set,json=callbackSecretSet,proto3" json:"callback_secret_set,omitempty"`
|
||||
CallbackIpWhitelist []string `protobuf:"bytes,12,rep,name=callback_ip_whitelist,json=callbackIpWhitelist,proto3" json:"callback_ip_whitelist,omitempty"`
|
||||
|
||||
@ -24,7 +24,7 @@ message GamePlatform {
|
||||
int64 created_at_ms = 7;
|
||||
int64 updated_at_ms = 8;
|
||||
string adapter_type = 9;
|
||||
// callback_secret is write-only for admin updates; list responses keep it empty.
|
||||
// callback_secret is visible to admin platform configuration pages and writable for key rotation.
|
||||
string callback_secret = 10;
|
||||
bool callback_secret_set = 11;
|
||||
repeated string callback_ip_whitelist = 12;
|
||||
|
||||
@ -10,7 +10,8 @@ type platformDTO struct {
|
||||
APIBaseURL string `json:"apiBaseUrl"`
|
||||
// adapterType 决定服务端按哪家厂商协议拼启动 URL 和处理回调。
|
||||
AdapterType string `json:"adapterType"`
|
||||
// callbackSecretSet 只告诉后台“已配置”,不把密钥明文下发到浏览器。
|
||||
// callbackSecret 是后台配置页可见的厂商 key/AppSecret;只对有 game:view 权限的管理员返回。
|
||||
CallbackSecret string `json:"callbackSecret"`
|
||||
CallbackSecretSet bool `json:"callbackSecretSet"`
|
||||
CallbackIPWhitelist []string `json:"callbackIpWhitelist"`
|
||||
// adapterConfigJson 承载 uid_mode/default_lang/token_ttl_seconds 等可热更新配置。
|
||||
@ -50,6 +51,7 @@ func platformFromProto(item *gamev1.GamePlatform) platformDTO {
|
||||
Status: item.GetStatus(),
|
||||
APIBaseURL: item.GetApiBaseUrl(),
|
||||
AdapterType: item.GetAdapterType(),
|
||||
CallbackSecret: item.GetCallbackSecret(),
|
||||
CallbackSecretSet: item.GetCallbackSecretSet(),
|
||||
CallbackIPWhitelist: item.GetCallbackIpWhitelist(),
|
||||
AdapterConfigJSON: item.GetAdapterConfigJson(),
|
||||
|
||||
24
server/admin/internal/modules/gamemanagement/dto_test.go
Normal file
24
server/admin/internal/modules/gamemanagement/dto_test.go
Normal file
@ -0,0 +1,24 @@
|
||||
package gamemanagement
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
gamev1 "hyapp.local/api/proto/game/v1"
|
||||
)
|
||||
|
||||
func TestPlatformFromProtoReturnsCallbackSecret(t *testing.T) {
|
||||
const secret = "yomi-or-lingxian-secret"
|
||||
|
||||
got := platformFromProto(&gamev1.GamePlatform{
|
||||
PlatformCode: "yomi",
|
||||
CallbackSecret: secret,
|
||||
CallbackSecretSet: true,
|
||||
})
|
||||
|
||||
if got.CallbackSecret != secret {
|
||||
t.Fatalf("CallbackSecret = %q, want %q", got.CallbackSecret, secret)
|
||||
}
|
||||
if !got.CallbackSecretSet {
|
||||
t.Fatal("CallbackSecretSet = false, want true")
|
||||
}
|
||||
}
|
||||
@ -46,7 +46,7 @@ func (h *Handler) UpdatePlatform(c *gin.Context) {
|
||||
h.upsertPlatform(c, strings.TrimSpace(c.Param("platform_code")))
|
||||
}
|
||||
|
||||
// upsertPlatform 创建或更新第三方游戏平台配置;平台密钥后续走单独的密钥托管接口。
|
||||
// upsertPlatform 创建或更新第三方游戏平台配置;callbackSecret 非空时才会轮换厂商密钥。
|
||||
func (h *Handler) upsertPlatform(c *gin.Context, platformCode string) {
|
||||
var req platformRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
|
||||
@ -37,7 +37,7 @@ type Platform struct {
|
||||
APIBaseURL string
|
||||
// AdapterType 决定启动参数、签名/解密规则、回包结构和错误码映射。
|
||||
AdapterType string
|
||||
// CallbackSecretCiphertext 存放厂商 key/AppSecret;接口响应不回显明文。
|
||||
// CallbackSecretCiphertext 存放厂商 key/AppSecret;后台配置页需要回显,回调校验也直接使用它。
|
||||
CallbackSecretCiphertext string
|
||||
CallbackSecretConfigured bool
|
||||
// CallbackIPWhitelist 为空表示不限制;配置后支持单 IP 或 CIDR。
|
||||
|
||||
@ -9,7 +9,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
jwt "github.com/golang-jwt/jwt/v5"
|
||||
gamev1 "hyapp.local/api/proto/game/v1"
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/pkg/xerr"
|
||||
gamedomain "hyapp/services/game-service/internal/domain/game"
|
||||
)
|
||||
@ -89,7 +91,14 @@ func (s *Service) handleLeaderCCUserInfo(ctx context.Context, app string, platfo
|
||||
if err := decodeLeaderCCJSON(req.GetRawBody(), &body); err != nil {
|
||||
return leaderccAdapterError(leaderccCodeInvalidArgument, "invalid payload", false, "")
|
||||
}
|
||||
// key 存在 callback_secret 字段里;后台不回显明文,更新时才覆盖。
|
||||
if leaderccTokenOnlyProbe(body.Token, body.GameID, body.UID, body.RoomID, body.Sign) {
|
||||
if _, err := s.validLeaderCCSession(ctx, app, body.Token); err != nil {
|
||||
return leaderccAdapterError(leaderccCodeTokenInvalid, "token invalid", true, "")
|
||||
}
|
||||
raw, contentType := leaderccJSON(leaderccCodeOK, "", map[string]any{})
|
||||
return raw, contentType, strconv.Itoa(leaderccCodeOK), true, "", nil
|
||||
}
|
||||
// key 存在 callback_secret 字段里;后台会回显当前值,便于直接提供给灵仙排查签名。
|
||||
if !leaderccSignValid(body.Sign, platform.CallbackSecretCiphertext, body.GameID, body.UID, body.Token, body.RoomID) {
|
||||
return leaderccAdapterError(leaderccCodeSignatureInvalid, "signature invalid", false, "")
|
||||
}
|
||||
@ -132,6 +141,13 @@ func (s *Service) handleLeaderCCChangeCoin(ctx context.Context, app string, plat
|
||||
return leaderccAdapterError(leaderccCodeInvalidArgument, "invalid payload", false, "")
|
||||
}
|
||||
providerOrderID := strings.TrimSpace(body.OrderID)
|
||||
if leaderccTokenOnlyProbe(body.Token, body.OrderID, body.GameID, body.RoundID, body.UID, body.WinID, body.RoomID, body.Sign) && body.Coin == 0 && body.Type == 0 && body.RewardType == 0 {
|
||||
if _, err := s.validLeaderCCSession(ctx, app, body.Token); err != nil {
|
||||
return leaderccAdapterError(leaderccCodeTokenInvalid, "token invalid", true, providerOrderID)
|
||||
}
|
||||
raw, contentType := leaderccJSON(leaderccCodeOK, "", map[string]any{})
|
||||
return raw, contentType, strconv.Itoa(leaderccCodeOK), true, providerOrderID, nil
|
||||
}
|
||||
// 文档签名顺序:orderId + gameId + roundId + uid + coin + type + rewardType + token + winId + roomId + key。
|
||||
if !leaderccSignValid(body.Sign, platform.CallbackSecretCiphertext, body.OrderID, body.GameID, body.RoundID, body.UID, strconv.FormatInt(body.Coin, 10), strconv.FormatInt(int64(body.Type), 10), strconv.FormatInt(int64(body.RewardType), 10), body.Token, body.WinID, body.RoomID) {
|
||||
return leaderccAdapterError(leaderccCodeSignatureInvalid, "signature invalid", false, providerOrderID)
|
||||
@ -142,6 +158,12 @@ func (s *Service) handleLeaderCCChangeCoin(ctx context.Context, app string, plat
|
||||
if err != nil || !leaderccSessionMatches(session, config, body.UID, body.GameID) {
|
||||
return leaderccAdapterError(leaderccCodeTokenInvalid, "token invalid", true, providerOrderID)
|
||||
}
|
||||
if session.ProviderGameID == "" {
|
||||
session.ProviderGameID = strings.TrimSpace(body.GameID)
|
||||
}
|
||||
if session.GameID == "" {
|
||||
session.GameID = firstNonEmpty(strings.TrimSpace(body.GameID), "leadercc")
|
||||
}
|
||||
opType := leaderccOpType(body.Type)
|
||||
// type=1 扣款、type=2 加款;coin=0 没有实际改账,但仍会按幂等订单记成功。
|
||||
if providerOrderID == "" || opType == "" || body.Coin < 0 {
|
||||
@ -170,6 +192,7 @@ func (s *Service) handleLeaderCCRepairOrder(ctx context.Context, app string, pla
|
||||
UID string `json:"uid"`
|
||||
Coin int64 `json:"coin"`
|
||||
RewardType int32 `json:"rewardType"`
|
||||
Token string `json:"token"`
|
||||
WinID string `json:"winId"`
|
||||
RoomID string `json:"roomId"`
|
||||
Sign string `json:"sign"`
|
||||
@ -178,6 +201,13 @@ func (s *Service) handleLeaderCCRepairOrder(ctx context.Context, app string, pla
|
||||
return leaderccAdapterError(leaderccCodeInvalidArgument, "invalid payload", false, "")
|
||||
}
|
||||
providerOrderID := strings.TrimSpace(body.OrderID)
|
||||
if leaderccTokenOnlyProbe(body.Token, body.OrderID, body.GameID, body.RoundID, body.UID, body.WinID, body.RoomID, body.Sign) && body.Coin == 0 && body.RewardType == 0 {
|
||||
if _, err := s.validLeaderCCSession(ctx, app, body.Token); err != nil {
|
||||
return leaderccAdapterError(leaderccCodeTokenInvalid, "token invalid", true, providerOrderID)
|
||||
}
|
||||
raw, contentType := leaderccJSON(leaderccCodeOK, "", map[string]any{})
|
||||
return raw, contentType, strconv.Itoa(leaderccCodeOK), true, providerOrderID, nil
|
||||
}
|
||||
// 文档签名顺序:orderId + gameId + roundId + uid + coin + rewardType + winId + roomId + key。
|
||||
if !leaderccSignValid(body.Sign, platform.CallbackSecretCiphertext, body.OrderID, body.GameID, body.RoundID, body.UID, strconv.FormatInt(body.Coin, 10), strconv.FormatInt(int64(body.RewardType), 10), body.WinID, body.RoomID) {
|
||||
return leaderccAdapterError(leaderccCodeSignatureInvalid, "signature invalid", false, providerOrderID)
|
||||
@ -202,7 +232,11 @@ func (s *Service) handleLeaderCCRepairOrder(ctx context.Context, app string, pla
|
||||
}
|
||||
|
||||
func (s *Service) validLeaderCCSession(ctx context.Context, app string, token string) (gamedomain.LaunchSession, error) {
|
||||
// 灵仙和 Yomi 都复用 game_launch_sessions;差异在字段名,不在 token 存储方式。
|
||||
// 灵仙回调直接使用 App 登录 access token;无需预先进入游戏创建 launch session。
|
||||
if strings.Count(strings.TrimSpace(token), ".") == 2 {
|
||||
return s.leaderccSessionFromAppToken(app, token)
|
||||
}
|
||||
// 兼容历史联调 token:老链路会先 launch,再按 game_launch_sessions 校验。
|
||||
session, err := s.validYomiSession(ctx, app, token)
|
||||
if err != nil {
|
||||
return gamedomain.LaunchSession{}, err
|
||||
@ -211,11 +245,50 @@ func (s *Service) validLeaderCCSession(ctx context.Context, app string, token st
|
||||
}
|
||||
|
||||
func leaderccSessionMatches(session gamedomain.LaunchSession, config leaderccAdapterConfig, uid string, gameID string) bool {
|
||||
// gameId 对应内部目录里的 provider_game_id;uid 取值由 uid_mode 决定。
|
||||
if strings.TrimSpace(gameID) != "" && strings.TrimSpace(gameID) != strings.TrimSpace(session.ProviderGameID) {
|
||||
// 使用 App token 时 session.ProviderGameID 可能为空,此时不强制要求先配置/启动游戏目录。
|
||||
if strings.TrimSpace(gameID) != "" && strings.TrimSpace(session.ProviderGameID) != "" && strings.TrimSpace(gameID) != strings.TrimSpace(session.ProviderGameID) {
|
||||
return false
|
||||
}
|
||||
return strings.TrimSpace(uid) == externalUserID(session, config.UIDMode)
|
||||
normalizedUID := strings.TrimSpace(uid)
|
||||
return normalizedUID == externalUserID(session, config.UIDMode) ||
|
||||
normalizedUID == strings.TrimSpace(session.DisplayUserID) ||
|
||||
normalizedUID == strconv.FormatInt(session.UserID, 10)
|
||||
}
|
||||
|
||||
func (s *Service) leaderccSessionFromAppToken(app string, token string) (gamedomain.LaunchSession, error) {
|
||||
claims := jwt.MapClaims{}
|
||||
_, _, err := new(jwt.Parser).ParseUnverified(strings.TrimSpace(token), claims)
|
||||
if err != nil {
|
||||
return gamedomain.LaunchSession{}, xerr.New(xerr.Unauthorized, "token invalid")
|
||||
}
|
||||
if typ := strings.TrimSpace(leaderccStringClaim(claims, "typ")); typ != "" && typ != "access" {
|
||||
return gamedomain.LaunchSession{}, xerr.New(xerr.Unauthorized, "token invalid")
|
||||
}
|
||||
expiresAt, err := claims.GetExpirationTime()
|
||||
if err != nil || expiresAt == nil || !expiresAt.After(s.now()) {
|
||||
return gamedomain.LaunchSession{}, xerr.New(xerr.SessionExpired, "token expired")
|
||||
}
|
||||
tokenApp := appcode.Normalize(leaderccStringClaim(claims, "app_code"))
|
||||
if tokenApp != "" && tokenApp != appcode.Normalize(app) {
|
||||
return gamedomain.LaunchSession{}, xerr.New(xerr.Unauthorized, "token invalid")
|
||||
}
|
||||
userID, err := strconv.ParseInt(strings.TrimSpace(leaderccStringClaim(claims, "user_id")), 10, 64)
|
||||
if err != nil || userID <= 0 {
|
||||
return gamedomain.LaunchSession{}, xerr.New(xerr.Unauthorized, "token invalid")
|
||||
}
|
||||
displayUserID := firstNonEmpty(
|
||||
leaderccStringClaim(claims, "display_user_id"),
|
||||
leaderccStringClaim(claims, "default_display_user_id"),
|
||||
strconv.FormatInt(userID, 10),
|
||||
)
|
||||
return gamedomain.LaunchSession{
|
||||
AppCode: tokenApp,
|
||||
SessionID: leaderccStringClaim(claims, "sid"),
|
||||
UserID: userID,
|
||||
DisplayUserID: displayUserID,
|
||||
Status: gamedomain.SessionActive,
|
||||
ExpiresAtMS: expiresAt.UnixMilli(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func leaderccOpType(value int32) string {
|
||||
@ -254,6 +327,23 @@ func leaderccSignValid(actual string, key string, parts ...string) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(actual), expected)
|
||||
}
|
||||
|
||||
func leaderccTokenOnlyProbe(token string, parts ...string) bool {
|
||||
if strings.TrimSpace(token) == "" {
|
||||
return false
|
||||
}
|
||||
for _, part := range parts {
|
||||
if strings.TrimSpace(part) != "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func leaderccStringClaim(claims jwt.MapClaims, key string) string {
|
||||
value, _ := claims[key].(string)
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
|
||||
func leaderccAdapterError(code int, message string, signatureValid bool, providerRequestID string) ([]byte, string, string, bool, string, error) {
|
||||
// signatureValid/providerRequestID 会写入 callback log,帮助后续排查签名和重复订单问题。
|
||||
raw, contentType := leaderccJSON(code, message, map[string]any{})
|
||||
@ -262,10 +352,13 @@ func leaderccAdapterError(code int, message string, signatureValid bool, provide
|
||||
|
||||
func leaderccJSON(errorCode int, message string, data any) ([]byte, string) {
|
||||
// 灵仙成功可以不带 message;失败时带 message 方便联调定位。
|
||||
payload := map[string]any{"errorCode": errorCode}
|
||||
payload := map[string]any{"code": errorCode, "errorCode": errorCode}
|
||||
if data != nil {
|
||||
payload["data"] = data
|
||||
}
|
||||
if errorCode == leaderccCodeOK && strings.TrimSpace(message) == "" {
|
||||
message = "ok"
|
||||
}
|
||||
if strings.TrimSpace(message) != "" {
|
||||
payload["message"] = message
|
||||
}
|
||||
|
||||
@ -7,10 +7,12 @@ import (
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
jwt "github.com/golang-jwt/jwt/v5"
|
||||
"google.golang.org/grpc"
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
gamev1 "hyapp.local/api/proto/game/v1"
|
||||
@ -227,6 +229,40 @@ func TestHandleLeaderCCUserInfoValidatesSignAndToken(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleLeaderCCUserInfoAcceptsAppAccessTokenWithoutLaunchSession(t *testing.T) {
|
||||
key := "leadercc-test-key"
|
||||
token := leaderccTestAccessToken(t, "lalu", 42, "420001", 1700003600)
|
||||
repo := &fakeRepository{
|
||||
platform: gamedomain.Platform{
|
||||
PlatformCode: "lingxian",
|
||||
AdapterType: gamedomain.AdapterLeaderCCV1,
|
||||
CallbackSecretCiphertext: key,
|
||||
AdapterConfigJSON: `{"uid_mode":"display_user_id"}`,
|
||||
},
|
||||
}
|
||||
wallet := &fakeWallet{balanceAfter: 1680}
|
||||
user := &fakeUser{}
|
||||
svc := New(Config{}, repo, wallet, user)
|
||||
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
||||
|
||||
sign := leaderccTestSign(key, "101", "420001", token, "room_1")
|
||||
raw, contentType, err := svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
||||
Meta: &gamev1.RequestMeta{RequestId: "req-leadercc-app-token-user", AppCode: "lalu"},
|
||||
PlatformCode: "lingxian",
|
||||
Operation: "userinfo",
|
||||
RawBody: []byte(`{"gameId":"101","uid":"420001","token":"` + token + `","roomId":"room_1","sign":"` + sign + `"}`),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("HandleCallback failed: %v", err)
|
||||
}
|
||||
if contentType != "application/json" || !strings.Contains(string(raw), `"code":0`) || !strings.Contains(string(raw), `"errorCode":0`) || !strings.Contains(string(raw), `"coin":1680`) {
|
||||
t.Fatalf("leadercc app-token user response mismatch: %s", raw)
|
||||
}
|
||||
if user.lastGet.GetUserId() != 42 {
|
||||
t.Fatalf("user request must use token user_id, got %+v", user.lastGet)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleLeaderCCChangeCoinAndRepairOrder(t *testing.T) {
|
||||
key := "leadercc-test-key"
|
||||
token := "gt_lingxian_token"
|
||||
@ -292,6 +328,52 @@ func TestHandleLeaderCCChangeCoinAndRepairOrder(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleLeaderCCChangeCoinAcceptsAppAccessTokenAndTokenOnlyProbe(t *testing.T) {
|
||||
key := "leadercc-test-key"
|
||||
token := leaderccTestAccessToken(t, "lalu", 42, "420001", 1700003600)
|
||||
repo := &fakeRepository{
|
||||
platform: gamedomain.Platform{
|
||||
PlatformCode: "lingxian",
|
||||
AdapterType: gamedomain.AdapterLeaderCCV1,
|
||||
CallbackSecretCiphertext: key,
|
||||
AdapterConfigJSON: `{"uid_mode":"display_user_id"}`,
|
||||
},
|
||||
}
|
||||
wallet := &fakeWallet{balanceAfter: 1560}
|
||||
svc := New(Config{}, repo, wallet, &fakeUser{})
|
||||
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
||||
|
||||
probeRaw, _, err := svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
||||
Meta: &gamev1.RequestMeta{RequestId: "req-leadercc-token-probe", AppCode: "lalu"},
|
||||
PlatformCode: "lingxian",
|
||||
Operation: "change_coin",
|
||||
RawBody: []byte(`{"token":"` + token + `"}`),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("token-only probe failed: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(probeRaw), `"code":0`) || wallet.lastApply != nil {
|
||||
t.Fatalf("token-only probe must return ok without applying wallet: raw=%s wallet=%+v", probeRaw, wallet.lastApply)
|
||||
}
|
||||
|
||||
changeSign := leaderccTestSign(key, "order_app_token_1", "101", "round_1", "420001", "120", "1", "2", token, "", "room_1")
|
||||
raw, _, err := svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
||||
Meta: &gamev1.RequestMeta{RequestId: "req-leadercc-app-token-change", AppCode: "lalu"},
|
||||
PlatformCode: "lingxian",
|
||||
Operation: "change_coin",
|
||||
RawBody: []byte(`{"orderId":"order_app_token_1","gameId":"101","roundId":"round_1","uid":"420001","coin":120,"type":1,"rewardType":2,"token":"` + token + `","winId":"","roomId":"room_1","sign":"` + changeSign + `"}`),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("HandleCallback failed: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(raw), `"code":0`) || !strings.Contains(string(raw), `"coin":1560`) {
|
||||
t.Fatalf("leadercc app-token change response mismatch: %s", raw)
|
||||
}
|
||||
if wallet.lastApply.GetUserId() != 42 || wallet.lastApply.GetGameId() != "101" || wallet.lastApply.GetCommandId() != "game:lingxian:order_app_token_1" {
|
||||
t.Fatalf("wallet command must use token user and request game: %+v", wallet.lastApply)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleYomiChangeBalanceDecryptsAndAppliesWallet(t *testing.T) {
|
||||
secret := "12345678901234567890123456789012"
|
||||
token := "gt_test_token"
|
||||
@ -654,3 +736,24 @@ func leaderccTestSign(key string, parts ...string) string {
|
||||
sum := md5.Sum([]byte(builder.String()))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func leaderccTestAccessToken(t *testing.T, app string, userID int64, displayUserID string, expiresAtSec int64) string {
|
||||
t.Helper()
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"iss": "hyapp",
|
||||
"app_code": app,
|
||||
"sub": strconv.FormatInt(userID, 10),
|
||||
"user_id": strconv.FormatInt(userID, 10),
|
||||
"display_user_id": displayUserID,
|
||||
"default_display_user_id": displayUserID,
|
||||
"sid": "sess_test_app_token",
|
||||
"typ": "access",
|
||||
"iat": int64(1700000000),
|
||||
"exp": expiresAtSec,
|
||||
})
|
||||
signed, err := token.SignedString([]byte("test-secret"))
|
||||
if err != nil {
|
||||
t.Fatalf("sign test access token: %v", err)
|
||||
}
|
||||
return signed
|
||||
}
|
||||
|
||||
@ -267,7 +267,8 @@ func platformToProto(item gamedomain.Platform) *gamev1.GamePlatform {
|
||||
CreatedAtMs: item.CreatedAtMS,
|
||||
UpdatedAtMs: item.UpdatedAtMS,
|
||||
AdapterType: item.AdapterType,
|
||||
// callback_secret 明文只允许写入,不允许通过列表/详情接口回显到后台前端。
|
||||
// admin 后台要把当前厂商 key 提供给运营/对接人员核对;更新接口仍以空值表示保留旧密钥。
|
||||
CallbackSecret: item.CallbackSecretCiphertext,
|
||||
CallbackSecretSet: item.CallbackSecretConfigured,
|
||||
CallbackIpWhitelist: item.CallbackIPWhitelist,
|
||||
AdapterConfigJson: item.AdapterConfigJSON,
|
||||
|
||||
24
services/game-service/internal/transport/grpc/server_test.go
Normal file
24
services/game-service/internal/transport/grpc/server_test.go
Normal file
@ -0,0 +1,24 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
gamedomain "hyapp/services/game-service/internal/domain/game"
|
||||
)
|
||||
|
||||
func TestPlatformToProtoExposesCallbackSecretForAdmin(t *testing.T) {
|
||||
const secret = "leadercc-admin-visible-key"
|
||||
|
||||
got := platformToProto(gamedomain.Platform{
|
||||
PlatformCode: "lingxian",
|
||||
CallbackSecretCiphertext: secret,
|
||||
CallbackSecretConfigured: true,
|
||||
})
|
||||
|
||||
if got.GetCallbackSecret() != secret {
|
||||
t.Fatalf("callback_secret = %q, want %q", got.GetCallbackSecret(), secret)
|
||||
}
|
||||
if !got.GetCallbackSecretSet() {
|
||||
t.Fatal("callback_secret_set = false, want true")
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user