KnowledgePointQuestionStatsService.php 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. <?php
  2. namespace App\Services;
  3. use App\Models\Textbook;
  4. use Illuminate\Support\Facades\DB;
  5. use Illuminate\Support\Facades\Schema;
  6. /**
  7. * 知识点题量统计:正式库 questions、待入库 questions_tem(剔除与 questions 题干重复)
  8. */
  9. class KnowledgePointQuestionStatsService
  10. {
  11. /** 下学期教材 semester 值(与 textbooks.semester 一致,默认 2) */
  12. public static function textbookSemesterForOrdering(): int
  13. {
  14. return (int) config('question_bank.kp_stats_semester', 2);
  15. }
  16. /**
  17. * 按年级下→教材章节顺序得到 kp_code → 排序权重(越小越靠前)
  18. *
  19. * @return array<string, int>
  20. */
  21. public function buildKpOrderFromTextbooks(): array
  22. {
  23. if (! Schema::hasTable('textbooks') || ! Schema::hasTable('textbook_catalog_nodes')) {
  24. return [];
  25. }
  26. $semester = self::textbookSemesterForOrdering();
  27. $textbookIds = Textbook::query()
  28. ->where('semester', $semester)
  29. ->orderBy('grade')
  30. ->orderBy('sort_order')
  31. ->orderBy('id')
  32. ->pluck('id')
  33. ->all();
  34. if ($textbookIds === []) {
  35. return [];
  36. }
  37. $diagnostic = app(DiagnosticChapterService::class);
  38. $order = 0;
  39. $map = [];
  40. $seen = [];
  41. foreach ($textbookIds as $tid) {
  42. $codes = $diagnostic->getTextbookKnowledgePointsInOrder((int) $tid);
  43. foreach ($codes as $kp) {
  44. if ($kp === '' || $kp === null) {
  45. continue;
  46. }
  47. if (isset($seen[$kp])) {
  48. continue;
  49. }
  50. $seen[$kp] = true;
  51. $map[$kp] = $order++;
  52. }
  53. }
  54. return $map;
  55. }
  56. /**
  57. * @return array<string, int> kp_code => questions.kp_code 题目数
  58. */
  59. public function questionsCountByKp(): array
  60. {
  61. if (! Schema::hasTable('questions')) {
  62. return [];
  63. }
  64. return DB::table('questions')
  65. ->selectRaw('kp_code, COUNT(*) as c')
  66. ->whereNotNull('kp_code')
  67. ->where('kp_code', '!=', '')
  68. ->groupBy('kp_code')
  69. ->pluck('c', 'kp_code')
  70. ->map(fn ($c) => (int) $c)
  71. ->toArray();
  72. }
  73. /**
  74. * questions_tem 中「与 questions 同 kp + 同题干」不重复的题目数,按 kp_code
  75. *
  76. * @return array<string, int>
  77. */
  78. public function temNonDuplicateCountByKp(): array
  79. {
  80. if (! Schema::hasTable('questions_tem') || ! Schema::hasTable('questions')) {
  81. return [];
  82. }
  83. $rows = DB::select(
  84. "SELECT t.kp_code AS kp_code, COUNT(*) AS c
  85. FROM questions_tem AS t
  86. WHERE t.kp_code IS NOT NULL AND t.kp_code != ''
  87. AND t.stem IS NOT NULL AND t.stem != ''
  88. AND NOT EXISTS (
  89. SELECT 1 FROM questions AS q
  90. WHERE q.kp_code = t.kp_code
  91. AND q.stem != ''
  92. AND q.stem = t.stem
  93. )
  94. GROUP BY t.kp_code"
  95. );
  96. $out = [];
  97. foreach ($rows as $row) {
  98. $out[(string) $row->kp_code] = (int) $row->c;
  99. }
  100. return $out;
  101. }
  102. /**
  103. * @return list<array{
  104. * kp_code: string,
  105. * kp_name: string,
  106. * questions_count: int,
  107. * tem_non_duplicate_count: int,
  108. * sort_order: int
  109. * }>
  110. */
  111. public function buildRows(): array
  112. {
  113. $orderMap = $this->buildKpOrderFromTextbooks();
  114. $qCounts = $this->questionsCountByKp();
  115. $temCounts = $this->temNonDuplicateCountByKp();
  116. $kpCodes = array_unique(array_merge(
  117. array_keys($qCounts),
  118. array_keys($temCounts),
  119. Schema::hasTable('knowledge_points')
  120. ? DB::table('knowledge_points')->pluck('kp_code')->all()
  121. : []
  122. ));
  123. sort($kpCodes);
  124. $names = [];
  125. if (Schema::hasTable('knowledge_points')) {
  126. $names = DB::table('knowledge_points')->pluck('name', 'kp_code')->toArray();
  127. }
  128. $unmappedBase = 1_000_000;
  129. $rows = [];
  130. foreach ($kpCodes as $kp) {
  131. if ($kp === '' || $kp === null) {
  132. continue;
  133. }
  134. $kp = (string) $kp;
  135. $qc = (int) ($qCounts[$kp] ?? 0);
  136. $tc = (int) ($temCounts[$kp] ?? 0);
  137. if ($qc === 0 && $tc === 0) {
  138. continue;
  139. }
  140. $rows[] = [
  141. 'kp_code' => $kp,
  142. 'kp_name' => trim((string) ($names[$kp] ?? '')),
  143. 'questions_count' => $qc,
  144. 'tem_non_duplicate_count' => $tc,
  145. 'sort_order' => $orderMap[$kp] ?? ($unmappedBase + (crc32($kp) % 100_000)),
  146. ];
  147. }
  148. usort($rows, function ($a, $b) {
  149. if ($a['sort_order'] !== $b['sort_order']) {
  150. return $a['sort_order'] <=> $b['sort_order'];
  151. }
  152. if ($a['questions_count'] !== $b['questions_count']) {
  153. return $a['questions_count'] <=> $b['questions_count'];
  154. }
  155. return strcmp($a['kp_code'], $b['kp_code']);
  156. });
  157. return $rows;
  158. }
  159. /**
  160. * Markdown 表格(含标题与说明)
  161. */
  162. public function toMarkdownTable(array $rows): string
  163. {
  164. $sem = self::textbookSemesterForOrdering();
  165. $lines = [];
  166. $lines[] = '# 知识点题量统计';
  167. $lines[] = '';
  168. $lines[] = sprintf(
  169. '- 排序:教材 **semester=%d**(默认下学期)按年级与章节关联知识点顺序优先,其次 **questions 题量升序**。',
  170. $sem
  171. );
  172. $lines[] = '- **tem 待入库(不重复)**:`questions_tem` 中与 `questions` 同 `kp_code` 且题干一致者不重复计数。';
  173. $lines[] = '';
  174. $lines[] = '| 知识点 ID | 知识点名称 | questions 题目数 | questions_tem 待入库(不含与 questions 重复) |';
  175. $lines[] = '| --- | --- | ---: | ---: |';
  176. foreach ($rows as $r) {
  177. $name = str_replace('|', '\\|', $r['kp_name'] !== '' ? $r['kp_name'] : '—');
  178. $lines[] = sprintf(
  179. '| `%s` | %s | %d | %d |',
  180. $r['kp_code'],
  181. $name,
  182. $r['questions_count'],
  183. $r['tem_non_duplicate_count']
  184. );
  185. }
  186. $lines[] = '';
  187. $sumQ = array_sum(array_column($rows, 'questions_count'));
  188. $sumT = array_sum(array_column($rows, 'tem_non_duplicate_count'));
  189. $lines[] = sprintf('**合计**:questions %d 题;questions_tem(不重复)%d 题。', $sumQ, $sumT);
  190. $lines[] = '';
  191. return implode("\n", $lines);
  192. }
  193. }