feat: 补充图片记账、交易渠道管理与前端偏好文档

- 新增「交易渠道管理」指南与「前端用户偏好」开发文档,并加入侧边栏导航
- 图片记账:补充多图上传、实时识别进度、结果勾选导入与「来源图片」回看,AI 配置新增视觉模型说明
- 数据模型补充 t_channel、t_ai_config/t_ai_image 等表与「类型+金额+时间」去重键说明,表数更新为 21 张
- API 文档补充 /api/channel、/api/transaction/batch、/api/ai/record/image(/stream)、/api/ai/image、/api/ai/channel 等接口
- 截图脚本新增渠道页、图片记账、来源图片与渠道 AI 辅助截图,并放宽导航超时
This commit is contained in:
2026-09-21 22:19:55 +08:00
parent a4ae7ac979
commit d2e4a51f2b
20 changed files with 557 additions and 40 deletions
+29 -4
View File
@@ -6,12 +6,14 @@
`DeepSeekClient` 基于 **OpenAI 官方 Java SDK**(`com.openai:openai-java`)调用,通过自定义 `baseUrl` 指向 DeepSeek 的 OpenAI 兼容接口:
- 使用 `OpenAIOkHttpClient`,按 `(baseUrl, apiKey)` 缓存客户端(上限 32,超出后清空重建)。
- `temperature = 0.2`,`response_format = {"type":"json_object"}`,非流式,`maxRetries(0)`,请求超时 60 秒。
- 使用 `OpenAIOkHttpClient`,按 `(baseUrl, apiKey, 超时)` 缓存客户端(上限 32,超出后清空重建)。
- `temperature = 0.2`,`response_format = {"type":"json_object"}`,非流式,`maxRetries(0)`;文本请求超时 **120 秒**,视觉请求超时 **240 秒**(`chatWithImages` 单独放宽)。
- 取 `choices[0].message.content` 与 `usage`(输入/输出/总 token;缓存命中 token 取 `promptTokensDetails.cachedTokens`,DeepSeek 私有字段读不到时为 0)。
- 对外保持 `chat(baseUrl, apiKey, model, systemPrompt, userPrompt)` 与 `DeepSeekResponse` 不变,各 `Ai*ServiceImpl` 无需改动。
- 另有 `chatWithImages(baseUrl, apiKey, model, systemPrompt, userText, imageDataUrls)`:将一张或多张图片以 base64 data URL 作为 `user` 消息的内容块发送(DeepSeek 视觉接口要求图片只能出现在 user 消息中),供图片记账使用。
- SDK 异常统一转 `BusinessException`;异常链含超时(`SocketTimeoutException` 等)时返回面向用户的友好提示。
配置存储于 `t_ai_config`(每用户一条):`enabled`、`api_key`、`base_url`(默认 `https://api.deepseek.com`)、`model`(默认 `deepseek-chat`)。API Key 对外脱敏,支持连通性测试。
配置存储于 `t_ai_config`(每用户一条):`enabled`、`api_key`、`base_url`(默认 `https://api.deepseek.com`)、`model`(默认 `deepseek-chat`)、`vision_model`(图片记账视觉模型,默认 `deepseek-flash`)。API Key 对外脱敏,支持连通性测试。
## 数据外发范围(隐私)
@@ -19,10 +21,11 @@
- **禁止**:`rawData`(账单原始 JSON)、金额、交易号、账户/卡号、姓名、地址、电话等。
- **允许**:收支类型、交易渠道、备注(必要时截断/脱敏)、分类与标签的名称/ID、用户主动输入的自然语言文本。
- **图片记账例外**:用户主动上传的账单/支付截图会**整张**发送给视觉模型(可能含金额、交易对方、订单号等),视为用户主动提供的输入;图片同时落盘于 `data/ai-images/`,仅本人可经 `GET /api/ai/image/{id}` 访问,禁止用于其他外发场景。
## 调用类型
`AiCallType` 定义了 9 类调用,全部记录用量:
`AiCallType` 定义了 11 类调用,全部记录用量:
| 枚举值 | 含义 |
|---|---|
@@ -30,9 +33,11 @@
| `TEST` | 连接测试 |
| `QUERY` | 自然语言查询解析为筛选条件 |
| `RECORD` | 自然语言记账解析为交易记录 |
| `RECORD_IMAGE` | 图片记账解析(视觉模型) |
| `DECORATE` | 分类 Emoji / 标签颜色建议 |
| `CATEGORY` | 自然语言生成分类 |
| `TAG` | 自然语言生成标签 |
| `CHANNEL` | 自然语言生成交易渠道 |
| `RULE` | 由已打标签记录生成分类与标签规则 |
| `STATISTICS` | 自然语言统计查询解析 |
@@ -42,6 +47,7 @@
- 先按「类型 + 交易渠道 + 备注」聚合去重,仅将代表记录送模型;单批 ≤ 100 条、总量 ≤ 500 条,超出则分批调用。
- 支持严格模式(仅从已有分类/标签中选择)与增强模式(可返回新分类/新标签)。
- **单条建议**额外下发当前用户的渠道候选(内置取中文名、自定义取用户名称),模型可返回 `channel`,服务端仅接受候选中的编码;批量建议不下发渠道候选。
- 应用时分类覆盖写入、标签追加;新建分类按「类型 + 名称」去重,新建标签按名称去重并校验颜色格式。
## 自然语言查询
@@ -52,6 +58,25 @@
`AiRecordServiceImpl` 解析类型、金额、分类、标签、交易渠道、交易时间、备注、计入统计等字段;金额无效直接报错,时间缺失回退当前时间。
## 生成交易渠道
`AiChannelServiceImpl` 根据用户对渠道用途的自然语言描述,使用 `ai-channel-system` 模板生成 2–6 字的渠道名称(返回 `name` 与 `reason`,名称长度上限 20),对应 `POST /api/ai/channel`。前端在新增/编辑记录的「交易渠道 · AI 新建」中使用,确认后经 `POST /api/channel` 创建并选中。
## 图片记账
`AiImageRecordServiceImpl` 处理截图上传到生成记录的完整链路,支持**一次上传多张图片**(最多 5 张,单张 ≤ 10MB、总 ≤ 30MB),合并为一次视觉模型调用:
1. 校验图片数量、单张与总大小、格式(JPEG/PNG/GIF/WebP,按魔数识别),并确认 AI 已启用且配置了 API Key。
2. 逐张经 `AiImageStorage` 落盘至 `data/ai-images/{userId}/{yyyyMM}/`(经 `AppHome` 解析运行目录),每张写入一条 `t_ai_image`(记录 MIME、尺寸、所用视觉模型等)。
3. 以 `ai-record-image-system` 模板构造提示词(注入可用分类、标签、渠道与当前时间,要求模型对每笔输出 `imageIndex` 与 `rawData`),调用 `chatWithImages` 使用**视觉模型**一次性识别。
4. 复用 `AiRecordJsonParser` 解析 `records` 数组(与文字记账共用:去除代码围栏、匹配分类/标签、校验渠道编码、回退时间),并回填摘要与各图记录数。
5. **重复检测**:`TransactionDedupUtils` 以「类型 + 金额 + 交易时间」为去重键,与已有记录及本批其它记录比对,命中则标记 `duplicate=true`(前端默认不勾选)。
6. 按模型标注的 `imageIndex` 将每笔记录关联到来源图片,前端确认后经 `POST /api/transaction/batch` 批量创建(同时写入 `rawData`、建立 `t_ai_image_transaction` 关联,并在导入阶段按同一去重键兜底跳过重复)。
**实时进度**:`AiImageRecordStreamService` 基于 SSE 将上述步骤的进度文本推送前端——`POST /api/ai/record/image/stream` 返回 `SseEmitter`,识别在后台线程执行,过程中逐条推送 `progress` 事件,结束推送 `done`(完整结果 JSON)或 `failed`(失败原因)。
**图片访问**:`GET /api/ai/image/{id}` 返回原图,`?thumbnail=true` 返回最长边 1280 的 JPEG 缩略图;仅图片所属用户可访问。交易列表的「来源图片」默认加载缩略图,用户可按需切换原图。
## 规则生成
`AiRuleServiceImpl` 由用户勾选的**已打标签记录**反推分类/标签规则:
+10 -3
View File
@@ -29,13 +29,14 @@
| 控制器 | 路径前缀 | 说明 |
|---|---|---|
| `UserController` | `/api/user` | 登录、注册、用户信息、注册开关 |
| `TransactionController` | `/api/transaction` | 收支记录增删改查、批量操作、导出、交易渠道列表 |
| `TransactionController` | `/api/transaction` | 收支记录增删改查、批量创建与操作、导出、交易渠道选项 |
| `BillImportController` | `/api/transaction/import` | 账单解析与确认导入 |
| `ChannelController` | `/api/channel` | 用户自定义交易渠道的增删改查 |
| `CategoryController` | `/api/category` | 分类管理 |
| `TagController` | `/api/tag` | 标签管理 |
| `RuleController` | `/api/rule` | 自定义规则(列表、分页查询、增删改、启停) |
| `StatisticsController` | `/api/statistics` | 统计汇总、分类、趋势、分组 |
| `AiController` | `/api/ai` | AI 建议、查询、记账、规则生成、统计搜索、用量 |
| `AiController` | `/api/ai` | AI 建议、查询、文字/图片记账、渠道生成、规则生成、统计搜索、用量 |
| `NotificationController` | `/api/notification` | 通知配置、渠道、发送日志 |
| `EmailBillController` | `/api/email-bill` | 邮箱配置、发件人规则、待处理账单 |
| `LogController` | `/api/log` | 访问日志、登录日志、运行日志与 SSE |
@@ -47,7 +48,13 @@
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/api/transaction/channels` | 获取系统支持的交易渠道编码列表 |
| GET | `/api/transaction/channels` | 获取交易渠道选项(内置 + 当前用户自定义,含 `code`/`name`/`builtin`) |
| POST | `/api/transaction/batch` | 批量创建交易记录(逐条可带 `imageId`/`rawData`),导入时按「类型+金额+时间」跳过重复 |
| GET / POST / PUT / DELETE | `/api/channel`、`/api/channel/{id}` | 自定义渠道列表(含使用账单数)、新建、改名、删除 |
| POST | `/api/ai/record/image` | 上传一至多张账单/支付截图(字段 `files`),调用视觉模型识别为多条交易记录 |
| POST | `/api/ai/record/image/stream` | 图片记账(SSE):实时推送识别进度,结束返回完整结果 |
| GET | `/api/ai/image/{id}` | 查看本人上传的图片记账图片,`?thumbnail=true` 返回缩略图 |
| POST | `/api/ai/channel` | 由自然语言描述生成交易渠道名称 |
| GET | `/api/rule/page` | 分页查询规则(`keyword` / `enabled` / `type`) |
| POST | `/api/ai/rule/suggest` | 依据所选已打标签记录生成规则建议(含 `duplicate` 标记) |
| POST | `/api/ai/rule/apply` | 批量创建规则(自动去重、按需新建分类/标签) |
+2 -2
View File
@@ -91,8 +91,8 @@ src/
1. **运行时动态数据源**:启动无需数据库,配置后热切换,见[动态数据源](/dev/database)。
2. **账单两阶段导入**:解析预览 → 确认导入,按交易号去重,见[账单解析](/dev/bill-parsing)。
3. **规则引擎**:分类取最高优先级、标签取并集,见[规则引擎](/dev/rule-engine)。
4. **体系化 AI 调用**:9 类调用类型并记录 token 用量,见 [AI 集成](/dev/ai)。
5. **SSE 实时能力**:运行日志、邮箱检查进度实时推送。
4. **体系化 AI 调用**:11 类调用类型并记录 token 用量,见 [AI 集成](/dev/ai)。
5. **SSE 实时能力**:运行日志、邮箱检查进度与图片记账识别进度实时推送。
6. **用户偏好缓存**:`t_user_preference` 的读取走 Spring Cache 进程内缓存(`ConcurrentMapCacheManager`),写入时按用户失效,切换数据源时整体清空。
## 相关文档
+32 -4
View File
@@ -1,6 +1,6 @@
# 数据模型
系统共 **18 张表**,覆盖用户、记账、标签、规则、AI、通知、邮箱账单与日志审计。所有表主键为自增 `id`,表间关系均为**逻辑外键**(通过 `user_id`、`category_id` 等字段在应用层维护,未声明物理外键)。
系统共 **21 张表**,覆盖用户、记账、渠道、标签、规则、AI、通知、邮箱账单与日志审计。所有表主键为自增 `id`,表间关系均为**逻辑外键**(通过 `user_id`、`category_id` 等字段在应用层维护,未声明物理外键)。
## 表清单
@@ -10,12 +10,15 @@
| 用户 | `t_user_preference` | 每用户多条的用户偏好(键值) |
| 记账 | `t_category` | 收支分类(收入/支出,含图标与排序) |
| 记账 | `t_transaction` | 收支记录(核心表) |
| 记账 | `t_channel` | 用户自定义交易渠道(内置渠道由枚举维护) |
| 标签 | `t_tag` | 标签 |
| 标签 | `t_transaction_tag` | 交易-标签关联(多对多) |
| 规则 | `t_rule` | 自定义归类规则 |
| 规则 | `t_rule_tag` | 规则-标签关联(多对多) |
| AI | `t_ai_config` | 每用户一条的 AI 配置 |
| AI | `t_ai_config` | 每用户一条的 AI 配置(含视觉模型) |
| AI | `t_ai_usage` | AI 调用用量记录 |
| AI | `t_ai_image` | 图片记账上传的截图及其解析摘要 |
| AI | `t_ai_image_transaction` | 图片与解析生成交易的关联 |
| 通知 | `t_notification_config` | 每用户一条的提醒配置 |
| 通知 | `t_notification_channel` | 通知渠道 |
| 通知 | `t_notification_log` | 通知发送日志 |
@@ -34,7 +37,9 @@
| 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_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 |
@@ -57,6 +62,10 @@
| 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 |
> 交易记录通过 `channel` 编码关联渠道:内置渠道编码取自 `ChannelType` 枚举,自定义渠道编码取自 `t_channel.code`(用户内唯一),因此渠道改名不影响历史记录。
### 通知与邮箱账单
@@ -84,7 +93,7 @@
| external_no | 外部账单交易号,导入去重 |
| transaction_time | 交易发生时间 |
| include_in_stats | 是否计入统计(默认 1) |
| raw_data | 账单导入原始 JSON(手动记录为空) |
| raw_data | 原始字段 JSON(账单导入的原始账单行,或图片记账由模型提取的原始字段;手动记录为空) |
关键索引:唯一索引 `uk_user_external (user_id, external_no)` 实现导入去重;`idx_user_time (user_id, transaction_time)` 支撑按时间查询与统计。
@@ -97,6 +106,25 @@
`name`、`enabled`、`sort_order`(优先级)、`type`、`keyword`、`min_amount`、`max_amount`、`category_id`。
### 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)`;每笔记录按模型标注的来源图分别建立关联。
> **去重键**:图片记账与批量导入使用「收支类型 + 金额 + 交易时间(到秒)」作为业务去重键(`TransactionDedupUtils`),用于标记疑似重复并在导入时跳过,弥补这类记录没有外部交易号的不足。
### t_user_preference
按用户存储的键值偏好:`pref_key` 为偏好键(如 `billSourceIncludeInStats`、`excludedDisplayStyle`),`pref_value` 为标量字符串或 JSON。唯一索引 `uk_user_pref (user_id, pref_key)` 保证每个用户每个偏好仅一条。未配置时由应用回退系统默认值。
@@ -129,5 +157,5 @@ IMAP 连接信息(`host`/`port`/`use_ssl`/`username`/`password`/`folder`)、
## 初始化与迁移
- 应用启动时由 `SchemaInitializer` 执行对应建表脚本(幂等)。
- 建表脚本只做 `CREATE TABLE IF NOT EXISTS`,无法修改已有库的列;需要就地变更列名时由 `SchemaMigrator` 在启动/切换数据源时**幂等迁移**(如 `t_transaction.payment_method` → `channel`,MySQL 用 `information_schema`、SQLite 用 `PRAGMA table_info` 检测)。
- 建表脚本只做 `CREATE TABLE IF NOT EXISTS`,无法修改已有库的列;需要就地变更列名或新增列时由 `SchemaMigrator` 在启动/切换数据源时**幂等迁移**(如 `t_transaction.payment_method` → `channel` 改名、`t_ai_config.vision_model` 加列;MySQL 用 `information_schema`、SQLite 用 `PRAGMA table_info` 检测列是否存在)。
- 新用户注册时由 `DefaultDataInitializer` 初始化默认分类、标签与邮箱发件人规则。
+3 -2
View File
@@ -5,12 +5,13 @@
## 设计与实现
- [整体架构](/dev/architecture):技术栈、后端分层、请求流程、认证与前端结构
- [数据模型](/dev/data-model):17 张表的字段与关系、双数据库脚本差异
- [数据模型](/dev/data-model):21 张表的字段与关系、双数据库脚本差异
- [账单解析](/dev/bill-parsing):解析器策略模式、两阶段导入、去重与 zip 解密
- [规则引擎](/dev/rule-engine):规则模型与匹配逻辑
- [AI 集成](/dev/ai):OpenAI SDK 客户端、9 类调用类型与用量记录
- [AI 集成](/dev/ai):OpenAI SDK 客户端、11 类调用类型与用量记录
- [定时通知](/dev/notifications):调度、渠道与发送日志
- [动态数据源](/dev/database):SQLite / MySQL 运行时配置与切换
- [前端用户偏好](/dev/preferences):偏好的存储介质、读写时机与完整清单
## 工程与规范
+221
View File
@@ -0,0 +1,221 @@
# 前端用户偏好与持久化
本文梳理前端「用户偏好」的保存逻辑:**为什么保存、保存了哪些偏好、存在哪里、何时读写、目的是什么**。
## 为什么保存
收支管理系统的不少界面是「多 Tab」或「带筛选条件」的,如果每次进入都重置为默认值,用户每次都要重新选一遍语言、筛选项、分页、排序、上次使用的记账方式等,体验割裂。
为此项目在 `finance-app/AGENTS.md` 中约定:
> 多 Tab 或可切换的界面偏好需**持久化记忆**(`localStorage`),下次打开时恢复到上次选择;存储键统一以 `finance.` 前缀命名,读取时对非法值回退默认项,写入时忽略存储异常。
据此形成三条实践原则:
1. **进入页面即还原**:`onMounted` 或模块初始化时读取上次选择。
2. **变化即保存**:用 `watch` 监听偏好变量,任一变化自动写入,无需用户点「保存」。
3. **容错**:读取时校验取值白名单,非法/损坏数据回退默认;写入包在 `try/catch` 中,忽略隐私模式等存储异常。
偏好分三层存储:
| 存储介质 | 生命周期 | 适用场景 |
|---|---|---|
| `localStorage` | 跨会话、跨标签页 | 语言、筛选条件、Tab、历史记录等界面偏好 |
| `sessionStorage` | 仅当前浏览器会话 | 内容区滚动位置(刷新/切换恢复) |
| 后端(按当前用户) | 跟随账号 | 需要跨设备/多端一致的外观与功能配置 |
> 登录凭证 `token` 也存放在 `localStorage`,但它属于**认证状态**而非界面偏好,见文末说明。
## 总览表
### localStorage 偏好
| 键名 | 偏好内容 | 文件 | 保存时机 | 读取时机 |
|---|---|---|---|---|
| `locale` | 界面语言 `zh-CN`/`en-US`/`ja-JP` | `locales/index.ts:16` | 切换语言 | 模块初始化 |
| `finance.dashboard.prefs` | 统计面板时间范围、粒度与 11 项筛选 | `views/Dashboard.vue:470` | 变量 `watch` 自动 | `onMounted` |
| `finance.transaction.filterPrefs` | 收支列表筛选、分页、排序 | `views/TransactionList.vue:369` | 变量 `watch` 自动 | `onMounted` |
| `finance.transaction.aiScope` | 批量 AI 建议范围 | `views/TransactionList.vue:146` | 变量 `watch` 自动 | ref 初始化 |
| `finance.transaction.aiRecordTab` | AI 记账方式(文字/图片) | `views/TransactionList.vue:206` | 变量 `watch` 自动 | ref 初始化 |
| `finance.transaction.aiQueryHistory` | AI 自然语言查询历史 | `views/TransactionList.vue:195` | 提交查询时 | `onMounted` |
| `finance.transaction.remarkHistory` | 备注关键词历史 | `views/TransactionList.vue:300` | 备注失焦/回车 | `onMounted` |
| `finance.dashboard.aiSearchHistory` | 统计面板 AI 查询历史 | `views/Dashboard.vue:91` | 提交 AI 搜索时 | `onMounted` |
| `finance.billImport.billType` | 上次选择的账单来源类型 | `views/BillImportPanel.vue:25` | 切换类型时 | ref 初始化 |
| `finance.transaction.relationType` | 账单关系类型偏好 | `components/TransactionRelationEditorModal.vue:14` | 创建模式切换类型时 | 弹窗初始化 |
| `emailBill.tab` | 邮箱账单页标签页 | `views/EmailBillManage.vue:43` | Tab 切换时 | 路由重定向 + 组件 |
| `logs.tab` | 系统日志页标签页 | `views/Logs.vue:25` | Tab 切换时 | 路由重定向 + 组件 |
> `locale`、`emailBill.tab`、`logs.tab` 是较早的实现,未使用 `finance.` 前缀;前两者分别为国际化与路由重定向的既有约定,改动时需保持键名稳定。
### sessionStorage 偏好
| 键名 | 偏好内容 | 文件 |
|---|---|---|
| `finance.scroll.<路由路径>` | 各页面内容区滚动位置 | `views/Layout.vue:83` |
### 后端按用户同步的偏好
| 接口 | 偏好内容 | 前端入口 |
|---|---|---|
| `GET/PUT /api/setting/excluded-display-style` | 「不计入统计」记录展示样式 | `components/DisplayConfigPanel.vue` |
| `GET/PUT /api/setting/bill-source-include-in-stats` | 各账单来源「计入统计」默认值 | `components/BillSourceStatsPanel.vue` |
| `GET/PUT /api/ai/config` | AI 启用开关、密钥、模型 | `components/AiConfigPanel.vue` |
## localStorage 偏好详解
### 界面语言 `locale`
- **文件**:`frontend/src/locales/index.ts`
- **取值**:`zh-CN`、`en-US`、`ja-JP`
- **读取**:`resolveInitialLocale()`(第 42 行)按「`localStorage` → `navigator.language` → `zh-CN`」优先级解析,作为 `createI18n` 初始语言。
- **保存**:`setLocale()`(第 75 行)在切换语言时写入,并同步 `document.documentElement.lang`。
- **目的**:记住用户语言选择,应用启动与切换即时生效。
- **入口**:`components/LocaleSwitch.vue`。
### 统计面板偏好 `finance.dashboard.prefs`
- **文件**:`frontend/src/views/Dashboard.vue`(键名第 470 行)
- **数据结构**(JSON 对象,保存见 `savePrefs()` 第 499 行):
| 字段 | 含义 | 默认值 |
|---|---|---|
| `rangeType` | 时间范围 `month`/`3month`/`12month`/`custom` | `month` |
| `customRange` | 自定义起止时间戳 `[number, number]` | `null` |
| `granularity` | 趋势粒度 `day`/`week`/`month`/`quarter`/`year` | `day` |
| `filterStatsScope` | 统计口径 `INCLUDED`/`ALL`/`EXCLUDED` | `INCLUDED` |
| `filterType` | 收支类型(多选) | `[]` |
| `filterCategory` | 分类(多选,`0` 表示未分类) | `[]` |
| `filterChannel` | 交易渠道(多选) | `[]` |
| `filterTag` | 标签(多选) | `[]` |
| `filterMinAmount` / `filterMaxAmount` | 金额区间 | `null` |
| `filterRemark` | 备注关键字 | `''` |
- **读取**:`loadPrefs()`(第 474 行)在 `onMounted`(第 699 行)调用,逐项校验白名单;兼容旧字段 `filterPaymentMethod`(第 486 行)。
- **保存**:`watch` 监听上述变量,任一变化调用 `savePrefs()`。
- **跨页写入**:点击趋势图跳转收支列表时,`applyTrendNavigation()`(第 381 行)把统计口径与筛选**合并写入**收支列表的偏好键,并保留分页、排序等既有设置。
- **目的**:下次进入统计面板自动还原时间范围、粒度与全部过滤条件。
### 收支列表筛选偏好 `finance.transaction.filterPrefs`
- **文件**:`frontend/src/views/TransactionList.vue`(键名第 369 行)
- **数据结构**(保存见 `saveFilterPrefs()` 第 396 行):
| 字段 | 含义 | 默认值 |
|---|---|---|
| `filterType` | 收支类型 | `[]` |
| `filterCategory` | 分类(`0` 表示未分类) | `[]` |
| `filterChannel` | 交易渠道 | `[]` |
| `filterDateRange` | 日期范围 `[number, number]` | `null` |
| `filterIncludeInStats` | 计入统计三态 `null`/`'true'`/`'false'` | `null` |
| `filterRemark` | 备注关键字 | `''` |
| `filterMinAmount` / `filterMaxAmount` | 金额区间 | `null` |
| `pageSize` | 每页条数 `10`/`20`/`50`/`100`/`200`/`500` | `20` |
| `sortValue` | 排序 `transactionTime-desc` 等 4 种 | `transactionTime-desc` |
| `filterTag` | 标签(`0` 表示无标签) | `[]` |
- **读取**:`loadFilterPrefs()`(第 372 行)在 `onMounted`(第 2030 行)调用;对 `pageSize` 校验白名单、对 `sortValue` 校验选项,兼容旧字段 `filterPaymentMethod`。
- **保存**:`watch` 深度监听 11 个变量,任一变化自动保存(第 417 行)。
- **目的**:记住筛选条件、每页条数与排序方式,下次进入自动还原。
### AI 相关偏好(收支列表)
| 键名 | 取值 | 说明 |
|---|---|---|
| `finance.transaction.aiScope` | `BOTH`/`CATEGORY`/`TAG` | 批量 AI 建议范围,非法值回退 `BOTH`(第 146 行) |
| `finance.transaction.aiRecordTab` | `text`/`image` | AI 记账方式,非法值回退 `text`(第 206 行) |
| `finance.transaction.aiQueryHistory` | 字符串数组(≤10) | AI 自然语言查询历史,最新在前 |
| `finance.transaction.remarkHistory` | 字符串数组(≤10) | 备注关键词历史,失焦/回车记录 |
- **`aiRecordTab`** 是 `AGENTS.md` 点名的参考实现:记住用户上次用的记账方式(文字或图片),下次打开 AI 记账弹窗直接落到该 Tab。
- **关键词历史**统一由 `composables/useKeywordHistory.ts` 管理:去重(忽略大小写)、最新在前、上限 10 条,损坏数据自动忽略,下拉末尾提供「清空历史」项。
- 读取:`load()`;记录:`record()`;清空:`clear()`。
- 三处使用:AI 查询历史、备注历史(均在 `TransactionList.vue`)、统计面板 AI 搜索历史(`Dashboard.vue`,键 `finance.dashboard.aiSearchHistory`)。
### 账单导入类型 `finance.billImport.billType`
- **文件**:`frontend/src/views/BillImportPanel.vue`(第 25 行)
- **取值**:账单来源编码(`ALIPAY`、`WECHAT`、`CMB` 等)
- **读取**:`readBillTypePref()`(第 62 行)作为初始值;`loadBillTypes()` 拿到后端列表后校正,非法值回退首个来源。
- **保存**:`watch(billType)`(第 226 行)在切换类型时写入,同时清空已选文件,避免格式与类型不匹配。
- **目的**:记住上次选择的账单来源。
### 账单关系类型 `finance.transaction.relationType`
- **文件**:`frontend/src/components/TransactionRelationEditorModal.vue`(第 14 行)
- **取值**:`PARENT_CHILD`(默认)、`SIBLING`
- **读取**:`loadPreferredRelationType()`(第 81 行)在新建弹窗初始化时调用。
- **保存**:`watch(editorType)`(第 69 行)**仅在创建模式**(`editingGroupId === null`)写入,编辑既有关系组不覆盖偏好。
- **目的**:新建关系组时默认沿用上次的关系类型。
### 页面标签页 `emailBill.tab` / `logs.tab`
- **文件**:`views/EmailBillManage.vue:43`、`views/Logs.vue:25`
- **取值**:邮箱页 `configs`/`senderRules`/`messages`;日志页 `http`/`login`/`app`
- **读取**:路由 `/email-bills`、`/logs` 的 `redirect` 读取偏好并拼接子路径(`router/index.ts:69`、第 93 行),带白名单校验,无记录回退默认。
- **保存**:`watch(activeTab, ..., { immediate: true })` 在 Tab 切换时写入。
- **目的**:下次点击「邮箱账单」「系统日志」回到上次标签页。
- **说明**:这两个是 `AGENTS.md` 点名的参考实现,但键名未加 `finance.` 前缀,属早期实现。
## sessionStorage 偏好:滚动位置
- **文件**:`frontend/src/views/Layout.vue`(前缀第 83 行,键为 `finance.scroll.<路由路径>`)
- **保存**:内容区 `scroll` 事件经 `requestAnimationFrame` 节流写入;路由切换前保存离开页;`beforeunload` 兜底(第 172 行)。
- **读取**:`restoreScroll()`(第 139 行)在 `onMounted` 与路由切换后恢复;因页面内容异步加载、切换后高度可能不足,用 `rAF` 轮询等待内容高度足够再定位,2 秒超时后按当前可达最大值定位。
- **目的**:刷新或页面切换后恢复阅读位置;仅当前会话有效,关闭浏览器即失效。
## 后端按用户同步的偏好
以下偏好需要跟随账号(换设备/多端一致),因此存后端,按当前用户隔离,前端通过设置页读写。
### 「不计入统计」展示样式
- **接口**:`GET /api/setting/excluded-display-style`、`PUT /api/setting/excluded-display-style`(`api/setting.ts:14`)
- **取值**:`strikethrough`(默认,整行删除线)、`muted`(灰色标签)
- **前端**:设置页 `components/DisplayConfigPanel.vue` 编辑保存;收支列表 `TransactionList.vue:647` 在 `onMounted` 读取并应用到「不计入统计」记录(失败保持默认删除线)。
- **目的**:统一用户对「不计入统计」记录的外观选择。
### 账单来源「计入统计」默认值
- **接口**:`GET/PUT /api/setting/bill-source-include-in-stats`(`api/setting.ts:4`)
- **数据结构**:`{ [账单来源编码]: boolean }`
- **前端**:设置页 `components/BillSourceStatsPanel.vue` 编辑保存(可点整行或开关切换)。
- **目的**:按账单来源设置导入时默认是否计入统计,仅影响统计口径,不改变导入结果。
### AI 配置
- **接口**:`GET /api/ai/config`、`PUT /api/ai/config`(`api/ai.ts:37`)
- **数据结构**:`{ enabled, apiKey, hasApiKey, baseUrl, model, visionModel }`,`apiKey` 留空表示不修改原值。
- **前端**:设置页 `components/AiConfigPanel.vue` 编辑保存。
- **目的**:用户级 AI 能力配置;其他页面的 AI 元素统一经 `getAiConfig().enabled` 控制显隐。
> 严格说 AI 配置属「功能配置」而非纯界面偏好,但因按用户存储,一并列出。设置页中的数据库配置、注册开关、定时通知为**系统级全局配置**,不在此列。
## 未实现的偏好
以下常见界面偏好当前**没有**持久化实现,改动时无需假设其存在:
- **主题 / 暗色模式**:无主题存储键,界面主题固定。
- **侧边栏折叠 / 展开**:`Layout.vue` 使用固定 `NLayoutSider`,无折叠状态。
- **表格列显示 / 隐藏 / 顺序**:各页列配置为固定 `computed`。
- **货币 / 金额格式**:无货币偏好。
- **独立日期格式**:仅由语言 `locale` 间接驱动 Naive UI 日期本地化,无单独偏好。
- **全局分页大小**:仅收支列表的 `pageSize` 持久化;日志、规则、邮箱账单等页面分页大小不持久化。
- **IndexedDB / cookie**:未使用。
- **Pinia 持久化插件 / vueuse `useStorage`**:未安装、未使用。
## 附:登录凭证 `token`
- **文件**:`frontend/src/store/user.ts`(第 8 行)
- **保存**:`setToken()` 写入 `localStorage`;`logout()` 移除。
- **使用**:`api/request.ts` 请求拦截器读取并写入 `Authorization` 头。
- **说明**:这是认证状态,**不属于界面偏好**,列出以免混淆。`nickname`、`userId` 不持久化,每次由 `fetchUserInfo()` 从后端获取。
## 新增偏好的约定
新增一项用户界面偏好时:
1. 存储键以 `finance.` 前缀命名,按页面/模块分段(如 `finance.transaction.xxx`)。
2. 优先用 `watch` 自动保存,避免要求用户手动点保存。
3. 读取时对取值做白名单校验,非法或损坏数据回退默认项。
4. 写入包 `try/catch`,忽略存储异常。
5. 需要跨设备一致时,改存后端并按当前用户隔离,而非 `localStorage`。
+2 -2
View File
@@ -39,7 +39,7 @@ finance-app/
| `notification` | 通知渠道发送器 |
| `scheduler` | 定时任务(通知、日志清理、邮箱账单) |
| `service` / `service.impl` | 业务层 |
| `util` | 工具类(`JwtUtil`、`UserContext`、`AppLogUtils` 等) |
| `util` | 工具类(`JwtUtil`、`UserContext`、`AppLogUtils`、`ImageUtils`、`AppHome`、`TransactionDedupUtils` 等) |
| `vo` | 响应对象 |
| `launcher` | 桌面 GUI 启动器(仅打入带 GUI 的 jar) |
@@ -83,7 +83,7 @@ views/ # 页面组件
|---|---|
| `config/database.json` | 数据库配置(SQLite / MySQL) |
| `config/app.json` | 应用配置(注册开关、通知链接、日志保留天数、按账单来源的计入统计默认值) |
| `data/` | SQLite 数据文件与邮箱账单附件 |
| `data/` | SQLite 数据文件、邮箱账单附件与图片记账截图(`data/ai-images/`) |
| `logs/` | 运行日志文件 |
## 相关文档