自动脚本计费

This commit is contained in:
wuweihua
2026-10-21 09:44:54 +08:00
parent 501056bb17
commit be3856a59c
3 changed files with 279 additions and 0 deletions
@@ -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
@@ -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 */
@@ -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.
*
* <p>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).</p>
*
* <p>Same rebill path as monitor manual retry / filter retry (reset WAITING then process).</p>
*/
@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<Long> 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;
}
}