【审计日志组件实践复盘】前端工程化 + 后端DDD架构(AOP切面拦截日志) + MBG

前言

近期我在项目中独立落地了日志审计完整业务模块,从前端页面布局、路由注册、国际化、接口封装、网络拦截,到后端自研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大文本字段:

  1. MBG不会为BLOB字段生成like、等值查询方法,因为各大数据库大字段语法不统一;

  2. 普通 selectByExample不查询BLOB字段 ,查询详情快照必须使用 selectByExampleWithBLOBs,否则detail字段永远为null;

  3. 关键字模糊检索大字段,必须手动调用底层方法拼接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();
    }

五、端到端全流程最终闭环复盘

结合我前后端所有落地代码,我完整串联出用户操作的真实全流程:

  1. 浏览器访问 /knowledge/logAudit,前端路由匹配成功,加载全局布局、菜单布局、审计日志页面;

  2. Less样式渲染自适应页面,i18n国际化渲染所有中文文案,页面初始化完成;

  3. 页面挂载完成,触发请求,调用封装好的POST分页接口,携带页码、时间、操作人、关键字等筛选参数;

  4. 全局Request拦截层自动携带Cookie鉴权、统一请求头,发起网络请求;

  5. 请求到达后端统一Controller,DDD网关根据枚举路由分发到 QuerySysAuditLog 领域模型;

  6. 领域类执行AOP注解execute方法,通过MBG Example构建动态查询条件,区分普通字段与BLOB大字段查询;

  7. Mapper层查询数据库,返回审计日志列表与详情快照数据;

  8. 后端封装数据返回前端,响应拦截自动剥离外层参数,页面渲染表格数据;

  9. 用户点击详情,通过ID请求单条日志完整数据,弹窗展示基础信息、变更摘要、前后快照、字段对比,完成全业务闭环。

致谢

谢谢大家的阅读,还有很多不足支出,欢迎大家在评论区指出,如果我的内容对你有帮助,可以点赞 , 收藏 ,大家的支持就是我坚持下去的动力!

"请赐予我平静,去接受我无法改变的 ;赐予我勇气,去改变我能改变的。"

相关推荐
xqqxqxxq3 小时前
AI智能旅游规划系统 - 前端技术笔记
前端·笔记·旅游
陈随易9 小时前
bm2,MoonBit实现的pm2替代品
前端·后端·程序员
a11177611 小时前
农业数字孪生大屏网页 html开源
前端·html
圣光SG12 小时前
web操作题练习(六)
前端
奇迹暖暖暖12 小时前
企业邮箱选型与部署:公司用什么邮箱比较好、如何注册企业邮箱
安全·架构·邮箱
闪亮的路灯13 小时前
威联通QTS使用自带web服务期代理前端(类似nginx)
前端·nginx·威联通
kyriewen13 小时前
我用AI写了半年代码——回头看,这5个能力正在退化
前端·javascript·ai编程
IT_陈寒14 小时前
Vite静态资源路径这个坑差点让我加班到凌晨
前端·人工智能·后端
珠***格14 小时前
分布式光伏电站:四可装置如何实现 “可观、可测、可控、可调”
网络·人工智能·分布式·架构·能源