package gameopen import ( "chatapp3-golang/internal/integration" "chatapp3-golang/internal/model" "chatapp3-golang/internal/utils" "context" "fmt" "net/http" "strconv" "strings" "time" ) // HandleQueryUser 处理统一查询用户信息回调。 func (s *GameOpenService) HandleQueryUser(ctx context.Context, req QueryUserRequest, rawJSON string) CallbackResponse { if err := s.validateQueryUserRequest(req); err != nil { resp := failResponse(gameOpenCodeBadRequest, err.Error()) s.saveCallbackLog(ctx, "query-user", rawJSON, resp, "", req, nil, gameOpenLogStatusFailed) return resp } if !verifySign(req.Sign, buildQueryUserSign(req, s.cfg.GameOpen.AppKey)) { resp := failResponse(gameOpenCodeSignatureError, "signature error") s.saveCallbackLog(ctx, "query-user", rawJSON, resp, "", req, nil, gameOpenLogStatusFailed) return resp } req.Token = normalizeCallbackToken(req.Token) credential, err := s.java.AuthenticateToken(ctx, req.Token) if err != nil { resp := failResponse(gameOpenCodeTokenInvalid, "token invalid") s.saveCallbackLog(ctx, "query-user", rawJSON, resp, "", req, nil, gameOpenLogStatusFailed) return resp } resolved, err := s.resolveUserWithToken(ctx, credential, req.UID) if err != nil { resp := failResponse(gameOpenCodeTokenInvalid, "uid token mismatch") userID := int64(credential.UserID) s.saveCallbackLog(ctx, "query-user", rawJSON, resp, credential.SysOrigin, req, &userID, gameOpenLogStatusFailed) return resp } userID := resolved.userID profile := resolved.profile if int64(profile.ID) == 0 { profile, err = s.java.GetUserProfile(ctx, userID) } if err != nil { resp := failResponse(gameOpenCodeServerError, "profile load failed") s.saveCallbackLog(ctx, "query-user", rawJSON, resp, credential.SysOrigin, req, &userID, gameOpenLogStatusFailed) return resp } balance, err := s.readUserBalance(ctx, userID) if err != nil { resp := failResponse(gameOpenCodeServerError, "balance load failed") s.saveCallbackLog(ctx, "query-user", rawJSON, resp, credential.SysOrigin, req, &userID, gameOpenLogStatusFailed) return resp } resp := successResponse(map[string]any{ "uid": req.UID, "nickname": profile.UserNickname, "avatar": profile.UserAvatar, "coin": balance, }) s.saveCallbackLog(ctx, "query-user", rawJSON, resp, credential.SysOrigin, req, &userID, gameOpenLogStatusSuccess) return resp } // HandleUpdateCoin 处理统一游戏币变更回调。 func (s *GameOpenService) HandleUpdateCoin(ctx context.Context, req UpdateCoinRequest, rawJSON string) CallbackResponse { if err := s.validateUpdateCoinRequest(req); err != nil { resp := failResponse(gameOpenCodeBadRequest, err.Error()) s.saveCallbackLog(ctx, "update-coin", rawJSON, resp, "", req, nil, gameOpenLogStatusFailed) return resp } if !verifySign(req.Sign, buildUpdateCoinSign(req, s.cfg.GameOpen.AppKey)) { resp := failResponse(gameOpenCodeSignatureError, "signature error") s.saveCallbackLog(ctx, "update-coin", rawJSON, resp, "", req, nil, gameOpenLogStatusFailed) return resp } req.Token = normalizeCallbackToken(req.Token) credential, err := s.java.AuthenticateToken(ctx, req.Token) if err != nil { resp := failResponse(gameOpenCodeTokenInvalid, "token invalid") s.saveCallbackLog(ctx, "update-coin", rawJSON, resp, "", req, nil, gameOpenLogStatusFailed) return resp } resolved, err := s.resolveUserWithToken(ctx, credential, req.UID) if err != nil { resp := failResponse(gameOpenCodeTokenInvalid, "uid token mismatch") userID := int64(credential.UserID) s.saveCallbackLog(ctx, "update-coin", rawJSON, resp, credential.SysOrigin, req, &userID, gameOpenLogStatusFailed) return resp } userID := resolved.userID eventID := "GAME_OPEN:" + strings.TrimSpace(req.OrderID) exists, err := s.java.ExistsGoldEvent(ctx, eventID) if err != nil { resp := failResponse(gameOpenCodeServerError, "event check failed") s.saveCallbackLog(ctx, "update-coin", rawJSON, resp, credential.SysOrigin, req, &userID, gameOpenLogStatusFailed) return resp } if exists { resp := failResponse(gameOpenCodeDuplicateOrder, "duplicate orderId") s.saveCallbackLog(ctx, "update-coin", rawJSON, resp, credential.SysOrigin, req, &userID, gameOpenLogStatusFailed) return resp } if err := s.java.ChangeGoldBalance(ctx, integration.GoldReceiptCommand{ ReceiptType: receiptTypeFromType(req.Type), UserID: userID, SysOrigin: credential.SysOrigin, EventID: eventID, Remark: fmt.Sprintf("game=%s rewardType=%d round=%s", req.GameID, req.RewardType, req.RoundID), Amount: integration.NewPennyAmountPayloadFromDollar(req.Coin), CloseDelayAsset: false, OpUserType: "APP", CustomizeOrigin: "GAME_OPEN_" + strings.TrimSpace(req.GameID), CustomizeOriginDesc: "GAME OPEN[" + strings.TrimSpace(req.GameID) + "]", }); err != nil { resp := failResponse(gameOpenCodeServerError, err.Error()) s.saveCallbackLog(ctx, "update-coin", rawJSON, resp, credential.SysOrigin, req, &userID, gameOpenLogStatusFailed) return resp } balance, err := s.readUserBalance(ctx, userID) if err != nil { resp := failResponse(gameOpenCodeServerError, "balance load failed") s.saveCallbackLog(ctx, "update-coin", rawJSON, resp, credential.SysOrigin, req, &userID, gameOpenLogStatusFailed) return resp } resp := successResponse(map[string]any{"coin": balance}) s.saveCallbackLog(ctx, "update-coin", rawJSON, resp, credential.SysOrigin, req, &userID, gameOpenLogStatusSuccess) return resp } // HandleSupplement 处理统一补单回调,当前只做签名校验和日志落库。 func (s *GameOpenService) HandleSupplement(ctx context.Context, req SupplementRequest, rawJSON string) CallbackResponse { if err := s.validateSupplementRequest(req); err != nil { resp := failResponse(gameOpenCodeBadRequest, err.Error()) s.saveCallbackLog(ctx, "supplement", rawJSON, resp, "", req, nil, gameOpenLogStatusFailed) return resp } if !verifySign(req.Sign, buildSupplementSign(req, s.cfg.GameOpen.AppKey)) { resp := failResponse(gameOpenCodeSignatureError, "signature error") s.saveCallbackLog(ctx, "supplement", rawJSON, resp, "", req, nil, gameOpenLogStatusFailed) return resp } userID, err := s.resolveUserID(ctx, req.UID) if err != nil { resp := failResponse(gameOpenCodeTokenInvalid, "uid invalid") s.saveCallbackLog(ctx, "supplement", rawJSON, resp, "", req, nil, gameOpenLogStatusFailed) return resp } balance, err := s.readUserBalance(ctx, userID) if err != nil { resp := failResponse(gameOpenCodeServerError, "balance load failed") s.saveCallbackLog(ctx, "supplement", rawJSON, resp, "", req, &userID, gameOpenLogStatusFailed) return resp } resp := successResponse(map[string]any{"coin": balance}) s.saveCallbackLog(ctx, "supplement", rawJSON, resp, "", req, &userID, gameOpenLogStatusPending) return resp } // GetIntegrationInfo 返回当前统一三方游戏联调信息。 func (s *GameOpenService) GetIntegrationInfo(ctx context.Context, publicBaseURL string) (*IntegrationInfoResponse, error) { baseURL := strings.TrimRight(strings.TrimSpace(publicBaseURL), "/") if baseURL == "" { baseURL = strings.TrimRight(strings.TrimSpace(s.cfg.GameOpen.PublicBaseURL), "/") } if baseURL == "" { return nil, NewAppError(http.StatusServiceUnavailable, "public_base_url_required", "public base url is required") } if s.cfg.GameOpen.TestUserID <= 0 { return nil, NewAppError(http.StatusServiceUnavailable, "test_user_required", "test user is required") } token, err := s.java.CreateIfAbsentUserCredentialToken(ctx, s.cfg.GameOpen.TestUserID) if err != nil { return nil, err } return &IntegrationInfoResponse{ Domain: baseURL, QueryUser: baseURL + "/game/open/user-info", UpdateCoin: baseURL + "/game/open/update-coin", Supplement: baseURL + "/game/open/supplement", UID: defaultIfBlank(s.cfg.GameOpen.TestAccount, strconv.FormatInt(s.cfg.GameOpen.TestUserID, 10)), Password: s.cfg.GameOpen.TestPassword, Token: strings.TrimSpace(token), Key: s.cfg.GameOpen.AppKey, }, nil } func (s *GameOpenService) readUserBalance(ctx context.Context, userID int64) (int64, error) { balances, err := s.java.MapGoldBalance(ctx, []int64{userID}) if err != nil { return 0, err } return balances[userID], nil } type resolvedGameOpenUser struct { userID int64 profile integration.UserProfile } func (s *GameOpenService) resolveUserWithToken(ctx context.Context, credential integration.UserCredential, uid string) (resolvedGameOpenUser, error) { userID := int64(credential.UserID) uid = strings.TrimSpace(uid) if uid == strconv.FormatInt(userID, 10) { return resolvedGameOpenUser{userID: userID}, nil } profile, err := s.java.GetUserProfileByAccount(ctx, credential.SysOrigin, uid) if err == nil { if int64(profile.ID) == userID { return resolvedGameOpenUser{userID: userID, profile: profile}, nil } return resolvedGameOpenUser{}, fmt.Errorf("uid token mismatch") } profile, err = s.java.GetUserProfile(ctx, userID) if err != nil { return resolvedGameOpenUser{}, err } if strings.EqualFold(strings.TrimSpace(profile.Account), uid) { return resolvedGameOpenUser{userID: userID, profile: profile}, nil } return resolvedGameOpenUser{}, fmt.Errorf("uid token mismatch") } func (s *GameOpenService) resolveUserID(ctx context.Context, uid string) (int64, error) { uid = strings.TrimSpace(uid) profile, err := s.java.GetUserProfileByAccountOne(ctx, uid) if err == nil && int64(profile.ID) > 0 { return int64(profile.ID), nil } if userID := parseInt64Default(uid); userID > 0 { return userID, nil } return 0, fmt.Errorf("uid invalid") } func (s *GameOpenService) saveCallbackLog(ctx context.Context, requestType, rawJSON string, resp CallbackResponse, sysOrigin string, req any, userID *int64, status string) { if s == nil || s.repo.DB == nil { return } id, err := utils.NextID() if err != nil { return } record := model.GameOpenCallbackLog{ ID: id, RequestType: requestType, SysOrigin: strings.TrimSpace(sysOrigin), RequestJSON: rawJSON, ResponseJSON: utils.MustJSONString(resp, ""), BizCode: resp.ErrorCode, BizMessage: resp.ErrorMsg, Status: status, CreateTime: time.Now(), UpdateTime: time.Now(), } if userID != nil && *userID > 0 { value := *userID record.UserID = &value } switch typed := req.(type) { case QueryUserRequest: record.GameID = typed.GameID record.RoomID = typed.RoomID case UpdateCoinRequest: record.OrderID = typed.OrderID record.GameID = typed.GameID record.RoundID = typed.RoundID record.RoomID = typed.RoomID case SupplementRequest: record.OrderID = typed.OrderID record.GameID = typed.GameID record.RoundID = typed.RoundID record.RoomID = typed.RoomID } _ = s.repo.DB.WithContext(ctx).Create(&record).Error } func (s *GameOpenService) validateQueryUserRequest(req QueryUserRequest) error { switch { case strings.TrimSpace(req.GameID) == "": return newBadRequest("gameId is required") case strings.TrimSpace(req.UID) == "": return newBadRequest("uid is required") case strings.TrimSpace(req.Token) == "": return newBadRequest("token is required") case strings.TrimSpace(req.Sign) == "": return newBadRequest("sign is required") case strings.TrimSpace(s.cfg.GameOpen.AppKey) == "": return newBadRequest("app key is blank") default: return nil } } func (s *GameOpenService) validateUpdateCoinRequest(req UpdateCoinRequest) error { switch { case strings.TrimSpace(req.OrderID) == "": return newBadRequest("orderId is required") case strings.TrimSpace(req.GameID) == "": return newBadRequest("gameId is required") case strings.TrimSpace(req.RoundID) == "": return newBadRequest("roundId is required") case strings.TrimSpace(req.UID) == "": return newBadRequest("uid is required") case req.Coin <= 0: return newBadRequest("coin is required") case req.Type != 1 && req.Type != 2: return newBadRequest("type must be 1 or 2") case strings.TrimSpace(req.Token) == "": return newBadRequest("token is required") case strings.TrimSpace(req.Sign) == "": return newBadRequest("sign is required") case strings.TrimSpace(s.cfg.GameOpen.AppKey) == "": return newBadRequest("app key is blank") default: return nil } } func (s *GameOpenService) validateSupplementRequest(req SupplementRequest) error { switch { case strings.TrimSpace(req.OrderID) == "": return newBadRequest("orderId is required") case strings.TrimSpace(req.GameID) == "": return newBadRequest("gameId is required") case strings.TrimSpace(req.RoundID) == "": return newBadRequest("roundId is required") case strings.TrimSpace(req.UID) == "": return newBadRequest("uid is required") case req.Coin <= 0: return newBadRequest("coin is required") case strings.TrimSpace(req.Sign) == "": return newBadRequest("sign is required") case strings.TrimSpace(s.cfg.GameOpen.AppKey) == "": return newBadRequest("app key is blank") default: return nil } } func defaultIfBlank(value, fallback string) string { if strings.TrimSpace(value) == "" { return fallback } return strings.TrimSpace(value) } func parseInt64Default(value string) int64 { parsed, _ := strconv.ParseInt(strings.TrimSpace(value), 10, 64) return parsed }