# 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 dev ``` 生产模式: ```bash pnpm build pnpm start ``` 默认开发和生产构建都使用腾讯 TDesign React 前端。需要单独检查或构建前端时,可以使用: ```bash pnpm check:next pnpm build:next ``` `build:next` 与 `pnpm build` 一样输出到 `dist/web`,可直接由生产 Fastify 服务提供。 本地开发首次初始化管理员使用 `pnpm admin:init`。生产安装器会在首次安装时提供管理员初始化向导;如果选择稍后创建,执行 `sudo tallynote-admin-init` 即可。也可以使用 `sudo tallynote-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/`(包括 `dist/server/cli/admin-init.js`)、生产依赖、匹配架构的 Node runtime、systemd 单元、`bin/tallynote-admin-init`、`uninstall.sh`,以及 `SHA256SUMS`。签名文件 `SHA256SUMS.sig` 是可选增强校验,不需要为普通安装准备公钥。安装器默认直接安装最新版本: ```bash curl --proto '=https' --tlsv1.2 -fsSL https://git.awaioi.com/awaioi/TallyNote/raw/branch/main/install.sh | sudo bash ``` 安装命令保持简洁。首次在 SSH/终端中安装时,安装器会交互询问监听方式、端口和公开访问地址: ```bash curl --proto '=https' --tlsv1.2 -fsSL https://git.awaioi.com/awaioi/TallyNote/raw/branch/main/install.sh | sudo bash ``` 首次安装完成网络配置后,向导会询问是否立即创建管理员。选择创建时,用户名、显示名称和密码都在当前 SSH 终端中输入;选择稍后创建也不会阻塞服务启动,之后执行 `sudo tallynote-admin-init` 即可。升级已有安装时,向导会自动识别现有管理员并跳过创建,不会覆盖账号或账目。 监听方式有两个选项:`127.0.0.1` 仅本机访问(默认、更安全),或 `0.0.0.0` 允许通过局域网/公网 IP 访问。安装时可输入自定义端口(直接回车使用默认端口),安装器会检查 TCP 端口是否已被占用;选择 `0.0.0.0` 时会尝试通过 HTTPS 自动获取公网 IPv4,并将 `http://公网IP:端口` 作为默认访问地址,也可以改填域名。不能填写 `http://0.0.0.0:3000`。直连 HTTP 未加密,安装器会要求明确确认,只适合受控网络。绑定域名后应改为 HTTPS 反向代理,设置真实的 `TALLYNOTE_PUBLIC_ORIGIN`、`TALLYNOTE_COOKIE_SECURE=true`、`TALLYNOTE_ALLOW_INSECURE_HTTP=false`,然后执行 `sudo systemctl restart tallynote.service`。服务启动后,安装器会先请求本机 `/health`;只有健康检查通过才会报告安装完成并输出最终访问链接。监听 `127.0.0.1` 时该链接只对服务器本机有效;需要公网或其他设备访问时请选择 `0.0.0.0`。健康检查失败时会输出 systemd 状态和最近日志并回滚本次切换。 安装器不会在已有安装的升级过程中反复询问网络配置,并会保留现有环境文件。自动化或无终端环境可使用 `--non-interactive`(默认安全配置 `127.0.0.1:3000`),也可以显式传入 `TALLYNOTE_HOST`、`TALLYNOTE_PORT`、`TALLYNOTE_PUBLIC_ORIGIN` 和 `TALLYNOTE_ALLOW_INSECURE_HTTP` 覆盖配置。 非交互安装命令: ```bash curl --proto '=https' --tlsv1.2 -fsSL https://git.awaioi.com/awaioi/TallyNote/raw/branch/main/install.sh | sudo bash -s -- --non-interactive ``` 脚本会从公开仓库的 latest Release 获取当前架构归档和 `SHA256SUMS`,并在安装前始终校验 SHA-256。也可以通过 `TALLYNOTE_REPOSITORY_URL`、`TALLYNOTE_RELEASE_API_URL`、`TALLYNOTE_RELEASE_ALLOWED_HOSTS` 和 `--release-base-url` 指向自己的仓库或受信 CDN。需要固定版本或预览时,仍可使用 `TALLYNOTE_VERSION`、`--version` 或 `--dry-run` 等高级选项。 安装过程会持续输出带统一前缀的阶段日志,不会在下载、校验或启动服务时静默等待。交互式 SSH/终端中会先显示网络配置选择,下载时还会显示 curl 进度条;非交互式运行(例如 CI)只输出干净的阶段日志。典型输出如下(版本号、架构和耗时会按实际环境变化): ```text tallynote installer: [阶段] 检查运行环境、权限和目标架构 tallynote installer: [完成] 运行环境可用:x64/glibc tallynote installer: [阶段] 从 Release API 获取最新版本 tallynote installer: [完成] 已解析最新版本:1.1.2 tallynote installer: [完成] Release 下载地址已准备 tallynote installer: [阶段] 获取发布包:tallynote-1.1.2-linux-x64-glibc.tar.gz tallynote installer: [完成] 发布包已下载并通过大小限制 tallynote installer: [阶段] 获取 SHA-256 校验清单 tallynote installer: [完成] SHA-256 校验清单已准备 tallynote installer: [阶段] 校验 SHA-256 和发布签名 tallynote installer: [完成] 发布包校验通过 tallynote installer: [阶段] 解包、校验包结构并原子切换到版本 1.1.2 tallynote installer: [完成] 版本 1.1.2 已切换为当前版本 tallynote installer: [阶段] 安装 systemd 单元、更新辅助程序和卸载器 tallynote installer: [完成] systemd 单元、更新辅助程序和卸载器已安装 tallynote installer: [阶段] 重新加载 systemd 并启动 TallyNote tallynote installer: [完成] TallyNote 服务已启用并启动 tallynote installer: [阶段] 清理旧版本并完成安装 tallynote installer: [完成] 旧版本清理完成 tallynote installer: [完成] 安装完成:TallyNote 1.1.2 tallynote installer: 访问地址:http://127.0.0.1:3000 tallynote installer: 查看服务状态:systemctl status tallynote.service ``` 任何阶段失败都会以 `tallynote installer:` 前缀写出原因并立即停止;不会把不完整版本切换为当前版本。 如需额外启用签名校验,在环境中设置 `TALLYNOTE_INSTALL_REQUIRE_SIGNATURE=true` 并提供 `--signing-key`;不设置时不会要求公钥或 `SHA256SUMS.sig`。 已有安装默认拒绝降级到不高于当前版本;确需回退时显式使用 `--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`;监听地址、端口和公开 Origin 由该环境文件控制,默认仍是 `127.0.0.1:3000`。 升级有两种方式: 1. 后台进入“系统更新”,点击“检查更新”后可先“下载更新包”,等待校验完成,再点击“立即更新”。应用只会把经过 HTTPS、主机白名单和 SHA-256 校验的请求写入队列;如果显式配置了公钥,再额外验证 Ed25519 签名。下载阶段主服务保持运行;应用阶段才会停机、备份、切换和健康检查,页面会显示重启倒计时并自动重试连接。Web 进程没有 `systemctl` 权限,队列中的 URL、文件地址和摘要不会直接驱动 root 下载。 2. 手动执行 `sudo /usr/local/sbin/tallynote-update --rollback` 可切回上一份 release。更新失败会自动保留旧版本并尝试恢复;不要删除 `/var/lib/tallynote`。 更新任务详情按发起管理员隔离;失败信息在浏览器中使用固定提示,不暴露服务器路径、命令输出或上游响应。系统同一时刻只允许一个更新任务。 ### 卸载 安装完成后会提供 `/usr/local/sbin/tallynote-admin-init` 和 `/usr/local/sbin/tallynote-uninstall`。普通卸载会停止并禁用 TallyNote 的 systemd 单元,删除当前版本、更新辅助程序、管理员初始化命令和已知配置,但保留 `/var/lib/tallynote` 以及更新备份,方便以后重新安装: ```bash sudo /usr/local/sbin/tallynote-uninstall ``` 如果确认不再需要数据库、附件、暂存、导出和更新备份,必须显式同时提供 `--purge-data --yes`: ```bash sudo /usr/local/sbin/tallynote-uninstall --purge-data --yes --purge-config ``` 卸载检测到未完成的更新状态时会停止并要求人工确认;确认更新已停止后再加 `--force`。也可以直接从公开仓库获取同一脚本执行普通卸载: ```bash curl --proto '=https' --tlsv1.2 -fsSL https://git.awaioi.com/awaioi/TallyNote/raw/branch/main/uninstall.sh | sudo bash ``` 卸载器会逐项输出停止、禁用和删除进度;每次 systemd/dbus 调用默认最多等待 30 秒,避免终端无限无响应。可通过 `TALLYNOTE_UNINSTALL_SYSTEMCTL_TIMEOUT_SECONDS` 调整超时时间。 公网反代推荐使用 HTTPS,并在环境文件中设置真实的 `TALLYNOTE_PUBLIC_ORIGIN=https://...`、`TALLYNOTE_COOKIE_SECURE=true` 和明确的 `TALLYNOTE_TRUST_PROXY` 跳数(不要使用生产值 `true`)。反代只需把域名转发到 TallyNote 端口并保留 `Host`、`X-Forwarded-Proto`;应用不会因为代理缺少或改写浏览器 `Origin` 而拦截登录。已认证写请求仍使用会话 Cookie 与 CSRF 令牌保护。 ### 构建发布包 在目标 Linux 架构的 CI runner 上执行(不能在 macOS 上冒充 Linux 架构): ```bash pnpm install --frozen-lockfile pnpm release:build 1.1.2 ./release ``` 将生成的 `tallynote-<版本>-linux-<架构>-.tar.gz` 上传到同一个 Gitea Release。推荐由 `.gitea/workflows/release.yml` 自动执行 `scripts/publish-gitea-release.sh`,统一生成并上传 `SHA256SUMS`;如果 CI 提供签名私钥,还会额外上传 `SHA256SUMS.sig`。CI 只需要 `GITEA_TOKEN`;签名私钥属于可选增强。 版本由 `package.json` 和 Git tag 双重约束:两者必须相同(例如 `1.1.2` 与 `v1.1.2`),workflow 会在构建前拒绝不一致的 tag。发布一个版本: ```bash git add . git commit -m "release: 1.1.2" git tag -a v1.1.2 -m "TallyNote 1.1.2" 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 反代访问。