Files
junhong_cmp_fiber/internal/polling/check_requester.go
邹进 0d966249b8 feat: 业务事件与读入口在按运营商账户调度时改为请求检查
- 请求检查器按场景配置(tb_polling_check_scene,缓存 30 秒)把卡的检查类型设为立即到期放入账户优先队列,同卡合并、冷却期内不请求、按场景追加延迟确认;设备按有效绑定卡展开
- 业务事件仍在原事务中写 Outbox(提交安全),carrier 模式下 Outbox 消费者把订单支付、套餐激活、自动续费购包、停复机成功事件转为请求检查,不再起三连查观测序列
- 读入口(C 端资产详情、后台资产实时状态、实名链接、设备操作)经模式接缝改为请求检查;设备信息同步保持原观测;OpenAPI 查询仍不触发
- legacy 模式下全部沿用原观测序列
2026-10-25 18:49:36 +09:00

162 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"
"encoding/json"
"strings"
"sync"
"time"
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/polling/carrierqueue"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
// sceneCacheTTL 是请求检查场景配置的进程内缓存时长。
const sceneCacheTTL = 30 * time.Second
// SceneConfig 是一个请求检查场景的生效配置。
type SceneConfig struct {
Scene string
CheckTypes []string
ConfirmDelays []time.Duration
Cooldown time.Duration
}
// CheckRequester 按场景配置为卡请求检查(业务事件与读入口):把对应检查类型设为立即到期放入账户优先队列,
// 同卡多次请求合并、冷却期内不请求、按场景追加延迟确认。只在按运营商账户调度时生效。
type CheckRequester struct {
scheduling *CarrierScheduling
db *gorm.DB
logger *zap.Logger
mu sync.Mutex
scenes map[string]SceneConfig
loadedAt time.Time
}
// NewCheckRequester 创建请求检查器。
func NewCheckRequester(scheduling *CarrierScheduling, db *gorm.DB, logger *zap.Logger) *CheckRequester {
if logger == nil {
logger = zap.NewNop()
}
return &CheckRequester{scheduling: scheduling, db: db, logger: logger}
}
// Active 判断当前是否按运营商账户调度(请求检查生效)。
func (r *CheckRequester) Active(ctx context.Context) bool {
return r != nil && r.scheduling.Active(ctx)
}
// RequestCards 为一组卡按场景请求检查;expected 为延迟确认的期望结果(online / offline,可空)。
// 失败只记日志,不影响业务。
func (r *CheckRequester) RequestCards(ctx context.Context, scene string, cardIDs []uint, expected string) {
if r == nil || len(cardIDs) == 0 {
return
}
config, ok := r.sceneConfig(ctx, scene)
if !ok || len(config.CheckTypes) == 0 {
return
}
carriers, err := r.cardCarriers(ctx, cardIDs)
if err != nil {
r.logger.Warn("请求检查:查询卡所属运营商账户失败", zap.String("scene", scene), zap.Error(err))
return
}
store := r.scheduling.Store()
for _, cardID := range cardIDs {
carrierID, found := carriers[cardID]
if !found {
continue
}
if _, err := store.RequestCheck(ctx, carrierqueue.CheckRequest{
CardID: cardID, CarrierID: carrierID, CheckTypes: config.CheckTypes, Cooldown: config.Cooldown,
ConfirmDelays: config.ConfirmDelays, Expected: expected,
}); err != nil {
r.logger.Warn("请求检查写入账户队列失败", zap.Uint("card_id", cardID), zap.String("scene", scene), zap.Error(err))
}
}
}
// RequestDevice 为设备下全部有效绑定卡按场景请求检查。
func (r *CheckRequester) RequestDevice(ctx context.Context, scene string, deviceID uint) {
if r == nil || deviceID == 0 {
return
}
var cardIDs []uint
if err := r.db.WithContext(ctx).Model(&model.DeviceSimBinding{}).
Where("device_id = ? AND bind_status = ?", deviceID, constants.BindStatusBound).Pluck("iot_card_id", &cardIDs).Error; err != nil {
r.logger.Warn("请求检查:查询设备绑定卡失败", zap.Uint("device_id", deviceID), zap.Error(err))
return
}
r.RequestCards(ctx, scene, cardIDs, "")
}
func (r *CheckRequester) cardCarriers(ctx context.Context, cardIDs []uint) (map[uint]uint, error) {
var cards []model.IotCard
if err := r.db.WithContext(ctx).Select("id", "carrier_id").Where("id IN ?", cardIDs).Find(&cards).Error; err != nil {
return nil, err
}
result := make(map[uint]uint, len(cards))
for _, card := range cards {
result[card.ID] = card.CarrierID
}
return result, nil
}
// sceneConfig 读取场景配置(缓存);场景未预置时不请求。
func (r *CheckRequester) sceneConfig(ctx context.Context, scene string) (SceneConfig, bool) {
r.mu.Lock()
defer r.mu.Unlock()
if r.scenes == nil || time.Since(r.loadedAt) >= sceneCacheTTL {
var rows []model.PollingCheckScene
if err := r.db.WithContext(ctx).Find(&rows).Error; err != nil {
r.logger.Warn("读取请求检查场景失败,沿用上一次配置", zap.Error(err))
} else {
scenes := make(map[string]SceneConfig, len(rows))
for _, row := range rows {
scenes[row.Scene] = ParseSceneConfig(row)
}
r.scenes, r.loadedAt = scenes, time.Now()
}
}
config, ok := r.scenes[scene]
return config, ok
}
// ParseSceneConfig 解析场景配置行。
func ParseSceneConfig(row model.PollingCheckScene) SceneConfig {
config := SceneConfig{Scene: row.Scene}
var types []string
_ = json.Unmarshal(row.CheckTypes, &types)
for _, checkType := range types {
if constants.CheckTypeCallsGateway(strings.TrimSpace(checkType)) {
config.CheckTypes = append(config.CheckTypes, strings.TrimSpace(checkType))
}
}
var delays []int
_ = json.Unmarshal(row.ConfirmDelays, &delays)
for _, seconds := range delays {
if seconds > 0 {
config.ConfirmDelays = append(config.ConfirmDelays, time.Duration(seconds)*time.Second)
}
}
if row.Cooldown != nil && *row.Cooldown > 0 {
config.Cooldown = time.Duration(*row.Cooldown) * time.Second
}
return config
}
// InvalidateScenes 使场景缓存立即失效(后台修改后调用)。
func (r *CheckRequester) InvalidateScenes() {
if r == nil {
return
}
r.mu.Lock()
r.loadedAt = time.Time{}
r.mu.Unlock()
}