Files
junhong_cmp_fiber/internal/domain/cardflow/rules.go
邹进 96771a204f
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
feat: 卡流量改为日快照计量并统一停复机规则
卡流量计量(按名单灰度,默认 off):
- 新增卡日快照与切换水位表(000241),按通道上游流量重置日换算自然月、重置日防旧值
- 已切换的卡按月对账产生增量,无生效中套餐只结算不发事件;第一次切换写水位
- 已切换卡读数有效即判定通道阈值,卡表本月用量用快照覆盖,日流量读快照
- 配置 JUNHONG_TRAFFIC_CALC_MODE/CARDS、JUNHONG_TRAFFIC_FLOW_SNAPSHOT_ENABLED

停复机规则(总开关 JUNHONG_STOP_RESUME_UNIFIED_RULE_ENABLED,默认关闭):
- 复机不看停机原因,持通道阈值锁仍拒绝;暂停轮询的卡不自动停复机
- 通道新增实名管控 realname_control_enabled(000242,默认 0),开启后不区分行业卡与实名策略
- 实名管控从 0 改 1 需确认,修改并入通道更新审计

OpenSpec Change: rebuild-card-traffic-metering-and-stop-resume
2026-10-20 17:49:26 +09:00

127 lines
4.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}