Skip to content

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

  1. Tiền tố legacy_, hai cách truy cập dữ liệu cho cùng một bảng
  2. Hai họ bảng khoá học, khớp nhau bằng id, không phải slug
  3. userId có 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ậpCú phápTên bảng thực tế
D1 clientgetD1Client(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.

ts
// 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ảngBảngVai trò
Danh mụclegacy_coursesDanh sách khoá mà thành viên nhìn thấy, cờ is_active
Chương trình họccoursesBả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.slugcourses.slugid chung
salesclose-salesc0c0c0c0-0008-…
customer-understandingcustomer-decodec0c0c0c0-0001-…
ts
// 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.ts

Quy 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.

ts
// 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
lockedHồ sơ chưa khai đủ 7 trường bắt buộc
basicHồ sơ đủ, chưa trả phí
paidHồ 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à TEXT chứa UUID, sinh bằng crypto.randomUUID()
  • Thời gian là TEXT theo 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ới CREATE 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

Tài liệu nội bộ nền tảng Conan School.