Mô hình dữ liệu
Toàn bộ dữ liệu nằm trong một cơ sở dữ liệu D1 duy nhất: conan-platform-production.
Ba cạm bẫy gây ra hầu hết lỗi trong dự án
- Tiền tố
legacy_, hai cách truy cập dữ liệu cho cùng một bảng - Hai họ bảng khoá học, khớp nhau bằng id, không phải slug
userIdcó thểundefinedở route công khai
Đọc kỹ ba mục dưới trước khi viết truy vấn.
Cạm bẫy 1: tiền tố legacy_
Có hai cách truy cập D1 trong API, và chúng đặt tên bảng khác nhau:
| Cách truy cập | Cú pháp | Tên bảng thực tế |
|---|---|---|
| D1 client | getD1Client(c).from('users') | legacy_users, tự thêm tiền tố |
| SQL thô | env.DB.prepare('SELECT … FROM users') | users, không thêm gì |
Nghĩa là from('courses') và FROM courses trỏ tới hai bảng khác nhau.
// SAI, bảng user_private_data không tồn tại
env.DB.prepare('SELECT * FROM user_private_data WHERE user_id=?1')
// ĐÚNG, SQL thô phải tự viết tiền tố
env.DB.prepare('SELECT * FROM legacy_user_private_data WHERE user_id=?1')Ngoài ra, D1 từ chối câu lệnh SELECT/VALUES ghép quá lớn. Khi ghi nhiều dòng, hãy dùng env.DB.batch() với nhiều câu lệnh nhỏ.
Cạm bẫy 2: hai họ bảng khoá học
Đây là nguyên nhân khiến 11/12 trang chi tiết khoá học từng lỗi 404.
| Họ bảng | Bảng | Vai trò |
|---|---|---|
| Danh mục | legacy_courses | Danh sách khoá mà thành viên nhìn thấy, cờ is_active |
| Chương trình học | courses | Bản ghi của course-intelligence: unit, concept, học liệu |
Hai bảng dùng chung id nhưng slug khác nhau:
legacy_courses.slug | courses.slug | id chung |
|---|---|---|
sales | close-sales | c0c0c0c0-0008-… |
customer-understanding | customer-decode | c0c0c0c0-0001-… |
// SAI, tra slug ở bảng courses sẽ trượt hết các khoá có slug lệch
FROM courses WHERE slug = ?
// ĐÚNG, trượt thì tra tiếp legacy theo slug, rồi nối bằng id
courseBySlug() // xem src/domains/course-intelligence/index.tsQuy tắc
Khi nối hai họ bảng khoá học, luôn nối bằng id. Slug chỉ dùng cho URL.
Cạm bẫy 3: userId ở route công khai
Xem Bản đồ nền tảng mục xác thực. Tóm tắt: route dùng optionalAuthMiddleware có thể không có userId. Dấu ! của TypeScript không bảo vệ lúc chạy.
// SAI, khách vãng lai gây D1_TYPE_ERROR → 500
const data = await personalize(c.get('userId')!)
// ĐÚNG
const viewerId = c.get('userId')
if (!viewerId) return c.json({ data: publicResult })Nhóm bảng chính
Người dùng và tư cách thành viên
legacy_users · legacy_user_private_data · membership_activation_events · platform_settings
Tư cách thành viên có ba mức, tính bởi resolveEntitlement():
| Mức | Điều kiện |
|---|---|
locked | Hồ sơ chưa khai đủ 7 trường bắt buộc |
basic | Hồ sơ đủ, chưa trả phí |
paid | Hồ sơ đủ và đã kích hoạt Membership |
Lưu ý: hồ sơ thiếu thì luôn là locked, kể cả khi đã trả tiền. Nhập mã kích hoạt lúc đang khai báo vẫn hợp lệ, quyền mở ngay khi hồ sơ hoàn tất.
Chương trình học
courses → course_learning_units → course_concepts → course_concept_5e_resources
Đánh giá: course_unit_quiz_bank → course_unit_checkpoint_runs → course_unit_checkpoint_items → course_unit_checkpoint_responses
Tiến độ: course_enrollments · course_concept_progress · course_diagnostic_runs
Chi tiết đầy đủ ở Mô hình chương trình học.
Trí tuệ thành viên
member_model_dimensions · learner_model_ai · content_relevance_assessments · member_intelligence_runs
Xem Learner Model & Recommendation.
Nội dung khác
legacy_ebooks (Deep Books) · library_papers · forum_threads · media_albums · workshops
Quy ước chung
- Khoá chính là
TEXTchứa UUID, sinh bằngcrypto.randomUUID() - Thời gian là
TEXTtheo ISO 8601 UTC:strftime('%Y-%m-%dT%H:%M:%fZ','now') - Cột JSON luôn kèm ràng buộc
CHECK(json_valid(...)) - Mọi bảng dữ liệu người dùng phải có
school_id - Bảng mới khai trong
api.conan.school/migrations/vớiCREATE TABLE IF NOT EXISTS(để chạy lại vô hại); CI áp migration trước bước deploy từ 01.09.2026. Vài module cũ (member-feedback,user/prompts,user/forum) còn tạo bảng lúc chạy, đó là nợ, không phải mẫu để theo