Files
order_site/apps/frontend/tests/product-rules-export.test.ts
yml2213 5466783017 feat(worker-platform): 接单模板列表支持一键导出到本地 CSV
后台接单平台「接单模板列表」右上角新增「导出」按钮:

- 按当前筛选(搜索关键词 + 一级/二级分类)导出全部模板,服务端分页的
  情况按每页 100 条循环拉全量(页数上限兜底),不会只导出当前页;
- 导出 UTF-8 带 BOM 的 CSV,Excel/WPS 双击直接打开,文件名带时间戳
  (接单模板_YYYYMMDD_HHmm.csv);逗号/引号/换行按 CSV 规范转义;
- 22 列:模板名称/标识/分类/系统/平台/商品SKU/接单金额/按件价/拼单配置/
  验收模式/自动建单/状态/超时策略/资料字段/已绑定商品与绑定明细/时间等;
- 空结果、超出上限、浏览器不支持下载分别给出明确提示。

导出逻辑集中在 apps/frontend/src/utils/product-rules-export.ts(纯函数,
无 @/ 别名与运行时相对导入,便于 node --experimental-strip-types 单测),
新增 tests/product-rules-export.test.ts 覆盖分类回退、金额与拼单转换、
绑定明细、CSV 转义、文件名、空列表与非浏览器环境。
2026-10-08 13:50:19 +08:00

156 lines
4.8 KiB
TypeScript

import assert from 'node:assert/strict'
import test from 'node:test'
import {
CSV_BOM,
buildProductRuleExportRows,
buildProductRulesCsv,
buildProductRulesExportFilename,
describeRuleMapping,
downloadProductRulesCsv,
downloadTextFile,
formatRuleCollectFields,
resolveRuleCategoryPath,
} from '../src/utils/product-rules-export.ts'
function createRule(overrides: Record<string, unknown> = {}) {
return {
ruleId: 1,
ruleKey: 'android-暗潮-1钻',
provider: '安卓',
platform: '快手',
shopId: '',
skuCode: 'SKU-001',
productName: '暗潮1钻',
matchType: 'contains',
match: {
sellerId: '',
itemId: '',
relItemId: '',
skuId: '',
relSkuId: '',
itemTitle: '',
skuNick: '',
itemTitleMatchType: 'contains',
skuNickMatchType: 'contains',
},
categoryId: 20,
categoryName: '暗潮',
enabled: true,
autoCreate: true,
rewardAmount: 12000,
unitPriceFen: 0,
sharing: { enabled: false, totalQuantity: 1, unitReward: 0, autoFromOrder: false },
requirement: { fields: [] },
timeoutMinutes: 60,
timeoutPolicy: 'reopen',
acceptanceMode: 'standard',
sortOrder: 1,
createdAt: '2026-02-21T10:00:00.000Z',
updatedAt: '2026-02-21T11:00:00.000Z',
...overrides,
} as never
}
test('接单模板导出:分类展示成「一级 / 二级」', () => {
const categoryMap = new Map([
[10, { categoryId: 10, name: '三角洲行动', parentId: null, sortOrder: 1 }],
[20, { categoryId: 20, name: '暗潮', parentId: 10, sortOrder: 1 }],
]) as never
assert.equal(resolveRuleCategoryPath(createRule(), categoryMap), '三角洲行动 / 暗潮')
})
test('接单模板导出:分类缺失时回退未分类,不再显示空列', () => {
assert.equal(
resolveRuleCategoryPath(createRule({ categoryId: null, categoryName: '' })),
'未分类',
)
})
test('接单模板导出:金额按元输出,随订单计算的模板明确标注', () => {
const [fixed] = buildProductRuleExportRows(
[createRule({ unitPriceFen: 5000 })],
new Map(),
new Map(),
)
assert.equal(fixed.rewardAmountYuan, '120.00')
assert.equal(fixed.unitPriceYuan, '50.00')
const [autoFromOrder] = buildProductRuleExportRows(
[
createRule({
rewardAmount: 0,
sharing: { enabled: true, autoFromOrder: true, totalQuantity: 5, unitReward: 2000 },
}),
],
new Map(),
new Map(),
)
assert.equal(autoFromOrder.rewardAmountYuan, '随订单计算')
assert.equal(autoFromOrder.sharingEnabled, true)
assert.equal(autoFromOrder.sharingTotalQuantity, 5)
assert.equal(autoFromOrder.sharingUnitRewardYuan, '20.00')
})
test('接单模板导出:商品绑定汇总带店铺、商品与启用状态', () => {
const row = {
mappingId: 1,
ruleId: 1,
ruleKey: 'android-暗潮-1钻',
productName: '暗潮1钻',
sellerIds: ['11300001'],
sellerId: '11300001',
relItemId: '',
itemTitle: '暗潮 1钻 代练',
relSkuId: '',
skuNick: '',
skuMatchMode: 'fuzzy',
mappingType: 'product_default',
enabled: true,
createdAt: '',
updatedAt: '',
} as never
assert.equal(describeRuleMapping(row), '商品默认·店铺[11300001]·商品[暗潮 1钻 代练]·启用')
})
test('接单模板导出:CSV 转义逗号、引号与换行', () => {
const rule = createRule({
productName: '暗潮,1钻 "特惠"',
requirement: { fields: [{ key: 'gameId', label: '游戏\n编号', options: [] }] },
})
const rows = buildProductRuleExportRows([rule], new Map(), new Map())
const csv = buildProductRulesCsv(rows)
assert.ok(csv.startsWith('模板名称,模板标识,分类,'))
assert.ok(csv.includes('"暗潮,1钻 ""特惠"""'))
// 换行被引号包裹后仍是同一行记录
assert.equal(csv.split('\r\n').length, 2)
})
test('接单模板导出:资料字段沿用 key:名称#选项 口径', () => {
assert.equal(
formatRuleCollectFields([
{ key: 'gameId', label: '游戏编号', options: [] },
{ key: 'system', label: '系统', options: ['安卓', '苹果'] },
] as never),
'gameId:游戏编号;system:系统#安卓,苹果',
)
})
test('接单模板导出:文件名带时间戳且为 .csv', () => {
const filename = buildProductRulesExportFilename(new Date(2026, 1, 21, 15, 30, 5))
assert.equal(filename, '接单模板_20260221_1530.csv')
assert.equal(CSV_BOM, '\uFEFF')
})
test('接单模板导出:空列表只输出表头', () => {
const csv = buildProductRulesCsv([])
assert.equal(csv.split('\r\n').length, 1)
assert.ok(csv.includes('绑定明细'))
})
test('接单模板导出:非浏览器环境不抛错,返回 false 由调用方提示', () => {
assert.equal(downloadTextFile('x.csv', 'a,b'), false)
assert.equal(downloadProductRulesCsv(buildProductRuleExportRows([], new Map(), new Map())), false)
})