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
2026-07-02 16:11:07 +08:00
parent a632cb0050
commit 11a3da65c0
21 changed files with 190 additions and 79 deletions
+33
View File
@@ -0,0 +1,33 @@
/**
* 高峰时段倍率的共享展示逻辑。
*
* 高峰窗口由后端按服务器全局时区判定(Group.PeakMultiplierAt),
* 前端展示必须带上服务器时区标注(来自公共设置 server_utc_offset),
* 避免用户按浏览器本地时间误读计费窗口。
*/
export interface PeakRateFields {
peak_rate_enabled?: boolean
peak_start?: string
peak_end?: string
peak_rate_multiplier?: number
}
export function hasPeakRate(fields?: PeakRateFields | null): boolean {
return Boolean(fields?.peak_rate_enabled && fields.peak_start && fields.peak_end)
}
/** "+08:00" → "UTC+08:00";旧缓存无该字段时返回空串,调用方降级为不带时区标注 */
export function serverTimezoneLabel(utcOffset?: string | null): string {
return utcOffset ? `UTC${utcOffset}` : ''
}
/** "14:00-18:00 ×2 (UTC+08:00)",tzLabel 为空时省略括号部分 */
export function formatPeakRateWindow(
fields: PeakRateFields | null | undefined,
tzLabel?: string
): string {
if (!hasPeakRate(fields) || !fields) return ''
const base = `${fields.peak_start}-${fields.peak_end} ×${fields.peak_rate_multiplier ?? 1}`
return tzLabel ? `${base} (${tzLabel})` : base
}