From f5f2b828c6d287a00f7add527eb8366da68cd0a4 Mon Sep 17 00:00:00 2001 From: zhx Date: Tue, 2 Jun 2026 15:49:00 +0800 Subject: [PATCH] Add test IM broadcast group prefix --- pkg/imgroup/imgroup.go | 84 +++++++++++++++++-- pkg/imgroup/imgroup_test.go | 32 +++++++ .../configs/config.docker.yaml | 1 + services/activity-service/internal/app/app.go | 1 + .../internal/config/config.go | 2 + .../internal/service/broadcast/service.go | 11 +-- .../service/broadcast/service_test.go | 30 +++++++ .../configs/config.docker.yaml | 1 + services/gateway-service/internal/app/app.go | 1 + .../gateway-service/internal/config/config.go | 3 + .../transport/http/callbackapi/handler.go | 1 + .../http/callbackapi/tencent_im_callback.go | 4 +- .../internal/transport/http/handler.go | 13 ++- .../internal/transport/http/response_test.go | 51 +++++++++++ .../internal/transport/http/router.go | 2 + .../transport/http/walletapi/handler.go | 3 + .../http/walletapi/red_packet_handler.go | 2 +- 17 files changed, 225 insertions(+), 17 deletions(-) diff --git a/pkg/imgroup/imgroup.go b/pkg/imgroup/imgroup.go index 20a89933..5594184c 100644 --- a/pkg/imgroup/imgroup.go +++ b/pkg/imgroup/imgroup.go @@ -40,11 +40,20 @@ type ParsedGroup struct { // GlobalBroadcastGroupID 生成 App 全局播报群 ID。 // app_code 先 Normalize 再进入 GroupID,确保 HTTP、gRPC、outbox 和腾讯回调看到的是同一租户标识。 func GlobalBroadcastGroupID(value string) (string, error) { + return GlobalBroadcastGroupIDWithPrefix("", value) +} + +// GlobalBroadcastGroupIDWithPrefix 生成带环境前缀的 App 全局播报群 ID。 +func GlobalBroadcastGroupIDWithPrefix(prefix string, value string) (string, error) { + prefix = strings.TrimSpace(prefix) + if err := validateGroupIDPrefix(prefix); err != nil { + return "", err + } app := appcode.Normalize(value) if err := validateAppCodeForGroupID(app); err != nil { return "", err } - groupID := "hy_" + app + globalSuffix + groupID := prefix + "hy_" + app + globalSuffix if len(groupID) > roomid.MaxStringIDLength { return "", fmt.Errorf("global broadcast group_id is too long") } @@ -54,6 +63,15 @@ func GlobalBroadcastGroupID(value string) (string, error) { // RegionBroadcastGroupID 生成区域播报群 ID。 // region_id <= 0 被拒绝,GLOBAL/未知区域不能变成一个可 join 的区域群。 func RegionBroadcastGroupID(value string, regionID int64) (string, error) { + return RegionBroadcastGroupIDWithPrefix("", value, regionID) +} + +// RegionBroadcastGroupIDWithPrefix 生成带环境前缀的区域播报群 ID。 +func RegionBroadcastGroupIDWithPrefix(prefix string, value string, regionID int64) (string, error) { + prefix = strings.TrimSpace(prefix) + if err := validateGroupIDPrefix(prefix); err != nil { + return "", err + } app := appcode.Normalize(value) if err := validateAppCodeForGroupID(app); err != nil { return "", err @@ -61,7 +79,7 @@ func RegionBroadcastGroupID(value string, regionID int64) (string, error) { if regionID <= 0 { return "", fmt.Errorf("region_id must be positive") } - groupID := "hy_" + app + regionToken + strconv.FormatInt(regionID, 10) + groupID := prefix + "hy_" + app + regionToken + strconv.FormatInt(regionID, 10) if len(groupID) > roomid.MaxStringIDLength { return "", fmt.Errorf("region broadcast group_id is too long") } @@ -76,6 +94,42 @@ func Parse(groupID string) ParsedGroup { return ParsedGroup{Kind: KindUnknown} } + if parsed := parseBroadcastGroupID(groupID); parsed.Kind != KindUnknown { + return parsed + } + if strings.HasPrefix(groupID, "hy_") && strings.Contains(groupID, "_bc_") { + return ParsedGroup{Kind: KindUnknown} + } + + if roomid.ValidStringID(groupID) { + return ParsedGroup{Kind: KindRoom, RoomID: groupID} + } + return ParsedGroup{Kind: KindUnknown} +} + +// ParseWithPrefix 使用配置的环境前缀解析腾讯云回调里的 GroupID。 +func ParseWithPrefix(groupID string, prefix string) ParsedGroup { + groupID = strings.TrimSpace(groupID) + prefix = strings.TrimSpace(prefix) + if groupID == "" || validateGroupIDPrefix(prefix) != nil { + return ParsedGroup{Kind: KindUnknown} + } + if prefix == "" { + return Parse(groupID) + } + if after, ok := strings.CutPrefix(groupID, prefix); ok { + return parseBroadcastGroupID(after) + } + if strings.HasPrefix(groupID, "hy_") && strings.Contains(groupID, "_bc_") { + return ParsedGroup{Kind: KindUnknown} + } + if roomid.ValidStringID(groupID) { + return ParsedGroup{Kind: KindRoom, RoomID: groupID} + } + return ParsedGroup{Kind: KindUnknown} +} + +func parseBroadcastGroupID(groupID string) ParsedGroup { if strings.HasPrefix(groupID, "hy_") && strings.HasSuffix(groupID, globalSuffix) { app := strings.TrimSuffix(strings.TrimPrefix(groupID, "hy_"), globalSuffix) if validateAppCodeForGroupID(app) == nil { @@ -98,12 +152,32 @@ func Parse(groupID string) ParsedGroup { } } - if roomid.ValidStringID(groupID) { - return ParsedGroup{Kind: KindRoom, RoomID: groupID} - } return ParsedGroup{Kind: KindUnknown} } +func validateGroupIDPrefix(prefix string) error { + if prefix == "" { + return nil + } + for i := 0; i < len(prefix); i++ { + char := prefix[i] + if char >= 'a' && char <= 'z' { + continue + } + if char >= 'A' && char <= 'Z' { + continue + } + if char >= '0' && char <= '9' { + continue + } + if char == '_' || char == '-' { + continue + } + return fmt.Errorf("group_id_prefix contains unsupported character") + } + return nil +} + func validateAppCodeForGroupID(app string) error { if app == "" { return fmt.Errorf("app_code is required") diff --git a/pkg/imgroup/imgroup_test.go b/pkg/imgroup/imgroup_test.go index 5f618fa2..11920f5c 100644 --- a/pkg/imgroup/imgroup_test.go +++ b/pkg/imgroup/imgroup_test.go @@ -42,3 +42,35 @@ func TestParseRoomAndRejectInvalidBroadcast(t *testing.T) { t.Fatal("app_code with spaces must be rejected") } } + +func TestBroadcastGroupIDWithPrefix(t *testing.T) { + global, err := GlobalBroadcastGroupIDWithPrefix("test_", "lalu") + if err != nil { + t.Fatalf("GlobalBroadcastGroupIDWithPrefix failed: %v", err) + } + if global != "test_hy_lalu_bc_g" { + t.Fatalf("prefixed global group mismatch: %s", global) + } + region, err := RegionBroadcastGroupIDWithPrefix("test_", "lalu", 1001) + if err != nil { + t.Fatalf("RegionBroadcastGroupIDWithPrefix failed: %v", err) + } + if region != "test_hy_lalu_bc_r_1001" { + t.Fatalf("prefixed region group mismatch: %s", region) + } + + parsedGlobal := ParseWithPrefix(global, "test_") + if parsedGlobal.Kind != KindGlobalBroadcast || parsedGlobal.AppCode != "lalu" { + t.Fatalf("prefixed global parse mismatch: %+v", parsedGlobal) + } + parsedRegion := ParseWithPrefix(region, "test_") + if parsedRegion.Kind != KindRegionBroadcast || parsedRegion.AppCode != "lalu" || parsedRegion.RegionID != 1001 { + t.Fatalf("prefixed region parse mismatch: %+v", parsedRegion) + } + if parsed := ParseWithPrefix("hy_lalu_bc_r_1001", "test_"); parsed.Kind != KindUnknown { + t.Fatalf("unprefixed broadcast group must be rejected when prefix is configured: %+v", parsed) + } + if parsed := ParseWithPrefix("room-1001", "test_"); parsed.Kind != KindRoom || parsed.RoomID != "room-1001" { + t.Fatalf("room parse with prefix mismatch: %+v", parsed) + } +} diff --git a/services/activity-service/configs/config.docker.yaml b/services/activity-service/configs/config.docker.yaml index 76e8c2c8..8db07f36 100644 --- a/services/activity-service/configs/config.docker.yaml +++ b/services/activity-service/configs/config.docker.yaml @@ -34,6 +34,7 @@ tencent_im: admin_user_sig_ttl: "24h" endpoint: "adminapisgp.im.qcloud.com" group_type: "ChatRoom" + group_id_prefix: "test_" request_timeout: "5s" broadcast: enabled: true diff --git a/services/activity-service/internal/app/app.go b/services/activity-service/internal/app/app.go index 69b964d3..aa3b86b1 100644 --- a/services/activity-service/internal/app/app.go +++ b/services/activity-service/internal/app/app.go @@ -136,6 +136,7 @@ func New(cfg config.Config) (*App, error) { WorkerMaxRetry: cfg.Broadcast.WorkerMaxRetry, WorkerPollInterval: cfg.Broadcast.WorkerPollInterval, EnsureGroupsOnStartup: cfg.Broadcast.EnsureGroupsOnStartup, + GroupIDPrefix: cfg.TencentIM.GroupIDPrefix, }, repository, broadcastPublisher, client.NewGRPCRegionSource(userConn)) broadcastSvc.SetSenderProfileSource(client.NewGRPCUserProfileSource(userConn)) luckyGiftSvc := luckygiftservice.New(repository, diff --git a/services/activity-service/internal/config/config.go b/services/activity-service/internal/config/config.go index cc2320a5..ab3cef1a 100644 --- a/services/activity-service/internal/config/config.go +++ b/services/activity-service/internal/config/config.go @@ -87,6 +87,7 @@ type TencentIMConfig struct { AdminUserSigTTL time.Duration `yaml:"admin_user_sig_ttl"` Endpoint string `yaml:"endpoint"` GroupType string `yaml:"group_type"` + GroupIDPrefix string `yaml:"group_id_prefix"` RequestTimeout time.Duration `yaml:"request_timeout"` } @@ -272,6 +273,7 @@ func Load(path string) (Config, error) { if cfg.TencentIM.GroupType == "" { cfg.TencentIM.GroupType = tencentim.DefaultGroupType } + cfg.TencentIM.GroupIDPrefix = strings.TrimSpace(cfg.TencentIM.GroupIDPrefix) if cfg.TencentIM.AdminUserSigTTL <= 0 { cfg.TencentIM.AdminUserSigTTL = 24 * time.Hour } diff --git a/services/activity-service/internal/service/broadcast/service.go b/services/activity-service/internal/service/broadcast/service.go index 103f71fa..ec0adaa0 100644 --- a/services/activity-service/internal/service/broadcast/service.go +++ b/services/activity-service/internal/service/broadcast/service.go @@ -72,6 +72,7 @@ type Config struct { WorkerMaxRetry int WorkerPollInterval time.Duration EnsureGroupsOnStartup bool + GroupIDPrefix string } // PublishInput 是服务端入队播报请求。 @@ -125,7 +126,7 @@ func (s *Service) EnsureBroadcastGroups(ctx context.Context) (int, error) { } app := appcode.FromContext(ctx) count := 0 - globalID, err := imgroup.GlobalBroadcastGroupID(app) + globalID, err := imgroup.GlobalBroadcastGroupIDWithPrefix(s.cfg.GroupIDPrefix, app) if err != nil { return 0, xerr.New(xerr.InvalidArgument, err.Error()) } @@ -147,7 +148,7 @@ func (s *Service) EnsureBroadcastGroups(ctx context.Context) (int, error) { // GLOBAL/未知区域不对应 IM 区域群,避免客户端加入一个语义不清的“0 区域群”。 continue } - groupID, err := imgroup.RegionBroadcastGroupID(app, regionID) + groupID, err := imgroup.RegionBroadcastGroupIDWithPrefix(s.cfg.GroupIDPrefix, app, regionID) if err != nil { return count, xerr.New(xerr.InvalidArgument, err.Error()) } @@ -165,7 +166,7 @@ func (s *Service) PublishRegionBroadcast(ctx context.Context, input PublishInput if input.RegionID <= 0 { return broadcastdomain.PublishResult{}, xerr.New(xerr.InvalidArgument, "region_id is required") } - groupID, err := imgroup.RegionBroadcastGroupID(appcode.FromContext(ctx), input.RegionID) + groupID, err := imgroup.RegionBroadcastGroupIDWithPrefix(s.cfg.GroupIDPrefix, appcode.FromContext(ctx), input.RegionID) if err != nil { return broadcastdomain.PublishResult{}, xerr.New(xerr.InvalidArgument, err.Error()) } @@ -183,7 +184,7 @@ func (s *Service) PublishRoomBroadcast(ctx context.Context, roomID string, input // PublishGlobalBroadcast 把全局播报写入持久化 outbox。 // 全局播报和区域播报共用同一张表,scope/group_id 决定最终投递目标。 func (s *Service) PublishGlobalBroadcast(ctx context.Context, input PublishInput) (broadcastdomain.PublishResult, error) { - groupID, err := imgroup.GlobalBroadcastGroupID(appcode.FromContext(ctx)) + groupID, err := imgroup.GlobalBroadcastGroupIDWithPrefix(s.cfg.GroupIDPrefix, appcode.FromContext(ctx)) if err != nil { return broadcastdomain.PublishResult{}, xerr.New(xerr.InvalidArgument, err.Error()) } @@ -203,7 +204,7 @@ func (s *Service) RemoveRegionBroadcastMember(ctx context.Context, userID int64, if s == nil || s.publisher == nil { return "", false, xerr.New(xerr.Unavailable, "broadcast publisher is not configured") } - groupID, err := imgroup.RegionBroadcastGroupID(appcode.FromContext(ctx), regionID) + groupID, err := imgroup.RegionBroadcastGroupIDWithPrefix(s.cfg.GroupIDPrefix, appcode.FromContext(ctx), regionID) if err != nil { return "", false, xerr.New(xerr.InvalidArgument, err.Error()) } diff --git a/services/activity-service/internal/service/broadcast/service_test.go b/services/activity-service/internal/service/broadcast/service_test.go index c6819010..59ec150d 100644 --- a/services/activity-service/internal/service/broadcast/service_test.go +++ b/services/activity-service/internal/service/broadcast/service_test.go @@ -254,6 +254,36 @@ func TestRemoveRegionBroadcastMemberDeletesOnlyUserMembership(t *testing.T) { } } +func TestRegionBroadcastUsesConfiguredGroupPrefix(t *testing.T) { + repository := newFakeRepository() + publisher := &fakePublisher{} + service := New(Config{NodeID: "node-a", GroupIDPrefix: "test_"}, repository, publisher, nil) + + result, err := service.PublishRegionBroadcast(appcode.WithContext(context.Background(), "lalu"), PublishInput{ + EventID: "evt-prefixed", + BroadcastType: broadcastdomain.TypeSuperGift, + RegionID: 1001, + PayloadJSON: `{"ok":true}`, + }) + if err != nil { + t.Fatalf("PublishRegionBroadcast failed: %v", err) + } + if result.GroupID != "test_hy_lalu_bc_r_1001" { + t.Fatalf("prefixed publish group mismatch: %+v", result) + } + if record := repository.records["evt-prefixed"]; record.GroupID != "test_hy_lalu_bc_r_1001" { + t.Fatalf("prefixed outbox group mismatch: %+v", record) + } + + groupID, removed, err := service.RemoveRegionBroadcastMember(appcode.WithContext(context.Background(), "lalu"), 42, 1001) + if err != nil { + t.Fatalf("RemoveRegionBroadcastMember failed: %v", err) + } + if !removed || groupID != "test_hy_lalu_bc_r_1001" || publisher.deletedGroupID != "test_hy_lalu_bc_r_1001" { + t.Fatalf("prefixed remove mismatch: group_id=%s removed=%t deleted=%s", groupID, removed, publisher.deletedGroupID) + } +} + func mustGiftEnvelope(t *testing.T, gift *roomeventsv1.RoomGiftSent) *roomeventsv1.EventEnvelope { t.Helper() body, err := proto.Marshal(gift) diff --git a/services/gateway-service/configs/config.docker.yaml b/services/gateway-service/configs/config.docker.yaml index c959e744..bc8f8869 100644 --- a/services/gateway-service/configs/config.docker.yaml +++ b/services/gateway-service/configs/config.docker.yaml @@ -99,6 +99,7 @@ tencent_im: user_sig_ttl: "24h" endpoint: "adminapisgp.im.qcloud.com" request_timeout: "5s" + group_id_prefix: "test_" callback_url: "" callback_auth_token: "" diff --git a/services/gateway-service/internal/app/app.go b/services/gateway-service/internal/app/app.go index fe67cae6..a7731e1f 100644 --- a/services/gateway-service/internal/app/app.go +++ b/services/gateway-service/internal/app/app.go @@ -130,6 +130,7 @@ func New(cfg config.Config) (*App, error) { UserSigTTL: cfg.TencentIM.UserSigTTL, AdminIdentifier: cfg.TencentIM.AdminIdentifier, CallbackAuthToken: cfg.TencentIM.CallbackAuthToken, + GroupIDPrefix: cfg.TencentIM.GroupIDPrefix, }, userProfileClient) if cfg.TencentIM.SDKAppID > 0 && strings.TrimSpace(cfg.TencentIM.SecretKey) != "" { imClient, err := tencentim.NewRESTClient(cfg.TencentIM.RESTConfig()) diff --git a/services/gateway-service/internal/config/config.go b/services/gateway-service/internal/config/config.go index 421a2767..05c3d678 100644 --- a/services/gateway-service/internal/config/config.go +++ b/services/gateway-service/internal/config/config.go @@ -166,6 +166,8 @@ type TencentIMConfig struct { Endpoint string `yaml:"endpoint"` // RequestTimeout 是 gateway 调腾讯 IM REST 的请求超时。 RequestTimeout time.Duration `yaml:"request_timeout"` + // GroupIDPrefix 给测试服等隔离环境使用,正式服为空以保持既有腾讯 IM 群不变。 + GroupIDPrefix string `yaml:"group_id_prefix"` // CallbackURL 是在腾讯云 IM 控制台配置的服务端回调地址。 CallbackURL string `yaml:"callback_url"` // CallbackAuthToken 是腾讯云 IM 回调鉴权 token,只用于校验回调来源。 @@ -462,6 +464,7 @@ func (cfg *Config) Normalize() error { if cfg.TencentIM.RequestTimeout <= 0 { cfg.TencentIM.RequestTimeout = 5 * time.Second } + cfg.TencentIM.GroupIDPrefix = strings.TrimSpace(cfg.TencentIM.GroupIDPrefix) cfg.TencentIM.CallbackURL = strings.TrimSpace(cfg.TencentIM.CallbackURL) cfg.TencentIM.CallbackAuthToken = strings.TrimSpace(cfg.TencentIM.CallbackAuthToken) diff --git a/services/gateway-service/internal/transport/http/callbackapi/handler.go b/services/gateway-service/internal/transport/http/callbackapi/handler.go index bc700443..0f7ec3f6 100644 --- a/services/gateway-service/internal/transport/http/callbackapi/handler.go +++ b/services/gateway-service/internal/transport/http/callbackapi/handler.go @@ -12,6 +12,7 @@ type TencentIMConfig struct { SDKAppID int64 AdminIdentifier string CallbackAuthToken string + GroupIDPrefix string } // TencentRTCConfig 是腾讯云 RTC 服务端回调校验需要的最小配置。 diff --git a/services/gateway-service/internal/transport/http/callbackapi/tencent_im_callback.go b/services/gateway-service/internal/transport/http/callbackapi/tencent_im_callback.go index 3292efd1..c7ac09bd 100644 --- a/services/gateway-service/internal/transport/http/callbackapi/tencent_im_callback.go +++ b/services/gateway-service/internal/transport/http/callbackapi/tencent_im_callback.go @@ -92,7 +92,7 @@ func (h *Handler) handleTencentIMCallback(writer http.ResponseWriter, request *h } func (h *Handler) handleTencentIMJoinCallback(writer http.ResponseWriter, request *http.Request, body tencentIMCallbackBody) { - parsed := imgroup.Parse(body.GroupID) + parsed := imgroup.ParseWithPrefix(body.GroupID, h.tencentIM.GroupIDPrefix) userID, ok := parseTencentIMUserID(body.Requestor) if !ok { writeTencentCallback(writer, http.StatusOK, 1, "join guard input invalid") @@ -149,7 +149,7 @@ func (h *Handler) handleTencentIMBroadcastJoinCallback(writer http.ResponseWrite } func (h *Handler) handleTencentIMSendCallback(writer http.ResponseWriter, request *http.Request, body tencentIMCallbackBody) { - parsed := imgroup.Parse(body.GroupID) + parsed := imgroup.ParseWithPrefix(body.GroupID, h.tencentIM.GroupIDPrefix) switch parsed.Kind { case imgroup.KindGlobalBroadcast, imgroup.KindRegionBroadcast: if h.tencentIMCallbackSenderIsServer(body) { diff --git a/services/gateway-service/internal/transport/http/handler.go b/services/gateway-service/internal/transport/http/handler.go index dbbc8805..b3dea9a3 100644 --- a/services/gateway-service/internal/transport/http/handler.go +++ b/services/gateway-service/internal/transport/http/handler.go @@ -63,6 +63,7 @@ type TencentIMConfig struct { UserSigTTL time.Duration AdminIdentifier string CallbackAuthToken string + GroupIDPrefix string } // TencentIMAccountImporter 是 gateway 在签发 UserSig 前补偿导入账号的最小边界。 @@ -333,7 +334,7 @@ func (h *Handler) issueTencentIMUserSig(writer http.ResponseWriter, request *htt } } - joinGroups, ok := imJoinGroupsForUser(app, user.GetRegionId()) + joinGroups, ok := imJoinGroupsForUser(app, user.GetRegionId(), h.tencentIM.GroupIDPrefix) if !ok { // 生成 join_groups 失败代表服务端 app_code/region_id 配置异常,不能返回缺失群列表让客户端自行猜。 httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") @@ -363,8 +364,12 @@ type tencentIMJoinGroup struct { RegionID int64 `json:"region_id,omitempty"` } -func imJoinGroupsForUser(app string, regionID int64) ([]tencentIMJoinGroup, bool) { - globalGroupID, err := imgroup.GlobalBroadcastGroupID(app) +func imJoinGroupsForUser(app string, regionID int64, groupIDPrefix ...string) ([]tencentIMJoinGroup, bool) { + prefix := "" + if len(groupIDPrefix) > 0 { + prefix = groupIDPrefix[0] + } + globalGroupID, err := imgroup.GlobalBroadcastGroupIDWithPrefix(prefix, app) if err != nil { return nil, false } @@ -375,7 +380,7 @@ func imJoinGroupsForUser(app string, regionID int64) ([]tencentIMJoinGroup, bool }} if regionID > 0 { // 区域群来自 user-service 当前 region_id,客户端不能自选区域群;区域变更后下一次 UserSig 返回新群。 - regionGroupID, err := imgroup.RegionBroadcastGroupID(app, regionID) + regionGroupID, err := imgroup.RegionBroadcastGroupIDWithPrefix(prefix, app, regionID) if err != nil { return nil, false } diff --git a/services/gateway-service/internal/transport/http/response_test.go b/services/gateway-service/internal/transport/http/response_test.go index 4e6ef505..596b6087 100644 --- a/services/gateway-service/internal/transport/http/response_test.go +++ b/services/gateway-service/internal/transport/http/response_test.go @@ -5294,6 +5294,36 @@ func TestTencentIMUserSigUsesAuthenticatedUserIDAndFailsClosed(t *testing.T) { t.Fatalf("region join group mismatch: %+v", groups[1]) } + prefixedRouter := NewHandlerWithConfig(&fakeRoomClient{}, nil, nil, nil, TencentIMConfig{ + SDKAppID: 1400000000, + SecretKey: "secret", + UserSigTTL: time.Hour, + GroupIDPrefix: "test_", + }, profileClient).Routes(auth.NewVerifier("secret")) + prefixedRequest := httptest.NewRequest(http.MethodGet, "/api/v1/im/usersig", nil) + prefixedRequest.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42)) + prefixedRecorder := httptest.NewRecorder() + prefixedRouter.ServeHTTP(prefixedRecorder, prefixedRequest) + if prefixedRecorder.Code != http.StatusOK { + t.Fatalf("prefixed status mismatch: got %d body=%s", prefixedRecorder.Code, prefixedRecorder.Body.String()) + } + var prefixedResponse httpkit.ResponseEnvelope + if err := json.NewDecoder(prefixedRecorder.Body).Decode(&prefixedResponse); err != nil { + t.Fatalf("decode prefixed response failed: %v", err) + } + prefixedData, ok := prefixedResponse.Data.(map[string]any) + if !ok { + t.Fatalf("prefixed response data mismatch: %+v", prefixedResponse) + } + prefixedGroups, ok := prefixedData["join_groups"].([]any) + if !ok || len(prefixedGroups) != 2 { + t.Fatalf("prefixed join_groups mismatch: %+v", prefixedData["join_groups"]) + } + prefixedRegionGroup, ok := prefixedGroups[1].(map[string]any) + if !ok || prefixedRegionGroup["group_id"] != "test_hy_lalu_bc_r_1001" { + t.Fatalf("prefixed region join group mismatch: %+v", prefixedGroups[1]) + } + missingConfigRouter := NewHandlerWithConfig(&fakeRoomClient{}, nil, nil, nil, TencentIMConfig{ SDKAppID: 1400000000, UserSigTTL: time.Hour, @@ -5645,6 +5675,27 @@ func TestTencentIMBroadcastCallbacksUseRegionGuardAndServerOnlySend(t *testing.T assertTencentCallback(t, adminSendRecorder, http.StatusOK, 0) } +func TestTencentIMBroadcastCallbacksUseConfiguredGroupPrefix(t *testing.T) { + profileClient := &fakeUserProfileClient{regionID: 1001} + router := NewHandlerWithConfig(&fakeRoomClient{}, &fakeRoomGuardClient{}, nil, nil, TencentIMConfig{ + SDKAppID: 1400000000, + AdminIdentifier: "administrator", + GroupIDPrefix: "test_", + }, profileClient).Routes(auth.NewVerifier("secret")) + + joinBody := []byte(`{"CallbackCommand":"Group.CallbackBeforeApplyJoinGroup","GroupId":"test_hy_lalu_bc_r_1001","Requestor_Account":"42"}`) + joinRequest := httptest.NewRequest(http.MethodPost, "/api/v1/tencent-im/callback?SdkAppid=1400000000&CallbackCommand=Group.CallbackBeforeApplyJoinGroup", bytes.NewReader(joinBody)) + joinRecorder := httptest.NewRecorder() + router.ServeHTTP(joinRecorder, joinRequest) + assertTencentCallback(t, joinRecorder, http.StatusOK, 0) + + oldGroupBody := []byte(`{"CallbackCommand":"Group.CallbackBeforeApplyJoinGroup","GroupId":"hy_lalu_bc_r_1001","Requestor_Account":"42"}`) + oldGroupRequest := httptest.NewRequest(http.MethodPost, "/api/v1/tencent-im/callback?SdkAppid=1400000000&CallbackCommand=Group.CallbackBeforeApplyJoinGroup", bytes.NewReader(oldGroupBody)) + oldGroupRecorder := httptest.NewRecorder() + router.ServeHTTP(oldGroupRecorder, oldGroupRequest) + assertTencentCallback(t, oldGroupRecorder, http.StatusOK, 1) +} + func TestTencentIMCallbackAuthAndUnknownCommand(t *testing.T) { router := NewHandlerWithConfig(&fakeRoomClient{}, &fakeRoomGuardClient{}, nil, nil, TencentIMConfig{ SDKAppID: 1400000000, diff --git a/services/gateway-service/internal/transport/http/router.go b/services/gateway-service/internal/transport/http/router.go index 8eb73abb..3bf162bf 100644 --- a/services/gateway-service/internal/transport/http/router.go +++ b/services/gateway-service/internal/transport/http/router.go @@ -45,6 +45,7 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler { UserIdentityClient: h.userIdentityClient, UserProfileClient: h.userProfileClient, UserHostClient: h.userHostClient, + IMGroupIDPrefix: h.tencentIM.GroupIDPrefix, }) userAPI := userapi.New(userapi.Config{ UserIdentityClient: h.userIdentityClient, @@ -97,6 +98,7 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler { SDKAppID: h.tencentIM.SDKAppID, AdminIdentifier: h.tencentIM.AdminIdentifier, CallbackAuthToken: h.tencentIM.CallbackAuthToken, + GroupIDPrefix: h.tencentIM.GroupIDPrefix, }, TencentRTC: callbackapi.TencentRTCConfig{ SDKAppID: h.tencentRTC.SDKAppID, diff --git a/services/gateway-service/internal/transport/http/walletapi/handler.go b/services/gateway-service/internal/transport/http/walletapi/handler.go index 1cdf12f5..c1be1e32 100644 --- a/services/gateway-service/internal/transport/http/walletapi/handler.go +++ b/services/gateway-service/internal/transport/http/walletapi/handler.go @@ -16,6 +16,7 @@ type Handler struct { userIdentityClient client.UserIdentityClient userProfileClient client.UserProfileClient userHostClient client.UserHostClient + imGroupIDPrefix string } type Config struct { @@ -25,6 +26,7 @@ type Config struct { UserIdentityClient client.UserIdentityClient UserProfileClient client.UserProfileClient UserHostClient client.UserHostClient + IMGroupIDPrefix string } func New(config Config) *Handler { @@ -35,6 +37,7 @@ func New(config Config) *Handler { userIdentityClient: config.UserIdentityClient, userProfileClient: config.UserProfileClient, userHostClient: config.UserHostClient, + imGroupIDPrefix: config.IMGroupIDPrefix, } } diff --git a/services/gateway-service/internal/transport/http/walletapi/red_packet_handler.go b/services/gateway-service/internal/transport/http/walletapi/red_packet_handler.go index a0c8e625..ad41b0ff 100644 --- a/services/gateway-service/internal/transport/http/walletapi/red_packet_handler.go +++ b/services/gateway-service/internal/transport/http/walletapi/red_packet_handler.go @@ -192,7 +192,7 @@ func (h *Handler) getCurrentRegionIMGroup(writer http.ResponseWriter, request *h httpkit.WriteError(writer, request, http.StatusConflict, "voice_room_region_missing", "conflict") return } - groupID, err := imgroup.RegionBroadcastGroupID(appcode.FromContext(request.Context()), user.GetRegionId()) + groupID, err := imgroup.RegionBroadcastGroupIDWithPrefix(h.imGroupIDPrefix, appcode.FromContext(request.Context()), user.GetRegionId()) if err != nil { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return