Files
junhong_cmp_fiber/internal/domain/cardobservation/network.go
邹进 d4fde99cc3
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m29s
fix: 停复机成功后上游状态滞后导致暂停轮询的卡到期或退款漏停
- 停复机成功后保护窗口内(polling.stop_resume.status_grace_seconds,默认 300 秒)查卡状态返回操作前的状态视为上游滞后,不回写网络状态
- 套餐到期、退款场景不看库里的开停状态,资产无可用主套餐即停机,上游已停时按已是目标状态结束
- 迁移 000257 插入保护窗口配置

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 10:41:45 +09:00

122 lines
4.6 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 cardobservation
import (
"strings"
"time"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
// NetworkObservation 是领域层接收的标准 Gateway 网络观测。
type NetworkObservation struct {
CardID uint
GatewayStatus string
GatewayExtend string
GatewayIMEI string
Metadata ObservationMetadata
}
// CardNetworkSnapshot 是网络规则需要的最小卡快照。
type CardNetworkSnapshot struct {
CardID uint
NetworkStatus int
StopReason string
EnablePolling bool
// StoppedAt、ResumedAt 是本系统最近一次停机、复机成功回写的时间;CommandGrace 是停复机成功后的状态保护窗口。
StoppedAt *time.Time
ResumedAt *time.Time
CommandGrace time.Duration
}
// NetworkDecision 描述一次网络观测可安全持久化的最终值。
type NetworkDecision struct {
StatusKnown bool
StatusChanged bool
BeforeStatus int
AfterStatus int
GatewayExtend string
GatewayIMEI string
UpdateIMEI bool
StopReason string
StopReasonChanged bool
StopPolling bool
// StatusSuppressed 为 true 表示观测到的状态与保护窗口内刚成功的停复机相反,判定为上游状态滞后,不回写网络状态。
StatusSuppressed bool
}
// ApplyNetwork 应用 Gateway 状态映射、运营商停机原因和风险终止卡关闭轮询规则。
func ApplyNetwork(snapshot CardNetworkSnapshot, observation NetworkObservation) (NetworkDecision, error) {
if err := validateObservationMetadata(observation.CardID, observation.Metadata); err != nil {
return NetworkDecision{}, err
}
if snapshot.NetworkStatus != constants.NetworkStatusOffline && snapshot.NetworkStatus != constants.NetworkStatusOnline {
return NetworkDecision{}, errors.New(errors.CodeInvalidStatus, "卡网络状态无效")
}
status, known := MapGatewayNetworkStatus(observation.GatewayStatus, observation.GatewayExtend)
extend := strings.TrimSpace(observation.GatewayExtend)
imei := strings.TrimSpace(observation.GatewayIMEI)
decision := NetworkDecision{
StatusKnown: known, BeforeStatus: snapshot.NetworkStatus, AfterStatus: snapshot.NetworkStatus,
GatewayExtend: extend, GatewayIMEI: imei, UpdateIMEI: imei != "", StopReason: snapshot.StopReason,
StopPolling: snapshot.EnablePolling && ShouldStopPollingForRisk(extend),
}
if !known {
return decision, nil
}
if status != snapshot.NetworkStatus && withinCommandGrace(snapshot, observation.Metadata.ObservedAt) {
// 停复机成功后上游状态有延迟:保护窗口内观测到操作前的状态不回写,窗口过后的检查以上游为准。
decision.StatusSuppressed = true
return decision, nil
}
decision.AfterStatus = status
decision.StatusChanged = status != snapshot.NetworkStatus
if decision.StatusChanged && status == constants.NetworkStatusOffline && snapshot.StopReason == "" &&
strings.TrimSpace(observation.GatewayStatus) == constants.GatewayCardStatusStopped {
decision.StopReason = constants.StopReasonCarrierStopped
decision.StopReasonChanged = true
}
return decision, nil
}
// MapGatewayNetworkStatus 将 Gateway 状态稳定映射为本地网络状态。
func MapGatewayNetworkStatus(cardStatus, extend string) (int, bool) {
status := strings.TrimSpace(cardStatus)
ext := strings.TrimSpace(extend)
switch status {
case constants.GatewayCardStatusNormal:
return constants.NetworkStatusOnline, true
case constants.GatewayCardStatusStopped, constants.GatewayCardStatusReady:
return constants.NetworkStatusOffline, true
}
if ext == constants.GatewayCardExtendPendingActivation {
return constants.NetworkStatusOffline, true
}
return constants.NetworkStatusOffline, false
}
// ShouldStopPollingForRisk 判断卡是否命中运营商风险终止状态(风险停机、已销户)。
// 不论是否绑定设备都关闭该卡自身轮询,不级联设备与设备下其他卡。
func ShouldStopPollingForRisk(extend string) bool {
return extend == constants.GatewayCardExtendRiskStop || extend == constants.GatewayCardExtendCancelled
}
// withinCommandGrace 判断观测时间是否落在本系统最近一次停复机成功后的保护窗口内:
// 库里为停机时看停机时间,为开机时看复机时间。
func withinCommandGrace(snapshot CardNetworkSnapshot, observedAt time.Time) bool {
if snapshot.CommandGrace <= 0 {
return false
}
last := snapshot.ResumedAt
if snapshot.NetworkStatus == constants.NetworkStatusOffline {
last = snapshot.StoppedAt
}
if last == nil || last.IsZero() {
return false
}
if observedAt.IsZero() {
observedAt = time.Now()
}
return observedAt.Sub(*last) < snapshot.CommandGrace
}