hyapp-server/pkg/imgroup/imgroup.go
2026-05-12 09:53:20 +08:00

127 lines
4.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package imgroup 定义腾讯云 IM GroupID 的唯一生成和解析规则。
// gateway、room-service、activity-service 必须共享这里的规则,避免某个服务手写字符串后把播报群误当房间群。
package imgroup
import (
"fmt"
"strconv"
"strings"
"hyapp/pkg/appcode"
"hyapp/pkg/roomid"
)
const (
// KindUnknown 表示 GroupID 既不是本项目播报群,也不是合法房间群;回调层必须 fail-closed。
KindUnknown Kind = "unknown"
// KindRoom 保持已有房间群规则:腾讯 IM GroupID 直接等于内部 room_id。
KindRoom Kind = "room"
// KindGlobalBroadcast 是 App 维度全局播报群,只允许服务端管理员账号发消息。
KindGlobalBroadcast Kind = "global_broadcast"
// KindRegionBroadcast 是区域播报群,用户只能加入自己 user-service 归属的区域群。
KindRegionBroadcast Kind = "region_broadcast"
// globalSuffix/regionToken 是可解析协议的一部分;不要在业务代码里重复拼接这些片段。
globalSuffix = "_bc_g"
regionToken = "_bc_r_"
)
// Kind 是回调分流和客户端 join 列表使用的稳定分类,不直接暴露腾讯云原始字符串判断。
type Kind string
// ParsedGroup 是 GroupID 的强类型解析结果;只有对应 Kind 的字段才有业务含义。
type ParsedGroup struct {
Kind Kind
AppCode string
RegionID int64
RoomID string
}
// GlobalBroadcastGroupID 生成 App 全局播报群 ID。
// app_code 先 Normalize 再进入 GroupID确保 HTTP、gRPC、outbox 和腾讯回调看到的是同一租户标识。
func GlobalBroadcastGroupID(value string) (string, error) {
app := appcode.Normalize(value)
if err := validateAppCodeForGroupID(app); err != nil {
return "", err
}
groupID := "hy_" + app + globalSuffix
if len(groupID) > roomid.MaxStringIDLength {
return "", fmt.Errorf("global broadcast group_id is too long")
}
return groupID, nil
}
// RegionBroadcastGroupID 生成区域播报群 ID。
// region_id <= 0 被拒绝GLOBAL/未知区域不能变成一个可 join 的区域群。
func RegionBroadcastGroupID(value string, regionID int64) (string, error) {
app := appcode.Normalize(value)
if err := validateAppCodeForGroupID(app); err != nil {
return "", err
}
if regionID <= 0 {
return "", fmt.Errorf("region_id must be positive")
}
groupID := "hy_" + app + regionToken + strconv.FormatInt(regionID, 10)
if len(groupID) > roomid.MaxStringIDLength {
return "", fmt.Errorf("region broadcast group_id is too long")
}
return groupID, nil
}
// Parse 在调用任何 room guard 前完成 GroupID 分类。
// 解析顺序必须先播报群、后房间群;否则 hy_<app>_bc_* 这类合法短字符串可能被 roomid 规则吞掉。
func Parse(groupID string) ParsedGroup {
groupID = strings.TrimSpace(groupID)
if groupID == "" {
return ParsedGroup{Kind: KindUnknown}
}
if strings.HasPrefix(groupID, "hy_") && strings.HasSuffix(groupID, globalSuffix) {
app := strings.TrimSuffix(strings.TrimPrefix(groupID, "hy_"), globalSuffix)
if validateAppCodeForGroupID(app) == nil {
return ParsedGroup{Kind: KindGlobalBroadcast, AppCode: app}
}
return ParsedGroup{Kind: KindUnknown}
}
if after, ok := strings.CutPrefix(groupID, "hy_"); ok {
body := after
tokenIndex := strings.LastIndex(body, regionToken)
if tokenIndex >= 0 {
app := body[:tokenIndex]
rawRegion := body[tokenIndex+len(regionToken):]
regionID, err := strconv.ParseInt(rawRegion, 10, 64)
if err == nil && regionID > 0 && validateAppCodeForGroupID(app) == nil {
return ParsedGroup{Kind: KindRegionBroadcast, AppCode: app, RegionID: regionID}
}
return ParsedGroup{Kind: KindUnknown}
}
}
if roomid.ValidStringID(groupID) {
return ParsedGroup{Kind: KindRoom, RoomID: groupID}
}
return ParsedGroup{Kind: KindUnknown}
}
func validateAppCodeForGroupID(app string) error {
if app == "" {
return fmt.Errorf("app_code is required")
}
// GroupID 是腾讯云侧长期存在的外部标识,只允许稳定 ASCII 子集,避免大小写、空格或 Unicode 造成不可逆映射。
for i := 0; i < len(app); i++ {
char := app[i]
if char >= 'a' && char <= 'z' {
continue
}
if char >= '0' && char <= '9' {
continue
}
if char == '_' || char == '-' {
continue
}
return fmt.Errorf("app_code contains unsupported character")
}
return nil
}