Files
junhong_cmp_fiber/internal/infrastructure/wecom/approval_recovery_task.go
break 73f5125d3d
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m26s
七月迭代短暂完结,还有很多后端的关键东西没有弄,这是一版赶时间做的东西
2026-07-25 17:06:58 +08:00

156 lines
4.9 KiB
Go

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,
})
}