// Package appcode 集中处理多 App 租户键的归一化和 context 透传。 package appcode import ( "context" "crypto/rand" "fmt" "strings" ) const ( // Default 是当前开发阶段的种子 App;旧请求未显式携带 app_code 时统一落到 lalu。 Default = "lalu" ) type contextKey struct{} // Normalize 把外部输入收敛成数据库使用的短 app_code。 func Normalize(value string) string { value = strings.ToLower(strings.TrimSpace(value)) if value == "" { // 开发阶段不保留历史兼容表结构,但测试和后台任务仍需要一个稳定默认租户。 return Default } return value } // WithContext 把 app_code 写入 context,供 service/repository 在不扩散方法签名的情况下取用。 func WithContext(ctx context.Context, value string) context.Context { return context.WithValue(ctx, contextKey{}, Normalize(value)) } // FromContext 从 context 读取 app_code;缺失时返回当前开发默认 App。 func FromContext(ctx context.Context) string { if ctx == nil { return Default } value, _ := ctx.Value(contextKey{}).(string) return Normalize(value) } // NewScopedID 生成对客户端暴露的 App 长 ID。 // 格式固定为 app_code + "_" + uuid,保证同一个 ID 可直接作为用户长 ID、房间 ID 和腾讯 IM/RTC 字符串标识。 func NewScopedID(value string) string { app := Normalize(value) var bytes [16]byte if _, err := rand.Read(bytes[:]); err != nil { // crypto/rand 失败极少见;这里退化为全 0 仍保留格式,后续数据库唯一键会兜底冲突。 return fmt.Sprintf("%s_00000000-0000-4000-8000-000000000000", app) } bytes[6] = (bytes[6] & 0x0f) | 0x40 bytes[8] = (bytes[8] & 0x3f) | 0x80 return fmt.Sprintf("%s_%08x-%04x-%04x-%04x-%012x", app, bytes[0:4], bytes[4:6], bytes[6:8], bytes[8:10], bytes[10:16], ) }