feat(收口): 补齐 8 月迭代缺口并同步 Spec 与证据链
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled

- 新增六对成对迁移 000232–000237:H5 弹窗类型、退款结算标识与申请人备注、优先轮询事实字段与两个新终态、通道阈值命中留痕、手机号最近解绑人、提现资格校验留痕
- 退款:原因必填与申请人备注、来源支付与渠道流水冻结、线下处理流水号补录审计、按订单查询可选退款方式、企微审批材料补齐且新增字段缺失映射即明确失败
- 优先轮询:人工关闭、有效期到期独立周期任务、失败与过期人工重触发、事实字段与异常重试查询、资产解析端点只读投影
- 通道阈值:命中事实同事务留痕与命中记录查询;员工账单:列表筛选与详情投影;商户池:列表投影与统计周期语义;H5:弹窗类型与类别排序
- 手机号:有效关联数量与最近解绑人、短信验证码失败次数限制;导出:佣金明细十五列与报表序号列
- 时间筛选:三处新增筛选纳入统一严格解析契约,员工账单产生时间参数改名
- 同步 12 份主 Spec 需求、两端点与异步任务证据链,门禁 context-health 与 OpenSpec 校验通过
This commit is contained in:
2026-09-18 15:34:29 +08:00
parent 5e78809b93
commit 5ed6b39deb
142 changed files with 7878 additions and 964 deletions

View File

@@ -132,7 +132,7 @@ func (b *PollingBase) priorityInPollingScope(_ context.Context, round *pollingPr
return false
}
// MarkExecution 标记即将真正发起上游调用,并在调用之前累加尝试次数。
// MarkExecution 标记即将真正发起上游调用,并在调用之前累加尝试次数与执行开始时间
// 崩溃遗留的执行中项被接管后据此按尝试上限收敛。
func (r *pollingPriorityRound) MarkExecution(_ context.Context) {
if r == nil {
@@ -140,7 +140,8 @@ func (r *pollingPriorityRound) MarkExecution(_ context.Context) {
}
ctx, cancel := pollingFallbackContext()
defer cancel()
hit, err := r.base.priorityStore.IncrementAttempt(ctx, r.item.ID)
now := time.Now()
hit, err := r.base.priorityStore.MarkExecutionStarted(ctx, r.item.ID, now)
if err != nil {
r.base.logger.Warn("累加优先轮询项尝试次数失败",
zap.Uint("priority_item_id", r.item.ID), zap.Error(err))
@@ -151,6 +152,40 @@ func (r *pollingPriorityRound) MarkExecution(_ context.Context) {
}
r.attempts = r.item.AttemptCount + 1
r.item.AttemptCount = r.attempts
r.item.StartedAt = &now
r.item.FinishedAt = nil
r.item.NextRunAt = nil
}
// RecordIntegrationLog 把本次执行对应的集成交互日志标识写回优先项。
// 只保存日志标识(不复制凭证或响应原文);写入失败只损失溯源细节,不阻断执行。
func (r *pollingPriorityRound) RecordIntegrationLog(integrationLogID string) {
if r == nil || r.item == nil || integrationLogID == "" {
return
}
ctx, cancel := pollingFallbackContext()
defer cancel()
if err := r.base.priorityStore.RecordIntegrationLog(ctx, r.item.ID, integrationLogID); err != nil {
r.base.logger.Warn("记录优先轮询项集成交互日志标识失败",
zap.Uint("priority_item_id", r.item.ID), zap.Error(err))
return
}
r.item.IntegrationLogID = integrationLogID
}
// nextRunAt 计算该项退回活动态后的下一次计划执行时间,复用与重入队完全相同的间隔解析。
func (r *pollingPriorityRound) nextRunAt() time.Time {
if r == nil || r.base == nil {
return time.Now().Add(pollingRequeueFallbackInterval)
}
ctx, cancel := pollingFallbackContext()
defer cancel()
nextCheckAt, ok := r.base.pollingNextCheckAt(ctx, r.item.CardID, r.taskType)
if !ok {
// 该任务类型轮询间隔为 NULL普通轮询不会接管仍写下兜底时间让重试路径可见。
return time.Now().Add(pollingRequeueFallbackInterval)
}
return nextCheckAt
}
// Complete 记录优先项执行成功并出队,该卡继续按普通轮询运行。
@@ -161,6 +196,7 @@ func (r *pollingPriorityRound) Complete(_ context.Context) {
ctx, cancel := pollingFallbackContext()
defer cancel()
before := r.stateData()
finishedAt := time.Now()
hit, err := r.base.priorityStore.MarkCompleted(ctx, r.item.ID)
if err != nil {
r.base.logger.Warn("更新优先轮询项完成状态失败",
@@ -174,11 +210,14 @@ func (r *pollingPriorityRound) Complete(_ context.Context) {
r.item.Status = constants.PollingPriorityStatusCompleted
r.item.Result = constants.PollingPriorityResultSuccess
r.item.FailureReason = ""
r.item.FinishedAt = &finishedAt
r.item.DequeuedAt = &finishedAt
r.item.NextRunAt = nil
r.writeAudit(constants.AuditActionPollingPriorityCompleted, "卡轮询优先项执行完成并出队", constants.AuditResultSuccess, "", "", before)
}
// FailRetryable 记录一次可恢复失败(上游超时、失败或响应无效)。
// 未达尝试上限时退回活动态并按既有轮询间隔由调用方重排;达到上限时记录失败终态并出队。
// 未达尝试上限时退回活动态并写下一次计划执行时间;达到上限时记录失败终态并出队。
func (r *pollingPriorityRound) FailRetryable(_ context.Context, reason string) {
if r == nil {
return
@@ -186,8 +225,10 @@ func (r *pollingPriorityRound) FailRetryable(_ context.Context, reason string) {
ctx, cancel := pollingFallbackContext()
defer cancel()
before := r.stateData()
finishedAt := time.Now()
if r.attempts < constants.PollingPriorityMaxAttempts {
hit, err := r.base.priorityStore.ReturnToPending(ctx, r.item.ID, reason)
nextRunAt := r.nextRunAt()
hit, err := r.base.priorityStore.ReturnToPending(ctx, r.item.ID, reason, nextRunAt)
if err != nil {
r.base.logger.Warn("退回优先轮询项活动状态失败",
zap.Uint("priority_item_id", r.item.ID), zap.Error(err))
@@ -201,6 +242,8 @@ func (r *pollingPriorityRound) FailRetryable(_ context.Context, reason string) {
r.item.Result = constants.PollingPriorityResultFailed
r.item.FailureReason = reason
r.item.ClaimedAt = nil
r.item.FinishedAt = &finishedAt
r.item.NextRunAt = &nextRunAt
r.writeAudit(constants.AuditActionPollingPriorityFailed, "卡轮询优先项执行失败", constants.AuditResultFailed, "", reason, before)
r.writeAudit(constants.AuditActionPollingPriorityRetried, "卡轮询优先项未达尝试上限,退回活动态重新排期", constants.AuditResultFailed, "", reason, before)
return
@@ -219,6 +262,9 @@ func (r *pollingPriorityRound) FailRetryable(_ context.Context, reason string) {
r.item.Status = constants.PollingPriorityStatusFailed
r.item.Result = constants.PollingPriorityResultFailed
r.item.FailureReason = reason
r.item.FinishedAt = &finishedAt
r.item.DequeuedAt = &finishedAt
r.item.NextRunAt = nil
r.writeAudit(constants.AuditActionPollingPriorityFailed, "卡轮询优先项执行失败", constants.AuditResultFailed, "", reason, before)
r.writeAudit(constants.AuditActionPollingPriorityDequeued, "卡轮询优先项达到尝试上限,失败出队", constants.AuditResultFailed, "", reason, before)
}
@@ -241,9 +287,13 @@ func (r *pollingPriorityRound) FailFinal(_ context.Context, reason string) {
zap.Uint("priority_item_id", r.item.ID))
return
}
finishedAt := time.Now()
r.item.Status = constants.PollingPriorityStatusFailed
r.item.Result = constants.PollingPriorityResultFailed
r.item.FailureReason = reason
r.item.FinishedAt = &finishedAt
r.item.DequeuedAt = &finishedAt
r.item.NextRunAt = nil
r.writeAudit(constants.AuditActionPollingPriorityFailed, "卡轮询优先项执行业务校验失败", constants.AuditResultFailed, "", reason, before)
r.writeAudit(constants.AuditActionPollingPriorityDequeued, "卡轮询优先项失败出队且不重试", constants.AuditResultFailed, "", reason, before)
}
@@ -251,17 +301,23 @@ func (r *pollingPriorityRound) FailFinal(_ context.Context, reason string) {
// stateData 返回当前优先项状态的前后值快照,覆盖触发来源、操作来源、时间与执行结果。
func (r *pollingPriorityRound) stateData() map[string]any {
return map[string]any{
"status": r.item.Status,
"result": r.item.Result,
"trigger_type": r.item.TriggerType,
"trigger_types": r.item.TriggerTypes,
"trigger_count": r.item.TriggerCount,
"last_triggered_at": r.item.LastTriggeredAt,
"attempt_count": r.item.AttemptCount,
"claimed_at": r.item.ClaimedAt,
"failure_reason": r.item.FailureReason,
"source": constants.AuditSourceWorker,
"occurred_at": time.Now(),
"status": r.item.Status,
"result": r.item.Result,
"trigger_type": r.item.TriggerType,
"trigger_types": r.item.TriggerTypes,
"trigger_count": r.item.TriggerCount,
"last_triggered_at": r.item.LastTriggeredAt,
"attempt_count": r.item.AttemptCount,
"attempt_limit": r.item.AttemptLimit,
"claimed_at": r.item.ClaimedAt,
"started_at": r.item.StartedAt,
"finished_at": r.item.FinishedAt,
"next_run_at": r.item.NextRunAt,
"dequeued_at": r.item.DequeuedAt,
"integration_log_id": r.item.IntegrationLogID,
"failure_reason": r.item.FailureReason,
"source": constants.AuditSourceWorker,
"occurred_at": time.Now(),
}
}