package jnpf.dmsService; import jnpf.dmsEntity.storage.approval.DmsApprovedBatch; import jnpf.dmsMapper.DmsApprovedBatchMapper; import jnpf.dmsService.impl.DmsWenjianBanbenServiceImpl; import jnpf.exception.DataException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.beans.factory.annotation.Value; import java.util.UUID; /** Persistent retry orchestration. Never hold a database transaction across a file RPC. */ @Slf4j @Service @RequiredArgsConstructor public class DmsApprovedFileRetryService { private final DmsApprovedBatchMapper receipts; private final DmsApprovedFileAcceptanceService acceptance; private final DmsApprovedContentService files; private final DmsApprovedFileActivationService activation; private final DmsWenjianBanbenServiceImpl business; private final DmsApprovedDisableProcessor disables; private final DmsInvalidationProperties invalidation; @Value("${dms.approved-files.retry-backoff-enabled:true}") private boolean retryBackoffEnabled = true; public void process(DmsApprovedBatch candidate) { if (DmsApprovedDisableProcessor.supports(candidate.getAuditType()) && !disables.isEnabled()) return; if ("shixiao_shenqing".equals(candidate.getAuditType()) && !invalidation.allows(candidate.getTenantId(), candidate.getId())) return; String token = UUID.randomUUID().toString(); if (receipts.claim(candidate.getTenantId(), candidate.getId(), token, retryBackoffEnabled) != 1) return; DmsApprovedBatch claimed = null; try { claimed = receipts.find(candidate.getTenantId(), candidate.getTaskId(), candidate.getAuditType()); if (claimed == null || !token.equals(claimed.getLeaseToken())) throw new DataException("登记租约归属不匹配"); if (DmsApprovedDisableProcessor.supports(claimed.getAuditType())) { disables.process(claimed); } else if ("PREPARING".equals(claimed.getStatus()) || "REGISTERING".equals(claimed.getStatus())) { DmsApprovedBatch staged = acceptance.stage(claimed.getTenantId(), claimed.getFlowId(), claimed.getTaskId(), claimed.getAuditType(), claimed.getActorId(), claimed.getApprovedAt()); staged.setLeaseToken(token); if ("REGISTERING".equals(staged.getStatus())) { DmsApprovedBatch accepted = files.register(staged); if (accepted == null) throw new DataException("文件登记未返回进度"); advance(accepted); } else advance(staged); } else advance(claimed); receipts.finish(candidate.getTenantId(), candidate.getId(), token, null, 0); } catch (RuntimeException failure) { String error = failure.getMessage() == null ? failure.getClass().getSimpleName() : failure.getMessage(); // Keep errors bounded; no file payloads or credentials are persisted here. error = error.substring(0, Math.min(error.length(), 1000)); try { receipts.finish(candidate.getTenantId(), candidate.getId(), token, error, retryBackoffEnabled ? retryDelay(claimed == null ? 1 : claimed.getAttempts()) : 0); } catch (RuntimeException persistenceFailure) { failure.addSuppressed(persistenceFailure); // Expired lease recovers a failed receipt write. } log.error("批准文件处理失败,已保留进度待自动重试 tenant={} batch={}", candidate.getTenantId(), candidate.getId(), failure); throw failure; } } static int retryDelay(int attempts) { return Math.min(3600, 60 * (1 << Math.min(6, Math.max(0, attempts - 1)))); } public void advance(DmsApprovedBatch receipt) { if (DmsApprovedDisableProcessor.supports(receipt.getAuditType())) throw new DataException("停用任务必须由统一重试入口分派"); DmsApprovedBatch activated; try { activated = activation.activate(receipt.getTenantId(), receipt.getId()); } catch (RuntimeException failure) { receipts.fail(receipt.getTenantId(), receipt.getId()); throw failure; } if ("EFFECTIVE".equals(activated.getStatus())) business.applyApproved(activated); if ("FAILED".equals(activated.getStatus())) throw new DataException("该批准文件生效失败,等待自动恢复"); } }