Backend Development 27 min read

Implementing an Audit Functionality with SpringBoot: Design Options and Complete Code Example

This article explains various approaches to implementing an audit feature in a SpringBoot application, compares their pros and cons, provides detailed SQL schema, Java backend controller code, and Vue frontend integration, and includes complete code snippets for database tables, file handling, and UI components.

Top Architect
Top Architect
Top Architect
Implementing an Audit Functionality with SpringBoot: Design Options and Complete Code Example

1. Audit Function Implementation Methods

1) Ordinary

Solution: Insert data into table A during processing, read from A after audit, then operate on target table B.

Advantages: Simple logic.

Disadvantages: Tight coupling with backend; audit data operations are not unified.

2) Modal Dialog

Solution: Frontend determines if permission control is needed; if so, a modal dialog is shown for the auditor to approve, after which the operation proceeds.

Advantages: No backend embedding; supports query, export, and all operations.

Disadvantages: Requires both requester and auditor to be present.

3) Buffering Input

Solution: Audit is an independent function; frontend stores request parameters in the database, and after approval the backend triggers the corresponding interface and notifies the requester.

Advantages: No embedding in front or back end; supports export and operations; requester and auditor can work asynchronously; audit data operations are unified.

Disadvantages: Requires framework support; logic is slightly more complex.

4) Temporary Table

Solution: Add a temporary audit table for each business table, containing an extra audit‑flow field while keeping other fields identical; operations first write to the temporary table, and after approval the backend syncs data to the main table.

Advantages: No framework dependency; supports export and operations; asynchronous work; unified audit data handling.

Disadvantages: High embedding with backend functions.

2. SpringBoot Implementation

1. Create Database Table SQL

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;

2. Java Backend (AuditController)

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.demo.common.Result;
import com.example.demo.entity.Audit;
import com.example.demo.entity.User;
import com.example.demo.mapper.FileMapper;
import com.example.demo.service.IAuditService;
import com.example.demo.utils.TokenUtils;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.util.List;

@CrossOrigin
@RestController
@RequestMapping("/audit")
public class AuditController {

    @Resource
    private IAuditService auditService;

    @Resource
    private FileMapper fileMapper;

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

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

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

    @GetMapping("/{id}")
    public Result findOne(@PathVariable Integer id) {
        return Result.success(auditService.getById(id));
    }

    @GetMapping("/username/{username}")
    public Result findByUsername(@PathVariable String username) {
        QueryWrapper
queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username", username);
        return Result.success(auditService.getOne(queryWrapper));
    }

    @GetMapping("/page")
    public Result findPage(@RequestParam Integer pageNum,
                           @RequestParam Integer pageSize,
                           @RequestParam(defaultValue = "") String name) {
        QueryWrapper
queryWrapper = new QueryWrapper<>();
        queryWrapper.orderByDesc("id");
        if (!"".equals(name)) {
            queryWrapper.like("name", name);
        }
        User currentUser = TokenUtils.getCurrentUser();
        // Role based filtering can be added here
        return Result.success(auditService.page(new Page<>(pageNum, pageSize), queryWrapper));
    }
}

3. Frontend Integration (Vue + Element‑UI)

1. Effect Screenshot

(Image omitted – shows a table with audit records and action buttons.)

2. Core Table Column Code

<el-table-column label="审核" width="240">
  <template v-slot="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>

3. Complete Vue Page Code

<template>
  <div>
    <div style="margin: 10px 0">
      <el-input style="width: 200px; margin-left: 10px" placeholder="请输入报修描述" clearable v-model="name"></el-input>
      <el-button class="ml-5" type="primary" @click="load"><i class="el-icon-search"/>搜索</el-button>
      <el-button type="warning" @click="reset"><i class="el-icon-refresh"/>刷新</el-button>
    </div>
    <div style="margin: 10px 0">
      <el-button type="primary" @click="handleAdd" class="ml-10"><i class="el-icon-circle-plus-outline"/>新增</el-button>
      <el-popconfirm class="ml-5" confirm-button-text='确认' cancel-button-text='取消' icon="el-icon-info" icon-color="red" title="确定批量删除这些信息吗?" @confirm="delBatch">
        <el-button type="danger" slot="reference"><i class="el-icon-remove-outline"/>删除</el-button>
      </el-popconfirm>
    </div>
    <el-table :data="tableData" border stripe :header-cell-class-name="'headerBg'" @selection-change="handleSelectionChange">
      <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 style="width: 100px; height: 100px" :src="scope.row.img" :preview-src-list="[scope.row.img]"></el-image>
        </template>
      </el-table-column>
      <el-table-column prop="state" label="进度"></el-table-column>
</el-table>
    <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="pageNum" :page-sizes="[5,10,15]" :page-size="pageSize" layout="total, sizes, prev, pager, next, jumper" :total="total"></el-pagination>
    <el-dialog title="用户信息" :visible.sync="dialogFormVisible" width="30%">
      <el-form label-width="100px" size="small">
        <el-form-item label="报修描述">
          <el-input v-model="form.name" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="物品图片">
          <el-upload action="http://localhost:9090/file/upload" ref="img" :on-success="handleImgUploadSuccess">
            <el-button size="small" type="primary">点击上传</el-button>
          </el-upload>
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button @click="dialogFormVisible = false">取 消</el-button>
        <el-button type="primary" @click="save">确 定</el-button>
      </div>
    </el-dialog>
  </div>
</template>

<script>
export default {
  name: "Audit",
  data() {
    return {
      tableData: [],
      total: 0,
      pageNum: 1,
      pageSize: 5,
      name: "",
      form: {},
      dialogFormVisible: false,
      multipleSelection: [],
      headerBg: "headerBg",
      user: JSON.parse(localStorage.getItem("user") || "{}")
    };
  },
  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; });
    },
    reset() { this.name = ""; this.load(); },
    handleAdd() { this.dialogFormVisible = true; this.form = {}; },
    handleEdit(row) { this.form = row; this.dialogFormVisible = true; },
    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("保存失败"); }
      });
    },
    delBatch() {
      const ids = this.multipleSelection.map(v => v.id);
      this.request.post("/audit/del/batch", ids).then(res => {
        if (res.code === '200') { this.$message.success("删除信息成功"); this.load(); }
        else { this.$message.error("删除信息失败"); }
      });
    },
    handleSelectionChange(val) { this.multipleSelection = val; },
    handleSizeChange(size) { this.pageSize = size; this.load(); },
    handleCurrentChange(num) { this.pageNum = num; this.load(); },
    changeState(row, state) { this.form = JSON.parse(JSON.stringify(row)); this.form.state = state; this.save(); },
    handleImgUploadSuccess(res) { this.form.img = res; }
  }
};
</script>

<style>
.headerBg { background: #eee !important; }
</style>

4. File Upload Backend (FileController)

1. File Table SQL

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;

2. FileController Java Code

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.demo.common.Constants;
import com.example.demo.common.Result;
import com.example.demo.entity.Files;
import com.example.demo.mapper.FileMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.List;

@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() + StrUtil.DOT + type;
        File uploadFile = new File(fileUploadPath + fileUUID);
        File parentFile = uploadFile.getParentFile();
        if (!parentFile.exists()) { parentFile.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;
    }

    @GetMapping("/{fileUUID}")
    public void download(@PathVariable String fileUUID, HttpServletResponse response) throws IOException {
        File uploadFile = new File(fileUploadPath + fileUUID);
        ServletOutputStream os = response.getOutputStream();
        response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileUUID, "UTF-8"));
        response.setContentType("application/octet-stream");
        os.write(FileUtil.readBytes(uploadFile));
        os.flush();
        os.close();
    }

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

    @PostMapping("/update")
    public Result update(@RequestBody Files files) {
        fileMapper.updateById(files);
        flushRedis(Constants.FILES_KEY);
        return Result.success();
    }

    @GetMapping("/detail/{id}")
    public Result getById(@PathVariable Integer id) {
        return Result.success(fileMapper.selectById(id));
    }

    @DeleteMapping("/{id}")
    public Result delete(@PathVariable Integer id) {
        Files files = fileMapper.selectById(id);
        files.setIsDelete(true);
        fileMapper.updateById(files);
        flushRedis(Constants.FILES_KEY);
        return Result.success();
    }

    @PostMapping("/del/batch")
    public Result deleteBatch(@RequestBody List
ids) {
        QueryWrapper
queryWrapper = new QueryWrapper<>();
        queryWrapper.in("id", ids);
        List
files = fileMapper.selectList(queryWrapper);
        for (Files file : files) {
            file.setIsDelete(true);
            fileMapper.updateById(file);
        }
        return Result.success();
    }

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

    private void flushRedis(String key) {
        stringRedisTemplate.delete(key);
    }
}

5. Conclusion

The article provides a comprehensive overview of how to design and implement an audit feature using SpringBoot, covering multiple architectural options, detailed SQL definitions, full Java controller implementations, and Vue front‑end integration, enabling developers to choose the most suitable approach for their projects.

backendDatabaseVueSpringBootauditFileUpload
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.