Tài liệu này dành cho partner tích hợp lần đầu API chia sẻ học liệu trên Trục LMS (bao gồm luồng upload tệp đính kèm và chia sẻ học liệu).
Chọn nhánh triển khai
Chọn đúng tài liệu theo loại đơn vị để triển khai nhanh và giảm nhầm lẫn:
| Đơn vị tích hợp | Tài liệu nên đọc trước | Endpoint học liệu | Ghi chú chính |
|---|---|---|---|
| Nhà trường | Tích hợp cho Nhà trường | POST /api/lms-base/learning-resources | Kiểm tra chặt authorPinCode/creatorPinCode theo ngữ cảnh dữ liệu Sở |
| Tổ chức | Tích hợp cho Tổ chức | POST /api/lms-base/learning-resources/organizations | Upsert theo partnerId + sourceId; có thể để trống thông tin người chia sẻ để hệ thống tự điền |
| Sở | Tham chiếu tài liệu tổng quan trang này | POST /api/lms-base/learning-resources/doets | Upsert theo scope Sở; hệ thống tự gán tác giả/người chia sẻ theo mã và tên Sở |
| Xã/phường | Tham chiếu tài liệu tổng quan trang này | POST /api/lms-base/learning-resources/divisions | Upsert theo scope xã/phường; hệ thống tự gán tác giả/người chia sẻ theo mã và tên xã/phường |
Trang này giữ vai trò tài liệu tổng quan + reference dùng chung cho cả 4 nhánh. Hướng dẫn tích hợp API tải học liệu đã duyệt: Tải học liệu đã duyệt.
Tích hợp nhanh
Happy path chuẩn:
- Upload file qua
POST /api/lms-base/attachments. - Nhận
idvàpathcho từng file. - Replace URL trong HTML (
src/href/poster) bằngpathđã upload. - Gọi API upsert học liệu theo đúng scope với
fileIdskhớp đúng các file đang dùng. - Poll trạng thái duyệt qua endpoint danh sách tương ứng scope:
- Nhà trường:
GET /api/lms-base/learning-resources - Tổ chức:
GET /api/lms-base/learning-resources/organizations - Sở:
GET /api/lms-base/learning-resources/doets - Xã/phường:
GET /api/lms-base/learning-resources/divisions
- Nhà trường:
Lưu ý quan trọng:
POST /learning-resourcesđang xử lý theo cơ chế upsert theo cặpschoolCode + sourceIdtrong phạm vi partner gọi API (X-Api-Key): nếu đã có bản ghiPending/Rejectedthì cập nhật, nếu chưa có thì tạo mới, nếu đãApprovedthì bị từ chối.- Nếu chia sẻ theo scope tổ chức, dùng
POST /learning-resources/organizations; cơ chế upsert theo cặppartnerId + sourceId. - Nếu chia sẻ theo scope Sở, dùng
POST /learning-resources/doets; cơ chế upsert theosourceIdtrong phạm vi partner +doetCode. - Nếu chia sẻ theo scope xã/phường, dùng
POST /learning-resources/divisions; cơ chế upsert theosourceIdtrong phạm vi partner +doetCode + divisionCode. authorPinCodevàcreatorPinCodevới endpointPOST /learning-resourcesphải là CCCD 12 số, tồn tại đúng ngữ cảnh dữ liệu Sở (doetCode/divisionCode/schoolCode/yearCode).gradeCode/subjectCode/knowledgeUnitCodephải map theo danh mục dùng chung:https://docs.hanoi.edu.vn/docs/knowledge-units.- Nếu truyền
fileIds, tất cảfileIdsbắt buộc phải thực sự xuất hiện trongthumbnailUrlhoặccontents. - Theo dõi trạng thái duyệt: ưu tiên dùng webhook (Trục chủ động gọi về partner khi đổi trạng thái — xem mục 7.6); có thể dùng polling API danh sách để đối soát/dự phòng.
1. Tổng quan bài toán
Bài toán tích hợp gồm 2 phần:
- Quản lý file dùng trong học liệu (attachments).
- Tạo bản ghi học liệu số (learning resource) và theo dõi duyệt.
Thông tin kết nối:
- Endpoint theo môi trường:
- Môi trường Production (
PROD):https://partner.hanoi.edu.vn/ - Môi trường Development (
DEV):https://lmsbpartnerapi.vstudy.edu.vn/
- Môi trường Production (
- Auth header:
X-Api-Key: <api-key-don-vi-duoc-cap> - JSON API:
Content-Type: application/json - Upload file:
multipart/form-datacho/api/lms-base/attachments
Lưu ý: các ví dụ curl trong tài liệu này đang dùng endpoint môi trường Development (DEV). Khi triển khai thật, đổi sang môi trường Production (PROD).
Liên hệ cấp X-Api-Key
Thông tin dưới đây chỉ dùng cho việc cấp API key tích hợp Partner API:
| Đơn vị | Người phụ trách | Số điện thoại | |
|---|---|---|---|
| QIG | Nguyễn Thành Luân | 0988933774 | thanhluan@qig.vn |
Format response chung:
{
"success": true,
"message": "Mô tả kết quả",
"data": {}
}2. Luồng tích hợp end-to-end
- Chuẩn bị metadata học liệu (
sourceId, mã phân loại, thông tin tác giả/người gửi). - Upload các file/media sẽ được tham chiếu trong học liệu (ảnh đại diện nếu dùng ảnh đã upload, file trong
contents, SCORM.zipnếu có). VớiYOUTUBE,IFRAMEhoặcthumbnailUrldùng URL ngoài hợp lệ, không cần upload file tương ứng. - Lưu lại
id+pathtừ response upload. - Replace URL trong
contents[].contentbằngpathtương ứng;thumbnailUrlcó thể dùng URL ảnh đã upload hoặc URL ảnh ngoài hợp lệ. - Tạo payload học liệu với
fileIdskhớp đúng file đang dùng. - Gọi API chia sẻ học liệu (upsert) theo scope phù hợp:
POST /api/lms-base/learning-resources(scope trường)POST /api/lms-base/learning-resources/organizations(scope tổ chức)POST /api/lms-base/learning-resources/doets(scope Sở)POST /api/lms-base/learning-resources/divisions(scope xã/phường)
- Poll API danh sách theo scope để theo dõi
status:- Nhà trường:
GET /api/lms-base/learning-resources - Tổ chức:
GET /api/lms-base/learning-resources/organizations - Sở:
GET /api/lms-base/learning-resources/doets - Xã/phường:
GET /api/lms-base/learning-resources/divisions
- Nhà trường:
- Khi học liệu đã được duyệt và đủ điều kiện khai thác, tích hợp API tải theo tài liệu: Tải học liệu đã duyệt.
- Nếu cần, kiểm tra trạng thái từng file bằng
GET /api/lms-base/attachments/{attachmentId}. - Nếu cần thu hồi bản ghi đang chờ duyệt, gọi API xóa theo scope:
- Nhà trường:
DELETE /api/lms-base/learning-resources - Tổ chức:
DELETE /api/lms-base/learning-resources/organizations - Sở:
DELETE /api/lms-base/learning-resources/doets - Xã/phường:
DELETE /api/lms-base/learning-resources/divisions
3. Checklist trước khi gọi API
Checklist bắt buộc:
- Đã có
X-Api-Keyhợp lệ. - Đã map đúng mã dùng chung của Sở (
gradeCode,subjectCode,knowledgeUnitCode) từ danh mụcknowledge-units. - Với endpoint
/learning-resources, đã xác thựcauthorPinCode/creatorPinCodeđúng người, đúng ngữ cảnh (doetCode/divisionCode/schoolCode/yearCode). - Đã chọn đúng endpoint theo scope chia sẻ:
POST /learning-resources: upsert theoschoolCode + sourceIdtrong phạm vi partner gọi API (X-Api-Key).POST /learning-resources/organizations: upsert theopartnerId + sourceId.POST /learning-resources/doets: upsert theosourceIdtrong phạm vi partner +doetCode.POST /learning-resources/divisions: upsert theosourceIdtrong phạm vi partner +doetCode + divisionCode.- Poll duyệt/xóa pending cũng theo đúng scope tương ứng.
- Nếu
sourceIdđã có bản ghiApproved, phải dùngsourceIdmới để chia sẻ tiếp (không cập nhật đè). - File upload đúng loại MIME cho mục đích sử dụng.
- Giới hạn request upload của API là
<= 1073741824byte (1 GB); từng định dạng tệp vẫn có thể bị giới hạn thấp hơn theo cấu hình hệ thống. - Tất cả URL trong
thumbnailUrlvàcontentslà URL tuyệt đối hợp lệ (http/https). - Nếu có
fileIds, mỗi id phải là ULID 26 ký tự, thuộc đúng partner và thực sự được dùng trongthumbnailUrlhoặccontents. - HTML content không chứa thành phần bị cấm (CSS/JS/iframe/object/embed/on*/protocol không an toàn).
4. Quickstart với 1 ví dụ đầy đủ
4.1. Nội dung HTML ban đầu (before replace)
<p>Xem video minh họa:</p>
<video controls poster="https://partner.example.com/media/thumb-phan-so.png">
<source src="https://partner.example.com/media/video-phan-so.mp4" type="video/mp4" />
</video>
<p>Tải tài liệu PDF: <a href="https://partner.example.com/media/phieu-bai-tap.pdf">Tại đây</a></p>4.2. Upload file như thế nào
Upload từng file riêng qua POST /api/lms-base/attachments.
curl --location 'https://lmsbpartnerapi.vstudy.edu.vn/api/lms-base/attachments' \
--header 'X-Api-Key: <api-key-don-vi-duoc-cap>' \
--form 'doetCode="01"' \
--form 'divisionCode="0001"' \
--form 'schoolCode="01000001"' \
--form 'educationLevelCode="02"' \
--form 'file=@"/path/to/thumb-phan-so.png"'{
"success": true,
"message": "Tải tệp thành công.",
"data": {
"id": "01JQ2S96MKZ8E8GHW8EXB4VPA7",
"path": "https://<cdn-or-storage>/partner-code/thumb-phan-so.png",
"useStatus": "Draft"
}
}Tương tự upload video và pdf (mỗi file 1 request), ví dụ nhận:
- Video:
id=01JQ2SAXA9QXRVQ8S9QPA4J2AN,path=https://<cdn-or-storage>/partner-code/video-phan-so.mp4 - PDF:
id=01JQ2SB6R52J01Q2RC9F0M6V7P,path=https://<cdn-or-storage>/partner-code/phieu-bai-tap.pdf
4.3. HTML sau khi replace URL (after replace)
<p>Xem video minh họa:</p>
<video controls poster="https://<cdn-or-storage>/partner-code/thumb-phan-so.png">
<source src="https://<cdn-or-storage>/partner-code/video-phan-so.mp4" type="video/mp4" />
</video>
<p>Tải tài liệu PDF: <a href="https://<cdn-or-storage>/partner-code/phieu-bai-tap.pdf">Tại đây</a></p>4.4. Payload cuối cùng gửi lên learning-resources
{
"sourceId": "LR-HN-000001",
"title": "Bài học: So sánh phân số",
"thumbnailUrl": "https://<cdn-or-storage>/partner-code/thumb-phan-so.png",
"authorPinCode": "001095123456",
"authorName": "Nguyen Van A",
"creatorPinCode": "001095123456",
"creatorName": "Nguyen Van A",
"fileIds": [
"01JQ2S96MKZ8E8GHW8EXB4VPA7",
"01JQ2SAXA9QXRVQ8S9QPA4J2AN",
"01JQ2SB6R52J01Q2RC9F0M6V7P",
"01JQ2SCF3X7VY7S5M9N0B8K4Q1"
],
"contents": [
{
"name": "Mở đầu",
"displayOrder": 1,
"type": "TEXT",
"content": "<p>Xem video minh họa:</p><video controls poster=\"https://<cdn-or-storage>/partner-code/thumb-phan-so.png\"><source src=\"https://<cdn-or-storage>/partner-code/video-phan-so.mp4\" type=\"video/mp4\" /></video><p>Tải tài liệu PDF: <a href=\"https://<cdn-or-storage>/partner-code/phieu-bai-tap.pdf\">Tại đây</a></p>",
"description": "Mở đầu"
},
{
"name": "Video tải lên",
"displayOrder": 2,
"type": "VIDEO",
"content": "https://<cdn-or-storage>/partner-code/video-phan-so-2.mp4",
"description": "Video chính của học liệu"
},
{
"name": "Video YouTube tham khảo",
"displayOrder": 3,
"type": "YOUTUBE",
"content": "https://www.youtube.com/watch?v=d3bn7DVrZOw",
"description": "Video tham khảo"
}
],
"gradeCode": "06",
"subjectCode": "0601",
"knowledgeUnitCode": "201100101",
"doetCode": "01",
"divisionCode": "0001",
"schoolCode": "01000001",
"yearCode": 2025,
"educationLevelCode": "02",
"semesterCode": 1
}Gọi API:
curl --location 'https://lmsbpartnerapi.vstudy.edu.vn/api/lms-base/learning-resources' \
--header 'X-Api-Key: <api-key-don-vi-duoc-cap>' \
--header 'Content-Type: application/json' \
--data '<payload-json-o-tren>'Response thành công:
{
"success": true,
"message": "Tạo mới học liệu số thành công.",
"data": {
"sourceId": "LR-HN-000001",
"id": "01JQ2T5W11Z9J6AEX4Q6F90HME",
"approvalStatus": 0,
"operation": "created"
}
}operation có 2 giá trị:
created: tạo mới học liệu.updated: cập nhật bản ghi đã có ở trạng tháiPendinghoặcRejected.
4.5. Poll trạng thái duyệt
curl --location --get 'https://lmsbpartnerapi.vstudy.edu.vn/api/lms-base/learning-resources' \
--header 'X-Api-Key: <api-key-don-vi-duoc-cap>' \
--data-urlencode 'approvalStatus=0' \
--data-urlencode 'doetCode=01' \
--data-urlencode 'schoolCode=01000001' \
--data-urlencode 'skip=0' \
--data-urlencode 'take=20'Response thành công:
{
"success": true,
"message": "Lấy danh sách học liệu thành công.",
"data": [
{
"id": "01JQ2T5W11Z9J6AEX4Q6F90HME",
"sourceId": "LR-HN-000001",
"title": "Bài học: So sánh phân số",
"status": "Pending",
"reason": null,
"occurredAt": "2026-04-12T07:12:44.0000000Z",
"canDelete": true,
"createdAt": "2026-04-12T07:12:44.0000000Z",
"updatedAt": "2026-04-12T07:12:44.0000000Z"
}
]
}Trường status trong item response là chuỗi, gồm 5 giá trị: Pending, Approved, Rejected, Hidden, Removed. Khi duyệt/kiểm duyệt xong, status đổi từ Pending sang Approved/Rejected (hoặc về sau có thể là Hidden/Removed); với Rejected/Removed sẽ kèm reason.
Lưu ý: query param approvalStatus để lọc vẫn dùng số (0/1/2), độc lập với trường status dạng chuỗi trong response.
Nếu chia sẻ theo scope khác, poll bằng endpoint tương ứng:
curl --location --get 'https://lmsbpartnerapi.vstudy.edu.vn/api/lms-base/learning-resources/organizations' \
--header 'X-Api-Key: <api-key-don-vi-duoc-cap>' \
--data-urlencode 'approvalStatus=0' \
--data-urlencode 'skip=0' \
--data-urlencode 'take=20'Các endpoint polling theo scope:
- Nhà trường:
GET /api/lms-base/learning-resources - Tổ chức:
GET /api/lms-base/learning-resources/organizations - Sở:
GET /api/lms-base/learning-resources/doets - Xã/phường:
GET /api/lms-base/learning-resources/divisions
Response giữa các endpoint có cùng cấu trúc (success, message, data[]).
5. Upload attachments
5.1. Upload tệp
- Method:
POST - Path:
/api/lms-base/attachments - Content-Type:
multipart/form-data
Form-data fields:
| Field | Bắt buộc | Kiểu dữ liệu | Kích thước/giới hạn | Mô tả |
|---|---|---|---|---|
doetCode | Có | string | Tối đa 50 ký tự | Mã Sở GDĐT |
divisionCode | Không | string / null | Tối đa 50 ký tự | Mã phường/xã |
schoolCode | Không | string / null | Tối đa 50 ký tự | Nếu truyền thì phải là mã trường hợp lệ thuộc doetCode; nếu để trống thì hệ thống xử lý theo luồng không gắn trường |
educationLevelCode | Không | string / null | Tối đa 50 ký tự | Nếu truyền thì phải là mã cấp học hợp lệ. Khi có schoolCode thì bắt buộc truyền và phải khớp cấp học của trường |
file | Có | IFormFile (binary) | > 0 và <= 1073741824 byte | File/media cần upload |
Ràng buộc kỹ thuật:
- Kích thước file:
> 0và<= 1073741824byte (1 GB). - Tệp upload phải có phần mở rộng và phần mở rộng phải nằm trong danh mục hệ thống hỗ trợ.
- Ngoài giới hạn request
1 GB, hệ thống còn đối chiếu giới hạn dung lượng theo từng phần mở rộng từ cấu hình. - MIME được kiểm tra theo chữ ký file (không chỉ extension).
- MIME được chấp nhận:
image:image/jpeg,image/png,image/gif,image/webp.document:application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document.spreadsheet:application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.slide:application/vnd.ms-powerpoint,application/vnd.openxmlformats-officedocument.presentationml.presentation.video:video/mp4,video/webm.audio:audio/mpeg.scorm:application/zip.- Nếu không truyền
schoolCode:doetCodebắt buộc;divisionCodenếu có phải thuộcdoetCode;educationLevelCodenếu có phải là mã cấp học hợp lệ. - Nếu truyền
schoolCode: hệ thống bắt buộc kiểm tra trường thuộcdoetCode;educationLevelCodebắt buộc và phải khớp dữ liệu trường;divisionCodenếu có phải khớp với trường và theo đúng năm học dữ liệu trường. - File upload phải được mapping dùng trong
thumbnailUrlhoặccontents; file upload không được mapping sử dụng sẽ bị xóa khỏi hệ thống. - Cơ chế dọn file không được mapping do hệ thống trục có job tự động quét dọn; partner không cần xử lý luồng này.
5.2. Kiểm tra trạng thái tệp
- Method:
GET - Path:
/api/lms-base/attachments/{attachmentId}
Ví dụ response:
{
"success": true,
"message": "Lấy trạng thái tệp thành công.",
"data": {
"useStatus": "Used",
"ownerId": "01JQ2T5W11Z9J6AEX4Q6F90HME",
"accessUrl": "https://<cdn-or-storage>/partner-code/uuid.mp4"
}
}Ý nghĩa useStatus:
Draft: file chưa gắn vào học liệu.Used: file đã gắn với một học liệu.
6. Tạo learning resource
6.1. Endpoint
- Method:
POST - Path (scope trường):
/api/lms-base/learning-resources - Path (scope tổ chức):
/api/lms-base/learning-resources/organizations - Path (scope Sở):
/api/lms-base/learning-resources/doets - Path (scope xã/phường):
/api/lms-base/learning-resources/divisions - Content-Type:
application/json
Rule upsert theo endpoint:
- Scope trường (
/learning-resources): xác định bản ghi theoschoolCode + sourceIdtrong phạm vi partner gọi API (X-Api-Key). - Scope tổ chức (
/learning-resources/organizations): xác định bản ghi theopartnerId + sourceId. - Scope Sở (
/learning-resources/doets): xác định bản ghi theosourceIdtrong phạm vi partner +doetCode. - Scope xã/phường (
/learning-resources/divisions): xác định bản ghi theosourceIdtrong phạm vi partner +doetCode + divisionCode. - Nếu đã có bản ghi
Approved, API từ chối cập nhật và yêu cầusourceIdmới.
6.2. Các trường request
Quy ước đọc cột Bắt buộc:
Có: bắt buộc cho mọi scope chia sẻ.Có (scope trường): bắt buộc vớiPOST /learning-resources.Có (scope trường, Sở, xã/phường): bắt buộc với 3 scope này; scope tổ chức được tự gán theo rule hệ thống.Có (scope xã/phường): chỉ bắt buộc vớiPOST /learning-resources/divisions.Không: không bắt buộc, nhưng nếu truyền vẫn bị validate định dạng/độ dài.
| Field | Bắt buộc | Kiểu dữ liệu | Kích thước/giới hạn | Ghi chú |
|---|---|---|---|---|
sourceId | Có | string | Tối đa 128 ký tự | Khóa upsert theo scope: trường dùng schoolCode + sourceId, tổ chức dùng partnerId + sourceId, Sở dùng doetCode + sourceId, xã/phường dùng doetCode + divisionCode + sourceId (đều trong phạm vi partner gọi API). Chỉ chấp nhận văn bản thuần, không chứa HTML |
title | Có | string | Tối đa 500 ký tự | Tiêu đề học liệu. Chỉ chấp nhận văn bản thuần, không chứa HTML |
description | Không | string / null | Tối đa 1024 ký tự | Mô tả học liệu dạng văn bản hoặc HTML an toàn (áp dụng rule an toàn nội dung) |
thumbnailUrl | Có | string | Tối đa 1024 ký tự; URL tuyệt đối | Bắt buộc. Có thể là path ảnh đã upload hoặc URL ảnh ngoài hợp lệ (.jpg, .jpeg, .png, .gif, .bmp, .webp) |
authorPinCode | Có (scope trường) | string | Tối đa 50 ký tự; scope trường yêu cầu CCCD 12 chữ số | Scope trường: bắt buộc CCCD hợp lệ theo ngữ cảnh dữ liệu Sở. Scope tổ chức: có thể để trống, hệ thống tự điền từ thông tin partner. Scope Sở/xã-phường: hệ thống tự gán theo mã đơn vị sở hữu |
authorName | Có (scope trường) | string | Tối đa 256 ký tự | Scope trường: tên tác giả do đơn vị truyền. Scope tổ chức: có thể để trống để hệ thống tự điền theo tên tổ chức. Scope Sở/xã-phường: hệ thống tự gán theo tên đơn vị sở hữu. Nếu truyền thì chỉ chấp nhận văn bản thuần, không chứa HTML |
creatorPinCode | Có (scope trường) | string | Tối đa 50 ký tự; scope trường yêu cầu CCCD 12 chữ số | Scope trường: CCCD người chia sẻ phải hợp lệ theo ngữ cảnh dữ liệu Sở. Scope tổ chức: có thể để trống, hệ thống tự điền từ thông tin partner. Scope Sở/xã-phường: hệ thống tự gán theo mã đơn vị sở hữu |
creatorName | Có (scope trường) | string | Tối đa 256 ký tự | Scope trường: tên người chia sẻ do đơn vị truyền. Scope tổ chức: có thể để trống để hệ thống tự điền theo tên tổ chức. Scope Sở/xã-phường: hệ thống tự gán theo tên đơn vị sở hữu. Nếu truyền thì chỉ chấp nhận văn bản thuần, không chứa HTML |
fileIds | Không | array<string> | Mỗi phần tử ULID 26 ký tự | Danh sách attachment dùng cho thumbnailUrl và contents; nếu truyền thì tất cả phải hợp lệ và thuộc đối tác |
contents | Có | array<object> | Ít nhất 1 phần tử | Danh sách nội dung học liệu |
gradeCode | Có | string | Tối đa 20 ký tự | Mã khối theo danh mục định danh của Sở |
subjectCode | Có | string | Tối đa 20 ký tự | Mã môn học theo danh mục định danh của Sở |
knowledgeUnitCode | Có | string | Tối đa 10 ký tự | Mã đơn vị kiến thức theo danh mục định danh của Sở |
doetCode | Có (scope trường, Sở, xã/phường) | string | Tối đa 20 ký tự | Scope trường/Sở/xã-phường: bắt buộc. Scope tổ chức: có thể để trống, hệ thống tự gán cố định 01 theo cấu hình hiện tại |
divisionCode | Có (scope xã/phường) | string / null | Tối đa 20 ký tự | Scope xã/phường: bắt buộc và phải thuộc doetCode + yearCode. Scope trường/tổ chức/Sở: không bắt buộc |
schoolCode | Có (scope trường) | string | Tối đa 50 ký tự | Scope trường: bắt buộc và dùng để validate ngữ cảnh. Scope tổ chức/Sở/xã-phường: không bắt buộc và thường để rỗng |
yearCode | Có | int | Từ 1000 đến 9999 | Năm học dạng 4 chữ số và phải tồn tại trong danh mục năm học hệ thống |
educationLevelCode | Có | string | Tối đa 20 ký tự | Mã cấp học |
semesterCode | Có | int | Chỉ nhận 1 hoặc 2 | Học kỳ |
6.3. Mapping mã dùng chung (bắt buộc)
Các trường phân loại học liệu phải dùng mã định danh do Sở quy định.
| Trường | Yêu cầu mapping | Nguồn tra cứu |
|---|---|---|
gradeCode | Map đúng mã khối thuộc educationLevelCode | https://docs.hanoi.edu.vn/docs/knowledge-units |
subjectCode | Map đúng mã môn học thuộc gradeCode | https://docs.hanoi.edu.vn/docs/knowledge-units |
knowledgeUnitCode | Map đúng mã đơn vị kiến thức thuộc cặp gradeCode/subjectCode | https://docs.hanoi.edu.vn/docs/knowledge-units |
6.4. Cấu trúc contents[]
| Field | Bắt buộc | Kiểu dữ liệu | Kích thước/giới hạn | Ghi chú |
|---|---|---|---|---|
name | Có | string | Tối đa 256 ký tự | Tên nội dung. Chỉ chấp nhận văn bản thuần, không chứa HTML |
displayOrder | Không | int / null | >= 1; không truyền thì tự gán | Nếu không truyền, hệ thống tự gán theo thứ tự mảng (bắt đầu từ 1) |
type | Có | string (enum) | Tối đa 20 ký tự | TEXT, DOCUMENT, SPREADSHEET, VIDEO, AUDIO, YOUTUBE, SCORM, SLIDE, IFRAME |
content | Có | string | Không giới hạn độ dài (nvarchar(max)) | Nội dung theo rule từng type |
description | Không | string / null | Tối đa 1024 ký tự | Mô tả nội dung dạng văn bản hoặc HTML an toàn |
Rule content theo type:
type | Rule cho content |
|---|---|
TEXT | Văn bản thuần hoặc HTML; nếu HTML thì URL trong src/href/poster phải là URL tuyệt đối hợp lệ |
DOCUMENT | URL tuyệt đối; nếu map file upload thì MIME phải là application/pdf, application/msword, hoặc application/vnd.openxmlformats-officedocument.wordprocessingml.document |
SPREADSHEET | URL tuyệt đối; nếu map file upload thì MIME phải là application/vnd.ms-excel hoặc application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
VIDEO | URL tuyệt đối; nếu map file upload thì MIME phải là video/mp4 hoặc video/webm |
AUDIO | URL tuyệt đối; nếu map file upload thì MIME phải là audio/mpeg |
YOUTUBE | Chỉ chấp nhận URL tuyệt đối dạng https://youtube.com/watch?v=... (bao gồm www.youtube.com/watch?v=...) hoặc https://youtu.be/...; không truyền iframe |
SCORM | URL tuyệt đối trỏ tới file .zip đã upload; MIME phải là application/zip |
SLIDE | URL tuyệt đối; nếu map file upload thì MIME phải là application/vnd.ms-powerpoint hoặc application/vnd.openxmlformats-officedocument.presentationml.presentation |
IFRAME | URL tuyệt đối (http/https) của trang/công cụ ngoài cần nhúng; domain phải thuộc danh sách cho phép (mặc định: youtube.com, olm.vn, sgk.olm.vn và các subdomain). Không map file upload, không truyền thẻ <iframe> HTML |
Định dạng được hỗ trợ theo type:
TEXT: nội dung inline.DOCUMENT:pdf,doc,docx.SPREADSHEET:xls,xlsx.VIDEO:mp4,webm.AUDIO:mp3.YOUTUBE: link YouTube tuyệt đối hợp lệ (không áp dụng định dạng file upload).SCORM:zip.SLIDE:ppt,pptx.IFRAME: URL tuyệt đối thuộc domain được phép (không áp dụng định dạng file upload).
Rule an toàn nội dung (bắt buộc):
contentkhông được chứa CSS/TypeScript/JavaScript.- Trong HTML (
TEXT/description) không được dùng thẻiframe,object,embed. Muốn nhúng trang/công cụ ngoài, dùng nội dung loạiIFRAME(truyền URL, không truyền thẻ<iframe>). - Không dùng event handler dạng
on*. - Không dùng protocol/script không an toàn:
javascript:,vbscript:,data:text/html,data:application/javascript. - Chỉ hỗ trợ SCORM
1.2hoặc2004. - Nội dung loại
IFRAMEchỉ chấp nhận URL tuyệt đối (http/https) có domain (hoặc subdomain) thuộc danh sách cho phép cấu hình ởLearningContentIframe:AllowedHosts(mặc định:youtube.com,olm.vn,sgk.olm.vn). - Nếu truyền
fileIds, tất cảfileIdsphải được dùng trongthumbnailUrlhoặccontents; nếu không sẽ bị từ chối.
7. Tra cứu trạng thái duyệt
7.1. Endpoint danh sách học liệu nhà trường
- Method:
GET - Path:
/api/lms-base/learning-resources - Bắt buộc truyền
doetCodetrong query.
Query params:
| Field | Kiểu dữ liệu | Giới hạn | Ghi chú |
|---|---|---|---|
sourceId | string / null | Tối đa 128 ký tự | Lọc theo mã học liệu nguồn |
educationLevelCode | string / null | Tối đa 20 ký tự | Trim khoảng trắng đầu/cuối trước khi lọc |
doetCode | string | Tối đa 20 ký tự | Bắt buộc, lọc theo mã Sở GDĐT |
divisionCode | string / null | Tối đa 20 ký tự | Lọc theo mã phường/xã |
schoolCode | string / null | Tối đa 50 ký tự | Lọc theo mã trường |
approvalStatus | int / null | 0, 1, 2 | 0=Pending, 1=Approved, 2=Rejected |
skip | int / null | >= 0 | Mặc định 0 |
take | int / null | 1..200 | Mặc định 20 |
Ví dụ gọi API:
curl --location --get 'https://lmsbpartnerapi.vstudy.edu.vn/api/lms-base/learning-resources' \
--header 'X-Api-Key: <api-key-don-vi-duoc-cap>' \
--data-urlencode 'approvalStatus=0' \
--data-urlencode 'doetCode=01' \
--data-urlencode 'skip=0' \
--data-urlencode 'take=20'Ví dụ item response:
{
"id": "01JQ2T5W11Z9J6AEX4Q6F90HME",
"sourceId": "LR-HN-000001",
"title": "Bài học: So sánh phân số",
"status": "Rejected",
"reason": "Nội dung chưa đúng chuẩn chương trình.",
"occurredAt": "2026-04-12T07:12:44.0000000Z",
"canDelete": false,
"createdAt": "2026-04-12T07:12:44.0000000Z",
"updatedAt": "2026-04-12T07:12:44.0000000Z"
}Các trường trong item response:
| Field | Kiểu dữ liệu | Ghi chú |
|---|---|---|
id | string (ULID) | Mã định danh học liệu trên trục |
sourceId | string | Mã nguồn học liệu phía đối tác |
title | string | Tiêu đề học liệu |
status | string | Trạng thái hiện tại: Pending, Approved, Rejected, Hidden, Removed. Trạng thái kiểm duyệt (Removed, Hidden) được ưu tiên trước trạng thái duyệt |
reason | string / null | Lý do theo trạng thái hiện tại; chỉ có giá trị khi Rejected (lý do từ chối) hoặc Removed (lý do gỡ bỏ) |
occurredAt | string (date-time) / null | Thời điểm UTC trạng thái hiện tại được ghi nhận (fallback về thời điểm cập nhật/tạo nếu thiếu) |
canDelete | bool | true khi học liệu đang Pending (được phép xóa) |
createdAt | string (date-time) | Thời điểm tạo UTC |
updatedAt | string (date-time) / null | Thời điểm cập nhật UTC |
Lưu ý: query param approvalStatus chỉ lọc theo 3 trạng thái duyệt (0=Pending, 1=Approved, 2=Rejected); chưa hỗ trợ lọc trực tiếp theo Hidden/Removed. Trường status trong response vẫn phản ánh đầy đủ 5 trạng thái.
7.2. Endpoint danh sách học liệu tổ chức
- Method:
GET - Path:
/api/lms-base/learning-resources/organizations
Query params (đều không bắt buộc):
| Field | Kiểu dữ liệu | Giới hạn | Ghi chú |
|---|---|---|---|
sourceId | string / null | Tối đa 128 ký tự | Lọc theo mã học liệu nguồn |
educationLevelCode | string / null | Tối đa 20 ký tự | Lọc theo mã cấp học |
approvalStatus | int / null | 0, 1, 2 | 0=Pending, 1=Approved, 2=Rejected |
skip | int / null | >= 0 | Mặc định 0 |
take | int / null | 1..200 | Mặc định 20 |
Ví dụ gọi API:
curl --location --get 'https://lmsbpartnerapi.vstudy.edu.vn/api/lms-base/learning-resources/organizations' \
--header 'X-Api-Key: <api-key-don-vi-duoc-cap>' \
--data-urlencode 'approvalStatus=0' \
--data-urlencode 'skip=0' \
--data-urlencode 'take=20'7.3. Endpoint danh sách học liệu Sở
- Method:
GET - Path:
/api/lms-base/learning-resources/doets - Bắt buộc truyền
doetCodetrong query.
Query params:
| Field | Kiểu dữ liệu | Giới hạn | Ghi chú |
|---|---|---|---|
sourceId | string / null | Tối đa 128 ký tự | Lọc theo mã học liệu nguồn |
educationLevelCode | string / null | Tối đa 20 ký tự | Lọc theo mã cấp học |
doetCode | string | Tối đa 20 ký tự | Bắt buộc, lọc theo mã Sở GDĐT |
divisionCode | string / null | - | Không hỗ trợ ở luồng Sở (nếu truyền sẽ bị từ chối) |
schoolCode | string / null | - | Không hỗ trợ ở luồng Sở (nếu truyền sẽ bị từ chối) |
approvalStatus | int / null | 0, 1, 2 | 0=Pending, 1=Approved, 2=Rejected |
skip | int / null | >= 0 | Mặc định 0 |
take | int / null | 1..200 | Mặc định 20 |
7.4. Endpoint danh sách học liệu xã/phường
- Method:
GET - Path:
/api/lms-base/learning-resources/divisions - Bắt buộc truyền
doetCodevàdivisionCodetrong query.
Query params:
| Field | Kiểu dữ liệu | Giới hạn | Ghi chú |
|---|---|---|---|
sourceId | string / null | Tối đa 128 ký tự | Lọc theo mã học liệu nguồn |
educationLevelCode | string / null | Tối đa 20 ký tự | Lọc theo mã cấp học |
doetCode | string | Tối đa 20 ký tự | Bắt buộc, lọc theo mã Sở GDĐT |
divisionCode | string | Tối đa 20 ký tự | Bắt buộc, lọc theo mã đơn vị quản lý |
schoolCode | string / null | - | Không hỗ trợ ở luồng xã/phường (nếu truyền sẽ bị từ chối) |
approvalStatus | int / null | 0, 1, 2 | 0=Pending, 1=Approved, 2=Rejected |
skip | int / null | >= 0 | Mặc định 0 |
take | int / null | 1..200 | Mặc định 20 |
7.5. Cách theo dõi duyệt cho partner
Có 2 cơ chế theo dõi trạng thái duyệt:
- Webhook (khuyến nghị): Trục LMS chủ động gọi tới endpoint của partner mỗi khi học liệu đổi trạng thái. Chi tiết ở mục 7.6.
- Polling (dự phòng/đối soát): partner tự gọi API danh sách theo scope để tra cứu trạng thái.
Endpoint polling theo scope:
- Nhà trường:
GET /api/lms-base/learning-resources - Tổ chức:
GET /api/lms-base/learning-resources/organizations - Sở:
GET /api/lms-base/learning-resources/doets - Xã/phường:
GET /api/lms-base/learning-resources/divisions
Lưu ý:
- Khuyến nghị lọc theo ngữ cảnh của đơn vị và theo
approvalStatusđể giảm dữ liệu trả về. - Partner API hiện chưa có endpoint tra cứu trạng thái duyệt theo từng
learningResourceIdriêng biệt; tra cứu chủ động vẫn dùng API danh sách. - Field trạng thái trong item response danh sách (xem chi tiết ở mục 7.1):
status: trạng thái hiện tại dạng chuỗi, gồmPending,Approved,Rejected,Hidden,Removed.reason: lý do theo trạng thái hiện tại (có khiRejected/Removed).occurredAt: thời điểm trạng thái hiện tại được ghi nhận (UTC).
7.6. Webhook đồng bộ trạng thái học liệu
Trục LMS hỗ trợ webhook để chủ động thông báo cho partner khi trạng thái học liệu thay đổi, giúp partner đồng bộ gần thời gian thực mà không phải polling liên tục.
7.6.1. Đăng ký webhook
Quy trình đăng ký (cấu hình ở phía Trục LMS theo từng đơn vị, không tự đăng ký qua API):
- Đơn vị cần tích hợp webhook cung cấp cho QIG endpoint nhận sự kiện (
WebhookUrl, khuyến nghị HTTPS). - QIG cấp lại cho đơn vị khóa bí mật webhook (
WebhookSecret) dùng để ký/xác minh HMAC. - QIG bật webhook cho đơn vị sau khi có đủ thông tin.
Webhook chỉ được gửi khi đơn vị đang bật webhook và đã cấu hình đủ WebhookUrl + WebhookSecret.
Đầu mối liên hệ tích hợp webhook:
| Đơn vị | Người phụ trách | Số điện thoại | |
|---|---|---|---|
| QIG | Nguyễn Minh Tâm | 0354898828 | tamnv@qig.vn |
7.6.2. Sự kiện và thời điểm gửi
eventType:learning-resource.status-changed.- Webhook được gửi khi trạng thái học liệu thay đổi sang một trong các giá trị sau:
status | Ý nghĩa | reason |
|---|---|---|
Approved | Học liệu đã được duyệt | null |
Rejected | Học liệu bị từ chối duyệt | Lý do từ chối |
Hidden | Học liệu bị tạm ẩn | null |
Removed | Học liệu bị gỡ bỏ | Lý do gỡ bỏ |
- Nếu trạng thái mới trùng trạng thái trước đó thì không gửi webhook.
- Trạng thái
Pendingkhông phát webhook. - Nhiều học liệu của cùng một partner đổi trạng thái trong cùng một đợt xử lý sẽ được gom chung vào một event (mảng
items).
7.6.3. Cấu trúc request gửi tới partner
- Method:
POSTtớiWebhookUrlcủa partner. - Content-Type:
application/json.
Headers:
| Header | Mô tả |
|---|---|
X-Webhook-Event-Id | Mã định danh duy nhất của event (ULID). Dùng để chống xử lý trùng (idempotency) |
X-Webhook-Timestamp | Thời điểm phát sinh event, ISO-8601 UTC (ví dụ 2026-05-25T03:15:20.1234567Z). Là phần tham gia ký chữ ký |
X-Webhook-Signature | Chữ ký HMAC-SHA256, định dạng sha256=<hex chữ thường> |
X-Webhook-Sender | Mã hệ thống gửi (mặc định base-lms) |
Body mẫu (JSON, key dạng camelCase):
{
"eventId": "01JW1234567890ABCDEFGHJKL",
"eventType": "learning-resource.status-changed",
"occurredAt": "2026-05-25T03:15:20.1234567Z",
"items": [
{ "sourceId": "LR-HN-000001", "status": "Approved", "reason": null },
{ "sourceId": "LR-HN-000002", "status": "Rejected", "reason": "Nội dung chưa đúng chuẩn chương trình." }
]
}Trường payload:
| Field | Kiểu dữ liệu | Mô tả |
|---|---|---|
eventId | string | Mã event (trùng header X-Webhook-Event-Id) |
eventType | string | Luôn là learning-resource.status-changed |
occurredAt | string (date-time) | Thời điểm UTC phát sinh event (trùng header X-Webhook-Timestamp) |
items | array<object> | Danh sách học liệu đổi trạng thái trong event |
items[].sourceId | string | Mã nguồn học liệu phía partner đã gửi khi chia sẻ |
items[].status | string | Approved | Rejected | Hidden | Removed |
items[].reason | string / null | Lý do thay đổi trạng thái (thường có khi Rejected/Removed) |
7.6.4. Xác minh chữ ký (bắt buộc)
Partner phải xác minh X-Webhook-Signature trước khi xử lý payload:
- Lấy
timestamp= giá trị headerX-Webhook-Timestamp. - Lấy
payload= nguyên văn body request (raw UTF-8, không serialize lại). - Tính
HMAC-SHA256với khóaWebhookSecrettrên chuỗi{timestamp}.{payload}. - Encode kết quả sang hex chữ thường và thêm tiền tố
sha256=. - So sánh an toàn (constant-time) với header
X-Webhook-Signature. Nếu khác → loại bỏ request.
Ví dụ tham khảo (Node.js):
const crypto = require("crypto");
function verifySignature(rawBody, timestamp, signatureHeader, webhookSecret) {
const expected =
"sha256=" +
crypto.createHmac("sha256", webhookSecret).update(`${timestamp}.${rawBody}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}7.6.5. Phản hồi và cơ chế retry
- Partner trả HTTP
2xxđể xác nhận đã nhận; nên phản hồi nhanh và xử lý nghiệp vụ bất đồng bộ. - Timeout mặc định mỗi lần gọi:
10giây. - Trục retry tối đa
3lần khi gặp lỗi tạm thời: HTTP408,429,5xx, lỗi mạng hoặc timeout. - Các mã
4xxkhác (trừ408/429) được coi là lỗi vĩnh viễn và không retry. - Vì có retry, partner phải chống trùng theo
eventId(xử lý idempotent).
7.6.6. Khuyến nghị cho partner
- Dùng HTTPS cho
WebhookUrl. - Lưu
eventIdđã xử lý để bỏ qua sự kiện trùng. - Vẫn nên có cron polling dự phòng (mục 7.5) để đối soát khi webhook lỗi kéo dài.
- Bảo mật
WebhookSecret; phối hợp đầu mối để xoay khóa khi cần.
8. Xóa học liệu pending
- Method:
DELETE - Path (scope trường):
/api/lms-base/learning-resources - Path (scope tổ chức):
/api/lms-base/learning-resources/organizations - Path (scope Sở):
/api/lms-base/learning-resources/doets - Path (scope xã/phường):
/api/lms-base/learning-resources/divisions - Content-Type:
application/json
8.1. Dữ liệu đầu vào (request)
Request body là mảng string[] chứa danh sách id học liệu (ULID) cần xóa.
| Field | Kiểu dữ liệu | Bắt buộc | Ràng buộc |
|---|---|---|---|
ids (request body) | array<string> | Có | Mỗi phần tử là ULID học liệu; mảng không rỗng; không trùng id trong cùng request; tối đa 200 id/lần |
8.2. Mô hình phản hồi (response model)
API trả về response envelope chuẩn:
success:true/falsetheo kết quả tổng thể của lô.message: thông điệp tổng hợp (ví dụ: số lượng thành công/thất bại).data: danh sách kết quả theo từng id đã gửi.
Chi tiết từng phần tử data[]:
| Field | Kiểu dữ liệu | Ý nghĩa |
|---|---|---|
sourceId | string | Mã học liệu nguồn tương ứng bản ghi được xử lý |
id | string (ULID) | Mã ULID học liệu đã xử lý |
deleted | bool | true: xóa thành công, false: không xóa được |
message | string | Lý do/diễn giải kết quả cho từng bản ghi |
processedAt | string (datetime UTC) | Thời điểm hệ thống xử lý bản ghi |
Lưu ý: API có thể trả 200 OK nhưng vẫn có item deleted = false (thành công một phần), cần xử lý theo từng phần tử data[].
8.3. Ví dụ request
Ví dụ request:
[
"01JQ2T5W11Z9J6AEX4Q6F90HME",
"01JQ2T5W11Z9J6AEX4Q6F90HMF"
]8.4. Ví dụ response (thành công một phần)
Ví dụ response:
{
"success": false,
"message": "Xử lý xóa 2 học liệu, xóa thành công 1, thất bại 1.",
"data": [
{
"sourceId": "LR-HN-000001",
"id": "01JQ2T5W11Z9J6AEX4Q6F90HME",
"deleted": true,
"message": "Xóa học liệu thành công.",
"processedAt": "2026-04-14T02:00:00.0000000Z"
},
{
"sourceId": "LR-HN-000002",
"id": "01JQ2T5W11Z9J6AEX4Q6F90HMF",
"deleted": false,
"message": "Chỉ được xóa học liệu ở trạng thái chờ duyệt.",
"processedAt": "2026-04-14T02:00:00.0000000Z"
}
]
}Ràng buộc xóa theo lô:
- Chỉ xóa được học liệu ở trạng thái
Pending (0). - Chỉ xóa được học liệu thuộc đúng đối tác gọi API (
X-Api-Key). - Tối đa
200id/lần. - Danh sách id không được rỗng và không được trùng trong cùng request.
- API có thể trả
200 OKnhưng có bản ghi xóa thất bại; luôn kiểm tradata[].deleted.
9. Các rule validate dễ sai
- API tạo/chia sẻ học liệu chạy theo cơ chế upsert theo
sourceId:/learning-resources: key làschoolCode + sourceIdtrong phạm vi partner gọi API (X-Api-Key)./learning-resources/organizations: key làpartnerId + sourceId./learning-resources/doets: key làdoetCode + sourceIdtrong phạm vi partner gọi API (X-Api-Key)./learning-resources/divisions: key làdoetCode + divisionCode + sourceIdtrong phạm vi partner gọi API (X-Api-Key).- Nếu bản ghi hiện tại đã
Approvedthì không được cập nhật đè.
authorPinCode/creatorPinCodeở endpoint/learning-resourcesphải là CCCD 12 số tồn tại trong hệ thống Sở theo đúng ngữ cảnh dữ liệu gửi lên.gradeCode/subjectCode/knowledgeUnitCodekhông dùng mã tự đặt; phải map từ danh mục dùng chung.sourceId,title,authorName,creatorName,contents[].namechỉ chấp nhận văn bản thuần, không chứa HTML.descriptionhọc liệu (nếu truyền) cho phép văn bản hoặc HTML an toàn, tối đa1024ký tự.- Khi tạo mới/cập nhật hợp lệ, hệ thống luôn gán trạng thái mặc định:
ApprovalStatus = Pending (0)vàModerationStatus = Normal. thumbnailUrllà bắt buộc và phải là URL tuyệt đối hợp lệ. Có thể dùng path ảnh đã upload hoặc URL ảnh ngoài đúng định dạng ảnh hỗ trợ.contentslà bắt buộc và phải có ít nhất1phần tử.SCORMphải là URL tới.zipđã upload, đúng MIMEapplication/zip, và đúng chuẩn SCORM hỗ trợ.YOUTUBEphải là liên kết YouTube tuyệt đối hợp lệ (không truyền iframe HTML).IFRAMEphải là URL tuyệt đối (http/https) có domain/subdomain thuộc danh sách cho phép (mặc địnhyoutube.com,olm.vn,sgk.olm.vn); không truyền thẻ<iframe>HTML.fileIdsnếu truyền thì tất cả phải thuộc partner và phải được dùng thực tế trongthumbnailUrlhoặccontents.- URL trong
TEXTHTML (src/href/poster) bắt buộc là URL tuyệt đối hợp lệ (http/https). - HTML không được chứa nội dung không an toàn (CSS/JS/iframe/object/embed/on*/protocol cấm).
semesterCodechỉ nhận1hoặc2;yearCodephải là năm 4 chữ số hợp lệ và tồn tại trong danh mục năm học hệ thống.- API danh sách:
- Nhà trường: bắt buộc
doetCode. - Tổ chức: không yêu cầu
doetCode. - Sở: bắt buộc
doetCode, không cho truyềndivisionCode/schoolCode. - Xã/phường: bắt buộc
doetCode+divisionCode, không cho truyềnschoolCode. - Tất cả endpoint danh sách đều yêu cầu
taketrong khoảng1..200vàskip >= 0.
- Nhà trường: bắt buộc
10. Các lỗi thường gặp và cách xử lý
10.1. Theo HTTP status
| HTTP status | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
401 Unauthorized | Thiếu/sai X-Api-Key | Kiểm tra key và header |
400 Bad Request | Sai format dữ liệu, ULID, mã danh mục, rule mapping nội dung/file | Đối chiếu payload theo checklist và rule validate |
404 Not Found | Không tìm thấy attachmentId | Kiểm tra lại id file và ngữ cảnh partner |
200 OK (API xóa lô) | Có thể thành công một phần | Luôn đọc data[].deleted và data[].message |
10.2. Theo message nghiệp vụ thường gặp
| Message lỗi | Nguyên nhân | Cách xử lý |
|---|---|---|
Mã sở giáo dục là bắt buộc. | Gọi API danh sách nhà trường/Sở/xã-phường nhưng thiếu doetCode | Bổ sung doetCode khi gọi GET /learning-resources, GET /learning-resources/doets hoặc GET /learning-resources/divisions; scope tổ chức dùng endpoint riêng và không yêu cầu trường này |
Không tìm thấy file cho các FileId: ... | fileIds không tồn tại hoặc sai id | Kiểm tra lại id upload trả về |
FileId ... không thuộc đối tác gọi API. | Dùng file của partner khác | Chỉ dùng attachment thuộc cùng X-Api-Key |
FileId ... đã được dùng bởi đối tượng khác. | File đã bị gán owner khác | Upload file mới hoặc dùng đúng file đang thuộc học liệu cần thao tác |
Các FileId đã upload nhưng chưa được replace vào nội dung: ... | Truyền fileIds nhưng chưa dùng trong thumbnailUrl/contents | Replace đúng URL trước khi gọi tạo học liệu |
Đường dẫn ảnh đại diện là bắt buộc. | Thiếu thumbnailUrl | Bổ sung thumbnailUrl hợp lệ trước khi gọi API |
Đường dẫn ảnh đại diện phải là URL tuyệt đối. | thumbnailUrl không phải URL tuyệt đối | Chuẩn hóa thành URL tuyệt đối trước khi gửi |
Đường dẫn ảnh đại diện phải là URL tuyệt đối hợp lệ. | thumbnailUrl không phải URL tuyệt đối http/https | Chuẩn hóa thành URL tuyệt đối hợp lệ |
Đường dẫn ảnh đại diện (URL ngoài) phải có định dạng ảnh hợp lệ (.jpg, .jpeg, .png, .gif, .bmp, .webp). | thumbnailUrl là URL ngoài nhưng không đúng định dạng ảnh hỗ trợ | Dùng URL ảnh ngoài đúng định dạng hỗ trợ hoặc dùng ảnh đã upload lên trục |
Mã học liệu nguồn chỉ chấp nhận văn bản thuần, không chứa HTML. | Truyền HTML vào sourceId | Chỉ gửi văn bản thuần cho mã nguồn học liệu |
Tên học liệu chỉ chấp nhận văn bản thuần, không chứa HTML. | Truyền HTML vào title | Chỉ gửi văn bản thuần cho trường metadata |
Tên tác giả chỉ chấp nhận văn bản thuần, không chứa HTML. | Truyền HTML vào authorName hoặc creatorName | Chỉ gửi văn bản thuần cho tên tác giả/người chia sẻ |
Tên nội dung học liệu chỉ chấp nhận văn bản thuần, không chứa HTML. | Truyền HTML vào contents[].name | Chỉ gửi văn bản thuần cho tên nội dung |
Học liệu phải có ít nhất 1 nội dung. | Mảng contents rỗng | Bổ sung ít nhất 1 phần tử nội dung hợp lệ |
SCORM content phải replace bằng path file package .zip đã upload lên trục. | SCORM không trỏ vào file .zip đã upload | Upload .zip đúng chuẩn rồi replace URL |
Content với type YOUTUBE chỉ chấp nhận dạng https://youtube.com/watch?v=... (bao gồm www.youtube.com/watch?v=...) hoặc https://youtu.be/... | YOUTUBE không đúng 1 trong 2 pattern hỗ trợ hoặc đang gửi iframe | Chỉ gửi URL tuyệt đối đúng pattern, không gửi HTML iframe |
Content với type IFRAME chỉ chấp nhận URL thuộc các domain được phép: ... | IFRAME trỏ tới domain không nằm trong danh sách cho phép, không phải URL tuyệt đối http/https, hoặc đang gửi thẻ <iframe> | Chỉ gửi URL tuyệt đối thuộc domain/subdomain được phép (mặc định youtube.com, olm.vn, sgk.olm.vn) |
Content type '...' không phù hợp định dạng file '...'. | type không khớp MIME file thực tế | Chọn type đúng với MIME đã upload |
Content không được chứa CSS/TypeScript/JavaScript, iframe hoặc mã không an toàn. | HTML vi phạm rule an toàn | Loại bỏ thành phần cấm trước khi gửi |
Học liệu SourceId '...' đã được duyệt. Không cho phép cập nhật, vui lòng tạo học liệu mới để chia sẻ. | Upsert gặp bản ghi cùng key đang ở trạng thái Approved | Dùng sourceId mới để tạo bản ghi chia sẻ mới |
Không tìm thấy học liệu để cập nhật. | Bản ghi upsert vừa đổi trạng thái hoặc không còn tồn tại tại thời điểm cập nhật | Gọi lại API với payload mới nhất |
Học liệu SourceId '...' đã được duyệt hoặc không còn ở trạng thái cho phép cập nhật. | Bản ghi không còn ở trạng thái Pending/Rejected khi xử lý cập nhật | Tạo bản ghi mới với sourceId khác |
Không thể tải tệp lên hệ thống. | File vi phạm validate extension/size/MIME hoặc lỗi upload storage | Kiểm tra extension, kích thước, MIME thực tế và thử upload lại |
11. FAQ cho partner
1) Có bắt buộc truyền fileIds không?
- Không bắt buộc.
- Nhưng nếu truyền thì tất cả id phải hợp lệ, thuộc partner, và được dùng thực tế trong
thumbnailUrlhoặccontents.
2) authorPinCode và creatorPinCode có thể giống nhau không?
- Có thể giống hoặc khác.
- Với endpoint
/learning-resources, cả hai phải là CCCD 12 số hợp lệ và tồn tại trong ngữ cảnh dữ liệu Sở. - Với endpoint
/learning-resources/organizations, có thể để trống để hệ thống tự điền mặc định theo thông tin tổ chức. - Với endpoint
/learning-resources/doets, hệ thống tự gán theo mã/tên Sở. - Với endpoint
/learning-resources/divisions, hệ thống tự gán theo mã/tên xã-phường.
3) Có thể gửi TEXT với HTML không?
- Có.
- Nhưng URL trong
src/href/posterphải là URL tuyệt đối và nội dung phải qua rule an toàn HTML.
4) Có webhook/callback khi duyệt học liệu không?
- Có. Trục LMS chủ động gửi webhook
learning-resource.status-changedtớiWebhookUrlcủa partner khi học liệu đổi trạng thái sangApproved,Rejected,HiddenhoặcRemoved. Chi tiết payload, headers, xác minh chữ ký và retry ở mục 7.6. - Webhook cần được đầu mối cấp
X-Api-Keycấu hình trước (WebhookUrl+WebhookSecret+ bật webhook). - Có thể kết hợp polling API danh sách theo scope để đối soát/dự phòng khi webhook lỗi kéo dài:
- Nhà trường:
GET /learning-resources - Tổ chức:
GET /learning-resources/organizations - Sở:
GET /learning-resources/doets - Xã/phường:
GET /learning-resources/divisions
- Nhà trường:
5) Khi nào được xóa học liệu?
- Chỉ xóa được học liệu ở trạng thái
Pending (0)và thuộc đúng partner.
6) Tôi bị lỗi trùng sourceId, xử lý thế nào?
- Nếu bản ghi cùng key đang
Approved, cần dùngsourceIdmới để chia sẻ bản ghi mới. - Nếu bản ghi cùng key đang
PendinghoặcRejected, API sẽ cập nhật bản ghi cũ và trảoperation = updated. - Khuyến nghị lưu mapping
sourceIdở hệ thống nguồn để kiểm soát luồng upsert.
11.1. Quy ước vận hành đã chốt
- Scope upsert của
sourceId:/learning-resources:schoolCode + sourceIdtrong phạm viX-Api-Key/learning-resources/organizations:partnerId + sourceId/learning-resources/doets:doetCode + sourceIdtrong phạm viX-Api-Key/learning-resources/divisions:doetCode + divisionCode + sourceIdtrong phạm viX-Api-Key
- Cơ chế dọn file upload không được mapping: hệ thống trục có job tự động quét dọn.
- Trạng thái duyệt: ưu tiên webhook
learning-resource.status-changed(mục 7.6); polling API danh sách dùng để đối soát/dự phòng.
