All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m37s
7.1 KiB
7.1 KiB
实现任务清单
Fix 1:not_realname 连坐修复
1.1 新增 isDeviceScopeReason 函数
- 文件:
internal/service/iot_card/stop_resume_service.go - 位置:紧接
isPollingStopReason函数之后 - 内容:
// isDeviceScopeReason 判断停机原因是否应扩散到设备维度 // 套餐/流量是设备共享资源,需整体停机;实名是单卡属性,只停该卡 func isDeviceScopeReason(reason string) bool { return reason == constants.StopReasonNoPackage || reason == constants.StopReasonTrafficExhausted }
1.2 修改 EvaluateAndAct 在线分支的停机判断
- 文件:
internal/service/iot_card/stop_resume_service.go - 修改位置:
EvaluateAndAct函数的NetworkStatusOnline分支 - 原逻辑:
if !card.IsStandalone { deviceID, bound, lookupErr := s.getCardDeviceID(ctx, card) if lookupErr != nil { ... } else if bound { s.stopDeviceCards(ctx, deviceID, primaryReason) return nil } } return s.stopCardWithRetry(ctx, card, primaryReason) - 改为:
if !card.IsStandalone && isDeviceScopeReason(primaryReason) { deviceID, bound, lookupErr := s.getCardDeviceID(ctx, card) if lookupErr != nil { ... } else if bound { s.stopDeviceCards(ctx, deviceID, primaryReason) return nil } } return s.stopCardWithRetry(ctx, card, primaryReason) - 注意:仅在条件中增加
&& isDeviceScopeReason(primaryReason),其余代码不变
1.3 数据库验证
- 使用 PostgreSQL MCP 确认设备 862639076038233 下 card_id=5(未实名)触发停机时,card_id=2 和 card_id=4 不再被停机
Fix 2:实名逆转防抖
2.1 新增 Redis Key 常量
- 文件:
pkg/constants/redis.go - 新增:
// RedisPollingRealnameReversalCountKey 实名逆转连续计数器,防止上游单次误报触发停机 // TTL: 10分钟 func RedisPollingRealnameReversalCountKey(cardID uint) string { return fmt.Sprintf("polling:card:realname:reversal:%d", cardID) }
2.2 新增防抖阈值常量
- 文件:
pkg/constants/iot.go - 新增(放在实名状态常量附近):
// PollingRealnameReversalThreshold 实名逆转防抖阈值:连续 N 次检测到逆转才触发停机 PollingRealnameReversalThreshold = 3
2.3 修改 polling_realname_handler.go 逆转处理逻辑
- 文件:
internal/task/polling_realname_handler.go - 修改位置:
Handle函数中else if newStatus == constants.RealNameStatusNotVerified分支 - 原逻辑(约第 154-167 行):
} else if newStatus == constants.RealNameStatusNotVerified { h.base.logger.Warn("检测到实名逆转,立即触发停机评估", ...) if h.stopResumeSvc != nil { freshCard, _ := h.iotCardStore.GetByID(ctx, cardID) h.stopResumeSvc.EvaluateAndAct(ctx, freshCard) } } - 改为:
} else if newStatus == constants.RealNameStatusNotVerified { reversalKey := constants.RedisPollingRealnameReversalCountKey(cardID) count, incrErr := h.base.redis.Incr(ctx, reversalKey).Result() if incrErr == nil { h.base.redis.Expire(ctx, reversalKey, 10*time.Minute) } h.base.logger.Warn("检测到实名逆转", zap.Uint("card_id", cardID), zap.Int64("reversal_count", count), zap.Int("threshold", constants.PollingRealnameReversalThreshold)) if count >= int64(constants.PollingRealnameReversalThreshold) { h.base.redis.Del(ctx, reversalKey) h.base.logger.Warn("实名逆转达到阈值,触发停机评估", zap.Uint("card_id", cardID)) if h.stopResumeSvc != nil { freshCard, loadErr := h.iotCardStore.GetByID(ctx, cardID) if loadErr == nil { if evalErr := h.stopResumeSvc.EvaluateAndAct(ctx, freshCard); evalErr != nil { h.base.logger.Warn("实名逆转后触发停机评估失败", zap.Uint("card_id", cardID), zap.Error(evalErr)) } } } } } - 同时:在
isFirstRealname分支(0→1)中,复机成功前清除计数器:放在// 0→1 时清除逆转计数器 h.base.redis.Del(ctx, constants.RedisPollingRealnameReversalCountKey(cardID))triggerFirstRealnameActivation调用之前
2.4 确认 PollingBase 有 redis 字段可访问
- 检查
internal/task/polling_base.go(或同类文件),确认h.base.redis可用 - 若不可用,通过
PollingRealnameHandler直接持有redis *redis.Client字段,在NewPollingRealnameHandler中注入
Fix 3:carrier_stopped 停机原因
3.1 新增 StopReasonCarrierStopped 常量
- 文件:
pkg/constants/iot.go - 新增(放在现有 StopReason 常量组中):
StopReasonCarrierStopped = "carrier_stopped" // 停机原因:运营商/Gateway 侧主动停机
3.2 修改 polling_cardstatus_handler.go 补写 stop_reason
- 文件:
internal/task/polling_cardstatus_handler.go - 修改位置:第 97-103 行,
statusChanged判断块内 - 原逻辑:
fields := map[string]any{"last_card_status_check_at": now} if statusChanged { fields["network_status"] = newNetworkStatus } - 改为:
fields := map[string]any{"last_card_status_check_at": now} if statusChanged { fields["network_status"] = newNetworkStatus // 网关侧停机事件:补写 stop_reason,便于状态追踪和区分手动停机 if newNetworkStatus == constants.NetworkStatusOffline && card.StopReason == "" { fields["stop_reason"] = constants.StopReasonCarrierStopped } } - 注意:
card.StopReason == ""条件确保不覆盖已有停机原因(如not_realname)
3.3 确认 isPollingStopReason 不包含 carrier_stopped
- 文件:
internal/service/iot_card/stop_resume_service.go - 确认:
isPollingStopReason函数的 switch case 中不包含carrier_stopped - 无需修改(已符合预期),仅做代码 review 确认
验证
V1:验证 Fix 1(连坐修复)
-- 通过 PostgreSQL MCP 检查:card_id=5 停机时,card_id=2、card_id=4 应保持在线
SELECT id, iccid, real_name_status, network_status, stop_reason
FROM tb_iot_card
WHERE device_virtual_no = '862639076038233'
ORDER BY id;
预期:card_id=5 被停机,card_id=2 和 card_id=4 不受影响(或按各自状态独立判断)
V2:验证 Fix 2(防抖)
- 在 Redis 中观察
polling:card:realname:reversal:*key 的变化 - 确认单次 false 后 key 计数为 1 且不触发停机日志
- 确认 3 次 false 后出现"实名逆转达到阈值"日志并触发停机
V3:验证 Fix 3(carrier_stopped)
-- 检查状态为 carrier_stopped 的卡能被正确识别
SELECT id, iccid, network_status, stop_reason, stopped_at
FROM tb_iot_card
WHERE stop_reason = 'carrier_stopped';
预期:stop_reason='carrier_stopped' 的卡不会被轮询系统自动复机(需观察日志)