feat: PollingRealnameHandler 新增设备级套餐实名激活联动

- 结构体新增 deviceSimBindingStore 字段,构造函数参数列表末尾追加
- 新增 triggerDeviceRealnameActivation:卡首次实名(0→1)时查询所属设备,
  若有绑定则提交 carrier_type=device 的 TaskTypePackageFirstActivation 任务
- 独立卡(无绑定设备)静默跳过,任务提交失败仅记录 Warn 日志不阻断流程
- registerPollingHandlers 传入 h.workerResult.Stores.DeviceSimBinding
- 修复「购买后某张卡实名」场景下设备级套餐永不激活的缺陷

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-04-18 09:30:50 +08:00
parent 8e61cb1a54
commit 28ac58ea18
2 changed files with 54 additions and 11 deletions

View File

@@ -18,11 +18,12 @@ import (
// 职责:调 Gateway 查实名状态 → 写 DB → 触发首次实名激活任务
// 不做:停复机决策(实名状态 0→1 时通过 EvaluateAndAct 触发 not_realname 复机)
type PollingRealnameHandler struct {
base *PollingBase
gateway *gateway.Client
iotCardStore *postgres.IotCardStore
asynqClient *asynq.Client
stopResumeSvc iot_card_svc.StopResumeServiceInterface
base *PollingBase
gateway *gateway.Client
iotCardStore *postgres.IotCardStore
asynqClient *asynq.Client
stopResumeSvc iot_card_svc.StopResumeServiceInterface
deviceSimBindingStore *postgres.DeviceSimBindingStore
}
// NewPollingRealnameHandler 创建实名检查任务处理器
@@ -32,13 +33,15 @@ func NewPollingRealnameHandler(
iotCardStore *postgres.IotCardStore,
asynqClient *asynq.Client,
stopResumeSvc iot_card_svc.StopResumeServiceInterface,
deviceSimBindingStore *postgres.DeviceSimBindingStore,
) *PollingRealnameHandler {
return &PollingRealnameHandler{
base: base,
gateway: gw,
iotCardStore: iotCardStore,
asynqClient: asynqClient,
stopResumeSvc: stopResumeSvc,
base: base,
gateway: gw,
iotCardStore: iotCardStore,
asynqClient: asynqClient,
stopResumeSvc: stopResumeSvc,
deviceSimBindingStore: deviceSimBindingStore,
}
}
@@ -138,6 +141,7 @@ func (h *PollingRealnameHandler) Handle(ctx context.Context, t *asynq.Task) erro
if isFirstRealname {
// 0→1首次实名触发套餐激活并立即复机评估不等待下一个 carddata/package 周期)
h.triggerFirstRealnameActivation(ctx, cardID)
h.triggerDeviceRealnameActivation(ctx, cardID)
if h.stopResumeSvc != nil {
freshCard, loadErr := h.iotCardStore.GetByID(ctx, cardID)
if loadErr == nil {
@@ -192,3 +196,42 @@ func (h *PollingRealnameHandler) triggerFirstRealnameActivation(ctx context.Cont
h.base.logger.Warn("提交首次实名激活任务失败", zap.Uint("card_id", cardID), zap.Error(err))
}
}
// triggerDeviceRealnameActivation 卡首次实名后,触发其所属设备的设备级套餐激活任务
// 若卡不属于任何设备(独立卡),静默跳过
func (h *PollingRealnameHandler) triggerDeviceRealnameActivation(ctx context.Context, cardID uint) {
if h.asynqClient == nil || h.deviceSimBindingStore == nil {
return
}
binding, err := h.deviceSimBindingStore.GetActiveBindingByCardID(ctx, cardID)
if err != nil {
if isNotFound(err) {
return
}
h.base.logger.Warn("查询卡绑定设备失败,跳过设备级套餐激活",
zap.Uint("card_id", cardID), zap.Error(err))
return
}
deviceID := binding.DeviceID
payload := map[string]any{
"carrier_type": "device",
"carrier_id": deviceID,
"activation_type": "realname",
"timestamp": time.Now().Unix(),
}
payloadBytes, err := sonic.Marshal(payload)
if err != nil {
h.base.logger.Warn("序列化设备实名激活任务载荷失败",
zap.Uint("card_id", cardID), zap.Uint("device_id", deviceID), zap.Error(err))
return
}
task := asynq.NewTask(constants.TaskTypePackageFirstActivation, payloadBytes,
asynq.MaxRetry(3),
asynq.Timeout(30*time.Second),
asynq.Queue(constants.QueueDefault),
)
if _, err := h.asynqClient.EnqueueContext(ctx, task); err != nil {
h.base.logger.Warn("提交设备实名激活任务失败",
zap.Uint("card_id", cardID), zap.Uint("device_id", deviceID), zap.Error(err))
}
}

View File

@@ -156,7 +156,7 @@ func (h *Handler) registerCommissionCalculationHandler() {
func (h *Handler) registerPollingHandlers() {
realnameHandler := task.NewPollingRealnameHandler(
h.pollingBase, h.gatewayClient, h.workerResult.Stores.IotCard,
h.asynqClient, h.pollingStopResumeSvc)
h.asynqClient, h.pollingStopResumeSvc, h.workerResult.Stores.DeviceSimBinding)
carrierStore := postgres.NewCarrierStore(h.db)
carddataHandler := task.NewPollingCarddataHandler(
h.pollingBase, h.gatewayClient, h.workerResult.Stores.IotCard,