订单列表重新计费优化

This commit is contained in:
wuweihua
2026-10-21 10:13:01 +08:00
parent be3856a59c
commit d7f7ebd981
9 changed files with 659 additions and 41 deletions
@@ -43,6 +43,7 @@ import com.bms.price.domain.vo.BatchAddIncomeBillFeeRequest;
import com.bms.price.domain.vo.BillingTaskSubmitRequest;
import com.bms.price.domain.vo.ChargeWeightImportRow;
import com.bms.price.domain.vo.ChargeWeightTemplateRequest;
import com.bms.price.domain.vo.LocalRebillRequest;
import com.bms.price.domain.vo.RecalcBillPriceRequest;
import com.bms.price.domain.vo.UpdateChargeWeightRequest;
import com.bms.price.domain.vo.UpdateIncomeBillFeeRequest;
@@ -197,6 +198,30 @@ public class BillingController extends BaseController
return success(orderBillProductCheckService.listOrdersByProduct(query));
}
/**
* Order-list rebill from local prc_order_bill only (no Oracle TAB_ORDER).
* Poll {@code GET /orderBill/rebill/task/{taskId}}.
*/
@PreAuthorize("@ss.hasPermi('price:orderBill:rebill')")
@Log(title = "Order bill local rebill", businessType = BusinessType.UPDATE)
@PostMapping("/orderBill/rebill")
public AjaxResult submitLocalRebill(@RequestBody LocalRebillRequest request)
{
return successTask(prcOrderBillService.submitLocalRebill(request));
}
@PreAuthorize("@ss.hasPermi('price:orderBill:rebill')")
@GetMapping("/orderBill/rebill/task/{taskId}")
public AjaxResult getLocalRebillTask(@PathVariable("taskId") String taskId)
{
BatchAddIncomeBillFeeTask task = prcOrderBillService.getLocalRebillTask(taskId);
if (task == null)
{
return error(MessageUtils.message("price.bill.recalc.taskNotExists"));
}
return success(task);
}
/**
* Order no options for remote-search dropdown (bill list multi search).
*/
@@ -0,0 +1,29 @@
package com.bms.price.domain.vo;
import java.io.Serializable;
import java.util.List;
import lombok.Data;
/**
* Order-list local rebill: use prc_order_bill only (no Oracle TAB_ORDER).
* scopeMode SELECTED = billIds; ALL = filters (orderIdentifiers / customer / bizDate...).
*/
@Data
public class LocalRebillRequest implements Serializable
{
private static final long serialVersionUID = 1L;
/** ALL | SELECTED */
private String scopeMode;
private List<String> billIds;
private List<String> orderNos;
private List<String> trackingNos;
private List<String> transferNos;
private List<String> orderIdentifiers;
private List<String> customerCodes;
private List<String> productCodes;
private String countryCode;
private String creditArrearsStatus;
private String beginBizDate;
private String endBizDate;
}
@@ -21,6 +21,12 @@ public interface PrcOrderBillMapper
public List<PrcOrderBill> selectPrcOrderBillList(PrcOrderBill bill);
/** bill_id only (same filters / data-scope as list), for large local rebill. */
List<String> selectPrcOrderBillIds(PrcOrderBill bill);
/** Count matching bills (same filters / data-scope as list). */
int countPrcOrderBillIds(PrcOrderBill bill);
/** Distinct customer+product groups for product-check (order counts in filter window). */
List<Map<String, Object>> selectDistinctProductGroups(PrcOrderBill bill);
@@ -10,6 +10,7 @@ import com.bms.price.domain.vo.BatchAddIncomeBillFeePreview;
import com.bms.price.domain.vo.BatchAddIncomeBillFeeRequest;
import com.bms.price.domain.vo.ChargeWeightImportRow;
import com.bms.price.domain.vo.ChargeWeightTemplateRequest;
import com.bms.price.domain.vo.LocalRebillRequest;
import com.bms.price.domain.vo.RecalcBillPriceRequest;
import com.bms.price.domain.vo.UpdateIncomeBillFeeRequest;
import com.bms.price.domain.vo.TmsPriceBillRequest;
@@ -135,4 +136,25 @@ public interface IPrcOrderBillService
* Requires existing prc_order_bill; skips AR editable guard.
*/
void recalcBillPriceByBillId(String billId, String mode);
/**
* Full rebill from local prc_order_bill only (no Oracle upstream).
* Rebuilds TmsPriceBillRequest from the bill row then calls {@link #bill}.
*/
TmsPriceQuoteResult rebillLocalByBillId(String billId);
/**
* Async local rebill by selected billIds or list filters. Returns taskId for progress poll.
* Large sets: only bill_id loaded, then processed in pages of 500 with parallel workers.
*/
String submitLocalRebill(LocalRebillRequest request);
/** Poll Redis progress for local rebill task. */
BatchAddIncomeBillFeeTask getLocalRebillTask(String taskId);
/** Data-scoped bill_id list for local rebill (same filters as order-bill list). */
List<String> selectBillIdsForLocalRebill(PrcOrderBill query);
/** Data-scoped count for local rebill. */
int countBillIdsForLocalRebill(PrcOrderBill query);
}
@@ -50,11 +50,13 @@ import com.bms.price.domain.vo.BatchAddIncomeBillFeePreview;
import com.bms.price.domain.vo.BatchAddIncomeBillFeeRequest;
import com.bms.price.domain.vo.ChargeWeightImportRow;
import com.bms.price.domain.vo.ChargeWeightTemplateRequest;
import com.bms.price.domain.vo.LocalRebillRequest;
import com.bms.price.domain.vo.RecalcBillPriceRequest;
import com.bms.price.domain.vo.UpdateIncomeBillFeeRequest;
import com.bms.price.task.PrcBatchAddIncomeFeeAsyncRunner;
import com.bms.price.task.PrcChargeWeightImportAsyncRunner;
import com.bms.price.task.PrcChargeWeightImportAsyncRunner.ChargeWeightImportWorkItem;
import com.bms.price.task.PrcOrderBillLocalRebillAsyncRunner;
import com.bms.price.task.PrcRecalcBillPriceAsyncRunner;
import com.bms.price.domain.vo.TmsPriceBillRequest;
import com.bms.price.domain.vo.TmsOrderBillStatusResult;
@@ -143,6 +145,9 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
@Autowired
private PrcRecalcBillPriceAsyncRunner recalcBillPriceAsyncRunner;
@Autowired
private PrcOrderBillLocalRebillAsyncRunner localRebillAsyncRunner;
@Autowired
private IBmsBizRuleService bizRuleService;
@@ -150,6 +155,8 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
private static final int BATCH_ADD_FEE_PREVIEW_LIMIT = 20;
private static final int CHARGE_WEIGHT_IMPORT_MAX = 5000;
private static final int RECALC_PRICE_MAX = 5000;
/** Soft max for one local-rebill job (id-only load + paged parallel). */
private static final int LOCAL_REBILL_MAX = 100000;
@Override
@Transactional(rollbackFor = Exception.class)
@@ -694,6 +701,168 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
persistProductOrRedlineOnly(bill, result, productMode);
}
@Override
@Transactional(rollbackFor = Exception.class)
public TmsPriceQuoteResult rebillLocalByBillId(String billId)
{
if (StringUtils.isEmpty(billId))
{
throw new ServiceException(MessageUtils.message("price.bill.billIdRequired"));
}
PrcOrderBill bill = orderBillMapper.selectPrcOrderBillById(billId.trim());
if (bill == null)
{
throw new ServiceException(MessageUtils.message("price.bill.notFound"));
}
if (StringUtils.isEmpty(bill.getOrderNo()))
{
throw new ServiceException(MessageUtils.message("price.bill.orderNoRequired"));
}
if (bill.getBizDate() == null)
{
throw new ServiceException(MessageUtils.message("price.bill.bizDateRequired"));
}
TmsPriceBillRequest req = buildBillRequestFromBill(bill);
// Full bill() path: AR guard, weight rules, quote, persist (local snapshot only).
return self.bill(req);
}
@Override
public String submitLocalRebill(LocalRebillRequest request)
{
if (request == null)
{
throw new ServiceException(MessageUtils.message("price.bill.recalc.filterRequired"));
}
PrcOrderBill query = buildLocalRebillQuery(request);
int total = self.countBillIdsForLocalRebill(query);
if (total <= 0)
{
throw new ServiceException(MessageUtils.message("price.bill.recalc.noMatch"));
}
if (total > LOCAL_REBILL_MAX)
{
throw new ServiceException(MessageUtils.message("price.bill.recalc.tooMany",
Integer.valueOf(LOCAL_REBILL_MAX)));
}
// Id-only: avoid loading full prc_order_bill rows for large jobs.
List<String> billIds = self.selectBillIdsForLocalRebill(query);
if (billIds == null || billIds.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.bill.recalc.noMatch"));
}
String taskId = IdUtils.fastSimpleUUID();
localRebillAsyncRunner.put(taskId, BatchAddIncomeBillFeeTask.STATUS_PENDING, 0, "queued", null,
billIds.size(), 0, 0);
localRebillAsyncRunner.run(taskId, billIds);
return taskId;
}
@Override
public BatchAddIncomeBillFeeTask getLocalRebillTask(String taskId)
{
if (StringUtils.isEmpty(taskId) || localRebillAsyncRunner == null)
{
return null;
}
return localRebillAsyncRunner.get(taskId);
}
@Override
@DataScope(deptAlias = "su", deptField = "dept_id", userAlias = "su", userField = "user_id", permission = "price:billing:bill")
public List<String> selectBillIdsForLocalRebill(PrcOrderBill query)
{
if (query == null)
{
return new ArrayList<String>();
}
normalizeLocalRebillQuery(query);
List<String> ids = orderBillMapper.selectPrcOrderBillIds(query);
return ids == null ? new ArrayList<String>() : ids;
}
@Override
@DataScope(deptAlias = "su", deptField = "dept_id", userAlias = "su", userField = "user_id", permission = "price:billing:bill")
public int countBillIdsForLocalRebill(PrcOrderBill query)
{
if (query == null)
{
return 0;
}
normalizeLocalRebillQuery(query);
return orderBillMapper.countPrcOrderBillIds(query);
}
private PrcOrderBill buildLocalRebillQuery(LocalRebillRequest request)
{
String scopeMode = StringUtils.nvl(request.getScopeMode(), "").trim().toUpperCase();
PrcOrderBill query = new PrcOrderBill();
if ("SELECTED".equals(scopeMode))
{
List<String> billIds = normalizeCodeList(request.getBillIds());
if (billIds == null || billIds.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.bill.recalc.selectedRequired"));
}
query.setBillIds(billIds);
return query;
}
List<String> orderNos = normalizeCodeList(request.getOrderNos());
List<String> trackingNos = normalizeCodeList(request.getTrackingNos());
List<String> transferNos = normalizeCodeList(request.getTransferNos());
List<String> orderIdentifiers = normalizeCodeList(request.getOrderIdentifiers());
List<String> customerCodes = normalizeCodeList(request.getCustomerCodes());
List<String> productCodes = normalizeCodeList(request.getProductCodes());
String creditArrearsStatus = StringUtils.isEmpty(request.getCreditArrearsStatus()) ? null
: request.getCreditArrearsStatus().trim();
String countryCode = StringUtils.isEmpty(request.getCountryCode()) ? null
: request.getCountryCode().trim().toUpperCase();
String beginBizDate = StringUtils.isEmpty(request.getBeginBizDate()) ? null : request.getBeginBizDate().trim();
String endBizDate = StringUtils.isEmpty(request.getEndBizDate()) ? null : request.getEndBizDate().trim();
boolean hasFilter = (orderNos != null && !orderNos.isEmpty())
|| (trackingNos != null && !trackingNos.isEmpty())
|| (transferNos != null && !transferNos.isEmpty())
|| (orderIdentifiers != null && !orderIdentifiers.isEmpty())
|| (customerCodes != null && !customerCodes.isEmpty())
|| (productCodes != null && !productCodes.isEmpty())
|| StringUtils.isNotEmpty(creditArrearsStatus)
|| StringUtils.isNotEmpty(countryCode)
|| StringUtils.isNotEmpty(beginBizDate)
|| StringUtils.isNotEmpty(endBizDate);
if (!hasFilter)
{
throw new ServiceException(MessageUtils.message("price.bill.recalc.filterRequired"));
}
query.setOrderNos(orderNos);
query.setTrackingNos(trackingNos);
query.setTransferNos(transferNos);
query.setOrderIdentifiers(orderIdentifiers);
query.setCustomerCodes(customerCodes);
query.setProductCodes(productCodes);
query.setCreditArrearsStatus(creditArrearsStatus);
query.setCountryCode(countryCode);
if (beginBizDate != null)
{
query.getParams().put("beginBizDate", beginBizDate);
}
if (endBizDate != null)
{
query.getParams().put("endBizDate", endBizDate);
}
return query;
}
private void normalizeLocalRebillQuery(PrcOrderBill query)
{
query.setOrderNos(normalizeCodeList(query.getOrderNos()));
query.setCustomerCodes(normalizeCodeList(query.getCustomerCodes()));
query.setProductCodes(normalizeCodeList(query.getProductCodes()));
query.setTrackingNos(normalizeCodeList(query.getTrackingNos()));
query.setTransferNos(normalizeCodeList(query.getTransferNos()));
query.setOrderIdentifiers(normalizeCodeList(query.getOrderIdentifiers()));
query.setBillIds(normalizeCodeList(query.getBillIds()));
}
private static String normalizeRecalcMode(String mode)
{
String m = StringUtils.nvl(mode, "").trim().toUpperCase();
@@ -0,0 +1,222 @@
package com.bms.price.task;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
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.AtomicInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import com.bms.common.core.redis.RedisCache;
import com.bms.common.utils.StringUtils;
import com.bms.price.config.BillingProperties;
import com.bms.price.domain.BatchAddIncomeBillFeeTask;
import com.bms.price.service.IPrcOrderBillService;
/**
* Async local rebill from prc_order_bill only (no Oracle / no MQ).
* Large jobs: process in pages of {@link #PAGE_SIZE}; each page runs in a short-lived worker pool.
*/
@Component
public class PrcOrderBillLocalRebillAsyncRunner
{
private static final Logger log = LoggerFactory.getLogger(PrcOrderBillLocalRebillAsyncRunner.class);
public static final String CACHE_KEY_PREFIX = "price:billing:localRebill:task:";
public static final int TTL_MINUTES = 120;
/** Bill this many, wait for page done, then next page. */
public static final int PAGE_SIZE = 500;
public static final int MIN_THREADS = 2;
public static final int MAX_THREADS = 16;
@Autowired
@Lazy
private IPrcOrderBillService orderBillService;
@Autowired
private RedisCache redisCache;
@Autowired
private BillingProperties billingProperties;
@Async("threadPoolTaskExecutor")
public void run(String taskId, List<String> billIds)
{
int total = billIds == null ? 0 : billIds.size();
AtomicInteger success = new AtomicInteger(0);
AtomicInteger fail = new AtomicInteger(0);
StringBuilder failTips = new StringBuilder();
int threads = clampThreads(billingProperties == null ? null : billingProperties.getOrderSourceSyncThreads());
ExecutorService pool = newWorkerPool(threads, "local-rebill");
try
{
put(taskId, BatchAddIncomeBillFeeTask.STATUS_RUNNING, 1, "running", null, total, 0, 0);
if (total == 0)
{
put(taskId, BatchAddIncomeBillFeeTask.STATUS_FAIL, 100, "failed", "no bills", 0, 0, 0);
return;
}
int processed = 0;
for (int from = 0; from < total; from += PAGE_SIZE)
{
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());
for (String billId : page)
{
final String one = billId;
jobs.add(() -> {
try
{
orderBillService.rebillLocalByBillId(one);
success.incrementAndGet();
return Boolean.TRUE;
}
catch (Exception e)
{
fail.incrementAndGet();
appendFail(failTips, one, e);
log.warn("local rebill failed taskId={} billId={}", taskId, one, e);
return Boolean.FALSE;
}
});
}
invokeAllQuiet(pool, jobs);
processed = to;
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());
}
String msg = "success=" + success.get() + ", fail=" + fail.get();
put(taskId, BatchAddIncomeBillFeeTask.STATUS_SUCCESS, 100, msg,
fail.get() > 0 && failTips.length() > 0 ? failTips.toString() : null,
total, success.get(), fail.get());
log.info("local rebill done taskId={} total={} success={} fail={} threads={} pageSize={}",
taskId, total, success.get(), fail.get(), threads, PAGE_SIZE);
}
catch (Exception e)
{
log.error("local rebill aborted taskId={}", taskId, e);
put(taskId, BatchAddIncomeBillFeeTask.STATUS_FAIL, 100, "failed", errorMessage(e),
total, success.get(), fail.get());
}
finally
{
shutdownPool(pool);
}
}
public void put(String taskId, String status, int percent, String message, String error,
int total, int success, int fail)
{
BatchAddIncomeBillFeeTask task = new BatchAddIncomeBillFeeTask();
task.setTaskId(taskId);
task.setStatus(status);
task.setPercent(percent);
task.setMessage(message);
task.setError(error);
task.setTotal(total);
task.setSuccess(success);
task.setFail(fail);
redisCache.setCacheObject(CACHE_KEY_PREFIX + taskId, task, TTL_MINUTES, TimeUnit.MINUTES);
}
public BatchAddIncomeBillFeeTask get(String taskId)
{
return redisCache.getCacheObject(CACHE_KEY_PREFIX + taskId);
}
private static void invokeAllQuiet(ExecutorService pool, List<Callable<Boolean>> jobs) throws InterruptedException
{
List<Future<Boolean>> futures = pool.invokeAll(jobs);
for (Future<Boolean> f : futures)
{
try
{
f.get();
}
catch (ExecutionException e)
{
// counted in job body
}
}
}
private static ExecutorService newWorkerPool(int threads, String namePrefix)
{
AtomicInteger seq = new AtomicInteger(0);
ThreadFactory factory = r -> {
Thread t = new Thread(r);
t.setName(namePrefix + "-" + seq.incrementAndGet());
t.setDaemon(true);
return t;
};
return Executors.newFixedThreadPool(Math.max(1, threads), factory);
}
private static void shutdownPool(ExecutorService pool)
{
if (pool == null)
{
return;
}
pool.shutdown();
try
{
if (!pool.awaitTermination(60, TimeUnit.SECONDS))
{
pool.shutdownNow();
}
}
catch (InterruptedException e)
{
Thread.currentThread().interrupt();
pool.shutdownNow();
}
}
static int clampThreads(Integer n)
{
int v = n == null ? 8 : n.intValue();
if (v < MIN_THREADS)
{
return MIN_THREADS;
}
if (v > MAX_THREADS)
{
return MAX_THREADS;
}
return v;
}
private static void appendFail(StringBuilder failTips, String billId, Exception e)
{
synchronized (failTips)
{
if (failTips.length() >= 800)
{
return;
}
if (failTips.length() > 0)
{
failTips.append("; ");
}
failTips.append(billId).append(":").append(errorMessage(e));
}
}
private static String errorMessage(Exception e)
{
return StringUtils.isEmpty(e.getMessage()) ? e.getClass().getSimpleName() : e.getMessage();
}
}
@@ -409,9 +409,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
limit #{limit}
</select>
<select id="selectPrcOrderBillList" parameterType="PrcOrderBill" resultMap="PrcOrderBillResult">
<include refid="selectPrcOrderBillVo"/>
<where>
<!-- Shared list filters for full row / id-only / count queries. -->
<sql id="prcOrderBillListWhere">
<if test="billId != null and billId != ''">
AND p.bill_id = #{billId}
</if>
@@ -540,10 +539,34 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
AND date_format(p.create_time,'%y%m%d') &lt;= date_format(#{params.endTime},'%y%m%d')
</if>
<include refid="orderBillDataScopeFilter"/>
</sql>
<select id="selectPrcOrderBillList" parameterType="PrcOrderBill" resultMap="PrcOrderBillResult">
<include refid="selectPrcOrderBillVo"/>
<where>
<include refid="prcOrderBillListWhere"/>
</where>
order by p.create_time desc, p.bill_id desc
</select>
<!-- Id-only for large local rebill (avoid loading full rows into memory). -->
<select id="selectPrcOrderBillIds" parameterType="PrcOrderBill" resultType="string">
select p.bill_id
from prc_order_bill p
<where>
<include refid="prcOrderBillListWhere"/>
</where>
order by p.bill_id asc
</select>
<select id="countPrcOrderBillIds" parameterType="PrcOrderBill" resultType="int">
select count(1)
from prc_order_bill p
<where>
<include refid="prcOrderBillListWhere"/>
</where>
</select>
<!-- rename_bill_tables.sql 已将需求文档中的 prc_bill 映射为本项目的 prc_order_bill。 -->
<!-- 生成弹窗预览:按客户、截止日、币种和订单号/运单号/转单号 OR 条件分页查询未入账订单。 -->
<sql id="orderIdentifierCondition">
+44
View File
@@ -85,6 +85,50 @@ export function listOrderBillsByProduct(data) {
})
}
/** Order-list local rebill (prc_order_bill only, no Oracle). Returns { taskId }. */
export function submitLocalRebill(data) {
return request({
url: '/price/billing/orderBill/rebill',
method: 'post',
data,
timeout: 120000
})
}
export function getLocalRebillTask(taskId) {
return request({
url: '/price/billing/orderBill/rebill/task/' + taskId,
method: 'get'
})
}
/**
* Async local rebill; poll until done.
* @param {object} data LocalRebillRequest
* @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')
const started = Date.now()
const maxMs = 30 * 60 * 1000
while (Date.now() - started < maxMs) {
const res = await getLocalRebillTask(String(taskId))
const task = res.data || {}
if (typeof onProgress === 'function') onProgress(task)
if (task.status === 'SUCCESS') return { task }
if (task.status === 'FAIL') {
const err = new Error(task.error || task.message || 'local rebill failed')
err.task = task
throw err
}
await new Promise(resolve => setTimeout(resolve, 800))
}
throw new Error('local rebill timeout')
}
/** Receivable bill detail */
export function getBill(billId) {
return request({
+115 -37
View File
@@ -20,6 +20,7 @@
type="primary"
plain
icon="Refresh"
:loading="batchLoading"
v-hasPermi="['price:orderBill:rebill']"
@click="openFilterRebill()"
>重新计费</el-button>
@@ -91,7 +92,35 @@
<el-tag size="small" :type="billingFailTagType(row.billingFailStatus)">{{ billingFailLabel(row.billingFailStatus) }}</el-tag>
</template>
</basic-table>
<order-source-sync-dialog ref="syncDialogRef" @success="onRebillSuccess" />
<el-dialog
v-model="rebillProgressVisible"
title="本地重新计费"
width="480px"
append-to-body
destroy-on-close
:close-on-click-modal="!batchLoading"
:close-on-press-escape="!batchLoading"
:show-close="!batchLoading"
>
<el-progress
:percentage="rebillProgressPercent"
:status="rebillProgressStatus"
:stroke-width="14"
striped
:striped-flow="batchLoading"
/>
<div class="rebill-progress__msg">{{ rebillProgressMessage }}</div>
<div class="rebill-progress__text">
总数 {{ rebillProgressTotal }}
/ 成功 {{ rebillProgressSuccess }}
/ 失败 {{ rebillProgressFail }}
</div>
<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>
</template>
</el-dialog>
<order-source-sync-log-drawer ref="logDrawerRef" />
<order-bill-product-check-drawer ref="productCheckDrawerRef" />
</div>
@@ -99,18 +128,22 @@
<script setup name="OrderBillQuery">
import { ElMessage, ElMessageBox } from 'element-plus'
import { listOrderBill, listOrderNoOptions, listTrackingNoOptions, listTransferNoOptions } from '@/api/price/billing'
import { syncOrderSourceSelected } from '@/api/monitor/orderSource'
import OrderSourceSyncDialog from '../../monitor/orderSource/components/OrderSourceSyncDialog.vue'
import { listOrderBill, listOrderNoOptions, listTrackingNoOptions, listTransferNoOptions, localRebillAsync } from '@/api/price/billing'
import OrderSourceSyncLogDrawer from '../../monitor/orderSource/components/OrderSourceSyncLogDrawer.vue'
import OrderBillProductCheckDrawer from './components/OrderBillProductCheckDrawer.vue'
const tableRef = ref()
const syncDialogRef = ref()
const logDrawerRef = ref()
const productCheckDrawerRef = ref()
const checkLoading = ref(false)
const batchLoading = ref(false)
const rebillProgressVisible = ref(false)
const rebillProgressPercent = ref(0)
const rebillProgressMessage = ref('')
const rebillProgressStatus = ref(undefined)
const rebillProgressTotal = ref(0)
const rebillProgressSuccess = ref(0)
const rebillProgressFail = ref(0)
const tableHeight = ref(480)
let tableResizeObserver = null
@@ -293,6 +326,62 @@ function handleCheckProducts() {
}
}
async function runLocalRebill(payload, confirmText) {
try {
await ElMessageBox.confirm(confirmText, '二次确认', { type: 'warning' })
} catch (e) {
return
}
batchLoading.value = true
rebillProgressVisible.value = true
rebillProgressPercent.value = 0
rebillProgressStatus.value = undefined
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 +
(fail && task.error ? (';' + task.error) : '')
tableRef.value?.refresh?.()
} catch (e) {
const task = e && e.task
rebillProgressStatus.value = 'exception'
rebillProgressMessage.value = (e && e.message) || '重新计费失败'
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?.()
}
} finally {
batchLoading.value = false
}
}
function openFilterRebill() {
const identifiers = currentIdentifiers()
if (!identifiers.length) {
@@ -303,30 +392,21 @@ function openFilterRebill() {
ElMessage.warning('单次最多重新计费 ' + MAX_REBILL + ' 个单号')
return
}
ElMessageBox.confirm(
'将按 ' + identifiers.length + ' 个单号查上游订单并重新计费,确认继续?',
'二次确认',
{ type: 'warning' }
).then(() => {
syncDialogRef.value?.open(buildRebillFilter(), true)
}).catch(() => {})
const filter = buildRebillFilter()
runLocalRebill(
{
scopeMode: 'ALL',
orderIdentifiers: filter.orderIdentifiers || identifiers,
customerCodes: filter.customerCodes || []
},
'将按 ' + identifiers.length + ' 个单号从本地订单表重新计费(不查上游),确认继续?'
)
}
function toSyncRow(row) {
return {
orderId: row.orderNo,
orgOrderId: row.trackingNo,
transferOrderId: row.transferNo,
customerCode: row.customerCode,
pickupDate: row.bizDate || row.pickupDate || row.orderTime || null
}
}
function handleBatchRebill(selection) {
const rows = selection || []
if (!rows.length) return
const missingTime = rows.filter(row => !toSyncRow(row).pickupDate).length
const missingTime = rows.filter(row => !(row.bizDate || row.pickupDate || row.orderTime)).length
if (missingTime) {
ElMessage.warning(missingTime + ' 条没有计费时间,无法重新计费')
return
@@ -335,20 +415,15 @@ function handleBatchRebill(selection) {
ElMessage.warning('选中重新计费最多 200 条')
return
}
ElMessageBox.confirm('确认重新计费选中的 ' + rows.length + ' 条订单?', '二次确认', { type: 'warning' }).then(() => {
batchLoading.value = true
return syncOrderSourceSelected(rows.map(toSyncRow))
}).then(res => {
const data = (res && res.data) || {}
ElMessage.success('完成:成功 ' + (data.ok || 0) + ' / 失败 ' + (data.fail || 0))
tableRef.value?.refresh?.()
}).catch(() => {}).finally(() => {
batchLoading.value = false
})
const billIds = rows.map(r => r.billId).filter(Boolean)
if (!billIds.length) {
ElMessage.warning('选中行缺少 billId,无法重新计费')
return
}
function onRebillSuccess() {
tableRef.value?.refresh?.()
runLocalRebill(
{ scopeMode: 'SELECTED', billIds },
'确认用本地订单数据重新计费选中的 ' + rows.length + ' 条订单?'
)
}
function searchOrderIdentifiers(keyword) {
@@ -418,4 +493,7 @@ onBeforeUnmount(() => {
.order-bill-query :deep(.basic-form .el-form-item .el-date-editor) {
width: 100%;
}
.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); }
</style>