Architecture Overview
The system employs a modern full-stack architecture with clear separation of concerns. The backend is built on Spring Boot, providing a robust RESTful API foundation. The frontend utilizes Vue.js for a responsive user interface, while UniApp enables cross-platform mobile functionality.
Backend Implementation
Authentication Service
@RestController
@RequestMapping("/api/auth")
public class AuthenticationController {
@Autowired
private UserService userService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@PostMapping("/authenticate")
public ResponseEntity<JwtResponse> authenticateUser(@RequestBody LoginRequest loginRequest) {
UserDetails userDetails = userService.authenticateUser(
loginRequest.getUsername(),
loginRequest.getPassword()
);
String token = jwtTokenUtil.generateToken(userDetails);
return ResponseEntity.ok(new JwtResponse(token));
}
}
Activity Management
@Service
public class ActivityService {
@Autowired
private ActivityRepository activityRepository;
public Page<Activity> getAvailableActivities(Pageable pageable) {
return activityRepository.findByStatus(
ActivityStatus.ACTIVE,
pageable
);
}
@Transactional
public EnrollmentResult enrollChild(EnrollmentRequest request) {
Activity activity = activityRepository.findById(request.getActivityId())
.orElseThrow(() -> new ActivityNotFoundException());
if (activity.getAvailableSlots() <= 0) {
return EnrollmentResult.noSlotsAvailable();
}
activity.decrementSlots();
activityRepository.save(activity);
return EnrollmentResult.success();
}
}
Frontend Components
Activity List Component
<template>
<div class="activity-container">
<div v-for="activity in activities"
:key="activity.id"
class="activity-card">
<h3>{{ activity.name }}</h3>
<p>{{ activity.description }}</p>
<div class="activity-meta">
<span>Age: {{ activity.ageGroup }}</span>
<span>Slots: {{ activity.availableSlots }}</span>
</div>
<button @click="enroll(activity.id)"
:disabled="activity.availableSlots === 0">
Enroll Now
</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
activities: []
}
},
created() {
this.fetchActivities();
},
methods: {
async fetchActivities() {
const response = await this.$http.get('/api/activities');
this.activities = response.data.content;
},
async enroll(activityId) {
try {
await this.$http.post(`/api/enrollments`, {
activityId: activityId,
childId: this.selectedChild
});
this.$notify.success('Enrollment successful!');
this.fetchActivities();
} catch (error) {
this.$notify.error('Enrollment failed');
}
}
}
}
</script>
Database Schema
Activity Table
CREATE TABLE activities (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
description TEXT,
age_group VARCHAR(20) NOT NULL,
max_capacity INT NOT NULL,
available_slots INT NOT NULL,
schedule VARCHAR(50),
fee DECIMAL(10,2),
status ENUM('ACTIVE', 'FULL', 'CANCELLED') DEFAULT 'ACTIVE',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE enrollments (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
activity_id BIGINT NOT NULL,
child_id BIGINT NOT NULL,
parent_id BIGINT NOT NULL,
enrollment_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status ENUM('CONFIRMED', 'WAITLIST', 'CANCELLED') DEFAULT 'CONFIRMED',
FOREIGN KEY (activity_id) REFERENCES activities(id),
FOREIGN KEY (child_id) REFERENCES children(id),
FOREIGN KEY (parent_id) REFERENCES parents(id)
);
Testing Strategies
API Testing
@SpringBootTest
@AutoConfigureTestDatabase
class EnrollmentControllerTest {
@Autowired
private TestRestTemplate restTemplate;
@MockBean
private ActivityService activityService;
@Test
void shouldEnrollChildSuccessfully() {
when(activityService.enrollChild(any(EnrollmentRequest.class)))
.thenReturn(EnrollmentResult.success());
ResponseEntity<String> response = restTemplate.postForEntity(
"/api/enrollments",
new EnrollmentRequest(1L, 1L),
String.class
);
assertEquals(HttpStatus.OK, response.getStatusCode());
}
@Test
void shouldRejectEnrollmentWhenFull() {
when(activityService.enrollChild(any(EnrollmentRequest.class)))
.thenReturn(EnrollmentResult.noSlotsAvailable());
ResponseEntity<String> response = restTemplate.postForEntity(
"/api/enrollments",
new EnrollmentRequest(1L, 1L),
String.class
);
assertEquals(HttpStatus.CONFLICT, response.getStatusCode());
}
}
Frontend Testing
import { mount } from '@vue/test-utils'
import ActivityList from '@/components/ActivityList.vue'
describe('ActivityList', () => {
it('displays available activities', async () => {
const mockActivities = [
{ id: 1, name: 'Art Class', availableSlots: 5 },
{ id: 2, name: 'Music Class', availableSlots: 0 }
]
const wrapper = mount(ActivityList, {
data() {
return { activities: mockActivities }
}
})
expect(wrapper.findAll('.activity-card')).toHaveLength(2)
expect(wrapper.text()).toContain('Art Class')
expect(wrapper.text()).toContain('Music Class')
})
it('disables enrollment button when full', async () => {
const wrapper = mount(ActivityList, {
data() {
return {
activities: [{ id: 1, name: 'Full Class', availableSlots: 0 }]
}
}
})
const button = wrapper.find('button')
expect(button.attributes('disabled')).toBeDefined()
})
})