生产更新:同步已提交的海外仓分析及业务修复(2026-10-16) #20

Merged
admin merged 63 commits from release/test into main 2026-10-16 18:03:19 +08:00
3 changed files with 25 additions and 7 deletions
Showing only changes of commit 6a905419fc - Show all commits
@@ -32,24 +32,41 @@ beforeEach(() => {
})
afterEach(() => { wrappers.splice(0).forEach(wrapper => wrapper.unmount()); vi.useRealTimers(); vi.restoreAllMocks() })
describe('warehouse operating dashboard', () => {
it.each(['PARTIAL', 'UNKNOWN'])('renders omitted nullable KPI fields for %s instead of rejecting the dashboard', async status => {
const response = structuredClone(dashboardFixture)
for (const metric of Object.values(response.kpis)) {
metric.value = null; metric.knownSubtotal = null; metric.unknownSkuCount = null
metric.outboundQuantity = null; metric.observedQuantity = null; metric.dataStatus = status
}
response.kpis.intervalOutboundQuantity.observedQuantity = '8'
response.kpis.physicalQuantity.knownSubtotal = '128'
// Match a server response serialized with NON_NULL, not an explicit-null fixture.
mocks.dashboard.mockResolvedValue(JSON.parse(JSON.stringify(response, (_, value) => value === null ? undefined : value)))
const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' })
expect(wrapper.text()).not.toContain('指标结构不完整')
expect(wrapper.findAll('.dashboard-kpis strong').map(node => node.text())).toEqual(['8', '—', '—', '—', '—', '—'])
expect(wrapper.text()).toContain('已知小计 128')
expect(wrapper.findAllComponents(DashboardBars)).toHaveLength(4)
})
it.each(['intervalOutboundQuantity', 'averageDailyOutboundQuantity', 'physicalQuantity', 'availableQuantity', 'physicalSkuCount', 'noOutboundSkuCount'] as const)('keeps the page usable when the response omits %s and recovers after refresh', async key => {
const invalid = structuredClone(dashboardFixture)
delete (invalid.kpis as Partial<typeof invalid.kpis>)[key]
mocks.dashboard.mockResolvedValue(invalid)
const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' })
expect(wrapper.text()).toContain('经营总览指标返回不完整')
expect(wrapper.text()).toContain('经营总览返回的指标结构不完整')
expect(wrapper.find('.dashboard-kpis').exists()).toBe(false)
expect(wrapper.findComponent(query).exists()).toBe(true)
mocks.dashboard.mockResolvedValue(dashboardFixture)
await findButton(wrapper, '整体刷新').trigger('click'); await flushPromises()
expect(wrapper.findAll('.dashboard-kpis section')).toHaveLength(6)
expect(wrapper.text()).not.toContain('经营总览指标返回不完整')
expect(wrapper.text()).not.toContain('经营总览返回的指标结构不完整')
})
it('preserves the last valid result when a refreshed response has a null KPI', async () => {
const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' })
mocks.dashboard.mockResolvedValue({ ...dashboardFixture, kpis: { ...dashboardFixture.kpis, averageDailyOutboundQuantity: null } })
await findButton(wrapper, '整体刷新').trigger('click'); await flushPromises()
expect(wrapper.text()).toContain('经营总览指标返回不完整,请更新服务后重新查询;展示上次查询')
expect(wrapper.text()).toContain('经营总览返回的指标结构不完整,请重试或联系管理员;展示上次查询')
expect(wrapper.findAll('.dashboard-kpis section')).toHaveLength(6)
})
@@ -100,9 +100,10 @@ async function load(refresh = false) {
if (!request.current()) return
if (!result?.context || metricNames.some(({ key }) => {
const metric = result?.kpis?.[key]
return metric == null || typeof metric !== 'object' || !('value' in metric)
// Unknown quantities may be omitted by the server's NON_NULL serialization.
return metric == null || typeof metric !== 'object' || !['COMPLETE', 'PARTIAL', 'UNKNOWN'].includes(metric.dataStatus)
})) {
error.value = `经营总览指标返回不完整,请更新服务后重新查询${data.value ? ';展示上次查询' : ''}`
error.value = `经营总览返回的指标结构不完整,请重试或联系管理员${data.value ? ';展示上次查询' : ''}`
return
}
if (pinned && !sameContext(pinned, result.context)) { error.value = '查询版本不一致,请整体刷新经营总览'; return }
@@ -183,7 +184,7 @@ onUnmounted(() => { dataFence.cancel(); trendFence.cancel(); poller.stop(); clea
<p v-if="!permitted" role="alert">没有自营经营数据查看权限。</p>
<template v-else>
<DataTable appearance="integrated" :data="[]" :columns="[]" :results-visible="false" column-settings-mode="hide">
<template #toolbar-start><div class="dashboard-title"><h2>经营总览</h2><span class="dashboard-snapshot">{{ runtime?.warehouses.find(item => item.warehouseCode === warehouseCode)?.warehouseName || warehouseCode }}<i />{{ loading ? '正在更新' : data ? '固定版本 · 北京时间' : '正在准备数据' }}</span></div></template>
<template #toolbar-start><div class="dashboard-title"><h2>经营总览</h2><span class="dashboard-snapshot">{{ runtime?.warehouses.find(item => item.warehouseCode === warehouseCode)?.warehouseName || warehouseCode }}<i />{{ loading ? '正在更新' : data ? '固定版本 · 北京时间' : error ? '查询未完成' : '正在准备数据' }}</span></div></template>
<template #toolbar-actions><div class="density-actions"><el-button :disabled="!data || contextExpired" @click="drill('sales', { analysis: 'SKU', rangePreset: appliedRange.rangePreset, ...(appliedRange.startDate ? { startDate: appliedRange.startDate, endDate: appliedRange.endDate! } : {}) })">查看销量</el-button><el-button :disabled="!warehouseCode" @click="router.push({ path: '/self-operated/inventory', query: { warehouseCode } })">{{ stockExpired ? '普通库存诊断' : '查看库存' }}</el-button><el-button :disabled="!warehouseCode" @click="router.push({ path: '/self-operated/collection', query: { warehouseCode } })">数据采集</el-button><el-button :disabled="!warehouseCode || loading || Boolean(routeError)" @click="load(true)">整体刷新</el-button></div></template>
<template #toolbar-end>
<el-popover placement="bottom-end" trigger="click" :width="460" title="统计口径与采集时间">
+1 -1
View File
@@ -32,7 +32,7 @@ export interface QueryContext {
}
export interface Scope { sourceAccount: string; warehouseCode: string }
export type DashboardQuery = Omit<BusinessQuery, 'current' | 'size' | 'sku' | 'customerCode'>
export interface DashboardKpi { value: Quantity; knownSubtotal: Quantity; unit: string; dataStatus: string; unknownSkuCount: Quantity; reasonCodes: string[]; outboundQuantity: Quantity; observedQuantity: Quantity }
export interface DashboardKpi { value?: Quantity; knownSubtotal?: Quantity; unit: string; dataStatus: string; unknownSkuCount?: Quantity; reasonCodes: string[]; outboundQuantity?: Quantity; observedQuantity?: Quantity }
export interface DashboardItem { customerCode: string; sku: string; productName: string | null; value: Quantity; unit: string; windowOutboundQuantity: Quantity; skuOutboundOrderCount: Quantity; physicalQuantity: Quantity; availableQuantity: Quantity; frozenQuantity: Quantity; defectivePhysicalQuantity: Quantity; averageDailyOutboundQuantity: Quantity; inventoryCoverageDays: Quantity }
export interface DashboardRanking { windowStartDate: string; windowEndDateExclusive: string; calculationWindowDays: number; eligibleCount: Quantity; unassessableCount: Quantity; items: DashboardItem[]; dataStatus: string; reasonCodes: string[] }
export interface DashboardBucket { bucketCode: CoverageBucket; skuCount: Quantity; knownPhysicalQuantity: Quantity; unknownPhysicalSkuCount: Quantity }