All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m35s
184 lines
6.2 KiB
Go
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,
|
|
})
|
|
}
|