Files
ledgerly-docs/dev/data-model.md
T

134 lines
6.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 数据模型
系统共 **18 张表**,覆盖用户、记账、标签、规则、AI、通知、邮箱账单与日志审计。所有表主键为自增 `id`,表间关系均为**逻辑外键**(通过 `user_id`、`category_id` 等字段在应用层维护,未声明物理外键)。
## 表清单
| 业务域 | 表名 | 说明 |
|---|---|---|
| 用户 | `t_user` | 用户表(用户名、BCrypt 密码、昵称) |
| 用户 | `t_user_preference` | 每用户多条的用户偏好(键值) |
| 记账 | `t_category` | 收支分类(收入/支出,含图标与排序) |
| 记账 | `t_transaction` | 收支记录(核心表) |
| 标签 | `t_tag` | 标签 |
| 标签 | `t_transaction_tag` | 交易-标签关联(多对多) |
| 规则 | `t_rule` | 自定义归类规则 |
| 规则 | `t_rule_tag` | 规则-标签关联(多对多) |
| AI | `t_ai_config` | 每用户一条的 AI 配置 |
| AI | `t_ai_usage` | AI 调用用量记录 |
| 通知 | `t_notification_config` | 每用户一条的提醒配置 |
| 通知 | `t_notification_channel` | 通知渠道 |
| 通知 | `t_notification_log` | 通知发送日志 |
| 邮箱账单 | `t_email_bill_config` | IMAP 邮箱配置 |
| 邮箱账单 | `t_email_bill_sender_rule` | 发件人规则 |
| 邮箱账单 | `t_email_bill_message` | 拉取到的账单消息 |
| 日志 | `t_http_log` | 接口访问日志 |
| 日志 | `t_login_log` | 登录日志 |
## 关系概览
### 以用户为中心(一对多)
| 主体 | 关系 | 对象 | 关联字段 |
|---|---|---|---|
| t_user | 1 : N | t_category | user_id |
| t_user | 1 : N | t_transaction | user_id |
| t_user | 1 : N | t_tag | user_id |
| t_user | 1 : N | t_rule | user_id |
| t_user | 1 : N | t_notification_channel | user_id |
| t_user | 1 : N | t_email_bill_config | user_id |
| t_user | 1 : N | t_email_bill_sender_rule | user_id |
| t_user | 1 : N | t_email_bill_message | user_id |
| t_user | 1 : N | t_user_preference | user_id |
| t_user | 1 : N | t_ai_usage / t_http_log / t_login_log / t_notification_log | user_id |
### 一对一(每用户一条)
| 主体 | 关系 | 对象 | 关联字段 |
|---|---|---|---|
| t_user | 1 : 1 | t_ai_config | user_id(唯一) |
| t_user | 1 : 1 | t_notification_config | user_id(唯一) |
### 记账与标签
| 主体 | 关系 | 对象 | 关联字段 / 中间表 |
|---|---|---|---|
| t_category | 1 : N | t_transaction | transaction.category_id(可空=未分类) |
| t_transaction | N : M | t_tag | 中间表 t_transaction_tag(联合唯一) |
| t_category | 1 : N | t_rule | rule.category_id(命中目标分类) |
| t_rule | N : M | t_tag | 中间表 t_rule_tag(联合唯一) |
### 通知与邮箱账单
| 主体 | 关系 | 对象 | 关联字段 |
|---|---|---|---|
| t_notification_channel | 1 : N | t_notification_log | channel_id(可空) |
| t_email_bill_config | 1 : N | t_email_bill_message | config_id |
| t_email_bill_sender_rule | 1 : N | t_email_bill_message | sender_rule_id(可空) |
> 邮箱配置与发件人规则的默认分类通过 `expense_category_id` / `income_category_id` 逻辑引用 `t_category`;默认标签以 JSON 数组字符串保存(未用关联表)。
## 核心表字段
### t_transaction(收支记录)
| 字段 | 说明 |
|---|---|
| user_id | 所属用户 |
| category_id | 分类(可空,未分类) |
| type | `INCOME` / `EXPENSE` |
| amount | 金额(DECIMAL(12,2)) |
| channel | 交易渠道(如 `ALIPAY` / `WECHAT` / `CASH` / `BANK`) |
| remark | 备注 |
| remark_hidden | 备注是否隐藏 |
| external_no | 外部账单交易号,导入去重 |
| transaction_time | 交易发生时间 |
| include_in_stats | 是否计入统计(默认 1) |
| raw_data | 账单导入原始 JSON(手动记录为空) |
关键索引:唯一索引 `uk_user_external (user_id, external_no)` 实现导入去重;`idx_user_time (user_id, transaction_time)` 支撑按时间查询与统计。
### t_category / t_tag
- 分类:`name`、`type`、`icon`(Emoji)、`sort_order`。
- 标签:`name`、`color`、`sort_order`,唯一索引 `uk_user_name (user_id, name)` 保证用户内标签名唯一。
### t_rule
`name`、`enabled`、`sort_order`(优先级)、`type`、`keyword`、`min_amount`、`max_amount`、`category_id`。
### t_user_preference
按用户存储的键值偏好:`pref_key` 为偏好键(如 `billSourceIncludeInStats`、`excludedDisplayStyle`),`pref_value` 为标量字符串或 JSON。唯一索引 `uk_user_pref (user_id, pref_key)` 保证每个用户每个偏好仅一条。未配置时由应用回退系统默认值。
### t_email_bill_config
IMAP 连接信息(`host`/`port`/`use_ssl`/`username`/`password`/`folder`)、状态(`status`/`last_error`/`last_connected_at`/`last_uid`)、默认归类(`expense_category_id`/`income_category_id`/`tag_ids`)与拉取范围(`fetch_scope`/`fetch_limit`)。
### t_email_bill_message
`config_id`、`uid`、`message_id`、`subject`、`from_address`、`received_at`、附件信息、命中的 `sender_rule_id` 与 `bill_type`、状态(`PULLED`/`CONFIRMED`/`IGNORED`)、`error`。唯一索引 `uk_email_bill_message (config_id, uid, attachment_index)` 防止重复拉取。
## MySQL 与 SQLite 双脚本
系统同时维护 `sql/init.sql`(MySQL)与 `sql/schema-sqlite.sql`(SQLite),表结构与语义保持一致,主要差异:
| 维度 | MySQL | SQLite |
|---|---|---|
| 主键自增 | `BIGINT AUTO_INCREMENT` | `INTEGER PRIMARY KEY AUTOINCREMENT` |
| 布尔 | `TINYINT(1)` | `INTEGER` |
| 金额 | `DECIMAL(12,2)` | `NUMERIC(12,2)` |
| JSON | `JSON` | `TEXT` |
| 时间默认值 | `DEFAULT CURRENT_TIMESTAMP` | `strftime(...,'localtime')` |
| `updated_at` 自动更新 | `ON UPDATE CURRENT_TIMESTAMP` | `AFTER UPDATE` 触发器 |
| 语句分隔符 | `;` | `;;`(兼容触发器内部的分号) |
| 列注释 | 支持 | 不支持 |
统计模块会依据当前数据源类型,切换 MySQL 与 SQLite 的时间分组函数(`DATE_FORMAT/QUARTER` 与 `strftime`)。
## 初始化与迁移
- 应用启动时由 `SchemaInitializer` 执行对应建表脚本(幂等)。
- 建表脚本只做 `CREATE TABLE IF NOT EXISTS`,无法修改已有库的列;需要就地变更列名时由 `SchemaMigrator` 在启动/切换数据源时**幂等迁移**(如 `t_transaction.payment_method` → `channel`,MySQL 用 `information_schema`、SQLite 用 `PRAGMA table_info` 检测)。
- 新用户注册时由 `DefaultDataInitializer` 初始化默认分类、标签与邮箱发件人规则。