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:
@@ -1,7 +1,9 @@
|
||||
package com.bms.web.controller.price;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
@@ -20,10 +22,13 @@ import com.bms.common.core.domain.AjaxResult;
|
||||
import com.bms.common.core.page.TableDataInfo;
|
||||
import com.bms.common.enums.BusinessType;
|
||||
import com.bms.common.utils.DateUtils;
|
||||
import com.bms.common.utils.MessageUtils;
|
||||
import com.bms.common.utils.StringUtils;
|
||||
import com.bms.price.domain.BatchAddIncomeBillFeeTask;
|
||||
import com.bms.price.domain.PrcOrderBill;
|
||||
import com.bms.price.domain.PrcQuote;
|
||||
import com.bms.price.domain.vo.AddIncomeBillFeeRequest;
|
||||
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.TmsPriceQuoteRequest;
|
||||
@@ -207,6 +212,38 @@ public class BillingController extends BaseController
|
||||
return success(prcOrderBillService.addIncomeFee(request));
|
||||
}
|
||||
|
||||
/** Preview matched bills for batch add fee (filter: customer / transferNo / country). */
|
||||
@PreAuthorize("@ss.hasPermi('price:billing:feeAdd')")
|
||||
@PostMapping("/bill/fee/batch/preview")
|
||||
public AjaxResult previewBatchAddIncomeFee(@RequestBody BatchAddIncomeBillFeeRequest request)
|
||||
{
|
||||
return success(prcOrderBillService.previewBatchAddIncomeFee(request));
|
||||
}
|
||||
|
||||
/** Submit async batch add fee; poll /bill/fee/batch/task/{taskId}. */
|
||||
@PreAuthorize("@ss.hasPermi('price:billing:feeAdd')")
|
||||
@Log(title = "Billing batch income fee", businessType = BusinessType.INSERT)
|
||||
@PostMapping("/bill/fee/batch")
|
||||
public AjaxResult submitBatchAddIncomeFee(@Validated @RequestBody BatchAddIncomeBillFeeRequest request)
|
||||
{
|
||||
String taskId = prcOrderBillService.submitBatchAddIncomeFee(request);
|
||||
Map<String, Object> payload = new HashMap<String, Object>();
|
||||
payload.put("taskId", taskId);
|
||||
return success(payload);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:billing:feeAdd')")
|
||||
@GetMapping("/bill/fee/batch/task/{taskId}")
|
||||
public AjaxResult getBatchAddIncomeFeeTask(@PathVariable String taskId)
|
||||
{
|
||||
BatchAddIncomeBillFeeTask task = prcOrderBillService.getBatchAddIncomeFeeTask(taskId);
|
||||
if (task == null)
|
||||
{
|
||||
return error(MessageUtils.message("price.bill.fee.batch.taskNotExists"));
|
||||
}
|
||||
return success(task);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:billing:feeEdit')")
|
||||
@Log(title = "Billing income fee", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/bill/fee")
|
||||
|
||||
@@ -175,6 +175,12 @@ price.bill.packageZonePriceNotHit=已匹配分区,但套餐版本无该分区/
|
||||
price.bill.packagePriceNotHit=套餐版本无匹配的国家/重量段价格
|
||||
price.bill.productNotInPackage=产品不在客户套餐版本中,无法计费
|
||||
price.bill.fee.scIdRequired=附加费不能为空
|
||||
price.bill.fee.batch.taskNotExists=批量增加费用任务不存在或已过期
|
||||
price.bill.fee.batch.tooMany=匹配订单超过上限 {0} 条,请缩小筛选范围
|
||||
price.bill.fee.batch.noMatch=未查询到匹配的计费订单,无法批量增加费用
|
||||
price.bill.fee.batch.filterRequired=请先在计费列表设置查询条件
|
||||
price.bill.fee.batch.selectedRequired=请勾选要增加费用的订单
|
||||
price.bill.fee.batch.scopeRequired=请选择添加范围
|
||||
price.bill.fee.surchargeNotFound=附加费配置不存在或已停用
|
||||
price.bill.fee.notFound=费用明细不存在
|
||||
price.bill.fee.notManual=仅手动增加的费用可编辑或删除
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/** Async batch add income fee progress (Redis). */
|
||||
public class BatchAddIncomeBillFeeTask implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public static final String STATUS_PENDING = "PENDING";
|
||||
public static final String STATUS_RUNNING = "RUNNING";
|
||||
public static final String STATUS_SUCCESS = "SUCCESS";
|
||||
public static final String STATUS_FAIL = "FAIL";
|
||||
|
||||
private String taskId;
|
||||
private String status;
|
||||
private int percent;
|
||||
private String message;
|
||||
private String error;
|
||||
private int total;
|
||||
private int success;
|
||||
private int fail;
|
||||
|
||||
public String getTaskId()
|
||||
{
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId)
|
||||
{
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getStatus()
|
||||
{
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status)
|
||||
{
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public int getPercent()
|
||||
{
|
||||
return percent;
|
||||
}
|
||||
|
||||
public void setPercent(int percent)
|
||||
{
|
||||
this.percent = percent;
|
||||
}
|
||||
|
||||
public String getMessage()
|
||||
{
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message)
|
||||
{
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getError()
|
||||
{
|
||||
return error;
|
||||
}
|
||||
|
||||
public void setError(String error)
|
||||
{
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public int getTotal()
|
||||
{
|
||||
return total;
|
||||
}
|
||||
|
||||
public void setTotal(int total)
|
||||
{
|
||||
this.total = total;
|
||||
}
|
||||
|
||||
public int getSuccess()
|
||||
{
|
||||
return success;
|
||||
}
|
||||
|
||||
public void setSuccess(int success)
|
||||
{
|
||||
this.success = success;
|
||||
}
|
||||
|
||||
public int getFail()
|
||||
{
|
||||
return fail;
|
||||
}
|
||||
|
||||
public void setFail(int fail)
|
||||
{
|
||||
this.fail = fail;
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,8 @@ public class PrcOrderBill extends BaseEntity
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String billId;
|
||||
/** 批量查询:多个账单ID精确匹配(仅查询用,非表字段) */
|
||||
private List<String> billIds;
|
||||
private String billKey;
|
||||
private String orderNo;
|
||||
/** 批量查询:多个订单号精确匹配(仅查询用,非表字段) */
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.bms.price.domain.vo;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.bms.price.domain.PrcOrderBill;
|
||||
import lombok.Data;
|
||||
|
||||
/** Preview matched bills before async batch add fee. */
|
||||
@Data
|
||||
public class BatchAddIncomeBillFeePreview implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private int matchCount;
|
||||
/** First N rows for UI preview. */
|
||||
private List<PrcOrderBill> samples = new ArrayList<PrcOrderBill>();
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package com.bms.price.domain.vo;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Batch add receivable fee lines.
|
||||
* scopeMode=SELECTED uses billIds; scopeMode=ALL uses list query filters.
|
||||
*/
|
||||
@Data
|
||||
public class BatchAddIncomeBillFeeRequest implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** ALL = list filters; SELECTED = billIds only. */
|
||||
private String scopeMode;
|
||||
|
||||
/** Selected bill ids when scopeMode=SELECTED. */
|
||||
private List<String> billIds;
|
||||
|
||||
/** Same filters as billing list search. */
|
||||
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;
|
||||
|
||||
/** Surcharge config id (prc_surcharge.sc_id). */
|
||||
@NotNull(message = "scId required")
|
||||
private Long scId;
|
||||
|
||||
/** Dict bms_fee_type; empty = use surcharge.feeType. */
|
||||
private String feeType;
|
||||
|
||||
/**
|
||||
* Fixed amount for every matched bill.
|
||||
* Empty = calculate per bill from surcharge chargeType / weight / freight.
|
||||
*/
|
||||
private BigDecimal amount;
|
||||
|
||||
/** Default CNY. */
|
||||
private String currency;
|
||||
|
||||
private String remark;
|
||||
}
|
||||
@@ -17,6 +17,10 @@ public interface PrcIncomeBillFeeMapper
|
||||
|
||||
public PrcIncomeBillFee selectByFeeId(String feeId);
|
||||
|
||||
/** Lookup by unique key uk_bill_fee_item (bill_id, fee_type, sc_id). */
|
||||
public PrcIncomeBillFee selectByBillIdAndFeeTypeAndScId(@Param("billId") String billId,
|
||||
@Param("feeType") String feeType, @Param("scId") Long scId);
|
||||
|
||||
/** Count fees of a bill under given ar_bill_status and fee_type (e.g. BILLED + BASIC_FREIGHT). */
|
||||
public int countByBillIdAndArBillStatusAndFeeType(@Param("billId") String billId,
|
||||
@Param("arBillStatus") String arBillStatus, @Param("feeType") String feeType);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
+172
-14
@@ -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);
|
||||
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
package com.bms.price.task;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
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.domain.vo.AddIncomeBillFeeRequest;
|
||||
import com.bms.price.service.IPrcOrderBillService;
|
||||
|
||||
/**
|
||||
* Async runner: add the same manual income fee to many order bills.
|
||||
*/
|
||||
@Component
|
||||
public class PrcBatchAddIncomeFeeAsyncRunner
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(PrcBatchAddIncomeFeeAsyncRunner.class);
|
||||
|
||||
public static final String CACHE_KEY_PREFIX = "price:billing:fee:batch:task:";
|
||||
public static final int TTL_MINUTES = 60;
|
||||
|
||||
@Autowired
|
||||
@Lazy
|
||||
private IPrcOrderBillService orderBillService;
|
||||
|
||||
@Autowired
|
||||
private RedisCache redisCache;
|
||||
|
||||
@Async("threadPoolTaskExecutor")
|
||||
public void run(String taskId, List<String> billIds, Long scId, String feeType, BigDecimal amount,
|
||||
String currency, String remark)
|
||||
{
|
||||
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
|
||||
{
|
||||
AddIncomeBillFeeRequest req = new AddIncomeBillFeeRequest();
|
||||
req.setBillId(billId);
|
||||
req.setScId(scId);
|
||||
req.setFeeType(feeType);
|
||||
req.setAmount(amount);
|
||||
req.setCurrency(currency);
|
||||
req.setRemark(remark);
|
||||
orderBillService.addIncomeFee(req);
|
||||
success++;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
fail++;
|
||||
if (failTips.length() < 800)
|
||||
{
|
||||
String tip = e.getMessage();
|
||||
if (StringUtils.isEmpty(tip))
|
||||
{
|
||||
tip = e.getClass().getSimpleName();
|
||||
}
|
||||
if (failTips.length() > 0)
|
||||
{
|
||||
failTips.append("; ");
|
||||
}
|
||||
failTips.append(billId).append(":").append(tip);
|
||||
}
|
||||
log.warn("batch add fee failed taskId={} billId={}", taskId, 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;
|
||||
if (fail > 0 && failTips.length() > 0)
|
||||
{
|
||||
put(taskId, BatchAddIncomeBillFeeTask.STATUS_SUCCESS, 100, msg, failTips.toString(),
|
||||
total, success, fail);
|
||||
}
|
||||
else
|
||||
{
|
||||
put(taskId, BatchAddIncomeBillFeeTask.STATUS_SUCCESS, 100, msg, null, total, success, fail);
|
||||
}
|
||||
log.info("batch add fee done taskId={} total={} success={} fail={}", taskId, total, success, fail);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.error("batch add fee aborted taskId={}", taskId, 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 String errorMessage(Exception e)
|
||||
{
|
||||
String msg = e.getMessage();
|
||||
if (StringUtils.isEmpty(msg))
|
||||
{
|
||||
msg = e.getClass().getSimpleName();
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
where fee_id = #{feeId}
|
||||
</select>
|
||||
|
||||
<select id="selectByBillIdAndFeeTypeAndScId" resultMap="PrcIncomeBillFeeResult">
|
||||
select fee_id, bill_id, income_bill_id, fee_type, fee_name, sc_id, amount, product_amount, redline_amount, currency, round_mode, remark,
|
||||
ar_bill_status, manual_flag, create_time
|
||||
from prc_income_bill_fee
|
||||
where bill_id = #{billId}
|
||||
and fee_type = #{feeType}
|
||||
and sc_id = #{scId}
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<select id="countByBillIdAndArBillStatusAndFeeType" resultType="int">
|
||||
select count(1)
|
||||
from prc_income_bill_fee
|
||||
@@ -101,7 +111,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
redline_amount = #{redlineAmount},
|
||||
currency = #{currency},
|
||||
round_mode = #{roundMode},
|
||||
remark = #{remark}
|
||||
remark = #{remark},
|
||||
income_bill_id = #{incomeBillId},
|
||||
manual_flag = #{manualFlag}
|
||||
where fee_id = #{feeId}
|
||||
</update>
|
||||
|
||||
|
||||
@@ -240,6 +240,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="billId != null and billId != ''">
|
||||
AND p.bill_id = #{billId}
|
||||
</if>
|
||||
<if test="billIds != null and billIds.size() > 0">
|
||||
AND p.bill_id IN
|
||||
<foreach collection="billIds" item="bid" open="(" separator="," close=")">
|
||||
#{bid}
|
||||
</foreach>
|
||||
</if>
|
||||
<if test="orderNo != null and orderNo != ''">
|
||||
AND p.order_no like concat('%', #{orderNo}, '%')
|
||||
</if>
|
||||
|
||||
@@ -97,6 +97,62 @@ export function addIncomeBillFee(data) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 批量增加费用:按筛选预览匹配订单 */
|
||||
export function previewBatchAddIncomeFee(data) {
|
||||
return request({
|
||||
url: '/price/billing/bill/fee/batch/preview',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/** 批量增加费用:提交异步任务 */
|
||||
export function submitBatchAddIncomeFee(data) {
|
||||
return request({
|
||||
url: '/price/billing/bill/fee/batch',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function getBatchAddIncomeFeeTask(taskId) {
|
||||
return request({
|
||||
url: '/price/billing/bill/fee/batch/task/' + taskId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
async function pollBatchAddIncomeFeeTask(taskId, onProgress) {
|
||||
const started = Date.now()
|
||||
const maxMs = 30 * 60 * 1000
|
||||
while (Date.now() - started < maxMs) {
|
||||
const res = await getBatchAddIncomeFeeTask(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 || 'batch add fee failed')
|
||||
err.task = task
|
||||
throw err
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 800))
|
||||
}
|
||||
throw new Error('batch add fee timeout')
|
||||
}
|
||||
|
||||
/**
|
||||
* Async batch add fee; poll until done.
|
||||
* @param {object} data BatchAddIncomeBillFeeRequest
|
||||
* @param {(task: object) => void} [onProgress]
|
||||
*/
|
||||
export async function batchAddIncomeFeeAsync(data, onProgress) {
|
||||
const submitRes = await submitBatchAddIncomeFee(data)
|
||||
const d = submitRes && submitRes.data
|
||||
const taskId = d && (d.taskId || d)
|
||||
if (!taskId) throw new Error('missing batch fee taskId')
|
||||
return pollBatchAddIncomeFeeTask(String(taskId), onProgress)
|
||||
}
|
||||
|
||||
/** 计费详情:编辑手动应收费用 */
|
||||
export function updateIncomeBillFee(data) {
|
||||
return request({
|
||||
|
||||
@@ -70,24 +70,30 @@
|
||||
<template #col-payTotal="{ row }">
|
||||
<span class="payable">{{ formatMoney(row.payTotal) }}</span>
|
||||
</template>
|
||||
<!-- <template #toolbar="{ selection, ids }">
|
||||
<template #toolbar>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="warning"
|
||||
type="primary"
|
||||
plain
|
||||
icon="Refresh"
|
||||
:disabled="!selection.length"
|
||||
v-hasPermi="['price:billing:arrearsUpdate']"
|
||||
@click="handleUpdateArrears(ids, selection)"
|
||||
>更新欠费状态</el-button>
|
||||
icon="Plus"
|
||||
v-hasPermi="['price:billing:feeAdd']"
|
||||
@click="openBatchFee"
|
||||
>批量增加费用</el-button>
|
||||
</el-col>
|
||||
</template> -->
|
||||
</template>
|
||||
<template #col-action="{ row }">
|
||||
<el-button link type="primary" icon="View" @click="handleView(row)">查看</el-button>
|
||||
</template>
|
||||
</basic-table>
|
||||
|
||||
|
||||
<bill-detail v-model="open" :title="title" :loading="loading" :detail="detail" @updated="onDetailUpdated" />
|
||||
<batch-add-income-fee-dialog
|
||||
v-model="batchFeeOpen"
|
||||
:list-query="batchFeeListQuery"
|
||||
:selected-ids="batchFeeSelectedIds"
|
||||
@success="onBatchFeeSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -96,6 +102,7 @@ import { listBill, getBill, listOrderNoOptions, listTrackingNoOptions, listTrans
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useProducts } from '@/components/ProductSelect/useProducts'
|
||||
import BillDetail from './components/BillDetail.vue'
|
||||
import BatchAddIncomeFeeDialog from './components/BatchAddIncomeFeeDialog.vue'
|
||||
|
||||
// 订单号下拉远程搜索:返回匹配的订单号列表(最多 20 条)
|
||||
function searchOrderNoOptions(keyword) {
|
||||
@@ -126,6 +133,9 @@ const open = ref(false)
|
||||
const title = ref('计费详情')
|
||||
const loading = ref(false)
|
||||
const detail = ref(null)
|
||||
const batchFeeOpen = ref(false)
|
||||
const batchFeeListQuery = ref({})
|
||||
const batchFeeSelectedIds = ref([])
|
||||
|
||||
const columns = [
|
||||
{ label: '订单号', prop: 'orderNo', minWidth: 140, showOverflowTooltip: true },
|
||||
@@ -213,6 +223,32 @@ function onDetailUpdated(data) {
|
||||
tableRef.value?.refresh?.()
|
||||
}
|
||||
|
||||
function hasBatchFeeQuery(q) {
|
||||
const hasArr = (arr) => Array.isArray(arr) && arr.some(v => v != null && String(v).trim() !== '')
|
||||
return hasArr(q.orderNos)
|
||||
|| hasArr(q.trackingNos)
|
||||
|| hasArr(q.transferNos)
|
||||
|| hasArr(q.customerCodes)
|
||||
|| hasArr(q.productCodes)
|
||||
|| !!(q.creditArrearsStatus && String(q.creditArrearsStatus).trim())
|
||||
}
|
||||
|
||||
function openBatchFee() {
|
||||
const q = tableRef.value?.getQueryParams?.() || {}
|
||||
const { pageNum, pageSize, ...rest } = q
|
||||
if (!hasBatchFeeQuery(rest)) {
|
||||
ElMessage.warning('请先设置列表查询条件')
|
||||
return
|
||||
}
|
||||
batchFeeListQuery.value = { ...rest }
|
||||
batchFeeSelectedIds.value = tableRef.value?.getSelectionIds?.() || []
|
||||
batchFeeOpen.value = true
|
||||
}
|
||||
|
||||
function onBatchFeeSuccess() {
|
||||
tableRef.value?.refresh?.()
|
||||
}
|
||||
|
||||
function formatMoney(val) {
|
||||
if (val === null || val === undefined || val === '') return '-'
|
||||
const num = Number(val)
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="modelValue"
|
||||
:title="dlgTitle"
|
||||
width="640px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
>
|
||||
<el-form label-width="110px" class="batch-scope">
|
||||
<el-form-item :label="lblQuery">
|
||||
<div v-if="queryTags.length" class="query-tags">
|
||||
<el-tag
|
||||
v-for="(t, i) in queryTags"
|
||||
:key="i"
|
||||
class="query-tag"
|
||||
type="info"
|
||||
effect="plain"
|
||||
>{{ t }}</el-tag>
|
||||
</div>
|
||||
<span v-else class="empty-query">{{ emptyQuery }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="lblScope" required>
|
||||
<el-radio-group v-model="scopeMode">
|
||||
<el-radio value="ALL">{{ optAll }}</el-radio>
|
||||
<el-radio value="SELECTED">{{ optSelected }}</el-radio>
|
||||
</el-radio-group>
|
||||
<div v-if="scopeMode === 'SELECTED'" class="selected-tip">{{ selectedTip }}</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-divider content-position="left">{{ feeSection }}</el-divider>
|
||||
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
|
||||
<el-form-item :label="lblFeeName" prop="scId">
|
||||
<el-select
|
||||
v-model="form.scId"
|
||||
filterable
|
||||
clearable
|
||||
:placeholder="phSurcharge"
|
||||
style="width: 100%;"
|
||||
@change="onSurchargeChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="s in surchargeOptions"
|
||||
:key="s.scId"
|
||||
:label="s.scName"
|
||||
:value="s.scId"
|
||||
>
|
||||
<span>{{ s.scName }}</span>
|
||||
<span style="float: right; color: var(--el-text-color-secondary); font-size: 12px;">{{ s.chargeValue }}</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="lblFeeType" prop="feeType">
|
||||
<el-select v-model="form.feeType" :placeholder="phFeeType" style="width: 100%;">
|
||||
<el-option
|
||||
v-for="d in (bms_fee_type || [])"
|
||||
:key="d.value"
|
||||
:label="d.label"
|
||||
:value="d.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="lblAmount" prop="amount">
|
||||
<el-input-number
|
||||
v-model="form.amount"
|
||||
:min="0"
|
||||
:precision="4"
|
||||
:step="0.1"
|
||||
controls-position="right"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
<div class="amount-hint">{{ amountHint }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="lblCurrency" prop="currency">
|
||||
<el-select v-model="form.currency" style="width: 100%;">
|
||||
<el-option
|
||||
v-for="d in (bms_currency || [])"
|
||||
:key="d.value"
|
||||
:label="d.label"
|
||||
:value="d.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="lblRemark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" :placeholder="phRemark" maxlength="200" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="$emit('update:modelValue', false)" :disabled="submitting">{{ btnCancel }}</el-button>
|
||||
<el-button type="primary" :loading="submitting" :disabled="!canSubmit" @click="submit">{{ btnOk }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="progressVisible"
|
||||
:title="progressTitle"
|
||||
width="420px"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:show-close="false"
|
||||
append-to-body
|
||||
>
|
||||
<div class="batch-progress">
|
||||
<el-progress :percentage="progressPercent" :status="progressStatus" />
|
||||
<div class="batch-progress-msg">{{ progressMessage }}</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { listSurchargeOptions } from '@/api/price/surcharge'
|
||||
import { batchAddIncomeFeeAsync } from '@/api/price/billing'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
listQuery: { type: Object, default: () => ({}) },
|
||||
selectedIds: { type: Array, default: () => [] }
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'success'])
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const { bms_fee_type, bms_currency } = useDict('bms_fee_type', 'bms_currency')
|
||||
|
||||
const dlgTitle = "批量增加费用"
|
||||
const lblScope = "添加范围"
|
||||
const optAll = "全部加(当前列表查询条件)"
|
||||
const optSelected = "只加列表勾选"
|
||||
const lblQuery = "列表查询条件"
|
||||
const emptyQuery = "无(无查询条件时不能添加费用)"
|
||||
const feeSection = "费用信息"
|
||||
const lblFeeName = "费用名称"
|
||||
const lblFeeType = "费用类型"
|
||||
const lblAmount = "金额"
|
||||
const lblCurrency = "币种"
|
||||
const lblRemark = "说明"
|
||||
const phSurcharge = "请选择附加费"
|
||||
const phFeeType = "请选择费用类型"
|
||||
const phRemark = "选填"
|
||||
const btnCancel = "取消"
|
||||
const btnOk = "确认添加"
|
||||
const amountHint = "留空则按附加费计费方式逐单计算;填写则所有订单使用该固定金额"
|
||||
const progressTitle = "正在批量增加费用"
|
||||
|
||||
const formRef = ref()
|
||||
const submitting = ref(false)
|
||||
const surchargeOptions = ref([])
|
||||
const scopeMode = ref('')
|
||||
|
||||
const progressVisible = ref(false)
|
||||
const progressPercent = ref(0)
|
||||
const progressMessage = ref('')
|
||||
const progressStatus = ref(undefined)
|
||||
|
||||
const form = ref(emptyForm())
|
||||
|
||||
const rules = {
|
||||
scId: [{ required: true, message: "请选择附加费", trigger: 'change' }],
|
||||
feeType: [{ required: true, message: "请选择费用类型", trigger: 'change' }],
|
||||
currency: [{ required: true, message: "请选择币种", trigger: 'change' }]
|
||||
}
|
||||
|
||||
const selectedCount = computed(() => (props.selectedIds || []).filter(Boolean).length)
|
||||
const selectedTip = computed(() => "已勾选 " + selectedCount.value + " 条")
|
||||
const queryTags = computed(() => buildQueryTags(props.listQuery || {}))
|
||||
const hasQuery = computed(() => queryTags.value.length > 0)
|
||||
const canSubmit = computed(() => {
|
||||
if (!hasQuery.value) return false
|
||||
if (!scopeMode.value) return false
|
||||
if (!form.value.scId) return false
|
||||
if (scopeMode.value === 'SELECTED') return selectedCount.value > 0
|
||||
return true
|
||||
})
|
||||
|
||||
function emptyForm() {
|
||||
return {
|
||||
scId: undefined,
|
||||
feeType: 'SURCHARGE',
|
||||
amount: undefined,
|
||||
currency: 'CNY',
|
||||
remark: ''
|
||||
}
|
||||
}
|
||||
|
||||
function fmtList(arr, max = 5) {
|
||||
const list = (arr || []).filter(v => v != null && String(v).trim() !== '')
|
||||
if (!list.length) return ''
|
||||
const shown = list.slice(0, max).map(String)
|
||||
return shown.join(', ') + (list.length > max ? ' …(+'+ (list.length - max) + ')' : '')
|
||||
}
|
||||
|
||||
function buildQueryTags(q) {
|
||||
const tags = []
|
||||
const order = fmtList(q.orderNos)
|
||||
if (order) tags.push("订单号" + ':' + order)
|
||||
const tracking = fmtList(q.trackingNos)
|
||||
if (tracking) tags.push("运单号" + ':' + tracking)
|
||||
const transfer = fmtList(q.transferNos)
|
||||
if (transfer) tags.push("转单号" + ':' + transfer)
|
||||
const customer = fmtList(q.customerCodes)
|
||||
if (customer) tags.push("客户" + ':' + customer)
|
||||
const product = fmtList(q.productCodes)
|
||||
if (product) tags.push("产品" + ':' + product)
|
||||
if (q.creditArrearsStatus) {
|
||||
const label = q.creditArrearsStatus === 'ARREARS'
|
||||
? "欠费"
|
||||
: (q.creditArrearsStatus === 'NORMAL' ? "正常" : q.creditArrearsStatus)
|
||||
tags.push("额度欠费" + ':' + label)
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
function loadSurcharges() {
|
||||
return listSurchargeOptions({ ssCode: 'on' }).then(res => {
|
||||
surchargeOptions.value = res.data || []
|
||||
})
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, (open) => {
|
||||
if (!open) return
|
||||
scopeMode.value = ''
|
||||
form.value = emptyForm()
|
||||
loadSurcharges().then(() => nextTick(() => formRef.value?.clearValidate()))
|
||||
})
|
||||
|
||||
function onSurchargeChange(scId) {
|
||||
const s = (surchargeOptions.value || []).find(x => String(x.scId) === String(scId))
|
||||
if (!s) return
|
||||
form.value.feeType = s.feeType || 'SURCHARGE'
|
||||
form.value.currency = 'CNY'
|
||||
const type = String(s.chargeType || 'FIXED').toUpperCase()
|
||||
if (type === 'FIXED' && s.chargeValue != null) {
|
||||
form.value.amount = Number(Number(s.chargeValue).toFixed(4))
|
||||
} else {
|
||||
form.value.amount = undefined
|
||||
}
|
||||
if (s.scNote && !form.value.remark) {
|
||||
form.value.remark = s.scNote
|
||||
}
|
||||
}
|
||||
|
||||
function buildPayload() {
|
||||
const q = props.listQuery || {}
|
||||
const payload = {
|
||||
scopeMode: scopeMode.value,
|
||||
scId: form.value.scId,
|
||||
feeType: form.value.feeType,
|
||||
amount: form.value.amount,
|
||||
currency: form.value.currency || 'CNY',
|
||||
remark: form.value.remark,
|
||||
orderNos: q.orderNos || [],
|
||||
trackingNos: q.trackingNos || [],
|
||||
transferNos: q.transferNos || [],
|
||||
customerCodes: q.customerCodes || [],
|
||||
productCodes: q.productCodes || [],
|
||||
creditArrearsStatus: q.creditArrearsStatus || undefined
|
||||
}
|
||||
if (scopeMode.value === 'SELECTED') {
|
||||
payload.billIds = (props.selectedIds || []).filter(Boolean)
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
function submit() {
|
||||
formRef.value?.validate(valid => {
|
||||
if (!valid) return
|
||||
if (!hasQuery.value) {
|
||||
proxy.$modal.msgWarning("请先在列表设置查询条件")
|
||||
return
|
||||
}
|
||||
if (!scopeMode.value) {
|
||||
proxy.$modal.msgWarning("请选择添加范围")
|
||||
return
|
||||
}
|
||||
if (scopeMode.value === 'SELECTED' && selectedCount.value <= 0) {
|
||||
proxy.$modal.msgWarning("请先在列表勾选要增加费用的订单")
|
||||
return
|
||||
}
|
||||
const confirmMsg = scopeMode.value === 'SELECTED'
|
||||
? ("确认向勾选的 " + selectedCount.value + " 条订单批量增加费用?")
|
||||
: "确认按当前列表查询条件批量增加费用?"
|
||||
proxy.$modal.confirm(confirmMsg).then(() => {
|
||||
submitting.value = true
|
||||
progressVisible.value = true
|
||||
progressPercent.value = 0
|
||||
progressMessage.value = "任务提交中…"
|
||||
progressStatus.value = undefined
|
||||
return batchAddIncomeFeeAsync(buildPayload(), (task) => {
|
||||
progressPercent.value = Math.min(100, Number(task.percent) || 0)
|
||||
const total = Number(task.total) || 0
|
||||
const success = Number(task.success) || 0
|
||||
const fail = Number(task.fail) || 0
|
||||
if (task.status === 'PENDING') progressMessage.value = "排队中…"
|
||||
else if (task.status === 'RUNNING') {
|
||||
progressMessage.value = "正在处理 " + success + '/' + total + (fail ? ("(失败 " + fail + ")") : '')
|
||||
} else if (task.status === 'SUCCESS') {
|
||||
progressMessage.value = "完成:成功 " + success + ",失败 " + fail
|
||||
progressStatus.value = fail > 0 ? 'warning' : 'success'
|
||||
}
|
||||
}).then(({ task }) => {
|
||||
progressPercent.value = 100
|
||||
const success = Number(task.success) || 0
|
||||
const fail = Number(task.fail) || 0
|
||||
const tip = "批量增加完成:成功 " + success + ",失败 " + fail
|
||||
if (fail > 0) proxy.$modal.msgWarning(tip + (task.error ? ';' + task.error : ''))
|
||||
else proxy.$modal.msgSuccess(tip)
|
||||
emit('update:modelValue', false)
|
||||
emit('success', task)
|
||||
}).catch((e) => {
|
||||
progressStatus.value = 'exception'
|
||||
const tip = (e && e.task && e.task.error) || (e && e.message) || "批量增加失败"
|
||||
progressMessage.value = tip
|
||||
proxy.$modal.msgError(tip)
|
||||
}).finally(() => {
|
||||
submitting.value = false
|
||||
setTimeout(() => { progressVisible.value = false }, 600)
|
||||
})
|
||||
}).catch(() => {})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.batch-scope { margin-bottom: 4px; }
|
||||
.query-tags { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.query-tag { max-width: 100%; }
|
||||
.empty-query {
|
||||
color: var(--el-color-danger);
|
||||
font-size: 13px;
|
||||
}
|
||||
.selected-tip {
|
||||
margin-top: 6px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
.amount-hint {
|
||||
margin-top: 4px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.batch-progress { padding: 8px 4px 4px; }
|
||||
.batch-progress-msg {
|
||||
margin-top: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
max-height: 160px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user