Building a Curated Chinese Animation Portal: Spring Boot and Vue.js Architecture Guide
System Architecture Overview
The platform operates on a decoupled Client/Server model utilizing a Browser/Server topology. The frontend layer, powered by Vue.js, handles dynamic rendering, state management, and user interaction flows. The backend leverages Spring Boot to expose RESTful endpoints for data processing, business logic execution, and database orchestration. MySQL serves as the relational storage engine, optimized for high-throughput read/write operations typical in content aggregation platforms.
Core functional domains include an interactive community space for content discovery, comment threads, and engagement metrics (views, likes), alongside an administrative console for role-based access control, user moderation, and analytics tracking.
Module Workflow Design
Authentication Sequence: Upon submitting credentials, the frontend transmits a JSON payload to the /api/auth/login endpoint. The backend validates inputs against the account repository, verifies cryptographic signatures, checks account status flags, and issues a signed session token. This token is cached client-side and appended to subsequent HTTP headers for authorized requests.
Content Submission Sequence: Authenticated users initiate post creation via the dashboard. The frontend serializes form data and metadata (tags, categories, media assets). The API gateway intercepts the request, performs input sanitization, delegates to the service layer for persistence mapping, and returns a structured success response containing the generated resource identifier.
Relational Data Modeling
The database schema normalizes entities in to five primary tables to support efficient querying and referential integrity:
| Table Name | Key Fields | Description |
|---|---|---|
user_accounts |
id, username, email, password_hash, status, created_at |
Stores credential data, verification states, and lifecycle timestamps. |
role_definitions |
id, role_name, description |
Defines permission tiers (e.g., Viewer, Editor, Administrator). |
permission_mappings |
id, role_id, menu_path, access_level |
Associates roles with navigable routes and action capabilities. |
content_articles |
id, title, summary, category, author_id, view_count, like_count, audit_status |
Central repository for posts, including moderation flags and engagement counters. |
interaction_comments |
id, article_id, user_id, content_body, parent_id, timestamp |
Manages threaded discussions linked to specific articles. |
Configuration Layer
Server connectivity and framework behavior are externalized into YAML configuration files. Connection pooling, character encoding, and multipart upload limits are explicitly defined.
server:
port: 8080
servlet:
context-path: /api/v1
tomcat:
uri-encoding: UTF-8
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/animation_hub?useSSL=false&serverTimezone=UTC&characterEncoding=utf8
username: ${DB_USER}
password: ${DB_PASS}
jackson:
time-zone: UTC
servlet:
multipart:
max-file-size: 15MB
max-request-size: 15MB
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
global-config:
db-config:
id-type: ASSIGN_ID
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
Authentication & Session Management
The core authentication controller handles registration, credential verification, token issuance, and session termination. Logic prioritizes stateless validation and secure credential hashing.
@RestController
@RequestMapping("/api/auth")
public class AuthenticationController {
private final AccountService accountService;
private final TokenManager tokenManager;
private final RoleResolver roleResolver;
public AuthenticationController(AccountService accountService,
TokenManager tokenManager,
RoleResolver roleResolver) {
this.accountService = accountService;
this.tokenManager = tokenManager;
this.roleResolver = roleResolver;
}
@PostMapping("/register")
public ResponseEntity<ApiResponse> register(@RequestBody UserRegistrationRequest req) {
boolean exists = accountService.findByUsername(req.getUsername()).isPresent();
if (exists) {
return ResponseEntity.status(HttpStatus.CONFLICT).body(ApiResponse.error("Username already taken"));
}
String hashedPass = SecurityUtils.encodePassword(req.getPassword());
UserEntity newUser = UserEntity.builder()
.username(req.getUsername())
.passwordHash(hashedPass)
.status(STATUS_PENDING)
.build();
accountService.save(newUser);
return ResponseEntity.ok(ApiResponse.success("Registration successful"));
}
@PostMapping("/login")
public ResponseEntity<ApiResponse> authenticate(@RequestBody LoginPayload payload) {
Optional<UserEntity> optionalUser = accountService.findByCredentials(payload.getUsername(), payload.getEmail());
if (optionalUser.isEmpty()) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ApiResponse.error("Invalid credentials"));
}
UserEntity user = optionalUser.get();
if (!SecurityUtils.matchesPassword(payload.getPassword(), user.getPasswordHash())) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ApiResponse.error("Incorrect password"));
}
if (user.getStatus().equals(STATUS_BANNED)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ApiResponse.error("Account suspended"));
}
UserRole role = roleResolver.resolveByUserId(user.getId());
if (!role.isActive()) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ApiResponse.error("Role approval pending"));
}
String sessionToken = UUID.randomUUID().toString().replace("-", "");
tokenManager.persist(sessionToken, user.getId());
Map<String, Object> responsePayload = new HashMap<>();
responsePayload.put("userId", user.getId());
responsePayload.put("username", user.getUsername());
responsePayload.put("sessionToken", sessionToken);
responsePayload.put("role", role.getName());
return ResponseEntity.ok(ApiResponse.success(responsePayload));
}
@GetMapping("/verify")
public ResponseEntity<ApiResponse> verifySession(HttpServletRequest request) {
String token = extractTokenFromHeader(request);
Integer userId = tokenManager.validate(token);
if (userId == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ApiResponse.error("Expired or invalid session"));
}
UserEntity user = accountService.findById(userId);
return ResponseEntity.ok(ApiResponse.success(buildPublicProfile(user)));
}
@PostMapping("/logout")
public ResponseEntity<ApiResponse> terminateSession(HttpServletRequest request) {
String token = extractTokenFromHeader(request);
tokenManager.invalidate(token);
return ResponseEntity.ok(ApiResponse.success("Session terminated"));
}
private String extractTokenFromHeader(HttpServletRequest req) {
String authHeader = req.getHeader("Authorization");
return (authHeader != null && authHeader.startsWith("Bearer "))
? authHeader.substring(7) : null;
}
}
Cryptographic Helper Utilities
Secure string transformation relies on standardized digest algorithms. The following utility abstracts hexadecimal conversion and stream-based hashing operations.
public final class CryptoHashHelper {
private static final char[] HEX_CHARS = "0123456789abcdef".toCharArray();
private CryptoHashHelper() {}
public static String generateDigest(String input) {
if (input == null || input.isBlank()) {
throw new IllegalArgumentException("Input cannot be null or empty");
}
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hashBytes = digest.digest(input.getBytes(StandardCharsets.UTF_8));
return bytesToHex(hashBytes);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Missing cryptographic provider", e);
}
}
public static String hashFile(String filePath) {
Path path = Paths.get(filePath);
if (!Files.exists(path)) return "";
try (InputStream fis = Files.newInputStream(path)) {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
digest.update(buffer, 0, bytesRead);
}
return bytesToHex(digest.digest());
} catch (IOException | NoSuchAlgorithmException e) {
throw new RuntimeException("Failed to compute file hash", e);
}
}
private static String bytesToHex(byte[] bytes) {
StringBuilder hexBuilder = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
hexBuilder.append(HEX_CHARS[(b >> 4) & 0x0F]);
hexBuilder.append(HEX_CHARS[b & 0x0F]);
}
return hexBuilder.toString();
}
}