增加失败原因和优化进度条
This commit is contained in:
@@ -224,6 +224,11 @@ price.bill.billingFail.weight=计费失败:销售价未匹配重量段
|
||||
price.bill.billingFail.catalogProduct=计费失败:产品价未匹配产品
|
||||
price.bill.billingFail.catalogCountry=计费失败:产品价未匹配国家
|
||||
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.taskNotExists=导出任务不存在或已过期
|
||||
price.bill.export.tooMany=匹配订单超过上限 {0} 条,请缩小筛选范围
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
* @param {string} taskId
|
||||
@@ -117,22 +120,48 @@ export function cancelLocalRebill(taskId) {
|
||||
export async function pollLocalRebillTask(taskId, onProgress) {
|
||||
const id = String(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 maxMs = 30 * 60 * 1000
|
||||
while (Date.now() - started < maxMs) {
|
||||
const res = await getLocalRebillTask(id)
|
||||
const task = res.data || {}
|
||||
if (typeof onProgress === 'function') onProgress(task)
|
||||
if (task.status === 'SUCCESS') return { task }
|
||||
if (task.status === 'CANCELLED') return { task, cancelled: true }
|
||||
if (task.status === 'FAIL') {
|
||||
const err = new Error(task.error || task.message || 'local rebill failed')
|
||||
err.task = task
|
||||
throw err
|
||||
let lastSig = ''
|
||||
try {
|
||||
while (Date.now() - started < maxMs) {
|
||||
if (ctrl.aborted) {
|
||||
const err = new Error('local rebill poll superseded')
|
||||
err.superseded = true
|
||||
throw err
|
||||
}
|
||||
// Hidden tab: slow down; no need to hammer Redis while user is away.
|
||||
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: 25 },
|
||||
{ label: '未绑定套餐', value: 4 },
|
||||
{ label: '无生效套餐版本', value: 5 },
|
||||
{ label: '套餐未启用/无生效版本', value: 5 },
|
||||
{ label: '已生成应收账单', value: 6 },
|
||||
{ label: '产品档案缺失', value: 7 },
|
||||
{ label: '未命中价格', value: 8 },
|
||||
|
||||
@@ -363,7 +363,7 @@ const billingFailStatusOptions = [
|
||||
{ label: '产品价未匹配国家', value: 24 },
|
||||
{ label: '产品价未匹配重量段', value: 25 },
|
||||
{ label: '未绑定套餐', value: 4 },
|
||||
{ label: '无生效套餐版本', value: 5 },
|
||||
{ label: '套餐未启用/无生效版本', value: 5 },
|
||||
{ label: '已生成应收账单', value: 6 },
|
||||
{ label: '产品档案缺失', value: 7 },
|
||||
{ label: '未命中价格', value: 8 },
|
||||
@@ -638,10 +638,15 @@ async function resumeRebillTask(taskId) {
|
||||
rebillProgressTotal.value = 0
|
||||
rebillProgressSuccess.value = 0
|
||||
rebillProgressFail.value = 0
|
||||
let superseded = false
|
||||
try {
|
||||
const result = await pollLocalRebillTask(taskId, applyRebillProgress)
|
||||
finishRebillFromTask(result && result.task, !!(result && result.cancelled))
|
||||
} catch (e) {
|
||||
if (e && e.superseded) {
|
||||
superseded = true
|
||||
return
|
||||
}
|
||||
const task = e && e.task
|
||||
rebillProgressStatus.value = 'exception'
|
||||
rebillProgressMessage.value = (e && e.message) || '重新计费失败'
|
||||
@@ -657,7 +662,8 @@ async function resumeRebillTask(taskId) {
|
||||
rebillProgressVisible.value = true
|
||||
scheduleHideRebillFloat()
|
||||
} 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(() => {
|
||||
if (batchLoading.value) rebillProgressVisible.value = false
|
||||
}, 600)
|
||||
let superseded = false
|
||||
try {
|
||||
const result = await localRebillAsync(
|
||||
payload,
|
||||
@@ -719,6 +726,10 @@ async function runLocalRebill(payload, confirmText) {
|
||||
)
|
||||
finishRebillFromTask(result && result.task, !!(result && result.cancelled))
|
||||
} catch (e) {
|
||||
if (e && e.superseded) {
|
||||
superseded = true
|
||||
return
|
||||
}
|
||||
const task = e && e.task
|
||||
rebillProgressStatus.value = 'exception'
|
||||
rebillProgressMessage.value = (e && e.message) || '重新计费失败'
|
||||
@@ -734,7 +745,7 @@ async function runLocalRebill(payload, confirmText) {
|
||||
rebillProgressVisible.value = true
|
||||
scheduleHideRebillFloat()
|
||||
} finally {
|
||||
batchLoading.value = false
|
||||
if (!superseded) batchLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,11 +44,11 @@
|
||||
</div>
|
||||
</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>
|
||||
<div class="build-choice__cards">
|
||||
<button
|
||||
v-if="!onlyImportFresh"
|
||||
type="button"
|
||||
class="build-card build-card--pull"
|
||||
:class="{ selected: isPullBuildMode(priceBuildMode) }"
|
||||
@@ -59,7 +59,7 @@
|
||||
<div class="build-card__desc">按产品库基础价生成套餐产品列表,可再导入 Excel 修改价格</div>
|
||||
</button>
|
||||
<button
|
||||
v-if="showImportInherit && !onlyImportFresh"
|
||||
v-if="showImportInherit"
|
||||
type="button"
|
||||
class="build-card build-card--import"
|
||||
:class="{ selected: isImportBuildMode(priceBuildMode) }"
|
||||
@@ -626,7 +626,7 @@ watch(
|
||||
() => [onlyImportFresh.value, props.priceBuildMode, props.buildModeReady],
|
||||
() => {
|
||||
if (!onlyImportFresh.value || !props.buildModeReady) return
|
||||
if (props.priceBuildMode) return
|
||||
if (isImportFreshBuildMode(props.priceBuildMode)) return
|
||||
if (buildModeLocked.value) return
|
||||
emit('update:priceBuildMode', 'IMPORT_FRESH')
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user