fix(clearance): 补齐历史店铺编号并显示清货价说明

This commit is contained in:
居居 2026-11-05 16:32:50 +08:00 committed by WuYongJie
parent c875549d2f
commit d4d9e0ba08
5 changed files with 114 additions and 43 deletions

View File

@ -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(<ClearancePricePushView canGenerate />)
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 () => {

View File

@ -350,14 +350,8 @@ export function ClearancePricePushView({
<th>成本价</th>
<th>开始日期</th>
<th className={pricePushStyles.labelHeader}>
<span>历史成本价</span>
<span
className={pricePushStyles.headerNote}
aria-label="即清货价"
title="即清货价"
>
注
</span>
<span className={pricePushStyles.headerTitle}>历史成本价</span>
<span className={pricePushStyles.headerDefinition}>即清货价</span>
</th>
<th>店铺编码</th>
<th>店铺名称</th>

View File

@ -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) {

View File

@ -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: '抖音-索菲娅精选家具专卖店',
}],
})
})
})

View File

@ -31,12 +31,64 @@ export type ClearancePricePushFilters = {
historicalCostPriceMax?: number
}
const SHOP_ID_MAPPING_TTL_MS = 60_000
let shopIdMappingCache: {
expiresAt: number
value: ReadonlyMap<string, string>
} | 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<string, string>()
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<string, string>()
}
}
async function resolveShopIds<T extends { shopId: string; shopName: string }>(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<ReturnType<typeof jstSkumapDistributionRepository.listShopInfos>> = []
try {
shopInfos = await jstSkumapDistributionRepository.listShopInfos()
} catch (error) {
console.warn('[clearance-price-push] jst_shopInfo mapping failed', error)
}
const shopIdByStoreName = new Map<string, string>()
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,
}))