package wecom import ( "context" "strings" "time" "github.com/hibiken/asynq" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/queue" ) // ApprovalRecoveryTaskHandler 周期恢复结果未知审批并轮询未终态详情。 type ApprovalRecoveryTaskHandler struct { contexts *ApprovalContextRepository infos *ApprovalInfoClient queue *queue.Client now func() time.Time } // NewApprovalRecoveryTaskHandler 创建企业微信审批主动恢复任务 Handler。 func NewApprovalRecoveryTaskHandler(contexts *ApprovalContextRepository, infos *ApprovalInfoClient, queueClient *queue.Client) *ApprovalRecoveryTaskHandler { return &ApprovalRecoveryTaskHandler{contexts: contexts, infos: infos, queue: queueClient, now: time.Now} } // Handle 扫描未终态和结果未知记录;结果未知只查询关联,绝不重新调用 applyevent。 func (h *ApprovalRecoveryTaskHandler) Handle(ctx context.Context, _ *asynq.Task) error { if h == nil || h.contexts == nil || h.infos == nil || h.queue == nil { return errors.New(errors.CodeServiceUnavailable, "企业微信审批主动恢复任务未配置") } now := h.now().UTC() if err := h.contexts.PromoteStaleSendingToUnknown(ctx, now.Add(-constants.WeComApprovalSendingLease)); err != nil { return err } if err := h.enqueuePendingSync(ctx, now); err != nil { return err } return h.recoverUnknown(ctx, now) } func (h *ApprovalRecoveryTaskHandler) enqueuePendingSync(ctx context.Context, now time.Time) error { records, err := h.contexts.ListPendingSync(ctx, now.Add(-constants.WeComApprovalPollingInterval), constants.WeComApprovalRecoveryBatchSize) if err != nil { return err } for _, record := range records { if err := h.enqueueDetailSync(ctx, record.ApplicationID, record.SPNo); err != nil { return err } } return nil } func (h *ApprovalRecoveryTaskHandler) recoverUnknown(ctx context.Context, now time.Time) error { cutoff := now.Add(-constants.WeComApprovalPollingInterval) records, err := h.contexts.ListUnknownRecovery(ctx, cutoff, constants.WeComApprovalUnknownRecoveryBatchSize) if err != nil { return err } for _, record := range records { claimed, err := h.contexts.ClaimUnknownRecovery(ctx, record.InstanceID, cutoff) if err != nil { return err } if !claimed { continue } spNo, err := h.contexts.FindSuccessfulSubmissionSPNo(ctx, record.InstanceID) if err != nil { return err } if spNo == "" { spNo, err = h.findUniqueSPNo(ctx, record, now) if err != nil { return err } } if spNo == "" { continue } recovered, err := h.contexts.RecoverSubmitted(ctx, record.InstanceID, spNo) if err != nil { return err } if recovered { if err := h.enqueueDetailSync(ctx, record.ApplicationID, spNo); err != nil { return err } } } return nil } // findUniqueSPNo 使用提交时间附近的固定窄窗口分页查询,并只接受唯一未关联候选。 func (h *ApprovalRecoveryTaskHandler) findUniqueSPNo(ctx context.Context, record ApprovalRecoveryRecord, now time.Time) (string, error) { startTime := record.SubmissionAttemptedAt.Add(-constants.WeComApprovalRecoveryWindow) endTime := record.SubmissionAttemptedAt.Add(constants.WeComApprovalRecoveryWindow) if endTime.After(now) { endTime = now } if !startTime.Before(endTime) { return "", nil } cursor := "" seenCursors := make(map[string]struct{}) seenCandidates := make(map[string]struct{}) unbound := make([]string, 0, 2) for { page, err := h.infos.List(ctx, ApprovalInfoQuery{ ApplicationID: record.ApplicationID, StartTime: startTime, EndTime: endTime, TemplateID: record.TemplateID, CreatorUserID: record.CreatorUserID, Cursor: cursor, Size: constants.WeComApprovalInfoMaxPageSize, }) if err != nil { return "", err } existing, err := h.contexts.ExistingSPNos(ctx, record.ApplicationID, page.SPNos) if err != nil { return "", err } for _, candidate := range page.SPNos { if _, seen := seenCandidates[candidate]; seen { continue } seenCandidates[candidate] = struct{}{} if _, exists := existing[candidate]; !exists { unbound = append(unbound, candidate) if len(unbound) > 1 { return "", nil } } } next := strings.TrimSpace(page.NextCursor) if next == "" { break } if _, exists := seenCursors[next]; exists { return "", errors.New(errors.CodeServiceUnavailable, "企业微信批量审批单号分页游标重复") } seenCursors[next] = struct{}{} cursor = next } if len(unbound) != 1 { return "", nil } return unbound[0], nil } func (h *ApprovalRecoveryTaskHandler) enqueueDetailSync(ctx context.Context, applicationID uint, spNo string) error { return h.queue.EnqueueTask(ctx, constants.TaskTypeWeComApprovalSync, ApprovalDetailSyncTask{ ApplicationID: applicationID, SPNo: spNo, Source: constants.ApprovalSyncSourcePolling, }) }