客户管理记录修改日志

This commit is contained in:
luojiaquan
2026-09-30 16:00:06 +08:00
parent 998aac74c8
commit 0145b24193
10 changed files with 652 additions and 6 deletions
@@ -0,0 +1,80 @@
package com.bms.customer.customerinfo.domain;
import java.util.List;
import lombok.Data;
import com.bms.common.core.domain.BaseEntity;
/**
* 客户业务操作日志对象 ctr_customer_log
*
* @author bms
* @date 2026-09-30
*/
@Data
public class BmsCustomerLog extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 操作类型:新建 */
public static final String OP_INSERT = "1";
/** 操作类型:修改 */
public static final String OP_UPDATE = "2";
/** 操作类型:删除 */
public static final String OP_DELETE = "3";
/** 日志ID */
private Long logId;
/** 客户ID */
private Long customerId;
/** 客户编码(操作时快照) */
private String customerCode;
/** 客户名称(操作时快照) */
private String customerName;
/** 操作类型(1新建 2修改 3删除) */
private String operationType;
/** 变更明细JSON(落库内容):[{field,fieldLabel,oldValue,newValue}] */
private String changeContent;
/** 操作人姓名 */
private String createByName;
/** 变更明细(由 changeContent 解析,供前端直接展示) */
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,30 @@
package com.bms.customer.customerinfo.mapper;
import java.util.List;
import com.bms.customer.customerinfo.domain.BmsCustomerLog;
/**
* 客户业务操作日志Mapper接口
*
* @author bms
* @date 2026-09-30
*/
public interface BmsCustomerLogMapper
{
/**
* 新增客户操作日志
*
* @param customerLog 日志
* @return 结果
*/
public int insertBmsCustomerLog(BmsCustomerLog customerLog);
/**
* 按客户查询全部业务日志(时间倒序)
*
* @param customerId 客户ID
* @return 日志列表
*/
public List<BmsCustomerLog> selectBmsCustomerLogList(Long customerId);
}
@@ -0,0 +1,35 @@
package com.bms.customer.customerinfo.service;
import java.util.List;
import com.bms.customer.customerinfo.domain.BmsCustomer;
import com.bms.customer.customerinfo.domain.BmsCustomerLog;
/**
* 客户业务操作日志Service接口
*
* @author bms
* @date 2026-09-30
*/
public interface IBmsCustomerLogService
{
/**
* 记录新建客户日志(含建档时的字段初始值)
*/
void recordInsert(BmsCustomer customer);
/**
* 记录修改客户日志(字段级 修改前→修改后,无实际变更时不记录)
*/
void recordUpdate(BmsCustomer oldCustomer, BmsCustomer newCustomer);
/**
* 记录删除客户日志
*/
void recordDelete(BmsCustomer customer);
/**
* 查询某客户的全部业务日志(时间倒序,含已解析的变更明细)
*/
List<BmsCustomerLog> selectByCustomerId(Long customerId);
}
@@ -5,6 +5,7 @@ import com.bms.customer.customerinfo.domain.BmsCustomer;
import com.bms.customer.customerinfo.domain.BmsCustomerAttachment;
import com.bms.customer.customerinfo.domain.BmsCustomerCompany;
import com.bms.customer.customerinfo.domain.BmsCustomerContact;
import com.bms.customer.customerinfo.domain.BmsCustomerLog;
/**
* 客户主Service接口
@@ -22,6 +23,14 @@ public interface IBmsCustomerService
*/
public BmsCustomer selectBmsCustomerByCustomerId(Long customerId);
/**
* 查询某客户的全部业务操作日志(时间倒序)
*
* @param customerId 客户ID
* @return 业务日志列表
*/
public List<BmsCustomerLog> selectCustomerLogList(Long customerId);
/**
* 查询客户主列表
*
@@ -0,0 +1,263 @@
package com.bms.customer.customerinfo.service.impl;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import com.alibaba.fastjson2.JSON;
import com.bms.common.core.domain.entity.SysUser;
import com.bms.common.utils.DateUtils;
import com.bms.common.utils.SecurityUtils;
import com.bms.customer.customerinfo.domain.BmsCustomer;
import com.bms.customer.customerinfo.domain.BmsCustomerLog;
import com.bms.customer.customerinfo.mapper.BmsCustomerLogMapper;
import com.bms.customer.customerinfo.service.IBmsCustomerLogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* 客户业务操作日志Service实现
*
* @author bms
* @date 2026-09-30
*/
@Service
public class BmsCustomerLogServiceImpl implements IBmsCustomerLogService
{
@Autowired
private BmsCustomerLogMapper bmsCustomerLogMapper;
/** 业务类型字典 */
private static final Map<String, String> CUSTOMER_TYPE_MAP = Map.of(
"0", "直客", "1", "同行", "2", "集运");
/** 启用状态字典 */
private static final Map<String, String> ENABLE_MAP = Map.of("0", "正常", "1", "停用");
/** 结算方式字典 */
private static final Map<String, String> SETTLEMENT_TYPE_MAP = Map.of(
"0", "月结", "1", "周结", "2", "日结");
/**
* 需要记录日志的主表字段(顺序即展示顺序)。
* requireNonBlank 为 true 的字段对应 update SQL 的 &lt;if test="x!=null and x!=''"&gt;,
* 其余动态字段仅在新值非 null 时落库;业务员/结算员/客服为无条件写入。
*/
private static final List<FieldDef> FIELD_DEFS = List.of(
new FieldDef("customerCode", "客户编码", BmsCustomer::getCustomerCode, true, Function.identity()),
new FieldDef("customerName", "客户名称", BmsCustomer::getCustomerName, true, Function.identity()),
new FieldDef("customerCodeDw", "对外代码", BmsCustomer::getCustomerCodeDw, false, Function.identity()),
new FieldDef("creditCode", "统一社会信用代码", BmsCustomer::getCreditCode, false, Function.identity()),
new FieldDef("customerType", "业务类型", BmsCustomer::getCustomerType, false, CUSTOMER_TYPE_MAP::get),
new FieldDef("isEnable", "启用状态", BmsCustomer::getIsEnable, false, ENABLE_MAP::get),
new FieldDef("salesUserId", "业务员", c -> userDisplay(c.getSalesUserId(), c.getSalesUser()), false, Function.identity()),
new FieldDef("ed", "资金额度(元)", BmsCustomer::getEd, false, BmsCustomerLogServiceImpl::numText),
new FieldDef("zq", "回款账期(天)", BmsCustomer::getZq, false, BmsCustomerLogServiceImpl::numText),
new FieldDef("settlementUserId", "结算员", c -> userDisplay(c.getSettlementUserId(), c.getSettlementUser()), false, Function.identity()),
new FieldDef("serviceUserId", "客服", c -> userDisplay(c.getServiceUserId(), c.getServiceUser()), false, Function.identity()),
new FieldDef("settlementType", "结算方式", BmsCustomer::getSettlementType, false, SETTLEMENT_TYPE_MAP::get),
new FieldDef("accountBalance", "账户余额(元)", BmsCustomer::getAccountBalance, false, BmsCustomerLogServiceImpl::numText),
new FieldDef("foreignCurrencyBalance", "外币余额(元)", BmsCustomer::getForeignCurrencyBalance, false, BmsCustomerLogServiceImpl::numText),
new FieldDef("weightThreshold", "重量阈值(kg)", BmsCustomer::getWeightThreshold, false, BmsCustomerLogServiceImpl::numText),
new FieldDef("throwRatio", "抛比(%)", BmsCustomer::getThrowRatio, false, BmsCustomerLogServiceImpl::numText),
new FieldDef("productCodes", "绑定产品", BmsCustomer::getProductCodes, false, Function.identity()),
new FieldDef("remark", "备注", BmsCustomer::getRemark, false, Function.identity()));
@Override
public void recordInsert(BmsCustomer customer)
{
if (customer == null || customer.getCustomerId() == null)
{
return;
}
List<BmsCustomerLog.FieldChange> changes = new ArrayList<>();
for (FieldDef def : FIELD_DEFS)
{
String value = def.display(customer);
if (value != null && !value.isEmpty())
{
changes.add(new BmsCustomerLog.FieldChange(def.field, def.label, "", value));
}
}
saveLog(customer, BmsCustomerLog.OP_INSERT, changes);
}
@Override
public void recordUpdate(BmsCustomer oldCustomer, BmsCustomer newCustomer)
{
if (oldCustomer == null || newCustomer == null || newCustomer.getCustomerId() == null)
{
return;
}
List<BmsCustomerLog.FieldChange> changes = new ArrayList<>();
for (FieldDef def : FIELD_DEFS)
{
// 与 update SQL 的动态条件保持一致:未提交(null/空串)的字段本次不落库,不参与比对
Object raw = def.rawGetter.apply(newCustomer);
if (raw == null)
{
continue;
}
if (def.requireNonBlank && raw.toString().trim().isEmpty())
{
continue;
}
String oldValue = def.display(oldCustomer);
String newValue = def.display(newCustomer);
if (!sameValue(def.rawGetter.apply(oldCustomer), raw))
{
changes.add(new BmsCustomerLog.FieldChange(def.field, def.label,
oldValue == null ? "" : oldValue, newValue == null ? "" : newValue));
}
}
if (changes.isEmpty())
{
return;
}
// 日志快照以修改后的客户编码/名称为准
saveLog(newCustomer, BmsCustomerLog.OP_UPDATE, changes);
}
@Override
public void recordDelete(BmsCustomer customer)
{
if (customer == null || customer.getCustomerId() == null)
{
return;
}
saveLog(customer, BmsCustomerLog.OP_DELETE, null);
}
@Override
public List<BmsCustomerLog> selectByCustomerId(Long customerId)
{
List<BmsCustomerLog> list = bmsCustomerLogMapper.selectBmsCustomerLogList(customerId);
for (BmsCustomerLog log : list)
{
if (log.getChangeContent() != null && !log.getChangeContent().isEmpty())
{
log.setChanges(JSON.parseArray(log.getChangeContent(), BmsCustomerLog.FieldChange.class));
}
}
return list;
}
private void saveLog(BmsCustomer customer, String operationType, List<BmsCustomerLog.FieldChange> changes)
{
BmsCustomerLog log = new BmsCustomerLog();
log.setCustomerId(customer.getCustomerId());
log.setCustomerCode(customer.getCustomerCode());
log.setCustomerName(customer.getCustomerName());
log.setOperationType(operationType);
if (changes != null)
{
log.setChangeContent(JSON.toJSONString(changes));
}
try
{
SysUser user = SecurityUtils.getLoginUser().getUser();
log.setCreateBy(user.getUserName());
log.setCreateByName(user.getNickName());
}
catch (Exception ignored)
{
// 无登录上下文(如系统任务)时不阻断业务
}
log.setCreateTime(DateUtils.getNowDate());
bmsCustomerLogMapper.insertBmsCustomerLog(log);
}
/** 人员字段展示:姓名(工号),缺姓名时只显示工号 */
private static String userDisplay(String code, String name)
{
String c = trimToNull(code);
String n = trimToNull(name);
if (c == null && n == null)
{
return "";
}
if (c == null)
{
return n;
}
if (n == null || n.equals(c))
{
return c;
}
return n + "(" + c + ")";
}
private static String numText(Object num)
{
return num == null ? "" : num.toString();
}
private static String trimToNull(String s)
{
if (s == null)
{
return null;
}
String t = s.trim();
return t.isEmpty() ? null : t;
}
/** 数值按大小比较(规避 BigDecimal 精度差异),其余按字符串比较 */
private static boolean sameValue(Object a, Object b)
{
if (a instanceof BigDecimal && b instanceof BigDecimal)
{
return ((BigDecimal) a).compareTo((BigDecimal) b) == 0;
}
return Objects.equals(a == null ? null : a.toString(), b == null ? null : b.toString());
}
/**
* 字段定义
*/
private static final class FieldDef
{
private final String field;
private final String label;
/** 取原始值(用于变更判断) */
private final Function<BmsCustomer, Object> rawGetter;
/** 动态写入时是否要求非空串(对应 update SQL 的 != '' 判断) */
private final boolean requireNonBlank;
/** 原始值 -> 字典翻译(查不到字典时回退原值) */
private final Function<String, String> dictTranslator;
private FieldDef(String field, String label, Function<BmsCustomer, ?> rawGetter,
boolean requireNonBlank, Function<String, String> dictTranslator)
{
this.field = field;
this.label = label;
this.rawGetter = rawGetter::apply;
this.requireNonBlank = requireNonBlank;
this.dictTranslator = dictTranslator;
}
/** 取日志展示值:人员/数字类直接是文本;字典类翻译失败回退原值 */
private String display(BmsCustomer c)
{
Object raw = rawGetter.apply(c);
if (raw == null)
{
return "";
}
String text = raw.toString();
if (text.isEmpty())
{
return "";
}
if (dictTranslator == null)
{
return text;
}
String translated = dictTranslator.apply(text);
return translated != null ? translated : text;
}
}
}
@@ -1,5 +1,6 @@
package com.bms.customer.customerinfo.service.impl;
import java.util.ArrayList;
import java.util.List;
import com.bms.common.annotation.DataScope;
@@ -14,6 +15,8 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.bms.customer.customerinfo.mapper.BmsCustomerMapper;
import com.bms.customer.customerinfo.domain.BmsCustomer;
import com.bms.customer.customerinfo.domain.BmsCustomerLog;
import com.bms.customer.customerinfo.service.IBmsCustomerLogService;
import com.bms.customer.customerinfo.service.IBmsCustomerService;
import com.bms.customer.customermoney.service.ICustomerMoneyService;
@@ -32,6 +35,9 @@ public class BmsCustomerServiceImpl implements IBmsCustomerService
@Autowired
private ICustomerMoneyService customerMoneyService;
@Autowired
private IBmsCustomerLogService customerLogService;
/**
* 查询客户主
*
@@ -58,9 +64,18 @@ public class BmsCustomerServiceImpl implements IBmsCustomerService
return bmsCustomer;
}
/**
* 查询某客户的全部业务操作日志(时间倒序)
*/
@Override
public List<BmsCustomerLog> selectCustomerLogList(Long customerId)
{
return customerLogService.selectByCustomerId(customerId);
}
/**
* 查询客户主列表
*
*
* @param bmsCustomer 客户主
* @return 客户主
*/
@@ -235,6 +250,8 @@ public class BmsCustomerServiceImpl implements IBmsCustomerService
saveBmsCustomerContacts(bmsCustomer.getCustomerId(), bmsCustomer.getContactList());
// 新建客户即初始化默认CNY账户:余额0、总额度1000 CNY,写INIT流水
customerMoneyService.initDefaultCnyAccount(bmsCustomer.getCustomerId());
// 业务日志:新建(含建档初始值)
customerLogService.recordInsert(bmsCustomer);
}
return rows;
}
@@ -252,6 +269,8 @@ public class BmsCustomerServiceImpl implements IBmsCustomerService
SysUser user = SecurityUtils.getLoginUser().getUser();
bmsCustomer.setUpdateTime(DateUtils.getNowDate());
bmsCustomer.setUpdateBy(user.getUserName());
// 修改前先取旧值快照,用于业务日志记录字段级变更
BmsCustomer oldCustomer = bmsCustomerMapper.selectBmsCustomerByCustomerId(bmsCustomer.getCustomerId());
// 仅重建客户档案模块的附件,收款水单等其他业务模块的附件保留不动
bmsCustomerMapper.deleteBmsCustomerProfileAttachmentByCustomerId(bmsCustomer.getCustomerId());
if (bmsCustomer.getAttachmentList() != null)
@@ -266,7 +285,13 @@ public class BmsCustomerServiceImpl implements IBmsCustomerService
saveBmsCustomerCompanies(bmsCustomer.getCustomerId(), bmsCustomer.getCompanyList());
// 联系人采用增量 upsert
saveBmsCustomerContacts(bmsCustomer.getCustomerId(), bmsCustomer.getContactList());
return bmsCustomerMapper.updateBmsCustomer(bmsCustomer);
int rows = bmsCustomerMapper.updateBmsCustomer(bmsCustomer);
// 业务日志:修改(字段级 修改前→修改后;无实际变更时不记录)
if (rows > 0)
{
customerLogService.recordUpdate(oldCustomer, bmsCustomer);
}
return rows;
}
/**
@@ -279,13 +304,31 @@ public class BmsCustomerServiceImpl implements IBmsCustomerService
@Override
public int deleteBmsCustomerByCustomerIds(Long[] customerIds)
{
// 删除前取客户快照,用于业务日志
List<BmsCustomer> snapshotList = new ArrayList<>();
for (Long customerId : customerIds)
{
BmsCustomer snapshot = bmsCustomerMapper.selectBmsCustomerByCustomerId(customerId);
if (snapshot != null)
{
snapshotList.add(snapshot);
}
}
for (Long customerId : customerIds)
{
bmsCustomerMapper.deleteBmsCustomerAttachmentByCustomerId(customerId);
bmsCustomerMapper.deleteBmsCustomerCompanyByCustomerId(customerId);
bmsCustomerMapper.deleteBmsCustomerContactByCustomerId(customerId);
}
return bmsCustomerMapper.deleteBmsCustomerByCustomerIds(customerIds);
int rows = bmsCustomerMapper.deleteBmsCustomerByCustomerIds(customerIds);
if (rows > 0)
{
for (BmsCustomer snapshot : snapshotList)
{
customerLogService.recordDelete(snapshot);
}
}
return rows;
}
/**
@@ -298,10 +341,16 @@ public class BmsCustomerServiceImpl implements IBmsCustomerService
@Override
public int deleteBmsCustomerByCustomerId(Long customerId)
{
BmsCustomer snapshot = bmsCustomerMapper.selectBmsCustomerByCustomerId(customerId);
bmsCustomerMapper.deleteBmsCustomerAttachmentByCustomerId(customerId);
bmsCustomerMapper.deleteBmsCustomerCompanyByCustomerId(customerId);
bmsCustomerMapper.deleteBmsCustomerContactByCustomerId(customerId);
return bmsCustomerMapper.deleteBmsCustomerByCustomerId(customerId);
int rows = bmsCustomerMapper.deleteBmsCustomerByCustomerId(customerId);
if (rows > 0 && snapshot != null)
{
customerLogService.recordDelete(snapshot);
}
return rows;
}
/**
@@ -0,0 +1,54 @@
<?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.customer.customerinfo.mapper.BmsCustomerLogMapper">
<resultMap type="com.bms.customer.customerinfo.domain.BmsCustomerLog" id="BmsCustomerLogResult">
<result property="logId" column="log_id" />
<result property="customerId" column="customer_id" />
<result property="customerCode" column="customer_code" />
<result property="customerName" column="customer_name" />
<result property="operationType" column="operation_type" />
<result property="changeContent" column="change_content" />
<result property="createBy" column="create_by" />
<result property="createByName" column="create_by_name" />
<result property="createTime" column="create_time" />
</resultMap>
<sql id="selectBmsCustomerLogVo">
select log_id, customer_id, customer_code, customer_name, operation_type, change_content,
create_by, create_by_name, create_time
from ctr_customer_log
</sql>
<insert id="insertBmsCustomerLog" parameterType="com.bms.customer.customerinfo.domain.BmsCustomerLog"
useGeneratedKeys="true" keyProperty="logId">
insert into ctr_customer_log
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="customerId != null">customer_id,</if>
<if test="customerCode != null">customer_code,</if>
<if test="customerName != null">customer_name,</if>
<if test="operationType != null">operation_type,</if>
<if test="changeContent != null">change_content,</if>
<if test="createBy != null">create_by,</if>
<if test="createByName != null">create_by_name,</if>
<if test="createTime != null">create_time,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="customerId != null">#{customerId},</if>
<if test="customerCode != null">#{customerCode},</if>
<if test="customerName != null">#{customerName},</if>
<if test="operationType != null">#{operationType},</if>
<if test="changeContent != null">#{changeContent},</if>
<if test="createBy != null">#{createBy},</if>
<if test="createByName != null">#{createByName},</if>
<if test="createTime != null">#{createTime},</if>
</trim>
</insert>
<select id="selectBmsCustomerLogList" parameterType="Long" resultMap="BmsCustomerLogResult">
<include refid="selectBmsCustomerLogVo"/>
where customer_id = #{customerId}
order by create_time desc, log_id desc
</select>
</mapper>