feat: 卡流量读数先过采集队列再入账,扣减按采集时刻确定套餐
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:
邹进
2026-10-29 12:18:47 +09:00
parent c33cc6f6c3
commit 9c3c258f16
55 changed files with 3142 additions and 881 deletions

View File

@@ -28,6 +28,7 @@ import (
refundchannelApp "github.com/break/junhong_cmp_fiber/internal/application/refundchannel"
walletApp "github.com/break/junhong_cmp_fiber/internal/application/wallet"
"github.com/break/junhong_cmp_fiber/internal/bootstrap"
"github.com/break/junhong_cmp_fiber/internal/domain/cardflow"
"github.com/break/junhong_cmp_fiber/internal/gateway"
approvalInfra "github.com/break/junhong_cmp_fiber/internal/infrastructure/approval"
assetAutoRenewalInfra "github.com/break/junhong_cmp_fiber/internal/infrastructure/assetautorenewal"
@@ -173,6 +174,7 @@ func runWorker(cfg *config.Config) {
if runsSingletonModules(cfg.Worker.Role) {
pollingInitializer = startPollingInitializer(ctx, runtime, appLogger)
startTrafficCollectionPartitionMaintainer(ctx, runtime, appLogger)
pollingScheduler = startPollingScheduler(ctx, runtime, pollingInitializer, appLogger)
asynqScheduler = startAsynqScheduler(cfg, runtime.redisAddr, appLogger)
}
@@ -858,6 +860,43 @@ func startOutboxRelay(ctx context.Context, runtime *workerRuntime, instanceName
appLogger.Info("Outbox Relay 已启动", zap.String("lease_owner", owner))
}
// startTrafficCollectionPartitionMaintainer 为 leader/all 维护卡流量采集记录月分区并扫描采集告警:
// 每 12 小时预建本月起 2 个月的分区、删除整月都早于保留期的分区;每天扫描一次前一天的采集告警。
func startTrafficCollectionPartitionMaintainer(ctx context.Context, runtime *workerRuntime, appLogger *zap.Logger) {
store := postgres.NewCardTrafficCollectionStore(runtime.db)
alerts := cardObservationInfra.NewTrafficAlertScanner(runtime.db, appLogger)
go func() {
ticker := time.NewTicker(time.Hour)
defer ticker.Stop()
var lastMaintained time.Time
var lastScannedDay string
for {
now := time.Now()
if now.Sub(lastMaintained) >= 12*time.Hour {
if err := store.MaintainPartitions(ctx, now, 2, constants.TrafficCollectionRetentionDays); err != nil && ctx.Err() == nil {
appLogger.Error("维护卡流量采集记录分区失败", zap.Error(err))
} else {
lastMaintained = now
}
}
// 每天扫描一次前一天(上海日期)的采集告警。
yesterday := cardflow.ShanghaiDate(now).AddDate(0, 0, -1)
if day := yesterday.Format("2006-01-02"); day != lastScannedDay {
if err := alerts.Scan(ctx, yesterday); err != nil && ctx.Err() == nil {
appLogger.Error("扫描卡流量采集告警失败", zap.Error(err))
} else {
lastScannedDay = day
}
}
select {
case <-ctx.Done():
return
case <-ticker.C:
}
}
}()
}
// startPollingInitializer 为 leader/all 启动渐进式初始化器,并保留配置变更重启逻辑。
func startPollingInitializer(ctx context.Context, runtime *workerRuntime, appLogger *zap.Logger) *polling.PollingInitializer {
pollingInitializer := polling.NewPollingInitializer(