Files
junhong_cmp_fiber/internal/polling/tier.go
邹进 98017ac053
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m37s
feat: 分层轮询、重置日前加密与激活前流量结算
- 轮询配置新增套餐状态、资产状态、重置日前窗口与窗口内流量间隔(迁移 248),间隔三态:正数按间隔、0 不检查、空继承
- 配置按 priority, id 排序合并;在库与已销售无套餐未匹配时告警不入队
- 新增优先级整体重排、匹配预览、立即重算到期时间(异步只提前)、分层统计与重置日前加密统计接口
- 已切换日快照计量的卡流量按观测时间归属,激活前用量只记已结算
- 订单购买与实名激活补齐资产级激活锁,激活前对已切换卡结算一次流量,失败只告警不阻塞,结算不触发通道阈值判定
2026-10-24 17:06:38 +09:00

169 lines
5.2 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 polling
import (
"context"
"sync"
"time"
"go.uber.org/zap"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
// TierFacts 是分层轮询匹配所需、卡表之外的业务事实。
type TierFacts struct {
PackageState string // 套餐状态,见 constants.PollingPackageState*
Known bool // 是否成功解析;解析失败时要求套餐状态的配置不匹配
}
type tierCacheEntry struct {
facts TierFacts
loadedAt time.Time
}
// TierResolver 计算卡的套餐状态(绑定设备的卡按设备、独立卡按卡,与停复机"是否有有效套餐"口径一致),
// 并读取运营商账户的流量重置日。结果在进程内短时缓存,层级最迟在缓存过期后的下一次执行时更新。
type TierResolver struct {
usage *postgres.PackageUsageStore
bindings *postgres.DeviceSimBindingStore
carriers *postgres.CarrierStore
logger *zap.Logger
mu sync.Mutex
cache map[uint]tierCacheEntry
resetDays map[uint]int
resetsAt time.Time
}
// NewTierResolver 创建分层事实解析器。
func NewTierResolver(usage *postgres.PackageUsageStore, bindings *postgres.DeviceSimBindingStore, carriers *postgres.CarrierStore, logger *zap.Logger) *TierResolver {
if logger == nil {
logger = zap.NewNop()
}
return &TierResolver{usage: usage, bindings: bindings, carriers: carriers, logger: logger, cache: map[uint]tierCacheEntry{}, resetDays: map[uint]int{}}
}
// Resolve 返回卡的分层事实;缓存未过期时直接返回。
func (r *TierResolver) Resolve(ctx context.Context, card *model.IotCard) TierFacts {
if r == nil || card == nil {
return TierFacts{}
}
if facts, ok := r.cached(card.ID); ok {
return facts
}
r.Prefill(ctx, []*model.IotCard{card})
facts, _ := r.cached(card.ID)
return facts
}
// Prefill 批量解析一组卡的分层事实并写入缓存,供全量初始化等批处理避免逐卡查库。
func (r *TierResolver) Prefill(ctx context.Context, cards []*model.IotCard) {
if r == nil || r.usage == nil || len(cards) == 0 {
return
}
var standaloneIDs, boundCardIDs []uint
for _, card := range cards {
if card == nil {
continue
}
if card.IsStandalone {
standaloneIDs = append(standaloneIDs, card.ID)
} else {
boundCardIDs = append(boundCardIDs, card.ID)
}
}
results := make(map[uint]TierFacts, len(cards))
if len(standaloneIDs) > 0 {
states, err := r.usage.PackageStatesByCarriers(ctx, constants.AssetTypeIotCard, standaloneIDs)
if err != nil {
r.logger.Warn("批量查询卡套餐状态失败,相关卡本次不按套餐状态匹配", zap.Error(err))
}
for _, id := range standaloneIDs {
if err == nil {
results[id] = TierFacts{PackageState: states[id], Known: true}
}
}
}
if len(boundCardIDs) > 0 && r.bindings != nil {
bindings, err := r.bindings.GetActiveBindingsByCardIDs(ctx, boundCardIDs)
if err != nil {
r.logger.Warn("批量查询卡绑定设备失败,相关卡本次不按套餐状态匹配", zap.Error(err))
} else {
deviceOf := make(map[uint]uint, len(bindings))
deviceIDs := make([]uint, 0, len(bindings))
for _, binding := range bindings {
deviceOf[binding.IotCardID] = binding.DeviceID
deviceIDs = append(deviceIDs, binding.DeviceID)
}
deviceStates, devErr := r.usage.PackageStatesByCarriers(ctx, constants.AssetTypeDevice, deviceIDs)
cardStates, cardErr := r.usage.PackageStatesByCarriers(ctx, constants.AssetTypeIotCard, boundCardIDs)
for _, id := range boundCardIDs {
// 有有效设备绑定按设备计算;绑定已失效的卡按卡自身计算(与 resolvePackageCarrier 口径一致)。
if deviceID, bound := deviceOf[id]; bound {
if devErr == nil {
results[id] = TierFacts{PackageState: deviceStates[deviceID], Known: true}
}
} else if cardErr == nil {
results[id] = TierFacts{PackageState: cardStates[id], Known: true}
}
}
}
}
now := time.Now()
r.mu.Lock()
for _, card := range cards {
if card == nil {
continue
}
r.cache[card.ID] = tierCacheEntry{facts: results[card.ID], loadedAt: now}
}
r.mu.Unlock()
}
// Invalidate 清除某卡的分层事实缓存(套餐变化后可调用,未调用时最迟缓存过期后更新)。
func (r *TierResolver) Invalidate(cardID uint) {
if r == nil {
return
}
r.mu.Lock()
delete(r.cache, cardID)
r.mu.Unlock()
}
func (r *TierResolver) cached(cardID uint) (TierFacts, bool) {
r.mu.Lock()
defer r.mu.Unlock()
entry, ok := r.cache[cardID]
if !ok || time.Since(entry.loadedAt) > constants.PollingTierFactsCacheSeconds*time.Second {
return TierFacts{}, false
}
return entry.facts, true
}
// ResetDay 返回运营商账户的流量重置日(1~28),缓存 10 分钟;读取失败按 1 号。
func (r *TierResolver) ResetDay(ctx context.Context, carrierID uint) int {
if r == nil || r.carriers == nil {
return 1
}
r.mu.Lock()
if time.Since(r.resetsAt) > 10*time.Minute {
r.resetDays = map[uint]int{}
r.resetsAt = time.Now()
}
day, ok := r.resetDays[carrierID]
r.mu.Unlock()
if ok {
return day
}
day = r.carriers.GetDataResetDay(ctx, carrierID)
if day < 1 || day > 28 {
day = 1
}
r.mu.Lock()
r.resetDays[carrierID] = day
r.mu.Unlock()
return day
}