Compare commits
10
Commits
a4ae7ac979
...
41d1a4d5a9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41d1a4d5a9 | ||
|
|
d94d4e1ed5 | ||
|
|
5eb1d331f9 | ||
|
|
ca1b8c05d5 | ||
|
|
8bfdde24f2 | ||
|
|
b1fb2c68a9 | ||
|
|
4425a6ba04 | ||
|
|
923d6db5c0 | ||
|
|
b979c6e498 | ||
|
|
d2e4a51f2b |
+13
-7
@@ -2,15 +2,15 @@ import { defineConfig } from 'vitepress'
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
lang: 'zh-CN',
|
lang: 'zh-CN',
|
||||||
title: '收支管理系统',
|
title: '汇账',
|
||||||
description: '个人收支管理系统使用与开发文档',
|
description: '个人汇账使用与开发文档',
|
||||||
head: [['link', { rel: 'icon', href: '/favicon.svg', type: 'image/svg+xml' }]],
|
head: [['link', { rel: 'icon', href: '/favicon.svg', type: 'image/svg+xml' }]],
|
||||||
lastUpdated: true,
|
lastUpdated: true,
|
||||||
cleanUrls: true,
|
cleanUrls: true,
|
||||||
ignoreDeadLinks: true,
|
ignoreDeadLinks: true,
|
||||||
// 维护类文档不参与站点构建
|
// 维护类文档不参与站点构建
|
||||||
srcExclude: ['AGENTS.md'],
|
srcExclude: ['AGENTS.md', 'todo.md'],
|
||||||
// 文档站默认端口从 5173 调整为 5175,把 5173 让给 finance-app 前端
|
// 文档站默认端口从 5173 调整为 5175,把 5173 让给 ledgerly 前端
|
||||||
vite: {
|
vite: {
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
@@ -30,6 +30,8 @@ export default defineConfig({
|
|||||||
items: [
|
items: [
|
||||||
{ text: '项目简介', link: '/guide/introduction' },
|
{ text: '项目简介', link: '/guide/introduction' },
|
||||||
{ text: '快速开始', link: '/guide/getting-started' },
|
{ text: '快速开始', link: '/guide/getting-started' },
|
||||||
|
{ text: '下载与安装', link: '/guide/download' },
|
||||||
|
{ text: '日历', link: '/guide/calendar' },
|
||||||
{ text: '统计面板', link: '/guide/dashboard' },
|
{ text: '统计面板', link: '/guide/dashboard' },
|
||||||
{ text: '页面总览', link: '/guide/pages' },
|
{ text: '页面总览', link: '/guide/pages' },
|
||||||
],
|
],
|
||||||
@@ -38,7 +40,9 @@ export default defineConfig({
|
|||||||
text: '记账与整理',
|
text: '记账与整理',
|
||||||
items: [
|
items: [
|
||||||
{ text: '收支记录', link: '/guide/transactions' },
|
{ text: '收支记录', link: '/guide/transactions' },
|
||||||
|
{ text: '账单关系', link: '/guide/relations' },
|
||||||
{ text: '分类与标签', link: '/guide/categories-tags' },
|
{ text: '分类与标签', link: '/guide/categories-tags' },
|
||||||
|
{ text: '交易渠道管理', link: '/guide/channels' },
|
||||||
{ text: '规则管理', link: '/guide/rules' },
|
{ text: '规则管理', link: '/guide/rules' },
|
||||||
{ text: '邮箱账单', link: '/guide/email-bills' },
|
{ text: '邮箱账单', link: '/guide/email-bills' },
|
||||||
],
|
],
|
||||||
@@ -47,14 +51,15 @@ export default defineConfig({
|
|||||||
text: '智能与自动化',
|
text: '智能与自动化',
|
||||||
items: [
|
items: [
|
||||||
{ text: 'AI 能力', link: '/guide/ai' },
|
{ text: 'AI 能力', link: '/guide/ai' },
|
||||||
|
{ text: '智能查数', link: '/guide/sql-query' },
|
||||||
{ text: '定时提醒与通知', link: '/guide/notifications' },
|
{ text: '定时提醒与通知', link: '/guide/notifications' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: '系统',
|
text: '系统',
|
||||||
items: [
|
items: [
|
||||||
{ text: '系统设置', link: '/guide/settings' },
|
{ text: '设置', link: '/guide/settings' },
|
||||||
{ text: '系统日志', link: '/guide/logs' },
|
{ text: '日志', link: '/guide/logs' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -87,6 +92,7 @@ export default defineConfig({
|
|||||||
{ text: 'AI 集成', link: '/dev/ai' },
|
{ text: 'AI 集成', link: '/dev/ai' },
|
||||||
{ text: '定时通知', link: '/dev/notifications' },
|
{ text: '定时通知', link: '/dev/notifications' },
|
||||||
{ text: '动态数据源', link: '/dev/database' },
|
{ text: '动态数据源', link: '/dev/database' },
|
||||||
|
{ text: '前端用户偏好', link: '/dev/preferences' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -103,7 +109,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
socialLinks: [],
|
socialLinks: [],
|
||||||
footer: {
|
footer: {
|
||||||
message: '基于 Spring Boot 3 + Vue 3 的个人收支管理系统',
|
message: '基于 Spring Boot 3 + Vue 3 的个人汇账',
|
||||||
},
|
},
|
||||||
docFooter: {
|
docFooter: {
|
||||||
prev: '上一页',
|
prev: '上一页',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 收支管理系统 · 文档站
|
# 汇账 · 文档站
|
||||||
|
|
||||||
本目录是「收支管理系统」(同级目录 `../finance-app`)的**使用文档**与**开发文档**站点,基于 [VitePress](https://vitepress.dev/) 构建,并附带一套 Node/TypeScript 维护脚本(截图、演示数据、资料抓取)。
|
本目录是「汇账」(同级目录 `../ledgerly`)的**使用文档**与**开发文档**站点,基于 [VitePress](https://vitepress.dev/) 构建,并附带一套 Node/TypeScript 维护脚本(截图、演示数据、资料抓取)。
|
||||||
|
|
||||||
> **在线体验**:[https://finance.rosercode.cn/](https://finance.rosercode.cn/)(演示账号 `demo` / `demo123456`,使用独立的 SQLite 演示库与虚构数据)
|
> **在线体验**:[https://finance.rosercode.cn/](https://finance.rosercode.cn/)(演示账号 `demo` / `demo123456`,使用独立的 SQLite 演示库与虚构数据)
|
||||||
|
|
||||||
@@ -54,10 +54,11 @@ doc-site/
|
|||||||
│ ├── config.mts # 站点配置(导航、侧边栏、搜索、端口 5175)
|
│ ├── config.mts # 站点配置(导航、侧边栏、搜索、端口 5175)
|
||||||
│ └── theme/ # 自定义主题(图片全屏查看、图片样式)
|
│ └── theme/ # 自定义主题(图片全屏查看、图片样式)
|
||||||
├── scripts/
|
├── scripts/
|
||||||
│ ├── ts/ # 维护脚本(TypeScript)
|
│ ├── ts/ # 维护脚本(TypeScript,含截图、演示数据、OSS 上传)
|
||||||
│ └── samples/ # 样例账单
|
│ └── samples/ # 虚构样例账单
|
||||||
├── refs/ # 联网检索到的资料原文(不参与构建)
|
├── refs/ # 联网检索到的资料原文(不参与构建)
|
||||||
├── jar-home/ # jar 一体化实例运行目录(测试库副本)
|
├── jar-home/ # jar 一体化实例运行目录(测试库副本)
|
||||||
|
├── .env.local # 截图 OSS 上传等本地配置(不入库)
|
||||||
├── package.json
|
├── package.json
|
||||||
└── tsconfig.json
|
└── tsconfig.json
|
||||||
```
|
```
|
||||||
@@ -67,13 +68,15 @@ doc-site/
|
|||||||
脚本位于 `scripts/ts/`,用 `tsx` 直接运行 TypeScript,浏览器自动化基于 `playwright-core`(Chromium),HTTP 基于 `axios`。
|
脚本位于 `scripts/ts/`,用 `tsx` 直接运行 TypeScript,浏览器自动化基于 `playwright-core`(Chromium),HTTP 基于 `axios`。
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm run capture # 一键重新截图(整页 + 元素 + 卡片 + 编辑/设置面板 + 解析预览)
|
npm run capture # 一键重新截图(整页 + 元素 + 卡片 + 编辑/设置面板 + 解析预览,慎用)
|
||||||
npm run capture:pages # 仅整页截图
|
npm run capture:pages # 仅整页截图
|
||||||
npm run capture:elements # 仅元素截图
|
npm run capture:elements # 仅元素截图
|
||||||
|
npm run make:bills # 生成虚构样例账单
|
||||||
npm run seed # 导入样例账单(演示数据)
|
npm run seed # 导入样例账单(演示数据)
|
||||||
npm run seed:rules # 补充标签与规则
|
npm run seed:rules # 补充标签与规则
|
||||||
npm run refs:search # 检索资料到 refs/
|
npm run refs:search # 检索资料到 refs/
|
||||||
npm run refs:fetch # 抓取指定 URL 到 refs/
|
npm run refs:fetch # 抓取指定 URL 到 refs/
|
||||||
|
npm run oss:upload # 上传截图到 OSS(读取 .env.local)
|
||||||
npm run test:browser # 验证浏览器链路
|
npm run test:browser # 验证浏览器链路
|
||||||
npm run typecheck:scripts # 类型检查
|
npm run typecheck:scripts # 类型检查
|
||||||
```
|
```
|
||||||
@@ -83,7 +86,7 @@ npm run typecheck:scripts # 类型检查
|
|||||||
1. 应用实例已启动(默认访问 `jar-home/` 下的 jar 实例 `localhost:18082`);
|
1. 应用实例已启动(默认访问 `jar-home/` 下的 jar 实例 `localhost:18082`);
|
||||||
2. 测试库中已配置 AI(用于截取启用 AI 后的界面)。
|
2. 测试库中已配置 AI(用于截取启用 AI 后的界面)。
|
||||||
|
|
||||||
可通过环境变量覆盖:`FINANCE_WEB`、`FINANCE_API`、`FINANCE_USER`、`FINANCE_PASSWORD`。
|
可通过环境变量覆盖:`LEDGERLY_WEB`、`LEDGERLY_API`、`LEDGERLY_USER`、`LEDGERLY_PASSWORD`。
|
||||||
|
|
||||||
> **环境安全**:截图与演示数据只操作测试环境,禁止访问真实环境(5173 / 8080 / MySQL 真实库)。
|
> **环境安全**:截图与演示数据只操作测试环境,禁止访问真实环境(5173 / 8080 / MySQL 真实库)。
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,16 @@
|
|||||||
|
|
||||||
## 客户端
|
## 客户端
|
||||||
|
|
||||||
`DeepSeekClient` 基于 **OpenAI 官方 Java SDK**(`com.openai:openai-java`)调用,通过自定义 `baseUrl` 指向 DeepSeek 的 OpenAI 兼容接口:
|
`DeepSeekClient` 直接调用 DeepSeek 的 **OpenAI 兼容 Chat Completions 接口**(`{baseUrl}/chat/completions`),为减小可执行 jar 体积,使用 **Spring `RestClient` + JDK `HttpClient`** 而非第三方 SDK:
|
||||||
|
|
||||||
- 使用 `OpenAIOkHttpClient`,按 `(baseUrl, apiKey)` 缓存客户端(上限 32,超出后清空重建)。
|
- 请求体包含 `model`、`messages`、`temperature = 0.2`、`response_format = {"type":"json_object"}`、`stream = false`。
|
||||||
- `temperature = 0.2`,`response_format = {"type":"json_object"}`,非流式,`maxRetries(0)`,请求超时 60 秒。
|
- 文本请求超时 **120 秒**,视觉请求超时 **240 秒**(`chatWithImages` 单独放宽)。
|
||||||
- 取 `choices[0].message.content` 与 `usage`(输入/输出/总 token;缓存命中 token 取 `promptTokensDetails.cachedTokens`,DeepSeek 私有字段读不到时为 0)。
|
- 解析 `choices[0].message.content` 与 `usage`(输入/输出/总 token;缓存命中 token 取 DeepSeek 的缓存字段,读不到时为 0)。
|
||||||
- 对外保持 `chat(baseUrl, apiKey, model, systemPrompt, userPrompt)` 与 `DeepSeekResponse` 不变,各 `Ai*ServiceImpl` 无需改动。
|
- 对外保持 `chat(baseUrl, apiKey, model, systemPrompt, userPrompt)` 与 `DeepSeekResponse` 不变,各 `Ai*ServiceImpl` 无需改动。
|
||||||
|
- 另有 `chatWithImages(baseUrl, apiKey, model, systemPrompt, userText, imageDataUrls)`:将一张或多张图片以 base64 data URL 作为 `user` 消息的内容块发送(DeepSeek 视觉接口要求图片只能出现在 user 消息中),供图片记账使用。
|
||||||
|
- 调用异常统一转 `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,19 @@
|
|||||||
|
|
||||||
- **禁止**:`rawData`(账单原始 JSON)、金额、交易号、账户/卡号、姓名、地址、电话等。
|
- **禁止**:`rawData`(账单原始 JSON)、金额、交易号、账户/卡号、姓名、地址、电话等。
|
||||||
- **允许**:收支类型、交易渠道、备注(必要时截断/脱敏)、分类与标签的名称/ID、用户主动输入的自然语言文本。
|
- **允许**:收支类型、交易渠道、备注(必要时截断/脱敏)、分类与标签的名称/ID、用户主动输入的自然语言文本。
|
||||||
|
- **智能查数**:仅发送用户提问与可查询的**表结构说明**,不发送任何账单数据行;数据查询与隔离在本地由安全网关完成。
|
||||||
|
- **图片记账例外**:用户主动上传的账单/支付截图会**整张**发送给视觉模型(可能含金额、交易对方、订单号等),视为用户主动提供的输入;图片同时落盘于 `data/ai-images/`,仅本人可经 `GET /api/ai/image/{id}` 访问,禁止用于其他外发场景。
|
||||||
|
|
||||||
|
## 提示词模板
|
||||||
|
|
||||||
|
各功能的提示词统一抽取为 **Thymeleaf 文本模板**,存放于 `src/main/resources/prompts/`(`spring.thymeleaf` 配置 `prefix=classpath:/prompts/`、`suffix=.tpl`、`mode=TEXT`),由 `PromptTemplateService` 加载渲染。
|
||||||
|
|
||||||
|
- 每个功能一个模板文件,如 `ai-record.tpl`、`ai-channel.tpl`、`ai-relation.tpl`、`ai-sql.tpl` 等,共 14 个。
|
||||||
|
- **系统提示词与用户提示词合并为同一文件的不同片段**,由注入的 `__fragment` 变量经 `th:if` 选择:一般功能为 `system` / `user` 两段,建议功能(`ai-suggestion.tpl`)为 `strictSystem` / `newSystem` / `user` 三段;服务端调用 `render(template, fragment, vars)` 时传入片段名与变量。
|
||||||
|
|
||||||
## 调用类型
|
## 调用类型
|
||||||
|
|
||||||
`AiCallType` 定义了 9 类调用,全部记录用量:
|
`AiCallType` 定义了 13 类调用,全部记录用量:
|
||||||
|
|
||||||
| 枚举值 | 含义 |
|
| 枚举值 | 含义 |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -30,11 +41,15 @@
|
|||||||
| `TEST` | 连接测试 |
|
| `TEST` | 连接测试 |
|
||||||
| `QUERY` | 自然语言查询解析为筛选条件 |
|
| `QUERY` | 自然语言查询解析为筛选条件 |
|
||||||
| `RECORD` | 自然语言记账解析为交易记录 |
|
| `RECORD` | 自然语言记账解析为交易记录 |
|
||||||
|
| `RECORD_IMAGE` | 图片记账解析(视觉模型) |
|
||||||
| `DECORATE` | 分类 Emoji / 标签颜色建议 |
|
| `DECORATE` | 分类 Emoji / 标签颜色建议 |
|
||||||
| `CATEGORY` | 自然语言生成分类 |
|
| `CATEGORY` | 自然语言生成分类 |
|
||||||
| `TAG` | 自然语言生成标签 |
|
| `TAG` | 自然语言生成标签 |
|
||||||
|
| `CHANNEL` | 自然语言生成交易渠道 |
|
||||||
| `RULE` | 由已打标签记录生成分类与标签规则 |
|
| `RULE` | 由已打标签记录生成分类与标签规则 |
|
||||||
| `STATISTICS` | 自然语言统计查询解析 |
|
| `STATISTICS` | 自然语言统计查询解析 |
|
||||||
|
| `RELATION` | 由关联账单生成关系名称与备注 |
|
||||||
|
| `SQL` | 自然语言转换为只读 SQL 查询(智能查数) |
|
||||||
|
|
||||||
## 分类/标签建议
|
## 分类/标签建议
|
||||||
|
|
||||||
@@ -42,6 +57,7 @@
|
|||||||
|
|
||||||
- 先按「类型 + 交易渠道 + 备注」聚合去重,仅将代表记录送模型;单批 ≤ 100 条、总量 ≤ 500 条,超出则分批调用。
|
- 先按「类型 + 交易渠道 + 备注」聚合去重,仅将代表记录送模型;单批 ≤ 100 条、总量 ≤ 500 条,超出则分批调用。
|
||||||
- 支持严格模式(仅从已有分类/标签中选择)与增强模式(可返回新分类/新标签)。
|
- 支持严格模式(仅从已有分类/标签中选择)与增强模式(可返回新分类/新标签)。
|
||||||
|
- **单条建议**额外下发当前用户的渠道候选(内置取中文名、自定义取用户名称),模型可返回 `channel`,服务端仅接受候选中的编码;批量建议不下发渠道候选。
|
||||||
- 应用时分类覆盖写入、标签追加;新建分类按「类型 + 名称」去重,新建标签按名称去重并校验颜色格式。
|
- 应用时分类覆盖写入、标签追加;新建分类按「类型 + 名称」去重,新建标签按名称去重并校验颜色格式。
|
||||||
|
|
||||||
## 自然语言查询
|
## 自然语言查询
|
||||||
@@ -52,18 +68,57 @@
|
|||||||
|
|
||||||
`AiRecordServiceImpl` 解析类型、金额、分类、标签、交易渠道、交易时间、备注、计入统计等字段;金额无效直接报错,时间缺失回退当前时间。
|
`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`,识别在固定线程池(8 线程)中执行,过程中逐条推送 `progress` 事件,结束推送 `done`(完整结果 JSON)或 `failed`(失败原因)。连接超时 **5 分钟**,并每 **15 秒**发送一次 SSE 注释行作为心跳保活(前端 SSE 客户端自动跳过注释行)。
|
||||||
|
|
||||||
|
**图片访问**:`GET /api/ai/image/{id}` 返回原图,`?thumbnail=true` 返回最长边 1280 的 JPEG 缩略图;仅图片所属用户可访问。收支记录「账单详情」弹窗中的来源图片默认加载缩略图,用户可按需切换原图。
|
||||||
|
|
||||||
## 规则生成
|
## 规则生成
|
||||||
|
|
||||||
`AiRuleServiceImpl` 由用户勾选的**已打标签记录**反推分类/标签规则:
|
`AiRuleServiceImpl` 由用户勾选的记录反推分类/标签规则,有两个入口:规则管理页(依据**已打标签记录**)与收支记录页批量操作(依据**任意勾选账单**):
|
||||||
|
|
||||||
- 仅加载属于当前用户且被勾选的记录(单次 ≤ 500 条),附带其标签名称与分类名;提示词字段为 `type / remark / channel / category / tags`(不含金额与 `rawData`),并对相同组合去重。
|
- 仅加载属于当前用户且被勾选的记录(单次 ≤ 500 条),附带其标签名称与分类名;提示词字段为 `type / remark / channel / category / tags`(不含金额与 `rawData`),并对相同「类型 + 备注 + 渠道 + 标签」组合去重。
|
||||||
- 解析并校验模型输出(分类 ID 归属与类型、标签归属、名称长度、颜色格式等)。
|
- 解析并校验模型输出(分类 ID 归属与类型、标签归属、名称长度、颜色格式等)。
|
||||||
- **去重**:按「类型 + 归一化关键词 + 分类 + 排序标签集合」计算签名;与已有规则或本次建议内部重复的条目标记 `duplicate=true`(前端默认不勾选且禁止导入),`apply` 时后端再次校验并跳过。
|
- **去重**:按「类型 + 归一化关键词 + 分类 + 排序标签集合」计算签名;与已有规则或本次建议内部重复的条目标记 `duplicate=true`(前端默认不勾选且禁止导入),`apply` 时后端再次校验并跳过。
|
||||||
|
- **编辑后重算去重**:规则弹窗(两处入口共用的 `AiRuleEditorTable.vue`)支持逐字段编辑;任一参与签名的字段(类型/关键词/金额区间/分类/标签)变化后防抖调用 `POST /api/ai/rule/check-duplicates`(`AiRuleServiceImpl.checkDuplicates`,纯本地复用签名逻辑)刷新 `duplicate`,并自动增删勾选。
|
||||||
|
- **应用到交易**:`POST /api/ai/rule/apply-transactions`(`applyToTransactions`)按传入规则对指定交易做**本地匹配**(不调用大模型、不外发数据),命中时覆盖分类、追加标签,必要时新建分类/标签;用于收支记录页勾选「同时应用到账单」的场景,该开关默认开启并经 `ledgerly.transaction.aiRuleApplyToSelection` 持久化。
|
||||||
|
|
||||||
## 统计搜索
|
## 统计搜索
|
||||||
|
|
||||||
`AiStatisticsServiceImpl` 在提示词中注入分类、标签与当前日期,将自然语言解析为统计面板参数:时间范围(预设→绝对时间)、收支类型、分类、标签、交易渠道、金额区间、备注、统计口径(`statsScope`)与趋势粒度(`dimension`)。
|
`AiStatisticsServiceImpl` 在提示词中注入分类、标签与当前日期,将自然语言解析为统计面板参数:时间范围(预设→绝对时间)、收支类型、分类、标签、交易渠道、金额区间、备注、统计口径(`statsScope`)与趋势粒度(`dimension`)。
|
||||||
|
|
||||||
|
## 生成账单关系名称
|
||||||
|
|
||||||
|
`AiRelationServiceImpl` 依据用户新建关系组时勾选的**关联账单**生成名称与备注,对应 `POST /api/ai/relation`。
|
||||||
|
|
||||||
|
- 请求体为 `AiRelationRequest`(`type` + `members`);每个成员仅携带白名单字段——**收支类型、交易渠道、分类名称与备注**(`AiRelationMemberRequest` 明确不含金额、交易号与 `rawData`)。
|
||||||
|
- 用户提示词由服务端集中拼接(关系类型中文名 + 成员字段,备注截断 30 字,最多 20 条);系统提示词模板 `ai-relation`(`system` 片段)要求输出 4–12 字名称、≤40 字备注,并说明借贷场景(借出/归还、分次归还)的判定,只输出 JSON。
|
||||||
|
- 解析后名称截断 ≤50 且必须非空、备注 ≤200;经 `AiUsageService` 记录 `AiCallType.RELATION` 用量。前端在[账单关系](/guide/relations)编辑弹窗中用它覆盖名称(备注非空时覆盖备注)。
|
||||||
|
|
||||||
|
## 智能查数(Text-to-SQL)
|
||||||
|
|
||||||
|
`AiSqlQueryService` 将自然语言交给大模型生成**只读 SQL** 并执行,配套独立的提问历史,对应 `POST /api/ai/sql`。
|
||||||
|
|
||||||
|
- 系统提示词模板 `ai-sql`(`system` 片段):按当前数据源方言(SQLite / MySQL)注入日期函数说明与**动态表结构**——`TextToSqlSchemaProvider` 从数据库元数据读取列(字段增删改自动同步),并剔除 `raw_data`、`external_no` 等敏感列;要求模型只输出 `{sql, explanation, chartType}`,且**不要自带 `user_id`**(由网关统一注入)。
|
||||||
|
- **安全网关** `cn.rosercode.ledgerly.util.sql.TextToSqlGuard` 是唯一执行通道:仅允许单条 `SELECT`(拒绝写操作/DDL/多语句/`SELECT INTO`/`FOR UPDATE`/危险函数);表白名单由 `TextToSqlTables` 静态登记(`t_transaction`、`t_category`、`t_tag`、`t_channel`、`t_transaction_group`、`t_rule` 及无 `user_id` 的关联表,新增可查表必须显式加入);对每张表强制注入 `user_id` 隔离条件(主表加 `WHERE`、JOIN 表加 `ON`、关联表用父表归属子查询);强制行数上限 `MAX_ROWS = 1000`。
|
||||||
|
- 结果 VO `AiSqlQueryResultVO` 含 `sql`、`explanation`、`chartType`(`SCALAR`/`TABLE`/`BAR`/`PIE`)、`columns`、`rows`、`rowCount`、`truncated`;列类型映射为 `NUMBER`/`STRING`/`DATE`/`DATETIME`/`BOOLEAN`。
|
||||||
|
- **提问历史**:`AiSqlHistoryService` 在每次成功后写入 `t_ai_sql_history`(按用户),提供 `GET /api/ai/sql/history`(limit 上限 200)与 `DELETE /api/ai/sql/history`。
|
||||||
|
- 用量记录 `AiCallType.SQL`。
|
||||||
|
|
||||||
## 用量记录
|
## 用量记录
|
||||||
|
|
||||||
- 每次调用写入 `t_ai_usage`:调用类型、模型、分析条数、输入/输出/总/缓存 token、是否成功、错误信息、耗时。
|
- 每次调用写入 `t_ai_usage`:调用类型、模型、分析条数、输入/输出/总/缓存 token、是否成功、错误信息、耗时。
|
||||||
|
|||||||
+48
-8
@@ -24,35 +24,75 @@
|
|||||||
- 无需鉴权的接口:`/api/user/login`、`/api/user/register`、`/api/user/registration-status`、`/api/database/status`。
|
- 无需鉴权的接口:`/api/user/login`、`/api/user/register`、`/api/user/registration-status`、`/api/database/status`。
|
||||||
- 日志实时推送等 SSE 接口因 `EventSource` 无法自定义请求头,兼容通过 `?token=` 查询参数鉴权。
|
- 日志实时推送等 SSE 接口因 `EventSource` 无法自定义请求头,兼容通过 `?token=` 查询参数鉴权。
|
||||||
|
|
||||||
|
## 接口数据加密
|
||||||
|
|
||||||
|
后端支持对 `/api/**` 的**请求体与响应体**做端到端加密(RSA + AES-GCM),浏览器登录后自动完成握手,对使用者透明。该能力为**机会式**:未携带加密会话头的请求按明文处理,因此 Knife4j 等工具仍可直接明文调试接口。
|
||||||
|
|
||||||
|
- **握手**:前端先 `GET /api/crypto/public-key` 获取 RSA 公钥(返回 `{enabled, keyId, algorithm, publicKey}`),本地生成 32 字节 AES 密钥并用 **RSA-OAEP(SHA-256)** 加密,再 `POST /api/crypto/handshake`(body `{keyId, key}`)换取 `sessionId`;此后请求统一携带 `X-Crypto-Session: <sessionId>` 头。
|
||||||
|
- **算法**:**AES-256-GCM**(IV 12 字节、认证标签 128 位),GCM 的 AAD 绑定 `sessionId` 以防跨会话重放;AES 会话密钥仅存后端内存,RSA 密钥对在首次启动时自动生成并持久化到运行目录 `config/crypto-keys.json`。
|
||||||
|
- **信封格式**:加解密对象为 JSON 信封 `{"enc":"A256GCM","iv":"<Base64>","data":"<Base64 密文+标签>"}`。
|
||||||
|
- **会话**:有效期 2 小时、上限 2000 个;失效返回业务码 `4201`,前端自动重新握手并重试一次。
|
||||||
|
- **豁免范围**:SSE 流式接口、multipart 文件上传、二进制下载/图片(导出、`/api/ai/image/**`)与握手接口本身不做加解密;`/api/crypto/**` 无需鉴权。
|
||||||
|
- **开关**:由编译期常量 `CryptoConstants.API_ENCRYPTION_ENABLED` 控制;关闭后 `public-key` 返回 `enabled:false`,前端自动回退明文。
|
||||||
|
|
||||||
## 接口分组
|
## 接口分组
|
||||||
|
|
||||||
| 控制器 | 路径前缀 | 说明 |
|
| 控制器 | 路径前缀 | 说明 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `UserController` | `/api/user` | 登录、注册、用户信息、注册开关 |
|
| `UserController` | `/api/user` | 登录、注册、用户信息、注册开关、修改密码 |
|
||||||
| `TransactionController` | `/api/transaction` | 收支记录增删改查、批量操作、导出、交易渠道列表 |
|
| `TransactionController` | `/api/transaction` | 收支记录增删改查、批量创建与操作、备注隐藏、导出、交易渠道选项 |
|
||||||
|
| `TransactionGroupController` | `/api/transaction/group` | 账单关系组的查询、创建、更新、删除与名称历史 |
|
||||||
| `BillImportController` | `/api/transaction/import` | 账单解析与确认导入 |
|
| `BillImportController` | `/api/transaction/import` | 账单解析与确认导入 |
|
||||||
|
| `ChannelController` | `/api/channel` | 用户自定义交易渠道的增删改查 |
|
||||||
| `CategoryController` | `/api/category` | 分类管理 |
|
| `CategoryController` | `/api/category` | 分类管理 |
|
||||||
| `TagController` | `/api/tag` | 标签管理 |
|
| `TagController` | `/api/tag` | 标签管理 |
|
||||||
| `RuleController` | `/api/rule` | 自定义规则(列表、分页查询、增删改、启停) |
|
| `RuleController` | `/api/rule` | 自定义规则(列表、分页查询、增删改、启停、批量删除、命中账单预览与一键应用) |
|
||||||
| `StatisticsController` | `/api/statistics` | 统计汇总、分类、趋势、分组 |
|
| `StatisticsController` | `/api/statistics` | 统计汇总、分类、趋势、分组 |
|
||||||
| `AiController` | `/api/ai` | AI 建议、查询、记账、规则生成、统计搜索、用量 |
|
| `AiController` | `/api/ai` | AI 建议、查询、智能查数、文字/图片记账、渠道/关系/规则生成、统计搜索、用量 |
|
||||||
| `NotificationController` | `/api/notification` | 通知配置、渠道、发送日志 |
|
| `NotificationController` | `/api/notification` | 通知配置、渠道、发送日志 |
|
||||||
| `EmailBillController` | `/api/email-bill` | 邮箱配置、发件人规则、待处理账单 |
|
| `EmailBillController` | `/api/email-bill` | 邮箱配置、发件人规则、待处理账单 |
|
||||||
| `LogController` | `/api/log` | 访问日志、登录日志、运行日志与 SSE |
|
| `LogController` | `/api/log` | 访问日志、登录日志、运行日志与 SSE |
|
||||||
| `DatabaseController` | `/api/database` | 数据库状态、测试与配置 |
|
| `DatabaseController` | `/api/database` | 数据库状态、测试与配置(含 SSE 进度) |
|
||||||
| `AppConfigController` | `/api/app` | 应用配置(通知链接、日志保留) |
|
| `AppConfigController` | `/api/app` | 应用配置(通知链接、日志保留) |
|
||||||
| `UserSettingController` | `/api/setting` | 用户偏好(账单来源统计默认值、不计入统计展示样式) |
|
| `UserSettingController` | `/api/setting` | 用户偏好(账单来源统计默认值、展示样式、隐藏备注显示、分页、导航) |
|
||||||
|
|
||||||
## 关键接口
|
## 关键接口
|
||||||
|
|
||||||
| 方法 | 路径 | 说明 |
|
| 方法 | 路径 | 说明 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| GET | `/api/transaction/channels` | 获取系统支持的交易渠道编码列表 |
|
| GET | `/api/transaction/channels` | 获取交易渠道选项(内置 + 当前用户自定义,含 `code`/`name`/`builtin`) |
|
||||||
|
| POST | `/api/transaction/batch` | 批量创建交易记录(逐条可带 `imageId`/`rawData`),导入时按「类型+金额+时间」跳过重复 |
|
||||||
|
| PUT | `/api/transaction/{id}/remark-hidden` | 隐藏 / 取消隐藏某条记录的备注 |
|
||||||
|
| GET / POST / PUT / DELETE | `/api/channel`、`/api/channel/{id}` | 自定义渠道列表(含使用账单数)、新建、改名、删除 |
|
||||||
|
| GET | `/api/transaction/group/by-transaction/{id}` | 查询某笔账单所属的全部关系组(含成员快照) |
|
||||||
|
| GET | `/api/transaction/group/names` | 关系名称历史(可按 `type` 过滤,去重、倒序) |
|
||||||
|
| POST / PUT / DELETE | `/api/transaction/group`、`/api/transaction/group/{id}` | 创建 / 更新 / 删除账单关系组(删除仅解除关联,不删除账单) |
|
||||||
|
| PUT | `/api/user/password` | 修改当前登录用户的密码(校验原密码,新旧需不同) |
|
||||||
|
| POST | `/api/ai/record/image` | 上传一至多张账单/支付截图(字段 `files`),调用视觉模型识别为多条交易记录 |
|
||||||
|
| POST | `/api/ai/record/image/stream` | 图片记账(SSE):实时推送识别进度,结束返回完整结果 |
|
||||||
|
| GET | `/api/ai/image/{id}` | 查看本人上传的图片记账图片,`?thumbnail=true` 返回缩略图 |
|
||||||
|
| POST | `/api/ai/channel` | 由自然语言描述生成交易渠道名称 |
|
||||||
|
| POST | `/api/ai/relation` | 由关联账单生成关系名称与备注 |
|
||||||
|
| POST | `/api/ai/sql` | 智能查数:自然语言生成只读 SQL 并执行 |
|
||||||
|
| GET / DELETE | `/api/ai/sql/history` | 查询 / 清空智能查数提问历史(`limit` 上限 200) |
|
||||||
| GET | `/api/rule/page` | 分页查询规则(`keyword` / `enabled` / `type`) |
|
| GET | `/api/rule/page` | 分页查询规则(`keyword` / `enabled` / `type`) |
|
||||||
|
| DELETE | `/api/rule/batch` | 批量删除规则(body `{ ids }`,返回实际删除数) |
|
||||||
|
| DELETE | `/api/tag/batch` | 批量删除标签(body `{ ids }`,先清理交易-标签与规则-标签关联再删除,返回实际删除数) |
|
||||||
|
| DELETE | `/api/category/batch` | 批量删除分类(body `{ ids }`,返回实际删除数) |
|
||||||
|
| POST | `/api/rule/apply-preview` | 预览所选规则命中的历史账单及将应用的目标分类/标签(body `{ ruleIds }`,返回 `RuleApplyPreviewVO`,明细上限 500 条,纯本地匹配) |
|
||||||
|
| POST | `/api/rule/apply` | 一键将所选规则的分类与标签应用到其命中的历史账单(分类覆盖、标签追加,返回 `RuleApplyResultVO`;纯本地匹配,不外发数据) |
|
||||||
| POST | `/api/ai/rule/suggest` | 依据所选已打标签记录生成规则建议(含 `duplicate` 标记) |
|
| POST | `/api/ai/rule/suggest` | 依据所选已打标签记录生成规则建议(含 `duplicate` 标记) |
|
||||||
| POST | `/api/ai/rule/apply` | 批量创建规则(自动去重、按需新建分类/标签) |
|
| POST | `/api/ai/rule/apply` | 批量创建规则(自动去重、按需新建分类/标签) |
|
||||||
|
| POST | `/api/ai/rule/apply-transactions` | 按传入规则对指定交易做本地匹配并应用(命中则覆盖分类、追加标签,必要时新建;纯本地匹配,不外发数据) |
|
||||||
|
| POST | `/api/ai/rule/check-duplicates` | 校验编辑后的规则是否与已有规则或批内其它规则重复(body `{ rules }`,返回逐条布尔标记;纯本地,不调用大模型) |
|
||||||
| POST | `/api/ai/statistics` | 自然语言解析为统计面板参数 |
|
| POST | `/api/ai/statistics` | 自然语言解析为统计面板参数 |
|
||||||
| GET / PUT | `/api/app/bill-source-include-in-stats` | 查询 / 更新按账单来源的「计入统计」默认值 |
|
| GET / PUT | `/api/setting/excluded-display-style` | 查询 / 更新「不计入统计」记录展示样式 |
|
||||||
|
| GET / PUT | `/api/setting/show-hidden-data` | 查询 / 更新是否显示已隐藏备注的记录 |
|
||||||
|
| GET / PUT | `/api/setting/chart-click-navigation` | 查询 / 更新统计面板图表点击是否跳转明细 |
|
||||||
|
| GET / PUT | `/api/setting/pagination-prefs` | 查询 / 更新表格分页候选列表与默认每页条数 |
|
||||||
|
| GET / PUT | `/api/setting/nav-menu` | 查询 / 更新左侧导航顺序与显隐 |
|
||||||
|
| GET / PUT | `/api/setting/bill-source-include-in-stats` | 查询 / 更新按账单来源的「计入统计」默认值 |
|
||||||
|
| POST | `/api/database/config/stream` | 应用数据库配置并通过 SSE 推送进度(另有 `/api/database/test/stream`) |
|
||||||
|
|
||||||
## 统一响应结构
|
## 统一响应结构
|
||||||
|
|
||||||
|
|||||||
+10
-5
@@ -12,8 +12,8 @@
|
|||||||
| 认证 | JWT(HMAC) |
|
| 认证 | JWT(HMAC) |
|
||||||
| 对象映射 | MapStruct 1.6.3 |
|
| 对象映射 | MapStruct 1.6.3 |
|
||||||
| 账单解析 | EasyExcel(xlsx)/ Apache POI(xls)+ commons-csv + PDFBox + Zip4j |
|
| 账单解析 | EasyExcel(xlsx)/ Apache POI(xls)+ commons-csv + PDFBox + Zip4j |
|
||||||
| AI | DeepSeek(OpenAI Java SDK) |
|
| AI | DeepSeek(OpenAI 兼容接口,Spring RestClient) |
|
||||||
| 通知 | Jakarta Mail(SMTP / IMAP)+ Server 酱 |
|
| 通知 | Jakarta Mail(SMTP / IMAP)+ Server 酱 + 飞书自定义机器人 |
|
||||||
| API 文档 | Knife4j(OpenAPI 3) |
|
| API 文档 | Knife4j(OpenAPI 3) |
|
||||||
|
|
||||||
## 后端分层
|
## 后端分层
|
||||||
@@ -50,10 +50,12 @@ entity 数据库表映射实体
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
浏览器
|
浏览器
|
||||||
│ /api/**
|
│ /api/**(可携带加密会话头 X-Crypto-Session)
|
||||||
▼
|
▼
|
||||||
AccessLogInterceptor 记录访问日志(方法/路径/耗时/状态/IP/用户)
|
AccessLogInterceptor 记录访问日志(方法/路径/耗时/状态/IP/用户)
|
||||||
│
|
│
|
||||||
|
CryptoSessionInterceptor 解析加密会话(无会话头则按明文处理)
|
||||||
|
│
|
||||||
JwtInterceptor 校验 Bearer Token(兼容 ?token= 供 SSE),写入 userId
|
JwtInterceptor 校验 Bearer Token(兼容 ?token= 供 SSE),写入 userId
|
||||||
│
|
│
|
||||||
Controller UserContext.getUserId()
|
Controller UserContext.getUserId()
|
||||||
@@ -63,6 +65,8 @@ Service → Mapper → 数据库(动态数据源)
|
|||||||
GlobalExceptionHandler 统一异常 → Result{code,message,data}
|
GlobalExceptionHandler 统一异常 → Result{code,message,data}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- **请求/响应体加解密**:`ApiCryptoRequestBodyAdvice` 在参数绑定前解密请求体,`ApiCryptoResponseBodyAdvice` 在写出前加密 `Result` 响应体,二者均依赖 `CryptoSessionInterceptor` 写入的会话。详见 [API 文档 · 接口数据加密](/dev/api#接口数据加密)。
|
||||||
|
|
||||||
- 登录、注册、注册状态、数据库状态等接口无需鉴权。
|
- 登录、注册、注册状态、数据库状态等接口无需鉴权。
|
||||||
- 数据库未配置时,`UnconfiguredDataSource` 抛出的异常会转为业务码 `5031`,前端据此跳转初始化引导页。
|
- 数据库未配置时,`UnconfiguredDataSource` 抛出的异常会转为业务码 `5031`,前端据此跳转初始化引导页。
|
||||||
|
|
||||||
@@ -91,9 +95,10 @@ src/
|
|||||||
1. **运行时动态数据源**:启动无需数据库,配置后热切换,见[动态数据源](/dev/database)。
|
1. **运行时动态数据源**:启动无需数据库,配置后热切换,见[动态数据源](/dev/database)。
|
||||||
2. **账单两阶段导入**:解析预览 → 确认导入,按交易号去重,见[账单解析](/dev/bill-parsing)。
|
2. **账单两阶段导入**:解析预览 → 确认导入,按交易号去重,见[账单解析](/dev/bill-parsing)。
|
||||||
3. **规则引擎**:分类取最高优先级、标签取并集,见[规则引擎](/dev/rule-engine)。
|
3. **规则引擎**:分类取最高优先级、标签取并集,见[规则引擎](/dev/rule-engine)。
|
||||||
4. **体系化 AI 调用**:9 类调用类型并记录 token 用量,见 [AI 集成](/dev/ai)。
|
4. **体系化 AI 调用**:13 类调用类型并记录 token 用量,见 [AI 集成](/dev/ai)。
|
||||||
5. **SSE 实时能力**:运行日志、邮箱检查进度实时推送。
|
5. **SSE 实时能力**:运行日志、邮箱检查进度、图片记账识别进度与数据库配置执行进度实时推送。
|
||||||
6. **用户偏好缓存**:`t_user_preference` 的读取走 Spring Cache 进程内缓存(`ConcurrentMapCacheManager`),写入时按用户失效,切换数据源时整体清空。
|
6. **用户偏好缓存**:`t_user_preference` 的读取走 Spring Cache 进程内缓存(`ConcurrentMapCacheManager`),写入时按用户失效,切换数据源时整体清空。
|
||||||
|
7. **接口数据加密**:`/api/**` 请求/响应体支持 RSA + AES-GCM 端到端加密,握手自动完成、对未加密客户端透明,见 [API 文档](/dev/api#接口数据加密)。
|
||||||
|
|
||||||
## 相关文档
|
## 相关文档
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -5,8 +5,8 @@
|
|||||||
## 样例与实现位置
|
## 样例与实现位置
|
||||||
|
|
||||||
- 编码:GBK(解析器兼容 UTF-8 / UTF-8 BOM)
|
- 编码:GBK(解析器兼容 UTF-8 / UTF-8 BOM)
|
||||||
- 解析器:`src/main/java/cn/rosercode/finance/importer/AlipayBillParser.java`
|
- 解析器:`src/main/java/cn/rosercode/ledgerly/importer/AlipayBillParser.java`
|
||||||
- 落库服务:`src/main/java/cn/rosercode/finance/service/impl/BillImportServiceImpl.java`
|
- 落库服务:`src/main/java/cn/rosercode/ledgerly/service/impl/BillImportServiceImpl.java`
|
||||||
|
|
||||||
## 文件结构
|
## 文件结构
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@
|
|||||||
- **时间兼容**:微信账单兼容 Excel 日期序列号与文本时间;支付宝在付款时间缺失时回退交易创建时间;淘宝订单同样兼容文本时间与 Excel 日期序列号。
|
- **时间兼容**:微信账单兼容 Excel 日期序列号与文本时间;支付宝在付款时间缺失时回退交易创建时间;淘宝订单同样兼容文本时间与 Excel 日期序列号。
|
||||||
- **订单聚合**:淘宝订单数据中多商品订单的订单级列(订单号、提交时间、状态、店铺、实付金额、运费)以合并单元格呈现,仅首个商品行有值;解析器按订单号分组聚合,每个订单输出一条记录,金额优先取订单级实付金额,为空时回退「Σ(商品金额×数量)+运费」。
|
- **订单聚合**:淘宝订单数据中多商品订单的订单级列(订单号、提交时间、状态、店铺、实付金额、运费)以合并单元格呈现,仅首个商品行有值;解析器按订单号分组聚合,每个订单输出一条记录,金额优先取订单级实付金额,为空时回退「Σ(商品金额×数量)+运费」。
|
||||||
- **不计收支处理**:京东流水的「不计收支」行(白条还款、小金库转入、全额退款等)标记为可导入但**不计入统计且默认不勾选**,类型按「交易说明含『退款』→ INCOME,否则 EXPENSE」推断。
|
- **不计收支处理**:京东流水的「不计收支」行(白条还款、小金库转入、全额退款等)标记为可导入但**不计入统计且默认不勾选**,类型按「交易说明含『退款』→ INCOME,否则 EXPENSE」推断。
|
||||||
- **计入统计默认值**:解析完成后按**账单来源**应用「计入统计」默认值(配置于系统设置,见 [动态数据源](/dev/database) 同级的应用配置)。采用「解析器判定 && 来源默认」语义——来源默认不计入时整批置为不计入;来源默认计入时保留解析器对「不计收支/中性」行的排除。确认导入时若请求未显式提供该字段,同样以来源默认兜底。
|
- **计入统计默认值**:解析完成后按**账单来源**应用「计入统计」默认值(配置于设置页,见 [动态数据源](/dev/database) 同级的应用配置)。采用「解析器判定 && 来源默认」语义——来源默认不计入时整批置为不计入;来源默认计入时保留解析器对「不计收支/中性」行的排除。确认导入时若请求未显式提供该字段,同样以来源默认兜底。
|
||||||
|
|
||||||
## 压缩包解密
|
## 压缩包解密
|
||||||
|
|
||||||
|
|||||||
+105
-42
@@ -1,9 +1,12 @@
|
|||||||
# 构建与部署
|
# 构建与部署
|
||||||
|
|
||||||
|
本章介绍「汇账」的构建方式、分发形态与生产部署流程。
|
||||||
|
|
||||||
## 前置条件
|
## 前置条件
|
||||||
|
|
||||||
- **JDK 21**(Temurin 21 LTS)
|
- **JDK 21**(Temurin 21 LTS,含 `jpackage`)
|
||||||
- **Node.js**(构建前端时需要)
|
- **Node.js**(构建前端时需要)
|
||||||
|
- 依赖仓库:本地 Nexus(Maven)与 verdaccio(npm);构建时经 `-I nexus-init.gradle` 注入 Maven 私服地址
|
||||||
|
|
||||||
## 开发模式
|
## 开发模式
|
||||||
|
|
||||||
@@ -11,86 +14,146 @@
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 后端(默认 8080)
|
# 后端(默认 8080)
|
||||||
cd finance-app
|
cd ledgerly
|
||||||
set JAVA_HOME=D:\venv\jdk-21
|
./gradlew bootRun # Windows: gradlew.bat bootRun
|
||||||
gradlew.bat bootRun
|
|
||||||
|
|
||||||
# 前端(默认 5173,接口代理到 8080)
|
# 前端(默认 5173,接口代理到 8080)
|
||||||
cd finance-app/frontend
|
cd ledgerly/frontend
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
- 后端地址:http://localhost:8080
|
- 后端地址:`http://localhost:8080`,接口文档 `http://localhost:8080/doc.html`
|
||||||
- 后端 API 文档:http://localhost:8080/doc.html
|
- 前端地址:`http://localhost:5173`,`/api` 经 Vite 代理到后端 8080
|
||||||
- 前端地址:http://localhost:5173,接口经 Vite 代理到后端 8080
|
|
||||||
|
|
||||||
## 打包可执行 jar(前后端一体化)
|
## 构建可执行 jar(前后端一体化)
|
||||||
|
|
||||||
`bootJar` 会先执行前端 `npm run build`,再把 `frontend/dist` 复制到 jar 内的 `BOOT-INF/classes/static/`,单文件同时提供页面与接口。
|
`bootJar` 会先执行前端 `npm run build`,再把 `frontend/dist` 复制到 jar 内的 `BOOT-INF/classes/static/`,单文件同时提供页面与接口。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gradle bootJar
|
# 普通 jar(无 GUI,不含 JavaFX)
|
||||||
# 或使用宿主机脚本
|
./gradlew -I nexus-init.gradle bootJar
|
||||||
|
|
||||||
|
# 带 GUI 启动器的 jar
|
||||||
|
./gradlew -I nexus-init.gradle bootGuiJar
|
||||||
|
|
||||||
|
# 或使用宿主机脚本(一次产出两者)
|
||||||
./package.sh
|
./package.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
输出:`build/libs/finance-app-0.0.1-SNAPSHOT.jar`
|
产物文件名携带目标 JDK 版本:
|
||||||
|
|
||||||
|
| 任务 | 产物 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `bootJar` | `build/libs/ledgerly-0.1-jdk21.jar` | 普通服务 jar |
|
||||||
|
| `bootGuiJar` | `build/libs/ledgerly-0.1-jdk21-gui.jar` | 入口为桌面启动器 |
|
||||||
|
|
||||||
启动(默认 8080,可另指定端口):
|
启动(默认 8080,可另指定端口):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
java -jar build/libs/finance-app-0.0.1-SNAPSHOT.jar --server.port=18081
|
java -jar build/libs/ledgerly-0.1-jdk21.jar --server.port=18081
|
||||||
```
|
```
|
||||||
|
|
||||||
访问对应端口即可;刷新 `/dashboard` 等子路由由后端 `SpaErrorController` 回退到 `index.html`。
|
构建任务链(`build.gradle`):`npmInstall` → `frontendLint` → `frontendBuild` → `bootJar`;`check` 任务另挂 Spotless、Checkstyle、SpotBugs。代码检查不通过会中断构建与打包,详见[开发环境与规范](/dev/development)。
|
||||||
|
|
||||||
::: tip 提示
|
::: tip 提示
|
||||||
jar 内置的前端为生产构建,访问时与后端同源,无需额外的反向代理。
|
jar 内置的前端为生产构建,访问时与后端同源,无需额外的反向代理。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
构建任务链(`build.gradle`):
|
为减小可执行 jar 体积,普通 jar 与 GUI jar 均做了裁剪:AI 调用改用 Spring `RestClient` 直连 OpenAI 兼容接口(不再打包 OpenAI Java SDK);接口文档仅保留 `/v3/api-docs`(JSON),Knife4j 的 `doc.html` UI 依赖为 `developmentOnly`,不进生产 jar;普通 jar 通过 `exclude` 排除桌面启动器包,保持无 JavaFX 依赖。
|
||||||
|
|
||||||
- `npmInstall`:安装前端依赖(`node_modules` 存在且 `package.json` 未变化时跳过)
|
运行时首次配置或切换数据库时,「测试连接」与「保存并应用」会通过 SSE **实时推送执行进度**(连接测试、建表、结构迁移、数据源切换等)。
|
||||||
- `frontendBuild`:执行 `npm run build`
|
|
||||||
- `bootJar`:依赖 `frontendBuild`,并将前端产物打入静态资源目录
|
|
||||||
|
|
||||||
## Docker 部署
|
## 构建免安装应用(jpackage app-image)
|
||||||
|
|
||||||
`run/docker-compose.yml` 定义前后端两个服务,纯复用本地镜像、不构建新镜像:
|
`jpackageAppImage` 聚合任务一次生成两个 app-image,自带精简运行时,双击即用;**仅能为当前操作系统生成**(Windows 上生成 Windows 产物,Linux 上生成 Linux 产物)。
|
||||||
|
|
||||||
| 服务 | 镜像 | 端口 | 说明 |
|
```bash
|
||||||
|
./gradlew -I nexus-init.gradle jpackageAppImage
|
||||||
|
# 也可单独执行 jpackageServerAppImage / jpackageGuiAppImage
|
||||||
|
```
|
||||||
|
|
||||||
|
| 任务 | 应用名 | 源 jar | 入口 |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| backend | `eclipse-temurin:21-jre` | 18080→8080 | 挂载 `run/backend/*.jar`,持久化 `config/`、`data/`、`logs/` |
|
| `jpackageServerAppImage` | `LedgerlyServer` | `bootJar` | `bin/LedgerlyServer`(Windows 为 `LedgerlyServer.exe`,附带控制台) |
|
||||||
| front | `nginx:alpine` | 15180→80 | 挂载 `run/front/dist` 与 Nginx 配置 |
|
| `jpackageGuiAppImage` | `Ledgerly` | `bootGuiJar` | `bin/Ledgerly`(Windows 为 `Ledgerly.exe`) |
|
||||||
|
|
||||||
环境变量 `DB_URL` / `DB_USERNAME` / `DB_PASSWORD` 仅作为首次启动的默认配置来源,也可在界面中完成数据库配置。
|
产物位于 `build/jpackage/LedgerlyServer/` 与 `build/jpackage/Ledgerly/`。
|
||||||
|
|
||||||
|
图标不随源码分发:打包时 `downloadAppIcon` 从 `src/main/resources/launcher/icon.url` 下载源图,再由 `tools/IconGen.java` 生成平台图标(`ico` / `icns` / `png`);可用 `-PsourceIcon=<png>` 指定本地源图。
|
||||||
|
|
||||||
## 桌面 GUI 启动器
|
## 桌面 GUI 启动器
|
||||||
|
|
||||||
`launcher` 包提供系统托盘 + 控制窗口的桌面启动器(启动/停止服务、打开浏览器、查看日志),通过 `bootGuiJar` 打包:
|
`cn.rosercode.ledgerly.launcher` 提供系统托盘 + 控制窗口的桌面启动器(进程内启停服务、设置端口、打开浏览器、实时查看日志、单实例运行),仅打入带 GUI 的 jar,普通 jar 不含 JavaFX 依赖。
|
||||||
|
|
||||||
```bash
|
|
||||||
gradle bootGuiJar
|
|
||||||
```
|
|
||||||
|
|
||||||
- 入口类:`cn.rosercode.finance.launcher.LauncherMain`
|
|
||||||
- 依赖 JavaFX 与 dorkbox SystemTray,仅加入带 GUI 的 jar(`guiRuntime` 配置),普通 jar 不含这些依赖
|
|
||||||
- 平台默认 Windows(可通过 `-PjavafxPlatform=linux` 本地验证)
|
|
||||||
- JavaFX 与 JNA 原生库需 `requiresUnpack` 从嵌套 jar 解压
|
|
||||||
|
|
||||||
> 注意:该启动器为规划中的能力,具体实现以当前源码为准。
|
|
||||||
|
|
||||||
## 分发形态
|
## 分发形态
|
||||||
|
|
||||||
| 形态 | 说明 |
|
| 形态 | 构建方式 | 说明 |
|
||||||
|---|---|
|
|---|---|---|
|
||||||
| jar | `bootJar`,需 JDK 21 运行 |
|
| 普通 jar | `bootJar` | 需 JDK 21 运行,跨平台 |
|
||||||
| jar(带 GUI) | `bootGuiJar`,内含桌面启动器 |
|
| 带 GUI 启动器 jar | `bootGuiJar` | 内含桌面启动器,跨平台(打包含 JavaFX 全平台原生库) |
|
||||||
| 可执行文件(控制台 / GUI) | 规划中,计划采用 `jpackage`(JDK 自带,各平台分别构建) |
|
| Windows 免安装包 | `jpackageAppImage` | 在 Windows 上构建,zip 分发 |
|
||||||
| Docker 镜像 | 通过 `run/docker-compose.yml` 编排 |
|
| Linux 免安装包 | `jpackageAppImage` | 在 Linux 上构建,tar.gz 分发 |
|
||||||
|
|
||||||
|
## 发布产物与下载地址
|
||||||
|
|
||||||
|
发布产物上传至阿里云 OSS(桶 `adccrac`,对象键前缀 `ledgerly/release/<版本>/`),通过 10 年有效的长期签名 URL 对外提供下载;每个文件同时提供 `.sha256` 校验文件与汇总清单 `SHA256SUMS`。
|
||||||
|
|
||||||
|
当前工程版本为 `0.1`;最新下载地址见使用文档的[下载与安装](/guide/download)(该页链接指向已上传的发布产物)。
|
||||||
|
|
||||||
|
上传方式:
|
||||||
|
|
||||||
|
- Linux 产物(jar 与 Linux app-image 包):工作区根 `oss-release-upload.py`,需在带 `oss2` 的虚拟环境执行(如 `20260807_131528/oss-venv`),上传后输出长期签名 URL。
|
||||||
|
- Windows 产物(Windows app-image 包):在 Windows 主机上用 Node 脚本(`ali-oss` + STS)直接上传,不回传本地。
|
||||||
|
|
||||||
|
## 生产部署(云服务器)
|
||||||
|
|
||||||
|
服务部署在云服务器 `101.35.20.223` 的 `/opt/ledgerly` 目录,由 Docker Compose 编排:
|
||||||
|
|
||||||
|
| 容器 | 镜像 | 端口 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `ledgerly` | `eclipse-temurin:21-jre` | `127.0.0.1:15180 → 8080` | 挂载 `app/ledgerly-0.1.jar`、`config/`、`data/`,JVM `-Xmx512m` |
|
||||||
|
| `ledgerly-docs` | `nginx:1.27` | `127.0.0.1:15181 → 80` | 挂载 `docs-dist/` 与 `docs/nginx.conf` |
|
||||||
|
|
||||||
|
云上 `nginx` 容器反代这两个回环端口并终止 HTTPS,证书由 `certbot` 容器负责续期:
|
||||||
|
|
||||||
|
- 服务:[https://finance.rosercode.cn](https://finance.rosercode.cn)
|
||||||
|
- 文档:[https://docs.finance.rosercode.cn](https://docs.finance.rosercode.cn)
|
||||||
|
|
||||||
|
### 安全配置
|
||||||
|
|
||||||
|
- **JWT 签名密钥**:默认使用内置密钥签发 Token(有效期 24 小时);生产环境可通过环境变量 `JWT_SECRET` 覆盖,未配置时回退内置默认值,避免个人自托管场景重启后登录态失效。
|
||||||
|
- **CORS 放行来源**:放开全部来源(`allowedOriginPatterns("*")` 并允许携带凭证)。因 `allowCredentials(true)` 不允许 `allowedOrigins("*")`,需使用 origin pattern 通配;接口认证基于 JWT(`Authorization` 头存于 `localStorage`,跨站脚本无法自动携带),风险可控。
|
||||||
|
- **反向代理转发头**:`application.yml` 配置 `server.forward-headers-strategy: framework`,信任 `X-Forwarded-Proto`/`Host`;经 nginx/frp 反代时使 HTTPS 同源请求被正确识别为同源,避免被误判为跨域(否则带 `Origin` 的请求会返回 `403 Invalid CORS request`)。
|
||||||
|
- **运行环境编码**:容器或直接运行 jar 的 JVM 需为 UTF-8 locale(`LANG`/`LC_ALL=C.UTF-8`);否则 `sun.jnu.encoding` 退化为 ASCII,含中文的文件名(如邮箱账单附件名)落盘会抛 `InvalidPathException`。
|
||||||
|
|
||||||
|
### 发布流程
|
||||||
|
|
||||||
|
服务(构建 → 复制 → 重启):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1) 本地构建 jar(见上)
|
||||||
|
# 2) 复制到云端部署目录,目标文件名须与 compose 挂载一致
|
||||||
|
scp -i ~/.ssh/id_ed25519_www.rosercode.cn \
|
||||||
|
build/libs/ledgerly-0.1-jdk21.jar \
|
||||||
|
root@101.35.20.223:/opt/ledgerly/app/ledgerly-0.1.jar
|
||||||
|
# 3) 重启容器
|
||||||
|
ssh -i ~/.ssh/id_ed25519_www.rosercode.cn root@101.35.20.223 \
|
||||||
|
'cd /opt/ledgerly && docker compose restart'
|
||||||
|
```
|
||||||
|
|
||||||
|
文档站(构建 → 覆盖 `docs-dist`,目录挂载即时生效):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd doc-site && npm run docs:build
|
||||||
|
scp -i ~/.ssh/id_ed25519_www.rosercode.cn -r .vitepress/dist/* \
|
||||||
|
root@101.35.20.223:/opt/ledgerly/docs-dist/
|
||||||
|
```
|
||||||
|
|
||||||
|
> 云服务器不能直连 DockerHub,新镜像需本地 `docker save | gzip` → `scp` → 云端 `docker load` 导入。
|
||||||
|
|
||||||
## 相关文档
|
## 相关文档
|
||||||
|
|
||||||
- [项目结构](/dev/project-structure)
|
- [项目结构](/dev/project-structure)
|
||||||
- [开发环境与规范](/dev/development)
|
- [开发环境与规范](/dev/development)
|
||||||
|
- [下载与安装(使用文档)](/guide/download)
|
||||||
- [快速开始(使用文档)](/guide/getting-started)
|
- [快速开始(使用文档)](/guide/getting-started)
|
||||||
|
|||||||
+47
-5
@@ -1,6 +1,6 @@
|
|||||||
# 数据模型
|
# 数据模型
|
||||||
|
|
||||||
系统共 **18 张表**,覆盖用户、记账、标签、规则、AI、通知、邮箱账单与日志审计。所有表主键为自增 `id`,表间关系均为**逻辑外键**(通过 `user_id`、`category_id` 等字段在应用层维护,未声明物理外键)。
|
系统共 **24 张表**,覆盖用户、记账、账单关系、渠道、标签、规则、AI、通知、邮箱账单与日志审计。所有表主键为自增 `id`,表间关系均为**逻辑外键**(通过 `user_id`、`category_id` 等字段在应用层维护,未声明物理外键)。
|
||||||
|
|
||||||
## 表清单
|
## 表清单
|
||||||
|
|
||||||
@@ -10,12 +10,18 @@
|
|||||||
| 用户 | `t_user_preference` | 每用户多条的用户偏好(键值) |
|
| 用户 | `t_user_preference` | 每用户多条的用户偏好(键值) |
|
||||||
| 记账 | `t_category` | 收支分类(收入/支出,含图标与排序) |
|
| 记账 | `t_category` | 收支分类(收入/支出,含图标与排序) |
|
||||||
| 记账 | `t_transaction` | 收支记录(核心表) |
|
| 记账 | `t_transaction` | 收支记录(核心表) |
|
||||||
|
| 记账 | `t_channel` | 用户自定义交易渠道(内置渠道由枚举维护) |
|
||||||
| 标签 | `t_tag` | 标签 |
|
| 标签 | `t_tag` | 标签 |
|
||||||
| 标签 | `t_transaction_tag` | 交易-标签关联(多对多) |
|
| 标签 | `t_transaction_tag` | 交易-标签关联(多对多) |
|
||||||
|
| 账单关系 | `t_transaction_group` | 账单关系组(父子/同级) |
|
||||||
|
| 账单关系 | `t_transaction_group_item` | 关系组成员(角色与顺序) |
|
||||||
| 规则 | `t_rule` | 自定义归类规则 |
|
| 规则 | `t_rule` | 自定义归类规则 |
|
||||||
| 规则 | `t_rule_tag` | 规则-标签关联(多对多) |
|
| 规则 | `t_rule_tag` | 规则-标签关联(多对多) |
|
||||||
| AI | `t_ai_config` | 每用户一条的 AI 配置 |
|
| AI | `t_ai_config` | 每用户一条的 AI 配置(含视觉模型) |
|
||||||
| AI | `t_ai_usage` | 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_config` | 每用户一条的提醒配置 |
|
||||||
| 通知 | `t_notification_channel` | 通知渠道 |
|
| 通知 | `t_notification_channel` | 通知渠道 |
|
||||||
| 通知 | `t_notification_log` | 通知发送日志 |
|
| 通知 | `t_notification_log` | 通知发送日志 |
|
||||||
@@ -34,7 +40,10 @@
|
|||||||
| t_user | 1 : N | t_category | user_id |
|
| t_user | 1 : N | t_category | user_id |
|
||||||
| t_user | 1 : N | t_transaction | user_id |
|
| t_user | 1 : N | t_transaction | user_id |
|
||||||
| t_user | 1 : N | t_tag | 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_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_notification_channel | user_id |
|
||||||
| t_user | 1 : N | t_email_bill_config | 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_sender_rule | user_id |
|
||||||
@@ -57,6 +66,12 @@
|
|||||||
| t_transaction | N : M | t_tag | 中间表 t_transaction_tag(联合唯一) |
|
| t_transaction | N : M | t_tag | 中间表 t_transaction_tag(联合唯一) |
|
||||||
| t_category | 1 : N | t_rule | rule.category_id(命中目标分类) |
|
| t_category | 1 : N | t_rule | rule.category_id(命中目标分类) |
|
||||||
| t_rule | N : M | t_tag | 中间表 t_rule_tag(联合唯一) |
|
| 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`(用户内唯一),因此渠道改名不影响历史记录。
|
||||||
|
|
||||||
### 通知与邮箱账单
|
### 通知与邮箱账单
|
||||||
|
|
||||||
@@ -84,7 +99,7 @@
|
|||||||
| external_no | 外部账单交易号,导入去重 |
|
| external_no | 外部账单交易号,导入去重 |
|
||||||
| transaction_time | 交易发生时间 |
|
| transaction_time | 交易发生时间 |
|
||||||
| include_in_stats | 是否计入统计(默认 1) |
|
| include_in_stats | 是否计入统计(默认 1) |
|
||||||
| raw_data | 账单导入原始 JSON(手动记录为空) |
|
| raw_data | 原始字段 JSON(账单导入的原始账单行,或图片记账由模型提取的原始字段;手动记录为空) |
|
||||||
|
|
||||||
关键索引:唯一索引 `uk_user_external (user_id, external_no)` 实现导入去重;`idx_user_time (user_id, transaction_time)` 支撑按时间查询与统计。
|
关键索引:唯一索引 `uk_user_external (user_id, external_no)` 实现导入去重;`idx_user_time (user_id, transaction_time)` 支撑按时间查询与统计。
|
||||||
|
|
||||||
@@ -92,14 +107,41 @@
|
|||||||
|
|
||||||
- 分类:`name`、`type`、`icon`(Emoji)、`sort_order`。
|
- 分类:`name`、`type`、`icon`(Emoji)、`sort_order`。
|
||||||
- 标签:`name`、`color`、`sort_order`,唯一索引 `uk_user_name (user_id, name)` 保证用户内标签名唯一。
|
- 标签:`name`、`color`、`sort_order`,唯一索引 `uk_user_name (user_id, name)` 保证用户内标签名唯一。
|
||||||
|
- 删除标签时会一并清理 `t_transaction_tag` 与 `t_rule_tag` 中的关联记录(单条与批量删除行为一致);删除分类仅移除分类本身,账单与规则上的 `category_id` 作为逻辑外键保留(账单会显示为「未分类」)。
|
||||||
|
|
||||||
### t_rule
|
### t_rule
|
||||||
|
|
||||||
`name`、`enabled`、`sort_order`(优先级)、`type`、`keyword`、`min_amount`、`max_amount`、`category_id`。
|
`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
|
### t_user_preference
|
||||||
|
|
||||||
按用户存储的键值偏好:`pref_key` 为偏好键(如 `billSourceIncludeInStats`、`excludedDisplayStyle`),`pref_value` 为标量字符串或 JSON。唯一索引 `uk_user_pref (user_id, pref_key)` 保证每个用户每个偏好仅一条。未配置时由应用回退系统默认值。
|
按用户存储的键值偏好:`pref_key` 为偏好键,`pref_value` 为标量字符串或 JSON。已使用的偏好键包括:`billSourceIncludeInStats`(各来源计入统计默认值)、`excludedDisplayStyle`(不计入记录展示样式)、`showHiddenData`(是否显示已隐藏备注的记录)、`chartClickNavigation`(统计面板图表点击是否跳转明细)、`paginationPrefs`(表格分页候选与默认值)、`navMenu`(左侧导航顺序与显隐)。唯一索引 `uk_user_pref (user_id, pref_key)` 保证每个用户每个偏好仅一条。未配置时由应用回退系统默认值。
|
||||||
|
|
||||||
### t_email_bill_config
|
### t_email_bill_config
|
||||||
|
|
||||||
@@ -129,5 +171,5 @@ IMAP 连接信息(`host`/`port`/`use_ssl`/`username`/`password`/`folder`)、
|
|||||||
## 初始化与迁移
|
## 初始化与迁移
|
||||||
|
|
||||||
- 应用启动时由 `SchemaInitializer` 执行对应建表脚本(幂等)。
|
- 应用启动时由 `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` 初始化默认分类、标签与邮箱发件人规则。
|
- 新用户注册时由 `DefaultDataInitializer` 初始化默认分类、标签与邮箱发件人规则。
|
||||||
|
|||||||
+2
-2
@@ -20,7 +20,7 @@
|
|||||||
| `SchemaMigrator` | 建表后的幂等结构迁移(如列重命名),启动与切换数据源时执行 |
|
| `SchemaMigrator` | 建表后的幂等结构迁移(如列重命名),启动与切换数据源时执行 |
|
||||||
| `DatabaseBootstrap` | 应用启动后加载配置并初始化 |
|
| `DatabaseBootstrap` | 应用启动后加载配置并初始化 |
|
||||||
|
|
||||||
`FinanceApplication` 刻意排除了 `DataSourceAutoConfiguration`,以便「零配置也能启动」。
|
`LedgerlyApplication` 刻意排除了 `DataSourceAutoConfiguration`,以便「零配置也能启动」。
|
||||||
|
|
||||||
## 配置来源与顺序
|
## 配置来源与顺序
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ MySQL 的 JDBC URL 会自动附带 UTF-8、关闭 SSL、`Asia/Shanghai` 时区
|
|||||||
|
|
||||||
## 切换行为
|
## 切换行为
|
||||||
|
|
||||||
登录后可在「系统设置 → 数据库配置」随时切换数据库。切换后原数据库的数据保留,**不做自动迁移**。
|
登录后可在「设置 → 数据库配置」随时切换数据库;测试与保存时通过 SSE 实时推送执行进度。切换后原数据库的数据保留,**不做自动迁移**。
|
||||||
|
|
||||||
切换成功后会清空进程内的用户偏好缓存(见[整体架构](/dev/architecture)),避免读到旧库的数据。
|
切换成功后会清空进程内的用户偏好缓存(见[整体架构](/dev/architecture)),避免读到旧库的数据。
|
||||||
|
|
||||||
|
|||||||
+42
-6
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
| 环境 | 前端 | 后端 | 数据库 |
|
| 环境 | 前端 | 后端 | 数据库 |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 测试环境 | 5174 | 8081 | 独立 SQLite 空库(`ai-home/data/ai-finance.db`) |
|
| 测试环境 | 5174 | 8081 | 独立 SQLite 空库(`ai-home/data/ai-ledgerly.db`) |
|
||||||
| 真实环境 | 5173 | 8080 | MySQL(真实数据) |
|
| 真实环境 | 5173 | 8080 | MySQL(真实数据) |
|
||||||
|
|
||||||
- 两个环境共用同一份源码:前端改动经 Vite HMR 两边同步;后端改动执行 `./reload.sh` 后两边 devtools 同时热重启。
|
- 两个环境共用同一份源码:前端改动经 Vite HMR 两边同步;后端改动执行 `./reload.sh` 后两边 devtools 同时热重启。
|
||||||
@@ -20,6 +20,8 @@
|
|||||||
- 前端改动**不使用 `npm run build` 验证**,直接依赖 Vite 开发服务器的 HMR。
|
- 前端改动**不使用 `npm run build` 验证**,直接依赖 Vite 开发服务器的 HMR。
|
||||||
- `npm run build`(`vue-tsc -b && vite build`)的产物仅用于生产部署,与开发模式无关。
|
- `npm run build`(`vue-tsc -b && vite build`)的产物仅用于生产部署,与开发模式无关。
|
||||||
- 界面文案统一在 `frontend/src/locales/messages.ts` 维护,新增 key 需同时提供中文(zh)、英文(en)、日文(ja)三语。
|
- 界面文案统一在 `frontend/src/locales/messages.ts` 维护,新增 key 需同时提供中文(zh)、英文(en)、日文(ja)三语。
|
||||||
|
- 界面偏好需持久化记忆,存储键统一以 `ledgerly.` 前缀命名并集中维护于 `frontend/src/utils/storageKeys.ts`,读写经 `frontend/src/utils/storage.ts`(禁止业务代码直接调用 `localStorage`/`sessionStorage`);旧 `finance.` 前缀由 `migrateLegacyPrefixedKeys()` 自动迁移。详见[前端用户偏好](/dev/preferences)。
|
||||||
|
- **与 AI 相关的界面元素必须受 AI 启用状态控制**:未启用时一律不渲染(`v-if="aiEnabled"`),启用状态统一走共享状态 `frontend/src/composables/useAiEnabled.ts`(侧边栏「智能查数」菜单项亦据此显隐)。
|
||||||
|
|
||||||
## 后端开发
|
## 后端开发
|
||||||
|
|
||||||
@@ -28,7 +30,7 @@
|
|||||||
- 修改 `src/main/resources` 下的资源文件后,需额外同步资源:
|
- 修改 `src/main/resources` 下的资源文件后,需额外同步资源:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker exec finance-backend-dev gradle -I /project/nexus-init.gradle processResources
|
docker exec ledgerly-backend-dev gradle -I /project/nexus-init.gradle processResources
|
||||||
```
|
```
|
||||||
|
|
||||||
- 仅在新增依赖、修改配置等需要彻底重启时才重启容器。
|
- 仅在新增依赖、修改配置等需要彻底重启时才重启容器。
|
||||||
@@ -38,19 +40,53 @@
|
|||||||
|
|
||||||
- **Javadoc**:新增 Java 文件的类级 javadoc 末尾必须加一行 `@date`,格式 `yyyy-MM-dd HH:mm:ss`,时间取文件创建时间。
|
- **Javadoc**:新增 Java 文件的类级 javadoc 末尾必须加一行 `@date`,格式 `yyyy-MM-dd HH:mm:ss`,时间取文件创建时间。
|
||||||
- **导入顺序**:由 Spotless 管理,三组、组间空行分隔:`java`/`javax` → 第三方 → `cn.rosercode`。
|
- **导入顺序**:由 Spotless 管理,三组、组间空行分隔:`java`/`javax` → 第三方 → `cn.rosercode`。
|
||||||
- 格式化:`docker exec finance-backend-dev gradle -I /project/nexus-init.gradle spotlessApply`
|
- 格式化:`docker exec ledgerly-backend-dev gradle -I /project/nexus-init.gradle spotlessApply`
|
||||||
- `spotlessCheck` 已挂到 `check`,`build` 时会校验。
|
- `spotlessCheck` 已挂到 `check`,`build` 时会校验。
|
||||||
- **日志**:统一使用 Lombok `@Slf4j`,按级别使用(`error` 未预期异常、`warn` 可预期业务失败、`info` 关键写操作与外部调用、`debug` 流程细节)。
|
- **日志**:统一使用 Lombok `@Slf4j`,按级别使用(`error` 未预期异常、`warn` 可预期业务失败、`info` 关键写操作与外部调用、`debug` 流程细节)。
|
||||||
- **控制器**:方法签名禁止声明 `HttpServletRequest` / `HttpServletResponse`,统一使用 `cn.rosercode.finance.util.UserContext`。
|
- **控制器**:方法签名禁止声明 `HttpServletRequest` / `HttpServletResponse`,统一使用 `cn.rosercode.ledgerly.util.UserContext`。
|
||||||
- **数据库表结构变更**:需同时维护 `sql/init.sql`(MySQL)与 `sql/schema-sqlite.sql`(SQLite),SQLite 以 `;;` 分隔语句。
|
- **数据库表结构变更**:需同时维护 `sql/init.sql`(MySQL)与 `sql/schema-sqlite.sql`(SQLite),SQLite 以 `;;` 分隔语句。
|
||||||
- **对象转换**:实体与 VO 之间统一使用 MapStruct(`converter.VoConverter`),不在 Service 中手写 setter。
|
- **对象转换**:实体与 VO 之间统一使用 MapStruct(`converter.VoConverter`),不在 Service 中手写 setter。
|
||||||
- **定时任务**:基于 Spring `@Scheduled`,新增调度器直接标注即可。
|
- **定时任务**:基于 Spring `@Scheduled`,新增调度器直接标注即可。
|
||||||
|
|
||||||
|
## 代码检查
|
||||||
|
|
||||||
|
项目统一引入代码检查工具并挂到构建/打包流程,检查不通过即阻断,详见[构建与部署](/dev/build-deploy)。
|
||||||
|
|
||||||
|
**后端(Gradle)**:
|
||||||
|
|
||||||
|
- **Spotless**:格式化与导入顺序(见「代码规范」),`spotlessCheck` 已挂到 `check` 且为 `compileJava` 前置。
|
||||||
|
- **Checkstyle**:配置 `gradle/checkstyle/checkstyle.xml`、抑制清单 `gradle/checkstyle/suppressions.xml`,`maxWarnings = 0`。
|
||||||
|
- **SpotBugs**:配置 `gradle/spotbugs/exclude.xml`,`effort = MAX`、`reportLevel = MEDIUM`;工具版本固定 `4.8.6`(规避其 BCEL 依赖与 Spring Boot BOM 选定 `commons-lang3` 版本的冲突)。
|
||||||
|
- 仅检查主源码(`checkstyleTest` / `spotbugsTest` 已禁用)。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 宿主机
|
||||||
|
./gradlew -I nexus-init.gradle lint # Spotless + Checkstyle + SpotBugs + 前端 ESLint
|
||||||
|
./gradlew -I nexus-init.gradle checkstyleMain spotbugsMain
|
||||||
|
# 容器内(开发模式)
|
||||||
|
docker exec ledgerly-backend-dev gradle -I /project/nexus-init.gradle checkstyleMain spotbugsMain
|
||||||
|
```
|
||||||
|
|
||||||
|
**前端(ESLint + Prettier)**:
|
||||||
|
|
||||||
|
- 配置:`frontend/eslint.config.js`(flat config)、`frontend/.prettierrc.json`、`frontend/.prettierignore`。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run lint # eslint .
|
||||||
|
npm run lint:fix # eslint . --fix
|
||||||
|
npm run format # prettier --write .
|
||||||
|
npm run format:check # prettier --check .
|
||||||
|
```
|
||||||
|
|
||||||
|
- Gradle 的 `frontendLint` 在 `frontendBuild` 前执行;`package.sh` 打包前也会执行 `spotlessCheck checkstyleMain spotbugsMain`,任一检查失败都会中断构建与打包。
|
||||||
|
|
||||||
## Git 提交规范
|
## Git 提交规范
|
||||||
|
|
||||||
- 提交范围仅包含源码与配置;个人账单原始数据、`docs/`、`reload.sh`、`.gitignore`、`nexus-init.gradle` 及 AI 测试环境基础设施不纳入提交。
|
- 提交范围仅包含源码与配置;个人账单原始数据、`docs/`、`reload.sh`、`.gitignore`、`nexus-init.gradle` 及 AI 测试环境基础设施不纳入提交。
|
||||||
- commit message 使用 `feat: 中文描述` 风格;纯重构/规范调整用 `refactor:`,构建与依赖配置用 `chore:` 或 `build:`。
|
- commit message 采用「标题 + 概述段 + 分组列表」结构;标题为 `<type>: <中文简述>`,纯重构/规范调整用 `refactor:`,构建与依赖配置用 `chore:` 或 `build:`。
|
||||||
- 远端为本地 Gitea,主分支 `master`;推送使用一次性内嵌令牌的 URL,避免令牌写入 `origin` 配置。
|
- 远端为本地 Gitea,**提交与推送仅使用 `dev` 分支**(`master` 仅用于稳定/发布,不纳入日常开发);推送使用一次性内嵌令牌的 URL,避免令牌写入 `origin` 配置。
|
||||||
|
- 每次创建 commit 后需执行 `./package.sh` 打包验证,并在 `frontend/` 执行一次 `npx vue-tsc -b --force` 类型检查。
|
||||||
|
|
||||||
## 相关文档
|
## 相关文档
|
||||||
|
|
||||||
|
|||||||
+4
-3
@@ -5,17 +5,18 @@
|
|||||||
## 设计与实现
|
## 设计与实现
|
||||||
|
|
||||||
- [整体架构](/dev/architecture):技术栈、后端分层、请求流程、认证与前端结构
|
- [整体架构](/dev/architecture):技术栈、后端分层、请求流程、认证与前端结构
|
||||||
- [数据模型](/dev/data-model):17 张表的字段与关系、双数据库脚本差异
|
- [数据模型](/dev/data-model):24 张表的字段与关系、双数据库脚本差异
|
||||||
- [账单解析](/dev/bill-parsing):解析器策略模式、两阶段导入、去重与 zip 解密
|
- [账单解析](/dev/bill-parsing):解析器策略模式、两阶段导入、去重与 zip 解密
|
||||||
- [规则引擎](/dev/rule-engine):规则模型与匹配逻辑
|
- [规则引擎](/dev/rule-engine):规则模型与匹配逻辑
|
||||||
- [AI 集成](/dev/ai):OpenAI SDK 客户端、9 类调用类型与用量记录
|
- [AI 集成](/dev/ai):RestClient 客户端、13 类调用类型与用量记录
|
||||||
- [定时通知](/dev/notifications):调度、渠道与发送日志
|
- [定时通知](/dev/notifications):调度、渠道与发送日志
|
||||||
- [动态数据源](/dev/database):SQLite / MySQL 运行时配置与切换
|
- [动态数据源](/dev/database):SQLite / MySQL 运行时配置与切换
|
||||||
|
- [前端用户偏好](/dev/preferences):偏好的存储介质、读写时机与完整清单
|
||||||
|
|
||||||
## 工程与规范
|
## 工程与规范
|
||||||
|
|
||||||
- [项目结构](/dev/project-structure):目录组织与各包职责
|
- [项目结构](/dev/project-structure):目录组织与各包职责
|
||||||
- [构建与部署](/dev/build-deploy):`bootJar`、Docker 编排、桌面 GUI 启动器与 jpackage
|
- [构建与部署](/dev/build-deploy):`bootJar` / `bootGuiJar`、jpackage 免安装包、发布产物下载与云服务器部署
|
||||||
- [开发环境与规范](/dev/development):AI 测试环境、前后端开发流程、代码规范、Git 提交规范
|
- [开发环境与规范](/dev/development):AI 测试环境、前后端开发流程、代码规范、Git 提交规范
|
||||||
- [账单字段说明](/dev/bill-fields):支付宝账单导入字段的读取与丢弃情况
|
- [账单字段说明](/dev/bill-fields):支付宝账单导入字段的读取与丢弃情况
|
||||||
- [API 文档](/dev/api):Knife4j(OpenAPI 3)接口文档
|
- [API 文档](/dev/api):Knife4j(OpenAPI 3)接口文档
|
||||||
|
|||||||
@@ -17,13 +17,14 @@
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `EMAIL` | 邮箱 SMTP(如 QQ 邮箱),动态构建 `JavaMailSenderImpl`,默认 `smtp.qq.com:465` + SSL |
|
| `EMAIL` | 邮箱 SMTP(如 QQ 邮箱),动态构建 `JavaMailSenderImpl`,默认 `smtp.qq.com:465` + SSL |
|
||||||
| `SERVERCHAN` | Server 酱 Turbo,推送到个人微信,校验返回 `code == 0` |
|
| `SERVERCHAN` | Server 酱 Turbo,推送到个人微信,校验返回 `code == 0` |
|
||||||
|
| `FEISHU` | 飞书自定义机器人 Webhook,post 富文本(标题保留为卡片标题、正文按行分段),可选加签(`timestamp` + HMAC-SHA256),校验 `code` / `StatusCode` |
|
||||||
|
|
||||||
发送器采用工厂模式:`NotificationSender` 接口 + `NotificationSenderFactory` 按类型分发,`AbstractHttpNotificationSender` 封装 JSON/表单 POST。
|
发送器采用工厂模式:`NotificationSender` 接口 + `NotificationSenderFactory` 按类型分发,`AbstractHttpNotificationSender` 封装 JSON/表单 POST。
|
||||||
|
|
||||||
## 配置与日志
|
## 配置与日志
|
||||||
|
|
||||||
- `t_notification_config`:启用开关、周期、每月日期、每周星期、提醒时间、最近发送时间。
|
- `t_notification_config`:启用开关、周期、每月日期、每周星期、提醒时间、最近发送时间。
|
||||||
- `t_notification_channel`:类型、名称、启用、配置 JSON(SMTP 账号或 SendKey)。邮箱密码留空表示不修改,返回时脱敏。
|
- `t_notification_channel`:类型、名称、启用、配置 JSON(SMTP 账号、Server 酱 SendKey 或飞书 `webhookUrl` / `secret`)。邮箱密码与飞书密钥留空表示不修改,返回时脱敏,`NotificationChannelVO` 以 `hasPassword` / `hasSecret` 标记是否已配置。
|
||||||
- `t_notification_log`:每次发送的结果(成功/失败、渠道类型、说明),面板展示最近 20 条。
|
- `t_notification_log`:每次发送的结果(成功/失败、渠道类型、说明),面板展示最近 20 条。
|
||||||
|
|
||||||
## 接口
|
## 接口
|
||||||
|
|||||||
@@ -0,0 +1,292 @@
|
|||||||
|
# 前端用户偏好与持久化
|
||||||
|
|
||||||
|
本文梳理前端「用户偏好」的保存逻辑:**为什么保存、保存了哪些偏好、存在哪里、何时读写、目的是什么**。
|
||||||
|
|
||||||
|
## 为什么保存
|
||||||
|
|
||||||
|
汇账的不少界面是「多 Tab」或「带筛选条件」的,如果每次进入都重置为默认值,用户每次都要重新选一遍语言、筛选项、分页、排序、上次使用的记账方式等,体验割裂。
|
||||||
|
|
||||||
|
为此项目在 `ledgerly/AGENTS.md` 中约定:
|
||||||
|
|
||||||
|
> 多 Tab 或可切换的界面偏好需**持久化记忆**(`localStorage`),下次打开时恢复到上次选择;存储键统一以 `ledgerly.` 前缀命名,读取时对非法值回退默认项,写入时忽略存储异常。
|
||||||
|
|
||||||
|
据此形成三条实践原则:
|
||||||
|
|
||||||
|
1. **进入页面即还原**:`onMounted` 或模块初始化时读取上次选择。
|
||||||
|
2. **变化即保存**:用 `watch` 监听偏好变量,任一变化自动写入,无需用户点「保存」。
|
||||||
|
3. **容错**:读取时校验取值白名单,非法/损坏数据回退默认;写入包在 `try/catch` 中,忽略隐私模式等存储异常。
|
||||||
|
|
||||||
|
偏好分三层存储:
|
||||||
|
|
||||||
|
| 存储介质 | 生命周期 | 适用场景 |
|
||||||
|
|---|---|---|
|
||||||
|
| `localStorage` | 跨会话、跨标签页 | 语言、筛选条件、Tab、历史记录等界面偏好 |
|
||||||
|
| `sessionStorage` | 仅当前浏览器会话 | 内容区滚动位置(刷新/切换恢复) |
|
||||||
|
| 后端(按当前用户) | 跟随账号 | 需要跨设备/多端一致的外观与功能配置 |
|
||||||
|
|
||||||
|
> 登录凭证 `token` 也存放在 `localStorage`,但它属于**认证状态**而非界面偏好,见文末说明。
|
||||||
|
|
||||||
|
## 总览表
|
||||||
|
|
||||||
|
### localStorage 偏好
|
||||||
|
|
||||||
|
| 键名 | 偏好内容 | 文件 | 保存时机 | 读取时机 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `ledgerly.locale` | 界面语言 `zh-CN`/`en-US`/`ja-JP` | `locales/index.ts` | 切换语言 | 模块初始化 |
|
||||||
|
| `ledgerly.calendar.view` | 日历视图 `month`/`year` | `views/Calendar.vue` | `useStringPreference` 自动 | 组件初始化 |
|
||||||
|
| `ledgerly.calendar.year` | 日历最近浏览年份(四位数字符串) | `views/Calendar.vue` | 变量 `watch` 自动 | 组件初始化 |
|
||||||
|
| `ledgerly.calendar.month` | 日历月视图最近浏览月份(`1`-`12`) | `views/Calendar.vue` | 变量 `watch` 自动 | 组件初始化 |
|
||||||
|
| `ledgerly.calendar.statsScope` | 日历统计口径 `INCLUDED`/`ALL`/`EXCLUDED` | `views/Calendar.vue` | `useStringPreference` 自动 | 组件初始化 |
|
||||||
|
| `ledgerly.dashboard.prefs` | 统计面板时间范围、粒度与多项筛选 | `views/Dashboard.vue` | 变量 `watch` 自动 | `onMounted` |
|
||||||
|
| `ledgerly.transaction.filterPrefs` | 收支列表筛选、分页、排序 | `views/TransactionList.vue` | 变量 `watch` 自动 | `onMounted` |
|
||||||
|
| `ledgerly.transaction.aiScope` | 批量 AI 建议范围 | `views/TransactionList.vue` | 变量 `watch` 自动 | ref 初始化 |
|
||||||
|
| `ledgerly.transaction.aiRecordTab` | AI 记账方式(文字/图片) | `views/TransactionList.vue` | 变量 `watch` 自动 | ref 初始化 |
|
||||||
|
| `ledgerly.transaction.aiRuleApplyToSelection` | AI 生成规则时是否同时应用到勾选账单(`true`/`false`,默认 `true`) | `components/AiRuleSuggestModal.vue` | `useStringPreference` 自动 | 弹窗初始化 |
|
||||||
|
| `ledgerly.transaction.aiQueryHistory` | AI 自然语言查询历史 | `views/TransactionList.vue` / `components/AiQueryWindow.vue` | 提交查询时 | 浮窗聚焦时 |
|
||||||
|
| `ledgerly.dashboard.aiSearchHistory` | 统计面板 AI 查询历史 | `views/Dashboard.vue` | 提交 AI 搜索时 | 浮窗聚焦时 |
|
||||||
|
| `ledgerly.transaction.aiQueryWindow` | 收支记录 AI 查询浮窗位置与尺寸 | `components/FloatingPanel.vue` | 拖拽/缩放结束时 | 浮窗初始化 |
|
||||||
|
| `ledgerly.dashboard.aiQueryWindow` | 统计面板 AI 搜索浮窗位置与尺寸 | `components/FloatingPanel.vue` | 拖拽/缩放结束时 | 浮窗初始化 |
|
||||||
|
| `ledgerly.transaction.remarkHistory` | 备注关键词历史 | `views/TransactionList.vue` | 备注失焦/回车 | `onMounted` |
|
||||||
|
| `ledgerly.billImport.billType` | 上次选择的账单来源类型 | `views/BillImportPanel.vue` | 切换类型时 | ref 初始化 |
|
||||||
|
| `ledgerly.transaction.relationType` | 账单关系类型偏好 | `components/TransactionRelationEditorModal.vue` | 创建模式切换类型时 | 弹窗初始化 |
|
||||||
|
| `ledgerly.pagination.tablePageSizes` | 各表格分别记住的每页条数 | `utils/tablePageSizePrefs.ts` | 切换每页条数时 | 表格初始化 |
|
||||||
|
| `ledgerly.emailBill.tab` | 邮箱账单页标签页 | `views/EmailBillManage.vue` | Tab 切换时 | 路由重定向 + 组件 |
|
||||||
|
| `ledgerly.logs.tab` | 日志页标签页 | `views/Logs.vue` | Tab 切换时 | 路由重定向 + 组件 |
|
||||||
|
|
||||||
|
> `locale`、`emailBill.tab`、`logs.tab` 是较早的实现,最初未加前缀,现已统一为 `ledgerly.` 前缀:在 `LEGACY_STORAGE_KEYS` 登记旧键、读取前用 `migrateKey` 一次性迁移;品牌改名(`finance.` → `ledgerly.`)的旧键由 `migrateLegacyPrefixedKeys()` 在模块加载时批量迁移,新键已存在时不覆盖。
|
||||||
|
|
||||||
|
### sessionStorage 偏好
|
||||||
|
|
||||||
|
| 键名 | 偏好内容 | 文件 |
|
||||||
|
|---|---|---|
|
||||||
|
| `ledgerly.scroll.<路由路径>` | 各页面内容区滚动位置 | `views/Layout.vue` |
|
||||||
|
| `ledgerly.sqlQuery.session` | 智能查数当前会话的提问、结果与视图(结果超 500 行时只存提问与视图) | `views/SqlQuery.vue` |
|
||||||
|
|
||||||
|
### 后端按用户同步的偏好
|
||||||
|
|
||||||
|
| 接口 | 偏好内容 | 前端入口 |
|
||||||
|
|---|---|---|
|
||||||
|
| `GET/PUT /api/setting/excluded-display-style` | 「不计入统计」记录展示样式 | `components/DisplayConfigPanel.vue` |
|
||||||
|
| `GET/PUT /api/setting/show-hidden-data` | 是否显示已隐藏备注的记录 | `components/DisplayConfigPanel.vue` |
|
||||||
|
| `GET/PUT /api/setting/chart-click-navigation` | 统计面板图表点击是否跳转明细 | `components/DisplayConfigPanel.vue` / `views/Dashboard.vue` |
|
||||||
|
| `GET/PUT /api/setting/pagination-prefs` | 表格分页候选与默认每页条数 | `components/DisplayConfigPanel.vue` |
|
||||||
|
| `GET/PUT /api/setting/nav-menu` | 左侧导航顺序与显隐 | `components/NavMenuConfigPanel.vue` |
|
||||||
|
| `GET/PUT /api/setting/bill-source-include-in-stats` | 各账单来源「计入统计」默认值 | `components/BillSourceStatsPanel.vue` |
|
||||||
|
| `GET/PUT /api/ai/config` | AI 启用开关、密钥、模型 | `components/AiConfigPanel.vue` |
|
||||||
|
|
||||||
|
## localStorage 偏好详解
|
||||||
|
|
||||||
|
### 界面语言 `ledgerly.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`。
|
||||||
|
|
||||||
|
### 日历偏好 `ledgerly.calendar.view` / `ledgerly.calendar.year` / `ledgerly.calendar.month` / `ledgerly.calendar.statsScope`
|
||||||
|
|
||||||
|
- **文件**:`frontend/src/views/Calendar.vue`
|
||||||
|
- **取值**:
|
||||||
|
- `ledgerly.calendar.view`:`month`(月视图,默认)/ `year`(年视图);非法值回退 `month`。
|
||||||
|
- `ledgerly.calendar.year`:最近浏览的年份(四位数字符串),月视图面板年份与年视图年份共用;非法值回退当前年。
|
||||||
|
- `ledgerly.calendar.month`:月视图最近浏览的月份(`1`-`12`);非法值回退当前月。
|
||||||
|
- `ledgerly.calendar.statsScope`:`INCLUDED`(仅计入统计,默认)/ `ALL`(全部记录)/ `EXCLUDED`(仅不计入);非法值回退 `INCLUDED`。
|
||||||
|
- **读取**:组件初始化时经 `useStringPreference` 读取;URL 中的 `view`/`y`/`m`(如 `?view=year&y=2026`)优先于偏好,无参数时按偏好恢复上次视图、年月与统计口径。
|
||||||
|
- **保存**:`view`/`statsScope` 由 `useStringPreference` 内部 `watch` 自动写入;`year`/`month` 由组件内 `watch(panelYear/panelMonth)`(月视图)与 `watch(yearViewYear)`(年视图)写入。
|
||||||
|
- **目的**:下次进入日历自动还原上次的月/年视图、年月与统计口径。
|
||||||
|
|
||||||
|
### 统计面板偏好 `ledgerly.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 行)把统计口径与筛选**合并写入**收支列表的偏好键,并保留分页、排序等既有设置。
|
||||||
|
- **目的**:下次进入统计面板自动还原时间范围、粒度与全部过滤条件。
|
||||||
|
|
||||||
|
### 收支列表筛选偏好 `ledgerly.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 相关偏好(收支列表)
|
||||||
|
|
||||||
|
| 键名 | 取值 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `ledgerly.transaction.aiScope` | `BOTH`/`CATEGORY`/`TAG` | 批量 AI 建议范围,非法值回退 `BOTH`(第 146 行) |
|
||||||
|
| `ledgerly.transaction.aiRecordTab` | `text`/`image` | AI 记账方式,非法值回退 `text`(第 206 行) |
|
||||||
|
| `ledgerly.transaction.aiQueryHistory` | 字符串数组(≤10) | AI 自然语言查询历史,最新在前 |
|
||||||
|
| `ledgerly.transaction.remarkHistory` | 字符串数组(≤10) | 备注关键词历史,失焦/回车记录 |
|
||||||
|
|
||||||
|
- **`aiRecordTab`** 是 `AGENTS.md` 点名的参考实现:记住用户上次用的记账方式(文字或图片),下次打开 AI 记账弹窗直接落到该 Tab。
|
||||||
|
- **关键词历史**统一由 `composables/useKeywordHistory.ts` 管理:去重(忽略大小写)、最新在前、上限 10 条,损坏数据自动忽略,下拉末尾提供「清空历史」项。
|
||||||
|
- 读取:`load()`;记录:`record()`;清空:`clear()`。
|
||||||
|
- 三处使用:AI 查询历史、备注历史(均在 `TransactionList.vue`)、统计面板 AI 搜索历史(`Dashboard.vue`,键 `ledgerly.dashboard.aiSearchHistory`)。
|
||||||
|
|
||||||
|
### 账单导入类型 `ledgerly.billImport.billType`
|
||||||
|
|
||||||
|
- **文件**:`frontend/src/views/BillImportPanel.vue`(第 25 行)
|
||||||
|
- **取值**:账单来源编码(`ALIPAY`、`WECHAT`、`CMB` 等)
|
||||||
|
- **读取**:`readBillTypePref()`(第 62 行)作为初始值;`loadBillTypes()` 拿到后端列表后校正,非法值回退首个来源。
|
||||||
|
- **保存**:`watch(billType)`(第 226 行)在切换类型时写入,同时清空已选文件,避免格式与类型不匹配。
|
||||||
|
- **目的**:记住上次选择的账单来源。
|
||||||
|
|
||||||
|
### 账单关系类型 `ledgerly.transaction.relationType`
|
||||||
|
|
||||||
|
- **文件**:`frontend/src/components/TransactionRelationEditorModal.vue`(第 14 行)
|
||||||
|
- **取值**:`PARENT_CHILD`(默认)、`SIBLING`
|
||||||
|
- **读取**:`loadPreferredRelationType()`(第 81 行)在新建弹窗初始化时调用。
|
||||||
|
- **保存**:`watch(editorType)`(第 69 行)**仅在创建模式**(`editingGroupId === null`)写入,编辑既有关系组不覆盖偏好。
|
||||||
|
- **目的**:新建关系组时默认沿用上次的关系类型。
|
||||||
|
|
||||||
|
### 页面标签页 `ledgerly.emailBill.tab` / `ledgerly.logs.tab`
|
||||||
|
|
||||||
|
- **文件**:`views/EmailBillManage.vue`、`views/Logs.vue`
|
||||||
|
- **取值**:邮箱页 `configs`/`senderRules`/`messages`;日志页 `http`/`login`/`app`
|
||||||
|
- **读取**:路由 `/email-bills`、`/logs` 的 `redirect` 读取偏好并拼接子路径(`router/index.ts`),带白名单校验,无记录回退默认。
|
||||||
|
- **保存**:`watch(activeTab, ..., { immediate: true })` 在 Tab 切换时写入。
|
||||||
|
- **目的**:下次点击「邮箱账单」「日志」回到上次标签页。
|
||||||
|
- **说明**:这两个是 `AGENTS.md` 点名的参考实现,早期未加前缀,现已统一为 `ledgerly.` 前缀并做旧键迁移。
|
||||||
|
|
||||||
|
### AI 查询浮窗位置 `ledgerly.transaction.aiQueryWindow` / `ledgerly.dashboard.aiQueryWindow`
|
||||||
|
|
||||||
|
- **文件**:`components/FloatingPanel.vue`(键经 `storageKey` 传入,由 `AiQueryWindow.vue` 复用)
|
||||||
|
- **取值**:`{ x, y, width, height }`,非法值回退居中与默认尺寸(默认 560 × 360)。
|
||||||
|
- **保存**:拖拽或缩放结束时写入;读取在浮窗初始化时。
|
||||||
|
- **目的**:记住 AI 查询浮窗上次的位置与大小,下次打开保持原样。
|
||||||
|
|
||||||
|
### 各表格每页条数 `ledgerly.pagination.tablePageSizes`
|
||||||
|
|
||||||
|
- **文件**:`frontend/src/utils/tablePageSizePrefs.ts`、`composables/useTablePageSize.ts`
|
||||||
|
- **取值**:`Record<tableKey, number>`,按表格键(`transaction`、`categoryExpense`、`rule`、`logsHttp`、`smart-query` 等)分别记录。
|
||||||
|
- **优先级**:本机记录(且须在当前分页候选中)> 全局默认值(后端 `paginationPrefs`)> 20。
|
||||||
|
- **保存**:切换某表格每页条数时写入;候选列表与全局默认值在[设置 → 外观设置](/guide/settings#外观设置)中配置(存后端)。
|
||||||
|
|
||||||
|
## sessionStorage 偏好:滚动位置
|
||||||
|
|
||||||
|
- **文件**:`frontend/src/views/Layout.vue`(前缀第 83 行,键为 `ledgerly.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` 编辑保存(可点整行或开关切换)。
|
||||||
|
- **目的**:按账单来源设置导入时默认是否计入统计,仅影响统计口径,不改变导入结果。
|
||||||
|
|
||||||
|
### 显示已隐藏备注的记录
|
||||||
|
|
||||||
|
- **接口**:`GET/PUT /api/setting/show-hidden-data`(`api/setting.ts`)
|
||||||
|
- **数据结构**:`{ show: boolean }`,默认 `false`。
|
||||||
|
- **存储**:`t_user_preference` 的 `pref_key = showHiddenData`,值以 `"true"`/`"false"` 保存。
|
||||||
|
- **前端**:设置页 `components/DisplayConfigPanel.vue` 切换即存;收支列表与 Excel 导出据此过滤(关闭时排除 `remark_hidden = true` 的记录),统计备注搜索不受影响。
|
||||||
|
- **目的**:统一控制「已隐藏备注的记录」是否出现在列表与导出中。
|
||||||
|
|
||||||
|
### 图表点击跳转明细
|
||||||
|
|
||||||
|
- **接口**:`GET/PUT /api/setting/chart-click-navigation`(`api/setting.ts`)
|
||||||
|
- **数据结构**:`{ enabled: boolean }`,默认 `true`(开启)。
|
||||||
|
- **存储**:`t_user_preference` 的 `pref_key = chartClickNavigation`,值以 `"true"`/`"false"` 保存;未配置时读取默认开启。
|
||||||
|
- **前端**:共享单例 `composables/useChartClickNavigation.ts`(`chartClickEnabled` + `ensureChartClickEnabledLoaded` / `refreshChartClickEnabled`,读取失败回退默认开启);设置页 `components/DisplayConfigPanel.vue` 与统计面板 `views/Dashboard.vue` 筛选区两处开关读写同一状态、即时联动。关闭后 `Dashboard.vue` 的点击跳转入口(`triggerChartNavigation` / `handleTrendBlankClick`)直接返回,并重绘图表以同步光标、隐藏点击提示。
|
||||||
|
- **目的**:允许用户关闭统计面板图表的点击下钻交互,跨设备生效。
|
||||||
|
|
||||||
|
### 表格分页偏好
|
||||||
|
|
||||||
|
- **接口**:`GET/PUT /api/setting/pagination-prefs`(`api/setting.ts`)
|
||||||
|
- **数据结构**:`{ options: number[], defaultSize: number }`,默认候选 `[10, 20, 50, 100]`、默认 20;后端会规范化(去重升序、1–1000、最多 10 项)。
|
||||||
|
- **存储**:`t_user_preference` 的 `pref_key = paginationPrefs`(JSON)。
|
||||||
|
- **前端**:设置页 `components/DisplayConfigPanel.vue` 编辑、防抖 500ms 自动保存;`composables/usePageSizeOptions.ts` 提供全局单例,`useTablePageSize(tableKey)` 读取各表记忆值。
|
||||||
|
- **目的**:统一所有表格的分页候选与默认每页条数。
|
||||||
|
|
||||||
|
### 导航顺序与显隐
|
||||||
|
|
||||||
|
- **接口**:`GET/PUT /api/setting/nav-menu`(`api/setting.ts`)
|
||||||
|
- **数据结构**:`{ items: [{ key, visible }] }`,未配置时以前端 `utils/navItems.ts` 的默认顺序与可见性为准。
|
||||||
|
- **存储**:`t_user_preference` 的 `pref_key = navMenu`(JSON)。
|
||||||
|
- **前端**:设置页 `components/NavMenuConfigPanel.vue`(`vuedraggable` 拖拽 + 开关,600ms 防抖保存、可恢复默认);`composables/useNavMenu.ts` 与后端合并,`Layout.vue` 据此渲染。隐藏仅移除菜单入口,路由仍可访问。
|
||||||
|
- **目的**:按账号自定义左侧导航的顺序与显隐,跨设备生效。
|
||||||
|
|
||||||
|
### AI 配置
|
||||||
|
|
||||||
|
- **接口**:`GET /api/ai/config`、`PUT /api/ai/config`(`api/ai.ts`)
|
||||||
|
- **数据结构**:`{ enabled, apiKey, hasApiKey, baseUrl, model, visionModel }`,`apiKey` 留空表示不修改原值。
|
||||||
|
- **前端**:设置页 `components/AiConfigPanel.vue` 编辑保存。
|
||||||
|
- **目的**:用户级 AI 能力配置;其他页面的 AI 元素统一经 `getAiConfig().enabled` 控制显隐。
|
||||||
|
|
||||||
|
> 严格说 AI 配置属「功能配置」而非纯界面偏好,但因按用户存储,一并列出。设置页中的数据库配置、注册开关、定时通知为**系统级全局配置**,不在此列。
|
||||||
|
|
||||||
|
## 未实现的偏好
|
||||||
|
|
||||||
|
以下常见界面偏好当前**没有**持久化实现,改动时无需假设其存在:
|
||||||
|
|
||||||
|
- **主题 / 暗色模式**:无主题存储键,界面主题固定。
|
||||||
|
- **侧边栏整体折叠 / 展开**:`Layout.vue` 使用固定 `NLayoutSider`,无折叠状态(但菜单项的**顺序与显隐**可配置,见上「导航顺序与显隐」)。
|
||||||
|
- **表格列显示 / 隐藏 / 顺序**:各页列配置为固定 `computed`。
|
||||||
|
- **货币 / 金额格式**:无货币偏好。
|
||||||
|
- **独立日期格式**:仅由语言 `locale` 间接驱动 Naive UI 日期本地化,无单独偏好。
|
||||||
|
- **IndexedDB / cookie**:未使用。
|
||||||
|
- **Pinia 持久化插件 / vueuse `useStorage`**:未安装、未使用。
|
||||||
|
|
||||||
|
## 附:登录凭证 `token`
|
||||||
|
|
||||||
|
- **文件**:`frontend/src/store/user.ts`(第 8 行)
|
||||||
|
- **保存**:`setToken()` 写入 `localStorage`;`logout()` 移除。
|
||||||
|
- **使用**:`api/request.ts` 请求拦截器读取并写入 `Authorization` 头。
|
||||||
|
- **说明**:这是认证状态,**不属于界面偏好**,列出以免混淆。`nickname`、`userId` 不持久化,每次由 `fetchUserInfo()` 从后端获取。
|
||||||
|
|
||||||
|
## 新增偏好的约定
|
||||||
|
|
||||||
|
新增一项用户界面偏好时:
|
||||||
|
|
||||||
|
1. 存储键常量集中登记在 `frontend/src/utils/storageKeys.ts` 的 `STORAGE_KEYS`,以 `ledgerly.` 前缀命名、按页面/模块分段(如 `ledgerly.transaction.xxx`);涉及改名时在 `LEGACY_STORAGE_KEYS` 登记旧键并用 `migrateKey` 迁移。
|
||||||
|
2. 读写统一经 `frontend/src/utils/storage.ts`,**禁止**在业务代码中直接调用 `localStorage`/`sessionStorage`。
|
||||||
|
3. 优先用 `watch` 自动保存,避免要求用户手动点保存。
|
||||||
|
4. 读取时对取值做白名单校验,非法或损坏数据回退默认项;写入包 `try/catch`,忽略存储异常。
|
||||||
|
5. 组件内的字符串型偏好用 `useStringPreference`,关键词历史用 `useKeywordHistory`,浮窗位置尺寸用 `FloatingPanel`(键经 `storageKey` 传入)。
|
||||||
|
6. 需要跨设备一致时,改存后端并按当前用户隔离,而非 `localStorage`。
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
## 仓库根目录
|
## 仓库根目录
|
||||||
|
|
||||||
```text
|
```text
|
||||||
finance-app/
|
ledgerly/
|
||||||
├── build.gradle # Gradle 构建脚本(依赖、打包、前端集成、Spotless)
|
├── build.gradle # Gradle 构建脚本(依赖、打包、前端集成、Spotless)
|
||||||
├── settings.gradle # Gradle 项目设置
|
├── settings.gradle # Gradle 项目设置
|
||||||
├── gradlew / gradlew.bat # Gradle Wrapper
|
├── gradlew / gradlew.bat # Gradle Wrapper
|
||||||
@@ -19,13 +19,13 @@ finance-app/
|
|||||||
|
|
||||||
## 后端包结构
|
## 后端包结构
|
||||||
|
|
||||||
`src/main/java/cn/rosercode/finance/`
|
`src/main/java/cn/rosercode/ledgerly/`
|
||||||
|
|
||||||
| 包 | 职责 |
|
| 包 | 职责 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `client` | 外部服务客户端(DeepSeek) |
|
| `client` | 外部服务客户端(DeepSeek) |
|
||||||
| `common` | 通用响应(`Result`、`PageResult`)、业务异常与全局异常处理 |
|
| `common` | 通用响应(`Result`、`PageResult`)、业务异常与全局异常处理 |
|
||||||
| `config` | 应用配置、动态数据源、Knife4j、MyBatis-Plus、SPA 路由回退 |
|
| `config` | 应用配置、动态数据源、Knife4j、MyBatis-Plus、SPA 路由回退;`config.crypto` 为接口加解密拦截器与请求/响应体 Advice |
|
||||||
| `controller` | REST 接口层 |
|
| `controller` | REST 接口层 |
|
||||||
| `converter` | MapStruct 对象转换器(`VoConverter`) |
|
| `converter` | MapStruct 对象转换器(`VoConverter`) |
|
||||||
| `dto` | 请求对象 |
|
| `dto` | 请求对象 |
|
||||||
@@ -38,8 +38,9 @@ finance-app/
|
|||||||
| `mapper` | 数据访问层(MyBatis-Plus) |
|
| `mapper` | 数据访问层(MyBatis-Plus) |
|
||||||
| `notification` | 通知渠道发送器 |
|
| `notification` | 通知渠道发送器 |
|
||||||
| `scheduler` | 定时任务(通知、日志清理、邮箱账单) |
|
| `scheduler` | 定时任务(通知、日志清理、邮箱账单) |
|
||||||
| `service` / `service.impl` | 业务层 |
|
| `service` / `service.impl` | 业务层(交易记录服务已按职责拆分:查询条件构建、VO 富化、标签写入、批量操作、导出组装) |
|
||||||
| `util` | 工具类(`JwtUtil`、`UserContext`、`AppLogUtils` 等) |
|
| `util` | 工具类(`JwtUtil`、`UserContext`、`AppLogUtils`、`ImageUtils`、`AppHome`、`TransactionDedupUtils` 等);`util.sql` 为智能查数的安全网关(`TextToSqlGuard` / `TextToSqlTables` / `TextToSqlSchemaProvider`);`util.crypto` 为接口加解密服务(`ApiCryptoService` / `CryptoSessionStore` / `RsaKeyManager`) |
|
||||||
|
| `prompt` | AI 提示词模板加载(`PromptTemplateService`,模板位于 `resources/prompts/`) |
|
||||||
| `vo` | 响应对象 |
|
| `vo` | 响应对象 |
|
||||||
| `launcher` | 桌面 GUI 启动器(仅打入带 GUI 的 jar) |
|
| `launcher` | 桌面 GUI 启动器(仅打入带 GUI 的 jar) |
|
||||||
|
|
||||||
@@ -51,6 +52,7 @@ finance-app/
|
|||||||
application.yml # 应用配置(JWT、MyBatis-Plus、springdoc、端口等)
|
application.yml # 应用配置(JWT、MyBatis-Plus、springdoc、端口等)
|
||||||
logback-spring.xml # 日志配置(控制台 + 按天滚动文件)
|
logback-spring.xml # 日志配置(控制台 + 按天滚动文件)
|
||||||
mapper/ # MyBatis XML
|
mapper/ # MyBatis XML
|
||||||
|
prompts/ # AI 提示词模板(Thymeleaf TEXT 模式,*.tpl;系统/用户提示词为同文件不同片段)
|
||||||
sql/
|
sql/
|
||||||
├── init.sql # MySQL 建表脚本
|
├── init.sql # MySQL 建表脚本
|
||||||
├── schema-sqlite.sql # SQLite 建表脚本(以 ;; 分隔语句)
|
├── schema-sqlite.sql # SQLite 建表脚本(以 ;; 分隔语句)
|
||||||
@@ -67,11 +69,12 @@ App.vue # 应用根组件
|
|||||||
main.ts # 入口(挂载应用与插件)
|
main.ts # 入口(挂载应用与插件)
|
||||||
style.css # 全局样式
|
style.css # 全局样式
|
||||||
api/ # 各模块 REST 接口封装(axios)
|
api/ # 各模块 REST 接口封装(axios)
|
||||||
components/ # 复用组件(账单预览、导入结果、AI/通知/数据库配置面板等)
|
components/ # 复用组件(账单预览、导入结果、AI/通知/数据库配置面板、账单关系、AI 查询浮窗等)
|
||||||
|
composables/ # 组合式函数(AI 启用状态、导航配置、分页选项、关键词历史等)
|
||||||
locales/ # 中/英/日三语文案(messages.ts)
|
locales/ # 中/英/日三语文案(messages.ts)
|
||||||
router/ # 路由与导航守卫
|
router/ # 路由与导航守卫
|
||||||
store/ # Pinia 状态(user / database / registration)
|
store/ # Pinia 状态(user / database / registration)
|
||||||
utils/ # 工具(交易渠道标签、SSE 封装、渲染辅助)
|
utils/ # 工具(交易渠道标签、SSE 封装、本地偏好存储、渲染辅助)
|
||||||
views/ # 页面组件
|
views/ # 页面组件
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -83,7 +86,8 @@ views/ # 页面组件
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `config/database.json` | 数据库配置(SQLite / MySQL) |
|
| `config/database.json` | 数据库配置(SQLite / MySQL) |
|
||||||
| `config/app.json` | 应用配置(注册开关、通知链接、日志保留天数、按账单来源的计入统计默认值) |
|
| `config/app.json` | 应用配置(注册开关、通知链接、日志保留天数、按账单来源的计入统计默认值) |
|
||||||
| `data/` | SQLite 数据文件与邮箱账单附件 |
|
| `config/crypto-keys.json` | 接口加解密用的 RSA 密钥对(首次启动自动生成) |
|
||||||
|
| `data/` | SQLite 数据文件、邮箱账单附件与图片记账截图(`data/ai-images/`) |
|
||||||
| `logs/` | 运行日志文件 |
|
| `logs/` | 运行日志文件 |
|
||||||
|
|
||||||
## 相关文档
|
## 相关文档
|
||||||
|
|||||||
+16
-4
@@ -29,19 +29,31 @@
|
|||||||
| 账单解析预览 | 展示命中的规则名与建议分类/标签 |
|
| 账单解析预览 | 展示命中的规则名与建议分类/标签 |
|
||||||
| 账单确认导入 | 规则建议分类优先于面板默认分类;标签与默认标签合并去重 |
|
| 账单确认导入 | 规则建议分类优先于面板默认分类;标签与默认标签合并去重 |
|
||||||
| 新建/编辑记录 | 不自动应用,但可借助 AI 推荐 |
|
| 新建/编辑记录 | 不自动应用,但可借助 AI 推荐 |
|
||||||
|
| 规则管理页「应用」 | 手动把规则应用到命中的历史账单(先预览确认;分类覆盖、标签追加) |
|
||||||
|
|
||||||
## 列表查询与分页
|
## 列表查询与分页
|
||||||
|
|
||||||
- `GET /api/rule/list`:返回当前用户全部规则(按优先级排序),供内部复用。
|
- `GET /api/rule/list`:返回当前用户全部规则(按优先级排序),供内部复用。
|
||||||
- `GET /api/rule/page`:分页查询,支持 `keyword`(匹配规则名称或关键词)、`enabled`、`type` 筛选,返回 `PageResult<RuleVO>`。
|
- `GET /api/rule/page`:分页查询,支持 `keyword`(匹配规则名称或关键词)、`enabled`、`type` 筛选,返回 `PageResult<RuleVO>`。
|
||||||
|
- `DELETE /api/rule/batch`:批量删除(body `{ ids }`);先按 `userId` 过滤归属,清理规则-标签关联后删除,返回实际删除数。
|
||||||
|
|
||||||
|
## 一键应用到匹配账单
|
||||||
|
|
||||||
|
规则默认只在导入时生效,不会改写历史账单;`RuleService.previewApply` / `applyToMatchedTransactions` 支持把规则手动应用到**历史账单**(前端规则管理页的行内「应用」/ 勾选后的「应用匹配账单」):
|
||||||
|
|
||||||
|
- `POST /api/rule/apply-preview`(`previewApply`):校验规则归属后加载当前用户**全部交易**,按规则条件(类型 + 关键词 + 金额区间;关键词匹配备注、忽略大小写)逐条匹配;**分类**取所选规则中优先级最高(`sort_order` 最小)且类型一致者,**标签**取所有命中规则标签的并集。返回命中明细 `RuleMatchedTransactionVO`(含当前分类/标签与目标分类/标签)与统计 `RuleApplyPreviewVO`(命中数、将改分类数、将加标签数、是否截断)。明细最多 500 条(`PREVIEW_MAX_ROWS`),超出置 `truncated`,统计仍按全部命中计算。
|
||||||
|
- `POST /api/rule/apply`(`applyToMatchedTransactions`):复用同一匹配逻辑,**分类覆盖写入**(仅类型一致时)、**标签仅追加缺失项**(不删除已有标签),返回 `RuleApplyResultVO`(命中/变更/新增标签数),幂等。
|
||||||
|
- 入参 DTO `RuleApplyRequest`(`ruleIds`,非空);均为**纯本地匹配**,不调用大模型;**不限规则是否启用**(停用规则也可手动应用)。
|
||||||
|
|
||||||
## AI 生成规则
|
## AI 生成规则
|
||||||
|
|
||||||
`AiRuleServiceImpl` 依据用户勾选的**已打标签记录**生成规则建议:
|
`AiRuleServiceImpl` 提供两个入口:规则管理页(生成 + 创建)与收支记录页批量操作(生成 + 可选应用到账单)。
|
||||||
|
|
||||||
- 输入字段为记录的类型、备注、交易渠道、已有分类与标签(不含金额与原始账单数据)。
|
- 生成建议(`suggest`,`POST /api/ai/rule/suggest`):依据用户勾选的记录生成规则建议;规则管理页限定**已打标签记录**,收支记录页为**任意勾选账单**。输入字段为记录的类型、备注、交易渠道、已有分类与标签(不含金额与原始账单数据),并按「类型 + 备注 + 渠道 + 标签」组合去重。
|
||||||
- 输出规则经 `RuleService.create` 落库,必要时新建分类/标签,规则默认启用。
|
- 创建规则(`apply`,`POST /api/ai/rule/apply`):输出规则经 `RuleService.create` 落库,必要时新建分类/标签,规则默认启用。
|
||||||
- **去重**:以「类型 + 归一化关键词 + 分类 + 排序标签集合」为签名;与已有规则或同批已创建规则重复时跳过(`apply` 返回实际创建数),建议阶段则标记 `duplicate` 供前端禁止勾选。
|
- 应用到交易(`applyToTransactions`,`POST /api/ai/rule/apply-transactions`):**纯本地匹配、不调用大模型**,按规则的类型/关键词/金额条件匹配指定交易,命中则聚合分类与标签,委托 `AiSuggestionService.apply` 落库(覆盖分类、追加标签,必要时新建)。
|
||||||
|
- 去重校验(`checkDuplicates`,`POST /api/ai/rule/check-duplicates`):**纯本地、不调用大模型**,复用同一签名逻辑,对**编辑后的规则**逐条返回是否重复(与已有规则签名一致或批内先前规则重复),入参 DTO `AiRuleDuplicateCheckRequest`(复用 `AiRuleApplyRequest.Item`)。前端规则弹窗编辑任一字段后防抖(约 300ms)调用它刷新 `duplicate` 标记。
|
||||||
|
- **去重签名**:以「类型 + 归一化关键词 + 分类 + 排序标签集合」为签名;与已有规则或同批已创建规则重复时跳过(`apply` 返回实际创建数),建议阶段与编辑重算阶段则标记 `duplicate`;前端重复项默认不勾选且禁止勾选,编辑后重算时「重复→不重复」自动勾选、「不重复→重复」自动取消并禁用。
|
||||||
|
|
||||||
## 校验
|
## 校验
|
||||||
|
|
||||||
|
|||||||
+60
-17
@@ -1,6 +1,6 @@
|
|||||||
# AI 能力
|
# AI 能力
|
||||||
|
|
||||||
系统集成 **DeepSeek** 大模型,在分类打标、查询、记账等环节提供智能辅助。所有 AI 功能需先在「系统设置 → AI 模型配置」中启用并配置 API Key。
|
系统集成 **DeepSeek** 大模型,在分类打标、查询、记账等环节提供智能辅助。所有 AI 功能需先在「设置 → AI 模型配置」中启用并配置 API Key。
|
||||||
|
|
||||||
## 数据与隐私
|
## 数据与隐私
|
||||||
|
|
||||||
@@ -18,69 +18,112 @@
|
|||||||
- 智能查询 / 统计:你输入的自然语言,以及分类与标签名称;
|
- 智能查询 / 统计:你输入的自然语言,以及分类与标签名称;
|
||||||
- 智能记账:你输入的一句话;
|
- 智能记账:你输入的一句话;
|
||||||
- 生成分类/标签、推荐 Emoji/颜色:你输入的名称或描述;
|
- 生成分类/标签、推荐 Emoji/颜色:你输入的名称或描述;
|
||||||
- 生成规则:所选记录的类型、备注、交易渠道、已有分类与标签(不含金额与原始账单数据)。
|
- 生成规则:所选记录的类型、备注、交易渠道、已有分类与标签(不含金额与原始账单数据);
|
||||||
|
- 生成账单关系名称:所选账单的收支类型、交易渠道、分类名称与备注(不含金额与原始账单数据);
|
||||||
|
- 智能查数:你输入的自然语言,以及可供查询的表结构说明(不含账单原始字段);
|
||||||
|
- 图片记账:你**主动上传**的账单/支付截图会**整张**发送给视觉模型(可能含金额、交易对方、订单号等),视为你主动提供的输入。
|
||||||
|
|
||||||
|
图片同时会保存在本地(`data/ai-images/`),仅本人可查看;请勿上传包含他人敏感信息的图片。
|
||||||
|
|
||||||
API Key 仅保存在本地数据库中,仅用于向你配置的接口地址发起请求,不会随上述数据一并外发。
|
API Key 仅保存在本地数据库中,仅用于向你配置的接口地址发起请求,不会随上述数据一并外发。
|
||||||
|
|
||||||
## 配置
|
## 配置
|
||||||
|
|
||||||
在「系统设置 → AI 模型配置」中:
|
在「设置 → AI 模型配置」中:
|
||||||
|
|
||||||
- 开启「启用 AI 建议」。
|
- 开启「启用 AI 建议」。
|
||||||
- 填写 **API Key**。
|
- 填写 **API Key**。
|
||||||
- 可选修改 **接口地址**(默认 `https://api.deepseek.com`)与**模型**(默认 `deepseek-chat`)。
|
- 可选修改 **接口地址**(默认 `https://api.deepseek.com`)、**模型**(默认 `deepseek-chat`)与**视觉模型**(默认 `deepseek-flash`,用于图片记账)。
|
||||||
- 点击「测试连接」验证配置。
|
- 点击「测试连接」验证配置。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 功能一览
|
## 功能一览
|
||||||
|
|
||||||
| 功能 | 入口 | 说明 |
|
| 功能 | 入口 | 说明 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 分类/标签建议 | 收支记录 → 一键设置 → AI 智能建议 | 对当前页记录批量建议分类与标签,可一键采纳 |
|
| 分类/标签建议 | 收支记录 → 一键设置 → AI 智能建议 | 对所选记录批量建议分类与标签(未勾选时为当前页),可一键采纳 |
|
||||||
| 单条建议 | 新增/编辑记录 → AI 推荐 | 推荐已有分类或标签 |
|
| 单条建议 | 新增/编辑记录 → AI 推荐 | 推荐已有分类、标签或交易渠道 |
|
||||||
| 智能查询 | 收支记录 → AI 智能查询 | 自然语言解析为筛选条件 |
|
| 智能查询 | 收支记录 → AI 智能查询 | 自然语言解析为筛选条件 |
|
||||||
| 智能记账 | 收支记录 → 智能记账 | 一句话解析为一笔记录 |
|
| 智能记账(文字) | 收支记录 → 智能记账 → 文字记账 | 一句话解析为一笔记录 |
|
||||||
|
| 智能记账(图片) | 收支记录 → 智能记账 → 图片记账 | 上传一至多张账单/支付截图,实时识别为多条记录,勾选后批量导入 |
|
||||||
| 生成分类 | 分类管理 → AI 生成 | 由用途描述生成分类 |
|
| 生成分类 | 分类管理 → AI 生成 | 由用途描述生成分类 |
|
||||||
|
| 生成渠道 | 新增/编辑记录 → 交易渠道 → AI 新建 | 由账单描述生成渠道名称并创建 |
|
||||||
| 推荐 Emoji | 分类管理 → AI 推荐 | 为分类推荐 Emoji 图标 |
|
| 推荐 Emoji | 分类管理 → AI 推荐 | 为分类推荐 Emoji 图标 |
|
||||||
| 推荐颜色 | 标签管理 → AI 推荐 | 为标签推荐颜色 |
|
| 推荐颜色 | 标签管理 → AI 推荐 | 为标签推荐颜色 |
|
||||||
| 生成规则 | 规则管理 → AI 生成规则 | 由已打标签的记录归纳分类与标签规则 |
|
| 生成规则 | 规则管理 → AI 生成规则(已打标签记录)/收支记录 → 批量操作 → AI 生成规则(勾选账单) | 由所选记录归纳分类与标签规则,规则可编辑、去重实时重算,可同时应用到勾选账单(默认开启) |
|
||||||
| 统计搜索 | 统计面板 → AI 搜索 | 自然语言解析为统计筛选条件与维度 |
|
| 统计搜索 | 统计面板 → AI 搜索 | 自然语言解析为统计筛选条件与维度 |
|
||||||
|
| 生成关系名称 | 收支记录 → 账单关系 → AI 生成 | 依据关联账单的类型、渠道、分类与备注生成关系名称与备注 |
|
||||||
|
| 智能查数 | 左侧菜单 → 智能查数 | 自然语言转只读 SQL 查询并展示结果 |
|
||||||
|
| 手动新建分类/标签/渠道 | 新增/编辑记录 → 新建分类 / 新建标签 / 新建渠道 | 不依赖 AI,直接创建并选中(始终可用) |
|
||||||
|
|
||||||
## 分类与标签建议
|
## 分类与标签建议
|
||||||
|
|
||||||
在收支记录页点「一键设置」,选择建议范围(分类与标签 / 仅分类 / 仅标签)后点「AI 智能建议」。系统会按记录的类型、备注、交易渠道等聚合后请求模型,返回建议并进入 AI 建议弹窗。
|
在收支记录页点「一键设置」,选择建议范围(分类与标签 / 仅分类 / 仅标签)后点「AI 智能建议」。作用范围为有勾选时的勾选记录、未勾选时的当前页记录;系统会按记录的类型、备注、交易渠道等聚合后请求模型,返回建议并进入 AI 建议弹窗。
|
||||||
|
|
||||||
弹窗中每条记录展示建议分类、建议标签与理由,默认全选。若建议了系统中不存在的分类或标签,会以「新建」标记,确认后自动创建。应用时分类会被覆盖、标签会被追加。
|
弹窗中每条记录展示建议分类、建议标签与理由,默认全选。若建议了系统中不存在的分类或标签,会以「新建」标记,确认后自动创建。应用时分类会被覆盖、标签会被追加。
|
||||||
|
|
||||||
## 智能查询
|
## 智能查询
|
||||||
|
|
||||||
在收支记录页的 AI 查询框中输入自然语言,例如:
|
在收支记录页点「AI 搜索」打开**可拖拽、可缩放的浮窗**,在输入框中用自然语言描述,例如:
|
||||||
|
|
||||||
- 「上个月餐饮」→ 时间范围 + 分类
|
- 「上个月餐饮」→ 时间范围 + 分类
|
||||||
- 「超过500的支出」→ 类型 + 金额区间
|
- 「超过500的支出」→ 类型 + 金额区间
|
||||||
- 「待报销的支出」→ 标签 + 类型
|
- 「待报销的支出」→ 标签 + 类型
|
||||||
- 「微信支付」→ 交易渠道
|
- 「微信支付」→ 交易渠道
|
||||||
|
|
||||||
解析结果会直接应用为筛选条件并立即查询。
|
按 `Ctrl + Enter`(Mac 为 `⌘ + Enter`)提交,解析结果会直接应用为筛选条件并立即查询;浮窗的位置与大小会被记住。详见[收支记录](/guide/transactions)。
|
||||||
|
|
||||||
## 智能记账
|
## 智能记账
|
||||||
|
|
||||||
在「智能记账」弹窗中描述一笔收支,AI 解析出类型、金额、分类、交易渠道、时间、备注、标签等字段,并预填到新增记录表单,确认后保存。
|
「智能记账」弹窗提供「**文字记账**」与「[图片记账](#图片记账)」两个标签页,再次打开时会记住上次使用的方式。
|
||||||
|
|
||||||
|
在「文字记账」中描述一笔收支,AI 解析出类型、金额、分类、交易渠道、时间、备注、标签等字段,并预填到新增记录表单,确认后保存。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## 图片记账
|
||||||
|
|
||||||
|
在「智能记账」弹窗切换到「**图片记账**」标签页,上传账单或支付截图,AI 会调用**视觉模型**一次性识别其中的每一笔交易。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- **多图上传**:一次最多 **5 张**,单张不超过 **10MB**、总大小不超过 **30MB**;支持 JPEG / PNG / GIF / WebP。
|
||||||
|
- **粘贴上传**:也可按 `Ctrl+V`(Mac 为 `⌘V`)直接粘贴剪贴板中的图片。
|
||||||
|
- **补充说明**(可选):在下方输入框补充提示(如「这是 9 月的账单」),帮助模型理解。
|
||||||
|
- 点击「识别图片」后,弹窗内会**实时显示识别进度**(校验图片、保存、调用模型、解析、查重等步骤),耗时较长的识别过程可见可控。
|
||||||
|
- 识别完成后进入**结果弹窗**:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- 结果以表格逐条展示,**默认全选**(疑似重复项默认不勾选),可直接编辑**类型、金额、分类、时间、备注**。
|
||||||
|
- **来源图**列标注该笔交易来自第几张上传图片;**重复**列标记与已有记录或本批其它记录「类型+金额+时间」一致的疑似重复项。
|
||||||
|
- **原始数据**列可查看模型从图片中提取的原始字段(时间、对方、商品、订单号等),用于核对。
|
||||||
|
- 顶部摘要说明识别到的账单内容;勾选后点击「导入勾选」批量写入收支记录(导入时会再次按去重键跳过重复)。
|
||||||
|
- 未识别到有效交易时会给出提示,不会写入任何记录。
|
||||||
|
|
||||||
|
> 图片记账使用的是视觉模型,可在「设置 → AI 模型配置」中单独配置(默认 `deepseek-flash`)。详见[设置](/guide/settings)。
|
||||||
|
|
||||||
|
导入后的记录可在[收支记录](/guide/transactions)的「**详情**」弹窗中回看来源图片(默认加载缩略图,可点「加载原图」切换为原图)。
|
||||||
|
|
||||||
## AI 统计搜索
|
## AI 统计搜索
|
||||||
|
|
||||||
在统计面板顶部的 AI 搜索框中用一句自然语言描述统计意图,例如「上个月餐饮花了多少」「近半年交通支出趋势,按周看」。系统会解析为**时间范围、收支类型、分类、标签、交易渠道、金额区间、备注、统计口径与趋势粒度**并自动回填、刷新图表。详见[统计面板](/guide/dashboard)。
|
在统计面板顶部点「AI 搜索」打开浮窗,用一句自然语言描述统计意图,例如「上个月餐饮花了多少」「近半年交通支出趋势,按周看」。系统会解析为**时间范围、收支类型、分类、标签、交易渠道、金额区间、备注、统计口径与趋势粒度**并自动回填、刷新图表。详见[统计面板](/guide/dashboard)。
|
||||||
|
|
||||||
|
> 想要更自由的问答式查询(直接生成查询并返回明细),可使用[智能查数](/guide/sql-query)。
|
||||||
|
|
||||||
## AI 生成规则
|
## AI 生成规则
|
||||||
|
|
||||||
在规则管理页点「AI 生成规则」,选择用于归纳的**已打标签记录**(默认全选当前页,支持搜索、筛选与分页),AI 会依据这些记录的**类型、备注、交易渠道、已有分类与标签**生成规则建议,预览编辑后批量创建。与已有规则重复的建议会标记「已存在」并禁止导入。详见[规则管理](/guide/rules)。
|
有两种生成方式:
|
||||||
|
|
||||||
|
- **规则管理页**:点「AI 生成规则」,选择用于归纳的**已打标签记录**(默认全选当前页,支持搜索、筛选与分页),AI 依据这些记录的**类型、备注、交易渠道、已有分类与标签**生成规则建议;建议可**逐字段编辑**(名称、类型、关键词、金额区间、分类、标签),编辑后自动重算去重,勾选后批量创建。
|
||||||
|
- **收支记录页**:在**批量操作**区勾选若干账单后点「AI 生成规则」,直接以**勾选的账单**为依据归纳规则(不要求已打标签);规则同样可编辑、去重实时重算,弹窗中还可勾选「**同时应用到当前勾选的账单**」(**默认开启**并记住上次选择),命中时覆盖这些账单的分类并追加标签。
|
||||||
|
|
||||||
|
两种方式都会对与已有规则重复的建议标记「已存在」并禁止导入,且**编辑后实时重算**(改到不重复自动勾选、改成重复自动取消并禁用)。详见[规则管理](/guide/rules#ai-生成规则)与[收支记录](/guide/transactions#ai-生成规则从勾选账单)。
|
||||||
|
|
||||||
## 用量统计
|
## 用量统计
|
||||||
|
|
||||||
「模型使用情况」页面(系统设置 → AI 模型配置 →「使用情况」)展示 AI 调用汇总与明细。
|
「模型使用情况」页面(设置 → AI 模型配置 →「使用情况」)展示 AI 调用汇总与明细。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -91,5 +134,5 @@ API Key 仅保存在本地数据库中,仅用于向你配置的接口地址发
|
|||||||
## 说明
|
## 说明
|
||||||
|
|
||||||
- AI 建议结果仅供参考,应用前可逐条取消勾选。
|
- AI 建议结果仅供参考,应用前可逐条取消勾选。
|
||||||
- 关闭「启用 AI 建议」后,系统不会发起任何 AI 请求,相关入口也会隐藏。
|
- 关闭「启用 AI 建议」后,系统不会发起任何 AI 请求,相关入口也会隐藏(分类/标签/渠道的**手动新建**入口不受影响,始终可用)。
|
||||||
- 账单类型识别(邮箱账单)使用的是**发件人规则**而非 AI,结果更确定、可预期。
|
- 账单类型识别(邮箱账单)使用的是**发件人规则**而非 AI,结果更确定、可预期。
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# 日历
|
||||||
|
|
||||||
|
「日历」是登录后的默认首页,用月视图与年视图直观查看每天的收支情况。左侧菜单默认置于首项,访问 `/` 也会自动进入本页。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 月视图
|
||||||
|
|
||||||
|
- 每天展示当天**收入**(绿色,`+`)、**支出**(红色,`-`)与**结余**;同一天既有收入又有支出时显示三行,否则只显示对应的一侧。
|
||||||
|
- **今天**以高亮标记;点击网格中相邻月份的灰色日期会自动切换到该月。
|
||||||
|
- 顶部四张卡片显示**当月合计**:总收入、总支出、结余、交易笔数。
|
||||||
|
|
||||||
|
## 切换月份与视图
|
||||||
|
|
||||||
|
表头提供:
|
||||||
|
|
||||||
|
- 「‹」「›」:切换上一月 / 下一月(年视图下为上一年 / 下一年);
|
||||||
|
- **年份 / 月份下拉**:直接跳转到任意年月;
|
||||||
|
- **今天**:回到当前月并选中今天;
|
||||||
|
- **月视图 / 年视图**:切换视图。
|
||||||
|
|
||||||
|
## 年视图
|
||||||
|
|
||||||
|
年视图以 12 个月的卡片展示每月收入、支出与结余,无数据的月份显示「无数据」,当前月高亮。点击任意月份卡片即可进入该月的月视图。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
某一年完全没有数据时,会显示「本年暂无交易数据」的空状态提示。
|
||||||
|
|
||||||
|
## 统计口径
|
||||||
|
|
||||||
|
表头的「**统计口径**」下拉控制参与汇总的记录范围:
|
||||||
|
|
||||||
|
| 选项 | 含义 |
|
||||||
|
|---|---|
|
||||||
|
| 仅计入统计(默认) | 只统计「计入统计」的记录 |
|
||||||
|
| 全部记录 | 计入与不计入的记录都统计 |
|
||||||
|
| 仅不计入 | 只统计「不计入统计」的记录 |
|
||||||
|
|
||||||
|
切换后日历中的金额、顶部合计与点击日期弹出的**当日明细**都会随之变化:明细只列出符合当前口径的记录,与汇总口径保持一致。
|
||||||
|
|
||||||
|
## 查看某天的明细
|
||||||
|
|
||||||
|
点击日历中的某一天,会弹出当天的**交易明细浮窗**:
|
||||||
|
|
||||||
|
- 顶部显示**当日合计**(收入 / 支出 / 结余);
|
||||||
|
- 下方逐条列出当天交易,每条在主行显示时间、分类、渠道与**金额**(收绿支红);
|
||||||
|
- 点击任意条目可打开该笔账单的**完整详情**(标签、账单关系、来源图片、原始数据等,为只读);
|
||||||
|
- 底部「**在收支记录中查看**」会携带当天日期与当前统计口径跳转到[收支记录](/guide/transactions),便于继续编辑整理。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> 当日明细(按当前统计口径)最多展示前 50 条,超出时浮窗内会提示总数,可点「在收支记录中查看」查看全部。
|
||||||
|
|
||||||
|
## 链接分享与偏好
|
||||||
|
|
||||||
|
- 当前视图与年月会同步到地址栏(形如 `?view=month&y=2026&m=9`),刷新页面或把链接分享给他人,都能还原到对应月份与视图。
|
||||||
|
- **视图**(月 / 年)、**统计口径**与**最近浏览的年月**会被记住,下次进入自动还原;地址栏带参数时以参数为准。
|
||||||
|
|
||||||
|
## 小技巧
|
||||||
|
|
||||||
|
- 想知道某一天具体花在哪,直接点该日期,先看当日合计,再逐条核对明细。
|
||||||
|
- 想看全年的分布,切到「年视图」浏览各月汇总,再点开支出最高的月份查看当天明细。
|
||||||
|
- 不计入统计的记录(如借款、账户间转账)默认不参与汇总,当日明细也不会列出;如需一并查看,把统计口径切换为「全部记录」。
|
||||||
|
|
||||||
|
> 想从更大的时间跨度看趋势与占比,可使用[统计面板](/guide/dashboard)的图表与筛选。
|
||||||
@@ -26,9 +26,12 @@
|
|||||||
|
|
||||||
- **编辑**:在分类行点击「编辑」修改。
|
- **编辑**:在分类行点击「编辑」修改。
|
||||||
- **删除**:点击「删除」并确认;已使用该分类的记录会变为「未分类」。
|
- **删除**:点击「删除」并确认;已使用该分类的记录会变为「未分类」。
|
||||||
|
- **批量删除**:点右上角「**批量操作**」进入批量选择模式,支出与收入两张表格都会出现勾选列,勾选后点「**批量删除**」并确认即可一次删除多个(两表勾选会合并处理);点「**退出批量**」退出并清空勾选。
|
||||||
- **图标**:使用 Emoji 选择器挑选,也可自定义输入。
|
- **图标**:使用 Emoji 选择器挑选,也可自定义输入。
|
||||||
- **AI 生成分类**:启用 AI 后,输入用途描述(如「日常通勤打车地铁」),点「AI 生成」自动填充名称、类型与图标。
|
- **AI 生成分类**:启用 AI 后,输入用途描述(如「日常通勤打车地铁」),点「AI 生成」自动填充名称、类型与图标。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
新注册用户会自动获得一套默认分类(如餐饮、交通、购物、居住、通讯、娱乐、医疗、教育等)。
|
新注册用户会自动获得一套默认分类(如餐饮、交通、购物、居住、通讯、娱乐、医疗、教育等)。
|
||||||
|
|
||||||
## 标签管理
|
## 标签管理
|
||||||
@@ -46,6 +49,9 @@
|
|||||||
- **颜色**:使用颜色选择器,内置常用色板;启用 AI 后点「AI 推荐」可按标签名称推荐颜色。
|
- **颜色**:使用颜色选择器,内置常用色板;启用 AI 后点「AI 推荐」可按标签名称推荐颜色。
|
||||||
- 标签以彩色标签形式在收支记录、导入预览等位置展示。
|
- 标签以彩色标签形式在收支记录、导入预览等位置展示。
|
||||||
- 删除标签会从所有交易记录中移除该标签,操作前会二次确认。
|
- 删除标签会从所有交易记录中移除该标签,操作前会二次确认。
|
||||||
|
- **批量删除**:点右上角「**批量操作**」后表格出现勾选列,勾选要删除的标签后点「**批量删除**」并确认,即可一次删除多个;点「**退出批量**」退出并清空勾选。批量删除同样会从所有交易记录中移除这些标签。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## 使用建议
|
## 使用建议
|
||||||
|
|
||||||
@@ -53,3 +59,4 @@
|
|||||||
- 建议为以下场景建立标签:报销/待报销、旅行、大额消费、订阅、礼物、家庭、工作等。
|
- 建议为以下场景建立标签:报销/待报销、旅行、大额消费、订阅、礼物、家庭、工作等。
|
||||||
- 需要按规则自动归类时,配合[规则管理](/guide/rules)使用:规则命中后会自动应用分类与标签。
|
- 需要按规则自动归类时,配合[规则管理](/guide/rules)使用:规则命中后会自动应用分类与标签。
|
||||||
- 分类与标签都支持在[账单导入](/guide/transactions)预览阶段批量指定为默认值。
|
- 分类与标签都支持在[账单导入](/guide/transactions)预览阶段批量指定为默认值。
|
||||||
|
- 在[新增/编辑记录](/guide/transactions#手动添加记录)弹窗中,可直接点分类/标签行右侧的「新建分类」「新建标签」创建并选用,无需先到本页维护。
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# 交易渠道管理
|
||||||
|
|
||||||
|
「交易渠道」用于标记一笔收支的付款或收款途径,既包括账单导入时的来源,也包括人工记账时选择的渠道。系统内置一组常用渠道,并支持按账号自定义。
|
||||||
|
|
||||||
|
## 入口
|
||||||
|
|
||||||
|
在[收支记录](/guide/transactions)页顶部操作区点击「**渠道管理**」进入(独立页面,不占用左侧菜单)。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
页面顶部提供返回按钮,可一键回到收支记录页。
|
||||||
|
|
||||||
|
## 内置渠道
|
||||||
|
|
||||||
|
以下渠道由系统内置,无需创建,名称随界面语言自动显示:
|
||||||
|
|
||||||
|
| 编码 | 名称 |
|
||||||
|
|---|---|
|
||||||
|
| `ALIPAY` | 支付宝 |
|
||||||
|
| `WECHAT` | 微信 |
|
||||||
|
| `MEITUAN` | 美团 |
|
||||||
|
| `TAOBAO` | 淘宝 |
|
||||||
|
| `JD` | 京东 |
|
||||||
|
| `CMB` | 招商银行 |
|
||||||
|
| `BOCOM` | 交通银行 |
|
||||||
|
| `CASH` | 现金 |
|
||||||
|
|
||||||
|
> 账单导入时,系统会按账单来源自动写入对应的内置渠道;人工记账默认可选「现金」或任意自定义渠道。
|
||||||
|
|
||||||
|
## 自定义渠道
|
||||||
|
|
||||||
|
除内置渠道外,你可以新建自己的渠道(例如「某张储蓄卡」「公司报销」等)。
|
||||||
|
|
||||||
|
### 新建渠道
|
||||||
|
|
||||||
|
点击右上角「**新建渠道**」,输入名称后确认。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- 渠道名称在**当前账号内不可重复**。
|
||||||
|
- 渠道编码由系统生成且**稳定不变**,交易记录按编码关联。
|
||||||
|
|
||||||
|
### 重命名渠道
|
||||||
|
|
||||||
|
在列表行点击「编辑」即可改名。**改名不影响历史账单**——所有已使用该渠道的记录会同步显示新名称。
|
||||||
|
|
||||||
|
### 删除渠道
|
||||||
|
|
||||||
|
在列表行点击「删除」即可删除。
|
||||||
|
|
||||||
|
- 若该渠道**已被账单使用**,系统会拒绝删除并提示使用数量,避免历史记录出现无效渠道。
|
||||||
|
- 列表中的「使用账单数」列可直观看到每个渠道被多少笔记录使用。
|
||||||
|
|
||||||
|
## 在哪些地方使用
|
||||||
|
|
||||||
|
自定义渠道创建后,可在以下场景直接选用:
|
||||||
|
|
||||||
|
- **新增 / 编辑记录**:交易渠道下拉中出现在内置渠道之后;也可点右侧「新建渠道」直接创建并选用(不受 AI 状态影响)。
|
||||||
|
- **筛选与统计**:收支记录页筛选、统计面板的渠道维度与筛选均可选择自定义渠道,图表与分组会展示其名称。
|
||||||
|
- **AI 能力**:智能查询、统计搜索与智能记账会把当前账号的自定义渠道一并作为可选渠道;新增/编辑记录时还可用「渠道 · AI 推荐」选择已有渠道,或「渠道 · AI 新建」由描述生成并创建渠道。
|
||||||
|
- **导出**:Excel 导出时渠道列展示自定义渠道名称。
|
||||||
|
|
||||||
|
> 删除前请先确认没有账单仍在使用该渠道;如需保留历史,建议改名而非删除。
|
||||||
|
|
||||||
|
## 相关
|
||||||
|
|
||||||
|
- [收支记录](/guide/transactions):记账、筛选与账单导入。
|
||||||
|
- [统计面板](/guide/dashboard):按渠道维度统计。
|
||||||
|
- [AI 能力](/guide/ai):自然语言查询与图片记账。
|
||||||
+32
-17
@@ -1,6 +1,6 @@
|
|||||||
# 统计面板
|
# 统计面板
|
||||||
|
|
||||||
「统计面板」是登录后的默认首页,用于查看指定时间范围内的收支汇总与图表。
|
「统计面板」用于查看指定时间范围内的收支汇总与图表,可从左侧菜单进入(登录后的默认首页为[日历](/guide/calendar))。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -23,15 +23,17 @@
|
|||||||
|
|
||||||
## AI 搜索
|
## AI 搜索
|
||||||
|
|
||||||
启用 AI 后,页面顶部会出现「**AI 搜索**」按钮,点击打开搜索弹窗。用一句自然语言描述统计意图,系统会调用大模型解析为统计参数并自动回填、刷新图表,例如:
|
启用 AI 后,页面顶部会出现「**AI 搜索**」按钮,点击打开一个**可拖拽、可缩放的浮窗**(非模态)。用一句自然语言描述统计意图,系统会调用大模型解析为统计参数并自动回填、刷新图表,例如:
|
||||||
|
|
||||||
- 「上个月餐饮花了多少」→ 时间范围 + 分类
|
- 「上个月餐饮花了多少」→ 时间范围 + 分类
|
||||||
- 「近半年交通支出趋势,按周看」→ 时间范围 + 分类 + 按周粒度
|
- 「近半年交通支出趋势,按周看」→ 时间范围 + 分类 + 按周粒度
|
||||||
- 「今年各类支出占比」→ 时间范围 + 类型
|
- 「今年各类支出占比」→ 时间范围 + 类型
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
可解析并回填的参数包括:**时间范围、收支类型、分类、标签、交易渠道、金额区间、备注、统计口径**与**趋势粒度**。弹窗内提供示例问题,输入框会记住历史记录(可一键清空);回车或点击「AI 查询」即可执行,无可用条件时会提示且不改动当前筛选。详见 [AI 能力](/guide/ai)。
|
可解析并回填的参数包括:**时间范围、收支类型、分类、标签、交易渠道、金额区间、备注、统计口径**与**趋势粒度**。浮窗内提供示例问题,输入框会记住历史记录(可一键清空);按 `Ctrl + Enter`(Mac 为 `⌘ + Enter`)或点击提交按钮执行,无可用条件时会提示且不改动当前筛选。详见 [AI 能力](/guide/ai)。
|
||||||
|
|
||||||
|
> 想要返回明细的自由问答式查询,可使用[智能查数](/guide/sql-query)。
|
||||||
|
|
||||||
## 筛选条件
|
## 筛选条件
|
||||||
|
|
||||||
@@ -39,11 +41,11 @@
|
|||||||
- **类型**:支出 / 收入(多选)。
|
- **类型**:支出 / 收入(多选)。
|
||||||
- **分类**:多选,含「未分类」。
|
- **分类**:多选,含「未分类」。
|
||||||
- **交易渠道**:多选。
|
- **交易渠道**:多选。
|
||||||
- **标签**:多选。
|
- **标签**:多选,含「无标签」。
|
||||||
- **金额区间**:最小 ~ 最大。
|
- **金额区间**:最小 ~ 最大。
|
||||||
- **备注搜索**。
|
- **备注搜索**。
|
||||||
|
|
||||||
点击「重置筛选」可清空筛选(保留时间范围与粒度)。筛选条件会记住,下次进入自动还原。
|
点击「重置筛选」可清空筛选(保留时间范围与粒度)。筛选条件会记住,下次进入自动还原。筛选区还提供「**点击图表查看明细**」开关(默认开启),可关闭图表点击下钻,详见 [点击图表查看明细](#点击图表查看明细)。
|
||||||
|
|
||||||
## 汇总卡片
|
## 汇总卡片
|
||||||
|
|
||||||
@@ -55,28 +57,41 @@
|
|||||||
|
|
||||||
| 图表 | 说明 |
|
| 图表 | 说明 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 收支趋势(柱状图) | 按所选粒度展示收入(绿)与支出(红),点击柱子或该列空白处可跳转查看对应明细 |
|
| 收支趋势(柱状图) | 按所选粒度展示收入(绿)与支出(红),**点击柱子或该列空白处**跳转查看对应明细 |
|
||||||
| 支出分类占比(环形图) | 各分类支出金额占比 |
|
| 支出分类占比(环形图) | 各分类支出金额占比,**点击扇区**跳转该分类明细 |
|
||||||
| 收入分类占比(环形图) | 各分类收入金额占比 |
|
| 收入分类占比(环形图) | 各分类收入金额占比,**点击扇区**跳转该分类明细 |
|
||||||
| 支出交易渠道占比(环形图) | 仅统计支出,按交易渠道 |
|
| 支出交易渠道占比(环形图) | 仅统计支出,按交易渠道,**点击扇区**跳转该渠道明细 |
|
||||||
| 收入交易渠道占比(环形图) | 仅统计收入,按交易渠道 |
|
| 收入交易渠道占比(环形图) | 仅统计收入,按交易渠道,**点击扇区**跳转该渠道明细 |
|
||||||
|
|
||||||
切换语言时,图表标题、图例与交易渠道名称会实时重绘。
|
切换语言时,图表标题、图例与交易渠道名称会实时重绘。
|
||||||
|
|
||||||
### 点击趋势图查看明细
|
### 点击图表查看明细
|
||||||
|
|
||||||
「收支趋势」柱状图的**整列都可点击**:点中柱子,或点击柱子顶端上方的空白处均可。点击后跳转到[收支记录](/guide/transactions),并自动带入该列对应的筛选条件:
|
所有图表都可点击下钻,跳转到[收支记录](/guide/transactions)并自动带入对应筛选条件:
|
||||||
|
|
||||||
- **类型**:点中柱子时取该柱子的收入 / 支出;点击空白处时不限定类型(沿用面板当前类型筛选);
|
- **趋势柱状图**:整列可点击——点中柱子,或点击柱子顶端上方的空白处均可。
|
||||||
- **日期范围**:该列代表的时间桶——按日→当天、按周→周一至周日、按月→当月、按季→当季、按年→当年;
|
- **类型**:点中柱子时取该柱子的收入 / 支出;点击空白处时不限定类型(沿用面板当前类型筛选);
|
||||||
- **其他筛选**:统计面板当前的统计口径、分类、交易渠道、标签、金额区间与备注关键字。
|
- **日期范围**:该列代表的时间桶——按日→当天、按周→周一至周日、按月→当月、按季→当季、按年→当年。
|
||||||
|
- **分类饼图**:点击某个扇区,按该分类(含「未分类」)与对应收支类型筛选。
|
||||||
|
- **渠道饼图**:点击某个扇区,按该交易渠道与对应收支类型筛选。
|
||||||
|
|
||||||
|
无论从哪张图下钻,都会同时保留面板当前的统计口径、标签、金额区间与备注关键字等筛选。
|
||||||
|
|
||||||
|
> 触屏设备上,点击趋势柱或饼图扇区会先弹出确认气泡(「查看明细」/「取消」),确认后才跳转,避免滑动或误触直接跳走;趋势图的**「整列可点」在触屏端禁用**,只能点击柱子本体。桌面端行为不变。
|
||||||
|
|
||||||
|
> **可关闭**:在统计面板筛选区或[设置 → 外观设置](/guide/settings#外观设置)中关闭「点击图表查看明细」(默认开启)后,点击任何图表元素都不再跳转,趋势图的「点击柱子查看对应明细」提示与饼图的手型光标也会一并隐藏;两处开关实时同步,重新开启即恢复下钻。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
跳转后可直接查看该时间段内的对应明细;筛选条件会照常记忆,收支记录页原有的每页条数与排序偏好不受影响。
|
页面底部的「**查看收支记录**」按钮则会携带**当前全部筛选条件**跳转,便于核对整体明细。
|
||||||
|
|
||||||
|
> 从[收支记录](/guide/transactions)点「查看统计」也会跳回本页,并还原收支记录当前的筛选条件(时间范围会切换为「自定义」)。
|
||||||
|
|
||||||
|
跳转后可直接查看对应明细;筛选条件会照常记忆,收支记录页原有的每页条数与排序偏好不受影响。
|
||||||
|
|
||||||
## 小技巧
|
## 小技巧
|
||||||
|
|
||||||
- 想知道「上个月餐饮花了多少」,可把时间范围设为「本月」或自定义,并在分类中只勾选「餐饮」。
|
- 想知道「上个月餐饮花了多少」,可把时间范围设为「本月」或自定义,并在分类中只勾选「餐饮」。
|
||||||
- 不计入统计的记录(如借款、账户间转账)默认不会计入汇总;如需查看全部,把统计口径切换为「全部记录」。
|
- 不计入统计的记录(如借款、账户间转账)默认不会计入汇总;如需查看全部,把统计口径切换为「全部记录」。
|
||||||
- 想快速核对某月、某周或某天的明细,直接点击「收支趋势」上对应的列(柱子或该列空白处),即可带着时间范围跳到收支记录。
|
- 想快速核对某月、某周或某天的明细,直接点击「收支趋势」上对应的列(柱子或该列空白处),即可带着时间范围跳到收支记录。
|
||||||
|
- 点击分类或渠道饼图的某个扇区,可一键下钻到该分类 / 渠道的明细;点「查看收支记录」则带上当前全部筛选。
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# 下载与安装
|
||||||
|
|
||||||
|
本章介绍如何获取「汇账」的安装包并在本机运行。
|
||||||
|
|
||||||
|
系统提供两种分发形态:
|
||||||
|
|
||||||
|
| 形态 | 适用场景 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| 免安装应用(app-image) | 推荐 | 自带精简 Java 运行时,解压即用,**无需另装 JDK** |
|
||||||
|
| 可执行 jar | 已有 JDK 21 的环境 | 单个 jar 文件,`java -jar` 运行 |
|
||||||
|
|
||||||
|
每个平台各提供两个版本:
|
||||||
|
|
||||||
|
- **个人记账(GUI 版)**:带图形界面启动器,可在窗口或系统托盘中启停服务、打开浏览器、实时查看日志。
|
||||||
|
- **后端服务(服务版)**:命令行启动,适合服务器或后台常驻运行。
|
||||||
|
|
||||||
|
## 下载地址
|
||||||
|
|
||||||
|
当前版本:`0.0.1-SNAPSHOT`。以下均为阿里云 OSS 长期签名地址,有效期至 **2036 年**。
|
||||||
|
|
||||||
|
### Windows(x64)
|
||||||
|
|
||||||
|
| 版本 | 下载 | 校验 |
|
||||||
|
|---|---|---|
|
||||||
|
| 个人记账(GUI) | [Ledgerly-windows-x64.zip](https://adccrac.oss-cn-beijing.aliyuncs.com/ledgerly/release/0.0.1-SNAPSHOT/windows/Ledgerly-windows-x64.zip?OSSAccessKeyId=LTAI5tA21KsXJTQoSmt4m7YJ&Expires=2105596492&Signature=2bzQh3TpGGA2qop%2FXh%2FbyN9av%2Fg%3D) | [sha256](https://adccrac.oss-cn-beijing.aliyuncs.com/ledgerly/release/0.0.1-SNAPSHOT/windows/Ledgerly-windows-x64.zip.sha256?OSSAccessKeyId=LTAI5tA21KsXJTQoSmt4m7YJ&Expires=2105596492&Signature=uKasei8G%2Bft2kZNscrJIRKfCg8c%3D) |
|
||||||
|
| 后端服务 | [LedgerlyServer-windows-x64.zip](https://adccrac.oss-cn-beijing.aliyuncs.com/ledgerly/release/0.0.1-SNAPSHOT/windows/LedgerlyServer-windows-x64.zip?OSSAccessKeyId=LTAI5tA21KsXJTQoSmt4m7YJ&Expires=2105596492&Signature=02PxWpHN%2FVjGnm%2BOAtzJakeG3GM%3D) | [sha256](https://adccrac.oss-cn-beijing.aliyuncs.com/ledgerly/release/0.0.1-SNAPSHOT/windows/LedgerlyServer-windows-x64.zip.sha256?OSSAccessKeyId=LTAI5tA21KsXJTQoSmt4m7YJ&Expires=2105596492&Signature=ToeO4rwW6I1%2FA12bMZOV4pKsMPs%3D) |
|
||||||
|
| 校验清单 | [SHA256SUMS](https://adccrac.oss-cn-beijing.aliyuncs.com/ledgerly/release/0.0.1-SNAPSHOT/windows/SHA256SUMS?OSSAccessKeyId=LTAI5tA21KsXJTQoSmt4m7YJ&Expires=2105596492&Signature=gyaLuqTqV9ra9CeJYEZZTOg1qFQ%3D) | |
|
||||||
|
|
||||||
|
### Linux(x64)
|
||||||
|
|
||||||
|
| 版本 | 下载 | 校验 |
|
||||||
|
|---|---|---|
|
||||||
|
| 个人记账(GUI) | [Ledgerly-linux-x64.tar.gz](https://adccrac.oss-cn-beijing.aliyuncs.com/ledgerly/release/0.0.1-SNAPSHOT/linux/Ledgerly-linux-x64.tar.gz?OSSAccessKeyId=LTAI5tA21KsXJTQoSmt4m7YJ&Expires=2105596492&Signature=cMLZ4xpv0lhz0GWC7FPXXThu4MM%3D) | [sha256](https://adccrac.oss-cn-beijing.aliyuncs.com/ledgerly/release/0.0.1-SNAPSHOT/linux/Ledgerly-linux-x64.tar.gz.sha256?OSSAccessKeyId=LTAI5tA21KsXJTQoSmt4m7YJ&Expires=2105596492&Signature=5hEMux5%2BmLcbrg%2BNuAbwvk15qSk%3D) |
|
||||||
|
| 后端服务 | [LedgerlyServer-linux-x64.tar.gz](https://adccrac.oss-cn-beijing.aliyuncs.com/ledgerly/release/0.0.1-SNAPSHOT/linux/LedgerlyServer-linux-x64.tar.gz?OSSAccessKeyId=LTAI5tA21KsXJTQoSmt4m7YJ&Expires=2105596492&Signature=VsJhCRQeWNqQuBfkyXI1llxbrko%3D) | [sha256](https://adccrac.oss-cn-beijing.aliyuncs.com/ledgerly/release/0.0.1-SNAPSHOT/linux/LedgerlyServer-linux-x64.tar.gz.sha256?OSSAccessKeyId=LTAI5tA21KsXJTQoSmt4m7YJ&Expires=2105596492&Signature=h2evKRHMhJhQTKgv2XFKSOMR14E%3D) |
|
||||||
|
| 校验清单 | [SHA256SUMS](https://adccrac.oss-cn-beijing.aliyuncs.com/ledgerly/release/0.0.1-SNAPSHOT/SHA256SUMS?OSSAccessKeyId=LTAI5tA21KsXJTQoSmt4m7YJ&Expires=2105596492&Signature=9AEbPWUyrZNxTbE3TCzVeIc3hy8%3D) | |
|
||||||
|
|
||||||
|
### 可执行 jar(跨平台)
|
||||||
|
|
||||||
|
需要本机已安装 **JDK 21**。
|
||||||
|
|
||||||
|
| 版本 | 下载 | 校验 |
|
||||||
|
|---|---|---|
|
||||||
|
| 普通 jar | [ledgerly-0.0.1-SNAPSHOT-jdk21.jar](https://adccrac.oss-cn-beijing.aliyuncs.com/ledgerly/release/0.0.1-SNAPSHOT/ledgerly-0.0.1-SNAPSHOT-jdk21.jar?OSSAccessKeyId=LTAI5tA21KsXJTQoSmt4m7YJ&Expires=2105596492&Signature=198KOxfQgVxy9Bp5EGyqTjbcoX4%3D) | [sha256](https://adccrac.oss-cn-beijing.aliyuncs.com/ledgerly/release/0.0.1-SNAPSHOT/ledgerly-0.0.1-SNAPSHOT-jdk21.jar.sha256?OSSAccessKeyId=LTAI5tA21KsXJTQoSmt4m7YJ&Expires=2105596492&Signature=YsP4w312R4%2BkqCFgqsaVUGY5q0k%3D) |
|
||||||
|
| 带 GUI 启动器 jar | [ledgerly-0.0.1-SNAPSHOT-jdk21-gui.jar](https://adccrac.oss-cn-beijing.aliyuncs.com/ledgerly/release/0.0.1-SNAPSHOT/ledgerly-0.0.1-SNAPSHOT-jdk21-gui.jar?OSSAccessKeyId=LTAI5tA21KsXJTQoSmt4m7YJ&Expires=2105596492&Signature=e6x%2FIxXBxINyaeX2BDkhnvgKvyY%3D) | [sha256](https://adccrac.oss-cn-beijing.aliyuncs.com/ledgerly/release/0.0.1-SNAPSHOT/ledgerly-0.0.1-SNAPSHOT-jdk21-gui.jar.sha256?OSSAccessKeyId=LTAI5tA21KsXJTQoSmt4m7YJ&Expires=2105596492&Signature=TfA5AwPl5OwdUv2FKLf%2F1XSXetQ%3D) |
|
||||||
|
|
||||||
|
## 运行
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
解压 zip 后进入解压出的目录:
|
||||||
|
|
||||||
|
- **个人记账(GUI)**:双击 `Ledgerly.exe`,在启动器界面上点击「启动服务」,再点「打开浏览器」。
|
||||||
|
- **后端服务**:运行 `LedgerlyServer.exe`,会附带控制台窗口显示日志;服务在本机 `http://localhost:8080` 提供页面与接口。
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
|
||||||
|
个人记账(GUI):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tar xzf Ledgerly-linux-x64.tar.gz
|
||||||
|
cd Ledgerly
|
||||||
|
./bin/Ledgerly
|
||||||
|
```
|
||||||
|
|
||||||
|
后端服务:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tar xzf LedgerlyServer-linux-x64.tar.gz
|
||||||
|
cd LedgerlyServer
|
||||||
|
./bin/LedgerlyServer
|
||||||
|
```
|
||||||
|
|
||||||
|
### 可执行 jar
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -jar ledgerly-0.0.1-SNAPSHOT-jdk21.jar
|
||||||
|
```
|
||||||
|
|
||||||
|
带 GUI 启动器的 jar(需要桌面环境):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -jar ledgerly-0.0.1-SNAPSHOT-jdk21-gui.jar
|
||||||
|
```
|
||||||
|
|
||||||
|
默认访问地址为 `http://localhost:8080`,可在「设置」中修改端口,或运行时用 `--server.port=18081` 指定。
|
||||||
|
|
||||||
|
## 校验下载文件
|
||||||
|
|
||||||
|
建议下载后校验文件完整性(`sha256` 文件内容即对应压缩包/ jar 的 SHA-256 值)。
|
||||||
|
|
||||||
|
Linux:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sha256sum -c Ledgerly-linux-x64.tar.gz.sha256
|
||||||
|
```
|
||||||
|
|
||||||
|
Windows PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-FileHash -Algorithm SHA256 .\Ledgerly-windows-x64.zip
|
||||||
|
# 与 .sha256 文件中的值比对
|
||||||
|
```
|
||||||
|
|
||||||
|
## 运行目录与数据
|
||||||
|
|
||||||
|
- 配置、日志、数据默认落在程序所在目录;可用 `--home=<目录>` 参数或环境变量 `LEDGERLY_HOME` 指定基准目录。
|
||||||
|
- 首次访问时,页面会引导选择 **SQLite**(本地单文件,零配置)或 **MySQL**,保存后自动建表。
|
||||||
|
|
||||||
|
> 更多安装与使用说明见[快速开始](/guide/getting-started);自行构建与部署见[构建与部署](/dev/build-deploy)。
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
行操作:
|
行操作:
|
||||||
|
|
||||||
- **测试连接**:验证 IMAP 配置。
|
- **测试连接**:验证 IMAP 配置。
|
||||||
- **立即检查**:实时展示检查进度,完成后提示新增待处理账单数量。
|
- **立即检查**:实时展示检查进度(每行带 `HH:mm:ss` 时间戳),完成后提示新增待处理账单数量。
|
||||||
- **编辑 / 删除**。
|
- **编辑 / 删除**。
|
||||||
|
|
||||||
## 发件人规则
|
## 发件人规则
|
||||||
|
|||||||
+4
-4
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
### 支持哪些账单?
|
### 支持哪些账单?
|
||||||
|
|
||||||
支付宝、微信、美团、招商银行、交通银行,详见[账单类型与获取](/guide/bills/overview)。
|
支付宝、微信、美团、淘宝、京东、招商银行、交通银行,详见[账单类型与获取](/guide/bills/overview)。
|
||||||
|
|
||||||
### 导入后重复了怎么办?
|
### 导入后重复了怎么办?
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
|
|
||||||
### 想让备注不显示在界面上?
|
### 想让备注不显示在界面上?
|
||||||
|
|
||||||
可在记录行使用「隐藏」功能,备注会显示为 `******`,需要时再「查看」或「取消隐藏」。
|
可在记录行使用「隐藏」功能,备注会显示为 `******`,需要时再「查看」或「取消隐藏」。默认这些记录会从收支记录列表与 Excel 导出中排除;如需让其显示,可在「设置 → 外观设置」开启「显示已隐藏备注的记录」。
|
||||||
|
|
||||||
### 分类和标签有什么区别?
|
### 分类和标签有什么区别?
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
|
|
||||||
### AI 功能不可用?
|
### AI 功能不可用?
|
||||||
|
|
||||||
请先在「系统设置 → AI 模型配置」中开启并填写 API Key,然后点「测试连接」验证。
|
请先在「设置 → AI 模型配置」中开启并填写 API Key,然后点「测试连接」验证。
|
||||||
|
|
||||||
### 邮箱账单会自动入库吗?
|
### 邮箱账单会自动入库吗?
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
|
|
||||||
### 忘记密码怎么办?
|
### 忘记密码怎么办?
|
||||||
|
|
||||||
当前版本未提供自助找回。可在数据库中重置对应用户的密码哈希,或新建账号后迁移数据。
|
登录后可在「设置 → 账号安全」中,凭**原密码**修改密码(也支持一键随机生成新密码)。若已完全忘记密码,当前版本暂未提供自助找回,可在数据库中重置该用户的密码哈希,或新建账号后迁移数据。
|
||||||
|
|
||||||
### 数据存在哪里?
|
### 数据存在哪里?
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
首次进入时按引导选择数据库类型并填写连接信息,点击「保存并应用」即可。登录后可通过右上角用户菜单 →「数据库设置」随时切换数据库;切换后原数据库的数据保留,不做自动迁移。
|
首次进入时按引导选择数据库类型并填写连接信息,点击「保存并应用」即可。登录后可通过右上角用户菜单 →「设置」→「数据库配置」随时切换数据库;切换后原数据库的数据保留,不做自动迁移。
|
||||||
|
|
||||||
## 注册与登录
|
## 注册与登录
|
||||||
|
|
||||||
@@ -44,22 +44,33 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
注册功能可在「系统设置 → 注册设置」中开启或关闭。新注册用户会自动获得一套默认分类、标签与邮箱发件人规则。
|
注册功能可在「设置 → 注册设置」中开启或关闭。新注册用户会自动获得一套默认分类、标签与邮箱发件人规则。
|
||||||
|
|
||||||
## 界面语言
|
## 界面语言
|
||||||
|
|
||||||
系统支持**简体中文、English、日本語**三种语言。登录页、注册页右上角以及主界面顶部栏均可切换语言。
|
系统支持**简体中文、English、日本語**三种语言。登录页、注册页右上角以及主界面顶部栏均可切换语言。
|
||||||
|
|
||||||
|
## 小屏与移动端适配
|
||||||
|
|
||||||
|
系统已适配手机、平板等小屏设备,直接用浏览器访问部署地址即可:
|
||||||
|
|
||||||
|
- **布局**:小屏下左侧导航收起为抽屉,点击顶部菜单按钮展开,切回桌面宽度自动收起。
|
||||||
|
- **表格**:收支记录等表格在窄屏支持**横向滚动**,不会挤压列内容。
|
||||||
|
- **弹窗与浮窗**:宽度自动适配视口,AI 搜索等浮窗可拖拽缩放。
|
||||||
|
- **表单与图表**:表单标签在小屏切换为顶部显示,图表高度随屏宽下调。
|
||||||
|
|
||||||
## 首次使用建议
|
## 首次使用建议
|
||||||
|
|
||||||
1. 完成数据库配置并注册登录。
|
1. 完成数据库配置并注册登录。登录后默认进入[日历](/guide/calendar),可先熟悉按天查看收支的用法。
|
||||||
2. 进入「分类管理」「标签管理」按自己的习惯调整分类与标签。
|
2. 进入「分类管理」「标签管理」按自己的习惯调整分类与标签。
|
||||||
3. 在「规则管理」配置常用自动归类规则(如「星巴克 → 餐饮」)。
|
3. 在「规则管理」配置常用自动归类规则(如「星巴克 → 餐饮」)。
|
||||||
4. 导出并导入历史账单(见[账单类型与获取](/guide/bills/overview))。
|
4. 导出并导入历史账单(见[账单类型与获取](/guide/bills/overview))。
|
||||||
5. 按需配置「AI 模型」「定时通知」「邮箱账单」。
|
5. 在「设置」中按需配置「AI 模型」「定时通知」「邮箱账单」,并可调整「外观设置」与「导航设置」。
|
||||||
|
6. 启用 AI 后,可用左侧「智能查数」以自然语言查询账本。
|
||||||
|
|
||||||
## 下一步
|
## 下一步
|
||||||
|
|
||||||
|
- [日历](/guide/calendar):登录后的默认首页,按天查看每日收支
|
||||||
- [统计面板](/guide/dashboard)
|
- [统计面板](/guide/dashboard)
|
||||||
- [收支记录](/guide/transactions)
|
- [收支记录](/guide/transactions)
|
||||||
- [账单导入](/guide/transactions)
|
- [账单导入](/guide/transactions)
|
||||||
|
|||||||
+9
-4
@@ -6,24 +6,29 @@
|
|||||||
|
|
||||||
- [项目简介](/guide/introduction):系统定位、核心功能与技术栈
|
- [项目简介](/guide/introduction):系统定位、核心功能与技术栈
|
||||||
- [快速开始](/guide/getting-started):数据库配置、注册登录、界面语言与首次使用建议
|
- [快速开始](/guide/getting-started):数据库配置、注册登录、界面语言与首次使用建议
|
||||||
|
- [下载与安装](/guide/download):Windows / Linux 安装包与可执行 jar 的下载、运行与校验
|
||||||
|
- [日历](/guide/calendar):按天查看每日收支、月视图与年视图、当日明细
|
||||||
- [统计面板](/guide/dashboard):时间范围、筛选条件与图表说明
|
- [统计面板](/guide/dashboard):时间范围、筛选条件与图表说明
|
||||||
|
|
||||||
## 记账与整理
|
## 记账与整理
|
||||||
|
|
||||||
- [收支记录](/guide/transactions):记录管理、筛选、批量操作、账单导入、AI 建议与记账
|
- [收支记录](/guide/transactions):记录管理、筛选、批量操作、账单导入、AI 建议与记账
|
||||||
|
- [账单关系](/guide/relations):父子/同级关系、关系图与 AI 生成关系名称
|
||||||
- [分类与标签](/guide/categories-tags):分类与标签的维护与使用建议
|
- [分类与标签](/guide/categories-tags):分类与标签的维护与使用建议
|
||||||
- [规则管理](/guide/rules):按关键词/金额/类型自动归类打标,支持 AI 生成规则、搜索与分页
|
- [交易渠道管理](/guide/channels):内置渠道与自定义渠道的新建、改名与删除
|
||||||
|
- [规则管理](/guide/rules):按关键词/金额/类型自动归类打标,支持 AI 生成规则、搜索、分页与批量删除
|
||||||
- [邮箱账单](/guide/email-bills):IMAP 自动拉取账单邮件
|
- [邮箱账单](/guide/email-bills):IMAP 自动拉取账单邮件
|
||||||
|
|
||||||
## 智能与自动化
|
## 智能与自动化
|
||||||
|
|
||||||
- [AI 能力](/guide/ai):分类标签建议、自然语言查询与记账、AI 搜索与规则生成、用量统计
|
- [AI 能力](/guide/ai):分类标签与渠道建议、自然语言查询与记账、多图图片记账(实时进度)、AI 搜索与规则生成、关系命名、用量统计
|
||||||
|
- [智能查数](/guide/sql-query):用自然语言生成只读 SQL 查询账本,结果支持卡片/表格/图表
|
||||||
- [定时提醒与通知](/guide/notifications):多渠道账单导入提醒
|
- [定时提醒与通知](/guide/notifications):多渠道账单导入提醒
|
||||||
|
|
||||||
## 系统
|
## 系统
|
||||||
|
|
||||||
- [系统设置](/guide/settings):数据库、注册、账单来源统计、外观、AI、通知配置
|
- [设置](/guide/settings):账号安全、数据库、注册、账单来源统计、外观、导航、AI、通知配置
|
||||||
- [系统日志](/guide/logs):访问日志、登录日志、运行日志
|
- [日志](/guide/logs):访问日志、登录日志、运行日志
|
||||||
|
|
||||||
## 账单获取
|
## 账单获取
|
||||||
|
|
||||||
|
|||||||
+10
-7
@@ -1,6 +1,6 @@
|
|||||||
# 项目简介
|
# 项目简介
|
||||||
|
|
||||||
收支管理系统是一个**自托管的个人收支管理应用**,用于把分散在支付宝、微信、美团、银行等平台的账单汇总到一处,统一分类、打标签、统计收支,并通过自定义规则与 AI 能力减少手工整理的工作量。
|
汇账是一个**自托管的个人收支管理应用**,用于把分散在支付宝、微信、美团、银行等平台的账单汇总到一处,统一分类、打标签、统计收支,并通过自定义规则与 AI 能力减少手工整理的工作量。
|
||||||
|
|
||||||
## 解决什么问题
|
## 解决什么问题
|
||||||
|
|
||||||
@@ -17,13 +17,16 @@
|
|||||||
|---|---|
|
|---|---|
|
||||||
| 收支记录 | 增删改查、分页与多条件筛选、批量设置分类/标签、批量删除、计入统计开关、备注隐藏、导出 Excel |
|
| 收支记录 | 增删改查、分页与多条件筛选、批量设置分类/标签、批量删除、计入统计开关、备注隐藏、导出 Excel |
|
||||||
| 分类与标签 | 收入/支出分类与标签的增删改查,新用户自动获得默认数据,AI 推荐 Emoji 与颜色 |
|
| 分类与标签 | 收入/支出分类与标签的增删改查,新用户自动获得默认数据,AI 推荐 Emoji 与颜色 |
|
||||||
| 统计图表 | 月度收支、分类占比、收支趋势、交易渠道占比,支持按日/周/月/季/年粒度,点击趋势图可下钻对应明细 |
|
| 交易渠道 | 内置渠道 + 自定义渠道的新建/改名/删除,改名不影响历史账单 |
|
||||||
|
| 日历 | 默认首页,月视图按天展示每日收支与当月合计,年视图一览全年各月,支持统计口径过滤与当日明细下钻 |
|
||||||
|
| 统计图表 | 月度收支、分类占比、收支趋势、交易渠道占比,支持按日/周/月/季/年粒度,图表可点击下钻对应明细 |
|
||||||
|
| 账单关系 | 父子/同级多笔账单关联、自动绘制关系图,AI 生成关系名称与备注 |
|
||||||
| 账单导入 | 七类账单解析、zip 解密、解析预览、按交易号去重、规则建议 |
|
| 账单导入 | 七类账单解析、zip 解密、解析预览、按交易号去重、规则建议 |
|
||||||
| 自定义规则 | 关键词/收支类型/金额区间组合,优先级排序、启用停用,导入时自动应用;支持 AI 生成规则、搜索与分页 |
|
| 自定义规则 | 关键词/收支类型/金额区间组合,优先级排序、启用停用,导入时自动应用;支持 AI 生成规则、搜索、分页与批量删除 |
|
||||||
| AI 能力 | 分类标签建议、自然语言查询、自然语言记账、AI 统计搜索、AI 生成规则、分类/标签生成、Emoji/颜色推荐、用量统计 |
|
| AI 能力 | 分类标签与渠道建议、自然语言查询、智能查数(自然语言转只读 SQL)、文字/多图图片记账、AI 统计搜索、AI 生成规则、分类/标签/渠道生成、关系命名、Emoji/颜色推荐、用量统计 |
|
||||||
| 邮箱账单 | 多邮箱 IMAP IDLE 监听、发件人规则识别账单类型、待处理列表先预览再导入 |
|
| 邮箱账单 | 多邮箱 IMAP IDLE 监听、发件人规则识别账单类型、待处理列表先预览再导入 |
|
||||||
| 定时通知 | 每天/每周/每月提醒,邮箱 SMTP 与 Server 酱渠道,发送日志 |
|
| 定时通知 | 每天/每周/每月提醒,邮箱 SMTP、Server 酱与飞书渠道,发送日志 |
|
||||||
| 系统日志 | 访问日志、登录日志、运行日志在线查看,运行日志实时推送,自动清理 |
|
| 日志 | 访问日志、登录日志、运行日志在线查看,运行日志实时推送,自动清理 |
|
||||||
| 账号与多语言 | JWT 登录、注册开关、运行时数据库配置(SQLite/MySQL)、中/英/日三语 |
|
| 账号与多语言 | JWT 登录、注册开关、运行时数据库配置(SQLite/MySQL)、中/英/日三语 |
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
@@ -37,7 +40,7 @@
|
|||||||
| 对象映射 | MapStruct 1.6.3 |
|
| 对象映射 | MapStruct 1.6.3 |
|
||||||
| 账单解析 | EasyExcel 4.0.3 + PDFBox 3.0.3 + Zip4j 2.11.5 |
|
| 账单解析 | EasyExcel 4.0.3 + PDFBox 3.0.3 + Zip4j 2.11.5 |
|
||||||
| AI 能力 | DeepSeek |
|
| AI 能力 | DeepSeek |
|
||||||
| 通知渠道 | 邮箱 SMTP / Server 酱 |
|
| 通知渠道 | 邮箱 SMTP / Server 酱 / 飞书 |
|
||||||
| 多语言 | 中文 / 英文 / 日文(vue-i18n) |
|
| 多语言 | 中文 / 英文 / 日文(vue-i18n) |
|
||||||
| API 文档 | Knife4j(OpenAPI 3) |
|
| API 文档 | Knife4j(OpenAPI 3) |
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
# 系统日志
|
# 日志
|
||||||
|
|
||||||
系统日志通过右上角用户菜单 →「系统日志」进入,包含三个标签页:**访问日志**、**登录日志**、**运行日志**。页头右上角可选择日志保留时长(30 天 / 90 天 / 180 天 / 永久保留)。
|
日志通过右上角用户菜单 →「日志」进入,包含三个标签页:**访问日志**、**登录日志**、**运行日志**。页头右上角可选择日志保留时长(30 天 / 90 天 / 180 天 / 永久保留)。
|
||||||
|
|
||||||
## 访问日志
|
## 访问日志
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# 定时提醒与通知
|
# 定时提醒与通知
|
||||||
|
|
||||||
系统可按周期提醒你导入新账单,并通过邮箱或 Server 酱推送通知。配置入口为「系统设置 → 定时通知」。
|
系统可按周期提醒你导入新账单,并通过邮箱、Server 酱或飞书推送通知。配置入口为「设置 → 定时通知」。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 提醒周期
|
## 提醒周期
|
||||||
|
|
||||||
@@ -13,15 +13,20 @@
|
|||||||
|
|
||||||
## 推送渠道
|
## 推送渠道
|
||||||
|
|
||||||
点击「添加渠道」新增,支持两种类型:
|
点击「添加渠道」新增,支持三种类型:
|
||||||
|
|
||||||
| 渠道 | 配置项 |
|
| 渠道 | 配置项 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Server 酱 | SendKey(在 sct.ftqq.com 获取),可推送到个人微信 |
|
| Server 酱 | SendKey(在 sct.ftqq.com 获取),可推送到个人微信 |
|
||||||
| 邮箱 | SMTP 服务器、端口、邮箱账号、授权码、收件人、是否使用 SSL |
|
| 邮箱 | SMTP 服务器、端口、邮箱账号、授权码、收件人、是否使用 SSL |
|
||||||
|
| 飞书 | Webhook 完整地址(飞书群「设置 → 群机器人 → 添加机器人 → 自定义机器人」获取);若机器人开启了「签名校验」,再填写签名密钥,可推送到飞书群 |
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
飞书渠道配置示例(类型选择「飞书」,填写 Webhook 地址与可选的签名密钥):
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
每个渠道可单独「测试」「编辑」「删除」,并可通过启用开关控制是否参与发送。
|
每个渠道可单独「测试」「编辑」「删除」,并可通过启用开关控制是否参与发送。
|
||||||
|
|
||||||
面板右上角的「立即发送」可向所有启用渠道立即发送一次提醒,用于验证配置。
|
面板右上角的「立即发送」可向所有启用渠道立即发送一次提醒,用于验证配置。
|
||||||
|
|||||||
+29
-13
@@ -9,47 +9,63 @@
|
|||||||
| 登录 | `/login` | 用户名密码登录 | 未登录访问受保护页面时自动跳转 |
|
| 登录 | `/login` | 用户名密码登录 | 未登录访问受保护页面时自动跳转 |
|
||||||
| 注册 | `/register` | 创建账号 | 登录页「没有账号?去注册」(注册开启时) |
|
| 注册 | `/register` | 创建账号 | 登录页「没有账号?去注册」(注册开启时) |
|
||||||
| 数据库初始化 | `/database-setup` | 首次配置数据库 | 数据库未配置时自动引导 |
|
| 数据库初始化 | `/database-setup` | 首次配置数据库 | 数据库未配置时自动引导 |
|
||||||
| 统计面板 | `/dashboard` | 收支汇总与图表 | 登录后默认页;左侧菜单 |
|
| 日历 | `/calendar` | 每日收支日历、月/年视图与当日明细 | **登录后默认页**;左侧菜单(默认置顶) |
|
||||||
|
| 统计面板 | `/dashboard` | 收支汇总与图表 | 左侧菜单 |
|
||||||
|
| 智能查数 | `/smart-query` | 自然语言转只读 SQL 查询 | 左侧菜单(**AI 启用时**) |
|
||||||
| 收支记录 | `/transactions` | 记录管理与**账单导入** | 左侧菜单 |
|
| 收支记录 | `/transactions` | 记录管理与**账单导入** | 左侧菜单 |
|
||||||
| 分类管理 | `/categories` | 收入/支出分类维护 | 左侧菜单 |
|
| 分类管理 | `/categories` | 收入/支出分类维护 | 左侧菜单 |
|
||||||
| 标签管理 | `/tags` | 标签维护 | 左侧菜单 |
|
| 标签管理 | `/tags` | 标签维护 | 左侧菜单 |
|
||||||
|
| 交易渠道管理 | `/channels` | 自定义交易渠道维护 | 收支记录页「渠道管理」按钮 |
|
||||||
| 规则管理 | `/rules` | 自动归类规则 | 左侧菜单 |
|
| 规则管理 | `/rules` | 自动归类规则 | 左侧菜单 |
|
||||||
| 邮箱账单 | `/email-bills/*` | 邮箱配置 / 发件人规则 / 待处理账单 | 左侧菜单 |
|
| 邮箱账单 | `/email-bills/*` | 邮箱配置 / 发件人规则 / 待处理账单 | 左侧菜单 |
|
||||||
| 系统设置 | `/settings` | 数据库 / 注册 / 账单来源统计 / 外观 / AI / 通知配置 | 顶部用户菜单 |
|
| 设置 | `/settings` | 账号安全 / 数据库 / 注册 / 账单来源统计 / 外观 / 导航 / AI / 通知配置 | 顶部用户菜单 |
|
||||||
| 模型使用情况 | `/ai-usage` | AI 调用统计与明细 | 设置页「AI 模型配置」→ 使用情况 |
|
| 模型使用情况 | `/ai-usage` | AI 调用统计与明细 | 设置页「AI 模型配置」→ 使用情况 |
|
||||||
| 系统日志 | `/logs/*` | 访问 / 登录 / 运行日志 | 顶部用户菜单 |
|
| 日志 | `/logs/*` | 访问 / 登录 / 运行日志 | 顶部用户菜单 |
|
||||||
| 账单导入(独立页) | `/bill-import` | 手机端上传账单 | **仅由通知邮件链接直达** |
|
| 账单导入(独立页) | `/bill-import` | 手机端上传账单 | **仅由通知邮件链接直达** |
|
||||||
|
|
||||||
|
> 「账单关系」不是独立页面,它是收支记录内的弹窗:由列表「关系」列或「批量操作 → 建立关系」进入,详见[账单关系](/guide/relations)。
|
||||||
|
|
||||||
## 全局导航
|
## 全局导航
|
||||||
|
|
||||||
### 左侧菜单
|
### 左侧菜单
|
||||||
|
|
||||||
统计面板 → 收支记录 → 分类管理 → 标签管理 → 规则管理 → 邮箱账单
|
日历 → 统计面板 → 智能查数(AI 启用时)→ 收支记录 → 分类管理 → 标签管理 → 规则管理 → 邮箱账单
|
||||||
|
|
||||||
|
左侧菜单的**顺序与显隐可自定义**(设置 → 导航设置),并在下次进入时还原。
|
||||||
|
|
||||||
### 顶部用户菜单
|
### 顶部用户菜单
|
||||||
|
|
||||||
- **系统日志**:`/logs`
|
- **日志**:`/logs`
|
||||||
- **系统设置**:`/settings`
|
- **设置**:`/settings`
|
||||||
- **退出**:清空登录态并返回 `/login`
|
- **退出**:清空登录态并返回 `/login`
|
||||||
|
|
||||||
> 「模型使用情况」没有独立菜单入口,需从「系统设置 → AI 模型配置 → 使用情况」进入。
|
> 「模型使用情况」没有独立菜单入口,需从「设置 → AI 模型配置 → 使用情况」进入。
|
||||||
|
>
|
||||||
|
> 「交易渠道管理」同样没有独立菜单入口,需从「收支记录 → 渠道管理」进入。
|
||||||
|
|
||||||
## 典型跳转路径
|
## 典型跳转路径
|
||||||
|
|
||||||
- **首次使用**:`/database-setup` → `/register` → `/login` → `/dashboard`
|
- **首次使用**:`/database-setup` → `/register` → `/login` → `/calendar`
|
||||||
|
- **日历看某天**:日历 → 点击某天 → 当日明细浮窗(按当前统计口径)→ 点条目查看账单详情 / 点「在收支记录中查看」跳转收支记录(按当日筛选并沿用统计口径)
|
||||||
- **手动记账**:收支记录 → 点击「新增记录」→ 表单弹窗 → 保存
|
- **手动记账**:收支记录 → 点击「新增记录」→ 表单弹窗 → 保存
|
||||||
- **查看趋势明细**:统计面板 → 点击「收支趋势」某一列(柱子或该列空白处)→ 收支记录(自动带入时间范围、类型与当前筛选条件)
|
- **查看趋势明细**:统计面板 → 点击趋势柱、分类/渠道饼图或「查看收支记录」→ 收支记录(自动带入对应筛选条件)
|
||||||
|
- **反向跳转**:收支记录 → 点击「查看统计」→ 统计面板(还原当前筛选条件)
|
||||||
- **账单导入**:收支记录 → 点击「导入账单」→ 解析预览 → 确认导入 → 回到收支记录
|
- **账单导入**:收支记录 → 点击「导入账单」→ 解析预览 → 确认导入 → 回到收支记录
|
||||||
- **手机导入**:通知邮件链接 → `/bill-import`(独立页,非弹窗)→ 上传解析 → 确认导入
|
- **手机导入**:通知邮件链接 → `/bill-import`(独立页,非弹窗)→ 上传解析 → 确认导入
|
||||||
|
- **渠道维护**:收支记录 → 点击「渠道管理」→ 新建 / 改名 / 删除自定义渠道 → 返回收支记录
|
||||||
|
- **账单关系**:收支记录「关系」列 → 关系弹窗;或「批量操作」勾选多条 → 建立关系
|
||||||
|
- **智能查数**:左侧菜单「智能查数」→ 输入自然语言 → 查看结果、生成的 SQL 与历史提问
|
||||||
- **自动归类**:规则管理配置规则 → 账单导入预览时命中并给出建议
|
- **自动归类**:规则管理配置规则 → 账单导入预览时命中并给出建议
|
||||||
- **邮箱账单**:邮箱配置 → 发件人规则 → 待处理账单 → 解析 → 导入到收支记录
|
- **邮箱账单**:邮箱配置 → 发件人规则 → 待处理账单 → 解析 → 导入到收支记录
|
||||||
- **AI 辅助**:系统设置启用 AI → 统计面板「AI 搜索」、收支记录「一键设置 / 智能记账 / AI 查询」、分类与标签「AI 推荐」、规则管理「AI 生成规则」
|
- **AI 辅助**:设置启用 AI → 统计面板「AI 搜索」、收支记录「一键设置 / 文字·图片记账 / AI 查询」、编辑记录「分类·标签·渠道 AI 推荐与新建」、分类与标签「AI 推荐」、规则管理「AI 生成规则」、收支记录批量操作「AI 生成规则」、账单关系「AI 生成」、左侧「智能查数」
|
||||||
- **系统维护**:顶部菜单 → 系统设置 / 系统日志
|
- **系统维护**:顶部菜单 → 设置 / 日志
|
||||||
|
|
||||||
## 页面关系说明
|
## 页面关系说明
|
||||||
|
|
||||||
- **收支记录**是核心页面:手动记账、账单导入、批量设置、AI 能力都从这里进入,导入完成后也回到该页查看。
|
- **日历**是登录后的默认落地页:按天查看每日收支、切换月/年视图与统计口径,并可点击某天下钻到当日明细与账单详情。
|
||||||
|
- **收支记录**是核心页面:手动记账、账单导入、批量操作、账单关系与 AI 能力都从这里进入,导入完成后也回到该页查看。
|
||||||
- **账单导入**有两条独立路径:
|
- **账单导入**有两条独立路径:
|
||||||
- 收支记录页的「导入账单」**弹窗**(桌面端常用);
|
- 收支记录页的「导入账单」**弹窗**(桌面端常用);
|
||||||
- 通知邮件直达的 **`/bill-import` 独立页**(面向手机,非弹窗,元素精简,系统内无其他入口)。
|
- 通知邮件直达的 **`/bill-import` 独立页**(面向手机,非弹窗,元素精简,系统内无其他入口)。
|
||||||
- **系统设置**聚合了数据库、注册、账单来源统计、外观、AI、通知六类配置;模型用量与日志分别从设置页与顶部菜单进入。
|
- **设置**聚合了账号安全、数据库、注册、账单来源统计、外观、导航、AI、通知八类配置;模型用量与日志分别从设置页与顶部菜单进入。
|
||||||
|
- **交易渠道管理**是收支记录页的附属页;**账单关系**是收支记录内的弹窗,两者都在整理完成后回到收支记录继续操作。
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# 账单关系
|
||||||
|
|
||||||
|
「账单关系」用于把**互相关联的多笔收支**组合在一起,例如「交通卡充值」与后续的乘车扣款、分期还款的每一期、支付链路上的多笔记录。建立关系只做关联,不会改动金额,也不影响统计。
|
||||||
|
|
||||||
|
## 两种关系类型
|
||||||
|
|
||||||
|
| 类型 | 含义 | 典型场景 |
|
||||||
|
|---|---|---|
|
||||||
|
| **父子关系** | 一个「父账单」(资金池)对应多个「子账单」(从资金池流出) | 交通卡充值 → 多次乘车;账户充值 → 逐笔消费 |
|
||||||
|
| **同级关系** | 多笔地位平等的账单聚合在一起 | 一笔消费在多个平台留下的关联记录、分次归还 |
|
||||||
|
|
||||||
|
- 父子关系中,**必须且只能有一条父账单**,其余为子账单。
|
||||||
|
- 同级关系中,所有成员地位相同,按加入顺序依次连接。
|
||||||
|
|
||||||
|
## 入口
|
||||||
|
|
||||||
|
| 入口 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| 收支记录列表「**关系**」列 | 该行所属关系组数量,点击「N 个关系」打开关系管理弹窗 |
|
||||||
|
| 收支记录列表右上角「**批量操作**」 | 进入多选模式,勾选**至少两条**后点「建立关系」,直接新建关系组 |
|
||||||
|
| 账单**详情**弹窗 | 「账单关系」分组内查看该笔账单的全部关系 |
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 新建与编辑关系
|
||||||
|
|
||||||
|
1. 选择**关系类型**(父子关系 / 同级关系)。
|
||||||
|
2. 填写**关系名称**(可选,如「交通卡充值」)与**备注**。
|
||||||
|
3. 点「添加账单」,在分页选择弹窗中勾选要关联的账单(支持备注搜索)。
|
||||||
|
4. 父子关系下,用单选指定其中一条为**父账单**(从批量操作进入时,默认以被操作的那条为父账单,且不可移除);同级关系无需指定,成员按列表顺序排列。
|
||||||
|
5. 点击「保存」。
|
||||||
|
|
||||||
|
约束与说明:
|
||||||
|
|
||||||
|
- 一个关系组**至少包含两条账单**,成员上限 50 条。
|
||||||
|
- **一笔账单不能同时加入两个同类型的关系组**;如需更换,请先在原关系组中解除。
|
||||||
|
- **关系名称会记住历史**:再次新建同名类型的关系时,名称输入框会自动给出历史名称建议(按最近使用、去重,最多 50 条)。
|
||||||
|
- 关系类型会被**记忆**:下次新建时默认沿用上次选择的类型。
|
||||||
|
|
||||||
|
## 关系图
|
||||||
|
|
||||||
|
关系管理弹窗中,每个关系组都会以自绘的关系图展示:
|
||||||
|
|
||||||
|
- 节点为圆形,**支出为红色、收入为绿色**;当前正在查看的账单以蓝色描边且节点更大。
|
||||||
|
- **父子关系**:父账单居中在上,子账单分行排列,父 → 子连线带箭头。
|
||||||
|
- **同级关系**:成员按顺序链式连接,边上标注「关联」。
|
||||||
|
- 节点内显示金额,节点下方显示备注或分类;鼠标悬停可查看完整信息。成员较多时自动换行,图形自适应宽度。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## AI 生成名称与备注
|
||||||
|
|
||||||
|
启用 AI 后,新建/编辑关系弹窗中会出现「**AI 生成**」按钮。系统会把成员账单的**收支类型、交易渠道、分类名称与备注**发送给模型(**不包含金额**),由其归纳出一个关系名称与简短备注,并自动填入;你可以在保存前修改。
|
||||||
|
|
||||||
|
> 成员少于两条时无法生成,会提示先添加账单。生成的名称会覆盖名称框(备注仅在返回非空时覆盖)。
|
||||||
|
|
||||||
|
## 删除关系
|
||||||
|
|
||||||
|
在关系组卡片上点「删除」,确认后**仅解除关联**,不会删除任何账单。删除单笔账单时,系统会自动把它从所属关系组中移除;若某个关系组剩余成员不足两条、或父子关系不再有父账单,该关系组会自动解散。
|
||||||
|
|
||||||
|
## 与统计的关系
|
||||||
|
|
||||||
|
账单关系只用于**查看与整理**,不参与统计口径。每笔账单是否计入统计,仍由各自的「计入统计」开关决定,详见[收支记录](/guide/transactions)。
|
||||||
|
|
||||||
|
## 相关
|
||||||
|
|
||||||
|
- [收支记录](/guide/transactions):记录管理与批量操作。
|
||||||
|
- [AI 能力](/guide/ai):AI 生成关系名称、分类标签建议等。
|
||||||
+30
-2
@@ -52,6 +52,32 @@
|
|||||||
|
|
||||||
点击「搜索」按条件查询,「清空」重置全部条件。新增规则后回到第一页,便于查看新规则。
|
点击「搜索」按条件查询,「清空」重置全部条件。新增规则后回到第一页,便于查看新规则。
|
||||||
|
|
||||||
|
## 多选与批量删除
|
||||||
|
|
||||||
|
为保持列表清爽,规则表格默认不显示勾选框。需要进行批量操作时,点右上角「**批量操作**」进入批量选择模式,此时每行前会出现勾选框,表头可**全选当前页**;完成后点「**退出批量**」退出并清空勾选。
|
||||||
|
|
||||||
|
- 勾选若干规则后,点右上角「**批量删除**」并确认,即可一次性删除选中的规则。
|
||||||
|
- 未勾选任何规则时「批量删除」不可点击。
|
||||||
|
|
||||||
|
> 删除规则只影响后续导入与手动应用,已应用的历史记录分类与标签不会被撤销。
|
||||||
|
|
||||||
|
## 一键应用到匹配账单
|
||||||
|
|
||||||
|
规则默认只在**账单导入**时对新增记录生效,不会自动改写已有记录。如需把规则应用到**历史账单**,可在规则列表使用「应用」:
|
||||||
|
|
||||||
|
1. **单条规则**:点击规则行的「**应用**」按钮。
|
||||||
|
2. **多条规则**:点右上角「**批量操作**」进入批量选择模式,勾选若干规则后点「**应用匹配账单**」。
|
||||||
|
|
||||||
|
两种方式都会先弹出**预览**:按规则条件(类型 + 关键词 + 金额区间)列出全部命中的账单,并显示每条账单的**当前分类/标签 → 目标分类/标签**,以及命中总数、将更新分类数与将追加标签数;确认后点「**确认应用**」执行。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- **分类**:覆盖写入(仅当规则分类与账单收支类型一致时)。
|
||||||
|
- **标签**:追加到账单已有标签之后,**不会删除**原有标签。
|
||||||
|
- 匹配范围为**全部历史账单**;**停用的规则**也可手动应用(应用后再启用不影响本次结果)。
|
||||||
|
- 命中超过 500 条时预览仅展示前 500 条,应用仍作用于全部命中;重复执行不会产生重复标签(幂等)。
|
||||||
|
- 全程本地运算,不调用 AI。
|
||||||
|
|
||||||
## AI 生成规则
|
## AI 生成规则
|
||||||
|
|
||||||
启用 AI 后,规则管理页右上角会出现「**AI 生成规则**」按钮,可让 AI 依据**已打标签的记录**自动归纳出「关键词 → 分类/标签」规则,无需手工逐条编写。
|
启用 AI 后,规则管理页右上角会出现「**AI 生成规则**」按钮,可让 AI 依据**已打标签的记录**自动归纳出「关键词 → 分类/标签」规则,无需手工逐条编写。
|
||||||
@@ -62,12 +88,14 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
2. **预览规则**:AI 依据所选记录的**类型、备注、交易渠道、已有分类与标签**归纳出规则建议,展示规则名称、关键词、目标分类与标签、理由。名称与关键词可编辑,勾选后点击「创建勾选规则」批量创建;系统会自动创建建议中不存在的新分类与标签。
|
2. **预览与编辑规则**:AI 依据所选记录的**类型、备注、交易渠道、已有分类与标签**归纳出规则建议,展示规则名称、类型、关键词、金额区间、目标分类与标签、理由。所有字段都可**直接编辑**(名称、类型、关键词、金额区间、分类、标签),AI 建议的新分类/新标签以下拉里的「**新建:xxx**」选项保留;勾选后点击「创建勾选规则」批量创建,系统会自动创建建议中不存在的新分类与标签。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
> 此外,也可在[收支记录页的批量操作区](/guide/transactions#ai-生成规则从勾选账单)勾选账单后直接生成规则(不要求已打标签),并可选「同时应用到勾选账单」(默认开启,会记住上次选择)。
|
||||||
|
|
||||||
::: tip 与已有规则去重
|
::: tip 与已有规则去重
|
||||||
AI 建议若与**数据库中已有规则重复**(按「类型 + 关键词 + 分类 + 标签集合」判定),会标注「**已存在**」,**默认不勾选且不可勾选**,无法导入,避免产生重复规则。
|
AI 建议若与**数据库中已有规则重复**(按「类型 + 关键词 + 分类 + 标签集合」判定),会标注「**已存在**」,默认不勾选且不可勾选。**编辑规则后会自动重新判定**:改成不重复会自动勾选,改成重复会自动取消勾选并禁用,避免产生重复规则。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## 示例
|
## 示例
|
||||||
|
|||||||
+53
-7
@@ -1,8 +1,20 @@
|
|||||||
# 系统设置
|
# 设置
|
||||||
|
|
||||||
系统设置通过右上角用户菜单 →「系统设置」进入,包含六个配置面板。
|
「设置」通过右上角用户菜单 →「设置」进入,包含八个配置面板。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## 账号安全
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
用于**修改当前登录账号的密码**:
|
||||||
|
|
||||||
|
- 输入**原密码**、**新密码**与**确认新密码**,点「修改密码」提交。
|
||||||
|
- 新密码长度需为 **6–50 个字符**,且不能与原密码相同。
|
||||||
|
- 可点「**随机生成**」自动生成一个 12 位、含大小写字母与数字的密码(已排除易混淆字符),生成后会明文显示并可一键**复制**;修改成功后请使用新密码登录。
|
||||||
|
|
||||||
|
> 修改密码仅校验原密码,**不提供**通过邮箱等方式的密码找回。
|
||||||
|
|
||||||
## 数据库配置
|
## 数据库配置
|
||||||
|
|
||||||
@@ -13,6 +25,8 @@
|
|||||||
- **MySQL**:主机、端口、数据库名、用户名、密码。
|
- **MySQL**:主机、端口、数据库名、用户名、密码。
|
||||||
- 提供「测试连接」与「保存并应用」。保存后立即生效并自动建表;已配置时密码留空表示不修改。
|
- 提供「测试连接」与「保存并应用」。保存后立即生效并自动建表;已配置时密码留空表示不修改。
|
||||||
|
|
||||||
|
测试与保存时会实时展示**执行进度**:连接测试、建表、结构迁移、数据源切换等步骤会以进度日志逐条推送,处理较久时请保持窗口打开,完成后点「关闭」即可。
|
||||||
|
|
||||||
系统支持启动后再配置数据库:未配置时前端会引导进入初始化页面,无需启动前准备。登录后也可随时切换数据库,切换后原数据库数据保留、不做自动迁移。
|
系统支持启动后再配置数据库:未配置时前端会引导进入初始化页面,无需启动前准备。登录后也可随时切换数据库,切换后原数据库数据保留、不做自动迁移。
|
||||||
|
|
||||||
## 注册设置
|
## 注册设置
|
||||||
@@ -41,12 +55,43 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
选择「不计入统计」的记录在[收支记录](/guide/transactions)列表中的展示方式:
|
外观设置包含四组配置,**修改后自动保存**,无需再点保存按钮:
|
||||||
|
|
||||||
|
**1. 不计入统计的记录样式**:选择「不计入统计」的记录在[收支记录](/guide/transactions)列表中的展示方式:
|
||||||
|
|
||||||
- **删除线(默认)**:整行加删除线,类型与金额列保持原样。
|
- **删除线(默认)**:整行加删除线,类型与金额列保持原样。
|
||||||
- **灰色标签**:类型列显示灰色「不计入」标签,金额列置灰且不显示正负号。
|
- **灰色标签**:类型列显示灰色「不计入」标签,金额列置灰且不显示正负号。
|
||||||
|
|
||||||
面板内提供**实时预览**,切换选项即可看到效果;该偏好按**当前账号**保存,登录后生效。
|
面板内提供**实时预览**,切换选项即可看到效果。
|
||||||
|
|
||||||
|
**2. 显示已隐藏备注的记录**(默认关闭):
|
||||||
|
|
||||||
|
- 关闭时,备注被隐藏的记录不会出现在收支记录列表与 Excel 导出中。
|
||||||
|
- 开启后,这些记录会正常显示(备注仍为 `******`,可临时「查看」)。
|
||||||
|
|
||||||
|
**3. 图表点击查看明细**(默认开启):
|
||||||
|
|
||||||
|
- 开启后,点击[统计面板](/guide/dashboard)的趋势柱子或饼图扇区可跳转查看对应收支明细。
|
||||||
|
- 关闭后图表仅用于展示:点击不再跳转,趋势图的「点击柱子查看对应明细」提示与饼图的手型光标一并隐藏。
|
||||||
|
- 该开关在统计面板的**筛选条件**区也提供,两处修改实时同步。
|
||||||
|
|
||||||
|
**4. 表格每页条数**:
|
||||||
|
|
||||||
|
- **表格每页条数选项**:所有表格分页共用的候选列表,默认 `10 / 20 / 50 / 100`,可自行增删(最多 10 项)。
|
||||||
|
- **默认每页条数**:从候选中选择,作为各表格未单独记忆时的默认值(默认 20)。
|
||||||
|
- 每张表格会分别记住上次选择的每页条数;本机记忆优先于全局默认值。
|
||||||
|
|
||||||
|
## 导航设置
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
调整左侧导航的**显示顺序与显隐**(按当前账号保存,跨设备生效):
|
||||||
|
|
||||||
|
- **拖动**每项左侧的手柄即可调整顺序,修改后自动保存。
|
||||||
|
- 用开关控制各项的显示或隐藏。
|
||||||
|
- 至少需保留一个可见项;「**恢复默认**」可还原为默认顺序与全部显示。
|
||||||
|
|
||||||
|
> 隐藏只移除菜单入口,页面仍可通过地址直接访问。当 AI 未启用时,依赖 AI 的菜单项(如「智能查数」)不会出现在此处,也不会显示在左侧导航。
|
||||||
|
|
||||||
## AI 模型配置
|
## AI 模型配置
|
||||||
|
|
||||||
@@ -56,6 +101,7 @@
|
|||||||
- **API Key**:DeepSeek 的 API Key(已配置时留空表示不修改)。
|
- **API Key**:DeepSeek 的 API Key(已配置时留空表示不修改)。
|
||||||
- **接口地址**:默认 `https://api.deepseek.com`。
|
- **接口地址**:默认 `https://api.deepseek.com`。
|
||||||
- **模型**:默认 `deepseek-chat`。
|
- **模型**:默认 `deepseek-chat`。
|
||||||
|
- **视觉模型**:默认 `deepseek-flash`,用于[图片记账](/guide/ai#图片记账)。
|
||||||
- 提供「保存」与「测试连接」。
|
- 提供「保存」与「测试连接」。
|
||||||
- 右上角「使用情况」可查看 AI 调用统计与明细,详见 [AI 能力](/guide/ai)。
|
- 右上角「使用情况」可查看 AI 调用统计与明细,详见 [AI 能力](/guide/ai)。
|
||||||
|
|
||||||
@@ -63,9 +109,9 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
配置账单导入提醒的周期、时间与推送渠道(Server 酱 / 邮箱),并可查看发送日志。详见[定时提醒与通知](/guide/notifications)。
|
配置账单导入提醒的周期、时间与推送渠道(Server 酱 / 邮箱 / 飞书),并可查看发送日志。详见[定时提醒与通知](/guide/notifications)。
|
||||||
|
|
||||||
## 相关
|
## 相关
|
||||||
|
|
||||||
- [系统日志](/guide/logs):查看访问、登录与运行日志。
|
- [日志](/guide/logs):查看访问、登录与运行日志。
|
||||||
- [快速开始](/guide/getting-started):数据库配置与注册登录。
|
- [快速开始](/guide/getting-started):数据库配置与注册登录。
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# 智能查数
|
||||||
|
|
||||||
|
「智能查数」让你用**自然语言**直接查询账本,由 AI 生成一条**只读 SQL** 并执行,结果以统计卡片、表格或图表展示,无需自己拼筛选条件。
|
||||||
|
|
||||||
|
> 这是与[统计面板的「AI 搜索」](/guide/dashboard#ai-搜索)不同的能力:AI 搜索是「自然语言 → 统计面板筛选条件」,智能查数则是「自然语言 → SQL 查询」,适合更自由、更灵活的问答。
|
||||||
|
|
||||||
|
## 入口与前提
|
||||||
|
|
||||||
|
- 先在[设置](/guide/settings#ai-模型配置)中**启用 AI** 并配置 API Key。
|
||||||
|
- 启用后,左侧菜单会出现「**智能查数**」(未启用时不显示)。
|
||||||
|
- 直接访问路由 `/smart-query` 也可进入。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 提问
|
||||||
|
|
||||||
|
在页面顶部的输入框用一句自然语言描述想查的内容,例如:
|
||||||
|
|
||||||
|
- 上个月一共花了多少钱
|
||||||
|
- 按分类统计今年的支出
|
||||||
|
- 今年收入最高的一个月
|
||||||
|
- 各渠道的支出占比
|
||||||
|
- 本月支出最高的 5 笔
|
||||||
|
|
||||||
|
输入框支持多行(自动增高),点击「查询」或按 `Ctrl + Enter`(Mac 为 `⌘ + Enter`)提交;页面也会给出示例问题,点击即可直接查询。
|
||||||
|
|
||||||
|
## 结果展示
|
||||||
|
|
||||||
|
系统会根据查询结果自动选择展示方式:
|
||||||
|
|
||||||
|
| 结果形态 | 展示 |
|
||||||
|
|---|---|
|
||||||
|
| 只有一行 | **统计卡片**,逐项展示各列的数值 |
|
||||||
|
| 多行且含数值列 | **动态表格**,可切换为**柱状图**或**饼图** |
|
||||||
|
| 多行但无数值列 | 表格 |
|
||||||
|
|
||||||
|
- 结果中的**渠道**列会自动显示渠道名称(含自定义渠道),而不是内部编码。
|
||||||
|
- 结果最多返回 **1000 行**;超过时会提示「仅展示前 1000 行」。
|
||||||
|
- 无结果的查询会显示空态提示。
|
||||||
|
|
||||||
|
## 生成的 SQL
|
||||||
|
|
||||||
|
查询结果上方会给出 AI 生成的 SQL,默认折叠:
|
||||||
|
|
||||||
|
- 使用 `sql-formatter` 缩进、`highlight.js` 语法高亮,便于阅读。
|
||||||
|
- 可点「复制 SQL」把格式化后的语句复制到剪贴板。
|
||||||
|
- 同时展示对查询意图的简要说明。
|
||||||
|
|
||||||
|
## 历史提问
|
||||||
|
|
||||||
|
页面下方是**历史提问**折叠面板,按账号在服务端持久化:
|
||||||
|
|
||||||
|
- 展示提问内容与时间(精确到分钟),默认展示最近若干条。
|
||||||
|
- 支持按关键字**搜索**历史。
|
||||||
|
- 点击某条历史可**回填并立即重新查询**;也可复制该次提问。
|
||||||
|
- 可一键**清空历史**。
|
||||||
|
|
||||||
|
> 当前会话内的提问与结果会缓存在浏览器会话中,切换账号时自动清除;结果超过 500 行时只缓存提问与视图,避免占用过多空间。
|
||||||
|
|
||||||
|
## 安全与隐私
|
||||||
|
|
||||||
|
智能查数始终以**只读**方式访问你自己的数据:
|
||||||
|
|
||||||
|
- 仅允许**单条 `SELECT`** 查询,拒绝任何写操作、DDL、多语句、`SELECT INTO`、`FOR UPDATE` 与危险函数。
|
||||||
|
- 只能查询**系统白名单内的数据表**,且每张表都会被强制注入当前用户的隔离条件,**只能看到本人数据**。
|
||||||
|
- 查询**不返回原始数据(`rawData`)与交易号**等敏感字段。
|
||||||
|
- 生成的 SQL 会经过安全网关校验后才执行,并限制执行时间与返回行数。
|
||||||
|
|
||||||
|
AI 本身收到的信息仅为你的自然语言提问与可查询的表结构说明,不包含账单原始字段。
|
||||||
|
|
||||||
|
## 相关
|
||||||
|
|
||||||
|
- [AI 能力](/guide/ai):系统全部 AI 功能一览。
|
||||||
|
- [统计面板](/guide/dashboard):固定的统计汇总与图表。
|
||||||
+79
-21
@@ -9,14 +9,40 @@
|
|||||||
| 按钮 | 功能 |
|
| 按钮 | 功能 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 排序 | 交易时间(新→旧 / 旧→新)、金额(高→低 / 低→高) |
|
| 排序 | 交易时间(新→旧 / 旧→新)、金额(高→低 / 低→高) |
|
||||||
| 一键设置 | 对当前表格展示的记录批量设置分类、计入统计与标签 |
|
| 查看统计 | 携带当前筛选条件跳转到[统计面板](/guide/dashboard) |
|
||||||
| 批量删除 | 删除表格中勾选的记录 |
|
|
||||||
| 导入账单 | 打开账单导入弹窗 |
|
| 导入账单 | 打开账单导入弹窗 |
|
||||||
| 导出 Excel | 按当前筛选条件导出 |
|
| 渠道管理 | 进入[交易渠道管理](/guide/channels)页,维护自定义渠道 |
|
||||||
|
| 导出 Excel | 按当前筛选条件导出;单次上限 **10000 条**,超出会提示缩小筛选范围后重试 |
|
||||||
| 刷新 | 按当前筛选条件与页码重新加载列表 |
|
| 刷新 | 按当前筛选条件与页码重新加载列表 |
|
||||||
| 智能记账 | 用一句话描述收支,AI 解析后预填表单(需启用 AI) |
|
| 智能记账 | 用一句话描述收支,AI 解析后预填表单(需启用 AI) |
|
||||||
| 新增记录 | 手动新增一笔记录 |
|
| 新增记录 | 手动新增一笔记录 |
|
||||||
|
|
||||||
|
## 批量操作
|
||||||
|
|
||||||
|
批量操作相关按钮位于**列表右上角**(分页统计信息的上方)。默认为普通模式,表格不显示勾选列,点「批量操作」后才进入多选模式:
|
||||||
|
|
||||||
|
| 按钮 | 功能 |
|
||||||
|
|---|---|
|
||||||
|
| 批量操作 / 退出批量 | 进入多选模式,表格左侧出现勾选列;点「退出批量」会清空已勾选记录并隐藏勾选列 |
|
||||||
|
| 一键设置 | 批量设置分类、计入统计与标签;**有勾选时作用于勾选记录,未勾选时作用于当前页全部记录**,详见[一键设置(批量设置)](#一键设置-批量设置) |
|
||||||
|
| 批量删除 | 勾选至少一条后出现,确认后删除所选记录 |
|
||||||
|
| 建立关系 | 勾选至少两条后出现,为所选记录[建立账单关系](/guide/relations) |
|
||||||
|
| AI 生成规则 | 启用 AI 后出现;勾选至少一条账单,由 AI 归纳分类/标签规则,规则可编辑、去重实时重算,可「同时应用到当前勾选的账单」(默认开启并记住),详见 [AI 生成规则](#ai-生成规则从勾选账单) |
|
||||||
|
|
||||||
|
> 翻页、刷新或退出批量模式都会清空已勾选记录。**批量删除**与**建立关系**只作用于勾选记录;**一键设置**在未勾选时才作用于当前页全部记录,且当次勾选范围以打开弹窗时的选择为准。
|
||||||
|
|
||||||
|
## AI 生成规则(从勾选账单)
|
||||||
|
|
||||||
|
启用 AI 后,批量操作区会出现「**AI 生成规则**」按钮。勾选若干账单后点击,AI 会依据这些账单的**类型、备注、交易渠道、已有分类与标签**归纳出「关键词 → 分类/标签」规则。
|
||||||
|
|
||||||
|
在弹窗中可**直接编辑**每条规则(名称、类型、关键词、金额区间、分类、标签;AI 建议的新分类/新标签以「新建:xxx」选项保留);编辑后系统会自动重新判定是否与已有规则重复,**改成不重复自动勾选、改成重复自动取消并禁用**。勾选后点「创建勾选规则」批量创建,系统会自动创建建议中不存在的新分类与标签。
|
||||||
|
|
||||||
|
弹窗底部可勾选「**同时应用到当前勾选的 N 条账单**」——命中规则时会**覆盖这些账单的分类并追加标签**(不改动备注与金额)。该开关**默认开启**,并会记住上次选择。生成与「同时应用」全程不发送金额、原始账单数据等敏感字段。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> 与[规则管理](/guide/rules#ai-生成规则)页的「AI 生成规则」(依据**已打标签记录**归纳)不同,此入口直接以**勾选的账单**为依据,不要求已打标签;两处的规则建议都可编辑、都会对与已有规则重复的建议标记「已存在」并禁止导入,且编辑后实时重算。
|
||||||
|
|
||||||
## 账单导入
|
## 账单导入
|
||||||
|
|
||||||
「收支记录」页支持把支付宝、微信、美团、淘宝、京东、招商银行、交通银行的账单文件解析并导入为收支记录。
|
「收支记录」页支持把支付宝、微信、美团、淘宝、京东、招商银行、交通银行的账单文件解析并导入为收支记录。
|
||||||
@@ -90,39 +116,50 @@
|
|||||||
|
|
||||||
## 筛选与 AI 智能查询
|
## 筛选与 AI 智能查询
|
||||||
|
|
||||||
**常用筛选**:类型、分类(含「未分类」)、日期范围、备注关键字、交易渠道。
|
**常用筛选**:类型、分类(含「未分类」)、日期范围、备注关键字、交易渠道。交易渠道下拉包含内置渠道与账号下的[自定义渠道](/guide/channels)。
|
||||||
|
|
||||||
**更多筛选**:标签(含「无标签」)、计入统计、金额区间。
|
**更多筛选**:标签(含「无标签」)、计入统计、金额区间。
|
||||||
|
|
||||||
启用 AI 后,顶部会出现 **AI 智能查询**输入框,可用自然语言查询,例如:
|
启用 AI 后,顶部会出现「**AI 搜索**」入口,点击打开一个**可拖拽、可缩放的浮窗**(非模态,打开后仍可继续操作主界面)。在浮窗中用自然语言查询,例如:
|
||||||
|
|
||||||
- 「上个月餐饮」
|
- 「上个月餐饮」
|
||||||
- 「超过500的支出」
|
- 「超过500的支出」
|
||||||
- 「待报销的支出」
|
- 「待报销的支出」
|
||||||
- 「微信支付」
|
- 「微信支付」
|
||||||
|
|
||||||
解析结果会直接应用为筛选条件并立即查询。筛选条件与每页条数、排序方式会自动记忆,下次进入自动还原。
|
浮窗输入框支持多行,按 `Ctrl + Enter`(Mac 为 `⌘ + Enter`)提交;解析结果会直接应用为筛选条件并立即查询。浮窗的位置与大小会被记住,输入时还能快速选择历史提问。筛选条件与每页条数、排序方式会自动记忆,下次进入自动还原。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## 表格与行操作
|
## 表格与行操作
|
||||||
|
|
||||||
表格展示:交易时间、类型、分类、金额、交易渠道、备注、标签、计入统计开关与操作。
|
表格展示:交易时间、类型、分类、金额、交易渠道、备注、标签、计入统计开关、关系与操作。
|
||||||
|
|
||||||
- **备注隐藏**:可将敏感备注持久隐藏为 `******`,需要时临时「查看」或「取消隐藏」。
|
- **备注隐藏**:可将敏感备注持久隐藏为 `******`,需要时临时「查看」或「取消隐藏」。默认这些记录会从列表与 Excel 导出中排除,可在[设置 → 外观设置](/guide/settings#外观设置)中开启「显示已隐藏备注的记录」。
|
||||||
- **计入统计**:行内开关可直接切换;关闭后该记录不纳入月度统计(适合借款、转账等),行内会以删除线标示。
|
- **计入统计**:行内开关可直接切换;关闭后该记录不纳入月度统计(适合借款、转账等),行内会以删除线标示(样式可在外观设置中调整)。
|
||||||
- **行操作**:编辑、查看「原始数据」(账单导入时的原始字段,以 JSON 展示)、删除。
|
- **关系**:展示该记录所属[账单关系](/guide/relations)组的数量,点击可查看与管理。
|
||||||
|
- **行操作**:编辑、详情、删除。
|
||||||
|
|
||||||
分页支持每页 10/20/50/100/200/500 条。
|
### 账单详情
|
||||||
|
|
||||||
## 查看原始数据
|
点击行内「**详情**」打开「账单详情」弹窗,集中展示单条账单的以下信息:
|
||||||
|
|
||||||
账单导入时,系统会完整保留每笔交易在原始账单中的字段(以 JSON 存储)。在记录行点击「**原始数据**」即可查看,用于核对金额、交易状态、对方账号、交易类型等明细。
|
- **基本信息**:类型、金额、分类、交易渠道、交易时间、是否计入统计、备注与创建时间。
|
||||||
|
- **标签**:该记录的全部标签。
|
||||||
|
- **账单关系**:所属的[账单关系](/guide/relations)组,可在弹窗内直接新建。
|
||||||
|
- **来源图片**:图片记账生成的记录会展示其来源截图,默认加载缩略图,可按需点「加载原图」查看原图。
|
||||||
|
- **原始数据**:账单导入或图片记账时保留的原始字段(以 JSON 展示),用于核对金额、交易状态、对方账号、交易类型等明细。
|
||||||
|
|
||||||
- 仅**账单导入**的记录有原始数据;**手动新增**的记录没有,该按钮不可用。
|

|
||||||
- 原始数据是排查导入异常、核对账单的重要依据。
|
|
||||||
|
> - **账单导入**记录保留原始账单行字段;**图片记账**记录保留模型从截图提取的原始字段;**手动新增**的记录没有来源图片与原始数据。
|
||||||
|
> - 原始数据是排查导入异常、核对账单的重要依据。
|
||||||
|
|
||||||
|
分页的每页条数**候选与默认值在[设置 → 外观设置](/guide/settings#外观设置)中统一配置**(默认候选 10 / 20 / 50 / 100,默认 20);各表格会分别记住上次选择的每页条数。
|
||||||
|
|
||||||
## 一键设置(批量设置)
|
## 一键设置(批量设置)
|
||||||
|
|
||||||
对**当前表格展示的记录**批量操作:
|
在列表右上角点「一键设置」,对**勾选的记录**批量设置;若未勾选任何记录,则作用于**当前表格展示的全部记录**:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -137,10 +174,15 @@
|
|||||||
|
|
||||||
## AI 智能记账
|
## AI 智能记账
|
||||||
|
|
||||||
在「智能记账」弹窗中用自然语言描述一笔收支,例如「昨天中午吃饭 35 支付宝」,AI 会解析为类型、金额、分类、交易渠道、时间、备注、标签等字段,并预填到新增记录表单,确认后保存。
|
「智能记账」弹窗提供**文字记账**与**图片记账**两种方式。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
- **文字记账**:用一句话描述一笔收支,例如「昨天中午吃饭 35 支付宝」,AI 会解析为类型、金额、分类、交易渠道、时间、备注、标签等字段,并预填到新增记录表单,确认后保存。
|
||||||
|
- **图片记账**:上传一至多张账单或支付截图(也可 `Ctrl+V` 粘贴),AI 实时识别其中的每一笔交易,生成可编辑的记录列表,勾选后批量导入。
|
||||||
|
|
||||||
|
再次打开弹窗会记住上次使用的记账方式。图片记账的用法与限制详见 [AI 能力 → 图片记账](/guide/ai#图片记账)。
|
||||||
|
|
||||||
## 手动添加记录
|
## 手动添加记录
|
||||||
|
|
||||||
1. 在收支记录页右上角点击「**新增记录**」。
|
1. 在收支记录页右上角点击「**新增记录**」。
|
||||||
@@ -149,18 +191,28 @@
|
|||||||
| 字段 | 说明 |
|
| 字段 | 说明 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 类型 | 支出 / 收入 |
|
| 类型 | 支出 / 收入 |
|
||||||
| 分类 | 新增时必填 |
|
| 分类 | 新增时必填;可点右侧「新建分类」直接创建 |
|
||||||
| 金额 | 大于 0 |
|
| 金额 | 大于 0 |
|
||||||
| 交易渠道 | 可选 |
|
| 交易渠道 | 可选,含内置渠道与[自定义渠道](/guide/channels);可点右侧「新建渠道」直接创建;启用 AI 时可一键推荐或新建 |
|
||||||
| 交易时间 | 默认当前时间 |
|
| 交易时间 | 默认当前时间 |
|
||||||
| 备注 | 可选 |
|
| 备注 | 可选 |
|
||||||
| 标签 | 可选,可多选 |
|
| 标签 | 可选,可多选;可点右侧「新建标签」直接创建 |
|
||||||
| 计入统计 | 默认开启;关闭后不纳入统计 |
|
| 计入统计 | 默认开启;关闭后不纳入统计 |
|
||||||
|
|
||||||
3. 点击「确定」保存。
|
3. 点击「确定」保存。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
::: tip 记账时直接新建分类 / 标签 / 渠道
|
||||||
|
新增与编辑弹窗中,**分类**、**标签**、**交易渠道** 三行在选择框右侧各有一个「**新建分类 / 新建标签 / 新建渠道**」按钮,**不受 AI 启用状态影响**,随时可用。点击后弹出精简表单,创建完成即自动填入当前账单:
|
||||||
|
|
||||||
|
- **新建分类**:填写名称、类型(默认跟随当前账单类型)与图标;创建后自动选中,并把账单类型同步为该分类的类型。
|
||||||
|
- **新建标签**:填写名称与颜色;创建后自动追加到已选标签。
|
||||||
|
- **新建渠道**:填写名称;创建后自动选中。
|
||||||
|
|
||||||
|

|
||||||
|
:::
|
||||||
|
|
||||||
## 编辑记录
|
## 编辑记录
|
||||||
|
|
||||||
在记录行点击「编辑」即可修改记录,弹窗字段与新增一致;编辑时允许不设置分类。
|
在记录行点击「编辑」即可修改记录,弹窗字段与新增一致;编辑时允许不设置分类。
|
||||||
@@ -169,7 +221,7 @@
|
|||||||
|
|
||||||
### 编辑时的 AI 辅助
|
### 编辑时的 AI 辅助
|
||||||
|
|
||||||
启用 AI 后,编辑弹窗中的**分类**与**标签**旁会出现 AI 辅助按钮(与新增记录一致):
|
启用 AI 后,编辑弹窗中的**分类**、**标签**与**交易渠道**旁会出现 AI 辅助按钮(与新增记录一致):
|
||||||
|
|
||||||
| 按钮 | 作用 |
|
| 按钮 | 作用 |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -177,8 +229,14 @@
|
|||||||
| 分类 · **AI 新建** | 根据当前账单信息生成新分类;确认「保存并选用」后创建并选中,若已存在同名同类型分类则直接选中 |
|
| 分类 · **AI 新建** | 根据当前账单信息生成新分类;确认「保存并选用」后创建并选中,若已存在同名同类型分类则直接选中 |
|
||||||
| 标签 · **AI 推荐** | 推荐已有标签并追加(自动去重) |
|
| 标签 · **AI 推荐** | 推荐已有标签并追加(自动去重) |
|
||||||
| 标签 · **AI 新建** | 生成新标签(含建议颜色),确认后创建并选中;同名标签直接选中 |
|
| 标签 · **AI 新建** | 生成新标签(含建议颜色),确认后创建并选中;同名标签直接选中 |
|
||||||
|
| 渠道 · **AI 推荐** | 根据当前类型、金额、渠道、备注,从已有渠道中推荐并填入 |
|
||||||
|
| 渠道 · **AI 新建** | 生成新渠道名称;确认「保存并选用」后创建并选中,同名渠道直接选中 |
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
> 「AI 新建」需先填写备注或金额,便于模型理解;生成结果会显示确认条,点击「保存并选用」才真正创建。
|
> 「AI 新建」需先填写备注或金额,便于模型理解;生成结果会显示确认条,点击「保存并选用」才真正创建。
|
||||||
|
>
|
||||||
|
> 上述 AI 辅助按钮仅在[启用 AI](/guide/ai)后出现;**分类、标签、交易渠道三行始终各有一个手动「新建」按钮**(不受 AI 状态影响),可直接填写并创建,详见[手动添加记录](#手动添加记录)中的说明。
|
||||||
|
|
||||||
编辑完成后点击「确定」保存。其他 AI 能力(批量建议、智能记账、智能查询)见 [AI 能力](/guide/ai)。
|
编辑完成后点击「确定」保存。其他 AI 能力(批量建议、智能记账、智能查询)见 [AI 能力](/guide/ai)。
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
layout: home
|
layout: home
|
||||||
|
|
||||||
hero:
|
hero:
|
||||||
name: 收支管理系统
|
name: 汇账
|
||||||
text: 个人收支管理 · 多来源账单导入 · AI 智能记账
|
text: 个人收支管理 · 多来源账单导入 · AI 智能记账
|
||||||
tagline: 支持支付宝、微信、美团、淘宝、京东、招商银行、交通银行账单导入,提供分类标签管理、自定义规则、AI 建议与定时提醒
|
tagline: 支持支付宝、微信、美团、淘宝、京东、招商银行、交通银行账单导入,提供分类标签与交易渠道管理、自定义规则、账单关系、AI 建议(含多图图片记账与智能查数)与定时提醒
|
||||||
image:
|
image:
|
||||||
src: https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/dashboard.png
|
src: https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/calendar.png
|
||||||
alt: 统计面板
|
alt: 日历
|
||||||
actions:
|
actions:
|
||||||
- theme: brand
|
- theme: brand
|
||||||
text: 使用文档
|
text: 使用文档
|
||||||
@@ -22,25 +22,31 @@ hero:
|
|||||||
features:
|
features:
|
||||||
- title: 多来源账单导入
|
- title: 多来源账单导入
|
||||||
details: 支持支付宝、微信、美团、淘宝、京东、招商银行、交通银行账单,解析预览后确认导入,按交易号自动去重。
|
details: 支持支付宝、微信、美团、淘宝、京东、招商银行、交通银行账单,解析预览后确认导入,按交易号自动去重。
|
||||||
|
- title: 账单关系
|
||||||
|
details: 把父子/同级的多笔账单组合关联,自动绘制关系图,并可由 AI 生成关系名称与备注。
|
||||||
- title: 分类与标签
|
- title: 分类与标签
|
||||||
details: 自定义收入/支出分类与标签,支持批量设置,AI 可推荐 Emoji 图标与标签颜色。
|
details: 自定义收入/支出分类与标签,支持批量设置,AI 可推荐 Emoji 图标与标签颜色。
|
||||||
|
- title: 交易渠道管理
|
||||||
|
details: 内置常用渠道,支持自定义渠道的新建、改名与删除,记账时可由 AI 推荐或生成;改名不影响历史账单,筛选与统计均可选用。
|
||||||
- title: 自定义规则
|
- title: 自定义规则
|
||||||
details: 按关键词、收支类型、金额区间组合条件,命中后自动应用分类与标签,导入预览即展示建议。
|
details: 按关键词、收支类型、金额区间组合条件,命中后自动应用分类与标签,导入预览即展示建议。
|
||||||
- title: AI 智能能力
|
- title: AI 智能能力
|
||||||
details: 基于 DeepSeek 的分类/标签建议、自然语言查询与记账、分类与标签生成,并统计 token 用量。
|
details: 基于 DeepSeek 的分类/标签/渠道建议、自然语言查询与查数(智能查数)、文字与多图图片记账(实时进度)、分类标签渠道生成、关系命名,并统计 token 用量。
|
||||||
|
- title: 日历视图
|
||||||
|
details: 默认首页,月视图按天展示每日收支与当月合计,年视图一览全年各月;点击某天查看当日明细并直达账单详情,支持统计口径过滤。
|
||||||
- title: 统计与图表
|
- title: 统计与图表
|
||||||
details: 月度收支汇总、分类占比、收支趋势、支付方式占比,支持多维度筛选与自定义时间范围。
|
details: 月度收支汇总、分类占比、收支趋势、交易渠道占比,支持多维度筛选与自定义时间范围,图表可点击下钻明细。
|
||||||
- title: 定时提醒与通知
|
- title: 定时提醒与通知
|
||||||
details: 按每天/每周/每月周期提醒导入新账单,支持邮箱 SMTP 与 Server 酱多渠道推送。
|
details: 按每天/每周/每月周期提醒导入新账单,支持邮箱 SMTP、Server 酱与飞书多渠道推送。
|
||||||
- title: 邮箱账单自动拉取
|
- title: 邮箱账单自动拉取
|
||||||
details: 通过 IMAP IDLE 监听邮箱,新账单邮件到达后自动拉取附件,先预览再确认导入。
|
details: 通过 IMAP IDLE 监听邮箱,新账单邮件到达后自动拉取附件,先预览再确认导入。
|
||||||
- title: 系统日志
|
- title: 日志
|
||||||
details: 接口访问日志、登录日志与后端运行日志在线查看,运行日志支持 SSE 实时推送。
|
details: 接口访问日志、登录日志与后端运行日志在线查看,运行日志支持 SSE 实时推送。
|
||||||
---
|
---
|
||||||
|
|
||||||
## 这是什么
|
## 这是什么
|
||||||
|
|
||||||
收支管理系统是一个自托管的个人记账应用,帮助你把分散在支付宝、微信、银行等平台的账单汇总到一处,统一分类、打标签、统计收支,并通过 AI 与自动化减少手工整理的工作量。
|
汇账是一个自托管的个人记账应用,帮助你把分散在支付宝、微信、银行等平台的账单汇总到一处,统一分类、打标签、统计收支,并通过 AI 与自动化减少手工整理的工作量。
|
||||||
|
|
||||||
> **在线体验**:[https://finance.rosercode.cn/](https://finance.rosercode.cn/)(演示账号 `demo` / `demo123456`,使用独立的 SQLite 演示库与虚构数据)
|
> **在线体验**:[https://finance.rosercode.cn/](https://finance.rosercode.cn/)(演示账号 `demo` / `demo123456`,使用独立的 SQLite 演示库与虚构数据)
|
||||||
|
|
||||||
@@ -55,7 +61,7 @@ features:
|
|||||||
|
|
||||||
- 想开始使用:[快速开始](/guide/getting-started)
|
- 想开始使用:[快速开始](/guide/getting-started)
|
||||||
- 想知道各类账单怎么导出:[账单类型与获取](/guide/bills/overview)
|
- 想知道各类账单怎么导出:[账单类型与获取](/guide/bills/overview)
|
||||||
- 想了解各功能怎么用:[统计面板](/guide/dashboard) 起逐页阅读
|
- 想了解各功能怎么用:[日历](/guide/calendar) 起逐页阅读
|
||||||
- 遇到问题:[常见问题](/guide/faq)
|
- 遇到问题:[常见问题](/guide/faq)
|
||||||
|
|
||||||
### 开发文档
|
### 开发文档
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "finance-app-docs",
|
"name": "ledgerly-docs",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.1",
|
"version": "0.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"docs:dev": "vitepress dev",
|
"docs:dev": "vitepress dev",
|
||||||
|
|||||||
@@ -30,7 +30,10 @@ export async function newContext(
|
|||||||
browser: Browser,
|
browser: Browser,
|
||||||
viewport: { width: number; height: number } = DEFAULT_VIEWPORT,
|
viewport: { width: number; height: number } = DEFAULT_VIEWPORT,
|
||||||
): Promise<BrowserContext> {
|
): Promise<BrowserContext> {
|
||||||
return browser.newContext({ viewport })
|
const context = await browser.newContext({ viewport })
|
||||||
|
// 首次访问需等待 Vite 按需编译,放宽默认导航超时,避免冷启动误判失败
|
||||||
|
context.setDefaultNavigationTimeout(120000)
|
||||||
|
return context
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
// - scripts/samples/alipay_sample.zip 样例账单存在。
|
// - scripts/samples/alipay_sample.zip 样例账单存在。
|
||||||
import { mkdir } from 'node:fs/promises'
|
import { mkdir } from 'node:fs/promises'
|
||||||
import { injectSession, launchBrowser, login, newContext } from './browser.js'
|
import { injectSession, launchBrowser, login, newContext } from './browser.js'
|
||||||
import { captureAiRule, captureBillParse, captureCards, captureEditRecord, captureModals, captureSettingsPanels, captureTrendJump } from './capture-elements.js'
|
import { captureAddRecord, captureAiRule, captureBillParse, captureCalendarDetail, captureCalendarMonth, captureCalendarYear, captureCards, captureEditRecord, captureInlineCreate, captureModals, captureSettingsPanels, captureTrendJump } from './capture-elements.js'
|
||||||
import { capturePages } from './capture-pages.js'
|
import { capturePages } from './capture-pages.js'
|
||||||
import { ELEM_DIR } from './config.js'
|
import { ELEM_DIR } from './config.js'
|
||||||
|
|
||||||
@@ -25,8 +25,13 @@ async function main(): Promise<void> {
|
|||||||
await injectSession(context, token)
|
await injectSession(context, token)
|
||||||
const page = await context.newPage()
|
const page = await context.newPage()
|
||||||
await capturePages(page)
|
await capturePages(page)
|
||||||
|
await captureCalendarMonth(page)
|
||||||
|
await captureCalendarYear(page)
|
||||||
|
await captureCalendarDetail(page)
|
||||||
|
await captureAddRecord(page)
|
||||||
await captureModals(page)
|
await captureModals(page)
|
||||||
await captureEditRecord(page)
|
await captureEditRecord(page)
|
||||||
|
await captureInlineCreate(page)
|
||||||
await captureAiRule(page)
|
await captureAiRule(page)
|
||||||
await captureSettingsPanels(page)
|
await captureSettingsPanels(page)
|
||||||
await captureBillParse(page)
|
await captureBillParse(page)
|
||||||
|
|||||||
+513
-10
@@ -1,9 +1,9 @@
|
|||||||
// 元素截图:截取登录/注册卡片、各功能弹窗与账单解析预览,写入 public/screenshots/elements/。
|
// 元素截图:截取登录/注册卡片、各功能弹窗与账单解析预览,写入 public/screenshots/elements/。
|
||||||
import { resolve } from 'node:path'
|
import { resolve } from 'node:path'
|
||||||
import { pathToFileURL } from 'node:url'
|
import { pathToFileURL } from 'node:url'
|
||||||
import type { Page } from 'playwright-core'
|
import type { Locator, Page } from 'playwright-core'
|
||||||
import { clickText, injectSession, launchBrowser, login, newContext, waitPage } from './browser.js'
|
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, OUT_DIR, SAMPLE_BILL, SAMPLE_IMAGE, SAMPLE_IMAGE2 } from './config.js'
|
||||||
|
|
||||||
/** 页面就绪标志:列表页表格、设置面板或卡片 */
|
/** 页面就绪标志:列表页表格、设置面板或卡片 */
|
||||||
const PAGE_READY = '.n-data-table, .config-card, .n-card'
|
const PAGE_READY = '.n-data-table, .config-card, .n-card'
|
||||||
@@ -16,7 +16,6 @@ const CARD_SCENES: Array<[string, string, string]> = [
|
|||||||
|
|
||||||
/** 弹窗截图:文件名、路由、触发按钮文本 */
|
/** 弹窗截图:文件名、路由、触发按钮文本 */
|
||||||
const MODAL_SCENES: Array<[string, string, string]> = [
|
const MODAL_SCENES: Array<[string, string, string]> = [
|
||||||
['add-record', '/transactions', '新增记录'],
|
|
||||||
['batch-set', '/transactions', '一键设置'],
|
['batch-set', '/transactions', '一键设置'],
|
||||||
['import-bill', '/transactions', '导入账单'],
|
['import-bill', '/transactions', '导入账单'],
|
||||||
['ai-record', '/transactions', '智能记账'],
|
['ai-record', '/transactions', '智能记账'],
|
||||||
@@ -26,6 +25,7 @@ const MODAL_SCENES: Array<[string, string, string]> = [
|
|||||||
['add-rule', '/rules', '新增规则'],
|
['add-rule', '/rules', '新增规则'],
|
||||||
['add-email-config', '/email-bills/configs', '新增'],
|
['add-email-config', '/email-bills/configs', '新增'],
|
||||||
['add-channel', '/settings', '添加渠道'],
|
['add-channel', '/settings', '添加渠道'],
|
||||||
|
['channel-create', '/channels', '新建渠道'],
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -57,9 +57,11 @@ export async function captureCards(page: Page): Promise<void> {
|
|||||||
* 截取各功能弹窗。
|
* 截取各功能弹窗。
|
||||||
*
|
*
|
||||||
* @param page Page 实例
|
* @param page Page 实例
|
||||||
|
* @param only 仅截取指定场景名(可选,缺省截取全部)
|
||||||
*/
|
*/
|
||||||
export async function captureModals(page: Page): Promise<void> {
|
export async function captureModals(page: Page, only?: string[]): Promise<void> {
|
||||||
for (const [name, path, button] of MODAL_SCENES) {
|
const scenes = only ? MODAL_SCENES.filter(([name]) => only.includes(name)) : MODAL_SCENES
|
||||||
|
for (const [name, path, button] of scenes) {
|
||||||
try {
|
try {
|
||||||
await page.setViewportSize({ width: 1440, height: 1000 })
|
await page.setViewportSize({ width: 1440, height: 1000 })
|
||||||
await page.goto(BASE_URL + path)
|
await page.goto(BASE_URL + path)
|
||||||
@@ -84,6 +86,64 @@ export async function captureModals(page: Page): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 截取「添加渠道」弹窗的飞书配置表单(elements/add-channel-feishu.png)。
|
||||||
|
*
|
||||||
|
* 打开设置页的添加渠道弹窗后,将「渠道类型」切换为飞书,展示 Webhook 地址与签名密钥表单项。
|
||||||
|
*
|
||||||
|
* @param page Page 实例
|
||||||
|
*/
|
||||||
|
export async function captureChannelFeishu(page: Page): Promise<void> {
|
||||||
|
try {
|
||||||
|
await page.setViewportSize({ width: 1440, height: 1000 })
|
||||||
|
await page.goto(BASE_URL + '/settings')
|
||||||
|
await waitPage(page, { selector: PAGE_READY })
|
||||||
|
if (!(await clickText(page, '添加渠道'))) throw new Error('未找到「添加渠道」按钮')
|
||||||
|
const modal = page.locator('.n-modal').first()
|
||||||
|
try {
|
||||||
|
await modal.waitFor({ state: 'visible', timeout: 8000 })
|
||||||
|
} catch {
|
||||||
|
// 首次点击可能未生效(页面状态未就绪),重试一次
|
||||||
|
await clickText(page, '添加渠道')
|
||||||
|
await modal.waitFor({ state: 'visible', timeout: 15000 })
|
||||||
|
}
|
||||||
|
// 弹窗内首个下拉为「渠道类型」,切换为飞书以展示 Webhook 与签名密钥表单
|
||||||
|
await modal.locator('.n-base-selection').first().click()
|
||||||
|
await page.getByText('飞书', { exact: true }).last().click()
|
||||||
|
await page.waitForTimeout(600)
|
||||||
|
await modal.screenshot({ path: resolve(ELEM_DIR, 'add-channel-feishu.png') })
|
||||||
|
console.log('[OK] modal/add-channel-feishu')
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`[FAIL] add-channel-feishu: ${err}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 截取新增记录弹窗(elements/add-record.png)。
|
||||||
|
*
|
||||||
|
* 先二次加载页面,确保模块级 AI 启用状态已就绪,使弹窗完整展示 AI 辅助按钮。
|
||||||
|
*
|
||||||
|
* @param page Page 实例
|
||||||
|
*/
|
||||||
|
export async function captureAddRecord(page: Page): Promise<void> {
|
||||||
|
try {
|
||||||
|
await page.setViewportSize({ width: 1440, height: 1000 })
|
||||||
|
await page.goto(BASE_URL + '/transactions')
|
||||||
|
await waitPage(page, { selector: PAGE_READY })
|
||||||
|
await page.reload()
|
||||||
|
await waitPage(page, { selector: PAGE_READY })
|
||||||
|
await page.waitForTimeout(1500)
|
||||||
|
if (!(await clickText(page, '新增记录'))) throw new Error('未找到「新增记录」按钮')
|
||||||
|
const modal = page.locator('.n-modal').first()
|
||||||
|
await modal.waitFor({ state: 'visible', timeout: 20000 })
|
||||||
|
await page.waitForTimeout(900)
|
||||||
|
await modal.screenshot({ path: resolve(ELEM_DIR, 'add-record.png') })
|
||||||
|
console.log('[OK] modal/add-record')
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`[FAIL] modal/add-record: ${err}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 截取账单解析预览:上传样例账单后解析。
|
* 截取账单解析预览:上传样例账单后解析。
|
||||||
*
|
*
|
||||||
@@ -110,6 +170,134 @@ export async function captureBillParse(page: Page): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 截取「图片记账」:多图上传 Tab 与识别结果弹窗。
|
||||||
|
*
|
||||||
|
* @param page Page 实例
|
||||||
|
* @param importRecords 识别后是否导入勾选记录(默认 true,便于后续截取来源图片)
|
||||||
|
*/
|
||||||
|
export async function captureAiRecordImage(page: Page, importRecords = true): Promise<void> {
|
||||||
|
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}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 截取「账单详情」弹窗(含基本信息、标签、账单关系、来源图片与原始数据)。
|
||||||
|
*
|
||||||
|
* 优先选择「有来源图片、无账单关系」的记录,使弹窗聚焦来源图片与原始数据。
|
||||||
|
*
|
||||||
|
* @param page Page 实例
|
||||||
|
*/
|
||||||
|
export async function captureBillDetail(page: Page): Promise<void> {
|
||||||
|
try {
|
||||||
|
await page.setViewportSize({ width: 1440, height: 1200 })
|
||||||
|
await page.goto(BASE_URL + '/transactions')
|
||||||
|
await waitPage(page, { selector: '.n-data-table-tbody tr' })
|
||||||
|
const rows = page.locator('.n-data-table-tbody tr')
|
||||||
|
const count = await rows.count()
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const detailBtn = rows.nth(i).locator('button', { hasText: '详情' }).first()
|
||||||
|
if ((await detailBtn.count()) === 0) continue
|
||||||
|
await detailBtn.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: 15000 }).catch(() => undefined)
|
||||||
|
await page.waitForTimeout(800)
|
||||||
|
|
||||||
|
const imageSection = modal.locator('.detail-section', { hasText: '来源图片' }).first()
|
||||||
|
const imgCount = await imageSection.locator('img').count()
|
||||||
|
const relationSection = modal.locator('.detail-section', { hasText: '账单关系' }).first()
|
||||||
|
const relationText = (await relationSection.innerText().catch(() => '')) || ''
|
||||||
|
const noRelation = relationText.includes('暂无关联账单')
|
||||||
|
|
||||||
|
if (imgCount > 0 && noRelation) {
|
||||||
|
// 加大视口并取消弹窗内部滚动,使来源图片与原始数据一并入镜
|
||||||
|
await page.setViewportSize({ width: 1440, height: 2400 })
|
||||||
|
await page.evaluate(() => {
|
||||||
|
document
|
||||||
|
.querySelectorAll<HTMLElement>('.n-modal, .n-modal .n-card, .n-modal .n-card__content')
|
||||||
|
.forEach((el) => {
|
||||||
|
el.style.maxHeight = 'none'
|
||||||
|
el.style.overflow = 'visible'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
await page.waitForTimeout(500)
|
||||||
|
await modal.screenshot({ path: resolve(ELEM_DIR, 'bill-detail.png') })
|
||||||
|
console.log('[OK] modal/bill-detail')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不满足条件则关闭弹窗,继续查看下一行
|
||||||
|
await page.locator('.n-modal .n-base-close').first().click().catch(() => undefined)
|
||||||
|
await page.keyboard.press('Escape').catch(() => undefined)
|
||||||
|
await page.waitForTimeout(400)
|
||||||
|
}
|
||||||
|
throw new Error('未找到「有来源图片且无账单关系」的记录')
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`[FAIL] modal/bill-detail: ${err}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 截取编辑记录弹窗中「交易渠道」的 AI 推荐与新建按钮。
|
||||||
|
*
|
||||||
|
* @param page Page 实例
|
||||||
|
*/
|
||||||
|
export async function captureChannelAi(page: Page): Promise<void> {
|
||||||
|
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}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 截取「点击趋势柱状图跳转收支记录并自动带入筛选条件」的筛选卡片。
|
* 截取「点击趋势柱状图跳转收支记录并自动带入筛选条件」的筛选卡片。
|
||||||
*
|
*
|
||||||
@@ -122,12 +310,12 @@ export async function captureTrendJump(page: Page): Promise<void> {
|
|||||||
await waitPage(page, { selector: 'canvas' })
|
await waitPage(page, { selector: 'canvas' })
|
||||||
// 切到「近12月 + 按月」,柱子更宽便于稳定点中;同时清空收支记录筛选偏好,避免旧条件干扰
|
// 切到「近12月 + 按月」,柱子更宽便于稳定点中;同时清空收支记录筛选偏好,避免旧条件干扰
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
localStorage.setItem('finance.dashboard.prefs', JSON.stringify({
|
localStorage.setItem('ledgerly.dashboard.prefs', JSON.stringify({
|
||||||
rangeType: '12month', granularity: 'month', filterStatsScope: 'INCLUDED',
|
rangeType: '12month', granularity: 'month', filterStatsScope: 'INCLUDED',
|
||||||
filterType: [], filterCategory: [], filterChannel: [], filterTag: [],
|
filterType: [], filterCategory: [], filterChannel: [], filterTag: [],
|
||||||
filterMinAmount: null, filterMaxAmount: null, filterRemark: '',
|
filterMinAmount: null, filterMaxAmount: null, filterRemark: '',
|
||||||
}))
|
}))
|
||||||
localStorage.removeItem('finance.transaction.filterPrefs')
|
localStorage.removeItem('ledgerly.transaction.filterPrefs')
|
||||||
})
|
})
|
||||||
await page.reload()
|
await page.reload()
|
||||||
await waitPage(page, { selector: 'canvas' })
|
await waitPage(page, { selector: 'canvas' })
|
||||||
@@ -159,10 +347,12 @@ export async function captureTrendJump(page: Page): Promise<void> {
|
|||||||
|
|
||||||
/** 系统设置面板:输出文件名与面板标题 */
|
/** 系统设置面板:输出文件名与面板标题 */
|
||||||
const SETTINGS_PANELS: Array<[string, string]> = [
|
const SETTINGS_PANELS: Array<[string, string]> = [
|
||||||
|
['panel-account-security', '账号安全'],
|
||||||
['panel-database', '数据库配置'],
|
['panel-database', '数据库配置'],
|
||||||
['panel-registration', '注册设置'],
|
['panel-registration', '注册设置'],
|
||||||
['panel-bill-source-stats', '账单来源统计默认值'],
|
['panel-bill-source-stats', '账单来源统计默认值'],
|
||||||
['panel-display', '外观设置'],
|
['panel-display', '外观设置'],
|
||||||
|
['panel-nav-menu', '导航设置'],
|
||||||
['panel-ai', 'AI 模型配置'],
|
['panel-ai', 'AI 模型配置'],
|
||||||
['panel-notification', '定时通知'],
|
['panel-notification', '定时通知'],
|
||||||
]
|
]
|
||||||
@@ -190,6 +380,31 @@ export async function captureEditRecord(page: Page): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 截取记账弹窗内「新建分类」子弹窗(体现新增/编辑记录时可直接新建分类)。
|
||||||
|
*
|
||||||
|
* @param page Page 实例
|
||||||
|
*/
|
||||||
|
export async function captureInlineCreate(page: Page): Promise<void> {
|
||||||
|
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 })
|
||||||
|
if (!(await clickText(page, '新建分类'))) throw new Error('未找到「新建分类」按钮')
|
||||||
|
await page.waitForTimeout(800)
|
||||||
|
// 子弹窗为后打开的最后一层弹窗
|
||||||
|
const sub = page.locator('.n-modal').last()
|
||||||
|
await sub.waitFor({ state: 'visible', timeout: 8000 })
|
||||||
|
await sub.screenshot({ path: resolve(ELEM_DIR, 'record-new-category.png') })
|
||||||
|
console.log('[OK] modal/record-new-category')
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`[FAIL] modal/record-new-category: ${err}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 截取「AI 生成规则」的两步弹窗:步骤一选择记录、步骤二预览规则建议。
|
* 截取「AI 生成规则」的两步弹窗:步骤一选择记录、步骤二预览规则建议。
|
||||||
*
|
*
|
||||||
@@ -228,15 +443,138 @@ export async function captureAiRule(page: Page): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 截取系统设置的六个配置面板。
|
* 截取「收支记录 → 批量操作 → AI 生成规则」弹窗(基于勾选账单生成规则)。
|
||||||
|
*
|
||||||
|
* <p>仅触发规则建议生成(只读),不点击「创建勾选规则」,不修改数据。
|
||||||
|
* 需目标环境已启用 AI 且存在可勾选账单。</p>
|
||||||
*
|
*
|
||||||
* @param page Page 实例
|
* @param page Page 实例
|
||||||
*/
|
*/
|
||||||
export async function captureSettingsPanels(page: Page): Promise<void> {
|
export async function captureAiRuleFromTransactions(page: Page): Promise<void> {
|
||||||
|
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('未找到「批量操作」按钮')
|
||||||
|
await page.waitForTimeout(500)
|
||||||
|
const checkboxes = page.locator('.n-data-table-tbody .n-checkbox')
|
||||||
|
const count = Math.min(await checkboxes.count(), 5)
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
await checkboxes.nth(i).click({ force: true }).catch(() => undefined)
|
||||||
|
}
|
||||||
|
await page.waitForTimeout(300)
|
||||||
|
|
||||||
|
// 点击「AI 生成规则」,等待弹窗与规则建议表(真实 AI 调用)
|
||||||
|
if (!(await clickText(page, 'AI 生成规则'))) throw new Error('未找到「AI 生成规则」按钮')
|
||||||
|
const modal = page.locator('.n-modal').first()
|
||||||
|
await modal.waitFor({ state: 'visible', timeout: 30000 })
|
||||||
|
await modal.locator('.n-data-table-tbody tr').first().waitFor({ state: 'visible', timeout: 90000 })
|
||||||
|
await page.waitForLoadState('networkidle', { timeout: 90000 }).catch(() => undefined)
|
||||||
|
await page.waitForTimeout(1000)
|
||||||
|
await modal.screenshot({ path: resolve(ELEM_DIR, 'ai-rule-transactions.png') })
|
||||||
|
console.log('[OK] modal/ai-rule-transactions')
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`[FAIL] modal/ai-rule-transactions: ${err}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 截取规则管理页「应用」命中的账单预览弹窗(elements/rule-apply-preview.png)。
|
||||||
|
*
|
||||||
|
* <p>打开首条规则的行内「应用」按钮,等待命中账单预览表加载后截图;只预览、不点击「确认应用」,不修改数据。
|
||||||
|
* 需目标环境存在至少一条可命中账单的规则。</p>
|
||||||
|
*
|
||||||
|
* @param page Page 实例
|
||||||
|
*/
|
||||||
|
export async function captureRuleApplyPreview(page: Page): Promise<void> {
|
||||||
|
try {
|
||||||
|
await page.setViewportSize({ width: 1440, height: 1000 })
|
||||||
|
await page.goto(BASE_URL + '/rules')
|
||||||
|
await waitPage(page, { selector: PAGE_READY })
|
||||||
|
// 首条规则行的「应用」按钮(避免误点顶部的「应用匹配账单」)
|
||||||
|
const applyBtn = page.locator('.n-data-table-tbody tr').first().locator('button', { hasText: '应用' }).first()
|
||||||
|
await applyBtn.waitFor({ state: 'visible', timeout: 20000 })
|
||||||
|
await applyBtn.evaluate((el) => (el as HTMLElement).click())
|
||||||
|
const modal = page.locator('.n-modal').first()
|
||||||
|
await modal.waitFor({ state: 'visible', timeout: 20000 })
|
||||||
|
// 命中账单表异步加载,等待数据行出现
|
||||||
|
await modal.locator('.n-data-table-tbody tr').first().waitFor({ state: 'visible', timeout: 20000 })
|
||||||
|
await page.waitForLoadState('networkidle', { timeout: 20000 }).catch(() => undefined)
|
||||||
|
await page.waitForTimeout(800)
|
||||||
|
await modal.screenshot({ path: resolve(ELEM_DIR, 'rule-apply-preview.png') })
|
||||||
|
console.log('[OK] modal/rule-apply-preview')
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`[FAIL] modal/rule-apply-preview: ${err}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 勾选表格前 n 行(用于进入批量模式后展示多选效果)。
|
||||||
|
*
|
||||||
|
* @param page Page 实例
|
||||||
|
* @param table 目标表格 Locator
|
||||||
|
* @param n 勾选行数
|
||||||
|
*/
|
||||||
|
async function pickRows(page: Page, table: Locator, n: number): Promise<void> {
|
||||||
|
const rows = table.locator('.n-data-table-tbody tr')
|
||||||
|
const count = Math.min(n, await rows.count())
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
await rows.nth(i).locator('.n-checkbox').first().click({ force: true }).catch(() => undefined)
|
||||||
|
}
|
||||||
|
await page.waitForTimeout(300)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 截取标签/分类管理页的「批量操作模式」(勾选列与批量删除入口)。
|
||||||
|
*
|
||||||
|
* <p>进入批量选择模式后勾选若干行,展示勾选列;仅截图,不执行任何删除。</p>
|
||||||
|
*
|
||||||
|
* @param page Page 实例
|
||||||
|
*/
|
||||||
|
export async function captureBatchModes(page: Page): Promise<void> {
|
||||||
|
// 标签管理批量模式
|
||||||
|
try {
|
||||||
|
await page.setViewportSize({ width: 1440, height: 900 })
|
||||||
|
await page.goto(BASE_URL + '/tags')
|
||||||
|
await waitPage(page, { selector: '.n-data-table-tbody tr' })
|
||||||
|
if (!(await clickText(page, '批量操作'))) throw new Error('未找到「批量操作」按钮')
|
||||||
|
await pickRows(page, page.locator('.n-data-table').first(), 2)
|
||||||
|
await page.screenshot({ path: resolve(OUT_DIR, 'tags-batch.png') })
|
||||||
|
console.log('[OK] page/tags-batch')
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`[FAIL] tags-batch: ${err}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分类管理批量模式(支出/收入两表各勾选若干)
|
||||||
|
try {
|
||||||
|
await page.setViewportSize({ width: 1440, height: 1100 })
|
||||||
|
await page.goto(BASE_URL + '/categories')
|
||||||
|
await waitPage(page, { selector: '.n-data-table-tbody tr' })
|
||||||
|
if (!(await clickText(page, '批量操作'))) throw new Error('未找到「批量操作」按钮')
|
||||||
|
const tables = page.locator('.n-data-table')
|
||||||
|
await pickRows(page, tables.nth(0), 2)
|
||||||
|
await pickRows(page, tables.nth(1), 2)
|
||||||
|
await page.screenshot({ path: resolve(OUT_DIR, 'categories-batch.png') })
|
||||||
|
console.log('[OK] page/categories-batch')
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`[FAIL] categories-batch: ${err}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 截取系统设置的配置面板。
|
||||||
|
*
|
||||||
|
* @param page Page 实例
|
||||||
|
* @param only 仅截取指定标题的面板(可选,缺省截取全部)
|
||||||
|
*/
|
||||||
|
export async function captureSettingsPanels(page: Page, only?: string[]): Promise<void> {
|
||||||
await page.setViewportSize({ width: 1440, height: 1200 })
|
await page.setViewportSize({ width: 1440, height: 1200 })
|
||||||
await page.goto(BASE_URL + '/settings')
|
await page.goto(BASE_URL + '/settings')
|
||||||
await waitPage(page, { selector: PAGE_READY })
|
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 {
|
try {
|
||||||
const card = page.locator('.config-card', { hasText: title }).first()
|
const card = page.locator('.config-card', { hasText: title }).first()
|
||||||
await card.waitFor({ state: 'visible', timeout: 20000 })
|
await card.waitFor({ state: 'visible', timeout: 20000 })
|
||||||
@@ -250,6 +588,159 @@ export async function captureSettingsPanels(page: Page): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 截取 AI 查询浮窗(统计面板顶部的「AI 搜索」入口)。
|
||||||
|
*
|
||||||
|
* @param page Page 实例
|
||||||
|
*/
|
||||||
|
export async function captureAiQueryWindow(page: Page): Promise<void> {
|
||||||
|
try {
|
||||||
|
await page.setViewportSize({ width: 1440, height: 1000 })
|
||||||
|
await page.goto(BASE_URL + '/dashboard')
|
||||||
|
await waitPage(page, { selector: 'canvas' })
|
||||||
|
if (!(await clickText(page, 'AI 搜索'))) throw new Error('未找到「AI 搜索」按钮')
|
||||||
|
const panel = page.locator('.floating-panel').first()
|
||||||
|
await panel.waitFor({ state: 'visible', timeout: 20000 })
|
||||||
|
await page.waitForTimeout(600)
|
||||||
|
await panel.screenshot({ path: resolve(ELEM_DIR, 'ai-query-window.png') })
|
||||||
|
console.log('[OK] modal/ai-query-window')
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`[FAIL] modal/ai-query-window: ${err}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 截取账单关系:关系查看面板(含关系图)与新建关系弹窗。
|
||||||
|
*
|
||||||
|
* @param page Page 实例
|
||||||
|
*/
|
||||||
|
export async function captureRelations(page: Page): Promise<void> {
|
||||||
|
try {
|
||||||
|
await page.setViewportSize({ width: 1440, height: 1100 })
|
||||||
|
await page.goto(BASE_URL + '/transactions')
|
||||||
|
await waitPage(page, { selector: '.n-data-table-tbody tr' })
|
||||||
|
const relBtn = page.locator('button', { hasText: /个关系/ }).first()
|
||||||
|
await relBtn.waitFor({ state: 'visible', timeout: 20000 })
|
||||||
|
await relBtn.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, 'relation-panel.png') })
|
||||||
|
console.log('[OK] modal/relation-panel')
|
||||||
|
|
||||||
|
const createBtn = modal.locator('button', { hasText: '新建关系' }).first()
|
||||||
|
await createBtn.waitFor({ state: 'visible', timeout: 15000 })
|
||||||
|
await createBtn.evaluate((el) => (el as HTMLElement).click())
|
||||||
|
const editor = page.locator('.n-modal', { hasText: '新建账单关系' }).first()
|
||||||
|
await editor.waitFor({ state: 'visible', timeout: 20000 })
|
||||||
|
await page.waitForTimeout(800)
|
||||||
|
await editor.screenshot({ path: resolve(ELEM_DIR, 'relation-editor.png') })
|
||||||
|
console.log('[OK] modal/relation-editor')
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`[FAIL] modal/relations: ${err}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 截取智能查数整页(先执行一次查询再截图)。
|
||||||
|
*
|
||||||
|
* @param page Page 实例
|
||||||
|
*/
|
||||||
|
export async function captureSqlQuery(page: Page): Promise<void> {
|
||||||
|
try {
|
||||||
|
await page.setViewportSize({ width: 1440, height: 1180 })
|
||||||
|
await page.goto(BASE_URL + '/smart-query')
|
||||||
|
await waitPage(page, { selector: '.n-card' })
|
||||||
|
const input = page.locator('textarea').first()
|
||||||
|
await input.waitFor({ state: 'visible', timeout: 20000 })
|
||||||
|
await input.fill('按分类统计今年的支出')
|
||||||
|
await Promise.all([
|
||||||
|
page.waitForResponse(
|
||||||
|
(r) => r.url().includes('/ai/sql') && r.request().method() === 'POST',
|
||||||
|
{ timeout: 130000 },
|
||||||
|
),
|
||||||
|
page.locator('button', { hasText: '查询' }).first().click(),
|
||||||
|
])
|
||||||
|
await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => undefined)
|
||||||
|
await page.waitForTimeout(1500)
|
||||||
|
await page.screenshot({ path: resolve(OUT_DIR, 'sql-query.png') })
|
||||||
|
console.log('[OK] page/sql-query')
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`[FAIL] page/sql-query: ${err}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 截取日历「月视图」整页:写入 screenshots/calendar.png。
|
||||||
|
*
|
||||||
|
* @param page Page 实例
|
||||||
|
*/
|
||||||
|
export async function captureCalendarMonth(page: Page): Promise<void> {
|
||||||
|
try {
|
||||||
|
await page.setViewportSize({ width: 1440, height: 1280 })
|
||||||
|
await page.goto(BASE_URL + '/calendar?view=month')
|
||||||
|
await waitPage(page, { selector: '.n-calendar' })
|
||||||
|
await page.locator('.day-cell__row').first().waitFor({ state: 'visible', timeout: 20000 }).catch(() => undefined)
|
||||||
|
await page.waitForTimeout(600)
|
||||||
|
await page.screenshot({ path: resolve(OUT_DIR, 'calendar.png') })
|
||||||
|
console.log('[OK] calendar')
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`[FAIL] calendar: ${err}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 截取日历「年视图」:切换到年视图后截取。
|
||||||
|
*
|
||||||
|
* @param page Page 实例
|
||||||
|
*/
|
||||||
|
export async function captureCalendarYear(page: Page): Promise<void> {
|
||||||
|
try {
|
||||||
|
await page.setViewportSize({ width: 1440, height: 1000 })
|
||||||
|
await page.goto(BASE_URL + '/calendar?view=month')
|
||||||
|
await waitPage(page, { selector: '.n-calendar' })
|
||||||
|
const viewBtn = page.locator('.cal-toolbar__view').first()
|
||||||
|
await viewBtn.waitFor({ state: 'visible', timeout: 15000 })
|
||||||
|
await viewBtn.evaluate((el) => (el as HTMLElement).click())
|
||||||
|
await page.locator('.year-cell').first().waitFor({ state: 'visible', timeout: 15000 })
|
||||||
|
await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => undefined)
|
||||||
|
await page.waitForTimeout(800)
|
||||||
|
await page.screenshot({ path: resolve(ELEM_DIR, 'calendar-year.png') })
|
||||||
|
console.log('[OK] calendar-year')
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`[FAIL] calendar-year: ${err}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 截取日历「当日明细浮窗」:点击某天有数据的日期后截取浮窗。
|
||||||
|
*
|
||||||
|
* @param page Page 实例
|
||||||
|
*/
|
||||||
|
export async function captureCalendarDetail(page: Page): Promise<void> {
|
||||||
|
try {
|
||||||
|
await page.setViewportSize({ width: 1440, height: 1100 })
|
||||||
|
await page.goto(BASE_URL + '/calendar?view=month')
|
||||||
|
await waitPage(page, { selector: '.n-calendar' })
|
||||||
|
await page.locator('.day-cell__row').first().waitFor({ state: 'visible', timeout: 20000 })
|
||||||
|
const cell = page
|
||||||
|
.locator('.n-calendar-cell')
|
||||||
|
.filter({ has: page.locator('.day-cell__row') })
|
||||||
|
.first()
|
||||||
|
await cell.evaluate((el) => (el as HTMLElement).click())
|
||||||
|
const modal = page.locator('.n-modal', { hasText: '交易明细' }).first()
|
||||||
|
await modal.waitFor({ state: 'visible', timeout: 20000 })
|
||||||
|
// 等待明细列表加载完成(出现条目后再截图,避免截到加载中状态)
|
||||||
|
await modal.locator('.detail-item').first().waitFor({ state: 'visible', timeout: 20000 })
|
||||||
|
await page.waitForTimeout(600)
|
||||||
|
await modal.screenshot({ path: resolve(ELEM_DIR, 'calendar-day-detail.png') })
|
||||||
|
console.log('[OK] calendar-day-detail')
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`[FAIL] calendar-day-detail: ${err}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
const browser = await launchBrowser()
|
const browser = await launchBrowser()
|
||||||
try {
|
try {
|
||||||
@@ -262,9 +753,21 @@ async function main(): Promise<void> {
|
|||||||
const context = await newContext(browser)
|
const context = await newContext(browser)
|
||||||
await injectSession(context, token)
|
await injectSession(context, token)
|
||||||
const page = await context.newPage()
|
const page = await context.newPage()
|
||||||
|
await captureCalendarMonth(page)
|
||||||
|
await captureCalendarYear(page)
|
||||||
|
await captureCalendarDetail(page)
|
||||||
|
await captureAddRecord(page)
|
||||||
await captureModals(page)
|
await captureModals(page)
|
||||||
|
await captureChannelFeishu(page)
|
||||||
await captureEditRecord(page)
|
await captureEditRecord(page)
|
||||||
|
await captureInlineCreate(page)
|
||||||
await captureAiRule(page)
|
await captureAiRule(page)
|
||||||
|
await captureAiRuleFromTransactions(page)
|
||||||
|
await captureRuleApplyPreview(page)
|
||||||
|
await captureBatchModes(page)
|
||||||
|
await captureAiRecordImage(page)
|
||||||
|
await captureBillDetail(page)
|
||||||
|
await captureChannelAi(page)
|
||||||
await captureSettingsPanels(page)
|
await captureSettingsPanels(page)
|
||||||
await captureBillParse(page)
|
await captureBillParse(page)
|
||||||
await captureTrendJump(page)
|
await captureTrendJump(page)
|
||||||
|
|||||||
@@ -9,16 +9,19 @@ import { BASE_URL, OUT_DIR } from './config.js'
|
|||||||
const PAGES: Array<[string, string, number, number, string]> = [
|
const PAGES: Array<[string, string, number, number, string]> = [
|
||||||
['login', '/login', 1440, 900, '.n-card'],
|
['login', '/login', 1440, 900, '.n-card'],
|
||||||
['register', '/register', 1440, 900, '.n-card'],
|
['register', '/register', 1440, 900, '.n-card'],
|
||||||
|
['calendar', '/calendar', 1440, 1280, '.n-calendar'],
|
||||||
['dashboard', '/dashboard', 1440, 1280, 'canvas'],
|
['dashboard', '/dashboard', 1440, 1280, 'canvas'],
|
||||||
|
['sql-query', '/smart-query', 1440, 1180, '.n-card'],
|
||||||
['transactions', '/transactions', 1720, 1200, '.n-data-table-tbody tr'],
|
['transactions', '/transactions', 1720, 1200, '.n-data-table-tbody tr'],
|
||||||
['categories', '/categories', 1440, 1100, '.n-data-table-tbody tr'],
|
['categories', '/categories', 1440, 1100, '.n-data-table-tbody tr'],
|
||||||
['tags', '/tags', 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'],
|
['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-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-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'],
|
['email-bills-messages', '/email-bills/messages', 1440, 1000, '.n-data-table-tbody tr'],
|
||||||
['bill-import', '/bill-import', 1440, 1000, '.n-card'],
|
['bill-import', '/bill-import', 1440, 1000, '.n-card'],
|
||||||
['settings', '/settings', 1440, 1320, '.config-card'],
|
['settings', '/settings', 1440, 1900, '.config-card'],
|
||||||
['ai-usage', '/ai-usage', 1440, 1000, '.n-card'],
|
['ai-usage', '/ai-usage', 1440, 1000, '.n-card'],
|
||||||
['logs-http', '/logs/http', 1440, 1000, '.n-data-table-tbody tr'],
|
['logs-http', '/logs/http', 1440, 1000, '.n-data-table-tbody tr'],
|
||||||
['logs-login', '/logs/login', 1440, 1000, '.n-data-table-tbody tr'],
|
['logs-login', '/logs/login', 1440, 1000, '.n-data-table-tbody tr'],
|
||||||
@@ -26,12 +29,14 @@ const PAGES: Array<[string, string, number, number, string]> = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 截取全部整页。
|
* 截取整页。
|
||||||
*
|
*
|
||||||
* @param page Page 实例
|
* @param page Page 实例
|
||||||
|
* @param only 仅截取指定页面名(可选,缺省截取全部)
|
||||||
*/
|
*/
|
||||||
export async function capturePages(page: Page): Promise<void> {
|
export async function capturePages(page: Page, only?: string[]): Promise<void> {
|
||||||
for (const [name, path, width, height, selector] of PAGES) {
|
const pages = only ? PAGES.filter(([name]) => only.includes(name)) : PAGES
|
||||||
|
for (const [name, path, width, height, selector] of pages) {
|
||||||
try {
|
try {
|
||||||
await page.setViewportSize({ width, height })
|
await page.setViewportSize({ width, height })
|
||||||
await page.goto(BASE_URL + path)
|
await page.goto(BASE_URL + path)
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import { fileURLToPath } from 'node:url'
|
|||||||
const currentDir = dirname(fileURLToPath(import.meta.url))
|
const currentDir = dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
/** 应用访问入口(默认 jar 一体化实例) */
|
/** 应用访问入口(默认 jar 一体化实例) */
|
||||||
export const BASE_URL = process.env.FINANCE_WEB ?? 'http://localhost:18082'
|
export const BASE_URL = process.env.LEDGERLY_WEB ?? 'http://localhost:18082'
|
||||||
/** 后端 API 地址 */
|
/** 后端 API 地址 */
|
||||||
export const API_URL = process.env.FINANCE_API ?? 'http://localhost:18082/api'
|
export const API_URL = process.env.LEDGERLY_API ?? 'http://localhost:18082/api'
|
||||||
/** 测试账号 */
|
/** 测试账号 */
|
||||||
export const USERNAME = process.env.FINANCE_USER ?? 'docuser'
|
export const USERNAME = process.env.LEDGERLY_USER ?? 'docuser'
|
||||||
export const PASSWORD = process.env.FINANCE_PASSWORD ?? 'doc123456'
|
export const PASSWORD = process.env.LEDGERLY_PASSWORD ?? 'doc123456'
|
||||||
|
|
||||||
/** Chromium 可执行文件(复用 Playwright 缓存,无需系统安装) */
|
/** Chromium 可执行文件(复用 Playwright 缓存,无需系统安装) */
|
||||||
export const CHROMIUM_BIN =
|
export const CHROMIUM_BIN =
|
||||||
@@ -24,3 +24,7 @@ export const OUT_DIR = resolve(ROOT, 'public', 'screenshots')
|
|||||||
export const ELEM_DIR = resolve(OUT_DIR, 'elements')
|
export const ELEM_DIR = resolve(OUT_DIR, 'elements')
|
||||||
/** 样例账单文件(虚构数据、未导入,用于「账单解析预览」截图) */
|
/** 样例账单文件(虚构数据、未导入,用于「账单解析预览」截图) */
|
||||||
export const SAMPLE_BILL = resolve(ROOT, 'scripts', 'samples', 'alipay_fake_preview.csv')
|
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')
|
||||||
|
|||||||
@@ -164,6 +164,9 @@ async function assumeRole(config: OssConfig): Promise<StsCredentials> {
|
|||||||
accessKeyId: config.accessKeyId,
|
accessKeyId: config.accessKeyId,
|
||||||
accessKeySecret: config.accessKeySecret,
|
accessKeySecret: config.accessKeySecret,
|
||||||
endpoint: STS_ENDPOINT,
|
endpoint: STS_ENDPOINT,
|
||||||
|
// 默认连接超时较短,网络抖动时易 ConnectTimeout,这里放宽
|
||||||
|
connectTimeout: 20000,
|
||||||
|
readTimeout: 20000,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const request = new AssumeRoleRequest({
|
const request = new AssumeRoleRequest({
|
||||||
|
|||||||
Reference in New Issue
Block a user