提单费用调整

This commit is contained in:
liuminghui
2026-11-10 15:21:03 +08:00
parent e6a9fcf0a2
commit da9efc422e
16 changed files with 911 additions and 170 deletions
@@ -73,7 +73,8 @@ public class SurchargeController extends BaseController
@PreAuthorize("@ss.hasPermi('price:surcharge:list') or @ss.hasPermi('price:surcharge:query')" @PreAuthorize("@ss.hasPermi('price:surcharge:list') or @ss.hasPermi('price:surcharge:query')"
+ " or @ss.hasPermi('price:set:query') or @ss.hasPermi('price:set:add') or @ss.hasPermi('price:set:edit')" + " or @ss.hasPermi('price:set:query') or @ss.hasPermi('price:set:add') or @ss.hasPermi('price:set:edit')"
+ " or @ss.hasPermi('price:billing:bill')" + " or @ss.hasPermi('price:billing:bill')"
+ " or @ss.hasPermi('price:pay:query') or @ss.hasPermi('price:pay:edit') or @ss.hasPermi('price:pay:import')") + " or @ss.hasPermi('price:pay:query') or @ss.hasPermi('price:pay:edit') or @ss.hasPermi('price:pay:import')"
+ " or @ss.hasPermi('price:airBill:fee')")
@GetMapping("/options") @GetMapping("/options")
public AjaxResult listOptions(PrcSurcharge surcharge) public AjaxResult listOptions(PrcSurcharge surcharge)
{ {
@@ -15,9 +15,24 @@ public class SrvBillAirFee
private String billCode; private String billCode;
/** 字典 bms_fee_type */ /** 附加费编号 prc_surcharge.sc_id */
private String feeType; private String feeType;
/** 附加费名称,仅展示,不入库 */
private String feeName;
/** 费用阶段,字典 bms_fee_level */
private String feeLevel;
/** 提单费用状态,字典 bill_fee_status。只随分摊、作废操作变化 */
private String billFeeStatus;
/** 是否分摊,字典 bill_fee_alloc:1是 0否 */
private String allocFlag;
/** 分摊维度(票),字典 bill_fee_alloc_dim:1订单 2重量。是否分摊为否时为空 */
private String allocDim;
private BigDecimal price; private BigDecimal price;
/** 字典 bms_currency */ /** 字典 bms_currency */
@@ -11,7 +11,13 @@ public class PayPriceCompare
private PayProduct newer; private PayProduct newer;
/** 有效期、来源文件、币种 */
private List<PayPriceDiff> versionDiffs;
private List<PayPriceDiff> weightDiffs; private List<PayPriceDiff> weightDiffs;
private List<PayPriceDiff> surchargeDiffs; private List<PayPriceDiff> surchargeDiffs;
/** 邮编等不能归进重量段和附加费的原文 */
private List<PayPriceDiff> extraDiffs;
} }
@@ -6,6 +6,9 @@ import lombok.Data;
@Data @Data
public class PayPriceDiff public class PayPriceDiff
{ {
/** 版本、重量段、附加费、其他配置 */
private String kind;
private String name; private String name;
private String zoneName; private String zoneName;
@@ -26,6 +26,12 @@ public class PrcPayBillFee implements Serializable
/** CALC 计算,LADING 提单分摊,MANUAL 人工 */ /** CALC 计算,LADING 提单分摊,MANUAL 人工 */
private String feeSource; private String feeSource;
/** 来源数据id */
private String sourceId;
/** 是否删除:0未删除 1已删除 */
private String isDeleted;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime; private Date createTime;
} }
@@ -24,6 +24,9 @@ public final class PayCostCalculator
{ {
private static final ZoneId SHANGHAI = ZoneId.of("GMT+8"); private static final ZoneId SHANGHAI = ZoneId.of("GMT+8");
// 定义常量集合,建议放在类级别作为 static final 常量
private static final Set<Integer> REFUND_STATUS_SET = Set.of(9, 10);
private PayCostCalculator() private PayCostCalculator()
{ {
} }
@@ -242,6 +245,23 @@ public final class PayCostCalculator
return kept; return kept;
} }
/**
* 添加相关附加费,根据计费规则去判断
* @param product
* @param weight
* @param length
* @param width
* @param height
* @param postcode
* @param zoneName
* @param bizDate
* @param orderStatus
* @param freight
* @param currency
* @param freightSection
* @param remoteHits
* @param result
*/
private static void addSurcharges(PayProduct product, BigDecimal weight, BigDecimal length, BigDecimal width, private static void addSurcharges(PayProduct product, BigDecimal weight, BigDecimal length, BigDecimal width,
BigDecimal height, String postcode, String zoneName, Date bizDate, Integer orderStatus, BigDecimal freight, String currency, BigDecimal height, String postcode, String zoneName, Date bizDate, Integer orderStatus, BigDecimal freight, String currency,
String freightSection, List<PayRemoteBook.Hit> remoteHits, PayOrderCostResult result) String freightSection, List<PayRemoteBook.Hit> remoteHits, PayOrderCostResult result)
@@ -317,6 +337,7 @@ public final class PayCostCalculator
addFlat(result.getFees(), fee, currency, "计费重超过 " + fee.getCalcParam() + "kg"); addFlat(result.getFees(), fee, currency, "计费重超过 " + fee.getCalcParam() + "kg");
} }
} }
// 超出尺寸费用
else if (PaySurchargeRules.SIZE.equals(rule)) else if (PaySurchargeRules.SIZE.equals(rule))
{ {
if (length == null || width == null || height == null) if (length == null || width == null || height == null)
@@ -332,13 +353,15 @@ public final class PayCostCalculator
} }
} }
} }
// 退件费用
else if (PaySurchargeRules.RETURN.equals(rule)) else if (PaySurchargeRules.RETURN.equals(rule))
{ {
if (orderStatus != null && orderStatus == 9) if (orderStatus != null && REFUND_STATUS_SET.contains(orderStatus))
{ {
addFlat(result.getFees(), fee, currency, "订单已退件"); addFlat(result.getFees(), fee, currency, "订单已退件");
} }
} }
// 超重费
else if (PaySurchargeRules.EXTRA_KG.equals(rule)) else if (PaySurchargeRules.EXTRA_KG.equals(rule))
{ {
if (postcodeDriven) if (postcodeDriven)
@@ -3,6 +3,7 @@ package com.bms.price.service.impl;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.math.BigDecimal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@@ -15,6 +16,7 @@ import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -164,6 +166,7 @@ public class PayProductServiceImpl implements IPayProductService
} }
else else
{ {
product.setProductType(PAY_TYPE_AP);
Long existsId = isBlank(product.getDictValue()) ? null : payProductMapper.selectTemplateId(product.getDictValue()); Long existsId = isBlank(product.getDictValue()) ? null : payProductMapper.selectTemplateId(product.getDictValue());
product.setDuplicated(existsId != null); product.setDuplicated(existsId != null);
product.setExistsId(existsId); product.setExistsId(existsId);
@@ -266,7 +269,7 @@ public class PayProductServiceImpl implements IPayProductService
skipped++; skipped++;
continue; continue;
} }
product.setProductType(requireProductType(product.getProductType())); product.setProductType(PAY_TYPE_AP);
product.setFilePath(resolveSource(product)); product.setFilePath(resolveSource(product));
prepareCounts(product); prepareCounts(product);
fitColumns(product); fitColumns(product);
@@ -1694,8 +1697,10 @@ public class PayProductServiceImpl implements IPayProductService
PayPriceCompare compared = new PayPriceCompare(); PayPriceCompare compared = new PayPriceCompare();
compared.setOlder(older); compared.setOlder(older);
compared.setNewer(newer); compared.setNewer(newer);
compared.setVersionDiffs(diffVersion(older, newer));
compared.setWeightDiffs(diffWeights(older, newer)); compared.setWeightDiffs(diffWeights(older, newer));
compared.setSurchargeDiffs(diffFees(older, newer)); compared.setSurchargeDiffs(diffFees(older, newer));
compared.setExtraDiffs(diffExtras(older, newer));
return compared; return compared;
} }
@@ -1753,15 +1758,6 @@ public class PayProductServiceImpl implements IPayProductService
return stableId(link.getProductId()); return stableId(link.getProductId());
} }
private String requireProductType(String productType)
{
if (isBlank(productType))
{
throw new ServiceException("请选择支付类型");
}
return payTypeOf(productType);
}
private String payTypeOf(String payType) private String payTypeOf(String payType)
{ {
if (isBlank(payType)) if (isBlank(payType))
@@ -1861,111 +1857,305 @@ public class PayProductServiceImpl implements IPayProductService
return left <= otherRight && otherLeft <= right; return left <= otherRight && otherLeft <= right;
} }
/** 有效期和来源文件记在版本上,价格没变时也要能看出来选的是哪两版。 */
private List<PayPriceDiff> diffVersion(PayProduct older, PayProduct newer)
{
PayProductTerm oldTerm = payProductTermMapper.selectByProductId(older.getProductId());
PayProductTerm newTerm = payProductTermMapper.selectByProductId(newer.getProductId());
List<PayPriceDiff> diffs = new ArrayList<>();
diffs.add(metaDiff("生效开始", termStart(oldTerm, older), termStart(newTerm, newer)));
diffs.add(metaDiff("生效结束", termEnd(oldTerm), termEnd(newTerm)));
diffs.add(metaDiff("来源文件", blankTo(older.getSourceFile(), "-"), blankTo(newer.getSourceFile(), "-")));
diffs.add(metaDiff("币种", blankTo(older.getCurrency(), "-"), blankTo(newer.getCurrency(), "-")));
return diffs;
}
private List<PayPriceDiff> diffWeights(PayProduct older, PayProduct newer) private List<PayPriceDiff> diffWeights(PayProduct older, PayProduct newer)
{ {
Map<String, PayWeightBand> oldRows = new LinkedHashMap<>(); return pairDiffs(groupBy(older.getWeightBands(), PayProductServiceImpl::bandKey),
Map<String, PayWeightBand> newRows = new LinkedHashMap<>(); groupBy(newer.getWeightBands(), PayProductServiceImpl::bandKey), (oldRow, newRow) ->
if (older.getWeightBands() != null)
{
for (PayWeightBand band : older.getWeightBands())
{ {
oldRows.put(bandKey(band), band); PayWeightBand sample = oldRow != null ? oldRow : newRow;
} PayPriceDiff diff = new PayPriceDiff();
} diff.setKind("重量段");
if (newer.getWeightBands() != null) diff.setName(bandName(sample));
{ diff.setZoneName(blankTo(sample.getZoneName(), ""));
for (PayWeightBand band : newer.getWeightBands()) diff.setOldValue(oldRow == null ? "" : priceText(oldRow));
{ diff.setNewValue(newRow == null ? "" : priceText(newRow));
newRows.put(bandKey(band), band); return diff;
} });
}
List<PayPriceDiff> diffs = new ArrayList<>();
for (Map.Entry<String, PayWeightBand> entry : oldRows.entrySet())
{
PayWeightBand current = newRows.remove(entry.getKey());
PayPriceDiff diff = new PayPriceDiff();
diff.setName(blankTo(entry.getValue().getWeightLabel(), ""));
diff.setZoneName(blankTo(entry.getValue().getZoneName(), ""));
diff.setOldValue(priceText(entry.getValue()));
diff.setNewValue(current == null ? "" : priceText(current));
diff.setChange(current == null ? "删除" : (diff.getOldValue().equals(diff.getNewValue()) ? "相同" : "变更"));
diffs.add(diff);
}
for (PayWeightBand band : newRows.values())
{
PayPriceDiff diff = new PayPriceDiff();
diff.setName(blankTo(band.getWeightLabel(), ""));
diff.setZoneName(blankTo(band.getZoneName(), ""));
diff.setOldValue("");
diff.setNewValue(priceText(band));
diff.setChange("新增");
diffs.add(diff);
}
return diffs;
} }
private List<PayPriceDiff> diffFees(PayProduct older, PayProduct newer) private List<PayPriceDiff> diffFees(PayProduct older, PayProduct newer)
{ {
Map<String, PaySurcharge> oldRows = new LinkedHashMap<>(); return pairDiffs(groupBy(older.getSurcharges(), PayProductServiceImpl::feeKey),
Map<String, PaySurcharge> newRows = new LinkedHashMap<>(); groupBy(newer.getSurcharges(), PayProductServiceImpl::feeKey), (oldRow, newRow) ->
if (older.getSurcharges() != null)
{
for (PaySurcharge fee : older.getSurcharges())
{ {
oldRows.put(feeKey(fee), fee); PaySurcharge sample = oldRow != null ? oldRow : newRow;
} PayPriceDiff diff = new PayPriceDiff();
} diff.setKind("附加费");
if (newer.getSurcharges() != null) diff.setName(joinText(sample.getSectionName(), sample.getFeeName()));
{ diff.setZoneName(blankTo(sample.getZoneName(), ""));
for (PaySurcharge fee : newer.getSurcharges()) diff.setOldValue(oldRow == null ? "" : feeText(oldRow));
diff.setNewValue(newRow == null ? "" : feeText(newRow));
return diff;
});
}
private List<PayPriceDiff> diffExtras(PayProduct older, PayProduct newer)
{
return pairDiffs(groupBy(older.getExtraLines(), PayProductServiceImpl::extraKey),
groupBy(newer.getExtraLines(), PayProductServiceImpl::extraKey), (oldRow, newRow) ->
{ {
newRows.put(feeKey(fee), fee); PayExtraLine sample = oldRow != null ? oldRow : newRow;
} PayPriceDiff diff = new PayPriceDiff();
} diff.setKind("其他配置");
diff.setName(blankTo(sample.getLineType(), "配置"));
diff.setZoneName("");
diff.setOldValue(oldRow == null ? "" : blankTo(oldRow.getLineText(), ""));
diff.setNewValue(newRow == null ? "" : blankTo(newRow.getLineText(), ""));
return diff;
});
}
/**
* 同一行按顺序对齐。相同费用有多条邮编或重量档时不能并成一条,否则后面的改动会被盖掉。
*/
private <T> List<PayPriceDiff> pairDiffs(Map<String, List<T>> oldRows, Map<String, List<T>> newRows, RowDiff<T> factory)
{
Set<String> keys = new LinkedHashSet<>();
keys.addAll(oldRows.keySet());
keys.addAll(newRows.keySet());
List<PayPriceDiff> diffs = new ArrayList<>(); List<PayPriceDiff> diffs = new ArrayList<>();
for (Map.Entry<String, PaySurcharge> entry : oldRows.entrySet()) for (String key : keys)
{ {
PaySurcharge current = newRows.remove(entry.getKey()); List<T> older = oldRows.getOrDefault(key, Collections.emptyList());
PayPriceDiff diff = new PayPriceDiff(); List<T> newer = newRows.getOrDefault(key, Collections.emptyList());
diff.setName(blankTo(entry.getValue().getFeeName(), "")); int count = Math.max(older.size(), newer.size());
diff.setZoneName(blankTo(entry.getValue().getZoneName(), "")); for (int i = 0; i < count; i++)
diff.setOldValue(blankTo(entry.getValue().getChargeText(), "")); {
diff.setNewValue(current == null ? "" : blankTo(current.getChargeText(), "")); T oldRow = i < older.size() ? older.get(i) : null;
diff.setChange(current == null ? "删除" : (diff.getOldValue().equals(diff.getNewValue()) ? "相同" : "变更")); T newRow = i < newer.size() ? newer.get(i) : null;
diffs.add(diff); PayPriceDiff diff = factory.build(oldRow, newRow);
} diff.setChange(oldRow == null ? "新增" : (newRow == null ? "删除"
for (PaySurcharge fee : newRows.values()) : (diff.getOldValue().equals(diff.getNewValue()) ? "相同" : "变更")));
{ diffs.add(diff);
PayPriceDiff diff = new PayPriceDiff(); }
diff.setName(blankTo(fee.getFeeName(), ""));
diff.setZoneName(blankTo(fee.getZoneName(), ""));
diff.setOldValue("");
diff.setNewValue(blankTo(fee.getChargeText(), ""));
diff.setChange("新增");
diffs.add(diff);
} }
return diffs; return diffs;
} }
private static String bandKey(PayWeightBand band) private <T> Map<String, List<T>> groupBy(List<T> rows, Function<T, String> keyFn)
{ {
return blankTo(band.getZoneName(), "") + "|" + blankTo(band.getWeightLabel(), "") + "|" + blankTo(band.getChargeName(), ""); Map<String, List<T>> grouped = new LinkedHashMap<>();
if (rows == null)
{
return grouped;
}
for (T row : rows)
{
grouped.computeIfAbsent(keyFn.apply(row), key -> new ArrayList<>()).add(row);
}
return grouped;
} }
private static PayPriceDiff metaDiff(String name, String oldValue, String newValue)
{
PayPriceDiff diff = new PayPriceDiff();
diff.setKind("版本");
diff.setName(name);
diff.setZoneName("");
diff.setOldValue(oldValue);
diff.setNewValue(newValue);
diff.setChange(oldValue.equals(newValue) ? "相同" : "变更");
return diff;
}
private static String termStart(PayProductTerm term, PayProduct product)
{
if (term != null)
{
return dayOrOpen(term.getEffectiveStart());
}
return dayOrOpen(product.getEffectiveDate());
}
private static String termEnd(PayProductTerm term)
{
return term == null ? "不限" : dayOrOpen(term.getEffectiveEnd());
}
private static String dayOrOpen(Date date)
{
return date == null ? "不限" : formatDay(date);
}
/** 分区、重量段和费用名确定一行单价,价格本身不参与对齐。 */
private static String bandKey(PayWeightBand band)
{
return joinKey(band.getSectionName(), band.getZoneName(), band.getCountryCode(), band.getChargeName(),
band.getWeightLabel(), numKey(band.getWeightMin()), numKey(band.getWeightMax()));
}
/** 同一费用、同一分区、同一重量档里的多条邮编按导入顺序逐条比对。 */
private static String feeKey(PaySurcharge fee) private static String feeKey(PaySurcharge fee)
{ {
return blankTo(fee.getFeeName(), "") + "|" + blankTo(fee.getZoneName(), "") + "|" + blankTo(fee.getCalcRule(), ""); return joinKey(fee.getSectionName(), fee.getFeeName(), fee.getZoneName(), numKey(fee.getWeightMin()), numKey(fee.getWeightMax()));
}
private static String extraKey(PayExtraLine line)
{
return blankTo(line.getLineType(), "");
}
private static String bandName(PayWeightBand band)
{
String label = blankTo(band.getWeightLabel(), rangeText(band.getWeightMin(), band.getWeightMax()));
return joinText(band.getSectionName(), label, band.getChargeName());
} }
private static String priceText(PayWeightBand band) private static String priceText(PayWeightBand band)
{ {
String price = band.getPrice() == null ? "" : band.getPrice().stripTrailingZeros().toPlainString(); String price = money(band.getPrice());
String unit = blankTo(band.getPriceUnit(), ""); String unit = blankTo(band.getPriceUnit(), "");
String volume = blankTo(band.getVolumeText(), ""); String volume = blankTo(band.getVolumeText(), "");
String text = (price + " " + unit).trim(); String text = (price + " " + unit).trim();
return volume.isEmpty() ? text : (text + " " + volume).trim(); return volume.isEmpty() ? text : (text + " " + volume).trim();
} }
/** 金额、收费原文、计费规则和参数都算差异。只比原文会把改过的邮编和续重看成没变。 */
private static String feeText(PaySurcharge fee)
{
StringBuilder text = new StringBuilder();
String amount = money(fee.getAmount());
String unit = blankTo(fee.getPriceUnit(), "");
String priced = (amount + " " + unit).trim();
String charge = blankTo(fee.getChargeText(), "");
appendPart(text, priced);
if (!charge.isEmpty() && !charge.equals(priced) && !charge.equals(amount))
{
appendPart(text, charge);
}
String rule = ruleText(fee.getCalcRule());
String param = blankTo(fee.getCalcParam(), "");
String postcode = blankTo(fee.getPostcodeParam(), "");
if (!postcode.isEmpty() && !postcode.equals(param))
{
param = param.isEmpty() ? postcode : param + " " + postcode;
}
if (!rule.isEmpty() || !param.isEmpty())
{
appendPart(text, (rule + " " + param).trim());
}
appendPart(text, rangeText(fee.getWeightMin(), fee.getWeightMax()));
String period = blankTo(fee.getPeriodText(), "");
if (period.isEmpty() && (fee.getEffectiveStart() != null || fee.getEffectiveEnd() != null))
{
period = dayOrOpen(fee.getEffectiveStart()) + " 至 " + dayOrOpen(fee.getEffectiveEnd());
}
appendPart(text, period);
return text.toString();
}
private static String ruleText(String rule)
{
if (isBlank(rule) || "MANUAL".equalsIgnoreCase(rule))
{
return "";
}
switch (rule.toUpperCase(Locale.ROOT))
{
case "POSTCODE":
return "邮编";
case "EXTRA_KG":
return "续重";
case "ALWAYS":
return "固定";
case "ZONE":
return "分区";
case "WEIGHT":
return "重量";
case "SIZE":
return "尺寸";
case "RETURN":
return "退件";
default:
return rule;
}
}
private static String rangeText(BigDecimal min, BigDecimal max)
{
String left = money(min);
String right = money(max);
if (left.isEmpty() && right.isEmpty())
{
return "";
}
return (left.isEmpty() ? "0" : left) + "-" + (right.isEmpty() ? "不限" : right) + "kg";
}
private static String money(BigDecimal value)
{
return value == null ? "" : value.stripTrailingZeros().toPlainString();
}
private static String numKey(BigDecimal value)
{
return money(value);
}
private static String joinKey(String... parts)
{
StringBuilder text = new StringBuilder();
for (String part : parts)
{
if (text.length() > 0)
{
text.append('|');
}
text.append(blankTo(part, ""));
}
return text.toString();
}
private static String joinText(String... parts)
{
StringBuilder text = new StringBuilder();
for (String part : parts)
{
if (isBlank(part))
{
continue;
}
if (text.length() > 0)
{
text.append(' ');
}
text.append(part.trim());
}
return text.toString();
}
private static void appendPart(StringBuilder text, String part)
{
if (isBlank(part))
{
return;
}
if (text.length() > 0)
{
text.append(';');
}
text.append(part.trim());
}
@FunctionalInterface
private interface RowDiff<T>
{
PayPriceDiff build(T older, T newer);
}
private static Date today() private static Date today()
{ {
return toDate(java.time.LocalDate.now(java.time.ZoneId.of("GMT+8"))); return toDate(java.time.LocalDate.now(java.time.ZoneId.of("GMT+8")));
@@ -21,8 +21,10 @@ import com.bms.common.utils.SecurityUtils;
import com.bms.price.airbill.domain.SrvBillAirFee; import com.bms.price.airbill.domain.SrvBillAirFee;
import com.bms.price.airbill.domain.SrvBillAirFeeLog; import com.bms.price.airbill.domain.SrvBillAirFeeLog;
import com.bms.price.airbill.domain.SrvBillAirFeeSave; import com.bms.price.airbill.domain.SrvBillAirFeeSave;
import com.bms.price.domain.PrcSurcharge;
import com.bms.price.mapper.SrvBillAirFeeMapper; import com.bms.price.mapper.SrvBillAirFeeMapper;
import com.bms.price.mapper.SrvBillAirMapper; import com.bms.price.mapper.SrvBillAirMapper;
import com.bms.price.service.IPrcSurchargeService;
import com.bms.price.service.ISrvBillAirFeeService; import com.bms.price.service.ISrvBillAirFeeService;
import com.bms.system.service.ISysDictTypeService; import com.bms.system.service.ISysDictTypeService;
@@ -32,13 +34,22 @@ import com.bms.system.service.ISysDictTypeService;
@Service @Service
public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
{ {
private static final String FEE_DICT = "bms_fee_type"; private static final String LEVEL_DICT = "bms_fee_level";
private static final String ALLOC_DICT = "bill_fee_alloc";
private static final String ALLOC_DIM_DICT = "bill_fee_alloc_dim";
private static final String STATUS_DICT = "bill_fee_status";
private static final String CURRENCY_DICT = "bms_currency"; private static final String CURRENCY_DICT = "bms_currency";
@Autowired @Autowired
private SrvBillAirFeeMapper feeMapper; private SrvBillAirFeeMapper feeMapper;
@Autowired
private IPrcSurchargeService surchargeService;
@Autowired @Autowired
private SrvBillAirMapper billMapper; private SrvBillAirMapper billMapper;
@@ -84,7 +95,10 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
{ {
throw new ServiceException("没有需要保存的变更"); throw new ServiceException("没有需要保存的变更");
} }
Map<String, String> feeLabels = dictLabels(FEE_DICT, "费用类型"); Map<String, String> levelLabels = dictLabels(LEVEL_DICT, "费用阶段");
Map<String, String> allocLabels = dictLabels(ALLOC_DICT, "是否分摊");
Map<String, String> dimLabels = dictLabels(ALLOC_DIM_DICT, "分摊维度");
Map<String, String> statusLabels = dictLabels(STATUS_DICT, "费用状态");
Map<String, String> currencyLabels = dictLabels(CURRENCY_DICT, "币种"); Map<String, String> currencyLabels = dictLabels(CURRENCY_DICT, "币种");
Operator operator = operator(); Operator operator = operator();
Date now = DateUtils.getNowDate(); Date now = DateUtils.getNowDate();
@@ -110,7 +124,7 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
int rows = feeMapper.deleteFee(current.getId(), billCode, operator.account, now); int rows = feeMapper.deleteFee(current.getId(), billCode, operator.account, now);
if (rows > 0) if (rows > 0)
{ {
writeLog(billCode, id, SrvBillAirFeeLog.DELETE, current, null, feeLabels, currencyLabels, operator, now); writeLog(billCode, id, SrvBillAirFeeLog.DELETE, current, null, feeNames(current, null), levelLabels, allocLabels, dimLabels, statusLabels, currencyLabels, operator, now);
removed++; removed++;
} }
} }
@@ -129,8 +143,10 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
{ {
throw new ServiceException("费用已删除或不属于该提单"); throw new ServiceException("费用已删除或不属于该提单");
} }
SrvBillAirFee next = normalize(incoming, feeLabels, currencyLabels); SrvBillAirFee next = normalize(incoming, levelLabels, allocLabels, dimLabels, currencyLabels);
List<SrvBillAirFeeLog.FieldChange> changes = diff(current, next, feeLabels, currencyLabels); next.setBillFeeStatus(current.getBillFeeStatus() == null || current.getBillFeeStatus().isBlank() ? "0" : current.getBillFeeStatus());
Map<String, String> feeNames = feeNames(current, next);
List<SrvBillAirFeeLog.FieldChange> changes = diff(current, next, feeNames, levelLabels, allocLabels, dimLabels, statusLabels, currencyLabels);
if (changes.isEmpty()) if (changes.isEmpty())
{ {
continue; continue;
@@ -141,17 +157,19 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
next.setUpdateTime(now); next.setUpdateTime(now);
if (feeMapper.updateFee(next) > 0) if (feeMapper.updateFee(next) > 0)
{ {
writeLog(billCode, current.getId(), SrvBillAirFeeLog.UPDATE, current, next, feeLabels, currencyLabels, operator, now); writeLog(billCode, current.getId(), SrvBillAirFeeLog.UPDATE, current, next, feeNames, levelLabels, allocLabels, dimLabels, statusLabels, currencyLabels, operator, now);
updated++; updated++;
} }
} }
for (SrvBillAirFee incoming : inserts) for (SrvBillAirFee incoming : inserts)
{ {
if (incoming == null || blank(incoming.getFeeType()) && incoming.getPrice() == null && blank(incoming.getCurrency()) && blank(incoming.getRemark())) if (incoming == null || blank(incoming.getFeeType()) && blank(incoming.getFeeLevel()) && incoming.getPrice() == null
&& blank(incoming.getCurrency()) && blank(incoming.getRemark()))
{ {
continue; continue;
} }
SrvBillAirFee next = normalize(incoming, feeLabels, currencyLabels); SrvBillAirFee next = normalize(incoming, levelLabels, allocLabels, dimLabels, currencyLabels);
next.setBillFeeStatus("0");
next.setId(null); next.setId(null);
next.setBillCode(billCode); next.setBillCode(billCode);
next.setCreateBy(operator.account); next.setCreateBy(operator.account);
@@ -159,7 +177,7 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
next.setUpdateBy(operator.account); next.setUpdateBy(operator.account);
next.setUpdateTime(now); next.setUpdateTime(now);
feeMapper.insertFee(next); feeMapper.insertFee(next);
writeLog(billCode, next.getId(), SrvBillAirFeeLog.INSERT, null, next, feeLabels, currencyLabels, operator, now); writeLog(billCode, next.getId(), SrvBillAirFeeLog.INSERT, null, next, feeNames(null, next), levelLabels, allocLabels, dimLabels, statusLabels, currencyLabels, operator, now);
inserted++; inserted++;
} }
if (inserted + updated + removed == 0) if (inserted + updated + removed == 0)
@@ -174,18 +192,48 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
return feeMapper.selectLive(id, billCode); return feeMapper.selectLive(id, billCode);
} }
private SrvBillAirFee normalize(SrvBillAirFee raw, Map<String, String> feeLabels, Map<String, String> currencyLabels) private SrvBillAirFee normalize(SrvBillAirFee raw, Map<String, String> levelLabels,
Map<String, String> allocLabels, Map<String, String> dimLabels, Map<String, String> currencyLabels)
{ {
String feeType = trim(raw.getFeeType()); String feeType = trim(raw.getFeeType());
String feeLevel = trim(raw.getFeeLevel());
String allocFlag = trim(raw.getAllocFlag());
String allocDim = trim(raw.getAllocDim());
String currency = trim(raw.getCurrency()); String currency = trim(raw.getCurrency());
String remark = trim(raw.getRemark()); String remark = trim(raw.getRemark());
if (feeType.isEmpty()) Long scId = parseScId(feeType);
if (scId == null)
{ {
throw new ServiceException("请选择费用项"); throw new ServiceException("请选择费用项");
} }
if (feeType.length() > 32 || !feeLabels.containsKey(feeType)) PrcSurcharge surcharge = surchargeService.selectPrcSurchargeById(scId);
if (surcharge == null)
{ {
throw new ServiceException("费用项不在费用类型字典中"); throw new ServiceException("费用项不存在");
}
feeType = String.valueOf(scId);
if (allocFlag.isEmpty() || !allocLabels.containsKey(allocFlag))
{
throw new ServiceException("请选择是否分摊");
}
if (!feeLevel.isEmpty() && (feeLevel.length() > 32 || !levelLabels.containsKey(feeLevel)))
{
throw new ServiceException("费用阶段不在字典中");
}
if ("1".equals(allocFlag) && feeLevel.isEmpty())
{
throw new ServiceException("如需分摊费用至订单,请选择费用所属阶段");
}
if ("1".equals(allocFlag))
{
if (allocDim.isEmpty() || !dimLabels.containsKey(allocDim))
{
throw new ServiceException("如需分摊费用至订单,请选择分摊维度");
}
}
else
{
allocDim = "";
} }
if (raw.getPrice() == null) if (raw.getPrice() == null)
{ {
@@ -213,6 +261,9 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
} }
SrvBillAirFee fee = new SrvBillAirFee(); SrvBillAirFee fee = new SrvBillAirFee();
fee.setFeeType(feeType); fee.setFeeType(feeType);
fee.setFeeLevel(feeLevel);
fee.setAllocFlag(allocFlag);
fee.setAllocDim(allocDim);
fee.setPrice(raw.getPrice()); fee.setPrice(raw.getPrice());
fee.setCurrency(currency); fee.setCurrency(currency);
fee.setRemark(remark); fee.setRemark(remark);
@@ -220,9 +271,10 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
} }
private void writeLog(String billCode, Long feeId, String operation, SrvBillAirFee before, SrvBillAirFee after, private void writeLog(String billCode, Long feeId, String operation, SrvBillAirFee before, SrvBillAirFee after,
Map<String, String> feeLabels, Map<String, String> currencyLabels, Operator operator, Date now) Map<String, String> feeLabels, Map<String, String> levelLabels, Map<String, String> allocLabels,
Map<String, String> dimLabels, Map<String, String> statusLabels, Map<String, String> currencyLabels, Operator operator, Date now)
{ {
List<SrvBillAirFeeLog.FieldChange> changes = diff(before, after, feeLabels, currencyLabels); List<SrvBillAirFeeLog.FieldChange> changes = diff(before, after, feeLabels, levelLabels, allocLabels, dimLabels, statusLabels, currencyLabels);
SrvBillAirFeeLog log = new SrvBillAirFeeLog(); SrvBillAirFeeLog log = new SrvBillAirFeeLog();
log.setBillCode(billCode); log.setBillCode(billCode);
log.setFeeId(feeId); log.setFeeId(feeId);
@@ -236,11 +288,20 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
} }
private static List<SrvBillAirFeeLog.FieldChange> diff(SrvBillAirFee before, SrvBillAirFee after, private static List<SrvBillAirFeeLog.FieldChange> diff(SrvBillAirFee before, SrvBillAirFee after,
Map<String, String> feeLabels, Map<String, String> currencyLabels) Map<String, String> feeLabels, Map<String, String> levelLabels, Map<String, String> allocLabels,
Map<String, String> dimLabels, Map<String, String> statusLabels, Map<String, String> currencyLabels)
{ {
List<SrvBillAirFeeLog.FieldChange> changes = new ArrayList<>(); List<SrvBillAirFeeLog.FieldChange> changes = new ArrayList<>();
addChange(changes, "feeType", "费用类型", label(feeLabels, value(before, SrvBillAirFee::getFeeType)), addChange(changes, "feeType", "费用项", label(feeLabels, value(before, SrvBillAirFee::getFeeType)),
label(feeLabels, value(after, SrvBillAirFee::getFeeType))); label(feeLabels, value(after, SrvBillAirFee::getFeeType)));
addChange(changes, "feeLevel", "费用阶段", label(levelLabels, value(before, SrvBillAirFee::getFeeLevel)),
label(levelLabels, value(after, SrvBillAirFee::getFeeLevel)));
addChange(changes, "allocFlag", "是否分摊", label(allocLabels, value(before, SrvBillAirFee::getAllocFlag)),
label(allocLabels, value(after, SrvBillAirFee::getAllocFlag)));
addChange(changes, "allocDim", "分摊维度(票)", label(dimLabels, value(before, SrvBillAirFee::getAllocDim)),
label(dimLabels, value(after, SrvBillAirFee::getAllocDim)));
addChange(changes, "billFeeStatus", "费用状态", label(statusLabels, value(before, SrvBillAirFee::getBillFeeStatus)),
label(statusLabels, value(after, SrvBillAirFee::getBillFeeStatus)));
addChange(changes, "price", "价格", priceText(before), priceText(after)); addChange(changes, "price", "价格", priceText(before), priceText(after));
addChange(changes, "currency", "收费币种", label(currencyLabels, value(before, SrvBillAirFee::getCurrency)), addChange(changes, "currency", "收费币种", label(currencyLabels, value(before, SrvBillAirFee::getCurrency)),
label(currencyLabels, value(after, SrvBillAirFee::getCurrency))); label(currencyLabels, value(after, SrvBillAirFee::getCurrency)));
@@ -329,6 +390,60 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
return fee.getPrice().stripTrailingZeros().toPlainString(); return fee.getPrice().stripTrailingZeros().toPlainString();
} }
private Map<String, String> feeNames(SrvBillAirFee before, SrvBillAirFee after)
{
Map<String, String> names = new LinkedHashMap<>();
putFeeName(names, before);
putFeeName(names, after);
return names;
}
private void putFeeName(Map<String, String> names, SrvBillAirFee fee)
{
if (fee == null)
{
return;
}
String id = trim(fee.getFeeType());
if (id.isEmpty() || names.containsKey(id))
{
return;
}
names.put(id, surchargeName(id));
}
private String surchargeName(String feeType)
{
Long scId = parseScId(feeType);
if (scId == null)
{
return feeType;
}
PrcSurcharge surcharge = surchargeService.selectPrcSurchargeById(scId);
if (surcharge == null || surcharge.getScName() == null || surcharge.getScName().isBlank())
{
return feeType;
}
return surcharge.getScName().trim();
}
private static Long parseScId(String feeType)
{
if (feeType == null || feeType.isBlank() || !feeType.chars().allMatch(Character::isDigit))
{
return null;
}
try
{
long id = Long.parseLong(feeType);
return id > 0 ? id : null;
}
catch (NumberFormatException ex)
{
return null;
}
}
private static String label(Map<String, String> labels, String value) private static String label(Map<String, String> labels, String value)
{ {
if (value == null || value.isBlank()) if (value == null || value.isBlank())
@@ -47,7 +47,7 @@
e.supplier_name as supplierName, e.supplier_name as supplierName,
(select group_concat(concat(ifnull(nullif(trim(f.fee_name), ''), ifnull(f.fee_type, '费用')), ' ', format(f.amount, 2)) (select group_concat(concat(ifnull(nullif(trim(f.fee_name), ''), ifnull(f.fee_type, '费用')), ' ', format(f.amount, 2))
order by field(f.fee_type, 'BASIC_FREIGHT', 'REGISTRATION_FEE', 'SURCHARGE', 'REFUND'), f.fee_id separator ';') order by field(f.fee_type, 'BASIC_FREIGHT', 'REGISTRATION_FEE', 'SURCHARGE', 'REFUND'), f.fee_id separator ';')
from prc_pay_bill_fee f where f.bill_id = o.bill_id) as feeText, from prc_pay_bill_fee f where f.bill_id = o.bill_id and f.is_deleted = '0') as feeText,
(select b.bill_no from pay_bill_line l (select b.bill_no from pay_bill_line l
inner join pay_bill b on b.bill_id = l.bill_id inner join pay_bill b on b.bill_id = l.bill_id
where l.order_bill_id = o.bill_id and b.status &lt;&gt; 'VOID' where l.order_bill_id = o.bill_id and b.status &lt;&gt; 'VOID'
@@ -81,9 +81,11 @@
</select> </select>
<select id="selectOrderFees" resultType="com.bms.price.domain.PrcPayBillFee"> <select id="selectOrderFees" resultType="com.bms.price.domain.PrcPayBillFee">
select fee_id as feeId, bill_id as billId, fee_name as feeName, amount, currency, fee_source as feeSource select fee_id as feeId, bill_id as billId, fee_name as feeName, amount, currency, fee_source as feeSource,
source_id as sourceId, is_deleted as isDeleted
from prc_pay_bill_fee from prc_pay_bill_fee
where bill_id in where is_deleted = '0'
and bill_id in
<foreach collection="billIds" item="id" open="(" separator="," close=")">#{id}</foreach> <foreach collection="billIds" item="id" open="(" separator="," close=")">#{id}</foreach>
order by bill_id, fee_id order by bill_id, fee_id
</select> </select>
@@ -15,31 +15,38 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="currency" column="currency" /> <result property="currency" column="currency" />
<result property="roundMode" column="round_mode" /> <result property="roundMode" column="round_mode" />
<result property="remark" column="remark" /> <result property="remark" column="remark" />
<result property="sourceId" column="source_id" />
<result property="isDeleted" column="is_deleted" />
<result property="createTime" column="create_time" /> <result property="createTime" column="create_time" />
</resultMap> </resultMap>
<select id="selectByBillId" parameterType="String" resultMap="PrcPayBillFeeResult"> <select id="selectByBillId" parameterType="String" resultMap="PrcPayBillFeeResult">
select fee_id, bill_id, pay_bill_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark, create_time select fee_id, bill_id, pay_bill_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark,
source_id, is_deleted, create_time
from prc_pay_bill_fee from prc_pay_bill_fee
where bill_id = #{billId} where bill_id = #{billId}
and is_deleted = '0'
order by field(fee_type, 'BASIC_FREIGHT', 'REGISTRATION_FEE', 'SURCHARGE', 'REFUND'), fee_id order by field(fee_type, 'BASIC_FREIGHT', 'REGISTRATION_FEE', 'SURCHARGE', 'REFUND'), fee_id
</select> </select>
<insert id="insertPrcPayBillFee" parameterType="PrcPayBillFee"> <insert id="insertPrcPayBillFee" parameterType="PrcPayBillFee">
insert into prc_pay_bill_fee ( insert into prc_pay_bill_fee (
fee_id, bill_id, pay_bill_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark, create_time fee_id, bill_id, pay_bill_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark,
source_id, is_deleted, create_time
) values ( ) values (
#{feeId}, #{billId}, #{payBillId}, #{feeType}, #{feeName}, #{scId}, #{amount}, #{currency}, #{roundMode}, #{remark}, sysdate() #{feeId}, #{billId}, #{payBillId}, #{feeType}, #{feeName}, #{scId}, #{amount}, #{currency}, #{roundMode}, #{remark},
#{sourceId}, ifnull(#{isDeleted}, '0'), sysdate()
) )
</insert> </insert>
<insert id="insertBatch" parameterType="java.util.List"> <insert id="insertBatch" parameterType="java.util.List">
insert into prc_pay_bill_fee ( insert into prc_pay_bill_fee (
fee_id, bill_id, pay_bill_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark, create_time fee_id, bill_id, pay_bill_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark,
source_id, is_deleted, create_time
) values ) values
<foreach collection="list" item="item" separator=","> <foreach collection="list" item="item" separator=",">
(#{item.feeId}, #{item.billId}, #{item.payBillId}, #{item.feeType}, #{item.feeName}, #{item.scId}, #{item.amount}, (#{item.feeId}, #{item.billId}, #{item.payBillId}, #{item.feeType}, #{item.feeName}, #{item.scId}, #{item.amount},
#{item.currency}, #{item.roundMode}, #{item.remark}, sysdate()) #{item.currency}, #{item.roundMode}, #{item.remark}, #{item.sourceId}, ifnull(#{item.isDeleted}, '0'), sysdate())
</foreach> </foreach>
</insert> </insert>
@@ -3,31 +3,37 @@
<mapper namespace="com.bms.price.mapper.SrvBillAirFeeMapper"> <mapper namespace="com.bms.price.mapper.SrvBillAirFeeMapper">
<select id="selectByBillCode" resultType="com.bms.price.airbill.domain.SrvBillAirFee"> <select id="selectByBillCode" resultType="com.bms.price.airbill.domain.SrvBillAirFee">
select id, bill_code as billCode, fee_type as feeType, price, currency, remark, select f.id, f.bill_code as billCode, f.fee_type as feeType, sc.sc_name as feeName,
create_by as createBy, create_time as createTime, update_by as updateBy, f.fee_level as feeLevel, f.bill_fee_status as billFeeStatus,
update_time as updateTime, del_flag as delFlag f.alloc_flag as allocFlag, f.alloc_dim as allocDim, f.price, f.currency, f.remark,
from srv_bill_air_fee f.create_by as createBy, f.create_time as createTime, f.update_by as updateBy,
where bill_code = #{billCode} f.update_time as updateTime, f.del_flag as delFlag
and del_flag = '0' from srv_bill_air_fee f
order by id left join prc_surcharge sc on sc.sc_id = f.fee_type and ifnull(sc.is_del, '0') = '0'
where f.bill_code = #{billCode}
and f.del_flag = '0'
order by f.id
</select> </select>
<select id="selectLive" resultType="com.bms.price.airbill.domain.SrvBillAirFee"> <select id="selectLive" resultType="com.bms.price.airbill.domain.SrvBillAirFee">
select id, bill_code as billCode, fee_type as feeType, price, currency, remark, select f.id, f.bill_code as billCode, f.fee_type as feeType, sc.sc_name as feeName,
create_by as createBy, create_time as createTime, update_by as updateBy, f.fee_level as feeLevel, f.bill_fee_status as billFeeStatus,
update_time as updateTime, del_flag as delFlag f.alloc_flag as allocFlag, f.alloc_dim as allocDim, f.price, f.currency, f.remark,
from srv_bill_air_fee f.create_by as createBy, f.create_time as createTime, f.update_by as updateBy,
where id = #{id} f.update_time as updateTime, f.del_flag as delFlag
and bill_code = #{billCode} from srv_bill_air_fee f
and del_flag = '0' left join prc_surcharge sc on sc.sc_id = f.fee_type and ifnull(sc.is_del, '0') = '0'
where f.id = #{id}
and f.bill_code = #{billCode}
and f.del_flag = '0'
</select> </select>
<insert id="insertFee" useGeneratedKeys="true" keyProperty="id"> <insert id="insertFee" useGeneratedKeys="true" keyProperty="id">
insert into srv_bill_air_fee ( insert into srv_bill_air_fee (
bill_code, fee_type, price, currency, remark, bill_code, fee_type, fee_level, bill_fee_status, alloc_flag, alloc_dim, price, currency, remark,
create_by, create_time, update_by, update_time, del_flag create_by, create_time, update_by, update_time, del_flag
) values ( ) values (
#{billCode}, #{feeType}, #{price}, #{currency}, #{remark}, #{billCode}, #{feeType}, #{feeLevel}, '0', #{allocFlag}, #{allocDim}, #{price}, #{currency}, #{remark},
#{createBy}, #{createTime}, #{updateBy}, #{updateTime}, '0' #{createBy}, #{createTime}, #{updateBy}, #{updateTime}, '0'
) )
</insert> </insert>
@@ -35,6 +41,9 @@
<update id="updateFee"> <update id="updateFee">
update srv_bill_air_fee update srv_bill_air_fee
set fee_type = #{feeType}, set fee_type = #{feeType},
fee_level = #{feeLevel},
alloc_flag = #{allocFlag},
alloc_dim = #{allocDim},
price = #{price}, price = #{price},
currency = #{currency}, currency = #{currency},
remark = #{remark}, remark = #{remark},
+119
View File
@@ -0,0 +1,119 @@
-- 提单费用阶段、提单费用状态、是否分摊、分摊维度。可重复执行。
SET NAMES utf8mb4;
INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark)
SELECT N'费用阶段', 'bms_fee_level', '0', 'admin', sysdate(), N'srv_bill_air_fee.fee_level:1 a段头程仓储,2 b段提单运输,3 c段清关,4 d段尾程'
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'bms_fee_level');
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark)
SELECT v.dict_sort, v.dict_label, v.dict_value, 'bms_fee_level', '', v.list_class, 'N', '0', 'admin', sysdate(), v.remark
FROM (
SELECT 1 AS dict_sort, N'a段(头程仓储)' AS dict_label, '1' AS dict_value, 'primary' AS list_class, N'fee_level=1' AS remark
UNION ALL SELECT 2, N'b段(提单运输)', '2', 'success', N'fee_level=2'
UNION ALL SELECT 3, N'c段(清关)', '3', 'warning', N'fee_level=3'
UNION ALL SELECT 4, N'd段(尾程)', '4', 'info', N'fee_level=4'
) v
WHERE NOT EXISTS (
SELECT 1 FROM sys_dict_data d WHERE d.dict_type = 'bms_fee_level' AND d.dict_value = v.dict_value
);
INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark)
SELECT N'提单费用状态', 'bill_fee_status', '0', 'admin', sysdate(), N'srv_bill_air_fee.bill_fee_status:0未分摊 1已分摊 2已作废'
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'bill_fee_status');
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark)
SELECT v.dict_sort, v.dict_label, v.dict_value, 'bill_fee_status', '', v.list_class, v.is_default, '0', 'admin', sysdate(), v.remark
FROM (
SELECT 1 AS dict_sort, N'未分摊' AS dict_label, '0' AS dict_value, 'info' AS list_class, 'Y' AS is_default, N'新建费用的初始状态' AS remark
UNION ALL SELECT 2, N'已分摊', '1', 'success', 'N', N'分摊操作完成后'
UNION ALL SELECT 3, N'已作废', '2', 'danger', 'N', N'作废操作完成后'
) v
WHERE NOT EXISTS (
SELECT 1 FROM sys_dict_data d WHERE d.dict_type = 'bill_fee_status' AND d.dict_value = v.dict_value
);
SET @sql = (
SELECT IF(
COUNT(*) = 0,
'ALTER TABLE srv_bill_air_fee ADD COLUMN bill_fee_status char(1) NOT NULL DEFAULT ''0'' COMMENT ''提单费用状态,字典 bill_fee_status:0未分摊 1已分摊 2已作废'' AFTER fee_level',
'SELECT 1'
)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'srv_bill_air_fee' AND COLUMN_NAME = 'bill_fee_status'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark)
SELECT N'是否分摊', 'bill_fee_alloc', '0', 'admin', sysdate(), N'srv_bill_air_fee.alloc_flag:1是 0否'
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'bill_fee_alloc');
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark)
SELECT v.dict_sort, v.dict_label, v.dict_value, 'bill_fee_alloc', '', v.list_class, v.is_default, '0', 'admin', sysdate(), v.remark
FROM (
SELECT 1 AS dict_sort, N'是' AS dict_label, '1' AS dict_value, 'success' AS list_class, 'N' AS is_default, N'这条费用参与分摊,须选择费用阶段' AS remark
UNION ALL SELECT 2, N'否', '0', 'info', 'Y', N'这条费用不分摊,默认'
) v
WHERE NOT EXISTS (
SELECT 1 FROM sys_dict_data d WHERE d.dict_type = 'bill_fee_alloc' AND d.dict_value = v.dict_value
);
SET @sql = (
SELECT IF(
COUNT(*) = 0,
'ALTER TABLE srv_bill_air_fee ADD COLUMN alloc_flag char(1) NOT NULL DEFAULT ''0'' COMMENT ''是否分摊,字典 bill_fee_alloc:1是 0否。为是时必须选择费用阶段'' AFTER bill_fee_status',
'SELECT 1'
)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'srv_bill_air_fee' AND COLUMN_NAME = 'alloc_flag'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
UPDATE sys_dict_data SET is_default = 'N', remark = N'这条费用参与分摊,须选择费用阶段'
WHERE dict_type = 'bill_fee_alloc' AND dict_value = '1';
UPDATE sys_dict_data SET is_default = 'Y', remark = N'这条费用不分摊,默认'
WHERE dict_type = 'bill_fee_alloc' AND dict_value = '0';
SET @sql = (
SELECT IF(
COUNT(*) > 0,
'ALTER TABLE srv_bill_air_fee MODIFY COLUMN alloc_flag char(1) NOT NULL DEFAULT ''0'' COMMENT ''是否分摊,字典 bill_fee_alloc:1是 0否。为是时必须选择费用阶段''',
'SELECT 1'
)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'srv_bill_air_fee' AND COLUMN_NAME = 'alloc_flag'
AND (COLUMN_DEFAULT IS NULL OR COLUMN_DEFAULT <> '0')
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark)
SELECT N'分摊维度(票)', 'bill_fee_alloc_dim', '0', 'admin', sysdate(), N'srv_bill_air_fee.alloc_dim:1订单 2重量。是否分摊为是时必填'
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'bill_fee_alloc_dim');
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark)
SELECT v.dict_sort, v.dict_label, v.dict_value, 'bill_fee_alloc_dim', '', v.list_class, 'N', '0', 'admin', sysdate(), v.remark
FROM (
SELECT 1 AS dict_sort, N'订单' AS dict_label, '1' AS dict_value, 'primary' AS list_class, N'按订单票数分摊' AS remark
UNION ALL SELECT 2, N'重量', '2', 'success', N'按重量占比分摊'
) v
WHERE NOT EXISTS (
SELECT 1 FROM sys_dict_data d WHERE d.dict_type = 'bill_fee_alloc_dim' AND d.dict_value = v.dict_value
);
SET @sql = (
SELECT IF(
COUNT(*) = 0,
'ALTER TABLE srv_bill_air_fee ADD COLUMN alloc_dim char(1) NOT NULL DEFAULT '''' COMMENT ''分摊维度(票),字典 bill_fee_alloc_dim:1订单 2重量。是否分摊为是时必填'' AFTER alloc_flag',
'SELECT 1'
)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'srv_bill_air_fee' AND COLUMN_NAME = 'alloc_dim'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
+26
View File
@@ -12,6 +12,32 @@ PREPARE stmt FROM @fee_source;
EXECUTE stmt; EXECUTE stmt;
DEALLOCATE PREPARE stmt; DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
COUNT(*) = 0,
'ALTER TABLE prc_pay_bill_fee ADD COLUMN source_id varchar(64) DEFAULT NULL COMMENT ''来源数据id'' AFTER fee_source',
'SELECT 1'
)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'prc_pay_bill_fee' AND COLUMN_NAME = 'source_id'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
COUNT(*) = 0,
'ALTER TABLE prc_pay_bill_fee ADD COLUMN is_deleted char(1) NOT NULL DEFAULT ''0'' COMMENT ''是否删除:0未删除 1已删除'' AFTER source_id',
'SELECT 1'
)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'prc_pay_bill_fee' AND COLUMN_NAME = 'is_deleted'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
CREATE TABLE IF NOT EXISTS `pay_lading` ( CREATE TABLE IF NOT EXISTS `pay_lading` (
`lading_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '提单ID', `lading_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '提单ID',
`lading_no` varchar(64) NOT NULL COMMENT '提单号或航空主单号', `lading_no` varchar(64) NOT NULL COMMENT '提单号或航空主单号',
@@ -1,11 +1,6 @@
<template> <template>
<el-dialog v-model="visible" title="导入应付价卡" width="1080px" append-to-body destroy-on-close @closed="reset"> <el-dialog v-model="visible" title="导入应付价卡" width="1080px" append-to-body destroy-on-close @closed="reset">
<el-form label-width="88px" class="import-form"> <el-form label-width="88px" class="import-form">
<el-form-item label="支付类型" required>
<el-select v-model="productType" filterable placeholder="请选择支付类型" style="width: 420px">
<el-option v-for="item in bms_pay_type" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="产品"> <el-form-item label="产品">
<el-select v-model="dictValue" clearable filterable placeholder="不选则按工作表名称匹配全部字典产品" style="width: 420px"> <el-select v-model="dictValue" clearable filterable placeholder="不选则按工作表名称匹配全部字典产品" style="width: 420px">
<el-option v-for="item in payment_product_type" :key="item.value" :label="item.label" :value="item.value" /> <el-option v-for="item in payment_product_type" :key="item.value" :label="item.label" :value="item.value" />
@@ -115,14 +110,13 @@ import PayConfigDetail from './PayConfigDetail.vue'
const emit = defineEmits(['saved']) const emit = defineEmits(['saved'])
const { proxy } = getCurrentInstance() const { proxy } = getCurrentInstance()
const { payment_product_type, bms_pay_type } = useDict('payment_product_type', 'bms_pay_type') const { payment_product_type } = useDict('payment_product_type')
const visible = ref(false) const visible = ref(false)
const recognizing = ref(false) const recognizing = ref(false)
const saving = ref(false) const saving = ref(false)
const overwrite = ref(false) const overwrite = ref(false)
const dictValue = ref('') const dictValue = ref('')
const productType = ref('AP')
const preview = ref(null) const preview = ref(null)
const selected = ref([]) const selected = ref([])
const tableRef = ref() const tableRef = ref()
@@ -150,7 +144,6 @@ function reset() {
selected.value = [] selected.value = []
overwrite.value = false overwrite.value = false
dictValue.value = '' dictValue.value = ''
productType.value = 'AP'
activeKey.value = '' activeKey.value = ''
recognizing.value = false recognizing.value = false
saving.value = false saving.value = false
@@ -265,10 +258,6 @@ function onSelect(rows) {
} }
async function save() { async function save() {
if (!productType.value) {
proxy.$modal.msgError('请选择支付类型')
return
}
if (!selected.value.length) { if (!selected.value.length) {
proxy.$modal.msgError('请勾选要保存的产品') proxy.$modal.msgError('请勾选要保存的产品')
return return
@@ -284,8 +273,7 @@ async function save() {
} }
saving.value = true saving.value = true
try { try {
const products = selected.value.map(row => ({ ...row, productType: productType.value })) const res = await savePay({ overwrite: overwrite.value, products: selected.value })
const res = await savePay({ overwrite: overwrite.value, products })
const message = res.msg || '保存成功' const message = res.msg || '保存成功'
reset() reset()
visible.value = false visible.value = false
+118 -18
View File
@@ -61,12 +61,12 @@
<pay-import-dialog ref="importRef" @saved="tableRef.refresh()" /> <pay-import-dialog ref="importRef" @saved="tableRef.refresh()" />
<el-drawer v-model="historyVisible" :title="historyTitle" size="980px" destroy-on-close> <el-drawer v-model="historyVisible" :title="historyTitle" size="980px" append-to-body destroy-on-close>
<div class="history-bar"> <div class="history-bar">
<el-button type="primary" plain :disabled="historyPicked.length !== 2" @click="openCompare">比对所选版本</el-button> <el-button type="primary" plain :disabled="historyPicked.length !== 2" @click="openCompare">比对所选版本</el-button>
<span>勾选两个版本后比对。历史订单按生效时间取价,不看当前开关。</span> <span>勾选两个版本后比对。历史订单按生效时间取价,不看当前开关。</span>
</div> </div>
<el-table v-loading="historyLoading" :data="historyRows" border size="small" empty-text="还没有价格版本" @selection-change="onHistoryPick"> <el-table v-loading="historyLoading" :data="historyRows" row-key="termId" border size="small" empty-text="还没有价格版本" @selection-change="onHistoryPick">
<el-table-column type="selection" width="42" align="center" /> <el-table-column type="selection" width="42" align="center" />
<el-table-column prop="versionNo" label="版本" width="70" align="center" /> <el-table-column prop="versionNo" label="版本" width="70" align="center" />
<el-table-column label="来源文件" min-width="160" show-overflow-tooltip> <el-table-column label="来源文件" min-width="160" show-overflow-tooltip>
@@ -123,15 +123,42 @@
</template> </template>
</el-dialog> </el-dialog>
<el-drawer v-model="compareVisible" :title="compareTitle" size="88%" append-to-body destroy-on-close> <el-drawer v-model="compareVisible" :title="compareTitle" size="88%" append-to-body :z-index="4000" destroy-on-close>
<el-table v-loading="compareLoading" :data="compareRows" border size="small" max-height="640" empty-text="没有差异"> <div class="compare-panel">
<el-table-column prop="kind" label="类型" width="90" align="center" /> <div v-if="compareReady && !compareLoading" class="history-bar">
<el-table-column prop="change" label="变化" width="80" align="center" /> <el-tag type="primary" effect="plain">变更 {{ compareStat.changed }}</el-tag>
<el-table-column prop="name" label="项目" min-width="160" show-overflow-tooltip /> <el-tag type="success" effect="plain">新增 {{ compareStat.added }}</el-tag>
<el-table-column prop="zoneName" label="地区" min-width="140" show-overflow-tooltip /> <el-tag type="danger" effect="plain">删除 {{ compareStat.removed }}</el-tag>
<el-table-column prop="oldValue" label="旧版" min-width="160" show-overflow-tooltip /> <el-tag type="info" effect="plain">相同 {{ compareStat.same }}</el-tag>
<el-table-column prop="newValue" label="新版" min-width="160" show-overflow-tooltip /> </div>
</el-table> <el-alert
v-if="compareReady && !compareLoading && !compareRows.length"
title="这两版的有效期、重量段和附加费一致"
type="success"
:closable="false"
show-icon
/>
<el-table v-show="compareLoading || compareRows.length" v-loading="compareLoading" :data="compareRows" :row-class-name="compareRowClass" border size="small" max-height="640" empty-text="没有差异">
<el-table-column prop="kind" label="类型" width="100" align="center" />
<el-table-column label="变化" width="90" align="center">
<template #default="{ row }">
<el-tag :type="changeTagType(row.change)" effect="light">{{ row.change }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="name" label="项目" min-width="180" show-overflow-tooltip />
<el-table-column prop="zoneName" label="地区" min-width="140" show-overflow-tooltip />
<el-table-column label="旧版" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
<span :class="changeTextClass(row.change, 'old')">{{ row.oldValue || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="新版" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
<span :class="changeTextClass(row.change, 'new')">{{ row.newValue || '-' }}</span>
</template>
</el-table-column>
</el-table>
</div>
</el-drawer> </el-drawer>
<el-drawer <el-drawer
@@ -187,6 +214,12 @@ const compareVisible = ref(false)
const compareLoading = ref(false) const compareLoading = ref(false)
const compareTitle = ref('版本比对') const compareTitle = ref('版本比对')
const compareRows = ref([]) const compareRows = ref([])
const compareAll = ref([])
const compareReady = ref(false)
const compareStat = computed(() => {
const count = (name) => compareAll.value.filter(row => row.change === name).length
return { changed: count('变更'), added: count('新增'), removed: count('删除'), same: count('相同') }
})
const detailTitle = computed(() => { const detailTitle = computed(() => {
if (!detail.value) return '应付配置' if (!detail.value) return '应付配置'
const no = detail.value.versionNo ? `第 ${detail.value.versionNo} 版` : '' const no = detail.value.versionNo ? `第 ${detail.value.versionNo} 版` : ''
@@ -356,23 +389,60 @@ function onHistoryPick(rows) {
function openCompare() { function openCompare() {
if (historyPicked.value.length !== 2) return if (historyPicked.value.length !== 2) return
const [first, second] = historyPicked.value const [first, second] = [...historyPicked.value].sort((a, b) => (Number(a.versionNo) || 0) - (Number(b.versionNo) || 0))
if (!first.productId || !second.productId) {
proxy.$modal.msgError('选中的版本没有价格数据')
return
}
compareTitle.value = `第 ${first.versionNo || '-'} 版 对比 第 ${second.versionNo || '-'} 版`
compareVisible.value = true compareVisible.value = true
compareLoading.value = true compareLoading.value = true
compareRows.value = [] compareRows.value = []
compareAll.value = []
compareReady.value = false
comparePay(first.productId, second.productId).then(res => { comparePay(first.productId, second.productId).then(res => {
const data = res.data || {} const data = res.data || {}
const older = data.older?.versionNo || '-' const older = data.older?.versionNo
const newer = data.newer?.versionNo || '-' const newer = data.newer?.versionNo
compareTitle.value = `第 ${older} 版 对比 第 ${newer} 版` if (older && newer) compareTitle.value = `第 ${older} 版 对比 第 ${newer} 版`
const weights = (data.weightDiffs || []).map(row => ({ ...row, kind: '重量段' })) const rows = []
const fees = (data.surchargeDiffs || []).map(row => ({ ...row, kind: '附加费' })) .concat(markKind(data.versionDiffs, '版本'))
compareRows.value = weights.concat(fees).filter(row => row.change !== '相同') .concat(markKind(data.weightDiffs, '重量段'))
.concat(markKind(data.surchargeDiffs, '附加费'))
.concat(markKind(data.extraDiffs, '其他配置'))
compareAll.value = rows
compareRows.value = rows.filter(row => row.change !== '相同')
compareReady.value = true
}).finally(() => { }).finally(() => {
compareLoading.value = false compareLoading.value = false
}) })
} }
function markKind(rows, kind) {
return (rows || []).map(row => ({ ...row, kind: row.kind || kind }))
}
function changeTagType(change) {
if (change === '变更') return 'primary'
if (change === '新增') return 'success'
if (change === '删除') return 'danger'
return 'info'
}
function compareRowClass({ row }) {
if (row.change === '变更') return 'compare-changed'
if (row.change === '新增') return 'compare-added'
if (row.change === '删除') return 'compare-removed'
return ''
}
function changeTextClass(change, side) {
if (change === '变更') return 'compare-text-changed'
if (change === '删除' && side === 'old') return 'compare-text-removed'
if (change === '新增' && side === 'new') return 'compare-text-added'
return ''
}
function loadHistory() { function loadHistory() {
if (!historyProductId.value) return if (!historyProductId.value) return
historyLoading.value = true historyLoading.value = true
@@ -446,4 +516,34 @@ function statusType(status) {
margin-bottom: 12px; margin-bottom: 12px;
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
} }
.compare-text-changed {
color: var(--el-color-primary);
font-weight: 600;
}
.compare-text-added {
color: var(--el-color-success);
font-weight: 600;
}
.compare-text-removed {
color: var(--el-color-danger);
font-weight: 600;
}
.compare-panel :deep(tr.compare-changed > td.el-table__cell) {
background: var(--el-color-primary-light-9);
}
.compare-panel :deep(tr.compare-added > td.el-table__cell) {
background: var(--el-color-success-light-9);
}
.compare-panel :deep(tr.compare-removed > td.el-table__cell) {
background: var(--el-color-danger-light-9);
}
.compare-panel :deep(tr.compare-changed:hover > td.el-table__cell) {
background: var(--el-color-primary-light-8);
}
.compare-panel :deep(tr.compare-added:hover > td.el-table__cell) {
background: var(--el-color-success-light-8);
}
.compare-panel :deep(tr.compare-removed:hover > td.el-table__cell) {
background: var(--el-color-danger-light-8);
}
</style> </style>
@@ -1,24 +1,57 @@
<template> <template>
<el-dialog v-model="visible" :title="title" width="980px" append-to-body destroy-on-close @closed="reset"> <el-dialog v-model="visible" :title="title" width="1360px" append-to-body destroy-on-close @closed="reset">
<div v-loading="loading"> <div v-loading="loading">
<div class="fee-toolbar"> <div class="fee-toolbar">
<el-button type="primary" plain icon="Plus" @click="addRow">新增</el-button> <el-button type="primary" plain icon="Plus" @click="addRow">新增</el-button>
<span class="fee-hint">只保存本次新增、修改和删除的费用,未改动的行不会重写。</span> <span class="fee-hint">只保存本次新增、修改和删除的费用,未改动的行不会重写。</span>
</div> </div>
<el-table :data="rows" border size="small" max-height="320" row-key="rowKey" empty-text="暂无费用,点击新增"> <el-table :data="rows" border size="small" max-height="320" row-key="rowKey" empty-text="暂无费用,点击新增">
<el-table-column label="费用项" min-width="160"> <el-table-column min-width="180">
<template #header><span class="col-label"><i v-if="allocRequired" class="req-star">*</i>费用阶段</span></template>
<template #default="{ row }"> <template #default="{ row }">
<el-select v-model="row.feeType" filterable clearable placeholder="请选择" style="width: 100%"> <el-select v-model="row.feeLevel" filterable clearable placeholder="请选择" style="width: 100%">
<el-option v-for="item in bms_fee_type" :key="item.value" :label="item.label" :value="item.value" /> <el-option v-for="item in bms_fee_level" :key="item.value" :label="item.label" :value="String(item.value)" />
</el-select> </el-select>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="价格" width="150"> <el-table-column width="120">
<template #header><span class="col-label"><i class="req-star">*</i>是否分摊</span></template>
<template #default="{ row }">
<el-select v-model="row.allocFlag" placeholder="请选择" style="width: 100%" @change="changeAlloc(row)">
<el-option v-for="item in bill_fee_alloc" :key="item.value" :label="item.label" :value="String(item.value)" />
</el-select>
</template>
</el-table-column>
<el-table-column v-if="allocRequired" min-width="150">
<template #header><span class="col-label"><i class="req-star">*</i>分摊维度(票)</span></template>
<template #default="{ row }">
<el-select v-if="String(row.allocFlag) === '1'" v-model="row.allocDim" placeholder="请选择" style="width: 100%">
<el-option v-for="item in bill_fee_alloc_dim" :key="item.value" :label="item.label" :value="String(item.value)" />
</el-select>
</template>
</el-table-column>
<el-table-column label="费用状态" width="100" align="center">
<template #default="{ row }">
<dict-tag :options="bill_fee_status" :value="row.billFeeStatus || '0'" />
</template>
</el-table-column>
<el-table-column min-width="160">
<template #header><span class="col-label"><i class="req-star">*</i>费用项</span></template>
<template #default="{ row }">
<el-select v-model="row.feeType" filterable remote clearable reserve-keyword placeholder="输入费用名称"
:remote-method="searchSurcharge" :loading="surchargeLoading" style="width: 100%" @change="name => onFeeChange(row, name)">
<el-option v-for="item in surchargeOptions" :key="item.scId" :label="item.scName" :value="item.scId" />
</el-select>
</template>
</el-table-column>
<el-table-column width="150">
<template #header><span class="col-label"><i class="req-star">*</i>价格</span></template>
<template #default="{ row }"> <template #default="{ row }">
<el-input-number v-model="row.price" :min="0" :precision="4" :controls="false" :value-on-clear="null" placeholder="价格" style="width: 100%" /> <el-input-number v-model="row.price" :min="0" :precision="4" :controls="false" :value-on-clear="null" placeholder="价格" style="width: 100%" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="收费币种" width="140"> <el-table-column width="140">
<template #header><span class="col-label"><i class="req-star">*</i>收费币种</span></template>
<template #default="{ row }"> <template #default="{ row }">
<el-select v-model="row.currency" filterable clearable placeholder="请选择" style="width: 100%"> <el-select v-model="row.currency" filterable clearable placeholder="请选择" style="width: 100%">
<el-option v-for="item in bms_currency" :key="item.value" :label="item.label" :value="item.value" /> <el-option v-for="item in bms_currency" :key="item.value" :label="item.label" :value="item.value" />
@@ -91,10 +124,11 @@
<script setup> <script setup>
import { listAirBillFee, listAirBillFeeLog, saveAirBillFee } from '@/api/price/airBill' import { listAirBillFee, listAirBillFeeLog, saveAirBillFee } from '@/api/price/airBill'
import { listSurchargeOptions } from '@/api/price/surcharge'
import { parseTime } from '@/utils/bms' import { parseTime } from '@/utils/bms'
const { proxy } = getCurrentInstance() const { proxy } = getCurrentInstance()
const { bms_fee_type, bms_currency } = useDict('bms_fee_type', 'bms_currency') const { bms_fee_level, bms_currency, bill_fee_status, bill_fee_alloc, bill_fee_alloc_dim } = useDict('bms_fee_level', 'bms_currency', 'bill_fee_status', 'bill_fee_alloc', 'bill_fee_alloc_dim')
const visible = ref(false) const visible = ref(false)
const loading = ref(false) const loading = ref(false)
@@ -107,10 +141,14 @@ const logs = ref([])
const logPageNum = ref(1) const logPageNum = ref(1)
const logPageSize = ref(10) const logPageSize = ref(10)
const logTotal = ref(0) const logTotal = ref(0)
const surchargeOptions = ref([])
const surchargeLoading = ref(false)
let rowSeed = 0 let rowSeed = 0
let logSeq = 0 let logSeq = 0
let surchargeSeq = 0
const title = computed(() => billCode.value ? `提单费用 - ${billCode.value}` : '提单费用') const title = computed(() => billCode.value ? `提单费用 - ${billCode.value}` : '提单费用')
const allocRequired = computed(() => rows.value.some(row => String(row.allocFlag) === '1'))
const dirty = computed(() => deletedIds.value.length > 0 || rows.value.some(row => rowChanged(row))) const dirty = computed(() => deletedIds.value.length > 0 || rows.value.some(row => rowChanged(row)))
function open(row) { function open(row) {
@@ -130,6 +168,9 @@ function reset() {
logPageSize.value = 10 logPageSize.value = 10
logTotal.value = 0 logTotal.value = 0
logSeq += 1 logSeq += 1
surchargeSeq += 1
surchargeOptions.value = []
surchargeLoading.value = false
saving.value = false saving.value = false
loading.value = false loading.value = false
logLoading.value = false logLoading.value = false
@@ -139,6 +180,7 @@ function load() {
logPageNum.value = 1 logPageNum.value = 1
loadFees() loadFees()
loadLogs() loadLogs()
searchSurcharge('')
} }
function loadFees() { function loadFees() {
@@ -151,16 +193,58 @@ function loadFees() {
rowKey: `fee-${item.id}`, rowKey: `fee-${item.id}`,
id: item.id, id: item.id,
feeType: item.feeType == null ? '' : String(item.feeType), feeType: item.feeType == null ? '' : String(item.feeType),
feeName: item.feeName || '',
feeLevel: item.feeLevel == null ? '' : String(item.feeLevel),
allocFlag: item.allocFlag == null || item.allocFlag === '' ? '0' : String(item.allocFlag),
allocDim: item.allocDim == null ? '' : String(item.allocDim),
billFeeStatus: item.billFeeStatus == null || item.billFeeStatus === '' ? '0' : String(item.billFeeStatus),
price: item.price === null || item.price === undefined ? null : Number(item.price), price: item.price === null || item.price === undefined ? null : Number(item.price),
currency: item.currency || '', currency: item.currency || '',
remark: item.remark || '', remark: item.remark || '',
origin: snapshot(item) origin: snapshot(item)
})) }))
pinSelectedFees()
}).finally(() => { }).finally(() => {
if (code === billCode.value) loading.value = false if (code === billCode.value) loading.value = false
}) })
} }
function pinSelectedFees() {
const known = new Map(surchargeOptions.value.map(item => [String(item.scId), item]))
for (const row of rows.value) {
if (!row.feeType) continue
if (!known.has(String(row.feeType))) {
known.set(String(row.feeType), { scId: String(row.feeType), scName: row.feeName || String(row.feeType) })
}
}
surchargeOptions.value = [...known.values()]
}
function searchSurcharge(keyword) {
const seq = ++surchargeSeq
surchargeLoading.value = true
const params = { ssCode: 'on' }
const name = String(keyword || '').trim()
if (name) params.scName = name
listSurchargeOptions(params).then(res => {
if (seq !== surchargeSeq) return
surchargeOptions.value = (res.data || []).map(item => ({
scId: item.scId == null ? '' : String(item.scId),
scName: item.scName || ''
})).filter(item => item.scId)
pinSelectedFees()
}).catch(() => {
if (seq === surchargeSeq) pinSelectedFees()
}).finally(() => {
if (seq === surchargeSeq) surchargeLoading.value = false
})
}
function onFeeChange(row, id) {
const found = surchargeOptions.value.find(item => String(item.scId) === String(id || ''))
row.feeName = found ? found.scName : ''
}
function unwrapLogs(res) { function unwrapLogs(res) {
const rows = res?.rows const rows = res?.rows
if (Array.isArray(rows)) { if (Array.isArray(rows)) {
@@ -207,6 +291,11 @@ function addRow() {
rowKey: `new-${rowSeed}`, rowKey: `new-${rowSeed}`,
id: null, id: null,
feeType: '', feeType: '',
feeName: '',
feeLevel: '',
allocFlag: '0',
allocDim: '',
billFeeStatus: '0',
price: null, price: null,
currency: '', currency: '',
remark: '', remark: '',
@@ -214,6 +303,10 @@ function addRow() {
}) })
} }
function changeAlloc(row) {
if (String(row.allocFlag) !== '1') row.allocDim = ''
}
function removeRow(row) { function removeRow(row) {
if (row.id != null && !deletedIds.value.some(id => String(id) === String(row.id))) { if (row.id != null && !deletedIds.value.some(id => String(id) === String(row.id))) {
deletedIds.value.push(row.id) deletedIds.value.push(row.id)
@@ -224,6 +317,9 @@ function removeRow(row) {
function snapshot(item) { function snapshot(item) {
return { return {
feeType: item.feeType || '', feeType: item.feeType || '',
feeLevel: item.feeLevel || '',
allocFlag: item.allocFlag == null || item.allocFlag === '' ? '0' : String(item.allocFlag),
allocDim: item.allocDim || '',
price: normPrice(item.price), price: normPrice(item.price),
currency: item.currency || '', currency: item.currency || '',
remark: (item.remark || '').trim() remark: (item.remark || '').trim()
@@ -233,6 +329,9 @@ function snapshot(item) {
function currentOf(row) { function currentOf(row) {
return { return {
feeType: row.feeType || '', feeType: row.feeType || '',
feeLevel: row.feeLevel || '',
allocFlag: row.allocFlag || '',
allocDim: String(row.allocFlag) === '1' ? (row.allocDim || '') : '',
price: normPrice(row.price), price: normPrice(row.price),
currency: row.currency || '', currency: row.currency || '',
remark: (row.remark || '').trim() remark: (row.remark || '').trim()
@@ -248,20 +347,40 @@ function normPrice(value) {
function rowChanged(row) { function rowChanged(row) {
if (!row.origin) { if (!row.origin) {
const now = currentOf(row) const now = currentOf(row)
return !!(now.feeType || now.price || now.currency || now.remark) return !!(now.feeType || now.feeLevel || now.allocFlag === '1' || now.allocDim || now.price || now.currency || now.remark)
} }
const now = currentOf(row) const now = currentOf(row)
return now.feeType !== row.origin.feeType || now.price !== row.origin.price return now.feeType !== row.origin.feeType || now.feeLevel !== row.origin.feeLevel || now.allocFlag !== row.origin.allocFlag
|| now.allocDim !== row.origin.allocDim || now.price !== row.origin.price
|| now.currency !== row.origin.currency || now.remark !== row.origin.remark || now.currency !== row.origin.currency || now.remark !== row.origin.remark
} }
function rowError(row) {
if (String(row.allocFlag) !== '0' && String(row.allocFlag) !== '1') return '请选择是否分摊'
if (String(row.allocFlag) === '1' && !row.feeLevel) return '如需分摊费用至订单,请选择费用所属阶段'
if (String(row.allocFlag) === '1' && !row.allocDim) return '如需分摊费用至订单,请选择分摊维度'
if (!row.feeType) return '请选择费用项'
if (row.price === null || row.price === undefined || row.price === '') return '请填写价格'
if (Number(row.price) < 0) return '价格不能小于0'
if (!row.currency) return '请选择收费币种'
return ''
}
function save() { function save() {
const invalid = rows.value.find(row => rowChanged(row) && rowError(row))
if (invalid) {
proxy.$modal.msgError(rowError(invalid))
return
}
const inserts = [] const inserts = []
const updates = [] const updates = []
rows.value.forEach(row => { rows.value.forEach(row => {
if (!rowChanged(row)) return if (!rowChanged(row)) return
const payload = { const payload = {
feeType: row.feeType || '', feeType: row.feeType || '',
feeLevel: row.feeLevel || '',
allocFlag: row.allocFlag || '',
allocDim: String(row.allocFlag) === '1' ? (row.allocDim || '') : '',
price: row.price, price: row.price,
currency: row.currency || '', currency: row.currency || '',
remark: (row.remark || '').trim() remark: (row.remark || '').trim()
@@ -323,6 +442,18 @@ defineExpose({ open })
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
font-size: 12px; font-size: 12px;
} }
.col-label {
display: inline-flex;
align-items: flex-start;
line-height: 1.2;
}
.req-star {
color: var(--el-color-danger);
font-style: normal;
font-weight: 700;
margin-right: 2px;
line-height: 1;
}
.log-title { .log-title {
margin: 16px 0 8px; margin: 16px 0 8px;
font-weight: 600; font-weight: 600;