news 2026/9/13 20:34:05

【口算王|01】HarmonyOS ArkTS 口算题生成实战:按年级、运算类型和难度生成可控题目

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【口算王|01】HarmonyOS ArkTS 口算题生成实战:按年级、运算类型和难度生成可控题目

【口算王|01】HarmonyOS ArkTS 口算题生成实战:按年级、运算类型和难度生成可控题目

口算应用看似只要随机两个数、拼一个运算符就能工作,真正落地时却会迅速遇到边界问题:一年级题目不能频繁出现负数,除法不能产生无法口算的小数,四则混合要遵守运算顺序,错误选项不能重复,随机练习不能改坏原题库,所谓“难度”还必须能被代码和测试明确解释。

本文基于口算王项目本地口算王工程(脱敏路径)的真实 ArkTS 源码,重点复核MockBanks.etsPracticePage.etsQuestionUtils.etsMathModels.ets。当前实现使用 ArkTS/ArkUI;本轮没有依据构建配置或设备运行结果断言具体系统版本。题目完全由本地算法生成,共有 6 个年级题库、7 类题型,每个年级生成 250 条题目记录,总计 1500 条;记录数、唯一 ID 数和不同题干数需要分别统计。文章会区分“已经实现的行为”和“为了让难度真正可控而建议增加的约束”,不会把设计方案写成现成功能。

证据边界:本文“当前实现”来自本轮对 MockBanks、PracticePage、QuestionUtils 与 MathModels 的静态复核,并用独立脚本按相同公式重放了题型数量和内容签名统计。DifficultyRule、GenerationSpec、稳定内容 ID、校验结果类型和性质测试均为建议设计。本轮没有修改工程源码,也没有执行 hvigor 构建、自动化测试、模拟器或真机验证。

一、先定义“可控”,再谈随机

可控题目不是每次得到同一个结果,而是无论随机种子如何变化,都满足一组不变量:

题干合法 答案唯一 选项不重复 减法结果符合年级边界 除法保证整除 混合运算遵循明确规则 题型与筛选条件一致 题量不超过候选池 原始题库不被洗牌污染 相同题目 ID 对应稳定内容

口算王当前生成器没有调用网络接口,也没有在页面运行时临时向服务端请求题目。MockBanks.ets在模块加载时构造原始题目并缓存展开后的Question[],这让题目总量、ID 和解析都可以从源码复核。

二、真实数据模型决定生成器能表达什么

MathModels.ets中的题目接口是:

export interface Question { id: string bankId: string chapterId: string type: string stem: string audioHint?: string options: Option[] answer: string analysis: string example?: string }

这里有bankIdchapterIdtype,但没有gradedifficultyoperandRangeskills字段。年级依赖bankId的命名约定推导,难度则隐含在操作数范围和题型中。

因此标题中的“按难度生成”必须谨慎解释:当前代码实现的是“按年级间接改变数值范围,并用题型表达部分认知复杂度”,不是一个可由用户显式选择的easy / medium / hard难度系统。

三、六个年级题库如何映射

源码定义了 6 个年级:

{ id: 'grade1', name: '一年级', shortName: '一', ... } { id: 'grade2', name: '二年级', shortName: '二', ... } { id: 'grade3', name: '三年级', shortName: '三', ... } { id: 'grade4', name: '四年级', shortName: '四', ... } { id: 'grade5', name: '五年级', shortName: '五', ... } { id: 'grade6', name: '六年级', shortName: '六', ... }

对应题库 ID 为b_grade1b_grade6。生成时通过字符串替换得到年级:

const grade = Number(bankId.replace('b_grade', ''))

这段代码简洁,但它依赖命名格式。如果传入grade1、空字符串或未知 ID,grade可能成为NaN。当前getQuestions()在缓存没有命中时仍会调用buildQuestions(bankId),所以路由参数校验不能省略。

四、250 题不是手写,而是七类题型轮转

每个题库目标数量为:

const BANK_TARGET_COUNT: number = 250

生成循环使用:

const slot = (i - 1) % 7

slot对应加、减、乘、除、混合、应用和限时七类题型。250 除以 7 的商是 35、余数是 5,因此每个年级中前五类各生成 36 题,应用题和限时题各生成 35 题。六个年级合计:

题型每年级六年级合计
add36216
sub36216
mul36216
div36216
mixed36216
word35210
speed35210
总计2501500

这不是统计平台数据,而是由确定性循环直接推导出的本地题库数量。

五、年级目前怎样影响操作数范围

加减题的两个基础操作数来自:

const a = 1 + ((i * 7 + grade * 3) % (grade <= 1 ? 10 : grade <= 2 ? 60 : 120)) const b = 1 + ((i * 5 + grade * 11) % (grade <= 1 ? 10 : grade <= 2 ? 50 : 90))

由此可见:

  • 一年级:ab主要落在 1 至 10。
  • 二年级:上限分别扩展到 60 和 50。
  • 三至六年级:上限分别为 120 和 90。

这里的“难度”主要表现为数值范围扩大。三至六年级虽然偏移量不同,但模数相同,并没有随年级持续扩大数值边界。源码中的章节名称包含小数、分数、百分数和比例,但当前生成器仍只生成整数四则与简单文字题。

这意味着不能仅根据章节文案宣称五、六年级已实现小数、分数和百分数出题。真实能力边界仍由buildQuestions()决定。

六、减法为什么不会出现负数

减法分支先排序两个操作数:

const big = Math.max(a, b) const small = Math.min(a, b) pushQuestion( raws, 'sub', `${big} - ${small} = ?`, big - small, '减法先保证被减数更大,避免出现负数。', i )

这个约束保证答案不小于 0,适合基础口算。如果未来增加“有理数”或“负数”难度档,不能直接复用这条规则,而应把allowNegative变成显式约束。

还应注意,big === small时会生成答案 0。这不算错误,但测试用例必须包含零结果,避免错误选项生成器把 0 替换掉或出现重复。

七、除法为什么一定整除

除法不是先随机被除数,而是先生成除数和商:

const divisor = 1 + ((i + grade) % 9) const quotient = 1 + ((i * 3 + grade) % 9) pushQuestion( raws, 'div', `${divisor * quotient} ÷ ${divisor} = ?`, quotient, '除法题保证整除,可以用乘法反推答案。', i )

被除数由divisor * quotient反推,因此满足:

divisor != 0 dividend % divisor == 0 answer == quotient

这是比“随机两个数后检查是否整除”更稳定的构造方式,不需要重试,也没有无限循环风险。对于基础口算,先生成答案空间再反推题干通常更容易保证约束。

八、乘法、混合运算与限时题的真实边界

乘法使用 1 至 9 的因子:

const x = 1 + ((i + grade) % 9) const y = 1 + ((i * 2 + grade) % 9)

这符合九九乘法范围,但各年级都使用同一因子上限。混合运算则固定为:

`${x} × ${y} + ${z} = ?`

解析明确要求先乘后加。限时题固定为:

`${x} - ${y} + ${grade} = ?`

按从左到右计算。这里的“限时”是题型标签,真正倒计时在PracticePageexam模式中实现;单个speed题本身没有独立计时字段。

九、应用题目前只有一种数量关系

应用题模板为:

`书架上有 ${first} 本书,又放上 ${more} 本,现在共有多少本?`

答案是first + more,解析是“原有数量 + 新增数量”。它可以验证文字题展示和数量提取,但不能代表已经覆盖减法比较、倍数、平均分、单位换算或多步应用题。

若要增加模板,建议先定义WordProblemTemplate,让模板携带题型、单位、约束和答案函数,而不是在一个if分支中继续堆叠字符串。

十、错误选项怎样避免重复

当前错误选项从答案偏移得到:

const offsets: number[] = [ 0, 1 + (seed % 4), -1 - (seed % 3), 5 + (seed % 6) ]

每个候选值都会经过两层处理:

if (value < 0) value = Math.abs(value) + 2 while (values.indexOf(value) >= 0) value += 2

第一层避免负数选项,第二层持续加 2,直到四个选项互不重复。随后把正确答案从位置 0 交换到seed % 4,让正确选项位置在 A 至 D 之间轮转。

这套方法保证“唯一”和“非负”,但干扰项质量仍有提升空间。例如答案较小时,负偏移会被镜像成较大的正数;对于乘除题,基于常见计算错误生成干扰项会更有教学价值。

十一、题目 ID 为什么稳定

展开阶段生成:

id: `${bankId}_q${i + 1}` chapterId: `${regionId}_c${(i % 6) + 1}`

只要题目顺序不改变,同一个bankId下的 ID 就稳定,收藏、错题和笔记可以通过 ID 找回题目。但如果以后在 250 题列表中间插入新题,后续 ID 会整体漂移,旧数据可能指向不同题目。

更稳妥的演进方式是由题型、操作数和模板版本生成稳定键,例如:

b_grade2:add:27:16:v1

现有代码尚未采用这种键,因此扩充题库前要先评估历史收藏和错题数据迁移。

十二、章节标题和生成规则目前没有真正绑定

每个年级有 6 个章节,章节标题很具体,例如一年级有“10以内加法”“20以内进位”,五年级有“小数加减”“分数入门”。

然而当前章节归属只是:

chapterId: `${regionId}_c${(i % 6) + 1}`

这会把七类题型按索引轮流分散到六章,而不是根据章节主题筛选。于是“10以内加法”章节中可能出现除法、混合运算或应用题;“小数加减”章节也仍是整数题。

这不是页面问题,而是生成模型缺少章节约束。文章必须明确这一点,不能把 UI 上的章节名当作题库内容已匹配的证据。

十三、运算类型筛选发生在练习页

PracticePage.aboutToAppear()先获取题库,再按questionType过滤:

this.questions = getQuestions(params.bankId) if (params.questionType) { this.questions = this.questions.filter( (question: Question) => question.type === params.questionType ) }

如果题型不存在,列表会保持为空,因为后续兜底条件明确排除了params.questionType。这有利于暴露错误参数,但页面必须提供清晰空状态,否则用户只会看到没有题目的界面。

过滤是对缓存数组的派生结果,不会改写题库。随机模式随后再对候选集取样,顺序正确:

选择年级题库 -> 可选题型过滤 -> 随机抽取 20 题

十四、随机练习不会修改缓存

QuestionUtils.shuffle()先复制:

const result = [...arr]

再执行 Fisher-Yates 洗牌。因为洗牌发生在副本上,QUESTIONS_CACHE中的题目顺序不会被一次练习永久改变。

随机模式最多取 20 题:

const take = Math.min(count, pool.length) return QuestionUtils.shuffle(pool).slice(0, take)

当候选池不足 20 时,返回全部候选题;候选池为空时返回空数组。指定startQuestionId时,找到目标题就把它固定在第一位,再从其余题目中洗牌补足。这些边界都可以用纯函数测试验证。

十五、挑战模式不是完全随机

挑战模式调用pickHourlyExamQuestions()。它以当前 UTC 小时和bankId哈希构造种子,再用线性同余序列驱动 Fisher-Yates 洗牌。

结果具有两个特点:

  • 同一题库在同一小时内题序稳定,便于用户重复进入时保持一致。
  • 小时变化后整体轮换,提供新的挑战组合。

这与普通随机模式的Math.random()不同。测试时不要把两者混为一谈:随机模式验证集合约束与不污染原数组;挑战模式还要验证同小时可复现和跨小时可变化。

十六、把“难度”从隐式经验升级为显式约束

当前代码没有难度字段。若要真正支持难度选择,可以先引入纯数据配置:

interface DifficultyRule { id: string minOperand: number maxOperand: number allowCarry: boolean allowBorrow: boolean allowNegative: boolean divisionExactOnly: boolean maxSteps: number }

再把年级、章节和题型映射到规则,而不是在buildQuestions()中继续增加三元表达式。生成器接收GenerationSpec

interface GenerationSpec { grade: number chapterId: string type: string difficultyId: string count: number seed: number }

这是改进建议,不是当前源码已经存在的接口。

十七、生成器适合放在纯逻辑层

口算题生成可以拆成五层:

  1. Spec:年级、章节、题型、难度、数量和种子。
  2. Constraint:操作数范围、进退位、整除、步数和结果边界。
  3. Generator:生成题干、答案、解析与例式。
  4. Distractor:生成唯一、合理的错误选项。
  5. Session:过滤、抽样、计时、作答和持久化。

当前MockBanks.ets同时承担目录、题目构造、选项生成、章节分配、缓存和计数同步。规模较小时可工作,但难度系统扩展后,纯函数边界能显著降低测试成本。

十八、必须覆盖的性质测试

生成算法最适合做性质测试,而不是只断言某一道题:

所有题目恰好 4 个选项 所有选项文本唯一 answer 一定是 A/B/C/D answer 对应文本等于计算结果 sub 的答案 >= 0 div 的除数 != 0 且保证整除 每个题目 ID 唯一 每个题目 chapterId 属于当前年级 按 type 过滤后所有题型一致 随机抽样数量 <= 20 shuffle 不修改输入数组 同小时挑战顺序稳定

还要测试无效bankId。当前命名解析可能产生NaN,如果路由把未知 ID 传入生成器,虽然循环仍可能产出题目,但内容边界失去年级含义。更安全的做法是先验证 ID 是否属于BANKS

十九、性能与内存要看真实规模

模块加载时RAW_MAP会立即为 6 个题库分别构造 250 道原始题,也就是 1500 个RawQuestionsyncCatalogCounts()随后调用getQuestions(),又把所有原始题展开为完整Question并放入缓存。

这意味着启动阶段会同时持有原始题与展开题。1500 题对当前应用通常不算大,但如果扩展到小数、分数、单位换算和多模板应用题,双份常驻数据会增加内存和启动成本。

可选优化包括按题库懒生成、只缓存展开题、把目录计数预计算到常量,或使用相同种子按需重建。优化前应先测量启动时间和内存,不能仅凭代码行数判断性能问题。

二十、当前实现可以确认什么

基于真实源码,可以确认:

  • 题目由本地算法生成。
  • 共有 6 个年级题库,每库 250 题,总计 1500 题。
  • 有 7 类题型,题型数量可由循环规则推导。
  • 减法不产生负答案。
  • 除法通过反推被除数保证整除。
  • 每题有 4 个互不重复的非负选项。
  • 正确答案位置随种子在 A 至 D 轮转。
  • 支持按题型过滤、随机 20 题和同小时稳定挑战。
  • 洗牌复制数组,不会改写缓存顺序。

不能声称:

  • 已有独立难度选择器或difficulty字段。
  • 章节标题已经约束对应题型和数值范围。
  • 五、六年级已生成小数、分数、百分数或比例题。
  • 所有年级的难度都随年级连续递增。
  • 自动化测试、真机性能或教学效果已经验证通过。

二十一、从现有生成器到可控题库的最小升级路线

建议按以下顺序演进:

第一步:为 bankId、chapterId、type 增加输入校验 第二步:补齐现有生成器的性质测试 第三步:把难度规则提取成 DifficultyRule 第四步:让章节显式绑定允许题型和数值约束 第五步:为题目建立不随数组位置变化的稳定 ID 第六步:按需生成并测量启动时间与内存 第七步:在 HarmonyOS 手机、平板和 2in1 上验证练习流程

口算题生成的关键不在于“随机得更像随机”,而在于把教学边界变成代码能够执行、测试能够证明的约束。口算王当前实现已经具备本地生成、整除控制、非负减法、唯一选项和安全洗牌等基础;下一步真正影响内容质量的,是让章节与难度从界面文案进入数据模型。只有这样,“按年级、运算类型和难度生成”才会从宣传描述变成可复核的工程能力。

二十二、1500 条记录不等于 1500 种题干

buildQuestions()的循环次数决定记录数,expandQuestions()又按数组位置生成唯一 ID,因此 6 个题库确实得到 1500 条记录和 1500 个不同 ID。但公式使用有限模数,索引继续增长后会回到已经出现过的操作数组合。工程验收若只检查数组长度和 ID 唯一性,就会漏掉内容重复。

本轮按源码的同一组公式重放 250 次,并用type + stem + answer作为内容签名。每个年级的统计如下:

年级记录数不同内容签名说明
一年级250129加减模数较小,重复最明显
二年级250181加减范围扩大
三年级250179乘除和应用题仍周期重复
四年级250178数值上限未继续扩大
五年级250180尚未生成小数或分数
六年级250181章节名称未改变生成公式

这个结果不是说重复记录必然没有训练价值:相同题干可以服务于复习频次。但产品若把“题量”理解为内容广度,就应单独展示去重后的题干数、模板数和技能点覆盖数,不能用记录条数替代。

二十三、题型数量均衡也不代表内容分布均衡

250 对 7 取模后,单库中加、减、乘、除、混合各 36 条,应用和限时各 35 条;六库汇总分别是 216、216、216、216、216、210、210。记录数量看起来十分均匀,但乘法和除法每个年级都只有 9 种不同的题干与答案组合,应用题也只有 20 种数字组合和一个固定模板。

数量均衡:每类记录条数接近 内容均衡:不同题干、答案区间和易错点分布合理 教学均衡:进位、退位、整除、估算等技能覆盖符合目标

三者需要不同指标。当前轮转规则只解决第一层,不能推出后两层。真正的难度审计还应统计答案分布、进退位比例、同题重复间隔、模板覆盖和相邻题相似度。

二十四、建立三本账:记录、身份和内容

建议为题库构建报告同时维护三种计数,避免“ID 唯一”掩盖“题干重复”。以下接口是建议设计,并未出现在当前工程:

interface QuestionCountReport { recordCount: number uniqueIdCount: number uniqueContentCount: number duplicateContentCount: number } function contentSignature(question: Question): string { return [question.type, question.stem, question.answer].join('|') }

recordCount用于验证生成器没有少产,uniqueIdCount保护收藏和错题引用,uniqueContentCount衡量内容广度。若重复是有意复习,还应记录templateIdskillId,把“同一知识点复现”和“无意的公式周期碰撞”区分开。

二十五、输入校验应先于生成和缓存

当前getQuestions()在缓存未命中时会执行RAW_MAP.get(bankId) || buildQuestions(bankId)。未知 ID 可能让年级解析成NaN,随后仍生成 250 条失去年级语义的记录,并被放进缓存。相比默默兜底,边界层更适合返回可识别的失败结果:

interface QuestionLoadResult { success: boolean questions: Question[] message: string } function loadQuestions(bankId: string): QuestionLoadResult { const bank = BANKS.find(item => item.id === bankId) if (!bank) { return { success: false, questions: [], message: '未知题库' } } return { success: true, questions: getQuestions(bank.id), message: '' } }

页面收到失败后进入明确的错误或空状态,不应再用另一次生成兜底掩盖路由参数问题。这样缓存键始终来自合法目录,统计也不会混入无法解释的题库。

二十六、可复现生成需要规则版本,不只需要随机种子

普通练习用Math.random()洗牌,挑战模式则以 UTC 小时和题库哈希产生可复现顺序。这里的种子只控制“从现有数组怎样排列”,并不固定题目内容。若以后修改操作数公式、题型轮转或模板,即使种子相同,抽到的业务内容仍会变化。

需要长期复现时,生成规格应同时携带ruleVersiontemplateVersionseed。规则版本决定候选集,模板版本决定题干语义,种子才决定候选集中的抽样顺序。历史考试记录保存这三个值后,才能解释“为什么当时出现这道题”。

二十七、把性质断言放在缓存写入之前

生成结果进入QUESTIONS_CACHE后会被搜索、分类、收藏、错题和考试多处消费。越晚发现坏题,影响面越大。建议构建阶段先校验整批结果,再把通过的不可变快照写入缓存。

assert recordCount == spec.count assert every id is unique assert every question has exactly 4 unique options assert answer key exists in options assert option(answer) equals calculated result assert subtraction result is non-negative assert division is exact and divisor is not zero assert chapterId belongs to selected bank assert type belongs to allowed types assert uniqueContentRatio meets the product threshold

最后一条阈值必须由产品和教学目标共同确定,而不是随意要求 100%。例如复习模式允许有计划地重复,但应保证重复间隔、错因覆盖和技能分布符合设计;新题模式则可以要求更高的内容唯一率。

二十八、验证顺序从纯函数走向页面

最小验证链可以分四层执行。第一层测试操作数和答案纯函数;第二层批量生成每个年级并输出三本账;第三层验证题型过滤、章节过滤、普通随机与同小时挑战;第四层才在 HarmonyOS 页面检查空状态、20 题上限、答题卡、收藏和错题回放。

层级重点输入关键证据
规则边界数、非法 ID明确成功或失败结果
批量生成6 年级 × 7 题型数量、唯一性、分布报告
会话抽样题型、章节、小时种子不污染缓存且数量不越界
页面空池、长题干、重复进入状态可见、导航正确、数据可追踪

本轮只完成静态复核和公式重放,没有运行这些工程测试。把验证边界写清楚,比用“理论上没问题”代替测试更可靠。

二十九、总结:先约束生成,再度量内容

口算王当前生成器已经具备非负减法、整除除法、四个唯一选项、按题型过滤、安全洗牌和同小时稳定抽样等基础能力。真正需要继续完善的,不是简单增加循环次数,而是把年级、章节、难度和内容多样性变成显式规格。

工程上应同时守住三条线:输入必须合法,生成结果必须满足性质,题量统计必须区分记录、身份和内容。这样“250 题”才不只是数组长度,“按难度”也不只是数值上限,而是能够被配置、测试、回放和解释的教学约束。


AI 辅助声明:本文在人工核对真实工程源码、题型分布、操作数规则、章节分配、缓存和抽样路径后,使用 AI 辅助整理结构、润色表达并生成示意图;当前实现、公式重放结果、建议扩展与未执行验证均已分别说明。

CSDN-SERIES:ALL-163186049

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 20:28:18

UE Capability深度解析:从LTE到NR,终端能力上报如何影响5G体验

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 20:23:12

红黑树原理与C语言实现:200行代码搞定插入删除修复

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 20:22:48

基于局部高斯分布拟合的医学图像分割算法实现

1. 项目概述&#xff1a;基于局部高斯分布拟合的活动轮廓模型在医学影像分析和计算机视觉领域&#xff0c;图像分割始终是基础且关键的预处理步骤。传统阈值分割、边缘检测等方法在面对复杂纹理、低对比度的图像时往往表现不佳。我们团队近期实现的这个基于变分水平集的主动轮廓…

作者头像 李华
网站建设 2026/9/13 20:20:58

Python安装与环境配置:从解释器到可复现开发环境

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 20:20:47

Dymola2018安装配置实战:Modelica建模仿真环境搭建指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华