diff --git a/furniture-3d-system/src/app/supply-chain/management/clearance/ClearancePricePushView.test.tsx b/furniture-3d-system/src/app/supply-chain/management/clearance/ClearancePricePushView.test.tsx index 5f0ab95a..b750a952 100644 --- a/furniture-3d-system/src/app/supply-chain/management/clearance/ClearancePricePushView.test.tsx +++ b/furniture-3d-system/src/app/supply-chain/management/clearance/ClearancePricePushView.test.tsx @@ -49,7 +49,7 @@ describe('ClearancePricePushView', () => { }) as jest.Mock }) - it('renders the specified headers and labels the clearance-price note', async () => { + it('renders the specified headers and shows the clearance-price definition', async () => { render() expect(await screen.findByText('SKU-A')).toBeInTheDocument() @@ -58,8 +58,7 @@ describe('ClearancePricePushView', () => { name: new RegExp(`^${header}(?:\\s|$)`), })).toBeInTheDocument() } - expect(screen.getByLabelText('即清货价')).toHaveAttribute('title', '即清货价') - expect(screen.queryByText('即清货价')).not.toBeInTheDocument() + expect(screen.getByText('即清货价')).toBeVisible() }) it('generates and archives the selected month', async () => { diff --git a/furniture-3d-system/src/app/supply-chain/management/clearance/ClearancePricePushView.tsx b/furniture-3d-system/src/app/supply-chain/management/clearance/ClearancePricePushView.tsx index ce6094e3..43de81e1 100644 --- a/furniture-3d-system/src/app/supply-chain/management/clearance/ClearancePricePushView.tsx +++ b/furniture-3d-system/src/app/supply-chain/management/clearance/ClearancePricePushView.tsx @@ -350,14 +350,8 @@ export function ClearancePricePushView({ 成本价 开始日期 - 历史成本价 - - 注 - + 历史成本价 + 即清货价 店铺编码 店铺名称 diff --git a/furniture-3d-system/src/app/supply-chain/management/clearance/clearance-price-push.module.css b/furniture-3d-system/src/app/supply-chain/management/clearance/clearance-price-push.module.css index 88b69181..1f8aa9c1 100644 --- a/furniture-3d-system/src/app/supply-chain/management/clearance/clearance-price-push.module.css +++ b/furniture-3d-system/src/app/supply-chain/management/clearance/clearance-price-push.module.css @@ -96,22 +96,20 @@ } .labelHeader { - position: relative; + min-width: 118px; } -.headerNote { - position: static; - display: inline-grid; - place-items: center; - margin-left: 3px; - width: 14px; - height: 14px; - border-radius: 50%; - background: #eef2f3; - color: #667174; - font-size: 9px; - font-weight: 800; - cursor: help; +.headerTitle, +.headerDefinition { + display: block; +} + +.headerDefinition { + margin-top: 2px; + color: #7a8588; + font-size: 10px; + font-weight: 500; + line-height: 1.2; } @media (max-width: 720px) { diff --git a/furniture-3d-system/src/server/repositories/__tests__/supply-chain-clearance-price-push.repository.test.ts b/furniture-3d-system/src/server/repositories/__tests__/supply-chain-clearance-price-push.repository.test.ts index 4d0863e2..b16983c4 100644 --- a/furniture-3d-system/src/server/repositories/__tests__/supply-chain-clearance-price-push.repository.test.ts +++ b/furniture-3d-system/src/server/repositories/__tests__/supply-chain-clearance-price-push.repository.test.ts @@ -1,11 +1,21 @@ const mockStoreAccountFindMany = jest.fn() const mockListShopInfos = jest.fn() +const mockSnapshotFindUnique = jest.fn() +const mockItemCount = jest.fn() +const mockItemFindMany = jest.fn() jest.mock('@/lib/prisma', () => ({ prisma: { storeAccount: { findMany: (...args: unknown[]) => mockStoreAccountFindMany(...args), }, + supplyChainClearancePricePushSnapshot: { + findUnique: (...args: unknown[]) => mockSnapshotFindUnique(...args), + }, + supplyChainClearancePricePushItem: { + count: (...args: unknown[]) => mockItemCount(...args), + findMany: (...args: unknown[]) => mockItemFindMany(...args), + }, }, })) @@ -32,6 +42,7 @@ describe('supply chain clearance price push repository', () => { mockListShopInfos.mockResolvedValue([ { shopId: '1001', shopName: '店铺一', shopSite: '淘宝天猫' }, { shopId: '1002', shopName: '店铺二', shopSite: '京东' }, + { shopId: '21414306', shopName: '抖音-索菲亚精选家具专卖店', shopSite: '头条放心购' }, ]) await expect(supplyChainClearancePricePushRepository.listStoresByProfitCenters([ @@ -45,4 +56,35 @@ describe('supply chain clearance price push repository', () => { })) expect(mockListShopInfos).toHaveBeenCalled() }) + + it('resolves missing shop ids in archived rows without regenerating the snapshot', async () => { + mockStoreAccountFindMany.mockResolvedValue([]) + mockListShopInfos.mockResolvedValue([ + { shopId: '21414306', shopName: '抖音-索菲亚精选家具专卖店', shopSite: '头条放心购' }, + ]) + mockSnapshotFindUnique.mockResolvedValue({ id: 'snapshot-1' }) + mockItemCount.mockResolvedValue(1) + mockItemFindMany.mockResolvedValue([{ + id: 'item-1', + productCode: 'SKU-A', + costPrice: '100.000', + startDate: new Date('2026-11-01T00:00:00.000Z'), + historicalCostPrice: '60.000', + shopId: '', + shopName: '抖音-索菲娅精选家具专卖店', + }]) + + await expect(supplyChainClearancePricePushRepository.listItems({ + periodKey: '2026-11', + page: 1, + pageSize: 50, + })).resolves.toMatchObject({ + total: 1, + rows: [{ + id: 'item-1', + shopId: '21414306', + shopName: '抖音-索菲娅精选家具专卖店', + }], + }) + }) }) diff --git a/furniture-3d-system/src/server/repositories/supply-chain-clearance-price-push.repository.ts b/furniture-3d-system/src/server/repositories/supply-chain-clearance-price-push.repository.ts index d0063f8c..7f2b64fe 100644 --- a/furniture-3d-system/src/server/repositories/supply-chain-clearance-price-push.repository.ts +++ b/furniture-3d-system/src/server/repositories/supply-chain-clearance-price-push.repository.ts @@ -31,12 +31,64 @@ export type ClearancePricePushFilters = { historicalCostPriceMax?: number } +const SHOP_ID_MAPPING_TTL_MS = 60_000 +let shopIdMappingCache: { + expiresAt: number + value: ReadonlyMap +} | null = null + function normalizedCodes(values: readonly string[]) { return Array.from(new Set( values.map((value) => value.trim().toUpperCase()).filter(Boolean), )) } +function normalizedShopName(value: unknown) { + return String(value ?? '') + .normalize('NFKC') + .trim() + .replace(/\s+/g, ' ') + .replace(/娅/g, '亚') + .toLocaleLowerCase('zh-CN') +} + +async function shopIdByStoreName() { + if (shopIdMappingCache && shopIdMappingCache.expiresAt > Date.now()) { + return shopIdMappingCache.value + } + try { + const shopInfos = await jstSkumapDistributionRepository.listShopInfos() + const mapping = new Map() + for (const shop of shopInfos) { + const shopName = normalizedShopName(shop.shopName) + const shopId = shop.shopId.trim() + if (shopName && shopId && !mapping.has(shopName)) { + mapping.set(shopName, shopId) + } + } + shopIdMappingCache = { + expiresAt: Date.now() + SHOP_ID_MAPPING_TTL_MS, + value: mapping, + } + return mapping + } catch (error) { + console.warn('[clearance-price-push] jst_shopInfo mapping failed', error) + return new Map() + } +} + +async function resolveShopIds(rows: readonly T[]) { + if (!rows.some((row) => !row.shopId.trim())) return rows + const mapping = await shopIdByStoreName() + return rows.map((row) => { + if (row.shopId.trim()) return row + return { + ...row, + shopId: mapping.get(normalizedShopName(row.shopName)) ?? '', + } + }) +} + function itemWhere( snapshotId: string, filters?: ClearancePricePushFilters, @@ -131,7 +183,7 @@ class SupplyChainClearancePricePushRepository { }, }), ]) - return { total, rows } + return { total, rows: await resolveShopIds(rows) } } async countItems(periodKey: string, filters?: ClearancePricePushFilters) { @@ -176,7 +228,7 @@ class SupplyChainClearancePricePushRepository { }, }) if (!rows.length) return - yield rows + yield await resolveShopIds(rows) if (rows.length < batchSize) return cursor = rows[rows.length - 1].id } @@ -199,23 +251,9 @@ class SupplyChainClearancePricePushRepository { if (!storeName || !profitCenter || !centers.includes(profitCenter)) continue storeByKey.set(`${profitCenter}\u0000${storeName}`, { storeName, profitCenter }) } - let shopInfos: Awaited> = [] - try { - shopInfos = await jstSkumapDistributionRepository.listShopInfos() - } catch (error) { - console.warn('[clearance-price-push] jst_shopInfo mapping failed', error) - } - const shopIdByStoreName = new Map() - for (const shop of shopInfos) { - const storeName = shop.shopName.trim() - const shopId = shop.shopId.trim() - const normalizedName = storeName.toLowerCase() - if (normalizedName && shopId && !shopIdByStoreName.has(normalizedName)) { - shopIdByStoreName.set(normalizedName, shopId) - } - } + const mapping = await shopIdByStoreName() return Array.from(storeByKey.values(), (store) => ({ - shopId: shopIdByStoreName.get(store.storeName.trim().toLowerCase()) ?? '', + shopId: mapping.get(normalizedShopName(store.storeName)) ?? '', shopName: store.storeName, profitCenter: store.profitCenter, }))