Files
saqel/backend/app/Services/TeacherSubmissionService.php
T

60 lines
7.2 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services;
use App\Core\Database;
final class TeacherSubmissionService {
public static function preflight(int $teacherId, string $lessonUuid, string $key, bool $replacement, ?string $submissionUuid = null): array {
if (!self::uuidValid($lessonUuid) || !preg_match('/^[A-Za-z0-9._:-]{16,128}$/', $key)) return self::result(400, 'error', 'بيانات هوية الدرس أو مفتاح الإعادة غير صالحة.');
$operation = $replacement ? 'replacement_preflight' : 'submission_preflight';
$hash = hash('sha256', json_encode([$lessonUuid, $replacement, $submissionUuid]));
$pdo = Database::getConnection(); $pdo->beginTransaction();
try {
$previous = Database::selectOne('SELECT request_sha256,response_json,http_status FROM submission_idempotency_keys WHERE teacher_id=? AND operation=? AND idempotency_key=? FOR UPDATE', [$teacherId,$operation,$key]);
if ($previous) {
if (!hash_equals((string)$previous['request_sha256'], $hash)) { $pdo->rollBack(); return self::result(409,'idempotency_conflict','استُخدم مفتاح الإعادة مع طلب مختلف.'); }
$pdo->commit(); return array_merge(json_decode((string)$previous['response_json'], true) ?: [], ['http_status'=>(int)$previous['http_status'],'replayed'=>true]);
}
$lesson = Database::selectOne("SELECT id,uuid,title FROM curriculum_lessons WHERE uuid=? AND source_status='approved' LIMIT 1 FOR UPDATE", [$lessonUuid]);
if (!$lesson) return self::commit($pdo,$teacherId,$operation,$key,$hash,self::result(404,'error','الدرس المنهجي غير معتمد أو غير موجود.'));
$submission = Database::selectOne('SELECT * FROM teacher_submissions WHERE teacher_id=? AND curriculum_lesson_id=? FOR UPDATE',[$teacherId,$lesson['id']]);
if ($submission && !$replacement) return self::commit($pdo,$teacherId,$operation,$key,$hash,['http_status'=>409,'status'=>'existing_submission','message'=>'لديك حصة مسجلة لهذا الدرس. استخدم الاستبدال لإنشاء نسخة مرشحة.','submission_id'=>$submission['uuid'],'current_published_video_version_id'=>$submission['current_published_video_version_id'] ?: null]);
if (!$submission && $replacement) return self::commit($pdo,$teacherId,$operation,$key,$hash,self::result(409,'replacement_target_missing','لا توجد حصة حالية لاستبدالها.'));
if ($submissionUuid && (!$submission || !hash_equals((string)$submission['uuid'],$submissionUuid))) return self::commit($pdo,$teacherId,$operation,$key,$hash,self::result(409,'replacement_target_mismatch','هدف الاستبدال لا يطابق حصة المعلم الحالية.'));
if (!$submission) { Database::insert("INSERT INTO teacher_submissions (uuid,teacher_id,curriculum_lesson_id,status) VALUES (?,?,?,'draft')",[self::uuid(),$teacherId,$lesson['id']]); $submission=Database::selectOne('SELECT * FROM teacher_submissions WHERE teacher_id=? AND curriculum_lesson_id=? FOR UPDATE',[$teacherId,$lesson['id']]); }
$next=(int)(Database::selectOne('SELECT COALESCE(MAX(version_number),0) n FROM video_versions WHERE teacher_submission_id=? FOR UPDATE',[$submission['id']])['n'] ?? 0)+1;
Database::insert("INSERT INTO video_versions (uuid,teacher_submission_id,version_number,status,replaces_video_version_id) VALUES (?,?,?,'draft',?)",[self::uuid(),$submission['id'],$next,$submission['current_published_video_version_id'] ?: null]);
$version=Database::selectOne('SELECT uuid FROM video_versions WHERE teacher_submission_id=? AND version_number=?',[$submission['id'],$next]);
return self::commit($pdo,$teacherId,$operation,$key,$hash,['http_status'=>201,'status'=>'ready_for_upload','submission_id'=>$submission['uuid'],'video_version_id'=>$version['uuid'],'version_number'=>$next,'replacement'=>$replacement,'lesson'=>['id'=>$lesson['uuid'],'title'=>$lesson['title']]]);
} catch (\Throwable $e) { if($pdo->inTransaction())$pdo->rollBack(); throw $e; }
}
public static function attachUploadedLesson(int $teacherId,string $versionUuid,int $lessonId,string $sha): void {
if(!self::uuidValid($versionUuid)||!preg_match('/^[0-9a-f]{64}$/i',$sha)) throw new \RuntimeException('هوية النسخة أو بصمة الملف غير صالحة.');
$pdo=Database::getConnection();$pdo->beginTransaction();try { $v=Database::selectOne("SELECT vv.id,vv.status,ts.id submission_id FROM video_versions vv JOIN teacher_submissions ts ON ts.id=vv.teacher_submission_id WHERE vv.uuid=? AND ts.teacher_id=? FOR UPDATE",[$versionUuid,$teacherId]); if(!$v||!in_array($v['status'],['draft','uploading'],true)) throw new \RuntimeException('نسخة الفيديو غير متاحة لهذا الرفع.'); Database::query("UPDATE video_versions SET source_lesson_id=?,content_sha256=?,status='review' WHERE id=?",[$lessonId,$sha,$v['id']]);Database::query("UPDATE teacher_submissions SET status='review' WHERE id=?",[$v['submission_id']]);$pdo->commit(); VideoReviewService::queueForVersion((int)$v['id']); } catch(\Throwable $e){if($pdo->inTransaction())$pdo->rollBack();throw $e;}
}
public static function reserveUpload(int $teacherId, string $versionUuid): bool {
if (!self::uuidValid($versionUuid)) return false;
return Database::execute(
"UPDATE video_versions vv JOIN teacher_submissions ts ON ts.id=vv.teacher_submission_id
SET vv.status='uploading', ts.status='uploading'
WHERE vv.uuid=? AND ts.teacher_id=? AND vv.status='draft'",
[$versionUuid, $teacherId]
) === 1;
}
/** Releases a reservation only when no media was accepted for review. */
public static function releaseUploadReservation(int $teacherId, string $versionUuid): void {
if (!self::uuidValid($versionUuid)) return;
Database::execute(
"UPDATE video_versions vv JOIN teacher_submissions ts ON ts.id=vv.teacher_submission_id
SET vv.status='draft',
ts.status=CASE WHEN ts.current_published_video_version_id IS NULL THEN 'draft' ELSE 'published' END
WHERE vv.uuid=? AND ts.teacher_id=? AND vv.status='uploading'",
[$versionUuid, $teacherId]
);
}
private static function commit(\PDO $pdo,int $teacher,string $op,string $key,string $hash,array $r):array {Database::insert('INSERT INTO submission_idempotency_keys (teacher_id,operation,idempotency_key,request_sha256,response_json,http_status) VALUES (?,?,?,?,?,?)',[$teacher,$op,$key,$hash,json_encode($r,JSON_UNESCAPED_UNICODE),$r['http_status']]);$pdo->commit();return $r;}
private static function result(int $status,string $code,string $message):array{return ['http_status'=>$status,'status'=>$code,'message'=>$message];}
private static function uuidValid(string $v):bool{return(bool)preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i',$v);}
private static function uuid():string{$b=random_bytes(16);$b[6]=chr((ord($b[6])&15)|64);$b[8]=chr((ord($b[8])&63)|128);return vsprintf('%s%s-%s-%s-%s-%s%s%s',str_split(bin2hex($b),4));}
}