| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123 |
- <?php
- namespace App\Services;
- use Illuminate\Support\Facades\DB;
- use Illuminate\Support\Facades\Schema;
- class QuestionDifficultyResolver
- {
- private const TABLE = 'question_difficulty_calibrations';
- private const SERVED_DIFF_CONFIDENCE_DENOMINATOR = 20.0;
- private ?bool $tableReady = null;
- /**
- * @param array<int, int|string> $questionIds
- * @return array<int, array<string, float|null>> question_bank_id => calibration snapshot
- */
- public function mapCalibratedDifficulty(array $questionIds): array
- {
- if (! $this->isReady()) {
- return [];
- }
- $questionIds = collect($questionIds)
- ->map(fn ($id) => (int) $id)
- ->filter(fn ($id) => $id > 0)
- ->unique()
- ->values()
- ->all();
- if ($questionIds === []) {
- return [];
- }
- return DB::table(self::TABLE)
- ->whereIn('question_bank_id', $questionIds)
- ->get(['question_bank_id', 'original_difficulty', 'calibrated_difficulty', 'weighted_attempts'])
- ->mapWithKeys(function ($row) {
- $qid = (int) ($row->question_bank_id ?? 0);
- if ($qid <= 0) {
- return [];
- }
- return [
- $qid => [
- 'original_difficulty' => $row->original_difficulty !== null ? (float) $row->original_difficulty : null,
- 'calibrated_difficulty' => $row->calibrated_difficulty !== null ? (float) $row->calibrated_difficulty : null,
- 'weighted_attempts' => $row->weighted_attempts !== null ? (float) $row->weighted_attempts : null,
- ],
- ];
- })
- ->all();
- }
- /**
- * 批量给题目数组计算组卷使用难度 served_diff:
- * served_diff = alpha * calibrated + (1 - alpha) * original
- * alpha = min(1, weighted_attempts / 20)
- * 前提:仅当 calibrated_difficulty 存在时才融合;否则保持原始 difficulty 不变。
- *
- * @param array<int, array<string, mixed>> $questions
- * @return array<int, array<string, mixed>>
- */
- public function applyCalibratedDifficulty(array $questions): array
- {
- if ($questions === []) {
- return $questions;
- }
- $ids = [];
- foreach ($questions as $q) {
- $id = (int) ($q['id'] ?? $q['question_id'] ?? $q['question_bank_id'] ?? 0);
- if ($id > 0) {
- $ids[] = $id;
- }
- }
- $map = $this->mapCalibratedDifficulty($ids);
- if ($map === []) {
- return $questions;
- }
- foreach ($questions as &$q) {
- $id = (int) ($q['id'] ?? $q['question_id'] ?? $q['question_bank_id'] ?? 0);
- if ($id <= 0 || ! array_key_exists($id, $map)) {
- continue;
- }
- $snapshot = $map[$id] ?? [];
- $calibrated = $snapshot['calibrated_difficulty'] ?? null;
- if ($calibrated === null) {
- continue;
- }
- $original = isset($q['difficulty']) && is_numeric($q['difficulty'])
- ? (float) $q['difficulty']
- : (float) ($snapshot['original_difficulty'] ?? $calibrated);
- $weightedAttempts = max(0.0, (float) ($snapshot['weighted_attempts'] ?? 0.0));
- $alpha = min(1.0, $weightedAttempts / self::SERVED_DIFF_CONFIDENCE_DENOMINATOR);
- $servedDifficulty = ($alpha * (float) $calibrated) + ((1.0 - $alpha) * $original);
- $q['difficulty'] = round($servedDifficulty, 4);
- $q['difficulty_source'] = $alpha >= 0.9999 ? 'calibrated' : 'served_blend';
- $q['difficulty_original'] = round($original, 4);
- $q['difficulty_calibrated'] = round((float) $calibrated, 4);
- $q['difficulty_alpha'] = round($alpha, 4);
- $q['difficulty_weighted_attempts'] = round($weightedAttempts, 4);
- }
- unset($q);
- return $questions;
- }
- private function isReady(): bool
- {
- if ($this->tableReady !== null) {
- return $this->tableReady;
- }
- $this->tableReady = Schema::hasTable(self::TABLE);
- return $this->tableReady;
- }
- }
|