This system provides a unified platform for managing laboratory assets, booking schedules, equipment maintenance logs, and user permissions across academic institutions. Built with a modern full-stack architecture, it supports web and mobile access through responsive Vue-based dashboards and cross-platform uniapp clients.
Architecture Overview
Backend: Spring Boot 2.7+
The backend leverages Spring Boot’s auto-configuration to minimize boilerplate setup. Embedded Tomcat eliminates external server dependencies, while Spring Security handles role-based authentication and endpoint protection. Key integrations include:
- Spring Data JPA for type-safe repository operations
- Spring Validation for declarative input constraints
- Spring AOP for centralized logging and audit trails
- RESTful API design following RFC 8288 conventions
Frontend: Vue 3 Composition API
The admin interface uses Vue 3 with the Composition API and Pinia for state management. Components follow atomic design principles—each encapsulates logic, template, and styling. Routing is handled via Vue Router with lazy-loaded modules to reduce initial bundle size. Authentication state is persisted using secure HTTP-only cookies.
Mobile Client: UniApp 3.x
The student and faculty mobile app is built with UniApp, enabling single-codebase deploymnet to iOS, Android, and WeChat Mini Programs. It consumes the same REST API as the web client and implements offline-first strategies using localForage for cached lab availability data.
Authentication & Authorization Flow
A token-based session mechanism replaces traditional cookie sessions. Upon successful login, the backend issues a time-bound JWT containing user ID, role, and scope. Each protected request includes the token in the Authorization header. An interceptor validates tokens against an in-memory cache backed by Redis for low-latency lookups.
Below is a refactored version of the authentication handler that improves security posture and maintainability:
public class AuthInterceptor implements HandlerInterceptor {
private static final String AUTH_HEADER = "Authorization";
private static final String BEARER_PREFIX = "Bearer ";
@Autowired
private JwtTokenService jwtService;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// Skip preflight requests
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
return true;
}
// Allow public endpoints annotated with @PermitAll
if (isPublicEndpoint(handler)) {
return true;
}
String authHeader = request.getHeader(AUTH_HEADER);
if (authHeader == null || !authHeader.startsWith(BEARER_PREFIX)) {
sendUnauthorizedResponse(response, "Missing or malformed token");
return false;
}
String rawToken = authHeader.substring(BEARER_PREFIX.length());
try {
JwtPayload payload = jwtService.verifyAndDecode(rawToken);
SecurityContextHolder.getContext()
.setAuthentication(new UsernamePasswordAuthenticationToken(
payload.getUsername(),
null,
Collections.singletonList(new SimpleGrantedAuthority(payload.getRole()))
));
return true;
} catch (JwtException e) {
sendUnauthorizedResponse(response, "Invalid or expired token");
return false;
}
}
private void sendUnauthorizedResponse(HttpServletResponse response, String message) throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(
new ObjectMapper().writeValueAsString(Map.of("code", 401, "msg", message))
);
}
private boolean isPublicEndpoint(Object handler) {
if (handler instanceof HandlerMethod) {
HandlerMethod method = (HandlerMethod) handler;
return method.hasMethodAnnotation(PermitAll.class) ||
method.getBeanType().isAnnotationPresent(PermitAll.class);
}
return false;
}
}
Database Schema Highlights
The relational model centers around four core entities: lab, equipment, booking, and user_profile. Foreign key constraints enforce referential integrity, and indexes are applied on frequently queried columns such as booking.start_time and equipment.status.
Example DDL for the session tracking table:
CREATE TABLE `session_token` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`user_id` BIGINT NOT NULL,
`token_hash` CHAR(64) NOT NULL COMMENT 'SHA-256 hash of raw token',
`role` ENUM('admin', 'instructor', 'student', 'technician') NOT NULL,
`issued_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`expires_at` DATETIME NOT NULL,
`last_used` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user_role (user_id, role),
INDEX idx_expires (expires_at),
FOREIGN KEY (`user_id`) REFERENCES `user_profile`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Functional Testing Strategy
Testing follows a layered approach:
- Unit tests cover service-layer business logic using JUnit 5 and Mockito
- Integration tests validate controller endpoints with @WebMvcTest and TestRestTemplate
- E2E tests simulate user workflows using Cypress for web and Appium for mobile
Key test scenarios include:
- Concurrent booking attempts for the same lab slot
- Role-specific UI rendering (e.g., students cannot view maintenance cost fields)
- Timezone-aware scheduling across multiple campuses
- Equipment status transitions (available → reserved → under_maintenance → available)