# 数据模型 系统共 **24 张表**,覆盖用户、记账、账单关系、渠道、标签、规则、AI、通知、邮箱账单与日志审计。所有表主键为自增 `id`,表间关系均为**逻辑外键**(通过 `user_id`、`category_id` 等字段在应用层维护,未声明物理外键)。 ## 表清单 | 业务域 | 表名 | 说明 | |---|---|---| | 用户 | `t_user` | 用户表(用户名、BCrypt 密码、昵称) | | 用户 | `t_user_preference` | 每用户多条的用户偏好(键值) | | 记账 | `t_category` | 收支分类(收入/支出,含图标与排序) | | 记账 | `t_transaction` | 收支记录(核心表) | | 记账 | `t_channel` | 用户自定义交易渠道(内置渠道由枚举维护) | | 标签 | `t_tag` | 标签 | | 标签 | `t_transaction_tag` | 交易-标签关联(多对多) | | 账单关系 | `t_transaction_group` | 账单关系组(父子/同级) | | 账单关系 | `t_transaction_group_item` | 关系组成员(角色与顺序) | | 规则 | `t_rule` | 自定义归类规则 | | 规则 | `t_rule_tag` | 规则-标签关联(多对多) | | AI | `t_ai_config` | 每用户一条的 AI 配置(含视觉模型) | | AI | `t_ai_usage` | AI 调用用量记录 | | AI | `t_ai_image` | 图片记账上传的截图及其解析摘要 | | AI | `t_ai_image_transaction` | 图片与解析生成交易的关联 | | AI | `t_ai_sql_history` | 智能查数提问历史(每用户多条) | | 通知 | `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_channel | user_id | | t_user | 1 : N | t_transaction_group | user_id | | t_user | 1 : N | t_rule | user_id | | t_user | 1 : N | t_ai_image | 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_ai_image | 1 : N | t_ai_image_transaction | image_id | | t_transaction | 1 : N | t_ai_image_transaction | transaction_id | | t_transaction_group | 1 : N | t_transaction_group_item | group_id(联合唯一 group_id + transaction_id) | | t_transaction | 1 : N | t_transaction_group_item | transaction_id | > 交易记录通过 `channel` 编码关联渠道:内置渠道编码取自 `ChannelType` 枚举,自定义渠道编码取自 `t_channel.code`(用户内唯一),因此渠道改名不影响历史记录。 ### 通知与邮箱账单 | 主体 | 关系 | 对象 | 关联字段 | |---|---|---|---| | 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_transaction_group / t_transaction_group_item(账单关系) - `t_transaction_group`:`user_id`、`type`(`PARENT_CHILD` 父子 / `SIBLING` 同级)、`name`、`remark`;索引 `user_id`。 - `t_transaction_group_item`:`group_id`、`transaction_id`、`role`(`PARENT` / `CHILD` / `MEMBER`)、`sort_order`(同级链路的顺序);唯一索引 `(group_id, transaction_id)` 防止同一账单在同一组内重复,另有 `transaction_id` 索引。 - 约束:一个关系组至少两条成员、上限 50 条;父子关系必须且只能有一条 `PARENT`;一笔账单不能同时加入两个**同类型**的关系组。删除账单时会自动清理其成员记录,成员不足两条或父子组缺父时整组删除(仅解除关联,不删除账单)。详见[账单关系(使用说明)](/guide/relations)。 ### t_channel(自定义交易渠道) | 字段 | 说明 | |---|---| | user_id | 所属用户 | | code | 渠道编码,后端生成(`CUSTOM_` + 随机串),稳定不变,交易记录按此关联 | | name | 渠道名称,用户可修改 | | sort_order | 排序序号,数值越小越靠前 | 唯一索引 `uk_channel_user_code (user_id, code)`、`uk_channel_user_name (user_id, name)` 分别保证用户内编码与名称唯一。仅存放用户自建渠道,内置渠道由 `ChannelType` 枚举维护。 ### t_ai_config / t_ai_image - `t_ai_config`:每用户一条,含 `enabled`、`api_key`、`base_url`、`model` 与 **`vision_model`**(图片记账使用的视觉模型,默认 `deepseek-flash`)。 - `t_ai_image`:图片记账上传的截图,一次上传可产生多条(最多 5 张),含 `user_id`、`file_path`(相对应用运行目录,落盘于 `data/ai-images/`)、`mime_type`、`file_size`、`width`、`height`、`model`(解析所用视觉模型)、`summary`(识别摘要)、`record_count`(该图识别出的记录数)。 - `t_ai_image_transaction`:图片与交易记录的关联,含 `image_id`、`transaction_id`,唯一索引 `uk_ai_image_transaction (image_id, transaction_id)`;每笔记录按模型标注的来源图分别建立关联。 - `t_ai_sql_history`:智能查数(自然语言转 SQL)的提问历史,含 `user_id`、`query_text`(截断 500 字符)、`created_at`;索引 `(user_id, created_at)`。 > **去重键**:图片记账与批量导入使用「收支类型 + 金额 + 交易时间(到秒)」作为业务去重键(`TransactionDedupUtils`),用于标记疑似重复并在导入时跳过,弥补这类记录没有外部交易号的不足。 ### t_user_preference 按用户存储的键值偏好:`pref_key` 为偏好键,`pref_value` 为标量字符串或 JSON。已使用的偏好键包括:`billSourceIncludeInStats`(各来源计入统计默认值)、`excludedDisplayStyle`(不计入记录展示样式)、`showHiddenData`(是否显示已隐藏备注的记录)、`paginationPrefs`(表格分页候选与默认值)、`navMenu`(左侧导航顺序与显隐)。唯一索引 `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` 改名、`t_ai_config.vision_model` 加列;MySQL 用 `information_schema`、SQLite 用 `PRAGMA table_info` 检测列是否存在)。 - 新用户注册时由 `DefaultDataInitializer` 初始化默认分类、标签与邮箱发件人规则。