fix: 高峰倍率全链路透传、计费术语修正与边界处理

- 高峰倍率信息透传至可用渠道、支付计划、结算信息等 API,前端
  GroupBadge / GroupOptionItem / SubscriptionPlanCard / PaymentView /
  SubscriptionsView 统一展示高峰时段与倍率标签
- 修正计费术语:"文本倍率" → "token 倍率",明确高峰倍率同时作用于
  token 计费的图片 token,图片按次计费不受高峰影响
- 允许高峰倍率 multiplier=0,支持高峰时段免费策略
- 切换分组类型为 standard 时自动清除高峰倍率配置
- 长上下文计费与标准计费路径改用内部实现,移除冗余中间调用
- 前端高峰倍率相关控件文案改为 i18n
- 新增多组高峰倍率相关单元测试
This commit is contained in:
xueshiji committed 2026-07-01 17:46:12 +08:00
1 parent ebd83dc0f9
commit 1034f576d7
32 files changed
+686 -145

No files matched your search

+10 -8
View File
@@ -866,7 +866,7 @@
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span>启用高峰时段倍率</span>
<span>{{ t("admin.groups.peakRate.enable") }}</span>
</label>
</div>
<div
@@ -874,7 +874,7 @@
class="mb-4 grid grid-cols-3 gap-3"
>
<div>
<label class="input-label">高峰开始</label>
<label class="input-label">{{ t("admin.groups.peakRate.peakStart") }}</label>
<input
v-model="createForm.peak_start"
type="time"
@@ -882,7 +882,7 @@
/>
</div>
<div>
<label class="input-label">高峰结束</label>
<label class="input-label">{{ t("admin.groups.peakRate.peakEnd") }}</label>
<input
v-model="createForm.peak_end"
type="time"
@@ -890,7 +890,7 @@
/>
</div>
<div>
<label class="input-label">高峰倍率</label>
<label class="input-label">{{ t("admin.groups.peakRate.peakMultiplier") }}</label>
<input
v-model.number="createForm.peak_rate_multiplier"
type="number"
@@ -898,6 +898,7 @@
min="0"
class="input"
placeholder="1"
:title="t('admin.groups.peakRate.multiplierHint')"
/>
</div>
</div>
@@ -2204,7 +2205,7 @@
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span>启用高峰时段倍率</span>
<span>{{ t("admin.groups.peakRate.enable") }}</span>
</label>
</div>
<div
@@ -2212,7 +2213,7 @@
class="mb-4 grid grid-cols-3 gap-3"
>
<div>
<label class="input-label">高峰开始</label>
<label class="input-label">{{ t("admin.groups.peakRate.peakStart") }}</label>
<input
v-model="editForm.peak_start"
type="time"
@@ -2220,7 +2221,7 @@
/>
</div>
<div>
<label class="input-label">高峰结束</label>
<label class="input-label">{{ t("admin.groups.peakRate.peakEnd") }}</label>
<input
v-model="editForm.peak_end"
type="time"
@@ -2228,7 +2229,7 @@
/>
</div>
<div>
<label class="input-label">高峰倍率</label>
<label class="input-label">{{ t("admin.groups.peakRate.peakMultiplier") }}</label>
<input
v-model.number="editForm.peak_rate_multiplier"
type="number"
@@ -2236,6 +2237,7 @@
min="0"
class="input"
placeholder="1"
:title="t('admin.groups.peakRate.multiplierHint')"
/>
</div>
</div>
+24
View File
@@ -144,6 +144,10 @@
:subscription-type="row.group.subscription_type"
:rate-multiplier="row.group.rate_multiplier"
:user-rate-multiplier="userGroupRates[row.group.id]"
:peak-rate-enabled="row.group.peak_rate_enabled"
:peak-start="row.group.peak_start"
:peak-end="row.group.peak_end"
:peak-rate-multiplier="row.group.peak_rate_multiplier"
/>
<span v-else class="text-sm text-gray-400 dark:text-dark-500">{{
t('keys.noGroup')
@@ -454,6 +458,10 @@
:subscription-type="(option as unknown as GroupOption).subscriptionType"
:rate-multiplier="(option as unknown as GroupOption).rate"
:user-rate-multiplier="(option as unknown as GroupOption).userRate"
:peak-rate-enabled="(option as unknown as GroupOption).peakRateEnabled"
:peak-start="(option as unknown as GroupOption).peakStart"
:peak-end="(option as unknown as GroupOption).peakEnd"
:peak-rate-multiplier="(option as unknown as GroupOption).peakRateMultiplier"
/>
<span v-else class="text-gray-400">{{ t('keys.selectGroup') }}</span>
</template>
@@ -464,6 +472,10 @@
:subscription-type="(option as unknown as GroupOption).subscriptionType"
:rate-multiplier="(option as unknown as GroupOption).rate"
:user-rate-multiplier="(option as unknown as GroupOption).userRate"
:peak-rate-enabled="(option as unknown as GroupOption).peakRateEnabled"
:peak-start="(option as unknown as GroupOption).peakStart"
:peak-end="(option as unknown as GroupOption).peakEnd"
:peak-rate-multiplier="(option as unknown as GroupOption).peakRateMultiplier"
:description="(option as unknown as GroupOption).description"
:selected="selected"
/>
@@ -1059,6 +1071,10 @@
:subscription-type="option.subscriptionType"
:rate-multiplier="option.rate"
:user-rate-multiplier="option.userRate"
:peak-rate-enabled="option.peakRateEnabled"
:peak-start="option.peakStart"
:peak-end="option.peakEnd"
:peak-rate-multiplier="option.peakRateMultiplier"
:description="option.description"
:selected="
selectedKeyForGroup?.group_id === option.value ||
@@ -1123,6 +1139,10 @@ interface GroupOption {
description: string | null
rate: number
userRate: number | null
peakRateEnabled: boolean
peakStart: string
peakEnd: string
peakRateMultiplier: number
subscriptionType: SubscriptionType
platform: GroupPlatform
}
@@ -1351,6 +1371,10 @@ const groupOptions = computed(() =>
description: group.description,
rate: group.rate_multiplier,
userRate: userGroupRates.value[group.id] ?? null,
peakRateEnabled: group.peak_rate_enabled,
peakStart: group.peak_start,
peakEnd: group.peak_end,
peakRateMultiplier: group.peak_rate_multiplier,
subscriptionType: group.subscription_type,
platform: group.platform
}))
+25
View File
@@ -121,6 +121,12 @@
<span :class="['text-lg font-bold', planTextClass]">×{{ selectedPlan.rate_multiplier ?? 1 }}</span>
</div>
</div>
<div v-if="planHasPeakRate(selectedPlan)">
<span class="text-xs text-gray-400 dark:text-gray-500">{{ t('payment.planCard.peakRate') }}</span>
<div class="text-sm font-semibold text-amber-700 dark:text-amber-300">
{{ planPeakRateLabel(selectedPlan) }}
</div>
</div>
<div v-if="selectedPlan.daily_limit_usd != null">
<span class="text-xs text-gray-400 dark:text-gray-500">{{ t('payment.planCard.dailyLimit') }}</span>
<div class="text-lg font-semibold text-gray-800 dark:text-gray-200">${{ selectedPlan.daily_limit_usd }}</div>
@@ -194,6 +200,7 @@
</div>
<div class="flex flex-wrap gap-x-3 text-[11px] text-gray-400 dark:text-gray-500">
<span>{{ t('payment.planCard.rate') }}: ×{{ sub.group?.rate_multiplier ?? 1 }}</span>
<span v-if="subscriptionHasPeakRate(sub)">{{ t('payment.planCard.peakRate') }}: {{ subscriptionPeakRateLabel(sub) }}</span>
<span v-if="sub.group?.daily_limit_usd == null && sub.group?.weekly_limit_usd == null && sub.group?.monthly_limit_usd == null">{{ t('payment.planCard.quota') }}: {{ t('payment.planCard.unlimited') }}</span>
<span v-if="sub.expires_at">{{ t('userSubscriptions.daysRemaining', { days: getDaysRemaining(sub.expires_at) }) }}</span>
<span v-else>{{ t('userSubscriptions.noExpiration') }}</span>
@@ -297,6 +304,16 @@ function getDaysRemaining(expiresAt: string): number {
return Math.max(0, Math.ceil(diff / (1000 * 60 * 60 * 24)))
}
function subscriptionHasPeakRate(sub: { group?: { peak_rate_enabled?: boolean; peak_start?: string; peak_end?: string } | null }): boolean {
const group = sub.group
return Boolean(group?.peak_rate_enabled && group.peak_start && group.peak_end)
}
function subscriptionPeakRateLabel(sub: { group?: { peak_start?: string; peak_end?: string; peak_rate_multiplier?: number } | null }): string {
const group = sub.group
return `${group?.peak_start}-${group?.peak_end} ×${group?.peak_rate_multiplier ?? 1}`
}
const loading = ref(true)
const submitting = ref(false)
const errorMessage = ref('')
@@ -695,6 +712,14 @@ const planValiditySuffix = computed(() => {
return `${selectedPlan.value.validity_days}${t('payment.days')}`
})
function planHasPeakRate(plan: SubscriptionPlan): boolean {
return Boolean(plan.peak_rate_enabled && plan.peak_start && plan.peak_end)
}
function planPeakRateLabel(plan: SubscriptionPlan): string {
return `${plan.peak_start}-${plan.peak_end} ×${plan.peak_rate_multiplier ?? 1}`
}
function selectPlan(plan: SubscriptionPlan) {
selectedPlan.value = plan
errorMessage.value = ''
@@ -49,6 +49,12 @@
<p v-if="subscription.group?.description" class="mt-0.5 text-xs text-gray-500 dark:text-dark-400">
{{ subscription.group.description }}
</p>
<div class="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-[11px] text-gray-400 dark:text-gray-500">
<span>{{ t('payment.planCard.rate') }}: ×{{ subscription.group?.rate_multiplier ?? 1 }}</span>
<span v-if="subscriptionHasPeakRate(subscription)" class="text-amber-700 dark:text-amber-300">
{{ t('payment.planCard.peakRate') }}: {{ subscriptionPeakRateLabel(subscription) }}
</span>
</div>
</div>
</div>
<div class="flex items-center gap-2">
@@ -271,6 +277,16 @@ const appStore = useAppStore()
const subscriptions = ref<UserSubscription[]>([])
const loading = ref(true)
function subscriptionHasPeakRate(subscription: UserSubscription): boolean {
const group = subscription.group
return Boolean(group?.peak_rate_enabled && group.peak_start && group.peak_end)
}
function subscriptionPeakRateLabel(subscription: UserSubscription): string {
const group = subscription.group
return `${group?.peak_start}-${group?.peak_end} ×${group?.peak_rate_multiplier ?? 1}`
}
async function loadSubscriptions() {
try {
loading.value = true