Full-Stack B/S Pandemic Relief Supply Management System Built with Spring Boot, Vue, and MySQL
The Browser-Server (B/S) architecture eliminates dedicated local client installation, requiring only a standard web browser. Clients initiate HTTP/HTTPS requests to a centralized server; the server processes these operations against business logic and a persistent database, then returns structured Vue-driven UI components or JSON payloads for rendering.
Core System Workflows
Key operational workflows include user onboarding/authentication, regional metadata management, supply catalog registration, inventory visibility, stock request submission, and multi-role stock approval/rejecsion.
Architecture Overview
The system is split into three logical layers:
- Presentation Layer: Vue-based single-page application (SPA) handling user interactions, form validation, and dynamic data display.
- Service Layer: Spring Boot RESTful APIs implementing business rules, role-based access control (RBAC), and data transformation.
- Data Layer: MySQL relational database storing user credentials, regional details, supply inventory, request records, and audit trails.
Database Overview
Critical tables include sys_user (for system and regional admins, requesters), region_info (district-level inventory zones), relief_supply (item metadata and stock counts), supply_request (request lifecycle records), and approval_log (audit entries for review actions).
Key Functional Implementation
User Authentication & Role Assignment
The authentication flow combines username/password validation, CAPTCHA verification, and Apache Shiro RBAC. Users select their assigned role during login to access appropriate dashboards.
Regional Metadata Management
Authorized regional admins can register, edit, or deactivate inventory zones, each mapped to a unique admin account for localized oversight.
Supply Inventory & Request Pipeline
- Catalog Registration: Regional admins add relief items with details like category, quantity, expiration date, and storage location.
- Inventory Visibility: All authenticated users view a filterable, paginated list of active supplies across accessible regions.
- Request Submission: Requesters draft requests specifying item, quantity, region, and urgency, then submit for review.
- Approval Workflow: Regional/system admins review requests, approve/reject with comments, and update inventory or notify requesters accordingly.
Configuration & Code Snippets
Spring Boot Data base Configuration
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
hikari:
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://127.0.0.1:3306/pandemic_supply_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: supply_admin
password: SecurePass123!
maximum-pool-size: 50
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
pool-name: ReliefSupplyHikariPool
Vue Login Component
<template>
<div class="login-container">
<el-card class="login-card">
<h2 class="login-title">Relief Supply System</h2>
<el-form :model="loginForm" :rules="loginRules" ref="loginFormRef" label-width="0px">
<el-form-item prop="account">
<el-input v-model="loginForm.account" placeholder="Enter account" prefix-icon="User" />
</el-form-item>
<el-form-item prop="credential">
<el-input v-model="loginForm.credential" type="password" placeholder="Enter password" prefix-icon="Lock" show-password />
</el-form-item>
<el-form-item prop="verifyCode">
<el-row :gutter="10">
<el-col :span="16">
<el-input v-model="loginForm.verifyCode" placeholder="Enter captcha" prefix-icon="Document" @keyup.enter="handleLogin" />
</el-col>
<el-col :span="8">
<img :src="captchaUrl" @click="refreshCaptcha" alt="Click to refresh" class="captcha-img" />
</el-col>
</el-row>
</el-form-item>
<el-form-item>
<el-select v-model="loginForm.role" placeholder="Select role" style="width: 100%;">
<el-option label="Regional Admin" value="REGIONAL_ADMIN" />
<el-option label="System Admin" value="SYSTEM_ADMIN" />
<el-option label="Supply Requester" value="REQUESTER" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" style="width: 100%;" @click="handleLogin" :loading="loginLoading">Login</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
import axios from 'axios'
const router = useRouter()
const loginFormRef = ref(null)
const loginLoading = ref(false)
const captchaUrl = ref('')
const loginForm = reactive({
account: '',
credential: '',
verifyCode: '',
role: ''
})
const loginRules = {
account: [{ required: true, message: 'Please enter account', trigger: 'blur' }],
credential: [{ required: true, message: 'Please enter password', trigger: 'blur' }],
verifyCode: [{ required: true, message: 'Please enter captcha', trigger: 'blur' }],
role: [{ required: true, message: 'Please select role', trigger: 'change' }]
}
const refreshCaptcha = () => {
captchaUrl.value = `/api/auth/captcha?timestamp=${Date.now()}`
}
const handleLogin = async () => {
await loginFormRef.value.validate(async (valid) => {
if (!valid) return
loginLoading.value = true
try {
const res = await axios.post('/api/auth/login', loginForm)
if (res.data.code === 200) {
localStorage.setItem('token', res.data.data.token)
localStorage.setItem('userRole', loginForm.role)
ElMessage.success('Login successful')
router.push('/dashboard')
} else {
ElMessage.error(res.data.message)
refreshCaptcha()
}
} catch (err) {
ElMessage.error('Network error, please try again')
} finally {
loginLoading.value = false
}
})
}
onMounted(() => {
refreshCaptcha()
})
</script>
<style scoped>
.login-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f0f2f5;
}
.login-card {
width: 400px;
padding: 20px;
}
.login-title {
text-align: center;
margin-bottom: 30px;
}
.captcha-img {
width: 100%;
height: 40px;
cursor: pointer;
border-radius: 4px;
}
</style>
Spring Boot Authentication Controller
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private CaptchaService captchaService;
@Autowired
private JwtService jwtService;
@GetMapping("/captcha")
public Map<String, String> getCaptcha(HttpSession session) {
String uuid = UUID.randomUUID().toString();
String code = captchaService.generateCaptcha();
session.setAttribute(uuid, code);
Map<String, String> result = new HashMap<>();
result.put("uuid", uuid);
result.put("image", captchaService.generateCaptchaImageBase64(code));
return result;
}
@PostMapping("/login")
public Map<String, Object> login(@RequestBody LoginRequest request, HttpSession session) {
Map<String, Object> response = new HashMap<>();
String storedCode = (String) session.getAttribute(request.getUuid());
if (storedCode == null || !storedCode.equalsIgnoreCase(request.getVerifyCode())) {
response.put("code", 400);
response.put("message", "Invalid captcha");
return response;
}
session.removeAttribute(request.getUuid());
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(
request.getAccount(), request.getCredential(), request.getRole()
);
try {
subject.login(token);
String jwt = jwtService.createToken(request.getAccount(), request.getRole());
response.put("code", 200);
response.put("message", "Success");
Map<String, String> data = new HashMap<>();
data.put("token", jwt);
response.put("data", data);
} catch (UnknownAccountException e) {
response.put("code", 401);
response.put("message", "Account not found");
} catch (IncorrectCredentialsException e) {
response.put("code", 401);
response.put("message", "Incorrect credentials");
} catch (LockedAccountException e) {
response.put("code", 403);
response.put("message", "Account locked");
} catch (AuthenticationException e) {
response.put("code", 401);
response.put("message", "Authentication failed");
}
return response;
}
}