From b5875d33b29818c777c8c45d5721dd691795002d Mon Sep 17 00:00:00 2001 From: yeuimu <2197651308@qq.com> Date: Sat, 29 Aug 2026 08:55:16 +0800 Subject: [PATCH] fix(sync): drop local size-code column (P/M/G/GG) from SDS size charts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 巴西/西语市场上游尺码表带一个名为「尺码」的列,值是本地尺码码 P/M/G/GG(=S/M/L/XL),与行首尺码重复且非测量值;解析时整列丢弃。 其他非数字参考列(身高/体重/建议体重)保留。存量 2 条详情已修复 并重算受影响族。 --- .../sync/sds-product-detail.mapper.spec.ts | 37 +++++++++++++++++++ .../api/src/sync/sds-product-detail.mapper.ts | 21 ++++++++++- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/apps/api/src/sync/sds-product-detail.mapper.spec.ts b/apps/api/src/sync/sds-product-detail.mapper.spec.ts index d6f0245..a2a93e9 100644 --- a/apps/api/src/sync/sds-product-detail.mapper.spec.ts +++ b/apps/api/src/sync/sds-product-detail.mapper.spec.ts @@ -33,6 +33,43 @@ describe('SDS product detail mapper', () => { expect(chart.rows[0].measurements[0]).toEqual({ key: 'bodyLength', cm: '71', in: '27.95' }); }); + it('drops the local size-code column (P/M/G/GG) named 尺码 when all values are non-numeric', () => { + // 巴西/西语市场的 SDS 上游在表头第二列再放一个「尺码」列,值是本地尺码码 + // P/M/G/GG(=S/M/L/XL),与行首尺码重复且非测量值,应整列丢弃 + const raw = JSON.stringify([ + ['尺码', '尺码(cm/in)', '肩宽(cm/in)', '胸围(cm/in)'].map((content) => ({ content, remark: '' })), + ['S', 'P', '52', '106'].map((content) => ({ content, remark: '' })), + ['M', 'M', '55', '112'].map((content) => ({ content, remark: '' })), + ['L', 'G', '58', '118'].map((content) => ({ content, remark: '' })), + ['XL', 'GG', '61', '124'].map((content) => ({ content, remark: '' })), + ]); + const chart = parseSizeChart(raw) as any; + expect(chart.columns.map((c: any) => c.name)).toEqual(['肩宽', '胸围']); + expect(chart.rows[0].measurements.map((m: any) => m.key)).toEqual(['shoulder', 'chest']); + expect(chart.rows[0].measurements[0]).toEqual({ key: 'shoulder', cm: '52', in: '20.47' }); + }); + + it('keeps a 尺码-named column when its values are numeric', () => { + const raw = JSON.stringify([ + ['尺码', '尺码(cm/in)', '胸围(cm/in)'].map((content) => ({ content, remark: '' })), + ['S', '1', '106'].map((content) => ({ content, remark: '' })), + ['M', '2', '112'].map((content) => ({ content, remark: '' })), + ]); + const chart = parseSizeChart(raw) as any; + expect(chart.columns).toHaveLength(2); + expect(chart.rows[0].measurements[0].cm).toBe('1'); + }); + + it('keeps non-numeric reference columns like 身高/体重', () => { + const raw = JSON.stringify([ + ['尺码', '身高(cm/in)', '体重(cm/in)'].map((content) => ({ content, remark: '' })), + ['S', '50-60', '3-4kg'].map((content) => ({ content, remark: '' })), + ['M', '60-70', '4-5kg'].map((content) => ({ content, remark: '' })), + ]); + const chart = parseSizeChart(raw) as any; + expect(chart.columns.map((c: any) => c.name)).toEqual(['身高', '体重']); + }); + it('parses packaging dimensions and weights', () => { const specs = parsePackageSpecs(packageTable) as any; expect(specs.rows).toHaveLength(2); diff --git a/apps/api/src/sync/sds-product-detail.mapper.ts b/apps/api/src/sync/sds-product-detail.mapper.ts index e5f0827..001d736 100644 --- a/apps/api/src/sync/sds-product-detail.mapper.ts +++ b/apps/api/src/sync/sds-product-detail.mapper.ts @@ -94,7 +94,7 @@ export function parseSizeChart(raw: unknown): Prisma.InputJsonValue | null { const table = parseTable(raw); if (!table) return null; const [header, ...body] = table; - const columns = header.slice(1).map((item, index) => { + let columns = header.slice(1).map((item, index) => { const name = text(item.content)?.replace(/\s*\(cm\/in\)\s*/i, '') ?? `规格${index + 1}`; return { key: measurementKey(name, index + 1), name }; }); @@ -117,6 +117,25 @@ export function parseSizeChart(raw: unknown): Prisma.InputJsonValue | null { }; }) .filter((row): row is NonNullable => row !== null); + // 巴西/西语市场上游会在表里再放一个「尺码」列,值是本地尺码码(P/M/G/GG = S/M/L/XL), + // 与行首尺码重复且不是测量值 → 整列丢弃。其他非数字列(身高/体重等参考信息)保留。 + const sizeCodeColumns = new Set( + columns + .filter((column) => /^尺码$/.test(column.name)) + .filter((column) => + rows.every((row) => { + const cm = row.measurements.find((m) => m.key === column.key)?.cm; + return cm === null || cm === undefined || !Number.isFinite(Number(cm)); + }), + ) + .map((column) => column.key), + ); + if (sizeCodeColumns.size > 0) { + columns = columns.filter((column) => !sizeCodeColumns.has(column.key)); + for (const row of rows) { + row.measurements = row.measurements.filter((m) => !sizeCodeColumns.has(m.key)); + } + } return { columns, rows } as Prisma.InputJsonValue; }