订单列表重新计费优化
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
+169
@@ -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();
|
||||
|
||||
+222
@@ -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') <= 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">
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
function onRebillSuccess() {
|
||||
tableRef.value?.refresh?.()
|
||||
const billIds = rows.map(r => r.billId).filter(Boolean)
|
||||
if (!billIds.length) {
|
||||
ElMessage.warning('选中行缺少 billId,无法重新计费')
|
||||
return
|
||||
}
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user