Backend Development 23 min read

Implementing an Audit Function in SpringBoot: Design Options and Full‑Stack Code Example

This article explains four design patterns for implementing an audit workflow, compares their advantages and disadvantages, and provides complete SpringBoot backend, MySQL table definitions, and Vue front‑end code to build a functional audit system with file upload support.

Top Architect
Top Architect
Top Architect
Implementing an Audit Function in SpringBoot: Design Options and Full‑Stack Code Example

The article begins by outlining four common ways to implement an audit feature in a business system: a simple two‑table approach, a modal‑dialog approach, a buffered‑parameter approach, and a temporary‑table approach, each with its own pros and cons.

1. Design Options

Ordinary: Insert data into table A, read from A after approval, then write to target table B. Simple logic but tightly couples audit to backend functions.

Modal Dialog: Front‑end checks permission and opens a dialog for the reviewer; after approval the operation proceeds. No backend embedding, supports query/export.

Buffered Parameters: Audit data is stored independently; after approval the backend triggers the actual business logic. Supports async operations and unified data handling, but requires framework support.

Temporary Table: Each audited table gets a corresponding temp table with an extra audit‑flow field; data is first written to the temp table and synced after approval. No framework dependency, supports async and export.

2. SpringBoot Implementation

SQL to create the audit table:

CREATE TABLE `audit` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '报修名称',
  `user` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '报修人',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '报修时间',
  `img` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '详情图片',
  `state` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT '待审核' COMMENT '待审核,审核通过,审核不通过',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Key parts of AuditController (REST API for CRUD, pagination, batch delete, and state change):

@RestController
@RequestMapping("/audit")
public class AuditController {
    @Resource
    private IAuditService auditService;
    @Resource
    private FileMapper fileMapper;

    @PostMapping
    public Result save(@RequestBody Audit audit) {
        if (audit.getId() == null) {
            audit.setUser(TokenUtils.getCurrentUser().getUsername());
        }
        auditService.saveOrUpdate(audit);
        return Result.success();
    }

    @PostMapping("/del/batch")
    public Result deleteBatch(@RequestBody List
ids) {
        return Result.success(auditService.removeByIds(ids));
    }

    @GetMapping
    public Result findAll() {
        return Result.success(auditService.list());
    }

    @GetMapping("/page")
    public Result findPage(@RequestParam Integer pageNum,
                           @RequestParam Integer pageSize,
                           @RequestParam(defaultValue = "") String name) {
        QueryWrapper
qw = new QueryWrapper<>();
        qw.orderByDesc("id");
        if (!"".equals(name)) {
            qw.like("name", name);
        }
        return Result.success(auditService.page(new Page<>(pageNum, pageSize), qw));
    }

    // other endpoints omitted for brevity
}

3. Front‑End Integration (Vue + Element‑UI)

Vue template for the audit list, search, add, edit, and batch delete:

<template>
  <div>
    <el-input v-model="name" placeholder="请输入报修描述"></el-input>
    <el-button type="primary" @click="load">搜索</el-button>
    <el-button type="warning" @click="reset">刷新</el-button>
    <el-button type="primary" @click="handleAdd">新增</el-button>
    <el-popconfirm title="确定批量删除这些信息吗?" @confirm="delBatch">
      <el-button type="danger" slot="reference">删除</el-button>
    </el-popconfirm>
    <el-table :data="tableData" border stripe>
      <el-table-column type="selection" width="55"></el-table-column>
      <el-table-column prop="name" label="报修描述"></el-table-column>
      <el-table-column prop="user" label="用户"></el-table-column>
      <el-table-column prop="createTime" label="创建时间"></el-table-column>
      <el-table-column label="图片">
        <template slot-scope="scope">
          <el-image :src="scope.row.img" style="width:100px;height:100px"></el-image>
        </template>
      </el-table-column>
      <el-table-column prop="state" label="进度"></el-table-column>
      <el-table-column label="操作" width="240">
        <template slot-scope="scope">
          <el-button type="success" @click="changeState(scope.row,'审核通过')" :disabled="scope.row.state!=='待审核'">审核通过</el-button>
          <el-button type="danger" @click="changeState(scope.row,'审核不通过')" :disabled="scope.row.state!=='待审核'">审核不通过</el-button>
        </template>
      </el-table-column>
    </el-table>
    <el-pagination :current-page="pageNum" :page-size="pageSize" :total="total"
                    @size-change="handleSizeChange" @current-change="handleCurrentChange"></el-pagination>
    <el-dialog title="用户信息" :visible.sync="dialogFormVisible" width="30%">
</el-dialog>
  </div>
</template>

<script>
export default {
  name: "Audit",
  data() { return { tableData: [], total: 0, pageNum: 1, pageSize: 5, name: "", form: {}, dialogFormVisible: false, multipleSelection: [] }; },
  created() { this.load(); },
  methods: {
    load() { this.request.get('/audit/page', { params: { pageNum: this.pageNum, pageSize: this.pageSize, name: this.name } })
      .then(res => { this.tableData = res.data.records; this.total = res.data.total; }); },
    handleAdd() { this.dialogFormVisible = true; this.form = {}; },
    changeState(row, state) { this.form = JSON.parse(JSON.stringify(row)); this.form.state = state; this.save(); },
    save() { this.request.post('/audit', this.form).then(res => { if (res.code === '200') { this.$message.success('保存成功'); this.dialogFormVisible = false; this.load(); } else { this.$message.error('保存失败'); } }); },
    // pagination and batch delete methods omitted for brevity
  }
}
</script>

4. Backend Management Page (similar Vue code for admin view)

The admin page reuses the same table component but removes the edit button and adds a batch‑delete pop‑confirm.

5. File Upload Handling

SQL for the file table:

CREATE TABLE `file` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '文件名称',
  `type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '文件类型',
  `size` bigint DEFAULT NULL COMMENT '文件大小(kb)',
  `url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '下载链接',
  `md5` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '文件md5',
  `creat_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '时间',
  `is_delete` tinyint(1) DEFAULT '0' COMMENT '是否删除',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=115 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Key part of FileController that uploads a file, checks MD5 to avoid duplicates, stores metadata, and returns the accessible URL:

@RestController
@RequestMapping("/file")
public class FileController {
    @Value("${files.upload.path}")
    private String fileUploadPath;
    @Value("${server.ip}")
    private String serverIp;
    @Resource
    private FileMapper fileMapper;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @PostMapping("/upload")
    public String upload(@RequestParam MultipartFile file) throws IOException {
        String originalFilename = file.getOriginalFilename();
        String type = FileUtil.extName(originalFilename);
        long size = file.getSize();
        String fileUUID = IdUtil.fastSimpleUUID() + "." + type;
        File uploadFile = new File(fileUploadPath + fileUUID);
        if (!uploadFile.getParentFile().exists()) uploadFile.getParentFile().mkdirs();
        String md5 = SecureUtil.md5(file.getInputStream());
        Files dbFiles = getFileByMd5(md5);
        String url;
        if (dbFiles != null) {
            url = dbFiles.getUrl();
        } else {
            file.transferTo(uploadFile);
            url = "http://" + serverIp + ":9090/file/" + fileUUID;
        }
        Files saveFile = new Files();
        saveFile.setName(originalFilename);
        saveFile.setType(type);
        saveFile.setSize(size / 1024);
        saveFile.setUrl(url);
        saveFile.setMd5(md5);
        fileMapper.insert(saveFile);
        return url;
    }

    private Files getFileByMd5(String md5) {
        QueryWrapper
qw = new QueryWrapper<>();
        qw.eq("md5", md5);
        List
list = fileMapper.selectList(qw);
        return list.isEmpty() ? null : list.get(0);
    }

    // download, update, delete, pagination methods omitted for brevity
}

6. Conclusion

The article demonstrates how to choose an appropriate audit implementation strategy, provides complete SpringBoot back‑end APIs, MySQL schema, and Vue front‑end components, and includes a reusable file‑upload service, giving readers a practical reference for building audit workflows in Java web applications.

BackendVueMySQLSpringBootRESTaudit
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.