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) 同步