How to Build a SpringBoot Audit System with Vue Frontend and File Upload
This article walks through four audit implementation strategies, provides the SQL schema and Java SpringBoot controller for an audit feature, shows how to integrate it with a Vue front‑end including file upload handling, and offers complete code snippets for both back‑end and front‑end components.
1. Audit Function Implementation Methods
1) Normal
Solution: Insert data into table A during processing, read from A after audit, then operate the target B table.
Advantages: Simple logic.
Disadvantages: Tight coupling with back‑end functions; data operations are not unified.
2) Popup
Solution: Implement on the front‑end; when an operation requires permission, show a popup for the auditor to approve. After approval, continue the operation.
Advantages: No back‑end embedding; supports query, export, and all operations.
Disadvantages: Requires both the requester and auditor to be present.
3) Buffering Parameters
Solution: The audit function is independent. The front‑end stores request parameters in the database; after approval, the back‑end triggers the corresponding API and notifies the requester of the result.
Advantages: No embedding in front or back end; supports export and operation; requester and auditor can work asynchronously; unified data handling.
Disadvantages: Requires framework support; slightly more complex logic.
4) Temporary Table
Solution: Add a corresponding temporary table for each audited entity, adding only an audit flow field while keeping other fields identical. Operations first write to the temporary table; after approval, the back‑end syncs data to the main table.
Advantages: No framework dependency; supports export and operation; asynchronous requester and auditor; unified data handling.
Disadvantages: High coupling with back‑end functions.
2. SpringBoot Implementation
2.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 'Report name',
`user` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'Reporter',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT 'Report time',
`img` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'Detail image',
`state` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT 'Pending' COMMENT 'Pending, Approved, Rejected',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;2.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;
@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<Integer> 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<Audit> 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<Audit> 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
3.1 Vue Frontend Page (Simplified)
<template>
<div>
<!-- Search Bar -->
<div style="margin:10px 0">
<el-input v-model="name" placeholder="Enter report description" clearable style="width:200px"></el-input>
<el-button type="primary" @click="load"><i class="el-icon-search"></i> Search</el-button>
<el-button type="warning" @click="reset"><i class="el-icon-refresh"></i> Refresh</el-button>
</div>
<!-- Action Buttons -->
<div style="margin:10px 0">
<el-button type="primary" @click="handleAdd" class="ml-10"><i class="el-icon-circle-plus-outline"></i> Add</el-button>
<el-popconfirm title="Confirm batch delete?" @confirm="delBatch">
<el-button type="danger" slot="reference"><i class="el-icon-remove-outline"></i> Delete</el-button>
</el-popconfirm>
</div>
<!-- Data Table -->
<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="Report Description"></el-table-column>
<el-table-column prop="user" label="User"></el-table-column>
<el-table-column prop="createTime" label="Create Time"></el-table-column>
<el-table-column label="Image">
<template slot-scope="scope">
<el-image :src="scope.row.img" style="width:100px;height:100px" :preview-src-list="[scope.row.img]"></el-image>
</template>
</el-table-column>
<el-table-column prop="state" label="Status"></el-table-column>
<el-table-column label="Actions" width="240">
<template v-slot="scope">
<el-button type="success" @click="changeState(scope.row,'Approved')" :disabled="scope.row.state !== 'Pending'">Approve</el-button>
<el-button type="danger" @click="changeState(scope.row,'Rejected')" :disabled="scope.row.state !== 'Pending'">Reject</el-button>
</template>
</el-table-column>
</el-table>
<!-- Pagination -->
<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>
<!-- Dialog for Add/Edit -->
<el-dialog title="Report" :visible.sync="dialogFormVisible" width="30%">
<el-form label-width="100px" size="small">
<el-form-item label="Description">
<el-input v-model="form.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="Image">
<el-upload action="http://localhost:9090/file/upload" :on-success="handleImgUploadSuccess">
<el-button size="small" type="primary">Upload</el-button>
</el-upload>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">Cancel</el-button>
<el-button type="primary" @click="save">Confirm</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; });
},
save() { this.request.post("/audit", this.form).then(res => { if (res.code === '200') { this.$message.success("Saved"); this.dialogFormVisible = false; this.load(); } else { this.$message.error("Save failed"); } }); },
handleAdd() { this.dialogFormVisible = true; this.form = {}; },
handleEdit(row) { this.form = row; this.dialogFormVisible = true; },
handleSelectionChange(val) { this.multipleSelection = val; },
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("Deleted"); this.load(); } else { this.$message.error("Delete failed"); } }); },
reset() { this.name = ""; this.load(); },
handleSizeChange(pageSize) { this.pageSize = pageSize; this.load(); },
handleCurrentChange(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>4. 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();
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 dbFile = getFileByMd5(md5);
String url;
if (dbFile != null) {
url = dbFile.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<Files> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("md5", md5);
List<Files> list = fileMapper.selectList(queryWrapper);
return list.isEmpty() ? null : list.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<Integer> ids) {
QueryWrapper<Files> queryWrapper = new QueryWrapper<>();
queryWrapper.in("id", ids);
List<Files> 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<Files> 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. Summary
This guide demonstrates four audit implementation patterns, provides a complete SpringBoot back‑end with SQL schema and controller code, shows how to connect it to a Vue front‑end that supports adding, editing, approving, and rejecting reports, and includes a reusable file‑upload controller that handles deduplication via MD5 and stores metadata in a database.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java High-Performance Architecture
Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
