Hệ thống quản lý phòng riêng tích hợp Spring Boot, Vue.js và UniApp

Đây là một hệ thống quản lý phòng riêng (karaoke, spa, hội nghị…) được xây dựng theo kiến trúc phân tầng rõ ràng: backend dựa trên Spring Boot với MyBatis làm lớp truy vấn dữ liệu; frontend sử dụng Vue.js cho giao diện web; đồng thời hỗ trợ triển khai đa nền tảng (web, iOS, Android) thông qua framework UniApp.

Backend: Spring Boot với cấu hình tối ưu

Spring Boot được lựa chọn nhờ khả năng khởi tạo nhanh, tự động cấu hình và tích hợp sẵn máy chủ nhúng. Hệ thống không yêu cầu cài đặt Tomcat thủ công — chỉ cần chạy class chính là ứng dụng đã sẵn sàng lắng nghe tại cổng mặc định.

Dưới đây là phiên bản cải tiến của lớp khởi động, áp dụng nguyên tắc tách biệt trách nhiệm và hỗ trợ mở rộng:

@SpringBootApplication
@EnableTransactionManagement
public class RoomManagementApplication {

    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(RoomManagementApplication.class);
        app.setBannerMode(Banner.Mode.OFF);
        app.run(args);
    }
}

@RestController
@RequestMapping("/api/v1/rooms")
public class RoomController {

    private final RoomService roomService;

    public RoomController(RoomService roomService) {
        this.roomService = roomService;
    }

    @GetMapping
    public ResponseEntity<List<RoomDto>> getAllRooms(@RequestParam(defaultValue = "0") int page,
                                                             @RequestParam(defaultValue = "10") int size) {
        return ResponseEntity.ok(roomService.fetchPaginatedRooms(page, size));
    }

    @PostMapping
    public ResponseEntity<RoomDto> createRoom(@Valid @RequestBody RoomCreationRequest request) {
        return ResponseEntity.status(HttpStatus.CREATED)
                .body(roomService.registerNewRoom(request));
    }
}

Frontend: Vue 3 + Composition API & UniApp

Giao diện người dùng được phát triển bằng Vue 3 với Composition API nhằm tăng tính tái sử dụng và dễ bảo trì. Toàn bộ logic xử lý trạng thái phòng, đặt lịch và phân quyền được đóng gói trong các composable riêng biệt như useRoomBooking() hoặc useAuthStore().

Mã ví dụ minh họa việc quản lý trạng thái phòng trong component Vue:

<template>
  <div class="room-list">
    <h3>Danh sách phòng hiện hoạt</h3>
    <div v-for="room in activeRooms" :key="room.id" class="room-card">
      <h4>{{ room.name }}</h4>
      <p>Trạng thái: <span :class="statusClass(room.status)">{{ room.statusLabel }}</span></p>
      <button @click="bookRoom(room.id)" :disabled="!canBook(room)">
        {{ room.isBooked ? 'Đã đặt' : 'Đặt ngay' }}
      </button>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue';
import { useRoomStore } from '@/stores/room';

const roomStore = useRoomStore();
const activeRooms = ref([]);

roomStore.loadActiveRooms().then(rooms => {
  activeRooms.value = rooms.map(r => ({
    ...r,
    statusLabel: r.status === 'AVAILABLE' ? 'Có sẵn' : 'Đang sử dụng',
    isBooked: r.bookingId != null
  }));
});

const statusClass = (status) => ({
  'status-available': status === 'AVAILABLE',
  'status-occupied': status !== 'AVAILABLE'
});

const canBook = (room) => room.status === 'AVAILABLE' && !room.isBooked;

const bookRoom = async (id) => {
  try {
    await roomStore.reserve(id);
    alert('Đặt phòng thành công!');
  } catch (err) {
    alert('Không thể đặt phòng: ' + err.message);
  }
};
</script>

Lớp truy vấn dữ liệu: MyBatis Plus với chiến lược tối ưu

Hệ thống sử dụng MyBatis Plus thay vì MyBatis thuần để giảm thiểu boilerplate code. Các thực thể được đánh dấu bằng @TableName, còn các phương thức truy vấn cơ bản (SELECT/INSERT/UPDATE/DELETE) được kế thừa từ BaseMapper.

Ví dụ cấu trúc bảng room:

CREATE TABLE `room` (
  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `code` VARCHAR(20) NOT NULL UNIQUE COMMENT 'Mã phòng duy nhất',
  `name` VARCHAR(100) NOT NULL COMMENT 'Tên phòng',
  `capacity` TINYINT NOT NULL DEFAULT 10 COMMENT 'Sức chứa tối đa',
  `status` ENUM('AVAILABLE', 'OCCUPIED', 'MAINTENANCE') NOT NULL DEFAULT 'AVAILABLE',
  `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
  `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  INDEX idx_status (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Kiểm thử chức năng theo phương pháp hộp đen

Các ca kiểm thử tập trung vào hành vi người dùng cuối — không phụ thuộc vào mã nguồn. Dưới đây là bảng kiểm thử chức năng "đặt phòng":

Đầu vào Kết quả mong đợi Kết quả thực tế Đánh giá
Chọn phòng A → nhấn "Đặt ngay" khi trạng thái = AVAILABLE Hiển thị thông báo thành công, trạng thái cập nhật thành OCCUPIED Thành công
Chọn phòng B → nhấn "Đặt ngay" khi trạng thái = OCCUPIED Thông báo "Phòng đang được sử dụng", không thay đổi trạng thái Thành công
Nhập mã phòng không tồn tại → gửi yêu cầu đặt Trả về mã lỗi 404 và thông báo "Phòng không tồn tại" Thành công

Bảo mật và xác thực dựa trên token

Hệ thống áp dụng cơ chế xác thực JWT nhẹ, kết hợp với bộ lọc tùy chỉnh để kiểm tra tính hợp lệ của token trước mỗi yêu cầu nhạy cảm:

@Component
public class JwtAuthorizationFilter implements Filter {

    private static final String AUTH_HEADER = "Authorization";
    private static final String BEARER_PREFIX = "Bearer ";

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        String authHeader = request.getHeader(AUTH_HEADER);
        if (authHeader == null || !authHeader.startsWith(BEARER_PREFIX)) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Token thiếu hoặc sai định dạng");
            return;
        }

        String token = authHeader.substring(BEARER_PREFIX.length());
        if (!JwtUtil.validateToken(token)) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Token hết hạn hoặc không hợp lệ");
            return;
        }

        // Trích xuất thông tin người dùng và thiết lập vào SecurityContext
        Authentication auth = JwtUtil.getAuthentication(token);
        SecurityContextHolder.getContext().setAuthentication(auth);
        chain.doFilter(req, res);
    }
}

Thẻ: spring-boot vuejs UniApp MyBatis-Plus jwt-authentication

Đăng vào ngày 3 tháng 7 lúc 22:35