应付计费邮编改造

This commit is contained in:
liuminghui
2026-11-13 18:26:21 +08:00
parent 880b4751c5
commit bde1d7d86e
30 changed files with 1761 additions and 254 deletions
@@ -40,10 +40,81 @@ public class PayPostcodeController extends BaseController
public TableDataInfo list(PayPostcodeGroup query)
{
startPage();
List<PayPostcodeGroup> list = payPostcodeService.selectGroupList(query);
List<PayPostcodeGroup> 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")
@@ -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<PayRemoteBook.Hit> 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<PayRemoteBook.Hit> 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()
{
@@ -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<String, Object> 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();
@@ -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 */
@@ -59,6 +59,15 @@ public class PayPostcodeGroup extends BaseEntity
/** 查询条件:邮编 */
private String postcode;
/** 国家下的分区名,例如 mainland。列表按国家归拢后,分区在抽屉里维护 */
private String zoneName;
/** 修改分区时的原名称 */
private String oldZoneName;
/** 这个国家下的分区个数 */
private Integer zoneCount;
/** 导入时挂上的明细,列表查询不填充 */
private List<PayPostcodeLine> lines = new ArrayList<>();
}
@@ -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已删除 */
@@ -10,6 +10,38 @@ public interface PayPostcodeMapper
{
List<PayPostcodeGroup> selectGroupList(PayPostcodeGroup query);
List<PayPostcodeGroup> selectCountryList(PayPostcodeGroup query);
List<PayPostcodeGroup> selectZoneList(PayPostcodeGroup query);
PayPostcodeGroup selectSampleGroup(PayPostcodeGroup query);
int countZoneName(PayPostcodeGroup query);
int updateCountryProfile(PayPostcodeGroup country);
List<Long> selectGroupIdsByCountry(PayPostcodeGroup query);
int renameZoneLines(PayPostcodeGroup zone);
int updateExclusiveZone(PayPostcodeGroup zone);
int insertLine(PayPostcodeLine line);
int updateLine(PayPostcodeLine line);
PayPostcodeLine selectLineById(Long lineId);
List<Long> selectGroupIdsByLineIds(@Param("lineIds") List<Long> lineIds);
int deleteLineByIds(@Param("lineIds") List<Long> lineIds);
int deleteLinesByZone(PayPostcodeGroup zone);
int refreshLineCount(Long groupId);
Long selectZoneGroupId(PayPostcodeGroup query);
/** 按国家去重区块标题 */
List<String> selectBlockTitlesByCountry(@Param("countryCode") String countryCode,
@Param("countryName") String countryName);
@@ -31,6 +63,10 @@ public interface PayPostcodeMapper
/** 报价表、销售产品或工作表与产品名称完全一致的邮编配置 */
List<PayPostcodeGroup> selectGroupsForProduct(@Param("names") List<String> names);
/** 目的国家下标记为偏远的邮编组。产品自己没有偏远名单时,用这一份做偏远附加费。 */
List<PayPostcodeGroup> selectRemoteGroupsByCountry(@Param("countryCode") String countryCode,
@Param("countryName") String countryName);
List<PayPostcodeLine> selectRegionKeys(@Param("groupIds") List<Long> groupIds);
List<PayPostcodeLine> selectLinesByGroups(@Param("groupIds") List<Long> groupIds);
@@ -31,6 +31,9 @@ public interface PrcPayBillFeeMapper
*/
public int insertBatch(@Param("list") List<PrcPayBillFee> fees);
/** 把还没标明来源的费用,关联到本行主键 id。 */
public int fillSourceIdByFeeIds(@Param("feeIds") List<String> feeIds);
/**
* 删除一票订单上来源为计算的费用。提单分摊和人工费用保留。
*
@@ -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<PayRemoteBook.Hit> hits, PaySurcharge fee, String rule)
/** 邮编配置命中后,金额相同的那一档才收。命中写了地区时要对上分区;没写地区时用价卡上的邮编前缀选档。 */
private static boolean bookApplies(List<PayRemoteBook.Hit> 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)
@@ -297,6 +297,7 @@ public final class PayCostCalculator
return;
}
List<PayCostLine> sized = new ArrayList<>();
List<PayCostLine> 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<PayCostLine> keepHighestByName(List<PayCostLine> lines)
{
List<PayCostLine> 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<PayRemoteBook.Hit> hits, PaySurcharge fee, String rule)
/** 邮编配置命中后,金额相同的那一档才收。命中写了地区时要对上分区;没写地区时用价卡上的邮编前缀选档。 */
private static boolean bookApplies(List<PayRemoteBook.Hit> 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<String, Object> 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<String, Object> 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)
@@ -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<String, Object> 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<Boolean> checks, BigDecimal actual, Object min, Object max)
@@ -10,6 +10,29 @@ public interface IPayPostcodeService
{
List<PayPostcodeGroup> selectGroupList(PayPostcodeGroup query);
/** 每个国家一行:邮编属性、服务类型、销售产品、渠道、分区数、邮编总数、导入时间 */
List<PayPostcodeGroup> selectCountryList(PayPostcodeGroup query);
/** 一个国家下面的分区,同一分区名只出现一行 */
List<PayPostcodeGroup> 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<String> selectBlockTitlesByCountry(String countryCode, String countryName);
@@ -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)
{
@@ -115,13 +115,9 @@ public class PayOrderCostServiceImpl implements IPayOrderCostService
boolean postcodeOn = ruleOn(billingRules, PaySurchargeRules.POSTCODE);
boolean zoneOn = ruleOn(billingRules, PaySurchargeRules.ZONE);
List<PayRemoteBook.Hit> 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<PaySurchargeRule> 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<PayRemoteBook.Hit> remoteHits(List<PayPostcodeGroup> 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<PayPostcodeGroup> countryGroups = payPostcodeMapper.selectRemoteGroupsByCountry(countryCode, countryName);
if (countryGroups == null || countryGroups.isEmpty())
{
return null;
}
List<PayRemoteBook.Hit> hits = hitsOf(countryGroups, postcode, countryCode, countryName);
return hits == null || hits.isEmpty() ? null : hits;
}
/** 在给定邮编组里做偏远匹配。没有偏远组时返回 null,有组但邮编没命中时返回空列表。 */
private List<PayRemoteBook.Hit> hitsOf(List<PayPostcodeGroup> groups, String postcode, String countryCode, String countryName)
{
List<Long> 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<PayPostcodeLine> 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);
}
/**
@@ -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();
}
@@ -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<PayPostcodeGroup> selectCountryList(PayPostcodeGroup query)
{
return payPostcodeMapper.selectCountryList(query);
}
@Override
public List<PayPostcodeGroup> 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<Long> 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<Long> 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<Long> ids = new ArrayList<>();
if (lineIds != null)
{
for (Long id : lineIds)
{
if (id != null)
{
ids.add(id);
}
}
}
if (ids.isEmpty())
{
return 0;
}
List<Long> groupIds = payPostcodeMapper.selectGroupIdsByLineIds(ids);
int rows = payPostcodeMapper.deleteLineByIds(ids);
if (groupIds != null)
{
for (Long groupId : groupIds)
{
payPostcodeMapper.refreshLineCount(groupId);
}
}
return rows;
}
@Override
public List<String> 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;
}
}
@@ -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;
@@ -43,6 +43,19 @@ public class PrcPayBillFeeServiceImpl implements IPrcPayBillFeeService
{
List<PrcPayBillFee> chunk = fees.subList(from, Math.min(from + BATCH_SIZE, fees.size()));
payBillFeeMapper.insertBatch(chunk);
List<String> 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())
@@ -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;
}
@@ -50,18 +50,292 @@
order by block_title asc
</select>
<select id="selectLineList" parameterType="com.bms.price.domain.PayPostcodeLine" resultType="com.bms.price.domain.PayPostcodeLine">
select line_id as lineId, group_id as groupId, country_name as countryName, country_code as countryCode,
region_name as regionName, region_name_cn as regionNameCn, city_name as cityName, region_code as regionCode,
zone_name as zoneName, postcode, postcode_raw as postcodeRaw, postcode_end as postcodeEnd,
extra_text as extraText, sort_no as sortNo
from pay_postcode_line
<sql id="countryMatch">
<choose>
<when test="countryCode != null and countryCode != ''">
and upper(trim(g.country_code)) = upper(trim(#{countryCode}))
</when>
<otherwise>
and (g.country_code is null or trim(g.country_code) = '')
and g.country_name = #{countryName}
</otherwise>
</choose>
</sql>
<sql id="zoneExpr">
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
</sql>
<select id="selectCountryList" parameterType="com.bms.price.domain.PayPostcodeGroup" resultType="com.bms.price.domain.PayPostcodeGroup">
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
<where>
<if test="groupId != null">and group_id = #{groupId}</if>
<if test="postcode != null and postcode != ''">and postcode like concat('%', #{postcode}, '%')</if>
<if test="regionName != null and regionName != ''">and (region_name like concat('%', #{regionName}, '%') or region_name_cn like concat('%', #{regionName}, '%') or zone_name like concat('%', #{regionName}, '%'))</if>
<if test="countryName != null and countryName != ''">
and (g.country_name like concat('%', #{countryName}, '%') or g.country_code like concat('%', #{countryName}, '%') or g.catalog_country like concat('%', #{countryName}, '%'))
</if>
<if test="serviceType != null and serviceType != ''">and g.service_type like concat('%', #{serviceType}, '%')</if>
<if test="productName != null and productName != ''">and g.product_name like concat('%', #{productName}, '%')</if>
<if test="channelName != null and channelName != ''">and g.channel_name like concat('%', #{channelName}, '%')</if>
<if test="coverage != null and coverage != ''">and g.coverage = #{coverage}</if>
<if test="postcode != null and postcode != ''">
and exists (
select 1 from pay_postcode_line lp
where lp.group_id = g.group_id and lp.postcode like concat('%', #{postcode}, '%')
)
</if>
</where>
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
</select>
<select id="selectZoneList" parameterType="com.bms.price.domain.PayPostcodeGroup" resultType="com.bms.price.domain.PayPostcodeGroup">
select t.zone_name as zoneName,
min(t.group_id) as groupId,
group_concat(distinct nullif(trim(t.coverage), '') order by t.coverage separator '、') as coverage,
count(t.line_id) as lineCount,
max(t.country_name) as countryName,
max(t.country_code) as countryCode
from (
select g.group_id, g.coverage, g.country_name, g.country_code, l.line_id,
case
when l.line_id is null then nullif(trim(g.block_title), '')
else
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
end as zone_name
from pay_postcode_group g
left join pay_postcode_line l on l.group_id = g.group_id
<where>
<include refid="countryMatch"/>
</where>
) t
where t.zone_name is not null and t.zone_name &lt;&gt; ''
group by t.zone_name
order by t.zone_name asc
</select>
<select id="selectSampleGroup" parameterType="com.bms.price.domain.PayPostcodeGroup" resultType="com.bms.price.domain.PayPostcodeGroup">
select group_id as groupId, source_file as sourceFile, file_md5 as fileMd5, sheet_name as sheetName,
block_title as blockTitle, config_kind as configKind, coverage, channel_name as channelName,
country_name as countryName, country_code as countryCode,
service_type as serviceType, catalog_country as catalogCountry, product_name as productName, quote_sheet as quoteSheet
from pay_postcode_group g
<where>
<include refid="countryMatch"/>
</where>
order by g.group_id asc
limit 1
</select>
<select id="countZoneName" parameterType="com.bms.price.domain.PayPostcodeGroup" resultType="int">
select count(*) from (
select 1
from pay_postcode_group g
left join pay_postcode_line l on l.group_id = g.group_id
<where>
<include refid="countryMatch"/>
and (
(l.line_id is null and trim(g.block_title) = #{zoneName})
or (l.line_id is not null and <include refid="zoneExpr"/> = #{zoneName})
)
</where>
limit 1
) hit
</select>
<select id="selectZoneGroupId" parameterType="com.bms.price.domain.PayPostcodeGroup" resultType="long">
select g.group_id
from pay_postcode_group g
left join pay_postcode_line l on l.group_id = g.group_id
<where>
<include refid="countryMatch"/>
and (
(g.source_file = '手工新增' and trim(g.block_title) = #{zoneName})
or (l.line_id is not null and <include refid="zoneExpr"/> = #{zoneName})
)
</where>
order by case when g.source_file = '手工新增' and trim(g.block_title) = #{zoneName} then 0 else 1 end, g.group_id asc
limit 1
</select>
<update id="updateCountryProfile" parameterType="com.bms.price.domain.PayPostcodeGroup">
update pay_postcode_group g
set g.service_type = #{serviceType},
g.product_name = #{productName},
g.channel_name = #{channelName}
<where>
<include refid="countryMatch"/>
</where>
</update>
<select id="selectGroupIdsByCountry" parameterType="com.bms.price.domain.PayPostcodeGroup" resultType="long">
select g.group_id from pay_postcode_group g
<where>
<include refid="countryMatch"/>
</where>
</select>
<update id="renameZoneLines" parameterType="com.bms.price.domain.PayPostcodeGroup">
update pay_postcode_line l
inner join pay_postcode_group g on g.group_id = l.group_id
set l.zone_name = #{zoneName}
<where>
<include refid="countryMatch"/>
and <include refid="zoneExpr"/> = #{oldZoneName}
</where>
</update>
<update id="updateExclusiveZone" parameterType="com.bms.price.domain.PayPostcodeGroup">
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
<where>
<include refid="countryMatch"/>
and (
trim(g.block_title) = #{oldZoneName}
or (
exists (
select 1 from pay_postcode_line l
where l.group_id = g.group_id and <include refid="zoneExpr"/> = #{zoneName}
)
and not exists (
select 1 from pay_postcode_line l
where l.group_id = g.group_id and <include refid="zoneExpr"/> &lt;&gt; #{zoneName}
)
)
)
</where>
</update>
<insert id="insertLine" parameterType="com.bms.price.domain.PayPostcodeLine" useGeneratedKeys="true" keyProperty="lineId">
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)
)
</insert>
<update id="updateLine" parameterType="com.bms.price.domain.PayPostcodeLine">
update pay_postcode_line
set city_name = #{cityName},
zone_name = #{zoneName},
postcode = #{postcode},
postcode_raw = #{postcodeRaw},
postcode_end = #{postcodeEnd}
where line_id = #{lineId}
</update>
<select id="selectLineById" resultType="com.bms.price.domain.PayPostcodeLine">
select line_id as lineId, group_id as groupId, country_name as countryName, country_code as countryCode,
city_name as cityName, zone_name as zoneName, postcode, postcode_raw as postcodeRaw, postcode_end as postcodeEnd
from pay_postcode_line
where line_id = #{lineId}
</select>
<select id="selectGroupIdsByLineIds" resultType="long">
select distinct group_id from pay_postcode_line
where line_id in
<foreach collection="lineIds" item="id" open="(" separator="," close=")">#{id}</foreach>
</select>
<delete id="deleteLineByIds">
delete from pay_postcode_line where line_id in
<foreach collection="lineIds" item="id" open="(" separator="," close=")">#{id}</foreach>
</delete>
<delete id="deleteLinesByZone" parameterType="com.bms.price.domain.PayPostcodeGroup">
delete l from pay_postcode_line l
inner join pay_postcode_group g on g.group_id = l.group_id
<where>
<include refid="countryMatch"/>
and <include refid="zoneExpr"/> = #{zoneName}
</where>
</delete>
<update id="refreshLineCount">
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}
</update>
<select id="selectLineList" parameterType="com.bms.price.domain.PayPostcodeLine" resultType="com.bms.price.domain.PayPostcodeLine">
select l.line_id as lineId, l.group_id as groupId, l.country_name as countryName, l.country_code as countryCode,
l.region_name as regionName, l.region_name_cn as regionNameCn, l.city_name as cityName, l.region_code as regionCode,
l.zone_name as zoneName, l.postcode, l.postcode_raw as postcodeRaw, l.postcode_end as postcodeEnd,
l.extra_text as extraText, l.sort_no as sortNo
from pay_postcode_line l
<where>
<if test="groupId != null">and l.group_id = #{groupId}</if>
<if test="postcode != null and postcode != ''">and l.postcode like concat('%', #{postcode}, '%')</if>
<if test="regionName != null and regionName != ''">and (l.region_name like concat('%', #{regionName}, '%') or l.region_name_cn like concat('%', #{regionName}, '%') or l.zone_name like concat('%', #{regionName}, '%') or l.city_name like concat('%', #{regionName}, '%'))</if>
<if test="zoneName != null and zoneName != ''">
and (
trim(l.zone_name) = #{zoneName}
or ((l.zone_name is null or trim(l.zone_name) = '') and trim(l.region_name) = #{zoneName})
or (
(l.zone_name is null or trim(l.zone_name) = '')
and (l.region_name is null or trim(l.region_name) = '')
and l.group_id in (select g.group_id from pay_postcode_group g where trim(g.block_title) = #{zoneName})
)
)
</if>
<if test="countryCode != null and countryCode != ''">
and l.group_id in (
select g.group_id from pay_postcode_group g where upper(trim(g.country_code)) = upper(trim(#{countryCode}))
)
</if>
<if test="(countryCode == null or countryCode == '') and countryName != null and countryName != ''">
and l.group_id in (
select g.group_id from pay_postcode_group g
where (g.country_code is null or trim(g.country_code) = '') and g.country_name = #{countryName}
)
</if>
</where>
order by l.sort_no asc, l.line_id asc
</select>
<insert id="insertGroup" parameterType="com.bms.price.domain.PayPostcodeGroup" useGeneratedKeys="true" keyProperty="groupId">
@@ -134,6 +408,30 @@
</foreach>
</select>
<select id="selectRemoteGroupsByCountry" resultType="com.bms.price.domain.PayPostcodeGroup">
select group_id as groupId, block_title as blockTitle, channel_name as channelName,
product_name as productName, quote_sheet as quoteSheet, sheet_name as sheetName,
config_kind as configKind, coverage, fee_name as feeName, fee_text as feeText,
country_name as countryName, country_code as countryCode
from pay_postcode_group
where (
upper(ifnull(coverage, '')) = 'REMOTE'
or ifnull(fee_name, '') like '%偏远%'
or lower(ifnull(fee_name, '')) like '%remote%'
)
<choose>
<when test="countryCode != null and countryCode != ''">
and upper(trim(country_code)) = upper(trim(#{countryCode}))
</when>
<when test="countryName != null and countryName != ''">
and trim(country_name) = trim(#{countryName})
</when>
<otherwise>
and 1 = 0
</otherwise>
</choose>
</select>
<select id="selectGroupsByTokens" resultType="com.bms.price.domain.PayPostcodeGroup">
select group_id as groupId, block_title as blockTitle, channel_name as channelName,
product_name as productName, quote_sheet as quoteSheet, sheet_name as sheetName,
@@ -6,6 +6,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<resultMap type="PrcPayBillFee" id="PrcPayBillFeeResult">
<id property="feeId" column="fee_id" />
<result property="id" column="id" />
<result property="billId" column="bill_id" />
<result property="payBillId" column="pay_bill_id" />
<result property="feeType" column="fee_type" />
@@ -23,7 +24,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap>
<select id="selectByBillId" parameterType="String" resultMap="PrcPayBillFeeResult">
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>
<update id="fillSourceIdByFeeIds">
update prc_pay_bill_fee
set source_id = id
where (source_id is null or source_id = '')
and fee_id in
<foreach collection="feeIds" item="feeId" open="(" separator="," close=")">#{feeId}</foreach>
</update>
<delete id="deleteByBillId" parameterType="String">
delete from prc_pay_bill_fee
where bill_id = #{billId}
+36
View File
@@ -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' })
}
@@ -0,0 +1,124 @@
<template>
<el-drawer v-model="visible" size="760px" append-to-body>
<template #header>
<div class="drawer-head">
<span class="drawer-title">邮编明细</span>
<span v-if="zone" class="drawer-sub">{{ zone.zoneName }}({{ total }} 条)</span>
</div>
</template>
<div class="drawer-toolbar">
<el-input v-model="query.postcode" clearable placeholder="搜索邮编" style="width: 200px" @keyup.enter="loadList" />
<el-button type="primary" plain icon="Search" size="small" @click="loadList">搜索</el-button>
<el-button type="primary" plain icon="Plus" size="small" @click="handleAdd" v-hasPermi="['data:payPostcode:edit']">新增邮编</el-button>
</div>
<el-table v-loading="loading" :data="list" border row-key="lineId" empty-text="暂无邮编">
<el-table-column label="地段" prop="cityName" min-width="140" show-overflow-tooltip>
<template #default="{ row }">{{ row.cityName || row.regionName || '-' }}</template>
</el-table-column>
<el-table-column label="邮编" prop="postcode" min-width="140" show-overflow-tooltip />
<el-table-column label="截止邮编" prop="postcodeEnd" width="140" show-overflow-tooltip />
<el-table-column label="操作" width="160" align="center">
<template #default="{ row }">
<el-button link type="primary" icon="Edit" @click="handleEdit(row)" v-hasPermi="['data:payPostcode:edit']">编辑</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(row)" v-hasPermi="['data:payPostcode:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="drawer-pager">
<el-pagination
v-model:current-page="query.pageNum"
v-model:page-size="query.pageSize"
:total="total"
:page-sizes="[20, 50, 100, 200]"
layout="total, sizes, prev, pager, next"
@size-change="loadList"
@current-change="loadList"
/>
</div>
<pay-postcode-form-dialog ref="formRef" @success="onChanged" />
</el-drawer>
</template>
<script setup name="PayPostcodeDrawer">
import { listPayPostcodeLine, delPayPostcodeLine } from '@/api/data/payPostcode'
import PayPostcodeFormDialog from './PayPostcodeFormDialog.vue'
const { proxy } = getCurrentInstance()
const emit = defineEmits(['changed'])
const visible = ref(false)
const loading = ref(false)
const country = ref(null)
const zone = ref(null)
const list = ref([])
const total = ref(0)
const query = ref({ pageNum: 1, pageSize: 50, postcode: '' })
const formRef = ref()
function open(countryRow, zoneRow) {
country.value = countryRow
zone.value = zoneRow
query.value = { pageNum: 1, pageSize: 50, postcode: '' }
visible.value = true
loadList()
}
function loadList() {
if (!zone.value) {
list.value = []
total.value = 0
return
}
loading.value = true
listPayPostcodeLine({
countryCode: country.value?.countryCode,
countryName: country.value?.countryName,
zoneName: zone.value.zoneName,
postcode: query.value.postcode || undefined,
pageNum: query.value.pageNum,
pageSize: query.value.pageSize
}).then(res => {
list.value = res.rows || []
total.value = res.total || 0
}).catch(() => {
list.value = []
total.value = 0
}).finally(() => {
loading.value = false
})
}
function onChanged() {
loadList()
emit('changed')
}
function handleAdd() {
formRef.value.open(country.value, zone.value)
}
function handleEdit(row) {
formRef.value.open(country.value, zone.value, row)
}
function handleDelete(row) {
proxy.$modal.confirm('删除邮编「' + row.postcode + '」?').then(() => delPayPostcodeLine(row.lineId)).then(() => {
proxy.$modal.msgSuccess('删除成功')
onChanged()
}).catch(() => {})
}
defineExpose({ open })
</script>
<style scoped>
.drawer-head { display: flex; align-items: center; gap: 10px; }
.drawer-title { font-size: 16px; font-weight: 600; }
.drawer-sub { color: #909399; font-size: 13px; }
.drawer-toolbar { margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
.drawer-pager { margin-top: 16px; display: flex; justify-content: flex-end; }
</style>
@@ -0,0 +1,78 @@
<template>
<el-dialog :title="title" v-model="visible" width="460px" append-to-body>
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px">
<el-form-item label="分区">
<span>{{ zone?.zoneName || '-' }}</span>
</el-form-item>
<el-form-item label="地段" prop="cityName">
<el-input v-model="form.cityName" maxlength="200" placeholder="例如罗马,可空" />
</el-form-item>
<el-form-item label="邮编" prop="postcode">
<el-input v-model="form.postcode" maxlength="200" placeholder="邮编或前缀" />
</el-form-item>
<el-form-item label="截止邮编" prop="postcodeEnd">
<el-input v-model="form.postcodeEnd" maxlength="200" placeholder="区间结束,可空" />
</el-form-item>
</el-form>
<template #footer>
<el-button type="primary" @click="submit">确定</el-button>
<el-button @click="visible = false">取消</el-button>
</template>
</el-dialog>
</template>
<script setup name="PayPostcodeFormDialog">
import { addPayPostcodeLine, updatePayPostcodeLine } from '@/api/data/payPostcode'
const { proxy } = getCurrentInstance()
const emit = defineEmits(['success'])
const visible = ref(false)
const title = ref('')
const formRef = ref()
const form = ref({})
const zone = ref(null)
const rules = {
postcode: [{ required: true, message: '请填写邮编', trigger: 'blur' }]
}
function open(country, zoneRow, row) {
zone.value = zoneRow
form.value = row
? {
lineId: row.lineId,
countryCode: country?.countryCode,
countryName: country?.countryName,
zoneName: zoneRow?.zoneName,
cityName: row.cityName || row.regionName || '',
postcode: row.postcode,
postcodeEnd: row.postcodeEnd || ''
}
: {
countryCode: country?.countryCode,
countryName: country?.countryName,
zoneName: zoneRow?.zoneName,
cityName: '',
postcode: '',
postcodeEnd: ''
}
title.value = row ? '编辑邮编' : '新增邮编'
visible.value = true
nextTick(() => formRef.value?.clearValidate())
}
function submit() {
formRef.value.validate(valid => {
if (!valid) return
const payload = { ...form.value, postcode: String(form.value.postcode || '').trim() }
const req = payload.lineId ? updatePayPostcodeLine(payload) : addPayPostcodeLine(payload)
req.then(() => {
proxy.$modal.msgSuccess(payload.lineId ? '修改成功' : '新增成功')
visible.value = false
emit('success')
})
})
}
defineExpose({ open })
</script>
@@ -0,0 +1,146 @@
<template>
<el-drawer v-model="visible" size="860px" append-to-body>
<template #header>
<div class="drawer-head">
<span class="drawer-title">国家分区</span>
<span v-if="country" class="drawer-sub">
{{ country.countryName || '未填写国家' }}
<template v-if="country.countryCode">({{ country.countryCode }})</template>
,共 {{ zoneList.length }} 个分区
</span>
</div>
</template>
<div v-loading="loading">
<div class="zone-toolbar">
<el-button type="primary" plain icon="Plus" size="small" :disabled="!country" @click="handleAdd" v-hasPermi="['data:payPostcode:edit']">新增分区</el-button>
</div>
<el-table :data="zoneList" border row-key="zoneName" :empty-text="country ? '这个国家还没有分区' : '请选择国家'">
<el-table-column label="分区" min-width="180" align="center">
<template #default="{ row }">
<div class="zone-country-cell">
<div class="zone-title">{{ row.zoneName }}</div>
<el-button link type="primary" class="zone-click" @click="openLines(row)" v-hasPermi="['data:payPostcode:query']">分区点击</el-button>
</div>
</template>
</el-table-column>
<el-table-column label="邮编属性" width="120" align="center">
<template #default="{ row }">
<el-tag v-if="row.coverage" size="small" :type="coverageType(firstPart(row.coverage))">{{ coverageLabel(firstPart(row.coverage)) }}</el-tag>
<el-tag v-else size="small" type="info">普通</el-tag>
</template>
</el-table-column>
<el-table-column label="邮编数" prop="lineCount" width="100" align="center" />
<el-table-column label="操作" width="220" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" icon="View" @click="openLines(row)" v-hasPermi="['data:payPostcode:query']">明细</el-button>
<el-button link type="primary" icon="Edit" @click="handleEdit(row)" v-hasPermi="['data:payPostcode:edit']">编辑</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(row)" v-hasPermi="['data:payPostcode:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<pay-zone-form-dialog ref="formRef" :country="country" @success="onChanged" />
<pay-postcode-drawer ref="lineRef" @changed="onChanged" />
</el-drawer>
</template>
<script setup name="PayZoneDrawer">
import { listPayPostcodeZone, delPayPostcodeZone } from '@/api/data/payPostcode'
import PayZoneFormDialog from './PayZoneFormDialog.vue'
import PayPostcodeDrawer from './PayPostcodeDrawer.vue'
const { proxy } = getCurrentInstance()
const emit = defineEmits(['changed'])
const visible = ref(false)
const loading = ref(false)
const country = ref(null)
const zoneList = ref([])
const formRef = ref()
const lineRef = ref()
const coverageOptions = [
{ label: '可达', value: 'REACH' },
{ label: '不可达', value: 'UNREACH' },
{ label: '偏远', value: 'REMOTE' }
]
function firstPart(value) {
return String(value || '').split('、').map(item => item.trim()).filter(Boolean)[0] || ''
}
function coverageLabel(value) {
return coverageOptions.find(item => item.value === value)?.label || value
}
function coverageType(value) {
if (value === 'UNREACH') return 'danger'
if (value === 'REMOTE') return 'warning'
return 'success'
}
function open(row) {
country.value = row
visible.value = true
loadList()
}
function loadList() {
if (!country.value) {
zoneList.value = []
return
}
loading.value = true
listPayPostcodeZone({ countryCode: country.value.countryCode, countryName: country.value.countryName }).then(res => {
zoneList.value = res.data || []
}).catch(() => {
zoneList.value = []
}).finally(() => {
loading.value = false
})
}
function onChanged() {
loadList()
emit('changed')
}
function handleAdd() {
formRef.value.open()
}
function handleEdit(row) {
formRef.value.open(row)
}
function handleDelete(row) {
proxy.$modal.confirm('删除分区「' + row.zoneName + '」及其邮编?').then(() => {
return delPayPostcodeZone({
countryCode: country.value.countryCode,
countryName: country.value.countryName,
zoneName: row.zoneName
})
}).then(() => {
proxy.$modal.msgSuccess('删除成功')
onChanged()
}).catch(() => {})
}
function openLines(row) {
lineRef.value.open(country.value, row)
}
defineExpose({ open })
</script>
<style scoped>
.drawer-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.drawer-title { font-size: 16px; font-weight: 600; }
.drawer-sub { color: #909399; font-size: 13px; }
.zone-toolbar { display: flex; align-items: center; margin-bottom: 12px; }
.zone-country-cell { display: flex; flex-direction: column; align-items: center; gap: 2px; padding: 4px 0; }
.zone-title { font-weight: 500; }
.zone-click { padding: 0; height: auto; font-size: 12px; }
</style>
@@ -0,0 +1,83 @@
<template>
<el-dialog :title="title" v-model="visible" width="480px" append-to-body>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="国家">
<span>{{ country?.countryName || '-' }}<template v-if="country?.countryCode">({{ country.countryCode }})</template></span>
</el-form-item>
<el-form-item label="分区" prop="zoneName">
<el-input v-model="form.zoneName" maxlength="200" placeholder="例如 mainland" />
</el-form-item>
<el-form-item label="邮编属性" prop="coverage">
<el-select v-model="form.coverage" clearable placeholder="普通" style="width: 100%">
<el-option v-for="item in coverageOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button type="primary" @click="submit">确定</el-button>
<el-button @click="visible = false">取消</el-button>
</template>
</el-dialog>
</template>
<script setup name="PayZoneFormDialog">
import { addPayPostcodeZone, updatePayPostcodeZone } from '@/api/data/payPostcode'
const { proxy } = getCurrentInstance()
const props = defineProps({
country: { type: Object, default: null }
})
const emit = defineEmits(['success'])
const visible = ref(false)
const title = ref('')
const formRef = ref()
const form = ref({})
const rules = {
zoneName: [{ required: true, message: '请填写分区', trigger: 'blur' }]
}
const coverageOptions = [
{ label: '可达', value: 'REACH' },
{ label: '不可达', value: 'UNREACH' },
{ label: '偏远', value: 'REMOTE' }
]
function open(row) {
form.value = row
? {
countryCode: props.country?.countryCode,
countryName: props.country?.countryName,
oldZoneName: row.zoneName,
zoneName: row.zoneName,
coverage: String(row.coverage || '').split('、')[0] || ''
}
: {
countryCode: props.country?.countryCode,
countryName: props.country?.countryName,
zoneName: '',
coverage: ''
}
title.value = row ? '编辑分区' : '新增分区'
visible.value = true
nextTick(() => formRef.value?.clearValidate())
}
function submit() {
formRef.value.validate(valid => {
if (!valid) return
const payload = {
...form.value,
zoneName: String(form.value.zoneName || '').trim(),
coverage: form.value.coverage || ''
}
const req = payload.oldZoneName ? updatePayPostcodeZone(payload) : addPayPostcodeZone(payload)
req.then(() => {
proxy.$modal.msgSuccess(payload.oldZoneName ? '修改成功' : '新增成功')
visible.value = false
emit('success')
})
})
}
defineExpose({ open })
</script>
+84 -166
View File
@@ -1,40 +1,41 @@
<template>
<div class="app-container">
<el-alert
type="info"
:closable="false"
show-icon
class="mb16"
title="每个国家一行。点「分区」维护分区和邮编。偏远分区按目的国家参与应付偏远附加费;某个产品的报价表自己已经挂了偏远名单时,仍只用那一份。"
/>
<basic-table
ref="tableRef"
:columns="columns"
:search-columns="searchColumns"
:query="queryList"
row-key="groupId"
selection
search-action-align="right"
>
<template #toolbar="{ ids, selection }">
<template #toolbar>
<el-col :span="1.5">
<el-button type="primary" plain icon="Upload" @click="open = true" v-hasPermi="['data:payPostcode:import']">导入</el-button>
<el-button type="primary" plain icon="Upload" @click="open = true" v-hasPermi="['data:payPostcode:import']">导入邮编</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="selection.length !== 1" @click="handleUpdate(selection[0])" v-hasPermi="['data:payPostcode:edit']">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="!ids.length" @click="handleDelete(ids)" v-hasPermi="['data:payPostcode:remove']">删除</el-button>
</el-col>
</template>
<template #col-configKind="{ row }">
<el-tag size="small" :type="row.configKind === 'FEE' ? 'warning' : 'success'">{{ row.configKind === 'FEE' ? '附加费' : '邮编分区' }}</el-tag>
</template>
<template #col-coverage="{ row }">
<el-tag v-if="row.coverage" size="small" :type="coverageType(row.coverage)">{{ coverageLabel(row.coverage) }}</el-tag>
<template v-if="coverageParts(row.coverage).length">
<el-tag v-for="item in coverageParts(row.coverage)" :key="item" size="small" class="cov-tag" :type="coverageType(item)">{{ coverageLabel(item) }}</el-tag>
</template>
<el-tag v-else size="small" type="info">普通</el-tag>
</template>
<template #col-action="{ row }">
<el-button link type="primary" @click="openLines(row)" v-hasPermi="['data:payPostcode:query']">邮编</el-button>
<el-button link type="primary" icon="Edit" @click="handleUpdate(row)" v-hasPermi="['data:payPostcode:edit']">修改</el-button>
<el-button link type="danger" @click="handleDelete([row.groupId])" v-hasPermi="['data:payPostcode:remove']">删除</el-button>
<el-button link type="primary" icon="List" @click.stop="openZones(row)" v-hasPermi="['data:payPostcode:query']">分区</el-button>
<el-button link type="primary" icon="Edit" @click.stop="handleUpdate(row)" v-hasPermi="['data:payPostcode:edit']">修改</el-button>
<el-button link type="danger" icon="Delete" @click.stop="handleClear(row)" v-hasPermi="['data:payPostcode:remove']">清空</el-button>
</template>
</basic-table>
<el-dialog v-model="open" title="导入应付邮编" width="480px" append-to-body destroy-on-close>
<p class="import-tip">表头里带邮编或附加费的工作表会被导入。目录里的分区表会带上服务类型、国家、销售产品和报价表。可达、可派记为可达,不可达记为不可达,偏远和岛屿记为偏远。渠道名称从标题里识别,例如 Royal Mail、EVRI。邮编末尾的 * 会去掉,同一国家、同一渠道、同一附加费、金额相同只保留一条。</p>
<p class="import-tip">导入后按国家归成一行。表头里带邮编或附加费的工作表会被导入。可达、可派记为可达,不可达记为不可达,偏远和岛屿记为偏远。</p>
<el-upload :auto-upload="false" :limit="1" accept=".xlsx" :on-change="onFile" :on-remove="() => file = null">
<el-button type="primary" plain>选择 xlsx</el-button>
</el-upload>
@@ -44,105 +45,44 @@
</template>
</el-dialog>
<el-dialog title="修改应付邮编" v-model="editOpen" width="560px" append-to-body destroy-on-close>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="服务类型" prop="serviceType">
<el-input v-model="form.serviceType" maxlength="100" placeholder="目录中的服务类型" />
<el-dialog title="修改国家邮编" v-model="editOpen" width="520px" append-to-body destroy-on-close>
<el-form ref="formRef" :model="form" label-width="100px">
<el-form-item label="国家">
<span>{{ form.countryName || '-' }}<template v-if="form.countryCode">({{ form.countryCode }})</template></span>
</el-form-item>
<el-form-item label="目录国家" prop="catalogCountry">
<el-input v-model="form.catalogCountry" maxlength="100" placeholder="目录中的国家" />
<el-form-item label="服务类型">
<el-input v-model="form.serviceType" maxlength="100" />
</el-form-item>
<el-form-item label="销售产品" prop="productName">
<el-input v-model="form.productName" maxlength="200" placeholder="目录中的销售产品" />
<el-form-item label="销售产品">
<el-input v-model="form.productName" maxlength="200" />
</el-form-item>
<el-form-item label="报价表" prop="quoteSheet">
<el-input v-model="form.quoteSheet" maxlength="200" placeholder="目录中的报价工作表" />
</el-form-item>
<el-form-item label="渠道名称" prop="channelName">
<el-form-item label="渠道名称">
<el-input v-model="form.channelName" maxlength="100" placeholder="如 Royal Mail、EVRI" />
</el-form-item>
<el-form-item label="类型" prop="configKind">
<el-select v-model="form.configKind" style="width: 100%">
<el-option v-for="item in kindOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="邮编属性" prop="coverage">
<el-select v-model="form.coverage" clearable placeholder="普通分区" style="width: 100%">
<el-option v-for="item in coverageOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="国家" prop="countryName">
<el-input v-model="form.countryName" maxlength="100" />
</el-form-item>
<el-form-item label="国家二字码" prop="countryCode">
<el-input v-model="form.countryCode" maxlength="20" />
</el-form-item>
<el-form-item label="附加费" prop="feeName">
<el-input v-model="form.feeName" maxlength="200" />
</el-form-item>
<el-form-item label="金额原文" prop="feeText">
<el-input v-model="form.feeText" maxlength="200" />
</el-form-item>
<el-form-item label="区块标题" prop="blockTitle">
<el-input v-model="form.blockTitle" maxlength="200" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="2" maxlength="500" />
</el-form-item>
</el-form>
<p class="import-tip">保存后,这个国家下的分区都使用这里的服务类型、销售产品和渠道。报价表保持不变。偏远附加费按目的国家的偏远邮编匹配,再用价卡上的分区或邮编前缀选定费用档。</p>
<template #footer>
<el-button type="primary" @click="submitEdit">确定</el-button>
<el-button @click="editOpen = false">取消</el-button>
</template>
</el-dialog>
<el-drawer v-model="lineOpen" :title="lineTitle" size="72%" append-to-body>
<el-input v-model="lineQuery.postcode" clearable placeholder="筛选邮编" class="line-filter" @change="loadLines" />
<el-table :data="lineRows" border size="small" max-height="640" empty-text="没有邮编">
<el-table-column prop="countryName" label="国家" width="90" show-overflow-tooltip />
<el-table-column prop="regionName" label="地区" min-width="140" show-overflow-tooltip />
<el-table-column prop="regionNameCn" label="地区中文" min-width="120" show-overflow-tooltip />
<el-table-column prop="cityName" label="城市" min-width="120" show-overflow-tooltip />
<el-table-column prop="regionCode" label="地区代码" width="100" show-overflow-tooltip />
<el-table-column prop="zoneName" label="分区" min-width="140" show-overflow-tooltip />
<el-table-column prop="postcode" label="邮编" min-width="120" show-overflow-tooltip />
<el-table-column prop="postcodeRaw" label="邮编原文" min-width="120" show-overflow-tooltip />
<el-table-column prop="postcodeEnd" label="截止邮编" width="120" show-overflow-tooltip />
<el-table-column prop="extraText" label="其余列" min-width="180" show-overflow-tooltip />
</el-table>
<el-pagination
v-if="lineTotal > lineQuery.pageSize"
class="line-page"
layout="total, prev, pager, next"
:total="lineTotal"
:page-size="lineQuery.pageSize"
v-model:current-page="lineQuery.pageNum"
@current-change="loadLines"
/>
</el-drawer>
<pay-zone-drawer ref="zoneDrawerRef" @changed="reload" />
</div>
</template>
<script setup name="PayPostcode">
import { listPayPostcode, getPayPostcode, updatePayPostcode, listPayPostcodeLine, importPayPostcode, delPayPostcode } from '@/api/data/payPostcode'
import { listPayPostcode, importPayPostcode, updatePayPostcodeCountry, delPayPostcodeCountry } from '@/api/data/payPostcode'
import PayZoneDrawer from './components/PayZoneDrawer.vue'
const { proxy } = getCurrentInstance()
const tableRef = ref()
const formRef = ref()
const zoneDrawerRef = ref()
const open = ref(false)
const editOpen = ref(false)
const importing = ref(false)
const file = ref(null)
const lineOpen = ref(false)
const lineTitle = ref('邮编')
const lineRows = ref([])
const lineTotal = ref(0)
const lineQuery = reactive({ groupId: undefined, postcode: '', pageNum: 1, pageSize: 20 })
const kindOptions = [
{ label: '附加费', value: 'FEE' },
{ label: '邮编分区', value: 'ZONE' }
]
const form = ref({})
const coverageOptions = [
{ label: '可达', value: 'REACH' },
@@ -150,8 +90,12 @@ const coverageOptions = [
{ label: '偏远', value: 'REMOTE' }
]
function coverageParts(value) {
return String(value || '').split('、').map(item => item.trim()).filter(Boolean)
}
function coverageLabel(value) {
return coverageOptions.find(item => item.value === value)?.label || ''
return coverageOptions.find(item => item.value === value)?.label || value
}
function coverageType(value) {
@@ -160,47 +104,39 @@ function coverageType(value) {
return 'success'
}
function firstPart(value) {
return coverageParts(value)[0] || ''
}
const searchColumns = [
{ label: '销售产品', prop: 'productName', component: 'input', placeholder: '销售产品', width: '140px' },
{ label: '报价表', prop: 'quoteSheet', component: 'input', placeholder: '报价表', width: '160px' },
{ label: '来源文件', prop: 'sourceFile', component: 'input', placeholder: '文件名', width: '180px' },
{ label: '工作表', prop: 'sheetName', component: 'input', placeholder: '工作表', width: '160px' },
{ label: '国家', prop: 'countryName', component: 'input', placeholder: '国家或二字码', width: '140px' },
{ label: '国家', prop: 'countryName', component: 'input', placeholder: '国家或二字码', width: '160px' },
{ label: '服务类型', prop: 'serviceType', component: 'input', placeholder: '服务类型', width: '140px' },
{ label: '销售产品', prop: 'productName', component: 'input', placeholder: '销售产品', width: '160px' },
{ label: '渠道', prop: 'channelName', component: 'input', placeholder: '渠道名称', width: '140px' },
{ label: '类型', prop: 'configKind', component: 'select', options: kindOptions, placeholder: '全部', width: '130px', props: { clearable: true } },
{ label: '邮编属性', prop: 'coverage', component: 'select', options: coverageOptions, placeholder: '全部', width: '130px', props: { clearable: true } },
{ label: '附加费', prop: 'feeName', component: 'input', placeholder: '附加费名称', width: '160px' },
{ label: '邮编', prop: 'postcode', component: 'input', placeholder: '邮编', width: '140px' }
]
const columns = [
{ label: '类型', prop: 'configKind', width: 100, align: 'center', slot: 'configKind' },
{ label: '邮编属性', prop: 'coverage', width: 100, align: 'center', slot: 'coverage' },
{ label: '服务类型', prop: 'serviceType', width: 100, showOverflowTooltip: true },
{ label: '目录国家', prop: 'catalogCountry', width: 100, showOverflowTooltip: true },
{ label: '销售产品', prop: 'productName', minWidth: 120, showOverflowTooltip: true },
{ label: '报价表', prop: 'quoteSheet', minWidth: 140, showOverflowTooltip: true },
{ label: '渠道名称', prop: 'channelName', minWidth: 120, showOverflowTooltip: true },
{ label: '国家', prop: 'countryName', width: 90, showOverflowTooltip: true },
{ label: '工作表', prop: 'sheetName', minWidth: 140, showOverflowTooltip: true },
{ label: '区块', prop: 'blockTitle', minWidth: 180, showOverflowTooltip: true },
{ label: '附加费', prop: 'feeName', minWidth: 140, showOverflowTooltip: true },
{ label: '金额原文', prop: 'feeText', minWidth: 140, showOverflowTooltip: true },
{ label: '邮编行数', prop: 'lineCount', width: 100, align: 'right' },
{ label: '来源文件', prop: 'sourceFile', minWidth: 220, showOverflowTooltip: true },
{ label: '国家', prop: 'countryName', minWidth: 120, showOverflowTooltip: true },
{ label: '邮编属性', prop: 'coverage', minWidth: 140, align: 'center', slot: 'coverage' },
{ label: '服务类型', prop: 'serviceType', minWidth: 120, showOverflowTooltip: true },
{ label: '销售产品', prop: 'productName', minWidth: 140, showOverflowTooltip: true },
{ label: '渠道名称', prop: 'channelName', minWidth: 140, showOverflowTooltip: true },
{ label: '分区数', prop: 'zoneCount', width: 90, align: 'center' },
{ label: '邮编总数', prop: 'lineCount', width: 100, align: 'center' },
{ label: '导入时间', prop: 'createTime', width: 170, align: 'center' },
{ label: '操作', prop: 'action', width: 170, align: 'center', fixed: 'right', slot: 'action', showInToolbar: false }
{ label: '操作', prop: 'action', width: 200, align: 'center', fixed: 'right', slot: 'action', showInToolbar: false }
]
const form = ref({})
const rules = {
configKind: [{ required: true, message: '请选择类型', trigger: 'change' }]
}
function queryList(params) {
return listPayPostcode(params)
}
function reload() {
tableRef.value?.refresh?.() || tableRef.value?.reload?.()
}
function onFile(uploadFile) {
file.value = uploadFile.raw
}
@@ -215,66 +151,48 @@ function submitImport() {
proxy.$modal.msgSuccess(res.data?.message || res.msg || '导入完成')
open.value = false
file.value = null
tableRef.value?.reload?.() || tableRef.value?.getList?.()
reload()
}).finally(() => {
importing.value = false
})
}
function openZones(row) {
zoneDrawerRef.value.open(row)
}
function handleUpdate(row) {
getPayPostcode(row.groupId).then(res => {
form.value = Object.assign({}, res.data || {})
editOpen.value = true
})
form.value = {
countryCode: row.countryCode,
countryName: row.countryName,
serviceType: firstPart(row.serviceType),
productName: firstPart(row.productName),
channelName: firstPart(row.channelName)
}
editOpen.value = true
}
function submitEdit() {
formRef.value.validate(valid => {
if (!valid) return
updatePayPostcode(form.value).then(() => {
proxy.$modal.msgSuccess('保存成功')
editOpen.value = false
tableRef.value?.reload?.()
})
updatePayPostcodeCountry(form.value).then(() => {
proxy.$modal.msgSuccess('保存成功')
editOpen.value = false
reload()
})
}
function openLines(row) {
lineTitle.value = `${coverageLabel(row.coverage)} ${row.countryName || ''} ${row.feeName || row.blockTitle || row.sheetName}`.trim()
lineQuery.groupId = row.groupId
lineQuery.postcode = ''
lineQuery.pageNum = 1
lineOpen.value = true
loadLines()
}
function loadLines() {
listPayPostcodeLine(lineQuery).then(res => {
lineRows.value = res.rows || []
lineTotal.value = res.total || 0
})
}
function handleDelete(ids) {
proxy.$modal.confirm('确认删除选中的邮编配置吗?邮编明细会一起删除。').then(() => delPayPostcode(ids)).then(() => {
proxy.$modal.msgSuccess('删除成功')
tableRef.value?.reload?.() || tableRef.value?.getList?.()
function handleClear(row) {
const name = row.countryName || row.countryCode
proxy.$modal.confirm('清空「' + name + '」下的全部分区和邮编?计费将不再使用这些邮编。').then(() => {
return delPayPostcodeCountry({ countryCode: row.countryCode, countryName: row.countryName })
}).then(() => {
proxy.$modal.msgSuccess('已清空')
reload()
}).catch(() => {})
}
</script>
<style scoped>
.import-tip {
margin: 0 0 12px;
color: var(--el-text-color-secondary);
line-height: 1.5;
}
.line-filter {
width: 240px;
margin-bottom: 8px;
}
.line-page {
margin-top: 8px;
justify-content: flex-end;
}
.mb16 { margin-bottom: 16px; }
.import-tip { margin: 0 0 12px; color: var(--el-text-color-secondary); line-height: 1.5; }
.cov-tag { margin: 0 4px 4px 0; }
</style>
@@ -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 })
</script>
@@ -281,10 +281,12 @@
<script setup name="PayBill">
import { getToken } from '@/utils/auth'
import useUserStore from '@/store/modules/user'
import { listSupplier } from '@/api/customersuppliermanagement/supplierinfo/supplier'
import { listPayBill, getPayBill, previewPayBill, generatePayBill, voidPayBill, submitPayBill, approvePayBill, rejectPayBill, savePayBillDiff, acceptPayBillDiff, removePayBillLine, savePayBillFile, deletePayBillFile, redPayBill, savePayFinance, exportPayStatement, importPaySupplier } from '@/api/price/payBill'
const { proxy } = getCurrentInstance()
const userStore = useUserStore()
const loading = ref(false)
const rows = ref([])
const total = ref(0)
@@ -480,7 +482,8 @@ function submitRow(row) {
}
function approveRow(row) {
proxy.$modal.confirm('审核通过?蓝字需要发票价税合计、税额和账单一致,审核人不能是提交人。').then(() => approvePayBill(row.billId)).then(() => {
const limit = String(userStore.id) === '1' ? '' : ',审核人不能是提交人'
proxy.$modal.confirm('审核通过?蓝字需要发票价税合计、税额和账单一致' + limit + '。').then(() => approvePayBill(row.billId)).then(() => {
proxy.$modal.msgSuccess('已审核通过')
loadList()
if (detail.value?.billId === row.billId) reloadDetail()
@@ -558,6 +561,11 @@ function onFileUploaded(res) {
fileUrl: res.url || res.fileName
}).then(() => {
proxy.$modal.msgSuccess('凭证已上传')
fileForm.fileType = 'INVOICE'
fileForm.invoiceNo = ''
fileForm.amount = null
fileForm.taxAmount = 0
fileForm.sellerName = ''
reloadDetail()
})
}
@@ -38,8 +38,8 @@
<el-button link type="primary" @click="openDetail(row)">查看</el-button>
<el-button v-if="row.status === 'DRAFT'" link type="primary" @click="confirmRow(row)" v-hasPermi="['price:payPayment:confirm']">确认付款</el-button>
<el-button v-if="row.status === 'DRAFT'" link type="danger" @click="voidRow(row)" v-hasPermi="['price:payPayment:void']">作废</el-button>
<el-button v-if="row.status === 'PAID'" link type="warning" @click="reverseRow(row)" v-hasPermi="['price:payPayment:confirm']">反核销</el-button>
<el-button v-if="row.status === 'PAID'" link type="danger" @click="refundRow(row)" v-hasPermi="['price:payPayment:confirm']">退款</el-button>
<el-button v-if="canAct(row)" link type="warning" @click="openAct(row, 'reverse')" v-hasPermi="['price:payPayment:confirm']">反核销</el-button>
<el-button v-if="canAct(row)" link type="danger" @click="openAct(row, 'refund')" v-hasPermi="['price:payPayment:confirm']">退款</el-button>
</template>
</el-table-column>
</el-table>
@@ -119,14 +119,55 @@
</el-table>
</template>
</el-drawer>
<el-dialog v-model="actVisible" :title="actTitle" width="920px" append-to-body>
<div v-loading="actLoading">
<template v-if="actPayment">
<el-descriptions :column="3" border size="small" class="mb8">
<el-descriptions-item label="付款单号">{{ actPayment.paymentNo }}</el-descriptions-item>
<el-descriptions-item label="供应商">{{ actPayment.supplierName }}</el-descriptions-item>
<el-descriptions-item label="币种">{{ actPayment.currency }}</el-descriptions-item>
<el-descriptions-item label="付款金额">{{ actPayment.amount }}</el-descriptions-item>
<el-descriptions-item label="已核销">{{ actPayment.allocAmount || 0 }}</el-descriptions-item>
<el-descriptions-item label="手续费">{{ actPayment.bankFee }}</el-descriptions-item>
<el-descriptions-item label="付款日">{{ actPayment.payDate }}</el-descriptions-item>
<el-descriptions-item label="方式">{{ methodText(actPayment.payMethod) }}</el-descriptions-item>
<el-descriptions-item label="账户">{{ actPayment.bankAccount || '-' }}</el-descriptions-item>
</el-descriptions>
<div class="mb8">核销账单</div>
<el-table :data="actAllocs" border size="small" class="mb8" empty-text="没有仍有效的核销">
<el-table-column prop="billNo" label="账单号" min-width="160" />
<el-table-column prop="allocAmount" label="核销金额" width="120" align="right" />
<el-table-column prop="currency" label="币种" width="80" align="center" />
</el-table>
<div class="mb8">费用明细</div>
<el-table :data="actFees" border size="small" max-height="280" empty-text="没有费用明细">
<el-table-column prop="billNo" label="账单号" min-width="150" />
<el-table-column prop="orderNo" label="订单号" min-width="140" />
<el-table-column prop="ladingNo" label="提单号" min-width="120" />
<el-table-column prop="feeName" label="费用" min-width="120" />
<el-table-column prop="amount" label="金额" width="110" align="right" />
<el-table-column prop="currency" label="币种" width="80" align="center" />
</el-table>
<div class="act-hint">{{ actHint }}</div>
</template>
</div>
<template #footer>
<el-button @click="actVisible = false">取消</el-button>
<el-button :type="actType === 'refund' ? 'danger' : 'warning'" :loading="actSaving" :disabled="!actPayment" @click="runAct">确认</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup name="PayPayment">
import useUserStore from '@/store/modules/user'
import { listSupplier } from '@/api/customersuppliermanagement/supplierinfo/supplier'
import { listPayPayment, getPayPayment, openPayBills, createPayPayment, confirmPayPayment, voidPayPayment, reversePayPayment, refundPayPayment } from '@/api/price/payPayment'
import { getPayBill } from '@/api/price/payBill'
const { proxy } = getCurrentInstance()
const userStore = useUserStore()
const { bms_currency } = useDict('bms_currency')
const loading = ref(false)
const saving = ref(false)
@@ -145,8 +186,23 @@ const openBills = ref([])
const form = reactive({ supplierId: null, currency: 'CNY', payDate: '', payMethod: 'BANK', bankAccount: '', amount: null, bankFee: 0, exchangeRate: 1 })
const detailVisible = ref(false)
const detail = ref(null)
const actVisible = ref(false)
const actLoading = ref(false)
const actSaving = ref(false)
const actType = ref('reverse')
const actPayment = ref(null)
const actFees = ref([])
const allocTotal = computed(() => openBills.value.reduce((sum, item) => sum + (Number(item.allocAmount) || 0), 0).toFixed(2))
const actTitle = computed(() => actType.value === 'refund' ? '应付退款' : '应付反核销')
const actAllocs = computed(() => (actPayment.value?.allocs || []).filter(item => item.allocStatus !== 'REVERSED'))
const actHint = computed(() => {
const admin = String(userStore.id) === '1'
if (actType.value === 'refund') {
return admin ? '确认后这些核销会冲回,付款单变为已退款。' : '确认后这些核销会冲回,付款单变为已退款。确认人不能自己退款。'
}
return admin ? '确认后这些核销会冲回,账单未付金额会回来。' : '确认后这些核销会冲回,账单未付金额会回来。确认人不能自己反核销。'
})
function statusText(value) {
return statusOptions.find(item => item.value === value)?.label || value || '-'
@@ -156,6 +212,10 @@ function methodText(value) {
return { BANK: '银行转账', CASH: '现金', OTHER: '其他' }[value] || value || '-'
}
function canAct(row) {
return row.status === 'PAID' && Number(row.allocAmount) > 0
}
function loadList() {
loading.value = true
listPayPayment(query).then(res => {
@@ -205,22 +265,46 @@ function openDetail(row) {
})
}
function reverseRow(row) {
proxy.$modal.confirm('反核销这张付款?账单未付金额会回来。确认人不能自己反核销。').then(() => reversePayPayment(row.paymentId)).then(() => {
proxy.$modal.msgSuccess('已反核销')
loadList()
}).catch(() => {})
function openAct(row, type) {
actType.value = type
actPayment.value = null
actFees.value = []
actVisible.value = true
actLoading.value = true
getPayPayment(row.paymentId).then(res => {
actPayment.value = res.data
const allocs = (res.data?.allocs || []).filter(item => item.allocStatus !== 'REVERSED' && item.billId)
return Promise.all(allocs.map(alloc => getPayBill(alloc.billId).then(billRes => {
const bill = billRes.data || {}
return (bill.lines || []).map(line => ({
billNo: alloc.billNo || bill.billNo,
orderNo: line.orderNo,
ladingNo: line.ladingNo,
feeName: line.feeName,
amount: line.amount,
currency: line.currency || bill.currency
}))
}).catch(() => [])))
}).then(groups => {
if (Array.isArray(groups)) actFees.value = groups.flat()
}).finally(() => { actLoading.value = false })
}
function refundRow(row) {
proxy.$modal.confirm('把这笔付款退回?已核销金额会冲回。').then(() => refundPayPayment(row.paymentId)).then(() => {
proxy.$modal.msgSuccess('已退款')
function runAct() {
if (!actPayment.value) return
const request = actType.value === 'refund' ? refundPayPayment : reversePayPayment
actSaving.value = true
request(actPayment.value.paymentId).then(() => {
proxy.$modal.msgSuccess(actType.value === 'refund' ? '已退款' : '已反核销')
actVisible.value = false
loadList()
}).catch(() => {})
if (detail.value?.paymentId === actPayment.value.paymentId) openDetail(actPayment.value)
}).finally(() => { actSaving.value = false })
}
function confirmRow(row) {
proxy.$modal.confirm('确认这张付款?登记人和确认人不能是同一人。').then(() => confirmPayPayment(row.paymentId)).then(() => {
const limit = String(userStore.id) === '1' ? '' : '登记人和确认人不能是同一人。'
proxy.$modal.confirm('确认这张付款?' + limit).then(() => confirmPayPayment(row.paymentId)).then(() => {
proxy.$modal.msgSuccess('已确认付款')
loadList()
}).catch(() => {})
@@ -238,4 +322,5 @@ loadList()
<style scoped>
.mb8 { margin-bottom: 8px; }
.act-hint { margin-top: 8px; }
</style>