后台接单平台「接单模板列表」右上角新增「导出」按钮: - 按当前筛选(搜索关键词 + 一级/二级分类)导出全部模板,服务端分页的 情况按每页 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 转义、文件名、空列表与非浏览器环境。
156 lines
4.8 KiB
TypeScript
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)
|
|
})
|