From ad6658e490c99389bd0bc249fc46fbea77d92cf9 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 16:09:31 +0800 Subject: [PATCH] =?UTF-8?q?task-74(ASIN=20=E6=95=B0=E6=8D=AE=E4=B8=AD?= =?UTF-8?q?=E5=BF=83):=20=E5=AE=9E=E7=8E=B0=E6=9F=A5=E8=AF=A2=20ASIN=20?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E6=8A=BD=E5=B1=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 抽屉副标题/5 国家草稿与变更差异(空值→删除)纯逻辑, 保存走 PUT/DELETE /api/admin/query-asins/{id}/countries/{country}, 9 个契约测试。 --- .../src/pages/asin/query-asin-detail-api.ts | 18 +++ .../src/pages/asin/query-asin-detail-model.ts | 76 +++++++++++++ admin-frontend-vue/tests/task-74.test.ts | 106 ++++++++++++++++++ 3 files changed, 200 insertions(+) create mode 100644 admin-frontend-vue/src/pages/asin/query-asin-detail-api.ts create mode 100644 admin-frontend-vue/src/pages/asin/query-asin-detail-model.ts create mode 100644 admin-frontend-vue/tests/task-74.test.ts diff --git a/admin-frontend-vue/src/pages/asin/query-asin-detail-api.ts b/admin-frontend-vue/src/pages/asin/query-asin-detail-api.ts new file mode 100644 index 00000000..62c7bbc9 --- /dev/null +++ b/admin-frontend-vue/src/pages/asin/query-asin-detail-api.ts @@ -0,0 +1,18 @@ +/** 查询 ASIN 详情抽屉保存适配(任务 74):PUT/DELETE /api/admin/query-asins/{id}/countries/{country}。 */ +import { http } from '@/api/http' + +export const QUERY_ASIN_DETAIL_ENDPOINT = '/api/admin/query-asins' + +function countryUrl(id: number, country: string): string { + return `${QUERY_ASIN_DETAIL_ENDPOINT}/${id}/countries/${country}` +} + +/** 保存某国家 ASIN(body { asin });返回后由列表刷新回读最新行。 */ +export async function updateQueryAsinCountryAsin(id: number, country: string, asin: string): Promise { + await http.put(countryUrl(id, country), { asin }) +} + +/** 删除某国家 ASIN(清空该站点 ASIN)。 */ +export async function deleteQueryAsinCountryAsin(id: number, country: string): Promise { + await http.delete(countryUrl(id, country)) +} diff --git a/admin-frontend-vue/src/pages/asin/query-asin-detail-model.ts b/admin-frontend-vue/src/pages/asin/query-asin-detail-model.ts new file mode 100644 index 00000000..ae3484f0 --- /dev/null +++ b/admin-frontend-vue/src/pages/asin/query-asin-detail-model.ts @@ -0,0 +1,76 @@ +/** 查询 ASIN 详情抽屉模型(任务 74):副标题、5 国家草稿与变更差异(空→删除),纯逻辑。 */ +import { QUERY_ASIN_COUNTRIES, asinForCountry, type QueryAsinCountry, type QueryAsinItem } from './query-asin-model.ts' + +export const QUERY_ASIN_COUNTRY_LABELS: Record = { + DE: '德国', + UK: '英国', + FR: '法国', + IT: '意大利', + ES: '西班牙', +} + +/** 国家码 → 中文标签;不属白名单原样返回。 */ +export function queryAsinCountryLabel(country: string): string { + return QUERY_ASIN_COUNTRY_LABELS[country as QueryAsinCountry] || country +} + +/** 抽屉副标题:`分组 / 店铺名`;无分组时仅店铺名。 */ +export function queryAsinDetailSubtitle(item: Pick): string { + const group = (item.groupName || '').trim() + const shop = (item.shopName || '').trim() + return group ? `${group} / ${shop}` : shop +} + +/** 抽屉单国家输入草稿(编辑前的初值)。 */ +export interface QueryAsinCountryDraft { + code: string + value: string +} + +/** 由列表行生成 5 国家草稿(当前值 trimmed,保持支持国家序)。 */ +export function queryAsinCountryDraftsOf(item: QueryAsinItem): QueryAsinCountryDraft[] { + return QUERY_ASIN_COUNTRIES.map((code) => ({ + code, + value: asinForCountry(item, code).trim(), + })) +} + +export type QueryAsinCountryChangeKind = 'update' | 'delete' + +export interface QueryAsinCountryChange { + code: string + label: string + kind: QueryAsinCountryChangeKind + asin?: string +} + +/** + * 与当前行比较草稿产出变更: + * - 归一(trim + 大写)后与当前相同 → 跳过; + * - 草稿留空(当前有值) → delete; + * - 新值非空 → update。 + * 未知国家码或缺失草稿不产生变更,避免误删;结果按支持国家序稳定输出。 + */ +export function diffQueryAsinCountryChanges( + item: QueryAsinItem, + drafts: QueryAsinCountryDraft[], +): QueryAsinCountryChange[] { + const byCode = new Map() + for (const draft of drafts || []) { + if (!draft || typeof draft.code !== 'string') continue + byCode.set(draft.code.trim().toUpperCase(), typeof draft.value === 'string' ? draft.value : '') + } + const changes: QueryAsinCountryChange[] = [] + for (const code of QUERY_ASIN_COUNTRIES) { + if (!byCode.has(code)) continue + const next = (byCode.get(code) || '').trim().toUpperCase() + const current = asinForCountry(item, code).trim().toUpperCase() + if (next === current) continue + if (!next) { + changes.push({ code, label: queryAsinCountryLabel(code), kind: 'delete' }) + } else { + changes.push({ code, label: queryAsinCountryLabel(code), kind: 'update', asin: next }) + } + } + return changes +} diff --git a/admin-frontend-vue/tests/task-74.test.ts b/admin-frontend-vue/tests/task-74.test.ts new file mode 100644 index 00000000..24382b11 --- /dev/null +++ b/admin-frontend-vue/tests/task-74.test.ts @@ -0,0 +1,106 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { + QUERY_ASIN_COUNTRY_LABELS, + queryAsinCountryLabel, + queryAsinDetailSubtitle, + queryAsinCountryDraftsOf, + diffQueryAsinCountryChanges, + type QueryAsinCountryChange, +} from '../src/pages/asin/query-asin-detail-model.ts' +import { toQueryAsinItem } from '../src/pages/asin/query-asin-model.ts' + +test('test_task_074_query_asin_detail_subtitle_normal_primary_path', () => { + // 正常主路径:抽屉副标题 = 分组 / 店铺名。 + const item = toQueryAsinItem({ id: 1, groupName: '华东组', shopName: 'BlueWave' })! + assert.equal(queryAsinDetailSubtitle(item), '华东组 / BlueWave') +}) + +test('test_task_074_query_asin_detail_subtitle_normal_variant_input', () => { + // 正常变体:无分组时仅店铺名;5 国家标签齐全。 + const item = toQueryAsinItem({ id: 2, shopName: 'RedSun' })! + assert.equal(queryAsinDetailSubtitle(item), 'RedSun') + assert.deepEqual(QUERY_ASIN_COUNTRY_LABELS, { + DE: '德国', UK: '英国', FR: '法国', IT: '意大利', ES: '西班牙', + }) + assert.equal(queryAsinCountryLabel('DE'), '德国') + assert.equal(queryAsinCountryLabel('FR'), '法国') +}) + +test('test_task_074_query_asin_detail_drafts_normal_primary', () => { + // 正常主路径:由行生成 5 国家草稿(trimmed 当前值)。 + const item = toQueryAsinItem({ id: 3, shopName: 'S', asinDe: ' B0DE1 ', asinEs: 'B0ES1' })! + const drafts = queryAsinCountryDraftsOf(item) + assert.equal(drafts.length, 5) + assert.equal(drafts[0].code, 'DE') + assert.equal(drafts[0].value, 'B0DE1') + assert.equal(drafts[4].value, 'B0ES1') + assert.equal(drafts[1].value, '') +}) + +test('test_task_074_query_asin_detail_diff_normal_no_change', () => { + // 正常重复:值未变(大小写/空白归一后相等) → 无任何变更。 + const item = toQueryAsinItem({ id: 4, shopName: 'S', asinDe: 'B0DE1' })! + const changes = diffQueryAsinCountryChanges(item, [ + { code: 'DE', value: ' b0de1 ' }, + { code: 'UK', value: '' }, + ]) + assert.deepEqual(changes, []) +}) + +test('test_task_074_query_asin_detail_diff_boundary_empty_means_delete', () => { + // 边界空值:当前有值但草稿留空 → 删除该国家。 + const item = toQueryAsinItem({ id: 5, shopName: 'S', asinUk: 'B0UK1', asinDe: 'B0DE1' })! + const changes = diffQueryAsinCountryChanges(item, [ + { code: 'UK', value: '' }, + { code: 'DE', value: 'B0DE1' }, + ]) + assert.equal(changes.length, 1) + assert.equal(changes[0].kind, 'delete') + assert.equal(changes[0].code, 'UK') +}) + +test('test_task_074_query_asin_detail_diff_normal_update', () => { + // 正常主路径:值不同且非空 → update,ASIN 大写归一。 + const item = toQueryAsinItem({ id: 6, shopName: 'S', asinFr: 'B0OLD' })! + const changes = diffQueryAsinCountryChanges(item, [{ code: 'FR', value: ' b0new1 ' }]) + assert.equal(changes.length, 1) + assert.equal(changes[0].kind, 'update') + assert.equal(changes[0].asin, 'B0NEW1') +}) + +test('test_task_074_query_asin_detail_diff_invalid_input_rejected', () => { + // 异常输入:草稿含未知国家码被忽略;缺失国家草稿不产生变更(不会误删)。 + const item = toQueryAsinItem({ id: 7, shopName: 'S', asinDe: 'B0DE1' })! + const changes = diffQueryAsinCountryChanges(item, [ + { code: 'XX', value: 'abc' }, + { code: 'DE', value: 'B0DE1' }, + ]) + assert.deepEqual(changes, []) +}) + +test('test_task_074_query_asin_detail_diff_boundary_multi', () => { + // 边界/多变更:同批 update + delete 混合且顺序稳定(按支持国家序)。 + const item = toQueryAsinItem({ id: 8, shopName: 'S', asinDe: 'D', asinIt: 'I', asinEs: 'E' })! + const changes: QueryAsinCountryChange[] = diffQueryAsinCountryChanges(item, [ + { code: 'IT', value: '' }, + { code: 'DE', value: 'NEWD' }, + { code: 'ES', value: 'E' }, + ]) + assert.equal(changes.length, 2) + assert.deepEqual(changes.map((c) => `${c.kind}:${c.code}`), ['update:DE', 'delete:IT']) +}) + +test('test_task_074_query_asin_detail_dependency_failure_returns_actionable_message', () => { + // 依赖失败/保存走 adapter:PUT/DELETE /query-asins/{id}/countries/{country},页面不内联。 + const api = readSource('src/pages/asin/query-asin-detail-api.ts') + assert.match(api, /query-asins/) + assert.match(api, /countries/) + assert.match(api, /http\.put/) + assert.match(api, /http\.delete/) + assert.match(api, /updateQueryAsinCountryAsin|saveQueryAsinCountry/) + assert.match(api, /deleteQueryAsinCountryAsin/) + const mod = readSource('src/pages/asin/query-asin-detail-model.ts') + assert.equal(/axios|http\./.test(mod), false, '详情抽屉模型保持纯逻辑') +})