增加失败原因和优化进度条

This commit is contained in:
wuweihua
2026-11-05 15:38:45 +08:00
parent 1727386811
commit 891fdea7d3
12 changed files with 220 additions and 38 deletions
@@ -1733,7 +1733,7 @@ public class ArBillServiceImpl implements IArBillService {
case CATALOG_COUNTRY -> "产品价未匹配国家";
case CATALOG_WEIGHT -> "产品价未匹配重量段";
case NO_CUSTOMER_PACKAGE -> "未绑定套餐";
case NO_ACTIVE_PACKAGE_VERSION -> "无生效套餐版本";
case NO_ACTIVE_PACKAGE_VERSION -> "套餐未启用/无生效版本";
case ALREADY_IN_AR_BILL -> "已生成应收账单";
case PRODUCT_ARCHIVE_MISSING -> "产品档案缺失";
case PRICE_NOT_HIT -> "未命中价格";
@@ -66,7 +66,7 @@ public enum BillTaskFailCode
{
return NO_CUSTOMER_PACKAGE.code;
}
if (containsAny(msg, "无生效套餐版本", "noActivePackageVersion", "NO_ACTIVE_PACKAGE_VERSION"))
if (containsAny(msg, "无生效套餐版本", "套餐未启用", "noActivePackageVersion", "NO_ACTIVE_PACKAGE_VERSION", "packageNotEnabled"))
{
return NO_ACTIVE_PACKAGE_VERSION.code;
}
@@ -82,6 +82,18 @@ public interface PrcPriceSetDetailMapper
@Param("countryName") String countryName,
@Param("zoneCode") String zoneCode);
/**
* Next FIXED weight band whose weight_min is strictly above {@code weight}
* (ceil into the gap between bands, e.g. 1.001 -> 1.01~2).
*/
public PrcPriceSetDetail selectFixedNextBandUp(
@Param("psvId") Long psvId,
@Param("productCode") String productCode,
@Param("countryCode") String countryCode,
@Param("countryName") String countryName,
@Param("zoneCode") String zoneCode,
@Param("weight") BigDecimal weight);
public int countZonedByPsvProductCountry(
@Param("psvId") Long psvId,
@Param("productCode") String productCode,
@@ -410,12 +410,16 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
}
if (binds == null || binds.isEmpty())
{
String msgKey = request instanceof TmsPriceBillRequest
? "price.bill.noCustomerPackage"
: "price.quote.noCustomerPackage";
throw new ServiceException(MessageUtils.message(msgKey));
if (request instanceof TmsPriceBillRequest)
{
// Soft-fail: persist billing_fail_status=4 on the order bill.
return buildBillingMissResult(request, productCode, productName, countryCode, countryName,
weight, bizDate, BillBillingFailStatus.NO_CUSTOMER_PACKAGE.getCode());
}
throw new ServiceException(MessageUtils.message("price.quote.noCustomerPackage"));
}
boolean anyEnabledSet = false;
boolean anyActiveVersion = false;
boolean productExcludedBySetProducts = false;
boolean attemptedPriceMatch = false;
@@ -428,10 +432,12 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
for (PrcCustomerPriceSet bind : binds)
{
PrcPriceSet set = priceSetMapper.selectPrcPriceSetById(bind.getPriceSetId());
// Only enabled (E) sets participate; draft/stop are skipped and recorded as fail below.
if (set == null || !"E".equals(set.getStatus()))
{
continue;
}
anyEnabledSet = true;
// Temporal gate: prc_price_set_version.effective_time / expire_time (not bind pr_begindate)
PrcPriceSetVersion version = versionMapper.selectActiveByPriceSetAt(set.getPriceSetId(), asOf);
if (version == null || version.getPsvId() == null)
@@ -560,9 +566,22 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
if (!anyActiveVersion)
{
String msgKey = request instanceof TmsPriceBillRequest
? "price.bill.noActivePackageVersion"
: "price.quote.noActivePackageVersion";
if (request instanceof TmsPriceBillRequest)
{
// Soft-fail: package bound but not enabled, or enabled with no version at bizDate.
// Persists billing_fail_status=5 so local rebill / MQ bill both record the miss.
TmsPriceQuoteResult miss = buildBillingMissResult(request, productCode, productName,
countryCode, countryName, weight, bizDate,
BillBillingFailStatus.NO_ACTIVE_PACKAGE_VERSION.getCode());
if (!anyEnabledSet)
{
miss.setMatchMessage(MessageUtils.message("price.bill.billingFail.packageNotEnabled"));
}
return miss;
}
String msgKey = anyEnabledSet
? "price.quote.noActivePackageVersion"
: "price.quote.packageNotEnabled";
throw new ServiceException(MessageUtils.message(msgKey));
}
if (!attemptedPriceMatch && productExcludedBySetProducts)
@@ -720,6 +739,14 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
{
return MessageUtils.message("price.bill.billingFail.catalogWeight");
}
if (missCode == BillBillingFailStatus.NO_CUSTOMER_PACKAGE.getCode())
{
return MessageUtils.message("price.bill.billingFail.noCustomerPackage");
}
if (missCode == BillBillingFailStatus.NO_ACTIVE_PACKAGE_VERSION.getCode())
{
return MessageUtils.message("price.bill.billingFail.noActivePackageVersion");
}
return MessageUtils.message("price.bill.packagePriceNotHit");
}
@@ -994,7 +1021,8 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
countryCode, countryName, zoneCode, weight);
BigDecimal billWeight = weight;
boolean snappedToMinBand = false;
// Miss: if charge weight is below the lowest band, bill at that band's weight_min.
boolean snappedUpToNextBand = false;
// Miss: below lowest band -> bill at that band's weight_min.
if (detail == null)
{
PrcPriceSetDetail minBand = detailMapper.selectFixedMinBand(
@@ -1007,6 +1035,19 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
snappedToMinBand = true;
}
}
// Miss in a gap (e.g. 1.001 between 0-1 and 1.01-2): ceil to next band's weight_min.
if (detail == null)
{
PrcPriceSetDetail nextBand = detailMapper.selectFixedNextBandUp(
psvId, productCode, countryCode, countryName, zoneCode, weight);
if (nextBand != null && nextBand.getWeightMin() != null)
{
detail = nextBand;
billWeight = nextBand.getWeightMin().setScale(3, RoundingMode.HALF_UP);
snappedUpToNextBand = true;
}
}
// Still no band (above last max, or no details): caller records WEIGHT miss.
if (detail == null)
{
return null;
@@ -1016,7 +1057,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
return null;
}
BmsProductCountry useCatalog = catalog;
if (snappedToMinBand)
if (snappedToMinBand || snappedUpToNextBand)
{
BmsProductCountry rematched = softMatchCatalog(
productCode, countryCode, countryName, zoneCode, billWeight, bizDate, resolvedZone);
@@ -1039,6 +1080,13 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
r.setChargeWeight(weight);
msg = msg + "; billed at min band " + billWeight.toPlainString() + " (persist original weight)";
}
else if (snappedUpToNextBand)
{
// Gap ceil: bill at next band min (1.001 -> 1.01); persist snapped charge_weight.
r.setWeight(weight);
r.setChargeWeight(billWeight);
msg = msg + "; snapped up " + weight.toPlainString() + " -> " + billWeight.toPlainString();
}
r.setMatchMessage(msg);
r.setCustomerId(bind.getCustomerId());
return r;
@@ -247,7 +247,7 @@ public final class OrderBillExportSupport
case 24: return "产品价未匹配国家";
case 25: return "产品价未匹配重量段";
case 4: return "未绑定套餐";
case 5: return "无生效套餐版本";
case 5: return "套餐未启用/无生效版本";
case 6: return "已生成应收账单";
case 7: return "产品档案缺失";
case 8: return "未命中价格";
@@ -15,6 +15,7 @@ import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -50,6 +51,9 @@ public class PrcOrderBillLocalRebillAsyncRunner
public static final int MIN_THREADS = 2;
public static final int MAX_THREADS = 16;
private static final int FAIL_DETAIL_MAX = 200;
/** Publish Redis progress at most this often while a page is running. */
private static final int PROGRESS_EVERY_N = 20;
private static final long PROGRESS_MIN_INTERVAL_MS = 500L;
private final ConcurrentHashMap<String, AtomicBoolean> cancelFlags = new ConcurrentHashMap<String, AtomicBoolean>();
@@ -81,6 +85,8 @@ public class PrcOrderBillLocalRebillAsyncRunner
int total = billIds == null ? 0 : billIds.size();
AtomicInteger success = new AtomicInteger(0);
AtomicInteger fail = new AtomicInteger(0);
AtomicInteger done = new AtomicInteger(0);
AtomicLong lastProgressAt = new AtomicLong(0L);
StringBuilder failTips = new StringBuilder();
List<Map<String, Object>> failDetails = Collections.synchronizedList(new ArrayList<Map<String, Object>>());
int threads = clampThreads(billingProperties == null ? null : billingProperties.getOrderSourceSyncThreads());
@@ -89,6 +95,7 @@ public class PrcOrderBillLocalRebillAsyncRunner
try
{
put(taskId, BatchAddIncomeBillFeeTask.STATUS_RUNNING, 1, "running", null, total, 0, 0);
lastProgressAt.set(System.currentTimeMillis());
if (total == 0)
{
put(taskId, BatchAddIncomeBillFeeTask.STATUS_FAIL, 100, "failed", "no bills", 0, 0, 0);
@@ -100,7 +107,6 @@ public class PrcOrderBillLocalRebillAsyncRunner
finishCancelled(taskId, request, total, success.get(), fail.get(), threads, failDetails);
return;
}
int processed = 0;
boolean cancelled = false;
for (int from = 0; from < total; from += PAGE_SIZE)
{
@@ -135,19 +141,22 @@ public class PrcOrderBillLocalRebillAsyncRunner
taskId, one, e.getMessage());
return Boolean.FALSE;
}
finally
{
int finished = done.incrementAndGet();
maybePublishProgress(taskId, total, finished, success.get(), fail.get(), lastProgressAt);
}
});
}
invokeAllQuiet(pool, jobs);
processed = to;
if (isCancelRequested(taskId))
{
cancelled = true;
break;
}
int percent = Math.min(99, (int) Math.floor(100.0 * processed / total));
put(taskId, BatchAddIncomeBillFeeTask.STATUS_RUNNING, percent,
"processing " + processed + "/" + total + " page=" + ((from / PAGE_SIZE) + 1),
null, total, success.get(), fail.get());
// Page boundary: always flush latest counts (even if throttle skipped mid-page).
publishProgress(taskId, total, done.get(), success.get(), fail.get());
lastProgressAt.set(System.currentTimeMillis());
}
if (cancelled || isCancelRequested(taskId))
{
@@ -332,6 +341,43 @@ public class PrcOrderBillLocalRebillAsyncRunner
return redisCache.getCacheObject(CACHE_KEY_PREFIX + taskId);
}
/**
* Throttled Redis progress so the UI moves during a large page (not only after 500).
* Does not change worker throughput; only how often status is published.
*/
private void maybePublishProgress(String taskId, int total, int finished, int ok, int fail,
AtomicLong lastProgressAt)
{
if (total <= 0 || finished <= 0)
{
return;
}
boolean byCount = finished % PROGRESS_EVERY_N == 0 || finished >= total;
long now = System.currentTimeMillis();
long prev = lastProgressAt.get();
boolean byTime = now - prev >= PROGRESS_MIN_INTERVAL_MS;
if (!byCount && !byTime)
{
return;
}
if (!lastProgressAt.compareAndSet(prev, now) && !byCount)
{
return;
}
publishProgress(taskId, total, finished, ok, fail);
}
private void publishProgress(String taskId, int total, int finished, int ok, int fail)
{
int percent = total <= 0 ? 0 : Math.min(99, (int) Math.floor(100.0 * finished / total));
if (finished >= total)
{
percent = Math.min(99, percent);
}
put(taskId, BatchAddIncomeBillFeeTask.STATUS_RUNNING, percent,
"processing " + finished + "/" + total, null, total, ok, fail);
}
private static void invokeAllQuiet(ExecutorService pool, List<Callable<Boolean>> jobs) throws InterruptedException
{
List<Future<Boolean>> futures = pool.invokeAll(jobs);
@@ -519,6 +519,37 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
limit 1
</select>
<!-- Gap between bands (e.g. 1.001 between 0-1 and 1.01-2): next higher weight_min. -->
<select id="selectFixedNextBandUp" resultMap="PrcPriceSetDetailResult">
<include refid="selectPrcPriceSetDetailVo"/>
where psv_id = #{psvId}
and ifnull(is_del, '0') = '0'
and product_code = #{productCode}
and weight_min is not null
and weight_min &gt; #{weight}
<choose>
<when test="countryCode != null and countryCode != ''">
and upper(ifnull(country_code,'')) = upper(#{countryCode})
</when>
<when test="countryName != null and countryName != ''">
and country_name = #{countryName}
</when>
<otherwise>
and 1 = 0
</otherwise>
</choose>
<choose>
<when test="zoneCode != null and zoneCode != ''">
and upper(ifnull(zone_code,'')) = upper(#{zoneCode})
</when>
<otherwise>
and (zone_code is null or zone_code = '')
</otherwise>
</choose>
order by weight_min asc, psd_id asc
limit 1
</select>
<select id="countZonedByPsvProductCountry" resultType="int">
select count(1)
from prc_price_set_detail