diff --git a/bms-back/bms-admin/src/main/java/com/bms/web/controller/data/PayPostcodeController.java b/bms-back/bms-admin/src/main/java/com/bms/web/controller/data/PayPostcodeController.java index fe81bfad..bddf6764 100644 --- a/bms-back/bms-admin/src/main/java/com/bms/web/controller/data/PayPostcodeController.java +++ b/bms-back/bms-admin/src/main/java/com/bms/web/controller/data/PayPostcodeController.java @@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; 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; @@ -43,6 +44,15 @@ public class PayPostcodeController extends BaseController return getDataTable(list); } + /** 按国家去重区块,供附加费编辑分区下拉 */ + @PreAuthorize("@ss.hasPermi('data:payPostcode:list')") + @GetMapping("/blockOptions") + public AjaxResult blockOptions(@RequestParam(required = false) String countryCode, + @RequestParam(required = false) String countryName) + { + return success(payPostcodeService.selectBlockTitlesByCountry(countryCode, countryName)); + } + @PreAuthorize("@ss.hasPermi('data:payPostcode:query')") @GetMapping("/{groupId}") public AjaxResult getInfo(@PathVariable Long groupId) 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 bd66466a..5ba7bd8e 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 @@ -147,6 +147,22 @@ public class BillingController extends BaseController @PreAuthorize("@ss.hasPermi('price:billing:bill')") @GetMapping("/bill/list") public TableDataInfo billList(PrcOrderBill query) + { + return doBillList(query); + } + + /** + * POST 计费列表:大批单号等筛选放请求体,避免 GET URL 过长触发 HTTP 431。 + * pageNum/pageSize 仍走 URL 查询参数,供 PageHelper 读取。 + */ + @PreAuthorize("@ss.hasPermi('price:billing:bill')") + @PostMapping("/bill/list") + public TableDataInfo billListPost(@RequestBody(required = false) PrcOrderBill query) + { + return doBillList(query); + } + + private TableDataInfo doBillList(PrcOrderBill query) { if (query == null) { @@ -220,6 +236,19 @@ public class BillingController extends BaseController return getDataTable(list); } + /** + * POST 列表:筛选(尤其是大批量单号)放请求体,避免 GET URL 过长触发 HTTP 431。 + * pageNum/pageSize 仍走 URL 查询参数,供 TableSupport/PageHelper 读取。 + */ + @PreAuthorize("@ss.hasPermi('price:orderBill:list')") + @PostMapping("/orderBill/list") + public TableDataInfo orderBillListPost(@RequestBody(required = false) PrcOrderBill query) + { + startPage(); + List list = prcOrderBillService.selectBillList(query == null ? new PrcOrderBill() : query); + return getDataTable(list); + } + /** 订单列表弹窗:按 billId 查询计费失败原因(受数据权限控制)。 */ @PreAuthorize("@ss.hasPermi('price:orderBill:list')") diff --git a/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/SurchargeController.java b/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/SurchargeController.java index effc5f6b..fd1a7493 100644 --- a/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/SurchargeController.java +++ b/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/SurchargeController.java @@ -20,6 +20,7 @@ import com.bms.common.core.page.TableDataInfo; import com.bms.common.enums.BusinessType; import com.bms.price.domain.PrcSurcharge; import com.bms.price.domain.PrcSurchargeRel; +import com.bms.price.service.IPayPostcodeService; import com.bms.price.service.IPrcSurchargeRelService; import com.bms.price.service.IPrcSurchargeService; @@ -38,6 +39,9 @@ public class SurchargeController extends BaseController @Autowired private IPrcSurchargeRelService surchargeRelService; + @Autowired + private IPayPostcodeService payPostcodeService; + @PreAuthorize("@ss.hasPermi('price:surcharge:list')") @GetMapping("/list") public TableDataInfo list(PrcSurcharge surcharge) @@ -47,6 +51,15 @@ public class SurchargeController extends BaseController return getDataTable(list); } + /** 按国家从应付邮编配置拉取区块/分区选项 */ + @PreAuthorize("@ss.hasPermi('price:surcharge:list') or @ss.hasPermi('price:surcharge:query') or @ss.hasPermi('price:surcharge:add') or @ss.hasPermi('price:surcharge:edit')") + @GetMapping("/blockOptions") + public AjaxResult blockOptions(@RequestParam(required = false) String countryCode, + @RequestParam(required = false) String countryName) + { + return success(payPostcodeService.selectBlockTitlesByCountry(countryCode, countryName)); + } + @PreAuthorize("@ss.hasPermi('price:surcharge:query')") @GetMapping("/{scId}") public AjaxResult getInfo(@PathVariable Long scId) diff --git a/bms-back/bms-admin/src/main/java/com/bms/web/controller/receivable/ArBillController.java b/bms-back/bms-admin/src/main/java/com/bms/web/controller/receivable/ArBillController.java index c66b2a04..8a5495c7 100644 --- a/bms-back/bms-admin/src/main/java/com/bms/web/controller/receivable/ArBillController.java +++ b/bms-back/bms-admin/src/main/java/com/bms/web/controller/receivable/ArBillController.java @@ -325,9 +325,16 @@ public class ArBillController extends BaseController { } /** - * 下载已成功生成的导出文件;文件内容由服务层从临时文件或配置存储读取。 + * 获取已成功生成导出文件的下载方式;OSS 文件返回短时签名链接,本地历史文件走原下载接口。 */ @PreAuthorize("@ss.hasPermi('ar:bill:download')") + @GetMapping("/export/{exportId}/download-url") + public AjaxResult exportDownloadInfo(@PathVariable Long exportId) { + return success(billService.getExportDownloadInfo(exportId)); + } + + /** 下载本地历史导出文件;OSS 文件由前端通过签名链接直接下载。 */ + @PreAuthorize("@ss.hasPermi('ar:bill:download')") @GetMapping("/export/{exportId}/download") public void download(@PathVariable Long exportId, HttpServletResponse response) throws Exception { billService.downloadExport(exportId, response); diff --git a/bms-back/bms-admin/src/main/resources/application.yml b/bms-back/bms-admin/src/main/resources/application.yml index b04a644e..fd1387ea 100644 --- a/bms-back/bms-admin/src/main/resources/application.yml +++ b/bms-back/bms-admin/src/main/resources/application.yml @@ -191,7 +191,7 @@ xss: # 过滤开关 enabled: true # 排除链接(多个用逗号分隔) - excludes: /system/notice + excludes: /system/notice,/tool/migration # 匹配链接 urlPatterns: /system/*,/monitor/*,/tool/* diff --git a/bms-back/bms-common/src/main/java/com/bms/common/utils/file/OssStorage.java b/bms-back/bms-common/src/main/java/com/bms/common/utils/file/OssStorage.java index 8226c3b7..f9732481 100644 --- a/bms-back/bms-common/src/main/java/com/bms/common/utils/file/OssStorage.java +++ b/bms-back/bms-common/src/main/java/com/bms/common/utils/file/OssStorage.java @@ -1,18 +1,25 @@ package com.bms.common.utils.file; +import java.net.URL; +import java.net.URLEncoder; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Date; import com.aliyun.oss.ClientBuilderConfiguration; +import com.aliyun.oss.HttpMethod; import com.aliyun.oss.OSS; import com.aliyun.oss.OSSClientBuilder; import com.aliyun.oss.model.CannedAccessControlList; +import com.aliyun.oss.model.GeneratePresignedUrlRequest; import com.aliyun.oss.model.OSSObject; import com.aliyun.oss.model.ObjectMetadata; import com.aliyun.oss.model.PutObjectRequest; +import com.aliyun.oss.model.ResponseHeaderOverrides; import com.bms.common.config.OssProperties; import com.bms.common.exception.ServiceException; import org.springframework.stereotype.Component; @@ -67,6 +74,28 @@ public class OssStorage { } } + /** 为私有 OSS 对象生成带附件文件名的限时下载链接。 */ + public String generatePresignedDownloadUrl(String storageKey, String fileName, Date expiresAt) { + String objectKey = resolveObjectKey(storageKey); + OSS client = createClient(); + try { + GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest( + properties.getBucket(), objectKey, HttpMethod.GET); + request.setExpiration(expiresAt); + + String downloadName = StringUtils.hasText(fileName) ? fileName : "账单导出.xlsx"; + String encodedFileName = URLEncoder.encode(downloadName, StandardCharsets.UTF_8).replace("+", "%20"); + ResponseHeaderOverrides responseHeaders = new ResponseHeaderOverrides(); + responseHeaders.setContentDisposition("attachment; filename=" + encodedFileName + + ";filename*=utf-8''" + encodedFileName); + request.setResponseHeaders(responseHeaders); + URL signedUrl = client.generatePresignedUrl(request); + return signedUrl.toExternalForm(); + } finally { + client.shutdown(); + } + } + public void delete(String storageKey) { String objectKey = resolveObjectKey(storageKey); OSS client = createClient(); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/PaySurcharge.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/PaySurcharge.java index c44a1167..3c5fcae6 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/domain/PaySurcharge.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/PaySurcharge.java @@ -73,4 +73,10 @@ public class PaySurcharge private BigDecimal weightMax; private Integer sortNo; + + /** + * 是否计费并落库:false 不写入 pay_surcharge;null/缺省视为要计费(兼容应付全量保存)。 + * 仅作为前端传参,不落库字段。 + */ + private Boolean needBill; } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/PrcSurcharge.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/PrcSurcharge.java index 212ab670..99266a8a 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/domain/PrcSurcharge.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/PrcSurcharge.java @@ -16,8 +16,10 @@ public class PrcSurcharge extends BaseEntity private String scName; /** 字典 bms_fee_type */ private String feeType; - /** FIXED / WEIGHT / RATE */ + /** 计费方式:ALWAYS/每票都收、POSTCODE/命中邮编、ZONE/命中分区、EXTRA_KG/超重续收、WEIGHT/超重、SIZE/超尺寸、RETURN/退件、MANUAL/暂不计费 */ private String chargeType; + /** 渠道产品(字典 payment_product_type.dict_value) */ + private String dictValue; private BigDecimal chargeValue; private String currency; /** 字典 bms_round_mode:UP / DOWN / HALF_UP */ @@ -29,6 +31,8 @@ public class PrcSurcharge extends BaseEntity public static final String IS_DEL_YES = "1"; private String isDel; private String countryCode; + /** 分区/区块(对应 pay_postcode_group.block_title) */ + private String blockTitle; private String scNote; private Long tmsId; } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/TmsPriceBillRequest.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/TmsPriceBillRequest.java index 899e6e3e..0cffe5be 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/TmsPriceBillRequest.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/TmsPriceBillRequest.java @@ -60,6 +60,9 @@ public class TmsPriceBillRequest extends TmsPriceQuoteRequest @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date pickupDate; private String orderRemark; + /** 上游 PRODUCT_FLOOR_CODE;打单计费在账单落库前也可直接用 */ + private String productFloorCode; + /** 上游 TAB_ORDER 整行,用于写入 prc_order_bill_ext 扩展字段 */ private Map upstreamRow; 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 007fc7e1..c260b381 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 @@ -22,7 +22,7 @@ public class TmsPriceQuoteRequest implements Serializable private String productCode; /** - * 打单套餐(LABEL)试算/计费用渠道ID;计费也可从订单扩展 productChannelId 解析。 + * 打单套餐(LABEL)试算用渠道ID;计费优先取订单 product_floor_code。 */ private String channelId; diff --git a/bms-back/bms-system/src/main/java/com/bms/price/mapper/PayPostcodeMapper.java b/bms-back/bms-system/src/main/java/com/bms/price/mapper/PayPostcodeMapper.java index 02bb6c98..8097bd09 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/mapper/PayPostcodeMapper.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/mapper/PayPostcodeMapper.java @@ -10,6 +10,10 @@ public interface PayPostcodeMapper { List selectGroupList(PayPostcodeGroup query); + /** 按国家去重区块标题 */ + List selectBlockTitlesByCountry(@Param("countryCode") String countryCode, + @Param("countryName") String countryName); + PayPostcodeGroup selectGroupById(Long groupId); int updateGroup(PayPostcodeGroup group); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/oracle/OracleOrderBillMapper.java b/bms-back/bms-system/src/main/java/com/bms/price/oracle/OracleOrderBillMapper.java index 3a857bbd..3e4171dd 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/oracle/OracleOrderBillMapper.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/oracle/OracleOrderBillMapper.java @@ -44,6 +44,7 @@ public class OracleOrderBillMapper } req.setProductCode(product); req.setProductName(firstStr(row, "LINE_NAME", "PRODUCT_NAME")); + req.setProductFloorCode(firstStr(row, "PRODUCT_FLOOR_CODE")); req.setCountryCode(firstStr(row, "COUNTRY")); String countryName = firstStr(row, "COUNTRY_NAME", "COUNTRY_EINGTH_NAME", "COUNTRY"); req.setCountryName(countryName); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/pay/LabelArCostCalculator.java b/bms-back/bms-system/src/main/java/com/bms/price/pay/LabelArCostCalculator.java new file mode 100644 index 00000000..b58df64d --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/pay/LabelArCostCalculator.java @@ -0,0 +1,830 @@ +package com.bms.price.pay; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import com.bms.common.enums.PaymentMesEnum; +import com.bms.price.domain.PayCostLine; +import com.bms.price.domain.PayOrderCostResult; +import com.bms.price.domain.PayProduct; +import com.bms.price.domain.PaySurcharge; +import com.bms.price.domain.PayWeightBand; + +/** + * 打单套餐应收计费(与应付 {@link PayCostCalculator} 独立)。 + * 按打单 AR 价卡的重量段、附加费计算;后续仅改本类,不影响应付计费。 + * 重量段价格默认是该段的一票价格;单位写明每公斤时才乘重量。 + */ +public final class LabelArCostCalculator +{ + private static final ZoneId SHANGHAI = ZoneId.of("GMT+8"); + + private static final Set REFUND_STATUS_SET = Set.of(9, 10); + + private LabelArCostCalculator() + { + } + + /** + * 按重量、尺寸和邮编计算一票打单应收。不指定地区,也不传入偏远命中结果。 + * + * @param product 当前价卡版本,含重量段和附加费 + * @param weight 计费重,公斤 + * @param length 长,厘米 + * @param width 宽,厘米 + * @param height 高,厘米 + * @param postcode 收件邮编 + * @param bizDate 取价日,用来判断附加费是否在有效期内 + * @param orderStatus 订单状态,退件时才收退件类附加费 + * @return 费用明细和算不了的附加费说明 + */ + public static PayOrderCostResult calculate(PayProduct product, BigDecimal weight, BigDecimal length, BigDecimal width, + BigDecimal height, String postcode, Date bizDate, Integer orderStatus) + { + return calculate(product, weight, length, width, height, postcode, null, bizDate, orderStatus); + } + + /** + * 在已经选定价卡地区时计算打单应收。偏远仍按价卡上的邮编条件判断。 + * + * @param product 当前价卡版本 + * @param weight 计费重,公斤 + * @param length 长,厘米 + * @param width 宽,厘米 + * @param height 高,厘米 + * @param postcode 收件邮编 + * @param zoneName 邮编对上的价卡地区,空表示不按地区过滤 + * @param bizDate 取价日 + * @param orderStatus 订单状态 + * @return 费用明细和算不了的附加费说明 + */ + public static PayOrderCostResult calculate(PayProduct product, BigDecimal weight, BigDecimal length, BigDecimal width, + BigDecimal height, String postcode, String zoneName, Date bizDate, Integer orderStatus) + { + return calculate(product, weight, length, width, height, postcode, zoneName, bizDate, orderStatus, null); + } + + /** + * 计算一票打单应收。remoteHits 为空列表表示产品有偏远邮编但这次没命中;为 null 表示没有这份配置,仍按价卡上的邮编条件。 + * + * @param product 当前价卡版本 + * @param weight 计费重,公斤 + * @param length 长,厘米 + * @param width 宽,厘米 + * @param height 高,厘米 + * @param postcode 收件邮编 + * @param zoneName 价卡地区 + * @param bizDate 取价日 + * @param orderStatus 订单状态 + * @param remoteHits 偏远邮编命中结果 + * @return 费用明细和算不了的附加费说明 + */ + public static PayOrderCostResult calculate(PayProduct product, BigDecimal weight, BigDecimal length, BigDecimal width, + BigDecimal height, String postcode, String zoneName, Date bizDate, Integer orderStatus, List remoteHits) + { + PayOrderCostResult result = new PayOrderCostResult(); + result.setFees(new ArrayList<>()); + result.setSkipped(new ArrayList<>()); + if (product == null) + { + result.setMessage(PaymentMesEnum.BMS_PAY_NO_PRODUCT.getCnMes()); + return result; + } + result.setProductId(product.getProductId()); + result.setProductName(product.getProductName()); + if (weight == null || weight.compareTo(BigDecimal.ZERO) <= 0) + { + result.setMessage(PaymentMesEnum.BMS_PAY_NO_WEIGHT.getCnMes()); + return result; + } + List usable = usableBands(product.getWeightBands()); + List bands = forZone(usable, zoneName); + if (zoneName != null && !zoneName.isBlank() && bands.isEmpty() && hasZoneFee(product, zoneName)) + { + bands = deliveryBands(usable, product); + } + if (zoneName != null && !zoneName.isBlank() && bands.isEmpty()) + { + result.setMessage("邮编对应的地区「" + zoneName + "」不在价卡重量段里"); + return result; + } + Set zones = zones(bands); + if (zones.size() > 1) + { + result.setMessage("产品「" + product.getProductName() + "」有多个地区(" + String.join("、", zones) + + "),邮编没有对上其中的地区,无法确定运费"); + return result; + } + PayWeightBand band = matchBand(bands, weight); + if (band == null) + { + PayWeightBand top = highestBand(bands); + if (top == null || weight.compareTo(top.getWeightMax()) <= 0 + || !hasExtraKg(product, zoneName, top.getWeightMax(), top.getSectionName())) + { + result.setMessage(outOfRange(product, bands, weight)); + return result; + } + band = top; + } + String currency = currencyOf(product, band); + BigDecimal freight = bandAmount(band, weight); + String freightRemark = band.getWeightLabel() + " " + plain(band.getPrice()) + " " + blank(band.getPriceUnit()); + if (zoneName != null && !zoneName.isBlank()) + { + freightRemark = freightRemark + ",地区 " + zoneName.replace('\n', ' '); + } + PayCostLine freightLine = line("BASIC_FREIGHT", "基础运费", 0L, freight, currency, freightRemark); + result.getFees().add(freightLine); + result.setCurrency(currency); + addSurcharges(product, weight, length, width, height, postcode, zoneName, bizDate, orderStatus, freight, currency, + band.getSectionName(), remoteHits, result); + BigDecimal total = BigDecimal.ZERO; + for (PayCostLine fee : result.getFees()) + { + total = total.add(fee.getAmount()); + } + result.setPayTotal(total.setScale(2, RoundingMode.HALF_UP)); + result.setMessage(summary(result)); + return result; + } + + /** 邮编落到偏远分区,而价卡重量段只有大陆派送费时,运费用大陆档。 */ + private static boolean hasZoneFee(PayProduct product, String zoneName) + { + if (product.getSurcharges() == null) + { + return false; + } + for (PaySurcharge fee : product.getSurcharges()) + { + if (fee != null && PaySurchargeRules.ZONE.equals(fee.getCalcRule()) && PayZoneMatcher.sameZone(fee.getZoneName(), zoneName)) + { + return true; + } + } + return false; + } + + private static List deliveryBands(List bands, PayProduct product) + { + List remoteZones = new ArrayList<>(); + if (product.getSurcharges() != null) + { + for (PaySurcharge fee : product.getSurcharges()) + { + if (fee != null && PaySurchargeRules.ZONE.equals(fee.getCalcRule()) && fee.getZoneName() != null && !fee.getZoneName().isBlank()) + { + remoteZones.add(fee.getZoneName()); + } + } + } + List kept = new ArrayList<>(); + for (PayWeightBand band : bands) + { + boolean remote = false; + for (String zone : remoteZones) + { + if (PayZoneMatcher.sameZone(band.getZoneName(), zone)) + { + remote = true; + break; + } + } + if (!remote) + { + kept.add(band); + } + } + return kept; + } + + /** 首重对更重的包裹仍然收取,超出部分由续重另计。 */ + private static boolean weightAtLeast(PaySurcharge fee, BigDecimal weight) + { + if (fee.getWeightMin() == null || weight == null) + { + return true; + } + return weight.compareTo(fee.getWeightMin()) >= 0; + } + + private static boolean weightIn(PaySurcharge fee, BigDecimal weight) + { + if (weight == null || (fee.getWeightMin() == null && fee.getWeightMax() == null)) + { + return false; + } + if (fee.getWeightMin() != null && weight.compareTo(fee.getWeightMin()) < 0) + { + return false; + } + return fee.getWeightMax() == null || weight.compareTo(fee.getWeightMax()) <= 0; + } + + private static List forZone(List bands, String zoneName) + { + if (zoneName == null || zoneName.isBlank()) + { + return bands; + } + List kept = new ArrayList<>(); + for (PayWeightBand band : bands) + { + if (PayZoneMatcher.sameZone(band.getZoneName(), zoneName)) + { + kept.add(band); + } + } + return kept; + } + + /** + * 添加相关附加费,根据计费规则去判断 + * @param product + * @param weight + * @param length + * @param width + * @param height + * @param postcode + * @param zoneName + * @param bizDate + * @param orderStatus + * @param freight + * @param currency + * @param freightSection + * @param remoteHits + * @param result + */ + private static void addSurcharges(PayProduct product, BigDecimal weight, BigDecimal length, BigDecimal width, + BigDecimal height, String postcode, String zoneName, Date bizDate, Integer orderStatus, BigDecimal freight, String currency, + String freightSection, List remoteHits, PayOrderCostResult result) + { + if (product.getSurcharges() == null) + { + return; + } + List sized = new ArrayList<>(); + for (PaySurcharge fee : product.getSurcharges()) + { + if (fee == null || !inForce(fee, bizDate)) + { + continue; + } + String rule = fee.getCalcRule() == null || fee.getCalcRule().isBlank() ? PaySurchargeRules.MANUAL : fee.getCalcRule(); + if (remoteHits != null && remoteFee(fee)) + { + String name = blank(fee.getFeeName()); + if (postcode == null || postcode.isBlank()) + { + result.getSkipped().add(name + ":订单没有邮编"); + } + else if (bookApplies(remoteHits, fee, rule)) + { + String remark = "邮编 " + postcode.trim(); + if (PaySurchargeRules.EXTRA_KG.equals(rule)) + { + addExtraKg(result.getFees(), fee, weight, currency); + } + else if (perKg(fee)) + { + addPerKg(result.getFees(), fee, weight, currency, remark); + } + else + { + addFlat(result.getFees(), fee, currency, remark); + } + } + continue; + } + boolean postcodeDriven = PaySurchargeRules.POSTCODE.equals(rule) || !blank(fee.getPostcodeParam()).isEmpty(); + if (!postcodeDriven && zoneName != null && !zoneName.isBlank() && fee.getZoneName() != null && !fee.getZoneName().isBlank() + && !PayZoneMatcher.sameZone(fee.getZoneName(), zoneName)) + { + continue; + } + String name = blank(fee.getFeeName()); + if (PaySurchargeRules.POSTCODE.equals(rule)) + { + String param = blank(fee.getPostcodeParam()).isEmpty() ? fee.getCalcParam() : fee.getPostcodeParam(); + if (postcode == null || postcode.isBlank()) + { + result.getSkipped().add(name + ":订单没有邮编"); + } + else if (PaySurchargeRules.matchesPostcode(param, postcode) && weightAtLeast(fee, weight)) + { + addFlat(result.getFees(), fee, currency, "邮编 " + postcode.trim()); + } + } + else if (PaySurchargeRules.ZONE.equals(rule)) + { + boolean already = result.getFees().stream().anyMatch(line -> name.equals(line.getFeeName())); + if (!already && zoneName != null && !zoneName.isBlank() && PayZoneMatcher.sameZone(fee.getZoneName(), zoneName) && weightIn(fee, weight)) + { + addFlat(result.getFees(), fee, currency, "地区 " + zoneName.replace('\n', ' ')); + } + } + else if (PaySurchargeRules.WEIGHT.equals(rule)) + { + if (PaySurchargeRules.matchesWeight(fee.getCalcParam(), weight)) + { + addFlat(result.getFees(), fee, currency, "计费重超过 " + fee.getCalcParam() + "kg"); + } + } + // 超出尺寸费用 + else if (PaySurchargeRules.SIZE.equals(rule)) + { + if (length == null || width == null || height == null) + { + result.getSkipped().add(name + ":订单没有长宽高"); + } + else if (PaySurchargeRules.matchesSize(fee.getCalcParam(), length, width, height)) + { + PayCostLine matched = matchedFee(fee, freight, currency, "尺寸命中 " + fee.getCalcParam()); + if (matched != null) + { + sized.add(matched); + } + } + } + // 退件费用 + else if (PaySurchargeRules.RETURN.equals(rule)) + { + if (orderStatus != null && REFUND_STATUS_SET.contains(orderStatus)) + { + addFlat(result.getFees(), fee, currency, "订单已退件"); + } + } + // 超重费 + else if (PaySurchargeRules.EXTRA_KG.equals(rule)) + { + if (postcodeDriven) + { + if (postcode == null || postcode.isBlank()) + { + result.getSkipped().add(name + ":订单没有邮编"); + } + else if (PaySurchargeRules.matchesPostcode(fee.getPostcodeParam(), postcode)) + { + addExtraKg(result.getFees(), fee, weight, currency); + } + } + else if (sameSection(fee.getSectionName(), freightSection)) + { + addExtraKg(result.getFees(), fee, weight, currency); + } + } + else if (PaySurchargeRules.ALWAYS.equals(rule)) + { + PayCostLine matched = matchedFee(fee, freight, currency, blank(fee.getChargeText())); + if (matched != null) + { + result.getFees().add(matched); + } + } + else if (!name.isEmpty() && !name.contains("报价说明")) + { + result.getSkipped().add(name + ":价卡没有可计算的条件,暂不计费"); + } + } + result.getFees().addAll(keepHighestByName(sized)); + } + + /** 同一费用名的尺寸档同时命中时,只收较高的一档,例如 70% 覆盖 35%。 */ + private static List keepHighestByName(List lines) + { + List kept = new ArrayList<>(); + for (PayCostLine line : lines) + { + PayCostLine same = null; + for (PayCostLine exists : kept) + { + if (exists.getFeeName().equals(line.getFeeName())) + { + same = exists; + break; + } + } + if (same == null) + { + kept.add(line); + } + else if (line.getAmount().compareTo(same.getAmount()) > 0) + { + kept.set(kept.indexOf(same), line); + } + } + return kept; + } + + private static void addExtraKg(List fees, PaySurcharge fee, BigDecimal weight, String currency) + { + if (fee.getAmount() == null || weight == null) + { + return; + } + BigDecimal threshold; + try + { + threshold = new BigDecimal(fee.getCalcParam() == null ? "" : fee.getCalcParam().trim()); + } + catch (NumberFormatException ex) + { + return; + } + if (weight.compareTo(threshold) <= 0) + { + return; + } + String feeCurrency = normalize(fee.getCurrency()); + if (!feeCurrency.isEmpty() && !feeCurrency.equals(currency)) + { + return; + } + BigDecimal over = weight.subtract(threshold); + BigDecimal amount = fee.getAmount().multiply(over).setScale(2, RoundingMode.HALF_UP); + long scId = fee.getSurchargeId() == null ? 0L : fee.getSurchargeId(); + String remark = "超出 " + plain(over) + "kg × " + plain(fee.getAmount()) + " " + blank(fee.getPriceUnit()); + fees.add(line("SURCHARGE", blank(fee.getFeeName()), scId, amount, currency, remark)); + } + + /** 同一地区有多张服务表时,续重只跟本次首重所在的那一张表。 */ + private static boolean sameSection(String feeSection, String freightSection) + { + if (feeSection == null || feeSection.isBlank() || freightSection == null || freightSection.isBlank()) + { + return true; + } + return feeSection.trim().equals(freightSection.trim()); + } + + /** 重量超过最高档,且该地区、该服务表有每公斤续重时,基础运费先用最高档。 */ + private static boolean hasExtraKg(PayProduct product, String zoneName, BigDecimal threshold, String section) + { + if (product.getSurcharges() == null || threshold == null) + { + return false; + } + for (PaySurcharge fee : product.getSurcharges()) + { + if (fee == null || !PaySurchargeRules.EXTRA_KG.equals(fee.getCalcRule()) || fee.getAmount() == null + || !blank(fee.getPostcodeParam()).isEmpty()) + { + continue; + } + if (zoneName != null && !zoneName.isBlank() && fee.getZoneName() != null && !fee.getZoneName().isBlank() + && !PayZoneMatcher.sameZone(fee.getZoneName(), zoneName)) + { + continue; + } + if (!sameSection(fee.getSectionName(), section)) + { + continue; + } + try + { + if (fee.getCalcParam() != null && new BigDecimal(fee.getCalcParam().trim()).compareTo(threshold) == 0) + { + return true; + } + } + catch (NumberFormatException ex) + { + continue; + } + } + return false; + } + + private static PayWeightBand highestBand(List bands) + { + PayWeightBand top = null; + for (PayWeightBand band : bands) + { + if (top == null || band.getWeightMax().compareTo(top.getWeightMax()) > 0) + { + top = band; + } + } + return top; + } + + private static boolean remoteFee(PaySurcharge fee) + { + String name = blank(fee.getFeeName()).toLowerCase(Locale.ROOT); + return name.contains("偏远") || name.contains("岛屿") || name.contains("remote") || name.contains("island"); + } + + /** 邮编配置命中后,金额相同的那一档才收;分区写明时还要对上地区。 */ + private static boolean bookApplies(List hits, PaySurcharge fee, String rule) + { + boolean extra = PaySurchargeRules.EXTRA_KG.equals(rule); + boolean zoned = !blank(fee.getZoneName()).isEmpty(); + for (PayRemoteBook.Hit hit : hits) + { + if (hit == null || !sameFeeName(hit.getFeeName(), fee.getFeeName())) + { + continue; + } + boolean amountKnown = hit.getAmount() != null && fee.getAmount() != null; + if (amountKnown && hit.getAmount().compareTo(fee.getAmount()) != 0 && !extra) + { + continue; + } + boolean areaKnown = !blank(hit.getZoneName()).isEmpty() || !blank(hit.getRegionName()).isEmpty(); + if (zoned && areaKnown && !sameArea(hit, fee)) + { + continue; + } + if (extra && zoned && !sameArea(hit, fee)) + { + continue; + } + if (extra && amountKnown && hit.getAmount().compareTo(fee.getAmount()) != 0 && !sameArea(hit, fee)) + { + continue; + } + return true; + } + return false; + } + + private static boolean sameFeeName(String bookName, String feeName) + { + String book = feeKey(bookName); + String fee = feeKey(feeName); + if (book.isEmpty()) + { + return fee.contains("偏远") || fee.contains("岛屿") || fee.contains("remote") || fee.contains("island"); + } + return fee.contains(book) || book.contains(fee); + } + + private static String feeKey(String name) + { + return blank(name).toLowerCase(Locale.ROOT).replace("地区", "").replace(" ", ""); + } + + private static boolean sameArea(PayRemoteBook.Hit hit, PaySurcharge fee) + { + return PayZoneMatcher.sameZone(fee.getZoneName(), hit.getZoneName()) + || PayZoneMatcher.sameZone(fee.getZoneName(), hit.getRegionName()); + } + + private static boolean perKg(PaySurcharge fee) + { + String unit = blank(fee.getPriceUnit()).toUpperCase(Locale.ROOT); + return unit.contains("KG") || unit.contains("公斤"); + } + + private static void addPerKg(List fees, PaySurcharge fee, BigDecimal weight, String currency, String remark) + { + if (fee.getAmount() == null || weight == null) + { + return; + } + String feeCurrency = normalize(fee.getCurrency()); + if (!feeCurrency.isEmpty() && !feeCurrency.equals(currency)) + { + return; + } + BigDecimal amount = fee.getAmount().multiply(weight).setScale(2, RoundingMode.HALF_UP); + long scId = fee.getSurchargeId() == null ? 0L : fee.getSurchargeId(); + fees.add(line("SURCHARGE", blank(fee.getFeeName()), scId, amount, currency, remark + " × " + plain(weight) + "kg")); + } + + private static void addFlat(List fees, PaySurcharge fee, String currency, String remark) + { + PayCostLine line = matchedFee(fee, null, currency, remark); + if (line != null && !percent(fee)) + { + fees.add(line); + } + } + + private static PayCostLine matchedFee(PaySurcharge fee, BigDecimal freight, String currency, String remark) + { + if (fee.getAmount() == null) + { + return null; + } + BigDecimal amount = fee.getAmount(); + if (percent(fee)) + { + if (freight == null) + { + return null; + } + amount = freight.multiply(fee.getAmount()).divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP); + } + else + { + amount = amount.setScale(2, RoundingMode.HALF_UP); + String feeCurrency = normalize(fee.getCurrency()); + if (!feeCurrency.isEmpty() && !feeCurrency.equals(currency)) + { + return null; + } + } + long scId = fee.getSurchargeId() == null ? 0L : fee.getSurchargeId(); + return line("SURCHARGE", blank(fee.getFeeName()), scId, amount, currency, remark); + } + + private static boolean percent(PaySurcharge fee) + { + String charge = fee.getChargeText() == null ? "" : fee.getChargeText(); + return charge.contains("%") || "%".equals(fee.getCurrency()); + } + + private static boolean inForce(PaySurcharge fee, Date bizDate) + { + if (bizDate == null || (fee.getEffectiveStart() == null && fee.getEffectiveEnd() == null)) + { + return true; + } + LocalDate day = toLocal(bizDate); + if (fee.getEffectiveStart() != null && day.isBefore(toLocal(fee.getEffectiveStart()))) + { + return false; + } + if (fee.getEffectiveEnd() != null && day.isAfter(toLocal(fee.getEffectiveEnd()))) + { + return false; + } + return true; + } + + private static List usableBands(List bands) + { + List usable = new ArrayList<>(); + if (bands == null) + { + return usable; + } + for (PayWeightBand band : bands) + { + if (band != null && band.getPrice() != null && band.getWeightMin() != null && band.getWeightMax() != null) + { + usable.add(band); + } + } + usable.sort((left, right) -> left.getWeightMin().compareTo(right.getWeightMin())); + return usable; + } + + private static Set zones(List bands) + { + Set zones = new LinkedHashSet<>(); + for (PayWeightBand band : bands) + { + if (band.getZoneName() != null && !band.getZoneName().isBlank()) + { + zones.add(band.getZoneName().trim()); + } + } + return zones; + } + + private static PayWeightBand matchBand(List bands, BigDecimal weight) + { + for (PayWeightBand band : bands) + { + if (weight.compareTo(band.getWeightMin()) >= 0 && weight.compareTo(band.getWeightMax()) <= 0) + { + return band; + } + } + for (int i = 0; i < bands.size() - 1; i++) + { + PayWeightBand current = bands.get(i); + PayWeightBand next = bands.get(i + 1); + BigDecimal gap = next.getWeightMin().subtract(current.getWeightMax()); + if (weight.compareTo(current.getWeightMax()) > 0 && weight.compareTo(next.getWeightMin()) < 0 + && gap.compareTo(new BigDecimal("0.01")) <= 0) + { + return next; + } + } + return null; + } + + private static String outOfRange(PayProduct product, List bands, BigDecimal weight) + { + String name = blank(product.getProductName()); + if (bands.isEmpty()) + { + return "产品「" + name + "」没有可匹配的重量段"; + } + BigDecimal max = bands.get(bands.size() - 1).getWeightMax(); + if (weight.compareTo(max) > 0) + { + return "计费重 " + plain(weight) + "kg 超出「" + name + "」的重量段,最大 " + plain(max) + "kg"; + } + return "计费重 " + plain(weight) + "kg 没有落在「" + name + "」的重量段里"; + } + + private static BigDecimal bandAmount(PayWeightBand band, BigDecimal weight) + { + BigDecimal price = band.getPrice(); + if (perKilogram(band)) + { + return price.multiply(weight).setScale(2, RoundingMode.HALF_UP); + } + return price.setScale(2, RoundingMode.HALF_UP); + } + + private static boolean perKilogram(PayWeightBand band) + { + String blob = (blank(band.getPriceUnit()) + " " + blank(band.getChargeName())).toLowerCase(Locale.ROOT); + return blob.contains("/kg") || blob.contains("每公斤") || blob.contains("每千克") || blob.contains("/公斤"); + } + + private static String currencyOf(PayProduct product, PayWeightBand band) + { + String unit = blank(band.getPriceUnit()).toUpperCase(Locale.ROOT); + if (unit.contains("GBP") || unit.contains("英镑")) + { + return "GBP"; + } + if (unit.contains("EUR") || unit.contains("欧元")) + { + return "EUR"; + } + if (unit.contains("USD") || unit.contains("美元")) + { + return "USD"; + } + if (unit.contains("CNY") || unit.contains("RMB") || unit.contains("人民币")) + { + return "CNY"; + } + String currency = blank(product.getCurrency()); + return currency.isEmpty() ? "CNY" : currency; + } + + private static PayCostLine line(String feeType, String feeName, Long scId, BigDecimal amount, String currency, String remark) + { + PayCostLine line = new PayCostLine(); + line.setFeeType(feeType); + line.setFeeName(feeName); + line.setScId(scId); + line.setAmount(amount.setScale(2, RoundingMode.HALF_UP)); + line.setCurrency(currency); + line.setRemark(remark == null ? "" : remark.trim()); + return line; + } + + private static String summary(PayOrderCostResult result) + { + String text = "打单应收 " + plain(result.getPayTotal()) + " " + result.getCurrency(); + if (result.getSkipped() != null && !result.getSkipped().isEmpty()) + { + text = text + ",另有 " + result.getSkipped().size() + " 项附加费未自动计算"; + } + return text; + } + + private static LocalDate toLocal(Date date) + { + if (date instanceof java.sql.Date sql) + { + return sql.toLocalDate(); + } + return date.toInstant().atZone(SHANGHAI).toLocalDate(); + } + + private static String plain(BigDecimal value) + { + return value == null ? "" : value.stripTrailingZeros().toPlainString(); + } + + private static String blank(String value) + { + return value == null ? "" : value.trim(); + } + + private static String normalize(String currency) + { + if (currency == null || currency.isBlank() || "%".equals(currency.trim())) + { + return ""; + } + String value = currency.trim().toUpperCase(Locale.ROOT); + if ("英镑".equals(currency)) + { + return "GBP"; + } + if ("欧元".equals(currency)) + { + return "EUR"; + } + return value; + } +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/pay/PayCostCalculator.java b/bms-back/bms-system/src/main/java/com/bms/price/pay/PayCostCalculator.java index 2b73980c..2118e836 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/pay/PayCostCalculator.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/pay/PayCostCalculator.java @@ -18,7 +18,8 @@ import com.bms.price.domain.PaySurcharge; import com.bms.price.domain.PayWeightBand; /** - * 用渠道上的应付产品,按订单重量、尺寸、邮编和是否退件计算应付。 + * 应付(AP)计费:用渠道应付产品按重量、尺寸、邮编和是否退件计算。 + * 打单应收请用 {@link LabelArCostCalculator},两者独立,互不影响。 * 重量段价格默认是该段的一票价格;单位写明每公斤时才乘重量。 */ public final class PayCostCalculator diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/IPayPostcodeService.java b/bms-back/bms-system/src/main/java/com/bms/price/service/IPayPostcodeService.java index 9b11e9ed..b8d88206 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/IPayPostcodeService.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/IPayPostcodeService.java @@ -10,6 +10,9 @@ public interface IPayPostcodeService { List selectGroupList(PayPostcodeGroup query); + /** 附加费分区下拉:按国家去重区块标题 */ + List selectBlockTitlesByCountry(String countryCode, String countryName); + PayPostcodeGroup selectGroupById(Long groupId); int updateGroup(PayPostcodeGroup group); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayPostcodeServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayPostcodeServiceImpl.java index 3f07be82..2417c28b 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayPostcodeServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayPostcodeServiceImpl.java @@ -34,6 +34,12 @@ public class PayPostcodeServiceImpl implements IPayPostcodeService return payPostcodeMapper.selectGroupList(query); } + @Override + public List selectBlockTitlesByCountry(String countryCode, String countryName) + { + return payPostcodeMapper.selectBlockTitlesByCountry(countryCode, countryName); + } + @Override public PayPostcodeGroup selectGroupById(Long groupId) { diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayProductServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayProductServiceImpl.java index b9e60317..85c56002 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayProductServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayProductServiceImpl.java @@ -461,6 +461,8 @@ public class PayProductServiceImpl implements IPayProductService { product.setExtraLines(new ArrayList<>()); } + // 计数前先按「是否计费」过滤,与落库行数一致 + retainBillableSurcharges(product); product.setWeightCount(product.getWeightBands().size()); product.setSurchargeCount(product.getSurcharges().size()); product.setExtraCount(product.getExtraLines().size()); @@ -1529,6 +1531,7 @@ public class PayProductServiceImpl implements IPayProductService private void insertSurcharges(PayProduct product) { + retainBillableSurcharges(product); List rows = product.getSurcharges(); if (rows == null || rows.isEmpty()) { @@ -1547,6 +1550,27 @@ public class PayProductServiceImpl implements IPayProductService } } + /** + * 仅保留需要计费的附加费:needBill=false 不入库;null 视为要计费。 + */ + private void retainBillableSurcharges(PayProduct product) + { + List rows = product.getSurcharges(); + if (rows == null || rows.isEmpty()) + { + return; + } + List kept = new ArrayList(); + for (PaySurcharge fee : rows) + { + if (fee != null && !Boolean.FALSE.equals(fee.getNeedBill())) + { + kept.add(fee); + } + } + product.setSurcharges(kept); + } + /** * 按附加费名称映射 prc_surcharge.sc_id;币种不能为空:附加费 -> 价卡 -> EUR。 */ 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 79a5ad0b..5ae57441 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 @@ -1716,21 +1716,13 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService return BigDecimal.ZERO; } RoundingMode rm = resolveFeeRoundingMode(surcharge.getRoundMode()); - String chargeType = surcharge.getChargeType() == null ? "FIXED" : surcharge.getChargeType().toUpperCase(); - BigDecimal amount; - if ("WEIGHT".equals(chargeType)) + String chargeType = surcharge.getChargeType() == null ? "MANUAL" : surcharge.getChargeType().toUpperCase(); + // MANUAL=暂不计费;其他计费方式暂按固定金额 charge_value + if ("MANUAL".equals(chargeType)) { - amount = surcharge.getChargeValue().multiply(nz(weight)); + return BigDecimal.ZERO.setScale(4, rm); } - else if ("RATE".equals(chargeType)) - { - amount = nz(freightAmount).multiply(surcharge.getChargeValue()); - } - else - { - amount = surcharge.getChargeValue(); - } - return amount.setScale(4, rm); + return surcharge.getChargeValue().setScale(4, rm); } private RoundingMode resolveFeeRoundingMode(String roundMode) @@ -3180,6 +3172,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService req.setCustomerName(bill.getCustomerName()); req.setProductCode(bill.getProductCode()); req.setProductName(resolveBillProductName(bill)); + req.setProductFloorCode(bill.getProductFloorCode()); req.setCountryCode(bill.getCountryCode()); req.setCountryName(bill.getCountryName()); req.setDestPostcode(bill.getDestPostcode()); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcSurchargeServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcSurchargeServiceImpl.java index 98eceeae..ccc06c97 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcSurchargeServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcSurchargeServiceImpl.java @@ -79,11 +79,13 @@ public class PrcSurchargeServiceImpl implements IPrcSurchargeService fresh.setScName(surcharge.getScName()); fresh.setFeeType(StringUtils.isNotEmpty(surcharge.getFeeType()) ? surcharge.getFeeType() : old.getFeeType()); fresh.setChargeType(surcharge.getChargeType()); + fresh.setDictValue(surcharge.getDictValue()); fresh.setChargeValue(surcharge.getChargeValue()); fresh.setCurrency(surcharge.getCurrency()); fresh.setRoundMode(surcharge.getRoundMode()); fresh.setSsCode(old.getSsCode()); fresh.setCountryCode(surcharge.getCountryCode()); + fresh.setBlockTitle(surcharge.getBlockTitle()); fresh.setScNote(surcharge.getScNote()); applyInsertDefaults(fresh); fresh.setCreateBy(operator); @@ -99,10 +101,12 @@ public class PrcSurchargeServiceImpl implements IPrcSurchargeService return eq(old.getScName(), updated.getScName()) && eq(old.getFeeType(), updated.getFeeType()) && eq(old.getChargeType(), updated.getChargeType()) + && eq(old.getDictValue(), updated.getDictValue()) && eqDecimal(old.getChargeValue(), updated.getChargeValue()) && eq(old.getCurrency(), updated.getCurrency()) && eq(old.getRoundMode(), updated.getRoundMode()) && eq(old.getCountryCode(), updated.getCountryCode()) + && eq(old.getBlockTitle(), updated.getBlockTitle()) && eq(old.getScNote(), updated.getScNote()); } @@ -161,6 +165,10 @@ public class PrcSurchargeServiceImpl implements IPrcSurchargeService { surcharge.setRoundMode("HALF_UP"); } + if (StringUtils.isEmpty(surcharge.getChargeType())) + { + surcharge.setChargeType("MANUAL"); + } } private void validate(PrcSurcharge surcharge) diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/TmsPriceQuoteServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/TmsPriceQuoteServiceImpl.java index a0319f40..8b3780ab 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/TmsPriceQuoteServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/TmsPriceQuoteServiceImpl.java @@ -32,6 +32,7 @@ import com.bms.price.domain.PayCostLine; import com.bms.price.domain.PayOrderCostResult; import com.bms.price.domain.PayProduct; import com.bms.price.domain.PrcCustomerPriceSet; +import com.bms.price.domain.PrcOrderBill; import com.bms.price.domain.PrcOrderBillExt; import com.bms.price.domain.PrcPriceSet; import com.bms.price.domain.PrcPriceSetDetail; @@ -41,7 +42,7 @@ import com.bms.price.domain.PrcQuote; import com.bms.price.domain.PrcQuoteFee; import com.bms.price.domain.PrcSurchargeRel; import com.bms.price.domain.PrcZone; -import com.bms.price.pay.PayCostCalculator; +import com.bms.price.pay.LabelArCostCalculator; import com.bms.price.mapper.PrcZoneDetailMapper; import com.bms.price.domain.vo.TmsPriceBillRequest; import com.bms.price.domain.vo.TmsPriceQuoteFeeLine; @@ -52,8 +53,11 @@ import com.bms.price.enums.BillCustomerActiveStatus; import com.bms.price.mapper.BmsProductCountryMapper; import com.bms.price.mapper.BmsProductMapper; import com.bms.price.mapper.BmsRedlineProductCountryMapper; +import com.bms.masterdata.domain.ServiceChannel; +import com.bms.masterdata.mapper.ServiceChannelMapper; import com.bms.price.mapper.PrcCustomerPriceSetMapper; import com.bms.price.mapper.PrcOrderBillExtMapper; +import com.bms.price.mapper.PrcOrderBillMapper; import com.bms.price.mapper.PrcPriceSetDetailMapper; import com.bms.price.mapper.PrcPriceSetMapper; import com.bms.price.mapper.PrcPriceSetProductMapper; @@ -143,6 +147,12 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService @Autowired private PrcOrderBillExtMapper orderBillExtMapper; + @Autowired + private PrcOrderBillMapper orderBillMapper; + + @Autowired + private ServiceChannelMapper serviceChannelMapper; + @Override @Transactional(rollbackFor = Exception.class) public TmsPriceQuoteResult quote(TmsPriceQuoteRequest request) @@ -199,7 +209,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService return result; } - /** LABEL 费用已由 PayCostCalculator 算完,不再叠加销售附加费。 */ + /** LABEL 费用已由 LabelArCostCalculator 算完,不再叠加销售附加费。 */ private void applySalesSurchargesIfNeeded(TmsPriceQuoteResult result) { if (result == null || SRC_LABEL.equals(result.getMatchSource())) @@ -655,7 +665,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService } /** - * 打单套餐计费:渠道 AR → product_id,套餐+时间 → term,PayCostCalculator 算费。 + * 打单套餐计费:渠道 AR → product_id,套餐+时间 → term,LabelArCostCalculator 算费(与应付独立)。 */ private TmsPriceQuoteResult doLabelQuote(TmsPriceQuoteRequest request, PrcCustomerPriceSet bind, PrcPriceSet set, String productCode, String productName, String countryCode, String countryName, @@ -697,7 +707,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService height = billReq.getPkgHeight(); orderStatus = billReq.getOrderStatus(); } - PayOrderCostResult cost = PayCostCalculator.calculate(product, weight, length, width, height, + PayOrderCostResult cost = LabelArCostCalculator.calculate(product, weight, length, width, height, request.getDestPostcode(), bizDate, orderStatus); if (cost == null || cost.getFees() == null || cost.getFees().isEmpty()) { @@ -715,7 +725,10 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService return r; } - /** 试算用 request.channelId;计费优先订单扩展 productChannelId。 */ + /** + * 试算用 request.channelId;计费优先 prc_order_bill.product_floor_code, + * 没有再取 prc_order_bill_ext.product_floor_code,再解析为渠道ID。 + */ private String resolveLabelChannelId(TmsPriceQuoteRequest request) { if (request != null && StringUtils.isNotEmpty(request.getChannelId())) @@ -727,22 +740,48 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService return null; } TmsPriceBillRequest billReq = (TmsPriceBillRequest) request; - PrcOrderBillExt ext = null; - if (StringUtils.isNotEmpty(billReq.getOrderNo())) + String floorCode = emptyToNull(billReq.getProductFloorCode()); + String orderNo = emptyToNull(billReq.getOrderNo()); + if (StringUtils.isEmpty(floorCode) && StringUtils.isNotEmpty(orderNo)) { - ext = orderBillExtMapper.selectByOrderNo(billReq.getOrderNo().trim()); + PrcOrderBill bill = orderBillMapper.selectPrcOrderBillByOrderNo(orderNo); + if (bill != null) + { + floorCode = emptyToNull(bill.getProductFloorCode()); + } } - if (ext == null) + if (StringUtils.isEmpty(floorCode) && StringUtils.isNotEmpty(orderNo)) + { + PrcOrderBillExt ext = orderBillExtMapper.selectByOrderNo(orderNo); + if (ext != null) + { + floorCode = emptyToNull(ext.getProductFloorCode()); + } + } + if (StringUtils.isEmpty(floorCode)) { return null; } - if (StringUtils.isNotEmpty(ext.getProductChannelId())) + return resolveChannelIdByFloorCode(floorCode); + } + + /** 将 product_floor_code 解析为 srv_service_channel.id(先按编码,再按ID) */ + private String resolveChannelIdByFloorCode(String floorCode) + { + if (StringUtils.isEmpty(floorCode) || serviceChannelMapper == null) { - return ext.getProductChannelId().trim(); + return null; } - if (StringUtils.isNotEmpty(ext.getProductFloorCode())) + String code = floorCode.trim(); + ServiceChannel byCode = serviceChannelMapper.selectServiceChannelByCode(code); + if (byCode != null && StringUtils.isNotEmpty(byCode.getId())) { - return ext.getProductFloorCode().trim(); + return byCode.getId().trim(); + } + ServiceChannel byId = serviceChannelMapper.selectServiceChannelById(code); + if (byId != null && StringUtils.isNotEmpty(byId.getId())) + { + return byId.getId().trim(); } return null; } @@ -1802,21 +1841,13 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService return BigDecimal.ZERO; } RoundingMode rm = resolveRoundingMode(rel.getRoundMode()); - String chargeType = rel.getChargeType() == null ? "FIXED" : rel.getChargeType().toUpperCase(); - BigDecimal amount; - if ("WEIGHT".equals(chargeType)) + String chargeType = rel.getChargeType() == null ? "MANUAL" : rel.getChargeType().toUpperCase(); + // MANUAL=暂不计费;其他计费方式暂按固定金额 charge_value + if ("MANUAL".equals(chargeType)) { - amount = rel.getChargeValue().multiply(nvl(weight)); + return BigDecimal.ZERO.setScale(2, rm); } - else if ("RATE".equals(chargeType)) - { - amount = nvl(freightAmount).multiply(rel.getChargeValue()); - } - else - { - amount = rel.getChargeValue(); - } - return amount.setScale(2, rm); + return rel.getChargeValue().setScale(2, rm); } private void persistQuote(String quoteKey, TmsPriceQuoteRequest request, BigDecimal weight, diff --git a/bms-back/bms-system/src/main/java/com/bms/receivable/service/IArBillService.java b/bms-back/bms-system/src/main/java/com/bms/receivable/service/IArBillService.java index acb25574..a0384626 100644 --- a/bms-back/bms-system/src/main/java/com/bms/receivable/service/IArBillService.java +++ b/bms-back/bms-system/src/main/java/com/bms/receivable/service/IArBillService.java @@ -3,6 +3,7 @@ package com.bms.receivable.service; import java.math.BigDecimal; import java.util.Date; import java.util.List; +import java.util.Map; import com.bms.receivable.domain.ArBill; import com.bms.receivable.domain.ArBankAccount; import com.bms.receivable.domain.ArBillDetail; @@ -85,6 +86,8 @@ public interface IArBillService List selectExports(Long arBillId); + Map getExportDownloadInfo(Long exportId); + void downloadExport(Long exportId, jakarta.servlet.http.HttpServletResponse response) throws Exception; List selectLogList(ArBillLog query); diff --git a/bms-back/bms-system/src/main/java/com/bms/receivable/service/impl/ArBillServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/receivable/service/impl/ArBillServiceImpl.java index 7795d202..8e27d0be 100644 --- a/bms-back/bms-system/src/main/java/com/bms/receivable/service/impl/ArBillServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/receivable/service/impl/ArBillServiceImpl.java @@ -119,6 +119,7 @@ public class ArBillServiceImpl implements IArBillService { private static final Logger log = LoggerFactory.getLogger(ArBillServiceImpl.class); private static final String EXPORT_TEMPLATE_BUSINESS_TYPE = "AR_BILL"; private static final String EXPORT_TEMPLATE_RESOURCE = "templates/receivable/ar_bill_template.xlsx"; + private static final long EXPORT_DOWNLOAD_URL_TTL_MILLIS = 10 * 60 * 1000L; /** * Excel 行号从 1 开始;模板第 4 行是表头,明细从第 5 行开始写入。 */ @@ -1245,17 +1246,29 @@ public class ArBillServiceImpl implements IArBillService { return exports; } + @Override + public Map getExportDownloadInfo(Long exportId) { + ArBillExport export = requireDownloadableExport(exportId); + String resource = export.getStorageKey(); + Map info = new HashMap<>(3); + if (resource.startsWith("oss://")) { + Date expiresAt = new Date(System.currentTimeMillis() + EXPORT_DOWNLOAD_URL_TTL_MILLIS); + info.put("direct", true); + info.put("downloadUrl", ossStorage.generatePresignedDownloadUrl(resource, export.getFileName(), expiresAt)); + info.put("expiresAt", expiresAt.getTime()); + } else { + if (!FileUtils.checkAllowDownload(resource)) { + throw new ServiceException("导出文件路径非法"); + } + info.put("direct", false); + } + return info; + } + @Override public void downloadExport(Long exportId, HttpServletResponse response) throws Exception { long startedAt = System.nanoTime(); - ArBillExport export = exportMapper.selectById(exportId); - if (export == null) { - throw new ServiceException("账单不存在或无权访问"); - } - billScope.requireBillAccessible(export.getArBillId()); - if (!ArExportStatus.SUCCESS.getCode().equals(export.getExportStatus()) || StringUtils.isEmpty(export.getStorageKey())) { - throw new ServiceException("导出文件不存在或尚未生成成功"); - } + ArBillExport export = requireDownloadableExport(exportId); String resource = export.getStorageKey(); boolean ossFile = resource.startsWith("oss://"); if (!ossFile && !FileUtils.checkAllowDownload(resource)) { @@ -1272,6 +1285,19 @@ public class ArBillServiceImpl implements IArBillService { exportId, export.getFileSize(), elapsedMillis(startedAt)); } + private ArBillExport requireDownloadableExport(Long exportId) { + ArBillExport export = exportMapper.selectById(exportId); + if (export == null) { + throw new ServiceException("账单不存在或无权访问"); + } + billScope.requireBillAccessible(export.getArBillId()); + if (!ArExportStatus.SUCCESS.getCode().equals(export.getExportStatus()) + || StringUtils.isEmpty(export.getStorageKey())) { + throw new ServiceException("导出文件不存在或尚未生成成功"); + } + return export; + } + @Override public List selectLogList(ArBillLog query) { long startedAt = System.nanoTime(); diff --git a/bms-back/bms-system/src/main/resources/mapper/price/PayPostcodeMapper.xml b/bms-back/bms-system/src/main/resources/mapper/price/PayPostcodeMapper.xml index 719bfeef..be2da3d6 100644 --- a/bms-back/bms-system/src/main/resources/mapper/price/PayPostcodeMapper.xml +++ b/bms-back/bms-system/src/main/resources/mapper/price/PayPostcodeMapper.xml @@ -30,6 +30,26 @@ order by group_id desc + + - + AND c.del_flag = '0' AND EXISTS ( - SELECT 1 FROM sys_user su - WHERE CAST(su.user_id AS CHAR) = c.sales_user_id AND su.del_flag = '0' - AND (1 = 0 - OR su.user_id = #{scope.userId} - OR su.dept_id = #{scope.deptId} + SELECT 1 FROM sys_user su + WHERE CAST(su.user_id AS CHAR) = c.sales_user_id AND su.del_flag = '0' + AND (1 = 0 + OR su.user_id = #{scope.userId} + + OR EXISTS ( + SELECT 1 FROM sys_dept dept + WHERE dept.dept_id = su.dept_id + AND (dept.dept_id = #{scope.deptId} + OR find_in_set(#{scope.deptId}, dept.ancestors)) + ) + + ) ) - ) AND 1 = 0 diff --git a/bms-back/sql/price/prc_surcharge_calc_mode_alter.sql b/bms-back/sql/price/prc_surcharge_calc_mode_alter.sql new file mode 100644 index 00000000..3d7899da --- /dev/null +++ b/bms-back/sql/price/prc_surcharge_calc_mode_alter.sql @@ -0,0 +1,55 @@ +-- prc_surcharge: drop calc_rule; keep dict_value / block_title; charge_type stores billing mode +SET NAMES utf8mb4; + +-- If calc_rule exists, copy into charge_type then drop +SET @col := ( + SELECT COLUMN_NAME FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'prc_surcharge' + AND COLUMN_NAME = 'calc_rule' + LIMIT 1 +); +SET @sql := IF(@col IS NULL, + 'SELECT 1', + 'UPDATE prc_surcharge SET charge_type = calc_rule WHERE calc_rule IS NOT NULL AND calc_rule <> '''''); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @col := ( + SELECT COLUMN_NAME FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'prc_surcharge' + AND COLUMN_NAME = 'calc_rule' + LIMIT 1 +); +SET @sql := IF(@col IS NULL, + 'SELECT 1', + 'ALTER TABLE prc_surcharge DROP COLUMN calc_rule'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @col := ( + SELECT COLUMN_NAME FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'prc_surcharge' + AND COLUMN_NAME = 'dict_value' + LIMIT 1 +); +SET @sql := IF(@col IS NULL, + 'ALTER TABLE prc_surcharge ADD COLUMN dict_value varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT ''渠道产品 payment_product_type.dict_value'' AFTER charge_type', + 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @col := ( + SELECT COLUMN_NAME FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'prc_surcharge' + AND COLUMN_NAME = 'block_title' + LIMIT 1 +); +SET @sql := IF(@col IS NULL, + 'ALTER TABLE prc_surcharge ADD COLUMN block_title varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT ''分区/区块(pay_postcode_group.block_title)'' AFTER country_code', + 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +-- legacy amount modes: FIXED -> ALWAYS; RATE no longer used as amount mode -> MANUAL +UPDATE prc_surcharge SET charge_type = 'ALWAYS' WHERE charge_type IS NULL OR charge_type = '' OR upper(charge_type) = 'FIXED'; +UPDATE prc_surcharge SET charge_type = 'MANUAL' WHERE upper(charge_type) = 'RATE'; diff --git a/bms-front/src/api/ar/bill.js b/bms-front/src/api/ar/bill.js index 6e080c87..e0ffc2f9 100644 --- a/bms-front/src/api/ar/bill.js +++ b/bms-front/src/api/ar/bill.js @@ -91,6 +91,10 @@ export function downloadArBillExport(exportId) { return request({ url: '/ar/bill/export/' + exportId + '/download', method: 'get', responseType: 'blob' }) } +export function getArBillExportDownloadInfo(exportId) { + return request({ url: '/ar/bill/export/' + exportId + '/download-url', method: 'get' }) +} + export function listArBillLogs(query) { return request({ url: '/ar/bill/log/list', method: 'get', params: query }) } diff --git a/bms-front/src/api/price/billing.js b/bms-front/src/api/price/billing.js index 6f81fd5e..58f5c706 100644 --- a/bms-front/src/api/price/billing.js +++ b/bms-front/src/api/price/billing.js @@ -50,18 +50,26 @@ export function billOrder(data) { /** Receivable bill list */ export function listBill(query) { + const { pageNum, pageSize, ...filters } = query || {} return request({ url: '/price/billing/bill/list', - method: 'get', - params: query + method: 'post', + // 分页参数走 URL(PageHelper);筛选(大批单号)走 body,避免 HTTP 431 + params: { pageNum, pageSize }, + data: filters, + timeout: 120000 }) } export function listOrderBill(query) { + const { pageNum, pageSize, ...filters } = query || {} return request({ url: '/price/billing/orderBill/list', - method: 'get', - params: query + method: 'post', + // 分页参数走 URL(PageHelper);筛选(大批单号)走 body,避免 HTTP 431 + params: { pageNum, pageSize }, + data: filters, + timeout: 120000 }) } diff --git a/bms-front/src/api/price/surcharge.js b/bms-front/src/api/price/surcharge.js index 8adf20f5..5eefc0ff 100644 --- a/bms-front/src/api/price/surcharge.js +++ b/bms-front/src/api/price/surcharge.js @@ -37,6 +37,15 @@ export function listSurchargeOptions(query) { }) } +/** 按国家拉取应付邮编区块(分区下拉) */ +export function listSurchargeBlockOptions(query) { + return request({ + url: '/price/surcharge/blockOptions', + method: 'get', + params: query + }) +} + /** 绑定附加费到产品/明细(scId + productCode + detailId) */ export function addSurchargeRel(data) { return request({ diff --git a/bms-front/src/views/ar/bill/index.vue b/bms-front/src/views/ar/bill/index.vue index 0ed33c99..84c2e87b 100644 --- a/bms-front/src/views/ar/bill/index.vue +++ b/bms-front/src/views/ar/bill/index.vue @@ -69,7 +69,7 @@ + + diff --git a/bms-front/src/views/price/basic/product/components/ProductSurchargeDrawer.vue b/bms-front/src/views/price/basic/product/components/ProductSurchargeDrawer.vue index 189dda6f..93e922d8 100644 --- a/bms-front/src/views/price/basic/product/components/ProductSurchargeDrawer.vue +++ b/bms-front/src/views/price/basic/product/components/ProductSurchargeDrawer.vue @@ -55,7 +55,7 @@ import SurchargeRelDialog from './SurchargeRelDialog.vue' const emit = defineEmits(['success']) const { proxy } = getCurrentInstance() -const { bms_round_mode, bms_charge_type } = useDict('bms_round_mode', 'bms_charge_type') +const { bms_round_mode } = useDict('bms_round_mode') const relRef = ref() const visible = ref(false) @@ -107,9 +107,22 @@ function priceTypeLabel(val) { return '产品价' } +const chargeTypeLabelMap = { + MANUAL: '暂不计费', + ALWAYS: '每票都收', + POSTCODE: '命中邮编', + ZONE: '命中分区', + EXTRA_KG: '超重续收', + WEIGHT: '超重', + SIZE: '超尺寸', + RETURN: '退件', + FIXED: '每票都收', + RATE: '暂不计费' +} + function chargeTypeLabel(val) { - const found = bms_charge_type.value.find(c => c.value === val) - return found ? found.label : (val || '-') + if (!val) return chargeTypeLabelMap.MANUAL + return chargeTypeLabelMap[val] || chargeTypeLabelMap[String(val).toUpperCase()] || val } function roundModeLabel(val) { @@ -120,7 +133,6 @@ function roundModeLabel(val) { function formatChargeValue(row) { if (row.chargeValue == null) return '-' const v = Number(row.chargeValue) - if (row.chargeType === 'RATE') return (v * 100).toFixed(2) + '%' return v + ' ' + (row.currency || 'CNY') } diff --git a/bms-front/src/views/price/basic/redline/components/RedlineSurchargeDrawer.vue b/bms-front/src/views/price/basic/redline/components/RedlineSurchargeDrawer.vue index d6e66183..a4a7da7f 100644 --- a/bms-front/src/views/price/basic/redline/components/RedlineSurchargeDrawer.vue +++ b/bms-front/src/views/price/basic/redline/components/RedlineSurchargeDrawer.vue @@ -55,7 +55,7 @@ import RedlineSurchargeRelDialog from './RedlineSurchargeRelDialog.vue' const emit = defineEmits(['success']) const { proxy } = getCurrentInstance() -const { bms_round_mode, bms_charge_type } = useDict('bms_round_mode', 'bms_charge_type') +const { bms_round_mode } = useDict('bms_round_mode') const relRef = ref() const visible = ref(false) @@ -110,9 +110,22 @@ function priceTypeLabel(val) { return '产品价' } +const chargeTypeLabelMap = { + MANUAL: '暂不计费', + ALWAYS: '每票都收', + POSTCODE: '命中邮编', + ZONE: '命中分区', + EXTRA_KG: '超重续收', + WEIGHT: '超重', + SIZE: '超尺寸', + RETURN: '退件', + FIXED: '每票都收', + RATE: '暂不计费' +} + function chargeTypeLabel(val) { - const found = bms_charge_type.value.find(c => c.value === val) - return found ? found.label : (val || '-') + if (!val) return chargeTypeLabelMap.MANUAL + return chargeTypeLabelMap[val] || chargeTypeLabelMap[String(val).toUpperCase()] || val } function roundModeLabel(val) { @@ -123,7 +136,6 @@ function roundModeLabel(val) { function formatChargeValue(row) { if (row.chargeValue == null) return '-' const v = Number(row.chargeValue) - if (row.chargeType === 'RATE') return (v * 100).toFixed(2) + '%' return v + ' ' + (row.currency || 'CNY') } diff --git a/bms-front/src/views/price/basic/surcharge/components/SurchargeFormDialog.vue b/bms-front/src/views/price/basic/surcharge/components/SurchargeFormDialog.vue index 108f87d2..f68c751d 100644 --- a/bms-front/src/views/price/basic/surcharge/components/SurchargeFormDialog.vue +++ b/bms-front/src/views/price/basic/surcharge/components/SurchargeFormDialog.vue @@ -1,5 +1,5 @@ - + @@ -79,6 +100,7 @@ @@ -104,6 +126,8 @@ const form = reactive({ basic: createEmptyBasicInfo() }) const productRows = ref([]) +const productTableRef = ref() +const selectedProducts = ref([]) const importMeta = reactive({ sourceFile: '', supplierName: '', @@ -141,7 +165,7 @@ const pageBanner = computed(() => { const headerTip = computed(() => { if (editVersionMode.value) return '可重新导入或修改明细;完成后覆盖本版本,不新增版本号' if (newVersionMode.value) return '导入后生成待生效新版本;生效时间请回详情页设置' - return '打单价卡与应付独立;解析后回填到此表,查看明细后可在行内保存修改' + return '打单价卡与应付独立;解析后回填到此表,可删除不需要的产品,编辑明细后点完成保存' }) function goBack() { @@ -196,7 +220,9 @@ async function loadEditProducts() { } } })) - productRows.value = details + // 库中已落库的附加费默认勾选(保留原计费项) + productRows.value = markSurchargeBillFlags(details, true) + selectedProducts.value = [] importMeta.sourceFile = details[0]?.sourceFile || '' importMeta.supplierName = details[0]?.supplierName || '' importMeta.overwrite = true @@ -209,8 +235,71 @@ async function openImport() { importRef.value?.open() } +function markSurchargeBillFlags(products, defaultNeedBill) { + return (products || []).map(product => ({ + ...product, + surcharges: (product.surcharges || []).map(fee => ({ + ...fee, + needBill: fee?.needBill === true ? true : (fee?.needBill === false ? false : !!defaultNeedBill) + })) + })) +} + +function withBillableSurcharges(products) { + return (products || []).map(product => ({ + ...product, + surcharges: (product.surcharges || []).filter(fee => fee && fee.needBill === true) + })) +} + +function onProductSelect(rows) { + selectedProducts.value = rows || [] +} + +function productKey(row) { + return row?.rowKey || `${row?.sheetName || ''}-${row?.productName || ''}` +} + +async function removeProduct(row) { + if (!row) return + try { + await proxy.$modal.confirm(`确认从本次导入中移除产品「${row.productName || row.sheetName || ''}」?`) + } catch { + return + } + const key = productKey(row) + productRows.value = productRows.value.filter(item => productKey(item) !== key) + selectedProducts.value = selectedProducts.value.filter(item => productKey(item) !== key) + if (detailRowKey.value === key) { + detailVisible.value = false + } + proxy.$modal.msgSuccess('已删除') +} + +async function removeSelectedProducts() { + const rows = selectedProducts.value || [] + if (!rows.length) { + proxy.$modal.msgWarning('请先勾选要删除的产品') + return + } + try { + await proxy.$modal.confirm(`确认删除所选 ${rows.length} 个产品?`) + } catch { + return + } + const keys = new Set(rows.map(productKey)) + productRows.value = productRows.value.filter(item => !keys.has(productKey(item))) + selectedProducts.value = [] + if (keys.has(detailRowKey.value)) { + detailVisible.value = false + } + proxy.$modal.msgSuccess('已批量删除') +} + function onImportConfirm(payload) { - productRows.value = payload?.products || [] + // 价卡解析行默认不计费,需手动勾选 + productRows.value = markSurchargeBillFlags(payload?.products || [], false) + selectedProducts.value = [] importMeta.sourceFile = payload?.sourceFile || '' importMeta.supplierName = payload?.supplierName || '' importMeta.effectiveDate = payload?.effectiveDate || '' @@ -226,6 +315,10 @@ function openDetail(row) { } function onDetailClosed() { + // 关闭抽屉时回写勾选结果(不依赖行内「保存」) + if (detailProduct.value && detailRowKey.value) { + applyEditedProduct() + } detailProduct.value = null detailRowKey.value = '' savingRowKey.value = '' @@ -303,6 +396,10 @@ async function finish() { proxy.$modal.msgWarning('请先导入并确认价卡产品') return } + // 抽屉未关闭时先回写勾选,避免丢失 + if (detailVisible.value && detailProduct.value && detailRowKey.value) { + applyEditedProduct() + } const tip = editVersionMode.value ? `将覆盖版本 V${versionNo.value} 的 ${productRows.value.length} 个产品,确认继续?` : newVersionMode.value @@ -317,7 +414,7 @@ async function finish() { try { const payload = { overwrite: editVersionMode.value ? true : importMeta.overwrite, - products: productRows.value + products: withBillableSurcharges(productRows.value) } if (editVersionMode.value) { payload.priceSetId = priceSetId.value @@ -367,6 +464,11 @@ watch([priceSetId, versionNo, () => route.query.editVersion, () => route.query.n color: #909399; font-weight: normal; } +.header-actions { + display: flex; + align-items: center; + gap: 8px; +} .meta-alert { margin-bottom: 12px; } diff --git a/bms-front/src/views/price/label/detail.vue b/bms-front/src/views/price/label/detail.vue index 3d724782..0452e728 100644 --- a/bms-front/src/views/price/label/detail.vue +++ b/bms-front/src/views/price/label/detail.vue @@ -192,7 +192,7 @@ function openCreateVersion() { }) } -/** 编辑待生效/已生效版本价卡 */ +/** 编辑版本价卡(不限状态) */ function openEditVersion(row) { if (!priceSetId.value || !row?.versionNo) return proxy.$router.push({ diff --git a/bms-front/src/views/price/set/components/SurchargeTags.vue b/bms-front/src/views/price/set/components/SurchargeTags.vue index f616b4f3..d25a462c 100644 --- a/bms-front/src/views/price/set/components/SurchargeTags.vue +++ b/bms-front/src/views/price/set/components/SurchargeTags.vue @@ -68,11 +68,22 @@ defineProps({ } }) -const { bms_charge_type } = useDict('bms_charge_type') +const chargeTypeLabelMap = { + MANUAL: '暂不计费', + ALWAYS: '每票都收', + POSTCODE: '命中邮编', + ZONE: '命中分区', + EXTRA_KG: '超重续收', + WEIGHT: '超重', + SIZE: '超尺寸', + RETURN: '退件', + FIXED: '每票都收', + RATE: '暂不计费' +} function chargeTypeLabel(val) { - const found = (bms_charge_type.value || []).find(c => c.value === val) - return found ? found.label : (val || '-') + if (!val) return chargeTypeLabelMap.MANUAL + return chargeTypeLabelMap[val] || chargeTypeLabelMap[String(val).toUpperCase()] || val } diff --git a/bms-front/src/views/price/set/components/create/SurchargePickerDialog.vue b/bms-front/src/views/price/set/components/create/SurchargePickerDialog.vue index 55f62901..6b838c75 100644 --- a/bms-front/src/views/price/set/components/create/SurchargePickerDialog.vue +++ b/bms-front/src/views/price/set/components/create/SurchargePickerDialog.vue @@ -62,8 +62,6 @@ const props = defineProps({ }) const emit = defineEmits(['update:modelValue', 'confirm']) -const { bms_charge_type } = useDict('bms_charge_type') - const tableRef = ref() const picked = ref([]) @@ -91,9 +89,22 @@ const rows = computed(() => { })) }) +const chargeTypeLabelMap = { + MANUAL: '暂不计费', + ALWAYS: '每票都收', + POSTCODE: '命中邮编', + ZONE: '命中分区', + EXTRA_KG: '超重续收', + WEIGHT: '超重', + SIZE: '超尺寸', + RETURN: '退件', + FIXED: '每票都收', + RATE: '暂不计费' +} + function chargeTypeLabel(val) { - const found = (bms_charge_type.value || []).find(c => c.value === val) - return found ? found.label : (val || '-') + if (!val) return chargeTypeLabelMap.MANUAL + return chargeTypeLabelMap[val] || chargeTypeLabelMap[String(val).toUpperCase()] || val } function onSelectionChange(selection) { diff --git a/bms-front/src/views/price/set/components/detail/SetVersionTable.vue b/bms-front/src/views/price/set/components/detail/SetVersionTable.vue index 1aa9c57f..d3caa626 100644 --- a/bms-front/src/views/price/set/components/detail/SetVersionTable.vue +++ b/bms-front/src/views/price/set/components/detail/SetVersionTable.vue @@ -30,11 +30,11 @@ - +