127 lines
4.3 KiB
Go
127 lines
4.3 KiB
Go
// 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
|
||
}
|