fix(clearance): 补齐历史店铺编号并显示清货价说明
This commit is contained in:
parent
c875549d2f
commit
d4d9e0ba08
@ -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 () => {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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: '抖音-索菲娅精选家具专卖店',
|
||||
}],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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,
|
||||
}))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user