367 lines
13 KiB
Go
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
|
|
}
|