TaxHacker 数据迁移实战:v0.3 升级 v0.5 的 SQLite → PostgreSQL 完整指南
【免费下载链接】TaxHackerSelf-hosted AI accounting app. LLM analyzer for receipts, invoices, transactions with custom prompts and categories项目地址: https://gitcode.com/GitHub_Trending/ta/TaxHacker
TaxHacker 在 v0.5 版本中完成了底层数据库从 SQLite 到 PostgreSQL 的切换,由于两种数据库无法无缝在线迁移,旧实例的数据需要借助应用内置的备份/恢复功能手动搬运。本文将围绕迁移文档(docs/migrate-0.3-0.5.md)给出的四步流程,结合当前仓库源码,讲解完整的迁移操作、备份 ZIP 归档的内部结构与恢复机制的底层实现,并给出大文件上传失败的排查方法。读完本文,你可以安全地在自己的自托管实例上完成 v0.3 → v0.5 的数据迁移,且不丢失任何交易记录、附件和配置。
迁移背景:为什么 v0.5 不能无缝升级
v0.5 将数据库从 SQLite 切换到了 PostgreSQL。从仓库的部署配置可以确认这一点:docker-compose.yml 中已经内置了独立的postgres服务(postgres:17-alpine),应用通过DATABASE_URL=postgresql://postgres:postgres@postgres:5432/taxhacker连接数据库;README 的环境变量表也将DATABASE_URL定义为 PostgreSQL 连接串(PostgreSQL 17+ 推荐)。
数据库引擎的更换意味着旧版本 SQLite 文件中积累的数据无法被新版本直接读取,因此官方文档明确说明:迁移需要手动完成。但有一个关键保证——即使你已经提前升级到了 v0.5,旧数据也没有丢失,它仍然安全地保存在旧实例的数据目录中,只要按正确顺序操作就能完整找回。
从源码结构看,Prisma 的迁移历史(prisma/migrations)从20250403104933_init起步,随后依次叠加了 storage、token limit、stripe、business details、app data、progress、cached parse result、split tx items 等迁移,最终演进到当前以 PostgreSQL 为底座的 schema,这从侧面印证了 v0.3 到 v0.5 之间数据库结构经历了重大变化。
整个迁移思路可以概括为四步:回滚到 v0.3.0 → 在旧版本上导出备份 → 升级到 latest → 在新版本上恢复备份。
Step 1:把 docker-compose 锁定回 v0.3.0
迁移的第一步是把应用镜像固定回 v0.3.0。之所以必须先回滚,是因为备份归档需要由旧版本自己的备份导出逻辑生成,才能保证与 v0.5 的恢复器兼容。修改 docker-compose 中app服务的镜像标签:
services: app: image: ghcr.io/vas3k/taxhacker:v0.3.0 ports: - "7331:7331" // 其余配置保持不变注意7331:7331是应用默认端口映射,容器内外均使用 7331 端口(对应 README 中PORT环境变量默认值)。除镜像标签外,其余所有配置(卷挂载、环境变量、服务依赖)都不要改动,避免引入额外变量。
Step 2:重启应用并导出备份归档
固定镜像版本后,重启应用使 v0.3.0 生效:
docker compose down docker compose up -d重启完成后,打开浏览器访问http://localhost:7331,进入Settings → Backups页面,点击Download Data Archive按钮,把生成的.zip归档文件保存到本地机器。
从源码看,这个按钮对应备份设置页面的下载逻辑(app/(app)/settings/backups/page.tsx/settings/backups/page.tsx>)):点击后会先启动一个 "backup" 类型的进度任务,然后请求GET /settings/backups/data?progressId=...下载taxhacker-backup.zip。下载期间界面会显示 "Archiving x/y files" 的实时进度。
备份归档内部结构
备份导出路由的实现位于 app/(app)/settings/backups/data/route.ts/settings/backups/data/route.ts>),它使用JSZip在内存中构建归档,内部结构如下:
taxhacker-backup.zip ├── data/ │ ├── metadata.json # 备份元信息:版本号、时间戳、包含的模型文件清单 │ ├── settings.json # 用户设置(含 LLM 提示词等) │ ├── currencies.json # 自定义货币 │ ├── categories.json # 分类 │ ├── projects.json # 项目 │ ├── fields.json # 自定义字段 │ ├── files.json # 文件元数据索引 │ ├── transactions.json # 全部交易记录 │ └── uploads/ # 上传的原始附件(收据、发票、PDF 等)几个值得注意的实现细节:
- 版本化元数据:归档根目录固定写入
metadata.json,其中version字段当前为"1.0",同时记录生成时间戳和模型清单。恢复端正是靠这个字段做兼容性校验。 - 模型数据以 JSON 导出:每个数据表对应一个 JSON 文件,导出逻辑由 models/backups.ts 中的
MODEL_BACKUP数组驱动,其顺序(settings → currencies → categories → projects → fields → files → transactions)在恢复时同样重要。 - 单文件 64MB 上限:导出时单个附件超过 64MB 会被跳过并打印警告(
Skipping large file ... > 64MB limit),避免把超大文件压入归档导致内存问题。 - 进度上报:每处理 2 秒或全部完成时向进度服务上报
current/total,这就是页面上进度文案的数据来源。
Step 3:升级 TaxHacker 到最新版本
备份归档安全落地后,把镜像标签改回最新版:
services: app: image: ghcr.io/vas3k/taxhacker:latest ports: - "7331:7331" // 其余配置保持不变再次重启让新版本(v0.5+,PostgreSQL 底座)生效:
docker compose down docker compose up -d此时新的实例使用全新的 PostgreSQL 数据库,是"干净"的状态,等待恢复数据。
Step 4:在新实例上恢复数据
进入Settings → Backups页面,找到Restore from a backup区域:选择之前保存的 ZIP 归档,勾选 "I understand that it will permanently delete all existing data" 确认框,点击Restore from backup。等待几秒钟(数据量大时按钮会显示 "Restoring from backup... (it can take a while)"),恢复成功后页面会展示详细的导入统计,列出每个数据文件的恢复条数。
恢复机制的源码级原理
恢复流程的服务端实现位于 app/(app)/settings/backups/actions.ts/settings/backups/actions.ts>) 的restoreBackupAction,整个流程分四步:
1. 归档校验。使用JSZip.loadAsync解压,若 ZIP 损坏直接报 "Bad zip archive";随后读取data/metadata.json校验版本,仅支持SUPPORTED_BACKUP_VERSIONS = ["1.0"],不匹配会提示 "Incompatible backup version"。另外上传文件超过MAX_BACKUP_SIZE = 256MB会直接拒绝。
2. 清空现有数据。cleanupUserTables按MODEL_BACKUP的逆序逐表deleteMany,先删交易、文件等引用方,再删分类、项目等被引用方,以规避外键约束;同时递归删除用户上传目录。
3. 按序恢复各表。遍历MODEL_BACKUP读取对应 JSON 文件,逐条调用modelFromJSON(models/backups.ts)写入数据库。恢复前preprocessRowData会做类型清洗:空字符串转null、JSON 字符串反序列化、ISO 日期字符串转Date、数字字符串转数值(id和*Code结尾的字段除外,保留字符串语义)。交易记录恢复时通过category: { connect: { userId_code: ... } }重新挂接分类与项目。
4. 恢复上传附件。根据files.json中记录的路径,从归档的data/uploads/目录提取对应二进制内容写回磁盘;写入前用safePathJoin并校验目标路径必须位于用户上传目录之内,若检测到路径穿越(path traversal)会记录错误并跳过,兼顾安全。文件元数据中的路径也会被规范化为相对路径后更新入库。
恢复成功后你能看到什么
页面会以绿色卡片展示 "Backup restored successfully" 及导入统计,例如:
settings.json: N itemstransactions.json: N itemsUploaded attachments: N items
至此,旧实例的交易记录、分类、项目、自定义字段、货币、设置以及所有附件都完整回到了新实例中。
故障排查:上传报"文件太大"怎么办
如果恢复时遇到关于文件大小的错误,原因出在 Next.js Server Actions 的请求体上限。迁移文档明确提示:编辑项目根目录的 next.config.ts,把experimental.serverActions.bodySizeLimit调大:
const nextConfig: NextConfig = { images: { unoptimized: true }, serverExternalPackages: ["@prisma/adapter-pg"], experimental: { serverActions: { bodySizeLimit: "256mb", // 恢复备份上传走 Server Action,受此限制 }, }, }当前仓库的默认值是"256mb"。恢复备份的上传请求正是通过 Server Action(restoreBackupAction)提交的,而服务端同时还有MAX_BACKUP_SIZE = 256MB的硬校验,所以当你的备份归档接近或超过 256MB 时,需要同时调大bodySizeLimit才能顺利上传。修改后重新构建并重启应用即可。
补充提示:导出侧还有单文件 64MB 的限制(data/route.ts/settings/backups/data/route.ts>) 中的
MAX_FILE_SIZE),如果你的实例中有超大附件,即使恢复成功,超大文件也不会被包含进归档,迁移前需单独留意这部分文件。
附录:备份归档数据文件速查
| 归档内文件 | 内容 | 对应数据模型 |
|---|---|---|
metadata.json | 版本(1.0)、时间戳、模型清单 | — |
settings.json | 用户设置与 LLM 提示词 | Setting |
currencies.json | 货币列表 | Currency |
categories.json | 分类及分类级 LLM 提示词 | Category |
projects.json | 项目及项目级 LLM 提示词 | Project |
fields.json | 自定义字段及提取提示词 | Field |
files.json | 附件元数据索引 | File |
transactions.json | 全部交易(含金额、币种、分类/项目代码、extra 字段) | Transaction |
uploads/ | 原始附件二进制 | 磁盘文件 |
这套备份格式由 models/backups.ts 的MODEL_BACKUP统一定义导出与恢复双向映射,是 TaxHacker 数据可移植性的基石——它不仅支撑本次 v0.3 → v0.5 的数据库迁移,也意味着你随时可以把数据完整导出,迁移到任何一台新的自托管服务器上。
【免费下载链接】TaxHackerSelf-hosted AI accounting app. LLM analyzer for receipts, invoices, transactions with custom prompts and categories项目地址: https://gitcode.com/GitHub_Trending/ta/TaxHacker
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考