Update Saqel Platform: 2026-09-02 07:52:23
This commit is contained in:
@@ -267,8 +267,8 @@ class CurriculumController
|
||||
$manifestLesson = CurriculumService::findLessonByFile($file);
|
||||
$title = $manifestLesson['title'] ?? basename($file, '.md');
|
||||
$existingLesson = \App\Core\Database::selectOne(
|
||||
"SELECT id, course_id FROM lessons WHERE title = ? OR markdown_content LIKE ? LIMIT 1",
|
||||
[$title, '%' . $file . '%']
|
||||
"SELECT id, course_id FROM lessons WHERE curriculum_key = ? OR title = ? OR markdown_content LIKE ? LIMIT 1",
|
||||
[$file, $title, '%' . $file . '%']
|
||||
);
|
||||
$courseId = $existingLesson
|
||||
? (int)$existingLesson['course_id']
|
||||
@@ -283,14 +283,14 @@ class CurriculumController
|
||||
if ($existingLesson) {
|
||||
$lessonId = (int)$existingLesson['id'];
|
||||
\App\Core\Database::query(
|
||||
"UPDATE lessons SET storage_type = 'api_upload', video_uuid = ?, bunny_video_id = '', local_path = ?, hls_url = ?, thumbnail_url = ?, duration_seconds = ?, encoding_status = 'ready', ai_video_url = ? WHERE id = ?",
|
||||
[$uploadResult['video_uuid'], $uploadResult['local_path'], $uploadResult['hls_url'], $uploadResult['thumbnail_url'], $uploadResult['duration'] ?? 0, $videoUrl, $lessonId]
|
||||
"UPDATE lessons SET curriculum_key = ?, storage_type = 'api_upload', video_uuid = ?, bunny_video_id = '', local_path = ?, hls_url = ?, thumbnail_url = ?, duration_seconds = ?, encoding_status = 'ready', ai_video_url = ? WHERE id = ?",
|
||||
[$file, $uploadResult['video_uuid'], $uploadResult['local_path'], $uploadResult['hls_url'], $uploadResult['thumbnail_url'], $uploadResult['duration'] ?? 0, $videoUrl, $lessonId]
|
||||
);
|
||||
} else {
|
||||
$lessonId = \App\Core\Database::insert(
|
||||
"INSERT INTO lessons (course_id, title, sequence_order, storage_type, video_uuid, bunny_video_id, local_path, hls_url, thumbnail_url, duration_seconds, is_free_preview, encoding_status, ai_video_url)
|
||||
VALUES (?, ?, 1, 'api_upload', ?, '', ?, ?, ?, ?, 0, 'ready', ?)",
|
||||
[$courseId, $title, $uploadResult['video_uuid'], $uploadResult['local_path'], $uploadResult['hls_url'], $uploadResult['thumbnail_url'], $uploadResult['duration'] ?? 0, $videoUrl]
|
||||
"INSERT INTO lessons (course_id, title, curriculum_key, sequence_order, storage_type, video_uuid, bunny_video_id, local_path, hls_url, thumbnail_url, duration_seconds, is_free_preview, encoding_status, ai_video_url)
|
||||
VALUES (?, ?, ?, 1, 'api_upload', ?, '', ?, ?, ?, ?, 0, 'ready', ?)",
|
||||
[$courseId, $title, $file, $uploadResult['video_uuid'], $uploadResult['local_path'], $uploadResult['hls_url'], $uploadResult['thumbnail_url'], $uploadResult['duration'] ?? 0, $videoUrl]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -385,13 +385,16 @@ class VideoController
|
||||
VideoService::ensureSchema();
|
||||
CurriculumService::ensureSchema();
|
||||
|
||||
$rawId = $request->getParam('id') ?? '';
|
||||
$curriculumKey = trim((string)($request->getQuery('curriculum_key') ?? ''));
|
||||
$rawId = $curriculumKey !== '' ? $curriculumKey : ($request->getParam('id') ?? '');
|
||||
$lesson = null;
|
||||
|
||||
if (is_numeric($rawId) && (int)$rawId > 0) {
|
||||
if ($curriculumKey !== '') {
|
||||
$lesson = Database::selectOne("SELECT * FROM lessons WHERE curriculum_key = ? LIMIT 1", [$curriculumKey]);
|
||||
} elseif (is_numeric($rawId) && (int)$rawId > 0) {
|
||||
$lesson = Database::selectOne("SELECT * FROM lessons WHERE id = ? LIMIT 1", [(int)$rawId]);
|
||||
} elseif (!empty($rawId)) {
|
||||
$lesson = Database::selectOne("SELECT * FROM lessons WHERE title LIKE ? OR local_path LIKE ? OR markdown_content LIKE ? LIMIT 1", ["%{$rawId}%", "%{$rawId}%", "%{$rawId}%"]);
|
||||
$lesson = Database::selectOne("SELECT * FROM lessons WHERE curriculum_key = ? OR title LIKE ? OR local_path LIKE ? OR markdown_content LIKE ? LIMIT 1", [$rawId, "%{$rawId}%", "%{$rawId}%", "%{$rawId}%"]);
|
||||
// Flutter curriculum lessons use the manifest slug (e.g. u1_l1_*),
|
||||
// while the database stores the canonical lesson title.
|
||||
if (!$lesson) {
|
||||
|
||||
@@ -37,6 +37,12 @@ class VideoService
|
||||
Database::query("ALTER TABLE lessons ADD COLUMN video_uuid CHAR(36) NULL AFTER storage_type");
|
||||
}
|
||||
|
||||
$colsCurriculumKey = Database::select("SHOW COLUMNS FROM lessons LIKE 'curriculum_key'");
|
||||
if (empty($colsCurriculumKey)) {
|
||||
Database::query("ALTER TABLE lessons ADD COLUMN curriculum_key VARCHAR(500) NULL AFTER title");
|
||||
Database::query("ALTER TABLE lessons ADD INDEX idx_lessons_curriculum_key (curriculum_key)");
|
||||
}
|
||||
|
||||
$colsPath = Database::select("SHOW COLUMNS FROM lessons LIKE 'local_path'");
|
||||
if (empty($colsPath)) {
|
||||
Database::query("ALTER TABLE lessons ADD COLUMN local_path VARCHAR(500) NULL AFTER bunny_video_id");
|
||||
|
||||
@@ -222,6 +222,7 @@ CREATE TABLE IF NOT EXISTS `lessons` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`course_id` BIGINT UNSIGNED NOT NULL,
|
||||
`title` VARCHAR(255) NOT NULL,
|
||||
`curriculum_key` VARCHAR(500) DEFAULT NULL COMMENT 'المسار الفريد لدرس الـManifest داخل storage/curriculum',
|
||||
`sequence_order` INT UNSIGNED NOT NULL DEFAULT 1,
|
||||
|
||||
-- المعلم الحقيقي (Teacher Video)
|
||||
@@ -249,6 +250,7 @@ CREATE TABLE IF NOT EXISTS `lessons` (
|
||||
`updated_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_lessons_course` (`course_id`),
|
||||
KEY `idx_lessons_curriculum_key` (`curriculum_key`),
|
||||
KEY `idx_lessons_video_uuid` (`video_uuid`),
|
||||
CONSTRAINT `fk_lessons_course` FOREIGN KEY (`course_id`) REFERENCES `courses` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
@@ -114,6 +114,7 @@ $router->post('/api/teacher/lessons/checkpoints', [\App\Controllers\VideoCon
|
||||
$router->get('/api/student/lessons', [\App\Controllers\VideoController::class, 'getStudentLessons']);
|
||||
$router->get('/api/videos/stream/{uuid}', [\App\Controllers\VideoController::class, 'streamLocalVideo']);
|
||||
$router->get('/api/videos/hls/{uuid}/{file}', [\App\Controllers\VideoController::class, 'streamHls']);
|
||||
$router->get('/api/lessons/playback', [\App\Controllers\VideoController::class, 'getPlaybackData']);
|
||||
$router->get('/api/lessons/{id}/playback', [\App\Controllers\VideoController::class, 'getPlaybackData']);
|
||||
|
||||
// Student & Teacher Chat Routes (API-Driven, Authenticated)
|
||||
|
||||
@@ -9,6 +9,10 @@ use App\Services\CurriculumService;
|
||||
|
||||
echo "=== SAQEL ENTERPRISE CURRICULUM DATABASE SYNC ===\n";
|
||||
|
||||
// Keep an existing production database compatible with the canonical
|
||||
// curriculum-key linkage before querying or inserting synchronized lessons.
|
||||
\App\Services\VideoService::ensureSchema();
|
||||
|
||||
$manifestFile = __DIR__ . '/../storage/curriculum/manifest.json';
|
||||
if (!file_exists($manifestFile)) {
|
||||
die("Error: manifest.json not found at $manifestFile\n");
|
||||
@@ -99,24 +103,24 @@ foreach ($manifest as $gradeKey => $grade) {
|
||||
$cheatSheet = $les['cheat_sheet'] ?? null;
|
||||
$socraticQuiz = isset($les['socratic_quiz']) ? json_encode($les['socratic_quiz'], JSON_UNESCAPED_UNICODE) : null;
|
||||
|
||||
// Match existing lesson by course_id and title or file path
|
||||
// The manifest file path is the canonical, globally unique lesson key.
|
||||
$dbLesson = Database::selectOne(
|
||||
"SELECT id FROM lessons WHERE course_id = ? AND title = ? LIMIT 1",
|
||||
[$courseId, $lessonTitle]
|
||||
"SELECT id FROM lessons WHERE curriculum_key = ? OR (course_id = ? AND title = ?) LIMIT 1",
|
||||
[$filePath, $courseId, $lessonTitle]
|
||||
);
|
||||
|
||||
if (!$dbLesson) {
|
||||
$lesId = Database::insert(
|
||||
"INSERT INTO lessons (course_id, title, sequence_order, ai_video_url, markdown_content, cheat_sheet_markdown, socratic_quiz_json, is_free_preview)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 1)",
|
||||
[$courseId, $lessonTitle, $seqOrder, $aiVideoUrl, $mdContent, $cheatSheet, $socraticQuiz]
|
||||
"INSERT INTO lessons (course_id, title, curriculum_key, sequence_order, ai_video_url, markdown_content, cheat_sheet_markdown, socratic_quiz_json, is_free_preview)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1)",
|
||||
[$courseId, $lessonTitle, $filePath, $seqOrder, $aiVideoUrl, $mdContent, $cheatSheet, $socraticQuiz]
|
||||
);
|
||||
echo " 📄 [NEW] Lesson inserted: {$lessonTitle} (ID: {$lesId})\n";
|
||||
} else {
|
||||
$lesId = (int)$dbLesson['id'];
|
||||
Database::query(
|
||||
"UPDATE lessons SET sequence_order = ?, markdown_content = ?, ai_video_url = COALESCE(?, ai_video_url), cheat_sheet_markdown = COALESCE(?, cheat_sheet_markdown), socratic_quiz_json = COALESCE(?, socratic_quiz_json) WHERE id = ?",
|
||||
[$seqOrder, $mdContent, $aiVideoUrl, $cheatSheet, $socraticQuiz, $lesId]
|
||||
"UPDATE lessons SET curriculum_key = ?, sequence_order = ?, markdown_content = ?, ai_video_url = COALESCE(?, ai_video_url), cheat_sheet_markdown = COALESCE(?, cheat_sheet_markdown), socratic_quiz_json = COALESCE(?, socratic_quiz_json) WHERE id = ?",
|
||||
[$filePath, $seqOrder, $mdContent, $aiVideoUrl, $cheatSheet, $socraticQuiz, $lesId]
|
||||
);
|
||||
echo " 🔄 [UPDATED] Lesson: {$lessonTitle} (ID: {$lesId})\n";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user