Files
junhong_cmp_fiber/internal/service/traffic/query_service.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

132 lines
4.4 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 traffic 提供流量数据查询服务
// 合并 Redis 缓冲(今日)和 DB(历史)两段数据源,对外提供统一的日粒度流量查询
package traffic
import (
"context"
"sort"
"strconv"
"time"
cardflow "github.com/break/junhong_cmp_fiber/internal/domain/cardflow"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/redis/go-redis/v9"
)
// DailyUsageItem 日粒度流量记录
type DailyUsageItem struct {
Date string `json:"date"` // 日期,格式 YYYY-MM-DD
UsageMB float64 `json:"usage_mb"` // 当天累计使用流量(MB)
}
// QueryService 流量查询服务
// 未切换日快照算法的卡合并 Redis(当天实时)和 DB(历史落盘)两段数据源;已切换的卡读日流量快照。
type QueryService struct {
redis *redis.Client
cardDailyUsageStore *postgres.CardDailyUsageStore
switched func(cardID uint) bool
flowDays *postgres.CardFlowDayStore
}
// WithFlowSnapshots 注入日快照数据源:switched 判定卡是否已切换日快照算法,已切换的卡改读快照。
func (s *QueryService) WithFlowSnapshots(switched func(cardID uint) bool, flowDays *postgres.CardFlowDayStore) *QueryService {
s.switched = switched
s.flowDays = flowDays
return s
}
// dailyUsageFromSnapshots 按快照计算每天用量:当天最大快照 − 本月此前最大快照,小于 0 按 0,跨月重新累计。
func (s *QueryService) dailyUsageFromSnapshots(ctx context.Context, cardID uint, startDate, endDate time.Time) ([]*DailyUsageItem, error) {
start := cardflow.ShanghaiDate(startDate)
end := cardflow.ShanghaiDate(endDate)
rows, err := s.flowDays.ListBetween(ctx, cardID, cardflow.MonthStart(start), end)
if err != nil {
return nil, err
}
items := make([]*DailyUsageItem, 0, len(rows))
var month string
var priorMax float64
for _, row := range rows {
date := row.FlowDate.Format("2006-01-02")
if rowMonth := date[:7]; rowMonth != month {
month, priorMax = rowMonth, 0
}
usage := cardflow.DailyUsage(row.MonthUsedMB, priorMax)
if row.MonthUsedMB > priorMax {
priorMax = row.MonthUsedMB
}
if date >= start.Format("2006-01-02") {
items = append(items, &DailyUsageItem{Date: date, UsageMB: usage})
}
}
return items, nil
}
// NewQueryService 创建流量查询服务实例
func NewQueryService(redisClient *redis.Client, cardDailyUsageStore *postgres.CardDailyUsageStore) *QueryService {
return &QueryService{
redis: redisClient,
cardDailyUsageStore: cardDailyUsageStore,
}
}
// GetDailyUsage 查询卡在指定日期范围内的日粒度流量
// 今天的数据从 Redis 缓冲读取(可能尚未落盘),历史数据从 DB 读取,合并后按日期 ASC 排序返回
func (s *QueryService) GetDailyUsage(ctx context.Context, cardID uint, startDate, endDate time.Time) ([]*DailyUsageItem, error) {
if s.switched != nil && s.flowDays != nil && s.switched(cardID) {
return s.dailyUsageFromSnapshots(ctx, cardID, startDate, endDate)
}
now := time.Now()
todayStr := now.Format("2006-01-02")
// 判断结束日期是否包含今天
endDateStr := endDate.Format("2006-01-02")
includeToday := endDateStr >= todayStr
// 历史部分:从 DB 查询(排除今天,因为今天的数据可能还在 Redis 中)
dbEndDate := endDate
if includeToday {
// DB 只查到昨天,今天从 Redis 取
dbEndDate = now.AddDate(0, 0, -1)
}
var items []*DailyUsageItem
// 只有 startDate <= dbEndDate 时才查 DB
if !startDate.After(dbEndDate) {
records, err := s.cardDailyUsageStore.ListByCardIDAndDateRange(ctx, cardID, startDate, dbEndDate)
if err != nil {
return nil, err
}
for _, r := range records {
items = append(items, &DailyUsageItem{
Date: r.Date.Format("2006-01-02"),
UsageMB: r.UsageMB,
})
}
}
// 今天部分:从 Redis 读取
if includeToday && !startDate.After(now) {
key := constants.RedisCardDailyTrafficKey(cardID, todayStr)
val, err := s.redis.Get(ctx, key).Result()
if err == nil && val != "" {
if usageMB, parseErr := strconv.ParseFloat(val, 64); parseErr == nil && usageMB > 0 {
items = append(items, &DailyUsageItem{
Date: todayStr,
UsageMB: usageMB,
})
}
}
// Redis key 不存在(redis.Nil)时忽略,表示今天暂无流量
}
// 按日期 ASC 排序
sort.Slice(items, func(i, j int) bool {
return items[i].Date < items[j].Date
})
return items, nil
}