灯光模拟HarmonyOS应用实战-73-题库标题回退成科一,查询条件却仍是非法科目:用SubjectResolution统一显示与选题
灯光模拟HarmonyOS应用实战-73-题库标题回退成科一,查询条件却仍是非法科目:用SubjectResolution统一显示与选题
题库页为了避免标题为空,提供了一个看似友好的回退:在科目标签中找不到当前值时,返回第一项“科一灯光题”。但原始 questionBankSubject 并没有随之修正。于是同一个界面可能同时持有两套事实:标题和模式来自科一,搜索过滤与开始练习仍使用非法科目字符串。
这不是单纯的文案问题。展示回退掩盖了状态损坏,后续选题会得到空数组,页面还可能借用内置第一题作为展示兜底,使错误继续向下传播。更稳的做法是建立 SubjectResolution:一次解析同时产出规范科目、标签配置、解析依据和错误状态,显示、搜索、路由与选题只能消费这一份结果。

本文会先还原当前分叉,再给出不静默伪装的解析结果、接入顺序、空题保护与回归矩阵。示例方案均与当前源码事实分开说明。
一、当前回退只返回第一张标签,没有修正原始科目
Index.ets 中,题库科目配置只有科一、科二和科四。getQuestionBankSubjectInfo() 遍历配置;如果找不到 questionBankSubject,最终直接返回 QUESTION_BANK_TABS[0]。
private getQuestionBankSubjectInfo(): QuestionBankSubjectTab {
for (let index = 0;
index < QUESTION_BANK_TABS.length; index++) {
if (QUESTION_BANK_TABS[index].subject ===
this.questionBankSubject) {
return QUESTION_BANK_TABS[index];
}
}
return QUESTION_BANK_TABS[0];
}
因此 getQuestionBankSubjectTitle()、getQuestionBankSubjectSubtitle() 和 getQuestionBankSubjectMode() 都会显示科一配置。可是函数没有给 this.questionBankSubject 赋值,也没有返回“发生过回退”的标志。调用方只能看到一张合法配置,无法知道输入本来非法。
| 输入状态 | 配置查询结果 | 原始字段 | 当前可见风险 |
|---|---|---|---|
subject1 | 科一配置 | subject1 | 语义一致 |
subject2 | 科二配置 | subject2 | 语义一致 |
subject4 | 科四配置 | subject4 | 语义一致 |
subject3 | 科一配置 | 仍为 subject3 | 标题合法、过滤条件不一致 |
| 空串或未知值 | 科一配置 | 仍为原值 | 状态损坏被展示回退隐藏 |
从当前 UI 的三个标签看,正常点击只会产生三个合法值。非法值可能来自未来恢复、深链、重构或其他入口;本文不能断言现网用户已经遇到它,但函数契约确实允许这种不一致继续存在。
二、搜索和开始练习都继续使用未解析值
标题卡读取的是回退后的配置,搜索循环比较的却是 questionBankSubject。当字段为未知值时,没有任何题目能满足比较。
private refreshQuestionBankResults(): void {
const result: QuestionItem[] = [];
for (let index = 0;
index < BUILTIN_QUESTIONS.length; index++) {
const rawQuestion = BUILTIN_QUESTIONS[index];
if (rawQuestion.subject !== this.questionBankSubject) {
continue;
}
// 关键词匹配后追加结果
}
this.questionBankDataSource.setQuestions(result);
}
点击“开始练习”时,代码同样把未解析的字段传给 openQuestionBankTest(),只是标题和模式来自回退配置。
this.openQuestionBankTest(
this.questionBankSubject,
this.getQuestionBankSubjectTitle(),
this.getQuestionBankSubjectMode(),
''
);
openQuestionBankTest() 会把 currentPage 设置为该字符串,随后按这个字符串选题。build() 对未知页面走最终 else,所以会渲染理论练习页,但 selectQuestions() 又按非法科目过滤,结果仍为空。这条静态控制流说明错误能够跨过页面分支;它不是一次真机操作记录。

三、内置第一题兜底会进一步模糊空会话
当前 getCurrentQuestion() 在 activeQuestions 为空时返回 BUILTIN_QUESTIONS[0]。页面因非法科目得到空选题后,仍可能绘制第一道内置题的文本。与此同时,answerTheoryQuestion() 会因为活动数组为空而直接返回。
private getCurrentQuestion(): QuestionItem {
if (this.activeQuestions.length === 0) {
return BUILTIN_QUESTIONS[0];
}
return this.activeQuestions[this.activeQuestionIndex];
}
private answerTheoryQuestion(answer: string): void {
if (this.selectedAnswer.length > 0 ||
this.activeQuestions.length === 0) {
return;
}
// 正常判题
}
这会形成一个“看得到却答不了”的幽灵题风险。第一题具体属于哪个科目取决于当前内置题库构建顺序,不能把它当作可靠的科一回退协议。空会话应该显示明确空态或错误态,而不是借一条真实题目装饰页面。
需要分别处理两个问题:科目解析失败时不要开始会话;合法科目暂时没有题时,也不要从其他科目借题。两者都返回空,但修复入口和用户提示不同。
四、SubjectResolution同时保留原始值与规范结果
解析器不应只返回一个标签对象。它要说明输入是否受支持、最终规范值是什么、依据来自哪里,供日志和页面选择正确分支。
export enum SubjectResolutionKind {
EXACT = 'exact',
RECOVERED = 'recovered',
REJECTED = 'rejected'
}
export interface SubjectResolution {
kind: SubjectResolutionKind;
rawSubject: string;
canonicalSubject: string;
tab: QuestionBankSubjectTab | undefined;
reasonCode: string;
}
正常标签点击返回 EXACT。如果产品确实允许旧值迁移,可以在受审阅的映射表中返回 RECOVERED,例如旧协议 subject-one 映射到 subject1。其他未知值返回 REJECTED,不伪装成科一。
rawSubject 只用于诊断或迁移判断,不应继续参与题目过滤。若它可能来自外部输入,日志只记录有限原因码和经过长度限制的值,不记录完整路由载荷。
五、解析器以标签目录作为唯一支持列表
支持科目不要再维护一套平行 if。直接在题库标签目录中查找,并把旧值迁移放在显式表里。
interface LegacySubjectAlias {
legacy: string;
canonical: string;
}
const SUBJECT_ALIASES: LegacySubjectAlias[] = [
{ legacy: 'subject-one', canonical: SUBJECT_ONE }
];
function resolveQuestionBankSubject(
rawSubject: string,
tabs: QuestionBankSubjectTab[]
): SubjectResolution {
const exact = findTab(rawSubject, tabs);
if (exact !== undefined) {
return {
kind: SubjectResolutionKind.EXACT,
rawSubject,
canonicalSubject: exact.subject,
tab: exact,
reasonCode: 'EXACT_TAB'
};
}
const alias = findAlias(rawSubject, SUBJECT_ALIASES);
const recovered = alias === undefined ? undefined :
findTab(alias.canonical, tabs);
if (recovered !== undefined) {
return {
kind: SubjectResolutionKind.RECOVERED,
rawSubject,
canonicalSubject: recovered.subject,
tab: recovered,
reasonCode: 'REVIEWED_ALIAS'
};
}
return {
kind: SubjectResolutionKind.REJECTED,
rawSubject,
canonicalSubject: '',
tab: undefined,
reasonCode: 'UNSUPPORTED_SUBJECT'
};
}
findTab() 和 findAlias() 是很薄的查找函数,接入时应按项目 ArkTS 约束实现。关键是未知值不再返回第一项。若业务希望应用首次启动默认科一,应在“初始化题库状态”这一明确场景设置 SUBJECT_ONE,不能让任意解析失败都共享这个默认。
六、一次解析要被显示、搜索和启动共同消费

在一次用户事件里重复调用四个 getter,仍可能因中途状态变化读到不同结果。可以先取得解析快照,再交给卡片、搜索和启动函数。
private prepareQuestionBankContext():
SubjectResolution {
return resolveQuestionBankSubject(
this.questionBankSubject,
QUESTION_BANK_TABS
);
}
private startResolvedQuestionBank(): void {
const resolution = this.prepareQuestionBankContext();
if (resolution.kind === SubjectResolutionKind.REJECTED ||
resolution.tab === undefined) {
this.showQuestionBankSubjectError(
resolution.reasonCode
);
return;
}
this.openQuestionBankTest(
resolution.canonicalSubject,
resolution.tab.title,
resolution.tab.mode,
''
);
}
搜索函数也要接收 resolution.canonicalSubject,不能继续读取页面原始字段。若解析结果是 RECOVERED,可在状态提交点把页面字段更新为规范值;提交应发生一次,避免 getter 在渲染期间偷偷改 @State。
卡片渲染遇到 REJECTED 时显示“科目状态不可用,请重新选择”,并提供三个合法标签。此时不显示科一标题,也不启用开始按钮。错误变得可见,恢复路径也明确。
七、规范科目要与页面路由分开建模
当前理论页面 ID 恰好与 SUBJECT_ONE、SUBJECT_TWO、SUBJECT_FOUR 字符串相同,这使 currentPage = subject 看起来方便,却把“内容分类”和“页面身份”绑在一起。若以后科目编码升级,路由也会被迫变化。
type TheoryPageId =
| 'subject1'
| 'subject2'
| 'subject4';
interface TheoryDestination {
pageId: TheoryPageId;
subject: string;
title: string;
mode: string;
}
function toTheoryDestination(
resolution: SubjectResolution
): TheoryDestination | undefined {
if (resolution.tab === undefined) return undefined;
return {
pageId: resolution.tab.subject as TheoryPageId,
subject: resolution.canonicalSubject,
title: resolution.tab.title,
mode: resolution.tab.mode
};
}
示例暂时保持当前页面 ID,目的是显式标出转换边界。正式实现可进一步让 pageId 使用独立常量。只有 toTheoryDestination() 成功后,页面才提交 currentPage、标题和会话查询,避免先跳页后发现选题失败。
八、启动结果要在提交页面状态前确认非空
合法科目也可能因内容配置、分类条件或数据加载问题得到空题组。可以先准备会话,再一次性提交。
type TheoryPreparation =
| { kind: 'ready'; subject: string;
questions: QuestionItem[] }
| { kind: 'empty'; subject: string }
| { kind: 'rejected'; reasonCode: string };
function prepareTheory(
resolution: SubjectResolution,
allQuestions: QuestionItem[]
): TheoryPreparation {
if (resolution.tab === undefined) {
return {
kind: 'rejected',
reasonCode: resolution.reasonCode
};
}
const questions = selectBySubject(
allQuestions,
resolution.canonicalSubject
);
if (questions.length === 0) {
return {
kind: 'empty',
subject: resolution.canonicalSubject
};
}
return {
kind: 'ready',
subject: resolution.canonicalSubject,
questions
};
}
ready 才允许进入理论页;empty 显示内容空态;rejected 要求重新选择科目。这样 getCurrentQuestion() 不再需要借用 BUILTIN_QUESTIONS[0] 作为业务兜底,页面也不会把“非法科目”和“合法但没有题”混成同一种空数组。
九、用解析矩阵锁定所有输入分支
解析器适合做纯函数测试。至少覆盖精确值、经过审阅的旧别名、空串、科三值、大小写变化和前后空格。是否允许 trim() 或大小写归一要写入协议,不能临时宽松处理。
const cases = [
{ raw: 'subject1', kind: 'exact', canonical: 'subject1' },
{ raw: 'subject2', kind: 'exact', canonical: 'subject2' },
{ raw: 'subject4', kind: 'exact', canonical: 'subject4' },
{ raw: 'subject-one', kind: 'recovered', canonical: 'subject1' },
{ raw: 'subject3', kind: 'rejected', canonical: '' },
{ raw: '', kind: 'rejected', canonical: '' }
];
cases.forEach((item) => {
const result = resolveQuestionBankSubject(
item.raw,
QUESTION_BANK_TABS
);
expect(result.kind).assertEqual(item.kind);
expect(result.canonicalSubject)
.assertEqual(item.canonical);
});
集成层再断言同一次解析结果同时驱动卡片标题、搜索科目和会话科目。只测解析器会漏掉接入处继续使用 questionBankSubject 的回归。
十、排障表、验证清单与事实边界
| 现象 | 优先核对 | 常见原因 | 修复方向 |
|---|---|---|---|
| 标题是科一,搜索始终为空 | 原始科目与解析结果 | getter 回退但字段未修正 | 统一消费 canonicalSubject |
| 页面显示第一题却不能作答 | activeQuestions 长度 | 空数组仍借内置第一题渲染 | 空会话显示专用空态 |
| 旧版本科目全部被拒绝 | 别名迁移表 | 忘记登记经过确认的旧值 | 加版本化别名,不做任意猜测 |
| 点击开始后标题与历史模式不一致 | 是否重复读取 getter | 中途状态变化或多套映射 | 事件开始时冻结一次解析快照 |
| 科三误进理论页 | 页面 ID 来源 | 把任意 subject 直接赋给 currentPage | 通过目的地转换后再跳页 |
| 错误自动变成科一且没有提示 | 初始化与恢复混用 | 任意失败都返回第一项 | 只在明确初始化场景默认科一 |
- 题库支持科目只来自一个标签目录。
- 未知科目返回
REJECTED,不伪装为第一项。 - 旧值只有进入受审阅别名表才能恢复。
- 标题、摘要、模式、搜索和选题消费同一解析快照。
- 原始科目不再直接参与过滤和路由。
- 空题组显示明确空态,不渲染其他科目的第一题。
- 页面状态只在解析和会话准备成功后一次提交。
- 错误态提供重新选择科目的恢复路径。
- 解析原因码进入最小诊断记录,不保存完整外部载荷。
- 合法三科、非法输入和旧别名都有集成回归。
当前源码能够确认:题库配置包含科一、科二、科四;查找失败时 getQuestionBankSubjectInfo() 返回第一项;搜索和开始按钮仍使用原始 questionBankSubject;理论会话为空时 getCurrentQuestion() 返回内置第一题。SubjectResolution、别名表、目的地转换和准备结果都是建议设计,尚未写入项目。
本文没有执行构建,没有生成 HAP,也没有在模拟器或真机制造非法科目状态。完成改造后还需用项目实际恢复入口、题库页面和历史写入做运行回归,不能把控制流分析扩大成已经修复的产品结论。
更多推荐



所有评论(0)