chore(release): 1.2.0 —— 数据迁移功能发版

- 版本号 1.1.1 → 1.2.0(root / backend / frontend 与两个 lockfile):
  部署后 /api/health 与「系统与更新」能直接分辨线上跑的是哪份代码,否则换库前后都显示 1.1.1。
- API 测试补齐:只导订单的包内容、/api/data/catalog、换类别预览(replan)、取消上传后不可再导。
- 文档补排查表:413、版本确认、权限位缺失、SQLITE_BUSY、包过期、整体回滚。
- 已验证 data.transfer 只补给超级管理员组(admin),董事长/总经理/销售经理都拿不到。
This commit is contained in:
Drsakura
2026-10-23 13:51:13 +08:00
parent 1e7c3fdcc1
commit 5acfbcea7d
7 changed files with 82 additions and 10 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "trade-management-backend",
"version": "1.1.1",
"version": "1.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "trade-management-backend",
"version": "1.1.1",
"version": "1.2.0",
"license": "ISC",
"dependencies": {
"better-sqlite3-multiple-ciphers": "^13.0.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "trade-management-backend",
"version": "1.1.1",
"version": "1.2.0",
"description": "外贸供应链管理系统 - 后端",
"main": "server.js",
"scripts": {
+59 -1
View File
@@ -20,6 +20,8 @@ const crypto = require('node:crypto')
const { spawn } = require('node:child_process')
const BACKEND = path.join(__dirname, '..')
const { readBundle } = require('../lib/dataTransfer/bundle')
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'happy-api-transfer-'))
const sourceDir = path.join(tmpRoot, 'source')
const targetDir = path.join(tmpRoot, 'target')
@@ -111,8 +113,64 @@ test('数据迁移接口端到端:导出 → 预览 → 导入 → 去重 →
assert.ok(bytes.length > 200, '导出的包不应该是空的')
assert.equal(bytes[0], 0x1f, '导出内容应是 gzip')
/* ---------------- 预览(不写库) ---------------- */
/* ---------------- 只导订单:包内不应出现产品 ---------------- */
const ordersResponse = await fetch(`http://127.0.0.1:${PORT_A}/api/data/export?categories=orders`, {
headers: { cookie: cookieA }
})
assert.equal(ordersResponse.status, 200)
const ordersFile = path.join(tmpRoot, 'orders.hdata.gz')
fs.writeFileSync(ordersFile, Buffer.from(await ordersResponse.arrayBuffer()))
const ordersBundle = await readBundle(ordersFile)
assert.deepEqual([...ordersBundle.rows.keys()].sort(), ['order_items', 'orders'])
/* ---------------- 目录接口 ---------------- */
const catalog = await (await fetch(`http://127.0.0.1:${PORT_A}/api/data/catalog`, { headers: { cookie: cookieA } })).json()
assert.equal(catalog.categories.length, 5)
assert.ok(catalog.categories.find(item => item.key === 'catalog').tables.length >= 10)
assert.ok(catalog.categories.find(item => item.key === 'catalog').tables.some(item => item.rows > 0))
const cookieB = await login(PORT_B)
/* ---------------- 换类别预览 / 取消上传 ---------------- */
/* 界面上的类别勾选走 replan:包已经在服务器上,换类别不该重传整个文件 */
const probe = new FormData()
probe.append('file', new Blob([fs.readFileSync(bundlePath)], { type: 'application/gzip' }), 'bundle.hdata.gz')
const probePreview = await (await fetch(`http://127.0.0.1:${PORT_B}/api/data/import/preview`, {
method: 'POST', headers: { cookie: cookieB }, body: probe
})).json()
const replan = async (categories) => (await fetch(`http://127.0.0.1:${PORT_B}/api/data/import/replan`, {
method: 'POST',
headers: { cookie: cookieB, 'content-type': 'application/json' },
body: JSON.stringify({ id: probePreview.id, categories })
})).json()
const onlyCustomers = await replan(['customers'])
assert.ok(onlyCustomers.plan.tables.length > 0)
assert.ok(onlyCustomers.plan.tables.every(item => item.category === 'customers'), '选客户类别时不应带出订单表')
const onlyOrders = await replan(['orders'])
assert.ok(onlyOrders.plan.tables.every(item => item.category === 'orders'))
assert.equal(onlyOrders.plan.totals.rows, 2, '订单类别只有 order + order_item 两行')
assert.deepEqual(onlyOrders.plan.stubbed.map(item => item.table), ['customers'], '只导订单应给客户补壳')
assert.equal(onlyOrders.plan.totals.insert, 2)
const stillEmpty = await (await fetch(`http://127.0.0.1:${PORT_B}/api/dashboard`, { headers: { cookie: cookieB } })).json()
assert.equal(stillEmpty.stats.products, 0, '换类别预览同样不能写库')
/* 取消:临时包应被丢弃,之后再用这个 id 导入应拿到 404 */
const cancelled = await fetch(`http://127.0.0.1:${PORT_B}/api/data/import/${probePreview.id}`, {
method: 'DELETE', headers: { cookie: cookieB }
})
assert.equal(cancelled.status, 200)
const afterCancel = await fetch(`http://127.0.0.1:${PORT_B}/api/data/import/apply`, {
method: 'POST',
headers: { cookie: cookieB, 'content-type': 'application/json' },
body: JSON.stringify({ id: probePreview.id, categories: ['orders'] })
})
assert.equal(afterCancel.status, 404, '已取消的数据包不能再导入')
/* ---------------- 预览(不写库) ---------------- */
const form = new FormData()
form.append('file', new Blob([fs.readFileSync(bundlePath)], { type: 'application/gzip' }), 'bundle.hdata.gz')
const previewResponse = await fetch(`http://127.0.0.1:${PORT_B}/api/data/import/preview`, {