feat: 卡流量读数先过采集队列再入账,扣减按采集时刻确定套餐
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m27s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m27s
- 每卡 Redis 采集队列(默认 N=3、跨天清空、回退容差 1 MB),攒满且不下降才写最后一个点 - 测试环境全量切换日快照计量,下线灰度名单、切换水位与卡表基线扣费;全部卡置待建起点 - 读数区分没给值与 0;有套餐的卡读数 0 也写快照与日用量 0 行;重置日比较基准为 0 照常防旧值 - 新增采集记录(按月分区保留 90 天)、人工冲正流水与后台接口、采集告警 - 扣减按采集时刻有效套餐查找,只对生效中套餐做状态迁移,查不到套餐按成功跳过 - 换货新卡(新设备下的卡)打待建起点;采集间隔上限只校验启用中的轮询配置 Change: add-card-traffic-queue-and-observed-deduction Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -32,28 +32,6 @@ type Config struct {
|
||||
Worker WorkerConfig `mapstructure:"worker"`
|
||||
WeCom WeComConfig `mapstructure:"wecom"`
|
||||
Approval ApprovalConfig `mapstructure:"approval"`
|
||||
TrafficCalc TrafficCalcConfig `mapstructure:"traffic_calc"`
|
||||
}
|
||||
|
||||
// 卡流量计量模式取值。
|
||||
const (
|
||||
TrafficCalcModeOff = "off"
|
||||
TrafficCalcModeGray = "gray"
|
||||
TrafficCalcModeFull = "full"
|
||||
)
|
||||
|
||||
// TrafficCalcConfig 卡流量计量(日快照算法)切换配置。
|
||||
// API 与全部 Worker 进程必须使用一致的取值,改动后先停掉全部进程再一起启动。
|
||||
type TrafficCalcConfig struct {
|
||||
// Mode 计量模式:off 全部走卡表基线算法;gray 只有名单内的卡走日快照算法;full 全部卡走日快照算法。
|
||||
// 环境变量:JUNHONG_TRAFFIC_CALC_MODE
|
||||
Mode string `mapstructure:"mode"`
|
||||
// Cards 灰度名单,逗号分隔的扣减载体:device:<设备ID> 或 iot_card:<卡ID>。
|
||||
// 环境变量:JUNHONG_TRAFFIC_CALC_CARDS
|
||||
Cards string `mapstructure:"cards"`
|
||||
// FlowSnapshotEnabled 未切换卡的日快照写入总开关,关闭后未切换卡停止写快照。
|
||||
// 环境变量:JUNHONG_TRAFFIC_FLOW_SNAPSHOT_ENABLED
|
||||
FlowSnapshotEnabled bool `mapstructure:"flow_snapshot_enabled"`
|
||||
}
|
||||
|
||||
// ServerConfig HTTP 服务器配置
|
||||
@@ -304,11 +282,6 @@ func (c *Config) ValidateRequired() error {
|
||||
}
|
||||
|
||||
func (c *Config) Validate() error {
|
||||
switch c.TrafficCalc.Mode {
|
||||
case TrafficCalcModeOff, TrafficCalcModeGray, TrafficCalcModeFull:
|
||||
default:
|
||||
return fmt.Errorf("invalid configuration: traffic_calc.mode: invalid mode (current value: %s, expected: off, gray, full)", c.TrafficCalc.Mode)
|
||||
}
|
||||
// 服务器验证
|
||||
if c.Server.Address == "" {
|
||||
return fmt.Errorf("invalid configuration: server.address: must be non-empty (current value: empty)")
|
||||
|
||||
@@ -168,12 +168,6 @@ approval:
|
||||
legacy_offline_recharge_pay_enabled: true
|
||||
|
||||
|
||||
# 卡流量计量(日快照算法)切换配置;API 与全部 Worker 必须一致,改动后先停全部进程再一起启动
|
||||
traffic_calc:
|
||||
mode: "off" # off / gray / full(环境变量:JUNHONG_TRAFFIC_CALC_MODE)
|
||||
cards: "" # 灰度名单,如 device:15276,iot_card:103154(环境变量:JUNHONG_TRAFFIC_CALC_CARDS)
|
||||
flow_snapshot_enabled: true # 未切换卡写日快照(环境变量:JUNHONG_TRAFFIC_FLOW_SNAPSHOT_ENABLED)
|
||||
|
||||
# 企业微信 Adapter 配置
|
||||
wecom:
|
||||
base_url: "https://qyapi.weixin.qq.com"
|
||||
|
||||
@@ -160,13 +160,9 @@ func bindEnvVariables(v *viper.Viper) {
|
||||
"wechat.payment.notify_url",
|
||||
"wechat.payment.http_debug",
|
||||
"wechat.payment.timeout",
|
||||
"traffic_calc.mode",
|
||||
"traffic_calc.cards",
|
||||
}
|
||||
|
||||
for _, key := range bindings {
|
||||
_ = v.BindEnv(key)
|
||||
}
|
||||
// 快照写入开关沿用方案约定的环境变量名,与配置键的默认映射不同,需要显式绑定。
|
||||
_ = v.BindEnv("traffic_calc.flow_snapshot_enabled", "JUNHONG_TRAFFIC_FLOW_SNAPSHOT_ENABLED")
|
||||
}
|
||||
|
||||
56
pkg/constants/card_traffic_queue.go
Normal file
56
pkg/constants/card_traffic_queue.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package constants
|
||||
|
||||
import "fmt"
|
||||
|
||||
// 卡流量采集队列系统配置(add-card-traffic-queue-and-observed-deduction)。
|
||||
const (
|
||||
// SystemConfigModuleTraffic 是卡流量采集配置模块。
|
||||
SystemConfigModuleTraffic = "traffic"
|
||||
// SystemConfigTrafficQueueSize 是采集队列长度 N。
|
||||
SystemConfigTrafficQueueSize = "traffic.queue.size"
|
||||
// SystemConfigTrafficQueueClearOnNewDay 是跨天是否清空队列。
|
||||
SystemConfigTrafficQueueClearOnNewDay = "traffic.queue.clear_on_new_day"
|
||||
// SystemConfigTrafficQueueMaxPointAgeHours 是队列里点的最长保留时长(小时),0 表示不限。
|
||||
SystemConfigTrafficQueueMaxPointAgeHours = "traffic.queue.max_point_age_hours"
|
||||
// SystemConfigTrafficQueueDropToleranceMB 是相邻两点回退不超过该值算持平(MB)。
|
||||
SystemConfigTrafficQueueDropToleranceMB = "traffic.queue.drop_tolerance_mb"
|
||||
|
||||
// TrafficQueueDefaultSize 等是采集队列配置默认值。
|
||||
TrafficQueueDefaultSize = 3
|
||||
TrafficQueueMinSize = 2
|
||||
TrafficQueueMaxSize = 10
|
||||
TrafficQueueDefaultMaxPointAgeHours = 72
|
||||
TrafficQueueDefaultDropToleranceMB = 1.0
|
||||
// TrafficQueueIntervalSpare 是采集间隔上限留的冗余次数:间隔 ≤ 窗口 ÷ (N + 冗余)。
|
||||
TrafficQueueIntervalSpare = 2
|
||||
// TrafficQueueUnlimitedTTLHours 是最长点龄为 0(不限)且跨天清空关闭时队列 key 的过期时长。
|
||||
TrafficQueueUnlimitedTTLHours = 24 * 7
|
||||
)
|
||||
|
||||
// 采集记录的读数结果。
|
||||
const (
|
||||
TrafficCollectionResultOK = "ok"
|
||||
TrafficCollectionResultFailed = "failed"
|
||||
TrafficCollectionResultRateLimited = "rate_limited"
|
||||
TrafficCollectionResultInvalidPayload = "invalid_payload"
|
||||
TrafficCollectionResultNoReading = "no_reading"
|
||||
TrafficCollectionResultInvalidReading = "invalid_reading"
|
||||
)
|
||||
|
||||
// 采集记录的写入判断。
|
||||
const (
|
||||
TrafficDecisionWritten = "written"
|
||||
TrafficDecisionBaseline = "baseline"
|
||||
TrafficDecisionQueueNotFull = "queue_not_full"
|
||||
TrafficDecisionSlopeNegative = "slope_negative"
|
||||
TrafficDecisionOutOfOrder = "out_of_order"
|
||||
TrafficDecisionSettledExisting = "settled_existing"
|
||||
)
|
||||
|
||||
// TrafficCollectionRetentionDays 是采集记录保留天数,过期按整月分区删除。
|
||||
const TrafficCollectionRetentionDays = 90
|
||||
|
||||
// RedisCardTrafficQueueKey 返回卡流量采集队列 key。
|
||||
func RedisCardTrafficQueueKey(cardID uint) string {
|
||||
return fmt.Sprintf("traffic:queue:%d", cardID)
|
||||
}
|
||||
Reference in New Issue
Block a user