【听见课堂 HarmonyOS NEXT 实战系列 04】用领域模型串起课堂证据:Course、Transcript、Scan、Task 建模
课堂助手的数据模型如果只围绕页面设计,很容易变成“首页卡片一个类型、复习页卡片一个类型、历史页再复制一个类型”。同一段字幕被反复转换,来源时间和确认状态也可能在转换过程中丢失。
听见课堂选择从领域事实出发:课程是上下文,字幕和扫描是课堂证据,任务是经过确认后形成的行动项。页面需要的统计与分组,再由这些 canonical data 计算出来。
一、先区分事实数据与页面快照
事实数据描述“发生了什么”,页面快照描述“当前要怎么展示”。这两类模型不应该混在一起。
听见课堂的核心事实模型包括:
exportclassCourseSummary{id:string;title:string;teacher:string;room:string;startTime:string;progress:number;colorToken:string;}exportclassTranscriptSegment{id:string;timestamp:string;speaker:string;text:string;isKeyPoint:boolean;}CourseSummary提供当前课堂上下文,TranscriptSegment用timestamp定位片段。speaker允许界面区分教师、同学或未知说话人,isKeyPoint则体现用户是否把它标为重点。
这里也能看到当前模型的真实边界:项目目前围绕“今日课程”运行,字幕没有单独保存courseId。如果未来支持多课程长期归档,就需要在数据库迁移中补充稳定外键,而不能只在页面里临时拼接课程标题。
二、Scan 不应该只保存 OCR 文本
扫描记录不仅要有 OCR 文本,还要说明内容标题、识别置信度和证据来源。当前项目模型如下:
exportclassScanNote{id:string;title:string;text:string;confidence:number;source:string;}其中text是可编辑的识别结果,confidence支持页面提示识别可信程度,source保存“板书/讲义 + 证据时间”等来源信息。原始拍摄图片由扫描服务流程管理,并没有直接塞进这个轻量领域对象。
实践中还要考虑:
- 图像文件不存在时显示占位与说明;
- OCR 失败时仍允许保存原图并稍后重试;
- 删除扫描记录时同步处理文件引用,避免产生孤儿文件;
- 导出时明确是否包含原始图片,而不是只导出文本。
三、Task 是行动项,也要保留课堂上下文
任务模型描述标题、截止时间、确认状态、完成状态和来源文案:
exportclassTaskItem{id:string;title:string;dueText:string;source:string;confirmed:boolean;completed:boolean;dueAtMillis:number;}source让任务卡片可以显示它来自课堂字幕还是板书证据,confirmed则把候选任务与正式任务分开。dueText负责保留用户看到的自然语言,dueAtMillis用于排序、逾期判断和日历分组。
当前source仍是字符串,不是结构化外键。它适合现阶段本地单课程原型,但还不能可靠地级联检查来源证据是否被删除。后续若需要完整追溯,应增加sourceType、sourceId和courseId,并同步修改表结构与迁移脚本。
四、时间统一用可计算值,展示交给页面
领域模型使用Millis数值,而不是直接保存“今天 18:00”“周三提交”这类展示文本。原因是:
- 排序和区间筛选需要统一的时间基准;
- “今天”“明天”“已逾期”会随当前时间变化;
- 手机、平板以及不同语言环境可能采用不同展示格式;
- 夏令时和时区转换不应由页面文案反向解析。
Service 可以基于dueAtMillis计算任务分组,页面再使用本地化格式显示。测试时应把当前时间作为可注入参数,避免依赖真实系统时钟导致用例在午夜前后随机失败。
五、Snapshot 是为页面服务的只读聚合
事实模型稳定之后,可以为具体页面定义快照,例如复习页快照和任务中心快照:
exportclassReviewSnapshot{completionPercent:number;masteryPercent:number;keyPointCount:number;scanCount:number;pendingCount:number;items:Array<ReviewTimelineItem>;}exportclassTaskCenterSnapshot{totalCount:number;pendingCount:number;completedCount:number;overdueCount:number;items:Array<TaskCenterItem>;calendarDays:Array<TaskCalendarDay>;}Snapshot 可以随着页面需求调整,但不应该反过来污染数据库表。例如任务卡片的按钮文字、背景色和展开状态不属于TaskItem,它们应由页面根据状态和 theme token 决定。
六、导出模型要显式表达隐私边界
课堂数据可能包含敏感对话、师生姓名和拍摄图片。听见课堂的导出模型应明确包含哪些内容,并对原始音频保持显式开关:
exportclassClassroomDataExport{schemaVersion:number;exportedAt:string;storageMode:string;course:CourseSummary;transcript:Array<TranscriptSegment>;scans:Array<ScanNote>;tasks:Array<TaskItem>;rawAudioIncluded:boolean;}如果当前版本没有保存原始音频,rawAudioIncluded就必须为false。不能因为产品展示了实时字幕,就在文案中暗示后台保存了完整录音。
同样,导出流程要由用户主动触发,并在导出前说明范围、目标位置和失败结果。日志中不应打印完整字幕、图片路径或个人信息。
七、模型演进要和数据库迁移同步
当后续增加“证据置信度”“用户修订历史”“跨课程任务”等字段时,不能只修改 ArkTS 接口。至少要同步处理:
- RelationalStore schema 版本;
- 旧数据默认值与迁移语句;
- Repository 的行映射;
- Service 的聚合规则;
- 导出格式兼容性;
- 空数据、脏数据和回滚策略。
如果后续把source拆成结构化来源类型,也要为未知旧值保留安全降级,避免升级后读取历史任务直接崩溃。
八、验证模型不能只看“能编译”
领域模型的验证应覆盖:
| 场景 | 需要确认的结果 |
|---|---|
| 空课程 | 页面进入空态,不构造虚假统计 |
| 未确认任务 | 只进入候选区,不直接进入正式任务中心 |
| OCR 失败 | 原图仍可保存,识别文本可为空 |
| 截止时间跨日 | 今日、逾期、未来分组稳定 |
| 删除来源证据 | 当前字符串来源的限制被明确提示;结构化关联后再验证级联策略 |
| 旧版本升级 | 缺失字段有默认值,历史数据可读取 |
| 数据导出 | 导出范围与隐私说明一致 |
这些用例适合在 Service 与 Repository 层测试,再通过真机验证文件、数据库与生命周期行为。页面截图无法证明数据关联一定正确。
总结
好的领域模型不会追着页面布局变化,而是稳定表达业务事实。听见课堂用Course提供上下文,用Transcript和Scan记录证据,用Task承接行动,再用 Snapshot 为不同页面聚合视图。
下一篇将回到 UI 外壳,拆解 12 个页面如何通过集中式RouteId、手机底部导航和平板侧栏保持一致的导航语义。