mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-31 16:26:30 +00:00
fix(group): harden peak-rate config handling and label peak windows with server timezone
Follow-up fixes to #3569 based on code audit: - Expose server_timezone / server_utc_offset in public settings (and the __APP_CONFIG__ injection payload) and label every peak-window display with the server UTC offset, so users don't misread the billing window as browser-local time - Unify CreateGroup/UpdateGroup peak-config sanitization via a single NormalizePeakRateConfig chokepoint: non-subscription groups always get peak fields cleared; unparseable window strings and negative multipliers are scrubbed when peak is disabled - Replace hot-path time.Parse in PeakMultiplierAt with a manual HH:MM parser (accept set verified byte-for-byte identical to time.Parse("15:04") by exhaustive fuzzing) and reuse it in validation - Revert the zero-behavior CalculateCost indirection churn in billing_service/gateway_service introduced by #3569 - Remove dead GetGroupPlatformMap and the duplicate deref helper in the admin handler package - Share frontend peak formatting via utils/peak-rate.ts, unify the ×N label format, and move hardcoded Chinese tooltips to i18n keys
This commit is contained in:
@@ -178,6 +178,8 @@ import SupportedModelChip from './SupportedModelChip.vue'
|
||||
import type { UserAvailableChannel, UserAvailableGroup, UserChannelPlatformSection } from '@/api/channels'
|
||||
import type { GroupPlatform, SubscriptionType } from '@/types'
|
||||
import { platformBadgeClass } from '@/utils/platformColors'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { hasPeakRate as groupHasPeakRate, formatPeakRateWindow, serverTimezoneLabel } from '@/utils/peak-rate'
|
||||
|
||||
const props = defineProps<{
|
||||
columns: {
|
||||
@@ -211,15 +213,17 @@ function publicGroups(section: UserChannelPlatformSection): UserAvailableGroup[]
|
||||
return section.groups.filter((g) => !g.is_exclusive)
|
||||
}
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
function hasPeakRate(group: UserAvailableGroup): boolean {
|
||||
return Boolean(group.peak_rate_enabled && group.peak_start && group.peak_end)
|
||||
return groupHasPeakRate(group)
|
||||
}
|
||||
|
||||
function peakRateLabel(group: UserAvailableGroup): string {
|
||||
return `${group.peak_start}-${group.peak_end} ${group.peak_rate_multiplier}x`
|
||||
return formatPeakRateWindow(group, serverTimezoneLabel(appStore.cachedPublicSettings?.server_utc_offset))
|
||||
}
|
||||
|
||||
function peakRateTitle(group: UserAvailableGroup): string {
|
||||
return `高峰倍率:${group.peak_start}-${group.peak_end} ${group.peak_rate_multiplier}x;token 计费的图片 token 同样适用,图片按次计费不受高峰影响`
|
||||
return t('common.peakRateTooltip', { window: peakRateLabel(group) }) + t('common.peakRateImageNote')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { SubscriptionType, GroupPlatform } from '@/types'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { formatPeakRateWindow, serverTimezoneLabel } from '@/utils/peak-rate'
|
||||
import PlatformIcon from './PlatformIcon.vue'
|
||||
|
||||
interface Props {
|
||||
@@ -75,16 +77,26 @@ const hasCustomRate = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const hasPeakRate = computed(() => {
|
||||
return Boolean(props.showRate && props.peakRateEnabled && props.peakStart && props.peakEnd)
|
||||
})
|
||||
|
||||
const peakRateText = computed(() => {
|
||||
return `${props.peakStart}-${props.peakEnd} ×${props.peakRateMultiplier ?? 1}`
|
||||
return formatPeakRateWindow(
|
||||
{
|
||||
peak_rate_enabled: props.peakRateEnabled,
|
||||
peak_start: props.peakStart,
|
||||
peak_end: props.peakEnd,
|
||||
peak_rate_multiplier: props.peakRateMultiplier
|
||||
},
|
||||
serverTimezoneLabel(appStore.cachedPublicSettings?.server_utc_offset)
|
||||
)
|
||||
})
|
||||
|
||||
const peakRateTitle = computed(() => {
|
||||
return `高峰倍率:${peakRateText.value}`
|
||||
return t('common.peakRateTooltip', { window: peakRateText.value })
|
||||
})
|
||||
|
||||
// 是否显示右侧标签
|
||||
|
||||
@@ -63,6 +63,8 @@ import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import GroupBadge from './GroupBadge.vue'
|
||||
import type { SubscriptionType, GroupPlatform } from '@/types'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { formatPeakRateWindow, serverTimezoneLabel } from '@/utils/peak-rate'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -99,16 +101,26 @@ const hasCustomRate = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const hasPeakRate = computed(() => {
|
||||
return Boolean(props.peakRateEnabled && props.peakStart && props.peakEnd)
|
||||
})
|
||||
|
||||
const peakRateText = computed(() => {
|
||||
return `${props.peakStart}-${props.peakEnd} ×${props.peakRateMultiplier ?? 1}`
|
||||
return formatPeakRateWindow(
|
||||
{
|
||||
peak_rate_enabled: props.peakRateEnabled,
|
||||
peak_start: props.peakStart,
|
||||
peak_end: props.peakEnd,
|
||||
peak_rate_multiplier: props.peakRateMultiplier
|
||||
},
|
||||
serverTimezoneLabel(appStore.cachedPublicSettings?.server_utc_offset)
|
||||
)
|
||||
})
|
||||
|
||||
const peakRateTitle = computed(() => {
|
||||
return `高峰倍率:${peakRateText.value}`
|
||||
return t('common.peakRateTooltip', { window: peakRateText.value })
|
||||
})
|
||||
|
||||
// Rate pill color matches platform badge color
|
||||
|
||||
@@ -103,6 +103,8 @@ import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { SubscriptionPlan } from '@/types/payment'
|
||||
import type { UserSubscription } from '@/types'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { hasPeakRate as groupHasPeakRate, formatPeakRateWindow, serverTimezoneLabel } from '@/utils/peak-rate'
|
||||
import {
|
||||
platformAccentBarClass,
|
||||
platformBadgeLightClass,
|
||||
@@ -144,12 +146,12 @@ const rateDisplay = computed(() => {
|
||||
return `×${Number(rate.toPrecision(10))}`
|
||||
})
|
||||
|
||||
const hasPeakRate = computed(() => {
|
||||
return Boolean(props.plan.peak_rate_enabled && props.plan.peak_start && props.plan.peak_end)
|
||||
})
|
||||
const appStore = useAppStore()
|
||||
|
||||
const hasPeakRate = computed(() => groupHasPeakRate(props.plan))
|
||||
|
||||
const peakRateDisplay = computed(() => {
|
||||
return `${props.plan.peak_start}-${props.plan.peak_end} ×${props.plan.peak_rate_multiplier ?? 1}`
|
||||
return formatPeakRateWindow(props.plan, serverTimezoneLabel(appStore.cachedPublicSettings?.server_utc_offset))
|
||||
})
|
||||
|
||||
const MODEL_SCOPE_LABELS: Record<string, string> = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createPinia } from "pinia";
|
||||
import { createI18n } from "vue-i18n";
|
||||
import SubscriptionPlanCard from "../SubscriptionPlanCard.vue";
|
||||
|
||||
@@ -42,7 +43,7 @@ const mountPlanCard = (groupPlatform: string) =>
|
||||
is_active: true,
|
||||
},
|
||||
},
|
||||
global: { plugins: [i18n] },
|
||||
global: { plugins: [i18n, createPinia()] },
|
||||
});
|
||||
|
||||
describe("SubscriptionPlanCard", () => {
|
||||
|
||||
Reference in New Issue
Block a user