通用im 中奖消息
This commit is contained in:
parent
a02e22ac74
commit
8e8878a038
@ -69,6 +69,8 @@ func main() {
|
||||
wheelService := wheel.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
||||
smashEggService := smashegg.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
||||
regionIMGroupService := regionimgroup.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
||||
baishunService.SetHighWinBroadcaster(regionIMGroupService)
|
||||
luckyGiftService.SetHighWinBroadcaster(regionIMGroupService)
|
||||
voiceRoomRedPacketService := voiceroomredpacket.NewService(app.Config, app.Repository.DB, app.Repository.Redis, app.Gateways.Wallet, regionIMGroupService, app.Gateways.Room)
|
||||
voiceRoomRedPacketService.SetRegionResolver(&app.Gateways)
|
||||
voiceRoomRocketService := voiceroomrocket.NewService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
||||
|
||||
@ -248,7 +248,7 @@ func registerBaishunRoutes(
|
||||
})
|
||||
callbackGroup.POST("/report", func(c *gin.Context) {
|
||||
raw, payload := readRawPayload(c)
|
||||
writeBaishunJSON(c, http.StatusOK, baishunService.HandleReport(c.Request.Context(), payload, raw))
|
||||
writeBaishunJSON(c, http.StatusOK, baishunService.HandleReport(c.Request.Context(), mapToReportRequest(payload), raw))
|
||||
})
|
||||
callbackGroup.POST("/balance-info", func(c *gin.Context) {
|
||||
raw, payload := readRawPayload(c)
|
||||
@ -338,6 +338,16 @@ func mapToChangeBalanceRequest(payload map[string]any) baishun.BaishunChangeBala
|
||||
}
|
||||
}
|
||||
|
||||
// mapToReportRequest 把百顺 report 回调报文映射成服务层 DTO。
|
||||
func mapToReportRequest(payload map[string]any) baishun.BaishunReportRequest {
|
||||
return baishun.BaishunReportRequest{
|
||||
ReportType: asString(payload["report_type"]),
|
||||
ReportMsg: asMap(payload["report_msg"]),
|
||||
UserID: asString(payload["user_id"]),
|
||||
SSToken: asString(payload["ss_token"]),
|
||||
}
|
||||
}
|
||||
|
||||
// mapToBalanceInfoRequest 把百顺 balance-info 回调报文映射成服务层 DTO。
|
||||
func mapToBalanceInfoRequest(payload map[string]any) baishun.BaishunBalanceInfoRequest {
|
||||
return baishun.BaishunBalanceInfoRequest{
|
||||
@ -350,6 +360,26 @@ func mapToBalanceInfoRequest(payload map[string]any) baishun.BaishunBalanceInfoR
|
||||
}
|
||||
}
|
||||
|
||||
// asMap 宽松读取对象字段。
|
||||
func asMap(value any) map[string]any {
|
||||
switch typed := value.(type) {
|
||||
case map[string]any:
|
||||
return typed
|
||||
case string:
|
||||
result := map[string]any{}
|
||||
_ = json.Unmarshal([]byte(typed), &result)
|
||||
return result
|
||||
default:
|
||||
body, err := json.Marshal(typed)
|
||||
if err != nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
result := map[string]any{}
|
||||
_ = json.Unmarshal(body, &result)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// asString 宽松读取字符串字段。
|
||||
func asString(value any) string {
|
||||
if value == nil {
|
||||
|
||||
@ -243,21 +243,6 @@ func (s *BaishunService) HandleChangeBalance(ctx context.Context, req BaishunCha
|
||||
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
|
||||
}
|
||||
|
||||
func (s *BaishunService) reportGameConsumeTaskEvent(ctx context.Context, session model.BaishunLaunchSession, req BaishunChangeBalanceRequest) {
|
||||
if s.taskReporter == nil || req.CurrencyDiff >= 0 {
|
||||
return
|
||||
|
||||
@ -49,6 +49,7 @@ func newTestBaishunService(t *testing.T) (*BaishunService, *gorm.DB) {
|
||||
&model.BaishunProviderConfig{},
|
||||
&model.BaishunGameCatalog{},
|
||||
&model.BaishunLaunchSession{},
|
||||
&model.BaishunCallbackLog{},
|
||||
&model.BaishunRoomState{},
|
||||
); err != nil {
|
||||
t.Fatalf("auto migrate: %v", err)
|
||||
|
||||
291
internal/service/baishun/report.go
Normal file
291
internal/service/baishun/report.go
Normal file
@ -0,0 +1,291 @@
|
||||
package baishun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"chatapp3-golang/internal/model"
|
||||
"chatapp3-golang/internal/service/highwin"
|
||||
"chatapp3-golang/internal/utils"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
baishunReportTypeStart = "game_start"
|
||||
baishunReportTypeSettle = "game_settle"
|
||||
baishunReportBetTTL = 24 * time.Hour
|
||||
baishunReportBetRedisPrefix = "baishun:report:bet"
|
||||
)
|
||||
|
||||
type baishunGameBetRecord struct {
|
||||
SysOrigin string `json:"sysOrigin"`
|
||||
RoomID string `json:"roomId"`
|
||||
GameID int64 `json:"gameId"`
|
||||
GameRoundID string `json:"gameRoundId"`
|
||||
UserID int64 `json:"userId"`
|
||||
Bet int64 `json:"bet"`
|
||||
}
|
||||
|
||||
func (s *BaishunService) HandleReport(ctx context.Context, req BaishunReportRequest, rawJSON string) baishunStandardResponse {
|
||||
resp := baishunStandardResponse{
|
||||
Code: 0,
|
||||
Message: "succeed",
|
||||
UniqueID: s.uniqueID(),
|
||||
Data: map[string]any{},
|
||||
}
|
||||
|
||||
sysOrigin := ""
|
||||
if session, ok := s.findSessionByToken(ctx, req.SSToken, req.UserID); ok {
|
||||
sysOrigin = session.SysOrigin
|
||||
if err := s.handleReportHighWins(ctx, session.SysOrigin, req); err != nil {
|
||||
log.Printf("handle baishun report high win failed userId=%s reportType=%s: %v", req.UserID, req.ReportType, err)
|
||||
}
|
||||
}
|
||||
|
||||
s.saveCallbackLog(
|
||||
ctx,
|
||||
"report",
|
||||
rawJSON,
|
||||
utils.MustJSONString(resp, ""),
|
||||
sysOrigin,
|
||||
req.UserID,
|
||||
nil,
|
||||
stringPtr(reportGameRoundID(req.ReportMsg)),
|
||||
baishunCallbackStatusSuccess,
|
||||
0,
|
||||
resp.Message,
|
||||
)
|
||||
return resp
|
||||
}
|
||||
|
||||
func (s *BaishunService) handleReportHighWins(ctx context.Context, sysOrigin string, req BaishunReportRequest) error {
|
||||
switch strings.ToLower(strings.TrimSpace(req.ReportType)) {
|
||||
case baishunReportTypeStart:
|
||||
return s.rememberBaishunGameBets(ctx, sysOrigin, req.ReportMsg)
|
||||
case baishunReportTypeSettle:
|
||||
return s.publishBaishunGameHighWins(ctx, sysOrigin, req.ReportMsg)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *BaishunService) rememberBaishunGameBets(ctx context.Context, sysOrigin string, payload map[string]any) error {
|
||||
if s == nil || s.repo.Redis == nil {
|
||||
return nil
|
||||
}
|
||||
roomID := reportText(payload, "room_id", "roomId")
|
||||
gameRoundID := reportGameRoundID(payload)
|
||||
if strings.TrimSpace(roomID) == "" || strings.TrimSpace(gameRoundID) == "" {
|
||||
return nil
|
||||
}
|
||||
gameID := reportInt64(payload, "game_id", "gameId")
|
||||
for _, player := range reportPlayers(payload) {
|
||||
if reportInt64(player, "is_ai", "isAi") != 0 {
|
||||
continue
|
||||
}
|
||||
userID := reportInt64(player, "user_id", "userId")
|
||||
bet := reportInt64(player, "bet")
|
||||
if userID <= 0 || bet <= 0 {
|
||||
continue
|
||||
}
|
||||
record := baishunGameBetRecord{
|
||||
SysOrigin: normalizeAdminSysOrigin(sysOrigin),
|
||||
RoomID: roomID,
|
||||
GameID: gameID,
|
||||
GameRoundID: gameRoundID,
|
||||
UserID: userID,
|
||||
Bet: bet,
|
||||
}
|
||||
body, err := json.Marshal(record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.repo.Redis.Set(ctx, baishunGameBetKey(record.SysOrigin, gameRoundID, userID), string(body), baishunReportBetTTL).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *BaishunService) publishBaishunGameHighWins(ctx context.Context, sysOrigin string, payload map[string]any) error {
|
||||
if s == nil || s.highWinBroadcaster == nil || s.repo.Redis == nil {
|
||||
return nil
|
||||
}
|
||||
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
|
||||
roomID := reportText(payload, "room_id", "roomId")
|
||||
gameRoundID := reportGameRoundID(payload)
|
||||
if strings.TrimSpace(roomID) == "" || strings.TrimSpace(gameRoundID) == "" {
|
||||
return nil
|
||||
}
|
||||
gameID := reportInt64(payload, "game_id", "gameId")
|
||||
gameCover := s.resolveBaishunReportGameCover(ctx, sysOrigin, roomID, gameID)
|
||||
|
||||
for _, player := range reportPlayers(payload) {
|
||||
if reportInt64(player, "is_ai", "isAi") != 0 {
|
||||
continue
|
||||
}
|
||||
userID := reportInt64(player, "user_id", "userId")
|
||||
reward := reportInt64(player, "reward")
|
||||
if userID <= 0 || reward <= 0 {
|
||||
continue
|
||||
}
|
||||
betRecord, ok, err := s.loadBaishunGameBet(ctx, sysOrigin, gameRoundID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok || betRecord.Bet <= 0 {
|
||||
continue
|
||||
}
|
||||
multiple := highwin.Multiple(reward, betRecord.Bet)
|
||||
if !highwin.ShouldBroadcast(multiple, reward) {
|
||||
continue
|
||||
}
|
||||
data := map[string]any{
|
||||
"gameId": gameID,
|
||||
"gameRoundId": gameRoundID,
|
||||
"gameUrl": gameCover,
|
||||
"betAmount": betRecord.Bet,
|
||||
"currencyDiff": reward,
|
||||
"rank": reportInt64(player, "rank"),
|
||||
"score": reportInt64(player, "score"),
|
||||
}
|
||||
if err := highwin.SendRegionBroadcast(ctx, s.highWinBroadcaster, highwin.BroadcastEvent{
|
||||
SysOrigin: sysOrigin,
|
||||
Type: highwin.MessageTypeBaishunWin,
|
||||
UserID: userID,
|
||||
RoomID: roomID,
|
||||
Multiple: multiple,
|
||||
Amount: reward,
|
||||
Data: data,
|
||||
}); err != nil {
|
||||
log.Printf("send baishun game high win region broadcast failed roundId=%s userId=%d: %v", gameRoundID, userID, err)
|
||||
}
|
||||
_ = s.repo.Redis.Del(ctx, baishunGameBetKey(sysOrigin, gameRoundID, userID)).Err()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *BaishunService) loadBaishunGameBet(ctx context.Context, sysOrigin, gameRoundID string, userID int64) (baishunGameBetRecord, bool, error) {
|
||||
raw, err := s.repo.Redis.Get(ctx, baishunGameBetKey(sysOrigin, gameRoundID, userID)).Result()
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return baishunGameBetRecord{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return baishunGameBetRecord{}, false, err
|
||||
}
|
||||
var record baishunGameBetRecord
|
||||
if err := json.Unmarshal([]byte(raw), &record); err != nil {
|
||||
return baishunGameBetRecord{}, false, err
|
||||
}
|
||||
return record, true, nil
|
||||
}
|
||||
|
||||
func (s *BaishunService) resolveBaishunReportGameCover(ctx context.Context, sysOrigin, roomID string, gameID int64) string {
|
||||
var state model.BaishunRoomState
|
||||
if strings.TrimSpace(roomID) != "" {
|
||||
err := s.repo.DB.WithContext(ctx).
|
||||
Where("sys_origin = ? AND room_id = ?", normalizeAdminSysOrigin(sysOrigin), roomID).
|
||||
First(&state).Error
|
||||
if err == nil && strings.TrimSpace(state.CurrentGameCover) != "" {
|
||||
return strings.TrimSpace(state.CurrentGameCover)
|
||||
}
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
if gameID <= 0 {
|
||||
return ""
|
||||
}
|
||||
profile, err := s.activeProfile(ctx, sysOrigin)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
var catalog model.BaishunGameCatalog
|
||||
if err := s.repo.DB.WithContext(ctx).
|
||||
Where("sys_origin = ? AND profile = ? AND vendor_game_id = ?", normalizeAdminSysOrigin(sysOrigin), profile, gameID).
|
||||
First(&catalog).Error; err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(catalog.Cover)
|
||||
}
|
||||
|
||||
func reportGameRoundID(payload map[string]any) string {
|
||||
return reportText(payload, "game_round_id", "gameRoundId")
|
||||
}
|
||||
|
||||
func reportPlayers(payload map[string]any) []map[string]any {
|
||||
raw, exists := payload["players"]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
switch typed := raw.(type) {
|
||||
case []map[string]any:
|
||||
return typed
|
||||
case []any:
|
||||
players := make([]map[string]any, 0, len(typed))
|
||||
for _, item := range typed {
|
||||
if player, ok := item.(map[string]any); ok {
|
||||
players = append(players, player)
|
||||
}
|
||||
}
|
||||
return players
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func reportText(payload map[string]any, keys ...string) string {
|
||||
for _, key := range keys {
|
||||
if value, exists := payload[key]; exists {
|
||||
text := strings.TrimSpace(toString(value))
|
||||
if text != "" {
|
||||
return text
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func reportInt64(payload map[string]any, keys ...string) int64 {
|
||||
for _, key := range keys {
|
||||
value, exists := payload[key]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
switch typed := value.(type) {
|
||||
case int:
|
||||
return int64(typed)
|
||||
case int64:
|
||||
return typed
|
||||
case float64:
|
||||
return int64(typed)
|
||||
case json.Number:
|
||||
parsed, _ := typed.Int64()
|
||||
return parsed
|
||||
case string:
|
||||
parsed, _ := strconv.ParseInt(strings.TrimSpace(typed), 10, 64)
|
||||
return parsed
|
||||
default:
|
||||
parsed, _ := strconv.ParseInt(strings.TrimSpace(fmt.Sprint(typed)), 10, 64)
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func baishunGameBetKey(sysOrigin, gameRoundID string, userID int64) string {
|
||||
return strings.Join([]string{
|
||||
baishunReportBetRedisPrefix,
|
||||
normalizeAdminSysOrigin(sysOrigin),
|
||||
strings.TrimSpace(gameRoundID),
|
||||
strconv.FormatInt(userID, 10),
|
||||
}, ":")
|
||||
}
|
||||
115
internal/service/baishun/report_test.go
Normal file
115
internal/service/baishun/report_test.go
Normal file
@ -0,0 +1,115 @@
|
||||
package baishun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"chatapp3-golang/internal/model"
|
||||
"chatapp3-golang/internal/service/regionimgroup"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
func TestHandleReportPublishesRegionHighWinAfterStartAndSettle(t *testing.T) {
|
||||
service, db := newTestBaishunService(t)
|
||||
mr := miniredis.RunT(t)
|
||||
redisClient := redis.NewClient(&redis.Options{Addr: mr.Addr()})
|
||||
t.Cleanup(func() { _ = redisClient.Close() })
|
||||
service.repo.Redis = redisClient
|
||||
broadcaster := &fakeBaishunRegionBroadcaster{}
|
||||
service.SetHighWinBroadcaster(broadcaster)
|
||||
|
||||
expireAt := time.Now().Add(time.Hour)
|
||||
token := "ss-token-1001"
|
||||
if err := db.Create(&model.BaishunLaunchSession{
|
||||
ID: 1,
|
||||
SysOrigin: "LIKEI",
|
||||
RoomID: "9001",
|
||||
UserID: 1001,
|
||||
InternalGameID: "bs_1146",
|
||||
VendorGameID: 1146,
|
||||
GameSessionID: "session-1",
|
||||
SSToken: &token,
|
||||
SSTokenExpireTime: &expireAt,
|
||||
Status: baishunSessionActive,
|
||||
CreateTime: time.Now(),
|
||||
UpdateTime: time.Now(),
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("seed session: %v", err)
|
||||
}
|
||||
if err := db.Create(&model.BaishunRoomState{
|
||||
ID: 2,
|
||||
SysOrigin: "LIKEI",
|
||||
RoomID: "9001",
|
||||
HostUserID: 1001,
|
||||
CurrentGameID: "bs_1146",
|
||||
CurrentVendorGameID: intPtr(1146),
|
||||
CurrentGameName: "LordOfOlympus",
|
||||
CurrentGameCover: "https://cdn.example.com/game.png",
|
||||
GameSessionID: "session-1",
|
||||
State: baishunRoomStatePlaying,
|
||||
CreateTime: time.Now(),
|
||||
UpdateTime: time.Now(),
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("seed room state: %v", err)
|
||||
}
|
||||
|
||||
startResp := service.HandleReport(context.Background(), BaishunReportRequest{
|
||||
ReportType: baishunReportTypeStart,
|
||||
UserID: "1001",
|
||||
SSToken: token,
|
||||
ReportMsg: map[string]any{
|
||||
"game_id": 1146,
|
||||
"room_id": "9001",
|
||||
"game_round_id": "round-1",
|
||||
"players": []any{
|
||||
map[string]any{"user_id": "1001", "is_ai": 0, "bet": 100},
|
||||
},
|
||||
},
|
||||
}, `{"report_type":"game_start"}`)
|
||||
if startResp.Code != 0 {
|
||||
t.Fatalf("start response = %+v", startResp)
|
||||
}
|
||||
|
||||
settleResp := service.HandleReport(context.Background(), BaishunReportRequest{
|
||||
ReportType: baishunReportTypeSettle,
|
||||
UserID: "1001",
|
||||
SSToken: token,
|
||||
ReportMsg: map[string]any{
|
||||
"game_id": 1146,
|
||||
"room_id": "9001",
|
||||
"game_round_id": "round-1",
|
||||
"players": []any{
|
||||
map[string]any{"user_id": "1001", "is_ai": 0, "reward": 1000, "rank": 1, "score": 88},
|
||||
},
|
||||
},
|
||||
}, `{"report_type":"game_settle"}`)
|
||||
if settleResp.Code != 0 {
|
||||
t.Fatalf("settle response = %+v", settleResp)
|
||||
}
|
||||
|
||||
if len(broadcaster.requests) != 1 {
|
||||
t.Fatalf("requests = %d, want 1", len(broadcaster.requests))
|
||||
}
|
||||
req := broadcaster.requests[0]
|
||||
if req.Type != "GAME_BAISHUN_WIN" {
|
||||
t.Fatalf("type = %q, want GAME_BAISHUN_WIN", req.Type)
|
||||
}
|
||||
if req.Data["userId"] != "1001" || req.Data["roomId"] != "9001" ||
|
||||
req.Data["multiple"] != int64(10) || req.Data["winAmount"] != int64(1000) ||
|
||||
req.Data["currencyDiff"] != int64(1000) ||
|
||||
req.Data["gameUrl"] != "https://cdn.example.com/game.png" {
|
||||
t.Fatalf("payload = %+v", req.Data)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeBaishunRegionBroadcaster struct {
|
||||
requests []regionimgroup.RegionBroadcastRequest
|
||||
}
|
||||
|
||||
func (f *fakeBaishunRegionBroadcaster) SendRegionBroadcast(_ context.Context, req regionimgroup.RegionBroadcastRequest) (*regionimgroup.RegionBroadcastResponse, error) {
|
||||
f.requests = append(f.requests, req)
|
||||
return ®ionimgroup.RegionBroadcastResponse{Type: req.Type}, nil
|
||||
}
|
||||
@ -3,6 +3,7 @@ package baishun
|
||||
import (
|
||||
"chatapp3-golang/internal/config"
|
||||
"chatapp3-golang/internal/integration"
|
||||
"chatapp3-golang/internal/service/highwin"
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
@ -61,11 +62,12 @@ type baishunPorts struct {
|
||||
|
||||
// BaishunService 负责百顺目录同步、游戏启动、房间状态以及回调处理。
|
||||
type BaishunService struct {
|
||||
cfg config.Config
|
||||
repo baishunPorts
|
||||
java baishunGateway
|
||||
httpClient *http.Client
|
||||
taskReporter taskEventReporter
|
||||
cfg config.Config
|
||||
repo baishunPorts
|
||||
java baishunGateway
|
||||
httpClient *http.Client
|
||||
taskReporter taskEventReporter
|
||||
highWinBroadcaster highwin.RegionBroadcaster
|
||||
}
|
||||
|
||||
// NewBaishunService 创建百顺接入服务实例。
|
||||
@ -84,6 +86,10 @@ func (s *BaishunService) SetTaskEventReporter(reporter taskEventReporter) {
|
||||
s.taskReporter = reporter
|
||||
}
|
||||
|
||||
func (s *BaishunService) SetHighWinBroadcaster(broadcaster highwin.RegionBroadcaster) {
|
||||
s.highWinBroadcaster = broadcaster
|
||||
}
|
||||
|
||||
// RoomGameListItem 是房间游戏列表中的单个游戏项。
|
||||
type RoomGameListItem struct {
|
||||
ID int64 `json:"id"`
|
||||
@ -418,6 +424,13 @@ type BaishunBalanceInfoRequest struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
type BaishunReportRequest struct {
|
||||
ReportType string `json:"report_type"`
|
||||
ReportMsg map[string]any `json:"report_msg"`
|
||||
UserID string `json:"user_id"`
|
||||
SSToken string `json:"ss_token"`
|
||||
}
|
||||
|
||||
// baishunStandardResponse 是百顺标准回包结构。
|
||||
type baishunStandardResponse struct {
|
||||
Code int `json:"code"`
|
||||
|
||||
148
internal/service/highwin/broadcast.go
Normal file
148
internal/service/highwin/broadcast.go
Normal file
@ -0,0 +1,148 @@
|
||||
package highwin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"chatapp3-golang/internal/service/regionimgroup"
|
||||
)
|
||||
|
||||
const (
|
||||
ThresholdMultiple = 10
|
||||
|
||||
MessageTypeLuckyGift = "GAME_LUCKY_GIFT"
|
||||
MessageTypeBaishunWin = "GAME_BAISHUN_WIN"
|
||||
)
|
||||
|
||||
type RegionBroadcaster interface {
|
||||
SendRegionBroadcast(ctx context.Context, req regionimgroup.RegionBroadcastRequest) (*regionimgroup.RegionBroadcastResponse, error)
|
||||
}
|
||||
|
||||
type BroadcastEvent struct {
|
||||
SysOrigin string
|
||||
Type string
|
||||
UserID int64
|
||||
RoomID string
|
||||
Multiple float64
|
||||
Amount int64
|
||||
Data map[string]any
|
||||
}
|
||||
|
||||
func IntegralMultiple(amount, base int64) int64 {
|
||||
if amount <= 0 || base <= 0 {
|
||||
return 0
|
||||
}
|
||||
return amount / base
|
||||
}
|
||||
|
||||
func Multiple(amount, base int64) float64 {
|
||||
if amount <= 0 || base <= 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(amount) / float64(base)
|
||||
}
|
||||
|
||||
func ShouldBroadcast(multiple float64, amount int64) bool {
|
||||
return amount > 0 && multiple >= ThresholdMultiple
|
||||
}
|
||||
|
||||
func SendRegionBroadcast(ctx context.Context, broadcaster RegionBroadcaster, event BroadcastEvent) error {
|
||||
if broadcaster == nil || !ShouldBroadcast(event.Multiple, event.Amount) {
|
||||
return nil
|
||||
}
|
||||
messageType := strings.TrimSpace(event.Type)
|
||||
if messageType == "" {
|
||||
return fmt.Errorf("high win broadcast type is required")
|
||||
}
|
||||
if event.UserID <= 0 {
|
||||
return fmt.Errorf("high win broadcast user id is required")
|
||||
}
|
||||
roomID := strings.TrimSpace(event.RoomID)
|
||||
if roomID == "" {
|
||||
return fmt.Errorf("high win broadcast room id is required")
|
||||
}
|
||||
|
||||
data := cloneData(event.Data)
|
||||
userIDText := strconv.FormatInt(event.UserID, 10)
|
||||
setRequired(data, "userId", userIDText)
|
||||
setRequired(data, "sendUserId", userIDText)
|
||||
setRequired(data, "senderUserId", userIDText)
|
||||
setRequired(data, "roomId", roomID)
|
||||
setRequired(data, "multiple", normalizeMultiple(event.Multiple))
|
||||
setRequired(data, "winMultiple", normalizeMultiple(event.Multiple))
|
||||
setRequired(data, "winAmount", event.Amount)
|
||||
setRequired(data, "awardAmount", event.Amount)
|
||||
setRequired(data, "msg", highWinMessage(data, messageType, userIDText, event.Multiple, event.Amount))
|
||||
|
||||
_, err := broadcaster.SendRegionBroadcast(ctx, regionimgroup.RegionBroadcastRequest{
|
||||
SysOrigin: strings.TrimSpace(event.SysOrigin),
|
||||
Type: messageType,
|
||||
Data: data,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func cloneData(data map[string]any) map[string]any {
|
||||
clone := map[string]any{}
|
||||
for key, value := range data {
|
||||
clone[key] = value
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
func setRequired(data map[string]any, key string, value any) {
|
||||
data[key] = value
|
||||
}
|
||||
|
||||
func normalizeMultiple(value float64) any {
|
||||
if value <= 0 {
|
||||
return 0
|
||||
}
|
||||
if math.Trunc(value) == value {
|
||||
return int64(value)
|
||||
}
|
||||
return math.Round(value*100) / 100
|
||||
}
|
||||
|
||||
func highWinMessage(data map[string]any, messageType, fallbackUser string, multiple float64, amount int64) string {
|
||||
user := firstTextValue(data, "userNickname", "nickname", "account", "actualAccount", "sendUserId", "senderUserId", "userId")
|
||||
if user == "" {
|
||||
user = fallbackUser
|
||||
}
|
||||
return fmt.Sprintf("%s play %s get x %s win %d coins!!!", user, highWinPlayTarget(messageType), formatMultipleText(multiple), amount)
|
||||
}
|
||||
|
||||
func highWinPlayTarget(messageType string) string {
|
||||
switch strings.TrimSpace(messageType) {
|
||||
case MessageTypeLuckyGift:
|
||||
return "lucky_gift"
|
||||
case MessageTypeBaishunWin:
|
||||
return "game"
|
||||
default:
|
||||
return "game"
|
||||
}
|
||||
}
|
||||
|
||||
func firstTextValue(data map[string]any, keys ...string) string {
|
||||
for _, key := range keys {
|
||||
if value := strings.TrimSpace(fmt.Sprint(data[key])); value != "" && value != "<nil>" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func formatMultipleText(value float64) string {
|
||||
normalized := normalizeMultiple(value)
|
||||
switch typed := normalized.(type) {
|
||||
case int64:
|
||||
return strconv.FormatInt(typed, 10)
|
||||
case float64:
|
||||
return strconv.FormatFloat(typed, 'f', -1, 64)
|
||||
default:
|
||||
return strings.TrimSpace(fmt.Sprint(typed))
|
||||
}
|
||||
}
|
||||
71
internal/service/highwin/broadcast_test.go
Normal file
71
internal/service/highwin/broadcast_test.go
Normal file
@ -0,0 +1,71 @@
|
||||
package highwin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"chatapp3-golang/internal/service/regionimgroup"
|
||||
)
|
||||
|
||||
func TestSendRegionBroadcastBuildsRequiredPayload(t *testing.T) {
|
||||
broadcaster := &fakeRegionBroadcaster{}
|
||||
|
||||
err := SendRegionBroadcast(context.Background(), broadcaster, BroadcastEvent{
|
||||
SysOrigin: "LIKEI",
|
||||
Type: MessageTypeBaishunWin,
|
||||
UserID: 1001,
|
||||
RoomID: "9001",
|
||||
Multiple: 10.5,
|
||||
Amount: 2100,
|
||||
Data: map[string]any{
|
||||
"gameId": 1146,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SendRegionBroadcast() error = %v", err)
|
||||
}
|
||||
if len(broadcaster.requests) != 1 {
|
||||
t.Fatalf("requests = %d, want 1", len(broadcaster.requests))
|
||||
}
|
||||
req := broadcaster.requests[0]
|
||||
if req.Type != MessageTypeBaishunWin {
|
||||
t.Fatalf("type = %q, want %q", req.Type, MessageTypeBaishunWin)
|
||||
}
|
||||
if req.Data["userId"] != "1001" || req.Data["roomId"] != "9001" ||
|
||||
req.Data["winAmount"] != int64(2100) || req.Data["multiple"] != 10.5 ||
|
||||
req.Data["msg"] != "1001 play game get x 10.5 win 2100 coins!!!" {
|
||||
t.Fatalf("payload = %+v", req.Data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendRegionBroadcastSkipsBelowThreshold(t *testing.T) {
|
||||
broadcaster := &fakeRegionBroadcaster{}
|
||||
|
||||
err := SendRegionBroadcast(context.Background(), broadcaster, BroadcastEvent{
|
||||
Type: MessageTypeLuckyGift,
|
||||
UserID: 1001,
|
||||
RoomID: "9001",
|
||||
Multiple: 9,
|
||||
Amount: 900,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SendRegionBroadcast() error = %v", err)
|
||||
}
|
||||
if len(broadcaster.requests) != 0 {
|
||||
t.Fatalf("requests = %d, want 0", len(broadcaster.requests))
|
||||
}
|
||||
}
|
||||
|
||||
type fakeRegionBroadcaster struct {
|
||||
requests []regionimgroup.RegionBroadcastRequest
|
||||
}
|
||||
|
||||
func (f *fakeRegionBroadcaster) SendRegionBroadcast(_ context.Context, req regionimgroup.RegionBroadcastRequest) (*regionimgroup.RegionBroadcastResponse, error) {
|
||||
f.requests = append(f.requests, req)
|
||||
return ®ionimgroup.RegionBroadcastResponse{
|
||||
SysOrigin: req.SysOrigin,
|
||||
RegionCode: "AR",
|
||||
GroupID: "@region-ar",
|
||||
Type: req.Type,
|
||||
}, nil
|
||||
}
|
||||
@ -140,6 +140,8 @@ func (s *LuckyGiftService) Draw(ctx context.Context, req LuckyGiftDrawRequest) (
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.publishHighWinRegionBroadcasts(ctx, req, results, balanceAfter)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
|
||||
51
internal/service/luckygift/high_win.go
Normal file
51
internal/service/luckygift/high_win.go
Normal file
@ -0,0 +1,51 @@
|
||||
package luckygift
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"strconv"
|
||||
|
||||
"chatapp3-golang/internal/service/highwin"
|
||||
)
|
||||
|
||||
func (s *LuckyGiftService) publishHighWinRegionBroadcasts(ctx context.Context, req LuckyGiftDrawRequest, results []LuckyGiftDrawResult, balanceAfter int64) {
|
||||
if s == nil || s.highWinBroadcaster == nil || req.GiftPrice <= 0 {
|
||||
return
|
||||
}
|
||||
for _, result := range results {
|
||||
if result.RewardNum <= 0 {
|
||||
continue
|
||||
}
|
||||
multiple := highwin.IntegralMultiple(result.RewardNum, req.GiftPrice)
|
||||
if !highwin.ShouldBroadcast(float64(multiple), result.RewardNum) {
|
||||
continue
|
||||
}
|
||||
data := map[string]any{
|
||||
"businessId": req.BusinessID,
|
||||
"orderId": result.ID,
|
||||
"providerOrderId": result.OrderID,
|
||||
"acceptUserId": strconv.FormatInt(result.AcceptUserID, 10),
|
||||
"giftId": strconv.FormatInt(req.GiftID, 10),
|
||||
"giftCandy": req.GiftPrice,
|
||||
"giftQuantity": req.GiftNum,
|
||||
"multipleType": luckyGiftMultipleType(multiple),
|
||||
"balance": balanceAfter,
|
||||
"globalNews": true,
|
||||
}
|
||||
if err := highwin.SendRegionBroadcast(ctx, s.highWinBroadcaster, highwin.BroadcastEvent{
|
||||
SysOrigin: req.SysOrigin,
|
||||
Type: highwin.MessageTypeLuckyGift,
|
||||
UserID: req.SendUserID,
|
||||
RoomID: strconv.FormatInt(req.RoomID, 10),
|
||||
Multiple: float64(multiple),
|
||||
Amount: result.RewardNum,
|
||||
Data: data,
|
||||
}); err != nil {
|
||||
log.Printf("send lucky gift high win region broadcast failed businessId=%s orderId=%s: %v", req.BusinessID, result.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func luckyGiftMultipleType(_ int64) string {
|
||||
return "WIN"
|
||||
}
|
||||
48
internal/service/luckygift/high_win_test.go
Normal file
48
internal/service/luckygift/high_win_test.go
Normal file
@ -0,0 +1,48 @@
|
||||
package luckygift
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"chatapp3-golang/internal/service/regionimgroup"
|
||||
)
|
||||
|
||||
func TestPublishHighWinRegionBroadcastsSendsOnlyTenTimesAndAbove(t *testing.T) {
|
||||
broadcaster := &fakeLuckyGiftRegionBroadcaster{}
|
||||
service := &LuckyGiftService{highWinBroadcaster: broadcaster}
|
||||
|
||||
service.publishHighWinRegionBroadcasts(context.Background(), LuckyGiftDrawRequest{
|
||||
BusinessID: "biz-1",
|
||||
SysOrigin: "LIKEI",
|
||||
RoomID: 9001,
|
||||
SendUserID: 1001,
|
||||
GiftID: 5001,
|
||||
GiftPrice: 100,
|
||||
GiftNum: 1,
|
||||
}, []LuckyGiftDrawResult{
|
||||
{ID: "low", AcceptUserID: 2001, RewardNum: 900},
|
||||
{ID: "high", OrderID: "LG_high", AcceptUserID: 2002, RewardNum: 1000},
|
||||
}, 90000)
|
||||
|
||||
if len(broadcaster.requests) != 1 {
|
||||
t.Fatalf("requests = %d, want 1", len(broadcaster.requests))
|
||||
}
|
||||
req := broadcaster.requests[0]
|
||||
if req.Type != "GAME_LUCKY_GIFT" {
|
||||
t.Fatalf("type = %q, want GAME_LUCKY_GIFT", req.Type)
|
||||
}
|
||||
if req.Data["sendUserId"] != "1001" || req.Data["roomId"] != "9001" ||
|
||||
req.Data["multiple"] != int64(10) || req.Data["awardAmount"] != int64(1000) ||
|
||||
req.Data["acceptUserId"] != "2002" {
|
||||
t.Fatalf("payload = %+v", req.Data)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeLuckyGiftRegionBroadcaster struct {
|
||||
requests []regionimgroup.RegionBroadcastRequest
|
||||
}
|
||||
|
||||
func (f *fakeLuckyGiftRegionBroadcaster) SendRegionBroadcast(_ context.Context, req regionimgroup.RegionBroadcastRequest) (*regionimgroup.RegionBroadcastResponse, error) {
|
||||
f.requests = append(f.requests, req)
|
||||
return ®ionimgroup.RegionBroadcastResponse{Type: req.Type}, nil
|
||||
}
|
||||
@ -3,6 +3,7 @@ package luckygift
|
||||
import (
|
||||
"chatapp3-golang/internal/config"
|
||||
"chatapp3-golang/internal/integration"
|
||||
"chatapp3-golang/internal/service/highwin"
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
@ -39,10 +40,11 @@ type luckyGiftPorts struct {
|
||||
|
||||
// LuckyGiftService 负责幸运礼物抽奖、三方调用和钱包返奖。
|
||||
type LuckyGiftService struct {
|
||||
cfg config.Config
|
||||
repo luckyGiftPorts
|
||||
java luckyGiftGateway
|
||||
httpClient *http.Client
|
||||
cfg config.Config
|
||||
repo luckyGiftPorts
|
||||
java luckyGiftGateway
|
||||
httpClient *http.Client
|
||||
highWinBroadcaster highwin.RegionBroadcaster
|
||||
}
|
||||
|
||||
// LuckyGiftDrawRequest 是幸运礼物抽奖入参。
|
||||
@ -60,6 +62,10 @@ type LuckyGiftDrawRequest struct {
|
||||
Orders []LuckyGiftDrawOrderInput `json:"orders"`
|
||||
}
|
||||
|
||||
func (s *LuckyGiftService) SetHighWinBroadcaster(broadcaster highwin.RegionBroadcaster) {
|
||||
s.highWinBroadcaster = broadcaster
|
||||
}
|
||||
|
||||
// LuckyGiftDrawOrderInput 表示单个收礼人的抽奖子单。
|
||||
type LuckyGiftDrawOrderInput struct {
|
||||
ID string `json:"id"`
|
||||
|
||||
@ -66,6 +66,7 @@ func (s *Service) SendRegionBroadcast(ctx context.Context, req RegionBroadcastRe
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
refreshHighWinMessage(data, messageType)
|
||||
|
||||
body := map[string]any{
|
||||
"type": messageType,
|
||||
@ -182,6 +183,41 @@ func firstInt64Value(values ...any) int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func refreshHighWinMessage(data map[string]any, messageType string) {
|
||||
target := highWinPlayTarget(messageType)
|
||||
if target == "" {
|
||||
return
|
||||
}
|
||||
user := firstTextValue(data["userNickname"], data["nickname"], data["account"], data["actualAccount"], data["userId"])
|
||||
multiple := firstTextValue(data["multiple"], data["winMultiple"])
|
||||
amount := firstTextValue(data["winAmount"], data["awardAmount"])
|
||||
if user == "" || multiple == "" || amount == "" {
|
||||
return
|
||||
}
|
||||
data["msg"] = fmt.Sprintf("%s play %s get x %s win %s coins!!!", user, target, multiple, amount)
|
||||
}
|
||||
|
||||
func highWinPlayTarget(messageType string) string {
|
||||
switch strings.TrimSpace(messageType) {
|
||||
case "GAME_LUCKY_GIFT":
|
||||
return "lucky_gift"
|
||||
case "GAME_BAISHUN_WIN":
|
||||
return "game"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func firstTextValue(values ...any) string {
|
||||
for _, value := range values {
|
||||
text := strings.TrimSpace(fmt.Sprint(value))
|
||||
if text != "" && text != "<nil>" {
|
||||
return text
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func firstInt64FromList(value any) int64 {
|
||||
switch typed := value.(type) {
|
||||
case []any:
|
||||
|
||||
@ -133,6 +133,52 @@ func TestSendRegionBroadcastResolvesRegionAndEnrichesGiftPayload(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendRegionBroadcastRefreshesHighWinMessageWithProfile(t *testing.T) {
|
||||
service, db := newRegionIMGroupTestService(t)
|
||||
fakeIM := &fakeIMGateway{}
|
||||
service.im = fakeIM
|
||||
now := time.Now()
|
||||
if err := db.Create(&model.VoiceRoomRegionIMGroup{
|
||||
ID: 3010,
|
||||
SysOrigin: "LIKEI",
|
||||
RegionCode: "AR",
|
||||
RegionName: "Arab",
|
||||
GroupID: "@region-ar",
|
||||
GroupName: "region-ar",
|
||||
Status: imGroupStatusActive,
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("seed im group: %v", err)
|
||||
}
|
||||
|
||||
_, err := service.SendRegionBroadcast(context.Background(), RegionBroadcastRequest{
|
||||
SysOrigin: "LIKEI",
|
||||
Type: "GAME_BAISHUN_WIN",
|
||||
Data: map[string]any{
|
||||
"userId": "1001",
|
||||
"roomId": "9001",
|
||||
"multiple": 10,
|
||||
"winAmount": 1000,
|
||||
"msg": "1001 play game get x 10 win 1000 coins!!!",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SendRegionBroadcast() error = %v", err)
|
||||
}
|
||||
body, ok := fakeIM.sent[0].body.(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("sent body type = %T", fakeIM.sent[0].body)
|
||||
}
|
||||
data, ok := body["data"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("sent data type = %T", body["data"])
|
||||
}
|
||||
if data["msg"] != "sender play game get x 10 win 1000 coins!!!" {
|
||||
t.Fatalf("msg = %v", data["msg"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendRegionBroadcastPrefersJavaRegionResolver(t *testing.T) {
|
||||
service, db := newRegionIMGroupTestService(t)
|
||||
service.regionResolver = fakeRegionResolver{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user