diff --git a/admin-frontend-vue/src/pages/shop/shop-group-model.ts b/admin-frontend-vue/src/pages/shop/shop-group-model.ts new file mode 100644 index 00000000..893904b1 --- /dev/null +++ b/admin-frontend-vue/src/pages/shop/shop-group-model.ts @@ -0,0 +1,34 @@ +/** 店铺分组选项解析模型(任务 90):解析 ShopManageGroupItemVo 数组为下拉选项,纯逻辑。 */ +import { unwrap } from '../../api/envelope.ts' +import type { ShopGroupOption } 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 toShopGroupOption(raw: unknown): ShopGroupOption | null { + if (!raw || typeof raw !== 'object') return null + const record = raw as Record + const id = numberOrNull(record.id) + if (id === null) return null + const option: ShopGroupOption = { id, groupName: text(record.groupName ?? record.group_name) } + const memberCount = numberOrNull(record.memberCount ?? record.member_count) + if (memberCount !== null) option.memberCount = memberCount + const leaderUsername = text(record.leaderUsername ?? record.leader_username) + if (leaderUsername) option.leaderUsername = leaderUsername + return option +} + +/** 归一化分组下拉负载(信封或已解包数组)为选项列表;空/无效项过滤。 */ +export function parseShopManageGroups(payload: unknown): ShopGroupOption[] { + const data = unwrap(payload) + if (!Array.isArray(data)) return [] + return data + .map((raw) => toShopGroupOption(raw)) + .filter((option): option is ShopGroupOption => option !== null) +} diff --git a/admin-frontend-vue/src/pages/shop/shop-manage-api.ts b/admin-frontend-vue/src/pages/shop/shop-manage-api.ts index ed9101bc..aebbcfa0 100644 --- a/admin-frontend-vue/src/pages/shop/shop-manage-api.ts +++ b/admin-frontend-vue/src/pages/shop/shop-manage-api.ts @@ -1,9 +1,11 @@ -/** 店铺列表加载适配(任务 88):GET /api/admin/shop-manages + 分页/筛选归一与解析。 */ +/** 店铺列表加载与分组选项适配(任务 88/90):GET /api/admin/shop-manages(/groups) + 归一与解析。 */ import { http } from '@/api/http' import { parseShopManagePage } from './shop-manage-model.ts' +import { parseShopManageGroups } from './shop-group-model.ts' import { normalizeShopListParams, toShopManagePageQuery, + type ShopGroupOption, type ShopManageListParams, type ShopPageResult, } from './shop-dto.ts' @@ -15,3 +17,11 @@ export async function fetchShopManageList(params: Partial const { data } = await http.get(SHOP_MANAGE_ENDPOINT, { params: toShopManagePageQuery(normalized) }) return parseShopManagePage(data) } + +export const SHOP_MANAGE_GROUPS_ENDPOINT = '/api/admin/shop-manages/groups' + +/** 加载店铺分组下拉选项(超级管理员取全部,普通管理员由后端限定可访问分组)。 */ +export async function fetchShopManageGroups(): Promise { + const { data } = await http.get(SHOP_MANAGE_GROUPS_ENDPOINT) + return parseShopManageGroups(data) +} diff --git a/admin-frontend-vue/tests/task-90.test.ts b/admin-frontend-vue/tests/task-90.test.ts new file mode 100644 index 00000000..1eb46cf6 --- /dev/null +++ b/admin-frontend-vue/tests/task-90.test.ts @@ -0,0 +1,72 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { parseShopManageGroups, toShopGroupOption } from '../src/pages/shop/shop-group-model.ts' + +test('test_task_090_shop_group_options_load_normal_primary_path', () => { + // 正常主路径:Java ShopManageGroupItemVo 数组封包解析为分组下拉项。 + const options = parseShopManageGroups({ + success: true, + data: [ + { id: 1, groupName: '华东组', memberCount: 3, leaderUsername: '张' }, + { id: 2, groupName: '华南组', memberCount: 5 }, + ], + }) + assert.equal(options.length, 2) + assert.equal(options[0].id, 1) + assert.equal(options[0].groupName, '华东组') + assert.equal(options[0].memberCount, 3) + assert.equal(options[0].leaderUsername, '张') + assert.equal(options[1].id, 2) +}) + +test('test_task_090_shop_group_options_load_normal_variant_input', () => { + // 正常变体:可选字段缺失时下拉仍可用。 + const option = toShopGroupOption({ id: 7, groupName: '备用组' }) + assert.ok(option) + assert.equal(option.id, 7) + assert.equal(option.groupName, '备用组') + assert.equal(option.memberCount, undefined) + assert.equal(option.leaderUsername, undefined) +}) + +test('test_task_090_shop_group_options_load_repeated_is_idempotent', () => { + // 正常重复:解析同一负载结果稳定、不改输入。 + const payload = { data: [{ id: 1, groupName: 'A', memberCount: 1 }] } + assert.deepEqual(parseShopManageGroups(payload), parseShopManageGroups(payload)) +}) + +test('test_task_090_shop_group_options_load_boundary_empty_input', () => { + // 边界空值:空数组/空负载返回空下拉选项。 + assert.deepEqual(parseShopManageGroups({ data: [] }), []) + assert.deepEqual(parseShopManageGroups({}), []) +}) + +test('test_task_090_shop_group_options_load_boundary_single_item', () => { + // 边界单元素:单分组可下拉。 + const options = parseShopManageGroups([{ id: 4, groupName: '单组' }]) + assert.equal(options.length, 1) + assert.equal(options[0].groupName, '单组') +}) + +test('test_task_090_shop_group_options_load_boundary_limit_or_missing_field', () => { + // 边界上限/缺字段:成员数为非数字按缺省处理。 + const option = toShopGroupOption({ id: 2, groupName: '组', memberCount: 'x' }) + assert.equal(option!.memberCount, undefined) +}) + +test('test_task_090_shop_group_options_load_invalid_input_rejected', () => { + // 异常输入:缺 id 的分组不进入下拉;success=false 抛后端 message。 + assert.equal(toShopGroupOption('garbage'), null) + assert.equal(toShopGroupOption({ groupName: 'no id' }), null) + assert.throws(() => parseShopManageGroups({ success: false, message: '无权访问分组' }), /无权访问/) +}) + +test('test_task_090_shop_group_options_load_dependency_failure_returns_actionable_message', () => { + // 依赖失败/加载走 adapter:GET /api/admin/shop-manages/groups + http.get + 解析。 + const api = readSource('src/pages/shop/shop-manage-api.ts') + assert.match(api, /\/api\/admin\/shop-manages\/groups/) + assert.match(api, /fetchShopManageGroups/) + const model = readSource('src/pages/shop/shop-group-model.ts') + assert.equal(/axios|http\./.test(model), false, '分组解析保持纯逻辑') +})