package cardflow import ( "math" "strings" "time" ) // 计量规则常量。 const ( // StaleGuardRatio 是重置日防旧值阈值:重置日读数超过比较基准的该倍数时按 0 计。 StaleGuardRatio = 0.9 // MinResetDay 与 MaxResetDay 是通道上游流量重置日的合法范围。 MinResetDay = 1 MaxResetDay = 28 // FallbackResetDayCUCC 是联通卡重置日查询失败时的兜底值。 FallbackResetDayCUCC = 27 // FallbackResetDayDefault 是非联通卡重置日查询失败时的兜底值。 FallbackResetDayDefault = 1 // carrierTypeCUCC 是联通运营商类型编码。 carrierTypeCUCC = "CUCC" ) // shanghaiLocation 是快照日期与周期边界的固定时区。 var shanghaiLocation = time.FixedZone("Asia/Shanghai", 8*60*60) // ShanghaiDate 返回观测时刻对应的上海日期(当天 0 点)。 func ShanghaiDate(t time.Time) time.Time { local := t.In(shanghaiLocation) return time.Date(local.Year(), local.Month(), local.Day(), 0, 0, 0, 0, shanghaiLocation) } // MonthStart 返回日期所在自然月 1 号(上海时区)。 func MonthStart(date time.Time) time.Time { local := date.In(shanghaiLocation) return time.Date(local.Year(), local.Month(), 1, 0, 0, 0, 0, shanghaiLocation) } // ResolveResetDay 返回可用的重置日;查询失败或取值非法时按运营商类型兜底,fallback 为 true 表示使用了兜底值。 func ResolveResetDay(queried int, queryOK bool, carrierType string) (resetDay int, fallback bool) { if queryOK && queried >= MinResetDay && queried <= MaxResetDay { return queried, false } if strings.EqualFold(strings.TrimSpace(carrierType), carrierTypeCUCC) { return FallbackResetDayCUCC, true } return FallbackResetDayDefault, true } // MonthContext 是换算与防旧值需要的已有快照取值,均为自然月累计(MB)。 type MonthContext struct { // ThisMonthBeforeResetMax 是本月 1 号~重置日前一天的最大快照。 ThisMonthBeforeResetMax float64 // PrevMonthMax 是上月最大快照。 PrevMonthMax float64 // PrevMonthBeforeResetMax 是上月 1 号~重置日前一天的最大快照。 PrevMonthBeforeResetMax float64 } // Conversion 是一次读数换算为自然月累计的结果。 type Conversion struct { // MonthUsedMB 是换算后的本月自然月累计。 MonthUsedMB float64 // StaleGuarded 为 true 表示本次读数在重置日触发了防旧值、按 0 参与换算。 StaleGuarded bool } // Convert 把运营商周期读数换算为自然月累计,并在重置日当天应用防旧值。 // // 重置日为 1 时运营商周期即自然月;重置日大于 1 时:日号 ≥ 重置日累加本月重置日前的最大快照, // 日号 < 重置日扣除上月重置日及以后的用量(上月最大 − 上月重置日前最大),结果小于 0 按 0。 func Convert(readingMB float64, day int, resetDay int, ctx MonthContext) Conversion { reading := readingMB guarded := isStale(readingMB, day, resetDay, ctx) if guarded { reading = 0 } var month float64 switch { case resetDay <= 1: month = reading case day >= resetDay: month = reading + ctx.ThisMonthBeforeResetMax default: month = reading - (ctx.PrevMonthMax - ctx.PrevMonthBeforeResetMax) } return Conversion{MonthUsedMB: nonNegative(round3(month)), StaleGuarded: guarded} } // isStale 判断重置日当天的读数是否仍接近上周期总量。 func isStale(readingMB float64, day int, resetDay int, ctx MonthContext) bool { if day != resetDay || readingMB <= 0 { return false } if resetDay <= 1 { return readingMB > ctx.PrevMonthMax*StaleGuardRatio } return ctx.ThisMonthBeforeResetMax > 0 && readingMB > ctx.ThisMonthBeforeResetMax*StaleGuardRatio } // MonthlyDelta 返回按月对账的待结算增量:本月最大快照 − 本月已结算合计,不大于 0 时返回 0。 func MonthlyDelta(monthMaxMB float64, settledSumMB float64) float64 { return nonNegative(round3(monthMaxMB - settledSumMB)) } // DailyUsage 返回某天用量:当天最大快照 − 本月此前最大快照,小于 0 按 0;本月第一条快照时 priorMax 传 0。 func DailyUsage(dayMaxMB float64, priorMaxMB float64) float64 { return nonNegative(round3(dayMaxMB - priorMaxMB)) } // ValidReading 判断运营商读数能否写入快照:必须为有限正数。 func ValidReading(readingMB float64) bool { return !math.IsNaN(readingMB) && !math.IsInf(readingMB, 0) && readingMB > 0 } func nonNegative(v float64) float64 { if v < 0 { return 0 } return v } // round3 按快照列精度(三位小数)取整,避免浮点误差产生极小的虚假增量。 func round3(v float64) float64 { return math.Round(v*1000) / 1000 }