1) 客服 Agent 四份交付文档 + 构建脚手架:品牌由包装占位 XX科技 / 旧名 南方财富 统一为南方基金(热线 400-889-8899 / 官网 nffund.com),系统名改为「智能服务系统」; 同步追加 §0.4 修订记录行,工程记录行保留原占位字面以支撑硬编码扫描验收。 2) 开发文档:清理 28 份已作废/残留文档(14 份移出归档 + 14 份仓库副本), 新增《文档规整方案与开发前待决事项-2026-09-17》。 3) 客服agent 四份交付文档首次纳入本分支。
52 lines
2.1 KiB
Python
52 lines
2.1 KiB
Python
"""限流策略值与错误载具(文档 §3.5 的 429、§3.6 的 `RATE_LIMITED`)。
|
||
|
||
放在 `app/core` 而不是 `errors.py`:码值与状态码的唯一口径在 `app/core/errors.py`,
|
||
这里只做两件事——把配置读成不可变策略对象,以及给 `429` 挂上文档要求的
|
||
`Retry-After` 数值载体(`RateLimitedError` 本身不携带秒数)。
|
||
"""
|
||
|
||
from dataclasses import dataclass
|
||
|
||
from app.core.config import Settings
|
||
from app.core.errors import RateLimitedError
|
||
|
||
|
||
class RateLimitExceededError(RateLimitedError):
|
||
"""`429 RATE_LIMITED` 的运行时载具:额外携带 `Retry-After` 秒数。
|
||
|
||
`code` / `status_code` / `retryable` 全部继承 `RateLimitedError`(文档 §3.6 口径:
|
||
429、可重试),`app/main.py` 的处理器据此输出统一错误信封并附加 `Retry-After` 头。
|
||
"""
|
||
|
||
def __init__(self, message: str, retry_after_seconds: int) -> None:
|
||
super().__init__(message)
|
||
# 头部取值必须是正整数秒:0 会让"立刻重试"变成忙等,负数非法。
|
||
self.retry_after_seconds = max(1, int(retry_after_seconds))
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class RateLimitPolicy:
|
||
"""限流策略:按"用户 + 方法 + 路由"在固定窗口内计数。
|
||
|
||
维度取"用户 + 接口"而不是全站总量:单个客户端的异常流量不应该把其他用户一起
|
||
拖下水;不同接口的成本差异很大,混在一个计数器里会让慢接口拖累快接口。
|
||
"""
|
||
|
||
enabled: bool
|
||
window_seconds: int
|
||
max_requests: int
|
||
key_prefix: str
|
||
|
||
@classmethod
|
||
def from_settings(cls, settings: Settings) -> "RateLimitPolicy":
|
||
return cls(
|
||
enabled=settings.rate_limit_enabled,
|
||
window_seconds=settings.rate_limit_window_seconds,
|
||
max_requests=settings.rate_limit_max_requests,
|
||
key_prefix=settings.rate_limit_key_prefix,
|
||
)
|
||
|
||
def key(self, user_id: str, method: str, route_path: str) -> str:
|
||
"""计数键。用路由**模板**而不是原始 URL:`/{run_id}` 下每个 run 都是同一接口。"""
|
||
return f"{self.key_prefix}:{user_id}:{method.upper()}:{route_path}"
|