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 转义、文件名、空列表与非浏览器环境。
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
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)
|
||||
})
|
||||
Reference in New Issue
Block a user