- scripts/data-transfer.js:tables / export / import --preview|--apply,与网页版共用同一套内核。 用于 nginx/CDN 挡住 1.22MB 上传时的备用通道,也可挂 cron 做异地备份。 - readBundle:校验失败中途抛错时关闭 readStream 与 readline,避免反复上传坏包耗尽文件句柄。 - docs:补命令行用法与体积限制说明。
152 lines
8.1 KiB
Markdown
152 lines
8.1 KiB
Markdown
# 业务数据导入导出(数据迁移)
|
||
|
||
> 对应 `backend/lib/dataTransfer/`、`backend/routes/dataTransfer.js`、`frontend/src/pages/DataTransfer.jsx`。
|
||
> 界面入口:侧栏 · 系统管理 · **数据迁移**(权限点 `data.transfer`,默认只给超级管理员组)。
|
||
|
||
## 为什么不再「整个 trade.db 换过去」
|
||
|
||
把本地 `trade.db` 直接覆盖到服务器,会把**整套系统状态**一起带过去:`users`(密码哈希)、
|
||
`sessions`(登录态)、`settings`(里面存着大模型 API Key 与合同认列规则)、`permission_groups`、
|
||
`audit_log`。这等于让「传数据」变成「换一套后台」,而且线上一旦有自己的账号和配置就会被抹掉。
|
||
|
||
所以数据分成两类,边界写死在 `backend/lib/dataTransfer/catalog.js`:
|
||
|
||
| 业务数据(可导出 / 可导入) | 系统数据(永不进包、永不覆盖) |
|
||
| --- | --- |
|
||
| 供应商、产品组、合同认列记录、产品、供应商报价、阶梯价、价格历史、产品属性、产品图片(含 BLOB)、合同原件(含 BLOB)、包装规格、清洗任务与明细 | 用户与密码 |
|
||
| 客户、联系人、商机、客户报价单与明细、询盘与明细、客户时间线、跟进记录 | 登录会话 |
|
||
| 订单、订单明细、订单杂费 | 组织信息 |
|
||
| 待办与日程 | 权限组、角色指派 |
|
||
| 新客开发任务与线索 | 系统设置(AI 密钥、合同规则、权限台账) |
|
||
| | 审计日志 |
|
||
|
||
`cart_items`(本机小推车)也不在迁移范围 —— 它只是临时挑选状态,不属于要交接的数据。
|
||
|
||
## 数据包格式
|
||
|
||
文件名形如 `happy-export-20261023-1512.hdata.gz`,内容是 **gzip 压缩的 JSONL**:
|
||
|
||
```
|
||
{"type":"header","format":"happy-data","version":1,"exportedAt":"…","categories":[…],"tableNames":[…],"exporter":{…}}
|
||
{"t":"suppliers","r":{"name":"宁波腾达","created_at":"…"}}
|
||
{"t":"products","r":{"sku":"TD-100","price":88,"__refs":{"supplier_id":"宁波腾达"}}}
|
||
…
|
||
{"type":"footer","tables":{"products":{"rows":1576,"sha256":"…"}},"rows":3854}
|
||
```
|
||
|
||
- **不用 zip、不直接拷 .db**:gzip + JSONL 只用 Node 内置能力,逐行流式读写,
|
||
未来合同原件变成几十 MB 也不会整包读进内存;
|
||
- **每张表一个 sha256 + 行数**:导入前先校验,整包任一表对不上就整体拒绝 ——
|
||
半个包导进去比导不进去更难收拾;
|
||
- `__refs` 是父记录的**自然键快照**(如订单里的客户公司名)。只导订单时,
|
||
目标库靠它按公司名找到客户,或补一个只有公司名的客户壳。
|
||
|
||
当前这套真实数据(1576 个产品、823 条合同、272 个供应商…)导出后约 **1.22 MB**、3854 行。
|
||
|
||
## 导入规则
|
||
|
||
1. **归属一律给导入人。** `owner_id` / `created_by` / `assigned_to` / `reviewed_by` 全部改写成
|
||
执行导入的账号,`org_id` 改写成该账号所属组织。同事之间快速同步、或职责交接时,
|
||
导完由管理员在客户页重新分配即可。
|
||
2. **冲突跳过,绝不覆盖。** 线上已有同一条记录就跳过,并写进报告:
|
||
产品按 `sku`、供应商按 `name`、订单按 `order_no`、客户按 `company`、报价单按 `quote_no`…
|
||
(完整对照见 `catalog.js` 的 `key`)。
|
||
3. **流水表按整行内容比对。** 价格历史这类没有自然业务键的表,用「所有列都相同」
|
||
判断重复 —— 曾经用 `(sku, changed_at)` 当键,实测把 643 行里的 186 行当成重复静默丢掉了
|
||
(一次批量改价会在同一毫秒写多条)。同一个包里逐列完全相同的两行也会都保留。
|
||
4. **父记录可补壳。** 只导订单而没导客户时,按 `__refs` 里的公司名补一个客户,
|
||
报告里单列「自动补建」,导完提示补全联系人、地址等信息。
|
||
5. **一个事务 + 导入前备份。** 写入在一个事务里完成,异常整体回滚;
|
||
导入前用 `VACUUM INTO` 把线上库备份到 `DATA_DIR/backups/pre-import-<时间>-<随机>.db`。
|
||
6. **预览等于真跑一遍。** 预览把库 `VACUUM INTO` 成副本,在副本上执行同一套导入代码,
|
||
跑完删掉副本 —— 所以预览报告里的「新增 / 跳过 / 失败」与真导结果同源,
|
||
不存在「预览说能导、真导却失败」的两套逻辑。
|
||
|
||
## 操作步骤:本地 → 云端
|
||
|
||
1. **服务器先升级到含本功能的版本**(见下节),重启后管理员登录。
|
||
2. **本地**(有真实数据的那台):侧栏 · 系统管理 · 数据迁移 → 勾选类别
|
||
(全选 = 全部业务数据,也可以只勾产品 → 只导 SKU)→ 导出,得到一个 `.hdata.gz`。
|
||
3. **云端**:同一页面上传该文件 → 等预览报告 → 检查「将新增 / 跳过」→ 确认导入。
|
||
4. 核对:工作台统计里的产品 / 供应商 / 客户 / 订单数量应与本地一致;
|
||
报告里的「自动补建」条目需要人工补全。
|
||
|
||
本次迁移的预期结果(本地 2026-10-23 的库):
|
||
|
||
```
|
||
包内 3854 行 → 新增 3854 / 跳过 0 / 失败 0
|
||
products 1576 · contracts 823 · price_history 643 · product_groups 516 · suppliers 272
|
||
```
|
||
|
||
## 服务器部署
|
||
|
||
```bash
|
||
cd /opt/1panel/www/sites/erp.foohere.de/index
|
||
git pull
|
||
cd backend && npm install --omit=dev --ignore-scripts
|
||
cd ../frontend && npm install && npm run build
|
||
systemctl restart erp
|
||
sleep 3 && curl -s http://127.0.0.1:4300/api/health
|
||
```
|
||
|
||
部署后**现有账号与设置保持不变**(这正是与整库替换的区别);新增的权限点 `data.transfer`
|
||
会由 `db.js` 的 `syncNewPermissions()` 自动补进超级管理员组。
|
||
|
||
反向也成立:云端 → 本地同样走这一页,包是同一个格式。
|
||
|
||
## 命令行(备用通道)
|
||
|
||
浏览器上传被 nginx / CDN 的体积限制挡住(默认 `client_max_body_size 1m`,而真实包 1.22 MB 就会 413),
|
||
或者想把包 `scp` 上去直接在服务器导,用同仓库的 CLI:
|
||
|
||
```bash
|
||
cd /opt/1panel/www/sites/erp.foohere.de/index/backend
|
||
|
||
# 各类别、各表当前行数
|
||
/usr/bin/node scripts/data-transfer.js tables
|
||
|
||
# 先预览:在库副本上真跑一遍,线上库不动
|
||
/usr/bin/node scripts/data-transfer.js import /root/bundle.hdata.gz --preview
|
||
|
||
# 确认后导入:自动备份到 data/backups/,归属给 --user 指定的账号
|
||
/usr/bin/node scripts/data-transfer.js import /root/bundle.hdata.gz --apply --user admin
|
||
|
||
# 定期导出做异地备份(配合 cron)
|
||
/usr/bin/node scripts/data-transfer.js export --out /root/backup-$(date +%F).hdata.gz
|
||
```
|
||
|
||
与网页版**共用同一套代码**(判重、归属改写、补壳、备份完全一致),
|
||
`--categories catalog,orders` 可以只搬某几类。
|
||
|
||
## 接口
|
||
|
||
| 方法 | 路径 | 说明 |
|
||
| --- | --- | --- |
|
||
| GET | `/api/data/catalog` | 类别与各表行数(界面用) |
|
||
| GET | `/api/data/export?categories=catalog,orders` | 导出,`all` 或省略 = 全部 |
|
||
| POST | `/api/data/import/preview` | multipart `file`,上传并出预览报告,不写库 |
|
||
| POST | `/api/data/import/replan` | `{ id, categories }`,换类别重算预览,不重传 |
|
||
| POST | `/api/data/import/apply` | `{ id, categories }`,真导(带备份) |
|
||
| DELETE | `/api/data/import/:id` | 取消,丢弃临时包 |
|
||
|
||
全部要登录 + `data.transfer` 权限;导出与导入都会写 `audit_log`。
|
||
|
||
## 限制
|
||
|
||
- 单包上限 **100 MB / 200 万行**,超过请按类别分开导;
|
||
- 上传的包保存在 `DATA_DIR/transfers/`,**24 小时后过期**(每次上传会顺带清理旧包);
|
||
- 导入备份 `DATA_DIR/backups/` **不会自动清理**,请自行定期删除;
|
||
- 导入是同步执行的(几千行级别毫秒到秒级),包特别大时会占住事件循环。
|
||
|
||
## 维护:新增业务表时
|
||
|
||
在 `backend/lib/dataTransfer/catalog.js` 的 `TABLES` 里**按依赖顺序**加一条:
|
||
|
||
```js
|
||
{ table: 'my_table', label: '中文名', category: 'catalog', key: ['业务唯一键'] }
|
||
```
|
||
|
||
- `key`:判重列;没有自然键的流水表改用 `contentKey: true`;
|
||
- 父记录允许补壳的表加 `stub: true`(目前只有 customers / suppliers / product_groups);
|
||
- 依赖顺序错了会撞外键。`backend/test/data-transfer.test.js` 会校验顺序与系统表是否被误导出。
|