Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
- 新增六对成对迁移 000232–000237:H5 弹窗类型、退款结算标识与申请人备注、优先轮询事实字段与两个新终态、通道阈值命中留痕、手机号最近解绑人、提现资格校验留痕 - 退款:原因必填与申请人备注、来源支付与渠道流水冻结、线下处理流水号补录审计、按订单查询可选退款方式、企微审批材料补齐且新增字段缺失映射即明确失败 - 优先轮询:人工关闭、有效期到期独立周期任务、失败与过期人工重触发、事实字段与异常重试查询、资产解析端点只读投影 - 通道阈值:命中事实同事务留痕与命中记录查询;员工账单:列表筛选与详情投影;商户池:列表投影与统计周期语义;H5:弹窗类型与类别排序 - 手机号:有效关联数量与最近解绑人、短信验证码失败次数限制;导出:佣金明细十五列与报表序号列 - 时间筛选:三处新增筛选纳入统一严格解析契约,员工账单产生时间参数改名 - 同步 12 份主 Spec 需求、两端点与异步任务证据链,门禁 context-health 与 OpenSpec 校验通过
142 lines
7.0 KiB
Go
142 lines
7.0 KiB
Go
package task
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hibiken/asynq"
|
|
"go.uber.org/zap"
|
|
|
|
cardapp "github.com/break/junhong_cmp_fiber/internal/application/cardobservation"
|
|
carddomain "github.com/break/junhong_cmp_fiber/internal/domain/cardobservation"
|
|
"github.com/break/junhong_cmp_fiber/internal/gateway"
|
|
"github.com/break/junhong_cmp_fiber/internal/infrastructure/integrationlog"
|
|
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
|
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
|
)
|
|
|
|
// PollingCarddataHandler 负责流量轮询编排,查询结果统一交给卡观测应用服务。
|
|
type PollingCarddataHandler struct {
|
|
base *PollingBase
|
|
gateway *gateway.Client
|
|
carrier *postgres.CarrierStore
|
|
observation *cardapp.Service
|
|
integration *integrationlog.Repository
|
|
}
|
|
|
|
// NewPollingCarddataHandler 创建流量检查任务处理器。
|
|
func NewPollingCarddataHandler(base *PollingBase, gw *gateway.Client, carrier *postgres.CarrierStore, observation *cardapp.Service, integration *integrationlog.Repository) *PollingCarddataHandler {
|
|
return &PollingCarddataHandler{base: base, gateway: gw, carrier: carrier, observation: observation, integration: integration}
|
|
}
|
|
|
|
// Handle 处理流量检查任务。
|
|
func (h *PollingCarddataHandler) Handle(ctx context.Context, task *asynq.Task) error {
|
|
startedAt := time.Now()
|
|
cardID, ok := parseTaskPayload(task.Payload(), h.base.logger)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
if !h.base.acquireConcurrency(ctx, constants.TaskTypePollingCarddata) {
|
|
h.base.logger.Debug("并发已满,重新入队", zap.Uint("card_id", cardID))
|
|
return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingCarddata)
|
|
}
|
|
defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingCarddata)
|
|
lockToken, locked, err := h.base.acquireCardTrafficSyncLock(ctx, cardID)
|
|
if err != nil || !locked {
|
|
h.base.logger.Warn("卡流量同步锁不可用,延迟重入队", zap.Uint("card_id", cardID), zap.Error(err))
|
|
return h.base.queueMgr.Requeue(ctx, cardID, constants.TaskTypePollingCarddata, time.Now().Add(5*time.Second))
|
|
}
|
|
defer h.base.releaseCardTrafficSyncLock(ctx, cardID, lockToken)
|
|
|
|
// 优先轮询互斥接缝:放在流量同步锁获取成功之后,避免认领后拿不到锁导致优先项空占。
|
|
priority, skip, priorityErr := h.base.beginPollingPriority(ctx, cardID, constants.TaskTypePollingCarddata)
|
|
if priorityErr != nil {
|
|
return h.failAndRequeue(ctx, cardID, startedAt, "查询活动优先轮询项失败", priorityErr)
|
|
}
|
|
if skip {
|
|
h.base.logger.Debug("卡轮询优先项已被其它执行持有,本轮跳过并延后",
|
|
zap.Uint("card_id", cardID), zap.String("task_type", constants.TaskTypePollingCarddata))
|
|
return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingCarddata)
|
|
}
|
|
|
|
h.base.invalidateCardCache(ctx, cardID)
|
|
card, err := h.base.iotCardStore.GetByID(ctx, cardID)
|
|
if err != nil {
|
|
if isNotFound(err) {
|
|
priority.FailFinal(ctx, "卡不存在")
|
|
return nil
|
|
}
|
|
// 本地读卡失败属可恢复失败:先按真实执行累加尝试次数,再按上限收敛,避免加急事实被静默丢弃。
|
|
priority.MarkExecution(ctx)
|
|
priority.FailRetryable(ctx, "获取卡信息失败")
|
|
return h.failAndRequeue(ctx, cardID, startedAt, "获取卡信息失败", err)
|
|
}
|
|
// 执行前轮询范围校验:优先通道不得重启已停止轮询的卡。
|
|
if !h.base.priorityInPollingScope(ctx, priority, card) {
|
|
return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingCarddata)
|
|
}
|
|
if h.gateway == nil {
|
|
priority.FailFinal(ctx, "流量查询能力未配置")
|
|
return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingCarddata)
|
|
}
|
|
priority.MarkExecution(ctx)
|
|
attempt := newGatewayAttempt(cardID, constants.IntegrationOperationGatewayTraffic, constants.CardObservationSceneTrafficPolling)
|
|
result, err := h.gateway.QueryFlow(ctx, &gateway.FlowQueryReq{CardNo: card.ICCID})
|
|
if err != nil {
|
|
_ = recordPriorityGatewayAttempt(ctx, h.integration, attempt, constants.IntegrationResultFailed, false, "查询流量失败", priority)
|
|
priority.FailRetryable(ctx, "查询流量失败")
|
|
return h.failAndRequeue(ctx, cardID, startedAt, "查询流量失败", err)
|
|
}
|
|
if strings.TrimSpace(result.ICCID) == "" {
|
|
_ = recordPriorityGatewayAttempt(ctx, h.integration, attempt, constants.IntegrationResultInvalidPayload, false, "流量查询响应缺少 ICCID", priority)
|
|
priority.FailRetryable(ctx, "流量查询响应缺少 ICCID")
|
|
return h.failAndRequeue(ctx, cardID, startedAt, "流量查询响应缺少 ICCID", nil)
|
|
}
|
|
ctx = withPollingWorkerAuditContext(ctx, constants.TaskTypePollingCarddata, "卡流量轮询任务", attempt.IntegrationID)
|
|
if h.observation == nil || h.carrier == nil {
|
|
priority.FailFinal(ctx, "卡流量观测能力未配置")
|
|
return h.failAndRequeue(ctx, cardID, startedAt, "卡流量观测能力未配置", nil)
|
|
}
|
|
now := time.Now()
|
|
decision, err := h.observation.ApplyTrafficObservation(ctx, carddomain.TrafficObservation{
|
|
CardID: cardID, GatewayReadingMB: float64(result.Used), ResetDay: h.carrier.GetDataResetDay(ctx, card.CarrierID),
|
|
Metadata: carddomain.ObservationMetadata{
|
|
ObservationID: attempt.IntegrationID, Source: constants.CardObservationSourcePolling,
|
|
Scene: constants.CardObservationSceneTrafficPolling, ObservedAt: now, CorrelationID: attempt.IntegrationID,
|
|
},
|
|
})
|
|
if err != nil {
|
|
_ = recordPriorityGatewayAttempt(ctx, h.integration, attempt, constants.IntegrationResultUnknown, false, "应用流量观测失败", priority)
|
|
priority.FailRetryable(ctx, "应用流量观测失败")
|
|
return h.failAndRequeue(ctx, cardID, startedAt, "应用流量观测失败", err)
|
|
}
|
|
changed := decision.IncrementMB > 0 || decision.CrossMonth
|
|
if changed {
|
|
_ = recordPriorityGatewayAttempt(ctx, h.integration, attempt, constants.IntegrationResultSuccess, true, "流量业务事实发生变化", priority)
|
|
}
|
|
if h.base.verboseLog {
|
|
h.base.logger.Info("流量轮询详情", zap.Uint("card_id", cardID), zap.String("iccid", card.ICCID),
|
|
zap.Float64("gateway_flow_mb", float64(result.Used)), zap.Float64("increment_mb", decision.IncrementMB),
|
|
zap.Bool("is_cross_month", decision.CrossMonth), zap.Bool("reading_accepted", decision.ReadingAccepted))
|
|
}
|
|
metric := "polling.observation.unchanged"
|
|
if changed {
|
|
metric = "polling.observation.persisted"
|
|
}
|
|
h.base.logger.Info("流量轮询观测完成", zap.Uint("card_id", cardID), zap.Bool("changed", changed), zap.String("metric", metric))
|
|
priority.Complete(ctx)
|
|
h.base.updateStats(ctx, constants.TaskTypePollingCarddata, true, time.Since(startedAt))
|
|
return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingCarddata)
|
|
}
|
|
|
|
func (h *PollingCarddataHandler) failAndRequeue(ctx context.Context, cardID uint, startedAt time.Time, message string, err error) error {
|
|
fields := []zap.Field{zap.Uint("card_id", cardID)}
|
|
if err != nil {
|
|
fields = append(fields, zap.Error(err))
|
|
}
|
|
h.base.logger.Warn(message, fields...)
|
|
h.base.updateStats(ctx, constants.TaskTypePollingCarddata, false, time.Since(startedAt))
|
|
return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingCarddata)
|
|
}
|