From 42722d0cc6acf59b9072c43fc67bbe9fe8998c67 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:42:34 +0800 Subject: [PATCH] =?UTF-8?q?task-88(=E5=BA=97=E9=93=BA=E4=B8=AD=E5=BF=83):?= =?UTF-8?q?=20=E5=AE=9E=E7=8E=B0=E5=BA=97=E9=93=BA=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 shop-manage-model.ts(ShopManagePageVo camel 解析,仅保留密码掩码) 与 shop-manage-api.ts(GET /api/admin/shop-manages 加载适配)。 TDD: task-88.test.ts 8 用例先 RED 后 GREEN。 --- .../src/pages/shop/shop-manage-api.ts | 17 +++ .../src/pages/shop/shop-manage-model.ts | 53 +++++++++ admin-frontend-vue/tests/task-88.test.ts | 112 ++++++++++++++++++ 3 files changed, 182 insertions(+) create mode 100644 admin-frontend-vue/src/pages/shop/shop-manage-api.ts create mode 100644 admin-frontend-vue/src/pages/shop/shop-manage-model.ts create mode 100644 admin-frontend-vue/tests/task-88.test.ts diff --git a/admin-frontend-vue/src/pages/shop/shop-manage-api.ts b/admin-frontend-vue/src/pages/shop/shop-manage-api.ts new file mode 100644 index 00000000..ed9101bc --- /dev/null +++ b/admin-frontend-vue/src/pages/shop/shop-manage-api.ts @@ -0,0 +1,17 @@ +/** 店铺列表加载适配(任务 88):GET /api/admin/shop-manages + 分页/筛选归一与解析。 */ +import { http } from '@/api/http' +import { parseShopManagePage } from './shop-manage-model.ts' +import { + normalizeShopListParams, + toShopManagePageQuery, + type ShopManageListParams, + type ShopPageResult, +} from './shop-dto.ts' + +export const SHOP_MANAGE_ENDPOINT = '/api/admin/shop-manages' + +export async function fetchShopManageList(params: Partial = {}): Promise { + const normalized = normalizeShopListParams(params) + const { data } = await http.get(SHOP_MANAGE_ENDPOINT, { params: toShopManagePageQuery(normalized) }) + return parseShopManagePage(data) +} diff --git a/admin-frontend-vue/src/pages/shop/shop-manage-model.ts b/admin-frontend-vue/src/pages/shop/shop-manage-model.ts new file mode 100644 index 00000000..23f0e175 --- /dev/null +++ b/admin-frontend-vue/src/pages/shop/shop-manage-model.ts @@ -0,0 +1,53 @@ +/** 店铺列表加载模型(任务 88):解析 ShopManagePageVo(camel) 店铺行,纯逻辑。 */ +import { unwrap } from '../../api/envelope.ts' +import { emptyShopPageResult, type ShopPageResult, type ShopSummary } from './shop-dto.ts' + +function text(value: unknown): string { + return typeof value === 'string' ? value.trim() : '' +} + +function numberOrNull(value: unknown): number | null { + return typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : null +} + +/** 解析单条店铺行;缺 id 视为无效;密码只保留掩码、不落明文字段。 */ +export function toShopSummary(raw: unknown): ShopSummary | null { + if (!raw || typeof raw !== 'object') return null + const record = raw as Record + const id = numberOrNull(record.id) + if (id === null) return null + const item: ShopSummary = { + id, + groupId: numberOrNull(record.groupId ?? record.group_id), + groupName: text(record.groupName ?? record.group_name), + shopName: text(record.shopName ?? record.shop_name), + mallName: text(record.mallName ?? record.mall_name), + znUsername: text(record.znUsername ?? record.zn_username), + account: text(record.account), + } + const masked = text(record.passwordMasked ?? record.password_masked) || text(record.password) + if (masked) item.passwordMasked = masked + const createdAt = text(record.createdAt ?? record.created_at) + if (createdAt) item.createdAt = createdAt + const updatedAt = text(record.updatedAt ?? record.updated_at) + if (updatedAt) item.updatedAt = updatedAt + return item +} + +/** 归一化店铺分页负载(信封或已解包 VO)为前端结果;缺省字段回默认。 */ +export function parseShopManagePage(payload: unknown): ShopPageResult { + const out = emptyShopPageResult() + const core = unwrap(payload) + if (!core || typeof core !== 'object') return out + const record = core as Record + if (Array.isArray(record.items)) { + out.items = record.items + .map((raw) => toShopSummary(raw)) + .filter((item): item is ShopSummary => item !== null) + } + if (typeof record.total === 'number') out.total = Math.floor(record.total) + if (typeof record.page === 'number' && record.page >= 1) out.page = Math.floor(record.page) + const rawSize = record.pageSize ?? record.page_size + if (typeof rawSize === 'number' && rawSize >= 1) out.pageSize = Math.floor(rawSize) + return out +} diff --git a/admin-frontend-vue/tests/task-88.test.ts b/admin-frontend-vue/tests/task-88.test.ts new file mode 100644 index 00000000..3cc107a4 --- /dev/null +++ b/admin-frontend-vue/tests/task-88.test.ts @@ -0,0 +1,112 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { parseShopManagePage, toShopSummary } from '../src/pages/shop/shop-manage-model.ts' + +test('test_task_088_shop_manage_list_load_normal_primary_path', () => { + // 正常主路径:Java ShopManagePageVo(camel) 封包解析为前端店铺分页结果。 + const page = parseShopManagePage({ + success: true, + data: { + items: [ + { + id: 5, + groupId: 3, + groupName: '华东组', + shopName: 'BlueWave', + mallName: '亚马逊美国站', + znUsername: 'robot', + account: 'sale@blue.com', + password: '******', + passwordMasked: '******', + createdAt: '2026-01-01T10:00:00', + updatedAt: '2026-01-02T11:00:00', + }, + ], + total: 1, + page: 1, + pageSize: 15, + }, + }) + assert.equal(page.items.length, 1) + const item = page.items[0] + assert.equal(item.id, 5) + assert.equal(item.groupId, 3) + assert.equal(item.groupName, '华东组') + assert.equal(item.shopName, 'BlueWave') + assert.equal(item.mallName, '亚马逊美国站') + assert.equal(item.znUsername, 'robot') + assert.equal(item.account, 'sale@blue.com') + assert.equal(item.passwordMasked, '******') + assert.equal('password' in item, false, '列表行不得携带明文字段') + assert.equal(page.total, 1) + assert.equal(page.pageSize, 15) +}) + +test('test_task_088_shop_manage_list_load_normal_variant_input', () => { + // 正常变体:已解包 VO 直传;无分组/缺掩码字段行留空可显示。 + const page = parseShopManagePage({ + items: [{ id: 6, shopName: 'RedSun', mallName: '亚马逊英国站' }], + total: 3, + page: 2, + pageSize: 20, + }) + const item = page.items[0] + assert.equal(item.groupId, null) + assert.equal(item.groupName, '') + assert.equal(item.znUsername, '') + assert.equal(item.account, '') + assert.equal(item.passwordMasked, undefined) + assert.equal(page.page, 2) +}) + +test('test_task_088_shop_manage_list_load_repeated_is_idempotent', () => { + // 正常重复:解析同一负载结果稳定、不改输入。 + const payload = { data: { items: [{ id: 1, shopName: 'A' }], total: 1, page: 1, pageSize: 15 } } + assert.deepEqual(parseShopManagePage(payload), parseShopManagePage(payload)) + assert.equal((payload as { data: { items: unknown[] } }).data.items.length, 1) +}) + +test('test_task_088_shop_manage_list_load_boundary_empty_input', () => { + // 边界空值:空负载回默认分页结果(空列表/total 0/page 1/页大小 15)。 + const page = parseShopManagePage({}) + assert.deepEqual(page.items, []) + assert.equal(page.total, 0) + assert.equal(page.page, 1) + assert.equal(page.pageSize, 15) +}) + +test('test_task_088_shop_manage_list_load_boundary_single_item', () => { + // 边界单元素:单条记录;缺 id 的行被过滤。 + const page = parseShopManagePage({ + data: { items: [{ id: 7, shopName: 'Z' }, { shopName: 'no-id' }], total: 2, page: 1, pageSize: 15 }, + }) + assert.equal(page.items.length, 1) + assert.equal(page.items[0].id, 7) +}) + +test('test_task_088_shop_manage_list_load_boundary_limit_or_missing_field', () => { + // 边界上限/缺字段:缺省字段回默认值;最小行可用。 + const minimal = toShopSummary({ id: 3 }) + assert.ok(minimal) + assert.equal(minimal.id, 3) + assert.equal(minimal.shopName, '') + assert.equal(minimal.createdAt, undefined) +}) + +test('test_task_088_shop_manage_list_load_invalid_input_rejected', () => { + // 异常输入:success=false 抛后端 message;非对象/非数字 id 行不入列表。 + assert.throws(() => parseShopManagePage({ success: false, message: '无权访问店铺数据' }), /无权访问/) + assert.equal(toShopSummary('garbage'), null) + assert.equal(toShopSummary({ shopName: 'no id' }), null) +}) + +test('test_task_088_shop_manage_list_load_dependency_failure_returns_actionable_message', () => { + // 依赖失败/加载走 adapter:GET /api/admin/shop-manages + http.get + 解析,页面不内联。 + const api = readSource('src/pages/shop/shop-manage-api.ts') + assert.match(api, /\/api\/admin\/shop-manages/) + assert.match(api, /http\.get/) + assert.match(api, /parseShopManagePage/) + const model = readSource('src/pages/shop/shop-manage-model.ts') + assert.equal(/axios|http\./.test(model), false, '店铺列表解析保持纯逻辑') +})