新增提单费用和费用日志

This commit is contained in:
liuminghui
2026-11-06 17:28:55 +08:00
committed by caojia
parent 6a02de8069
commit e0dc755ee3
13 changed files with 1115 additions and 1 deletions
@@ -0,0 +1,40 @@
package com.bms.price.airbill.domain;
import java.math.BigDecimal;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/**
* 空运提单费用明细 srv_bill_air_fee。
*/
@Data
public class SrvBillAirFee
{
private Long id;
private String billCode;
/** 字典 bms_fee_type */
private String feeType;
private BigDecimal price;
/** 字典 bms_currency */
private String currency;
private String remark;
private String createBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
private String updateBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTime;
/** 0未删除 1已删除 */
private String delFlag;
}
@@ -0,0 +1,64 @@
package com.bms.price.airbill.domain;
import java.util.Date;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/**
* 空运提单费用操作日志。
*/
@Data
public class SrvBillAirFeeLog
{
public static final String INSERT = "1";
public static final String UPDATE = "2";
public static final String DELETE = "3";
private Long logId;
private String billCode;
private Long feeId;
private String operationType;
private String summary;
private String changeContent;
private String createBy;
private String createByName;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
private List<FieldChange> changes;
@Data
public static class FieldChange
{
private String field;
private String fieldLabel;
private String oldValue;
private String newValue;
public FieldChange()
{
}
public FieldChange(String field, String fieldLabel, String oldValue, String newValue)
{
this.field = field;
this.fieldLabel = fieldLabel;
this.oldValue = oldValue;
this.newValue = newValue;
}
}
}
@@ -0,0 +1,20 @@
package com.bms.price.airbill.domain;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
/**
* 保存提单费用时只带有变动的行。未出现的费用保持原样。
*/
@Data
public class SrvBillAirFeeSave
{
private String billCode;
private List<SrvBillAirFee> inserts = new ArrayList<>();
private List<SrvBillAirFee> updates = new ArrayList<>();
private List<Long> deletes = new ArrayList<>();
}
@@ -0,0 +1,28 @@
package com.bms.price.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.bms.price.airbill.domain.SrvBillAirFee;
import com.bms.price.airbill.domain.SrvBillAirFeeLog;
/**
* 空运提单费用及操作日志。
*/
public interface SrvBillAirFeeMapper
{
List<SrvBillAirFee> selectByBillCode(String billCode);
/** 只取这一票提单上仍有效的费用,供修改和删除前核对。 */
SrvBillAirFee selectLive(@Param("id") Long id, @Param("billCode") String billCode);
int insertFee(SrvBillAirFee fee);
int updateFee(SrvBillAirFee fee);
int deleteFee(@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,19 @@
package com.bms.price.service;
import java.util.List;
import com.bms.price.airbill.domain.SrvBillAirFee;
import com.bms.price.airbill.domain.SrvBillAirFeeLog;
import com.bms.price.airbill.domain.SrvBillAirFeeSave;
/**
* 空运提单费用维护。
*/
public interface ISrvBillAirFeeService
{
List<SrvBillAirFee> listFees(String billCode);
List<SrvBillAirFeeLog> listLogs(String billCode);
/** 只写入有变动的行,并按行记录修改前后。返回给页面的提示。 */
String save(SrvBillAirFeeSave command);
}
@@ -0,0 +1,382 @@
package com.bms.price.service.impl;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
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.SysDictData;
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.PageUtils;
import com.bms.common.utils.SecurityUtils;
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.mapper.SrvBillAirFeeMapper;
import com.bms.price.mapper.SrvBillAirMapper;
import com.bms.price.service.ISrvBillAirFeeService;
import com.bms.system.service.ISysDictTypeService;
/**
* 提单费用只处理本次新增、修改、删除的行。没有变化的费用不更新。
*/
@Service
public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
{
private static final String FEE_DICT = "bms_fee_type";
private static final String CURRENCY_DICT = "bms_currency";
@Autowired
private SrvBillAirFeeMapper feeMapper;
@Autowired
private SrvBillAirMapper billMapper;
@Autowired
private ISysDictTypeService dictTypeService;
@Override
public List<SrvBillAirFee> listFees(String billCode)
{
requireBill(billCode);
return feeMapper.selectByBillCode(billCode.trim());
}
@Override
public List<SrvBillAirFeeLog> listLogs(String billCode)
{
requireBill(billCode);
PageUtils.startPage();
List<SrvBillAirFeeLog> logs = feeMapper.selectLogs(billCode.trim());
for (SrvBillAirFeeLog log : logs)
{
if (log.getChangeContent() != null && !log.getChangeContent().isBlank())
{
log.setChanges(JSON.parseArray(log.getChangeContent(), SrvBillAirFeeLog.FieldChange.class));
}
}
return logs;
}
@Override
@Transactional(rollbackFor = Exception.class)
public String save(SrvBillAirFeeSave command)
{
if (command == null)
{
throw new ServiceException("请提交要保存的费用");
}
String billCode = requireBill(command.getBillCode());
List<SrvBillAirFee> inserts = command.getInserts() == null ? List.of() : command.getInserts();
List<SrvBillAirFee> updates = command.getUpdates() == null ? List.of() : command.getUpdates();
List<Long> deletes = command.getDeletes() == null ? List.of() : command.getDeletes();
if (inserts.isEmpty() && updates.isEmpty() && deletes.isEmpty())
{
throw new ServiceException("没有需要保存的变更");
}
Map<String, String> feeLabels = dictLabels(FEE_DICT, "费用类型");
Map<String, String> currencyLabels = dictLabels(CURRENCY_DICT, "币种");
Operator operator = operator();
Date now = DateUtils.getNowDate();
Set<Long> seen = new HashSet<>();
int inserted = 0;
int updated = 0;
int removed = 0;
for (Long id : deletes)
{
if (id == null)
{
throw new ServiceException("要删除的费用缺少编号");
}
if (!seen.add(id))
{
throw new ServiceException("同一条费用不能重复提交");
}
SrvBillAirFee current = liveFee(id, billCode);
if (current == null)
{
throw new ServiceException("费用已删除或不属于该提单");
}
int rows = feeMapper.deleteFee(current.getId(), billCode, operator.account, now);
if (rows > 0)
{
writeLog(billCode, id, SrvBillAirFeeLog.DELETE, current, null, feeLabels, currencyLabels, operator, now);
removed++;
}
}
for (SrvBillAirFee incoming : updates)
{
if (incoming == null || incoming.getId() == null)
{
throw new ServiceException("要修改的费用缺少编号");
}
if (!seen.add(incoming.getId()))
{
throw new ServiceException("同一条费用不能重复提交");
}
SrvBillAirFee current = liveFee(incoming.getId(), billCode);
if (current == null)
{
throw new ServiceException("费用已删除或不属于该提单");
}
SrvBillAirFee next = normalize(incoming, feeLabels, currencyLabels);
List<SrvBillAirFeeLog.FieldChange> changes = diff(current, next, feeLabels, currencyLabels);
if (changes.isEmpty())
{
continue;
}
next.setId(current.getId());
next.setBillCode(billCode);
next.setUpdateBy(operator.account);
next.setUpdateTime(now);
if (feeMapper.updateFee(next) > 0)
{
writeLog(billCode, current.getId(), SrvBillAirFeeLog.UPDATE, current, next, feeLabels, currencyLabels, operator, now);
updated++;
}
}
for (SrvBillAirFee incoming : inserts)
{
if (incoming == null || blank(incoming.getFeeType()) && incoming.getPrice() == null && blank(incoming.getCurrency()) && blank(incoming.getRemark()))
{
continue;
}
SrvBillAirFee next = normalize(incoming, feeLabels, currencyLabels);
next.setId(null);
next.setBillCode(billCode);
next.setCreateBy(operator.account);
next.setCreateTime(now);
next.setUpdateBy(operator.account);
next.setUpdateTime(now);
feeMapper.insertFee(next);
writeLog(billCode, next.getId(), SrvBillAirFeeLog.INSERT, null, next, feeLabels, currencyLabels, operator, now);
inserted++;
}
if (inserted + updated + removed == 0)
{
throw new ServiceException("没有需要保存的变更");
}
return "已保存:新增 " + inserted + " 条,修改 " + updated + " 条,删除 " + removed + " 条";
}
private SrvBillAirFee liveFee(Long id, String billCode)
{
return feeMapper.selectLive(id, billCode);
}
private SrvBillAirFee normalize(SrvBillAirFee raw, Map<String, String> feeLabels, Map<String, String> currencyLabels)
{
String feeType = trim(raw.getFeeType());
String currency = trim(raw.getCurrency());
String remark = trim(raw.getRemark());
if (feeType.isEmpty())
{
throw new ServiceException("请选择费用项");
}
if (feeType.length() > 32 || !feeLabels.containsKey(feeType))
{
throw new ServiceException("费用项不在费用类型字典中");
}
if (raw.getPrice() == null)
{
throw new ServiceException("请填写价格");
}
if (raw.getPrice().signum() < 0)
{
throw new ServiceException("价格不能小于0");
}
if (raw.getPrice().precision() - raw.getPrice().scale() > 16)
{
throw new ServiceException("价格整数部分不能超过16位");
}
if (currency.isEmpty())
{
throw new ServiceException("请选择收费币种");
}
if (currency.length() > 16 || !currencyLabels.containsKey(currency))
{
throw new ServiceException("收费币种不在币种字典中");
}
if (remark.length() > 500)
{
throw new ServiceException("备注不能超过500字");
}
SrvBillAirFee fee = new SrvBillAirFee();
fee.setFeeType(feeType);
fee.setPrice(raw.getPrice());
fee.setCurrency(currency);
fee.setRemark(remark);
return fee;
}
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)
{
List<SrvBillAirFeeLog.FieldChange> changes = diff(before, after, feeLabels, currencyLabels);
SrvBillAirFeeLog log = new SrvBillAirFeeLog();
log.setBillCode(billCode);
log.setFeeId(feeId);
log.setOperationType(operation);
log.setSummary(cut(summary(operation, changes), 1000));
log.setChangeContent(JSON.toJSONString(changes));
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> currencyLabels)
{
List<SrvBillAirFeeLog.FieldChange> changes = new ArrayList<>();
addChange(changes, "feeType", "费用类型", label(feeLabels, value(before, SrvBillAirFee::getFeeType)),
label(feeLabels, value(after, SrvBillAirFee::getFeeType)));
addChange(changes, "price", "价格", priceText(before), priceText(after));
addChange(changes, "currency", "收费币种", label(currencyLabels, value(before, SrvBillAirFee::getCurrency)),
label(currencyLabels, value(after, SrvBillAirFee::getCurrency)));
addChange(changes, "remark", "备注", text(before == null ? null : before.getRemark()), text(after == null ? null : after.getRemark()));
return changes;
}
private static void addChange(List<SrvBillAirFeeLog.FieldChange> changes, String field, String fieldLabel, String oldValue, String newValue)
{
String left = oldValue == null ? "" : oldValue;
String right = newValue == null ? "" : newValue;
if (left.equals(right))
{
return;
}
changes.add(new SrvBillAirFeeLog.FieldChange(field, fieldLabel, left, right));
}
private static String summary(String operation, List<SrvBillAirFeeLog.FieldChange> changes)
{
String action = SrvBillAirFeeLog.INSERT.equals(operation) ? "新增费用"
: SrvBillAirFeeLog.DELETE.equals(operation) ? "删除费用" : "修改费用";
StringBuilder text = new StringBuilder(action);
for (SrvBillAirFeeLog.FieldChange change : changes)
{
text.append(";").append(change.getFieldLabel()).append(":")
.append(show(change.getOldValue())).append(" → ").append(show(change.getNewValue()));
}
return text.toString();
}
private Map<String, String> dictLabels(String dictType, String name)
{
List<SysDictData> rows = dictTypeService.selectDictDataByType(dictType);
Map<String, String> labels = new LinkedHashMap<>();
if (rows != null)
{
for (SysDictData row : rows)
{
if (row != null && row.getDictValue() != null && !row.getDictValue().isBlank())
{
labels.put(row.getDictValue().trim(), row.getDictLabel() == null ? row.getDictValue().trim() : row.getDictLabel().trim());
}
}
}
if (labels.isEmpty())
{
throw new ServiceException(name + "字典还没有数据");
}
return labels;
}
private String requireBill(String billCode)
{
if (billCode == null || billCode.isBlank())
{
throw new ServiceException("提单号不能为空");
}
String code = billCode.trim();
if (billMapper.selectSrvBillAirByBillCode(code) == null)
{
throw new ServiceException("提单不存在");
}
return code;
}
private static Operator operator()
{
SysUser user = SecurityUtils.getLoginUser().getUser();
String account = user.getUserName() == null ? "" : user.getUserName();
String name = user.getNickName() == null || user.getNickName().isBlank() ? account : user.getNickName();
return new Operator(account, name);
}
private static String value(SrvBillAirFee fee, java.util.function.Function<SrvBillAirFee, String> getter)
{
return fee == null ? "" : text(getter.apply(fee));
}
private static String priceText(SrvBillAirFee fee)
{
if (fee == null || fee.getPrice() == null)
{
return "";
}
return fee.getPrice().stripTrailingZeros().toPlainString();
}
private static String label(Map<String, String> labels, String value)
{
if (value == null || value.isBlank())
{
return "";
}
return labels.getOrDefault(value, value);
}
private static String show(String value)
{
return value == null || value.isBlank() ? "空" : value;
}
private static String text(String value)
{
return value == null ? "" : value.trim();
}
private static String trim(String value)
{
return value == null ? "" : value.trim();
}
private static boolean blank(String value)
{
return value == null || value.trim().isEmpty();
}
private static String cut(String value, int max)
{
if (value == null || value.length() <= max)
{
return value;
}
return value.substring(0, max);
}
private static final class Operator
{
private final String account;
private final String name;
private Operator(String account, String name)
{
this.account = account;
this.name = name;
}
}
}
@@ -0,0 +1,76 @@
<?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.SrvBillAirFeeMapper">
<select id="selectByBillCode" resultType="com.bms.price.airbill.domain.SrvBillAirFee">
select id, bill_code as billCode, fee_type as feeType, price, currency, remark,
create_by as createBy, create_time as createTime, update_by as updateBy,
update_time as updateTime, del_flag as delFlag
from srv_bill_air_fee
where bill_code = #{billCode}
and del_flag = '0'
order by id
</select>
<select id="selectLive" resultType="com.bms.price.airbill.domain.SrvBillAirFee">
select id, bill_code as billCode, fee_type as feeType, price, currency, remark,
create_by as createBy, create_time as createTime, update_by as updateBy,
update_time as updateTime, del_flag as delFlag
from srv_bill_air_fee
where id = #{id}
and bill_code = #{billCode}
and del_flag = '0'
</select>
<insert id="insertFee" useGeneratedKeys="true" keyProperty="id">
insert into srv_bill_air_fee (
bill_code, fee_type, price, currency, remark,
create_by, create_time, update_by, update_time, del_flag
) values (
#{billCode}, #{feeType}, #{price}, #{currency}, #{remark},
#{createBy}, #{createTime}, #{updateBy}, #{updateTime}, '0'
)
</insert>
<update id="updateFee">
update srv_bill_air_fee
set fee_type = #{feeType},
price = #{price},
currency = #{currency},
remark = #{remark},
update_by = #{updateBy},
update_time = #{updateTime}
where id = #{id}
and bill_code = #{billCode}
and del_flag = '0'
</update>
<update id="deleteFee">
update srv_bill_air_fee
set del_flag = '1',
update_by = #{updateBy},
update_time = #{updateTime}
where id = #{id}
and bill_code = #{billCode}
and del_flag = '0'
</update>
<insert id="insertLog" useGeneratedKeys="true" keyProperty="logId">
insert into srv_bill_air_fee_log (
bill_code, fee_id, operation_type, summary, change_content,
create_by, create_by_name, create_time
) values (
#{billCode}, #{feeId}, #{operationType}, #{summary}, #{changeContent},
#{createBy}, #{createByName}, #{createTime}
)
</insert>
<select id="selectLogs" resultType="com.bms.price.airbill.domain.SrvBillAirFeeLog">
select log_id as logId, bill_code as billCode, fee_id as feeId, operation_type as operationType,
summary, change_content as changeContent, create_by as createBy,
create_by_name as createByName, create_time as createTime
from srv_bill_air_fee_log
where bill_code = #{billCode}
order by log_id desc
</select>
</mapper>