QuestionDifficultyResolver.php 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. <?php
  2. namespace App\Services;
  3. use Illuminate\Support\Facades\DB;
  4. use Illuminate\Support\Facades\Schema;
  5. class QuestionDifficultyResolver
  6. {
  7. private const TABLE = 'question_difficulty_calibrations';
  8. private const SERVED_DIFF_CONFIDENCE_DENOMINATOR = 20.0;
  9. private ?bool $tableReady = null;
  10. /**
  11. * @param array<int, int|string> $questionIds
  12. * @return array<int, array<string, float|null>> question_bank_id => calibration snapshot
  13. */
  14. public function mapCalibratedDifficulty(array $questionIds): array
  15. {
  16. if (! $this->isReady()) {
  17. return [];
  18. }
  19. $questionIds = collect($questionIds)
  20. ->map(fn ($id) => (int) $id)
  21. ->filter(fn ($id) => $id > 0)
  22. ->unique()
  23. ->values()
  24. ->all();
  25. if ($questionIds === []) {
  26. return [];
  27. }
  28. return DB::table(self::TABLE)
  29. ->whereIn('question_bank_id', $questionIds)
  30. ->get(['question_bank_id', 'original_difficulty', 'calibrated_difficulty', 'weighted_attempts'])
  31. ->mapWithKeys(function ($row) {
  32. $qid = (int) ($row->question_bank_id ?? 0);
  33. if ($qid <= 0) {
  34. return [];
  35. }
  36. return [
  37. $qid => [
  38. 'original_difficulty' => $row->original_difficulty !== null ? (float) $row->original_difficulty : null,
  39. 'calibrated_difficulty' => $row->calibrated_difficulty !== null ? (float) $row->calibrated_difficulty : null,
  40. 'weighted_attempts' => $row->weighted_attempts !== null ? (float) $row->weighted_attempts : null,
  41. ],
  42. ];
  43. })
  44. ->all();
  45. }
  46. /**
  47. * 批量给题目数组计算组卷使用难度 served_diff:
  48. * served_diff = alpha * calibrated + (1 - alpha) * original
  49. * alpha = min(1, weighted_attempts / 20)
  50. * 前提:仅当 calibrated_difficulty 存在时才融合;否则保持原始 difficulty 不变。
  51. *
  52. * @param array<int, array<string, mixed>> $questions
  53. * @return array<int, array<string, mixed>>
  54. */
  55. public function applyCalibratedDifficulty(array $questions): array
  56. {
  57. if ($questions === []) {
  58. return $questions;
  59. }
  60. $ids = [];
  61. foreach ($questions as $q) {
  62. $id = (int) ($q['id'] ?? $q['question_id'] ?? $q['question_bank_id'] ?? 0);
  63. if ($id > 0) {
  64. $ids[] = $id;
  65. }
  66. }
  67. $map = $this->mapCalibratedDifficulty($ids);
  68. if ($map === []) {
  69. return $questions;
  70. }
  71. foreach ($questions as &$q) {
  72. $id = (int) ($q['id'] ?? $q['question_id'] ?? $q['question_bank_id'] ?? 0);
  73. if ($id <= 0 || ! array_key_exists($id, $map)) {
  74. continue;
  75. }
  76. $snapshot = $map[$id] ?? [];
  77. $calibrated = $snapshot['calibrated_difficulty'] ?? null;
  78. if ($calibrated === null) {
  79. continue;
  80. }
  81. $original = isset($q['difficulty']) && is_numeric($q['difficulty'])
  82. ? (float) $q['difficulty']
  83. : (float) ($snapshot['original_difficulty'] ?? $calibrated);
  84. $weightedAttempts = max(0.0, (float) ($snapshot['weighted_attempts'] ?? 0.0));
  85. $alpha = min(1.0, $weightedAttempts / self::SERVED_DIFF_CONFIDENCE_DENOMINATOR);
  86. $servedDifficulty = ($alpha * (float) $calibrated) + ((1.0 - $alpha) * $original);
  87. $q['difficulty'] = round($servedDifficulty, 4);
  88. $q['difficulty_source'] = $alpha >= 0.9999 ? 'calibrated' : 'served_blend';
  89. $q['difficulty_original'] = round($original, 4);
  90. $q['difficulty_calibrated'] = round((float) $calibrated, 4);
  91. $q['difficulty_alpha'] = round($alpha, 4);
  92. $q['difficulty_weighted_attempts'] = round($weightedAttempts, 4);
  93. }
  94. unset($q);
  95. return $questions;
  96. }
  97. private function isReady(): bool
  98. {
  99. if ($this->tableReady !== null) {
  100. return $this->tableReady;
  101. }
  102. $this->tableReady = Schema::hasTable(self::TABLE);
  103. return $this->tableReady;
  104. }
  105. }