订单列表重新计费优化

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">