Tài liệu này cung cấp hướng dẫn chi tiết về backend cho hệ thống mô phỏng nhà hàng, được thiết kế cho người mới bắt đầu. Chúng tôi sẽ khám phá các công nghệ, cấu trúc mã, cơ chế hoạt động và các vấn đề gặp phải trong quá trình phát triển.
1. Tổng quan về Hệ thống Backend
Backend đóng vai trò là bộ não điều khiển của hệ thống, xử lý các tác vụ sau:
- Tiếp nhận yêu cầu từ Frontend: Khởi chạy, dừng mô phỏng, yêu cầu phân tích AI.
- Thực thi logic mô phỏng: Sử dụng mô hình phân phối chuẩn để mô phỏng hành vi của khách hàng (đến, xếp hàng, gọi món, ngồi bàn).
- Lưu trữ dữ liệu: Ghi lại các trạng thái mô phỏng theo từng giây vào cơ sở dữ liệu SQLite.
- Đẩy dữ liệu thời gian thực: Sử dụng Socket.IO để gửi dữ liệu mô phỏng đến Frontend.
- Tạo báo cáo đánh giá: Tính toán điểm trải nghiệm cho quầy phục vụ và bàn ăn sau khi mô phỏng kết thúc.
- Xử lý yêu cầu AI: Chuyển tiếp yêu cầu đến các API AI bên ngoài.
Sau khi khởi chạy backend, bạn có thể truy cập giao diện người dùng tại http://127.0.0.1:5001.
2. Công nghệ Sử dụng cho Backend
| Công nghệ | Phiên bản | Vai trò |
|---|---|---|
| Python 3 | 3.x | Ngôn ngữ lập trình chính |
| Flask | 2.3.3 | Framework web, xử lý routing và request |
| Flask-SocketIO | 5.3.6 | Hỗ trợ WebSocket/Socket.IO cho Flask |
| Flask-CORS | 4.0.0 | Xử lý các yêu cầu cross-origin |
| SQLite | Tích hợp sẵn trong Python | Cơ sở dữ liệu (qua module sqlite3) |
| threading | Tích hợp sẵn trong Python | Quản lý đa luồng cho hoạt động mô phỏng đồng thời |
| urllib | Tích hợp sẵn trong Python | Thực hiện yêu cầu HTTP (chuyển tiếp cho AI) |
| python-dotenv | 1.0.0 | Tải biến môi trường từ tệp .env |
Lý do chọn Flask thay vì Django
Flask là một micro-framework, chỉ cung cấp các chức năng cốt lõi và cho phép mở rộng theo nhu cầu. So với Django:
- Flask nhẹ nhàng hơn, dễ học hơn, phù hợp cho các dự án vừa và nhỏ.
- Việc tích hợp Flask-SocketIO rất thuận tiện.
- Logic nghiệp vụ của chúng ta không quá phức tạp, không yêu cầu bộ đầy đủ ORM, template engine, middleware của Django.
3. Cấu trúc Mã Backend
Backend được tổ chức theo cấu trúc 3 lớp, với 4 tệp Python đảm nhận các vai trò riêng biệt:
app.py ← Lớp trên cùng: Điểm vào của Flask, định nghĩa routes, CORS, SocketIO, khởi chạy ứng dụng
↓ Gọi
simulation.py ← Lớp giữa: Engine mô phỏng, quản lý luồng, vòng lặp mô phỏng, đẩy dữ liệu, tính toán đánh giá
↓ Gọi
database.py ← Lớp dưới cùng: Thao tác với SQLite, CRUD, cấu hình đồng thời, dọn dẹp dữ liệu
config.py ← Lớp cấu hình: Chứa tất cả các hằng số (ngưỡng, tham số phân phối chuẩn, danh sách CORS, v.v.)
Chi tiết trách nhiệm từng tệp
app.py (Tệp nhập, ~300 dòng)
Quản lý khởi tạo Flask, cấu hình CORS và Socket.IO, định nghĩa các API endpoints và xử lý các sự kiện Socket.IO cơ bản.
- Khởi tạo ứng dụng Flask.
- Cấu hình chính sách CORS.
- Khởi tạo đối tượng SocketIO.
- Khởi tạo trình quản lý mô phỏng.
- Cấu hình xác thực (tùy chọn).
- HTTP Routes (3):
POST /api/start-simulationPOST /api/end-simulationPOST /api/ai-analyze
- SocketIO Events (1):
bind_session→join_room()
- Tĩnh file & Root route:
GET /→simulation.html/static/→ Thư mục tĩnh
simulation.py (Engine mô phỏng, ~250 dòng)
Chứa lớp RestaurantSimulation để quản lý vòng lặp mô phỏng, luồng, và tính toán kết quả.
- Thuộc tính:
simulation_threads: Lưu trữ các luồng mô phỏng theosession_id.simulation_stop_flags: Cờ báo hiệu dừng mô phỏng cho từngsession_id.simulation_ids: Ánh xạsession_idtới ID mô phỏng trong DB.
- Phương thức:
start_simulation(): Khởi chạy luồng mới.stop_simulation(): Dừng luồng._simulation_loop(): Vòng lặp chính của mô phỏng.calculate_evaluation(): Tính toán đánh giá.get_simulation_data(): Lấy dữ liệu mô phỏng.get_current_simulation(): Truy vấn các mô phỏng đang chạy.
database.py (Lớp cơ sở dữ liệu, ~330 dòng)
Cung cấp các phương thức để tương tác với cơ sở dữ liệu SQLite, bao gồm lưu trữ và truy xuất thông tin mô phỏng.
- Lớp
SimulationDatabase(singletondbtoàn cục). - Bảng:
simulation_info: Thông tin về các phiên mô phỏng.simulation_data: Dữ liệu snapshot theo từng giây.
- Phương thức:
add_simulation(): Thêm thông tin mô phỏng.get_current_simulation(): Lấy mô phỏng đang chạy.get_simulation_info_by_session(): Lấy thông tin theosession_id.end_simulation(): Cập nhật trạng thái kết thúc.add_simulation_data(): Thêm dữ liệu mô phỏng.get_simulation_data(): Truy vấn dữ liệu mô phỏng.cleanup_old_data(): Dọn dẹp dữ liệu cũ.
4. Cơ chế Hoạt động của Engine Mô phỏng
4.1 Vòng lặp Mô phỏng (Thực thi mỗi giây)
- Giải phóng bàn đã hết hạn: Duyệt qua tất cả bàn, giải phóng các bàn đã hết thời gian sử dụng.
- Tạo số lượng khách mới đến: Sử dụng phân phối chuẩn
N(μ, σ), giới hạn trong khoảng[0, max]. - Phân bổ vào hàng đợi tại quầy phục vụ: Mỗi khách mới chọn quầy dựa trên phân phối chuẩn (ưu tiên quầy giữa).
- Xử lý gọi món: Mỗi quầy phục vụ hoàn thành
1 / meal_timengười mỗi giây. Sử dụng bộ tích lũy số thực để tránh sai số. - Phân bổ bàn: Khách từ quầy phục vụ được phân bổ vào bàn trống, thời gian dùng bàn có biến động ngẫu nhiên.
- Ghi vào cơ sở dữ liệu: Thực hiện câu lệnh
INSERTvào bảngsimulation_data. - Đẩy dữ liệu tới Frontend: Gửi dữ liệu qua Socket.IO tới phòng của
session_idtương ứng. - Chờ 1 giây để bắt đầu vòng lặp tiếp theo.
4.2 Mô hình Luồng (Thread Model)
Mỗi phiên mô phỏng chạy trên một luồng daemon độc lập:
thread = threading.Thread(
target=self._simulation_loop,
args=(...),
daemon=True, # Luồng daemon: Tự động kết thúc khi tiến trình chính thoát
)
thread.start()
daemon=Trueđảm bảo các luồng mô phỏng tự động kết thúc khi Flask dừng, tránh treo tiến trình.- Cờ
stop_flagscho phép dừng luồng một cách "lịch sự": đặtstop_flags[session_id] = True, luồng sẽ kiểm tra và thoát vòng lặp. - Nếu luồng không phản hồi tín hiệu dừng trong 5 giây,
join(timeout=5)sẽ được gọi để đợi cưỡng bức.
4.3 Giới hạn Số lượng Mô phỏng Đồng thời
Hằng số MAX_CONCURRENT_SIMULATIONS = 5 giới hạn số lượng mô phỏng chạy cùng lúc. Trước khi khởi chạy mô phỏng mới, hệ thống kiểm tra số lượng luồng đang hoạt động và từ chối nếu vượt quá giới hạn, tránh làm quá tải máy chủ.
5. Chi tiết Mô hình Phân phối Chuẩn
Phân phối chuẩn (hay phân phối Gaussian) được sử dụng trong hai trường hợp chính:
5.1 Mô hình Khách hàng Đến
Số lượng khách đến mỗi phút được giới hạn bởi max_people.
- Tham số phân phối chuẩn:
μ(Trung bình):max_people_per_sec * 0.6σ(Độ lệch chuẩn):max_people_per_sec * 0.2- Giới hạn trên:
max_people_per_sec * 1.5
Lý do sử dụng phân phối chuẩn: Lưu lượng khách tại nhà hàng thường không đều đặn, có xu hướng tập trung quanh một giá trị trung bình và biến động nhẹ. Hình dạng "chuông" của phân phối chuẩn mô phỏng tốt đặc điểm này.
Xử lý giới hạn (Truncation): Giá trị sinh ra được giới hạn trong khoảng [0, upper_bound] bằng cách sử dụng max(0, min(random_value, upper_bound)) để loại bỏ các giá trị không hợp lý (âm hoặc quá lớn).
5.2 Mô hình Phân bổ Quầy Phục vụ
- Số thứ tự quầy: từ
0đếnwindow_num - 1. - Tham số phân phối chuẩn:
μ_w:(window_num - 1) * 0.5(quầy ở giữa có xác suất cao nhất).σ_w:window_num * 0.3(phạm vi phân phối).
Lý do: Trong thực tế, khách hàng thường có xu hướng xếp hàng ở các quầy ở giữa vì chúng dễ thấy và thuận tiện hơn. Phân phối chuẩn mô tả chính xác hiện tượng "nhiều ở giữa, ít ở hai bên".
5.3 Bộ tích lũy Số thực (Khắc phục Lỗi Quan trọng)
Lỗi ở mã cũ: Sử dụng round(1 / meal_time). Khi meal_time > 1 (ví dụ meal_time = 5), 1 / 5 = 0.2, round(0.2) = 0. Điều này dẫn đến việc không có khách hàng nào được phục vụ xong.
Giải pháp mới: Sử dụng bộ tích lũy số thực.
# Cộng dồn phần thập phân mỗi giây window_accumulator[i] += 1.0 / meal_time # Ví dụ: meal_time = 5 -> cộng dồn 0.2 # Lấy phần nguyên làm số lượng người hoàn thành processed = int(window_accumulator[i]) # 0.2->0, 0.4->0, 0.6->0, 0.8->0, 1.0->1 ✓ # Trừ đi phần đã xử lý, giữ lại phần thập phân window_accumulator[i] -= processed
Với cách này, ví dụ meal_time = 5, cứ sau 5 giây sẽ có 1 người được phục vụ xong, đảm bảo độ chính xác.
6. Thiết kế API Endpoints
6.1 POST /api/start-simulation
- Input (JSON body):
dining_time: Thời gian dùng bữa trung bình (phút).meal_time: Thời gian gọi món trung bình (giây/người).max_people: Lượng khách tối đa đến mỗi phút.window_num: Số lượng quầy phục vụ.table_num: Số lượng bàn ăn.session_id(tùy chọn): ID phiên do Frontend tạo.
- Output:
success:truemsg: Thông báo thành công.data: Chứasession_id.
Quy trình xử lý: Kiểm tra tham số đầu vào, tạo session_id nếu cần, gọi simulation.start_simulation() để ghi vào DB và khởi chạy luồng, sau đó trả về session_id cho Frontend.
6.2 POST /api/end-simulation
- Input:
{"session_id": "..."} - Output:
success:truemsg: Thông báo thành công.data: Chứawindow_evaluation,table_evaluation,window_trend,table_trend.
Thiết kế quan trọng: Ưu tiên truy xuất dữ liệu trước khi dừng mô phỏng. Lý do là trạng thái mô phỏng sẽ thay đổi thành "đã kết thúc" sau khi dừng, khiến hàm get_current_simulation (chỉ tìm trạng thái "đang chạy") trả về None.
6.3 POST /api/ai-analyze
- Input: Thông tin API AI (URL, key, payload).
- Output: Phản hồi đầy đủ từ API AI.
Tại sao lại là proxy? API như DashScope không hỗ trợ CORS cho trình duyệt. Backend sử dụng urllib.request để chuyển tiếp yêu cầu (backend không bị giới hạn bởi CORS của trình duyệt), cho phép Frontend chỉ cần gọi API của backend.
7. Triển khai Đẩy Dữ liệu Thời gian thực với Socket.IO
7.1 Cơ chế Phòng (Room Mechanism)
Mỗi phiên mô phỏng có một session_id duy nhất. Backend sử dụng join_room(session_id) để đưa kết nối của client vào phòng tương ứng. Khi gửi dữ liệu, socketio.emit('simulation_data', data, room=session_id) đảm bảo chỉ các client trong phòng đó nhận được dữ liệu.
@socketio.on('bind_session')
def bind_session(session_id):
join_room(session_id) # Client tham gia phòng của phiên mô phỏng
7.2 Cấu hình Tham số Quan trọng
socketio = SocketIO(
app,
cors_allowed_origins=cors_allowed_origins_func,
async_mode='threading', # Sử dụng chế độ luồng (không phải async)
ping_timeout=60, # Thời gian chờ heartbeat (giây)
transports=['websocket', 'polling'], # Các phương thức truyền tải
)
async_mode='threading': Tương thích với các luồng daemon của mô phỏng.ping_timeout=60: Nếu không có hoạt động trong 60 giây, kết nối sẽ bị đóng.transports: Ưu tiên WebSocket, nếu không được sẽ dùng Polling.
8. Chính sách CORS
8.1 CORS là gì?
CORS (Cross-Origin Resource Sharing) là một cơ chế bảo mật của trình duyệt, ngăn chặn các trang web từ một nguồn (origin) truy cập tài nguyên từ nguồn khác nếu máy chủ không cho phép rõ ràng thông qua các tiêu đề HTTP.
8.2 Cách triển khai của chúng tôi
Thay vì sử dụng hành vi mặc định của Flask-CORS, chúng tôi quản lý CORS thủ công để kiểm soát chặt chẽ hơn.
# Kiểm tra xem nguồn gốc của yêu cầu có nằm trong danh sách cho phép hay không
def _is_allowed_origin(origin):
for allowed in CORS_ALLOWED_ORIGINS:
if origin.rstrip('/') == allowed.rstrip('/'):
return True
return False
@app.after_request
def add_cors_headers(response):
origin = request.headers.get('Origin')
if not origin:
return response # Bỏ qua các yêu cầu không phải từ trình duyệt
allowed = _get_allow_origin(origin)
if not allowed:
return response # Nguồn gốc không được phép, không thêm header CORS
response.headers['Access-Control-Allow-Origin'] = allowed
response.headers['Access-Control-Allow-Credentials'] = 'true'
return response
Điểm mấu chốt: http://localhost:5001 và http://127.0.0.1:5001 được coi là hai nguồn gốc (origin) khác nhau. Do đó, danh sách cho phép (whitelist) phải bao gồm cả hai.
9. Quản lý Luồng và Kiểm soát Đồng thời
9.1 Tại sao cần Đa luồng?
Flask mặc định hoạt động đơn luồng. Nếu vòng lặp mô phỏng while True chạy trên luồng chính, toàn bộ máy chủ sẽ bị treo, không thể xử lý các yêu cầu khác. Giải pháp là chạy mỗi mô phỏng trên một luồng riêng biệt.
9.2 An toàn Luồng (Thread Safety)
Trong database.py, chúng ta sử dụng:
self.conn = sqlite3.connect(DB_PATH, check_same_thread=False) # ↑ Cho phép nhiều luồng sử dụng cùng một kết nối
SQLite mặc định không cho phép truy cập kết nối từ các luồng khác nhau. check_same_thread=False cho phép luồng mô phỏng và luồng chính của Flask chia sẻ cùng một kết nối.
9.3 Dừng Luồng Lịch sự
def stop_simulation(self, session_id):
# 1. Đặt cờ dừng
self.simulation_stop_flags[session_id] = True
# 2. Chờ luồng kết thúc (tối đa 5 giây)
self.simulation_threads[session_id].join(timeout=5)
# 3. Đảm bảo mô phỏng được đánh dấu là đã kết thúc trong DB
simulation_id = db.end_simulation(session_id)
# 4. Dọn dẹp tài nguyên
self.simulation_threads.pop(session_id, None)
self.simulation_stop_flags.pop(session_id, None)
10. Cơ chế Hoạt động của AI Proxy
Luồng hoạt động như sau:
- Trình duyệt gửi yêu cầu
POST /api/ai-analyzeđến Backend. - Backend nhận yêu cầu, trích xuất thông tin API và payload.
- Backend sử dụng
urllib.requestđể gửi yêu cầu POST tới API AI thực tế với header xác thực (Authorization: Bearer api_key) và payload tương ứng. - API AI trả về kết quả cho Backend.
- Backend chuyển tiếp kết quả này về cho trình duyệt.
Lý do: Trình duyệt không thể gọi trực tiếp các API AI bên ngoài do lỗi CORS. Backend, với vai trò là máy chủ, có thể thực hiện các yêu cầu HTTP mà không bị ràng buộc bởi chính sách CORS của trình duyệt.
11. Cơ chế Xác thực
Một lớp bảo mật tùy chọn có thể được kích hoạt bằng cách đặt biến môi trường SIMULATION_API_TOKEN.
API_TOKEN = os.environ.get('SIMULATION_API_TOKEN', '')
def _check_auth():
if not API_TOKEN: # Nếu không có token, bỏ qua xác thực
return True
token = request.headers.get('X-API-Token', '')
return token == API_TOKEN
- Không đặt biến môi trường: Tất cả các yêu cầu đều được chấp nhận (thường dùng cho môi trường phát triển).
- Đặt biến môi trường: Mỗi yêu cầu API phải có header
X-API-Tokenvới giá trị khớp với token đã cấu hình.
Đây là biện pháp bảo mật để ngăn chặn truy cập trái phép khi triển khai hệ thống trên môi trường công cộng.
12. Các Vấn đề Gặp phải và Giải pháp
12.1 Cạnh tranh Thời điểm (Race Condition) - Mất dữ liệu giây đầu tiên
- Triệu chứng: Frontend không nhận được dữ liệu của giây đầu tiên sau khi mô phỏng bắt đầu.
- Nguyên nhân: Luồng mô phỏng bắt đầu gửi dữ liệu ngay lập tức, trong khi Frontend chưa kịp xử lý
bind_session. - Giải pháp: Yêu cầu Frontend thực hiện
bind_sessiontrước khi khởi chạy mô phỏng.
12.2 Không nhất quán Chính sách CORS
- Triệu chứng: Lỗi CORS trên trình duyệt, không gọi được API.
- Nguyên nhân:
localhostvà127.0.0.1được coi là các nguồn gốc khác nhau. - Giải pháp: Bao gồm cả hai trong danh sách cho phép CORS và không thêm header CORS cho các nguồn gốc không được phép.
12.3 Mất độ chính xác Tốc độ Gọi món
- Triệu chứng: Hàng đợi tại quầy phục vụ không giảm khi
meal_time > 1. - Nguyên nhân: Làm tròn số nguyên
round(1/5) = 0, dẫn đến xử lý 0 người mỗi giây. - Giải pháp: Sử dụng bộ tích lũy số thực
accumulator += 1.0 / meal_time.
12.4 Không truy vấn được dữ liệu khi Kết thúc Mô phỏng
- Triệu chứng: API kết thúc trả về thông báo "Không có mô phỏng nào đang chạy".
- Nguyên nhân: Gọi hàm dừng mô phỏng (làm thay đổi trạng thái thành 1) trước khi truy vấn dữ liệu (chỉ tìm trạng thái 0).
- Giải pháp: Thay đổi thứ tự thực hiện: ưu tiên truy vấn thông tin, sau đó mới dừng luồng.
12.5 Khóa Luồng SQLite (Locking)
- Triệu chứng: Lỗi
database is lockedthỉnh thoảng xảy ra. - Nguyên nhân: Luồng mô phỏng ghi dữ liệu trong khi luồng chính của Flask đọc dữ liệu, SQLite có khả năng đồng thời hạn chế theo mặc định.
- Giải pháp: Sử dụng
check_same_thread=False, tăngtimeout=30và kích hoạt chế độ WAL.
12.6 Cổng bị Chiếm dụng (Port Conflict)
- Triệu chứng: Lỗi khởi chạy trên Windows do cổng 5000 bị hệ thống dự trữ.
- Nguyên nhân: Một số phiên bản Windows dành cổng 5000 cho dịch vụ hệ thống.
- Giải pháp: Đổi cổng mặc định sang 5001.
13. Các Câu hỏi Thường gặp (Q&A)
Q1: Flask là gì? Tại sao lại chọn Flask?
A: Flask là một micro-framework web cho Python. Nó cung cấp các chức năng cốt lõi và cho phép tùy chỉnh. Lý do chọn Flask: nhẹ, dễ học, tích hợp Flask-SocketIO thuận tiện, và phù hợp với nhu cầu chức năng tập trung của hệ thống.
Q2: Sự khác biệt giữa Socket.IO và yêu cầu HTTP thông thường?
A: HTTP là mô hình "hỏi-đáp" một chiều. Socket.IO cho phép máy chủ chủ động đẩy dữ liệu tới client thông qua kết nối dài. Điều này cần thiết cho việc cập nhật dữ liệu mô phỏng theo thời gian thực.
Q3: Làm thế nào quản lý luồng mô phỏng và xử lý nhiều phiên chạy đồng thời?
A: Mỗi mô phỏng chạy trên một luồng daemon riêng. Ba dictionary quản lý: simulation_threads, simulation_stop_flags, simulation_ids. Giới hạn số lượng mô phỏng đồng thời bằng MAX_CONCURRENT_SIMULATIONS.
Q4: Phân phối chuẩn được sử dụng như thế nào trong mô phỏng?
A: Hai trường hợp: (1) Số lượng khách đến mỗi giây tuân theo phân phối chuẩn N(μ, σ). (2) Khách hàng chọn quầy phục vụ dựa trên phân phối chuẩn tập trung ở giữa. Phân phối chuẩn mô phỏng sự biến động ngẫu nhiên thực tế.
Q5: Tại sao cần bộ tích lũy số thực cho tốc độ gọi món?
A: Để tránh sai số làm tròn. Ví dụ, meal_time=5, mỗi giây xử lý 0.2 người. Làm tròn thành 0 sẽ không bao giờ hoàn thành. Bộ tích lũy cộng dồn phần thập phân và xử lý khi đủ 1 người.
Q6: CORS là gì và cách xử lý?
A: CORS là chính sách bảo mật của trình duyệt ngăn chặn yêu cầu giữa các nguồn gốc khác nhau. Chúng tôi quản lý CORS thủ công bằng cách kiểm tra nguồn gốc yêu cầu và chỉ thêm header cho phép nếu nó nằm trong danh sách cho phép (localhost:5001 và 127.0.0.1:5001).
Q7: Luồng daemon là gì và tại sao sử dụng nó?
A: Luồng daemon tự động kết thúc khi tiến trình chính kết thúc. Sử dụng để đảm bảo các luồng mô phỏng không chạy tiếp và gây treo tiến trình sau khi Flask đã dừng.
Q8: Cơ chế xác thực API hoạt động ra sao?
A: Tùy chọn thông qua biến môi trường SIMULATION_API_TOKEN. Nếu được đặt, yêu cầu API phải có header X-API-Token khớp với giá trị cấu hình.
Q9: Tại sao AI proxy có thời gian chờ 60 giây?
A: Để xử lý các yêu cầu phân tích AI phức tạp có thể mất thời gian. 60 giây là một giới hạn hợp lý, tránh yêu cầu hết hạn quá sớm hoặc người dùng phải chờ quá lâu.
Q10: Backend phát hiện client ngắt kết nối như thế nào?
A: Socket.IO sử dụng cơ chế heartbeat (ping_timeout=60). Tuy nhiên, luồng mô phỏng không tự động dừng; cần client gọi API /api/end-simulation hoặc thực hiện hành động tương tự trước khi đóng trình duyệt.
Q11: Tại sao cổng trong config.py và app.py khác nhau?
A: config.py chứa giá trị mặc định ban đầu (5000). app.py đọc cổng thực tế từ biến môi trường, mặc định là 5001. Giá trị trong app.py mới là giá trị được sử dụng khi chạy.
Q12: Tiêu chí đánh giá "Trải nghiệm tốt"/"Kém" là gì?
A: Số người xếp hàng trung bình tại quầy ≥ 20 người (WINDOW_EVAL_THRESHOLD) hoặc tỷ lệ sử dụng bàn ≥ 80% (TABLE_EVAL_THRESHOLD) được coi là "trải nghiệm kém". Các ngưỡng này được định nghĩa trong config.py.
Q13: Xử lý thế nào khi luồng mô phỏng gặp lỗi?
A: Vòng lặp _simulation_loop có khối try/except để bắt và ghi lại lỗi. Khối finally đảm bảo tài nguyên được dọn dẹp. Hàm stop_simulation có cơ chế dự phòng để đảm bảo dữ liệu không bị mất ngay cả khi luồng bị lỗi.
Q14: Làm thế nào backend xử lý đồng thời nhiều mô phỏng và vẫn phản hồi HTTP?
A: GIL (Global Interpreter Lock) của Python giới hạn thực thi code Python tại một thời điểm, nhưng luồng có thể chuyển đổi nhanh chóng. Các hoạt động I/O (mạng, DB) giải phóng GIL. Luồng mô phỏng ngủ (sleep(1)) không chiếm dụng CPU, cho phép luồng chính của Flask xử lý các yêu cầu HTTP một cách bình thường.