Files
junhong_cmp_fiber/internal/application/carrierthreshold/service.go
break 59b3df868a
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 12m59s
feat(通道流量阈值): AUG26-011 运营商通道流量阈值达量停机与周期复机
2026-09-16 15:54:49 +08:00

108 lines
4.7 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 carrierthreshold 编排运营商通道流量阈值的达量判定与周期锁事实。
//
// 本包拥有 tb_carrier_traffic_threshold_lock 的全部读写:达量判定在流量观测事务内写入周期锁
// 与可靠停机事件,消费者以提交认领字段取得至多一次的外部调用权,周期处理与恢复扫描只查询锁行。
// 本包不调用任何运营商接口,也不在数据库事务内发起外部 I/O停复机执行通过 CardCommander
// 端口复用既有停复机单一事实源。
package carrierthreshold
import (
"context"
"time"
"go.uber.org/zap"
"gorm.io/gorm"
domain "github.com/break/junhong_cmp_fiber/internal/domain/carrierthreshold"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
// CardCommander 是通道阈值停复机对卡与运营商接口的能力边界。
//
// 实现必须复用既有停复机单一事实源重试、Integration Log、统一审计、卡状态观测序列
// 既有套餐/流量/实名/风险判定;本包绝不复制这些规则,也绝不直接调用运营商接口。
type CardCommander interface {
// StopCardForThreshold 执行通道阈值达量停机,成功时写回卡停机状态与停因。
StopCardForThreshold(ctx context.Context, cardID uint) (domain.CommandOutcome, error)
// ResumeCardForThreshold 执行新周期复机,成功时写回卡在线状态(只清除通道阈值停因)。
ResumeCardForThreshold(ctx context.Context, cardID uint) (domain.CommandOutcome, error)
// ResumeReady 判断解锁后的卡是否满足自动复机条件;不满足时返回可安全记录的原因。
ResumeReady(ctx context.Context, cardID uint) (ready bool, reason string, err error)
// CardNetworkStatus 只查询运营商卡状态并映射为本地网络状态known 为 false 表示状态不可判定。
CardNetworkStatus(ctx context.Context, cardID uint) (status int, known bool, integrationID string, err error)
// ConfirmCardState 按已确认的运营商结果补写卡状态,覆盖 Gateway 成功但 DB 更新失败的场景。
ConfirmCardState(ctx context.Context, cardID uint, offline bool) error
}
// Service 执行通道阈值达量判定并维护周期锁事实。
type Service struct {
db *gorm.DB
// repository 是公共 Outbox 仓储达量停机事件与周期复机事件必须与锁事实在同一事务写入ENG-OUTBOX-001
repository *outbox.Repository
logger *zap.Logger
// commander 是停复机执行端口;未注入时消费者与 cron 拒绝执行外部调用。
commander CardCommander
now func() time.Time
}
// NewService 创建通道阈值用例repository 决定达量判定能否写出可靠停复机事件。
func NewService(db *gorm.DB, repository *outbox.Repository) *Service {
return &Service{db: db, repository: repository, logger: zap.NewNop(), now: time.Now}
}
// SetLogger 注入通道阈值运行日志。
func (s *Service) SetLogger(logger *zap.Logger) *Service {
if s == nil {
return s
}
if logger == nil {
s.logger = zap.NewNop()
return s
}
s.logger = logger
return s
}
// SetCommander 注入停复机执行端口(消费者与周期处理必需)。
func (s *Service) SetCommander(commander CardCommander) *Service {
if s == nil {
return s
}
s.commander = commander
return s
}
// ChannelThresholdLocked 判断该卡当前计费周期是否持有通道阈值停机锁。
//
// 供复机入口前置拒绝复用:持锁即拒绝,判定口径与锁生效口径完全一致(按锁行自身运营商的
// data_reset_day 判断周期归属),不另立一套判定。
func (s *Service) ChannelThresholdLocked(ctx context.Context, cardID uint) (bool, error) {
lock, err := s.ActiveLock(ctx, cardID, s.now())
if err != nil {
return false, err
}
return lock != nil, nil
}
// carrierResetDays 按运营商 ID 集合显式查询上游流量重置日ENG-MODEL-001不使用关联标签
// 返回结果只包含仍然存在的运营商,缺失 ID 由调用方按各自语义处理。
func (s *Service) carrierResetDays(ctx context.Context, carrierIDs []uint) (map[uint]int, error) {
resetDays := make(map[uint]int, len(carrierIDs))
if len(carrierIDs) == 0 {
return resetDays, nil
}
var carriers []model.Carrier
if err := s.db.WithContext(ctx).
Select("id", "data_reset_day").
Where("id IN ?", carrierIDs).
Find(&carriers).Error; err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询运营商上游流量重置日失败")
}
for index := range carriers {
resetDays[carriers[index].ID] = carriers[index].DataResetDay
}
return resetDays, nil
}