增加产品价和红线价重计费功能
This commit is contained in:
@@ -41,6 +41,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.RecalcBillPriceRequest;
|
||||
import com.bms.price.domain.vo.UpdateChargeWeightRequest;
|
||||
import com.bms.price.domain.vo.UpdateIncomeBillFeeRequest;
|
||||
import com.bms.price.domain.vo.TmsPriceBillRequest;
|
||||
@@ -303,6 +304,53 @@ public class BillingController extends BaseController
|
||||
return success(task);
|
||||
}
|
||||
|
||||
/** Async recalc product_amount only. Poll /bill/recalcPrice/task/{taskId}. */
|
||||
@PreAuthorize("@ss.hasPermi('price:billing:recalc')")
|
||||
@Log(title = "Billing recalc product amount", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/bill/recalcProduct")
|
||||
public AjaxResult submitRecalcProductAmount(@RequestBody RecalcBillPriceRequest request)
|
||||
{
|
||||
if (request == null)
|
||||
{
|
||||
request = new RecalcBillPriceRequest();
|
||||
}
|
||||
request.setMode("PRODUCT");
|
||||
return successTask(prcOrderBillService.submitRecalcBillPrice(request));
|
||||
}
|
||||
|
||||
/** Async recalc redline_amount only. Poll /bill/recalcPrice/task/{taskId}. */
|
||||
@PreAuthorize("@ss.hasPermi('price:billing:recalc')")
|
||||
@Log(title = "Billing recalc redline amount", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/bill/recalcRedline")
|
||||
public AjaxResult submitRecalcRedlineAmount(@RequestBody RecalcBillPriceRequest request)
|
||||
{
|
||||
if (request == null)
|
||||
{
|
||||
request = new RecalcBillPriceRequest();
|
||||
}
|
||||
request.setMode("REDLINE");
|
||||
return successTask(prcOrderBillService.submitRecalcBillPrice(request));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:billing:recalc')")
|
||||
@GetMapping("/bill/recalcPrice/task/{taskId}")
|
||||
public AjaxResult getRecalcBillPriceTask(@PathVariable String taskId)
|
||||
{
|
||||
BatchAddIncomeBillFeeTask task = prcOrderBillService.getRecalcBillPriceTask(taskId);
|
||||
if (task == null)
|
||||
{
|
||||
return error(MessageUtils.message("price.bill.recalc.taskNotExists"));
|
||||
}
|
||||
return success(task);
|
||||
}
|
||||
|
||||
private AjaxResult successTask(String taskId)
|
||||
{
|
||||
Map<String, Object> payload = new HashMap<String, Object>();
|
||||
payload.put("taskId", taskId);
|
||||
return success(payload);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:billing:feeEdit')")
|
||||
@Log(title = "Billing income fee", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/bill/fee")
|
||||
|
||||
@@ -187,6 +187,14 @@ price.bill.fee.batch.noMatch=未查询到匹配的计费订单,无法批量增
|
||||
price.bill.fee.batch.filterRequired=请先在计费列表设置查询条件
|
||||
price.bill.fee.batch.selectedRequired=请勾选要增加费用的订单
|
||||
price.bill.fee.batch.scopeRequired=请选择添加范围
|
||||
price.bill.recalc.taskNotExists=重算价格任务不存在或已过期
|
||||
price.bill.recalc.tooMany=匹配订单超过上限 {0} 条,请缩小筛选范围
|
||||
price.bill.recalc.noMatch=未查询到已计费订单,无法重算产品价/红线价
|
||||
price.bill.recalc.filterRequired=请先在计费列表设置查询条件
|
||||
price.bill.recalc.selectedRequired=请勾选要重算的订单
|
||||
price.bill.recalc.modeRequired=请指定重算类型(PRODUCT/产品价或 REDLINE/红线价)
|
||||
price.bill.recalc.scopeRequired=请选择重算范围
|
||||
price.bill.recalc.quoteEmpty=计价结果为空,无法更新产品价/红线价
|
||||
price.bill.fee.surchargeNotFound=附加费配置不存在或已停用
|
||||
price.bill.fee.notFound=费用明细不存在
|
||||
price.bill.fee.notManual=仅手动增加的费用可编辑或删除
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.bms.price.domain.vo;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
|
||||
/** Batch recalc product_amount or redline_amount only (existing bills). */
|
||||
@Data
|
||||
public class RecalcBillPriceRequest implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** PRODUCT | REDLINE */
|
||||
private String mode;
|
||||
|
||||
/** ALL = filters; SELECTED = billIds. */
|
||||
private String scopeMode;
|
||||
private List<String> billIds;
|
||||
private List<String> orderNos;
|
||||
private List<String> trackingNos;
|
||||
private List<String> transferNos;
|
||||
private List<String> customerCodes;
|
||||
private List<String> productCodes;
|
||||
private String countryCode;
|
||||
private String creditArrearsStatus;
|
||||
private String beginBizDate;
|
||||
private String endBizDate;
|
||||
}
|
||||
@@ -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.RecalcBillPriceRequest;
|
||||
import com.bms.price.domain.vo.UpdateIncomeBillFeeRequest;
|
||||
import com.bms.price.domain.vo.TmsPriceBillRequest;
|
||||
import com.bms.price.domain.vo.TmsPriceQuoteResult;
|
||||
@@ -111,4 +112,21 @@ public interface IPrcOrderBillService
|
||||
* otherwise fill orderNo/customer from list filters (data-scoped).
|
||||
*/
|
||||
List<ChargeWeightImportRow> buildChargeWeightTemplateRows(ChargeWeightTemplateRequest request);
|
||||
|
||||
/**
|
||||
* Submit async recalc of product_amount or redline_amount (request.mode).
|
||||
* Existing bills only; no AR restriction; sales amounts untouched.
|
||||
*
|
||||
* @return taskId
|
||||
*/
|
||||
String submitRecalcBillPrice(RecalcBillPriceRequest request);
|
||||
|
||||
/** Poll Redis progress for product/redline recalc task. */
|
||||
BatchAddIncomeBillFeeTask getRecalcBillPriceTask(String taskId);
|
||||
|
||||
/**
|
||||
* Recalc and persist product_amount or redline_amount by mode (PRODUCT/REDLINE).
|
||||
* Requires existing prc_order_bill; skips AR editable guard.
|
||||
*/
|
||||
void recalcBillPriceByBillId(String billId, String mode);
|
||||
}
|
||||
|
||||
+202
-23
@@ -49,10 +49,12 @@ 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.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.PrcRecalcBillPriceAsyncRunner;
|
||||
import com.bms.price.domain.vo.TmsPriceBillRequest;
|
||||
import com.bms.price.domain.vo.TmsOrderBillStatusResult;
|
||||
import com.bms.price.domain.vo.TmsPriceQuoteFeeLine;
|
||||
@@ -137,9 +139,13 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
@Autowired
|
||||
private PrcChargeWeightImportAsyncRunner chargeWeightImportAsyncRunner;
|
||||
|
||||
@Autowired
|
||||
private PrcRecalcBillPriceAsyncRunner recalcBillPriceAsyncRunner;
|
||||
|
||||
private static final int BATCH_ADD_FEE_MAX = 5000;
|
||||
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;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -591,34 +597,208 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
return batchAddIncomeFeeAsyncRunner.get(taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve matched bills with data scope.
|
||||
* scopeMode required: SELECTED uses billIds; ALL uses list filters (at least one required).
|
||||
*/
|
||||
@Override
|
||||
public String submitRecalcBillPrice(RecalcBillPriceRequest request)
|
||||
{
|
||||
if (request == null)
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.bill.recalc.filterRequired"));
|
||||
}
|
||||
String mode = normalizeRecalcMode(request.getMode());
|
||||
List<PrcOrderBill> bills = resolveListScopedBills(request.getScopeMode(), request.getBillIds(),
|
||||
request.getOrderNos(), request.getTrackingNos(), request.getTransferNos(),
|
||||
request.getCustomerCodes(), request.getProductCodes(), request.getCountryCode(),
|
||||
request.getCreditArrearsStatus(), request.getBeginBizDate(), request.getEndBizDate(),
|
||||
"price.bill.recalc.filterRequired", "price.bill.recalc.scopeRequired",
|
||||
"price.bill.recalc.selectedRequired");
|
||||
if (bills.isEmpty())
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.bill.recalc.noMatch"));
|
||||
}
|
||||
if (bills.size() > RECALC_PRICE_MAX)
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.bill.recalc.tooMany",
|
||||
Integer.valueOf(RECALC_PRICE_MAX)));
|
||||
}
|
||||
List<String> billIds = new ArrayList<String>();
|
||||
for (PrcOrderBill bill : bills)
|
||||
{
|
||||
if (bill != null && StringUtils.isNotEmpty(bill.getBillId()))
|
||||
{
|
||||
billIds.add(bill.getBillId());
|
||||
}
|
||||
}
|
||||
if (billIds.isEmpty())
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.bill.recalc.noMatch"));
|
||||
}
|
||||
String taskId = IdUtils.fastSimpleUUID();
|
||||
recalcBillPriceAsyncRunner.put(taskId, BatchAddIncomeBillFeeTask.STATUS_PENDING, 0, "queued", null,
|
||||
billIds.size(), 0, 0);
|
||||
recalcBillPriceAsyncRunner.run(taskId, mode, billIds);
|
||||
return taskId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BatchAddIncomeBillFeeTask getRecalcBillPriceTask(String taskId)
|
||||
{
|
||||
if (StringUtils.isEmpty(taskId) || recalcBillPriceAsyncRunner == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return recalcBillPriceAsyncRunner.get(taskId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void recalcBillPriceByBillId(String billId, String mode)
|
||||
{
|
||||
boolean productMode = "PRODUCT".equals(normalizeRecalcMode(mode));
|
||||
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"));
|
||||
}
|
||||
TmsPriceBillRequest req = buildBillRequestFromBill(bill);
|
||||
if (StringUtils.isEmpty(req.getBizDate()))
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.bill.bizDateRequired"));
|
||||
}
|
||||
TmsPriceQuoteResult result = tmsPriceQuoteService.calculate(req);
|
||||
if (result == null)
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.bill.recalc.quoteEmpty"));
|
||||
}
|
||||
persistProductOrRedlineOnly(bill, result, productMode);
|
||||
}
|
||||
|
||||
private static String normalizeRecalcMode(String mode)
|
||||
{
|
||||
String m = StringUtils.nvl(mode, "").trim().toUpperCase();
|
||||
if (!"PRODUCT".equals(m) && !"REDLINE".equals(m))
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.bill.recalc.modeRequired"));
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
/** Update income header + fee snapshots for product or redline only. */
|
||||
private void persistProductOrRedlineOnly(PrcOrderBill bill, TmsPriceQuoteResult result, boolean productMode)
|
||||
{
|
||||
PrcIncomeBill incomeBill = incomeBillMapper.selectByBillId(bill.getBillId());
|
||||
boolean insert = incomeBill == null;
|
||||
if (insert)
|
||||
{
|
||||
incomeBill = new PrcIncomeBill();
|
||||
incomeBill.setIncomeBillId(UUID.randomUUID().toString());
|
||||
incomeBill.setBillId(bill.getBillId());
|
||||
incomeBill.setCurrency(StringUtils.isEmpty(bill.getCurrency()) ? "CNY" : bill.getCurrency());
|
||||
incomeBill.setSalesAmount(nz(bill.getIncomeTotal()));
|
||||
incomeBill.setIncomeTotal(nz(bill.getIncomeTotal()));
|
||||
incomeBill.setBelongSaler(bill.getBelongSaler());
|
||||
incomeBill.setBelongSalerCode(bill.getBelongSalerCode());
|
||||
incomeBill.setSettlementMan(bill.getSettlementMan());
|
||||
incomeBill.setSettlementManCode(bill.getSettlementManCode());
|
||||
incomeBill.setCreateBy(bill.getCreateBy());
|
||||
}
|
||||
if (productMode)
|
||||
{
|
||||
incomeBill.setProductAmount(result.getProductAmount());
|
||||
}
|
||||
else
|
||||
{
|
||||
incomeBill.setRedlineAmount(result.getRedlineAmount());
|
||||
}
|
||||
if (insert)
|
||||
{
|
||||
incomeBillMapper.insertPrcIncomeBill(incomeBill);
|
||||
}
|
||||
else
|
||||
{
|
||||
incomeBill.setUpdateBy(bill.getUpdateBy());
|
||||
incomeBillMapper.updatePrcIncomeBill(incomeBill);
|
||||
}
|
||||
|
||||
List<PrcIncomeBillFee> fees = incomeBillFeeMapper.selectByBillId(bill.getBillId());
|
||||
if (fees == null || fees.isEmpty() || result.getFeeDetails() == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
java.util.Map<String, TmsPriceQuoteFeeLine> quoteByKey = new java.util.HashMap<String, TmsPriceQuoteFeeLine>();
|
||||
for (TmsPriceQuoteFeeLine line : result.getFeeDetails())
|
||||
{
|
||||
if (line != null && StringUtils.isNotEmpty(line.getFeeType()))
|
||||
{
|
||||
quoteByKey.put(feeUniqueKey(line.getFeeType(), line.getScId()), line);
|
||||
}
|
||||
}
|
||||
for (PrcIncomeBillFee fee : fees)
|
||||
{
|
||||
if (fee == null || StringUtils.isEmpty(fee.getFeeType()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
TmsPriceQuoteFeeLine line = quoteByKey.get(feeUniqueKey(fee.getFeeType(), fee.getScId()));
|
||||
if (line == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (productMode)
|
||||
{
|
||||
fee.setProductAmount(line.getProductAmount());
|
||||
}
|
||||
else
|
||||
{
|
||||
fee.setRedlineAmount(line.getRedlineAmount());
|
||||
}
|
||||
incomeBillFeeMapper.updatePrcIncomeBillFee(fee);
|
||||
}
|
||||
}
|
||||
|
||||
private List<PrcOrderBill> resolveBatchAddFeeBills(BatchAddIncomeBillFeeRequest request)
|
||||
{
|
||||
if (request == null)
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.bill.fee.batch.filterRequired"));
|
||||
}
|
||||
String scopeMode = StringUtils.nvl(request.getScopeMode(), "").trim().toUpperCase();
|
||||
return resolveListScopedBills(request.getScopeMode(), request.getBillIds(),
|
||||
request.getOrderNos(), request.getTrackingNos(), request.getTransferNos(),
|
||||
request.getCustomerCodes(), request.getProductCodes(), request.getCountryCode(),
|
||||
request.getCreditArrearsStatus(), request.getBeginBizDate(), request.getEndBizDate(),
|
||||
"price.bill.fee.batch.filterRequired", "price.bill.fee.batch.scopeRequired",
|
||||
"price.bill.fee.batch.selectedRequired");
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared list-scope resolve for batch fee / recalc price.
|
||||
* scopeMode: SELECTED uses billIds; ALL uses filters (at least one required).
|
||||
*/
|
||||
private List<PrcOrderBill> resolveListScopedBills(String scopeModeRaw, List<String> billIdsRaw,
|
||||
List<String> orderNosRaw, List<String> trackingNosRaw, List<String> transferNosRaw,
|
||||
List<String> customerCodesRaw, List<String> productCodesRaw, String countryCodeRaw,
|
||||
String creditArrearsStatusRaw, String beginBizDateRaw, String endBizDateRaw,
|
||||
String msgFilter, String msgScope, String msgSelected)
|
||||
{
|
||||
String scopeMode = StringUtils.nvl(scopeModeRaw, "").trim().toUpperCase();
|
||||
if (!"ALL".equals(scopeMode) && !"SELECTED".equals(scopeMode))
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.bill.fee.batch.scopeRequired"));
|
||||
throw new ServiceException(MessageUtils.message(msgScope));
|
||||
}
|
||||
List<String> orderNos = normalizeCodeList(request.getOrderNos());
|
||||
List<String> trackingNos = normalizeCodeList(request.getTrackingNos());
|
||||
List<String> transferNos = normalizeCodeList(request.getTransferNos());
|
||||
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();
|
||||
List<String> orderNos = normalizeCodeList(orderNosRaw);
|
||||
List<String> trackingNos = normalizeCodeList(trackingNosRaw);
|
||||
List<String> transferNos = normalizeCodeList(transferNosRaw);
|
||||
List<String> customerCodes = normalizeCodeList(customerCodesRaw);
|
||||
List<String> productCodes = normalizeCodeList(productCodesRaw);
|
||||
String creditArrearsStatus = StringUtils.isEmpty(creditArrearsStatusRaw) ? null
|
||||
: creditArrearsStatusRaw.trim();
|
||||
String countryCode = StringUtils.isEmpty(countryCodeRaw) ? null
|
||||
: countryCodeRaw.trim().toUpperCase();
|
||||
String beginBizDate = StringUtils.isEmpty(beginBizDateRaw) ? null : beginBizDateRaw.trim();
|
||||
String endBizDate = StringUtils.isEmpty(endBizDateRaw) ? null : endBizDateRaw.trim();
|
||||
boolean hasFilter = (orderNos != null && !orderNos.isEmpty())
|
||||
|| (trackingNos != null && !trackingNos.isEmpty())
|
||||
|| (transferNos != null && !transferNos.isEmpty())
|
||||
@@ -630,15 +810,15 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
|| StringUtils.isNotEmpty(endBizDate);
|
||||
if (!hasFilter)
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.bill.fee.batch.filterRequired"));
|
||||
throw new ServiceException(MessageUtils.message(msgFilter));
|
||||
}
|
||||
PrcOrderBill query = new PrcOrderBill();
|
||||
if ("SELECTED".equals(scopeMode))
|
||||
{
|
||||
List<String> billIds = normalizeCodeList(request.getBillIds());
|
||||
List<String> billIds = normalizeCodeList(billIdsRaw);
|
||||
if (billIds == null || billIds.isEmpty())
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.bill.fee.batch.selectedRequired"));
|
||||
throw new ServiceException(MessageUtils.message(msgSelected));
|
||||
}
|
||||
query.setBillIds(billIds);
|
||||
}
|
||||
@@ -660,7 +840,6 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
query.getParams().put("endBizDate", endBizDate);
|
||||
}
|
||||
}
|
||||
// Use self proxy so @DataScope applies
|
||||
List<PrcOrderBill> list = self.selectBillList(query);
|
||||
return list == null ? new ArrayList<PrcOrderBill>() : list;
|
||||
}
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
package com.bms.price.task;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
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.domain.BatchAddIncomeBillFeeTask;
|
||||
import com.bms.price.service.IPrcOrderBillService;
|
||||
|
||||
/** Async: recalc product_amount or redline_amount only (no AR guard). */
|
||||
@Component
|
||||
public class PrcRecalcBillPriceAsyncRunner
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(PrcRecalcBillPriceAsyncRunner.class);
|
||||
public static final String CACHE_KEY_PREFIX = "price:billing:recalcPrice:task:";
|
||||
public static final int TTL_MINUTES = 120;
|
||||
|
||||
@Autowired
|
||||
@Lazy
|
||||
private IPrcOrderBillService orderBillService;
|
||||
|
||||
@Autowired
|
||||
private RedisCache redisCache;
|
||||
|
||||
@Async("threadPoolTaskExecutor")
|
||||
public void run(String taskId, String mode, List<String> billIds)
|
||||
{
|
||||
int total = billIds == null ? 0 : billIds.size();
|
||||
int success = 0;
|
||||
int fail = 0;
|
||||
StringBuilder failTips = new StringBuilder();
|
||||
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;
|
||||
}
|
||||
for (int i = 0; i < total; i++)
|
||||
{
|
||||
String billId = billIds.get(i);
|
||||
try
|
||||
{
|
||||
orderBillService.recalcBillPriceByBillId(billId, mode);
|
||||
success++;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
fail++;
|
||||
appendFail(failTips, billId, e);
|
||||
log.warn("recalc price failed taskId={} mode={} billId={}", taskId, mode, billId, e);
|
||||
}
|
||||
int percent = Math.min(99, (int) Math.floor(100.0 * (i + 1) / total));
|
||||
put(taskId, BatchAddIncomeBillFeeTask.STATUS_RUNNING, percent,
|
||||
"processing " + (i + 1) + "/" + total, null, total, success, fail);
|
||||
}
|
||||
String msg = "success=" + success + ", fail=" + fail;
|
||||
put(taskId, BatchAddIncomeBillFeeTask.STATUS_SUCCESS, 100, msg,
|
||||
fail > 0 && failTips.length() > 0 ? failTips.toString() : null, total, success, fail);
|
||||
log.info("recalc price done taskId={} mode={} total={} success={} fail={}",
|
||||
taskId, mode, total, success, fail);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.error("recalc price aborted taskId={} mode={}", taskId, mode, e);
|
||||
put(taskId, BatchAddIncomeBillFeeTask.STATUS_FAIL, 100, "failed", errorMessage(e), total, success, fail);
|
||||
}
|
||||
}
|
||||
|
||||
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 appendFail(StringBuilder failTips, String billId, Exception e)
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
-- billing list: recalc product/redline amount (async; no AR restriction)
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT 2332, N'重计费', m.menu_id, 22, '', NULL, 1, 0, 'F', '0', '0', 'price:billing:recalc', '#', 'admin', sysdate(), N'计费列表异步重算产品价/红线价(已出账也可)'
|
||||
FROM (
|
||||
SELECT menu_id FROM sys_menu
|
||||
WHERE perms = 'price:billing:bill' AND menu_type = 'C'
|
||||
UNION ALL
|
||||
SELECT menu_id FROM sys_menu
|
||||
WHERE component = 'price/billing/bill' AND menu_type = 'C'
|
||||
) m
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_menu x WHERE x.menu_id = 2332 OR x.perms = 'price:billing:recalc')
|
||||
LIMIT 1;
|
||||
|
||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT 1, 2332
|
||||
FROM DUAL
|
||||
WHERE EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2332)
|
||||
AND NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = 2332);
|
||||
|
||||
-- cleanup old split permissions if previously inserted
|
||||
DELETE FROM sys_role_menu WHERE menu_id = 2333;
|
||||
DELETE FROM sys_menu WHERE menu_id = 2333 OR perms = 'price:billing:recalcRedline';
|
||||
UPDATE sys_menu SET menu_name = N'重计费', perms = 'price:billing:recalc',
|
||||
remark = N'计费列表异步重算产品价/红线价(已出账也可)'
|
||||
WHERE menu_id = 2332 OR perms IN ('price:billing:recalcProduct', 'price:billing:recalc');
|
||||
@@ -286,3 +286,48 @@ export function listChargeWeightImportAttachments(query) {
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
/** Submit async recalc (PRODUCT / REDLINE) */
|
||||
export function submitRecalcBillPrice(mode, data) {
|
||||
const path = mode === 'REDLINE' ? 'recalcRedline' : 'recalcProduct'
|
||||
return request({
|
||||
url: '/price/billing/bill/' + path,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function getRecalcBillPriceTask(taskId) {
|
||||
return request({
|
||||
url: '/price/billing/bill/recalcPrice/task/' + taskId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Async recalc product/redline amount; poll until done.
|
||||
* @param {'PRODUCT'|'REDLINE'} mode
|
||||
* @param {object} data
|
||||
* @param {(task: object) => void} [onProgress]
|
||||
*/
|
||||
export async function recalcBillPriceAsync(mode, data, onProgress) {
|
||||
const submitRes = await submitRecalcBillPrice(mode, data)
|
||||
const d = submitRes && submitRes.data
|
||||
const taskId = d && (d.taskId || d)
|
||||
if (!taskId) throw new Error('missing recalc taskId')
|
||||
const started = Date.now()
|
||||
const maxMs = 30 * 60 * 1000
|
||||
while (Date.now() - started < maxMs) {
|
||||
const res = await getRecalcBillPriceTask(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 || 'recalc price failed')
|
||||
err.task = task
|
||||
throw err
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 800))
|
||||
}
|
||||
throw new Error('recalc price timeout')
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
search-action-align="center"
|
||||
:search-per-row="4"
|
||||
row-key="billId"
|
||||
selection
|
||||
>
|
||||
<template #search-orderNos="{ model }">
|
||||
<multi-value-input v-model="model.orderNos" placeholder="订单号" width="100%" :search-method="searchOrderNoOptions" />
|
||||
@@ -104,6 +105,15 @@
|
||||
@click="openChargeWeightImportAttach"
|
||||
>导入记录</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
icon="Refresh"
|
||||
v-hasPermi="['price:billing:recalc']"
|
||||
@click="openRecalcDialog"
|
||||
>重计费</el-button>
|
||||
</el-col>
|
||||
</template>
|
||||
<template #col-action="{ row }">
|
||||
<el-button link type="primary" icon="View" @click="handleView(row)">查看</el-button>
|
||||
@@ -161,9 +171,36 @@
|
||||
</template>
|
||||
</excel-import-dialog>
|
||||
<charge-weight-import-attach-drawer ref="cwImportAttachRef" />
|
||||
|
||||
<el-dialog
|
||||
v-model="recalcDialogOpen"
|
||||
title="重计费"
|
||||
width="420px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form label-width="90px">
|
||||
<el-form-item label="计费类型" required>
|
||||
<el-radio-group v-model="recalcMode">
|
||||
<el-radio value="PRODUCT">产品价</el-radio>
|
||||
<el-radio value="REDLINE">红线价</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="仅更新所选类型对应字段;有勾选按勾选,否则按当前筛选。已出账也可重算。"
|
||||
/>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="recalcDialogOpen = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!recalcMode" @click="confirmRecalc">开始重计费</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
v-model="cwImportProgressVisible"
|
||||
title="正在导入修改计费重"
|
||||
:title="cwImportProgressTitle"
|
||||
width="420px"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
@@ -184,7 +221,7 @@
|
||||
</template>
|
||||
|
||||
<script setup name="BillingBill">
|
||||
import { listBill, getBill, refundBill, listOrderNoOptions, listTrackingNoOptions, listTransferNoOptions, updateCreditArrears, importChargeWeightAsync, downloadChargeWeightTemplate } from '@/api/price/billing'
|
||||
import { listBill, getBill, refundBill, listOrderNoOptions, listTrackingNoOptions, listTransferNoOptions, updateCreditArrears, importChargeWeightAsync, downloadChargeWeightTemplate, recalcBillPriceAsync } from '@/api/price/billing'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useProducts } from '@/components/ProductSelect/useProducts'
|
||||
import BillDetail from './components/BillDetail.vue'
|
||||
@@ -242,6 +279,9 @@ const cwImportProgressPercent = ref(0)
|
||||
const cwImportProgressMessage = ref('')
|
||||
const cwImportProgressStatus = ref(undefined)
|
||||
const cwImportProgressIndeterminate = ref(false)
|
||||
const cwImportProgressTitle = ref('正在导入修改计费重')
|
||||
const recalcDialogOpen = ref(false)
|
||||
const recalcMode = ref('PRODUCT')
|
||||
|
||||
const columns = [
|
||||
{ label: '订单号', prop: 'orderNo', minWidth: 140, showOverflowTooltip: true },
|
||||
@@ -411,6 +451,88 @@ function onBatchFeeSuccess() {
|
||||
tableRef.value?.refresh?.()
|
||||
}
|
||||
|
||||
function buildRecalcPayload() {
|
||||
const q = tableRef.value?.getQueryParams?.() || {}
|
||||
const { pageNum, pageSize, bizDateRange, ...rest } = q
|
||||
if (!hasBatchFeeQuery({ ...rest, bizDateRange })) {
|
||||
ElMessage.warning('请先设置列表查询条件')
|
||||
return null
|
||||
}
|
||||
const selectedIds = (tableRef.value?.getSelectionIds?.() || []).filter(Boolean)
|
||||
const payload = {
|
||||
scopeMode: selectedIds.length ? 'SELECTED' : 'ALL',
|
||||
billIds: selectedIds,
|
||||
orderNos: rest.orderNos || [],
|
||||
trackingNos: rest.trackingNos || [],
|
||||
transferNos: rest.transferNos || [],
|
||||
customerCodes: rest.customerCodes || [],
|
||||
productCodes: rest.productCodes || [],
|
||||
creditArrearsStatus: rest.creditArrearsStatus || undefined
|
||||
}
|
||||
if (Array.isArray(bizDateRange) && (bizDateRange[0] || bizDateRange[1])) {
|
||||
payload.beginBizDate = bizDateRange[0] || null
|
||||
payload.endBizDate = bizDateRange[1] || null
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
function openRecalcDialog() {
|
||||
const payload = buildRecalcPayload()
|
||||
if (!payload) return
|
||||
recalcMode.value = 'PRODUCT'
|
||||
recalcDialogOpen.value = true
|
||||
}
|
||||
|
||||
function confirmRecalc() {
|
||||
if (!recalcMode.value) {
|
||||
ElMessage.warning('请选择计费类型')
|
||||
return
|
||||
}
|
||||
recalcDialogOpen.value = false
|
||||
runRecalc(recalcMode.value)
|
||||
}
|
||||
|
||||
async function runRecalc(mode) {
|
||||
const payload = buildRecalcPayload()
|
||||
if (!payload) return
|
||||
const label = mode === 'REDLINE' ? '红线价' : '产品价'
|
||||
cwImportProgressTitle.value = '正在重算' + label
|
||||
cwImportProgressVisible.value = true
|
||||
cwImportProgressPercent.value = 0
|
||||
cwImportProgressIndeterminate.value = false
|
||||
cwImportProgressStatus.value = undefined
|
||||
cwImportProgressMessage.value = '任务提交中…'
|
||||
try {
|
||||
const { task } = await recalcBillPriceAsync(mode, payload, (t) => {
|
||||
cwImportProgressPercent.value = Math.min(100, Number(t.percent) || 0)
|
||||
const total = Number(t.total) || 0
|
||||
const success = Number(t.success) || 0
|
||||
const fail = Number(t.fail) || 0
|
||||
if (t.status === 'PENDING') cwImportProgressMessage.value = '排队中…'
|
||||
else if (t.status === 'RUNNING') {
|
||||
cwImportProgressMessage.value = '正在处理 ' + success + '/' + total +
|
||||
(fail ? ('(失败 ' + fail + ')') : '')
|
||||
}
|
||||
})
|
||||
const success = Number(task && task.success) || 0
|
||||
const fail = Number(task && task.fail) || 0
|
||||
const tip = label + '重算完成:成功 ' + success + ',失败 ' + fail
|
||||
cwImportProgressPercent.value = 100
|
||||
cwImportProgressStatus.value = fail > 0 ? 'warning' : 'success'
|
||||
cwImportProgressMessage.value = tip + (fail && task.error ? (';' + task.error) : '')
|
||||
if (fail > 0) ElMessage.warning(cwImportProgressMessage.value)
|
||||
else ElMessage.success(tip)
|
||||
tableRef.value?.refresh?.()
|
||||
} catch (e) {
|
||||
cwImportProgressStatus.value = 'exception'
|
||||
const tip = (e && e.task && e.task.error) || (e && e.message) || '重算失败'
|
||||
cwImportProgressMessage.value = tip
|
||||
ElMessage.error(tip)
|
||||
} finally {
|
||||
setTimeout(() => { cwImportProgressVisible.value = false }, 800)
|
||||
}
|
||||
}
|
||||
|
||||
function openEditChargeWeight(row) {
|
||||
if (!row || !row.billId) return
|
||||
if (row.refundStatus === 'REFUNDED') {
|
||||
@@ -490,6 +612,7 @@ async function runChargeWeightImportAsync(uploadFile) {
|
||||
if (!raw) throw new Error('\u8bf7\u5148\u9009\u62e9\u6587\u4ef6')
|
||||
// Close import dialog so progress dialog is the only visible wait UI
|
||||
chargeWeightImportRef.value?.close?.()
|
||||
cwImportProgressTitle.value = '正在导入修改计费重'
|
||||
cwImportProgressVisible.value = true
|
||||
cwImportProgressPercent.value = 0
|
||||
cwImportProgressIndeterminate.value = true
|
||||
|
||||
Reference in New Issue
Block a user