Files
ledgerly-docs/dev/data-model.md
T
rosercode 41d1a4d5a9 docs: 补充管理页批量操作说明并同步规则/AI 规则文档
为规则、分类、标签管理页新增的「批量操作模式」与多选删除补充使用与开发文档,同时补上此前已完成但未同步的 AI 规则建议可编辑、编辑后实时重算去重等文档,并登记批量模式截图脚本。

变更内容:
- guide/categories-tags.md:补充分类与标签的批量删除说明,并回填 tags-batch.png、categories-batch.png 配图。
- guide/rules.md:批量删除与应用匹配账单改为需先进入「批量操作」模式(默认不显示勾选框)。
- dev/api.md:新增 DELETE /api/tag/batch、/api/category/batch,并补齐规则命中账单预览/一键应用、AI 规则去重校验等接口。
- dev/data-model.md:说明标签删除清理交易标签与规则标签关联、分类删除保留逻辑引用(账单显示未分类)。
- dev/rule-engine.md、dev/ai.md、dev/preferences.md、guide/ai.md、guide/transactions.md:同步 AI 规则建议可编辑、编辑后实时重算去重、应用开关默认开启并持久化等说明。
- scripts/ts/capture-elements.ts:新增 captureBatchModes()(标签/分类批量模式截图)并纳入 main()。

说明:
- 截图经测试环境(前端 5174 / 后端 8081,虚构数据)生成,已上传 OSS,正文以 OSS 长期地址引用;public/screenshots 目录不入库。
2026-10-14 01:07:46 +08:00

176 lines
11 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.
# 数据模型
系统共 **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_transaction_tag` 与 `t_rule_tag` 中的关联记录(单条与批量删除行为一致);删除分类仅移除分类本身,账单与规则上的 `category_id` 作为逻辑外键保留(账单会显示为「未分类」)。
### 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`(是否显示已隐藏备注的记录)、`chartClickNavigation`(统计面板图表点击是否跳转明细)、`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` 初始化默认分类、标签与邮箱发件人规则。