20251023 operlog-文档整理

44 阅读5分钟

20251023 operlog-文档整理

基础服务的文档实在是不够健全, 本人遂整理一份,关于操作日志的

对于操作日志, 有的公司有几种类型:埋点接口捕获对象、埋点接口捕获差异对象、埋点接口触发信息上报

当前项目中, 使用的则是折中的方案,但是认为拉链表等完整归档的策略, 才是erp此类系统最重要的; 如果只是捕获差异对象,效果也不是很完美;换位思考,当对于做过手机toC相关应用的nodejs后端来说,反而大而全的埋点接口触发上报,也比较重要,用于分析当前app的日活用户操作等相关数据,同时也减少了数据库的相关压力(太全的数据占用很大)

当前拆解的项目, 属于ERP系统的基础服务中的操作日志服务; 由需要放三方服务消息队列对应topic生产出来对应的日志消息, 并且提供对应的RPC查询功能;感兴趣请联系 big.lu@foxmail.com

概览

模块定位:统一的“操作日志(OperLog)/ 系统预警(SysWarn)”服务。
负责日志消息生产 → RocketMQ 投递 → 消费入库 → Dubbo RPC 查询的闭环。
消息统一入口:单一 RocketMQ 主题(topic-operlogs)。
通过消息体的 level 字段区分两类:TOPIC_OPERLOGS(操作日志)与 TOPIC_SYSWARN(系统预警)。

核心能力

更新场景的字段级差异比对:支持主表 + 子表,数值等值判断、字典/ID 值转换、时间格式化、空值策略。
无差异丢弃:避免噪音(UPDATE 无字段变化不落库)。
RPC 查询:通过 Dubbo 提供列表/分页检索。

代码结构与职责

仅列关键文件与要点,

生产端

OperLogsMessageProxy
解析实体上 @OperLogFieldMeta → 生成 OperLogConfig(字段描述、值转换、日期格式)。
构建 OperLog 并投递至 RocketMqTopicTypeEnums.TOPIC_OPERLOGS。
支持:新增/删除(简化参数)与更新(OperLogParamBuilder)以及子表。

SysWarnMessageProxy
生产系统预警消息,level=TOPIC_SYSWARN,仍投递到同一主题。

消费端

RocketmqConsumerInit
动态读取 groupId/threadNums/topicName/httpEndpoint/accessKey/secretKey 等环境配置,完成订阅与启动。

OperlogConsumerListener
统一消费入口,根据 level 分流:
TOPIC_OPERLOGS:反序列化为 OperLog,路由至 IOperLogsService.handleLog。
TOPIC_SYSWARN:反序列化为 SysWarn,路由至 ISysWarnService.handleWarn。

服务层(入库与查询)

IOperLogsService / OperLogsService
handleLog(OperLog):分发至 INSERT/DELETE 与 UPDATE 处理。
INSERT/DELETE:直接构建 OperLogsContent 入库。
UPDATE:依据 OperLogConfig、needCompareFields、compareNullFields 做差异比对;递归处理 childOperLogs;无差异丢弃,有差异入库。

IOperLogsApiService
Dubbo 查询接口:getList(...),返回含 OperLogsContent 的结果集。
OperLogsMapper(.xml)
MyBatis 持久化与条件检索。

领域模型(核心数据结构)

OperLog
承载 old/new 数据、OperLogConfig、子日志、比对字段集合、空值比对集合等。

OperLogParamBuilder
更新日志的便捷构造器:主对象/子对象 + 字段集合配置。

OperLogConfig
由注解解析而来:字段描述表、值转换表、日期格式表。

OperLogsContent
入库内容:动作类型、业务键、字段变更列表、子内容。

注解与值转换规范

@OperLogFieldMeta 字段注解(第三方服务在使用提供的接口的时候, 对于需要比对的字段的实体类, 都需要使用这个注解, 因为第三方服务中利用这个字段来处理对应的日志对比逻辑的)
目的:声明“哪些字段可记录”,以及如何展示(描述、转换、日期格式)。

关键属性
descr:人类可读的字段名(必须;未配置则该字段不参与比对)。
isValueTrans:是否启用值转换。
valueTransType:转换类型(字符串常量,见项目的 DefaultValueTransType)。
extTransParam:转换扩展参数(如字典编码)。
dateFormat:日期展示格式(如 yyyy-MM-dd HH:mm:ss)。

比对参与规则
默认以注解收集到的字段集作为候选集合;若构造时显式传入 needCompareFields,则取交集进行比较。
没有 descr 的字段不会参与比对。

常见值转换类型(demo)

TRANS_DICT_DATA:码值 → 文本(extTransParam 传字典编码,如 op_product_location)。
TRANS_USER_ID:用户 ID → 姓名。
TRANS_STORE_AREA_ID:库区/库位 ID → 名称。
TRANS_ORG_ID:组织/部门 ID → 名称。
TRANS_GOODS_ID:商品/物料 ID → 名称。
以项目中 DefaultValueTransType 为准。

日期与数值处理

日期:按字段的 dateFormat 序列化后再做比较与展示,避免“同一时间不同格式”误判。
数值:统一以 BigDecimal 等值判断,避免 "1.00" vs "1" 误判差异。

消息与入库流程(端到端)

生产端(业务服务内)

新增 / 删除
OperLogsMessageProxy.buildLogAndSendMessage(ILogBusinessType, LogActionTypeEnum.INSERT/DELETE, businessId, businessKey)
无需字段差异,构建 OperLog 后直接发送。

更新(主表 + 子表)
OperLogParamBuilder 组装:
主对象:旧/新值、businessType、businessKey、可选 needCompareFields、compareNullFields。
子对象:支持新增/删除/更新与批量对比。

OperLogsMessageProxy.buildLogAndSendMessage(builder) 发送:
反射解析注解 → 生成 OperLogConfig。
序列化 oldData/newData,封装 childOperLogs。
设置 level=TOPIC_OPERLOGS → MQ。

消费端(operlogs 服务)

RocketmqConsumerInit 启动并订阅主题。

OperlogConsumerListener 收取消息:
识别 level:
TOPIC_OPERLOGS → 反序列化 OperLog → IOperLogsService.handleLog。
TOPIC_SYSWARN → 反序列化 SysWarn → ISysWarnService.handleWarn。

OperLogsService 处理:
INSERT/DELETE:直接入库(构建 OperLogsContent)。
UPDATE:
依据 OperLogConfig + needCompareFields + compareNullFields 做字段级对比。

值转换与日期格式化后比较 → 生成 fieldInfos。
递归处理 childOperLogs。
若主内容与子内容均无差异 → 丢弃;否则入库(携带 businessType/businessKey/businessId/operatorId/operatorTime/msgId)。

RPC 查询

接口:IOperLogsApiService.getList(...)
典型条件:时间范围、业务类型(businessType)、业务键(businessKey/businessId)、操作人等。
返回:列表/分页,其中 content 为 OperLogsContent(包含字段变更明细与子内容)。
服务暴露:@DubboService(包扫描由 application.yml 的 dubbo.scan.base-packages 控制;端口 dubbo.protocol.port=-1 动态)。

工程配置要点

RocketMQ(application.yml 下 ali.rocketmq.topic-operlogs.*)
topicName、groupId、threadNums、httpEndpoint、accessKey、secretKey。

Dubbo
dubbo.scan.base-packages 指向 com.idelamu.pis.operlogs.**.service。
dubbo.protocol.port: -1。

Spring 环境
bootstrap.yml 常用 spring.profiles.active=${RUN_ENV}。
建议将 MQ 与注册中心密钥以环境变量/配置中心托管,避免硬编码。

第三方服务接入指南(最小闭环)

新增/删除日志

在业务代码中调用:
buildLogAndSendMessage(bizType, INSERT/DELETE, businessId, businessKey)

启动 operlogs 消费端,确认消息入库。

通过 Dubbo 调用 getList 验证可检索到该记录。

更新日志(含差异)

在需要记录的实体字段上添加注解 @OperLogFieldMeta(descr=..., ...)。

如:erpSku(“SKU”)、erpSkuName(“SKU名称”)、ownerName/ownerNameId(负责人/负责人ID)、launchDate(“上市日期”,dateFormat=yyyy-MM-dd)。

构建器组装主对象旧/新值,并按需设置:

needCompareFields:限定比对范围(可空)。
compareNullFields:指定即使为 null 也要纳入变更判断的字段。

子表变更:按需调用 addChildOperLogDtoByUpdate/Insert/Delete(...)。
operLogsMessageProxy.buildLogAndSendMessage(builder) 发送。

消费入库后,通过 getList 查看 fieldInfos 与子内容。

常见问题(FAQ)

Q1:发送了 UPDATE 日志,为什么数据库没有记录?
原因:系统有“无差异丢弃”的逻辑。若比对后 fieldInfos 为空且无子内容,则不入库。

排查:
实体关键字段是否已加 @OperLogFieldMeta(descr=...)?未加不会参与比对。
若使用了 needCompareFields,请确认字段名与实体属性名一致,且这些字段具备注解。
数值/日期是否因格式问题被误判一致?(例如未配置 dateFormat)。
compareNullFields 是否需要包含你期望记录的 null 字段?

Q2:指定了 needCompareFields,仍不生效?

说明:在消费者侧,实际比对集合为注解字段集 ∩ 传入字段集。
没有注解的字段,即便显式指定,也不会被比较。

Q3:展示的值没有被转换(仍是 ID/码值)?

检查:该字段是否 isValueTrans=true 且 valueTransType/extTransParam 配置正确,且对应转换器可用。

Q4:如何记录子表(明细)变化?

做法:使用构建器的 addChildOperLogDtoByUpdate/Insert/Delete(...)(支持批量聚合),并在子对象实体上同样配置注解。

调试建议

断点位置

生产端:OperLogsMessageProxy.buildLogAndSendMessage(...)(看 OperLogConfig 与最终消息体)。
消费端:OperlogConsumerListener.doConsumeMsg(...)(看 level 分流)、OperLogsService.handleUpdateActionLog(...)(看 fieldInfos 构造)。

关键观测点

operLog.operLogConfig.fieldDescrMap/valueTransMap/dateFormatMap。
needCompareFields 与注解字段集的交集结果。
数值比较是否走了等值判断;日期是否按期望格式化。

环境变量

MQ:topicName/groupId/threadNums/httpEndpoint/accessKey/secretKey。
Dubbo:注册中心与包扫描。

本地验证

运行消费端,确保订阅 topic-operlogs。
发送一条包含已注解字段变更的 UPDATE 消息。
观察消费日志与数据库 oper_logs 的新增记录与 content.fieldInfos。

使用与设计建议(设计优势)

最小注解集:仅为有业务价值、需要长期追踪的字段添加注解,降低噪音与维护成本。
限定比对范围:在热点更新路径中使用 needCompareFields 精准收敛,提升性能与可读性。
空值策略:对“空值也需识别”的字段,放入 compareNullFields,避免遗漏。
字典与 ID 转换:确保 valueTransType/extTransParam 与项目字典/服务保持一致,必要时在集成测试验证。
子表管理:优先用构建器的批量 API 聚合子项增删改,避免多条碎片日志。
消息级别:务必设置 level,保证消费者正确分流日志与预警。

典型场景速记

新增/删除
用简化重载:buildLogAndSendMessage(bizType, INSERT/DELETE, businessId, businessKey)

更新(主 + 子)
构建器:主对象旧/新值;可选 needCompareFields、compareNullFields;按需添加子对象变更。
发送:buildLogAndSendMessage(builder)

系统预警
sysWarnSendMessage(bizType, warnType, warnContent),level=TOPIC_SYSWARN,同主题投递。

已知坑与修复范式(目前已经遇到的一些坑点)

问题:WlmListing 的更新逻辑里“调用了发送”,但消费侧无落库。
根因:实体未加 @OperLogFieldMeta,导致无字段可比对 → 命中“无差异丢弃”。
修复:为关键字段加注解(如 erpSku/erpSkuName/ownerName/ownerNameId/launchDate 等,日期配置 yyyy-MM-dd)。
效果:注解字段集建立后,更新比对产生 fieldInfos,即可入库并被 RPC 查询到。

术语速览

OperLog:操作日志消息;含旧/新数据、字段配置、子日志。

SysWarn:系统预警消息;与 OperLog 共用主题,通过 level 区分。

OperLogConfig:由注解解析而来的字段配置(描述、值转换、日期格式)。

needCompareFields:显式限定参与比对的字段集合(与注解字段集取交集)。

compareNullFields:即使为 null 也参与差异判定的字段集合。

OperLogsContent:入库内容(动作类型、业务键、字段变更列表、子内容)。

工程包名结构

<groupId>com.idelamu.pis</groupId>
<artifactId>idelamu-tunan-pis</artifactId>
<version>${revision}</version>
存放在idelamu-tunan-pis工程下
```.preview-wrapper pre::before { position: absolute; top: 0; right: 0; color: #ccc; text-align: center; font-size: 0.8em; padding: 5px 10px 0; line-height: 15px; height: 15px; font-weight: 600; } .hljs.code\_\_pre > .mac-sign { display: flex; } .code\_\_pre { padding: 0 !important; } .hljs.code\_\_pre code { display: -webkit-box; padding: 0.5em 1em 1em; overflow-x: auto; text-indent: 0; } h2 strong { color: inherit !important; }

> 本文使用 [文章同步助手](https://juejin.cn/post/6940875049587097631) 同步