mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-05-05 21:50:44 +08:00
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:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user