Backend Development 26 min read

Implementing an Audit Function with SpringBoot: Design Options, Database Schema, Backend Controllers, and Vue Frontend Integration

This article explains four audit implementation approaches, provides the SQL schema for the audit and file tables, shows complete SpringBoot controller code for handling audit operations and file uploads, and presents full Vue front‑end code for creating, editing, listing, and approving audit records, offering a comprehensive guide for backend developers.

Top Architect
Top Architect
Top Architect
Implementing an Audit Function with SpringBoot: Design Options, Database Schema, Backend Controllers, and Vue Frontend Integration

The article introduces four ways to implement an audit feature in a system: a simple two‑table approach, a modal dialog approach, a buffered input approach, and a temporary‑table approach, each with its own advantages and disadvantages.

Database Schema

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;
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;

SpringBoot Implementation

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.Sanitation;
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;

    @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("/{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();
        // if (RoleEnum.ROLE_USER.toString().equals(currentUser.getRole())) {
        //     queryWrapper.eq("user", currentUser.getUsername());
        // }
        return Result.success(auditService.page(new Page<>(pageNum, pageSize), queryWrapper));
    }
}

Vue Front‑End Code (Audit List and Form)

<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-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>
    </el-table>
    <div style="padding: 10px 0">
      <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>
    </div>
    <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",
      roles: [],
      user: localStorage.getItem("user") ? JSON.parse(localStorage.getItem("user")) : {}
    };
  },
  created() {
    this.load();
  },
  methods: {
    load: function () {
      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;
      });
    },
    home() {
      this.$router.push("/");
    },
    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("保存失败");
        }
      });
    },
    handleAdd() {
      this.dialogFormVisible = true;
      this.form = {};
    },
    handleEdit(row) {
      this.form = row;
      this.dialogFormVisible = true;
    },
    handleSelectionChange(val) {
      console.log(val);
      this.multipleSelection = val;
    },
    delBatch() {
      let 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("删除信息失败");
        }
      });
    },
    reset() {
      this.name = "";
      this.load();
    },
    handleSizeChange(pageSize) {
      console.log(pageSize);
      this.pageSize = pageSize;
      this.load();
    },
    handleCurrentChange(pageNum) {
      console.log(pageNum);
      this.pageNum = pageNum;
      this.load();
    },
    handleImgUploadSuccess(res) {
      this.form.img = res;
    },
    changeState(row, state) {
      this.form = JSON.parse(JSON.stringify(row));
      this.form.state = state;
      this.save();
    }
  }
};
</script>

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

File Upload Controller (SpringBoot)

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();
        // Define a unique identifier for the file
        String fileUUID = IdUtil.fastSimpleUUID() + StrUtil.DOT + type;
        File uploadFile = new File(fileUploadPath + fileUUID);
        // Ensure the directory exists
        File parentFile = uploadFile.getParentFile();
        if (!parentFile.exists()) {
            parentFile.mkdirs();
        }
        String url;
        // Compute MD5 of the file
        String md5 = SecureUtil.md5(file.getInputStream());
        // Check if a file with the same MD5 already exists
        Files dbFiles = getFileByMd5(md5);
        if (dbFiles != null) {
            url = dbFiles.getUrl();
        } else {
            // Save the file to disk
            file.transferTo(uploadFile);
            url = "http://" + serverIp + ":9090/file/" + fileUUID;
        }
        // Store file metadata in the database
        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.size() == 0 ? 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);
    }
}

The article concludes by encouraging readers to like the post if it helped them and invites further discussion, while also promoting related community resources and services.

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