From 6a50b0f122ff6f6a421532da12078f3145b3d7fa Mon Sep 17 00:00:00 2001 From: wuweihua <88888888> Date: Wed, 14 Oct 2026 15:25:26 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=8F=AF=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E8=AE=A1=E8=B4=B9=E9=87=8D=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/price/BillingController.java | 67 ++++ .../price/domain/ChargeWeightImportTask.java | 103 +++++ .../domain/vo/ChargeWeightImportRow.java | 22 ++ .../vo/ChargeWeightTemplateRequest.java | 24 ++ .../price/domain/vo/TmsPriceQuoteRequest.java | 2 +- .../domain/vo/UpdateChargeWeightRequest.java | 22 ++ .../price/service/IPrcOrderBillService.java | 34 ++ .../service/impl/PrcOrderBillServiceImpl.java | 362 +++++++++++++++++- .../PrcChargeWeightImportAsyncRunner.java | 195 ++++++++++ .../sql/price/billing_charge_weight_menu.sql | 20 + .../charge-weight-import-template.xlsx | Bin 0 -> 5117 bytes bms-front/src/api/price/billing.js | 90 +++++ .../components/ExcelImportDialog/index.vue | 26 +- bms-front/src/views/price/billing/bill.vue | 213 ++++++++++- .../price/billing/components/BillDetail.vue | 22 ++ .../components/EditChargeWeightDialog.vue | 114 ++++++ 16 files changed, 1306 insertions(+), 10 deletions(-) create mode 100644 bms-back/bms-system/src/main/java/com/bms/price/domain/ChargeWeightImportTask.java create mode 100644 bms-back/bms-system/src/main/java/com/bms/price/domain/vo/ChargeWeightImportRow.java create mode 100644 bms-back/bms-system/src/main/java/com/bms/price/domain/vo/ChargeWeightTemplateRequest.java create mode 100644 bms-back/bms-system/src/main/java/com/bms/price/domain/vo/UpdateChargeWeightRequest.java create mode 100644 bms-back/bms-system/src/main/java/com/bms/price/task/PrcChargeWeightImportAsyncRunner.java create mode 100644 bms-back/sql/price/billing_charge_weight_menu.sql create mode 100644 bms-front/public/templates/billing/charge-weight-import-template.xlsx create mode 100644 bms-front/src/views/price/billing/components/EditChargeWeightDialog.vue diff --git a/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/BillingController.java b/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/BillingController.java index a211ecf6..1ee75a26 100644 --- a/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/BillingController.java +++ b/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/BillingController.java @@ -4,6 +4,7 @@ import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; @@ -16,6 +17,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; import com.bms.common.annotation.Log; import com.bms.common.core.controller.BaseController; import com.bms.common.core.domain.AjaxResult; @@ -24,13 +26,18 @@ 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.common.utils.poi.ExcelUtil; import com.bms.price.domain.BatchAddIncomeBillFeeTask; +import com.bms.price.domain.ChargeWeightImportTask; import com.bms.price.domain.PrcBillingTask; 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.BillingTaskSubmitRequest; +import com.bms.price.domain.vo.ChargeWeightImportRow; +import com.bms.price.domain.vo.ChargeWeightTemplateRequest; +import com.bms.price.domain.vo.UpdateChargeWeightRequest; import com.bms.price.domain.vo.UpdateIncomeBillFeeRequest; import com.bms.price.domain.vo.TmsPriceBillRequest; import com.bms.price.domain.vo.TmsPriceQuoteRequest; @@ -312,4 +319,64 @@ public class BillingController extends BaseController TmsPriceQuoteResult result = prcOrderBillService.bill(request); return success(result); } + + /** Update charge_weight (keep weight), then rebill. */ + @PreAuthorize("@ss.hasPermi('price:billing:chargeWeightEdit')") + @Log(title = "Billing charge weight", businessType = BusinessType.UPDATE) + @PutMapping("/bill/chargeWeight") + public AjaxResult updateChargeWeight(@Validated @RequestBody UpdateChargeWeightRequest request) + { + return success(prcOrderBillService.updateChargeWeight(request.getBillId(), request.getChargeWeight())); + } + + /** + * Download import template. fillByFilter=true fills orderNo/customer from list filters. + */ + @PreAuthorize("@ss.hasPermi('price:billing:chargeWeightEdit')") + @PostMapping("/bill/chargeWeight/importTemplate") + public void importChargeWeightTemplate(HttpServletResponse response, + @RequestBody(required = false) ChargeWeightTemplateRequest request) + { + if (request == null) + { + request = new ChargeWeightTemplateRequest(); + } + List rows = prcOrderBillService.buildChargeWeightTemplateRows(request); + ExcelUtil util = new ExcelUtil(ChargeWeightImportRow.class); + if (rows == null || rows.isEmpty()) + { + util.importTemplateExcel(response, "charge_weight"); + return; + } + util.exportExcel(response, rows, "charge_weight"); + } + + /** + * Async import charge_weight updates and rebill. + * Returns taskId; poll /bill/chargeWeight/import/task/{taskId}. + */ + @PreAuthorize("@ss.hasPermi('price:billing:chargeWeightEdit')") + @Log(title = "Billing charge weight import", businessType = BusinessType.IMPORT) + @PostMapping("/bill/chargeWeight/importData") + public AjaxResult importChargeWeight(MultipartFile file) throws Exception + { + ExcelUtil util = new ExcelUtil(ChargeWeightImportRow.class); + List rows = util.importExcel(file.getInputStream()); + String taskId = prcOrderBillService.submitImportChargeWeight(rows); + Map payload = new HashMap(); + payload.put("taskId", taskId); + return success(payload); + } + + @PreAuthorize("@ss.hasPermi('price:billing:chargeWeightEdit')") + @GetMapping("/bill/chargeWeight/import/task/{taskId}") + public AjaxResult getChargeWeightImportTask(@PathVariable String taskId) + { + ChargeWeightImportTask task = prcOrderBillService.getChargeWeightImportTask(taskId); + if (task == null) + { + return error(MessageUtils.message("price.bill.fee.batch.taskNotExists")); + } + return success(task); + } } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/ChargeWeightImportTask.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/ChargeWeightImportTask.java new file mode 100644 index 00000000..a7f81008 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/ChargeWeightImportTask.java @@ -0,0 +1,103 @@ +package com.bms.price.domain; + +import java.io.Serializable; + +/** Async charge-weight import progress (Redis). */ +public class ChargeWeightImportTask 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; + } +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/ChargeWeightImportRow.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/ChargeWeightImportRow.java new file mode 100644 index 00000000..05c79a74 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/ChargeWeightImportRow.java @@ -0,0 +1,22 @@ +package com.bms.price.domain.vo; + +import java.io.Serializable; +import java.math.BigDecimal; +import com.bms.common.annotation.Excel; +import lombok.Data; + +/** Excel row for batch charge_weight update. */ +@Data +public class ChargeWeightImportRow implements Serializable +{ + private static final long serialVersionUID = 1L; + + @Excel(name = "订单号") + private String orderNo; + + @Excel(name = "客户") + private String customer; + + @Excel(name = "人工修改计费重") + private BigDecimal chargeWeight; +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/ChargeWeightTemplateRequest.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/ChargeWeightTemplateRequest.java new file mode 100644 index 00000000..7bbb6e30 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/ChargeWeightTemplateRequest.java @@ -0,0 +1,24 @@ +package com.bms.price.domain.vo; + +import java.io.Serializable; +import java.util.List; +import lombok.Data; + +/** Download charge-weight import template; optionally fill order/customer by list filters. */ +@Data +public class ChargeWeightTemplateRequest implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** true = fill orderNo/customer from current list filters */ + private boolean fillByFilter; + + private List orderNos; + private List trackingNos; + private List transferNos; + private List customerCodes; + private List productCodes; + private String creditArrearsStatus; + private String beginBizDate; + private String endBizDate; +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/TmsPriceQuoteRequest.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/TmsPriceQuoteRequest.java index ecb9e9d6..864a5b77 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/TmsPriceQuoteRequest.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/TmsPriceQuoteRequest.java @@ -35,6 +35,6 @@ public class TmsPriceQuoteRequest implements Serializable /** yyyy-MM-dd or yyyy-MM-dd HH:mm:ss; quote defaults today; bill required */ private String bizDate; private Integer tmsId; - /** Header X-Api-Code 优先;Body 兜底 */ + /** Header X-Api-Code first; body as fallback */ private String apiCode; } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/UpdateChargeWeightRequest.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/UpdateChargeWeightRequest.java new file mode 100644 index 00000000..8ad7ff6d --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/UpdateChargeWeightRequest.java @@ -0,0 +1,22 @@ +package com.bms.price.domain.vo; + +import java.io.Serializable; +import java.math.BigDecimal; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** Single-bill update for charge_weight (manual override; weight kept). */ +@Data +public class UpdateChargeWeightRequest implements Serializable +{ + private static final long serialVersionUID = 1L; + + @NotBlank(message = "billId is required") + private String billId; + + @NotNull(message = "chargeWeight is required") + @DecimalMin(value = "0.001", message = "chargeWeight must be > 0") + private BigDecimal chargeWeight; +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/IPrcOrderBillService.java b/bms-back/bms-system/src/main/java/com/bms/price/service/IPrcOrderBillService.java index 25ff2294..e15cc023 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/IPrcOrderBillService.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/IPrcOrderBillService.java @@ -4,9 +4,12 @@ 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.ChargeWeightImportTask; 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.ChargeWeightImportRow; +import com.bms.price.domain.vo.ChargeWeightTemplateRequest; import com.bms.price.domain.vo.UpdateIncomeBillFeeRequest; import com.bms.price.domain.vo.TmsPriceBillRequest; import com.bms.price.domain.vo.TmsPriceQuoteResult; @@ -77,4 +80,35 @@ public interface IPrcOrderBillService * @return 更新后的计费结果 */ TmsPriceQuoteResult updateCreditArrearsStatus(String billId); + + /** + * Update charge_weight (manual override), keep weight, and rebill. + * Applies data-scope check. + * + * @param billId bill id + * @param chargeWeight new charge weight (kg) + */ + TmsPriceQuoteResult updateChargeWeight(String billId, BigDecimal chargeWeight); + + /** + * Update charge_weight without data-scope check (pre-validated async import). + */ + TmsPriceQuoteResult updateChargeWeightByBillId(String billId, BigDecimal chargeWeight); + + /** + * Submit async import of charge_weight updates; returns taskId. + * + * @param rows excel rows + * @return taskId + */ + String submitImportChargeWeight(List rows); + + /** Poll Redis progress for charge-weight import task. */ + ChargeWeightImportTask getChargeWeightImportTask(String taskId); + + /** + * Build import-template rows. Empty when fillByFilter=false; + * otherwise fill orderNo/customer from list filters (data-scoped). + */ + List buildChargeWeightTemplateRows(ChargeWeightTemplateRequest request); } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcOrderBillServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcOrderBillServiceImpl.java index 92d42d56..01aa1b3c 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcOrderBillServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcOrderBillServiceImpl.java @@ -43,11 +43,16 @@ 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.ChargeWeightImportTask; 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.ChargeWeightImportRow; +import com.bms.price.domain.vo.ChargeWeightTemplateRequest; 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.domain.vo.TmsPriceBillRequest; import com.bms.price.domain.vo.TmsOrderBillStatusResult; import com.bms.price.domain.vo.TmsPriceQuoteFeeLine; @@ -128,8 +133,12 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService @Autowired private PrcBatchAddIncomeFeeAsyncRunner batchAddIncomeFeeAsyncRunner; + @Autowired + private PrcChargeWeightImportAsyncRunner chargeWeightImportAsyncRunner; + 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; @Override @Transactional(rollbackFor = Exception.class) @@ -148,9 +157,12 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService } Date bizDate = resolveBizDate(request.getBizDate()); assertOrderBillEditable(request.getOrderNo()); - BigDecimal weight = request.getWeight(); + PrcOrderBill existed = orderBillMapper.selectPrcOrderBillByOrderNo(request.getOrderNo()); + BigDecimal systemWeight = request.getWeight(); + BigDecimal calcWeight = resolveCalcWeight(existed, systemWeight); + request.setWeight(calcWeight); TmsPriceQuoteResult result = tmsPriceQuoteService.calculate(request); - persistBill(request, weight, bizDate, result); + persistBill(request, systemWeight, bizDate, result); return result; } catch (RuntimeException e) @@ -1097,6 +1109,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService bill.setProductName(result.getProductName()); bill.setCountryCode(result.getCountryCode()); bill.setCountryName(result.getCountryName()); + // weight = system/original; charge_weight = effective (may be manual override) bill.setWeight(weight); bill.setBizDate(bizDate); bill.setTmsId(request.getTmsId() == null ? null : Long.valueOf(request.getTmsId())); @@ -1115,7 +1128,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService bill.setWeightMax(result.getWeightMax()); bill.setWeightLabel(result.getWeightLabel()); bill.setEffectiveDate(result.getEffectiveDate()); - bill.setChargeWeight(result.getChargeWeight()); + bill.setChargeWeight(resolvePersistChargeWeight(weight, result.getChargeWeight())); bill.setFreightRate(result.getFreightRate()); bill.setBaseFreightRate(result.getBaseFreightRate()); bill.setDiscountPercent(result.getDiscountPercent()); @@ -1754,6 +1767,349 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService } } + + @Override + @Transactional(rollbackFor = Exception.class) + public TmsPriceQuoteResult updateChargeWeight(String billId, BigDecimal chargeWeight) + { + if (StringUtils.isEmpty(billId)) + { + throw new ServiceException(MessageUtils.message("price.bill.billIdRequired")); + } + // data-scope check via list query + self.getBill(billId.trim()); + return self.updateChargeWeightByBillId(billId.trim(), chargeWeight); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public TmsPriceQuoteResult updateChargeWeightByBillId(String billId, BigDecimal chargeWeight) + { + 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")); + } + assertOrderBillEditable(bill); + if (BillRefundStatus.REFUNDED.getCode().equalsIgnoreCase(bill.getRefundStatus())) + { + throw new ServiceException("订单已退费,不可修改计费重"); + } + if (chargeWeight == null || chargeWeight.compareTo(BigDecimal.ZERO) <= 0) + { + throw new ServiceException("人工修改计费重必须大于 0"); + } + BigDecimal cw = chargeWeight.setScale(3, RoundingMode.HALF_UP); + TmsPriceBillRequest req = buildBillRequestFromBill(bill); + req.setWeight(cw); + Date bizDate = bill.getBizDate(); + if (bizDate == null) + { + throw new ServiceException(MessageUtils.message("price.bill.bizDateRequired")); + } + TmsPriceQuoteResult result = tmsPriceQuoteService.calculate(req); + // keep original system weight; charge_weight becomes the manual override + persistBill(req, bill.getWeight(), bizDate, result); + return toResult(bill.getBillId()); + } + + @Override + public String submitImportChargeWeight(List rows) + { + if (rows == null || rows.isEmpty()) + { + throw new ServiceException("导入数据不能为空"); + } + java.util.Map dedup = new java.util.LinkedHashMap(); + int lineNo = 1; + int initialFail = 0; + StringBuilder preFailTips = new StringBuilder(); + for (ChargeWeightImportRow row : rows) + { + lineNo++; + if (row == null) + { + continue; + } + String orderNo = row.getOrderNo() == null ? "" : row.getOrderNo().trim(); + if (orderNo.isEmpty()) + { + initialFail++; + if (preFailTips.length() < 800) + { + if (preFailTips.length() > 0) + { + preFailTips.append("; "); + } + preFailTips.append("第").append(lineNo).append("行:订单号为空"); + } + continue; + } + dedup.put(orderNo, row); + } + List items = new ArrayList(); + for (java.util.Map.Entry entry : dedup.entrySet()) + { + String orderNo = entry.getKey(); + ChargeWeightImportRow row = entry.getValue(); + try + { + if (row.getChargeWeight() == null || row.getChargeWeight().compareTo(BigDecimal.ZERO) <= 0) + { + throw new ServiceException("人工修改计费重必须大于 0"); + } + String customer = row.getCustomer() == null ? "" : row.getCustomer().trim(); + if (customer.isEmpty()) + { + throw new ServiceException("客户不能为空"); + } + PrcOrderBill bill = orderBillMapper.selectPrcOrderBillByOrderNo(orderNo); + if (bill == null) + { + throw new ServiceException("订单不存在"); + } + // data-scope in request thread + self.getBill(bill.getBillId()); + if (!customerMatches(bill, customer)) + { + throw new ServiceException("客户与订单不匹配"); + } + if (BillRefundStatus.REFUNDED.getCode().equalsIgnoreCase(bill.getRefundStatus())) + { + throw new ServiceException("订单已退费"); + } + items.add(new ChargeWeightImportWorkItem(bill.getBillId(), orderNo, + row.getChargeWeight().setScale(3, RoundingMode.HALF_UP))); + } + catch (RuntimeException e) + { + initialFail++; + if (preFailTips.length() < 800) + { + String tip = e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage(); + if (preFailTips.length() > 0) + { + preFailTips.append("; "); + } + preFailTips.append(orderNo).append(":").append(tip); + } + } + } + if (items.isEmpty() && initialFail == 0) + { + throw new ServiceException("导入数据不能为空"); + } + if (items.size() > CHARGE_WEIGHT_IMPORT_MAX) + { + throw new ServiceException("导入行数超限,最多 " + + CHARGE_WEIGHT_IMPORT_MAX + " 条"); + } + String taskId = IdUtils.fastSimpleUUID(); + int total = items.size() + initialFail; + chargeWeightImportAsyncRunner.put(taskId, ChargeWeightImportTask.STATUS_PENDING, 0, "queued", + preFailTips.length() > 0 ? preFailTips.toString() : null, total, 0, initialFail); + chargeWeightImportAsyncRunner.run(taskId, items, initialFail); + return taskId; + } + + @Override + public ChargeWeightImportTask getChargeWeightImportTask(String taskId) + { + if (StringUtils.isEmpty(taskId) || chargeWeightImportAsyncRunner == null) + { + return null; + } + return chargeWeightImportAsyncRunner.get(taskId); + } + + @Override + public List buildChargeWeightTemplateRows(ChargeWeightTemplateRequest request) + { + if (request == null || !request.isFillByFilter()) + { + return new ArrayList(); + } + List orderNos = normalizeCodeList(request.getOrderNos()); + List trackingNos = normalizeCodeList(request.getTrackingNos()); + List transferNos = normalizeCodeList(request.getTransferNos()); + List customerCodes = normalizeCodeList(request.getCustomerCodes()); + List productCodes = normalizeCodeList(request.getProductCodes()); + String creditArrearsStatus = StringUtils.isEmpty(request.getCreditArrearsStatus()) ? null + : request.getCreditArrearsStatus().trim(); + 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()) + || (customerCodes != null && !customerCodes.isEmpty()) + || (productCodes != null && !productCodes.isEmpty()) + || StringUtils.isNotEmpty(creditArrearsStatus) + || StringUtils.isNotEmpty(beginBizDate) + || StringUtils.isNotEmpty(endBizDate); + if (!hasFilter) + { + throw new ServiceException("\u8bf7\u5148\u8bbe\u7f6e\u5217\u8868\u7b5b\u9009\u6761\u4ef6"); + } + PrcOrderBill query = new PrcOrderBill(); + query.setOrderNos(orderNos); + query.setTrackingNos(trackingNos); + query.setTransferNos(transferNos); + query.setCustomerCodes(customerCodes); + query.setProductCodes(productCodes); + query.setCreditArrearsStatus(creditArrearsStatus); + if (beginBizDate != null) + { + query.getParams().put("beginBizDate", beginBizDate); + } + if (endBizDate != null) + { + query.getParams().put("endBizDate", endBizDate); + } + List list = self.selectBillList(query); + if (list == null || list.isEmpty()) + { + throw new ServiceException("\u5f53\u524d\u7b5b\u9009\u6761\u4ef6\u4e0b\u6ca1\u6709\u53ef\u5bfc\u51fa\u7684\u8ba2\u5355"); + } + if (list.size() > CHARGE_WEIGHT_IMPORT_MAX) + { + throw new ServiceException("\u7b5b\u9009\u7ed3\u679c\u8d85\u8fc7 " + + CHARGE_WEIGHT_IMPORT_MAX + " \u6761\uff0c\u8bf7\u7f29\u5c0f\u7b5b\u9009\u8303\u56f4"); + } + List rows = new ArrayList(list.size()); + for (PrcOrderBill bill : list) + { + if (bill == null || StringUtils.isEmpty(bill.getOrderNo())) + { + continue; + } + if (BillRefundStatus.REFUNDED.getCode().equalsIgnoreCase(bill.getRefundStatus())) + { + continue; + } + ChargeWeightImportRow row = new ChargeWeightImportRow(); + row.setOrderNo(bill.getOrderNo()); + String customer = StringUtils.isNotEmpty(bill.getCustomerCode()) ? bill.getCustomerCode() + : bill.getCustomerName(); + row.setCustomer(customer); + row.setChargeWeight(null); + rows.add(row); + } + if (rows.isEmpty()) + { + throw new ServiceException("\u5f53\u524d\u7b5b\u9009\u6761\u4ef6\u4e0b\u6ca1\u6709\u53ef\u5bfc\u51fa\u7684\u8ba2\u5355"); + } + return rows; + } + + private TmsPriceQuoteResult toResult(String billId) + { + PrcOrderBill bill = orderBillMapper.selectPrcOrderBillById(billId); + if (bill == null) + { + throw new ServiceException(MessageUtils.message("price.bill.notFound")); + } + return toResult(bill, incomeBillFeeMapper.selectByBillId(bill.getBillId()), + payBillFeeMapper.selectByBillId(bill.getBillId())); + } + + /** charge_weight differs from weight => treated as manual override. */ + private static boolean isManualChargeWeight(PrcOrderBill bill) + { + if (bill == null || bill.getChargeWeight() == null || bill.getWeight() == null) + { + return false; + } + return bill.getChargeWeight().compareTo(bill.getWeight()) != 0; + } + + /** Prefer manual charge_weight when present; otherwise system weight. */ + private static BigDecimal resolveCalcWeight(PrcOrderBill existed, BigDecimal systemWeight) + { + if (isManualChargeWeight(existed)) + { + return existed.getChargeWeight(); + } + return systemWeight; + } + + /** + * Persist charge_weight: keep manual override when calc weight != system weight, + * or when existing bill already had a manual override. + */ + private static BigDecimal resolvePersistChargeWeight(BigDecimal systemWeight, BigDecimal resultChargeWeight) + { + // calc weight != system weight => keep result as manual charge_weight + if (systemWeight != null && resultChargeWeight != null + && systemWeight.compareTo(resultChargeWeight) != 0) + { + return resultChargeWeight; + } + return resultChargeWeight; + } + + private static boolean customerMatches(PrcOrderBill bill, String customer) + { + if (bill == null || StringUtils.isEmpty(customer)) + { + return false; + } + String code = bill.getCustomerCode() == null ? "" : bill.getCustomerCode().trim(); + String name = bill.getCustomerName() == null ? "" : bill.getCustomerName().trim(); + return customer.equalsIgnoreCase(code) || customer.equals(name); + } + + private TmsPriceBillRequest buildBillRequestFromBill(PrcOrderBill bill) + { + TmsPriceBillRequest req = new TmsPriceBillRequest(); + req.setOrderNo(bill.getOrderNo()); + req.setTrackingNo(bill.getTrackingNo()); + req.setTransferNo(bill.getTransferNo()); + req.setBelongSaler(bill.getBelongSaler()); + req.setBelongSalerCode(bill.getBelongSalerCode()); + req.setSettlementMan(bill.getSettlementMan()); + req.setSettlementManCode(bill.getSettlementManCode()); + req.setServiceMan(bill.getServiceMan()); + req.setServiceManCode(bill.getServiceManCode()); + req.setCustomerId(bill.getCustomerId()); + req.setCustomerCode(bill.getCustomerCode()); + req.setCustomerName(bill.getCustomerName()); + req.setProductCode(bill.getProductCode()); + req.setCountryCode(bill.getCountryCode()); + req.setCountryName(bill.getCountryName()); + req.setDestPostcode(bill.getDestPostcode()); + req.setPieces(bill.getPieces()); + req.setActualWeight(bill.getActualWeight()); + req.setReceiveWeight(bill.getReceiveWeight()); + req.setVolume(bill.getVolume()); + req.setVolumeWeight(bill.getVolumeWeight()); + req.setPkgLength(bill.getPkgLength()); + req.setPkgWidth(bill.getPkgWidth()); + req.setPkgHeight(bill.getPkgHeight()); + req.setShipperName(bill.getShipperName()); + req.setConsigneeName(bill.getConsigneeName()); + req.setWarehouseCode(bill.getWarehouseCode()); + req.setOrderTime(bill.getOrderTime()); + req.setOrderRemark(bill.getOrderRemark()); + if (bill.getTmsId() != null) + { + req.setTmsId(Integer.valueOf(bill.getTmsId().intValue())); + } + if (bill.getBizDate() != null) + { + req.setBizDate(DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", bill.getBizDate())); + } + BigDecimal w = bill.getChargeWeight() != null ? bill.getChargeWeight() : bill.getWeight(); + req.setWeight(w); + return req; + } + + private Date resolveBizDate(String bizDate) { if (StringUtils.isEmpty(bizDate)) diff --git a/bms-back/bms-system/src/main/java/com/bms/price/task/PrcChargeWeightImportAsyncRunner.java b/bms-back/bms-system/src/main/java/com/bms/price/task/PrcChargeWeightImportAsyncRunner.java new file mode 100644 index 00000000..f197b4d4 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/task/PrcChargeWeightImportAsyncRunner.java @@ -0,0 +1,195 @@ +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.ChargeWeightImportTask; +import com.bms.price.service.IPrcOrderBillService; + +/** + * Async runner: import charge_weight updates and rebill each order. + */ +@Component +public class PrcChargeWeightImportAsyncRunner +{ + private static final Logger log = LoggerFactory.getLogger(PrcChargeWeightImportAsyncRunner.class); + + public static final String CACHE_KEY_PREFIX = "price:billing:chargeWeight:import:task:"; + public static final int TTL_MINUTES = 60; + + @Autowired + @Lazy + private IPrcOrderBillService orderBillService; + + @Autowired + private RedisCache redisCache; + + /** + * @param items pre-validated rows with billId filled in remark field unused; + * use orderNo + chargeWeight; billId stored in customer field as billId carrier + * -- better use a dedicated work item. See run(List of Object[]). + */ + @Async("threadPoolTaskExecutor") + public void run(String taskId, List items, int initialFail) + { + int total = (items == null ? 0 : items.size()) + Math.max(initialFail, 0); + int success = 0; + int fail = Math.max(initialFail, 0); + StringBuilder failTips = new StringBuilder(); + try + { + put(taskId, ChargeWeightImportTask.STATUS_RUNNING, 1, "running", null, total, 0, fail); + int workSize = items == null ? 0 : items.size(); + if (workSize == 0) + { + if (fail > 0) + { + put(taskId, ChargeWeightImportTask.STATUS_SUCCESS, 100, + "success=0, fail=" + fail, "all rows failed validation", total, 0, fail); + } + else + { + put(taskId, ChargeWeightImportTask.STATUS_FAIL, 100, "failed", "no rows", 0, 0, 0); + } + return; + } + for (int i = 0; i < workSize; i++) + { + ChargeWeightImportWorkItem item = items.get(i); + try + { + orderBillService.updateChargeWeightByBillId(item.getBillId(), item.getChargeWeight()); + 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(item.getOrderNo()).append(":").append(tip); + } + log.warn("charge weight import failed taskId={} orderNo={}", taskId, item.getOrderNo(), e); + } + int done = i + 1 + Math.max(initialFail, 0); + int percent = Math.min(99, (int) Math.floor(100.0 * done / Math.max(total, 1))); + put(taskId, ChargeWeightImportTask.STATUS_RUNNING, percent, + "processing " + done + "/" + total, null, total, success, fail); + } + String msg = "success=" + success + ", fail=" + fail; + if (fail > 0 && failTips.length() > 0) + { + put(taskId, ChargeWeightImportTask.STATUS_SUCCESS, 100, msg, failTips.toString(), + total, success, fail); + } + else + { + put(taskId, ChargeWeightImportTask.STATUS_SUCCESS, 100, msg, null, total, success, fail); + } + log.info("charge weight import done taskId={} total={} success={} fail={}", + taskId, total, success, fail); + } + catch (Exception e) + { + log.error("charge weight import aborted taskId={}", taskId, e); + put(taskId, ChargeWeightImportTask.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) + { + ChargeWeightImportTask task = new ChargeWeightImportTask(); + 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 ChargeWeightImportTask 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; + } + + /** Pre-validated work item for async import. */ + public static class ChargeWeightImportWorkItem implements java.io.Serializable + { + private static final long serialVersionUID = 1L; + private String billId; + private String orderNo; + private BigDecimal chargeWeight; + + public ChargeWeightImportWorkItem() + { + } + + public ChargeWeightImportWorkItem(String billId, String orderNo, BigDecimal chargeWeight) + { + this.billId = billId; + this.orderNo = orderNo; + this.chargeWeight = chargeWeight; + } + + public String getBillId() + { + return billId; + } + + public void setBillId(String billId) + { + this.billId = billId; + } + + public String getOrderNo() + { + return orderNo; + } + + public void setOrderNo(String orderNo) + { + this.orderNo = orderNo; + } + + public BigDecimal getChargeWeight() + { + return chargeWeight; + } + + public void setChargeWeight(BigDecimal chargeWeight) + { + this.chargeWeight = chargeWeight; + } + } +} diff --git a/bms-back/sql/price/billing_charge_weight_menu.sql b/bms-back/sql/price/billing_charge_weight_menu.sql new file mode 100644 index 00000000..699bb958 --- /dev/null +++ b/bms-back/sql/price/billing_charge_weight_menu.sql @@ -0,0 +1,20 @@ +-- billing list: edit/import charge weight (reuse charge_weight; keep weight) +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 2331, N'修改计费重', m.menu_id, 21, '', NULL, 1, 0, 'F', '0', '0', 'price:billing:chargeWeightEdit', '#', '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 = 2331 OR x.perms = 'price:billing:chargeWeightEdit') +LIMIT 1; + +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT 1, 2331 +FROM DUAL +WHERE EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2331) + AND NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = 2331); diff --git a/bms-front/public/templates/billing/charge-weight-import-template.xlsx b/bms-front/public/templates/billing/charge-weight-import-template.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..057eeb5d898bfa05068b8c32d34a3157291d33b1 GIT binary patch literal 5117 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 b?!O32R}%-F`v3s==(iktv{(L!#sL2Tdi>|> literal 0 HcmV?d00001 diff --git a/bms-front/src/api/price/billing.js b/bms-front/src/api/price/billing.js index 96454daf..41e2c56a 100644 --- a/bms-front/src/api/price/billing.js +++ b/bms-front/src/api/price/billing.js @@ -1,4 +1,8 @@ import request from '@/utils/request' +import { saveAs } from 'file-saver' +import { blobValidate } from '@/utils/bms' +import { ElLoading } from 'element-plus' +import errorCode from '@/utils/errorCode' /** Quote trial for billing center */ export function quoteBilling(data) { @@ -187,3 +191,89 @@ export function updateCreditArrears(billIds) { data: billIds }) } + +/** 计费列表:单票修改计费重(写 charge_weight,保留 weight)并重新计费 */ +export function updateChargeWeight(data) { + return request({ + url: '/price/billing/bill/chargeWeight', + method: 'put', + data + }) +} + +export function getChargeWeightImportTask(taskId) { + return request({ + url: '/price/billing/bill/chargeWeight/import/task/' + taskId, + method: 'get' + }) +} + +function pickChargeWeightImportTaskId(res) { + const d = res && res.data + if (!d) return '' + if (typeof d === 'string' || typeof d === 'number') return String(d) + return String(d.taskId || d.id || '') +} + +async function pollChargeWeightImportTask(taskId, onProgress) { + const started = Date.now() + const maxMs = 30 * 60 * 1000 + while (Date.now() - started < maxMs) { + const res = await getChargeWeightImportTask(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 || 'charge weight import failed') + err.task = task + throw err + } + await new Promise(resolve => setTimeout(resolve, 800)) + } + throw new Error('charge weight import timeout') +} + +/** + * Async import charge weight + rebill; poll until done. + * @param {File|Blob} file + * @param {(task: object) => void} [onProgress] + */ +export async function importChargeWeightAsync(file, onProgress) { + const formData = new FormData() + formData.append('file', file) + const submitRes = await request({ + url: '/price/billing/bill/chargeWeight/importData', + method: 'post', + data: formData, + headers: { 'Content-Type': 'multipart/form-data', repeatSubmit: false }, + // Backend parses Excel + validates rows before returning taskId + timeout: 10 * 60 * 1000 + }) + const taskId = pickChargeWeightImportTaskId(submitRes) + if (!taskId) throw new Error('missing charge weight import taskId') + return pollChargeWeightImportTask(taskId, onProgress) +} + +/** Download charge-weight import template (optionally fill by list filters). */ +export async function downloadChargeWeightTemplate(data, filename) { + const loading = ElLoading.service({ text: '正在下载模板,请稍候', background: 'rgba(0, 0, 0, 0.7)' }) + try { + const blobData = await request({ + url: '/price/billing/bill/chargeWeight/importTemplate', + method: 'post', + data: data || {}, + responseType: 'blob', + timeout: 120000 + }) + if (blobValidate(blobData)) { + saveAs(new Blob([blobData]), filename || ('批量修改计费重导入模板_' + Date.now() + '.xlsx')) + return + } + const resText = await blobData.text() + const rspObj = JSON.parse(resText) + const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default'] || '下载模板失败' + throw new Error(errMsg) + } finally { + loading.close() + } +} diff --git a/bms-front/src/components/ExcelImportDialog/index.vue b/bms-front/src/components/ExcelImportDialog/index.vue index 54e1d28a..804642fa 100644 --- a/bms-front/src/components/ExcelImportDialog/index.vue +++ b/bms-front/src/components/ExcelImportDialog/index.vue @@ -186,6 +186,11 @@ const props = defineProps({ type: Function, default: null }, + // Custom template download: () => void | Promise; overrides templateUrl / templateAction + templateDownloadHandler: { + type: Function, + default: null + }, // 确定按钮文字 submitText: { type: String, @@ -217,7 +222,7 @@ const uploadUrl = computed(() => { return import.meta.env.VITE_APP_BASE_API + props.action + '?' + query.toString() }) -const hasTemplate = computed(() => !!(props.templateUrl || props.templateAction)) +const hasTemplate = computed(() => !!(props.templateDownloadHandler || props.templateUrl || props.templateAction)) // 打开对话框(供父组件通过 ref 调用) function open() { @@ -231,6 +236,10 @@ function open() { }) } +function close() { + visible.value = false +} + function handleClosed() { isUploading.value = false progressPercent.value = 0 @@ -238,8 +247,17 @@ function handleClosed() { selectedFile.value = null } -// 下载模板:静态文件直接 ,接口走 proxy.download -function handleDownloadTemplate() { +// 下载模板:自定义 handler > 静态文件 > 接口 proxy.download +async function handleDownloadTemplate() { + if (typeof props.templateDownloadHandler === 'function') { + try { + await props.templateDownloadHandler() + } catch (e) { + const tip = (e && e.message) ? e.message : '下载模板失败' + proxy.$modal.msgError(tip) + } + return + } if (props.templateUrl) { const link = document.createElement('a') link.href = props.templateUrl @@ -353,7 +371,7 @@ async function handleSubmit() { uploadRef.value?.submit?.() } -defineExpose({ open }) +defineExpose({ open, close })