From be3856a59c0141e98ff48337ed2c411b9997da34 Mon Sep 17 00:00:00 2001 From: wuweihua <88888888> Date: Wed, 21 Oct 2026 09:44:54 +0800 Subject: [PATCH] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=84=9A=E6=9C=AC=E8=AE=A1?= =?UTF-8?q?=E8=B4=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application.yml | 8 + .../bms/price/config/BillingProperties.java | 68 ++++++ .../task/PrcBillingTaskNightRetryTask.java | 203 ++++++++++++++++++ 3 files changed, 279 insertions(+) create mode 100644 bms-back/bms-system/src/main/java/com/bms/price/task/PrcBillingTaskNightRetryTask.java diff --git a/bms-back/bms-admin/src/main/resources/application.yml b/bms-back/bms-admin/src/main/resources/application.yml index 8dd70f1a..a61fcc06 100644 --- a/bms-back/bms-admin/src/main/resources/application.yml +++ b/bms-back/bms-admin/src/main/resources/application.yml @@ -40,6 +40,14 @@ bms: # 同步已直接复用订单行(跳过每单 Oracle 公网查询),瓶颈在 MySQL 计费写入, # 同客户行锁冲突由 billWithDbRetry 3 次重试兜底,16 路实测吞吐最高。 order-source-sync-threads: 16 + # Night auto-rebill: hours 0-8, pull 500 (all statuses) -> bill -> next 500 until empty / 09:00. + # Empty page => no billing. + night-retry: + enabled: true + start-hour: 0 + end-hour: 9 + batch-size: 500 + cron: "0 0/1 0-8 * * ?" oracle: # SLAVE datasource Oracle main-order table (schema.table allowed) order-table: WEISHI_USER.TAB_ORDER diff --git a/bms-back/bms-system/src/main/java/com/bms/price/config/BillingProperties.java b/bms-back/bms-system/src/main/java/com/bms/price/config/BillingProperties.java index 6e56d5b6..02fa4b0e 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/config/BillingProperties.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/config/BillingProperties.java @@ -20,6 +20,8 @@ public class BillingProperties private int consumerThreads = 8; /** Concurrent threads for monitor order-source sync */ private int orderSourceSyncThreads = 8; + /** Night window auto-retry of prc_billing_task (all statuses) */ + private NightRetry nightRetry = new NightRetry(); private Oracle oracle = new Oracle(); public String getMqMode() @@ -82,6 +84,16 @@ public class BillingProperties this.orderSourceSyncThreads = orderSourceSyncThreads; } + public NightRetry getNightRetry() + { + return nightRetry; + } + + public void setNightRetry(NightRetry nightRetry) + { + this.nightRetry = nightRetry; + } + public Oracle getOracle() { return oracle; @@ -92,6 +104,62 @@ public class BillingProperties this.oracle = oracle; } + /** + * Overnight rebill window: pull prc_billing_task (all success_status) and retryTask. + * Default hours [0, 9) local time; empty result skips billing. + */ + public static class NightRetry + { + /** Master switch */ + private boolean enabled = true; + /** Inclusive start hour (0 = 00:00) */ + private int startHour = 0; + /** Exclusive end hour (9 = stop at 09:00) */ + private int endHour = 9; + /** Page size: bill this many then pull next page (default 500) */ + private int batchSize = 500; + + public boolean isEnabled() + { + return enabled; + } + + public void setEnabled(boolean enabled) + { + this.enabled = enabled; + } + + public int getStartHour() + { + return startHour; + } + + public void setStartHour(int startHour) + { + this.startHour = startHour; + } + + public int getEndHour() + { + return endHour; + } + + public void setEndHour(int endHour) + { + this.endHour = endHour; + } + + public int getBatchSize() + { + return batchSize; + } + + public void setBatchSize(int batchSize) + { + this.batchSize = batchSize; + } + } + public static class Oracle { /** Upstream main-order table name on SLAVE/Oracle DS */ diff --git a/bms-back/bms-system/src/main/java/com/bms/price/task/PrcBillingTaskNightRetryTask.java b/bms-back/bms-system/src/main/java/com/bms/price/task/PrcBillingTaskNightRetryTask.java new file mode 100644 index 00000000..2b141c4d --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/task/PrcBillingTaskNightRetryTask.java @@ -0,0 +1,203 @@ +package com.bms.price.task; + +import java.util.Calendar; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import com.bms.price.config.BillingProperties; +import com.bms.price.domain.PrcBillingTask; +import com.bms.price.enums.BillTaskSuccessStatus; +import com.bms.price.mapper.PrcBillingTaskMapper; +import com.bms.price.service.IPrcBillingTaskService; + +/** + * Night window auto-rebill from prc_billing_task. + * + *

During [startHour, endHour) (default 00:00-09:00): pull up to {@value #DEFAULT_BATCH} + * task ids (all success_status) -> bill them all -> immediately pull the next page. + * Empty page => skip billing and end this run (next cron may pick up new rows).

+ * + *

Same rebill path as monitor manual retry / filter retry (reset WAITING then process).

+ */ +@Component +public class PrcBillingTaskNightRetryTask +{ + private static final Logger log = LoggerFactory.getLogger(PrcBillingTaskNightRetryTask.class); + + /** Default page size: bill this many, then pull next page */ + public static final int DEFAULT_BATCH = 500; + public static final int MIN_BATCH = 1; + public static final int MAX_BATCH = 1000; + + private final AtomicBoolean running = new AtomicBoolean(false); + + /** Keyset cursor within the current local-day window; reset when the window day changes. */ + private volatile long lastTaskId = 0L; + private volatile int windowDayOfYear = -1; + private volatile int windowYear = -1; + + @Autowired + private BillingProperties billingProperties; + + @Autowired + private PrcBillingTaskMapper billingTaskMapper; + + @Autowired + @Lazy + private IPrcBillingTaskService billingTaskService; + + /** + * Fire every minute; if a previous run is still paging batches, skip. + * Hour gate uses bms.billing.night-retry start/end. + */ + @Scheduled(cron = "${bms.billing.night-retry.cron:0 0/1 0-8 * * ?}") + public void scheduledTick() + { + BillingProperties.NightRetry cfg = billingProperties == null ? null : billingProperties.getNightRetry(); + if (cfg == null || !cfg.isEnabled()) + { + return; + } + if (!inWindow(cfg.getStartHour(), cfg.getEndHour())) + { + return; + } + if (!running.compareAndSet(false, true)) + { + log.debug("billing night-retry skip: previous run still paging"); + return; + } + try + { + runUntilEmptyOrEnd(cfg); + } + catch (Exception e) + { + log.error("billing night-retry run failed", e); + } + finally + { + running.set(false); + } + } + + /** + * Pull batch -> bill all -> pull next batch, until no data or outside window. + */ + private void runUntilEmptyOrEnd(BillingProperties.NightRetry cfg) + { + resetCursorIfNewWindowDay(); + int batch = clampBatch(cfg.getBatchSize()); + PrcBillingTask query = new PrcBillingTask(); + int page = 0; + int totalOk = 0; + int totalFail = 0; + + while (inWindow(cfg.getStartHour(), cfg.getEndHour())) + { + List ids = billingTaskMapper.selectTaskIdsByFilter( + query, Long.valueOf(lastTaskId), batch); + if (ids == null || ids.isEmpty()) + { + if (lastTaskId > 0L) + { + lastTaskId = 0L; + log.info("billing night-retry full scan done pages={} ok={} fail={}, cursor reset", + page, totalOk, totalFail); + } + else + { + log.info("billing night-retry no data, skip billing"); + } + return; + } + + page++; + log.info("billing night-retry page={} pull batchSize={} fetched={} lastTaskIdBefore={}", + page, batch, ids.size(), lastTaskId); + + int ok = 0; + int fail = 0; + for (Long taskId : ids) + { + if (taskId == null) + { + continue; + } + lastTaskId = taskId.longValue(); + if (!inWindow(cfg.getStartHour(), cfg.getEndHour())) + { + log.info("billing night-retry stop mid-page: reached end-hour page={} ok={} fail={}", + page, totalOk + ok, totalFail + fail); + return; + } + try + { + PrcBillingTask task = billingTaskService.retryTask(taskId); + if (task != null && BillTaskSuccessStatus.SUCCESS.getCode().equals(task.getSuccessStatus())) + { + ok++; + } + else + { + fail++; + } + } + catch (RuntimeException e) + { + fail++; + log.warn("billing night-retry failed taskId={} msg={}", taskId, e.getMessage()); + } + } + totalOk += ok; + totalFail += fail; + log.info("billing night-retry page={} done fetched={} ok={} fail={} lastTaskId={}", + page, ids.size(), ok, fail, lastTaskId); + // This page billed; loop immediately pulls the next page. + } + log.info("billing night-retry window closed pages={} ok={} fail={}", page, totalOk, totalFail); + } + + private void resetCursorIfNewWindowDay() + { + Calendar cal = Calendar.getInstance(); + int year = cal.get(Calendar.YEAR); + int day = cal.get(Calendar.DAY_OF_YEAR); + if (year != windowYear || day != windowDayOfYear) + { + windowYear = year; + windowDayOfYear = day; + lastTaskId = 0L; + } + } + + static boolean inWindow(int startHour, int endHour) + { + int start = Math.max(0, Math.min(23, startHour)); + int end = Math.max(1, Math.min(24, endHour)); + if (start >= end) + { + return false; + } + int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY); + return hour >= start && hour < end; + } + + static int clampBatch(int batchSize) + { + if (batchSize < MIN_BATCH) + { + return DEFAULT_BATCH; + } + if (batchSize > MAX_BATCH) + { + return MAX_BATCH; + } + return batchSize; + } +}