2026-04-28 18:05:13 +08:00

367 lines
13 KiB
Go

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
}