task-116(任务与重复分析中心): 实现重复检查分布视图
新增 duplicate-distribution.ts:按国家/站点聚合撞款分布与撞款 ASIN top 店铺。 TDD: task-116.test.ts 8 用例先 RED 后 GREEN。
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
/** 重复检查分布视图(任务 116):按国家/站点聚合撞款与 top 店铺分布;纯逻辑。 */
|
||||
import type { DuplicateShop } from './duplicate-model.ts'
|
||||
|
||||
export interface CountryDistributionRow {
|
||||
country: string
|
||||
asinCount: number
|
||||
shopCount: number
|
||||
}
|
||||
|
||||
/** 按国家聚合分布(每店铺 country_codes 全量计入)。 */
|
||||
export function countryDistribution(shops: readonly DuplicateShop[]): CountryDistributionRow[] {
|
||||
const byCountry = new Map<string, { asinCount: number; shopCount: number }>()
|
||||
for (const item of shops) {
|
||||
for (const code of item.countryCodes) {
|
||||
const country = code.trim().toUpperCase()
|
||||
if (!country) continue
|
||||
const entry = byCountry.get(country) ?? { asinCount: 0, shopCount: 0 }
|
||||
entry.asinCount += item.asinCount
|
||||
entry.shopCount += 1
|
||||
byCountry.set(country, entry)
|
||||
}
|
||||
}
|
||||
return [...byCountry.entries()]
|
||||
.map(([country, entry]) => ({ country, asinCount: entry.asinCount, shopCount: entry.shopCount }))
|
||||
.sort((a, b) => b.asinCount - a.asinCount || a.country.localeCompare(b.country))
|
||||
}
|
||||
|
||||
export interface SiteDistributionRow {
|
||||
site: string
|
||||
asinCount: number
|
||||
shopCount: number
|
||||
}
|
||||
|
||||
/** 按站点(site)聚合:以单国家维度汇总撞款,站点取国家码。 */
|
||||
export function duplicateSiteDistribution(shops: readonly DuplicateShop[]): SiteDistributionRow[] {
|
||||
const rows = countryDistribution(shops)
|
||||
return rows.map((row) => ({ site: row.country, asinCount: row.asinCount, shopCount: row.shopCount }))
|
||||
}
|
||||
|
||||
/** 按撞款 ASIN 数取 top 店铺(降序),limit<=0 返回空。 */
|
||||
export function topShopsByAsin(shops: readonly DuplicateShop[], limit: number): DuplicateShop[] {
|
||||
if (!(limit > 0)) return []
|
||||
return [...shops]
|
||||
.sort((a, b) => b.asinCount - a.asinCount || a.shopName.localeCompare(b.shopName))
|
||||
.slice(0, Math.floor(limit))
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readSource } from './helpers.ts'
|
||||
import {
|
||||
countryDistribution,
|
||||
duplicateSiteDistribution,
|
||||
topShopsByAsin,
|
||||
} from '../src/pages/tasks/duplicate-distribution.ts'
|
||||
import type { DuplicateShop } from '../src/pages/tasks/duplicate-model.ts'
|
||||
|
||||
function shop(shopName: string, countryCodes: string[], asinCount: number): DuplicateShop {
|
||||
return { shopName, groupName: '', countryCodes, asinCount, recordCount: 0 }
|
||||
}
|
||||
|
||||
test('test_task_116_duplicate_distribution_normal_primary_path', () => {
|
||||
// 正常主路径:按国家/站点聚合撞款分布(合计 asin 与店铺数)。
|
||||
const shops = [shop('A', ['DE', 'UK'], 10), shop('B', ['DE'], 5), shop('C', ['FR'], 3)]
|
||||
const dist = countryDistribution(shops)
|
||||
const de = dist.find((d) => d.country === 'DE')
|
||||
assert.equal(de!.asinCount, 15)
|
||||
assert.equal(de!.shopCount, 2)
|
||||
assert.equal(dist.length, 3)
|
||||
})
|
||||
|
||||
test('test_task_116_duplicate_distribution_normal_variant_input', () => {
|
||||
// 正常变体:site 取单国家码首字段。
|
||||
const dist = duplicateSiteDistribution([shop('A', ['DE'], 4)])
|
||||
assert.equal(dist.length, 1)
|
||||
assert.equal(dist[0].site, 'DE')
|
||||
assert.equal(dist[0].asinCount, 4)
|
||||
})
|
||||
|
||||
test('test_task_116_duplicate_distribution_repeated_is_idempotent', () => {
|
||||
// 正常重复:聚合稳定、不改输入。
|
||||
const shops = [shop('A', ['DE'], 3)]
|
||||
assert.deepEqual(countryDistribution(shops), countryDistribution(shops))
|
||||
assert.equal(shops[0].asinCount, 3)
|
||||
})
|
||||
|
||||
test('test_task_116_duplicate_distribution_boundary_empty_input', () => {
|
||||
// 边界空值:空店铺列表无分布。
|
||||
assert.deepEqual(countryDistribution([]), [])
|
||||
assert.deepEqual(topShopsByAsin([], 5), [])
|
||||
})
|
||||
|
||||
test('test_task_116_duplicate_distribution_boundary_single_item', () => {
|
||||
// 边界单元素:单店单国家。
|
||||
const dist = countryDistribution([shop('A', ['UK'], 2)])
|
||||
assert.equal(dist.length, 1)
|
||||
assert.equal(dist[0].shopCount, 1)
|
||||
})
|
||||
|
||||
test('test_task_116_duplicate_distribution_boundary_limit_or_missing_field', () => {
|
||||
// 边界上限/缺字段:无国家的店铺不计入国家分布。
|
||||
const dist = countryDistribution([shop('A', [], 7)])
|
||||
assert.deepEqual(dist, [])
|
||||
})
|
||||
|
||||
test('test_task_116_duplicate_distribution_invalid_input_rejected', () => {
|
||||
// 异常输入:top 负数回空或 0 条。
|
||||
assert.deepEqual(topShopsByAsin([shop('A', ['DE'], 9)], 0), [])
|
||||
})
|
||||
|
||||
test('test_task_116_duplicate_distribution_dependency_failure_returns_actionable_message', () => {
|
||||
// 依赖失败/可操作:分布计算纯逻辑、无框架/http。
|
||||
const mod = readSource('src/pages/tasks/duplicate-distribution.ts')
|
||||
assert.equal(/axios|http\.|vue/.test(mod), false, '分布计算保持纯逻辑')
|
||||
assert.match(mod, /topShopsByAsin/)
|
||||
})
|
||||
Reference in New Issue
Block a user