2026-07-12 00:47:20 +08:00

76 lines
2.7 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 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 使用与客户端断连解耦的短 contextRedis 暂时失败时租约 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)
}