Haruka 取自《明日方舟》遥干员的英文名。
作者曾经是随手记的拥簇,但是随手记太大太冗余太臃肿了,以及最近正好在找记账app,没有一个符合自己要求的记账App,所以就有了轮子再创造 —— Haruka
Haruka 是一个单例软件,理论上它只服务于一个用户。
需要 Rust stable、Node.js 24 和 npm。Ubuntu/Debian 从源码构建还需要 C 编译工具及 OpenSSL 开发文件:
sudo apt-get update
sudo apt-get install build-essential pkg-config libssl-dev首次运行先安装前端构建依赖并生成本地浏览器资源:
npm ci
npm run web:build
cargo runTailwind 样式由 assets/tailwind.css 扫描模板和 Rust 源码后编译到 static/app.css,开发样式时可使用 npm run css:watch。static/app.css 是被 Git 忽略的本地构建产物,不提交;本地编译 Rust 前须先生成它,CI 和 Docker 也会先运行 npm run web:build 再编译 Rust。web:build 还会复制锁定版本的 htmx、Chart.js、Tesseract.js Web Worker/WASM 和简体中文、英文 OCR 模型;这些浏览器资源仍提交在 static/,CI 单独检查它们与锁定依赖一致。所有资源均嵌入二进制,运行时不依赖第三方 CDN。
Askama 模板会在 Rust 编译时检查语法。遇到模板解析错误时,先修复第一条错误所在模板的 if、for、block 闭合;随后出现的模板 render() 方法缺失可能只是派生失败的连带错误。
浏览器会注册一个轻量 Service Worker,仅缓存编译后的 CSS 和锁定版本的非敏感前端运行文件。包含账户、账单等解密内容的 HTML/JSON 不会进入离线缓存,任何 AJAX 或账务写入也不会离线排队;断网操作会明确失败,恢复网络后不会自动重放。
首次设置密码、解锁和密码恢复页面不渲染顶部菜单栏;解锁页内的 Passkey 兼容绑定也保持无菜单布局。成功解锁进入仪表板后显示正常导航,主动锁定后重新隐藏菜单。菜单在模板层移除,不依赖 JavaScript 或 CSS 隐藏。
解锁后的顶部导航包含仪表板、记一笔、账单、账户、计划、借贷、分析、工具八个一级入口。“记一笔”独立直达 /bills/new;账单下包含高级搜索、分类、隐私模式、分享管理,账户下包含基金校准,计划下包含订阅、分期、定投、理财方案,借贷下包含借贷对象、借还请求与证明,分析下包含收支统计、预算,工具下包含银行短信、短信模板、货币换算、设置。原有独立页面和 URL 保持不变。
桌面端的仪表板、记一笔、账单、账户可直接点击;使用鼠标时,悬停在分组上即在下方展开浮动菜单,移入浮层保持打开,移出后短暂延迟收起;账单和账户旁的箭头仍可点击展开。手机端保持点按折叠菜单,长菜单在面板内滚动。导航栏吸顶,锁定按钮位于菜单外侧。菜单支持 Enter/空格展开、Tab 访问链接、Escape 逐层关闭并返回触发器,点击外部或离开导航时收起;禁用 JavaScript 后仍可通过原生 details 点击展开和跳转。当前页面及所属分组在 htmx 切页与历史返回后更新高亮。
公共页面的样式 URL 自动携带所嵌入 CSS 内容的指纹,避免旧 Service Worker 或浏览器缓存让新版界面缺少样式;不需要手动修改版本号或清缓存。仅缓存非敏感静态资源的边界不变。
列表、下拉和资金流水中的账户标签使用 名称(标识):有银行卡号时标识只显示最后四位,没有卡号时完整显示配置的账户用户名,包括短用户名;无标识则只显示名称。账户详情的“账户信息”显示完整原始账户名、完整用户名,但银行卡号仍只显示尾号,不展开完整卡号。编辑页同样只显示卡号尾号、完整当前用户名;输入留空表示保留,勾选清除才清空。
点击卡号时才通过已解锁、禁止缓存的 POST /accounts/{id}/card-number 读取并复制完整卡号,不将完整卡号写入页面 HTML、属性或浏览器持久存储。支持异步剪贴板的安全环境会在点按手势内启动复制,再等待卡号返回,以兼容 Safari;普通 HTTP 或不支持该能力时,先读取并明确提示“再次点击复制”,第二次点按完成兼容复制。待复制卡号只留在当前文档内存,复制成功即清除,未复制时 30 秒失效。
账户原名与用户名直接从当前完整文本复制,剪贴板 API 不可用或拒绝时尝试兼容复制,仍失败时明确提示。所有详情和标识响应均禁止缓存。既有公开分享快照保持不可变,银行卡完整标识的公开访问边界不放宽。
解锁后进入“工具 → 设置 → 网站页脚”,配置多行页脚文字(最多 2000 个字符)、可选的 ICP 备案号和公安备案号(各最多 80 个字符)。ICP备案号固定链接到工信部备案网站 https://beian.miit.gov.cn/,公安备案号固定链接到 https://beian.gov.cn/,两者并列展示。请填写完整备案号;所有内容仅按纯文本展示,HTML 不会执行。
保存后统一显示在业务页面、登录/恢复页面、错误页、公开账单分享和借还证明页面。各项留空可单独关闭,三项均留空保存则不显示页脚;保存时会去除首尾空白。页脚在页面底部正常排版,不悬浮遮挡内容,打印/另存 PDF 时隐藏。禁用 JavaScript 后仍可展示和通过原生表单保存。
这些内容属于公开网站信息,明文保存,请勿填写个人隐私或敏感信息。旧数据库启动时会自动补齐字段,保留已有页脚和 ICP 备案号,无需删除数据库;服务重启后会重新加载配置,未解锁访问者也能看到页脚。服务端只缓存这三项公开配置,不缓存解密后的账务页面或数据。
记一笔时可勾选“隐私账单”,收入、支出及混合批量草稿逐笔保存该选择;扫描票据只填草稿,不自动保存或清除已有隐私选择。无 JavaScript 时也能通过普通表单创建隐私账单,但查看隐私详情需要 JavaScript。
从“账单 → 隐私模式”进入 /privacy,首次设置独立隐私密码,之后用该密码进入 /privacy/bills。主密码不能代替隐私密码,不提供主密码重置隐私密码的旁路;修改隐私密码需要已解锁隐私模式并验证当前隐私密码,同时撤销现有隐私授权。连续输错十次会冷却一分钟,与主密码尝试次数和永久封锁轮次互不影响。
隐私授权绑定当前普通解锁会话,额外令牌只保存在当前页面内存,通过请求头发送,不进入 Cookie、localStorage、sessionStorage 或 URL,因此其他标签页不会继承。只有真实操作延长有效期,读取数据不会续期;无操作一分钟后,浏览器清除私密内容并回到解锁页,服务端同样拒绝过期令牌。刷新、离开当前文档、关闭标签页、主动锁定或服务重启后均需重新进入隐私模式。htmx 切页不会保存解密页面的本地历史快照,业务响应禁止缓存。
普通账单列表、高级搜索结果和 CSV 均不包含隐私账单,解锁隐私模式也不会改变普通列表的筛选边界。普通仪表板、账户流水、统计和分期页面不展示隐私账单的原分类与备注;必要流水及分期信息使用“隐私账单”占位。所有金额统计均纳入隐私账单的真实资金影响,包括月度收支、余额、信用欠款、预算、食品支出、恩格尔系数及财务比率;统计分类汇总单列“隐私账单”,金额排行也使用占位且不提供私密详情链接。分类笔数提醒不是金额统计,仍不读取隐私分类。
隐私账单不能分享;将普通账单改为隐私时,同一事务撤销该账单此前创建的分享。隐私分期还款产生的费用账单继续标记为隐私,关联转账只写通用说明,不复制原备注。旧数据库启动时自动补齐 bills.is_private,原账单保持普通账单。
这是应用内的详情访问隔离,不是第二层独立数据加密:账单字段仍由原有 DEK 加密,数据库另存隐私密码的随机盐及 Argon2id 校验值,不保存密码明文。它不能防御掌握服务端进程、主数据密钥或数据库管理权限的人。
密码登录、设置页重置恢复助记词、Passkey 兼容绑定共用服务器进程级的密码验证门禁,不按 IP、浏览器或 Cookie 分开计数。每次启动最多允许累计 10 次主密码验证失败,成功验证不会清除已经发生的失败;只有密码校验失败计次,未进入密码校验的无效请求及数据库查询异常不计次。
第 10 次失败返回 HTTP 429,记录一轮耗尽,并禁止当前进程继续验证任何主密码,包括正确密码。第一、二轮耗尽后必须重启服务才能恢复本轮次数;已耗尽轮次保存在 meta.failed_password_rounds,不会因重启清除。第三轮耗尽后永久禁止密码验证,重启也不解除,数据库和账务记录不会删除。 老数据库启动时自动补齐该明文字段,初始为 0。
锁定页面显示原因并禁用密码输入及提交。直接通过有效 Passkey PRF 解锁、使用恢复助记词恢复,以及既有已解锁会话不受密码门禁限制,但均不会清除失败次数或轮次;需要主密码的 Passkey 兼容绑定仍受限制。助记词恢复原本使旧会话失效的规则不变。轮次标记写入失败会返回明确的 500 并保持当前进程封锁,修复存储后补写同轮标记,不重复增加;应先处理持久化异常,不能依赖未成功写入的标记跨重启保留。
管理员解除永久锁定:
-
停止所有使用该数据库的 haruka 进程,并备份数据库。
-
对
DATABASE_URL指向的实际 SQLite 文件执行以下命令;默认文件为haruka.db:sqlite3 haruka.db 'UPDATE meta SET failed_password_rounds = 0 WHERE id = 1;' -
重新启动 haruka。密码本身不变;只清除失败标记,不要删除数据库或
meta记录。
该策略限制在线试错,不防止持有数据库文件者离线猜解弱密码;恶意访问者也可能故意耗尽次数。部署时仍需使用强密码,并限制解锁接口的暴露范围。
快速记账的“扫描票据”在浏览器 Web Worker 中运行 Tesseract.js WASM,图片不会上传至 haruka 后端。首次识别需要从 haruka 自身加载约 18 MB 的本地 OCR 运行文件和中英文模型,之后语言数据由 Tesseract.js 缓存在浏览器 IndexedDB 中。
识别后的文字可以用本地规则提取金额和时间,也可以由浏览器直接调用用户填写的 OpenAI Chat Completions 兼容完整 URL。URL 与模型只保存在当前浏览器 localStorage,API Key 不持久保存、刷新页面即清除;后端不会代理或看到 URL、密钥、图片及 OCR 文字。自定义 AI 服务必须允许 haruka 当前来源进行 CORS 请求,否则浏览器会明确报告跨域失败。所有识别结果都只填入可编辑快速记账草稿,仍需用户确认后才会写入账单。
订阅可以绑定一个同币种账户,并设置在到期前 0 到 30 天开始检查余额。到期后,已解锁浏览器访问仪表板时会调用受会话保护的接口生成普通支出账单,并把订阅顺延一个周期;这只是在 Haruka 账本中自动记账,不会连接银行、信用卡或支付平台发起真实扣款。服务锁定或重启期间无法使用 DEK,遗漏的到期订阅会在下次解锁进入仪表板后补执行。
普通账户按余额判断是否足够,信用卡和信贷服务按剩余可用额度判断。进入提前检查期后若不足,仪表板顶部会显示高优先级红色提醒;余额不足时不会生成账单或顺延日期。信贷服务还会在仪表板独立展示当前欠款、已用额度、剩余额度、总授信额和额度使用比例。
订阅卡片显示“全年费用估算”,列表按当前筛选的全部订阅、分页前按原币分别汇总。按当前金额持续续订一年估算:日付 × 365、周付 × 365 ÷ 7(四舍五入到分)、月付 × 12、季付 × 4、年付 × 1。例如每月 CNY 100,全年估算 CNY 1200。手动与自动扣款均计入,已到期不等于取消,也保留在估算中;这不是实际已支付金额或当年账单,不按到期日倒推扣款次数。
分期列表显示访问者当前自然年的应还及未还金额,包含本金、利息和手续费,并在分页前对当前筛选的全部计划按原币分别汇总。详情页显示当年应还、已还、未还,可展开查看计划全部年份的年度明细,计算不受期次分页影响。每期按还款日归属年份,提前还款仅降低未还,不移动所属年份;逾期未还仍保留在原到期年。
不同币种不直接相加。以上均为只读的费用估算或还款安排,不新增收入/支出,不修改余额、流水、到期时间或分期状态。
“记一笔”分为收入、支出、转账、还款四栏。普通手动转账的来源允许普通资金账户及当前余额大于 0 的信用卡;零/负余额信用卡和所有信贷服务不能转出。接收端仅限非信用资金账户,信用卡和信贷服务入账须使用“还款”。报价和保存都会重查来源资格,信用卡转账本金加手续费不得超过当前正余额,不能使用授信额度。还款来源必须为同币种非信用资金账户。自定义信用卡还款允许超过欠款,也允许零欠款或已有正余额时继续存入,保留实际溢缴款;信贷服务仍不能形成正余额。转账、还款本金均不计收入或支出,也不会自动标记分期期次已还。
分期支持“偿还本期”和“全部结清”,按已确认计划合计扣除本金、利息和手续费,不自动猜测提前结清减免。最低还款由用户填写机构实际最低金额、期限、利率及总手续费:先实际支付该金额,再将剩余普通欠款转为分期,不新增消费、不重复记支出;已经分期的本金不能再次转换。估算与实际欠款不一致时拒绝转换,须先核对未来日期流水或余额调整。未发生分期还款的余额转换可撤销,已发生的最低还款仍保留。
消费全额退款立即停止未还期次,默认看板、年度未还和仪表板提醒不再催还;已还本金和费用原样保留。部分退款或撤销退款后暂停剩余还款,需按银行实际剩余本金、日期、利息和手续费逐项核对,核对成功才可继续支付。银行调整了期数时可增加实际剩余期次,核对不产生资金流水。
分期页会在分页和筛选之外检测旧账中“已有退款但未删除”的计划,通过“查看待清理退款分期”入口集中处理,已核对或已还清的历史计划同样可达。删除已有退款的计划只移除分期安排,不补退剩余金额、不改变余额,未退款净额及真实已还本金按普通流水重新计算。没有退款时先显示明确确认页:只有确认已实际收到全额退款,才在同一事务补记退款并删除;取消或拒绝按退货处理不作任何变更。普通会话不能借此补记隐私退款,须先在隐私模式完成退款。
删除使用保留历史的软删除,不移除已还记录、真实转账或费用,重复提交不重复退款。可在高级搜索中查看“已删除”历史;删除安排不等于金融机构取消债务,实际溢缴款继续保留。数据库升级自动补齐字段并兼容旧的非空消费关联,无需删除数据库。无 JavaScript 时,可从记账页进入 /repayments/new 完成普通还款、分期支付与最低还款转换,时间按 UTC 输入。
普通账户和具体基金的新建/编辑页可设置“默认转出费率”,例如填写 0.1 表示 0.1%;费率范围为 0%–100%,最多两位小数,默认 0。投资分组不能直接收付款,因此不设置转出费率。账户设置仅供手动转账带入默认值,不会强制覆盖每笔转账的实际费用。
独立“记一笔”页的转账表单支持两种调整:
- 修改本次费率:按转出本金乘费率计算手续费,四舍五入到整数分;费率留空使用来源账户默认,填写
0覆盖默认费率。 - 手填手续费金额:以来源账户原币填写,优先于费率计算结果,可以与计算值不同;填写
0免手续费,清空金额恢复按费率计算。预览显示手续费和来源合计扣款,更改本金或费率不会覆盖已经手填的金额。
手续费在本金之外额外从转出账户扣除,不减少目标账户到账本金;跨币种转账的手续费仍按来源原币保存。例:转出 CNY 1000、费率 0.1%,来源合计扣除 CNY 1001,目标收到转账指定的本金。普通账户和正余额信用卡均必须有足够现有余额支付本金和费用;信贷服务不能作为来源,所有信用账户均不能作为普通转账接收端,入账请使用还款入口。
转出账户余额旁的“全部余额”可一键重新读取当前原币余额并精确填入转出本金,包括正余额信用卡,同时更新手续费、合计扣款和跨币到账预览。手续费仍额外扣除,不会自动免除;填入全部余额后若另有手续费,需调整本金或按真实情况修改费用。切换账户、切换记账类型或手动修改金额后,迟到的余额请求不会覆盖新草稿;读取失败时明确提示且不使用旧余额。还款或消费后信用卡来源资格同步更新;已选信用卡归零或不可用时清空来源并禁止提交,不会自动换到其他资金账户。无 JavaScript 时仍可手动输入金额,转出和转入资格与增强模式一致。
非零手续费在同一事务中生成关联的“转账手续费”普通支出,计入支出统计、预算和分类笔数限额,转账本金仍不计入收支。费用账单不能单独编辑或删除;删除原转账或关联账户时一并撤销,并校验其他受影响账户的最终余额,失败不写入。账户默认费率和每笔费率/费用快照均使用 DEK 加密,修改账户默认不会改写历史转账;旧数据库启动时自动补齐字段,旧转账保持零手续费。账户默认不额外应用到定投、分期还款或短信生成的转账,它们保留原有费用规则。无 JavaScript 时同样提交可见表单值,空费率/空金额由服务端按上述规则处理。
“预算”页面可以分别设置日预算、周预算和月预算,留空即可单独停用某个周期。预算金额使用默认货币并加密保存;普通支出(含转账手续费)会按汇率折算后计入自然日、周一至周日和自然月的当前进度,转账本金、借还和定投本金不占用预算。预算页和仪表板都会显示已使用比例、剩余金额或超支金额,达到 80% 后用醒目样式提醒。
顶部“分类”入口打开独立的 /categories 页面,收入和支出分别管理;设置页只保留跳转入口。支出分类可以选择每日、每周或每月的笔数限额,默认自然月,填写正整数启用,留空停用;收入分类没有笔数限额。限额数值加密保存。
按访问者时区统计当前自然周期截至今天的普通支出账单,每笔计一次,未来日期暂不计入,不受账户或币种影响。订阅、短信生成的支出及转账/定投/分期手续费同样计入;隐私账单、收入、转账本金、借还和定投本金不计入。达到 80% 或仅剩一笔时,分类页及仪表板醒目提醒;达到、超出分别标记。限额是消费提醒,不阻止真实支出入账。
账单保存稳定的分类关联,并保留原来的分类名称与食品标记快照。升级后的分类改名不会重置次数,删除后同名重建不会自动继承旧账单;编辑账单重新选择当前有效分类时,计数会随关联更新。
启动时自动补齐旧数据库字段,无需删除数据库。首次解锁只为尚未关联的旧账单按当前同类型、同名分类建立关联,无法匹配的记录不会自动计入后来创建的分类。升级前的数据没有稳定分类身份,因此无法复原此前改名、删除及同名重建的关联历史;旧账单的金额、名称、食品标记和时间不被改写。
信贷额度概览使用中性卡片:桌面每排 3 张、平板 2 张、手机 1 张。移除整块面板和卡片的紫色/红色强调底色,浅色模式统一白底、深色模式统一深灰底;额度紧张与逾期继续通过红色文字、边框和进度条提示,不改变额度或账单计算。
信用卡和信贷服务的当前余额为 0 时,不显示仪表板信用账户卡片,无论授信额度是否为 0。零授信信用卡不参与信用账户额度概览及账单估算;若仍有余额,则在普通账户概览中保留,继续计入账户净额,账户管理、记账选择和历史统计不受影响。此规则不排除非零余额的信贷服务。其他信用账户产生欠款后显示卡片,结清归零后隐藏;有正余额时仍展示,但不提示“额度紧张”。额度、欠款和剩余额度紧凑并列展示,详细估算说明可展开;还款信息分为三档,均按账户原币显示:
- 未出账普通消费:尚未到对应账单日的未抵扣普通欠款,不计入已出账应还;账单日当天的消费归入当天账期。
- 分期:没有未还分期时直接显示“无”;否则单列剩余未还本金,以及最早未还期的还款日和月供(含本金、利息、手续费),不受“未来 7 日提醒”窗口限制。剩余本金仍占用信用额度,但不能再整体加到应还金额。
- 已出账应还(估算):按原还款日分组,合并该日期的普通欠款与已出账、未支付分期期次月供;逾期保留原日期,不顺延到下月。尚未出账的分期期次不计入本档。
普通入账按交易时间先抵扣较早的普通欠款,剩余作为预付款抵扣后续普通消费;不会据此猜测分期期次已还。仍保留的消费分期,其关联退款先冲减该笔尚未偿还本金,超额才抵扣普通欠款,避免退货额再次偿还其他消费。分期只按实际还款状态移除对应期次,关联本金转账不再二次抵扣普通欠款;分期删除后,原消费净额、退款及真实本金还款一起重新按普通流水计算。计算只生成展示结果,不修改余额、资金流水或分期状态。
以上为已记录流水的自动估算,不是银行账单。没有金融机构的出账快照、普通还款账期归属及完整账单日修改历史,历史初始余额、校准和退款可能造成差异;存在初始余额或校准时页面会提示核对。未来自然日流水不计入本次估算,但仍按原规则影响账户余额和额度,页面会提示两者可能不一致。实际应还金额和日期以金融机构账单为准。
编辑信用账户的账单日时,可选择“立即生效”或“下个日历月 1 日生效”,默认下月;新建或首次转为信用账户时立即使用填写的账单日。生效月份按访问者时区计算,没有有效时区时使用 UTC。还款日留空时跟随当月有效账单日;已设置的固定还款日不受影响,月内不存在该日期时按月末计算。
账户列表、详情和编辑页显示当前账单日及待生效的新日期。待生效值会预填到编辑表单,无关编辑不会延后计划;改回当前账单日可以取消计划,也可以选择立即激活待生效值。生效计划持久保存在数据库中,重启不丢失,到期后读取时自动采用新值,无需后台任务。
旧数据库只保存当前账单日,没有修改历史;升级保留现有值作为当前有效设置,不猜测旧账单日或追溯改写历史记录。新增计划字段自动补齐,无需删除数据库。
业务时间始终按 UTC 保存,不迁移或偏移旧数据。启用 JavaScript 时,浏览器通过非敏感的 haruka_time_zone 会话 Cookie 提供 IANA 时区;日期搜索、账户本月汇总、仪表板、统计和日/周/月预算使用同一当地自然日口径,并在页面显示当前时区。没有时区时明确回退到 UTC。账单搜索可用 time_zone 指定口径,筛选、翻页和 CSV 导出均保留该值;纯日期还款日不转换,定投交易日仍按北京时间判断。
账单页、高级搜索页顶部固定显示“本月收入”“本月支出”“本月恩格尔系数”,按当前时区本月 1 日至今日统计,不随下方搜索条件或分页变化。普通页面的月度卡片汇总全部普通及隐私账单,但下方结果仍隐藏隐私行;隐私页面的卡片及列表只统计隐私账单。食品占比使用账单保存的食品标记快照,无支出时显示 —。退款不计收入,原消费按扣除累计退款后的净额统计。
仪表板顶部显示所有已录入普通收支账单的“总收入”“总支出”,不限制月份,包含隐私账单金额及已录入的未来日期账单,但不展示隐私分类或备注;不再显示恩格尔系数或下方日、周、月、年收支图表。两页均不把转账、借还、强制平账和余额调整计入收支,异币种金额仍按当前参考汇率折算为默认货币;详细图表继续在独立“收支统计”页查看。
仪表板只展示账户概览、总额和提醒,不再嵌入快速记账表单或收支报表;点击一级菜单或仪表板的“记一笔”进入独立 /bills/new。该页分为收入、支出、转账、还款四栏,借还统一进入“借贷 → 借还请求与证明”。启用 JavaScript 时只有一套可见表单,共用金额、时间、备注和提交区域;非当前类型的字段禁用,不会混入提交。切换保留共享草稿,保存成功后原地刷新余额并清空金额、备注,可继续记录。
备注输入框、转账费率与手续费金额、信用支出的分期勾选框直接展开,不需要先点开折叠区;只有分期期限、利率等参数在勾选后显示。跨币实际到账按账户币种显示,手续费与来源合计扣款预览始终可见。时间旁提供“重置到现在”,按访问者当地时间更新并通过唯一隐藏字段提交对应 UTC 时间。普通收支继续支持原子批量,逐笔保留隐私标记;扫描结果只填入可编辑收支草稿。桌面使用两列,手机使用单列;无 JavaScript 时退回普通收支、转账两张完整表单,并提供独立原生还款页入口,明确按 UTC 输入时间,同样不提供直接借还录入。
统计页提供独立的“区间财务指标”,随快捷区间或自定义起止日期更新:
- 恩格尔系数:全部食品支出 ÷ 全部支出,包含隐私账单保存的食品标记与金额。食品与其他支出的饼图使用同一口径,但不展示隐私原分类和备注;没有支出时显示
—。 - 结余率:(收入 − 支出)÷ 收入。超支时保留负值,不钳制为 0。
- 支出收入比:支出 ÷ 收入。超过 100% 表示该区间支出高于收入。
三项指标均包含隐私账单金额,但不展示隐私原分类和备注;后两项没有收入时显示 —,不误报为 0%。三个指标均不包含内部转账、借还和余额调整,也不把退款当作收入;原支出按退款后净额计算,仅作收支分析,不代表投资收益率。
统计页包含收入与支出的分类、账户排行和占比饼图;分期撤销会同时校验本金扣回账户及返还本金、利息、手续费账户的最终余额,任何一端不满足余额约束时整次撤销不写库。
仪表板和统计页共用在公共页面头部预先加载的本地 Chart.js,确保 htmx 点击导航时图表库已就绪;同一文档内切页不重复加载图表库。离开图表页时销毁旧实例,首次进入、重复切页和浏览器前进/后退均可正常绘图,无需手动刷新。
在账单页点击支出记录的“退款”,可选择退回全部剩余金额,或输入部分退款金额。允许分次退款,但累计不能超过原支出;“退全部”按提交时的最新剩余金额计算,不会重复退回已经退款的部分。
退款生成同一账户、同一币种的关联退款流水,数据库仍以 income 保存,分类为“退款收入”,账户余额按退款金额增加;原支出与实际对账流水保留,不删除或改写资金记录。统计只按 refund_of_bill_id 识别退款,不按分类名猜测:退款流水不计收入,原消费按扣除累计退款后的净额统计,归属原消费日期,即使退款发生在其他月份也会追溯扣减。全额退款不再计入支出金额、笔数、排行或预算使用额;部分退款按剩余金额计入,仍计一笔。仪表板、账单月汇总、统计图表、账户收支摘要、预算、分类限额和理财收入推算保持同一口径。账单与账户流水分别标注“部分退款”和“已退款”。隐私退款继承原账单隐私标记,只有隐私模式能查看详情。
退款流水不能直接编辑,可以按余额约束删除撤销,撤销后对应支出统计和退款状态自动恢复。已有退款的普通原支出不能编辑或删除;分期原消费即使已有退款或还款,也可从账单列表或分期详情修改分类与备注,服务端拒绝修改金额、账户、类型、时间、隐私设置和分期参数。转账、定投和分期的手续费支出也支持退款,但撤销所属转账或分期还款前,必须先撤销对应手续费的退款。原分期消费全退后停止未还安排,部分或撤销退款后需按实际金融机构剩余计划核对;既有已还记录不改写。退款与删除安排的确认规则见“信用账户还款与退货分期”。
账单页和高级搜索页均可导出当前筛选结果。CSV 包含普通收支、转账、借还及余额调整的合并流水,不受当前分页限制;日期筛选和导出时间使用页面显示的同一时区。文件使用带 BOM 的 UTF-8 编码以兼容常见表格软件,并对账户、分类、对象及备注等用户文本进行电子表格公式注入防护。
普通收入或支出账单可以创建限时分享链接,地址格式为 /s/<随机字符>。创建时可设置精确的结束时间和可选访问密码;分享管理页可以重新复制链接或随时撤销。公开页面只展示创建当时的账单快照,新快照的账户同样使用名称与安全尾号,不包含完整卡号或用户名;既有不可变快照不回写,之后编辑或删除原账单也不会修改已经发出的内容。
快照使用分享 URL 中的随机令牌与可选密码共同派生的独立密钥加密,密码不会保存到数据库。分享访问不依赖已解锁会话,因此服务重启后链接仍然可用;超过有效期或被撤销后立即停止展示。公开响应禁止缓存和搜索引擎收录。生产部署请使用 HTTPS,并注意 URL 本身就是访问凭据:只交给需要查看的人,设置密码时最好通过另一渠道发送;反向代理也应避免长期记录完整的 /s/ 请求路径。
从账户详情的“对账模式”进入,选择包含首尾日期的区间及 IANA 时区,生成该账户原币口径的完整对账单,不受账户流水分页限制。投资分类仍进入基金列表,具体基金可单独生成对账单。对账单显示期初余额、分类资金净变动、每笔滚动余额和期末余额;同一发生时间按创建时间、记录类型及编号稳定排序。
所有已入账资金影响均计入,包括隐私收支、退款、转账、自动手续费及借还。隐私账单只显示收支方向和金额,不展示原分类与备注;跨币转账按本账户的实际原币收付金额计入,手续费只通过对应支出计入一次。余额校准只计“调整后 − 调整前”,结合当前 offset 还原基线,不重复累加目标余额。期末余额按所选区间计算,可能不同于包含更晚流水及未来日期记录的当前账本余额。
可填写银行或平台的实际期末余额做只读比较,支持负余额,显示“实际 − 账本”的差额。该值仅通过 POST 提交,不写入网址或数据库,不会修改余额、创建流水或自动冲账。所有对账响应禁止缓存;结果依据生成时的当前账本记录推算,后续补录、修改或删除历史流水会改变推算,不是银行或平台出具的对账凭证。
点击“打印 / 保存 PDF”,在浏览器打印窗口选择打印机或“另存为 PDF”。账户对账单使用 A4 横向布局,长流水跨页并重复表头,操作控件不打印;未启用 JavaScript 时仍可生成和比较,通过浏览器菜单打印。导出文件包含真实资金金额,请自行妥善保管。
借出、借入、收回还款和偿还借款统一从“借贷 → 借还请求与证明”创建,每笔新流水都绑定请求与证明。“记一笔”不再提供借还录入,旧的 POST /debts 也不再接受写入。
- 需要对方确认(默认):生成限时加密链接,对方填写姓名、联系方式、收款或打款账户、实际金额、时间和说明。公开提交只保存声明,不改变余额;账主核实实际到账或付出后才入账,重复提交与确认不会重复记账。新借款可在核实时按对方信息新建借贷对象。
- 不需要对方确认:必须选择已有借贷对象,填写实际金额与收付时间;请求、单方陈述、借还流水在同一事务保存并立即入账。管理页和公开证明明确标为“单方记录,未经对方确认”,不能再通过公开提交或确认把它变成对方认可的证明。
还款必须选择同一对象、同币种、相反方向的原借款,可以分次偿还。每次入账都会在资金写入锁内重新核对原借款剩余额度及该对象同币种未还金额,超额、币种不符或余额不足时整次失败;不会用其他币种的欠款抵扣。旧原始借款也可关联新的还款请求,不补造历史关联或对方确认。已关联请求、证明或原借款的流水不能单独删除,相关账户和对象也禁止通过级联删除绕过保护;账单页和账户流水提供回到请求与证明的入口。
已入账的原借款可在管理页执行“强制平账”:适用于对方不再归还或自己不再偿还的单方账本结清,可填写说明并二次确认。系统在金融锁内按本笔借款及对象同币种尚欠上限核对,只结清部分还款后的剩余金额,从“待收回 / 待归还”中移除;不生成假还款,不改变账户余额,也不计收入、支出或预算。原借款、实际还款和证明均保留;此操作不可撤销,重复提交不覆盖原说明或重复结清。该笔借款不能继续创建或确认还款,已有未入账的关联请求公开链接失效,但已入账的还款证明仍可读取。各对象原币债务先净额汇总再换算,避免结清后出现汇率舍入尾差。
结清金额以 DEK 加密,单方说明以原公开令牌派生密钥加密,与 UTC 结清时间一起追加到验证链;公开证明及打印 PDF 会标明强制平账金额、说明、时间和新的链头指纹,不改写原始收付或对方确认。单方平账不代表实际还款、对方同意或法律债务免除;旧数据库自动补齐结清字段,不需要删除数据库。
快照和对方提交资料由分享 URL 中的 256 位随机令牌派生独立密钥加密保存,令牌本身使用账本 DEK 加密。完整收款卡号或用户名不写入公开 HTML,需要打款时才提供按需复制接口;新的出账请求不保存或公开账主完整付款标识。
新证明使用 v2 SHA-256 验证链,创建摘要约束确认方式和原借款关联,随后追加陈述与入账记录;管理页和公开回执显示链头指纹。旧 v1 证明按原摘要验证,不重签、不改写历史签收或补造流水。双方需各自另存并核对指纹;它不等于银行回单、实名电子签名或多节点共识,单方证明也不代表对方认可。
管理页点击“打开证明后打印 / 保存 PDF”,在证明页使用浏览器打印或“另存为 PDF”。A4 纵向打印件保留借还方向、请求与实际金额、原借款关联、单方/待核实/已入账状态、UTC 与访问者本地事件时间、证明版本及当前验证链指纹;长说明可跨页,按钮和确认表单不打印。尚未核实的提交只能打印“待核实回执”,单方记录不会变成双方认可的证明。
保存前关闭浏览器的“页眉和页脚”,避免把含秘密令牌的网址印进文件。PDF 是保存时的账本记录,可被编辑;验证链指纹不是 PDF 文件哈希、数字签名或第三方认证,不能单独证明真实资金收付或当事人身份。请结合有效原证明页面、双方另存的指纹、银行回单等材料核对,不将它宣称为司法认定或实名电子签署。
顶部“理财”(/finance)参考《我怎么开始多多理财》提供可编辑的起点:入门模板按月收入的 10% 投资,纳斯达克 100 / 沪港深 500 各占一半;均衡参考按 30% 投资,海外股票、国内股票、债券、黄金各占 25%。也可以完全自定义资产名称、类别、投资比例、应急金月份、扣款账户、具体基金、估算交易日、固定或聪明定投策略及手续费。
- 收入推算:默认按访问者时区统计最近 3 个完整自然月的普通收入,范围可设为 1–12 个月;包含零收入月,排除尚未结束的本月,转账与借还不计入。多币种收入按当前最近可用参考汇率折算,均值四舍五入到分。没有正收入或无法取得必要汇率时明确要求手动填写;手动月生活费 / 收入基数始终可以覆盖推算,一次性进账需自行核对。
- 预算与应急金:月投资预算为采用基数乘自定比例;其余为生活费 / 现金留存参考。应急金目标按完整基数乘目标月份保守估算,可设为 0 停用,也可选择多个非信用账户查看当前应急金、缺口及覆盖月份。应急不足只提醒,不替用户作投资决定。方案货币保存在配置中,修改系统默认货币不会静默改变既有方案。
- 资产配置与预览:最多 20 项,各比例支持两位小数,必须精确合计 100.00%,同一具体基金不能重复归类。月预算按整数分做最大余数分配,每日基准再按估算交易日向下取整到分;默认 20 天不是实际月交易日保证。异币种扣款先按明确显示日期的参考汇率换算,再生成扣款账户原币的每日金额,不使用 1:1 假定。聪明定投会在基准的 50%–150% 间调整,手续费另计,因此月预算不是扣款硬上限。
- 明确确认后才生成:保存只加密保存配置并刷新服务器预览,不修改账户余额。确认才会在一个事务中创建必要的零价值投资分类、具体基金和实际定投,或更新本方案已跟踪的计划。重复同步复用原计划,不重复创建;零预算或零配比暂停但保留映射,恢复时不补扣暂停期间日期。移除项目暂停旧计划、不删除历史,不影响用户另建的计划。已有启用计划保留原开始日期和执行进度,表单起始日只作用于新增计划。
- 防止旧预览误操作:并发保存、收入/汇率或已跟踪计划变化导致版本或预览不一致时拒绝同步,需重新查看预览再确认;任何基金或计划创建失败都会整批回滚。配置及计划关联以 DEK 加密保存,页面和 JSON 禁止缓存,htmx 不保存理财页的本地历史快照。
- 失效账户可修正:已选扣款账户、基金或应急金账户被删除或类型发生变化时,理财编辑器仍可进入,明确提示重新选择并禁止生成。原配置不会被自动清空;重新保存和生成时仍严格校验账户,读取失败或数据库错误不会被当成正常配置。
- 定期复核:预览按当前已绑定基金价值显示目标金额和再平衡差额,只供参考,不会自动校准资产或转账。请先在基金校准页面核对真实持仓,再调整配置;方案不会连接银行或购买金融产品,也不保证收益。生成后的实际执行、余额校验、交易日历及补执行仍使用下述现有定投系统。
“定投”页面可创建每日基金定投计划。计划固定绑定一个非信用扣款账户和一个投资分类下的具体基金,两端必须使用相同货币,并可设置手续费率,例如 0.15%。每个中国大陆交易日会把本金以普通转账转入该基金;实际手续费按本期本金乘费率计算并四舍五入到分,额外从扣款账户扣除,单独生成“投资手续费”支出。这样本金不会被误算成消费,而手续费会正常进入支出统计。
“投资账户”现在是基金分组,例如建立“沪港深 500”分类,再添加不同名称的基金。每只基金独立记录资金流水和持仓价值,分类总价值自动汇总;转账、记账和定投必须选择具体基金,不能直接向分类总额入账。基金继承分类货币,分类建立后不能直接修改类型或货币。有持仓、历史流水或关联配置的基金不能删除,以保留历史。
“账户”菜单中的“基金校准”及账户页的“全部基金校准”进入 /funds/valuation,一次列出所有投资分组下的全部基金(包括零持仓),不分页;各组按原币展示当前总价值,不跨组或跨币种相加。原有 /accounts/{id}/funds/valuation 单组校准保留,全局页也可选择“仅校准本组”。
校准页左侧为只读当前价值,右侧预填实际持仓价值。未修改且未勾选“已核对”的行、未勾选时留空的行不会写入,也不会覆盖期间新增的资金流水;明确填写 0 才会清空价值。价值未变但已核对时,勾选可更新核对日期并保存零差额审计,不重写余额偏移量。所有修改和显式核对共用金融锁、同一事务及整数分 baseline 并发保护,任一行无效、溢出或余额已变化时整批回滚。校准不产生普通收支或转账,保存后刷新不会重复写入审计。
手机上的校准行使用双列布局,不需要横向寻找输入框或小复选框;点按带“已核对”文字的整块区域即可勾选。核对区域与保存按钮至少高 44px,金额输入使用 16px 字号,减少 Safari 自动放大对点按的干扰。桌面保留表格布局,全局和单组校准使用相同组件,原子提交、并发校验及审计规则不变。
升级后首次解锁会自动将每个旧投资账户迁移为“原投资账户 → 默认基金”。原持仓价值、余额调整、资金流水和计划关联保留,旧计划的银行短信基金名称移入默认基金的加密别名,不需要删除数据库。招商银行及自定义定投短信按扣款银行卡尾号和具体基金的名称或银行别名匹配,不按投资分类名称匹配;银行别名在基金编辑页设置。多个计划同时匹配时拒绝自动选择。
扣款策略可选择固定金额、“聪明定投”、每日手动金额或银行短信实扣。手动模式只确认北京时间当天的交易日本金,不按历史起始日补扣;切换为手动模式时从当天或已设定的未来交易日继续。金额可填写 0,表示确认当日无扣款:保留执行记录并顺延至下一交易日,不创建本金转账或手续费账单。短信模式则按内置招商银行模板或用户配置的成功模板中的实际扣款金额生成账户转账,失败短信只形成提醒和审计记录。短信实扣计划目前只支持 CNY 账户。所有模式都以“计划 + 日期”去重,同一天不会重复扣款。
账单页允许删除定投本金转账。删除会在同一事务中撤销对应执行记录、关联手续费账单和本金转账,按实际流水恢复两端及手续费账户余额;任一账户不满足余额限制时整笔拒绝,不会只删除部分数据。该操作不回退计划的下一交易日,也不自动补扣已删除的日期。手动确认的零扣款记录没有本金转账,不属于此删除入口。
短信生成的定投转账删除后,保留加密短信审计及其去重状态,只清除已删除流水的关联,重复接收同一短信不会重新入账。删除定投计划仍保留历史资金流水,并保留手续费与转账的关联,以便之后整体撤销;升级前若已删除计划且丢失执行记录,无法可靠恢复的旧手续费关联不会凭备注猜测补造。
短信接口为 POST /api/sms,请求体为 {"time":"2026-09-15T10:00:00+08:00","raw":"【招商银行】...","sender":"95555"},其中 time 必须是带时区的 ISO8601 日期时间,sender 可省略且默认为空字符串。请求头推荐使用标准的 Authorization: Bearer <token>,也兼容现有调用方的 Authentication 头。可以在设置页输入或随机生成专属 Token,数据库只保存不可逆的 SHA-256 校验值;没有页面专属 Token 时使用 SMS_API_TOKEN,两者均未配置时兼容使用 AppleToken。接口先匹配启用的自定义模板,没有匹配时回退原有招商银行定投成功、定投失败、实时转至他行和快捷支付模板,旧快捷指令无需新增模板。内置模板核对短信年月日及时分与 time 的北京时间;定投还同时核对基金名称和扣款银行卡尾号。
设置页可加密保存本人姓名。使用内置招商银行模板时,实时转账的收款人与本人姓名完全一致会进入“本人账户转账”待确认状态;包含“信用卡还款-本人姓名”的快捷支付会进入“信用卡还款”待确认状态。由于这两类短信不包含目标账户卡号,Haruka 不会擅自修改余额,必须在独立“短信”页(/sms)选择本人名下的目标账户后才能生成转账。其他收款人或普通快捷支付只保留为无法确认的加密短信记录。
本人姓名输入框默认留空,不在页面、属性或占位提示中回显原姓名。留空保存保留原值,填写新姓名后点击“保存本人姓名”才覆盖加密存储;保存后输入框再次留空,不影响短信模板继续使用已保存姓名匹配。
短信页支持关键词、状态筛选和 50/100/200 条分页,旧待确认候选不会因为新短信而失去入口。解锁后的补处理使用 POST /sms/process-pending,确认使用 POST /sms/{id}/confirm;定投页只保留计划和短信页入口。补处理前数据库查询失败不会消费内存队列,恢复后可以重试。
聪明定投支持沪深 300(000300)、中证沪港深 500(H30455)和恒生指数(HSI),均线周期可选 120、180、250 或 500 个指数交易日,默认 180 日。执行 T 日计划时,使用 T 日之前最近一个指数交易日的收盘价(即正常情况下的 T-1)和截至该日的均线比较:偏离不超过 2% 时仍按基准金额的 100%;高于均线 (2,3]、(3,4]、(4,5]、(5,6]、(6,+∞) 时依次按 90%、80%、70%、60%、50%;低于均线的相同区间依次按 110%、120%、130%、140%、150%。页面可在保存前联网预览当前档位。
这里的“执行”只会在 Haruka 内生成资金流水,不会连接招商银行、基金销售平台或自动发起真实扣款和申购。
沪深 300 和中证沪港深 500 日线来自中证指数官网,恒生指数日线来自东方财富公开行情;行情会持久缓存。联网更新失败但本地已有足够的历史数据时会明确提示并继续使用缓存,缓存不足时保留待执行计划并报错,不会静默按固定金额执行。每次成功执行都会固化基准金额、扣款比例、行情日期、收盘点位和均线,之后行情数据变化也不会改写历史计算依据。手续费按聪明定投调整后的本期本金计算。
haruka 按北京时间判断交易日:周六、周日不执行,并排除上海证券交易所公告的休市日。程序内置 2025、2026 年官方休市安排;后续年份可在定投页面的“交易日历校准”中补充工作日休市日期。
由于数据库 DEK 只存在于已解锁的内存会话,服务锁定或重启后不能在后台无人值守地解密定投金额。期间遗漏的交易日会保留,用户解锁并访问定投页后由联网 AJAX 幂等补执行;同一计划同一交易日最多生成一次流水。余额不足时计划会保持待执行并显示原因,不会静默跳过。短信到达时若已有任一解锁会话会立即处理;否则原文只暂存在服务内存,待下次解锁后处理,不会明文落盘,但服务重启会丢失尚未处理的内存短信。处理后的原文和结果使用账本 DEK 加密保存。
基金不维护代码、份额或每日净值;聪明定投读取的指数行情只用于计算本期扣款比例,不代表基金净值或真实成交价。投资分类的“批量校准基金价值”页面左侧显示每只基金当前价值,右侧预填可编辑的实际价值。留空或未修改的行不写入;明确填写 0 会清空该基金价值。价值未变但已核对时可勾选“已核对”,只更新核对日期并保留零差额审计。整批校准在同一事务中提交;任何已修改或已核对的基金出现新流水或估值冲突,整批拒绝保存。校准差额只产生不可删除的余额调整审计,不生成转账,也不计入普通收入、支出或预算。
建议每月从基金平台核对各基金实际持仓价值。正持仓满 30 天未校准或核对时,仪表板会提示,并可从顶部“基金校准”(/funds/valuation-reminders)进入;空持仓不提醒。分类总价值随各基金价值变化自动汇总,不能独立覆盖。
快捷指令“获取 URL 内容”应使用完整的 Haruka 地址加 /api/sms、POST 方法和 JSON 请求体,不要选“表单”或“文件”。携带 Content-Type: application/json、Accept: application/json 和 Authorization: Bearer <短信 API 令牌>;令牌优先使用设置页配置,没有页面专属令牌时才回退到环境变量或兼容默认值。局域网接入时,手机不能把 127.0.0.1 当成运行 Haruka 的电脑,应使用电脑的局域网地址,并确认服务监听该网卡。
raw 填短信原文,time 填对应短信时间的带时区 ISO8601 值,sender 可省略。项目分享的快捷指令默认把执行时的“当前日期”格式化为 ISO 8601,适合即时触发;延迟执行或手动重放历史短信时,必须改用该条短信对应的时间。内置实时转账和快捷支付按北京时间核对到分钟,定投核对日期;不要通过放宽校验或改成当前时间绕过日期检查。
应查看返回的 JSON,而不只判断“获取 URL 内容”动作有没有完成:
202、status: queued:当前账本未解锁,已进服务内存队列,解锁后补处理;重启会丢失尚未处理的队列。200、status: executed:定投已记账;duplicate表示没有重复记账;pending表示仍需在短信页选择目标账户确认。200、ok: false:查看message。unmatched可能是短信格式、时间或账户/基金配置不匹配;failed是银行报告扣款失败。内置普通快捷支付只保留审计,不自动创建普通支出;要自动记普通消费,需明确配置绑定账户与分类的自定义支出模板。- HTTP
400/422:检查 JSON 内容、字段类型和时间格式;401检查令牌;405表示请求方法不支持,GET /api/sms返回405且Allow: POST;415检查 JSON 请求体与 Content-Type。/api/sms的 HTTP 错误始终返回{ ok: false, status, error }JSON,即使调用方漏发协商头,也不返回 HTML。
可以先执行下面的不带鉴权头的 POST,检查手机所用地址对应的连接链路。正常应收到 401 和 JSON“短信接口认证失败”;这个请求不会进入短信队列或记账:
curl --include --max-time 15 \
-H 'Content-Type: application/json' \
--data '{"time":"2026-10-08T12:30:00+08:00","raw":"无鉴权连通性检查"}' \
'https://your-haruka.example/api/sms'The network connection was lost 本身没有指出 HTTP 状态或模板是否匹配。若仍断连,应对照同一时间的服务日志与反向代理日志,区分 TLS/代理/手机网络中断与服务端 panic;提供脱敏短信、实际 time、方法及请求体类型即可排查,不要公开令牌。内置金额与余额换算使用有界乘法,超范围值返回明确处理结果,不得因十进制乘法溢出关闭连接。
进入“短信 → 短信模板”(/sms/templates),可以新增、编辑、启停和删除模板。页面提供招商银行定投成功/失败、实时转账、快捷支付以及通用金额/内容预设;点击预设只填入可编辑草稿和样例,不会保存或记账。
每个模板配置以下内容:
- 发送号码、银行标记:至少设置一个,例如
95500和示例银行。银行标记匹配短信中【】内的完整内容;两项同时填写时必须全部符合。配置号码条件后,短信转发工具必须在sender中传入实际发送号码。号码和银行标记只是匹配条件,不能代替 API 令牌认证。 - 命名正则:用
(?P<amount>...)提取金额、(?P<content>...)提取备注内容。定投可以额外提取fund、last4,也可直接绑定一个启用的短信实扣计划;未绑定计划时必须提取基金名和卡尾号,绑定后提取到的基金名或卡尾号仍须校验。如果提取year/month/day/hour/minute,会与time的北京时间核对。 - 匹配后动作:默认仅保存审计,不改变余额。可选定投成功/失败、转出候选、转入候选,或明确启用自动生成普通支出/收入账单。普通账单必须选择账户及对应类型分类;金额使用所选账户原币,执行前仍校验余额、授信额和信贷服务余额上限。
例如,短信 【示例银行】支出12.34元,内容:午餐。 可以使用:
^【示例银行】支出(?P<amount>[0-9]+(?:\.[0-9]{1,2})?)元,内容:(?P<content>.+?)[。.]?$先填写样例时间、号码与原文,点击“预览匹配结果(不保存)”查看金额和内容,确认后再保存启用。预览不接收短信、不保存样例、不创建审计或资金流水,也不会校验其他已保存模板是否同时匹配。表达式使用 Rust regex,不支持环视和反向引用,长度最多 4096 字节并限制编译复杂度;金额必须是正数且最多两位小数。
自定义模板优先匹配,但多个启用模板同时匹配同一短信时会明确报错,不会按顺序随意选择或记账。可以停用或收紧重叠模板,再在短信页补处理;余额不足或配置引用失效的事件也保留供修复后重试,已成功事件不会再次写入账单。
转入/转出模板只生成候选:模板绑定自己的来源/目标账户,用户须在短信页选择另一同币种账户后确认;确认会按实际方向校验两端余额,不允许用信用额度转出。候选保存独立的加密匹配快照,因此事后编辑或删除模板不改变已识别的金额、方向和账户绑定。短信入账仅记入 Haruka 账本,不向银行发起支付。
所有待确认候选都可选择“确认并记账”或“忽略,不记账”。忽略不会生成账单或转账,也不改变余额;忽略后默认列表隐藏该记录,可筛选“已忽略”查看保留的加密审计。重复接收同一短信或点击补处理不会将其恢复为待确认;已记账的记录不能通过忽略撤销。确认与忽略共用写入锁,同一候选不能同时完成两种操作。
模板名称、号码、银行标记、表达式以及短信原文、号码和解析快照均使用账本 DEK 加密保存。锁定时短信只能暂存在服务内存;补处理读取数据库失败不会消费内存队列,逐条失败也会保留供重试。现有数据库会在启动时自动补齐字段,无需删除数据库。
仓库内有两条构建工作流:
.github/workflows/build.yml会在 push、Pull Request 或手动触发时构建 Linux 二进制;.github/workflows/container.yml会构建linux/amd64和linux/arm64镜像。Pull Request 只验证镜像能够构建,推送到main或推送v*标签时才发布到 GHCR。
二进制工作流会执行以下检查:
- 使用
npm ci安装锁定的前端依赖并重新生成浏览器资源; - 检查仍跟踪的
static/浏览器运行库、OCR 模型和扫描脚本是否与锁定依赖一致;生成的static/app.css不提交,也不纳入资源一致性检查; - 执行
cargo fmt --check和cargo build --locked --release; - 上传保留 14 天的
haruka-linux-x86_64构建产物。
二进制工作流使用 Swatinem/rust-cache 缓存 Cargo 依赖和 target/ 编译产物(包含本项目),而不只是下载的依赖源码。缓存随 Rust 编译器、依赖和构建环境变化而失效;恢复缓存后仍正常执行格式检查和 release 构建,由 Cargo 判断哪些文件需要重新编译。
从 GitHub Actions 页面下载并解压 haruka-linux-x86_64.tar.gz 后即可取得在 Ubuntu 24.04 x86_64 上构建的可执行文件。CSS、前端运行库和 OCR 模型均已嵌入二进制,部署机器不需要安装 Node.js,也不需要额外复制 templates/ 或 static/。其他操作系统或较旧的 Linux 发行版建议按照“本地运行”一节从源码构建。
容器工作流使用仓库自带的 GITHUB_TOKEN 发布 ghcr.io/<仓库所有者>/haruka,不需要额外配置镜像仓库密码。main 对应 latest 和 main 标签,版本标签(例如 v0.2.0)会发布 0.2.0、0.2 等标签,同时每次构建还会生成提交 SHA 标签。首次发布后可在 GitHub Packages 设置中将镜像改为 Public;如果保持 Private,部署机器需要先使用具有 read:packages 权限的令牌执行 docker login ghcr.io。
容器工作流通过矩阵在 ubuntu-24.04 和 ubuntu-24.04-arm 上分别原生构建 AMD64、ARM64,不再使用 QEMU 模拟编译。每个架构先用 cargo-chef 构建 Rust 依赖,并将包含编译产物的镜像层独立导出到 haruka-rust-deps-amd64 / haruka-rust-deps-arm64 缓存;生产镜像同时读取该架构的依赖缓存和 haruka-container-amd64 / haruka-container-arm64 镜像缓存,避免不同架构互相覆盖。日常源码、模板和静态资源修改可复用依赖层,应用仍会重新编译;基础镜像固定到 digest,升级基础镜像或修改依赖时会重建依赖层。依赖步骤成功导出后,即使后续镜像构建失败或取消,该架构的依赖缓存仍能供下一次使用。首次切换到新缓存作用域需要冷构建。
两种架构各自有 60 分钟的 job 上限,并行构建时互不占用对方的时间;一个架构失败不会提前取消另一个架构的缓存构建,但会阻止正式标签发布。非 PR 构建先按 digest 推送各架构镜像,保留 provenance 和 SBOM;仅在两者全部成功后,由独立的 10 分钟合并 job 发布包含双架构的 latest、分支、版本及 SHA 标签。合并 job 只下载 digests-* 产物,避免误读 Docker 构建记录。PR 使用显式 cacheonly 输出验证两个架构,不登录或推送 GHCR,不上传发布用 digest 或运行合并 job。仍保留构建检查和警告输出;冷构建耗时及缓存可用性取决于 runner 和网络,不能保证固定完成时间。
镜像默认监听容器内的 0.0.0.0:3000,默认把 SQLite 数据库放在 /data/haruka.db。下面使用 Docker 命名卷保存整个数据库目录,并只把服务发布到宿主机回环地址,适合在 Caddy、Nginx 等 HTTPS 反向代理后运行:
如需先在本机从当前源码构建镜像,可执行 docker build --tag haruka:local .;多阶段 Dockerfile 会在构建阶段重新生成 Tailwind CSS 和 release 二进制,最终镜像不包含 Node.js、Rust 工具链或源码。
export HARUKA_IMAGE='ghcr.io/YOUR_GITHUB_OWNER/haruka:latest'
docker volume create haruka-data
docker pull "$HARUKA_IMAGE"
docker run -d \
--name haruka \
--restart unless-stopped \
--publish 127.0.0.1:3000:3000 \
--mount type=volume,src=haruka-data,dst=/data \
--env PORT=3000 \
--env PASSKEY_ORIGIN='https://haruka.example.com' \
--env PASSKEY_RP_ID='haruka.example.com' \
"$HARUKA_IMAGE"将 YOUR_GITHUB_OWNER 替换为发布镜像的 GitHub 用户名或组织名,并把 Passkey 两项改成实际对外域名。升级时保留同一个命名卷并重建容器:
docker pull "$HARUKA_IMAGE"
docker rm --force haruka
docker run -d \
--name haruka \
--restart unless-stopped \
--publish 127.0.0.1:3000:3000 \
--mount type=volume,src=haruka-data,dst=/data \
--env PORT=3000 \
--env PASSKEY_ORIGIN='https://haruka.example.com' \
--env PASSKEY_RP_ID='haruka.example.com' \
"$HARUKA_IMAGE"也可以使用 Compose:
services:
haruka:
image: ghcr.io/YOUR_GITHUB_OWNER/haruka:latest
restart: unless-stopped
ports:
- "127.0.0.1:3000:3000"
environment:
PORT: "3000"
PASSKEY_ORIGIN: https://haruka.example.com
PASSKEY_RP_ID: haruka.example.com
volumes:
- haruka-data:/data
volumes:
haruka-data:保存为 compose.yaml 后执行 docker compose up -d。镜像已经设置 DATABASE_URL=sqlite:///data/haruka.db?mode=rwc;如需改用其他容器内目录或文件名,可在 environment 中覆盖。备份时应备份整个 haruka-data 卷,因为 SQLite 运行时可能同时存在 WAL 和 SHM 文件。
如果要直接向局域网发布而不使用同机反向代理,可把端口映射改成 3000:3000。Passkey 在非 localhost 环境仍然需要 HTTPS,不能仅靠开放 HTTP 端口使用。
不传配置时 haruka 只监听 127.0.0.1:3000。配置优先级为:命令行参数、LISTEN_ADDR、PORT、默认值。
| 配置 | 行为 |
|---|---|
--listen 127.0.0.1:8080 |
精确设置监听地址和端口,适合放在同机反向代理后面 |
--port 8080 |
监听 0.0.0.0:8080 |
LISTEN_ADDR=0.0.0.0:8080 |
通过环境变量精确设置监听地址和端口 |
PORT=8080 |
监听 0.0.0.0:8080,适合自动注入 PORT 的部署平台 |
SMS_API_TOKEN=随机长令牌 |
设置 /api/sms 的 Bearer Token;未配置时为兼容当前调用方使用 AppleToken |
直接运行二进制时:
./haruka --listen 127.0.0.1:8080
./haruka --port 8080
PORT=8080 ./haruka从源码运行时,需要用 -- 把参数传给 haruka:
cargo run -- --port 8080--port 和 PORT 会监听所有网络接口。若不需要外部设备直接访问,建议使用 --listen 127.0.0.1:端口,再通过 HTTPS 反向代理提供服务。
默认数据库是当前工作目录下的 haruka.db。生产环境建议把数据库放在持久化目录,并确保运行用户拥有该目录的写权限:
DATABASE_URL='sqlite:///var/lib/haruka/haruka.db?mode=rwc' \
./haruka --listen 127.0.0.1:3000持久化或备份时需要保留 haruka.db,以及 SQLite 运行期间可能出现的 haruka.db-wal、haruka.db-shm。容器部署时应把整个数据库目录挂载到持久卷,而不是只挂载单个文件。
服务器还需要能够通过 HTTPS 访问 api.frankfurter.dev 获取汇率。网络不可用时会回退到数据库中的历史汇率缓存;某个货币对完全没有缓存时,相关操作会向客户端明确报错。
生产环境的 Passkey 必须使用 HTTPS,并且公开来源和 RP ID 在首次注册后应保持不变:
DATABASE_URL='sqlite:///var/lib/haruka/haruka.db?mode=rwc' \
PASSKEY_ORIGIN='https://haruka.example.com' \
PASSKEY_RP_ID='haruka.example.com' \
./haruka --listen 127.0.0.1:3000例如使用 Caddy 终止 TLS:
haruka.example.com {
reverse_proxy 127.0.0.1:3000
}PASSKEY_ORIGIN 必须是用户在浏览器中实际访问的完整 HTTPS 来源,PASSKEY_RP_ID 只填写域名。切换域名、协议或 RP ID 后,原有 Passkey 将无法继续使用。
假设二进制位于 /opt/haruka/haruka,数据库目录为 /var/lib/haruka:
[Unit]
Description=haruka accounting service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=haruka
Group=haruka
WorkingDirectory=/var/lib/haruka
ExecStart=/opt/haruka/haruka --listen 127.0.0.1:3000
Environment="DATABASE_URL=sqlite:///var/lib/haruka/haruka.db?mode=rwc"
Environment="PASSKEY_ORIGIN=https://haruka.example.com"
Environment="PASSKEY_RP_ID=haruka.example.com"
Restart=on-failure
RestartSec=3
[Install]
WantedBy=multi-user.target修改后执行 systemctl daemon-reload,再使用 systemctl enable --now haruka 启动服务。
本地默认使用当前监听端口对应的 http://localhost:<端口> 作为 WebAuthn 来源;需要用这个地址访问(不要改用 127.0.0.1)才能注册和登录 Passkey。部署到其他域名时固定配置:
PASSKEY_ORIGIN=https://haruka.example.com PASSKEY_RP_ID=haruka.example.com cargo run生产环境必须使用 HTTPS。已有 Passkey 与来源和 RP ID 绑定,后续不要随意更改这两个值。
本机登录入口使用可发现凭据流程,包含通过 iCloud 钥匙串同步到当前设备的 Passkey;手机和 Mac 必须访问注册时相同的 HTTPS 域名与 RP ID,不能把 Mac 的 localhost 换成局域网 IP。同步了凭据不等于当前系统或凭据提供方支持用于解密的 PRF;没有 32 字节 PRF 输出时不能降级为仅凭签名解锁。
注册追加确认保持原认证器模式,并只允许选择刚创建的凭据。若 WebAuthn 签名已验证,但本次 PRF 无法解开已有 DEK 包裹,登录页允许在五分钟内用主密码做一次设备兼容绑定;新包裹追加保存,原 Mac 或其他设备的包裹不被覆盖。绑定过期可重新点击 Passkey 进行认证,不需要删除原凭据。外部登录按钮目前保持隐藏,设置页仍可选择外部认证器注册。
Passkey 注册与兼容绑定表单禁用 htmx boost,由页面 JavaScript 提交,避免同时发起刷新请求而中断注册或清空绑定提示。主密码错误会保留绑定表单供重试;绑定成功后通过完整导航进入仪表板。
服务端备份状态变化、相同 PRF 登录及不同 PRF 的主密码绑定路径已用软件认证器签名验证;这不替代真实 iPhone/iCloud 的兼容性验证。若同步后无法登录,请记录页面错误原文,并核对两端访问域名与系统的 PRF 支持。
- 内置订阅管理,妈妈再也不用担心我忘了续费啦!
- 恩格尔系数看板(闲着没事写上去的哈哈哈)
- 自动化 OCR 设计,你只需要确认
- 可选的AI Endpoint(AI传输内容不过服务端,你怎么设置怎么来,你完全可以使用本地的 ollama 来进行回复!)
- 自带 iCloud Shortcuts,你甚至可以直接截图然后自动记账(截图目前预计支持微信/支付宝/四大加一招行)
- 货币支持(CNY/HKD/USD,汇率随时变动)
- 可能的ETF,持仓等分析
本项目使用 vibe coding 技术强力驱动并使用 MIT 授权协议,你想怎么用就怎么用去。