增加失败原因和优化进度条
This commit is contained in:
@@ -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,
|
||||
|
||||
+57
-9
@@ -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;
|
||||
|
||||
+1
-1
@@ -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 "未命中价格";
|
||||
|
||||
+52
-6
@@ -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 > #{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
|
||||
|
||||
Reference in New Issue
Block a user