Skip to content

Proposal: UniSpeaking 当前阶段数据库设计方案 #14

Description

@pionxe

RFC:UniSpeaking 原始数据库实体关系设计

一、提议摘要

UniSpeaking 当前需要支持以下核心数据:

  • 用户账号与学习画像;
  • 用户创建的练习场景;
  • 场景对应的单词、短语和句子学习资产;
  • 用户每一次真实练习过程;
  • 练习产生的评分结果;
  • 调用实时语音、音素评分和文本模型时产生的供应商用量。

本 RFC 提议先建立以下十个核心实体:

用户
学习画像
用户自定义场景
学习资产
学习资产单词
学习资产短语
学习资产句子
练习会话
练习结果
供应商用量

本次设计遵循三个原则:

  1. 一个实体只负责一类清晰的数据。
  2. 复杂且需要整体展示的明细使用 JSONB。
  3. 需要查询、统计和绘制趋势图的数据使用普通字段。

本 RFC 先确定原始 ER 结构和实体属性,不展开接口、索引优化、分库分表和具体代码实现。


二、为什么需要重新梳理数据库

当前 UniSpeaking 的业务已经不再只有“用户进行一次实时对话”。

完整场景练习包含:

学 → 读 → 说

其中:

  • “学”需要展示单词和短语;
  • “读”需要展示参考句并进行音素评分;
  • “说”需要进行实时场景对话,并保存句子级、单轮级和整场评分;
  • 每次练习还可能调用多个供应商和多个模型。

如果把这些数据都放进少数几张大表,会出现以下问题:

  • 场景内容、练习过程和评分结果混在一起;
  • 后续难以维护单词、短语和句子;
  • 难以查看用户历史练习;
  • 难以绘制准确度、流利度、语法、词汇和自然度雷达图;
  • 难以统计不同模型和供应商的用量。

因此,需要先从最原始的实体关系开始,明确每类数据应该放在哪里。


三、总体实体关系

3.1 总体说明

当前数据库围绕三条主线组织:

用户主线
用户 → 学习画像
用户 → 自定义场景
用户 → 练习会话
场景内容主线
自定义场景 → 学习资产
学习资产 → 单词、短语、句子
练习记录主线
练习会话 → 练习结果
练习会话 → 供应商用量

3.2 总 ER 图

本图只表达实体、联系和基数,不展示属性。

flowchart TB
    USER["用户"]
    USER_PROFILE["学习画像"]

    CUSTOM_SCENE["用户自定义场景"]
    LEARNING_ASSET["学习资产"]
    ASSET_WORD["学习资产单词"]
    ASSET_PHRASE["学习资产短语"]
    ASSET_SENTENCE["学习资产句子"]

    PRACTICE_SESSION["练习会话"]
    PRACTICE_RESULT["练习结果"]
    PROVIDER_USAGE["供应商用量"]

    HAS_PROFILE{"拥有画像"}
    CREATES_SCENE{"创建场景"}
    HAS_ASSET{"配置学习资产"}

    CONTAINS_WORD{"包含单词"}
    CONTAINS_PHRASE{"包含短语"}
    CONTAINS_SENTENCE{"包含句子"}

    STARTS_SESSION{"进行练习"}
    USES_SCENE{"基于场景"}
    HAS_RESULT{"形成结果"}
    PRODUCES_USAGE{"产生用量"}

    USER ---|1| HAS_PROFILE
    HAS_PROFILE ---|0..1| USER_PROFILE

    USER ---|1| CREATES_SCENE
    CREATES_SCENE ---|0..N| CUSTOM_SCENE

    CUSTOM_SCENE ---|1| HAS_ASSET
    HAS_ASSET ---|1| LEARNING_ASSET

    LEARNING_ASSET ---|1| CONTAINS_WORD
    CONTAINS_WORD ---|0..N| ASSET_WORD

    LEARNING_ASSET ---|1| CONTAINS_PHRASE
    CONTAINS_PHRASE ---|0..N| ASSET_PHRASE

    LEARNING_ASSET ---|1| CONTAINS_SENTENCE
    CONTAINS_SENTENCE ---|0..N| ASSET_SENTENCE

    USER ---|1| STARTS_SESSION
    STARTS_SESSION ---|0..N| PRACTICE_SESSION

    CUSTOM_SCENE ---|0..1| USES_SCENE
    USES_SCENE ---|0..N| PRACTICE_SESSION

    PRACTICE_SESSION ---|1| HAS_RESULT
    HAS_RESULT ---|0..1| PRACTICE_RESULT

    PRACTICE_SESSION ---|1| PRODUCES_USAGE
    PRODUCES_USAGE ---|0..N| PROVIDER_USAGE
Loading

3.3 关系结论

当前关系含义如下:

  • 一个用户可以没有学习画像,也可以拥有一个学习画像。
  • 一个用户可以创建多个自定义场景。
  • 一个自定义场景对应一个学习资产。
  • 一个学习资产可以包含多个单词、短语和句子。
  • 一个用户可以进行多次练习。
  • 一次练习会话可以不基于场景,例如自由对话;也可以基于一个场景。
  • 一个场景可以被练习多次。
  • 一次练习会话最多形成一份练习结果。
  • 一次练习会话可以产生多条供应商用量记录。

四、用户实体

4.1 作用

用户实体只负责账号身份、登录信息和最基础的展示信息。

学习偏好、长期记忆和英语等级不放在用户实体中,而放在学习画像中,避免账号数据和学习数据混杂。

4.2 属性 ER 图

flowchart LR
    USER["用户"]

    U_ID(["id<br/>UUID<br/>PK"])
    U_USERNAME(["username<br/>VARCHAR 32<br/>UK,非空"])
    U_PASSWORD(["password<br/>VARCHAR 255<br/>密码哈希"])
    U_NICKNAME(["nickname<br/>VARCHAR 32"])
    U_ROLE(["role<br/>VARCHAR 16<br/>USER 或 ADMIN"])
    U_CREATED_AT(["created_at<br/>TIMESTAMPTZ"])
    U_UPDATED_AT(["updated_at<br/>TIMESTAMPTZ"])

    U_ID --- USER
    U_USERNAME --- USER
    U_PASSWORD --- USER
    U_NICKNAME --- USER
    U_ROLE --- USER
    U_CREATED_AT --- USER
    U_UPDATED_AT --- USER
Loading

4.3 属性说明

id

用户在系统内部的唯一标识。

使用 UUID,可以避免依赖连续数字,也方便不同服务独立生成 ID。

username

用户登录名。

必须唯一且不能为空,同一用户名不能创建多个账号。

password

保存密码哈希,不保存明文密码。

字段名称暂时保留为 password,但业务代码必须明确它保存的是加密后的哈希结果。

nickname

用户在客户端显示的名称。

昵称和登录名职责不同,昵称可以用于页面展示,登录名用于登录和唯一识别。

role

当前只支持:

USER
ADMIN

普通用户默认使用 USER。当前阶段不拆分角色表和用户角色关联表,避免过度设计。

created_at

账号创建时间。

updated_at

账号资料最后更新时间。

4.4 本实体结论

用户实体保持简单,只保存账号和基础展示信息,不承担学习数据和练习数据。


五、学习画像实体

5.1 作用

学习画像用于保存用户当前的学习偏好、长期记忆和英语等级。

用户刚注册时可能还没有形成画像,因此用户与学习画像是:

1 → 0..1

5.2 属性 ER 图

flowchart LR
    USER_PROFILE["学习画像"]

    UP_USER_ID(["user_id<br/>UUID<br/>PK,FK:users.id"])
    UP_VOICE(["preferred_voice<br/>VARCHAR 64<br/>可空"])
    UP_WPM(["preferred_ai_speech_wpm<br/>SMALLINT<br/>默认 140"])
    UP_PREFERENCES(["preferences<br/>JSONB<br/>默认空对象"])
    UP_MEMORY_TEXT(["memory_text<br/>TEXT"])
    UP_CEFR_LEVEL(["cefr_level<br/>VARCHAR 4"])
    UP_CREATED_AT(["created_at<br/>TIMESTAMPTZ"])
    UP_UPDATED_AT(["updated_at<br/>TIMESTAMPTZ"])

    UP_USER_ID --- USER_PROFILE
    UP_VOICE --- USER_PROFILE
    UP_WPM --- USER_PROFILE
    UP_PREFERENCES --- USER_PROFILE
    UP_MEMORY_TEXT --- USER_PROFILE
    UP_CEFR_LEVEL --- USER_PROFILE
    UP_CREATED_AT --- USER_PROFILE
    UP_UPDATED_AT --- USER_PROFILE
Loading

5.3 属性说明

user_id

同时作为主键和外键,关联 users.id

这种设计可以直接保证一个用户最多只有一条学习画像。

preferred_voice

为空表示用户没有主动选择音色,由系统使用默认音色。

这里保存的是 UniSpeaking 内部使用的音色标识。

preferred_ai_speech_wpm

表示用户期望的目标语速。
范围 80 至 200。

preferences

后续出现不值得单独建列的扩展偏好时,再加入 JSON 键。

使用 JSONB,是因为偏好内容可能逐步增加,不需要每增加一种偏好就立即修改表结构。

memory_text

保存系统根据多次练习整理出的当前长期记忆。

它不是完整对话记录,也不是每次练习总结,而是较稳定的信息摘要。

cefr_level

保存当前生效的英语等级:

A1
A2
B1
B2
C1
C2

该等级用于控制练习难度。

created_at

画像创建时间。

updated_at

画像最后更新时间。

5.4 本实体结论

学习画像保存用户当前有效的学习状态,不承担完整历史记录。


六、用户自定义场景实体

6.1 作用

用户自定义场景用于描述一次场景练习的背景、双方角色、学习目标和额外要求。

它是场景练习的业务入口,也是学习资产和练习会话的来源。

6.2 属性 ER 图

flowchart LR
    CUSTOM_SCENE["用户自定义场景"]

    CS_ID(["id<br/>UUID<br/>PK"])
    CS_USER_ID(["user_id<br/>UUID<br/>FK:users.id"])
    CS_TITLE(["title<br/>VARCHAR 128"])
    CS_BACKGROUND(["background<br/>TEXT"])
    CS_AI_ROLE(["ai_role<br/>VARCHAR 128"])
    CS_USER_ROLE(["user_role<br/>VARCHAR 128"])
    CS_LEARNING_GOAL(["learning_goal<br/>TEXT"])
    CS_CUSTOM_INSTRUCTION(["custom_instruction<br/>TEXT"])
    CS_CREATED_AT(["created_at<br/>TIMESTAMPTZ"])
    CS_UPDATED_AT(["updated_at<br/>TIMESTAMPTZ"])
    CS_DELETED_AT(["deleted_at<br/>TIMESTAMPTZ"])

    CS_ID --- CUSTOM_SCENE
    CS_USER_ID --- CUSTOM_SCENE
    CS_TITLE --- CUSTOM_SCENE
    CS_BACKGROUND --- CUSTOM_SCENE
    CS_AI_ROLE --- CUSTOM_SCENE
    CS_USER_ROLE --- CUSTOM_SCENE
    CS_LEARNING_GOAL --- CUSTOM_SCENE
    CS_CUSTOM_INSTRUCTION --- CUSTOM_SCENE
    CS_CREATED_AT --- CUSTOM_SCENE
    CS_UPDATED_AT --- CUSTOM_SCENE
    CS_DELETED_AT --- CUSTOM_SCENE
Loading

6.3 属性说明

id

自定义场景的唯一标识。

user_id

场景所有者,关联 users.id

一个用户可以创建多个场景,但一个场景只能属于一个用户。

title

场景名称,例如:

酒店办理入住
英文技术面试
餐厅处理点餐错误

background

描述对话发生的背景和上下文。

ai_role

描述 AI 在场景中扮演的角色。

user_role

描述用户在场景中扮演的角色。

learning_goal

描述用户希望在本场景中练习和完成的目标。

custom_instruction

保存额外要求,例如回复长度、纠错时机和对话难度。

created_at

场景创建时间。

updated_at

场景最后更新时间。

deleted_at

场景软删除时间。

为空表示场景正常存在,有值表示场景已被用户删除。

场景被软删除后,历史练习会话仍然保留原有 scene_id

6.4 场景修改约束

当前练习会话不保存场景快照,因此需要约定:

  • 场景尚未被练习使用时,可以直接修改;
  • 场景已经产生练习会话后,不直接修改核心内容;
  • 用户希望修改时,创建一个新场景;
  • 删除场景时只更新 deleted_at

这样可以避免旧练习的场景语义发生变化。

6.5 本实体结论

自定义场景保存场景定义,但不直接保存学习内容和练习结果。


七、学习资产实体

7.1 作用

学习资产是一个场景对应的“学—读”内容集合。

它本身不保存具体单词、短语和句子,而是作为三类学习内容的统一父实体。

7.2 属性 ER 图

flowchart LR
    LEARNING_ASSET["学习资产"]

    LA_ID(["id<br/>UUID<br/>PK"])
    LA_SCENE_ID(["scene_id<br/>UUID<br/>FK,UK"])
    LA_CREATED_AT(["created_at<br/>TIMESTAMPTZ"])
    LA_UPDATED_AT(["updated_at<br/>TIMESTAMPTZ"])

    LA_ID --- LEARNING_ASSET
    LA_SCENE_ID --- LEARNING_ASSET
    LA_CREATED_AT --- LEARNING_ASSET
    LA_UPDATED_AT --- LEARNING_ASSET
Loading

7.3 属性说明

id

学习资产自身的唯一标识。

虽然学习资产和场景是一对一关系,但仍保留独立 id,这样单词、短语和句子统一关联 learning_asset_id,语义更清晰。

scene_id

关联 custom_scenes.id

添加唯一约束,保证一个场景最多只有一个学习资产。

created_at

学习资产创建时间。

updated_at

学习资产最后更新时间。

7.4 本实体结论

学习资产作为内容集合根节点,将场景和具体学习内容分开。


八、学习资产单词实体

8.1 作用

学习资产单词保存“学”阶段需要展示的单词、音标和翻译。

单词不进行跟读和评分,因此不保存用户得分。

8.2 属性 ER 图

flowchart LR
    ASSET_WORD["学习资产单词"]

    AW_ID(["id<br/>UUID<br/>PK"])
    AW_ASSET_ID(["learning_asset_id<br/>UUID<br/>FK,组合 UK"])
    AW_WORD(["word<br/>VARCHAR 64<br/>组合 UK"])
    AW_PHONETIC(["phonetic<br/>VARCHAR 128"])
    AW_TRANSLATION(["translation<br/>TEXT"])
    AW_CREATED_AT(["created_at<br/>TIMESTAMPTZ"])
    AW_UPDATED_AT(["updated_at<br/>TIMESTAMPTZ"])

    AW_ID --- ASSET_WORD
    AW_ASSET_ID --- ASSET_WORD
    AW_WORD --- ASSET_WORD
    AW_PHONETIC --- ASSET_WORD
    AW_TRANSLATION --- ASSET_WORD
    AW_CREATED_AT --- ASSET_WORD
    AW_UPDATED_AT --- ASSET_WORD
Loading

8.3 属性说明

id

单词记录的唯一标识。

learning_asset_id

关联所属学习资产。

word

保存单词本身。

phonetic

保存当前采用的主要音标。

当前阶段不同时拆分美式音标和英式音标。

translation

保存中文翻译或简短释义。

created_at

单词记录创建时间。

updated_at

单词记录最后更新时间。

8.4 唯一约束

同一个学习资产中不允许出现重复单词:

learning_asset_id + word

该组合需要建立唯一约束。

不同学习资产可以包含相同单词。

8.5 本实体结论

单词实体只负责静态学习内容,不保存用户学习进度和评分。


九、学习资产短语实体

9.1 作用

学习资产短语保存“学”阶段需要展示的常用表达。

它和单词类似,但内容长度更长,因此字段长度也相应增加。

9.2 属性 ER 图

flowchart LR
    ASSET_PHRASE["学习资产短语"]

    AP_ID(["id<br/>UUID<br/>PK"])
    AP_ASSET_ID(["learning_asset_id<br/>UUID<br/>FK,组合 UK"])
    AP_PHRASE(["phrase<br/>VARCHAR 255<br/>组合 UK"])
    AP_PHONETIC(["phonetic<br/>VARCHAR 255"])
    AP_TRANSLATION(["translation<br/>TEXT"])
    AP_CREATED_AT(["created_at<br/>TIMESTAMPTZ"])
    AP_UPDATED_AT(["updated_at<br/>TIMESTAMPTZ"])

    AP_ID --- ASSET_PHRASE
    AP_ASSET_ID --- ASSET_PHRASE
    AP_PHRASE --- ASSET_PHRASE
    AP_PHONETIC --- ASSET_PHRASE
    AP_TRANSLATION --- ASSET_PHRASE
    AP_CREATED_AT --- ASSET_PHRASE
    AP_UPDATED_AT --- ASSET_PHRASE
Loading

9.3 属性说明

id

短语记录的唯一标识。

learning_asset_id

关联所属学习资产。

phrase

保存短语本身。

phonetic

保存短语的整体音标。

translation

保存短语翻译。

created_at

短语记录创建时间。

updated_at

短语记录最后更新时间。

9.4 唯一约束

同一个学习资产中不允许出现重复短语:

learning_asset_id + phrase

9.5 本实体结论

短语实体只承担学习展示,不承担跟读和评分。


十、学习资产句子实体

10.1 作用

学习资产句子用于“读”阶段。

用户会跟读这些固定参考句,因此句子除了正文和翻译外,还需要保存参考节奏信息。

用户的实际跟读评分不放在句子实体中,而放在具体练习结果中。

10.2 属性 ER 图

flowchart LR
    ASSET_SENTENCE["学习资产句子"]

    AS_ID(["id<br/>UUID<br/>PK"])
    AS_ASSET(["learning_asset_id<br/>UUID<br/>FK,组合 UK"])
    AS_SENTENCE(["sentence<br/>TEXT<br/>组合 UK"])
    AS_TRANSLATION(["translation<br/>TEXT"])
    AS_RHYTHM(["rhythm_details<br/>JSONB<br/>标准节奏提示"])
    AS_READING(["reading_details<br/>JSONB<br/>最新跟读评分"])
    AS_CREATED(["created_at<br/>TIMESTAMPTZ"])
    AS_UPDATED(["updated_at<br/>TIMESTAMPTZ"])

    AS_ID --- ASSET_SENTENCE
    AS_ASSET --- ASSET_SENTENCE
    AS_SENTENCE --- ASSET_SENTENCE
    AS_TRANSLATION --- ASSET_SENTENCE
    AS_RHYTHM --- ASSET_SENTENCE
    AS_READING --- ASSET_SENTENCE
    AS_CREATED --- ASSET_SENTENCE
    AS_UPDATED --- ASSET_SENTENCE
Loading

10.3 属性说明

id

句子记录的唯一标识。

learning_asset_id

关联所属学习资产。

sentence

保存参考句正文。

translation

保存句子翻译。

rhythm_details

保存参考句的节奏信息,例如:

  • 意群划分;
  • 重读单词;
  • 停顿位置;
  • 连读提示;
  • 弱读提示。

初步设定:

rhythm_details                                      # 固定参考句的标准朗读节奏提示,不保存用户评分
├── words[]                                         # 句子中的单词列表,按原句顺序排列
│   ├── index                                       # 单词序号,从 0 开始,供意群和语调结构引用
│   ├── text                                        # 当前单词文本,用于前端展示和数据校验
│   ├── start_char                                  # 单词在原始 sentence 中的起始字符位置,从 0 开始
│   ├── end_char                                    # 单词在原始 sentence 中的结束位置,采用左闭右开
│   ├── stress                                      # 标准重读等级:NONE、SECONDARY、PRIMARY
│   ├── weak_form                                   # 当前句中是否建议弱读,true 表示建议弱读
│   └── link_to_next                                # 是否建议与下一个单词连读,最后一个单词固定为 false
│
├── chunks[]                                        # 句子的标准意群划分
│   ├── start_word_index                            # 当前意群起始单词序号,包含该单词
│   ├── end_word_index                              # 当前意群结束单词序号,包含该单词
│   └── pause_after                                 # 意群结束后的建议停顿等级
│
└── intonation_segments[]                           # 句子不同部分的标准语调走势
    ├── start_word_index                            # 当前语调段起始单词序号
    ├── end_word_index                              # 当前语调段结束单词序号
    └── pattern                                     # 语调模式:LEVEL、RISING、FALLING、FALL_RISE、RISE_FALL

枚举值说明:

stress
├── NONE                                            # 不需要特别重读
├── SECONDARY                                       # 次重读,强调程度较弱
└── PRIMARY                                         # 主要重读,句子或意群中的重点词
pause_after
├── NONE                                            # 不需要明显停顿
├── SHORT                                           # 短停顿,普通意群边界
├── MEDIUM                                          # 中等停顿,常用于分句
└── LONG                                            # 明显停顿,用于较强语义分隔
pattern
├── LEVEL                                           # 整体语调相对平稳
├── RISING                                          # 语调逐渐上升
├── FALLING                                         # 语调逐渐下降
├── FALL_RISE                                       # 先降后升
└── RISE_FALL                                       # 先升后降

使用 JSONB,是因为节奏信息具有嵌套结构,不适合拆成多个固定字段。

reading_details

当前句子最近一次成功完成的跟读评分。

初步设定:

reading_details                                     # 当前句子最近一次成功跟读的评分结果,复练成功后整体覆盖
├── overall_score                                   # 整句话的综合跟读评分,范围 0 至 100
├── pronunciation_score                             # 整句话的发音质量评分,范围 0 至 100
├── fluency_score                                   # 整句话的流利度评分,反映卡顿和连续性
├── integrity_score                                 # 整句话的完整度评分,主要反映漏读情况
├── rhythm_score                                    # 用户实际节奏、停顿和重读表现的评分
├── ending_tone                                     # 讯飞检测出的句末语调,如 RISE、FALL
│
└── words[]                                         # 每个单词的实际跟读评分结果
    ├── index                                       # 单词在当前句子中的序号,从 0 开始
    ├── text                                        # 当前被评测的单词文本
    ├── read_status                                 # 单词朗读状态:NORMAL、INSERTION_BEFORE、OMITTED
    ├── overall_score                               # 当前单词的综合评分,范围 0 至 100
    ├── pronunciation_score                         # 当前单词的发音评分,范围 0 至 100
    ├── is_prominent                                # 是否检测到用户实际重读了该单词
    │
    └── phonemes[]                                  # 当前单词包含的音素评分列表
        ├── index                                   # 音素在当前单词中的序号,从 0 开始
        ├── symbol                                  # 被评测的参考音素符号,例如 k、ʊ、d
        └── pronunciation_score                     # 用户对该音素的发音评分,范围 0 至 100

枚举值说明:

read_status
├── NORMAL                                          # 当前单词正常朗读
├── INSERTION_BEFORE                                # 当前单词之前出现了额外读入内容
└── OMITTED                                         # 当前单词被用户漏读
ending_tone
├── RISE                                            # 句末升调
├── FALL                                            # 句末降调
├── LEVEL                                           # 句末语调相对平稳
└── UNKNOWN                                         # 供应商未返回或无法判断

created_at

句子记录创建时间。

updated_at

句子记录最后更新时间。

10.4 唯一约束

同一个学习资产中不允许出现完全相同的参考句:

learning_asset_id + sentence

10.5 本实体结论

学习资产句子保存标准学习内容,用户每次跟读产生的评分属于练习结果。


十一、练习会话实体

11.1 作用

练习会话表示用户从开始到结束的一次真实练习过程。

每次复练都创建新的 session_id,不复用旧会话。

一次场景练习可以包含:

学 → 读 → 说

自由对话则可以不关联场景。

11.2 属性 ER 图

flowchart LR
    PRACTICE_SESSION["练习会话"]

    PS_ID(["id<br/>UUID<br/>PK"])
    PS_USER_ID(["user_id<br/>UUID<br/>FK:users.id"])
    PS_SCENE_ID(["scene_id<br/>UUID<br/>FK,可空"])
    PS_MODE(["practice_mode<br/>VARCHAR 16"])
    PS_STATUS(["status<br/>VARCHAR 16"])
    PS_STARTED_AT(["started_at<br/>TIMESTAMPTZ"])
    PS_ENDED_AT(["ended_at<br/>TIMESTAMPTZ"])
    PS_CREATED_AT(["created_at<br/>TIMESTAMPTZ"])
    PS_UPDATED_AT(["updated_at<br/>TIMESTAMPTZ"])

    PS_ID --- PRACTICE_SESSION
    PS_USER_ID --- PRACTICE_SESSION
    PS_SCENE_ID --- PRACTICE_SESSION
    PS_MODE --- PRACTICE_SESSION
    PS_STATUS --- PRACTICE_SESSION
    PS_STARTED_AT --- PRACTICE_SESSION
    PS_ENDED_AT --- PRACTICE_SESSION
    PS_CREATED_AT --- PRACTICE_SESSION
    PS_UPDATED_AT --- PRACTICE_SESSION
Loading

11.3 属性说明

id

一次练习过程的唯一标识。

用户复练时创建新的 ID。

user_id

发起练习的用户。

scene_id

关联本次练习使用的自定义场景。

自由对话时允许为空。

practice_mode

当前支持:

FREE_CHAT
SCENE

通过明确字段表示练习类型,而不是完全依赖 scene_id 推断。

建议保持以下一致性:

FREE_CHAT → scene_id 为空
SCENE → scene_id 非空

status

当前状态建议包括:

CREATED
ACTIVE
COMPLETED
CANCELLED
FAILED

用于区分会话尚未连接、进行中、正常结束、用户退出和系统失败。

started_at

练习真正开始的时间。

它可能晚于记录创建时间。

ended_at

练习结束时间。

会话未结束时允许为空。

created_at

练习会话记录创建时间。

updated_at

练习会话最后更新时间。

11.4 不保存场景快照

练习会话不保存 scene_snapshot

为保证历史一致性,已经产生练习会话的场景不直接修改核心内容;需要调整时创建新场景。

11.5 本实体结论

练习会话只描述一次练习的身份、类型、状态和时间,不直接保存评分结果与供应商用量明细。


十二、练习结果实体

12.1 作用

练习结果保存一次练习最终形成的评分和展示明细。

一次练习会话最多对应一份结果。

同一会话重新评分时覆盖当前结果;用户复练时创建新的练习会话和新的结果。

12.2 属性 ER 图

flowchart TB
    PRACTICE_RESULT["练习结果"]

    PR_SESSION_ID(["session_id<br/>UUID<br/>PK,FK"])
    PR_DETAILS(["result_details<br/>JSONB"])

    PR_ACCURACY(["accuracy_score<br/>NUMERIC 5,2"])
    PR_FLUENCY(["fluency_score<br/>NUMERIC 5,2"])
    PR_GRAMMAR(["grammar_score<br/>NUMERIC 5,2"])
    PR_VOCABULARY(["vocabulary_score<br/>NUMERIC 5,2"])
    PR_NATURALNESS(["naturalness_score<br/>NUMERIC 5,2"])

    PR_PRONUNCIATION(["pronunciation_score<br/>NUMERIC 5,2"])
    PR_LANGUAGE(["language_quality_score<br/>NUMERIC 5,2"])

    PR_GOAL(["goal_coverage_score<br/>NUMERIC 5,2"])
    PR_COMMUNICATION(["communication_effectiveness_score<br/>NUMERIC 5,2"])
    PR_INTERACTION(["interaction_completion_score<br/>NUMERIC 5,2"])
    PR_TASK(["task_completion_score<br/>NUMERIC 5,2"])

    PR_FINAL(["final_score<br/>NUMERIC 5,2"])
    PR_CREATED_AT(["created_at<br/>TIMESTAMPTZ"])
    PR_UPDATED_AT(["updated_at<br/>TIMESTAMPTZ"])

    PR_SESSION_ID --- PRACTICE_RESULT
    PR_DETAILS --- PRACTICE_RESULT

    PR_ACCURACY --- PRACTICE_RESULT
    PR_FLUENCY --- PRACTICE_RESULT
    PR_GRAMMAR --- PRACTICE_RESULT
    PR_VOCABULARY --- PRACTICE_RESULT
    PR_NATURALNESS --- PRACTICE_RESULT

    PR_PRONUNCIATION --- PRACTICE_RESULT
    PR_LANGUAGE --- PRACTICE_RESULT

    PR_GOAL --- PRACTICE_RESULT
    PR_COMMUNICATION --- PRACTICE_RESULT
    PR_INTERACTION --- PRACTICE_RESULT
    PR_TASK --- PRACTICE_RESULT

    PR_FINAL --- PRACTICE_RESULT
    PR_CREATED_AT --- PRACTICE_RESULT
    PR_UPDATED_AT --- PRACTICE_RESULT
Loading

12.3 属性说明

session_id

同时作为主键和外键,关联 practice_sessions.id

这样可以直接保证一个练习会话最多只有一份结果。

result_details

保存复杂的嵌套明细,包括:

读阶段
- 参考句
- 跟读分数
- 单词分数
- 音素分数
- 错误类型

说阶段
- 用户发言气泡
- 气泡中的句子
- 每句评分
- 单词和音素明细
- 单轮评分

使用一个统一 JSONB 字段,内部通过 readingspeaking 节点区分阶段。

数据库只保存业务和前端需要的标准化结果,不完整保存供应商原始响应。

初步设定:

result_details                                     # 实时对话的逐气泡评分、反馈和整场评价依据
├── utterances[]                                   # 用户在本次实时对话中的所有发言气泡
│   ├── utterance_no                               # 用户发言气泡序号,从 1 开始
│   ├── transcript                                 # 实时语音模型或 ASR 返回的用户完整发言文本
│   │
│   ├── scores                                     # 当前发言气泡的多维评分
│   │   ├── overall_score                          # 当前气泡综合分,由发音表现和语言质量计算
│   │   ├── accuracy_score                         # 当前气泡的发音准确度评分
│   │   ├── fluency_score                          # 当前气泡的表达流利度评分
│   │   ├── grammar_score                          # 当前气泡的语法正确性评分
│   │   ├── vocabulary_score                       # 当前气泡的词汇使用质量评分
│   │   ├── naturalness_score                      # 当前气泡的表达自然度评分
│   │   ├── pronunciation_score                    # 当前气泡的综合发音表现分
│   │   └── language_quality_score                 # 当前气泡的综合语言质量分
│   │
│   ├── feedback                                   # 当前气泡的文本反馈和修改建议
│   │   ├── summary                                # 对当前发言表现的简短总结
│   │   ├── corrected_text                         # 仅纠正语法和明显错误后的完整表达
│   │   ├── natural_expression                     # 更自然、更符合英语习惯的推荐表达
│   │   │
│   │   ├── grammar_issues[]                       # 当前发言中识别出的语法问题
│   │   │   ├── original_text                      # 原始存在语法问题的文本片段
│   │   │   ├── corrected_text                     # 修改后的正确表达
│   │   │   └── explanation                        # 简短说明为什么需要修改
│   │   │
│   │   ├── vocabulary_suggestions[]               # 当前发言中的词汇改进建议
│   │   │   ├── original_text                      # 用户原本使用的单词或短语
│   │   │   ├── suggested_text                     # 推荐使用的单词或短语
│   │   │   └── explanation                        # 简短说明推荐理由和使用差异
│   │   │
│   │   └── naturalness_suggestions[]              # 当前发言中不够自然的表达建议
│   │       ├── original_text                      # 用户原本使用的表达
│   │       ├── suggested_text                     # 更自然的英语表达
│   │       └── explanation                        # 简短说明原表达为何不自然
│   │
│   └── pronunciation_details                      # 当前气泡的发音评分明细
│       └── sentences[]                             # 将一个气泡内部按句切分后的发音结果
│           ├── sentence_no                         # 当前句在气泡中的顺序,从 1 开始
│           ├── transcript                          # 从用户气泡中切分出的原始句子文本
│           ├── reference_text                      # 最终提交给音素评测服务的参考文本
│           ├── accuracy_score                      # 当前句子的发音准确度评分
│           ├── fluency_score                       # 当前句子的流利度评分
│           │
│           └── words[]                             # 当前句子中的单词发音明细
│               ├── index                           # 单词在当前句子中的序号,从 0 开始
│               ├── text                            # 当前被评测的单词文本
│               ├── pronunciation_score             # 当前单词的发音评分
│               │
│               └── phonemes[]                      # 当前单词的音素评分列表
│                   ├── index                       # 音素在当前单词中的序号,从 0 开始
│                   ├── symbol                      # 被评测的参考音素符号
│                   └── pronunciation_score         # 用户对当前音素的发音评分
│
└── session_feedback                               # 对整场实时对话的总结和评分依据
    ├── summary                                     # 对整场练习表现的总体文字总结
    ├── strengths[]                                 # 用户本场表现较好的方面
    ├── improvements[]                              # 用户后续最值得改进的方面
    ├── goal_coverage_evidence[]                    # 支撑目标覆盖评分的对话证据
    ├── communication_effectiveness_evidence[]      # 支撑沟通有效性评分的对话证据
    └── interaction_completion_evidence[]           # 支撑互动完成度评分的对话证据

accuracy_score

整场说阶段的准确度汇总分。

fluency_score

整场说阶段的流利度汇总分。

grammar_score

整场说阶段的语法分。

vocabulary_score

整场说阶段的词汇分。

naturalness_score

整场说阶段的表达自然度分。

上述五个基础指标用于:

  • 评分雷达图;
  • 历史趋势;
  • 用户能力分析;
  • 排序和统计。

pronunciation_score

发音表现综合分:

准确度 × 60%
+
流利度 × 40%

language_quality_score

语言质量综合分:

语法 × 55%
+
词汇 × 25%
+
自然度 × 20%

goal_coverage_score

用户是否覆盖场景要求的核心目标。

communication_effectiveness_score

用户的表达是否有效,是否让对方理解并推动对话。

interaction_completion_score

用户是否完成场景中的关键互动过程。

task_completion_score

任务完成度综合分:

目标覆盖 × 40%
+
沟通有效性 × 30%
+
互动完成度 × 30%

final_score

场景练习最终总分:

发音表现 × 40%
+
语言质量 × 35%
+
任务完成度 × 25%

自由对话没有明确任务目标,因此任务相关字段和最终总分可以为空。

created_at

结果首次创建时间。

updated_at

同一会话重新评分并覆盖结果时的更新时间。

12.4 为什么基础分和综合分都保存

综合分虽然可以通过基础分计算,但仍建议保存。

原因不是计算性能,而是评分公式未来可能调整。

保存当时生成的综合分,可以保证历史成绩不会因为新公式上线而变化。

12.5 本实体结论

练习结果采用:

普通字段
保存需要查询和统计的汇总分数

JSONB
保存数量不固定、层级较深、主要整体展示的评分明细

十三、供应商用量实体

13.1 作用

供应商用量记录一次练习过程中产生的模型调用、任务状态、耗时和 Token 用量。

一次练习会话可能调用:

  • 实时语音模型;
  • 音素评分模型;
  • 语法和词汇评分模型;
  • 总结模型。

因此练习会话和供应商用量是一对多关系。

当前字段先完整保留,等待负责复杂调用链路的开发人员根据真实协议进一步确认和精简。

13.2 属性 ER 图

flowchart TB
    PROVIDER_USAGE["供应商用量"]

    PU_ID(["id<br/>UUID<br/>PK"])
    PU_SESSION_ID(["practice_session_id<br/>UUID<br/>FK"])

    PU_PROVIDER(["provider<br/>VARCHAR 32"])
    PU_TASK_ID(["provider_task_id<br/>VARCHAR 128"])
    PU_REQUEST_ID(["provider_request_id<br/>VARCHAR 128"])
    PU_API_KEY_ID(["api_key_id<br/>VARCHAR 64"])
    PU_WORKSPACE_ID(["workspace_id<br/>VARCHAR 128"])
    PU_PROVIDER_USER_ID(["provider_user_id<br/>VARCHAR 64"])
    PU_MODEL(["model<br/>VARCHAR 128"])
    PU_CHANNEL(["channel<br/>VARCHAR 32"])
    PU_METADATA(["provider_metadata<br/>JSONB"])
    PU_STATUS_CODE(["provider_status_code<br/>INTEGER"])

    PU_PROVIDER_STARTED_AT(["provider_started_at<br/>TIMESTAMPTZ"])
    PU_DURATION(["duration_ms<br/>BIGINT"])
    PU_FIRST_OUTPUT(["first_output_duration_ms<br/>BIGINT"])

    PU_INPUT_TEXT(["input_text_tokens<br/>BIGINT"])
    PU_INPUT_AUDIO(["input_audio_tokens<br/>BIGINT"])
    PU_INPUT_TOTAL(["input_tokens<br/>BIGINT"])

    PU_OUTPUT_TEXT(["output_text_tokens<br/>BIGINT"])
    PU_OUTPUT_AUDIO(["output_audio_tokens<br/>BIGINT"])
    PU_OUTPUT_TOTAL(["output_tokens<br/>BIGINT"])
    PU_TOTAL(["total_tokens<br/>BIGINT"])

    PU_SYNC_STATUS(["sync_status<br/>VARCHAR 16"])
    PU_SYNC_ERROR(["sync_error<br/>VARCHAR 512"])
    PU_FETCHED_AT(["fetched_at<br/>TIMESTAMPTZ"])
    PU_CREATED_AT(["created_at<br/>TIMESTAMPTZ"])
    PU_UPDATED_AT(["updated_at<br/>TIMESTAMPTZ"])

    PU_ID --- PROVIDER_USAGE
    PU_SESSION_ID --- PROVIDER_USAGE

    PU_PROVIDER --- PROVIDER_USAGE
    PU_TASK_ID --- PROVIDER_USAGE
    PU_REQUEST_ID --- PROVIDER_USAGE
    PU_API_KEY_ID --- PROVIDER_USAGE
    PU_WORKSPACE_ID --- PROVIDER_USAGE
    PU_PROVIDER_USER_ID --- PROVIDER_USAGE
    PU_MODEL --- PROVIDER_USAGE
    PU_CHANNEL --- PROVIDER_USAGE
    PU_METADATA --- PROVIDER_USAGE
    PU_STATUS_CODE --- PROVIDER_USAGE

    PU_PROVIDER_STARTED_AT --- PROVIDER_USAGE
    PU_DURATION --- PROVIDER_USAGE
    PU_FIRST_OUTPUT --- PROVIDER_USAGE

    PU_INPUT_TEXT --- PROVIDER_USAGE
    PU_INPUT_AUDIO --- PROVIDER_USAGE
    PU_INPUT_TOTAL --- PROVIDER_USAGE

    PU_OUTPUT_TEXT --- PROVIDER_USAGE
    PU_OUTPUT_AUDIO --- PROVIDER_USAGE
    PU_OUTPUT_TOTAL --- PROVIDER_USAGE
    PU_TOTAL --- PROVIDER_USAGE

    PU_SYNC_STATUS --- PROVIDER_USAGE
    PU_SYNC_ERROR --- PROVIDER_USAGE
    PU_FETCHED_AT --- PROVIDER_USAGE
    PU_CREATED_AT --- PROVIDER_USAGE
    PU_UPDATED_AT --- PROVIDER_USAGE
Loading

13.3 属性说明

身份与关联字段

id 是用量记录主键。

practice_session_id 表示该调用属于哪一次练习。

供应商识别字段

provider 表示供应商。

provider_task_idprovider_request_id 保存供应商侧的任务和请求标识。

api_key_id 只保存 API Key 的内部标识,不保存密钥明文。

workspace_id 保存供应商工作区。

provider_user_id 保存供应商侧用户标识。

model 保存实际调用的模型。

channel 保存调用渠道,例如 WebRTC、WebSocket 或普通 HTTP。

扩展与状态字段

provider_metadata 保存协议、资源 ID、区域和音频格式等少量扩展信息。

不保存完整供应商响应、大段日志和密钥。

provider_status_code 保存供应商返回的状态码。

耗时字段

provider_started_at 表示供应商任务开始时间。

duration_ms 表示总耗时。

first_output_duration_ms 表示首次收到输出的耗时,用于分析实时体验。

Token 字段

分别保存文本和音频的输入、输出及总 Token。

不同供应商的统计口径可能不同,因此暂时不建立强制加总约束。

同步字段

sync_status 当前支持:

PENDING
COMPLETED
FAILED

默认状态为 PENDING

sync_error 保存脱敏后的同步错误。

fetched_at 表示成功获得最终用量数据的时间。

created_atupdated_at 分别表示记录创建和最后更新时间。

13.4 可空规则

供应商不一定返回所有字段,因此多数供应商专用字段允许为空。

必须区分:

NULL
供应商未返回、尚未同步或不适用

0
供应商明确返回数值为零

13.5 本实体结论

供应商用量表先以完整观测需求为目标保留字段,待真实调用链路稳定后再决定是否精简。


十四、数据写入与删除规则

14.1 场景删除

删除自定义场景时:

更新 deleted_at
不物理删除
不清空练习会话中的 scene_id

14.2 场景复练

每次复练:

创建新的 practice_session
创建新的 session_id

不复用旧会话。

14.3 练习结果更新

同一次练习会话重新评分时:

覆盖当前 practice_result
不创建评分历史版本

用户重新练习时,因为创建了新的会话,所以会形成新的练习结果。

14.4 供应商用量

同一次练习会话可以产生多条供应商用量记录。

这些记录用于区分不同模型、不同任务和不同调用。


十五、当前暂不纳入的设计

本 RFC 暂时不引入以下内容:

  • 场景版本表;
  • 场景快照;
  • 单词和短语掌握进度;
  • 独立的用户发言表;
  • 独立的口语句子评分表;
  • 独立的单轮评分表;
  • 评分历史版本;
  • 角色表和用户角色关联表;
  • 供应商原始响应完整存储;
  • 分表、分库和归档策略。

这些内容只有在出现明确业务需求后再单独讨论。


十六、待确认事项

16.1 供应商用量字段

当前字段来自完整观测需求,需要负责实时语音和供应商调用链路的开发人员确认:

  • 哪些供应商字段实际能够获取;
  • 一条记录对应一次请求、一个任务还是一次结算单元;
  • 哪些字段需要唯一约束;
  • Token 总量字段的具体口径。

16.2 自由对话最终分

当前场景练习总分包含任务完成度。

自由对话没有明确场景任务,因此暂定:

任务相关字段为空
final_score 为空

是否为自由对话制定另一套最终分公式,需要后续单独决定。

16.3 练习结果文字反馈

当前练习结果只确定评分字段和结果明细。

如果成绩单需要长期展示以下内容:

  • 本次表现总结;
  • 主要优点;
  • 主要问题;
  • 下一步建议;

则需要在练习结果中增加文字反馈字段。


十七、最终结论

本 RFC 将 UniSpeaking 当前数据库划分为三组清晰的数据:

用户数据
用户
学习画像
场景与学习内容
用户自定义场景
学习资产
学习资产单词
学习资产短语
学习资产句子
练习与观测数据
练习会话
练习结果
供应商用量

该设计的核心价值是:

  1. 账号、学习内容、练习过程和评分结果各自独立。
  2. 每次复练都形成新的练习会话,历史数据语义清晰。
  3. 评分基础维度使用普通字段,支持雷达图和趋势分析。
  4. 句子、单词和音素等复杂明细使用 JSONB,避免过度拆表。
  5. 一次练习可以记录多个供应商和模型的调用用量。
  6. 当前结构保持简单,同时为后续业务扩展保留空间。

本 RFC 通过后,可继续进入以下讨论:

字段是否允许为空
默认值
CHECK 约束
外键删除行为
索引设计
建表 SQL

Metadata

Metadata

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions