# TallyNote TallyNote 是一个本地优先的采购报销记录网站:记录支付时间、金额、备注、付款凭证和发票,按月整理后导出 Excel 与原始附件。 每笔活动账目至少需要一张付款凭证;发票与“无发票原因”严格二选一。没有发票时,在新增或编辑抽屉勾选“无发票”并填写原因,原因会显示在列表、详情和导出的 Excel“无发票原因”列中。删除最后一张发票时,系统也会在确认弹窗中要求填写原因,并与删除操作原子保存。 导出 ZIP 默认包含 `报销清单.xlsx` 和附件目录。账目列表中的“包含 manifest.json”选项默认关闭;开启后会额外导出附件元数据及 SHA-256 校验值清单。 导出目录示例: ```text TallyNote_报销资料_xxxxxxxx.zip ├── 报销清单.xlsx ├── 001_20260827_12.34_ab12cd34/ │ ├── 付款凭证/ │ │ └── 付款截图.png │ └── 发票/ │ └── invoice.pdf └── manifest.json # 仅勾选“包含 manifest.json”时生成 ``` ## 本地运行 ```bash pnpm install pnpm admin:init pnpm dev ``` 生产模式: ```bash pnpm build pnpm start ``` 首次初始化会要求交互式输入管理员密码。也可以使用 `pnpm admin:init -- --username admin --display-name 管理员 --generate` 生成一次性临时密码。 默认地址为 `http://127.0.0.1:3000`,开发界面为 `http://127.0.0.1:5173`。配置项见 `.env.example`。 ## 无 Docker 安装(systemd) 安装器正式支持 **Linux x86_64(x64)**,脚本和运行时也支持在对应原生 runner 上发布 **aarch64(arm64)**;当前仓库内置 workflow 只生成 x64,arm64 需要在原生 ARM64 runner 上单独构建并发布。ARMv7/ARM32 仅实验性支持;Linux x86 32 位(`i386`、`i686`、`ia32`)明确不支持,因为 Node.js 24 和项目原生依赖没有可维护的官方构建。不要在 32 位系统上强行安装。 发布包必须包含 `dist/`、生产依赖、匹配架构的 Node runtime、systemd 单元,以及 `SHA256SUMS` 和 `SHA256SUMS.sig`。安装器默认 dry-run,只有显式 `--apply` 才会下载或写盘;正式安装必须提供独立核对过的 Ed25519 公钥: ```bash curl --proto '=https' --tlsv1.2 -fsSL \ https://git.awaioi.com/awaioi/TallyNote/raw/branch/main/install.sh \ | sudo bash -s -- --apply --version 1.0.1 \ --signing-key /root/tallynote-update.pub \ --update-public-key-file /root/tallynote-update.pub ``` 指定版本时,脚本会从 `https://git.awaioi.com/awaioi/TallyNote/releases/download/v<版本>/` 获取归档、`SHA256SUMS` 和签名。也可以通过 `TALLYNOTE_REPOSITORY_URL`、`TALLYNOTE_RELEASE_API_URL`、`TALLYNOTE_RELEASE_ALLOWED_HOSTS` 和 `--release-base-url` 指向自己的仓库或受信 CDN。`--allow-unsigned` 仅供隔离开发机测试,不能用于公网或真实财务数据。 已有安装默认拒绝降级到不高于当前版本;确需回退时显式使用 `--allow-downgrade`,正常更新不会覆盖当前或更高版本。 安装布局为 `/opt/tallynote/releases/` 加 `/opt/tallynote/current` 符号链接;切换通过临时链接和原子重命名完成。root 更新器使用前缀下独立的 `/opt/tallynote/.update-work`(`0700 root:root`)和 `.update-state` 恢复标记,不会把 root 解包工作区放进应用可写暂存目录。SQLite 数据、附件、暂存、导出和更新队列始终在外置 `/var/lib/tallynote`,不会随版本包删除。服务单元位于 `/etc/systemd/system/tallynote.service`,配置文件为 `/etc/tallynote/tallynote.env`,默认仅监听 `127.0.0.1:3000`。 升级有两种方式: 1. 后台进入“系统更新”,点击“检查更新”后确认版本。应用只会把经过 HTTPS、主机白名单、SHA-256 和 Ed25519 签名校验的请求写入队列;root 权限的 `tallynote-update.path`/`tallynote-update.service` 会重新获取配置源、验证签名,再执行停机、备份、切换和健康检查。Web 进程没有 `systemctl` 权限,队列中的 URL、文件地址和摘要不会直接驱动 root 下载。 2. 手动执行 `sudo /usr/local/sbin/tallynote-update --rollback` 可切回上一份 release。更新失败会自动保留旧版本并尝试恢复;不要删除 `/var/lib/tallynote`。 更新任务详情按发起管理员隔离;失败信息在浏览器中使用固定提示,不暴露服务器路径、命令输出或上游响应。系统同一时刻只允许一个更新任务。 公网反代必须使用 HTTPS,并在环境文件中设置真实的 `TALLYNOTE_PUBLIC_ORIGIN=https://...`、`TALLYNOTE_COOKIE_SECURE=true` 和明确的 `TALLYNOTE_TRUST_PROXY` 跳数(不要使用生产值 `true`)。 ### 构建发布包 在目标 Linux 架构的 CI runner 上执行(不能在 macOS 上冒充 Linux 架构): ```bash pnpm install --frozen-lockfile pnpm release:build 1.0.1 ./release ``` 将生成的 `tallynote-<版本>-linux-<架构>-.tar.gz` 上传到同一个 Gitea Release。推荐由 `.gitea/workflows/release.yml` 自动执行 `scripts/publish-gitea-release.sh`,统一生成并上传 `SHA256SUMS` 与 `SHA256SUMS.sig`;当前仓库还没有首个 tag/release 时,后台会明确显示不可用,不会下载未验证文件。CI 需要 `GITEA_TOKEN` 和 `TALLYNOTE_RELEASE_SIGNING_KEY` secrets。 版本由 `package.json` 和 Git tag 双重约束:两者必须相同(例如 `1.0.1` 与 `v1.0.1`),workflow 会在构建前拒绝不一致的 tag。发布一个版本: ```bash git add . git commit -m "release: 1.0.1" git tag -a v1.0.1 -m "TallyNote 1.0.1" git push origin main --follow-tags ``` ## Docker ```bash docker compose up -d --build docker compose run --rm --no-deps tallynote node dist/server/cli/admin-init.js --username admin --display-name 管理员 --generate ``` 只运行一个应用副本,并将 `/data` 作为持久化卷。SQLite、附件和导出文件必须位于同一台主机的本地文件系统;不支持 NFS/NAS 或多个副本共享 SQLite。 ## 备份 业务导出不是系统备份。停服后复制完整数据目录(数据库、WAL/SHM、`files/`、`staging/`、`exports/` 和更新任务文件),恢复时保持目录 `0700`、文件 `0600` 权限,并在启动前确保没有其他 TallyNote 进程使用该目录。更新器会在切换前额外写入 `/var/lib/tallynote-backups/`,但仍建议保留服务器级备份。 应用层会拒绝非 HTTPS 更新源、未匹配主机、无 SHA-256/签名的归档、路径穿越、特殊文件和符号链接;附件与导出下载需要登录并写入审计。拥有服务器文件权限的人仍然可以直接读取 SQLite 和附件,部署时应限制 SSH、备份和磁盘权限,并通过 HTTPS 反代访问。