前言
近期我在项目中独立落地了日志审计完整业务模块,从前端页面布局、路由注册、国际化、接口封装、网络拦截,到后端自研DDD领域模型架构、MBG动态SQL底层、AOP审计注解、数据库查询整个端到端的过程。
本文我从零完整复盘:用户访问页面 → 前端渲染 → 网络请求 → 后端网关路由 → 领域模型执行 → MBG数据查询 → 数据库返回 → 数据回填渲染的全流程


技术覆盖:前端分层工程化、路由嵌套、Less自适应布局、i18n国际化、POST/GET请求差异、Axios统一拦截、后端DDD领域驱动设计、MBG Example/Criteria动态SQL、BLOB字段查询坑点、自研框架注解、AOP自动审计原理。

一、端到端架构总览
1.1 功能核心
审计日志是系统操作追溯、问题复盘、风险定位的核心模块,系统会自动记录用户所有增删改查、配置变更、数据操作行为,支持多条件筛选日志、查看操作详情、查看数据变更前后快照、追溯链路追踪ID,完整还原每一次系统操作。


1.2 我梳理的完整端到端链路
这套模块最大的特点就是前后端完全分层解耦、架构标准化,完整链路如下:
浏览器URL访问 → 前端路由匹配嵌套布局 → 页面样式+国际化渲染 → 组件挂载触发接口请求 → 全局Request拦截处理请求 → 后端统一Controller入口 → DDD领域网关路由分发 → 对应领域模型execute执行 → MBG Mapper动态SQL查询数据库 → 数据原路返回 → 前端解析渲染表格与弹窗
1.3 前后端分层架构对照
我将整套模块拆分为前端五层架构 + 后端DDD领域架构,各司其职:
前端工程层:路由层 → 视图样式层 → 国际化文案层 → Service接口层 → 网络拦截层
后端业务层:统一网关Controller → 领域模型网关 → 具体业务Domain领域类 → MBG Mapper数据层 → 数据库
二、前端全链路工程化实现
2.1 路由层:注册页面访问入口,实现嵌套布局
首先我需要给审计日志页面配置路由,让浏览器URL可以精准映射到日志审计组件。前端路由只负责页面渲染跳转,不参与任何数据交互。
我配置的核心路由规则:
|------------------------------------------------------------------------------------|
| Plain Text { name: '日志审计', path: '/knowledge/logAudit', component: './AuditLog', } |
同时该页面采用项目标准双层嵌套布局:外层 CommonLayout 全局公共布局承载顶部导航,内层 MenuLayout 侧边菜单布局承载侧边栏与面包屑,最终将我的 AuditLog 业务页面渲染在布局插槽中,完美贴合系统整体UI架构。
AuditLog(index.tsx)
java
/** 模块枚举映射 */
const MODULE_OPTIONS = [
{ value: 'KNOWLEDGE_BASE', label: '知识库管理' },
{ value: 'DATASOURCE', label: '数据源管理' },
{ value: 'SYSTEM_CONFIG', label: '系统配置' },
];
/** 操作动作枚举映射(按模块联动) */
const ACTION_OPTIONS_MAP: Record<string, { value: string; label: string }[]> = {
KNOWLEDGE_BASE: [
{ value: 'CREATE', label: '新增' },
{ value: 'DELETE', label: '删除' },
{ value: 'UPDATE', label: '修改' },
{ value: 'PUBLISH', label: '发布版本' },
{ value: 'ROLLBACK', label: '回退版本' },
{ value: 'SYNC', label: '数据同步' },
{ value: 'EXPORT', label: '导出' },
{ value: 'IMPORT', label: '导入' },
{ value: 'ENABLE', label: '开启' },
{ value: 'DISABLE', label: '关闭' },
{ value: 'EDIT_FIELD', label: '编辑字段' },
{ value: 'RESTORE', label: '还原' },
],
DATASOURCE: [
{ value: 'CREATE', label: '新增' },
{ value: 'UPDATE', label: '修改' },
{ value: 'DELETE', label: '删除' },
],
SYSTEM_CONFIG: [
{ value: 'UPDATE', label: '修改' },
],
};
/** 所有操作动作的合并列表(未选模块时展示) */
const ALL_ACTION_OPTIONS = [
{ value: 'CREATE', label: '新增' },
{ value: 'DELETE', label: '删除' },
{ value: 'UPDATE', label: '修改' },
{ value: 'PUBLISH', label: '发布版本' },
{ value: 'ROLLBACK', label: '回退版本' },
{ value: 'SYNC', label: '数据同步' },
{ value: 'EXPORT', label: '导出' },
{ value: 'IMPORT', label: '导入' },
{ value: 'ENABLE', label: '开启' },
{ value: 'DISABLE', label: '关闭' },
{ value: 'EDIT_FIELD', label: '编辑字段' },
{ value: 'RESTORE', label: '还原' },
];
type LogItem = {
id: string;
userId: number;
userName: string;
module: string;
moduleName: string;
action: string;
actionName: string;
targetType: string;
targetName: string;
knowledgebaseName: string;
detailSummary: string;
detailJson: string;
ipAddress: string;
operateTime: string;
requestId: string;
};
type PaginationInfo = {
pageIndex: number;
pageSize: number;
total: number;
};
最终访问地址:/knowledge/logAudit,路由自动逐层渲染布局,完成页面初始化展示。
2.2 视图样式层:Less实现自适应企业级布局
页面样式采用Flex 弹性布局,解决常见的「滚动条错乱、页面缩放变形、弹窗排版混乱」问题。
核心布局思路我总结为:固定头部筛选区,剩余高度全部交给表格自适应。
我通过外层容器禁止滚动、子区域自主滚动的方式,彻底杜绝双层滚动条bug:
|--------------------------------------------------------------------------------------------------|
| Plain Text .container { display: flex; flex-direction: column; height: 100%; overflow: hidden; } |
同时给头部、筛选栏设置 flex-shrink: 0 禁止压缩,表格区域设置 flex:1 自适应填充剩余高度。
日志详情弹窗:两栏网格展示基础信息、独立区块展示变更摘要、JSON格式化展示操作前后快照,通过伪元素自动补全中文冒号、超长内容自动滚动换行,减少了冗余代码。
2.3 国际化文案层:统一管理所有页面文本
为了避免页面中文硬编码、文案不统一、无法多语言拓展的问题,我将审计日志页面所有展示文本全部抽离i18n语言包。
采用 模块名.字段名 的命名空间规范,通过 auditLog 统一隔离模块文案,避免全局key冲突,同时分类管理页面标题、表头、占位符、弹窗文本、提示语、动态数值。
比如分页总数动态文案 共 {total} 条,支持前端动态传参渲染,完美适配分页业务场景,所有文案修改只需改一处,全局生效。
java
export default {
'auditLog.title': '日志审计',
'auditLog.operateTime': '操作时间',
'auditLog.operator': '操作人',
'auditLog.module': '操作页面',
'auditLog.action': '操作动作',
'auditLog.targetName': '操作对象',
'auditLog.detail': '日志详情',
'auditLog.ipAddress': '操作IP',
'auditLog.keyword': '关键字',
'auditLog.search': '搜索',
'auditLog.reset': '重置',
'auditLog.viewDetail': '详情',
'auditLog.copy': '复制',
'auditLog.copySuccess': '复制成功',
'auditLog.detailTitle': '日志详情',
'auditLog.basicInfo': '基本信息',
'auditLog.changeSummary': '变更摘要',
'auditLog.changeCompare': '变更对比',
'auditLog.beforeSnapshot': '操作前快照',
'auditLog.afterSnapshot': '操作后快照',
'auditLog.traceId': '链路追踪ID',
'auditLog.fieldName': '字段名',
'auditLog.beforeValue': '变更前',
'auditLog.afterValue': '变更后',
'auditLog.noDetail': '暂无详情',
'auditLog.requestFailed': '查询失败',
'auditLog.startTime': '开始时间',
'auditLog.endTime': '结束时间',
'auditLog.allModules': '全部页面',
'auditLog.allActions': '全部动作',
'auditLog.placeholder.operator': '请输入操作人',
'auditLog.placeholder.targetName': '请输入操作对象',
'auditLog.placeholder.keyword': '搜索日志详情内容',
'auditLog.emptyText': '暂无审计日志',
'auditLog.total': '共 {total} 条',
'auditLog.expand': '展开',
'auditLog.collapse': '收起',
};
2.4 Service接口层:封装标准POST业务接口
页面渲染完成后,需要向后端请求审计数据,我封装了两个核心POST接口,覆盖列表查询与详情查询全场景。
1. 分页查询审计日志:支持页码、页大小、时间范围、操作人、模块、关键字等多维度筛选,参数齐全、类型约束严格。
2. 根据ID查询日志详情:通过单条日志ID,请求后端返回数据变更快照、字段对比、链路ID等核心详情数据。
java
function querySysAuditLog(req: any, res: any) {
return res.json({
resultCode: '0',
resultMsg: 'SUCCESS',
resultObject: {
list: [
{
id: 1001,
userId: 1,
userName: 'admin',
module: 'KNOWLEDGE_BASE',
moduleName: '知识库管理',
action: 'CREATE',
actionName: '新增',
targetType: 'KNOWLEDGE_BASE',
targetName: '销售数据分析库',
knowledgebaseName: '销售数据分析库',
detailSummary: '新增知识库「销售数据分析库」',
detailJson:
'{"summary":"新增知识库「销售数据分析库」","after":{"name":"销售数据分析库","knowledgeBaseId":1553940562431496192},"diff":[]}',
ipAddress: '10.45.135.163',
operateTime: '2026-07-23 10:30:00',
requestId: 'req-001',
},
{
id: 1002,
userId: 2,
userName: 'zhangsan',
module: 'DATASOURCE',
moduleName: '数据源管理',
action: 'UPDATE',
actionName: '修改',
targetType: 'DATASOURCE',
targetName: 'MySQL生产库',
knowledgebaseName: '销售数据分析库',
detailSummary: '修改数据源「MySQL生产库」',
detailJson:
'{"summary":"修改数据源「MySQL生产库」","after":{"name":"MySQL生产库","dbType":"MySQL"},"diff":[{"label":"数据库地址","from":"10.0.0.1","to":"10.0.0.2"}]}',
ipAddress: '10.45.135.164',
operateTime: '2026-07-23 09:15:00',
requestId: 'req-002',
},
{
id: 1003,
userId: 1,
userName: 'admin',
module: 'SYSTEM_CONFIG',
moduleName: '系统配置',
action: 'UPDATE',
actionName: '修改',
targetType: 'SYSTEM_CONFIG',
targetName: 'BASE_CONFIG',
knowledgebaseName: '',
detailSummary: '修改系统配置项',
detailJson:
'{"summary":"修改系统配置项","after":{"paramCode":"BASE_CONFIG","paramValue":"{\\"envName\\":\\"nanwang\\"}"},"diff":[]}',
ipAddress: '10.45.135.163',
operateTime: '2026-07-22 16:45:00',
requestId: 'req-003',
},
],
total: 3,
pageIndex: 1,
pageSize: 20,
},
});
}
function getSysAuditLogDetail(req: any, res: any) {
const { id } = req.body || {};
return res.json({
resultCode: '0',
resultMsg: 'SUCCESS',
resultObject: {
id: id || 1001,
userId: 1,
userName: 'admin',
module: 'KNOWLEDGE_BASE',
moduleName: '知识库管理',
action: 'CREATE',
actionName: '新增',
targetType: 'KNOWLEDGE_BASE',
targetName: '销售数据分析库',
knowledgebaseName: '销售数据分析库',
detailSummary: '新增知识库「销售数据分析库」',
detailJson:
'{"summary":"新增知识库「销售数据分析库」","after":{"name":"销售数据分析库","description":"用于销售数据分析","knowledgeBaseId":1553940562431496192,"viewId":1554213876367605760},"diff":[]}',
ipAddress: '10.45.135.163',
operateTime: '2026-07-23 10:30:00',
requestId: 'req-001',
},
});
}
为什么复杂业务查询统一用POST,不用GET?
因为审计日志筛选参数多、结构复杂,GET存在URL长度限制、参数暴露在地址栏、浏览器默认缓存数据的问题;而POST通过Body传递JSON参数,无长度限制、私密性更好、无默认缓存,同时贴合项目 callDomainModel 领域模型网关统一POST的规范。
GET接口(会话信息查询):参数拼接在URL、无请求体、无Content-Type、浏览器默认缓存,只适用于简单只读接口。
POST接口(审计日志业务):参数存JSON Body、自动携带JSON请求头、无缓存、支持复杂结构体,适配所有中台业务查询、提交场景。
2.5 网络拦截层:Request全局统一处理请求
我没有在接口中重复写Axios配置,而是依托项目全局 request.ts 实现统一能力:
统一配置baseURL环境地址、自动携带Cookie会话鉴权、统一拦截业务错误、自动剥离后端外层返回体、全局统一报错提示。
只需要关心业务参数和接口地址。
三、后端核心架构:DDD领域模型
后端采用自研DDD领域驱动架构,也是我本次复盘的核心重点。
3.1 项目后端完整调用链路(对应前端请求)
当前端POST请求到达后端后,完整执行流程:
前端POST请求 → 项目唯一通用Controller入口 → DDD领域网关 → 根据枚举路由匹配对应领域类 → 执行execute核心方法 → MBG Mapper动态SQL查询数据库 → 封装结果返回前端
3.2 DDD架构和传统架构的核心区别
传统架构:一个Service堆积所有增删改查代码,业务耦合严重、维护困难、无法统一拦截处理。
DDD架构 :一个业务动作,一个独立领域类,完全单一职责。
对应审计日志业务:查询审计日志单独对应QuerySysAuditLog 领域模型,和删除、新增、修改业务完全隔离,代码解耦、便于维护、支持统一事务、统一审计、统一日志拦截。

3.3注解实现框架调度和自动审计
1. @DomainModel 核心路由注解:标识当前类为可调度领域业务模型,通过枚举编码让网关可以精准路由,同时支持消息异步机制。
2. @AuditLog AOP自动审计注解 :这是审计日志模块的核心!无需手写日志入库代码,通过AOP无侵入拦截,自动识别操作模块、操作动作、操作对象,自动抓取入参中的ID和名称,自动拼接操作描述,最终自动生成审计日志入库,这也是页面审计数据的数据来源。
3. @Transactional 事务注解:保证单次业务所有数据库操作要么全部成功、要么全部回滚,保证数据一致性。
四、DAO层核心底层:MBG、Example、Criteria
本项目所有数据库CRUD全部由 MyBatis Generator(MBG) 自动生成
4.1 MBG三大核心文件
MBG自动生成:PO实体类、Example动态条件构造器、Mapper接口与XML文件,不用每次查询库表手写SQL、拼接if判断代码。
4.2 Example + Criteria 动态SQL核心原理
Example:条件大容器,存放所有查询条件组
Criteria:一组括号内的AND条件集合
Criterion:最小原子查询条件
核心规则:同一个Criteria内条件为AND,多个Criteria组之间为OR,完美实现复杂组合查询。
4.3 BLOB大字段查询失效问题
审计日志详情字段 detail 为数据库BLOB大文本字段:
-
MBG不会为BLOB字段生成like、等值查询方法,因为各大数据库大字段语法不统一;
-
普通 selectByExample不查询BLOB字段 ,查询详情快照必须使用 selectByExampleWithBLOBs,否则detail字段永远为null;
-
关键字模糊检索大字段,必须手动调用底层方法拼接SQL条件,实现日志详情模糊搜索能力。
java
// detail 是 BLOB 字段,使用 addCriterion 添加自定义 LIKE 条件
if (StringUtils.isNotBlank(input.getKeyword())) {
criteria.addCriterion("detail like", "%" + input.getKeyword() + "%", "detail");
}
List<SysAuditLog> list = sysAuditLogMapper.selectByExampleWithBLOBs(example);
PageInfo pageInfo = page.toPageInfo();
// 转换为 VO(枚举 code → 中文名、detail JSON → summary)
List<QuerySysAuditLogVo.SysAuditLogItem> itemList = new ArrayList<>();
if (list != null) {
for (SysAuditLog raw : list) {
itemList.add(toItem(raw));
}
}
return QuerySysAuditLogVo.builder()
.list(itemList)
.total(pageInfo.getTotal())
.pageIndex(input.getPageIndex())
.pageSize(input.getPageSize())
.build();
}
五、端到端全流程最终闭环复盘
结合我前后端所有落地代码,我完整串联出用户操作的真实全流程:
-
浏览器访问 /knowledge/logAudit,前端路由匹配成功,加载全局布局、菜单布局、审计日志页面;
-
Less样式渲染自适应页面,i18n国际化渲染所有中文文案,页面初始化完成;
-
页面挂载完成,触发请求,调用封装好的POST分页接口,携带页码、时间、操作人、关键字等筛选参数;
-
全局Request拦截层自动携带Cookie鉴权、统一请求头,发起网络请求;
-
请求到达后端统一Controller,DDD网关根据枚举路由分发到 QuerySysAuditLog 领域模型;
-
领域类执行AOP注解execute方法,通过MBG Example构建动态查询条件,区分普通字段与BLOB大字段查询;
-
Mapper层查询数据库,返回审计日志列表与详情快照数据;
-
后端封装数据返回前端,响应拦截自动剥离外层参数,页面渲染表格数据;
-
用户点击详情,通过ID请求单条日志完整数据,弹窗展示基础信息、变更摘要、前后快照、字段对比,完成全业务闭环。
致谢
谢谢大家的阅读,还有很多不足支出,欢迎大家在评论区指出,如果我的内容对你有帮助,可以点赞 , 收藏 ,大家的支持就是我坚持下去的动力!
"请赐予我平静,去接受我无法改变的 ;赐予我勇气,去改变我能改变的。"
