package http import ( "net/http" "strings" "time" roomv1 "hyapp/api/proto/room/v1" "hyapp/pkg/idgen" "hyapp/pkg/tencentim" "hyapp/services/gateway-service/internal/auth" "hyapp/services/gateway-service/internal/client" ) // Handler 是 gateway 的 HTTP transport adapter。 // 它只依赖跨服务 client,不持有房间状态,也不直接实现房间命令语义。 type Handler struct { roomClient client.RoomClient roomGuardClient client.RoomGuardClient roomQueryClient client.RoomQueryClient userClient client.UserAuthClient userIdentityClient client.UserIdentityClient userProfileClient client.UserProfileClient userCountryClient client.UserCountryQueryClient tencentIM TencentIMConfig tencentRTC TencentRTCConfig authRateLimitConfig AuthRateLimitConfig authRateLimiter publicAuthRateLimiter } // TencentIMConfig 是 gateway transport 层签发客户端腾讯云 IM 登录票据所需的配置。 type TencentIMConfig struct { SDKAppID int64 SecretKey string UserSigTTL time.Duration CallbackAuthToken string } // TencentRTCConfig 是 gateway transport 层签发客户端腾讯 RTC 进房票据所需的配置。 type TencentRTCConfig struct { Enabled bool SDKAppID int64 SecretKey string UserSigTTL time.Duration RoomIDType string AppScene string } // NewHandler 装配 gateway HTTP handler 的内部依赖。 func NewHandler(roomClient client.RoomClient, userClient ...client.UserAuthClient) *Handler { config := defaultAuthRateLimitConfig() handler := &Handler{roomClient: roomClient, authRateLimitConfig: config, authRateLimiter: newMemoryAuthRateLimiter(config)} if len(userClient) > 0 { handler.userClient = userClient[0] } return handler } // NewHandlerWithClients 装配需要用户资料接口的 gateway HTTP handler。 func NewHandlerWithClients(roomClient client.RoomClient, userClient client.UserAuthClient, userIdentityClient client.UserIdentityClient, userProfileClient ...client.UserProfileClient) *Handler { handler := &Handler{ roomClient: roomClient, userClient: userClient, userIdentityClient: userIdentityClient, } handler.SetAuthRateLimit(defaultAuthRateLimitConfig()) if len(userProfileClient) > 0 { handler.userProfileClient = userProfileClient[0] } return handler } // NewHandlerWithConfig 装配完整 gateway HTTP handler 和外部云服务配置。 func NewHandlerWithConfig(roomClient client.RoomClient, roomGuardClient client.RoomGuardClient, userClient client.UserAuthClient, userIdentityClient client.UserIdentityClient, tencentIM TencentIMConfig, userProfileClient ...client.UserProfileClient) *Handler { handler := &Handler{ roomClient: roomClient, roomGuardClient: roomGuardClient, userClient: userClient, userIdentityClient: userIdentityClient, tencentIM: tencentIM, } handler.SetAuthRateLimit(defaultAuthRateLimitConfig()) if len(userProfileClient) > 0 { handler.userProfileClient = userProfileClient[0] } return handler } // SetTencentRTC 注入 RTC 签发配置;缺失或 disabled 时 token endpoint 会 fail-closed。 func (h *Handler) SetTencentRTC(config TencentRTCConfig) { h.tencentRTC = config } // SetRoomQueryClient 注入 room-service 读模型查询 client。 func (h *Handler) SetRoomQueryClient(roomQueryClient client.RoomQueryClient) { h.roomQueryClient = roomQueryClient } // SetUserCountryQueryClient 注入注册页国家列表查询 client。 func (h *Handler) SetUserCountryQueryClient(userCountryClient client.UserCountryQueryClient) { h.userCountryClient = userCountryClient } // meta 把外部 HTTP 请求上下文转换成 room-service 命令元信息。 // request_id 只做链路追踪;command_id 才是 room-service command log 使用的幂等键。 func meta(request *http.Request, roomID string, commandID string) *roomv1.RequestMeta { return &roomv1.RequestMeta{ RequestId: requestIDFromContext(request.Context()), CommandId: commandIDOrNew(commandID), ActorUserId: auth.UserIDFromContext(request.Context()), RoomId: roomID, GatewayNodeId: "gateway-local", SessionId: idgen.New("sess"), SentAtMs: time.Now().UnixMilli(), } } func commandIDOrNew(commandID string) string { commandID = strings.TrimSpace(commandID) if commandID != "" { // 客户端重试同一业务命令时必须复用该值,room-service 才能命中幂等日志。 return commandID } // 自动生成只保证本次请求有 command_id,不保证客户端重试仍命中同一个命令。 return idgen.New("cmd") } // decode 统一处理 HTTP JSON 入参解析失败分支。 // gateway 在这里拒绝非法 JSON,不做房间业务校验,避免把 Room Cell 规则复制到入口层。 func decode(writer http.ResponseWriter, request *http.Request, out any) bool { if err := decodeJSON(request.Body, out); err != nil { writeError(writer, request, http.StatusBadRequest, codeInvalidJSON, "invalid request body") return false } return true } // write 统一把内部 gRPC 调用结果转换为 HTTP JSON 响应。 // 首版不在 gateway 细分 gRPC 错误码,避免入口层提前耦合 room-service 的业务错误模型。 func write(writer http.ResponseWriter, request *http.Request, body any, err error) { if err != nil { writeRPCError(writer, request, err) return } writeOK(writer, request, body) } func (h *Handler) issueTencentIMUserSig(writer http.ResponseWriter, request *http.Request) { userID := auth.UserIDFromContext(request.Context()) if userID <= 0 { // 正常路径已由 auth middleware 拦截;这里兜底避免签发匿名 IM 票据。 writeError(writer, request, http.StatusUnauthorized, codeUnauthorized, "unauthorized") return } result, err := tencentim.GenerateUserSig(tencentim.UserSigConfig{ SDKAppID: h.tencentIM.SDKAppID, SecretKey: h.tencentIM.SecretKey, TTL: h.tencentIM.UserSigTTL, }, tencentim.FormatUserID(userID), time.Now()) if err != nil { // 缺少 SDKAppID 或 SecretKey 属于服务端配置错误,不能返回假票据给客户端。 writeError(writer, request, http.StatusInternalServerError, codeInternalError, "internal error") return } writeOK(writer, request, result) }