feat(通道流量阈值): AUG26-011 运营商通道流量阈值达量停机与周期复机
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 12m59s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 12m59s
This commit is contained in:
107
internal/application/carrierthreshold/evaluate.go
Normal file
107
internal/application/carrierthreshold/evaluate.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package carrierthreshold
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
domain "github.com/break/junhong_cmp_fiber/internal/domain/carrierthreshold"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// EvaluateInTx 在流量观测事务内判定通道阈值,达量时写周期锁并同事务写停机事件。
|
||||
//
|
||||
// 判定前提由调用方保证:只有「读数被接受」(ReadingAccepted)的观测才进入本方法,
|
||||
// 异常下降保护命中的观测不参与判定。重复达量观测由部分唯一索引的 23505 识别为
|
||||
// 「该周期已处理」并幂等跳过:不重复建锁、不重复写停机事件,该冲突与流量基线 CAS 的
|
||||
// CodeConflict 语义不同,绝不能混流。返回错误表示必须整体回滚(与既有流量事实同事务)。
|
||||
func (s *Service) EvaluateInTx(ctx context.Context, tx *gorm.DB, evaluation domain.Evaluation) error {
|
||||
if s == nil || s.db == nil || s.repository == nil {
|
||||
return errors.New(errors.CodeInternalError, "通道流量阈值判定能力未完整配置")
|
||||
}
|
||||
if tx == nil {
|
||||
return errors.New(errors.CodeInvalidStatus, "通道流量阈值判定必须传入事务句柄")
|
||||
}
|
||||
if evaluation.CardID == 0 || evaluation.CarrierID == 0 {
|
||||
return nil
|
||||
}
|
||||
var carrier model.Carrier
|
||||
// 只取判定所需列:周期起点必须来自该运营商的 data_reset_day,漏取会让周期判定失去依据。
|
||||
if err := tx.WithContext(ctx).Select("id", "data_reset_day", "traffic_threshold_enabled", "traffic_threshold_value", "traffic_threshold_unit").
|
||||
Where("id = ?", evaluation.CarrierID).First(&carrier).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
// 卡引用的运营商已不存在:不判定、不建锁,保持与既有卡事实不一致的现状。
|
||||
return nil
|
||||
}
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询运营商通道流量阈值配置失败")
|
||||
}
|
||||
threshold := thresholdOf(carrier)
|
||||
if !threshold.Enabled {
|
||||
return nil
|
||||
}
|
||||
if !threshold.Valid() {
|
||||
// 配置半残(启用但无数值/单位,或单位未知)时绝不按 0 判定,跳过并留可观测日志。
|
||||
s.logger.Warn("运营商通道流量阈值配置不完整,跳过达量判定",
|
||||
zap.Uint("carrier_id", carrier.ID), zap.String("unit", threshold.Unit), zap.Float64("value", threshold.Value))
|
||||
return nil
|
||||
}
|
||||
reached, err := threshold.Reached(evaluation.ReadingMB)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !reached {
|
||||
return nil
|
||||
}
|
||||
periodStart, err := domain.PeriodStart(evaluation.ObservedAt, carrier.DataResetDay)
|
||||
if err != nil {
|
||||
s.logger.Warn("运营商上游流量重置日非法,跳过达量判定",
|
||||
zap.Uint("carrier_id", carrier.ID), zap.Int("data_reset_day", carrier.DataResetDay))
|
||||
return nil
|
||||
}
|
||||
lock := &model.CarrierTrafficThresholdLock{
|
||||
CarrierID: evaluation.CarrierID,
|
||||
CardID: evaluation.CardID,
|
||||
PeriodStart: periodStart,
|
||||
Status: domain.LockStatusLocked,
|
||||
StopStatus: domain.TaskStatusPending,
|
||||
ResumeStatus: domain.TaskStatusPending,
|
||||
}
|
||||
// 唯一冲突(该周期已处理)在 createLockInTx 内以保存点隔离:不重复建锁、不重复写停机事件。
|
||||
created, err := s.createLockInTx(ctx, tx, lock)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !created {
|
||||
s.logger.Info("该计费周期已存在通道阈值停机锁,跳过重复判定",
|
||||
zap.Uint("carrier_id", evaluation.CarrierID), zap.Uint("card_id", evaluation.CardID))
|
||||
return nil
|
||||
}
|
||||
if err := AppendStopRequested(ctx, tx, s.repository, lock); err != nil {
|
||||
return err
|
||||
}
|
||||
s.logger.Info("卡流量达到运营商通道阈值,已写周期锁与停机事件",
|
||||
zap.Uint("carrier_id", evaluation.CarrierID), zap.Uint("card_id", evaluation.CardID),
|
||||
zap.Uint("lock_id", lock.ID), zap.Float64("reading_mb", evaluation.ReadingMB),
|
||||
zap.Time("period_start", periodStart))
|
||||
return nil
|
||||
}
|
||||
|
||||
// thresholdOf 把运营商持久化列转换为领域阈值配置。
|
||||
func thresholdOf(carrier model.Carrier) domain.Threshold {
|
||||
threshold := domain.Threshold{
|
||||
Enabled: carrier.TrafficThresholdEnabled == 1,
|
||||
Unit: carrier.TrafficThresholdUnit,
|
||||
}
|
||||
if carrier.TrafficThresholdValue != nil {
|
||||
threshold.Value = *carrier.TrafficThresholdValue
|
||||
}
|
||||
return threshold
|
||||
}
|
||||
|
||||
// lockKeyValue 返回周期锁在 Outbox 事件中的稳定字符串标识。
|
||||
func lockKeyValue(lockID uint) string {
|
||||
return strconv.FormatUint(uint64(lockID), 10)
|
||||
}
|
||||
Reference in New Issue
Block a user