Implement batch fee management features in BillingController, including preview, submission, and task retrieval. Update related services and mappers to support batch processing. Add necessary error messages in properties file and enhance front-end API for batch operations.

This commit is contained in:
wuweihua
2026-10-09 10:28:29 +08:00
parent 95df65b4f6
commit 5b4cbaffd6
15 changed files with 1014 additions and 23 deletions
@@ -3,7 +3,10 @@ package com.bms.price.service;
import java.math.BigDecimal;
import java.util.List;
import com.bms.price.domain.PrcOrderBill;
import com.bms.price.domain.BatchAddIncomeBillFeeTask;
import com.bms.price.domain.vo.AddIncomeBillFeeRequest;
import com.bms.price.domain.vo.BatchAddIncomeBillFeePreview;
import com.bms.price.domain.vo.BatchAddIncomeBillFeeRequest;
import com.bms.price.domain.vo.UpdateIncomeBillFeeRequest;
import com.bms.price.domain.vo.TmsPriceBillRequest;
import com.bms.price.domain.vo.TmsPriceQuoteResult;
@@ -45,6 +48,15 @@ public interface IPrcOrderBillService
/** Delete manual UNBILLED income fee; refresh receivable totals. */
TmsPriceQuoteResult deleteIncomeFee(String feeId);
/** Preview matched bills for batch add fee (data-scoped). */
BatchAddIncomeBillFeePreview previewBatchAddIncomeFee(BatchAddIncomeBillFeeRequest request);
/** Submit async batch add fee; returns taskId. */
String submitBatchAddIncomeFee(BatchAddIncomeBillFeeRequest request);
/** Poll Redis progress for batch add fee task. */
BatchAddIncomeBillFeeTask getBatchAddIncomeFeeTask(String taskId);
/** 提供给 TMS 判断订单是否可以继续处理的客户状态查询。 */
TmsOrderBillStatusResult queryTmsOrderBillStatus(String identifier);
@@ -30,6 +30,7 @@ import com.bms.common.utils.MessageUtils;
import com.bms.common.utils.SecurityUtils;
import com.bms.common.utils.StringUtils;
import com.bms.common.utils.sign.Md5Utils;
import com.bms.common.utils.uuid.IdUtils;
import com.bms.customer.customerinfo.domain.BmsCustomer;
import com.bms.customer.customerinfo.mapper.BmsCustomerMapper;
import com.bms.system.mapper.SysUserMapper;
@@ -40,8 +41,12 @@ import com.bms.price.domain.PrcOrderBillExt;
import com.bms.price.domain.PrcPayBill;
import com.bms.price.domain.PrcPayBillFee;
import com.bms.price.domain.PrcSurcharge;
import com.bms.price.domain.BatchAddIncomeBillFeeTask;
import com.bms.price.domain.vo.AddIncomeBillFeeRequest;
import com.bms.price.domain.vo.BatchAddIncomeBillFeePreview;
import com.bms.price.domain.vo.BatchAddIncomeBillFeeRequest;
import com.bms.price.domain.vo.UpdateIncomeBillFeeRequest;
import com.bms.price.task.PrcBatchAddIncomeFeeAsyncRunner;
import com.bms.price.domain.vo.TmsPriceBillRequest;
import com.bms.price.domain.vo.TmsOrderBillStatusResult;
import com.bms.price.domain.vo.TmsPriceQuoteFeeLine;
@@ -118,6 +123,12 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
@Autowired
private PrcCreditArrearsClearAsyncRunner creditArrearsClearAsyncRunner;
@Autowired
private PrcBatchAddIncomeFeeAsyncRunner batchAddIncomeFeeAsyncRunner;
private static final int BATCH_ADD_FEE_MAX = 5000;
private static final int BATCH_ADD_FEE_PREVIEW_LIMIT = 20;
@Override
@Transactional(rollbackFor = Exception.class)
public TmsPriceQuoteResult bill(TmsPriceBillRequest request)
@@ -387,20 +398,48 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
amount = BigDecimal.ZERO;
}
PrcIncomeBillFee fee = new PrcIncomeBillFee();
fee.setFeeId(UUID.randomUUID().toString());
fee.setBillId(bill.getBillId());
fee.setIncomeBillId(incomeBill == null ? null : incomeBill.getIncomeBillId());
fee.setFeeType(feeType);
fee.setFeeName(surcharge.getScName());
fee.setScId(surcharge.getScId());
fee.setAmount(amount);
fee.setCurrency(currency);
fee.setRoundMode(StringUtils.isEmpty(surcharge.getRoundMode()) ? bill.getRoundMode() : surcharge.getRoundMode());
fee.setRemark(request.getRemark());
fee.setArBillStatus(ArIncomeBillFeeStatus.UNBILLED.getCode());
fee.setManualFlag("1");
incomeBillFeeMapper.insertPrcIncomeBillFee(fee);
// uk_bill_fee_item (bill_id, fee_type, sc_id): overwrite when exists
PrcIncomeBillFee existing = incomeBillFeeMapper.selectByBillIdAndFeeTypeAndScId(
bill.getBillId(), feeType, surcharge.getScId());
if (existing != null)
{
if (ArIncomeBillFeeStatus.BILLED.getCode().equals(existing.getArBillStatus()))
{
throw new ServiceException(MessageUtils.message("price.bill.fee.alreadyBilled"));
}
existing.setFeeType(feeType);
existing.setFeeName(surcharge.getScName());
existing.setScId(surcharge.getScId());
existing.setAmount(amount);
existing.setCurrency(currency);
existing.setRoundMode(StringUtils.isEmpty(surcharge.getRoundMode()) ? bill.getRoundMode()
: surcharge.getRoundMode());
existing.setRemark(request.getRemark());
existing.setManualFlag("1");
if (incomeBill != null && StringUtils.isEmpty(existing.getIncomeBillId()))
{
existing.setIncomeBillId(incomeBill.getIncomeBillId());
}
incomeBillFeeMapper.updatePrcIncomeBillFee(existing);
}
else
{
PrcIncomeBillFee fee = new PrcIncomeBillFee();
fee.setFeeId(UUID.randomUUID().toString());
fee.setBillId(bill.getBillId());
fee.setIncomeBillId(incomeBill == null ? null : incomeBill.getIncomeBillId());
fee.setFeeType(feeType);
fee.setFeeName(surcharge.getScName());
fee.setScId(surcharge.getScId());
fee.setAmount(amount);
fee.setCurrency(currency);
fee.setRoundMode(StringUtils.isEmpty(surcharge.getRoundMode()) ? bill.getRoundMode()
: surcharge.getRoundMode());
fee.setRemark(request.getRemark());
fee.setArBillStatus(ArIncomeBillFeeStatus.UNBILLED.getCode());
fee.setManualFlag("1");
incomeBillFeeMapper.insertPrcIncomeBillFee(fee);
}
return refreshBillIncomeTotals(bill);
}
@@ -472,6 +511,125 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
return refreshBillIncomeTotals(bill);
}
@Override
public BatchAddIncomeBillFeePreview previewBatchAddIncomeFee(BatchAddIncomeBillFeeRequest request)
{
List<PrcOrderBill> bills = resolveBatchAddFeeBills(request);
BatchAddIncomeBillFeePreview preview = new BatchAddIncomeBillFeePreview();
preview.setMatchCount(bills.size());
int limit = Math.min(bills.size(), BATCH_ADD_FEE_PREVIEW_LIMIT);
preview.setSamples(bills.subList(0, limit));
return preview;
}
@Override
public String submitBatchAddIncomeFee(BatchAddIncomeBillFeeRequest request)
{
if (request == null || request.getScId() == null)
{
throw new ServiceException(MessageUtils.message("price.bill.fee.scIdRequired"));
}
requireActiveSurcharge(request.getScId());
List<PrcOrderBill> bills = resolveBatchAddFeeBills(request);
if (bills.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.bill.fee.batch.noMatch"));
}
if (bills.size() > BATCH_ADD_FEE_MAX)
{
throw new ServiceException(MessageUtils.message("price.bill.fee.batch.tooMany",
Integer.valueOf(BATCH_ADD_FEE_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.fee.batch.noMatch"));
}
String taskId = IdUtils.fastSimpleUUID();
batchAddIncomeFeeAsyncRunner.put(taskId, BatchAddIncomeBillFeeTask.STATUS_PENDING, 0, "queued", null,
billIds.size(), 0, 0);
batchAddIncomeFeeAsyncRunner.run(taskId, billIds, request.getScId(), request.getFeeType(),
request.getAmount(), request.getCurrency(), request.getRemark());
return taskId;
}
@Override
public BatchAddIncomeBillFeeTask getBatchAddIncomeFeeTask(String taskId)
{
if (StringUtils.isEmpty(taskId) || batchAddIncomeFeeAsyncRunner == null)
{
return null;
}
return batchAddIncomeFeeAsyncRunner.get(taskId);
}
/**
* Resolve matched bills with data scope.
* scopeMode required: SELECTED uses billIds; ALL uses list filters (at least one required).
*/
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();
if (!"ALL".equals(scopeMode) && !"SELECTED".equals(scopeMode))
{
throw new ServiceException(MessageUtils.message("price.bill.fee.batch.scopeRequired"));
}
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();
boolean hasFilter = (orderNos != null && !orderNos.isEmpty())
|| (trackingNos != null && !trackingNos.isEmpty())
|| (transferNos != null && !transferNos.isEmpty())
|| (customerCodes != null && !customerCodes.isEmpty())
|| (productCodes != null && !productCodes.isEmpty())
|| StringUtils.isNotEmpty(creditArrearsStatus)
|| StringUtils.isNotEmpty(countryCode);
if (!hasFilter)
{
throw new ServiceException(MessageUtils.message("price.bill.fee.batch.filterRequired"));
}
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.fee.batch.selectedRequired"));
}
query.setBillIds(billIds);
}
else
{
query.setOrderNos(orderNos);
query.setTrackingNos(trackingNos);
query.setTransferNos(transferNos);
query.setCustomerCodes(customerCodes);
query.setProductCodes(productCodes);
query.setCreditArrearsStatus(creditArrearsStatus);
query.setCountryCode(countryCode);
}
// Use self proxy so @DataScope applies
List<PrcOrderBill> list = self.selectBillList(query);
return list == null ? new ArrayList<PrcOrderBill>() : list;
}
private PrcSurcharge requireActiveSurcharge(Long scId)
{
PrcSurcharge surcharge = surchargeService.selectPrcSurchargeById(scId);