增加失败原因和优化进度条
This commit is contained in:
@@ -224,6 +224,11 @@ price.bill.billingFail.weight=计费失败:销售价未匹配重量段
|
|||||||
price.bill.billingFail.catalogProduct=计费失败:产品价未匹配产品
|
price.bill.billingFail.catalogProduct=计费失败:产品价未匹配产品
|
||||||
price.bill.billingFail.catalogCountry=计费失败:产品价未匹配国家
|
price.bill.billingFail.catalogCountry=计费失败:产品价未匹配国家
|
||||||
price.bill.billingFail.catalogWeight=计费失败:产品价未匹配重量段
|
price.bill.billingFail.catalogWeight=计费失败:产品价未匹配重量段
|
||||||
|
price.bill.billingFail.packageNotEnabled=计费失败:套餐未启用
|
||||||
|
price.bill.billingFail.noActivePackageVersion=计费失败:计费日无生效套餐版本
|
||||||
|
price.bill.billingFail.noCustomerPackage=计费失败:客户未绑定套餐
|
||||||
|
price.bill.packageNotEnabled=客户已绑定套餐但套餐未启用,无法计费
|
||||||
|
price.quote.packageNotEnabled=客户已绑定套餐但套餐未启用,无法试算
|
||||||
price.bill.export.filterRequired=请先设置列表查询条件后再导出
|
price.bill.export.filterRequired=请先设置列表查询条件后再导出
|
||||||
price.bill.export.taskNotExists=导出任务不存在或已过期
|
price.bill.export.taskNotExists=导出任务不存在或已过期
|
||||||
price.bill.export.tooMany=匹配订单超过上限 {0} 条,请缩小筛选范围
|
price.bill.export.tooMany=匹配订单超过上限 {0} 条,请缩小筛选范围
|
||||||
|
|||||||
@@ -1733,7 +1733,7 @@ public class ArBillServiceImpl implements IArBillService {
|
|||||||
case CATALOG_COUNTRY -> "产品价未匹配国家";
|
case CATALOG_COUNTRY -> "产品价未匹配国家";
|
||||||
case CATALOG_WEIGHT -> "产品价未匹配重量段";
|
case CATALOG_WEIGHT -> "产品价未匹配重量段";
|
||||||
case NO_CUSTOMER_PACKAGE -> "未绑定套餐";
|
case NO_CUSTOMER_PACKAGE -> "未绑定套餐";
|
||||||
case NO_ACTIVE_PACKAGE_VERSION -> "无生效套餐版本";
|
case NO_ACTIVE_PACKAGE_VERSION -> "套餐未启用/无生效版本";
|
||||||
case ALREADY_IN_AR_BILL -> "已生成应收账单";
|
case ALREADY_IN_AR_BILL -> "已生成应收账单";
|
||||||
case PRODUCT_ARCHIVE_MISSING -> "产品档案缺失";
|
case PRODUCT_ARCHIVE_MISSING -> "产品档案缺失";
|
||||||
case PRICE_NOT_HIT -> "未命中价格";
|
case PRICE_NOT_HIT -> "未命中价格";
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ public enum BillTaskFailCode
|
|||||||
{
|
{
|
||||||
return NO_CUSTOMER_PACKAGE.code;
|
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;
|
return NO_ACTIVE_PACKAGE_VERSION.code;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,18 @@ public interface PrcPriceSetDetailMapper
|
|||||||
@Param("countryName") String countryName,
|
@Param("countryName") String countryName,
|
||||||
@Param("zoneCode") String zoneCode);
|
@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(
|
public int countZonedByPsvProductCountry(
|
||||||
@Param("psvId") Long psvId,
|
@Param("psvId") Long psvId,
|
||||||
@Param("productCode") String productCode,
|
@Param("productCode") String productCode,
|
||||||
|
|||||||
+57
-9
@@ -410,12 +410,16 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
|||||||
}
|
}
|
||||||
if (binds == null || binds.isEmpty())
|
if (binds == null || binds.isEmpty())
|
||||||
{
|
{
|
||||||
String msgKey = request instanceof TmsPriceBillRequest
|
if (request instanceof TmsPriceBillRequest)
|
||||||
? "price.bill.noCustomerPackage"
|
{
|
||||||
: "price.quote.noCustomerPackage";
|
// Soft-fail: persist billing_fail_status=4 on the order bill.
|
||||||
throw new ServiceException(MessageUtils.message(msgKey));
|
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 anyActiveVersion = false;
|
||||||
boolean productExcludedBySetProducts = false;
|
boolean productExcludedBySetProducts = false;
|
||||||
boolean attemptedPriceMatch = false;
|
boolean attemptedPriceMatch = false;
|
||||||
@@ -428,10 +432,12 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
|||||||
for (PrcCustomerPriceSet bind : binds)
|
for (PrcCustomerPriceSet bind : binds)
|
||||||
{
|
{
|
||||||
PrcPriceSet set = priceSetMapper.selectPrcPriceSetById(bind.getPriceSetId());
|
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()))
|
if (set == null || !"E".equals(set.getStatus()))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
anyEnabledSet = true;
|
||||||
// Temporal gate: prc_price_set_version.effective_time / expire_time (not bind pr_begindate)
|
// Temporal gate: prc_price_set_version.effective_time / expire_time (not bind pr_begindate)
|
||||||
PrcPriceSetVersion version = versionMapper.selectActiveByPriceSetAt(set.getPriceSetId(), asOf);
|
PrcPriceSetVersion version = versionMapper.selectActiveByPriceSetAt(set.getPriceSetId(), asOf);
|
||||||
if (version == null || version.getPsvId() == null)
|
if (version == null || version.getPsvId() == null)
|
||||||
@@ -560,9 +566,22 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
|||||||
|
|
||||||
if (!anyActiveVersion)
|
if (!anyActiveVersion)
|
||||||
{
|
{
|
||||||
String msgKey = request instanceof TmsPriceBillRequest
|
if (request instanceof TmsPriceBillRequest)
|
||||||
? "price.bill.noActivePackageVersion"
|
{
|
||||||
: "price.quote.noActivePackageVersion";
|
// 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));
|
throw new ServiceException(MessageUtils.message(msgKey));
|
||||||
}
|
}
|
||||||
if (!attemptedPriceMatch && productExcludedBySetProducts)
|
if (!attemptedPriceMatch && productExcludedBySetProducts)
|
||||||
@@ -720,6 +739,14 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
|||||||
{
|
{
|
||||||
return MessageUtils.message("price.bill.billingFail.catalogWeight");
|
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");
|
return MessageUtils.message("price.bill.packagePriceNotHit");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -994,7 +1021,8 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
|||||||
countryCode, countryName, zoneCode, weight);
|
countryCode, countryName, zoneCode, weight);
|
||||||
BigDecimal billWeight = weight;
|
BigDecimal billWeight = weight;
|
||||||
boolean snappedToMinBand = false;
|
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)
|
if (detail == null)
|
||||||
{
|
{
|
||||||
PrcPriceSetDetail minBand = detailMapper.selectFixedMinBand(
|
PrcPriceSetDetail minBand = detailMapper.selectFixedMinBand(
|
||||||
@@ -1007,6 +1035,19 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
|||||||
snappedToMinBand = true;
|
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)
|
if (detail == null)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
@@ -1016,7 +1057,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
BmsProductCountry useCatalog = catalog;
|
BmsProductCountry useCatalog = catalog;
|
||||||
if (snappedToMinBand)
|
if (snappedToMinBand || snappedUpToNextBand)
|
||||||
{
|
{
|
||||||
BmsProductCountry rematched = softMatchCatalog(
|
BmsProductCountry rematched = softMatchCatalog(
|
||||||
productCode, countryCode, countryName, zoneCode, billWeight, bizDate, resolvedZone);
|
productCode, countryCode, countryName, zoneCode, billWeight, bizDate, resolvedZone);
|
||||||
@@ -1039,6 +1080,13 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
|||||||
r.setChargeWeight(weight);
|
r.setChargeWeight(weight);
|
||||||
msg = msg + "; billed at min band " + billWeight.toPlainString() + " (persist original 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.setMatchMessage(msg);
|
||||||
r.setCustomerId(bind.getCustomerId());
|
r.setCustomerId(bind.getCustomerId());
|
||||||
return r;
|
return r;
|
||||||
|
|||||||
+1
-1
@@ -247,7 +247,7 @@ public final class OrderBillExportSupport
|
|||||||
case 24: return "产品价未匹配国家";
|
case 24: return "产品价未匹配国家";
|
||||||
case 25: return "产品价未匹配重量段";
|
case 25: return "产品价未匹配重量段";
|
||||||
case 4: return "未绑定套餐";
|
case 4: return "未绑定套餐";
|
||||||
case 5: return "无生效套餐版本";
|
case 5: return "套餐未启用/无生效版本";
|
||||||
case 6: return "已生成应收账单";
|
case 6: return "已生成应收账单";
|
||||||
case 7: return "产品档案缺失";
|
case 7: return "产品档案缺失";
|
||||||
case 8: return "未命中价格";
|
case 8: return "未命中价格";
|
||||||
|
|||||||
+52
-6
@@ -15,6 +15,7 @@ import java.util.concurrent.ThreadFactory;
|
|||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
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 MIN_THREADS = 2;
|
||||||
public static final int MAX_THREADS = 16;
|
public static final int MAX_THREADS = 16;
|
||||||
private static final int FAIL_DETAIL_MAX = 200;
|
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>();
|
private final ConcurrentHashMap<String, AtomicBoolean> cancelFlags = new ConcurrentHashMap<String, AtomicBoolean>();
|
||||||
|
|
||||||
@@ -81,6 +85,8 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
|||||||
int total = billIds == null ? 0 : billIds.size();
|
int total = billIds == null ? 0 : billIds.size();
|
||||||
AtomicInteger success = new AtomicInteger(0);
|
AtomicInteger success = new AtomicInteger(0);
|
||||||
AtomicInteger fail = new AtomicInteger(0);
|
AtomicInteger fail = new AtomicInteger(0);
|
||||||
|
AtomicInteger done = new AtomicInteger(0);
|
||||||
|
AtomicLong lastProgressAt = new AtomicLong(0L);
|
||||||
StringBuilder failTips = new StringBuilder();
|
StringBuilder failTips = new StringBuilder();
|
||||||
List<Map<String, Object>> failDetails = Collections.synchronizedList(new ArrayList<Map<String, Object>>());
|
List<Map<String, Object>> failDetails = Collections.synchronizedList(new ArrayList<Map<String, Object>>());
|
||||||
int threads = clampThreads(billingProperties == null ? null : billingProperties.getOrderSourceSyncThreads());
|
int threads = clampThreads(billingProperties == null ? null : billingProperties.getOrderSourceSyncThreads());
|
||||||
@@ -89,6 +95,7 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
put(taskId, BatchAddIncomeBillFeeTask.STATUS_RUNNING, 1, "running", null, total, 0, 0);
|
put(taskId, BatchAddIncomeBillFeeTask.STATUS_RUNNING, 1, "running", null, total, 0, 0);
|
||||||
|
lastProgressAt.set(System.currentTimeMillis());
|
||||||
if (total == 0)
|
if (total == 0)
|
||||||
{
|
{
|
||||||
put(taskId, BatchAddIncomeBillFeeTask.STATUS_FAIL, 100, "failed", "no bills", 0, 0, 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);
|
finishCancelled(taskId, request, total, success.get(), fail.get(), threads, failDetails);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int processed = 0;
|
|
||||||
boolean cancelled = false;
|
boolean cancelled = false;
|
||||||
for (int from = 0; from < total; from += PAGE_SIZE)
|
for (int from = 0; from < total; from += PAGE_SIZE)
|
||||||
{
|
{
|
||||||
@@ -135,19 +141,22 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
|||||||
taskId, one, e.getMessage());
|
taskId, one, e.getMessage());
|
||||||
return Boolean.FALSE;
|
return Boolean.FALSE;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
int finished = done.incrementAndGet();
|
||||||
|
maybePublishProgress(taskId, total, finished, success.get(), fail.get(), lastProgressAt);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
invokeAllQuiet(pool, jobs);
|
invokeAllQuiet(pool, jobs);
|
||||||
processed = to;
|
|
||||||
if (isCancelRequested(taskId))
|
if (isCancelRequested(taskId))
|
||||||
{
|
{
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
int percent = Math.min(99, (int) Math.floor(100.0 * processed / total));
|
// Page boundary: always flush latest counts (even if throttle skipped mid-page).
|
||||||
put(taskId, BatchAddIncomeBillFeeTask.STATUS_RUNNING, percent,
|
publishProgress(taskId, total, done.get(), success.get(), fail.get());
|
||||||
"processing " + processed + "/" + total + " page=" + ((from / PAGE_SIZE) + 1),
|
lastProgressAt.set(System.currentTimeMillis());
|
||||||
null, total, success.get(), fail.get());
|
|
||||||
}
|
}
|
||||||
if (cancelled || isCancelRequested(taskId))
|
if (cancelled || isCancelRequested(taskId))
|
||||||
{
|
{
|
||||||
@@ -332,6 +341,43 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
|||||||
return redisCache.getCacheObject(CACHE_KEY_PREFIX + taskId);
|
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
|
private static void invokeAllQuiet(ExecutorService pool, List<Callable<Boolean>> jobs) throws InterruptedException
|
||||||
{
|
{
|
||||||
List<Future<Boolean>> futures = pool.invokeAll(jobs);
|
List<Future<Boolean>> futures = pool.invokeAll(jobs);
|
||||||
|
|||||||
@@ -519,6 +519,37 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
limit 1
|
limit 1
|
||||||
</select>
|
</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 id="countZonedByPsvProductCountry" resultType="int">
|
||||||
select count(1)
|
select count(1)
|
||||||
from prc_price_set_detail
|
from prc_price_set_detail
|
||||||
|
|||||||
@@ -109,6 +109,9 @@ export function cancelLocalRebill(taskId) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** One in-flight poll per taskId — avoids stacked loops that flood XHR and freeze the page. */
|
||||||
|
const localRebillPollControllers = new Map()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Poll local rebill task until terminal status.
|
* Poll local rebill task until terminal status.
|
||||||
* @param {string} taskId
|
* @param {string} taskId
|
||||||
@@ -117,22 +120,48 @@ export function cancelLocalRebill(taskId) {
|
|||||||
export async function pollLocalRebillTask(taskId, onProgress) {
|
export async function pollLocalRebillTask(taskId, onProgress) {
|
||||||
const id = String(taskId || '')
|
const id = String(taskId || '')
|
||||||
if (!id) throw new Error('missing local rebill taskId')
|
if (!id) throw new Error('missing local rebill taskId')
|
||||||
|
|
||||||
|
const prev = localRebillPollControllers.get(id)
|
||||||
|
if (prev) prev.aborted = true
|
||||||
|
const ctrl = { aborted: false }
|
||||||
|
localRebillPollControllers.set(id, ctrl)
|
||||||
|
|
||||||
const started = Date.now()
|
const started = Date.now()
|
||||||
const maxMs = 30 * 60 * 1000
|
const maxMs = 30 * 60 * 1000
|
||||||
while (Date.now() - started < maxMs) {
|
let lastSig = ''
|
||||||
const res = await getLocalRebillTask(id)
|
try {
|
||||||
const task = res.data || {}
|
while (Date.now() - started < maxMs) {
|
||||||
if (typeof onProgress === 'function') onProgress(task)
|
if (ctrl.aborted) {
|
||||||
if (task.status === 'SUCCESS') return { task }
|
const err = new Error('local rebill poll superseded')
|
||||||
if (task.status === 'CANCELLED') return { task, cancelled: true }
|
err.superseded = true
|
||||||
if (task.status === 'FAIL') {
|
throw err
|
||||||
const err = new Error(task.error || task.message || 'local rebill failed')
|
}
|
||||||
err.task = task
|
// Hidden tab: slow down; no need to hammer Redis while user is away.
|
||||||
throw err
|
const hidden = typeof document !== 'undefined' && document.visibilityState === 'hidden'
|
||||||
|
const res = await getLocalRebillTask(id)
|
||||||
|
const task = res.data || {}
|
||||||
|
const st = String(task.status || '').toUpperCase()
|
||||||
|
const sig = [st, task.percent, task.success, task.fail, task.total].join('|')
|
||||||
|
if (typeof onProgress === 'function' && sig !== lastSig) {
|
||||||
|
lastSig = sig
|
||||||
|
onProgress(task)
|
||||||
|
}
|
||||||
|
if (st === 'SUCCESS') return { task }
|
||||||
|
if (st === 'CANCELLED') return { task, cancelled: true }
|
||||||
|
if (st === 'FAIL') {
|
||||||
|
const err = new Error(task.error || task.message || 'local rebill failed')
|
||||||
|
err.task = task
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
// 1s visible / 2.5s hidden — dense 400ms polls were freezing the list page.
|
||||||
|
await new Promise(resolve => setTimeout(resolve, hidden ? 2500 : 1000))
|
||||||
|
}
|
||||||
|
throw new Error('local rebill timeout')
|
||||||
|
} finally {
|
||||||
|
if (localRebillPollControllers.get(id) === ctrl) {
|
||||||
|
localRebillPollControllers.delete(id)
|
||||||
}
|
}
|
||||||
await new Promise(resolve => setTimeout(resolve, 800))
|
|
||||||
}
|
}
|
||||||
throw new Error('local rebill timeout')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -345,7 +345,7 @@ const billingFailStatusOptions = [
|
|||||||
{ label: '产品价未匹配国家', value: 24 },
|
{ label: '产品价未匹配国家', value: 24 },
|
||||||
{ label: '产品价未匹配重量段', value: 25 },
|
{ label: '产品价未匹配重量段', value: 25 },
|
||||||
{ label: '未绑定套餐', value: 4 },
|
{ label: '未绑定套餐', value: 4 },
|
||||||
{ label: '无生效套餐版本', value: 5 },
|
{ label: '套餐未启用/无生效版本', value: 5 },
|
||||||
{ label: '已生成应收账单', value: 6 },
|
{ label: '已生成应收账单', value: 6 },
|
||||||
{ label: '产品档案缺失', value: 7 },
|
{ label: '产品档案缺失', value: 7 },
|
||||||
{ label: '未命中价格', value: 8 },
|
{ label: '未命中价格', value: 8 },
|
||||||
|
|||||||
@@ -363,7 +363,7 @@ const billingFailStatusOptions = [
|
|||||||
{ label: '产品价未匹配国家', value: 24 },
|
{ label: '产品价未匹配国家', value: 24 },
|
||||||
{ label: '产品价未匹配重量段', value: 25 },
|
{ label: '产品价未匹配重量段', value: 25 },
|
||||||
{ label: '未绑定套餐', value: 4 },
|
{ label: '未绑定套餐', value: 4 },
|
||||||
{ label: '无生效套餐版本', value: 5 },
|
{ label: '套餐未启用/无生效版本', value: 5 },
|
||||||
{ label: '已生成应收账单', value: 6 },
|
{ label: '已生成应收账单', value: 6 },
|
||||||
{ label: '产品档案缺失', value: 7 },
|
{ label: '产品档案缺失', value: 7 },
|
||||||
{ label: '未命中价格', value: 8 },
|
{ label: '未命中价格', value: 8 },
|
||||||
@@ -638,10 +638,15 @@ async function resumeRebillTask(taskId) {
|
|||||||
rebillProgressTotal.value = 0
|
rebillProgressTotal.value = 0
|
||||||
rebillProgressSuccess.value = 0
|
rebillProgressSuccess.value = 0
|
||||||
rebillProgressFail.value = 0
|
rebillProgressFail.value = 0
|
||||||
|
let superseded = false
|
||||||
try {
|
try {
|
||||||
const result = await pollLocalRebillTask(taskId, applyRebillProgress)
|
const result = await pollLocalRebillTask(taskId, applyRebillProgress)
|
||||||
finishRebillFromTask(result && result.task, !!(result && result.cancelled))
|
finishRebillFromTask(result && result.task, !!(result && result.cancelled))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (e && e.superseded) {
|
||||||
|
superseded = true
|
||||||
|
return
|
||||||
|
}
|
||||||
const task = e && e.task
|
const task = e && e.task
|
||||||
rebillProgressStatus.value = 'exception'
|
rebillProgressStatus.value = 'exception'
|
||||||
rebillProgressMessage.value = (e && e.message) || '重新计费失败'
|
rebillProgressMessage.value = (e && e.message) || '重新计费失败'
|
||||||
@@ -657,7 +662,8 @@ async function resumeRebillTask(taskId) {
|
|||||||
rebillProgressVisible.value = true
|
rebillProgressVisible.value = true
|
||||||
scheduleHideRebillFloat()
|
scheduleHideRebillFloat()
|
||||||
} finally {
|
} finally {
|
||||||
batchLoading.value = false
|
// Keep loading if a newer poll replaced this one.
|
||||||
|
if (!superseded) batchLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -711,6 +717,7 @@ async function runLocalRebill(payload, confirmText) {
|
|||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
if (batchLoading.value) rebillProgressVisible.value = false
|
if (batchLoading.value) rebillProgressVisible.value = false
|
||||||
}, 600)
|
}, 600)
|
||||||
|
let superseded = false
|
||||||
try {
|
try {
|
||||||
const result = await localRebillAsync(
|
const result = await localRebillAsync(
|
||||||
payload,
|
payload,
|
||||||
@@ -719,6 +726,10 @@ async function runLocalRebill(payload, confirmText) {
|
|||||||
)
|
)
|
||||||
finishRebillFromTask(result && result.task, !!(result && result.cancelled))
|
finishRebillFromTask(result && result.task, !!(result && result.cancelled))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (e && e.superseded) {
|
||||||
|
superseded = true
|
||||||
|
return
|
||||||
|
}
|
||||||
const task = e && e.task
|
const task = e && e.task
|
||||||
rebillProgressStatus.value = 'exception'
|
rebillProgressStatus.value = 'exception'
|
||||||
rebillProgressMessage.value = (e && e.message) || '重新计费失败'
|
rebillProgressMessage.value = (e && e.message) || '重新计费失败'
|
||||||
@@ -734,7 +745,7 @@ async function runLocalRebill(payload, confirmText) {
|
|||||||
rebillProgressVisible.value = true
|
rebillProgressVisible.value = true
|
||||||
scheduleHideRebillFloat()
|
scheduleHideRebillFloat()
|
||||||
} finally {
|
} finally {
|
||||||
batchLoading.value = false
|
if (!superseded) batchLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,11 +44,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="build-choice">
|
<!-- Exclusive quote: only Excel fresh build; hide the mode picker and default IMPORT_FRESH -->
|
||||||
|
<div v-if="!onlyImportFresh" class="build-choice">
|
||||||
<p class="build-choice__tip">创建价格方式</p>
|
<p class="build-choice__tip">创建价格方式</p>
|
||||||
<div class="build-choice__cards">
|
<div class="build-choice__cards">
|
||||||
<button
|
<button
|
||||||
v-if="!onlyImportFresh"
|
|
||||||
type="button"
|
type="button"
|
||||||
class="build-card build-card--pull"
|
class="build-card build-card--pull"
|
||||||
:class="{ selected: isPullBuildMode(priceBuildMode) }"
|
:class="{ selected: isPullBuildMode(priceBuildMode) }"
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
<div class="build-card__desc">按产品库基础价生成套餐产品列表,可再导入 Excel 修改价格</div>
|
<div class="build-card__desc">按产品库基础价生成套餐产品列表,可再导入 Excel 修改价格</div>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="showImportInherit && !onlyImportFresh"
|
v-if="showImportInherit"
|
||||||
type="button"
|
type="button"
|
||||||
class="build-card build-card--import"
|
class="build-card build-card--import"
|
||||||
:class="{ selected: isImportBuildMode(priceBuildMode) }"
|
:class="{ selected: isImportBuildMode(priceBuildMode) }"
|
||||||
@@ -626,7 +626,7 @@ watch(
|
|||||||
() => [onlyImportFresh.value, props.priceBuildMode, props.buildModeReady],
|
() => [onlyImportFresh.value, props.priceBuildMode, props.buildModeReady],
|
||||||
() => {
|
() => {
|
||||||
if (!onlyImportFresh.value || !props.buildModeReady) return
|
if (!onlyImportFresh.value || !props.buildModeReady) return
|
||||||
if (props.priceBuildMode) return
|
if (isImportFreshBuildMode(props.priceBuildMode)) return
|
||||||
if (buildModeLocked.value) return
|
if (buildModeLocked.value) return
|
||||||
emit('update:priceBuildMode', 'IMPORT_FRESH')
|
emit('update:priceBuildMode', 'IMPORT_FRESH')
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user