增加进度条
This commit is contained in:
@@ -305,6 +305,14 @@ public class BillingController extends BaseController
|
||||
return success(task);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:orderBill:rebill')")
|
||||
@Log(title = "Order bill local rebill cancel", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/orderBill/rebill/task/{taskId}/cancel")
|
||||
public AjaxResult cancelLocalRebill(@PathVariable("taskId") String taskId)
|
||||
{
|
||||
return success(prcOrderBillService.cancelLocalRebill(taskId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Order no options for remote-search dropdown (bill list multi search).
|
||||
*/
|
||||
|
||||
@@ -232,3 +232,6 @@ price.bill.export.notReady=导出文件尚未生成或已失败
|
||||
price.bill.export.fileMissing=导出文件不存在或路径非法
|
||||
price.set.customer.batch.empty=请选择要绑定的客户
|
||||
price.set.customer.batch.done=批量绑定完成:共 {0} 个客户,新绑定 {1} 个,更换 {2} 个,跳过 {3} 个
|
||||
price.bill.recalc.taskCancelled=本地重新计费已取消
|
||||
price.bill.recalc.taskCancelNotRunning=任务已结束或不存在,无法取消
|
||||
price.bill.recalc.taskIdRequired=任务ID不能为空
|
||||
|
||||
@@ -11,6 +11,7 @@ public class BatchAddIncomeBillFeeTask implements Serializable
|
||||
public static final String STATUS_RUNNING = "RUNNING";
|
||||
public static final String STATUS_SUCCESS = "SUCCESS";
|
||||
public static final String STATUS_FAIL = "FAIL";
|
||||
public static final String STATUS_CANCELLED = "CANCELLED";
|
||||
|
||||
private String taskId;
|
||||
private String status;
|
||||
|
||||
@@ -166,6 +166,9 @@ public interface IPrcOrderBillService
|
||||
/** Poll Redis progress for local rebill task. */
|
||||
BatchAddIncomeBillFeeTask getLocalRebillTask(String taskId);
|
||||
|
||||
/** Cooperative cancel for a running local rebill task. */
|
||||
BatchAddIncomeBillFeeTask cancelLocalRebill(String taskId);
|
||||
|
||||
/** Data-scoped bill_id list for local rebill (same filters as order-bill list). */
|
||||
List<String> selectBillIdsForLocalRebill(PrcOrderBill query);
|
||||
|
||||
|
||||
+19
@@ -972,6 +972,25 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
return localRebillAsyncRunner.get(taskId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BatchAddIncomeBillFeeTask cancelLocalRebill(String taskId)
|
||||
{
|
||||
if (StringUtils.isEmpty(taskId))
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.bill.recalc.taskIdRequired"));
|
||||
}
|
||||
if (localRebillAsyncRunner == null)
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.bill.recalc.taskNotExists"));
|
||||
}
|
||||
BatchAddIncomeBillFeeTask task = localRebillAsyncRunner.requestCancel(taskId.trim());
|
||||
if (task == null)
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.bill.recalc.taskNotExists"));
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String submitOrderBillExport(String exportType, PrcOrderBill query)
|
||||
{
|
||||
|
||||
+108
-2
@@ -6,12 +6,14 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -32,8 +34,9 @@ import com.bms.price.search.TabOrderSyncEsLog;
|
||||
import com.bms.price.service.IPrcOrderBillService;
|
||||
|
||||
/**
|
||||
* 仅根据 prc_order_bill 异步本地重计费(不走 Oracle / 不走 MQ)。
|
||||
* 大批量按 {@link #PAGE_SIZE} 分页处理;每一页在短生命周期线程池中执行。
|
||||
* Async local rebill from prc_order_bill only (no Oracle / MQ).
|
||||
* Large jobs page by {@link #PAGE_SIZE}; each page runs in a short-lived pool.
|
||||
* Cancel is cooperative: current in-flight bills finish, remaining bills are skipped.
|
||||
*/
|
||||
@Component
|
||||
public class PrcOrderBillLocalRebillAsyncRunner
|
||||
@@ -48,6 +51,8 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
public static final int MAX_THREADS = 16;
|
||||
private static final int FAIL_DETAIL_MAX = 200;
|
||||
|
||||
private final ConcurrentHashMap<String, AtomicBoolean> cancelFlags = new ConcurrentHashMap<String, AtomicBoolean>();
|
||||
|
||||
@Autowired
|
||||
@Lazy
|
||||
private IPrcOrderBillService orderBillService;
|
||||
@@ -79,6 +84,7 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
StringBuilder failTips = new StringBuilder();
|
||||
List<Map<String, Object>> failDetails = Collections.synchronizedList(new ArrayList<Map<String, Object>>());
|
||||
int threads = clampThreads(billingProperties == null ? null : billingProperties.getOrderSourceSyncThreads());
|
||||
cancelFlags.computeIfAbsent(taskId, k -> new AtomicBoolean(false));
|
||||
ExecutorService pool = newWorkerPool(threads, "local-rebill");
|
||||
try
|
||||
{
|
||||
@@ -89,9 +95,20 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
saveDoneLog(taskId, request, total, 0, 0, threads, failDetails, "FAIL", "no bills");
|
||||
return;
|
||||
}
|
||||
if (isCancelRequested(taskId))
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (isCancelRequested(taskId))
|
||||
{
|
||||
cancelled = true;
|
||||
break;
|
||||
}
|
||||
int to = Math.min(from + PAGE_SIZE, total);
|
||||
List<String> page = billIds.subList(from, to);
|
||||
List<Callable<Boolean>> jobs = new ArrayList<Callable<Boolean>>(page.size());
|
||||
@@ -99,6 +116,10 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
{
|
||||
final String one = billId;
|
||||
jobs.add(() -> {
|
||||
if (isCancelRequested(taskId))
|
||||
{
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
try
|
||||
{
|
||||
orderBillService.rebillLocalByBillId(one);
|
||||
@@ -118,11 +139,21 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
}
|
||||
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());
|
||||
}
|
||||
if (cancelled || isCancelRequested(taskId))
|
||||
{
|
||||
finishCancelled(taskId, request, total, success.get(), fail.get(), threads, failDetails);
|
||||
return;
|
||||
}
|
||||
String msg = "success=" + success.get() + ", fail=" + fail.get();
|
||||
String err = fail.get() > 0 && failTips.length() > 0 ? failTips.toString() : null;
|
||||
put(taskId, BatchAddIncomeBillFeeTask.STATUS_SUCCESS, 100, msg, err,
|
||||
@@ -135,6 +166,11 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (isCancelRequested(taskId))
|
||||
{
|
||||
finishCancelled(taskId, request, total, success.get(), fail.get(), threads, failDetails);
|
||||
return;
|
||||
}
|
||||
log.error("local rebill aborted taskId={}", taskId, e);
|
||||
String msg = errorMessage(e);
|
||||
put(taskId, BatchAddIncomeBillFeeTask.STATUS_FAIL, 100, "failed", msg,
|
||||
@@ -144,10 +180,62 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
}
|
||||
finally
|
||||
{
|
||||
cancelFlags.remove(taskId);
|
||||
shutdownPool(pool);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request cooperative cancel. Returns current task snapshot (may still be RUNNING until page ends).
|
||||
*/
|
||||
public BatchAddIncomeBillFeeTask requestCancel(String taskId)
|
||||
{
|
||||
if (StringUtils.isEmpty(taskId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
BatchAddIncomeBillFeeTask task = get(taskId);
|
||||
if (task == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
String st = task.getStatus();
|
||||
if (BatchAddIncomeBillFeeTask.STATUS_SUCCESS.equals(st)
|
||||
|| BatchAddIncomeBillFeeTask.STATUS_FAIL.equals(st)
|
||||
|| BatchAddIncomeBillFeeTask.STATUS_CANCELLED.equals(st))
|
||||
{
|
||||
return task;
|
||||
}
|
||||
cancelFlags.computeIfAbsent(taskId, k -> new AtomicBoolean(false)).set(true);
|
||||
// Keep RUNNING so clients keep polling until finishCancelled writes CANCELLED.
|
||||
put(taskId, BatchAddIncomeBillFeeTask.STATUS_RUNNING,
|
||||
Math.max(task.getPercent(), 1),
|
||||
"cancelling",
|
||||
null,
|
||||
task.getTotal(), task.getSuccess(), task.getFail());
|
||||
log.info("local rebill cancel requested taskId={} success={} fail={} total={}",
|
||||
taskId, task.getSuccess(), task.getFail(), task.getTotal());
|
||||
return get(taskId);
|
||||
}
|
||||
|
||||
private void finishCancelled(String taskId, LocalRebillRequest request, int total, int ok, int fail,
|
||||
int threads, List<Map<String, Object>> failDetails)
|
||||
{
|
||||
int percent = total <= 0 ? 100 : Math.min(100, (int) Math.floor(100.0 * (ok + fail) / total));
|
||||
String msg = "cancelled success=" + ok + ", fail=" + fail;
|
||||
put(taskId, BatchAddIncomeBillFeeTask.STATUS_CANCELLED, Math.max(percent, 1), msg,
|
||||
"cancelled by user", total, ok, fail);
|
||||
saveDoneLog(taskId, request, total, ok, fail, threads, failDetails,
|
||||
EsBizLogSupport.STATUS_FAIL, "cancelled by user");
|
||||
log.info("local rebill cancelled taskId={} total={} success={} fail={}", taskId, total, ok, fail);
|
||||
}
|
||||
|
||||
private boolean isCancelRequested(String taskId)
|
||||
{
|
||||
AtomicBoolean flag = cancelFlags.get(taskId);
|
||||
return flag != null && flag.get();
|
||||
}
|
||||
|
||||
private void saveDoneLog(String taskId, LocalRebillRequest request, int total, int ok, int fail,
|
||||
int threads, List<Map<String, Object>> failDetails, String status, String errorMessage)
|
||||
{
|
||||
@@ -209,6 +297,24 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
public void put(String taskId, String status, int percent, String message, String error,
|
||||
int total, int success, int fail)
|
||||
{
|
||||
BatchAddIncomeBillFeeTask existing = get(taskId);
|
||||
if (existing != null
|
||||
&& BatchAddIncomeBillFeeTask.STATUS_CANCELLED.equals(existing.getStatus())
|
||||
&& !BatchAddIncomeBillFeeTask.STATUS_CANCELLED.equals(status))
|
||||
{
|
||||
// Keep cancelled: do not let a late RUNNING overwrite after user cancel.
|
||||
BatchAddIncomeBillFeeTask keep = new BatchAddIncomeBillFeeTask();
|
||||
keep.setTaskId(taskId);
|
||||
keep.setStatus(BatchAddIncomeBillFeeTask.STATUS_CANCELLED);
|
||||
keep.setPercent(Math.max(existing.getPercent(), percent));
|
||||
keep.setMessage(existing.getMessage());
|
||||
keep.setError(existing.getError());
|
||||
keep.setTotal(total > 0 ? total : existing.getTotal());
|
||||
keep.setSuccess(Math.max(existing.getSuccess(), success));
|
||||
keep.setFail(Math.max(existing.getFail(), fail));
|
||||
redisCache.setCacheObject(CACHE_KEY_PREFIX + taskId, keep, TTL_MINUTES, TimeUnit.MINUTES);
|
||||
return;
|
||||
}
|
||||
BatchAddIncomeBillFeeTask task = new BatchAddIncomeBillFeeTask();
|
||||
task.setTaskId(taskId);
|
||||
task.setStatus(status);
|
||||
|
||||
@@ -102,23 +102,29 @@ export function getLocalRebillTask(taskId) {
|
||||
})
|
||||
}
|
||||
|
||||
export function cancelLocalRebill(taskId) {
|
||||
return request({
|
||||
url: '/price/billing/orderBill/rebill/task/' + taskId + '/cancel',
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Async local rebill; poll until done.
|
||||
* @param {object} data LocalRebillRequest
|
||||
* Poll local rebill task until terminal status.
|
||||
* @param {string} taskId
|
||||
* @param {(task: object) => void} [onProgress]
|
||||
*/
|
||||
export async function localRebillAsync(data, onProgress) {
|
||||
const submitRes = await submitLocalRebill(data)
|
||||
const d = submitRes && submitRes.data
|
||||
const taskId = d && (d.taskId || d)
|
||||
if (!taskId) throw new Error('missing local rebill taskId')
|
||||
export async function pollLocalRebillTask(taskId, onProgress) {
|
||||
const id = String(taskId || '')
|
||||
if (!id) throw new Error('missing local rebill taskId')
|
||||
const started = Date.now()
|
||||
const maxMs = 30 * 60 * 1000
|
||||
while (Date.now() - started < maxMs) {
|
||||
const res = await getLocalRebillTask(String(taskId))
|
||||
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
|
||||
@@ -129,6 +135,21 @@ export async function localRebillAsync(data, onProgress) {
|
||||
throw new Error('local rebill timeout')
|
||||
}
|
||||
|
||||
/**
|
||||
* Async local rebill; poll until done.
|
||||
* @param {object} data LocalRebillRequest
|
||||
* @param {(task: object) => void} [onProgress]
|
||||
* @param {{ getTaskId?: (taskId: string) => void }} [options]
|
||||
*/
|
||||
export async function localRebillAsync(data, onProgress, options) {
|
||||
const submitRes = await submitLocalRebill(data)
|
||||
const d = submitRes && submitRes.data
|
||||
const taskId = d && (d.taskId || d)
|
||||
if (!taskId) throw new Error('missing local rebill taskId')
|
||||
if (options && typeof options.getTaskId === 'function') options.getTaskId(String(taskId))
|
||||
return pollLocalRebillTask(String(taskId), onProgress)
|
||||
}
|
||||
|
||||
/** Receivable bill detail */
|
||||
export function getBill(billId) {
|
||||
return request({
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
<template>
|
||||
<div class="app-container order-bill-query">
|
||||
<div v-if="rebillFloatVisible" class="retry-status-float">
|
||||
<el-tooltip :content="rebillFloatTooltip" placement="bottom">
|
||||
<el-button class="retry-status-button" type="primary" plain size="small" @click="openRebillProgress()">
|
||||
<span class="retry-progress-ring" :style="{ '--retry-progress': rebillProgressDegree + 'deg' }" aria-hidden="true">
|
||||
<span class="retry-progress-value">{{ rebillProgressText }}</span>
|
||||
</span>
|
||||
<span class="retry-status-text">
|
||||
<strong>{{ rebillFloatTitle }}</strong>
|
||||
<small>{{ rebillFloatSummary }}</small>
|
||||
</span>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<basic-table
|
||||
ref="tableRef"
|
||||
:columns="columns"
|
||||
@@ -161,13 +175,12 @@
|
||||
</basic-table>
|
||||
<el-dialog
|
||||
v-model="rebillProgressVisible"
|
||||
title="本地重新计费"
|
||||
:title="rebillDialogTitle"
|
||||
width="480px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
:close-on-click-modal="!batchLoading"
|
||||
:close-on-press-escape="!batchLoading"
|
||||
:show-close="!batchLoading"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="true"
|
||||
:show-close="true"
|
||||
>
|
||||
<el-progress
|
||||
:percentage="rebillProgressPercent"
|
||||
@@ -182,10 +195,17 @@
|
||||
/ 成功 {{ rebillProgressSuccess }}
|
||||
/ 失败 {{ rebillProgressFail }}
|
||||
</div>
|
||||
<p v-if="batchLoading" class="rebill-progress__hint">请勿关闭页面,大量订单重新计费可能需要一些时间</p>
|
||||
<p v-if="batchLoading" class="rebill-progress__hint">计费在后台进行,可收起弹窗继续操作;请勿关闭或刷新本页</p>
|
||||
<template #footer>
|
||||
<el-button v-if="!batchLoading" type="primary" @click="rebillProgressVisible = false">关闭</el-button>
|
||||
<el-button v-else disabled>执行中...</el-button>
|
||||
<el-button v-if="batchLoading" @click="rebillProgressVisible = false">收起</el-button>
|
||||
<el-button
|
||||
v-if="batchLoading"
|
||||
type="danger"
|
||||
plain
|
||||
:loading="rebillCancelLoading"
|
||||
@click="handleCancelRebill"
|
||||
>取消计费</el-button>
|
||||
<el-button v-else type="primary" @click="rebillProgressVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<order-source-sync-log-drawer ref="logDrawerRef" />
|
||||
@@ -201,7 +221,7 @@
|
||||
|
||||
<script setup name="OrderBillQuery">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { listOrderBill, localRebillAsync } from '@/api/price/billing'
|
||||
import { listOrderBill, localRebillAsync, pollLocalRebillTask, cancelLocalRebill, getLocalRebillTask } from '@/api/price/billing'
|
||||
import { exportAsyncFromTable } from '@/utils/excelExport'
|
||||
import ExcelExportFieldDialog from '@/components/ExcelExportFieldDialog/index.vue'
|
||||
import { ORDER_BILL_EXPORT_FIELDS } from '@/utils/exportFieldDefs'
|
||||
@@ -214,6 +234,7 @@ const logDrawerRef = ref()
|
||||
const productCheckDrawerRef = ref()
|
||||
const checkLoading = ref(false)
|
||||
const batchLoading = ref(false)
|
||||
const rebillFinished = ref(false)
|
||||
const rebillProgressVisible = ref(false)
|
||||
const rebillProgressPercent = ref(0)
|
||||
const rebillProgressMessage = ref('')
|
||||
@@ -221,6 +242,69 @@ const rebillProgressStatus = ref(undefined)
|
||||
const rebillProgressTotal = ref(0)
|
||||
const rebillProgressSuccess = ref(0)
|
||||
const rebillProgressFail = ref(0)
|
||||
const rebillTaskId = ref('')
|
||||
const rebillCancelLoading = ref(false)
|
||||
let rebillFloatHideTimer = null
|
||||
const REBILL_TASK_STORAGE_KEY = 'price:orderBill:localRebillTaskId'
|
||||
|
||||
function saveRebillTaskId(taskId) {
|
||||
rebillTaskId.value = taskId ? String(taskId) : ''
|
||||
try {
|
||||
if (rebillTaskId.value) sessionStorage.setItem(REBILL_TASK_STORAGE_KEY, rebillTaskId.value)
|
||||
else sessionStorage.removeItem(REBILL_TASK_STORAGE_KEY)
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
function clearRebillTaskId() {
|
||||
saveRebillTaskId('')
|
||||
}
|
||||
|
||||
function readStoredRebillTaskId() {
|
||||
try {
|
||||
return String(sessionStorage.getItem(REBILL_TASK_STORAGE_KEY) || '').trim()
|
||||
} catch (e) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
const rebillFloatVisible = computed(() => batchLoading.value || rebillFinished.value)
|
||||
const rebillProgressDegree = computed(() => Math.round(((rebillProgressPercent.value || 0) / 100) * 360))
|
||||
const rebillProgressText = computed(() => (rebillProgressPercent.value || 0) + '%')
|
||||
const rebillDialogTitle = computed(() => {
|
||||
if (batchLoading.value) return '异步重新计费进行中'
|
||||
if (rebillFinished.value) return '异步重新计费完成'
|
||||
return '本地重新计费'
|
||||
})
|
||||
const rebillFloatTitle = computed(() => {
|
||||
if (batchLoading.value) return '异步重新计费中'
|
||||
if (rebillProgressStatus.value === 'exception') return '重新计费失败'
|
||||
if (rebillProgressStatus.value === 'warning' && String(rebillProgressMessage.value || '').includes('已取消')) return '重新计费已取消'
|
||||
if (rebillFinished.value) return '重新计费完成'
|
||||
return '异步重新计费'
|
||||
})
|
||||
const rebillFloatSummary = computed(() =>
|
||||
'成功 ' + (rebillProgressSuccess.value || 0) +
|
||||
' / 失败 ' + (rebillProgressFail.value || 0) +
|
||||
' / 总数 ' + (rebillProgressTotal.value || 0)
|
||||
)
|
||||
const rebillFloatTooltip = computed(() => rebillProgressMessage.value || '点击查看详情')
|
||||
|
||||
function openRebillProgress() {
|
||||
rebillProgressVisible.value = true
|
||||
}
|
||||
|
||||
function scheduleHideRebillFloat() {
|
||||
if (rebillFloatHideTimer) {
|
||||
clearTimeout(rebillFloatHideTimer)
|
||||
rebillFloatHideTimer = null
|
||||
}
|
||||
rebillFloatHideTimer = window.setTimeout(() => {
|
||||
if (!batchLoading.value && rebillFinished.value) {
|
||||
rebillFinished.value = false
|
||||
}
|
||||
rebillFloatHideTimer = null
|
||||
}, 8000)
|
||||
}
|
||||
const tableHeight = ref(480)
|
||||
let tableResizeObserver = null
|
||||
const MAX_ORDER_IDENTIFIERS = 1000
|
||||
@@ -504,49 +588,64 @@ function handleCheckProducts() {
|
||||
}
|
||||
}
|
||||
|
||||
async function runLocalRebill(payload, confirmText) {
|
||||
try {
|
||||
await ElMessageBox.confirm(confirmText, '二次确认', { type: 'warning' })
|
||||
} catch (e) {
|
||||
return
|
||||
function applyRebillProgress(t) {
|
||||
const total = Number(t.total) || 0
|
||||
const success = Number(t.success) || 0
|
||||
const fail = Number(t.fail) || 0
|
||||
rebillProgressPercent.value = Math.min(100, Number(t.percent) || 0)
|
||||
rebillProgressTotal.value = total
|
||||
rebillProgressSuccess.value = success
|
||||
rebillProgressFail.value = fail
|
||||
if (t.status === 'PENDING') {
|
||||
rebillProgressMessage.value = '排队中…'
|
||||
} else if (t.status === 'RUNNING' && String(t.message || '').startsWith('cancelling')) {
|
||||
rebillProgressMessage.value = '正在取消,等待当前批次结束… ' + success + '/' + total
|
||||
} else if (t.status === 'RUNNING') {
|
||||
rebillProgressMessage.value = '正在重新计费 ' + success + '/' + total +
|
||||
(fail ? ('(失败 ' + fail + ')') : '')
|
||||
}
|
||||
}
|
||||
|
||||
function finishRebillFromTask(task, cancelled) {
|
||||
const success = Number(task && task.success) || 0
|
||||
const fail = Number(task && task.fail) || 0
|
||||
rebillProgressPercent.value = Math.min(100, Number(task && task.percent) || 100)
|
||||
rebillProgressStatus.value = cancelled ? 'warning' : (fail > 0 ? 'warning' : 'success')
|
||||
rebillProgressTotal.value = Number(task && task.total) || rebillProgressTotal.value
|
||||
rebillProgressSuccess.value = success
|
||||
rebillProgressFail.value = fail
|
||||
rebillProgressMessage.value = cancelled
|
||||
? ('已取消:成功 ' + success + ' / 失败 ' + fail)
|
||||
: ('完成:成功 ' + success + ' / 失败 ' + fail)
|
||||
rebillFinished.value = true
|
||||
clearRebillTaskId()
|
||||
tableRef.value?.refresh?.()
|
||||
if (cancelled) ElMessage.warning(rebillProgressMessage.value)
|
||||
else ElMessage.success(rebillProgressMessage.value)
|
||||
scheduleHideRebillFloat()
|
||||
}
|
||||
|
||||
async function resumeRebillTask(taskId) {
|
||||
if (!taskId || batchLoading.value) return
|
||||
batchLoading.value = true
|
||||
rebillProgressVisible.value = true
|
||||
rebillFinished.value = false
|
||||
rebillCancelLoading.value = false
|
||||
saveRebillTaskId(taskId)
|
||||
rebillProgressVisible.value = false
|
||||
rebillProgressPercent.value = 0
|
||||
rebillProgressStatus.value = undefined
|
||||
rebillProgressMessage.value = '任务提交中…'
|
||||
rebillProgressMessage.value = '恢复进度中…'
|
||||
rebillProgressTotal.value = 0
|
||||
rebillProgressSuccess.value = 0
|
||||
rebillProgressFail.value = 0
|
||||
try {
|
||||
const { task } = await localRebillAsync(payload, (t) => {
|
||||
const total = Number(t.total) || 0
|
||||
const success = Number(t.success) || 0
|
||||
const fail = Number(t.fail) || 0
|
||||
rebillProgressPercent.value = Math.min(100, Number(t.percent) || 0)
|
||||
rebillProgressTotal.value = total
|
||||
rebillProgressSuccess.value = success
|
||||
rebillProgressFail.value = fail
|
||||
if (t.status === 'PENDING') {
|
||||
rebillProgressMessage.value = '排队中…'
|
||||
} else if (t.status === 'RUNNING') {
|
||||
rebillProgressMessage.value = '正在重新计费 ' + success + '/' + total +
|
||||
(fail ? ('(失败 ' + fail + ')') : '')
|
||||
}
|
||||
})
|
||||
const success = Number(task && task.success) || 0
|
||||
const fail = Number(task && task.fail) || 0
|
||||
rebillProgressPercent.value = 100
|
||||
rebillProgressStatus.value = fail > 0 ? 'warning' : 'success'
|
||||
rebillProgressTotal.value = Number(task && task.total) || rebillProgressTotal.value
|
||||
rebillProgressSuccess.value = success
|
||||
rebillProgressFail.value = fail
|
||||
rebillProgressMessage.value = '完成:成功 ' + success + ' / 失败 ' + fail
|
||||
tableRef.value?.refresh?.()
|
||||
const result = await pollLocalRebillTask(taskId, applyRebillProgress)
|
||||
finishRebillFromTask(result && result.task, !!(result && result.cancelled))
|
||||
} catch (e) {
|
||||
const task = e && e.task
|
||||
rebillProgressStatus.value = 'exception'
|
||||
rebillProgressMessage.value = (e && e.message) || '重新计费失败'
|
||||
rebillFinished.value = true
|
||||
if (task) {
|
||||
rebillProgressPercent.value = Math.min(100, Number(task.percent) || 100)
|
||||
rebillProgressTotal.value = Number(task.total) || 0
|
||||
@@ -554,11 +653,119 @@ async function runLocalRebill(payload, confirmText) {
|
||||
rebillProgressFail.value = Number(task.fail) || 0
|
||||
tableRef.value?.refresh?.()
|
||||
}
|
||||
clearRebillTaskId()
|
||||
rebillProgressVisible.value = true
|
||||
scheduleHideRebillFloat()
|
||||
} finally {
|
||||
batchLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function tryRestoreRebillProgress() {
|
||||
const taskId = readStoredRebillTaskId()
|
||||
if (!taskId) return
|
||||
try {
|
||||
const res = await getLocalRebillTask(taskId)
|
||||
const task = (res && res.data) || {}
|
||||
const st = String(task.status || '').toUpperCase()
|
||||
if (st === 'PENDING' || st === 'RUNNING') {
|
||||
applyRebillProgress(task)
|
||||
resumeRebillTask(taskId)
|
||||
return
|
||||
}
|
||||
if (st === 'SUCCESS' || st === 'CANCELLED' || st === 'FAIL') {
|
||||
clearRebillTaskId()
|
||||
return
|
||||
}
|
||||
clearRebillTaskId()
|
||||
} catch (e) {
|
||||
clearRebillTaskId()
|
||||
}
|
||||
}
|
||||
|
||||
async function runLocalRebill(payload, confirmText) {
|
||||
if (batchLoading.value) {
|
||||
openRebillProgress()
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(confirmText, '二次确认', { type: 'warning' })
|
||||
} catch (e) {
|
||||
return
|
||||
}
|
||||
if (rebillFloatHideTimer) {
|
||||
clearTimeout(rebillFloatHideTimer)
|
||||
rebillFloatHideTimer = null
|
||||
}
|
||||
batchLoading.value = true
|
||||
rebillFinished.value = false
|
||||
rebillCancelLoading.value = false
|
||||
clearRebillTaskId()
|
||||
rebillProgressVisible.value = true
|
||||
rebillProgressPercent.value = 0
|
||||
rebillProgressStatus.value = undefined
|
||||
rebillProgressMessage.value = '任务提交中…'
|
||||
rebillProgressTotal.value = 0
|
||||
rebillProgressSuccess.value = 0
|
||||
rebillProgressFail.value = 0
|
||||
window.setTimeout(() => {
|
||||
if (batchLoading.value) rebillProgressVisible.value = false
|
||||
}, 600)
|
||||
try {
|
||||
const result = await localRebillAsync(
|
||||
payload,
|
||||
applyRebillProgress,
|
||||
{ getTaskId: (id) => { saveRebillTaskId(id) } }
|
||||
)
|
||||
finishRebillFromTask(result && result.task, !!(result && result.cancelled))
|
||||
} catch (e) {
|
||||
const task = e && e.task
|
||||
rebillProgressStatus.value = 'exception'
|
||||
rebillProgressMessage.value = (e && e.message) || '重新计费失败'
|
||||
rebillFinished.value = true
|
||||
if (task) {
|
||||
rebillProgressPercent.value = Math.min(100, Number(task.percent) || 100)
|
||||
rebillProgressTotal.value = Number(task.total) || 0
|
||||
rebillProgressSuccess.value = Number(task.success) || 0
|
||||
rebillProgressFail.value = Number(task.fail) || 0
|
||||
tableRef.value?.refresh?.()
|
||||
}
|
||||
clearRebillTaskId()
|
||||
rebillProgressVisible.value = true
|
||||
scheduleHideRebillFloat()
|
||||
} finally {
|
||||
batchLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCancelRebill() {
|
||||
if (!batchLoading.value || !rebillTaskId.value || rebillCancelLoading.value) return
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'确认取消当前重新计费?已计费完成的订单会保留,未开始的将停止。',
|
||||
'取消计费',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
} catch (e) {
|
||||
return
|
||||
}
|
||||
rebillCancelLoading.value = true
|
||||
try {
|
||||
const res = await cancelLocalRebill(rebillTaskId.value)
|
||||
const task = (res && res.data) || {}
|
||||
rebillProgressPercent.value = Math.min(100, Number(task.percent) || rebillProgressPercent.value || 0)
|
||||
rebillProgressTotal.value = Number(task.total) || rebillProgressTotal.value
|
||||
rebillProgressSuccess.value = Number(task.success) || rebillProgressSuccess.value
|
||||
rebillProgressFail.value = Number(task.fail) || rebillProgressFail.value
|
||||
rebillProgressMessage.value = '取消请求已提交,等待当前批次结束…'
|
||||
ElMessage.success('已提交取消')
|
||||
} catch (e) {
|
||||
// global interceptor shows error
|
||||
} finally {
|
||||
rebillCancelLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openFilterRebill() {
|
||||
const filter = buildRebillFilter()
|
||||
const identifiers = filter.orderIdentifiers || []
|
||||
@@ -645,6 +852,7 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
window.addEventListener('resize', updateTableHeight)
|
||||
tryRestoreRebillProgress()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@@ -653,6 +861,10 @@ onBeforeUnmount(() => {
|
||||
tableResizeObserver.disconnect()
|
||||
tableResizeObserver = null
|
||||
}
|
||||
if (rebillFloatHideTimer) {
|
||||
clearTimeout(rebillFloatHideTimer)
|
||||
rebillFloatHideTimer = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -666,4 +878,64 @@ onBeforeUnmount(() => {
|
||||
.rebill-progress__msg { margin-top: 12px; color: var(--el-text-color-regular); }
|
||||
.rebill-progress__text { margin-top: 8px; font-size: 13px; color: var(--el-text-color-secondary); }
|
||||
.rebill-progress__hint { margin-top: 12px; font-size: 12px; color: var(--el-color-warning); }
|
||||
.order-bill-query .retry-status-float {
|
||||
position: fixed;
|
||||
top: 92px;
|
||||
right: 24px;
|
||||
z-index: 3001;
|
||||
}
|
||||
.order-bill-query .retry-status-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 200px;
|
||||
min-height: 68px;
|
||||
padding: 6px 16px 6px 7px;
|
||||
border-radius: 36px;
|
||||
}
|
||||
.order-bill-query .retry-progress-ring {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 54px;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
border-radius: 50%;
|
||||
background: conic-gradient(
|
||||
var(--el-color-primary) 0deg var(--retry-progress, 45deg),
|
||||
var(--el-color-primary-light-8) var(--retry-progress, 45deg) 360deg
|
||||
);
|
||||
}
|
||||
.order-bill-query .retry-progress-ring::after {
|
||||
position: absolute;
|
||||
inset: 5px;
|
||||
content: '';
|
||||
border-radius: 50%;
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
.order-bill-query .retry-progress-value {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: var(--el-color-primary);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
}
|
||||
.order-bill-query .retry-status-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 3px;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.order-bill-query .retry-status-text strong {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.order-bill-query .retry-status-text small {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 11px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user