From d2e4a51f2b69cfd262e89b574686870b4b5d4a43 Mon Sep 17 00:00:00 2001 From: rosercode Date: Mon, 21 Sep 2026 22:19:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A1=A5=E5=85=85=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E8=AE=B0=E8=B4=A6=E3=80=81=E4=BA=A4=E6=98=93=E6=B8=A0=E9=81=93?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E4=B8=8E=E5=89=8D=E7=AB=AF=E5=81=8F=E5=A5=BD?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增「交易渠道管理」指南与「前端用户偏好」开发文档,并加入侧边栏导航 - 图片记账:补充多图上传、实时识别进度、结果勾选导入与「来源图片」回看,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 辅助截图,并放宽导航超时 --- .vitepress/config.mts | 2 + dev/ai.md | 33 ++++- dev/api.md | 13 +- dev/architecture.md | 4 +- dev/data-model.md | 36 +++++- dev/index.md | 5 +- dev/preferences.md | 221 +++++++++++++++++++++++++++++++++ dev/project-structure.md | 4 +- guide/ai.md | 41 +++++- guide/channels.md | 69 ++++++++++ guide/index.md | 3 +- guide/introduction.md | 3 +- guide/pages.md | 7 +- guide/settings.md | 1 + guide/transactions.md | 30 +++-- index.md | 6 +- scripts/ts/browser.ts | 5 +- scripts/ts/capture-elements.ts | 109 +++++++++++++++- scripts/ts/capture-pages.ts | 1 + scripts/ts/config.ts | 4 + 20 files changed, 557 insertions(+), 40 deletions(-) create mode 100644 dev/preferences.md create mode 100644 guide/channels.md diff --git a/.vitepress/config.mts b/.vitepress/config.mts index c20a3b6..45b10a2 100644 --- a/.vitepress/config.mts +++ b/.vitepress/config.mts @@ -39,6 +39,7 @@ export default defineConfig({ items: [ { text: '收支记录', link: '/guide/transactions' }, { text: '分类与标签', link: '/guide/categories-tags' }, + { text: '交易渠道管理', link: '/guide/channels' }, { text: '规则管理', link: '/guide/rules' }, { text: '邮箱账单', link: '/guide/email-bills' }, ], @@ -87,6 +88,7 @@ export default defineConfig({ { text: 'AI 集成', link: '/dev/ai' }, { text: '定时通知', link: '/dev/notifications' }, { text: '动态数据源', link: '/dev/database' }, + { text: '前端用户偏好', link: '/dev/preferences' }, ], }, { diff --git a/dev/ai.md b/dev/ai.md index b3d5f27..1e9fb1e 100644 --- a/dev/ai.md +++ b/dev/ai.md @@ -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` 由用户勾选的**已打标签记录**反推分类/标签规则: diff --git a/dev/api.md b/dev/api.md index e9a2463..7ce4153 100644 --- a/dev/api.md +++ b/dev/api.md @@ -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` | 批量创建规则(自动去重、按需新建分类/标签) | diff --git a/dev/architecture.md b/dev/architecture.md index 246e82c..af66d48 100644 --- a/dev/architecture.md +++ b/dev/architecture.md @@ -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`),写入时按用户失效,切换数据源时整体清空。 ## 相关文档 diff --git a/dev/data-model.md b/dev/data-model.md index 556583d..026fb96 100644 --- a/dev/data-model.md +++ b/dev/data-model.md @@ -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` 初始化默认分类、标签与邮箱发件人规则。 diff --git a/dev/index.md b/dev/index.md index 7bef43a..e03d457 100644 --- a/dev/index.md +++ b/dev/index.md @@ -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):偏好的存储介质、读写时机与完整清单 ## 工程与规范 diff --git a/dev/preferences.md b/dev/preferences.md new file mode 100644 index 0000000..4f7f510 --- /dev/null +++ b/dev/preferences.md @@ -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`。 diff --git a/dev/project-structure.md b/dev/project-structure.md index aa4d424..ae7ac75 100644 --- a/dev/project-structure.md +++ b/dev/project-structure.md @@ -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/` | 运行日志文件 | ## 相关文档 diff --git a/guide/ai.md b/guide/ai.md index a9b72ae..dd77a6d 100644 --- a/guide/ai.md +++ b/guide/ai.md @@ -18,7 +18,10 @@ - 智能查询 / 统计:你输入的自然语言,以及分类与标签名称; - 智能记账:你输入的一句话; - 生成分类/标签、推荐 Emoji/颜色:你输入的名称或描述; -- 生成规则:所选记录的类型、备注、交易渠道、已有分类与标签(不含金额与原始账单数据)。 +- 生成规则:所选记录的类型、备注、交易渠道、已有分类与标签(不含金额与原始账单数据); +- 图片记账:你**主动上传**的账单/支付截图会**整张**发送给视觉模型(可能含金额、交易对方、订单号等),视为你主动提供的输入。 + +图片同时会保存在本地(`data/ai-images/`),仅本人可查看;请勿上传包含他人敏感信息的图片。 API Key 仅保存在本地数据库中,仅用于向你配置的接口地址发起请求,不会随上述数据一并外发。 @@ -28,7 +31,7 @@ API Key 仅保存在本地数据库中,仅用于向你配置的接口地址发 - 开启「启用 AI 建议」。 - 填写 **API Key**。 -- 可选修改 **接口地址**(默认 `https://api.deepseek.com`)与**模型**(默认 `deepseek-chat`)。 +- 可选修改 **接口地址**(默认 `https://api.deepseek.com`)、**模型**(默认 `deepseek-chat`)与**视觉模型**(默认 `deepseek-flash`,用于图片记账)。 - 点击「测试连接」验证配置。 ![系统设置](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/settings.png) @@ -38,10 +41,12 @@ API Key 仅保存在本地数据库中,仅用于向你配置的接口地址发 | 功能 | 入口 | 说明 | |---|---|---| | 分类/标签建议 | 收支记录 → 一键设置 → AI 智能建议 | 对当前页记录批量建议分类与标签,可一键采纳 | -| 单条建议 | 新增/编辑记录 → AI 推荐 | 推荐已有分类或标签 | +| 单条建议 | 新增/编辑记录 → AI 推荐 | 推荐已有分类、标签或交易渠道 | | 智能查询 | 收支记录 → AI 智能查询 | 自然语言解析为筛选条件 | -| 智能记账 | 收支记录 → 智能记账 | 一句话解析为一笔记录 | +| 智能记账(文字) | 收支记录 → 智能记账 → 文字记账 | 一句话解析为一笔记录 | +| 智能记账(图片) | 收支记录 → 智能记账 → 图片记账 | 上传一至多张账单/支付截图,实时识别为多条记录,勾选后批量导入 | | 生成分类 | 分类管理 → AI 生成 | 由用途描述生成分类 | +| 生成渠道 | 新增/编辑记录 → 交易渠道 → AI 新建 | 由账单描述生成渠道名称并创建 | | 推荐 Emoji | 分类管理 → AI 推荐 | 为分类推荐 Emoji 图标 | | 推荐颜色 | 标签管理 → AI 推荐 | 为标签推荐颜色 | | 生成规则 | 规则管理 → AI 生成规则 | 由已打标签的记录归纳分类与标签规则 | @@ -66,10 +71,36 @@ API Key 仅保存在本地数据库中,仅用于向你配置的接口地址发 ## 智能记账 -在「智能记账」弹窗中描述一笔收支,AI 解析出类型、金额、分类、交易渠道、时间、备注、标签等字段,并预填到新增记录表单,确认后保存。 +「智能记账」弹窗提供「**文字记账**」与「[图片记账](#图片记账)」两个标签页,再次打开时会记住上次使用的方式。 + +在「文字记账」中描述一笔收支,AI 解析出类型、金额、分类、交易渠道、时间、备注、标签等字段,并预填到新增记录表单,确认后保存。 ![AI 智能记账弹窗](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/ai-record.png) +## 图片记账 + +在「智能记账」弹窗切换到「**图片记账**」标签页,上传账单或支付截图,AI 会调用**视觉模型**一次性识别其中的每一笔交易。 + +![图片记账](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/ai-record-image.png) + +- **多图上传**:一次最多 **5 张**,单张不超过 **10MB**、总大小不超过 **30MB**;支持 JPEG / PNG / GIF / WebP。 +- **粘贴上传**:也可按 `Ctrl+V`(Mac 为 `⌘V`)直接粘贴剪贴板中的图片。 +- **补充说明**(可选):在下方输入框补充提示(如「这是 9 月的账单」),帮助模型理解。 +- 点击「识别图片」后,弹窗内会**实时显示识别进度**(校验图片、保存、调用模型、解析、查重等步骤),耗时较长的识别过程可见可控。 +- 识别完成后进入**结果弹窗**: + +![图片记账结果](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/ai-record-result.png) + +- 结果以表格逐条展示,**默认全选**(疑似重复项默认不勾选),可直接编辑**类型、金额、分类、时间、备注**。 +- **来源图**列标注该笔交易来自第几张上传图片;**重复**列标记与已有记录或本批其它记录「类型+金额+时间」一致的疑似重复项。 +- **原始数据**列可查看模型从图片中提取的原始字段(时间、对方、商品、订单号等),用于核对。 +- 顶部摘要说明识别到的账单内容;勾选后点击「导入勾选」批量写入收支记录(导入时会再次按去重键跳过重复)。 +- 未识别到有效交易时会给出提示,不会写入任何记录。 + +> 图片记账使用的是视觉模型,可在「系统设置 → AI 模型配置」中单独配置(默认 `deepseek-flash`)。详见[系统设置](/guide/settings)。 + +导入后的记录在[收支记录](/guide/transactions)页的「**来源图片**」列可回看原图(默认加载缩略图,可切换为原图)。 + ## AI 统计搜索 在统计面板顶部的 AI 搜索框中用一句自然语言描述统计意图,例如「上个月餐饮花了多少」「近半年交通支出趋势,按周看」。系统会解析为**时间范围、收支类型、分类、标签、交易渠道、金额区间、备注、统计口径与趋势粒度**并自动回填、刷新图表。详见[统计面板](/guide/dashboard)。 diff --git a/guide/channels.md b/guide/channels.md new file mode 100644 index 0000000..8f4e9e7 --- /dev/null +++ b/guide/channels.md @@ -0,0 +1,69 @@ +# 交易渠道管理 + +「交易渠道」用于标记一笔收支的付款或收款途径,既包括账单导入时的来源,也包括人工记账时选择的渠道。系统内置一组常用渠道,并支持按账号自定义。 + +## 入口 + +在[收支记录](/guide/transactions)页顶部操作区点击「**渠道管理**」进入(独立页面,不占用左侧菜单)。 + +![交易渠道管理](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/channels.png) + +页面顶部提供返回按钮,可一键回到收支记录页。 + +## 内置渠道 + +以下渠道由系统内置,无需创建,名称随界面语言自动显示: + +| 编码 | 名称 | +|---|---| +| `ALIPAY` | 支付宝 | +| `WECHAT` | 微信 | +| `MEITUAN` | 美团 | +| `TAOBAO` | 淘宝 | +| `JD` | 京东 | +| `CMB` | 招商银行 | +| `BOCOM` | 交通银行 | +| `CASH` | 现金 | + +> 账单导入时,系统会按账单来源自动写入对应的内置渠道;人工记账默认可选「现金」或任意自定义渠道。 + +## 自定义渠道 + +除内置渠道外,你可以新建自己的渠道(例如「某张储蓄卡」「公司报销」等)。 + +### 新建渠道 + +点击右上角「**新建渠道**」,输入名称后确认。 + +![新建渠道](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/channel-create.png) + +- 渠道名称在**当前账号内不可重复**。 +- 渠道编码由系统生成且**稳定不变**,交易记录按编码关联。 + +### 重命名渠道 + +在列表行点击「编辑」即可改名。**改名不影响历史账单**——所有已使用该渠道的记录会同步显示新名称。 + +### 删除渠道 + +在列表行点击「删除」即可删除。 + +- 若该渠道**已被账单使用**,系统会拒绝删除并提示使用数量,避免历史记录出现无效渠道。 +- 列表中的「使用账单数」列可直观看到每个渠道被多少笔记录使用。 + +## 在哪些地方使用 + +自定义渠道创建后,可在以下场景直接选用: + +- **新增 / 编辑记录**:交易渠道下拉中出现在内置渠道之后。 +- **筛选与统计**:收支记录页筛选、统计面板的渠道维度与筛选均可选择自定义渠道,图表与分组会展示其名称。 +- **AI 能力**:智能查询、统计搜索与智能记账会把当前账号的自定义渠道一并作为可选渠道;新增/编辑记录时还可用「渠道 · AI 推荐」选择已有渠道,或「渠道 · AI 新建」由描述生成并创建渠道。 +- **导出**:Excel 导出时渠道列展示自定义渠道名称。 + +> 删除前请先确认没有账单仍在使用该渠道;如需保留历史,建议改名而非删除。 + +## 相关 + +- [收支记录](/guide/transactions):记账、筛选与账单导入。 +- [统计面板](/guide/dashboard):按渠道维度统计。 +- [AI 能力](/guide/ai):自然语言查询与图片记账。 diff --git a/guide/index.md b/guide/index.md index d8b6faa..aa06125 100644 --- a/guide/index.md +++ b/guide/index.md @@ -12,12 +12,13 @@ - [收支记录](/guide/transactions):记录管理、筛选、批量操作、账单导入、AI 建议与记账 - [分类与标签](/guide/categories-tags):分类与标签的维护与使用建议 +- [交易渠道管理](/guide/channels):内置渠道与自定义渠道的新建、改名与删除 - [规则管理](/guide/rules):按关键词/金额/类型自动归类打标,支持 AI 生成规则、搜索与分页 - [邮箱账单](/guide/email-bills):IMAP 自动拉取账单邮件 ## 智能与自动化 -- [AI 能力](/guide/ai):分类标签建议、自然语言查询与记账、AI 搜索与规则生成、用量统计 +- [AI 能力](/guide/ai):分类标签与渠道建议、自然语言查询与记账、多图图片记账(实时进度)、AI 搜索与规则生成、用量统计 - [定时提醒与通知](/guide/notifications):多渠道账单导入提醒 ## 系统 diff --git a/guide/introduction.md b/guide/introduction.md index 1ba6452..fe5d164 100644 --- a/guide/introduction.md +++ b/guide/introduction.md @@ -17,10 +17,11 @@ |---|---| | 收支记录 | 增删改查、分页与多条件筛选、批量设置分类/标签、批量删除、计入统计开关、备注隐藏、导出 Excel | | 分类与标签 | 收入/支出分类与标签的增删改查,新用户自动获得默认数据,AI 推荐 Emoji 与颜色 | +| 交易渠道 | 内置渠道 + 自定义渠道的新建/改名/删除,改名不影响历史账单 | | 统计图表 | 月度收支、分类占比、收支趋势、交易渠道占比,支持按日/周/月/季/年粒度,点击趋势图可下钻对应明细 | | 账单导入 | 七类账单解析、zip 解密、解析预览、按交易号去重、规则建议 | | 自定义规则 | 关键词/收支类型/金额区间组合,优先级排序、启用停用,导入时自动应用;支持 AI 生成规则、搜索与分页 | -| AI 能力 | 分类标签建议、自然语言查询、自然语言记账、AI 统计搜索、AI 生成规则、分类/标签生成、Emoji/颜色推荐、用量统计 | +| AI 能力 | 分类标签与渠道建议、自然语言查询、文字/多图图片记账、AI 统计搜索、AI 生成规则、分类/标签/渠道生成、Emoji/颜色推荐、用量统计 | | 邮箱账单 | 多邮箱 IMAP IDLE 监听、发件人规则识别账单类型、待处理列表先预览再导入 | | 定时通知 | 每天/每周/每月提醒,邮箱 SMTP 与 Server 酱渠道,发送日志 | | 系统日志 | 访问日志、登录日志、运行日志在线查看,运行日志实时推送,自动清理 | diff --git a/guide/pages.md b/guide/pages.md index b19c15d..73c545c 100644 --- a/guide/pages.md +++ b/guide/pages.md @@ -13,6 +13,7 @@ | 收支记录 | `/transactions` | 记录管理与**账单导入** | 左侧菜单 | | 分类管理 | `/categories` | 收入/支出分类维护 | 左侧菜单 | | 标签管理 | `/tags` | 标签维护 | 左侧菜单 | +| 交易渠道管理 | `/channels` | 自定义交易渠道维护 | 收支记录页「渠道管理」按钮 | | 规则管理 | `/rules` | 自动归类规则 | 左侧菜单 | | 邮箱账单 | `/email-bills/*` | 邮箱配置 / 发件人规则 / 待处理账单 | 左侧菜单 | | 系统设置 | `/settings` | 数据库 / 注册 / 账单来源统计 / 外观 / AI / 通知配置 | 顶部用户菜单 | @@ -33,6 +34,8 @@ - **退出**:清空登录态并返回 `/login` > 「模型使用情况」没有独立菜单入口,需从「系统设置 → AI 模型配置 → 使用情况」进入。 +> +> 「交易渠道管理」同样没有独立菜单入口,需从「收支记录 → 渠道管理」进入。 ## 典型跳转路径 @@ -41,9 +44,10 @@ - **查看趋势明细**:统计面板 → 点击「收支趋势」某一列(柱子或该列空白处)→ 收支记录(自动带入时间范围、类型与当前筛选条件) - **账单导入**:收支记录 → 点击「导入账单」→ 解析预览 → 确认导入 → 回到收支记录 - **手机导入**:通知邮件链接 → `/bill-import`(独立页,非弹窗)→ 上传解析 → 确认导入 +- **渠道维护**:收支记录 → 点击「渠道管理」→ 新建 / 改名 / 删除自定义渠道 → 返回收支记录 - **自动归类**:规则管理配置规则 → 账单导入预览时命中并给出建议 - **邮箱账单**:邮箱配置 → 发件人规则 → 待处理账单 → 解析 → 导入到收支记录 -- **AI 辅助**:系统设置启用 AI → 统计面板「AI 搜索」、收支记录「一键设置 / 智能记账 / AI 查询」、分类与标签「AI 推荐」、规则管理「AI 生成规则」 +- **AI 辅助**:系统设置启用 AI → 统计面板「AI 搜索」、收支记录「一键设置 / 文字·图片记账 / AI 查询」、编辑记录「分类·标签·渠道 AI 推荐与新建」、分类与标签「AI 推荐」、规则管理「AI 生成规则」 - **系统维护**:顶部菜单 → 系统设置 / 系统日志 ## 页面关系说明 @@ -53,3 +57,4 @@ - 收支记录页的「导入账单」**弹窗**(桌面端常用); - 通知邮件直达的 **`/bill-import` 独立页**(面向手机,非弹窗,元素精简,系统内无其他入口)。 - **系统设置**聚合了数据库、注册、账单来源统计、外观、AI、通知六类配置;模型用量与日志分别从设置页与顶部菜单进入。 +- **交易渠道管理**是收支记录页的附属页,用于维护自定义渠道,维护完成后返回收支记录继续记账或筛选。 diff --git a/guide/settings.md b/guide/settings.md index d5d96c5..00f015e 100644 --- a/guide/settings.md +++ b/guide/settings.md @@ -56,6 +56,7 @@ - **API Key**:DeepSeek 的 API Key(已配置时留空表示不修改)。 - **接口地址**:默认 `https://api.deepseek.com`。 - **模型**:默认 `deepseek-chat`。 +- **视觉模型**:默认 `deepseek-flash`,用于[图片记账](/guide/ai#图片记账)。 - 提供「保存」与「测试连接」。 - 右上角「使用情况」可查看 AI 调用统计与明细,详见 [AI 能力](/guide/ai)。 diff --git a/guide/transactions.md b/guide/transactions.md index c25589a..e7f51bf 100644 --- a/guide/transactions.md +++ b/guide/transactions.md @@ -12,6 +12,7 @@ | 一键设置 | 对当前表格展示的记录批量设置分类、计入统计与标签 | | 批量删除 | 删除表格中勾选的记录 | | 导入账单 | 打开账单导入弹窗 | +| 渠道管理 | 进入[交易渠道管理](/guide/channels)页,维护自定义渠道 | | 导出 Excel | 按当前筛选条件导出 | | 刷新 | 按当前筛选条件与页码重新加载列表 | | 智能记账 | 用一句话描述收支,AI 解析后预填表单(需启用 AI) | @@ -90,7 +91,7 @@ ## 筛选与 AI 智能查询 -**常用筛选**:类型、分类(含「未分类」)、日期范围、备注关键字、交易渠道。 +**常用筛选**:类型、分类(含「未分类」)、日期范围、备注关键字、交易渠道。交易渠道下拉包含内置渠道与账号下的[自定义渠道](/guide/channels)。 **更多筛选**:标签(含「无标签」)、计入统计、金额区间。 @@ -105,19 +106,23 @@ ## 表格与行操作 -表格展示:交易时间、类型、分类、金额、交易渠道、备注、标签、计入统计开关与操作。 +表格展示:交易时间、类型、分类、金额、交易渠道、备注、标签、来源图片、计入统计开关与操作。 - **备注隐藏**:可将敏感备注持久隐藏为 `******`,需要时临时「查看」或「取消隐藏」。 - **计入统计**:行内开关可直接切换;关闭后该记录不纳入月度统计(适合借款、转账等),行内会以删除线标示。 -- **行操作**:编辑、查看「原始数据」(账单导入时的原始字段,以 JSON 展示)、删除。 +- **来源图片**:图片记账生成的记录会显示「查看(N)」入口,弹窗内先加载缩略图,可按需「加载原图」查看对应截图。 +- **行操作**:编辑、查看「原始数据」(账单导入或图片记账时的原始字段,以 JSON 展示)、删除。 + +![来源图片弹窗](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/source-images.png) 分页支持每页 10/20/50/100/200/500 条。 ## 查看原始数据 -账单导入时,系统会完整保留每笔交易在原始账单中的字段(以 JSON 存储)。在记录行点击「**原始数据**」即可查看,用于核对金额、交易状态、对方账号、交易类型等明细。 +账单导入或图片记账时,系统会完整保留每笔交易在原始账单中的字段(以 JSON 存储)。在记录行点击「**原始数据**」即可查看,用于核对金额、交易状态、对方账号、交易类型等明细。 -- 仅**账单导入**的记录有原始数据;**手动新增**的记录没有,该按钮不可用。 +- **账单导入**记录保留原始账单行字段;**图片记账**记录保留模型从截图提取的原始字段。 +- **手动新增**的记录没有原始数据,该按钮不可用。 - 原始数据是排查导入异常、核对账单的重要依据。 ## 一键设置(批量设置) @@ -137,10 +142,15 @@ ## AI 智能记账 -在「智能记账」弹窗中用自然语言描述一笔收支,例如「昨天中午吃饭 35 支付宝」,AI 会解析为类型、金额、分类、交易渠道、时间、备注、标签等字段,并预填到新增记录表单,确认后保存。 +「智能记账」弹窗提供**文字记账**与**图片记账**两种方式。 ![AI 智能记账弹窗](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/ai-record.png) +- **文字记账**:用一句话描述一笔收支,例如「昨天中午吃饭 35 支付宝」,AI 会解析为类型、金额、分类、交易渠道、时间、备注、标签等字段,并预填到新增记录表单,确认后保存。 +- **图片记账**:上传一至多张账单或支付截图(也可 `Ctrl+V` 粘贴),AI 实时识别其中的每一笔交易,生成可编辑的记录列表,勾选后批量导入。 + +再次打开弹窗会记住上次使用的记账方式。图片记账的用法与限制详见 [AI 能力 → 图片记账](/guide/ai#图片记账)。 + ## 手动添加记录 1. 在收支记录页右上角点击「**新增记录**」。 @@ -151,7 +161,7 @@ | 类型 | 支出 / 收入 | | 分类 | 新增时必填 | | 金额 | 大于 0 | - | 交易渠道 | 可选 | + | 交易渠道 | 可选,含内置渠道与[自定义渠道](/guide/channels);启用 AI 时可一键推荐或新建 | | 交易时间 | 默认当前时间 | | 备注 | 可选 | | 标签 | 可选,可多选 | @@ -169,7 +179,7 @@ ### 编辑时的 AI 辅助 -启用 AI 后,编辑弹窗中的**分类**与**标签**旁会出现 AI 辅助按钮(与新增记录一致): +启用 AI 后,编辑弹窗中的**分类**、**标签**与**交易渠道**旁会出现 AI 辅助按钮(与新增记录一致): | 按钮 | 作用 | |---|---| @@ -177,6 +187,10 @@ | 分类 · **AI 新建** | 根据当前账单信息生成新分类;确认「保存并选用」后创建并选中,若已存在同名同类型分类则直接选中 | | 标签 · **AI 推荐** | 推荐已有标签并追加(自动去重) | | 标签 · **AI 新建** | 生成新标签(含建议颜色),确认后创建并选中;同名标签直接选中 | +| 渠道 · **AI 推荐** | 根据当前类型、金额、渠道、备注,从已有渠道中推荐并填入 | +| 渠道 · **AI 新建** | 生成新渠道名称;确认「保存并选用」后创建并选中,同名渠道直接选中 | + +![编辑记录中的渠道 AI 辅助](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/ai-channel-suggest.png) > 「AI 新建」需先填写备注或金额,便于模型理解;生成结果会显示确认条,点击「保存并选用」才真正创建。 diff --git a/index.md b/index.md index cdf8474..f0edaec 100644 --- a/index.md +++ b/index.md @@ -4,7 +4,7 @@ layout: home hero: name: 收支管理系统 text: 个人收支管理 · 多来源账单导入 · AI 智能记账 - tagline: 支持支付宝、微信、美团、淘宝、京东、招商银行、交通银行账单导入,提供分类标签管理、自定义规则、AI 建议与定时提醒 + tagline: 支持支付宝、微信、美团、淘宝、京东、招商银行、交通银行账单导入,提供分类标签与交易渠道管理、自定义规则、AI 建议(含多图图片记账)与定时提醒 image: src: https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/dashboard.png alt: 统计面板 @@ -24,10 +24,12 @@ features: details: 支持支付宝、微信、美团、淘宝、京东、招商银行、交通银行账单,解析预览后确认导入,按交易号自动去重。 - title: 分类与标签 details: 自定义收入/支出分类与标签,支持批量设置,AI 可推荐 Emoji 图标与标签颜色。 + - title: 交易渠道管理 + details: 内置常用渠道,支持自定义渠道的新建、改名与删除,记账时可由 AI 推荐或生成;改名不影响历史账单,筛选与统计均可选用。 - title: 自定义规则 details: 按关键词、收支类型、金额区间组合条件,命中后自动应用分类与标签,导入预览即展示建议。 - title: AI 智能能力 - details: 基于 DeepSeek 的分类/标签建议、自然语言查询与记账、分类与标签生成,并统计 token 用量。 + details: 基于 DeepSeek 的分类/标签/渠道建议、自然语言查询、文字与多图图片记账(实时进度)、分类标签渠道生成,并统计 token 用量。 - title: 统计与图表 details: 月度收支汇总、分类占比、收支趋势、支付方式占比,支持多维度筛选与自定义时间范围。 - title: 定时提醒与通知 diff --git a/scripts/ts/browser.ts b/scripts/ts/browser.ts index 25479f5..75dc080 100644 --- a/scripts/ts/browser.ts +++ b/scripts/ts/browser.ts @@ -30,7 +30,10 @@ export async function newContext( browser: Browser, viewport: { width: number; height: number } = DEFAULT_VIEWPORT, ): Promise { - return browser.newContext({ viewport }) + const context = await browser.newContext({ viewport }) + // 首次访问需等待 Vite 按需编译,放宽默认导航超时,避免冷启动误判失败 + context.setDefaultNavigationTimeout(120000) + return context } /** diff --git a/scripts/ts/capture-elements.ts b/scripts/ts/capture-elements.ts index bc7747d..f959ff2 100644 --- a/scripts/ts/capture-elements.ts +++ b/scripts/ts/capture-elements.ts @@ -3,7 +3,7 @@ import { resolve } from 'node:path' import { pathToFileURL } from 'node:url' import type { Page } from 'playwright-core' import { clickText, injectSession, launchBrowser, login, newContext, waitPage } from './browser.js' -import { BASE_URL, ELEM_DIR, SAMPLE_BILL } from './config.js' +import { BASE_URL, ELEM_DIR, SAMPLE_BILL, SAMPLE_IMAGE, SAMPLE_IMAGE2 } from './config.js' /** 页面就绪标志:列表页表格、设置面板或卡片 */ const PAGE_READY = '.n-data-table, .config-card, .n-card' @@ -26,6 +26,7 @@ const MODAL_SCENES: Array<[string, string, string]> = [ ['add-rule', '/rules', '新增规则'], ['add-email-config', '/email-bills/configs', '新增'], ['add-channel', '/settings', '添加渠道'], + ['channel-create', '/channels', '新建渠道'], ] /** @@ -110,6 +111,101 @@ export async function captureBillParse(page: Page): Promise { } } +/** + * 截取「图片记账」:多图上传 Tab 与识别结果弹窗。 + * + * @param page Page 实例 + * @param importRecords 识别后是否导入勾选记录(默认 true,便于后续截取来源图片) + */ +export async function captureAiRecordImage(page: Page, importRecords = true): Promise { + try { + await page.setViewportSize({ width: 1440, height: 1000 }) + await page.goto(BASE_URL + '/transactions') + await waitPage(page, { selector: PAGE_READY }) + if (!(await clickText(page, '智能记账'))) throw new Error('未找到「智能记账」按钮') + const modal = page.locator('.n-modal').first() + await modal.waitFor({ state: 'visible', timeout: 20000 }) + + // 切到「图片记账」标签页,上传两张虚构账单截图后截取上传界面 + await modal.locator('.n-tabs-tab', { hasText: '图片记账' }).first().click() + await page.waitForTimeout(600) + await modal.locator("input[type='file']").setInputFiles([SAMPLE_IMAGE, SAMPLE_IMAGE2]) + await page.waitForTimeout(600) + await modal.screenshot({ path: resolve(ELEM_DIR, 'ai-record-image.png') }) + console.log('[OK] modal/ai-record-image') + + const recognize = modal.locator('button', { hasText: '识别图片' }).first() + await recognize.evaluate((el) => (el as HTMLElement).click()) + + // 结果弹窗出现(标题「图片记账结果」)且表格有数据 + const resultModal = page.locator('.n-modal', { hasText: '图片记账结果' }).first() + await resultModal.waitFor({ state: 'visible', timeout: 180000 }) + await resultModal.locator('.n-data-table-tbody tr').first().waitFor({ state: 'visible', timeout: 180000 }) + await page.waitForLoadState('networkidle', { timeout: 20000 }).catch(() => undefined) + await page.waitForTimeout(800) + await resultModal.screenshot({ path: resolve(ELEM_DIR, 'ai-record-result.png') }) + console.log('[OK] modal/ai-record-result') + + // 导入勾选记录,便于后续「来源图片」截图(去重键保证重复执行不会产生新记录) + if (importRecords) { + const importBtn = resultModal.locator('button', { hasText: '导入勾选' }).first() + await importBtn.evaluate((el) => (el as HTMLElement).click()) + await resultModal.waitFor({ state: 'hidden', timeout: 60000 }).catch(() => undefined) + await page.waitForTimeout(1000) + console.log('[OK] modal/ai-record-import') + } + } catch (err) { + console.log(`[FAIL] modal/ai-record-image: ${err}`) + } +} + +/** + * 截取「来源图片」查看弹窗(点击表格中图片记账记录的「查看(N)」)。 + * + * @param page Page 实例 + */ +export async function captureSourceImages(page: Page): Promise { + try { + await page.setViewportSize({ width: 1440, height: 1000 }) + await page.goto(BASE_URL + '/transactions') + await waitPage(page, { selector: '.n-data-table-tbody tr' }) + const viewBtn = page.locator('button', { hasText: /查看(\d+)/ }).first() + await viewBtn.waitFor({ state: 'visible', timeout: 20000 }) + await viewBtn.evaluate((el) => (el as HTMLElement).click()) + const modal = page.locator('.n-modal', { hasText: '来源图片' }).first() + await modal.waitFor({ state: 'visible', timeout: 20000 }) + await page.waitForLoadState('networkidle', { timeout: 20000 }).catch(() => undefined) + await page.waitForTimeout(1000) + await modal.screenshot({ path: resolve(ELEM_DIR, 'source-images.png') }) + console.log('[OK] modal/source-images') + } catch (err) { + console.log(`[FAIL] modal/source-images: ${err}`) + } +} + +/** + * 截取编辑记录弹窗中「交易渠道」的 AI 推荐与新建按钮。 + * + * @param page Page 实例 + */ +export async function captureChannelAi(page: Page): Promise { + try { + await page.setViewportSize({ width: 1440, height: 1000 }) + await page.goto(BASE_URL + '/transactions') + await waitPage(page, { selector: PAGE_READY }) + const editBtn = page.locator('tbody tr').first().locator('button', { hasText: '编辑' }).first() + await editBtn.waitFor({ state: 'visible', timeout: 20000 }) + await editBtn.click() + const modal = page.locator('.n-modal').first() + await modal.waitFor({ state: 'visible', timeout: 20000 }) + await page.waitForTimeout(800) + await modal.screenshot({ path: resolve(ELEM_DIR, 'ai-channel-suggest.png') }) + console.log('[OK] modal/ai-channel-suggest') + } catch (err) { + console.log(`[FAIL] modal/ai-channel-suggest: ${err}`) + } +} + /** * 截取「点击趋势柱状图跳转收支记录并自动带入筛选条件」的筛选卡片。 * @@ -228,15 +324,17 @@ export async function captureAiRule(page: Page): Promise { } /** - * 截取系统设置的六个配置面板。 + * 截取系统设置的配置面板。 * * @param page Page 实例 + * @param only 仅截取指定标题的面板(可选,缺省截取全部) */ -export async function captureSettingsPanels(page: Page): Promise { +export async function captureSettingsPanels(page: Page, only?: string[]): Promise { await page.setViewportSize({ width: 1440, height: 1200 }) await page.goto(BASE_URL + '/settings') await waitPage(page, { selector: PAGE_READY }) - for (const [name, title] of SETTINGS_PANELS) { + const panels = only ? SETTINGS_PANELS.filter(([, title]) => only.includes(title)) : SETTINGS_PANELS + for (const [name, title] of panels) { try { const card = page.locator('.config-card', { hasText: title }).first() await card.waitFor({ state: 'visible', timeout: 20000 }) @@ -265,6 +363,9 @@ async function main(): Promise { await captureModals(page) await captureEditRecord(page) await captureAiRule(page) + await captureAiRecordImage(page) + await captureSourceImages(page) + await captureChannelAi(page) await captureSettingsPanels(page) await captureBillParse(page) await captureTrendJump(page) diff --git a/scripts/ts/capture-pages.ts b/scripts/ts/capture-pages.ts index a142a2c..f103729 100644 --- a/scripts/ts/capture-pages.ts +++ b/scripts/ts/capture-pages.ts @@ -14,6 +14,7 @@ const PAGES: Array<[string, string, number, number, string]> = [ ['categories', '/categories', 1440, 1100, '.n-data-table-tbody tr'], ['tags', '/tags', 1440, 1100, '.n-data-table-tbody tr'], ['rules', '/rules', 1440, 1100, '.n-data-table-tbody tr'], + ['channels', '/channels', 1440, 1000, '.n-data-table-tbody tr'], ['email-bills-configs', '/email-bills/configs', 1440, 1000, '.n-data-table-tbody tr'], ['email-bills-sender-rules', '/email-bills/senderRules', 1440, 1000, '.n-data-table-tbody tr'], ['email-bills-messages', '/email-bills/messages', 1440, 1000, '.n-data-table-tbody tr'], diff --git a/scripts/ts/config.ts b/scripts/ts/config.ts index 24db487..5007807 100644 --- a/scripts/ts/config.ts +++ b/scripts/ts/config.ts @@ -24,3 +24,7 @@ export const OUT_DIR = resolve(ROOT, 'public', 'screenshots') export const ELEM_DIR = resolve(OUT_DIR, 'elements') /** 样例账单文件(虚构数据、未导入,用于「账单解析预览」截图) */ export const SAMPLE_BILL = resolve(ROOT, 'scripts', 'samples', 'alipay_fake_preview.csv') +/** 样例账单截图(虚构数据,用于「图片记账」识别截图) */ +export const SAMPLE_IMAGE = resolve(ROOT, 'scripts', 'samples', 'ai-bill_fake.png') +/** 第二张样例账单截图(虚构数据,用于演示多图识别) */ +export const SAMPLE_IMAGE2 = resolve(ROOT, 'scripts', 'samples', 'ai-bill_fake2.png')