package baishun import ( "chatapp3-golang/internal/integration" "chatapp3-golang/internal/model" "chatapp3-golang/internal/utils" "context" "crypto/md5" "crypto/rand" "encoding/hex" "encoding/json" "fmt" "strconv" "strings" "time" "gorm.io/gorm/clause" ) // HandleToken 处理百顺获取启动 code/token 的回调。 func (s *BaishunService) HandleToken(ctx context.Context, req BaishunTokenRequest, rawJSON string) baishunStandardResponse { runtimeCfg, ok := s.validateCommonSignature(ctx, req.AppID, "", req.Signature, req.SignatureNonce, req.Timestamp) if !ok { return s.failStandard(baishunCodeSignatureError, "signature error") } var session model.BaishunLaunchSession if err := s.repo.DB.WithContext(ctx). Where("launch_code = ?", strings.TrimSpace(req.Code)). First(&session).Error; err != nil { resp := s.failStandard(baishunCodeTokenExpired, "code invalid") s.saveCallbackLog(ctx, "token", rawJSON, utils.MustJSONString(resp, ""), "", req.UserID, nil, nil, baishunCallbackStatusFailed, resp.Code, resp.Message) return resp } if session.Status != baishunSessionCodeIssued || time.Now().After(session.LaunchCodeExpireTime) { resp := s.failStandard(baishunCodeTokenExpired, "code expired") s.saveCallbackLog(ctx, "token", rawJSON, utils.MustJSONString(resp, ""), session.SysOrigin, req.UserID, nil, nil, baishunCallbackStatusFailed, resp.Code, resp.Message) return resp } if strings.TrimSpace(req.UserID) != "" && strconv.FormatInt(session.UserID, 10) != strings.TrimSpace(req.UserID) { resp := s.failStandard(baishunCodeTokenExpired, "code invalid") s.saveCallbackLog(ctx, "token", rawJSON, utils.MustJSONString(resp, ""), session.SysOrigin, req.UserID, nil, nil, baishunCallbackStatusFailed, resp.Code, resp.Message) return resp } ssToken, err := s.randomToken(32) if err != nil { resp := s.failStandard(baishunCodeServerError, "token create failed") s.saveCallbackLog(ctx, "token", rawJSON, utils.MustJSONString(resp, ""), session.SysOrigin, req.UserID, nil, nil, baishunCallbackStatusFailed, resp.Code, resp.Message) return resp } expire := time.Now().Add(time.Duration(runtimeCfg.ssTokenTTLSeconds()) * time.Second) if err := s.repo.DB.WithContext(ctx). Model(&model.BaishunLaunchSession{}). Where("id = ?", session.ID). Updates(map[string]any{ "ss_token": ssToken, "ss_token_expire_time": expire, "status": baishunSessionTokenIssued, "update_time": time.Now(), }).Error; err != nil { resp := s.failStandard(baishunCodeServerError, "session update failed") s.saveCallbackLog(ctx, "token", rawJSON, utils.MustJSONString(resp, ""), session.SysOrigin, req.UserID, nil, nil, baishunCallbackStatusFailed, resp.Code, resp.Message) return resp } userInfo := s.readUserInfo(ctx, session.UserID) balance := s.readUserBalance(ctx, session.UserID) resp := baishunStandardResponse{ Code: 0, Message: "succeed", UniqueID: s.uniqueID(), Data: map[string]any{ "ss_token": ssToken, "expire_date": expire.UnixMilli(), }, UserInfo: map[string]any{ "user_id": req.UserID, "user_name": userInfo.UserNickname, "user_avatar": userInfo.UserAvatar, "balance": balance, }, } s.saveCallbackLog(ctx, "token", rawJSON, utils.MustJSONString(resp, ""), session.SysOrigin, req.UserID, nil, nil, baishunCallbackStatusSuccess, 0, resp.Message) return resp } // HandleProfile 处理百顺获取用户资料的回调。 func (s *BaishunService) HandleProfile(ctx context.Context, req BaishunProfileRequest, rawJSON string) baishunStandardResponse { if _, ok := s.validateCommonSignature(ctx, req.AppID, "", req.Signature, req.SignatureNonce, req.Timestamp); !ok { return s.failStandard(baishunCodeSignatureError, "signature error") } session, ok := s.findSessionByToken(ctx, req.SSToken, req.UserID) if !ok { resp := s.failStandard(baishunCodeTokenExpired, "ss_token invalid") s.saveCallbackLog(ctx, "profile", rawJSON, utils.MustJSONString(resp, ""), "", req.UserID, nil, nil, baishunCallbackStatusFailed, resp.Code, resp.Message) return resp } userInfo := s.readUserInfo(ctx, session.UserID) balance := s.readUserBalance(ctx, session.UserID) resp := baishunStandardResponse{ Code: 0, Message: "succeed", UniqueID: s.uniqueID(), Data: map[string]any{ "user_id": req.UserID, "user_name": userInfo.UserNickname, "user_avatar": userInfo.UserAvatar, "balance": balance, "balance_list": []map[string]any{{"name": "Gold", "currency_type": 0, "currency_amount": balance}}, "user_type": 1, }, } _ = s.repo.DB.WithContext(ctx). Model(&model.BaishunLaunchSession{}). Where("id = ?", session.ID). Updates(map[string]any{"status": baishunSessionActive, "update_time": time.Now()}).Error s.saveCallbackLog(ctx, "profile", rawJSON, utils.MustJSONString(resp, ""), session.SysOrigin, req.UserID, nil, nil, baishunCallbackStatusSuccess, 0, resp.Message) return resp } // HandleUpdateToken 处理百顺刷新 ss_token 的回调。 func (s *BaishunService) HandleUpdateToken(ctx context.Context, req BaishunUpdateTokenRequest, rawJSON string) baishunStandardResponse { runtimeCfg, ok := s.validateCommonSignature(ctx, req.AppID, "", req.Signature, req.SignatureNonce, req.Timestamp) if !ok { return s.failStandard(baishunCodeSignatureError, "signature error") } session, ok := s.findSessionByToken(ctx, req.SSToken, req.UserID) if !ok { resp := s.failStandard(baishunCodeTokenExpired, "ss_token invalid") s.saveCallbackLog(ctx, "update-token", rawJSON, utils.MustJSONString(resp, ""), "", req.UserID, nil, nil, baishunCallbackStatusFailed, resp.Code, resp.Message) return resp } newToken, err := s.randomToken(32) if err != nil { resp := s.failStandard(baishunCodeServerError, "token create failed") s.saveCallbackLog(ctx, "update-token", rawJSON, utils.MustJSONString(resp, ""), session.SysOrigin, req.UserID, nil, nil, baishunCallbackStatusFailed, resp.Code, resp.Message) return resp } expire := time.Now().Add(time.Duration(runtimeCfg.ssTokenTTLSeconds()) * time.Second) if err := s.repo.DB.WithContext(ctx). Model(&model.BaishunLaunchSession{}). Where("id = ?", session.ID). Updates(map[string]any{ "ss_token": newToken, "ss_token_expire_time": expire, "status": baishunSessionActive, "update_time": time.Now(), }).Error; err != nil { resp := s.failStandard(baishunCodeServerError, "session update failed") s.saveCallbackLog(ctx, "update-token", rawJSON, utils.MustJSONString(resp, ""), session.SysOrigin, req.UserID, nil, nil, baishunCallbackStatusFailed, resp.Code, resp.Message) return resp } resp := baishunStandardResponse{ Code: 0, Message: "succeed", UniqueID: s.uniqueID(), Data: map[string]any{ "ss_token": newToken, "expire_date": expire.UnixMilli(), }, } s.saveCallbackLog(ctx, "update-token", rawJSON, utils.MustJSONString(resp, ""), session.SysOrigin, req.UserID, nil, nil, baishunCallbackStatusSuccess, 0, resp.Message) return resp } // HandleChangeBalance 处理百顺扣增币回调,并驱动钱包变更。 func (s *BaishunService) HandleChangeBalance(ctx context.Context, req BaishunChangeBalanceRequest, rawJSON string) baishunStandardResponse { if _, ok := s.validateCommonSignature(ctx, req.AppID, "", req.Signature, req.SignatureNonce, req.Timestamp); !ok { return s.failStandard(baishunCodeSignatureError, "signature error") } session, ok := s.findSessionByToken(ctx, req.SSToken, req.UserID) if !ok { resp := s.failStandard(baishunCodeTokenExpired, "ss_token invalid") s.saveCallbackLog(ctx, "change-balance", rawJSON, utils.MustJSONString(resp, ""), "", req.UserID, &req.OrderID, &req.GameRoundID, baishunCallbackStatusFailed, resp.Code, resp.Message) return resp } if req.CurrencyDiff == 0 { resp := s.failStandard(baishunCodeServerError, "currency_diff invalid") s.saveCallbackLog(ctx, "change-balance", rawJSON, utils.MustJSONString(resp, ""), session.SysOrigin, req.UserID, &req.OrderID, &req.GameRoundID, baishunCallbackStatusFailed, resp.Code, resp.Message) return resp } lockKey := fmt.Sprintf("bs:lock:user:%s:%s", session.SysOrigin, req.UserID) lockOK, err := s.repo.Redis.SetNX(ctx, lockKey, "1", 5*time.Second).Result() if err == nil && lockOK { defer s.repo.Redis.Del(context.Background(), lockKey) } var order model.BaishunOrderIdempotency err = s.repo.DB.WithContext(ctx). Where("sys_origin = ? AND order_id = ?", session.SysOrigin, req.OrderID). First(&order).Error if err == nil && order.Status == baishunOrderStatusSuccess && strings.TrimSpace(order.ResponseJSON) != "" { var resp baishunStandardResponse if json.Unmarshal([]byte(order.ResponseJSON), &resp) == nil { s.saveCallbackLog(ctx, "change-balance", rawJSON, order.ResponseJSON, session.SysOrigin, req.UserID, &req.OrderID, &req.GameRoundID, baishunCallbackStatusSuccess, 0, resp.Message) return resp } } walletEventID := fmt.Sprintf("BAISHUN:%s:%s", session.SysOrigin, req.OrderID) exists, existsErr := s.java.ExistsGoldEvent(ctx, walletEventID) if existsErr == nil && !exists { changeErr := s.java.ChangeGoldBalance(ctx, integration.GoldReceiptCommand{ ReceiptType: receiptTypeFromDiff(req.CurrencyDiff), UserID: session.UserID, SysOrigin: session.SysOrigin, EventID: walletEventID, Remark: defaultIfBlank(req.DiffMsg, req.MsgType), Amount: integration.NewPennyAmountPayloadFromDollar(abs64(req.CurrencyDiff)), CloseDelayAsset: false, OpUserType: "APP", CustomizeOrigin: fmt.Sprintf("BAISHUN_GAME_%d", req.GameID), CustomizeOriginDesc: fmt.Sprintf("BAISHUN GAME[%d]", req.GameID), }) if changeErr != nil { message := changeErr.Error() code := baishunCodeServerError if strings.Contains(strings.ToLower(message), "insufficient") || strings.Contains(message, "5000") { code = baishunCodeInsufficientAssets } resp := s.failStandard(code, message) s.persistOrderRecord(ctx, session.SysOrigin, req, walletEventID, baishunOrderStatusFailed, rawJSON, utils.MustJSONString(resp, ""), nil) s.saveCallbackLog(ctx, "change-balance", rawJSON, utils.MustJSONString(resp, ""), session.SysOrigin, req.UserID, &req.OrderID, &req.GameRoundID, baishunCallbackStatusFailed, resp.Code, resp.Message) return resp } } balance := s.readUserBalance(ctx, session.UserID) resp := baishunStandardResponse{ Code: 0, Message: "succeed", UniqueID: s.uniqueID(), Data: map[string]any{ "currency_balance": balance, }, } s.persistOrderRecord(ctx, session.SysOrigin, req, walletEventID, baishunOrderStatusSuccess, rawJSON, utils.MustJSONString(resp, ""), nil) s.saveCallbackLog(ctx, "change-balance", rawJSON, utils.MustJSONString(resp, ""), session.SysOrigin, req.UserID, &req.OrderID, &req.GameRoundID, baishunCallbackStatusSuccess, 0, resp.Message) return resp } // HandleReport 处理百顺上报类回调,目前仅记录日志并返回成功。 func (s *BaishunService) HandleReport(ctx context.Context, payload map[string]any, rawJSON string) baishunStandardResponse { userID := toString(payload["user_id"]) orderID := toString(payload["order_id"]) gameRoundID := toString(payload["game_round_id"]) resp := baishunStandardResponse{ Code: 0, Message: "succeed", UniqueID: s.uniqueID(), Data: map[string]any{}, } s.saveCallbackLog(ctx, "report", rawJSON, utils.MustJSONString(resp, ""), "", userID, stringPtr(orderID), stringPtr(gameRoundID), baishunCallbackStatusSuccess, 0, resp.Message) return resp } // HandleBalanceInfo 处理百顺查询余额回调。 func (s *BaishunService) HandleBalanceInfo(ctx context.Context, req BaishunBalanceInfoRequest, rawJSON string) baishunBalanceInfoResponse { if _, ok := s.validateCommonSignature(ctx, req.AppID, req.AppChannel, req.Signature, req.SignatureNonce, req.Timestamp); !ok { return baishunBalanceInfoResponse{Code: baishunCodeSignatureError, Msg: "signature error"} } userID, _ := strconv.ParseInt(req.UserID, 10, 64) balance := s.readUserBalance(ctx, userID) resp := baishunBalanceInfoResponse{ Code: 0, Msg: "success", Data: map[string]any{"cur_coin": balance}, } s.saveCallbackLog(ctx, "balance-info", rawJSON, utils.MustJSONString(resp, ""), "", req.UserID, nil, nil, baishunCallbackStatusSuccess, 0, resp.Msg) return resp } // validateCommonSignature 校验百顺回调签名。 func (s *BaishunService) validateCommonSignature(ctx context.Context, appID int64, appChannel, signature, nonce string, timestamp int64) (baishunRuntimeConfig, bool) { runtimeCfg, err := s.resolveRuntimeConfigByApp(ctx, appID, appChannel) if err != nil { return baishunRuntimeConfig{}, false } if runtimeCfg.AppID != 0 && appID != 0 && appID != runtimeCfg.AppID { return baishunRuntimeConfig{}, false } if strings.TrimSpace(appChannel) != "" && runtimeCfg.appChannel() != "" && appChannel != runtimeCfg.appChannel() { return baishunRuntimeConfig{}, false } if strings.TrimSpace(signature) == "" || strings.TrimSpace(nonce) == "" || timestamp == 0 || strings.TrimSpace(runtimeCfg.AppKey) == "" { return baishunRuntimeConfig{}, false } if abs64(time.Now().Unix()-timestamp) > 15 { return baishunRuntimeConfig{}, false } expected := signBAISHUN(runtimeCfg.AppKey, timestamp, nonce) if !strings.EqualFold(expected, signature) { return baishunRuntimeConfig{}, false } if !s.rememberNonce(ctx, nonce) { return baishunRuntimeConfig{}, false } return runtimeCfg, true } // rememberNonce 记录已用 nonce,防止回调重放。 func (s *BaishunService) rememberNonce(ctx context.Context, nonce string) bool { ok, err := s.repo.Redis.SetNX(ctx, "bs:sign:nonce:"+nonce, "1", 15*time.Second).Result() return err == nil && ok } // signBAISHUN 按百顺约定生成签名。 func signBAISHUN(appKey string, timestamp int64, nonce string) string { sum := md5.Sum([]byte(nonce + appKey + strconv.FormatInt(timestamp, 10))) return hex.EncodeToString(sum[:]) } // randomNonce 生成随机 nonce。 func (s *BaishunService) randomNonce() string { token, err := s.randomToken(8) if err != nil { return fmt.Sprintf("%d", time.Now().UnixNano()) } return token } // randomToken 生成指定长度的随机 token。 func (s *BaishunService) randomToken(size int) (string, error) { buf := make([]byte, size) if _, err := rand.Read(buf); err != nil { return "", err } return hex.EncodeToString(buf), nil } // uniqueID 生成一条可返回给百顺的唯一 ID。 func (s *BaishunService) uniqueID() string { id, err := utils.NextID() if err != nil { return strconv.FormatInt(time.Now().UnixNano(), 10) } return strconv.FormatInt(id, 10) } // persistOrderRecord 持久化扣增币回调的幂等记录。 func (s *BaishunService) persistOrderRecord(ctx context.Context, sysOrigin string, req BaishunChangeBalanceRequest, walletEventID, status, requestJSON, responseJSON string, assetRecordID *int64) { id, err := utils.NextID() if err != nil { return } record := model.BaishunOrderIdempotency{ ID: id, SysOrigin: sysOrigin, OrderID: req.OrderID, GameRoundID: req.GameRoundID, RoomID: req.RoomID, UserID: parseInt64Default(req.UserID), VendorGameID: req.GameID, CurrencyDiff: req.CurrencyDiff, DiffMsg: req.DiffMsg, MsgType: req.MsgType, CurrencyType: req.CurrencyType, WalletEventID: walletEventID, WalletAssetRecordID: assetRecordID, Status: status, RequestJSON: requestJSON, ResponseJSON: responseJSON, CreateTime: time.Now(), UpdateTime: time.Now(), } _ = s.repo.DB.WithContext(ctx).Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "sys_origin"}, {Name: "order_id"}}, DoUpdates: clause.AssignmentColumns([]string{ "wallet_event_id", "wallet_asset_record_id", "status", "request_json", "response_json", "update_time", }), }).Create(&record).Error } // saveCallbackLog 保存百顺回调审计日志。 func (s *BaishunService) saveCallbackLog(ctx context.Context, requestType, requestJSON, responseJSON, sysOrigin, userID string, orderID, gameRoundID *string, status string, bizCode int, bizMessage string) { id, err := utils.NextID() if err != nil { return } logRecord := model.BaishunCallbackLog{ ID: id, SysOrigin: sysOrigin, RequestType: requestType, RequestJSON: requestJSON, ResponseJSON: responseJSON, BizCode: strconv.Itoa(bizCode), BizMessage: bizMessage, Status: status, CreateTime: time.Now(), UpdateTime: time.Now(), } if orderID != nil { logRecord.OrderID = *orderID } if gameRoundID != nil { logRecord.GameRoundID = *gameRoundID } if strings.TrimSpace(userID) != "" { parsed := parseInt64Default(userID) logRecord.UserID = &parsed } err = s.repo.DB.WithContext(ctx).Create(&logRecord).Error if err != nil && isBizMessageTooLongError(err) { logRecord.BizMessage = truncateRunes(logRecord.BizMessage, legacyBizMessageRuneLimit) _ = s.repo.DB.WithContext(ctx).Create(&logRecord).Error } } // failStandard 构造百顺标准失败回包。 func (s *BaishunService) failStandard(code int, message string) baishunStandardResponse { return baishunStandardResponse{ Code: code, Message: defaultIfBlank(message, "failed"), UniqueID: s.uniqueID(), } }