feat(通道流量阈值): AUG26-011 运营商通道流量阈值达量停机与周期复机
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 12m59s

This commit is contained in:
2026-09-16 15:54:49 +08:00
parent 41722760b1
commit 59b3df868a
36 changed files with 2431 additions and 93 deletions

View File

@@ -0,0 +1,199 @@
package carrierthreshold
import (
"context"
"go.uber.org/zap"
domain "github.com/break/junhong_cmp_fiber/internal/domain/carrierthreshold"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
// 无法从运营商确认结果时的可安全展示原因(不含内部细节与渠道报文)。
const (
failedStopReason = "运营商停机调用失败,等待状态查询确认"
unknownStopReason = "运营商停机结果未知,等待状态查询确认"
failedResumeReason = "运营商复机调用失败,等待状态查询确认"
unknownResumeReason = "运营商复机结果未知,等待状态查询确认"
)
// ExecuteStop 执行一次通道阈值达量停机,保证至多一次外部调用。
//
// 流程提交认领stop_submitted_at IS NULL 且仍持锁)→ 卡已停机则直接确认成功,不调运营商 →
// 否则复用既有停机重试、Integration Log 与统一审计执行停机。认领失败表示该锁已提交过
// (事件重复投递或人工重放),本次只结束,绝不重复调用;结果由恢复扫描查询收敛。
// 任何分支都不删除锁行、不解锁。
func (s *Service) ExecuteStop(ctx context.Context, lockID uint) error {
if err := s.requireExecution(); err != nil {
return err
}
lock, err := s.loadLock(ctx, lockID)
if err != nil {
return err
}
if lock == nil {
s.logger.Info("通道阈值停机事件对应锁不存在,幂等跳过", zap.Uint("lock_id", lockID))
return nil
}
claimed, err := s.ClaimStopSubmission(ctx, lockID, s.now())
if err != nil {
return err
}
if !claimed {
s.logger.Info("通道阈值停机已提交过,只等待恢复扫描确认",
zap.Uint("lock_id", lockID), zap.String("stop_status", lock.StopStatus))
return nil
}
card, err := s.loadCard(ctx, lock.CardID)
if err != nil {
return err
}
if card == nil {
// 卡事实不存在时无法调运营商,保留 submitted 由恢复扫描按窗口标记异常转人工。
s.logger.Warn("通道阈值停机锁对应卡不存在,等待恢复扫描处理",
zap.Uint("lock_id", lockID), zap.Uint("card_id", lock.CardID))
return nil
}
if card.NetworkStatus == constants.NetworkStatusOffline {
// 其他停因已先行停机:不重复调用运营商,直接确认本次停机目标已达成。
if _, err := s.markTaskConfirmed(ctx, lockID, stopTask, ""); err != nil {
return err
}
s.logger.Info("卡已停机,通道阈值停机直接确认成功",
zap.Uint("lock_id", lockID), zap.Uint("card_id", lock.CardID), zap.String("stop_reason", card.StopReason))
return nil
}
outcome, callErr := s.commander.StopCardForThreshold(ctx, lock.CardID)
if callErr != nil && outcome.Result == "" {
// 基础设施故障(读卡、写审计或写卡状态失败)导致结果无法判定:保留 submitted
// 交由恢复扫描查询确认,本次不判定终态、不重复调用。
s.logger.Error("通道阈值停机执行失败,等待恢复扫描确认",
zap.Uint("lock_id", lockID), zap.Uint("card_id", lock.CardID), zap.Error(callErr))
return nil
}
if callErr != nil {
s.logger.Warn("通道阈值停机运营商调用未成功,已按结果分类回填",
zap.Uint("lock_id", lockID), zap.Uint("card_id", lock.CardID),
zap.String("result", outcome.Result), zap.Error(callErr))
}
_, err = s.markTaskOutcome(ctx, lockID, stopTask, domain.TaskStatusSubmitted,
taskResultOf(outcome), outcome.IntegrationID, stopFailureReason(outcome))
if err != nil {
return err
}
s.logger.Info("通道阈值停机任务已回填结果",
zap.Uint("lock_id", lockID), zap.Uint("card_id", lock.CardID),
zap.String("result", outcome.Result), zap.String("integration_id", outcome.IntegrationID))
return nil
}
// ExecuteResume 执行一次通道阈值新周期复机,保证至多一次外部调用。
//
// 流程提交认领resume_submitted_at IS NULL→ 卡已在线则直接确认成功,不调运营商 →
// 否则复用既有复机重试、Integration Log 与统一审计执行复机(成功时同一事务写回卡状态,
// 且只在停因为通道阈值时清除停因)。认领失败表示已提交过,绝不重复调用。
func (s *Service) ExecuteResume(ctx context.Context, lockID uint) error {
if err := s.requireExecution(); err != nil {
return err
}
lock, err := s.loadLock(ctx, lockID)
if err != nil {
return err
}
if lock == nil {
s.logger.Info("通道阈值复机事件对应锁不存在,幂等跳过", zap.Uint("lock_id", lockID))
return nil
}
claimed, err := s.ClaimResumeSubmission(ctx, lockID, s.now())
if err != nil {
return err
}
if !claimed {
s.logger.Info("通道阈值复机已提交过,只等待恢复扫描确认",
zap.Uint("lock_id", lockID), zap.String("resume_status", lock.ResumeStatus))
return nil
}
card, err := s.loadCard(ctx, lock.CardID)
if err != nil {
return err
}
if card == nil {
s.logger.Warn("通道阈值复机锁对应卡不存在,等待恢复扫描处理",
zap.Uint("lock_id", lockID), zap.Uint("card_id", lock.CardID))
return nil
}
if card.NetworkStatus == constants.NetworkStatusOnline {
if _, err := s.markTaskConfirmed(ctx, lockID, resumeTask, ""); err != nil {
return err
}
s.logger.Info("卡已在线,通道阈值复机直接确认成功",
zap.Uint("lock_id", lockID), zap.Uint("card_id", lock.CardID))
return nil
}
outcome, callErr := s.commander.ResumeCardForThreshold(ctx, lock.CardID)
if callErr != nil && outcome.Result == "" {
s.logger.Error("通道阈值复机执行失败,等待恢复扫描确认",
zap.Uint("lock_id", lockID), zap.Uint("card_id", lock.CardID), zap.Error(callErr))
return nil
}
if callErr != nil {
s.logger.Warn("通道阈值复机运营商调用未成功,已按结果分类回填",
zap.Uint("lock_id", lockID), zap.Uint("card_id", lock.CardID),
zap.String("result", outcome.Result), zap.Error(callErr))
}
_, err = s.markTaskOutcome(ctx, lockID, resumeTask, domain.TaskStatusSubmitted,
taskResultOf(outcome), outcome.IntegrationID, resumeFailureReason(outcome))
if err != nil {
return err
}
s.logger.Info("通道阈值复机任务已回填结果",
zap.Uint("lock_id", lockID), zap.Uint("card_id", lock.CardID),
zap.String("result", outcome.Result), zap.String("integration_id", outcome.IntegrationID))
return nil
}
// requireExecution 校验消费者与周期处理所需的端口已配置。
func (s *Service) requireExecution() error {
if s == nil || s.db == nil || s.repository == nil {
return errors.New(errors.CodeServiceUnavailable, "通道流量阈值执行能力未完整配置")
}
if s.commander == nil {
return errors.New(errors.CodeServiceUnavailable, "通道流量阈值停复机执行端口未配置")
}
return nil
}
// taskResultOf 把运营商调用结果映射为子任务终态。
func taskResultOf(outcome domain.CommandOutcome) string {
switch outcome.Result {
case constants.AuditResultSuccess:
return domain.TaskStatusConfirmed
case constants.AuditResultUnknown:
return domain.TaskStatusUnknown
default:
return domain.TaskStatusFailed
}
}
// stopFailureReason 生成停机子任务的可安全失败原因,成功时为空。
func stopFailureReason(outcome domain.CommandOutcome) string {
if outcome.Confirmed() {
return ""
}
if outcome.Unresolved() {
return unknownStopReason
}
return failedStopReason
}
// resumeFailureReason 生成复机子任务的可安全失败原因,成功时为空。
func resumeFailureReason(outcome domain.CommandOutcome) string {
if outcome.Confirmed() {
return ""
}
if outcome.Unresolved() {
return unknownResumeReason
}
return failedResumeReason
}