数据变更日志详解
模块路径:
wemirr-platform-framework/diff-log-spring-boot-starter包路径:com.wemirr.framework.log.diff
概述
diff-log-spring-boot-starter 是一个操作审计/数据变更日志模块,提供:
- 自动记录数据变更(字段级差异对比)
- 支持 SpEL 表达式动态生成日志内容
- 操作人、IP、设备等上下文信息自动收集
- 灵活的日志模板配置
核心原理
工作流程
┌─────────────────────────────────────────────────────────────────┐
│ 请求流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 用户发起请求 │
│ │ │
│ ▼ │
│ 2. Controller 接收请求 │
│ │ │
│ ▼ │
│ 3. Service 方法执行 │
│ │ ├─ 查询旧数据: User oldUser = userMapper.selectById(id) │
│ │ ├─ 设置上下文: DiffLogContext.putDiffItem(oldUser, newUser)│
│ │ └─ 更新数据: userMapper.updateById(newUser) │
│ │
│ 4. @DiffLog 注解触发 AOP 拦截 │
│ │ ├─ 计算字段差异 │
│ │ ├─ 解析 SpEL 表达式 │
│ │ ├─ 收集上下文信息(用户、IP、设备) │
│ │ └─ 保存日志 │
│ │
└─────────────────────────────────────────────────────────────────┘目录结构
diff-log/
├── configuration/
│ └── DiffLogProperties.java # 配置属性
├── core/
│ ├── annotation/
│ │ ├── DiffLog.java # 日志注解
│ │ ├── DiffLogs.java # 重复注解
│ │ ├── DiffField.java # 字段注解
│ │ └── EnableDiffLog.java # 启用日志
│ └── context/
│ └── DiffLogContext.java # 日志上下文
├── domain/
│ ├── DiffLogInfo.java # 日志实体
│ ├── FieldChange.java # 字段变更记录
│ └── ChangeAction.java # 变更动作枚举
├── service/
│ ├── IDiffLogService.java # 日志服务接口
│ └── IDiffItemsToLogContentService.java # 差异计算服务
└── support/
└── aop/
├── DiffLogInterceptor.java # AOP 拦截器
└── DiffLogOperationSource.java # 操作源核心注解
1. @DiffLog - 日志注解
文件: DiffLog.java
java
@DiffLog(
group = "用户管理", // 业务分组
tag = "编辑用户", // 操作标签
businessKey = "{{#id}}", // 业务ID(SpEL)
success = "更新用户信息 {_DIFF{#_newObj}}" // 成功日志模板
)
public void modify(Long id, UserUpdateReq req) {
// 业务代码
}参数说明:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
group | String | 业务分组,用于日志归类 | |
tag | String | ✓ | 操作标签,如:编辑用户、删除订单 |
businessKey | String | 业务唯一标识,支持 SpEL | |
success | String | 成功日志模板,支持 SpEL | |
fail | String | 失败日志模板 | |
condition | String | 记录条件表达式 | |
successCondition | String | 成功判断条件 |
2. @DiffField - 字段注解
文件: DiffField.java
java
public class User {
@DiffField(name = "用户名")
private String username;
@DiffField(name = "用户状态", function = "statusName")
private Integer status;
@DiffField(name = "创建时间", strategy = DiffFieldStrategy.IGNORE)
private Instant createTime;
}参数说明:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
name | String | 字段显示名称 | |
function | String | "" | 解析函数名 |
strategy | DiffFieldStrategy | ALWAYS | 对比策略 |
核心类详解
1. DiffLogContext - 日志上下文
文件: DiffLogContext.java
作用: 在业务方法中传递对比对象
java
@Service
public class UserService {
public void updateUser(Long id, UserUpdateReq req) {
// 1. 查询旧数据
User oldUser = userMapper.selectById(id);
// 2. 构造新对象
User newUser = BeanUtil.copyProperties(oldUser, User.class);
BeanUtil.copyProperties(req, newUser);
// 3. 设置到上下文(关键!)
DiffLogContext.putDiffItem(oldUser, newUser);
// 4. 执行更新
userMapper.updateById(newUser);
}
}API 说明:
| 方法 | 说明 |
|---|---|
putDiffItem(oldVal, newVal) | 设置新旧对比对象 |
putVariable(name, value) | 设置自定义变量 |
putGlobalVariable(name, value) | 设置全局变量 |
getVariable(key) | 获取变量值 |
clear() | 清理方法级变量 |
clearGlobal() | 清理全局变量 |
2. DiffLogInfo - 日志实体
文件: DiffLogInfo.java
java
@Data
@Builder
public class DiffLogInfo {
private String serviceName; // 服务名
private String businessGroup; // 业务分组
private String businessTag; // 操作标签
private String businessKey; // 业务ID
private String description; // 日志内容
private Boolean status; // 日志状态(成功/失败)
private Long tenantId; // 租户ID
private Long createBy; // 操作人ID
private String createName; // 操作人名称
private Instant createTime; // 创建时间
private Map<String, Object> extra; // 请求上下文
private List<FieldChange> variables; // 字段变更列表
}3. FieldChange - 字段变更记录
文件: FieldChange.java
java
@Data
@Builder
public class FieldChange {
private String name; // 字段名
private String label; // 显示名称
private Object oldVal; // 旧值
private Object newVal; // 新值
}示例:
json
{
"name": "username",
"label": "用户名",
"oldVal": "old_name",
"newVal": "new_name"
}使用示例
基础用法
java
@Service
public class UserService {
@DiffLog(
group = "用户管理",
tag = "编辑用户",
businessKey = "{{#id}}",
success = "更新用户信息 {_DIFF{#_newObj}}"
)
public void updateUser(Long id, UserUpdateReq req) {
// 1. 查询旧数据
User oldUser = userMapper.selectById(id);
// 2. 构造新对象
User newUser = BeanUtil.copyProperties(oldUser, User.class);
BeanUtil.copyProperties(req, newUser);
// 3. 设置对比对象
DiffLogContext.putDiffItem(oldUser, newUser);
// 4. 执行更新
userMapper.updateById(newUser);
}
}生成的日志:
json
{
"businessGroup": "用户管理",
"businessTag": "编辑用户",
"businessKey": "1001",
"description": "更新用户信息 【用户名】从【张三】修改为【李四】;【手机号】从【13800138000】修改为【13900139000】",
"status": true,
"createBy": 1,
"createName": "管理员",
"createTime": "2026-04-09T10:30:00Z",
"extra": {
"ip": "192.168.1.100",
"uri": "/api/user/update",
"browser": "Chrome",
"os": "Windows"
},
"variables": [
{"name": "username", "label": "用户名", "oldVal": "张三", "newVal": "李四"},
{"name": "phone", "label": "手机号", "oldVal": "13800138000", "newVal": "13900139000"}
]
}SpEL 表达式使用
java
// 1. 使用方法参数
@DiffLog(
businessKey = "{{#id}}", // 使用参数 id
success = "删除用户 {{#id}}" // 使用参数 id
)
public void deleteUser(Long id) {
userMapper.deleteById(id);
}
// 2. 使用对象属性
@DiffLog(
businessKey = "{{#req.userId}}", // 使用参数的属性
success = "用户 {{#req.username}} 请求重置密码"
)
public void resetPassword(ResetReq req) {
// ...
}
// 3. 使用上下文变量
@DiffLog(
success = "当前操作人: {_USER{#_context.nickname}}"
)
public void someMethod() {
// _context 是框架内置的认证上下文
}
// 4. 条件记录
@DiffLog(
condition = "{{#req.type == 'important'}}", // 只在特定条件下记录
success = "重要操作: {{#req.content}}"
)
public void doSomething(Req req) {
// ...
}内置变量:
| 变量 | 说明 |
|---|---|
{{#_newObj}} | 新对象(DiffLogContext 设置的新值) |
{{#_oldObj}} | 旧对象(DiffLogContext 设置的旧值) |
{{#_context}} | 认证上下文(AuthenticationContext) |
{{#_USER{...}}} | 用户信息函数 |
{{#_DIFF{...}}} | 差异计算函数 |
自定义字段解析
java
// 1. 定义字段注解
public class User {
@DiffField(name = "用户状态", function = "parseStatus")
private Integer status;
// getter/setter...
}
// 2. 实现解析函数
@Component
public class UserParseFunction implements IParseFunction {
@Override
public String functionName() {
return "parseStatus";
}
@Override
public String apply(Object value) {
Integer status = (Integer) value;
return switch (status) {
case 1 -> "正常";
case 0 -> "禁用";
default -> "未知";
};
}
}
// 3. 生成的日志
// 【用户状态】从【正常】修改为【禁用】批量操作日志
java
@DiffLogs({
@DiffLog(
group = "用户管理",
tag = "批量删除",
businessKey = "{{#ids}}",
success = "批量删除用户: {{#ids}}"
),
@DiffLog(
group = "用户管理",
tag = "删除详情",
businessKey = "{{#_eachId}}",
success = "删除用户 {{#_eachId}}",
condition = "{{#_eachId != null}}"
)
})
public void batchDeleteUsers(List<Long> ids) {
for (Long id : ids) {
DiffLogContext.putVariable("_eachId", id);
userMapper.deleteById(id);
DiffLogContext.clear();
}
}配置说明
application.yml
yaml
extend:
boot:
log:
diff:
# 是否记录日志
diff-log: true
# 全局忽略字段(不参与对比)
ignore-global-fields:
- deleted
- createTime
- createBy
- createName
- lastModifyTime
- lastModifyBy
- lastModifyName
# 日志模板配置
# 添加(从空到有值)
add-template: "【__fieldName】从【空】修改为【__targetValue】"
# 更新(值变更)
update-template: "【__fieldName】从【__sourceValue】修改为【__targetValue】"
# 删除(设为空)
delete-template: "删除了【__fieldName】:【__sourceValue】"
# 字段分隔符
field-separator: ";"
# 列表项分隔符
list-item-separator: ","
# 是否检查注解
check-annotation: true
# 美化输出
pretty-value-printer: true模板变量:
| 变量 | 说明 |
|---|---|
__fieldName | 字段名称 |
__sourceValue | 旧值 |
__targetValue | 新值 |
__addValues | 列表新增项 |
__delValues | 列表删除项 |
实现日志存储
业务系统需要实现 IDiffLogService 接口:
java
@Service
public class MyDiffLogService implements IDiffLogService {
@Autowired
private DiffLogMapper diffLogMapper;
@Override
public void handler(DiffLogInfo logInfo) {
// 保存到数据库
DiffLogEntity entity = new DiffLogEntity();
BeanUtil.copyProperties(logInfo, entity);
// 字段变更列表转为 JSON 存储
entity.setVariables(JSON.toJSONString(logInfo.getVariables()));
entity.setExtra(JSON.toJSONString(logInfo.getExtra()));
diffLogMapper.insert(entity);
}
@Override
public List<DiffLogInfo> queryLog(DiffLogInfoQueryReq req) {
// 查询日志
return diffLogMapper.selectList(req);
}
}数据库表设计:
sql
CREATE TABLE `sys_diff_log` (
`id` BIGINT NOT NULL COMMENT '主键',
`service_name` VARCHAR(64) COMMENT '服务名',
`business_group` VARCHAR(64) COMMENT '业务分组',
`business_tag` VARCHAR(64) COMMENT '操作标签',
`business_key` VARCHAR(128) COMMENT '业务ID',
`description` TEXT COMMENT '日志内容',
`status` TINYINT COMMENT '状态',
`tenant_id` BIGINT COMMENT '租户ID',
`create_by` BIGINT COMMENT '操作人ID',
`create_name` VARCHAR(64) COMMENT '操作人名称',
`create_time` DATETIME COMMENT '创建时间',
`extra` TEXT COMMENT '请求上下文(JSON)',
`variables` TEXT COMMENT '字段变更(JSON)',
PRIMARY KEY (`id`),
INDEX `idx_tenant_business` (`tenant_id`, `business_key`, `create_time`)
) COMMENT='操作审计日志';常见问题 (Q&A)
Q1: DiffLogContext 为什么要放在方法内部?
A: 因为需要查询旧数据后再对比
java
public void updateUser(Long id, UserUpdateReq req) {
// 必须先查旧数据
User oldUser = userMapper.selectById(id);
// 再构造新对象
User newUser = ...;
// 然后设置对比对象
DiffLogContext.putDiffItem(oldUser, newUser);
// 才能更新
userMapper.updateById(newUser);
}Q2: 如果不设置 DiffLogContext 会怎样?
A: 日志仍会记录,但没有字段变更详情
java
// 不设置 DiffLogContext
@DiffLog(success = "更新用户信息")
public void updateUser(Long id, UserUpdateReq req) {
userMapper.updateById(req);
}
// 生成的日志
{
"description": "更新用户信息",
"variables": null // 没有字段变更
}Q3: SpEL 表达式 {{#id}} 是什么意思?
A: SpEL = Spring Expression Language,用于动态获取值
| 表达式 | 说明 |
|---|---|
{{#id}} | 获取参数 id 的值 |
{{#req.userId}} | 获取参数对象的属性 |
{{#_newObj.username}} | 获取新对象的属性 |
{{#_context.userId}} | 获取当前用户ID |
Q4: 如何忽略某些字段不参与对比?
A: 方式1: 配置全局忽略;方式2: 使用 @DiffField 策略
java
// 方式1: 配置文件
extend:
boot:
log:
diff:
ignore-global-fields:
- createTime
- updateTime
// 方式2: 字段注解
@DiffField(name = "创建时间", strategy = DiffFieldStrategy.IGNORE)
private Instant createTime;Q5: 异步场景下如何使用?
A: 使用 InheritableThreadLocal,子线程可继承父线程上下文
java
@Async
public void asyncMethod() {
// 可以获取父线程设置的 DiffLogContext 变量
Object oldObj = DiffLogContext.getVariable(DiffParseFunction.OLD_OBJECT);
}学习建议
- 先掌握基础用法: @DiffLog + DiffLogContext.putDiffItem
- 理解 SpEL 表达式: 灵活生成动态日志
- 自定义字段解析: 处理枚举、字典等特殊字段
- 实现日志存储: 根据业务需求持久化日志
下一步学习
Excel 处理- 导入导出WebSocket- 实时消息国际化- 多语言支持