2026-06-08 14:12:23 +08:00

419 lines
16 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 http
import (
"context"
"database/sql"
"hyapp/services/gateway-service/internal/transport/http/httpkit"
"io"
"net/http"
"strings"
"time"
"hyapp/pkg/appcode"
"hyapp/pkg/imgroup"
"hyapp/pkg/tencentim"
"hyapp/services/gateway-service/internal/auth"
"hyapp/services/gateway-service/internal/client"
"hyapp/services/gateway-service/internal/transport/http/appapi"
userv1 "hyapp.local/api/proto/user/v1"
)
// 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
userSocialClient client.UserSocialClient
userCPClient client.UserCPClient
userDeviceClient client.UserDeviceClient
userCountryClient client.UserCountryQueryClient
userHostClient client.UserHostClient
appRegistryClient client.AppRegistryClient
walletClient client.WalletClient
messageClient client.MessageInboxClient
taskClient client.TaskClient
growthLevelClient client.GrowthLevelClient
achievementClient client.AchievementClient
registrationReward client.RegistrationRewardClient
firstRechargeReward client.FirstRechargeRewardClient
cumulativeRecharge client.CumulativeRechargeRewardClient
sevenDayCheckIn client.SevenDayCheckInClient
luckyGift client.LuckyGiftClient
roomTurnoverReward client.RoomTurnoverRewardClient
weeklyStar client.WeeklyStarClient
broadcastClient client.BroadcastClient
gameClient client.GameClient
appConfigReader appapi.ConfigReader
leaderboardWalletDB *sql.DB
tencentIM TencentIMConfig
tencentIMImporter TencentIMAccountImporter
tencentRTC TencentRTCConfig
objectUploader ObjectUploader
authRateLimitConfig AuthRateLimitConfig
authRateLimiter publicAuthRateLimiter
loginRiskConfig LoginRiskConfig
loginRiskCache loginRiskCache
}
// TencentIMConfig 是 gateway transport 层签发客户端腾讯云 IM 登录票据所需的配置。
type TencentIMConfig struct {
SDKAppID int64
SecretKey string
UserSigTTL time.Duration
AdminIdentifier string
CallbackAuthToken string
GroupIDPrefix string
}
// TencentIMAccountImporter 是 gateway 在签发 UserSig 前补偿导入账号的最小边界。
type TencentIMAccountImporter interface {
ImportAccount(ctx context.Context, userID int64, nickname string, faceURL string) error
}
// TencentRTCConfig 是 gateway transport 层签发客户端腾讯 RTC 进房票据所需的配置。
type TencentRTCConfig struct {
Enabled bool
SDKAppID int64
SecretKey string
UserSigTTL time.Duration
RoomIDType string
AppScene string
CallbackSignKey string
}
// ObjectUploader 是 gateway 上传接口依赖的对象存储最小能力。
type ObjectUploader interface {
PutObject(ctx context.Context, key string, reader io.Reader, sizeBytes int64, contentType string) (string, error)
}
// NewHandler 装配 gateway HTTP handler 的内部依赖。
func NewHandler(roomClient client.RoomClient, userClient ...client.UserAuthClient) *Handler {
config := defaultAuthRateLimitConfig()
handler := &Handler{roomClient: roomClient, authRateLimitConfig: config, authRateLimiter: newMemoryAuthRateLimiter(config), loginRiskConfig: defaultLoginRiskConfig()}
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,
loginRiskConfig: defaultLoginRiskConfig(),
}
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,
loginRiskConfig: defaultLoginRiskConfig(),
}
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
}
// SetTencentIMAccountImporter 注入腾讯 IM 账号导入边界nil 表示仅签发 UserSig不做 REST 补偿。
func (h *Handler) SetTencentIMAccountImporter(importer TencentIMAccountImporter) {
h.tencentIMImporter = importer
}
// SetObjectUploader 注入对象存储上传实现;生产环境使用腾讯云 COS。
func (h *Handler) SetObjectUploader(uploader ObjectUploader) {
h.objectUploader = uploader
}
// 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
}
// SetUserDeviceClient 注入 user-service 设备推送 token client。
func (h *Handler) SetUserDeviceClient(userDeviceClient client.UserDeviceClient) {
h.userDeviceClient = userDeviceClient
}
// SetUserHostClient 注入 user-service host domain 身份查询 client。
func (h *Handler) SetUserHostClient(userHostClient client.UserHostClient) {
h.userHostClient = userHostClient
}
// SetUserSocialClient 注入 user-service 访问、关注和好友关系 client。
func (h *Handler) SetUserSocialClient(userSocialClient client.UserSocialClient) {
h.userSocialClient = userSocialClient
}
// SetUserCPClient 注入 user-service CP/兄弟/姐妹关系 client。
func (h *Handler) SetUserCPClient(userCPClient client.UserCPClient) {
h.userCPClient = userCPClient
}
// SetAppRegistryClient 注入 App 注册表解析 clientgateway 用它把包名转换成内部 app_code。
func (h *Handler) SetAppRegistryClient(appRegistryClient client.AppRegistryClient) {
h.appRegistryClient = appRegistryClient
}
// SetWalletClient 注入 wallet-service 余额查询 client。
func (h *Handler) SetWalletClient(walletClient client.WalletClient) {
h.walletClient = walletClient
}
// SetMessageInboxClient 注入 activity-service message inbox client。
func (h *Handler) SetMessageInboxClient(messageClient client.MessageInboxClient) {
h.messageClient = messageClient
}
// SetTaskClient 注入 activity-service task client。
func (h *Handler) SetTaskClient(taskClient client.TaskClient) {
h.taskClient = taskClient
}
// SetGrowthLevelClient 注入 activity-service 等级查询 client。
func (h *Handler) SetGrowthLevelClient(growthLevelClient client.GrowthLevelClient) {
h.growthLevelClient = growthLevelClient
}
// SetAchievementClient 注入 activity-service 成就和徽章 client。
func (h *Handler) SetAchievementClient(achievementClient client.AchievementClient) {
h.achievementClient = achievementClient
}
// SetRegistrationRewardClient 注入 activity-service 注册奖励查询 client。
func (h *Handler) SetRegistrationRewardClient(registrationReward client.RegistrationRewardClient) {
h.registrationReward = registrationReward
}
// SetFirstRechargeRewardClient 注入 activity-service 首冲奖励查询 client。
func (h *Handler) SetFirstRechargeRewardClient(firstRechargeReward client.FirstRechargeRewardClient) {
h.firstRechargeReward = firstRechargeReward
}
// SetCumulativeRechargeRewardClient 注入 activity-service 累充奖励查询 client。
func (h *Handler) SetCumulativeRechargeRewardClient(cumulativeRecharge client.CumulativeRechargeRewardClient) {
h.cumulativeRecharge = cumulativeRecharge
}
// SetSevenDayCheckInClient 注入 activity-service 七日签到 client。
func (h *Handler) SetSevenDayCheckInClient(sevenDayCheckIn client.SevenDayCheckInClient) {
h.sevenDayCheckIn = sevenDayCheckIn
}
// SetLuckyGiftClient 注入 activity-service 幸运礼物资格检查 client。
func (h *Handler) SetLuckyGiftClient(luckyGift client.LuckyGiftClient) {
h.luckyGift = luckyGift
}
// SetRoomTurnoverRewardClient 注入 activity-service 房间流水奖励查询 client。
func (h *Handler) SetRoomTurnoverRewardClient(roomTurnoverReward client.RoomTurnoverRewardClient) {
h.roomTurnoverReward = roomTurnoverReward
}
// SetWeeklyStarClient 注入 activity-service 周星活动 client。
func (h *Handler) SetWeeklyStarClient(weeklyStar client.WeeklyStarClient) {
h.weeklyStar = weeklyStar
}
// SetBroadcastClient 注入 activity-service 播报群成员关系 client。
func (h *Handler) SetBroadcastClient(broadcastClient client.BroadcastClient) {
h.broadcastClient = broadcastClient
}
// SetGameClient 注入 game-service 游戏列表、启动和回调 client。
func (h *Handler) SetGameClient(gameClient client.GameClient) {
h.gameClient = gameClient
}
// SetLeaderboardWalletDB 注入 App 榜单只读钱包库连接。
func (h *Handler) SetLeaderboardWalletDB(db *sql.DB) {
h.leaderboardWalletDB = db
}
// SetAppConfigReader 注入后台 App 配置读取依赖。
func (h *Handler) SetAppConfigReader(reader appapi.ConfigReader) {
h.appConfigReader = reader
}
func normalizeCommandID(commandID string) string {
// command_id 是客户端按用户动作生成的写命令幂等键gateway 只裁剪空白,不自动补值。
return strings.TrimSpace(commandID)
}
// decode 统一处理 HTTP JSON 入参解析失败分支。
// gateway 在这里拒绝非法 JSON不做房间业务校验避免把 Room Cell 规则复制到入口层。
func decode(writer http.ResponseWriter, request *http.Request, out any) bool {
if err := httpkit.DecodeJSON(request.Body, out); err != nil {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.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 {
httpkit.WriteRPCError(writer, request, err)
return
}
httpkit.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 票据。
httpkit.WriteError(writer, request, http.StatusUnauthorized, httpkit.CodeUnauthorized, "unauthorized")
return
}
if h.userProfileClient == nil {
// 播报群 join 列表必须来自 user-service 当前资料,不能靠 token 快照猜区域。
httpkit.WriteError(writer, request, http.StatusInternalServerError, httpkit.CodeInternalError, "internal error")
return
}
userResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
Meta: authRequestMeta(request, ""),
UserId: userID,
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
user := userResp.GetUser()
if user == nil {
httpkit.WriteError(writer, request, http.StatusInternalServerError, httpkit.CodeInternalError, "internal error")
return
}
if !user.GetProfileCompleted() {
httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodeProfileRequired, "profile required")
return
}
app := appcode.FromContext(request.Context())
if user.GetAppCode() != "" && appcode.Normalize(user.GetAppCode()) != app {
// UserSig 是 IM 登录凭证,不能给跨 app_code 用户签发,否则会绕过播报群租户隔离。
httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
return
}
result, err := tencentim.GenerateUserSig(tencentim.UserSigConfig{
SDKAppID: h.tencentIM.SDKAppID,
SecretKey: h.tencentIM.SecretKey,
TTL: h.tencentIM.UserSigTTL,
}, tencentim.FormatUserID(userID), time.Now().UTC())
if err != nil {
if h.tencentIMImporter != nil {
_ = h.tencentIMImporter.ImportAccount(request.Context(), userID, user.GetUsername(), user.GetAvatar())
result, err = tencentim.GenerateUserSig(tencentim.UserSigConfig{
SDKAppID: h.tencentIM.SDKAppID,
SecretKey: h.tencentIM.SecretKey,
TTL: h.tencentIM.UserSigTTL,
}, tencentim.FormatUserID(userID), time.Now().UTC())
}
}
if err != nil {
// 缺少 SDKAppID 或 SecretKey 属于服务端配置错误,不能返回假票据给客户端。
httpkit.WriteError(writer, request, http.StatusInternalServerError, httpkit.CodeInternalError, "internal error")
return
}
if h.tencentIMImporter != nil {
if err := h.tencentIMImporter.ImportAccount(request.Context(), userID, user.GetUsername(), user.GetAvatar()); err != nil {
// UserSig 只证明签名有效;账号未导入时 SDK C2C 仍会失败,因此这里必须 fail-closed。
httpkit.WriteError(writer, request, http.StatusInternalServerError, httpkit.CodeInternalError, "internal error")
return
}
}
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")
return
}
httpkit.WriteOK(writer, request, tencentIMUserSigResponse{
SDKAppID: result.SDKAppID,
UserID: result.UserID,
UserSig: result.UserSig,
ExpireAtMS: result.ExpireAtMS,
JoinGroups: joinGroups,
})
}
type tencentIMUserSigResponse struct {
SDKAppID int64 `json:"sdk_app_id"`
UserID string `json:"user_id"`
UserSig string `json:"user_sig"`
ExpireAtMS int64 `json:"expire_at_ms"`
JoinGroups []tencentIMJoinGroup `json:"join_groups"`
}
type tencentIMJoinGroup struct {
GroupID string `json:"group_id"`
Type string `json:"type"`
RegionID int64 `json:"region_id,omitempty"`
}
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
}
// 全局群始终返回;实际消息量应很低,主要用于全服级运营通知或极少数全局事件。
groups := []tencentIMJoinGroup{{
GroupID: globalGroupID,
Type: string(imgroup.KindGlobalBroadcast),
}}
if regionID > 0 {
// 区域群来自 user-service 当前 region_id客户端不能自选区域群区域变更后下一次 UserSig 返回新群。
regionGroupID, err := imgroup.RegionBroadcastGroupIDWithPrefix(prefix, app, regionID)
if err != nil {
return nil, false
}
groups = append(groups, tencentIMJoinGroup{
GroupID: regionGroupID,
Type: string(imgroup.KindRegionBroadcast),
RegionID: regionID,
})
}
return groups, true
}