Skip to main content

yggdrasil/pages/admin/settings/
ratelimit_section.rs

1//! 接口限流分区:各限流桶(含 MCP 令牌桶)的每秒请求数与突发上限、日限额与回收间隔。
2//!
3//! 写入数据库,下次启动生效(限流桶在首次请求时从启动配置构建)。
4
5use leptos::prelude::*;
6
7use super::kit::{use_section, SettingRow, SettingsCard};
8use super::SettingsSection;
9use crate::api::error::ApiError;
10use crate::api::settings::{get_rate_limit_settings, update_rate_limit_settings};
11use crate::components::forms::NumberInput;
12use crate::models::settings::RateLimitSettings;
13
14async fn save(d: RateLimitSettings) -> Result<RateLimitSettings, ApiError> {
15    update_rate_limit_settings(
16        d.strict_per_sec,
17        d.strict_burst,
18        d.upload_per_sec,
19        d.upload_burst,
20        d.image_per_sec,
21        d.image_burst,
22        d.comment_per_sec,
23        d.comment_burst,
24        d.comment_upload_per_sec,
25        d.comment_upload_burst,
26        d.comment_upload_daily,
27        d.code_exec_per_sec,
28        d.code_exec_burst,
29        d.code_exec_daily,
30        d.unknown_per_sec,
31        d.unknown_burst,
32        d.mcp_per_sec,
33        d.mcp_burst,
34        d.mcp_upload_per_sec,
35        d.mcp_upload_burst,
36        d.gc_interval_secs,
37    )
38    .await
39}
40
41/// 一个限流桶:每秒请求数与突发上限两个字段。
42type Bucket = (
43    &'static str,
44    &'static str,
45    fn(&RateLimitSettings) -> (u32, u32),
46    fn(&mut RateLimitSettings, u32, u32),
47);
48
49const BUCKETS: [Bucket; 9] = [
50    (
51        "登录与注册",
52        "strict",
53        |s| (s.strict_per_sec, s.strict_burst),
54        |s, a, b| {
55            s.strict_per_sec = a;
56            s.strict_burst = b;
57        },
58    ),
59    (
60        "图片上传",
61        "upload",
62        |s| (s.upload_per_sec, s.upload_burst),
63        |s, a, b| {
64            s.upload_per_sec = a;
65            s.upload_burst = b;
66        },
67    ),
68    (
69        "图片访问",
70        "image",
71        |s| (s.image_per_sec, s.image_burst),
72        |s, a, b| {
73            s.image_per_sec = a;
74            s.image_burst = b;
75        },
76    ),
77    (
78        "发表评论",
79        "comment",
80        |s| (s.comment_per_sec, s.comment_burst),
81        |s, a, b| {
82            s.comment_per_sec = a;
83            s.comment_burst = b;
84        },
85    ),
86    (
87        "评论传图",
88        "comment-upload",
89        |s| (s.comment_upload_per_sec, s.comment_upload_burst),
90        |s, a, b| {
91            s.comment_upload_per_sec = a;
92            s.comment_upload_burst = b;
93        },
94    ),
95    (
96        "运行代码",
97        "code-exec",
98        |s| (s.code_exec_per_sec, s.code_exec_burst),
99        |s, a, b| {
100            s.code_exec_per_sec = a;
101            s.code_exec_burst = b;
102        },
103    ),
104    (
105        "无法识别 IP",
106        "unknown",
107        |s| (s.unknown_per_sec, s.unknown_burst),
108        |s, a, b| {
109            s.unknown_per_sec = a;
110            s.unknown_burst = b;
111        },
112    ),
113    (
114        "MCP 请求",
115        "mcp",
116        |s| (s.mcp_per_sec, s.mcp_burst),
117        |s, a, b| {
118            s.mcp_per_sec = a;
119            s.mcp_burst = b;
120        },
121    ),
122    (
123        "MCP 传图",
124        "mcp-upload",
125        |s| (s.mcp_upload_per_sec, s.mcp_upload_burst),
126        |s, a, b| {
127            s.mcp_upload_per_sec = a;
128            s.mcp_upload_burst = b;
129        },
130    ),
131];
132
133/// 接口限流分区组件。
134#[component]
135pub fn RateLimitSection() -> impl IntoView {
136    let st = use_section(SettingsSection::RateLimit, get_rate_limit_settings, save);
137    view! {
138        <SettingsCard
139            section=SettingsSection::RateLimit
140            description="按 IP 计的令牌桶(MCP 两项按令牌计):每秒补充的请求数与可以一次用掉的突发上限。"
141            ready=st.ready()
142            load_failed=st.load_failed()
143            failed=st.failed()
144        >
145            {BUCKETS
146                .into_iter()
147                .map(|(label, key, get, set)| {
148                    view! {
149                        <SettingRow label=label>
150                            <div class="settings-pair">
151                                <NumberInput
152                                    id=format!("rl-{key}-rate")
153                                    value=st.field(move |d| i64::from(get(d).0))
154                                    min=1
155                                    max=10_000
156                                    unit="次/秒"
157                                    onchange=move |v: i64| {
158                                        st.edit(|s| {
159                                            let (_, b) = get(s);
160                                            set(s, v as u32, b);
161                                        })
162                                    }
163                                />
164                                <NumberInput
165                                    id=format!("rl-{key}-burst")
166                                    value=st.field(move |d| i64::from(get(d).1))
167                                    min=1
168                                    max=10_000
169                                    unit="突发"
170                                    onchange=move |v: i64| {
171                                        st.edit(|s| {
172                                            let (a, _) = get(s);
173                                            set(s, a, v as u32);
174                                        })
175                                    }
176                                />
177                            </div>
178                        </SettingRow>
179                    }
180                })
181                .collect_view()}
182            <SettingRow label="评论传图日限额" html_for="rl-comment-upload-daily">
183                <NumberInput
184                    id="rl-comment-upload-daily"
185                    value=st.field(|d| i64::from(d.comment_upload_daily))
186                    min=1
187                    max=100_000
188                    unit="次/天"
189                    hint="评论区允许匿名传图,日限额是防止磁盘被占满的关键防线。"
190                    onchange=move |v: i64| st.edit(|d| d.comment_upload_daily = v as u32)
191                />
192            </SettingRow>
193            <SettingRow label="运行代码日限额" html_for="rl-code-exec-daily">
194                <NumberInput
195                    id="rl-code-exec-daily"
196                    value=st.field(|d| i64::from(d.code_exec_daily))
197                    min=1
198                    max=100_000
199                    unit="次/天"
200                    onchange=move |v: i64| st.edit(|d| d.code_exec_daily = v as u32)
201                />
202            </SettingRow>
203            <SettingRow label="回收间隔" html_for="rl-gc">
204                <NumberInput
205                    id="rl-gc"
206                    value=st.field(|d| i64::from(d.gc_interval_secs))
207                    min=10
208                    max=86_400
209                    step=60
210                    unit="秒"
211                    hint="定期清掉不活跃 IP 的限流桶,释放内存。"
212                    onchange=move |v: i64| st.edit(|d| d.gc_interval_secs = v as u32)
213                />
214            </SettingRow>
215        </SettingsCard>
216    }
217}