fix(通道流量阈值): AUG26-011 修复失败/未知结果收敛、锁定 carrier 缺失出路与审计回归
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 13m29s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 13m29s
This commit is contained in:
@@ -122,12 +122,36 @@ func (s *Service) ClaimResumeSubmission(ctx context.Context, lockID uint, now ti
|
||||
return claimed.RowsAffected == 1, nil
|
||||
}
|
||||
|
||||
// ScanExpiredLocks 扫描已跨期但仍持锁的锁行,供周期处理解锁与条件复机。
|
||||
// DueLockKind 描述一条仍在持锁的锁行的跨期判定结果。
|
||||
type DueLockKind string
|
||||
|
||||
const (
|
||||
// DueLockExpired 表示已跨期:锁行 period_start 早于该锁行自身运营商按 data_reset_day 算出的当前周期起点。
|
||||
DueLockExpired DueLockKind = "expired"
|
||||
// DueLockUnresolvable 表示周期归属不可判定:锁行引用的运营商已不存在(含软删)或重置日非法。
|
||||
DueLockUnresolvable DueLockKind = "unresolvable"
|
||||
)
|
||||
|
||||
// unresolvableCarrierReason 是周期归属不可判定时写入锁行的可安全原因。
|
||||
const unresolvableCarrierReason = "锁行引用的运营商已不存在或上游流量重置日非法,已按跨期解除通道锁并转人工核对"
|
||||
|
||||
// DueLock 是周期处理扫描到的一条待处理锁行。
|
||||
type DueLock struct {
|
||||
// Lock 是持锁锁行本身。
|
||||
Lock model.CarrierTrafficThresholdLock
|
||||
// Kind 是跨期判定结果:已跨期或周期归属不可判定。
|
||||
Kind DueLockKind
|
||||
// Reason 是周期归属不可判定时的可安全原因(Kind 为 DueLockExpired 时为空)。
|
||||
Reason string
|
||||
}
|
||||
|
||||
// ScanDueLocks 扫描需要周期处理的持锁锁行,供周期处理解锁与条件复机。
|
||||
//
|
||||
// 过期判断按锁行自身运营商的 data_reset_day 计算其当前周期起点,与锁行 period_start 不一致即已跨期,
|
||||
// 因此换运营商后的旧锁仍按其旧 carrier 的归属被正确识别。锁行引用的运营商已不存在时无法计算周期,
|
||||
// 本次不返回该锁(不猜测),交由周期处理标记异常转人工。
|
||||
func (s *Service) ScanExpiredLocks(ctx context.Context, now time.Time, limit int) ([]model.CarrierTrafficThresholdLock, error) {
|
||||
// 因此换运营商后的旧锁仍按其旧 carrier 的归属被正确识别。锁行引用的运营商已不存在或重置日非法时
|
||||
// 无法计算周期归属,这类行以 DueLockUnresolvable 返回:周期处理必须给出出路(按跨期语义解锁并转人工),
|
||||
// 绝不能让持锁卡因配置缺失而永久禁止复机。
|
||||
func (s *Service) ScanDueLocks(ctx context.Context, now time.Time, limit int) ([]DueLock, error) {
|
||||
if s == nil || s.db == nil {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -144,34 +168,40 @@ func (s *Service) ScanExpiredLocks(ctx context.Context, now time.Time, limit int
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
expired := make([]model.CarrierTrafficThresholdLock, 0, len(locks))
|
||||
due := make([]DueLock, 0, len(locks))
|
||||
for index := range locks {
|
||||
resetDay, ok := resetDays[locks[index].CarrierID]
|
||||
lock := locks[index]
|
||||
resetDay, ok := resetDays[lock.CarrierID]
|
||||
if !ok {
|
||||
due = append(due, DueLock{Lock: lock, Kind: DueLockUnresolvable, Reason: unresolvableCarrierReason})
|
||||
continue
|
||||
}
|
||||
periodStart, err := domain.PeriodStart(now, resetDay)
|
||||
if err != nil {
|
||||
periodStart, periodErr := domain.PeriodStart(now, resetDay)
|
||||
if periodErr != nil {
|
||||
due = append(due, DueLock{Lock: lock, Kind: DueLockUnresolvable, Reason: unresolvableCarrierReason})
|
||||
continue
|
||||
}
|
||||
if locks[index].PeriodStart.Before(periodStart) {
|
||||
expired = append(expired, locks[index])
|
||||
if lock.PeriodStart.Before(periodStart) {
|
||||
due = append(due, DueLock{Lock: lock, Kind: DueLockExpired})
|
||||
}
|
||||
}
|
||||
return expired, nil
|
||||
return due, nil
|
||||
}
|
||||
|
||||
// ScanSubmittedLocks 扫描存在已提交子任务的锁行,供恢复扫描查询运营商状态回填。
|
||||
// ScanSubmittedLocks 扫描存在未决子任务的锁行,供恢复扫描查询运营商状态回填。
|
||||
//
|
||||
// 未决集合为 {submitted, unknown, failed}(domain.UnresolvedTaskStatuses):调用失败或结果未知的行
|
||||
// 仍可能已在运营商侧生效,因此必须继续用只读状态查询收敛,不得退出链路。
|
||||
// 已标记异常(转人工)的锁必须退出扫描,否则每次扫描都会重复查询同一笔无法收敛的结果。
|
||||
func (s *Service) ScanSubmittedLocks(ctx context.Context, limit int) ([]model.CarrierTrafficThresholdLock, error) {
|
||||
if s == nil || s.db == nil {
|
||||
return nil, nil
|
||||
}
|
||||
unresolved := domain.UnresolvedTaskStatuses()
|
||||
var locks []model.CarrierTrafficThresholdLock
|
||||
if err := s.db.WithContext(ctx).
|
||||
Where("anomaly_flag = ? AND (stop_status = ? OR resume_status = ?)",
|
||||
domain.AnomalyFlagNone, domain.TaskStatusSubmitted, domain.TaskStatusSubmitted).
|
||||
Where("anomaly_flag = ? AND (stop_status IN ? OR resume_status IN ?)",
|
||||
domain.AnomalyFlagNone, unresolved, unresolved).
|
||||
Order("id ASC").Limit(limit).Find(&locks).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "扫描待确认通道流量阈值任务失败")
|
||||
}
|
||||
@@ -238,10 +268,10 @@ func (s *Service) loadCard(ctx context.Context, cardID uint) (*model.IotCard, er
|
||||
return &card, nil
|
||||
}
|
||||
|
||||
// markTaskOutcome 按 expected 状态条件更新把子任务推进到终态(ENG-CONC-001)。
|
||||
// 返回 false 表示记录已被并发推进,调用方必须按幂等处理,不再重复执行外部动作。
|
||||
func (s *Service) markTaskOutcome(ctx context.Context, lockID uint, task lockTask, expected, result, integrationID, failureReason string) (bool, error) {
|
||||
if s == nil || s.db == nil || lockID == 0 {
|
||||
// markTaskOutcome 按 expected 状态集合条件更新把子任务推进到终态(ENG-CONC-001)。
|
||||
// 返回 false 表示记录已被并发推进或已处于终态,调用方必须按幂等处理,不再重复执行外部动作。
|
||||
func (s *Service) markTaskOutcome(ctx context.Context, lockID uint, task lockTask, expected []string, result, integrationID, failureReason string) (bool, error) {
|
||||
if s == nil || s.db == nil || lockID == 0 || len(expected) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
updates := map[string]any{task.statusColumn: result}
|
||||
@@ -252,7 +282,7 @@ func (s *Service) markTaskOutcome(ctx context.Context, lockID uint, task lockTas
|
||||
updates["failure_reason"] = safeFailureReason(failureReason)
|
||||
}
|
||||
result_ := s.db.WithContext(ctx).Model(&model.CarrierTrafficThresholdLock{}).
|
||||
Where("id = ? AND "+task.statusColumn+" = ?", lockID, expected).
|
||||
Where("id = ? AND "+task.statusColumn+" IN ?", lockID, expected).
|
||||
Updates(updates)
|
||||
if result_.Error != nil {
|
||||
return false, errors.Wrap(errors.CodeDatabaseError, result_.Error, "回填通道阈值子任务状态失败")
|
||||
@@ -261,8 +291,10 @@ func (s *Service) markTaskOutcome(ctx context.Context, lockID uint, task lockTas
|
||||
}
|
||||
|
||||
// markTaskConfirmed 由恢复扫描在运营商状态确认成功后把子任务回填为已确认。
|
||||
// expected 取未决集合 {submitted, unknown, failed}:失败与结果未知同样可能已在运营商侧生效,
|
||||
// 必须允许收敛为已确认;confirmed 不在集合内,因此确认只会写入一次。
|
||||
func (s *Service) markTaskConfirmed(ctx context.Context, lockID uint, task lockTask, integrationID string) (bool, error) {
|
||||
return s.markTaskOutcome(ctx, lockID, task, domain.TaskStatusSubmitted, domain.TaskStatusConfirmed, integrationID, "")
|
||||
return s.markTaskOutcome(ctx, lockID, task, domain.UnresolvedTaskStatuses(), domain.TaskStatusConfirmed, integrationID, "")
|
||||
}
|
||||
|
||||
// markAnomaly 把锁标记为需人工核对并退出自动扫描。
|
||||
|
||||
Reference in New Issue
Block a user