feat(rpm): RPM 限流模块优化

P0:
- rpm_override 嵌入 Auth Cache Snapshot,消除每请求 DB 查询 (snapshot v6→v7)
- 429 RPM 响应返回 Retry-After 头(当前分钟剩余秒数)

P1:
- ClearAll 按钮直连 DELETE API,带 loading 防重复
- 新增 GET /admin/users/:id/rpm-status 管理员 RPM 用量查询端点

优化:
- checkRPM 从级联互斥改为并行取最严,user.rpm_limit 作为全局硬上限始终生效
- Override/Group 变更后自动失效 auth cache
- fail-open 语义不变,Redis 故障不阻塞业务
This commit is contained in:
james-6-23
2026-04-23 03:33:52 +08:00
parent ef967d8f8a
commit dc5d42addc
79 changed files with 2831 additions and 140 deletions

View File

@@ -308,6 +308,15 @@
t("admin.groups.rateMultipliers")
}}</span>
</button>
<button
@click="handleRPMOverrides(row)"
class="flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-orange-600 dark:hover:bg-dark-700 dark:hover:text-orange-400"
>
<Icon name="bolt" size="sm" />
<span class="text-xs">{{
t("admin.groups.rpmOverrides")
}}</span>
</button>
<button
@click="handleDelete(row)"
class="flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20 dark:hover:text-red-400"
@@ -491,6 +500,18 @@
/>
<p class="input-hint">{{ t("admin.groups.rateMultiplierHint") }}</p>
</div>
<div>
<label class="input-label">{{ t("admin.groups.form.rpmLimit") }}</label>
<input
v-model.number="createForm.rpm_limit"
type="number"
min="0"
step="1"
class="input"
:placeholder="t('admin.groups.form.rpmLimitPlaceholder')"
/>
<p class="input-hint">{{ t("admin.groups.form.rpmLimitHint") }}</p>
</div>
<div
v-if="createForm.subscription_type !== 'subscription'"
data-tour="group-form-exclusive"
@@ -1612,6 +1633,18 @@
data-tour="group-form-multiplier"
/>
</div>
<div>
<label class="input-label">{{ t("admin.groups.form.rpmLimit") }}</label>
<input
v-model.number="editForm.rpm_limit"
type="number"
min="0"
step="1"
class="input"
:placeholder="t('admin.groups.form.rpmLimitPlaceholder')"
/>
<p class="input-hint">{{ t("admin.groups.form.rpmLimitHint") }}</p>
</div>
<div v-if="editForm.subscription_type !== 'subscription'">
<div class="mb-1.5 flex items-center gap-1">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">
@@ -2689,6 +2722,14 @@
@close="showRateMultipliersModal = false"
@success="loadGroups"
/>
<!-- Group RPM Overrides Modal -->
<GroupRPMOverridesModal
:show="showRPMOverridesModal"
:group="rpmOverridesGroup"
@close="showRPMOverridesModal = false"
@success="loadGroups"
/>
</AppLayout>
</template>
@@ -2711,6 +2752,7 @@ import Select from "@/components/common/Select.vue";
import PlatformIcon from "@/components/common/PlatformIcon.vue";
import Icon from "@/components/icons/Icon.vue";
import GroupRateMultipliersModal from "@/components/admin/group/GroupRateMultipliersModal.vue";
import GroupRPMOverridesModal from "@/components/admin/group/GroupRPMOverridesModal.vue";
import GroupCapacityBadge from "@/components/common/GroupCapacityBadge.vue";
import { VueDraggable } from "vue-draggable-plus";
import { createStableObjectKeyResolver } from "@/utils/stableObjectKey";
@@ -2951,6 +2993,8 @@ const editingGroup = ref<AdminGroup | null>(null);
const deletingGroup = ref<AdminGroup | null>(null);
const showRateMultipliersModal = ref(false);
const rateMultipliersGroup = ref<AdminGroup | null>(null);
const showRPMOverridesModal = ref(false);
const rpmOverridesGroup = ref<AdminGroup | null>(null);
const sortableGroups = ref<AdminGroup[]>([]);
const createMessagesDispatchDefaults = createDefaultMessagesDispatchFormState();
const editMessagesDispatchDefaults = createDefaultMessagesDispatchFormState();
@@ -2990,6 +3034,8 @@ const createForm = reactive({
mcp_xml_inject: true,
// 从分组复制账号
copy_accounts_from_group_ids: [] as number[],
// 分组级 RPM 限制每用户每分钟最大请求数0 = 不限制)
rpm_limit: 0 as number,
});
// 简单账号类型(用于模型路由选择)
@@ -3271,6 +3317,8 @@ const editForm = reactive({
mcp_xml_inject: true,
// 从分组复制账号
copy_accounts_from_group_ids: [] as number[],
// 分组级 RPM 限制每用户每分钟最大请求数0 = 不限制)
rpm_limit: 0 as number,
});
// 根据分组类型返回不同的删除确认消息
@@ -3562,6 +3610,7 @@ const handleEdit = async (group: AdminGroup) => {
];
editForm.mcp_xml_inject = group.mcp_xml_inject ?? true;
editForm.copy_accounts_from_group_ids = []; // 复制账号字段每次编辑时重置为空
editForm.rpm_limit = group.rpm_limit ?? 0;
// 加载模型路由规则(异步加载账号名称)
editModelRoutingRules.value = await convertApiFormatToRoutingRules(
group.model_routing,
@@ -3670,6 +3719,11 @@ const handleRateMultipliers = (group: AdminGroup) => {
showRateMultipliersModal.value = true;
};
const handleRPMOverrides = (group: AdminGroup) => {
rpmOverridesGroup.value = group;
showRPMOverridesModal.value = true;
};
const handleDelete = (group: AdminGroup) => {
deletingGroup.value = group;
showDeleteDialog.value = true;

View File

@@ -2170,6 +2170,24 @@
{{ t("admin.settings.defaults.defaultConcurrencyHint") }}
</p>
</div>
<div>
<label
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ t("admin.settings.defaults.defaultUserRpmLimit") }}
</label>
<input
v-model.number="form.default_user_rpm_limit"
type="number"
min="0"
step="1"
class="input"
placeholder="0"
/>
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
{{ t("admin.settings.defaults.defaultUserRpmLimitHint") }}
</p>
</div>
</div>
<div class="border-t border-gray-100 pt-4 dark:border-dark-700">
@@ -4867,6 +4885,7 @@ const form = reactive<SettingsForm>({
default_concurrency: 1,
default_subscriptions: [],
force_email_on_third_party_signup: false,
default_user_rpm_limit: 0,
site_name: "Sub2API",
site_logo: "",
site_subtitle: "Subscription to API Conversion Platform",
@@ -5783,6 +5802,7 @@ async function saveSettings() {
default_concurrency: form.default_concurrency,
default_subscriptions: normalizedDefaultSubscriptions,
force_email_on_third_party_signup: form.force_email_on_third_party_signup,
default_user_rpm_limit: form.default_user_rpm_limit,
site_name: form.site_name,
site_logo: form.site_logo,
site_subtitle: form.site_subtitle,