568 lines
21 KiB
Go
568 lines
21 KiB
Go
package game
|
||
|
||
import (
|
||
"context"
|
||
"crypto/sha256"
|
||
"crypto/subtle"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"fmt"
|
||
"math"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
gamev1 "hyapp.local/api/proto/game/v1"
|
||
"hyapp/pkg/xerr"
|
||
gamedomain "hyapp/services/game-service/internal/domain/game"
|
||
)
|
||
|
||
const (
|
||
// ZGame 文档的业务码会被游戏服务端直接消费;HTTP 200 只表示网关成功接收并处理回调。
|
||
zgameCodeOK = 0
|
||
zgameCodeInvalidSession = 201
|
||
zgameCodeInvalidSignature = 400
|
||
zgameCodeUnauthorized = 401
|
||
zgameCodeInvalidCode = 403
|
||
zgameCodeInsufficientBalance = 501
|
||
zgameCodeInternalError = 500
|
||
|
||
zgameDefaultTokenTTL = 24 * time.Hour
|
||
)
|
||
|
||
type zgameAdapterConfig struct {
|
||
// uid_mode 必须和启动链接、login open_id、后续签名 open_id 保持一致,避免同一 token 被不同用户标识复用。
|
||
UIDMode string `json:"uid_mode"`
|
||
DefaultLang string `json:"default_lang"`
|
||
TokenTTLSeconds int64 `json:"token_ttl_seconds"`
|
||
// game_urls 支持 ZGame 每款游戏独立 H5 地址;launch_url_template 用于批量上新时按 provider_game_id 生成地址。
|
||
GameURLs map[string]string `json:"game_urls"`
|
||
LaunchURLTemplate string `json:"launch_url_template"`
|
||
// callback_base 可按环境覆盖为完整公网 HTTPS 地址,避免 H5 在第三方域名下把相对路径解析到厂商域名。
|
||
CallbackBase string `json:"callback_base"`
|
||
CallbackBaseAlias string `json:"callbackBase"`
|
||
// merchant_payload 是 ZGame consume 回调的透传字段来源;只参与厂商回传,不进入内部业务判断。
|
||
MerchantPayload string `json:"merchant_payload"`
|
||
// level/gender 是 ZGame 用户资料响应里的展示字段;真实等级接入前保持显式默认值,避免从其它体系误推。
|
||
Level int32 `json:"level"`
|
||
Gender int32 `json:"gender"`
|
||
}
|
||
|
||
func zgameConfigFromPlatform(value any) zgameAdapterConfig {
|
||
var raw string
|
||
switch typed := value.(type) {
|
||
case gamedomain.Platform:
|
||
raw = typed.AdapterConfigJSON
|
||
case gamedomain.LaunchableGame:
|
||
raw = typed.AdapterConfigJSON
|
||
}
|
||
var config zgameAdapterConfig
|
||
_ = json.Unmarshal([]byte(strings.TrimSpace(raw)), &config)
|
||
config.UIDMode = strings.ToLower(strings.TrimSpace(config.UIDMode))
|
||
if config.UIDMode == "" {
|
||
// laluparty 游戏侧展示和签名都用短号作为 open_id;历史配置未写 uid_mode 时按文档默认走 display_user_id。
|
||
config.UIDMode = "display_user_id"
|
||
}
|
||
config.DefaultLang = strings.TrimSpace(config.DefaultLang)
|
||
config.LaunchURLTemplate = strings.TrimSpace(config.LaunchURLTemplate)
|
||
config.CallbackBase = strings.TrimSpace(firstNonEmpty(config.CallbackBase, config.CallbackBaseAlias))
|
||
config.MerchantPayload = strings.TrimSpace(config.MerchantPayload)
|
||
config.GameURLs = normalizeStringMap(config.GameURLs)
|
||
return config
|
||
}
|
||
|
||
func (c zgameAdapterConfig) TokenTTL() time.Duration {
|
||
if c.TokenTTLSeconds > 0 {
|
||
return time.Duration(c.TokenTTLSeconds) * time.Second
|
||
}
|
||
return zgameDefaultTokenTTL
|
||
}
|
||
|
||
func (c zgameAdapterConfig) CallbackBaseValue() string {
|
||
if c.CallbackBase != "" {
|
||
return c.CallbackBase
|
||
}
|
||
return "/api/v1/zgame/callback"
|
||
}
|
||
|
||
func zgameLaunchBaseURL(game gamedomain.LaunchableGame, config zgameAdapterConfig) string {
|
||
for _, key := range []string{game.ProviderGameID, game.GameID, strings.ToLower(game.GameID)} {
|
||
if raw := strings.TrimSpace(config.GameURLs[strings.TrimSpace(key)]); raw != "" {
|
||
return raw
|
||
}
|
||
}
|
||
if config.LaunchURLTemplate == "" {
|
||
return ""
|
||
}
|
||
replacer := strings.NewReplacer(
|
||
"{provider_game_id}", strings.TrimSpace(game.ProviderGameID),
|
||
"{providerGameId}", strings.TrimSpace(game.ProviderGameID),
|
||
"{game_id}", strings.TrimSpace(game.GameID),
|
||
"{gameId}", strings.TrimSpace(game.GameID),
|
||
)
|
||
return strings.TrimSpace(replacer.Replace(config.LaunchURLTemplate))
|
||
}
|
||
|
||
func (s *Service) handleZGameOperation(ctx context.Context, app string, platform gamedomain.Platform, req *gamev1.CallbackRequest, operation string, requestHash string) ([]byte, string, string, bool, string, error) {
|
||
// IP 白名单为空时兼容联调环境;一旦后台配置,固定回调入口先挡掉非厂商来源。
|
||
if !callbackIPAllowed(req, platform.CallbackIPWhitelist) {
|
||
return zgameAdapterError(zgameCodeUnauthorized, "Unauthorized", false, "")
|
||
}
|
||
config := zgameConfigFromPlatform(platform)
|
||
switch zgameOperation(operation) {
|
||
case "login":
|
||
return s.handleZGameLogin(ctx, app, config, req)
|
||
case "session_renew":
|
||
return s.handleZGameSessionRenew(ctx, app, config, platform.CallbackSecretCiphertext, req)
|
||
case "user_info_batch":
|
||
return s.handleZGameUserInfoBatch(ctx, app, config, platform.CallbackSecretCiphertext, req)
|
||
case "consume":
|
||
return s.handleZGameConsume(ctx, app, config, platform.CallbackSecretCiphertext, req, requestHash)
|
||
default:
|
||
return zgameAdapterError(zgameCodeOK, "success", false, "")
|
||
}
|
||
}
|
||
|
||
func zgameOperation(operation string) string {
|
||
normalized := strings.Trim(strings.ToLower(strings.TrimSpace(operation)), "/")
|
||
normalized = strings.TrimPrefix(normalized, "api/")
|
||
normalized = strings.TrimPrefix(normalized, "server/")
|
||
normalized = strings.TrimPrefix(normalized, "client/")
|
||
return strings.Trim(normalized, "/")
|
||
}
|
||
|
||
type zgameLoginBody struct {
|
||
OpenID string `json:"open_id"`
|
||
OpenIDV2 string `json:"openId"`
|
||
JSCode string `json:"js_code"`
|
||
JSCodeV2 string `json:"jsCode"`
|
||
JSCodeV3 string `json:"code"`
|
||
CallbackURL string `json:"callback_url"`
|
||
}
|
||
|
||
func (body zgameLoginBody) openID() string {
|
||
return firstNonEmpty(body.OpenID, body.OpenIDV2)
|
||
}
|
||
|
||
func (body zgameLoginBody) jsCode() string {
|
||
return firstNonEmpty(body.JSCode, body.JSCodeV2, body.JSCodeV3)
|
||
}
|
||
|
||
func (s *Service) handleZGameLogin(ctx context.Context, app string, config zgameAdapterConfig, req *gamev1.CallbackRequest) ([]byte, string, string, bool, string, error) {
|
||
var body zgameLoginBody
|
||
if err := decodeYomiJSON(req.GetRawBody(), &body); err != nil {
|
||
return zgameAdapterError(zgameCodeInvalidCode, "Invalid or Expired Code", false, "")
|
||
}
|
||
openID := body.openID()
|
||
jsCode := body.jsCode()
|
||
session, err := s.validZGameSession(ctx, app, gamedomain.PlatformCodeZGame, jsCode, "")
|
||
if err != nil || !zgameSessionMatches(session, config, openID, "") {
|
||
return zgameAdapterError(zgameCodeInvalidCode, "Invalid or Expired Code", true, "")
|
||
}
|
||
snapshot, err := s.yomiUserSnapshot(ctx, app, req, session.UserID)
|
||
if err != nil {
|
||
return zgameAdapterError(zgameCodeFromError(err), zgameMessageFromError(err), true, "")
|
||
}
|
||
raw, contentType := zgameJSON(zgameCodeOK, "success", zgameLoginData(session, config, openID, jsCode, snapshot, s.now()))
|
||
return raw, contentType, strconv.Itoa(zgameCodeOK), true, "", nil
|
||
}
|
||
|
||
type zgameSignedSessionBody struct {
|
||
OpenID string `json:"open_id"`
|
||
OpenIDV2 string `json:"openId"`
|
||
Session string `json:"session"`
|
||
Signature string `json:"signature"`
|
||
RawGameID any `json:"game_id"`
|
||
RawGameIDV2 any `json:"gameId"`
|
||
RawTimestamp any `json:"timestamp"`
|
||
}
|
||
|
||
func (body zgameSignedSessionBody) openID() string {
|
||
return firstNonEmpty(body.OpenID, body.OpenIDV2)
|
||
}
|
||
|
||
func (body zgameSignedSessionBody) gameIDText() string {
|
||
return zgameAnyString(firstNonEmpty(zgameAnyString(body.RawGameID), zgameAnyString(body.RawGameIDV2)))
|
||
}
|
||
|
||
func (s *Service) handleZGameSessionRenew(ctx context.Context, app string, config zgameAdapterConfig, secret string, req *gamev1.CallbackRequest) ([]byte, string, string, bool, string, error) {
|
||
body, signed, session, err := s.zgameSignedSession(ctx, app, config, secret, req)
|
||
if !signed {
|
||
return zgameAdapterError(zgameCodeInvalidSignature, "Invalid Signature", false, "")
|
||
}
|
||
if err != nil || !zgameSessionMatches(session, config, body.openID(), body.gameIDText()) {
|
||
return zgameAdapterError(zgameCodeInvalidSession, "Invalid Session", true, "")
|
||
}
|
||
raw, contentType := zgameJSON(zgameCodeOK, "success", map[string]any{
|
||
"session": body.Session,
|
||
"session_expired": zgameSessionTTLMS(session, s.now()),
|
||
})
|
||
return raw, contentType, strconv.Itoa(zgameCodeOK), true, "", nil
|
||
}
|
||
|
||
type zgameUserInfoBatchBody struct {
|
||
OpenIDList []string `json:"open_id_list"`
|
||
OpenIDListV2 []string `json:"openIdList"`
|
||
OpenID string `json:"open_id"`
|
||
OpenIDV2 string `json:"openId"`
|
||
Session string `json:"session"`
|
||
Signature string `json:"signature"`
|
||
}
|
||
|
||
func (body zgameUserInfoBatchBody) openID() string {
|
||
return firstNonEmpty(body.OpenID, body.OpenIDV2)
|
||
}
|
||
|
||
func (body zgameUserInfoBatchBody) openIDList() []string {
|
||
if len(body.OpenIDList) > 0 {
|
||
return body.OpenIDList
|
||
}
|
||
return body.OpenIDListV2
|
||
}
|
||
|
||
func (s *Service) handleZGameUserInfoBatch(ctx context.Context, app string, config zgameAdapterConfig, secret string, req *gamev1.CallbackRequest) ([]byte, string, string, bool, string, error) {
|
||
var body zgameUserInfoBatchBody
|
||
if err := decodeYomiJSON(req.GetRawBody(), &body); err != nil {
|
||
return zgameAdapterError(zgameCodeInvalidSession, "Invalid Session", false, "")
|
||
}
|
||
if !zgameSignatureValid(body.openID(), body.Session, body.Signature, secret) {
|
||
return zgameAdapterError(zgameCodeInvalidSignature, "Invalid Signature", false, "")
|
||
}
|
||
session, err := s.validZGameSession(ctx, app, gamedomain.PlatformCodeZGame, body.Session, "")
|
||
if err != nil || !zgameSessionMatches(session, config, body.openID(), "") {
|
||
return zgameAdapterError(zgameCodeInvalidSession, "Invalid Session", true, "")
|
||
}
|
||
userInfos := make(map[string]any, len(body.openIDList()))
|
||
for _, openID := range body.openIDList() {
|
||
openID = strings.TrimSpace(openID)
|
||
if openID == "" {
|
||
continue
|
||
}
|
||
data, ok := s.zgameUserInfoForOpenID(ctx, app, config, req, session, openID)
|
||
if ok {
|
||
userInfos[openID] = data
|
||
}
|
||
}
|
||
raw, contentType := zgameJSON(zgameCodeOK, "success", map[string]any{"user_infos": userInfos})
|
||
return raw, contentType, strconv.Itoa(zgameCodeOK), true, "", nil
|
||
}
|
||
|
||
type zgameConsumeBody struct {
|
||
OpenID string `json:"open_id"`
|
||
OpenIDV2 string `json:"openId"`
|
||
Session string `json:"session"`
|
||
Amount json.Number `json:"amount"`
|
||
OrderID string `json:"order_id"`
|
||
OrderIDV2 string `json:"orderId"`
|
||
GameID string `json:"game_id"`
|
||
GameIDV2 string `json:"gameId"`
|
||
Timestamp int64 `json:"timestamp"`
|
||
RoundID string `json:"round_id"`
|
||
RoundIDV2 string `json:"roundId"`
|
||
Type int32 `json:"type"`
|
||
MerchantPayload string `json:"merchant_payload"`
|
||
MerchantPayloadV2 string `json:"merchantPayload"`
|
||
Signature string `json:"signature"`
|
||
}
|
||
|
||
func (body zgameConsumeBody) openID() string {
|
||
return firstNonEmpty(body.OpenID, body.OpenIDV2)
|
||
}
|
||
|
||
func (body zgameConsumeBody) orderID() string {
|
||
return firstNonEmpty(body.OrderID, body.OrderIDV2)
|
||
}
|
||
|
||
func (body zgameConsumeBody) gameID() string {
|
||
return firstNonEmpty(body.GameID, body.GameIDV2)
|
||
}
|
||
|
||
func (body zgameConsumeBody) roundID() string {
|
||
return firstNonEmpty(body.RoundID, body.RoundIDV2)
|
||
}
|
||
|
||
func (body zgameConsumeBody) merchantPayload() string {
|
||
return firstNonEmpty(body.MerchantPayload, body.MerchantPayloadV2)
|
||
}
|
||
|
||
func (s *Service) handleZGameConsume(ctx context.Context, app string, config zgameAdapterConfig, secret string, req *gamev1.CallbackRequest, requestHash string) ([]byte, string, string, bool, string, error) {
|
||
var body zgameConsumeBody
|
||
if err := decodeYomiJSON(req.GetRawBody(), &body); err != nil {
|
||
return zgameAdapterError(zgameCodeUnauthorized, "Unauthorized", false, "")
|
||
}
|
||
if !zgameSignatureValid(body.openID(), body.Session, body.Signature, secret) {
|
||
return zgameAdapterError(zgameCodeInvalidSignature, "Invalid Signature", false, body.orderID())
|
||
}
|
||
session, err := s.validZGameSession(ctx, app, gamedomain.PlatformCodeZGame, body.Session, body.gameID())
|
||
if err != nil || !zgameSessionMatches(session, config, body.openID(), body.gameID()) {
|
||
return zgameAdapterError(zgameCodeUnauthorized, "Unauthorized", true, body.orderID())
|
||
}
|
||
amount, amountOK := zgameAmount(body.Amount)
|
||
opType, coinAmount := zgameOpType(body.Type, amount)
|
||
if body.orderID() == "" || body.gameID() == "" || !amountOK || coinAmount < 0 || opType == "" {
|
||
return zgameAdapterError(zgameCodeUnauthorized, "Unauthorized", true, body.orderID())
|
||
}
|
||
if session.GameID == "" {
|
||
session.GameID = body.gameID()
|
||
}
|
||
if session.ProviderGameID == "" {
|
||
session.ProviderGameID = body.gameID()
|
||
}
|
||
providerOrderID := zgameProviderOrderID(body.orderID(), opType)
|
||
result, err := s.applyYomiCoinChange(ctx, app, req, session, providerOrderID, body.roundID(), opType, coinAmount, "", requestHash)
|
||
if err != nil {
|
||
code := zgameCodeFromError(err)
|
||
balance := s.bestEffortCoinBalance(ctx, app, req.GetMeta().GetRequestId(), session.UserID)
|
||
raw, contentType := zgameBalanceJSON(code, zgameMessageFromError(err), balance)
|
||
return raw, contentType, strconv.Itoa(code), true, body.orderID(), nil
|
||
}
|
||
raw, contentType := zgameBalanceJSON(zgameCodeOK, "success", result.BalanceAfter)
|
||
return raw, contentType, strconv.Itoa(zgameCodeOK), true, body.orderID(), nil
|
||
}
|
||
|
||
func (s *Service) zgameSignedSession(ctx context.Context, app string, config zgameAdapterConfig, secret string, req *gamev1.CallbackRequest) (zgameSignedSessionBody, bool, gamedomain.LaunchSession, error) {
|
||
var body zgameSignedSessionBody
|
||
if err := decodeYomiJSON(req.GetRawBody(), &body); err != nil {
|
||
return zgameSignedSessionBody{}, false, gamedomain.LaunchSession{}, xerr.New(xerr.InvalidArgument, "invalid payload")
|
||
}
|
||
if !zgameSignatureValid(body.openID(), body.Session, body.Signature, secret) {
|
||
return body, false, gamedomain.LaunchSession{}, xerr.New(xerr.Unauthorized, "signature invalid")
|
||
}
|
||
session, err := s.validZGameSession(ctx, app, gamedomain.PlatformCodeZGame, body.Session, body.gameIDText())
|
||
if err != nil {
|
||
return body, true, gamedomain.LaunchSession{}, err
|
||
}
|
||
if !zgameSessionMatches(session, config, body.openID(), body.gameIDText()) {
|
||
return body, true, gamedomain.LaunchSession{}, xerr.New(xerr.Unauthorized, "session mismatch")
|
||
}
|
||
return body, true, session, nil
|
||
}
|
||
|
||
func (s *Service) validZGameSession(ctx context.Context, app string, platformCode string, token string, providerGameID string) (gamedomain.LaunchSession, error) {
|
||
token = strings.TrimSpace(token)
|
||
if token == "" {
|
||
return gamedomain.LaunchSession{}, xerr.New(xerr.Unauthorized, "session invalid")
|
||
}
|
||
// 新版启动链路使用服务端短期 js_code;保留 JWT 兼容旧测试链接,避免已打开的 H5 立即失效。
|
||
if strings.Count(token, ".") == 2 {
|
||
if session, err := s.appSessionFromAccessToken(app, token); err == nil {
|
||
return session, nil
|
||
}
|
||
}
|
||
if session, err := s.validScopedYomiSession(ctx, app, platformCode, token, providerGameID); err == nil {
|
||
return session, nil
|
||
}
|
||
if strings.Count(token, ".") == 2 {
|
||
return s.appSessionFromAccessToken(app, token)
|
||
}
|
||
return gamedomain.LaunchSession{}, xerr.New(xerr.Unauthorized, "session invalid")
|
||
}
|
||
|
||
func zgameSessionMatches(session gamedomain.LaunchSession, config zgameAdapterConfig, openID string, gameID string) bool {
|
||
if strings.TrimSpace(gameID) != "" && strings.TrimSpace(session.ProviderGameID) != "" && strings.TrimSpace(gameID) != strings.TrimSpace(session.ProviderGameID) {
|
||
return false
|
||
}
|
||
openID = strings.TrimSpace(openID)
|
||
return openID != "" && (openID == externalUserID(session, config.UIDMode) ||
|
||
openID == strings.TrimSpace(session.DisplayUserID) ||
|
||
openID == strconv.FormatInt(session.UserID, 10))
|
||
}
|
||
|
||
func zgameSignatureValid(openID string, session string, signature string, secret string) bool {
|
||
openID = strings.TrimSpace(openID)
|
||
session = strings.TrimSpace(session)
|
||
secret = strings.TrimSpace(secret)
|
||
signature = strings.TrimSpace(signature)
|
||
if openID == "" || session == "" || secret == "" || signature == "" {
|
||
return false
|
||
}
|
||
sum := sha256.Sum256([]byte(openID + session + secret))
|
||
expected := hex.EncodeToString(sum[:])
|
||
return subtle.ConstantTimeCompare([]byte(strings.ToLower(signature)), []byte(expected)) == 1
|
||
}
|
||
|
||
func zgameLoginData(session gamedomain.LaunchSession, config zgameAdapterConfig, requestedOpenID string, token string, snapshot yomiUserSnapshot, now time.Time) map[string]any {
|
||
openID := strings.TrimSpace(requestedOpenID)
|
||
if openID == "" {
|
||
openID = externalUserID(session, config.UIDMode)
|
||
}
|
||
return map[string]any{
|
||
"session": token,
|
||
"session_expired": zgameSessionTTLMS(session, now),
|
||
"openId": openID,
|
||
"userName": snapshot.Nickname,
|
||
"nickname": snapshot.Nickname,
|
||
"avatar": snapshot.Avatar,
|
||
"balance": snapshot.Balance,
|
||
"level": config.Level,
|
||
"country": "",
|
||
"gender": config.Gender,
|
||
"sex": config.Gender,
|
||
}
|
||
}
|
||
|
||
func (s *Service) zgameUserInfoForOpenID(ctx context.Context, app string, config zgameAdapterConfig, req *gamev1.CallbackRequest, session gamedomain.LaunchSession, openID string) (map[string]any, bool) {
|
||
userID := int64(0)
|
||
if openID == externalUserID(session, config.UIDMode) || openID == strings.TrimSpace(session.DisplayUserID) {
|
||
userID = session.UserID
|
||
} else if !strings.EqualFold(config.UIDMode, "display_user_id") {
|
||
// 批量查询没有 display_id -> user_id 解析依赖;默认 user_id 模式可直接按 open_id 查用户资料。
|
||
parsed, err := strconv.ParseInt(openID, 10, 64)
|
||
if err == nil && parsed > 0 {
|
||
userID = parsed
|
||
}
|
||
}
|
||
if userID <= 0 {
|
||
return nil, false
|
||
}
|
||
snapshot, err := s.yomiUserSnapshot(ctx, app, req, userID)
|
||
if err != nil {
|
||
return nil, false
|
||
}
|
||
return zgameUserInfoData(config, snapshot), true
|
||
}
|
||
|
||
func zgameUserInfoData(config zgameAdapterConfig, snapshot yomiUserSnapshot) map[string]any {
|
||
return map[string]any{
|
||
"userName": snapshot.Nickname,
|
||
"nickname": snapshot.Nickname,
|
||
"gender": config.Gender,
|
||
"level": config.Level,
|
||
"avatar": snapshot.Avatar,
|
||
"balance": snapshot.Balance,
|
||
}
|
||
}
|
||
|
||
func zgameSessionTTLMS(session gamedomain.LaunchSession, now time.Time) int64 {
|
||
if session.ExpiresAtMS <= 0 {
|
||
return int64(zgameDefaultTokenTTL / time.Millisecond)
|
||
}
|
||
ttl := session.ExpiresAtMS - now.UnixMilli()
|
||
if ttl < 0 {
|
||
return 0
|
||
}
|
||
return ttl
|
||
}
|
||
|
||
func zgameAmount(value json.Number) (int64, bool) {
|
||
raw := strings.TrimSpace(value.String())
|
||
if raw == "" {
|
||
return 0, false
|
||
}
|
||
amount, err := strconv.ParseInt(raw, 10, 64)
|
||
if err != nil {
|
||
return 0, false
|
||
}
|
||
return amount, true
|
||
}
|
||
|
||
func zgameOpType(op int32, amount int64) (string, int64) {
|
||
if amount == -1<<63 {
|
||
return "", -1
|
||
}
|
||
coinAmount := amount
|
||
if coinAmount < 0 {
|
||
coinAmount = -coinAmount
|
||
}
|
||
if amount < 0 || op == 1 {
|
||
return "credit", coinAmount
|
||
}
|
||
if op == 0 {
|
||
return "debit", coinAmount
|
||
}
|
||
return "", coinAmount
|
||
}
|
||
|
||
func zgameProviderOrderID(orderID string, opType string) string {
|
||
orderID = strings.TrimSpace(orderID)
|
||
opType = strings.TrimSpace(opType)
|
||
if orderID == "" || opType == "" {
|
||
return orderID
|
||
}
|
||
return orderID + ":" + opType
|
||
}
|
||
|
||
func zgameAnyString(value any) string {
|
||
switch typed := value.(type) {
|
||
case nil:
|
||
return ""
|
||
case string:
|
||
return strings.TrimSpace(typed)
|
||
case json.Number:
|
||
return strings.TrimSpace(typed.String())
|
||
case float64:
|
||
if typed == math.Trunc(typed) {
|
||
return strconv.FormatInt(int64(typed), 10)
|
||
}
|
||
return strconv.FormatFloat(typed, 'f', -1, 64)
|
||
case float32:
|
||
return strconv.FormatFloat(float64(typed), 'f', -1, 32)
|
||
case int:
|
||
return strconv.Itoa(typed)
|
||
case int64:
|
||
return strconv.FormatInt(typed, 10)
|
||
case int32:
|
||
return strconv.FormatInt(int64(typed), 10)
|
||
case uint64:
|
||
return strconv.FormatUint(typed, 10)
|
||
default:
|
||
return strings.TrimSpace(fmt.Sprint(value))
|
||
}
|
||
}
|
||
|
||
func zgameAdapterError(code int, message string, signatureValid bool, providerRequestID string) ([]byte, string, string, bool, string, error) {
|
||
raw, contentType := zgameJSON(code, message, nil)
|
||
return raw, contentType, strconv.Itoa(code), signatureValid, strings.TrimSpace(providerRequestID), nil
|
||
}
|
||
|
||
func zgameJSON(code int, message string, data any) ([]byte, string) {
|
||
if strings.TrimSpace(message) == "" && code == zgameCodeOK {
|
||
message = "success"
|
||
}
|
||
return jsonResponse(map[string]any{
|
||
"code": code,
|
||
"errmsg": strings.TrimSpace(message),
|
||
"data": data,
|
||
})
|
||
}
|
||
|
||
func zgameBalanceJSON(code int, message string, balance int64) ([]byte, string) {
|
||
if strings.TrimSpace(message) == "" && code == zgameCodeOK {
|
||
message = "success"
|
||
}
|
||
return jsonResponse(map[string]any{
|
||
"code": code,
|
||
"errmsg": strings.TrimSpace(message),
|
||
"balance": balance,
|
||
"data": map[string]any{
|
||
"balance": balance,
|
||
},
|
||
})
|
||
}
|
||
|
||
func zgameCodeFromError(err error) int {
|
||
switch {
|
||
case err == nil:
|
||
return zgameCodeOK
|
||
case xerr.IsCode(err, xerr.InsufficientBalance):
|
||
return zgameCodeInsufficientBalance
|
||
case xerr.IsCode(err, xerr.SessionExpired), xerr.IsCode(err, xerr.Unauthorized):
|
||
return zgameCodeUnauthorized
|
||
default:
|
||
return zgameCodeInternalError
|
||
}
|
||
}
|
||
|
||
func zgameMessageFromError(err error) string {
|
||
switch zgameCodeFromError(err) {
|
||
case zgameCodeInsufficientBalance:
|
||
return "Insufficient Funds"
|
||
case zgameCodeUnauthorized:
|
||
return "Unauthorized"
|
||
case zgameCodeOK:
|
||
return "success"
|
||
default:
|
||
return "Internal Error"
|
||
}
|
||
}
|