Xây Dựng Hệ Thống Đặt Món Ăn Căn Trường Với Spring Boot, Vue.js Và Uniapp

Tổng Quan Hệ Thống

Hệ thống đặt món ăn căn trường được phát triển nhằm tối ưu hóa quy trình order thực phẩm cho sinh viên và giảng viên. Giải pháp này kết hợp giữa backend mạnh mẽ, frontend linh hoạt và ứng dụng đa nền tảng để đảm bảo trải nghiệm người dùng mượt mà.

Kiến Trúc Công Nghệ

Backend: Spring Boot

Spring Boot đóng vai trò là nền tảng cốt lõi cho phía máy chủ. Framework này loại bỏ các cấu hình phức tạp của Spring truyền thống thông qua cơ chế tự động cấu hình (auto-configuration). Nó tích hợp sẵn các container như Tomcat, giúp triển khai ứng dụng độc lập mà không cần môi trường máy chủ bên ngoài.

Ưu điểm chính bao gồm khả năng tích hợp dễ dàng với các module như Spring Security cho xác thực, Spring Data cho truy xuất dữ liệu và cộng đồng hỗ trợ lớn. Dưới đây là ví dụ về cách khởi tạo một dịch vụ cơ bản:

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@EnableAutoConfiguration
@ComponentScan
@RestController
public class DeliveryPlatform {

    public static void main(String[] args) {
        org.springframework.boot.SpringApplication.run(DeliveryPlatform.class, args);
    }

    @RequestMapping("/api/status")
    public String checkSystemStatus() {
        return "System Operational";
    }
}

Đoạn mã trên định nghĩa lớp chính `DeliveryPlatform` với các注解 cần thiết để kích hoạt ứng dụng. Phương thức `checkSystemStatus` phản hồi trạng thái hệ thống khi gọi đến đường dẫn `/api/status`.

Frontend: Vue.js

Vue.js được sử dụng để xây dựng giao diện người dùng nhờ vào cơ chế reactive data binding và Virtual DOM. Khi trạng thái dữ liệu thay đổi, giao diện sẽ tự động cập nhật mà không cần thao tác DOM thủ công, giúp tăng hiệu suất render.

Ví dụ minh họa cách Vue xử lý dữ liệu động:

<html>
<head>
  <title>Campus Order Interface</title>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
  <div id="campus-order">
    <h3>Trạng Thái Đơn: {{ orderStatus }}</h3>
    <button @click="updateStatus">Cập Nhật Trạng Thái</button>
  </div>

  <script>
    var orderApp = new Vue({
      el: '#campus-order',
      data: {
        orderStatus: 'Đang chờ xử lý'
      },
      methods: {
        updateStatus: function() {
          this.orderStatus = 'Đã xác nhận';
        }
      }
    });
  </script>
</body>
</html>

Trong ví dụ này, biến `orderStatus` được liên kết với giao diện. Khi hàm `updateStatus` được kích hoạt qua sự kiện click, giá trị hiển thị sẽ thay đổi ngay lập tức.

Persistence: MyBatis

MyBatis hoạt động như lớp trung gian giữa ứng dụng Java và cơ sở dữ liệu. Nó cho phép ánh xạ các đối tượng Java tới các câu lệnh SQL thông qua file XML hoặc注解, giúp tách biệt logic business và truy vấn dữ liệu.

Các lợi ích chính bao gồm hỗ trợ SQL động, cơ chế caching để giảm tải cho database và khả năng tùy biến cao thông qua plugin.

Triển Khai Chức Năng Xác Thực

Hệ thống sử dụng cơ chế Token để quản lý phiên làm việc và phân quyền truy cập. Dưới đây là cách triển khai logic đăng nhập và bộ lọc bảo mật.

// Annotation cho phép truy cập công khai
@PublicAccess
@PostMapping(value = "/authenticate")
public Response authenticate(String account, String passcode, String verifyCode, HttpServletRequest req) {
   // Truy vấn thông tin tài khoản
   AccountInfo acc = accountRepo.findByAccountName(account);
   // Xác thực thông tin
   if(acc == null || !acc.getPasscode().equals(passcode)) {
      return Response.fail("Thông tin đăng nhập không hợp lệ");
   }
   // Tạo chuỗi xác thực
   String accessToken = credentialMgr.createCredential(acc.getId(), account, "students", acc.getRole());
   return Response.success().add("accessToken", accessToken);
}

// Phương thức tạo credential
@Override
public String createCredential(Long uid, String uname, String tableName, String role) {
   // Kiểm tra credential cũ
   AuthCredential oldCredential = this.findByUserId(uid, role);
   // Tạo chuỗi ngẫu nhiên
   String newToken = Utility.generateRandomString(32);
   // Thiết lập thời gian hết hạn
   Calendar cal = Calendar.getInstance();   
   cal.setTime(new Date());   
   cal.add(Calendar.HOUR_OF_DAY, 1);
   if(oldCredential != null) {
      // Cập nhật thông tin
      oldCredential.setTokenValue(newToken);
      oldCredential.setExpireTime(cal.getTime());
      this.updateRecord(oldCredential);
   } else {
      // Ghi mới
      this.insertRecord(new AuthCredential(uid, uname, tableName, role, newToken, cal.getTime()));
   }
   return newToken;
}

/**
 * Bộ lọc kiểm soát truy cập
 */
@Component
public class RequestSecurityFilter implements HandlerInterceptor {

    public static final String HEADER_TOKEN = "Authorization";

    @Autowired
    private CredentialManager credentialMgr;
    
    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) throws Exception {
        // Cấu hình CORS
        res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
        res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Origin");
        res.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));

        // Xử lý preflight request
        if (req.getMethod().equals(RequestMethod.OPTIONS.name())) {
            res.setStatus(HttpStatus.OK.value());
            return false;
        }
        
        // Kiểm tra annotation
        PublicAccess access;
        if (handler instanceof HandlerMethod) {
            access = ((HandlerMethod) handler).getMethodAnnotation(PublicAccess.class);
        } else {
            return true;
        }

        // Lấy token từ header
        String token = req.getHeader(HEADER_TOKEN);
        
        /**
         * Bỏ qua kiểm tra nếu là public
         */
        if(access != null) {
            return true;
        }
        
        // Xác thực token
        AuthCredential credential = null;
        if(StringUtils.isNotBlank(token)) {
            credential = credentialMgr.getCredentialByToken(token);
        }
        
        if(credential != null) {
            // Lưu thông tin vào session
            req.getSession().setAttribute("uid", credential.getUserId());
            req.getSession().setAttribute("role", credential.getRole());
            req.getSession().setAttribute("table", credential.getTableName());
            req.getSession().setAttribute("uname", credential.getUserName());
            return true;
        }
        
        // Trả về lỗi 401 nếu không hợp lệ
        PrintWriter out = null;
        res.setCharacterEncoding("UTF-8");
        res.setContentType("application/json; charset=utf-8");
        try {
            out = res.getWriter();
            out.print(JSONObject.toJSONString(Response.fail(401, "Yêu cầu xác thực")));
        } finally {
            if(out != null){
                out.close();
            }
        }
        return false;
    }
}

Đoạn mã trên mô tả quy trình xác thực người dùng. Annotation `@PublicAccess` đánh dấu các endpoint không cần kiểm tra quyền. Bộ lọc `RequestSecurityFilter` sẽ chặn các yêu cầu thiếu token hợp lệ và trả về mã lỗi 401.

Thiết Kế Cơ Sở Dữ Liệu

Dưới đây là cấu trúc bảng lưu trữ thông tin món ăn trong hệ thống.

-- ----------------------------
-- Table structure for food_menu
-- ----------------------------
DROP TABLE IF EXISTS `food_menu`;
CREATE TABLE `food_menu` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'Mã định danh',
  `dish_name` varchar(100) NOT NULL COMMENT 'Tên món',
  `unit_price` decimal(10, 2) NOT NULL COMMENT 'Giá bán',
  `detail_info` varchar(200) DEFAULT NULL COMMENT 'Mô tả chi tiết',
  `quantity` int(11) NOT NULL COMMENT 'Số lượng tồn',
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Thời gian tạo',
  `modified_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Thời gian sửa',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='Bảng thực đơn';

Các trường dữ liệu bao gồm mã định danh duy nhất, tên món, giá tiền, mô tả, số lượng库存 và các mốc thời gian ghi nhận thay đổi.

Dữ liệu mẫu được插入 vào bảng như sau:

INSERT INTO `food_menu` (`dish_name`, `unit_price`, `detail_info`, `quantity`)
VALUES ('Cơm Gà Rang', 45000.00, 'Cơm trắng ăn kèm gà rang gừng', 200);

INSERT INTO `food_menu` (`dish_name`, `unit_price`, `detail_info`, `quantity`)
VALUES ('Bún Bò Huế', 50000.00, 'Bún bò đặc biệt nhiều thịt', 150);

INSERT INTO `food_menu` (`dish_name`, `unit_price`, `detail_info`, `quantity`)
VALUES ('Trà Sữa Trân Châu', 25000.00, 'Trà sữa đường đen hạt trân châu', 300);

Quy Trình Kiểm Thử Hệ Thống

Mục Đích Kiểm Thử

Kiểm thử hệ thống là bước cuối cùng trong vòng đời phát triển để đảm bảo tính ổn định trước khi bàn giao. Mục tiêu chính là phát hiện các lỗi tiềm ẩn về logic nghiệp vụ và trải nghiệm người dùng.

Quá trình này tập trung vào việc xác minh xem hệ thống có đáp ứng đúng các yêu cầu kỹ thuật đã đề ra hay không. Các kịch bản kiểm thử được xây dựng dựa trên hành vi thực tế của người dùng cuối.

Kiểm Thử Chức Năng

Phương pháp kiểm thử hộp đen được áp dụng để đánh giá các module chức năng. Dưới đây là ví dụ về kịch bản kiểm thử đăng nhập:

Dữ Liệu Đầu Vào Kết Quả Mong Đợi Kết Quả Thực Tế Đánh Giá
Tài khoản: admin, Mật khẩu: 123456, Mã xác thực: Đúng Đăng nhập thành công Chuyển hướng trang chủ Đạt
Tài khoản: admin, Mật khẩu: sai, Mã xác thực: Đúng Thông báo lỗi mật khẩu Hiện thông báo lỗi Đạt
Tài khoản: admin, Mật khẩu: 123456, Mã xác thực: Sai Thông báo lỗi mã xác thực Hiện thông báo lỗi Đạt
Tài khoản: trống, Mật khẩu: 123456, Mã xác thực: Đúng Yêu cầu nhập tài khoản Hiện cảnh báo Đạt

Các chức năng quản lý người dùng như thêm, sửa, xóa cũng được kiểm tra tương tự以确保 tính toàn vẹn dữ liệu. Ví dụ, khi thêm một người dùng trùng tên hệ thống, phải hiển thị thông báo lỗi phù hợp thay vì ghi đè dữ liệu cũ.

Thẻ: spring-boot Vue.js mybatis UniApp java-backend

Đăng vào ngày 23 tháng 9 lúc 20:19