From bde1d7d86ec1f341318f37a1a86c629f76edb0aa Mon Sep 17 00:00:00 2001 From: liuminghui <468701464@qq.com> Date: Fri, 13 Nov 2026 18:26:21 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BA=94=E4=BB=98=E8=AE=A1=E8=B4=B9=E9=82=AE?= =?UTF-8?q?=E7=BC=96=E6=94=B9=E9=80=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/PayPostcodeController.java | 73 +++- .../bms/price/pay/PayCostCalculatorTest.java | 38 +++ .../price/pay/PaySurchargeConfigCalcTest.java | 40 +++ .../airbill/domain/SrvBillAirAllocFee.java | 2 +- .../bms/price/domain/PayPostcodeGroup.java | 9 + .../com/bms/price/domain/PrcPayBillFee.java | 5 +- .../bms/price/mapper/PayPostcodeMapper.java | 36 ++ .../bms/price/mapper/PrcPayBillFeeMapper.java | 3 + .../bms/price/pay/LabelArCostCalculator.java | 37 +- .../com/bms/price/pay/PayCostCalculator.java | 155 ++++++++- .../com/bms/price/pay/PaySurchargeRules.java | 16 +- .../price/service/IPayPostcodeService.java | 23 ++ .../service/impl/PayBillServiceImpl.java | 8 +- .../service/impl/PayOrderCostServiceImpl.java | 46 ++- .../service/impl/PayPaymentServiceImpl.java | 6 +- .../service/impl/PayPostcodeServiceImpl.java | 272 +++++++++++++++ .../impl/PaySurchargeRuleServiceImpl.java | 2 +- .../impl/PrcPayBillFeeServiceImpl.java | 13 + .../impl/SrvBillAirAllocServiceImpl.java | 1 - .../mapper/price/PayPostcodeMapper.xml | 318 +++++++++++++++++- .../mapper/price/PrcPayBillFeeMapper.xml | 11 +- bms-front/src/api/data/payPostcode.js | 36 ++ .../components/PayPostcodeDrawer.vue | 124 +++++++ .../components/PayPostcodeFormDialog.vue | 78 +++++ .../payPostcode/components/PayZoneDrawer.vue | 146 ++++++++ .../components/PayZoneFormDialog.vue | 83 +++++ .../src/views/data/payPostcode/index.vue | 250 +++++--------- .../pay/components/PaySurchargeRuleDialog.vue | 65 ++-- .../src/views/price/billing/payBill/index.vue | 10 +- .../views/price/billing/payPayment/index.vue | 109 +++++- 30 files changed, 1761 insertions(+), 254 deletions(-) create mode 100644 bms-front/src/views/data/payPostcode/components/PayPostcodeDrawer.vue create mode 100644 bms-front/src/views/data/payPostcode/components/PayPostcodeFormDialog.vue create mode 100644 bms-front/src/views/data/payPostcode/components/PayZoneDrawer.vue create mode 100644 bms-front/src/views/data/payPostcode/components/PayZoneFormDialog.vue 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 bddf6764..e79e6e3b 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 @@ -40,10 +40,81 @@ public class PayPostcodeController extends BaseController public TableDataInfo list(PayPostcodeGroup query) { startPage(); - List list = payPostcodeService.selectGroupList(query); + List list = payPostcodeService.selectCountryList(query); return getDataTable(list); } + @PreAuthorize("@ss.hasPermi('data:payPostcode:query')") + @GetMapping("/zones") + public AjaxResult zones(PayPostcodeGroup query) + { + return success(payPostcodeService.selectZoneList(query)); + } + + @PreAuthorize("@ss.hasPermi('data:payPostcode:edit')") + @Log(title = "应付邮编配置", businessType = BusinessType.INSERT) + @PostMapping("/zones") + public AjaxResult addZone(@RequestBody PayPostcodeGroup zone) + { + return toAjax(payPostcodeService.addZone(zone, getUsername())); + } + + @PreAuthorize("@ss.hasPermi('data:payPostcode:edit')") + @Log(title = "应付邮编配置", businessType = BusinessType.UPDATE) + @PutMapping("/zones") + public AjaxResult editZone(@RequestBody PayPostcodeGroup zone) + { + return toAjax(payPostcodeService.updateZone(zone)); + } + + @PreAuthorize("@ss.hasPermi('data:payPostcode:remove')") + @Log(title = "应付邮编配置", businessType = BusinessType.DELETE) + @DeleteMapping("/zones") + public AjaxResult removeZone(PayPostcodeGroup zone) + { + return toAjax(payPostcodeService.deleteZone(zone)); + } + + @PreAuthorize("@ss.hasPermi('data:payPostcode:edit')") + @Log(title = "应付邮编配置", businessType = BusinessType.UPDATE) + @PutMapping("/country") + public AjaxResult editCountry(@RequestBody PayPostcodeGroup country) + { + return toAjax(payPostcodeService.updateCountry(country)); + } + + @PreAuthorize("@ss.hasPermi('data:payPostcode:remove')") + @Log(title = "应付邮编配置", businessType = BusinessType.DELETE) + @DeleteMapping("/country") + public AjaxResult removeCountry(PayPostcodeGroup country) + { + return toAjax(payPostcodeService.deleteCountry(country)); + } + + @PreAuthorize("@ss.hasPermi('data:payPostcode:edit')") + @Log(title = "应付邮编配置", businessType = BusinessType.INSERT) + @PostMapping("/lines") + public AjaxResult addLine(@RequestBody PayPostcodeLine line) + { + return toAjax(payPostcodeService.addLine(line)); + } + + @PreAuthorize("@ss.hasPermi('data:payPostcode:edit')") + @Log(title = "应付邮编配置", businessType = BusinessType.UPDATE) + @PutMapping("/lines") + public AjaxResult editLine(@RequestBody PayPostcodeLine line) + { + return toAjax(payPostcodeService.updateLine(line)); + } + + @PreAuthorize("@ss.hasPermi('data:payPostcode:remove')") + @Log(title = "应付邮编配置", businessType = BusinessType.DELETE) + @DeleteMapping("/lines/{lineIds}") + public AjaxResult removeLines(@PathVariable Long[] lineIds) + { + return toAjax(payPostcodeService.deleteLines(lineIds)); + } + /** 按国家去重区块,供附加费编辑分区下拉 */ @PreAuthorize("@ss.hasPermi('data:payPostcode:list')") @GetMapping("/blockOptions") diff --git a/bms-back/bms-admin/src/test/java/com/bms/price/pay/PayCostCalculatorTest.java b/bms-back/bms-admin/src/test/java/com/bms/price/pay/PayCostCalculatorTest.java index 4332169d..ce8cd883 100644 --- a/bms-back/bms-admin/src/test/java/com/bms/price/pay/PayCostCalculatorTest.java +++ b/bms-back/bms-admin/src/test/java/com/bms/price/pay/PayCostCalculatorTest.java @@ -228,6 +228,44 @@ class PayCostCalculatorTest assertTrue(hasFee(result, "偏远附加费", "0.70")); } + @Test + void countryRemoteHitWithoutAZoneUsesTheFeePostcodeTier() + { + PayProduct product = new PayProduct(); + product.setProductName("CTT 48H"); + product.setCurrency("EUR"); + product.setWeightBands(new ArrayList<>(List.of(band("0", "5", "2.05", "马德里")))); + PaySurcharge balearic = fee(1L, "偏远附加费", "7.1", "EUR", "POSTCODE", "07"); + balearic.setZoneName("Balearic Islands 07邮编开头"); + balearic.setPostcodeParam("07"); + PaySurcharge canary = fee(2L, "偏远附加费", "8.9", "EUR", "POSTCODE", "35,38"); + canary.setZoneName("Canary Islands 35、38邮编开头"); + canary.setPostcodeParam("35,38"); + PaySurcharge extra = fee(3L, "偏远附加费", "4", "EUR", "EXTRA_KG", "1"); + extra.setZoneName("Balearic Islands 07邮编开头"); + extra.setPostcodeParam("07"); + extra.setPriceUnit("EUR/KG"); + product.setSurcharges(List.of(balearic, canary, extra)); + List hits = List.of(new PayRemoteBook.Hit("", null, "", "")); + + PayOrderCostResult light = PayCostCalculator.calculate(product, new BigDecimal("0.8"), + null, null, null, "07008", "马德里", null, 1, hits, false, false, "ES", "西班牙"); + assertTrue(hasFee(light, "偏远附加费", "7.1")); + assertTrue(light.getFees().stream().noneMatch(fee -> fee.getFeeName().equals("偏远附加费") + && fee.getAmount().compareTo(new BigDecimal("8.9")) == 0)); + assertTrue(light.getFees().stream().noneMatch(fee -> fee.getAmount().compareTo(new BigDecimal("4")) == 0)); + + PayOrderCostResult heavy = PayCostCalculator.calculate(product, new BigDecimal("3"), + null, null, null, "07008", "马德里", null, 1, hits, false, false, "ES", "西班牙"); + assertTrue(hasFee(heavy, "偏远附加费", "7.1")); + assertTrue(hasFee(heavy, "偏远附加费", "8")); + + List named = List.of(new PayRemoteBook.Hit("", null, "", "Aland Islands")); + PayOrderCostResult skipped = PayCostCalculator.calculate(product, new BigDecimal("0.8"), + null, null, null, "07008", "马德里", null, 1, named, false, false, "ES", "西班牙"); + assertTrue(skipped.getFees().stream().noneMatch(fee -> "偏远附加费".equals(fee.getFeeName()))); + } + @Test void remoteHitsFollowTheProductGroupAndCountry() { diff --git a/bms-back/bms-admin/src/test/java/com/bms/price/pay/PaySurchargeConfigCalcTest.java b/bms-back/bms-admin/src/test/java/com/bms/price/pay/PaySurchargeConfigCalcTest.java index 4be0c8f2..3af77b64 100644 --- a/bms-back/bms-admin/src/test/java/com/bms/price/pay/PaySurchargeConfigCalcTest.java +++ b/bms-back/bms-admin/src/test/java/com/bms/price/pay/PaySurchargeConfigCalcTest.java @@ -68,6 +68,24 @@ class PaySurchargeConfigCalcTest assertTrue(peak.getFees().stream().noneMatch(fee -> fee.getFeeName().contains("超尺寸"))); } + @Test + void sumOverThreeHundredChargesEachExtraTenCentimetres() + { + PayOrderCostResult result = PayCostCalculator.calculate(sumStep(), new BigDecimal("0.039"), + new BigDecimal("300"), new BigDecimal("300"), new BigDecimal("300"), + null, null, null, 8); + assertTrue(hasFee(result, "基础运费", "2.40")); + assertTrue(hasFee(result, "超尺寸附加费", "2.43")); + assertTrue(hasFee(result, "超尺寸附加费", "378.00")); + assertEquals(new BigDecimal("382.83"), result.getPayTotal()); + + PayOrderCostResult oneStep = PayCostCalculator.calculate(sumStep(), new BigDecimal("0.039"), + new BigDecimal("100"), new BigDecimal("100"), new BigDecimal("101"), + null, null, null, 8); + assertTrue(hasFee(oneStep, "超尺寸附加费", "2.43")); + assertTrue(hasFee(oneStep, "超尺寸附加费", "6.30")); + } + private static boolean hasFee(PayOrderCostResult result, String name, String amount) { return result.getFees().stream().anyMatch(fee -> name.equals(fee.getFeeName()) @@ -79,6 +97,28 @@ class PaySurchargeConfigCalcTest return Date.from(LocalDate.of(year, month, day).atStartOfDay(ZoneId.systemDefault()).toInstant()); } + private static PayProduct sumStep() + { + PayProduct product = new PayProduct(); + product.setProductName("CTT 48H"); + product.setCurrency("EUR"); + product.setWeightBands(List.of(band("0", "1", "2.40"))); + PaySurcharge flat = fee("超尺寸附加费", "2.43", "SIZE"); + PaySurcharge step = fee("超尺寸附加费", "6.30", "SIZE"); + step.setChargeText("6.3EUR/增加10cm"); + product.setSurcharges(List.of(flat, step)); + PaySurchargeRule rule = new PaySurchargeRule(); + rule.setCalcRule("SIZE"); + rule.setEnabled("1"); + Map detail = new LinkedHashMap<>(); + detail.put("matchMode", "ANY"); + detail.put("sumMin", new BigDecimal("300")); + detail.put("exceedStepCm", new BigDecimal("10")); + rule.setDetail(detail); + product.setBillingRules(List.of(rule)); + return product; + } + private static PayProduct imile() { PayProduct product = new PayProduct(); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/airbill/domain/SrvBillAirAllocFee.java b/bms-back/bms-system/src/main/java/com/bms/price/airbill/domain/SrvBillAirAllocFee.java index 87370984..19fe0d10 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/airbill/domain/SrvBillAirAllocFee.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/airbill/domain/SrvBillAirAllocFee.java @@ -11,7 +11,7 @@ public class SrvBillAirAllocFee private String billCode; - /** 提单主键 srv_bill_air.guid,写入订单费用的 source_id */ + /** 提单主键 srv_bill_air.guid */ private String ladingId; /** 费用阶段,字典 bms_fee_level */ diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/PayPostcodeGroup.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayPostcodeGroup.java index b2121492..792c577d 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/domain/PayPostcodeGroup.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayPostcodeGroup.java @@ -59,6 +59,15 @@ public class PayPostcodeGroup extends BaseEntity /** 查询条件:邮编 */ private String postcode; + /** 国家下的分区名,例如 mainland。列表按国家归拢后,分区在抽屉里维护 */ + private String zoneName; + + /** 修改分区时的原名称 */ + private String oldZoneName; + + /** 这个国家下的分区个数 */ + private Integer zoneCount; + /** 导入时挂上的明细,列表查询不填充 */ private List lines = new ArrayList<>(); } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/PrcPayBillFee.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/PrcPayBillFee.java index 35011024..85272d87 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/domain/PrcPayBillFee.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/PrcPayBillFee.java @@ -12,6 +12,9 @@ public class PrcPayBillFee implements Serializable { private static final long serialVersionUID = 1L; + /** 主键 */ + private Long id; + private String feeId; private String billId; /** 应付主表ID */ @@ -30,7 +33,7 @@ public class PrcPayBillFee implements Serializable /** CALC 计算,LADING 提单分摊,MANUAL 人工 */ private String feeSource; - /** 来源数据id */ + /** 对应明细费用主键 prc_pay_bill_fee.id */ private String sourceId; /** 是否删除:0未删除 1已删除 */ 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 8097bd09..4ca8080e 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,38 @@ public interface PayPostcodeMapper { List selectGroupList(PayPostcodeGroup query); + List selectCountryList(PayPostcodeGroup query); + + List selectZoneList(PayPostcodeGroup query); + + PayPostcodeGroup selectSampleGroup(PayPostcodeGroup query); + + int countZoneName(PayPostcodeGroup query); + + int updateCountryProfile(PayPostcodeGroup country); + + List selectGroupIdsByCountry(PayPostcodeGroup query); + + int renameZoneLines(PayPostcodeGroup zone); + + int updateExclusiveZone(PayPostcodeGroup zone); + + int insertLine(PayPostcodeLine line); + + int updateLine(PayPostcodeLine line); + + PayPostcodeLine selectLineById(Long lineId); + + List selectGroupIdsByLineIds(@Param("lineIds") List lineIds); + + int deleteLineByIds(@Param("lineIds") List lineIds); + + int deleteLinesByZone(PayPostcodeGroup zone); + + int refreshLineCount(Long groupId); + + Long selectZoneGroupId(PayPostcodeGroup query); + /** 按国家去重区块标题 */ List selectBlockTitlesByCountry(@Param("countryCode") String countryCode, @Param("countryName") String countryName); @@ -31,6 +63,10 @@ public interface PayPostcodeMapper /** 报价表、销售产品或工作表与产品名称完全一致的邮编配置 */ List selectGroupsForProduct(@Param("names") List names); + /** 目的国家下标记为偏远的邮编组。产品自己没有偏远名单时,用这一份做偏远附加费。 */ + List selectRemoteGroupsByCountry(@Param("countryCode") String countryCode, + @Param("countryName") String countryName); + List selectRegionKeys(@Param("groupIds") List groupIds); List selectLinesByGroups(@Param("groupIds") List groupIds); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/mapper/PrcPayBillFeeMapper.java b/bms-back/bms-system/src/main/java/com/bms/price/mapper/PrcPayBillFeeMapper.java index 505ceb68..9c540059 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/mapper/PrcPayBillFeeMapper.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/mapper/PrcPayBillFeeMapper.java @@ -31,6 +31,9 @@ public interface PrcPayBillFeeMapper */ public int insertBatch(@Param("list") List fees); + /** 把还没标明来源的费用,关联到本行主键 id。 */ + public int fillSourceIdByFeeIds(@Param("feeIds") List feeIds); + /** * 删除一票订单上来源为计算的费用。提单分摊和人工费用保留。 * 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 index 5d17b918..57648208 100644 --- 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 @@ -311,7 +311,7 @@ public final class LabelArCostCalculator { result.getSkipped().add(name + ":订单没有邮编"); } - else if (bookApplies(remoteHits, fee, rule)) + else if (bookApplies(remoteHits, fee, rule, postcode)) { String remark = "邮编 " + postcode.trim(); if (PaySurchargeRules.EXTRA_KG.equals(rule)) @@ -547,10 +547,11 @@ public final class LabelArCostCalculator return name.contains("偏远") || name.contains("岛屿") || name.contains("remote") || name.contains("island"); } - /** 邮编配置命中后,金额相同的那一档才收;分区写明时还要对上地区。 */ - private static boolean bookApplies(List hits, PaySurcharge fee, String rule) + /** 邮编配置命中后,金额相同的那一档才收。命中写了地区时要对上分区;没写地区时用价卡上的邮编前缀选档。 */ + private static boolean bookApplies(List hits, PaySurcharge fee, String rule, String postcode) { boolean extra = PaySurchargeRules.EXTRA_KG.equals(rule); + boolean tier = extra || PaySurchargeRules.POSTCODE.equals(rule); boolean zoned = !blank(fee.getZoneName()).isEmpty(); for (PayRemoteBook.Hit hit : hits) { @@ -568,11 +569,15 @@ public final class LabelArCostCalculator { continue; } - if (extra && zoned && !sameArea(hit, fee)) + if ((zoned && !areaKnown || !zoned && tier) && !feePostcodeMatches(fee, rule, postcode)) { continue; } - if (extra && amountKnown && hit.getAmount().compareTo(fee.getAmount()) != 0 && !sameArea(hit, fee)) + if (extra && zoned && areaKnown && !sameArea(hit, fee)) + { + continue; + } + if (extra && amountKnown && hit.getAmount().compareTo(fee.getAmount()) != 0 && areaKnown && !sameArea(hit, fee)) { continue; } @@ -581,6 +586,21 @@ public final class LabelArCostCalculator return false; } + /** 价卡这一档写了邮编时,订单邮编必须落在里面。续重的重量门槛不算邮编。 */ + private static boolean feePostcodeMatches(PaySurcharge fee, String rule, String postcode) + { + String param = blank(fee.getPostcodeParam()); + if (param.isEmpty() && !PaySurchargeRules.EXTRA_KG.equals(rule)) + { + param = blank(fee.getCalcParam()); + } + if (param.isEmpty()) + { + return false; + } + return PaySurchargeRules.matchesPostcode(param, postcode); + } + private static boolean sameFeeName(String bookName, String feeName) { String book = feeKey(bookName); @@ -599,8 +619,11 @@ public final class LabelArCostCalculator private static boolean sameArea(PayRemoteBook.Hit hit, PaySurcharge fee) { - return PayZoneMatcher.sameZone(fee.getZoneName(), hit.getZoneName()) - || PayZoneMatcher.sameZone(fee.getZoneName(), hit.getRegionName()); + if (!blank(hit.getZoneName()).isEmpty() && PayZoneMatcher.sameZone(fee.getZoneName(), hit.getZoneName())) + { + return true; + } + return !blank(hit.getRegionName()).isEmpty() && PayZoneMatcher.sameZone(fee.getZoneName(), hit.getRegionName()); } private static boolean perKg(PaySurcharge fee) 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 fa35951b..435304ac 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 @@ -297,6 +297,7 @@ public final class PayCostCalculator return; } List sized = new ArrayList<>(); + List stepped = new ArrayList<>(); for (PaySurcharge fee : product.getSurcharges()) { if (fee == null || !inForce(fee, bizDate)) @@ -314,7 +315,7 @@ public final class PayCostCalculator { result.getSkipped().add(name + ":订单没有邮编"); } - else if (bookApplies(remoteHits, fee, rule)) + else if (bookApplies(remoteHits, fee, rule, postcode)) { String remark = "邮编 " + postcode.trim(); if (PaySurchargeRules.EXTRA_KG.equals(rule)) @@ -413,10 +414,17 @@ public final class PayCostCalculator } else if (sizeHit(product, fee, length, width, height, weight)) { - PayCostLine matched = matchedFee(fee, freight, currency, "尺寸命中"); + PayCostLine matched = sizedFee(product, fee, length, width, height, freight, currency); if (matched != null) { - sized.add(matched); + if (stepPriced(fee)) + { + stepped.add(matched); + } + else + { + sized.add(matched); + } } } } @@ -465,9 +473,10 @@ public final class PayCostCalculator } } result.getFees().addAll(keepHighestByName(sized)); + result.getFees().addAll(stepped); } - /** 同一费用名的尺寸档同时命中时,只收较高的一档,例如 70% 覆盖 35%。 */ + /** 同一费用名的按票尺寸档同时命中时,只收较高的一档,例如 70% 覆盖 35%。按超出单位计的不并进这一档。 */ private static List keepHighestByName(List lines) { List kept = new ArrayList<>(); @@ -660,10 +669,11 @@ public final class PayCostCalculator return name.contains("偏远") || name.contains("岛屿") || name.contains("remote") || name.contains("island"); } - /** 邮编配置命中后,金额相同的那一档才收;分区写明时还要对上地区。 */ - private static boolean bookApplies(List hits, PaySurcharge fee, String rule) + /** 邮编配置命中后,金额相同的那一档才收。命中写了地区时要对上分区;没写地区时用价卡上的邮编前缀选档。 */ + private static boolean bookApplies(List hits, PaySurcharge fee, String rule, String postcode) { boolean extra = PaySurchargeRules.EXTRA_KG.equals(rule); + boolean tier = extra || PaySurchargeRules.POSTCODE.equals(rule); boolean zoned = !blank(fee.getZoneName()).isEmpty(); for (PayRemoteBook.Hit hit : hits) { @@ -677,15 +687,19 @@ public final class PayCostCalculator continue; } boolean areaKnown = !blank(hit.getZoneName()).isEmpty() || !blank(hit.getRegionName()).isEmpty(); - if (zoned && (!areaKnown || !sameArea(hit, fee))) + if (zoned && areaKnown && !sameArea(hit, fee)) { continue; } - if (extra && zoned && !sameArea(hit, fee)) + if ((zoned && !areaKnown || !zoned && tier) && !feePostcodeMatches(fee, rule, postcode)) { continue; } - if (extra && amountKnown && hit.getAmount().compareTo(fee.getAmount()) != 0 && !sameArea(hit, fee)) + if (extra && zoned && areaKnown && !sameArea(hit, fee)) + { + continue; + } + if (extra && amountKnown && hit.getAmount().compareTo(fee.getAmount()) != 0 && areaKnown && !sameArea(hit, fee)) { continue; } @@ -694,6 +708,21 @@ public final class PayCostCalculator return false; } + /** 价卡这一档写了邮编时,订单邮编必须落在里面。续重的重量门槛不算邮编。 */ + private static boolean feePostcodeMatches(PaySurcharge fee, String rule, String postcode) + { + String param = blank(fee.getPostcodeParam()); + if (param.isEmpty() && !PaySurchargeRules.EXTRA_KG.equals(rule)) + { + param = blank(fee.getCalcParam()); + } + if (param.isEmpty()) + { + return false; + } + return PaySurchargeRules.matchesPostcode(param, postcode); + } + private static boolean sameFeeName(String bookName, String feeName) { String book = feeKey(bookName); @@ -712,8 +741,11 @@ public final class PayCostCalculator private static boolean sameArea(PayRemoteBook.Hit hit, PaySurcharge fee) { - return PayZoneMatcher.sameZone(fee.getZoneName(), hit.getZoneName()) - || PayZoneMatcher.sameZone(fee.getZoneName(), hit.getRegionName()); + if (!blank(hit.getZoneName()).isEmpty() && PayZoneMatcher.sameZone(fee.getZoneName(), hit.getZoneName())) + { + return true; + } + return !blank(hit.getRegionName()).isEmpty() && PayZoneMatcher.sameZone(fee.getZoneName(), hit.getRegionName()); } private static boolean perKg(PaySurcharge fee) @@ -747,6 +779,107 @@ public final class PayCostCalculator } } + /** + * 收费标准写了每增加若干厘米时,按三边之和超出下限的档数乘单价。 + * 没有步长配置的尺寸费仍按价卡金额收一次。 + */ + private static PayCostLine sizedFee(PayProduct product, PaySurcharge fee, BigDecimal length, BigDecimal width, + BigDecimal height, BigDecimal freight, String currency) + { + if (stepPriced(fee)) + { + PayCostLine stepped = stepBySum(product, fee, length, width, height, currency); + if (stepped != null || hasSumStep(product)) + { + return stepped; + } + } + return matchedFee(fee, freight, currency, "尺寸命中"); + } + + /** 价卡写成每增加、每超出时,金额是一个步长的单价。 */ + private static boolean stepPriced(PaySurcharge fee) + { + String charge = fee.getChargeText() == null ? "" : fee.getChargeText(); + return charge.contains("增加") || charge.contains("每超出"); + } + + private static boolean hasSumStep(PayProduct product) + { + for (Map detail : ruleDetails(product, PaySurchargeRules.SIZE)) + { + BigDecimal step = decimal(detail.get("exceedStepCm")); + BigDecimal threshold = decimal(detail.get("sumMin")); + if (step != null && step.compareTo(BigDecimal.ZERO) > 0 && threshold != null) + { + return true; + } + } + return false; + } + + /** 超出厘米数按步长向上取整。刚超过不足一个步长也收一档。 */ + private static PayCostLine stepBySum(PayProduct product, PaySurcharge fee, BigDecimal length, BigDecimal width, + BigDecimal height, String currency) + { + if (fee.getAmount() == null) + { + return null; + } + BigDecimal sum = length.add(width).add(height); + for (Map detail : ruleDetails(product, PaySurchargeRules.SIZE)) + { + BigDecimal step = decimal(detail.get("exceedStepCm")); + BigDecimal threshold = decimal(detail.get("sumMin")); + if (step == null || step.compareTo(BigDecimal.ZERO) <= 0 || threshold == null || sum.compareTo(threshold) <= 0) + { + continue; + } + String feeCurrency = normalize(fee.getCurrency()); + if (!feeCurrency.isEmpty() && !feeCurrency.equals(currency)) + { + return null; + } + BigDecimal excess = sum.subtract(threshold); + BigDecimal steps = excess.divide(step, 0, RoundingMode.CEILING); + if (steps.compareTo(BigDecimal.ZERO) <= 0) + { + continue; + } + BigDecimal amount = fee.getAmount().multiply(steps); + String remark = "三边之和 " + plain(sum) + "cm,超出 " + plain(excess) + "cm,每 " + plain(step) + "cm " + + plain(fee.getAmount()) + "," + plain(steps) + " 档"; + long scId = fee.getSurchargeId() == null ? 0L : fee.getSurchargeId(); + return line("SURCHARGE", blank(fee.getFeeName()), scId, amount, currency, remark); + } + return null; + } + + private static BigDecimal decimal(Object value) + { + if (value == null) + { + return null; + } + if (value instanceof BigDecimal decimal) + { + return decimal; + } + String text = String.valueOf(value).trim(); + if (text.isEmpty()) + { + return null; + } + try + { + return new BigDecimal(text); + } + catch (NumberFormatException ex) + { + return null; + } + } + private static PayCostLine matchedFee(PaySurcharge fee, BigDecimal freight, String currency, String remark) { if (fee.getAmount() == null) diff --git a/bms-back/bms-system/src/main/java/com/bms/price/pay/PaySurchargeRules.java b/bms-back/bms-system/src/main/java/com/bms/price/pay/PaySurchargeRules.java index 2b54c05d..f09193da 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/pay/PaySurchargeRules.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/pay/PaySurchargeRules.java @@ -374,6 +374,7 @@ public final class PaySurchargeRules return false; } java.time.LocalDate day = bizDate == null ? null : bizDate.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate(); + boolean open = false; for (java.util.Map detail : details) { if (detail == null) @@ -381,6 +382,14 @@ public final class PaySurchargeRules continue; } String ruleName = detail.get("feeName") == null ? "" : String.valueOf(detail.get("feeName")).trim(); + if (ruleName.isEmpty()) + { + if (!hasPeriod(detail)) + { + open = true; + } + continue; + } if (!ruleName.equals(name)) { continue; @@ -389,12 +398,9 @@ public final class PaySurchargeRules { return true; } - if (day != null && inPeriod(day, detail)) - { - return true; - } + return day != null && inPeriod(day, detail); } - return false; + return open; } private static void addBound(List checks, BigDecimal actual, Object min, Object max) 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 b8d88206..56c72724 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,29 @@ public interface IPayPostcodeService { List selectGroupList(PayPostcodeGroup query); + /** 每个国家一行:邮编属性、服务类型、销售产品、渠道、分区数、邮编总数、导入时间 */ + List selectCountryList(PayPostcodeGroup query); + + /** 一个国家下面的分区,同一分区名只出现一行 */ + List selectZoneList(PayPostcodeGroup query); + + int addZone(PayPostcodeGroup zone, String username); + + int updateZone(PayPostcodeGroup zone); + + int deleteZone(PayPostcodeGroup zone); + + /** 保存后写回这个国家下的全部分组,报价表和工作表保持不动,计费仍能对上产品 */ + int updateCountry(PayPostcodeGroup country); + + int deleteCountry(PayPostcodeGroup country); + + int addLine(PayPostcodeLine line); + + int updateLine(PayPostcodeLine line); + + int deleteLines(Long[] lineIds); + /** 附加费分区下拉:按国家去重区块标题 */ List selectBlockTitlesByCountry(String countryCode, String countryName); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayBillServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayBillServiceImpl.java index e20e8115..33527f3e 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayBillServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayBillServiceImpl.java @@ -399,7 +399,7 @@ public class PayBillServiceImpl implements IPayBillService throw PaymentMesEnum.BMS_PAY_PENDING.exception(); } String operator = SecurityUtils.getUsername(); - if (operator.equals(bill.getCreateBy()) || (bill.getSubmitBy() != null && operator.equals(bill.getSubmitBy()))) + if (!SecurityUtils.isAdmin() && (operator.equals(bill.getCreateBy()) || (bill.getSubmitBy() != null && operator.equals(bill.getSubmitBy())))) { throw PaymentMesEnum.BMS_PAY_SAME_USER.exception(); } @@ -584,11 +584,15 @@ public class PayBillServiceImpl implements IPayBillService adjust.setAmount(delta); adjust.setCurrency(line.getCurrency()); adjust.setFeeSource("MANUAL"); - adjust.setSourceId(String.valueOf(open.getDiffId())); + adjust.setSourceId(source != null && source.getId() != null ? String.valueOf(source.getId()) : null); adjust.setRemark(open.getReason()); adjust.setSettleSupplierId(bill.getSupplierId()); adjust.setSettleSupplierName(bill.getSupplierName()); payBillFeeMapper.insertPrcPayBillFee(adjust); + if (adjust.getSourceId() == null || adjust.getSourceId().isBlank()) + { + payBillFeeMapper.fillSourceIdByFeeIds(List.of(adjust.getFeeId())); + } payFinanceMapper.stampFee(adjust.getFeeId(), bill.getSupplierId(), bill.getSupplierName()); if (line.getOrderBillId() != null) { diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayOrderCostServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayOrderCostServiceImpl.java index 9598f1c3..fd123062 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayOrderCostServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayOrderCostServiceImpl.java @@ -115,13 +115,9 @@ public class PayOrderCostServiceImpl implements IPayOrderCostService boolean postcodeOn = ruleOn(billingRules, PaySurchargeRules.POSTCODE); boolean zoneOn = ruleOn(billingRules, PaySurchargeRules.ZONE); List remoteHits = null; - if (postcodeOn && !hasPrefixRules(billingRules, PaySurchargeRules.POSTCODE)) + if (!hasPrefixRules(billingRules, PaySurchargeRules.POSTCODE)) { remoteHits = remoteHits(linked, bill.getDestPostcode(), bill.getCountryCode(), bill.getCountryName()); - if (remoteHits == null) - { - remoteHits = new ArrayList<>(); - } } PayOrderCostResult result = PayCostCalculator.calculate(product, weight, bill.getPkgLength(), bill.getPkgWidth(), bill.getPkgHeight(), bill.getDestPostcode(), zoneName, bizDate, orderStatus, remoteHits, postcodeOn, zoneOn, @@ -196,7 +192,7 @@ public class PayOrderCostServiceImpl implements IPayOrderCostService priced.setBillingRules(rules == null ? new ArrayList<>() : rules); } - /** 配置页已经写了邮编前缀时,命中邮编不再去应付邮编配置里查偏远。 */ + /** 附加费规则里已经写了邮编前缀时,偏远不再去应付邮编配置里查。 */ private static boolean hasPrefixRules(List rules, String calcRule) { if (rules == null) @@ -265,19 +261,49 @@ public class PayOrderCostServiceImpl implements IPayOrderCostService } /** - * 用产品自己的偏远邮编判断这票是否偏远。没有偏远配置时返回 null,计费改看价卡上的邮编条件。 + * 判断这票是否偏远。产品报价表已经挂了偏远名单时只用那一份,没命中就不再回价卡邮编。 + * 没有这份产品名单时,改查目的国家的偏远邮编;国家名单没命中则返回 null,价卡上的邮编条件仍可用。 * * @param linked 已对上的邮编组 * @param postcode 收件邮编 * @param countryCode 目的国二字码 * @param countryName 目的国名称 - * @return 命中的偏远分区;有配置但没命中时返回空列表 + * @return 命中的偏远分区;产品有名单但没命中时返回空列表 */ private List remoteHits(List linked, String postcode, String countryCode, String countryName) + { + boolean productBook = false; + for (PayPostcodeGroup group : linked) + { + if (group != null && PayRemoteBook.remoteGroup(group)) + { + productBook = true; + break; + } + } + if (productBook) + { + return hitsOf(linked, postcode, countryCode, countryName); + } + if (blank(countryCode) && blank(countryName)) + { + return null; + } + List countryGroups = payPostcodeMapper.selectRemoteGroupsByCountry(countryCode, countryName); + if (countryGroups == null || countryGroups.isEmpty()) + { + return null; + } + List hits = hitsOf(countryGroups, postcode, countryCode, countryName); + return hits == null || hits.isEmpty() ? null : hits; + } + + /** 在给定邮编组里做偏远匹配。没有偏远组时返回 null,有组但邮编没命中时返回空列表。 */ + private List hitsOf(List groups, String postcode, String countryCode, String countryName) { List remoteIds = new ArrayList<>(); boolean anyRemote = false; - for (PayPostcodeGroup group : linked) + for (PayPostcodeGroup group : groups) { if (group == null || group.getGroupId() == null || !PayRemoteBook.remoteGroup(group)) { @@ -299,7 +325,7 @@ public class PayOrderCostServiceImpl implements IPayOrderCostService } List lines = postcode == null || postcode.isBlank() ? new ArrayList<>() : payPostcodeMapper.selectLinesByGroups(remoteIds); - return PayRemoteBook.hits(linked, lines, postcode, countryCode, countryName); + return PayRemoteBook.hits(groups, lines, postcode, countryCode, countryName); } /** diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayPaymentServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayPaymentServiceImpl.java index ca85a16f..a657169c 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayPaymentServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayPaymentServiceImpl.java @@ -131,7 +131,7 @@ public class PayPaymentServiceImpl implements IPayPaymentService payment.setAllocs(payPaymentMapper.selectAllocs(paymentId)); checkAllocs(payment); String operator = SecurityUtils.getUsername(); - if (operator.equals(payment.getCreateBy())) + if (!SecurityUtils.isAdmin() && operator.equals(payment.getCreateBy())) { throw PaymentMesEnum.BMS_PAY_SAME_PAY.exception(); } @@ -170,7 +170,7 @@ public class PayPaymentServiceImpl implements IPayPaymentService throw PaymentMesEnum.BMS_PAY_NO_REVERSE.exception(); } String operator = SecurityUtils.getUsername(); - if (operator.equals(payment.getConfirmBy())) + if (!SecurityUtils.isAdmin() && operator.equals(payment.getConfirmBy())) { throw PaymentMesEnum.BMS_PAY_SAME_PAY.exception(); } @@ -192,7 +192,7 @@ public class PayPaymentServiceImpl implements IPayPaymentService throw PaymentMesEnum.BMS_PAY_NO_REVERSE.exception(); } String operator = SecurityUtils.getUsername(); - if (operator.equals(payment.getConfirmBy())) + if (!SecurityUtils.isAdmin() && operator.equals(payment.getConfirmBy())) { throw PaymentMesEnum.BMS_PAY_SAME_PAY.exception(); } 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 2417c28b..86e0e59a 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 @@ -4,6 +4,8 @@ import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import java.util.UUID; +import com.bms.common.exception.ServiceException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -34,6 +36,227 @@ public class PayPostcodeServiceImpl implements IPayPostcodeService return payPostcodeMapper.selectGroupList(query); } + @Override + public List selectCountryList(PayPostcodeGroup query) + { + return payPostcodeMapper.selectCountryList(query); + } + + @Override + public List selectZoneList(PayPostcodeGroup query) + { + requireCountry(query); + return payPostcodeMapper.selectZoneList(query); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public int addZone(PayPostcodeGroup zone, String username) + { + requireCountry(zone); + String zoneName = requireZoneName(zone.getZoneName()); + zone.setZoneName(zoneName); + if (payPostcodeMapper.countZoneName(zone) > 0) + { + throw new ServiceException("分区「" + zoneName + "」已存在"); + } + PayPostcodeGroup sample = payPostcodeMapper.selectSampleGroup(zone); + PayPostcodeGroup row = new PayPostcodeGroup(); + row.setSourceFile("手工新增"); + row.setFileMd5(UUID.randomUUID().toString().replace("-", "")); + row.setBlockTitle(zoneName); + row.setConfigKind("ZONE"); + row.setCoverage(normalizeCoverage(zone.getCoverage())); + row.setCountryName(firstText(zone.getCountryName(), sample == null ? "" : sample.getCountryName())); + row.setCountryCode(firstText(zone.getCountryCode(), sample == null ? "" : sample.getCountryCode())); + row.setServiceType(sample == null ? "" : trimToEmpty(sample.getServiceType())); + row.setCatalogCountry(sample == null ? "" : trimToEmpty(sample.getCatalogCountry())); + if ("REMOTE".equals(row.getCoverage())) + { + row.setSheetName("手工偏远"); + row.setChannelName(""); + row.setProductName(""); + row.setQuoteSheet(""); + row.setFeeName("偏远"); + } + else + { + row.setSheetName(sample != null && !trimToEmpty(sample.getSheetName()).isEmpty() ? sample.getSheetName() : zoneName); + row.setChannelName(sample == null ? "" : trimToEmpty(sample.getChannelName())); + row.setProductName(sample == null ? "" : trimToEmpty(sample.getProductName())); + row.setQuoteSheet(sample == null ? "" : trimToEmpty(sample.getQuoteSheet())); + row.setFeeName(""); + } + row.setFeeText(""); + row.setLineCount(0); + row.setCreateBy(username); + return payPostcodeMapper.insertGroup(row); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public int updateZone(PayPostcodeGroup zone) + { + requireCountry(zone); + String zoneName = requireZoneName(zone.getZoneName()); + String oldName = requireZoneName(zone.getOldZoneName()); + zone.setZoneName(zoneName); + zone.setOldZoneName(oldName); + zone.setCoverage(normalizeCoverage(zone.getCoverage())); + if (!oldName.equals(zoneName) && payPostcodeMapper.countZoneName(zone) > 0) + { + throw new ServiceException("分区「" + zoneName + "」已存在"); + } + payPostcodeMapper.renameZoneLines(zone); + return payPostcodeMapper.updateExclusiveZone(zone); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public int deleteZone(PayPostcodeGroup zone) + { + requireCountry(zone); + zone.setZoneName(requireZoneName(zone.getZoneName())); + payPostcodeMapper.deleteLinesByZone(zone); + List groupIds = payPostcodeMapper.selectGroupIdsByCountry(zone); + int removed = 0; + if (groupIds != null) + { + for (Long groupId : groupIds) + { + payPostcodeMapper.refreshLineCount(groupId); + PayPostcodeGroup group = payPostcodeMapper.selectGroupById(groupId); + if (group != null && "手工新增".equals(group.getSourceFile()) && zone.getZoneName().equals(trimToEmpty(group.getBlockTitle())) + && (group.getLineCount() == null || group.getLineCount() == 0)) + { + payPostcodeMapper.deleteGroups(List.of(groupId)); + removed++; + } + } + } + return removed + 1; + } + + @Override + public int updateCountry(PayPostcodeGroup country) + { + requireCountry(country); + country.setServiceType(trimToEmpty(country.getServiceType())); + country.setProductName(trimToEmpty(country.getProductName())); + country.setChannelName(trimToEmpty(country.getChannelName())); + return payPostcodeMapper.updateCountryProfile(country); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public int deleteCountry(PayPostcodeGroup country) + { + requireCountry(country); + List ids = payPostcodeMapper.selectGroupIdsByCountry(country); + if (ids == null || ids.isEmpty()) + { + return 0; + } + payPostcodeMapper.deleteLines(ids); + return payPostcodeMapper.deleteGroups(ids); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public int addLine(PayPostcodeLine line) + { + if (line == null) + { + throw new ServiceException("请填写邮编"); + } + PayPostcodeGroup zone = countryOf(line); + zone.setZoneName(requireZoneName(line.getZoneName())); + Long groupId = payPostcodeMapper.selectZoneGroupId(zone); + if (groupId == null) + { + addZone(zone, "system"); + groupId = payPostcodeMapper.selectZoneGroupId(zone); + } + if (groupId == null) + { + throw new ServiceException("分区不存在"); + } + PayPostcodeGroup group = payPostcodeMapper.selectGroupById(groupId); + line.setGroupId(groupId); + line.setZoneName(zone.getZoneName()); + line.setCountryName(group == null ? zone.getCountryName() : group.getCountryName()); + line.setCountryCode(group == null ? zone.getCountryCode() : group.getCountryCode()); + line.setCityName(trimToEmpty(line.getCityName())); + line.setPostcode(cleanPostcode(line.getPostcode())); + line.setPostcodeRaw(trimToEmpty(line.getPostcodeRaw()).isEmpty() ? line.getPostcode() : trimToEmpty(line.getPostcodeRaw())); + line.setPostcodeEnd(cleanOptionalPostcode(line.getPostcodeEnd())); + if (line.getPostcode().isEmpty()) + { + throw new ServiceException("请填写邮编"); + } + int rows = payPostcodeMapper.insertLine(line); + payPostcodeMapper.refreshLineCount(groupId); + return rows; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public int updateLine(PayPostcodeLine line) + { + if (line == null || line.getLineId() == null) + { + throw new ServiceException("请选择邮编"); + } + PayPostcodeLine existed = payPostcodeMapper.selectLineById(line.getLineId()); + if (existed == null) + { + throw new ServiceException("邮编不存在或已删除"); + } + line.setCityName(trimToEmpty(line.getCityName())); + line.setZoneName(trimToEmpty(line.getZoneName()).isEmpty() ? existed.getZoneName() : trimToEmpty(line.getZoneName())); + line.setPostcode(cleanPostcode(line.getPostcode())); + line.setPostcodeRaw(line.getPostcode()); + line.setPostcodeEnd(cleanOptionalPostcode(line.getPostcodeEnd())); + if (line.getPostcode().isEmpty()) + { + throw new ServiceException("请填写邮编"); + } + int rows = payPostcodeMapper.updateLine(line); + payPostcodeMapper.refreshLineCount(existed.getGroupId()); + return rows; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public int deleteLines(Long[] lineIds) + { + List ids = new ArrayList<>(); + if (lineIds != null) + { + for (Long id : lineIds) + { + if (id != null) + { + ids.add(id); + } + } + } + if (ids.isEmpty()) + { + return 0; + } + List groupIds = payPostcodeMapper.selectGroupIdsByLineIds(ids); + int rows = payPostcodeMapper.deleteLineByIds(ids); + if (groupIds != null) + { + for (Long groupId : groupIds) + { + payPostcodeMapper.refreshLineCount(groupId); + } + } + return rows; + } + @Override public List selectBlockTitlesByCountry(String countryCode, String countryName) { @@ -180,4 +403,53 @@ public class PayPostcodeServiceImpl implements IPayPostcodeService } throw PaymentMesEnum.BMS_PAY_ZIP_ATTR.exception(); } + + private static void requireCountry(PayPostcodeGroup country) + { + if (country == null || (trimToEmpty(country.getCountryCode()).isEmpty() && trimToEmpty(country.getCountryName()).isEmpty())) + { + throw new ServiceException("请选择国家"); + } + } + + private static String requireZoneName(String zoneName) + { + String name = trimToEmpty(zoneName); + if (name.isEmpty()) + { + throw new ServiceException("请填写分区"); + } + return name; + } + + private static PayPostcodeGroup countryOf(PayPostcodeLine line) + { + PayPostcodeGroup country = new PayPostcodeGroup(); + country.setCountryCode(line.getCountryCode()); + country.setCountryName(line.getCountryName()); + requireCountry(country); + return country; + } + + private static String firstText(String preferred, String fallback) + { + String value = trimToEmpty(preferred); + return value.isEmpty() ? trimToEmpty(fallback) : value; + } + + private static String cleanPostcode(String postcode) + { + String value = trimToEmpty(postcode); + while (value.endsWith("*")) + { + value = value.substring(0, value.length() - 1).trim(); + } + return value; + } + + private static String cleanOptionalPostcode(String postcode) + { + String value = cleanPostcode(postcode); + return value.isEmpty() ? null : value; + } } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PaySurchargeRuleServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PaySurchargeRuleServiceImpl.java index bbc4b5c3..078c9d3a 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PaySurchargeRuleServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PaySurchargeRuleServiceImpl.java @@ -175,7 +175,7 @@ public class PaySurchargeRuleServiceImpl implements IPaySurchargeRuleService String feeName = text(row.getDetail().get("feeName")); if (feeName != null && !alwaysFees.add(feeName)) { - throw new ServiceException("每票都收的费用项不能重复:" + feeName); + throw new ServiceException("存在两笔同类型的附加费:" + feeName); } } return rules; diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcPayBillFeeServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcPayBillFeeServiceImpl.java index 49ef88a3..05cff2b5 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcPayBillFeeServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcPayBillFeeServiceImpl.java @@ -43,6 +43,19 @@ public class PrcPayBillFeeServiceImpl implements IPrcPayBillFeeService { List chunk = fees.subList(from, Math.min(from + BATCH_SIZE, fees.size())); payBillFeeMapper.insertBatch(chunk); + List ownIds = new ArrayList<>(); + for (PrcPayBillFee fee : chunk) + { + if (fee != null && fee.getFeeId() != null && !fee.getFeeId().isBlank() + && (fee.getSourceId() == null || fee.getSourceId().isBlank())) + { + ownIds.add(fee.getFeeId()); + } + } + if (!ownIds.isEmpty()) + { + payBillFeeMapper.fillSourceIdByFeeIds(ownIds); + } for (PrcPayBillFee fee : chunk) { if (fee != null && fee.getBillId() != null && !fee.getBillId().isBlank()) diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/SrvBillAirAllocServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/SrvBillAirAllocServiceImpl.java index 60cbb67f..5795f947 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/SrvBillAirAllocServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/SrvBillAirAllocServiceImpl.java @@ -247,7 +247,6 @@ public class SrvBillAirAllocServiceImpl implements ISrvBillAirAllocService row.setRoundMode("HALF_UP"); row.setRemark(remark(fee)); row.setFeeSource(FEE_SOURCE); - row.setSourceId(fee.getLadingId().trim()); row.setIsDeleted("0"); return row; } 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 14d8a716..caf87e39 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 @@ -50,18 +50,292 @@ order by block_title asc - + select min(g.group_id) as groupId, + max(g.country_name) as countryName, + max(g.country_code) as countryCode, + group_concat(distinct nullif(trim(g.coverage), '') order by g.coverage separator '、') as coverage, + group_concat(distinct nullif(trim(g.service_type), '') order by g.service_type separator '、') as serviceType, + group_concat(distinct nullif(trim(g.product_name), '') order by g.product_name separator '、') as productName, + group_concat(distinct nullif(trim(g.channel_name), '') order by g.channel_name separator '、') as channelName, + count(distinct + case + when l.line_id is not null then + case + when nullif(trim(l.zone_name), '') is not null then trim(l.zone_name) + when nullif(trim(l.region_name), '') is not null then trim(l.region_name) + when nullif(trim(g.block_title), '') is not null then trim(g.block_title) + else '未分区' + end + when nullif(trim(g.block_title), '') is not null then trim(g.block_title) + else null + end + ) as zoneCount, + count(l.line_id) as lineCount, + max(g.create_time) as createTime + from pay_postcode_group g + left join pay_postcode_line l on l.group_id = g.group_id - and group_id = #{groupId} - and postcode like concat('%', #{postcode}, '%') - and (region_name like concat('%', #{regionName}, '%') or region_name_cn like concat('%', #{regionName}, '%') or zone_name like concat('%', #{regionName}, '%')) + + and (g.country_name like concat('%', #{countryName}, '%') or g.country_code like concat('%', #{countryName}, '%') or g.catalog_country like concat('%', #{countryName}, '%')) + + and g.service_type like concat('%', #{serviceType}, '%') + and g.product_name like concat('%', #{productName}, '%') + and g.channel_name like concat('%', #{channelName}, '%') + and g.coverage = #{coverage} + + and exists ( + select 1 from pay_postcode_line lp + where lp.group_id = g.group_id and lp.postcode like concat('%', #{postcode}, '%') + ) + - order by sort_no asc, line_id asc + group by case + when nullif(trim(g.country_code), '') is not null then upper(trim(g.country_code)) + when nullif(trim(g.country_name), '') is not null then trim(g.country_name) + else '未填写国家' + end + order by max(g.create_time) desc + + + + + + + + + + + + update pay_postcode_group g + set g.service_type = #{serviceType}, + g.product_name = #{productName}, + g.channel_name = #{channelName} + + + + + + + + + update pay_postcode_line l + inner join pay_postcode_group g on g.group_id = l.group_id + set l.zone_name = #{zoneName} + + + and = #{oldZoneName} + + + + + update pay_postcode_group g + set g.block_title = case + when trim(g.block_title) = #{oldZoneName} or g.source_file = '手工新增' then #{zoneName} + else g.block_title + end, + g.coverage = #{coverage}, + g.fee_name = case + when #{coverage} = 'REMOTE' and (g.fee_name is null or trim(g.fee_name) = '') then '偏远' + else g.fee_name + end + + + and ( + trim(g.block_title) = #{oldZoneName} + or ( + exists ( + select 1 from pay_postcode_line l + where l.group_id = g.group_id and = #{zoneName} + ) + and not exists ( + select 1 from pay_postcode_line l + where l.group_id = g.group_id and <> #{zoneName} + ) + ) + ) + + + + + insert into pay_postcode_line ( + group_id, country_name, country_code, region_name, region_name_cn, city_name, region_code, + zone_name, postcode, postcode_raw, postcode_end, extra_text, sort_no + ) values ( + #{groupId}, #{countryName}, #{countryCode}, #{regionName}, #{regionNameCn}, #{cityName}, #{regionCode}, + #{zoneName}, #{postcode}, #{postcodeRaw}, #{postcodeEnd}, #{extraText}, ifnull(#{sortNo}, 0) + ) + + + + update pay_postcode_line + set city_name = #{cityName}, + zone_name = #{zoneName}, + postcode = #{postcode}, + postcode_raw = #{postcodeRaw}, + postcode_end = #{postcodeEnd} + where line_id = #{lineId} + + + + + + + + delete from pay_postcode_line where line_id in + #{id} + + + + delete l from pay_postcode_line l + inner join pay_postcode_group g on g.group_id = l.group_id + + + and = #{zoneName} + + + + + update pay_postcode_group g + set g.line_count = (select count(*) from pay_postcode_line l where l.group_id = g.group_id) + where g.group_id = #{groupId} + + + @@ -134,6 +408,30 @@ + + - select fee_id, bill_id, pay_bill_id, fee_type, fee_name, sc_id, fee_level, amount, currency, round_mode, remark, + select id, fee_id, bill_id, pay_bill_id, fee_type, fee_name, sc_id, fee_level, amount, currency, round_mode, remark, fee_source, source_id, is_deleted, create_time from prc_pay_bill_fee where bill_id = #{billId} @@ -92,6 +93,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" and ifnull(is_deleted, '0') = '0' + + update prc_pay_bill_fee + set source_id = id + where (source_id is null or source_id = '') + and fee_id in + #{feeId} + + delete from prc_pay_bill_fee where bill_id = #{billId} diff --git a/bms-front/src/api/data/payPostcode.js b/bms-front/src/api/data/payPostcode.js index b6c79184..5c88f293 100644 --- a/bms-front/src/api/data/payPostcode.js +++ b/bms-front/src/api/data/payPostcode.js @@ -51,3 +51,39 @@ export function delPayPostcode(groupIds) { method: 'delete' }) } + +export function listPayPostcodeZone(query) { + return request({ url: '/data/payPostcode/zones', method: 'get', params: query }) +} + +export function addPayPostcodeZone(data) { + return request({ url: '/data/payPostcode/zones', method: 'post', data }) +} + +export function updatePayPostcodeZone(data) { + return request({ url: '/data/payPostcode/zones', method: 'put', data }) +} + +export function delPayPostcodeZone(query) { + return request({ url: '/data/payPostcode/zones', method: 'delete', params: query }) +} + +export function updatePayPostcodeCountry(data) { + return request({ url: '/data/payPostcode/country', method: 'put', data }) +} + +export function delPayPostcodeCountry(query) { + return request({ url: '/data/payPostcode/country', method: 'delete', params: query }) +} + +export function addPayPostcodeLine(data) { + return request({ url: '/data/payPostcode/lines', method: 'post', data }) +} + +export function updatePayPostcodeLine(data) { + return request({ url: '/data/payPostcode/lines', method: 'put', data }) +} + +export function delPayPostcodeLine(lineIds) { + return request({ url: '/data/payPostcode/lines/' + lineIds, method: 'delete' }) +} diff --git a/bms-front/src/views/data/payPostcode/components/PayPostcodeDrawer.vue b/bms-front/src/views/data/payPostcode/components/PayPostcodeDrawer.vue new file mode 100644 index 00000000..cd4b130c --- /dev/null +++ b/bms-front/src/views/data/payPostcode/components/PayPostcodeDrawer.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/bms-front/src/views/data/payPostcode/components/PayPostcodeFormDialog.vue b/bms-front/src/views/data/payPostcode/components/PayPostcodeFormDialog.vue new file mode 100644 index 00000000..193b456d --- /dev/null +++ b/bms-front/src/views/data/payPostcode/components/PayPostcodeFormDialog.vue @@ -0,0 +1,78 @@ + + + diff --git a/bms-front/src/views/data/payPostcode/components/PayZoneDrawer.vue b/bms-front/src/views/data/payPostcode/components/PayZoneDrawer.vue new file mode 100644 index 00000000..0209a376 --- /dev/null +++ b/bms-front/src/views/data/payPostcode/components/PayZoneDrawer.vue @@ -0,0 +1,146 @@ + + + + + diff --git a/bms-front/src/views/data/payPostcode/components/PayZoneFormDialog.vue b/bms-front/src/views/data/payPostcode/components/PayZoneFormDialog.vue new file mode 100644 index 00000000..9e3b6594 --- /dev/null +++ b/bms-front/src/views/data/payPostcode/components/PayZoneFormDialog.vue @@ -0,0 +1,83 @@ + + + diff --git a/bms-front/src/views/data/payPostcode/index.vue b/bms-front/src/views/data/payPostcode/index.vue index c565e68f..2b17cf00 100644 --- a/bms-front/src/views/data/payPostcode/index.vue +++ b/bms-front/src/views/data/payPostcode/index.vue @@ -1,40 +1,41 @@ diff --git a/bms-front/src/views/price/basic/pay/components/PaySurchargeRuleDialog.vue b/bms-front/src/views/price/basic/pay/components/PaySurchargeRuleDialog.vue index 8ffbbdc5..f7cae94d 100644 --- a/bms-front/src/views/price/basic/pay/components/PaySurchargeRuleDialog.vue +++ b/bms-front/src/views/price/basic/pay/components/PaySurchargeRuleDialog.vue @@ -420,6 +420,7 @@ const keyword = ref('') const found = ref([]) const products = ref([]) const feeOptions = ref([]) +const feeCounts = ref({}) const zoneOptions = ref([]) const enabled = reactive({}) const groups = reactive({}) @@ -455,6 +456,7 @@ function reset() { found.value = [] products.value = [] feeOptions.value = [] + feeCounts.value = {} zoneOptions.value = [] blankGroups() saving.value = false @@ -504,24 +506,32 @@ function loadFees() { const bound = products.value.slice() if (!bound.length) { feeOptions.value = [] + feeCounts.value = {} zoneOptions.value = keepZones([]) - return + return Promise.resolve() } - Promise.all(bound.map(item => getPay(item.feeProductId || item.productId).catch(() => null))).then(results => { + return Promise.all(bound.map(item => getPay(item.feeProductId || item.productId).catch(() => null))).then(results => { if (token !== feeToken) return const map = new Map() + const counts = {} const zones = new Set() results.forEach(res => { + const local = {} const rows = res?.data?.surcharges || [] rows.forEach(fee => { const name = String(fee.feeName || '').trim() const zone = String(fee.zoneName || '').trim() if (zone) zones.add(zone) - if (!name || map.has(name)) return - map.set(name, { feeName: name, scId: fee.scId || '' }) + if (!name) return + local[name] = (local[name] || 0) + 1 + if (!map.has(name)) map.set(name, { feeName: name, scId: fee.scId || '' }) + }) + Object.keys(local).forEach(name => { + counts[name] = Math.max(counts[name] || 0, local[name]) }) }) zoneOptions.value = keepZones([...zones]) + feeCounts.value = counts for (const row of groups.ALWAYS || []) { const name = String(row.feeName || '').trim() if (name && !map.has(name)) map.set(name, { feeName: name, scId: row.scId || '' }) @@ -659,35 +669,48 @@ function save() { } } if (enabled.ALWAYS) { - const names = new Set() for (const row of groups.ALWAYS) { - const name = String(row.feeName || '').trim() - if (!name) { + if (!String(row.feeName || '').trim()) { proxy.$modal.msgError('每票都收要选择费用项') return } - if (names.has(name)) { - proxy.$modal.msgError('每票都收的费用项不能重复:' + name) - return - } - names.add(name) } } saving.value = true - savePaySurchargeRule({ - products: products.value.map(item => ({ - productId: item.productId, - productName: item.productName - })), - rules: payloadRules() - }).then(() => { - proxy.$modal.msgSuccess('已保存,计算时按当前生效版本上的附加费执行') - visible.value = false + loadFees().then(() => { + const duplicated = sameTypeFees() + if (duplicated.length) { + proxy.$modal.alert('存在两笔同类型的附加费:' + duplicated.join('、')) + return + } + return savePaySurchargeRule({ + products: products.value.map(item => ({ + productId: item.productId, + productName: item.productName + })), + rules: payloadRules() + }).then(() => { + proxy.$modal.msgSuccess('已保存,计算时按当前生效版本上的附加费执行') + visible.value = false + }) }).finally(() => { saving.value = false }) } +function sameTypeFees() { + if (!enabled.ALWAYS) return [] + const seen = new Set() + const duplicated = [] + for (const row of groups.ALWAYS) { + const name = String(row.feeName || '').trim() + if (!name) continue + if (seen.has(name) || Number(feeCounts.value[name] || 0) >= 2) duplicated.push(name) + seen.add(name) + } + return [...new Set(duplicated)] +} + defineExpose({ open }) diff --git a/bms-front/src/views/price/billing/payBill/index.vue b/bms-front/src/views/price/billing/payBill/index.vue index 100c3d06..10b32958 100644 --- a/bms-front/src/views/price/billing/payBill/index.vue +++ b/bms-front/src/views/price/billing/payBill/index.vue @@ -281,10 +281,12 @@