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:
shaw committed 2026-07-02 16:11:07 +08:00
1 parent a632cb0050
commit 11a3da65c0
21 files changed
+190 -79

No files matched your search

+14 -2
View File
@@ -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