提单费用均摊处理

This commit is contained in:
liuminghui
2026-11-10 17:24:52 +08:00
parent 3c49175cea
commit fbc138f041
23 changed files with 1355 additions and 32 deletions
@@ -0,0 +1,11 @@
package com.bms.price.airbill.domain;
import java.util.List;
import lombok.Data;
/** 提单费用分摊请求。一次最多 5 个提单号。 */
@Data
public class SrvBillAirAllocCommand
{
private List<String> billCodes;
}
@@ -0,0 +1,45 @@
package com.bms.price.airbill.domain;
import java.math.BigDecimal;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
/** 待分摊的一条提单费用,供费用处理弹框展示。 */
@Data
public class SrvBillAirAllocFee
{
private Long id;
private String billCode;
/** 提单主键 srv_bill_air.guid,写入订单费用的 source_id */
private String ladingId;
/** 费用阶段,字典 bms_fee_level */
private String feeLevel;
/** 费用状态,字典 bill_fee_status */
private String billFeeStatus;
private String feeName;
private BigDecimal price;
private String currency;
/** 分摊维度:1订单 2重量 */
private String allocDim;
/** 提单实际重量。只在按重量分摊时按提单号填入,不返回页面 */
@JsonIgnore
private BigDecimal actualWeight;
/** 干线打板重量。只在按重量分摊时按提单号填入,不返回页面 */
@JsonIgnore
private BigDecimal bandBoxWeight;
private Integer orderCount;
/** 不能分摊时的原因。空表示可以分摊 */
private String blockReason;
}
@@ -0,0 +1,19 @@
package com.bms.price.airbill.domain;
import java.math.BigDecimal;
import lombok.Data;
/** 提单下参与分摊的一票订单。 */
@Data
public class SrvBillAirAllocOrder
{
private String billCode;
private String orderNo;
/** 计费订单 ID,对应 prc_order_bill.bill_id */
private String billId;
/** 计费重,没有计费重时依次取实际重量、收货重量 */
private BigDecimal chargeWeight;
}
@@ -17,6 +17,12 @@ public class SrvBillAirFeeLog
public static final String DELETE = "3";
/** 已分摊费用确定。日志不记录字段变化。 */
public static final String CONFIRM = "4";
/** 已分摊或已确定费用作废。 */
public static final String VOID = "5";
private Long logId;
private String billCode;
@@ -19,6 +19,9 @@ public class PrcPayBillFee implements Serializable
private String feeType;
private String feeName;
private Long scId;
/** 费用所属阶段:1 a段,2 b段,3 c段,4 d段 */
private String feeLevel;
private BigDecimal amount;
private String currency;
private String roundMode;
@@ -38,4 +38,21 @@ public interface PrcPayBillFeeMapper
* @return 影响行数
*/
public int deleteByBillId(String billId);
/**
* 逻辑删除某一条提单费用分摊到订单上的明细。
*
* @param feeId 提单费用 ID,保存在 sc_id
* @return 影响行数
*/
public int markLadingFeeDeleted(@Param("feeId") Long feeId);
/**
* 这条提单费用分摊到的订单里,取一个已经生成应付账单的订单号。
* 是否出账看 prc_order_bill_ext.is_meet:1 已生成。
*
* @param feeId 提单费用 ID,保存在 sc_id
* @return 订单号,没有已出账订单时为空
*/
public String selectBilledOrderNo(@Param("feeId") Long feeId);
}
@@ -0,0 +1,56 @@
package com.bms.price.mapper;
import java.util.Date;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.bms.price.airbill.domain.SrvBillAirAllocFee;
import com.bms.price.airbill.domain.SrvBillAirAllocOrder;
import com.bms.price.domain.PrcPayBill;
/**
* 提单费用分摊到订单。
*/
public interface SrvBillAirAllocMapper
{
/**
* 查询这些提单上尚未分摊、且标记为需要分摊的费用。
*
* @param billCodes 提单号
* @return 待分摊费用
*/
List<SrvBillAirAllocFee> selectPendingFees(@Param("billCodes") List<String> billCodes);
/**
* 按提单号取出实际重量和打板重量,只供按重量分摊计算。
*
* @param billCodes 提单号
* @return 提单号、实际重量、打板重量
*/
List<SrvBillAirAllocFee> selectLadingWeight(@Param("billCodes") List<String> billCodes);
/**
* 按提单号查出关联订单。订单号在扩展表,计费单号和重量在订单表。
*
* @param billCodes 提单号,对应 prc_order_bill_ext.air_no_old
* @return 订单
*/
List<SrvBillAirAllocOrder> selectOrders(@Param("billCodes") List<String> billCodes);
/**
* 按计费订单号取出已有的应付主表。
*
* @param billIds 计费订单 ID
* @return 应付主表编号
*/
List<PrcPayBill> selectPayBills(@Param("billIds") List<String> billIds);
/**
* 把仍是未分摊的费用改为已分摊。已经不是未分摊的行不会被改到。
*
* @param ids 提单费用 ID
* @param updateBy 操作人
* @param updateTime 操作时间
* @return 实际改到的行数
*/
int markAllocated(@Param("ids") List<Long> ids, @Param("updateBy") String updateBy, @Param("updateTime") Date updateTime);
}
@@ -22,6 +22,14 @@ public interface SrvBillAirFeeMapper
int deleteFee(@Param("id") Long id, @Param("billCode") String billCode, @Param("updateBy") String updateBy,
@Param("updateTime") java.util.Date updateTime);
/** 已分摊改为已确定。 */
int confirmFee(@Param("id") Long id, @Param("billCode") String billCode, @Param("updateBy") String updateBy,
@Param("updateTime") java.util.Date updateTime);
/** 已分摊或已确定的费用作废:状态改为已作废,并逻辑删除。 */
int voidFee(@Param("id") Long id, @Param("billCode") String billCode, @Param("updateBy") String updateBy,
@Param("updateTime") java.util.Date updateTime);
int insertLog(SrvBillAirFeeLog log);
List<SrvBillAirFeeLog> selectLogs(String billCode);
@@ -0,0 +1,26 @@
package com.bms.price.service;
import java.util.List;
import com.bms.price.airbill.domain.SrvBillAirAllocFee;
/**
* 把提单上待分摊的费用均摊到关联订单。
*/
public interface ISrvBillAirAllocService
{
/**
* 查出勾选提单上需要分摊、且尚未分摊的费用。
*
* @param billCodes 提单号,最多 5 个
* @return 费用和不能分摊的原因
*/
List<SrvBillAirAllocFee> preview(List<String> billCodes);
/**
* 按费用上的分摊维度把金额摊到每个订单,并改费用状态为已分摊。整次处理在一个事务里,写入按 1000 条一批。
*
* @param billCodes 提单号,最多 5 个
* @return 给页面的处理结果
*/
String allocate(List<String> billCodes);
}
@@ -16,4 +16,22 @@ public interface ISrvBillAirFeeService
/** 只写入有变动的行,并按行记录修改前后。返回给页面的提示。 */
String save(SrvBillAirFeeSave command);
/**
* 已分摊的费用确认为已确定。
*
* @param billCode 提单号
* @param feeId 费用 ID
* @return 给页面的提示
*/
String confirm(String billCode, Long feeId);
/**
* 作废已分摊或已确定的费用。订单上的分摊明细逻辑删除,这条提单费用也逻辑删除。
*
* @param billCode 提单号
* @param feeId 费用 ID
* @return 给页面的提示
*/
String voidFee(String billCode, Long feeId);
}
@@ -0,0 +1,472 @@
package com.bms.price.service.impl;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.alibaba.fastjson2.JSON;
import com.bms.common.core.domain.entity.SysUser;
import com.bms.common.exception.ServiceException;
import com.bms.common.utils.DateUtils;
import com.bms.common.utils.SecurityUtils;
import com.bms.price.airbill.domain.SrvBillAirAllocFee;
import com.bms.price.airbill.domain.SrvBillAirAllocOrder;
import com.bms.price.airbill.domain.SrvBillAirFeeLog;
import com.bms.price.domain.PrcPayBill;
import com.bms.price.domain.PrcPayBillFee;
import com.bms.price.mapper.SrvBillAirAllocMapper;
import com.bms.price.mapper.SrvBillAirFeeMapper;
import com.bms.price.mapper.SrvBillAirMapper;
import com.bms.price.mapper.PrcPayBillFeeMapper;
import com.bms.price.service.ISrvBillAirAllocService;
/**
* 提单费用分摊。只处理标记为分摊、状态仍是未分摊的费用。
*/
@Service
public class SrvBillAirAllocServiceImpl implements ISrvBillAirAllocService
{
private static final int MAX_BILLS = 5;
private static final int BATCH_SIZE = 1000;
private static final String DIM_ORDER = "1";
private static final String DIM_WEIGHT = "2";
private static final String FEE_TYPE = "LADING";
private static final String FEE_SOURCE = "LADING";
private static final BigDecimal ZERO = BigDecimal.ZERO;
@Autowired
private SrvBillAirAllocMapper allocMapper;
@Autowired
private SrvBillAirMapper billMapper;
@Autowired
private SrvBillAirFeeMapper feeMapper;
@Autowired
private PrcPayBillFeeMapper payBillFeeMapper;
@Override
public List<SrvBillAirAllocFee> preview(List<String> billCodes)
{
List<String> codes = normalize(billCodes);
List<SrvBillAirAllocFee> fees = dedupe(allocMapper.selectPendingFees(codes));
fillWeight(fees);
Map<String, List<SrvBillAirAllocOrder>> orders = ordersOf(codes);
for (SrvBillAirAllocFee fee : fees)
{
List<SrvBillAirAllocOrder> rows = orders.getOrDefault(fee.getBillCode(), List.of());
fee.setOrderCount(rows.size());
fee.setBlockReason(blockReason(fee, rows));
}
return fees;
}
@Override
@Transactional(rollbackFor = Exception.class)
public String allocate(List<String> billCodes)
{
List<String> codes = normalize(billCodes);
List<SrvBillAirAllocFee> fees = dedupe(allocMapper.selectPendingFees(codes));
fillWeight(fees);
if (fees.isEmpty())
{
throw new ServiceException("所选提单没有需要分摊的费用");
}
Map<String, List<SrvBillAirAllocOrder>> orders = ordersOf(codes);
for (SrvBillAirAllocFee fee : fees)
{
List<SrvBillAirAllocOrder> rows = orders.getOrDefault(fee.getBillCode(), List.of());
fee.setOrderCount(rows.size());
String reason = blockReason(fee, rows);
if (!reason.isEmpty())
{
throw new ServiceException(fee.getBillCode() + " " + blankName(fee) + ":" + reason);
}
}
Map<String, String> payBillIds = payBillIds(orders);
List<PrcPayBillFee> batch = new ArrayList<>(BATCH_SIZE);
int lineCount = 0;
for (SrvBillAirAllocFee fee : fees)
{
List<SrvBillAirAllocOrder> rows = orders.get(fee.getBillCode());
fee.setOrderCount(rows.size());
boolean byWeight = DIM_WEIGHT.equals(fee.getAllocDim());
List<BigDecimal> shares = split(byWeight ? weightAmount(fee) : fee.getPrice(), rows, byWeight);
for (int i = 0; i < rows.size(); i++)
{
batch.add(toOrderFee(fee, rows.get(i), shares.get(i), payBillIds));
if (batch.size() == BATCH_SIZE)
{
lineCount += flush(batch);
}
}
}
lineCount += flush(batch);
Date now = DateUtils.getNowDate();
String operator = SecurityUtils.getUsername();
int updated = allocMapper.markAllocated(feeIds(fees), operator, now);
if (updated != fees.size())
{
throw new ServiceException("费用状态已变化,请重新打开后再分摊");
}
writeLogs(fees, operator, now);
return "已将 " + fees.size() + " 条费用分摊为 " + lineCount + " 笔订单费用";
}
/**
* 按重量分摊的费用,按提单号补上实际重量和打板重量。按订单数量分摊的不查、不填。
* 这两个值只参与计算,不返回给页面。
*/
private void fillWeight(List<SrvBillAirAllocFee> fees)
{
List<String> codes = new ArrayList<>();
for (SrvBillAirAllocFee fee : fees)
{
if (fee != null && DIM_WEIGHT.equals(fee.getAllocDim()) && fee.getBillCode() != null && !codes.contains(fee.getBillCode()))
{
codes.add(fee.getBillCode());
}
}
if (codes.isEmpty())
{
return;
}
Map<String, SrvBillAirAllocFee> weights = new LinkedHashMap<>();
for (SrvBillAirAllocFee row : allocMapper.selectLadingWeight(codes))
{
if (row != null && row.getBillCode() != null)
{
weights.put(row.getBillCode(), row);
}
}
for (SrvBillAirAllocFee fee : fees)
{
if (fee == null || !DIM_WEIGHT.equals(fee.getAllocDim()))
{
continue;
}
SrvBillAirAllocFee weight = weights.get(fee.getBillCode());
if (weight == null)
{
continue;
}
fee.setActualWeight(weight.getActualWeight());
fee.setBandBoxWeight(weight.getBandBoxWeight());
}
}
/** 提单号去重、去空白,并限制单次 5 条。 */
private List<String> normalize(List<String> billCodes)
{
LinkedHashSet<String> codes = new LinkedHashSet<>();
if (billCodes != null)
{
for (String raw : billCodes)
{
if (raw == null)
{
continue;
}
for (String part : raw.split("[,,\\s]+"))
{
if (!part.isBlank())
{
codes.add(part.trim());
}
}
}
}
if (codes.isEmpty())
{
throw new ServiceException("请勾选提单");
}
if (codes.size() > MAX_BILLS)
{
throw new ServiceException("单次最多处理5条提单");
}
for (String code : codes)
{
if (billMapper.selectSrvBillAirByBillCode(code) == null)
{
throw new ServiceException("提单不存在:" + code);
}
}
return new ArrayList<>(codes);
}
private Map<String, List<SrvBillAirAllocOrder>> ordersOf(List<String> codes)
{
Map<String, List<SrvBillAirAllocOrder>> grouped = new LinkedHashMap<>();
for (SrvBillAirAllocOrder order : allocMapper.selectOrders(codes))
{
if (order.getBillId() == null || order.getBillId().isBlank())
{
continue;
}
grouped.computeIfAbsent(order.getBillCode(), key -> new ArrayList<>()).add(order);
}
return grouped;
}
private Map<String, String> payBillIds(Map<String, List<SrvBillAirAllocOrder>> orders)
{
List<String> billIds = new ArrayList<>();
for (List<SrvBillAirAllocOrder> rows : orders.values())
{
for (SrvBillAirAllocOrder order : rows)
{
billIds.add(order.getBillId());
}
}
Map<String, String> payIds = new LinkedHashMap<>();
for (int from = 0; from < billIds.size(); from += BATCH_SIZE)
{
List<String> chunk = billIds.subList(from, Math.min(from + BATCH_SIZE, billIds.size()));
for (PrcPayBill payBill : allocMapper.selectPayBills(chunk))
{
if (payBill.getBillId() != null && payBill.getPayBillId() != null)
{
payIds.put(payBill.getBillId(), payBill.getPayBillId());
}
}
}
return payIds;
}
private String blockReason(SrvBillAirAllocFee fee, List<SrvBillAirAllocOrder> orders)
{
if (fee.getPrice() == null)
{
return "费用没有价格";
}
if (fee.getPrice().signum() < 0)
{
return "费用价格不能小于0";
}
if (fee.getCurrency() == null || fee.getCurrency().isBlank())
{
return "费用没有币种";
}
if (fee.getLadingId() == null || fee.getLadingId().isBlank())
{
return "提单缺少主键";
}
if (!DIM_ORDER.equals(fee.getAllocDim()) && !DIM_WEIGHT.equals(fee.getAllocDim()))
{
return "未选择分摊维度";
}
if (orders == null || orders.isEmpty())
{
return "提单下没有关联订单";
}
if (DIM_WEIGHT.equals(fee.getAllocDim()))
{
if (fee.getActualWeight() == null || fee.getActualWeight().signum() <= 0)
{
return "提单没有实际重量,无法按重量分摊";
}
if (fee.getBandBoxWeight() == null || fee.getBandBoxWeight().signum() <= 0)
{
return "干线没有打板重量,无法按重量分摊";
}
if (totalWeight(orders).signum() <= 0)
{
return "订单没有可用重量,无法按重量分摊";
}
}
return "";
}
private PrcPayBillFee toOrderFee(SrvBillAirAllocFee fee, SrvBillAirAllocOrder order, BigDecimal amount, Map<String, String> payBillIds)
{
PrcPayBillFee row = new PrcPayBillFee();
row.setFeeId(UUID.randomUUID().toString());
row.setBillId(order.getBillId());
row.setPayBillId(payBillIds.get(order.getBillId()));
row.setFeeType(FEE_TYPE);
row.setFeeName(blankName(fee));
row.setScId(fee.getId() == null ? 0L : fee.getId());
row.setFeeLevel(fee.getFeeLevel());
row.setAmount(amount);
row.setCurrency(fee.getCurrency().trim());
row.setRoundMode("HALF_UP");
row.setRemark(remark(fee));
row.setFeeSource(FEE_SOURCE);
row.setSourceId(fee.getLadingId().trim());
row.setIsDeleted("0");
return row;
}
private int flush(List<PrcPayBillFee> batch)
{
if (batch.isEmpty())
{
return 0;
}
int rows = batch.size();
payBillFeeMapper.insertBatch(batch);
batch.clear();
return rows;
}
private void writeLogs(List<SrvBillAirAllocFee> fees, String operator, Date now)
{
SysUser user = SecurityUtils.getLoginUser().getUser();
String name = user.getNickName() == null || user.getNickName().isBlank() ? operator : user.getNickName();
for (SrvBillAirAllocFee fee : fees)
{
SrvBillAirFeeLog log = new SrvBillAirFeeLog();
log.setBillCode(fee.getBillCode());
log.setFeeId(fee.getId());
log.setOperationType(SrvBillAirFeeLog.UPDATE);
log.setSummary("分摊到 " + fee.getOrderCount() + " 票订单");
log.setChangeContent(JSON.toJSONString(List.of(
new SrvBillAirFeeLog.FieldChange("billFeeStatus", "费用状态", "未分摊", "已分摊"))));
log.setCreateBy(operator);
log.setCreateByName(name);
log.setCreateTime(now);
feeMapper.insertLog(log);
}
}
/**
* 按重量分摊时,先算出这一票提单实际可分摊的金额。
* 百分比 = 提单实际重量 srv_bill_air.actual_weight / 干线打板重量 srv_bill_air_flight.band_box_weight。
* 可分摊金额 = 费用价格 × 百分比,保留两位,四舍五入。
* 调用前须已确认实际重量和打板重量都大于 0。
*
* @param fee 待分摊的提单费用,带有实际重量和打板重量
* @return 按百分比算出的可分摊金额
*/
private static BigDecimal weightAmount(SrvBillAirAllocFee fee)
{
BigDecimal price = fee.getPrice().setScale(2, RoundingMode.HALF_UP);
return price.multiply(fee.getActualWeight()).divide(fee.getBandBoxWeight(), 2, RoundingMode.HALF_UP);
}
/**
* 把金额拆到每一票订单,结果保留两位。
* 按订单数量:每一票先向下取整,差额补在最后一票,合计等于费用价格。
* 按重量:传入的金额已是「费用价格 × 提单实际重量 / 干线打板重量」。
* 再按订单重量占这些订单总重量的比例分摊;前面的订单向下取整,差额补在最后一票有重量的订单上,合计等于这笔金额。
* 订单重量优先取计费重,没有计费重时取实际重量,再没有取收货重量。
*
* @param price 本次要拆开的金额。按重量时是百分比计算后的可分摊金额
* @param orders 该提单下的订单
* @param byWeight true 按订单重量占比,false 按订单数量均分
* @return 与订单顺序一致的分摊金额
*/
private static List<BigDecimal> split(BigDecimal price, List<SrvBillAirAllocOrder> orders, boolean byWeight)
{
BigDecimal total = price.setScale(2, RoundingMode.HALF_UP);
int count = orders.size();
List<BigDecimal> shares = new ArrayList<>(count);
if (!byWeight)
{
BigDecimal each = total.divide(BigDecimal.valueOf(count), 2, RoundingMode.DOWN);
BigDecimal used = ZERO;
for (int i = 0; i < count - 1; i++)
{
shares.add(each);
used = used.add(each);
}
shares.add(total.subtract(used));
return shares;
}
BigDecimal sum = totalWeight(orders);
int last = count - 1;
for (int i = count - 1; i >= 0; i--)
{
if (weight(orders.get(i)).signum() > 0)
{
last = i;
break;
}
}
BigDecimal used = ZERO;
for (int i = 0; i < count; i++)
{
if (i == last)
{
shares.add(null);
continue;
}
BigDecimal share = total.multiply(weight(orders.get(i))).divide(sum, 2, RoundingMode.DOWN);
shares.add(share);
used = used.add(share);
}
shares.set(last, total.subtract(used));
return shares;
}
private static BigDecimal totalWeight(List<SrvBillAirAllocOrder> orders)
{
BigDecimal sum = ZERO;
for (SrvBillAirAllocOrder order : orders)
{
sum = sum.add(weight(order));
}
return sum;
}
private static BigDecimal weight(SrvBillAirAllocOrder order)
{
return order.getChargeWeight() == null ? ZERO : order.getChargeWeight();
}
private static List<SrvBillAirAllocFee> dedupe(List<SrvBillAirAllocFee> fees)
{
Map<Long, SrvBillAirAllocFee> unique = new LinkedHashMap<>();
if (fees != null)
{
for (SrvBillAirAllocFee fee : fees)
{
if (fee != null && fee.getId() != null)
{
unique.putIfAbsent(fee.getId(), fee);
}
}
}
return new ArrayList<>(unique.values());
}
private static List<Long> feeIds(List<SrvBillAirAllocFee> fees)
{
List<Long> ids = new ArrayList<>();
for (SrvBillAirAllocFee fee : fees)
{
ids.add(fee.getId());
}
return ids;
}
private static String blankName(SrvBillAirAllocFee fee)
{
if (fee.getFeeName() != null && !fee.getFeeName().isBlank())
{
return fee.getFeeName().trim();
}
return "提单费用";
}
private static String remark(SrvBillAirAllocFee fee)
{
String dim = DIM_WEIGHT.equals(fee.getAllocDim()) ? "重量" : "订单";
return "提单" + fee.getBillCode() + "按" + dim + "分摊";
}
}
@@ -22,6 +22,7 @@ import com.bms.price.airbill.domain.SrvBillAirFee;
import com.bms.price.airbill.domain.SrvBillAirFeeLog;
import com.bms.price.airbill.domain.SrvBillAirFeeSave;
import com.bms.price.domain.PrcSurcharge;
import com.bms.price.mapper.PrcPayBillFeeMapper;
import com.bms.price.mapper.SrvBillAirFeeMapper;
import com.bms.price.mapper.SrvBillAirMapper;
import com.bms.price.service.IPrcSurchargeService;
@@ -42,11 +43,18 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
private static final String STATUS_DICT = "bill_fee_status";
private static final String STATUS_ALLOCATED = "1";
private static final String STATUS_CONFIRMED = "3";
private static final String CURRENCY_DICT = "bms_currency";
@Autowired
private SrvBillAirFeeMapper feeMapper;
@Autowired
private PrcPayBillFeeMapper payBillFeeMapper;
@Autowired
private IPrcSurchargeService surchargeService;
@@ -121,6 +129,10 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
{
throw new ServiceException("费用已删除或不属于该提单");
}
if (locked(current))
{
throw new ServiceException("已分摊或已确定的费用不能删除,请使用作废");
}
int rows = feeMapper.deleteFee(current.getId(), billCode, operator.account, now);
if (rows > 0)
{
@@ -143,6 +155,10 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
{
throw new ServiceException("费用已删除或不属于该提单");
}
if (locked(current))
{
throw new ServiceException("已分摊或已确定的费用不能修改");
}
SrvBillAirFee next = normalize(incoming, levelLabels, allocLabels, dimLabels, currencyLabels);
next.setBillFeeStatus(current.getBillFeeStatus() == null || current.getBillFeeStatus().isBlank() ? "0" : current.getBillFeeStatus());
Map<String, String> feeNames = feeNames(current, next);
@@ -187,11 +203,90 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
return "已保存:新增 " + inserted + " 条,修改 " + updated + " 条,删除 " + removed + " 条";
}
@Override
@Transactional(rollbackFor = Exception.class)
public String confirm(String billCode, Long feeId)
{
String code = requireBill(billCode);
if (feeId == null)
{
throw new ServiceException("请选择要确定的费用");
}
SrvBillAirFee current = liveFee(feeId, code);
if (current == null)
{
throw new ServiceException("费用已删除或不属于该提单");
}
if (!STATUS_ALLOCATED.equals(current.getBillFeeStatus()))
{
throw new ServiceException("只有已分摊的费用可以确定");
}
Operator operator = operator();
Date now = DateUtils.getNowDate();
if (feeMapper.confirmFee(feeId, code, operator.account, now) != 1)
{
throw new ServiceException("费用状态已变化,请刷新后再确定");
}
writePlainLog(code, feeId, SrvBillAirFeeLog.CONFIRM, "确定费用", operator, now);
return "费用已确定";
}
@Override
@Transactional(rollbackFor = Exception.class)
public String voidFee(String billCode, Long feeId)
{
String code = requireBill(billCode);
if (feeId == null)
{
throw new ServiceException("请选择要作废的费用");
}
SrvBillAirFee current = liveFee(feeId, code);
if (current == null)
{
throw new ServiceException("费用已删除或不属于该提单");
}
if (!locked(current))
{
throw new ServiceException("只有已分摊或已确定的费用可以作废");
}
String billedOrderNo = payBillFeeMapper.selectBilledOrderNo(feeId);
if (billedOrderNo != null)
{
String orderNo = billedOrderNo.trim();
if (orderNo.isEmpty())
{
orderNo = "未知订单";
}
throw new ServiceException("该费用存在单笔或多笔订单已出账单-如:" + orderNo + ",不可作废");
}
Operator operator = operator();
Date now = DateUtils.getNowDate();
payBillFeeMapper.markLadingFeeDeleted(feeId);
if (feeMapper.voidFee(feeId, code, operator.account, now) != 1)
{
throw new ServiceException("费用状态已变化,请刷新后再作废");
}
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, "币种");
writeLog(code, feeId, SrvBillAirFeeLog.VOID, current, null, feeNames(current, null),
levelLabels, allocLabels, dimLabels, statusLabels, currencyLabels, operator, now);
return "费用已作废";
}
private SrvBillAirFee liveFee(Long id, String billCode)
{
return feeMapper.selectLive(id, billCode);
}
private static boolean locked(SrvBillAirFee fee)
{
String status = fee == null ? "" : text(fee.getBillFeeStatus());
return STATUS_ALLOCATED.equals(status) || STATUS_CONFIRMED.equals(status);
}
private SrvBillAirFee normalize(SrvBillAirFee raw, Map<String, String> levelLabels,
Map<String, String> allocLabels, Map<String, String> dimLabels, Map<String, String> currencyLabels)
{
@@ -270,6 +365,22 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
return fee;
}
/**
* 费用日志组装
* @param billCode
* @param feeId
* @param operation
* @param before
* @param after
* @param feeLabels
* @param levelLabels
* @param allocLabels
* @param dimLabels
* @param statusLabels
* @param currencyLabels
* @param operator
* @param now
*/
private void writeLog(String billCode, Long feeId, String operation, SrvBillAirFee before, SrvBillAirFee after,
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)
@@ -287,6 +398,21 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
feeMapper.insertLog(log);
}
/** 确定费用不比较字段,更改前和更改后都留空。 */
private void writePlainLog(String billCode, Long feeId, String operation, String summary, Operator operator, Date now)
{
SrvBillAirFeeLog log = new SrvBillAirFeeLog();
log.setBillCode(billCode);
log.setFeeId(feeId);
log.setOperationType(operation);
log.setSummary(summary);
log.setChangeContent("[]");
log.setCreateBy(operator.account);
log.setCreateByName(operator.name);
log.setCreateTime(now);
feeMapper.insertLog(log);
}
private static List<SrvBillAirFeeLog.FieldChange> diff(SrvBillAirFee before, SrvBillAirFee after,
Map<String, String> feeLabels, Map<String, String> levelLabels, Map<String, String> allocLabels,
Map<String, String> dimLabels, Map<String, String> statusLabels, Map<String, String> currencyLabels)
@@ -323,7 +449,9 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
private static String summary(String operation, List<SrvBillAirFeeLog.FieldChange> changes)
{
String action = SrvBillAirFeeLog.INSERT.equals(operation) ? "新增费用"
: SrvBillAirFeeLog.DELETE.equals(operation) ? "删除费用" : "修改费用";
: SrvBillAirFeeLog.DELETE.equals(operation) ? "删除费用"
: SrvBillAirFeeLog.CONFIRM.equals(operation) ? "确定费用"
: SrvBillAirFeeLog.VOID.equals(operation) ? "作废费用" : "修改费用";
StringBuilder text = new StringBuilder(action);
for (SrvBillAirFeeLog.FieldChange change : changes)
{
@@ -11,7 +11,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="feeType" column="fee_type" />
<result property="feeName" column="fee_name" />
<result property="scId" column="sc_id" />
<result property="feeLevel" column="fee_level" />
<result property="amount" column="amount" />
<result property="feeSource" column="fee_source" />
<result property="currency" column="currency" />
<result property="roundMode" column="round_mode" />
<result property="remark" column="remark" />
@@ -21,8 +23,8 @@ 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, amount, currency, round_mode, remark,
source_id, is_deleted, create_time
select 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}
and is_deleted = '0'
@@ -31,25 +33,46 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<insert id="insertPrcPayBillFee" parameterType="PrcPayBillFee">
insert into prc_pay_bill_fee (
fee_id, bill_id, pay_bill_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark,
source_id, is_deleted, create_time
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
) values (
#{feeId}, #{billId}, #{payBillId}, #{feeType}, #{feeName}, #{scId}, #{amount}, #{currency}, #{roundMode}, #{remark},
#{sourceId}, ifnull(#{isDeleted}, '0'), sysdate()
#{feeId}, #{billId}, #{payBillId}, #{feeType}, #{feeName}, #{scId}, #{feeLevel}, #{amount}, #{currency}, #{roundMode}, #{remark},
ifnull(#{feeSource}, 'CALC'), #{sourceId}, ifnull(#{isDeleted}, '0'), sysdate()
)
</insert>
<insert id="insertBatch" parameterType="java.util.List">
insert into prc_pay_bill_fee (
fee_id, bill_id, pay_bill_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark,
source_id, is_deleted, create_time
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
) values
<foreach collection="list" item="item" separator=",">
(#{item.feeId}, #{item.billId}, #{item.payBillId}, #{item.feeType}, #{item.feeName}, #{item.scId}, #{item.amount},
#{item.currency}, #{item.roundMode}, #{item.remark}, #{item.sourceId}, ifnull(#{item.isDeleted}, '0'), sysdate())
(#{item.feeId}, #{item.billId}, #{item.payBillId}, #{item.feeType}, #{item.feeName}, #{item.scId}, #{item.feeLevel},
#{item.amount}, #{item.currency}, #{item.roundMode}, #{item.remark}, ifnull(#{item.feeSource}, 'CALC'),
#{item.sourceId}, ifnull(#{item.isDeleted}, '0'), sysdate())
</foreach>
</insert>
<select id="selectBilledOrderNo" resultType="String">
select e.order_no
from prc_pay_bill_fee f
inner join prc_order_bill_ext e on e.bill_id = f.bill_id
where f.fee_source = 'LADING'
and f.sc_id = #{feeId}
and ifnull(f.is_deleted, '0') = '0'
and e.is_meet = 1
order by e.order_no
limit 1
</select>
<update id="markLadingFeeDeleted">
update prc_pay_bill_fee
set is_deleted = '1'
where fee_source = 'LADING'
and sc_id = #{feeId}
and ifnull(is_deleted, '0') = '0'
</update>
<delete id="deleteByBillId" parameterType="String">
delete from prc_pay_bill_fee
where bill_id = #{billId}
@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bms.price.mapper.SrvBillAirAllocMapper">
<select id="selectPendingFees" resultType="com.bms.price.airbill.domain.SrvBillAirAllocFee">
select f.id, f.bill_code as billCode, a.guid as ladingId, f.fee_level as feeLevel,
f.bill_fee_status as billFeeStatus, sc.sc_name as feeName, f.price, f.currency,
f.alloc_dim as allocDim
from srv_bill_air_fee f
left join srv_bill_air a on a.bill_code = f.bill_code
left join prc_surcharge sc on sc.sc_id = f.fee_type and ifnull(sc.is_del, '0') = '0'
where f.del_flag = '0'
and f.alloc_flag = '1'
and f.bill_fee_status = '0'
and f.bill_code in
<foreach collection="billCodes" item="code" open="(" separator="," close=")">#{code}</foreach>
order by f.bill_code, f.id
</select>
<select id="selectLadingWeight" resultType="com.bms.price.airbill.domain.SrvBillAirAllocFee">
select a.bill_code as billCode, a.actual_weight as actualWeight, fl.band_box_weight as bandBoxWeight
from srv_bill_air a
left join srv_bill_air_flight fl on fl.bill_code = a.bill_code
where a.bill_code in
<foreach collection="billCodes" item="code" open="(" separator="," close=")">#{code}</foreach>
</select>
<select id="selectOrders" resultType="com.bms.price.airbill.domain.SrvBillAirAllocOrder">
select trim(e.air_no_old) as billCode, e.order_no as orderNo, e.bill_id as billId,
case
when ifnull(b.charge_weight, 0) &gt; 0 then b.charge_weight
when ifnull(b.actual_weight, 0) &gt; 0 then b.actual_weight
when ifnull(b.receive_weight, 0) &gt; 0 then b.receive_weight
else 0
end as chargeWeight
from prc_order_bill_ext e
inner join prc_order_bill b on b.bill_id = e.bill_id
where trim(e.air_no_old) in
<foreach collection="billCodes" item="code" open="(" separator="," close=")">#{code}</foreach>
order by e.air_no_old, e.order_no, e.bill_id
</select>
<select id="selectPayBills" resultType="com.bms.price.domain.PrcPayBill">
select bill_id as billId, pay_bill_id as payBillId
from prc_pay_bill
where bill_id in
<foreach collection="billIds" item="id" open="(" separator="," close=")">#{id}</foreach>
</select>
<update id="markAllocated">
update srv_bill_air_fee
set bill_fee_status = '1',
update_by = #{updateBy},
update_time = #{updateTime}
where del_flag = '0'
and alloc_flag = '1'
and bill_fee_status = '0'
and id in
<foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>
</update>
</mapper>
@@ -12,6 +12,7 @@
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'
and ifnull(f.bill_fee_status, '0') &lt;&gt; '2'
order by f.id
</select>
@@ -26,6 +27,7 @@
where f.id = #{id}
and f.bill_code = #{billCode}
and f.del_flag = '0'
and ifnull(f.bill_fee_status, '0') &lt;&gt; '2'
</select>
<insert id="insertFee" useGeneratedKeys="true" keyProperty="id">
@@ -52,6 +54,30 @@
where id = #{id}
and bill_code = #{billCode}
and del_flag = '0'
and ifnull(bill_fee_status, '0') not in ('1', '3')
</update>
<update id="confirmFee">
update srv_bill_air_fee
set bill_fee_status = '3',
update_by = #{updateBy},
update_time = #{updateTime}
where id = #{id}
and bill_code = #{billCode}
and del_flag = '0'
and bill_fee_status = '1'
</update>
<update id="voidFee">
update srv_bill_air_fee
set del_flag = '1',
bill_fee_status = '2',
update_by = #{updateBy},
update_time = #{updateTime}
where id = #{id}
and bill_code = #{billCode}
and del_flag = '0'
and bill_fee_status in ('1', '3')
</update>
<update id="deleteFee">
@@ -62,6 +88,7 @@
where id = #{id}
and bill_code = #{billCode}
and del_flag = '0'
and ifnull(bill_fee_status, '0') not in ('1', '3')
</update>
<insert id="insertLog" useGeneratedKeys="true" keyProperty="logId">