fix: 出题分配算法重构 + 20题模板 + 非技术人员模板

问题修复:
- Math.round 导致合计偏差(3题→4题,5题→6题等)
- 补充轮次破坏维度权重比例
- 各维度无题型比例控制

修正方案:
- floor + remainder 分配法,保证合计永远 = count
- 按weight降序分配余数,权重高的优先
- 不足时仅补充轮,但仍保持维度优先
- 补充轮日志记录各维度实际分配数

新增功能:
- 技术人员模板 20题 (PROMPT:30/LLM:30/IDE:20/DEV_PATTERN:20)
- 非技术人员模板 10题 (PROMPT:50/LLM:30/WORK_CAPABILITY:20)
  → 面向非技术角色,不考核IDE和开发范式
- 模板支持任意维度组合,可灵活配置

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Developer
2026-06-09 11:24:32 +08:00
parent c166d298b8
commit 1aee7e0baf
3 changed files with 446 additions and 17 deletions
@@ -534,30 +534,59 @@ export class QuestionBankService {
const usedIds = new Set<string>();
const selected: QuestionBankItem[] = [];
let availableItems = [...allItems];
const selectedDetail: string[] = [];
if (dimensionWeights && dimensionWeights.length > 0) {
// ── 按权重公平分配题数(floor + remainder,保证总和 = count)──
const totalWeight = dimensionWeights.reduce((s, d) => s + d.weight, 0);
for (const dw of dimensionWeights) {
const dimName = dw.name as QuestionDimension;
const targetForDim = Math.round(count * dw.weight / totalWeight);
let pool = availableItems.filter(i => i.dimension === dimName && !usedIds.has(i.id));
// 第一轮: floor 分配
const targets: { dw: typeof dimensionWeights[0]; target: number; taken: number }[]
= dimensionWeights.map(dw => ({
dw,
target: Math.floor(count * dw.weight / totalWeight),
taken: 0,
}));
let allocated = targets.reduce((s, t) => s + t.target, 0);
// 第二轮: 按 weight 降序分配余数(保证总和 = count)
const remainder = count - allocated;
if (remainder > 0) {
const sortedByWeight = [...targets].sort((a, b) => b.dw.weight - a.dw.weight);
for (let i = 0; i < remainder; i++) {
sortedByWeight[i % sortedByWeight.length].target++;
}
}
// 各维度抽题
for (const t of targets) {
const dimName = t.dw.name as QuestionDimension;
let pool = allItems.filter(i => i.dimension === dimName && !usedIds.has(i.id));
pool = this.shuffleArray(pool);
const take = Math.min(targetForDim, pool.length);
const take = Math.min(t.target, pool.length);
for (let i = 0; i < take; i++) {
selected.push(pool[i]);
usedIds.add(pool[i].id);
t.taken++;
}
selectedDetail.push(`${dimName}: ${t.taken}/${t.target}`);
}
// 如果有维度出题不足,从其他维度补
if (selected.length < count) {
const remaining = allItems.filter(i => !usedIds.has(i.id));
const shuffled = this.shuffleArray(remaining);
for (const item of shuffled) {
if (selected.length >= count) break;
selected.push(item);
usedIds.add(item.id);
selectedDetail.push(`${item.dimension}(补)`);
}
}
availableItems = availableItems.filter(i => !usedIds.has(i.id));
availableItems = this.shuffleArray(availableItems);
while (selected.length < count && availableItems.length > 0) {
const item = availableItems.pop()!;
selected.push(item);
usedIds.add(item.id);
}
} else {
// ── 无维度权重:轮询 DIMENSIONS 列表 ──
let dimIdx = 0;
const availableItems = [...allItems];
while (selected.length < count && availableItems.length > 0) {
const dim = DIMENSIONS[dimIdx % DIMENSIONS.length];
dimIdx++;
@@ -567,13 +596,13 @@ export class QuestionBankService {
const item = pool[idx];
selected.push(item);
usedIds.add(item.id);
availableItems = availableItems.filter(i => i.id !== item.id);
availableItems.splice(availableItems.findIndex(i => i.id === item.id), 1);
}
if (dimIdx >= DIMENSIONS.length * 3) break;
}
if (selected.length < count && availableItems.length > 0) {
availableItems = this.shuffleArray(availableItems);
for (const item of availableItems) {
const shuffled = this.shuffleArray(availableItems);
for (const item of shuffled) {
if (selected.length >= count) break;
if (!usedIds.has(item.id)) {
selected.push(item);
@@ -583,6 +612,7 @@ export class QuestionBankService {
}
}
// 最后兜底
if (selected.length < count) {
const remaining = allItems.filter((i) => !usedIds.has(i.id));
const shuffled = remaining.sort(() => Math.random() - 0.5);
@@ -594,7 +624,7 @@ export class QuestionBankService {
}
this.logger.log(
`[selectQuestions] Selected ${selected.length} questions from bank ${bankId}`,
`[selectQuestions] Selected ${selected.length}/${count} questions from bank ${bankId} | ${selectedDetail.join(', ')}`,
);
return this.shuffleArray(selected);
}