KnowledgePointQuestionStatsService.php 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  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 表题目数
  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. $hasContent = Schema::hasColumn('questions_tem', 'content');
  84. $stemExpr = $hasContent
  85. ? 'IFNULL(NULLIF(TRIM(t.stem), \'\'), t.content)'
  86. : 'TRIM(t.stem)';
  87. $rows = DB::select(
  88. "SELECT t.kp_code AS kp_code, COUNT(*) AS c
  89. FROM questions_tem AS t
  90. WHERE t.kp_code IS NOT NULL AND t.kp_code != ''
  91. AND NOT EXISTS (
  92. SELECT 1 FROM questions AS q
  93. WHERE q.kp_code = t.kp_code
  94. AND q.stem = ({$stemExpr})
  95. )
  96. GROUP BY t.kp_code"
  97. );
  98. $out = [];
  99. foreach ($rows as $row) {
  100. $out[(string) $row->kp_code] = (int) $row->c;
  101. }
  102. return $out;
  103. }
  104. /**
  105. * @return list<array{
  106. * kp_code: string,
  107. * kp_name: string,
  108. * questions_count: int,
  109. * tem_non_duplicate_count: int,
  110. * sort_order: int
  111. * }>
  112. */
  113. public function buildRows(): array
  114. {
  115. $orderMap = $this->buildKpOrderFromTextbooks();
  116. $qCounts = $this->questionsCountByKp();
  117. $temCounts = $this->temNonDuplicateCountByKp();
  118. $kpCodes = array_unique(array_merge(
  119. array_keys($qCounts),
  120. array_keys($temCounts),
  121. Schema::hasTable('knowledge_points')
  122. ? DB::table('knowledge_points')->pluck('kp_code')->all()
  123. : []
  124. ));
  125. sort($kpCodes);
  126. $names = [];
  127. if (Schema::hasTable('knowledge_points')) {
  128. $names = DB::table('knowledge_points')->pluck('name', 'kp_code')->toArray();
  129. }
  130. $unmappedBase = 1_000_000;
  131. $rows = [];
  132. foreach ($kpCodes as $kp) {
  133. if ($kp === '' || $kp === null) {
  134. continue;
  135. }
  136. $kp = (string) $kp;
  137. $qc = (int) ($qCounts[$kp] ?? 0);
  138. $tc = (int) ($temCounts[$kp] ?? 0);
  139. if ($qc === 0 && $tc === 0) {
  140. continue;
  141. }
  142. $rows[] = [
  143. 'kp_code' => $kp,
  144. 'kp_name' => trim((string) ($names[$kp] ?? '')),
  145. 'questions_count' => $qc,
  146. 'tem_non_duplicate_count' => $tc,
  147. 'sort_order' => $orderMap[$kp] ?? ($unmappedBase + (crc32($kp) % 100_000)),
  148. ];
  149. }
  150. usort($rows, function ($a, $b) {
  151. if ($a['sort_order'] !== $b['sort_order']) {
  152. return $a['sort_order'] <=> $b['sort_order'];
  153. }
  154. if ($a['questions_count'] !== $b['questions_count']) {
  155. return $a['questions_count'] <=> $b['questions_count'];
  156. }
  157. return strcmp($a['kp_code'], $b['kp_code']);
  158. });
  159. return $rows;
  160. }
  161. /**
  162. * Markdown 表格(含标题与说明)
  163. */
  164. public function toMarkdownTable(array $rows): string
  165. {
  166. $sem = self::textbookSemesterForOrdering();
  167. $lines = [];
  168. $lines[] = '# 知识点题量统计';
  169. $lines[] = '';
  170. $lines[] = sprintf(
  171. '- 排序:教材 **semester=%d**(默认下学期)按年级与章节关联知识点顺序优先,其次 **questions 题量升序**。',
  172. $sem
  173. );
  174. $lines[] = '- **tem 待入库(不重复)**:`questions_tem` 中与 `questions` 同 `kp_code` 且题干一致者不重复计数。';
  175. $lines[] = '';
  176. $lines[] = '| 知识点 ID | 知识点名称 | questions 题目数 | questions_tem 待入库(不含与 questions 重复) |';
  177. $lines[] = '| --- | --- | ---: | ---: |';
  178. foreach ($rows as $r) {
  179. $name = str_replace('|', '\\|', $r['kp_name'] !== '' ? $r['kp_name'] : '—');
  180. $lines[] = sprintf(
  181. '| `%s` | %s | %d | %d |',
  182. $r['kp_code'],
  183. $name,
  184. $r['questions_count'],
  185. $r['tem_non_duplicate_count']
  186. );
  187. }
  188. $lines[] = '';
  189. $sumQ = array_sum(array_column($rows, 'questions_count'));
  190. $sumT = array_sum(array_column($rows, 'tem_non_duplicate_count'));
  191. $lines[] = sprintf('**合计**:questions %d 题;questions_tem(不重复)%d 题。', $sumQ, $sumT);
  192. $lines[] = '';
  193. return implode("\n", $lines);
  194. }
  195. }