Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Full-Stack B/S Pandemic Relief Supply Management System Built with Spring Boot, Vue, and MySQL

Tech May 8 4

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:

  1. Presentation Layer: Vue-based single-page application (SPA) handling user interactions, form validation, and dynamic data display.
  2. Service Layer: Spring Boot RESTful APIs implementing business rules, role-based access control (RBAC), and data transformation.
  3. 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

  1. Catalog Registration: Regional admins add relief items with details like category, quantity, expiration date, and storage location.
  2. Inventory Visibility: All authenticated users view a filterable, paginated list of active supplies across accessible regions.
  3. Request Submission: Requesters draft requests specifying item, quantity, region, and urgency, then submit for review.
  4. 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;
    }
}

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.