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