Files
junhong_cmp_fiber/internal/infrastructure/wecom/approval_recovery_task.go
break b063617153
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m35s
完善退款审批材料与恢复流程
2026-09-20 17:59:52 +08:00

184 lines
6.2 KiB
Go

package wecom
import (
"context"
"strings"
"time"
"github.com/bytedance/sonic"
"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 支持全局扫描任务与按实例人工任务,二者共享同一 unknown 确认算法。
func (h *ApprovalRecoveryTaskHandler) Handle(ctx context.Context, task *asynq.Task) error {
if h == nil || h.contexts == nil || h.infos == nil || h.queue == nil {
return errors.New(errors.CodeServiceUnavailable, "企业微信审批主动恢复任务未配置")
}
var payload struct {
InstanceID uint `json:"instance_id"`
}
if task != nil && len(task.Payload()) > 0 {
_ = sonic.Unmarshal(task.Payload(), &payload)
}
now := h.now().UTC()
if payload.InstanceID > 0 {
return h.recoverUnknownInstance(ctx, payload.InstanceID, now)
}
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) recoverUnknownInstance(ctx context.Context, instanceID uint, now time.Time) error {
record, err := h.contexts.GetRecoveryRecord(ctx, instanceID)
if err != nil || record == nil {
return err
}
claimed, err := h.contexts.ClaimUnknownRecovery(ctx, instanceID, now.Add(-constants.WeComApprovalPollingInterval))
if err != nil || !claimed {
return err
}
return h.recoverUnknownRecord(ctx, *record, now)
}
func (h *ApprovalRecoveryTaskHandler) recoverUnknownRecord(ctx context.Context, record ApprovalRecoveryRecord, now time.Time) error {
spNo, submissionIntegrationID, err := h.contexts.FindSuccessfulSubmissionSPNo(ctx, record.InstanceID)
if err != nil {
return err
}
integrationIDs := []string{submissionIntegrationID}
if spNo == "" {
var ids []string
spNo, ids, err = h.findUniqueSPNo(ctx, record, now)
if err != nil {
return err
}
integrationIDs = append(integrationIDs, ids...)
}
if spNo == "" {
return nil
}
recovered, err := h.contexts.RecoverSubmitted(ctx, record.InstanceID, spNo, integrationIDs, constants.AuditActorScheduledJob, constants.ApprovalAuditActorRecoveryJob, constants.AuditSourceScheduler)
if err != nil || !recovered {
return err
}
return h.enqueueDetailSync(ctx, record.ApplicationID, spNo)
}
func (h *ApprovalRecoveryTaskHandler) recoverUnknown(ctx context.Context, now time.Time) error {
records, err := h.contexts.ListUnknownRecovery(ctx, now.Add(-constants.WeComApprovalPollingInterval), constants.WeComApprovalUnknownRecoveryBatchSize)
if err != nil {
return err
}
for _, record := range records {
claimed, err := h.contexts.ClaimUnknownRecovery(ctx, record.InstanceID, now.Add(-constants.WeComApprovalPollingInterval))
if err != nil {
return err
}
if !claimed {
continue
}
if err := h.recoverUnknownRecord(ctx, record, now); err != nil {
return err
}
}
return nil
}
// findUniqueSPNo 使用提交时间附近的固定窄窗口分页查询,并只接受唯一未关联候选。
func (h *ApprovalRecoveryTaskHandler) findUniqueSPNo(ctx context.Context, record ApprovalRecoveryRecord, now time.Time) (string, []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, nil
}
cursor := ""
seenCursors := make(map[string]struct{})
seenCandidates := make(map[string]struct{})
unbound := make([]string, 0, 2)
integrationIDs := 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 "", nil, err
}
integrationIDs = append(integrationIDs, page.IntegrationID)
existing, err := h.contexts.ExistingSPNos(ctx, record.ApplicationID, page.SPNos)
if err != nil {
return "", nil, 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 "", integrationIDs, nil
}
}
}
next := strings.TrimSpace(page.NextCursor)
if next == "" {
break
}
if _, exists := seenCursors[next]; exists {
return "", nil, errors.New(errors.CodeServiceUnavailable, "企业微信批量审批单号分页游标重复")
}
seenCursors[next] = struct{}{}
cursor = next
}
if len(unbound) != 1 {
return "", integrationIDs, nil
}
return unbound[0], integrationIDs, 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,
})
}