package roomapi import ( "context" "net/http" "strconv" "time" "hyapp/pkg/appcode" "hyapp/services/gateway-service/internal/auth" "hyapp/services/gateway-service/internal/transport/http/giftlimit" "hyapp/services/gateway-service/internal/transport/http/httpkit" ) const giftCapacityReleaseTimeout = time.Second type giftRateLimitedData struct { RetryAfterMS int64 `json:"retry_after_ms"` } // acquireGiftCapacity 在任何用户资料或 room-service RPC 前占用容量。 // 返回的 lease 必须 defer 释放;令牌代表本次请求已经给下游造成的成本,不因后续业务失败退还。 func (h *Handler) acquireGiftCapacity(writer http.ResponseWriter, request *http.Request, roomID string, totalGiftUnits int) (giftlimit.Lease, bool) { if !h.giftCapacityEnabled { return giftlimit.Lease{}, true } if h.giftCapacityLimiter == nil { // 生产配置要求保护默认开启;后端缺失时 fail-closed,不能在 Redis/装配故障时静默放开洪峰。 httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return giftlimit.Lease{}, false } if totalGiftUnits < 1 { totalGiftUnits = 1 } decision, err := h.giftCapacityLimiter.Acquire(request.Context(), giftlimit.Input{ AppCode: appcode.FromContext(request.Context()), UserID: auth.UserIDFromContext(request.Context()), RoomID: roomID, RequestID: httpkit.ResponseRequestID(request), Weight: totalGiftUnits, }) if err != nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return giftlimit.Lease{}, false } if decision.Allowed { return decision.Lease, true } retryAfter := decision.RetryAfter if retryAfter < time.Millisecond { retryAfter = time.Millisecond } retryAfterMS := retryAfter.Milliseconds() // Retry-After 是标准 HTTP 秒级提示;data.retry_after_ms 保留连击送礼所需的毫秒精度。 writer.Header().Set("Retry-After", strconv.FormatInt((retryAfterMS+999)/1000, 10)) httpkit.WriteEnvelope(writer, http.StatusTooManyRequests, httpkit.ResponseEnvelope{ Code: httpkit.CodeRateLimited, Message: "rate limited", RequestID: httpkit.ResponseRequestID(request), Data: giftRateLimitedData{RetryAfterMS: retryAfterMS}, }) return giftlimit.Lease{}, false } // releaseGiftCapacity 使用与客户端断连解耦的短 context;Redis 暂时失败时租约 TTL 仍会兜底回收。 func (h *Handler) releaseGiftCapacity(lease giftlimit.Lease) { if h.giftCapacityLimiter == nil { return } ctx, cancel := context.WithTimeout(context.Background(), giftCapacityReleaseTimeout) defer cancel() _ = h.giftCapacityLimiter.Release(ctx, lease) }