
## 一、数据库概览

| 数据库 | 类型 | 连接地址 | 端口 | 角色 |
|--------|------|----------|------|------|
| `vala_bi` | PostgreSQL | bj-postgres-16pob4sg.sql.tencentcdb.com | 28591 | BI 数据（订单、用户、学习行为） |
| `vala` / `vala_user` / `vala_order` | MySQL | bj-cdb-dh2fkqa0.sql.tencentcdb.com | 27751 | 配置数据 + 用户行为源数据 |

> 🔔 **所有数据库操作均为只读，禁止写入。**

---

## 二、PostgreSQL `vala_bi` 库

### 📦 bi_vala_order（订单表）

> 🔔 **金额计算统一使用 `pay_amount_int`（分），显示为元时除以 100。不要用 `pay_amount`（varchar 字符串）。**

| 字段 | 类型 | 释义 | 计算说明 |
|------|------|------|----------|
| `id` | bigint | 订单主键 | `COUNT(DISTINCT id)` = 订单数 |
| `account_id` | bigint | 关联用户账号 | JOIN `bi_vala_app_account.id` |
| `trade_no` | varchar | 交易流水号 | 关联退费表 `bi_refund_order.trade_no` |
| `out_trade_no` | varchar | 商户订单号 | 关联退费表 `bi_refund_order.out_trade_no` |
| `pay_amount_int` | integer | 支付金额（分） | **GMV/GSV 统计字段**，元 = 值/100 |
| `pay_amount` | varchar | 支付金额（元） | ⚠️ 字符串格式，不推荐使用 |
| `order_status` | integer | 订单状态 | 1=未支付 2=已支付 3=已完成 **4=已退款** 5=已取消 |
| `pay_success_date` | timestamptz | 支付成功时间 | 用于时间维度统计（新增付费用户日期基准） |
| `key_from` | text | 销售渠道标识 | 渠道映射见[渠道映射表](#九渠道映射表) |
| `sale_channel` | integer | 端内购课渠道编号 | 仅 `key_from=app-active-h5-0-0` 时有效 |
| `goods_name` | text | 商品名称 | 购买的课程/商品名称 |
| `pay_channel` | integer | 支付渠道 | 微信/支付宝等 |
| `is_used` | integer | 是否已使用 | 订单是否已被消费 |
| `quantity` | integer | 购买数量 | 默认 1 |
| `expire_days` | integer | 有效期天数 | 课程有效期 |
| `created_at` | timestamptz | 创建时间 | |
| `updated_at` | timestamptz | 更新时间 | |
| `deleted_at` | timestamptz | 删除时间 | NULL=未删除 |

**关键 JOIN 条件**：
```sql
-- 测试账号剔除（所有订单统计必须）
LEFT JOIN bi_vala_app_account a ON o.account_id = a.id AND a.status = 1

-- 退费关联（退费成功的订单）
LEFT JOIN bi_refund_order r
  ON o.trade_no = r.trade_no
  AND o.out_trade_no = r.out_trade_no
  AND r.status = 3          -- 退费成功
  AND r.deleted_at IS NULL
```

---

### 📦 bi_vala_app_account（用户账号表）

> 🔔 **所有涉及订单/用户的统计，必须 `WHERE status = 1` 剔除测试账号。`download_channel` 为汉字格式，用 `LIKE '%关键词%'` 匹配。**

| 字段 | 类型 | 释义 | 计算说明 |
|------|------|------|----------|
| `id` | bigint | 账号主键 | 被多方关联的关键字段 |
| `tel` | varchar | 手机号 | 明文 |
| `tel_encrypt` | varchar | 加密手机号 | |
| `name` | varchar | 用户姓名 | |
| `status` | smallint | 账号状态 | **1=正常用户，2=测试用户**。统计必须 `=1` |
| `key_from` | varchar | 注册来源 | 渠道标识 |
| `download_channel` | text | 下载渠道（汉字） | 新增用户分渠道统计字段，匹配用 `LIKE` |
| `pay_status` | integer | 付费状态 | |
| `login_times` | integer | 登录次数 | |
| `created_at` | timestamptz | 注册时间 | 新增注册用户统计的时间基准 |
| `updated_at` | timestamptz | 更新时间 | |
| `deleted_at` | timestamptz | 删除时间 | NULL=未删除 |
| `id_card` | varchar | 身份证 | ⚠️ 敏感信息 |
| `remark` | text | 备注 | |

---

### 📦 bi_vala_app_character（角色表）

> 🔔 **关联路径：`account_id` → `bi_vala_app_account.id`，`id`（=角色ID）→ 学习记录表的 `user_id`。**

| 字段 | 类型 | 释义 | 计算说明 |
|------|------|------|----------|
| `id` | bigint | 角色主键 | 关联学习记录表的 `user_id` |
| `account_id` | bigint | 关联账号 | JOIN `bi_vala_app_account.id` |
| `nickname` | varchar | 角色昵称 | |
| `gender` | smallint | 性别 | 0=girl 1=boy |
| `birthday` | varchar | 出生日期 | YYYY-MM-DD，`split_part(birthday,'-',1)` 取年份 |
| `pay_status` | smallint | 付费状态 | 0=未付费，1=已付费 |
| `status` | smallint | 角色状态 | |
| `latest_login` | timestamptz | 最后登录 | |
| `created_at` | timestamptz | 创建时间 | |
| `updated_at` | timestamptz | 更新时间 | |
| `deleted_at` | timestamptz | 删除时间 | NULL=未删除 |
| `avatar` | text | 头像 URL | |
| `total_sp_point` | integer | 总 SP 积分 | |
| `purchase_season_package` | text | 赛季包购买状态 | JSON 数组 |

---

### 📦 bi_refund_order（退费订单表）

> 🔔 **退费校验必须同时满足双条件：`status = 3` AND 关联订单 `order_status = 4`。缺一不可。**

| 字段 | 类型 | 释义 | 计算说明 |
|------|------|------|----------|
| `id` | bigint | 退费主键 | |
| `account_id` | bigint | 退费用户 | |
| `trade_no` | varchar | 交易流水号 | 关联 `bi_vala_order.trade_no` |
| `out_trade_no` | varchar | 商户订单号 | 关联 `bi_vala_order.out_trade_no` |
| `refund_amount_int` | integer | 退费金额（分） | 退费金额计算字段，元 = 值/100 |
| `refund_amount` | varchar | 退费金额（元） | ⚠️ 字符串格式 |
| `status` | integer | 退费状态 | **3 = 退费成功** |
| `reason` | text | 退费原因 | |
| `sale_channel` | integer | 销售渠道 | |
| `is_admin` | boolean | 是否管理员操作 | |
| `refund_type` | integer | 退费类型 | |
| `refund_method` | integer | 退费方式 | |
| `created_at` | timestamptz | 创建时间 | |
| `updated_at` | timestamptz | 更新时间 | |
| `deleted_at` | timestamptz | 删除时间 | NULL=未删除 |

---

### 📦 bi_user_course_detail（用户课程明细表）

> 🔔 **`expire_time` 不为空 = 正式课，为空 = 体验课。`deleted_at` 有值 = 已删除课程。**

| 字段 | 类型 | 释义 | 计算说明 |
|------|------|------|----------|
| `id` | integer | 主键 | |
| `account_id` | integer | 用户账号 ID | JOIN `bi_vala_app_account.id` |
| `user_id` | integer | 角色 ID | JOIN `bi_vala_app_character.id` |
| `course_level` | text | 课程等级 | L1 / L2 |
| `active_time` | timestamptz | 激活时间 | |
| `expire_time` | timestamptz | 过期时间 | **不为空=正式课，为空=体验课** |
| `latest_unit_index` | integer | 最新单元索引 | |
| `latest_lesson_index` | integer | 最新课时索引 | |
| `learn_duration` | integer | 学习时长 | |
| `last_learn_time` | timestamptz | 最后学习时间 | |
| `created_at` | timestamptz | 创建时间 | |
| `updated_at` | timestamptz | 更新时间 | |
| `deleted_at` | timestamptz | 删除时间 | 有值=已删除，统计时过滤 |

---

### 📦 bi_level_unit_lesson（课程结构映射表）

> 🔔 **`id` = `chapter_id`，直接关联播放记录表。课程匹配统一使用此表。示例：L1 S0 U00 L01 → id=343。**

| 字段 | 类型 | 释义 | 计算说明 |
|------|------|------|----------|
| `id` | integer | chapter_id | 关联播放记录表的 `chapter_id` |
| `course_level` | varchar | 课程等级 | L1 / L2（对应 A1/A2） |
| `course_season` | varchar | 季度 | S0 / S1 / S2 ... |
| `course_unit` | varchar | 单元 | U00 / U01 / U02 ... |
| `course_lesson` | varchar | 课时 | L01 / L02 / L03 / L04 / L05 |

**课程匹配 SQL 模板**：
```sql
-- 查某课程的学习数据，先通过此表拿到 chapter_id
SELECT id FROM bi_level_unit_lesson
WHERE course_level = 'L1' AND course_season = 'S0'
  AND course_unit = 'U00' AND course_lesson = 'L01';
-- 返回 id = 343，然后用 343 去查播放记录
```

---

### 📦 bi_user_chapter_play_record_0 ~ bi_user_chapter_play_record_7（课时播放记录，8 张分表）

> 🔔 **统计时必须用 `UNION ALL` 合并 8 张表。首次完成时间按 `user_id + chapter_id` 分组取 `MIN(created_at)`。`chapter_unique_id` 关联组件播放记录。**

| 字段 | 类型 | 释义 | 计算说明 |
|------|------|------|----------|
| `id` | bigint | 主键 | |
| `user_id` | bigint | 角色 ID | JOIN `bi_vala_app_character.id` |
| `chapter_id` | bigint | 课时 ID | JOIN `bi_level_unit_lesson.id` |
| `chapter_unique_id` | varchar | 完课唯一标识 | 关联 `bi_user_component_play_record.chapter_unique_id` |
| `play_status` | smallint | 播放状态 | **1=正常完成**，0=未开始，2=中途退出 |
| `created_at` | timestamptz | 创建时间 | 首次完成时间 = `MIN(created_at)` 分组 |
| `updated_at` | timestamptz | 更新时间 | |
| `deleted_at` | timestamptz | 删除时间 | NULL=未删除 |
| `story_id` | bigint | 故事 ID | |
| `level` | varchar | 等级 | |
| `map_position` | text | 地图位置 | |
| `dialog_info` | text | 对话信息 JSON | |

**分表合并模板**：
```sql
SELECT user_id, chapter_id, chapter_unique_id, play_status,
       MIN(created_at) AS first_finish_time
FROM (
    SELECT * FROM bi_user_chapter_play_record_0 WHERE deleted_at IS NULL
    UNION ALL
    SELECT * FROM bi_user_chapter_play_record_1 WHERE deleted_at IS NULL
    UNION ALL
    -- ... 2~7 同理
    SELECT * FROM bi_user_chapter_play_record_7 WHERE deleted_at IS NULL
) AS all_records
WHERE play_status = 1  -- 正常完成
GROUP BY user_id, chapter_id, chapter_unique_id, play_status
```

---

### 📦 bi_user_component_play_record_0 ~ bi_user_component_play_record_7（组件播放记录，8 张分表）

> 🔔 **`interval_time` 求和得课时总耗时（毫秒），÷60000 = 分钟。退出率计算：有效参与 = `play_status IN (1,2)`，退出 = `play_status=2`。`component_unique_code` 用于组件维度分组。**

| 字段 | 类型 | 释义 | 计算说明 |
|------|------|------|----------|
| `id` | bigint | 主键 | |
| `user_id` | bigint | 角色 ID | |
| `chapter_id` | bigint | 课时 ID | |
| `chapter_unique_id` | varchar | 完课唯一标识 | 关联课时播放记录 |
| `component_unique_code` | varchar | 组件唯一编码 | 如 `core_reading_order-0101004`，组件维度统计的分组键 |
| `component_id` | bigint | 组件 ID | |
| `component_type` | smallint | 组件类型 | |
| `comp_type` | varchar | 组件类型名 | |
| `play_status` | smallint | 播放状态 | **1=完成，2=退出**（退出率：`IN (1,2)` 为有效参与） |
| `play_result` | varchar | 练习结果 | **Perfect / Good / Oops** |
| `interval_time` | integer | 播放耗时（毫秒） | 课时总耗时 = `SUM(interval_time) / 60000` 分钟 |
| `pass_time` | integer | 通过时间 | |
| `read_word_count` | integer | 阅读词数 | |
| `speak_count` | integer | 口语次数 | |
| `listen_sentence_count` | integer | 听力句数 | |
| `write_word_count` | integer | 写作词数 | |
| `created_at` | timestamptz | 创建时间 | |
| `updated_at` | timestamptz | 更新时间 | |
| `deleted_at` | timestamptz | 删除时间 | 统计需过滤 |
| `session_id` | varchar | 会话 ID | |
| `listen_time` | integer | 听力时间 | |
| `skill_points` | integer | 技能点 | |
| `finish_question_count` | integer | 完成题目数 | |

**Oops率/退出率计算模板**：
```sql
-- 组件 Oops率
SELECT component_unique_code,
       COUNT(CASE WHEN play_result = 'Oops' THEN 1 END) * 100.0 / COUNT(*) AS oops_rate
FROM (
    SELECT * FROM bi_user_component_play_record_0 WHERE deleted_at IS NULL
    UNION ALL ... -- 1~7 同理
) AS all_comp
WHERE play_status = 1
GROUP BY component_unique_code;

-- 组件退出率
SELECT component_unique_code,
       COUNT(CASE WHEN play_status = 2 THEN 1 END) * 100.0 / COUNT(*) AS exit_rate
FROM (
    SELECT * FROM bi_user_component_play_record_0 WHERE deleted_at IS NULL
    UNION ALL ... -- 1~7 同理
) AS all_comp
WHERE play_status IN (1, 2)   -- 有效参与 = 完成 + 退出
GROUP BY component_unique_code;
```

---

### 📦 bi_user_unit_review_question_result（课程巩固 Review 源表）

> 🔔 **巩固用时 = `play_time / 1000` 秒。巩固正确率 = `正确数 / 总题数 × 10000`（万分比）。`question_list` 为 JSON 数组，需解析 `isRight` 和 `isSkip`。**

| 字段 | 类型 | 释义 | 计算说明 |
|------|------|------|----------|
| `id` | bigint | 主键 | |
| `user_id` | bigint | 角色 ID | |
| `story_id` | bigint | 故事 ID | |
| `score` | integer | 得分 | |
| `score_text` | varchar | 评分等级 | Perfect / Good / Oops |
| `question_list` | text | 题目列表 JSON | 解析 `isRight`、`isSkip` 计算正确率 |
| `play_time` | bigint | 播放时间（毫秒） | **巩固用时 = play_time / 1000（秒）** |
| `created_at` | timestamptz | 创建时间 | |
| `updated_at` | timestamptz | 更新时间 | |
| `deleted_at` | timestamptz | 删除时间 | |
| `sp_value` | integer | SP 值 | |
| `exp` | integer | 经验值 | |
| `level` | varchar | 等级 | |

**正确率计算模板**：
```sql
-- 巩固正确率（万分比）
-- 先解析 question_list JSON 数组，统计 isRight=true 的数量
-- 正确率 = SUM(isRight=true) / COUNT(*) * 10000
```

---

### 📦 bi_user_unit_summary_km_result（单元强化 Summary 源表）

> 🔔 **强化进入时间 = `created_at`。**

| 字段 | 类型 | 释义 | 计算说明 |
|------|------|------|----------|
| `id` | bigint | 主键 | |
| `user_id` | bigint | 角色 ID | |
| `story_id` | bigint | 故事 ID | |
| `km_id` | varchar | 知识模块 ID | |
| `km_type` | varchar | 知识模块类型 | |
| `score_text` | varchar | 评分等级 | |
| `play_time` | bigint | 播放时间 | |
| `created_at` | timestamptz | 首次进入时间 | **强化进入时间的基准** |
| `updated_at` | timestamptz | 更新时间 | |
| `deleted_at` | timestamptz | 删除时间 | |
| `sum_explain` | text | 总结说明 | |
| `sp_value` | integer | SP 值 | |
| `level` | varchar | 等级 | |
