From b5a1a514035300b68300a72e253c6b180ddd9e2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Sat, 5 Sep 2026 17:24:34 +0800 Subject: [PATCH] =?UTF-8?q?task-116(=E4=BB=BB=E5=8A=A1=E4=B8=8E=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E5=88=86=E6=9E=90=E4=B8=AD=E5=BF=83):=20=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E9=87=8D=E5=A4=8D=E6=A3=80=E6=9F=A5=E5=88=86=E5=B8=83?= =?UTF-8?q?=E8=A7=86=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 duplicate-distribution.ts:按国家/站点聚合撞款分布与撞款 ASIN top 店铺。 TDD: task-116.test.ts 8 用例先 RED 后 GREEN。 --- .../src/pages/tasks/duplicate-distribution.ts | 46 +++++++++++++ admin-frontend-vue/tests/task-116.test.ts | 69 +++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 admin-frontend-vue/src/pages/tasks/duplicate-distribution.ts create mode 100644 admin-frontend-vue/tests/task-116.test.ts diff --git a/admin-frontend-vue/src/pages/tasks/duplicate-distribution.ts b/admin-frontend-vue/src/pages/tasks/duplicate-distribution.ts new file mode 100644 index 00000000..187ded84 --- /dev/null +++ b/admin-frontend-vue/src/pages/tasks/duplicate-distribution.ts @@ -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() + 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)) +} diff --git a/admin-frontend-vue/tests/task-116.test.ts b/admin-frontend-vue/tests/task-116.test.ts new file mode 100644 index 00000000..91566183 --- /dev/null +++ b/admin-frontend-vue/tests/task-116.test.ts @@ -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/) +})