Thiết kế và triển khai hệ thống đặt món ăn đơn giản với SpringBoot/SSM + Vue + uniapp

Tổng quan kiến trúc hệ thống

Hệ thống đặt món ăn được xây dựng theo mô hình ba tầng với các thành phần chính: tầng giao diện người dùng (frontend) sử dụng Vue.js và uniapp, tầng xử lý logic nghiệp vụ (backend) xây dựng trên nền tảng SpringBoot, và tầng truy xuất dữ liệu (persistence layer) sử dụng MyBatisPlus. Kiến trúc này đảm bảo tính tách biệt rõ ràng giữa các tầng, giúp hệ thống dễ dàng mở rộng và bảo trì trong quá trình phát triển.

Frontend được phát triển theo hướng Single Page Application với khả năng hiển thị đa nền tảng, cho phép người dùng truy cập hệ thống từ các thiết bị khác nhau như điện thoại thông minh, máy tính bảng và máy tính để bàn. Backend cung cấp các API RESTful để giao tiếp với frontend, đảm bảo tính bảo mật và hiệu suất trong việc xử lý các yêu cầu từ người dùng.

Công nghệ sử dụng

SpringBoot cho tầng Backend

SpringBoot là framework phổ biến trong phát triển ứng dụng Java Enterprise, cung cấp nhiều tính năng tự động hóa quá trình cấu hình. Server Tomcat được tích hợp sẵn trong SpringBoot, cho phép triển khai ứng dụng mà không cần cài đặt server riêng biệt. Tính năng auto-configuration của SpringBoot phân tích các dependency trong project và tự động cấu hình các bean cần thiết, giúp developers giảm đáng kể thời gian viết cấu hình thủ công.

Framework này tích hợp nhiều module mạnh mẽ như Spring Data để làm việc với cơ sở dữ liệu, Spring Security cho xác thực và phân quyền, cùng Spring Cloud cho kiến trúc microservices. Với cộng đồng hỗ trợ rộng lớn và tài liệu phong phú, SpringBoot giúp đội ngũ phát triển xây dựng ứng dụng web một cách nhanh chóng và hiệu quả.

Vue.js cho tầng Frontend

Vue.js sử dụng công nghệ Virtual DOM để tối ưu hóa hiệu suất rendering giao diện. Khi dữ liệu thay đổi, Vue.js so sánh Virtual DOM mới với Virtual DOM cũ và chỉ cập nhật những phần tử thực sự thay đổi trên DOM thực, giảm thiểu tài nguyên xử lý. Cơ chế reactive data binding cho phép giao diện tự động cập nhật khi dữ liệu底层 thay đổi mà không cần can thiệp thủ công.

Mô hình component-based trong Vue.js giúp tái sử dụng code và quản lý giao diện theo từng khối độc lập. Developers có thể xây dựng các component như button, form, table một lần và sử dụng nhiều lần trong ứng dụng, giúp code trở nên gọn gàng và dễ bảo trì hơn.

MyBatisPlus cho tầng Persistence

MyBatisPlus là phiên bản mở rộng của MyBatis, cung cấp nhiều tiện ích trong việc thao tác với cơ sở dữ liệu. Framework hỗ trợ đa dạng các loại database như MySQL, Oracle, SQL Server và PostgreSQL, cho phép developers linh hoạt trong việc lựa chọn hệ quản trị cơ sở dữ liệu phù hợp với yêu cầu dự án.

Với các annotation và API được thiết kế intuitive, MyBatisPlus cho phép thực hiện các thao tác CRUD mà không cần viết SQL thủ công. Tính năng code generator tự động tạo các file entity, mapper interface và XML mapping, giúp tiết kiệm đáng kể thời gian phát triển. Ngoài ra, MyBatisPlus còn hỗ trợ phân trang, truy vấn động, optimistic locking và các công cụ phân tích hiệu suất.

Thiết kế cơ sở dữ liệu

Cơ sở dữ liệu của hệ thống được thiết kế với các bảng chính bao gồm: bảng lưu trữ thông tin người dùng (users), bảng lưu trữ thông tin món ăn (dishes), bảng đơn đặt hàng (orders), bảng chi tiết đơn hàng (order_items), và bảng quản lý token xác thực (token). Mối quan hệ giữa các bảng được thiết kế theo chuẩn third normal form (3NF) để đảm bảo tính toàn vẹn dữ liệu và tránh dư thừa.

-- Bảng lưu trữ token xác thực người dùng
CREATE TABLE `auth_token` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `user_id` bigint(20) NOT NULL COMMENT 'ID người dùng',
  `username` varchar(100) NOT NULL COMMENT 'Tên đăng nhập',
  `table_name` varchar(100) DEFAULT NULL COMMENT 'Tên bảng dữ liệu',
  `role` varchar(100) DEFAULT NULL COMMENT 'Vai trò người dùng',
  `token_value` varchar(200) NOT NULL COMMENT 'Giá trị token',
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Thời điểm tạo',
  `expired_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT 'Thời điểm hết hạn'
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Bảng token xác thực';

Bảng auth_token đóng vai trò quan trọng trong việc quản lý phiên đăng nhập của người dùng. Mỗi khi người dùng đăng nhập thành công, hệ thống tạo một token duy nhất và lưu trữ thông tin liên quan bao gồm ID người dùng, vai trò và thời hạn sử dụng.

Triển khai backend

Xử lý đăng nhập và xác thực

Hệ thống sử dụng cơ chế token-based authentication để quản lý phiên làm việc của người dùng. Khi người dùng đăng nhập, backend xác thực thông tin credentials và tạo một token duy nhất trả về cho client. Token này được sử dụng trong các request tiếp theo để xác định danh tính người dùng.

@RestController
@RequestMapping("/api/auth")
public class AuthenticationController {

    @Autowired
    private UserService userService;
    
    @Autowired
    private TokenService tokenService;

    @PostMapping("/signin")
    public ResponseEntity<?> authenticateUser(
            @RequestParam String username, 
            @RequestParam String password) {
        
        UserEntity user = userService.findByUsername(username);
        
        if (user == null || !passwordEncoder.matches(password, user.getPassword())) {
            return ResponseEntity.badRequest()
                    .body(createErrorResponse("Tên đăng nhập hoặc mật khẩu không đúng"));
        }
        
        String token = tokenService.createSessionToken(
            user.getId(), 
            username, 
            "users", 
            user.getRole()
        );
        
        return ResponseEntity.ok(createSuccessResponse("token", token));
    }
}

Controller xử lý yêu cầu đăng nhập nhận username và password từ request parameters, sau đó xác thực thông tin qua UserService. Nếu thông tin hợp lệ, hệ thống tạo token phiên làm việc và trả về cho client.

@Service
public class TokenServiceImpl implements TokenService {

    @Override
    public String createSessionToken(
            Long userId, 
            String username, 
            String tableName, 
            String role) {
        
        TokenEntity existingToken = this.findByUserIdAndRole(userId, role);
        String newToken = generateRandomString(32);
        
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(new Date());
        calendar.add(Calendar.HOUR_OF_DAY, 1);
        
        if (existingToken != null) {
            existingToken.setTokenValue(newToken);
            existingToken.setExpiredAt(calendar.getTime());
            this.updateById(existingToken);
        } else {
            TokenEntity newTokenEntity = new TokenEntity();
            newTokenEntity.setUserId(userId);
            newTokenEntity.setUsername(username);
            newTokenEntity.setTableName(tableName);
            newTokenEntity.setRole(role);
            newTokenEntity.setTokenValue(newToken);
            newTokenEntity.setExpiredAt(calendar.getTime());
            this.save(newTokenEntity);
        }
        
        return newToken;
    }
    
    private String generateRandomString(int length) {
        // Sinh chuỗi ngẫu nhiên với độ dài xác định
        return UUID.randomUUID().toString().replace("-", "").substring(0, length);
    }
}

TokenService quản lý vòng đời của các token xác thực. Mỗi token có thời hạn sử dụng 1 giờ, sau thời gian này token sẽ hết hạn và người dùng cần đăng nhập lại. Nếu người dùng đã có token hợp lệ, hệ thống sẽ cập nhật token mới thay vì tạo mới.

Interceptor xác thực request

AuthorizationInterceptor được sử dụng để kiểm tra tính hợp lệ của token trong mỗi request trước khi chuyển đến controller. Interceptor này hoạt động như một gatekeeper, chặn các request không hợp lệ và cho phép các request hợp lệ đi qua.

@Component
public class AuthorizationInterceptor implements HandlerInterceptor {

    public static final String TOKEN_HEADER = "Authorization";
    
    @Autowired
    private TokenService tokenService;
    
    @Override
    public boolean preHandle(
            HttpServletRequest request, 
            HttpServletResponse response, 
            Object handler) throws Exception {
        
        // Cấu hình CORS cho cross-origin requests
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Headers", 
            "Origin, Content-Type, Accept, Authorization");
        
        // Xử lý preflight request từ trình duyệt
        if (HttpMethod.OPTIONS.matches(request.getMethod())) {
            response.setStatus(HttpStatus.OK.value());
            return false;
        }
        
        // Bỏ qua xác thực cho các endpoint không yêu cầu đăng nhập
        if (handler instanceof HandlerMethod) {
            HandlerMethod method = (HandlerMethod) handler;
            SkipAuthentication skipAuth = method.getMethodAnnotation(SkipAuthentication.class);
            if (skipAuth != null) {
                return true;
            }
        } else {
            return true;
        }
        
        // Lấy token từ header
        String token = request.getHeader(TOKEN_HEADER);
        
        if (StringUtils.isBlank(token)) {
            sendUnauthorizedResponse(response, "Vui lòng đăng nhập để tiếp tục");
            return false;
        }
        
        TokenEntity tokenEntity = tokenService.validateToken(token);
        
        if (tokenEntity != null) {
            // Lưu thông tin người dùng vào session
            request.getSession().setAttribute("userId", tokenEntity.getUserId());
            request.getSession().setAttribute("role", tokenEntity.getRole());
            request.getSession().setAttribute("tableName", tokenEntity.getTableName());
            request.getSession().setAttribute("username", tokenEntity.getUsername());
            return true;
        }
        
        sendUnauthorizedResponse(response, "Phiên đăng nhập đã hết hạn");
        return false;
    }
    
    private void sendUnauthorizedResponse(HttpServletResponse response, String message) 
            throws IOException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        PrintWriter writer = response.getWriter();
        writer.print(JSONObject.toJSONString(
            ResponseEntity.status(401).body(createErrorResponse(message))
        ));
    }
}

Interceptor xử lý các yêu cầu từ phía trước, kiểm tra token và thiết lập thông tin người dùng vào session. Nếu token không hợp lệ hoặc hết hạn, hệ thống trả về mã lỗi 401 và thông báo yêu cầu đăng nhập lại.

Kiểm thử hệ thống

Quá trình kiểm thử đóng vai trò quan trọng trong việc đảm bảo chất lượng sản phẩm phần mềm. Thông qua kiểm thử, đội ngũ phát triển có thể phát hiện sớm các lỗi tiềm ẩn, đảm bảo hệ thống hoạt động đúng theo yêu cầu và mang lại trải nghiệm tốt nhất cho người dùng cuối.

Mục tiêu kiểm thử

Kiểm thử hệ thống nhằm mục đích xác minh rằng hệ thống đáp ứng đầy đủ các yêu cầu được đặt ra trong specification. Quá trình này giúp phát hiện các khiếm khuyết về chức năng, logic xử lý và giao diện người dùng. Việc kiểm thử kỹ lưỡng trước khi đưa sản phẩm vào sử dụng thực tế giúp giảm thiểu rủi ro và chi phí sửa lỗi sau này.

Từ góc độ người dùng, hệ thống cần đảm bảo các chức năng hoạt động đúng như mong đợi, giao diện trực quan và dễ sử dụng, thời gian phản hồi nhanh chóng. Từ góc độ kỹ thuật, hệ thống cần đảm bảo tính bảo mật, hiệu suất ổn định và khả năng xử lý đồng thời nhiều request.

Kiểm thử chức năng đăng nhập

Chức năng đăng nhập là cửa ngõ đầu tiên của hệ thống, cần được kiểm thử kỹ lưỡng để đảm bảo tính bảo mật. Các trường hợp kiểm thử bao gồm: đăng nhập thành công với thông tin hợp lệ, đăng nhập thất bại với mật khẩu sai, đăng nhập thất bại với tên đăng nhập không tồn tại, và xác thực các trường bắt buộc.

Dữ liệu nhậpKết quả mong đợiKết quả thực tếĐánh giá
Username: admin, Password: matkhau123Đăng nhập thành côngĐăng nhập thành côngĐạt
Username: admin, Password: sai123Hiển thị lỗi mật khẩuHiển thị lỗi mật khẩuĐạt
Username: admin, Captcha: saiHiển thị lỗi captchaHiển thị lỗi captchaĐạt
Username: rỗng, Password: matkhau123Thông báo nhập usernameThông báo nhập usernameĐạt
Username: admin, Password: rỗngThông báo nhập mật khẩuThông báo nhập mật khẩuĐạt

Kiểm thử chức năng quản lý người dùng

Module quản lý người dùng cho phép thực hiện các thao tác thêm, sửa, xóa và tìm kiếm thông tin người dùng. Kiểm thử tập trung vào các trường hợp: validation dữ liệu đầu vào, kiểm tra trùng lặp thông tin, xác nhận trước khi xóa và cập nhật giao diện sau thao tác.

Dữ liệu nhậpKết quả mong đợiKết quả thực tếĐánh giá
Thêm user mới hợp lệThêm thành công, hiển thị trong danh sáchThêm thành công, hiển thị trong danh sáchĐạt
Cập nhật thông tin userCập nhật thành côngCập nhật thành côngĐạt
Xóa user đã chọnXác nhận và xóa khỏi hệ thốngXác nhận và xóa khỏi hệ thốngĐạt
Thêm user không nhập usernameThông báo lỗi validationThông báo lỗi validationĐạt
Thêm user trùng usernameThông báo username đã tồn tạiThông báo username đã tồn tạiĐạt

Kết luận kiểm thử

Sau quá trình kiểm thử toàn diện, hệ thống đã đáp ứng các yêu cầu về chức năng và hiệu suất. Tất cả các module chính bao gồm đăng nhập, quản lý người dùng, quản lý món ăn và đặt hàng đều hoạt động đúng như thiết kế. Các lỗi phát hiện trong quá trình kiểm thử đã được khắc phục, hệ thống sẵn sàng cho việc triển khai thực tế.

Việc kiểm thử không chỉ giúp phát hiện lỗi mà còn cung cấp cái nhìn tổng thể về chất lượng sản phẩm. Hệ thống hoàn thành đảm bảo tính ổn định, dễ sử dụng và có khả năng mở rộng trong tương lai khi nhu cầu người dùng tăng cao.

Thẻ: SpringBoot Vue.js mybatisplus java-web UniApp

Đăng vào ngày 8 tháng 9 lúc 10:57