From e351a69912275e71291ca7b6ff8a4733293fc11a Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Sat, 10 Oct 2026 11:50:18 +0800 Subject: [PATCH] feat(web): add shop column to SYB product export (#372) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Insert a text "店铺" column (row.shopName) right after SYB订单 and move the 0.00 number format to the shifted price column. Sync the wiki mirror. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F --- docs/03-business-rules-and-glossary.md | 6 ++--- web/src/utils/list-export.js | 6 ++--- web/tests/unit/list-export.test.cjs | 31 ++++++++++++++------------ 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/docs/03-business-rules-and-glossary.md b/docs/03-business-rules-and-glossary.md index df474d5..f5f3b8d 100644 --- a/docs/03-business-rules-and-glossary.md +++ b/docs/03-business-rules-and-glossary.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Business-Rules-and-Glossary wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Business-Rules-and-Glossary.- -wiki_revision: 84e5a638893e7136b70bfdf7b0ce0a5186cd4ee4 -synchronized_at: 2026-10-10T03:10:34Z +wiki_revision: d427a46283561df71714929c13b7156ee42debea +synchronized_at: 2026-10-10T03:47:23Z @@ -955,7 +955,7 @@ Android 0.9.64 / versionCode 77,源码 `6550b9f`(分支实现,尚未安装 - 格式:时间 `YYYY-MM-DD HH:mm:ss`(浏览器本地时区);金额单位元,写成 Excel **数字单元格**(按分换算,例如 1230 分 → 12.3,可直接求和)并显示为 `0.00`;空值留空,0 写成 0;文件名 `采购导出_YYYYMMDD-HHmmss.xlsx`、`SYB商品导出_YYYYMMDD-HHmmss.xlsx`。不导出收货人、地址、账号等个人信息。 - 采购导出列(按顺序):SYB订单号、虾皮商品ID、颜色、尺码(均为 SYB 目标规格快照)、数量、状态(页面同款中文)、PDD单号、下单时间、价格(元)(数字单元格)。价格取 `pddOrderAmountCent / 100`,即 Agent 在 PDD 待付款页读到的应付金额;旧版 Agent 或人工补录前为空则留空。要导出成功采购,先把状态筛为「订单已创建」并按需选择下单日期。 - 采购管理新增「下单日期」筛选:含首尾两天,按 `order_submitted_at`,日期边界 UTC+8,与 SYB 商品页创建时间筛选一致;没有下单时间的任务在设置该筛选时不出现。 -- SYB 商品导出列(按顺序):SYB订单、虾皮商品ID、**SYB售价(TWD)**、SYB颜色、SYB尺码、SYB数量、yeeke颜色、yeeke尺码、yeeke数量、匹配时间。yeeke 颜色/尺码来自该商品有效(`matched` 或 `confirmed`)退货匹配的 `variationName`,按第一个英文或中文逗号拆分,没有逗号时整段放 yeeke 颜色;yeeke 数量为对应退货商品数量;SYB售价(TWD)取商品行 `unitPriceCent / 100`,与页面「售价」列同源(SYB `detail/listByStock` 的 `productPrice`,单件成交单价,元/TWD),数字单元格,无值留空;没有有效匹配时 yeeke 列与匹配时间留空。要导出已用退货,先把处理阶段筛为「已用退货」。 +- SYB 商品导出列(按顺序):SYB订单、**店铺**、虾皮商品ID、**SYB售价(TWD)**、SYB颜色、SYB尺码、SYB数量、yeeke颜色、yeeke尺码、yeeke数量、匹配时间。店铺取商品行 `shopName`(SYB 订单的店铺,与页面「店铺」列同值,文本单元格,无值留空,不使用店铺筛选下拉的显示名);yeeke 颜色/尺码来自该商品有效(`matched` 或 `confirmed`)退货匹配的 `variationName`,按第一个英文或中文逗号拆分,没有逗号时整段放 yeeke 颜色;yeeke 数量为对应退货商品数量;SYB售价(TWD)取商品行 `unitPriceCent / 100`,与页面「售价」列同源(SYB `detail/listByStock` 的 `productPrice`,单件成交单价,元/TWD),数字单元格,无值留空;没有有效匹配时 yeeke 列与匹配时间留空。要导出已用退货,先把处理阶段筛为「已用退货」。 - 权限:不新增按钮权限,能打开列表的用户(含采购员)都可导出。 ## 设备离线诊断的事务边界(#378,实现基线) diff --git a/web/src/utils/list-export.js b/web/src/utils/list-export.js index 9a698f7..bbc2781 100644 --- a/web/src/utils/list-export.js +++ b/web/src/utils/list-export.js @@ -51,7 +51,7 @@ const blank = value => (value === null || value === undefined ? '' : value) // Column indexes written as numbers; the saver shows them as 0.00. export const PURCHASE_EXPORT_NUMBER_COLS = [8] -export const SYB_PRODUCT_EXPORT_NUMBER_COLS = [2] +export const SYB_PRODUCT_EXPORT_NUMBER_COLS = [3] export const PURCHASE_EXPORT_HEADER = ['SYB订单号', '虾皮商品ID', '颜色', '尺码', '数量', '状态', 'PDD单号', '下单时间', '价格(元)'] @@ -64,7 +64,7 @@ export function purchaseExportRow(task, statusLabel = value => value) { ] } -export const SYB_PRODUCT_EXPORT_HEADER = ['SYB订单', '虾皮商品ID', 'SYB售价(TWD)', 'SYB颜色', 'SYB尺码', 'SYB数量', 'yeeke颜色', 'yeeke尺码', 'yeeke数量', '匹配时间'] +export const SYB_PRODUCT_EXPORT_HEADER = ['SYB订单', '店铺', '虾皮商品ID', 'SYB售价(TWD)', 'SYB颜色', 'SYB尺码', 'SYB数量', 'yeeke颜色', 'yeeke尺码', 'yeeke数量', '匹配时间'] export const isActiveReturnMatch = match => Boolean(match) && (match.status === 'matched' || match.status === 'confirmed') @@ -73,7 +73,7 @@ export function sybProductExportRow(product, match) { const active = isActiveReturnMatch(match) ? match : null const yeeke = active ? splitYeekeVariation(active.variationName) : { color: '', size: '' } return [ - blank(product.orderCode), blank(product.shopeeItemId), centToYuanNumber(product.unitPriceCent), blank(product.targetColor), blank(product.targetSize), blank(product.quantity), + blank(product.orderCode), blank(product.shopName), blank(product.shopeeItemId), centToYuanNumber(product.unitPriceCent), blank(product.targetColor), blank(product.targetSize), blank(product.quantity), yeeke.color, yeeke.size, active ? blank(active.yeekeQuantity) : '', active ? formatExportTime(active.matchedAt) : '' ] } diff --git a/web/tests/unit/list-export.test.cjs b/web/tests/unit/list-export.test.cjs index fbad6cc..3a32bf7 100644 --- a/web/tests/unit/list-export.test.cjs +++ b/web/tests/unit/list-export.test.cjs @@ -58,17 +58,18 @@ test('cent to yuan number is numeric, zero stays zero, empty stays empty', () => }) test('syb product row uses only an active match and ignores cancelled ones', () => { - const product = { orderCode: 'O1', shopeeItemId: '99', unitPriceCent: 1230, targetColor: '白色', targetSize: 'L', quantity: 1 } + const product = { orderCode: 'O1', shopName: 'shop-a', shopeeItemId: '99', unitPriceCent: 1230, targetColor: '白色', targetSize: 'L', quantity: 1 } const matchedAt = new Date(2026, 9, 9, 9, 0, 0).toISOString() - assert.deepEqual(lib.SYB_PRODUCT_EXPORT_HEADER, ['SYB订单', '虾皮商品ID', 'SYB售价(TWD)', 'SYB颜色', 'SYB尺码', 'SYB数量', 'yeeke颜色', 'yeeke尺码', 'yeeke数量', '匹配时间']) - assert.deepEqual(lib.SYB_PRODUCT_EXPORT_NUMBER_COLS, [2]) + assert.deepEqual(lib.SYB_PRODUCT_EXPORT_HEADER, ['SYB订单', '店铺', '虾皮商品ID', 'SYB售价(TWD)', 'SYB颜色', 'SYB尺码', 'SYB数量', 'yeeke颜色', 'yeeke尺码', 'yeeke数量', '匹配时间']) + assert.deepEqual(lib.SYB_PRODUCT_EXPORT_NUMBER_COLS, [3]) assert.deepEqual(lib.sybProductExportRow(product, { status: 'confirmed', variationName: '白色,L', yeekeQuantity: 3, matchedAt }), - ['O1', '99', 12.3, '白色', 'L', 1, '白色', 'L', 3, '2026-10-09 09:00:00']) - assert.deepEqual(lib.sybProductExportRow(product, { status: 'matched', variationName: '均碼', yeekeQuantity: 1, matchedAt }).slice(6, 9), ['均碼', '', 1]) - assert.deepEqual(lib.sybProductExportRow(product, { status: 'cancelled', variationName: 'x', yeekeQuantity: 1, matchedAt }), ['O1', '99', 12.3, '白色', 'L', 1, '', '', '', '']) - assert.deepEqual(lib.sybProductExportRow(product, null), ['O1', '99', 12.3, '白色', 'L', 1, '', '', '', '']) - assert.equal(lib.sybProductExportRow({ ...product, unitPriceCent: 0 }, null)[2], 0) - for (const value of [null, undefined]) assert.equal(lib.sybProductExportRow({ ...product, unitPriceCent: value }, null)[2], '') + ['O1', 'shop-a', '99', 12.3, '白色', 'L', 1, '白色', 'L', 3, '2026-10-09 09:00:00']) + assert.deepEqual(lib.sybProductExportRow(product, { status: 'matched', variationName: '均碼', yeekeQuantity: 1, matchedAt }).slice(7, 10), ['均碼', '', 1]) + assert.deepEqual(lib.sybProductExportRow(product, { status: 'cancelled', variationName: 'x', yeekeQuantity: 1, matchedAt }), ['O1', 'shop-a', '99', 12.3, '白色', 'L', 1, '', '', '', '']) + assert.deepEqual(lib.sybProductExportRow(product, null), ['O1', 'shop-a', '99', 12.3, '白色', 'L', 1, '', '', '', '']) + assert.equal(lib.sybProductExportRow({ ...product, unitPriceCent: 0 }, null)[3], 0) + for (const value of [null, undefined]) assert.equal(lib.sybProductExportRow({ ...product, unitPriceCent: value }, null)[3], '') + for (const value of [null, undefined]) assert.equal(lib.sybProductExportRow({ ...product, shopName: value }, null)[1], '') }) // Runs the real Export2Excel writer with file-saver stubbed and reads the xlsx back. @@ -96,15 +97,17 @@ test('excel writer makes numeric cells, 0.00 format only for requested columns', test('real export row types and format end up in the sheet', () => { const header = lib.SYB_PRODUCT_EXPORT_HEADER const rows = [ - lib.sybProductExportRow({ orderCode: 'O1', unitPriceCent: 1230 }, null), + lib.sybProductExportRow({ orderCode: 'O1', shopName: 'shop-a', unitPriceCent: 1230 }, null), lib.sybProductExportRow({ orderCode: 'O2', unitPriceCent: 0 }, null), lib.sybProductExportRow({ orderCode: 'O3', unitPriceCent: null }, null) ] const sheet = writeWorkbook({ header, data: rows, filename: 't', numberFormatCols: lib.SYB_PRODUCT_EXPORT_NUMBER_COLS }) - assert.equal(sheet.C1.v, 'SYB售价(TWD)') - assert.equal(sheet.C2.t, 'n'); assert.equal(sheet.C2.v, 12.3); assert.equal(sheet.C2.z, '0.00') - assert.equal(sheet.C3.t, 'n'); assert.equal(sheet.C3.v, 0) - assert.equal(sheet.C4.v, ''); assert.notEqual(sheet.C4.t, 'n') + assert.equal(sheet.B1.v, '店铺') + assert.equal(sheet.B2.t, 's'); assert.equal(sheet.B2.v, 'shop-a'); assert.notEqual(sheet.B2.z, '0.00') + assert.equal(sheet.D1.v, 'SYB售价(TWD)') + assert.equal(sheet.D2.t, 'n'); assert.equal(sheet.D2.v, 12.3); assert.equal(sheet.D2.z, '0.00') + assert.equal(sheet.D3.t, 'n'); assert.equal(sheet.D3.v, 0) + assert.equal(sheet.D4.v, ''); assert.notEqual(sheet.D4.t, 'n') }) function harness(total, options = {}) {