Compare commits

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

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

说明:
- 截图经测试环境(前端 5174 / 后端 8081,虚构数据)生成,已上传 OSS,正文以 OSS 长期地址引用;public/screenshots 目录不入库。
2026-10-14 01:07:46 +08:00
rosercode d94d4e1ed5 docs: 补充收支记录页 AI 生成规则、CORS/反向代理与邮箱检查进度时间戳
同步近期 ledgerly 功能与配置变更到文档站:新增收支记录页「AI 生成规则」入口的用户与开发说明、补充新接口与规则应用逻辑、修正 CORS/反向代理与运行环境编码说明,并更新邮箱检查进度时间戳描述。

变更内容:
- guide/transactions.md 批量操作区新增「AI 生成规则」按钮与「AI 生成规则(从勾选账单)」小节(含「同时应用到勾选账单」),引用新截图。
- guide/ai.md、guide/rules.md、guide/pages.md 补充两处生成入口与「同时应用」说明。
- dev/api.md 新增 POST /api/ai/rule/apply-transactions;dev/rule-engine.md、dev/ai.md 补充 applyToTransactions 与两个生成入口。
- dev/build-deploy.md 将 CORS 更新为放开全部来源并信任反向代理转发头,新增运行环境 UTF-8 locale 说明。
- guide/email-bills.md 补充「立即检查」进度每行带 HH:mm:ss 时间戳。
- scripts/ts/capture-elements.ts 新增 captureAiRuleFromTransactions 截图场景(交易页 AI 生成规则弹窗)。

说明:
- 新截图 elements/ai-rule-transactions.png 已上传 OSS,正文以 OSS 长期地址引用。
2026-10-13 22:39:40 +08:00
rosercode 5eb1d331f9 docs: 补充接口加解密、代码检查与安全配置等开发文档
同步 ledgerly 近期开发改动(接口请求/响应体加解密、代码检查工具、安全与稳定性修复、AI 提示词模板抽取),补齐对应开发文档并修正过时表述,同时在快速开始补充小屏适配说明。

变更内容:
- dev/api.md:新增「接口数据加密」小节,说明 RSA + AES-GCM 握手流程、信封格式、会话有效期与失效码、豁免范围及编译期开关。
- dev/architecture.md:请求处理流程补入加密会话拦截器与请求/响应体 Advice,关键设计亮点新增接口数据加密一条。
- dev/development.md:新增「代码检查」小节,登记 Spotless / Checkstyle / SpotBugs 与前端 ESLint / Prettier 配置及运行命令。
- dev/build-deploy.md:新增「安全配置」(JWT_SECRET 覆盖、CORS 放行来源),构建任务链补入前端 lint 校验说明。
- dev/ai.md:修正已重命名的提示词模板名,补充「提示词模板」片段机制与图片记账 SSE 超时、心跳说明。
- dev/project-structure.md:补充 config.crypto / util.crypto 包、prompts 资源目录、crypto-keys.json 运行时文件与交易服务拆分说明。
- guide/getting-started.md:新增「小屏与移动端适配」说明。
- guide/transactions.md:导出说明补充单次 10000 条上限。
2026-10-13 21:02:53 +08:00
rosercode ca1b8c05d5 docs: 补充统计面板图表点击跳转开关说明
统计面板图表的点击下钻交互新增了可关闭开关(默认开启),可在统计面板筛选区或设置页「外观设置」关闭;关闭后点击趋势柱/饼图不再跳转,并隐藏点击提示与饼图手型光标。本次同步更新使用与开发文档,并重截外观设置面板配图。

变更内容:
- `guide/dashboard.md`:筛选条件小节与「点击图表查看明细」小节补充开关说明(两处入口、关闭效果与实时同步)。
- `guide/settings.md`:外观设置由三组改为四组,新增「图表点击查看明细」并对其余分组重新编号;配图 `elements/panel-display.png` 重截并上传 OSS。
- `dev/preferences.md`:后端按用户同步偏好总览表加行,并新增「图表点击跳转明细」小节(接口、默认值、存储键、共享 composable 与消费入口)。
- `dev/api.md`:新增 `GET/PUT /api/setting/chart-click-navigation` 接口。
- `dev/data-model.md`:`t_user_preference` 已用偏好键补充 `chartClickNavigation`。
2026-10-13 16:02:49 +08:00
rosercode 8bfdde24f2 docs: 定时通知补充飞书自定义机器人渠道说明
定时通知新增飞书渠道后同步文档:使用文档补充飞书渠道的配置方式与示例截图,开发文档补充 FEISHU 渠道类型与发送器实现,并在功能概览、技术栈等处同步“飞书”。

变更内容:
- guide/notifications.md 增加飞书渠道配置项与获取路径,新增飞书渠道配置示例图
- dev/notifications.md 渠道表新增 FEISHU(post 富文本、可选加签),补充 webhookUrl/secret 与 hasSecret 脱敏说明
- guide/introduction.md、guide/settings.md、index.md、dev/architecture.md 同步“飞书”
- scripts/ts/capture-elements.ts 新增 captureChannelFeishu 并纳入 main 全量调用
2026-10-13 12:52:43 +08:00
rosercode b1fb2c68a9 docs: 新增日历页文档并补充记账直接新建分类/标签/渠道说明
补上此前已完成但未提交的文档更新:新增「日历」页面使用文档,并把登录后默认落地页由统计面板调整为日历,同步页面流程与偏好说明;同时补充记账弹窗内可直接新建分类/标签/渠道的说明,并登记相应截图场景。

变更内容:
- 新增 guide/calendar.md,并在 .vitepress/config.mts 侧边栏与 guide/pages.md、guide/index.md 登记日历页
- index.md、guide/getting-started.md、guide/dashboard.md、guide/pages.md 同步日历为登录后默认落地页与页面流程
- dev/preferences.md 新增日历偏好 ledgerly.calendar.view/year/month/statsScope 说明
- guide/transactions.md 补充记账弹窗内直接新建分类/标签/渠道的说明,guide/ai.md、guide/categories-tags.md、guide/channels.md 同步相关描述
- scripts/ts/capture-all.ts、capture-pages.ts、capture-elements.ts 增加日历与新建分类等截图场景,captureModals 支持按场景过滤
2026-10-13 12:52:08 +08:00
rosercode 4425a6ba04 docs: 补充收支记录批量操作说明并同步一键设置作用范围
在收支记录文档新增「批量操作」章节,逐一介绍批量操作/退出批量、一键设置、批量删除、建立关系按钮及作用范围,并同步相关文档描述。

变更内容:
- guide/transactions.md:新增「批量操作」章节,页头操作表移除已迁移的批量按钮,一键设置节改为勾选优先
- guide/ai.md:分类与标签建议的作用范围同步为「有勾选时勾选记录、未勾选时当前页」
- guide/relations.md:入口补充按钮位于列表右上角
2026-09-26 23:25:56 +08:00
rosercode 923d6db5c0 docs: 同步汇账最新功能,新增智能查数、账单关系等文档
对照 ledgerly 自文档上次更新以来的全部改动,补充新功能文档、修正品牌与版本残留,并新增下载与安装页;配图已重新截取并上传 OSS,本地截图目录不纳入版本管理。

变更内容:
- 新增「智能查数」「账单关系」两个使用文档页,并登记侧边栏与总览
- 设置页扩充为八个面板(账号安全、数据库实时进度、外观、导航等)并改为即时保存
- 收支记录补充关系列、批量操作、AI 搜索浮窗、可配置分页与「账单详情」弹窗
- 统计面板补充饼图/渠道图下钻与筛选双向联动;规则管理补充批量删除
- AI 能力补充智能查数与关系命名;新增下载与安装页
- 开发文档同步:AI 13 类调用、数据模型 24 张表、API、前端偏好、构建部署
- 统一「汇账 Ledgerly」品牌与「设置/日志」菜单文案,版本号对齐 0.1
- 截图脚本新增账单详情等场景并更新登记

说明:
- download.md 的 OSS 下载链接保持待发布产物回填,暂不调整
- 已通过 npm run docs:build 与 npm run typecheck:scripts
2026-09-26 21:57:38 +08:00
rosercode b979c6e498 refactor: 文档站品牌改名为汇账 Ledgerly,站点与脚本标识统一为 ledgerly
- 站点标题、首页与文档正文品牌名由「收支管理系统」改为「汇账 Ledgerly」
- package.json 包名改为 ledgerly-docs
- 维护脚本环境变量由 FINANCE_* 改为 LEDGERLY_*,测试库路径与示例同步
- 项目结构、构建部署、开发环境等文档中的目录名与 jar 名改为 ledgerly
- 保留在线体验域名与 OSS 截图前缀不变
2026-09-23 17:33:59 +08:00
rosercode d2e4a51f2b feat: 补充图片记账、交易渠道管理与前端偏好文档
- 新增「交易渠道管理」指南与「前端用户偏好」开发文档,并加入侧边栏导航
- 图片记账:补充多图上传、实时识别进度、结果勾选导入与「来源图片」回看,AI 配置新增视觉模型说明
- 数据模型补充 t_channel、t_ai_config/t_ai_image 等表与「类型+金额+时间」去重键说明,表数更新为 21 张
- API 文档补充 /api/channel、/api/transaction/batch、/api/ai/record/image(/stream)、/api/ai/image、/api/ai/channel 等接口
- 截图脚本新增渠道页、图片记账、来源图片与渠道 AI 辅助截图,并放宽导航超时
2026-09-21 22:19:55 +08:00
43 changed files with 1954 additions and 242 deletions
+13 -7
View File
@@ -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: '上一页',
+9 -6
View File
@@ -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 真实库)。
+63 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -27,7 +27,7 @@
- **时间兼容**:微信账单兼容 Excel 日期序列号与文本时间;支付宝在付款时间缺失时回退交易创建时间;淘宝订单同样兼容文本时间与 Excel 日期序列号。 - **时间兼容**:微信账单兼容 Excel 日期序列号与文本时间;支付宝在付款时间缺失时回退交易创建时间;淘宝订单同样兼容文本时间与 Excel 日期序列号。
- **订单聚合**:淘宝订单数据中多商品订单的订单级列(订单号、提交时间、状态、店铺、实付金额、运费)以合并单元格呈现,仅首个商品行有值;解析器按订单号分组聚合,每个订单输出一条记录,金额优先取订单级实付金额,为空时回退「Σ(商品金额×数量)+运费」。 - **订单聚合**:淘宝订单数据中多商品订单的订单级列(订单号、提交时间、状态、店铺、实付金额、运费)以合并单元格呈现,仅首个商品行有值;解析器按订单号分组聚合,每个订单输出一条记录,金额优先取订单级实付金额,为空时回退「Σ(商品金额×数量)+运费」。
- **不计收支处理**:京东流水的「不计收支」行(白条还款、小金库转入、全额退款等)标记为可导入但**不计入统计且默认不勾选**,类型按「交易说明含『退款』→ INCOME,否则 EXPENSE」推断。 - **不计收支处理**:京东流水的「不计收支」行(白条还款、小金库转入、全额退款等)标记为可导入但**不计入统计且默认不勾选**,类型按「交易说明含『退款』→ INCOME,否则 EXPENSE」推断。
- **计入统计默认值**:解析完成后按**账单来源**应用「计入统计」默认值(配置于系统设置,见 [动态数据源](/dev/database) 同级的应用配置)。采用「解析器判定 && 来源默认」语义——来源默认不计入时整批置为不计入;来源默认计入时保留解析器对「不计收支/中性」行的排除。确认导入时若请求未显式提供该字段,同样以来源默认兜底。 - **计入统计默认值**:解析完成后按**账单来源**应用「计入统计」默认值(配置于设置页,见 [动态数据源](/dev/database) 同级的应用配置)。采用「解析器判定 && 来源默认」语义——来源默认不计入时整批置为不计入;来源默认计入时保留解析器对「不计收支/中性」行的排除。确认导入时若请求未显式提供该字段,同样以来源默认兜底。
## 压缩包解密 ## 压缩包解密
+105 -42
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)接口文档
+2 -1
View File
@@ -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 条。
## 接口 ## 接口
+292
View File
@@ -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`。
+12 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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`,用于图片记账)。
- 点击「测试连接」验证配置。 - 点击「测试连接」验证配置。
![系统设置](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/settings.png) ![设置](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/settings.png)
## 功能一览 ## 功能一览
| 功能 | 入口 | 说明 | | 功能 | 入口 | 说明 |
|---|---|---| |---|---|---|
| 分类/标签建议 | 收支记录 → 一键设置 → 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 智能记账弹窗](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/ai-record.png) ![AI 智能记账弹窗](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/ai-record.png)
## 图片记账
在「智能记账」弹窗切换到「**图片记账**」标签页,上传账单或支付截图,AI 会调用**视觉模型**一次性识别其中的每一笔交易。
![图片记账](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/ai-record-image.png)
- **多图上传**:一次最多 **5 张**,单张不超过 **10MB**、总大小不超过 **30MB**;支持 JPEG / PNG / GIF / WebP。
- **粘贴上传**:也可按 `Ctrl+V`(Mac 为 `⌘V`)直接粘贴剪贴板中的图片。
- **补充说明**(可选):在下方输入框补充提示(如「这是 9 月的账单」),帮助模型理解。
- 点击「识别图片」后,弹窗内会**实时显示识别进度**(校验图片、保存、调用模型、解析、查重等步骤),耗时较长的识别过程可见可控。
- 识别完成后进入**结果弹窗**:
![图片记账结果](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/ai-record-result.png)
- 结果以表格逐条展示,**默认全选**(疑似重复项默认不勾选),可直接编辑**类型、金额、分类、时间、备注**。
- **来源图**列标注该笔交易来自第几张上传图片;**重复**列标记与已有记录或本批其它记录「类型+金额+时间」一致的疑似重复项。
- **原始数据**列可查看模型从图片中提取的原始字段(时间、对方、商品、订单号等),用于核对。
- 顶部摘要说明识别到的账单内容;勾选后点击「导入勾选」批量写入收支记录(导入时会再次按去重键跳过重复)。
- 未识别到有效交易时会给出提示,不会写入任何记录。
> 图片记账使用的是视觉模型,可在「设置 → AI 模型配置」中单独配置(默认 `deepseek-flash`)。详见[设置](/guide/settings)。
导入后的记录可在[收支记录](/guide/transactions)的「**详情**」弹窗中回看来源图片(默认加载缩略图,可点「加载原图」切换为原图)。
## AI 统计搜索 ## AI 统计搜索
在统计面板顶部的 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 调用汇总与明细。
![模型使用情况](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/ai-usage.png) ![模型使用情况](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/ai-usage.png)
@@ -91,5 +134,5 @@ API Key 仅保存在本地数据库中,仅用于向你配置的接口地址发
## 说明 ## 说明
- AI 建议结果仅供参考,应用前可逐条取消勾选。 - AI 建议结果仅供参考,应用前可逐条取消勾选。
- 关闭「启用 AI 建议」后,系统不会发起任何 AI 请求,相关入口也会隐藏。 - 关闭「启用 AI 建议」后,系统不会发起任何 AI 请求,相关入口也会隐藏(分类/标签/渠道的**手动新建**入口不受影响,始终可用)。
- 账单类型识别(邮箱账单)使用的是**发件人规则**而非 AI,结果更确定、可预期。 - 账单类型识别(邮箱账单)使用的是**发件人规则**而非 AI,结果更确定、可预期。
+66
View File
@@ -0,0 +1,66 @@
# 日历
「日历」是登录后的默认首页,用月视图与年视图直观查看每天的收支情况。左侧菜单默认置于首项,访问 `/` 也会自动进入本页。
![日历(月视图)](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/calendar.png)
## 月视图
- 每天展示当天**收入**(绿色,`+`)、**支出**(红色,`-`)与**结余**;同一天既有收入又有支出时显示三行,否则只显示对应的一侧。
- **今天**以高亮标记;点击网格中相邻月份的灰色日期会自动切换到该月。
- 顶部四张卡片显示**当月合计**:总收入、总支出、结余、交易笔数。
## 切换月份与视图
表头提供:
- 「‹」「›」:切换上一月 / 下一月(年视图下为上一年 / 下一年);
- **年份 / 月份下拉**:直接跳转到任意年月;
- **今天**:回到当前月并选中今天;
- **月视图 / 年视图**:切换视图。
## 年视图
年视图以 12 个月的卡片展示每月收入、支出与结余,无数据的月份显示「无数据」,当前月高亮。点击任意月份卡片即可进入该月的月视图。
![日历年视图](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/calendar-year.png)
某一年完全没有数据时,会显示「本年暂无交易数据」的空状态提示。
## 统计口径
表头的「**统计口径**」下拉控制参与汇总的记录范围:
| 选项 | 含义 |
|---|---|
| 仅计入统计(默认) | 只统计「计入统计」的记录 |
| 全部记录 | 计入与不计入的记录都统计 |
| 仅不计入 | 只统计「不计入统计」的记录 |
切换后日历中的金额、顶部合计与点击日期弹出的**当日明细**都会随之变化:明细只列出符合当前口径的记录,与汇总口径保持一致。
## 查看某天的明细
点击日历中的某一天,会弹出当天的**交易明细浮窗**:
- 顶部显示**当日合计**(收入 / 支出 / 结余);
- 下方逐条列出当天交易,每条在主行显示时间、分类、渠道与**金额**(收绿支红);
- 点击任意条目可打开该笔账单的**完整详情**(标签、账单关系、来源图片、原始数据等,为只读);
- 底部「**在收支记录中查看**」会携带当天日期与当前统计口径跳转到[收支记录](/guide/transactions),便于继续编辑整理。
![当日明细浮窗](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/calendar-day-detail.png)
> 当日明细(按当前统计口径)最多展示前 50 条,超出时浮窗内会提示总数,可点「在收支记录中查看」查看全部。
## 链接分享与偏好
- 当前视图与年月会同步到地址栏(形如 `?view=month&y=2026&m=9`),刷新页面或把链接分享给他人,都能还原到对应月份与视图。
- **视图**(月 / 年)、**统计口径**与**最近浏览的年月**会被记住,下次进入自动还原;地址栏带参数时以参数为准。
## 小技巧
- 想知道某一天具体花在哪,直接点该日期,先看当日合计,再逐条核对明细。
- 想看全年的分布,切到「年视图」浏览各月汇总,再点开支出最高的月份查看当天明细。
- 不计入统计的记录(如借款、账户间转账)默认不参与汇总,当日明细也不会列出;如需一并查看,把统计口径切换为「全部记录」。
> 想从更大的时间跨度看趋势与占比,可使用[统计面板](/guide/dashboard)的图表与筛选。
+7
View File
@@ -26,9 +26,12 @@
- **编辑**:在分类行点击「编辑」修改。 - **编辑**:在分类行点击「编辑」修改。
- **删除**:点击「删除」并确认;已使用该分类的记录会变为「未分类」。 - **删除**:点击「删除」并确认;已使用该分类的记录会变为「未分类」。
- **批量删除**:点右上角「**批量操作**」进入批量选择模式,支出与收入两张表格都会出现勾选列,勾选后点「**批量删除**」并确认即可一次删除多个(两表勾选会合并处理);点「**退出批量**」退出并清空勾选。
- **图标**:使用 Emoji 选择器挑选,也可自定义输入。 - **图标**:使用 Emoji 选择器挑选,也可自定义输入。
- **AI 生成分类**:启用 AI 后,输入用途描述(如「日常通勤打车地铁」),点「AI 生成」自动填充名称、类型与图标。 - **AI 生成分类**:启用 AI 后,输入用途描述(如「日常通勤打车地铁」),点「AI 生成」自动填充名称、类型与图标。
![分类批量删除](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/categories-batch.png)
新注册用户会自动获得一套默认分类(如餐饮、交通、购物、居住、通讯、娱乐、医疗、教育等)。 新注册用户会自动获得一套默认分类(如餐饮、交通、购物、居住、通讯、娱乐、医疗、教育等)。
## 标签管理 ## 标签管理
@@ -46,6 +49,9 @@
- **颜色**:使用颜色选择器,内置常用色板;启用 AI 后点「AI 推荐」可按标签名称推荐颜色。 - **颜色**:使用颜色选择器,内置常用色板;启用 AI 后点「AI 推荐」可按标签名称推荐颜色。
- 标签以彩色标签形式在收支记录、导入预览等位置展示。 - 标签以彩色标签形式在收支记录、导入预览等位置展示。
- 删除标签会从所有交易记录中移除该标签,操作前会二次确认。 - 删除标签会从所有交易记录中移除该标签,操作前会二次确认。
- **批量删除**:点右上角「**批量操作**」后表格出现勾选列,勾选要删除的标签后点「**批量删除**」并确认,即可一次删除多个;点「**退出批量**」退出并清空勾选。批量删除同样会从所有交易记录中移除这些标签。
![标签批量删除](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/tags-batch.png)
## 使用建议 ## 使用建议
@@ -53,3 +59,4 @@
- 建议为以下场景建立标签:报销/待报销、旅行、大额消费、订阅、礼物、家庭、工作等。 - 建议为以下场景建立标签:报销/待报销、旅行、大额消费、订阅、礼物、家庭、工作等。
- 需要按规则自动归类时,配合[规则管理](/guide/rules)使用:规则命中后会自动应用分类与标签。 - 需要按规则自动归类时,配合[规则管理](/guide/rules)使用:规则命中后会自动应用分类与标签。
- 分类与标签都支持在[账单导入](/guide/transactions)预览阶段批量指定为默认值。 - 分类与标签都支持在[账单导入](/guide/transactions)预览阶段批量指定为默认值。
- 在[新增/编辑记录](/guide/transactions#手动添加记录)弹窗中,可直接点分类/标签行右侧的「新建分类」「新建标签」创建并选用,无需先到本页维护。
+69
View File
@@ -0,0 +1,69 @@
# 交易渠道管理
「交易渠道」用于标记一笔收支的付款或收款途径,既包括账单导入时的来源,也包括人工记账时选择的渠道。系统内置一组常用渠道,并支持按账号自定义。
## 入口
在[收支记录](/guide/transactions)页顶部操作区点击「**渠道管理**」进入(独立页面,不占用左侧菜单)。
![交易渠道管理](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/channels.png)
页面顶部提供返回按钮,可一键回到收支记录页。
## 内置渠道
以下渠道由系统内置,无需创建,名称随界面语言自动显示:
| 编码 | 名称 |
|---|---|
| `ALIPAY` | 支付宝 |
| `WECHAT` | 微信 |
| `MEITUAN` | 美团 |
| `TAOBAO` | 淘宝 |
| `JD` | 京东 |
| `CMB` | 招商银行 |
| `BOCOM` | 交通银行 |
| `CASH` | 现金 |
> 账单导入时,系统会按账单来源自动写入对应的内置渠道;人工记账默认可选「现金」或任意自定义渠道。
## 自定义渠道
除内置渠道外,你可以新建自己的渠道(例如「某张储蓄卡」「公司报销」等)。
### 新建渠道
点击右上角「**新建渠道**」,输入名称后确认。
![新建渠道](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/channel-create.png)
- 渠道名称在**当前账号内不可重复**。
- 渠道编码由系统生成且**稳定不变**,交易记录按编码关联。
### 重命名渠道
在列表行点击「编辑」即可改名。**改名不影响历史账单**——所有已使用该渠道的记录会同步显示新名称。
### 删除渠道
在列表行点击「删除」即可删除。
- 若该渠道**已被账单使用**,系统会拒绝删除并提示使用数量,避免历史记录出现无效渠道。
- 列表中的「使用账单数」列可直观看到每个渠道被多少笔记录使用。
## 在哪些地方使用
自定义渠道创建后,可在以下场景直接选用:
- **新增 / 编辑记录**:交易渠道下拉中出现在内置渠道之后;也可点右侧「新建渠道」直接创建并选用(不受 AI 状态影响)。
- **筛选与统计**:收支记录页筛选、统计面板的渠道维度与筛选均可选择自定义渠道,图表与分组会展示其名称。
- **AI 能力**:智能查询、统计搜索与智能记账会把当前账号的自定义渠道一并作为可选渠道;新增/编辑记录时还可用「渠道 · AI 推荐」选择已有渠道,或「渠道 · AI 新建」由描述生成并创建渠道。
- **导出**:Excel 导出时渠道列展示自定义渠道名称。
> 删除前请先确认没有账单仍在使用该渠道;如需保留历史,建议改名而非删除。
## 相关
- [收支记录](/guide/transactions):记账、筛选与账单导入。
- [统计面板](/guide/dashboard):按渠道维度统计。
- [AI 能力](/guide/ai):自然语言查询与图片记账。
+32 -17
View File
@@ -1,6 +1,6 @@
# 统计面板 # 统计面板
「统计面板」是登录后的默认首页,用于查看指定时间范围内的收支汇总与图表。 「统计面板」用于查看指定时间范围内的收支汇总与图表,可从左侧菜单进入(登录后的默认首页为[日历](/guide/calendar))。
![统计面板](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/dashboard.png) ![统计面板](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/dashboard.png)
@@ -23,15 +23,17 @@
## AI 搜索 ## AI 搜索
启用 AI 后,页面顶部会出现「**AI 搜索**」按钮,点击打开搜索弹窗。用一句自然语言描述统计意图,系统会调用大模型解析为统计参数并自动回填、刷新图表,例如: 启用 AI 后,页面顶部会出现「**AI 搜索**」按钮,点击打开一个**可拖拽、可缩放的浮窗**(非模态)。用一句自然语言描述统计意图,系统会调用大模型解析为统计参数并自动回填、刷新图表,例如:
- 「上个月餐饮花了多少」→ 时间范围 + 分类 - 「上个月餐饮花了多少」→ 时间范围 + 分类
- 「近半年交通支出趋势,按周看」→ 时间范围 + 分类 + 按周粒度 - 「近半年交通支出趋势,按周看」→ 时间范围 + 分类 + 按周粒度
- 「今年各类支出占比」→ 时间范围 + 类型 - 「今年各类支出占比」→ 时间范围 + 类型
![AI 统计搜索弹窗](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/ai-search.png) ![AI 统计搜索浮窗](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/ai-query-window.png)
可解析并回填的参数包括:**时间范围、收支类型、分类、标签、交易渠道、金额区间、备注、统计口径**与**趋势粒度**。弹窗内提供示例问题,输入框会记住历史记录(可一键清空);回车或点击「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#外观设置)中关闭「点击图表查看明细」(默认开启)后,点击任何图表元素都不再跳转,趋势图的「点击柱子查看对应明细」提示与饼图的手型光标也会一并隐藏;两处开关实时同步,重新开启即恢复下钻。
![点击趋势柱状图跳转收支记录](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/dashboard-trend-jump.png) ![点击趋势柱状图跳转收支记录](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/dashboard-trend-jump.png)
跳转后可直接查看该时间段内的对应明细;筛选条件会照常记忆,收支记录页原有的每页条数与排序偏好不受影响。 页面底部的「**查看收支记录**」按钮则会携带**当前全部筛选条件**跳转,便于核对整体明细。
> 从[收支记录](/guide/transactions)点「查看统计」也会跳回本页,并还原收支记录当前的筛选条件(时间范围会切换为「自定义」)。
跳转后可直接查看对应明细;筛选条件会照常记忆,收支记录页原有的每页条数与排序偏好不受影响。
## 小技巧 ## 小技巧
- 想知道「上个月餐饮花了多少」,可把时间范围设为「本月」或自定义,并在分类中只勾选「餐饮」。 - 想知道「上个月餐饮花了多少」,可把时间范围设为「本月」或自定义,并在分类中只勾选「餐饮」。
- 不计入统计的记录(如借款、账户间转账)默认不会计入汇总;如需查看全部,把统计口径切换为「全部记录」。 - 不计入统计的记录(如借款、账户间转账)默认不会计入汇总;如需查看全部,把统计口径切换为「全部记录」。
- 想快速核对某月、某周或某天的明细,直接点击「收支趋势」上对应的列(柱子或该列空白处),即可带着时间范围跳到收支记录。 - 想快速核对某月、某周或某天的明细,直接点击「收支趋势」上对应的列(柱子或该列空白处),即可带着时间范围跳到收支记录。
- 点击分类或渠道饼图的某个扇区,可一键下钻到该分类 / 渠道的明细;点「查看收支记录」则带上当前全部筛选。
+109
View File
@@ -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)。
+1 -1
View File
@@ -32,7 +32,7 @@
行操作: 行操作:
- **测试连接**:验证 IMAP 配置。 - **测试连接**:验证 IMAP 配置。
- **立即检查**:实时展示检查进度,完成后提示新增待处理账单数量。 - **立即检查**:实时展示检查进度(每行带 `HH:mm:ss` 时间戳),完成后提示新增待处理账单数量。
- **编辑 / 删除**。 - **编辑 / 删除**。
## 发件人规则 ## 发件人规则
+4 -4
View File
@@ -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 @@
### 忘记密码怎么办? ### 忘记密码怎么办?
当前版本未提供自助找回。可在数据库中重置对应用户的密码哈希,或新建账号后迁移数据。 登录后可在「设置 → 账号安全」中,凭**原密码**修改密码(也支持一键随机生成新密码)。若已完全忘记密码,当前版本暂未提供自助找回,可在数据库中重置该用户的密码哈希,或新建账号后迁移数据。
### 数据存在哪里? ### 数据存在哪里?
+15 -4
View File
@@ -33,7 +33,7 @@
![数据库初始化](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/settings.png) ![数据库初始化](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/settings.png)
首次进入时按引导选择数据库类型并填写连接信息,点击「保存并应用」即可。登录后可通过右上角用户菜单 →「数据库设置」随时切换数据库;切换后原数据库的数据保留,不做自动迁移。 首次进入时按引导选择数据库类型并填写连接信息,点击「保存并应用」即可。登录后可通过右上角用户菜单 →「设置」→「数据库配置」随时切换数据库;切换后原数据库的数据保留,不做自动迁移。
## 注册与登录 ## 注册与登录
@@ -44,22 +44,33 @@
![注册卡片](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/register-card.png) ![注册卡片](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/register-card.png)
注册功能可在「系统设置 → 注册设置」中开启或关闭。新注册用户会自动获得一套默认分类、标签与邮箱发件人规则。 注册功能可在「设置 → 注册设置」中开启或关闭。新注册用户会自动获得一套默认分类、标签与邮箱发件人规则。
## 界面语言 ## 界面语言
系统支持**简体中文、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
View File
@@ -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
View File
@@ -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
View File
@@ -1,6 +1,6 @@
# 系统日志 # 日志
系统日志通过右上角用户菜单 →「系统日志」进入,包含三个标签页:**访问日志**、**登录日志**、**运行日志**。页头右上角可选择日志保留时长(30 天 / 90 天 / 180 天 / 永久保留)。 日志通过右上角用户菜单 →「日志」进入,包含三个标签页:**访问日志**、**登录日志**、**运行日志**。页头右上角可选择日志保留时长(30 天 / 90 天 / 180 天 / 永久保留)。
## 访问日志 ## 访问日志
+8 -3
View File
@@ -1,8 +1,8 @@
# 定时提醒与通知 # 定时提醒与通知
系统可按周期提醒你导入新账单,并通过邮箱或 Server 酱推送通知。配置入口为「系统设置 → 定时通知」。 系统可按周期提醒你导入新账单,并通过邮箱、Server 酱或飞书推送通知。配置入口为「设置 → 定时通知」。
![系统设置](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/settings.png) ![设置](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/settings.png)
## 提醒周期 ## 提醒周期
@@ -13,15 +13,20 @@
## 推送渠道 ## 推送渠道
点击「添加渠道」新增,支持两种类型: 点击「添加渠道」新增,支持三种类型:
| 渠道 | 配置项 | | 渠道 | 配置项 |
|---|---| |---|---|
| Server 酱 | SendKey(在 sct.ftqq.com 获取),可推送到个人微信 | | Server 酱 | SendKey(在 sct.ftqq.com 获取),可推送到个人微信 |
| 邮箱 | SMTP 服务器、端口、邮箱账号、授权码、收件人、是否使用 SSL | | 邮箱 | SMTP 服务器、端口、邮箱账号、授权码、收件人、是否使用 SSL |
| 飞书 | Webhook 完整地址(飞书群「设置 → 群机器人 → 添加机器人 → 自定义机器人」获取);若机器人开启了「签名校验」,再填写签名密钥,可推送到飞书群 |
![添加渠道弹窗](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/add-channel.png) ![添加渠道弹窗](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/add-channel.png)
飞书渠道配置示例(类型选择「飞书」,填写 Webhook 地址与可选的签名密钥):
![飞书渠道配置](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/add-channel-feishu.png)
每个渠道可单独「测试」「编辑」「删除」,并可通过启用开关控制是否参与发送。 每个渠道可单独「测试」「编辑」「删除」,并可通过启用开关控制是否参与发送。
面板右上角的「立即发送」可向所有启用渠道立即发送一次提醒,用于验证配置。 面板右上角的「立即发送」可向所有启用渠道立即发送一次提醒,用于验证配置。
+29 -13
View File
@@ -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、通知八类配置;模型用量与日志分别从设置页与顶部菜单进入。
- **交易渠道管理**是收支记录页的附属页;**账单关系**是收支记录内的弹窗,两者都在整理完成后回到收支记录继续操作。
+68
View File
@@ -0,0 +1,68 @@
# 账单关系
「账单关系」用于把**互相关联的多笔收支**组合在一起,例如「交通卡充值」与后续的乘车扣款、分期还款的每一期、支付链路上的多笔记录。建立关系只做关联,不会改动金额,也不影响统计。
## 两种关系类型
| 类型 | 含义 | 典型场景 |
|---|---|---|
| **父子关系** | 一个「父账单」(资金池)对应多个「子账单」(从资金池流出) | 交通卡充值 → 多次乘车;账户充值 → 逐笔消费 |
| **同级关系** | 多笔地位平等的账单聚合在一起 | 一笔消费在多个平台留下的关联记录、分次归还 |
- 父子关系中,**必须且只能有一条父账单**,其余为子账单。
- 同级关系中,所有成员地位相同,按加入顺序依次连接。
## 入口
| 入口 | 说明 |
|---|---|
| 收支记录列表「**关系**」列 | 该行所属关系组数量,点击「N 个关系」打开关系管理弹窗 |
| 收支记录列表右上角「**批量操作**」 | 进入多选模式,勾选**至少两条**后点「建立关系」,直接新建关系组 |
| 账单**详情**弹窗 | 「账单关系」分组内查看该笔账单的全部关系 |
![新建账单关系弹窗](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/relation-editor.png)
## 新建与编辑关系
1. 选择**关系类型**(父子关系 / 同级关系)。
2. 填写**关系名称**(可选,如「交通卡充值」)与**备注**。
3. 点「添加账单」,在分页选择弹窗中勾选要关联的账单(支持备注搜索)。
4. 父子关系下,用单选指定其中一条为**父账单**(从批量操作进入时,默认以被操作的那条为父账单,且不可移除);同级关系无需指定,成员按列表顺序排列。
5. 点击「保存」。
约束与说明:
- 一个关系组**至少包含两条账单**,成员上限 50 条。
- **一笔账单不能同时加入两个同类型的关系组**;如需更换,请先在原关系组中解除。
- **关系名称会记住历史**:再次新建同名类型的关系时,名称输入框会自动给出历史名称建议(按最近使用、去重,最多 50 条)。
- 关系类型会被**记忆**:下次新建时默认沿用上次选择的类型。
## 关系图
关系管理弹窗中,每个关系组都会以自绘的关系图展示:
- 节点为圆形,**支出为红色、收入为绿色**;当前正在查看的账单以蓝色描边且节点更大。
- **父子关系**:父账单居中在上,子账单分行排列,父 → 子连线带箭头。
- **同级关系**:成员按顺序链式连接,边上标注「关联」。
- 节点内显示金额,节点下方显示备注或分类;鼠标悬停可查看完整信息。成员较多时自动换行,图形自适应宽度。
![账单关系图](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/relation-panel.png)
## AI 生成名称与备注
启用 AI 后,新建/编辑关系弹窗中会出现「**AI 生成**」按钮。系统会把成员账单的**收支类型、交易渠道、分类名称与备注**发送给模型(**不包含金额**),由其归纳出一个关系名称与简短备注,并自动填入;你可以在保存前修改。
> 成员少于两条时无法生成,会提示先添加账单。生成的名称会覆盖名称框(备注仅在返回非空时覆盖)。
## 删除关系
在关系组卡片上点「删除」,确认后**仅解除关联**,不会删除任何账单。删除单笔账单时,系统会自动把它从所属关系组中移除;若某个关系组剩余成员不足两条、或父子关系不再有父账单,该关系组会自动解散。
## 与统计的关系
账单关系只用于**查看与整理**,不参与统计口径。每笔账单是否计入统计,仍由各自的「计入统计」开关决定,详见[收支记录](/guide/transactions)。
## 相关
- [收支记录](/guide/transactions):记录管理与批量操作。
- [AI 能力](/guide/ai):AI 生成关系名称、分类标签建议等。
+30 -2
View File
@@ -52,6 +52,32 @@
点击「搜索」按条件查询,「清空」重置全部条件。新增规则后回到第一页,便于查看新规则。 点击「搜索」按条件查询,「清空」重置全部条件。新增规则后回到第一页,便于查看新规则。
## 多选与批量删除
为保持列表清爽,规则表格默认不显示勾选框。需要进行批量操作时,点右上角「**批量操作**」进入批量选择模式,此时每行前会出现勾选框,表头可**全选当前页**;完成后点「**退出批量**」退出并清空勾选。
- 勾选若干规则后,点右上角「**批量删除**」并确认,即可一次性删除选中的规则。
- 未勾选任何规则时「批量删除」不可点击。
> 删除规则只影响后续导入与手动应用,已应用的历史记录分类与标签不会被撤销。
## 一键应用到匹配账单
规则默认只在**账单导入**时对新增记录生效,不会自动改写已有记录。如需把规则应用到**历史账单**,可在规则列表使用「应用」:
1. **单条规则**:点击规则行的「**应用**」按钮。
2. **多条规则**:点右上角「**批量操作**」进入批量选择模式,勾选若干规则后点「**应用匹配账单**」。
两种方式都会先弹出**预览**:按规则条件(类型 + 关键词 + 金额区间)列出全部命中的账单,并显示每条账单的**当前分类/标签 → 目标分类/标签**,以及命中总数、将更新分类数与将追加标签数;确认后点「**确认应用**」执行。
![应用规则到匹配账单](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/rule-apply-preview.png)
- **分类**:覆盖写入(仅当规则分类与账单收支类型一致时)。
- **标签**:追加到账单已有标签之后,**不会删除**原有标签。
- 匹配范围为**全部历史账单**;**停用的规则**也可手动应用(应用后再启用不影响本次结果)。
- 命中超过 500 条时预览仅展示前 500 条,应用仍作用于全部命中;重复执行不会产生重复标签(幂等)。
- 全程本地运算,不调用 AI。
## AI 生成规则 ## AI 生成规则
启用 AI 后,规则管理页右上角会出现「**AI 生成规则**」按钮,可让 AI 依据**已打标签的记录**自动归纳出「关键词 → 分类/标签」规则,无需手工逐条编写。 启用 AI 后,规则管理页右上角会出现「**AI 生成规则**」按钮,可让 AI 依据**已打标签的记录**自动归纳出「关键词 → 分类/标签」规则,无需手工逐条编写。
@@ -62,12 +88,14 @@
![AI 生成规则 · 选择记录](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/ai-rule-select.png) ![AI 生成规则 · 选择记录](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/ai-rule-select.png)
2. **预览规则**:AI 依据所选记录的**类型、备注、交易渠道、已有分类与标签**归纳出规则建议,展示规则名称、关键词、目标分类与标签、理由。名称与关键词可编辑,勾选后点击「创建勾选规则」批量创建;系统会自动创建建议中不存在的新分类与标签。 2. **预览与编辑规则**:AI 依据所选记录的**类型、备注、交易渠道、已有分类与标签**归纳出规则建议,展示规则名称、类型、关键词、金额区间、目标分类与标签、理由。所有字段都可**直接编辑**(名称、类型、关键词、金额区间、分类、标签),AI 建议的新分类/新标签以下拉里的「**新建:xxx**」选项保留;勾选后点击「创建勾选规则」批量创建,系统会自动创建建议中不存在的新分类与标签。
![AI 生成规则 · 预览规则](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/ai-rule-preview.png) ![AI 生成规则 · 预览规则](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/ai-rule-preview.png)
> 此外,也可在[收支记录页的批量操作区](/guide/transactions#ai-生成规则从勾选账单)勾选账单后直接生成规则(不要求已打标签),并可选「同时应用到勾选账单」(默认开启,会记住上次选择)。
::: tip 与已有规则去重 ::: tip 与已有规则去重
AI 建议若与**数据库中已有规则重复**(按「类型 + 关键词 + 分类 + 标签集合」判定),会标注「**已存在**」,**默认不勾选且不可勾选**,无法导入,避免产生重复规则。 AI 建议若与**数据库中已有规则重复**(按「类型 + 关键词 + 分类 + 标签集合」判定),会标注「**已存在**」,默认不勾选且不可勾选。**编辑规则后会自动重新判定**:改成不重复会自动勾选,改成重复会自动取消勾选并禁用,避免产生重复规则。
::: :::
## 示例 ## 示例
+53 -7
View File
@@ -1,8 +1,20 @@
# 系统设置 # 设置
系统设置通过右上角用户菜单 →「系统设置」进入,包含六个配置面板。 「设置」通过右上角用户菜单 →「设置」进入,包含八个配置面板。
![系统设置](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/settings.png) ![设置](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/settings.png)
## 账号安全
![账号安全面板](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/panel-account-security.png)
用于**修改当前登录账号的密码**:
- 输入**原密码**、**新密码**与**确认新密码**,点「修改密码」提交。
- 新密码长度需为 **6–50 个字符**,且不能与原密码相同。
- 可点「**随机生成**」自动生成一个 12 位、含大小写字母与数字的密码(已排除易混淆字符),生成后会明文显示并可一键**复制**;修改成功后请使用新密码登录。
> 修改密码仅校验原密码,**不提供**通过邮箱等方式的密码找回。
## 数据库配置 ## 数据库配置
@@ -13,6 +25,8 @@
- **MySQL**:主机、端口、数据库名、用户名、密码。 - **MySQL**:主机、端口、数据库名、用户名、密码。
- 提供「测试连接」与「保存并应用」。保存后立即生效并自动建表;已配置时密码留空表示不修改。 - 提供「测试连接」与「保存并应用」。保存后立即生效并自动建表;已配置时密码留空表示不修改。
测试与保存时会实时展示**执行进度**:连接测试、建表、结构迁移、数据源切换等步骤会以进度日志逐条推送,处理较久时请保持窗口打开,完成后点「关闭」即可。
系统支持启动后再配置数据库:未配置时前端会引导进入初始化页面,无需启动前准备。登录后也可随时切换数据库,切换后原数据库数据保留、不做自动迁移。 系统支持启动后再配置数据库:未配置时前端会引导进入初始化页面,无需启动前准备。登录后也可随时切换数据库,切换后原数据库数据保留、不做自动迁移。
## 注册设置 ## 注册设置
@@ -41,12 +55,43 @@
![外观设置面板](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/panel-display.png) ![外观设置面板](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/panel-display.png)
选择「不计入统计」的记录在[收支记录](/guide/transactions)列表中的展示方式: 外观设置包含四组配置,**修改后自动保存**,无需再点保存按钮:
**1. 不计入统计的记录样式**:选择「不计入统计」的记录在[收支记录](/guide/transactions)列表中的展示方式:
- **删除线(默认)**:整行加删除线,类型与金额列保持原样。 - **删除线(默认)**:整行加删除线,类型与金额列保持原样。
- **灰色标签**:类型列显示灰色「不计入」标签,金额列置灰且不显示正负号。 - **灰色标签**:类型列显示灰色「不计入」标签,金额列置灰且不显示正负号。
面板内提供**实时预览**,切换选项即可看到效果;该偏好按**当前账号**保存,登录后生效。 面板内提供**实时预览**,切换选项即可看到效果。
**2. 显示已隐藏备注的记录**(默认关闭):
- 关闭时,备注被隐藏的记录不会出现在收支记录列表与 Excel 导出中。
- 开启后,这些记录会正常显示(备注仍为 `******`,可临时「查看」)。
**3. 图表点击查看明细**(默认开启):
- 开启后,点击[统计面板](/guide/dashboard)的趋势柱子或饼图扇区可跳转查看对应收支明细。
- 关闭后图表仅用于展示:点击不再跳转,趋势图的「点击柱子查看对应明细」提示与饼图的手型光标一并隐藏。
- 该开关在统计面板的**筛选条件**区也提供,两处修改实时同步。
**4. 表格每页条数**:
- **表格每页条数选项**:所有表格分页共用的候选列表,默认 `10 / 20 / 50 / 100`,可自行增删(最多 10 项)。
- **默认每页条数**:从候选中选择,作为各表格未单独记忆时的默认值(默认 20)。
- 每张表格会分别记住上次选择的每页条数;本机记忆优先于全局默认值。
## 导航设置
![导航设置面板](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/panel-nav-menu.png)
调整左侧导航的**显示顺序与显隐**(按当前账号保存,跨设备生效):
- **拖动**每项左侧的手柄即可调整顺序,修改后自动保存。
- 用开关控制各项的显示或隐藏。
- 至少需保留一个可见项;「**恢复默认**」可还原为默认顺序与全部显示。
> 隐藏只移除菜单入口,页面仍可通过地址直接访问。当 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 @@
![定时通知面板](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/panel-notification.png) ![定时通知面板](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/panel-notification.png)
配置账单导入提醒的周期、时间与推送渠道(Server 酱 / 邮箱),并可查看发送日志。详见[定时提醒与通知](/guide/notifications)。 配置账单导入提醒的周期、时间与推送渠道(Server 酱 / 邮箱 / 飞书),并可查看发送日志。详见[定时提醒与通知](/guide/notifications)。
## 相关 ## 相关
- [系统日志](/guide/logs):查看访问、登录与运行日志。 - [日志](/guide/logs):查看访问、登录与运行日志。
- [快速开始](/guide/getting-started):数据库配置与注册登录。 - [快速开始](/guide/getting-started):数据库配置与注册登录。
+74
View File
@@ -0,0 +1,74 @@
# 智能查数
「智能查数」让你用**自然语言**直接查询账本,由 AI 生成一条**只读 SQL** 并执行,结果以统计卡片、表格或图表展示,无需自己拼筛选条件。
> 这是与[统计面板的「AI 搜索」](/guide/dashboard#ai-搜索)不同的能力:AI 搜索是「自然语言 → 统计面板筛选条件」,智能查数则是「自然语言 → SQL 查询」,适合更自由、更灵活的问答。
## 入口与前提
- 先在[设置](/guide/settings#ai-模型配置)中**启用 AI** 并配置 API Key。
- 启用后,左侧菜单会出现「**智能查数**」(未启用时不显示)。
- 直接访问路由 `/smart-query` 也可进入。
![智能查数](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/sql-query.png)
## 提问
在页面顶部的输入框用一句自然语言描述想查的内容,例如:
- 上个月一共花了多少钱
- 按分类统计今年的支出
- 今年收入最高的一个月
- 各渠道的支出占比
- 本月支出最高的 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
View File
@@ -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 条账单**」——命中规则时会**覆盖这些账单的分类并追加标签**(不改动备注与金额)。该开关**默认开启**,并会记住上次选择。生成与「同时应用」全程不发送金额、原始账单数据等敏感字段。
![AI 生成规则(收支记录)](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/ai-rule-transactions.png)
> 与[规则管理](/guide/rules#ai-生成规则)页的「AI 生成规则」(依据**已打标签记录**归纳)不同,此入口直接以**勾选的账单**为依据,不要求已打标签;两处的规则建议都可编辑、都会对与已有规则重复的建议标记「已存在」并禁止导入,且编辑后实时重算。
## 账单导入 ## 账单导入
「收支记录」页支持把支付宝、微信、美团、淘宝、京东、招商银行、交通银行的账单文件解析并导入为收支记录。 「收支记录」页支持把支付宝、微信、美团、淘宝、京东、招商银行、交通银行的账单文件解析并导入为收支记录。
@@ -90,39 +116,50 @@
## 筛选与 AI 智能查询 ## 筛选与 AI 智能查询
**常用筛选**:类型、分类(含「未分类」)、日期范围、备注关键字、交易渠道。 **常用筛选**:类型、分类(含「未分类」)、日期范围、备注关键字、交易渠道。交易渠道下拉包含内置渠道与账号下的[自定义渠道](/guide/channels)。
**更多筛选**:标签(含「无标签」)、计入统计、金额区间。 **更多筛选**:标签(含「无标签」)、计入统计、金额区间。
启用 AI 后,顶部会出现 **AI 智能查询**输入框,可用自然语言查询,例如: 启用 AI 后,顶部会出现「**AI 搜索**」入口,点击打开一个**可拖拽、可缩放的浮窗**(非模态,打开后仍可继续操作主界面)。在浮窗中用自然语言查询,例如:
- 「上个月餐饮」 - 「上个月餐饮」
- 「超过500的支出」 - 「超过500的支出」
- 「待报销的支出」 - 「待报销的支出」
- 「微信支付」 - 「微信支付」
解析结果会直接应用为筛选条件并立即查询。筛选条件与每页条数、排序方式会自动记忆,下次进入自动还原。 浮窗输入框支持多行,按 `Ctrl + Enter`(Mac 为 `⌘ + Enter`)提交;解析结果会直接应用为筛选条件并立即查询。浮窗的位置与大小会被记住,输入时还能快速选择历史提问。筛选条件与每页条数、排序方式会自动记忆,下次进入自动还原。
![AI 查询浮窗](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/ai-query-window.png)
## 表格与行操作 ## 表格与行操作
表格展示:交易时间、类型、分类、金额、交易渠道、备注、标签、计入统计开关与操作。 表格展示:交易时间、类型、分类、金额、交易渠道、备注、标签、计入统计开关、关系与操作。
- **备注隐藏**:可将敏感备注持久隐藏为 `******`,需要时临时「查看」或「取消隐藏」。 - **备注隐藏**:可将敏感备注持久隐藏为 `******`,需要时临时「查看」或「取消隐藏」。默认这些记录会从列表与 Excel 导出中排除,可在[设置 → 外观设置](/guide/settings#外观设置)中开启「显示已隐藏备注的记录」。
- **计入统计**:行内开关可直接切换;关闭后该记录不纳入月度统计(适合借款、转账等),行内会以删除线标示。 - **计入统计**:行内开关可直接切换;关闭后该记录不纳入月度统计(适合借款、转账等),行内会以删除线标示(样式可在外观设置中调整)。
- **行操作**:编辑、查看「原始数据」(账单导入时的原始字段,以 JSON 展示)、删除。 - **关系**:展示该记录所属[账单关系](/guide/relations)组的数量,点击可查看与管理。
- **行操作**:编辑、详情、删除。
分页支持每页 10/20/50/100/200/500 条。 ### 账单详情
## 查看原始数据 点击行内「**详情**」打开「账单详情」弹窗,集中展示单条账单的以下信息:
账单导入时,系统会完整保留每笔交易在原始账单中的字段(以 JSON 存储)。在记录行点击「**原始数据**」即可查看,用于核对金额、交易状态、对方账号、交易类型等明细。 - **基本信息**:类型、金额、分类、交易渠道、交易时间、是否计入统计、备注与创建时间。
- **标签**:该记录的全部标签。
- **账单关系**:所属的[账单关系](/guide/relations)组,可在弹窗内直接新建。
- **来源图片**:图片记账生成的记录会展示其来源截图,默认加载缩略图,可按需点「加载原图」查看原图。
- **原始数据**:账单导入或图片记账时保留的原始字段(以 JSON 展示),用于核对金额、交易状态、对方账号、交易类型等明细。
- 仅**账单导入**的记录有原始数据;**手动新增**的记录没有,该按钮不可用。 ![账单详情弹窗](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/bill-detail.png)
- 原始数据是排查导入异常、核对账单的重要依据。
> - **账单导入**记录保留原始账单行字段;**图片记账**记录保留模型从截图提取的原始字段;**手动新增**的记录没有来源图片与原始数据。
> - 原始数据是排查导入异常、核对账单的重要依据。
分页的每页条数**候选与默认值在[设置 → 外观设置](/guide/settings#外观设置)中统一配置**(默认候选 10 / 20 / 50 / 100,默认 20);各表格会分别记住上次选择的每页条数。
## 一键设置(批量设置) ## 一键设置(批量设置)
对**当前表格展示的记录**批量操作: 在列表右上角点「一键设置」,对**勾选的记录**批量设置;若未勾选任何记录,则作用于**当前表格展示的全部记录**:
![一键设置弹窗](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/batch-set.png) ![一键设置弹窗](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/batch-set.png)
@@ -137,10 +174,15 @@
## AI 智能记账 ## AI 智能记账
在「智能记账」弹窗中用自然语言描述一笔收支,例如「昨天中午吃饭 35 支付宝」,AI 会解析为类型、金额、分类、交易渠道、时间、备注、标签等字段,并预填到新增记录表单,确认后保存。 「智能记账」弹窗提供**文字记账**与**图片记账**两种方式。
![AI 智能记账弹窗](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/ai-record.png) ![AI 智能记账弹窗](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/ai-record.png)
- **文字记账**:用一句话描述一笔收支,例如「昨天中午吃饭 35 支付宝」,AI 会解析为类型、金额、分类、交易渠道、时间、备注、标签等字段,并预填到新增记录表单,确认后保存。
- **图片记账**:上传一至多张账单或支付截图(也可 `Ctrl+V` 粘贴),AI 实时识别其中的每一笔交易,生成可编辑的记录列表,勾选后批量导入。
再次打开弹窗会记住上次使用的记账方式。图片记账的用法与限制详见 [AI 能力 → 图片记账](/guide/ai#图片记账)。
## 手动添加记录 ## 手动添加记录
1. 在收支记录页右上角点击「**新增记录**」。 1. 在收支记录页右上角点击「**新增记录**」。
@@ -149,18 +191,28 @@
| 字段 | 说明 | | 字段 | 说明 |
|---|---| |---|---|
| 类型 | 支出 / 收入 | | 类型 | 支出 / 收入 |
| 分类 | 新增时必填 | | 分类 | 新增时必填;可点右侧「新建分类」直接创建 |
| 金额 | 大于 0 | | 金额 | 大于 0 |
| 交易渠道 | 可选 | | 交易渠道 | 可选,含内置渠道与[自定义渠道](/guide/channels);可点右侧「新建渠道」直接创建;启用 AI 时可一键推荐或新建 |
| 交易时间 | 默认当前时间 | | 交易时间 | 默认当前时间 |
| 备注 | 可选 | | 备注 | 可选 |
| 标签 | 可选,可多选 | | 标签 | 可选,可多选;可点右侧「新建标签」直接创建 |
| 计入统计 | 默认开启;关闭后不纳入统计 | | 计入统计 | 默认开启;关闭后不纳入统计 |
3. 点击「确定」保存。 3. 点击「确定」保存。
![新增记录弹窗](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/add-record.png) ![新增记录弹窗](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/add-record.png)
::: tip 记账时直接新建分类 / 标签 / 渠道
新增与编辑弹窗中,**分类**、**标签**、**交易渠道** 三行在选择框右侧各有一个「**新建分类 / 新建标签 / 新建渠道**」按钮,**不受 AI 启用状态影响**,随时可用。点击后弹出精简表单,创建完成即自动填入当前账单:
- **新建分类**:填写名称、类型(默认跟随当前账单类型)与图标;创建后自动选中,并把账单类型同步为该分类的类型。
- **新建标签**:填写名称与颜色;创建后自动追加到已选标签。
- **新建渠道**:填写名称;创建后自动选中。
![记账弹窗内新建分类](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/record-new-category.png)
:::
## 编辑记录 ## 编辑记录
在记录行点击「编辑」即可修改记录,弹窗字段与新增一致;编辑时允许不设置分类。 在记录行点击「编辑」即可修改记录,弹窗字段与新增一致;编辑时允许不设置分类。
@@ -169,7 +221,7 @@
### 编辑时的 AI 辅助 ### 编辑时的 AI 辅助
启用 AI 后,编辑弹窗中的**分类**与**标签**旁会出现 AI 辅助按钮(与新增记录一致): 启用 AI 后,编辑弹窗中的**分类**、**标签**与**交易渠道**旁会出现 AI 辅助按钮(与新增记录一致):
| 按钮 | 作用 | | 按钮 | 作用 |
|---|---| |---|---|
@@ -177,8 +229,14 @@
| 分类 · **AI 新建** | 根据当前账单信息生成新分类;确认「保存并选用」后创建并选中,若已存在同名同类型分类则直接选中 | | 分类 · **AI 新建** | 根据当前账单信息生成新分类;确认「保存并选用」后创建并选中,若已存在同名同类型分类则直接选中 |
| 标签 · **AI 推荐** | 推荐已有标签并追加(自动去重) | | 标签 · **AI 推荐** | 推荐已有标签并追加(自动去重) |
| 标签 · **AI 新建** | 生成新标签(含建议颜色),确认后创建并选中;同名标签直接选中 | | 标签 · **AI 新建** | 生成新标签(含建议颜色),确认后创建并选中;同名标签直接选中 |
| 渠道 · **AI 推荐** | 根据当前类型、金额、渠道、备注,从已有渠道中推荐并填入 |
| 渠道 · **AI 新建** | 生成新渠道名称;确认「保存并选用」后创建并选中,同名渠道直接选中 |
![编辑记录中的渠道 AI 辅助](https://adcarac.oss-cn-beijing.aliyuncs.com/finance-app-docs/screenshots/elements/ai-channel-suggest.png)
> 「AI 新建」需先填写备注或金额,便于模型理解;生成结果会显示确认条,点击「保存并选用」才真正创建。 > 「AI 新建」需先填写备注或金额,便于模型理解;生成结果会显示确认条,点击「保存并选用」才真正创建。
>
> 上述 AI 辅助按钮仅在[启用 AI](/guide/ai)后出现;**分类、标签、交易渠道三行始终各有一个手动「新建」按钮**(不受 AI 状态影响),可直接填写并创建,详见[手动添加记录](#手动添加记录)中的说明。
编辑完成后点击「确定」保存。其他 AI 能力(批量建议、智能记账、智能查询)见 [AI 能力](/guide/ai)。 编辑完成后点击「确定」保存。其他 AI 能力(批量建议、智能记账、智能查询)见 [AI 能力](/guide/ai)。
+16 -10
View File
@@ -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
View File
@@ -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",
+4 -1
View File
@@ -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
} }
/** /**
+6 -1
View File
@@ -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
View File
@@ -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 -4
View File
@@ -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)
+8 -4
View File
@@ -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')
+3
View File
@@ -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({