RFC:UniSpeaking 原始数据库实体关系设计
一、提议摘要
UniSpeaking 当前需要支持以下核心数据:
用户账号与学习画像;
用户创建的练习场景;
场景对应的单词、短语和句子学习资产;
用户每一次真实练习过程;
练习产生的评分结果;
调用实时语音、音素评分和文本模型时产生的供应商用量。
本 RFC 提议先建立以下十个核心实体:
用户
学习画像
用户自定义场景
学习资产
学习资产单词
学习资产短语
学习资产句子
练习会话
练习结果
供应商用量
本次设计遵循三个原则:
一个实体只负责一类清晰的数据。
复杂且需要整体展示的明细使用 JSONB。
需要查询、统计和绘制趋势图的数据使用普通字段。
本 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。当前阶段不拆分角色表和用户角色关联表,避免过度设计。
created_at
账号创建时间。
updated_at
账号资料最后更新时间。
4.4 本实体结论
用户实体保持简单,只保存账号和基础展示信息,不承担学习数据和练习数据。
五、学习画像实体
5.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
保存当前生效的英语等级:
该等级用于控制练习难度。
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 唯一约束
同一个学习资产中不允许出现重复单词:
该组合需要建立唯一约束。
不同学习资产可以包含相同单词。
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
当前支持:
通过明确字段表示练习类型,而不是完全依赖 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 字段,内部通过 reading 和 speaking 节点区分阶段。
数据库只保存业务和前端需要的标准化结果,不完整保存供应商原始响应。
初步设定:
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
发音表现综合分:
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_id 和 provider_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。
sync_error 保存脱敏后的同步错误。
fetched_at 表示成功获得最终用量数据的时间。
created_at 和 updated_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 自由对话最终分
当前场景练习总分包含任务完成度。
自由对话没有明确场景任务,因此暂定:
是否为自由对话制定另一套最终分公式,需要后续单独决定。
16.3 练习结果文字反馈
当前练习结果只确定评分字段和结果明细。
如果成绩单需要长期展示以下内容:
本次表现总结;
主要优点;
主要问题;
下一步建议;
则需要在练习结果中增加文字反馈字段。
十七、最终结论
本 RFC 将 UniSpeaking 当前数据库划分为三组清晰的数据:
场景与学习内容
用户自定义场景
学习资产
学习资产单词
学习资产短语
学习资产句子
该设计的核心价值是:
账号、学习内容、练习过程和评分结果各自独立。
每次复练都形成新的练习会话,历史数据语义清晰。
评分基础维度使用普通字段,支持雷达图和趋势分析。
句子、单词和音素等复杂明细使用 JSONB,避免过度拆表。
一次练习可以记录多个供应商和模型的调用用量。
当前结构保持简单,同时为后续业务扩展保留空间。
本 RFC 通过后,可继续进入以下讨论:
字段是否允许为空
默认值
CHECK 约束
外键删除行为
索引设计
建表 SQL
RFC:UniSpeaking 原始数据库实体关系设计
一、提议摘要
UniSpeaking 当前需要支持以下核心数据:
本 RFC 提议先建立以下十个核心实体:
本次设计遵循三个原则:
本 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_USAGE3.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 --- USER4.3 属性说明
id用户在系统内部的唯一标识。
使用 UUID,可以避免依赖连续数字,也方便不同服务独立生成 ID。
username用户登录名。
必须唯一且不能为空,同一用户名不能创建多个账号。
password保存密码哈希,不保存明文密码。
字段名称暂时保留为
password,但业务代码必须明确它保存的是加密后的哈希结果。nickname用户在客户端显示的名称。
昵称和登录名职责不同,昵称可以用于页面展示,登录名用于登录和唯一识别。
role当前只支持:
普通用户默认使用
USER。当前阶段不拆分角色表和用户角色关联表,避免过度设计。created_at账号创建时间。
updated_at账号资料最后更新时间。
4.4 本实体结论
用户实体保持简单,只保存账号和基础展示信息,不承担学习数据和练习数据。
五、学习画像实体
5.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_PROFILE5.3 属性说明
user_id同时作为主键和外键,关联
users.id。这种设计可以直接保证一个用户最多只有一条学习画像。
preferred_voice为空表示用户没有主动选择音色,由系统使用默认音色。
这里保存的是 UniSpeaking 内部使用的音色标识。
preferred_ai_speech_wpm表示用户期望的目标语速。
范围 80 至 200。
preferences后续出现不值得单独建列的扩展偏好时,再加入 JSON 键。
使用 JSONB,是因为偏好内容可能逐步增加,不需要每增加一种偏好就立即修改表结构。
memory_text保存系统根据多次练习整理出的当前长期记忆。
它不是完整对话记录,也不是每次练习总结,而是较稳定的信息摘要。
cefr_level保存当前生效的英语等级:
该等级用于控制练习难度。
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_SCENE6.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_ASSET7.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_WORD8.3 属性说明
id单词记录的唯一标识。
learning_asset_id关联所属学习资产。
word保存单词本身。
phonetic保存当前采用的主要音标。
当前阶段不同时拆分美式音标和英式音标。
translation保存中文翻译或简短释义。
created_at单词记录创建时间。
updated_at单词记录最后更新时间。
8.4 唯一约束
同一个学习资产中不允许出现重复单词:
该组合需要建立唯一约束。
不同学习资产可以包含相同单词。
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_PHRASE9.3 属性说明
id短语记录的唯一标识。
learning_asset_id关联所属学习资产。
phrase保存短语本身。
phonetic保存短语的整体音标。
translation保存短语翻译。
created_at短语记录创建时间。
updated_at短语记录最后更新时间。
9.4 唯一约束
同一个学习资产中不允许出现重复短语:
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_SENTENCE10.3 属性说明
id句子记录的唯一标识。
learning_asset_id关联所属学习资产。
sentence保存参考句正文。
translation保存句子翻译。
rhythm_details保存参考句的节奏信息,例如:
初步设定:
枚举值说明:
使用 JSONB,是因为节奏信息具有嵌套结构,不适合拆成多个固定字段。
reading_details当前句子最近一次成功完成的跟读评分。
初步设定:
枚举值说明:
created_at句子记录创建时间。
updated_at句子记录最后更新时间。
10.4 唯一约束
同一个学习资产中不允许出现完全相同的参考句:
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_SESSION11.3 属性说明
id一次练习过程的唯一标识。
用户复练时创建新的 ID。
user_id发起练习的用户。
scene_id关联本次练习使用的自定义场景。
自由对话时允许为空。
practice_mode当前支持:
通过明确字段表示练习类型,而不是完全依赖
scene_id推断。建议保持以下一致性:
status当前状态建议包括:
用于区分会话尚未连接、进行中、正常结束、用户退出和系统失败。
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_RESULT12.3 属性说明
session_id同时作为主键和外键,关联
practice_sessions.id。这样可以直接保证一个练习会话最多只有一份结果。
result_details保存复杂的嵌套明细,包括:
使用一个统一 JSONB 字段,内部通过
reading和speaking节点区分阶段。数据库只保存业务和前端需要的标准化结果,不完整保存供应商原始响应。
初步设定:
accuracy_score整场说阶段的准确度汇总分。
fluency_score整场说阶段的流利度汇总分。
grammar_score整场说阶段的语法分。
vocabulary_score整场说阶段的词汇分。
naturalness_score整场说阶段的表达自然度分。
上述五个基础指标用于:
pronunciation_score发音表现综合分:
language_quality_score语言质量综合分:
goal_coverage_score用户是否覆盖场景要求的核心目标。
communication_effectiveness_score用户的表达是否有效,是否让对方理解并推动对话。
interaction_completion_score用户是否完成场景中的关键互动过程。
task_completion_score任务完成度综合分:
final_score场景练习最终总分:
自由对话没有明确任务目标,因此任务相关字段和最终总分可以为空。
created_at结果首次创建时间。
updated_at同一会话重新评分并覆盖结果时的更新时间。
12.4 为什么基础分和综合分都保存
综合分虽然可以通过基础分计算,但仍建议保存。
原因不是计算性能,而是评分公式未来可能调整。
保存当时生成的综合分,可以保证历史成绩不会因为新公式上线而变化。
12.5 本实体结论
练习结果采用:
十三、供应商用量实体
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_USAGE13.3 属性说明
身份与关联字段
id是用量记录主键。practice_session_id表示该调用属于哪一次练习。供应商识别字段
provider表示供应商。provider_task_id和provider_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。sync_error保存脱敏后的同步错误。fetched_at表示成功获得最终用量数据的时间。created_at和updated_at分别表示记录创建和最后更新时间。13.4 可空规则
供应商不一定返回所有字段,因此多数供应商专用字段允许为空。
必须区分:
13.5 本实体结论
供应商用量表先以完整观测需求为目标保留字段,待真实调用链路稳定后再决定是否精简。
十四、数据写入与删除规则
14.1 场景删除
删除自定义场景时:
14.2 场景复练
每次复练:
不复用旧会话。
14.3 练习结果更新
同一次练习会话重新评分时:
用户重新练习时,因为创建了新的会话,所以会形成新的练习结果。
14.4 供应商用量
同一次练习会话可以产生多条供应商用量记录。
这些记录用于区分不同模型、不同任务和不同调用。
十五、当前暂不纳入的设计
本 RFC 暂时不引入以下内容:
这些内容只有在出现明确业务需求后再单独讨论。
十六、待确认事项
16.1 供应商用量字段
当前字段来自完整观测需求,需要负责实时语音和供应商调用链路的开发人员确认:
16.2 自由对话最终分
当前场景练习总分包含任务完成度。
自由对话没有明确场景任务,因此暂定:
是否为自由对话制定另一套最终分公式,需要后续单独决定。
16.3 练习结果文字反馈
当前练习结果只确定评分字段和结果明细。
如果成绩单需要长期展示以下内容:
则需要在练习结果中增加文字反馈字段。
十七、最终结论
本 RFC 将 UniSpeaking 当前数据库划分为三组清晰的数据:
该设计的核心价值是:
本 RFC 通过后,可继续进入以下讨论: