|
|
@@ -13,6 +13,14 @@ use Illuminate\Support\Str;
|
|
|
|
|
|
class QuestionLocalService
|
|
|
{
|
|
|
+ private DifficultyDistributionService $difficultyDistributionService;
|
|
|
+
|
|
|
+ public function __construct(?DifficultyDistributionService $difficultyDistributionService = null)
|
|
|
+ {
|
|
|
+ $this->difficultyDistributionService = $difficultyDistributionService
|
|
|
+ ?? app(DifficultyDistributionService::class);
|
|
|
+ }
|
|
|
+
|
|
|
public function listQuestions(int $page = 1, int $perPage = 50, array $filters = []): array
|
|
|
{
|
|
|
$query = $this->applyFilters(Question::query(), $filters);
|
|
|
@@ -562,8 +570,9 @@ class QuestionLocalService
|
|
|
*
|
|
|
* @param array $questions 候选题目数组
|
|
|
* @param int $totalQuestions 总题目数
|
|
|
- * @param int $difficultyCategory 难度类别 (1-4)
|
|
|
- * - 1: 0-0.25范围占50%,其他占50%
|
|
|
+ * @param int $difficultyCategory 难度类别 (0-4)
|
|
|
+ * - 0: 0-0.1占90%,0.1-0.25占10%
|
|
|
+ * - 1: 0-0.25占90%,0.25-1占10%
|
|
|
* - 2: 0.25-0.5范围占50%,<0.25占25%,>0.5占25%
|
|
|
* - 3: 0.5-0.75范围占50%,<0.5占25%,>0.75占25%
|
|
|
* - 4: 0.75-1范围占50%,其他占50%
|
|
|
@@ -575,7 +584,6 @@ class QuestionLocalService
|
|
|
Log::info('QuestionLocalService: 根据难度系数分布选择题目', [
|
|
|
'total_questions' => $totalQuestions,
|
|
|
'difficulty_category' => $difficultyCategory,
|
|
|
- 'input_questions' => count($questions)
|
|
|
]);
|
|
|
|
|
|
if (empty($questions)) {
|
|
|
@@ -584,19 +592,15 @@ class QuestionLocalService
|
|
|
}
|
|
|
|
|
|
// 【恢复】简化逻辑,避免复杂处理
|
|
|
- $distribution = $this->calculateDifficultyDistribution($difficultyCategory, $totalQuestions);
|
|
|
-
|
|
|
- Log::info('QuestionLocalService: 难度分布计算', [
|
|
|
- 'distribution' => $distribution,
|
|
|
- 'target_count' => $totalQuestions
|
|
|
- ]);
|
|
|
+ $distribution = $this->difficultyDistributionService->calculateDistribution($difficultyCategory, $totalQuestions);
|
|
|
|
|
|
// 按难度范围分桶
|
|
|
- $buckets = $this->groupQuestionsByDifficultyRange($questions, $difficultyCategory);
|
|
|
+ $buckets = $this->difficultyDistributionService->groupQuestionsByDifficultyRange($questions, $difficultyCategory);
|
|
|
|
|
|
Log::info('QuestionLocalService: 题目分桶', [
|
|
|
'buckets' => array_map(fn($bucket) => count($bucket), $buckets),
|
|
|
- 'total_input' => count($questions)
|
|
|
+ 'total_input' => count($questions),
|
|
|
+ 'distribution' => $distribution
|
|
|
]);
|
|
|
|
|
|
// 根据分布选择题目
|
|
|
@@ -613,16 +617,9 @@ class QuestionLocalService
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
- $rangeKey = $this->mapDifficultyLevelToRangeKey($level, $difficultyCategory);
|
|
|
+ $rangeKey = $this->difficultyDistributionService->mapDifficultyLevelToRangeKey($level, $difficultyCategory);
|
|
|
$bucket = $buckets[$rangeKey] ?? [];
|
|
|
|
|
|
- Log::debug('QuestionLocalService: 处理难度层级', [
|
|
|
- 'level' => $level,
|
|
|
- 'range_key' => $rangeKey,
|
|
|
- 'target_count' => $targetCount,
|
|
|
- 'bucket_size' => count($bucket)
|
|
|
- ]);
|
|
|
-
|
|
|
// 随机打乱
|
|
|
shuffle($bucket);
|
|
|
|
|
|
@@ -646,267 +643,94 @@ class QuestionLocalService
|
|
|
'range_key' => $rangeKey,
|
|
|
'target' => $targetCount,
|
|
|
'actual' => $taken,
|
|
|
- 'bucket_size' => count($bucket),
|
|
|
- 'note' => '将在后续步骤中从其他范围补充题目'
|
|
|
- ]);
|
|
|
- } else {
|
|
|
- Log::debug('QuestionLocalService: 难度层级选择', [
|
|
|
- 'level' => $level,
|
|
|
- 'target' => $targetCount,
|
|
|
- 'actual' => $taken,
|
|
|
- 'bucket_size' => count($bucket),
|
|
|
- 'range_key' => $rangeKey
|
|
|
+ 'bucket_size' => count($bucket)
|
|
|
]);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- Log::info('QuestionLocalService: 分布选择后统计', [
|
|
|
- 'selected_count' => count($selected),
|
|
|
- 'target_count' => $totalQuestions,
|
|
|
- 'need_more' => max(0, $totalQuestions - count($selected))
|
|
|
- ]);
|
|
|
-
|
|
|
// 如果数量不足,从剩余题目中补充
|
|
|
if (count($selected) < $totalQuestions) {
|
|
|
Log::warning('QuestionLocalService: 开始补充题目(难度分布无法满足要求)', [
|
|
|
'need_more' => $totalQuestions - count($selected),
|
|
|
'selected_count' => count($selected),
|
|
|
'difficulty_category' => $difficultyCategory,
|
|
|
- 'note' => '由于难度分布限制,将从所有剩余题目中补充'
|
|
|
+ 'note' => '优先从次级桶补充,不足再放宽'
|
|
|
]);
|
|
|
|
|
|
- $remaining = [];
|
|
|
- foreach ($questions as $q) {
|
|
|
- $id = $q['id'] ?? null;
|
|
|
- if ($id && !in_array($id, $usedIds)) {
|
|
|
- $remaining[] = $q;
|
|
|
+ $needMore = $totalQuestions - count($selected);
|
|
|
+ $supplemented = 0;
|
|
|
+ $supplementOrder = $this->difficultyDistributionService->getSupplementOrder($difficultyCategory);
|
|
|
+
|
|
|
+ foreach ($supplementOrder as $bucketKey) {
|
|
|
+ if ($supplemented >= $needMore) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ $bucket = $buckets[$bucketKey] ?? [];
|
|
|
+ if (empty($bucket)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ shuffle($bucket);
|
|
|
+ foreach ($bucket as $q) {
|
|
|
+ if ($supplemented >= $needMore) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ $id = $q['id'] ?? null;
|
|
|
+ if ($id && !in_array($id, $usedIds)) {
|
|
|
+ $selected[] = $q;
|
|
|
+ $usedIds[] = $id;
|
|
|
+ $supplemented++;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- Log::info('QuestionLocalService: 剩余题目统计', [
|
|
|
- 'remaining_count' => count($remaining),
|
|
|
- 'need_more' => $totalQuestions - count($selected)
|
|
|
- ]);
|
|
|
+ if ($supplemented < $needMore) {
|
|
|
+ $remaining = [];
|
|
|
+ foreach ($questions as $q) {
|
|
|
+ $id = $q['id'] ?? null;
|
|
|
+ if ($id && !in_array($id, $usedIds)) {
|
|
|
+ $remaining[] = $q;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- // 【修复】打乱剩余题目顺序,确保随机性
|
|
|
- shuffle($remaining);
|
|
|
- $needMore = $totalQuestions - count($selected);
|
|
|
- $supplementCount = min($needMore, count($remaining));
|
|
|
- $selected = array_merge($selected, array_slice($remaining, 0, $supplementCount));
|
|
|
+ shuffle($remaining);
|
|
|
+ $supplementCount = min($needMore - $supplemented, count($remaining));
|
|
|
+ $selected = array_merge($selected, array_slice($remaining, 0, $supplementCount));
|
|
|
+ $supplemented += $supplementCount;
|
|
|
+ }
|
|
|
|
|
|
Log::warning('QuestionLocalService: 补充完成', [
|
|
|
- 'supplement_added' => $supplementCount,
|
|
|
+ 'supplement_added' => $supplemented,
|
|
|
'final_count_before_truncate' => count($selected),
|
|
|
- 'remaining_unused' => count($remaining) - $supplementCount
|
|
|
+ 'remaining_unused' => max(0, count($questions) - count($selected))
|
|
|
]);
|
|
|
}
|
|
|
|
|
|
// 截断至目标数量
|
|
|
$selected = array_slice($selected, 0, $totalQuestions);
|
|
|
|
|
|
+ $finalBuckets = $this->difficultyDistributionService->groupQuestionsByDifficultyRange($selected, $difficultyCategory);
|
|
|
+ $finalTotal = max(1, count($selected));
|
|
|
+ $distributionStats = array_map(static function ($bucket) use ($finalTotal) {
|
|
|
+ $count = count($bucket);
|
|
|
+ return [
|
|
|
+ 'count' => $count,
|
|
|
+ 'ratio' => round(($count / $finalTotal) * 100, 2),
|
|
|
+ ];
|
|
|
+ }, $finalBuckets);
|
|
|
+
|
|
|
Log::info('QuestionLocalService: 难度分布选择完成', [
|
|
|
'final_count' => count($selected),
|
|
|
'target_count' => $totalQuestions,
|
|
|
'success' => count($selected) === $totalQuestions,
|
|
|
'input_count' => count($questions),
|
|
|
- 'distribution_applied' => true
|
|
|
+ 'distribution_applied' => true,
|
|
|
+ 'final_distribution' => $distributionStats
|
|
|
]);
|
|
|
|
|
|
return $selected;
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 计算难度分布配置
|
|
|
- *
|
|
|
- * @param int $category 难度类别 (1-4)
|
|
|
- * @param int $totalQuestions 总题目数
|
|
|
- * @return array 分布配置
|
|
|
- */
|
|
|
- private function calculateDifficultyDistribution(int $category, int $totalQuestions): array
|
|
|
- {
|
|
|
- // 标准化:25% 低级,50% 基准,25% 拔高
|
|
|
-// $lowPercentage = 25;
|
|
|
-// $mediumPercentage = 50;
|
|
|
-// $highPercentage = 25;
|
|
|
-
|
|
|
- // 根据难度类别调整分布
|
|
|
- switch ($category) {
|
|
|
- case 1:
|
|
|
- // 基础型:0-0.25占50%,其他占50%
|
|
|
- $mediumPercentage = 90; // 0-0.25作为基准
|
|
|
- $lowPercentage = 0; // 其他低难度
|
|
|
- $highPercentage = 10; // 其他高难度
|
|
|
- break;
|
|
|
-
|
|
|
- case 2:
|
|
|
- // 进阶型:0.25-0.5占50%,<0.25占25%,>0.5占25%
|
|
|
- $mediumPercentage = 50; // 0.25-0.5作为基准
|
|
|
- $lowPercentage = 25; // <0.25
|
|
|
- $highPercentage = 25; // >0.5
|
|
|
- break;
|
|
|
-
|
|
|
- case 3:
|
|
|
- // 中等型:0.5-0.75占50%,<0.5占25%,>0.75占25%
|
|
|
- $mediumPercentage = 50; // 0.5-0.75作为基准
|
|
|
- $lowPercentage = 25; // <0.5
|
|
|
- $highPercentage = 25; // >0.75
|
|
|
- break;
|
|
|
-
|
|
|
- case 4:
|
|
|
- // 拔高型:0.75-1占50%,其他占50%
|
|
|
- $mediumPercentage = 50; // 0.75-1作为基准
|
|
|
- $lowPercentage = 25; // 其他低难度
|
|
|
- $highPercentage = 25; // 其他高难度
|
|
|
- break;
|
|
|
- default:
|
|
|
- $lowPercentage = 25;
|
|
|
- $mediumPercentage = 50;
|
|
|
- $highPercentage = 25;
|
|
|
- }
|
|
|
-
|
|
|
- // 计算题目数量
|
|
|
- $lowCount = (int) round($totalQuestions * $lowPercentage / 100);
|
|
|
- $mediumCount = (int) round($totalQuestions * $mediumPercentage / 100);
|
|
|
- $highCount = $totalQuestions - $lowCount - $mediumCount;
|
|
|
-
|
|
|
- return [
|
|
|
- 'low' => [
|
|
|
- 'percentage' => $lowPercentage,
|
|
|
- 'count' => $lowCount,
|
|
|
- 'label' => '低级难度'
|
|
|
- ],
|
|
|
- 'medium' => [
|
|
|
- 'percentage' => $mediumPercentage,
|
|
|
- 'count' => $mediumCount,
|
|
|
- 'label' => '基准难度'
|
|
|
- ],
|
|
|
- 'high' => [
|
|
|
- 'percentage' => $highPercentage,
|
|
|
- 'count' => $highCount,
|
|
|
- 'label' => '拔高难度'
|
|
|
- ]
|
|
|
- ];
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 将题目按难度范围分桶
|
|
|
- *
|
|
|
- * @param array $questions 题目数组
|
|
|
- * @param int $category 难度类别
|
|
|
- * @return array 分桶结果
|
|
|
- */
|
|
|
- private function groupQuestionsByDifficultyRange(array $questions, int $category): array
|
|
|
- {
|
|
|
- $buckets = [
|
|
|
- 'primary_low' => [], // 主要低难度范围
|
|
|
- 'primary_medium' => [], // 主要中等难度范围
|
|
|
- 'primary_high' => [], // 主要高难度范围
|
|
|
- 'secondary' => [], // 次要范围
|
|
|
- 'other' => [] // 其他
|
|
|
- ];
|
|
|
-
|
|
|
- foreach ($questions as $question) {
|
|
|
- $difficulty = (float) ($question['difficulty'] ?? 0);
|
|
|
- $rangeKey = $this->classifyQuestionByDifficulty($difficulty, $category);
|
|
|
- $buckets[$rangeKey][] = $question;
|
|
|
- }
|
|
|
-
|
|
|
- return $buckets;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 根据难度值和类别分类题目
|
|
|
- *
|
|
|
- * @param float $difficulty 难度值 (0-1)
|
|
|
- * @param int $category 难度类别 (1-4)
|
|
|
- * @return string 范围键
|
|
|
- */
|
|
|
- private function classifyQuestionByDifficulty(float $difficulty, int $category): string
|
|
|
- {
|
|
|
- switch ($category) {
|
|
|
- case 1:
|
|
|
- // 基础型:0-0.25作为主要中等,0.25-1作为其他
|
|
|
- if ($difficulty >= 0 && $difficulty <= 0.25) {
|
|
|
- return 'primary_medium';
|
|
|
- }
|
|
|
- return 'other';
|
|
|
-
|
|
|
- case 2:
|
|
|
- // 进阶型:0.25-0.5作为主要中等,<0.25作为主要低,>0.5作为主要高
|
|
|
- if ($difficulty >= 0.25 && $difficulty <= 0.5) {
|
|
|
- return 'primary_medium';
|
|
|
- } elseif ($difficulty < 0.25) {
|
|
|
- return 'primary_low';
|
|
|
- }
|
|
|
- return 'primary_high';
|
|
|
-
|
|
|
- case 3:
|
|
|
- // 中等型:0.5-0.75作为主要中等,<0.5作为主要低,>0.75作为主要高
|
|
|
- if ($difficulty >= 0.5 && $difficulty <= 0.75) {
|
|
|
- return 'primary_medium';
|
|
|
- } elseif ($difficulty < 0.5) {
|
|
|
- return 'primary_low';
|
|
|
- }
|
|
|
- return 'primary_high';
|
|
|
-
|
|
|
- case 4:
|
|
|
- // 拔高型:0.75-1作为主要中等,0-0.75作为其他
|
|
|
- if ($difficulty >= 0.75 && $difficulty <= 1.0) {
|
|
|
- return 'primary_medium';
|
|
|
- }
|
|
|
- return 'other';
|
|
|
-
|
|
|
- default:
|
|
|
- return 'other';
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 将难度层级映射到范围键
|
|
|
- *
|
|
|
- * @param string $level 难度层级 (low/medium/high)
|
|
|
- * @param int $category 难度类别
|
|
|
- * @return string 范围键
|
|
|
- */
|
|
|
- private function mapDifficultyLevelToRangeKey(string $level, int $category): string
|
|
|
- {
|
|
|
- // 【修复】难度分布映射逻辑:确保 'other' 桶中的题目能被正确选择
|
|
|
- // 对于 difficulty_category=1,'other' 桶中的题目应该映射到 'secondary' 桶
|
|
|
- switch ($category) {
|
|
|
- case 1:
|
|
|
- return match($level) {
|
|
|
- 'low' => 'secondary', // 修复:映射到 secondary 桶,而非 other
|
|
|
- 'medium' => 'primary_medium',
|
|
|
- 'high' => 'secondary', // 修复:映射到 secondary 桶,而非 other
|
|
|
- default => 'secondary'
|
|
|
- };
|
|
|
-
|
|
|
- case 2:
|
|
|
- return match($level) {
|
|
|
- 'low' => 'primary_low',
|
|
|
- 'medium' => 'primary_medium',
|
|
|
- 'high' => 'primary_high',
|
|
|
- default => 'other'
|
|
|
- };
|
|
|
-
|
|
|
- case 3:
|
|
|
- return match($level) {
|
|
|
- 'low' => 'primary_low',
|
|
|
- 'medium' => 'primary_medium',
|
|
|
- 'high' => 'primary_high',
|
|
|
- default => 'other'
|
|
|
- };
|
|
|
-
|
|
|
- case 4:
|
|
|
- return match($level) {
|
|
|
- 'low' => 'secondary', // 修复:映射到 secondary 桶,而非 other
|
|
|
- 'medium' => 'primary_medium',
|
|
|
- 'high' => 'secondary', // 修复:映射到 secondary 桶,而非 other
|
|
|
- default => 'secondary'
|
|
|
- };
|
|
|
-
|
|
|
- default:
|
|
|
- return 'other';
|
|
|
- }
|
|
|
- }
|
|
|
}
|