- 请求检查器按场景配置(tb_polling_check_scene,缓存 30 秒)把卡的检查类型设为立即到期放入账户优先队列,同卡合并、冷却期内不请求、按场景追加延迟确认;设备按有效绑定卡展开 - 业务事件仍在原事务中写 Outbox(提交安全),carrier 模式下 Outbox 消费者把订单支付、套餐激活、自动续费购包、停复机成功事件转为请求检查,不再起三连查观测序列 - 读入口(C 端资产详情、后台资产实时状态、实名链接、设备操作)经模式接缝改为请求检查;设备信息同步保持原观测;OpenAPI 查询仍不触发 - legacy 模式下全部沿用原观测序列
162 lines
5.2 KiB
Go
162 lines
5.2 KiB
Go
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()
|
||
}
|