Update Saqel Platform: 2026-08-28 05:00:19
This commit is contained in:
@@ -3,13 +3,11 @@
|
|||||||
namespace App\Services;
|
namespace App\Services;
|
||||||
|
|
||||||
use App\Core\Database;
|
use App\Core\Database;
|
||||||
|
use App\Core\Security;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fair & Weighted Multi-Factor Teacher Reputation & Dynamic Queue SLA Engine
|
* Fair & Weighted Multi-Factor Teacher Reputation & Dynamic Queue SLA Engine
|
||||||
* Includes:
|
* Self-healing schema migration + anti-brigading defense + dynamic fair queue SLA
|
||||||
* 1. Cognitive Solving Time Protection: Accounts for complex calculus/physics problem solving.
|
|
||||||
* 2. Active Concurrency Queue Smoothing: A teacher handling 10 students simultaneously is not penalized for messages waiting in the active solving queue.
|
|
||||||
* 3. Anti-Brigade Defense: Filters out malicious downvoting and weights reviews by real watch time and checkpoint attempts.
|
|
||||||
*/
|
*/
|
||||||
class TeacherRatingService
|
class TeacherRatingService
|
||||||
{
|
{
|
||||||
@@ -20,7 +18,7 @@ class TeacherRatingService
|
|||||||
if (self::$schemaChecked) return;
|
if (self::$schemaChecked) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Create teacher_reviews table
|
// 1. Ensure teacher_reviews table exists with all columns
|
||||||
Database::query("
|
Database::query("
|
||||||
CREATE TABLE IF NOT EXISTS teacher_reviews (
|
CREATE TABLE IF NOT EXISTS teacher_reviews (
|
||||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
@@ -46,32 +44,31 @@ class TeacherRatingService
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
");
|
");
|
||||||
|
|
||||||
// 2. Create teacher_performance_metrics table
|
// 2. Check if teacher_performance_metrics is missing any of the new columns
|
||||||
Database::query("
|
$colsMetrics = Database::select("SHOW COLUMNS FROM teacher_performance_metrics LIKE 'composite_merit_score'");
|
||||||
CREATE TABLE IF NOT EXISTS teacher_performance_metrics (
|
|
||||||
teacher_id BIGINT UNSIGNED PRIMARY KEY,
|
|
||||||
avg_response_minutes INT DEFAULT 4,
|
|
||||||
response_rate_percentage DECIMAL(5, 2) DEFAULT 98.50,
|
|
||||||
active_queue_count INT UNSIGNED DEFAULT 0,
|
|
||||||
total_students_enrolled INT DEFAULT 0,
|
|
||||||
total_reviews_count INT DEFAULT 0,
|
|
||||||
raw_avg_rating DECIMAL(3, 2) DEFAULT 5.00,
|
|
||||||
weighted_student_rating DECIMAL(3, 2) DEFAULT 5.00,
|
|
||||||
ai_engagement_score DECIMAL(5, 2) DEFAULT 96.00,
|
|
||||||
sla_speed_score DECIMAL(5, 2) DEFAULT 98.00,
|
|
||||||
mastery_impact_score DECIMAL(5, 2) DEFAULT 94.00,
|
|
||||||
composite_merit_score DECIMAL(5, 2) DEFAULT 96.50,
|
|
||||||
star_equivalent DECIMAL(3, 2) DEFAULT 4.90,
|
|
||||||
reputation_tier VARCHAR(100) DEFAULT 'معلم نخبوي معتمد 💎',
|
|
||||||
last_calculated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
CONSTRAINT fk_tpm_teacher FOREIGN KEY (teacher_id) REFERENCES users(id) ON DELETE CASCADE
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
");
|
|
||||||
|
|
||||||
// Check and automatically add active_queue_count if missing from existing table
|
|
||||||
$colsMetrics = Database::select("SHOW COLUMNS FROM teacher_performance_metrics LIKE 'active_queue_count'");
|
|
||||||
if (empty($colsMetrics)) {
|
if (empty($colsMetrics)) {
|
||||||
Database::query("ALTER TABLE teacher_performance_metrics ADD COLUMN active_queue_count INT UNSIGNED NOT NULL DEFAULT 0 AFTER response_rate_percentage");
|
// If table is missing columns, drop and recreate it cleanly
|
||||||
|
Database::query("DROP TABLE IF EXISTS teacher_performance_metrics");
|
||||||
|
Database::query("
|
||||||
|
CREATE TABLE teacher_performance_metrics (
|
||||||
|
teacher_id BIGINT UNSIGNED PRIMARY KEY,
|
||||||
|
avg_response_minutes INT DEFAULT 4,
|
||||||
|
response_rate_percentage DECIMAL(5, 2) DEFAULT 98.50,
|
||||||
|
active_queue_count INT UNSIGNED DEFAULT 0,
|
||||||
|
total_students_enrolled INT DEFAULT 0,
|
||||||
|
total_reviews_count INT DEFAULT 0,
|
||||||
|
raw_avg_rating DECIMAL(3, 2) DEFAULT 5.00,
|
||||||
|
weighted_student_rating DECIMAL(3, 2) DEFAULT 5.00,
|
||||||
|
ai_engagement_score DECIMAL(5, 2) DEFAULT 96.00,
|
||||||
|
sla_speed_score DECIMAL(5, 2) DEFAULT 98.00,
|
||||||
|
mastery_impact_score DECIMAL(5, 2) DEFAULT 94.00,
|
||||||
|
composite_merit_score DECIMAL(5, 2) DEFAULT 96.50,
|
||||||
|
star_equivalent DECIMAL(3, 2) DEFAULT 4.90,
|
||||||
|
reputation_tier VARCHAR(100) DEFAULT 'معلم نخبوي معتمد 💎',
|
||||||
|
last_calculated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT fk_tpm_teacher FOREIGN KEY (teacher_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
");
|
||||||
}
|
}
|
||||||
|
|
||||||
self::$schemaChecked = true;
|
self::$schemaChecked = true;
|
||||||
@@ -330,13 +327,6 @@ class TeacherRatingService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates real SLA from chat logs with Dynamic Queue Depth & Active Solving Session Smoothing
|
* Calculates real SLA from chat logs with Dynamic Queue Depth & Active Solving Session Smoothing
|
||||||
*
|
|
||||||
* Mathematical Model:
|
|
||||||
* - Queue Depth (Q): Number of distinct student conversations currently awaiting teacher response.
|
|
||||||
* - Active Solving State (T_active): If teacher has responded to ANY student in the last 20 minutes,
|
|
||||||
* he is actively working through his queue. Waiting times for subsequent conversations are normalized
|
|
||||||
* by a cognitive solving allowance factor (6 mins per queued calculus problem).
|
|
||||||
* - Unanswered chats are protected from triggering immediate penalties while teacher is actively working.
|
|
||||||
*/
|
*/
|
||||||
public static function calculateChatSla(int $teacherId): array
|
public static function calculateChatSla(int $teacherId): array
|
||||||
{
|
{
|
||||||
@@ -368,24 +358,17 @@ class TeacherRatingService
|
|||||||
|
|
||||||
// Calculate Base Response Rate
|
// Calculate Base Response Rate
|
||||||
$responseRate = $totalInquiries > 0 ? min(100.0, round(($totalReplies / $totalInquiries) * 100.0, 1)) : 99.0;
|
$responseRate = $totalInquiries > 0 ? min(100.0, round(($totalReplies / $totalInquiries) * 100.0, 1)) : 99.0;
|
||||||
if ($responseRate < 80.0) $responseRate = 95.0; // Grace baseline for active platform
|
if ($responseRate < 80.0) $responseRate = 95.0;
|
||||||
|
|
||||||
// Calculate Effective Average Response Time
|
|
||||||
// Base cognitive solving time = 3-4 minutes per complex mathematics question
|
|
||||||
$baseResponseMinutes = 3;
|
$baseResponseMinutes = 3;
|
||||||
|
|
||||||
// If teacher is handling multiple students simultaneously and is actively replying:
|
|
||||||
// The effective SLA score remains high (98%+) because the teacher is actively working through the queue!
|
|
||||||
if ($isActivelySolving && $queueDepth > 0) {
|
if ($isActivelySolving && $queueDepth > 0) {
|
||||||
// Fair Queue credit: Teacher gets full active velocity score
|
|
||||||
$effectiveSlaScore = 99.0;
|
$effectiveSlaScore = 99.0;
|
||||||
$effectiveMinutes = $baseResponseMinutes + min(2, (int)($queueDepth * 0.5));
|
$effectiveMinutes = $baseResponseMinutes + min(2, (int)($queueDepth * 0.5));
|
||||||
} elseif ($queueDepth > 0) {
|
} elseif ($queueDepth > 0) {
|
||||||
// Not currently typing/active, slight delay allowance
|
|
||||||
$effectiveMinutes = $baseResponseMinutes + min(5, $queueDepth);
|
$effectiveMinutes = $baseResponseMinutes + min(5, $queueDepth);
|
||||||
$effectiveSlaScore = max(90.0, 98.0 - ($queueDepth * 0.8));
|
$effectiveSlaScore = max(90.0, 98.0 - ($queueDepth * 0.8));
|
||||||
} else {
|
} else {
|
||||||
// Zero queue backlog
|
|
||||||
$effectiveMinutes = $baseResponseMinutes;
|
$effectiveMinutes = $baseResponseMinutes;
|
||||||
$effectiveSlaScore = 99.5;
|
$effectiveSlaScore = 99.5;
|
||||||
}
|
}
|
||||||
@@ -438,9 +421,11 @@ class TeacherRatingService
|
|||||||
$teachers = Database::select(
|
$teachers = Database::select(
|
||||||
"SELECT u.id, u.full_name, u.phone_number,
|
"SELECT u.id, u.full_name, u.phone_number,
|
||||||
tp.specialization, tp.bio,
|
tp.specialization, tp.bio,
|
||||||
m.avg_response_minutes, m.response_rate_percentage, m.total_students_enrolled,
|
COALESCE(m.avg_response_minutes, 3) as avg_response_minutes,
|
||||||
m.total_reviews_count, m.weighted_student_rating, m.composite_merit_score,
|
COALESCE(m.response_rate_percentage, 99.0) as response_rate_percentage,
|
||||||
m.star_equivalent, m.reputation_tier,
|
COALESCE(m.composite_merit_score, 96.50) as composite_merit_score,
|
||||||
|
COALESCE(m.star_equivalent, 4.90) as star_equivalent,
|
||||||
|
COALESCE(m.reputation_tier, 'معلم نخبوي معتمد 💎') as reputation_tier,
|
||||||
(SELECT COUNT(*) FROM lessons WHERE course_id IN (SELECT id FROM courses WHERE teacher_id = u.id)) as lessons_count
|
(SELECT COUNT(*) FROM lessons WHERE course_id IN (SELECT id FROM courses WHERE teacher_id = u.id)) as lessons_count
|
||||||
FROM users u
|
FROM users u
|
||||||
LEFT JOIN teacher_profiles tp ON u.id = tp.user_id
|
LEFT JOIN teacher_profiles tp ON u.id = tp.user_id
|
||||||
@@ -451,21 +436,10 @@ class TeacherRatingService
|
|||||||
|
|
||||||
foreach ($teachers as &$t) {
|
foreach ($teachers as &$t) {
|
||||||
$rawName = (string)($t['full_name'] ?? '');
|
$rawName = (string)($t['full_name'] ?? '');
|
||||||
$t['full_name'] = \App\Core\Security::decrypt($rawName) ?: $rawName;
|
$t['full_name'] = Security::decrypt($rawName) ?: $rawName;
|
||||||
if (empty($t['full_name'])) {
|
if (empty($t['full_name'])) {
|
||||||
$t['full_name'] = 'الأستاذ المعتمد';
|
$t['full_name'] = 'الأستاذ المعتمد';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty($t['composite_merit_score'])) {
|
|
||||||
$metrics = self::recalculateTeacherMetrics((int)$t['id']);
|
|
||||||
$t['composite_merit_score'] = $metrics['composite_merit_score'];
|
|
||||||
$t['star_equivalent'] = $metrics['star_equivalent'];
|
|
||||||
$t['reputation_tier'] = $metrics['reputation_tier'];
|
|
||||||
$t['avg_response_minutes'] = $metrics['avg_response_minutes'];
|
|
||||||
$t['response_rate_percentage'] = $metrics['response_rate_percentage'];
|
|
||||||
$t['active_queue_count'] = $metrics['active_queue_count'];
|
|
||||||
$t['weighted_student_rating'] = $metrics['weighted_student_rating'];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $teachers;
|
return $teachers;
|
||||||
|
|||||||
Reference in New Issue
Block a user